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, WeakFocusHandle, Window, actions, div, point, prelude::FluentBuilder as _, px,
12};
13use gpui::{Autocapitalize, TextInputAction, TextInputConfiguration};
14use ropey::{Rope, RopeSlice};
15use serde::Deserialize;
16use std::borrow::Cow;
17use std::cell::Cell;
18use std::ops::Range;
19use std::rc::Rc;
20use sum_tree::Bias;
21use unicode_segmentation::*;
22
23use super::{
24 DiagnosticSet, DisplayMap, InputContextMenuCapabilities, InputEditorStyle,
25 InputHighlighterFactory, MASK_CHAR, MaskPattern, NativeMenu, NumberStep, WrappingIndent,
26 blink_cursor::BlinkCursor,
27 change::Change,
28 cursor::{CursorSelection, Selections},
29 element::{
30 EditorScrollbar, EditorScrollbarSnapshot, LongestLineKey, TextElement,
31 clamp_horizontal_scroll_offset,
32 },
33 kind::InputModeKind,
34 mask_pattern::normalize_number_input,
35 mode::LayoutMode,
36 undo_manager::{EditIntent, UndoManager},
37};
38use crate::actions::{SelectDown, SelectLeft, SelectRight, SelectUp};
39use crate::input::blink_cursor::CURSOR_WIDTH;
40use crate::input::movement::MoveDirection;
41use crate::input::{
42 InputExtras as _, Position, RopeExt as _, element::RIGHT_MARGIN, layout::LastLayout,
43};
44use crate::overscroll::Overscroll;
45use crate::{AutoScroll, StepAction};
46
47pub(crate) enum ScrollPadding {
49 Minimal,
50 SurroundingLines,
51}
52
53#[derive(Action, Clone, PartialEq, Eq, Deserialize)]
54#[action(namespace = input, no_json)]
55pub struct Enter {
56 pub secondary: bool,
58 pub shift: bool,
60}
61
62impl Enter {
63 pub fn is_primary(action: &dyn Action) -> bool {
66 action.partial_eq(&Enter {
67 secondary: false,
68 shift: false,
69 }) || action.partial_eq(&Enter {
70 secondary: false,
71 shift: true,
72 })
73 }
74}
75
76actions!(
77 input,
78 [
79 ActivateToken,
80 Backspace,
81 Delete,
82 DeleteToBeginningOfLine,
83 DeleteToEndOfLine,
84 DeleteToPreviousWordStart,
85 DeleteToNextWordEnd,
86 Indent,
87 Outdent,
88 IndentInline,
89 OutdentInline,
90 MoveUp,
91 MoveDown,
92 MoveLeft,
93 MoveRight,
94 MoveHome,
95 MoveEnd,
96 MovePageUp,
97 MovePageDown,
98 AddCursorAbove,
99 AddCursorBelow,
100 SelectAll,
101 SelectToStartOfLine,
102 SelectToEndOfLine,
103 SelectToStart,
104 SelectToEnd,
105 SelectToPreviousWordStart,
106 SelectToNextWordEnd,
107 ShowCharacterPalette,
108 Copy,
109 Cut,
110 Paste,
111 Undo,
112 Redo,
113 MoveToStartOfLine,
114 MoveToEndOfLine,
115 MoveToStart,
116 MoveToEnd,
117 MoveToPreviousWord,
118 MoveToNextWord,
119 Escape,
120 ToggleCodeActions,
121 ShowCompletions,
122 Search,
123 Replace,
124 GoToDefinition,
125 ]
126);
127
128#[derive(Clone)]
129pub enum InputEvent {
130 Change,
131 PressEnter { secondary: bool, shift: bool },
132 Focus,
133 Blur,
134}
135
136pub(super) const CONTEXT: &str = "Input";
137
138pub(crate) fn init(cx: &mut App) {
139 cx.bind_keys([
140 KeyBinding::new("backspace", Backspace, Some(CONTEXT)),
141 KeyBinding::new("shift-backspace", Backspace, Some(CONTEXT)),
142 #[cfg(target_os = "macos")]
143 KeyBinding::new("ctrl-backspace", Backspace, Some(CONTEXT)),
144 KeyBinding::new("delete", Delete, Some(CONTEXT)),
145 KeyBinding::new("shift-delete", Delete, Some(CONTEXT)),
146 #[cfg(target_os = "macos")]
147 KeyBinding::new("cmd-backspace", DeleteToBeginningOfLine, Some(CONTEXT)),
148 #[cfg(target_os = "macos")]
149 KeyBinding::new("cmd-delete", DeleteToEndOfLine, Some(CONTEXT)),
150 #[cfg(target_os = "macos")]
151 KeyBinding::new("alt-backspace", DeleteToPreviousWordStart, Some(CONTEXT)),
152 #[cfg(not(target_os = "macos"))]
153 KeyBinding::new("ctrl-backspace", DeleteToPreviousWordStart, Some(CONTEXT)),
154 #[cfg(target_os = "macos")]
155 KeyBinding::new("alt-delete", DeleteToNextWordEnd, Some(CONTEXT)),
156 #[cfg(not(target_os = "macos"))]
157 KeyBinding::new("ctrl-delete", DeleteToNextWordEnd, Some(CONTEXT)),
158 KeyBinding::new(
159 "enter",
160 Enter {
161 secondary: false,
162 shift: false,
163 },
164 Some(CONTEXT),
165 ),
166 KeyBinding::new(
167 "shift-enter",
168 Enter {
169 secondary: false,
170 shift: true,
171 },
172 Some(CONTEXT),
173 ),
174 KeyBinding::new(
175 "secondary-enter",
176 Enter {
177 secondary: true,
178 shift: false,
179 },
180 Some(CONTEXT),
181 ),
182 KeyBinding::new("escape", Escape, Some(CONTEXT)),
183 KeyBinding::new("up", MoveUp, Some(CONTEXT)),
184 KeyBinding::new("down", MoveDown, Some(CONTEXT)),
185 KeyBinding::new("left", MoveLeft, Some(CONTEXT)),
186 KeyBinding::new("right", MoveRight, Some(CONTEXT)),
187 KeyBinding::new("pageup", MovePageUp, Some(CONTEXT)),
188 KeyBinding::new("pagedown", MovePageDown, Some(CONTEXT)),
189 KeyBinding::new("tab", IndentInline, Some(CONTEXT)),
190 KeyBinding::new("shift-tab", OutdentInline, Some(CONTEXT)),
191 #[cfg(target_os = "macos")]
192 KeyBinding::new("cmd-]", Indent, Some(CONTEXT)),
193 #[cfg(not(target_os = "macos"))]
194 KeyBinding::new("ctrl-]", Indent, Some(CONTEXT)),
195 #[cfg(target_os = "macos")]
196 KeyBinding::new("cmd-[", Outdent, Some(CONTEXT)),
197 #[cfg(not(target_os = "macos"))]
198 KeyBinding::new("ctrl-[", Outdent, Some(CONTEXT)),
199 KeyBinding::new("shift-left", SelectLeft, Some(CONTEXT)),
200 KeyBinding::new("shift-right", SelectRight, Some(CONTEXT)),
201 KeyBinding::new("shift-up", SelectUp, Some(CONTEXT)),
202 KeyBinding::new("shift-down", SelectDown, Some(CONTEXT)),
203 #[cfg(not(any(target_os = "macos", target_os = "linux")))]
204 KeyBinding::new("shift-alt-left", SelectLeft, Some(CONTEXT)),
205 #[cfg(not(any(target_os = "macos", target_os = "linux")))]
206 KeyBinding::new("shift-alt-right", SelectRight, Some(CONTEXT)),
207 #[cfg(target_os = "macos")]
209 KeyBinding::new("cmd-alt-up", AddCursorAbove, Some(CONTEXT)),
210 #[cfg(target_os = "macos")]
211 KeyBinding::new("cmd-alt-down", AddCursorBelow, Some(CONTEXT)),
212 #[cfg(target_os = "windows")]
213 KeyBinding::new("ctrl-alt-up", AddCursorAbove, Some(CONTEXT)),
214 #[cfg(not(any(target_os = "macos", target_os = "windows")))]
215 KeyBinding::new("shift-alt-up", AddCursorAbove, Some(CONTEXT)),
216 #[cfg(target_os = "windows")]
217 KeyBinding::new("ctrl-alt-down", AddCursorBelow, Some(CONTEXT)),
218 #[cfg(not(any(target_os = "macos", target_os = "windows")))]
219 KeyBinding::new("shift-alt-down", AddCursorBelow, Some(CONTEXT)),
220 KeyBinding::new("home", MoveHome, Some(CONTEXT)),
221 KeyBinding::new("end", MoveEnd, Some(CONTEXT)),
222 #[cfg(not(target_os = "macos"))]
223 KeyBinding::new("ctrl-home", MoveToStart, Some(CONTEXT)),
224 #[cfg(not(target_os = "macos"))]
225 KeyBinding::new("ctrl-end", MoveToEnd, Some(CONTEXT)),
226 #[cfg(not(target_os = "macos"))]
227 KeyBinding::new("ctrl-shift-home", SelectToStart, Some(CONTEXT)),
228 #[cfg(not(target_os = "macos"))]
229 KeyBinding::new("ctrl-shift-end", SelectToEnd, Some(CONTEXT)),
230 KeyBinding::new("shift-home", SelectToStartOfLine, Some(CONTEXT)),
231 KeyBinding::new("shift-end", SelectToEndOfLine, Some(CONTEXT)),
232 #[cfg(target_os = "macos")]
233 KeyBinding::new("ctrl-shift-a", SelectToStartOfLine, Some(CONTEXT)),
234 #[cfg(target_os = "macos")]
235 KeyBinding::new("ctrl-shift-e", SelectToEndOfLine, Some(CONTEXT)),
236 #[cfg(target_os = "macos")]
237 KeyBinding::new("shift-cmd-left", SelectToStartOfLine, Some(CONTEXT)),
238 #[cfg(target_os = "macos")]
239 KeyBinding::new("shift-cmd-right", SelectToEndOfLine, Some(CONTEXT)),
240 #[cfg(any(target_os = "macos", target_os = "linux"))]
241 KeyBinding::new("alt-shift-left", SelectToPreviousWordStart, Some(CONTEXT)),
242 #[cfg(not(target_os = "macos"))]
243 KeyBinding::new("ctrl-shift-left", SelectToPreviousWordStart, Some(CONTEXT)),
244 #[cfg(any(target_os = "macos", target_os = "linux"))]
245 KeyBinding::new("alt-shift-right", SelectToNextWordEnd, Some(CONTEXT)),
246 #[cfg(not(target_os = "macos"))]
247 KeyBinding::new("ctrl-shift-right", SelectToNextWordEnd, Some(CONTEXT)),
248 #[cfg(target_os = "macos")]
249 KeyBinding::new("ctrl-cmd-space", ShowCharacterPalette, Some(CONTEXT)),
250 #[cfg(target_os = "macos")]
251 KeyBinding::new("cmd-a", SelectAll, Some(CONTEXT)),
252 #[cfg(not(target_os = "macos"))]
253 KeyBinding::new("ctrl-a", SelectAll, Some(CONTEXT)),
254 #[cfg(target_os = "macos")]
255 KeyBinding::new("cmd-c", Copy, Some(CONTEXT)),
256 #[cfg(not(target_os = "macos"))]
257 KeyBinding::new("ctrl-c", Copy, Some(CONTEXT)),
258 #[cfg(target_os = "macos")]
259 KeyBinding::new("cmd-x", Cut, Some(CONTEXT)),
260 #[cfg(not(target_os = "macos"))]
261 KeyBinding::new("ctrl-x", Cut, Some(CONTEXT)),
262 #[cfg(target_os = "macos")]
263 KeyBinding::new("cmd-v", Paste, Some(CONTEXT)),
264 #[cfg(not(target_os = "macos"))]
265 KeyBinding::new("ctrl-v", Paste, Some(CONTEXT)),
266 #[cfg(target_os = "macos")]
267 KeyBinding::new("ctrl-a", MoveHome, Some(CONTEXT)),
268 #[cfg(target_os = "macos")]
269 KeyBinding::new("cmd-left", MoveHome, Some(CONTEXT)),
270 #[cfg(target_os = "macos")]
271 KeyBinding::new("ctrl-e", MoveEnd, Some(CONTEXT)),
272 #[cfg(target_os = "macos")]
273 KeyBinding::new("cmd-right", MoveEnd, Some(CONTEXT)),
274 #[cfg(target_os = "macos")]
275 KeyBinding::new("cmd-z", Undo, Some(CONTEXT)),
276 #[cfg(target_os = "macos")]
277 KeyBinding::new("cmd-shift-z", Redo, Some(CONTEXT)),
278 #[cfg(target_os = "macos")]
279 KeyBinding::new("cmd-up", MoveToStart, Some(CONTEXT)),
280 #[cfg(target_os = "macos")]
281 KeyBinding::new("cmd-down", MoveToEnd, Some(CONTEXT)),
282 #[cfg(target_os = "macos")]
283 KeyBinding::new("alt-left", MoveToPreviousWord, Some(CONTEXT)),
284 #[cfg(target_os = "macos")]
285 KeyBinding::new("alt-right", MoveToNextWord, Some(CONTEXT)),
286 #[cfg(not(target_os = "macos"))]
287 KeyBinding::new("ctrl-left", MoveToPreviousWord, Some(CONTEXT)),
288 #[cfg(not(target_os = "macos"))]
289 KeyBinding::new("ctrl-right", MoveToNextWord, Some(CONTEXT)),
290 #[cfg(target_os = "macos")]
291 KeyBinding::new("cmd-shift-up", SelectToStart, Some(CONTEXT)),
292 #[cfg(target_os = "macos")]
293 KeyBinding::new("cmd-shift-down", SelectToEnd, Some(CONTEXT)),
294 #[cfg(not(target_os = "macos"))]
295 KeyBinding::new("ctrl-z", Undo, Some(CONTEXT)),
296 #[cfg(not(target_os = "macos"))]
297 KeyBinding::new("ctrl-y", Redo, Some(CONTEXT)),
298 #[cfg(target_os = "macos")]
299 KeyBinding::new("cmd-.", ToggleCodeActions, Some(CONTEXT)),
300 #[cfg(not(target_os = "macos"))]
301 KeyBinding::new("ctrl-.", ToggleCodeActions, Some(CONTEXT)),
302 #[cfg(target_os = "macos")]
303 KeyBinding::new("cmd-f", Search, Some(CONTEXT)),
304 #[cfg(not(target_os = "macos"))]
305 KeyBinding::new("ctrl-f", Search, Some(CONTEXT)),
306 #[cfg(target_os = "macos")]
307 KeyBinding::new("cmd-shift-f", Replace, Some(CONTEXT)),
308 #[cfg(not(target_os = "macos"))]
309 KeyBinding::new("ctrl-h", Replace, Some(CONTEXT)),
310 ]);
311}
312
313#[derive(Debug, Clone, Copy, PartialEq, Eq)]
320pub(super) struct ColumnarPoint {
321 offset: usize,
322 columns_past_line_end: usize,
323}
324
325impl ColumnarPoint {
326 pub(super) fn new(offset: usize, columns_past_line_end: usize) -> Self {
327 Self {
328 offset,
329 columns_past_line_end,
330 }
331 }
332}
333
334#[derive(Debug, PartialEq)]
338struct PasteTarget {
339 document_revision: u64,
340 selections: Vec<CursorSelection>,
341}
342
343#[derive(Clone, Copy, Debug)]
350#[doc(hidden)]
351pub struct PrepaintCaretGeometry {
352 cursor_bounds: Bounds<Pixels>,
353 line_height: Pixels,
354 input_bounds: Bounds<Pixels>,
355 scroll_offset: Point<Pixels>,
356 pub(super) resize_reveal_owned: bool,
358}
359
360impl PrepaintCaretGeometry {
361 pub(super) fn new(
362 cursor_bounds: Bounds<Pixels>,
363 line_height: Pixels,
364 input_bounds: Bounds<Pixels>,
365 scroll_offset: Point<Pixels>,
366 ) -> Self {
367 Self {
368 cursor_bounds,
369 line_height,
370 input_bounds,
371 scroll_offset,
372 resize_reveal_owned: false,
373 }
374 }
375
376 pub fn cursor_bounds(&self) -> Bounds<Pixels> {
378 self.cursor_bounds
379 }
380
381 pub fn line_height(&self) -> Pixels {
383 self.line_height
384 }
385
386 pub fn input_bounds(&self) -> Bounds<Pixels> {
388 self.input_bounds
389 }
390
391 pub fn scroll_offset(&self) -> Point<Pixels> {
394 self.scroll_offset
395 }
396}
397
398pub struct InputBaseState<M: InputModeKind> {
409 pub(crate) extras: M::Extras,
411 pub(super) focus_handle: FocusHandle,
412 selection_focus: Option<WeakFocusHandle>,
413 pub(super) mode: LayoutMode,
414 pub(super) text: Rope,
415 pub(super) display_map: DisplayMap,
416 pub(super) undo_manager: UndoManager,
417 pub(super) inline_tokens: Option<Box<super::inline_tokens::InlineTokenStore>>,
418 pub(super) pending_token: Option<super::InlineToken>,
419 pub(super) replaying_history: bool,
420 pub(super) validated_token_edit: bool,
421 pub(super) document_revision: u64,
422 pub(super) token_presentation: super::InlineTokenPresentation,
423 pub(super) token_layout_cache: Option<Box<super::token_presentation::TokenLayoutCache>>,
424 pub(super) token_bounds: std::collections::HashMap<usize, Bounds<Pixels>>,
427 pub(super) hovered_token: Option<super::token_presentation::HoverSnapshot>,
432 pub(super) pending_token_hover_exits: Vec<super::token_presentation::HoverSnapshot>,
433 pub(super) pressed_token: Option<(usize, u64, Point<Pixels>)>,
436 pub(super) search_session: super::SearchSession,
437 pub(super) search_activation_revision: u64,
440 pub(super) searchable: bool,
441 pub(super) replaceable: bool,
442 pub(super) soft_wrap: bool,
443 pub(super) wrapping_indent: WrappingIndent,
444 pub(super) scroll_beyond_last_line: Option<usize>,
445 pub(super) cursor_surrounding_lines: Option<usize>,
446 pub(super) blink_cursor: Entity<BlinkCursor>,
447 pub(super) loading: bool,
448 pub(super) selections: Selections,
452 pub(super) selected_word_range: Option<CursorSelection>,
454 pub(super) ime_marked_range: Option<CursorSelection>,
456 pub(super) last_layout: Option<LastLayout>,
457 pub(crate) prepaint_caret_geometry: Option<PrepaintCaretGeometry>,
463 pub(super) last_cursor: Option<usize>,
464 pub(super) input_bounds: Bounds<Pixels>,
466 pub(super) last_bounds: Option<Bounds<Pixels>>,
468 pub(super) last_selected_range: Option<CursorSelection>,
469 pub(super) selecting: bool,
470 pub(super) column_select_start: Option<ColumnarPoint>,
472 pub(super) touch_selection: super::touch::TouchSelection,
474 pub(crate) disabled: bool,
475 pub(crate) readonly: bool,
476 pub(crate) text_align: TextAlign,
477 pub(super) masked: bool,
478 pub(super) clean_on_escape: bool,
479 pub(super) submit_on_enter: bool,
480 pub(super) input_action: Option<TextInputAction>,
482 pub(super) autocorrect: Option<bool>,
483 pub(super) autocapitalize: Option<Autocapitalize>,
484 pub(super) show_whitespaces: bool,
485 pub(crate) cursor_line_end_affinity: bool,
487 pub(super) pattern: Option<regex::Regex>,
488 pub(super) validate: Option<Box<dyn Fn(&str, &mut App) -> bool + 'static>>,
489 pub(crate) number_step: Option<NumberStep>,
492 pub(crate) number_min: Option<f64>,
494 pub(crate) number_max: Option<f64>,
496 pub(crate) scroll_handle: ScrollHandle,
497 scroll_bounce: bool,
499 pub(super) overscroll: Overscroll,
501 pub(crate) deferred_scroll_offset: Option<Point<Pixels>>,
503 pub(crate) scroll_size: gpui::Size<Pixels>,
505 pub(super) editor_scrollbar_snapshot: Cell<Option<EditorScrollbarSnapshot>>,
506 pub(super) longest_line_width: Cell<Option<(LongestLineKey, Pixels)>>,
508 pub(super) editor_paddings: Edges<Pixels>,
509 pub(super) editor_style: InputEditorStyle,
514 projected_editor_style: InputEditorStyle,
519
520 pub(crate) mask_pattern: MaskPattern,
522 pub(super) mask_pattern_set: bool,
526 pub(super) placeholder: SharedString,
527
528 pub(super) diagnostic_popover: Option<Rc<crate::input::DiagnosticEntry>>,
530
531 context_menu_handler: Option<
532 Rc<dyn Fn(NativeMenu, InputContextMenuCapabilities, Point<Pixels>, &mut Window, &mut App)>,
533 >,
534 pending_context_menu: Option<(Point<Pixels>, usize)>,
535
536 pub(super) enable_context_menu: bool,
539
540 pub(super) completion_inserting: bool,
542 pub(super) overlay_action_handler: Option<
543 Rc<
544 dyn Fn(
545 super::InputOverlayKind,
546 Box<dyn Action>,
547 &mut Window,
548 &mut Context<InputBaseState<M>>,
549 ) -> bool,
550 >,
551 >,
552
553 _pending_update: bool,
557 pub(super) silent_replace_text: bool,
559 pub(super) emit_events: bool,
561
562 _subscriptions: Vec<Subscription>,
563
564 pub(super) auto_scroll: AutoScroll,
565}
566
567#[derive(Clone)]
572pub struct InputPresentation {
573 focus_handle: FocusHandle,
574 disabled: bool,
575 readonly: bool,
576 loading: bool,
577 masked: bool,
578 multi_line: bool,
579 code_editor: bool,
580 text_align: TextAlign,
581 placeholder: SharedString,
582 mask_placeholder: Option<String>,
583}
584
585impl InputPresentation {
586 pub fn focus_handle(&self) -> &FocusHandle {
587 &self.focus_handle
588 }
589
590 pub fn is_disabled(&self) -> bool {
591 self.disabled
592 }
593
594 pub fn is_readonly(&self) -> bool {
595 self.readonly
596 }
597
598 pub fn is_editable(&self) -> bool {
602 !self.disabled && !self.readonly
603 }
604
605 pub fn is_loading(&self) -> bool {
606 self.loading
607 }
608
609 pub fn is_masked(&self) -> bool {
610 self.masked
611 }
612
613 pub fn is_multi_line(&self) -> bool {
614 self.multi_line
615 }
616
617 pub fn is_code_editor(&self) -> bool {
618 self.code_editor
619 }
620
621 pub fn text_align(&self) -> TextAlign {
622 self.text_align
623 }
624
625 pub fn placeholder(&self) -> &SharedString {
626 &self.placeholder
627 }
628
629 pub fn mask_placeholder(&self) -> Option<&str> {
631 self.mask_placeholder.as_deref()
632 }
633}
634
635impl<M: InputModeKind> EventEmitter<InputEvent> for InputBaseState<M> {}
636
637impl<M: InputModeKind> InputBaseState<M> {
638 #[doc(hidden)]
639 pub fn cursor_layout(&self) -> Option<(Bounds<Pixels>, Pixels)> {
640 let layout = self.last_layout.as_ref()?;
641 Some((layout.cursor_bounds?, layout.line_height))
642 }
643
644 #[doc(hidden)]
649 pub fn prepaint_caret_geometry(&self) -> Option<PrepaintCaretGeometry> {
650 self.prepaint_caret_geometry
651 }
652
653 pub fn input_bounds(&self) -> Bounds<Pixels> {
654 self.input_bounds
655 }
656
657 pub fn text_bounds(&self) -> Option<Bounds<Pixels>> {
658 self.last_bounds
659 }
660
661 pub fn display_map(&self) -> &DisplayMap {
672 &self.display_map
673 }
674
675 pub fn prewrap(
686 &mut self,
687 font: gpui::Font,
688 font_size: Pixels,
689 width: Pixels,
690 cx: &mut Context<Self>,
691 ) {
692 if self.last_layout.is_some() {
693 return;
694 }
695 let wrap_width = (width - RIGHT_MARGIN).max(px(1.));
696 self.display_map.set_font(font, font_size, cx);
697 self.display_map
698 .on_layout_changed(self.soft_wrap.then_some(wrap_width), cx);
699 self.display_map
700 .set_wrapping_indent(self.wrapping_indent, cx);
701 self.display_map.ensure_text_prepared(&self.text, cx);
702 }
703
704 pub fn diagnostic_popover(&self) -> Option<Rc<crate::input::DiagnosticEntry>> {
705 self.diagnostic_popover.clone()
706 }
707
708 pub fn presentation(&self) -> InputPresentation {
709 InputPresentation {
710 focus_handle: self.focus_handle.clone(),
711 disabled: self.disabled,
712 readonly: self.readonly,
713 loading: self.loading,
714 masked: self.masked,
715 multi_line: self.is_multi_line(),
716 code_editor: self.is_code_editor(),
717 text_align: self.text_align,
718 placeholder: self.placeholder.clone(),
719 mask_placeholder: self.mask_pattern.placeholder(),
720 }
721 }
722
723 #[inline]
728 pub(crate) fn shows_scrollbar(&self) -> bool {
736 self.is_multi_line()
737 }
738
739 pub fn is_multi_line(&self) -> bool {
740 M::MULTI_LINE
741 }
742
743 #[inline]
745 pub fn is_single_line(&self) -> bool {
746 !M::MULTI_LINE
747 }
748
749 #[inline]
751 pub fn is_code_editor(&self) -> bool {
752 M::CODE_EDITOR
753 }
754
755 pub fn is_copyable(&self) -> bool {
759 self.selections.iter().any(|sel| !sel.is_empty()) && !self.masked
760 }
761
762 pub fn context_menu_capabilities(&self) -> InputContextMenuCapabilities {
763 let (go_to_definition, code_actions) = self.extras.context_menu_capabilities();
764 InputContextMenuCapabilities::new()
765 .disabled(self.disabled)
766 .readonly(self.readonly)
767 .code_editor(self.is_code_editor())
768 .selection(!self.active_selection().is_empty())
769 .masked(self.masked)
770 .go_to_definition(go_to_definition)
771 .code_actions(code_actions)
772 }
773
774 pub fn set_text_align(&mut self, text_align: TextAlign, cx: &mut Context<Self>) {
775 if !self.is_single_line() || self.text_align == text_align {
776 return;
777 }
778
779 self.text_align = text_align;
780 cx.notify();
781 }
782
783 pub fn toggle_masked(&mut self, _: &mut Window, cx: &mut Context<Self>) {
789 self.masked = !self.masked;
790 cx.notify();
791 }
792
793 pub fn on_context_menu(
794 &mut self,
795 handler: Rc<
796 dyn Fn(NativeMenu, InputContextMenuCapabilities, Point<Pixels>, &mut Window, &mut App),
797 >,
798 ) {
799 self.context_menu_handler = Some(handler);
800 }
801
802 pub fn set_selection_focus(&mut self, focus: Option<FocusHandle>, cx: &mut Context<Self>) {
807 self.selection_focus = focus.as_ref().map(FocusHandle::downgrade);
808 cx.notify();
809 }
810
811 pub fn has_selection_focus(&self, window: &Window, cx: &App) -> bool {
813 self.focus_handle.is_focused(window)
814 || self
815 .selection_focus
816 .as_ref()
817 .and_then(WeakFocusHandle::upgrade)
818 .is_some_and(|focus| focus.is_focused(window) || focus.contains_focused(window, cx))
819 }
820
821 fn new_in_mode(window: &mut Window, cx: &mut Context<Self>) -> Self {
823 let focus_handle = cx.focus_handle().tab_stop(true);
824 let blink_cursor = cx.new(|_| BlinkCursor::new());
825 let undo_manager = UndoManager::new();
826
827 let _subscriptions = vec![
828 cx.intercept_keystrokes({
831 let focus_handle = focus_handle.clone();
832 let blink_cursor = blink_cursor.downgrade();
833 move |_, window, cx| {
834 if focus_handle.is_focused(window) {
835 _ = blink_cursor.update(cx, |cursor, cx| cursor.pause(cx));
836 }
837 }
838 }),
839 cx.observe(&blink_cursor, |_, _, cx| cx.notify()),
841 cx.observe_window_activation(window, |input, window, cx| {
843 if window.is_window_active() {
844 let focus_handle = input.focus_handle.clone();
845 if focus_handle.is_focused(window) {
846 input.blink_cursor.update(cx, |blink_cursor, cx| {
847 blink_cursor.start(cx);
848 });
849 }
850 }
851 }),
852 cx.on_focus(&focus_handle, window, Self::on_focus),
853 cx.on_blur(&focus_handle, window, Self::on_blur),
854 ];
855
856 let text_style = window.text_style();
857
858 Self {
859 extras: M::Extras::default(),
860 focus_handle: focus_handle.clone(),
861 selection_focus: None,
862 text: "".into(),
863 display_map: DisplayMap::new(text_style.font(), window.rem_size(), None),
864 search_session: super::SearchSession::default(),
865 search_activation_revision: 0,
866 searchable: false,
867 replaceable: true,
868 soft_wrap: true,
869 wrapping_indent: WrappingIndent::default(),
870 scroll_beyond_last_line: None,
871 cursor_surrounding_lines: None,
872 blink_cursor,
873 undo_manager,
874 inline_tokens: None,
875 pending_token: None,
876 replaying_history: false,
877 validated_token_edit: false,
878 document_revision: 0,
879 token_presentation: Default::default(),
880 token_bounds: Default::default(),
881 hovered_token: None,
882 pending_token_hover_exits: Vec::new(),
883 token_layout_cache: None,
884 pressed_token: None,
885 selections: Selections::default(),
886 selected_word_range: None,
887 ime_marked_range: None,
888 input_bounds: Bounds::default(),
889 selecting: false,
890 touch_selection: Default::default(),
891 disabled: false,
892 readonly: false,
893 text_align: TextAlign::Left,
894 masked: false,
895 clean_on_escape: false,
896 submit_on_enter: false,
897 input_action: None,
898 autocorrect: None,
899 autocapitalize: None,
900 show_whitespaces: false,
901 loading: false,
902 pattern: None,
903 validate: None,
904 number_step: Some(NumberStep::Fixed(1.)),
905 number_min: None,
906 number_max: None,
907 mode: LayoutMode::default(),
908 last_layout: None,
909 prepaint_caret_geometry: None,
910 last_bounds: None,
911 last_selected_range: None,
912 column_select_start: None,
913 last_cursor: None,
914 scroll_handle: ScrollHandle::new(),
915 scroll_bounce: crate::ScrollBounce::enabled_by_default(),
916 overscroll: Overscroll::default(),
917 scroll_size: gpui::size(px(0.), px(0.)),
918 editor_scrollbar_snapshot: Cell::new(None),
919 longest_line_width: Cell::new(None),
920 editor_paddings: Edges::default(),
921 deferred_scroll_offset: None,
922 placeholder: SharedString::default(),
923 mask_pattern: MaskPattern::default(),
924 mask_pattern_set: false,
925 editor_style: InputEditorStyle::default(),
926 projected_editor_style: InputEditorStyle::default(),
927 diagnostic_popover: None,
928 context_menu_handler: None,
929 pending_context_menu: None,
930 enable_context_menu: true,
931 completion_inserting: false,
932 overlay_action_handler: None,
933 silent_replace_text: false,
934 emit_events: true,
935 _subscriptions,
936 _pending_update: false,
937 cursor_line_end_affinity: false,
938 auto_scroll: AutoScroll::default(),
939 }
940 }
941
942 pub fn context_menu(mut self, enable: bool) -> Self {
949 self.enable_context_menu = enable;
950 self
951 }
952
953 pub fn set_context_menu_enabled(&mut self, enabled: bool) {
954 self.enable_context_menu = enabled;
955 }
956
957 #[doc(hidden)]
959 pub fn replaceable(mut self, allow: bool) -> Self {
960 self.replaceable = allow;
961 self
962 }
963
964 pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
966 self.placeholder = placeholder.into();
967 self
968 }
969
970 pub fn set_highlighter(
972 &mut self,
973 new_language: impl Into<SharedString>,
974 cx: &mut Context<Self>,
975 ) {
976 match &mut self.mode {
977 LayoutMode::CodeEditor {
978 language,
979 highlighter,
980 ..
981 } => {
982 language.set_name(new_language.into());
983 *highlighter.borrow_mut() = None;
984 }
985 _ => {}
986 }
987 self.refresh(cx);
988 }
989
990 fn reset_highlighter(&mut self, cx: &mut Context<Self>) {
991 match &mut self.mode {
992 LayoutMode::CodeEditor { highlighter, .. } => {
993 *highlighter.borrow_mut() = None;
994 }
995 _ => {}
996 }
997 cx.notify();
998 }
999
1000 pub fn set_highlighter_factory(
1002 &mut self,
1003 factory: InputHighlighterFactory,
1004 cx: &mut Context<Self>,
1005 ) {
1006 self.mode.set_highlighter_factory(factory);
1007 self._pending_update = true;
1008 self.token_layout_cache = None;
1009 cx.notify();
1010 }
1011
1012 pub fn ensure_highlighter_factory(&mut self, factory: InputHighlighterFactory) {
1014 self.mode.ensure_highlighter_factory(factory);
1015 }
1016
1017 pub fn set_editor_style(&mut self, style: InputEditorStyle) {
1018 self.editor_style = style.clone();
1019 self.projected_editor_style = style;
1020 }
1021
1022 #[doc(hidden)]
1024 pub fn set_editor_paddings(&mut self, paddings: Edges<Pixels>) {
1025 self.editor_paddings = paddings;
1026 }
1027
1028 pub fn apply_highlighter_fold_candidates(
1029 &mut self,
1030 candidates: Vec<crate::input::FoldRange>,
1031 cx: &mut Context<Self>,
1032 ) {
1033 if self.mode.is_folding() {
1034 self.display_map.set_fold_candidates(candidates);
1035 }
1036 cx.notify();
1037 }
1038
1039 #[inline]
1040 pub fn diagnostics(&self) -> Option<&DiagnosticSet> {
1041 self.mode.diagnostics()
1042 }
1043
1044 #[inline]
1045 pub fn diagnostics_mut(&mut self) -> Option<&mut DiagnosticSet> {
1046 self.mode.diagnostics_mut()
1047 }
1048
1049 pub fn set_placeholder(
1051 &mut self,
1052 placeholder: impl Into<SharedString>,
1053 _: &mut Window,
1054 cx: &mut Context<Self>,
1055 ) {
1056 self.placeholder = placeholder.into();
1057 cx.notify();
1058 }
1059
1060 pub(super) fn line_and_position_for_offset(
1068 &self,
1069 offset: usize,
1070 ) -> (usize, usize, Option<Point<Pixels>>) {
1071 let Some(last_layout) = &self.last_layout else {
1072 return (0, 0, None);
1073 };
1074 let line_height = last_layout.line_height;
1075
1076 let mut y_offset = last_layout.visible_top;
1077 for (vi, line) in last_layout.lines.iter().enumerate() {
1078 let prev_lines_offset = last_layout.visible_line_byte_offsets[vi];
1079 let local_offset = offset.saturating_sub(prev_lines_offset);
1080 if let Some(pos) = line.position_for_index(local_offset, last_layout, false) {
1081 let sub_line_index = (pos.y / line_height) as usize;
1082 let adjusted_pos = point(pos.x + last_layout.line_number_width, pos.y + y_offset);
1083 return (vi, sub_line_index, Some(adjusted_pos));
1084 }
1085
1086 y_offset += line.size(line_height).height;
1087 }
1088 (0, 0, None)
1089 }
1090
1091 pub fn set_value(
1100 &mut self,
1101 value: impl Into<super::InputContent>,
1102 window: &mut Window,
1103 cx: &mut Context<Self>,
1104 ) {
1105 let content = value.into();
1106 self.inline_tokens = None;
1107 self.undo_manager.set_ignoring(true);
1108 self.emit_events = false;
1109 self.replace_text(content.text().clone(), window, cx);
1110 self.install_tokens(content);
1111 self.undo_manager.set_ignoring(false);
1112 self.emit_events = true;
1113
1114 self.reset_selection();
1115 self.reset_lsp_state();
1116 self.reset_scroll_to_start();
1117
1118 self.undo_manager.clear();
1119 cx.notify();
1120 }
1121
1122 pub fn replace_all(
1132 &mut self,
1133 text: impl Into<SharedString>,
1134 window: &mut Window,
1135 cx: &mut Context<Self>,
1136 ) {
1137 self.replace_text(text, window, cx);
1138 self.reset_selection();
1139 self.reset_lsp_state();
1140 self.reset_scroll_to_start();
1141
1142 cx.notify();
1143 }
1144
1145 pub(super) fn with_edits_allowed(&mut self, f: impl FnOnce(&mut Self)) {
1150 let (was_disabled, was_readonly) = (self.disabled, self.readonly);
1151 (self.disabled, self.readonly) = (false, false);
1152 f(self);
1153 (self.disabled, self.readonly) = (was_disabled, was_readonly);
1154 }
1155
1156 pub fn insert(
1160 &mut self,
1161 text: impl Into<SharedString>,
1162 window: &mut Window,
1163 cx: &mut Context<Self>,
1164 ) {
1165 let text: SharedString = text.into();
1166 self.with_edits_allowed(|this| {
1167 this.undo_manager.set_pending_intent(EditIntent::Atomic);
1168 let range_utf16 = this.range_to_utf16(&(this.cursor()..this.cursor()));
1169 this.replace_text_in_range_silent(Some(range_utf16), &text, window, cx);
1170 let end = this.active_selection().end;
1171 this.set_cursor_to(end);
1172 });
1173 }
1174
1175 pub fn replace(
1179 &mut self,
1180 text: impl Into<SharedString>,
1181 window: &mut Window,
1182 cx: &mut Context<Self>,
1183 ) {
1184 let text: SharedString = text.into();
1185 self.with_edits_allowed(|this| {
1186 this.undo_manager.set_pending_intent(EditIntent::Atomic);
1187 this.replace_text_in_range_silent(None, &text, window, cx);
1188 let end = this.active_selection().end;
1189 this.set_cursor_to(end);
1190 });
1191 }
1192
1193 fn replace_text(
1194 &mut self,
1195 text: impl Into<SharedString>,
1196 window: &mut Window,
1197 cx: &mut Context<Self>,
1198 ) {
1199 let text: SharedString = text.into();
1200 self.with_edits_allowed(|this| {
1201 this.undo_manager.set_pending_intent(EditIntent::Atomic);
1202 let range = 0..this.text.chars().map(|c| c.len_utf16()).sum();
1203 this.replace_text_in_range_silent(Some(range), &text, window, cx);
1204 this.reset_highlighter(cx);
1205 });
1206 }
1207
1208 fn reset_selection(&mut self) {
1209 self.selections.remove_all_but_active();
1210
1211 if self.is_single_line() {
1215 let end = self.text.len();
1216 self.set_cursor_to(end);
1217 } else {
1218 self.active_selection_mut().clear();
1219 }
1220 }
1221
1222 fn reset_lsp_state(&mut self) {
1223 if self.is_code_editor() {
1224 self._pending_update = true;
1225 M::reset_language_features(self);
1226 }
1227 }
1228
1229 fn reset_scroll_to_start(&mut self) {
1230 self.scroll_handle.set_offset(point(px(0.), px(0.)));
1234 if self.is_single_line() {
1235 self.deferred_scroll_offset = Some(point(px(0.), px(0.)));
1236 }
1237 }
1238
1239 #[allow(unused)]
1243 pub(crate) fn disabled(mut self, disabled: bool) -> Self {
1244 self.disabled = disabled;
1245 self
1246 }
1247
1248 pub fn set_disabled(&mut self, disabled: bool, cx: &mut Context<Self>) {
1249 if self.disabled == disabled {
1250 return;
1251 }
1252
1253 self.disabled = disabled;
1254 if disabled {
1255 M::hide_context_menu(self, cx);
1256 M::clear_inline_completion(self, cx);
1257 }
1258 cx.notify();
1259 }
1260
1261 #[allow(unused)]
1269 pub(crate) fn readonly(mut self, readonly: bool) -> Self {
1270 self.readonly = readonly;
1271 self
1272 }
1273
1274 pub fn set_readonly(&mut self, readonly: bool, cx: &mut Context<Self>) {
1275 if self.readonly == readonly {
1276 return;
1277 }
1278
1279 self.readonly = readonly;
1280 if readonly {
1281 M::hide_context_menu(self, cx);
1282 M::clear_inline_completion(self, cx);
1283 self.search_session.replace_mode = false;
1284 }
1285 cx.notify();
1286 }
1287
1288 pub fn is_editable(&self) -> bool {
1293 !self.disabled && !self.readonly
1294 }
1295
1296 pub fn clean_on_escape(mut self) -> Self {
1300 self.clean_on_escape = true;
1301 self
1302 }
1303
1304 pub fn set_clean_on_escape(&mut self, clean: bool) {
1305 self.clean_on_escape = clean;
1306 }
1307
1308 #[doc(hidden)]
1313 pub fn submit_on_enter(mut self, submit: bool) -> Self {
1314 self.submit_on_enter = submit;
1315 self
1316 }
1317
1318 pub fn set_submit_on_enter(&mut self, submit: bool, cx: &mut Context<Self>) {
1319 self.submit_on_enter = submit;
1320 cx.notify();
1321 }
1322
1323 pub fn input_action(mut self, action: TextInputAction) -> Self {
1330 self.input_action = Some(action);
1331 self
1332 }
1333
1334 pub fn autocorrect(mut self, autocorrect: bool) -> Self {
1340 self.autocorrect = Some(autocorrect);
1341 self
1342 }
1343
1344 pub fn autocapitalize(mut self, autocapitalize: Autocapitalize) -> Self {
1349 self.autocapitalize = Some(autocapitalize);
1350 self
1351 }
1352
1353 pub fn text_input_configuration(&self) -> TextInputConfiguration {
1357 let autocorrect = self.autocorrect.unwrap_or(false);
1361 let input_action =
1362 self.input_action
1363 .unwrap_or(if self.is_multi_line() && !self.submit_on_enter {
1364 TextInputAction::Enter
1365 } else {
1366 TextInputAction::Done
1367 });
1368 TextInputConfiguration {
1369 autocorrect,
1370 autocapitalize: self.autocapitalize.unwrap_or(Autocapitalize::None),
1371 suggestions: autocorrect,
1372 input_action,
1373 }
1374 }
1375
1376 #[doc(hidden)]
1378 pub fn show_whitespaces(mut self, show: bool) -> Self {
1379 self.show_whitespaces = show;
1380 self
1381 }
1382
1383 pub fn set_show_whitespaces(&mut self, show: bool, _: &mut Window, cx: &mut Context<Self>) {
1385 self.show_whitespaces = show;
1386 cx.notify();
1387 }
1388
1389 pub fn scroll_beyond_last_line(mut self, rows: Option<usize>) -> Self {
1399 self.scroll_beyond_last_line = rows;
1400 self
1401 }
1402
1403 pub fn set_scroll_beyond_last_line(
1405 &mut self,
1406 rows: Option<usize>,
1407 _: &mut Window,
1408 cx: &mut Context<Self>,
1409 ) {
1410 if self.scroll_beyond_last_line == rows {
1411 return;
1412 }
1413 self.scroll_beyond_last_line = rows;
1414 cx.notify();
1415 }
1416
1417 pub fn cursor_surrounding_lines(mut self, lines: Option<usize>) -> Self {
1431 self.cursor_surrounding_lines = lines;
1432 self
1433 }
1434
1435 pub fn set_cursor_surrounding_lines(
1437 &mut self,
1438 lines: Option<usize>,
1439 _: &mut Window,
1440 cx: &mut Context<Self>,
1441 ) {
1442 if self.cursor_surrounding_lines == lines {
1443 return;
1444 }
1445 self.cursor_surrounding_lines = lines;
1446 cx.notify();
1447 }
1448
1449 pub fn default_value(mut self, value: impl Into<SharedString>) -> Self {
1451 let text: SharedString = value.into();
1452 self.text = Rope::from(self.normalize_input(&text).as_ref());
1453 if let Some(diagnostics) = self.mode.diagnostics_mut() {
1454 diagnostics.reset(&self.text)
1455 }
1456 self._pending_update = true;
1459 self
1460 }
1461
1462 pub fn value(&self) -> SharedString {
1467 SharedString::new(self.text.to_string())
1468 }
1469
1470 pub fn selected_value(&self) -> SharedString {
1476 SharedString::new(self.selected_text().to_string())
1477 }
1478
1479 pub fn unmask_value(&self) -> SharedString {
1481 self.mask_pattern.unmask(&self.text.to_string()).into()
1482 }
1483
1484 #[doc(hidden)]
1490 pub fn prepare(&mut self, _: &mut Window, _: &mut Context<Self>) {}
1491
1492 pub fn text(&self) -> &Rope {
1497 &self.text
1498 }
1499
1500 pub fn cursor_position(&self) -> Position {
1502 let offset = self.cursor();
1503 self.text.offset_to_position(offset)
1504 }
1505
1506 pub fn set_cursor_position(
1510 &mut self,
1511 position: impl Into<Position>,
1512 window: &mut Window,
1513 cx: &mut Context<Self>,
1514 ) {
1515 let position: Position = position.into();
1516 let offset = self.text.position_to_offset(&position);
1517
1518 self.move_to(offset, None, cx);
1519 self.update_preferred_column();
1520 self.focus(window, cx);
1521 }
1522
1523 pub fn focus(&self, window: &mut Window, cx: &mut Context<Self>) {
1525 self.focus_handle.focus(window, cx);
1526 self.blink_cursor.update(cx, |cursor, cx| {
1527 cursor.start(cx);
1528 });
1529 }
1530
1531 pub fn refresh(&mut self, cx: &mut Context<Self>) {
1545 self._pending_update = true;
1546 cx.notify();
1547 }
1548
1549 pub(super) fn select_left(&mut self, _: &SelectLeft, _: &mut Window, cx: &mut Context<Self>) {
1550 self.undo_manager.break_transaction_coalescing();
1551 self.select_all_cursors_to(|s, sel| s.previous_boundary(sel.cursor_offset()), cx);
1552 }
1553
1554 pub(super) fn select_right(&mut self, _: &SelectRight, _: &mut Window, cx: &mut Context<Self>) {
1555 self.select_all_cursors_to(|s, sel| s.next_boundary(sel.cursor_offset()), cx);
1556 }
1557
1558 pub(super) fn select_up(&mut self, _: &SelectUp, _: &mut Window, cx: &mut Context<Self>) {
1559 if self.is_single_line() {
1560 return;
1561 }
1562 self.select_all_cursors_to_with_affinity(
1563 |s, sel| {
1564 s.vertical_selection_target(
1565 sel.cursor_offset(),
1566 sel.column_anchor,
1567 s.line_end_affinity_for(sel),
1568 -1,
1569 )
1570 },
1571 true,
1572 cx,
1573 );
1574 }
1575
1576 pub(super) fn select_down(&mut self, _: &SelectDown, _: &mut Window, cx: &mut Context<Self>) {
1577 if self.is_single_line() {
1578 return;
1579 }
1580 self.select_all_cursors_to_with_affinity(
1581 |s, sel| {
1582 s.vertical_selection_target(
1583 sel.cursor_offset(),
1584 sel.column_anchor,
1585 s.line_end_affinity_for(sel),
1586 1,
1587 )
1588 },
1589 true,
1590 cx,
1591 );
1592 }
1593
1594 pub(super) fn on_action_select_all(
1595 &mut self,
1596 _: &SelectAll,
1597 window: &mut Window,
1598 cx: &mut Context<Self>,
1599 ) {
1600 self.select_all(window, cx);
1601 }
1602
1603 pub(super) fn select_to_start(
1604 &mut self,
1605 _: &SelectToStart,
1606 _: &mut Window,
1607 cx: &mut Context<Self>,
1608 ) {
1609 self.undo_manager.break_transaction_coalescing();
1610 self.select_all_cursors_to(|_, _| 0, cx);
1611 }
1612
1613 pub(super) fn select_to_end(
1614 &mut self,
1615 _: &SelectToEnd,
1616 _: &mut Window,
1617 cx: &mut Context<Self>,
1618 ) {
1619 self.undo_manager.break_transaction_coalescing();
1620 let end = self.text.len();
1621 self.select_all_cursors_to(move |_, _| end, cx);
1622 }
1623
1624 pub(super) fn select_to_start_of_line(
1625 &mut self,
1626 _: &SelectToStartOfLine,
1627 _: &mut Window,
1628 cx: &mut Context<Self>,
1629 ) {
1630 self.undo_manager.break_transaction_coalescing();
1631 self.select_all_cursors_to(
1632 |s, sel| s.start_of_line_at(sel.cursor_offset(), s.line_end_affinity_for(sel)),
1633 cx,
1634 );
1635 }
1636
1637 pub(super) fn select_to_end_of_line(
1638 &mut self,
1639 _: &SelectToEndOfLine,
1640 _: &mut Window,
1641 cx: &mut Context<Self>,
1642 ) {
1643 self.undo_manager.break_transaction_coalescing();
1644 self.select_all_cursors_to_with_affinity(
1645 |s, sel| {
1646 (
1647 s.end_of_line_at(sel.cursor_offset(), s.line_end_affinity_for(sel)),
1648 true,
1649 )
1650 },
1651 false,
1652 cx,
1653 );
1654 }
1655
1656 pub(super) fn select_to_previous_word(
1657 &mut self,
1658 _: &SelectToPreviousWordStart,
1659 _: &mut Window,
1660 cx: &mut Context<Self>,
1661 ) {
1662 self.undo_manager.break_transaction_coalescing();
1663 self.select_all_cursors_to(
1664 |s, sel| s.previous_start_of_word_at(sel.cursor_offset()),
1665 cx,
1666 );
1667 }
1668
1669 pub(super) fn select_to_next_word(
1670 &mut self,
1671 _: &SelectToNextWordEnd,
1672 _: &mut Window,
1673 cx: &mut Context<Self>,
1674 ) {
1675 self.undo_manager.break_transaction_coalescing();
1676 self.select_all_cursors_to(|s, sel| s.next_end_of_word_at(sel.cursor_offset()), cx);
1677 }
1678
1679 pub(super) fn previous_start_of_word_at(&self, offset: usize) -> usize {
1682 if self.masked {
1683 return 0;
1687 }
1688
1689 let offset = self.offset_from_utf16(self.offset_to_utf16(offset));
1690 let left_part = self.text.slice(0..offset).to_string();
1692
1693 let target = UnicodeSegmentation::split_word_bound_indices(left_part.as_str())
1694 .rfind(|(_, s)| !s.trim_start().is_empty())
1695 .map(|(i, _)| i)
1696 .unwrap_or(0);
1697 self.token_boundary(target, Bias::Left)
1698 }
1699
1700 pub(super) fn next_end_of_word_at(&self, offset: usize) -> usize {
1702 if self.masked {
1703 return self.text.len();
1705 }
1706
1707 let offset = self.offset_from_utf16(self.offset_to_utf16(offset));
1708 let right_part = self.text.slice(offset..self.text.len()).to_string();
1709
1710 let target = UnicodeSegmentation::split_word_bound_indices(right_part.as_str())
1711 .find(|(_, s)| !s.trim_start().is_empty())
1712 .map(|(i, s)| offset + i + s.len())
1713 .unwrap_or(self.text.len());
1714 self.token_boundary(target, Bias::Right)
1715 }
1716
1717 pub(super) fn start_of_line_at(&self, offset: usize, line_end_affinity: bool) -> usize {
1722 if self.is_single_line() {
1723 return 0;
1724 }
1725
1726 let row = self.text.offset_to_point(offset).row;
1727 let logical_start = self.text.line_start_offset(row);
1728
1729 if self.soft_wrap && self.is_code_editor() {
1730 let wrap_point = self
1731 .display_map
1732 .offset_to_wrap_display_point_with_affinity(offset, line_end_affinity);
1733 if let Some(line) = self.display_map.line(row)
1734 && let Some(range) = line.wrapped_lines.get(wrap_point.local_row)
1735 {
1736 let visual_start = logical_start + range.start;
1737 if offset != visual_start {
1738 return visual_start;
1739 }
1740 }
1741 }
1742
1743 logical_start
1744 }
1745
1746 pub(super) fn end_of_line_at(&self, offset: usize, line_end_affinity: bool) -> usize {
1751 if self.is_single_line() {
1752 return self.text.len();
1753 }
1754
1755 let row = self.text.offset_to_point(offset).row;
1756 let logical_start = self.text.line_start_offset(row);
1757 let logical_end = self.text.line_end_offset(row);
1758
1759 if self.soft_wrap && self.is_code_editor() {
1760 let wrap_point = self
1764 .display_map
1765 .offset_to_wrap_display_point_with_affinity(offset, line_end_affinity);
1766 if let Some(line) = self.display_map.line(row)
1767 && let Some(range) = line.wrapped_lines.get(wrap_point.local_row)
1768 {
1769 let visual_end = logical_start + range.end;
1770 if offset != visual_end {
1771 return visual_end;
1772 }
1773 }
1774 }
1775
1776 logical_end
1777 }
1778
1779 pub(super) fn indent_of_next_line(&mut self) -> String {
1783 self.indent_of_next_line_at(self.cursor())
1784 }
1785
1786 pub(super) fn indent_of_next_line_at(&mut self, offset: usize) -> String {
1788 if self.is_single_line() {
1789 return "".into();
1790 }
1791
1792 let mut current_indent = String::new();
1793 let mut next_indent = String::new();
1794 let line_end_affinity = self.line_end_affinity_at(offset);
1795 let current_line_start_pos = self.start_of_line_at(offset, line_end_affinity);
1796 let next_line_start_pos = self.end_of_line_at(offset, line_end_affinity);
1797 for c in self.text.slice(current_line_start_pos..).chars() {
1798 if !c.is_whitespace() {
1799 break;
1800 }
1801 if c == '\n' || c == '\r' {
1802 break;
1803 }
1804 current_indent.push(c);
1805 }
1806
1807 for c in self.text.slice(next_line_start_pos..).chars() {
1808 if !c.is_whitespace() {
1809 break;
1810 }
1811 if c == '\n' || c == '\r' {
1812 break;
1813 }
1814 next_indent.push(c);
1815 }
1816
1817 if next_indent.len() > current_indent.len() {
1818 return next_indent;
1819 }
1820
1821 if let Some(rules) = self.mode.language_config() {
1824 if self.mode.is_smart_indent()
1825 && M::editing_syntax_context(self, offset) == crate::input::SyntaxContext::Code
1826 {
1827 let line_before: String = self
1828 .text
1829 .slice(current_line_start_pos..offset)
1830 .chars()
1831 .collect();
1832 if rules.opens_indent(line_before.trim_end()) {
1833 let tab = self.mode.tab_size();
1834 if tab.hard_tabs {
1835 current_indent.push('\t');
1836 } else {
1837 for _ in 0..tab.tab_size {
1838 current_indent.push(' ');
1839 }
1840 }
1841 } else {
1842 let line_after = self.text.slice(offset..next_line_start_pos).to_string();
1843 if rules.closes_indent(&line_after) {
1844 let tab = self.mode.tab_size();
1845 if current_indent.ends_with('\t') {
1846 current_indent.pop();
1847 } else {
1848 for _ in 0..tab.tab_size {
1849 if !current_indent.ends_with(' ') {
1850 break;
1851 }
1852 current_indent.pop();
1853 }
1854 }
1855 }
1856 }
1857 }
1858 }
1859
1860 current_indent
1861 }
1862
1863 fn delete_selections(
1871 &mut self,
1872 silent: bool,
1873 collapsed_intent: EditIntent,
1874 mut collapsed_target: impl FnMut(&mut Self, usize) -> Range<usize>,
1875 window: &mut Window,
1876 cx: &mut Context<Self>,
1877 ) {
1878 if !self.is_editable() {
1879 return;
1880 }
1881 let cursors: Vec<CursorSelection> = self.selections.iter().copied().collect();
1882 let intent = if cursors.iter().all(|sel| sel.is_empty()) {
1883 collapsed_intent
1884 } else {
1885 EditIntent::Atomic
1886 };
1887 let mut new_selections: Vec<CursorSelection> = Vec::with_capacity(cursors.len());
1888 for sel in &cursors {
1889 let range = if sel.is_empty() {
1890 collapsed_target(self, sel.cursor_offset())
1891 } else {
1892 sel.start..sel.end
1893 };
1894 let (start, end) = (range.start.min(range.end), range.start.max(range.end));
1895 let mut selection = *sel;
1896 selection.start = start;
1897 selection.end = end;
1898 new_selections.push(selection);
1899 }
1900 self.undo_manager.begin_transaction_with(intent);
1903 self.undo_manager
1904 .record_selections(cursors.clone(), cursors.clone());
1905 self.selections.replace_all(new_selections);
1906 self.undo_manager.set_pending_intent(intent);
1907
1908 let was_silent = self.silent_replace_text;
1909 self.silent_replace_text = silent;
1910 self.replace_text_in_range(None, "", window, cx);
1911 self.silent_replace_text = was_silent;
1912 self.undo_manager
1913 .record_selections(cursors, self.selections.iter().copied().collect());
1914 self.undo_manager.commit_transaction();
1915 self.pause_blink_cursor(cx);
1916 }
1917
1918 fn auto_close_target(&self, range: &Range<usize>, text: &str) -> Option<(usize, SharedString)> {
1921 if self.silent_replace_text
1922 || self.ime_marked_range.is_some()
1923 || !self.selections.is_single()
1924 || !self.active_selection().is_empty()
1925 || !range.is_empty()
1926 || range.start != self.cursor()
1927 || text.chars().count() != 1
1928 || !self.mode.is_auto_close()
1929 {
1930 return None;
1931 }
1932 let rules = self.mode.language_config()?;
1933 if self
1934 .text
1935 .chars_at(range.start)
1936 .next()
1937 .is_some_and(|c| !c.is_whitespace() && !rules.auto_close_before.contains(c))
1938 {
1939 return None;
1940 }
1941 for (open, close, not_in) in rules.closing_pairs() {
1942 let Some(prefix) = open.strip_suffix(text) else {
1943 continue;
1944 };
1945 if !self.text_before_matches(range.start, prefix) {
1946 continue;
1947 }
1948 let start = range.start - prefix.len();
1949 if open == close
1950 && (self.is_escaped_at(start)
1951 || self
1952 .text
1953 .chars_at(start)
1954 .reversed()
1955 .next()
1956 .is_some_and(|c| c.is_alphanumeric() || c == '_'))
1957 {
1958 continue;
1959 }
1960 if not_in.contains(&M::editing_syntax_context(self, start)) {
1961 continue;
1962 }
1963 return Some((open.len(), close.into()));
1964 }
1965 None
1966 }
1967
1968 fn text_before_matches(&self, offset: usize, text: &str) -> bool {
1969 offset >= text.len()
1970 && self.text.is_char_boundary(offset - text.len())
1971 && self.text.slice(offset - text.len()..offset) == text
1972 }
1973
1974 fn text_after_matches(&self, offset: usize, text: &str) -> bool {
1975 offset + text.len() <= self.text.len()
1976 && self.text.is_char_boundary(offset + text.len())
1977 && self.text.slice(offset..offset + text.len()) == text
1978 }
1979
1980 fn is_escaped_at(&self, offset: usize) -> bool {
1981 self.text
1982 .chars_at(offset)
1983 .reversed()
1984 .take_while(|c| *c == '\\')
1985 .count()
1986 % 2
1987 == 1
1988 }
1989
1990 fn skip_over_target(&self, new_text: &str) -> Option<usize> {
1997 if !self.mode.is_auto_close() || new_text.chars().count() != 1 {
1998 return None;
1999 }
2000 let rules = self.mode.language_config()?;
2001 let cursor = self.cursor();
2002 for (open, close, not_in) in rules.closing_pairs() {
2003 for (index, _) in close.char_indices() {
2004 if !close[index..].starts_with(new_text)
2005 || !self.text_before_matches(cursor, &close[..index])
2006 || !self.text_after_matches(cursor, &close[index..])
2007 {
2008 continue;
2009 }
2010 let context = M::editing_syntax_context(self, cursor);
2011 let generated =
2012 self.mode
2013 .auto_closed_pairs()
2014 .contains_closer(cursor, index, close.len());
2015 if !generated
2016 && not_in.contains(&context)
2017 && !(context == crate::input::SyntaxContext::String && open == close)
2018 {
2019 continue;
2020 }
2021 if self.is_escaped_at(cursor - index) {
2022 continue;
2023 }
2024 return Some(cursor + new_text.len());
2025 }
2026 }
2027 None
2028 }
2029
2030 pub(super) fn backspace(&mut self, _: &Backspace, window: &mut Window, cx: &mut Context<Self>) {
2031 if self.selections.is_single() && self.active_selection().is_empty() && self.cursor() == 0 {
2037 cx.propagate();
2038 return;
2039 }
2040
2041 if self.mode.is_auto_close()
2044 && self.selections.is_single()
2045 && self.active_selection().is_empty()
2046 {
2047 let off = self.cursor();
2048 let deletion = self.mode.language_config().and_then(|rules| {
2049 rules.closing_pairs().find_map(|(open, close, not_in)| {
2050 if !self.text_before_matches(off, open) || !self.text_after_matches(off, close)
2051 {
2052 return None;
2053 }
2054 let start = off - open.len();
2055 let generated = self
2056 .mode
2057 .auto_closed_pairs()
2058 .contains(start..off, off..off + close.len());
2059 if self.is_escaped_at(start)
2060 || (!generated && not_in.contains(&M::editing_syntax_context(self, start)))
2061 {
2062 return None;
2063 }
2064 Some(start..off + close.len())
2065 })
2066 });
2067 if let Some(range) = deletion {
2068 let utf16 = self.range_to_utf16(&range);
2069 self.replace_text_in_range_silent(Some(utf16), "", window, cx);
2070 return;
2071 }
2072 }
2073
2074 self.delete_selections(
2075 false,
2076 EditIntent::Backspace,
2077 |s, offset| s.previous_boundary(offset)..offset,
2078 window,
2079 cx,
2080 );
2081 }
2082
2083 pub(super) fn delete(&mut self, _: &Delete, window: &mut Window, cx: &mut Context<Self>) {
2084 self.delete_selections(
2085 false,
2086 EditIntent::DeleteForward,
2087 |s, offset| offset..s.next_boundary(offset),
2088 window,
2089 cx,
2090 );
2091 }
2092
2093 pub(super) fn delete_to_beginning_of_line(
2094 &mut self,
2095 _: &DeleteToBeginningOfLine,
2096 window: &mut Window,
2097 cx: &mut Context<Self>,
2098 ) {
2099 self.delete_selections(
2100 true,
2101 EditIntent::Atomic,
2102 |s, offset| {
2103 let mut start = s.start_of_line_at(offset, s.line_end_affinity_at(offset));
2104 if start == offset {
2105 start = start.saturating_sub(1);
2106 }
2107 start..offset
2108 },
2109 window,
2110 cx,
2111 );
2112 }
2113
2114 pub(super) fn delete_to_end_of_line(
2115 &mut self,
2116 _: &DeleteToEndOfLine,
2117 window: &mut Window,
2118 cx: &mut Context<Self>,
2119 ) {
2120 self.delete_selections(
2121 true,
2122 EditIntent::Atomic,
2123 |s, offset| {
2124 let mut end = s.end_of_line_at(offset, s.line_end_affinity_at(offset));
2125 if end == offset {
2126 end = (end + 1).clamp(0, s.text.len());
2127 }
2128 offset..end
2129 },
2130 window,
2131 cx,
2132 );
2133 }
2134
2135 pub(super) fn delete_previous_word(
2136 &mut self,
2137 _: &DeleteToPreviousWordStart,
2138 window: &mut Window,
2139 cx: &mut Context<Self>,
2140 ) {
2141 self.delete_selections(
2142 true,
2143 EditIntent::Atomic,
2144 |s, offset| s.previous_start_of_word_at(offset)..offset,
2145 window,
2146 cx,
2147 );
2148 }
2149
2150 pub(super) fn delete_next_word(
2151 &mut self,
2152 _: &DeleteToNextWordEnd,
2153 window: &mut Window,
2154 cx: &mut Context<Self>,
2155 ) {
2156 self.delete_selections(
2157 true,
2158 EditIntent::Atomic,
2159 |s, offset| offset..s.next_end_of_word_at(offset),
2160 window,
2161 cx,
2162 );
2163 }
2164
2165 pub(super) fn enter(&mut self, action: &Enter, window: &mut Window, cx: &mut Context<Self>) {
2166 if M::handle_context_menu_action(self, Box::new(action.clone()), window, cx) {
2167 return;
2168 }
2169
2170 if M::has_inline_completion(self) {
2172 M::clear_inline_completion(self, cx);
2173 }
2174
2175 let insert_newline = self.is_multi_line() && (!self.submit_on_enter || action.shift);
2180
2181 if insert_newline {
2182 if !self.selections.is_single() {
2183 self.selections.merge_overlapping();
2185 let selections: Vec<CursorSelection> = self.selections.iter().copied().collect();
2186 let mut edits: Vec<(Range<usize>, String)> = Vec::with_capacity(selections.len());
2187 for sel in &selections {
2188 let indent = if self.is_code_editor() {
2189 self.indent_of_next_line_at(sel.cursor_offset())
2190 } else {
2191 String::new()
2192 };
2193 edits.push((sel.start..sel.end, format!("\n{}", indent)));
2194 }
2195 edits.sort_by_key(|(range, _)| range.start);
2196 self.replace_text_in_ranges(&edits, window, cx);
2197 self.pause_blink_cursor(cx);
2198 } else {
2199 let mut split = false;
2205 if self.is_code_editor() && self.active_selection().is_empty() {
2206 if let Some(rules) = self.mode.language_config() {
2207 if self.mode.is_smart_indent()
2208 && M::editing_syntax_context(self, self.cursor())
2209 == crate::input::SyntaxContext::Code
2210 {
2211 let off = self.cursor();
2212 let is_pair = rules.brackets.iter().any(|p| {
2213 !p.open.is_empty()
2214 && !p.close.is_empty()
2215 && p.open != p.close
2216 && self.text_before_matches(off, p.open.as_ref())
2217 && self.text_after_matches(off, p.close.as_ref())
2218 });
2219 if is_pair {
2220 let line_end_affinity = self.line_end_affinity_at(off);
2222 let line_start = self.start_of_line_at(off, line_end_affinity);
2223 let mut indent = String::new();
2224 for c in self.text.slice(line_start..).chars() {
2225 if !c.is_whitespace() || c == '\n' || c == '\r' {
2226 break;
2227 }
2228 indent.push(c);
2229 }
2230 let mut inner = indent.clone();
2232 if self.mode.is_smart_indent() {
2233 let tab = self.mode.tab_size();
2234 if tab.hard_tabs {
2235 inner.push('\t');
2236 } else {
2237 for _ in 0..tab.tab_size {
2238 inner.push(' ');
2239 }
2240 }
2241 }
2242 let new_line_text = format!("\n{inner}\n{indent}");
2243 self.replace_text_in_range_silent(None, &new_line_text, window, cx);
2244 self.set_cursor_to(off + 1 + inner.len());
2245 self.update_preferred_column();
2246 let cursors = self.selections.iter().copied().collect::<Vec<_>>();
2247 self.undo_manager
2248 .record_selections(cursors.clone(), cursors);
2249 self.pause_blink_cursor(cx);
2250 split = true;
2251 }
2252 }
2253 }
2254 }
2255 if !split {
2256 let indent = if self.is_code_editor() {
2258 self.indent_of_next_line()
2259 } else {
2260 "".to_string()
2261 };
2262
2263 let new_line_text = format!("\n{}", indent);
2265 self.replace_text_in_range_silent(None, &new_line_text, window, cx);
2266 self.pause_blink_cursor(cx);
2267 }
2268 }
2269 } else {
2270 self.undo_manager.break_transaction_coalescing();
2273 cx.propagate();
2274 }
2275
2276 cx.emit(InputEvent::PressEnter {
2277 secondary: action.secondary,
2278 shift: action.shift,
2279 });
2280 }
2281
2282 pub fn clean(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2283 self.replace_text("", window, cx);
2284 self.set_selection(0, 0);
2285 self.scroll_to(0, None, cx);
2286 }
2287
2288 pub(super) fn escape(&mut self, action: &Escape, window: &mut Window, cx: &mut Context<Self>) {
2289 if M::handle_context_menu_action(self, Box::new(action.clone()), window, cx) {
2290 return;
2291 }
2292
2293 M::hide_context_menu(self, cx);
2295
2296 if !self.selections.is_single() {
2298 self.undo_manager.break_transaction_coalescing();
2299 self.selections.remove_all_but_active();
2300 cx.notify();
2301 return;
2302 }
2303
2304 if M::has_inline_completion(self) {
2306 M::clear_inline_completion(self, cx);
2307 return; }
2309 M::clear_inline_completion(self, cx);
2310
2311 if self.touch_selection().is_some() {
2313 self.dismiss_touch_selection(cx);
2314 return;
2315 }
2316
2317 if self.ime_marked_range.is_some() {
2318 self.unmark_text(window, cx);
2319 }
2320
2321 if self.clean_on_escape && !cfg!(target_os = "android") {
2324 return self.clean(window, cx);
2325 }
2326
2327 cx.propagate();
2328 }
2329
2330 pub(crate) fn handle_right_click_menu(
2332 &mut self,
2333 position: Point<Pixels>,
2334 offset: usize,
2335 window: &mut Window,
2336 cx: &mut Context<Self>,
2337 ) {
2338 if self.disabled {
2339 return;
2340 }
2341 if crate::GlobalState::is_in_deferred_context(cx) {
2342 return;
2343 }
2344
2345 if !self.active_selection().contains(offset) {
2346 self.move_to(offset, None, cx);
2347 }
2348
2349 if self.is_code_editor() {
2350 M::on_hover_definition(self, offset, window, cx);
2351 }
2352
2353 if let Some(handler) = self.context_menu_handler.clone() {
2354 let capabilities = self.context_menu_capabilities();
2355 let input = cx.weak_entity();
2356 window.defer(cx, move |window, cx| {
2359 if input.upgrade().is_none() {
2360 return;
2361 }
2362 let previous_focus = window.focused(cx);
2363 handler(NativeMenu::new(), capabilities, position, window, cx);
2364 input
2367 .update(cx, |this, cx| {
2368 if window.focused(cx) == previous_focus
2369 && !this.focus_handle.is_focused(window)
2370 && this.has_selection_focus(window, cx)
2371 {
2372 return;
2374 }
2375 let menu_focus = window.focused(cx).filter(|focus| {
2376 Some(focus) != previous_focus.as_ref() && *focus != this.focus_handle
2377 });
2378 this.set_selection_focus(menu_focus, cx);
2379 })
2380 .ok();
2381 });
2382 }
2383 }
2384
2385 pub(super) fn add_cursor_above(
2386 &mut self,
2387 _: &AddCursorAbove,
2388 _: &mut Window,
2389 cx: &mut Context<Self>,
2390 ) {
2391 self.add_cursor_vertical(-1, cx);
2392 }
2393
2394 pub(super) fn add_cursor_below(
2395 &mut self,
2396 _: &AddCursorBelow,
2397 _: &mut Window,
2398 cx: &mut Context<Self>,
2399 ) {
2400 self.add_cursor_vertical(1, cx);
2401 }
2402
2403 fn add_cursor_vertical(&mut self, move_lines: isize, cx: &mut Context<Self>) {
2408 if !self.is_multi_line() {
2409 return;
2410 }
2411
2412 self.pause_blink_cursor(cx);
2413 self.undo_manager.break_transaction_coalescing();
2415
2416 let sources: Vec<(usize, Option<(Pixels, usize)>, bool)> = self
2417 .selections
2418 .iter()
2419 .map(|sel| {
2420 (
2421 sel.cursor_offset(),
2422 sel.column_anchor,
2423 self.line_end_affinity_for(sel),
2424 )
2425 })
2426 .collect();
2427 let mut offsets: std::collections::HashSet<usize> =
2428 sources.iter().map(|(offset, _, _)| *offset).collect();
2429
2430 let mut newest: Option<usize> = None;
2431 for (offset, anchor, line_end_affinity) in sources {
2432 let anchor = anchor.or_else(|| self.preferred_column_for(offset));
2433 let (target, _) = self.vertical_target(offset, anchor, line_end_affinity, move_lines);
2434 if target == offset || offsets.contains(&target) {
2435 continue;
2436 }
2437 offsets.insert(target);
2438 let id = self.selections.generate_id();
2439 let mut cursor = CursorSelection::new(id, target, target);
2440 cursor.column_anchor = anchor;
2441 self.selections.add(cursor);
2442 newest = Some(target);
2443 }
2444
2445 if let Some(newest) = newest {
2446 self.scroll_to(newest, None, cx);
2447 }
2448 cx.notify();
2449 }
2450
2451 pub(super) fn add_cursor_at(&mut self, offset: usize, cx: &mut Context<Self>) {
2456 if !self.is_multi_line() {
2457 return;
2458 }
2459
2460 for sel in self.selections.iter() {
2461 if sel.contains(offset) {
2462 return;
2463 }
2464 if sel.is_collapsed() && sel.cursor_offset() == offset {
2465 return;
2466 }
2467 }
2468
2469 self.undo_manager.break_transaction_coalescing();
2470 let id = self.selections.generate_id();
2471 let mut selection = CursorSelection::new(id, offset, offset);
2472 selection.column_anchor = self.preferred_column_for(offset);
2473 self.selections.add(selection);
2474 cx.notify();
2475 }
2476
2477 pub(super) fn build_columnar_selection(
2485 &mut self,
2486 start: ColumnarPoint,
2487 end: ColumnarPoint,
2488 cx: &mut Context<Self>,
2489 ) {
2490 if !self.is_multi_line() {
2491 return;
2492 }
2493
2494 self.undo_manager.break_transaction_coalescing();
2495
2496 let (start_row, start_col) = self.columnar_row_column(start);
2497 let (end_row, end_col) = self.columnar_row_column(end);
2498 let (start_row, end_row) = (start_row.min(end_row), start_row.max(end_row));
2499 let (start_col, end_col) = (start_col.min(end_col), start_col.max(end_col));
2500
2501 let mut new_selections = Vec::with_capacity(end_row - start_row + 1);
2502 for row in start_row..=end_row {
2503 let sel_start = self
2504 .display_map
2505 .display_row_column_to_offset(row, start_col);
2506 let sel_end = self.display_map.display_row_column_to_offset(row, end_col);
2507 let id = self.selections.generate_id();
2508 let sel_start = self.text.clip_offset(sel_start, Bias::Left);
2509 let sel_end = self.text.clip_offset(sel_end, Bias::Left);
2510 new_selections.push(CursorSelection::new(id, sel_start, sel_end));
2511 }
2512
2513 self.selections.replace_all(new_selections);
2514 cx.notify();
2515 }
2516
2517 fn columnar_row_column(&self, point: ColumnarPoint) -> (usize, usize) {
2520 let display_point = self.display_map.offset_to_wrap_display_point(point.offset);
2521 let row = self
2522 .display_map
2523 .wrap_row_to_display_row(display_point.row)
2524 .unwrap_or_else(|| {
2525 self.display_map
2526 .nearest_visible_display_row(display_point.row)
2527 });
2528
2529 (row, display_point.column + point.columns_past_line_end)
2530 }
2531
2532 pub(super) fn on_mouse_down(
2533 &mut self,
2534 event: &MouseDownEvent,
2535 window: &mut Window,
2536 cx: &mut Context<Self>,
2537 ) {
2538 if window.default_prevented() {
2541 return;
2542 }
2543 self.undo_manager.break_transaction_coalescing();
2544 if !self.disabled {
2548 self.focus(window, cx);
2549 self.pause_blink_cursor(cx);
2550 }
2551 crate::global_state::GlobalState::suppress_text_selection(cx);
2554
2555 M::clear_inline_completion(self, cx);
2557 if event.button == MouseButton::Left
2561 && event.click_count == 1
2562 && self.reopen_edit_menu_at(event.position, cx)
2563 {
2564 return;
2565 }
2566 self.dismiss_touch_selection(cx);
2567
2568 if let Some(ime_marked_range) = &self.ime_marked_range {
2571 if ime_marked_range.len() == 0 {
2572 self.ime_marked_range = None;
2573 }
2574 }
2575
2576 self.selecting = true;
2577 let (offset, line_end_affinity, columns_past_line_end) =
2578 self.resolve_mouse_position(event.position);
2579
2580 if M::on_click(self, event, offset, window, cx) {
2581 return;
2582 }
2583
2584 if event.button == MouseButton::Left && event.click_count >= 3 {
2586 self.select_line(offset, window, cx);
2587 return;
2588 }
2589
2590 if event.button == MouseButton::Left && event.click_count == 2 {
2592 self.select_word(offset, window, cx);
2593 if crate::GlobalState::is_touch_press(cx) {
2596 self.keep_touch_selection(cx);
2597 }
2598 return;
2599 }
2600
2601 if event.button == MouseButton::Right {
2603 if self.enable_context_menu {
2604 if !self.active_selection().contains(offset) {
2605 self.move_to(offset, None, cx);
2606 }
2607 self.pending_context_menu = Some((event.position, offset));
2608 }
2609 return;
2610 }
2611
2612 if self.is_multi_line() && event.button == MouseButton::Left {
2614 if event.modifiers.alt
2615 && (event.modifiers.shift || (cfg!(target_os = "linux") && event.modifiers.control))
2616 {
2617 self.column_select_start = Some(ColumnarPoint::new(offset, columns_past_line_end));
2620 self.selecting = true;
2621 self.move_to_with_affinity(offset, None, line_end_affinity, cx);
2622 return;
2623 } else if event.modifiers.alt {
2624 self.add_cursor_at(offset, cx);
2625 self.column_select_start = Some(ColumnarPoint::new(offset, columns_past_line_end));
2628 return;
2629 }
2630 }
2631
2632 if event.modifiers.shift {
2633 self.select_to_with_affinity(offset, line_end_affinity, cx);
2634 } else {
2635 self.move_to_with_affinity(offset, None, line_end_affinity, cx)
2636 }
2637 }
2638
2639 pub(super) fn on_mouse_up(
2640 &mut self,
2641 event: &MouseUpEvent,
2642 window: &mut Window,
2643 cx: &mut Context<Self>,
2644 ) {
2645 if event.button == MouseButton::Right {
2646 if let Some((position, offset)) = self.pending_context_menu.take() {
2647 self.handle_right_click_menu(position, offset, window, cx);
2648 }
2649 }
2650 if self.active_selection().is_empty() {
2651 self.active_selection_mut().reversed = false;
2652 }
2653 self.selecting = false;
2654 self.selected_word_range = None;
2655 self.column_select_start = None;
2656 self.auto_scroll.stop();
2657 }
2658
2659 pub(super) fn on_mouse_move(
2660 &mut self,
2661 event: &MouseMoveEvent,
2662 window: &mut Window,
2663 cx: &mut Context<Self>,
2664 ) {
2665 let within_bounds = self
2667 .last_bounds
2668 .as_ref()
2669 .map(|bounds| bounds.contains(&event.position))
2670 .unwrap_or(false);
2671
2672 if !within_bounds {
2673 M::clear_hover_state(self, cx);
2675 return;
2676 }
2677
2678 let (offset, _) = self.index_for_mouse_position(event.position);
2680 M::on_mouse_move(self, offset, event, window, cx);
2681
2682 if self.is_code_editor() {
2683 match self
2687 .mode
2688 .diagnostics()
2689 .and_then(|set| set.for_offset(offset))
2690 {
2691 Some(diagnostic) => {
2692 if self.diagnostic_popover.as_deref() != Some(diagnostic) {
2693 self.diagnostic_popover = Some(Rc::new(diagnostic.clone()));
2694 cx.notify();
2695 }
2696 }
2697 None => {
2698 if self.diagnostic_popover.take().is_some() {
2699 cx.notify();
2700 }
2701 }
2702 }
2703 }
2704 }
2705
2706 pub(super) fn on_scroll_wheel(
2707 &mut self,
2708 event: &ScrollWheelEvent,
2709 window: &mut Window,
2710 cx: &mut Context<Self>,
2711 ) {
2712 let line_height = self
2713 .last_layout
2714 .as_ref()
2715 .map(|layout| layout.line_height)
2716 .unwrap_or(window.line_height());
2717 let delta = event.delta.pixel_delta(line_height);
2718
2719 let old_offset = self.scroll_handle.offset();
2720 let precise = matches!(event.delta, gpui::ScrollDelta::Pixels(_));
2721 if !precise || !self.scroll_bounce || cx.reduce_motion() || !self.overflows_vertically() {
2722 let displaced = self.overscroll.offset() != 0.;
2723 self.overscroll.reset();
2724 if displaced {
2725 cx.notify();
2726 }
2727 }
2728 let bounces = self.scroll_bounce
2729 && precise
2730 && !cx.reduce_motion()
2731 && delta.y.abs() >= delta.x.abs()
2732 && self.overflows_vertically();
2733 if bounces {
2734 let mut overscroll = std::mem::take(&mut self.overscroll);
2735 let stretched = overscroll.packet(
2736 delta.y.as_f32(),
2737 event.touch_phase,
2738 self.input_bounds.size.height.as_f32(),
2739 cx.background_executor().now(),
2740 |dy| {
2741 let before = self.scroll_handle.offset();
2742 self.update_scroll_offset(
2743 Some(point(before.x + delta.x, before.y + px(dy))),
2744 cx,
2745 );
2746 (self.scroll_handle.offset().y - before.y).as_f32()
2747 },
2748 );
2749 self.overscroll = overscroll;
2750 if stretched {
2751 cx.notify();
2752 }
2753 } else {
2754 if matches!(
2756 event.touch_phase,
2757 gpui::TouchPhase::Ended | gpui::TouchPhase::Cancelled
2758 ) && self.overscroll.offset() != 0.
2759 {
2760 self.overscroll.packet(
2761 0.,
2762 event.touch_phase,
2763 self.input_bounds.size.height.as_f32(),
2764 cx.background_executor().now(),
2765 |_| 0.,
2766 );
2767 cx.notify();
2768 }
2769 self.update_scroll_offset(Some(old_offset + delta), cx);
2770 }
2771
2772 if self.scroll_handle.offset() != old_offset || self.overflows_along(delta) {
2776 cx.stop_propagation();
2777 }
2778
2779 if self.diagnostic_popover.take().is_some() {
2780 cx.notify();
2781 }
2782 self.edit_menu_on_scroll(event.touch_phase, cx);
2785 }
2786
2787 pub(super) fn takes_scroll(&self, delta: Point<Pixels>) -> bool {
2793 if delta.y.abs() >= delta.x.abs() {
2794 self.overflows_vertically()
2795 } else {
2796 self.scroll_size.width > self.input_bounds.size.width
2797 }
2798 }
2799
2800 fn overflows_along(&self, delta: Point<Pixels>) -> bool {
2804 if delta.y.abs() >= delta.x.abs() {
2805 delta.y != px(0.) && self.overflows_vertically()
2806 } else {
2807 self.scroll_size.width > self.input_bounds.size.width
2808 }
2809 }
2810
2811 fn overflows_vertically(&self) -> bool {
2813 self.is_multi_line() && self.scroll_size.height > self.input_bounds.size.height
2814 }
2815
2816 pub(super) fn update_scroll_offset(
2817 &mut self,
2818 offset: Option<Point<Pixels>>,
2819 cx: &mut Context<Self>,
2820 ) {
2821 let mut offset = offset.unwrap_or(self.scroll_handle.offset());
2822 let safe_y_range =
2823 (-self.scroll_size.height + self.input_bounds.size.height).min(px(0.0))..px(0.);
2824
2825 offset.y = if self.is_single_line() {
2826 px(0.)
2827 } else {
2828 offset.y.clamp(safe_y_range.start, safe_y_range.end)
2829 };
2830 offset.x = clamp_horizontal_scroll_offset(
2831 offset.x,
2832 self.scroll_size.width,
2833 self.input_bounds.size.width,
2834 self.text_align,
2835 );
2836 if self.scroll_handle.offset() != offset {
2837 self.scroll_handle.set_offset(offset);
2838 cx.notify();
2839 }
2840 }
2841
2842 pub(crate) fn scroll_to(
2846 &mut self,
2847 offset: usize,
2848 direction: Option<MoveDirection>,
2849 cx: &mut Context<Self>,
2850 ) {
2851 let padding = if direction.is_some() {
2852 ScrollPadding::SurroundingLines
2853 } else {
2854 ScrollPadding::Minimal
2855 };
2856 self.scroll_to_with_padding(offset, direction, padding, cx);
2857 }
2858
2859 pub(crate) fn scroll_to_with_padding(
2862 &mut self,
2863 offset: usize,
2864 direction: Option<MoveDirection>,
2865 padding: ScrollPadding,
2866 cx: &mut Context<Self>,
2867 ) {
2868 let Some(last_layout) = self.last_layout.as_ref() else {
2869 return;
2870 };
2871 let Some(bounds) = self.last_bounds.as_ref() else {
2872 return;
2873 };
2874
2875 let mut scroll_offset = self.scroll_handle.offset();
2876 let was_offset = scroll_offset;
2877 let line_height = last_layout.line_height;
2878
2879 let point = self.text.offset_to_point(offset);
2880
2881 let row = point.row;
2882
2883 let display_row = self
2885 .display_map
2886 .offset_to_display_row(offset, self.line_end_affinity_at(offset));
2887 let row_offset_y = line_height * display_row;
2888
2889 let safety_margin = match last_layout.text_align {
2892 TextAlign::Left => RIGHT_MARGIN,
2893 TextAlign::Right => px(0.),
2894 TextAlign::Center => CURSOR_WIDTH,
2895 };
2896 if let Some(vi) = last_layout
2897 .visible_buffer_lines
2898 .iter()
2899 .position(|&line| line == row)
2900 {
2901 let line = &last_layout.lines[vi];
2902 let local_offset = offset.saturating_sub(last_layout.visible_line_byte_offsets[vi]);
2903 if let Some(pos) = line.position_for_index(local_offset, last_layout, false) {
2904 let bounds_width = bounds.size.width - last_layout.line_number_width;
2905 let col_offset_x = pos.x;
2906 if col_offset_x - safety_margin < -scroll_offset.x {
2907 scroll_offset.x = -col_offset_x + safety_margin;
2909 } else if col_offset_x + safety_margin > -scroll_offset.x + bounds_width {
2910 scroll_offset.x = -(col_offset_x - bounds_width + safety_margin);
2911 }
2912 }
2913 }
2914
2915 let edge_height =
2920 if matches!(padding, ScrollPadding::SurroundingLines) && self.is_code_editor() {
2921 super::element::cursor_surrounding_padding(
2922 self.mode.is_auto_grow(),
2923 self.cursor_surrounding_lines,
2924 super::element::viewport_visible_lines(bounds.size.height, line_height),
2925 line_height,
2926 )
2927 } else {
2928 px(0.)
2929 };
2930 scroll_offset.y = super::element::reveal_row(
2931 scroll_offset.y,
2932 row_offset_y,
2933 line_height,
2934 bounds.size.height,
2935 edge_height,
2936 );
2937
2938 if direction == Some(MoveDirection::Up) {
2940 scroll_offset.y = scroll_offset.y.max(was_offset.y);
2941 } else if direction == Some(MoveDirection::Down) {
2942 scroll_offset.y = scroll_offset.y.min(was_offset.y);
2943 }
2944
2945 let safe_y_min = (-self.scroll_size.height + self.input_bounds.size.height).min(px(0.));
2949 scroll_offset.x = scroll_offset.x.min(px(0.));
2950 scroll_offset.y = scroll_offset.y.clamp(safe_y_min, px(0.));
2951 self.deferred_scroll_offset = Some(scroll_offset);
2952 cx.notify();
2953 }
2954
2955 pub(super) fn show_character_palette(
2956 &mut self,
2957 _: &ShowCharacterPalette,
2958 window: &mut Window,
2959 _: &mut Context<Self>,
2960 ) {
2961 window.show_character_palette();
2962 }
2963
2964 fn selected_texts(&self) -> Vec<String> {
2966 let mut selections: Vec<CursorSelection> = self
2967 .selections
2968 .iter()
2969 .copied()
2970 .filter(|sel| !sel.is_empty())
2971 .collect();
2972 selections.sort_by_key(|sel| sel.start);
2973 selections
2974 .iter()
2975 .map(|sel| self.text.slice(*sel).to_string())
2976 .collect()
2977 }
2978
2979 pub(super) fn copy(&mut self, _: &Copy, _: &mut Window, cx: &mut Context<Self>) {
2980 if !self.is_copyable() {
2981 return;
2982 }
2983
2984 let texts = self.selected_texts();
2985
2986 cx.write_to_clipboard(ClipboardItem::new_string(texts.join("\n")));
2987 }
2988
2989 pub(super) fn cut(&mut self, _: &Cut, window: &mut Window, cx: &mut Context<Self>) {
2990 if !self.is_copyable() {
2991 return;
2992 }
2993
2994 let texts = self.selected_texts();
2995
2996 cx.write_to_clipboard(ClipboardItem::new_string(texts.join("\n")));
2997
2998 self.undo_manager.set_pending_intent(EditIntent::Atomic);
2999 self.replace_text_in_range_silent(None, "", window, cx);
3000 }
3001
3002 pub(super) fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
3003 if !self.is_editable() {
3004 return;
3005 }
3006 if let Some(clipboard) = cx.read_from_clipboard() {
3007 self.insert_clipboard(clipboard, window, cx);
3008 return;
3009 }
3010 let read = cx.read_from_clipboard_async();
3015 let target = self.paste_target();
3016 cx.spawn_in(window, async move |this, cx| match read.await {
3017 Ok(Some(clipboard)) => {
3018 this.update_in(cx, |this, window, cx| {
3019 if this.is_editable()
3026 && this.focus_handle.is_focused(window)
3027 && this.paste_target() == target
3028 {
3029 this.insert_clipboard(clipboard, window, cx);
3030 }
3031 })
3032 .ok();
3033 }
3034 Ok(None) => {}
3035 Err(error) => tracing::warn!("failed to read the clipboard for paste: {error}"),
3036 })
3037 .detach();
3038 }
3039
3040 fn paste_target(&self) -> PasteTarget {
3044 PasteTarget {
3045 document_revision: self.document_revision,
3046 selections: self.selections.iter().copied().collect(),
3047 }
3048 }
3049
3050 fn insert_clipboard(
3056 &mut self,
3057 clipboard: ClipboardItem,
3058 window: &mut Window,
3059 cx: &mut Context<Self>,
3060 ) {
3061 let Some(mut new_text) = clipboard.text().filter(|text| !text.is_empty()) else {
3062 return;
3063 };
3064 self.undo_manager.set_pending_intent(EditIntent::Atomic);
3066
3067 if !self.is_multi_line() {
3068 new_text = new_text.replace('\n', "");
3069 self.replace_text_in_range_silent(None, &new_text, window, cx);
3070 self.scroll_to(self.cursor(), None, cx);
3071 return;
3072 }
3073
3074 if !self.selections.is_single() {
3077 self.selections.merge_overlapping();
3078 }
3079 let lines: Vec<String> = new_text.split('\n').map(|s| s.to_string()).collect();
3080 let count = self.selections.len();
3081 if count > 1 && lines.len() == count {
3082 let mut selections: Vec<CursorSelection> = self.selections.iter().copied().collect();
3083 selections.sort_by_key(|sel| sel.start);
3084 let edits: Vec<(Range<usize>, String)> = selections
3085 .iter()
3086 .zip(lines)
3087 .map(|(sel, line)| (sel.start..sel.end, line))
3088 .collect();
3089 self.replace_text_in_ranges(&edits, window, cx);
3090 } else {
3091 self.replace_text_in_range_silent(None, &new_text, window, cx);
3092 }
3093 self.scroll_to(self.cursor(), None, cx);
3094 }
3095
3096 fn typing_intent(&self, edits: &[(Range<usize>, String)], new_text: &str) -> EditIntent {
3099 if !new_text.is_empty()
3100 && !new_text.contains(['\n', '\r'])
3101 && edits.iter().all(|(range, _)| range.is_empty())
3102 {
3103 EditIntent::Typing
3104 } else {
3105 EditIntent::Atomic
3106 }
3107 }
3108
3109 fn collapse_for_intent(
3115 intent: EditIntent,
3116 mut selection: CursorSelection,
3117 range: &Range<usize>,
3118 ) -> CursorSelection {
3119 match intent {
3120 EditIntent::Backspace => selection.place_at(range.end, None),
3121 EditIntent::DeleteForward => selection.place_at(range.start, None),
3122 EditIntent::Typing | EditIntent::Atomic => {}
3123 }
3124 selection
3125 }
3126
3127 fn rewrites_typed_char(
3130 &self,
3131 selection: CursorSelection,
3132 range: &Range<usize>,
3133 old_text: &str,
3134 new_text: &str,
3135 ) -> bool {
3136 !self.silent_replace_text
3137 && selection.is_collapsed()
3138 && selection.cursor_offset() == range.end
3139 && old_text.chars().count() == 1
3140 && !old_text.contains(['\n', '\r'])
3141 && !new_text.contains(['\n', '\r'])
3142 }
3143
3144 fn push_history(
3145 &mut self,
3146 text: &Rope,
3147 range: &Range<usize>,
3148 new_text: &str,
3149 requested_intent: Option<EditIntent>,
3150 selection_before: CursorSelection,
3151 selection_after: Option<CursorSelection>,
3152 ) -> bool {
3153 self.document_revision = self.document_revision.wrapping_add(1);
3154 let token_delta = self.edit_tokens(range, new_text.len());
3155 if self.undo_manager.is_ignoring() {
3156 return false;
3157 }
3158
3159 let range =
3160 text.clip_offset(range.start, Bias::Left)..text.clip_offset(range.end, Bias::Right);
3161 let old_text = text.slice(range.clone()).to_string();
3162 let new_range = range.start..range.start + new_text.len();
3163
3164 let intent = requested_intent.unwrap_or_else(|| {
3165 if range.is_empty()
3166 && old_text.is_empty()
3167 && !new_text.is_empty()
3168 && !new_text.contains(['\n', '\r'])
3169 || self.rewrites_typed_char(selection_before, &range, &old_text, new_text)
3170 {
3171 EditIntent::Typing
3172 } else {
3173 EditIntent::Atomic
3174 }
3175 });
3176
3177 let selection_before = Self::collapse_for_intent(intent, selection_before, &range);
3178 let selection_after =
3179 selection_after.unwrap_or_else(|| (new_range.end..new_range.end).into());
3180
3181 let open_transaction = self.undo_manager.has_open_transaction();
3182 let mut change = Change::new(range, &old_text, new_range, new_text);
3183 change.token_delta = token_delta;
3184 let recorded = self.undo_manager.record_transaction(change, intent);
3185 if recorded && !open_transaction {
3188 self.undo_manager
3189 .record_selections(vec![selection_before], vec![selection_after]);
3190 }
3191 recorded
3192 }
3193
3194 pub(super) fn undo(&mut self, _: &Undo, window: &mut Window, cx: &mut Context<Self>) {
3195 self.replay_history(true, window, cx);
3196 }
3197
3198 pub(super) fn redo(&mut self, _: &Redo, window: &mut Window, cx: &mut Context<Self>) {
3199 self.replay_history(false, window, cx);
3200 }
3201
3202 fn replay_history(&mut self, undo: bool, window: &mut Window, cx: &mut Context<Self>) {
3203 if !self.is_editable() {
3204 return;
3205 }
3206 let replay = if undo {
3207 self.undo_manager.undo()
3208 } else {
3209 self.undo_manager.redo()
3210 };
3211 let Some(replay) = replay else {
3212 return;
3213 };
3214 let token_aware = self.inline_tokens.is_some()
3215 || replay
3216 .changes
3217 .iter()
3218 .any(|change| change.token_delta.is_some());
3219 let emit_events = self.emit_events;
3220 if token_aware {
3221 self.emit_events = false;
3222 }
3223 self.replaying_history = token_aware;
3224 self.undo_manager.set_ignoring(true);
3225 for change in &replay.changes {
3226 let (range, text) = if undo {
3227 (change.new_range.into(), &change.old_text)
3228 } else {
3229 (change.old_range.into(), &change.new_text)
3230 };
3231 let range_utf16 = self.range_to_utf16(&range);
3232 self.replace_text_in_range_silent(Some(range_utf16), text, window, cx);
3233 self.replay_tokens(&range, text.len(), change.token_delta.as_deref(), undo);
3234 }
3235 self.restore_selections(replay.selections);
3236 self.mode
3237 .restore_auto_closed_pairs(replay.auto_closed_pairs.unwrap_or_default());
3238 if token_aware {
3239 self.ime_marked_range = None;
3242 }
3243 self.undo_manager.set_ignoring(false);
3244 self.replaying_history = false;
3245 self.emit_events = emit_events;
3246 if token_aware && emit_events {
3247 cx.emit(InputEvent::Change);
3248 }
3249 cx.notify();
3250 }
3251
3252 fn restore_selections(&mut self, selections: Option<Vec<CursorSelection>>) {
3256 let Some(selections) = selections else {
3257 return;
3258 };
3259
3260 let len = self.text.len();
3261 let restored: Vec<CursorSelection> = selections
3262 .into_iter()
3263 .map(|mut sel| {
3264 sel.start = sel.start.min(len);
3265 sel.end = sel.end.min(len);
3266 sel
3267 })
3268 .collect();
3269 self.selections.replace_all(restored);
3270 self.selections.merge_overlapping();
3271 }
3272
3273 pub fn cursor(&self) -> usize {
3277 if let Some(ime_marked_range) = &self.ime_marked_range {
3278 return ime_marked_range.end;
3279 }
3280
3281 self.selections.active().cursor_offset()
3282 }
3283
3284 pub(super) fn active_selection(&self) -> &CursorSelection {
3286 self.selections.active()
3287 }
3288
3289 pub(super) fn active_selection_mut(&mut self) -> &mut CursorSelection {
3291 self.selections.active_mut()
3292 }
3293
3294 pub(super) fn set_selection(&mut self, start: usize, end: usize) {
3297 let range = self.normalize_token_range(start..end);
3298 let (start, end) = (range.start, range.end);
3299 let active = self.active_selection_mut();
3300 active.start = start;
3301 active.end = end;
3302 }
3303
3304 pub(super) fn set_cursor_to(&mut self, offset: usize) {
3307 let offset = self.token_boundary(offset, Bias::Right);
3308 let active = self.active_selection_mut();
3309 active.start = offset;
3310 active.end = offset;
3311 active.reversed = false;
3312 }
3313
3314 pub fn visible_row_range(&self) -> Option<std::ops::Range<usize>> {
3316 self.last_layout.as_ref().map(|l| l.visible_range.clone())
3317 }
3318
3319 pub fn scroll_offset(&self) -> gpui::Point<gpui::Pixels> {
3321 self.scroll_handle.offset()
3322 }
3323
3324 pub fn set_scroll_offset(&mut self, offset: gpui::Point<gpui::Pixels>, cx: &mut Context<Self>) {
3328 self.deferred_scroll_offset = Some(offset);
3329 cx.notify();
3330 }
3331
3332 pub fn line_height(&self) -> Option<gpui::Pixels> {
3334 self.last_layout.as_ref().map(|l| l.line_height)
3335 }
3336
3337 pub fn selected_range(&self) -> std::ops::Range<usize> {
3345 (*self.selections.active()).into()
3346 }
3347
3348 pub fn select_all(&mut self, _: &mut Window, cx: &mut Context<Self>) {
3349 self.undo_manager.break_transaction_coalescing();
3350 self.selections.remove_all_but_active();
3351 self.set_selection(0, self.text.len());
3352 cx.notify();
3353 }
3354
3355 pub fn set_selected_range(&mut self, range: Range<usize>, cx: &mut Context<Self>) {
3360 let range = self.normalize_token_range(range);
3361 let end_bias = if range.start == range.end {
3362 Bias::Left
3363 } else {
3364 Bias::Right
3365 };
3366 let start = self.text.clip_offset(range.start, Bias::Left);
3367 let end = self.text.clip_offset(range.end, end_bias);
3368
3369 self.move_to(start, None, cx);
3370 self.active_selection_mut().reversed = false;
3371 self.selected_word_range = None;
3372 self.select_to(end, cx);
3373 }
3374
3375 pub fn add_selection(&mut self, range: Range<usize>, cx: &mut Context<Self>) {
3386 if !self.is_multi_line() {
3387 self.set_selected_range(range, cx);
3388 return;
3389 }
3390
3391 let range = self.normalize_token_range(range);
3392 let end_bias = if range.start == range.end {
3393 Bias::Left
3394 } else {
3395 Bias::Right
3396 };
3397 let start = self.cursor_boundary(range.start, Bias::Left);
3398 let end = self.cursor_boundary(self.text.clip_offset(range.end, end_bias), Bias::Left);
3401
3402 for sel in self.selections.iter() {
3403 if sel.start <= start && end <= sel.end {
3404 return;
3405 }
3406 }
3407
3408 self.undo_manager.break_transaction_coalescing();
3409 self.selected_word_range = None;
3410 self.pause_blink_cursor(cx);
3411 M::hide_context_menu(self, cx);
3412 M::clear_inline_completion(self, cx);
3413 let id = self.selections.generate_id();
3414 let mut selection = CursorSelection::new(id, start, end);
3415 selection.column_anchor = self.preferred_column_for(end);
3416 self.selections.add(selection);
3417 self.selections.merge_overlapping();
3418 self.scroll_to(end, None, cx);
3419 cx.notify();
3420 }
3421
3422 pub(crate) fn index_for_mouse_position(&self, position: Point<Pixels>) -> (usize, bool) {
3430 let (offset, line_end_affinity, _) = self.resolve_mouse_position(position);
3431 (offset, line_end_affinity)
3432 }
3433
3434 pub(super) fn resolve_mouse_position(&self, position: Point<Pixels>) -> (usize, bool, usize) {
3441 if self.text.len() == 0 {
3443 return (0, false, 0);
3444 }
3445
3446 let (Some(bounds), Some(last_layout)) =
3447 (self.last_bounds.as_ref(), self.last_layout.as_ref())
3448 else {
3449 return (0, false, 0);
3450 };
3451
3452 let line_height = last_layout.line_height;
3453 let line_number_width = last_layout.line_number_width;
3454
3455 let inner_position = position - bounds.origin - point(line_number_width, px(0.));
3466
3467 let mut y_offset = last_layout.visible_top;
3468 let mut last_line_pos = None;
3471
3472 for (vi, (line_layout, _buffer_line)) in last_layout
3474 .lines
3475 .iter()
3476 .zip(last_layout.visible_buffer_lines.iter())
3477 .enumerate()
3478 {
3479 let line_start_offset = last_layout.visible_line_byte_offsets[vi];
3480
3481 let line_origin = point(px(0.), y_offset);
3483 let pos = inner_position - line_origin;
3484
3485 if self.is_single_line() {
3487 let local_index = line_layout.closest_index_for_x(pos.x, last_layout);
3488 return (
3490 self.resolve_index(line_start_offset + local_index),
3491 false,
3492 0,
3493 );
3494 }
3495
3496 if let Some((local_index, line_end_affinity)) =
3498 line_layout.closest_index_for_position(pos, last_layout)
3499 {
3500 return (
3501 self.resolve_index(line_start_offset + local_index),
3502 line_end_affinity,
3503 line_layout.columns_past_line_end(pos, last_layout),
3504 );
3505 } else if pos.y < px(0.) {
3506 return (self.resolve_index(line_start_offset), false, 0);
3508 }
3509
3510 y_offset += line_layout.size(line_height).height;
3511 last_line_pos = Some(pos);
3512 }
3513
3514 let last_position = last_layout
3517 .lines
3518 .last()
3519 .zip(last_line_pos)
3520 .zip(last_layout.visible_line_byte_offsets.last())
3521 .map(|((line_layout, pos), line_start)| {
3522 let last_row_top = (line_layout.size(line_height).height - line_height).max(px(0.));
3523 let pos = point(pos.x, last_row_top);
3524 (
3525 self.resolve_index(line_start + line_layout.len()),
3526 false,
3527 line_layout.columns_past_line_end(pos, last_layout),
3528 )
3529 });
3530
3531 last_position.unwrap_or((self.text.len(), false, 0))
3532 }
3533
3534 fn resolve_index(&self, index: usize) -> usize {
3537 if self.masked {
3538 self.text.char_index_to_offset(index / MASK_CHAR.len_utf8())
3539 } else {
3540 index.min(self.text.len())
3541 }
3542 }
3543
3544 fn extend_selection(
3554 sel: &mut CursorSelection,
3555 offset: usize,
3556 word_range: Option<CursorSelection>,
3557 ) {
3558 if sel.reversed {
3559 sel.start = offset;
3560 } else {
3561 sel.end = offset;
3562 }
3563
3564 if sel.end < sel.start {
3565 sel.reversed = !sel.reversed;
3566 std::mem::swap(&mut sel.start, &mut sel.end);
3567 }
3568
3569 if let Some(word_range) = word_range {
3570 if sel.start > word_range.start {
3571 sel.start = word_range.start;
3572 }
3573 if sel.end < word_range.end {
3574 sel.end = word_range.end;
3575 }
3576 }
3577 }
3578
3579 pub(crate) fn select_to(&mut self, offset: usize, cx: &mut Context<Self>) {
3581 self.select_to_with_affinity(offset, false, cx);
3582 }
3583
3584 pub(crate) fn select_to_with_affinity(
3591 &mut self,
3592 offset: usize,
3593 line_end_affinity: bool,
3594 cx: &mut Context<Self>,
3595 ) {
3596 M::clear_inline_completion(self, cx);
3597
3598 self.cursor_line_end_affinity = line_end_affinity;
3599 let offset = self.cursor_boundary(offset, Bias::Left);
3600 let word_range = self.selected_word_range;
3601 Self::extend_selection(self.active_selection_mut(), offset, word_range);
3602 let range = self.normalize_token_range(self.selected_range());
3603 self.set_selection(range.start, range.end);
3604
3605 if self.active_selection().is_empty() {
3606 self.update_preferred_column();
3607 }
3608 cx.notify()
3609 }
3610
3611 fn select_all_cursors_to(
3614 &mut self,
3615 f: impl Fn(&Self, &CursorSelection) -> usize,
3616 cx: &mut Context<Self>,
3617 ) {
3618 self.select_all_cursors_to_with_affinity(|s, sel| (f(s, sel), false), false, cx);
3619 }
3620
3621 fn select_all_cursors_to_with_affinity(
3624 &mut self,
3625 f: impl Fn(&Self, &CursorSelection) -> (usize, bool),
3626 preserve_column: bool,
3627 cx: &mut Context<Self>,
3628 ) {
3629 self.pause_blink_cursor(cx);
3630 self.undo_manager.break_transaction_coalescing();
3631 M::clear_inline_completion(self, cx);
3632
3633 let mut active_affinity = false;
3634 let new_selections: Vec<CursorSelection> = self
3635 .selections
3636 .iter()
3637 .map(|sel| {
3638 let (offset, affinity) = f(self, sel);
3639 if sel.id == self.active_selection().id {
3640 active_affinity = affinity;
3641 }
3642 let offset = self.cursor_boundary(offset, Bias::Left);
3643 let mut new_sel = *sel;
3644 Self::extend_selection(&mut new_sel, offset, None);
3645 let range = self.normalize_token_range(new_sel.start..new_sel.end);
3646 new_sel.start = range.start;
3647 new_sel.end = range.end;
3648 if !preserve_column {
3649 new_sel.column_anchor =
3650 self.preferred_column_for_with_affinity(new_sel.cursor_offset(), affinity);
3651 } else if new_sel.column_anchor.is_none() {
3652 new_sel.column_anchor = self.preferred_column_for_with_affinity(
3653 sel.cursor_offset(),
3654 self.line_end_affinity_for(sel),
3655 );
3656 }
3657 new_sel
3658 })
3659 .collect();
3660 self.cursor_line_end_affinity = active_affinity;
3661 self.selections.replace_all(new_selections);
3662 self.selections.merge_overlapping();
3663
3664 self.scroll_to(self.cursor(), None, cx);
3665 cx.notify()
3666 }
3667
3668 pub fn unselect(&mut self, _: &mut Window, cx: &mut Context<Self>) {
3670 self.undo_manager.break_transaction_coalescing();
3671 let offset = self.cursor();
3672 self.set_cursor_to(offset);
3673 cx.notify()
3674 }
3675
3676 #[inline]
3677 pub(super) fn offset_from_utf16(&self, offset: usize) -> usize {
3678 self.text.offset_utf16_to_offset(offset)
3679 }
3680
3681 #[inline]
3682 pub(super) fn offset_to_utf16(&self, offset: usize) -> usize {
3683 self.text.offset_to_offset_utf16(offset)
3684 }
3685
3686 #[inline]
3687 pub(crate) fn range_to_utf16(&self, range: &Range<usize>) -> Range<usize> {
3688 self.offset_to_utf16(range.start)..self.offset_to_utf16(range.end)
3689 }
3690
3691 #[inline]
3692 pub(super) fn range_from_utf16(&self, range_utf16: &Range<usize>) -> Range<usize> {
3693 self.offset_from_utf16(range_utf16.start)..self.offset_from_utf16(range_utf16.end)
3694 }
3695
3696 fn clamp_offset_to_visible_backward(&self, offset: usize) -> usize {
3699 let line = self.text.offset_to_point(offset).row;
3700 if self.display_map.is_buffer_line_hidden(line) {
3701 for fold in self.display_map.folded_ranges() {
3702 if line > fold.start_line && line <= fold.end_line {
3703 return self.text.line_end_offset(fold.start_line);
3704 }
3705 }
3706 }
3707 offset
3708 }
3709
3710 fn clamp_offset_to_visible_forward(&self, offset: usize) -> usize {
3713 let line = self.text.offset_to_point(offset).row;
3714 if self.display_map.is_buffer_line_hidden(line) {
3715 for fold in self.display_map.folded_ranges() {
3716 if line > fold.start_line && line <= fold.end_line {
3717 return self.text.line_start_offset(fold.end_line);
3718 }
3719 }
3720 }
3721 offset
3722 }
3723
3724 pub(super) fn cursor_boundary(&self, offset: usize, bias: Bias) -> usize {
3727 let offset = self.token_boundary(self.text.clip_offset(offset, bias), bias);
3728 if offset > 0
3729 && self.text.char_at(offset - 1) == Some('\r')
3730 && self.text.char_at(offset) == Some('\n')
3731 {
3732 if bias == Bias::Left {
3733 offset - 1
3734 } else {
3735 offset + 1
3736 }
3737 } else {
3738 offset
3739 }
3740 }
3741
3742 pub(super) fn previous_boundary(&self, offset: usize) -> usize {
3743 let offset = self.cursor_boundary(offset.saturating_sub(1), Bias::Left);
3744 self.clamp_offset_to_visible_backward(offset)
3745 }
3746
3747 pub(super) fn next_boundary(&self, offset: usize) -> usize {
3748 let offset = self.cursor_boundary(offset.saturating_add(1), Bias::Right);
3749 self.clamp_offset_to_visible_forward(offset)
3750 }
3751
3752 pub(crate) fn show_cursor(&self, window: &Window, cx: &App) -> bool {
3754 (self.focus_handle.is_focused(window) || M::is_context_menu_open(self, cx))
3755 && !self.disabled
3756 && self.blink_cursor.read(cx).visible()
3757 && window.is_window_active()
3758 }
3759
3760 fn on_focus(&mut self, _: &mut Window, cx: &mut Context<Self>) {
3761 self.blink_cursor.update(cx, |cursor, cx| {
3762 cursor.start(cx);
3763 });
3764 cx.emit(InputEvent::Focus);
3765 }
3766
3767 fn on_blur(&mut self, window: &mut Window, cx: &mut Context<Self>) {
3768 if M::is_context_menu_open(self, cx) {
3769 return;
3770 }
3771
3772 M::hide_context_menu(self, cx);
3773 self.undo_manager.break_transaction_coalescing();
3774
3775 M::clear_hover_state(self, cx);
3779 self.diagnostic_popover = None;
3780 M::clear_inline_completion(self, cx);
3781 self.dismiss_touch_selection(cx);
3782 self.blink_cursor.update(cx, |cursor, cx| {
3783 cursor.stop(cx);
3784 });
3785 self.clamp_number_value(window, cx);
3786 cx.emit(InputEvent::Blur);
3787 cx.notify();
3788 }
3789
3790 fn clamp_number_value(&mut self, window: &mut Window, cx: &mut Context<Self>) {
3795 if !self.is_single_line() {
3796 return;
3797 }
3798 if !matches!(self.mask_pattern, MaskPattern::Number { .. }) {
3799 return;
3800 }
3801 if self.number_min.is_none() && self.number_max.is_none() {
3802 return;
3803 }
3804
3805 let Ok(value) = self.unmask_value().parse::<f64>() else {
3806 return;
3807 };
3808
3809 let clamped = match (self.number_min, self.number_max) {
3810 (Some(min), _) if value < min => min,
3811 (_, Some(max)) if value > max => max,
3812 _ => return,
3813 };
3814
3815 let new_text = clamped.to_string();
3818 if !self.is_valid_input(&new_text, cx) {
3819 return;
3820 }
3821
3822 let range = self.range_to_utf16(&(0..self.text.len()));
3823 self.replace_text_in_range_silent(Some(range), &new_text, window, cx);
3824 }
3825
3826 pub(super) fn pause_blink_cursor(&mut self, cx: &mut Context<Self>) {
3827 self.blink_cursor.update(cx, |cursor, cx| {
3828 cursor.pause(cx);
3829 });
3830 }
3831
3832 pub(super) fn on_drag_move(
3833 &mut self,
3834 event: &MouseMoveEvent,
3835 window: &mut Window,
3836 cx: &mut Context<Self>,
3837 ) {
3838 if self.text.len() == 0 {
3839 return;
3840 }
3841
3842 if self.last_layout.is_none() {
3843 return;
3844 }
3845
3846 if !self.focus_handle.is_focused(window) {
3847 return;
3848 }
3849
3850 if !self.selecting {
3851 return;
3852 }
3853
3854 self.auto_scroll.last_drag_position = Some(event.position);
3855 let (offset, line_end_affinity, columns_past_line_end) =
3856 self.resolve_mouse_position(event.position);
3857 if let Some(start) = self.column_select_start {
3858 let end = ColumnarPoint::new(offset, columns_past_line_end);
3859 self.build_columnar_selection(start, end, cx);
3860 } else {
3861 self.select_to_with_affinity(offset, line_end_affinity, cx);
3862 }
3863
3864 if !self.is_single_line() {
3865 let delta = AutoScroll::compute_delta(event.position.y, self.input_bounds);
3866 let scroll_delta = delta.map(|d| -d);
3868 self.auto_scroll.set(scroll_delta, cx, |delta, state, cx| {
3869 let current = state.scroll_handle.offset();
3870 state.update_scroll_offset(Some(point(current.x, current.y + delta)), cx);
3871 if let Some(pos) = state.auto_scroll.last_drag_position {
3872 let (offset, line_end_affinity, columns_past_line_end) =
3873 state.resolve_mouse_position(pos);
3874 if let Some(start) = state.column_select_start {
3875 let end = ColumnarPoint::new(offset, columns_past_line_end);
3876 state.build_columnar_selection(start, end, cx);
3877 } else {
3878 state.select_to_with_affinity(offset, line_end_affinity, cx);
3879 }
3880 }
3881 });
3882 }
3883 }
3884
3885 pub(super) fn normalize_input<'a>(&self, new_text: &'a str) -> Cow<'a, str> {
3891 let normalized = if matches!(self.mask_pattern, MaskPattern::Number { .. }) {
3892 normalize_number_input(new_text)
3893 } else {
3894 Cow::Borrowed(new_text)
3895 };
3896
3897 if self.is_single_line() && normalized.contains(['\n', '\r']) {
3898 Cow::Owned(normalized.replace(['\n', '\r'], ""))
3899 } else {
3900 normalized
3901 }
3902 }
3903
3904 pub(crate) fn is_valid_input(&self, new_text: &str, cx: &mut Context<Self>) -> bool {
3905 if new_text.is_empty() {
3906 return true;
3907 }
3908
3909 if let Some(validate) = &self.validate {
3910 if !validate(new_text, cx) {
3911 return false;
3912 }
3913 }
3914
3915 if !self.mask_pattern.is_valid(new_text) {
3916 return false;
3917 }
3918
3919 let Some(pattern) = &self.pattern else {
3920 return true;
3921 };
3922
3923 pattern.is_match(new_text)
3924 }
3925
3926 pub fn mask_pattern(mut self, pattern: impl Into<MaskPattern>) -> Self {
3936 self.mask_pattern = pattern.into();
3937 self.mask_pattern_set = true;
3938 if let Some(placeholder) = self.mask_pattern.placeholder() {
3939 self.placeholder = placeholder.into();
3940 }
3941 self
3942 }
3943
3944 pub fn set_mask_pattern(
3945 &mut self,
3946 pattern: impl Into<MaskPattern>,
3947 _: &mut Window,
3948 cx: &mut Context<Self>,
3949 ) {
3950 self.mask_pattern = pattern.into();
3951 self.mask_pattern_set = true;
3952 if let Some(placeholder) = self.mask_pattern.placeholder() {
3953 self.placeholder = placeholder.into();
3954 }
3955 cx.notify();
3956 }
3957
3958 pub fn ensure_number_mask(&mut self) {
3960 if self.mask_pattern_set {
3961 return;
3962 }
3963 self.mask_pattern = MaskPattern::Number {
3964 separator: None,
3965 fraction: None,
3966 };
3967 }
3968
3969 pub(super) fn set_input_bounds(&mut self, new_bounds: Bounds<Pixels>, cx: &mut Context<Self>) {
3970 let wrap_width_changed = self.input_bounds.size.width != new_bounds.size.width;
3971 self.input_bounds = new_bounds;
3972
3973 if let Some(last_layout) = self.last_layout.as_ref() {
3975 if wrap_width_changed {
3976 let wrap_width = if !self.soft_wrap {
3977 None
3979 } else {
3980 last_layout.wrap_width
3981 };
3982
3983 self.display_map.on_layout_changed(wrap_width, cx);
3984 if self.is_multi_line() {
3985 self.mode.update_auto_grow(&self.display_map);
3986 }
3987 cx.notify();
3988 }
3989 }
3990 }
3991
3992 pub fn selected_text(&self) -> RopeSlice<'_> {
3997 let range_utf16 = self.range_to_utf16(&self.selected_range());
3998 let range = self.range_from_utf16(&range_utf16);
3999 self.text.slice(range)
4000 }
4001
4002 pub fn range_to_bounds(&self, range: &Range<usize>) -> Option<Bounds<Pixels>> {
4006 let Some(last_layout) = self.last_layout.as_ref() else {
4007 return None;
4008 };
4009
4010 let Some(last_bounds) = self.last_bounds else {
4011 return None;
4012 };
4013
4014 let (_, _, start_pos) = self.line_and_position_for_offset(range.start);
4015 let (_, _, end_pos) = self.line_and_position_for_offset(range.end);
4016
4017 let Some(start_pos) = start_pos else {
4018 return None;
4019 };
4020 let Some(end_pos) = end_pos else {
4021 return None;
4022 };
4023
4024 Some(Bounds::from_corners(
4025 last_bounds.origin + start_pos,
4026 last_bounds.origin + end_pos + point(px(0.), last_layout.line_height),
4027 ))
4028 }
4029
4030 pub(crate) fn replace_text_in_range_silent(
4034 &mut self,
4035 range_utf16: Option<Range<usize>>,
4036 new_text: &str,
4037 window: &mut Window,
4038 cx: &mut Context<Self>,
4039 ) {
4040 self.silent_replace_text = true;
4041 self.replace_text_in_range(range_utf16, new_text, window, cx);
4042 self.silent_replace_text = false;
4043 }
4044
4045 pub(crate) fn replace_text_in_ranges(
4050 &mut self,
4051 edits: &[(Range<usize>, String)],
4052 window: &mut Window,
4053 cx: &mut Context<Self>,
4054 ) {
4055 if !self.is_editable() || edits.is_empty() {
4056 return;
4057 }
4058
4059 M::hide_context_menu(self, cx);
4062 M::clear_inline_completion(self, cx);
4063
4064 let mut sorted: Vec<(Range<usize>, &str)> = edits
4067 .iter()
4068 .map(|(range, text)| (self.normalize_token_range(range.clone()), text.as_str()))
4069 .collect();
4070 sorted.sort_by_key(|edit| std::cmp::Reverse(edit.0.start));
4071 if !self.token_spans().is_empty() {
4072 let mut merged: Vec<(Range<usize>, &str)> = Vec::with_capacity(sorted.len());
4073 for (range, text) in sorted {
4074 if let Some(last) = merged
4075 .last_mut()
4076 .filter(|last| range.end > last.0.start || range == last.0)
4077 {
4078 last.0.start = range.start.min(last.0.start);
4079 last.0.end = range.end.max(last.0.end);
4080 } else {
4081 merged.push((range, text));
4082 }
4083 }
4084 sorted = merged;
4085 }
4086
4087 #[cfg(debug_assertions)]
4088 for pair in sorted.windows(2) {
4089 debug_assert!(
4090 pair[1].0.end <= pair[0].0.start,
4091 "replace_text_in_ranges requires disjoint ranges"
4092 );
4093 }
4094
4095 let requested_intent = self.undo_manager.take_pending_intent();
4099 let selection_before = *self.active_selection();
4100 let original_selections: Vec<CursorSelection> = self.selections.iter().copied().collect();
4101 let selections_before: Vec<CursorSelection> = self
4105 .selections
4106 .iter()
4107 .map(|selection| {
4108 Self::collapse_for_intent(
4109 requested_intent.unwrap_or(EditIntent::Atomic),
4110 *selection,
4111 &(selection.start..selection.end),
4112 )
4113 })
4114 .collect();
4115 let group = sorted.len() > 1;
4116 if group {
4117 self.undo_manager.begin_transaction();
4118 }
4119
4120 let auto_closed_pairs_before = self.mode.auto_closed_pairs().clone();
4121 let mut recorded = false;
4122 let mut highlighter_edits = Vec::new();
4125 for (range, new_text) in &sorted {
4126 let old_text = self.text.clone();
4127 self.mode.adjust_auto_closed_pair(range, new_text.len());
4128 self.text.replace(range.clone(), new_text);
4129
4130 M::adjust_annotations(self, range, new_text.len());
4131 recorded |= self.push_history(
4132 &old_text,
4133 range,
4134 new_text,
4135 requested_intent,
4136 selection_before,
4137 None,
4138 );
4139
4140 self.display_map
4142 .adjust_folds_for_edit(&old_text, range, new_text);
4143 self.display_map
4144 .on_text_changed(&self.text, range, &Rope::from(*new_text), cx);
4145
4146 if group {
4147 if self.is_code_editor() {
4148 let edit =
4149 super::mode::replacement_input_edit(&old_text, &self.text, range, new_text);
4150 highlighter_edits.push((edit, self.text.clone()));
4151 }
4152 } else {
4153 self.mode.update_highlighter(
4154 super::mode::HighlighterUpdate {
4155 selected_range: range,
4156 old_text: &old_text,
4157 new_text: &self.text,
4158 change_text: new_text,
4159 force: true,
4160 },
4161 window,
4162 cx,
4163 );
4164
4165 self.update_fold_candidates_incremental(range, new_text);
4166 }
4167 }
4168
4169 if group {
4170 self.mode
4171 .update_highlighter_batch::<M>(&highlighter_edits, window, cx);
4172
4173 let mut shift = 0isize;
4177 for (range, new_text) in sorted.iter().rev() {
4178 let start = (range.start as isize + shift) as usize;
4179 self.update_fold_candidates_incremental(&(start..start), new_text);
4180 shift += new_text.len() as isize - range.len() as isize;
4181 }
4182
4183 self.undo_manager.commit_transaction();
4184 }
4185
4186 if let Some(diagnostics) = self.mode.diagnostics_mut() {
4188 diagnostics.reset(&self.text)
4189 }
4190 M::refresh_language_features(self, window, cx);
4191 self.update_search(cx);
4192
4193 let mut ascending: Vec<(Range<usize>, &str)> = sorted.clone();
4196 ascending.sort_by_key(|edit| edit.0.start);
4197 let text_len = self.text.len();
4198 let mut delta: isize = 0;
4199 let mut edit_results = Vec::with_capacity(ascending.len());
4200 for (range, new_text) in &ascending {
4201 let offset = ((range.start as isize + delta) as usize + new_text.len()).min(text_len);
4202 edit_results.push((range.clone(), offset));
4203 delta += new_text.len() as isize - (range.end as isize - range.start as isize);
4204 }
4205
4206 let mut used = vec![false; edit_results.len()];
4207 let mut new_selections: Vec<CursorSelection> = Vec::with_capacity(ascending.len());
4208 for selection in original_selections {
4209 if let Ok(index) = edit_results
4210 .binary_search_by_key(&(selection.start, selection.end), |(range, _)| {
4211 (range.start, range.end)
4212 })
4213 && !used[index]
4214 {
4215 let offset = edit_results[index].1;
4216 used[index] = true;
4217 let mut selection = selection;
4218 selection.place_at(offset, None);
4219 new_selections.push(selection);
4220 }
4221 }
4222 for (index, (_, offset)) in edit_results.into_iter().enumerate() {
4223 if !used[index] {
4224 new_selections.push(CursorSelection::new(
4225 self.selections.generate_id(),
4226 offset,
4227 offset,
4228 ));
4229 }
4230 }
4231 self.selections.replace_all(new_selections);
4232 self.selections.merge_overlapping();
4233
4234 let selections_after: Vec<CursorSelection> = self.selections.iter().copied().collect();
4236 if recorded {
4237 self.undo_manager.record_auto_closed_pairs(
4238 auto_closed_pairs_before,
4239 self.mode.auto_closed_pairs().clone(),
4240 );
4241 self.undo_manager
4242 .record_selections(selections_before, selections_after);
4243 }
4244
4245 self.ime_marked_range.take();
4246 self.update_preferred_column();
4247 self.dismiss_touch_selection(cx);
4248 if self.is_multi_line() {
4249 self.mode.update_auto_grow(&self.display_map);
4250 }
4251 if self.emit_events {
4252 cx.emit(InputEvent::Change);
4253 }
4254 cx.notify();
4255 }
4256
4257 fn update_fold_candidates(&mut self) {
4260 if !self.mode.is_folding() {
4261 return;
4262 }
4263
4264 let Some(highlighter_rc) = self.mode.highlighter() else {
4265 return;
4266 };
4267
4268 let highlighter = highlighter_rc.borrow();
4269 let Some(highlighter) = highlighter.as_ref() else {
4270 return;
4271 };
4272
4273 let fold_ranges = highlighter.fold_ranges(&self.text);
4274 self.display_map.set_fold_candidates(fold_ranges);
4275 }
4276
4277 fn update_fold_candidates_incremental(&mut self, edit_range: &Range<usize>, new_text: &str) {
4280 if !self.mode.is_folding() {
4281 return;
4282 }
4283
4284 let Some(highlighter_rc) = self.mode.highlighter() else {
4285 return;
4286 };
4287
4288 let highlighter = highlighter_rc.borrow();
4289 let Some(highlighter) = highlighter.as_ref() else {
4290 return;
4291 };
4292
4293 let new_end = edit_range.start + new_text.len();
4295 self.display_map.update_fold_candidates_for_edit(
4296 |range, text| highlighter.fold_ranges_for_edit(range, text),
4297 edit_range.start..new_end,
4298 &self.text,
4299 );
4300 }
4301}
4302
4303impl<M: InputModeKind> EntityInputHandler for InputBaseState<M> {
4304 fn text_for_range(
4305 &mut self,
4306 range_utf16: Range<usize>,
4307 adjusted_range: &mut Option<Range<usize>>,
4308 _window: &mut Window,
4309 _cx: &mut Context<Self>,
4310 ) -> Option<String> {
4311 let range = self.range_from_utf16(&range_utf16);
4312 adjusted_range.replace(self.range_to_utf16(&range));
4313 Some(self.text.slice(range).to_string())
4314 }
4315
4316 fn selected_text_range(
4317 &mut self,
4318 _ignore_disabled_input: bool,
4319 _window: &mut Window,
4320 _cx: &mut Context<Self>,
4321 ) -> Option<UTF16Selection> {
4322 Some(UTF16Selection {
4323 range: self.range_to_utf16(&self.selected_range()),
4324 reversed: self.active_selection().reversed,
4325 })
4326 }
4327
4328 fn marked_text_range(
4329 &self,
4330 _window: &mut Window,
4331 _cx: &mut Context<Self>,
4332 ) -> Option<Range<usize>> {
4333 self.ime_marked_range
4334 .map(|range| self.range_to_utf16(&range.into()))
4335 }
4336
4337 fn unmark_text(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {
4338 self.ime_marked_range = None;
4339 self.undo_manager.commit_transaction();
4340 }
4341
4342 fn replace_text_in_range(
4347 &mut self,
4348 range_utf16: Option<Range<usize>>,
4349 new_text: &str,
4350 window: &mut Window,
4351 cx: &mut Context<Self>,
4352 ) {
4353 let requested_intent = self.undo_manager.take_pending_intent();
4354 if !self.is_editable() {
4355 return;
4356 }
4357 let selection_before = *self.active_selection();
4358 let ends_composition = self.ime_marked_range.is_some();
4361
4362 self.pause_blink_cursor(cx);
4363
4364 let new_text = if self.replaying_history {
4368 Cow::Borrowed(new_text)
4369 } else {
4370 self.normalize_input(new_text)
4371 };
4372 let new_text: &str = &new_text;
4373
4374 let range = range_utf16
4375 .as_ref()
4376 .map(|range_utf16| self.range_from_utf16(range_utf16))
4377 .or(self.ime_marked_range.map(|range| {
4378 let range = self.range_to_utf16(&(range.start..range.end));
4379 self.range_from_utf16(&range)
4380 }))
4381 .unwrap_or(self.selected_range());
4382 let range = self.normalize_token_range(range);
4383
4384 if range.is_empty()
4390 && range.start == self.cursor()
4391 && self.ime_marked_range.is_none()
4392 && !self.silent_replace_text
4393 && self.is_code_editor()
4394 && self.selections.is_single()
4395 && self.active_selection().is_empty()
4396 {
4397 if let Some(target) = self.skip_over_target(new_text) {
4398 self.set_cursor_to(target);
4399 self.update_preferred_column();
4400 cx.notify();
4401 return;
4402 }
4403 }
4404
4405 if self.is_multi_line() {
4406 let multi_cursor = range_utf16.is_none()
4407 && self.ime_marked_range.is_none()
4408 && !self.selections.is_single();
4409 if multi_cursor {
4410 self.selections.merge_overlapping();
4411 let mut edits: Vec<(Range<usize>, String)> = self
4412 .selections
4413 .iter()
4414 .map(|sel| (sel.start..sel.end, new_text.to_string()))
4415 .collect();
4416 edits.sort_by_key(|(range, _)| range.start);
4417 let intent =
4421 requested_intent.unwrap_or_else(|| self.typing_intent(&edits, new_text));
4422 self.undo_manager.begin_transaction_with(intent);
4423 self.undo_manager.set_pending_intent(intent);
4424 self.replace_text_in_ranges(&edits, window, cx);
4425 self.undo_manager.commit_transaction();
4426 } else {
4427 if range_utf16.is_some() {
4428 self.selections.remove_all_but_active();
4429 }
4430 if let Some(intent) = requested_intent {
4431 self.undo_manager.set_pending_intent(intent);
4432 }
4433 if let Some((open_len, closer)) = self.auto_close_target(&range, new_text) {
4434 let replacement = format!("{new_text}{closer}");
4436 self.replace_text_in_ranges(&[(range.clone(), replacement)], window, cx);
4437 let cursor = range.start + new_text.len();
4438 self.mode.track_auto_closed_pair(
4439 cursor - open_len..cursor,
4440 cursor..cursor + closer.len(),
4441 );
4442 self.undo_manager
4443 .record_auto_closed_pairs_after(self.mode.auto_closed_pairs().clone());
4444 self.set_cursor_to(cursor);
4445 self.update_preferred_column();
4446 self.undo_manager.record_selections(
4447 vec![selection_before],
4448 self.selections.iter().copied().collect(),
4449 );
4450 } else {
4451 self.replace_text_in_ranges(
4452 &[(range.clone(), new_text.to_string())],
4453 window,
4454 cx,
4455 );
4456 }
4457 }
4458 if ends_composition {
4459 self.undo_manager.commit_transaction();
4460 }
4461
4462 if !self.silent_replace_text {
4463 M::on_text_typed(self, &range, new_text, window, cx);
4464 }
4465 return;
4466 }
4467
4468 let old_text = self.text.clone();
4470 self.mode.adjust_auto_closed_pair(&range, new_text.len());
4471 self.text.replace(range.clone(), new_text);
4472
4473 let mut new_offset = (range.start + new_text.len()).min(self.text.len());
4474
4475 let mut mask_changed = false;
4478
4479 if self.is_single_line() {
4480 let pending_text = self.text.to_string();
4481 if !self.replaying_history
4487 && !self.validated_token_edit
4488 && !self.is_valid_input(&pending_text, cx)
4489 && self.is_valid_input(&old_text.to_string(), cx)
4490 {
4491 self.text = old_text;
4492 return;
4493 }
4494
4495 if !self.replaying_history && !self.mask_pattern.is_none() {
4496 let mask_text = self.mask_pattern.mask(&pending_text);
4497 mask_changed = mask_text.as_str() != pending_text;
4498 self.text = Rope::from(mask_text.as_str());
4499 let new_text_len =
4500 (new_text.len() + mask_text.len()).saturating_sub(pending_text.len());
4501 new_offset = (range.start + new_text_len).min(mask_text.len());
4502 }
4503 }
4504
4505 if mask_changed {
4506 M::reset_annotations(self);
4509 } else {
4510 M::adjust_annotations(self, &range, new_text.len());
4511 }
4512 if mask_changed {
4513 self.push_history(
4517 &old_text,
4518 &(0..old_text.len()),
4519 &self.text.to_string(),
4520 Some(EditIntent::Atomic),
4521 selection_before,
4522 Some((new_offset..new_offset).into()),
4523 );
4524 } else {
4525 self.push_history(
4526 &old_text,
4527 &range,
4528 &new_text,
4529 requested_intent,
4530 selection_before,
4531 None,
4532 );
4533 }
4534 if let Some(diagnostics) = self.mode.diagnostics_mut() {
4535 diagnostics.reset(&self.text)
4536 }
4537 self.display_map
4539 .adjust_folds_for_edit(&old_text, &range, new_text);
4540 self.display_map
4541 .on_text_changed(&self.text, &range, &Rope::from(new_text), cx);
4542
4543 self.mode.update_highlighter::<M>(
4544 super::mode::HighlighterUpdate {
4545 selected_range: &range,
4546 old_text: &old_text,
4547 new_text: &self.text,
4548 change_text: &new_text,
4549 force: true,
4550 },
4551 window,
4552 cx,
4553 );
4554
4555 self.update_fold_candidates_incremental(&range, new_text);
4556 M::refresh_language_features(self, window, cx);
4557 self.set_cursor_to(new_offset);
4558 self.ime_marked_range.take();
4559 if ends_composition {
4565 self.undo_manager
4566 .record_selections(vec![selection_before], vec![*self.active_selection()]);
4567 self.undo_manager.commit_transaction();
4568 }
4569 self.update_preferred_column();
4570 self.update_search(cx);
4571 self.dismiss_touch_selection(cx);
4572 if self.is_multi_line() {
4573 self.mode.update_auto_grow(&self.display_map);
4574 }
4575 if !self.silent_replace_text {
4576 M::on_text_typed(self, &range, &new_text, window, cx);
4577 }
4578 if self.emit_events {
4579 cx.emit(InputEvent::Change);
4580 }
4581 cx.notify();
4582 }
4583
4584 fn replace_and_mark_text_in_range(
4586 &mut self,
4587 range_utf16: Option<Range<usize>>,
4588 new_text: &str,
4589 new_selected_range_utf16: Option<Range<usize>>,
4590 window: &mut Window,
4591 cx: &mut Context<Self>,
4592 ) {
4593 let requested_intent = self.undo_manager.take_pending_intent();
4594 if !self.is_editable() {
4595 return;
4596 }
4597 let selection_before = *self.active_selection();
4598
4599 let starts_composition = self.ime_marked_range.is_none();
4600 if starts_composition {
4601 self.undo_manager.break_transaction_coalescing();
4604 self.undo_manager.begin_transaction();
4605 }
4606
4607 self.selections.remove_all_but_active();
4609
4610 M::reset_language_features(self);
4611
4612 let new_text = if self.replaying_history {
4614 Cow::Borrowed(new_text)
4615 } else {
4616 self.normalize_input(new_text)
4617 };
4618 let new_text: &str = &new_text;
4619
4620 let range = range_utf16
4621 .as_ref()
4622 .map(|range_utf16| self.range_from_utf16(range_utf16))
4623 .or(self.ime_marked_range.map(|range| {
4624 let range = self.range_to_utf16(&(range.start..range.end));
4625 self.range_from_utf16(&range)
4626 }))
4627 .unwrap_or(self.selected_range());
4628 let range = self.normalize_token_range(range);
4629
4630 let auto_closed_pairs_before = self.mode.auto_closed_pairs().clone();
4631 let old_text = self.text.clone();
4632 self.mode.adjust_auto_closed_pair(&range, new_text.len());
4633 self.text.replace(range.clone(), new_text);
4634
4635 if self.is_single_line() {
4636 let pending_text = self.text.to_string();
4637 if !self.replaying_history
4639 && !self.validated_token_edit
4640 && !self.is_valid_input(&pending_text, cx)
4641 && self.is_valid_input(&old_text.to_string(), cx)
4642 {
4643 self.text = old_text;
4644 if starts_composition {
4645 self.undo_manager.commit_transaction();
4646 }
4647 return;
4648 }
4649 }
4650
4651 M::adjust_annotations(self, &range, new_text.len());
4652 if let Some(diagnostics) = self.mode.diagnostics_mut() {
4653 diagnostics.reset(&self.text)
4654 }
4655 self.display_map
4657 .adjust_folds_for_edit(&old_text, &range, new_text);
4658 self.display_map
4659 .on_text_changed(&self.text, &range, &Rope::from(new_text), cx);
4660
4661 self.mode.update_highlighter::<M>(
4662 super::mode::HighlighterUpdate {
4663 selected_range: &range,
4664 old_text: &old_text,
4665 new_text: &self.text,
4666 change_text: &new_text,
4667 force: true,
4668 },
4669 window,
4670 cx,
4671 );
4672
4673 self.update_fold_candidates_incremental(&range, new_text);
4674 M::refresh_language_features(self, window, cx);
4675 if new_text.is_empty() {
4676 self.set_cursor_to(range.start);
4678 self.ime_marked_range = None;
4679 } else {
4680 self.ime_marked_range = Some((range.start..range.start + new_text.len()).into());
4681 let new_range = new_selected_range_utf16
4682 .as_ref()
4683 .map(|range_utf16| {
4684 let new_text = Rope::from(new_text);
4685 range.start + new_text.offset_utf16_to_offset(range_utf16.start)
4686 ..range.start + new_text.offset_utf16_to_offset(range_utf16.end)
4687 })
4688 .unwrap_or_else(|| range.start + new_text.len()..range.start + new_text.len());
4689 self.set_selection(new_range.start, new_range.end);
4690 }
4691 if self.is_multi_line() {
4692 self.mode.update_auto_grow(&self.display_map);
4693 }
4694 if self.push_history(
4695 &old_text,
4696 &range,
4697 new_text,
4698 requested_intent,
4699 selection_before,
4700 Some(*self.active_selection()),
4701 ) {
4702 self.undo_manager
4703 .record_selections(vec![selection_before], vec![*self.active_selection()]);
4704 self.undo_manager.record_auto_closed_pairs(
4705 auto_closed_pairs_before,
4706 self.mode.auto_closed_pairs().clone(),
4707 );
4708 }
4709 if new_text.is_empty() {
4710 self.undo_manager.commit_transaction();
4711 }
4712 cx.notify();
4713 }
4714
4715 fn bounds_for_range(
4717 &mut self,
4718 range_utf16: Range<usize>,
4719 bounds: Bounds<Pixels>,
4720 _window: &mut Window,
4721 _cx: &mut Context<Self>,
4722 ) -> Option<Bounds<Pixels>> {
4723 let last_layout = self.last_layout.as_ref()?;
4724 let line_height = last_layout.line_height;
4725 let line_number_width = last_layout.line_number_width;
4726 let range = self.range_from_utf16(&range_utf16);
4727
4728 let mut start_origin = None;
4729 let mut end_origin = None;
4730 let line_number_origin = point(line_number_width, px(0.));
4731 let mut y_offset = last_layout.visible_top;
4732
4733 for (vi, line) in last_layout.lines.iter().enumerate() {
4734 if start_origin.is_some() && end_origin.is_some() {
4735 break;
4736 }
4737
4738 let index_offset = last_layout.visible_line_byte_offsets[vi];
4739
4740 if start_origin.is_none() {
4741 if let Some(p) = line.position_for_index(
4742 range.start.saturating_sub(index_offset),
4743 last_layout,
4744 false,
4745 ) {
4746 start_origin = Some(p + point(px(0.), y_offset));
4747 }
4748 }
4749
4750 if end_origin.is_none() {
4751 if let Some(p) = line.position_for_index(
4752 range.end.saturating_sub(index_offset),
4753 last_layout,
4754 false,
4755 ) {
4756 end_origin = Some(p + point(px(0.), y_offset));
4757 }
4758 }
4759
4760 y_offset += line.size(line_height).height;
4761 }
4762
4763 let start_origin = start_origin.or_else(|| {
4764 let offset = self.last_cursor.or(Some(self.cursor()))?;
4765 let (_, _, origin) = self.line_and_position_for_offset(offset);
4766 origin.map(|origin| origin - line_number_origin)
4767 })?;
4768 let mut end_origin = end_origin.unwrap_or(start_origin);
4769 end_origin.y = start_origin.y;
4771
4772 Some(Bounds::from_corners(
4773 bounds.origin + line_number_origin + start_origin,
4774 bounds.origin + line_number_origin + point(end_origin.x, end_origin.y + line_height),
4776 ))
4777 }
4778
4779 fn character_index_for_point(
4780 &mut self,
4781 point: gpui::Point<Pixels>,
4782 _window: &mut Window,
4783 _cx: &mut Context<Self>,
4784 ) -> Option<usize> {
4785 let last_layout = self.last_layout.as_ref()?;
4786 let line_point = self.last_bounds?.localize(&point)?;
4787
4788 for (vi, line) in last_layout.lines.iter().enumerate() {
4789 let offset = last_layout.visible_line_byte_offsets[vi];
4790 if let Some(utf8_index) = line.index_for_position(line_point, last_layout) {
4791 return Some(self.offset_to_utf16(offset + utf8_index));
4792 }
4793 }
4794
4795 None
4796 }
4797
4798 fn text_input_configuration(
4799 &mut self,
4800 _window: &mut Window,
4801 _cx: &mut Context<Self>,
4802 ) -> TextInputConfiguration {
4803 InputBaseState::text_input_configuration(self)
4804 }
4805}
4806
4807impl<M: InputModeKind> Focusable for InputBaseState<M> {
4808 fn focus_handle(&self, _cx: &App) -> FocusHandle {
4809 self.focus_handle.clone()
4810 }
4811}
4812
4813impl<M: InputModeKind> Render for InputBaseState<M> {
4814 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
4815 self.editor_style = self
4818 .projected_editor_style
4819 .resolved(&crate::Theme::global(cx).tokens);
4820 let entity = cx.entity();
4821 if self._pending_update {
4822 self.mode.update_highlighter::<M>(
4823 super::mode::HighlighterUpdate {
4824 selected_range: &(0..0),
4825 old_text: &self.text,
4826 new_text: &self.text,
4827 change_text: "",
4828 force: false,
4829 },
4830 window,
4831 cx,
4832 );
4833
4834 self.update_fold_candidates();
4835 M::refresh_language_features(self, window, cx);
4836 self._pending_update = false;
4837 }
4838
4839 let element = div()
4840 .id("input-state")
4841 .key_context(CONTEXT)
4842 .track_focus(&self.focus_handle)
4843 .when(self.is_editable(), |this| {
4844 this.on_action(window.listener_for(&entity, InputBaseState::backspace))
4845 .on_action(window.listener_for(&entity, InputBaseState::delete))
4846 .on_action(
4847 window.listener_for(&entity, InputBaseState::delete_to_beginning_of_line),
4848 )
4849 .on_action(window.listener_for(&entity, InputBaseState::delete_to_end_of_line))
4850 .on_action(window.listener_for(&entity, InputBaseState::delete_previous_word))
4851 .on_action(window.listener_for(&entity, InputBaseState::delete_next_word))
4852 .on_action(window.listener_for(&entity, InputBaseState::enter))
4853 .on_action(window.listener_for(&entity, InputBaseState::escape))
4854 .on_action(window.listener_for(&entity, InputBaseState::paste))
4855 .on_action(window.listener_for(&entity, InputBaseState::cut))
4856 .on_action(window.listener_for(&entity, InputBaseState::undo))
4857 .on_action(window.listener_for(&entity, InputBaseState::redo))
4858 .when(self.is_multi_line(), |this| {
4859 this.on_action(window.listener_for(&entity, InputBaseState::indent_inline))
4860 .on_action(window.listener_for(&entity, InputBaseState::outdent_inline))
4861 .on_action(window.listener_for(&entity, InputBaseState::indent_block))
4862 .on_action(window.listener_for(&entity, InputBaseState::outdent_block))
4863 })
4864 })
4865 .on_action(window.listener_for(&entity, InputBaseState::left))
4866 .on_action(window.listener_for(&entity, InputBaseState::right))
4867 .on_action(window.listener_for(&entity, InputBaseState::select_left))
4868 .on_action(window.listener_for(&entity, InputBaseState::select_right))
4869 .when(self.is_multi_line(), |this| {
4870 this.on_action(window.listener_for(&entity, InputBaseState::up))
4871 .on_action(window.listener_for(&entity, InputBaseState::down))
4872 .on_action(window.listener_for(&entity, InputBaseState::select_up))
4873 .on_action(window.listener_for(&entity, InputBaseState::select_down))
4874 .on_action(window.listener_for(&entity, InputBaseState::page_up))
4875 .on_action(window.listener_for(&entity, InputBaseState::page_down))
4876 .on_action(window.listener_for(&entity, InputBaseState::add_cursor_above))
4877 .on_action(window.listener_for(&entity, InputBaseState::add_cursor_below))
4878 })
4879 .on_action(window.listener_for(&entity, InputBaseState::on_action_select_all))
4880 .on_action(window.listener_for(&entity, InputBaseState::select_to_start_of_line))
4881 .on_action(window.listener_for(&entity, InputBaseState::select_to_end_of_line))
4882 .on_action(window.listener_for(&entity, InputBaseState::select_to_previous_word))
4883 .on_action(window.listener_for(&entity, InputBaseState::select_to_next_word))
4884 .on_action(window.listener_for(&entity, InputBaseState::home))
4885 .on_action(window.listener_for(&entity, InputBaseState::end))
4886 .on_action(window.listener_for(&entity, InputBaseState::move_to_start))
4887 .on_action(window.listener_for(&entity, InputBaseState::move_to_end))
4888 .on_action(window.listener_for(&entity, InputBaseState::move_to_previous_word))
4889 .on_action(window.listener_for(&entity, InputBaseState::move_to_next_word))
4890 .on_action(window.listener_for(&entity, InputBaseState::select_to_start))
4891 .on_action(window.listener_for(&entity, InputBaseState::select_to_end))
4892 .on_action(window.listener_for(&entity, InputBaseState::show_character_palette))
4893 .on_action({
4894 let entity = entity.clone();
4895 move |_: &ActivateToken, window, cx| {
4896 let state = entity.read(cx);
4897 let activation = state
4898 .token_spans()
4899 .iter()
4900 .find(|span| span.range() == state.selected_range())
4901 .and_then(|span| {
4902 state.range_to_bounds(&span.range()).and_then(|bounds| {
4903 state.token_activation(
4904 span.range().start,
4905 bounds,
4906 gpui::ClickEvent::Keyboard(gpui::KeyboardClickEvent {
4907 bounds,
4908 ..Default::default()
4909 }),
4910 )
4911 })
4912 });
4913 if let Some((listener, event)) = activation {
4914 listener(&event, window, cx);
4915 }
4916 }
4917 })
4918 .on_action(window.listener_for(&entity, InputBaseState::copy))
4919 .on_action(window.listener_for(&entity, InputBaseState::on_action_search))
4920 .on_action(window.listener_for(&entity, InputBaseState::on_action_replace))
4921 .on_mouse_down(
4922 MouseButton::Left,
4923 window.listener_for(&entity, InputBaseState::on_mouse_down),
4924 )
4925 .on_mouse_down(
4926 MouseButton::Right,
4927 window.listener_for(&entity, InputBaseState::on_mouse_down),
4928 )
4929 .on_mouse_up(
4930 MouseButton::Left,
4931 window.listener_for(&entity, InputBaseState::on_mouse_up),
4932 )
4933 .on_mouse_up(
4934 MouseButton::Right,
4935 window.listener_for(&entity, InputBaseState::on_mouse_up),
4936 )
4937 .on_mouse_move(window.listener_for(&entity, InputBaseState::on_mouse_move))
4938 .on_scroll_wheel(window.listener_for(&entity, InputBaseState::on_scroll_wheel))
4939 .when(self.is_multi_line() && !self.disabled, |this| {
4940 this.on_modifiers_changed(cx.listener(|_, _, _, cx| cx.notify()))
4941 })
4942 .when(!self.disabled, |this| {
4943 if self.is_multi_line() && window.modifiers().alt {
4944 this.cursor_crosshair()
4945 } else {
4946 this.cursor_text()
4947 }
4948 })
4949 .flex_1()
4950 .map(|this| {
4956 if self.is_multi_line() {
4957 this.h_full()
4958 } else {
4959 this.min_h_full().flex().items_center()
4960 }
4961 })
4962 .flex_grow_1()
4963 .overflow_x_hidden()
4964 .when(self.is_multi_line(), |this| {
4965 this.pt(self.editor_paddings.top)
4966 .pr(self.editor_paddings.right)
4967 .pb(self.editor_paddings.bottom)
4968 .pl(self.editor_paddings.left)
4969 })
4970 .child(TextElement::new(entity.clone()).placeholder(self.placeholder.clone()))
4971 .when(self.shows_scrollbar(), |this| {
4972 this.child(EditorScrollbar::new(entity.clone()))
4973 });
4974
4975 M::register_actions(element, &entity, window)
4978 }
4979}
4980
4981#[cfg(test)]
4982mod tests {
4983 use super::*;
4984
4985 #[gpui::test]
4986 fn revealing_a_password_keeps_text_assistance_disabled(cx: &mut TestAppContext) {
4987 cx.update(crate::init);
4988 cx.open_window(size(px(400.), px(100.)), |window, cx| {
4989 let state = cx.new(|cx| crate::input::InputState::new(window, cx).masked(true));
4990 let hidden = state.read(cx).text_input_configuration();
4991 assert!(!hidden.autocorrect && !hidden.suggestions);
4992 assert_eq!(hidden.autocapitalize, Autocapitalize::None);
4993
4994 state.update(cx, |state, cx| state.toggle_masked(window, cx));
4996 assert_eq!(state.read(cx).text_input_configuration(), hidden);
4997 state.update(cx, |state, cx| state.toggle_masked(window, cx));
4998 assert_eq!(state.read(cx).text_input_configuration(), hidden);
4999 gpui::EmptyView
5000 });
5001 }
5002
5003 #[gpui::test]
5004 fn text_input_configuration_follows_the_input(cx: &mut TestAppContext) {
5005 use crate::input::{InputState, TextareaState};
5006
5007 cx.update(crate::init);
5008 let mut states = None;
5009 cx.open_window(size(px(400.), px(100.)), |window, cx| {
5010 states = Some((
5011 cx.new(|cx| InputState::new(window, cx)),
5012 cx.new(|cx| InputState::new(window, cx).masked(true)),
5013 cx.new(|cx| TextareaState::new(window, cx)),
5014 cx.new(|cx| {
5015 InputState::new(window, cx)
5016 .input_action(TextInputAction::Search)
5017 .autocorrect(true)
5018 .autocapitalize(Autocapitalize::Words)
5019 }),
5020 ));
5021 gpui::EmptyView
5022 });
5023 let (plain, masked, textarea, search) = states.unwrap();
5024
5025 cx.read(|cx| {
5026 assert_eq!(
5027 plain.read(cx).text_input_configuration(),
5028 TextInputConfiguration {
5029 input_action: TextInputAction::Done,
5030 ..Default::default()
5031 }
5032 );
5033 assert_eq!(
5034 masked.read(cx).text_input_configuration(),
5035 TextInputConfiguration {
5036 input_action: TextInputAction::Done,
5037 ..Default::default()
5038 }
5039 );
5040 assert_eq!(
5041 textarea.read(cx).text_input_configuration(),
5042 TextInputConfiguration {
5043 input_action: TextInputAction::Enter,
5044 ..Default::default()
5045 }
5046 );
5047 let search = search.read(cx).text_input_configuration();
5048 assert_eq!(search.input_action, TextInputAction::Search);
5049 assert_eq!(search.autocapitalize, Autocapitalize::Words);
5050 assert!(search.autocorrect);
5051 assert!(search.suggestions);
5052 });
5053 }
5054
5055 #[gpui::test]
5056 fn test_multicursor_unicode_completion_uses_post_edit_start(cx: &mut TestAppContext) {
5057 use crate::input::{CompletionProvider, EditorState};
5058 use gpui::Task;
5059 use lsp_types::{CompletionContext, CompletionResponse};
5060 use std::cell::RefCell;
5061
5062 #[derive(Default)]
5063 struct Provider {
5064 starts: RefCell<Vec<usize>>,
5065 queries: RefCell<Vec<(usize, String)>>,
5066 }
5067
5068 impl CompletionProvider for Provider {
5069 fn is_completion_trigger(&self, offset: usize, _: &str, _: &mut App) -> bool {
5070 self.starts.borrow_mut().push(offset);
5071 true
5072 }
5073
5074 fn completions(
5075 &self,
5076 _: &Rope,
5077 offset: usize,
5078 trigger: CompletionContext,
5079 _: &mut Window,
5080 _: &mut App,
5081 ) -> Task<anyhow::Result<CompletionResponse>> {
5082 self.queries
5083 .borrow_mut()
5084 .push((offset, trigger.trigger_character.unwrap()));
5085 Task::ready(Ok(CompletionResponse::Array(vec![])))
5086 }
5087 }
5088
5089 cx.update(crate::init);
5090 let mut editor = None;
5091 let window = cx.open_window(size(px(400.), px(100.)), |window, cx| {
5092 editor = Some(cx.new(|cx| EditorState::new(window, cx).default_value("aa")));
5093 gpui::EmptyView
5094 });
5095 let editor = editor.unwrap();
5096 let provider = Rc::new(Provider::default());
5097 window
5098 .update(cx, |_, window, cx| {
5099 editor.update(cx, |state, cx| {
5100 state.extras.lsp.completion_provider = Some(provider.clone());
5101 state
5102 .extras
5103 .context_menu_content
5104 .completion
5105 .trigger_start_offset = Some(0);
5106 state.extras.context_menu_content.completion.query = "aa".into();
5107 state.set_cursor_to(1);
5108 state.add_cursor_at(0, cx);
5109
5110 state.replace_text_in_range(None, "中", window, cx);
5111
5112 assert_eq!(state.value(), "中a中a");
5113 assert_eq!(provider.starts.borrow().as_slice(), &[4]);
5114 assert_eq!(provider.queries.borrow().as_slice(), &[(7, "中".into())]);
5115 });
5116 })
5117 .unwrap();
5118 }
5119
5120 #[gpui::test]
5121 fn test_inline_token_wrap_and_size_refresh(cx: &mut TestAppContext) {
5122 use crate::input::{InlineToken, InlineTokenPresentation};
5123 let width = Rc::new(Cell::new(4000.));
5124 let render_width = width.clone();
5125 let view = InputView::build_textarea(cx, |state| state.rows(4).default_value("@a@b"));
5126 view.window_handle
5127 .update(cx, |_, window, cx| {
5128 view.input.update(cx, |state, cx| {
5129 state
5130 .replace_range_with_token(0..2, InlineToken::new("a", "@a"), window, cx)
5131 .unwrap();
5132 state
5133 .replace_range_with_token(2..4, InlineToken::new("b", "@b"), window, cx)
5134 .unwrap();
5135 state.set_token_presentation(
5136 InlineTokenPresentation::default()
5137 .token(move |_, _, _| div().w(px(render_width.get()))),
5138 );
5139 });
5140 })
5141 .unwrap();
5142 let mut visual = VisualTestContext::from_window(view.window_handle.into(), cx);
5143 visual.update(|window, cx| window.draw(cx).clear(cx));
5144 view.input.read_with(&visual, |state, _| {
5145 assert_eq!(state.display_map.wrap_row_count(), 2);
5146 let first = state.range_to_bounds(&(0..2)).unwrap();
5147 let second = state.range_to_bounds(&(2..4)).unwrap();
5148 assert!(second.origin.y > first.origin.y);
5149 assert_eq!(
5150 state.last_layout.as_ref().unwrap().lines[0].wrapped_lines[0].len,
5151 2
5152 );
5153 });
5154 width.set(100.);
5155 view.input.update(&mut visual, |_, cx| cx.notify());
5156 visual.update(|window, cx| window.draw(cx).clear(cx));
5157 view.input.read_with(&visual, |state, _| {
5158 assert_eq!(state.display_map.wrap_row_count(), 1)
5159 });
5160 }
5161
5162 #[gpui::test]
5163 fn test_inline_token_geometry_and_reentrant_activation(cx: &mut TestAppContext) {
5164 use crate::input::{InlineToken, InlineTokenPresentation};
5165 let view = InputView::build(cx, |state| state.default_value("before @alice after"));
5166 let target = view.input.clone();
5167 view.window_handle
5168 .update(cx, |_, window, cx| {
5169 view.input.update(cx, |state, cx| {
5170 state
5171 .replace_range_with_token(
5172 7..13,
5173 InlineToken::new("a", "@alice"),
5174 window,
5175 cx,
5176 )
5177 .unwrap();
5178 state.set_token_presentation(
5179 InlineTokenPresentation::default()
5180 .token(|_, _, _| div().w(px(100.)).h(px(20.)))
5181 .on_token_click(move |_, window, cx| {
5182 target.update(cx, |state, cx| {
5183 state.set_value("activated", window, cx)
5184 })
5185 }),
5186 );
5187 state.focus(window, cx);
5188 state.set_selected_range(7..13, cx);
5189 });
5190 })
5191 .unwrap();
5192 let mut visual = VisualTestContext::from_window(view.window_handle.into(), cx);
5193 visual.update(|window, cx| window.draw(cx).clear(cx));
5194 visual.update(|window, cx| {
5195 let state = view.input.read(cx);
5196 let bounds = state.range_to_bounds(&(7..13)).unwrap();
5197 assert!((bounds.size.width - px(100.)).abs() < px(1.), "{bounds:?}");
5198 let (left, _) = state.index_for_mouse_position(bounds.origin + point(px(10.), px(5.)));
5199 let (right, _) = state.index_for_mouse_position(bounds.origin + point(px(90.), px(5.)));
5200 assert_eq!((left, right), (7, 13));
5201 window.dispatch_action(Box::new(ActivateToken), cx);
5202 });
5203 visual.run_until_parked();
5204 view.input.read_with(&visual, |state, _| {
5205 assert_eq!(state.value().as_ref(), "activated")
5206 });
5207 }
5208
5209 #[gpui::test]
5210 fn test_inline_token_click_selects_it(cx: &mut TestAppContext) {
5211 use crate::input::InlineToken;
5212 cx.update(crate::init);
5213 let view = InputView::build(cx, |state| state.default_value("before @alice after"));
5214 view.window_handle
5215 .update(cx, |_, window, cx| {
5216 view.input.update(cx, |state, cx| {
5217 state
5218 .replace_range_with_token(
5219 7..13,
5220 InlineToken::new("a", "@alice"),
5221 window,
5222 cx,
5223 )
5224 .unwrap();
5225 state.set_selected_range(0..0, cx);
5226 });
5227 })
5228 .unwrap();
5229 let mut visual = VisualTestContext::from_window(view.window_handle.into(), cx);
5230 visual.update(|window, cx| window.draw(cx).clear(cx));
5231 let bounds = view
5232 .input
5233 .read_with(&visual, |state, _| state.range_to_bounds(&(7..13)).unwrap());
5234 visual.simulate_click(bounds.center(), gpui::Modifiers::default());
5235 visual.run_until_parked();
5236 view.input.read_with(&visual, |state, _| {
5237 assert_eq!(state.selected_range(), 7..13, "a click selects the token");
5238 });
5239 }
5240
5241 #[gpui::test]
5242 fn test_inline_token_hover_reports_presence_without_selecting(cx: &mut TestAppContext) {
5243 use crate::input::{InlineToken, InlineTokenPresentation};
5244 use gpui::{Bounds, point, px, size};
5245 cx.update(crate::init);
5246 let view = InputView::build(cx, |state| state.default_value("before @alice after"));
5247 let bounds = Bounds::new(point(px(10.), px(20.)), size(px(100.), px(20.)));
5248 view.window_handle
5249 .update(cx, |_, window, cx| {
5250 view.input.update(cx, |state, cx| {
5251 state
5252 .replace_range_with_token(
5253 7..13,
5254 InlineToken::new("a", "@alice"),
5255 window,
5256 cx,
5257 )
5258 .unwrap();
5259 state.set_token_presentation(
5260 InlineTokenPresentation::default().on_token_hover(|_, _, _| {}),
5261 );
5262 state.set_selected_range(0..0, cx);
5263 });
5264 })
5265 .unwrap();
5266 view.window_handle
5267 .update(cx, |_, _, cx| {
5268 view.input.update(cx, |state, _| {
5269 let (_, entered) = state
5270 .token_hover(7, bounds, true, None)
5271 .expect("hover enters an enabled token");
5272 assert!(entered.is_hovered());
5273 assert_eq!(entered.token().id().as_ref(), "a");
5274 assert_eq!(entered.range(), 7..13);
5275 assert_eq!(entered.bounds(), bounds);
5276 let (_, left) = state
5277 .token_hover(7, bounds, false, None)
5278 .expect("hover leaves the same token");
5279 assert!(!left.is_hovered());
5280 assert_eq!(state.selected_range(), 0..0, "hover never selects");
5281 assert!(state.token_hover(99, bounds, true, None).is_none());
5282 });
5283 })
5284 .unwrap();
5285
5286 view.window_handle
5288 .update(cx, |_, _, cx| {
5289 view.input.update(cx, |state, cx| {
5290 state.set_disabled(true, cx);
5291 });
5292 })
5293 .unwrap();
5294 view.window_handle
5295 .update(cx, |_, _, cx| {
5296 view.input.update(cx, |state, _| {
5297 assert!(state.token_hover(7, bounds, true, None).is_none())
5298 })
5299 })
5300 .unwrap();
5301 view.window_handle
5302 .update(cx, |_, _, cx| {
5303 view.input.update(cx, |state, cx| {
5304 state.set_disabled(false, cx);
5305 state.set_readonly(true, cx);
5306 });
5307 })
5308 .unwrap();
5309 view.window_handle
5310 .update(cx, |_, _, cx| {
5311 view.input.update(cx, |state, _| {
5312 assert!(state.token_hover(7, bounds, true, None).is_some())
5313 })
5314 })
5315 .unwrap();
5316
5317 view.window_handle
5319 .update(cx, |_, _, cx| {
5320 view.input.update(cx, |state, _| {
5321 state.set_token_presentation(InlineTokenPresentation::default());
5322 });
5323 })
5324 .unwrap();
5325 view.window_handle
5326 .update(cx, |_, _, cx| {
5327 view.input.update(cx, |state, _| {
5328 assert!(state.token_hover(7, bounds, true, None).is_none())
5329 })
5330 })
5331 .unwrap();
5332 }
5333
5334 #[gpui::test]
5335 fn test_inline_token_hover_exit_delivered_when_token_removed(cx: &mut TestAppContext) {
5336 use crate::input::{InlineToken, InlineTokenPresentation};
5337 use std::cell::RefCell;
5338 use std::rc::Rc;
5339 cx.update(crate::init);
5340 let view = InputView::build(cx, |state| state.default_value("before @alice after"));
5341 let events: Rc<RefCell<Vec<(SharedString, bool)>>> = Rc::new(RefCell::new(Vec::new()));
5342 let sink = events.clone();
5343 view.window_handle
5344 .update(cx, |_, window, cx| {
5345 view.input.update(cx, |state, cx| {
5346 state
5347 .replace_range_with_token(
5348 7..13,
5349 InlineToken::new("a", "@alice"),
5350 window,
5351 cx,
5352 )
5353 .unwrap();
5354 state.set_token_presentation(
5355 InlineTokenPresentation::default()
5356 .token(|_, _, _| div().w(px(100.)).h(px(20.)))
5357 .on_token_hover(move |event, _, _| {
5358 sink.borrow_mut()
5359 .push((event.token().id().clone(), event.is_hovered()));
5360 }),
5361 );
5362 });
5363 })
5364 .unwrap();
5365 let mut visual = VisualTestContext::from_window(view.window_handle.into(), cx);
5366 visual.update(|window, cx| window.draw(cx).clear(cx));
5367 let known_bounds = view.input.read_with(&visual, |state, _| {
5368 state
5369 .token_bounds
5370 .get(&7)
5371 .copied()
5372 .expect("placed token bounds")
5373 });
5374 visual.simulate_mouse_move(known_bounds.center(), None, gpui::Modifiers::default());
5375 visual.run_until_parked();
5376 visual.update(|window, cx| window.draw(cx).clear(cx));
5377 assert_eq!(events.borrow().as_slice(), &[("a".into(), true)]);
5378
5379 view.window_handle
5383 .update(cx, |_, window, cx| {
5384 view.input
5385 .update(cx, |state, cx| state.set_value("no tokens", window, cx));
5386 })
5387 .unwrap();
5388 visual.run_until_parked();
5389 visual.update(|window, cx| window.draw(cx).clear(cx));
5390 assert_eq!(
5391 events.borrow().as_slice(),
5392 &[("a".into(), true), ("a".into(), false)]
5393 );
5394 }
5395
5396 #[gpui::test]
5397 fn test_inline_token_hover_bounds_stay_single_row_at_wrap_boundary(cx: &mut TestAppContext) {
5398 use crate::input::{InlineToken, InlineTokenPresentation};
5399 cx.update(crate::init);
5400 let view = InputView::build_textarea(cx, |state| {
5401 state
5402 .rows(4)
5403 .default_value("aaaa bbbb cccc dddd @alice eeee ffff")
5404 });
5405 view.window_handle
5406 .update(cx, |_, window, cx| {
5407 view.input.update(cx, |state, cx| {
5408 state
5409 .replace_range_with_token(
5410 20..26,
5411 InlineToken::new("a", "@alice"),
5412 window,
5413 cx,
5414 )
5415 .unwrap();
5416 state.set_token_presentation(
5417 InlineTokenPresentation::default()
5418 .token(|_, _, _| div().w(px(60.)).h(px(20.))),
5419 );
5420 });
5421 })
5422 .unwrap();
5423 let mut visual = VisualTestContext::from_window(view.window_handle.into(), cx);
5424 visual.update(|window, cx| window.draw(cx).clear(cx));
5425 let (placed, ranged, line_height) = view.input.read_with(&visual, |state, _| {
5426 (
5427 state
5428 .token_bounds
5429 .get(&20)
5430 .copied()
5431 .expect("placed token bounds"),
5432 state.range_to_bounds(&(20..26)),
5433 state.last_layout.as_ref().unwrap().line_height,
5434 )
5435 });
5436 assert!(placed.size.height <= line_height + px(1.), "{placed:?}");
5440 assert!(
5441 placed.size.width <= px(61.) && placed.size.width > px(0.),
5442 "{placed:?}"
5443 );
5444 if let Some(ranged) = ranged {
5445 assert!(
5446 ranged.size.height >= placed.size.height - px(1.),
5447 "{ranged:?} vs {placed:?}"
5448 );
5449 }
5450 }
5451
5452 #[gpui::test]
5453 fn test_inline_token_hover_same_offset_replacement_exits_predecessor(cx: &mut TestAppContext) {
5454 use crate::input::{InlineToken, InlineTokenPresentation};
5455 use std::{cell::RefCell, rc::Rc};
5456 cx.update(crate::init);
5457 let view = InputView::build(cx, |state| state.default_value("@alice!"));
5458 let events = Rc::new(RefCell::new(Vec::new()));
5459 let sink = events.clone();
5460 view.window_handle
5461 .update(cx, |_, window, cx| {
5462 view.input.update(cx, |state, cx| {
5463 state
5464 .replace_range_with_token(0..6, InlineToken::new("a", "@alice"), window, cx)
5465 .unwrap();
5466 state.set_token_presentation(
5467 InlineTokenPresentation::default()
5468 .token(|_, _, _| div().w(px(100.)).h(px(20.)))
5469 .on_token_hover(move |event, _, _| {
5470 sink.borrow_mut()
5471 .push((event.token().id().clone(), event.is_hovered()));
5472 }),
5473 );
5474 });
5475 })
5476 .unwrap();
5477 let mut visual = VisualTestContext::from_window(view.window_handle.into(), cx);
5478 visual.update(|window, cx| window.draw(cx).clear(cx));
5479 let bounds = view
5480 .input
5481 .read_with(&visual, |state, _| state.token_bounds[&0]);
5482 visual.simulate_mouse_move(bounds.center(), None, gpui::Modifiers::default());
5483 visual.run_until_parked();
5484 visual.update(|window, cx| window.draw(cx).clear(cx));
5485 assert_eq!(events.borrow().as_slice(), &[("a".into(), true)]);
5486
5487 view.window_handle
5488 .update(cx, |_, window, cx| {
5489 view.input.update(cx, |state, cx| {
5490 state
5491 .replace_range_with_token(0..6, InlineToken::new("b", "@alice"), window, cx)
5492 .unwrap();
5493 });
5494 })
5495 .unwrap();
5496 visual.run_until_parked();
5497 visual.update(|window, cx| window.draw(cx).clear(cx));
5498 visual.simulate_mouse_move(
5499 bounds.center() + gpui::point(px(1.), px(0.)),
5500 None,
5501 gpui::Modifiers::default(),
5502 );
5503 visual.run_until_parked();
5504 visual.update(|window, cx| window.draw(cx).clear(cx));
5505 assert_eq!(
5506 events.borrow().as_slice(),
5507 &[("a".into(), true), ("a".into(), false), ("b".into(), true)]
5508 );
5509 view.input
5512 .update(&mut visual, |state, cx| state.set_disabled(true, cx));
5513 visual.update(|window, cx| window.draw(cx).clear(cx));
5514 visual.run_until_parked();
5515 assert_eq!(events.borrow().last(), Some(&("b".into(), false)));
5516 view.input
5517 .update(&mut visual, |state, cx| state.set_disabled(false, cx));
5518 visual.update(|window, cx| window.draw(cx).clear(cx));
5519 visual.run_until_parked();
5520 visual.simulate_mouse_move(bounds.center(), None, gpui::Modifiers::default());
5521 visual.run_until_parked();
5522 assert_eq!(
5523 events.borrow().as_slice(),
5524 &[
5525 ("a".into(), true),
5526 ("a".into(), false),
5527 ("b".into(), true),
5528 ("b".into(), false),
5529 ("b".into(), true)
5530 ]
5531 );
5532 visual.simulate_mouse_move(
5533 bounds.bottom_right() + gpui::point(px(10.), px(10.)),
5534 None,
5535 gpui::Modifiers::default(),
5536 );
5537 visual.run_until_parked();
5538 assert_eq!(
5539 events.borrow().as_slice(),
5540 &[
5541 ("a".into(), true),
5542 ("a".into(), false),
5543 ("b".into(), true),
5544 ("b".into(), false),
5545 ("b".into(), true),
5546 ("b".into(), false)
5547 ]
5548 );
5549 }
5550
5551 #[gpui::test]
5552 fn test_inline_token_hover_older_exit_keeps_newer_snapshot(cx: &mut TestAppContext) {
5553 use crate::input::{InlineToken, InlineTokenPresentation};
5554 use gpui::{Bounds, point, px, size};
5555 cx.update(crate::init);
5556 let view = InputView::build(cx, |state| state.default_value("@a @b"));
5557 let bounds = Bounds::new(point(px(10.), px(20.)), size(px(100.), px(20.)));
5558 view.window_handle
5559 .update(cx, |_, window, cx| {
5560 view.input.update(cx, |state, cx| {
5561 state
5562 .replace_range_with_token(0..2, InlineToken::new("a", "@a"), window, cx)
5563 .unwrap();
5564 state
5565 .replace_range_with_token(3..5, InlineToken::new("b", "@b"), window, cx)
5566 .unwrap();
5567 state.set_token_presentation(
5568 InlineTokenPresentation::default().on_token_hover(|_, _, _| {}),
5569 );
5570 state
5573 .token_hover(0, bounds, true, None)
5574 .expect("hover enters A");
5575 state
5576 .token_hover(3, bounds, true, None)
5577 .expect("hover enters B");
5578 let (_, exit_a) = state.token_hover(0, bounds, false, None).expect("A exits");
5579 assert!(!exit_a.is_hovered());
5580 assert_eq!(
5582 state
5583 .hovered_token
5584 .as_ref()
5585 .expect("B still hovered")
5586 .span
5587 .token()
5588 .id()
5589 .as_ref(),
5590 "b"
5591 );
5592 state.set_value("no tokens", window, cx);
5594 state.token_bounds.clear();
5595 let (listener, exit_b) = state.reconcile_token_hover().expect("B must exit");
5596 assert!(!exit_b.is_hovered());
5597 assert_eq!(exit_b.token().id().as_ref(), "b");
5598 drop(listener);
5599 });
5600 })
5601 .unwrap();
5602 }
5603
5604 #[gpui::test]
5605 fn test_inline_token_hover_late_exit_matches_replaced_token(cx: &mut TestAppContext) {
5606 use crate::input::{InlineToken, InlineTokenPresentation};
5607 cx.update(crate::init);
5608 let view = InputView::build(cx, |state| state.default_value("@a"));
5609 view.window_handle
5610 .update(cx, |_, window, cx| {
5611 view.input.update(cx, |state, cx| {
5612 let a = InlineToken::new("a", "@a");
5613 let b = InlineToken::new("b", "@a");
5614 state
5615 .replace_range_with_token(0..2, a.clone(), window, cx)
5616 .unwrap();
5617 state.set_token_presentation(
5618 InlineTokenPresentation::default().on_token_hover(|_, _, _| {}),
5619 );
5620 let bounds = gpui::Bounds::default();
5621 state.token_hover(0, bounds, true, Some(&a)).unwrap();
5622 state
5623 .replace_range_with_token(0..2, b.clone(), window, cx)
5624 .unwrap();
5625 state.token_hover(0, bounds, true, Some(&b)).unwrap();
5626 let (_, exit_a) = state.token_hover(0, bounds, false, Some(&a)).unwrap();
5627 assert_eq!(exit_a.token().id().as_ref(), "a");
5628 assert_eq!(state.hovered_token.as_ref().unwrap().span.token(), &b);
5629 let (_, exit_b) = state.token_hover(0, bounds, false, Some(&b)).unwrap();
5630 assert_eq!(exit_b.token().id().as_ref(), "b");
5631 assert!(state.pending_token_hover_exits.is_empty());
5632 });
5633 })
5634 .unwrap();
5635 }
5636
5637 #[gpui::test]
5638 fn test_inline_token_hover_exit_keeps_entry_utf16_after_edit(cx: &mut TestAppContext) {
5639 use crate::input::{InlineToken, InlineTokenPresentation};
5640 use gpui::EntityInputHandler as _;
5641 cx.update(crate::init);
5642 let view = InputView::build(cx, |state| state.default_value("🙂 @a"));
5643 view.window_handle
5644 .update(cx, |_, window, cx| {
5645 view.input.update(cx, |state, cx| {
5646 state
5647 .replace_range_with_token(5..7, InlineToken::new("a", "@a"), window, cx)
5648 .unwrap();
5649 state.set_token_presentation(
5650 InlineTokenPresentation::default().on_token_hover(|_, _, _| {}),
5651 );
5652 let bounds = gpui::Bounds::default();
5653 let (_, enter) = state.token_hover(5, bounds, true, None).unwrap();
5654 assert_eq!(enter.range_utf16(), (3, 5));
5655 state.replace_text_in_range(Some(0..2), "abcd", window, cx);
5656 assert_eq!(state.token_spans()[0].range(), 5..7);
5657 let (_, exit) = state.token_hover(5, bounds, false, None).unwrap();
5658 assert_eq!(exit.range_utf16(), (3, 5));
5659 });
5660 })
5661 .unwrap();
5662 }
5663
5664 #[gpui::test]
5665 fn test_inline_token_hover_callback_edit_preserves_both_exits(cx: &mut TestAppContext) {
5666 use crate::input::{InlineToken, InlineTokenPresentation};
5667 cx.update(crate::init);
5668 let view = InputView::build(cx, |state| state.default_value("@a @b"));
5669 view.window_handle
5670 .update(cx, |_, window, cx| {
5671 view.input.update(cx, |state, cx| {
5672 state
5673 .replace_range_with_token(0..2, InlineToken::new("a", "@a"), window, cx)
5674 .unwrap();
5675 state
5676 .replace_range_with_token(3..5, InlineToken::new("b", "@b"), window, cx)
5677 .unwrap();
5678 state.set_token_presentation(
5679 InlineTokenPresentation::default().on_token_hover(|_, _, _| {}),
5680 );
5681 let bounds = gpui::Bounds::default();
5682 state.token_hover(0, bounds, true, None).unwrap();
5683 state.token_hover(3, bounds, true, None).unwrap();
5684 state.set_value("gone", window, cx);
5686 let (_, exit_a) = state.token_hover(0, bounds, false, None).unwrap();
5687 assert_eq!(exit_a.token().id().as_ref(), "a");
5688 let (_, exit_b) = state.reconcile_token_hover().unwrap();
5689 assert_eq!(exit_b.token().id().as_ref(), "b");
5690 assert!(state.token_hover(0, bounds, false, None).is_none());
5691 assert!(state.reconcile_token_hover().is_none());
5692 });
5693 })
5694 .unwrap();
5695 }
5696
5697 #[gpui::test]
5698 fn test_inline_token_edit_history_and_validation(cx: &mut TestAppContext) {
5699 let view = InputView::build(cx, |state| state.default_value("问 @alice!"));
5700 view.window_handle
5701 .update(cx, |_, window, cx| {
5702 view.input.update(cx, |state, cx| {
5703 let token =
5704 crate::input::InlineToken::new("alice-1", "@alice").with_label("Alice");
5705 state
5706 .replace_range_with_token(4..10, token.clone(), window, cx)
5707 .unwrap();
5708 assert_eq!(state.value().as_ref(), "问 @alice!");
5709 assert_eq!(state.tokens()[0].range(), 4..10);
5710 assert_eq!(state.next_end_of_word_at(4), 10);
5711 assert_eq!(state.previous_start_of_word_at(10), 4);
5712 state.undo(&Undo, window, cx);
5713 assert!(
5714 state.tokens().is_empty(),
5715 "identity-only association is undoable"
5716 );
5717 assert_eq!(state.value().as_ref(), "问 @alice!");
5718 state.redo(&Redo, window, cx);
5719 assert_eq!(state.tokens()[0].token(), &token);
5720 state.set_selected_range(6..7, cx);
5721 assert_eq!(state.selected_range(), 4..10);
5722 state.replace("", window, cx);
5723 assert_eq!(state.value().as_ref(), "问 !");
5724 assert!(state.tokens().is_empty());
5725 state.undo(&Undo, window, cx);
5726 assert_eq!(state.tokens()[0].token(), &token);
5727 state.set_selected_range(0..0, cx);
5728 state.replace("🙂", window, cx);
5729 assert_eq!(state.tokens()[0].range(), 8..14);
5730 state.undo(&Undo, window, cx);
5731 assert_eq!(state.tokens()[0].range(), 4..10);
5732 let before = state.content();
5733 state
5734 .replace_range_with_token(0..0, token.clone(), window, cx)
5735 .expect("the same reference may occur twice");
5736 assert_eq!(state.value().as_ref(), "@alice问 @alice!");
5737 assert_eq!(state.tokens().len(), 2);
5738 assert_eq!(state.tokens()[1].token(), &token);
5739 state.undo(&Undo, window, cx);
5740 assert_eq!(state.content(), before);
5741 state.set_value(before.text().clone(), window, cx);
5742 assert!(state.tokens().is_empty());
5743 assert!(!state.undo_manager.has_undos());
5744 });
5745 })
5746 .unwrap();
5747 }
5748
5749 #[gpui::test]
5750 fn test_inline_token_textarea_ime_and_modes(cx: &mut TestAppContext) {
5751 use crate::input::{InlineToken, InlineTokenError, InputContent};
5752 let view = InputView::build_textarea(cx, |state| state);
5753 view.window_handle
5754 .update(cx, |_, window, cx| {
5755 view.input.update(cx, |state, cx| {
5756 let content = InputContent::new("@a@b\n后面")
5757 .with_token(0..2, InlineToken::new("a", "@a"))
5758 .unwrap()
5759 .with_token(2..4, InlineToken::new("b", "@b"))
5760 .unwrap();
5761 state.set_value(content.clone(), window, cx);
5762 state.set_selected_range(2..2, cx);
5763 assert_eq!(state.previous_boundary(2), 0);
5764 assert_eq!(state.next_boundary(2), 4);
5765 state.replace_and_mark_text_in_range(Some(1..2), "中", Some(1..1), window, cx);
5766 assert_eq!(state.value().as_ref(), "中@b\n后面");
5767 assert_eq!(state.tokens().len(), 1);
5768 assert_eq!(
5769 state.replace_with_token(InlineToken::new("x", "x"), window, cx),
5770 Err(InlineTokenError::CompositionActive)
5771 );
5772 state.replace_text_in_range(None, "中文", window, cx);
5773 state.undo(&Undo, window, cx);
5774 assert_eq!(state.content(), content);
5775 state.redo(&Redo, window, cx);
5776 assert_eq!(state.value().as_ref(), "中文@b\n后面");
5777 state.undo(&Undo, window, cx);
5778 state.replace_all("plain", window, cx);
5779 assert!(state.tokens().is_empty());
5780 state.undo(&Undo, window, cx);
5781 assert_eq!(state.content(), content);
5782 });
5783 })
5784 .unwrap();
5785 let view = InputView::build(cx, |state| state);
5786 view.window_handle
5787 .update(cx, |_, window, cx| {
5788 view.input.update(cx, |state, cx| {
5789 assert_eq!(
5790 InputContent::new("a\u{301}")
5791 .with_token(0..1, InlineToken::new("bad", "a"))
5792 .err(),
5793 Some(InlineTokenError::InvalidBoundary)
5794 );
5795 assert_eq!(
5796 InputContent::new("@a@a")
5797 .with_token(0..2, InlineToken::new("a", "@a"))
5798 .unwrap()
5799 .with_token(1..3, InlineToken::new("x", "a@"))
5800 .err(),
5801 Some(InlineTokenError::OverlappingTokens)
5802 );
5803 state
5804 .replace_with_token(InlineToken::new("a", "@a"), window, cx)
5805 .unwrap();
5806 let before = state.content();
5807 state.set_masked(true, window, cx);
5808 assert!(!state.tokens_visible());
5809 assert_eq!(
5810 state.replace_with_token(InlineToken::new("b", "b"), window, cx),
5811 Err(InlineTokenError::UnsupportedMode)
5812 );
5813 assert_eq!(state.content(), before);
5814 state.undo(&Undo, window, cx);
5815 assert!(state.tokens().is_empty());
5816 state.redo(&Redo, window, cx);
5817 assert_eq!(state.content(), before);
5818 assert!(!state.tokens_visible());
5819 });
5820 })
5821 .unwrap();
5822 }
5823
5824 use crate::theme::Theme;
5825 use gpui::{TestAppContext, VisualTestContext, size};
5826
5827 use crate::input::{EditorMode, EditorState, InputMode, LanguageConfig, TextareaMode};
5828
5829 fn set_test_syntax_provider(
5830 provider: Rc<dyn crate::input::SyntaxContextProvider>,
5831 cx: &mut App,
5832 ) {
5833 struct TestLanguages(Rc<dyn crate::input::SyntaxContextProvider>);
5834 impl crate::input::LanguageProvider for TestLanguages {
5835 fn syntax_context_provider(
5836 &self,
5837 _: &str,
5838 ) -> Option<Rc<dyn crate::input::SyntaxContextProvider>> {
5839 Some(self.0.clone())
5840 }
5841 }
5842 crate::input::set_language_provider(Rc::new(TestLanguages(provider)), cx);
5843 }
5844
5845 struct TestRoot<M: InputModeKind>(Entity<InputBaseState<M>>);
5846
5847 impl<M: InputModeKind> Render for TestRoot<M> {
5848 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
5849 div().size_full().child(self.0.clone())
5850 }
5851 }
5852
5853 struct InputView<M: InputModeKind> {
5854 input: Entity<InputBaseState<M>>,
5855 window_handle: gpui::WindowHandle<TestRoot<M>>,
5856 }
5857
5858 impl<M: InputModeKind> InputView<M> {
5860 fn build_with(
5861 cx: &mut TestAppContext,
5862 make: impl FnOnce(&mut Window, &mut Context<InputBaseState<M>>) -> InputBaseState<M>
5863 + 'static,
5864 ) -> Self {
5865 let mut input: Option<Entity<InputBaseState<M>>> = None;
5866
5867 let window = cx.update(|cx| {
5868 cx.open_window(Default::default(), |window, cx| {
5869 cx.set_global(Theme::default());
5871 super::super::init(cx);
5873
5874 input = Some(cx.new(|cx| make(window, cx)));
5875
5876 cx.new(|_| TestRoot(input.clone().unwrap()))
5877 })
5878 .unwrap()
5879 });
5880
5881 Self {
5882 input: input.clone().unwrap(),
5883 window_handle: window,
5884 }
5885 }
5886 }
5887
5888 impl InputView<EditorMode> {
5889 fn new(cx: &mut TestAppContext) -> Self {
5891 Self::build_editor(cx, |state| state)
5892 }
5893
5894 fn build_editor(
5895 cx: &mut TestAppContext,
5896 f: impl FnOnce(InputBaseState<EditorMode>) -> InputBaseState<EditorMode> + 'static,
5897 ) -> Self {
5898 Self::build_with(cx, move |window, cx| {
5899 f(crate::input::EditorState::new(window, cx).language("sql"))
5900 })
5901 }
5902 }
5903
5904 impl InputView<TextareaMode> {
5905 fn build_textarea(
5906 cx: &mut TestAppContext,
5907 f: impl FnOnce(InputBaseState<TextareaMode>) -> InputBaseState<TextareaMode> + 'static,
5908 ) -> Self {
5909 Self::build_with(cx, move |window, cx| {
5910 f(crate::input::TextareaState::new(window, cx))
5911 })
5912 }
5913 }
5914
5915 impl InputView<InputMode> {
5916 fn build(
5918 cx: &mut TestAppContext,
5919 f: impl FnOnce(InputBaseState<InputMode>) -> InputBaseState<InputMode> + 'static,
5920 ) -> Self {
5921 Self::build_with(cx, move |window, cx| {
5922 f(crate::input::InputState::new(window, cx))
5923 })
5924 }
5925 }
5926
5927 fn swipe<M: InputModeKind>(
5929 view: &InputView<M>,
5930 cx: &mut VisualTestContext,
5931 dy: f32,
5932 phase: gpui::TouchPhase,
5933 ) {
5934 cx.update(|window, cx| {
5935 view.input.update(cx, |state, cx| {
5936 state.on_scroll_wheel(
5937 &ScrollWheelEvent {
5938 delta: gpui::ScrollDelta::Pixels(point(px(0.), px(dy))),
5939 touch_phase: phase,
5940 ..Default::default()
5941 },
5942 window,
5943 cx,
5944 )
5945 })
5946 });
5947 }
5948
5949 fn long_text() -> String {
5950 (0..60)
5951 .map(|n| format!("line {n}"))
5952 .collect::<Vec<_>>()
5953 .join("\n")
5954 }
5955
5956 #[gpui::test]
5957 fn test_scroll_bounce_stretches_past_the_end_and_springs_back(cx: &mut TestAppContext) {
5958 use gpui::TouchPhase::{Ended, Moved, Started};
5959
5960 let view = InputView::build_textarea(cx, |state| {
5961 state.scroll_bounce(true).default_value(long_text())
5962 });
5963 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
5964 cx.update(|window, cx| window.draw(cx).clear(cx));
5965 let drawn_at = |cx: &mut VisualTestContext| {
5966 view.input
5967 .read_with(cx, |state, _| state.last_bounds.unwrap().origin.y)
5968 };
5969
5970 swipe(&view, &mut cx, -20., Started);
5971 swipe(&view, &mut cx, -100_000., Moved);
5972 let (end, stretch) = view.input.read_with(&cx, |state, _| {
5973 (state.scroll_handle.offset().y, state.overscroll.offset())
5974 });
5975 assert!(end < px(0.), "the text scrolled to its end");
5976 assert!(stretch < 0., "the rest stretched the edge: {stretch}");
5977 cx.update(|window, cx| window.draw(cx).clear(cx));
5978 let stretched_at = drawn_at(&mut cx);
5979
5980 swipe(&view, &mut cx, 0., Ended);
5981 for _ in 0..120 {
5982 cx.executor()
5983 .advance_clock(std::time::Duration::from_millis(16));
5984 cx.update(|window, cx| window.draw(cx).clear(cx));
5985 }
5986 view.input.read_with(&cx, |state, _| {
5987 assert_eq!(state.overscroll.offset(), 0., "the edge sprang back");
5988 assert_eq!(
5989 state.scroll_handle.offset().y,
5990 end,
5991 "scrolling stayed exact"
5992 );
5993 });
5994 let moved = (stretched_at - drawn_at(&mut cx)).as_f32();
5995 assert!(
5996 (moved - stretch).abs() < 0.001,
5997 "the stretch only moved where the text was drawn: {moved} vs {stretch}"
5998 );
5999 }
6000
6001 fn swipe_past_the_end(
6004 view: &InputView<TextareaMode>,
6005 cx: &mut VisualTestContext,
6006 ) -> (Pixels, f32) {
6007 use gpui::TouchPhase::{Moved, Started};
6008
6009 cx.update(|window, cx| window.draw(cx).clear(cx));
6010 swipe(view, cx, -20., Started);
6011 swipe(view, cx, -100_000., Moved);
6012 view.input.read_with(cx, |state, _| {
6013 (state.scroll_handle.offset().y, state.overscroll.offset())
6014 })
6015 }
6016
6017 #[gpui::test]
6018 fn test_scroll_bounce_stays_put_under_reduced_motion(cx: &mut TestAppContext) {
6019 cx.update(|cx| cx.set_reduce_motion(true));
6020 let view = InputView::build_textarea(cx, |state| {
6021 state.scroll_bounce(true).default_value(long_text())
6022 });
6023 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
6024 let (end, stretch) = swipe_past_the_end(&view, &mut cx);
6025 assert!(end < px(0.), "the text still scrolls");
6026 assert_eq!(stretch, 0., "the edge does not stretch");
6027 }
6028
6029 #[gpui::test]
6030 fn test_scroll_bounce_follows_the_platform_by_default(cx: &mut TestAppContext) {
6031 let view = InputView::build_textarea(cx, |state| state.default_value(long_text()));
6032 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
6033 let (end, stretch) = swipe_past_the_end(&view, &mut cx);
6034 assert!(end < px(0.), "the text scrolls");
6035 assert_eq!(
6036 stretch != 0.,
6037 crate::ScrollBounce::enabled_by_default(),
6038 "the edge stretches where the platform's scroll views do"
6039 );
6040 }
6041
6042 #[gpui::test]
6043 fn test_scroll_bounce_leaves_text_that_fits(cx: &mut TestAppContext) {
6044 let view = InputView::build_textarea(cx, |state| {
6045 state.scroll_bounce(true).default_value("one line")
6046 });
6047 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
6048 let (end, stretch) = swipe_past_the_end(&view, &mut cx);
6049 assert_eq!((end, stretch), (px(0.), 0.));
6050 }
6051
6052 #[gpui::test]
6053 fn test_scroll_bounce_resets_when_replacement_text_fits(cx: &mut TestAppContext) {
6054 let view = InputView::build_textarea(cx, |state| {
6055 state.scroll_bounce(true).default_value(long_text())
6056 });
6057 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
6058 let (_, stretch) = swipe_past_the_end(&view, &mut cx);
6059 assert!(stretch < 0.);
6060 cx.update(|window, cx| {
6061 view.input
6062 .update(cx, |state, cx| state.set_value("short", window, cx));
6063 window.draw(cx).clear(cx);
6064 });
6065 swipe(&view, &mut cx, 0., gpui::TouchPhase::Ended);
6066 view.input.read_with(&cx, |state, _| {
6067 assert_eq!(state.overscroll.offset(), 0.);
6068 assert_eq!(state.scroll_handle.offset().y, px(0.));
6069 });
6070 }
6071
6072 #[gpui::test]
6073 fn test_scroll_bounce_releases_when_the_end_packet_is_horizontal(cx: &mut TestAppContext) {
6074 let view = InputView::build_textarea(cx, |state| {
6075 state.scroll_bounce(true).default_value(long_text())
6076 });
6077 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
6078 let (_, stretch) = swipe_past_the_end(&view, &mut cx);
6079 assert!(stretch < 0.);
6080 cx.update(|window, cx| {
6081 view.input.update(cx, |state, cx| {
6082 state.on_scroll_wheel(
6083 &ScrollWheelEvent {
6084 delta: gpui::ScrollDelta::Pixels(point(px(1.), px(0.))),
6085 touch_phase: gpui::TouchPhase::Ended,
6086 ..Default::default()
6087 },
6088 window,
6089 cx,
6090 );
6091 });
6092 });
6093 for _ in 0..120 {
6094 cx.executor()
6095 .advance_clock(std::time::Duration::from_millis(16));
6096 cx.update(|window, cx| window.draw(cx).clear(cx));
6097 }
6098 assert_eq!(
6099 view.input
6100 .read_with(&cx, |state, _| state.overscroll.offset()),
6101 0.
6102 );
6103 }
6104
6105 #[gpui::test]
6106 fn test_scroll_bounce_ordinary_mouse_wheel_does_not_stretch(cx: &mut TestAppContext) {
6107 let view = InputView::build_textarea(cx, |state| {
6108 state.scroll_bounce(true).default_value(long_text())
6109 });
6110 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
6111 cx.update(|window, cx| {
6112 window.draw(cx).clear(cx);
6113 view.input.update(cx, |state, cx| {
6114 state.on_scroll_wheel(
6115 &ScrollWheelEvent {
6116 delta: gpui::ScrollDelta::Lines(point(0., -100_000.)),
6117 touch_phase: gpui::TouchPhase::Moved,
6118 ..Default::default()
6119 },
6120 window,
6121 cx,
6122 );
6123 assert!(state.scroll_handle.offset().y < px(0.));
6124 assert_eq!(state.overscroll.offset(), 0.);
6125 });
6126 });
6127 }
6128
6129 #[gpui::test]
6130 fn test_scroll_bounce_new_wheel_input_interrupts_the_return(cx: &mut TestAppContext) {
6131 let view = InputView::build_textarea(cx, |state| {
6132 state.scroll_bounce(true).default_value(long_text())
6133 });
6134 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
6135 let (end, stretch) = swipe_past_the_end(&view, &mut cx);
6136 assert!(stretch < 0.);
6137 swipe(&view, &mut cx, 0., gpui::TouchPhase::Ended);
6138 cx.executor()
6139 .advance_clock(std::time::Duration::from_millis(16));
6140 cx.update(|window, cx| {
6141 window.draw(cx).clear(cx);
6142 view.input.update(cx, |state, cx| {
6143 state.on_scroll_wheel(
6144 &ScrollWheelEvent {
6145 delta: gpui::ScrollDelta::Lines(point(0., 2.)),
6146 touch_phase: gpui::TouchPhase::Moved,
6147 ..Default::default()
6148 },
6149 window,
6150 cx,
6151 );
6152 assert!(state.scroll_handle.offset().y > end);
6153 assert_eq!(state.overscroll.offset(), 0.);
6154 });
6155 });
6156 }
6157
6158 #[gpui::test]
6159 fn test_turning_scroll_bounce_off_drops_the_stretch(cx: &mut TestAppContext) {
6160 let view = InputView::build_textarea(cx, |state| {
6161 state.scroll_bounce(true).default_value(long_text())
6162 });
6163 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
6164 let (_, stretch) = swipe_past_the_end(&view, &mut cx);
6165 assert!(stretch < 0., "the edge stretched: {stretch}");
6166 view.input
6167 .update(&mut cx, |state, cx| state.set_scroll_bounce(false, cx));
6168 assert_eq!(
6169 view.input
6170 .read_with(&cx, |state, _| state.overscroll.offset()),
6171 0.
6172 );
6173 }
6174
6175 #[gpui::test]
6178 fn single_line_is_centered_in_a_taller_frame(cx: &mut TestAppContext) {
6179 struct Frame(Entity<InputBaseState<InputMode>>);
6180 impl Render for Frame {
6181 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
6182 crate::input::InputBase::new("frame")
6183 .h(px(60.))
6184 .child(crate::input::Input::new(&self.0))
6185 }
6186 }
6187
6188 cx.update(crate::init);
6189 let mut input = None;
6190 let window = cx.open_window(size(px(400.), px(100.)), |window, cx| {
6191 let state = cx.new(|cx| crate::input::InputState::new(window, cx).default_value("a"));
6192 input = Some(state.clone());
6193 Frame(state)
6194 });
6195 let input = input.unwrap();
6196 let mut cx = VisualTestContext::from_window(window.into(), cx);
6197 cx.update(|window, cx| window.draw(cx).clear(cx));
6198
6199 input.read_with(&cx, |state, _| {
6200 let line = state.input_bounds;
6201 assert_eq!(line.center().y, px(30.), "line {line:?}");
6202 });
6203 }
6204
6205 #[gpui::test]
6206 fn test_noop_scroll_notifies_diagnostic_dismissal(cx: &mut TestAppContext) {
6207 use std::{cell::Cell, rc::Rc};
6208
6209 cx.update(crate::init);
6210 let mut input = None;
6211 let window = cx.open_window(size(px(400.), px(100.)), |window, cx| {
6212 input = Some(cx.new(|cx| crate::input::EditorState::new(window, cx)));
6213 gpui::EmptyView
6214 });
6215 let input = input.unwrap();
6216 input.update(cx, |state, cx| {
6217 state.input_bounds = Bounds::new(Point::default(), size(px(100.), px(20.)));
6218 state.scroll_size = size(px(100.), px(20.));
6219 state.present_diagnostic(crate::input::DiagnosticEntry::default(), cx);
6220 });
6221 let notifications = Rc::new(Cell::new(0));
6222 let count = notifications.clone();
6223 let _subscription =
6224 cx.update(|cx| cx.observe(&input, move |_, _| count.set(count.get() + 1)));
6225
6226 window
6227 .update(cx, |_, window, cx| {
6228 input.update(cx, |state, cx| {
6229 state.on_scroll_wheel(
6230 &ScrollWheelEvent {
6231 delta: gpui::ScrollDelta::Pixels(point(px(0.), px(-40.))),
6232 ..Default::default()
6233 },
6234 window,
6235 cx,
6236 );
6237 assert_eq!(state.scroll_handle.offset(), Point::default());
6238 assert!(state.diagnostic_popover().is_none());
6239 });
6240 })
6241 .unwrap();
6242 assert_eq!(
6243 notifications.get(),
6244 1,
6245 "clearing a diagnostic must notify even when scrolling is clamped"
6246 );
6247 window
6248 .update(cx, |_, window, cx| {
6249 input.update(cx, |state, cx| {
6250 state.on_scroll_wheel(
6251 &ScrollWheelEvent {
6252 delta: gpui::ScrollDelta::Pixels(point(px(0.), px(-40.))),
6253 ..Default::default()
6254 },
6255 window,
6256 cx,
6257 );
6258 });
6259 })
6260 .unwrap();
6261 assert_eq!(notifications.get(), 1, "an unchanged input must stay quiet");
6262 }
6263
6264 #[gpui::test]
6265 fn test_cursor_layout_consumer_updates_after_selection(cx: &mut TestAppContext) {
6266 use std::{cell::Cell, rc::Rc};
6267 struct Panel {
6268 input: Entity<crate::input::EditorState>,
6269 observed: Rc<Cell<Option<(Bounds<Pixels>, Pixels)>>>,
6270 }
6271 impl Render for Panel {
6272 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
6273 self.observed.set(self.input.read(cx).cursor_layout());
6274 div().size_full().child(self.input.clone())
6275 }
6276 }
6277 struct Root(Entity<Panel>);
6278 impl Render for Root {
6279 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
6280 div().size_full().child(
6281 self.0
6282 .clone()
6283 .cached(gpui::StyleRefinement::default().size_full()),
6284 )
6285 }
6286 }
6287 cx.update(crate::init);
6288 let observed = Rc::new(Cell::new(None));
6289 let mut input = None;
6290 let window = cx.open_window(size(px(400.), px(100.)), |window, cx| {
6291 let state = cx.new(|cx| {
6292 let mut state = crate::input::EditorState::new(window, cx);
6293 state.set_value("abcdefgh", window, cx);
6294 state
6295 });
6296 input = Some(state.clone());
6297 Root(cx.new(|_| Panel {
6298 input: state,
6299 observed: observed.clone(),
6300 }))
6301 });
6302 cx.run_until_parked();
6303 let input = input.unwrap();
6304 let before = input.read_with(cx, |state, _| state.cursor_layout());
6305 input.update(cx, |_, cx| cx.notify());
6310 cx.run_until_parked();
6311 assert_eq!(observed.get(), before);
6312 window
6313 .update(cx, |_, _, cx| {
6314 input.update(cx, |state, cx| state.select_to_with_affinity(3, false, cx));
6315 })
6316 .unwrap();
6317 cx.run_until_parked();
6318 window.update(cx, |_, _, cx| cx.notify()).unwrap();
6319 let after = input.read_with(cx, |state, _| state.cursor_layout());
6320 assert_ne!(after, before, "the caret must actually move");
6321 assert_eq!(
6322 observed.get(),
6323 after,
6324 "render consumers must receive the newly painted caret geometry"
6325 );
6326 }
6327
6328 #[gpui::test]
6329 fn test_input_scroll_offset_notifies_only_on_change(cx: &mut TestAppContext) {
6330 use std::{cell::Cell, rc::Rc};
6331
6332 cx.update(crate::init);
6333 let mut input = None;
6334 let _window = cx.open_window(size(px(400.), px(100.)), |window, cx| {
6335 input = Some(cx.new(|cx| crate::input::InputState::new(window, cx)));
6336 gpui::EmptyView
6337 });
6338 let input = input.unwrap();
6339 let notifications = Rc::new(Cell::new(0));
6340 let count = notifications.clone();
6341 let _subscription =
6342 cx.update(|cx| cx.observe(&input, move |_, _| count.set(count.get() + 1)));
6343
6344 input.update(cx, |state, _| {
6345 state.input_bounds = Bounds::new(Point::default(), size(px(100.), px(20.)));
6346 state.scroll_size = size(px(300.), px(20.));
6347 });
6348 for target in [
6349 None,
6350 Some(point(px(0.), px(0.))),
6351 Some(point(px(20.), px(50.))),
6352 ] {
6353 input.update(cx, |state, cx| state.update_scroll_offset(target, cx));
6354 assert_eq!(
6355 notifications.get(),
6356 0,
6357 "an unchanged clamped offset must stay quiet"
6358 );
6359 }
6360 input.update(cx, |state, cx| {
6361 state.update_scroll_offset(Some(point(px(-40.), px(0.))), cx);
6362 assert_eq!(state.scroll_handle.offset(), point(px(-40.), px(0.)));
6363 });
6364 assert_eq!(notifications.get(), 1, "a scroll must notify");
6365 input.update(cx, |state, cx| {
6366 state.update_scroll_offset(Some(point(px(-400.), px(50.))), cx);
6367 assert_eq!(state.scroll_handle.offset(), point(px(-200.), px(0.)));
6368 });
6369 assert_eq!(notifications.get(), 2);
6370 input.update(cx, |state, cx| {
6371 state.update_scroll_offset(Some(point(px(-500.), px(0.))), cx);
6372 });
6373 assert_eq!(
6374 notifications.get(),
6375 2,
6376 "clamping to the current offset must stay quiet"
6377 );
6378 input.update(cx, |state, cx| {
6379 state.scroll_size.width = px(110.);
6380 state.update_scroll_offset(None, cx);
6381 assert_eq!(state.scroll_handle.offset(), point(px(-10.), px(0.)));
6382 });
6383 assert_eq!(
6384 notifications.get(),
6385 3,
6386 "a smaller scroll range must clamp and notify"
6387 );
6388 }
6389
6390 #[gpui::test]
6391 fn test_set_line_number_notifies_only_on_change(cx: &mut TestAppContext) {
6392 use std::{cell::Cell, rc::Rc};
6393
6394 cx.update(crate::init);
6395 let mut input = None;
6396 let window = cx.open_window(size(px(400.), px(100.)), |window, cx| {
6397 input = Some(cx.new(|cx| crate::input::EditorState::new(window, cx)));
6398 gpui::EmptyView
6399 });
6400 let input = input.unwrap();
6401 let shown = input.read_with(cx, |state, _| state.mode.line_number());
6402 let notifications = Rc::new(Cell::new(0));
6403 let count = notifications.clone();
6404 let _subscription =
6405 cx.update(|cx| cx.observe(&input, move |_, _| count.set(count.get() + 1)));
6406
6407 window
6408 .update(cx, |_, window, cx| {
6409 input.update(cx, |state, cx| state.set_line_number(shown, window, cx))
6410 })
6411 .unwrap();
6412 assert_eq!(notifications.get(), 0);
6413
6414 window
6415 .update(cx, |_, window, cx| {
6416 input.update(cx, |state, cx| state.set_line_number(!shown, window, cx))
6417 })
6418 .unwrap();
6419 assert_eq!(notifications.get(), 1);
6420 assert_eq!(
6421 input.read_with(cx, |state, _| state.mode.line_number()),
6422 !shown
6423 );
6424 }
6425
6426 #[gpui::test]
6427 fn test_set_value_on_unfocused_input_stays_quiet(cx: &mut TestAppContext) {
6428 use std::{cell::Cell, rc::Rc};
6429
6430 cx.update(crate::init);
6431 let mut input = None;
6432 let window = cx.open_window(size(px(400.), px(100.)), |window, cx| {
6433 input = Some(cx.new(|cx| crate::input::InputState::new(window, cx)));
6434 gpui::EmptyView
6435 });
6436 let input = input.unwrap();
6437 cx.run_until_parked();
6438
6439 let notifications = Rc::new(Cell::new(0));
6440 let count = notifications.clone();
6441 let _subscription =
6442 cx.update(|cx| cx.observe(&input, move |_, _| count.set(count.get() + 1)));
6443 cx.run_until_parked();
6444
6445 window
6448 .update(cx, |_, window, cx| {
6449 input.update(cx, |state, cx| state.set_value("seeded", window, cx));
6450 })
6451 .unwrap();
6452 cx.run_until_parked();
6453 let settled = notifications.get();
6454
6455 cx.executor()
6456 .advance_clock(std::time::Duration::from_secs(3));
6457 cx.run_until_parked();
6458
6459 assert_eq!(
6460 notifications.get(),
6461 settled,
6462 "an unfocused input is blinking, and every blink repaints the view it is in"
6463 );
6464 }
6465
6466 #[gpui::test]
6467 fn test_input_does_not_invalidate_cached_parent_during_paint(cx: &mut TestAppContext) {
6468 use std::{cell::Cell, rc::Rc};
6469
6470 struct Panel {
6471 input: Entity<crate::input::InputState>,
6472 renders: Rc<Cell<usize>>,
6473 }
6474 impl Render for Panel {
6475 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
6476 self.renders.set(self.renders.get() + 1);
6477 div().size_full().child(self.input.clone())
6478 }
6479 }
6480 struct Root(Entity<Panel>);
6481 impl Render for Root {
6482 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
6483 div().size_full().child(
6484 self.0
6485 .clone()
6486 .cached(gpui::StyleRefinement::default().size_full()),
6487 )
6488 }
6489 }
6490
6491 cx.update(crate::init);
6492 let renders = Rc::new(Cell::new(0));
6493 let mut input = None;
6494 let window = cx.open_window(size(px(400.), px(100.)), |window, cx| {
6495 let state = cx.new(|cx| crate::input::InputState::new(window, cx));
6496 input = Some(state.clone());
6497 Root(cx.new(|_| Panel {
6498 input: state,
6499 renders: renders.clone(),
6500 }))
6501 });
6502 cx.run_until_parked();
6503 let before = renders.get();
6504 assert!(before > 0);
6505 for _ in 0..60 {
6506 window.update(cx, |_, _, cx| cx.notify()).unwrap();
6507 }
6508 assert_eq!(
6509 renders.get(),
6510 before,
6511 "an idle input must not invalidate its cached parent"
6512 );
6513
6514 let input = input.unwrap();
6515 window
6516 .update(cx, |_, window, cx| {
6517 input.update(cx, |state, cx| state.set_value("changed", window, cx));
6518 })
6519 .unwrap();
6520 assert!(
6521 renders.get() > before,
6522 "text changes must invalidate the parent"
6523 );
6524 window.update(cx, |_, _, cx| cx.notify()).unwrap();
6526 let before = renders.get();
6527 for _ in 0..60 {
6528 window.update(cx, |_, _, cx| cx.notify()).unwrap();
6529 }
6530 assert_eq!(renders.get(), before, "the edited input must settle again");
6531
6532 cx.simulate_window_resize(window.into(), size(px(240.), px(100.)));
6533 cx.run_until_parked();
6534 assert!(renders.get() > before, "resizing must invalidate layout");
6535 }
6536
6537 #[gpui::test]
6538 fn textarea_cursor_treats_crlf_as_one_newline(cx: &mut TestAppContext) {
6539 cx.update(crate::init);
6540 let view = InputView::build_textarea(cx, |state| state);
6541 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
6542 cx.update(|window, cx| {
6543 view.input.update(cx, |state, cx| {
6544 for original in ["\r\nlast", "\u{feff}first\r\nlast\n", "first\nlast\r\n"] {
6545 state.set_value(original, window, cx);
6546 let newline = original.find('\n').unwrap();
6547 let end = if original.as_bytes()[newline.saturating_sub(1)] == b'\r' {
6548 newline - 1
6549 } else {
6550 newline
6551 };
6552 state.end(&MoveEnd, window, cx);
6553 assert_eq!(state.cursor(), end);
6554 state.right(&MoveRight, window, cx);
6555 assert_eq!(state.cursor(), newline + 1);
6556 state.left(&MoveLeft, window, cx);
6557 assert_eq!(state.cursor(), end);
6558 state.select_to_end_of_line(&SelectToEndOfLine, window, cx);
6559 assert_eq!(state.selected_range(), end..end);
6560 state.move_to_end(&MoveToEnd, window, cx);
6561 assert_eq!(state.cursor(), original.len());
6562 state.move_to_start(&MoveToStart, window, cx);
6563 assert_eq!(state.cursor(), 0);
6564 assert_eq!(state.value().as_bytes(), original.as_bytes());
6565 }
6566 state.set_value("a\rb", window, cx);
6568 state.right(&MoveRight, window, cx);
6569 assert_eq!(state.cursor(), 1);
6570 state.right(&MoveRight, window, cx);
6571 assert_eq!(state.cursor(), 2);
6572 })
6573 });
6574 }
6575
6576 #[gpui::test]
6577 fn only_a_multi_line_input_paints_scrollbars(cx: &mut TestAppContext) {
6578 cx.update(crate::init);
6579
6580 let single = InputView::build(cx, |state| state);
6584 single
6585 .input
6586 .update(cx, |state, _| assert!(!state.shows_scrollbar()));
6587
6588 let multi = InputView::build_textarea(cx, |state| state);
6589 multi
6590 .input
6591 .update(cx, |state, _| assert!(state.shows_scrollbar()));
6592 }
6593
6594 #[gpui::test]
6595 fn context_menu_handler_is_deferred_and_respects_disabled(cx: &mut TestAppContext) {
6596 use std::{cell::Cell, rc::Rc};
6597 cx.update(crate::init);
6598 let input_view = InputView::new(cx);
6599 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6600 let input = input_view.input;
6601 let calls = Rc::new(Cell::new(0usize));
6602 let items = Rc::new(Cell::new(0usize));
6603 let menu_focus = cx.update(|_, cx| cx.focus_handle());
6604
6605 cx.update(|window, cx| {
6606 input.update(cx, |state, cx| {
6607 let calls2 = calls.clone();
6608 let items2 = items.clone();
6609 let menu_focus = menu_focus.clone();
6610 state.on_context_menu(Rc::new(move |menu, _, _, window, cx| {
6611 calls2.set(calls2.get() + 1);
6612 items2.set(menu.items.len());
6613 menu_focus.focus(window, cx);
6614 }));
6615 state.handle_right_click_menu(point(px(0.), px(0.)), 0, window, cx);
6616 })
6617 });
6618 assert_eq!(calls.get(), 1);
6619 assert_eq!(items.get(), 0);
6620 input.read_with(&cx, |state, _| {
6621 assert_eq!(
6622 state
6623 .selection_focus
6624 .as_ref()
6625 .and_then(WeakFocusHandle::upgrade)
6626 .as_ref(),
6627 Some(&menu_focus),
6628 "the menu callback must retain selection without reborrowing its input"
6629 );
6630 });
6631
6632 cx.update(|window, cx| {
6634 input.update(cx, |state, cx| {
6635 state.handle_right_click_menu(point(px(0.), px(0.)), 0, window, cx);
6636 });
6637 });
6638 assert_eq!(calls.get(), 2);
6639 cx.update(|window, cx| {
6640 assert!(input.read(cx).has_selection_focus(window, cx));
6641 });
6642
6643 cx.update(|window, cx| {
6644 input.update(cx, |state, cx| {
6645 state.disabled = true;
6646 state.handle_right_click_menu(point(px(0.), px(0.)), 0, window, cx);
6647 })
6648 });
6649 assert_eq!(calls.get(), 2);
6650 }
6651
6652 #[gpui::test]
6653 fn context_menu_handler_can_read_its_input(cx: &mut TestAppContext) {
6654 use std::{cell::Cell, rc::Rc};
6655 cx.update(crate::init);
6656 let input_view = InputView::new(cx);
6657 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6658 let input = input_view.input;
6659 let cursor = Rc::new(Cell::new(None));
6660
6661 cx.update(|window, cx| {
6662 input.update(cx, |state, cx| {
6663 state.set_value("hello", window, cx);
6664 let weak = cx.weak_entity();
6665 let cursor = cursor.clone();
6666 state.on_context_menu(Rc::new(move |_, _, _, _, cx| {
6667 cursor.set(weak.upgrade().map(|input| input.read(cx).cursor()));
6668 }));
6669 state.handle_right_click_menu(point(px(0.), px(0.)), 2, window, cx);
6670 })
6671 });
6672 assert_eq!(cursor.get(), Some(2));
6673 }
6674
6675 #[gpui::test]
6676 fn test_readonly_rejects_user_edits_only(cx: &mut TestAppContext) {
6677 let input_view = InputView::new(cx);
6678 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6679 let input = input_view.input;
6680
6681 cx.update(|window, cx| {
6682 input.update(cx, |state, cx| {
6683 state.set_value("hello", window, cx);
6684 state.set_readonly(true, cx);
6685 });
6686 });
6687
6688 cx.update(|_, cx| {
6689 input.read_with(cx, |state, _| {
6690 assert!(!state.is_editable());
6691 assert!(!state.is_replaceable());
6692 });
6693 });
6694
6695 cx.update(|window, cx| {
6697 input.update(cx, |state, cx| {
6698 state.replace_text_in_range(None, " world", window, cx);
6699 state.replace_and_mark_text_in_range(None, "あ", None, window, cx);
6700 });
6701 });
6702 cx.update(|_, cx| {
6703 input.read_with(cx, |state, _| assert_eq!(state.value(), "hello"));
6704 });
6705
6706 cx.update(|window, cx| {
6708 input.update(cx, |state, cx| {
6709 state.insert(" world", window, cx);
6710 state.set_value("changed", window, cx);
6711 });
6712 });
6713 cx.update(|_, cx| {
6714 input.read_with(cx, |state, _| assert_eq!(state.value(), "changed"));
6715 });
6716
6717 cx.update(|window, cx| {
6720 input.update(cx, |state, cx| {
6721 state.set_readonly(false, cx);
6722 state.replace_text_in_range(None, "!", window, cx);
6723 });
6724 });
6725 cx.update(|_, cx| {
6726 input.read_with(cx, |state, _| {
6727 assert!(state.is_editable());
6728 assert_eq!(state.value(), "!changed");
6729 });
6730 });
6731 }
6732
6733 #[gpui::test]
6738 fn test_edit_reveals_far_offscreen_caret(cx: &mut TestAppContext) {
6739 let mut input = None;
6740 let window = cx.open_window(size(px(720.), px(400.)), |window, cx| {
6741 cx.set_global(Theme::default());
6742 super::super::init(cx);
6743 let state = cx.new(|cx| crate::input::TextareaState::new(window, cx).auto_grow(1, 6));
6744 input = Some(state.clone());
6745 TestRoot(state)
6746 });
6747 let mut cx = VisualTestContext::from_window(window.into(), cx);
6748 let input = input.unwrap();
6749
6750 let text: String = (1..=100)
6751 .map(|i| format!("line {i}"))
6752 .collect::<Vec<_>>()
6753 .join("\n");
6754 cx.update(|window, cx| {
6755 input.update(cx, |state, cx| {
6756 state.set_value(text, window, cx);
6757 let end = state.text.len();
6758 state.set_selection(end, end);
6759 });
6760 });
6761 cx.run_until_parked();
6762
6763 cx.update(|_, cx| {
6765 input.update(cx, |state, cx| {
6766 state.scroll_handle.set_offset(point(px(0.), px(0.)));
6767 cx.notify();
6768 });
6769 });
6770 cx.run_until_parked();
6771
6772 cx.update(|window, cx| {
6773 input.update(cx, |state, cx| {
6774 state.replace_text_in_range(None, "X", window, cx);
6775 });
6776 });
6777 cx.run_until_parked();
6778
6779 cx.update(|_, cx| {
6780 input.read_with(cx, |state, _| {
6781 assert!(state.value().ends_with("line 100X"));
6782 let (caret, _) = state.cursor_layout().expect("caret painted");
6783 let viewport = state.input_bounds();
6784 let scroll_y = state.scroll_handle.offset().y;
6785 let top = caret.origin.y - viewport.origin.y + scroll_y;
6786 assert!(
6787 top >= px(0.) && top + caret.size.height <= viewport.size.height,
6788 "caret top {top:?} outside viewport height {:?} (scroll {scroll_y:?})",
6789 viewport.size.height,
6790 );
6791 });
6792 });
6793 }
6794
6795 #[gpui::test]
6799 fn test_set_auto_grow_keeps_rows_fitted(cx: &mut TestAppContext) {
6800 let mut input = None;
6801 let window = cx.open_window(size(px(720.), px(400.)), |window, cx| {
6802 cx.set_global(Theme::default());
6803 super::super::init(cx);
6804 let state = cx.new(|cx| crate::input::TextareaState::new(window, cx).auto_grow(1, 10));
6805 input = Some(state.clone());
6806 TestRoot(state)
6807 });
6808 let mut cx = VisualTestContext::from_window(window.into(), cx);
6809 let input = input.unwrap();
6810
6811 let text: String = (1..=8)
6812 .map(|i| format!("line {i}"))
6813 .collect::<Vec<_>>()
6814 .join("\n");
6815 cx.update(|window, cx| {
6816 input.update(cx, |state, cx| state.set_value(text, window, cx));
6817 });
6818 cx.run_until_parked();
6819 cx.update(|_, cx| input.read_with(cx, |state, _| assert_eq!(state.mode.rows(), 8)));
6820
6821 cx.update(|_, cx| input.update(cx, |state, cx| state.set_auto_grow(1, 12, cx)));
6823 cx.run_until_parked();
6824 cx.update(|_, cx| input.read_with(cx, |state, _| assert_eq!(state.mode.rows(), 8)));
6825
6826 cx.update(|_, cx| input.update(cx, |state, cx| state.set_auto_grow(1, 4, cx)));
6828 cx.run_until_parked();
6829 cx.update(|_, cx| input.read_with(cx, |state, _| assert_eq!(state.mode.rows(), 4)));
6830 }
6831
6832 #[gpui::test]
6838 fn test_scroll_to_eob_does_not_overshoot_safe_range(cx: &mut TestAppContext) {
6839 let input_view = InputView::new(cx);
6840 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6841 let input = input_view.input;
6842
6843 cx.update(|window, cx| {
6845 input.update(cx, |state, cx| {
6846 state.set_scroll_beyond_last_line(Some(1), window, cx);
6847 state.set_cursor_surrounding_lines(Some(1), window, cx);
6848 let text: String = (1..=50)
6849 .map(|i| format!("line {i}"))
6850 .collect::<Vec<_>>()
6851 .join("\n");
6852 state.set_value(text, window, cx);
6853 });
6854 });
6855 cx.run_until_parked();
6856
6857 cx.update(|_, cx| {
6860 input.read_with(cx, |state, _| {
6861 assert!(
6862 state.scroll_size.height > px(0.),
6863 "scroll_size not populated by initial paint"
6864 );
6865 assert!(
6866 state.input_bounds.size.height > px(0.),
6867 "input_bounds not populated by initial paint"
6868 );
6869 });
6870 });
6871
6872 cx.update(|_, cx| {
6877 input.update(cx, |state, cx| {
6878 let end = state.text.len();
6879 state.move_to(end, Some(MoveDirection::Down), cx);
6880
6881 let deferred = state
6882 .deferred_scroll_offset
6883 .expect("scroll_to should populate deferred_scroll_offset");
6884 let safe_y_min =
6885 (-state.scroll_size.height + state.input_bounds.size.height).min(px(0.));
6886
6887 assert!(
6888 deferred.y >= safe_y_min,
6889 "deferred_scroll_offset.y = {:?} below safe_y_min = {:?} \
6890 — paint would jitter (Bug C regression)",
6891 deferred.y,
6892 safe_y_min,
6893 );
6894 });
6895 });
6896 }
6897
6898 #[gpui::test]
6899 fn test_next_search_match_reveals_with_padding_after_manual_scroll(cx: &mut TestAppContext) {
6900 assert_search_reveals_with_padding_after_manual_scroll(false, cx);
6901 }
6902
6903 #[gpui::test]
6904 fn test_previous_search_match_reveals_with_padding_after_manual_scroll(
6905 cx: &mut TestAppContext,
6906 ) {
6907 assert_search_reveals_with_padding_after_manual_scroll(true, cx);
6908 }
6909
6910 fn assert_search_reveals_with_padding_after_manual_scroll(
6911 previous: bool,
6912 cx: &mut TestAppContext,
6913 ) {
6914 let input_view = InputView::new(cx);
6915 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6916 let input = input_view.input;
6917 let text = (0..160)
6918 .map(|row| {
6919 if matches!(row, 20 | 60 | 100) {
6920 format!("match on row {row}")
6921 } else {
6922 format!("line {row}")
6923 }
6924 })
6925 .collect::<Vec<_>>()
6926 .join("\n");
6927 let start = text.match_indices("match").nth(1).unwrap().0;
6928 let expected_match = start..start + "match".len();
6929 cx.update(|window, cx| {
6930 input.update(cx, |state, cx| {
6931 state.set_cursor_surrounding_lines(Some(3), window, cx);
6932 state.set_value(text, window, cx);
6933 state.set_search_query("match", true, cx);
6934 if previous {
6935 state.search_session.matcher.next();
6936 state.search_session.matcher.next();
6937 }
6938 });
6939 });
6940 cx.run_until_parked();
6941 cx.update(|_, cx| {
6942 input.update(cx, |state, cx| {
6943 let line_height = state.last_layout.as_ref().unwrap().line_height;
6944 let y = if previous { px(0.) } else { -line_height * 80. };
6945 state.set_scroll_offset(point(px(0.), y), cx);
6946 });
6947 });
6948 cx.run_until_parked();
6949 input.read_with(&cx, |state, _| {
6950 let visible = state.visible_row_range().unwrap();
6951 if previous {
6952 assert!(visible.end <= 60, "target must be below the viewport");
6953 } else {
6954 assert!(visible.start > 60, "target must be above the viewport");
6955 }
6956 });
6957 cx.update(|_, cx| {
6958 input.update(cx, |state, cx| {
6959 let range = if previous {
6960 state.previous_search_match(cx)
6961 } else {
6962 state.next_search_match(cx)
6963 };
6964 assert_eq!(range, Some(expected_match));
6965 assert_eq!(state.search_session.matcher.label(), "2/3");
6966 });
6967 });
6968 cx.run_until_parked();
6969 input.read_with(&cx, |state, _| {
6970 assert!(state.visible_row_range().unwrap().contains(&60));
6971 let line_height = state.last_layout.as_ref().unwrap().line_height;
6972 let target_y = line_height * 60. + state.scroll_handle.offset().y;
6973 assert!(target_y >= line_height * 2. - px(0.1));
6975 assert!(
6976 target_y + line_height * 3.
6977 <= state.last_bounds.as_ref().unwrap().size.height + px(0.1),
6978 "search must preserve the configured surrounding-line padding"
6979 );
6980 });
6981 }
6982
6983 struct SearchHost {
6985 editor: Entity<InputBaseState<EditorMode>>,
6986 search_requests: Rc<Cell<usize>>,
6987 }
6988
6989 impl Render for SearchHost {
6990 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
6991 let search_requests = self.search_requests.clone();
6992 div()
6993 .size_full()
6994 .on_action(cx.listener(move |_, _: &Search, _, _| {
6995 search_requests.set(search_requests.get() + 1);
6996 }))
6997 .child(self.editor.clone())
6998 }
6999 }
7000
7001 fn press_search_shortcut(
7004 cx: &mut TestAppContext,
7005 searchable: bool,
7006 ) -> (Entity<InputBaseState<EditorMode>>, Rc<Cell<usize>>) {
7007 let search_requests = Rc::new(Cell::new(0));
7008 let mut editor = None;
7009 let window = cx.update(|cx| {
7010 cx.open_window(Default::default(), |window, cx| {
7011 cx.set_global(Theme::default());
7012 super::super::init(cx);
7013 let state =
7014 cx.new(|cx| crate::input::EditorState::new(window, cx).searchable(searchable));
7015 editor = Some(state.clone());
7016 cx.new(|_| SearchHost {
7017 editor: state,
7018 search_requests: search_requests.clone(),
7019 })
7020 })
7021 .unwrap()
7022 });
7023 let editor = editor.unwrap();
7024 let mut cx = VisualTestContext::from_window(window.into(), cx);
7025 cx.update(|window, cx| {
7026 editor.update(cx, |state, cx| state.focus(window, cx));
7027 });
7028 cx.run_until_parked();
7029 #[cfg(target_os = "macos")]
7030 cx.simulate_keystrokes("cmd-f");
7031 #[cfg(not(target_os = "macos"))]
7032 cx.simulate_keystrokes("ctrl-f");
7033 cx.run_until_parked();
7034 (editor, search_requests)
7035 }
7036
7037 #[gpui::test]
7038 fn test_search_shortcut_reaches_the_host_when_not_searchable(cx: &mut TestAppContext) {
7039 let (editor, search_requests) = press_search_shortcut(cx, false);
7040 assert_eq!(search_requests.get(), 1);
7041 editor.read_with(cx, |state, _| {
7042 assert!(!state.search_session().open);
7043 assert!(!state.search_session().is_active());
7044 });
7045 }
7046
7047 #[gpui::test]
7048 fn test_search_shortcut_opens_the_panel_when_searchable(cx: &mut TestAppContext) {
7049 let (editor, search_requests) = press_search_shortcut(cx, true);
7050 assert_eq!(search_requests.get(), 0);
7051 editor.read_with(cx, |state, _| {
7052 assert!(state.search_session().open);
7053 assert!(state.search_session().is_active());
7054 });
7055 }
7056
7057 #[gpui::test]
7058 fn test_set_search_query_highlights_without_the_panel(cx: &mut TestAppContext) {
7059 let input_view = InputView::build_editor(cx, |state| state.searchable(false));
7060 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7061 let input = input_view.input;
7062 cx.update(|window, cx| {
7063 input.update(cx, |state, cx| {
7064 state.set_value("foo bar foo", window, cx);
7065 state.set_search_query("foo", true, cx);
7066 });
7067 });
7068 cx.run_until_parked();
7069 input.read_with(&cx, |state, _| {
7070 let session = state.search_session();
7071 assert!(session.is_active());
7072 assert!(!session.open);
7073 assert_eq!(session.matcher.len(), 2);
7074 });
7075 cx.update(|_, cx| {
7076 input.update(cx, |state, cx| state.close_search(cx));
7077 });
7078 input.read_with(&cx, |state, _| {
7079 assert!(!state.search_session().is_active());
7080 });
7081 }
7082
7083 #[gpui::test]
7086 fn test_closed_search_resyncs_matches_after_edits(cx: &mut TestAppContext) {
7087 let input_view = InputView::build_editor(cx, |state| state.searchable(false));
7088 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7089 let input = input_view.input;
7090 cx.update(|window, cx| {
7091 input.update(cx, |state, cx| {
7092 state.set_value("foo bar foo", window, cx);
7093 state.set_search_query("foo", true, cx);
7094 state.close_search(cx);
7095
7096 state.set_value("bar foo", window, cx);
7097 assert_eq!(state.next_search_match(cx), Some(4..7));
7098
7099 state.set_value("foo foo foo", window, cx);
7100 state.set_search_query("foo", true, cx);
7101 assert_eq!(state.search_session().matcher.len(), 3);
7102 });
7103 });
7104 }
7105
7106 #[gpui::test]
7107 fn test_search_reveals_offscreen_wrapped_match(cx: &mut TestAppContext) {
7108 let input_view = InputView::new(cx);
7109 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7110 let input = input_view.input;
7111 let text = format!(
7112 "match\n{}\n{}match\n{}",
7113 "line\n".repeat(80),
7114 "wrapped text ".repeat(500),
7115 "line\n".repeat(80)
7116 );
7117 cx.update(|window, cx| {
7118 input.update(cx, |state, cx| {
7119 state.set_cursor_surrounding_lines(Some(3), window, cx);
7120 state.set_value(text, window, cx);
7121 state.set_search_query("match", true, cx);
7122 });
7123 });
7124 cx.run_until_parked();
7125 cx.update(|_, cx| {
7126 input.update(cx, |state, cx| {
7127 state.next_search_match(cx).unwrap();
7128 });
7129 });
7130 cx.run_until_parked();
7131 input.read_with(&cx, |state, _| {
7132 let range = state.search_session.matcher.matched_ranges()[1].clone();
7133 let layout = state.last_layout.as_ref().unwrap();
7134 let (_, _, position) = state.line_and_position_for_offset(range.end);
7135 let y = position.expect("wrapped match must be laid out").y
7136 + state.scroll_handle.offset().y;
7137 assert!(y >= layout.line_height * 2. - px(0.1));
7138 assert!(
7139 y + layout.line_height * 3. <= state.last_bounds.unwrap().size.height + px(0.1),
7140 "wrapped match must retain surrounding display rows"
7141 );
7142 });
7143 }
7144
7145 #[gpui::test]
7146 fn test_number_step(cx: &mut TestAppContext) {
7147 let input = InputView::build(cx, |state| state).input;
7148
7149 cx.update(|cx| {
7150 input.update(cx, |_state, cx| {
7151 assert_eq!(
7152 NumberStep::from(5.).value(123., StepAction::Increment, cx),
7153 5.
7154 );
7155
7156 let step = NumberStep::by_value(|value, action, _cx| {
7159 let below = match action {
7160 StepAction::Increment => value < 1.0,
7161 StepAction::Decrement => value <= 1.0,
7162 };
7163 if below { 0.1 } else { 0.5 }
7164 });
7165 assert_eq!(step.value(0.5, StepAction::Increment, cx), 0.1);
7166 assert_eq!(step.value(1.0, StepAction::Increment, cx), 0.5);
7167 assert_eq!(step.value(1.0, StepAction::Decrement, cx), 0.1);
7168 assert_eq!(step.value(2.0, StepAction::Decrement, cx), 0.5);
7169 });
7170 });
7171 }
7172
7173 #[gpui::test]
7174 fn test_number_input_normalization(cx: &mut TestAppContext) {
7175 let input_view = InputView::build(cx, |state| {
7176 state.mask_pattern(MaskPattern::Number {
7177 separator: None,
7178 fraction: None,
7179 })
7180 });
7181 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7182 let input = input_view.input;
7183
7184 cx.update(|window, cx| {
7188 input.update(cx, |state, cx| {
7189 state.replace_text_in_range(None, "12。5", window, cx);
7190 });
7191 });
7192 cx.run_until_parked();
7193 cx.update(|_, cx| {
7194 input.read_with(cx, |state, _| {
7195 assert_eq!(state.value(), "12.5");
7196 let cursor: Range<usize> = state.selected_range();
7197 assert_eq!(cursor, 4..4);
7198 });
7199 });
7200
7201 cx.update(|window, cx| {
7203 input.update(cx, |state, cx| {
7204 state.replace_text_in_range(None, "abc", window, cx);
7205 });
7206 });
7207 cx.run_until_parked();
7208 cx.update(|_, cx| {
7209 input.read_with(cx, |state, _| {
7210 assert_eq!(state.value(), "12.5");
7211 });
7212 });
7213
7214 cx.update(|window, cx| {
7217 input.update(cx, |state, cx| {
7218 let range = state.range_to_utf16(&(0..state.text.len()));
7219 state.replace_text_in_range(Some(range), "。", window, cx);
7220 });
7221 });
7222 cx.run_until_parked();
7223 cx.update(|_, cx| {
7224 input.read_with(cx, |state, _| {
7225 assert_eq!(state.value(), ".");
7226 let cursor: Range<usize> = state.selected_range();
7227 assert_eq!(cursor, 1..1);
7228 });
7229 });
7230 }
7231
7232 #[gpui::test]
7233 fn test_mask_pattern_accepts_non_ascii_text(cx: &mut TestAppContext) {
7234 let input_view = InputView::build(cx, |state| state.mask_pattern("9999年99月"));
7235 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7236 let input = input_view.input;
7237
7238 cx.update(|window, cx| {
7239 input.update(cx, |state, cx| {
7240 state.replace_text_in_range(None, "2024年12月", window, cx);
7241 });
7242 });
7243 cx.run_until_parked();
7244 cx.update(|_, cx| {
7245 input.read_with(cx, |state, _| {
7246 assert_eq!(state.value(), "2024年12月");
7247 });
7248 });
7249 }
7250
7251 #[gpui::test]
7252 fn test_number_input_normalization_with_separator(cx: &mut TestAppContext) {
7253 let input_view = InputView::build(cx, |state| {
7254 state.mask_pattern(MaskPattern::Number {
7255 separator: Some(','),
7256 fraction: Some(2),
7257 })
7258 });
7259 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7260 let input = input_view.input;
7261
7262 cx.update(|window, cx| {
7263 input.update(cx, |state, cx| {
7264 state.replace_text_in_range(None, "1234", window, cx);
7265 });
7266 });
7267 cx.run_until_parked();
7268 cx.update(|_, cx| {
7269 input.read_with(cx, |state, _| {
7270 assert_eq!(state.value(), "1,234");
7271 assert_eq!(state.unmask_value(), "1234");
7272 });
7273 });
7274 }
7275
7276 #[gpui::test]
7277 fn test_number_input_clamp_on_blur(cx: &mut TestAppContext) {
7278 let input_view = InputView::build(cx, |state| {
7279 state
7280 .mask_pattern(MaskPattern::Number {
7281 separator: None,
7282 fraction: None,
7283 })
7284 .min(10.)
7285 .max(100.)
7286 });
7287 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7288 let input = input_view.input;
7289
7290 cx.update(|window, cx| {
7292 input.update(cx, |state, cx| {
7293 state.replace_text_in_range(None, "1000", window, cx);
7294 assert_eq!(state.value(), "1000");
7295 state.clamp_number_value(window, cx);
7296 assert_eq!(state.value(), "100");
7297
7298 let range = state.range_to_utf16(&(0..state.text.len()));
7299 state.replace_text_in_range(Some(range), "1", window, cx);
7300 assert_eq!(state.value(), "1");
7301 state.clamp_number_value(window, cx);
7302 assert_eq!(state.value(), "10");
7303 });
7304 });
7305 }
7306
7307 #[gpui::test]
7308 fn test_number_input_undo_with_mask(cx: &mut TestAppContext) {
7309 let input_view = InputView::build(cx, |state| {
7310 state.mask_pattern(MaskPattern::Number {
7311 separator: Some(','),
7312 fraction: None,
7313 })
7314 });
7315 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7316 let input = input_view.input;
7317
7318 cx.update(|window, cx| {
7321 input.update(cx, |state, cx| {
7322 state.replace_text_in_range(None, "1234", window, cx);
7323 assert_eq!(state.value(), "1,234");
7324 state.replace_text_in_range(None, "5", window, cx);
7325 assert_eq!(state.value(), "12,345");
7326
7327 state.undo(&Undo, window, cx);
7331 assert_eq!(state.value(), "1,234");
7332 state.undo(&Undo, window, cx);
7333 assert_eq!(state.value(), "");
7334 state.redo(&Redo, window, cx);
7335 assert_eq!(state.value(), "1,234");
7336 state.redo(&Redo, window, cx);
7337 assert_eq!(state.value(), "12,345");
7338 });
7339 });
7340 }
7341
7342 #[gpui::test]
7343 fn test_undo_manager_coalesces_adjacent_typing_transactions(cx: &mut TestAppContext) {
7344 let input_view = InputView::build(cx, |state| state);
7345 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7346 let input = input_view.input;
7347
7348 cx.update(|window, cx| {
7349 input.update(cx, |state, cx| {
7350 state.replace_text_in_range(None, "a", window, cx);
7351 state.replace_text_in_range(None, "b", window, cx);
7352 assert_eq!(state.value(), "ab");
7353
7354 state.undo(&Undo, window, cx);
7355 assert_eq!(state.value(), "");
7356 });
7357 });
7358 }
7359
7360 #[gpui::test]
7361 fn test_undo_manager_cursor_movement_splits_typing(cx: &mut TestAppContext) {
7362 let input_view = InputView::build(cx, |state| state);
7363 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7364 let input = input_view.input;
7365
7366 cx.update(|window, cx| {
7367 input.update(cx, |state, cx| {
7368 state.replace_text_in_range(None, "a", window, cx);
7369 state.replace_text_in_range(None, "b", window, cx);
7370 state.left(&MoveLeft, window, cx);
7371 state.replace_text_in_range(None, "x", window, cx);
7372 assert_eq!(state.value(), "axb");
7373
7374 state.undo(&Undo, window, cx);
7375 assert_eq!(state.value(), "ab");
7376 state.undo(&Undo, window, cx);
7377 assert_eq!(state.value(), "");
7378 });
7379 });
7380 }
7381
7382 #[gpui::test]
7383 fn test_undo_manager_splits_backward_and_forward_delete(cx: &mut TestAppContext) {
7384 let input_view = InputView::build(cx, |state| state);
7385 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7386 let input = input_view.input;
7387
7388 cx.update(|window, cx| {
7389 input.update(cx, |state, cx| {
7390 state.set_value("abcd", window, cx);
7391 state.set_selected_range(2..2, cx);
7392 state.backspace(&Backspace, window, cx);
7393 state.delete(&Delete, window, cx);
7394 assert_eq!(state.value(), "ad");
7395
7396 state.undo(&Undo, window, cx);
7397 assert_eq!(state.value(), "acd");
7398 state.undo(&Undo, window, cx);
7399 assert_eq!(state.value(), "abcd");
7400 });
7401 });
7402 }
7403
7404 #[gpui::test]
7405 fn test_undo_manager_coalesces_directional_character_deletes(cx: &mut TestAppContext) {
7406 let input_view = InputView::build(cx, |state| state);
7407 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7408 let input = input_view.input;
7409
7410 cx.update(|window, cx| {
7411 input.update(cx, |state, cx| {
7412 state.set_value("abcd", window, cx);
7413 state.backspace(&Backspace, window, cx);
7414 state.backspace(&Backspace, window, cx);
7415 assert_eq!(state.value(), "ab");
7416 state.undo(&Undo, window, cx);
7417 assert_eq!(state.value(), "abcd");
7418 assert_eq!(state.selected_range(), 4..4);
7419
7420 state.set_value("abcd", window, cx);
7421 state.set_selected_range(1..1, cx);
7422 state.delete(&Delete, window, cx);
7423 state.delete(&Delete, window, cx);
7424 assert_eq!(state.value(), "ad");
7425 state.undo(&Undo, window, cx);
7426 assert_eq!(state.value(), "abcd");
7427 assert_eq!(state.selected_range(), 1..1);
7428 });
7429 });
7430 }
7431
7432 #[gpui::test]
7433 fn test_undo_manager_atomic_paste_isolated_from_typing(cx: &mut TestAppContext) {
7434 let input_view = InputView::build(cx, |state| state);
7435 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7436 let input = input_view.input;
7437
7438 cx.update(|window, cx| {
7439 cx.write_to_clipboard(ClipboardItem::new_string("P".to_string()));
7440 input.update(cx, |state, cx| {
7441 state.replace_text_in_range(None, "a", window, cx);
7442 state.paste(&Paste, window, cx);
7443 state.replace_text_in_range(None, "b", window, cx);
7444 assert_eq!(state.value(), "aPb");
7445
7446 state.undo(&Undo, window, cx);
7447 assert_eq!(state.value(), "aP");
7448 state.undo(&Undo, window, cx);
7449 assert_eq!(state.value(), "a");
7450 state.undo(&Undo, window, cx);
7451 assert_eq!(state.value(), "");
7452 });
7453 });
7454 }
7455
7456 #[gpui::test]
7457 fn test_undo_manager_programmatic_insert_is_atomic(cx: &mut TestAppContext) {
7458 let input_view = InputView::build(cx, |state| state);
7459 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7460 let input = input_view.input;
7461
7462 cx.update(|window, cx| {
7463 input.update(cx, |state, cx| {
7464 state.replace_text_in_range(None, "a", window, cx);
7465 state.insert("P", window, cx);
7466 state.replace_text_in_range(None, "b", window, cx);
7467 assert_eq!(state.value(), "aPb");
7468
7469 state.undo(&Undo, window, cx);
7470 assert_eq!(state.value(), "aP");
7471 state.undo(&Undo, window, cx);
7472 assert_eq!(state.value(), "a");
7473 });
7474 });
7475 }
7476
7477 #[gpui::test]
7478 fn test_undo_manager_selection_round_trip_splits_typing(cx: &mut TestAppContext) {
7479 let input_view = InputView::build(cx, |state| state);
7480 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7481 let input = input_view.input;
7482
7483 cx.update(|window, cx| {
7484 input.update(cx, |state, cx| {
7485 state.replace_text_in_range(None, "a", window, cx);
7486 state.replace_text_in_range(None, "b", window, cx);
7487 state.select_all(window, cx);
7488 state.unselect(window, cx);
7489 state.replace_text_in_range(None, "c", window, cx);
7490
7491 state.undo(&Undo, window, cx);
7492 assert_eq!(state.value(), "ab");
7493 state.undo(&Undo, window, cx);
7494 assert_eq!(state.value(), "");
7495 });
7496 });
7497 }
7498
7499 #[gpui::test]
7500 fn test_undo_manager_enter_is_atomic(cx: &mut TestAppContext) {
7501 let input_view = InputView::build_textarea(cx, |state| state);
7502 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7503 let input = input_view.input;
7504
7505 cx.update(|window, cx| {
7506 input.update(cx, |state, cx| {
7507 state.replace_text_in_range(None, "a", window, cx);
7508 state.enter(
7509 &Enter {
7510 secondary: false,
7511 shift: false,
7512 },
7513 window,
7514 cx,
7515 );
7516 state.replace_text_in_range(None, "b", window, cx);
7517
7518 state.undo(&Undo, window, cx);
7519 assert_eq!(state.value(), "a\n");
7520 state.undo(&Undo, window, cx);
7521 assert_eq!(state.value(), "a");
7522 state.undo(&Undo, window, cx);
7523 assert_eq!(state.value(), "");
7524 });
7525 });
7526 }
7527
7528 #[gpui::test]
7529 fn test_undo_manager_single_line_return_commits_the_typing_session(cx: &mut TestAppContext) {
7530 let input_view = InputView::build(cx, |state| state);
7531 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7532 let input = input_view.input;
7533
7534 cx.update(|window, cx| {
7535 input.update(cx, |state, cx| {
7536 for part in ["a", "b", "c"] {
7537 state.replace_text_in_range(None, part, window, cx);
7538 }
7539 state.enter(
7540 &Enter {
7541 secondary: false,
7542 shift: false,
7543 },
7544 window,
7545 cx,
7546 );
7547 for part in ["d", "e", "f"] {
7548 state.replace_text_in_range(None, part, window, cx);
7549 }
7550 assert_eq!(state.value(), "abcdef");
7551
7552 state.undo(&Undo, window, cx);
7553 assert_eq!(state.value(), "abc");
7554 state.undo(&Undo, window, cx);
7555 assert_eq!(state.value(), "");
7556 });
7557 });
7558 }
7559
7560 #[gpui::test]
7561 fn test_undo_manager_submit_on_enter_commits_the_textarea_session(cx: &mut TestAppContext) {
7562 let input_view = InputView::build_textarea(cx, |state| state.submit_on_enter(true));
7563 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7564 let input = input_view.input;
7565
7566 cx.update(|window, cx| {
7567 input.update(cx, |state, cx| {
7568 state.replace_text_in_range(None, "before submit", window, cx);
7569 state.enter(
7570 &Enter {
7571 secondary: false,
7572 shift: false,
7573 },
7574 window,
7575 cx,
7576 );
7577 state.replace_text_in_range(None, " after submit", window, cx);
7578 assert_eq!(state.value(), "before submit after submit");
7579
7580 state.undo(&Undo, window, cx);
7581 assert_eq!(state.value(), "before submit");
7582 state.undo(&Undo, window, cx);
7583 assert_eq!(state.value(), "");
7584 });
7585 });
7586 }
7587
7588 #[gpui::test]
7589 fn test_undo_manager_blur_commits_the_typing_session(cx: &mut TestAppContext) {
7590 let input_view = InputView::build(cx, |state| state);
7591 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7592 let input = input_view.input;
7593
7594 cx.update(|window, cx| {
7595 input.update(cx, |state, cx| {
7596 state.replace_text_in_range(None, "before blur", window, cx);
7597 state.on_blur(window, cx);
7598 state.on_focus(window, cx);
7599 state.replace_text_in_range(None, " after focus", window, cx);
7600 assert_eq!(state.value(), "before blur after focus");
7601
7602 state.undo(&Undo, window, cx);
7603 assert_eq!(state.value(), "before blur");
7604 state.undo(&Undo, window, cx);
7605 assert_eq!(state.value(), "");
7606 });
7607 });
7608 }
7609
7610 #[gpui::test]
7611 fn test_undo_manager_keeps_rapid_lines_in_distinct_transactions(cx: &mut TestAppContext) {
7612 let input_view = InputView::build_textarea(cx, |state| state);
7613 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7614 let input = input_view.input;
7615
7616 cx.update(|window, cx| {
7617 input.update(cx, |state, cx| {
7618 let enter = Enter {
7619 secondary: false,
7620 shift: false,
7621 };
7622 state.replace_text_in_range(None, "a", window, cx);
7623 state.enter(&enter, window, cx);
7624 state.replace_text_in_range(None, "b", window, cx);
7625 state.enter(&enter, window, cx);
7626 state.replace_text_in_range(None, "c", window, cx);
7627 assert_eq!(state.value(), "a\nb\nc");
7628
7629 for expected in ["a\nb\n", "a\nb", "a\n", "a", ""] {
7630 state.undo(&Undo, window, cx);
7631 assert_eq!(state.value(), expected);
7632 }
7633 });
7634 });
7635 }
7636
7637 #[gpui::test]
7638 fn test_undo_manager_coalesces_long_unicode_typing_without_a_timer(cx: &mut TestAppContext) {
7639 let input_view = InputView::build_textarea(cx, |state| state);
7640 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7641 let input = input_view.input;
7642 let parts = [
7643 "The ",
7644 "quick ",
7645 "brown fox, ",
7646 "你好,世界 ",
7647 "🦀 jumps over 13 lazy dogs.",
7648 ];
7649 let expected = parts.concat();
7650
7651 cx.update(|window, cx| {
7652 input.update(cx, |state, cx| {
7653 for part in parts {
7654 state.replace_text_in_range(None, part, window, cx);
7655 }
7656 assert_eq!(state.value(), expected);
7657
7658 state.undo(&Undo, window, cx);
7659 assert_eq!(state.value(), "");
7660 state.redo(&Redo, window, cx);
7661 assert_eq!(state.value(), expected);
7662 });
7663 });
7664 }
7665
7666 #[gpui::test]
7667 fn test_undo_manager_long_multiline_sequence_has_structural_boundaries(
7668 cx: &mut TestAppContext,
7669 ) {
7670 let input_view = InputView::build_textarea(cx, |state| state);
7671 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7672 let input = input_view.input;
7673 let enter = Enter {
7674 secondary: false,
7675 shift: false,
7676 };
7677
7678 cx.update(|window, cx| {
7679 input.update(cx, |state, cx| {
7680 for (index, line) in [
7681 "first line with punctuation!",
7682 "第二行包含 Unicode 🦀",
7683 "third line has several words",
7684 ]
7685 .into_iter()
7686 .enumerate()
7687 {
7688 for chunk in line.split_inclusive(' ') {
7689 state.replace_text_in_range(None, chunk, window, cx);
7690 }
7691 if index < 2 {
7692 state.enter(&enter, window, cx);
7693 }
7694 }
7695
7696 assert_eq!(
7697 state.value(),
7698 "first line with punctuation!\n第二行包含 Unicode 🦀\nthird line has several words"
7699 );
7700 state.undo(&Undo, window, cx);
7701 assert_eq!(
7702 state.value(),
7703 "first line with punctuation!\n第二行包含 Unicode 🦀\n"
7704 );
7705 state.undo(&Undo, window, cx);
7706 assert_eq!(
7707 state.value(),
7708 "first line with punctuation!\n第二行包含 Unicode 🦀"
7709 );
7710 state.undo(&Undo, window, cx);
7711 assert_eq!(state.value(), "first line with punctuation!\n");
7712 });
7713 });
7714 }
7715
7716 #[gpui::test]
7717 fn test_masked_input_keeps_its_value_out_of_the_clipboard(cx: &mut TestAppContext) {
7718 let input_view = InputView::build(cx, |state| state);
7719 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7720 let input = input_view.input;
7721
7722 cx.update(|window, cx| {
7723 input.update(cx, |state, cx| {
7724 state.set_value("hunter2", window, cx);
7725 state.set_masked(true, window, cx);
7726 state.select_all(window, cx);
7727 cx.write_to_clipboard(ClipboardItem::new_string("sentinel".into()));
7728
7729 state.copy(&Copy, window, cx);
7730 assert_eq!(
7731 cx.read_from_clipboard().and_then(|item| item.text()),
7732 Some("sentinel".to_string())
7733 );
7734
7735 state.cut(&Cut, window, cx);
7737 assert_eq!(state.value(), "hunter2");
7738 assert_eq!(
7739 cx.read_from_clipboard().and_then(|item| item.text()),
7740 Some("sentinel".to_string())
7741 );
7742
7743 state.set_masked(false, window, cx);
7745 state.copy(&Copy, window, cx);
7746 assert_eq!(
7747 cx.read_from_clipboard().and_then(|item| item.text()),
7748 Some("hunter2".to_string())
7749 );
7750 });
7751 });
7752 }
7753
7754 #[gpui::test]
7755 fn test_masked_input_collapses_word_boundaries(cx: &mut TestAppContext) {
7756 let input_view = InputView::build(cx, |state| state);
7757 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7758 let input = input_view.input;
7759
7760 cx.update(|window, cx| {
7761 input.update(cx, |state, cx| {
7762 state.set_value("aaa bbb ccc", window, cx);
7763 state.set_masked(true, window, cx);
7764 state.set_selected_range(7..7, cx);
7765
7766 state.delete_previous_word(&DeleteToPreviousWordStart, window, cx);
7769 assert_eq!(state.value(), " ccc");
7770 assert_eq!(state.selected_range(), 0..0);
7771
7772 state.delete_next_word(&DeleteToNextWordEnd, window, cx);
7773 assert_eq!(state.value(), "");
7774
7775 state.set_value("aaa bbb ccc", window, cx);
7777 state.select_word(9, window, cx);
7778 assert_eq!(state.selected_range(), 0..11);
7779
7780 state.set_masked(false, window, cx);
7782 state.set_value("aaa bbb ccc", window, cx);
7783 state.set_selected_range(11..11, cx);
7784 state.delete_previous_word(&DeleteToPreviousWordStart, window, cx);
7785 assert_eq!(state.value(), "aaa bbb ");
7786
7787 state.set_value("aaa bbb ccc", window, cx);
7788 state.select_word(9, window, cx);
7789 assert_eq!(state.selected_range(), 8..11);
7790 });
7791 });
7792 }
7793
7794 #[gpui::test]
7795 fn test_masked_input_disables_the_copy_context_menu_items(cx: &mut TestAppContext) {
7796 let input_view = InputView::build(cx, |state| state);
7797 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7798 let input = input_view.input;
7799
7800 cx.update(|window, cx| {
7801 input.update(cx, |state, cx| {
7802 state.set_value("hunter2", window, cx);
7803 state.select_all(window, cx);
7804 assert!(state.context_menu_capabilities().is_copyable());
7805
7806 state.set_masked(true, window, cx);
7807 let capabilities = state.context_menu_capabilities();
7808 assert!(capabilities.is_masked());
7809 assert!(capabilities.has_selection());
7810 assert!(!capabilities.is_copyable());
7811 });
7812 });
7813 }
7814
7815 #[gpui::test]
7816 fn test_undo_manager_cut_and_repeated_pastes_are_distinct_transactions(
7817 cx: &mut TestAppContext,
7818 ) {
7819 let input_view = InputView::build(cx, |state| state);
7820 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7821 let input = input_view.input;
7822
7823 cx.update(|window, cx| {
7824 input.update(cx, |state, cx| {
7825 state.replace_text_in_range(None, "alpha beta gamma", window, cx);
7826 state.set_selected_range(6..10, cx);
7827 state.cut(&Cut, window, cx);
7828 assert_eq!(state.value(), "alpha gamma");
7829
7830 state.paste(&Paste, window, cx);
7831 state.paste(&Paste, window, cx);
7832 assert_eq!(state.value(), "alpha betabeta gamma");
7833
7834 state.undo(&Undo, window, cx);
7835 assert_eq!(state.value(), "alpha beta gamma");
7836 state.undo(&Undo, window, cx);
7837 assert_eq!(state.value(), "alpha gamma");
7838 state.undo(&Undo, window, cx);
7839 assert_eq!(state.value(), "alpha beta gamma");
7840 });
7841 });
7842 }
7843
7844 #[gpui::test]
7845 fn test_undo_manager_word_and_line_deletes_do_not_coalesce(cx: &mut TestAppContext) {
7846 let input_view = InputView::build_textarea(cx, |state| state);
7847 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7848 let input = input_view.input;
7849
7850 cx.update(|window, cx| {
7851 input.update(cx, |state, cx| {
7852 state.set_value("one two three\nfour five", window, cx);
7853 state.set_selected_range(13..13, cx);
7854 state.delete_previous_word(&DeleteToPreviousWordStart, window, cx);
7855 assert_eq!(state.value(), "one two \nfour five");
7856 state.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
7857 assert_eq!(state.value(), "one two four five");
7858
7859 state.undo(&Undo, window, cx);
7860 assert_eq!(state.value(), "one two \nfour five");
7861 state.undo(&Undo, window, cx);
7862 assert_eq!(state.value(), "one two three\nfour five");
7863 });
7864 });
7865 }
7866
7867 #[gpui::test]
7868 fn test_undo_manager_multiline_replacement_is_one_atomic_transaction(cx: &mut TestAppContext) {
7869 let input_view = InputView::build_textarea(cx, |state| state);
7870 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7871 let input = input_view.input;
7872
7873 cx.update(|window, cx| {
7874 input.update(cx, |state, cx| {
7875 state.replace_text_in_range(None, "before", window, cx);
7876 state.set_selected_range(0..6, cx);
7877 state.replace_text_in_range(None, "line one\nline two\n第三行", window, cx);
7878 assert_eq!(state.value(), "line one\nline two\n第三行");
7879
7880 state.undo(&Undo, window, cx);
7881 assert_eq!(state.value(), "before");
7882 assert_eq!(state.selected_range(), 0..6);
7883 state.redo(&Redo, window, cx);
7884 assert_eq!(state.value(), "line one\nline two\n第三行");
7885 });
7886 });
7887 }
7888
7889 #[gpui::test]
7890 fn test_undo_manager_composition_isolated_from_long_typing(cx: &mut TestAppContext) {
7891 let input_view = InputView::build(cx, |state| state);
7892 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7893 let input = input_view.input;
7894
7895 cx.update(|window, cx| {
7896 input.update(cx, |state, cx| {
7897 state.replace_text_in_range(None, "prefix ", window, cx);
7898 state.replace_and_mark_text_in_range(None, "n", None, window, cx);
7899 state.replace_and_mark_text_in_range(None, "ni", None, window, cx);
7900 state.replace_and_mark_text_in_range(None, "你", None, window, cx);
7901 state.unmark_text(window, cx);
7902 state.replace_text_in_range(None, " suffix", window, cx);
7903 assert_eq!(state.value(), "prefix 你 suffix");
7904
7905 state.undo(&Undo, window, cx);
7906 assert_eq!(state.value(), "prefix 你");
7907 state.undo(&Undo, window, cx);
7908 assert_eq!(state.value(), "prefix ");
7909 state.undo(&Undo, window, cx);
7910 assert_eq!(state.value(), "");
7911 });
7912 });
7913 }
7914
7915 #[gpui::test]
7916 fn test_undo_manager_selected_replacement_is_atomic(cx: &mut TestAppContext) {
7917 let input_view = InputView::build(cx, |state| state);
7918 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7919 let input = input_view.input;
7920
7921 cx.update(|window, cx| {
7922 input.update(cx, |state, cx| {
7923 state.replace_text_in_range(None, "abc", window, cx);
7924 state.set_selected_range(1..2, cx);
7925 state.replace_text_in_range(None, "X", window, cx);
7926 state.replace_text_in_range(None, "z", window, cx);
7927 assert_eq!(state.value(), "aXzc");
7928
7929 state.undo(&Undo, window, cx);
7930 assert_eq!(state.value(), "aXc");
7931 state.undo(&Undo, window, cx);
7932 assert_eq!(state.value(), "abc");
7933 state.undo(&Undo, window, cx);
7934 assert_eq!(state.value(), "");
7935 });
7936 });
7937 }
7938
7939 #[gpui::test]
7940 fn test_number_input_leading_dot_editable(cx: &mut TestAppContext) {
7941 let input_view = InputView::build(cx, |state| {
7942 state.mask_pattern(MaskPattern::Number {
7943 separator: None,
7944 fraction: None,
7945 })
7946 });
7947 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7948 let input = input_view.input;
7949
7950 cx.update(|window, cx| {
7951 input.update(cx, |state, cx| {
7952 state.replace_text_in_range(None, "1.2", window, cx);
7953
7954 let range = state.range_to_utf16(&(0..1));
7958 state.replace_text_in_range(Some(range), "", window, cx);
7959 assert_eq!(state.value(), ".2");
7960 let cursor: Range<usize> = state.selected_range();
7961 assert_eq!(cursor, 0..0);
7962
7963 state.replace_text_in_range(Some(0..0), "3", window, cx);
7965 assert_eq!(state.value(), "3.2");
7966 });
7967 });
7968 }
7969
7970 #[gpui::test]
7971 fn test_number_input_escape_invalid_text(cx: &mut TestAppContext) {
7972 let input_view = InputView::build(cx, |state| {
7975 state
7976 .mask_pattern(MaskPattern::Number {
7977 separator: None,
7978 fraction: None,
7979 })
7980 .default_value("1,234")
7981 });
7982 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7983 let input = input_view.input;
7984
7985 cx.update(|window, cx| {
7986 input.update(cx, |state, cx| {
7987 let range = state.range_to_utf16(&(4..5));
7991 state.replace_text_in_range(Some(range), "", window, cx);
7992 assert_eq!(state.value(), "1,23");
7993
7994 let range = state.range_to_utf16(&(1..2));
7996 state.replace_text_in_range(Some(range), "", window, cx);
7997 assert_eq!(state.value(), "123");
7998 state.replace_text_in_range(None, "a", window, cx);
7999 assert_eq!(state.value(), "123");
8000 });
8001 });
8002 }
8003
8004 #[gpui::test]
8008 fn test_set_value_single_line_caret_at_end_view_at_start(cx: &mut TestAppContext) {
8009 let input_view = InputView::build(cx, |state| state);
8010 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8011 let input = input_view.input;
8012
8013 let value = format!("https://example.com/v1/users?{}", "x=1&".repeat(120));
8015 let len = value.len();
8016
8017 cx.update(|window, cx| {
8020 input.update(cx, |state, cx| {
8021 state.set_value(value.clone(), window, cx);
8022
8023 assert_eq!(
8024 state.selected_range(),
8025 len..len,
8026 "single-line caret should be at the end after set_value"
8027 );
8028 assert_eq!(
8029 state.deferred_scroll_offset,
8030 Some(point(px(0.), px(0.))),
8031 "the view should be forced back to the start"
8032 );
8033 });
8034 });
8035
8036 cx.run_until_parked();
8039 cx.update(|_, cx| {
8040 input.read_with(cx, |state, _| {
8041 assert!(
8042 state.scroll_size.width > state.input_bounds.size.width,
8043 "value must overflow the input width or this test is vacuous"
8044 );
8045 assert_eq!(
8046 state.scroll_handle.offset().x,
8047 px(0.),
8048 "long value should display from its start, not its tail"
8049 );
8050 });
8051 });
8052 }
8053
8054 #[gpui::test]
8058 fn test_replace_all_single_line(cx: &mut TestAppContext) {
8059 let input_view = InputView::build(cx, |state| state);
8060 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8061 let input = input_view.input;
8062
8063 let value = format!("https://example.com/v1/users?{}", "x=1&".repeat(120));
8065 let len = value.len();
8066
8067 cx.update(|window, cx| {
8071 input.update(cx, |state, cx| {
8072 state.set_value("hello", window, cx);
8073 state.replace_all(value.clone(), window, cx);
8074 assert_eq!(state.value(), value);
8075 assert_eq!(
8076 state.selected_range(),
8077 len..len,
8078 "single-line caret should be at the end after replace_all"
8079 );
8080 assert_eq!(
8081 state.scroll_handle.offset(),
8082 point(px(0.), px(0.)),
8083 "the scroll offset should be reset to the start"
8084 );
8085 assert_eq!(
8086 state.deferred_scroll_offset,
8087 Some(point(px(0.), px(0.))),
8088 "single-line should set a deferred scroll offset to keep the start visible"
8089 );
8090 });
8091 });
8092
8093 cx.run_until_parked();
8096 cx.update(|_, cx| {
8097 input.read_with(cx, |state, _| {
8098 assert!(
8099 state.scroll_size.width > state.input_bounds.size.width,
8100 "value must overflow the input width or this test is vacuous"
8101 );
8102 assert_eq!(
8103 state.scroll_handle.offset().x,
8104 px(0.),
8105 "long value should display from its start, not its tail"
8106 );
8107 });
8108 });
8109 }
8110
8111 #[gpui::test]
8112 fn test_single_line_removes_newlines(cx: &mut TestAppContext) {
8113 let input_view = InputView::build(cx, |state| state.default_value("default\nvalue"));
8114 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8115 let input = input_view.input;
8116
8117 cx.update(|window, cx| {
8118 input.update(cx, |state, cx| {
8119 assert_eq!(state.value(), "defaultvalue");
8120
8121 state.set_value("first\nsecond\r\nthird\rfourth", window, cx);
8122 assert_eq!(state.value(), "firstsecondthirdfourth");
8123
8124 state.set_value("", window, cx);
8125 state.insert("a\nb", window, cx);
8126 assert_eq!(state.value(), "ab");
8127 });
8128
8129 cx.write_to_clipboard(ClipboardItem::new_string("a\r\nb\nc\rd".to_string()));
8130 input.update(cx, |state, cx| {
8131 state.set_value("", window, cx);
8132 state.paste(&Paste, window, cx);
8133 assert_eq!(state.value(), "abcd");
8134 });
8135 });
8136
8137 cx.run_until_parked();
8138 }
8139
8140 #[gpui::test]
8144 fn test_replace_all_multi_line(cx: &mut TestAppContext) {
8145 let input_view = InputView::build_textarea(cx, |state| state);
8146 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8147 let input = input_view.input;
8148
8149 cx.update(|window, cx| {
8150 input.update(cx, |state, cx| {
8151 state.set_value("foo\nbar", window, cx);
8152 state.replace_all("baz\nqux", window, cx);
8153 assert_eq!(state.value(), "baz\nqux");
8154 assert_eq!(
8155 state.selected_range(),
8156 0..0,
8157 "multi-line selection should be cleared after replace_all"
8158 );
8159 assert_eq!(
8160 state.scroll_handle.offset(),
8161 point(px(0.), px(0.)),
8162 "the scroll offset should be reset to the start"
8163 );
8164 assert!(
8165 state.deferred_scroll_offset.is_none(),
8166 "multi-line should not set a deferred scroll offset"
8167 );
8168 });
8169 });
8170 }
8171
8172 #[gpui::test]
8175 fn test_replace_all_preserves_undo_history(cx: &mut TestAppContext) {
8176 let input_view = InputView::build(cx, |state| state);
8177 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8178 let input = input_view.input;
8179
8180 cx.update(|window, cx| {
8181 input.update(cx, |state, cx| {
8182 state.set_value("first", window, cx);
8184 assert!(
8185 !state.undo_manager.has_undos(),
8186 "history should be empty after set_value"
8187 );
8188
8189 state.replace_all("second", window, cx);
8191 assert_eq!(state.value(), "second");
8192 assert!(
8193 state.undo_manager.has_undos(),
8194 "replace_all should record an undo step"
8195 );
8196
8197 state.undo(&Undo, window, cx);
8199 assert_eq!(state.value(), "first");
8200
8201 state.redo(&Redo, window, cx);
8203 assert_eq!(state.value(), "second");
8204 });
8205 });
8206 }
8207
8208 #[gpui::test]
8211 fn test_replace_all_code_editor(cx: &mut TestAppContext) {
8212 let input_view = InputView::new(cx);
8213 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8214 let input = input_view.input;
8215
8216 cx.update(|window, cx| {
8217 input.update(cx, |state, cx| {
8218 state.set_value("select 1", window, cx);
8220 state._pending_update = false;
8221
8222 state.replace_all("select 2", window, cx);
8223 assert_eq!(state.value(), "select 2");
8224 assert!(
8225 state._pending_update,
8226 "replace_all on a code editor should request a pending update"
8227 );
8228 });
8229 });
8230 }
8231
8232 #[gpui::test]
8233 fn test_set_selected_range(cx: &mut TestAppContext) {
8234 let input_view = InputView::build(cx, |state| state.default_value("hello world"));
8235 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8236 let input = input_view.input;
8237
8238 cx.update(|_, cx| {
8239 input.update(cx, |s, cx| {
8240 s.set_selected_range(0..5, cx);
8241 assert_eq!(s.selected_range(), 0..5);
8242 assert_eq!(s.selected_text().to_string(), "hello");
8243
8244 s.set_selected_range(6..11, cx);
8245 assert_eq!(s.selected_text().to_string(), "world");
8246
8247 s.set_selected_range(100..100, cx);
8249 assert_eq!(s.selected_range(), 11..11);
8250 });
8251 });
8252 }
8253
8254 #[gpui::test]
8256 fn test_replace_text_in_ranges_single_edit(cx: &mut TestAppContext) {
8257 let input_view = InputView::build_textarea(cx, |state| state.default_value("hello world"));
8258 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8259 let input = input_view.input;
8260
8261 cx.update(|window, cx| {
8262 input.update(cx, |s, cx| {
8263 s.replace_text_in_ranges(&[(0..5, "HELLO".to_string())], window, cx);
8264 assert_eq!(s.value(), "HELLO world");
8265
8266 s.undo(&Undo, window, cx);
8267 assert_eq!(s.value(), "hello world");
8268
8269 s.redo(&Redo, window, cx);
8270 assert_eq!(s.value(), "HELLO world");
8271 });
8272 });
8273 }
8274
8275 #[gpui::test]
8278 fn test_replace_text_in_ranges_multi_edit_transaction(cx: &mut TestAppContext) {
8279 let input_view = InputView::build_textarea(cx, |state| state.default_value("aaa bbb ccc"));
8280 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8281 let input = input_view.input;
8282
8283 cx.update(|window, cx| {
8284 input.update(cx, |s, cx| {
8285 s.replace_text_in_ranges(
8288 &[(0..3, "X".to_string()), (8..11, "Y".to_string())],
8289 window,
8290 cx,
8291 );
8292 assert_eq!(s.value(), "X bbb Y");
8293
8294 let cursors: Vec<usize> =
8296 s.selections.iter().map(|sel| sel.cursor_offset()).collect();
8297 assert_eq!(cursors, vec![1, 7]);
8298
8299 assert_eq!(s.undo_manager.undo_count(), 1);
8301
8302 s.undo(&Undo, window, cx);
8304 assert_eq!(s.value(), "aaa bbb ccc");
8305
8306 s.redo(&Redo, window, cx);
8308 assert_eq!(s.value(), "X bbb Y");
8309 });
8310 });
8311 }
8312
8313 #[gpui::test]
8317 fn test_replace_text_in_ranges_drives_the_highlighter_once(cx: &mut TestAppContext) {
8318 use crate::input::{FoldRange, HighlightStyleResolver, InputEdit, InputHighlighter};
8319 use gpui::HighlightStyle;
8320 use std::cell::RefCell;
8321
8322 struct RecordingHighlighter(Rc<RefCell<Vec<Vec<String>>>>);
8323
8324 impl InputHighlighter for RecordingHighlighter {
8325 fn language(&self) -> SharedString {
8326 "recording".into()
8327 }
8328
8329 fn update(
8330 &mut self,
8331 _: Option<InputEdit>,
8332 text: &Rope,
8333 _: bool,
8334 _: &mut Window,
8335 _: &mut Context<EditorState>,
8336 ) {
8337 self.0.borrow_mut().push(vec![text.to_string()]);
8338 }
8339
8340 fn update_batch(
8341 &mut self,
8342 edits: &[(InputEdit, Rope)],
8343 _: bool,
8344 _: &mut Window,
8345 _: &mut Context<EditorState>,
8346 ) {
8347 let texts = edits.iter().map(|(_, text)| text.to_string()).collect();
8348 self.0.borrow_mut().push(texts);
8349 }
8350
8351 fn styles(
8352 &self,
8353 range: &Range<usize>,
8354 _: &dyn HighlightStyleResolver,
8355 ) -> Vec<(Range<usize>, HighlightStyle)> {
8356 vec![(range.clone(), HighlightStyle::default())]
8357 }
8358
8359 fn fold_ranges(&self, _: &Rope) -> Vec<FoldRange> {
8360 Vec::new()
8361 }
8362 }
8363
8364 let updates = Rc::new(RefCell::new(Vec::new()));
8365 let input_view = InputView::new(cx);
8366 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8367 let input = input_view.input;
8368
8369 cx.update(|window, cx| {
8370 input.update(cx, |s, cx| {
8371 s.set_value("aaa bbb ccc", window, cx);
8372 let recorded = updates.clone();
8373 s.set_highlighter_factory(
8374 Rc::new(move |_: &str| {
8375 let highlighter: Box<dyn InputHighlighter> =
8376 Box::new(RecordingHighlighter(recorded.clone()));
8377 Some(highlighter)
8378 }),
8379 cx,
8380 );
8381 s.replace_text_in_ranges(
8382 &[(0..3, "X".to_string()), (8..11, "Y".to_string())],
8383 window,
8384 cx,
8385 );
8386 });
8387 });
8388
8389 assert_eq!(
8390 *updates.borrow(),
8391 vec![vec!["aaa bbb Y".to_string(), "X bbb Y".to_string()]]
8392 );
8393 }
8394
8395 #[gpui::test]
8397 fn test_ime_composition_undoes_as_one_unit(cx: &mut TestAppContext) {
8398 let input_view = InputView::build_textarea(cx, |state| state.default_value(""));
8399 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8400 let input = input_view.input;
8401
8402 cx.update(|window, cx| {
8403 input.update(cx, |s, cx| {
8404 s.replace_and_mark_text_in_range(None, "n", Some(1..1), window, cx);
8406 s.replace_and_mark_text_in_range(None, "ni", Some(2..2), window, cx);
8407 s.replace_text_in_range(None, "你", window, cx);
8408 assert_eq!(s.value(), "你");
8409
8410 assert_eq!(s.undo_manager.undo_count(), 1);
8412
8413 s.undo(&Undo, window, cx);
8414 assert_eq!(s.value(), "");
8415
8416 s.redo(&Redo, window, cx);
8417 assert_eq!(s.value(), "你");
8418 });
8419 });
8420 }
8421
8422 #[gpui::test]
8425 fn test_edit_after_composition_is_separate_undo(cx: &mut TestAppContext) {
8426 let input_view = InputView::build_textarea(cx, |state| state.default_value(""));
8427 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8428 let input = input_view.input;
8429
8430 cx.update(|window, cx| {
8431 input.update(cx, |s, cx| {
8432 s.replace_and_mark_text_in_range(None, "n", Some(1..1), window, cx);
8433 s.replace_text_in_range(None, "你", window, cx);
8434 assert_eq!(s.value(), "你");
8435 assert_eq!(s.undo_manager.undo_count(), 1);
8436
8437 s.replace_text_in_range(None, "x", window, cx);
8439 assert_eq!(s.value(), "你x");
8440 assert_eq!(s.undo_manager.undo_count(), 2);
8441
8442 s.undo(&Undo, window, cx);
8443 assert_eq!(s.value(), "你");
8444 s.undo(&Undo, window, cx);
8445 assert_eq!(s.value(), "");
8446 });
8447 });
8448 }
8449
8450 #[gpui::test]
8453 fn test_composition_cancel_via_unmark_does_not_leak(cx: &mut TestAppContext) {
8454 let input_view = InputView::build_textarea(cx, |state| state.default_value(""));
8455 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8456 let input = input_view.input;
8457
8458 cx.update(|window, cx| {
8459 input.update(cx, |s, cx| {
8460 s.replace_and_mark_text_in_range(None, "n", Some(1..1), window, cx);
8462 s.unmark_text(window, cx);
8463 let after_cancel = s.undo_manager.undo_count();
8464
8465 s.replace_text_in_range(None, "x", window, cx);
8467 assert_eq!(s.undo_manager.undo_count(), after_cancel + 1);
8468 });
8469 });
8470 }
8471
8472 #[gpui::test]
8473 fn test_set_selected_range_clips_to_utf8_boundaries(cx: &mut TestAppContext) {
8474 let input_view = InputView::build(cx, |state| state.default_value("éx"));
8475 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8476 let input = input_view.input;
8477
8478 cx.update(|window, cx| {
8479 input.update(cx, |state, cx| {
8480 state.set_selected_range(0..1, cx);
8481 assert_eq!(state.selected_range(), 0..2);
8482 state.copy(&Copy, window, cx);
8483
8484 state.set_selected_range(1..1, cx);
8485 assert_eq!(state.selected_range(), 0..0);
8486 });
8487 });
8488 }
8489
8490 #[gpui::test]
8491 fn test_ime_selection_is_relative_to_replacement_start(cx: &mut TestAppContext) {
8492 let input_view = InputView::build(cx, |state| state.default_value("你好 "));
8493 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8494 let input = input_view.input;
8495
8496 cx.update(|window, cx| {
8497 input.update(cx, |state, cx| {
8498 state.set_selected_range(7..7, cx);
8499 state.replace_and_mark_text_in_range(None, "s", Some(1..1), window, cx);
8500 state.replace_and_mark_text_in_range(None, "sh", Some(2..2), window, cx);
8501
8502 assert_eq!(state.value(), "你好 sh");
8503 assert_eq!(state.selected_range(), 9..9);
8504 assert_eq!(state.ime_marked_range, Some((7..9).into()));
8505 });
8506 });
8507 }
8508
8509 #[gpui::test]
8510 fn test_undo_manager_composition_is_one_undo_group(cx: &mut TestAppContext) {
8511 let input_view = InputView::build(cx, |state| state);
8512 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8513 let input = input_view.input;
8514
8515 cx.update(|window, cx| {
8516 input.update(cx, |state, cx| {
8517 state.set_value("a", window, cx);
8518 state.replace_and_mark_text_in_range(None, "s", None, window, cx);
8519 state.replace_and_mark_text_in_range(None, "sh", None, window, cx);
8520 state.replace_text_in_range(None, "是", window, cx);
8521 assert_eq!(state.value(), "a是");
8522
8523 state.undo(&Undo, window, cx);
8524 assert_eq!(state.value(), "a");
8525 state.redo(&Redo, window, cx);
8526 assert_eq!(state.value(), "a是");
8527 });
8528 });
8529 }
8530
8531 #[gpui::test]
8532 fn test_undo_manager_consecutive_compositions_are_separate_groups(cx: &mut TestAppContext) {
8533 let input_view = InputView::build(cx, |state| state);
8534 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8535 let input = input_view.input;
8536
8537 cx.update(|window, cx| {
8538 input.update(cx, |state, cx| {
8539 state.replace_and_mark_text_in_range(None, "j", None, window, cx);
8541 state.replace_and_mark_text_in_range(None, "jin", None, window, cx);
8542 state.replace_text_in_range(None, "今天", window, cx);
8543 state.replace_and_mark_text_in_range(None, "w", None, window, cx);
8545 state.replace_and_mark_text_in_range(None, "wo", None, window, cx);
8546 state.replace_text_in_range(None, "我们", window, cx);
8547 assert_eq!(state.value(), "今天我们");
8548 assert_eq!(state.selected_range(), 12..12);
8549
8550 state.undo(&Undo, window, cx);
8551 assert_eq!(state.value(), "今天");
8552 assert_eq!(state.selected_range(), 6..6);
8553
8554 state.undo(&Undo, window, cx);
8555 assert_eq!(state.value(), "");
8556 assert_eq!(state.selected_range(), 0..0);
8557
8558 state.redo(&Redo, window, cx);
8559 assert_eq!(state.value(), "今天");
8560 assert_eq!(state.selected_range(), 6..6);
8561
8562 state.redo(&Redo, window, cx);
8563 assert_eq!(state.value(), "今天我们");
8564 assert_eq!(state.selected_range(), 12..12);
8565 });
8566 });
8567 }
8568
8569 #[gpui::test]
8570 fn test_undo_manager_typing_after_composition_is_a_separate_group(cx: &mut TestAppContext) {
8571 let input_view = InputView::build(cx, |state| state);
8572 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8573 let input = input_view.input;
8574
8575 cx.update(|window, cx| {
8576 input.update(cx, |state, cx| {
8577 state.replace_and_mark_text_in_range(None, "n", None, window, cx);
8578 state.replace_text_in_range(None, "你", window, cx);
8579 state.undo_manager.set_pending_intent(EditIntent::Typing);
8580 state.replace_text_in_range(None, "a", window, cx);
8581 state.undo_manager.set_pending_intent(EditIntent::Typing);
8582 state.replace_text_in_range(None, "b", window, cx);
8583 assert_eq!(state.value(), "你ab");
8584
8585 state.undo(&Undo, window, cx);
8586 assert_eq!(state.value(), "你");
8587
8588 state.undo(&Undo, window, cx);
8589 assert_eq!(state.value(), "");
8590 });
8591 });
8592 }
8593
8594 #[gpui::test]
8595 fn test_undo_manager_composition_cancel_leaves_no_entry(cx: &mut TestAppContext) {
8596 let input_view = InputView::build(cx, |state| state);
8597 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8598 let input = input_view.input;
8599
8600 cx.update(|window, cx| {
8601 input.update(cx, |state, cx| {
8602 state.set_value("a", window, cx);
8603 state.replace_and_mark_text_in_range(None, "s", None, window, cx);
8604 state.replace_and_mark_text_in_range(None, "", None, window, cx);
8605
8606 assert_eq!(state.value(), "a");
8607 assert!(!state.undo_manager.has_undos());
8608 });
8609 });
8610 }
8611
8612 #[gpui::test]
8614 fn test_undo_manager_ime_rewrites_of_typed_char_are_one_group(cx: &mut TestAppContext) {
8615 let input_view = InputView::build(cx, |state| state);
8616 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8617 let input = input_view.input;
8618
8619 cx.update(|window, cx| {
8620 input.update(cx, |state, cx| {
8621 state.set_value("é ", window, cx);
8622 state.set_selected_range(3..3, cx);
8623 state.replace_text_in_range(None, "ㅎ", window, cx);
8625 state.replace_text_in_range(Some(2..3), "하", window, cx);
8626 state.replace_text_in_range(Some(2..3), "한", window, cx);
8627 state.replace_text_in_range(None, "ㄱ", window, cx);
8628 state.replace_text_in_range(Some(3..4), "그", window, cx);
8629 state.replace_text_in_range(Some(3..4), "글", window, cx);
8630 assert_eq!(state.value(), "é 한글");
8631 assert_eq!(state.undo_manager.undo_count(), 1);
8632
8633 state.undo(&Undo, window, cx);
8634 assert_eq!(state.value(), "é ");
8635 assert_eq!(state.selected_range(), 3..3);
8636
8637 state.redo(&Redo, window, cx);
8638 assert_eq!(state.value(), "é 한글");
8639 assert_eq!(state.selected_range(), 9..9);
8640 });
8641 });
8642 }
8643
8644 #[gpui::test]
8645 fn test_undo_manager_rewrite_after_cursor_movement_is_separate(cx: &mut TestAppContext) {
8646 let input_view = InputView::build(cx, |state| state);
8647 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8648 let input = input_view.input;
8649
8650 cx.update(|window, cx| {
8651 input.update(cx, |state, cx| {
8652 state.replace_text_in_range(None, "c", window, cx);
8653 state.replace_text_in_range(None, "e", window, cx);
8654 state.left(&MoveLeft, window, cx);
8655 state.right(&MoveRight, window, cx);
8656 state.replace_text_in_range(Some(1..2), "é", window, cx);
8657 assert_eq!(state.value(), "cé");
8658
8659 state.undo(&Undo, window, cx);
8660 assert_eq!(state.value(), "ce");
8661 state.undo(&Undo, window, cx);
8662 assert_eq!(state.value(), "");
8663 });
8664 });
8665 }
8666
8667 #[gpui::test]
8668 fn test_undo_manager_selection_restored_by_undo_and_redo(cx: &mut TestAppContext) {
8669 let input_view = InputView::build(cx, |state| state);
8670 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8671 let input = input_view.input;
8672
8673 cx.update(|window, cx| {
8674 input.update(cx, |state, cx| {
8675 state.set_value("abc", window, cx);
8676 state.set_selected_range(1..2, cx);
8677 state.replace_text_in_range(None, "X", window, cx);
8678
8679 state.undo(&Undo, window, cx);
8680 assert_eq!(state.value(), "abc");
8681 assert_eq!(state.selected_range(), 1..2);
8682
8683 state.redo(&Redo, window, cx);
8684 assert_eq!(state.value(), "aXc");
8685 assert_eq!(state.selected_range(), 2..2);
8686 });
8687 });
8688 }
8689
8690 #[gpui::test]
8691 fn test_undo_manager_forward_delete_restores_cursor(cx: &mut TestAppContext) {
8692 let input_view = InputView::build(cx, |state| state);
8693 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8694 let input = input_view.input;
8695
8696 cx.update(|window, cx| {
8697 input.update(cx, |state, cx| {
8698 state.set_value("abc", window, cx);
8699 state.set_selected_range(1..1, cx);
8700 state.delete(&Delete, window, cx);
8701
8702 state.undo(&Undo, window, cx);
8703 assert_eq!(state.value(), "abc");
8704 assert_eq!(state.selected_range(), 1..1);
8705 });
8706 });
8707 }
8708
8709 #[gpui::test]
8710 fn test_undo_manager_selection_movement_preserves_redo(cx: &mut TestAppContext) {
8711 let input_view = InputView::build(cx, |state| state);
8712 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8713 let input = input_view.input;
8714
8715 cx.update(|window, cx| {
8716 input.update(cx, |state, cx| {
8717 state.replace_text_in_range(None, "ab", window, cx);
8718 state.undo(&Undo, window, cx);
8719 state.set_selected_range(0..0, cx);
8720 state.redo(&Redo, window, cx);
8721 assert_eq!(state.value(), "ab");
8722
8723 state.undo(&Undo, window, cx);
8724 state.replace_text_in_range(None, "x", window, cx);
8725 state.redo(&Redo, window, cx);
8726 assert_eq!(state.value(), "x");
8727 });
8728 });
8729 }
8730
8731 #[gpui::test]
8732 fn test_undo_manager_noop_edit_preserves_redo(cx: &mut TestAppContext) {
8733 let input_view = InputView::build(cx, |state| state);
8734 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8735 let input = input_view.input;
8736
8737 cx.update(|window, cx| {
8738 input.update(cx, |state, cx| {
8739 state.replace_text_in_range(None, "a", window, cx);
8740 state.move_to(0, None, cx);
8741 state.replace_text_in_range(None, "", window, cx);
8742 state.undo(&Undo, window, cx);
8743 assert_eq!(state.value(), "");
8744 state.redo(&Redo, window, cx);
8745 assert_eq!(state.value(), "a");
8746 assert_eq!(state.cursor(), 1);
8747 });
8748 });
8749 }
8750
8751 #[gpui::test]
8752 fn test_cursor_round_trip_stops_typing_coalescing(cx: &mut TestAppContext) {
8753 let input_view = InputView::build(cx, |state| state);
8754 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8755 let input = input_view.input;
8756
8757 cx.update(|window, cx| {
8758 input.update(cx, |state, cx| {
8759 state.replace_text_in_range(None, "a", window, cx);
8760 state.left(&MoveLeft, window, cx);
8761 state.right(&MoveRight, window, cx);
8762 state.replace_text_in_range(None, "b", window, cx);
8763
8764 state.undo(&Undo, window, cx);
8765 assert_eq!(state.value(), "a");
8766 state.undo(&Undo, window, cx);
8767 assert_eq!(state.value(), "");
8768 });
8769 });
8770 }
8771
8772 #[gpui::test]
8773 fn test_undo_manager_noop_edit_breaks_coalescing_without_clearing_history(
8774 cx: &mut TestAppContext,
8775 ) {
8776 let input_view = InputView::build(cx, |state| state);
8777 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8778 let input = input_view.input;
8779
8780 cx.update(|window, cx| {
8781 input.update(cx, |state, cx| {
8782 state.replace_text_in_range(None, "alpha", window, cx);
8783 state.replace_text_in_range(None, "", window, cx);
8784 state.replace_text_in_range(None, "beta", window, cx);
8785 assert_eq!(state.value(), "alphabeta");
8786
8787 state.undo(&Undo, window, cx);
8788 assert_eq!(state.value(), "alpha");
8789 state.undo(&Undo, window, cx);
8790 assert_eq!(state.value(), "");
8791 });
8792 });
8793 }
8794
8795 #[gpui::test]
8796 fn test_undo_manager_masked_redo_restores_actual_cursor(cx: &mut TestAppContext) {
8797 let input_view = InputView::build(cx, |state| {
8798 state.mask_pattern(MaskPattern::Number {
8799 separator: Some(','),
8800 fraction: None,
8801 })
8802 });
8803 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8804 let input = input_view.input;
8805
8806 cx.update(|window, cx| {
8807 input.update(cx, |state, cx| {
8808 state.set_value("12345", window, cx);
8809 state.set_selected_range(2..2, cx);
8810 state.replace_text_in_range(None, "9", window, cx);
8811 let selection_after_edit = state.selected_range();
8812 assert_ne!(selection_after_edit.end, state.value().len());
8813
8814 state.undo(&Undo, window, cx);
8815 state.redo(&Redo, window, cx);
8816 assert_eq!(state.selected_range(), selection_after_edit);
8817 });
8818 });
8819 }
8820
8821 #[gpui::test]
8827 fn test_unfold_at(cx: &mut TestAppContext) {
8828 use crate::input::{FoldRange, Position};
8829
8830 let view = InputView::<EditorMode>::new(cx);
8831 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8832 let input = view.input;
8833
8834 cx.update(|window, cx| {
8837 input.update(cx, |state, cx| {
8838 state.set_value("a\nb\nc\nd\ne\nf\ng\nh\ni\nj\nk\nl", window, cx);
8839 state.apply_highlighter_fold_candidates(
8840 vec![
8841 FoldRange::new(0, 5),
8842 FoldRange::new(2, 4),
8843 FoldRange::new(7, 10),
8844 ],
8845 cx,
8846 );
8847 state.display_map.set_folded(0, true);
8848 state.display_map.set_folded(2, true);
8849 state.display_map.set_folded(7, true);
8850 });
8851 });
8852
8853 for line in [0, 5] {
8856 cx.update(|_, cx| {
8857 input.update(cx, |state, cx| {
8858 assert!(!state.display_map.is_buffer_line_hidden(line));
8859 assert!(!state.unfold_at(Position::new(line as u32, 0), cx));
8860 });
8861 input.read_with(cx, |state, _| {
8862 assert!(state.display_map.is_folded_at(0));
8863 assert!(state.display_map.is_folded_at(2));
8864 assert!(state.display_map.is_folded_at(7));
8865 });
8866 });
8867 }
8868
8869 cx.update(|_, cx| {
8872 input.update(cx, |state, cx| {
8873 assert!(state.unfold_at(Position::new(3, 0), cx));
8874 });
8875 input.read_with(cx, |state, _| {
8876 assert!(!state.display_map.is_buffer_line_hidden(3));
8877 assert!(!state.display_map.is_folded_at(0));
8878 assert!(!state.display_map.is_folded_at(2));
8879 assert!(state.display_map.is_folded_at(7));
8880 assert!(state.display_map.is_fold_candidate(0));
8882 assert!(state.display_map.is_fold_candidate(2));
8883 });
8884 });
8885
8886 cx.update(|_, cx| {
8888 input.update(cx, |state, cx| {
8889 assert!(!state.unfold_at(Position::new(3, 0), cx));
8890 });
8891 });
8892 }
8893
8894 #[gpui::test]
8899 fn test_blur_keeps_decorations(cx: &mut TestAppContext) {
8900 let view = InputView::<EditorMode>::new(cx);
8901 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8902 let input = view.input;
8903
8904 cx.update(|window, cx| {
8905 input.update(cx, |state, cx| {
8906 state.set_value("select 1", window, cx);
8907 let _collection = state.create_decorations_collection(
8908 vec![crate::input::TextDecoration::new(
8909 0..6,
8910 gpui::HighlightStyle {
8911 font_weight: Some(gpui::FontWeight::BOLD),
8912 ..Default::default()
8913 },
8914 )],
8915 cx,
8916 );
8917 state.present_hover(
8918 0..6,
8919 lsp_types::Hover {
8920 contents: lsp_types::HoverContents::Scalar(
8921 lsp_types::MarkedString::String("docs".into()),
8922 ),
8923 range: None,
8924 },
8925 cx,
8926 );
8927 assert!(state.hover_popover().is_some());
8928
8929 state.on_blur(window, cx);
8930
8931 assert!(
8932 state.hover_popover().is_none(),
8933 "blur should hide the hover popover"
8934 );
8935 let decorations = state.extras.decoration_layers();
8936 assert!(
8937 decorations.iter().any(|layer| !layer.is_empty()),
8938 "blur must not discard decorations"
8939 );
8940 });
8941 });
8942 }
8943
8944 #[gpui::test]
8949 fn test_kind_does_not_follow_the_row_count(cx: &mut TestAppContext) {
8950 let view = InputView::build_textarea(cx, |state| state.auto_grow(1, 1));
8951 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8952 view.input.read_with(&mut cx, |state, _| {
8953 assert!(state.is_multi_line());
8954 assert!(!state.is_single_line());
8955 assert!(!state.is_code_editor());
8956 });
8957 }
8958
8959 #[gpui::test]
8960 fn test_textarea_prewrap_prepares_content_rows_before_layout(cx: &mut TestAppContext) {
8961 cx.update(crate::init);
8962 for (value, expected_rows) in [("", 1), ("one\ntwo", 2)] {
8963 cx.open_window(size(px(400.), px(100.)), |window, cx| {
8964 let font = window.text_style().font();
8965 cx.new(|cx| {
8966 let mut state = crate::input::TextareaState::new(window, cx)
8967 .rows(12)
8968 .default_value(value);
8969 assert_eq!(state.display_map().wrap_row_count(), 0);
8970 state.prewrap(font, px(14.), px(120.), cx);
8971 assert_eq!(state.display_map().wrap_row_count(), expected_rows);
8972 assert_eq!(state.value(), value);
8973 state
8974 });
8975 gpui::EmptyView
8976 });
8977 }
8978 }
8979
8980 #[gpui::test]
8981 fn test_textarea_prewrap_respects_soft_wrap_before_layout(cx: &mut TestAppContext) {
8982 cx.update(crate::init);
8983 let value = "select id, name, email, phone from users where active\nlimit 10";
8984 for soft_wrap in [true, false] {
8985 cx.open_window(size(px(400.), px(100.)), |window, cx| {
8986 let font = window.text_style().font();
8987 cx.new(|cx| {
8988 let mut state = crate::input::TextareaState::new(window, cx)
8989 .rows(1)
8990 .soft_wrap(soft_wrap)
8991 .default_value(value);
8992 state.prewrap(font, px(14.), px(120.), cx);
8993 let rows = state.display_map().wrap_row_count();
8994 if soft_wrap {
8995 assert!(rows > 2, "the first logical line must wrap");
8996 } else {
8997 assert_eq!(rows, 2);
8998 }
8999 assert_eq!(state.value(), value);
9000 state
9001 });
9002 gpui::EmptyView
9003 });
9004 }
9005 }
9006
9007 #[gpui::test]
9008 fn test_prewrap_counts_wrapped_rows_before_the_first_paint(cx: &mut TestAppContext) {
9009 cx.update(crate::init);
9010 let text = "select id, name, email, phone from users where active\nlimit 10";
9011 for (soft_wrap, wraps) in [(true, true), (false, false)] {
9012 let mut input = None;
9013 cx.open_window(size(px(400.), px(100.)), |window, cx| {
9014 let font = window.text_style().font();
9015 let editor = cx.new(|cx| {
9016 let mut state = crate::input::EditorState::new(window, cx)
9017 .soft_wrap(soft_wrap)
9018 .default_value(text);
9019 state.prewrap(font, px(14.), px(120.), cx);
9020 state
9021 });
9022 input = Some(editor);
9023 gpui::EmptyView
9024 });
9025 let rows = input
9026 .unwrap()
9027 .read_with(cx, |state, _| state.display_map().wrap_row_count());
9028 assert_eq!(rows > 2, wraps, "soft_wrap({soft_wrap}): {rows} rows");
9030 if !wraps {
9031 assert_eq!(rows, 2);
9032 }
9033 }
9034 }
9035
9036 #[gpui::test]
9039 fn test_prewrap_wraps_with_the_wrapping_indent(cx: &mut TestAppContext) {
9040 cx.update(crate::init);
9041 let text = " select id, name, email, phone from users where active";
9042 let mut rows = Vec::new();
9043 for indent in [WrappingIndent::Same, WrappingIndent::None] {
9044 let mut input = None;
9045 cx.open_window(size(px(400.), px(100.)), |window, cx| {
9046 let font = window.text_style().font();
9047 input = Some(cx.new(|cx| {
9048 let mut state = crate::input::EditorState::new(window, cx)
9049 .wrapping_indent(indent)
9050 .default_value(text);
9051 state.prewrap(font, px(14.), px(200.), cx);
9052 state
9053 }));
9054 gpui::EmptyView
9055 });
9056 rows.push(
9057 input
9058 .unwrap()
9059 .read_with(cx, |state, _| state.display_map().wrap_row_count()),
9060 );
9061 }
9062 assert!(rows[0] > rows[1], "rows with Same, None: {rows:?}");
9063 }
9064
9065 #[gpui::test]
9067 fn test_prewrap_does_nothing_once_laid_out(cx: &mut TestAppContext) {
9068 struct Frame(Entity<crate::input::EditorState>);
9069 impl Render for Frame {
9070 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
9071 crate::input::InputBase::new("frame")
9072 .h(px(100.))
9073 .child(crate::input::Editor::new(&self.0))
9074 }
9075 }
9076
9077 cx.update(crate::init);
9078 let text = "select id, name, email, phone from users where active\nlimit 10";
9079 let mut input = None;
9080 let window = cx.open_window(size(px(400.), px(100.)), |window, cx| {
9081 let state = cx.new(|cx| crate::input::EditorState::new(window, cx).default_value(text));
9082 input = Some(state.clone());
9083 Frame(state)
9084 });
9085 let input = input.unwrap();
9086 let mut cx = VisualTestContext::from_window(window.into(), cx);
9087 cx.update(|window, cx| window.draw(cx).clear(cx));
9088
9089 let laid_out = input.read_with(&cx, |state, _| state.display_map().wrap_row_count());
9090 assert!(laid_out >= 2, "laid out to {laid_out} rows");
9091 let font = cx.update(|window, _| window.text_style().font());
9092 input.update(&mut cx, |state, cx| {
9093 state.prewrap(font, px(14.), px(60.), cx)
9094 });
9095 assert_eq!(
9096 input.read_with(&cx, |state, _| state.display_map().wrap_row_count()),
9097 laid_out
9098 );
9099 }
9100
9101 #[gpui::test]
9106 fn test_soft_wrap_is_enabled_by_default(cx: &mut TestAppContext) {
9107 let textarea = InputView::build_textarea(cx, |state| state);
9108 let mut textarea_cx = VisualTestContext::from_window(textarea.window_handle.into(), cx);
9109 textarea
9110 .input
9111 .read_with(&mut textarea_cx, |state, _| assert!(state.soft_wrap));
9112
9113 let editor = InputView::<EditorMode>::new(cx);
9114 let mut editor_cx = VisualTestContext::from_window(editor.window_handle.into(), cx);
9115 editor
9116 .input
9117 .read_with(&mut editor_cx, |state, _| assert!(state.soft_wrap));
9118 }
9119
9120 fn parse_cursor_spec(input: &str) -> (String, Vec<usize>) {
9124 let mut full_text = String::new();
9125 let mut cursor_offsets = Vec::new();
9126 let non_empty_lines: Vec<&str> = input.lines().filter(|l| !l.is_empty()).collect();
9127
9128 for (line_idx, line) in non_empty_lines.iter().enumerate() {
9129 let mut positions = Vec::new();
9130 let mut text = String::new();
9131 for ch in line.chars() {
9132 if ch == '|' {
9133 positions.push(text.len());
9134 } else {
9135 text.push(ch);
9136 }
9137 }
9138
9139 if line_idx > 0 {
9140 full_text.push('\n');
9141 }
9142 let line_start = full_text.len();
9143 for pos in positions {
9144 cursor_offsets.push(line_start + pos);
9145 }
9146 full_text.push_str(&text);
9147 }
9148 full_text.push('\n');
9149
9150 (full_text, cursor_offsets)
9151 }
9152
9153 fn multi_line(cx: &mut TestAppContext) -> InputView<TextareaMode> {
9155 InputView::build_textarea(cx, |state| state)
9156 }
9157
9158 fn setup_cursors<M: InputModeKind>(
9160 cx: &mut VisualTestContext,
9161 input: &Entity<InputBaseState<M>>,
9162 spec: &str,
9163 ) {
9164 let (full_text, offsets) = parse_cursor_spec(spec);
9165 cx.update(|window, cx| {
9166 input.update(cx, |state, cx| {
9167 state.set_value(&full_text, window, cx);
9168 let selections = offsets
9169 .into_iter()
9170 .map(|offset| {
9171 CursorSelection::new(state.selections.generate_id(), offset, offset)
9172 })
9173 .collect();
9174 state.selections.replace_all(selections);
9175 cx.notify();
9176 });
9177 });
9178 }
9179
9180 #[track_caller]
9182 fn assert_cursors<M: InputModeKind>(
9183 cx: &mut VisualTestContext,
9184 input: &Entity<InputBaseState<M>>,
9185 spec: &str,
9186 ) {
9187 let (expected_text, mut expected_cursors) = parse_cursor_spec(spec);
9188 expected_cursors.sort();
9189
9190 let (actual_text, mut actual_cursors) = input.read_with(cx, |state, _| {
9191 (
9192 state.text.to_string(),
9193 state
9194 .selections
9195 .iter()
9196 .map(|s| s.cursor_offset())
9197 .collect::<Vec<_>>(),
9198 )
9199 });
9200 actual_cursors.sort();
9201
9202 assert_eq!(
9203 actual_text, expected_text,
9204 "Text mismatch:\nExpected: {expected_text:?}\nActual: {actual_text:?}"
9205 );
9206 assert_eq!(
9207 actual_cursors, expected_cursors,
9208 "Cursor mismatch:\nExpected: {expected_cursors:?}\nActual: {actual_cursors:?}"
9209 );
9210 }
9211
9212 #[gpui::test]
9213 fn test_alt_drag_selects_a_block_and_replaces_each_row(cx: &mut TestAppContext) {
9214 cx.update(crate::init);
9215 let view = InputView::<EditorMode>::new(cx);
9216 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9217 for modifiers in [
9218 gpui::Modifiers {
9219 alt: true,
9220 ..Default::default()
9221 },
9222 gpui::Modifiers {
9223 alt: true,
9224 shift: true,
9225 ..Default::default()
9226 },
9227 #[cfg(target_os = "linux")]
9228 gpui::Modifiers {
9229 alt: true,
9230 control: true,
9231 ..Default::default()
9232 },
9233 ] {
9234 setup_cursors(&mut cx, &view.input, "|abcd\nabcd\nabcd");
9235 cx.update(|window, cx| {
9236 view.input.update(cx, |state, cx| state.focus(window, cx));
9237 });
9238 let (start, end) = view.input.read_with(&cx, |state, _| {
9239 let layout = state.last_layout.as_ref().unwrap();
9240 let origin = state.last_bounds.unwrap().origin;
9241 let position = |row: usize, col| {
9242 let local = layout.lines[row]
9243 .position_for_index(col, layout, false)
9244 .unwrap();
9245 origin
9246 + point(
9247 layout.line_number_width + local.x,
9248 layout.line_height * (row as f32 + 0.5),
9249 )
9250 };
9251 (position(0, 1), position(2, 3))
9252 });
9253 let end = point(end.x - px(0.5), end.y);
9256 cx.update(|_, cx| {
9258 view.input.update(cx, |state, _| {
9259 state.extras.hover_definition.update(
9260 0..4,
9261 vec![lsp_types::LocationLink {
9262 origin_selection_range: None,
9263 target_uri: "file:///tmp/column-selection.rs".parse().unwrap(),
9264 target_range: Default::default(),
9265 target_selection_range: Default::default(),
9266 }],
9267 );
9268 });
9269 });
9270 cx.simulate_mouse_down(start, MouseButton::Left, modifiers);
9271 cx.simulate_mouse_move(end, MouseButton::Left, modifiers);
9272 cx.simulate_mouse_up(end, MouseButton::Left, modifiers);
9273 view.input.read_with(&cx, |state, _| {
9274 let ranges: Vec<_> = state
9275 .selections
9276 .iter()
9277 .map(|sel| sel.start..sel.end)
9278 .collect();
9279 assert_eq!(ranges, vec![1..3, 6..8, 11..13]);
9280 });
9281 cx.simulate_mouse_move(start, None, modifiers);
9283 cx.simulate_keystrokes("x");
9284 assert_cursors(&mut cx, &view.input, "ax|d\nax|d\nax|d");
9285 }
9286 }
9287
9288 #[gpui::test]
9289 fn test_alt_drag_extends_upward_from_an_existing_cursor(cx: &mut TestAppContext) {
9290 cx.update(crate::init);
9291 let view = InputView::<EditorMode>::new(cx);
9292 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9293 setup_cursors(&mut cx, &view.input, "abcd\nabcd\na|bcd");
9294 cx.update(|window, cx| {
9295 view.input.update(cx, |state, cx| state.focus(window, cx));
9296 });
9297 let (start, end) = view.input.read_with(&cx, |state, _| {
9298 let layout = state.last_layout.as_ref().unwrap();
9299 let origin = state.last_bounds.unwrap().origin;
9300 let local = layout.lines[0]
9301 .position_for_index(1, layout, false)
9302 .unwrap();
9303 let x = layout.line_number_width + local.x;
9304 (
9305 origin + point(x, layout.line_height * 2.5),
9306 origin + point(x, layout.line_height * 0.5),
9307 )
9308 });
9309 let modifiers = gpui::Modifiers {
9310 alt: true,
9311 ..Default::default()
9312 };
9313 cx.simulate_mouse_down(start, MouseButton::Left, modifiers);
9314 cx.simulate_mouse_move(end, MouseButton::Left, modifiers);
9315 cx.simulate_mouse_up(end, MouseButton::Left, modifiers);
9316 assert_cursors(&mut cx, &view.input, "a|bcd\na|bcd\na|bcd");
9317 }
9318
9319 #[gpui::test]
9320 fn test_alt_drag_over_a_short_row_keeps_the_block_width(cx: &mut TestAppContext) {
9321 cx.update(crate::init);
9322 let view = InputView::<EditorMode>::new(cx);
9323 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9324 setup_cursors(&mut cx, &view.input, "abcdef\nab\nabcdef");
9325 cx.update(|window, cx| {
9326 view.input.update(cx, |state, cx| state.focus(window, cx));
9327 });
9328 let (start, end) = view.input.read_with(&cx, |state, _| {
9331 let layout = state.last_layout.as_ref().unwrap();
9332 let origin = state.last_bounds.unwrap().origin;
9333 let x_of = |index| {
9334 layout.line_number_width
9335 + layout.lines[0]
9336 .position_for_index(index, layout, false)
9337 .unwrap()
9338 .x
9339 };
9340 (
9341 origin + point(x_of(1), layout.line_height * 0.5),
9342 origin + point(x_of(5), layout.line_height * 1.5),
9343 )
9344 });
9345 let modifiers = gpui::Modifiers {
9346 alt: true,
9347 shift: true,
9348 ..Default::default()
9349 };
9350 cx.simulate_mouse_down(start, MouseButton::Left, modifiers);
9351 cx.simulate_mouse_move(end, MouseButton::Left, modifiers);
9352 cx.simulate_mouse_up(end, MouseButton::Left, modifiers);
9353
9354 view.input.read_with(&cx, |state, _| {
9356 let ranges: Vec<_> = state.selections.iter().map(|s| (s.start, s.end)).collect();
9357 assert_eq!(ranges, vec![(1, 5), (8, 9)]);
9358 });
9359 }
9360
9361 #[gpui::test]
9362 fn test_alt_mouse_release_outside_editor_ends_column_selection(cx: &mut TestAppContext) {
9363 cx.update(crate::init);
9364 let view = InputView::<EditorMode>::new(cx);
9365 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9366 setup_cursors(&mut cx, &view.input, "|abcd\nabcd");
9367 let position = view.input.read_with(&cx, |state, _| {
9368 let layout = state.last_layout.as_ref().unwrap();
9369 state.last_bounds.unwrap().origin
9370 + point(layout.line_number_width + px(2.), layout.line_height * 0.5)
9371 });
9372 let modifiers = gpui::Modifiers {
9373 alt: true,
9374 ..Default::default()
9375 };
9376 cx.simulate_mouse_down(position, MouseButton::Left, modifiers);
9377 cx.simulate_mouse_up(point(px(-100.), px(-100.)), MouseButton::Left, modifiers);
9378 view.input.read_with(&cx, |state, _| {
9379 assert!(!state.selecting);
9380 assert!(state.column_select_start.is_none());
9381 });
9382 }
9383
9384 #[gpui::test]
9385 fn test_consumed_keystrokes_keep_cursor_visible(cx: &mut TestAppContext) {
9386 let view = InputView::<EditorMode>::new(cx);
9387 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9388 setup_cursors(&mut cx, &view.input, "a|b");
9389 cx.update(|window, cx| {
9390 view.input.update(cx, |state, cx| {
9391 state.focus(window, cx);
9392 state.pause_blink_cursor(cx);
9393 });
9394 });
9395 cx.run_until_parked();
9396 cx.executor()
9397 .advance_clock(std::time::Duration::from_millis(300));
9398 cx.run_until_parked();
9399 view.input.read_with(&cx, |state, cx| {
9400 assert!(!state.blink_cursor.read(cx).visible());
9401 });
9402 for _ in 0..5 {
9404 #[cfg(target_os = "macos")]
9405 cx.simulate_keystrokes("cmd-c");
9406 #[cfg(not(target_os = "macos"))]
9407 cx.simulate_keystrokes("ctrl-c");
9408 cx.run_until_parked();
9409 cx.executor()
9410 .advance_clock(std::time::Duration::from_millis(200));
9411 cx.run_until_parked();
9412 view.input.read_with(&cx, |state, cx| {
9413 assert!(state.blink_cursor.read(cx).visible());
9414 });
9415 }
9416 }
9417
9418 #[gpui::test]
9419 fn test_mouse_down_shows_cursor_like_a_cursor_move(cx: &mut TestAppContext) {
9420 cx.update(crate::init);
9421 let view = InputView::<EditorMode>::new(cx);
9422 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9423 setup_cursors(&mut cx, &view.input, "abcd|efgh");
9424 let position = |offset: Pixels, cx: &mut VisualTestContext| {
9425 view.input.read_with(cx, |state, _| {
9426 let layout = state.last_layout.as_ref().unwrap();
9427 state.last_bounds.unwrap().origin
9428 + point(layout.line_number_width + offset, layout.line_height * 0.5)
9429 })
9430 };
9431 let visible = |cx: &mut VisualTestContext| {
9432 view.input
9433 .read_with(cx, |state, cx| state.blink_cursor.read(cx).visible())
9434 };
9435
9436 let first = position(px(2.), &mut cx);
9439 cx.simulate_mouse_down(first, MouseButton::Left, Default::default());
9440 assert!(visible(&mut cx));
9441 cx.simulate_mouse_up(first, MouseButton::Left, Default::default());
9442 cx.run_until_parked();
9443
9444 cx.executor()
9447 .advance_clock(std::time::Duration::from_millis(400));
9448 cx.run_until_parked();
9449 assert!(!visible(&mut cx));
9450 let second = position(px(30.), &mut cx);
9451 cx.simulate_mouse_down(second, MouseButton::Left, Default::default());
9452 assert!(visible(&mut cx));
9453 cx.executor()
9454 .advance_clock(std::time::Duration::from_millis(250));
9455 cx.run_until_parked();
9456 assert!(visible(&mut cx));
9457 cx.simulate_mouse_up(second, MouseButton::Left, Default::default());
9458 }
9459
9460 #[gpui::test]
9461 fn test_multi_cursor_actions_reveal_hidden_carets(cx: &mut TestAppContext) {
9462 let view = InputView::<EditorMode>::new(cx);
9463 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9464 setup_cursors(&mut cx, &view.input, "ab\na|b\nab");
9465 for action in 0..6 {
9468 cx.update(|_, cx| {
9469 view.input.update(cx, |state, cx| {
9470 state.blink_cursor = cx.new(|_| BlinkCursor::new());
9471 state.blink_cursor.update(cx, |cursor, cx| cursor.start(cx));
9472 });
9473 });
9474 cx.run_until_parked();
9475 cx.executor()
9476 .advance_clock(std::time::Duration::from_millis(500));
9477 cx.run_until_parked();
9478 view.input.read_with(&cx, |state, cx| {
9479 assert!(!state.blink_cursor.read(cx).visible(), "action {action}");
9480 });
9481
9482 cx.update(|window, cx| {
9483 view.input.update(cx, |state, cx| {
9484 match action {
9485 0 => state.add_cursor_above(&AddCursorAbove, window, cx),
9486 1 => state.add_cursor_below(&AddCursorBelow, window, cx),
9487 2 => state.select_up(&SelectUp, window, cx),
9488 3 => state.select_down(&SelectDown, window, cx),
9489 4 => state.replace_text_in_range(None, "x", window, cx),
9490 _ => state.backspace(&Backspace, window, cx),
9491 }
9492 assert!(state.blink_cursor.read(cx).visible(), "action {action}");
9493 });
9494 });
9495 }
9496 }
9497
9498 #[gpui::test]
9499 fn test_multi_cursor_keyboard_dispatch(cx: &mut TestAppContext) {
9500 let view = InputView::<EditorMode>::new(cx);
9501 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9502 setup_cursors(&mut cx, &view.input, "ab\na|b\nab");
9503 cx.update(|window, cx| {
9504 view.input.update(cx, |state, cx| state.focus(window, cx));
9505 });
9506 #[cfg(target_os = "macos")]
9507 cx.simulate_keystrokes("cmd-alt-up");
9508 #[cfg(target_os = "windows")]
9509 cx.simulate_keystrokes("ctrl-alt-up");
9510 #[cfg(not(any(target_os = "macos", target_os = "windows")))]
9511 cx.simulate_keystrokes("alt-shift-up");
9512 view.input
9513 .read_with(&cx, |state, _| assert_eq!(state.selections.len(), 2));
9514 #[cfg(target_os = "macos")]
9515 cx.simulate_keystrokes("cmd-alt-down");
9516 #[cfg(target_os = "windows")]
9517 cx.simulate_keystrokes("ctrl-alt-down");
9518 #[cfg(not(any(target_os = "macos", target_os = "windows")))]
9519 cx.simulate_keystrokes("alt-shift-down");
9520 view.input
9521 .read_with(&cx, |state, _| assert_eq!(state.selections.len(), 3));
9522 cx.simulate_keystrokes("x");
9523 assert_cursors(&mut cx, &view.input, "ax|b\nax|b\nax|b");
9524 }
9525
9526 #[gpui::test]
9527 fn test_multi_cursor_platform_word_selection_dispatch(cx: &mut TestAppContext) {
9528 let view = InputView::<EditorMode>::new(cx);
9529 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9530 setup_cursors(&mut cx, &view.input, "one |two\none |two");
9531 cx.update(|window, cx| {
9532 view.input.update(cx, |state, cx| state.focus(window, cx));
9533 });
9534 #[cfg(any(target_os = "macos", target_os = "linux"))]
9535 cx.simulate_keystrokes("alt-shift-right");
9536 #[cfg(not(any(target_os = "macos", target_os = "linux")))]
9537 cx.simulate_keystrokes("ctrl-shift-right");
9538 cx.simulate_keystrokes("x");
9539 assert_cursors(&mut cx, &view.input, "one x|\none x|");
9540 setup_cursors(&mut cx, &view.input, "one two|\none two|");
9541 #[cfg(any(target_os = "macos", target_os = "linux"))]
9542 cx.simulate_keystrokes("alt-shift-left");
9543 #[cfg(not(any(target_os = "macos", target_os = "linux")))]
9544 cx.simulate_keystrokes("ctrl-shift-left");
9545 cx.simulate_keystrokes("x");
9546 assert_cursors(&mut cx, &view.input, "one x|\none x|");
9547 }
9548
9549 #[cfg(not(target_os = "macos"))]
9550 #[gpui::test]
9551 fn test_multi_cursor_horizontal_selection_dispatch(cx: &mut TestAppContext) {
9552 let view = InputView::<EditorMode>::new(cx);
9553 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9554 setup_cursors(&mut cx, &view.input, "ab\na|b");
9555 cx.update(|window, cx| {
9556 view.input.update(cx, |state, cx| state.focus(window, cx));
9557 });
9558 #[cfg(target_os = "windows")]
9559 cx.simulate_keystrokes("ctrl-alt-up");
9560 #[cfg(not(target_os = "windows"))]
9561 cx.simulate_keystrokes("alt-shift-up");
9562 #[cfg(target_os = "linux")]
9563 cx.simulate_keystrokes("shift-right");
9564 #[cfg(not(target_os = "linux"))]
9565 cx.simulate_keystrokes("alt-shift-right");
9566 view.input.read_with(&cx, |state, _| {
9567 assert_eq!(
9568 state
9569 .selections
9570 .iter()
9571 .map(|s| s.start..s.end)
9572 .collect::<Vec<_>>(),
9573 vec![4..5, 1..2]
9574 );
9575 });
9576 #[cfg(target_os = "linux")]
9577 cx.simulate_keystrokes("shift-left shift-left");
9578 #[cfg(not(target_os = "linux"))]
9579 cx.simulate_keystrokes("alt-shift-left alt-shift-left");
9580 view.input.read_with(&cx, |state, _| {
9581 assert_eq!(
9582 state
9583 .selections
9584 .iter()
9585 .map(|s| s.start..s.end)
9586 .collect::<Vec<_>>(),
9587 vec![3..4, 0..1]
9588 );
9589 });
9590 cx.simulate_keystrokes("x");
9591 assert_cursors(&mut cx, &view.input, "x|b\nx|b");
9592 }
9593
9594 #[gpui::test]
9595 fn test_multi_cursor_alt_click_dispatch(cx: &mut TestAppContext) {
9596 cx.update(crate::init);
9597 let view = InputView::<EditorMode>::new(cx);
9598 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9599 setup_cursors(&mut cx, &view.input, "a|b\nab\nab");
9600 cx.update(|window, cx| {
9601 view.input.update(cx, |state, cx| state.focus(window, cx));
9602 });
9603 let position = view.input.read_with(&cx, |state, _| {
9604 let bounds = state.last_bounds.unwrap();
9605 let layout = state.last_layout.as_ref().unwrap();
9606 bounds.origin + point(layout.line_number_width + px(2.), layout.line_height * 1.5)
9607 });
9608 cx.simulate_click(
9609 position,
9610 gpui::Modifiers {
9611 alt: true,
9612 ..Default::default()
9613 },
9614 );
9615 view.input
9616 .read_with(&cx, |state, _| assert_eq!(state.selections.len(), 2));
9617 }
9618
9619 #[gpui::test]
9620 fn test_add_selection(cx: &mut TestAppContext) {
9621 let view = InputView::<EditorMode>::new(cx);
9622 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9623 cx.update(|window, cx| {
9624 view.input.update(cx, |state, cx| {
9625 state.focus(window, cx);
9626 state.set_value("foo bar foo", window, cx);
9627 state.set_selected_range(0..3, cx);
9628 state.add_selection(8..11, cx);
9630 state.add_selection(0..3, cx);
9632 });
9633 });
9634 view.input.read_with(&cx, |state, _| {
9635 let mut ranges: Vec<Range<usize>> =
9636 state.selections.iter().map(|s| s.start..s.end).collect();
9637 ranges.sort_by_key(|r| r.start);
9638 assert_eq!(ranges, vec![0..3, 8..11]);
9639 });
9640 cx.simulate_keystrokes("x");
9642 view.input.read_with(&cx, |state, _| {
9643 assert_eq!(state.text.to_string(), "x bar x")
9644 });
9645 }
9646
9647 #[gpui::test]
9648 fn test_add_selection_preserves_crlf_boundaries(cx: &mut TestAppContext) {
9649 let view = InputView::build_textarea(cx, |state| state.default_value("a\r\nb"));
9650 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9651 cx.update(|window, cx| {
9652 view.input.update(cx, |state, cx| {
9653 state.focus(window, cx);
9654 state.set_selected_range(4..4, cx);
9655 state.add_selection(2..2, cx);
9656 let ranges: Vec<_> = state.selections.iter().map(|s| s.start..s.end).collect();
9657 assert_eq!(ranges, vec![4..4, 1..1]);
9658 });
9659 });
9660 cx.simulate_keystrokes("x");
9661 view.input.read_with(&cx, |state, _| {
9662 assert_eq!(state.value(), "ax\r\nbx");
9663 });
9664 cx.update(|window, cx| {
9665 view.input.update(cx, |state, cx| {
9666 state.set_value("a\r\nb", window, cx);
9667 state.set_selected_range(4..4, cx);
9668 state.add_selection(2..3, cx);
9669 state.cut(&Cut, window, cx);
9670 assert_eq!(cx.read_from_clipboard().unwrap().text().unwrap(), "\r\n");
9671 assert_eq!(state.value(), "ab");
9672 });
9673 });
9674 }
9675
9676 #[gpui::test]
9677 fn test_add_selection_merges_overlaps_before_copy_and_cut(cx: &mut TestAppContext) {
9678 let view = InputView::<EditorMode>::new(cx);
9679 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9680 cx.update(|window, cx| {
9681 view.input.update(cx, |state, cx| {
9682 for additions in [vec![2..5], vec![0..5], vec![4..5, 2..4]] {
9685 state.set_value("abcdef", window, cx);
9686 state.set_selected_range(0..3, cx);
9687 let active_id = state.active_selection().id;
9688 for range in additions {
9689 state.add_selection(range, cx);
9690 }
9691 assert_eq!(state.active_selection().id, active_id);
9692 assert_eq!(state.selected_range(), 0..5);
9693 assert_eq!(state.selections.len(), 1);
9694 state.copy(&Copy, window, cx);
9695 assert_eq!(cx.read_from_clipboard().unwrap().text().unwrap(), "abcde");
9696 state.cut(&Cut, window, cx);
9697 assert_eq!(cx.read_from_clipboard().unwrap().text().unwrap(), "abcde");
9698 assert_eq!(state.value(), "f");
9699 state.undo(&Undo, window, cx);
9700 assert_eq!(state.value(), "abcdef");
9701 assert_eq!(state.selected_range(), 0..5);
9702 }
9703 });
9704 });
9705 }
9706
9707 #[gpui::test]
9708 fn test_add_selection_clears_inline_completion(cx: &mut TestAppContext) {
9709 use crate::input::CompletionProvider;
9710 use gpui::Task;
9711 use lsp_types::{
9712 CompletionContext, CompletionResponse, InlineCompletionContext, InlineCompletionItem,
9713 InlineCompletionResponse,
9714 };
9715 use std::time::Duration;
9716
9717 struct Provider(Rc<Cell<usize>>);
9718
9719 impl CompletionProvider for Provider {
9720 fn is_completion_trigger(&self, _: usize, _: &str, _: &mut App) -> bool {
9721 false
9722 }
9723
9724 fn completions(
9725 &self,
9726 _: &Rope,
9727 _: usize,
9728 _: CompletionContext,
9729 _: &mut Window,
9730 _: &mut App,
9731 ) -> Task<anyhow::Result<CompletionResponse>> {
9732 Task::ready(Ok(CompletionResponse::Array(vec![])))
9733 }
9734
9735 fn inline_completion(
9736 &self,
9737 _: &Rope,
9738 _: usize,
9739 _: InlineCompletionContext,
9740 _: &mut Window,
9741 _: &mut App,
9742 ) -> Task<anyhow::Result<InlineCompletionResponse>> {
9743 self.0.set(self.0.get() + 1);
9744 Task::ready(Ok(InlineCompletionResponse::Array(vec![])))
9745 }
9746 }
9747
9748 let view = InputView::<EditorMode>::new(cx);
9749 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9750 let calls = Rc::new(Cell::new(0));
9751 cx.update(|window, cx| {
9752 view.input.update(cx, |state, cx| {
9753 state.focus(window, cx);
9754 state.set_value("aa\naa", window, cx);
9755 state.set_selected_range(2..2, cx);
9756 state.extras.inline_completion.item = Some(InlineCompletionItem {
9757 insert_text: "suggestion".into(),
9758 filter_text: None,
9759 range: None,
9760 command: None,
9761 insert_text_format: None,
9762 });
9763 state.add_selection(5..5, cx);
9764 assert!(!state.has_inline_completion());
9765 state.indent_inline(&IndentInline, window, cx);
9766 assert_eq!(state.value(), "aa \naa ");
9767 assert_eq!(state.selections.len(), 2);
9768
9769 state.set_value("aa\naa", window, cx);
9772 state.set_selected_range(2..2, cx);
9773 state.extras.lsp.completion_provider = Some(Rc::new(Provider(calls.clone())));
9774 state.schedule_inline_completion(window, cx);
9775 });
9776 });
9777 cx.run_until_parked();
9778 cx.update(|_, cx| {
9779 view.input
9780 .update(cx, |state, cx| state.add_selection(5..5, cx));
9781 });
9782 cx.executor().advance_clock(Duration::from_secs(1));
9783 cx.run_until_parked();
9784 assert_eq!(calls.get(), 0);
9785 view.input.read_with(&cx, |state, _| {
9786 assert!(!state.has_inline_completion());
9787 assert_eq!(state.selections.len(), 2);
9788 assert_eq!(state.value(), "aa\naa");
9789 });
9790 }
9791
9792 #[gpui::test]
9793 fn test_word_delete_undo_restores_caret(cx: &mut TestAppContext) {
9794 let view = multi_line(cx);
9795 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9796 setup_cursors(&mut cx, &view.input, "hello|");
9797 cx.update(|window, cx| {
9798 view.input.update(cx, |state, cx| {
9799 state.delete_previous_word(&DeleteToPreviousWordStart, window, cx);
9800 state.undo(&Undo, window, cx);
9801 assert_eq!(state.selected_range(), 5..5);
9802 });
9803 });
9804 }
9805
9806 #[gpui::test]
9807 fn test_merged_delete_undo_restores_all_carets(cx: &mut TestAppContext) {
9808 let view = multi_line(cx);
9809 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9810 setup_cursors(&mut cx, &view.input, "a|b|c");
9811 cx.update(|window, cx| {
9812 view.input.update(cx, |state, cx| {
9813 state.backspace(&Backspace, window, cx);
9814 state.undo(&Undo, window, cx);
9815 });
9816 });
9817 assert_cursors(&mut cx, &view.input, "a|b|c");
9818 }
9819
9820 #[gpui::test]
9821 fn test_shift_end_respects_soft_wrap_end(cx: &mut TestAppContext) {
9822 let view = InputView::<EditorMode>::new(cx);
9823 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9824 cx.update(|window, cx| {
9825 view.input.update(cx, |state, cx| {
9826 state.set_value("abcdef ".repeat(30), window, cx);
9827 state.display_map.on_layout_changed(Some(px(60.)), cx);
9828 let line = state.display_map.line(0).unwrap();
9829 assert!(line.wrapped_lines.len() > 1);
9830 let boundary = line.wrapped_lines[0].end;
9831 state.move_to_with_affinity(boundary, None, true, cx);
9832 state.select_to_end_of_line(&SelectToEndOfLine, window, cx);
9833 assert_eq!(state.selected_range(), boundary..state.text.len());
9834 });
9835 });
9836 }
9837
9838 #[gpui::test]
9839 fn test_outdent_unindented_unicode_is_unchanged(cx: &mut TestAppContext) {
9840 let view = multi_line(cx);
9841 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9842 setup_cursors(&mut cx, &view.input, "|你好\n|世界");
9843 cx.update(|window, cx| {
9844 view.input.update(cx, |state, cx| {
9845 state.outdent(false, window, cx);
9846 state.outdent(true, window, cx);
9847 });
9848 });
9849 assert_cursors(&mut cx, &view.input, "|你好\n|世界");
9850 }
9851
9852 #[gpui::test]
9853 fn test_column_selection_stays_on_unicode_boundaries(cx: &mut TestAppContext) {
9854 let view = multi_line(cx);
9855 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9856 setup_cursors(&mut cx, &view.input, "ab\n你好\ncd");
9857 cx.update(|window, cx| {
9858 view.input.update(cx, |state, cx| {
9859 state.build_columnar_selection(
9860 ColumnarPoint::new(1, 0),
9861 ColumnarPoint::new(11, 0),
9862 cx,
9863 );
9864 let text = state.value();
9865 for sel in state.selections.iter() {
9866 assert!(text.is_char_boundary(sel.start));
9867 assert!(text.is_char_boundary(sel.end));
9868 }
9869 state.replace_text_in_range(None, "X", window, cx);
9870 });
9871 });
9872 }
9873
9874 #[gpui::test]
9875 fn test_editor_decorations_follow_typing(cx: &mut TestAppContext) {
9876 let view = InputView::<EditorMode>::new(cx);
9877 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9878 cx.update(|window, cx| {
9879 view.input.update(cx, |state, cx| {
9880 state.set_value("abc def", window, cx);
9881 state.create_decorations_collection(
9882 vec![crate::input::TextDecoration::new(
9883 4..7,
9884 gpui::HighlightStyle::default(),
9885 )],
9886 cx,
9887 );
9888 state.set_selected_range(0..0, cx);
9889 state.replace_text_in_range(None, "X", window, cx);
9890 let layers = state.extras.decoration_layers();
9891 assert_eq!(layers.into_iter().flatten().next().unwrap().range, 5..8);
9892 });
9893 });
9894 }
9895
9896 #[gpui::test]
9897 fn test_backspace_preserves_escaped_quote_terminator(cx: &mut TestAppContext) {
9898 let view = InputView::<EditorMode>::new(cx);
9899 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9900 setup_cursors(&mut cx, &view.input, r#"{"s":"\"|"}"#);
9901 cx.update(|window, cx| {
9902 view.input.update(cx, |state, cx| {
9903 set_test_syntax_provider(std::rc::Rc::new(StringAllProvider), cx);
9904 state.backspace(&Backspace, window, cx);
9905 });
9906 });
9907 assert_cursors(&mut cx, &view.input, r#"{"s":"\|"}"#);
9908 }
9909
9910 #[gpui::test]
9911 fn test_ordinary_typing_does_not_query_syntax(cx: &mut TestAppContext) {
9912 struct UnexpectedQuery;
9913 impl crate::input::SyntaxContextProvider for UnexpectedQuery {
9914 fn context_at(&self, _: &ropey::Rope, _: usize) -> crate::input::SyntaxContext {
9915 panic!("ordinary typing must not query syntax");
9916 }
9917 }
9918 let view = InputView::<EditorMode>::new(cx);
9919 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9920 setup_cursors(&mut cx, &view.input, "|");
9921 cx.update(|window, cx| {
9922 view.input.update(cx, |state, cx| {
9923 set_test_syntax_provider(std::rc::Rc::new(UnexpectedQuery), cx);
9924 for c in ["a", "b", "c"] {
9925 state.replace_text_in_range(None, c, window, cx);
9926 }
9927 });
9928 });
9929 assert_cursors(&mut cx, &view.input, "abc|");
9930 }
9931
9932 #[gpui::test]
9933 fn test_auto_close_is_atomic_at_history_limit(cx: &mut TestAppContext) {
9934 let view = InputView::<EditorMode>::new(cx);
9935 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9936 setup_cursors(&mut cx, &view.input, "|");
9937 cx.update(|window, cx| {
9938 view.input.update(cx, |state, cx| {
9939 for _ in 0..999 {
9940 state.replace_text_in_range(None, "a", window, cx);
9941 }
9942 state.replace_text_in_range(None, "(", window, cx);
9943 state.undo(&Undo, window, cx);
9944 assert!(!state.text.to_string().contains('('));
9945 assert!(!state.text.to_string().contains(')'));
9946 state.redo(&Redo, window, cx);
9947 });
9948 });
9949 assert_cursors(&mut cx, &view.input, &format!("{}(|)", "a".repeat(999)));
9950 }
9951
9952 #[gpui::test]
9953 fn test_comment_closer_is_inserted_literally(cx: &mut TestAppContext) {
9954 let view = InputView::<EditorMode>::new(cx);
9955 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9956 setup_cursors(&mut cx, &view.input, "// (|)");
9957 cx.update(|window, cx| {
9958 view.input.update(cx, |state, cx| {
9959 set_test_syntax_provider(std::rc::Rc::new(CommentAllProvider), cx);
9960 state.replace_text_in_range(None, ")", window, cx);
9961 });
9962 });
9963 assert_cursors(&mut cx, &view.input, "// ()|)");
9964 }
9965
9966 #[gpui::test]
9967 fn test_explicit_range_skip_does_not_delete_existing_closer_on_undo(cx: &mut TestAppContext) {
9968 let view = InputView::<EditorMode>::new(cx);
9969 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9970 setup_cursors(&mut cx, &view.input, "(a|)");
9971 let before = view.input.read_with(&cx, |state, _| state.text.to_string());
9972 cx.update(|window, cx| {
9973 view.input.update(cx, |state, cx| {
9974 state.replace_text_in_range(Some(2..2), ")", window, cx);
9975 });
9976 });
9977 assert_cursors(&mut cx, &view.input, "(a)|");
9978 cx.update(|window, cx| {
9979 view.input
9980 .update(cx, |state, cx| state.undo(&Undo, window, cx));
9981 });
9982 view.input.read_with(&cx, |state, _| {
9983 assert_eq!(
9984 state.text.to_string(),
9985 before,
9986 "skip must not remove an existing closer on undo"
9987 );
9988 });
9989 }
9990
9991 #[gpui::test]
9992 fn test_pair_redo_restores_interior_cursor(cx: &mut TestAppContext) {
9993 let view = InputView::<EditorMode>::new(cx);
9994 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9995 setup_cursors(&mut cx, &view.input, "|");
9996 cx.update(|window, cx| {
9997 view.input.update(cx, |state, cx| {
9998 state.replace_text_in_range(None, "(", window, cx);
9999 });
10000 });
10001 assert_cursors(&mut cx, &view.input, "(|)");
10002 cx.update(|window, cx| {
10003 view.input.update(cx, |state, cx| {
10004 state.undo(&Undo, window, cx);
10005 state.redo(&Redo, window, cx);
10006 });
10007 });
10008 assert_cursors(&mut cx, &view.input, "(|)");
10009 }
10010
10011 #[gpui::test]
10012 fn test_pair_enter_redo_restores_interior_cursor(cx: &mut TestAppContext) {
10013 let view = InputView::<EditorMode>::new(cx);
10014 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10015 setup_cursors(&mut cx, &view.input, "{|}");
10016 cx.update(|window, cx| {
10017 view.input.update(cx, |state, cx| {
10018 state.enter(
10019 &Enter {
10020 secondary: false,
10021 shift: false,
10022 },
10023 window,
10024 cx,
10025 );
10026 });
10027 });
10028 assert_cursors(&mut cx, &view.input, "{\n |\n}");
10029 cx.update(|window, cx| {
10030 view.input.update(cx, |state, cx| {
10031 state.undo(&Undo, window, cx);
10032 });
10033 });
10034 assert_cursors(&mut cx, &view.input, "{|}");
10035 cx.update(|window, cx| {
10036 view.input
10037 .update(cx, |state, cx| state.redo(&Redo, window, cx));
10038 });
10039 assert_cursors(&mut cx, &view.input, "{\n |\n}");
10040 }
10041
10042 #[gpui::test]
10043 fn test_auto_close_parens(cx: &mut TestAppContext) {
10044 let view = InputView::<EditorMode>::new(cx);
10045 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10046 setup_cursors(&mut cx, &view.input, "|");
10047 cx.update(|window, cx| {
10048 view.input.update(cx, |state, cx| {
10049 state.replace_text_in_range(None, "(", window, cx);
10050 });
10051 });
10052 assert_cursors(&mut cx, &view.input, "(|)");
10053 cx.update(|window, cx| {
10055 view.input.update(cx, |state, cx| {
10056 state.replace_text_in_range(None, "x", window, cx);
10057 });
10058 });
10059 assert_cursors(&mut cx, &view.input, "(x|)");
10060 }
10061
10062 #[gpui::test]
10063 fn test_auto_close_skip_over_closer(cx: &mut TestAppContext) {
10064 let view = InputView::<EditorMode>::new(cx);
10065 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10066 setup_cursors(&mut cx, &view.input, "(|)");
10067 cx.update(|window, cx| {
10068 view.input.update(cx, |state, cx| {
10069 state.replace_text_in_range(None, ")", window, cx);
10070 });
10071 });
10072 assert_cursors(&mut cx, &view.input, "()|");
10074 view.input.read_with(&cx, |state, _| {
10076 assert!(state.active_selection().is_empty());
10077 });
10078 }
10079
10080 #[gpui::test]
10081 fn test_auto_close_no_pair_inside_word(cx: &mut TestAppContext) {
10082 let view = InputView::<EditorMode>::new(cx);
10083 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10084 setup_cursors(&mut cx, &view.input, "don|");
10085 cx.update(|window, cx| {
10086 view.input.update(cx, |state, cx| {
10087 state.replace_text_in_range(None, "'", window, cx);
10088 });
10089 });
10090 assert_cursors(&mut cx, &view.input, "don'|");
10092 }
10093
10094 #[gpui::test]
10095 fn test_auto_close_quote_after_cjk(cx: &mut TestAppContext) {
10096 let view = InputView::<EditorMode>::new(cx);
10097 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10098 setup_cursors(&mut cx, &view.input, "中|");
10101 cx.update(|window, cx| {
10102 view.input.update(cx, |state, cx| {
10103 state.replace_text_in_range(None, "'", window, cx);
10104 });
10105 });
10106 assert_cursors(&mut cx, &view.input, "中'|");
10107 }
10108
10109 #[gpui::test]
10110 fn test_auto_close_closer_skips_at_string_end(cx: &mut TestAppContext) {
10111 let view = InputView::<EditorMode>::new(cx);
10112 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10113 setup_cursors(&mut cx, &view.input, "\"hello|\"");
10115 cx.update(|window, cx| {
10116 view.input.update(cx, |state, cx| {
10117 state.replace_text_in_range(None, "\"", window, cx);
10118 });
10119 });
10120 assert_cursors(&mut cx, &view.input, "\"hello\"|");
10121 view.input.read_with(&cx, |state, _| {
10122 assert!(state.active_selection().is_empty());
10123 });
10124 }
10125
10126 #[gpui::test]
10127 fn test_backspace_pair_with_cjk_prefix(cx: &mut TestAppContext) {
10128 let view = InputView::<EditorMode>::new(cx);
10129 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10130 setup_cursors(&mut cx, &view.input, "中(|)abc");
10132 cx.update(|window, cx| {
10133 view.input
10134 .update(cx, |state, cx| state.backspace(&Backspace, window, cx));
10135 });
10136 assert_cursors(&mut cx, &view.input, "中|abc");
10137 }
10138
10139 #[gpui::test]
10140 fn test_enter_split_respects_smart_indent_off(cx: &mut TestAppContext) {
10141 let view = InputView::<EditorMode>::new(cx);
10142 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10143 setup_cursors(&mut cx, &view.input, "{|}");
10144 cx.update(|window, cx| {
10145 view.input.update(cx, |state, cx| {
10146 state.set_smart_indent(false, window, cx);
10147 state.enter(
10148 &Enter {
10149 secondary: false,
10150 shift: false,
10151 },
10152 window,
10153 cx,
10154 );
10155 });
10156 });
10157 assert_cursors(&mut cx, &view.input, "{\n|}");
10159 }
10160
10161 #[gpui::test]
10162 fn test_enter_split_is_independent_of_auto_close(cx: &mut TestAppContext) {
10163 let view = InputView::<EditorMode>::new(cx);
10164 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10165 setup_cursors(&mut cx, &view.input, "{|}");
10166 cx.update(|window, cx| {
10167 view.input.update(cx, |state, cx| {
10168 state.set_auto_close(false, window, cx);
10169 state.enter(
10170 &Enter {
10171 secondary: false,
10172 shift: false,
10173 },
10174 window,
10175 cx,
10176 );
10177 });
10178 });
10179 assert_cursors(&mut cx, &view.input, "{\n |\n}");
10181 }
10182
10183 #[gpui::test]
10184 fn test_language_config_applies_before_render_and_on_language_change(cx: &mut TestAppContext) {
10185 use crate::input::{AutoClosingPair, set_language_config};
10186 let view = InputView::<EditorMode>::new(cx);
10187 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10188 cx.update(|window, cx| {
10189 set_language_config(
10190 "alpha",
10191 LanguageConfig::default().auto_closing_pairs([AutoClosingPair::new("«", "»")]),
10192 cx,
10193 );
10194 set_language_config(
10195 "beta",
10196 LanguageConfig::default().auto_closing_pairs([AutoClosingPair::new("‹", "›")]),
10197 cx,
10198 );
10199 let first = cx.new(|cx| EditorState::new(window, cx).language("ALPHA"));
10200 let second = cx.new(|cx| EditorState::new(window, cx).language("beta"));
10201 first.update(cx, |state, cx| {
10202 state.replace_text_in_range(None, "«", window, cx);
10203 assert_eq!(state.text().to_string(), "«»");
10204 state.set_value("", window, cx);
10205 state.set_smart_indent(false, window, cx);
10206 state.set_highlighter("beta", cx);
10207 assert!(!state.mode.is_smart_indent());
10208 state.replace_text_in_range(None, "‹", window, cx);
10209 assert_eq!(state.text().to_string(), "‹›");
10210 state.set_value("", window, cx);
10211 state.set_highlighter("unknown", cx);
10212 state.replace_text_in_range(None, "(", window, cx);
10213 assert_eq!(state.text().to_string(), "()");
10214 });
10215 second.update(cx, |state, cx| {
10216 state.replace_text_in_range(None, "‹", window, cx);
10217 assert_eq!(state.text().to_string(), "‹›");
10218 });
10219 });
10220 }
10221
10222 #[gpui::test]
10223 fn test_language_service_replacement_updates_syntax_without_render(cx: &mut TestAppContext) {
10224 use crate::input::{LanguageProvider, SyntaxContextProvider, set_language_provider};
10225 struct StringLanguages(Rc<Cell<usize>>);
10226 impl LanguageProvider for StringLanguages {
10227 fn syntax_context_provider(&self, _: &str) -> Option<Rc<dyn SyntaxContextProvider>> {
10228 self.0.set(self.0.get() + 1);
10229 Some(Rc::new(StringAllProvider))
10230 }
10231 }
10232 struct CodeLanguages;
10233 impl LanguageProvider for CodeLanguages {}
10234 let view = InputView::<EditorMode>::new(cx);
10235 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10236 cx.update(|window, cx| {
10237 let creations = Rc::new(Cell::new(0));
10238 set_language_provider(Rc::new(StringLanguages(creations.clone())), cx);
10239 view.input.update(cx, |state, cx| {
10240 state.replace_text_in_range(None, "(", window, cx);
10241 state.replace_text_in_range(None, "[", window, cx);
10242 assert_eq!(state.text().to_string(), "([");
10243 assert_eq!(
10244 creations.get(),
10245 1,
10246 "retain the document provider between edits"
10247 );
10248 });
10249 set_language_provider(Rc::new(CodeLanguages), cx);
10250 view.input.update(cx, |state, cx| {
10251 state.replace_text_in_range(None, "{", window, cx);
10252 assert_eq!(state.text().to_string(), "([{}");
10253 });
10254 });
10255 }
10256
10257 #[gpui::test]
10258 fn test_language_config_updates_existing_editors(cx: &mut TestAppContext) {
10259 use crate::input::{AutoClosingPair, set_language_config};
10260 let view = InputView::<EditorMode>::new(cx);
10261 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10262 setup_cursors(&mut cx, &view.input, "|");
10263 cx.update(|window, cx| {
10264 view.input.update(cx, |state, cx| {
10265 state.set_highlighter("CUSTOM", cx);
10266 state.set_auto_close(false, window, cx);
10267 state.set_smart_indent(false, window, cx);
10268 });
10269 set_language_config(
10270 "custom",
10271 LanguageConfig::default().auto_closing_pairs([AutoClosingPair::new("«", "»")]),
10272 cx,
10273 );
10274 set_language_config("other", LanguageConfig::default(), cx);
10276 view.input.update(cx, |state, cx| {
10277 assert!(!state.mode.is_auto_close());
10278 assert!(!state.mode.is_smart_indent());
10279 state.set_auto_close(true, window, cx);
10280 state.replace_text_in_range(None, "«", window, cx);
10281 });
10282 });
10283 assert_cursors(&mut cx, &view.input, "«|»");
10284 }
10285
10286 #[gpui::test]
10287 fn test_auto_close_before_is_language_configurable(cx: &mut TestAppContext) {
10288 let view = InputView::<EditorMode>::new(cx);
10289 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10290 setup_cursors(&mut cx, &view.input, "|word");
10291 cx.update(|window, cx| {
10292 view.input.update(cx, |state, cx| {
10293 state.replace_text_in_range(None, "(", window, cx)
10294 });
10295 });
10296 assert_cursors(&mut cx, &view.input, "(|word");
10297 setup_cursors(&mut cx, &view.input, "|word");
10298 cx.update(|window, cx| {
10299 view.input.update(cx, |state, cx| {
10300 crate::input::set_language_config(
10301 state.language_name(),
10302 LanguageConfig::default().auto_close_before("w"),
10303 cx,
10304 );
10305 state.replace_text_in_range(None, "(", window, cx);
10306 });
10307 });
10308 assert_cursors(&mut cx, &view.input, "(|)word");
10309 }
10310
10311 #[gpui::test]
10312 fn test_pair_context_restrictions_are_per_pair(cx: &mut TestAppContext) {
10313 use crate::input::{AutoClosingPair, SyntaxContext};
10314 let view = InputView::<EditorMode>::new(cx);
10315 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10316 setup_cursors(&mut cx, &view.input, "|");
10317 cx.update(|window, cx| {
10318 view.input.update(cx, |state, cx| {
10319 set_test_syntax_provider(std::rc::Rc::new(CommentAllProvider), cx);
10320 crate::input::set_language_config(
10321 state.language_name(),
10322 LanguageConfig::default().auto_closing_pairs([
10323 AutoClosingPair::new("(", ")").not_in([SyntaxContext::String]),
10324 AutoClosingPair::new("[", "]").not_in([SyntaxContext::Comment]),
10325 ]),
10326 cx,
10327 );
10328 state.replace_text_in_range(None, "(", window, cx);
10329 state.replace_text_in_range(None, "[", window, cx);
10330 });
10331 });
10332 assert_cursors(&mut cx, &view.input, "([|)");
10333 }
10334
10335 #[gpui::test]
10336 fn test_multichar_delimiters_insert_delete_and_skip(cx: &mut TestAppContext) {
10337 use crate::input::{AutoClosingPair, SyntaxContext};
10338 struct BlockComment;
10339 impl crate::input::SyntaxContextProvider for BlockComment {
10340 fn context_at(&self, text: &ropey::Rope, _: usize) -> SyntaxContext {
10341 if text.to_string().contains("/*") {
10342 SyntaxContext::Comment
10343 } else {
10344 SyntaxContext::Code
10345 }
10346 }
10347 }
10348 let view = InputView::<EditorMode>::new(cx);
10349 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10350 setup_cursors(&mut cx, &view.input, "|");
10351 cx.update(|window, cx| {
10352 view.input.update(cx, |state, cx| {
10353 set_test_syntax_provider(std::rc::Rc::new(BlockComment), cx);
10354 crate::input::set_language_config(
10355 state.language_name(),
10356 LanguageConfig::default()
10357 .auto_closing_pairs([AutoClosingPair::new("/*", "*/")
10358 .not_in([SyntaxContext::String, SyntaxContext::Comment])]),
10359 cx,
10360 );
10361 state.replace_text_in_range(None, "/", window, cx);
10362 state.replace_text_in_range(None, "*", window, cx);
10363 });
10364 });
10365 assert_cursors(&mut cx, &view.input, "/*|*/");
10366 cx.update(|window, cx| {
10367 view.input
10368 .update(cx, |state, cx| state.backspace(&Backspace, window, cx));
10369 });
10370 assert_cursors(&mut cx, &view.input, "|");
10371 cx.update(|window, cx| {
10372 view.input.update(cx, |state, cx| {
10373 state.undo(&Undo, window, cx);
10374 state.replace_text_in_range(None, "*", window, cx);
10375 state.replace_text_in_range(None, "/", window, cx);
10376 });
10377 });
10378 assert_cursors(&mut cx, &view.input, "/**/|");
10379 }
10380
10381 #[gpui::test]
10382 fn test_auto_closed_pairs_fallback_and_explicit_disable(cx: &mut TestAppContext) {
10383 use crate::input::BracketPair;
10384 let view = InputView::<EditorMode>::new(cx);
10385 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10386 setup_cursors(&mut cx, &view.input, "|");
10387 cx.update(|window, cx| {
10388 view.input.update(cx, |state, cx| {
10389 let mut rules = LanguageConfig::default().brackets([BracketPair::new("«", "»")]);
10390 rules.auto_closing_pairs = None;
10391 crate::input::set_language_config(state.language_name(), rules.clone(), cx);
10392 state.replace_text_in_range(None, "«", window, cx);
10393 crate::input::set_language_config(
10394 state.language_name(),
10395 rules.auto_closing_pairs([]),
10396 cx,
10397 );
10398 state.replace_text_in_range(None, "«", window, cx);
10399 });
10400 });
10401 assert_cursors(&mut cx, &view.input, "««|»");
10402 }
10403
10404 #[gpui::test]
10405 fn test_undo_open_composition_preserves_generated_pairs(cx: &mut TestAppContext) {
10406 use crate::input::{AutoClosingPair, SyntaxContext};
10407 let view = InputView::<EditorMode>::new(cx);
10408 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10409 setup_cursors(&mut cx, &view.input, "|");
10410 cx.update(|window, cx| {
10411 view.input.update(cx, |state, cx| {
10412 crate::input::set_language_config(
10413 state.language_name(),
10414 LanguageConfig::default()
10415 .auto_closing_pairs([
10416 AutoClosingPair::new("/*", "*/").not_in([SyntaxContext::Comment])
10417 ]),
10418 cx,
10419 );
10420 state.replace_text_in_range(None, "/", window, cx);
10421 state.replace_text_in_range(None, "*", window, cx);
10422 set_test_syntax_provider(std::rc::Rc::new(CommentAllProvider), cx);
10423 state.replace_and_mark_text_in_range(None, "x", Some(1..1), window, cx);
10424 state.undo(&Undo, window, cx);
10425 state.replace_text_in_range(None, "*", window, cx);
10426 state.replace_text_in_range(None, "/", window, cx);
10427 });
10428 });
10429 assert_cursors(&mut cx, &view.input, "/**/|");
10430 }
10431
10432 #[gpui::test]
10433 fn test_undo_does_not_promote_literal_comment_delimiters(cx: &mut TestAppContext) {
10434 use crate::input::{AutoClosingPair, SyntaxContext};
10435 let view = InputView::<EditorMode>::new(cx);
10436 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10437 setup_cursors(&mut cx, &view.input, "/*|*/");
10438 cx.update(|window, cx| {
10439 view.input.update(cx, |state, cx| {
10440 set_test_syntax_provider(std::rc::Rc::new(CommentAllProvider), cx);
10441 crate::input::set_language_config(
10442 state.language_name(),
10443 LanguageConfig::default()
10444 .auto_closing_pairs([
10445 AutoClosingPair::new("/*", "*/").not_in([SyntaxContext::Comment])
10446 ]),
10447 cx,
10448 );
10449 state.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
10450 state.undo(&Undo, window, cx);
10451 state.backspace(&Backspace, window, cx);
10452 });
10453 });
10454 assert_cursors(&mut cx, &view.input, "/|*/");
10455 }
10456
10457 #[gpui::test]
10458 fn test_multiple_generated_pairs_retain_their_identity(cx: &mut TestAppContext) {
10459 use crate::input::{AutoClosingPair, SyntaxContext};
10460 struct CommentScope;
10461 impl crate::input::SyntaxContextProvider for CommentScope {
10462 fn context_at(&self, text: &ropey::Rope, offset: usize) -> SyntaxContext {
10463 if text
10464 .to_string()
10465 .find("*/")
10466 .is_some_and(|end| offset < end + 2)
10467 {
10468 SyntaxContext::Comment
10469 } else {
10470 SyntaxContext::Code
10471 }
10472 }
10473 }
10474 let view = InputView::<EditorMode>::new(cx);
10475 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10476 setup_cursors(&mut cx, &view.input, "|");
10477 cx.update(|window, cx| {
10478 view.input.update(cx, |state, cx| {
10479 set_test_syntax_provider(std::rc::Rc::new(CommentScope), cx);
10480 crate::input::set_language_config(
10481 state.language_name(),
10482 LanguageConfig::default().auto_closing_pairs([
10483 AutoClosingPair::new("/*", "*/").not_in([SyntaxContext::Comment]),
10484 AutoClosingPair::new("(", ")").not_in([SyntaxContext::Comment]),
10485 ]),
10486 cx,
10487 );
10488 state.replace_text_in_range(None, "/", window, cx);
10489 state.replace_text_in_range(None, "*", window, cx);
10490 state.set_selected_range(4..4, cx);
10491 state.replace_text_in_range(None, "(", window, cx);
10492 state.set_selected_range(2..2, cx);
10493 state.replace_text_in_range(None, "*", window, cx);
10494 state.replace_text_in_range(None, "/", window, cx);
10495 });
10496 });
10497 assert_cursors(&mut cx, &view.input, "/**/|()");
10498 cx.update(|window, cx| {
10499 view.input.update(cx, |state, cx| {
10500 state.set_selected_range(2..2, cx);
10501 state.backspace(&Backspace, window, cx);
10502 state.undo(&Undo, window, cx);
10503 state.redo(&Redo, window, cx);
10504 });
10505 });
10506 assert_cursors(&mut cx, &view.input, "|()");
10507 }
10508
10509 #[gpui::test]
10510 fn test_replacing_generated_opener_invalidates_the_pair(cx: &mut TestAppContext) {
10511 use crate::input::{AutoClosingPair, SyntaxContext};
10512 let view = InputView::<EditorMode>::new(cx);
10513 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10514 setup_cursors(&mut cx, &view.input, "|");
10515 cx.update(|window, cx| {
10516 view.input.update(cx, |state, cx| {
10517 crate::input::set_language_config(
10518 state.language_name(),
10519 LanguageConfig::default()
10520 .auto_closing_pairs([
10521 AutoClosingPair::new("/*", "*/").not_in([SyntaxContext::Comment])
10522 ]),
10523 cx,
10524 );
10525 state.replace_text_in_range(None, "/", window, cx);
10526 state.replace_text_in_range(None, "*", window, cx);
10527 state.replace_text_in_range(Some(0..2), "//", window, cx);
10528 set_test_syntax_provider(std::rc::Rc::new(CommentAllProvider), cx);
10529 state.replace_text_in_range(None, "*", window, cx);
10530 });
10531 });
10532 assert_cursors(&mut cx, &view.input, "//*|*/");
10533 }
10534
10535 #[gpui::test]
10536 fn test_configuration_preserves_compiled_regex_options(cx: &mut TestAppContext) {
10537 use crate::input::IndentationRules;
10538 let view = InputView::<EditorMode>::new(cx);
10539 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10540 setup_cursors(&mut cx, &view.input, "BEGIN|");
10541 cx.update(|window, cx| {
10542 view.input.update(cx, |state, cx| {
10543 let rules = |insensitive| {
10544 LanguageConfig::default().indentation_rules(IndentationRules::new(
10545 regex::RegexBuilder::new("begin$")
10546 .case_insensitive(insensitive)
10547 .build()
10548 .unwrap(),
10549 regex::Regex::new("^end").unwrap(),
10550 ))
10551 };
10552 crate::input::set_language_config(state.language_name(), rules(false), cx);
10553 crate::input::set_language_config(state.language_name(), rules(true), cx);
10554 state.enter(
10555 &Enter {
10556 secondary: false,
10557 shift: false,
10558 },
10559 window,
10560 cx,
10561 );
10562 });
10563 });
10564 assert_cursors(&mut cx, &view.input, "BEGIN\n |");
10565 }
10566
10567 #[gpui::test]
10568 fn test_indent_patterns_are_applied_on_enter(cx: &mut TestAppContext) {
10569 use crate::input::IndentationRules;
10570 let view = InputView::<EditorMode>::new(cx);
10571 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10572 let rules = LanguageConfig::default().indentation_rules(IndentationRules::new(
10573 regex::Regex::new(r"\bbegin\s*$").unwrap(),
10574 regex::Regex::new(r"^\s*end\b").unwrap(),
10575 ));
10576 for (before, expected) in [("begin|", "begin\n |"), (" |end", " \n|end")] {
10577 setup_cursors(&mut cx, &view.input, before);
10578 cx.update(|window, cx| {
10579 view.input.update(cx, |state, cx| {
10580 crate::input::set_language_config(state.language_name(), rules.clone(), cx);
10581 state.enter(
10582 &Enter {
10583 secondary: false,
10584 shift: false,
10585 },
10586 window,
10587 cx,
10588 );
10589 });
10590 });
10591 assert_cursors(&mut cx, &view.input, expected);
10592 }
10593 }
10594
10595 #[gpui::test]
10596 fn test_custom_language_config(cx: &mut TestAppContext) {
10597 let view = InputView::<EditorMode>::new(cx);
10598 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10599 cx.update(|_window, cx| {
10600 view.input.update(cx, |state, cx| {
10601 crate::input::set_language_config(
10602 state.language_name(),
10603 crate::input::language_config::LanguageConfig::default()
10604 .brackets([crate::input::BracketPair::new("«", "»")])
10605 .auto_closing_pairs([crate::input::AutoClosingPair::new("«", "»")]),
10606 cx,
10607 );
10608 });
10609 });
10610 setup_cursors(&mut cx, &view.input, "|");
10612 cx.update(|window, cx| {
10613 view.input.update(cx, |state, cx| {
10614 state.replace_text_in_range(None, "«", window, cx);
10615 });
10616 });
10617 assert_cursors(&mut cx, &view.input, "«|»");
10618 cx.update(|window, cx| {
10620 view.input.update(cx, |state, cx| {
10621 state.set_value("\n", window, cx);
10622 state.set_selected_range(0..0, cx);
10623 state.replace_text_in_range(None, "(", window, cx);
10624 });
10625 });
10626 assert_cursors(&mut cx, &view.input, "(|");
10627 }
10628
10629 #[gpui::test]
10630 fn test_skip_records_no_history(cx: &mut TestAppContext) {
10631 let view = InputView::<EditorMode>::new(cx);
10632 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10633 setup_cursors(&mut cx, &view.input, "(a|)");
10634 cx.update(|window, cx| {
10635 view.input.update(cx, |state, cx| {
10636 state.replace_text_in_range(None, "b", window, cx);
10638 });
10639 });
10640 assert_cursors(&mut cx, &view.input, "(ab|)");
10641 cx.update(|window, cx| {
10642 view.input.update(cx, |state, cx| {
10643 state.replace_text_in_range(None, ")", window, cx);
10644 });
10645 });
10646 assert_cursors(&mut cx, &view.input, "(ab)|");
10648 cx.update(|window, cx| {
10650 view.input
10651 .update(cx, |state, cx| state.undo(&Undo, window, cx));
10652 });
10653 assert_cursors(&mut cx, &view.input, "(a|)");
10654 }
10655
10656 #[gpui::test]
10657 fn test_pair_insert_undo_removes_both(cx: &mut TestAppContext) {
10658 let view = InputView::<EditorMode>::new(cx);
10659 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10660 setup_cursors(&mut cx, &view.input, "|");
10661 cx.update(|window, cx| {
10662 view.input.update(cx, |state, cx| {
10663 state.replace_text_in_range(None, "(", window, cx);
10664 });
10665 });
10666 assert_cursors(&mut cx, &view.input, "(|)");
10667 cx.update(|window, cx| {
10669 view.input
10670 .update(cx, |state, cx| state.undo(&Undo, window, cx));
10671 });
10672 assert_cursors(&mut cx, &view.input, "|");
10673 }
10674
10675 #[gpui::test]
10676 fn test_escaped_quote_does_not_skip(cx: &mut TestAppContext) {
10677 let view = InputView::<EditorMode>::new(cx);
10678 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10679 setup_cursors(&mut cx, &view.input, "\"hello\\|\"");
10681 cx.update(|window, cx| {
10682 view.input.update(cx, |state, cx| {
10683 state.replace_text_in_range(None, "\"", window, cx);
10684 });
10685 });
10686 assert_cursors(&mut cx, &view.input, "\"hello\\\"|\"");
10687 }
10688
10689 #[gpui::test]
10690 fn test_even_backslashes_still_skip(cx: &mut TestAppContext) {
10691 let view = InputView::<EditorMode>::new(cx);
10692 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10693 setup_cursors(&mut cx, &view.input, "\"hello\\\\|\"");
10695 cx.update(|window, cx| {
10696 view.input.update(cx, |state, cx| {
10697 state.replace_text_in_range(None, "\"", window, cx);
10698 });
10699 });
10700 assert_cursors(&mut cx, &view.input, "\"hello\\\\\"|");
10701 }
10702
10703 #[gpui::test]
10704 fn test_editor_options_survive_configuration_replacement(cx: &mut TestAppContext) {
10705 use crate::input::{BracketPair, LanguageConfig};
10706
10707 let view = InputView::<EditorMode>::new(cx);
10708 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10709 cx.update(|window, cx| {
10710 view.input.update(cx, |state, cx| {
10711 state.set_smart_indent(false, window, cx);
10713 crate::input::set_language_config(
10714 state.language_name(),
10715 LanguageConfig::default().brackets([BracketPair::new("«", "»")]),
10716 cx,
10717 );
10718 });
10719 });
10720 view.input.read_with(&cx, |state, _| {
10721 assert!(!state.mode.is_smart_indent(), "editor option preserved");
10722 assert!(state.mode.is_auto_close());
10723 let pairs = &state.mode.language_config().unwrap().brackets;
10724 assert_eq!(pairs.len(), 1, "pairs follow configuration");
10725 assert_eq!(pairs[0].open.as_ref(), "«");
10726 });
10727 }
10728
10729 struct CommentAllProvider;
10730 impl crate::input::SyntaxContextProvider for CommentAllProvider {
10731 fn context_at(&self, _text: &ropey::Rope, _offset: usize) -> crate::input::SyntaxContext {
10732 crate::input::SyntaxContext::Comment
10733 }
10734 }
10735
10736 struct StringAllProvider;
10737 impl crate::input::SyntaxContextProvider for StringAllProvider {
10738 fn context_at(&self, _text: &ropey::Rope, _offset: usize) -> crate::input::SyntaxContext {
10739 crate::input::SyntaxContext::String
10740 }
10741 }
10742
10743 #[gpui::test]
10744 fn test_smart_indent_suppressed_in_strings(cx: &mut TestAppContext) {
10745 use std::rc::Rc;
10746
10747 let view = InputView::<EditorMode>::new(cx);
10748 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10749 cx.update(|_window, cx| {
10750 set_test_syntax_provider(Rc::new(StringAllProvider), cx);
10751 });
10752 setup_cursors(&mut cx, &view.input, " x = {|");
10754 cx.update(|window, cx| {
10755 view.input.update(cx, |state, cx| {
10756 state.enter(
10757 &Enter {
10758 secondary: false,
10759 shift: false,
10760 },
10761 window,
10762 cx,
10763 );
10764 });
10765 });
10766 assert_cursors(&mut cx, &view.input, " x = {\n |");
10767 }
10768
10769 #[gpui::test]
10770 fn test_split_suppressed_in_strings(cx: &mut TestAppContext) {
10771 use std::rc::Rc;
10772
10773 let view = InputView::<EditorMode>::new(cx);
10774 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10775 cx.update(|_window, cx| {
10776 set_test_syntax_provider(Rc::new(StringAllProvider), cx);
10777 });
10778 setup_cursors(&mut cx, &view.input, "{|}");
10780 cx.update(|window, cx| {
10781 view.input.update(cx, |state, cx| {
10782 state.enter(
10783 &Enter {
10784 secondary: false,
10785 shift: false,
10786 },
10787 window,
10788 cx,
10789 );
10790 });
10791 });
10792 assert_cursors(&mut cx, &view.input, "{\n|}");
10793 }
10794
10795 #[gpui::test]
10796 fn test_comment_context_disables_pairing(cx: &mut TestAppContext) {
10797 use std::rc::Rc;
10798
10799 let view = InputView::<EditorMode>::new(cx);
10800 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10801 cx.update(|_window, cx| {
10802 set_test_syntax_provider(Rc::new(CommentAllProvider), cx);
10803 });
10804 setup_cursors(&mut cx, &view.input, "|");
10805 cx.update(|window, cx| {
10806 view.input.update(cx, |state, cx| {
10807 state.replace_text_in_range(None, "(", window, cx);
10808 });
10809 });
10810 assert_cursors(&mut cx, &view.input, "(|");
10812 }
10813
10814 #[gpui::test]
10815 fn test_backspace_deletes_empty_pair(cx: &mut TestAppContext) {
10816 let view = InputView::<EditorMode>::new(cx);
10817 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10818 setup_cursors(&mut cx, &view.input, "{|}");
10819 cx.update(|window, cx| {
10820 view.input
10821 .update(cx, |state, cx| state.backspace(&Backspace, window, cx));
10822 });
10823 assert_cursors(&mut cx, &view.input, "|");
10824 }
10825
10826 #[gpui::test]
10827 fn test_backspace_keeps_closer_with_content(cx: &mut TestAppContext) {
10828 let view = InputView::<EditorMode>::new(cx);
10829 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10830 setup_cursors(&mut cx, &view.input, "{d|}");
10831 cx.update(|window, cx| {
10832 view.input
10833 .update(cx, |state, cx| state.backspace(&Backspace, window, cx));
10834 });
10835 assert_cursors(&mut cx, &view.input, "{|}");
10837 }
10838
10839 #[gpui::test]
10840 fn test_enter_splits_brackets(cx: &mut TestAppContext) {
10841 let view = InputView::<EditorMode>::new(cx);
10842 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10843 setup_cursors(&mut cx, &view.input, "{|}");
10844 cx.update(|window, cx| {
10845 view.input.update(cx, |state, cx| {
10846 state.enter(
10847 &Enter {
10848 secondary: false,
10849 shift: false,
10850 },
10851 window,
10852 cx,
10853 );
10854 });
10855 });
10856 assert_cursors(&mut cx, &view.input, "{\n |\n}");
10857 view.input.read_with(&cx, |state, _| {
10859 assert!(state.active_selection().is_empty());
10860 });
10861 cx.update(|window, cx| {
10862 view.input.update(cx, |state, cx| {
10863 state.replace_text_in_range(None, "x", window, cx);
10864 });
10865 });
10866 assert_cursors(&mut cx, &view.input, "{\n x|\n}");
10867 }
10868
10869 #[gpui::test]
10870 fn test_enter_smart_indent_after_brace(cx: &mut TestAppContext) {
10871 let view = InputView::<EditorMode>::new(cx);
10872 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10873 setup_cursors(&mut cx, &view.input, "fn main() {|");
10874 cx.update(|window, cx| {
10875 view.input.update(cx, |state, cx| {
10876 state.enter(
10877 &Enter {
10878 secondary: false,
10879 shift: false,
10880 },
10881 window,
10882 cx,
10883 );
10884 });
10885 });
10886 assert_cursors(&mut cx, &view.input, "fn main() {\n |");
10887 }
10888
10889 #[gpui::test]
10890 fn test_block_indent_tracks_all_preceding_edits(cx: &mut TestAppContext) {
10891 let view = multi_line(cx);
10892 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10893 setup_cursors(&mut cx, &view.input, "|ab\n|cd\n|ef");
10894 cx.update(|window, cx| {
10895 view.input
10896 .update(cx, |state, cx| state.indent(true, window, cx));
10897 });
10898 assert_cursors(&mut cx, &view.input, " |ab\n |cd\n |ef");
10899 cx.update(|window, cx| {
10900 view.input
10901 .update(cx, |state, cx| state.outdent(true, window, cx));
10902 });
10903 assert_cursors(&mut cx, &view.input, "|ab\n|cd\n|ef");
10904 }
10905
10906 #[gpui::test]
10907 fn test_block_outdent_clamps_cursor_inside_indent(cx: &mut TestAppContext) {
10908 let view = multi_line(cx);
10909 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10910 setup_cursors(&mut cx, &view.input, "ab\n | cd");
10911 cx.update(|window, cx| {
10912 view.input
10913 .update(cx, |state, cx| state.outdent(true, window, cx));
10914 });
10915 assert_cursors(&mut cx, &view.input, "ab\n|cd");
10916 }
10917
10918 #[gpui::test]
10919 fn test_ime_restores_original_selection(cx: &mut TestAppContext) {
10920 let view = InputView::build(cx, |state| state);
10921 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10922 cx.update(|window, cx| {
10923 view.input.update(cx, |state, cx| {
10924 state.set_value("abc", window, cx);
10925 state.set_selected_range(1..2, cx);
10926 state.replace_and_mark_text_in_range(None, "ni", None, window, cx);
10927 state.replace_text_in_range(None, "你", window, cx);
10928 state.undo(&Undo, window, cx);
10929 assert_eq!(state.value(), "abc");
10930 assert_eq!(state.selected_range(), 1..2);
10931 state.redo(&Redo, window, cx);
10932 assert_eq!(state.selected_range(), 4..4);
10933 });
10934 });
10935 }
10936
10937 #[gpui::test]
10938 fn test_noop_does_not_change_redo_selection(cx: &mut TestAppContext) {
10939 let view = multi_line(cx);
10940 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10941 cx.update(|window, cx| {
10942 view.input.update(cx, |state, cx| {
10943 state.set_value("abc", window, cx);
10944 state.set_selected_range(1..1, cx);
10945 state.replace_text_in_range(None, "X", window, cx);
10946 state.set_selected_range(0..0, cx);
10947 state.replace_text_in_range(None, "", window, cx);
10948 state.undo(&Undo, window, cx);
10949 state.redo(&Redo, window, cx);
10950 assert_eq!(state.value(), "aXbc");
10951 assert_eq!(state.selected_range(), 2..2);
10952 });
10953 });
10954 }
10955
10956 #[gpui::test]
10957 fn test_multi_cursor_insert_text(cx: &mut TestAppContext) {
10958 let view = multi_line(cx);
10959 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10960 let input = view.input;
10961
10962 setup_cursors(&mut cx, &input, "|hello |world|");
10963 cx.update(|window, cx| {
10964 input.update(cx, |state, cx| {
10965 state.replace_text_in_range(None, ">>>", window, cx);
10966 });
10967 });
10968 assert_cursors(&mut cx, &input, ">>>|hello >>>|world>>>|");
10969 }
10970
10971 #[gpui::test]
10972 fn test_multi_cursor_delete_backward(cx: &mut TestAppContext) {
10973 let view = multi_line(cx);
10974 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10975 let input = view.input;
10976
10977 setup_cursors(&mut cx, &input, "|islands| cars|");
10978 cx.update(|window, cx| {
10979 input.update(cx, |state, cx| {
10980 state.backspace(&Backspace, window, cx);
10981 });
10982 });
10983 assert_cursors(&mut cx, &input, "|island| car|");
10985 }
10986
10987 #[gpui::test]
10988 fn test_multi_cursor_delete_forward_merges(cx: &mut TestAppContext) {
10989 let view = multi_line(cx);
10990 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10991 let input = view.input;
10992
10993 setup_cursors(&mut cx, &input, "hello| |world");
10994 cx.update(|window, cx| {
10995 input.update(cx, |state, cx| {
10996 state.delete(&Delete, window, cx);
10997 });
10998 });
10999 assert_cursors(&mut cx, &input, "hello|orld");
11001 }
11002
11003 #[gpui::test]
11004 fn test_multi_cursor_multiline_insert_and_delete(cx: &mut TestAppContext) {
11005 let view = multi_line(cx);
11006 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11007 let input = view.input;
11008
11009 setup_cursors(&mut cx, &input, "|1\n|2\n|3");
11010 cx.update(|window, cx| {
11011 input.update(cx, |state, cx| {
11012 state.replace_text_in_range(None, "a", window, cx);
11013 });
11014 });
11015 assert_cursors(&mut cx, &input, "a|1\na|2\na|3");
11016
11017 input.read_with(&cx, |state, _| {
11019 assert_eq!(state.undo_manager.undo_count(), 1);
11020 });
11021
11022 cx.update(|window, cx| {
11023 input.update(cx, |state, cx| {
11024 state.backspace(&Backspace, window, cx);
11025 });
11026 });
11027 assert_cursors(&mut cx, &input, "|1\n|2\n|3");
11028 }
11029
11030 #[gpui::test]
11031 fn test_add_cursor_below_preserves_column(cx: &mut TestAppContext) {
11032 let view = multi_line(cx);
11033 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11034 let input = view.input;
11035
11036 setup_cursors(&mut cx, &input, "ab|cd\nabcd");
11037 cx.update(|window, cx| {
11038 input.update(cx, |state, cx| {
11039 state.add_cursor_below(&AddCursorBelow, window, cx);
11040 });
11041 });
11042 assert_cursors(&mut cx, &input, "ab|cd\nab|cd");
11043 }
11044
11045 #[gpui::test]
11046 fn test_add_cursor_at_rejects_duplicates(cx: &mut TestAppContext) {
11047 let view = multi_line(cx);
11048 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11049 let input = view.input;
11050
11051 setup_cursors(&mut cx, &input, "he|llo");
11052 cx.update(|_, cx| {
11053 input.update(cx, |state, cx| {
11054 state.add_cursor_at(2, cx);
11056 assert_eq!(state.selections.len(), 1);
11057 state.add_cursor_at(4, cx);
11059 assert_eq!(state.selections.len(), 2);
11060 });
11061 });
11062 }
11063
11064 #[gpui::test]
11065 fn test_multi_cursor_undo_redo_restores_selections(cx: &mut TestAppContext) {
11066 let view = multi_line(cx);
11067 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11068 let input = view.input;
11069
11070 setup_cursors(&mut cx, &input, "|1\n|2\n|3");
11071 cx.update(|window, cx| {
11072 input.update(cx, |state, cx| {
11073 state.replace_text_in_range(None, "a", window, cx);
11074 });
11075 });
11076 assert_cursors(&mut cx, &input, "a|1\na|2\na|3");
11077
11078 cx.update(|window, cx| {
11079 input.update(cx, |state, cx| {
11080 state.undo(&Undo, window, cx);
11081 });
11082 });
11083 assert_cursors(&mut cx, &input, "|1\n|2\n|3");
11084
11085 cx.update(|window, cx| {
11086 input.update(cx, |state, cx| {
11087 state.redo(&Redo, window, cx);
11088 });
11089 });
11090 assert_cursors(&mut cx, &input, "a|1\na|2\na|3");
11091 }
11092
11093 #[gpui::test]
11094 fn test_multi_cursor_undo_redo_different_line_lengths(cx: &mut TestAppContext) {
11095 let view = multi_line(cx);
11096 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11097 let input = view.input;
11098
11099 setup_cursors(&mut cx, &input, "abc123|\nabc12345|\nabc1234567|");
11100 cx.update(|window, cx| {
11101 input.update(cx, |state, cx| {
11102 state.replace_text_in_range(None, "a", window, cx);
11103 });
11104 });
11105 assert_cursors(&mut cx, &input, "abc123a|\nabc12345a|\nabc1234567a|");
11106
11107 cx.update(|window, cx| {
11108 input.update(cx, |state, cx| {
11109 state.undo(&Undo, window, cx);
11110 });
11111 });
11112 assert_cursors(&mut cx, &input, "abc123|\nabc12345|\nabc1234567|");
11113
11114 cx.update(|window, cx| {
11115 input.update(cx, |state, cx| {
11116 state.redo(&Redo, window, cx);
11117 });
11118 });
11119 assert_cursors(&mut cx, &input, "abc123a|\nabc12345a|\nabc1234567a|");
11120 }
11121
11122 #[gpui::test]
11123 fn test_multi_cursor_undo_multiple_inserts(cx: &mut TestAppContext) {
11124 let view = multi_line(cx);
11125 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11126 let input = view.input;
11127
11128 setup_cursors(&mut cx, &input, "|1\n|2\n|3");
11129 for ch in ['a', 'b', 'c'] {
11130 cx.update(|window, cx| {
11131 input.update(cx, |state, cx| {
11132 state.replace_text_in_range(None, &ch.to_string(), window, cx);
11133 });
11134 });
11135 }
11136 assert_cursors(&mut cx, &input, "abc|1\nabc|2\nabc|3");
11137
11138 cx.update(|window, cx| {
11140 input.update(cx, |state, cx| {
11141 assert_eq!(state.undo_manager.undo_count(), 1);
11142 state.undo(&Undo, window, cx);
11143 });
11144 });
11145 assert_cursors(&mut cx, &input, "|1\n|2\n|3");
11146
11147 cx.update(|window, cx| {
11148 input.update(cx, |state, cx| {
11149 state.redo(&Redo, window, cx);
11150 });
11151 });
11152 assert_cursors(&mut cx, &input, "abc|1\nabc|2\nabc|3");
11153 }
11154
11155 #[gpui::test]
11156 fn test_multi_cursor_backspace_run_is_one_undo(cx: &mut TestAppContext) {
11157 let view = multi_line(cx);
11158 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11159 let input = view.input;
11160
11161 setup_cursors(&mut cx, &input, "abc|1\nabc|2\nabc|3");
11162 for _ in 0..3 {
11163 cx.update(|window, cx| {
11164 input.update(cx, |state, cx| {
11165 state.backspace(&Backspace, window, cx);
11166 });
11167 });
11168 }
11169 assert_cursors(&mut cx, &input, "|1\n|2\n|3");
11170
11171 cx.update(|window, cx| {
11172 input.update(cx, |state, cx| {
11173 assert_eq!(state.undo_manager.undo_count(), 1);
11174 state.undo(&Undo, window, cx);
11175 });
11176 });
11177 assert_cursors(&mut cx, &input, "abc|1\nabc|2\nabc|3");
11178 }
11179
11180 #[gpui::test]
11181 fn test_adding_a_cursor_splits_the_typing_gesture(cx: &mut TestAppContext) {
11182 let view = multi_line(cx);
11183 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11184 let input = view.input;
11185
11186 setup_cursors(&mut cx, &input, "|1\n2\n3");
11187 cx.update(|window, cx| {
11188 input.update(cx, |state, cx| {
11189 state.replace_text_in_range(None, "a", window, cx);
11190 state.add_cursor_below(&AddCursorBelow, window, cx);
11191 state.replace_text_in_range(None, "b", window, cx);
11192 });
11193 });
11194 assert_cursors(&mut cx, &input, "ab|1\n2b|\n3");
11195
11196 cx.update(|window, cx| {
11198 input.update(cx, |state, cx| {
11199 state.undo(&Undo, window, cx);
11200 });
11201 });
11202 assert_cursors(&mut cx, &input, "a|1\n2|\n3");
11203 }
11204
11205 #[gpui::test]
11206 fn test_multi_cursor_indent_is_one_undo(cx: &mut TestAppContext) {
11207 let view = multi_line(cx);
11208 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11209 let input = view.input;
11210
11211 setup_cursors(&mut cx, &input, "|1\n|2\n|3");
11212 cx.update(|window, cx| {
11213 input.update(cx, |state, cx| {
11214 state.indent_inline(&IndentInline, window, cx);
11215 assert_eq!(state.undo_manager.undo_count(), 1);
11216 state.undo(&Undo, window, cx);
11217 });
11218 });
11219 assert_cursors(&mut cx, &input, "|1\n|2\n|3");
11220 }
11221
11222 #[gpui::test]
11223 fn test_multi_cursor_indent_then_outdent_roundtrips(cx: &mut TestAppContext) {
11224 let view = multi_line(cx);
11225 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11226 let input = view.input;
11227
11228 setup_cursors(&mut cx, &input, "|1\n|2\n|3");
11230 cx.update(|window, cx| {
11231 input.update(cx, |state, cx| {
11232 state.indent(false, window, cx);
11233 });
11234 });
11235 assert_cursors(&mut cx, &input, " |1\n |2\n |3");
11236
11237 cx.update(|window, cx| {
11238 input.update(cx, |state, cx| {
11239 state.outdent(false, window, cx);
11240 });
11241 });
11242 assert_cursors(&mut cx, &input, "|1\n|2\n|3");
11243 }
11244
11245 #[gpui::test]
11246 fn test_inline_outdent_only_removes_line_indentation(cx: &mut TestAppContext) {
11247 let view = multi_line(cx);
11248 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11249 let input = view.input;
11250
11251 setup_cursors(&mut cx, &input, "1|2\n1|2");
11254 cx.update(|window, cx| {
11255 input.update(cx, |state, cx| {
11256 state.indent(false, window, cx);
11257 });
11258 });
11259 assert_cursors(&mut cx, &input, "1 |2\n1 |2");
11260
11261 cx.update(|window, cx| {
11262 input.update(cx, |state, cx| {
11263 state.outdent(false, window, cx);
11264 });
11265 });
11266 assert_cursors(&mut cx, &input, "1 |2\n1 |2");
11267
11268 setup_cursors(&mut cx, &input, " 1|2\n 1|2");
11270 cx.update(|window, cx| {
11271 input.update(cx, |state, cx| {
11272 state.outdent(false, window, cx);
11273 });
11274 });
11275 assert_cursors(&mut cx, &input, "1|2\n1|2");
11276 }
11277
11278 #[gpui::test]
11279 fn test_readonly_multi_cursor_commands_leave_state_unchanged(cx: &mut TestAppContext) {
11280 let view = multi_line(cx);
11281 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11282 let input = view.input;
11283
11284 setup_cursors(&mut cx, &input, "a|b\nc|d");
11285 cx.update(|window, cx| {
11286 input.update(cx, |state, cx| {
11287 let before: Vec<_> = state.selections.iter().copied().collect();
11288 state.set_readonly(true, cx);
11289 state.backspace(&Backspace, window, cx);
11290 state.indent_inline(&IndentInline, window, cx);
11291
11292 assert_eq!(state.value(), "ab\ncd\n");
11293 assert_eq!(state.selections.iter().copied().collect::<Vec<_>>(), before);
11294 });
11295 });
11296 }
11297
11298 #[gpui::test]
11299 fn test_multi_cursor_edit_preserves_the_active_cursor(cx: &mut TestAppContext) {
11300 let view = multi_line(cx);
11301 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11302 let input = view.input;
11303
11304 setup_cursors(&mut cx, &input, "x|\n|y");
11305 cx.update(|window, cx| {
11306 input.update(cx, |state, cx| {
11307 let mut selections: Vec<_> = state.selections.iter().copied().collect();
11308 selections.swap(0, 1);
11309 state.selections.replace_all(selections);
11310 let active_id = state.active_selection().id;
11311 state.replace_text_in_range(None, "!", window, cx);
11312 assert_eq!(state.active_selection().id, active_id);
11313 });
11314 });
11315 }
11316
11317 #[gpui::test]
11318 fn test_block_indent_outdent_with_selection(cx: &mut TestAppContext) {
11319 let view = multi_line(cx);
11320 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11321 let input = view.input;
11322
11323 cx.update(|window, cx| {
11324 input.update(cx, |state, cx| {
11325 state.set_value("line1\nline2\nline3", window, cx);
11326 let id = state.selections.generate_id();
11327 state
11328 .selections
11329 .replace_all(vec![CursorSelection::new(id, 0, 17)]);
11330 cx.notify();
11331 });
11332 });
11333
11334 cx.update(|window, cx| {
11335 input.update(cx, |state, cx| {
11336 state.indent(true, window, cx);
11337 });
11338 });
11339 input.read_with(&cx, |state, _| {
11340 assert_eq!(state.text.to_string(), " line1\n line2\n line3");
11341 });
11342
11343 cx.update(|window, cx| {
11344 input.update(cx, |state, cx| {
11345 state.outdent(true, window, cx);
11346 });
11347 });
11348 input.read_with(&cx, |state, _| {
11349 assert_eq!(state.text.to_string(), "line1\nline2\nline3");
11350 });
11351 }
11352
11353 #[gpui::test]
11354 fn test_multi_cursor_word_movement(cx: &mut TestAppContext) {
11355 let view = multi_line(cx);
11356 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11357 let input = view.input;
11358
11359 setup_cursors(
11360 &mut cx,
11361 &input,
11362 "on|e two three\none t|wo three\non|e two three",
11363 );
11364
11365 cx.update(|window, cx| {
11366 input.update(cx, |state, cx| {
11367 state.move_to_next_word(&MoveToNextWord, window, cx);
11368 });
11369 });
11370 assert_cursors(
11371 &mut cx,
11372 &input,
11373 "one| two three\none two| three\none| two three",
11374 );
11375
11376 cx.update(|window, cx| {
11377 input.update(cx, |state, cx| {
11378 state.move_to_previous_word(&MoveToPreviousWord, window, cx);
11379 });
11380 });
11381 assert_cursors(
11382 &mut cx,
11383 &input,
11384 "|one two three\none |two three\n|one two three",
11385 );
11386
11387 cx.update(|window, cx| {
11389 input.update(cx, |state, cx| {
11390 state.move_to_end(&MoveToEnd, window, cx);
11391 });
11392 });
11393 input.read_with(&cx, |state, _| {
11394 let cursors: Vec<usize> = state.selections.iter().map(|s| s.cursor_offset()).collect();
11395 assert_eq!(cursors, vec![state.text.len()]);
11396 });
11397
11398 cx.update(|window, cx| {
11399 input.update(cx, |state, cx| {
11400 state.move_to_start(&MoveToStart, window, cx);
11401 });
11402 });
11403 input.read_with(&cx, |state, _| {
11404 let cursors: Vec<usize> = state.selections.iter().map(|s| s.cursor_offset()).collect();
11405 assert_eq!(cursors, vec![0]);
11406 });
11407 }
11408
11409 #[gpui::test]
11410 fn test_multi_cursor_selection_commands(cx: &mut TestAppContext) {
11411 let view = multi_line(cx);
11412 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11413 let input = view.input;
11414
11415 setup_cursors(
11416 &mut cx,
11417 &input,
11418 "on|e two three\none t|wo three\non|e two three",
11419 );
11420 cx.update(|window, cx| {
11421 input.update(cx, |state, cx| {
11422 state.select_to_start_of_line(&SelectToStartOfLine, window, cx);
11423 });
11424 });
11425 assert_cursors(
11426 &mut cx,
11427 &input,
11428 "|one two three\n|one two three\n|one two three",
11429 );
11430
11431 setup_cursors(
11433 &mut cx,
11434 &input,
11435 "on|e two three\none t|wo three\non|e two three",
11436 );
11437 cx.update(|window, cx| {
11438 input.update(cx, |state, cx| {
11439 state.select_to_start(&SelectToStart, window, cx);
11440 });
11441 });
11442 input.read_with(&cx, |state, _| {
11443 let cursors: Vec<usize> = state.selections.iter().map(|s| s.cursor_offset()).collect();
11444 assert_eq!(cursors, vec![0]);
11445 });
11446 }
11447
11448 #[gpui::test]
11449 fn test_multi_cursor_replace_selection(cx: &mut TestAppContext) {
11450 let view = multi_line(cx);
11451 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11452 let input = view.input;
11453
11454 setup_cursors(&mut cx, &input, "|a\n|b\n|c");
11455 cx.update(|window, cx| {
11456 input.update(cx, |state, cx| {
11457 state.select_right(&SelectRight, window, cx);
11458 });
11459 });
11460 input.read_with(&cx, |state, _| {
11461 let ranges: Vec<_> = state.selections.iter().map(|s| (s.start, s.end)).collect();
11462 assert_eq!(ranges, vec![(0, 1), (2, 3), (4, 5)]);
11463 });
11464
11465 cx.update(|window, cx| {
11466 input.update(cx, |state, cx| {
11467 state.replace_text_in_range(None, "x", window, cx);
11468 });
11469 });
11470 assert_cursors(&mut cx, &input, "x|\nx|\nx|");
11471 }
11472
11473 #[gpui::test]
11474 fn test_multi_cursor_escape_collapses(cx: &mut TestAppContext) {
11475 let view = multi_line(cx);
11476 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11477 let input = view.input;
11478
11479 setup_cursors(&mut cx, &input, "|a\n|b\n|c");
11480 cx.update(|window, cx| {
11481 input.update(cx, |state, cx| {
11482 state.escape(&Escape, window, cx);
11483 });
11484 });
11485 input.read_with(&cx, |state, _| {
11486 assert_eq!(state.selections.len(), 1);
11487 });
11488 }
11489
11490 #[gpui::test]
11491 fn test_build_columnar_selection(cx: &mut TestAppContext) {
11492 let view = multi_line(cx);
11493 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11494 let input = view.input;
11495
11496 setup_cursors(&mut cx, &input, "abcd\nabcd\nabcd");
11497 cx.update(|_, cx| {
11498 input.update(cx, |state, cx| {
11499 state.build_columnar_selection(
11501 ColumnarPoint::new(1, 0),
11502 ColumnarPoint::new(13, 0),
11503 cx,
11504 );
11505 let ranges: Vec<_> = state.selections.iter().map(|s| (s.start, s.end)).collect();
11506 assert_eq!(ranges, vec![(1, 3), (6, 8), (11, 13)]);
11507 });
11508 });
11509 }
11510
11511 #[gpui::test]
11512 fn test_columnar_selection_keeps_width_over_short_rows(cx: &mut TestAppContext) {
11513 let view = multi_line(cx);
11514 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11515 let input = view.input;
11516
11517 setup_cursors(&mut cx, &input, "abcdef\nab\nabcdef");
11518 cx.update(|_, cx| {
11519 input.update(cx, |state, cx| {
11520 state.build_columnar_selection(
11523 ColumnarPoint::new(1, 0),
11524 ColumnarPoint::new(9, 3),
11525 cx,
11526 );
11527 let ranges: Vec<_> = state.selections.iter().map(|s| (s.start, s.end)).collect();
11528 assert_eq!(ranges, vec![(1, 5), (8, 9)]);
11529 });
11530 });
11531 }
11532
11533 #[gpui::test]
11534 fn test_multi_cursor_paste_distributes_lines(cx: &mut TestAppContext) {
11535 let view = multi_line(cx);
11536 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11537 let input = view.input;
11538
11539 setup_cursors(&mut cx, &input, "|1\n|2\n|3");
11540 cx.update(|_, cx| {
11541 cx.write_to_clipboard(ClipboardItem::new_string("x\ny\nz".to_string()));
11542 });
11543 cx.update(|window, cx| {
11544 input.update(cx, |state, cx| {
11545 state.paste(&Paste, window, cx);
11546 });
11547 });
11548 assert_cursors(&mut cx, &input, "x|1\ny|2\nz|3");
11550 }
11551
11552 #[gpui::test]
11553 fn test_paste_without_text_leaves_the_selection_alone(cx: &mut TestAppContext) {
11554 use gpui::{ClipboardEntry, Image, ImageFormat};
11555
11556 let input_view = InputView::build(cx, |state| state);
11557 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
11558 let input = input_view.input;
11559
11560 cx.update(|window, cx| {
11561 input.update(cx, |state, cx| {
11562 state.set_value("hello world", window, cx);
11563 state.select_all(window, cx);
11564 cx.write_to_clipboard(ClipboardItem {
11565 entries: vec![ClipboardEntry::Image(Image::from_bytes(
11566 ImageFormat::Png,
11567 vec![0x89, b'P', b'N', b'G'],
11568 ))],
11569 });
11570
11571 state.paste(&Paste, window, cx);
11574 assert_eq!(state.value(), "hello world");
11575 assert_eq!(state.selected_range(), 0..11);
11576 });
11577 });
11578 }
11579
11580 #[gpui::test]
11581 fn test_paste_target_tracks_edits_and_selections(cx: &mut TestAppContext) {
11582 let view = multi_line(cx);
11583 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11584 let input = view.input;
11585
11586 setup_cursors(&mut cx, &input, "ab|c");
11587 let target = input.read_with(&cx, |state, _| state.paste_target());
11588 assert_eq!(
11590 input.read_with(&cx, |state, _| state.paste_target()),
11591 target
11592 );
11593
11594 cx.update(|_, cx| {
11596 input.update(cx, |state, cx| {
11597 let id = state.selections.generate_id();
11598 state
11599 .selections
11600 .replace_all(vec![CursorSelection::new(id, 1, 1)]);
11601 cx.notify();
11602 });
11603 });
11604 let moved = input.read_with(&cx, |state, _| state.paste_target());
11605 assert_ne!(moved, target);
11606
11607 cx.update(|window, cx| {
11609 input.update(cx, |state, cx| {
11610 state.replace_text_in_range_silent(None, "x", window, cx);
11611 let id = state.selections.generate_id();
11612 state
11613 .selections
11614 .replace_all(vec![CursorSelection::new(id, 1, 1)]);
11615 });
11616 });
11617 assert_ne!(input.read_with(&cx, |state, _| state.paste_target()), moved);
11618 }
11619
11620 #[gpui::test]
11621 fn test_ime_candidate_bounds_fallback_before_repaint(cx: &mut TestAppContext) {
11622 let input_view = InputView::build_textarea(cx, |state| {
11623 state.default_value("这是一段已经输入的中文文字")
11624 });
11625 let mut visual = VisualTestContext::from_window(input_view.window_handle.into(), cx);
11626 visual.update(|window, cx| window.draw(cx).clear(cx));
11627
11628 visual.update(|window, cx| {
11629 input_view.input.update(cx, |state, cx| {
11630 state.focus(window, cx);
11631 let len = state.text.len();
11632 state.set_cursor_to(len);
11633 });
11634 });
11635 visual.update(|window, cx| window.draw(cx).clear(cx));
11636
11637 visual.update(|window, cx| {
11638 input_view.input.update(cx, |state, cx| {
11639 let bounds = state.text_bounds().unwrap_or_default();
11640 let before = state.selected_text_range(false, window, cx).unwrap().range;
11641 let previous = state
11642 .bounds_for_range(before.end..before.end, bounds, window, cx)
11643 .unwrap();
11644
11645 state.replace_and_mark_text_in_range(None, "n", None, window, cx);
11646
11647 let after = state.selected_text_range(false, window, cx).unwrap().range;
11648 let pending = state
11649 .bounds_for_range(after.end..after.end, bounds, window, cx)
11650 .unwrap();
11651
11652 assert!(
11653 (pending.origin.x - previous.origin.x).abs() < px(40.0),
11654 "pending origin.x {:?} must remain close to previous origin.x {:?}",
11655 pending.origin.x,
11656 previous.origin.x
11657 );
11658
11659 let range_bounds = state
11660 .bounds_for_range(before.end..after.end, bounds, window, cx)
11661 .unwrap();
11662 assert!(
11663 (range_bounds.origin.x - previous.origin.x).abs() < px(40.0),
11664 "range_bounds origin.x {:?} must remain close to previous origin.x {:?}",
11665 range_bounds.origin.x,
11666 previous.origin.x
11667 );
11668 assert!(range_bounds.size.width >= px(0.0));
11669 });
11670 });
11671 }
11672}
11673
11674impl InputBaseState<crate::input::InputMode> {
11676 pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
11678 Self::new_in_mode(window, cx)
11679 }
11680
11681 pub fn step_by(mut self, f: impl Fn(f64, StepAction, &mut App) -> f64 + 'static) -> Self {
11696 self.number_step = Some(NumberStep::by_value(f));
11697 self
11698 }
11699
11700 pub fn masked(mut self, masked: bool) -> Self {
11702 self.masked = masked;
11703 self
11704 }
11705
11706 pub fn set_masked(&mut self, masked: bool, _: &mut Window, cx: &mut Context<Self>) {
11708 self.masked = masked;
11709 cx.notify();
11710 }
11711
11712 pub fn pattern(mut self, pattern: regex::Regex) -> Self {
11714 self.pattern = Some(pattern);
11715 self
11716 }
11717
11718 pub fn set_pattern(
11720 &mut self,
11721 pattern: regex::Regex,
11722 _window: &mut Window,
11723 _cx: &mut Context<Self>,
11724 ) {
11725 self.pattern = Some(pattern);
11726 }
11727
11728 pub fn validate(mut self, f: impl Fn(&str, &mut App) -> bool + 'static) -> Self {
11730 self.validate = Some(Box::new(f));
11731 self
11732 }
11733
11734 pub fn set_validator(
11735 &mut self,
11736 validate: impl Fn(&str, &mut App) -> bool + 'static,
11737 _cx: &mut Context<Self>,
11738 ) {
11739 self.validate = Some(Box::new(validate));
11740 }
11741
11742 pub fn step(mut self, step: impl Into<NumberStep>) -> Self {
11752 self.number_step = Some(step.into());
11753 self
11754 }
11755
11756 pub fn min(mut self, min: f64) -> Self {
11762 self.number_min = Some(min);
11763 self
11764 }
11765
11766 pub fn max(mut self, max: f64) -> Self {
11772 self.number_max = Some(max);
11773 self
11774 }
11775
11776 pub fn set_step(
11781 &mut self,
11782 step: impl Into<Option<NumberStep>>,
11783 _: &mut Window,
11784 _: &mut Context<Self>,
11785 ) {
11786 self.number_step = step.into();
11787 }
11788
11789 pub fn set_min(&mut self, min: Option<f64>, _: &mut Window, _: &mut Context<Self>) {
11791 self.number_min = min;
11792 }
11793
11794 pub fn set_max(&mut self, max: Option<f64>, _: &mut Window, _: &mut Context<Self>) {
11796 self.number_max = max;
11797 }
11798
11799 pub fn set_loading(&mut self, loading: bool, _: &mut Window, cx: &mut Context<Self>) {
11801 self.loading = loading;
11802 cx.notify();
11803 }
11804}
11805
11806impl<M: crate::input::MultiLineMode> InputBaseState<M> {
11809 pub fn scroll_bounce(mut self, scroll_bounce: bool) -> Self {
11817 self.scroll_bounce = scroll_bounce;
11818 self
11819 }
11820
11821 pub fn set_scroll_bounce(&mut self, scroll_bounce: bool, cx: &mut Context<Self>) {
11825 if self.scroll_bounce != scroll_bounce {
11826 self.scroll_bounce = scroll_bounce;
11827 self.overscroll.reset();
11828 cx.notify();
11829 }
11830 }
11831
11832 pub fn searchable(mut self, searchable: bool) -> Self {
11840 self.searchable = searchable;
11841 self
11842 }
11843
11844 pub fn set_searchable(&mut self, searchable: bool, cx: &mut Context<Self>) {
11846 self.searchable = searchable;
11847 cx.notify();
11848 }
11849
11850 #[doc(hidden)]
11852 pub fn soft_wrap(mut self, wrap: bool) -> Self {
11853 self.soft_wrap = wrap;
11854 self
11855 }
11856
11857 pub fn set_soft_wrap(&mut self, wrap: bool, _: &mut Window, cx: &mut Context<Self>) {
11859 self.soft_wrap = wrap;
11860 if wrap {
11861 let wrap_width = self
11862 .last_layout
11863 .as_ref()
11864 .and_then(|b| b.wrap_width)
11865 .unwrap_or(self.input_bounds.size.width);
11866
11867 self.display_map.on_layout_changed(Some(wrap_width), cx);
11868
11869 let mut offset = self.scroll_handle.offset();
11871 offset.x = px(0.);
11872 self.scroll_handle.set_offset(offset);
11873 } else {
11874 self.display_map.on_layout_changed(None, cx);
11875 }
11876 cx.notify();
11877 }
11878
11879 #[doc(hidden)]
11881 pub fn wrapping_indent(mut self, wrapping_indent: WrappingIndent) -> Self {
11882 self.wrapping_indent = wrapping_indent;
11883 self
11884 }
11885
11886 pub fn set_wrapping_indent(
11888 &mut self,
11889 wrapping_indent: WrappingIndent,
11890 _: &mut Window,
11891 cx: &mut Context<Self>,
11892 ) {
11893 self.wrapping_indent = wrapping_indent;
11894 self.display_map.set_wrapping_indent(wrapping_indent, cx);
11895 cx.notify();
11896 }
11897}
11898
11899impl InputBaseState<crate::input::TextareaMode> {
11901 pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
11906 Self::new_in_mode(window, cx)
11907 }
11908
11909 pub fn set_auto_grow(&mut self, min_rows: usize, max_rows: usize, cx: &mut Context<Self>) {
11910 self.mode = LayoutMode::auto_grow(min_rows, max_rows.max(min_rows));
11911 self.mode.update_auto_grow(&self.display_map);
11912 cx.notify();
11913 }
11914
11915 #[doc(hidden)]
11922 pub fn rows(mut self, rows: usize) -> Self {
11923 match &mut self.mode {
11924 LayoutMode::PlainText { rows: r, .. } | LayoutMode::CodeEditor { rows: r, .. } => {
11925 *r = rows
11926 }
11927 LayoutMode::AutoGrow {
11928 max_rows: max_r,
11929 rows: r,
11930 ..
11931 } => {
11932 *r = rows;
11933 *max_r = rows;
11934 }
11935 }
11936 self
11937 }
11938
11939 pub fn set_rows(&mut self, rows: usize, cx: &mut Context<Self>) {
11940 match &mut self.mode {
11941 LayoutMode::PlainText { rows: value, .. }
11942 | LayoutMode::CodeEditor { rows: value, .. } => *value = rows,
11943 LayoutMode::AutoGrow {
11944 rows: value,
11945 max_rows,
11946 ..
11947 } => {
11948 *value = rows;
11949 *max_rows = rows;
11950 }
11951 }
11952 cx.notify();
11953 }
11954
11955 pub fn auto_grow(mut self, min_rows: usize, max_rows: usize) -> Self {
11957 self.mode = LayoutMode::auto_grow(min_rows, max_rows);
11958 self
11959 }
11960}
11961
11962impl InputBaseState<crate::input::EditorMode> {
11964 pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
11975 let mut state = Self::new_in_mode(window, cx);
11976 state.mode = LayoutMode::code_editor(super::EditorLanguage::new(cx));
11977 state.searchable = true;
11978 state
11979 }
11980
11981 pub fn language(mut self, language: impl Into<SharedString>) -> Self {
11985 if let LayoutMode::CodeEditor {
11986 language: l,
11987 highlighter,
11988 ..
11989 } = &mut self.mode
11990 {
11991 l.set_name(language.into());
11992 *highlighter.borrow_mut() = None;
11993 }
11994 self
11995 }
11996
11997 pub fn language_name(&self) -> SharedString {
11999 match &self.mode {
12000 LayoutMode::CodeEditor { language, .. } => language.name(),
12001 _ => SharedString::default(),
12002 }
12003 }
12004
12005 #[doc(hidden)]
12009 pub fn folding(mut self, folding: bool) -> Self {
12010 if let LayoutMode::CodeEditor { folding: f, .. } = &mut self.mode {
12011 *f = folding;
12012 }
12013 self
12014 }
12015
12016 pub fn set_folding(&mut self, folding: bool, _: &mut Window, cx: &mut Context<Self>) {
12020 if let LayoutMode::CodeEditor { folding: f, .. } = &mut self.mode {
12021 *f = folding;
12022 }
12023 if !folding {
12024 self.display_map.clear_folds();
12025 }
12026 cx.notify();
12027 }
12028
12029 pub fn unfold_at(&mut self, position: impl Into<Position>, cx: &mut Context<Self>) -> bool {
12042 let offset = self.text.position_to_offset(&position.into());
12043 let line = self.text.offset_to_point(offset).row;
12044 let covering: Vec<usize> = self
12047 .display_map
12048 .folded_ranges()
12049 .iter()
12050 .filter(|fold| line > fold.start_line && line < fold.end_line)
12051 .map(|fold| fold.start_line)
12052 .collect();
12053 if covering.is_empty() {
12054 return false;
12055 }
12056
12057 for start_line in covering {
12058 self.display_map.set_folded(start_line, false);
12059 }
12060 cx.notify();
12061 true
12062 }
12063
12064 #[doc(hidden)]
12066 pub fn line_number(mut self, line_number: bool) -> Self {
12067 if let LayoutMode::CodeEditor { line_number: l, .. } = &mut self.mode {
12068 *l = line_number;
12069 }
12070 self
12071 }
12072
12073 pub fn set_line_number(&mut self, line_number: bool, _: &mut Window, cx: &mut Context<Self>) {
12075 if let LayoutMode::CodeEditor { line_number: l, .. } = &mut self.mode {
12076 if *l == line_number {
12077 return;
12078 }
12079 *l = line_number;
12080 }
12081 cx.notify();
12082 }
12083
12084 #[doc(hidden)]
12090 pub fn auto_close(mut self, auto_close: bool) -> Self {
12091 self.mode.set_auto_close(auto_close);
12092 self
12093 }
12094
12095 pub fn set_auto_close(&mut self, auto_close: bool, _: &mut Window, cx: &mut Context<Self>) {
12097 self.mode.set_auto_close(auto_close);
12098 cx.notify();
12099 }
12100
12101 #[doc(hidden)]
12107 pub fn smart_indent(mut self, smart_indent: bool) -> Self {
12108 self.mode.set_smart_indent(smart_indent);
12109 self
12110 }
12111
12112 pub fn set_smart_indent(&mut self, smart_indent: bool, _: &mut Window, cx: &mut Context<Self>) {
12114 self.mode.set_smart_indent(smart_indent);
12115 cx.notify();
12116 }
12117}