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, ScrollEvent, ScrollHandle, SharedString, StatefulInteractiveElement as _, Styled as _,
11 Subscription, UTF16Selection, WeakFocusHandle, Window, actions, div, point,
12 prelude::FluentBuilder as _, px,
13};
14use gpui::{Autocapitalize, TextInputAction, TextInputConfiguration};
15use ropey::{Rope, RopeSlice};
16use serde::Deserialize;
17use std::borrow::Cow;
18use std::cell::Cell;
19use std::ops::Range;
20use std::rc::Rc;
21use sum_tree::Bias;
22use unicode_segmentation::*;
23
24use super::{
25 DiagnosticSet, DisplayMap, InputContextMenuCapabilities, InputEditorStyle,
26 InputHighlighterFactory, MASK_CHAR, MaskPattern, NativeMenu, NumberStep, WrappingIndent,
27 blink_cursor::BlinkCursor,
28 change::Change,
29 cursor::{CursorSelection, Selections},
30 element::{
31 EditorScrollbar, EditorScrollbarSnapshot, LongestLineKey, TextElement,
32 clamp_horizontal_scroll_offset,
33 },
34 kind::InputModeKind,
35 mask_pattern::normalize_number_input,
36 mode::LayoutMode,
37 undo_manager::{EditIntent, UndoManager},
38};
39use crate::actions::{SelectDown, SelectLeft, SelectRight, SelectUp};
40use crate::input::blink_cursor::CURSOR_WIDTH;
41use crate::input::movement::MoveDirection;
42use crate::input::{
43 InputExtras as _, Position, RopeExt as _, element::RIGHT_MARGIN, layout::LastLayout,
44};
45use crate::overscroll::Overscroll;
46use crate::{AutoScroll, StepAction, TestSupportExt as _};
47
48pub(super) const EDITOR_ELEMENT_ID: &str = "input-state";
49
50pub(crate) enum ScrollPadding {
52 Minimal,
53 SurroundingLines,
54}
55
56#[derive(Action, Clone, PartialEq, Eq, Deserialize)]
57#[action(namespace = input, no_json)]
58pub struct Enter {
59 pub secondary: bool,
61 pub shift: bool,
63}
64
65impl Enter {
66 pub fn is_primary(action: &dyn Action) -> bool {
69 action.partial_eq(&Enter {
70 secondary: false,
71 shift: false,
72 }) || action.partial_eq(&Enter {
73 secondary: false,
74 shift: true,
75 })
76 }
77}
78
79actions!(
80 input,
81 [
82 ActivateToken,
83 Backspace,
84 Delete,
85 DeleteToBeginningOfLine,
86 DeleteToEndOfLine,
87 DeleteToPreviousWordStart,
88 DeleteToNextWordEnd,
89 Indent,
90 Outdent,
91 IndentInline,
92 OutdentInline,
93 MoveUp,
94 MoveDown,
95 MoveLeft,
96 MoveRight,
97 MoveHome,
98 MoveEnd,
99 MovePageUp,
100 MovePageDown,
101 AddCursorAbove,
102 AddCursorBelow,
103 SelectAll,
104 SelectToStartOfLine,
105 SelectToEndOfLine,
106 SelectToStart,
107 SelectToEnd,
108 SelectToPreviousWordStart,
109 SelectToNextWordEnd,
110 ShowCharacterPalette,
111 Copy,
112 Cut,
113 Paste,
114 Undo,
115 Redo,
116 MoveToStartOfLine,
117 MoveToEndOfLine,
118 MoveToStart,
119 MoveToEnd,
120 MoveToPreviousWord,
121 MoveToNextWord,
122 Escape,
123 ToggleCodeActions,
124 ShowCompletions,
125 Search,
126 Replace,
127 GoToDefinition,
128 ]
129);
130
131#[derive(Clone)]
132pub enum InputEvent {
133 Change,
134 PressEnter { secondary: bool, shift: bool },
135 Focus,
136 Blur,
137}
138
139pub(super) const CONTEXT: &str = "Input";
140
141pub(crate) fn init(cx: &mut App) {
142 cx.bind_keys([
143 KeyBinding::new("backspace", Backspace, Some(CONTEXT)),
144 KeyBinding::new("shift-backspace", Backspace, Some(CONTEXT)),
145 #[cfg(target_os = "macos")]
146 KeyBinding::new("ctrl-backspace", Backspace, Some(CONTEXT)),
147 KeyBinding::new("delete", Delete, Some(CONTEXT)),
148 KeyBinding::new("shift-delete", Delete, Some(CONTEXT)),
149 #[cfg(target_os = "macos")]
150 KeyBinding::new("cmd-backspace", DeleteToBeginningOfLine, Some(CONTEXT)),
151 #[cfg(target_os = "macos")]
152 KeyBinding::new("cmd-delete", DeleteToEndOfLine, Some(CONTEXT)),
153 #[cfg(target_os = "macos")]
154 KeyBinding::new("alt-backspace", DeleteToPreviousWordStart, Some(CONTEXT)),
155 #[cfg(not(target_os = "macos"))]
156 KeyBinding::new("ctrl-backspace", DeleteToPreviousWordStart, Some(CONTEXT)),
157 #[cfg(target_os = "macos")]
158 KeyBinding::new("alt-delete", DeleteToNextWordEnd, Some(CONTEXT)),
159 #[cfg(not(target_os = "macos"))]
160 KeyBinding::new("ctrl-delete", DeleteToNextWordEnd, Some(CONTEXT)),
161 KeyBinding::new(
162 "enter",
163 Enter {
164 secondary: false,
165 shift: false,
166 },
167 Some(CONTEXT),
168 ),
169 KeyBinding::new(
170 "shift-enter",
171 Enter {
172 secondary: false,
173 shift: true,
174 },
175 Some(CONTEXT),
176 ),
177 KeyBinding::new(
178 if cx.platform_info().is_mac() {
179 "cmd-enter"
180 } else {
181 "ctrl-enter"
182 },
183 Enter {
184 secondary: true,
185 shift: false,
186 },
187 Some(CONTEXT),
188 ),
189 KeyBinding::new("escape", Escape, Some(CONTEXT)),
190 KeyBinding::new("up", MoveUp, Some(CONTEXT)),
191 KeyBinding::new("down", MoveDown, Some(CONTEXT)),
192 KeyBinding::new("left", MoveLeft, Some(CONTEXT)),
193 KeyBinding::new("right", MoveRight, Some(CONTEXT)),
194 KeyBinding::new("pageup", MovePageUp, Some(CONTEXT)),
195 KeyBinding::new("pagedown", MovePageDown, Some(CONTEXT)),
196 KeyBinding::new("tab", IndentInline, Some(CONTEXT)),
197 KeyBinding::new("shift-tab", OutdentInline, Some(CONTEXT)),
198 #[cfg(target_os = "macos")]
199 KeyBinding::new("cmd-]", Indent, Some(CONTEXT)),
200 #[cfg(not(target_os = "macos"))]
201 KeyBinding::new("ctrl-]", Indent, Some(CONTEXT)),
202 #[cfg(target_os = "macos")]
203 KeyBinding::new("cmd-[", Outdent, Some(CONTEXT)),
204 #[cfg(not(target_os = "macos"))]
205 KeyBinding::new("ctrl-[", Outdent, Some(CONTEXT)),
206 KeyBinding::new("shift-left", SelectLeft, Some(CONTEXT)),
207 KeyBinding::new("shift-right", SelectRight, Some(CONTEXT)),
208 KeyBinding::new("shift-up", SelectUp, Some(CONTEXT)),
209 KeyBinding::new("shift-down", SelectDown, Some(CONTEXT)),
210 #[cfg(not(any(target_os = "macos", target_os = "linux")))]
211 KeyBinding::new("shift-alt-left", SelectLeft, Some(CONTEXT)),
212 #[cfg(not(any(target_os = "macos", target_os = "linux")))]
213 KeyBinding::new("shift-alt-right", SelectRight, Some(CONTEXT)),
214 #[cfg(target_os = "macos")]
216 KeyBinding::new("cmd-alt-up", AddCursorAbove, Some(CONTEXT)),
217 #[cfg(target_os = "macos")]
218 KeyBinding::new("cmd-alt-down", AddCursorBelow, Some(CONTEXT)),
219 #[cfg(target_os = "windows")]
220 KeyBinding::new("ctrl-alt-up", AddCursorAbove, Some(CONTEXT)),
221 #[cfg(not(any(target_os = "macos", target_os = "windows")))]
222 KeyBinding::new("shift-alt-up", AddCursorAbove, Some(CONTEXT)),
223 #[cfg(target_os = "windows")]
224 KeyBinding::new("ctrl-alt-down", AddCursorBelow, Some(CONTEXT)),
225 #[cfg(not(any(target_os = "macos", target_os = "windows")))]
226 KeyBinding::new("shift-alt-down", AddCursorBelow, Some(CONTEXT)),
227 KeyBinding::new("home", MoveHome, Some(CONTEXT)),
228 KeyBinding::new("end", MoveEnd, Some(CONTEXT)),
229 #[cfg(not(target_os = "macos"))]
230 KeyBinding::new("ctrl-home", MoveToStart, Some(CONTEXT)),
231 #[cfg(not(target_os = "macos"))]
232 KeyBinding::new("ctrl-end", MoveToEnd, Some(CONTEXT)),
233 #[cfg(not(target_os = "macos"))]
234 KeyBinding::new("ctrl-shift-home", SelectToStart, Some(CONTEXT)),
235 #[cfg(not(target_os = "macos"))]
236 KeyBinding::new("ctrl-shift-end", SelectToEnd, Some(CONTEXT)),
237 KeyBinding::new("shift-home", SelectToStartOfLine, Some(CONTEXT)),
238 KeyBinding::new("shift-end", SelectToEndOfLine, Some(CONTEXT)),
239 #[cfg(target_os = "macos")]
240 KeyBinding::new("ctrl-shift-a", SelectToStartOfLine, Some(CONTEXT)),
241 #[cfg(target_os = "macos")]
242 KeyBinding::new("ctrl-shift-e", SelectToEndOfLine, Some(CONTEXT)),
243 #[cfg(target_os = "macos")]
244 KeyBinding::new("shift-cmd-left", SelectToStartOfLine, Some(CONTEXT)),
245 #[cfg(target_os = "macos")]
246 KeyBinding::new("shift-cmd-right", SelectToEndOfLine, Some(CONTEXT)),
247 #[cfg(any(target_os = "macos", target_os = "linux"))]
248 KeyBinding::new("alt-shift-left", SelectToPreviousWordStart, Some(CONTEXT)),
249 #[cfg(not(target_os = "macos"))]
250 KeyBinding::new("ctrl-shift-left", SelectToPreviousWordStart, Some(CONTEXT)),
251 #[cfg(any(target_os = "macos", target_os = "linux"))]
252 KeyBinding::new("alt-shift-right", SelectToNextWordEnd, Some(CONTEXT)),
253 #[cfg(not(target_os = "macos"))]
254 KeyBinding::new("ctrl-shift-right", SelectToNextWordEnd, Some(CONTEXT)),
255 #[cfg(target_os = "macos")]
256 KeyBinding::new("ctrl-cmd-space", ShowCharacterPalette, Some(CONTEXT)),
257 #[cfg(target_os = "macos")]
258 KeyBinding::new("cmd-a", SelectAll, Some(CONTEXT)),
259 #[cfg(not(target_os = "macos"))]
260 KeyBinding::new("ctrl-a", SelectAll, Some(CONTEXT)),
261 #[cfg(target_os = "macos")]
262 KeyBinding::new("cmd-c", Copy, Some(CONTEXT)),
263 #[cfg(not(target_os = "macos"))]
264 KeyBinding::new("ctrl-c", Copy, Some(CONTEXT)),
265 #[cfg(target_os = "macos")]
266 KeyBinding::new("cmd-x", Cut, Some(CONTEXT)),
267 #[cfg(not(target_os = "macos"))]
268 KeyBinding::new("ctrl-x", Cut, Some(CONTEXT)),
269 #[cfg(target_os = "macos")]
270 KeyBinding::new("cmd-v", Paste, Some(CONTEXT)),
271 #[cfg(not(target_os = "macos"))]
272 KeyBinding::new("ctrl-v", Paste, Some(CONTEXT)),
273 #[cfg(target_os = "macos")]
274 KeyBinding::new("ctrl-a", MoveHome, Some(CONTEXT)),
275 #[cfg(target_os = "macos")]
276 KeyBinding::new("cmd-left", MoveHome, Some(CONTEXT)),
277 #[cfg(target_os = "macos")]
278 KeyBinding::new("ctrl-e", MoveEnd, Some(CONTEXT)),
279 #[cfg(target_os = "macos")]
280 KeyBinding::new("cmd-right", MoveEnd, Some(CONTEXT)),
281 #[cfg(target_os = "macos")]
282 KeyBinding::new("cmd-z", Undo, Some(CONTEXT)),
283 #[cfg(target_os = "macos")]
284 KeyBinding::new("cmd-shift-z", Redo, Some(CONTEXT)),
285 #[cfg(target_os = "macos")]
286 KeyBinding::new("cmd-up", MoveToStart, Some(CONTEXT)),
287 #[cfg(target_os = "macos")]
288 KeyBinding::new("cmd-down", MoveToEnd, Some(CONTEXT)),
289 #[cfg(target_os = "macos")]
290 KeyBinding::new("alt-left", MoveToPreviousWord, Some(CONTEXT)),
291 #[cfg(target_os = "macos")]
292 KeyBinding::new("alt-right", MoveToNextWord, Some(CONTEXT)),
293 #[cfg(not(target_os = "macos"))]
294 KeyBinding::new("ctrl-left", MoveToPreviousWord, Some(CONTEXT)),
295 #[cfg(not(target_os = "macos"))]
296 KeyBinding::new("ctrl-right", MoveToNextWord, Some(CONTEXT)),
297 #[cfg(target_os = "macos")]
298 KeyBinding::new("cmd-shift-up", SelectToStart, Some(CONTEXT)),
299 #[cfg(target_os = "macos")]
300 KeyBinding::new("cmd-shift-down", SelectToEnd, Some(CONTEXT)),
301 #[cfg(not(target_os = "macos"))]
302 KeyBinding::new("ctrl-z", Undo, Some(CONTEXT)),
303 #[cfg(not(target_os = "macos"))]
304 KeyBinding::new("ctrl-y", Redo, Some(CONTEXT)),
305 #[cfg(target_os = "macos")]
306 KeyBinding::new("cmd-.", ToggleCodeActions, Some(CONTEXT)),
307 #[cfg(not(target_os = "macos"))]
308 KeyBinding::new("ctrl-.", ToggleCodeActions, Some(CONTEXT)),
309 #[cfg(target_os = "macos")]
310 KeyBinding::new("cmd-f", Search, Some(CONTEXT)),
311 #[cfg(not(target_os = "macos"))]
312 KeyBinding::new("ctrl-f", Search, Some(CONTEXT)),
313 #[cfg(target_os = "macos")]
314 KeyBinding::new("cmd-shift-f", Replace, Some(CONTEXT)),
315 #[cfg(not(target_os = "macos"))]
316 KeyBinding::new("ctrl-h", Replace, Some(CONTEXT)),
317 ]);
318}
319
320#[derive(Debug, Clone, Copy, PartialEq, Eq)]
327pub(super) struct ColumnarPoint {
328 offset: usize,
329 columns_past_line_end: usize,
330}
331
332impl ColumnarPoint {
333 pub(super) fn new(offset: usize, columns_past_line_end: usize) -> Self {
334 Self {
335 offset,
336 columns_past_line_end,
337 }
338 }
339}
340
341#[derive(Debug, PartialEq)]
345struct PasteTarget {
346 document_revision: u64,
347 selections: Vec<CursorSelection>,
348}
349
350#[derive(Clone, Copy, Debug)]
357#[doc(hidden)]
358pub struct PrepaintCaretGeometry {
359 cursor_bounds: Bounds<Pixels>,
360 line_height: Pixels,
361 input_bounds: Bounds<Pixels>,
362 scroll_offset: Point<Pixels>,
363 pub(super) resize_reveal_owned: bool,
365}
366
367impl PrepaintCaretGeometry {
368 pub(super) fn new(
369 cursor_bounds: Bounds<Pixels>,
370 line_height: Pixels,
371 input_bounds: Bounds<Pixels>,
372 scroll_offset: Point<Pixels>,
373 ) -> Self {
374 Self {
375 cursor_bounds,
376 line_height,
377 input_bounds,
378 scroll_offset,
379 resize_reveal_owned: false,
380 }
381 }
382
383 pub fn cursor_bounds(&self) -> Bounds<Pixels> {
385 self.cursor_bounds
386 }
387
388 pub fn line_height(&self) -> Pixels {
390 self.line_height
391 }
392
393 pub fn input_bounds(&self) -> Bounds<Pixels> {
395 self.input_bounds
396 }
397
398 pub fn scroll_offset(&self) -> Point<Pixels> {
401 self.scroll_offset
402 }
403}
404
405pub struct InputBaseState<M: InputModeKind> {
416 pub(crate) extras: M::Extras,
418 pub(super) focus_handle: FocusHandle,
419 selection_focus: Option<WeakFocusHandle>,
420 pub(super) mode: LayoutMode,
421 pub(super) text: Rope,
422 pub(super) display_map: DisplayMap,
423 pub(super) undo_manager: UndoManager,
424 pub(super) inline_tokens: Option<Box<super::inline_tokens::InlineTokenStore>>,
425 pub(super) pending_token: Option<super::InlineToken>,
426 pub(super) replaying_history: bool,
427 pub(super) validated_token_edit: bool,
428 pub(super) document_revision: u64,
429 pub(super) token_presentation: super::InlineTokenPresentation,
430 pub(super) token_layout_cache: Option<Box<super::token_presentation::TokenLayoutCache>>,
431 pub(super) token_bounds: std::collections::HashMap<usize, Bounds<Pixels>>,
434 pub(super) hovered_token: Option<super::token_presentation::HoverSnapshot>,
439 pub(super) pending_token_hover_exits: Vec<super::token_presentation::HoverSnapshot>,
440 pub(super) pressed_token: Option<(usize, u64, Point<Pixels>)>,
443 pub(super) search_session: super::SearchSession,
444 pub(super) search_activation_revision: u64,
447 pub(super) searchable: bool,
448 pub(super) replaceable: bool,
449 pub(super) soft_wrap: bool,
450 pub(super) wrapping_indent: WrappingIndent,
451 pub(super) scroll_beyond_last_line: Option<usize>,
452 pub(super) cursor_surrounding_lines: Option<usize>,
453 pub(super) blink_cursor: Entity<BlinkCursor>,
454 pub(super) loading: bool,
455 pub(super) selections: Selections,
459 pub(super) selected_word_range: Option<CursorSelection>,
461 pub(super) ime_marked_range: Option<CursorSelection>,
463 pub(super) last_layout: Option<LastLayout>,
464 pub(crate) prepaint_caret_geometry: Option<PrepaintCaretGeometry>,
470 pub(super) last_cursor: Option<usize>,
471 pub(super) input_bounds: Bounds<Pixels>,
473 pub(super) last_bounds: Option<Bounds<Pixels>>,
475 pub(super) revealed_viewport: Option<Bounds<Pixels>>,
478 pub(super) last_selected_range: Option<CursorSelection>,
479 pub(super) selecting: bool,
480 pub(super) column_select_start: Option<ColumnarPoint>,
482 pub(super) touch_selection: super::touch::TouchSelection,
484 pub(crate) disabled: bool,
485 pub(crate) readonly: bool,
486 pub(crate) text_align: TextAlign,
487 pub(super) masked: bool,
488 pub(super) clean_on_escape: bool,
489 pub(super) submit_on_enter: bool,
490 pub(super) input_action: Option<TextInputAction>,
492 pub(super) autocorrect: Option<bool>,
493 pub(super) autocapitalize: Option<Autocapitalize>,
494 pub(super) show_whitespaces: bool,
495 pub(crate) cursor_line_end_affinity: bool,
497 pub(super) pattern: Option<regex::Regex>,
498 pub(super) validate: Option<Box<dyn Fn(&str, &mut App) -> bool + 'static>>,
499 pub(crate) number_step: Option<NumberStep>,
502 pub(crate) number_min: Option<f64>,
504 pub(crate) number_max: Option<f64>,
506 pub(crate) scroll_handle: ScrollHandle,
507 scroll_bounce: bool,
509 pub(super) overscroll: Overscroll,
511 pub(crate) deferred_scroll_offset: Option<Point<Pixels>>,
513 pub(crate) scroll_size: gpui::Size<Pixels>,
515 pub(super) editor_scrollbar_snapshot: Cell<Option<EditorScrollbarSnapshot>>,
516 pub(super) longest_line_width: Cell<Option<(LongestLineKey, Pixels)>>,
518 pub(super) editor_paddings: Edges<Pixels>,
519 pub(super) editor_style: InputEditorStyle,
524 projected_editor_style: InputEditorStyle,
529 accessibility: Option<InputAccessibility>,
530 pub(super) scrollbar_renderer: Option<crate::input::EditorScrollbarRenderer>,
533
534 pub(crate) mask_pattern: MaskPattern,
536 pub(super) mask_pattern_set: bool,
540 pub(super) placeholder: SharedString,
541
542 pub(super) diagnostic_popover: Option<Rc<crate::input::DiagnosticEntry>>,
544
545 context_menu_handler: Option<
546 Rc<dyn Fn(NativeMenu, InputContextMenuCapabilities, Point<Pixels>, &mut Window, &mut App)>,
547 >,
548 pending_context_menu: Option<(Point<Pixels>, usize)>,
549
550 pub(super) enable_context_menu: bool,
553
554 pub(super) completion_inserting: bool,
556 pub(super) overlay_action_handler: Option<
557 Rc<
558 dyn Fn(
559 super::InputOverlayKind,
560 Box<dyn Action>,
561 &mut Window,
562 &mut Context<InputBaseState<M>>,
563 ) -> bool,
564 >,
565 >,
566
567 _pending_update: bool,
571 pub(super) silent_replace_text: bool,
573 pub(super) emit_events: bool,
575
576 _subscriptions: Vec<Subscription>,
577
578 pub(super) auto_scroll: AutoScroll,
579}
580
581#[derive(Clone, Debug)]
588pub struct InputAccessibility {
589 role: Option<gpui::Role>,
590 label: Option<SharedString>,
591 placeholder: Option<SharedString>,
592 accessibility_id: Option<SharedString>,
593 value_exposed: bool,
594}
595
596impl InputAccessibility {
597 pub fn new(role: Option<gpui::Role>) -> Self {
598 Self {
599 role,
600 label: None,
601 placeholder: None,
602 accessibility_id: None,
603 value_exposed: true,
604 }
605 }
606
607 pub fn with_label(mut self, label: Option<SharedString>) -> Self {
608 self.label = label;
609 self
610 }
611
612 pub fn with_placeholder(mut self, placeholder: Option<SharedString>) -> Self {
613 self.placeholder = placeholder;
614 self
615 }
616
617 pub fn with_accessibility_id(mut self, id: Option<SharedString>) -> Self {
618 self.accessibility_id = id;
619 self
620 }
621
622 pub fn with_value_exposed(mut self, exposed: bool) -> Self {
624 self.value_exposed = exposed;
625 self
626 }
627
628 pub fn role(&self) -> Option<gpui::Role> {
629 self.role
630 }
631
632 pub fn label(&self) -> Option<&SharedString> {
633 self.label.as_ref()
634 }
635
636 pub fn placeholder(&self) -> Option<&SharedString> {
637 self.placeholder.as_ref()
638 }
639
640 pub fn accessibility_id(&self) -> Option<&SharedString> {
641 self.accessibility_id.as_ref()
642 }
643
644 pub fn value_exposed(&self) -> bool {
645 self.value_exposed
646 }
647}
648
649#[derive(Clone)]
654pub struct InputPresentation {
655 focus_handle: FocusHandle,
656 disabled: bool,
657 readonly: bool,
658 loading: bool,
659 masked: bool,
660 multi_line: bool,
661 code_editor: bool,
662 text_align: TextAlign,
663 placeholder: SharedString,
664 mask_placeholder: Option<String>,
665}
666
667impl InputPresentation {
668 pub fn focus_handle(&self) -> &FocusHandle {
669 &self.focus_handle
670 }
671
672 pub fn is_disabled(&self) -> bool {
673 self.disabled
674 }
675
676 pub fn is_readonly(&self) -> bool {
677 self.readonly
678 }
679
680 pub fn is_editable(&self) -> bool {
684 !self.disabled && !self.readonly
685 }
686
687 pub fn is_loading(&self) -> bool {
688 self.loading
689 }
690
691 pub fn is_masked(&self) -> bool {
692 self.masked
693 }
694
695 pub fn is_multi_line(&self) -> bool {
696 self.multi_line
697 }
698
699 pub fn is_code_editor(&self) -> bool {
700 self.code_editor
701 }
702
703 pub fn text_align(&self) -> TextAlign {
704 self.text_align
705 }
706
707 pub fn placeholder(&self) -> &SharedString {
708 &self.placeholder
709 }
710
711 pub fn mask_placeholder(&self) -> Option<&str> {
713 self.mask_placeholder.as_deref()
714 }
715}
716
717impl<M: InputModeKind> EventEmitter<InputEvent> for InputBaseState<M> {}
718
719impl<M: InputModeKind> InputBaseState<M> {
720 #[doc(hidden)]
721 pub fn cursor_layout(&self) -> Option<(Bounds<Pixels>, Pixels)> {
722 let layout = self.last_layout.as_ref()?;
723 Some((layout.cursor_bounds?, layout.line_height))
724 }
725
726 #[doc(hidden)]
731 pub fn prepaint_caret_geometry(&self) -> Option<PrepaintCaretGeometry> {
732 self.prepaint_caret_geometry
733 }
734
735 pub fn input_bounds(&self) -> Bounds<Pixels> {
736 self.input_bounds
737 }
738
739 pub fn text_bounds(&self) -> Option<Bounds<Pixels>> {
740 self.last_bounds
741 }
742
743 pub fn display_map(&self) -> &DisplayMap {
754 &self.display_map
755 }
756
757 pub fn prewrap(
768 &mut self,
769 font: gpui::Font,
770 font_size: Pixels,
771 width: Pixels,
772 cx: &mut Context<Self>,
773 ) {
774 if self.last_layout.is_some() {
775 return;
776 }
777 let wrap_width = (width - RIGHT_MARGIN).max(px(1.));
778 self.display_map.set_font(font, font_size, cx);
779 self.display_map
780 .set_tab_size(self.mode.tab_size().tab_size, cx);
781 self.display_map
782 .on_layout_changed(self.soft_wrap.then_some(wrap_width), cx);
783 self.display_map
784 .set_wrapping_indent(self.wrapping_indent, cx);
785 self.display_map.ensure_text_prepared(&self.text, cx);
786 }
787
788 pub fn diagnostic_popover(&self) -> Option<Rc<crate::input::DiagnosticEntry>> {
789 self.diagnostic_popover.clone()
790 }
791
792 pub fn presentation(&self) -> InputPresentation {
793 InputPresentation {
794 focus_handle: self.focus_handle.clone(),
795 disabled: self.disabled,
796 readonly: self.readonly,
797 loading: self.loading,
798 masked: self.masked,
799 multi_line: self.is_multi_line(),
800 code_editor: self.is_code_editor(),
801 text_align: self.text_align,
802 placeholder: self.placeholder.clone(),
803 mask_placeholder: self.mask_pattern.placeholder(),
804 }
805 }
806
807 #[inline]
812 pub(crate) fn shows_scrollbar(&self) -> bool {
820 self.is_multi_line()
821 }
822
823 pub fn is_multi_line(&self) -> bool {
824 M::MULTI_LINE
825 }
826
827 #[inline]
829 pub fn is_single_line(&self) -> bool {
830 !M::MULTI_LINE
831 }
832
833 #[inline]
835 pub fn is_code_editor(&self) -> bool {
836 M::CODE_EDITOR
837 }
838
839 pub fn is_copyable(&self) -> bool {
843 self.selections.iter().any(|sel| !sel.is_empty()) && !self.masked
844 }
845
846 pub fn context_menu_capabilities(&self) -> InputContextMenuCapabilities {
847 let (go_to_definition, code_actions) = self.extras.context_menu_capabilities();
848 InputContextMenuCapabilities::new()
849 .disabled(self.disabled)
850 .readonly(self.readonly)
851 .code_editor(self.is_code_editor())
852 .selection(!self.active_selection().is_empty())
853 .masked(self.masked)
854 .go_to_definition(go_to_definition)
855 .code_actions(code_actions)
856 }
857
858 pub fn set_text_align(&mut self, text_align: TextAlign, cx: &mut Context<Self>) {
859 if !self.is_single_line() || self.text_align == text_align {
860 return;
861 }
862
863 self.text_align = text_align;
864 cx.notify();
865 }
866
867 pub fn toggle_masked(&mut self, _: &mut Window, cx: &mut Context<Self>) {
873 self.masked = !self.masked;
874 cx.notify();
875 }
876
877 pub fn on_context_menu(
878 &mut self,
879 handler: Rc<
880 dyn Fn(NativeMenu, InputContextMenuCapabilities, Point<Pixels>, &mut Window, &mut App),
881 >,
882 ) {
883 self.context_menu_handler = Some(handler);
884 }
885
886 pub fn set_selection_focus(&mut self, focus: Option<FocusHandle>, cx: &mut Context<Self>) {
891 self.selection_focus = focus.as_ref().map(FocusHandle::downgrade);
892 cx.notify();
893 }
894
895 pub fn has_selection_focus(&self, window: &Window, cx: &App) -> bool {
897 self.focus_handle.is_focused(window)
898 || self
899 .selection_focus
900 .as_ref()
901 .and_then(WeakFocusHandle::upgrade)
902 .is_some_and(|focus| focus.is_focused(window) || focus.contains_focused(window, cx))
903 }
904
905 fn new_in_mode(window: &mut Window, cx: &mut Context<Self>) -> Self {
907 let focus_handle = cx.focus_handle().tab_stop(true);
908 let blink_cursor = cx.new(|_| BlinkCursor::new());
909 let undo_manager = UndoManager::new();
910
911 let _subscriptions = vec![
912 cx.intercept_keystrokes({
915 let focus_handle = focus_handle.clone();
916 let blink_cursor = blink_cursor.downgrade();
917 move |_, window, cx| {
918 if focus_handle.is_focused(window) {
919 _ = blink_cursor.update(cx, |cursor, cx| cursor.pause(cx));
920 }
921 }
922 }),
923 cx.observe(&blink_cursor, |_, _, cx| cx.notify()),
925 cx.observe_window_activation(window, |input, window, cx| {
927 if window.is_window_active() {
928 let focus_handle = input.focus_handle.clone();
929 if focus_handle.is_focused(window) {
930 input.blink_cursor.update(cx, |blink_cursor, cx| {
931 blink_cursor.start(cx);
932 });
933 }
934 }
935 }),
936 cx.on_focus(&focus_handle, window, Self::on_focus),
937 cx.on_blur(&focus_handle, window, Self::on_blur),
938 ];
939
940 let text_style = window.text_style();
941
942 Self {
943 extras: M::Extras::default(),
944 focus_handle: focus_handle.clone(),
945 selection_focus: None,
946 text: "".into(),
947 display_map: DisplayMap::new(text_style.font(), window.rem_size(), None),
948 search_session: super::SearchSession::default(),
949 search_activation_revision: 0,
950 searchable: false,
951 replaceable: true,
952 soft_wrap: true,
953 wrapping_indent: WrappingIndent::default(),
954 scroll_beyond_last_line: None,
955 cursor_surrounding_lines: None,
956 blink_cursor,
957 undo_manager,
958 inline_tokens: None,
959 pending_token: None,
960 replaying_history: false,
961 validated_token_edit: false,
962 document_revision: 0,
963 token_presentation: Default::default(),
964 token_bounds: Default::default(),
965 hovered_token: None,
966 pending_token_hover_exits: Vec::new(),
967 token_layout_cache: None,
968 pressed_token: None,
969 selections: Selections::default(),
970 selected_word_range: None,
971 ime_marked_range: None,
972 input_bounds: Bounds::default(),
973 selecting: false,
974 touch_selection: Default::default(),
975 disabled: false,
976 readonly: false,
977 text_align: TextAlign::Left,
978 masked: false,
979 clean_on_escape: false,
980 submit_on_enter: false,
981 input_action: None,
982 autocorrect: None,
983 autocapitalize: None,
984 show_whitespaces: false,
985 loading: false,
986 pattern: None,
987 validate: None,
988 number_step: Some(NumberStep::Fixed(1.)),
989 number_min: None,
990 number_max: None,
991 mode: LayoutMode::default(),
992 last_layout: None,
993 prepaint_caret_geometry: None,
994 last_bounds: None,
995 revealed_viewport: None,
996 last_selected_range: None,
997 column_select_start: None,
998 last_cursor: None,
999 scroll_handle: ScrollHandle::new(),
1000 scroll_bounce: crate::ScrollBounce::enabled_by_default(),
1001 overscroll: Overscroll::default(),
1002 scroll_size: gpui::size(px(0.), px(0.)),
1003 editor_scrollbar_snapshot: Cell::new(None),
1004 longest_line_width: Cell::new(None),
1005 editor_paddings: Edges::default(),
1006 deferred_scroll_offset: None,
1007 placeholder: SharedString::default(),
1008 mask_pattern: MaskPattern::default(),
1009 mask_pattern_set: false,
1010 editor_style: InputEditorStyle::default(),
1011 projected_editor_style: InputEditorStyle::default(),
1012 accessibility: None,
1013 scrollbar_renderer: None,
1014 diagnostic_popover: None,
1015 context_menu_handler: None,
1016 pending_context_menu: None,
1017 enable_context_menu: true,
1018 completion_inserting: false,
1019 overlay_action_handler: None,
1020 silent_replace_text: false,
1021 emit_events: true,
1022 _subscriptions,
1023 _pending_update: false,
1024 cursor_line_end_affinity: false,
1025 auto_scroll: AutoScroll::default(),
1026 }
1027 }
1028
1029 pub fn context_menu(mut self, enable: bool) -> Self {
1036 self.enable_context_menu = enable;
1037 self
1038 }
1039
1040 pub fn set_context_menu_enabled(&mut self, enabled: bool) {
1041 self.enable_context_menu = enabled;
1042 }
1043
1044 #[doc(hidden)]
1046 pub fn replaceable(mut self, allow: bool) -> Self {
1047 self.replaceable = allow;
1048 self
1049 }
1050
1051 pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
1053 self.placeholder = placeholder.into();
1054 self
1055 }
1056
1057 pub fn set_highlighter(
1059 &mut self,
1060 new_language: impl Into<SharedString>,
1061 cx: &mut Context<Self>,
1062 ) {
1063 match &mut self.mode {
1064 LayoutMode::CodeEditor {
1065 language,
1066 highlighter,
1067 ..
1068 } => {
1069 language.set_name(new_language.into());
1070 *highlighter.borrow_mut() = None;
1071 }
1072 _ => {}
1073 }
1074 self.refresh(cx);
1075 }
1076
1077 fn reset_highlighter(&mut self, cx: &mut Context<Self>) {
1078 match &mut self.mode {
1079 LayoutMode::CodeEditor { highlighter, .. } => {
1080 *highlighter.borrow_mut() = None;
1081 }
1082 _ => {}
1083 }
1084 cx.notify();
1085 }
1086
1087 pub fn set_highlighter_factory(
1089 &mut self,
1090 factory: InputHighlighterFactory,
1091 cx: &mut Context<Self>,
1092 ) {
1093 self.mode.set_highlighter_factory(factory);
1094 self._pending_update = true;
1095 self.token_layout_cache = None;
1096 cx.notify();
1097 }
1098
1099 pub fn ensure_highlighter_factory(&mut self, factory: InputHighlighterFactory) {
1101 self.mode.ensure_highlighter_factory(factory);
1102 }
1103
1104 pub fn set_editor_style(&mut self, style: InputEditorStyle) {
1105 self.editor_style = style.clone();
1106 self.projected_editor_style = style;
1107 }
1108
1109 pub fn set_accessibility(&mut self, accessibility: InputAccessibility) {
1111 self.accessibility = Some(accessibility);
1112 }
1113
1114 fn handle_accessibility_focus(&self, window: &mut Window, cx: &mut Context<Self>) {
1115 if !self.disabled {
1116 self.focus(window, cx);
1117 }
1118 }
1119
1120 fn handle_accessibility_set_value(
1121 &mut self,
1122 data: Option<&gpui::accesskit::ActionData>,
1123 window: &mut Window,
1124 cx: &mut Context<Self>,
1125 ) {
1126 let Some(gpui::accesskit::ActionData::Value(value)) = data else {
1127 return;
1128 };
1129 if self.is_editable() {
1131 self.replace_all(value.to_string(), window, cx);
1132 }
1133 }
1134
1135 #[doc(hidden)]
1137 pub fn set_editor_paddings(&mut self, paddings: Edges<Pixels>) {
1138 self.editor_paddings = paddings;
1139 }
1140
1141 pub fn set_scrollbar_renderer(
1149 &mut self,
1150 renderer: Option<crate::input::EditorScrollbarRenderer>,
1151 ) {
1152 self.scrollbar_renderer = renderer;
1153 }
1154
1155 pub fn apply_highlighter_fold_candidates(
1156 &mut self,
1157 candidates: Vec<crate::input::FoldRange>,
1158 cx: &mut Context<Self>,
1159 ) {
1160 if self.mode.is_folding() {
1161 self.display_map.set_fold_candidates(candidates);
1162 }
1163 cx.notify();
1164 }
1165
1166 #[inline]
1167 pub fn diagnostics(&self) -> Option<&DiagnosticSet> {
1168 self.mode.diagnostics()
1169 }
1170
1171 #[inline]
1172 pub fn diagnostics_mut(&mut self) -> Option<&mut DiagnosticSet> {
1173 self.mode.diagnostics_mut()
1174 }
1175
1176 pub fn set_placeholder(
1178 &mut self,
1179 placeholder: impl Into<SharedString>,
1180 _: &mut Window,
1181 cx: &mut Context<Self>,
1182 ) {
1183 self.placeholder = placeholder.into();
1184 cx.notify();
1185 }
1186
1187 pub(super) fn line_and_position_for_offset(
1195 &self,
1196 offset: usize,
1197 ) -> (usize, usize, Option<Point<Pixels>>) {
1198 let Some(last_layout) = &self.last_layout else {
1199 return (0, 0, None);
1200 };
1201 let line_height = last_layout.line_height;
1202
1203 let mut y_offset = last_layout.visible_top;
1204 for (vi, line) in last_layout.lines.iter().enumerate() {
1205 let prev_lines_offset = last_layout.visible_line_byte_offsets[vi];
1206 let local_offset = offset.saturating_sub(prev_lines_offset);
1207 if let Some(pos) = line.position_for_index(local_offset, last_layout, false) {
1208 let sub_line_index = (pos.y / line_height) as usize;
1209 let adjusted_pos = point(pos.x + last_layout.line_number_width, pos.y + y_offset);
1210 return (vi, sub_line_index, Some(adjusted_pos));
1211 }
1212
1213 y_offset += line.size(line_height).height;
1214 }
1215 (0, 0, None)
1216 }
1217
1218 pub fn set_value(
1227 &mut self,
1228 value: impl Into<super::InputContent>,
1229 window: &mut Window,
1230 cx: &mut Context<Self>,
1231 ) {
1232 let content = value.into();
1233 self.inline_tokens = None;
1234 self.undo_manager.set_ignoring(true);
1235 self.emit_events = false;
1236 self.replace_text(content.text().clone(), window, cx);
1237 self.install_tokens(content);
1238 self.undo_manager.set_ignoring(false);
1239 self.emit_events = true;
1240
1241 self.reset_selection();
1242 self.reset_lsp_state();
1243 self.reset_scroll_to_start();
1244
1245 self.undo_manager.clear();
1246 cx.notify();
1247 }
1248
1249 pub fn replace_all(
1259 &mut self,
1260 text: impl Into<SharedString>,
1261 window: &mut Window,
1262 cx: &mut Context<Self>,
1263 ) {
1264 self.replace_text(text, window, cx);
1265 self.reset_selection();
1266 self.reset_lsp_state();
1267 self.reset_scroll_to_start();
1268
1269 cx.notify();
1270 }
1271
1272 pub(super) fn with_edits_allowed(&mut self, f: impl FnOnce(&mut Self)) {
1277 let (was_disabled, was_readonly) = (self.disabled, self.readonly);
1278 (self.disabled, self.readonly) = (false, false);
1279 f(self);
1280 (self.disabled, self.readonly) = (was_disabled, was_readonly);
1281 }
1282
1283 pub fn insert(
1287 &mut self,
1288 text: impl Into<SharedString>,
1289 window: &mut Window,
1290 cx: &mut Context<Self>,
1291 ) {
1292 let text: SharedString = text.into();
1293 self.with_edits_allowed(|this| {
1294 this.undo_manager.set_pending_intent(EditIntent::Atomic);
1295 let range_utf16 = this.range_to_utf16(&(this.cursor()..this.cursor()));
1296 this.replace_text_in_range_silent(Some(range_utf16), &text, window, cx);
1297 let end = this.active_selection().end;
1298 this.set_cursor_to(end);
1299 });
1300 }
1301
1302 pub fn replace(
1306 &mut self,
1307 text: impl Into<SharedString>,
1308 window: &mut Window,
1309 cx: &mut Context<Self>,
1310 ) {
1311 let text: SharedString = text.into();
1312 self.with_edits_allowed(|this| {
1313 this.undo_manager.set_pending_intent(EditIntent::Atomic);
1314 this.replace_text_in_range_silent(None, &text, window, cx);
1315 let end = this.active_selection().end;
1316 this.set_cursor_to(end);
1317 });
1318 }
1319
1320 fn replace_text(
1321 &mut self,
1322 text: impl Into<SharedString>,
1323 window: &mut Window,
1324 cx: &mut Context<Self>,
1325 ) {
1326 let text: SharedString = text.into();
1327 self.with_edits_allowed(|this| {
1328 this.undo_manager.set_pending_intent(EditIntent::Atomic);
1329 let range = 0..this.text.chars().map(|c| c.len_utf16()).sum();
1330 this.replace_text_in_range_silent(Some(range), &text, window, cx);
1331 this.reset_highlighter(cx);
1332 });
1333 }
1334
1335 fn reset_selection(&mut self) {
1336 self.selections.remove_all_but_active();
1337
1338 if self.is_single_line() {
1342 let end = self.text.len();
1343 self.set_cursor_to(end);
1344 } else {
1345 self.active_selection_mut().clear();
1346 }
1347 }
1348
1349 fn reset_lsp_state(&mut self) {
1350 if self.is_code_editor() {
1351 self._pending_update = true;
1352 M::reset_language_features(self);
1353 }
1354 }
1355
1356 fn reset_scroll_to_start(&mut self) {
1357 self.scroll_handle.set_offset(point(px(0.), px(0.)));
1361 if self.is_single_line() {
1362 self.deferred_scroll_offset = Some(point(px(0.), px(0.)));
1363 }
1364 }
1365
1366 #[allow(unused)]
1370 pub(crate) fn disabled(mut self, disabled: bool) -> Self {
1371 self.disabled = disabled;
1372 self
1373 }
1374
1375 pub fn set_disabled(&mut self, disabled: bool, cx: &mut Context<Self>) {
1376 if self.disabled == disabled {
1377 return;
1378 }
1379
1380 self.disabled = disabled;
1381 if disabled {
1382 M::hide_context_menu(self, cx);
1383 M::clear_inline_completion(self, cx);
1384 }
1385 cx.notify();
1386 }
1387
1388 #[allow(unused)]
1396 pub(crate) fn readonly(mut self, readonly: bool) -> Self {
1397 self.readonly = readonly;
1398 self
1399 }
1400
1401 pub fn set_readonly(&mut self, readonly: bool, cx: &mut Context<Self>) {
1402 if self.readonly == readonly {
1403 return;
1404 }
1405
1406 self.readonly = readonly;
1407 if readonly {
1408 M::hide_context_menu(self, cx);
1409 M::clear_inline_completion(self, cx);
1410 self.search_session.replace_mode = false;
1411 }
1412 cx.notify();
1413 }
1414
1415 pub fn is_editable(&self) -> bool {
1420 !self.disabled && !self.readonly
1421 }
1422
1423 pub fn clean_on_escape(mut self) -> Self {
1427 self.clean_on_escape = true;
1428 self
1429 }
1430
1431 pub fn set_clean_on_escape(&mut self, clean: bool) {
1432 self.clean_on_escape = clean;
1433 }
1434
1435 #[doc(hidden)]
1440 pub fn submit_on_enter(mut self, submit: bool) -> Self {
1441 self.submit_on_enter = submit;
1442 self
1443 }
1444
1445 pub fn set_submit_on_enter(&mut self, submit: bool, cx: &mut Context<Self>) {
1446 self.submit_on_enter = submit;
1447 cx.notify();
1448 }
1449
1450 pub fn input_action(mut self, action: TextInputAction) -> Self {
1457 self.input_action = Some(action);
1458 self
1459 }
1460
1461 pub fn autocorrect(mut self, autocorrect: bool) -> Self {
1467 self.autocorrect = Some(autocorrect);
1468 self
1469 }
1470
1471 pub fn autocapitalize(mut self, autocapitalize: Autocapitalize) -> Self {
1476 self.autocapitalize = Some(autocapitalize);
1477 self
1478 }
1479
1480 pub fn text_input_configuration(&self) -> TextInputConfiguration {
1484 let autocorrect = self.autocorrect.unwrap_or(false);
1488 let input_action =
1489 self.input_action
1490 .unwrap_or(if self.is_multi_line() && !self.submit_on_enter {
1491 TextInputAction::Enter
1492 } else {
1493 TextInputAction::Done
1494 });
1495 TextInputConfiguration {
1496 autocorrect,
1497 autocapitalize: self.autocapitalize.unwrap_or(Autocapitalize::None),
1498 suggestions: autocorrect,
1499 input_action,
1500 }
1501 }
1502
1503 #[doc(hidden)]
1505 pub fn show_whitespaces(mut self, show: bool) -> Self {
1506 self.show_whitespaces = show;
1507 self
1508 }
1509
1510 pub fn set_show_whitespaces(&mut self, show: bool, _: &mut Window, cx: &mut Context<Self>) {
1512 self.show_whitespaces = show;
1513 cx.notify();
1514 }
1515
1516 pub fn scroll_beyond_last_line(mut self, rows: Option<usize>) -> Self {
1526 self.scroll_beyond_last_line = rows;
1527 self
1528 }
1529
1530 pub fn set_scroll_beyond_last_line(
1532 &mut self,
1533 rows: Option<usize>,
1534 _: &mut Window,
1535 cx: &mut Context<Self>,
1536 ) {
1537 if self.scroll_beyond_last_line == rows {
1538 return;
1539 }
1540 self.scroll_beyond_last_line = rows;
1541 cx.notify();
1542 }
1543
1544 pub fn cursor_surrounding_lines(mut self, lines: Option<usize>) -> Self {
1558 self.cursor_surrounding_lines = lines;
1559 self
1560 }
1561
1562 pub fn set_cursor_surrounding_lines(
1564 &mut self,
1565 lines: Option<usize>,
1566 _: &mut Window,
1567 cx: &mut Context<Self>,
1568 ) {
1569 if self.cursor_surrounding_lines == lines {
1570 return;
1571 }
1572 self.cursor_surrounding_lines = lines;
1573 cx.notify();
1574 }
1575
1576 pub fn default_value(mut self, value: impl Into<SharedString>) -> Self {
1578 let text: SharedString = value.into();
1579 self.text = Rope::from(self.normalize_input(&text).as_ref());
1580 if let Some(diagnostics) = self.mode.diagnostics_mut() {
1581 diagnostics.reset(&self.text)
1582 }
1583 self._pending_update = true;
1586 self
1587 }
1588
1589 pub fn value(&self) -> SharedString {
1594 SharedString::new(self.text.to_string())
1595 }
1596
1597 pub fn selected_value(&self) -> SharedString {
1603 SharedString::new(self.selected_text().to_string())
1604 }
1605
1606 pub fn unmask_value(&self) -> SharedString {
1608 self.mask_pattern.unmask(&self.text.to_string()).into()
1609 }
1610
1611 #[doc(hidden)]
1617 pub fn prepare(&mut self, _: &mut Window, _: &mut Context<Self>) {}
1618
1619 pub fn text(&self) -> &Rope {
1624 &self.text
1625 }
1626
1627 pub fn cursor_position(&self) -> Position {
1629 let offset = self.cursor();
1630 self.text.offset_to_position(offset)
1631 }
1632
1633 pub fn set_cursor_position(
1637 &mut self,
1638 position: impl Into<Position>,
1639 window: &mut Window,
1640 cx: &mut Context<Self>,
1641 ) {
1642 let position: Position = position.into();
1643 let offset = self.text.position_to_offset(&position);
1644
1645 self.move_to(offset, None, cx);
1646 self.update_preferred_column();
1647 self.focus(window, cx);
1648 }
1649
1650 pub fn focus(&self, window: &mut Window, cx: &mut Context<Self>) {
1652 self.focus_handle.focus(window, cx);
1653 self.blink_cursor.update(cx, |cursor, cx| {
1654 cursor.start(cx);
1655 });
1656 }
1657
1658 pub fn refresh(&mut self, cx: &mut Context<Self>) {
1672 self._pending_update = true;
1673 cx.notify();
1674 }
1675
1676 pub(super) fn select_left(&mut self, _: &SelectLeft, _: &mut Window, cx: &mut Context<Self>) {
1677 self.undo_manager.break_transaction_coalescing();
1678 self.select_all_cursors_to(|s, sel| s.previous_boundary(sel.cursor_offset()), cx);
1679 }
1680
1681 pub(super) fn select_right(&mut self, _: &SelectRight, _: &mut Window, cx: &mut Context<Self>) {
1682 self.select_all_cursors_to(|s, sel| s.next_boundary(sel.cursor_offset()), cx);
1683 }
1684
1685 pub(super) fn select_up(&mut self, _: &SelectUp, _: &mut Window, cx: &mut Context<Self>) {
1686 if self.is_single_line() {
1687 return;
1688 }
1689 self.select_all_cursors_to_with_affinity(
1690 |s, sel| {
1691 s.vertical_selection_target(
1692 sel.cursor_offset(),
1693 sel.column_anchor,
1694 s.line_end_affinity_for(sel),
1695 -1,
1696 )
1697 },
1698 true,
1699 cx,
1700 );
1701 }
1702
1703 pub(super) fn select_down(&mut self, _: &SelectDown, _: &mut Window, cx: &mut Context<Self>) {
1704 if self.is_single_line() {
1705 return;
1706 }
1707 self.select_all_cursors_to_with_affinity(
1708 |s, sel| {
1709 s.vertical_selection_target(
1710 sel.cursor_offset(),
1711 sel.column_anchor,
1712 s.line_end_affinity_for(sel),
1713 1,
1714 )
1715 },
1716 true,
1717 cx,
1718 );
1719 }
1720
1721 pub(super) fn on_action_select_all(
1722 &mut self,
1723 _: &SelectAll,
1724 window: &mut Window,
1725 cx: &mut Context<Self>,
1726 ) {
1727 self.select_all(window, cx);
1728 }
1729
1730 pub(super) fn select_to_start(
1731 &mut self,
1732 _: &SelectToStart,
1733 _: &mut Window,
1734 cx: &mut Context<Self>,
1735 ) {
1736 self.undo_manager.break_transaction_coalescing();
1737 self.select_all_cursors_to(|_, _| 0, cx);
1738 }
1739
1740 pub(super) fn select_to_end(
1741 &mut self,
1742 _: &SelectToEnd,
1743 _: &mut Window,
1744 cx: &mut Context<Self>,
1745 ) {
1746 self.undo_manager.break_transaction_coalescing();
1747 let end = self.text.len();
1748 self.select_all_cursors_to(move |_, _| end, cx);
1749 }
1750
1751 pub(super) fn select_to_start_of_line(
1752 &mut self,
1753 _: &SelectToStartOfLine,
1754 _: &mut Window,
1755 cx: &mut Context<Self>,
1756 ) {
1757 self.undo_manager.break_transaction_coalescing();
1758 self.select_all_cursors_to(
1759 |s, sel| s.start_of_line_at(sel.cursor_offset(), s.line_end_affinity_for(sel)),
1760 cx,
1761 );
1762 }
1763
1764 pub(super) fn select_to_end_of_line(
1765 &mut self,
1766 _: &SelectToEndOfLine,
1767 _: &mut Window,
1768 cx: &mut Context<Self>,
1769 ) {
1770 self.undo_manager.break_transaction_coalescing();
1771 self.select_all_cursors_to_with_affinity(
1772 |s, sel| {
1773 (
1774 s.end_of_line_at(sel.cursor_offset(), s.line_end_affinity_for(sel)),
1775 true,
1776 )
1777 },
1778 false,
1779 cx,
1780 );
1781 }
1782
1783 pub(super) fn select_to_previous_word(
1784 &mut self,
1785 _: &SelectToPreviousWordStart,
1786 _: &mut Window,
1787 cx: &mut Context<Self>,
1788 ) {
1789 self.undo_manager.break_transaction_coalescing();
1790 self.select_all_cursors_to(
1791 |s, sel| s.previous_start_of_word_at(sel.cursor_offset()),
1792 cx,
1793 );
1794 }
1795
1796 pub(super) fn select_to_next_word(
1797 &mut self,
1798 _: &SelectToNextWordEnd,
1799 _: &mut Window,
1800 cx: &mut Context<Self>,
1801 ) {
1802 self.undo_manager.break_transaction_coalescing();
1803 self.select_all_cursors_to(|s, sel| s.next_end_of_word_at(sel.cursor_offset()), cx);
1804 }
1805
1806 pub(super) fn previous_start_of_word_at(&self, offset: usize) -> usize {
1809 if self.masked {
1810 return 0;
1814 }
1815
1816 let offset = self.offset_from_utf16(self.offset_to_utf16(offset));
1817 let left_part = self.text.slice(0..offset).to_string();
1819
1820 let target = UnicodeSegmentation::split_word_bound_indices(left_part.as_str())
1821 .rfind(|(_, s)| !s.trim_start().is_empty())
1822 .map(|(i, _)| i)
1823 .unwrap_or(0);
1824 self.token_boundary(target, Bias::Left)
1825 }
1826
1827 pub(super) fn next_end_of_word_at(&self, offset: usize) -> usize {
1829 if self.masked {
1830 return self.text.len();
1832 }
1833
1834 let offset = self.offset_from_utf16(self.offset_to_utf16(offset));
1835 let right_part = self.text.slice(offset..self.text.len()).to_string();
1836
1837 let target = UnicodeSegmentation::split_word_bound_indices(right_part.as_str())
1838 .find(|(_, s)| !s.trim_start().is_empty())
1839 .map(|(i, s)| offset + i + s.len())
1840 .unwrap_or(self.text.len());
1841 self.token_boundary(target, Bias::Right)
1842 }
1843
1844 pub(super) fn start_of_line_at(&self, offset: usize, line_end_affinity: bool) -> usize {
1849 if self.is_single_line() {
1850 return 0;
1851 }
1852
1853 let row = self.text.offset_to_point(offset).row;
1854 let logical_start = self.text.line_start_offset(row);
1855
1856 if self.soft_wrap && self.is_code_editor() {
1857 let wrap_point = self
1858 .display_map
1859 .offset_to_wrap_display_point_with_affinity(offset, line_end_affinity);
1860 if let Some(line) = self.display_map.line(row)
1861 && let Some(range) = line.wrapped_lines.get(wrap_point.local_row)
1862 {
1863 let visual_start = logical_start + range.start;
1864 if offset != visual_start {
1865 return visual_start;
1866 }
1867 }
1868 }
1869
1870 logical_start
1871 }
1872
1873 pub(super) fn end_of_line_at(&self, offset: usize, line_end_affinity: bool) -> usize {
1878 if self.is_single_line() {
1879 return self.text.len();
1880 }
1881
1882 let row = self.text.offset_to_point(offset).row;
1883 let logical_start = self.text.line_start_offset(row);
1884 let logical_end = self.text.line_end_offset(row);
1885
1886 if self.soft_wrap && self.is_code_editor() {
1887 let wrap_point = self
1891 .display_map
1892 .offset_to_wrap_display_point_with_affinity(offset, line_end_affinity);
1893 if let Some(line) = self.display_map.line(row)
1894 && let Some(range) = line.wrapped_lines.get(wrap_point.local_row)
1895 {
1896 let visual_end = logical_start + range.end;
1897 if offset != visual_end {
1898 return visual_end;
1899 }
1900 }
1901 }
1902
1903 logical_end
1904 }
1905
1906 pub(super) fn indent_of_next_line(&mut self) -> String {
1910 self.indent_of_next_line_at(self.cursor())
1911 }
1912
1913 pub(super) fn indent_of_next_line_at(&mut self, offset: usize) -> String {
1915 if self.is_single_line() {
1916 return "".into();
1917 }
1918
1919 let mut current_indent = String::new();
1920 let mut next_indent = String::new();
1921 let line_end_affinity = self.line_end_affinity_at(offset);
1922 let current_line_start_pos = self.start_of_line_at(offset, line_end_affinity);
1923 let next_line_start_pos = self.end_of_line_at(offset, line_end_affinity);
1924 for c in self.text.slice(current_line_start_pos..).chars() {
1925 if !c.is_whitespace() {
1926 break;
1927 }
1928 if c == '\n' || c == '\r' {
1929 break;
1930 }
1931 current_indent.push(c);
1932 }
1933
1934 for c in self.text.slice(next_line_start_pos..).chars() {
1935 if !c.is_whitespace() {
1936 break;
1937 }
1938 if c == '\n' || c == '\r' {
1939 break;
1940 }
1941 next_indent.push(c);
1942 }
1943
1944 if next_indent.len() > current_indent.len() {
1945 return next_indent;
1946 }
1947
1948 if let Some(rules) = self.mode.language_config() {
1951 if self.mode.is_smart_indent()
1952 && M::editing_syntax_context(self, offset) == crate::input::SyntaxContext::Code
1953 {
1954 let line_before: String = self
1955 .text
1956 .slice(current_line_start_pos..offset)
1957 .chars()
1958 .collect();
1959 if rules.opens_indent(line_before.trim_end()) {
1960 let tab = self.mode.tab_size();
1961 if tab.hard_tabs {
1962 current_indent.push('\t');
1963 } else {
1964 for _ in 0..tab.tab_size {
1965 current_indent.push(' ');
1966 }
1967 }
1968 } else {
1969 let line_after = self.text.slice(offset..next_line_start_pos).to_string();
1970 if rules.closes_indent(&line_after) {
1971 let tab = self.mode.tab_size();
1972 if current_indent.ends_with('\t') {
1973 current_indent.pop();
1974 } else {
1975 for _ in 0..tab.tab_size {
1976 if !current_indent.ends_with(' ') {
1977 break;
1978 }
1979 current_indent.pop();
1980 }
1981 }
1982 }
1983 }
1984 }
1985 }
1986
1987 current_indent
1988 }
1989
1990 fn delete_selections(
1998 &mut self,
1999 silent: bool,
2000 collapsed_intent: EditIntent,
2001 mut collapsed_target: impl FnMut(&mut Self, usize) -> Range<usize>,
2002 window: &mut Window,
2003 cx: &mut Context<Self>,
2004 ) {
2005 if !self.is_editable() {
2006 return;
2007 }
2008 let cursors: Vec<CursorSelection> = self.selections.iter().copied().collect();
2009 let intent = if cursors.iter().all(|sel| sel.is_empty()) {
2010 collapsed_intent
2011 } else {
2012 EditIntent::Atomic
2013 };
2014 let mut new_selections: Vec<CursorSelection> = Vec::with_capacity(cursors.len());
2015 for sel in &cursors {
2016 let range = if sel.is_empty() {
2017 collapsed_target(self, sel.cursor_offset())
2018 } else {
2019 sel.start..sel.end
2020 };
2021 let (start, end) = (range.start.min(range.end), range.start.max(range.end));
2022 let mut selection = *sel;
2023 selection.start = start;
2024 selection.end = end;
2025 new_selections.push(selection);
2026 }
2027 self.undo_manager.begin_transaction_with(intent);
2030 self.undo_manager
2031 .record_selections(cursors.clone(), cursors.clone());
2032 self.selections.replace_all(new_selections);
2033 self.undo_manager.set_pending_intent(intent);
2034
2035 let was_silent = self.silent_replace_text;
2036 self.silent_replace_text = silent;
2037 self.replace_text_in_range(None, "", window, cx);
2038 self.silent_replace_text = was_silent;
2039 self.undo_manager
2040 .record_selections(cursors, self.selections.iter().copied().collect());
2041 self.undo_manager.commit_transaction();
2042 self.pause_blink_cursor(cx);
2043 }
2044
2045 fn auto_close_target(&self, range: &Range<usize>, text: &str) -> Option<(usize, SharedString)> {
2048 if self.silent_replace_text
2049 || self.ime_marked_range.is_some()
2050 || !self.selections.is_single()
2051 || !self.active_selection().is_empty()
2052 || !range.is_empty()
2053 || range.start != self.cursor()
2054 || text.chars().count() != 1
2055 || !self.mode.is_auto_close()
2056 {
2057 return None;
2058 }
2059 let rules = self.mode.language_config()?;
2060 if self
2061 .text
2062 .chars_at(range.start)
2063 .next()
2064 .is_some_and(|c| !c.is_whitespace() && !rules.auto_close_before.contains(c))
2065 {
2066 return None;
2067 }
2068 for (open, close, not_in) in rules.closing_pairs() {
2069 let Some(prefix) = open.strip_suffix(text) else {
2070 continue;
2071 };
2072 if !self.text_before_matches(range.start, prefix) {
2073 continue;
2074 }
2075 let start = range.start - prefix.len();
2076 if open == close
2077 && (self.is_escaped_at(start)
2078 || self
2079 .text
2080 .chars_at(start)
2081 .reversed()
2082 .next()
2083 .is_some_and(|c| c.is_alphanumeric() || c == '_'))
2084 {
2085 continue;
2086 }
2087 if not_in.contains(&M::editing_syntax_context(self, start)) {
2088 continue;
2089 }
2090 return Some((open.len(), close.into()));
2091 }
2092 None
2093 }
2094
2095 fn text_before_matches(&self, offset: usize, text: &str) -> bool {
2096 offset >= text.len()
2097 && self.text.is_char_boundary(offset - text.len())
2098 && self.text.slice(offset - text.len()..offset) == text
2099 }
2100
2101 fn text_after_matches(&self, offset: usize, text: &str) -> bool {
2102 offset + text.len() <= self.text.len()
2103 && self.text.is_char_boundary(offset + text.len())
2104 && self.text.slice(offset..offset + text.len()) == text
2105 }
2106
2107 fn is_escaped_at(&self, offset: usize) -> bool {
2108 self.text
2109 .chars_at(offset)
2110 .reversed()
2111 .take_while(|c| *c == '\\')
2112 .count()
2113 % 2
2114 == 1
2115 }
2116
2117 fn skip_over_target(&self, new_text: &str) -> Option<usize> {
2124 if !self.mode.is_auto_close() || new_text.chars().count() != 1 {
2125 return None;
2126 }
2127 let rules = self.mode.language_config()?;
2128 let cursor = self.cursor();
2129 for (open, close, not_in) in rules.closing_pairs() {
2130 for (index, _) in close.char_indices() {
2131 if !close[index..].starts_with(new_text)
2132 || !self.text_before_matches(cursor, &close[..index])
2133 || !self.text_after_matches(cursor, &close[index..])
2134 {
2135 continue;
2136 }
2137 let context = M::editing_syntax_context(self, cursor);
2138 let generated =
2139 self.mode
2140 .auto_closed_pairs()
2141 .contains_closer(cursor, index, close.len());
2142 if !generated
2143 && not_in.contains(&context)
2144 && !(context == crate::input::SyntaxContext::String && open == close)
2145 {
2146 continue;
2147 }
2148 if self.is_escaped_at(cursor - index) {
2149 continue;
2150 }
2151 return Some(cursor + new_text.len());
2152 }
2153 }
2154 None
2155 }
2156
2157 pub(super) fn backspace(&mut self, _: &Backspace, window: &mut Window, cx: &mut Context<Self>) {
2158 if self.selections.is_single() && self.active_selection().is_empty() && self.cursor() == 0 {
2164 cx.propagate();
2165 return;
2166 }
2167
2168 if self.mode.is_auto_close()
2171 && self.selections.is_single()
2172 && self.active_selection().is_empty()
2173 {
2174 let off = self.cursor();
2175 let deletion = self.mode.language_config().and_then(|rules| {
2176 rules.closing_pairs().find_map(|(open, close, not_in)| {
2177 if !self.text_before_matches(off, open) || !self.text_after_matches(off, close)
2178 {
2179 return None;
2180 }
2181 let start = off - open.len();
2182 let generated = self
2183 .mode
2184 .auto_closed_pairs()
2185 .contains(start..off, off..off + close.len());
2186 if self.is_escaped_at(start)
2187 || (!generated && not_in.contains(&M::editing_syntax_context(self, start)))
2188 {
2189 return None;
2190 }
2191 Some(start..off + close.len())
2192 })
2193 });
2194 if let Some(range) = deletion {
2195 let utf16 = self.range_to_utf16(&range);
2196 self.replace_text_in_range_silent(Some(utf16), "", window, cx);
2197 return;
2198 }
2199 }
2200
2201 self.delete_selections(
2202 false,
2203 EditIntent::Backspace,
2204 |s, offset| s.previous_boundary(offset)..offset,
2205 window,
2206 cx,
2207 );
2208 }
2209
2210 pub(super) fn delete(&mut self, _: &Delete, window: &mut Window, cx: &mut Context<Self>) {
2211 self.delete_selections(
2212 false,
2213 EditIntent::DeleteForward,
2214 |s, offset| offset..s.next_boundary(offset),
2215 window,
2216 cx,
2217 );
2218 }
2219
2220 pub(super) fn delete_to_beginning_of_line(
2221 &mut self,
2222 _: &DeleteToBeginningOfLine,
2223 window: &mut Window,
2224 cx: &mut Context<Self>,
2225 ) {
2226 self.delete_selections(
2227 true,
2228 EditIntent::Atomic,
2229 |s, offset| {
2230 let mut start = s.start_of_line_at(offset, s.line_end_affinity_at(offset));
2231 if start == offset {
2232 start = start.saturating_sub(1);
2233 }
2234 start..offset
2235 },
2236 window,
2237 cx,
2238 );
2239 }
2240
2241 pub(super) fn delete_to_end_of_line(
2242 &mut self,
2243 _: &DeleteToEndOfLine,
2244 window: &mut Window,
2245 cx: &mut Context<Self>,
2246 ) {
2247 self.delete_selections(
2248 true,
2249 EditIntent::Atomic,
2250 |s, offset| {
2251 let mut end = s.end_of_line_at(offset, s.line_end_affinity_at(offset));
2252 if end == offset {
2253 end = (end + 1).clamp(0, s.text.len());
2254 }
2255 offset..end
2256 },
2257 window,
2258 cx,
2259 );
2260 }
2261
2262 pub(super) fn delete_previous_word(
2263 &mut self,
2264 _: &DeleteToPreviousWordStart,
2265 window: &mut Window,
2266 cx: &mut Context<Self>,
2267 ) {
2268 self.delete_selections(
2269 true,
2270 EditIntent::Atomic,
2271 |s, offset| s.previous_start_of_word_at(offset)..offset,
2272 window,
2273 cx,
2274 );
2275 }
2276
2277 pub(super) fn delete_next_word(
2278 &mut self,
2279 _: &DeleteToNextWordEnd,
2280 window: &mut Window,
2281 cx: &mut Context<Self>,
2282 ) {
2283 self.delete_selections(
2284 true,
2285 EditIntent::Atomic,
2286 |s, offset| offset..s.next_end_of_word_at(offset),
2287 window,
2288 cx,
2289 );
2290 }
2291
2292 pub(super) fn enter(&mut self, action: &Enter, window: &mut Window, cx: &mut Context<Self>) {
2293 if M::handle_context_menu_action(self, Box::new(action.clone()), window, cx) {
2294 return;
2295 }
2296
2297 if M::has_inline_completion(self) {
2299 M::clear_inline_completion(self, cx);
2300 }
2301
2302 let insert_newline = self.is_multi_line() && (!self.submit_on_enter || action.shift);
2307
2308 if insert_newline {
2309 if !self.selections.is_single() {
2310 self.selections.merge_overlapping();
2312 let selections: Vec<CursorSelection> = self.selections.iter().copied().collect();
2313 let mut edits: Vec<(Range<usize>, String)> = Vec::with_capacity(selections.len());
2314 for sel in &selections {
2315 let indent = if self.is_code_editor() {
2316 self.indent_of_next_line_at(sel.cursor_offset())
2317 } else {
2318 String::new()
2319 };
2320 edits.push((sel.start..sel.end, format!("\n{}", indent)));
2321 }
2322 edits.sort_by_key(|(range, _)| range.start);
2323 self.replace_text_in_ranges(&edits, window, cx);
2324 self.pause_blink_cursor(cx);
2325 } else {
2326 let mut split = false;
2332 if self.is_code_editor() && self.active_selection().is_empty() {
2333 if let Some(rules) = self.mode.language_config() {
2334 if self.mode.is_smart_indent()
2335 && M::editing_syntax_context(self, self.cursor())
2336 == crate::input::SyntaxContext::Code
2337 {
2338 let off = self.cursor();
2339 let is_pair = rules.brackets.iter().any(|p| {
2340 !p.open.is_empty()
2341 && !p.close.is_empty()
2342 && p.open != p.close
2343 && self.text_before_matches(off, p.open.as_ref())
2344 && self.text_after_matches(off, p.close.as_ref())
2345 });
2346 if is_pair {
2347 let line_end_affinity = self.line_end_affinity_at(off);
2349 let line_start = self.start_of_line_at(off, line_end_affinity);
2350 let mut indent = String::new();
2351 for c in self.text.slice(line_start..).chars() {
2352 if !c.is_whitespace() || c == '\n' || c == '\r' {
2353 break;
2354 }
2355 indent.push(c);
2356 }
2357 let mut inner = indent.clone();
2359 if self.mode.is_smart_indent() {
2360 let tab = self.mode.tab_size();
2361 if tab.hard_tabs {
2362 inner.push('\t');
2363 } else {
2364 for _ in 0..tab.tab_size {
2365 inner.push(' ');
2366 }
2367 }
2368 }
2369 let new_line_text = format!("\n{inner}\n{indent}");
2370 self.replace_text_in_range_silent(None, &new_line_text, window, cx);
2371 self.set_cursor_to(off + 1 + inner.len());
2372 self.update_preferred_column();
2373 let cursors = self.selections.iter().copied().collect::<Vec<_>>();
2374 self.undo_manager
2375 .record_selections(cursors.clone(), cursors);
2376 self.pause_blink_cursor(cx);
2377 split = true;
2378 }
2379 }
2380 }
2381 }
2382 if !split {
2383 let indent = if self.is_code_editor() {
2385 self.indent_of_next_line()
2386 } else {
2387 "".to_string()
2388 };
2389
2390 let new_line_text = format!("\n{}", indent);
2392 self.replace_text_in_range_silent(None, &new_line_text, window, cx);
2393 self.pause_blink_cursor(cx);
2394 }
2395 }
2396 } else {
2397 self.undo_manager.break_transaction_coalescing();
2400 cx.propagate();
2401 }
2402
2403 cx.emit(InputEvent::PressEnter {
2404 secondary: action.secondary,
2405 shift: action.shift,
2406 });
2407 }
2408
2409 pub fn clean(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2410 self.replace_text("", window, cx);
2411 self.set_selection(0, 0);
2412 self.scroll_to(0, None, cx);
2413 }
2414
2415 pub(super) fn escape(&mut self, action: &Escape, window: &mut Window, cx: &mut Context<Self>) {
2416 if M::handle_context_menu_action(self, Box::new(action.clone()), window, cx) {
2417 return;
2418 }
2419
2420 M::hide_context_menu(self, cx);
2422
2423 if !self.selections.is_single() {
2425 self.undo_manager.break_transaction_coalescing();
2426 self.selections.remove_all_but_active();
2427 cx.notify();
2428 return;
2429 }
2430
2431 if M::has_inline_completion(self) {
2433 M::clear_inline_completion(self, cx);
2434 return; }
2436 M::clear_inline_completion(self, cx);
2437
2438 if self.touch_selection().is_some() {
2440 self.dismiss_touch_selection(cx);
2441 return;
2442 }
2443
2444 if self.ime_marked_range.is_some() {
2445 self.unmark_text(window, cx);
2446 }
2447
2448 if self.clean_on_escape && !cfg!(target_os = "android") {
2451 return self.clean(window, cx);
2452 }
2453
2454 cx.propagate();
2455 }
2456
2457 pub(crate) fn handle_right_click_menu(
2459 &mut self,
2460 position: Point<Pixels>,
2461 offset: usize,
2462 window: &mut Window,
2463 cx: &mut Context<Self>,
2464 ) {
2465 if self.disabled {
2466 return;
2467 }
2468 if crate::GlobalState::is_in_deferred_context(cx) {
2469 return;
2470 }
2471
2472 if !self.active_selection().contains(offset) {
2473 self.move_to(offset, None, cx);
2474 }
2475
2476 if self.is_code_editor() {
2477 M::on_hover_definition(self, offset, window, cx);
2478 }
2479
2480 if let Some(handler) = self.context_menu_handler.clone() {
2481 let capabilities = self.context_menu_capabilities();
2482 let input = cx.weak_entity();
2483 window.defer(cx, move |window, cx| {
2486 if input.upgrade().is_none() {
2487 return;
2488 }
2489 let previous_focus = window.focused(cx);
2490 handler(NativeMenu::new(), capabilities, position, window, cx);
2491 input
2494 .update(cx, |this, cx| {
2495 if window.focused(cx) == previous_focus
2496 && !this.focus_handle.is_focused(window)
2497 && this.has_selection_focus(window, cx)
2498 {
2499 return;
2501 }
2502 let menu_focus = window.focused(cx).filter(|focus| {
2503 Some(focus) != previous_focus.as_ref() && *focus != this.focus_handle
2504 });
2505 this.set_selection_focus(menu_focus, cx);
2506 })
2507 .ok();
2508 });
2509 }
2510 }
2511
2512 pub(super) fn add_cursor_above(
2513 &mut self,
2514 _: &AddCursorAbove,
2515 _: &mut Window,
2516 cx: &mut Context<Self>,
2517 ) {
2518 self.add_cursor_vertical(-1, cx);
2519 }
2520
2521 pub(super) fn add_cursor_below(
2522 &mut self,
2523 _: &AddCursorBelow,
2524 _: &mut Window,
2525 cx: &mut Context<Self>,
2526 ) {
2527 self.add_cursor_vertical(1, cx);
2528 }
2529
2530 fn add_cursor_vertical(&mut self, move_lines: isize, cx: &mut Context<Self>) {
2535 if !self.is_multi_line() {
2536 return;
2537 }
2538
2539 self.pause_blink_cursor(cx);
2540 self.undo_manager.break_transaction_coalescing();
2542
2543 let sources: Vec<(usize, Option<(Pixels, usize)>, bool)> = self
2544 .selections
2545 .iter()
2546 .map(|sel| {
2547 (
2548 sel.cursor_offset(),
2549 sel.column_anchor,
2550 self.line_end_affinity_for(sel),
2551 )
2552 })
2553 .collect();
2554 let mut offsets: std::collections::HashSet<usize> =
2555 sources.iter().map(|(offset, _, _)| *offset).collect();
2556
2557 let mut newest: Option<usize> = None;
2558 for (offset, anchor, line_end_affinity) in sources {
2559 let anchor = anchor.or_else(|| self.preferred_column_for(offset));
2560 let (target, _) = self.vertical_target(offset, anchor, line_end_affinity, move_lines);
2561 if target == offset || offsets.contains(&target) {
2562 continue;
2563 }
2564 offsets.insert(target);
2565 let id = self.selections.generate_id();
2566 let mut cursor = CursorSelection::new(id, target, target);
2567 cursor.column_anchor = anchor;
2568 self.selections.add(cursor);
2569 newest = Some(target);
2570 }
2571
2572 if let Some(newest) = newest {
2573 self.scroll_to(newest, None, cx);
2574 }
2575 cx.notify();
2576 }
2577
2578 pub(super) fn add_cursor_at(&mut self, offset: usize, cx: &mut Context<Self>) {
2583 if !self.is_multi_line() {
2584 return;
2585 }
2586
2587 for sel in self.selections.iter() {
2588 if sel.contains(offset) {
2589 return;
2590 }
2591 if sel.is_collapsed() && sel.cursor_offset() == offset {
2592 return;
2593 }
2594 }
2595
2596 self.undo_manager.break_transaction_coalescing();
2597 let id = self.selections.generate_id();
2598 let mut selection = CursorSelection::new(id, offset, offset);
2599 selection.column_anchor = self.preferred_column_for(offset);
2600 self.selections.add(selection);
2601 cx.notify();
2602 }
2603
2604 pub(super) fn build_columnar_selection(
2612 &mut self,
2613 start: ColumnarPoint,
2614 end: ColumnarPoint,
2615 cx: &mut Context<Self>,
2616 ) {
2617 if !self.is_multi_line() {
2618 return;
2619 }
2620
2621 self.undo_manager.break_transaction_coalescing();
2622
2623 let (start_row, start_col) = self.columnar_row_column(start);
2624 let (end_row, end_col) = self.columnar_row_column(end);
2625 let (start_row, end_row) = (start_row.min(end_row), start_row.max(end_row));
2626 let (start_col, end_col) = (start_col.min(end_col), start_col.max(end_col));
2627
2628 let mut new_selections = Vec::with_capacity(end_row - start_row + 1);
2629 for row in start_row..=end_row {
2630 let sel_start = self
2631 .display_map
2632 .display_row_column_to_offset(row, start_col);
2633 let sel_end = self.display_map.display_row_column_to_offset(row, end_col);
2634 let id = self.selections.generate_id();
2635 let sel_start = self.text.clip_offset(sel_start, Bias::Left);
2636 let sel_end = self.text.clip_offset(sel_end, Bias::Left);
2637 new_selections.push(CursorSelection::new(id, sel_start, sel_end));
2638 }
2639
2640 self.selections.replace_all(new_selections);
2641 cx.notify();
2642 }
2643
2644 fn columnar_row_column(&self, point: ColumnarPoint) -> (usize, usize) {
2647 let display_point = self.display_map.offset_to_wrap_display_point(point.offset);
2648 let row = self
2649 .display_map
2650 .wrap_row_to_display_row(display_point.row)
2651 .unwrap_or_else(|| {
2652 self.display_map
2653 .nearest_visible_display_row(display_point.row)
2654 });
2655
2656 (row, display_point.column + point.columns_past_line_end)
2657 }
2658
2659 pub(super) fn on_mouse_down(
2660 &mut self,
2661 event: &MouseDownEvent,
2662 window: &mut Window,
2663 cx: &mut Context<Self>,
2664 ) {
2665 if window.default_prevented() {
2668 return;
2669 }
2670 if event.button == MouseButton::Left
2673 && crate::GlobalState::is_touch_press(cx)
2674 && !self.disabled
2675 && !self.readonly
2676 {
2677 window.request_virtual_keyboard();
2678 }
2679 self.undo_manager.break_transaction_coalescing();
2680 if !self.disabled {
2684 self.focus(window, cx);
2685 self.pause_blink_cursor(cx);
2686 }
2687 crate::global_state::GlobalState::suppress_text_selection(cx);
2690
2691 M::clear_inline_completion(self, cx);
2693 if event.button == MouseButton::Left
2697 && event.click_count == 1
2698 && self.reopen_edit_menu_at(event.position, cx)
2699 {
2700 return;
2701 }
2702 self.dismiss_touch_selection(cx);
2703
2704 if let Some(ime_marked_range) = &self.ime_marked_range {
2707 if ime_marked_range.len() == 0 {
2708 self.ime_marked_range = None;
2709 }
2710 }
2711
2712 self.selecting = true;
2713 let (offset, line_end_affinity, columns_past_line_end) =
2714 self.resolve_mouse_position(event.position);
2715
2716 if M::on_click(self, event, offset, window, cx) {
2717 return;
2718 }
2719
2720 if event.button == MouseButton::Left && event.click_count >= 3 {
2722 self.select_line(offset, window, cx);
2723 return;
2724 }
2725
2726 if event.button == MouseButton::Left && event.click_count == 2 {
2728 self.select_word(offset, window, cx);
2729 if crate::GlobalState::is_touch_press(cx) {
2732 self.keep_touch_selection(cx);
2733 }
2734 return;
2735 }
2736
2737 if event.button == MouseButton::Right {
2739 if self.enable_context_menu {
2740 if !self.active_selection().contains(offset) {
2741 self.move_to(offset, None, cx);
2742 }
2743 self.pending_context_menu = Some((event.position, offset));
2744 }
2745 return;
2746 }
2747
2748 if self.is_multi_line() && event.button == MouseButton::Left {
2750 if event.modifiers.alt
2751 && (event.modifiers.shift || (cfg!(target_os = "linux") && event.modifiers.control))
2752 {
2753 self.column_select_start = Some(ColumnarPoint::new(offset, columns_past_line_end));
2756 self.selecting = true;
2757 self.move_to_with_affinity(offset, None, line_end_affinity, cx);
2758 return;
2759 } else if event.modifiers.alt {
2760 self.add_cursor_at(offset, cx);
2761 self.column_select_start = Some(ColumnarPoint::new(offset, columns_past_line_end));
2764 return;
2765 }
2766 }
2767
2768 if event.modifiers.shift {
2769 self.select_to_with_affinity(offset, line_end_affinity, cx);
2770 } else {
2771 self.move_to_with_affinity(offset, None, line_end_affinity, cx)
2772 }
2773 }
2774
2775 pub(super) fn on_mouse_up(
2776 &mut self,
2777 event: &MouseUpEvent,
2778 window: &mut Window,
2779 cx: &mut Context<Self>,
2780 ) {
2781 if event.button == MouseButton::Right {
2782 if let Some((position, offset)) = self.pending_context_menu.take() {
2783 self.handle_right_click_menu(position, offset, window, cx);
2784 }
2785 }
2786 if self.active_selection().is_empty() {
2787 self.active_selection_mut().reversed = false;
2788 }
2789 self.selecting = false;
2790 self.selected_word_range = None;
2791 self.column_select_start = None;
2792 self.auto_scroll.stop();
2793 }
2794
2795 pub(super) fn on_mouse_move(
2796 &mut self,
2797 event: &MouseMoveEvent,
2798 window: &mut Window,
2799 cx: &mut Context<Self>,
2800 ) {
2801 let within_bounds = self
2803 .last_bounds
2804 .as_ref()
2805 .map(|bounds| bounds.contains(&event.position))
2806 .unwrap_or(false);
2807
2808 if !within_bounds {
2809 M::clear_hover_state(self, cx);
2811 return;
2812 }
2813
2814 let (offset, _) = self.index_for_mouse_position(event.position);
2816 M::on_mouse_move(self, offset, event, window, cx);
2817
2818 if self.is_code_editor() {
2819 match self
2823 .mode
2824 .diagnostics()
2825 .and_then(|set| set.for_offset(offset))
2826 {
2827 Some(diagnostic) => {
2828 if self.diagnostic_popover.as_deref() != Some(diagnostic) {
2829 self.diagnostic_popover = Some(Rc::new(diagnostic.clone()));
2830 cx.notify();
2831 }
2832 }
2833 None => {
2834 if self.diagnostic_popover.take().is_some() {
2835 cx.notify();
2836 }
2837 }
2838 }
2839 }
2840 }
2841
2842 pub(super) fn on_scroll_wheel(
2843 &mut self,
2844 event: &ScrollEvent,
2845 window: &mut Window,
2846 cx: &mut Context<Self>,
2847 ) {
2848 let line_height = self
2849 .last_layout
2850 .as_ref()
2851 .map(|layout| layout.line_height)
2852 .unwrap_or(window.line_height());
2853 let delta = event.delta.pixel_delta(line_height);
2854
2855 let old_offset = self.scroll_handle.offset();
2856 let precise = matches!(event.delta, gpui::ScrollDelta::Pixels(_));
2857 if !precise || !self.scroll_bounce || cx.reduce_motion() || !self.overflows_vertically() {
2858 let displaced = self.overscroll.offset() != 0.;
2859 self.overscroll.reset();
2860 if displaced {
2861 cx.notify();
2862 }
2863 }
2864 let bounces = self.scroll_bounce
2865 && precise
2866 && !cx.reduce_motion()
2867 && delta.y.abs() >= delta.x.abs()
2868 && self.overflows_vertically();
2869 if bounces {
2870 let mut overscroll = std::mem::take(&mut self.overscroll);
2871 let stretched = overscroll.packet(
2872 delta.y.as_f32(),
2873 event.touch_phase,
2874 self.input_bounds.size.height.as_f32(),
2875 cx.background_executor().now(),
2876 |dy| {
2877 let before = self.scroll_handle.offset();
2878 self.update_scroll_offset(
2879 Some(point(before.x + delta.x, before.y + px(dy))),
2880 cx,
2881 );
2882 (self.scroll_handle.offset().y - before.y).as_f32()
2883 },
2884 );
2885 self.overscroll = overscroll;
2886 if stretched {
2887 cx.notify();
2888 }
2889 } else {
2890 if matches!(
2892 event.touch_phase,
2893 gpui::TouchPhase::Ended | gpui::TouchPhase::Cancelled
2894 ) && self.overscroll.offset() != 0.
2895 {
2896 self.overscroll.packet(
2897 0.,
2898 event.touch_phase,
2899 self.input_bounds.size.height.as_f32(),
2900 cx.background_executor().now(),
2901 |_| 0.,
2902 );
2903 cx.notify();
2904 }
2905 self.update_scroll_offset(Some(old_offset + delta), cx);
2906 }
2907
2908 if self.scroll_handle.offset() != old_offset || self.overflows_along(delta) {
2912 cx.stop_propagation();
2913 }
2914
2915 if self.diagnostic_popover.take().is_some() {
2916 cx.notify();
2917 }
2918 self.edit_menu_on_scroll(event.touch_phase, cx);
2921 }
2922
2923 pub(super) fn takes_scroll(&self, delta: Point<Pixels>) -> bool {
2929 if delta.y.abs() >= delta.x.abs() {
2930 self.overflows_vertically()
2931 } else {
2932 self.scroll_size.width > self.input_bounds.size.width
2933 }
2934 }
2935
2936 fn overflows_along(&self, delta: Point<Pixels>) -> bool {
2940 if delta.y.abs() >= delta.x.abs() {
2941 delta.y != px(0.) && self.overflows_vertically()
2942 } else {
2943 self.scroll_size.width > self.input_bounds.size.width
2944 }
2945 }
2946
2947 fn overflows_vertically(&self) -> bool {
2949 self.is_multi_line() && self.scroll_size.height > self.input_bounds.size.height
2950 }
2951
2952 pub(super) fn update_scroll_offset(
2953 &mut self,
2954 offset: Option<Point<Pixels>>,
2955 cx: &mut Context<Self>,
2956 ) {
2957 let mut offset = offset.unwrap_or(self.scroll_handle.offset());
2958 let safe_y_range =
2959 (-self.scroll_size.height + self.input_bounds.size.height).min(px(0.0))..px(0.);
2960
2961 offset.y = if self.is_single_line() {
2962 px(0.)
2963 } else {
2964 offset.y.clamp(safe_y_range.start, safe_y_range.end)
2965 };
2966 offset.x = clamp_horizontal_scroll_offset(
2967 offset.x,
2968 self.scroll_size.width,
2969 self.input_bounds.size.width,
2970 self.text_align,
2971 );
2972 if self.scroll_handle.offset() != offset {
2973 self.scroll_handle.set_offset(offset);
2974 cx.notify();
2975 }
2976 }
2977
2978 pub(crate) fn scroll_to(
2982 &mut self,
2983 offset: usize,
2984 direction: Option<MoveDirection>,
2985 cx: &mut Context<Self>,
2986 ) {
2987 let padding = if direction.is_some() {
2988 ScrollPadding::SurroundingLines
2989 } else {
2990 ScrollPadding::Minimal
2991 };
2992 self.scroll_to_with_padding(offset, direction, padding, cx);
2993 }
2994
2995 pub(crate) fn scroll_to_with_padding(
2998 &mut self,
2999 offset: usize,
3000 direction: Option<MoveDirection>,
3001 padding: ScrollPadding,
3002 cx: &mut Context<Self>,
3003 ) {
3004 let Some(last_layout) = self.last_layout.as_ref() else {
3005 return;
3006 };
3007 let Some(bounds) = self.last_bounds.as_ref() else {
3008 return;
3009 };
3010
3011 let mut scroll_offset = self.scroll_handle.offset();
3012 let was_offset = scroll_offset;
3013 let line_height = last_layout.line_height;
3014
3015 let point = self.text.offset_to_point(offset);
3016
3017 let row = point.row;
3018
3019 let display_row = self
3021 .display_map
3022 .offset_to_display_row(offset, self.line_end_affinity_at(offset));
3023 let row_offset_y = line_height * display_row;
3024
3025 let safety_margin = match last_layout.text_align {
3028 TextAlign::Left => RIGHT_MARGIN,
3029 TextAlign::Right => px(0.),
3030 TextAlign::Center => CURSOR_WIDTH,
3031 };
3032 if let Some(vi) = last_layout
3033 .visible_buffer_lines
3034 .iter()
3035 .position(|&line| line == row)
3036 {
3037 let line = &last_layout.lines[vi];
3038 let local_offset = offset.saturating_sub(last_layout.visible_line_byte_offsets[vi]);
3039 if let Some(pos) = line.position_for_index(local_offset, last_layout, false) {
3040 let bounds_width = bounds.size.width - last_layout.line_number_width;
3041 let col_offset_x = pos.x;
3042 if col_offset_x - safety_margin < -scroll_offset.x {
3043 scroll_offset.x = -col_offset_x + safety_margin;
3045 } else if col_offset_x + safety_margin > -scroll_offset.x + bounds_width {
3046 scroll_offset.x = -(col_offset_x - bounds_width + safety_margin);
3047 }
3048 }
3049 }
3050
3051 let edge_height =
3056 if matches!(padding, ScrollPadding::SurroundingLines) && self.is_code_editor() {
3057 super::element::cursor_surrounding_padding(
3058 self.mode.is_auto_grow(),
3059 self.cursor_surrounding_lines,
3060 super::element::viewport_visible_lines(bounds.size.height, line_height),
3061 line_height,
3062 )
3063 } else {
3064 px(0.)
3065 };
3066 scroll_offset.y = super::element::reveal_row(
3067 scroll_offset.y,
3068 row_offset_y,
3069 line_height,
3070 bounds.size.height,
3071 edge_height,
3072 );
3073
3074 if direction == Some(MoveDirection::Up) {
3076 scroll_offset.y = scroll_offset.y.max(was_offset.y);
3077 } else if direction == Some(MoveDirection::Down) {
3078 scroll_offset.y = scroll_offset.y.min(was_offset.y);
3079 }
3080
3081 let safe_y_min = (-self.scroll_size.height + self.input_bounds.size.height).min(px(0.));
3085 scroll_offset.x = scroll_offset.x.min(px(0.));
3086 scroll_offset.y = scroll_offset.y.clamp(safe_y_min, px(0.));
3087 self.deferred_scroll_offset = Some(scroll_offset);
3088 cx.notify();
3089 }
3090
3091 pub(super) fn show_character_palette(
3092 &mut self,
3093 _: &ShowCharacterPalette,
3094 window: &mut Window,
3095 _: &mut Context<Self>,
3096 ) {
3097 window.show_character_palette();
3098 }
3099
3100 fn selected_texts(&self) -> Vec<String> {
3102 let mut selections: Vec<CursorSelection> = self
3103 .selections
3104 .iter()
3105 .copied()
3106 .filter(|sel| !sel.is_empty())
3107 .collect();
3108 selections.sort_by_key(|sel| sel.start);
3109 selections
3110 .iter()
3111 .map(|sel| self.text.slice(*sel).to_string())
3112 .collect()
3113 }
3114
3115 pub(super) fn copy(&mut self, _: &Copy, _: &mut Window, cx: &mut Context<Self>) {
3116 if !self.is_copyable() {
3117 return;
3118 }
3119
3120 let texts = self.selected_texts();
3121
3122 cx.write_to_clipboard(ClipboardItem::new_string(texts.join("\n")));
3123 }
3124
3125 pub(super) fn cut(&mut self, _: &Cut, window: &mut Window, cx: &mut Context<Self>) {
3126 if !self.is_copyable() {
3127 return;
3128 }
3129
3130 let texts = self.selected_texts();
3131
3132 cx.write_to_clipboard(ClipboardItem::new_string(texts.join("\n")));
3133
3134 self.undo_manager.set_pending_intent(EditIntent::Atomic);
3135 self.replace_text_in_range_silent(None, "", window, cx);
3136 }
3137
3138 pub(super) fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
3139 if !self.is_editable() {
3140 return;
3141 }
3142 if let Some(clipboard) = cx.read_from_clipboard() {
3143 self.insert_clipboard(clipboard, window, cx);
3144 return;
3145 }
3146 let read = cx.read_from_clipboard_async();
3151 let target = self.paste_target();
3152 cx.spawn_in(window, async move |this, cx| match read.await {
3153 Ok(Some(clipboard)) => {
3154 this.update_in(cx, |this, window, cx| {
3155 if this.is_editable()
3162 && this.focus_handle.is_focused(window)
3163 && this.paste_target() == target
3164 {
3165 this.insert_clipboard(clipboard, window, cx);
3166 }
3167 })
3168 .ok();
3169 }
3170 Ok(None) => {}
3171 Err(error) => tracing::warn!("failed to read the clipboard for paste: {error}"),
3172 })
3173 .detach();
3174 }
3175
3176 fn paste_target(&self) -> PasteTarget {
3180 PasteTarget {
3181 document_revision: self.document_revision,
3182 selections: self.selections.iter().copied().collect(),
3183 }
3184 }
3185
3186 fn insert_clipboard(
3192 &mut self,
3193 clipboard: ClipboardItem,
3194 window: &mut Window,
3195 cx: &mut Context<Self>,
3196 ) {
3197 let Some(mut new_text) = clipboard.text().filter(|text| !text.is_empty()) else {
3198 return;
3199 };
3200 self.undo_manager.set_pending_intent(EditIntent::Atomic);
3202
3203 if !self.is_multi_line() {
3204 new_text = new_text.replace('\n', "");
3205 self.replace_text_in_range_silent(None, &new_text, window, cx);
3206 self.scroll_to(self.cursor(), None, cx);
3207 return;
3208 }
3209
3210 if !self.selections.is_single() {
3213 self.selections.merge_overlapping();
3214 }
3215 let lines: Vec<String> = new_text.split('\n').map(|s| s.to_string()).collect();
3216 let count = self.selections.len();
3217 if count > 1 && lines.len() == count {
3218 let mut selections: Vec<CursorSelection> = self.selections.iter().copied().collect();
3219 selections.sort_by_key(|sel| sel.start);
3220 let edits: Vec<(Range<usize>, String)> = selections
3221 .iter()
3222 .zip(lines)
3223 .map(|(sel, line)| (sel.start..sel.end, line))
3224 .collect();
3225 self.replace_text_in_ranges(&edits, window, cx);
3226 } else {
3227 self.replace_text_in_range_silent(None, &new_text, window, cx);
3228 }
3229 self.scroll_to(self.cursor(), None, cx);
3230 }
3231
3232 fn typing_intent(&self, edits: &[(Range<usize>, String)], new_text: &str) -> EditIntent {
3235 if !new_text.is_empty()
3236 && !new_text.contains(['\n', '\r'])
3237 && edits.iter().all(|(range, _)| range.is_empty())
3238 {
3239 EditIntent::Typing
3240 } else {
3241 EditIntent::Atomic
3242 }
3243 }
3244
3245 fn collapse_for_intent(
3251 intent: EditIntent,
3252 mut selection: CursorSelection,
3253 range: &Range<usize>,
3254 ) -> CursorSelection {
3255 match intent {
3256 EditIntent::Backspace => selection.place_at(range.end, None),
3257 EditIntent::DeleteForward => selection.place_at(range.start, None),
3258 EditIntent::Typing | EditIntent::Atomic => {}
3259 }
3260 selection
3261 }
3262
3263 fn rewrites_typed_char(
3266 &self,
3267 selection: CursorSelection,
3268 range: &Range<usize>,
3269 old_text: &str,
3270 new_text: &str,
3271 ) -> bool {
3272 !self.silent_replace_text
3273 && selection.is_collapsed()
3274 && selection.cursor_offset() == range.end
3275 && old_text.chars().count() == 1
3276 && !old_text.contains(['\n', '\r'])
3277 && !new_text.contains(['\n', '\r'])
3278 }
3279
3280 fn push_history(
3281 &mut self,
3282 text: &Rope,
3283 range: &Range<usize>,
3284 new_text: &str,
3285 requested_intent: Option<EditIntent>,
3286 selection_before: CursorSelection,
3287 selection_after: Option<CursorSelection>,
3288 ) -> bool {
3289 self.document_revision = self.document_revision.wrapping_add(1);
3290 let token_delta = self.edit_tokens(range, new_text.len());
3291 if self.undo_manager.is_ignoring() {
3292 return false;
3293 }
3294
3295 let range =
3296 text.clip_offset(range.start, Bias::Left)..text.clip_offset(range.end, Bias::Right);
3297 let old_text = text.slice(range.clone()).to_string();
3298 let new_range = range.start..range.start + new_text.len();
3299
3300 let intent = requested_intent.unwrap_or_else(|| {
3301 if range.is_empty()
3302 && old_text.is_empty()
3303 && !new_text.is_empty()
3304 && !new_text.contains(['\n', '\r'])
3305 || self.rewrites_typed_char(selection_before, &range, &old_text, new_text)
3306 {
3307 EditIntent::Typing
3308 } else {
3309 EditIntent::Atomic
3310 }
3311 });
3312
3313 let selection_before = Self::collapse_for_intent(intent, selection_before, &range);
3314 let selection_after =
3315 selection_after.unwrap_or_else(|| (new_range.end..new_range.end).into());
3316
3317 let open_transaction = self.undo_manager.has_open_transaction();
3318 let mut change = Change::new(range, &old_text, new_range, new_text);
3319 change.token_delta = token_delta;
3320 let recorded = self.undo_manager.record_transaction(change, intent);
3321 if recorded && !open_transaction {
3324 self.undo_manager
3325 .record_selections(vec![selection_before], vec![selection_after]);
3326 }
3327 recorded
3328 }
3329
3330 pub(super) fn undo(&mut self, _: &Undo, window: &mut Window, cx: &mut Context<Self>) {
3331 self.replay_history(true, window, cx);
3332 }
3333
3334 pub(super) fn redo(&mut self, _: &Redo, window: &mut Window, cx: &mut Context<Self>) {
3335 self.replay_history(false, window, cx);
3336 }
3337
3338 fn replay_history(&mut self, undo: bool, window: &mut Window, cx: &mut Context<Self>) {
3339 if !self.is_editable() {
3340 return;
3341 }
3342 let replay = if undo {
3343 self.undo_manager.undo()
3344 } else {
3345 self.undo_manager.redo()
3346 };
3347 let Some(replay) = replay else {
3348 return;
3349 };
3350 let token_aware = self.inline_tokens.is_some()
3351 || replay
3352 .changes
3353 .iter()
3354 .any(|change| change.token_delta.is_some());
3355 let emit_events = self.emit_events;
3356 if token_aware {
3357 self.emit_events = false;
3358 }
3359 self.replaying_history = token_aware;
3360 self.undo_manager.set_ignoring(true);
3361 for change in &replay.changes {
3362 let (range, text) = if undo {
3363 (change.new_range.into(), &change.old_text)
3364 } else {
3365 (change.old_range.into(), &change.new_text)
3366 };
3367 let range_utf16 = self.range_to_utf16(&range);
3368 self.replace_text_in_range_silent(Some(range_utf16), text, window, cx);
3369 self.replay_tokens(&range, text.len(), change.token_delta.as_deref(), undo);
3370 }
3371 self.restore_selections(replay.selections);
3372 self.mode
3373 .restore_auto_closed_pairs(replay.auto_closed_pairs.unwrap_or_default());
3374 if token_aware {
3375 self.ime_marked_range = None;
3378 }
3379 self.undo_manager.set_ignoring(false);
3380 self.replaying_history = false;
3381 self.emit_events = emit_events;
3382 if token_aware && emit_events {
3383 cx.emit(InputEvent::Change);
3384 }
3385 cx.notify();
3386 }
3387
3388 fn restore_selections(&mut self, selections: Option<Vec<CursorSelection>>) {
3392 let Some(selections) = selections else {
3393 return;
3394 };
3395
3396 let len = self.text.len();
3397 let restored: Vec<CursorSelection> = selections
3398 .into_iter()
3399 .map(|mut sel| {
3400 sel.start = sel.start.min(len);
3401 sel.end = sel.end.min(len);
3402 sel
3403 })
3404 .collect();
3405 self.selections.replace_all(restored);
3406 self.selections.merge_overlapping();
3407 }
3408
3409 pub fn cursor(&self) -> usize {
3413 if let Some(ime_marked_range) = &self.ime_marked_range {
3414 return ime_marked_range.end;
3415 }
3416
3417 self.selections.active().cursor_offset()
3418 }
3419
3420 pub(super) fn active_selection(&self) -> &CursorSelection {
3422 self.selections.active()
3423 }
3424
3425 pub(super) fn active_selection_mut(&mut self) -> &mut CursorSelection {
3427 self.selections.active_mut()
3428 }
3429
3430 pub(super) fn set_selection(&mut self, start: usize, end: usize) {
3433 let range = self.normalize_token_range(start..end);
3434 let (start, end) = (range.start, range.end);
3435 let active = self.active_selection_mut();
3436 active.start = start;
3437 active.end = end;
3438 }
3439
3440 pub(super) fn set_cursor_to(&mut self, offset: usize) {
3443 let offset = self.token_boundary(offset, Bias::Right);
3444 let active = self.active_selection_mut();
3445 active.start = offset;
3446 active.end = offset;
3447 active.reversed = false;
3448 }
3449
3450 pub fn visible_row_range(&self) -> Option<std::ops::Range<usize>> {
3452 self.last_layout.as_ref().map(|l| l.visible_range.clone())
3453 }
3454
3455 pub fn scroll_offset(&self) -> gpui::Point<gpui::Pixels> {
3457 self.scroll_handle.offset()
3458 }
3459
3460 pub fn set_scroll_offset(&mut self, offset: gpui::Point<gpui::Pixels>, cx: &mut Context<Self>) {
3464 self.deferred_scroll_offset = Some(offset);
3465 cx.notify();
3466 }
3467
3468 pub fn line_height(&self) -> Option<gpui::Pixels> {
3470 self.last_layout.as_ref().map(|l| l.line_height)
3471 }
3472
3473 pub fn selected_range(&self) -> std::ops::Range<usize> {
3481 (*self.selections.active()).into()
3482 }
3483
3484 pub fn select_all(&mut self, _: &mut Window, cx: &mut Context<Self>) {
3485 self.undo_manager.break_transaction_coalescing();
3486 self.selections.remove_all_but_active();
3487 self.set_selection(0, self.text.len());
3488 cx.notify();
3489 }
3490
3491 pub fn set_selected_range(&mut self, range: Range<usize>, cx: &mut Context<Self>) {
3496 let range = self.normalize_token_range(range);
3497 let end_bias = if range.start == range.end {
3498 Bias::Left
3499 } else {
3500 Bias::Right
3501 };
3502 let start = self.text.clip_offset(range.start, Bias::Left);
3503 let end = self.text.clip_offset(range.end, end_bias);
3504
3505 self.move_to(start, None, cx);
3506 self.active_selection_mut().reversed = false;
3507 self.selected_word_range = None;
3508 self.select_to(end, cx);
3509 }
3510
3511 pub fn add_selection(&mut self, range: Range<usize>, cx: &mut Context<Self>) {
3522 if !self.is_multi_line() {
3523 self.set_selected_range(range, cx);
3524 return;
3525 }
3526
3527 let range = self.normalize_token_range(range);
3528 let end_bias = if range.start == range.end {
3529 Bias::Left
3530 } else {
3531 Bias::Right
3532 };
3533 let start = self.cursor_boundary(range.start, Bias::Left);
3534 let end = self.cursor_boundary(self.text.clip_offset(range.end, end_bias), Bias::Left);
3537
3538 for sel in self.selections.iter() {
3539 if sel.start <= start && end <= sel.end {
3540 return;
3541 }
3542 }
3543
3544 self.undo_manager.break_transaction_coalescing();
3545 self.selected_word_range = None;
3546 self.pause_blink_cursor(cx);
3547 M::hide_context_menu(self, cx);
3548 M::clear_inline_completion(self, cx);
3549 let id = self.selections.generate_id();
3550 let mut selection = CursorSelection::new(id, start, end);
3551 selection.column_anchor = self.preferred_column_for(end);
3552 self.selections.add(selection);
3553 self.selections.merge_overlapping();
3554 self.scroll_to(end, None, cx);
3555 cx.notify();
3556 }
3557
3558 pub(crate) fn index_for_mouse_position(&self, position: Point<Pixels>) -> (usize, bool) {
3566 let (offset, line_end_affinity, _) = self.resolve_mouse_position(position);
3567 (offset, line_end_affinity)
3568 }
3569
3570 pub(super) fn resolve_mouse_position(&self, position: Point<Pixels>) -> (usize, bool, usize) {
3577 if self.text.len() == 0 {
3579 return (0, false, 0);
3580 }
3581
3582 let (Some(bounds), Some(last_layout)) =
3583 (self.last_bounds.as_ref(), self.last_layout.as_ref())
3584 else {
3585 return (0, false, 0);
3586 };
3587
3588 let line_height = last_layout.line_height;
3589 let line_number_width = last_layout.line_number_width;
3590
3591 let inner_position = position - bounds.origin - point(line_number_width, px(0.));
3602
3603 let mut y_offset = last_layout.visible_top;
3604 let mut last_line_pos = None;
3607
3608 for (vi, (line_layout, _buffer_line)) in last_layout
3610 .lines
3611 .iter()
3612 .zip(last_layout.visible_buffer_lines.iter())
3613 .enumerate()
3614 {
3615 let line_start_offset = last_layout.visible_line_byte_offsets[vi];
3616
3617 let line_origin = point(px(0.), y_offset);
3619 let pos = inner_position - line_origin;
3620
3621 if self.is_single_line() {
3623 let local_index = line_layout.closest_index_for_x(pos.x, last_layout);
3624 return (
3626 self.resolve_index(line_start_offset + local_index),
3627 false,
3628 0,
3629 );
3630 }
3631
3632 if let Some((local_index, line_end_affinity)) =
3634 line_layout.closest_index_for_position(pos, last_layout)
3635 {
3636 return (
3637 self.resolve_index(line_start_offset + local_index),
3638 line_end_affinity,
3639 line_layout.columns_past_line_end(pos, last_layout),
3640 );
3641 } else if pos.y < px(0.) {
3642 return (self.resolve_index(line_start_offset), false, 0);
3644 }
3645
3646 y_offset += line_layout.size(line_height).height;
3647 last_line_pos = Some(pos);
3648 }
3649
3650 let last_position = last_layout
3653 .lines
3654 .last()
3655 .zip(last_line_pos)
3656 .zip(last_layout.visible_line_byte_offsets.last())
3657 .map(|((line_layout, pos), line_start)| {
3658 let last_row_top = (line_layout.size(line_height).height - line_height).max(px(0.));
3659 let pos = point(pos.x, last_row_top);
3660 (
3661 self.resolve_index(line_start + line_layout.len()),
3662 false,
3663 line_layout.columns_past_line_end(pos, last_layout),
3664 )
3665 });
3666
3667 last_position.unwrap_or((self.text.len(), false, 0))
3668 }
3669
3670 fn resolve_index(&self, index: usize) -> usize {
3673 if self.masked {
3674 self.text.char_index_to_offset(index / MASK_CHAR.len_utf8())
3675 } else {
3676 index.min(self.text.len())
3677 }
3678 }
3679
3680 fn extend_selection(
3690 sel: &mut CursorSelection,
3691 offset: usize,
3692 word_range: Option<CursorSelection>,
3693 ) {
3694 if sel.reversed {
3695 sel.start = offset;
3696 } else {
3697 sel.end = offset;
3698 }
3699
3700 if sel.end < sel.start {
3701 sel.reversed = !sel.reversed;
3702 std::mem::swap(&mut sel.start, &mut sel.end);
3703 }
3704
3705 if let Some(word_range) = word_range {
3706 if sel.start > word_range.start {
3707 sel.start = word_range.start;
3708 }
3709 if sel.end < word_range.end {
3710 sel.end = word_range.end;
3711 }
3712 }
3713 }
3714
3715 pub(crate) fn select_to(&mut self, offset: usize, cx: &mut Context<Self>) {
3717 self.select_to_with_affinity(offset, false, cx);
3718 }
3719
3720 pub(crate) fn select_to_with_affinity(
3727 &mut self,
3728 offset: usize,
3729 line_end_affinity: bool,
3730 cx: &mut Context<Self>,
3731 ) {
3732 M::clear_inline_completion(self, cx);
3733
3734 self.cursor_line_end_affinity = line_end_affinity;
3735 let offset = self.cursor_boundary(offset, Bias::Left);
3736 let word_range = self.selected_word_range;
3737 Self::extend_selection(self.active_selection_mut(), offset, word_range);
3738 let range = self.normalize_token_range(self.selected_range());
3739 self.set_selection(range.start, range.end);
3740
3741 if self.active_selection().is_empty() {
3742 self.update_preferred_column();
3743 }
3744 cx.notify()
3745 }
3746
3747 fn select_all_cursors_to(
3750 &mut self,
3751 f: impl Fn(&Self, &CursorSelection) -> usize,
3752 cx: &mut Context<Self>,
3753 ) {
3754 self.select_all_cursors_to_with_affinity(|s, sel| (f(s, sel), false), false, cx);
3755 }
3756
3757 fn select_all_cursors_to_with_affinity(
3760 &mut self,
3761 f: impl Fn(&Self, &CursorSelection) -> (usize, bool),
3762 preserve_column: bool,
3763 cx: &mut Context<Self>,
3764 ) {
3765 self.pause_blink_cursor(cx);
3766 self.undo_manager.break_transaction_coalescing();
3767 M::clear_inline_completion(self, cx);
3768
3769 let mut active_affinity = false;
3770 let new_selections: Vec<CursorSelection> = self
3771 .selections
3772 .iter()
3773 .map(|sel| {
3774 let (offset, affinity) = f(self, sel);
3775 if sel.id == self.active_selection().id {
3776 active_affinity = affinity;
3777 }
3778 let offset = self.cursor_boundary(offset, Bias::Left);
3779 let mut new_sel = *sel;
3780 Self::extend_selection(&mut new_sel, offset, None);
3781 let range = self.normalize_token_range(new_sel.start..new_sel.end);
3782 new_sel.start = range.start;
3783 new_sel.end = range.end;
3784 if !preserve_column {
3785 new_sel.column_anchor =
3786 self.preferred_column_for_with_affinity(new_sel.cursor_offset(), affinity);
3787 } else if new_sel.column_anchor.is_none() {
3788 new_sel.column_anchor = self.preferred_column_for_with_affinity(
3789 sel.cursor_offset(),
3790 self.line_end_affinity_for(sel),
3791 );
3792 }
3793 new_sel
3794 })
3795 .collect();
3796 self.cursor_line_end_affinity = active_affinity;
3797 self.selections.replace_all(new_selections);
3798 self.selections.merge_overlapping();
3799
3800 self.scroll_to(self.cursor(), None, cx);
3801 cx.notify()
3802 }
3803
3804 pub fn unselect(&mut self, _: &mut Window, cx: &mut Context<Self>) {
3806 self.undo_manager.break_transaction_coalescing();
3807 let offset = self.cursor();
3808 self.set_cursor_to(offset);
3809 cx.notify()
3810 }
3811
3812 #[inline]
3813 pub(super) fn offset_from_utf16(&self, offset: usize) -> usize {
3814 self.text.offset_utf16_to_offset(offset)
3815 }
3816
3817 #[inline]
3818 pub(super) fn offset_to_utf16(&self, offset: usize) -> usize {
3819 self.text.offset_to_offset_utf16(offset)
3820 }
3821
3822 #[inline]
3823 pub(crate) fn range_to_utf16(&self, range: &Range<usize>) -> Range<usize> {
3824 self.offset_to_utf16(range.start)..self.offset_to_utf16(range.end)
3825 }
3826
3827 #[inline]
3828 pub(super) fn range_from_utf16(&self, range_utf16: &Range<usize>) -> Range<usize> {
3829 self.offset_from_utf16(range_utf16.start)..self.offset_from_utf16(range_utf16.end)
3830 }
3831
3832 fn clamp_offset_to_visible_backward(&self, offset: usize) -> usize {
3835 let line = self.text.offset_to_point(offset).row;
3836 if self.display_map.is_buffer_line_hidden(line) {
3837 for fold in self.display_map.folded_ranges() {
3838 if line > fold.start_line && line <= fold.end_line {
3839 return self.text.line_end_offset(fold.start_line);
3840 }
3841 }
3842 }
3843 offset
3844 }
3845
3846 fn clamp_offset_to_visible_forward(&self, offset: usize) -> usize {
3849 let line = self.text.offset_to_point(offset).row;
3850 if self.display_map.is_buffer_line_hidden(line) {
3851 for fold in self.display_map.folded_ranges() {
3852 if line > fold.start_line && line <= fold.end_line {
3853 return self.text.line_start_offset(fold.end_line);
3854 }
3855 }
3856 }
3857 offset
3858 }
3859
3860 pub(super) fn cursor_boundary(&self, offset: usize, bias: Bias) -> usize {
3863 let offset = self.token_boundary(self.text.clip_offset(offset, bias), bias);
3864 if offset > 0
3865 && self.text.char_at(offset - 1) == Some('\r')
3866 && self.text.char_at(offset) == Some('\n')
3867 {
3868 if bias == Bias::Left {
3869 offset - 1
3870 } else {
3871 offset + 1
3872 }
3873 } else {
3874 offset
3875 }
3876 }
3877
3878 pub(super) fn previous_boundary(&self, offset: usize) -> usize {
3879 let offset = self.cursor_boundary(offset.saturating_sub(1), Bias::Left);
3880 self.clamp_offset_to_visible_backward(offset)
3881 }
3882
3883 pub(super) fn next_boundary(&self, offset: usize) -> usize {
3884 let offset = self.cursor_boundary(offset.saturating_add(1), Bias::Right);
3885 self.clamp_offset_to_visible_forward(offset)
3886 }
3887
3888 pub(crate) fn show_cursor(&self, window: &Window, cx: &App) -> bool {
3890 (self.focus_handle.is_focused(window) || M::is_context_menu_open(self, cx))
3891 && !self.disabled
3892 && self.blink_cursor.read(cx).visible()
3893 && window.is_window_active()
3894 }
3895
3896 fn on_focus(&mut self, _: &mut Window, cx: &mut Context<Self>) {
3897 self.blink_cursor.update(cx, |cursor, cx| {
3898 cursor.start(cx);
3899 });
3900 cx.emit(InputEvent::Focus);
3901 }
3902
3903 fn on_blur(&mut self, window: &mut Window, cx: &mut Context<Self>) {
3904 self.revealed_viewport = None;
3905 if M::is_context_menu_open(self, cx) {
3906 return;
3907 }
3908
3909 M::hide_context_menu(self, cx);
3910 self.undo_manager.break_transaction_coalescing();
3911
3912 M::clear_hover_state(self, cx);
3916 self.diagnostic_popover = None;
3917 M::clear_inline_completion(self, cx);
3918 self.dismiss_touch_selection(cx);
3919 self.blink_cursor.update(cx, |cursor, cx| {
3920 cursor.stop(cx);
3921 });
3922 self.clamp_number_value(window, cx);
3923 cx.emit(InputEvent::Blur);
3924 cx.notify();
3925 }
3926
3927 fn clamp_number_value(&mut self, window: &mut Window, cx: &mut Context<Self>) {
3932 if !self.is_single_line() {
3933 return;
3934 }
3935 if !matches!(self.mask_pattern, MaskPattern::Number { .. }) {
3936 return;
3937 }
3938 if self.number_min.is_none() && self.number_max.is_none() {
3939 return;
3940 }
3941
3942 let Ok(value) = self.unmask_value().parse::<f64>() else {
3943 return;
3944 };
3945
3946 let clamped = match (self.number_min, self.number_max) {
3947 (Some(min), _) if value < min => min,
3948 (_, Some(max)) if value > max => max,
3949 _ => return,
3950 };
3951
3952 let new_text = clamped.to_string();
3955 if !self.is_valid_input(&new_text, cx) {
3956 return;
3957 }
3958
3959 let range = self.range_to_utf16(&(0..self.text.len()));
3960 self.replace_text_in_range_silent(Some(range), &new_text, window, cx);
3961 }
3962
3963 pub(super) fn pause_blink_cursor(&mut self, cx: &mut Context<Self>) {
3964 self.blink_cursor.update(cx, |cursor, cx| {
3965 cursor.pause(cx);
3966 });
3967 }
3968
3969 pub(super) fn on_drag_move(
3970 &mut self,
3971 event: &MouseMoveEvent,
3972 window: &mut Window,
3973 cx: &mut Context<Self>,
3974 ) {
3975 if self.text.len() == 0 {
3976 return;
3977 }
3978
3979 if self.last_layout.is_none() {
3980 return;
3981 }
3982
3983 if !self.focus_handle.is_focused(window) {
3984 return;
3985 }
3986
3987 if !self.selecting {
3988 return;
3989 }
3990
3991 self.auto_scroll.last_drag_position = Some(event.position);
3992 let (offset, line_end_affinity, columns_past_line_end) =
3993 self.resolve_mouse_position(event.position);
3994 if let Some(start) = self.column_select_start {
3995 let end = ColumnarPoint::new(offset, columns_past_line_end);
3996 self.build_columnar_selection(start, end, cx);
3997 } else {
3998 self.select_to_with_affinity(offset, line_end_affinity, cx);
3999 }
4000
4001 if !self.is_single_line() {
4002 let delta = AutoScroll::compute_delta(event.position.y, self.input_bounds);
4003 let scroll_delta = delta.map(|d| -d);
4005 self.auto_scroll.set(scroll_delta, cx, |delta, state, cx| {
4006 let current = state.scroll_handle.offset();
4007 state.update_scroll_offset(Some(point(current.x, current.y + delta)), cx);
4008 if let Some(pos) = state.auto_scroll.last_drag_position {
4009 let (offset, line_end_affinity, columns_past_line_end) =
4010 state.resolve_mouse_position(pos);
4011 if let Some(start) = state.column_select_start {
4012 let end = ColumnarPoint::new(offset, columns_past_line_end);
4013 state.build_columnar_selection(start, end, cx);
4014 } else {
4015 state.select_to_with_affinity(offset, line_end_affinity, cx);
4016 }
4017 }
4018 });
4019 }
4020 }
4021
4022 pub(super) fn normalize_input<'a>(&self, new_text: &'a str) -> Cow<'a, str> {
4028 let normalized = if matches!(self.mask_pattern, MaskPattern::Number { .. }) {
4029 normalize_number_input(new_text)
4030 } else {
4031 Cow::Borrowed(new_text)
4032 };
4033
4034 if self.is_single_line() && normalized.contains(['\n', '\r']) {
4035 Cow::Owned(normalized.replace(['\n', '\r'], ""))
4036 } else {
4037 normalized
4038 }
4039 }
4040
4041 pub(crate) fn is_valid_input(&self, new_text: &str, cx: &mut Context<Self>) -> bool {
4042 if new_text.is_empty() {
4043 return true;
4044 }
4045
4046 if let Some(validate) = &self.validate {
4047 if !validate(new_text, cx) {
4048 return false;
4049 }
4050 }
4051
4052 if !self.mask_pattern.is_valid(new_text) {
4053 return false;
4054 }
4055
4056 let Some(pattern) = &self.pattern else {
4057 return true;
4058 };
4059
4060 pattern.is_match(new_text)
4061 }
4062
4063 pub fn mask_pattern(mut self, pattern: impl Into<MaskPattern>) -> Self {
4073 self.mask_pattern = pattern.into();
4074 self.mask_pattern_set = true;
4075 if let Some(placeholder) = self.mask_pattern.placeholder() {
4076 self.placeholder = placeholder.into();
4077 }
4078 self
4079 }
4080
4081 pub fn set_mask_pattern(
4082 &mut self,
4083 pattern: impl Into<MaskPattern>,
4084 _: &mut Window,
4085 cx: &mut Context<Self>,
4086 ) {
4087 self.mask_pattern = pattern.into();
4088 self.mask_pattern_set = true;
4089 if let Some(placeholder) = self.mask_pattern.placeholder() {
4090 self.placeholder = placeholder.into();
4091 }
4092 cx.notify();
4093 }
4094
4095 pub fn ensure_number_mask(&mut self) {
4097 if self.mask_pattern_set {
4098 return;
4099 }
4100 self.mask_pattern = MaskPattern::Number {
4101 separator: None,
4102 fraction: None,
4103 };
4104 }
4105
4106 pub(super) fn set_input_bounds(&mut self, new_bounds: Bounds<Pixels>, cx: &mut Context<Self>) {
4107 let wrap_width_changed = self.input_bounds.size.width != new_bounds.size.width;
4108 self.input_bounds = new_bounds;
4109
4110 if let Some(last_layout) = self.last_layout.as_ref() {
4112 if wrap_width_changed {
4113 let wrap_width = if !self.soft_wrap {
4114 None
4116 } else {
4117 last_layout.wrap_width
4118 };
4119
4120 self.display_map.on_layout_changed(wrap_width, cx);
4121 if self.is_multi_line() {
4122 self.mode.update_auto_grow(&self.display_map);
4123 }
4124 cx.notify();
4125 }
4126 }
4127 }
4128
4129 pub fn selected_text(&self) -> RopeSlice<'_> {
4134 let range_utf16 = self.range_to_utf16(&self.selected_range());
4135 let range = self.range_from_utf16(&range_utf16);
4136 self.text.slice(range)
4137 }
4138
4139 pub fn range_to_bounds(&self, range: &Range<usize>) -> Option<Bounds<Pixels>> {
4143 let Some(last_layout) = self.last_layout.as_ref() else {
4144 return None;
4145 };
4146
4147 let Some(last_bounds) = self.last_bounds else {
4148 return None;
4149 };
4150
4151 let (_, _, start_pos) = self.line_and_position_for_offset(range.start);
4152 let (_, _, end_pos) = self.line_and_position_for_offset(range.end);
4153
4154 let Some(start_pos) = start_pos else {
4155 return None;
4156 };
4157 let Some(end_pos) = end_pos else {
4158 return None;
4159 };
4160
4161 Some(Bounds::from_corners(
4162 last_bounds.origin + start_pos,
4163 last_bounds.origin + end_pos + point(px(0.), last_layout.line_height),
4164 ))
4165 }
4166
4167 pub(crate) fn replace_text_in_range_silent(
4171 &mut self,
4172 range_utf16: Option<Range<usize>>,
4173 new_text: &str,
4174 window: &mut Window,
4175 cx: &mut Context<Self>,
4176 ) {
4177 self.silent_replace_text = true;
4178 self.replace_text_in_range(range_utf16, new_text, window, cx);
4179 self.silent_replace_text = false;
4180 }
4181
4182 pub(crate) fn replace_text_in_ranges(
4187 &mut self,
4188 edits: &[(Range<usize>, String)],
4189 window: &mut Window,
4190 cx: &mut Context<Self>,
4191 ) {
4192 if !self.is_editable() || edits.is_empty() {
4193 return;
4194 }
4195
4196 M::hide_context_menu(self, cx);
4199 M::clear_inline_completion(self, cx);
4200
4201 let mut sorted: Vec<(Range<usize>, &str)> = edits
4204 .iter()
4205 .map(|(range, text)| (self.normalize_token_range(range.clone()), text.as_str()))
4206 .collect();
4207 sorted.sort_by_key(|edit| std::cmp::Reverse(edit.0.start));
4208 if !self.token_spans().is_empty() {
4209 let mut merged: Vec<(Range<usize>, &str)> = Vec::with_capacity(sorted.len());
4210 for (range, text) in sorted {
4211 if let Some(last) = merged
4212 .last_mut()
4213 .filter(|last| range.end > last.0.start || range == last.0)
4214 {
4215 last.0.start = range.start.min(last.0.start);
4216 last.0.end = range.end.max(last.0.end);
4217 } else {
4218 merged.push((range, text));
4219 }
4220 }
4221 sorted = merged;
4222 }
4223
4224 #[cfg(debug_assertions)]
4225 for pair in sorted.windows(2) {
4226 debug_assert!(
4227 pair[1].0.end <= pair[0].0.start,
4228 "replace_text_in_ranges requires disjoint ranges"
4229 );
4230 }
4231
4232 let requested_intent = self.undo_manager.take_pending_intent();
4236 let selection_before = *self.active_selection();
4237 let original_selections: Vec<CursorSelection> = self.selections.iter().copied().collect();
4238 let selections_before: Vec<CursorSelection> = self
4242 .selections
4243 .iter()
4244 .map(|selection| {
4245 Self::collapse_for_intent(
4246 requested_intent.unwrap_or(EditIntent::Atomic),
4247 *selection,
4248 &(selection.start..selection.end),
4249 )
4250 })
4251 .collect();
4252 let group = sorted.len() > 1;
4253 if group {
4254 self.undo_manager.begin_transaction();
4255 }
4256
4257 let auto_closed_pairs_before = self.mode.auto_closed_pairs().clone();
4258 let mut recorded = false;
4259 let mut highlighter_edits = Vec::new();
4262 for (range, new_text) in &sorted {
4263 let old_text = self.text.clone();
4264 self.mode.adjust_auto_closed_pair(range, new_text.len());
4265 self.text.replace(range.clone(), new_text);
4266
4267 M::adjust_annotations(self, range, new_text.len());
4268 recorded |= self.push_history(
4269 &old_text,
4270 range,
4271 new_text,
4272 requested_intent,
4273 selection_before,
4274 None,
4275 );
4276
4277 self.display_map
4279 .adjust_folds_for_edit(&old_text, range, new_text);
4280 self.display_map
4281 .on_text_changed(&self.text, range, &Rope::from(*new_text), cx);
4282
4283 if group {
4284 if self.is_code_editor() {
4285 let edit =
4286 super::mode::replacement_input_edit(&old_text, &self.text, range, new_text);
4287 highlighter_edits.push((edit, self.text.clone()));
4288 }
4289 } else {
4290 self.mode.update_highlighter(
4291 super::mode::HighlighterUpdate {
4292 selected_range: range,
4293 old_text: &old_text,
4294 new_text: &self.text,
4295 change_text: new_text,
4296 force: true,
4297 },
4298 window,
4299 cx,
4300 );
4301
4302 self.update_fold_candidates_incremental(range, new_text);
4303 }
4304 }
4305
4306 if group {
4307 self.mode
4308 .update_highlighter_batch::<M>(&highlighter_edits, window, cx);
4309
4310 let mut shift = 0isize;
4314 for (range, new_text) in sorted.iter().rev() {
4315 let start = (range.start as isize + shift) as usize;
4316 self.update_fold_candidates_incremental(&(start..start), new_text);
4317 shift += new_text.len() as isize - range.len() as isize;
4318 }
4319
4320 self.undo_manager.commit_transaction();
4321 }
4322
4323 if let Some(diagnostics) = self.mode.diagnostics_mut() {
4325 diagnostics.reset(&self.text)
4326 }
4327 M::refresh_language_features(self, window, cx);
4328 self.update_search(cx);
4329
4330 let mut ascending: Vec<(Range<usize>, &str)> = sorted.clone();
4333 ascending.sort_by_key(|edit| edit.0.start);
4334 let text_len = self.text.len();
4335 let mut delta: isize = 0;
4336 let mut edit_results = Vec::with_capacity(ascending.len());
4337 for (range, new_text) in &ascending {
4338 let offset = ((range.start as isize + delta) as usize + new_text.len()).min(text_len);
4339 edit_results.push((range.clone(), offset));
4340 delta += new_text.len() as isize - (range.end as isize - range.start as isize);
4341 }
4342
4343 let mut used = vec![false; edit_results.len()];
4344 let mut new_selections: Vec<CursorSelection> = Vec::with_capacity(ascending.len());
4345 for selection in original_selections {
4346 if let Ok(index) = edit_results
4347 .binary_search_by_key(&(selection.start, selection.end), |(range, _)| {
4348 (range.start, range.end)
4349 })
4350 && !used[index]
4351 {
4352 let offset = edit_results[index].1;
4353 used[index] = true;
4354 let mut selection = selection;
4355 selection.place_at(offset, None);
4356 new_selections.push(selection);
4357 }
4358 }
4359 for (index, (_, offset)) in edit_results.into_iter().enumerate() {
4360 if !used[index] {
4361 new_selections.push(CursorSelection::new(
4362 self.selections.generate_id(),
4363 offset,
4364 offset,
4365 ));
4366 }
4367 }
4368 self.selections.replace_all(new_selections);
4369 self.selections.merge_overlapping();
4370
4371 let selections_after: Vec<CursorSelection> = self.selections.iter().copied().collect();
4373 if recorded {
4374 self.undo_manager.record_auto_closed_pairs(
4375 auto_closed_pairs_before,
4376 self.mode.auto_closed_pairs().clone(),
4377 );
4378 self.undo_manager
4379 .record_selections(selections_before, selections_after);
4380 }
4381
4382 self.ime_marked_range.take();
4383 self.update_preferred_column();
4384 self.dismiss_touch_selection(cx);
4385 if self.is_multi_line() {
4386 self.mode.update_auto_grow(&self.display_map);
4387 }
4388 if self.emit_events {
4389 cx.emit(InputEvent::Change);
4390 }
4391 cx.notify();
4392 }
4393
4394 fn update_fold_candidates(&mut self) {
4397 if !self.mode.is_folding() {
4398 return;
4399 }
4400
4401 let Some(highlighter_rc) = self.mode.highlighter() else {
4402 return;
4403 };
4404
4405 let highlighter = highlighter_rc.borrow();
4406 let Some(highlighter) = highlighter.as_ref() else {
4407 return;
4408 };
4409
4410 let fold_ranges = highlighter.fold_ranges(&self.text);
4411 self.display_map.set_fold_candidates(fold_ranges);
4412 }
4413
4414 fn update_fold_candidates_incremental(&mut self, edit_range: &Range<usize>, new_text: &str) {
4417 if !self.mode.is_folding() {
4418 return;
4419 }
4420
4421 let Some(highlighter_rc) = self.mode.highlighter() else {
4422 return;
4423 };
4424
4425 let highlighter = highlighter_rc.borrow();
4426 let Some(highlighter) = highlighter.as_ref() else {
4427 return;
4428 };
4429
4430 let new_end = edit_range.start + new_text.len();
4432 self.display_map.update_fold_candidates_for_edit(
4433 |range, text| highlighter.fold_ranges_for_edit(range, text),
4434 edit_range.start..new_end,
4435 &self.text,
4436 );
4437 }
4438}
4439
4440impl<M: InputModeKind> EntityInputHandler for InputBaseState<M> {
4441 fn text_for_range(
4442 &mut self,
4443 range_utf16: Range<usize>,
4444 adjusted_range: &mut Option<Range<usize>>,
4445 _window: &mut Window,
4446 _cx: &mut Context<Self>,
4447 ) -> Option<String> {
4448 let range = self.range_from_utf16(&range_utf16);
4449 adjusted_range.replace(self.range_to_utf16(&range));
4450 Some(self.text.slice(range).to_string())
4451 }
4452
4453 fn selected_text_range(
4454 &mut self,
4455 _ignore_disabled_input: bool,
4456 _window: &mut Window,
4457 _cx: &mut Context<Self>,
4458 ) -> Option<UTF16Selection> {
4459 Some(UTF16Selection {
4460 range: self.range_to_utf16(&self.selected_range()),
4461 reversed: self.active_selection().reversed,
4462 })
4463 }
4464
4465 fn marked_text_range(
4466 &self,
4467 _window: &mut Window,
4468 _cx: &mut Context<Self>,
4469 ) -> Option<Range<usize>> {
4470 self.ime_marked_range
4471 .map(|range| self.range_to_utf16(&range.into()))
4472 }
4473
4474 fn unmark_text(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {
4475 self.ime_marked_range = None;
4476 self.undo_manager.commit_transaction();
4477 }
4478
4479 fn replace_text_in_range(
4484 &mut self,
4485 range_utf16: Option<Range<usize>>,
4486 new_text: &str,
4487 window: &mut Window,
4488 cx: &mut Context<Self>,
4489 ) {
4490 let requested_intent = self.undo_manager.take_pending_intent();
4491 if !self.is_editable() {
4492 return;
4493 }
4494 let selection_before = *self.active_selection();
4495 let ends_composition = self.ime_marked_range.is_some();
4498
4499 self.pause_blink_cursor(cx);
4500
4501 let new_text = if self.replaying_history {
4505 Cow::Borrowed(new_text)
4506 } else {
4507 self.normalize_input(new_text)
4508 };
4509 let new_text: &str = &new_text;
4510
4511 let range = range_utf16
4512 .as_ref()
4513 .map(|range_utf16| self.range_from_utf16(range_utf16))
4514 .or(self.ime_marked_range.map(|range| {
4515 let range = self.range_to_utf16(&(range.start..range.end));
4516 self.range_from_utf16(&range)
4517 }))
4518 .unwrap_or(self.selected_range());
4519 let range = self.normalize_token_range(range);
4520
4521 if range.is_empty()
4527 && range.start == self.cursor()
4528 && self.ime_marked_range.is_none()
4529 && !self.silent_replace_text
4530 && self.is_code_editor()
4531 && self.selections.is_single()
4532 && self.active_selection().is_empty()
4533 {
4534 if let Some(target) = self.skip_over_target(new_text) {
4535 self.set_cursor_to(target);
4536 self.update_preferred_column();
4537 cx.notify();
4538 return;
4539 }
4540 }
4541
4542 if self.is_multi_line() {
4543 let multi_cursor = range_utf16.is_none()
4544 && self.ime_marked_range.is_none()
4545 && !self.selections.is_single();
4546 if multi_cursor {
4547 self.selections.merge_overlapping();
4548 let mut edits: Vec<(Range<usize>, String)> = self
4549 .selections
4550 .iter()
4551 .map(|sel| (sel.start..sel.end, new_text.to_string()))
4552 .collect();
4553 edits.sort_by_key(|(range, _)| range.start);
4554 let intent =
4558 requested_intent.unwrap_or_else(|| self.typing_intent(&edits, new_text));
4559 self.undo_manager.begin_transaction_with(intent);
4560 self.undo_manager.set_pending_intent(intent);
4561 self.replace_text_in_ranges(&edits, window, cx);
4562 self.undo_manager.commit_transaction();
4563 } else {
4564 if range_utf16.is_some() {
4565 self.selections.remove_all_but_active();
4566 }
4567 if let Some(intent) = requested_intent {
4568 self.undo_manager.set_pending_intent(intent);
4569 }
4570 if let Some((open_len, closer)) = self.auto_close_target(&range, new_text) {
4571 let replacement = format!("{new_text}{closer}");
4573 self.replace_text_in_ranges(&[(range.clone(), replacement)], window, cx);
4574 let cursor = range.start + new_text.len();
4575 self.mode.track_auto_closed_pair(
4576 cursor - open_len..cursor,
4577 cursor..cursor + closer.len(),
4578 );
4579 self.undo_manager
4580 .record_auto_closed_pairs_after(self.mode.auto_closed_pairs().clone());
4581 self.set_cursor_to(cursor);
4582 self.update_preferred_column();
4583 self.undo_manager.record_selections(
4584 vec![selection_before],
4585 self.selections.iter().copied().collect(),
4586 );
4587 } else {
4588 self.replace_text_in_ranges(
4589 &[(range.clone(), new_text.to_string())],
4590 window,
4591 cx,
4592 );
4593 }
4594 }
4595 if ends_composition {
4596 self.undo_manager.commit_transaction();
4597 }
4598
4599 if !self.silent_replace_text {
4600 M::on_text_typed(self, &range, new_text, window, cx);
4601 }
4602 return;
4603 }
4604
4605 let old_text = self.text.clone();
4607 self.mode.adjust_auto_closed_pair(&range, new_text.len());
4608 self.text.replace(range.clone(), new_text);
4609
4610 let mut new_offset = (range.start + new_text.len()).min(self.text.len());
4611
4612 let mut mask_changed = false;
4615
4616 if self.is_single_line() {
4617 let pending_text = self.text.to_string();
4618 if !self.replaying_history
4624 && !self.validated_token_edit
4625 && !self.is_valid_input(&pending_text, cx)
4626 && self.is_valid_input(&old_text.to_string(), cx)
4627 {
4628 self.text = old_text;
4629 return;
4630 }
4631
4632 if !self.replaying_history && !self.mask_pattern.is_none() {
4633 let mask_text = self.mask_pattern.mask(&pending_text);
4634 mask_changed = mask_text.as_str() != pending_text;
4635 self.text = Rope::from(mask_text.as_str());
4636 let new_text_len =
4637 (new_text.len() + mask_text.len()).saturating_sub(pending_text.len());
4638 new_offset = (range.start + new_text_len).min(mask_text.len());
4639 }
4640 }
4641
4642 if mask_changed {
4643 M::reset_annotations(self);
4646 } else {
4647 M::adjust_annotations(self, &range, new_text.len());
4648 }
4649 if mask_changed {
4650 self.push_history(
4654 &old_text,
4655 &(0..old_text.len()),
4656 &self.text.to_string(),
4657 Some(EditIntent::Atomic),
4658 selection_before,
4659 Some((new_offset..new_offset).into()),
4660 );
4661 } else {
4662 self.push_history(
4663 &old_text,
4664 &range,
4665 &new_text,
4666 requested_intent,
4667 selection_before,
4668 None,
4669 );
4670 }
4671 if let Some(diagnostics) = self.mode.diagnostics_mut() {
4672 diagnostics.reset(&self.text)
4673 }
4674 self.display_map
4676 .adjust_folds_for_edit(&old_text, &range, new_text);
4677 self.display_map
4678 .on_text_changed(&self.text, &range, &Rope::from(new_text), cx);
4679
4680 self.mode.update_highlighter::<M>(
4681 super::mode::HighlighterUpdate {
4682 selected_range: &range,
4683 old_text: &old_text,
4684 new_text: &self.text,
4685 change_text: &new_text,
4686 force: true,
4687 },
4688 window,
4689 cx,
4690 );
4691
4692 self.update_fold_candidates_incremental(&range, new_text);
4693 M::refresh_language_features(self, window, cx);
4694 self.set_cursor_to(new_offset);
4695 self.ime_marked_range.take();
4696 if ends_composition {
4702 self.undo_manager
4703 .record_selections(vec![selection_before], vec![*self.active_selection()]);
4704 self.undo_manager.commit_transaction();
4705 }
4706 self.update_preferred_column();
4707 self.update_search(cx);
4708 self.dismiss_touch_selection(cx);
4709 if self.is_multi_line() {
4710 self.mode.update_auto_grow(&self.display_map);
4711 }
4712 if !self.silent_replace_text {
4713 M::on_text_typed(self, &range, &new_text, window, cx);
4714 }
4715 if self.emit_events {
4716 cx.emit(InputEvent::Change);
4717 }
4718 cx.notify();
4719 }
4720
4721 fn replace_and_mark_text_in_range(
4723 &mut self,
4724 range_utf16: Option<Range<usize>>,
4725 new_text: &str,
4726 new_selected_range_utf16: Option<Range<usize>>,
4727 window: &mut Window,
4728 cx: &mut Context<Self>,
4729 ) {
4730 let requested_intent = self.undo_manager.take_pending_intent();
4731 if !self.is_editable() {
4732 return;
4733 }
4734 let selection_before = *self.active_selection();
4735
4736 let starts_composition = self.ime_marked_range.is_none();
4737 if starts_composition {
4738 self.undo_manager.break_transaction_coalescing();
4741 self.undo_manager.begin_transaction();
4742 }
4743
4744 self.selections.remove_all_but_active();
4746
4747 M::reset_language_features(self);
4748
4749 let new_text = if self.replaying_history {
4751 Cow::Borrowed(new_text)
4752 } else {
4753 self.normalize_input(new_text)
4754 };
4755 let new_text: &str = &new_text;
4756
4757 let range = range_utf16
4758 .as_ref()
4759 .map(|range_utf16| self.range_from_utf16(range_utf16))
4760 .or(self.ime_marked_range.map(|range| {
4761 let range = self.range_to_utf16(&(range.start..range.end));
4762 self.range_from_utf16(&range)
4763 }))
4764 .unwrap_or(self.selected_range());
4765 let range = self.normalize_token_range(range);
4766
4767 let auto_closed_pairs_before = self.mode.auto_closed_pairs().clone();
4768 let old_text = self.text.clone();
4769 self.mode.adjust_auto_closed_pair(&range, new_text.len());
4770 self.text.replace(range.clone(), new_text);
4771
4772 if self.is_single_line() {
4773 let pending_text = self.text.to_string();
4774 if !self.replaying_history
4776 && !self.validated_token_edit
4777 && !self.is_valid_input(&pending_text, cx)
4778 && self.is_valid_input(&old_text.to_string(), cx)
4779 {
4780 self.text = old_text;
4781 if starts_composition {
4782 self.undo_manager.commit_transaction();
4783 }
4784 return;
4785 }
4786 }
4787
4788 M::adjust_annotations(self, &range, new_text.len());
4789 if let Some(diagnostics) = self.mode.diagnostics_mut() {
4790 diagnostics.reset(&self.text)
4791 }
4792 self.display_map
4794 .adjust_folds_for_edit(&old_text, &range, new_text);
4795 self.display_map
4796 .on_text_changed(&self.text, &range, &Rope::from(new_text), cx);
4797
4798 self.mode.update_highlighter::<M>(
4799 super::mode::HighlighterUpdate {
4800 selected_range: &range,
4801 old_text: &old_text,
4802 new_text: &self.text,
4803 change_text: &new_text,
4804 force: true,
4805 },
4806 window,
4807 cx,
4808 );
4809
4810 self.update_fold_candidates_incremental(&range, new_text);
4811 M::refresh_language_features(self, window, cx);
4812 if new_text.is_empty() {
4813 self.set_cursor_to(range.start);
4815 self.ime_marked_range = None;
4816 } else {
4817 self.ime_marked_range = Some((range.start..range.start + new_text.len()).into());
4818 let new_range = new_selected_range_utf16
4819 .as_ref()
4820 .map(|range_utf16| {
4821 let new_text = Rope::from(new_text);
4822 range.start + new_text.offset_utf16_to_offset(range_utf16.start)
4823 ..range.start + new_text.offset_utf16_to_offset(range_utf16.end)
4824 })
4825 .unwrap_or_else(|| range.start + new_text.len()..range.start + new_text.len());
4826 self.set_selection(new_range.start, new_range.end);
4827 }
4828 if self.is_multi_line() {
4829 self.mode.update_auto_grow(&self.display_map);
4830 }
4831 if self.push_history(
4832 &old_text,
4833 &range,
4834 new_text,
4835 requested_intent,
4836 selection_before,
4837 Some(*self.active_selection()),
4838 ) {
4839 self.undo_manager
4840 .record_selections(vec![selection_before], vec![*self.active_selection()]);
4841 self.undo_manager.record_auto_closed_pairs(
4842 auto_closed_pairs_before,
4843 self.mode.auto_closed_pairs().clone(),
4844 );
4845 }
4846 if new_text.is_empty() {
4847 self.undo_manager.commit_transaction();
4848 }
4849 cx.notify();
4850 }
4851
4852 fn bounds_for_range(
4854 &mut self,
4855 range_utf16: Range<usize>,
4856 bounds: Bounds<Pixels>,
4857 _window: &mut Window,
4858 _cx: &mut Context<Self>,
4859 ) -> Option<Bounds<Pixels>> {
4860 let last_layout = self.last_layout.as_ref()?;
4861 let line_height = last_layout.line_height;
4862 let line_number_width = last_layout.line_number_width;
4863 let range = self.range_from_utf16(&range_utf16);
4864
4865 let mut start_origin = None;
4866 let mut end_origin = None;
4867 let line_number_origin = point(line_number_width, px(0.));
4868 let mut y_offset = last_layout.visible_top;
4869
4870 for (vi, line) in last_layout.lines.iter().enumerate() {
4871 if start_origin.is_some() && end_origin.is_some() {
4872 break;
4873 }
4874
4875 let index_offset = last_layout.visible_line_byte_offsets[vi];
4876
4877 if start_origin.is_none() {
4878 if let Some(p) = line.position_for_index(
4879 range.start.saturating_sub(index_offset),
4880 last_layout,
4881 false,
4882 ) {
4883 start_origin = Some(p + point(px(0.), y_offset));
4884 }
4885 }
4886
4887 if end_origin.is_none() {
4888 if let Some(p) = line.position_for_index(
4889 range.end.saturating_sub(index_offset),
4890 last_layout,
4891 false,
4892 ) {
4893 end_origin = Some(p + point(px(0.), y_offset));
4894 }
4895 }
4896
4897 y_offset += line.size(line_height).height;
4898 }
4899
4900 let start_origin = start_origin.or_else(|| {
4901 let offset = self.last_cursor.or(Some(self.cursor()))?;
4902 let (_, _, origin) = self.line_and_position_for_offset(offset);
4903 origin.map(|origin| origin - line_number_origin)
4904 })?;
4905 let mut end_origin = end_origin.unwrap_or(start_origin);
4906 end_origin.y = start_origin.y;
4908
4909 Some(Bounds::from_corners(
4910 bounds.origin + line_number_origin + start_origin,
4911 bounds.origin + line_number_origin + point(end_origin.x, end_origin.y + line_height),
4913 ))
4914 }
4915
4916 fn character_index_for_point(
4917 &mut self,
4918 point: gpui::Point<Pixels>,
4919 _window: &mut Window,
4920 _cx: &mut Context<Self>,
4921 ) -> Option<usize> {
4922 let last_layout = self.last_layout.as_ref()?;
4923 let line_point = self.last_bounds?.localize(&point)?;
4924
4925 for (vi, line) in last_layout.lines.iter().enumerate() {
4926 let offset = last_layout.visible_line_byte_offsets[vi];
4927 if let Some(utf8_index) = line.index_for_position(line_point, last_layout) {
4928 return Some(self.offset_to_utf16(offset + utf8_index));
4929 }
4930 }
4931
4932 None
4933 }
4934
4935 fn text_input_configuration(
4936 &mut self,
4937 _window: &mut Window,
4938 _cx: &mut Context<Self>,
4939 ) -> TextInputConfiguration {
4940 InputBaseState::text_input_configuration(self)
4941 }
4942}
4943
4944impl<M: InputModeKind> Focusable for InputBaseState<M> {
4945 fn focus_handle(&self, _cx: &App) -> FocusHandle {
4946 self.focus_handle.clone()
4947 }
4948}
4949
4950impl<M: InputModeKind> Render for InputBaseState<M> {
4951 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
4952 self.editor_style = self
4955 .projected_editor_style
4956 .resolved(&crate::Theme::global(cx).tokens);
4957 let entity = cx.entity();
4958 if self._pending_update {
4959 self.mode.update_highlighter::<M>(
4960 super::mode::HighlighterUpdate {
4961 selected_range: &(0..0),
4962 old_text: &self.text,
4963 new_text: &self.text,
4964 change_text: "",
4965 force: false,
4966 },
4967 window,
4968 cx,
4969 );
4970
4971 self.update_fold_candidates();
4972 M::refresh_language_features(self, window, cx);
4973 self._pending_update = false;
4974 }
4975
4976 let element = div()
4977 .id(EDITOR_ELEMENT_ID)
4978 .key_context(CONTEXT)
4979 .when_some(self.accessibility.as_ref(), |this, accessibility| {
4980 this.when_some(accessibility.role, |this, role| this.role(role))
4981 .when_some(accessibility.label.clone(), |this, label| {
4982 this.aria_label(label)
4983 })
4984 .when_some(accessibility.placeholder.clone(), |this, placeholder| {
4985 this.aria_placeholder(placeholder)
4986 })
4987 .when_some(accessibility.accessibility_id.clone(), |this, id| {
4988 this.accessibility_id(id)
4989 })
4990 .when(
4991 accessibility.value_exposed
4992 && !self.masked
4993 && (window.is_a11y_active() || cfg!(feature = "test-support")),
4994 |this| this.aria_value(self.text.to_string()),
4995 )
4996 .on_a11y_action(gpui::AccessibleAction::Focus, {
4997 let entity = entity.clone();
4998 move |_, window, cx| {
4999 entity.update(cx, |state, cx| {
5000 state.handle_accessibility_focus(window, cx)
5001 });
5002 }
5003 })
5004 .when(self.is_editable(), |this| {
5005 this.on_a11y_action(gpui::AccessibleAction::SetValue, {
5006 let entity = entity.clone();
5007 move |data, window, cx| {
5008 entity.update(cx, |state, cx| {
5009 state.handle_accessibility_set_value(data, window, cx)
5010 });
5011 }
5012 })
5013 })
5014 })
5015 .when(self.is_editable(), |this| {
5016 this.on_action(window.listener_for(&entity, InputBaseState::backspace))
5017 .on_action(window.listener_for(&entity, InputBaseState::delete))
5018 .on_action(
5019 window.listener_for(&entity, InputBaseState::delete_to_beginning_of_line),
5020 )
5021 .on_action(window.listener_for(&entity, InputBaseState::delete_to_end_of_line))
5022 .on_action(window.listener_for(&entity, InputBaseState::delete_previous_word))
5023 .on_action(window.listener_for(&entity, InputBaseState::delete_next_word))
5024 .on_action(window.listener_for(&entity, InputBaseState::enter))
5025 .on_action(window.listener_for(&entity, InputBaseState::escape))
5026 .on_action(window.listener_for(&entity, InputBaseState::paste))
5027 .on_action(window.listener_for(&entity, InputBaseState::cut))
5028 .on_action(window.listener_for(&entity, InputBaseState::undo))
5029 .on_action(window.listener_for(&entity, InputBaseState::redo))
5030 .when(self.is_multi_line(), |this| {
5031 this.on_action(window.listener_for(&entity, InputBaseState::indent_inline))
5032 .on_action(window.listener_for(&entity, InputBaseState::outdent_inline))
5033 .on_action(window.listener_for(&entity, InputBaseState::indent_block))
5034 .on_action(window.listener_for(&entity, InputBaseState::outdent_block))
5035 })
5036 })
5037 .on_action(window.listener_for(&entity, InputBaseState::left))
5038 .on_action(window.listener_for(&entity, InputBaseState::right))
5039 .on_action(window.listener_for(&entity, InputBaseState::select_left))
5040 .on_action(window.listener_for(&entity, InputBaseState::select_right))
5041 .when(self.is_multi_line(), |this| {
5042 this.on_action(window.listener_for(&entity, InputBaseState::up))
5043 .on_action(window.listener_for(&entity, InputBaseState::down))
5044 .on_action(window.listener_for(&entity, InputBaseState::select_up))
5045 .on_action(window.listener_for(&entity, InputBaseState::select_down))
5046 .on_action(window.listener_for(&entity, InputBaseState::page_up))
5047 .on_action(window.listener_for(&entity, InputBaseState::page_down))
5048 .on_action(window.listener_for(&entity, InputBaseState::add_cursor_above))
5049 .on_action(window.listener_for(&entity, InputBaseState::add_cursor_below))
5050 })
5051 .on_action(window.listener_for(&entity, InputBaseState::on_action_select_all))
5052 .on_action(window.listener_for(&entity, InputBaseState::select_to_start_of_line))
5053 .on_action(window.listener_for(&entity, InputBaseState::select_to_end_of_line))
5054 .on_action(window.listener_for(&entity, InputBaseState::select_to_previous_word))
5055 .on_action(window.listener_for(&entity, InputBaseState::select_to_next_word))
5056 .on_action(window.listener_for(&entity, InputBaseState::home))
5057 .on_action(window.listener_for(&entity, InputBaseState::end))
5058 .on_action(window.listener_for(&entity, InputBaseState::move_to_start))
5059 .on_action(window.listener_for(&entity, InputBaseState::move_to_end))
5060 .on_action(window.listener_for(&entity, InputBaseState::move_to_previous_word))
5061 .on_action(window.listener_for(&entity, InputBaseState::move_to_next_word))
5062 .on_action(window.listener_for(&entity, InputBaseState::select_to_start))
5063 .on_action(window.listener_for(&entity, InputBaseState::select_to_end))
5064 .on_action(window.listener_for(&entity, InputBaseState::show_character_palette))
5065 .on_action({
5066 let entity = entity.clone();
5067 move |_: &ActivateToken, window, cx| {
5068 let state = entity.read(cx);
5069 let activation = state
5070 .token_spans()
5071 .iter()
5072 .find(|span| span.range() == state.selected_range())
5073 .and_then(|span| {
5074 state.range_to_bounds(&span.range()).and_then(|bounds| {
5075 state.token_activation(
5076 span.range().start,
5077 bounds,
5078 gpui::ClickEvent::Keyboard(gpui::KeyboardClickEvent {
5079 bounds,
5080 ..Default::default()
5081 }),
5082 )
5083 })
5084 });
5085 if let Some((listener, event)) = activation {
5086 listener(&event, window, cx);
5087 }
5088 }
5089 })
5090 .on_action(window.listener_for(&entity, InputBaseState::copy))
5091 .on_action(window.listener_for(&entity, InputBaseState::on_action_search))
5092 .on_action(window.listener_for(&entity, InputBaseState::on_action_replace))
5093 .on_mouse_down(
5094 MouseButton::Left,
5095 window.listener_for(&entity, InputBaseState::on_mouse_down),
5096 )
5097 .on_mouse_down(
5098 MouseButton::Right,
5099 window.listener_for(&entity, InputBaseState::on_mouse_down),
5100 )
5101 .on_mouse_up(
5102 MouseButton::Left,
5103 window.listener_for(&entity, InputBaseState::on_mouse_up),
5104 )
5105 .on_mouse_up(
5106 MouseButton::Right,
5107 window.listener_for(&entity, InputBaseState::on_mouse_up),
5108 )
5109 .on_mouse_move(window.listener_for(&entity, InputBaseState::on_mouse_move))
5110 .on_scroll(window.listener_for(&entity, InputBaseState::on_scroll_wheel))
5111 .when(self.is_multi_line() && !self.disabled, |this| {
5112 this.on_modifiers_changed(cx.listener(|_, _, _, cx| cx.notify()))
5113 })
5114 .when(!self.disabled, |this| {
5115 if self.is_multi_line() && window.modifiers().alt {
5116 this.cursor_crosshair()
5117 } else {
5118 this.cursor_text()
5119 }
5120 })
5121 .flex_1()
5122 .map(|this| {
5128 if self.is_multi_line() {
5129 this.h_full()
5130 } else {
5131 this.min_h_full().flex().items_center()
5132 }
5133 })
5134 .flex_grow_1()
5135 .overflow_x_hidden()
5136 .when(self.is_multi_line(), |this| {
5137 this.pt(self.editor_paddings.top)
5138 .pr(self.editor_paddings.right)
5139 .pb(self.editor_paddings.bottom)
5140 .pl(self.editor_paddings.left)
5141 })
5142 .child(TextElement::new(entity.clone()).placeholder(self.placeholder.clone()))
5143 .when(self.shows_scrollbar(), |this| {
5144 this.child(EditorScrollbar::new(entity.clone()))
5145 });
5146
5147 M::register_actions(element, &entity, window)
5150 .test_support()
5151 .track_focus(&self.focus_handle)
5152 }
5153}
5154
5155#[cfg(test)]
5156mod tests {
5157 use super::*;
5158
5159 #[gpui::test]
5160 fn accessibility_actions_recheck_live_editability(cx: &mut TestAppContext) {
5161 cx.update(crate::init);
5162 let (state, cx) = cx.add_window_view(|window, cx| {
5163 crate::input::InputState::new(window, cx).default_value("initial")
5164 });
5165 let changes = Rc::new(Cell::new(0));
5166 let observed = changes.clone();
5167 let _subscription = cx.update(|_, cx| {
5168 cx.subscribe(&state, move |_, event: &InputEvent, _| {
5169 if matches!(event, InputEvent::Change) {
5170 observed.set(observed.get() + 1);
5171 }
5172 })
5173 });
5174 cx.update(|window, cx| {
5175 state.update(cx, |state, cx| {
5176 state.handle_accessibility_set_value(None, window, cx);
5177 state.handle_accessibility_focus(window, cx);
5178 assert!(state.focus_handle.is_focused(window));
5179 });
5180 window.draw(cx).clear(cx);
5181 });
5182 assert_eq!(state.read_with(cx, |state, _| state.value()), "initial");
5183 assert_eq!(changes.get(), 0);
5184
5185 let action = gpui::accesskit::ActionData::Value("updated🦀".into());
5186 cx.update(|window, cx| {
5187 state.update(cx, |state, cx| {
5188 state.handle_accessibility_set_value(Some(&action), window, cx)
5189 });
5190 });
5191 assert_eq!(state.read_with(cx, |state, _| state.value()), "updated🦀");
5192 assert_eq!(changes.get(), 1);
5193 for disabled in [false, true] {
5194 cx.update(|window, cx| {
5195 window.blur(cx);
5196 state.update(cx, |state, cx| {
5197 state.set_disabled(disabled, cx);
5198 state.set_readonly(!disabled, cx);
5199 state.handle_accessibility_focus(window, cx);
5200 assert_eq!(state.focus_handle.is_focused(window), !disabled);
5201 let rejected = gpui::accesskit::ActionData::Value("rejected".into());
5202 state.handle_accessibility_set_value(Some(&rejected), window, cx);
5203 });
5204 });
5205 assert_eq!(state.read_with(cx, |state, _| state.value()), "updated🦀");
5206 assert_eq!(changes.get(), 1);
5207 }
5208 cx.update(|window, cx| {
5209 state.update(cx, |state, cx| {
5210 state.set_disabled(false, cx);
5211 state.set_readonly(false, cx);
5212 state.handle_accessibility_focus(window, cx);
5213 });
5214 window.draw(cx).clear(cx);
5215 window.dispatch_action(Box::new(Undo), cx);
5216 });
5217 assert_eq!(state.read_with(cx, |state, _| state.value()), "initial");
5218 cx.update(|window, cx| {
5219 state.update(cx, |state, cx| {
5220 state.set_masked(true, window, cx);
5221 state.handle_accessibility_set_value(Some(&action), window, cx);
5222 });
5223 });
5224 assert_eq!(state.read_with(cx, |state, _| state.value()), "updated🦀");
5225 }
5226
5227 #[gpui::test]
5228 fn accessibility_set_value_preserves_exact_editor_text(cx: &mut TestAppContext) {
5229 cx.update(crate::init);
5230 let (editor, cx) = cx.add_window_view(|window, cx| {
5231 crate::input::EditorState::new(window, cx).language("rust")
5232 });
5233 let action = gpui::accesskit::ActionData::Value("(".into());
5234 cx.update(|window, cx| {
5235 editor.update(cx, |editor, cx| {
5236 editor.handle_accessibility_set_value(Some(&action), window, cx)
5237 });
5238 });
5239 assert_eq!(editor.read_with(cx, |editor, _| editor.value()), "(");
5240 }
5241
5242 #[gpui::test]
5243 fn revealing_a_password_keeps_text_assistance_disabled(cx: &mut TestAppContext) {
5244 cx.update(crate::init);
5245 cx.open_window(size(px(400.), px(100.)), |window, cx| {
5246 let state = cx.new(|cx| crate::input::InputState::new(window, cx).masked(true));
5247 let hidden = state.read(cx).text_input_configuration();
5248 assert!(!hidden.autocorrect && !hidden.suggestions);
5249 assert_eq!(hidden.autocapitalize, Autocapitalize::None);
5250
5251 state.update(cx, |state, cx| state.toggle_masked(window, cx));
5253 assert_eq!(state.read(cx).text_input_configuration(), hidden);
5254 state.update(cx, |state, cx| state.toggle_masked(window, cx));
5255 assert_eq!(state.read(cx).text_input_configuration(), hidden);
5256 gpui::EmptyView
5257 });
5258 }
5259
5260 #[gpui::test]
5261 fn text_input_configuration_follows_the_input(cx: &mut TestAppContext) {
5262 use crate::input::{InputState, TextareaState};
5263
5264 cx.update(crate::init);
5265 let mut states = None;
5266 cx.open_window(size(px(400.), px(100.)), |window, cx| {
5267 states = Some((
5268 cx.new(|cx| InputState::new(window, cx)),
5269 cx.new(|cx| InputState::new(window, cx).masked(true)),
5270 cx.new(|cx| TextareaState::new(window, cx)),
5271 cx.new(|cx| {
5272 InputState::new(window, cx)
5273 .input_action(TextInputAction::Search)
5274 .autocorrect(true)
5275 .autocapitalize(Autocapitalize::Words)
5276 }),
5277 ));
5278 gpui::EmptyView
5279 });
5280 let (plain, masked, textarea, search) = states.unwrap();
5281
5282 cx.read(|cx| {
5283 assert_eq!(
5284 plain.read(cx).text_input_configuration(),
5285 TextInputConfiguration {
5286 input_action: TextInputAction::Done,
5287 ..Default::default()
5288 }
5289 );
5290 assert_eq!(
5291 masked.read(cx).text_input_configuration(),
5292 TextInputConfiguration {
5293 input_action: TextInputAction::Done,
5294 ..Default::default()
5295 }
5296 );
5297 assert_eq!(
5298 textarea.read(cx).text_input_configuration(),
5299 TextInputConfiguration {
5300 input_action: TextInputAction::Enter,
5301 ..Default::default()
5302 }
5303 );
5304 let search = search.read(cx).text_input_configuration();
5305 assert_eq!(search.input_action, TextInputAction::Search);
5306 assert_eq!(search.autocapitalize, Autocapitalize::Words);
5307 assert!(search.autocorrect);
5308 assert!(search.suggestions);
5309 });
5310 }
5311
5312 #[gpui::test]
5313 fn test_multicursor_unicode_completion_uses_post_edit_start(cx: &mut TestAppContext) {
5314 use crate::input::{CompletionProvider, EditorState};
5315 use gpui::Task;
5316 use lsp_types::{CompletionContext, CompletionResponse};
5317 use std::cell::RefCell;
5318
5319 #[derive(Default)]
5320 struct Provider {
5321 starts: RefCell<Vec<usize>>,
5322 queries: RefCell<Vec<(usize, String)>>,
5323 }
5324
5325 impl CompletionProvider for Provider {
5326 fn is_completion_trigger(&self, offset: usize, _: &str, _: &mut App) -> bool {
5327 self.starts.borrow_mut().push(offset);
5328 true
5329 }
5330
5331 fn completions(
5332 &self,
5333 _: &Rope,
5334 offset: usize,
5335 trigger: CompletionContext,
5336 _: &mut Window,
5337 _: &mut App,
5338 ) -> Task<anyhow::Result<CompletionResponse>> {
5339 self.queries
5340 .borrow_mut()
5341 .push((offset, trigger.trigger_character.unwrap()));
5342 Task::ready(Ok(CompletionResponse::Array(vec![])))
5343 }
5344 }
5345
5346 cx.update(crate::init);
5347 let mut editor = None;
5348 let window = cx.open_window(size(px(400.), px(100.)), |window, cx| {
5349 editor = Some(cx.new(|cx| EditorState::new(window, cx).default_value("aa")));
5350 gpui::EmptyView
5351 });
5352 let editor = editor.unwrap();
5353 let provider = Rc::new(Provider::default());
5354 window
5355 .update(cx, |_, window, cx| {
5356 editor.update(cx, |state, cx| {
5357 state.extras.lsp.completion_provider = Some(provider.clone());
5358 state
5359 .extras
5360 .context_menu_content
5361 .completion
5362 .trigger_start_offset = Some(0);
5363 state.extras.context_menu_content.completion.query = "aa".into();
5364 state.set_cursor_to(1);
5365 state.add_cursor_at(0, cx);
5366
5367 state.replace_text_in_range(None, "中", window, cx);
5368
5369 assert_eq!(state.value(), "中a中a");
5370 assert_eq!(provider.starts.borrow().as_slice(), &[4]);
5371 assert_eq!(provider.queries.borrow().as_slice(), &[(7, "中".into())]);
5372 });
5373 })
5374 .unwrap();
5375 }
5376
5377 #[gpui::test]
5378 fn test_inline_token_wrap_and_size_refresh(cx: &mut TestAppContext) {
5379 use crate::input::{InlineToken, InlineTokenPresentation};
5380 let width = Rc::new(Cell::new(4000.));
5381 let render_width = width.clone();
5382 let view = InputView::build_textarea(cx, |state| state.rows(4).default_value("@a@b"));
5383 view.window_handle
5384 .update(cx, |_, window, cx| {
5385 view.input.update(cx, |state, cx| {
5386 state
5387 .replace_range_with_token(0..2, InlineToken::new("a", "@a"), window, cx)
5388 .unwrap();
5389 state
5390 .replace_range_with_token(2..4, InlineToken::new("b", "@b"), window, cx)
5391 .unwrap();
5392 state.set_token_presentation(
5393 InlineTokenPresentation::default()
5394 .token(move |_, _, _| div().w(px(render_width.get()))),
5395 );
5396 });
5397 })
5398 .unwrap();
5399 let mut visual = VisualTestContext::from_window(view.window_handle.into(), cx);
5400 visual.update(|window, cx| window.draw(cx).clear(cx));
5401 view.input.read_with(&visual, |state, _| {
5402 assert_eq!(state.display_map.wrap_row_count(), 2);
5403 let first = state.range_to_bounds(&(0..2)).unwrap();
5404 let second = state.range_to_bounds(&(2..4)).unwrap();
5405 assert!(second.origin.y > first.origin.y);
5406 assert_eq!(
5407 state.last_layout.as_ref().unwrap().lines[0].wrapped_lines[0].len,
5408 2
5409 );
5410 });
5411 width.set(100.);
5412 view.input.update(&mut visual, |_, cx| cx.notify());
5413 visual.update(|window, cx| window.draw(cx).clear(cx));
5414 view.input.read_with(&visual, |state, _| {
5415 assert_eq!(state.display_map.wrap_row_count(), 1)
5416 });
5417 }
5418
5419 #[gpui::test]
5420 fn test_inline_token_geometry_and_reentrant_activation(cx: &mut TestAppContext) {
5421 use crate::input::{InlineToken, InlineTokenPresentation};
5422 let view = InputView::build(cx, |state| state.default_value("before @alice after"));
5423 let target = view.input.clone();
5424 view.window_handle
5425 .update(cx, |_, window, cx| {
5426 view.input.update(cx, |state, cx| {
5427 state
5428 .replace_range_with_token(
5429 7..13,
5430 InlineToken::new("a", "@alice"),
5431 window,
5432 cx,
5433 )
5434 .unwrap();
5435 state.set_token_presentation(
5436 InlineTokenPresentation::default()
5437 .token(|_, _, _| div().w(px(100.)).h(px(20.)))
5438 .on_token_click(move |_, window, cx| {
5439 target.update(cx, |state, cx| {
5440 state.set_value("activated", window, cx)
5441 })
5442 }),
5443 );
5444 state.focus(window, cx);
5445 state.set_selected_range(7..13, cx);
5446 });
5447 })
5448 .unwrap();
5449 let mut visual = VisualTestContext::from_window(view.window_handle.into(), cx);
5450 visual.update(|window, cx| window.draw(cx).clear(cx));
5451 visual.update(|window, cx| {
5452 let state = view.input.read(cx);
5453 let bounds = state.range_to_bounds(&(7..13)).unwrap();
5454 assert!((bounds.size.width - px(100.)).abs() < px(1.), "{bounds:?}");
5455 let (left, _) = state.index_for_mouse_position(bounds.origin + point(px(10.), px(5.)));
5456 let (right, _) = state.index_for_mouse_position(bounds.origin + point(px(90.), px(5.)));
5457 assert_eq!((left, right), (7, 13));
5458 window.dispatch_action(Box::new(ActivateToken), cx);
5459 });
5460 visual.run_until_parked();
5461 view.input.read_with(&visual, |state, _| {
5462 assert_eq!(state.value().as_ref(), "activated")
5463 });
5464 }
5465
5466 #[gpui::test]
5467 fn test_inline_token_click_selects_it(cx: &mut TestAppContext) {
5468 use crate::input::InlineToken;
5469 cx.update(crate::init);
5470 let view = InputView::build(cx, |state| state.default_value("before @alice after"));
5471 view.window_handle
5472 .update(cx, |_, window, cx| {
5473 view.input.update(cx, |state, cx| {
5474 state
5475 .replace_range_with_token(
5476 7..13,
5477 InlineToken::new("a", "@alice"),
5478 window,
5479 cx,
5480 )
5481 .unwrap();
5482 state.set_selected_range(0..0, cx);
5483 });
5484 })
5485 .unwrap();
5486 let mut visual = VisualTestContext::from_window(view.window_handle.into(), cx);
5487 visual.update(|window, cx| window.draw(cx).clear(cx));
5488 let bounds = view
5489 .input
5490 .read_with(&visual, |state, _| state.range_to_bounds(&(7..13)).unwrap());
5491 visual.simulate_click(bounds.center(), gpui::Modifiers::default());
5492 visual.run_until_parked();
5493 view.input.read_with(&visual, |state, _| {
5494 assert_eq!(state.selected_range(), 7..13, "a click selects the token");
5495 });
5496 }
5497
5498 #[gpui::test]
5499 fn test_inline_token_hover_reports_presence_without_selecting(cx: &mut TestAppContext) {
5500 use crate::input::{InlineToken, InlineTokenPresentation};
5501 use gpui::{Bounds, point, px, size};
5502 cx.update(crate::init);
5503 let view = InputView::build(cx, |state| state.default_value("before @alice after"));
5504 let bounds = Bounds::new(point(px(10.), px(20.)), size(px(100.), px(20.)));
5505 view.window_handle
5506 .update(cx, |_, window, cx| {
5507 view.input.update(cx, |state, cx| {
5508 state
5509 .replace_range_with_token(
5510 7..13,
5511 InlineToken::new("a", "@alice"),
5512 window,
5513 cx,
5514 )
5515 .unwrap();
5516 state.set_token_presentation(
5517 InlineTokenPresentation::default().on_token_hover(|_, _, _| {}),
5518 );
5519 state.set_selected_range(0..0, cx);
5520 });
5521 })
5522 .unwrap();
5523 view.window_handle
5524 .update(cx, |_, _, cx| {
5525 view.input.update(cx, |state, _| {
5526 let (_, entered) = state
5527 .token_hover(7, bounds, true, None)
5528 .expect("hover enters an enabled token");
5529 assert!(entered.is_hovered());
5530 assert_eq!(entered.token().id().as_ref(), "a");
5531 assert_eq!(entered.range(), 7..13);
5532 assert_eq!(entered.bounds(), bounds);
5533 let (_, left) = state
5534 .token_hover(7, bounds, false, None)
5535 .expect("hover leaves the same token");
5536 assert!(!left.is_hovered());
5537 assert_eq!(state.selected_range(), 0..0, "hover never selects");
5538 assert!(state.token_hover(99, bounds, true, None).is_none());
5539 });
5540 })
5541 .unwrap();
5542
5543 view.window_handle
5545 .update(cx, |_, _, cx| {
5546 view.input.update(cx, |state, cx| {
5547 state.set_disabled(true, cx);
5548 });
5549 })
5550 .unwrap();
5551 view.window_handle
5552 .update(cx, |_, _, cx| {
5553 view.input.update(cx, |state, _| {
5554 assert!(state.token_hover(7, bounds, true, None).is_none())
5555 })
5556 })
5557 .unwrap();
5558 view.window_handle
5559 .update(cx, |_, _, cx| {
5560 view.input.update(cx, |state, cx| {
5561 state.set_disabled(false, cx);
5562 state.set_readonly(true, cx);
5563 });
5564 })
5565 .unwrap();
5566 view.window_handle
5567 .update(cx, |_, _, cx| {
5568 view.input.update(cx, |state, _| {
5569 assert!(state.token_hover(7, bounds, true, None).is_some())
5570 })
5571 })
5572 .unwrap();
5573
5574 view.window_handle
5576 .update(cx, |_, _, cx| {
5577 view.input.update(cx, |state, _| {
5578 state.set_token_presentation(InlineTokenPresentation::default());
5579 });
5580 })
5581 .unwrap();
5582 view.window_handle
5583 .update(cx, |_, _, cx| {
5584 view.input.update(cx, |state, _| {
5585 assert!(state.token_hover(7, bounds, true, None).is_none())
5586 })
5587 })
5588 .unwrap();
5589 }
5590
5591 #[gpui::test]
5592 fn test_inline_token_hover_exit_delivered_when_token_removed(cx: &mut TestAppContext) {
5593 use crate::input::{InlineToken, InlineTokenPresentation};
5594 use std::cell::RefCell;
5595 use std::rc::Rc;
5596 cx.update(crate::init);
5597 let view = InputView::build(cx, |state| state.default_value("before @alice after"));
5598 let events: Rc<RefCell<Vec<(SharedString, bool)>>> = Rc::new(RefCell::new(Vec::new()));
5599 let sink = events.clone();
5600 view.window_handle
5601 .update(cx, |_, window, cx| {
5602 view.input.update(cx, |state, cx| {
5603 state
5604 .replace_range_with_token(
5605 7..13,
5606 InlineToken::new("a", "@alice"),
5607 window,
5608 cx,
5609 )
5610 .unwrap();
5611 state.set_token_presentation(
5612 InlineTokenPresentation::default()
5613 .token(|_, _, _| div().w(px(100.)).h(px(20.)))
5614 .on_token_hover(move |event, _, _| {
5615 sink.borrow_mut()
5616 .push((event.token().id().clone(), event.is_hovered()));
5617 }),
5618 );
5619 });
5620 })
5621 .unwrap();
5622 let mut visual = VisualTestContext::from_window(view.window_handle.into(), cx);
5623 visual.update(|window, cx| window.draw(cx).clear(cx));
5624 let known_bounds = view.input.read_with(&visual, |state, _| {
5625 state
5626 .token_bounds
5627 .get(&7)
5628 .copied()
5629 .expect("placed token bounds")
5630 });
5631 visual.simulate_mouse_move(known_bounds.center(), None, gpui::Modifiers::default());
5632 visual.run_until_parked();
5633 visual.update(|window, cx| window.draw(cx).clear(cx));
5634 assert_eq!(events.borrow().as_slice(), &[("a".into(), true)]);
5635
5636 view.window_handle
5640 .update(cx, |_, window, cx| {
5641 view.input
5642 .update(cx, |state, cx| state.set_value("no tokens", window, cx));
5643 })
5644 .unwrap();
5645 visual.run_until_parked();
5646 visual.update(|window, cx| window.draw(cx).clear(cx));
5647 assert_eq!(
5648 events.borrow().as_slice(),
5649 &[("a".into(), true), ("a".into(), false)]
5650 );
5651 }
5652
5653 #[gpui::test]
5654 fn test_inline_token_hover_bounds_stay_single_row_at_wrap_boundary(cx: &mut TestAppContext) {
5655 use crate::input::{InlineToken, InlineTokenPresentation};
5656 cx.update(crate::init);
5657 let view = InputView::build_textarea(cx, |state| {
5658 state
5659 .rows(4)
5660 .default_value("aaaa bbbb cccc dddd @alice eeee ffff")
5661 });
5662 view.window_handle
5663 .update(cx, |_, window, cx| {
5664 view.input.update(cx, |state, cx| {
5665 state
5666 .replace_range_with_token(
5667 20..26,
5668 InlineToken::new("a", "@alice"),
5669 window,
5670 cx,
5671 )
5672 .unwrap();
5673 state.set_token_presentation(
5674 InlineTokenPresentation::default()
5675 .token(|_, _, _| div().w(px(60.)).h(px(20.))),
5676 );
5677 });
5678 })
5679 .unwrap();
5680 let mut visual = VisualTestContext::from_window(view.window_handle.into(), cx);
5681 visual.update(|window, cx| window.draw(cx).clear(cx));
5682 let (placed, ranged, line_height) = view.input.read_with(&visual, |state, _| {
5683 (
5684 state
5685 .token_bounds
5686 .get(&20)
5687 .copied()
5688 .expect("placed token bounds"),
5689 state.range_to_bounds(&(20..26)),
5690 state.last_layout.as_ref().unwrap().line_height,
5691 )
5692 });
5693 assert!(placed.size.height <= line_height + px(1.), "{placed:?}");
5697 assert!(
5698 placed.size.width <= px(61.) && placed.size.width > px(0.),
5699 "{placed:?}"
5700 );
5701 if let Some(ranged) = ranged {
5702 assert!(
5703 ranged.size.height >= placed.size.height - px(1.),
5704 "{ranged:?} vs {placed:?}"
5705 );
5706 }
5707 }
5708
5709 #[gpui::test]
5710 fn test_inline_token_hover_same_offset_replacement_exits_predecessor(cx: &mut TestAppContext) {
5711 use crate::input::{InlineToken, InlineTokenPresentation};
5712 use std::{cell::RefCell, rc::Rc};
5713 cx.update(crate::init);
5714 let view = InputView::build(cx, |state| state.default_value("@alice!"));
5715 let events = Rc::new(RefCell::new(Vec::new()));
5716 let sink = events.clone();
5717 view.window_handle
5718 .update(cx, |_, window, cx| {
5719 view.input.update(cx, |state, cx| {
5720 state
5721 .replace_range_with_token(0..6, InlineToken::new("a", "@alice"), window, cx)
5722 .unwrap();
5723 state.set_token_presentation(
5724 InlineTokenPresentation::default()
5725 .token(|_, _, _| div().w(px(100.)).h(px(20.)))
5726 .on_token_hover(move |event, _, _| {
5727 sink.borrow_mut()
5728 .push((event.token().id().clone(), event.is_hovered()));
5729 }),
5730 );
5731 });
5732 })
5733 .unwrap();
5734 let mut visual = VisualTestContext::from_window(view.window_handle.into(), cx);
5735 visual.update(|window, cx| window.draw(cx).clear(cx));
5736 let bounds = view
5737 .input
5738 .read_with(&visual, |state, _| state.token_bounds[&0]);
5739 visual.simulate_mouse_move(bounds.center(), None, gpui::Modifiers::default());
5740 visual.run_until_parked();
5741 visual.update(|window, cx| window.draw(cx).clear(cx));
5742 assert_eq!(events.borrow().as_slice(), &[("a".into(), true)]);
5743
5744 view.window_handle
5745 .update(cx, |_, window, cx| {
5746 view.input.update(cx, |state, cx| {
5747 state
5748 .replace_range_with_token(0..6, InlineToken::new("b", "@alice"), window, cx)
5749 .unwrap();
5750 });
5751 })
5752 .unwrap();
5753 visual.run_until_parked();
5754 visual.update(|window, cx| window.draw(cx).clear(cx));
5755 visual.simulate_mouse_move(
5756 bounds.center() + gpui::point(px(1.), px(0.)),
5757 None,
5758 gpui::Modifiers::default(),
5759 );
5760 visual.run_until_parked();
5761 visual.update(|window, cx| window.draw(cx).clear(cx));
5762 assert_eq!(
5763 events.borrow().as_slice(),
5764 &[("a".into(), true), ("a".into(), false), ("b".into(), true)]
5765 );
5766 view.input
5769 .update(&mut visual, |state, cx| state.set_disabled(true, cx));
5770 visual.update(|window, cx| window.draw(cx).clear(cx));
5771 visual.run_until_parked();
5772 assert_eq!(events.borrow().last(), Some(&("b".into(), false)));
5773 view.input
5774 .update(&mut visual, |state, cx| state.set_disabled(false, cx));
5775 visual.update(|window, cx| window.draw(cx).clear(cx));
5776 visual.run_until_parked();
5777 visual.simulate_mouse_move(bounds.center(), None, gpui::Modifiers::default());
5778 visual.run_until_parked();
5779 assert_eq!(
5780 events.borrow().as_slice(),
5781 &[
5782 ("a".into(), true),
5783 ("a".into(), false),
5784 ("b".into(), true),
5785 ("b".into(), false),
5786 ("b".into(), true)
5787 ]
5788 );
5789 visual.simulate_mouse_move(
5790 bounds.bottom_right() + gpui::point(px(10.), px(10.)),
5791 None,
5792 gpui::Modifiers::default(),
5793 );
5794 visual.run_until_parked();
5795 assert_eq!(
5796 events.borrow().as_slice(),
5797 &[
5798 ("a".into(), true),
5799 ("a".into(), false),
5800 ("b".into(), true),
5801 ("b".into(), false),
5802 ("b".into(), true),
5803 ("b".into(), false)
5804 ]
5805 );
5806 }
5807
5808 #[gpui::test]
5809 fn test_inline_token_hover_older_exit_keeps_newer_snapshot(cx: &mut TestAppContext) {
5810 use crate::input::{InlineToken, InlineTokenPresentation};
5811 use gpui::{Bounds, point, px, size};
5812 cx.update(crate::init);
5813 let view = InputView::build(cx, |state| state.default_value("@a @b"));
5814 let bounds = Bounds::new(point(px(10.), px(20.)), size(px(100.), px(20.)));
5815 view.window_handle
5816 .update(cx, |_, window, cx| {
5817 view.input.update(cx, |state, cx| {
5818 state
5819 .replace_range_with_token(0..2, InlineToken::new("a", "@a"), window, cx)
5820 .unwrap();
5821 state
5822 .replace_range_with_token(3..5, InlineToken::new("b", "@b"), window, cx)
5823 .unwrap();
5824 state.set_token_presentation(
5825 InlineTokenPresentation::default().on_token_hover(|_, _, _| {}),
5826 );
5827 state
5830 .token_hover(0, bounds, true, None)
5831 .expect("hover enters A");
5832 state
5833 .token_hover(3, bounds, true, None)
5834 .expect("hover enters B");
5835 let (_, exit_a) = state.token_hover(0, bounds, false, None).expect("A exits");
5836 assert!(!exit_a.is_hovered());
5837 assert_eq!(
5839 state
5840 .hovered_token
5841 .as_ref()
5842 .expect("B still hovered")
5843 .span
5844 .token()
5845 .id()
5846 .as_ref(),
5847 "b"
5848 );
5849 state.set_value("no tokens", window, cx);
5851 state.token_bounds.clear();
5852 let (listener, exit_b) = state.reconcile_token_hover().expect("B must exit");
5853 assert!(!exit_b.is_hovered());
5854 assert_eq!(exit_b.token().id().as_ref(), "b");
5855 drop(listener);
5856 });
5857 })
5858 .unwrap();
5859 }
5860
5861 #[gpui::test]
5862 fn test_inline_token_hover_late_exit_matches_replaced_token(cx: &mut TestAppContext) {
5863 use crate::input::{InlineToken, InlineTokenPresentation};
5864 cx.update(crate::init);
5865 let view = InputView::build(cx, |state| state.default_value("@a"));
5866 view.window_handle
5867 .update(cx, |_, window, cx| {
5868 view.input.update(cx, |state, cx| {
5869 let a = InlineToken::new("a", "@a");
5870 let b = InlineToken::new("b", "@a");
5871 state
5872 .replace_range_with_token(0..2, a.clone(), window, cx)
5873 .unwrap();
5874 state.set_token_presentation(
5875 InlineTokenPresentation::default().on_token_hover(|_, _, _| {}),
5876 );
5877 let bounds = gpui::Bounds::default();
5878 state.token_hover(0, bounds, true, Some(&a)).unwrap();
5879 state
5880 .replace_range_with_token(0..2, b.clone(), window, cx)
5881 .unwrap();
5882 state.token_hover(0, bounds, true, Some(&b)).unwrap();
5883 let (_, exit_a) = state.token_hover(0, bounds, false, Some(&a)).unwrap();
5884 assert_eq!(exit_a.token().id().as_ref(), "a");
5885 assert_eq!(state.hovered_token.as_ref().unwrap().span.token(), &b);
5886 let (_, exit_b) = state.token_hover(0, bounds, false, Some(&b)).unwrap();
5887 assert_eq!(exit_b.token().id().as_ref(), "b");
5888 assert!(state.pending_token_hover_exits.is_empty());
5889 });
5890 })
5891 .unwrap();
5892 }
5893
5894 #[gpui::test]
5895 fn test_inline_token_hover_exit_keeps_entry_utf16_after_edit(cx: &mut TestAppContext) {
5896 use crate::input::{InlineToken, InlineTokenPresentation};
5897 use gpui::EntityInputHandler as _;
5898 cx.update(crate::init);
5899 let view = InputView::build(cx, |state| state.default_value("🙂 @a"));
5900 view.window_handle
5901 .update(cx, |_, window, cx| {
5902 view.input.update(cx, |state, cx| {
5903 state
5904 .replace_range_with_token(5..7, InlineToken::new("a", "@a"), window, cx)
5905 .unwrap();
5906 state.set_token_presentation(
5907 InlineTokenPresentation::default().on_token_hover(|_, _, _| {}),
5908 );
5909 let bounds = gpui::Bounds::default();
5910 let (_, enter) = state.token_hover(5, bounds, true, None).unwrap();
5911 assert_eq!(enter.range_utf16(), (3, 5));
5912 state.replace_text_in_range(Some(0..2), "abcd", window, cx);
5913 assert_eq!(state.token_spans()[0].range(), 5..7);
5914 let (_, exit) = state.token_hover(5, bounds, false, None).unwrap();
5915 assert_eq!(exit.range_utf16(), (3, 5));
5916 });
5917 })
5918 .unwrap();
5919 }
5920
5921 #[gpui::test]
5922 fn test_inline_token_hover_callback_edit_preserves_both_exits(cx: &mut TestAppContext) {
5923 use crate::input::{InlineToken, InlineTokenPresentation};
5924 cx.update(crate::init);
5925 let view = InputView::build(cx, |state| state.default_value("@a @b"));
5926 view.window_handle
5927 .update(cx, |_, window, cx| {
5928 view.input.update(cx, |state, cx| {
5929 state
5930 .replace_range_with_token(0..2, InlineToken::new("a", "@a"), window, cx)
5931 .unwrap();
5932 state
5933 .replace_range_with_token(3..5, InlineToken::new("b", "@b"), window, cx)
5934 .unwrap();
5935 state.set_token_presentation(
5936 InlineTokenPresentation::default().on_token_hover(|_, _, _| {}),
5937 );
5938 let bounds = gpui::Bounds::default();
5939 state.token_hover(0, bounds, true, None).unwrap();
5940 state.token_hover(3, bounds, true, None).unwrap();
5941 state.set_value("gone", window, cx);
5943 let (_, exit_a) = state.token_hover(0, bounds, false, None).unwrap();
5944 assert_eq!(exit_a.token().id().as_ref(), "a");
5945 let (_, exit_b) = state.reconcile_token_hover().unwrap();
5946 assert_eq!(exit_b.token().id().as_ref(), "b");
5947 assert!(state.token_hover(0, bounds, false, None).is_none());
5948 assert!(state.reconcile_token_hover().is_none());
5949 });
5950 })
5951 .unwrap();
5952 }
5953
5954 #[gpui::test]
5955 fn test_inline_token_edit_history_and_validation(cx: &mut TestAppContext) {
5956 let view = InputView::build(cx, |state| state.default_value("问 @alice!"));
5957 view.window_handle
5958 .update(cx, |_, window, cx| {
5959 view.input.update(cx, |state, cx| {
5960 let token =
5961 crate::input::InlineToken::new("alice-1", "@alice").with_label("Alice");
5962 state
5963 .replace_range_with_token(4..10, token.clone(), window, cx)
5964 .unwrap();
5965 assert_eq!(state.value().as_ref(), "问 @alice!");
5966 assert_eq!(state.tokens()[0].range(), 4..10);
5967 assert_eq!(state.next_end_of_word_at(4), 10);
5968 assert_eq!(state.previous_start_of_word_at(10), 4);
5969 state.undo(&Undo, window, cx);
5970 assert!(
5971 state.tokens().is_empty(),
5972 "identity-only association is undoable"
5973 );
5974 assert_eq!(state.value().as_ref(), "问 @alice!");
5975 state.redo(&Redo, window, cx);
5976 assert_eq!(state.tokens()[0].token(), &token);
5977 state.set_selected_range(6..7, cx);
5978 assert_eq!(state.selected_range(), 4..10);
5979 state.replace("", window, cx);
5980 assert_eq!(state.value().as_ref(), "问 !");
5981 assert!(state.tokens().is_empty());
5982 state.undo(&Undo, window, cx);
5983 assert_eq!(state.tokens()[0].token(), &token);
5984 state.set_selected_range(0..0, cx);
5985 state.replace("🙂", window, cx);
5986 assert_eq!(state.tokens()[0].range(), 8..14);
5987 state.undo(&Undo, window, cx);
5988 assert_eq!(state.tokens()[0].range(), 4..10);
5989 let before = state.content();
5990 state
5991 .replace_range_with_token(0..0, token.clone(), window, cx)
5992 .expect("the same reference may occur twice");
5993 assert_eq!(state.value().as_ref(), "@alice问 @alice!");
5994 assert_eq!(state.tokens().len(), 2);
5995 assert_eq!(state.tokens()[1].token(), &token);
5996 state.undo(&Undo, window, cx);
5997 assert_eq!(state.content(), before);
5998 state.set_value(before.text().clone(), window, cx);
5999 assert!(state.tokens().is_empty());
6000 assert!(!state.undo_manager.has_undos());
6001 });
6002 })
6003 .unwrap();
6004 }
6005
6006 #[gpui::test]
6007 fn test_inline_token_textarea_ime_and_modes(cx: &mut TestAppContext) {
6008 use crate::input::{InlineToken, InlineTokenError, InputContent};
6009 let view = InputView::build_textarea(cx, |state| state);
6010 view.window_handle
6011 .update(cx, |_, window, cx| {
6012 view.input.update(cx, |state, cx| {
6013 let content = InputContent::new("@a@b\n后面")
6014 .with_token(0..2, InlineToken::new("a", "@a"))
6015 .unwrap()
6016 .with_token(2..4, InlineToken::new("b", "@b"))
6017 .unwrap();
6018 state.set_value(content.clone(), window, cx);
6019 state.set_selected_range(2..2, cx);
6020 assert_eq!(state.previous_boundary(2), 0);
6021 assert_eq!(state.next_boundary(2), 4);
6022 state.replace_and_mark_text_in_range(Some(1..2), "中", Some(1..1), window, cx);
6023 assert_eq!(state.value().as_ref(), "中@b\n后面");
6024 assert_eq!(state.tokens().len(), 1);
6025 assert_eq!(
6026 state.replace_with_token(InlineToken::new("x", "x"), window, cx),
6027 Err(InlineTokenError::CompositionActive)
6028 );
6029 state.replace_text_in_range(None, "中文", window, cx);
6030 state.undo(&Undo, window, cx);
6031 assert_eq!(state.content(), content);
6032 state.redo(&Redo, window, cx);
6033 assert_eq!(state.value().as_ref(), "中文@b\n后面");
6034 state.undo(&Undo, window, cx);
6035 state.replace_all("plain", window, cx);
6036 assert!(state.tokens().is_empty());
6037 state.undo(&Undo, window, cx);
6038 assert_eq!(state.content(), content);
6039 });
6040 })
6041 .unwrap();
6042 let view = InputView::build(cx, |state| state);
6043 view.window_handle
6044 .update(cx, |_, window, cx| {
6045 view.input.update(cx, |state, cx| {
6046 assert_eq!(
6047 InputContent::new("a\u{301}")
6048 .with_token(0..1, InlineToken::new("bad", "a"))
6049 .err(),
6050 Some(InlineTokenError::InvalidBoundary)
6051 );
6052 assert_eq!(
6053 InputContent::new("@a@a")
6054 .with_token(0..2, InlineToken::new("a", "@a"))
6055 .unwrap()
6056 .with_token(1..3, InlineToken::new("x", "a@"))
6057 .err(),
6058 Some(InlineTokenError::OverlappingTokens)
6059 );
6060 state
6061 .replace_with_token(InlineToken::new("a", "@a"), window, cx)
6062 .unwrap();
6063 let before = state.content();
6064 state.set_masked(true, window, cx);
6065 assert!(!state.tokens_visible());
6066 assert_eq!(
6067 state.replace_with_token(InlineToken::new("b", "b"), window, cx),
6068 Err(InlineTokenError::UnsupportedMode)
6069 );
6070 assert_eq!(state.content(), before);
6071 state.undo(&Undo, window, cx);
6072 assert!(state.tokens().is_empty());
6073 state.redo(&Redo, window, cx);
6074 assert_eq!(state.content(), before);
6075 assert!(!state.tokens_visible());
6076 });
6077 })
6078 .unwrap();
6079 }
6080
6081 use crate::theme::Theme;
6082 use gpui::{TestAppContext, VisualTestContext, size};
6083
6084 use crate::input::{EditorMode, EditorState, InputMode, LanguageConfig, TabSize, TextareaMode};
6085
6086 fn set_test_syntax_provider(
6087 provider: Rc<dyn crate::input::SyntaxContextProvider>,
6088 cx: &mut App,
6089 ) {
6090 struct TestLanguages(Rc<dyn crate::input::SyntaxContextProvider>);
6091 impl crate::input::LanguageProvider for TestLanguages {
6092 fn syntax_context_provider(
6093 &self,
6094 _: &str,
6095 ) -> Option<Rc<dyn crate::input::SyntaxContextProvider>> {
6096 Some(self.0.clone())
6097 }
6098 }
6099 crate::input::set_language_provider(Rc::new(TestLanguages(provider)), cx);
6100 }
6101
6102 struct TestRoot<M: InputModeKind>(Entity<InputBaseState<M>>);
6103
6104 impl<M: InputModeKind> Render for TestRoot<M> {
6105 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
6106 div().size_full().child(self.0.clone())
6107 }
6108 }
6109
6110 struct InputView<M: InputModeKind> {
6111 input: Entity<InputBaseState<M>>,
6112 window_handle: gpui::WindowHandle<TestRoot<M>>,
6113 }
6114
6115 impl<M: InputModeKind> InputView<M> {
6117 fn build_with(
6118 cx: &mut TestAppContext,
6119 make: impl FnOnce(&mut Window, &mut Context<InputBaseState<M>>) -> InputBaseState<M>
6120 + 'static,
6121 ) -> Self {
6122 let mut input: Option<Entity<InputBaseState<M>>> = None;
6123
6124 let window = cx.update(|cx| {
6125 cx.open_window(Default::default(), |window, cx| {
6126 cx.set_global(Theme::default());
6128 super::super::init(cx);
6130
6131 input = Some(cx.new(|cx| make(window, cx)));
6132
6133 cx.new(|_| TestRoot(input.clone().unwrap()))
6134 })
6135 .unwrap()
6136 });
6137
6138 Self {
6139 input: input.clone().unwrap(),
6140 window_handle: window,
6141 }
6142 }
6143 }
6144
6145 impl InputView<EditorMode> {
6146 fn new(cx: &mut TestAppContext) -> Self {
6148 Self::build_editor(cx, |state| state)
6149 }
6150
6151 fn build_editor(
6152 cx: &mut TestAppContext,
6153 f: impl FnOnce(InputBaseState<EditorMode>) -> InputBaseState<EditorMode> + 'static,
6154 ) -> Self {
6155 Self::build_with(cx, move |window, cx| {
6156 f(crate::input::EditorState::new(window, cx).language("sql"))
6157 })
6158 }
6159 }
6160
6161 impl InputView<TextareaMode> {
6162 fn build_textarea(
6163 cx: &mut TestAppContext,
6164 f: impl FnOnce(InputBaseState<TextareaMode>) -> InputBaseState<TextareaMode> + 'static,
6165 ) -> Self {
6166 Self::build_with(cx, move |window, cx| {
6167 f(crate::input::TextareaState::new(window, cx))
6168 })
6169 }
6170 }
6171
6172 impl InputView<InputMode> {
6173 fn build(
6175 cx: &mut TestAppContext,
6176 f: impl FnOnce(InputBaseState<InputMode>) -> InputBaseState<InputMode> + 'static,
6177 ) -> Self {
6178 Self::build_with(cx, move |window, cx| {
6179 f(crate::input::InputState::new(window, cx))
6180 })
6181 }
6182 }
6183
6184 fn swipe<M: InputModeKind>(
6186 view: &InputView<M>,
6187 cx: &mut VisualTestContext,
6188 dy: f32,
6189 phase: gpui::TouchPhase,
6190 ) {
6191 cx.update(|window, cx| {
6192 view.input.update(cx, |state, cx| {
6193 state.on_scroll_wheel(
6194 &ScrollEvent {
6195 delta: gpui::ScrollDelta::Pixels(point(px(0.), px(dy))),
6196 touch_phase: phase,
6197 ..Default::default()
6198 },
6199 window,
6200 cx,
6201 )
6202 })
6203 });
6204 }
6205
6206 fn long_text() -> String {
6207 (0..60)
6208 .map(|n| format!("line {n}"))
6209 .collect::<Vec<_>>()
6210 .join("\n")
6211 }
6212
6213 #[gpui::test]
6214 fn test_scroll_bounce_stretches_past_the_end_and_springs_back(cx: &mut TestAppContext) {
6215 use gpui::TouchPhase::{Ended, Moved, Started};
6216
6217 let view = InputView::build_textarea(cx, |state| {
6218 state.scroll_bounce(true).default_value(long_text())
6219 });
6220 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
6221 cx.update(|window, cx| window.draw(cx).clear(cx));
6222 let drawn_at = |cx: &mut VisualTestContext| {
6223 view.input
6224 .read_with(cx, |state, _| state.last_bounds.unwrap().origin.y)
6225 };
6226
6227 swipe(&view, &mut cx, -20., Started);
6228 swipe(&view, &mut cx, -100_000., Moved);
6229 let (end, stretch) = view.input.read_with(&cx, |state, _| {
6230 (state.scroll_handle.offset().y, state.overscroll.offset())
6231 });
6232 assert!(end < px(0.), "the text scrolled to its end");
6233 assert!(stretch < 0., "the rest stretched the edge: {stretch}");
6234 cx.update(|window, cx| window.draw(cx).clear(cx));
6235 let stretched_at = drawn_at(&mut cx);
6236
6237 swipe(&view, &mut cx, 0., Ended);
6238 for _ in 0..120 {
6239 cx.executor()
6240 .advance_clock(std::time::Duration::from_millis(16));
6241 cx.update(|window, cx| window.draw(cx).clear(cx));
6242 }
6243 view.input.read_with(&cx, |state, _| {
6244 assert_eq!(state.overscroll.offset(), 0., "the edge sprang back");
6245 assert_eq!(
6246 state.scroll_handle.offset().y,
6247 end,
6248 "scrolling stayed exact"
6249 );
6250 });
6251 let moved = (stretched_at - drawn_at(&mut cx)).as_f32();
6252 assert!(
6253 (moved - stretch).abs() < 0.001,
6254 "the stretch only moved where the text was drawn: {moved} vs {stretch}"
6255 );
6256 }
6257
6258 fn swipe_past_the_end(
6261 view: &InputView<TextareaMode>,
6262 cx: &mut VisualTestContext,
6263 ) -> (Pixels, f32) {
6264 use gpui::TouchPhase::{Moved, Started};
6265
6266 cx.update(|window, cx| window.draw(cx).clear(cx));
6267 swipe(view, cx, -20., Started);
6268 swipe(view, cx, -100_000., Moved);
6269 view.input.read_with(cx, |state, _| {
6270 (state.scroll_handle.offset().y, state.overscroll.offset())
6271 })
6272 }
6273
6274 #[gpui::test]
6275 fn test_scroll_bounce_stays_put_under_reduced_motion(cx: &mut TestAppContext) {
6276 cx.update(|cx| cx.set_reduce_motion(true));
6277 let view = InputView::build_textarea(cx, |state| {
6278 state.scroll_bounce(true).default_value(long_text())
6279 });
6280 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
6281 let (end, stretch) = swipe_past_the_end(&view, &mut cx);
6282 assert!(end < px(0.), "the text still scrolls");
6283 assert_eq!(stretch, 0., "the edge does not stretch");
6284 }
6285
6286 #[gpui::test]
6287 fn test_scroll_bounce_follows_the_platform_by_default(cx: &mut TestAppContext) {
6288 let view = InputView::build_textarea(cx, |state| state.default_value(long_text()));
6289 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
6290 let (end, stretch) = swipe_past_the_end(&view, &mut cx);
6291 assert!(end < px(0.), "the text scrolls");
6292 assert_eq!(
6293 stretch != 0.,
6294 crate::ScrollBounce::enabled_by_default(),
6295 "the edge stretches where the platform's scroll views do"
6296 );
6297 }
6298
6299 #[gpui::test]
6300 fn test_scroll_bounce_leaves_text_that_fits(cx: &mut TestAppContext) {
6301 let view = InputView::build_textarea(cx, |state| {
6302 state.scroll_bounce(true).default_value("one line")
6303 });
6304 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
6305 let (end, stretch) = swipe_past_the_end(&view, &mut cx);
6306 assert_eq!((end, stretch), (px(0.), 0.));
6307 }
6308
6309 #[gpui::test]
6310 fn test_scroll_bounce_resets_when_replacement_text_fits(cx: &mut TestAppContext) {
6311 let view = InputView::build_textarea(cx, |state| {
6312 state.scroll_bounce(true).default_value(long_text())
6313 });
6314 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
6315 let (_, stretch) = swipe_past_the_end(&view, &mut cx);
6316 assert!(stretch < 0.);
6317 cx.update(|window, cx| {
6318 view.input
6319 .update(cx, |state, cx| state.set_value("short", window, cx));
6320 window.draw(cx).clear(cx);
6321 });
6322 swipe(&view, &mut cx, 0., gpui::TouchPhase::Ended);
6323 view.input.read_with(&cx, |state, _| {
6324 assert_eq!(state.overscroll.offset(), 0.);
6325 assert_eq!(state.scroll_handle.offset().y, px(0.));
6326 });
6327 }
6328
6329 #[gpui::test]
6330 fn test_scroll_bounce_releases_when_the_end_packet_is_horizontal(cx: &mut TestAppContext) {
6331 let view = InputView::build_textarea(cx, |state| {
6332 state.scroll_bounce(true).default_value(long_text())
6333 });
6334 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
6335 let (_, stretch) = swipe_past_the_end(&view, &mut cx);
6336 assert!(stretch < 0.);
6337 cx.update(|window, cx| {
6338 view.input.update(cx, |state, cx| {
6339 state.on_scroll_wheel(
6340 &ScrollEvent {
6341 delta: gpui::ScrollDelta::Pixels(point(px(1.), px(0.))),
6342 touch_phase: gpui::TouchPhase::Ended,
6343 ..Default::default()
6344 },
6345 window,
6346 cx,
6347 );
6348 });
6349 });
6350 for _ in 0..120 {
6351 cx.executor()
6352 .advance_clock(std::time::Duration::from_millis(16));
6353 cx.update(|window, cx| window.draw(cx).clear(cx));
6354 }
6355 assert_eq!(
6356 view.input
6357 .read_with(&cx, |state, _| state.overscroll.offset()),
6358 0.
6359 );
6360 }
6361
6362 #[gpui::test]
6363 fn test_scroll_bounce_ordinary_mouse_wheel_does_not_stretch(cx: &mut TestAppContext) {
6364 let view = InputView::build_textarea(cx, |state| {
6365 state.scroll_bounce(true).default_value(long_text())
6366 });
6367 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
6368 cx.update(|window, cx| {
6369 window.draw(cx).clear(cx);
6370 view.input.update(cx, |state, cx| {
6371 state.on_scroll_wheel(
6372 &ScrollEvent {
6373 delta: gpui::ScrollDelta::Lines(point(0., -100_000.)),
6374 touch_phase: gpui::TouchPhase::Moved,
6375 ..Default::default()
6376 },
6377 window,
6378 cx,
6379 );
6380 assert!(state.scroll_handle.offset().y < px(0.));
6381 assert_eq!(state.overscroll.offset(), 0.);
6382 });
6383 });
6384 }
6385
6386 #[gpui::test]
6387 fn test_scroll_bounce_new_wheel_input_interrupts_the_return(cx: &mut TestAppContext) {
6388 let view = InputView::build_textarea(cx, |state| {
6389 state.scroll_bounce(true).default_value(long_text())
6390 });
6391 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
6392 let (end, stretch) = swipe_past_the_end(&view, &mut cx);
6393 assert!(stretch < 0.);
6394 swipe(&view, &mut cx, 0., gpui::TouchPhase::Ended);
6395 cx.executor()
6396 .advance_clock(std::time::Duration::from_millis(16));
6397 cx.update(|window, cx| {
6398 window.draw(cx).clear(cx);
6399 view.input.update(cx, |state, cx| {
6400 state.on_scroll_wheel(
6401 &ScrollEvent {
6402 delta: gpui::ScrollDelta::Lines(point(0., 2.)),
6403 touch_phase: gpui::TouchPhase::Moved,
6404 ..Default::default()
6405 },
6406 window,
6407 cx,
6408 );
6409 assert!(state.scroll_handle.offset().y > end);
6410 assert_eq!(state.overscroll.offset(), 0.);
6411 });
6412 });
6413 }
6414
6415 #[gpui::test]
6416 fn test_turning_scroll_bounce_off_drops_the_stretch(cx: &mut TestAppContext) {
6417 let view = InputView::build_textarea(cx, |state| {
6418 state.scroll_bounce(true).default_value(long_text())
6419 });
6420 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
6421 let (_, stretch) = swipe_past_the_end(&view, &mut cx);
6422 assert!(stretch < 0., "the edge stretched: {stretch}");
6423 view.input
6424 .update(&mut cx, |state, cx| state.set_scroll_bounce(false, cx));
6425 assert_eq!(
6426 view.input
6427 .read_with(&cx, |state, _| state.overscroll.offset()),
6428 0.
6429 );
6430 }
6431
6432 #[gpui::test]
6435 fn single_line_is_centered_in_a_taller_frame(cx: &mut TestAppContext) {
6436 struct Frame(Entity<InputBaseState<InputMode>>);
6437 impl Render for Frame {
6438 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
6439 crate::input::InputBase::new("frame")
6440 .h(px(60.))
6441 .child(crate::input::Input::new(&self.0))
6442 }
6443 }
6444
6445 cx.update(crate::init);
6446 let mut input = None;
6447 let window = cx.open_window(size(px(400.), px(100.)), |window, cx| {
6448 let state = cx.new(|cx| crate::input::InputState::new(window, cx).default_value("a"));
6449 input = Some(state.clone());
6450 Frame(state)
6451 });
6452 let input = input.unwrap();
6453 let mut cx = VisualTestContext::from_window(window.into(), cx);
6454 cx.update(|window, cx| window.draw(cx).clear(cx));
6455
6456 input.read_with(&cx, |state, _| {
6457 let line = state.input_bounds;
6458 assert_eq!(line.center().y, px(30.), "line {line:?}");
6459 });
6460 }
6461
6462 #[gpui::test]
6463 fn test_noop_scroll_notifies_diagnostic_dismissal(cx: &mut TestAppContext) {
6464 use std::{cell::Cell, rc::Rc};
6465
6466 cx.update(crate::init);
6467 let mut input = None;
6468 let window = cx.open_window(size(px(400.), px(100.)), |window, cx| {
6469 input = Some(cx.new(|cx| crate::input::EditorState::new(window, cx)));
6470 gpui::EmptyView
6471 });
6472 let input = input.unwrap();
6473 input.update(cx, |state, cx| {
6474 state.input_bounds = Bounds::new(Point::default(), size(px(100.), px(20.)));
6475 state.scroll_size = size(px(100.), px(20.));
6476 state.present_diagnostic(crate::input::DiagnosticEntry::default(), cx);
6477 });
6478 let notifications = Rc::new(Cell::new(0));
6479 let count = notifications.clone();
6480 let _subscription =
6481 cx.update(|cx| cx.observe(&input, move |_, _| count.set(count.get() + 1)));
6482
6483 window
6484 .update(cx, |_, window, cx| {
6485 input.update(cx, |state, cx| {
6486 state.on_scroll_wheel(
6487 &ScrollEvent {
6488 delta: gpui::ScrollDelta::Pixels(point(px(0.), px(-40.))),
6489 ..Default::default()
6490 },
6491 window,
6492 cx,
6493 );
6494 assert_eq!(state.scroll_handle.offset(), Point::default());
6495 assert!(state.diagnostic_popover().is_none());
6496 });
6497 })
6498 .unwrap();
6499 assert_eq!(
6500 notifications.get(),
6501 1,
6502 "clearing a diagnostic must notify even when scrolling is clamped"
6503 );
6504 window
6505 .update(cx, |_, window, cx| {
6506 input.update(cx, |state, cx| {
6507 state.on_scroll_wheel(
6508 &ScrollEvent {
6509 delta: gpui::ScrollDelta::Pixels(point(px(0.), px(-40.))),
6510 ..Default::default()
6511 },
6512 window,
6513 cx,
6514 );
6515 });
6516 })
6517 .unwrap();
6518 assert_eq!(notifications.get(), 1, "an unchanged input must stay quiet");
6519 }
6520
6521 #[gpui::test]
6522 fn test_cursor_layout_consumer_updates_after_selection(cx: &mut TestAppContext) {
6523 use std::{cell::Cell, rc::Rc};
6524 struct Panel {
6525 input: Entity<crate::input::EditorState>,
6526 observed: Rc<Cell<Option<(Bounds<Pixels>, Pixels)>>>,
6527 }
6528 impl Render for Panel {
6529 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
6530 self.observed.set(self.input.read(cx).cursor_layout());
6531 div().size_full().child(self.input.clone())
6532 }
6533 }
6534 struct Root(Entity<Panel>);
6535 impl Render for Root {
6536 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
6537 div().size_full().child(
6538 self.0
6539 .clone()
6540 .cached(gpui::StyleRefinement::default().size_full()),
6541 )
6542 }
6543 }
6544 cx.update(crate::init);
6545 let observed = Rc::new(Cell::new(None));
6546 let mut input = None;
6547 let window = cx.open_window(size(px(400.), px(100.)), |window, cx| {
6548 let state = cx.new(|cx| {
6549 let mut state = crate::input::EditorState::new(window, cx);
6550 state.set_value("abcdefgh", window, cx);
6551 state
6552 });
6553 input = Some(state.clone());
6554 Root(cx.new(|_| Panel {
6555 input: state,
6556 observed: observed.clone(),
6557 }))
6558 });
6559 cx.run_until_parked();
6560 let input = input.unwrap();
6561 let before = input.read_with(cx, |state, _| state.cursor_layout());
6562 input.update(cx, |_, cx| cx.notify());
6567 cx.run_until_parked();
6568 assert_eq!(observed.get(), before);
6569 window
6570 .update(cx, |_, _, cx| {
6571 input.update(cx, |state, cx| state.select_to_with_affinity(3, false, cx));
6572 })
6573 .unwrap();
6574 cx.run_until_parked();
6575 window.update(cx, |_, _, cx| cx.notify()).unwrap();
6576 let after = input.read_with(cx, |state, _| state.cursor_layout());
6577 assert_ne!(after, before, "the caret must actually move");
6578 assert_eq!(
6579 observed.get(),
6580 after,
6581 "render consumers must receive the newly painted caret geometry"
6582 );
6583 }
6584
6585 #[gpui::test]
6586 fn test_input_scroll_offset_notifies_only_on_change(cx: &mut TestAppContext) {
6587 use std::{cell::Cell, rc::Rc};
6588
6589 cx.update(crate::init);
6590 let mut input = None;
6591 let _window = cx.open_window(size(px(400.), px(100.)), |window, cx| {
6592 input = Some(cx.new(|cx| crate::input::InputState::new(window, cx)));
6593 gpui::EmptyView
6594 });
6595 let input = input.unwrap();
6596 let notifications = Rc::new(Cell::new(0));
6597 let count = notifications.clone();
6598 let _subscription =
6599 cx.update(|cx| cx.observe(&input, move |_, _| count.set(count.get() + 1)));
6600
6601 input.update(cx, |state, _| {
6602 state.input_bounds = Bounds::new(Point::default(), size(px(100.), px(20.)));
6603 state.scroll_size = size(px(300.), px(20.));
6604 });
6605 for target in [
6606 None,
6607 Some(point(px(0.), px(0.))),
6608 Some(point(px(20.), px(50.))),
6609 ] {
6610 input.update(cx, |state, cx| state.update_scroll_offset(target, cx));
6611 assert_eq!(
6612 notifications.get(),
6613 0,
6614 "an unchanged clamped offset must stay quiet"
6615 );
6616 }
6617 input.update(cx, |state, cx| {
6618 state.update_scroll_offset(Some(point(px(-40.), px(0.))), cx);
6619 assert_eq!(state.scroll_handle.offset(), point(px(-40.), px(0.)));
6620 });
6621 assert_eq!(notifications.get(), 1, "a scroll must notify");
6622 input.update(cx, |state, cx| {
6623 state.update_scroll_offset(Some(point(px(-400.), px(50.))), cx);
6624 assert_eq!(state.scroll_handle.offset(), point(px(-200.), px(0.)));
6625 });
6626 assert_eq!(notifications.get(), 2);
6627 input.update(cx, |state, cx| {
6628 state.update_scroll_offset(Some(point(px(-500.), px(0.))), cx);
6629 });
6630 assert_eq!(
6631 notifications.get(),
6632 2,
6633 "clamping to the current offset must stay quiet"
6634 );
6635 input.update(cx, |state, cx| {
6636 state.scroll_size.width = px(110.);
6637 state.update_scroll_offset(None, cx);
6638 assert_eq!(state.scroll_handle.offset(), point(px(-10.), px(0.)));
6639 });
6640 assert_eq!(
6641 notifications.get(),
6642 3,
6643 "a smaller scroll range must clamp and notify"
6644 );
6645 }
6646
6647 #[gpui::test]
6648 fn test_set_line_number_notifies_only_on_change(cx: &mut TestAppContext) {
6649 use std::{cell::Cell, rc::Rc};
6650
6651 cx.update(crate::init);
6652 let mut input = None;
6653 let window = cx.open_window(size(px(400.), px(100.)), |window, cx| {
6654 input = Some(cx.new(|cx| crate::input::EditorState::new(window, cx)));
6655 gpui::EmptyView
6656 });
6657 let input = input.unwrap();
6658 let shown = input.read_with(cx, |state, _| state.mode.line_number());
6659 let notifications = Rc::new(Cell::new(0));
6660 let count = notifications.clone();
6661 let _subscription =
6662 cx.update(|cx| cx.observe(&input, move |_, _| count.set(count.get() + 1)));
6663
6664 window
6665 .update(cx, |_, window, cx| {
6666 input.update(cx, |state, cx| state.set_line_number(shown, window, cx))
6667 })
6668 .unwrap();
6669 assert_eq!(notifications.get(), 0);
6670
6671 window
6672 .update(cx, |_, window, cx| {
6673 input.update(cx, |state, cx| state.set_line_number(!shown, window, cx))
6674 })
6675 .unwrap();
6676 assert_eq!(notifications.get(), 1);
6677 assert_eq!(
6678 input.read_with(cx, |state, _| state.mode.line_number()),
6679 !shown
6680 );
6681 }
6682
6683 #[gpui::test]
6684 fn test_set_value_on_unfocused_input_stays_quiet(cx: &mut TestAppContext) {
6685 use std::{cell::Cell, rc::Rc};
6686
6687 cx.update(crate::init);
6688 let mut input = None;
6689 let window = cx.open_window(size(px(400.), px(100.)), |window, cx| {
6690 input = Some(cx.new(|cx| crate::input::InputState::new(window, cx)));
6691 gpui::EmptyView
6692 });
6693 let input = input.unwrap();
6694 cx.run_until_parked();
6695
6696 let notifications = Rc::new(Cell::new(0));
6697 let count = notifications.clone();
6698 let _subscription =
6699 cx.update(|cx| cx.observe(&input, move |_, _| count.set(count.get() + 1)));
6700 cx.run_until_parked();
6701
6702 window
6705 .update(cx, |_, window, cx| {
6706 input.update(cx, |state, cx| state.set_value("seeded", window, cx));
6707 })
6708 .unwrap();
6709 cx.run_until_parked();
6710 let settled = notifications.get();
6711
6712 cx.executor()
6713 .advance_clock(std::time::Duration::from_secs(3));
6714 cx.run_until_parked();
6715
6716 assert_eq!(
6717 notifications.get(),
6718 settled,
6719 "an unfocused input is blinking, and every blink repaints the view it is in"
6720 );
6721 }
6722
6723 #[gpui::test]
6724 fn test_input_does_not_invalidate_cached_parent_during_paint(cx: &mut TestAppContext) {
6725 use std::{cell::Cell, rc::Rc};
6726
6727 struct Panel {
6728 input: Entity<crate::input::InputState>,
6729 renders: Rc<Cell<usize>>,
6730 }
6731 impl Render for Panel {
6732 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
6733 self.renders.set(self.renders.get() + 1);
6734 div().size_full().child(self.input.clone())
6735 }
6736 }
6737 struct Root(Entity<Panel>);
6738 impl Render for Root {
6739 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
6740 div().size_full().child(
6741 self.0
6742 .clone()
6743 .cached(gpui::StyleRefinement::default().size_full()),
6744 )
6745 }
6746 }
6747
6748 cx.update(crate::init);
6749 let renders = Rc::new(Cell::new(0));
6750 let mut input = None;
6751 let window = cx.open_window(size(px(400.), px(100.)), |window, cx| {
6752 let state = cx.new(|cx| crate::input::InputState::new(window, cx));
6753 input = Some(state.clone());
6754 Root(cx.new(|_| Panel {
6755 input: state,
6756 renders: renders.clone(),
6757 }))
6758 });
6759 cx.run_until_parked();
6760 let before = renders.get();
6761 assert!(before > 0);
6762 for _ in 0..60 {
6763 window.update(cx, |_, _, cx| cx.notify()).unwrap();
6764 }
6765 assert_eq!(
6766 renders.get(),
6767 before,
6768 "an idle input must not invalidate its cached parent"
6769 );
6770
6771 let input = input.unwrap();
6772 window
6773 .update(cx, |_, window, cx| {
6774 input.update(cx, |state, cx| state.set_value("changed", window, cx));
6775 })
6776 .unwrap();
6777 assert!(
6778 renders.get() > before,
6779 "text changes must invalidate the parent"
6780 );
6781 window.update(cx, |_, _, cx| cx.notify()).unwrap();
6783 let before = renders.get();
6784 for _ in 0..60 {
6785 window.update(cx, |_, _, cx| cx.notify()).unwrap();
6786 }
6787 assert_eq!(renders.get(), before, "the edited input must settle again");
6788
6789 cx.simulate_window_resize(window.into(), size(px(240.), px(100.)));
6790 cx.run_until_parked();
6791 assert!(renders.get() > before, "resizing must invalidate layout");
6792 }
6793
6794 #[gpui::test]
6795 fn textarea_cursor_treats_crlf_as_one_newline(cx: &mut TestAppContext) {
6796 cx.update(crate::init);
6797 let view = InputView::build_textarea(cx, |state| state);
6798 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
6799 cx.update(|window, cx| {
6800 view.input.update(cx, |state, cx| {
6801 for original in ["\r\nlast", "\u{feff}first\r\nlast\n", "first\nlast\r\n"] {
6802 state.set_value(original, window, cx);
6803 let newline = original.find('\n').unwrap();
6804 let end = if original.as_bytes()[newline.saturating_sub(1)] == b'\r' {
6805 newline - 1
6806 } else {
6807 newline
6808 };
6809 state.end(&MoveEnd, window, cx);
6810 assert_eq!(state.cursor(), end);
6811 state.right(&MoveRight, window, cx);
6812 assert_eq!(state.cursor(), newline + 1);
6813 state.left(&MoveLeft, window, cx);
6814 assert_eq!(state.cursor(), end);
6815 state.select_to_end_of_line(&SelectToEndOfLine, window, cx);
6816 assert_eq!(state.selected_range(), end..end);
6817 state.move_to_end(&MoveToEnd, window, cx);
6818 assert_eq!(state.cursor(), original.len());
6819 state.move_to_start(&MoveToStart, window, cx);
6820 assert_eq!(state.cursor(), 0);
6821 assert_eq!(state.value().as_bytes(), original.as_bytes());
6822 }
6823 state.set_value("a\rb", window, cx);
6825 state.right(&MoveRight, window, cx);
6826 assert_eq!(state.cursor(), 1);
6827 state.right(&MoveRight, window, cx);
6828 assert_eq!(state.cursor(), 2);
6829 })
6830 });
6831 }
6832
6833 #[gpui::test]
6834 fn only_a_multi_line_input_paints_scrollbars(cx: &mut TestAppContext) {
6835 cx.update(crate::init);
6836
6837 let single = InputView::build(cx, |state| state);
6841 single
6842 .input
6843 .update(cx, |state, _| assert!(!state.shows_scrollbar()));
6844
6845 let multi = InputView::build_textarea(cx, |state| state);
6846 multi
6847 .input
6848 .update(cx, |state, _| assert!(state.shows_scrollbar()));
6849 }
6850
6851 #[gpui::test]
6852 fn context_menu_handler_is_deferred_and_respects_disabled(cx: &mut TestAppContext) {
6853 use std::{cell::Cell, rc::Rc};
6854 cx.update(crate::init);
6855 let input_view = InputView::new(cx);
6856 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6857 let input = input_view.input;
6858 let calls = Rc::new(Cell::new(0usize));
6859 let items = Rc::new(Cell::new(0usize));
6860 let menu_focus = cx.update(|_, cx| cx.focus_handle());
6861
6862 cx.update(|window, cx| {
6863 input.update(cx, |state, cx| {
6864 let calls2 = calls.clone();
6865 let items2 = items.clone();
6866 let menu_focus = menu_focus.clone();
6867 state.on_context_menu(Rc::new(move |menu, _, _, window, cx| {
6868 calls2.set(calls2.get() + 1);
6869 items2.set(menu.items.len());
6870 menu_focus.focus(window, cx);
6871 }));
6872 state.handle_right_click_menu(point(px(0.), px(0.)), 0, window, cx);
6873 })
6874 });
6875 assert_eq!(calls.get(), 1);
6876 assert_eq!(items.get(), 0);
6877 input.read_with(&cx, |state, _| {
6878 assert_eq!(
6879 state
6880 .selection_focus
6881 .as_ref()
6882 .and_then(WeakFocusHandle::upgrade)
6883 .as_ref(),
6884 Some(&menu_focus),
6885 "the menu callback must retain selection without reborrowing its input"
6886 );
6887 });
6888
6889 cx.update(|window, cx| {
6891 input.update(cx, |state, cx| {
6892 state.handle_right_click_menu(point(px(0.), px(0.)), 0, window, cx);
6893 });
6894 });
6895 assert_eq!(calls.get(), 2);
6896 cx.update(|window, cx| {
6897 assert!(input.read(cx).has_selection_focus(window, cx));
6898 });
6899
6900 cx.update(|window, cx| {
6901 input.update(cx, |state, cx| {
6902 state.disabled = true;
6903 state.handle_right_click_menu(point(px(0.), px(0.)), 0, window, cx);
6904 })
6905 });
6906 assert_eq!(calls.get(), 2);
6907 }
6908
6909 #[gpui::test]
6910 fn context_menu_handler_can_read_its_input(cx: &mut TestAppContext) {
6911 use std::{cell::Cell, rc::Rc};
6912 cx.update(crate::init);
6913 let input_view = InputView::new(cx);
6914 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6915 let input = input_view.input;
6916 let cursor = Rc::new(Cell::new(None));
6917
6918 cx.update(|window, cx| {
6919 input.update(cx, |state, cx| {
6920 state.set_value("hello", window, cx);
6921 let weak = cx.weak_entity();
6922 let cursor = cursor.clone();
6923 state.on_context_menu(Rc::new(move |_, _, _, _, cx| {
6924 cursor.set(weak.upgrade().map(|input| input.read(cx).cursor()));
6925 }));
6926 state.handle_right_click_menu(point(px(0.), px(0.)), 2, window, cx);
6927 })
6928 });
6929 assert_eq!(cursor.get(), Some(2));
6930 }
6931
6932 #[gpui::test]
6933 fn test_readonly_rejects_user_edits_only(cx: &mut TestAppContext) {
6934 let input_view = InputView::new(cx);
6935 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6936 let input = input_view.input;
6937
6938 cx.update(|window, cx| {
6939 input.update(cx, |state, cx| {
6940 state.set_value("hello", window, cx);
6941 state.set_readonly(true, cx);
6942 });
6943 });
6944
6945 cx.update(|_, cx| {
6946 input.read_with(cx, |state, _| {
6947 assert!(!state.is_editable());
6948 assert!(!state.is_replaceable());
6949 });
6950 });
6951
6952 cx.update(|window, cx| {
6954 input.update(cx, |state, cx| {
6955 state.replace_text_in_range(None, " world", window, cx);
6956 state.replace_and_mark_text_in_range(None, "あ", None, window, cx);
6957 });
6958 });
6959 cx.update(|_, cx| {
6960 input.read_with(cx, |state, _| assert_eq!(state.value(), "hello"));
6961 });
6962
6963 cx.update(|window, cx| {
6965 input.update(cx, |state, cx| {
6966 state.insert(" world", window, cx);
6967 state.set_value("changed", window, cx);
6968 });
6969 });
6970 cx.update(|_, cx| {
6971 input.read_with(cx, |state, _| assert_eq!(state.value(), "changed"));
6972 });
6973
6974 cx.update(|window, cx| {
6977 input.update(cx, |state, cx| {
6978 state.set_readonly(false, cx);
6979 state.replace_text_in_range(None, "!", window, cx);
6980 });
6981 });
6982 cx.update(|_, cx| {
6983 input.read_with(cx, |state, _| {
6984 assert!(state.is_editable());
6985 assert_eq!(state.value(), "!changed");
6986 });
6987 });
6988 }
6989
6990 #[gpui::test]
6995 fn test_edit_reveals_far_offscreen_caret(cx: &mut TestAppContext) {
6996 let mut input = None;
6997 let window = cx.open_window(size(px(720.), px(400.)), |window, cx| {
6998 cx.set_global(Theme::default());
6999 super::super::init(cx);
7000 let state = cx.new(|cx| crate::input::TextareaState::new(window, cx).auto_grow(1, 6));
7001 input = Some(state.clone());
7002 TestRoot(state)
7003 });
7004 let mut cx = VisualTestContext::from_window(window.into(), cx);
7005 let input = input.unwrap();
7006
7007 let text: String = (1..=100)
7008 .map(|i| format!("line {i}"))
7009 .collect::<Vec<_>>()
7010 .join("\n");
7011 cx.update(|window, cx| {
7012 input.update(cx, |state, cx| {
7013 state.set_value(text, window, cx);
7014 let end = state.text.len();
7015 state.set_selection(end, end);
7016 });
7017 });
7018 cx.run_until_parked();
7019
7020 cx.update(|_, cx| {
7022 input.update(cx, |state, cx| {
7023 state.scroll_handle.set_offset(point(px(0.), px(0.)));
7024 cx.notify();
7025 });
7026 });
7027 cx.run_until_parked();
7028
7029 cx.update(|window, cx| {
7030 input.update(cx, |state, cx| {
7031 state.replace_text_in_range(None, "X", window, cx);
7032 });
7033 });
7034 cx.run_until_parked();
7035
7036 cx.update(|_, cx| {
7037 input.read_with(cx, |state, _| {
7038 assert!(state.value().ends_with("line 100X"));
7039 let (caret, _) = state.cursor_layout().expect("caret painted");
7040 let viewport = state.input_bounds();
7041 let scroll_y = state.scroll_handle.offset().y;
7042 let top = caret.origin.y - viewport.origin.y + scroll_y;
7043 assert!(
7044 top >= px(0.) && top + caret.size.height <= viewport.size.height,
7045 "caret top {top:?} outside viewport height {:?} (scroll {scroll_y:?})",
7046 viewport.size.height,
7047 );
7048 });
7049 });
7050 }
7051
7052 #[gpui::test]
7056 fn test_set_auto_grow_keeps_rows_fitted(cx: &mut TestAppContext) {
7057 let mut input = None;
7058 let window = cx.open_window(size(px(720.), px(400.)), |window, cx| {
7059 cx.set_global(Theme::default());
7060 super::super::init(cx);
7061 let state = cx.new(|cx| crate::input::TextareaState::new(window, cx).auto_grow(1, 10));
7062 input = Some(state.clone());
7063 TestRoot(state)
7064 });
7065 let mut cx = VisualTestContext::from_window(window.into(), cx);
7066 let input = input.unwrap();
7067
7068 let text: String = (1..=8)
7069 .map(|i| format!("line {i}"))
7070 .collect::<Vec<_>>()
7071 .join("\n");
7072 cx.update(|window, cx| {
7073 input.update(cx, |state, cx| state.set_value(text, window, cx));
7074 });
7075 cx.run_until_parked();
7076 cx.update(|_, cx| input.read_with(cx, |state, _| assert_eq!(state.mode.rows(), 8)));
7077
7078 cx.update(|_, cx| input.update(cx, |state, cx| state.set_auto_grow(1, 12, cx)));
7080 cx.run_until_parked();
7081 cx.update(|_, cx| input.read_with(cx, |state, _| assert_eq!(state.mode.rows(), 8)));
7082
7083 cx.update(|_, cx| input.update(cx, |state, cx| state.set_auto_grow(1, 4, cx)));
7085 cx.run_until_parked();
7086 cx.update(|_, cx| input.read_with(cx, |state, _| assert_eq!(state.mode.rows(), 4)));
7087 }
7088
7089 #[gpui::test]
7095 fn test_scroll_to_eob_does_not_overshoot_safe_range(cx: &mut TestAppContext) {
7096 let input_view = InputView::new(cx);
7097 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7098 let input = input_view.input;
7099
7100 cx.update(|window, cx| {
7102 input.update(cx, |state, cx| {
7103 state.set_scroll_beyond_last_line(Some(1), window, cx);
7104 state.set_cursor_surrounding_lines(Some(1), window, cx);
7105 let text: String = (1..=50)
7106 .map(|i| format!("line {i}"))
7107 .collect::<Vec<_>>()
7108 .join("\n");
7109 state.set_value(text, window, cx);
7110 });
7111 });
7112 cx.run_until_parked();
7113
7114 cx.update(|_, cx| {
7117 input.read_with(cx, |state, _| {
7118 assert!(
7119 state.scroll_size.height > px(0.),
7120 "scroll_size not populated by initial paint"
7121 );
7122 assert!(
7123 state.input_bounds.size.height > px(0.),
7124 "input_bounds not populated by initial paint"
7125 );
7126 });
7127 });
7128
7129 cx.update(|_, cx| {
7134 input.update(cx, |state, cx| {
7135 let end = state.text.len();
7136 state.move_to(end, Some(MoveDirection::Down), cx);
7137
7138 let deferred = state
7139 .deferred_scroll_offset
7140 .expect("scroll_to should populate deferred_scroll_offset");
7141 let safe_y_min =
7142 (-state.scroll_size.height + state.input_bounds.size.height).min(px(0.));
7143
7144 assert!(
7145 deferred.y >= safe_y_min,
7146 "deferred_scroll_offset.y = {:?} below safe_y_min = {:?} \
7147 — paint would jitter (Bug C regression)",
7148 deferred.y,
7149 safe_y_min,
7150 );
7151 });
7152 });
7153 }
7154
7155 #[gpui::test]
7156 fn test_next_search_match_reveals_with_padding_after_manual_scroll(cx: &mut TestAppContext) {
7157 assert_search_reveals_with_padding_after_manual_scroll(false, cx);
7158 }
7159
7160 #[gpui::test]
7161 fn test_previous_search_match_reveals_with_padding_after_manual_scroll(
7162 cx: &mut TestAppContext,
7163 ) {
7164 assert_search_reveals_with_padding_after_manual_scroll(true, cx);
7165 }
7166
7167 fn assert_search_reveals_with_padding_after_manual_scroll(
7168 previous: bool,
7169 cx: &mut TestAppContext,
7170 ) {
7171 let input_view = InputView::new(cx);
7172 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7173 let input = input_view.input;
7174 let text = (0..160)
7175 .map(|row| {
7176 if matches!(row, 20 | 60 | 100) {
7177 format!("match on row {row}")
7178 } else {
7179 format!("line {row}")
7180 }
7181 })
7182 .collect::<Vec<_>>()
7183 .join("\n");
7184 let start = text.match_indices("match").nth(1).unwrap().0;
7185 let expected_match = start..start + "match".len();
7186 cx.update(|window, cx| {
7187 input.update(cx, |state, cx| {
7188 state.set_cursor_surrounding_lines(Some(3), window, cx);
7189 state.set_value(text, window, cx);
7190 state.set_search_query("match", true, cx);
7191 if previous {
7192 state.search_session.matcher.next();
7193 state.search_session.matcher.next();
7194 }
7195 });
7196 });
7197 cx.run_until_parked();
7198 cx.update(|_, cx| {
7199 input.update(cx, |state, cx| {
7200 let line_height = state.last_layout.as_ref().unwrap().line_height;
7201 let y = if previous { px(0.) } else { -line_height * 80. };
7202 state.set_scroll_offset(point(px(0.), y), cx);
7203 });
7204 });
7205 cx.run_until_parked();
7206 input.read_with(&cx, |state, _| {
7207 let visible = state.visible_row_range().unwrap();
7208 if previous {
7209 assert!(visible.end <= 60, "target must be below the viewport");
7210 } else {
7211 assert!(visible.start > 60, "target must be above the viewport");
7212 }
7213 });
7214 cx.update(|_, cx| {
7215 input.update(cx, |state, cx| {
7216 let range = if previous {
7217 state.previous_search_match(cx)
7218 } else {
7219 state.next_search_match(cx)
7220 };
7221 assert_eq!(range, Some(expected_match));
7222 assert_eq!(state.search_session.matcher.label(), "2/3");
7223 });
7224 });
7225 cx.run_until_parked();
7226 input.read_with(&cx, |state, _| {
7227 assert!(state.visible_row_range().unwrap().contains(&60));
7228 let line_height = state.last_layout.as_ref().unwrap().line_height;
7229 let target_y = line_height * 60. + state.scroll_handle.offset().y;
7230 assert!(target_y >= line_height * 2. - px(0.1));
7232 assert!(
7233 target_y + line_height * 3.
7234 <= state.last_bounds.as_ref().unwrap().size.height + px(0.1),
7235 "search must preserve the configured surrounding-line padding"
7236 );
7237 });
7238 }
7239
7240 struct SearchHost {
7242 editor: Entity<InputBaseState<EditorMode>>,
7243 search_requests: Rc<Cell<usize>>,
7244 }
7245
7246 impl Render for SearchHost {
7247 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
7248 let search_requests = self.search_requests.clone();
7249 div()
7250 .size_full()
7251 .on_action(cx.listener(move |_, _: &Search, _, _| {
7252 search_requests.set(search_requests.get() + 1);
7253 }))
7254 .child(self.editor.clone())
7255 }
7256 }
7257
7258 fn press_search_shortcut(
7261 cx: &mut TestAppContext,
7262 searchable: bool,
7263 ) -> (Entity<InputBaseState<EditorMode>>, Rc<Cell<usize>>) {
7264 let search_requests = Rc::new(Cell::new(0));
7265 let mut editor = None;
7266 let window = cx.update(|cx| {
7267 cx.open_window(Default::default(), |window, cx| {
7268 cx.set_global(Theme::default());
7269 super::super::init(cx);
7270 let state =
7271 cx.new(|cx| crate::input::EditorState::new(window, cx).searchable(searchable));
7272 editor = Some(state.clone());
7273 cx.new(|_| SearchHost {
7274 editor: state,
7275 search_requests: search_requests.clone(),
7276 })
7277 })
7278 .unwrap()
7279 });
7280 let editor = editor.unwrap();
7281 let mut cx = VisualTestContext::from_window(window.into(), cx);
7282 cx.update(|window, cx| {
7283 editor.update(cx, |state, cx| state.focus(window, cx));
7284 });
7285 cx.run_until_parked();
7286 #[cfg(target_os = "macos")]
7287 cx.simulate_keystrokes("cmd-f");
7288 #[cfg(not(target_os = "macos"))]
7289 cx.simulate_keystrokes("ctrl-f");
7290 cx.run_until_parked();
7291 (editor, search_requests)
7292 }
7293
7294 #[gpui::test]
7295 fn test_search_shortcut_reaches_the_host_when_not_searchable(cx: &mut TestAppContext) {
7296 let (editor, search_requests) = press_search_shortcut(cx, false);
7297 assert_eq!(search_requests.get(), 1);
7298 editor.read_with(cx, |state, _| {
7299 assert!(!state.search_session().open);
7300 assert!(!state.search_session().is_active());
7301 });
7302 }
7303
7304 #[gpui::test]
7305 fn test_search_shortcut_opens_the_panel_when_searchable(cx: &mut TestAppContext) {
7306 let (editor, search_requests) = press_search_shortcut(cx, true);
7307 assert_eq!(search_requests.get(), 0);
7308 editor.read_with(cx, |state, _| {
7309 assert!(state.search_session().open);
7310 assert!(state.search_session().is_active());
7311 });
7312 }
7313
7314 #[gpui::test]
7315 fn test_set_search_query_highlights_without_the_panel(cx: &mut TestAppContext) {
7316 let input_view = InputView::build_editor(cx, |state| state.searchable(false));
7317 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7318 let input = input_view.input;
7319 cx.update(|window, cx| {
7320 input.update(cx, |state, cx| {
7321 state.set_value("foo bar foo", window, cx);
7322 state.set_search_query("foo", true, cx);
7323 });
7324 });
7325 cx.run_until_parked();
7326 input.read_with(&cx, |state, _| {
7327 let session = state.search_session();
7328 assert!(session.is_active());
7329 assert!(!session.open);
7330 assert_eq!(session.matcher.len(), 2);
7331 });
7332 cx.update(|_, cx| {
7333 input.update(cx, |state, cx| state.close_search(cx));
7334 });
7335 input.read_with(&cx, |state, _| {
7336 assert!(!state.search_session().is_active());
7337 });
7338 }
7339
7340 #[gpui::test]
7343 fn test_closed_search_resyncs_matches_after_edits(cx: &mut TestAppContext) {
7344 let input_view = InputView::build_editor(cx, |state| state.searchable(false));
7345 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7346 let input = input_view.input;
7347 cx.update(|window, cx| {
7348 input.update(cx, |state, cx| {
7349 state.set_value("foo bar foo", window, cx);
7350 state.set_search_query("foo", true, cx);
7351 state.close_search(cx);
7352
7353 state.set_value("bar foo", window, cx);
7354 assert_eq!(state.next_search_match(cx), Some(4..7));
7355
7356 state.set_value("foo foo foo", window, cx);
7357 state.set_search_query("foo", true, cx);
7358 assert_eq!(state.search_session().matcher.len(), 3);
7359 });
7360 });
7361 }
7362
7363 #[gpui::test]
7364 fn test_search_reveals_offscreen_wrapped_match(cx: &mut TestAppContext) {
7365 let input_view = InputView::new(cx);
7366 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7367 let input = input_view.input;
7368 let text = format!(
7369 "match\n{}\n{}match\n{}",
7370 "line\n".repeat(80),
7371 "wrapped text ".repeat(500),
7372 "line\n".repeat(80)
7373 );
7374 cx.update(|window, cx| {
7375 input.update(cx, |state, cx| {
7376 state.set_cursor_surrounding_lines(Some(3), window, cx);
7377 state.set_value(text, window, cx);
7378 state.set_search_query("match", true, cx);
7379 });
7380 });
7381 cx.run_until_parked();
7382 cx.update(|_, cx| {
7383 input.update(cx, |state, cx| {
7384 state.next_search_match(cx).unwrap();
7385 });
7386 });
7387 cx.run_until_parked();
7388 input.read_with(&cx, |state, _| {
7389 let range = state.search_session.matcher.matched_ranges()[1].clone();
7390 let layout = state.last_layout.as_ref().unwrap();
7391 let (_, _, position) = state.line_and_position_for_offset(range.end);
7392 let y = position.expect("wrapped match must be laid out").y
7393 + state.scroll_handle.offset().y;
7394 assert!(y >= layout.line_height * 2. - px(0.1));
7395 assert!(
7396 y + layout.line_height * 3. <= state.last_bounds.unwrap().size.height + px(0.1),
7397 "wrapped match must retain surrounding display rows"
7398 );
7399 });
7400 }
7401
7402 #[gpui::test]
7403 fn test_number_step(cx: &mut TestAppContext) {
7404 let input = InputView::build(cx, |state| state).input;
7405
7406 cx.update(|cx| {
7407 input.update(cx, |_state, cx| {
7408 assert_eq!(
7409 NumberStep::from(5.).value(123., StepAction::Increment, cx),
7410 5.
7411 );
7412
7413 let step = NumberStep::by_value(|value, action, _cx| {
7416 let below = match action {
7417 StepAction::Increment => value < 1.0,
7418 StepAction::Decrement => value <= 1.0,
7419 };
7420 if below { 0.1 } else { 0.5 }
7421 });
7422 assert_eq!(step.value(0.5, StepAction::Increment, cx), 0.1);
7423 assert_eq!(step.value(1.0, StepAction::Increment, cx), 0.5);
7424 assert_eq!(step.value(1.0, StepAction::Decrement, cx), 0.1);
7425 assert_eq!(step.value(2.0, StepAction::Decrement, cx), 0.5);
7426 });
7427 });
7428 }
7429
7430 #[gpui::test]
7431 fn test_number_input_normalization(cx: &mut TestAppContext) {
7432 let input_view = InputView::build(cx, |state| {
7433 state.mask_pattern(MaskPattern::Number {
7434 separator: None,
7435 fraction: None,
7436 })
7437 });
7438 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7439 let input = input_view.input;
7440
7441 cx.update(|window, cx| {
7445 input.update(cx, |state, cx| {
7446 state.replace_text_in_range(None, "12。5", window, cx);
7447 });
7448 });
7449 cx.run_until_parked();
7450 cx.update(|_, cx| {
7451 input.read_with(cx, |state, _| {
7452 assert_eq!(state.value(), "12.5");
7453 let cursor: Range<usize> = state.selected_range();
7454 assert_eq!(cursor, 4..4);
7455 });
7456 });
7457
7458 cx.update(|window, cx| {
7460 input.update(cx, |state, cx| {
7461 state.replace_text_in_range(None, "abc", window, cx);
7462 });
7463 });
7464 cx.run_until_parked();
7465 cx.update(|_, cx| {
7466 input.read_with(cx, |state, _| {
7467 assert_eq!(state.value(), "12.5");
7468 });
7469 });
7470
7471 cx.update(|window, cx| {
7474 input.update(cx, |state, cx| {
7475 let range = state.range_to_utf16(&(0..state.text.len()));
7476 state.replace_text_in_range(Some(range), "。", window, cx);
7477 });
7478 });
7479 cx.run_until_parked();
7480 cx.update(|_, cx| {
7481 input.read_with(cx, |state, _| {
7482 assert_eq!(state.value(), ".");
7483 let cursor: Range<usize> = state.selected_range();
7484 assert_eq!(cursor, 1..1);
7485 });
7486 });
7487 }
7488
7489 #[gpui::test]
7490 fn test_mask_pattern_accepts_non_ascii_text(cx: &mut TestAppContext) {
7491 let input_view = InputView::build(cx, |state| state.mask_pattern("9999年99月"));
7492 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7493 let input = input_view.input;
7494
7495 cx.update(|window, cx| {
7496 input.update(cx, |state, cx| {
7497 state.replace_text_in_range(None, "2024年12月", window, cx);
7498 });
7499 });
7500 cx.run_until_parked();
7501 cx.update(|_, cx| {
7502 input.read_with(cx, |state, _| {
7503 assert_eq!(state.value(), "2024年12月");
7504 });
7505 });
7506 }
7507
7508 #[gpui::test]
7509 fn test_number_input_normalization_with_separator(cx: &mut TestAppContext) {
7510 let input_view = InputView::build(cx, |state| {
7511 state.mask_pattern(MaskPattern::Number {
7512 separator: Some(','),
7513 fraction: Some(2),
7514 })
7515 });
7516 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7517 let input = input_view.input;
7518
7519 cx.update(|window, cx| {
7520 input.update(cx, |state, cx| {
7521 state.replace_text_in_range(None, "1234", window, cx);
7522 });
7523 });
7524 cx.run_until_parked();
7525 cx.update(|_, cx| {
7526 input.read_with(cx, |state, _| {
7527 assert_eq!(state.value(), "1,234");
7528 assert_eq!(state.unmask_value(), "1234");
7529 });
7530 });
7531 }
7532
7533 #[gpui::test]
7534 fn test_number_input_clamp_on_blur(cx: &mut TestAppContext) {
7535 let input_view = InputView::build(cx, |state| {
7536 state
7537 .mask_pattern(MaskPattern::Number {
7538 separator: None,
7539 fraction: None,
7540 })
7541 .min(10.)
7542 .max(100.)
7543 });
7544 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7545 let input = input_view.input;
7546
7547 cx.update(|window, cx| {
7549 input.update(cx, |state, cx| {
7550 state.replace_text_in_range(None, "1000", window, cx);
7551 assert_eq!(state.value(), "1000");
7552 state.clamp_number_value(window, cx);
7553 assert_eq!(state.value(), "100");
7554
7555 let range = state.range_to_utf16(&(0..state.text.len()));
7556 state.replace_text_in_range(Some(range), "1", window, cx);
7557 assert_eq!(state.value(), "1");
7558 state.clamp_number_value(window, cx);
7559 assert_eq!(state.value(), "10");
7560 });
7561 });
7562 }
7563
7564 #[gpui::test]
7565 fn test_number_input_undo_with_mask(cx: &mut TestAppContext) {
7566 let input_view = InputView::build(cx, |state| {
7567 state.mask_pattern(MaskPattern::Number {
7568 separator: Some(','),
7569 fraction: None,
7570 })
7571 });
7572 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7573 let input = input_view.input;
7574
7575 cx.update(|window, cx| {
7578 input.update(cx, |state, cx| {
7579 state.replace_text_in_range(None, "1234", window, cx);
7580 assert_eq!(state.value(), "1,234");
7581 state.replace_text_in_range(None, "5", window, cx);
7582 assert_eq!(state.value(), "12,345");
7583
7584 state.undo(&Undo, window, cx);
7588 assert_eq!(state.value(), "1,234");
7589 state.undo(&Undo, window, cx);
7590 assert_eq!(state.value(), "");
7591 state.redo(&Redo, window, cx);
7592 assert_eq!(state.value(), "1,234");
7593 state.redo(&Redo, window, cx);
7594 assert_eq!(state.value(), "12,345");
7595 });
7596 });
7597 }
7598
7599 #[gpui::test]
7600 fn test_undo_manager_coalesces_adjacent_typing_transactions(cx: &mut TestAppContext) {
7601 let input_view = InputView::build(cx, |state| state);
7602 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7603 let input = input_view.input;
7604
7605 cx.update(|window, cx| {
7606 input.update(cx, |state, cx| {
7607 state.replace_text_in_range(None, "a", window, cx);
7608 state.replace_text_in_range(None, "b", window, cx);
7609 assert_eq!(state.value(), "ab");
7610
7611 state.undo(&Undo, window, cx);
7612 assert_eq!(state.value(), "");
7613 });
7614 });
7615 }
7616
7617 #[gpui::test]
7618 fn test_undo_manager_cursor_movement_splits_typing(cx: &mut TestAppContext) {
7619 let input_view = InputView::build(cx, |state| state);
7620 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7621 let input = input_view.input;
7622
7623 cx.update(|window, cx| {
7624 input.update(cx, |state, cx| {
7625 state.replace_text_in_range(None, "a", window, cx);
7626 state.replace_text_in_range(None, "b", window, cx);
7627 state.left(&MoveLeft, window, cx);
7628 state.replace_text_in_range(None, "x", window, cx);
7629 assert_eq!(state.value(), "axb");
7630
7631 state.undo(&Undo, window, cx);
7632 assert_eq!(state.value(), "ab");
7633 state.undo(&Undo, window, cx);
7634 assert_eq!(state.value(), "");
7635 });
7636 });
7637 }
7638
7639 #[gpui::test]
7640 fn test_undo_manager_splits_backward_and_forward_delete(cx: &mut TestAppContext) {
7641 let input_view = InputView::build(cx, |state| state);
7642 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7643 let input = input_view.input;
7644
7645 cx.update(|window, cx| {
7646 input.update(cx, |state, cx| {
7647 state.set_value("abcd", window, cx);
7648 state.set_selected_range(2..2, cx);
7649 state.backspace(&Backspace, window, cx);
7650 state.delete(&Delete, window, cx);
7651 assert_eq!(state.value(), "ad");
7652
7653 state.undo(&Undo, window, cx);
7654 assert_eq!(state.value(), "acd");
7655 state.undo(&Undo, window, cx);
7656 assert_eq!(state.value(), "abcd");
7657 });
7658 });
7659 }
7660
7661 #[gpui::test]
7662 fn test_undo_manager_coalesces_directional_character_deletes(cx: &mut TestAppContext) {
7663 let input_view = InputView::build(cx, |state| state);
7664 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7665 let input = input_view.input;
7666
7667 cx.update(|window, cx| {
7668 input.update(cx, |state, cx| {
7669 state.set_value("abcd", window, cx);
7670 state.backspace(&Backspace, window, cx);
7671 state.backspace(&Backspace, window, cx);
7672 assert_eq!(state.value(), "ab");
7673 state.undo(&Undo, window, cx);
7674 assert_eq!(state.value(), "abcd");
7675 assert_eq!(state.selected_range(), 4..4);
7676
7677 state.set_value("abcd", window, cx);
7678 state.set_selected_range(1..1, cx);
7679 state.delete(&Delete, window, cx);
7680 state.delete(&Delete, window, cx);
7681 assert_eq!(state.value(), "ad");
7682 state.undo(&Undo, window, cx);
7683 assert_eq!(state.value(), "abcd");
7684 assert_eq!(state.selected_range(), 1..1);
7685 });
7686 });
7687 }
7688
7689 #[gpui::test]
7690 fn test_undo_manager_atomic_paste_isolated_from_typing(cx: &mut TestAppContext) {
7691 let input_view = InputView::build(cx, |state| state);
7692 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7693 let input = input_view.input;
7694
7695 cx.update(|window, cx| {
7696 cx.write_to_clipboard(ClipboardItem::new_string("P".to_string()));
7697 input.update(cx, |state, cx| {
7698 state.replace_text_in_range(None, "a", window, cx);
7699 state.paste(&Paste, window, cx);
7700 state.replace_text_in_range(None, "b", window, cx);
7701 assert_eq!(state.value(), "aPb");
7702
7703 state.undo(&Undo, window, cx);
7704 assert_eq!(state.value(), "aP");
7705 state.undo(&Undo, window, cx);
7706 assert_eq!(state.value(), "a");
7707 state.undo(&Undo, window, cx);
7708 assert_eq!(state.value(), "");
7709 });
7710 });
7711 }
7712
7713 #[gpui::test]
7714 fn test_undo_manager_programmatic_insert_is_atomic(cx: &mut TestAppContext) {
7715 let input_view = InputView::build(cx, |state| state);
7716 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7717 let input = input_view.input;
7718
7719 cx.update(|window, cx| {
7720 input.update(cx, |state, cx| {
7721 state.replace_text_in_range(None, "a", window, cx);
7722 state.insert("P", window, cx);
7723 state.replace_text_in_range(None, "b", window, cx);
7724 assert_eq!(state.value(), "aPb");
7725
7726 state.undo(&Undo, window, cx);
7727 assert_eq!(state.value(), "aP");
7728 state.undo(&Undo, window, cx);
7729 assert_eq!(state.value(), "a");
7730 });
7731 });
7732 }
7733
7734 #[gpui::test]
7735 fn test_undo_manager_selection_round_trip_splits_typing(cx: &mut TestAppContext) {
7736 let input_view = InputView::build(cx, |state| state);
7737 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7738 let input = input_view.input;
7739
7740 cx.update(|window, cx| {
7741 input.update(cx, |state, cx| {
7742 state.replace_text_in_range(None, "a", window, cx);
7743 state.replace_text_in_range(None, "b", window, cx);
7744 state.select_all(window, cx);
7745 state.unselect(window, cx);
7746 state.replace_text_in_range(None, "c", window, cx);
7747
7748 state.undo(&Undo, window, cx);
7749 assert_eq!(state.value(), "ab");
7750 state.undo(&Undo, window, cx);
7751 assert_eq!(state.value(), "");
7752 });
7753 });
7754 }
7755
7756 #[gpui::test]
7757 fn test_undo_manager_enter_is_atomic(cx: &mut TestAppContext) {
7758 let input_view = InputView::build_textarea(cx, |state| state);
7759 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7760 let input = input_view.input;
7761
7762 cx.update(|window, cx| {
7763 input.update(cx, |state, cx| {
7764 state.replace_text_in_range(None, "a", window, cx);
7765 state.enter(
7766 &Enter {
7767 secondary: false,
7768 shift: false,
7769 },
7770 window,
7771 cx,
7772 );
7773 state.replace_text_in_range(None, "b", window, cx);
7774
7775 state.undo(&Undo, window, cx);
7776 assert_eq!(state.value(), "a\n");
7777 state.undo(&Undo, window, cx);
7778 assert_eq!(state.value(), "a");
7779 state.undo(&Undo, window, cx);
7780 assert_eq!(state.value(), "");
7781 });
7782 });
7783 }
7784
7785 #[gpui::test]
7786 fn test_undo_manager_single_line_return_commits_the_typing_session(cx: &mut TestAppContext) {
7787 let input_view = InputView::build(cx, |state| state);
7788 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7789 let input = input_view.input;
7790
7791 cx.update(|window, cx| {
7792 input.update(cx, |state, cx| {
7793 for part in ["a", "b", "c"] {
7794 state.replace_text_in_range(None, part, window, cx);
7795 }
7796 state.enter(
7797 &Enter {
7798 secondary: false,
7799 shift: false,
7800 },
7801 window,
7802 cx,
7803 );
7804 for part in ["d", "e", "f"] {
7805 state.replace_text_in_range(None, part, window, cx);
7806 }
7807 assert_eq!(state.value(), "abcdef");
7808
7809 state.undo(&Undo, window, cx);
7810 assert_eq!(state.value(), "abc");
7811 state.undo(&Undo, window, cx);
7812 assert_eq!(state.value(), "");
7813 });
7814 });
7815 }
7816
7817 #[gpui::test]
7818 fn test_undo_manager_submit_on_enter_commits_the_textarea_session(cx: &mut TestAppContext) {
7819 let input_view = InputView::build_textarea(cx, |state| state.submit_on_enter(true));
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, "before submit", window, cx);
7826 state.enter(
7827 &Enter {
7828 secondary: false,
7829 shift: false,
7830 },
7831 window,
7832 cx,
7833 );
7834 state.replace_text_in_range(None, " after submit", window, cx);
7835 assert_eq!(state.value(), "before submit after submit");
7836
7837 state.undo(&Undo, window, cx);
7838 assert_eq!(state.value(), "before submit");
7839 state.undo(&Undo, window, cx);
7840 assert_eq!(state.value(), "");
7841 });
7842 });
7843 }
7844
7845 #[gpui::test]
7846 fn test_undo_manager_blur_commits_the_typing_session(cx: &mut TestAppContext) {
7847 let input_view = InputView::build(cx, |state| state);
7848 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7849 let input = input_view.input;
7850
7851 cx.update(|window, cx| {
7852 input.update(cx, |state, cx| {
7853 state.replace_text_in_range(None, "before blur", window, cx);
7854 state.on_blur(window, cx);
7855 state.on_focus(window, cx);
7856 state.replace_text_in_range(None, " after focus", window, cx);
7857 assert_eq!(state.value(), "before blur after focus");
7858
7859 state.undo(&Undo, window, cx);
7860 assert_eq!(state.value(), "before blur");
7861 state.undo(&Undo, window, cx);
7862 assert_eq!(state.value(), "");
7863 });
7864 });
7865 }
7866
7867 #[gpui::test]
7868 fn test_undo_manager_keeps_rapid_lines_in_distinct_transactions(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 let enter = Enter {
7876 secondary: false,
7877 shift: false,
7878 };
7879 state.replace_text_in_range(None, "a", window, cx);
7880 state.enter(&enter, window, cx);
7881 state.replace_text_in_range(None, "b", window, cx);
7882 state.enter(&enter, window, cx);
7883 state.replace_text_in_range(None, "c", window, cx);
7884 assert_eq!(state.value(), "a\nb\nc");
7885
7886 for expected in ["a\nb\n", "a\nb", "a\n", "a", ""] {
7887 state.undo(&Undo, window, cx);
7888 assert_eq!(state.value(), expected);
7889 }
7890 });
7891 });
7892 }
7893
7894 #[gpui::test]
7895 fn test_undo_manager_coalesces_long_unicode_typing_without_a_timer(cx: &mut TestAppContext) {
7896 let input_view = InputView::build_textarea(cx, |state| state);
7897 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7898 let input = input_view.input;
7899 let parts = [
7900 "The ",
7901 "quick ",
7902 "brown fox, ",
7903 "你好,世界 ",
7904 "🦀 jumps over 13 lazy dogs.",
7905 ];
7906 let expected = parts.concat();
7907
7908 cx.update(|window, cx| {
7909 input.update(cx, |state, cx| {
7910 for part in parts {
7911 state.replace_text_in_range(None, part, window, cx);
7912 }
7913 assert_eq!(state.value(), expected);
7914
7915 state.undo(&Undo, window, cx);
7916 assert_eq!(state.value(), "");
7917 state.redo(&Redo, window, cx);
7918 assert_eq!(state.value(), expected);
7919 });
7920 });
7921 }
7922
7923 #[gpui::test]
7924 fn test_undo_manager_long_multiline_sequence_has_structural_boundaries(
7925 cx: &mut TestAppContext,
7926 ) {
7927 let input_view = InputView::build_textarea(cx, |state| state);
7928 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7929 let input = input_view.input;
7930 let enter = Enter {
7931 secondary: false,
7932 shift: false,
7933 };
7934
7935 cx.update(|window, cx| {
7936 input.update(cx, |state, cx| {
7937 for (index, line) in [
7938 "first line with punctuation!",
7939 "第二行包含 Unicode 🦀",
7940 "third line has several words",
7941 ]
7942 .into_iter()
7943 .enumerate()
7944 {
7945 for chunk in line.split_inclusive(' ') {
7946 state.replace_text_in_range(None, chunk, window, cx);
7947 }
7948 if index < 2 {
7949 state.enter(&enter, window, cx);
7950 }
7951 }
7952
7953 assert_eq!(
7954 state.value(),
7955 "first line with punctuation!\n第二行包含 Unicode 🦀\nthird line has several words"
7956 );
7957 state.undo(&Undo, window, cx);
7958 assert_eq!(
7959 state.value(),
7960 "first line with punctuation!\n第二行包含 Unicode 🦀\n"
7961 );
7962 state.undo(&Undo, window, cx);
7963 assert_eq!(
7964 state.value(),
7965 "first line with punctuation!\n第二行包含 Unicode 🦀"
7966 );
7967 state.undo(&Undo, window, cx);
7968 assert_eq!(state.value(), "first line with punctuation!\n");
7969 });
7970 });
7971 }
7972
7973 #[gpui::test]
7974 fn test_masked_input_keeps_its_value_out_of_the_clipboard(cx: &mut TestAppContext) {
7975 let input_view = InputView::build(cx, |state| state);
7976 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7977 let input = input_view.input;
7978
7979 cx.update(|window, cx| {
7980 input.update(cx, |state, cx| {
7981 state.set_value("hunter2", window, cx);
7982 state.set_masked(true, window, cx);
7983 state.select_all(window, cx);
7984 cx.write_to_clipboard(ClipboardItem::new_string("sentinel".into()));
7985
7986 state.copy(&Copy, window, cx);
7987 assert_eq!(
7988 cx.read_from_clipboard().and_then(|item| item.text()),
7989 Some("sentinel".to_string())
7990 );
7991
7992 state.cut(&Cut, window, cx);
7994 assert_eq!(state.value(), "hunter2");
7995 assert_eq!(
7996 cx.read_from_clipboard().and_then(|item| item.text()),
7997 Some("sentinel".to_string())
7998 );
7999
8000 state.set_masked(false, window, cx);
8002 state.copy(&Copy, window, cx);
8003 assert_eq!(
8004 cx.read_from_clipboard().and_then(|item| item.text()),
8005 Some("hunter2".to_string())
8006 );
8007 });
8008 });
8009 }
8010
8011 #[gpui::test]
8012 fn test_masked_input_collapses_word_boundaries(cx: &mut TestAppContext) {
8013 let input_view = InputView::build(cx, |state| state);
8014 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8015 let input = input_view.input;
8016
8017 cx.update(|window, cx| {
8018 input.update(cx, |state, cx| {
8019 state.set_value("aaa bbb ccc", window, cx);
8020 state.set_masked(true, window, cx);
8021 state.set_selected_range(7..7, cx);
8022
8023 state.delete_previous_word(&DeleteToPreviousWordStart, window, cx);
8026 assert_eq!(state.value(), " ccc");
8027 assert_eq!(state.selected_range(), 0..0);
8028
8029 state.delete_next_word(&DeleteToNextWordEnd, window, cx);
8030 assert_eq!(state.value(), "");
8031
8032 state.set_value("aaa bbb ccc", window, cx);
8034 state.select_word(9, window, cx);
8035 assert_eq!(state.selected_range(), 0..11);
8036
8037 state.set_masked(false, window, cx);
8039 state.set_value("aaa bbb ccc", window, cx);
8040 state.set_selected_range(11..11, cx);
8041 state.delete_previous_word(&DeleteToPreviousWordStart, window, cx);
8042 assert_eq!(state.value(), "aaa bbb ");
8043
8044 state.set_value("aaa bbb ccc", window, cx);
8045 state.select_word(9, window, cx);
8046 assert_eq!(state.selected_range(), 8..11);
8047 });
8048 });
8049 }
8050
8051 #[gpui::test]
8052 fn test_masked_input_disables_the_copy_context_menu_items(cx: &mut TestAppContext) {
8053 let input_view = InputView::build(cx, |state| state);
8054 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8055 let input = input_view.input;
8056
8057 cx.update(|window, cx| {
8058 input.update(cx, |state, cx| {
8059 state.set_value("hunter2", window, cx);
8060 state.select_all(window, cx);
8061 assert!(state.context_menu_capabilities().is_copyable());
8062
8063 state.set_masked(true, window, cx);
8064 let capabilities = state.context_menu_capabilities();
8065 assert!(capabilities.is_masked());
8066 assert!(capabilities.has_selection());
8067 assert!(!capabilities.is_copyable());
8068 });
8069 });
8070 }
8071
8072 #[gpui::test]
8073 fn test_undo_manager_cut_and_repeated_pastes_are_distinct_transactions(
8074 cx: &mut TestAppContext,
8075 ) {
8076 let input_view = InputView::build(cx, |state| state);
8077 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8078 let input = input_view.input;
8079
8080 cx.update(|window, cx| {
8081 input.update(cx, |state, cx| {
8082 state.replace_text_in_range(None, "alpha beta gamma", window, cx);
8083 state.set_selected_range(6..10, cx);
8084 state.cut(&Cut, window, cx);
8085 assert_eq!(state.value(), "alpha gamma");
8086
8087 state.paste(&Paste, window, cx);
8088 state.paste(&Paste, window, cx);
8089 assert_eq!(state.value(), "alpha betabeta gamma");
8090
8091 state.undo(&Undo, window, cx);
8092 assert_eq!(state.value(), "alpha beta gamma");
8093 state.undo(&Undo, window, cx);
8094 assert_eq!(state.value(), "alpha gamma");
8095 state.undo(&Undo, window, cx);
8096 assert_eq!(state.value(), "alpha beta gamma");
8097 });
8098 });
8099 }
8100
8101 #[gpui::test]
8102 fn test_undo_manager_word_and_line_deletes_do_not_coalesce(cx: &mut TestAppContext) {
8103 let input_view = InputView::build_textarea(cx, |state| state);
8104 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8105 let input = input_view.input;
8106
8107 cx.update(|window, cx| {
8108 input.update(cx, |state, cx| {
8109 state.set_value("one two three\nfour five", window, cx);
8110 state.set_selected_range(13..13, cx);
8111 state.delete_previous_word(&DeleteToPreviousWordStart, window, cx);
8112 assert_eq!(state.value(), "one two \nfour five");
8113 state.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
8114 assert_eq!(state.value(), "one two four five");
8115
8116 state.undo(&Undo, window, cx);
8117 assert_eq!(state.value(), "one two \nfour five");
8118 state.undo(&Undo, window, cx);
8119 assert_eq!(state.value(), "one two three\nfour five");
8120 });
8121 });
8122 }
8123
8124 #[gpui::test]
8125 fn test_undo_manager_multiline_replacement_is_one_atomic_transaction(cx: &mut TestAppContext) {
8126 let input_view = InputView::build_textarea(cx, |state| state);
8127 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8128 let input = input_view.input;
8129
8130 cx.update(|window, cx| {
8131 input.update(cx, |state, cx| {
8132 state.replace_text_in_range(None, "before", window, cx);
8133 state.set_selected_range(0..6, cx);
8134 state.replace_text_in_range(None, "line one\nline two\n第三行", window, cx);
8135 assert_eq!(state.value(), "line one\nline two\n第三行");
8136
8137 state.undo(&Undo, window, cx);
8138 assert_eq!(state.value(), "before");
8139 assert_eq!(state.selected_range(), 0..6);
8140 state.redo(&Redo, window, cx);
8141 assert_eq!(state.value(), "line one\nline two\n第三行");
8142 });
8143 });
8144 }
8145
8146 #[gpui::test]
8147 fn test_undo_manager_composition_isolated_from_long_typing(cx: &mut TestAppContext) {
8148 let input_view = InputView::build(cx, |state| state);
8149 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8150 let input = input_view.input;
8151
8152 cx.update(|window, cx| {
8153 input.update(cx, |state, cx| {
8154 state.replace_text_in_range(None, "prefix ", window, cx);
8155 state.replace_and_mark_text_in_range(None, "n", None, window, cx);
8156 state.replace_and_mark_text_in_range(None, "ni", None, window, cx);
8157 state.replace_and_mark_text_in_range(None, "你", None, window, cx);
8158 state.unmark_text(window, cx);
8159 state.replace_text_in_range(None, " suffix", window, cx);
8160 assert_eq!(state.value(), "prefix 你 suffix");
8161
8162 state.undo(&Undo, window, cx);
8163 assert_eq!(state.value(), "prefix 你");
8164 state.undo(&Undo, window, cx);
8165 assert_eq!(state.value(), "prefix ");
8166 state.undo(&Undo, window, cx);
8167 assert_eq!(state.value(), "");
8168 });
8169 });
8170 }
8171
8172 #[gpui::test]
8173 fn test_undo_manager_selected_replacement_is_atomic(cx: &mut TestAppContext) {
8174 let input_view = InputView::build(cx, |state| state);
8175 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8176 let input = input_view.input;
8177
8178 cx.update(|window, cx| {
8179 input.update(cx, |state, cx| {
8180 state.replace_text_in_range(None, "abc", window, cx);
8181 state.set_selected_range(1..2, cx);
8182 state.replace_text_in_range(None, "X", window, cx);
8183 state.replace_text_in_range(None, "z", window, cx);
8184 assert_eq!(state.value(), "aXzc");
8185
8186 state.undo(&Undo, window, cx);
8187 assert_eq!(state.value(), "aXc");
8188 state.undo(&Undo, window, cx);
8189 assert_eq!(state.value(), "abc");
8190 state.undo(&Undo, window, cx);
8191 assert_eq!(state.value(), "");
8192 });
8193 });
8194 }
8195
8196 #[gpui::test]
8197 fn test_number_input_leading_dot_editable(cx: &mut TestAppContext) {
8198 let input_view = InputView::build(cx, |state| {
8199 state.mask_pattern(MaskPattern::Number {
8200 separator: None,
8201 fraction: None,
8202 })
8203 });
8204 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8205 let input = input_view.input;
8206
8207 cx.update(|window, cx| {
8208 input.update(cx, |state, cx| {
8209 state.replace_text_in_range(None, "1.2", window, cx);
8210
8211 let range = state.range_to_utf16(&(0..1));
8215 state.replace_text_in_range(Some(range), "", window, cx);
8216 assert_eq!(state.value(), ".2");
8217 let cursor: Range<usize> = state.selected_range();
8218 assert_eq!(cursor, 0..0);
8219
8220 state.replace_text_in_range(Some(0..0), "3", window, cx);
8222 assert_eq!(state.value(), "3.2");
8223 });
8224 });
8225 }
8226
8227 #[gpui::test]
8228 fn test_number_input_escape_invalid_text(cx: &mut TestAppContext) {
8229 let input_view = InputView::build(cx, |state| {
8232 state
8233 .mask_pattern(MaskPattern::Number {
8234 separator: None,
8235 fraction: None,
8236 })
8237 .default_value("1,234")
8238 });
8239 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8240 let input = input_view.input;
8241
8242 cx.update(|window, cx| {
8243 input.update(cx, |state, cx| {
8244 let range = state.range_to_utf16(&(4..5));
8248 state.replace_text_in_range(Some(range), "", window, cx);
8249 assert_eq!(state.value(), "1,23");
8250
8251 let range = state.range_to_utf16(&(1..2));
8253 state.replace_text_in_range(Some(range), "", window, cx);
8254 assert_eq!(state.value(), "123");
8255 state.replace_text_in_range(None, "a", window, cx);
8256 assert_eq!(state.value(), "123");
8257 });
8258 });
8259 }
8260
8261 #[gpui::test]
8265 fn test_set_value_single_line_caret_at_end_view_at_start(cx: &mut TestAppContext) {
8266 let input_view = InputView::build(cx, |state| state);
8267 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8268 let input = input_view.input;
8269
8270 let value = format!("https://example.com/v1/users?{}", "x=1&".repeat(120));
8272 let len = value.len();
8273
8274 cx.update(|window, cx| {
8277 input.update(cx, |state, cx| {
8278 state.set_value(value.clone(), window, cx);
8279
8280 assert_eq!(
8281 state.selected_range(),
8282 len..len,
8283 "single-line caret should be at the end after set_value"
8284 );
8285 assert_eq!(
8286 state.deferred_scroll_offset,
8287 Some(point(px(0.), px(0.))),
8288 "the view should be forced back to the start"
8289 );
8290 });
8291 });
8292
8293 cx.run_until_parked();
8296 cx.update(|_, cx| {
8297 input.read_with(cx, |state, _| {
8298 assert!(
8299 state.scroll_size.width > state.input_bounds.size.width,
8300 "value must overflow the input width or this test is vacuous"
8301 );
8302 assert_eq!(
8303 state.scroll_handle.offset().x,
8304 px(0.),
8305 "long value should display from its start, not its tail"
8306 );
8307 });
8308 });
8309 }
8310
8311 #[gpui::test]
8315 fn test_replace_all_single_line(cx: &mut TestAppContext) {
8316 let input_view = InputView::build(cx, |state| state);
8317 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8318 let input = input_view.input;
8319
8320 let value = format!("https://example.com/v1/users?{}", "x=1&".repeat(120));
8322 let len = value.len();
8323
8324 cx.update(|window, cx| {
8328 input.update(cx, |state, cx| {
8329 state.set_value("hello", window, cx);
8330 state.replace_all(value.clone(), window, cx);
8331 assert_eq!(state.value(), value);
8332 assert_eq!(
8333 state.selected_range(),
8334 len..len,
8335 "single-line caret should be at the end after replace_all"
8336 );
8337 assert_eq!(
8338 state.scroll_handle.offset(),
8339 point(px(0.), px(0.)),
8340 "the scroll offset should be reset to the start"
8341 );
8342 assert_eq!(
8343 state.deferred_scroll_offset,
8344 Some(point(px(0.), px(0.))),
8345 "single-line should set a deferred scroll offset to keep the start visible"
8346 );
8347 });
8348 });
8349
8350 cx.run_until_parked();
8353 cx.update(|_, cx| {
8354 input.read_with(cx, |state, _| {
8355 assert!(
8356 state.scroll_size.width > state.input_bounds.size.width,
8357 "value must overflow the input width or this test is vacuous"
8358 );
8359 assert_eq!(
8360 state.scroll_handle.offset().x,
8361 px(0.),
8362 "long value should display from its start, not its tail"
8363 );
8364 });
8365 });
8366 }
8367
8368 #[gpui::test]
8369 fn test_single_line_removes_newlines(cx: &mut TestAppContext) {
8370 let input_view = InputView::build(cx, |state| state.default_value("default\nvalue"));
8371 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8372 let input = input_view.input;
8373
8374 cx.update(|window, cx| {
8375 input.update(cx, |state, cx| {
8376 assert_eq!(state.value(), "defaultvalue");
8377
8378 state.set_value("first\nsecond\r\nthird\rfourth", window, cx);
8379 assert_eq!(state.value(), "firstsecondthirdfourth");
8380
8381 state.set_value("", window, cx);
8382 state.insert("a\nb", window, cx);
8383 assert_eq!(state.value(), "ab");
8384 });
8385
8386 cx.write_to_clipboard(ClipboardItem::new_string("a\r\nb\nc\rd".to_string()));
8387 input.update(cx, |state, cx| {
8388 state.set_value("", window, cx);
8389 state.paste(&Paste, window, cx);
8390 assert_eq!(state.value(), "abcd");
8391 });
8392 });
8393
8394 cx.run_until_parked();
8395 }
8396
8397 #[gpui::test]
8401 fn test_replace_all_multi_line(cx: &mut TestAppContext) {
8402 let input_view = InputView::build_textarea(cx, |state| state);
8403 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8404 let input = input_view.input;
8405
8406 cx.update(|window, cx| {
8407 input.update(cx, |state, cx| {
8408 state.set_value("foo\nbar", window, cx);
8409 state.replace_all("baz\nqux", window, cx);
8410 assert_eq!(state.value(), "baz\nqux");
8411 assert_eq!(
8412 state.selected_range(),
8413 0..0,
8414 "multi-line selection should be cleared after replace_all"
8415 );
8416 assert_eq!(
8417 state.scroll_handle.offset(),
8418 point(px(0.), px(0.)),
8419 "the scroll offset should be reset to the start"
8420 );
8421 assert!(
8422 state.deferred_scroll_offset.is_none(),
8423 "multi-line should not set a deferred scroll offset"
8424 );
8425 });
8426 });
8427 }
8428
8429 #[gpui::test]
8432 fn test_replace_all_preserves_undo_history(cx: &mut TestAppContext) {
8433 let input_view = InputView::build(cx, |state| state);
8434 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8435 let input = input_view.input;
8436
8437 cx.update(|window, cx| {
8438 input.update(cx, |state, cx| {
8439 state.set_value("first", window, cx);
8441 assert!(
8442 !state.undo_manager.has_undos(),
8443 "history should be empty after set_value"
8444 );
8445
8446 state.replace_all("second", window, cx);
8448 assert_eq!(state.value(), "second");
8449 assert!(
8450 state.undo_manager.has_undos(),
8451 "replace_all should record an undo step"
8452 );
8453
8454 state.undo(&Undo, window, cx);
8456 assert_eq!(state.value(), "first");
8457
8458 state.redo(&Redo, window, cx);
8460 assert_eq!(state.value(), "second");
8461 });
8462 });
8463 }
8464
8465 #[gpui::test]
8468 fn test_replace_all_code_editor(cx: &mut TestAppContext) {
8469 let input_view = InputView::new(cx);
8470 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8471 let input = input_view.input;
8472
8473 cx.update(|window, cx| {
8474 input.update(cx, |state, cx| {
8475 state.set_value("select 1", window, cx);
8477 state._pending_update = false;
8478
8479 state.replace_all("select 2", window, cx);
8480 assert_eq!(state.value(), "select 2");
8481 assert!(
8482 state._pending_update,
8483 "replace_all on a code editor should request a pending update"
8484 );
8485 });
8486 });
8487 }
8488
8489 #[gpui::test]
8490 fn test_set_selected_range(cx: &mut TestAppContext) {
8491 let input_view = InputView::build(cx, |state| state.default_value("hello world"));
8492 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8493 let input = input_view.input;
8494
8495 cx.update(|_, cx| {
8496 input.update(cx, |s, cx| {
8497 s.set_selected_range(0..5, cx);
8498 assert_eq!(s.selected_range(), 0..5);
8499 assert_eq!(s.selected_text().to_string(), "hello");
8500
8501 s.set_selected_range(6..11, cx);
8502 assert_eq!(s.selected_text().to_string(), "world");
8503
8504 s.set_selected_range(100..100, cx);
8506 assert_eq!(s.selected_range(), 11..11);
8507 });
8508 });
8509 }
8510
8511 #[gpui::test]
8513 fn test_replace_text_in_ranges_single_edit(cx: &mut TestAppContext) {
8514 let input_view = InputView::build_textarea(cx, |state| state.default_value("hello world"));
8515 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8516 let input = input_view.input;
8517
8518 cx.update(|window, cx| {
8519 input.update(cx, |s, cx| {
8520 s.replace_text_in_ranges(&[(0..5, "HELLO".to_string())], window, cx);
8521 assert_eq!(s.value(), "HELLO world");
8522
8523 s.undo(&Undo, window, cx);
8524 assert_eq!(s.value(), "hello world");
8525
8526 s.redo(&Redo, window, cx);
8527 assert_eq!(s.value(), "HELLO world");
8528 });
8529 });
8530 }
8531
8532 #[gpui::test]
8535 fn test_replace_text_in_ranges_multi_edit_transaction(cx: &mut TestAppContext) {
8536 let input_view = InputView::build_textarea(cx, |state| state.default_value("aaa bbb ccc"));
8537 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8538 let input = input_view.input;
8539
8540 cx.update(|window, cx| {
8541 input.update(cx, |s, cx| {
8542 s.replace_text_in_ranges(
8545 &[(0..3, "X".to_string()), (8..11, "Y".to_string())],
8546 window,
8547 cx,
8548 );
8549 assert_eq!(s.value(), "X bbb Y");
8550
8551 let cursors: Vec<usize> =
8553 s.selections.iter().map(|sel| sel.cursor_offset()).collect();
8554 assert_eq!(cursors, vec![1, 7]);
8555
8556 assert_eq!(s.undo_manager.undo_count(), 1);
8558
8559 s.undo(&Undo, window, cx);
8561 assert_eq!(s.value(), "aaa bbb ccc");
8562
8563 s.redo(&Redo, window, cx);
8565 assert_eq!(s.value(), "X bbb Y");
8566 });
8567 });
8568 }
8569
8570 #[gpui::test]
8574 fn test_replace_text_in_ranges_drives_the_highlighter_once(cx: &mut TestAppContext) {
8575 use crate::input::{FoldRange, HighlightStyleResolver, InputEdit, InputHighlighter};
8576 use gpui::HighlightStyle;
8577 use std::cell::RefCell;
8578
8579 struct RecordingHighlighter(Rc<RefCell<Vec<Vec<String>>>>);
8580
8581 impl InputHighlighter for RecordingHighlighter {
8582 fn language(&self) -> SharedString {
8583 "recording".into()
8584 }
8585
8586 fn update(
8587 &mut self,
8588 _: Option<InputEdit>,
8589 text: &Rope,
8590 _: bool,
8591 _: &mut Window,
8592 _: &mut Context<EditorState>,
8593 ) {
8594 self.0.borrow_mut().push(vec![text.to_string()]);
8595 }
8596
8597 fn update_batch(
8598 &mut self,
8599 edits: &[(InputEdit, Rope)],
8600 _: bool,
8601 _: &mut Window,
8602 _: &mut Context<EditorState>,
8603 ) {
8604 let texts = edits.iter().map(|(_, text)| text.to_string()).collect();
8605 self.0.borrow_mut().push(texts);
8606 }
8607
8608 fn styles(
8609 &self,
8610 range: &Range<usize>,
8611 _: &dyn HighlightStyleResolver,
8612 ) -> Vec<(Range<usize>, HighlightStyle)> {
8613 vec![(range.clone(), HighlightStyle::default())]
8614 }
8615
8616 fn fold_ranges(&self, _: &Rope) -> Vec<FoldRange> {
8617 Vec::new()
8618 }
8619 }
8620
8621 let updates = Rc::new(RefCell::new(Vec::new()));
8622 let input_view = InputView::new(cx);
8623 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8624 let input = input_view.input;
8625
8626 cx.update(|window, cx| {
8627 input.update(cx, |s, cx| {
8628 s.set_value("aaa bbb ccc", window, cx);
8629 let recorded = updates.clone();
8630 s.set_highlighter_factory(
8631 Rc::new(move |_: &str| {
8632 let highlighter: Box<dyn InputHighlighter> =
8633 Box::new(RecordingHighlighter(recorded.clone()));
8634 Some(highlighter)
8635 }),
8636 cx,
8637 );
8638 s.replace_text_in_ranges(
8639 &[(0..3, "X".to_string()), (8..11, "Y".to_string())],
8640 window,
8641 cx,
8642 );
8643 });
8644 });
8645
8646 assert_eq!(
8647 *updates.borrow(),
8648 vec![vec!["aaa bbb Y".to_string(), "X bbb Y".to_string()]]
8649 );
8650 }
8651
8652 #[gpui::test]
8654 fn test_ime_composition_undoes_as_one_unit(cx: &mut TestAppContext) {
8655 let input_view = InputView::build_textarea(cx, |state| state.default_value(""));
8656 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8657 let input = input_view.input;
8658
8659 cx.update(|window, cx| {
8660 input.update(cx, |s, cx| {
8661 s.replace_and_mark_text_in_range(None, "n", Some(1..1), window, cx);
8663 s.replace_and_mark_text_in_range(None, "ni", Some(2..2), window, cx);
8664 s.replace_text_in_range(None, "你", window, cx);
8665 assert_eq!(s.value(), "你");
8666
8667 assert_eq!(s.undo_manager.undo_count(), 1);
8669
8670 s.undo(&Undo, window, cx);
8671 assert_eq!(s.value(), "");
8672
8673 s.redo(&Redo, window, cx);
8674 assert_eq!(s.value(), "你");
8675 });
8676 });
8677 }
8678
8679 #[gpui::test]
8682 fn test_edit_after_composition_is_separate_undo(cx: &mut TestAppContext) {
8683 let input_view = InputView::build_textarea(cx, |state| state.default_value(""));
8684 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8685 let input = input_view.input;
8686
8687 cx.update(|window, cx| {
8688 input.update(cx, |s, cx| {
8689 s.replace_and_mark_text_in_range(None, "n", Some(1..1), window, cx);
8690 s.replace_text_in_range(None, "你", window, cx);
8691 assert_eq!(s.value(), "你");
8692 assert_eq!(s.undo_manager.undo_count(), 1);
8693
8694 s.replace_text_in_range(None, "x", window, cx);
8696 assert_eq!(s.value(), "你x");
8697 assert_eq!(s.undo_manager.undo_count(), 2);
8698
8699 s.undo(&Undo, window, cx);
8700 assert_eq!(s.value(), "你");
8701 s.undo(&Undo, window, cx);
8702 assert_eq!(s.value(), "");
8703 });
8704 });
8705 }
8706
8707 #[gpui::test]
8710 fn test_composition_cancel_via_unmark_does_not_leak(cx: &mut TestAppContext) {
8711 let input_view = InputView::build_textarea(cx, |state| state.default_value(""));
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, |s, cx| {
8717 s.replace_and_mark_text_in_range(None, "n", Some(1..1), window, cx);
8719 s.unmark_text(window, cx);
8720 let after_cancel = s.undo_manager.undo_count();
8721
8722 s.replace_text_in_range(None, "x", window, cx);
8724 assert_eq!(s.undo_manager.undo_count(), after_cancel + 1);
8725 });
8726 });
8727 }
8728
8729 #[gpui::test]
8730 fn test_set_selected_range_clips_to_utf8_boundaries(cx: &mut TestAppContext) {
8731 let input_view = InputView::build(cx, |state| state.default_value("éx"));
8732 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8733 let input = input_view.input;
8734
8735 cx.update(|window, cx| {
8736 input.update(cx, |state, cx| {
8737 state.set_selected_range(0..1, cx);
8738 assert_eq!(state.selected_range(), 0..2);
8739 state.copy(&Copy, window, cx);
8740
8741 state.set_selected_range(1..1, cx);
8742 assert_eq!(state.selected_range(), 0..0);
8743 });
8744 });
8745 }
8746
8747 #[gpui::test]
8748 fn test_ime_selection_is_relative_to_replacement_start(cx: &mut TestAppContext) {
8749 let input_view = InputView::build(cx, |state| state.default_value("你好 "));
8750 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8751 let input = input_view.input;
8752
8753 cx.update(|window, cx| {
8754 input.update(cx, |state, cx| {
8755 state.set_selected_range(7..7, cx);
8756 state.replace_and_mark_text_in_range(None, "s", Some(1..1), window, cx);
8757 state.replace_and_mark_text_in_range(None, "sh", Some(2..2), window, cx);
8758
8759 assert_eq!(state.value(), "你好 sh");
8760 assert_eq!(state.selected_range(), 9..9);
8761 assert_eq!(state.ime_marked_range, Some((7..9).into()));
8762 });
8763 });
8764 }
8765
8766 #[gpui::test]
8767 fn test_undo_manager_composition_is_one_undo_group(cx: &mut TestAppContext) {
8768 let input_view = InputView::build(cx, |state| state);
8769 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8770 let input = input_view.input;
8771
8772 cx.update(|window, cx| {
8773 input.update(cx, |state, cx| {
8774 state.set_value("a", window, cx);
8775 state.replace_and_mark_text_in_range(None, "s", None, window, cx);
8776 state.replace_and_mark_text_in_range(None, "sh", None, window, cx);
8777 state.replace_text_in_range(None, "是", window, cx);
8778 assert_eq!(state.value(), "a是");
8779
8780 state.undo(&Undo, window, cx);
8781 assert_eq!(state.value(), "a");
8782 state.redo(&Redo, window, cx);
8783 assert_eq!(state.value(), "a是");
8784 });
8785 });
8786 }
8787
8788 #[gpui::test]
8789 fn test_undo_manager_consecutive_compositions_are_separate_groups(cx: &mut TestAppContext) {
8790 let input_view = InputView::build(cx, |state| state);
8791 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8792 let input = input_view.input;
8793
8794 cx.update(|window, cx| {
8795 input.update(cx, |state, cx| {
8796 state.replace_and_mark_text_in_range(None, "j", None, window, cx);
8798 state.replace_and_mark_text_in_range(None, "jin", None, window, cx);
8799 state.replace_text_in_range(None, "今天", window, cx);
8800 state.replace_and_mark_text_in_range(None, "w", None, window, cx);
8802 state.replace_and_mark_text_in_range(None, "wo", None, window, cx);
8803 state.replace_text_in_range(None, "我们", window, cx);
8804 assert_eq!(state.value(), "今天我们");
8805 assert_eq!(state.selected_range(), 12..12);
8806
8807 state.undo(&Undo, window, cx);
8808 assert_eq!(state.value(), "今天");
8809 assert_eq!(state.selected_range(), 6..6);
8810
8811 state.undo(&Undo, window, cx);
8812 assert_eq!(state.value(), "");
8813 assert_eq!(state.selected_range(), 0..0);
8814
8815 state.redo(&Redo, window, cx);
8816 assert_eq!(state.value(), "今天");
8817 assert_eq!(state.selected_range(), 6..6);
8818
8819 state.redo(&Redo, window, cx);
8820 assert_eq!(state.value(), "今天我们");
8821 assert_eq!(state.selected_range(), 12..12);
8822 });
8823 });
8824 }
8825
8826 #[gpui::test]
8827 fn test_undo_manager_typing_after_composition_is_a_separate_group(cx: &mut TestAppContext) {
8828 let input_view = InputView::build(cx, |state| state);
8829 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8830 let input = input_view.input;
8831
8832 cx.update(|window, cx| {
8833 input.update(cx, |state, cx| {
8834 state.replace_and_mark_text_in_range(None, "n", None, window, cx);
8835 state.replace_text_in_range(None, "你", window, cx);
8836 state.undo_manager.set_pending_intent(EditIntent::Typing);
8837 state.replace_text_in_range(None, "a", window, cx);
8838 state.undo_manager.set_pending_intent(EditIntent::Typing);
8839 state.replace_text_in_range(None, "b", window, cx);
8840 assert_eq!(state.value(), "你ab");
8841
8842 state.undo(&Undo, window, cx);
8843 assert_eq!(state.value(), "你");
8844
8845 state.undo(&Undo, window, cx);
8846 assert_eq!(state.value(), "");
8847 });
8848 });
8849 }
8850
8851 #[gpui::test]
8852 fn test_undo_manager_composition_cancel_leaves_no_entry(cx: &mut TestAppContext) {
8853 let input_view = InputView::build(cx, |state| state);
8854 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8855 let input = input_view.input;
8856
8857 cx.update(|window, cx| {
8858 input.update(cx, |state, cx| {
8859 state.set_value("a", window, cx);
8860 state.replace_and_mark_text_in_range(None, "s", None, window, cx);
8861 state.replace_and_mark_text_in_range(None, "", None, window, cx);
8862
8863 assert_eq!(state.value(), "a");
8864 assert!(!state.undo_manager.has_undos());
8865 });
8866 });
8867 }
8868
8869 #[gpui::test]
8871 fn test_undo_manager_ime_rewrites_of_typed_char_are_one_group(cx: &mut TestAppContext) {
8872 let input_view = InputView::build(cx, |state| state);
8873 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8874 let input = input_view.input;
8875
8876 cx.update(|window, cx| {
8877 input.update(cx, |state, cx| {
8878 state.set_value("é ", window, cx);
8879 state.set_selected_range(3..3, cx);
8880 state.replace_text_in_range(None, "ㅎ", window, cx);
8882 state.replace_text_in_range(Some(2..3), "하", window, cx);
8883 state.replace_text_in_range(Some(2..3), "한", window, cx);
8884 state.replace_text_in_range(None, "ㄱ", window, cx);
8885 state.replace_text_in_range(Some(3..4), "그", window, cx);
8886 state.replace_text_in_range(Some(3..4), "글", window, cx);
8887 assert_eq!(state.value(), "é 한글");
8888 assert_eq!(state.undo_manager.undo_count(), 1);
8889
8890 state.undo(&Undo, window, cx);
8891 assert_eq!(state.value(), "é ");
8892 assert_eq!(state.selected_range(), 3..3);
8893
8894 state.redo(&Redo, window, cx);
8895 assert_eq!(state.value(), "é 한글");
8896 assert_eq!(state.selected_range(), 9..9);
8897 });
8898 });
8899 }
8900
8901 #[gpui::test]
8902 fn test_undo_manager_rewrite_after_cursor_movement_is_separate(cx: &mut TestAppContext) {
8903 let input_view = InputView::build(cx, |state| state);
8904 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8905 let input = input_view.input;
8906
8907 cx.update(|window, cx| {
8908 input.update(cx, |state, cx| {
8909 state.replace_text_in_range(None, "c", window, cx);
8910 state.replace_text_in_range(None, "e", window, cx);
8911 state.left(&MoveLeft, window, cx);
8912 state.right(&MoveRight, window, cx);
8913 state.replace_text_in_range(Some(1..2), "é", window, cx);
8914 assert_eq!(state.value(), "cé");
8915
8916 state.undo(&Undo, window, cx);
8917 assert_eq!(state.value(), "ce");
8918 state.undo(&Undo, window, cx);
8919 assert_eq!(state.value(), "");
8920 });
8921 });
8922 }
8923
8924 #[gpui::test]
8925 fn test_undo_manager_selection_restored_by_undo_and_redo(cx: &mut TestAppContext) {
8926 let input_view = InputView::build(cx, |state| state);
8927 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8928 let input = input_view.input;
8929
8930 cx.update(|window, cx| {
8931 input.update(cx, |state, cx| {
8932 state.set_value("abc", window, cx);
8933 state.set_selected_range(1..2, cx);
8934 state.replace_text_in_range(None, "X", window, cx);
8935
8936 state.undo(&Undo, window, cx);
8937 assert_eq!(state.value(), "abc");
8938 assert_eq!(state.selected_range(), 1..2);
8939
8940 state.redo(&Redo, window, cx);
8941 assert_eq!(state.value(), "aXc");
8942 assert_eq!(state.selected_range(), 2..2);
8943 });
8944 });
8945 }
8946
8947 #[gpui::test]
8948 fn test_undo_manager_forward_delete_restores_cursor(cx: &mut TestAppContext) {
8949 let input_view = InputView::build(cx, |state| state);
8950 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8951 let input = input_view.input;
8952
8953 cx.update(|window, cx| {
8954 input.update(cx, |state, cx| {
8955 state.set_value("abc", window, cx);
8956 state.set_selected_range(1..1, cx);
8957 state.delete(&Delete, window, cx);
8958
8959 state.undo(&Undo, window, cx);
8960 assert_eq!(state.value(), "abc");
8961 assert_eq!(state.selected_range(), 1..1);
8962 });
8963 });
8964 }
8965
8966 #[gpui::test]
8967 fn test_undo_manager_selection_movement_preserves_redo(cx: &mut TestAppContext) {
8968 let input_view = InputView::build(cx, |state| state);
8969 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8970 let input = input_view.input;
8971
8972 cx.update(|window, cx| {
8973 input.update(cx, |state, cx| {
8974 state.replace_text_in_range(None, "ab", window, cx);
8975 state.undo(&Undo, window, cx);
8976 state.set_selected_range(0..0, cx);
8977 state.redo(&Redo, window, cx);
8978 assert_eq!(state.value(), "ab");
8979
8980 state.undo(&Undo, window, cx);
8981 state.replace_text_in_range(None, "x", window, cx);
8982 state.redo(&Redo, window, cx);
8983 assert_eq!(state.value(), "x");
8984 });
8985 });
8986 }
8987
8988 #[gpui::test]
8989 fn test_undo_manager_noop_edit_preserves_redo(cx: &mut TestAppContext) {
8990 let input_view = InputView::build(cx, |state| state);
8991 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8992 let input = input_view.input;
8993
8994 cx.update(|window, cx| {
8995 input.update(cx, |state, cx| {
8996 state.replace_text_in_range(None, "a", window, cx);
8997 state.move_to(0, None, cx);
8998 state.replace_text_in_range(None, "", window, cx);
8999 state.undo(&Undo, window, cx);
9000 assert_eq!(state.value(), "");
9001 state.redo(&Redo, window, cx);
9002 assert_eq!(state.value(), "a");
9003 assert_eq!(state.cursor(), 1);
9004 });
9005 });
9006 }
9007
9008 #[gpui::test]
9009 fn test_cursor_round_trip_stops_typing_coalescing(cx: &mut TestAppContext) {
9010 let input_view = InputView::build(cx, |state| state);
9011 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
9012 let input = input_view.input;
9013
9014 cx.update(|window, cx| {
9015 input.update(cx, |state, cx| {
9016 state.replace_text_in_range(None, "a", window, cx);
9017 state.left(&MoveLeft, window, cx);
9018 state.right(&MoveRight, window, cx);
9019 state.replace_text_in_range(None, "b", window, cx);
9020
9021 state.undo(&Undo, window, cx);
9022 assert_eq!(state.value(), "a");
9023 state.undo(&Undo, window, cx);
9024 assert_eq!(state.value(), "");
9025 });
9026 });
9027 }
9028
9029 #[gpui::test]
9030 fn test_undo_manager_noop_edit_breaks_coalescing_without_clearing_history(
9031 cx: &mut TestAppContext,
9032 ) {
9033 let input_view = InputView::build(cx, |state| state);
9034 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
9035 let input = input_view.input;
9036
9037 cx.update(|window, cx| {
9038 input.update(cx, |state, cx| {
9039 state.replace_text_in_range(None, "alpha", window, cx);
9040 state.replace_text_in_range(None, "", window, cx);
9041 state.replace_text_in_range(None, "beta", window, cx);
9042 assert_eq!(state.value(), "alphabeta");
9043
9044 state.undo(&Undo, window, cx);
9045 assert_eq!(state.value(), "alpha");
9046 state.undo(&Undo, window, cx);
9047 assert_eq!(state.value(), "");
9048 });
9049 });
9050 }
9051
9052 #[gpui::test]
9053 fn test_undo_manager_masked_redo_restores_actual_cursor(cx: &mut TestAppContext) {
9054 let input_view = InputView::build(cx, |state| {
9055 state.mask_pattern(MaskPattern::Number {
9056 separator: Some(','),
9057 fraction: None,
9058 })
9059 });
9060 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
9061 let input = input_view.input;
9062
9063 cx.update(|window, cx| {
9064 input.update(cx, |state, cx| {
9065 state.set_value("12345", window, cx);
9066 state.set_selected_range(2..2, cx);
9067 state.replace_text_in_range(None, "9", window, cx);
9068 let selection_after_edit = state.selected_range();
9069 assert_ne!(selection_after_edit.end, state.value().len());
9070
9071 state.undo(&Undo, window, cx);
9072 state.redo(&Redo, window, cx);
9073 assert_eq!(state.selected_range(), selection_after_edit);
9074 });
9075 });
9076 }
9077
9078 #[gpui::test]
9084 fn test_unfold_at(cx: &mut TestAppContext) {
9085 use crate::input::{FoldRange, Position};
9086
9087 let view = InputView::<EditorMode>::new(cx);
9088 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9089 let input = view.input;
9090
9091 cx.update(|window, cx| {
9094 input.update(cx, |state, cx| {
9095 state.set_value("a\nb\nc\nd\ne\nf\ng\nh\ni\nj\nk\nl", window, cx);
9096 state.apply_highlighter_fold_candidates(
9097 vec![
9098 FoldRange::new(0, 5),
9099 FoldRange::new(2, 4),
9100 FoldRange::new(7, 10),
9101 ],
9102 cx,
9103 );
9104 state.display_map.set_folded(0, true);
9105 state.display_map.set_folded(2, true);
9106 state.display_map.set_folded(7, true);
9107 });
9108 });
9109
9110 for line in [0, 5] {
9113 cx.update(|_, cx| {
9114 input.update(cx, |state, cx| {
9115 assert!(!state.display_map.is_buffer_line_hidden(line));
9116 assert!(!state.unfold_at(Position::new(line as u32, 0), cx));
9117 });
9118 input.read_with(cx, |state, _| {
9119 assert!(state.display_map.is_folded_at(0));
9120 assert!(state.display_map.is_folded_at(2));
9121 assert!(state.display_map.is_folded_at(7));
9122 });
9123 });
9124 }
9125
9126 cx.update(|_, cx| {
9129 input.update(cx, |state, cx| {
9130 assert!(state.unfold_at(Position::new(3, 0), cx));
9131 });
9132 input.read_with(cx, |state, _| {
9133 assert!(!state.display_map.is_buffer_line_hidden(3));
9134 assert!(!state.display_map.is_folded_at(0));
9135 assert!(!state.display_map.is_folded_at(2));
9136 assert!(state.display_map.is_folded_at(7));
9137 assert!(state.display_map.is_fold_candidate(0));
9139 assert!(state.display_map.is_fold_candidate(2));
9140 });
9141 });
9142
9143 cx.update(|_, cx| {
9145 input.update(cx, |state, cx| {
9146 assert!(!state.unfold_at(Position::new(3, 0), cx));
9147 });
9148 });
9149 }
9150
9151 #[gpui::test]
9156 fn test_blur_keeps_decorations(cx: &mut TestAppContext) {
9157 let view = InputView::<EditorMode>::new(cx);
9158 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9159 let input = view.input;
9160
9161 cx.update(|window, cx| {
9162 input.update(cx, |state, cx| {
9163 state.set_value("select 1", window, cx);
9164 let _collection = state.create_decorations_collection(
9165 vec![crate::input::TextDecoration::new(
9166 0..6,
9167 gpui::HighlightStyle {
9168 font_weight: Some(gpui::FontWeight::BOLD),
9169 ..Default::default()
9170 },
9171 )],
9172 cx,
9173 );
9174 state.present_hover(
9175 0..6,
9176 lsp_types::Hover {
9177 contents: lsp_types::HoverContents::Scalar(
9178 lsp_types::MarkedString::String("docs".into()),
9179 ),
9180 range: None,
9181 },
9182 cx,
9183 );
9184 assert!(state.hover_popover().is_some());
9185
9186 state.on_blur(window, cx);
9187
9188 assert!(
9189 state.hover_popover().is_none(),
9190 "blur should hide the hover popover"
9191 );
9192 let decorations = state.extras.decoration_layers();
9193 assert!(
9194 decorations.iter().any(|layer| !layer.is_empty()),
9195 "blur must not discard decorations"
9196 );
9197 });
9198 });
9199 }
9200
9201 #[gpui::test]
9206 fn test_kind_does_not_follow_the_row_count(cx: &mut TestAppContext) {
9207 let view = InputView::build_textarea(cx, |state| state.auto_grow(1, 1));
9208 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9209 view.input.read_with(&mut cx, |state, _| {
9210 assert!(state.is_multi_line());
9211 assert!(!state.is_single_line());
9212 assert!(!state.is_code_editor());
9213 });
9214 }
9215
9216 #[gpui::test]
9217 fn test_textarea_prewrap_prepares_content_rows_before_layout(cx: &mut TestAppContext) {
9218 cx.update(crate::init);
9219 for (value, expected_rows) in [("", 1), ("one\ntwo", 2)] {
9220 cx.open_window(size(px(400.), px(100.)), |window, cx| {
9221 let font = window.text_style().font();
9222 cx.new(|cx| {
9223 let mut state = crate::input::TextareaState::new(window, cx)
9224 .rows(12)
9225 .default_value(value);
9226 assert_eq!(state.display_map().wrap_row_count(), 0);
9227 state.prewrap(font, px(14.), px(120.), cx);
9228 assert_eq!(state.display_map().wrap_row_count(), expected_rows);
9229 assert_eq!(state.value(), value);
9230 state
9231 });
9232 gpui::EmptyView
9233 });
9234 }
9235 }
9236
9237 #[gpui::test]
9238 fn test_textarea_prewrap_respects_soft_wrap_before_layout(cx: &mut TestAppContext) {
9239 cx.update(crate::init);
9240 let value = "select id, name, email, phone from users where active\nlimit 10";
9241 for soft_wrap in [true, false] {
9242 cx.open_window(size(px(400.), px(100.)), |window, cx| {
9243 let font = window.text_style().font();
9244 cx.new(|cx| {
9245 let mut state = crate::input::TextareaState::new(window, cx)
9246 .rows(1)
9247 .soft_wrap(soft_wrap)
9248 .default_value(value);
9249 state.prewrap(font, px(14.), px(120.), cx);
9250 let rows = state.display_map().wrap_row_count();
9251 if soft_wrap {
9252 assert!(rows > 2, "the first logical line must wrap");
9253 } else {
9254 assert_eq!(rows, 2);
9255 }
9256 assert_eq!(state.value(), value);
9257 state
9258 });
9259 gpui::EmptyView
9260 });
9261 }
9262 }
9263
9264 #[gpui::test]
9265 fn test_prewrap_counts_wrapped_rows_before_the_first_paint(cx: &mut TestAppContext) {
9266 cx.update(crate::init);
9267 let text = "select id, name, email, phone from users where active\nlimit 10";
9268 for (soft_wrap, wraps) in [(true, true), (false, false)] {
9269 let mut input = None;
9270 cx.open_window(size(px(400.), px(100.)), |window, cx| {
9271 let font = window.text_style().font();
9272 let editor = cx.new(|cx| {
9273 let mut state = crate::input::EditorState::new(window, cx)
9274 .soft_wrap(soft_wrap)
9275 .default_value(text);
9276 state.prewrap(font, px(14.), px(120.), cx);
9277 state
9278 });
9279 input = Some(editor);
9280 gpui::EmptyView
9281 });
9282 let rows = input
9283 .unwrap()
9284 .read_with(cx, |state, _| state.display_map().wrap_row_count());
9285 assert_eq!(rows > 2, wraps, "soft_wrap({soft_wrap}): {rows} rows");
9287 if !wraps {
9288 assert_eq!(rows, 2);
9289 }
9290 }
9291 }
9292
9293 #[gpui::test]
9294 fn test_prewrap_uses_the_configured_tab_stops(cx: &mut TestAppContext) {
9295 cx.update(crate::init);
9296 cx.open_window(size(px(400.), px(100.)), |window, cx| {
9297 let font = window.text_style().font();
9298 let font_size = px(14.);
9299 let font_id = window.text_system().resolve_font(&font);
9300 let space_width = window.text_system().layout_width(font_id, font_size, ' ');
9301 cx.new(|cx| {
9302 let mut state = crate::input::EditorState::new(window, cx)
9303 .tab_size(TabSize {
9304 tab_size: 4,
9305 hard_tabs: true,
9306 })
9307 .default_value("\t\t");
9308 for (tab_size, expected_rows) in [(4, 2), (2, 1), (8, 2)] {
9309 state.set_tab_size(
9310 TabSize {
9311 tab_size,
9312 hard_tabs: true,
9313 },
9314 cx,
9315 );
9316 state.prewrap(font.clone(), font_size, space_width * 5. + RIGHT_MARGIN, cx);
9317 assert_eq!(state.display_map().wrap_row_count(), expected_rows);
9318 }
9319 state
9320 });
9321 gpui::EmptyView
9322 });
9323 }
9324
9325 #[gpui::test]
9328 fn test_prewrap_wraps_with_the_wrapping_indent(cx: &mut TestAppContext) {
9329 cx.update(crate::init);
9330 let text = " select id, name, email, phone from users where active";
9331 let mut rows = Vec::new();
9332 for indent in [WrappingIndent::Same, WrappingIndent::None] {
9333 let mut input = None;
9334 cx.open_window(size(px(400.), px(100.)), |window, cx| {
9335 let font = window.text_style().font();
9336 input = Some(cx.new(|cx| {
9337 let mut state = crate::input::EditorState::new(window, cx)
9338 .wrapping_indent(indent)
9339 .default_value(text);
9340 state.prewrap(font, px(14.), px(200.), cx);
9341 state
9342 }));
9343 gpui::EmptyView
9344 });
9345 rows.push(
9346 input
9347 .unwrap()
9348 .read_with(cx, |state, _| state.display_map().wrap_row_count()),
9349 );
9350 }
9351 assert!(rows[0] > rows[1], "rows with Same, None: {rows:?}");
9352 }
9353
9354 #[gpui::test]
9356 fn test_prewrap_does_nothing_once_laid_out(cx: &mut TestAppContext) {
9357 struct Frame(Entity<crate::input::EditorState>);
9358 impl Render for Frame {
9359 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
9360 crate::input::InputBase::new("frame")
9361 .h(px(100.))
9362 .child(crate::input::Editor::new(&self.0))
9363 }
9364 }
9365
9366 cx.update(crate::init);
9367 let text = "select id, name, email, phone from users where active\nlimit 10";
9368 let mut input = None;
9369 let window = cx.open_window(size(px(400.), px(100.)), |window, cx| {
9370 let state = cx.new(|cx| crate::input::EditorState::new(window, cx).default_value(text));
9371 input = Some(state.clone());
9372 Frame(state)
9373 });
9374 let input = input.unwrap();
9375 let mut cx = VisualTestContext::from_window(window.into(), cx);
9376 cx.update(|window, cx| window.draw(cx).clear(cx));
9377
9378 let laid_out = input.read_with(&cx, |state, _| state.display_map().wrap_row_count());
9379 assert!(laid_out >= 2, "laid out to {laid_out} rows");
9380 let font = cx.update(|window, _| window.text_style().font());
9381 input.update(&mut cx, |state, cx| {
9382 state.prewrap(font, px(14.), px(60.), cx)
9383 });
9384 assert_eq!(
9385 input.read_with(&cx, |state, _| state.display_map().wrap_row_count()),
9386 laid_out
9387 );
9388 }
9389
9390 #[gpui::test]
9395 fn test_soft_wrap_is_enabled_by_default(cx: &mut TestAppContext) {
9396 let textarea = InputView::build_textarea(cx, |state| state);
9397 let mut textarea_cx = VisualTestContext::from_window(textarea.window_handle.into(), cx);
9398 textarea
9399 .input
9400 .read_with(&mut textarea_cx, |state, _| assert!(state.soft_wrap));
9401
9402 let editor = InputView::<EditorMode>::new(cx);
9403 let mut editor_cx = VisualTestContext::from_window(editor.window_handle.into(), cx);
9404 editor
9405 .input
9406 .read_with(&mut editor_cx, |state, _| assert!(state.soft_wrap));
9407 }
9408
9409 fn parse_cursor_spec(input: &str) -> (String, Vec<usize>) {
9413 let mut full_text = String::new();
9414 let mut cursor_offsets = Vec::new();
9415 let non_empty_lines: Vec<&str> = input.lines().filter(|l| !l.is_empty()).collect();
9416
9417 for (line_idx, line) in non_empty_lines.iter().enumerate() {
9418 let mut positions = Vec::new();
9419 let mut text = String::new();
9420 for ch in line.chars() {
9421 if ch == '|' {
9422 positions.push(text.len());
9423 } else {
9424 text.push(ch);
9425 }
9426 }
9427
9428 if line_idx > 0 {
9429 full_text.push('\n');
9430 }
9431 let line_start = full_text.len();
9432 for pos in positions {
9433 cursor_offsets.push(line_start + pos);
9434 }
9435 full_text.push_str(&text);
9436 }
9437 full_text.push('\n');
9438
9439 (full_text, cursor_offsets)
9440 }
9441
9442 fn multi_line(cx: &mut TestAppContext) -> InputView<TextareaMode> {
9444 InputView::build_textarea(cx, |state| state)
9445 }
9446
9447 fn setup_cursors<M: InputModeKind>(
9449 cx: &mut VisualTestContext,
9450 input: &Entity<InputBaseState<M>>,
9451 spec: &str,
9452 ) {
9453 let (full_text, offsets) = parse_cursor_spec(spec);
9454 cx.update(|window, cx| {
9455 input.update(cx, |state, cx| {
9456 state.set_value(&full_text, window, cx);
9457 let selections = offsets
9458 .into_iter()
9459 .map(|offset| {
9460 CursorSelection::new(state.selections.generate_id(), offset, offset)
9461 })
9462 .collect();
9463 state.selections.replace_all(selections);
9464 cx.notify();
9465 });
9466 });
9467 }
9468
9469 #[track_caller]
9471 fn assert_cursors<M: InputModeKind>(
9472 cx: &mut VisualTestContext,
9473 input: &Entity<InputBaseState<M>>,
9474 spec: &str,
9475 ) {
9476 let (expected_text, mut expected_cursors) = parse_cursor_spec(spec);
9477 expected_cursors.sort();
9478
9479 let (actual_text, mut actual_cursors) = input.read_with(cx, |state, _| {
9480 (
9481 state.text.to_string(),
9482 state
9483 .selections
9484 .iter()
9485 .map(|s| s.cursor_offset())
9486 .collect::<Vec<_>>(),
9487 )
9488 });
9489 actual_cursors.sort();
9490
9491 assert_eq!(
9492 actual_text, expected_text,
9493 "Text mismatch:\nExpected: {expected_text:?}\nActual: {actual_text:?}"
9494 );
9495 assert_eq!(
9496 actual_cursors, expected_cursors,
9497 "Cursor mismatch:\nExpected: {expected_cursors:?}\nActual: {actual_cursors:?}"
9498 );
9499 }
9500
9501 #[gpui::test]
9502 fn test_alt_drag_selects_a_block_and_replaces_each_row(cx: &mut TestAppContext) {
9503 cx.update(crate::init);
9504 let view = InputView::<EditorMode>::new(cx);
9505 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9506 for modifiers in [
9507 gpui::Modifiers {
9508 alt: true,
9509 ..Default::default()
9510 },
9511 gpui::Modifiers {
9512 alt: true,
9513 shift: true,
9514 ..Default::default()
9515 },
9516 #[cfg(target_os = "linux")]
9517 gpui::Modifiers {
9518 alt: true,
9519 control: true,
9520 ..Default::default()
9521 },
9522 ] {
9523 setup_cursors(&mut cx, &view.input, "|abcd\nabcd\nabcd");
9524 cx.update(|window, cx| {
9525 view.input.update(cx, |state, cx| state.focus(window, cx));
9526 });
9527 let (start, end) = view.input.read_with(&cx, |state, _| {
9528 let layout = state.last_layout.as_ref().unwrap();
9529 let origin = state.last_bounds.unwrap().origin;
9530 let position = |row: usize, col| {
9531 let local = layout.lines[row]
9532 .position_for_index(col, layout, false)
9533 .unwrap();
9534 origin
9535 + point(
9536 layout.line_number_width + local.x,
9537 layout.line_height * (row as f32 + 0.5),
9538 )
9539 };
9540 (position(0, 1), position(2, 3))
9541 });
9542 let end = point(end.x - px(0.5), end.y);
9545 cx.update(|_, cx| {
9547 view.input.update(cx, |state, _| {
9548 state.extras.hover_definition.update(
9549 0..4,
9550 vec![lsp_types::LocationLink {
9551 origin_selection_range: None,
9552 target_uri: "file:///tmp/column-selection.rs".parse().unwrap(),
9553 target_range: Default::default(),
9554 target_selection_range: Default::default(),
9555 }],
9556 );
9557 });
9558 });
9559 cx.simulate_mouse_down(start, MouseButton::Left, modifiers);
9560 cx.simulate_mouse_move(end, MouseButton::Left, modifiers);
9561 cx.simulate_mouse_up(end, MouseButton::Left, modifiers);
9562 view.input.read_with(&cx, |state, _| {
9563 let ranges: Vec<_> = state
9564 .selections
9565 .iter()
9566 .map(|sel| sel.start..sel.end)
9567 .collect();
9568 assert_eq!(ranges, vec![1..3, 6..8, 11..13]);
9569 });
9570 cx.simulate_mouse_move(start, None, modifiers);
9572 cx.simulate_keystrokes("x");
9573 assert_cursors(&mut cx, &view.input, "ax|d\nax|d\nax|d");
9574 }
9575 }
9576
9577 #[gpui::test]
9578 fn test_alt_drag_extends_upward_from_an_existing_cursor(cx: &mut TestAppContext) {
9579 cx.update(crate::init);
9580 let view = InputView::<EditorMode>::new(cx);
9581 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9582 setup_cursors(&mut cx, &view.input, "abcd\nabcd\na|bcd");
9583 cx.update(|window, cx| {
9584 view.input.update(cx, |state, cx| state.focus(window, cx));
9585 });
9586 let (start, end) = view.input.read_with(&cx, |state, _| {
9587 let layout = state.last_layout.as_ref().unwrap();
9588 let origin = state.last_bounds.unwrap().origin;
9589 let local = layout.lines[0]
9590 .position_for_index(1, layout, false)
9591 .unwrap();
9592 let x = layout.line_number_width + local.x;
9593 (
9594 origin + point(x, layout.line_height * 2.5),
9595 origin + point(x, layout.line_height * 0.5),
9596 )
9597 });
9598 let modifiers = gpui::Modifiers {
9599 alt: true,
9600 ..Default::default()
9601 };
9602 cx.simulate_mouse_down(start, MouseButton::Left, modifiers);
9603 cx.simulate_mouse_move(end, MouseButton::Left, modifiers);
9604 cx.simulate_mouse_up(end, MouseButton::Left, modifiers);
9605 assert_cursors(&mut cx, &view.input, "a|bcd\na|bcd\na|bcd");
9606 }
9607
9608 #[gpui::test]
9609 fn test_alt_drag_over_a_short_row_keeps_the_block_width(cx: &mut TestAppContext) {
9610 cx.update(crate::init);
9611 let view = InputView::<EditorMode>::new(cx);
9612 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9613 setup_cursors(&mut cx, &view.input, "abcdef\nab\nabcdef");
9614 cx.update(|window, cx| {
9615 view.input.update(cx, |state, cx| state.focus(window, cx));
9616 });
9617 let (start, end) = view.input.read_with(&cx, |state, _| {
9620 let layout = state.last_layout.as_ref().unwrap();
9621 let origin = state.last_bounds.unwrap().origin;
9622 let x_of = |index| {
9623 layout.line_number_width
9624 + layout.lines[0]
9625 .position_for_index(index, layout, false)
9626 .unwrap()
9627 .x
9628 };
9629 (
9630 origin + point(x_of(1), layout.line_height * 0.5),
9631 origin + point(x_of(5), layout.line_height * 1.5),
9632 )
9633 });
9634 let modifiers = gpui::Modifiers {
9635 alt: true,
9636 shift: true,
9637 ..Default::default()
9638 };
9639 cx.simulate_mouse_down(start, MouseButton::Left, modifiers);
9640 cx.simulate_mouse_move(end, MouseButton::Left, modifiers);
9641 cx.simulate_mouse_up(end, MouseButton::Left, modifiers);
9642
9643 view.input.read_with(&cx, |state, _| {
9645 let ranges: Vec<_> = state.selections.iter().map(|s| (s.start, s.end)).collect();
9646 assert_eq!(ranges, vec![(1, 5), (8, 9)]);
9647 });
9648 }
9649
9650 #[gpui::test]
9651 fn test_alt_mouse_release_outside_editor_ends_column_selection(cx: &mut TestAppContext) {
9652 cx.update(crate::init);
9653 let view = InputView::<EditorMode>::new(cx);
9654 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9655 setup_cursors(&mut cx, &view.input, "|abcd\nabcd");
9656 let position = view.input.read_with(&cx, |state, _| {
9657 let layout = state.last_layout.as_ref().unwrap();
9658 state.last_bounds.unwrap().origin
9659 + point(layout.line_number_width + px(2.), layout.line_height * 0.5)
9660 });
9661 let modifiers = gpui::Modifiers {
9662 alt: true,
9663 ..Default::default()
9664 };
9665 cx.simulate_mouse_down(position, MouseButton::Left, modifiers);
9666 cx.simulate_mouse_up(point(px(-100.), px(-100.)), MouseButton::Left, modifiers);
9667 view.input.read_with(&cx, |state, _| {
9668 assert!(!state.selecting);
9669 assert!(state.column_select_start.is_none());
9670 });
9671 }
9672
9673 #[gpui::test]
9674 fn test_consumed_keystrokes_keep_cursor_visible(cx: &mut TestAppContext) {
9675 let view = InputView::<EditorMode>::new(cx);
9676 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9677 setup_cursors(&mut cx, &view.input, "a|b");
9678 cx.update(|window, cx| {
9679 view.input.update(cx, |state, cx| {
9680 state.focus(window, cx);
9681 state.pause_blink_cursor(cx);
9682 });
9683 });
9684 cx.run_until_parked();
9685 cx.executor()
9686 .advance_clock(std::time::Duration::from_millis(300));
9687 cx.run_until_parked();
9688 view.input.read_with(&cx, |state, cx| {
9689 assert!(!state.blink_cursor.read(cx).visible());
9690 });
9691 for _ in 0..5 {
9693 #[cfg(target_os = "macos")]
9694 cx.simulate_keystrokes("cmd-c");
9695 #[cfg(not(target_os = "macos"))]
9696 cx.simulate_keystrokes("ctrl-c");
9697 cx.run_until_parked();
9698 cx.executor()
9699 .advance_clock(std::time::Duration::from_millis(200));
9700 cx.run_until_parked();
9701 view.input.read_with(&cx, |state, cx| {
9702 assert!(state.blink_cursor.read(cx).visible());
9703 });
9704 }
9705 }
9706
9707 #[gpui::test]
9708 fn test_mouse_down_shows_cursor_like_a_cursor_move(cx: &mut TestAppContext) {
9709 cx.update(crate::init);
9710 let view = InputView::<EditorMode>::new(cx);
9711 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9712 setup_cursors(&mut cx, &view.input, "abcd|efgh");
9713 let position = |offset: Pixels, cx: &mut VisualTestContext| {
9714 view.input.read_with(cx, |state, _| {
9715 let layout = state.last_layout.as_ref().unwrap();
9716 state.last_bounds.unwrap().origin
9717 + point(layout.line_number_width + offset, layout.line_height * 0.5)
9718 })
9719 };
9720 let visible = |cx: &mut VisualTestContext| {
9721 view.input
9722 .read_with(cx, |state, cx| state.blink_cursor.read(cx).visible())
9723 };
9724
9725 let first = position(px(2.), &mut cx);
9728 cx.simulate_mouse_down(first, MouseButton::Left, Default::default());
9729 assert!(visible(&mut cx));
9730 cx.simulate_mouse_up(first, MouseButton::Left, Default::default());
9731 cx.run_until_parked();
9732
9733 cx.executor()
9736 .advance_clock(std::time::Duration::from_millis(400));
9737 cx.run_until_parked();
9738 assert!(!visible(&mut cx));
9739 let second = position(px(30.), &mut cx);
9740 cx.simulate_mouse_down(second, MouseButton::Left, Default::default());
9741 assert!(visible(&mut cx));
9742 cx.executor()
9743 .advance_clock(std::time::Duration::from_millis(250));
9744 cx.run_until_parked();
9745 assert!(visible(&mut cx));
9746 cx.simulate_mouse_up(second, MouseButton::Left, Default::default());
9747 }
9748
9749 #[gpui::test]
9750 fn test_multi_cursor_actions_reveal_hidden_carets(cx: &mut TestAppContext) {
9751 let view = InputView::<EditorMode>::new(cx);
9752 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9753 setup_cursors(&mut cx, &view.input, "ab\na|b\nab");
9754 for action in 0..6 {
9757 cx.update(|_, cx| {
9758 view.input.update(cx, |state, cx| {
9759 state.blink_cursor = cx.new(|_| BlinkCursor::new());
9760 state.blink_cursor.update(cx, |cursor, cx| cursor.start(cx));
9761 });
9762 });
9763 cx.run_until_parked();
9764 cx.executor()
9765 .advance_clock(std::time::Duration::from_millis(500));
9766 cx.run_until_parked();
9767 view.input.read_with(&cx, |state, cx| {
9768 assert!(!state.blink_cursor.read(cx).visible(), "action {action}");
9769 });
9770
9771 cx.update(|window, cx| {
9772 view.input.update(cx, |state, cx| {
9773 match action {
9774 0 => state.add_cursor_above(&AddCursorAbove, window, cx),
9775 1 => state.add_cursor_below(&AddCursorBelow, window, cx),
9776 2 => state.select_up(&SelectUp, window, cx),
9777 3 => state.select_down(&SelectDown, window, cx),
9778 4 => state.replace_text_in_range(None, "x", window, cx),
9779 _ => state.backspace(&Backspace, window, cx),
9780 }
9781 assert!(state.blink_cursor.read(cx).visible(), "action {action}");
9782 });
9783 });
9784 }
9785 }
9786
9787 #[gpui::test]
9788 fn test_multi_cursor_keyboard_dispatch(cx: &mut TestAppContext) {
9789 let view = InputView::<EditorMode>::new(cx);
9790 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9791 setup_cursors(&mut cx, &view.input, "ab\na|b\nab");
9792 cx.update(|window, cx| {
9793 view.input.update(cx, |state, cx| state.focus(window, cx));
9794 });
9795 #[cfg(target_os = "macos")]
9796 cx.simulate_keystrokes("cmd-alt-up");
9797 #[cfg(target_os = "windows")]
9798 cx.simulate_keystrokes("ctrl-alt-up");
9799 #[cfg(not(any(target_os = "macos", target_os = "windows")))]
9800 cx.simulate_keystrokes("alt-shift-up");
9801 view.input
9802 .read_with(&cx, |state, _| assert_eq!(state.selections.len(), 2));
9803 #[cfg(target_os = "macos")]
9804 cx.simulate_keystrokes("cmd-alt-down");
9805 #[cfg(target_os = "windows")]
9806 cx.simulate_keystrokes("ctrl-alt-down");
9807 #[cfg(not(any(target_os = "macos", target_os = "windows")))]
9808 cx.simulate_keystrokes("alt-shift-down");
9809 view.input
9810 .read_with(&cx, |state, _| assert_eq!(state.selections.len(), 3));
9811 cx.simulate_keystrokes("x");
9812 assert_cursors(&mut cx, &view.input, "ax|b\nax|b\nax|b");
9813 }
9814
9815 #[gpui::test]
9816 fn test_multi_cursor_platform_word_selection_dispatch(cx: &mut TestAppContext) {
9817 let view = InputView::<EditorMode>::new(cx);
9818 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9819 setup_cursors(&mut cx, &view.input, "one |two\none |two");
9820 cx.update(|window, cx| {
9821 view.input.update(cx, |state, cx| state.focus(window, cx));
9822 });
9823 #[cfg(any(target_os = "macos", target_os = "linux"))]
9824 cx.simulate_keystrokes("alt-shift-right");
9825 #[cfg(not(any(target_os = "macos", target_os = "linux")))]
9826 cx.simulate_keystrokes("ctrl-shift-right");
9827 cx.simulate_keystrokes("x");
9828 assert_cursors(&mut cx, &view.input, "one x|\none x|");
9829 setup_cursors(&mut cx, &view.input, "one two|\none two|");
9830 #[cfg(any(target_os = "macos", target_os = "linux"))]
9831 cx.simulate_keystrokes("alt-shift-left");
9832 #[cfg(not(any(target_os = "macos", target_os = "linux")))]
9833 cx.simulate_keystrokes("ctrl-shift-left");
9834 cx.simulate_keystrokes("x");
9835 assert_cursors(&mut cx, &view.input, "one x|\none x|");
9836 }
9837
9838 #[cfg(not(target_os = "macos"))]
9839 #[gpui::test]
9840 fn test_multi_cursor_horizontal_selection_dispatch(cx: &mut TestAppContext) {
9841 let view = InputView::<EditorMode>::new(cx);
9842 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9843 setup_cursors(&mut cx, &view.input, "ab\na|b");
9844 cx.update(|window, cx| {
9845 view.input.update(cx, |state, cx| state.focus(window, cx));
9846 });
9847 #[cfg(target_os = "windows")]
9848 cx.simulate_keystrokes("ctrl-alt-up");
9849 #[cfg(not(target_os = "windows"))]
9850 cx.simulate_keystrokes("alt-shift-up");
9851 #[cfg(target_os = "linux")]
9852 cx.simulate_keystrokes("shift-right");
9853 #[cfg(not(target_os = "linux"))]
9854 cx.simulate_keystrokes("alt-shift-right");
9855 view.input.read_with(&cx, |state, _| {
9856 assert_eq!(
9857 state
9858 .selections
9859 .iter()
9860 .map(|s| s.start..s.end)
9861 .collect::<Vec<_>>(),
9862 vec![4..5, 1..2]
9863 );
9864 });
9865 #[cfg(target_os = "linux")]
9866 cx.simulate_keystrokes("shift-left shift-left");
9867 #[cfg(not(target_os = "linux"))]
9868 cx.simulate_keystrokes("alt-shift-left alt-shift-left");
9869 view.input.read_with(&cx, |state, _| {
9870 assert_eq!(
9871 state
9872 .selections
9873 .iter()
9874 .map(|s| s.start..s.end)
9875 .collect::<Vec<_>>(),
9876 vec![3..4, 0..1]
9877 );
9878 });
9879 cx.simulate_keystrokes("x");
9880 assert_cursors(&mut cx, &view.input, "x|b\nx|b");
9881 }
9882
9883 #[gpui::test]
9884 fn test_multi_cursor_alt_click_dispatch(cx: &mut TestAppContext) {
9885 cx.update(crate::init);
9886 let view = InputView::<EditorMode>::new(cx);
9887 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9888 setup_cursors(&mut cx, &view.input, "a|b\nab\nab");
9889 cx.update(|window, cx| {
9890 view.input.update(cx, |state, cx| state.focus(window, cx));
9891 });
9892 let position = view.input.read_with(&cx, |state, _| {
9893 let bounds = state.last_bounds.unwrap();
9894 let layout = state.last_layout.as_ref().unwrap();
9895 bounds.origin + point(layout.line_number_width + px(2.), layout.line_height * 1.5)
9896 });
9897 cx.simulate_click(
9898 position,
9899 gpui::Modifiers {
9900 alt: true,
9901 ..Default::default()
9902 },
9903 );
9904 view.input
9905 .read_with(&cx, |state, _| assert_eq!(state.selections.len(), 2));
9906 }
9907
9908 #[gpui::test]
9909 fn test_add_selection(cx: &mut TestAppContext) {
9910 let view = InputView::<EditorMode>::new(cx);
9911 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9912 cx.update(|window, cx| {
9913 view.input.update(cx, |state, cx| {
9914 state.focus(window, cx);
9915 state.set_value("foo bar foo", window, cx);
9916 state.set_selected_range(0..3, cx);
9917 state.add_selection(8..11, cx);
9919 state.add_selection(0..3, cx);
9921 });
9922 });
9923 view.input.read_with(&cx, |state, _| {
9924 let mut ranges: Vec<Range<usize>> =
9925 state.selections.iter().map(|s| s.start..s.end).collect();
9926 ranges.sort_by_key(|r| r.start);
9927 assert_eq!(ranges, vec![0..3, 8..11]);
9928 });
9929 cx.simulate_keystrokes("x");
9931 view.input.read_with(&cx, |state, _| {
9932 assert_eq!(state.text.to_string(), "x bar x")
9933 });
9934 }
9935
9936 #[gpui::test]
9937 fn test_add_selection_preserves_crlf_boundaries(cx: &mut TestAppContext) {
9938 let view = InputView::build_textarea(cx, |state| state.default_value("a\r\nb"));
9939 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9940 cx.update(|window, cx| {
9941 view.input.update(cx, |state, cx| {
9942 state.focus(window, cx);
9943 state.set_selected_range(4..4, cx);
9944 state.add_selection(2..2, cx);
9945 let ranges: Vec<_> = state.selections.iter().map(|s| s.start..s.end).collect();
9946 assert_eq!(ranges, vec![4..4, 1..1]);
9947 });
9948 });
9949 cx.simulate_keystrokes("x");
9950 view.input.read_with(&cx, |state, _| {
9951 assert_eq!(state.value(), "ax\r\nbx");
9952 });
9953 cx.update(|window, cx| {
9954 view.input.update(cx, |state, cx| {
9955 state.set_value("a\r\nb", window, cx);
9956 state.set_selected_range(4..4, cx);
9957 state.add_selection(2..3, cx);
9958 state.cut(&Cut, window, cx);
9959 assert_eq!(cx.read_from_clipboard().unwrap().text().unwrap(), "\r\n");
9960 assert_eq!(state.value(), "ab");
9961 });
9962 });
9963 }
9964
9965 #[gpui::test]
9966 fn test_add_selection_merges_overlaps_before_copy_and_cut(cx: &mut TestAppContext) {
9967 let view = InputView::<EditorMode>::new(cx);
9968 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9969 cx.update(|window, cx| {
9970 view.input.update(cx, |state, cx| {
9971 for additions in [vec![2..5], vec![0..5], vec![4..5, 2..4]] {
9974 state.set_value("abcdef", window, cx);
9975 state.set_selected_range(0..3, cx);
9976 let active_id = state.active_selection().id;
9977 for range in additions {
9978 state.add_selection(range, cx);
9979 }
9980 assert_eq!(state.active_selection().id, active_id);
9981 assert_eq!(state.selected_range(), 0..5);
9982 assert_eq!(state.selections.len(), 1);
9983 state.copy(&Copy, window, cx);
9984 assert_eq!(cx.read_from_clipboard().unwrap().text().unwrap(), "abcde");
9985 state.cut(&Cut, window, cx);
9986 assert_eq!(cx.read_from_clipboard().unwrap().text().unwrap(), "abcde");
9987 assert_eq!(state.value(), "f");
9988 state.undo(&Undo, window, cx);
9989 assert_eq!(state.value(), "abcdef");
9990 assert_eq!(state.selected_range(), 0..5);
9991 }
9992 });
9993 });
9994 }
9995
9996 #[gpui::test]
9997 fn test_add_selection_clears_inline_completion(cx: &mut TestAppContext) {
9998 use crate::input::CompletionProvider;
9999 use gpui::Task;
10000 use lsp_types::{
10001 CompletionContext, CompletionResponse, InlineCompletionContext, InlineCompletionItem,
10002 InlineCompletionResponse,
10003 };
10004 use std::time::Duration;
10005
10006 struct Provider(Rc<Cell<usize>>);
10007
10008 impl CompletionProvider for Provider {
10009 fn is_completion_trigger(&self, _: usize, _: &str, _: &mut App) -> bool {
10010 false
10011 }
10012
10013 fn completions(
10014 &self,
10015 _: &Rope,
10016 _: usize,
10017 _: CompletionContext,
10018 _: &mut Window,
10019 _: &mut App,
10020 ) -> Task<anyhow::Result<CompletionResponse>> {
10021 Task::ready(Ok(CompletionResponse::Array(vec![])))
10022 }
10023
10024 fn inline_completion(
10025 &self,
10026 _: &Rope,
10027 _: usize,
10028 _: InlineCompletionContext,
10029 _: &mut Window,
10030 _: &mut App,
10031 ) -> Task<anyhow::Result<InlineCompletionResponse>> {
10032 self.0.set(self.0.get() + 1);
10033 Task::ready(Ok(InlineCompletionResponse::Array(vec![])))
10034 }
10035 }
10036
10037 let view = InputView::<EditorMode>::new(cx);
10038 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10039 let calls = Rc::new(Cell::new(0));
10040 cx.update(|window, cx| {
10041 view.input.update(cx, |state, cx| {
10042 state.focus(window, cx);
10043 state.set_value("aa\naa", window, cx);
10044 state.set_selected_range(2..2, cx);
10045 state.extras.inline_completion.item = Some(InlineCompletionItem {
10046 insert_text: "suggestion".into(),
10047 filter_text: None,
10048 range: None,
10049 command: None,
10050 insert_text_format: None,
10051 });
10052 state.add_selection(5..5, cx);
10053 assert!(!state.has_inline_completion());
10054 state.indent_inline(&IndentInline, window, cx);
10055 assert_eq!(state.value(), "aa \naa ");
10056 assert_eq!(state.selections.len(), 2);
10057
10058 state.set_value("aa\naa", window, cx);
10061 state.set_selected_range(2..2, cx);
10062 state.extras.lsp.completion_provider = Some(Rc::new(Provider(calls.clone())));
10063 state.schedule_inline_completion(window, cx);
10064 });
10065 });
10066 cx.run_until_parked();
10067 cx.update(|_, cx| {
10068 view.input
10069 .update(cx, |state, cx| state.add_selection(5..5, cx));
10070 });
10071 cx.executor().advance_clock(Duration::from_secs(1));
10072 cx.run_until_parked();
10073 assert_eq!(calls.get(), 0);
10074 view.input.read_with(&cx, |state, _| {
10075 assert!(!state.has_inline_completion());
10076 assert_eq!(state.selections.len(), 2);
10077 assert_eq!(state.value(), "aa\naa");
10078 });
10079 }
10080
10081 #[gpui::test]
10082 fn test_word_delete_undo_restores_caret(cx: &mut TestAppContext) {
10083 let view = multi_line(cx);
10084 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10085 setup_cursors(&mut cx, &view.input, "hello|");
10086 cx.update(|window, cx| {
10087 view.input.update(cx, |state, cx| {
10088 state.delete_previous_word(&DeleteToPreviousWordStart, window, cx);
10089 state.undo(&Undo, window, cx);
10090 assert_eq!(state.selected_range(), 5..5);
10091 });
10092 });
10093 }
10094
10095 #[gpui::test]
10096 fn test_merged_delete_undo_restores_all_carets(cx: &mut TestAppContext) {
10097 let view = multi_line(cx);
10098 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10099 setup_cursors(&mut cx, &view.input, "a|b|c");
10100 cx.update(|window, cx| {
10101 view.input.update(cx, |state, cx| {
10102 state.backspace(&Backspace, window, cx);
10103 state.undo(&Undo, window, cx);
10104 });
10105 });
10106 assert_cursors(&mut cx, &view.input, "a|b|c");
10107 }
10108
10109 #[gpui::test]
10110 fn test_shift_end_respects_soft_wrap_end(cx: &mut TestAppContext) {
10111 let view = InputView::<EditorMode>::new(cx);
10112 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10113 cx.update(|window, cx| {
10114 view.input.update(cx, |state, cx| {
10115 state.set_value("abcdef ".repeat(30), window, cx);
10116 state.display_map.on_layout_changed(Some(px(60.)), cx);
10117 let line = state.display_map.line(0).unwrap();
10118 assert!(line.wrapped_lines.len() > 1);
10119 let boundary = line.wrapped_lines[0].end;
10120 state.move_to_with_affinity(boundary, None, true, cx);
10121 state.select_to_end_of_line(&SelectToEndOfLine, window, cx);
10122 assert_eq!(state.selected_range(), boundary..state.text.len());
10123 });
10124 });
10125 }
10126
10127 #[gpui::test]
10128 fn test_outdent_unindented_unicode_is_unchanged(cx: &mut TestAppContext) {
10129 let view = multi_line(cx);
10130 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10131 setup_cursors(&mut cx, &view.input, "|你好\n|世界");
10132 cx.update(|window, cx| {
10133 view.input.update(cx, |state, cx| {
10134 state.outdent(false, window, cx);
10135 state.outdent(true, window, cx);
10136 });
10137 });
10138 assert_cursors(&mut cx, &view.input, "|你好\n|世界");
10139 }
10140
10141 #[gpui::test]
10142 fn test_column_selection_stays_on_unicode_boundaries(cx: &mut TestAppContext) {
10143 let view = multi_line(cx);
10144 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10145 setup_cursors(&mut cx, &view.input, "ab\n你好\ncd");
10146 cx.update(|window, cx| {
10147 view.input.update(cx, |state, cx| {
10148 state.build_columnar_selection(
10149 ColumnarPoint::new(1, 0),
10150 ColumnarPoint::new(11, 0),
10151 cx,
10152 );
10153 let text = state.value();
10154 for sel in state.selections.iter() {
10155 assert!(text.is_char_boundary(sel.start));
10156 assert!(text.is_char_boundary(sel.end));
10157 }
10158 state.replace_text_in_range(None, "X", window, cx);
10159 });
10160 });
10161 }
10162
10163 #[gpui::test]
10164 fn test_editor_decorations_follow_typing(cx: &mut TestAppContext) {
10165 let view = InputView::<EditorMode>::new(cx);
10166 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10167 cx.update(|window, cx| {
10168 view.input.update(cx, |state, cx| {
10169 state.set_value("abc def", window, cx);
10170 state.create_decorations_collection(
10171 vec![crate::input::TextDecoration::new(
10172 4..7,
10173 gpui::HighlightStyle::default(),
10174 )],
10175 cx,
10176 );
10177 state.set_selected_range(0..0, cx);
10178 state.replace_text_in_range(None, "X", window, cx);
10179 let layers = state.extras.decoration_layers();
10180 assert_eq!(layers.into_iter().flatten().next().unwrap().range, 5..8);
10181 });
10182 });
10183 }
10184
10185 #[gpui::test]
10186 fn test_backspace_preserves_escaped_quote_terminator(cx: &mut TestAppContext) {
10187 let view = InputView::<EditorMode>::new(cx);
10188 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10189 setup_cursors(&mut cx, &view.input, r#"{"s":"\"|"}"#);
10190 cx.update(|window, cx| {
10191 view.input.update(cx, |state, cx| {
10192 set_test_syntax_provider(std::rc::Rc::new(StringAllProvider), cx);
10193 state.backspace(&Backspace, window, cx);
10194 });
10195 });
10196 assert_cursors(&mut cx, &view.input, r#"{"s":"\|"}"#);
10197 }
10198
10199 #[gpui::test]
10200 fn test_ordinary_typing_does_not_query_syntax(cx: &mut TestAppContext) {
10201 struct UnexpectedQuery;
10202 impl crate::input::SyntaxContextProvider for UnexpectedQuery {
10203 fn context_at(&self, _: &ropey::Rope, _: usize) -> crate::input::SyntaxContext {
10204 panic!("ordinary typing must not query syntax");
10205 }
10206 }
10207 let view = InputView::<EditorMode>::new(cx);
10208 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10209 setup_cursors(&mut cx, &view.input, "|");
10210 cx.update(|window, cx| {
10211 view.input.update(cx, |state, cx| {
10212 set_test_syntax_provider(std::rc::Rc::new(UnexpectedQuery), cx);
10213 for c in ["a", "b", "c"] {
10214 state.replace_text_in_range(None, c, window, cx);
10215 }
10216 });
10217 });
10218 assert_cursors(&mut cx, &view.input, "abc|");
10219 }
10220
10221 #[gpui::test]
10222 fn test_auto_close_is_atomic_at_history_limit(cx: &mut TestAppContext) {
10223 let view = InputView::<EditorMode>::new(cx);
10224 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10225 setup_cursors(&mut cx, &view.input, "|");
10226 cx.update(|window, cx| {
10227 view.input.update(cx, |state, cx| {
10228 for _ in 0..999 {
10229 state.replace_text_in_range(None, "a", window, cx);
10230 }
10231 state.replace_text_in_range(None, "(", window, cx);
10232 state.undo(&Undo, window, cx);
10233 assert!(!state.text.to_string().contains('('));
10234 assert!(!state.text.to_string().contains(')'));
10235 state.redo(&Redo, window, cx);
10236 });
10237 });
10238 assert_cursors(&mut cx, &view.input, &format!("{}(|)", "a".repeat(999)));
10239 }
10240
10241 #[gpui::test]
10242 fn test_comment_closer_is_inserted_literally(cx: &mut TestAppContext) {
10243 let view = InputView::<EditorMode>::new(cx);
10244 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10245 setup_cursors(&mut cx, &view.input, "// (|)");
10246 cx.update(|window, cx| {
10247 view.input.update(cx, |state, cx| {
10248 set_test_syntax_provider(std::rc::Rc::new(CommentAllProvider), cx);
10249 state.replace_text_in_range(None, ")", window, cx);
10250 });
10251 });
10252 assert_cursors(&mut cx, &view.input, "// ()|)");
10253 }
10254
10255 #[gpui::test]
10256 fn test_explicit_range_skip_does_not_delete_existing_closer_on_undo(cx: &mut TestAppContext) {
10257 let view = InputView::<EditorMode>::new(cx);
10258 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10259 setup_cursors(&mut cx, &view.input, "(a|)");
10260 let before = view.input.read_with(&cx, |state, _| state.text.to_string());
10261 cx.update(|window, cx| {
10262 view.input.update(cx, |state, cx| {
10263 state.replace_text_in_range(Some(2..2), ")", window, cx);
10264 });
10265 });
10266 assert_cursors(&mut cx, &view.input, "(a)|");
10267 cx.update(|window, cx| {
10268 view.input
10269 .update(cx, |state, cx| state.undo(&Undo, window, cx));
10270 });
10271 view.input.read_with(&cx, |state, _| {
10272 assert_eq!(
10273 state.text.to_string(),
10274 before,
10275 "skip must not remove an existing closer on undo"
10276 );
10277 });
10278 }
10279
10280 #[gpui::test]
10281 fn test_pair_redo_restores_interior_cursor(cx: &mut TestAppContext) {
10282 let view = InputView::<EditorMode>::new(cx);
10283 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10284 setup_cursors(&mut cx, &view.input, "|");
10285 cx.update(|window, cx| {
10286 view.input.update(cx, |state, cx| {
10287 state.replace_text_in_range(None, "(", window, cx);
10288 });
10289 });
10290 assert_cursors(&mut cx, &view.input, "(|)");
10291 cx.update(|window, cx| {
10292 view.input.update(cx, |state, cx| {
10293 state.undo(&Undo, window, cx);
10294 state.redo(&Redo, window, cx);
10295 });
10296 });
10297 assert_cursors(&mut cx, &view.input, "(|)");
10298 }
10299
10300 #[gpui::test]
10301 fn test_pair_enter_redo_restores_interior_cursor(cx: &mut TestAppContext) {
10302 let view = InputView::<EditorMode>::new(cx);
10303 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10304 setup_cursors(&mut cx, &view.input, "{|}");
10305 cx.update(|window, cx| {
10306 view.input.update(cx, |state, cx| {
10307 state.enter(
10308 &Enter {
10309 secondary: false,
10310 shift: false,
10311 },
10312 window,
10313 cx,
10314 );
10315 });
10316 });
10317 assert_cursors(&mut cx, &view.input, "{\n |\n}");
10318 cx.update(|window, cx| {
10319 view.input.update(cx, |state, cx| {
10320 state.undo(&Undo, window, cx);
10321 });
10322 });
10323 assert_cursors(&mut cx, &view.input, "{|}");
10324 cx.update(|window, cx| {
10325 view.input
10326 .update(cx, |state, cx| state.redo(&Redo, window, cx));
10327 });
10328 assert_cursors(&mut cx, &view.input, "{\n |\n}");
10329 }
10330
10331 #[gpui::test]
10332 fn test_auto_close_parens(cx: &mut TestAppContext) {
10333 let view = InputView::<EditorMode>::new(cx);
10334 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10335 setup_cursors(&mut cx, &view.input, "|");
10336 cx.update(|window, cx| {
10337 view.input.update(cx, |state, cx| {
10338 state.replace_text_in_range(None, "(", window, cx);
10339 });
10340 });
10341 assert_cursors(&mut cx, &view.input, "(|)");
10342 cx.update(|window, cx| {
10344 view.input.update(cx, |state, cx| {
10345 state.replace_text_in_range(None, "x", window, cx);
10346 });
10347 });
10348 assert_cursors(&mut cx, &view.input, "(x|)");
10349 }
10350
10351 #[gpui::test]
10352 fn test_auto_close_skip_over_closer(cx: &mut TestAppContext) {
10353 let view = InputView::<EditorMode>::new(cx);
10354 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10355 setup_cursors(&mut cx, &view.input, "(|)");
10356 cx.update(|window, cx| {
10357 view.input.update(cx, |state, cx| {
10358 state.replace_text_in_range(None, ")", window, cx);
10359 });
10360 });
10361 assert_cursors(&mut cx, &view.input, "()|");
10363 view.input.read_with(&cx, |state, _| {
10365 assert!(state.active_selection().is_empty());
10366 });
10367 }
10368
10369 #[gpui::test]
10370 fn test_auto_close_no_pair_inside_word(cx: &mut TestAppContext) {
10371 let view = InputView::<EditorMode>::new(cx);
10372 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10373 setup_cursors(&mut cx, &view.input, "don|");
10374 cx.update(|window, cx| {
10375 view.input.update(cx, |state, cx| {
10376 state.replace_text_in_range(None, "'", window, cx);
10377 });
10378 });
10379 assert_cursors(&mut cx, &view.input, "don'|");
10381 }
10382
10383 #[gpui::test]
10384 fn test_auto_close_quote_after_cjk(cx: &mut TestAppContext) {
10385 let view = InputView::<EditorMode>::new(cx);
10386 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10387 setup_cursors(&mut cx, &view.input, "中|");
10390 cx.update(|window, cx| {
10391 view.input.update(cx, |state, cx| {
10392 state.replace_text_in_range(None, "'", window, cx);
10393 });
10394 });
10395 assert_cursors(&mut cx, &view.input, "中'|");
10396 }
10397
10398 #[gpui::test]
10399 fn test_auto_close_closer_skips_at_string_end(cx: &mut TestAppContext) {
10400 let view = InputView::<EditorMode>::new(cx);
10401 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10402 setup_cursors(&mut cx, &view.input, "\"hello|\"");
10404 cx.update(|window, cx| {
10405 view.input.update(cx, |state, cx| {
10406 state.replace_text_in_range(None, "\"", window, cx);
10407 });
10408 });
10409 assert_cursors(&mut cx, &view.input, "\"hello\"|");
10410 view.input.read_with(&cx, |state, _| {
10411 assert!(state.active_selection().is_empty());
10412 });
10413 }
10414
10415 #[gpui::test]
10416 fn test_backspace_pair_with_cjk_prefix(cx: &mut TestAppContext) {
10417 let view = InputView::<EditorMode>::new(cx);
10418 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10419 setup_cursors(&mut cx, &view.input, "中(|)abc");
10421 cx.update(|window, cx| {
10422 view.input
10423 .update(cx, |state, cx| state.backspace(&Backspace, window, cx));
10424 });
10425 assert_cursors(&mut cx, &view.input, "中|abc");
10426 }
10427
10428 #[gpui::test]
10429 fn test_enter_split_respects_smart_indent_off(cx: &mut TestAppContext) {
10430 let view = InputView::<EditorMode>::new(cx);
10431 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10432 setup_cursors(&mut cx, &view.input, "{|}");
10433 cx.update(|window, cx| {
10434 view.input.update(cx, |state, cx| {
10435 state.set_smart_indent(false, window, cx);
10436 state.enter(
10437 &Enter {
10438 secondary: false,
10439 shift: false,
10440 },
10441 window,
10442 cx,
10443 );
10444 });
10445 });
10446 assert_cursors(&mut cx, &view.input, "{\n|}");
10448 }
10449
10450 #[gpui::test]
10451 fn test_enter_split_is_independent_of_auto_close(cx: &mut TestAppContext) {
10452 let view = InputView::<EditorMode>::new(cx);
10453 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10454 setup_cursors(&mut cx, &view.input, "{|}");
10455 cx.update(|window, cx| {
10456 view.input.update(cx, |state, cx| {
10457 state.set_auto_close(false, window, cx);
10458 state.enter(
10459 &Enter {
10460 secondary: false,
10461 shift: false,
10462 },
10463 window,
10464 cx,
10465 );
10466 });
10467 });
10468 assert_cursors(&mut cx, &view.input, "{\n |\n}");
10470 }
10471
10472 #[gpui::test]
10473 fn test_language_config_applies_before_render_and_on_language_change(cx: &mut TestAppContext) {
10474 use crate::input::{AutoClosingPair, set_language_config};
10475 let view = InputView::<EditorMode>::new(cx);
10476 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10477 cx.update(|window, cx| {
10478 set_language_config(
10479 "alpha",
10480 LanguageConfig::default().auto_closing_pairs([AutoClosingPair::new("«", "»")]),
10481 cx,
10482 );
10483 set_language_config(
10484 "beta",
10485 LanguageConfig::default().auto_closing_pairs([AutoClosingPair::new("‹", "›")]),
10486 cx,
10487 );
10488 let first = cx.new(|cx| EditorState::new(window, cx).language("ALPHA"));
10489 let second = cx.new(|cx| EditorState::new(window, cx).language("beta"));
10490 first.update(cx, |state, cx| {
10491 state.replace_text_in_range(None, "«", window, cx);
10492 assert_eq!(state.text().to_string(), "«»");
10493 state.set_value("", window, cx);
10494 state.set_smart_indent(false, window, cx);
10495 state.set_highlighter("beta", cx);
10496 assert!(!state.mode.is_smart_indent());
10497 state.replace_text_in_range(None, "‹", window, cx);
10498 assert_eq!(state.text().to_string(), "‹›");
10499 state.set_value("", window, cx);
10500 state.set_highlighter("unknown", cx);
10501 state.replace_text_in_range(None, "(", window, cx);
10502 assert_eq!(state.text().to_string(), "()");
10503 });
10504 second.update(cx, |state, cx| {
10505 state.replace_text_in_range(None, "‹", window, cx);
10506 assert_eq!(state.text().to_string(), "‹›");
10507 });
10508 });
10509 }
10510
10511 #[gpui::test]
10512 fn test_language_service_replacement_updates_syntax_without_render(cx: &mut TestAppContext) {
10513 use crate::input::{LanguageProvider, SyntaxContextProvider, set_language_provider};
10514 struct StringLanguages(Rc<Cell<usize>>);
10515 impl LanguageProvider for StringLanguages {
10516 fn syntax_context_provider(&self, _: &str) -> Option<Rc<dyn SyntaxContextProvider>> {
10517 self.0.set(self.0.get() + 1);
10518 Some(Rc::new(StringAllProvider))
10519 }
10520 }
10521 struct CodeLanguages;
10522 impl LanguageProvider for CodeLanguages {}
10523 let view = InputView::<EditorMode>::new(cx);
10524 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10525 cx.update(|window, cx| {
10526 let creations = Rc::new(Cell::new(0));
10527 set_language_provider(Rc::new(StringLanguages(creations.clone())), cx);
10528 view.input.update(cx, |state, cx| {
10529 state.replace_text_in_range(None, "(", window, cx);
10530 state.replace_text_in_range(None, "[", window, cx);
10531 assert_eq!(state.text().to_string(), "([");
10532 assert_eq!(
10533 creations.get(),
10534 1,
10535 "retain the document provider between edits"
10536 );
10537 });
10538 set_language_provider(Rc::new(CodeLanguages), cx);
10539 view.input.update(cx, |state, cx| {
10540 state.replace_text_in_range(None, "{", window, cx);
10541 assert_eq!(state.text().to_string(), "([{}");
10542 });
10543 });
10544 }
10545
10546 #[gpui::test]
10547 fn test_language_config_updates_existing_editors(cx: &mut TestAppContext) {
10548 use crate::input::{AutoClosingPair, set_language_config};
10549 let view = InputView::<EditorMode>::new(cx);
10550 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10551 setup_cursors(&mut cx, &view.input, "|");
10552 cx.update(|window, cx| {
10553 view.input.update(cx, |state, cx| {
10554 state.set_highlighter("CUSTOM", cx);
10555 state.set_auto_close(false, window, cx);
10556 state.set_smart_indent(false, window, cx);
10557 });
10558 set_language_config(
10559 "custom",
10560 LanguageConfig::default().auto_closing_pairs([AutoClosingPair::new("«", "»")]),
10561 cx,
10562 );
10563 set_language_config("other", LanguageConfig::default(), cx);
10565 view.input.update(cx, |state, cx| {
10566 assert!(!state.mode.is_auto_close());
10567 assert!(!state.mode.is_smart_indent());
10568 state.set_auto_close(true, window, cx);
10569 state.replace_text_in_range(None, "«", window, cx);
10570 });
10571 });
10572 assert_cursors(&mut cx, &view.input, "«|»");
10573 }
10574
10575 #[gpui::test]
10576 fn test_auto_close_before_is_language_configurable(cx: &mut TestAppContext) {
10577 let view = InputView::<EditorMode>::new(cx);
10578 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10579 setup_cursors(&mut cx, &view.input, "|word");
10580 cx.update(|window, cx| {
10581 view.input.update(cx, |state, cx| {
10582 state.replace_text_in_range(None, "(", window, cx)
10583 });
10584 });
10585 assert_cursors(&mut cx, &view.input, "(|word");
10586 setup_cursors(&mut cx, &view.input, "|word");
10587 cx.update(|window, cx| {
10588 view.input.update(cx, |state, cx| {
10589 crate::input::set_language_config(
10590 state.language_name(),
10591 LanguageConfig::default().auto_close_before("w"),
10592 cx,
10593 );
10594 state.replace_text_in_range(None, "(", window, cx);
10595 });
10596 });
10597 assert_cursors(&mut cx, &view.input, "(|)word");
10598 }
10599
10600 #[gpui::test]
10601 fn test_pair_context_restrictions_are_per_pair(cx: &mut TestAppContext) {
10602 use crate::input::{AutoClosingPair, SyntaxContext};
10603 let view = InputView::<EditorMode>::new(cx);
10604 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10605 setup_cursors(&mut cx, &view.input, "|");
10606 cx.update(|window, cx| {
10607 view.input.update(cx, |state, cx| {
10608 set_test_syntax_provider(std::rc::Rc::new(CommentAllProvider), cx);
10609 crate::input::set_language_config(
10610 state.language_name(),
10611 LanguageConfig::default().auto_closing_pairs([
10612 AutoClosingPair::new("(", ")").not_in([SyntaxContext::String]),
10613 AutoClosingPair::new("[", "]").not_in([SyntaxContext::Comment]),
10614 ]),
10615 cx,
10616 );
10617 state.replace_text_in_range(None, "(", window, cx);
10618 state.replace_text_in_range(None, "[", window, cx);
10619 });
10620 });
10621 assert_cursors(&mut cx, &view.input, "([|)");
10622 }
10623
10624 #[gpui::test]
10625 fn test_multichar_delimiters_insert_delete_and_skip(cx: &mut TestAppContext) {
10626 use crate::input::{AutoClosingPair, SyntaxContext};
10627 struct BlockComment;
10628 impl crate::input::SyntaxContextProvider for BlockComment {
10629 fn context_at(&self, text: &ropey::Rope, _: usize) -> SyntaxContext {
10630 if text.to_string().contains("/*") {
10631 SyntaxContext::Comment
10632 } else {
10633 SyntaxContext::Code
10634 }
10635 }
10636 }
10637 let view = InputView::<EditorMode>::new(cx);
10638 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10639 setup_cursors(&mut cx, &view.input, "|");
10640 cx.update(|window, cx| {
10641 view.input.update(cx, |state, cx| {
10642 set_test_syntax_provider(std::rc::Rc::new(BlockComment), cx);
10643 crate::input::set_language_config(
10644 state.language_name(),
10645 LanguageConfig::default()
10646 .auto_closing_pairs([AutoClosingPair::new("/*", "*/")
10647 .not_in([SyntaxContext::String, SyntaxContext::Comment])]),
10648 cx,
10649 );
10650 state.replace_text_in_range(None, "/", window, cx);
10651 state.replace_text_in_range(None, "*", window, cx);
10652 });
10653 });
10654 assert_cursors(&mut cx, &view.input, "/*|*/");
10655 cx.update(|window, cx| {
10656 view.input
10657 .update(cx, |state, cx| state.backspace(&Backspace, window, cx));
10658 });
10659 assert_cursors(&mut cx, &view.input, "|");
10660 cx.update(|window, cx| {
10661 view.input.update(cx, |state, cx| {
10662 state.undo(&Undo, window, cx);
10663 state.replace_text_in_range(None, "*", window, cx);
10664 state.replace_text_in_range(None, "/", window, cx);
10665 });
10666 });
10667 assert_cursors(&mut cx, &view.input, "/**/|");
10668 }
10669
10670 #[gpui::test]
10671 fn test_auto_closed_pairs_fallback_and_explicit_disable(cx: &mut TestAppContext) {
10672 use crate::input::BracketPair;
10673 let view = InputView::<EditorMode>::new(cx);
10674 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10675 setup_cursors(&mut cx, &view.input, "|");
10676 cx.update(|window, cx| {
10677 view.input.update(cx, |state, cx| {
10678 let mut rules = LanguageConfig::default().brackets([BracketPair::new("«", "»")]);
10679 rules.auto_closing_pairs = None;
10680 crate::input::set_language_config(state.language_name(), rules.clone(), cx);
10681 state.replace_text_in_range(None, "«", window, cx);
10682 crate::input::set_language_config(
10683 state.language_name(),
10684 rules.auto_closing_pairs([]),
10685 cx,
10686 );
10687 state.replace_text_in_range(None, "«", window, cx);
10688 });
10689 });
10690 assert_cursors(&mut cx, &view.input, "««|»");
10691 }
10692
10693 #[gpui::test]
10694 fn test_undo_open_composition_preserves_generated_pairs(cx: &mut TestAppContext) {
10695 use crate::input::{AutoClosingPair, SyntaxContext};
10696 let view = InputView::<EditorMode>::new(cx);
10697 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10698 setup_cursors(&mut cx, &view.input, "|");
10699 cx.update(|window, cx| {
10700 view.input.update(cx, |state, cx| {
10701 crate::input::set_language_config(
10702 state.language_name(),
10703 LanguageConfig::default()
10704 .auto_closing_pairs([
10705 AutoClosingPair::new("/*", "*/").not_in([SyntaxContext::Comment])
10706 ]),
10707 cx,
10708 );
10709 state.replace_text_in_range(None, "/", window, cx);
10710 state.replace_text_in_range(None, "*", window, cx);
10711 set_test_syntax_provider(std::rc::Rc::new(CommentAllProvider), cx);
10712 state.replace_and_mark_text_in_range(None, "x", Some(1..1), window, cx);
10713 state.undo(&Undo, window, cx);
10714 state.replace_text_in_range(None, "*", window, cx);
10715 state.replace_text_in_range(None, "/", window, cx);
10716 });
10717 });
10718 assert_cursors(&mut cx, &view.input, "/**/|");
10719 }
10720
10721 #[gpui::test]
10722 fn test_undo_does_not_promote_literal_comment_delimiters(cx: &mut TestAppContext) {
10723 use crate::input::{AutoClosingPair, SyntaxContext};
10724 let view = InputView::<EditorMode>::new(cx);
10725 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10726 setup_cursors(&mut cx, &view.input, "/*|*/");
10727 cx.update(|window, cx| {
10728 view.input.update(cx, |state, cx| {
10729 set_test_syntax_provider(std::rc::Rc::new(CommentAllProvider), cx);
10730 crate::input::set_language_config(
10731 state.language_name(),
10732 LanguageConfig::default()
10733 .auto_closing_pairs([
10734 AutoClosingPair::new("/*", "*/").not_in([SyntaxContext::Comment])
10735 ]),
10736 cx,
10737 );
10738 state.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
10739 state.undo(&Undo, window, cx);
10740 state.backspace(&Backspace, window, cx);
10741 });
10742 });
10743 assert_cursors(&mut cx, &view.input, "/|*/");
10744 }
10745
10746 #[gpui::test]
10747 fn test_multiple_generated_pairs_retain_their_identity(cx: &mut TestAppContext) {
10748 use crate::input::{AutoClosingPair, SyntaxContext};
10749 struct CommentScope;
10750 impl crate::input::SyntaxContextProvider for CommentScope {
10751 fn context_at(&self, text: &ropey::Rope, offset: usize) -> SyntaxContext {
10752 if text
10753 .to_string()
10754 .find("*/")
10755 .is_some_and(|end| offset < end + 2)
10756 {
10757 SyntaxContext::Comment
10758 } else {
10759 SyntaxContext::Code
10760 }
10761 }
10762 }
10763 let view = InputView::<EditorMode>::new(cx);
10764 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10765 setup_cursors(&mut cx, &view.input, "|");
10766 cx.update(|window, cx| {
10767 view.input.update(cx, |state, cx| {
10768 set_test_syntax_provider(std::rc::Rc::new(CommentScope), cx);
10769 crate::input::set_language_config(
10770 state.language_name(),
10771 LanguageConfig::default().auto_closing_pairs([
10772 AutoClosingPair::new("/*", "*/").not_in([SyntaxContext::Comment]),
10773 AutoClosingPair::new("(", ")").not_in([SyntaxContext::Comment]),
10774 ]),
10775 cx,
10776 );
10777 state.replace_text_in_range(None, "/", window, cx);
10778 state.replace_text_in_range(None, "*", window, cx);
10779 state.set_selected_range(4..4, cx);
10780 state.replace_text_in_range(None, "(", window, cx);
10781 state.set_selected_range(2..2, cx);
10782 state.replace_text_in_range(None, "*", window, cx);
10783 state.replace_text_in_range(None, "/", window, cx);
10784 });
10785 });
10786 assert_cursors(&mut cx, &view.input, "/**/|()");
10787 cx.update(|window, cx| {
10788 view.input.update(cx, |state, cx| {
10789 state.set_selected_range(2..2, cx);
10790 state.backspace(&Backspace, window, cx);
10791 state.undo(&Undo, window, cx);
10792 state.redo(&Redo, window, cx);
10793 });
10794 });
10795 assert_cursors(&mut cx, &view.input, "|()");
10796 }
10797
10798 #[gpui::test]
10799 fn test_replacing_generated_opener_invalidates_the_pair(cx: &mut TestAppContext) {
10800 use crate::input::{AutoClosingPair, SyntaxContext};
10801 let view = InputView::<EditorMode>::new(cx);
10802 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10803 setup_cursors(&mut cx, &view.input, "|");
10804 cx.update(|window, cx| {
10805 view.input.update(cx, |state, cx| {
10806 crate::input::set_language_config(
10807 state.language_name(),
10808 LanguageConfig::default()
10809 .auto_closing_pairs([
10810 AutoClosingPair::new("/*", "*/").not_in([SyntaxContext::Comment])
10811 ]),
10812 cx,
10813 );
10814 state.replace_text_in_range(None, "/", window, cx);
10815 state.replace_text_in_range(None, "*", window, cx);
10816 state.replace_text_in_range(Some(0..2), "//", window, cx);
10817 set_test_syntax_provider(std::rc::Rc::new(CommentAllProvider), cx);
10818 state.replace_text_in_range(None, "*", window, cx);
10819 });
10820 });
10821 assert_cursors(&mut cx, &view.input, "//*|*/");
10822 }
10823
10824 #[gpui::test]
10825 fn test_configuration_preserves_compiled_regex_options(cx: &mut TestAppContext) {
10826 use crate::input::IndentationRules;
10827 let view = InputView::<EditorMode>::new(cx);
10828 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10829 setup_cursors(&mut cx, &view.input, "BEGIN|");
10830 cx.update(|window, cx| {
10831 view.input.update(cx, |state, cx| {
10832 let rules = |insensitive| {
10833 LanguageConfig::default().indentation_rules(IndentationRules::new(
10834 regex::RegexBuilder::new("begin$")
10835 .case_insensitive(insensitive)
10836 .build()
10837 .unwrap(),
10838 regex::Regex::new("^end").unwrap(),
10839 ))
10840 };
10841 crate::input::set_language_config(state.language_name(), rules(false), cx);
10842 crate::input::set_language_config(state.language_name(), rules(true), cx);
10843 state.enter(
10844 &Enter {
10845 secondary: false,
10846 shift: false,
10847 },
10848 window,
10849 cx,
10850 );
10851 });
10852 });
10853 assert_cursors(&mut cx, &view.input, "BEGIN\n |");
10854 }
10855
10856 #[gpui::test]
10857 fn test_indent_patterns_are_applied_on_enter(cx: &mut TestAppContext) {
10858 use crate::input::IndentationRules;
10859 let view = InputView::<EditorMode>::new(cx);
10860 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10861 let rules = LanguageConfig::default().indentation_rules(IndentationRules::new(
10862 regex::Regex::new(r"\bbegin\s*$").unwrap(),
10863 regex::Regex::new(r"^\s*end\b").unwrap(),
10864 ));
10865 for (before, expected) in [("begin|", "begin\n |"), (" |end", " \n|end")] {
10866 setup_cursors(&mut cx, &view.input, before);
10867 cx.update(|window, cx| {
10868 view.input.update(cx, |state, cx| {
10869 crate::input::set_language_config(state.language_name(), rules.clone(), cx);
10870 state.enter(
10871 &Enter {
10872 secondary: false,
10873 shift: false,
10874 },
10875 window,
10876 cx,
10877 );
10878 });
10879 });
10880 assert_cursors(&mut cx, &view.input, expected);
10881 }
10882 }
10883
10884 #[gpui::test]
10885 fn test_custom_language_config(cx: &mut TestAppContext) {
10886 let view = InputView::<EditorMode>::new(cx);
10887 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10888 cx.update(|_window, cx| {
10889 view.input.update(cx, |state, cx| {
10890 crate::input::set_language_config(
10891 state.language_name(),
10892 crate::input::language_config::LanguageConfig::default()
10893 .brackets([crate::input::BracketPair::new("«", "»")])
10894 .auto_closing_pairs([crate::input::AutoClosingPair::new("«", "»")]),
10895 cx,
10896 );
10897 });
10898 });
10899 setup_cursors(&mut cx, &view.input, "|");
10901 cx.update(|window, cx| {
10902 view.input.update(cx, |state, cx| {
10903 state.replace_text_in_range(None, "«", window, cx);
10904 });
10905 });
10906 assert_cursors(&mut cx, &view.input, "«|»");
10907 cx.update(|window, cx| {
10909 view.input.update(cx, |state, cx| {
10910 state.set_value("\n", window, cx);
10911 state.set_selected_range(0..0, cx);
10912 state.replace_text_in_range(None, "(", window, cx);
10913 });
10914 });
10915 assert_cursors(&mut cx, &view.input, "(|");
10916 }
10917
10918 #[gpui::test]
10919 fn test_skip_records_no_history(cx: &mut TestAppContext) {
10920 let view = InputView::<EditorMode>::new(cx);
10921 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10922 setup_cursors(&mut cx, &view.input, "(a|)");
10923 cx.update(|window, cx| {
10924 view.input.update(cx, |state, cx| {
10925 state.replace_text_in_range(None, "b", window, cx);
10927 });
10928 });
10929 assert_cursors(&mut cx, &view.input, "(ab|)");
10930 cx.update(|window, cx| {
10931 view.input.update(cx, |state, cx| {
10932 state.replace_text_in_range(None, ")", window, cx);
10933 });
10934 });
10935 assert_cursors(&mut cx, &view.input, "(ab)|");
10937 cx.update(|window, cx| {
10939 view.input
10940 .update(cx, |state, cx| state.undo(&Undo, window, cx));
10941 });
10942 assert_cursors(&mut cx, &view.input, "(a|)");
10943 }
10944
10945 #[gpui::test]
10946 fn test_pair_insert_undo_removes_both(cx: &mut TestAppContext) {
10947 let view = InputView::<EditorMode>::new(cx);
10948 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10949 setup_cursors(&mut cx, &view.input, "|");
10950 cx.update(|window, cx| {
10951 view.input.update(cx, |state, cx| {
10952 state.replace_text_in_range(None, "(", window, cx);
10953 });
10954 });
10955 assert_cursors(&mut cx, &view.input, "(|)");
10956 cx.update(|window, cx| {
10958 view.input
10959 .update(cx, |state, cx| state.undo(&Undo, window, cx));
10960 });
10961 assert_cursors(&mut cx, &view.input, "|");
10962 }
10963
10964 #[gpui::test]
10965 fn test_escaped_quote_does_not_skip(cx: &mut TestAppContext) {
10966 let view = InputView::<EditorMode>::new(cx);
10967 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10968 setup_cursors(&mut cx, &view.input, "\"hello\\|\"");
10970 cx.update(|window, cx| {
10971 view.input.update(cx, |state, cx| {
10972 state.replace_text_in_range(None, "\"", window, cx);
10973 });
10974 });
10975 assert_cursors(&mut cx, &view.input, "\"hello\\\"|\"");
10976 }
10977
10978 #[gpui::test]
10979 fn test_even_backslashes_still_skip(cx: &mut TestAppContext) {
10980 let view = InputView::<EditorMode>::new(cx);
10981 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10982 setup_cursors(&mut cx, &view.input, "\"hello\\\\|\"");
10984 cx.update(|window, cx| {
10985 view.input.update(cx, |state, cx| {
10986 state.replace_text_in_range(None, "\"", window, cx);
10987 });
10988 });
10989 assert_cursors(&mut cx, &view.input, "\"hello\\\\\"|");
10990 }
10991
10992 #[gpui::test]
10993 fn test_editor_options_survive_configuration_replacement(cx: &mut TestAppContext) {
10994 use crate::input::{BracketPair, LanguageConfig};
10995
10996 let view = InputView::<EditorMode>::new(cx);
10997 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10998 cx.update(|window, cx| {
10999 view.input.update(cx, |state, cx| {
11000 state.set_smart_indent(false, window, cx);
11002 crate::input::set_language_config(
11003 state.language_name(),
11004 LanguageConfig::default().brackets([BracketPair::new("«", "»")]),
11005 cx,
11006 );
11007 });
11008 });
11009 view.input.read_with(&cx, |state, _| {
11010 assert!(!state.mode.is_smart_indent(), "editor option preserved");
11011 assert!(state.mode.is_auto_close());
11012 let pairs = &state.mode.language_config().unwrap().brackets;
11013 assert_eq!(pairs.len(), 1, "pairs follow configuration");
11014 assert_eq!(pairs[0].open.as_ref(), "«");
11015 });
11016 }
11017
11018 struct CommentAllProvider;
11019 impl crate::input::SyntaxContextProvider for CommentAllProvider {
11020 fn context_at(&self, _text: &ropey::Rope, _offset: usize) -> crate::input::SyntaxContext {
11021 crate::input::SyntaxContext::Comment
11022 }
11023 }
11024
11025 struct StringAllProvider;
11026 impl crate::input::SyntaxContextProvider for StringAllProvider {
11027 fn context_at(&self, _text: &ropey::Rope, _offset: usize) -> crate::input::SyntaxContext {
11028 crate::input::SyntaxContext::String
11029 }
11030 }
11031
11032 #[gpui::test]
11033 fn test_smart_indent_suppressed_in_strings(cx: &mut TestAppContext) {
11034 use std::rc::Rc;
11035
11036 let view = InputView::<EditorMode>::new(cx);
11037 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11038 cx.update(|_window, cx| {
11039 set_test_syntax_provider(Rc::new(StringAllProvider), cx);
11040 });
11041 setup_cursors(&mut cx, &view.input, " x = {|");
11043 cx.update(|window, cx| {
11044 view.input.update(cx, |state, cx| {
11045 state.enter(
11046 &Enter {
11047 secondary: false,
11048 shift: false,
11049 },
11050 window,
11051 cx,
11052 );
11053 });
11054 });
11055 assert_cursors(&mut cx, &view.input, " x = {\n |");
11056 }
11057
11058 #[gpui::test]
11059 fn test_split_suppressed_in_strings(cx: &mut TestAppContext) {
11060 use std::rc::Rc;
11061
11062 let view = InputView::<EditorMode>::new(cx);
11063 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11064 cx.update(|_window, cx| {
11065 set_test_syntax_provider(Rc::new(StringAllProvider), cx);
11066 });
11067 setup_cursors(&mut cx, &view.input, "{|}");
11069 cx.update(|window, cx| {
11070 view.input.update(cx, |state, cx| {
11071 state.enter(
11072 &Enter {
11073 secondary: false,
11074 shift: false,
11075 },
11076 window,
11077 cx,
11078 );
11079 });
11080 });
11081 assert_cursors(&mut cx, &view.input, "{\n|}");
11082 }
11083
11084 #[gpui::test]
11085 fn test_comment_context_disables_pairing(cx: &mut TestAppContext) {
11086 use std::rc::Rc;
11087
11088 let view = InputView::<EditorMode>::new(cx);
11089 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11090 cx.update(|_window, cx| {
11091 set_test_syntax_provider(Rc::new(CommentAllProvider), cx);
11092 });
11093 setup_cursors(&mut cx, &view.input, "|");
11094 cx.update(|window, cx| {
11095 view.input.update(cx, |state, cx| {
11096 state.replace_text_in_range(None, "(", window, cx);
11097 });
11098 });
11099 assert_cursors(&mut cx, &view.input, "(|");
11101 }
11102
11103 #[gpui::test]
11104 fn test_backspace_deletes_empty_pair(cx: &mut TestAppContext) {
11105 let view = InputView::<EditorMode>::new(cx);
11106 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11107 setup_cursors(&mut cx, &view.input, "{|}");
11108 cx.update(|window, cx| {
11109 view.input
11110 .update(cx, |state, cx| state.backspace(&Backspace, window, cx));
11111 });
11112 assert_cursors(&mut cx, &view.input, "|");
11113 }
11114
11115 #[gpui::test]
11116 fn test_backspace_keeps_closer_with_content(cx: &mut TestAppContext) {
11117 let view = InputView::<EditorMode>::new(cx);
11118 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11119 setup_cursors(&mut cx, &view.input, "{d|}");
11120 cx.update(|window, cx| {
11121 view.input
11122 .update(cx, |state, cx| state.backspace(&Backspace, window, cx));
11123 });
11124 assert_cursors(&mut cx, &view.input, "{|}");
11126 }
11127
11128 #[gpui::test]
11129 fn test_enter_splits_brackets(cx: &mut TestAppContext) {
11130 let view = InputView::<EditorMode>::new(cx);
11131 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11132 setup_cursors(&mut cx, &view.input, "{|}");
11133 cx.update(|window, cx| {
11134 view.input.update(cx, |state, cx| {
11135 state.enter(
11136 &Enter {
11137 secondary: false,
11138 shift: false,
11139 },
11140 window,
11141 cx,
11142 );
11143 });
11144 });
11145 assert_cursors(&mut cx, &view.input, "{\n |\n}");
11146 view.input.read_with(&cx, |state, _| {
11148 assert!(state.active_selection().is_empty());
11149 });
11150 cx.update(|window, cx| {
11151 view.input.update(cx, |state, cx| {
11152 state.replace_text_in_range(None, "x", window, cx);
11153 });
11154 });
11155 assert_cursors(&mut cx, &view.input, "{\n x|\n}");
11156 }
11157
11158 #[gpui::test]
11159 fn test_enter_smart_indent_after_brace(cx: &mut TestAppContext) {
11160 let view = InputView::<EditorMode>::new(cx);
11161 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11162 setup_cursors(&mut cx, &view.input, "fn main() {|");
11163 cx.update(|window, cx| {
11164 view.input.update(cx, |state, cx| {
11165 state.enter(
11166 &Enter {
11167 secondary: false,
11168 shift: false,
11169 },
11170 window,
11171 cx,
11172 );
11173 });
11174 });
11175 assert_cursors(&mut cx, &view.input, "fn main() {\n |");
11176 }
11177
11178 #[gpui::test]
11179 fn test_block_indent_tracks_all_preceding_edits(cx: &mut TestAppContext) {
11180 let view = multi_line(cx);
11181 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11182 setup_cursors(&mut cx, &view.input, "|ab\n|cd\n|ef");
11183 cx.update(|window, cx| {
11184 view.input
11185 .update(cx, |state, cx| state.indent(true, window, cx));
11186 });
11187 assert_cursors(&mut cx, &view.input, " |ab\n |cd\n |ef");
11188 cx.update(|window, cx| {
11189 view.input
11190 .update(cx, |state, cx| state.outdent(true, window, cx));
11191 });
11192 assert_cursors(&mut cx, &view.input, "|ab\n|cd\n|ef");
11193 }
11194
11195 #[gpui::test]
11196 fn test_block_outdent_clamps_cursor_inside_indent(cx: &mut TestAppContext) {
11197 let view = multi_line(cx);
11198 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11199 setup_cursors(&mut cx, &view.input, "ab\n | cd");
11200 cx.update(|window, cx| {
11201 view.input
11202 .update(cx, |state, cx| state.outdent(true, window, cx));
11203 });
11204 assert_cursors(&mut cx, &view.input, "ab\n|cd");
11205 }
11206
11207 #[gpui::test]
11208 fn test_ime_restores_original_selection(cx: &mut TestAppContext) {
11209 let view = InputView::build(cx, |state| state);
11210 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11211 cx.update(|window, cx| {
11212 view.input.update(cx, |state, cx| {
11213 state.set_value("abc", window, cx);
11214 state.set_selected_range(1..2, cx);
11215 state.replace_and_mark_text_in_range(None, "ni", None, window, cx);
11216 state.replace_text_in_range(None, "你", window, cx);
11217 state.undo(&Undo, window, cx);
11218 assert_eq!(state.value(), "abc");
11219 assert_eq!(state.selected_range(), 1..2);
11220 state.redo(&Redo, window, cx);
11221 assert_eq!(state.selected_range(), 4..4);
11222 });
11223 });
11224 }
11225
11226 #[gpui::test]
11227 fn test_noop_does_not_change_redo_selection(cx: &mut TestAppContext) {
11228 let view = multi_line(cx);
11229 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11230 cx.update(|window, cx| {
11231 view.input.update(cx, |state, cx| {
11232 state.set_value("abc", window, cx);
11233 state.set_selected_range(1..1, cx);
11234 state.replace_text_in_range(None, "X", window, cx);
11235 state.set_selected_range(0..0, cx);
11236 state.replace_text_in_range(None, "", window, cx);
11237 state.undo(&Undo, window, cx);
11238 state.redo(&Redo, window, cx);
11239 assert_eq!(state.value(), "aXbc");
11240 assert_eq!(state.selected_range(), 2..2);
11241 });
11242 });
11243 }
11244
11245 #[gpui::test]
11246 fn test_multi_cursor_insert_text(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, "|hello |world|");
11252 cx.update(|window, cx| {
11253 input.update(cx, |state, cx| {
11254 state.replace_text_in_range(None, ">>>", window, cx);
11255 });
11256 });
11257 assert_cursors(&mut cx, &input, ">>>|hello >>>|world>>>|");
11258 }
11259
11260 #[gpui::test]
11261 fn test_multi_cursor_delete_backward(cx: &mut TestAppContext) {
11262 let view = multi_line(cx);
11263 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11264 let input = view.input;
11265
11266 setup_cursors(&mut cx, &input, "|islands| cars|");
11267 cx.update(|window, cx| {
11268 input.update(cx, |state, cx| {
11269 state.backspace(&Backspace, window, cx);
11270 });
11271 });
11272 assert_cursors(&mut cx, &input, "|island| car|");
11274 }
11275
11276 #[gpui::test]
11277 fn test_multi_cursor_delete_forward_merges(cx: &mut TestAppContext) {
11278 let view = multi_line(cx);
11279 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11280 let input = view.input;
11281
11282 setup_cursors(&mut cx, &input, "hello| |world");
11283 cx.update(|window, cx| {
11284 input.update(cx, |state, cx| {
11285 state.delete(&Delete, window, cx);
11286 });
11287 });
11288 assert_cursors(&mut cx, &input, "hello|orld");
11290 }
11291
11292 #[gpui::test]
11293 fn test_multi_cursor_multiline_insert_and_delete(cx: &mut TestAppContext) {
11294 let view = multi_line(cx);
11295 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11296 let input = view.input;
11297
11298 setup_cursors(&mut cx, &input, "|1\n|2\n|3");
11299 cx.update(|window, cx| {
11300 input.update(cx, |state, cx| {
11301 state.replace_text_in_range(None, "a", window, cx);
11302 });
11303 });
11304 assert_cursors(&mut cx, &input, "a|1\na|2\na|3");
11305
11306 input.read_with(&cx, |state, _| {
11308 assert_eq!(state.undo_manager.undo_count(), 1);
11309 });
11310
11311 cx.update(|window, cx| {
11312 input.update(cx, |state, cx| {
11313 state.backspace(&Backspace, window, cx);
11314 });
11315 });
11316 assert_cursors(&mut cx, &input, "|1\n|2\n|3");
11317 }
11318
11319 #[gpui::test]
11320 fn test_add_cursor_below_preserves_column(cx: &mut TestAppContext) {
11321 let view = multi_line(cx);
11322 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11323 let input = view.input;
11324
11325 setup_cursors(&mut cx, &input, "ab|cd\nabcd");
11326 cx.update(|window, cx| {
11327 input.update(cx, |state, cx| {
11328 state.add_cursor_below(&AddCursorBelow, window, cx);
11329 });
11330 });
11331 assert_cursors(&mut cx, &input, "ab|cd\nab|cd");
11332 }
11333
11334 #[gpui::test]
11335 fn test_add_cursor_at_rejects_duplicates(cx: &mut TestAppContext) {
11336 let view = multi_line(cx);
11337 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11338 let input = view.input;
11339
11340 setup_cursors(&mut cx, &input, "he|llo");
11341 cx.update(|_, cx| {
11342 input.update(cx, |state, cx| {
11343 state.add_cursor_at(2, cx);
11345 assert_eq!(state.selections.len(), 1);
11346 state.add_cursor_at(4, cx);
11348 assert_eq!(state.selections.len(), 2);
11349 });
11350 });
11351 }
11352
11353 #[gpui::test]
11354 fn test_multi_cursor_undo_redo_restores_selections(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(&mut cx, &input, "|1\n|2\n|3");
11360 cx.update(|window, cx| {
11361 input.update(cx, |state, cx| {
11362 state.replace_text_in_range(None, "a", window, cx);
11363 });
11364 });
11365 assert_cursors(&mut cx, &input, "a|1\na|2\na|3");
11366
11367 cx.update(|window, cx| {
11368 input.update(cx, |state, cx| {
11369 state.undo(&Undo, window, cx);
11370 });
11371 });
11372 assert_cursors(&mut cx, &input, "|1\n|2\n|3");
11373
11374 cx.update(|window, cx| {
11375 input.update(cx, |state, cx| {
11376 state.redo(&Redo, window, cx);
11377 });
11378 });
11379 assert_cursors(&mut cx, &input, "a|1\na|2\na|3");
11380 }
11381
11382 #[gpui::test]
11383 fn test_multi_cursor_undo_redo_different_line_lengths(cx: &mut TestAppContext) {
11384 let view = multi_line(cx);
11385 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11386 let input = view.input;
11387
11388 setup_cursors(&mut cx, &input, "abc123|\nabc12345|\nabc1234567|");
11389 cx.update(|window, cx| {
11390 input.update(cx, |state, cx| {
11391 state.replace_text_in_range(None, "a", window, cx);
11392 });
11393 });
11394 assert_cursors(&mut cx, &input, "abc123a|\nabc12345a|\nabc1234567a|");
11395
11396 cx.update(|window, cx| {
11397 input.update(cx, |state, cx| {
11398 state.undo(&Undo, window, cx);
11399 });
11400 });
11401 assert_cursors(&mut cx, &input, "abc123|\nabc12345|\nabc1234567|");
11402
11403 cx.update(|window, cx| {
11404 input.update(cx, |state, cx| {
11405 state.redo(&Redo, window, cx);
11406 });
11407 });
11408 assert_cursors(&mut cx, &input, "abc123a|\nabc12345a|\nabc1234567a|");
11409 }
11410
11411 #[gpui::test]
11412 fn test_multi_cursor_undo_multiple_inserts(cx: &mut TestAppContext) {
11413 let view = multi_line(cx);
11414 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11415 let input = view.input;
11416
11417 setup_cursors(&mut cx, &input, "|1\n|2\n|3");
11418 for ch in ['a', 'b', 'c'] {
11419 cx.update(|window, cx| {
11420 input.update(cx, |state, cx| {
11421 state.replace_text_in_range(None, &ch.to_string(), window, cx);
11422 });
11423 });
11424 }
11425 assert_cursors(&mut cx, &input, "abc|1\nabc|2\nabc|3");
11426
11427 cx.update(|window, cx| {
11429 input.update(cx, |state, cx| {
11430 assert_eq!(state.undo_manager.undo_count(), 1);
11431 state.undo(&Undo, window, cx);
11432 });
11433 });
11434 assert_cursors(&mut cx, &input, "|1\n|2\n|3");
11435
11436 cx.update(|window, cx| {
11437 input.update(cx, |state, cx| {
11438 state.redo(&Redo, window, cx);
11439 });
11440 });
11441 assert_cursors(&mut cx, &input, "abc|1\nabc|2\nabc|3");
11442 }
11443
11444 #[gpui::test]
11445 fn test_multi_cursor_backspace_run_is_one_undo(cx: &mut TestAppContext) {
11446 let view = multi_line(cx);
11447 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11448 let input = view.input;
11449
11450 setup_cursors(&mut cx, &input, "abc|1\nabc|2\nabc|3");
11451 for _ in 0..3 {
11452 cx.update(|window, cx| {
11453 input.update(cx, |state, cx| {
11454 state.backspace(&Backspace, window, cx);
11455 });
11456 });
11457 }
11458 assert_cursors(&mut cx, &input, "|1\n|2\n|3");
11459
11460 cx.update(|window, cx| {
11461 input.update(cx, |state, cx| {
11462 assert_eq!(state.undo_manager.undo_count(), 1);
11463 state.undo(&Undo, window, cx);
11464 });
11465 });
11466 assert_cursors(&mut cx, &input, "abc|1\nabc|2\nabc|3");
11467 }
11468
11469 #[gpui::test]
11470 fn test_adding_a_cursor_splits_the_typing_gesture(cx: &mut TestAppContext) {
11471 let view = multi_line(cx);
11472 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11473 let input = view.input;
11474
11475 setup_cursors(&mut cx, &input, "|1\n2\n3");
11476 cx.update(|window, cx| {
11477 input.update(cx, |state, cx| {
11478 state.replace_text_in_range(None, "a", window, cx);
11479 state.add_cursor_below(&AddCursorBelow, window, cx);
11480 state.replace_text_in_range(None, "b", window, cx);
11481 });
11482 });
11483 assert_cursors(&mut cx, &input, "ab|1\n2b|\n3");
11484
11485 cx.update(|window, cx| {
11487 input.update(cx, |state, cx| {
11488 state.undo(&Undo, window, cx);
11489 });
11490 });
11491 assert_cursors(&mut cx, &input, "a|1\n2|\n3");
11492 }
11493
11494 #[gpui::test]
11495 fn test_multi_cursor_indent_is_one_undo(cx: &mut TestAppContext) {
11496 let view = multi_line(cx);
11497 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11498 let input = view.input;
11499
11500 setup_cursors(&mut cx, &input, "|1\n|2\n|3");
11501 cx.update(|window, cx| {
11502 input.update(cx, |state, cx| {
11503 state.indent_inline(&IndentInline, window, cx);
11504 assert_eq!(state.undo_manager.undo_count(), 1);
11505 state.undo(&Undo, window, cx);
11506 });
11507 });
11508 assert_cursors(&mut cx, &input, "|1\n|2\n|3");
11509 }
11510
11511 #[gpui::test]
11512 fn test_multi_cursor_indent_then_outdent_roundtrips(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, "|1\n|2\n|3");
11519 cx.update(|window, cx| {
11520 input.update(cx, |state, cx| {
11521 state.indent(false, window, cx);
11522 });
11523 });
11524 assert_cursors(&mut cx, &input, " |1\n |2\n |3");
11525
11526 cx.update(|window, cx| {
11527 input.update(cx, |state, cx| {
11528 state.outdent(false, window, cx);
11529 });
11530 });
11531 assert_cursors(&mut cx, &input, "|1\n|2\n|3");
11532 }
11533
11534 #[gpui::test]
11535 fn test_inline_outdent_only_removes_line_indentation(cx: &mut TestAppContext) {
11536 let view = multi_line(cx);
11537 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11538 let input = view.input;
11539
11540 setup_cursors(&mut cx, &input, "1|2\n1|2");
11543 cx.update(|window, cx| {
11544 input.update(cx, |state, cx| {
11545 state.indent(false, window, cx);
11546 });
11547 });
11548 assert_cursors(&mut cx, &input, "1 |2\n1 |2");
11549
11550 cx.update(|window, cx| {
11551 input.update(cx, |state, cx| {
11552 state.outdent(false, window, cx);
11553 });
11554 });
11555 assert_cursors(&mut cx, &input, "1 |2\n1 |2");
11556
11557 setup_cursors(&mut cx, &input, " 1|2\n 1|2");
11559 cx.update(|window, cx| {
11560 input.update(cx, |state, cx| {
11561 state.outdent(false, window, cx);
11562 });
11563 });
11564 assert_cursors(&mut cx, &input, "1|2\n1|2");
11565 }
11566
11567 #[gpui::test]
11568 fn test_readonly_multi_cursor_commands_leave_state_unchanged(cx: &mut TestAppContext) {
11569 let view = multi_line(cx);
11570 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11571 let input = view.input;
11572
11573 setup_cursors(&mut cx, &input, "a|b\nc|d");
11574 cx.update(|window, cx| {
11575 input.update(cx, |state, cx| {
11576 let before: Vec<_> = state.selections.iter().copied().collect();
11577 state.set_readonly(true, cx);
11578 state.backspace(&Backspace, window, cx);
11579 state.indent_inline(&IndentInline, window, cx);
11580
11581 assert_eq!(state.value(), "ab\ncd\n");
11582 assert_eq!(state.selections.iter().copied().collect::<Vec<_>>(), before);
11583 });
11584 });
11585 }
11586
11587 #[gpui::test]
11588 fn test_multi_cursor_edit_preserves_the_active_cursor(cx: &mut TestAppContext) {
11589 let view = multi_line(cx);
11590 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11591 let input = view.input;
11592
11593 setup_cursors(&mut cx, &input, "x|\n|y");
11594 cx.update(|window, cx| {
11595 input.update(cx, |state, cx| {
11596 let mut selections: Vec<_> = state.selections.iter().copied().collect();
11597 selections.swap(0, 1);
11598 state.selections.replace_all(selections);
11599 let active_id = state.active_selection().id;
11600 state.replace_text_in_range(None, "!", window, cx);
11601 assert_eq!(state.active_selection().id, active_id);
11602 });
11603 });
11604 }
11605
11606 #[gpui::test]
11607 fn test_block_indent_outdent_with_selection(cx: &mut TestAppContext) {
11608 let view = multi_line(cx);
11609 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11610 let input = view.input;
11611
11612 cx.update(|window, cx| {
11613 input.update(cx, |state, cx| {
11614 state.set_value("line1\nline2\nline3", window, cx);
11615 let id = state.selections.generate_id();
11616 state
11617 .selections
11618 .replace_all(vec![CursorSelection::new(id, 0, 17)]);
11619 cx.notify();
11620 });
11621 });
11622
11623 cx.update(|window, cx| {
11624 input.update(cx, |state, cx| {
11625 state.indent(true, window, cx);
11626 });
11627 });
11628 input.read_with(&cx, |state, _| {
11629 assert_eq!(state.text.to_string(), " line1\n line2\n line3");
11630 });
11631
11632 cx.update(|window, cx| {
11633 input.update(cx, |state, cx| {
11634 state.outdent(true, window, cx);
11635 });
11636 });
11637 input.read_with(&cx, |state, _| {
11638 assert_eq!(state.text.to_string(), "line1\nline2\nline3");
11639 });
11640 }
11641
11642 #[gpui::test]
11643 fn test_multi_cursor_word_movement(cx: &mut TestAppContext) {
11644 let view = multi_line(cx);
11645 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11646 let input = view.input;
11647
11648 setup_cursors(
11649 &mut cx,
11650 &input,
11651 "on|e two three\none t|wo three\non|e two three",
11652 );
11653
11654 cx.update(|window, cx| {
11655 input.update(cx, |state, cx| {
11656 state.move_to_next_word(&MoveToNextWord, window, cx);
11657 });
11658 });
11659 assert_cursors(
11660 &mut cx,
11661 &input,
11662 "one| two three\none two| three\none| two three",
11663 );
11664
11665 cx.update(|window, cx| {
11666 input.update(cx, |state, cx| {
11667 state.move_to_previous_word(&MoveToPreviousWord, window, cx);
11668 });
11669 });
11670 assert_cursors(
11671 &mut cx,
11672 &input,
11673 "|one two three\none |two three\n|one two three",
11674 );
11675
11676 cx.update(|window, cx| {
11678 input.update(cx, |state, cx| {
11679 state.move_to_end(&MoveToEnd, window, cx);
11680 });
11681 });
11682 input.read_with(&cx, |state, _| {
11683 let cursors: Vec<usize> = state.selections.iter().map(|s| s.cursor_offset()).collect();
11684 assert_eq!(cursors, vec![state.text.len()]);
11685 });
11686
11687 cx.update(|window, cx| {
11688 input.update(cx, |state, cx| {
11689 state.move_to_start(&MoveToStart, window, cx);
11690 });
11691 });
11692 input.read_with(&cx, |state, _| {
11693 let cursors: Vec<usize> = state.selections.iter().map(|s| s.cursor_offset()).collect();
11694 assert_eq!(cursors, vec![0]);
11695 });
11696 }
11697
11698 #[gpui::test]
11699 fn test_multi_cursor_selection_commands(cx: &mut TestAppContext) {
11700 let view = multi_line(cx);
11701 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11702 let input = view.input;
11703
11704 setup_cursors(
11705 &mut cx,
11706 &input,
11707 "on|e two three\none t|wo three\non|e two three",
11708 );
11709 cx.update(|window, cx| {
11710 input.update(cx, |state, cx| {
11711 state.select_to_start_of_line(&SelectToStartOfLine, window, cx);
11712 });
11713 });
11714 assert_cursors(
11715 &mut cx,
11716 &input,
11717 "|one two three\n|one two three\n|one two three",
11718 );
11719
11720 setup_cursors(
11722 &mut cx,
11723 &input,
11724 "on|e two three\none t|wo three\non|e two three",
11725 );
11726 cx.update(|window, cx| {
11727 input.update(cx, |state, cx| {
11728 state.select_to_start(&SelectToStart, window, cx);
11729 });
11730 });
11731 input.read_with(&cx, |state, _| {
11732 let cursors: Vec<usize> = state.selections.iter().map(|s| s.cursor_offset()).collect();
11733 assert_eq!(cursors, vec![0]);
11734 });
11735 }
11736
11737 #[gpui::test]
11738 fn test_multi_cursor_replace_selection(cx: &mut TestAppContext) {
11739 let view = multi_line(cx);
11740 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11741 let input = view.input;
11742
11743 setup_cursors(&mut cx, &input, "|a\n|b\n|c");
11744 cx.update(|window, cx| {
11745 input.update(cx, |state, cx| {
11746 state.select_right(&SelectRight, window, cx);
11747 });
11748 });
11749 input.read_with(&cx, |state, _| {
11750 let ranges: Vec<_> = state.selections.iter().map(|s| (s.start, s.end)).collect();
11751 assert_eq!(ranges, vec![(0, 1), (2, 3), (4, 5)]);
11752 });
11753
11754 cx.update(|window, cx| {
11755 input.update(cx, |state, cx| {
11756 state.replace_text_in_range(None, "x", window, cx);
11757 });
11758 });
11759 assert_cursors(&mut cx, &input, "x|\nx|\nx|");
11760 }
11761
11762 #[gpui::test]
11763 fn test_multi_cursor_escape_collapses(cx: &mut TestAppContext) {
11764 let view = multi_line(cx);
11765 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11766 let input = view.input;
11767
11768 setup_cursors(&mut cx, &input, "|a\n|b\n|c");
11769 cx.update(|window, cx| {
11770 input.update(cx, |state, cx| {
11771 state.escape(&Escape, window, cx);
11772 });
11773 });
11774 input.read_with(&cx, |state, _| {
11775 assert_eq!(state.selections.len(), 1);
11776 });
11777 }
11778
11779 #[gpui::test]
11780 fn test_build_columnar_selection(cx: &mut TestAppContext) {
11781 let view = multi_line(cx);
11782 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11783 let input = view.input;
11784
11785 setup_cursors(&mut cx, &input, "abcd\nabcd\nabcd");
11786 cx.update(|_, cx| {
11787 input.update(cx, |state, cx| {
11788 state.build_columnar_selection(
11790 ColumnarPoint::new(1, 0),
11791 ColumnarPoint::new(13, 0),
11792 cx,
11793 );
11794 let ranges: Vec<_> = state.selections.iter().map(|s| (s.start, s.end)).collect();
11795 assert_eq!(ranges, vec![(1, 3), (6, 8), (11, 13)]);
11796 });
11797 });
11798 }
11799
11800 #[gpui::test]
11801 fn test_columnar_selection_keeps_width_over_short_rows(cx: &mut TestAppContext) {
11802 let view = multi_line(cx);
11803 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11804 let input = view.input;
11805
11806 setup_cursors(&mut cx, &input, "abcdef\nab\nabcdef");
11807 cx.update(|_, cx| {
11808 input.update(cx, |state, cx| {
11809 state.build_columnar_selection(
11812 ColumnarPoint::new(1, 0),
11813 ColumnarPoint::new(9, 3),
11814 cx,
11815 );
11816 let ranges: Vec<_> = state.selections.iter().map(|s| (s.start, s.end)).collect();
11817 assert_eq!(ranges, vec![(1, 5), (8, 9)]);
11818 });
11819 });
11820 }
11821
11822 #[gpui::test]
11823 fn test_multi_cursor_paste_distributes_lines(cx: &mut TestAppContext) {
11824 let view = multi_line(cx);
11825 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11826 let input = view.input;
11827
11828 setup_cursors(&mut cx, &input, "|1\n|2\n|3");
11829 cx.update(|_, cx| {
11830 cx.write_to_clipboard(ClipboardItem::new_string("x\ny\nz".to_string()));
11831 });
11832 cx.update(|window, cx| {
11833 input.update(cx, |state, cx| {
11834 state.paste(&Paste, window, cx);
11835 });
11836 });
11837 assert_cursors(&mut cx, &input, "x|1\ny|2\nz|3");
11839 }
11840
11841 #[gpui::test]
11842 fn test_paste_without_text_leaves_the_selection_alone(cx: &mut TestAppContext) {
11843 use gpui::{ClipboardEntry, Image, ImageFormat};
11844
11845 let input_view = InputView::build(cx, |state| state);
11846 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
11847 let input = input_view.input;
11848
11849 cx.update(|window, cx| {
11850 input.update(cx, |state, cx| {
11851 state.set_value("hello world", window, cx);
11852 state.select_all(window, cx);
11853 cx.write_to_clipboard(ClipboardItem {
11854 entries: vec![ClipboardEntry::Image(Image::from_bytes(
11855 ImageFormat::Png,
11856 vec![0x89, b'P', b'N', b'G'],
11857 ))],
11858 });
11859
11860 state.paste(&Paste, window, cx);
11863 assert_eq!(state.value(), "hello world");
11864 assert_eq!(state.selected_range(), 0..11);
11865 });
11866 });
11867 }
11868
11869 #[gpui::test]
11870 fn test_paste_target_tracks_edits_and_selections(cx: &mut TestAppContext) {
11871 let view = multi_line(cx);
11872 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11873 let input = view.input;
11874
11875 setup_cursors(&mut cx, &input, "ab|c");
11876 let target = input.read_with(&cx, |state, _| state.paste_target());
11877 assert_eq!(
11879 input.read_with(&cx, |state, _| state.paste_target()),
11880 target
11881 );
11882
11883 cx.update(|_, cx| {
11885 input.update(cx, |state, cx| {
11886 let id = state.selections.generate_id();
11887 state
11888 .selections
11889 .replace_all(vec![CursorSelection::new(id, 1, 1)]);
11890 cx.notify();
11891 });
11892 });
11893 let moved = input.read_with(&cx, |state, _| state.paste_target());
11894 assert_ne!(moved, target);
11895
11896 cx.update(|window, cx| {
11898 input.update(cx, |state, cx| {
11899 state.replace_text_in_range_silent(None, "x", window, cx);
11900 let id = state.selections.generate_id();
11901 state
11902 .selections
11903 .replace_all(vec![CursorSelection::new(id, 1, 1)]);
11904 });
11905 });
11906 assert_ne!(input.read_with(&cx, |state, _| state.paste_target()), moved);
11907 }
11908
11909 #[gpui::test]
11910 fn test_ime_candidate_bounds_fallback_before_repaint(cx: &mut TestAppContext) {
11911 let input_view = InputView::build_textarea(cx, |state| {
11912 state.default_value("这是一段已经输入的中文文字")
11913 });
11914 let mut visual = VisualTestContext::from_window(input_view.window_handle.into(), cx);
11915 visual.update(|window, cx| window.draw(cx).clear(cx));
11916
11917 visual.update(|window, cx| {
11918 input_view.input.update(cx, |state, cx| {
11919 state.focus(window, cx);
11920 let len = state.text.len();
11921 state.set_cursor_to(len);
11922 });
11923 });
11924 visual.update(|window, cx| window.draw(cx).clear(cx));
11925
11926 visual.update(|window, cx| {
11927 input_view.input.update(cx, |state, cx| {
11928 let bounds = state.text_bounds().unwrap_or_default();
11929 let before = state.selected_text_range(false, window, cx).unwrap().range;
11930 let previous = state
11931 .bounds_for_range(before.end..before.end, bounds, window, cx)
11932 .unwrap();
11933
11934 state.replace_and_mark_text_in_range(None, "n", None, window, cx);
11935
11936 let after = state.selected_text_range(false, window, cx).unwrap().range;
11937 let pending = state
11938 .bounds_for_range(after.end..after.end, bounds, window, cx)
11939 .unwrap();
11940
11941 assert!(
11942 (pending.origin.x - previous.origin.x).abs() < px(40.0),
11943 "pending origin.x {:?} must remain close to previous origin.x {:?}",
11944 pending.origin.x,
11945 previous.origin.x
11946 );
11947
11948 let range_bounds = state
11949 .bounds_for_range(before.end..after.end, bounds, window, cx)
11950 .unwrap();
11951 assert!(
11952 (range_bounds.origin.x - previous.origin.x).abs() < px(40.0),
11953 "range_bounds origin.x {:?} must remain close to previous origin.x {:?}",
11954 range_bounds.origin.x,
11955 previous.origin.x
11956 );
11957 assert!(range_bounds.size.width >= px(0.0));
11958 });
11959 });
11960 }
11961}
11962
11963impl InputBaseState<crate::input::InputMode> {
11965 pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
11967 Self::new_in_mode(window, cx)
11968 }
11969
11970 pub fn step_by(mut self, f: impl Fn(f64, StepAction, &mut App) -> f64 + 'static) -> Self {
11985 self.number_step = Some(NumberStep::by_value(f));
11986 self
11987 }
11988
11989 pub fn masked(mut self, masked: bool) -> Self {
11991 self.masked = masked;
11992 self
11993 }
11994
11995 pub fn set_masked(&mut self, masked: bool, _: &mut Window, cx: &mut Context<Self>) {
11997 self.masked = masked;
11998 cx.notify();
11999 }
12000
12001 pub fn pattern(mut self, pattern: regex::Regex) -> Self {
12003 self.pattern = Some(pattern);
12004 self
12005 }
12006
12007 pub fn set_pattern(
12009 &mut self,
12010 pattern: regex::Regex,
12011 _window: &mut Window,
12012 _cx: &mut Context<Self>,
12013 ) {
12014 self.pattern = Some(pattern);
12015 }
12016
12017 pub fn validate(mut self, f: impl Fn(&str, &mut App) -> bool + 'static) -> Self {
12019 self.validate = Some(Box::new(f));
12020 self
12021 }
12022
12023 pub fn set_validator(
12024 &mut self,
12025 validate: impl Fn(&str, &mut App) -> bool + 'static,
12026 _cx: &mut Context<Self>,
12027 ) {
12028 self.validate = Some(Box::new(validate));
12029 }
12030
12031 pub fn step(mut self, step: impl Into<NumberStep>) -> Self {
12041 self.number_step = Some(step.into());
12042 self
12043 }
12044
12045 pub fn min(mut self, min: f64) -> Self {
12051 self.number_min = Some(min);
12052 self
12053 }
12054
12055 pub fn max(mut self, max: f64) -> Self {
12061 self.number_max = Some(max);
12062 self
12063 }
12064
12065 pub fn set_step(
12070 &mut self,
12071 step: impl Into<Option<NumberStep>>,
12072 _: &mut Window,
12073 _: &mut Context<Self>,
12074 ) {
12075 self.number_step = step.into();
12076 }
12077
12078 pub fn set_min(&mut self, min: Option<f64>, _: &mut Window, _: &mut Context<Self>) {
12080 self.number_min = min;
12081 }
12082
12083 pub fn set_max(&mut self, max: Option<f64>, _: &mut Window, _: &mut Context<Self>) {
12085 self.number_max = max;
12086 }
12087
12088 pub fn set_loading(&mut self, loading: bool, _: &mut Window, cx: &mut Context<Self>) {
12090 self.loading = loading;
12091 cx.notify();
12092 }
12093}
12094
12095impl<M: crate::input::MultiLineMode> InputBaseState<M> {
12098 pub fn scroll_bounce(mut self, scroll_bounce: bool) -> Self {
12106 self.scroll_bounce = scroll_bounce;
12107 self
12108 }
12109
12110 pub fn set_scroll_bounce(&mut self, scroll_bounce: bool, cx: &mut Context<Self>) {
12114 if self.scroll_bounce != scroll_bounce {
12115 self.scroll_bounce = scroll_bounce;
12116 self.overscroll.reset();
12117 cx.notify();
12118 }
12119 }
12120
12121 pub fn searchable(mut self, searchable: bool) -> Self {
12129 self.searchable = searchable;
12130 self
12131 }
12132
12133 pub fn set_searchable(&mut self, searchable: bool, cx: &mut Context<Self>) {
12135 self.searchable = searchable;
12136 cx.notify();
12137 }
12138
12139 #[doc(hidden)]
12141 pub fn soft_wrap(mut self, wrap: bool) -> Self {
12142 self.soft_wrap = wrap;
12143 self
12144 }
12145
12146 pub fn set_soft_wrap(&mut self, wrap: bool, _: &mut Window, cx: &mut Context<Self>) {
12148 self.soft_wrap = wrap;
12149 if wrap {
12150 let wrap_width = self
12151 .last_layout
12152 .as_ref()
12153 .and_then(|b| b.wrap_width)
12154 .unwrap_or(self.input_bounds.size.width);
12155
12156 self.display_map.on_layout_changed(Some(wrap_width), cx);
12157
12158 let mut offset = self.scroll_handle.offset();
12160 offset.x = px(0.);
12161 self.scroll_handle.set_offset(offset);
12162 } else {
12163 self.display_map.on_layout_changed(None, cx);
12164 }
12165 cx.notify();
12166 }
12167
12168 #[doc(hidden)]
12170 pub fn wrapping_indent(mut self, wrapping_indent: WrappingIndent) -> Self {
12171 self.wrapping_indent = wrapping_indent;
12172 self
12173 }
12174
12175 pub fn set_wrapping_indent(
12177 &mut self,
12178 wrapping_indent: WrappingIndent,
12179 _: &mut Window,
12180 cx: &mut Context<Self>,
12181 ) {
12182 self.wrapping_indent = wrapping_indent;
12183 self.display_map.set_wrapping_indent(wrapping_indent, cx);
12184 cx.notify();
12185 }
12186}
12187
12188impl InputBaseState<crate::input::TextareaMode> {
12190 pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
12195 Self::new_in_mode(window, cx)
12196 }
12197
12198 pub fn set_auto_grow(&mut self, min_rows: usize, max_rows: usize, cx: &mut Context<Self>) {
12199 self.mode = LayoutMode::auto_grow(min_rows, max_rows.max(min_rows));
12200 self.mode.update_auto_grow(&self.display_map);
12201 cx.notify();
12202 }
12203
12204 #[doc(hidden)]
12211 pub fn rows(mut self, rows: usize) -> Self {
12212 match &mut self.mode {
12213 LayoutMode::PlainText { rows: r, .. } | LayoutMode::CodeEditor { rows: r, .. } => {
12214 *r = rows
12215 }
12216 LayoutMode::AutoGrow {
12217 max_rows: max_r,
12218 rows: r,
12219 ..
12220 } => {
12221 *r = rows;
12222 *max_r = rows;
12223 }
12224 }
12225 self
12226 }
12227
12228 pub fn set_rows(&mut self, rows: usize, cx: &mut Context<Self>) {
12229 match &mut self.mode {
12230 LayoutMode::PlainText { rows: value, .. }
12231 | LayoutMode::CodeEditor { rows: value, .. } => *value = rows,
12232 LayoutMode::AutoGrow {
12233 rows: value,
12234 max_rows,
12235 ..
12236 } => {
12237 *value = rows;
12238 *max_rows = rows;
12239 }
12240 }
12241 cx.notify();
12242 }
12243
12244 pub fn auto_grow(mut self, min_rows: usize, max_rows: usize) -> Self {
12246 self.mode = LayoutMode::auto_grow(min_rows, max_rows);
12247 self
12248 }
12249}
12250
12251impl InputBaseState<crate::input::EditorMode> {
12253 pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
12264 let mut state = Self::new_in_mode(window, cx);
12265 state.mode = LayoutMode::code_editor(super::EditorLanguage::new(cx));
12266 state.searchable = true;
12267 state
12268 }
12269
12270 pub fn language(mut self, language: impl Into<SharedString>) -> Self {
12274 if let LayoutMode::CodeEditor {
12275 language: l,
12276 highlighter,
12277 ..
12278 } = &mut self.mode
12279 {
12280 l.set_name(language.into());
12281 *highlighter.borrow_mut() = None;
12282 }
12283 self
12284 }
12285
12286 pub fn language_name(&self) -> SharedString {
12288 match &self.mode {
12289 LayoutMode::CodeEditor { language, .. } => language.name(),
12290 _ => SharedString::default(),
12291 }
12292 }
12293
12294 #[doc(hidden)]
12298 pub fn folding(mut self, folding: bool) -> Self {
12299 if let LayoutMode::CodeEditor { folding: f, .. } = &mut self.mode {
12300 *f = folding;
12301 }
12302 self
12303 }
12304
12305 pub fn set_folding(&mut self, folding: bool, _: &mut Window, cx: &mut Context<Self>) {
12309 if let LayoutMode::CodeEditor { folding: f, .. } = &mut self.mode {
12310 *f = folding;
12311 }
12312 if !folding {
12313 self.display_map.clear_folds();
12314 }
12315 cx.notify();
12316 }
12317
12318 pub fn unfold_at(&mut self, position: impl Into<Position>, cx: &mut Context<Self>) -> bool {
12331 let offset = self.text.position_to_offset(&position.into());
12332 let line = self.text.offset_to_point(offset).row;
12333 let covering: Vec<usize> = self
12336 .display_map
12337 .folded_ranges()
12338 .iter()
12339 .filter(|fold| line > fold.start_line && line < fold.end_line)
12340 .map(|fold| fold.start_line)
12341 .collect();
12342 if covering.is_empty() {
12343 return false;
12344 }
12345
12346 for start_line in covering {
12347 self.display_map.set_folded(start_line, false);
12348 }
12349 cx.notify();
12350 true
12351 }
12352
12353 #[doc(hidden)]
12355 pub fn line_number(mut self, line_number: bool) -> Self {
12356 if let LayoutMode::CodeEditor { line_number: l, .. } = &mut self.mode {
12357 *l = line_number;
12358 }
12359 self
12360 }
12361
12362 pub fn set_line_number(&mut self, line_number: bool, _: &mut Window, cx: &mut Context<Self>) {
12364 if let LayoutMode::CodeEditor { line_number: l, .. } = &mut self.mode {
12365 if *l == line_number {
12366 return;
12367 }
12368 *l = line_number;
12369 }
12370 cx.notify();
12371 }
12372
12373 #[doc(hidden)]
12379 pub fn auto_close(mut self, auto_close: bool) -> Self {
12380 self.mode.set_auto_close(auto_close);
12381 self
12382 }
12383
12384 pub fn set_auto_close(&mut self, auto_close: bool, _: &mut Window, cx: &mut Context<Self>) {
12386 self.mode.set_auto_close(auto_close);
12387 cx.notify();
12388 }
12389
12390 #[doc(hidden)]
12396 pub fn smart_indent(mut self, smart_indent: bool) -> Self {
12397 self.mode.set_smart_indent(smart_indent);
12398 self
12399 }
12400
12401 pub fn set_smart_indent(&mut self, smart_indent: bool, _: &mut Window, cx: &mut Context<Self>) {
12403 self.mode.set_smart_indent(smart_indent);
12404 cx.notify();
12405 }
12406}