1use gpui::TextAlign;
6use gpui::{
7 Action, App, AppContext, Bounds, ClipboardItem, Context, Edges, Entity, EntityInputHandler,
8 EventEmitter, FocusHandle, Focusable, InteractiveElement as _, IntoElement, KeyBinding,
9 MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels, Point,
10 Render, ScrollHandle, ScrollWheelEvent, SharedString, Styled as _, Subscription,
11 UTF16Selection, Window, actions, div, point, prelude::FluentBuilder as _, px,
12};
13use ropey::{Rope, RopeSlice};
14use serde::Deserialize;
15use std::borrow::Cow;
16use std::cell::Cell;
17use std::ops::Range;
18use std::rc::Rc;
19use sum_tree::Bias;
20use unicode_segmentation::*;
21
22use super::{
23 DiagnosticSet, DisplayMap, InputContextMenuCapabilities, InputEditorStyle,
24 InputHighlighterFactory, MASK_CHAR, MaskPattern, NativeMenu, NumberStep, WrappingIndent,
25 blink_cursor::BlinkCursor,
26 change::Change,
27 cursor::{CursorSelection, Selections},
28 element::{EditorScrollbar, EditorScrollbarSnapshot, TextElement},
29 kind::InputModeKind,
30 mask_pattern::normalize_number_input,
31 mode::LayoutMode,
32 undo_manager::{EditIntent, UndoManager},
33};
34use crate::actions::{SelectDown, SelectLeft, SelectRight, SelectUp};
35use crate::input::blink_cursor::CURSOR_WIDTH;
36use crate::input::movement::MoveDirection;
37use crate::input::{
38 InputExtras as _, Position, RopeExt as _, element::RIGHT_MARGIN, layout::LastLayout,
39};
40use crate::{AutoScroll, StepAction};
41
42pub(crate) enum ScrollPadding {
44 Minimal,
45 SurroundingLines,
46}
47
48#[derive(Action, Clone, PartialEq, Eq, Deserialize)]
49#[action(namespace = input, no_json)]
50pub struct Enter {
51 pub secondary: bool,
53 pub shift: bool,
55}
56
57impl Enter {
58 pub fn is_primary(action: &dyn Action) -> bool {
61 action.partial_eq(&Enter {
62 secondary: false,
63 shift: false,
64 }) || action.partial_eq(&Enter {
65 secondary: false,
66 shift: true,
67 })
68 }
69}
70
71actions!(
72 input,
73 [
74 Backspace,
75 Delete,
76 DeleteToBeginningOfLine,
77 DeleteToEndOfLine,
78 DeleteToPreviousWordStart,
79 DeleteToNextWordEnd,
80 Indent,
81 Outdent,
82 IndentInline,
83 OutdentInline,
84 MoveUp,
85 MoveDown,
86 MoveLeft,
87 MoveRight,
88 MoveHome,
89 MoveEnd,
90 MovePageUp,
91 MovePageDown,
92 AddCursorAbove,
93 AddCursorBelow,
94 SelectAll,
95 SelectToStartOfLine,
96 SelectToEndOfLine,
97 SelectToStart,
98 SelectToEnd,
99 SelectToPreviousWordStart,
100 SelectToNextWordEnd,
101 ShowCharacterPalette,
102 Copy,
103 Cut,
104 Paste,
105 Undo,
106 Redo,
107 MoveToStartOfLine,
108 MoveToEndOfLine,
109 MoveToStart,
110 MoveToEnd,
111 MoveToPreviousWord,
112 MoveToNextWord,
113 Escape,
114 ToggleCodeActions,
115 Search,
116 Replace,
117 GoToDefinition,
118 ]
119);
120
121#[derive(Clone)]
122pub enum InputEvent {
123 Change,
124 PressEnter { secondary: bool, shift: bool },
125 Focus,
126 Blur,
127}
128
129pub(super) const CONTEXT: &str = "Input";
130
131pub(crate) fn init(cx: &mut App) {
132 cx.bind_keys([
133 KeyBinding::new("backspace", Backspace, Some(CONTEXT)),
134 KeyBinding::new("shift-backspace", Backspace, Some(CONTEXT)),
135 #[cfg(target_os = "macos")]
136 KeyBinding::new("ctrl-backspace", Backspace, Some(CONTEXT)),
137 KeyBinding::new("delete", Delete, Some(CONTEXT)),
138 KeyBinding::new("shift-delete", Delete, Some(CONTEXT)),
139 #[cfg(target_os = "macos")]
140 KeyBinding::new("cmd-backspace", DeleteToBeginningOfLine, Some(CONTEXT)),
141 #[cfg(target_os = "macos")]
142 KeyBinding::new("cmd-delete", DeleteToEndOfLine, Some(CONTEXT)),
143 #[cfg(target_os = "macos")]
144 KeyBinding::new("alt-backspace", DeleteToPreviousWordStart, Some(CONTEXT)),
145 #[cfg(not(target_os = "macos"))]
146 KeyBinding::new("ctrl-backspace", DeleteToPreviousWordStart, Some(CONTEXT)),
147 #[cfg(target_os = "macos")]
148 KeyBinding::new("alt-delete", DeleteToNextWordEnd, Some(CONTEXT)),
149 #[cfg(not(target_os = "macos"))]
150 KeyBinding::new("ctrl-delete", DeleteToNextWordEnd, Some(CONTEXT)),
151 KeyBinding::new(
152 "enter",
153 Enter {
154 secondary: false,
155 shift: false,
156 },
157 Some(CONTEXT),
158 ),
159 KeyBinding::new(
160 "shift-enter",
161 Enter {
162 secondary: false,
163 shift: true,
164 },
165 Some(CONTEXT),
166 ),
167 KeyBinding::new(
168 "secondary-enter",
169 Enter {
170 secondary: true,
171 shift: false,
172 },
173 Some(CONTEXT),
174 ),
175 KeyBinding::new("escape", Escape, Some(CONTEXT)),
176 KeyBinding::new("up", MoveUp, Some(CONTEXT)),
177 KeyBinding::new("down", MoveDown, Some(CONTEXT)),
178 KeyBinding::new("left", MoveLeft, Some(CONTEXT)),
179 KeyBinding::new("right", MoveRight, Some(CONTEXT)),
180 KeyBinding::new("pageup", MovePageUp, Some(CONTEXT)),
181 KeyBinding::new("pagedown", MovePageDown, Some(CONTEXT)),
182 KeyBinding::new("tab", IndentInline, Some(CONTEXT)),
183 KeyBinding::new("shift-tab", OutdentInline, Some(CONTEXT)),
184 #[cfg(target_os = "macos")]
185 KeyBinding::new("cmd-]", Indent, Some(CONTEXT)),
186 #[cfg(not(target_os = "macos"))]
187 KeyBinding::new("ctrl-]", Indent, Some(CONTEXT)),
188 #[cfg(target_os = "macos")]
189 KeyBinding::new("cmd-[", Outdent, Some(CONTEXT)),
190 #[cfg(not(target_os = "macos"))]
191 KeyBinding::new("ctrl-[", Outdent, Some(CONTEXT)),
192 KeyBinding::new("shift-left", SelectLeft, Some(CONTEXT)),
193 KeyBinding::new("shift-right", SelectRight, Some(CONTEXT)),
194 KeyBinding::new("shift-up", SelectUp, Some(CONTEXT)),
195 KeyBinding::new("shift-down", SelectDown, Some(CONTEXT)),
196 #[cfg(not(any(target_os = "macos", target_os = "linux")))]
197 KeyBinding::new("shift-alt-left", SelectLeft, Some(CONTEXT)),
198 #[cfg(not(any(target_os = "macos", target_os = "linux")))]
199 KeyBinding::new("shift-alt-right", SelectRight, Some(CONTEXT)),
200 #[cfg(target_os = "macos")]
202 KeyBinding::new("cmd-alt-up", AddCursorAbove, Some(CONTEXT)),
203 #[cfg(target_os = "macos")]
204 KeyBinding::new("cmd-alt-down", AddCursorBelow, Some(CONTEXT)),
205 #[cfg(target_os = "windows")]
206 KeyBinding::new("ctrl-alt-up", AddCursorAbove, Some(CONTEXT)),
207 #[cfg(not(any(target_os = "macos", target_os = "windows")))]
208 KeyBinding::new("shift-alt-up", AddCursorAbove, Some(CONTEXT)),
209 #[cfg(target_os = "windows")]
210 KeyBinding::new("ctrl-alt-down", AddCursorBelow, Some(CONTEXT)),
211 #[cfg(not(any(target_os = "macos", target_os = "windows")))]
212 KeyBinding::new("shift-alt-down", AddCursorBelow, Some(CONTEXT)),
213 KeyBinding::new("home", MoveHome, Some(CONTEXT)),
214 KeyBinding::new("end", MoveEnd, Some(CONTEXT)),
215 #[cfg(not(target_os = "macos"))]
216 KeyBinding::new("ctrl-home", MoveToStart, Some(CONTEXT)),
217 #[cfg(not(target_os = "macos"))]
218 KeyBinding::new("ctrl-end", MoveToEnd, Some(CONTEXT)),
219 #[cfg(not(target_os = "macos"))]
220 KeyBinding::new("ctrl-shift-home", SelectToStart, Some(CONTEXT)),
221 #[cfg(not(target_os = "macos"))]
222 KeyBinding::new("ctrl-shift-end", SelectToEnd, Some(CONTEXT)),
223 KeyBinding::new("shift-home", SelectToStartOfLine, Some(CONTEXT)),
224 KeyBinding::new("shift-end", SelectToEndOfLine, Some(CONTEXT)),
225 #[cfg(target_os = "macos")]
226 KeyBinding::new("ctrl-shift-a", SelectToStartOfLine, Some(CONTEXT)),
227 #[cfg(target_os = "macos")]
228 KeyBinding::new("ctrl-shift-e", SelectToEndOfLine, Some(CONTEXT)),
229 #[cfg(target_os = "macos")]
230 KeyBinding::new("shift-cmd-left", SelectToStartOfLine, Some(CONTEXT)),
231 #[cfg(target_os = "macos")]
232 KeyBinding::new("shift-cmd-right", SelectToEndOfLine, Some(CONTEXT)),
233 #[cfg(any(target_os = "macos", target_os = "linux"))]
234 KeyBinding::new("alt-shift-left", SelectToPreviousWordStart, Some(CONTEXT)),
235 #[cfg(not(target_os = "macos"))]
236 KeyBinding::new("ctrl-shift-left", SelectToPreviousWordStart, Some(CONTEXT)),
237 #[cfg(any(target_os = "macos", target_os = "linux"))]
238 KeyBinding::new("alt-shift-right", SelectToNextWordEnd, Some(CONTEXT)),
239 #[cfg(not(target_os = "macos"))]
240 KeyBinding::new("ctrl-shift-right", SelectToNextWordEnd, Some(CONTEXT)),
241 #[cfg(target_os = "macos")]
242 KeyBinding::new("ctrl-cmd-space", ShowCharacterPalette, Some(CONTEXT)),
243 #[cfg(target_os = "macos")]
244 KeyBinding::new("cmd-a", SelectAll, Some(CONTEXT)),
245 #[cfg(not(target_os = "macos"))]
246 KeyBinding::new("ctrl-a", SelectAll, Some(CONTEXT)),
247 #[cfg(target_os = "macos")]
248 KeyBinding::new("cmd-c", Copy, Some(CONTEXT)),
249 #[cfg(not(target_os = "macos"))]
250 KeyBinding::new("ctrl-c", Copy, Some(CONTEXT)),
251 #[cfg(target_os = "macos")]
252 KeyBinding::new("cmd-x", Cut, Some(CONTEXT)),
253 #[cfg(not(target_os = "macos"))]
254 KeyBinding::new("ctrl-x", Cut, Some(CONTEXT)),
255 #[cfg(target_os = "macos")]
256 KeyBinding::new("cmd-v", Paste, Some(CONTEXT)),
257 #[cfg(not(target_os = "macos"))]
258 KeyBinding::new("ctrl-v", Paste, Some(CONTEXT)),
259 #[cfg(target_os = "macos")]
260 KeyBinding::new("ctrl-a", MoveHome, Some(CONTEXT)),
261 #[cfg(target_os = "macos")]
262 KeyBinding::new("cmd-left", MoveHome, Some(CONTEXT)),
263 #[cfg(target_os = "macos")]
264 KeyBinding::new("ctrl-e", MoveEnd, Some(CONTEXT)),
265 #[cfg(target_os = "macos")]
266 KeyBinding::new("cmd-right", MoveEnd, Some(CONTEXT)),
267 #[cfg(target_os = "macos")]
268 KeyBinding::new("cmd-z", Undo, Some(CONTEXT)),
269 #[cfg(target_os = "macos")]
270 KeyBinding::new("cmd-shift-z", Redo, Some(CONTEXT)),
271 #[cfg(target_os = "macos")]
272 KeyBinding::new("cmd-up", MoveToStart, Some(CONTEXT)),
273 #[cfg(target_os = "macos")]
274 KeyBinding::new("cmd-down", MoveToEnd, Some(CONTEXT)),
275 #[cfg(target_os = "macos")]
276 KeyBinding::new("alt-left", MoveToPreviousWord, Some(CONTEXT)),
277 #[cfg(target_os = "macos")]
278 KeyBinding::new("alt-right", MoveToNextWord, Some(CONTEXT)),
279 #[cfg(not(target_os = "macos"))]
280 KeyBinding::new("ctrl-left", MoveToPreviousWord, Some(CONTEXT)),
281 #[cfg(not(target_os = "macos"))]
282 KeyBinding::new("ctrl-right", MoveToNextWord, Some(CONTEXT)),
283 #[cfg(target_os = "macos")]
284 KeyBinding::new("cmd-shift-up", SelectToStart, Some(CONTEXT)),
285 #[cfg(target_os = "macos")]
286 KeyBinding::new("cmd-shift-down", SelectToEnd, Some(CONTEXT)),
287 #[cfg(not(target_os = "macos"))]
288 KeyBinding::new("ctrl-z", Undo, Some(CONTEXT)),
289 #[cfg(not(target_os = "macos"))]
290 KeyBinding::new("ctrl-y", Redo, Some(CONTEXT)),
291 #[cfg(target_os = "macos")]
292 KeyBinding::new("cmd-.", ToggleCodeActions, Some(CONTEXT)),
293 #[cfg(not(target_os = "macos"))]
294 KeyBinding::new("ctrl-.", ToggleCodeActions, Some(CONTEXT)),
295 #[cfg(target_os = "macos")]
296 KeyBinding::new("cmd-f", Search, Some(CONTEXT)),
297 #[cfg(not(target_os = "macos"))]
298 KeyBinding::new("ctrl-f", Search, Some(CONTEXT)),
299 #[cfg(target_os = "macos")]
300 KeyBinding::new("cmd-shift-f", Replace, Some(CONTEXT)),
301 #[cfg(not(target_os = "macos"))]
302 KeyBinding::new("ctrl-h", Replace, Some(CONTEXT)),
303 ]);
304}
305
306#[derive(Debug, Clone, Copy, PartialEq, Eq)]
313pub(super) struct ColumnarPoint {
314 offset: usize,
315 columns_past_line_end: usize,
316}
317
318impl ColumnarPoint {
319 pub(super) fn new(offset: usize, columns_past_line_end: usize) -> Self {
320 Self {
321 offset,
322 columns_past_line_end,
323 }
324 }
325}
326
327pub struct InputBaseState<M: InputModeKind> {
338 pub(crate) extras: M::Extras,
340 pub(super) focus_handle: FocusHandle,
341 pub(super) mode: LayoutMode,
342 pub(super) text: Rope,
343 pub(super) display_map: DisplayMap,
344 pub(super) undo_manager: UndoManager,
345 pub(super) search_session: super::SearchSession,
346 pub(super) search_activation_revision: u64,
349 pub(super) searchable: bool,
350 pub(super) replaceable: bool,
351 pub(super) soft_wrap: bool,
352 pub(super) wrapping_indent: WrappingIndent,
353 pub(super) scroll_beyond_last_line: Option<usize>,
354 pub(super) cursor_surrounding_lines: Option<usize>,
355 pub(super) blink_cursor: Entity<BlinkCursor>,
356 pub(super) loading: bool,
357 pub(super) selections: Selections,
361 pub(super) selected_word_range: Option<CursorSelection>,
363 pub(super) ime_marked_range: Option<CursorSelection>,
365 pub(super) last_layout: Option<LastLayout>,
366 pub(super) last_cursor: Option<usize>,
367 pub(super) input_bounds: Bounds<Pixels>,
369 pub(super) last_bounds: Option<Bounds<Pixels>>,
371 pub(super) last_selected_range: Option<CursorSelection>,
372 pub(super) selecting: bool,
373 pub(super) column_select_start: Option<ColumnarPoint>,
375 pub(crate) disabled: bool,
376 pub(crate) readonly: bool,
377 pub(crate) text_align: TextAlign,
378 pub(super) masked: bool,
379 pub(super) clean_on_escape: bool,
380 pub(super) submit_on_enter: bool,
381 pub(super) show_whitespaces: bool,
382 pub(crate) cursor_line_end_affinity: bool,
384 pub(super) pattern: Option<regex::Regex>,
385 pub(super) validate: Option<Box<dyn Fn(&str, &mut App) -> bool + 'static>>,
386 pub(crate) number_step: Option<NumberStep>,
389 pub(crate) number_min: Option<f64>,
391 pub(crate) number_max: Option<f64>,
393 pub(crate) scroll_handle: ScrollHandle,
394 pub(crate) deferred_scroll_offset: Option<Point<Pixels>>,
396 pub(crate) scroll_size: gpui::Size<Pixels>,
398 pub(super) editor_scrollbar_snapshot: Cell<Option<EditorScrollbarSnapshot>>,
399 pub(super) editor_paddings: Edges<Pixels>,
400 pub(super) editor_style: InputEditorStyle,
405 projected_editor_style: InputEditorStyle,
410
411 pub(crate) mask_pattern: MaskPattern,
413 pub(super) mask_pattern_set: bool,
417 pub(super) placeholder: SharedString,
418
419 pub(super) diagnostic_popover: Option<Rc<crate::input::DiagnosticEntry>>,
421
422 context_menu_handler: Option<
423 Rc<dyn Fn(NativeMenu, InputContextMenuCapabilities, Point<Pixels>, &mut Window, &mut App)>,
424 >,
425 pending_context_menu: Option<(Point<Pixels>, usize)>,
426
427 pub(super) enable_context_menu: bool,
430
431 pub(super) completion_inserting: bool,
433 pub(super) overlay_action_handler: Option<
434 Rc<
435 dyn Fn(
436 super::InputOverlayKind,
437 Box<dyn Action>,
438 &mut Window,
439 &mut Context<InputBaseState<M>>,
440 ) -> bool,
441 >,
442 >,
443
444 _pending_update: bool,
448 pub(super) silent_replace_text: bool,
450 pub(super) emit_events: bool,
452
453 _subscriptions: Vec<Subscription>,
454
455 pub(super) auto_scroll: AutoScroll,
456}
457
458#[derive(Clone)]
463pub struct InputPresentation {
464 focus_handle: FocusHandle,
465 disabled: bool,
466 readonly: bool,
467 loading: bool,
468 masked: bool,
469 multi_line: bool,
470 code_editor: bool,
471 text_align: TextAlign,
472 placeholder: SharedString,
473 mask_placeholder: Option<String>,
474}
475
476impl InputPresentation {
477 pub fn focus_handle(&self) -> &FocusHandle {
478 &self.focus_handle
479 }
480
481 pub fn is_disabled(&self) -> bool {
482 self.disabled
483 }
484
485 pub fn is_readonly(&self) -> bool {
486 self.readonly
487 }
488
489 pub fn is_editable(&self) -> bool {
493 !self.disabled && !self.readonly
494 }
495
496 pub fn is_loading(&self) -> bool {
497 self.loading
498 }
499
500 pub fn is_masked(&self) -> bool {
501 self.masked
502 }
503
504 pub fn is_multi_line(&self) -> bool {
505 self.multi_line
506 }
507
508 pub fn is_code_editor(&self) -> bool {
509 self.code_editor
510 }
511
512 pub fn text_align(&self) -> TextAlign {
513 self.text_align
514 }
515
516 pub fn placeholder(&self) -> &SharedString {
517 &self.placeholder
518 }
519
520 pub fn mask_placeholder(&self) -> Option<&str> {
522 self.mask_placeholder.as_deref()
523 }
524}
525
526impl<M: InputModeKind> EventEmitter<InputEvent> for InputBaseState<M> {}
527
528impl<M: InputModeKind> InputBaseState<M> {
529 #[doc(hidden)]
530 pub fn cursor_layout(&self) -> Option<(Bounds<Pixels>, Pixels)> {
531 let layout = self.last_layout.as_ref()?;
532 Some((layout.cursor_bounds?, layout.line_height))
533 }
534
535 pub fn input_bounds(&self) -> Bounds<Pixels> {
536 self.input_bounds
537 }
538
539 pub fn text_bounds(&self) -> Option<Bounds<Pixels>> {
540 self.last_bounds
541 }
542
543 pub fn diagnostic_popover(&self) -> Option<Rc<crate::input::DiagnosticEntry>> {
544 self.diagnostic_popover.clone()
545 }
546
547 pub fn presentation(&self) -> InputPresentation {
548 InputPresentation {
549 focus_handle: self.focus_handle.clone(),
550 disabled: self.disabled,
551 readonly: self.readonly,
552 loading: self.loading,
553 masked: self.masked,
554 multi_line: self.is_multi_line(),
555 code_editor: self.is_code_editor(),
556 text_align: self.text_align,
557 placeholder: self.placeholder.clone(),
558 mask_placeholder: self.mask_pattern.placeholder(),
559 }
560 }
561
562 #[inline]
567 pub(crate) fn shows_scrollbar(&self) -> bool {
575 self.is_multi_line()
576 }
577
578 pub fn is_multi_line(&self) -> bool {
579 M::MULTI_LINE
580 }
581
582 #[inline]
584 pub fn is_single_line(&self) -> bool {
585 !M::MULTI_LINE
586 }
587
588 #[inline]
590 pub fn is_code_editor(&self) -> bool {
591 M::CODE_EDITOR
592 }
593
594 pub fn is_copyable(&self) -> bool {
598 self.selections.iter().any(|sel| !sel.is_empty()) && !self.masked
599 }
600
601 pub fn context_menu_capabilities(&self) -> InputContextMenuCapabilities {
602 let (go_to_definition, code_actions) = self.extras.context_menu_capabilities();
603 InputContextMenuCapabilities::new()
604 .disabled(self.disabled)
605 .readonly(self.readonly)
606 .code_editor(self.is_code_editor())
607 .selection(!self.active_selection().is_empty())
608 .masked(self.masked)
609 .go_to_definition(go_to_definition)
610 .code_actions(code_actions)
611 }
612
613 pub fn set_text_align(&mut self, text_align: TextAlign, cx: &mut Context<Self>) {
614 if !self.is_single_line() || self.text_align == text_align {
615 return;
616 }
617
618 self.text_align = text_align;
619 cx.notify();
620 }
621
622 pub fn toggle_masked(&mut self, _: &mut Window, cx: &mut Context<Self>) {
628 self.masked = !self.masked;
629 cx.notify();
630 }
631
632 pub fn on_context_menu(
633 &mut self,
634 handler: Rc<
635 dyn Fn(NativeMenu, InputContextMenuCapabilities, Point<Pixels>, &mut Window, &mut App),
636 >,
637 ) {
638 self.context_menu_handler = Some(handler);
639 }
640
641 fn new_in_mode(window: &mut Window, cx: &mut Context<Self>) -> Self {
643 let focus_handle = cx.focus_handle().tab_stop(true);
644 let blink_cursor = cx.new(|_| BlinkCursor::new());
645 let undo_manager = UndoManager::new();
646
647 let _subscriptions = vec![
648 cx.intercept_keystrokes({
651 let focus_handle = focus_handle.clone();
652 let blink_cursor = blink_cursor.downgrade();
653 move |_, window, cx| {
654 if focus_handle.is_focused(window) {
655 _ = blink_cursor.update(cx, |cursor, cx| cursor.pause(cx));
656 }
657 }
658 }),
659 cx.observe(&blink_cursor, |_, _, cx| cx.notify()),
661 cx.observe_window_activation(window, |input, window, cx| {
663 if window.is_window_active() {
664 let focus_handle = input.focus_handle.clone();
665 if focus_handle.is_focused(window) {
666 input.blink_cursor.update(cx, |blink_cursor, cx| {
667 blink_cursor.start(cx);
668 });
669 }
670 }
671 }),
672 cx.on_focus(&focus_handle, window, Self::on_focus),
673 cx.on_blur(&focus_handle, window, Self::on_blur),
674 ];
675
676 let text_style = window.text_style();
677
678 Self {
679 extras: M::Extras::default(),
680 focus_handle: focus_handle.clone(),
681 text: "".into(),
682 display_map: DisplayMap::new(text_style.font(), window.rem_size(), None),
683 search_session: super::SearchSession::default(),
684 search_activation_revision: 0,
685 searchable: false,
686 replaceable: true,
687 soft_wrap: true,
688 wrapping_indent: WrappingIndent::default(),
689 scroll_beyond_last_line: None,
690 cursor_surrounding_lines: None,
691 blink_cursor,
692 undo_manager,
693 selections: Selections::default(),
694 selected_word_range: None,
695 ime_marked_range: None,
696 input_bounds: Bounds::default(),
697 selecting: false,
698 disabled: false,
699 readonly: false,
700 text_align: TextAlign::Left,
701 masked: false,
702 clean_on_escape: false,
703 submit_on_enter: false,
704 show_whitespaces: false,
705 loading: false,
706 pattern: None,
707 validate: None,
708 number_step: Some(NumberStep::Fixed(1.)),
709 number_min: None,
710 number_max: None,
711 mode: LayoutMode::default(),
712 last_layout: None,
713 last_bounds: None,
714 last_selected_range: None,
715 column_select_start: None,
716 last_cursor: None,
717 scroll_handle: ScrollHandle::new(),
718 scroll_size: gpui::size(px(0.), px(0.)),
719 editor_scrollbar_snapshot: Cell::new(None),
720 editor_paddings: Edges::default(),
721 deferred_scroll_offset: None,
722 placeholder: SharedString::default(),
723 mask_pattern: MaskPattern::default(),
724 mask_pattern_set: false,
725 editor_style: InputEditorStyle::default(),
726 projected_editor_style: InputEditorStyle::default(),
727 diagnostic_popover: None,
728 context_menu_handler: None,
729 pending_context_menu: None,
730 enable_context_menu: true,
731 completion_inserting: false,
732 overlay_action_handler: None,
733 silent_replace_text: false,
734 emit_events: true,
735 _subscriptions,
736 _pending_update: false,
737 cursor_line_end_affinity: false,
738 auto_scroll: AutoScroll::default(),
739 }
740 }
741
742 pub fn context_menu(mut self, enable: bool) -> Self {
747 self.enable_context_menu = enable;
748 self
749 }
750
751 pub fn set_context_menu_enabled(&mut self, enabled: bool) {
752 self.enable_context_menu = enabled;
753 }
754
755 #[doc(hidden)]
757 pub fn replaceable(mut self, allow: bool) -> Self {
758 self.replaceable = allow;
759 self
760 }
761
762 pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
764 self.placeholder = placeholder.into();
765 self
766 }
767
768 pub fn set_highlighter(
770 &mut self,
771 new_language: impl Into<SharedString>,
772 cx: &mut Context<Self>,
773 ) {
774 match &mut self.mode {
775 LayoutMode::CodeEditor {
776 language,
777 highlighter,
778 ..
779 } => {
780 language.set_name(new_language.into());
781 *highlighter.borrow_mut() = None;
782 }
783 _ => {}
784 }
785 self.refresh(cx);
786 }
787
788 fn reset_highlighter(&mut self, cx: &mut Context<Self>) {
789 match &mut self.mode {
790 LayoutMode::CodeEditor { highlighter, .. } => {
791 *highlighter.borrow_mut() = None;
792 }
793 _ => {}
794 }
795 cx.notify();
796 }
797
798 pub fn set_highlighter_factory(
800 &mut self,
801 factory: InputHighlighterFactory,
802 cx: &mut Context<Self>,
803 ) {
804 self.mode.set_highlighter_factory(factory);
805 self._pending_update = true;
806 cx.notify();
807 }
808
809 pub fn ensure_highlighter_factory(&mut self, factory: InputHighlighterFactory) {
811 self.mode.ensure_highlighter_factory(factory);
812 }
813
814 pub fn set_editor_style(&mut self, style: InputEditorStyle) {
815 self.editor_style = style.clone();
816 self.projected_editor_style = style;
817 }
818
819 #[doc(hidden)]
821 pub fn set_editor_paddings(&mut self, paddings: Edges<Pixels>) {
822 self.editor_paddings = paddings;
823 }
824
825 pub fn apply_highlighter_fold_candidates(
826 &mut self,
827 candidates: Vec<crate::input::FoldRange>,
828 cx: &mut Context<Self>,
829 ) {
830 if self.mode.is_folding() {
831 self.display_map.set_fold_candidates(candidates);
832 }
833 cx.notify();
834 }
835
836 #[inline]
837 pub fn diagnostics(&self) -> Option<&DiagnosticSet> {
838 self.mode.diagnostics()
839 }
840
841 #[inline]
842 pub fn diagnostics_mut(&mut self) -> Option<&mut DiagnosticSet> {
843 self.mode.diagnostics_mut()
844 }
845
846 pub fn set_placeholder(
848 &mut self,
849 placeholder: impl Into<SharedString>,
850 _: &mut Window,
851 cx: &mut Context<Self>,
852 ) {
853 self.placeholder = placeholder.into();
854 cx.notify();
855 }
856
857 pub(super) fn line_and_position_for_offset(
865 &self,
866 offset: usize,
867 ) -> (usize, usize, Option<Point<Pixels>>) {
868 let Some(last_layout) = &self.last_layout else {
869 return (0, 0, None);
870 };
871 let line_height = last_layout.line_height;
872
873 let mut y_offset = last_layout.visible_top;
874 for (vi, line) in last_layout.lines.iter().enumerate() {
875 let prev_lines_offset = last_layout.visible_line_byte_offsets[vi];
876 let local_offset = offset.saturating_sub(prev_lines_offset);
877 if let Some(pos) = line.position_for_index(local_offset, last_layout, false) {
878 let sub_line_index = (pos.y / line_height) as usize;
879 let adjusted_pos = point(pos.x + last_layout.line_number_width, pos.y + y_offset);
880 return (vi, sub_line_index, Some(adjusted_pos));
881 }
882
883 y_offset += line.size(line_height).height;
884 }
885 (0, 0, None)
886 }
887
888 pub fn set_value(
895 &mut self,
896 value: impl Into<SharedString>,
897 window: &mut Window,
898 cx: &mut Context<Self>,
899 ) {
900 self.undo_manager.set_ignoring(true);
901 self.emit_events = false;
902 self.replace_text(value, window, cx);
903 self.undo_manager.set_ignoring(false);
904 self.emit_events = true;
905
906 self.reset_selection();
907 self.reset_lsp_state();
908 self.reset_scroll_to_start();
909
910 self.undo_manager.clear();
911 cx.notify();
912 }
913
914 pub fn replace_all(
924 &mut self,
925 text: impl Into<SharedString>,
926 window: &mut Window,
927 cx: &mut Context<Self>,
928 ) {
929 self.replace_text(text, window, cx);
930 self.reset_selection();
931 self.reset_lsp_state();
932 self.reset_scroll_to_start();
933
934 cx.notify();
935 }
936
937 fn with_edits_allowed(&mut self, f: impl FnOnce(&mut Self)) {
942 let (was_disabled, was_readonly) = (self.disabled, self.readonly);
943 (self.disabled, self.readonly) = (false, false);
944 f(self);
945 (self.disabled, self.readonly) = (was_disabled, was_readonly);
946 }
947
948 pub fn insert(
952 &mut self,
953 text: impl Into<SharedString>,
954 window: &mut Window,
955 cx: &mut Context<Self>,
956 ) {
957 let text: SharedString = text.into();
958 self.with_edits_allowed(|this| {
959 this.undo_manager.set_pending_intent(EditIntent::Atomic);
960 let range_utf16 = this.range_to_utf16(&(this.cursor()..this.cursor()));
961 this.replace_text_in_range_silent(Some(range_utf16), &text, window, cx);
962 let end = this.active_selection().end;
963 this.set_cursor_to(end);
964 });
965 }
966
967 pub fn replace(
971 &mut self,
972 text: impl Into<SharedString>,
973 window: &mut Window,
974 cx: &mut Context<Self>,
975 ) {
976 let text: SharedString = text.into();
977 self.with_edits_allowed(|this| {
978 this.undo_manager.set_pending_intent(EditIntent::Atomic);
979 this.replace_text_in_range_silent(None, &text, window, cx);
980 let end = this.active_selection().end;
981 this.set_cursor_to(end);
982 });
983 }
984
985 fn replace_text(
986 &mut self,
987 text: impl Into<SharedString>,
988 window: &mut Window,
989 cx: &mut Context<Self>,
990 ) {
991 let text: SharedString = text.into();
992 self.with_edits_allowed(|this| {
993 this.undo_manager.set_pending_intent(EditIntent::Atomic);
994 let range = 0..this.text.chars().map(|c| c.len_utf16()).sum();
995 this.replace_text_in_range_silent(Some(range), &text, window, cx);
996 this.reset_highlighter(cx);
997 });
998 }
999
1000 fn reset_selection(&mut self) {
1001 self.selections.remove_all_but_active();
1002
1003 if self.is_single_line() {
1007 let end = self.text.len();
1008 self.set_cursor_to(end);
1009 } else {
1010 self.active_selection_mut().clear();
1011 }
1012 }
1013
1014 fn reset_lsp_state(&mut self) {
1015 if self.is_code_editor() {
1016 self._pending_update = true;
1017 M::reset_language_features(self);
1018 }
1019 }
1020
1021 fn reset_scroll_to_start(&mut self) {
1022 self.scroll_handle.set_offset(point(px(0.), px(0.)));
1026 if self.is_single_line() {
1027 self.deferred_scroll_offset = Some(point(px(0.), px(0.)));
1028 }
1029 }
1030
1031 #[allow(unused)]
1035 pub(crate) fn disabled(mut self, disabled: bool) -> Self {
1036 self.disabled = disabled;
1037 self
1038 }
1039
1040 pub fn set_disabled(&mut self, disabled: bool, cx: &mut Context<Self>) {
1041 if self.disabled == disabled {
1042 return;
1043 }
1044
1045 self.disabled = disabled;
1046 cx.notify();
1047 }
1048
1049 #[allow(unused)]
1057 pub(crate) fn readonly(mut self, readonly: bool) -> Self {
1058 self.readonly = readonly;
1059 self
1060 }
1061
1062 pub fn set_readonly(&mut self, readonly: bool, cx: &mut Context<Self>) {
1063 if self.readonly == readonly {
1064 return;
1065 }
1066
1067 self.readonly = readonly;
1068 if readonly {
1069 self.search_session.replace_mode = false;
1070 }
1071 cx.notify();
1072 }
1073
1074 pub fn is_editable(&self) -> bool {
1079 !self.disabled && !self.readonly
1080 }
1081
1082 pub fn clean_on_escape(mut self) -> Self {
1084 self.clean_on_escape = true;
1085 self
1086 }
1087
1088 pub fn set_clean_on_escape(&mut self, clean: bool) {
1089 self.clean_on_escape = clean;
1090 }
1091
1092 #[doc(hidden)]
1097 pub fn submit_on_enter(mut self, submit: bool) -> Self {
1098 self.submit_on_enter = submit;
1099 self
1100 }
1101
1102 pub fn set_submit_on_enter(&mut self, submit: bool, cx: &mut Context<Self>) {
1103 self.submit_on_enter = submit;
1104 cx.notify();
1105 }
1106
1107 #[doc(hidden)]
1109 pub fn show_whitespaces(mut self, show: bool) -> Self {
1110 self.show_whitespaces = show;
1111 self
1112 }
1113
1114 pub fn set_show_whitespaces(&mut self, show: bool, _: &mut Window, cx: &mut Context<Self>) {
1116 self.show_whitespaces = show;
1117 cx.notify();
1118 }
1119
1120 pub fn scroll_beyond_last_line(mut self, rows: Option<usize>) -> Self {
1130 self.scroll_beyond_last_line = rows;
1131 self
1132 }
1133
1134 pub fn set_scroll_beyond_last_line(
1136 &mut self,
1137 rows: Option<usize>,
1138 _: &mut Window,
1139 cx: &mut Context<Self>,
1140 ) {
1141 if self.scroll_beyond_last_line == rows {
1142 return;
1143 }
1144 self.scroll_beyond_last_line = rows;
1145 cx.notify();
1146 }
1147
1148 pub fn cursor_surrounding_lines(mut self, lines: Option<usize>) -> Self {
1159 self.cursor_surrounding_lines = lines;
1160 self
1161 }
1162
1163 pub fn set_cursor_surrounding_lines(
1165 &mut self,
1166 lines: Option<usize>,
1167 _: &mut Window,
1168 cx: &mut Context<Self>,
1169 ) {
1170 if self.cursor_surrounding_lines == lines {
1171 return;
1172 }
1173 self.cursor_surrounding_lines = lines;
1174 cx.notify();
1175 }
1176
1177 pub fn default_value(mut self, value: impl Into<SharedString>) -> Self {
1179 let text: SharedString = value.into();
1180 self.text = Rope::from(self.normalize_input(&text).as_ref());
1181 if let Some(diagnostics) = self.mode.diagnostics_mut() {
1182 diagnostics.reset(&self.text)
1183 }
1184 self._pending_update = true;
1187 self
1188 }
1189
1190 pub fn value(&self) -> SharedString {
1195 SharedString::new(self.text.to_string())
1196 }
1197
1198 pub fn selected_value(&self) -> SharedString {
1204 SharedString::new(self.selected_text().to_string())
1205 }
1206
1207 pub fn unmask_value(&self) -> SharedString {
1209 self.mask_pattern.unmask(&self.text.to_string()).into()
1210 }
1211
1212 #[doc(hidden)]
1218 pub fn prepare(&mut self, _: &mut Window, _: &mut Context<Self>) {}
1219
1220 pub fn text(&self) -> &Rope {
1225 &self.text
1226 }
1227
1228 pub fn cursor_position(&self) -> Position {
1230 let offset = self.cursor();
1231 self.text.offset_to_position(offset)
1232 }
1233
1234 pub fn set_cursor_position(
1238 &mut self,
1239 position: impl Into<Position>,
1240 window: &mut Window,
1241 cx: &mut Context<Self>,
1242 ) {
1243 let position: Position = position.into();
1244 let offset = self.text.position_to_offset(&position);
1245
1246 self.move_to(offset, None, cx);
1247 self.update_preferred_column();
1248 self.focus(window, cx);
1249 }
1250
1251 pub fn focus(&self, window: &mut Window, cx: &mut Context<Self>) {
1253 self.focus_handle.focus(window, cx);
1254 self.blink_cursor.update(cx, |cursor, cx| {
1255 cursor.start(cx);
1256 });
1257 }
1258
1259 pub fn refresh(&mut self, cx: &mut Context<Self>) {
1273 self._pending_update = true;
1274 cx.notify();
1275 }
1276
1277 pub(super) fn select_left(&mut self, _: &SelectLeft, _: &mut Window, cx: &mut Context<Self>) {
1278 self.undo_manager.break_transaction_coalescing();
1279 self.select_all_cursors_to(|s, sel| s.previous_boundary(sel.cursor_offset()), cx);
1280 }
1281
1282 pub(super) fn select_right(&mut self, _: &SelectRight, _: &mut Window, cx: &mut Context<Self>) {
1283 self.select_all_cursors_to(|s, sel| s.next_boundary(sel.cursor_offset()), cx);
1284 }
1285
1286 pub(super) fn select_up(&mut self, _: &SelectUp, _: &mut Window, cx: &mut Context<Self>) {
1287 if self.is_single_line() {
1288 return;
1289 }
1290 self.undo_manager.break_transaction_coalescing();
1291 self.select_all_cursors_to(
1292 |s, sel| {
1293 let offset = s
1294 .start_of_line_at(sel.cursor_offset(), s.line_end_affinity_for(sel))
1295 .saturating_sub(1);
1296 s.previous_boundary(offset)
1297 },
1298 cx,
1299 );
1300 }
1301
1302 pub(super) fn select_down(&mut self, _: &SelectDown, _: &mut Window, cx: &mut Context<Self>) {
1303 if self.is_single_line() {
1304 return;
1305 }
1306 self.undo_manager.break_transaction_coalescing();
1307 let len = self.text.len();
1308 self.select_all_cursors_to(
1309 |s, sel| {
1310 let offset = (s.end_of_line_at(sel.cursor_offset(), s.line_end_affinity_for(sel))
1311 + 1)
1312 .min(len);
1313 s.next_boundary(offset)
1314 },
1315 cx,
1316 );
1317 }
1318
1319 pub(super) fn on_action_select_all(
1320 &mut self,
1321 _: &SelectAll,
1322 window: &mut Window,
1323 cx: &mut Context<Self>,
1324 ) {
1325 self.select_all(window, cx);
1326 }
1327
1328 pub(super) fn select_to_start(
1329 &mut self,
1330 _: &SelectToStart,
1331 _: &mut Window,
1332 cx: &mut Context<Self>,
1333 ) {
1334 self.undo_manager.break_transaction_coalescing();
1335 self.select_all_cursors_to(|_, _| 0, cx);
1336 }
1337
1338 pub(super) fn select_to_end(
1339 &mut self,
1340 _: &SelectToEnd,
1341 _: &mut Window,
1342 cx: &mut Context<Self>,
1343 ) {
1344 self.undo_manager.break_transaction_coalescing();
1345 let end = self.text.len();
1346 self.select_all_cursors_to(move |_, _| end, cx);
1347 }
1348
1349 pub(super) fn select_to_start_of_line(
1350 &mut self,
1351 _: &SelectToStartOfLine,
1352 _: &mut Window,
1353 cx: &mut Context<Self>,
1354 ) {
1355 self.undo_manager.break_transaction_coalescing();
1356 self.select_all_cursors_to(
1357 |s, sel| s.start_of_line_at(sel.cursor_offset(), s.line_end_affinity_for(sel)),
1358 cx,
1359 );
1360 }
1361
1362 pub(super) fn select_to_end_of_line(
1363 &mut self,
1364 _: &SelectToEndOfLine,
1365 _: &mut Window,
1366 cx: &mut Context<Self>,
1367 ) {
1368 self.undo_manager.break_transaction_coalescing();
1369 self.select_all_cursors_to(
1370 |s, sel| s.end_of_line_at(sel.cursor_offset(), s.line_end_affinity_for(sel)),
1371 cx,
1372 );
1373 self.cursor_line_end_affinity = true;
1375 }
1376
1377 pub(super) fn select_to_previous_word(
1378 &mut self,
1379 _: &SelectToPreviousWordStart,
1380 _: &mut Window,
1381 cx: &mut Context<Self>,
1382 ) {
1383 self.undo_manager.break_transaction_coalescing();
1384 self.select_all_cursors_to(
1385 |s, sel| s.previous_start_of_word_at(sel.cursor_offset()),
1386 cx,
1387 );
1388 }
1389
1390 pub(super) fn select_to_next_word(
1391 &mut self,
1392 _: &SelectToNextWordEnd,
1393 _: &mut Window,
1394 cx: &mut Context<Self>,
1395 ) {
1396 self.undo_manager.break_transaction_coalescing();
1397 self.select_all_cursors_to(|s, sel| s.next_end_of_word_at(sel.cursor_offset()), cx);
1398 }
1399
1400 pub(super) fn previous_start_of_word_at(&self, offset: usize) -> usize {
1403 if self.masked {
1404 return 0;
1408 }
1409
1410 let offset = self.offset_from_utf16(self.offset_to_utf16(offset));
1411 let left_part = self.text.slice(0..offset).to_string();
1413
1414 UnicodeSegmentation::split_word_bound_indices(left_part.as_str())
1415 .rfind(|(_, s)| !s.trim_start().is_empty())
1416 .map(|(i, _)| i)
1417 .unwrap_or(0)
1418 }
1419
1420 pub(super) fn next_end_of_word_at(&self, offset: usize) -> usize {
1422 if self.masked {
1423 return self.text.len();
1425 }
1426
1427 let offset = self.offset_from_utf16(self.offset_to_utf16(offset));
1428 let right_part = self.text.slice(offset..self.text.len()).to_string();
1429
1430 UnicodeSegmentation::split_word_bound_indices(right_part.as_str())
1431 .find(|(_, s)| !s.trim_start().is_empty())
1432 .map(|(i, s)| offset + i + s.len())
1433 .unwrap_or(self.text.len())
1434 }
1435
1436 pub(super) fn start_of_line_at(&self, offset: usize, line_end_affinity: bool) -> usize {
1441 if self.is_single_line() {
1442 return 0;
1443 }
1444
1445 let row = self.text.offset_to_point(offset).row;
1446 let logical_start = self.text.line_start_offset(row);
1447
1448 if self.soft_wrap && self.is_code_editor() {
1449 let wrap_point = self
1450 .display_map
1451 .offset_to_wrap_display_point_with_affinity(offset, line_end_affinity);
1452 if let Some(line) = self.display_map.line(row)
1453 && let Some(range) = line.wrapped_lines.get(wrap_point.local_row)
1454 {
1455 let visual_start = logical_start + range.start;
1456 if offset != visual_start {
1457 return visual_start;
1458 }
1459 }
1460 }
1461
1462 logical_start
1463 }
1464
1465 pub(super) fn end_of_line_at(&self, offset: usize, line_end_affinity: bool) -> usize {
1470 if self.is_single_line() {
1471 return self.text.len();
1472 }
1473
1474 let row = self.text.offset_to_point(offset).row;
1475 let logical_start = self.text.line_start_offset(row);
1476 let logical_end = self.text.line_end_offset(row);
1477
1478 if self.soft_wrap && self.is_code_editor() {
1479 let wrap_point = self
1483 .display_map
1484 .offset_to_wrap_display_point_with_affinity(offset, line_end_affinity);
1485 if let Some(line) = self.display_map.line(row)
1486 && let Some(range) = line.wrapped_lines.get(wrap_point.local_row)
1487 {
1488 let visual_end = logical_start + range.end;
1489 if offset != visual_end {
1490 return visual_end;
1491 }
1492 }
1493 }
1494
1495 logical_end
1496 }
1497
1498 pub(super) fn indent_of_next_line(&mut self) -> String {
1502 self.indent_of_next_line_at(self.cursor())
1503 }
1504
1505 pub(super) fn indent_of_next_line_at(&mut self, offset: usize) -> String {
1507 if self.is_single_line() {
1508 return "".into();
1509 }
1510
1511 let mut current_indent = String::new();
1512 let mut next_indent = String::new();
1513 let line_end_affinity = self.line_end_affinity_at(offset);
1514 let current_line_start_pos = self.start_of_line_at(offset, line_end_affinity);
1515 let next_line_start_pos = self.end_of_line_at(offset, line_end_affinity);
1516 for c in self.text.slice(current_line_start_pos..).chars() {
1517 if !c.is_whitespace() {
1518 break;
1519 }
1520 if c == '\n' || c == '\r' {
1521 break;
1522 }
1523 current_indent.push(c);
1524 }
1525
1526 for c in self.text.slice(next_line_start_pos..).chars() {
1527 if !c.is_whitespace() {
1528 break;
1529 }
1530 if c == '\n' || c == '\r' {
1531 break;
1532 }
1533 next_indent.push(c);
1534 }
1535
1536 if next_indent.len() > current_indent.len() {
1537 return next_indent;
1538 }
1539
1540 if let Some(rules) = self.mode.language_config() {
1543 if self.mode.is_smart_indent()
1544 && M::editing_syntax_context(self, offset) == crate::input::SyntaxContext::Code
1545 {
1546 let line_before: String = self
1547 .text
1548 .slice(current_line_start_pos..offset)
1549 .chars()
1550 .collect();
1551 if rules.opens_indent(line_before.trim_end()) {
1552 let tab = self.mode.tab_size();
1553 if tab.hard_tabs {
1554 current_indent.push('\t');
1555 } else {
1556 for _ in 0..tab.tab_size {
1557 current_indent.push(' ');
1558 }
1559 }
1560 } else {
1561 let line_after = self.text.slice(offset..next_line_start_pos).to_string();
1562 if rules.closes_indent(&line_after) {
1563 let tab = self.mode.tab_size();
1564 if current_indent.ends_with('\t') {
1565 current_indent.pop();
1566 } else {
1567 for _ in 0..tab.tab_size {
1568 if !current_indent.ends_with(' ') {
1569 break;
1570 }
1571 current_indent.pop();
1572 }
1573 }
1574 }
1575 }
1576 }
1577 }
1578
1579 current_indent
1580 }
1581
1582 fn delete_selections(
1590 &mut self,
1591 silent: bool,
1592 collapsed_intent: EditIntent,
1593 mut collapsed_target: impl FnMut(&mut Self, usize) -> Range<usize>,
1594 window: &mut Window,
1595 cx: &mut Context<Self>,
1596 ) {
1597 if !self.is_editable() {
1598 return;
1599 }
1600 let cursors: Vec<CursorSelection> = self.selections.iter().copied().collect();
1601 let intent = if cursors.iter().all(|sel| sel.is_empty()) {
1602 collapsed_intent
1603 } else {
1604 EditIntent::Atomic
1605 };
1606 let mut new_selections: Vec<CursorSelection> = Vec::with_capacity(cursors.len());
1607 for sel in &cursors {
1608 let range = if sel.is_empty() {
1609 collapsed_target(self, sel.cursor_offset())
1610 } else {
1611 sel.start..sel.end
1612 };
1613 let (start, end) = (range.start.min(range.end), range.start.max(range.end));
1614 let mut selection = *sel;
1615 selection.start = start;
1616 selection.end = end;
1617 new_selections.push(selection);
1618 }
1619 self.undo_manager.begin_transaction_with(intent);
1622 self.undo_manager
1623 .record_selections(cursors.clone(), cursors.clone());
1624 self.selections.replace_all(new_selections);
1625 self.undo_manager.set_pending_intent(intent);
1626
1627 let was_silent = self.silent_replace_text;
1628 self.silent_replace_text = silent;
1629 self.replace_text_in_range(None, "", window, cx);
1630 self.silent_replace_text = was_silent;
1631 self.undo_manager
1632 .record_selections(cursors, self.selections.iter().copied().collect());
1633 self.undo_manager.commit_transaction();
1634 self.pause_blink_cursor(cx);
1635 }
1636
1637 fn auto_close_target(&self, range: &Range<usize>, text: &str) -> Option<(usize, SharedString)> {
1640 if self.silent_replace_text
1641 || self.ime_marked_range.is_some()
1642 || !self.selections.is_single()
1643 || !self.active_selection().is_empty()
1644 || !range.is_empty()
1645 || range.start != self.cursor()
1646 || text.chars().count() != 1
1647 || !self.mode.is_auto_close()
1648 {
1649 return None;
1650 }
1651 let rules = self.mode.language_config()?;
1652 if self
1653 .text
1654 .chars_at(range.start)
1655 .next()
1656 .is_some_and(|c| !c.is_whitespace() && !rules.auto_close_before.contains(c))
1657 {
1658 return None;
1659 }
1660 for (open, close, not_in) in rules.closing_pairs() {
1661 let Some(prefix) = open.strip_suffix(text) else {
1662 continue;
1663 };
1664 if !self.text_before_matches(range.start, prefix) {
1665 continue;
1666 }
1667 let start = range.start - prefix.len();
1668 if open == close
1669 && (self.is_escaped_at(start)
1670 || self
1671 .text
1672 .chars_at(start)
1673 .reversed()
1674 .next()
1675 .is_some_and(|c| c.is_alphanumeric() || c == '_'))
1676 {
1677 continue;
1678 }
1679 if not_in.contains(&M::editing_syntax_context(self, start)) {
1680 continue;
1681 }
1682 return Some((open.len(), close.into()));
1683 }
1684 None
1685 }
1686
1687 fn text_before_matches(&self, offset: usize, text: &str) -> bool {
1688 offset >= text.len()
1689 && self.text.is_char_boundary(offset - text.len())
1690 && self.text.slice(offset - text.len()..offset) == text
1691 }
1692
1693 fn text_after_matches(&self, offset: usize, text: &str) -> bool {
1694 offset + text.len() <= self.text.len()
1695 && self.text.is_char_boundary(offset + text.len())
1696 && self.text.slice(offset..offset + text.len()) == text
1697 }
1698
1699 fn is_escaped_at(&self, offset: usize) -> bool {
1700 self.text
1701 .chars_at(offset)
1702 .reversed()
1703 .take_while(|c| *c == '\\')
1704 .count()
1705 % 2
1706 == 1
1707 }
1708
1709 fn skip_over_target(&self, new_text: &str) -> Option<usize> {
1716 if !self.mode.is_auto_close() || new_text.chars().count() != 1 {
1717 return None;
1718 }
1719 let rules = self.mode.language_config()?;
1720 let cursor = self.cursor();
1721 for (open, close, not_in) in rules.closing_pairs() {
1722 for (index, _) in close.char_indices() {
1723 if !close[index..].starts_with(new_text)
1724 || !self.text_before_matches(cursor, &close[..index])
1725 || !self.text_after_matches(cursor, &close[index..])
1726 {
1727 continue;
1728 }
1729 let context = M::editing_syntax_context(self, cursor);
1730 let generated =
1731 self.mode
1732 .auto_closed_pairs()
1733 .contains_closer(cursor, index, close.len());
1734 if !generated
1735 && not_in.contains(&context)
1736 && !(context == crate::input::SyntaxContext::String && open == close)
1737 {
1738 continue;
1739 }
1740 if self.is_escaped_at(cursor - index) {
1741 continue;
1742 }
1743 return Some(cursor + new_text.len());
1744 }
1745 }
1746 None
1747 }
1748
1749 pub(super) fn backspace(&mut self, _: &Backspace, window: &mut Window, cx: &mut Context<Self>) {
1750 if self.selections.is_single() && self.active_selection().is_empty() && self.cursor() == 0 {
1756 cx.propagate();
1757 return;
1758 }
1759
1760 if self.mode.is_auto_close()
1763 && self.selections.is_single()
1764 && self.active_selection().is_empty()
1765 {
1766 let off = self.cursor();
1767 let deletion = self.mode.language_config().and_then(|rules| {
1768 rules.closing_pairs().find_map(|(open, close, not_in)| {
1769 if !self.text_before_matches(off, open) || !self.text_after_matches(off, close)
1770 {
1771 return None;
1772 }
1773 let start = off - open.len();
1774 let generated = self
1775 .mode
1776 .auto_closed_pairs()
1777 .contains(start..off, off..off + close.len());
1778 if self.is_escaped_at(start)
1779 || (!generated && not_in.contains(&M::editing_syntax_context(self, start)))
1780 {
1781 return None;
1782 }
1783 Some(start..off + close.len())
1784 })
1785 });
1786 if let Some(range) = deletion {
1787 let utf16 = self.range_to_utf16(&range);
1788 self.replace_text_in_range_silent(Some(utf16), "", window, cx);
1789 return;
1790 }
1791 }
1792
1793 self.delete_selections(
1794 false,
1795 EditIntent::Backspace,
1796 |s, offset| s.previous_boundary(offset)..offset,
1797 window,
1798 cx,
1799 );
1800 }
1801
1802 pub(super) fn delete(&mut self, _: &Delete, window: &mut Window, cx: &mut Context<Self>) {
1803 self.delete_selections(
1804 false,
1805 EditIntent::DeleteForward,
1806 |s, offset| offset..s.next_boundary(offset),
1807 window,
1808 cx,
1809 );
1810 }
1811
1812 pub(super) fn delete_to_beginning_of_line(
1813 &mut self,
1814 _: &DeleteToBeginningOfLine,
1815 window: &mut Window,
1816 cx: &mut Context<Self>,
1817 ) {
1818 self.delete_selections(
1819 true,
1820 EditIntent::Atomic,
1821 |s, offset| {
1822 let mut start = s.start_of_line_at(offset, s.line_end_affinity_at(offset));
1823 if start == offset {
1824 start = start.saturating_sub(1);
1825 }
1826 start..offset
1827 },
1828 window,
1829 cx,
1830 );
1831 }
1832
1833 pub(super) fn delete_to_end_of_line(
1834 &mut self,
1835 _: &DeleteToEndOfLine,
1836 window: &mut Window,
1837 cx: &mut Context<Self>,
1838 ) {
1839 self.delete_selections(
1840 true,
1841 EditIntent::Atomic,
1842 |s, offset| {
1843 let mut end = s.end_of_line_at(offset, s.line_end_affinity_at(offset));
1844 if end == offset {
1845 end = (end + 1).clamp(0, s.text.len());
1846 }
1847 offset..end
1848 },
1849 window,
1850 cx,
1851 );
1852 }
1853
1854 pub(super) fn delete_previous_word(
1855 &mut self,
1856 _: &DeleteToPreviousWordStart,
1857 window: &mut Window,
1858 cx: &mut Context<Self>,
1859 ) {
1860 self.delete_selections(
1861 true,
1862 EditIntent::Atomic,
1863 |s, offset| s.previous_start_of_word_at(offset)..offset,
1864 window,
1865 cx,
1866 );
1867 }
1868
1869 pub(super) fn delete_next_word(
1870 &mut self,
1871 _: &DeleteToNextWordEnd,
1872 window: &mut Window,
1873 cx: &mut Context<Self>,
1874 ) {
1875 self.delete_selections(
1876 true,
1877 EditIntent::Atomic,
1878 |s, offset| offset..s.next_end_of_word_at(offset),
1879 window,
1880 cx,
1881 );
1882 }
1883
1884 pub(super) fn enter(&mut self, action: &Enter, window: &mut Window, cx: &mut Context<Self>) {
1885 if M::handle_context_menu_action(self, Box::new(action.clone()), window, cx) {
1886 return;
1887 }
1888
1889 if M::has_inline_completion(self) {
1891 M::clear_inline_completion(self, cx);
1892 }
1893
1894 let insert_newline = self.is_multi_line() && (!self.submit_on_enter || action.shift);
1899
1900 if insert_newline {
1901 if !self.selections.is_single() {
1902 self.selections.merge_overlapping();
1904 let selections: Vec<CursorSelection> = self.selections.iter().copied().collect();
1905 let mut edits: Vec<(Range<usize>, String)> = Vec::with_capacity(selections.len());
1906 for sel in &selections {
1907 let indent = if self.is_code_editor() {
1908 self.indent_of_next_line_at(sel.cursor_offset())
1909 } else {
1910 String::new()
1911 };
1912 edits.push((sel.start..sel.end, format!("\n{}", indent)));
1913 }
1914 edits.sort_by_key(|(range, _)| range.start);
1915 self.replace_text_in_ranges(&edits, window, cx);
1916 self.pause_blink_cursor(cx);
1917 } else {
1918 let mut split = false;
1924 if self.is_code_editor() && self.active_selection().is_empty() {
1925 if let Some(rules) = self.mode.language_config() {
1926 if self.mode.is_smart_indent()
1927 && M::editing_syntax_context(self, self.cursor())
1928 == crate::input::SyntaxContext::Code
1929 {
1930 let off = self.cursor();
1931 let is_pair = rules.brackets.iter().any(|p| {
1932 !p.open.is_empty()
1933 && !p.close.is_empty()
1934 && p.open != p.close
1935 && self.text_before_matches(off, p.open.as_ref())
1936 && self.text_after_matches(off, p.close.as_ref())
1937 });
1938 if is_pair {
1939 let line_end_affinity = self.line_end_affinity_at(off);
1941 let line_start = self.start_of_line_at(off, line_end_affinity);
1942 let mut indent = String::new();
1943 for c in self.text.slice(line_start..).chars() {
1944 if !c.is_whitespace() || c == '\n' || c == '\r' {
1945 break;
1946 }
1947 indent.push(c);
1948 }
1949 let mut inner = indent.clone();
1951 if self.mode.is_smart_indent() {
1952 let tab = self.mode.tab_size();
1953 if tab.hard_tabs {
1954 inner.push('\t');
1955 } else {
1956 for _ in 0..tab.tab_size {
1957 inner.push(' ');
1958 }
1959 }
1960 }
1961 let new_line_text = format!("\n{inner}\n{indent}");
1962 self.replace_text_in_range_silent(None, &new_line_text, window, cx);
1963 self.set_cursor_to(off + 1 + inner.len());
1964 self.update_preferred_column();
1965 let cursors = self.selections.iter().copied().collect::<Vec<_>>();
1966 self.undo_manager
1967 .record_selections(cursors.clone(), cursors);
1968 self.pause_blink_cursor(cx);
1969 split = true;
1970 }
1971 }
1972 }
1973 }
1974 if !split {
1975 let indent = if self.is_code_editor() {
1977 self.indent_of_next_line()
1978 } else {
1979 "".to_string()
1980 };
1981
1982 let new_line_text = format!("\n{}", indent);
1984 self.replace_text_in_range_silent(None, &new_line_text, window, cx);
1985 self.pause_blink_cursor(cx);
1986 }
1987 }
1988 } else {
1989 self.undo_manager.break_transaction_coalescing();
1992 cx.propagate();
1993 }
1994
1995 cx.emit(InputEvent::PressEnter {
1996 secondary: action.secondary,
1997 shift: action.shift,
1998 });
1999 }
2000
2001 pub fn clean(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2002 self.replace_text("", window, cx);
2003 self.set_selection(0, 0);
2004 self.scroll_to(0, None, cx);
2005 }
2006
2007 pub(super) fn escape(&mut self, action: &Escape, window: &mut Window, cx: &mut Context<Self>) {
2008 if M::handle_context_menu_action(self, Box::new(action.clone()), window, cx) {
2009 return;
2010 }
2011
2012 if !self.selections.is_single() {
2014 self.undo_manager.break_transaction_coalescing();
2015 self.selections.remove_all_but_active();
2016 cx.notify();
2017 return;
2018 }
2019
2020 if M::has_inline_completion(self) {
2022 M::clear_inline_completion(self, cx);
2023 return; }
2025
2026 if self.ime_marked_range.is_some() {
2027 self.unmark_text(window, cx);
2028 }
2029
2030 if self.clean_on_escape {
2031 return self.clean(window, cx);
2032 }
2033
2034 cx.propagate();
2035 }
2036
2037 pub(crate) fn handle_right_click_menu(
2039 &mut self,
2040 position: Point<Pixels>,
2041 offset: usize,
2042 window: &mut Window,
2043 cx: &mut Context<Self>,
2044 ) {
2045 if self.disabled {
2046 return;
2047 }
2048 if crate::GlobalState::is_in_deferred_context(cx) {
2049 return;
2050 }
2051
2052 if !self.active_selection().contains(offset) {
2053 self.move_to(offset, None, cx);
2054 }
2055
2056 if self.is_code_editor() {
2057 M::on_hover_definition(self, offset, window, cx);
2058 }
2059
2060 if let Some(handler) = self.context_menu_handler.clone() {
2061 let capabilities = self.context_menu_capabilities();
2062 cx.defer_in(window, move |_, window, cx| {
2063 handler(NativeMenu::new(), capabilities, position, window, cx);
2064 });
2065 }
2066 }
2067
2068 pub(super) fn add_cursor_above(
2069 &mut self,
2070 _: &AddCursorAbove,
2071 _: &mut Window,
2072 cx: &mut Context<Self>,
2073 ) {
2074 self.add_cursor_vertical(-1, cx);
2075 }
2076
2077 pub(super) fn add_cursor_below(
2078 &mut self,
2079 _: &AddCursorBelow,
2080 _: &mut Window,
2081 cx: &mut Context<Self>,
2082 ) {
2083 self.add_cursor_vertical(1, cx);
2084 }
2085
2086 fn add_cursor_vertical(&mut self, move_lines: isize, cx: &mut Context<Self>) {
2091 if !self.is_multi_line() {
2092 return;
2093 }
2094
2095 self.pause_blink_cursor(cx);
2096 self.undo_manager.break_transaction_coalescing();
2098
2099 let sources: Vec<(usize, Option<(Pixels, usize)>, bool)> = self
2100 .selections
2101 .iter()
2102 .map(|sel| {
2103 (
2104 sel.cursor_offset(),
2105 sel.column_anchor,
2106 self.line_end_affinity_for(sel),
2107 )
2108 })
2109 .collect();
2110 let mut offsets: std::collections::HashSet<usize> =
2111 sources.iter().map(|(offset, _, _)| *offset).collect();
2112
2113 let mut newest: Option<usize> = None;
2114 for (offset, anchor, line_end_affinity) in sources {
2115 let anchor = anchor.or_else(|| self.preferred_column_for(offset));
2116 let (target, _) = self.vertical_target(offset, anchor, line_end_affinity, move_lines);
2117 if target == offset || offsets.contains(&target) {
2118 continue;
2119 }
2120 offsets.insert(target);
2121 let id = self.selections.generate_id();
2122 let mut cursor = CursorSelection::new(id, target, target);
2123 cursor.column_anchor = anchor;
2124 self.selections.add(cursor);
2125 newest = Some(target);
2126 }
2127
2128 if let Some(newest) = newest {
2129 self.scroll_to(newest, None, cx);
2130 }
2131 cx.notify();
2132 }
2133
2134 pub(super) fn add_cursor_at(&mut self, offset: usize, cx: &mut Context<Self>) {
2139 if !self.is_multi_line() {
2140 return;
2141 }
2142
2143 for sel in self.selections.iter() {
2144 if sel.contains(offset) {
2145 return;
2146 }
2147 if sel.is_collapsed() && sel.cursor_offset() == offset {
2148 return;
2149 }
2150 }
2151
2152 self.undo_manager.break_transaction_coalescing();
2153 let id = self.selections.generate_id();
2154 self.selections
2155 .add(CursorSelection::new(id, offset, offset));
2156 cx.notify();
2157 }
2158
2159 pub(super) fn build_columnar_selection(
2167 &mut self,
2168 start: ColumnarPoint,
2169 end: ColumnarPoint,
2170 cx: &mut Context<Self>,
2171 ) {
2172 if !self.is_multi_line() {
2173 return;
2174 }
2175
2176 self.undo_manager.break_transaction_coalescing();
2177
2178 let (start_row, start_col) = self.columnar_row_column(start);
2179 let (end_row, end_col) = self.columnar_row_column(end);
2180 let (start_row, end_row) = (start_row.min(end_row), start_row.max(end_row));
2181 let (start_col, end_col) = (start_col.min(end_col), start_col.max(end_col));
2182
2183 let mut new_selections = Vec::with_capacity(end_row - start_row + 1);
2184 for row in start_row..=end_row {
2185 let sel_start = self
2186 .display_map
2187 .display_row_column_to_offset(row, start_col);
2188 let sel_end = self.display_map.display_row_column_to_offset(row, end_col);
2189 let id = self.selections.generate_id();
2190 let sel_start = self.text.clip_offset(sel_start, Bias::Left);
2191 let sel_end = self.text.clip_offset(sel_end, Bias::Left);
2192 new_selections.push(CursorSelection::new(id, sel_start, sel_end));
2193 }
2194
2195 self.selections.replace_all(new_selections);
2196 cx.notify();
2197 }
2198
2199 fn columnar_row_column(&self, point: ColumnarPoint) -> (usize, usize) {
2202 let display_point = self.display_map.offset_to_wrap_display_point(point.offset);
2203 let row = self
2204 .display_map
2205 .wrap_row_to_display_row(display_point.row)
2206 .unwrap_or_else(|| {
2207 self.display_map
2208 .nearest_visible_display_row(display_point.row)
2209 });
2210
2211 (row, display_point.column + point.columns_past_line_end)
2212 }
2213
2214 pub(super) fn on_mouse_down(
2215 &mut self,
2216 event: &MouseDownEvent,
2217 window: &mut Window,
2218 cx: &mut Context<Self>,
2219 ) {
2220 self.undo_manager.break_transaction_coalescing();
2221 crate::global_state::GlobalState::suppress_text_selection(cx);
2224
2225 M::clear_inline_completion(self, cx);
2227
2228 if let Some(ime_marked_range) = &self.ime_marked_range {
2231 if ime_marked_range.len() == 0 {
2232 self.ime_marked_range = None;
2233 }
2234 }
2235
2236 self.selecting = true;
2237 let (offset, line_end_affinity, columns_past_line_end) =
2238 self.resolve_mouse_position(event.position);
2239
2240 if M::on_click(self, event, offset, window, cx) {
2241 return;
2242 }
2243
2244 if event.button == MouseButton::Left && event.click_count >= 3 {
2246 self.select_line(offset, window, cx);
2247 return;
2248 }
2249
2250 if event.button == MouseButton::Left && event.click_count == 2 {
2252 self.select_word(offset, window, cx);
2253 return;
2254 }
2255
2256 if event.button == MouseButton::Right {
2258 if self.enable_context_menu {
2259 if !self.active_selection().contains(offset) {
2260 self.move_to(offset, None, cx);
2261 }
2262 self.pending_context_menu = Some((event.position, offset));
2263 }
2264 return;
2265 }
2266
2267 if self.is_multi_line() && event.button == MouseButton::Left {
2269 if event.modifiers.alt
2270 && (event.modifiers.shift || (cfg!(target_os = "linux") && event.modifiers.control))
2271 {
2272 self.column_select_start = Some(ColumnarPoint::new(offset, columns_past_line_end));
2275 self.selecting = true;
2276 self.move_to_with_affinity(offset, None, line_end_affinity, cx);
2277 return;
2278 } else if event.modifiers.alt {
2279 self.add_cursor_at(offset, cx);
2280 self.column_select_start = Some(ColumnarPoint::new(offset, columns_past_line_end));
2283 return;
2284 }
2285 }
2286
2287 if event.modifiers.shift {
2288 self.select_to_with_affinity(offset, line_end_affinity, cx);
2289 } else {
2290 self.move_to_with_affinity(offset, None, line_end_affinity, cx)
2291 }
2292 }
2293
2294 pub(super) fn on_mouse_up(
2295 &mut self,
2296 event: &MouseUpEvent,
2297 window: &mut Window,
2298 cx: &mut Context<Self>,
2299 ) {
2300 if event.button == MouseButton::Right {
2301 if let Some((position, offset)) = self.pending_context_menu.take() {
2302 self.handle_right_click_menu(position, offset, window, cx);
2303 }
2304 }
2305 if self.active_selection().is_empty() {
2306 self.active_selection_mut().reversed = false;
2307 }
2308 self.selecting = false;
2309 self.selected_word_range = None;
2310 self.column_select_start = None;
2311 self.auto_scroll.stop();
2312 }
2313
2314 pub(super) fn on_mouse_move(
2315 &mut self,
2316 event: &MouseMoveEvent,
2317 window: &mut Window,
2318 cx: &mut Context<Self>,
2319 ) {
2320 let within_bounds = self
2322 .last_bounds
2323 .as_ref()
2324 .map(|bounds| bounds.contains(&event.position))
2325 .unwrap_or(false);
2326
2327 if !within_bounds {
2328 M::clear_hover_state(self, cx);
2330 return;
2331 }
2332
2333 let (offset, _) = self.index_for_mouse_position(event.position);
2335 M::on_mouse_move(self, offset, event, window, cx);
2336
2337 if self.is_code_editor() {
2338 if let Some(diagnostic) = self
2339 .mode
2340 .diagnostics()
2341 .and_then(|set| set.for_offset(offset))
2342 {
2343 self.diagnostic_popover = Some(Rc::new(diagnostic.clone()));
2344 cx.notify();
2345 } else {
2346 self.diagnostic_popover = None;
2347 }
2348 }
2349 }
2350
2351 pub(super) fn on_scroll_wheel(
2352 &mut self,
2353 event: &ScrollWheelEvent,
2354 window: &mut Window,
2355 cx: &mut Context<Self>,
2356 ) {
2357 let line_height = self
2358 .last_layout
2359 .as_ref()
2360 .map(|layout| layout.line_height)
2361 .unwrap_or(window.line_height());
2362 let delta = event.delta.pixel_delta(line_height);
2363
2364 let old_offset = self.scroll_handle.offset();
2365 self.update_scroll_offset(Some(old_offset + delta), cx);
2366
2367 if self.scroll_handle.offset() != old_offset {
2369 cx.stop_propagation();
2370 }
2371
2372 if self.diagnostic_popover.take().is_some() {
2373 cx.notify();
2374 }
2375 }
2376
2377 pub(super) fn update_scroll_offset(
2378 &mut self,
2379 offset: Option<Point<Pixels>>,
2380 cx: &mut Context<Self>,
2381 ) {
2382 let mut offset = offset.unwrap_or(self.scroll_handle.offset());
2383 let safe_x_offset = if self.text_align == TextAlign::Left {
2385 px(0.)
2386 } else {
2387 -CURSOR_WIDTH
2388 };
2389
2390 let safe_y_range =
2391 (-self.scroll_size.height + self.input_bounds.size.height).min(px(0.0))..px(0.);
2392 let safe_x_range = (-self.scroll_size.width + self.input_bounds.size.width + safe_x_offset)
2393 .min(safe_x_offset)..px(0.);
2394
2395 offset.y = if self.is_single_line() {
2396 px(0.)
2397 } else {
2398 offset.y.clamp(safe_y_range.start, safe_y_range.end)
2399 };
2400 offset.x = offset.x.clamp(safe_x_range.start, safe_x_range.end);
2401 if self.scroll_handle.offset() != offset {
2402 self.scroll_handle.set_offset(offset);
2403 cx.notify();
2404 }
2405 }
2406
2407 pub(crate) fn scroll_to(
2411 &mut self,
2412 offset: usize,
2413 direction: Option<MoveDirection>,
2414 cx: &mut Context<Self>,
2415 ) {
2416 let padding = if direction.is_some() {
2417 ScrollPadding::SurroundingLines
2418 } else {
2419 ScrollPadding::Minimal
2420 };
2421 self.scroll_to_with_padding(offset, direction, padding, cx);
2422 }
2423
2424 pub(crate) fn scroll_to_with_padding(
2427 &mut self,
2428 offset: usize,
2429 direction: Option<MoveDirection>,
2430 padding: ScrollPadding,
2431 cx: &mut Context<Self>,
2432 ) {
2433 let Some(last_layout) = self.last_layout.as_ref() else {
2434 return;
2435 };
2436 let Some(bounds) = self.last_bounds.as_ref() else {
2437 return;
2438 };
2439
2440 let mut scroll_offset = self.scroll_handle.offset();
2441 let was_offset = scroll_offset;
2442 let line_height = last_layout.line_height;
2443
2444 let point = self.text.offset_to_point(offset);
2445
2446 let row = point.row;
2447
2448 let mut row_offset_y = line_height * self.display_map.buffer_line_to_display_row(row);
2450
2451 let safety_margin = match last_layout.text_align {
2454 TextAlign::Left => RIGHT_MARGIN,
2455 TextAlign::Right => px(0.),
2456 TextAlign::Center => CURSOR_WIDTH,
2457 };
2458 if let Some(line) = last_layout
2459 .lines
2460 .get(row.saturating_sub(last_layout.visible_range.start))
2461 {
2462 if let Some(pos) = line.position_for_index(point.column, last_layout, false) {
2464 let bounds_width = bounds.size.width - last_layout.line_number_width;
2465 let col_offset_x = pos.x;
2466 row_offset_y += pos.y;
2467 if col_offset_x - safety_margin < -scroll_offset.x {
2468 scroll_offset.x = -col_offset_x + safety_margin;
2470 } else if col_offset_x + safety_margin > -scroll_offset.x + bounds_width {
2471 scroll_offset.x = -(col_offset_x - bounds_width + safety_margin);
2472 }
2473 }
2474 }
2475
2476 let edge_height =
2481 if matches!(padding, ScrollPadding::SurroundingLines) && self.is_code_editor() {
2482 super::element::cursor_surrounding_padding(
2483 self.mode.is_auto_grow(),
2484 self.cursor_surrounding_lines,
2485 last_layout.visible_range.len(),
2486 line_height,
2487 )
2488 } else {
2489 line_height
2490 };
2491 if row_offset_y - edge_height + line_height < -scroll_offset.y {
2492 scroll_offset.y = -row_offset_y + edge_height - line_height;
2494 } else if row_offset_y + edge_height > -scroll_offset.y + bounds.size.height {
2495 scroll_offset.y = -(row_offset_y - bounds.size.height + edge_height);
2497 }
2498
2499 if direction == Some(MoveDirection::Up) {
2501 scroll_offset.y = scroll_offset.y.max(was_offset.y);
2502 } else if direction == Some(MoveDirection::Down) {
2503 scroll_offset.y = scroll_offset.y.min(was_offset.y);
2504 }
2505
2506 let safe_y_min = (-self.scroll_size.height + self.input_bounds.size.height).min(px(0.));
2510 scroll_offset.x = scroll_offset.x.min(px(0.));
2511 scroll_offset.y = scroll_offset.y.clamp(safe_y_min, px(0.));
2512 self.deferred_scroll_offset = Some(scroll_offset);
2513 cx.notify();
2514 }
2515
2516 pub(super) fn show_character_palette(
2517 &mut self,
2518 _: &ShowCharacterPalette,
2519 window: &mut Window,
2520 _: &mut Context<Self>,
2521 ) {
2522 window.show_character_palette();
2523 }
2524
2525 fn selected_texts(&self) -> Vec<String> {
2527 let mut selections: Vec<CursorSelection> = self
2528 .selections
2529 .iter()
2530 .copied()
2531 .filter(|sel| !sel.is_empty())
2532 .collect();
2533 selections.sort_by_key(|sel| sel.start);
2534 selections
2535 .iter()
2536 .map(|sel| self.text.slice(*sel).to_string())
2537 .collect()
2538 }
2539
2540 pub(super) fn copy(&mut self, _: &Copy, _: &mut Window, cx: &mut Context<Self>) {
2541 if !self.is_copyable() {
2542 return;
2543 }
2544
2545 let texts = self.selected_texts();
2546
2547 cx.write_to_clipboard(ClipboardItem::new_string(texts.join("\n")));
2548 }
2549
2550 pub(super) fn cut(&mut self, _: &Cut, window: &mut Window, cx: &mut Context<Self>) {
2551 if !self.is_copyable() {
2552 return;
2553 }
2554
2555 let texts = self.selected_texts();
2556
2557 cx.write_to_clipboard(ClipboardItem::new_string(texts.join("\n")));
2558
2559 self.undo_manager.set_pending_intent(EditIntent::Atomic);
2560 self.replace_text_in_range_silent(None, "", window, cx);
2561 }
2562
2563 pub(super) fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
2564 if !self.is_editable() {
2565 return;
2566 }
2567 let Some(clipboard) = cx.read_from_clipboard() else {
2568 return;
2569 };
2570 let mut new_text = clipboard.text().unwrap_or_default();
2571 self.undo_manager.set_pending_intent(EditIntent::Atomic);
2573
2574 if !self.is_multi_line() {
2575 new_text = new_text.replace('\n', "");
2576 self.replace_text_in_range_silent(None, &new_text, window, cx);
2577 self.scroll_to(self.cursor(), None, cx);
2578 return;
2579 }
2580
2581 if !self.selections.is_single() {
2584 self.selections.merge_overlapping();
2585 }
2586 let lines: Vec<String> = new_text.split('\n').map(|s| s.to_string()).collect();
2587 let count = self.selections.len();
2588 if count > 1 && lines.len() == count {
2589 let mut selections: Vec<CursorSelection> = self.selections.iter().copied().collect();
2590 selections.sort_by_key(|sel| sel.start);
2591 let edits: Vec<(Range<usize>, String)> = selections
2592 .iter()
2593 .zip(lines)
2594 .map(|(sel, line)| (sel.start..sel.end, line))
2595 .collect();
2596 self.replace_text_in_ranges(&edits, window, cx);
2597 } else {
2598 self.replace_text_in_range_silent(None, &new_text, window, cx);
2599 }
2600 self.scroll_to(self.cursor(), None, cx);
2601 }
2602
2603 fn typing_intent(&self, edits: &[(Range<usize>, String)], new_text: &str) -> EditIntent {
2606 if !new_text.is_empty()
2607 && !new_text.contains(['\n', '\r'])
2608 && edits.iter().all(|(range, _)| range.is_empty())
2609 {
2610 EditIntent::Typing
2611 } else {
2612 EditIntent::Atomic
2613 }
2614 }
2615
2616 fn collapse_for_intent(
2622 intent: EditIntent,
2623 mut selection: CursorSelection,
2624 range: &Range<usize>,
2625 ) -> CursorSelection {
2626 match intent {
2627 EditIntent::Backspace => selection.place_at(range.end, None),
2628 EditIntent::DeleteForward => selection.place_at(range.start, None),
2629 EditIntent::Typing | EditIntent::Atomic => {}
2630 }
2631 selection
2632 }
2633
2634 fn push_history(
2635 &mut self,
2636 text: &Rope,
2637 range: &Range<usize>,
2638 new_text: &str,
2639 requested_intent: Option<EditIntent>,
2640 selection_before: CursorSelection,
2641 selection_after: Option<CursorSelection>,
2642 ) -> bool {
2643 if self.undo_manager.is_ignoring() {
2644 return false;
2645 }
2646
2647 let range =
2648 text.clip_offset(range.start, Bias::Left)..text.clip_offset(range.end, Bias::Right);
2649 let old_text = text.slice(range.clone()).to_string();
2650 let new_range = range.start..range.start + new_text.len();
2651
2652 let intent = requested_intent.unwrap_or_else(|| {
2653 if range.is_empty()
2654 && old_text.is_empty()
2655 && !new_text.is_empty()
2656 && !new_text.contains(['\n', '\r'])
2657 {
2658 EditIntent::Typing
2659 } else {
2660 EditIntent::Atomic
2661 }
2662 });
2663
2664 let selection_before = Self::collapse_for_intent(intent, selection_before, &range);
2665 let selection_after =
2666 selection_after.unwrap_or_else(|| (new_range.end..new_range.end).into());
2667
2668 let open_transaction = self.undo_manager.has_open_transaction();
2669 let recorded = self
2670 .undo_manager
2671 .record_transaction(Change::new(range, &old_text, new_range, new_text), intent);
2672 if recorded && !open_transaction {
2675 self.undo_manager
2676 .record_selections(vec![selection_before], vec![selection_after]);
2677 }
2678 recorded
2679 }
2680
2681 pub(super) fn undo(&mut self, _: &Undo, window: &mut Window, cx: &mut Context<Self>) {
2682 self.undo_manager.set_ignoring(true);
2683 if let Some(replay) = self.undo_manager.undo() {
2685 for change in &replay.changes {
2686 let range_utf16 = self.range_to_utf16(&change.new_range.into());
2687 self.replace_text_in_range_silent(Some(range_utf16), &change.old_text, window, cx);
2688 }
2689 self.restore_selections(replay.selections);
2690 self.mode
2691 .restore_auto_closed_pairs(replay.auto_closed_pairs.unwrap_or_default());
2692 }
2693 self.undo_manager.set_ignoring(false);
2694 }
2695
2696 pub(super) fn redo(&mut self, _: &Redo, window: &mut Window, cx: &mut Context<Self>) {
2697 self.undo_manager.set_ignoring(true);
2698 if let Some(replay) = self.undo_manager.redo() {
2700 for change in &replay.changes {
2701 let range_utf16 = self.range_to_utf16(&change.old_range.into());
2702 self.replace_text_in_range_silent(Some(range_utf16), &change.new_text, window, cx);
2703 }
2704 self.restore_selections(replay.selections);
2705 self.mode
2706 .restore_auto_closed_pairs(replay.auto_closed_pairs.unwrap_or_default());
2707 }
2708 self.undo_manager.set_ignoring(false);
2709 }
2710
2711 fn restore_selections(&mut self, selections: Option<Vec<CursorSelection>>) {
2715 let Some(selections) = selections else {
2716 return;
2717 };
2718
2719 let len = self.text.len();
2720 let restored: Vec<CursorSelection> = selections
2721 .into_iter()
2722 .map(|mut sel| {
2723 sel.start = sel.start.min(len);
2724 sel.end = sel.end.min(len);
2725 sel
2726 })
2727 .collect();
2728 self.selections.replace_all(restored);
2729 self.selections.merge_overlapping();
2730 }
2731
2732 pub fn cursor(&self) -> usize {
2736 if let Some(ime_marked_range) = &self.ime_marked_range {
2737 return ime_marked_range.end;
2738 }
2739
2740 self.selections.active().cursor_offset()
2741 }
2742
2743 pub(super) fn active_selection(&self) -> &CursorSelection {
2745 self.selections.active()
2746 }
2747
2748 pub(super) fn active_selection_mut(&mut self) -> &mut CursorSelection {
2750 self.selections.active_mut()
2751 }
2752
2753 pub(super) fn set_selection(&mut self, start: usize, end: usize) {
2756 let active = self.active_selection_mut();
2757 active.start = start;
2758 active.end = end;
2759 }
2760
2761 pub(super) fn set_cursor_to(&mut self, offset: usize) {
2764 let active = self.active_selection_mut();
2765 active.start = offset;
2766 active.end = offset;
2767 active.reversed = false;
2768 }
2769
2770 pub fn visible_row_range(&self) -> Option<std::ops::Range<usize>> {
2772 self.last_layout.as_ref().map(|l| l.visible_range.clone())
2773 }
2774
2775 pub fn scroll_offset(&self) -> gpui::Point<gpui::Pixels> {
2777 self.scroll_handle.offset()
2778 }
2779
2780 pub fn set_scroll_offset(&mut self, offset: gpui::Point<gpui::Pixels>, cx: &mut Context<Self>) {
2784 self.deferred_scroll_offset = Some(offset);
2785 cx.notify();
2786 }
2787
2788 pub fn line_height(&self) -> Option<gpui::Pixels> {
2790 self.last_layout.as_ref().map(|l| l.line_height)
2791 }
2792
2793 pub fn selected_range(&self) -> std::ops::Range<usize> {
2801 (*self.selections.active()).into()
2802 }
2803
2804 pub fn select_all(&mut self, _: &mut Window, cx: &mut Context<Self>) {
2805 self.undo_manager.break_transaction_coalescing();
2806 self.selections.remove_all_but_active();
2807 self.set_selection(0, self.text.len());
2808 cx.notify();
2809 }
2810
2811 pub fn set_selected_range(&mut self, range: Range<usize>, cx: &mut Context<Self>) {
2816 let end_bias = if range.start == range.end {
2817 Bias::Left
2818 } else {
2819 Bias::Right
2820 };
2821 let start = self.text.clip_offset(range.start, Bias::Left);
2822 let end = self.text.clip_offset(range.end, end_bias);
2823
2824 self.move_to(start, None, cx);
2825 self.active_selection_mut().reversed = false;
2826 self.selected_word_range = None;
2827 self.select_to(end, cx);
2828 }
2829
2830 pub(crate) fn index_for_mouse_position(&self, position: Point<Pixels>) -> (usize, bool) {
2838 let (offset, line_end_affinity, _) = self.resolve_mouse_position(position);
2839 (offset, line_end_affinity)
2840 }
2841
2842 fn resolve_mouse_position(&self, position: Point<Pixels>) -> (usize, bool, usize) {
2849 if self.text.len() == 0 {
2851 return (0, false, 0);
2852 }
2853
2854 let (Some(bounds), Some(last_layout)) =
2855 (self.last_bounds.as_ref(), self.last_layout.as_ref())
2856 else {
2857 return (0, false, 0);
2858 };
2859
2860 let line_height = last_layout.line_height;
2861 let line_number_width = last_layout.line_number_width;
2862
2863 let inner_position = position - bounds.origin - point(line_number_width, px(0.));
2874
2875 let mut y_offset = last_layout.visible_top;
2876 let mut last_line_pos = None;
2879
2880 for (vi, (line_layout, _buffer_line)) in last_layout
2882 .lines
2883 .iter()
2884 .zip(last_layout.visible_buffer_lines.iter())
2885 .enumerate()
2886 {
2887 let line_start_offset = last_layout.visible_line_byte_offsets[vi];
2888
2889 let line_origin = point(px(0.), y_offset);
2891 let pos = inner_position - line_origin;
2892
2893 if self.is_single_line() {
2895 let local_index = line_layout.closest_index_for_x(pos.x, last_layout);
2896 return (
2898 self.resolve_index(line_start_offset + local_index),
2899 false,
2900 0,
2901 );
2902 }
2903
2904 if let Some((local_index, line_end_affinity)) =
2906 line_layout.closest_index_for_position(pos, last_layout)
2907 {
2908 return (
2909 self.resolve_index(line_start_offset + local_index),
2910 line_end_affinity,
2911 line_layout.columns_past_line_end(pos, last_layout),
2912 );
2913 } else if pos.y < px(0.) {
2914 return (self.resolve_index(line_start_offset), false, 0);
2916 }
2917
2918 y_offset += line_layout.size(line_height).height;
2919 last_line_pos = Some(pos);
2920 }
2921
2922 let columns_past_line_end = last_layout
2926 .lines
2927 .last()
2928 .zip(last_line_pos)
2929 .map(|(line_layout, pos)| {
2930 let last_row_top = (line_layout.size(line_height).height - line_height).max(px(0.));
2931 line_layout.columns_past_line_end(point(pos.x, last_row_top), last_layout)
2932 })
2933 .unwrap_or(0);
2934
2935 (self.text.len(), false, columns_past_line_end)
2936 }
2937
2938 fn resolve_index(&self, index: usize) -> usize {
2941 if self.masked {
2942 self.text.char_index_to_offset(index / MASK_CHAR.len_utf8())
2943 } else {
2944 index.min(self.text.len())
2945 }
2946 }
2947
2948 fn extend_selection(
2958 sel: &mut CursorSelection,
2959 offset: usize,
2960 word_range: Option<CursorSelection>,
2961 ) {
2962 if sel.reversed {
2963 sel.start = offset;
2964 } else {
2965 sel.end = offset;
2966 }
2967
2968 if sel.end < sel.start {
2969 sel.reversed = !sel.reversed;
2970 std::mem::swap(&mut sel.start, &mut sel.end);
2971 }
2972
2973 if let Some(word_range) = word_range {
2974 if sel.start > word_range.start {
2975 sel.start = word_range.start;
2976 }
2977 if sel.end < word_range.end {
2978 sel.end = word_range.end;
2979 }
2980 }
2981 }
2982
2983 pub(crate) fn select_to(&mut self, offset: usize, cx: &mut Context<Self>) {
2985 self.select_to_with_affinity(offset, false, cx);
2986 }
2987
2988 pub(crate) fn select_to_with_affinity(
2995 &mut self,
2996 offset: usize,
2997 line_end_affinity: bool,
2998 cx: &mut Context<Self>,
2999 ) {
3000 M::clear_inline_completion(self, cx);
3001
3002 self.cursor_line_end_affinity = line_end_affinity;
3003 let offset = self.cursor_boundary(offset, Bias::Left);
3004 let word_range = self.selected_word_range;
3005 Self::extend_selection(self.active_selection_mut(), offset, word_range);
3006
3007 if self.active_selection().is_empty() {
3008 self.update_preferred_column();
3009 }
3010 cx.notify()
3011 }
3012
3013 fn select_all_cursors_to(
3016 &mut self,
3017 f: impl Fn(&Self, &CursorSelection) -> usize,
3018 cx: &mut Context<Self>,
3019 ) {
3020 self.pause_blink_cursor(cx);
3021 self.undo_manager.break_transaction_coalescing();
3022 M::clear_inline_completion(self, cx);
3023
3024 let new_selections: Vec<CursorSelection> = self
3025 .selections
3026 .iter()
3027 .map(|sel| {
3028 let offset = self.cursor_boundary(f(self, sel), Bias::Left);
3029 let mut new_sel = *sel;
3030 Self::extend_selection(&mut new_sel, offset, None);
3031 new_sel
3032 })
3033 .collect();
3034 self.cursor_line_end_affinity = false;
3036 self.selections.replace_all(new_selections);
3037 self.selections.merge_overlapping();
3038
3039 if self.active_selection().is_empty() {
3040 self.update_preferred_column();
3041 }
3042 self.scroll_to(self.cursor(), None, cx);
3043 cx.notify()
3044 }
3045
3046 pub fn unselect(&mut self, _: &mut Window, cx: &mut Context<Self>) {
3048 self.undo_manager.break_transaction_coalescing();
3049 let offset = self.cursor();
3050 self.set_cursor_to(offset);
3051 cx.notify()
3052 }
3053
3054 #[inline]
3055 pub(super) fn offset_from_utf16(&self, offset: usize) -> usize {
3056 self.text.offset_utf16_to_offset(offset)
3057 }
3058
3059 #[inline]
3060 pub(super) fn offset_to_utf16(&self, offset: usize) -> usize {
3061 self.text.offset_to_offset_utf16(offset)
3062 }
3063
3064 #[inline]
3065 pub(crate) fn range_to_utf16(&self, range: &Range<usize>) -> Range<usize> {
3066 self.offset_to_utf16(range.start)..self.offset_to_utf16(range.end)
3067 }
3068
3069 #[inline]
3070 pub(super) fn range_from_utf16(&self, range_utf16: &Range<usize>) -> Range<usize> {
3071 self.offset_from_utf16(range_utf16.start)..self.offset_from_utf16(range_utf16.end)
3072 }
3073
3074 fn clamp_offset_to_visible_backward(&self, offset: usize) -> usize {
3077 let line = self.text.offset_to_point(offset).row;
3078 if self.display_map.is_buffer_line_hidden(line) {
3079 for fold in self.display_map.folded_ranges() {
3080 if line > fold.start_line && line <= fold.end_line {
3081 return self.text.line_end_offset(fold.start_line);
3082 }
3083 }
3084 }
3085 offset
3086 }
3087
3088 fn clamp_offset_to_visible_forward(&self, offset: usize) -> usize {
3091 let line = self.text.offset_to_point(offset).row;
3092 if self.display_map.is_buffer_line_hidden(line) {
3093 for fold in self.display_map.folded_ranges() {
3094 if line > fold.start_line && line <= fold.end_line {
3095 return self.text.line_start_offset(fold.end_line);
3096 }
3097 }
3098 }
3099 offset
3100 }
3101
3102 pub(super) fn cursor_boundary(&self, offset: usize, bias: Bias) -> usize {
3105 let offset = self.text.clip_offset(offset, bias);
3106 if offset > 0
3107 && self.text.char_at(offset - 1) == Some('\r')
3108 && self.text.char_at(offset) == Some('\n')
3109 {
3110 if bias == Bias::Left {
3111 offset - 1
3112 } else {
3113 offset + 1
3114 }
3115 } else {
3116 offset
3117 }
3118 }
3119
3120 pub(super) fn previous_boundary(&self, offset: usize) -> usize {
3121 let offset = self.cursor_boundary(offset.saturating_sub(1), Bias::Left);
3122 self.clamp_offset_to_visible_backward(offset)
3123 }
3124
3125 pub(super) fn next_boundary(&self, offset: usize) -> usize {
3126 let offset = self.cursor_boundary(offset.saturating_add(1), Bias::Right);
3127 self.clamp_offset_to_visible_forward(offset)
3128 }
3129
3130 pub(crate) fn show_cursor(&self, window: &Window, cx: &App) -> bool {
3132 (self.focus_handle.is_focused(window) || M::is_context_menu_open(self, cx))
3133 && !self.disabled
3134 && self.blink_cursor.read(cx).visible()
3135 && window.is_window_active()
3136 }
3137
3138 fn on_focus(&mut self, _: &mut Window, cx: &mut Context<Self>) {
3139 self.blink_cursor.update(cx, |cursor, cx| {
3140 cursor.start(cx);
3141 });
3142 cx.emit(InputEvent::Focus);
3143 }
3144
3145 fn on_blur(&mut self, window: &mut Window, cx: &mut Context<Self>) {
3146 if M::is_context_menu_open(self, cx) {
3147 return;
3148 }
3149
3150 self.undo_manager.break_transaction_coalescing();
3151
3152 M::clear_hover_state(self, cx);
3156 self.diagnostic_popover = None;
3157 M::clear_inline_completion(self, cx);
3158 self.blink_cursor.update(cx, |cursor, cx| {
3159 cursor.stop(cx);
3160 });
3161 self.clamp_number_value(window, cx);
3162 cx.emit(InputEvent::Blur);
3163 cx.notify();
3164 }
3165
3166 fn clamp_number_value(&mut self, window: &mut Window, cx: &mut Context<Self>) {
3171 if !self.is_single_line() {
3172 return;
3173 }
3174 if !matches!(self.mask_pattern, MaskPattern::Number { .. }) {
3175 return;
3176 }
3177 if self.number_min.is_none() && self.number_max.is_none() {
3178 return;
3179 }
3180
3181 let Ok(value) = self.unmask_value().parse::<f64>() else {
3182 return;
3183 };
3184
3185 let clamped = match (self.number_min, self.number_max) {
3186 (Some(min), _) if value < min => min,
3187 (_, Some(max)) if value > max => max,
3188 _ => return,
3189 };
3190
3191 let new_text = clamped.to_string();
3194 if !self.is_valid_input(&new_text, cx) {
3195 return;
3196 }
3197
3198 let range = self.range_to_utf16(&(0..self.text.len()));
3199 self.replace_text_in_range_silent(Some(range), &new_text, window, cx);
3200 }
3201
3202 pub(super) fn pause_blink_cursor(&mut self, cx: &mut Context<Self>) {
3203 self.blink_cursor.update(cx, |cursor, cx| {
3204 cursor.pause(cx);
3205 });
3206 }
3207
3208 pub(super) fn on_drag_move(
3209 &mut self,
3210 event: &MouseMoveEvent,
3211 window: &mut Window,
3212 cx: &mut Context<Self>,
3213 ) {
3214 if self.text.len() == 0 {
3215 return;
3216 }
3217
3218 if self.last_layout.is_none() {
3219 return;
3220 }
3221
3222 if !self.focus_handle.is_focused(window) {
3223 return;
3224 }
3225
3226 if !self.selecting {
3227 return;
3228 }
3229
3230 self.auto_scroll.last_drag_position = Some(event.position);
3231 let (offset, line_end_affinity, columns_past_line_end) =
3232 self.resolve_mouse_position(event.position);
3233 if let Some(start) = self.column_select_start {
3234 let end = ColumnarPoint::new(offset, columns_past_line_end);
3235 self.build_columnar_selection(start, end, cx);
3236 } else {
3237 self.select_to_with_affinity(offset, line_end_affinity, cx);
3238 }
3239
3240 if !self.is_single_line() {
3241 let delta = AutoScroll::compute_delta(event.position.y, self.input_bounds);
3242 let scroll_delta = delta.map(|d| -d);
3244 self.auto_scroll.set(scroll_delta, cx, |delta, state, cx| {
3245 let current = state.scroll_handle.offset();
3246 state.update_scroll_offset(Some(point(current.x, current.y + delta)), cx);
3247 if let Some(pos) = state.auto_scroll.last_drag_position {
3248 let (offset, line_end_affinity, columns_past_line_end) =
3249 state.resolve_mouse_position(pos);
3250 if let Some(start) = state.column_select_start {
3251 let end = ColumnarPoint::new(offset, columns_past_line_end);
3252 state.build_columnar_selection(start, end, cx);
3253 } else {
3254 state.select_to_with_affinity(offset, line_end_affinity, cx);
3255 }
3256 }
3257 });
3258 }
3259 }
3260
3261 fn normalize_input<'a>(&self, new_text: &'a str) -> Cow<'a, str> {
3267 let normalized = if matches!(self.mask_pattern, MaskPattern::Number { .. }) {
3268 normalize_number_input(new_text)
3269 } else {
3270 Cow::Borrowed(new_text)
3271 };
3272
3273 if self.is_single_line() && normalized.contains(['\n', '\r']) {
3274 Cow::Owned(normalized.replace(['\n', '\r'], ""))
3275 } else {
3276 normalized
3277 }
3278 }
3279
3280 pub(crate) fn is_valid_input(&self, new_text: &str, cx: &mut Context<Self>) -> bool {
3281 if new_text.is_empty() {
3282 return true;
3283 }
3284
3285 if let Some(validate) = &self.validate {
3286 if !validate(new_text, cx) {
3287 return false;
3288 }
3289 }
3290
3291 if !self.mask_pattern.is_valid(new_text) {
3292 return false;
3293 }
3294
3295 let Some(pattern) = &self.pattern else {
3296 return true;
3297 };
3298
3299 pattern.is_match(new_text)
3300 }
3301
3302 pub fn mask_pattern(mut self, pattern: impl Into<MaskPattern>) -> Self {
3312 self.mask_pattern = pattern.into();
3313 self.mask_pattern_set = true;
3314 if let Some(placeholder) = self.mask_pattern.placeholder() {
3315 self.placeholder = placeholder.into();
3316 }
3317 self
3318 }
3319
3320 pub fn set_mask_pattern(
3321 &mut self,
3322 pattern: impl Into<MaskPattern>,
3323 _: &mut Window,
3324 cx: &mut Context<Self>,
3325 ) {
3326 self.mask_pattern = pattern.into();
3327 self.mask_pattern_set = true;
3328 if let Some(placeholder) = self.mask_pattern.placeholder() {
3329 self.placeholder = placeholder.into();
3330 }
3331 cx.notify();
3332 }
3333
3334 pub fn ensure_number_mask(&mut self) {
3336 if self.mask_pattern_set {
3337 return;
3338 }
3339 self.mask_pattern = MaskPattern::Number {
3340 separator: None,
3341 fraction: None,
3342 };
3343 }
3344
3345 pub(super) fn set_input_bounds(&mut self, new_bounds: Bounds<Pixels>, cx: &mut Context<Self>) {
3346 let wrap_width_changed = self.input_bounds.size.width != new_bounds.size.width;
3347 self.input_bounds = new_bounds;
3348
3349 if let Some(last_layout) = self.last_layout.as_ref() {
3351 if wrap_width_changed {
3352 let wrap_width = if !self.soft_wrap {
3353 None
3355 } else {
3356 last_layout.wrap_width
3357 };
3358
3359 self.display_map.on_layout_changed(wrap_width, cx);
3360 if self.is_multi_line() {
3361 self.mode.update_auto_grow(&self.display_map);
3362 }
3363 cx.notify();
3364 }
3365 }
3366 }
3367
3368 pub fn selected_text(&self) -> RopeSlice<'_> {
3373 let range_utf16 = self.range_to_utf16(&self.selected_range());
3374 let range = self.range_from_utf16(&range_utf16);
3375 self.text.slice(range)
3376 }
3377
3378 pub fn range_to_bounds(&self, range: &Range<usize>) -> Option<Bounds<Pixels>> {
3382 let Some(last_layout) = self.last_layout.as_ref() else {
3383 return None;
3384 };
3385
3386 let Some(last_bounds) = self.last_bounds else {
3387 return None;
3388 };
3389
3390 let (_, _, start_pos) = self.line_and_position_for_offset(range.start);
3391 let (_, _, end_pos) = self.line_and_position_for_offset(range.end);
3392
3393 let Some(start_pos) = start_pos else {
3394 return None;
3395 };
3396 let Some(end_pos) = end_pos else {
3397 return None;
3398 };
3399
3400 Some(Bounds::from_corners(
3401 last_bounds.origin + start_pos,
3402 last_bounds.origin + end_pos + point(px(0.), last_layout.line_height),
3403 ))
3404 }
3405
3406 pub(crate) fn replace_text_in_range_silent(
3410 &mut self,
3411 range_utf16: Option<Range<usize>>,
3412 new_text: &str,
3413 window: &mut Window,
3414 cx: &mut Context<Self>,
3415 ) {
3416 self.silent_replace_text = true;
3417 self.replace_text_in_range(range_utf16, new_text, window, cx);
3418 self.silent_replace_text = false;
3419 }
3420
3421 pub(crate) fn replace_text_in_ranges(
3426 &mut self,
3427 edits: &[(Range<usize>, String)],
3428 window: &mut Window,
3429 cx: &mut Context<Self>,
3430 ) {
3431 if !self.is_editable() || edits.is_empty() {
3432 return;
3433 }
3434
3435 let mut sorted: Vec<(Range<usize>, &str)> = edits
3438 .iter()
3439 .map(|(range, text)| (range.clone(), text.as_str()))
3440 .collect();
3441 sorted.sort_by_key(|edit| std::cmp::Reverse(edit.0.start));
3442
3443 #[cfg(debug_assertions)]
3444 for pair in sorted.windows(2) {
3445 debug_assert!(
3446 pair[1].0.end <= pair[0].0.start,
3447 "replace_text_in_ranges requires disjoint ranges"
3448 );
3449 }
3450
3451 let requested_intent = self.undo_manager.take_pending_intent();
3455 let selection_before = *self.active_selection();
3456 let original_selections: Vec<CursorSelection> = self.selections.iter().copied().collect();
3457 let selections_before: Vec<CursorSelection> = self
3461 .selections
3462 .iter()
3463 .map(|selection| {
3464 Self::collapse_for_intent(
3465 requested_intent.unwrap_or(EditIntent::Atomic),
3466 *selection,
3467 &(selection.start..selection.end),
3468 )
3469 })
3470 .collect();
3471 let group = sorted.len() > 1;
3472 if group {
3473 self.undo_manager.begin_transaction();
3474 }
3475
3476 let auto_closed_pairs_before = self.mode.auto_closed_pairs().clone();
3477 let mut recorded = false;
3478 for (range, new_text) in &sorted {
3479 let old_text = self.text.clone();
3480 self.mode.adjust_auto_closed_pair(range, new_text.len());
3481 self.text.replace(range.clone(), new_text);
3482
3483 M::adjust_annotations(self, range, new_text.len());
3484 recorded |= self.push_history(
3485 &old_text,
3486 range,
3487 new_text,
3488 requested_intent,
3489 selection_before,
3490 None,
3491 );
3492
3493 self.display_map
3495 .adjust_folds_for_edit(&old_text, range, new_text);
3496 self.display_map
3497 .on_text_changed(&self.text, range, &Rope::from(*new_text), cx);
3498
3499 self.mode.update_highlighter(
3500 super::mode::HighlighterUpdate {
3501 selected_range: range,
3502 old_text: &old_text,
3503 new_text: &self.text,
3504 change_text: new_text,
3505 force: true,
3506 },
3507 window,
3508 cx,
3509 );
3510
3511 self.update_fold_candidates_incremental(range, new_text);
3512 }
3513
3514 if group {
3515 self.undo_manager.commit_transaction();
3516 }
3517
3518 if let Some(diagnostics) = self.mode.diagnostics_mut() {
3520 diagnostics.reset(&self.text)
3521 }
3522 M::refresh_language_features(self, window, cx);
3523 self.update_search(cx);
3524
3525 let mut ascending: Vec<(Range<usize>, &str)> = sorted.clone();
3528 ascending.sort_by_key(|edit| edit.0.start);
3529 let text_len = self.text.len();
3530 let mut delta: isize = 0;
3531 let mut edit_results = Vec::with_capacity(ascending.len());
3532 for (range, new_text) in &ascending {
3533 let offset = ((range.start as isize + delta) as usize + new_text.len()).min(text_len);
3534 edit_results.push((range.clone(), offset));
3535 delta += new_text.len() as isize - (range.end as isize - range.start as isize);
3536 }
3537
3538 let mut used = vec![false; edit_results.len()];
3539 let mut new_selections: Vec<CursorSelection> = Vec::with_capacity(ascending.len());
3540 for selection in original_selections {
3541 if let Ok(index) = edit_results
3542 .binary_search_by_key(&(selection.start, selection.end), |(range, _)| {
3543 (range.start, range.end)
3544 })
3545 && !used[index]
3546 {
3547 let offset = edit_results[index].1;
3548 used[index] = true;
3549 let mut selection = selection;
3550 selection.place_at(offset, None);
3551 new_selections.push(selection);
3552 }
3553 }
3554 for (index, (_, offset)) in edit_results.into_iter().enumerate() {
3555 if !used[index] {
3556 new_selections.push(CursorSelection::new(
3557 self.selections.generate_id(),
3558 offset,
3559 offset,
3560 ));
3561 }
3562 }
3563 self.selections.replace_all(new_selections);
3564 self.selections.merge_overlapping();
3565
3566 let selections_after: Vec<CursorSelection> = self.selections.iter().copied().collect();
3568 if recorded {
3569 self.undo_manager.record_auto_closed_pairs(
3570 auto_closed_pairs_before,
3571 self.mode.auto_closed_pairs().clone(),
3572 );
3573 self.undo_manager
3574 .record_selections(selections_before, selections_after);
3575 }
3576
3577 self.ime_marked_range.take();
3578 self.update_preferred_column();
3579 if self.is_multi_line() {
3580 self.mode.update_auto_grow(&self.display_map);
3581 }
3582 if self.emit_events {
3583 cx.emit(InputEvent::Change);
3584 }
3585 cx.notify();
3586 }
3587
3588 fn update_fold_candidates(&mut self) {
3591 if !self.mode.is_folding() {
3592 return;
3593 }
3594
3595 let Some(highlighter_rc) = self.mode.highlighter() else {
3596 return;
3597 };
3598
3599 let highlighter = highlighter_rc.borrow();
3600 let Some(highlighter) = highlighter.as_ref() else {
3601 return;
3602 };
3603
3604 let fold_ranges = highlighter.fold_ranges(&self.text);
3605 self.display_map.set_fold_candidates(fold_ranges);
3606 }
3607
3608 fn update_fold_candidates_incremental(&mut self, edit_range: &Range<usize>, new_text: &str) {
3611 if !self.mode.is_folding() {
3612 return;
3613 }
3614
3615 let Some(highlighter_rc) = self.mode.highlighter() else {
3616 return;
3617 };
3618
3619 let highlighter = highlighter_rc.borrow();
3620 let Some(highlighter) = highlighter.as_ref() else {
3621 return;
3622 };
3623
3624 let new_end = edit_range.start + new_text.len();
3626 self.display_map.update_fold_candidates_for_edit(
3627 |range, text| highlighter.fold_ranges_for_edit(range, text),
3628 edit_range.start..new_end,
3629 &self.text,
3630 );
3631 }
3632}
3633
3634impl<M: InputModeKind> EntityInputHandler for InputBaseState<M> {
3635 fn text_for_range(
3636 &mut self,
3637 range_utf16: Range<usize>,
3638 adjusted_range: &mut Option<Range<usize>>,
3639 _window: &mut Window,
3640 _cx: &mut Context<Self>,
3641 ) -> Option<String> {
3642 let range = self.range_from_utf16(&range_utf16);
3643 adjusted_range.replace(self.range_to_utf16(&range));
3644 Some(self.text.slice(range).to_string())
3645 }
3646
3647 fn selected_text_range(
3648 &mut self,
3649 _ignore_disabled_input: bool,
3650 _window: &mut Window,
3651 _cx: &mut Context<Self>,
3652 ) -> Option<UTF16Selection> {
3653 Some(UTF16Selection {
3654 range: self.range_to_utf16(&self.selected_range()),
3655 reversed: false,
3656 })
3657 }
3658
3659 fn marked_text_range(
3660 &self,
3661 _window: &mut Window,
3662 _cx: &mut Context<Self>,
3663 ) -> Option<Range<usize>> {
3664 self.ime_marked_range
3665 .map(|range| self.range_to_utf16(&range.into()))
3666 }
3667
3668 fn unmark_text(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {
3669 self.ime_marked_range = None;
3670 self.undo_manager.commit_transaction();
3671 }
3672
3673 fn replace_text_in_range(
3678 &mut self,
3679 range_utf16: Option<Range<usize>>,
3680 new_text: &str,
3681 window: &mut Window,
3682 cx: &mut Context<Self>,
3683 ) {
3684 let requested_intent = self.undo_manager.take_pending_intent();
3685 if !self.is_editable() {
3686 return;
3687 }
3688 let selection_before = *self.active_selection();
3689 let ends_composition = self.ime_marked_range.is_some();
3692
3693 self.pause_blink_cursor(cx);
3694
3695 let new_text = self.normalize_input(new_text);
3699 let new_text: &str = &new_text;
3700
3701 let range = range_utf16
3702 .as_ref()
3703 .map(|range_utf16| self.range_from_utf16(range_utf16))
3704 .or(self.ime_marked_range.map(|range| {
3705 let range = self.range_to_utf16(&(range.start..range.end));
3706 self.range_from_utf16(&range)
3707 }))
3708 .unwrap_or(self.selected_range());
3709
3710 if range.is_empty()
3716 && range.start == self.cursor()
3717 && self.ime_marked_range.is_none()
3718 && !self.silent_replace_text
3719 && self.is_code_editor()
3720 && self.selections.is_single()
3721 && self.active_selection().is_empty()
3722 {
3723 if let Some(target) = self.skip_over_target(new_text) {
3724 self.set_cursor_to(target);
3725 self.update_preferred_column();
3726 cx.notify();
3727 return;
3728 }
3729 }
3730
3731 if self.is_multi_line() {
3732 let multi_cursor = range_utf16.is_none()
3733 && self.ime_marked_range.is_none()
3734 && !self.selections.is_single();
3735 if multi_cursor {
3736 self.selections.merge_overlapping();
3737 let mut edits: Vec<(Range<usize>, String)> = self
3738 .selections
3739 .iter()
3740 .map(|sel| (sel.start..sel.end, new_text.to_string()))
3741 .collect();
3742 edits.sort_by_key(|(range, _)| range.start);
3743 let intent =
3747 requested_intent.unwrap_or_else(|| self.typing_intent(&edits, new_text));
3748 self.undo_manager.begin_transaction_with(intent);
3749 self.undo_manager.set_pending_intent(intent);
3750 self.replace_text_in_ranges(&edits, window, cx);
3751 self.undo_manager.commit_transaction();
3752 } else {
3753 if range_utf16.is_some() {
3754 self.selections.remove_all_but_active();
3755 }
3756 if let Some(intent) = requested_intent {
3757 self.undo_manager.set_pending_intent(intent);
3758 }
3759 if let Some((open_len, closer)) = self.auto_close_target(&range, new_text) {
3760 let replacement = format!("{new_text}{closer}");
3762 self.replace_text_in_ranges(&[(range.clone(), replacement)], window, cx);
3763 let cursor = range.start + new_text.len();
3764 self.mode.track_auto_closed_pair(
3765 cursor - open_len..cursor,
3766 cursor..cursor + closer.len(),
3767 );
3768 self.undo_manager
3769 .record_auto_closed_pairs_after(self.mode.auto_closed_pairs().clone());
3770 self.set_cursor_to(cursor);
3771 self.update_preferred_column();
3772 self.undo_manager.record_selections(
3773 vec![selection_before],
3774 self.selections.iter().copied().collect(),
3775 );
3776 } else {
3777 self.replace_text_in_ranges(
3778 &[(range.clone(), new_text.to_string())],
3779 window,
3780 cx,
3781 );
3782 }
3783 }
3784 if ends_composition {
3785 self.undo_manager.commit_transaction();
3786 }
3787
3788 if !self.silent_replace_text {
3789 M::on_text_typed(self, &range, new_text, window, cx);
3790 }
3791 return;
3792 }
3793
3794 let old_text = self.text.clone();
3796 self.mode.adjust_auto_closed_pair(&range, new_text.len());
3797 self.text.replace(range.clone(), new_text);
3798
3799 let mut new_offset = (range.start + new_text.len()).min(self.text.len());
3800
3801 let mut mask_changed = false;
3804
3805 if self.is_single_line() {
3806 let pending_text = self.text.to_string();
3807 if !self.is_valid_input(&pending_text, cx)
3813 && self.is_valid_input(&old_text.to_string(), cx)
3814 {
3815 self.text = old_text;
3816 return;
3817 }
3818
3819 if !self.mask_pattern.is_none() {
3820 let mask_text = self.mask_pattern.mask(&pending_text);
3821 mask_changed = mask_text.as_str() != pending_text;
3822 self.text = Rope::from(mask_text.as_str());
3823 let new_text_len =
3824 (new_text.len() + mask_text.len()).saturating_sub(pending_text.len());
3825 new_offset = (range.start + new_text_len).min(mask_text.len());
3826 }
3827 }
3828
3829 if mask_changed {
3830 M::reset_annotations(self);
3833 } else {
3834 M::adjust_annotations(self, &range, new_text.len());
3835 }
3836 if mask_changed {
3837 self.push_history(
3841 &old_text,
3842 &(0..old_text.len()),
3843 &self.text.to_string(),
3844 Some(EditIntent::Atomic),
3845 selection_before,
3846 Some((new_offset..new_offset).into()),
3847 );
3848 } else {
3849 self.push_history(
3850 &old_text,
3851 &range,
3852 &new_text,
3853 requested_intent,
3854 selection_before,
3855 None,
3856 );
3857 }
3858 if let Some(diagnostics) = self.mode.diagnostics_mut() {
3859 diagnostics.reset(&self.text)
3860 }
3861 self.display_map
3863 .adjust_folds_for_edit(&old_text, &range, new_text);
3864 self.display_map
3865 .on_text_changed(&self.text, &range, &Rope::from(new_text), cx);
3866
3867 self.mode.update_highlighter::<M>(
3868 super::mode::HighlighterUpdate {
3869 selected_range: &range,
3870 old_text: &old_text,
3871 new_text: &self.text,
3872 change_text: &new_text,
3873 force: true,
3874 },
3875 window,
3876 cx,
3877 );
3878
3879 self.update_fold_candidates_incremental(&range, new_text);
3880 M::refresh_language_features(self, window, cx);
3881 self.set_cursor_to(new_offset);
3882 self.ime_marked_range.take();
3883 if ends_composition {
3889 self.undo_manager
3890 .record_selections(vec![selection_before], vec![*self.active_selection()]);
3891 self.undo_manager.commit_transaction();
3892 }
3893 self.update_preferred_column();
3894 self.update_search(cx);
3895 if self.is_multi_line() {
3896 self.mode.update_auto_grow(&self.display_map);
3897 }
3898 if !self.silent_replace_text {
3899 M::on_text_typed(self, &range, &new_text, window, cx);
3900 }
3901 if self.emit_events {
3902 cx.emit(InputEvent::Change);
3903 }
3904 cx.notify();
3905 }
3906
3907 fn replace_and_mark_text_in_range(
3909 &mut self,
3910 range_utf16: Option<Range<usize>>,
3911 new_text: &str,
3912 new_selected_range_utf16: Option<Range<usize>>,
3913 window: &mut Window,
3914 cx: &mut Context<Self>,
3915 ) {
3916 let requested_intent = self.undo_manager.take_pending_intent();
3917 if !self.is_editable() {
3918 return;
3919 }
3920 let selection_before = *self.active_selection();
3921
3922 let starts_composition = self.ime_marked_range.is_none();
3923 if starts_composition {
3924 self.undo_manager.begin_transaction();
3925 }
3926
3927 self.selections.remove_all_but_active();
3929
3930 M::reset_language_features(self);
3931
3932 let new_text = self.normalize_input(new_text);
3934 let new_text: &str = &new_text;
3935
3936 let range = range_utf16
3937 .as_ref()
3938 .map(|range_utf16| self.range_from_utf16(range_utf16))
3939 .or(self.ime_marked_range.map(|range| {
3940 let range = self.range_to_utf16(&(range.start..range.end));
3941 self.range_from_utf16(&range)
3942 }))
3943 .unwrap_or(self.selected_range());
3944
3945 let auto_closed_pairs_before = self.mode.auto_closed_pairs().clone();
3946 let old_text = self.text.clone();
3947 self.mode.adjust_auto_closed_pair(&range, new_text.len());
3948 self.text.replace(range.clone(), new_text);
3949
3950 if self.is_single_line() {
3951 let pending_text = self.text.to_string();
3952 if !self.is_valid_input(&pending_text, cx)
3954 && self.is_valid_input(&old_text.to_string(), cx)
3955 {
3956 self.text = old_text;
3957 if starts_composition {
3958 self.undo_manager.commit_transaction();
3959 }
3960 return;
3961 }
3962 }
3963
3964 M::adjust_annotations(self, &range, new_text.len());
3965 if let Some(diagnostics) = self.mode.diagnostics_mut() {
3966 diagnostics.reset(&self.text)
3967 }
3968 self.display_map
3970 .adjust_folds_for_edit(&old_text, &range, new_text);
3971 self.display_map
3972 .on_text_changed(&self.text, &range, &Rope::from(new_text), cx);
3973
3974 self.mode.update_highlighter::<M>(
3975 super::mode::HighlighterUpdate {
3976 selected_range: &range,
3977 old_text: &old_text,
3978 new_text: &self.text,
3979 change_text: &new_text,
3980 force: true,
3981 },
3982 window,
3983 cx,
3984 );
3985
3986 self.update_fold_candidates_incremental(&range, new_text);
3987 M::refresh_language_features(self, window, cx);
3988 if new_text.is_empty() {
3989 self.set_cursor_to(range.start);
3991 self.ime_marked_range = None;
3992 } else {
3993 self.ime_marked_range = Some((range.start..range.start + new_text.len()).into());
3994 let new_range = new_selected_range_utf16
3995 .as_ref()
3996 .map(|range_utf16| {
3997 let new_text = Rope::from(new_text);
3998 range.start + new_text.offset_utf16_to_offset(range_utf16.start)
3999 ..range.start + new_text.offset_utf16_to_offset(range_utf16.end)
4000 })
4001 .unwrap_or_else(|| range.start + new_text.len()..range.start + new_text.len());
4002 self.set_selection(new_range.start, new_range.end);
4003 }
4004 if self.is_multi_line() {
4005 self.mode.update_auto_grow(&self.display_map);
4006 }
4007 if self.push_history(
4008 &old_text,
4009 &range,
4010 new_text,
4011 requested_intent,
4012 selection_before,
4013 Some(*self.active_selection()),
4014 ) {
4015 self.undo_manager
4016 .record_selections(vec![selection_before], vec![*self.active_selection()]);
4017 self.undo_manager.record_auto_closed_pairs(
4018 auto_closed_pairs_before,
4019 self.mode.auto_closed_pairs().clone(),
4020 );
4021 }
4022 if new_text.is_empty() {
4023 self.undo_manager.commit_transaction();
4024 }
4025 cx.notify();
4026 }
4027
4028 fn bounds_for_range(
4030 &mut self,
4031 range_utf16: Range<usize>,
4032 bounds: Bounds<Pixels>,
4033 _window: &mut Window,
4034 _cx: &mut Context<Self>,
4035 ) -> Option<Bounds<Pixels>> {
4036 let last_layout = self.last_layout.as_ref()?;
4037 let line_height = last_layout.line_height;
4038 let line_number_width = last_layout.line_number_width;
4039 let range = self.range_from_utf16(&range_utf16);
4040
4041 let mut start_origin = None;
4042 let mut end_origin = None;
4043 let line_number_origin = point(line_number_width, px(0.));
4044 let mut y_offset = last_layout.visible_top;
4045
4046 for (vi, line) in last_layout.lines.iter().enumerate() {
4047 if start_origin.is_some() && end_origin.is_some() {
4048 break;
4049 }
4050
4051 let index_offset = last_layout.visible_line_byte_offsets[vi];
4052
4053 if start_origin.is_none() {
4054 if let Some(p) = line.position_for_index(
4055 range.start.saturating_sub(index_offset),
4056 last_layout,
4057 false,
4058 ) {
4059 start_origin = Some(p + point(px(0.), y_offset));
4060 }
4061 }
4062
4063 if end_origin.is_none() {
4064 if let Some(p) = line.position_for_index(
4065 range.end.saturating_sub(index_offset),
4066 last_layout,
4067 false,
4068 ) {
4069 end_origin = Some(p + point(px(0.), y_offset));
4070 }
4071 }
4072
4073 y_offset += line.size(line_height).height;
4074 }
4075
4076 let start_origin = start_origin.unwrap_or_default();
4077 let mut end_origin = end_origin.unwrap_or_default();
4078 end_origin.y = start_origin.y;
4080
4081 Some(Bounds::from_corners(
4082 bounds.origin + line_number_origin + start_origin,
4083 bounds.origin + line_number_origin + point(end_origin.x, end_origin.y + line_height),
4085 ))
4086 }
4087
4088 fn character_index_for_point(
4089 &mut self,
4090 point: gpui::Point<Pixels>,
4091 _window: &mut Window,
4092 _cx: &mut Context<Self>,
4093 ) -> Option<usize> {
4094 let last_layout = self.last_layout.as_ref()?;
4095 let line_point = self.last_bounds?.localize(&point)?;
4096
4097 for (vi, line) in last_layout.lines.iter().enumerate() {
4098 let offset = last_layout.visible_line_byte_offsets[vi];
4099 if let Some(utf8_index) = line.index_for_position(line_point, last_layout) {
4100 return Some(self.offset_to_utf16(offset + utf8_index));
4101 }
4102 }
4103
4104 None
4105 }
4106}
4107
4108impl<M: InputModeKind> Focusable for InputBaseState<M> {
4109 fn focus_handle(&self, _cx: &App) -> FocusHandle {
4110 self.focus_handle.clone()
4111 }
4112}
4113
4114impl<M: InputModeKind> Render for InputBaseState<M> {
4115 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
4116 self.editor_style = self
4119 .projected_editor_style
4120 .resolved(&crate::Theme::global(cx).tokens);
4121 let entity = cx.entity();
4122 if self._pending_update {
4123 self.mode.update_highlighter::<M>(
4124 super::mode::HighlighterUpdate {
4125 selected_range: &(0..0),
4126 old_text: &self.text,
4127 new_text: &self.text,
4128 change_text: "",
4129 force: false,
4130 },
4131 window,
4132 cx,
4133 );
4134
4135 self.update_fold_candidates();
4136 M::refresh_language_features(self, window, cx);
4137 self._pending_update = false;
4138 }
4139
4140 let element = div()
4141 .id("input-state")
4142 .key_context(CONTEXT)
4143 .track_focus(&self.focus_handle)
4144 .when(self.is_editable(), |this| {
4145 this.on_action(window.listener_for(&entity, InputBaseState::backspace))
4146 .on_action(window.listener_for(&entity, InputBaseState::delete))
4147 .on_action(
4148 window.listener_for(&entity, InputBaseState::delete_to_beginning_of_line),
4149 )
4150 .on_action(window.listener_for(&entity, InputBaseState::delete_to_end_of_line))
4151 .on_action(window.listener_for(&entity, InputBaseState::delete_previous_word))
4152 .on_action(window.listener_for(&entity, InputBaseState::delete_next_word))
4153 .on_action(window.listener_for(&entity, InputBaseState::enter))
4154 .on_action(window.listener_for(&entity, InputBaseState::escape))
4155 .on_action(window.listener_for(&entity, InputBaseState::paste))
4156 .on_action(window.listener_for(&entity, InputBaseState::cut))
4157 .on_action(window.listener_for(&entity, InputBaseState::undo))
4158 .on_action(window.listener_for(&entity, InputBaseState::redo))
4159 .when(self.is_multi_line(), |this| {
4160 this.on_action(window.listener_for(&entity, InputBaseState::indent_inline))
4161 .on_action(window.listener_for(&entity, InputBaseState::outdent_inline))
4162 .on_action(window.listener_for(&entity, InputBaseState::indent_block))
4163 .on_action(window.listener_for(&entity, InputBaseState::outdent_block))
4164 })
4165 })
4166 .on_action(window.listener_for(&entity, InputBaseState::left))
4167 .on_action(window.listener_for(&entity, InputBaseState::right))
4168 .on_action(window.listener_for(&entity, InputBaseState::select_left))
4169 .on_action(window.listener_for(&entity, InputBaseState::select_right))
4170 .when(self.is_multi_line(), |this| {
4171 this.on_action(window.listener_for(&entity, InputBaseState::up))
4172 .on_action(window.listener_for(&entity, InputBaseState::down))
4173 .on_action(window.listener_for(&entity, InputBaseState::select_up))
4174 .on_action(window.listener_for(&entity, InputBaseState::select_down))
4175 .on_action(window.listener_for(&entity, InputBaseState::page_up))
4176 .on_action(window.listener_for(&entity, InputBaseState::page_down))
4177 .on_action(window.listener_for(&entity, InputBaseState::add_cursor_above))
4178 .on_action(window.listener_for(&entity, InputBaseState::add_cursor_below))
4179 })
4180 .on_action(window.listener_for(&entity, InputBaseState::on_action_select_all))
4181 .on_action(window.listener_for(&entity, InputBaseState::select_to_start_of_line))
4182 .on_action(window.listener_for(&entity, InputBaseState::select_to_end_of_line))
4183 .on_action(window.listener_for(&entity, InputBaseState::select_to_previous_word))
4184 .on_action(window.listener_for(&entity, InputBaseState::select_to_next_word))
4185 .on_action(window.listener_for(&entity, InputBaseState::home))
4186 .on_action(window.listener_for(&entity, InputBaseState::end))
4187 .on_action(window.listener_for(&entity, InputBaseState::move_to_start))
4188 .on_action(window.listener_for(&entity, InputBaseState::move_to_end))
4189 .on_action(window.listener_for(&entity, InputBaseState::move_to_previous_word))
4190 .on_action(window.listener_for(&entity, InputBaseState::move_to_next_word))
4191 .on_action(window.listener_for(&entity, InputBaseState::select_to_start))
4192 .on_action(window.listener_for(&entity, InputBaseState::select_to_end))
4193 .on_action(window.listener_for(&entity, InputBaseState::show_character_palette))
4194 .on_action(window.listener_for(&entity, InputBaseState::copy))
4195 .on_action(window.listener_for(&entity, InputBaseState::on_action_search))
4196 .on_action(window.listener_for(&entity, InputBaseState::on_action_replace))
4197 .on_mouse_down(
4198 MouseButton::Left,
4199 window.listener_for(&entity, InputBaseState::on_mouse_down),
4200 )
4201 .on_mouse_down(
4202 MouseButton::Right,
4203 window.listener_for(&entity, InputBaseState::on_mouse_down),
4204 )
4205 .on_mouse_up(
4206 MouseButton::Left,
4207 window.listener_for(&entity, InputBaseState::on_mouse_up),
4208 )
4209 .on_mouse_up(
4210 MouseButton::Right,
4211 window.listener_for(&entity, InputBaseState::on_mouse_up),
4212 )
4213 .on_mouse_move(window.listener_for(&entity, InputBaseState::on_mouse_move))
4214 .on_scroll_wheel(window.listener_for(&entity, InputBaseState::on_scroll_wheel))
4215 .when(self.is_multi_line() && !self.disabled, |this| {
4216 this.on_modifiers_changed(cx.listener(|_, _, _, cx| cx.notify()))
4217 })
4218 .when(!self.disabled, |this| {
4219 if self.is_multi_line() && window.modifiers().alt {
4220 this.cursor_crosshair()
4221 } else {
4222 this.cursor_text()
4223 }
4224 })
4225 .flex_1()
4226 .when(self.is_multi_line(), |this| this.h_full())
4227 .flex_grow_1()
4228 .overflow_x_hidden()
4229 .when(self.is_multi_line(), |this| {
4230 this.pt(self.editor_paddings.top)
4231 .pr(self.editor_paddings.right)
4232 .pb(self.editor_paddings.bottom)
4233 .pl(self.editor_paddings.left)
4234 })
4235 .child(TextElement::new(entity.clone()).placeholder(self.placeholder.clone()))
4236 .when(self.shows_scrollbar(), |this| {
4237 this.child(EditorScrollbar::new(entity.clone()))
4238 });
4239
4240 M::register_actions(element, &entity, window)
4243 }
4244}
4245
4246#[cfg(test)]
4247mod tests {
4248 use super::*;
4249
4250 use crate::theme::Theme;
4251 use gpui::{TestAppContext, VisualTestContext, size};
4252
4253 use crate::input::{EditorMode, EditorState, InputMode, LanguageConfig, TextareaMode};
4254
4255 fn set_test_syntax_provider(
4256 provider: Rc<dyn crate::input::SyntaxContextProvider>,
4257 cx: &mut App,
4258 ) {
4259 struct TestLanguages(Rc<dyn crate::input::SyntaxContextProvider>);
4260 impl crate::input::LanguageProvider for TestLanguages {
4261 fn syntax_context_provider(
4262 &self,
4263 _: &str,
4264 ) -> Option<Rc<dyn crate::input::SyntaxContextProvider>> {
4265 Some(self.0.clone())
4266 }
4267 }
4268 crate::input::set_language_provider(Rc::new(TestLanguages(provider)), cx);
4269 }
4270
4271 struct TestRoot<M: InputModeKind>(Entity<InputBaseState<M>>);
4272
4273 impl<M: InputModeKind> Render for TestRoot<M> {
4274 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
4275 div().size_full().child(self.0.clone())
4276 }
4277 }
4278
4279 struct InputView<M: InputModeKind> {
4280 input: Entity<InputBaseState<M>>,
4281 window_handle: gpui::WindowHandle<TestRoot<M>>,
4282 }
4283
4284 impl<M: InputModeKind> InputView<M> {
4286 fn build_with(
4287 cx: &mut TestAppContext,
4288 make: impl FnOnce(&mut Window, &mut Context<InputBaseState<M>>) -> InputBaseState<M>
4289 + 'static,
4290 ) -> Self {
4291 let mut input: Option<Entity<InputBaseState<M>>> = None;
4292
4293 let window = cx.update(|cx| {
4294 cx.open_window(Default::default(), |window, cx| {
4295 cx.set_global(Theme::default());
4297 super::super::init(cx);
4299
4300 input = Some(cx.new(|cx| make(window, cx)));
4301
4302 cx.new(|_| TestRoot(input.clone().unwrap()))
4303 })
4304 .unwrap()
4305 });
4306
4307 Self {
4308 input: input.clone().unwrap(),
4309 window_handle: window,
4310 }
4311 }
4312 }
4313
4314 impl InputView<EditorMode> {
4315 fn new(cx: &mut TestAppContext) -> Self {
4317 Self::build_editor(cx, |state| state)
4318 }
4319
4320 fn build_editor(
4321 cx: &mut TestAppContext,
4322 f: impl FnOnce(InputBaseState<EditorMode>) -> InputBaseState<EditorMode> + 'static,
4323 ) -> Self {
4324 Self::build_with(cx, move |window, cx| {
4325 f(crate::input::EditorState::new(window, cx).language("sql"))
4326 })
4327 }
4328 }
4329
4330 impl InputView<TextareaMode> {
4331 fn build_textarea(
4332 cx: &mut TestAppContext,
4333 f: impl FnOnce(InputBaseState<TextareaMode>) -> InputBaseState<TextareaMode> + 'static,
4334 ) -> Self {
4335 Self::build_with(cx, move |window, cx| {
4336 f(crate::input::TextareaState::new(window, cx))
4337 })
4338 }
4339 }
4340
4341 impl InputView<InputMode> {
4342 fn build(
4344 cx: &mut TestAppContext,
4345 f: impl FnOnce(InputBaseState<InputMode>) -> InputBaseState<InputMode> + 'static,
4346 ) -> Self {
4347 Self::build_with(cx, move |window, cx| {
4348 f(crate::input::InputState::new(window, cx))
4349 })
4350 }
4351 }
4352
4353 #[gpui::test]
4354 fn test_noop_scroll_notifies_diagnostic_dismissal(cx: &mut TestAppContext) {
4355 use std::{cell::Cell, rc::Rc};
4356
4357 cx.update(crate::init);
4358 let mut input = None;
4359 let window = cx.open_window(size(px(400.), px(100.)), |window, cx| {
4360 input = Some(cx.new(|cx| crate::input::EditorState::new(window, cx)));
4361 gpui::EmptyView
4362 });
4363 let input = input.unwrap();
4364 input.update(cx, |state, cx| {
4365 state.input_bounds = Bounds::new(Point::default(), size(px(100.), px(20.)));
4366 state.scroll_size = size(px(100.), px(20.));
4367 state.present_diagnostic(crate::input::DiagnosticEntry::default(), cx);
4368 });
4369 let notifications = Rc::new(Cell::new(0));
4370 let count = notifications.clone();
4371 let _subscription =
4372 cx.update(|cx| cx.observe(&input, move |_, _| count.set(count.get() + 1)));
4373
4374 window
4375 .update(cx, |_, window, cx| {
4376 input.update(cx, |state, cx| {
4377 state.on_scroll_wheel(
4378 &ScrollWheelEvent {
4379 delta: gpui::ScrollDelta::Pixels(point(px(0.), px(-40.))),
4380 ..Default::default()
4381 },
4382 window,
4383 cx,
4384 );
4385 assert_eq!(state.scroll_handle.offset(), Point::default());
4386 assert!(state.diagnostic_popover().is_none());
4387 });
4388 })
4389 .unwrap();
4390 assert_eq!(
4391 notifications.get(),
4392 1,
4393 "clearing a diagnostic must notify even when scrolling is clamped"
4394 );
4395 window
4396 .update(cx, |_, window, cx| {
4397 input.update(cx, |state, cx| {
4398 state.on_scroll_wheel(
4399 &ScrollWheelEvent {
4400 delta: gpui::ScrollDelta::Pixels(point(px(0.), px(-40.))),
4401 ..Default::default()
4402 },
4403 window,
4404 cx,
4405 );
4406 });
4407 })
4408 .unwrap();
4409 assert_eq!(notifications.get(), 1, "an unchanged input must stay quiet");
4410 }
4411
4412 #[gpui::test]
4413 fn test_cursor_layout_consumer_updates_after_selection(cx: &mut TestAppContext) {
4414 use std::{cell::Cell, rc::Rc};
4415 struct Panel {
4416 input: Entity<crate::input::EditorState>,
4417 observed: Rc<Cell<Option<(Bounds<Pixels>, Pixels)>>>,
4418 }
4419 impl Render for Panel {
4420 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
4421 self.observed.set(self.input.read(cx).cursor_layout());
4422 div().size_full().child(self.input.clone())
4423 }
4424 }
4425 struct Root(Entity<Panel>);
4426 impl Render for Root {
4427 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
4428 div().size_full().child(
4429 self.0
4430 .clone()
4431 .cached(gpui::StyleRefinement::default().size_full()),
4432 )
4433 }
4434 }
4435 cx.update(crate::init);
4436 let observed = Rc::new(Cell::new(None));
4437 let mut input = None;
4438 let window = cx.open_window(size(px(400.), px(100.)), |window, cx| {
4439 let state = cx.new(|cx| {
4440 let mut state = crate::input::EditorState::new(window, cx);
4441 state.set_value("abcdefgh", window, cx);
4442 state
4443 });
4444 input = Some(state.clone());
4445 Root(cx.new(|_| Panel {
4446 input: state,
4447 observed: observed.clone(),
4448 }))
4449 });
4450 cx.run_until_parked();
4451 let input = input.unwrap();
4452 let before = input.read_with(cx, |state, _| state.cursor_layout());
4453 assert_eq!(observed.get(), before);
4454 window
4455 .update(cx, |_, _, cx| {
4456 input.update(cx, |state, cx| state.select_to_with_affinity(3, false, cx));
4457 })
4458 .unwrap();
4459 cx.run_until_parked();
4460 window.update(cx, |_, _, cx| cx.notify()).unwrap();
4461 let after = input.read_with(cx, |state, _| state.cursor_layout());
4462 assert_ne!(after, before, "the caret must actually move");
4463 assert_eq!(
4464 observed.get(),
4465 after,
4466 "render consumers must receive the newly painted caret geometry"
4467 );
4468 }
4469
4470 #[gpui::test]
4471 fn test_input_scroll_offset_notifies_only_on_change(cx: &mut TestAppContext) {
4472 use std::{cell::Cell, rc::Rc};
4473
4474 cx.update(crate::init);
4475 let mut input = None;
4476 let _window = cx.open_window(size(px(400.), px(100.)), |window, cx| {
4477 input = Some(cx.new(|cx| crate::input::InputState::new(window, cx)));
4478 gpui::EmptyView
4479 });
4480 let input = input.unwrap();
4481 let notifications = Rc::new(Cell::new(0));
4482 let count = notifications.clone();
4483 let _subscription =
4484 cx.update(|cx| cx.observe(&input, move |_, _| count.set(count.get() + 1)));
4485
4486 input.update(cx, |state, _| {
4487 state.input_bounds = Bounds::new(Point::default(), size(px(100.), px(20.)));
4488 state.scroll_size = size(px(300.), px(20.));
4489 });
4490 for target in [
4491 None,
4492 Some(point(px(0.), px(0.))),
4493 Some(point(px(20.), px(50.))),
4494 ] {
4495 input.update(cx, |state, cx| state.update_scroll_offset(target, cx));
4496 assert_eq!(
4497 notifications.get(),
4498 0,
4499 "an unchanged clamped offset must stay quiet"
4500 );
4501 }
4502 input.update(cx, |state, cx| {
4503 state.update_scroll_offset(Some(point(px(-40.), px(0.))), cx);
4504 assert_eq!(state.scroll_handle.offset(), point(px(-40.), px(0.)));
4505 });
4506 assert_eq!(notifications.get(), 1, "a scroll must notify");
4507 input.update(cx, |state, cx| {
4508 state.update_scroll_offset(Some(point(px(-400.), px(50.))), cx);
4509 assert_eq!(state.scroll_handle.offset(), point(px(-200.), px(0.)));
4510 });
4511 assert_eq!(notifications.get(), 2);
4512 input.update(cx, |state, cx| {
4513 state.update_scroll_offset(Some(point(px(-500.), px(0.))), cx);
4514 });
4515 assert_eq!(
4516 notifications.get(),
4517 2,
4518 "clamping to the current offset must stay quiet"
4519 );
4520 input.update(cx, |state, cx| {
4521 state.scroll_size.width = px(110.);
4522 state.update_scroll_offset(None, cx);
4523 assert_eq!(state.scroll_handle.offset(), point(px(-10.), px(0.)));
4524 });
4525 assert_eq!(
4526 notifications.get(),
4527 3,
4528 "a smaller scroll range must clamp and notify"
4529 );
4530 }
4531
4532 #[gpui::test]
4533 fn test_input_does_not_invalidate_cached_parent_during_paint(cx: &mut TestAppContext) {
4534 use std::{cell::Cell, rc::Rc};
4535
4536 struct Panel {
4537 input: Entity<crate::input::InputState>,
4538 renders: Rc<Cell<usize>>,
4539 }
4540 impl Render for Panel {
4541 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
4542 self.renders.set(self.renders.get() + 1);
4543 div().size_full().child(self.input.clone())
4544 }
4545 }
4546 struct Root(Entity<Panel>);
4547 impl Render for Root {
4548 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
4549 div().size_full().child(
4550 self.0
4551 .clone()
4552 .cached(gpui::StyleRefinement::default().size_full()),
4553 )
4554 }
4555 }
4556
4557 cx.update(crate::init);
4558 let renders = Rc::new(Cell::new(0));
4559 let mut input = None;
4560 let window = cx.open_window(size(px(400.), px(100.)), |window, cx| {
4561 let state = cx.new(|cx| crate::input::InputState::new(window, cx));
4562 input = Some(state.clone());
4563 Root(cx.new(|_| Panel {
4564 input: state,
4565 renders: renders.clone(),
4566 }))
4567 });
4568 cx.run_until_parked();
4569 let before = renders.get();
4570 assert!(before > 0);
4571 for _ in 0..60 {
4572 window.update(cx, |_, _, cx| cx.notify()).unwrap();
4573 }
4574 assert_eq!(
4575 renders.get(),
4576 before,
4577 "an idle input must not invalidate its cached parent"
4578 );
4579
4580 let input = input.unwrap();
4581 window
4582 .update(cx, |_, window, cx| {
4583 input.update(cx, |state, cx| state.set_value("changed", window, cx));
4584 })
4585 .unwrap();
4586 assert!(
4587 renders.get() > before,
4588 "text changes must invalidate the parent"
4589 );
4590 window.update(cx, |_, _, cx| cx.notify()).unwrap();
4592 let before = renders.get();
4593 for _ in 0..60 {
4594 window.update(cx, |_, _, cx| cx.notify()).unwrap();
4595 }
4596 assert_eq!(renders.get(), before, "the edited input must settle again");
4597
4598 cx.simulate_window_resize(window.into(), size(px(240.), px(100.)));
4599 cx.run_until_parked();
4600 assert!(renders.get() > before, "resizing must invalidate layout");
4601 }
4602
4603 #[gpui::test]
4604 fn textarea_cursor_treats_crlf_as_one_newline(cx: &mut TestAppContext) {
4605 cx.update(crate::init);
4606 let view = InputView::build_textarea(cx, |state| state);
4607 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
4608 cx.update(|window, cx| {
4609 view.input.update(cx, |state, cx| {
4610 for original in ["\r\nlast", "\u{feff}first\r\nlast\n", "first\nlast\r\n"] {
4611 state.set_value(original, window, cx);
4612 let newline = original.find('\n').unwrap();
4613 let end = if original.as_bytes()[newline.saturating_sub(1)] == b'\r' {
4614 newline - 1
4615 } else {
4616 newline
4617 };
4618 state.end(&MoveEnd, window, cx);
4619 assert_eq!(state.cursor(), end);
4620 state.right(&MoveRight, window, cx);
4621 assert_eq!(state.cursor(), newline + 1);
4622 state.left(&MoveLeft, window, cx);
4623 assert_eq!(state.cursor(), end);
4624 state.select_to_end_of_line(&SelectToEndOfLine, window, cx);
4625 assert_eq!(state.selected_range(), end..end);
4626 state.move_to_end(&MoveToEnd, window, cx);
4627 assert_eq!(state.cursor(), original.len());
4628 state.move_to_start(&MoveToStart, window, cx);
4629 assert_eq!(state.cursor(), 0);
4630 assert_eq!(state.value().as_bytes(), original.as_bytes());
4631 }
4632 state.set_value("a\rb", window, cx);
4634 state.right(&MoveRight, window, cx);
4635 assert_eq!(state.cursor(), 1);
4636 state.right(&MoveRight, window, cx);
4637 assert_eq!(state.cursor(), 2);
4638 })
4639 });
4640 }
4641
4642 #[gpui::test]
4643 fn only_a_multi_line_input_paints_scrollbars(cx: &mut TestAppContext) {
4644 cx.update(crate::init);
4645
4646 let single = InputView::build(cx, |state| state);
4650 single
4651 .input
4652 .update(cx, |state, _| assert!(!state.shows_scrollbar()));
4653
4654 let multi = InputView::build_textarea(cx, |state| state);
4655 multi
4656 .input
4657 .update(cx, |state, _| assert!(state.shows_scrollbar()));
4658 }
4659
4660 #[gpui::test]
4661 fn context_menu_handler_is_deferred_and_respects_disabled(cx: &mut TestAppContext) {
4662 use std::{cell::Cell, rc::Rc};
4663 cx.update(crate::init);
4664 let input_view = InputView::new(cx);
4665 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
4666 let input = input_view.input;
4667 let calls = Rc::new(Cell::new(0usize));
4668 let items = Rc::new(Cell::new(0usize));
4669
4670 cx.update(|window, cx| {
4671 input.update(cx, |state, cx| {
4672 let calls2 = calls.clone();
4673 let items2 = items.clone();
4674 state.on_context_menu(Rc::new(move |menu, _, _, _, _| {
4675 calls2.set(calls2.get() + 1);
4676 items2.set(menu.items.len());
4677 }));
4678 state.handle_right_click_menu(point(px(0.), px(0.)), 0, window, cx);
4679 })
4680 });
4681 assert_eq!(calls.get(), 1);
4682 assert_eq!(items.get(), 0);
4683
4684 cx.update(|window, cx| {
4685 input.update(cx, |state, cx| {
4686 state.disabled = true;
4687 state.handle_right_click_menu(point(px(0.), px(0.)), 0, window, cx);
4688 })
4689 });
4690 assert_eq!(calls.get(), 1);
4691 }
4692
4693 #[gpui::test]
4694 fn test_readonly_rejects_user_edits_only(cx: &mut TestAppContext) {
4695 let input_view = InputView::new(cx);
4696 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
4697 let input = input_view.input;
4698
4699 cx.update(|window, cx| {
4700 input.update(cx, |state, cx| {
4701 state.set_value("hello", window, cx);
4702 state.set_readonly(true, cx);
4703 });
4704 });
4705
4706 cx.update(|_, cx| {
4707 input.read_with(cx, |state, _| {
4708 assert!(!state.is_editable());
4709 assert!(!state.is_replaceable());
4710 });
4711 });
4712
4713 cx.update(|window, cx| {
4715 input.update(cx, |state, cx| {
4716 state.replace_text_in_range(None, " world", window, cx);
4717 state.replace_and_mark_text_in_range(None, "あ", None, window, cx);
4718 });
4719 });
4720 cx.update(|_, cx| {
4721 input.read_with(cx, |state, _| assert_eq!(state.value(), "hello"));
4722 });
4723
4724 cx.update(|window, cx| {
4726 input.update(cx, |state, cx| {
4727 state.insert(" world", window, cx);
4728 state.set_value("changed", window, cx);
4729 });
4730 });
4731 cx.update(|_, cx| {
4732 input.read_with(cx, |state, _| assert_eq!(state.value(), "changed"));
4733 });
4734
4735 cx.update(|window, cx| {
4738 input.update(cx, |state, cx| {
4739 state.set_readonly(false, cx);
4740 state.replace_text_in_range(None, "!", window, cx);
4741 });
4742 });
4743 cx.update(|_, cx| {
4744 input.read_with(cx, |state, _| {
4745 assert!(state.is_editable());
4746 assert_eq!(state.value(), "!changed");
4747 });
4748 });
4749 }
4750
4751 #[gpui::test]
4757 fn test_scroll_to_eob_does_not_overshoot_safe_range(cx: &mut TestAppContext) {
4758 let input_view = InputView::new(cx);
4759 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
4760 let input = input_view.input;
4761
4762 cx.update(|window, cx| {
4764 input.update(cx, |state, cx| {
4765 state.set_scroll_beyond_last_line(Some(1), window, cx);
4766 state.set_cursor_surrounding_lines(Some(1), window, cx);
4767 let text: String = (1..=50)
4768 .map(|i| format!("line {i}"))
4769 .collect::<Vec<_>>()
4770 .join("\n");
4771 state.set_value(text, window, cx);
4772 });
4773 });
4774 cx.run_until_parked();
4775
4776 cx.update(|_, cx| {
4779 input.read_with(cx, |state, _| {
4780 assert!(
4781 state.scroll_size.height > px(0.),
4782 "scroll_size not populated by initial paint"
4783 );
4784 assert!(
4785 state.input_bounds.size.height > px(0.),
4786 "input_bounds not populated by initial paint"
4787 );
4788 });
4789 });
4790
4791 cx.update(|_, cx| {
4796 input.update(cx, |state, cx| {
4797 let end = state.text.len();
4798 state.move_to(end, Some(MoveDirection::Down), cx);
4799
4800 let deferred = state
4801 .deferred_scroll_offset
4802 .expect("scroll_to should populate deferred_scroll_offset");
4803 let safe_y_min =
4804 (-state.scroll_size.height + state.input_bounds.size.height).min(px(0.));
4805
4806 assert!(
4807 deferred.y >= safe_y_min,
4808 "deferred_scroll_offset.y = {:?} below safe_y_min = {:?} \
4809 — paint would jitter (Bug C regression)",
4810 deferred.y,
4811 safe_y_min,
4812 );
4813 });
4814 });
4815 }
4816
4817 #[gpui::test]
4818 fn test_next_search_match_reveals_with_padding_after_manual_scroll(cx: &mut TestAppContext) {
4819 assert_search_reveals_with_padding_after_manual_scroll(false, cx);
4820 }
4821
4822 #[gpui::test]
4823 fn test_previous_search_match_reveals_with_padding_after_manual_scroll(
4824 cx: &mut TestAppContext,
4825 ) {
4826 assert_search_reveals_with_padding_after_manual_scroll(true, cx);
4827 }
4828
4829 fn assert_search_reveals_with_padding_after_manual_scroll(
4830 previous: bool,
4831 cx: &mut TestAppContext,
4832 ) {
4833 let input_view = InputView::new(cx);
4834 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
4835 let input = input_view.input;
4836 let text = (0..160)
4837 .map(|row| {
4838 if matches!(row, 20 | 60 | 100) {
4839 format!("match on row {row}")
4840 } else {
4841 format!("line {row}")
4842 }
4843 })
4844 .collect::<Vec<_>>()
4845 .join("\n");
4846 let start = text.match_indices("match").nth(1).unwrap().0;
4847 let expected_match = start..start + "match".len();
4848 cx.update(|window, cx| {
4849 input.update(cx, |state, cx| {
4850 state.set_cursor_surrounding_lines(Some(3), window, cx);
4851 state.set_value(text, window, cx);
4852 state.set_search_query("match", true, cx);
4853 if previous {
4854 state.search_session.matcher.next();
4855 state.search_session.matcher.next();
4856 }
4857 });
4858 });
4859 cx.run_until_parked();
4860 cx.update(|_, cx| {
4861 input.update(cx, |state, cx| {
4862 let line_height = state.last_layout.as_ref().unwrap().line_height;
4863 let y = if previous { px(0.) } else { -line_height * 80. };
4864 state.set_scroll_offset(point(px(0.), y), cx);
4865 });
4866 });
4867 cx.run_until_parked();
4868 input.read_with(&cx, |state, _| {
4869 let visible = state.visible_row_range().unwrap();
4870 if previous {
4871 assert!(visible.end <= 60, "target must be below the viewport");
4872 } else {
4873 assert!(visible.start > 60, "target must be above the viewport");
4874 }
4875 });
4876 cx.update(|_, cx| {
4877 input.update(cx, |state, cx| {
4878 let range = if previous {
4879 state.previous_search_match(cx)
4880 } else {
4881 state.next_search_match(cx)
4882 };
4883 assert_eq!(range, Some(expected_match));
4884 assert_eq!(state.search_session.matcher.label(), "2/3");
4885 });
4886 });
4887 cx.run_until_parked();
4888 input.read_with(&cx, |state, _| {
4889 assert!(state.visible_row_range().unwrap().contains(&60));
4890 let line_height = state.last_layout.as_ref().unwrap().line_height;
4891 let target_y = line_height * 60. + state.scroll_handle.offset().y;
4892 assert!(target_y >= line_height * 2. - px(0.1));
4894 assert!(
4895 target_y + line_height * 3.
4896 <= state.last_bounds.as_ref().unwrap().size.height + px(0.1),
4897 "search must preserve the configured surrounding-line padding"
4898 );
4899 });
4900 }
4901
4902 #[gpui::test]
4903 fn test_number_step(cx: &mut TestAppContext) {
4904 let input = InputView::build(cx, |state| state).input;
4905
4906 cx.update(|cx| {
4907 input.update(cx, |_state, cx| {
4908 assert_eq!(
4909 NumberStep::from(5.).value(123., StepAction::Increment, cx),
4910 5.
4911 );
4912
4913 let step = NumberStep::by_value(|value, action, _cx| {
4916 let below = match action {
4917 StepAction::Increment => value < 1.0,
4918 StepAction::Decrement => value <= 1.0,
4919 };
4920 if below { 0.1 } else { 0.5 }
4921 });
4922 assert_eq!(step.value(0.5, StepAction::Increment, cx), 0.1);
4923 assert_eq!(step.value(1.0, StepAction::Increment, cx), 0.5);
4924 assert_eq!(step.value(1.0, StepAction::Decrement, cx), 0.1);
4925 assert_eq!(step.value(2.0, StepAction::Decrement, cx), 0.5);
4926 });
4927 });
4928 }
4929
4930 #[gpui::test]
4931 fn test_number_input_normalization(cx: &mut TestAppContext) {
4932 let input_view = InputView::build(cx, |state| {
4933 state.mask_pattern(MaskPattern::Number {
4934 separator: None,
4935 fraction: None,
4936 })
4937 });
4938 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
4939 let input = input_view.input;
4940
4941 cx.update(|window, cx| {
4945 input.update(cx, |state, cx| {
4946 state.replace_text_in_range(None, "12。5", window, cx);
4947 });
4948 });
4949 cx.run_until_parked();
4950 cx.update(|_, cx| {
4951 input.read_with(cx, |state, _| {
4952 assert_eq!(state.value(), "12.5");
4953 let cursor: Range<usize> = state.selected_range();
4954 assert_eq!(cursor, 4..4);
4955 });
4956 });
4957
4958 cx.update(|window, cx| {
4960 input.update(cx, |state, cx| {
4961 state.replace_text_in_range(None, "abc", window, cx);
4962 });
4963 });
4964 cx.run_until_parked();
4965 cx.update(|_, cx| {
4966 input.read_with(cx, |state, _| {
4967 assert_eq!(state.value(), "12.5");
4968 });
4969 });
4970
4971 cx.update(|window, cx| {
4974 input.update(cx, |state, cx| {
4975 let range = state.range_to_utf16(&(0..state.text.len()));
4976 state.replace_text_in_range(Some(range), "。", window, cx);
4977 });
4978 });
4979 cx.run_until_parked();
4980 cx.update(|_, cx| {
4981 input.read_with(cx, |state, _| {
4982 assert_eq!(state.value(), ".");
4983 let cursor: Range<usize> = state.selected_range();
4984 assert_eq!(cursor, 1..1);
4985 });
4986 });
4987 }
4988
4989 #[gpui::test]
4990 fn test_number_input_normalization_with_separator(cx: &mut TestAppContext) {
4991 let input_view = InputView::build(cx, |state| {
4992 state.mask_pattern(MaskPattern::Number {
4993 separator: Some(','),
4994 fraction: Some(2),
4995 })
4996 });
4997 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
4998 let input = input_view.input;
4999
5000 cx.update(|window, cx| {
5001 input.update(cx, |state, cx| {
5002 state.replace_text_in_range(None, "1234", window, cx);
5003 });
5004 });
5005 cx.run_until_parked();
5006 cx.update(|_, cx| {
5007 input.read_with(cx, |state, _| {
5008 assert_eq!(state.value(), "1,234");
5009 assert_eq!(state.unmask_value(), "1234");
5010 });
5011 });
5012 }
5013
5014 #[gpui::test]
5015 fn test_number_input_clamp_on_blur(cx: &mut TestAppContext) {
5016 let input_view = InputView::build(cx, |state| {
5017 state
5018 .mask_pattern(MaskPattern::Number {
5019 separator: None,
5020 fraction: None,
5021 })
5022 .min(10.)
5023 .max(100.)
5024 });
5025 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
5026 let input = input_view.input;
5027
5028 cx.update(|window, cx| {
5030 input.update(cx, |state, cx| {
5031 state.replace_text_in_range(None, "1000", window, cx);
5032 assert_eq!(state.value(), "1000");
5033 state.clamp_number_value(window, cx);
5034 assert_eq!(state.value(), "100");
5035
5036 let range = state.range_to_utf16(&(0..state.text.len()));
5037 state.replace_text_in_range(Some(range), "1", window, cx);
5038 assert_eq!(state.value(), "1");
5039 state.clamp_number_value(window, cx);
5040 assert_eq!(state.value(), "10");
5041 });
5042 });
5043 }
5044
5045 #[gpui::test]
5046 fn test_number_input_undo_with_mask(cx: &mut TestAppContext) {
5047 let input_view = InputView::build(cx, |state| {
5048 state.mask_pattern(MaskPattern::Number {
5049 separator: Some(','),
5050 fraction: None,
5051 })
5052 });
5053 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
5054 let input = input_view.input;
5055
5056 cx.update(|window, cx| {
5059 input.update(cx, |state, cx| {
5060 state.replace_text_in_range(None, "1234", window, cx);
5061 assert_eq!(state.value(), "1,234");
5062 state.replace_text_in_range(None, "5", window, cx);
5063 assert_eq!(state.value(), "12,345");
5064
5065 state.undo(&Undo, window, cx);
5069 assert_eq!(state.value(), "1,234");
5070 state.undo(&Undo, window, cx);
5071 assert_eq!(state.value(), "");
5072 state.redo(&Redo, window, cx);
5073 assert_eq!(state.value(), "1,234");
5074 state.redo(&Redo, window, cx);
5075 assert_eq!(state.value(), "12,345");
5076 });
5077 });
5078 }
5079
5080 #[gpui::test]
5081 fn test_undo_manager_coalesces_adjacent_typing_transactions(cx: &mut TestAppContext) {
5082 let input_view = InputView::build(cx, |state| state);
5083 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
5084 let input = input_view.input;
5085
5086 cx.update(|window, cx| {
5087 input.update(cx, |state, cx| {
5088 state.replace_text_in_range(None, "a", window, cx);
5089 state.replace_text_in_range(None, "b", window, cx);
5090 assert_eq!(state.value(), "ab");
5091
5092 state.undo(&Undo, window, cx);
5093 assert_eq!(state.value(), "");
5094 });
5095 });
5096 }
5097
5098 #[gpui::test]
5099 fn test_undo_manager_cursor_movement_splits_typing(cx: &mut TestAppContext) {
5100 let input_view = InputView::build(cx, |state| state);
5101 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
5102 let input = input_view.input;
5103
5104 cx.update(|window, cx| {
5105 input.update(cx, |state, cx| {
5106 state.replace_text_in_range(None, "a", window, cx);
5107 state.replace_text_in_range(None, "b", window, cx);
5108 state.left(&MoveLeft, window, cx);
5109 state.replace_text_in_range(None, "x", window, cx);
5110 assert_eq!(state.value(), "axb");
5111
5112 state.undo(&Undo, window, cx);
5113 assert_eq!(state.value(), "ab");
5114 state.undo(&Undo, window, cx);
5115 assert_eq!(state.value(), "");
5116 });
5117 });
5118 }
5119
5120 #[gpui::test]
5121 fn test_undo_manager_splits_backward_and_forward_delete(cx: &mut TestAppContext) {
5122 let input_view = InputView::build(cx, |state| state);
5123 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
5124 let input = input_view.input;
5125
5126 cx.update(|window, cx| {
5127 input.update(cx, |state, cx| {
5128 state.set_value("abcd", window, cx);
5129 state.set_selected_range(2..2, cx);
5130 state.backspace(&Backspace, window, cx);
5131 state.delete(&Delete, window, cx);
5132 assert_eq!(state.value(), "ad");
5133
5134 state.undo(&Undo, window, cx);
5135 assert_eq!(state.value(), "acd");
5136 state.undo(&Undo, window, cx);
5137 assert_eq!(state.value(), "abcd");
5138 });
5139 });
5140 }
5141
5142 #[gpui::test]
5143 fn test_undo_manager_coalesces_directional_character_deletes(cx: &mut TestAppContext) {
5144 let input_view = InputView::build(cx, |state| state);
5145 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
5146 let input = input_view.input;
5147
5148 cx.update(|window, cx| {
5149 input.update(cx, |state, cx| {
5150 state.set_value("abcd", window, cx);
5151 state.backspace(&Backspace, window, cx);
5152 state.backspace(&Backspace, window, cx);
5153 assert_eq!(state.value(), "ab");
5154 state.undo(&Undo, window, cx);
5155 assert_eq!(state.value(), "abcd");
5156 assert_eq!(state.selected_range(), 4..4);
5157
5158 state.set_value("abcd", window, cx);
5159 state.set_selected_range(1..1, cx);
5160 state.delete(&Delete, window, cx);
5161 state.delete(&Delete, window, cx);
5162 assert_eq!(state.value(), "ad");
5163 state.undo(&Undo, window, cx);
5164 assert_eq!(state.value(), "abcd");
5165 assert_eq!(state.selected_range(), 1..1);
5166 });
5167 });
5168 }
5169
5170 #[gpui::test]
5171 fn test_undo_manager_atomic_paste_isolated_from_typing(cx: &mut TestAppContext) {
5172 let input_view = InputView::build(cx, |state| state);
5173 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
5174 let input = input_view.input;
5175
5176 cx.update(|window, cx| {
5177 cx.write_to_clipboard(ClipboardItem::new_string("P".to_string()));
5178 input.update(cx, |state, cx| {
5179 state.replace_text_in_range(None, "a", window, cx);
5180 state.paste(&Paste, window, cx);
5181 state.replace_text_in_range(None, "b", window, cx);
5182 assert_eq!(state.value(), "aPb");
5183
5184 state.undo(&Undo, window, cx);
5185 assert_eq!(state.value(), "aP");
5186 state.undo(&Undo, window, cx);
5187 assert_eq!(state.value(), "a");
5188 state.undo(&Undo, window, cx);
5189 assert_eq!(state.value(), "");
5190 });
5191 });
5192 }
5193
5194 #[gpui::test]
5195 fn test_undo_manager_programmatic_insert_is_atomic(cx: &mut TestAppContext) {
5196 let input_view = InputView::build(cx, |state| state);
5197 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
5198 let input = input_view.input;
5199
5200 cx.update(|window, cx| {
5201 input.update(cx, |state, cx| {
5202 state.replace_text_in_range(None, "a", window, cx);
5203 state.insert("P", window, cx);
5204 state.replace_text_in_range(None, "b", window, cx);
5205 assert_eq!(state.value(), "aPb");
5206
5207 state.undo(&Undo, window, cx);
5208 assert_eq!(state.value(), "aP");
5209 state.undo(&Undo, window, cx);
5210 assert_eq!(state.value(), "a");
5211 });
5212 });
5213 }
5214
5215 #[gpui::test]
5216 fn test_undo_manager_selection_round_trip_splits_typing(cx: &mut TestAppContext) {
5217 let input_view = InputView::build(cx, |state| state);
5218 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
5219 let input = input_view.input;
5220
5221 cx.update(|window, cx| {
5222 input.update(cx, |state, cx| {
5223 state.replace_text_in_range(None, "a", window, cx);
5224 state.replace_text_in_range(None, "b", window, cx);
5225 state.select_all(window, cx);
5226 state.unselect(window, cx);
5227 state.replace_text_in_range(None, "c", window, cx);
5228
5229 state.undo(&Undo, window, cx);
5230 assert_eq!(state.value(), "ab");
5231 state.undo(&Undo, window, cx);
5232 assert_eq!(state.value(), "");
5233 });
5234 });
5235 }
5236
5237 #[gpui::test]
5238 fn test_undo_manager_enter_is_atomic(cx: &mut TestAppContext) {
5239 let input_view = InputView::build_textarea(cx, |state| state);
5240 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
5241 let input = input_view.input;
5242
5243 cx.update(|window, cx| {
5244 input.update(cx, |state, cx| {
5245 state.replace_text_in_range(None, "a", window, cx);
5246 state.enter(
5247 &Enter {
5248 secondary: false,
5249 shift: false,
5250 },
5251 window,
5252 cx,
5253 );
5254 state.replace_text_in_range(None, "b", window, cx);
5255
5256 state.undo(&Undo, window, cx);
5257 assert_eq!(state.value(), "a\n");
5258 state.undo(&Undo, window, cx);
5259 assert_eq!(state.value(), "a");
5260 state.undo(&Undo, window, cx);
5261 assert_eq!(state.value(), "");
5262 });
5263 });
5264 }
5265
5266 #[gpui::test]
5267 fn test_undo_manager_single_line_return_commits_the_typing_session(cx: &mut TestAppContext) {
5268 let input_view = InputView::build(cx, |state| state);
5269 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
5270 let input = input_view.input;
5271
5272 cx.update(|window, cx| {
5273 input.update(cx, |state, cx| {
5274 for part in ["a", "b", "c"] {
5275 state.replace_text_in_range(None, part, window, cx);
5276 }
5277 state.enter(
5278 &Enter {
5279 secondary: false,
5280 shift: false,
5281 },
5282 window,
5283 cx,
5284 );
5285 for part in ["d", "e", "f"] {
5286 state.replace_text_in_range(None, part, window, cx);
5287 }
5288 assert_eq!(state.value(), "abcdef");
5289
5290 state.undo(&Undo, window, cx);
5291 assert_eq!(state.value(), "abc");
5292 state.undo(&Undo, window, cx);
5293 assert_eq!(state.value(), "");
5294 });
5295 });
5296 }
5297
5298 #[gpui::test]
5299 fn test_undo_manager_submit_on_enter_commits_the_textarea_session(cx: &mut TestAppContext) {
5300 let input_view = InputView::build_textarea(cx, |state| state.submit_on_enter(true));
5301 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
5302 let input = input_view.input;
5303
5304 cx.update(|window, cx| {
5305 input.update(cx, |state, cx| {
5306 state.replace_text_in_range(None, "before submit", window, cx);
5307 state.enter(
5308 &Enter {
5309 secondary: false,
5310 shift: false,
5311 },
5312 window,
5313 cx,
5314 );
5315 state.replace_text_in_range(None, " after submit", window, cx);
5316 assert_eq!(state.value(), "before submit after submit");
5317
5318 state.undo(&Undo, window, cx);
5319 assert_eq!(state.value(), "before submit");
5320 state.undo(&Undo, window, cx);
5321 assert_eq!(state.value(), "");
5322 });
5323 });
5324 }
5325
5326 #[gpui::test]
5327 fn test_undo_manager_blur_commits_the_typing_session(cx: &mut TestAppContext) {
5328 let input_view = InputView::build(cx, |state| state);
5329 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
5330 let input = input_view.input;
5331
5332 cx.update(|window, cx| {
5333 input.update(cx, |state, cx| {
5334 state.replace_text_in_range(None, "before blur", window, cx);
5335 state.on_blur(window, cx);
5336 state.on_focus(window, cx);
5337 state.replace_text_in_range(None, " after focus", window, cx);
5338 assert_eq!(state.value(), "before blur after focus");
5339
5340 state.undo(&Undo, window, cx);
5341 assert_eq!(state.value(), "before blur");
5342 state.undo(&Undo, window, cx);
5343 assert_eq!(state.value(), "");
5344 });
5345 });
5346 }
5347
5348 #[gpui::test]
5349 fn test_undo_manager_keeps_rapid_lines_in_distinct_transactions(cx: &mut TestAppContext) {
5350 let input_view = InputView::build_textarea(cx, |state| state);
5351 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
5352 let input = input_view.input;
5353
5354 cx.update(|window, cx| {
5355 input.update(cx, |state, cx| {
5356 let enter = Enter {
5357 secondary: false,
5358 shift: false,
5359 };
5360 state.replace_text_in_range(None, "a", window, cx);
5361 state.enter(&enter, window, cx);
5362 state.replace_text_in_range(None, "b", window, cx);
5363 state.enter(&enter, window, cx);
5364 state.replace_text_in_range(None, "c", window, cx);
5365 assert_eq!(state.value(), "a\nb\nc");
5366
5367 for expected in ["a\nb\n", "a\nb", "a\n", "a", ""] {
5368 state.undo(&Undo, window, cx);
5369 assert_eq!(state.value(), expected);
5370 }
5371 });
5372 });
5373 }
5374
5375 #[gpui::test]
5376 fn test_undo_manager_coalesces_long_unicode_typing_without_a_timer(cx: &mut TestAppContext) {
5377 let input_view = InputView::build_textarea(cx, |state| state);
5378 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
5379 let input = input_view.input;
5380 let parts = [
5381 "The ",
5382 "quick ",
5383 "brown fox, ",
5384 "你好,世界 ",
5385 "🦀 jumps over 13 lazy dogs.",
5386 ];
5387 let expected = parts.concat();
5388
5389 cx.update(|window, cx| {
5390 input.update(cx, |state, cx| {
5391 for part in parts {
5392 state.replace_text_in_range(None, part, window, cx);
5393 }
5394 assert_eq!(state.value(), expected);
5395
5396 state.undo(&Undo, window, cx);
5397 assert_eq!(state.value(), "");
5398 state.redo(&Redo, window, cx);
5399 assert_eq!(state.value(), expected);
5400 });
5401 });
5402 }
5403
5404 #[gpui::test]
5405 fn test_undo_manager_long_multiline_sequence_has_structural_boundaries(
5406 cx: &mut TestAppContext,
5407 ) {
5408 let input_view = InputView::build_textarea(cx, |state| state);
5409 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
5410 let input = input_view.input;
5411 let enter = Enter {
5412 secondary: false,
5413 shift: false,
5414 };
5415
5416 cx.update(|window, cx| {
5417 input.update(cx, |state, cx| {
5418 for (index, line) in [
5419 "first line with punctuation!",
5420 "第二行包含 Unicode 🦀",
5421 "third line has several words",
5422 ]
5423 .into_iter()
5424 .enumerate()
5425 {
5426 for chunk in line.split_inclusive(' ') {
5427 state.replace_text_in_range(None, chunk, window, cx);
5428 }
5429 if index < 2 {
5430 state.enter(&enter, window, cx);
5431 }
5432 }
5433
5434 assert_eq!(
5435 state.value(),
5436 "first line with punctuation!\n第二行包含 Unicode 🦀\nthird line has several words"
5437 );
5438 state.undo(&Undo, window, cx);
5439 assert_eq!(
5440 state.value(),
5441 "first line with punctuation!\n第二行包含 Unicode 🦀\n"
5442 );
5443 state.undo(&Undo, window, cx);
5444 assert_eq!(
5445 state.value(),
5446 "first line with punctuation!\n第二行包含 Unicode 🦀"
5447 );
5448 state.undo(&Undo, window, cx);
5449 assert_eq!(state.value(), "first line with punctuation!\n");
5450 });
5451 });
5452 }
5453
5454 #[gpui::test]
5455 fn test_masked_input_keeps_its_value_out_of_the_clipboard(cx: &mut TestAppContext) {
5456 let input_view = InputView::build(cx, |state| state);
5457 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
5458 let input = input_view.input;
5459
5460 cx.update(|window, cx| {
5461 input.update(cx, |state, cx| {
5462 state.set_value("hunter2", window, cx);
5463 state.set_masked(true, window, cx);
5464 state.select_all(window, cx);
5465 cx.write_to_clipboard(ClipboardItem::new_string("sentinel".into()));
5466
5467 state.copy(&Copy, window, cx);
5468 assert_eq!(
5469 cx.read_from_clipboard().and_then(|item| item.text()),
5470 Some("sentinel".to_string())
5471 );
5472
5473 state.cut(&Cut, window, cx);
5475 assert_eq!(state.value(), "hunter2");
5476 assert_eq!(
5477 cx.read_from_clipboard().and_then(|item| item.text()),
5478 Some("sentinel".to_string())
5479 );
5480
5481 state.set_masked(false, window, cx);
5483 state.copy(&Copy, window, cx);
5484 assert_eq!(
5485 cx.read_from_clipboard().and_then(|item| item.text()),
5486 Some("hunter2".to_string())
5487 );
5488 });
5489 });
5490 }
5491
5492 #[gpui::test]
5493 fn test_masked_input_collapses_word_boundaries(cx: &mut TestAppContext) {
5494 let input_view = InputView::build(cx, |state| state);
5495 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
5496 let input = input_view.input;
5497
5498 cx.update(|window, cx| {
5499 input.update(cx, |state, cx| {
5500 state.set_value("aaa bbb ccc", window, cx);
5501 state.set_masked(true, window, cx);
5502 state.set_selected_range(7..7, cx);
5503
5504 state.delete_previous_word(&DeleteToPreviousWordStart, window, cx);
5507 assert_eq!(state.value(), " ccc");
5508 assert_eq!(state.selected_range(), 0..0);
5509
5510 state.delete_next_word(&DeleteToNextWordEnd, window, cx);
5511 assert_eq!(state.value(), "");
5512
5513 state.set_value("aaa bbb ccc", window, cx);
5515 state.select_word(9, window, cx);
5516 assert_eq!(state.selected_range(), 0..11);
5517
5518 state.set_masked(false, window, cx);
5520 state.set_value("aaa bbb ccc", window, cx);
5521 state.set_selected_range(11..11, cx);
5522 state.delete_previous_word(&DeleteToPreviousWordStart, window, cx);
5523 assert_eq!(state.value(), "aaa bbb ");
5524
5525 state.set_value("aaa bbb ccc", window, cx);
5526 state.select_word(9, window, cx);
5527 assert_eq!(state.selected_range(), 8..11);
5528 });
5529 });
5530 }
5531
5532 #[gpui::test]
5533 fn test_masked_input_disables_the_copy_context_menu_items(cx: &mut TestAppContext) {
5534 let input_view = InputView::build(cx, |state| state);
5535 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
5536 let input = input_view.input;
5537
5538 cx.update(|window, cx| {
5539 input.update(cx, |state, cx| {
5540 state.set_value("hunter2", window, cx);
5541 state.select_all(window, cx);
5542 assert!(state.context_menu_capabilities().is_copyable());
5543
5544 state.set_masked(true, window, cx);
5545 let capabilities = state.context_menu_capabilities();
5546 assert!(capabilities.is_masked());
5547 assert!(capabilities.has_selection());
5548 assert!(!capabilities.is_copyable());
5549 });
5550 });
5551 }
5552
5553 #[gpui::test]
5554 fn test_undo_manager_cut_and_repeated_pastes_are_distinct_transactions(
5555 cx: &mut TestAppContext,
5556 ) {
5557 let input_view = InputView::build(cx, |state| state);
5558 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
5559 let input = input_view.input;
5560
5561 cx.update(|window, cx| {
5562 input.update(cx, |state, cx| {
5563 state.replace_text_in_range(None, "alpha beta gamma", window, cx);
5564 state.set_selected_range(6..10, cx);
5565 state.cut(&Cut, window, cx);
5566 assert_eq!(state.value(), "alpha gamma");
5567
5568 state.paste(&Paste, window, cx);
5569 state.paste(&Paste, window, cx);
5570 assert_eq!(state.value(), "alpha betabeta gamma");
5571
5572 state.undo(&Undo, window, cx);
5573 assert_eq!(state.value(), "alpha beta gamma");
5574 state.undo(&Undo, window, cx);
5575 assert_eq!(state.value(), "alpha gamma");
5576 state.undo(&Undo, window, cx);
5577 assert_eq!(state.value(), "alpha beta gamma");
5578 });
5579 });
5580 }
5581
5582 #[gpui::test]
5583 fn test_undo_manager_word_and_line_deletes_do_not_coalesce(cx: &mut TestAppContext) {
5584 let input_view = InputView::build_textarea(cx, |state| state);
5585 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
5586 let input = input_view.input;
5587
5588 cx.update(|window, cx| {
5589 input.update(cx, |state, cx| {
5590 state.set_value("one two three\nfour five", window, cx);
5591 state.set_selected_range(13..13, cx);
5592 state.delete_previous_word(&DeleteToPreviousWordStart, window, cx);
5593 assert_eq!(state.value(), "one two \nfour five");
5594 state.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
5595 assert_eq!(state.value(), "one two four five");
5596
5597 state.undo(&Undo, window, cx);
5598 assert_eq!(state.value(), "one two \nfour five");
5599 state.undo(&Undo, window, cx);
5600 assert_eq!(state.value(), "one two three\nfour five");
5601 });
5602 });
5603 }
5604
5605 #[gpui::test]
5606 fn test_undo_manager_multiline_replacement_is_one_atomic_transaction(cx: &mut TestAppContext) {
5607 let input_view = InputView::build_textarea(cx, |state| state);
5608 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
5609 let input = input_view.input;
5610
5611 cx.update(|window, cx| {
5612 input.update(cx, |state, cx| {
5613 state.replace_text_in_range(None, "before", window, cx);
5614 state.set_selected_range(0..6, cx);
5615 state.replace_text_in_range(None, "line one\nline two\n第三行", window, cx);
5616 assert_eq!(state.value(), "line one\nline two\n第三行");
5617
5618 state.undo(&Undo, window, cx);
5619 assert_eq!(state.value(), "before");
5620 assert_eq!(state.selected_range(), 0..6);
5621 state.redo(&Redo, window, cx);
5622 assert_eq!(state.value(), "line one\nline two\n第三行");
5623 });
5624 });
5625 }
5626
5627 #[gpui::test]
5628 fn test_undo_manager_composition_isolated_from_long_typing(cx: &mut TestAppContext) {
5629 let input_view = InputView::build(cx, |state| state);
5630 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
5631 let input = input_view.input;
5632
5633 cx.update(|window, cx| {
5634 input.update(cx, |state, cx| {
5635 state.replace_text_in_range(None, "prefix ", window, cx);
5636 state.replace_and_mark_text_in_range(None, "n", None, window, cx);
5637 state.replace_and_mark_text_in_range(None, "ni", None, window, cx);
5638 state.replace_and_mark_text_in_range(None, "你", None, window, cx);
5639 state.unmark_text(window, cx);
5640 state.replace_text_in_range(None, " suffix", window, cx);
5641 assert_eq!(state.value(), "prefix 你 suffix");
5642
5643 state.undo(&Undo, window, cx);
5644 assert_eq!(state.value(), "prefix 你");
5645 state.undo(&Undo, window, cx);
5646 assert_eq!(state.value(), "prefix ");
5647 state.undo(&Undo, window, cx);
5648 assert_eq!(state.value(), "");
5649 });
5650 });
5651 }
5652
5653 #[gpui::test]
5654 fn test_undo_manager_selected_replacement_is_atomic(cx: &mut TestAppContext) {
5655 let input_view = InputView::build(cx, |state| state);
5656 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
5657 let input = input_view.input;
5658
5659 cx.update(|window, cx| {
5660 input.update(cx, |state, cx| {
5661 state.replace_text_in_range(None, "abc", window, cx);
5662 state.set_selected_range(1..2, cx);
5663 state.replace_text_in_range(None, "X", window, cx);
5664 state.replace_text_in_range(None, "z", window, cx);
5665 assert_eq!(state.value(), "aXzc");
5666
5667 state.undo(&Undo, window, cx);
5668 assert_eq!(state.value(), "aXc");
5669 state.undo(&Undo, window, cx);
5670 assert_eq!(state.value(), "abc");
5671 state.undo(&Undo, window, cx);
5672 assert_eq!(state.value(), "");
5673 });
5674 });
5675 }
5676
5677 #[gpui::test]
5678 fn test_number_input_leading_dot_editable(cx: &mut TestAppContext) {
5679 let input_view = InputView::build(cx, |state| {
5680 state.mask_pattern(MaskPattern::Number {
5681 separator: None,
5682 fraction: None,
5683 })
5684 });
5685 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
5686 let input = input_view.input;
5687
5688 cx.update(|window, cx| {
5689 input.update(cx, |state, cx| {
5690 state.replace_text_in_range(None, "1.2", window, cx);
5691
5692 let range = state.range_to_utf16(&(0..1));
5696 state.replace_text_in_range(Some(range), "", window, cx);
5697 assert_eq!(state.value(), ".2");
5698 let cursor: Range<usize> = state.selected_range();
5699 assert_eq!(cursor, 0..0);
5700
5701 state.replace_text_in_range(Some(0..0), "3", window, cx);
5703 assert_eq!(state.value(), "3.2");
5704 });
5705 });
5706 }
5707
5708 #[gpui::test]
5709 fn test_number_input_escape_invalid_text(cx: &mut TestAppContext) {
5710 let input_view = InputView::build(cx, |state| {
5713 state
5714 .mask_pattern(MaskPattern::Number {
5715 separator: None,
5716 fraction: None,
5717 })
5718 .default_value("1,234")
5719 });
5720 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
5721 let input = input_view.input;
5722
5723 cx.update(|window, cx| {
5724 input.update(cx, |state, cx| {
5725 let range = state.range_to_utf16(&(4..5));
5729 state.replace_text_in_range(Some(range), "", window, cx);
5730 assert_eq!(state.value(), "1,23");
5731
5732 let range = state.range_to_utf16(&(1..2));
5734 state.replace_text_in_range(Some(range), "", window, cx);
5735 assert_eq!(state.value(), "123");
5736 state.replace_text_in_range(None, "a", window, cx);
5737 assert_eq!(state.value(), "123");
5738 });
5739 });
5740 }
5741
5742 #[gpui::test]
5746 fn test_set_value_single_line_caret_at_end_view_at_start(cx: &mut TestAppContext) {
5747 let input_view = InputView::build(cx, |state| state);
5748 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
5749 let input = input_view.input;
5750
5751 let value = format!("https://example.com/v1/users?{}", "x=1&".repeat(120));
5753 let len = value.len();
5754
5755 cx.update(|window, cx| {
5758 input.update(cx, |state, cx| {
5759 state.set_value(value.clone(), window, cx);
5760
5761 assert_eq!(
5762 state.selected_range(),
5763 len..len,
5764 "single-line caret should be at the end after set_value"
5765 );
5766 assert_eq!(
5767 state.deferred_scroll_offset,
5768 Some(point(px(0.), px(0.))),
5769 "the view should be forced back to the start"
5770 );
5771 });
5772 });
5773
5774 cx.run_until_parked();
5777 cx.update(|_, cx| {
5778 input.read_with(cx, |state, _| {
5779 assert!(
5780 state.scroll_size.width > state.input_bounds.size.width,
5781 "value must overflow the input width or this test is vacuous"
5782 );
5783 assert_eq!(
5784 state.scroll_handle.offset().x,
5785 px(0.),
5786 "long value should display from its start, not its tail"
5787 );
5788 });
5789 });
5790 }
5791
5792 #[gpui::test]
5796 fn test_replace_all_single_line(cx: &mut TestAppContext) {
5797 let input_view = InputView::build(cx, |state| state);
5798 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
5799 let input = input_view.input;
5800
5801 let value = format!("https://example.com/v1/users?{}", "x=1&".repeat(120));
5803 let len = value.len();
5804
5805 cx.update(|window, cx| {
5809 input.update(cx, |state, cx| {
5810 state.set_value("hello", window, cx);
5811 state.replace_all(value.clone(), window, cx);
5812 assert_eq!(state.value(), value);
5813 assert_eq!(
5814 state.selected_range(),
5815 len..len,
5816 "single-line caret should be at the end after replace_all"
5817 );
5818 assert_eq!(
5819 state.scroll_handle.offset(),
5820 point(px(0.), px(0.)),
5821 "the scroll offset should be reset to the start"
5822 );
5823 assert_eq!(
5824 state.deferred_scroll_offset,
5825 Some(point(px(0.), px(0.))),
5826 "single-line should set a deferred scroll offset to keep the start visible"
5827 );
5828 });
5829 });
5830
5831 cx.run_until_parked();
5834 cx.update(|_, cx| {
5835 input.read_with(cx, |state, _| {
5836 assert!(
5837 state.scroll_size.width > state.input_bounds.size.width,
5838 "value must overflow the input width or this test is vacuous"
5839 );
5840 assert_eq!(
5841 state.scroll_handle.offset().x,
5842 px(0.),
5843 "long value should display from its start, not its tail"
5844 );
5845 });
5846 });
5847 }
5848
5849 #[gpui::test]
5850 fn test_single_line_removes_newlines(cx: &mut TestAppContext) {
5851 let input_view = InputView::build(cx, |state| state.default_value("default\nvalue"));
5852 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
5853 let input = input_view.input;
5854
5855 cx.update(|window, cx| {
5856 input.update(cx, |state, cx| {
5857 assert_eq!(state.value(), "defaultvalue");
5858
5859 state.set_value("first\nsecond\r\nthird\rfourth", window, cx);
5860 assert_eq!(state.value(), "firstsecondthirdfourth");
5861
5862 state.set_value("", window, cx);
5863 state.insert("a\nb", window, cx);
5864 assert_eq!(state.value(), "ab");
5865 });
5866
5867 cx.write_to_clipboard(ClipboardItem::new_string("a\r\nb\nc\rd".to_string()));
5868 input.update(cx, |state, cx| {
5869 state.set_value("", window, cx);
5870 state.paste(&Paste, window, cx);
5871 assert_eq!(state.value(), "abcd");
5872 });
5873 });
5874
5875 cx.run_until_parked();
5876 }
5877
5878 #[gpui::test]
5882 fn test_replace_all_multi_line(cx: &mut TestAppContext) {
5883 let input_view = InputView::build_textarea(cx, |state| state);
5884 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
5885 let input = input_view.input;
5886
5887 cx.update(|window, cx| {
5888 input.update(cx, |state, cx| {
5889 state.set_value("foo\nbar", window, cx);
5890 state.replace_all("baz\nqux", window, cx);
5891 assert_eq!(state.value(), "baz\nqux");
5892 assert_eq!(
5893 state.selected_range(),
5894 0..0,
5895 "multi-line selection should be cleared after replace_all"
5896 );
5897 assert_eq!(
5898 state.scroll_handle.offset(),
5899 point(px(0.), px(0.)),
5900 "the scroll offset should be reset to the start"
5901 );
5902 assert!(
5903 state.deferred_scroll_offset.is_none(),
5904 "multi-line should not set a deferred scroll offset"
5905 );
5906 });
5907 });
5908 }
5909
5910 #[gpui::test]
5913 fn test_replace_all_preserves_undo_history(cx: &mut TestAppContext) {
5914 let input_view = InputView::build(cx, |state| state);
5915 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
5916 let input = input_view.input;
5917
5918 cx.update(|window, cx| {
5919 input.update(cx, |state, cx| {
5920 state.set_value("first", window, cx);
5922 assert!(
5923 !state.undo_manager.has_undos(),
5924 "history should be empty after set_value"
5925 );
5926
5927 state.replace_all("second", window, cx);
5929 assert_eq!(state.value(), "second");
5930 assert!(
5931 state.undo_manager.has_undos(),
5932 "replace_all should record an undo step"
5933 );
5934
5935 state.undo(&Undo, window, cx);
5937 assert_eq!(state.value(), "first");
5938
5939 state.redo(&Redo, window, cx);
5941 assert_eq!(state.value(), "second");
5942 });
5943 });
5944 }
5945
5946 #[gpui::test]
5949 fn test_replace_all_code_editor(cx: &mut TestAppContext) {
5950 let input_view = InputView::new(cx);
5951 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
5952 let input = input_view.input;
5953
5954 cx.update(|window, cx| {
5955 input.update(cx, |state, cx| {
5956 state.set_value("select 1", window, cx);
5958 state._pending_update = false;
5959
5960 state.replace_all("select 2", window, cx);
5961 assert_eq!(state.value(), "select 2");
5962 assert!(
5963 state._pending_update,
5964 "replace_all on a code editor should request a pending update"
5965 );
5966 });
5967 });
5968 }
5969
5970 #[gpui::test]
5971 fn test_set_selected_range(cx: &mut TestAppContext) {
5972 let input_view = InputView::build(cx, |state| state.default_value("hello world"));
5973 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
5974 let input = input_view.input;
5975
5976 cx.update(|_, cx| {
5977 input.update(cx, |s, cx| {
5978 s.set_selected_range(0..5, cx);
5979 assert_eq!(s.selected_range(), 0..5);
5980 assert_eq!(s.selected_text().to_string(), "hello");
5981
5982 s.set_selected_range(6..11, cx);
5983 assert_eq!(s.selected_text().to_string(), "world");
5984
5985 s.set_selected_range(100..100, cx);
5987 assert_eq!(s.selected_range(), 11..11);
5988 });
5989 });
5990 }
5991
5992 #[gpui::test]
5994 fn test_replace_text_in_ranges_single_edit(cx: &mut TestAppContext) {
5995 let input_view = InputView::build_textarea(cx, |state| state.default_value("hello world"));
5996 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
5997 let input = input_view.input;
5998
5999 cx.update(|window, cx| {
6000 input.update(cx, |s, cx| {
6001 s.replace_text_in_ranges(&[(0..5, "HELLO".to_string())], window, cx);
6002 assert_eq!(s.value(), "HELLO world");
6003
6004 s.undo(&Undo, window, cx);
6005 assert_eq!(s.value(), "hello world");
6006
6007 s.redo(&Redo, window, cx);
6008 assert_eq!(s.value(), "HELLO world");
6009 });
6010 });
6011 }
6012
6013 #[gpui::test]
6016 fn test_replace_text_in_ranges_multi_edit_transaction(cx: &mut TestAppContext) {
6017 let input_view = InputView::build_textarea(cx, |state| state.default_value("aaa bbb ccc"));
6018 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6019 let input = input_view.input;
6020
6021 cx.update(|window, cx| {
6022 input.update(cx, |s, cx| {
6023 s.replace_text_in_ranges(
6026 &[(0..3, "X".to_string()), (8..11, "Y".to_string())],
6027 window,
6028 cx,
6029 );
6030 assert_eq!(s.value(), "X bbb Y");
6031
6032 let cursors: Vec<usize> =
6034 s.selections.iter().map(|sel| sel.cursor_offset()).collect();
6035 assert_eq!(cursors, vec![1, 7]);
6036
6037 assert_eq!(s.undo_manager.undo_count(), 1);
6039
6040 s.undo(&Undo, window, cx);
6042 assert_eq!(s.value(), "aaa bbb ccc");
6043
6044 s.redo(&Redo, window, cx);
6046 assert_eq!(s.value(), "X bbb Y");
6047 });
6048 });
6049 }
6050
6051 #[gpui::test]
6053 fn test_ime_composition_undoes_as_one_unit(cx: &mut TestAppContext) {
6054 let input_view = InputView::build_textarea(cx, |state| state.default_value(""));
6055 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6056 let input = input_view.input;
6057
6058 cx.update(|window, cx| {
6059 input.update(cx, |s, cx| {
6060 s.replace_and_mark_text_in_range(None, "n", Some(1..1), window, cx);
6062 s.replace_and_mark_text_in_range(None, "ni", Some(2..2), window, cx);
6063 s.replace_text_in_range(None, "你", window, cx);
6064 assert_eq!(s.value(), "你");
6065
6066 assert_eq!(s.undo_manager.undo_count(), 1);
6068
6069 s.undo(&Undo, window, cx);
6070 assert_eq!(s.value(), "");
6071
6072 s.redo(&Redo, window, cx);
6073 assert_eq!(s.value(), "你");
6074 });
6075 });
6076 }
6077
6078 #[gpui::test]
6081 fn test_edit_after_composition_is_separate_undo(cx: &mut TestAppContext) {
6082 let input_view = InputView::build_textarea(cx, |state| state.default_value(""));
6083 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6084 let input = input_view.input;
6085
6086 cx.update(|window, cx| {
6087 input.update(cx, |s, cx| {
6088 s.replace_and_mark_text_in_range(None, "n", Some(1..1), window, cx);
6089 s.replace_text_in_range(None, "你", window, cx);
6090 assert_eq!(s.value(), "你");
6091 assert_eq!(s.undo_manager.undo_count(), 1);
6092
6093 s.replace_text_in_range(None, "x", window, cx);
6095 assert_eq!(s.value(), "你x");
6096 assert_eq!(s.undo_manager.undo_count(), 2);
6097
6098 s.undo(&Undo, window, cx);
6099 assert_eq!(s.value(), "你");
6100 s.undo(&Undo, window, cx);
6101 assert_eq!(s.value(), "");
6102 });
6103 });
6104 }
6105
6106 #[gpui::test]
6109 fn test_composition_cancel_via_unmark_does_not_leak(cx: &mut TestAppContext) {
6110 let input_view = InputView::build_textarea(cx, |state| state.default_value(""));
6111 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6112 let input = input_view.input;
6113
6114 cx.update(|window, cx| {
6115 input.update(cx, |s, cx| {
6116 s.replace_and_mark_text_in_range(None, "n", Some(1..1), window, cx);
6118 s.unmark_text(window, cx);
6119 let after_cancel = s.undo_manager.undo_count();
6120
6121 s.replace_text_in_range(None, "x", window, cx);
6123 assert_eq!(s.undo_manager.undo_count(), after_cancel + 1);
6124 });
6125 });
6126 }
6127
6128 #[gpui::test]
6129 fn test_set_selected_range_clips_to_utf8_boundaries(cx: &mut TestAppContext) {
6130 let input_view = InputView::build(cx, |state| state.default_value("éx"));
6131 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6132 let input = input_view.input;
6133
6134 cx.update(|window, cx| {
6135 input.update(cx, |state, cx| {
6136 state.set_selected_range(0..1, cx);
6137 assert_eq!(state.selected_range(), 0..2);
6138 state.copy(&Copy, window, cx);
6139
6140 state.set_selected_range(1..1, cx);
6141 assert_eq!(state.selected_range(), 0..0);
6142 });
6143 });
6144 }
6145
6146 #[gpui::test]
6147 fn test_ime_selection_is_relative_to_replacement_start(cx: &mut TestAppContext) {
6148 let input_view = InputView::build(cx, |state| state.default_value("你好 "));
6149 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6150 let input = input_view.input;
6151
6152 cx.update(|window, cx| {
6153 input.update(cx, |state, cx| {
6154 state.set_selected_range(7..7, cx);
6155 state.replace_and_mark_text_in_range(None, "s", Some(1..1), window, cx);
6156 state.replace_and_mark_text_in_range(None, "sh", Some(2..2), window, cx);
6157
6158 assert_eq!(state.value(), "你好 sh");
6159 assert_eq!(state.selected_range(), 9..9);
6160 assert_eq!(state.ime_marked_range, Some((7..9).into()));
6161 });
6162 });
6163 }
6164
6165 #[gpui::test]
6166 fn test_undo_manager_composition_is_one_undo_group(cx: &mut TestAppContext) {
6167 let input_view = InputView::build(cx, |state| state);
6168 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6169 let input = input_view.input;
6170
6171 cx.update(|window, cx| {
6172 input.update(cx, |state, cx| {
6173 state.set_value("a", window, cx);
6174 state.replace_and_mark_text_in_range(None, "s", None, window, cx);
6175 state.replace_and_mark_text_in_range(None, "sh", None, window, cx);
6176 state.replace_text_in_range(None, "是", window, cx);
6177 assert_eq!(state.value(), "a是");
6178
6179 state.undo(&Undo, window, cx);
6180 assert_eq!(state.value(), "a");
6181 state.redo(&Redo, window, cx);
6182 assert_eq!(state.value(), "a是");
6183 });
6184 });
6185 }
6186
6187 #[gpui::test]
6188 fn test_undo_manager_consecutive_compositions_are_separate_groups(cx: &mut TestAppContext) {
6189 let input_view = InputView::build(cx, |state| state);
6190 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6191 let input = input_view.input;
6192
6193 cx.update(|window, cx| {
6194 input.update(cx, |state, cx| {
6195 state.replace_and_mark_text_in_range(None, "j", None, window, cx);
6197 state.replace_and_mark_text_in_range(None, "jin", None, window, cx);
6198 state.replace_text_in_range(None, "今天", window, cx);
6199 state.replace_and_mark_text_in_range(None, "w", None, window, cx);
6201 state.replace_and_mark_text_in_range(None, "wo", None, window, cx);
6202 state.replace_text_in_range(None, "我们", window, cx);
6203 assert_eq!(state.value(), "今天我们");
6204 assert_eq!(state.selected_range(), 12..12);
6205
6206 state.undo(&Undo, window, cx);
6207 assert_eq!(state.value(), "今天");
6208 assert_eq!(state.selected_range(), 6..6);
6209
6210 state.undo(&Undo, window, cx);
6211 assert_eq!(state.value(), "");
6212 assert_eq!(state.selected_range(), 0..0);
6213
6214 state.redo(&Redo, window, cx);
6215 assert_eq!(state.value(), "今天");
6216 assert_eq!(state.selected_range(), 6..6);
6217
6218 state.redo(&Redo, window, cx);
6219 assert_eq!(state.value(), "今天我们");
6220 assert_eq!(state.selected_range(), 12..12);
6221 });
6222 });
6223 }
6224
6225 #[gpui::test]
6226 fn test_undo_manager_typing_after_composition_is_a_separate_group(cx: &mut TestAppContext) {
6227 let input_view = InputView::build(cx, |state| state);
6228 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6229 let input = input_view.input;
6230
6231 cx.update(|window, cx| {
6232 input.update(cx, |state, cx| {
6233 state.replace_and_mark_text_in_range(None, "n", None, window, cx);
6234 state.replace_text_in_range(None, "你", window, cx);
6235 state.undo_manager.set_pending_intent(EditIntent::Typing);
6236 state.replace_text_in_range(None, "a", window, cx);
6237 state.undo_manager.set_pending_intent(EditIntent::Typing);
6238 state.replace_text_in_range(None, "b", window, cx);
6239 assert_eq!(state.value(), "你ab");
6240
6241 state.undo(&Undo, window, cx);
6242 assert_eq!(state.value(), "你");
6243
6244 state.undo(&Undo, window, cx);
6245 assert_eq!(state.value(), "");
6246 });
6247 });
6248 }
6249
6250 #[gpui::test]
6251 fn test_undo_manager_composition_cancel_leaves_no_entry(cx: &mut TestAppContext) {
6252 let input_view = InputView::build(cx, |state| state);
6253 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6254 let input = input_view.input;
6255
6256 cx.update(|window, cx| {
6257 input.update(cx, |state, cx| {
6258 state.set_value("a", window, cx);
6259 state.replace_and_mark_text_in_range(None, "s", None, window, cx);
6260 state.replace_and_mark_text_in_range(None, "", None, window, cx);
6261
6262 assert_eq!(state.value(), "a");
6263 assert!(!state.undo_manager.has_undos());
6264 });
6265 });
6266 }
6267
6268 #[gpui::test]
6269 fn test_undo_manager_selection_restored_by_undo_and_redo(cx: &mut TestAppContext) {
6270 let input_view = InputView::build(cx, |state| state);
6271 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6272 let input = input_view.input;
6273
6274 cx.update(|window, cx| {
6275 input.update(cx, |state, cx| {
6276 state.set_value("abc", window, cx);
6277 state.set_selected_range(1..2, cx);
6278 state.replace_text_in_range(None, "X", window, cx);
6279
6280 state.undo(&Undo, window, cx);
6281 assert_eq!(state.value(), "abc");
6282 assert_eq!(state.selected_range(), 1..2);
6283
6284 state.redo(&Redo, window, cx);
6285 assert_eq!(state.value(), "aXc");
6286 assert_eq!(state.selected_range(), 2..2);
6287 });
6288 });
6289 }
6290
6291 #[gpui::test]
6292 fn test_undo_manager_forward_delete_restores_cursor(cx: &mut TestAppContext) {
6293 let input_view = InputView::build(cx, |state| state);
6294 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6295 let input = input_view.input;
6296
6297 cx.update(|window, cx| {
6298 input.update(cx, |state, cx| {
6299 state.set_value("abc", window, cx);
6300 state.set_selected_range(1..1, cx);
6301 state.delete(&Delete, window, cx);
6302
6303 state.undo(&Undo, window, cx);
6304 assert_eq!(state.value(), "abc");
6305 assert_eq!(state.selected_range(), 1..1);
6306 });
6307 });
6308 }
6309
6310 #[gpui::test]
6311 fn test_undo_manager_selection_movement_preserves_redo(cx: &mut TestAppContext) {
6312 let input_view = InputView::build(cx, |state| state);
6313 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6314 let input = input_view.input;
6315
6316 cx.update(|window, cx| {
6317 input.update(cx, |state, cx| {
6318 state.replace_text_in_range(None, "ab", window, cx);
6319 state.undo(&Undo, window, cx);
6320 state.set_selected_range(0..0, cx);
6321 state.redo(&Redo, window, cx);
6322 assert_eq!(state.value(), "ab");
6323
6324 state.undo(&Undo, window, cx);
6325 state.replace_text_in_range(None, "x", window, cx);
6326 state.redo(&Redo, window, cx);
6327 assert_eq!(state.value(), "x");
6328 });
6329 });
6330 }
6331
6332 #[gpui::test]
6333 fn test_undo_manager_noop_edit_preserves_redo(cx: &mut TestAppContext) {
6334 let input_view = InputView::build(cx, |state| state);
6335 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6336 let input = input_view.input;
6337
6338 cx.update(|window, cx| {
6339 input.update(cx, |state, cx| {
6340 state.replace_text_in_range(None, "a", window, cx);
6341 state.move_to(0, None, cx);
6342 state.replace_text_in_range(None, "", window, cx);
6343 state.undo(&Undo, window, cx);
6344 assert_eq!(state.value(), "");
6345 state.redo(&Redo, window, cx);
6346 assert_eq!(state.value(), "a");
6347 assert_eq!(state.cursor(), 1);
6348 });
6349 });
6350 }
6351
6352 #[gpui::test]
6353 fn test_cursor_round_trip_stops_typing_coalescing(cx: &mut TestAppContext) {
6354 let input_view = InputView::build(cx, |state| state);
6355 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6356 let input = input_view.input;
6357
6358 cx.update(|window, cx| {
6359 input.update(cx, |state, cx| {
6360 state.replace_text_in_range(None, "a", window, cx);
6361 state.left(&MoveLeft, window, cx);
6362 state.right(&MoveRight, window, cx);
6363 state.replace_text_in_range(None, "b", window, cx);
6364
6365 state.undo(&Undo, window, cx);
6366 assert_eq!(state.value(), "a");
6367 state.undo(&Undo, window, cx);
6368 assert_eq!(state.value(), "");
6369 });
6370 });
6371 }
6372
6373 #[gpui::test]
6374 fn test_undo_manager_noop_edit_breaks_coalescing_without_clearing_history(
6375 cx: &mut TestAppContext,
6376 ) {
6377 let input_view = InputView::build(cx, |state| state);
6378 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6379 let input = input_view.input;
6380
6381 cx.update(|window, cx| {
6382 input.update(cx, |state, cx| {
6383 state.replace_text_in_range(None, "alpha", window, cx);
6384 state.replace_text_in_range(None, "", window, cx);
6385 state.replace_text_in_range(None, "beta", window, cx);
6386 assert_eq!(state.value(), "alphabeta");
6387
6388 state.undo(&Undo, window, cx);
6389 assert_eq!(state.value(), "alpha");
6390 state.undo(&Undo, window, cx);
6391 assert_eq!(state.value(), "");
6392 });
6393 });
6394 }
6395
6396 #[gpui::test]
6397 fn test_undo_manager_masked_redo_restores_actual_cursor(cx: &mut TestAppContext) {
6398 let input_view = InputView::build(cx, |state| {
6399 state.mask_pattern(MaskPattern::Number {
6400 separator: Some(','),
6401 fraction: None,
6402 })
6403 });
6404 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6405 let input = input_view.input;
6406
6407 cx.update(|window, cx| {
6408 input.update(cx, |state, cx| {
6409 state.set_value("12345", window, cx);
6410 state.set_selected_range(2..2, cx);
6411 state.replace_text_in_range(None, "9", window, cx);
6412 let selection_after_edit = state.selected_range();
6413 assert_ne!(selection_after_edit.end, state.value().len());
6414
6415 state.undo(&Undo, window, cx);
6416 state.redo(&Redo, window, cx);
6417 assert_eq!(state.selected_range(), selection_after_edit);
6418 });
6419 });
6420 }
6421
6422 #[gpui::test]
6428 fn test_unfold_at(cx: &mut TestAppContext) {
6429 use crate::input::{FoldRange, Position};
6430
6431 let view = InputView::<EditorMode>::new(cx);
6432 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
6433 let input = view.input;
6434
6435 cx.update(|window, cx| {
6438 input.update(cx, |state, cx| {
6439 state.set_value("a\nb\nc\nd\ne\nf\ng\nh\ni\nj\nk\nl", window, cx);
6440 state.apply_highlighter_fold_candidates(
6441 vec![
6442 FoldRange::new(0, 5),
6443 FoldRange::new(2, 4),
6444 FoldRange::new(7, 10),
6445 ],
6446 cx,
6447 );
6448 state.display_map.set_folded(0, true);
6449 state.display_map.set_folded(2, true);
6450 state.display_map.set_folded(7, true);
6451 });
6452 });
6453
6454 for line in [0, 5] {
6457 cx.update(|_, cx| {
6458 input.update(cx, |state, cx| {
6459 assert!(!state.display_map.is_buffer_line_hidden(line));
6460 assert!(!state.unfold_at(Position::new(line as u32, 0), cx));
6461 });
6462 input.read_with(cx, |state, _| {
6463 assert!(state.display_map.is_folded_at(0));
6464 assert!(state.display_map.is_folded_at(2));
6465 assert!(state.display_map.is_folded_at(7));
6466 });
6467 });
6468 }
6469
6470 cx.update(|_, cx| {
6473 input.update(cx, |state, cx| {
6474 assert!(state.unfold_at(Position::new(3, 0), cx));
6475 });
6476 input.read_with(cx, |state, _| {
6477 assert!(!state.display_map.is_buffer_line_hidden(3));
6478 assert!(!state.display_map.is_folded_at(0));
6479 assert!(!state.display_map.is_folded_at(2));
6480 assert!(state.display_map.is_folded_at(7));
6481 assert!(state.display_map.is_fold_candidate(0));
6483 assert!(state.display_map.is_fold_candidate(2));
6484 });
6485 });
6486
6487 cx.update(|_, cx| {
6489 input.update(cx, |state, cx| {
6490 assert!(!state.unfold_at(Position::new(3, 0), cx));
6491 });
6492 });
6493 }
6494
6495 #[gpui::test]
6500 fn test_blur_keeps_decorations(cx: &mut TestAppContext) {
6501 let view = InputView::<EditorMode>::new(cx);
6502 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
6503 let input = view.input;
6504
6505 cx.update(|window, cx| {
6506 input.update(cx, |state, cx| {
6507 state.set_value("select 1", window, cx);
6508 let _collection = state.create_decorations_collection(
6509 vec![crate::input::TextDecoration::new(
6510 0..6,
6511 gpui::HighlightStyle {
6512 font_weight: Some(gpui::FontWeight::BOLD),
6513 ..Default::default()
6514 },
6515 )],
6516 cx,
6517 );
6518 state.present_hover(
6519 0..6,
6520 lsp_types::Hover {
6521 contents: lsp_types::HoverContents::Scalar(
6522 lsp_types::MarkedString::String("docs".into()),
6523 ),
6524 range: None,
6525 },
6526 cx,
6527 );
6528 assert!(state.hover_popover().is_some());
6529
6530 state.on_blur(window, cx);
6531
6532 assert!(
6533 state.hover_popover().is_none(),
6534 "blur should hide the hover popover"
6535 );
6536 let decorations = state.extras.decoration_layers();
6537 assert!(
6538 decorations.iter().any(|layer| !layer.is_empty()),
6539 "blur must not discard decorations"
6540 );
6541 });
6542 });
6543 }
6544
6545 #[gpui::test]
6550 fn test_kind_does_not_follow_the_row_count(cx: &mut TestAppContext) {
6551 let view = InputView::build_textarea(cx, |state| state.auto_grow(1, 1));
6552 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
6553 view.input.read_with(&mut cx, |state, _| {
6554 assert!(state.is_multi_line());
6555 assert!(!state.is_single_line());
6556 assert!(!state.is_code_editor());
6557 });
6558 }
6559
6560 #[gpui::test]
6565 fn test_soft_wrap_is_enabled_by_default(cx: &mut TestAppContext) {
6566 let textarea = InputView::build_textarea(cx, |state| state);
6567 let mut textarea_cx = VisualTestContext::from_window(textarea.window_handle.into(), cx);
6568 textarea
6569 .input
6570 .read_with(&mut textarea_cx, |state, _| assert!(state.soft_wrap));
6571
6572 let editor = InputView::<EditorMode>::new(cx);
6573 let mut editor_cx = VisualTestContext::from_window(editor.window_handle.into(), cx);
6574 editor
6575 .input
6576 .read_with(&mut editor_cx, |state, _| assert!(state.soft_wrap));
6577 }
6578
6579 fn parse_cursor_spec(input: &str) -> (String, Vec<usize>) {
6583 let mut full_text = String::new();
6584 let mut cursor_offsets = Vec::new();
6585 let non_empty_lines: Vec<&str> = input.lines().filter(|l| !l.is_empty()).collect();
6586
6587 for (line_idx, line) in non_empty_lines.iter().enumerate() {
6588 let mut positions = Vec::new();
6589 let mut text = String::new();
6590 for ch in line.chars() {
6591 if ch == '|' {
6592 positions.push(text.len());
6593 } else {
6594 text.push(ch);
6595 }
6596 }
6597
6598 if line_idx > 0 {
6599 full_text.push('\n');
6600 }
6601 let line_start = full_text.len();
6602 for pos in positions {
6603 cursor_offsets.push(line_start + pos);
6604 }
6605 full_text.push_str(&text);
6606 }
6607 full_text.push('\n');
6608
6609 (full_text, cursor_offsets)
6610 }
6611
6612 fn multi_line(cx: &mut TestAppContext) -> InputView<TextareaMode> {
6614 InputView::build_textarea(cx, |state| state)
6615 }
6616
6617 fn setup_cursors<M: InputModeKind>(
6619 cx: &mut VisualTestContext,
6620 input: &Entity<InputBaseState<M>>,
6621 spec: &str,
6622 ) {
6623 let (full_text, offsets) = parse_cursor_spec(spec);
6624 cx.update(|window, cx| {
6625 input.update(cx, |state, cx| {
6626 state.set_value(&full_text, window, cx);
6627 let selections = offsets
6628 .into_iter()
6629 .map(|offset| {
6630 CursorSelection::new(state.selections.generate_id(), offset, offset)
6631 })
6632 .collect();
6633 state.selections.replace_all(selections);
6634 cx.notify();
6635 });
6636 });
6637 }
6638
6639 #[track_caller]
6641 fn assert_cursors<M: InputModeKind>(
6642 cx: &mut VisualTestContext,
6643 input: &Entity<InputBaseState<M>>,
6644 spec: &str,
6645 ) {
6646 let (expected_text, mut expected_cursors) = parse_cursor_spec(spec);
6647 expected_cursors.sort();
6648
6649 let (actual_text, mut actual_cursors) = input.read_with(cx, |state, _| {
6650 (
6651 state.text.to_string(),
6652 state
6653 .selections
6654 .iter()
6655 .map(|s| s.cursor_offset())
6656 .collect::<Vec<_>>(),
6657 )
6658 });
6659 actual_cursors.sort();
6660
6661 assert_eq!(
6662 actual_text, expected_text,
6663 "Text mismatch:\nExpected: {expected_text:?}\nActual: {actual_text:?}"
6664 );
6665 assert_eq!(
6666 actual_cursors, expected_cursors,
6667 "Cursor mismatch:\nExpected: {expected_cursors:?}\nActual: {actual_cursors:?}"
6668 );
6669 }
6670
6671 #[gpui::test]
6672 fn test_alt_drag_selects_a_block_and_replaces_each_row(cx: &mut TestAppContext) {
6673 cx.update(crate::init);
6674 let view = InputView::<EditorMode>::new(cx);
6675 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
6676 for modifiers in [
6677 gpui::Modifiers {
6678 alt: true,
6679 ..Default::default()
6680 },
6681 gpui::Modifiers {
6682 alt: true,
6683 shift: true,
6684 ..Default::default()
6685 },
6686 #[cfg(target_os = "linux")]
6687 gpui::Modifiers {
6688 alt: true,
6689 control: true,
6690 ..Default::default()
6691 },
6692 ] {
6693 setup_cursors(&mut cx, &view.input, "|abcd\nabcd\nabcd");
6694 cx.update(|window, cx| {
6695 view.input.update(cx, |state, cx| state.focus(window, cx));
6696 });
6697 let (start, end) = view.input.read_with(&cx, |state, _| {
6698 let layout = state.last_layout.as_ref().unwrap();
6699 let origin = state.last_bounds.unwrap().origin;
6700 let position = |row: usize, col| {
6701 let local = layout.lines[row]
6702 .position_for_index(col, layout, false)
6703 .unwrap();
6704 origin
6705 + point(
6706 layout.line_number_width + local.x,
6707 layout.line_height * (row as f32 + 0.5),
6708 )
6709 };
6710 (position(0, 1), position(2, 3))
6711 });
6712 cx.update(|_, cx| {
6714 view.input.update(cx, |state, _| {
6715 state.extras.hover_definition.update(
6716 0..4,
6717 vec![lsp_types::LocationLink {
6718 origin_selection_range: None,
6719 target_uri: "file:///tmp/column-selection.rs".parse().unwrap(),
6720 target_range: Default::default(),
6721 target_selection_range: Default::default(),
6722 }],
6723 );
6724 });
6725 });
6726 cx.simulate_mouse_down(start, MouseButton::Left, modifiers);
6727 cx.simulate_mouse_move(end, MouseButton::Left, modifiers);
6728 cx.simulate_mouse_up(end, MouseButton::Left, modifiers);
6729 view.input.read_with(&cx, |state, _| {
6730 let ranges: Vec<_> = state
6731 .selections
6732 .iter()
6733 .map(|sel| sel.start..sel.end)
6734 .collect();
6735 assert_eq!(ranges, vec![1..3, 6..8, 11..13]);
6736 });
6737 cx.simulate_mouse_move(start, None, modifiers);
6739 cx.simulate_keystrokes("x");
6740 assert_cursors(&mut cx, &view.input, "ax|d\nax|d\nax|d");
6741 }
6742 }
6743
6744 #[gpui::test]
6745 fn test_alt_drag_extends_upward_from_an_existing_cursor(cx: &mut TestAppContext) {
6746 cx.update(crate::init);
6747 let view = InputView::<EditorMode>::new(cx);
6748 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
6749 setup_cursors(&mut cx, &view.input, "abcd\nabcd\na|bcd");
6750 cx.update(|window, cx| {
6751 view.input.update(cx, |state, cx| state.focus(window, cx));
6752 });
6753 let (start, end) = view.input.read_with(&cx, |state, _| {
6754 let layout = state.last_layout.as_ref().unwrap();
6755 let origin = state.last_bounds.unwrap().origin;
6756 let local = layout.lines[0]
6757 .position_for_index(1, layout, false)
6758 .unwrap();
6759 let x = layout.line_number_width + local.x;
6760 (
6761 origin + point(x, layout.line_height * 2.5),
6762 origin + point(x, layout.line_height * 0.5),
6763 )
6764 });
6765 let modifiers = gpui::Modifiers {
6766 alt: true,
6767 ..Default::default()
6768 };
6769 cx.simulate_mouse_down(start, MouseButton::Left, modifiers);
6770 cx.simulate_mouse_move(end, MouseButton::Left, modifiers);
6771 cx.simulate_mouse_up(end, MouseButton::Left, modifiers);
6772 assert_cursors(&mut cx, &view.input, "a|bcd\na|bcd\na|bcd");
6773 }
6774
6775 #[gpui::test]
6776 fn test_alt_drag_over_a_short_row_keeps_the_block_width(cx: &mut TestAppContext) {
6777 cx.update(crate::init);
6778 let view = InputView::<EditorMode>::new(cx);
6779 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
6780 setup_cursors(&mut cx, &view.input, "abcdef\nab\nabcdef");
6781 cx.update(|window, cx| {
6782 view.input.update(cx, |state, cx| state.focus(window, cx));
6783 });
6784 let (start, end) = view.input.read_with(&cx, |state, _| {
6787 let layout = state.last_layout.as_ref().unwrap();
6788 let origin = state.last_bounds.unwrap().origin;
6789 let x_of = |index| {
6790 layout.line_number_width
6791 + layout.lines[0]
6792 .position_for_index(index, layout, false)
6793 .unwrap()
6794 .x
6795 };
6796 (
6797 origin + point(x_of(1), layout.line_height * 0.5),
6798 origin + point(x_of(5), layout.line_height * 1.5),
6799 )
6800 });
6801 let modifiers = gpui::Modifiers {
6802 alt: true,
6803 shift: true,
6804 ..Default::default()
6805 };
6806 cx.simulate_mouse_down(start, MouseButton::Left, modifiers);
6807 cx.simulate_mouse_move(end, MouseButton::Left, modifiers);
6808 cx.simulate_mouse_up(end, MouseButton::Left, modifiers);
6809
6810 view.input.read_with(&cx, |state, _| {
6812 let ranges: Vec<_> = state.selections.iter().map(|s| (s.start, s.end)).collect();
6813 assert_eq!(ranges, vec![(1, 5), (8, 9)]);
6814 });
6815 }
6816
6817 #[gpui::test]
6818 fn test_alt_mouse_release_outside_editor_ends_column_selection(cx: &mut TestAppContext) {
6819 cx.update(crate::init);
6820 let view = InputView::<EditorMode>::new(cx);
6821 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
6822 setup_cursors(&mut cx, &view.input, "|abcd\nabcd");
6823 let position = view.input.read_with(&cx, |state, _| {
6824 let layout = state.last_layout.as_ref().unwrap();
6825 state.last_bounds.unwrap().origin
6826 + point(layout.line_number_width + px(2.), layout.line_height * 0.5)
6827 });
6828 let modifiers = gpui::Modifiers {
6829 alt: true,
6830 ..Default::default()
6831 };
6832 cx.simulate_mouse_down(position, MouseButton::Left, modifiers);
6833 cx.simulate_mouse_up(point(px(-100.), px(-100.)), MouseButton::Left, modifiers);
6834 view.input.read_with(&cx, |state, _| {
6835 assert!(!state.selecting);
6836 assert!(state.column_select_start.is_none());
6837 });
6838 }
6839
6840 #[gpui::test]
6841 fn test_consumed_keystrokes_keep_cursor_visible(cx: &mut TestAppContext) {
6842 let view = InputView::<EditorMode>::new(cx);
6843 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
6844 setup_cursors(&mut cx, &view.input, "a|b");
6845 cx.update(|window, cx| {
6846 view.input.update(cx, |state, cx| {
6847 state.focus(window, cx);
6848 state.pause_blink_cursor(cx);
6849 });
6850 });
6851 cx.run_until_parked();
6852 cx.executor()
6853 .advance_clock(std::time::Duration::from_millis(300));
6854 cx.run_until_parked();
6855 view.input.read_with(&cx, |state, cx| {
6856 assert!(!state.blink_cursor.read(cx).visible());
6857 });
6858 for _ in 0..5 {
6860 #[cfg(target_os = "macos")]
6861 cx.simulate_keystrokes("cmd-c");
6862 #[cfg(not(target_os = "macos"))]
6863 cx.simulate_keystrokes("ctrl-c");
6864 cx.run_until_parked();
6865 cx.executor()
6866 .advance_clock(std::time::Duration::from_millis(200));
6867 cx.run_until_parked();
6868 view.input.read_with(&cx, |state, cx| {
6869 assert!(state.blink_cursor.read(cx).visible());
6870 });
6871 }
6872 }
6873
6874 #[gpui::test]
6875 fn test_multi_cursor_actions_reveal_hidden_carets(cx: &mut TestAppContext) {
6876 let view = InputView::<EditorMode>::new(cx);
6877 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
6878 setup_cursors(&mut cx, &view.input, "ab\na|b\nab");
6879 cx.update(|window, cx| {
6880 view.input.update(cx, |state, cx| {
6881 for action in 0..6 {
6884 state.blink_cursor = cx.new(|_| BlinkCursor::new());
6885 assert!(!state.blink_cursor.read(cx).visible());
6886 match action {
6887 0 => state.add_cursor_above(&AddCursorAbove, window, cx),
6888 1 => state.add_cursor_below(&AddCursorBelow, window, cx),
6889 2 => state.select_up(&SelectUp, window, cx),
6890 3 => state.select_down(&SelectDown, window, cx),
6891 4 => state.replace_text_in_range(None, "x", window, cx),
6892 _ => state.backspace(&Backspace, window, cx),
6893 }
6894 assert!(state.blink_cursor.read(cx).visible(), "action {action}");
6895 }
6896 });
6897 });
6898 }
6899
6900 #[gpui::test]
6901 fn test_multi_cursor_keyboard_dispatch(cx: &mut TestAppContext) {
6902 let view = InputView::<EditorMode>::new(cx);
6903 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
6904 setup_cursors(&mut cx, &view.input, "ab\na|b\nab");
6905 cx.update(|window, cx| {
6906 view.input.update(cx, |state, cx| state.focus(window, cx));
6907 });
6908 #[cfg(target_os = "macos")]
6909 cx.simulate_keystrokes("cmd-alt-up");
6910 #[cfg(target_os = "windows")]
6911 cx.simulate_keystrokes("ctrl-alt-up");
6912 #[cfg(not(any(target_os = "macos", target_os = "windows")))]
6913 cx.simulate_keystrokes("alt-shift-up");
6914 view.input
6915 .read_with(&cx, |state, _| assert_eq!(state.selections.len(), 2));
6916 #[cfg(target_os = "macos")]
6917 cx.simulate_keystrokes("cmd-alt-down");
6918 #[cfg(target_os = "windows")]
6919 cx.simulate_keystrokes("ctrl-alt-down");
6920 #[cfg(not(any(target_os = "macos", target_os = "windows")))]
6921 cx.simulate_keystrokes("alt-shift-down");
6922 view.input
6923 .read_with(&cx, |state, _| assert_eq!(state.selections.len(), 3));
6924 cx.simulate_keystrokes("x");
6925 assert_cursors(&mut cx, &view.input, "ax|b\nax|b\nax|b");
6926 }
6927
6928 #[gpui::test]
6929 fn test_multi_cursor_platform_word_selection_dispatch(cx: &mut TestAppContext) {
6930 let view = InputView::<EditorMode>::new(cx);
6931 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
6932 setup_cursors(&mut cx, &view.input, "one |two\none |two");
6933 cx.update(|window, cx| {
6934 view.input.update(cx, |state, cx| state.focus(window, cx));
6935 });
6936 #[cfg(any(target_os = "macos", target_os = "linux"))]
6937 cx.simulate_keystrokes("alt-shift-right");
6938 #[cfg(not(any(target_os = "macos", target_os = "linux")))]
6939 cx.simulate_keystrokes("ctrl-shift-right");
6940 cx.simulate_keystrokes("x");
6941 assert_cursors(&mut cx, &view.input, "one x|\none x|");
6942 setup_cursors(&mut cx, &view.input, "one two|\none two|");
6943 #[cfg(any(target_os = "macos", target_os = "linux"))]
6944 cx.simulate_keystrokes("alt-shift-left");
6945 #[cfg(not(any(target_os = "macos", target_os = "linux")))]
6946 cx.simulate_keystrokes("ctrl-shift-left");
6947 cx.simulate_keystrokes("x");
6948 assert_cursors(&mut cx, &view.input, "one x|\none x|");
6949 }
6950
6951 #[cfg(not(target_os = "macos"))]
6952 #[gpui::test]
6953 fn test_multi_cursor_horizontal_selection_dispatch(cx: &mut TestAppContext) {
6954 let view = InputView::<EditorMode>::new(cx);
6955 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
6956 setup_cursors(&mut cx, &view.input, "ab\na|b");
6957 cx.update(|window, cx| {
6958 view.input.update(cx, |state, cx| state.focus(window, cx));
6959 });
6960 #[cfg(target_os = "windows")]
6961 cx.simulate_keystrokes("ctrl-alt-up");
6962 #[cfg(not(target_os = "windows"))]
6963 cx.simulate_keystrokes("alt-shift-up");
6964 #[cfg(target_os = "linux")]
6965 cx.simulate_keystrokes("shift-right");
6966 #[cfg(not(target_os = "linux"))]
6967 cx.simulate_keystrokes("alt-shift-right");
6968 view.input.read_with(&cx, |state, _| {
6969 assert_eq!(
6970 state
6971 .selections
6972 .iter()
6973 .map(|s| s.start..s.end)
6974 .collect::<Vec<_>>(),
6975 vec![4..5, 1..2]
6976 );
6977 });
6978 #[cfg(target_os = "linux")]
6979 cx.simulate_keystrokes("shift-left shift-left");
6980 #[cfg(not(target_os = "linux"))]
6981 cx.simulate_keystrokes("alt-shift-left alt-shift-left");
6982 view.input.read_with(&cx, |state, _| {
6983 assert_eq!(
6984 state
6985 .selections
6986 .iter()
6987 .map(|s| s.start..s.end)
6988 .collect::<Vec<_>>(),
6989 vec![3..4, 0..1]
6990 );
6991 });
6992 cx.simulate_keystrokes("x");
6993 assert_cursors(&mut cx, &view.input, "x|b\nx|b");
6994 }
6995
6996 #[gpui::test]
6997 fn test_multi_cursor_alt_click_dispatch(cx: &mut TestAppContext) {
6998 cx.update(crate::init);
6999 let view = InputView::<EditorMode>::new(cx);
7000 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7001 setup_cursors(&mut cx, &view.input, "a|b\nab\nab");
7002 cx.update(|window, cx| {
7003 view.input.update(cx, |state, cx| state.focus(window, cx));
7004 });
7005 let position = view.input.read_with(&cx, |state, _| {
7006 let bounds = state.last_bounds.unwrap();
7007 let layout = state.last_layout.as_ref().unwrap();
7008 bounds.origin + point(layout.line_number_width + px(2.), layout.line_height * 1.5)
7009 });
7010 cx.simulate_click(
7011 position,
7012 gpui::Modifiers {
7013 alt: true,
7014 ..Default::default()
7015 },
7016 );
7017 view.input
7018 .read_with(&cx, |state, _| assert_eq!(state.selections.len(), 2));
7019 }
7020
7021 #[gpui::test]
7022 fn test_word_delete_undo_restores_caret(cx: &mut TestAppContext) {
7023 let view = multi_line(cx);
7024 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7025 setup_cursors(&mut cx, &view.input, "hello|");
7026 cx.update(|window, cx| {
7027 view.input.update(cx, |state, cx| {
7028 state.delete_previous_word(&DeleteToPreviousWordStart, window, cx);
7029 state.undo(&Undo, window, cx);
7030 assert_eq!(state.selected_range(), 5..5);
7031 });
7032 });
7033 }
7034
7035 #[gpui::test]
7036 fn test_merged_delete_undo_restores_all_carets(cx: &mut TestAppContext) {
7037 let view = multi_line(cx);
7038 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7039 setup_cursors(&mut cx, &view.input, "a|b|c");
7040 cx.update(|window, cx| {
7041 view.input.update(cx, |state, cx| {
7042 state.backspace(&Backspace, window, cx);
7043 state.undo(&Undo, window, cx);
7044 });
7045 });
7046 assert_cursors(&mut cx, &view.input, "a|b|c");
7047 }
7048
7049 #[gpui::test]
7050 fn test_shift_end_respects_soft_wrap_end(cx: &mut TestAppContext) {
7051 let view = InputView::<EditorMode>::new(cx);
7052 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7053 cx.update(|window, cx| {
7054 view.input.update(cx, |state, cx| {
7055 state.set_value("abcdef ".repeat(30), window, cx);
7056 state.display_map.on_layout_changed(Some(px(60.)), cx);
7057 let line = state.display_map.line(0).unwrap();
7058 assert!(line.wrapped_lines.len() > 1);
7059 let boundary = line.wrapped_lines[0].end;
7060 state.move_to_with_affinity(boundary, None, true, cx);
7061 state.select_to_end_of_line(&SelectToEndOfLine, window, cx);
7062 assert_eq!(state.selected_range(), boundary..state.text.len());
7063 });
7064 });
7065 }
7066
7067 #[gpui::test]
7068 fn test_outdent_unindented_unicode_is_unchanged(cx: &mut TestAppContext) {
7069 let view = multi_line(cx);
7070 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7071 setup_cursors(&mut cx, &view.input, "|你好\n|世界");
7072 cx.update(|window, cx| {
7073 view.input.update(cx, |state, cx| {
7074 state.outdent(false, window, cx);
7075 state.outdent(true, window, cx);
7076 });
7077 });
7078 assert_cursors(&mut cx, &view.input, "|你好\n|世界");
7079 }
7080
7081 #[gpui::test]
7082 fn test_column_selection_stays_on_unicode_boundaries(cx: &mut TestAppContext) {
7083 let view = multi_line(cx);
7084 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7085 setup_cursors(&mut cx, &view.input, "ab\n你好\ncd");
7086 cx.update(|window, cx| {
7087 view.input.update(cx, |state, cx| {
7088 state.build_columnar_selection(
7089 ColumnarPoint::new(1, 0),
7090 ColumnarPoint::new(11, 0),
7091 cx,
7092 );
7093 let text = state.value();
7094 for sel in state.selections.iter() {
7095 assert!(text.is_char_boundary(sel.start));
7096 assert!(text.is_char_boundary(sel.end));
7097 }
7098 state.replace_text_in_range(None, "X", window, cx);
7099 });
7100 });
7101 }
7102
7103 #[gpui::test]
7104 fn test_editor_decorations_follow_typing(cx: &mut TestAppContext) {
7105 let view = InputView::<EditorMode>::new(cx);
7106 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7107 cx.update(|window, cx| {
7108 view.input.update(cx, |state, cx| {
7109 state.set_value("abc def", window, cx);
7110 state.create_decorations_collection(
7111 vec![crate::input::TextDecoration::new(
7112 4..7,
7113 gpui::HighlightStyle::default(),
7114 )],
7115 cx,
7116 );
7117 state.set_selected_range(0..0, cx);
7118 state.replace_text_in_range(None, "X", window, cx);
7119 let layers = state.extras.decoration_layers();
7120 assert_eq!(layers.into_iter().flatten().next().unwrap().range, 5..8);
7121 });
7122 });
7123 }
7124
7125 #[gpui::test]
7126 fn test_backspace_preserves_escaped_quote_terminator(cx: &mut TestAppContext) {
7127 let view = InputView::<EditorMode>::new(cx);
7128 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7129 setup_cursors(&mut cx, &view.input, r#"{"s":"\"|"}"#);
7130 cx.update(|window, cx| {
7131 view.input.update(cx, |state, cx| {
7132 set_test_syntax_provider(std::rc::Rc::new(StringAllProvider), cx);
7133 state.backspace(&Backspace, window, cx);
7134 });
7135 });
7136 assert_cursors(&mut cx, &view.input, r#"{"s":"\|"}"#);
7137 }
7138
7139 #[gpui::test]
7140 fn test_ordinary_typing_does_not_query_syntax(cx: &mut TestAppContext) {
7141 struct UnexpectedQuery;
7142 impl crate::input::SyntaxContextProvider for UnexpectedQuery {
7143 fn context_at(&self, _: &ropey::Rope, _: usize) -> crate::input::SyntaxContext {
7144 panic!("ordinary typing must not query syntax");
7145 }
7146 }
7147 let view = InputView::<EditorMode>::new(cx);
7148 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7149 setup_cursors(&mut cx, &view.input, "|");
7150 cx.update(|window, cx| {
7151 view.input.update(cx, |state, cx| {
7152 set_test_syntax_provider(std::rc::Rc::new(UnexpectedQuery), cx);
7153 for c in ["a", "b", "c"] {
7154 state.replace_text_in_range(None, c, window, cx);
7155 }
7156 });
7157 });
7158 assert_cursors(&mut cx, &view.input, "abc|");
7159 }
7160
7161 #[gpui::test]
7162 fn test_auto_close_is_atomic_at_history_limit(cx: &mut TestAppContext) {
7163 let view = InputView::<EditorMode>::new(cx);
7164 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7165 setup_cursors(&mut cx, &view.input, "|");
7166 cx.update(|window, cx| {
7167 view.input.update(cx, |state, cx| {
7168 for _ in 0..999 {
7169 state.replace_text_in_range(None, "a", window, cx);
7170 }
7171 state.replace_text_in_range(None, "(", window, cx);
7172 state.undo(&Undo, window, cx);
7173 assert!(!state.text.to_string().contains('('));
7174 assert!(!state.text.to_string().contains(')'));
7175 state.redo(&Redo, window, cx);
7176 });
7177 });
7178 assert_cursors(&mut cx, &view.input, &format!("{}(|)", "a".repeat(999)));
7179 }
7180
7181 #[gpui::test]
7182 fn test_comment_closer_is_inserted_literally(cx: &mut TestAppContext) {
7183 let view = InputView::<EditorMode>::new(cx);
7184 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7185 setup_cursors(&mut cx, &view.input, "// (|)");
7186 cx.update(|window, cx| {
7187 view.input.update(cx, |state, cx| {
7188 set_test_syntax_provider(std::rc::Rc::new(CommentAllProvider), cx);
7189 state.replace_text_in_range(None, ")", window, cx);
7190 });
7191 });
7192 assert_cursors(&mut cx, &view.input, "// ()|)");
7193 }
7194
7195 #[gpui::test]
7196 fn test_explicit_range_skip_does_not_delete_existing_closer_on_undo(cx: &mut TestAppContext) {
7197 let view = InputView::<EditorMode>::new(cx);
7198 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7199 setup_cursors(&mut cx, &view.input, "(a|)");
7200 let before = view.input.read_with(&cx, |state, _| state.text.to_string());
7201 cx.update(|window, cx| {
7202 view.input.update(cx, |state, cx| {
7203 state.replace_text_in_range(Some(2..2), ")", window, cx);
7204 });
7205 });
7206 assert_cursors(&mut cx, &view.input, "(a)|");
7207 cx.update(|window, cx| {
7208 view.input
7209 .update(cx, |state, cx| state.undo(&Undo, window, cx));
7210 });
7211 view.input.read_with(&cx, |state, _| {
7212 assert_eq!(
7213 state.text.to_string(),
7214 before,
7215 "skip must not remove an existing closer on undo"
7216 );
7217 });
7218 }
7219
7220 #[gpui::test]
7221 fn test_pair_redo_restores_interior_cursor(cx: &mut TestAppContext) {
7222 let view = InputView::<EditorMode>::new(cx);
7223 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7224 setup_cursors(&mut cx, &view.input, "|");
7225 cx.update(|window, cx| {
7226 view.input.update(cx, |state, cx| {
7227 state.replace_text_in_range(None, "(", window, cx);
7228 });
7229 });
7230 assert_cursors(&mut cx, &view.input, "(|)");
7231 cx.update(|window, cx| {
7232 view.input.update(cx, |state, cx| {
7233 state.undo(&Undo, window, cx);
7234 state.redo(&Redo, window, cx);
7235 });
7236 });
7237 assert_cursors(&mut cx, &view.input, "(|)");
7238 }
7239
7240 #[gpui::test]
7241 fn test_pair_enter_redo_restores_interior_cursor(cx: &mut TestAppContext) {
7242 let view = InputView::<EditorMode>::new(cx);
7243 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7244 setup_cursors(&mut cx, &view.input, "{|}");
7245 cx.update(|window, cx| {
7246 view.input.update(cx, |state, cx| {
7247 state.enter(
7248 &Enter {
7249 secondary: false,
7250 shift: false,
7251 },
7252 window,
7253 cx,
7254 );
7255 });
7256 });
7257 assert_cursors(&mut cx, &view.input, "{\n |\n}");
7258 cx.update(|window, cx| {
7259 view.input.update(cx, |state, cx| {
7260 state.undo(&Undo, window, cx);
7261 });
7262 });
7263 assert_cursors(&mut cx, &view.input, "{|}");
7264 cx.update(|window, cx| {
7265 view.input
7266 .update(cx, |state, cx| state.redo(&Redo, window, cx));
7267 });
7268 assert_cursors(&mut cx, &view.input, "{\n |\n}");
7269 }
7270
7271 #[gpui::test]
7272 fn test_auto_close_parens(cx: &mut TestAppContext) {
7273 let view = InputView::<EditorMode>::new(cx);
7274 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7275 setup_cursors(&mut cx, &view.input, "|");
7276 cx.update(|window, cx| {
7277 view.input.update(cx, |state, cx| {
7278 state.replace_text_in_range(None, "(", window, cx);
7279 });
7280 });
7281 assert_cursors(&mut cx, &view.input, "(|)");
7282 cx.update(|window, cx| {
7284 view.input.update(cx, |state, cx| {
7285 state.replace_text_in_range(None, "x", window, cx);
7286 });
7287 });
7288 assert_cursors(&mut cx, &view.input, "(x|)");
7289 }
7290
7291 #[gpui::test]
7292 fn test_auto_close_skip_over_closer(cx: &mut TestAppContext) {
7293 let view = InputView::<EditorMode>::new(cx);
7294 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7295 setup_cursors(&mut cx, &view.input, "(|)");
7296 cx.update(|window, cx| {
7297 view.input.update(cx, |state, cx| {
7298 state.replace_text_in_range(None, ")", window, cx);
7299 });
7300 });
7301 assert_cursors(&mut cx, &view.input, "()|");
7303 view.input.read_with(&cx, |state, _| {
7305 assert!(state.active_selection().is_empty());
7306 });
7307 }
7308
7309 #[gpui::test]
7310 fn test_auto_close_no_pair_inside_word(cx: &mut TestAppContext) {
7311 let view = InputView::<EditorMode>::new(cx);
7312 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7313 setup_cursors(&mut cx, &view.input, "don|");
7314 cx.update(|window, cx| {
7315 view.input.update(cx, |state, cx| {
7316 state.replace_text_in_range(None, "'", window, cx);
7317 });
7318 });
7319 assert_cursors(&mut cx, &view.input, "don'|");
7321 }
7322
7323 #[gpui::test]
7324 fn test_auto_close_quote_after_cjk(cx: &mut TestAppContext) {
7325 let view = InputView::<EditorMode>::new(cx);
7326 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7327 setup_cursors(&mut cx, &view.input, "中|");
7330 cx.update(|window, cx| {
7331 view.input.update(cx, |state, cx| {
7332 state.replace_text_in_range(None, "'", window, cx);
7333 });
7334 });
7335 assert_cursors(&mut cx, &view.input, "中'|");
7336 }
7337
7338 #[gpui::test]
7339 fn test_auto_close_closer_skips_at_string_end(cx: &mut TestAppContext) {
7340 let view = InputView::<EditorMode>::new(cx);
7341 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7342 setup_cursors(&mut cx, &view.input, "\"hello|\"");
7344 cx.update(|window, cx| {
7345 view.input.update(cx, |state, cx| {
7346 state.replace_text_in_range(None, "\"", window, cx);
7347 });
7348 });
7349 assert_cursors(&mut cx, &view.input, "\"hello\"|");
7350 view.input.read_with(&cx, |state, _| {
7351 assert!(state.active_selection().is_empty());
7352 });
7353 }
7354
7355 #[gpui::test]
7356 fn test_backspace_pair_with_cjk_prefix(cx: &mut TestAppContext) {
7357 let view = InputView::<EditorMode>::new(cx);
7358 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7359 setup_cursors(&mut cx, &view.input, "中(|)abc");
7361 cx.update(|window, cx| {
7362 view.input
7363 .update(cx, |state, cx| state.backspace(&Backspace, window, cx));
7364 });
7365 assert_cursors(&mut cx, &view.input, "中|abc");
7366 }
7367
7368 #[gpui::test]
7369 fn test_enter_split_respects_smart_indent_off(cx: &mut TestAppContext) {
7370 let view = InputView::<EditorMode>::new(cx);
7371 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7372 setup_cursors(&mut cx, &view.input, "{|}");
7373 cx.update(|window, cx| {
7374 view.input.update(cx, |state, cx| {
7375 state.set_smart_indent(false, window, cx);
7376 state.enter(
7377 &Enter {
7378 secondary: false,
7379 shift: false,
7380 },
7381 window,
7382 cx,
7383 );
7384 });
7385 });
7386 assert_cursors(&mut cx, &view.input, "{\n|}");
7388 }
7389
7390 #[gpui::test]
7391 fn test_enter_split_is_independent_of_auto_close(cx: &mut TestAppContext) {
7392 let view = InputView::<EditorMode>::new(cx);
7393 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7394 setup_cursors(&mut cx, &view.input, "{|}");
7395 cx.update(|window, cx| {
7396 view.input.update(cx, |state, cx| {
7397 state.set_auto_close(false, window, cx);
7398 state.enter(
7399 &Enter {
7400 secondary: false,
7401 shift: false,
7402 },
7403 window,
7404 cx,
7405 );
7406 });
7407 });
7408 assert_cursors(&mut cx, &view.input, "{\n |\n}");
7410 }
7411
7412 #[gpui::test]
7413 fn test_language_config_applies_before_render_and_on_language_change(cx: &mut TestAppContext) {
7414 use crate::input::{AutoClosingPair, set_language_config};
7415 let view = InputView::<EditorMode>::new(cx);
7416 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7417 cx.update(|window, cx| {
7418 set_language_config(
7419 "alpha",
7420 LanguageConfig::default().auto_closing_pairs([AutoClosingPair::new("«", "»")]),
7421 cx,
7422 );
7423 set_language_config(
7424 "beta",
7425 LanguageConfig::default().auto_closing_pairs([AutoClosingPair::new("‹", "›")]),
7426 cx,
7427 );
7428 let first = cx.new(|cx| EditorState::new(window, cx).language("ALPHA"));
7429 let second = cx.new(|cx| EditorState::new(window, cx).language("beta"));
7430 first.update(cx, |state, cx| {
7431 state.replace_text_in_range(None, "«", window, cx);
7432 assert_eq!(state.text().to_string(), "«»");
7433 state.set_value("", window, cx);
7434 state.set_smart_indent(false, window, cx);
7435 state.set_highlighter("beta", cx);
7436 assert!(!state.mode.is_smart_indent());
7437 state.replace_text_in_range(None, "‹", window, cx);
7438 assert_eq!(state.text().to_string(), "‹›");
7439 state.set_value("", window, cx);
7440 state.set_highlighter("unknown", cx);
7441 state.replace_text_in_range(None, "(", window, cx);
7442 assert_eq!(state.text().to_string(), "()");
7443 });
7444 second.update(cx, |state, cx| {
7445 state.replace_text_in_range(None, "‹", window, cx);
7446 assert_eq!(state.text().to_string(), "‹›");
7447 });
7448 });
7449 }
7450
7451 #[gpui::test]
7452 fn test_language_service_replacement_updates_syntax_without_render(cx: &mut TestAppContext) {
7453 use crate::input::{LanguageProvider, SyntaxContextProvider, set_language_provider};
7454 struct StringLanguages(Rc<Cell<usize>>);
7455 impl LanguageProvider for StringLanguages {
7456 fn syntax_context_provider(&self, _: &str) -> Option<Rc<dyn SyntaxContextProvider>> {
7457 self.0.set(self.0.get() + 1);
7458 Some(Rc::new(StringAllProvider))
7459 }
7460 }
7461 struct CodeLanguages;
7462 impl LanguageProvider for CodeLanguages {}
7463 let view = InputView::<EditorMode>::new(cx);
7464 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7465 cx.update(|window, cx| {
7466 let creations = Rc::new(Cell::new(0));
7467 set_language_provider(Rc::new(StringLanguages(creations.clone())), cx);
7468 view.input.update(cx, |state, cx| {
7469 state.replace_text_in_range(None, "(", window, cx);
7470 state.replace_text_in_range(None, "[", window, cx);
7471 assert_eq!(state.text().to_string(), "([");
7472 assert_eq!(
7473 creations.get(),
7474 1,
7475 "retain the document provider between edits"
7476 );
7477 });
7478 set_language_provider(Rc::new(CodeLanguages), cx);
7479 view.input.update(cx, |state, cx| {
7480 state.replace_text_in_range(None, "{", window, cx);
7481 assert_eq!(state.text().to_string(), "([{}");
7482 });
7483 });
7484 }
7485
7486 #[gpui::test]
7487 fn test_language_config_updates_existing_editors(cx: &mut TestAppContext) {
7488 use crate::input::{AutoClosingPair, set_language_config};
7489 let view = InputView::<EditorMode>::new(cx);
7490 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7491 setup_cursors(&mut cx, &view.input, "|");
7492 cx.update(|window, cx| {
7493 view.input.update(cx, |state, cx| {
7494 state.set_highlighter("CUSTOM", cx);
7495 state.set_auto_close(false, window, cx);
7496 state.set_smart_indent(false, window, cx);
7497 });
7498 set_language_config(
7499 "custom",
7500 LanguageConfig::default().auto_closing_pairs([AutoClosingPair::new("«", "»")]),
7501 cx,
7502 );
7503 set_language_config("other", LanguageConfig::default(), cx);
7505 view.input.update(cx, |state, cx| {
7506 assert!(!state.mode.is_auto_close());
7507 assert!(!state.mode.is_smart_indent());
7508 state.set_auto_close(true, window, cx);
7509 state.replace_text_in_range(None, "«", window, cx);
7510 });
7511 });
7512 assert_cursors(&mut cx, &view.input, "«|»");
7513 }
7514
7515 #[gpui::test]
7516 fn test_auto_close_before_is_language_configurable(cx: &mut TestAppContext) {
7517 let view = InputView::<EditorMode>::new(cx);
7518 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7519 setup_cursors(&mut cx, &view.input, "|word");
7520 cx.update(|window, cx| {
7521 view.input.update(cx, |state, cx| {
7522 state.replace_text_in_range(None, "(", window, cx)
7523 });
7524 });
7525 assert_cursors(&mut cx, &view.input, "(|word");
7526 setup_cursors(&mut cx, &view.input, "|word");
7527 cx.update(|window, cx| {
7528 view.input.update(cx, |state, cx| {
7529 crate::input::set_language_config(
7530 state.language_name(),
7531 LanguageConfig::default().auto_close_before("w"),
7532 cx,
7533 );
7534 state.replace_text_in_range(None, "(", window, cx);
7535 });
7536 });
7537 assert_cursors(&mut cx, &view.input, "(|)word");
7538 }
7539
7540 #[gpui::test]
7541 fn test_pair_context_restrictions_are_per_pair(cx: &mut TestAppContext) {
7542 use crate::input::{AutoClosingPair, SyntaxContext};
7543 let view = InputView::<EditorMode>::new(cx);
7544 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7545 setup_cursors(&mut cx, &view.input, "|");
7546 cx.update(|window, cx| {
7547 view.input.update(cx, |state, cx| {
7548 set_test_syntax_provider(std::rc::Rc::new(CommentAllProvider), cx);
7549 crate::input::set_language_config(
7550 state.language_name(),
7551 LanguageConfig::default().auto_closing_pairs([
7552 AutoClosingPair::new("(", ")").not_in([SyntaxContext::String]),
7553 AutoClosingPair::new("[", "]").not_in([SyntaxContext::Comment]),
7554 ]),
7555 cx,
7556 );
7557 state.replace_text_in_range(None, "(", window, cx);
7558 state.replace_text_in_range(None, "[", window, cx);
7559 });
7560 });
7561 assert_cursors(&mut cx, &view.input, "([|)");
7562 }
7563
7564 #[gpui::test]
7565 fn test_multichar_delimiters_insert_delete_and_skip(cx: &mut TestAppContext) {
7566 use crate::input::{AutoClosingPair, SyntaxContext};
7567 struct BlockComment;
7568 impl crate::input::SyntaxContextProvider for BlockComment {
7569 fn context_at(&self, text: &ropey::Rope, _: usize) -> SyntaxContext {
7570 if text.to_string().contains("/*") {
7571 SyntaxContext::Comment
7572 } else {
7573 SyntaxContext::Code
7574 }
7575 }
7576 }
7577 let view = InputView::<EditorMode>::new(cx);
7578 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7579 setup_cursors(&mut cx, &view.input, "|");
7580 cx.update(|window, cx| {
7581 view.input.update(cx, |state, cx| {
7582 set_test_syntax_provider(std::rc::Rc::new(BlockComment), cx);
7583 crate::input::set_language_config(
7584 state.language_name(),
7585 LanguageConfig::default()
7586 .auto_closing_pairs([AutoClosingPair::new("/*", "*/")
7587 .not_in([SyntaxContext::String, SyntaxContext::Comment])]),
7588 cx,
7589 );
7590 state.replace_text_in_range(None, "/", window, cx);
7591 state.replace_text_in_range(None, "*", window, cx);
7592 });
7593 });
7594 assert_cursors(&mut cx, &view.input, "/*|*/");
7595 cx.update(|window, cx| {
7596 view.input
7597 .update(cx, |state, cx| state.backspace(&Backspace, window, cx));
7598 });
7599 assert_cursors(&mut cx, &view.input, "|");
7600 cx.update(|window, cx| {
7601 view.input.update(cx, |state, cx| {
7602 state.undo(&Undo, window, cx);
7603 state.replace_text_in_range(None, "*", window, cx);
7604 state.replace_text_in_range(None, "/", window, cx);
7605 });
7606 });
7607 assert_cursors(&mut cx, &view.input, "/**/|");
7608 }
7609
7610 #[gpui::test]
7611 fn test_auto_closed_pairs_fallback_and_explicit_disable(cx: &mut TestAppContext) {
7612 use crate::input::BracketPair;
7613 let view = InputView::<EditorMode>::new(cx);
7614 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7615 setup_cursors(&mut cx, &view.input, "|");
7616 cx.update(|window, cx| {
7617 view.input.update(cx, |state, cx| {
7618 let mut rules = LanguageConfig::default().brackets([BracketPair::new("«", "»")]);
7619 rules.auto_closing_pairs = None;
7620 crate::input::set_language_config(state.language_name(), rules.clone(), cx);
7621 state.replace_text_in_range(None, "«", window, cx);
7622 crate::input::set_language_config(
7623 state.language_name(),
7624 rules.auto_closing_pairs([]),
7625 cx,
7626 );
7627 state.replace_text_in_range(None, "«", window, cx);
7628 });
7629 });
7630 assert_cursors(&mut cx, &view.input, "««|»");
7631 }
7632
7633 #[gpui::test]
7634 fn test_undo_open_composition_preserves_generated_pairs(cx: &mut TestAppContext) {
7635 use crate::input::{AutoClosingPair, SyntaxContext};
7636 let view = InputView::<EditorMode>::new(cx);
7637 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7638 setup_cursors(&mut cx, &view.input, "|");
7639 cx.update(|window, cx| {
7640 view.input.update(cx, |state, cx| {
7641 crate::input::set_language_config(
7642 state.language_name(),
7643 LanguageConfig::default()
7644 .auto_closing_pairs([
7645 AutoClosingPair::new("/*", "*/").not_in([SyntaxContext::Comment])
7646 ]),
7647 cx,
7648 );
7649 state.replace_text_in_range(None, "/", window, cx);
7650 state.replace_text_in_range(None, "*", window, cx);
7651 set_test_syntax_provider(std::rc::Rc::new(CommentAllProvider), cx);
7652 state.replace_and_mark_text_in_range(None, "x", Some(1..1), window, cx);
7653 state.undo(&Undo, window, cx);
7654 state.replace_text_in_range(None, "*", window, cx);
7655 state.replace_text_in_range(None, "/", window, cx);
7656 });
7657 });
7658 assert_cursors(&mut cx, &view.input, "/**/|");
7659 }
7660
7661 #[gpui::test]
7662 fn test_undo_does_not_promote_literal_comment_delimiters(cx: &mut TestAppContext) {
7663 use crate::input::{AutoClosingPair, SyntaxContext};
7664 let view = InputView::<EditorMode>::new(cx);
7665 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7666 setup_cursors(&mut cx, &view.input, "/*|*/");
7667 cx.update(|window, cx| {
7668 view.input.update(cx, |state, cx| {
7669 set_test_syntax_provider(std::rc::Rc::new(CommentAllProvider), cx);
7670 crate::input::set_language_config(
7671 state.language_name(),
7672 LanguageConfig::default()
7673 .auto_closing_pairs([
7674 AutoClosingPair::new("/*", "*/").not_in([SyntaxContext::Comment])
7675 ]),
7676 cx,
7677 );
7678 state.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
7679 state.undo(&Undo, window, cx);
7680 state.backspace(&Backspace, window, cx);
7681 });
7682 });
7683 assert_cursors(&mut cx, &view.input, "/|*/");
7684 }
7685
7686 #[gpui::test]
7687 fn test_multiple_generated_pairs_retain_their_identity(cx: &mut TestAppContext) {
7688 use crate::input::{AutoClosingPair, SyntaxContext};
7689 struct CommentScope;
7690 impl crate::input::SyntaxContextProvider for CommentScope {
7691 fn context_at(&self, text: &ropey::Rope, offset: usize) -> SyntaxContext {
7692 if text
7693 .to_string()
7694 .find("*/")
7695 .is_some_and(|end| offset < end + 2)
7696 {
7697 SyntaxContext::Comment
7698 } else {
7699 SyntaxContext::Code
7700 }
7701 }
7702 }
7703 let view = InputView::<EditorMode>::new(cx);
7704 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7705 setup_cursors(&mut cx, &view.input, "|");
7706 cx.update(|window, cx| {
7707 view.input.update(cx, |state, cx| {
7708 set_test_syntax_provider(std::rc::Rc::new(CommentScope), cx);
7709 crate::input::set_language_config(
7710 state.language_name(),
7711 LanguageConfig::default().auto_closing_pairs([
7712 AutoClosingPair::new("/*", "*/").not_in([SyntaxContext::Comment]),
7713 AutoClosingPair::new("(", ")").not_in([SyntaxContext::Comment]),
7714 ]),
7715 cx,
7716 );
7717 state.replace_text_in_range(None, "/", window, cx);
7718 state.replace_text_in_range(None, "*", window, cx);
7719 state.set_selected_range(4..4, cx);
7720 state.replace_text_in_range(None, "(", window, cx);
7721 state.set_selected_range(2..2, cx);
7722 state.replace_text_in_range(None, "*", window, cx);
7723 state.replace_text_in_range(None, "/", window, cx);
7724 });
7725 });
7726 assert_cursors(&mut cx, &view.input, "/**/|()");
7727 cx.update(|window, cx| {
7728 view.input.update(cx, |state, cx| {
7729 state.set_selected_range(2..2, cx);
7730 state.backspace(&Backspace, window, cx);
7731 state.undo(&Undo, window, cx);
7732 state.redo(&Redo, window, cx);
7733 });
7734 });
7735 assert_cursors(&mut cx, &view.input, "|()");
7736 }
7737
7738 #[gpui::test]
7739 fn test_replacing_generated_opener_invalidates_the_pair(cx: &mut TestAppContext) {
7740 use crate::input::{AutoClosingPair, SyntaxContext};
7741 let view = InputView::<EditorMode>::new(cx);
7742 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7743 setup_cursors(&mut cx, &view.input, "|");
7744 cx.update(|window, cx| {
7745 view.input.update(cx, |state, cx| {
7746 crate::input::set_language_config(
7747 state.language_name(),
7748 LanguageConfig::default()
7749 .auto_closing_pairs([
7750 AutoClosingPair::new("/*", "*/").not_in([SyntaxContext::Comment])
7751 ]),
7752 cx,
7753 );
7754 state.replace_text_in_range(None, "/", window, cx);
7755 state.replace_text_in_range(None, "*", window, cx);
7756 state.replace_text_in_range(Some(0..2), "//", window, cx);
7757 set_test_syntax_provider(std::rc::Rc::new(CommentAllProvider), cx);
7758 state.replace_text_in_range(None, "*", window, cx);
7759 });
7760 });
7761 assert_cursors(&mut cx, &view.input, "//*|*/");
7762 }
7763
7764 #[gpui::test]
7765 fn test_configuration_preserves_compiled_regex_options(cx: &mut TestAppContext) {
7766 use crate::input::IndentationRules;
7767 let view = InputView::<EditorMode>::new(cx);
7768 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7769 setup_cursors(&mut cx, &view.input, "BEGIN|");
7770 cx.update(|window, cx| {
7771 view.input.update(cx, |state, cx| {
7772 let rules = |insensitive| {
7773 LanguageConfig::default().indentation_rules(IndentationRules::new(
7774 regex::RegexBuilder::new("begin$")
7775 .case_insensitive(insensitive)
7776 .build()
7777 .unwrap(),
7778 regex::Regex::new("^end").unwrap(),
7779 ))
7780 };
7781 crate::input::set_language_config(state.language_name(), rules(false), cx);
7782 crate::input::set_language_config(state.language_name(), rules(true), cx);
7783 state.enter(
7784 &Enter {
7785 secondary: false,
7786 shift: false,
7787 },
7788 window,
7789 cx,
7790 );
7791 });
7792 });
7793 assert_cursors(&mut cx, &view.input, "BEGIN\n |");
7794 }
7795
7796 #[gpui::test]
7797 fn test_indent_patterns_are_applied_on_enter(cx: &mut TestAppContext) {
7798 use crate::input::IndentationRules;
7799 let view = InputView::<EditorMode>::new(cx);
7800 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7801 let rules = LanguageConfig::default().indentation_rules(IndentationRules::new(
7802 regex::Regex::new(r"\bbegin\s*$").unwrap(),
7803 regex::Regex::new(r"^\s*end\b").unwrap(),
7804 ));
7805 for (before, expected) in [("begin|", "begin\n |"), (" |end", " \n|end")] {
7806 setup_cursors(&mut cx, &view.input, before);
7807 cx.update(|window, cx| {
7808 view.input.update(cx, |state, cx| {
7809 crate::input::set_language_config(state.language_name(), rules.clone(), cx);
7810 state.enter(
7811 &Enter {
7812 secondary: false,
7813 shift: false,
7814 },
7815 window,
7816 cx,
7817 );
7818 });
7819 });
7820 assert_cursors(&mut cx, &view.input, expected);
7821 }
7822 }
7823
7824 #[gpui::test]
7825 fn test_custom_language_config(cx: &mut TestAppContext) {
7826 let view = InputView::<EditorMode>::new(cx);
7827 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7828 cx.update(|_window, cx| {
7829 view.input.update(cx, |state, cx| {
7830 crate::input::set_language_config(
7831 state.language_name(),
7832 crate::input::language_config::LanguageConfig::default()
7833 .brackets([crate::input::BracketPair::new("«", "»")])
7834 .auto_closing_pairs([crate::input::AutoClosingPair::new("«", "»")]),
7835 cx,
7836 );
7837 });
7838 });
7839 setup_cursors(&mut cx, &view.input, "|");
7841 cx.update(|window, cx| {
7842 view.input.update(cx, |state, cx| {
7843 state.replace_text_in_range(None, "«", window, cx);
7844 });
7845 });
7846 assert_cursors(&mut cx, &view.input, "«|»");
7847 cx.update(|window, cx| {
7849 view.input.update(cx, |state, cx| {
7850 state.set_value("\n", window, cx);
7851 state.set_selected_range(0..0, cx);
7852 state.replace_text_in_range(None, "(", window, cx);
7853 });
7854 });
7855 assert_cursors(&mut cx, &view.input, "(|");
7856 }
7857
7858 #[gpui::test]
7859 fn test_skip_records_no_history(cx: &mut TestAppContext) {
7860 let view = InputView::<EditorMode>::new(cx);
7861 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7862 setup_cursors(&mut cx, &view.input, "(a|)");
7863 cx.update(|window, cx| {
7864 view.input.update(cx, |state, cx| {
7865 state.replace_text_in_range(None, "b", window, cx);
7867 });
7868 });
7869 assert_cursors(&mut cx, &view.input, "(ab|)");
7870 cx.update(|window, cx| {
7871 view.input.update(cx, |state, cx| {
7872 state.replace_text_in_range(None, ")", window, cx);
7873 });
7874 });
7875 assert_cursors(&mut cx, &view.input, "(ab)|");
7877 cx.update(|window, cx| {
7879 view.input
7880 .update(cx, |state, cx| state.undo(&Undo, window, cx));
7881 });
7882 assert_cursors(&mut cx, &view.input, "(a|)");
7883 }
7884
7885 #[gpui::test]
7886 fn test_pair_insert_undo_removes_both(cx: &mut TestAppContext) {
7887 let view = InputView::<EditorMode>::new(cx);
7888 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7889 setup_cursors(&mut cx, &view.input, "|");
7890 cx.update(|window, cx| {
7891 view.input.update(cx, |state, cx| {
7892 state.replace_text_in_range(None, "(", window, cx);
7893 });
7894 });
7895 assert_cursors(&mut cx, &view.input, "(|)");
7896 cx.update(|window, cx| {
7898 view.input
7899 .update(cx, |state, cx| state.undo(&Undo, window, cx));
7900 });
7901 assert_cursors(&mut cx, &view.input, "|");
7902 }
7903
7904 #[gpui::test]
7905 fn test_escaped_quote_does_not_skip(cx: &mut TestAppContext) {
7906 let view = InputView::<EditorMode>::new(cx);
7907 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7908 setup_cursors(&mut cx, &view.input, "\"hello\\|\"");
7910 cx.update(|window, cx| {
7911 view.input.update(cx, |state, cx| {
7912 state.replace_text_in_range(None, "\"", window, cx);
7913 });
7914 });
7915 assert_cursors(&mut cx, &view.input, "\"hello\\\"|\"");
7916 }
7917
7918 #[gpui::test]
7919 fn test_even_backslashes_still_skip(cx: &mut TestAppContext) {
7920 let view = InputView::<EditorMode>::new(cx);
7921 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7922 setup_cursors(&mut cx, &view.input, "\"hello\\\\|\"");
7924 cx.update(|window, cx| {
7925 view.input.update(cx, |state, cx| {
7926 state.replace_text_in_range(None, "\"", window, cx);
7927 });
7928 });
7929 assert_cursors(&mut cx, &view.input, "\"hello\\\\\"|");
7930 }
7931
7932 #[gpui::test]
7933 fn test_editor_options_survive_configuration_replacement(cx: &mut TestAppContext) {
7934 use crate::input::{BracketPair, LanguageConfig};
7935
7936 let view = InputView::<EditorMode>::new(cx);
7937 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7938 cx.update(|window, cx| {
7939 view.input.update(cx, |state, cx| {
7940 state.set_smart_indent(false, window, cx);
7942 crate::input::set_language_config(
7943 state.language_name(),
7944 LanguageConfig::default().brackets([BracketPair::new("«", "»")]),
7945 cx,
7946 );
7947 });
7948 });
7949 view.input.read_with(&cx, |state, _| {
7950 assert!(!state.mode.is_smart_indent(), "editor option preserved");
7951 assert!(state.mode.is_auto_close());
7952 let pairs = &state.mode.language_config().unwrap().brackets;
7953 assert_eq!(pairs.len(), 1, "pairs follow configuration");
7954 assert_eq!(pairs[0].open.as_ref(), "«");
7955 });
7956 }
7957
7958 struct CommentAllProvider;
7959 impl crate::input::SyntaxContextProvider for CommentAllProvider {
7960 fn context_at(&self, _text: &ropey::Rope, _offset: usize) -> crate::input::SyntaxContext {
7961 crate::input::SyntaxContext::Comment
7962 }
7963 }
7964
7965 struct StringAllProvider;
7966 impl crate::input::SyntaxContextProvider for StringAllProvider {
7967 fn context_at(&self, _text: &ropey::Rope, _offset: usize) -> crate::input::SyntaxContext {
7968 crate::input::SyntaxContext::String
7969 }
7970 }
7971
7972 #[gpui::test]
7973 fn test_smart_indent_suppressed_in_strings(cx: &mut TestAppContext) {
7974 use std::rc::Rc;
7975
7976 let view = InputView::<EditorMode>::new(cx);
7977 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7978 cx.update(|_window, cx| {
7979 set_test_syntax_provider(Rc::new(StringAllProvider), cx);
7980 });
7981 setup_cursors(&mut cx, &view.input, " x = {|");
7983 cx.update(|window, cx| {
7984 view.input.update(cx, |state, cx| {
7985 state.enter(
7986 &Enter {
7987 secondary: false,
7988 shift: false,
7989 },
7990 window,
7991 cx,
7992 );
7993 });
7994 });
7995 assert_cursors(&mut cx, &view.input, " x = {\n |");
7996 }
7997
7998 #[gpui::test]
7999 fn test_split_suppressed_in_strings(cx: &mut TestAppContext) {
8000 use std::rc::Rc;
8001
8002 let view = InputView::<EditorMode>::new(cx);
8003 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8004 cx.update(|_window, cx| {
8005 set_test_syntax_provider(Rc::new(StringAllProvider), cx);
8006 });
8007 setup_cursors(&mut cx, &view.input, "{|}");
8009 cx.update(|window, cx| {
8010 view.input.update(cx, |state, cx| {
8011 state.enter(
8012 &Enter {
8013 secondary: false,
8014 shift: false,
8015 },
8016 window,
8017 cx,
8018 );
8019 });
8020 });
8021 assert_cursors(&mut cx, &view.input, "{\n|}");
8022 }
8023
8024 #[gpui::test]
8025 fn test_comment_context_disables_pairing(cx: &mut TestAppContext) {
8026 use std::rc::Rc;
8027
8028 let view = InputView::<EditorMode>::new(cx);
8029 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8030 cx.update(|_window, cx| {
8031 set_test_syntax_provider(Rc::new(CommentAllProvider), cx);
8032 });
8033 setup_cursors(&mut cx, &view.input, "|");
8034 cx.update(|window, cx| {
8035 view.input.update(cx, |state, cx| {
8036 state.replace_text_in_range(None, "(", window, cx);
8037 });
8038 });
8039 assert_cursors(&mut cx, &view.input, "(|");
8041 }
8042
8043 #[gpui::test]
8044 fn test_backspace_deletes_empty_pair(cx: &mut TestAppContext) {
8045 let view = InputView::<EditorMode>::new(cx);
8046 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8047 setup_cursors(&mut cx, &view.input, "{|}");
8048 cx.update(|window, cx| {
8049 view.input
8050 .update(cx, |state, cx| state.backspace(&Backspace, window, cx));
8051 });
8052 assert_cursors(&mut cx, &view.input, "|");
8053 }
8054
8055 #[gpui::test]
8056 fn test_backspace_keeps_closer_with_content(cx: &mut TestAppContext) {
8057 let view = InputView::<EditorMode>::new(cx);
8058 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8059 setup_cursors(&mut cx, &view.input, "{d|}");
8060 cx.update(|window, cx| {
8061 view.input
8062 .update(cx, |state, cx| state.backspace(&Backspace, window, cx));
8063 });
8064 assert_cursors(&mut cx, &view.input, "{|}");
8066 }
8067
8068 #[gpui::test]
8069 fn test_enter_splits_brackets(cx: &mut TestAppContext) {
8070 let view = InputView::<EditorMode>::new(cx);
8071 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8072 setup_cursors(&mut cx, &view.input, "{|}");
8073 cx.update(|window, cx| {
8074 view.input.update(cx, |state, cx| {
8075 state.enter(
8076 &Enter {
8077 secondary: false,
8078 shift: false,
8079 },
8080 window,
8081 cx,
8082 );
8083 });
8084 });
8085 assert_cursors(&mut cx, &view.input, "{\n |\n}");
8086 view.input.read_with(&cx, |state, _| {
8088 assert!(state.active_selection().is_empty());
8089 });
8090 cx.update(|window, cx| {
8091 view.input.update(cx, |state, cx| {
8092 state.replace_text_in_range(None, "x", window, cx);
8093 });
8094 });
8095 assert_cursors(&mut cx, &view.input, "{\n x|\n}");
8096 }
8097
8098 #[gpui::test]
8099 fn test_enter_smart_indent_after_brace(cx: &mut TestAppContext) {
8100 let view = InputView::<EditorMode>::new(cx);
8101 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8102 setup_cursors(&mut cx, &view.input, "fn main() {|");
8103 cx.update(|window, cx| {
8104 view.input.update(cx, |state, cx| {
8105 state.enter(
8106 &Enter {
8107 secondary: false,
8108 shift: false,
8109 },
8110 window,
8111 cx,
8112 );
8113 });
8114 });
8115 assert_cursors(&mut cx, &view.input, "fn main() {\n |");
8116 }
8117
8118 #[gpui::test]
8119 fn test_block_indent_tracks_all_preceding_edits(cx: &mut TestAppContext) {
8120 let view = multi_line(cx);
8121 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8122 setup_cursors(&mut cx, &view.input, "|ab\n|cd\n|ef");
8123 cx.update(|window, cx| {
8124 view.input
8125 .update(cx, |state, cx| state.indent(true, window, cx));
8126 });
8127 assert_cursors(&mut cx, &view.input, " |ab\n |cd\n |ef");
8128 cx.update(|window, cx| {
8129 view.input
8130 .update(cx, |state, cx| state.outdent(true, window, cx));
8131 });
8132 assert_cursors(&mut cx, &view.input, "|ab\n|cd\n|ef");
8133 }
8134
8135 #[gpui::test]
8136 fn test_block_outdent_clamps_cursor_inside_indent(cx: &mut TestAppContext) {
8137 let view = multi_line(cx);
8138 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8139 setup_cursors(&mut cx, &view.input, "ab\n | cd");
8140 cx.update(|window, cx| {
8141 view.input
8142 .update(cx, |state, cx| state.outdent(true, window, cx));
8143 });
8144 assert_cursors(&mut cx, &view.input, "ab\n|cd");
8145 }
8146
8147 #[gpui::test]
8148 fn test_ime_restores_original_selection(cx: &mut TestAppContext) {
8149 let view = InputView::build(cx, |state| state);
8150 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8151 cx.update(|window, cx| {
8152 view.input.update(cx, |state, cx| {
8153 state.set_value("abc", window, cx);
8154 state.set_selected_range(1..2, cx);
8155 state.replace_and_mark_text_in_range(None, "ni", None, window, cx);
8156 state.replace_text_in_range(None, "你", window, cx);
8157 state.undo(&Undo, window, cx);
8158 assert_eq!(state.value(), "abc");
8159 assert_eq!(state.selected_range(), 1..2);
8160 state.redo(&Redo, window, cx);
8161 assert_eq!(state.selected_range(), 4..4);
8162 });
8163 });
8164 }
8165
8166 #[gpui::test]
8167 fn test_noop_does_not_change_redo_selection(cx: &mut TestAppContext) {
8168 let view = multi_line(cx);
8169 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8170 cx.update(|window, cx| {
8171 view.input.update(cx, |state, cx| {
8172 state.set_value("abc", window, cx);
8173 state.set_selected_range(1..1, cx);
8174 state.replace_text_in_range(None, "X", window, cx);
8175 state.set_selected_range(0..0, cx);
8176 state.replace_text_in_range(None, "", window, cx);
8177 state.undo(&Undo, window, cx);
8178 state.redo(&Redo, window, cx);
8179 assert_eq!(state.value(), "aXbc");
8180 assert_eq!(state.selected_range(), 2..2);
8181 });
8182 });
8183 }
8184
8185 #[gpui::test]
8186 fn test_multi_cursor_insert_text(cx: &mut TestAppContext) {
8187 let view = multi_line(cx);
8188 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8189 let input = view.input;
8190
8191 setup_cursors(&mut cx, &input, "|hello |world|");
8192 cx.update(|window, cx| {
8193 input.update(cx, |state, cx| {
8194 state.replace_text_in_range(None, ">>>", window, cx);
8195 });
8196 });
8197 assert_cursors(&mut cx, &input, ">>>|hello >>>|world>>>|");
8198 }
8199
8200 #[gpui::test]
8201 fn test_multi_cursor_delete_backward(cx: &mut TestAppContext) {
8202 let view = multi_line(cx);
8203 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8204 let input = view.input;
8205
8206 setup_cursors(&mut cx, &input, "|islands| cars|");
8207 cx.update(|window, cx| {
8208 input.update(cx, |state, cx| {
8209 state.backspace(&Backspace, window, cx);
8210 });
8211 });
8212 assert_cursors(&mut cx, &input, "|island| car|");
8214 }
8215
8216 #[gpui::test]
8217 fn test_multi_cursor_delete_forward_merges(cx: &mut TestAppContext) {
8218 let view = multi_line(cx);
8219 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8220 let input = view.input;
8221
8222 setup_cursors(&mut cx, &input, "hello| |world");
8223 cx.update(|window, cx| {
8224 input.update(cx, |state, cx| {
8225 state.delete(&Delete, window, cx);
8226 });
8227 });
8228 assert_cursors(&mut cx, &input, "hello|orld");
8230 }
8231
8232 #[gpui::test]
8233 fn test_multi_cursor_multiline_insert_and_delete(cx: &mut TestAppContext) {
8234 let view = multi_line(cx);
8235 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8236 let input = view.input;
8237
8238 setup_cursors(&mut cx, &input, "|1\n|2\n|3");
8239 cx.update(|window, cx| {
8240 input.update(cx, |state, cx| {
8241 state.replace_text_in_range(None, "a", window, cx);
8242 });
8243 });
8244 assert_cursors(&mut cx, &input, "a|1\na|2\na|3");
8245
8246 input.read_with(&cx, |state, _| {
8248 assert_eq!(state.undo_manager.undo_count(), 1);
8249 });
8250
8251 cx.update(|window, cx| {
8252 input.update(cx, |state, cx| {
8253 state.backspace(&Backspace, window, cx);
8254 });
8255 });
8256 assert_cursors(&mut cx, &input, "|1\n|2\n|3");
8257 }
8258
8259 #[gpui::test]
8260 fn test_add_cursor_below_preserves_column(cx: &mut TestAppContext) {
8261 let view = multi_line(cx);
8262 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8263 let input = view.input;
8264
8265 setup_cursors(&mut cx, &input, "ab|cd\nabcd");
8266 cx.update(|window, cx| {
8267 input.update(cx, |state, cx| {
8268 state.add_cursor_below(&AddCursorBelow, window, cx);
8269 });
8270 });
8271 assert_cursors(&mut cx, &input, "ab|cd\nab|cd");
8272 }
8273
8274 #[gpui::test]
8275 fn test_add_cursor_at_rejects_duplicates(cx: &mut TestAppContext) {
8276 let view = multi_line(cx);
8277 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8278 let input = view.input;
8279
8280 setup_cursors(&mut cx, &input, "he|llo");
8281 cx.update(|_, cx| {
8282 input.update(cx, |state, cx| {
8283 state.add_cursor_at(2, cx);
8285 assert_eq!(state.selections.len(), 1);
8286 state.add_cursor_at(4, cx);
8288 assert_eq!(state.selections.len(), 2);
8289 });
8290 });
8291 }
8292
8293 #[gpui::test]
8294 fn test_multi_cursor_undo_redo_restores_selections(cx: &mut TestAppContext) {
8295 let view = multi_line(cx);
8296 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8297 let input = view.input;
8298
8299 setup_cursors(&mut cx, &input, "|1\n|2\n|3");
8300 cx.update(|window, cx| {
8301 input.update(cx, |state, cx| {
8302 state.replace_text_in_range(None, "a", window, cx);
8303 });
8304 });
8305 assert_cursors(&mut cx, &input, "a|1\na|2\na|3");
8306
8307 cx.update(|window, cx| {
8308 input.update(cx, |state, cx| {
8309 state.undo(&Undo, window, cx);
8310 });
8311 });
8312 assert_cursors(&mut cx, &input, "|1\n|2\n|3");
8313
8314 cx.update(|window, cx| {
8315 input.update(cx, |state, cx| {
8316 state.redo(&Redo, window, cx);
8317 });
8318 });
8319 assert_cursors(&mut cx, &input, "a|1\na|2\na|3");
8320 }
8321
8322 #[gpui::test]
8323 fn test_multi_cursor_undo_redo_different_line_lengths(cx: &mut TestAppContext) {
8324 let view = multi_line(cx);
8325 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8326 let input = view.input;
8327
8328 setup_cursors(&mut cx, &input, "abc123|\nabc12345|\nabc1234567|");
8329 cx.update(|window, cx| {
8330 input.update(cx, |state, cx| {
8331 state.replace_text_in_range(None, "a", window, cx);
8332 });
8333 });
8334 assert_cursors(&mut cx, &input, "abc123a|\nabc12345a|\nabc1234567a|");
8335
8336 cx.update(|window, cx| {
8337 input.update(cx, |state, cx| {
8338 state.undo(&Undo, window, cx);
8339 });
8340 });
8341 assert_cursors(&mut cx, &input, "abc123|\nabc12345|\nabc1234567|");
8342
8343 cx.update(|window, cx| {
8344 input.update(cx, |state, cx| {
8345 state.redo(&Redo, window, cx);
8346 });
8347 });
8348 assert_cursors(&mut cx, &input, "abc123a|\nabc12345a|\nabc1234567a|");
8349 }
8350
8351 #[gpui::test]
8352 fn test_multi_cursor_undo_multiple_inserts(cx: &mut TestAppContext) {
8353 let view = multi_line(cx);
8354 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8355 let input = view.input;
8356
8357 setup_cursors(&mut cx, &input, "|1\n|2\n|3");
8358 for ch in ['a', 'b', 'c'] {
8359 cx.update(|window, cx| {
8360 input.update(cx, |state, cx| {
8361 state.replace_text_in_range(None, &ch.to_string(), window, cx);
8362 });
8363 });
8364 }
8365 assert_cursors(&mut cx, &input, "abc|1\nabc|2\nabc|3");
8366
8367 cx.update(|window, cx| {
8369 input.update(cx, |state, cx| {
8370 assert_eq!(state.undo_manager.undo_count(), 1);
8371 state.undo(&Undo, window, cx);
8372 });
8373 });
8374 assert_cursors(&mut cx, &input, "|1\n|2\n|3");
8375
8376 cx.update(|window, cx| {
8377 input.update(cx, |state, cx| {
8378 state.redo(&Redo, window, cx);
8379 });
8380 });
8381 assert_cursors(&mut cx, &input, "abc|1\nabc|2\nabc|3");
8382 }
8383
8384 #[gpui::test]
8385 fn test_multi_cursor_backspace_run_is_one_undo(cx: &mut TestAppContext) {
8386 let view = multi_line(cx);
8387 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8388 let input = view.input;
8389
8390 setup_cursors(&mut cx, &input, "abc|1\nabc|2\nabc|3");
8391 for _ in 0..3 {
8392 cx.update(|window, cx| {
8393 input.update(cx, |state, cx| {
8394 state.backspace(&Backspace, window, cx);
8395 });
8396 });
8397 }
8398 assert_cursors(&mut cx, &input, "|1\n|2\n|3");
8399
8400 cx.update(|window, cx| {
8401 input.update(cx, |state, cx| {
8402 assert_eq!(state.undo_manager.undo_count(), 1);
8403 state.undo(&Undo, window, cx);
8404 });
8405 });
8406 assert_cursors(&mut cx, &input, "abc|1\nabc|2\nabc|3");
8407 }
8408
8409 #[gpui::test]
8410 fn test_adding_a_cursor_splits_the_typing_gesture(cx: &mut TestAppContext) {
8411 let view = multi_line(cx);
8412 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8413 let input = view.input;
8414
8415 setup_cursors(&mut cx, &input, "|1\n2\n3");
8416 cx.update(|window, cx| {
8417 input.update(cx, |state, cx| {
8418 state.replace_text_in_range(None, "a", window, cx);
8419 state.add_cursor_below(&AddCursorBelow, window, cx);
8420 state.replace_text_in_range(None, "b", window, cx);
8421 });
8422 });
8423 assert_cursors(&mut cx, &input, "ab|1\n2b|\n3");
8424
8425 cx.update(|window, cx| {
8427 input.update(cx, |state, cx| {
8428 state.undo(&Undo, window, cx);
8429 });
8430 });
8431 assert_cursors(&mut cx, &input, "a|1\n2|\n3");
8432 }
8433
8434 #[gpui::test]
8435 fn test_multi_cursor_indent_is_one_undo(cx: &mut TestAppContext) {
8436 let view = multi_line(cx);
8437 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8438 let input = view.input;
8439
8440 setup_cursors(&mut cx, &input, "|1\n|2\n|3");
8441 cx.update(|window, cx| {
8442 input.update(cx, |state, cx| {
8443 state.indent_inline(&IndentInline, window, cx);
8444 assert_eq!(state.undo_manager.undo_count(), 1);
8445 state.undo(&Undo, window, cx);
8446 });
8447 });
8448 assert_cursors(&mut cx, &input, "|1\n|2\n|3");
8449 }
8450
8451 #[gpui::test]
8452 fn test_multi_cursor_indent_then_outdent_roundtrips(cx: &mut TestAppContext) {
8453 let view = multi_line(cx);
8454 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8455 let input = view.input;
8456
8457 setup_cursors(&mut cx, &input, "|1\n|2\n|3");
8459 cx.update(|window, cx| {
8460 input.update(cx, |state, cx| {
8461 state.indent(false, window, cx);
8462 });
8463 });
8464 assert_cursors(&mut cx, &input, " |1\n |2\n |3");
8465
8466 cx.update(|window, cx| {
8467 input.update(cx, |state, cx| {
8468 state.outdent(false, window, cx);
8469 });
8470 });
8471 assert_cursors(&mut cx, &input, "|1\n|2\n|3");
8472 }
8473
8474 #[gpui::test]
8475 fn test_inline_outdent_only_removes_line_indentation(cx: &mut TestAppContext) {
8476 let view = multi_line(cx);
8477 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8478 let input = view.input;
8479
8480 setup_cursors(&mut cx, &input, "1|2\n1|2");
8483 cx.update(|window, cx| {
8484 input.update(cx, |state, cx| {
8485 state.indent(false, window, cx);
8486 });
8487 });
8488 assert_cursors(&mut cx, &input, "1 |2\n1 |2");
8489
8490 cx.update(|window, cx| {
8491 input.update(cx, |state, cx| {
8492 state.outdent(false, window, cx);
8493 });
8494 });
8495 assert_cursors(&mut cx, &input, "1 |2\n1 |2");
8496
8497 setup_cursors(&mut cx, &input, " 1|2\n 1|2");
8499 cx.update(|window, cx| {
8500 input.update(cx, |state, cx| {
8501 state.outdent(false, window, cx);
8502 });
8503 });
8504 assert_cursors(&mut cx, &input, "1|2\n1|2");
8505 }
8506
8507 #[gpui::test]
8508 fn test_readonly_multi_cursor_commands_leave_state_unchanged(cx: &mut TestAppContext) {
8509 let view = multi_line(cx);
8510 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8511 let input = view.input;
8512
8513 setup_cursors(&mut cx, &input, "a|b\nc|d");
8514 cx.update(|window, cx| {
8515 input.update(cx, |state, cx| {
8516 let before: Vec<_> = state.selections.iter().copied().collect();
8517 state.set_readonly(true, cx);
8518 state.backspace(&Backspace, window, cx);
8519 state.indent_inline(&IndentInline, window, cx);
8520
8521 assert_eq!(state.value(), "ab\ncd\n");
8522 assert_eq!(state.selections.iter().copied().collect::<Vec<_>>(), before);
8523 });
8524 });
8525 }
8526
8527 #[gpui::test]
8528 fn test_multi_cursor_edit_preserves_the_active_cursor(cx: &mut TestAppContext) {
8529 let view = multi_line(cx);
8530 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8531 let input = view.input;
8532
8533 setup_cursors(&mut cx, &input, "x|\n|y");
8534 cx.update(|window, cx| {
8535 input.update(cx, |state, cx| {
8536 let mut selections: Vec<_> = state.selections.iter().copied().collect();
8537 selections.swap(0, 1);
8538 state.selections.replace_all(selections);
8539 let active_id = state.active_selection().id;
8540 state.replace_text_in_range(None, "!", window, cx);
8541 assert_eq!(state.active_selection().id, active_id);
8542 });
8543 });
8544 }
8545
8546 #[gpui::test]
8547 fn test_block_indent_outdent_with_selection(cx: &mut TestAppContext) {
8548 let view = multi_line(cx);
8549 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8550 let input = view.input;
8551
8552 cx.update(|window, cx| {
8553 input.update(cx, |state, cx| {
8554 state.set_value("line1\nline2\nline3", window, cx);
8555 let id = state.selections.generate_id();
8556 state
8557 .selections
8558 .replace_all(vec![CursorSelection::new(id, 0, 17)]);
8559 cx.notify();
8560 });
8561 });
8562
8563 cx.update(|window, cx| {
8564 input.update(cx, |state, cx| {
8565 state.indent(true, window, cx);
8566 });
8567 });
8568 input.read_with(&cx, |state, _| {
8569 assert_eq!(state.text.to_string(), " line1\n line2\n line3");
8570 });
8571
8572 cx.update(|window, cx| {
8573 input.update(cx, |state, cx| {
8574 state.outdent(true, window, cx);
8575 });
8576 });
8577 input.read_with(&cx, |state, _| {
8578 assert_eq!(state.text.to_string(), "line1\nline2\nline3");
8579 });
8580 }
8581
8582 #[gpui::test]
8583 fn test_multi_cursor_word_movement(cx: &mut TestAppContext) {
8584 let view = multi_line(cx);
8585 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8586 let input = view.input;
8587
8588 setup_cursors(
8589 &mut cx,
8590 &input,
8591 "on|e two three\none t|wo three\non|e two three",
8592 );
8593
8594 cx.update(|window, cx| {
8595 input.update(cx, |state, cx| {
8596 state.move_to_next_word(&MoveToNextWord, window, cx);
8597 });
8598 });
8599 assert_cursors(
8600 &mut cx,
8601 &input,
8602 "one| two three\none two| three\none| two three",
8603 );
8604
8605 cx.update(|window, cx| {
8606 input.update(cx, |state, cx| {
8607 state.move_to_previous_word(&MoveToPreviousWord, window, cx);
8608 });
8609 });
8610 assert_cursors(
8611 &mut cx,
8612 &input,
8613 "|one two three\none |two three\n|one two three",
8614 );
8615
8616 cx.update(|window, cx| {
8618 input.update(cx, |state, cx| {
8619 state.move_to_end(&MoveToEnd, window, cx);
8620 });
8621 });
8622 input.read_with(&cx, |state, _| {
8623 let cursors: Vec<usize> = state.selections.iter().map(|s| s.cursor_offset()).collect();
8624 assert_eq!(cursors, vec![state.text.len()]);
8625 });
8626
8627 cx.update(|window, cx| {
8628 input.update(cx, |state, cx| {
8629 state.move_to_start(&MoveToStart, window, cx);
8630 });
8631 });
8632 input.read_with(&cx, |state, _| {
8633 let cursors: Vec<usize> = state.selections.iter().map(|s| s.cursor_offset()).collect();
8634 assert_eq!(cursors, vec![0]);
8635 });
8636 }
8637
8638 #[gpui::test]
8639 fn test_multi_cursor_selection_commands(cx: &mut TestAppContext) {
8640 let view = multi_line(cx);
8641 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8642 let input = view.input;
8643
8644 setup_cursors(
8645 &mut cx,
8646 &input,
8647 "on|e two three\none t|wo three\non|e two three",
8648 );
8649 cx.update(|window, cx| {
8650 input.update(cx, |state, cx| {
8651 state.select_to_start_of_line(&SelectToStartOfLine, window, cx);
8652 });
8653 });
8654 assert_cursors(
8655 &mut cx,
8656 &input,
8657 "|one two three\n|one two three\n|one two three",
8658 );
8659
8660 setup_cursors(
8662 &mut cx,
8663 &input,
8664 "on|e two three\none t|wo three\non|e two three",
8665 );
8666 cx.update(|window, cx| {
8667 input.update(cx, |state, cx| {
8668 state.select_to_start(&SelectToStart, window, cx);
8669 });
8670 });
8671 input.read_with(&cx, |state, _| {
8672 let cursors: Vec<usize> = state.selections.iter().map(|s| s.cursor_offset()).collect();
8673 assert_eq!(cursors, vec![0]);
8674 });
8675 }
8676
8677 #[gpui::test]
8678 fn test_multi_cursor_replace_selection(cx: &mut TestAppContext) {
8679 let view = multi_line(cx);
8680 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8681 let input = view.input;
8682
8683 setup_cursors(&mut cx, &input, "|a\n|b\n|c");
8684 cx.update(|window, cx| {
8685 input.update(cx, |state, cx| {
8686 state.select_right(&SelectRight, window, cx);
8687 });
8688 });
8689 input.read_with(&cx, |state, _| {
8690 let ranges: Vec<_> = state.selections.iter().map(|s| (s.start, s.end)).collect();
8691 assert_eq!(ranges, vec![(0, 1), (2, 3), (4, 5)]);
8692 });
8693
8694 cx.update(|window, cx| {
8695 input.update(cx, |state, cx| {
8696 state.replace_text_in_range(None, "x", window, cx);
8697 });
8698 });
8699 assert_cursors(&mut cx, &input, "x|\nx|\nx|");
8700 }
8701
8702 #[gpui::test]
8703 fn test_multi_cursor_escape_collapses(cx: &mut TestAppContext) {
8704 let view = multi_line(cx);
8705 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8706 let input = view.input;
8707
8708 setup_cursors(&mut cx, &input, "|a\n|b\n|c");
8709 cx.update(|window, cx| {
8710 input.update(cx, |state, cx| {
8711 state.escape(&Escape, window, cx);
8712 });
8713 });
8714 input.read_with(&cx, |state, _| {
8715 assert_eq!(state.selections.len(), 1);
8716 });
8717 }
8718
8719 #[gpui::test]
8720 fn test_build_columnar_selection(cx: &mut TestAppContext) {
8721 let view = multi_line(cx);
8722 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8723 let input = view.input;
8724
8725 setup_cursors(&mut cx, &input, "abcd\nabcd\nabcd");
8726 cx.update(|_, cx| {
8727 input.update(cx, |state, cx| {
8728 state.build_columnar_selection(
8730 ColumnarPoint::new(1, 0),
8731 ColumnarPoint::new(13, 0),
8732 cx,
8733 );
8734 let ranges: Vec<_> = state.selections.iter().map(|s| (s.start, s.end)).collect();
8735 assert_eq!(ranges, vec![(1, 3), (6, 8), (11, 13)]);
8736 });
8737 });
8738 }
8739
8740 #[gpui::test]
8741 fn test_columnar_selection_keeps_width_over_short_rows(cx: &mut TestAppContext) {
8742 let view = multi_line(cx);
8743 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8744 let input = view.input;
8745
8746 setup_cursors(&mut cx, &input, "abcdef\nab\nabcdef");
8747 cx.update(|_, cx| {
8748 input.update(cx, |state, cx| {
8749 state.build_columnar_selection(
8752 ColumnarPoint::new(1, 0),
8753 ColumnarPoint::new(9, 3),
8754 cx,
8755 );
8756 let ranges: Vec<_> = state.selections.iter().map(|s| (s.start, s.end)).collect();
8757 assert_eq!(ranges, vec![(1, 5), (8, 9)]);
8758 });
8759 });
8760 }
8761
8762 #[gpui::test]
8763 fn test_multi_cursor_paste_distributes_lines(cx: &mut TestAppContext) {
8764 let view = multi_line(cx);
8765 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8766 let input = view.input;
8767
8768 setup_cursors(&mut cx, &input, "|1\n|2\n|3");
8769 cx.update(|_, cx| {
8770 cx.write_to_clipboard(ClipboardItem::new_string("x\ny\nz".to_string()));
8771 });
8772 cx.update(|window, cx| {
8773 input.update(cx, |state, cx| {
8774 state.paste(&Paste, window, cx);
8775 });
8776 });
8777 assert_cursors(&mut cx, &input, "x|1\ny|2\nz|3");
8779 }
8780}
8781
8782impl InputBaseState<crate::input::InputMode> {
8784 pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
8786 Self::new_in_mode(window, cx)
8787 }
8788
8789 pub fn step_by(mut self, f: impl Fn(f64, StepAction, &mut App) -> f64 + 'static) -> Self {
8804 self.number_step = Some(NumberStep::by_value(f));
8805 self
8806 }
8807
8808 pub fn masked(mut self, masked: bool) -> Self {
8810 self.masked = masked;
8811 self
8812 }
8813
8814 pub fn set_masked(&mut self, masked: bool, _: &mut Window, cx: &mut Context<Self>) {
8816 self.masked = masked;
8817 cx.notify();
8818 }
8819
8820 pub fn pattern(mut self, pattern: regex::Regex) -> Self {
8822 self.pattern = Some(pattern);
8823 self
8824 }
8825
8826 pub fn set_pattern(
8828 &mut self,
8829 pattern: regex::Regex,
8830 _window: &mut Window,
8831 _cx: &mut Context<Self>,
8832 ) {
8833 self.pattern = Some(pattern);
8834 }
8835
8836 pub fn validate(mut self, f: impl Fn(&str, &mut App) -> bool + 'static) -> Self {
8838 self.validate = Some(Box::new(f));
8839 self
8840 }
8841
8842 pub fn set_validator(
8843 &mut self,
8844 validate: impl Fn(&str, &mut App) -> bool + 'static,
8845 _cx: &mut Context<Self>,
8846 ) {
8847 self.validate = Some(Box::new(validate));
8848 }
8849
8850 pub fn step(mut self, step: impl Into<NumberStep>) -> Self {
8860 self.number_step = Some(step.into());
8861 self
8862 }
8863
8864 pub fn min(mut self, min: f64) -> Self {
8870 self.number_min = Some(min);
8871 self
8872 }
8873
8874 pub fn max(mut self, max: f64) -> Self {
8880 self.number_max = Some(max);
8881 self
8882 }
8883
8884 pub fn set_step(
8889 &mut self,
8890 step: impl Into<Option<NumberStep>>,
8891 _: &mut Window,
8892 _: &mut Context<Self>,
8893 ) {
8894 self.number_step = step.into();
8895 }
8896
8897 pub fn set_min(&mut self, min: Option<f64>, _: &mut Window, _: &mut Context<Self>) {
8899 self.number_min = min;
8900 }
8901
8902 pub fn set_max(&mut self, max: Option<f64>, _: &mut Window, _: &mut Context<Self>) {
8904 self.number_max = max;
8905 }
8906
8907 pub fn set_loading(&mut self, loading: bool, _: &mut Window, cx: &mut Context<Self>) {
8909 self.loading = loading;
8910 cx.notify();
8911 }
8912}
8913
8914impl<M: crate::input::MultiLineMode> InputBaseState<M> {
8917 #[doc(hidden)]
8919 pub fn searchable(mut self, searchable: bool) -> Self {
8920 self.searchable = searchable;
8921 self
8922 }
8923
8924 pub fn set_searchable(&mut self, searchable: bool, cx: &mut Context<Self>) {
8925 self.searchable = searchable;
8926 cx.notify();
8927 }
8928
8929 #[doc(hidden)]
8931 pub fn soft_wrap(mut self, wrap: bool) -> Self {
8932 self.soft_wrap = wrap;
8933 self
8934 }
8935
8936 pub fn set_soft_wrap(&mut self, wrap: bool, _: &mut Window, cx: &mut Context<Self>) {
8938 self.soft_wrap = wrap;
8939 if wrap {
8940 let wrap_width = self
8941 .last_layout
8942 .as_ref()
8943 .and_then(|b| b.wrap_width)
8944 .unwrap_or(self.input_bounds.size.width);
8945
8946 self.display_map.on_layout_changed(Some(wrap_width), cx);
8947
8948 let mut offset = self.scroll_handle.offset();
8950 offset.x = px(0.);
8951 self.scroll_handle.set_offset(offset);
8952 } else {
8953 self.display_map.on_layout_changed(None, cx);
8954 }
8955 cx.notify();
8956 }
8957
8958 #[doc(hidden)]
8960 pub fn wrapping_indent(mut self, wrapping_indent: WrappingIndent) -> Self {
8961 self.wrapping_indent = wrapping_indent;
8962 self
8963 }
8964
8965 pub fn set_wrapping_indent(
8967 &mut self,
8968 wrapping_indent: WrappingIndent,
8969 _: &mut Window,
8970 cx: &mut Context<Self>,
8971 ) {
8972 self.wrapping_indent = wrapping_indent;
8973 self.display_map.set_wrapping_indent(wrapping_indent, cx);
8974 cx.notify();
8975 }
8976}
8977
8978impl InputBaseState<crate::input::TextareaMode> {
8980 pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
8985 Self::new_in_mode(window, cx)
8986 }
8987
8988 pub fn set_auto_grow(&mut self, min_rows: usize, max_rows: usize, cx: &mut Context<Self>) {
8989 self.mode = LayoutMode::auto_grow(min_rows, max_rows.max(min_rows));
8990 cx.notify();
8991 }
8992
8993 #[doc(hidden)]
8999 pub fn rows(mut self, rows: usize) -> Self {
9000 match &mut self.mode {
9001 LayoutMode::PlainText { rows: r, .. } | LayoutMode::CodeEditor { rows: r, .. } => {
9002 *r = rows
9003 }
9004 LayoutMode::AutoGrow {
9005 max_rows: max_r,
9006 rows: r,
9007 ..
9008 } => {
9009 *r = rows;
9010 *max_r = rows;
9011 }
9012 }
9013 self
9014 }
9015
9016 pub fn set_rows(&mut self, rows: usize, cx: &mut Context<Self>) {
9017 match &mut self.mode {
9018 LayoutMode::PlainText { rows: value, .. }
9019 | LayoutMode::CodeEditor { rows: value, .. } => *value = rows,
9020 LayoutMode::AutoGrow {
9021 rows: value,
9022 max_rows,
9023 ..
9024 } => {
9025 *value = rows;
9026 *max_rows = rows;
9027 }
9028 }
9029 cx.notify();
9030 }
9031
9032 pub fn auto_grow(mut self, min_rows: usize, max_rows: usize) -> Self {
9034 self.mode = LayoutMode::auto_grow(min_rows, max_rows);
9035 self
9036 }
9037}
9038
9039impl InputBaseState<crate::input::EditorMode> {
9041 pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
9052 let mut state = Self::new_in_mode(window, cx);
9053 state.mode = LayoutMode::code_editor(super::EditorLanguage::new(cx));
9054 state.searchable = true;
9055 state
9056 }
9057
9058 pub fn language(mut self, language: impl Into<SharedString>) -> Self {
9062 if let LayoutMode::CodeEditor {
9063 language: l,
9064 highlighter,
9065 ..
9066 } = &mut self.mode
9067 {
9068 l.set_name(language.into());
9069 *highlighter.borrow_mut() = None;
9070 }
9071 self
9072 }
9073
9074 pub fn language_name(&self) -> SharedString {
9076 match &self.mode {
9077 LayoutMode::CodeEditor { language, .. } => language.name(),
9078 _ => SharedString::default(),
9079 }
9080 }
9081
9082 #[doc(hidden)]
9086 pub fn folding(mut self, folding: bool) -> Self {
9087 if let LayoutMode::CodeEditor { folding: f, .. } = &mut self.mode {
9088 *f = folding;
9089 }
9090 self
9091 }
9092
9093 pub fn set_folding(&mut self, folding: bool, _: &mut Window, cx: &mut Context<Self>) {
9097 if let LayoutMode::CodeEditor { folding: f, .. } = &mut self.mode {
9098 *f = folding;
9099 }
9100 if !folding {
9101 self.display_map.clear_folds();
9102 }
9103 cx.notify();
9104 }
9105
9106 pub fn unfold_at(&mut self, position: impl Into<Position>, cx: &mut Context<Self>) -> bool {
9119 let offset = self.text.position_to_offset(&position.into());
9120 let line = self.text.offset_to_point(offset).row;
9121 let covering: Vec<usize> = self
9124 .display_map
9125 .folded_ranges()
9126 .iter()
9127 .filter(|fold| line > fold.start_line && line < fold.end_line)
9128 .map(|fold| fold.start_line)
9129 .collect();
9130 if covering.is_empty() {
9131 return false;
9132 }
9133
9134 for start_line in covering {
9135 self.display_map.set_folded(start_line, false);
9136 }
9137 cx.notify();
9138 true
9139 }
9140
9141 #[doc(hidden)]
9143 pub fn line_number(mut self, line_number: bool) -> Self {
9144 if let LayoutMode::CodeEditor { line_number: l, .. } = &mut self.mode {
9145 *l = line_number;
9146 }
9147 self
9148 }
9149
9150 pub fn set_line_number(&mut self, line_number: bool, _: &mut Window, cx: &mut Context<Self>) {
9152 if let LayoutMode::CodeEditor { line_number: l, .. } = &mut self.mode {
9153 *l = line_number;
9154 }
9155 cx.notify();
9156 }
9157
9158 #[doc(hidden)]
9164 pub fn auto_close(mut self, auto_close: bool) -> Self {
9165 self.mode.set_auto_close(auto_close);
9166 self
9167 }
9168
9169 pub fn set_auto_close(&mut self, auto_close: bool, _: &mut Window, cx: &mut Context<Self>) {
9171 self.mode.set_auto_close(auto_close);
9172 cx.notify();
9173 }
9174
9175 #[doc(hidden)]
9181 pub fn smart_indent(mut self, smart_indent: bool) -> Self {
9182 self.mode.set_smart_indent(smart_indent);
9183 self
9184 }
9185
9186 pub fn set_smart_indent(&mut self, smart_indent: bool, _: &mut Window, cx: &mut Context<Self>) {
9188 self.mode.set_smart_indent(smart_indent);
9189 cx.notify();
9190 }
9191}