editor: Avoid scroll on select all. (#1277)

- Add `replace_text_in_range_silent` for some case to avoid show
completion, for example on cut, paster ...
- Improve to only call validation, mask on single line mode.
- Improve move cursor to scroll to visible to keep edge 3 lines.

https://github.com/user-attachments/assets/ca8bcca8-a55b-4c98-af7c-7639d1c34923

- Fix `RopeExt::offset_to_position` may crash.
- Add deffered_scroll_offset to improve paste to scroll to cursor.

https://github.com/user-attachments/assets/1af09a50-37f0-47c8-8185-4b78820d064a
This commit is contained in:
Jason Lee 2025-09-23 15:59:48 +08:00 committed by GitHub
parent 4b3731bf14
commit 993ce34bd8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 156 additions and 86 deletions

34
Cargo.lock generated
View file

@ -757,7 +757,7 @@ dependencies = [
"bitflags 2.9.1",
"cexpr",
"clang-sys",
"itertools 0.12.1",
"itertools 0.11.0",
"lazy_static",
"lazycell",
"log",
@ -780,7 +780,7 @@ dependencies = [
"bitflags 2.9.1",
"cexpr",
"clang-sys",
"itertools 0.13.0",
"itertools 0.11.0",
"log",
"prettyplease",
"proc-macro2",
@ -1449,7 +1449,7 @@ dependencies = [
[[package]]
name = "collections"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed.git#fbe06238e45c2fec6955c70b86eb26fd6a864266"
source = "git+https://github.com/zed-industries/zed.git#4532765ae845b8c98c73c88cc916f7d771b429d5"
dependencies = [
"indexmap 2.10.0",
"rustc-hash 2.1.1",
@ -1918,7 +1918,7 @@ dependencies = [
[[package]]
name = "derive_refineable"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed.git#fbe06238e45c2fec6955c70b86eb26fd6a864266"
source = "git+https://github.com/zed-industries/zed.git#4532765ae845b8c98c73c88cc916f7d771b429d5"
dependencies = [
"proc-macro2",
"quote",
@ -3112,7 +3112,7 @@ dependencies = [
[[package]]
name = "gpui"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed.git#fbe06238e45c2fec6955c70b86eb26fd6a864266"
source = "git+https://github.com/zed-industries/zed.git#4532765ae845b8c98c73c88cc916f7d771b429d5"
dependencies = [
"anyhow",
"as-raw-xcb-connection",
@ -3297,7 +3297,7 @@ dependencies = [
[[package]]
name = "gpui_macros"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed.git#fbe06238e45c2fec6955c70b86eb26fd6a864266"
source = "git+https://github.com/zed-industries/zed.git#4532765ae845b8c98c73c88cc916f7d771b429d5"
dependencies = [
"heck 0.5.0",
"proc-macro2",
@ -3566,7 +3566,7 @@ dependencies = [
[[package]]
name = "http_client"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed.git#fbe06238e45c2fec6955c70b86eb26fd6a864266"
source = "git+https://github.com/zed-industries/zed.git#4532765ae845b8c98c73c88cc916f7d771b429d5"
dependencies = [
"anyhow",
"bytes",
@ -3586,7 +3586,7 @@ dependencies = [
[[package]]
name = "http_client_tls"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed.git#fbe06238e45c2fec6955c70b86eb26fd6a864266"
source = "git+https://github.com/zed-industries/zed.git#4532765ae845b8c98c73c88cc916f7d771b429d5"
dependencies = [
"rustls",
"rustls-platform-verifier",
@ -4544,7 +4544,7 @@ dependencies = [
[[package]]
name = "media"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed.git#fbe06238e45c2fec6955c70b86eb26fd6a864266"
source = "git+https://github.com/zed-industries/zed.git#4532765ae845b8c98c73c88cc916f7d771b429d5"
dependencies = [
"anyhow",
"bindgen 0.71.1",
@ -5602,7 +5602,7 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "perf"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed.git#fbe06238e45c2fec6955c70b86eb26fd6a864266"
source = "git+https://github.com/zed-industries/zed.git#4532765ae845b8c98c73c88cc916f7d771b429d5"
dependencies = [
"collections",
"serde",
@ -6519,7 +6519,7 @@ dependencies = [
[[package]]
name = "refineable"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed.git#fbe06238e45c2fec6955c70b86eb26fd6a864266"
source = "git+https://github.com/zed-industries/zed.git#4532765ae845b8c98c73c88cc916f7d771b429d5"
dependencies = [
"derive_refineable",
"workspace-hack",
@ -6672,7 +6672,7 @@ dependencies = [
[[package]]
name = "reqwest_client"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed.git#fbe06238e45c2fec6955c70b86eb26fd6a864266"
source = "git+https://github.com/zed-industries/zed.git#4532765ae845b8c98c73c88cc916f7d771b429d5"
dependencies = [
"anyhow",
"bytes",
@ -6756,7 +6756,7 @@ dependencies = [
[[package]]
name = "rope"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed.git#fbe06238e45c2fec6955c70b86eb26fd6a864266"
source = "git+https://github.com/zed-industries/zed.git#4532765ae845b8c98c73c88cc916f7d771b429d5"
dependencies = [
"arrayvec",
"log",
@ -7263,7 +7263,7 @@ checksum = "0f7d95a54511e0c7be3f51e8867aa8cf35148d7b9445d44de2f943e2b206e749"
[[package]]
name = "semantic_version"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed.git#fbe06238e45c2fec6955c70b86eb26fd6a864266"
source = "git+https://github.com/zed-industries/zed.git#4532765ae845b8c98c73c88cc916f7d771b429d5"
dependencies = [
"anyhow",
"serde",
@ -7900,7 +7900,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "sum_tree"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed.git#fbe06238e45c2fec6955c70b86eb26fd6a864266"
source = "git+https://github.com/zed-industries/zed.git#4532765ae845b8c98c73c88cc916f7d771b429d5"
dependencies = [
"arrayvec",
"log",
@ -9216,7 +9216,7 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "util"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed.git#fbe06238e45c2fec6955c70b86eb26fd6a864266"
source = "git+https://github.com/zed-industries/zed.git#4532765ae845b8c98c73c88cc916f7d771b429d5"
dependencies = [
"anyhow",
"async-fs 2.1.3",
@ -9251,7 +9251,7 @@ dependencies = [
[[package]]
name = "util_macros"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed.git#fbe06238e45c2fec6955c70b86eb26fd6a864266"
source = "git+https://github.com/zed-industries/zed.git#4532765ae845b8c98c73c88cc916f7d771b429d5"
dependencies = [
"perf",
"quote",

View file

@ -22,12 +22,11 @@ impl Example {
let editor = cx.new(|cx| {
InputState::new(window, cx)
.multi_line()
.line_number(true)
.tab_size(TabSize {
tab_size: 4,
hard_tabs: false,
})
.soft_wrap(false)
.soft_wrap(true)
.placeholder("Enter your code here...")
.default_value(text)
});

View file

@ -164,10 +164,8 @@ impl TextElement {
if let (Some(cursor_pos), Some(cursor_start), Some(cursor_end)) =
(cursor_pos, cursor_start, cursor_end)
{
let cursor_moved = state.last_cursor != Some(cursor);
let selection_changed = state.last_selected_range != Some(selected_range);
if cursor_moved || selection_changed {
if selection_changed {
scroll_offset.x = if scroll_offset.x + cursor_pos.x
> (bounds.size.width - line_number_width - RIGHT_MARGIN)
{
@ -225,6 +223,10 @@ impl TextElement {
));
}
if let Some(deferred_scroll_offset) = state.deferred_scroll_offset {
scroll_offset = deferred_scroll_offset;
}
bounds.origin = bounds.origin + scroll_offset;
(cursor_bounds, scroll_offset, current_row)
@ -455,7 +457,11 @@ impl TextElement {
}
let total_lines = state.text_wrapper.len();
let scroll_top = state.scroll_handle.offset().y;
let scroll_top = if let Some(deferred_scroll_offset) = state.deferred_scroll_offset {
deferred_scroll_offset.y
} else {
state.scroll_handle.offset().y
};
let mut visible_range = 0..total_lines;
let mut line_bottom = px(0.);
@ -1084,11 +1090,7 @@ impl Element for TextElement {
input_bounds.size.height,
),
},
cx.theme()
.highlight_theme
.style
.background
.unwrap_or(cx.theme().background),
cx.theme().background,
));
// Each item is the normal lines.
@ -1126,6 +1128,8 @@ impl Element for TextElement {
state
.scroll_handle
.set_offset(prepaint.cursor_scroll_offset);
state.deferred_scroll_offset = None;
cx.notify();
});

View file

@ -1,5 +1,5 @@
use anyhow::Result;
use gpui::{App, Context, EntityInputHandler, MouseMoveEvent, Task, Window};
use gpui::{App, Context, MouseMoveEvent, Task, Window};
use std::rc::Rc;
use crate::input::{popovers::ContextMenu, InputState, RopeExt};
@ -99,7 +99,7 @@ impl InputState {
let end = self.text.position_to_offset(&edit.range.end);
let range_utf16 = self.range_to_utf16(&(start..end));
self.replace_text_in_range(Some(range_utf16), &edit.new_text, window, cx);
self.replace_text_in_range_silent(Some(range_utf16), &edit.new_text, window, cx);
}
}

View file

@ -105,6 +105,10 @@ impl InputMode {
}
pub(super) fn update_auto_grow(&mut self, text_wrapper: &TextWrapper) {
if self.is_single_line() {
return;
}
let wrapped_lines = text_wrapper.len();
self.set_rows(wrapped_lines);
}

View file

@ -2,9 +2,9 @@ use std::rc::Rc;
use gpui::{
canvas, deferred, div, prelude::FluentBuilder, px, relative, Action, AnyElement, App,
AppContext, Bounds, Context, DismissEvent, Empty, Entity, EntityInputHandler, EventEmitter,
HighlightStyle, InteractiveElement as _, IntoElement, ParentElement, Pixels, Point, Render,
RenderOnce, SharedString, Styled, StyledText, Subscription, Window,
AppContext, Bounds, Context, DismissEvent, Empty, Entity, EventEmitter, HighlightStyle,
InteractiveElement as _, IntoElement, ParentElement, Pixels, Point, Render, RenderOnce,
SharedString, Styled, StyledText, Subscription, Window,
};
use lsp_types::{CompletionItem, CompletionTextEdit};
@ -262,7 +262,7 @@ impl CompletionMenu {
range = offset..offset;
}
editor.replace_text_in_range(
editor.replace_text_in_range_silent(
Some(editor.range_to_utf16(&range)),
&new_text,
window,

View file

@ -127,7 +127,8 @@ impl RopeExt for Rope {
fn offset_to_position(&self, offset: usize) -> Position {
let point = self.offset_to_point(offset);
let line = self.line(point.row as usize);
let character = line.slice(0..point.column as usize).chars().count();
let column = line.clip_offset(point.column as usize, sum_tree::Bias::Left);
let character = line.slice(0..column).chars().count();
Position::new(point.row, character as u32)
}

View file

@ -4,8 +4,8 @@ use std::{ops::Range, rc::Rc};
use gpui::{
actions, canvas, div, prelude::FluentBuilder as _, App, AppContext as _, Context, Empty,
Entity, EntityInputHandler, FocusHandle, Focusable, Half, InteractiveElement as _, IntoElement,
KeyBinding, ParentElement as _, Pixels, Render, Styled, Subscription, Window,
Entity, FocusHandle, Focusable, Half, InteractiveElement as _, IntoElement, KeyBinding,
ParentElement as _, Pixels, Render, Styled, Subscription, Window,
};
use rope::Rope;
@ -345,7 +345,7 @@ impl SearchPanel {
text_state.update(cx, |state, cx| {
let range_utf16 = state.range_to_utf16(&range);
state.scroll_to(next_range.end, cx);
state.replace_text_in_range(
state.replace_text_in_range_silent(
Some(range_utf16),
new_text.as_str(),
window,
@ -375,7 +375,7 @@ impl SearchPanel {
for range in ranges.iter().rev() {
rope.replace(range.clone(), new_text.as_str());
}
state.replace_text_in_range(
state.replace_text_in_range_silent(
Some(0..state.text.len()),
&rope.to_string(),
window,

View file

@ -5,7 +5,7 @@
use anyhow::Result;
use gpui::{
actions, div, point, prelude::FluentBuilder as _, px, Action, App, AppContext, Bounds,
ClipboardItem, Context, Entity, EntityInputHandler, EventEmitter, FocusHandle, Focusable, Half,
ClipboardItem, Context, Entity, EntityInputHandler, EventEmitter, FocusHandle, Focusable,
InteractiveElement as _, IntoElement, KeyBinding, KeyDownEvent, MouseButton, MouseDownEvent,
MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels, Point, Render, ScrollHandle,
ScrollWheelEvent, SharedString, Styled as _, Subscription, Task, UTF16Selection, Window,
@ -284,6 +284,8 @@ pub struct InputState {
pub(super) pattern: Option<regex::Regex>,
pub(super) validate: Option<Box<dyn Fn(&str, &mut Context<Self>) -> bool + 'static>>,
pub(crate) scroll_handle: ScrollHandle,
/// The deferred scroll offset to apply on next layout.
pub(crate) deferred_scroll_offset: Option<Point<Pixels>>,
pub(super) scroll_state: ScrollbarState,
/// The size of the scrollable content.
pub(crate) scroll_size: gpui::Size<Pixels>,
@ -304,6 +306,9 @@ pub struct InputState {
pub lsp: Lsp,
/// A flag to indicate if we should ignore the next completion event.
pub(super) silent_replace_text: bool,
/// To remember the horizontal column (x-coordinate) of the cursor position for keep column for move up/down.
///
/// The first element is the x-coordinate (Pixels), preferred to use this.
@ -378,6 +383,7 @@ impl InputState {
scroll_handle: ScrollHandle::new(),
scroll_state: ScrollbarState::default(),
scroll_size: gpui::size(px(0.), px(0.)),
deferred_scroll_offset: None,
preferred_column: None,
placeholder: SharedString::default(),
mask_pattern: MaskPattern::default(),
@ -387,6 +393,7 @@ impl InputState {
completion_inserting: false,
hover_popover: None,
hover_definition: None,
silent_replace_text: false,
_subscriptions,
_context_menu_task: Task::ready(Ok(())),
}
@ -448,6 +455,7 @@ impl InputState {
/// Set this input is searchable, default is false (Default true for Code Editor).
pub fn searchable(mut self, searchable: bool) -> Self {
debug_assert!(self.mode.is_multi_line());
self.searchable = searchable;
self
}
@ -460,6 +468,7 @@ impl InputState {
/// Set enable/disable line number, only for [`InputMode::CodeEditor`] mode.
pub fn line_number(mut self, line_number: bool) -> Self {
debug_assert!(self.mode.is_code_editor());
if let InputMode::CodeEditor { line_number: l, .. } = &mut self.mode {
*l = line_number;
}
@ -468,6 +477,7 @@ impl InputState {
/// Set line number, only for [`InputMode::CodeEditor`] mode.
pub fn set_line_number(&mut self, line_number: bool, _: &mut Window, cx: &mut Context<Self>) {
debug_assert!(self.mode.is_code_editor());
if let InputMode::CodeEditor { line_number: l, .. } = &mut self.mode {
*l = line_number;
}
@ -478,6 +488,7 @@ impl InputState {
///
/// Only for [`InputMode::MultiLine`] and [`InputMode::CodeEditor`] mode.
pub fn tab_size(mut self, tab: TabSize) -> Self {
debug_assert!(self.mode.is_multi_line() || self.mode.is_code_editor());
match &mut self.mode {
InputMode::MultiLine { tab: t, .. } => *t = tab,
InputMode::CodeEditor { tab: t, .. } => *t = tab,
@ -699,7 +710,7 @@ impl InputState {
) {
let text: SharedString = text.into();
let range_utf16 = self.range_to_utf16(&(self.cursor()..self.cursor()));
self.replace_text_in_range(Some(range_utf16), &text, window, cx);
self.replace_text_in_range_silent(Some(range_utf16), &text, window, cx);
self.selected_range = (self.selected_range.end..self.selected_range.end).into();
}
@ -713,7 +724,7 @@ impl InputState {
cx: &mut Context<Self>,
) {
let text: SharedString = text.into();
self.replace_text_in_range(None, &text, window, cx);
self.replace_text_in_range_silent(None, &text, window, cx);
self.selected_range = (self.selected_range.end..self.selected_range.end).into();
}
@ -725,7 +736,7 @@ impl InputState {
) {
let text: SharedString = text.into();
let range = 0..self.text.chars().map(|c| c.len_utf16()).sum();
self.replace_text_in_range(Some(range), &text, window, cx);
self.replace_text_in_range_silent(Some(range), &text, window, cx);
self.reset_highlighter(cx);
}
@ -739,13 +750,19 @@ impl InputState {
}
/// Set with password masked state.
///
/// Only for [`InputMode::SingleLine`] mode.
pub fn masked(mut self, masked: bool) -> Self {
debug_assert!(self.mode.is_single_line());
self.masked = masked;
self
}
/// Set the password masked state of the input field.
///
/// Only for [`InputMode::SingleLine`] mode.
pub fn set_masked(&mut self, masked: bool, _: &mut Window, cx: &mut Context<Self>) {
debug_assert!(self.mode.is_single_line());
self.masked = masked;
cx.notify();
}
@ -758,12 +775,14 @@ impl InputState {
/// Set the soft wrap mode for multi-line input, default is true.
pub fn soft_wrap(mut self, wrap: bool) -> Self {
debug_assert!(self.mode.is_multi_line());
self.soft_wrap = wrap;
self
}
/// Update the soft wrap mode for multi-line input, default is true.
pub fn set_soft_wrap(&mut self, wrap: bool, _: &mut Window, cx: &mut Context<Self>) {
debug_assert!(self.mode.is_multi_line());
self.soft_wrap = wrap;
if wrap {
let wrap_width = self
@ -785,29 +804,41 @@ impl InputState {
}
/// Set the regular expression pattern of the input field.
///
/// Only for [`InputMode::SingleLine`] mode.
pub fn pattern(mut self, pattern: regex::Regex) -> Self {
debug_assert!(self.mode.is_single_line());
self.pattern = Some(pattern);
self
}
/// Set the regular expression pattern of the input field with reference.
///
/// Only for [`InputMode::SingleLine`] mode.
pub fn set_pattern(
&mut self,
pattern: regex::Regex,
_window: &mut Window,
_cx: &mut Context<Self>,
) {
debug_assert!(self.mode.is_single_line());
self.pattern = Some(pattern);
}
/// Set the validation function of the input field.
///
/// Only for [`InputMode::SingleLine`] mode.
pub fn validate(mut self, f: impl Fn(&str, &mut Context<Self>) -> bool + 'static) -> Self {
debug_assert!(self.mode.is_single_line());
self.validate = Some(Box::new(f));
self
}
/// Set true to show indicator at the input right.
///
/// Only for [`InputMode::SingleLine`] mode.
pub fn set_loading(&mut self, loading: bool, _: &mut Window, cx: &mut Context<Self>) {
debug_assert!(self.mode.is_single_line());
self.loading = loading;
cx.notify();
}
@ -862,8 +893,6 @@ impl InputState {
.text
.point_to_offset(rope::Point::new(row as u32, col as u32));
// TODO: Scroll to make the row in center of viewport.
self.move_to(offset, cx);
self.update_preferred_column();
self.focus(window, cx);
@ -990,8 +1019,8 @@ impl InputState {
}
pub(super) fn select_all(&mut self, _: &SelectAll, _: &mut Window, cx: &mut Context<Self>) {
self.move_to(0, cx);
self.select_to(self.text.len(), cx);
self.selected_range = (0..self.text.len()).into();
cx.notify();
}
pub(super) fn home(&mut self, _: &MoveHome, _: &mut Window, cx: &mut Context<Self>) {
@ -1101,6 +1130,7 @@ impl InputState {
/// Return the start offset of the previous word.
fn previous_start_of_word(&mut self) -> usize {
let offset = self.selected_range.start;
let offset = self.offset_from_utf16(self.offset_to_utf16(offset));
// FIXME: Avoid to_string
let left_part = self.text.slice(0..offset).to_string();
@ -1114,6 +1144,7 @@ impl InputState {
/// Return the next end offset of the next word.
fn next_end_of_word(&mut self) -> usize {
let offset = self.cursor();
let offset = self.offset_from_utf16(self.offset_to_utf16(offset));
let right_part = self.text.slice(offset..self.text.len()).to_string();
UnicodeSegmentation::split_word_bound_indices(right_part.as_str())
@ -1230,7 +1261,7 @@ impl InputState {
if offset == self.cursor() {
offset = offset.saturating_sub(1);
}
self.replace_text_in_range(
self.replace_text_in_range_silent(
Some(self.range_to_utf16(&(offset..self.cursor()))),
"",
window,
@ -1250,7 +1281,7 @@ impl InputState {
if offset == self.cursor() {
offset = (offset + 1).clamp(0, self.text.len());
}
self.replace_text_in_range(
self.replace_text_in_range_silent(
Some(self.range_to_utf16(&(self.cursor()..offset))),
"",
window,
@ -1266,7 +1297,7 @@ impl InputState {
cx: &mut Context<Self>,
) {
let offset = self.previous_start_of_word();
self.replace_text_in_range(
self.replace_text_in_range_silent(
Some(self.range_to_utf16(&(offset..self.cursor()))),
"",
window,
@ -1282,7 +1313,7 @@ impl InputState {
cx: &mut Context<Self>,
) {
let offset = self.next_end_of_word();
self.replace_text_in_range(
self.replace_text_in_range_silent(
Some(self.range_to_utf16(&(self.cursor()..offset))),
"",
window,
@ -1306,7 +1337,8 @@ impl InputState {
// Add newline and indent
let new_line_text = format!("\n{}", indent);
self.replace_text_in_range(None, &new_line_text, window, cx);
self.replace_text_in_range_silent(None, &new_line_text, window, cx);
self.pause_blink_cursor(cx);
} else {
// Single line input, just emit the event (e.g.: In a modal dialog to confirm).
cx.propagate();
@ -1372,7 +1404,7 @@ impl InputState {
.unwrap_or("".into());
for line in selected_text.split('\n') {
self.replace_text_in_range(
self.replace_text_in_range_silent(
Some(self.range_to_utf16(&(offset..offset))),
&tab_indent,
window,
@ -1392,7 +1424,7 @@ impl InputState {
} else {
// Selected none
let offset = self.selected_range.start;
self.replace_text_in_range(
self.replace_text_in_range_silent(
Some(self.range_to_utf16(&(offset..offset))),
&tab_indent,
window,
@ -1430,7 +1462,7 @@ impl InputState {
for line in selected_text.split('\n') {
if line.starts_with(tab_indent.as_ref()) {
self.replace_text_in_range(
self.replace_text_in_range_silent(
Some(self.range_to_utf16(&(offset..offset + tab_indent.len()))),
"",
window,
@ -1457,6 +1489,7 @@ impl InputState {
// Selected none
let start_offset = self.selected_range.start;
let offset = self.start_of_line_of_selection(window, cx);
let offset = self.offset_from_utf16(self.offset_to_utf16(offset));
// FIXME: To improve performance
if self
.text
@ -1464,7 +1497,7 @@ impl InputState {
.to_string()
.starts_with(tab_indent.as_ref())
{
self.replace_text_in_range(
self.replace_text_in_range_silent(
Some(self.range_to_utf16(&(offset..offset + tab_indent.len()))),
"",
window,
@ -1647,15 +1680,19 @@ impl InputState {
// Check if row_offset_y is out of the viewport
// If row offset is not in the viewport, scroll to make it visible
if row_offset_y < -scroll_offset.y {
let edge_height = 3 * line_height;
if row_offset_y - edge_height < -scroll_offset.y {
// Scroll up
scroll_offset.y = -row_offset_y - line_height + bounds.size.height.half();
} else if row_offset_y + line_height > -scroll_offset.y + bounds.size.height {
scroll_offset.y = -row_offset_y + edge_height;
} else if row_offset_y + edge_height > -scroll_offset.y + bounds.size.height {
// Scroll down
scroll_offset.y = -(row_offset_y - bounds.size.height.half());
scroll_offset.y = -(row_offset_y - bounds.size.height + edge_height);
}
self.update_scroll_offset(Some(scroll_offset), cx);
scroll_offset.x = scroll_offset.x.min(px(0.));
scroll_offset.y = scroll_offset.y.min(px(0.));
self.deferred_scroll_offset = Some(scroll_offset);
cx.notify();
}
pub(super) fn show_character_palette(
@ -1683,7 +1720,8 @@ impl InputState {
let selected_text = self.text.slice(self.selected_range.into()).to_string();
cx.write_to_clipboard(ClipboardItem::new_string(selected_text));
self.replace_text_in_range(None, "", window, cx);
self.replace_text_in_range_silent(None, "", window, cx);
}
pub(super) fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
@ -1693,7 +1731,8 @@ impl InputState {
new_text = new_text.replace('\n', "");
}
self.replace_text_in_range(None, &new_text, window, cx);
self.replace_text_in_range_silent(None, &new_text, window, cx);
self.scroll_to(self.cursor(), cx);
}
}
@ -1715,7 +1754,7 @@ impl InputState {
if let Some(changes) = self.history.undo() {
for change in changes {
let range_utf16 = self.range_to_utf16(&change.new_range.into());
self.replace_text_in_range(Some(range_utf16), &change.old_text, window, cx);
self.replace_text_in_range_silent(Some(range_utf16), &change.old_text, window, cx);
}
}
self.history.ignore = false;
@ -1726,7 +1765,7 @@ impl InputState {
if let Some(changes) = self.history.redo() {
for change in changes {
let range_utf16 = self.range_to_utf16(&change.old_range.into());
self.replace_text_in_range(Some(range_utf16), &change.new_text, window, cx);
self.replace_text_in_range_silent(Some(range_utf16), &change.new_text, window, cx);
}
}
self.history.ignore = false;
@ -2152,7 +2191,9 @@ impl InputState {
}
pub(super) fn selected_text(&self) -> Rope {
self.text.slice(self.selected_range.into())
let range_utf16 = self.range_to_utf16(&self.selected_range.into());
let range = self.range_from_utf16(&range_utf16);
self.text.slice(range)
}
pub(crate) fn range_to_bounds(&self, range: &Range<usize>) -> Option<Bounds<Pixels>> {
@ -2193,13 +2234,28 @@ impl InputState {
) {
let start = self.text.position_to_offset(&lsp_range.start);
let end = self.text.position_to_offset(&lsp_range.end);
self.replace_text_in_range(
self.replace_text_in_range_silent(
Some(self.range_to_utf16(&(start..end))),
new_text,
window,
cx,
);
}
/// Replace text in range in silent.
///
/// This will not trigger any UI interaction, such as auto-completion.
pub(crate) fn replace_text_in_range_silent(
&mut self,
range_utf16: Option<Range<usize>>,
new_text: &str,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.silent_replace_text = true;
self.replace_text_in_range(range_utf16, new_text, window, cx);
self.silent_replace_text = false;
}
}
impl EntityInputHandler for InputState {
@ -2269,20 +2325,23 @@ impl EntityInputHandler for InputState {
let old_text = self.text.clone();
self.text.replace(range.clone(), new_text);
let pending_text = self.text.to_string();
// Check if the new text is valid
if !self.is_valid_input(&pending_text, cx) {
self.text = old_text;
return;
}
let mut new_offset = (range.start + new_text.len()).min(self.text.len());
if !self.mask_pattern.is_none() {
let mask_text = self.mask_pattern.mask(&pending_text);
self.text = Rope::from(mask_text.as_str());
let new_text_len =
(new_text.len() + mask_text.len()).saturating_sub(pending_text.len());
new_offset = (range.start + new_text_len).min(mask_text.len());
if self.mode.is_single_line() {
let pending_text = self.text.to_string();
// Check if the new text is valid
if !self.is_valid_input(&pending_text, cx) {
self.text = old_text;
return;
}
if !self.mask_pattern.is_none() {
let mask_text = self.mask_pattern.mask(&pending_text);
self.text = Rope::from(mask_text.as_str());
let new_text_len =
(new_text.len() + mask_text.len()).saturating_sub(pending_text.len());
new_offset = (range.start + new_text_len).min(mask_text.len());
}
}
self.push_history(&old_text, &range, &new_text);
@ -2296,10 +2355,11 @@ impl EntityInputHandler for InputState {
self.selected_range = (new_offset..new_offset).into();
self.ime_marked_range.take();
self.update_preferred_column();
self.update_scroll_offset(None, cx);
self.update_search(cx);
self.mode.update_auto_grow(&self.text_wrapper);
self.handle_completion_trigger(&range, &new_text, window, cx);
if !self.silent_replace_text {
self.handle_completion_trigger(&range, &new_text, window, cx);
}
cx.emit(InputEvent::Change);
cx.notify();
}
@ -2328,11 +2388,13 @@ impl EntityInputHandler for InputState {
let old_text = self.text.clone();
self.text.replace(range.clone(), new_text);
let pending_text = self.text.to_string();
if !self.is_valid_input(&pending_text, cx) {
self.text = old_text;
return;
if self.mode.is_single_line() {
let pending_text = self.text.to_string();
if !self.is_valid_input(&pending_text, cx) {
self.text = old_text;
return;
}
}
self.push_history(&old_text, &range, new_text);