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