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:
parent
4b3731bf14
commit
993ce34bd8
9 changed files with 156 additions and 86 deletions
34
Cargo.lock
generated
34
Cargo.lock
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue