1use gpui::TextAlign;
6use gpui::{
7 Action, App, AppContext, Bounds, ClipboardItem, Context, Edges, Entity, EntityInputHandler,
8 EventEmitter, FocusHandle, Focusable, Hsla, 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, FoldPlaceholderRenderer, InputContextMenuCapabilities,
26 InputEditorStyle, InputHighlighterFactory, MASK_CHAR, MaskPattern, NativeMenu, NumberStep,
27 WrappingIndent,
28 blink_cursor::BlinkCursor,
29 change::Change,
30 cursor::{CursorSelection, Selections},
31 element::{
32 EditorScrollbar, EditorScrollbarSnapshot, LongestLineKey, OverviewRulerRows, TextElement,
33 clamp_horizontal_scroll_offset,
34 },
35 kind::InputModeKind,
36 mask_pattern::normalize_number_input,
37 mode::LayoutMode,
38 undo_manager::{EditIntent, UndoManager},
39};
40use crate::actions::{SelectDown, SelectLeft, SelectRight, SelectUp};
41use crate::input::blink_cursor::CURSOR_WIDTH;
42use crate::input::movement::MoveDirection;
43use crate::input::{
44 InputEdit, InputExtras as _, Position, RopeExt as _, element::RIGHT_MARGIN, layout::LastLayout,
45};
46use crate::overscroll::Overscroll;
47use crate::{AutoScroll, StepAction, TestSupportExt as _};
48
49pub(super) const EDITOR_ELEMENT_ID: &str = "input-state";
50
51pub(crate) enum ScrollPadding {
53 Minimal,
54 SurroundingLines,
55}
56
57#[derive(Action, Clone, PartialEq, Eq, Deserialize)]
58#[action(namespace = input, no_json)]
59pub struct Enter {
60 pub secondary: bool,
62 pub shift: bool,
64}
65
66impl Enter {
67 pub fn is_primary(action: &dyn Action) -> bool {
70 action.partial_eq(&Enter {
71 secondary: false,
72 shift: false,
73 }) || action.partial_eq(&Enter {
74 secondary: false,
75 shift: true,
76 })
77 }
78}
79
80actions!(
81 input,
82 [
83 ActivateToken,
84 Backspace,
85 Delete,
86 DeleteToBeginningOfLine,
87 DeleteToEndOfLine,
88 DeleteToPreviousWordStart,
89 DeleteToNextWordEnd,
90 Indent,
91 Outdent,
92 IndentInline,
93 OutdentInline,
94 MoveUp,
95 MoveDown,
96 MoveLeft,
97 MoveRight,
98 MoveHome,
99 MoveEnd,
100 MovePageUp,
101 MovePageDown,
102 AddCursorAbove,
103 AddCursorBelow,
104 SelectAll,
105 SelectToStartOfLine,
106 SelectToEndOfLine,
107 SelectToStart,
108 SelectToEnd,
109 SelectToPreviousWordStart,
110 SelectToNextWordEnd,
111 ShowCharacterPalette,
112 Copy,
113 Cut,
114 Paste,
115 Undo,
116 Redo,
117 MoveToStartOfLine,
118 MoveToEndOfLine,
119 MoveToStart,
120 MoveToEnd,
121 MoveToPreviousWord,
122 MoveToNextWord,
123 Escape,
124 ToggleCodeActions,
125 ShowCompletions,
126 Search,
127 Replace,
128 GoToDefinition,
129 ]
130);
131
132#[derive(Clone)]
138pub enum InputEvent {
139 Change,
140 PressEnter { secondary: bool, shift: bool },
141 Focus,
142 Blur,
143}
144
145#[derive(Clone)]
151pub struct InputEditsEvent {
152 edits: Vec<(InputEdit, Rope)>,
153}
154
155impl InputEditsEvent {
156 pub fn edits(&self) -> &[(InputEdit, Rope)] {
158 &self.edits
159 }
160}
161
162pub(super) const CONTEXT: &str = "Input";
163
164pub(crate) fn init(cx: &mut App) {
165 cx.bind_keys([
166 KeyBinding::new("backspace", Backspace, Some(CONTEXT)),
167 KeyBinding::new("shift-backspace", Backspace, Some(CONTEXT)),
168 #[cfg(target_os = "macos")]
169 KeyBinding::new("ctrl-backspace", Backspace, Some(CONTEXT)),
170 KeyBinding::new("delete", Delete, Some(CONTEXT)),
171 KeyBinding::new("shift-delete", Delete, Some(CONTEXT)),
172 #[cfg(target_os = "macos")]
173 KeyBinding::new("cmd-backspace", DeleteToBeginningOfLine, Some(CONTEXT)),
174 #[cfg(target_os = "macos")]
175 KeyBinding::new("cmd-delete", DeleteToEndOfLine, Some(CONTEXT)),
176 #[cfg(target_os = "macos")]
177 KeyBinding::new("alt-backspace", DeleteToPreviousWordStart, Some(CONTEXT)),
178 #[cfg(not(target_os = "macos"))]
179 KeyBinding::new("ctrl-backspace", DeleteToPreviousWordStart, Some(CONTEXT)),
180 #[cfg(target_os = "macos")]
181 KeyBinding::new("alt-delete", DeleteToNextWordEnd, Some(CONTEXT)),
182 #[cfg(not(target_os = "macos"))]
183 KeyBinding::new("ctrl-delete", DeleteToNextWordEnd, Some(CONTEXT)),
184 KeyBinding::new(
185 "enter",
186 Enter {
187 secondary: false,
188 shift: false,
189 },
190 Some(CONTEXT),
191 ),
192 KeyBinding::new(
193 "shift-enter",
194 Enter {
195 secondary: false,
196 shift: true,
197 },
198 Some(CONTEXT),
199 ),
200 KeyBinding::new(
201 if cx.platform_info().is_mac() {
202 "cmd-enter"
203 } else {
204 "ctrl-enter"
205 },
206 Enter {
207 secondary: true,
208 shift: false,
209 },
210 Some(CONTEXT),
211 ),
212 KeyBinding::new("escape", Escape, Some(CONTEXT)),
213 KeyBinding::new("up", MoveUp, Some(CONTEXT)),
214 KeyBinding::new("down", MoveDown, Some(CONTEXT)),
215 KeyBinding::new("left", MoveLeft, Some(CONTEXT)),
216 KeyBinding::new("right", MoveRight, Some(CONTEXT)),
217 KeyBinding::new("pageup", MovePageUp, Some(CONTEXT)),
218 KeyBinding::new("pagedown", MovePageDown, Some(CONTEXT)),
219 KeyBinding::new("tab", IndentInline, Some(CONTEXT)),
220 KeyBinding::new("shift-tab", OutdentInline, Some(CONTEXT)),
221 #[cfg(target_os = "macos")]
222 KeyBinding::new("cmd-]", Indent, Some(CONTEXT)),
223 #[cfg(not(target_os = "macos"))]
224 KeyBinding::new("ctrl-]", Indent, Some(CONTEXT)),
225 #[cfg(target_os = "macos")]
226 KeyBinding::new("cmd-[", Outdent, Some(CONTEXT)),
227 #[cfg(not(target_os = "macos"))]
228 KeyBinding::new("ctrl-[", Outdent, Some(CONTEXT)),
229 KeyBinding::new("shift-left", SelectLeft, Some(CONTEXT)),
230 KeyBinding::new("shift-right", SelectRight, Some(CONTEXT)),
231 KeyBinding::new("shift-up", SelectUp, Some(CONTEXT)),
232 KeyBinding::new("shift-down", SelectDown, Some(CONTEXT)),
233 #[cfg(not(any(target_os = "macos", target_os = "linux")))]
234 KeyBinding::new("shift-alt-left", SelectLeft, Some(CONTEXT)),
235 #[cfg(not(any(target_os = "macos", target_os = "linux")))]
236 KeyBinding::new("shift-alt-right", SelectRight, Some(CONTEXT)),
237 #[cfg(target_os = "macos")]
239 KeyBinding::new("cmd-alt-up", AddCursorAbove, Some(CONTEXT)),
240 #[cfg(target_os = "macos")]
241 KeyBinding::new("cmd-alt-down", AddCursorBelow, Some(CONTEXT)),
242 #[cfg(target_os = "windows")]
243 KeyBinding::new("ctrl-alt-up", AddCursorAbove, Some(CONTEXT)),
244 #[cfg(not(any(target_os = "macos", target_os = "windows")))]
245 KeyBinding::new("shift-alt-up", AddCursorAbove, Some(CONTEXT)),
246 #[cfg(target_os = "windows")]
247 KeyBinding::new("ctrl-alt-down", AddCursorBelow, Some(CONTEXT)),
248 #[cfg(not(any(target_os = "macos", target_os = "windows")))]
249 KeyBinding::new("shift-alt-down", AddCursorBelow, Some(CONTEXT)),
250 KeyBinding::new("home", MoveHome, Some(CONTEXT)),
251 KeyBinding::new("end", MoveEnd, Some(CONTEXT)),
252 #[cfg(not(target_os = "macos"))]
253 KeyBinding::new("ctrl-home", MoveToStart, Some(CONTEXT)),
254 #[cfg(not(target_os = "macos"))]
255 KeyBinding::new("ctrl-end", MoveToEnd, Some(CONTEXT)),
256 #[cfg(not(target_os = "macos"))]
257 KeyBinding::new("ctrl-shift-home", SelectToStart, Some(CONTEXT)),
258 #[cfg(not(target_os = "macos"))]
259 KeyBinding::new("ctrl-shift-end", SelectToEnd, Some(CONTEXT)),
260 KeyBinding::new("shift-home", SelectToStartOfLine, Some(CONTEXT)),
261 KeyBinding::new("shift-end", SelectToEndOfLine, Some(CONTEXT)),
262 #[cfg(target_os = "macos")]
263 KeyBinding::new("ctrl-shift-a", SelectToStartOfLine, Some(CONTEXT)),
264 #[cfg(target_os = "macos")]
265 KeyBinding::new("ctrl-shift-e", SelectToEndOfLine, Some(CONTEXT)),
266 #[cfg(target_os = "macos")]
267 KeyBinding::new("shift-cmd-left", SelectToStartOfLine, Some(CONTEXT)),
268 #[cfg(target_os = "macos")]
269 KeyBinding::new("shift-cmd-right", SelectToEndOfLine, Some(CONTEXT)),
270 #[cfg(any(target_os = "macos", target_os = "linux"))]
271 KeyBinding::new("alt-shift-left", SelectToPreviousWordStart, Some(CONTEXT)),
272 #[cfg(not(target_os = "macos"))]
273 KeyBinding::new("ctrl-shift-left", SelectToPreviousWordStart, Some(CONTEXT)),
274 #[cfg(any(target_os = "macos", target_os = "linux"))]
275 KeyBinding::new("alt-shift-right", SelectToNextWordEnd, Some(CONTEXT)),
276 #[cfg(not(target_os = "macos"))]
277 KeyBinding::new("ctrl-shift-right", SelectToNextWordEnd, Some(CONTEXT)),
278 #[cfg(target_os = "macos")]
279 KeyBinding::new("ctrl-cmd-space", ShowCharacterPalette, Some(CONTEXT)),
280 #[cfg(target_os = "macos")]
281 KeyBinding::new("cmd-a", SelectAll, Some(CONTEXT)),
282 #[cfg(not(target_os = "macos"))]
283 KeyBinding::new("ctrl-a", SelectAll, Some(CONTEXT)),
284 #[cfg(target_os = "macos")]
285 KeyBinding::new("cmd-c", Copy, Some(CONTEXT)),
286 #[cfg(not(target_os = "macos"))]
287 KeyBinding::new("ctrl-c", Copy, Some(CONTEXT)),
288 #[cfg(target_os = "macos")]
289 KeyBinding::new("cmd-x", Cut, Some(CONTEXT)),
290 #[cfg(not(target_os = "macos"))]
291 KeyBinding::new("ctrl-x", Cut, Some(CONTEXT)),
292 #[cfg(target_os = "macos")]
293 KeyBinding::new("cmd-v", Paste, Some(CONTEXT)),
294 #[cfg(not(target_os = "macos"))]
295 KeyBinding::new("ctrl-v", Paste, Some(CONTEXT)),
296 #[cfg(target_os = "macos")]
297 KeyBinding::new("ctrl-a", MoveHome, Some(CONTEXT)),
298 #[cfg(target_os = "macos")]
299 KeyBinding::new("cmd-left", MoveHome, Some(CONTEXT)),
300 #[cfg(target_os = "macos")]
301 KeyBinding::new("ctrl-e", MoveEnd, Some(CONTEXT)),
302 #[cfg(target_os = "macos")]
303 KeyBinding::new("cmd-right", MoveEnd, Some(CONTEXT)),
304 #[cfg(target_os = "macos")]
305 KeyBinding::new("cmd-z", Undo, Some(CONTEXT)),
306 #[cfg(target_os = "macos")]
307 KeyBinding::new("cmd-shift-z", Redo, Some(CONTEXT)),
308 #[cfg(target_os = "macos")]
309 KeyBinding::new("cmd-up", MoveToStart, Some(CONTEXT)),
310 #[cfg(target_os = "macos")]
311 KeyBinding::new("cmd-down", MoveToEnd, Some(CONTEXT)),
312 #[cfg(target_os = "macos")]
313 KeyBinding::new("alt-left", MoveToPreviousWord, Some(CONTEXT)),
314 #[cfg(target_os = "macos")]
315 KeyBinding::new("alt-right", MoveToNextWord, Some(CONTEXT)),
316 #[cfg(not(target_os = "macos"))]
317 KeyBinding::new("ctrl-left", MoveToPreviousWord, Some(CONTEXT)),
318 #[cfg(not(target_os = "macos"))]
319 KeyBinding::new("ctrl-right", MoveToNextWord, Some(CONTEXT)),
320 #[cfg(target_os = "macos")]
321 KeyBinding::new("cmd-shift-up", SelectToStart, Some(CONTEXT)),
322 #[cfg(target_os = "macos")]
323 KeyBinding::new("cmd-shift-down", SelectToEnd, Some(CONTEXT)),
324 #[cfg(not(target_os = "macos"))]
325 KeyBinding::new("ctrl-z", Undo, Some(CONTEXT)),
326 #[cfg(not(target_os = "macos"))]
327 KeyBinding::new("ctrl-y", Redo, Some(CONTEXT)),
328 #[cfg(target_os = "macos")]
329 KeyBinding::new("cmd-.", ToggleCodeActions, Some(CONTEXT)),
330 #[cfg(not(target_os = "macos"))]
331 KeyBinding::new("ctrl-.", ToggleCodeActions, Some(CONTEXT)),
332 #[cfg(target_os = "macos")]
333 KeyBinding::new("cmd-f", Search, Some(CONTEXT)),
334 #[cfg(not(target_os = "macos"))]
335 KeyBinding::new("ctrl-f", Search, Some(CONTEXT)),
336 #[cfg(target_os = "macos")]
337 KeyBinding::new("cmd-shift-f", Replace, Some(CONTEXT)),
338 #[cfg(not(target_os = "macos"))]
339 KeyBinding::new("ctrl-h", Replace, Some(CONTEXT)),
340 ]);
341}
342
343#[derive(Debug, Clone, Copy, PartialEq, Eq)]
350pub(super) struct ColumnarPoint {
351 offset: usize,
352 columns_past_line_end: usize,
353}
354
355impl ColumnarPoint {
356 pub(super) fn new(offset: usize, columns_past_line_end: usize) -> Self {
357 Self {
358 offset,
359 columns_past_line_end,
360 }
361 }
362}
363
364#[derive(Debug, PartialEq)]
368struct PasteTarget {
369 document_revision: u64,
370 selections: Vec<CursorSelection>,
371}
372
373#[derive(Clone, Copy, Debug)]
378#[doc(hidden)]
379pub struct PrepaintCaretGeometry {
380 cursor_bounds: Bounds<Pixels>,
381 line_height: Pixels,
382 input_bounds: Bounds<Pixels>,
383 scroll_offset: Point<Pixels>,
384 pub(super) resize_reveal_owned: bool,
386}
387
388impl PrepaintCaretGeometry {
389 pub(super) fn new(
390 cursor_bounds: Bounds<Pixels>,
391 line_height: Pixels,
392 input_bounds: Bounds<Pixels>,
393 scroll_offset: Point<Pixels>,
394 ) -> Self {
395 Self {
396 cursor_bounds,
397 line_height,
398 input_bounds,
399 scroll_offset,
400 resize_reveal_owned: false,
401 }
402 }
403
404 pub fn cursor_bounds(&self) -> Bounds<Pixels> {
406 self.cursor_bounds
407 }
408
409 pub fn line_height(&self) -> Pixels {
411 self.line_height
412 }
413
414 pub fn input_bounds(&self) -> Bounds<Pixels> {
416 self.input_bounds
417 }
418
419 pub fn scroll_offset(&self) -> Point<Pixels> {
422 self.scroll_offset
423 }
424}
425
426pub struct InputBaseState<M: InputModeKind> {
437 pub(crate) extras: M::Extras,
439 pub(super) focus_handle: FocusHandle,
440 selection_focus: Option<WeakFocusHandle>,
441 pub(super) mode: LayoutMode,
442 pub(super) text: Rope,
443 pub(super) display_map: DisplayMap,
444 pub(super) undo_manager: UndoManager,
445 pub(super) inline_tokens: Option<Box<super::inline_tokens::InlineTokenStore>>,
446 pub(super) pending_token: Option<super::InlineToken>,
447 pub(super) replaying_history: bool,
448 pub(super) validated_token_edit: bool,
449 pub(super) document_revision: u64,
450 pub(super) token_presentation: super::InlineTokenPresentation,
451 pub(super) token_layout_cache: Option<Box<super::token_presentation::TokenLayoutCache>>,
452 pub(super) token_bounds: std::collections::HashMap<usize, Bounds<Pixels>>,
455 pub(super) hovered_token: Option<super::token_presentation::HoverSnapshot>,
460 pub(super) pending_token_hover_exits: Vec<super::token_presentation::HoverSnapshot>,
461 pub(super) pressed_token: Option<(usize, u64, Point<Pixels>)>,
464 pub(super) search_session: super::SearchSession,
465 pub(super) search_activation_revision: u64,
468 pub(super) searchable: bool,
469 pub(super) replaceable: bool,
470 pub(super) soft_wrap: bool,
471 pub(super) wrapping_indent: WrappingIndent,
472 pub(super) scroll_beyond_last_line: Option<usize>,
473 pub(super) cursor_surrounding_lines: Option<usize>,
474 pub(super) blink_cursor: Entity<BlinkCursor>,
475 pub(super) loading: bool,
476 pub(super) selections: Selections,
480 pub(super) selected_word_range: Option<CursorSelection>,
482 pub(super) ime_marked_range: Option<CursorSelection>,
484 ime_original_text: Option<Rope>,
486 pub(super) last_layout: Option<LastLayout>,
487 pub(crate) prepaint_caret_geometry: Option<PrepaintCaretGeometry>,
492 pub(super) last_cursor: Option<usize>,
493 pub(super) ime_anchor: Option<(Point<Pixels>, Option<Bounds<Pixels>>)>,
495 pub(super) input_bounds: Bounds<Pixels>,
497 pub(super) last_bounds: Option<Bounds<Pixels>>,
499 pub(super) revealed_viewport: Option<Bounds<Pixels>>,
502 pub(super) last_selected_range: Option<CursorSelection>,
503 pub(super) last_input_edits: Vec<(InputEdit, Rope)>,
505 pub(super) selecting: bool,
506 pub(super) column_select_start: Option<ColumnarPoint>,
508 pub(super) touch_selection: super::touch::TouchSelection,
510 pub(crate) disabled: bool,
511 pub(crate) readonly: bool,
512 pub(crate) text_align: TextAlign,
513 pub(super) masked: bool,
514 pub(super) clean_on_escape: bool,
515 pub(super) submit_on_enter: bool,
516 pub(super) input_action: Option<TextInputAction>,
518 pub(super) autocorrect: Option<bool>,
519 pub(super) autocapitalize: Option<Autocapitalize>,
520 pub(super) show_whitespaces: bool,
521 pub(crate) cursor_line_end_affinity: bool,
523 pub(super) pattern: Option<regex::Regex>,
524 pub(super) validate: Option<Box<dyn Fn(&str, &mut App) -> bool + 'static>>,
525 pub(crate) number_step: Option<NumberStep>,
528 pub(crate) number_min: Option<f64>,
530 pub(crate) number_max: Option<f64>,
532 pub(crate) scroll_handle: ScrollHandle,
533 scroll_bounce: bool,
535 pub(super) overscroll: Overscroll,
537 pub(crate) deferred_scroll_offset: Option<Point<Pixels>>,
539 pub(crate) scroll_size: gpui::Size<Pixels>,
541 pub(super) editor_scrollbar_snapshot: Cell<Option<EditorScrollbarSnapshot>>,
542 pub(super) overview_ruler_rows: std::cell::RefCell<OverviewRulerRows>,
544 pub(super) longest_line_width: Cell<Option<(LongestLineKey, Pixels)>>,
546 pub(super) editor_paddings: Edges<Pixels>,
547 pub(super) editor_style: InputEditorStyle,
552 projected_editor_style: InputEditorStyle,
557 pub(super) fold_placeholder_renderer: Option<FoldPlaceholderRenderer>,
558 pub(super) fold_background: Option<Hsla>,
559 accessibility: Option<InputAccessibility>,
560 pub(super) scrollbar_renderer: Option<crate::input::EditorScrollbarRenderer>,
563
564 pub(crate) mask_pattern: MaskPattern,
566 pub(super) mask_pattern_set: bool,
570 pub(super) placeholder: SharedString,
571
572 pub(super) diagnostic_popover: Option<Rc<crate::input::DiagnosticEntry>>,
574
575 context_menu_handler: Option<
576 Rc<dyn Fn(NativeMenu, InputContextMenuCapabilities, Point<Pixels>, &mut Window, &mut App)>,
577 >,
578 pending_context_menu: Option<(Point<Pixels>, usize)>,
579
580 pub(super) enable_context_menu: bool,
583
584 pub(super) completion_inserting: bool,
586 pub(super) overlay_action_handler: Option<
587 Rc<
588 dyn Fn(
589 super::InputOverlayKind,
590 Box<dyn Action>,
591 &mut Window,
592 &mut Context<InputBaseState<M>>,
593 ) -> bool,
594 >,
595 >,
596
597 _pending_update: bool,
601 pub(super) silent_replace_text: bool,
603 pub(super) emit_events: bool,
605
606 _subscriptions: Vec<Subscription>,
607
608 pub(super) auto_scroll: AutoScroll,
609}
610
611#[derive(Clone, Debug)]
618pub struct InputAccessibility {
619 role: Option<gpui::Role>,
620 label: Option<SharedString>,
621 placeholder: Option<SharedString>,
622 accessibility_id: Option<SharedString>,
623 value_exposed: bool,
624}
625
626impl InputAccessibility {
627 pub fn new(role: Option<gpui::Role>) -> Self {
628 Self {
629 role,
630 label: None,
631 placeholder: None,
632 accessibility_id: None,
633 value_exposed: true,
634 }
635 }
636
637 pub fn with_label(mut self, label: Option<SharedString>) -> Self {
638 self.label = label;
639 self
640 }
641
642 pub fn with_placeholder(mut self, placeholder: Option<SharedString>) -> Self {
643 self.placeholder = placeholder;
644 self
645 }
646
647 pub fn with_accessibility_id(mut self, id: Option<SharedString>) -> Self {
648 self.accessibility_id = id;
649 self
650 }
651
652 pub fn with_value_exposed(mut self, exposed: bool) -> Self {
654 self.value_exposed = exposed;
655 self
656 }
657
658 pub fn role(&self) -> Option<gpui::Role> {
659 self.role
660 }
661
662 pub fn label(&self) -> Option<&SharedString> {
663 self.label.as_ref()
664 }
665
666 pub fn placeholder(&self) -> Option<&SharedString> {
667 self.placeholder.as_ref()
668 }
669
670 pub fn accessibility_id(&self) -> Option<&SharedString> {
671 self.accessibility_id.as_ref()
672 }
673
674 pub fn value_exposed(&self) -> bool {
675 self.value_exposed
676 }
677}
678
679#[derive(Clone)]
684pub struct InputPresentation {
685 focus_handle: FocusHandle,
686 disabled: bool,
687 readonly: bool,
688 loading: bool,
689 masked: bool,
690 multi_line: bool,
691 code_editor: bool,
692 text_align: TextAlign,
693 placeholder: SharedString,
694 mask_placeholder: Option<String>,
695}
696
697impl InputPresentation {
698 pub fn focus_handle(&self) -> &FocusHandle {
699 &self.focus_handle
700 }
701
702 pub fn is_disabled(&self) -> bool {
703 self.disabled
704 }
705
706 pub fn is_readonly(&self) -> bool {
707 self.readonly
708 }
709
710 pub fn is_editable(&self) -> bool {
714 !self.disabled && !self.readonly
715 }
716
717 pub fn is_loading(&self) -> bool {
718 self.loading
719 }
720
721 pub fn is_masked(&self) -> bool {
722 self.masked
723 }
724
725 pub fn is_multi_line(&self) -> bool {
726 self.multi_line
727 }
728
729 pub fn is_code_editor(&self) -> bool {
730 self.code_editor
731 }
732
733 pub fn text_align(&self) -> TextAlign {
734 self.text_align
735 }
736
737 pub fn placeholder(&self) -> &SharedString {
738 &self.placeholder
739 }
740
741 pub fn mask_placeholder(&self) -> Option<&str> {
743 self.mask_placeholder.as_deref()
744 }
745}
746
747impl<M: InputModeKind> EventEmitter<InputEvent> for InputBaseState<M> {}
748impl<M: InputModeKind> EventEmitter<InputEditsEvent> for InputBaseState<M> {}
749
750impl<M: InputModeKind> InputBaseState<M> {
751 #[doc(hidden)]
752 pub fn cursor_layout(&self) -> Option<(Bounds<Pixels>, Pixels)> {
753 let layout = self.last_layout.as_ref()?;
754 Some((layout.cursor_bounds?, layout.line_height))
755 }
756
757 #[doc(hidden)]
762 pub fn prepaint_caret_geometry(&self) -> Option<PrepaintCaretGeometry> {
763 self.prepaint_caret_geometry
764 }
765
766 pub fn input_bounds(&self) -> Bounds<Pixels> {
767 self.input_bounds
768 }
769
770 pub fn text_bounds(&self) -> Option<Bounds<Pixels>> {
771 self.last_bounds
772 }
773
774 pub fn display_map(&self) -> &DisplayMap {
785 &self.display_map
786 }
787
788 pub fn prewrap(
799 &mut self,
800 font: gpui::Font,
801 font_size: Pixels,
802 width: Pixels,
803 cx: &mut Context<Self>,
804 ) {
805 if self.last_layout.is_some() {
806 return;
807 }
808 let wrap_width = (width - RIGHT_MARGIN).max(px(1.));
809 self.display_map.set_font(font, font_size, cx);
810 self.display_map
811 .set_tab_size(self.mode.tab_size().tab_size, cx);
812 self.display_map
813 .on_layout_changed(self.soft_wrap.then_some(wrap_width), cx);
814 self.display_map
815 .set_wrapping_indent(self.wrapping_indent, cx);
816 self.display_map.ensure_text_prepared(&self.text, cx);
817 }
818
819 pub fn diagnostic_popover(&self) -> Option<Rc<crate::input::DiagnosticEntry>> {
820 self.diagnostic_popover.clone()
821 }
822
823 pub fn presentation(&self) -> InputPresentation {
824 InputPresentation {
825 focus_handle: self.focus_handle.clone(),
826 disabled: self.disabled,
827 readonly: self.readonly,
828 loading: self.loading,
829 masked: self.masked,
830 multi_line: self.is_multi_line(),
831 code_editor: self.is_code_editor(),
832 text_align: self.text_align,
833 placeholder: self.placeholder.clone(),
834 mask_placeholder: self.mask_pattern.placeholder(),
835 }
836 }
837
838 #[inline]
843 pub(crate) fn shows_scrollbar(&self) -> bool {
851 self.is_multi_line()
852 }
853
854 pub fn is_multi_line(&self) -> bool {
855 M::MULTI_LINE
856 }
857
858 #[inline]
860 pub fn is_single_line(&self) -> bool {
861 !M::MULTI_LINE
862 }
863
864 #[inline]
866 pub fn is_code_editor(&self) -> bool {
867 M::CODE_EDITOR
868 }
869
870 pub fn is_copyable(&self) -> bool {
874 self.selections.iter().any(|sel| !sel.is_empty()) && !self.masked
875 }
876
877 pub fn context_menu_capabilities(&self) -> InputContextMenuCapabilities {
878 let (go_to_definition, code_actions) = self.extras.context_menu_capabilities();
879 InputContextMenuCapabilities::new()
880 .disabled(self.disabled)
881 .readonly(self.readonly)
882 .code_editor(self.is_code_editor())
883 .selection(!self.active_selection().is_empty())
884 .masked(self.masked)
885 .go_to_definition(go_to_definition)
886 .code_actions(code_actions)
887 }
888
889 pub fn set_text_align(&mut self, text_align: TextAlign, cx: &mut Context<Self>) {
890 if !self.is_single_line() || self.text_align == text_align {
891 return;
892 }
893
894 self.text_align = text_align;
895 cx.notify();
896 }
897
898 pub fn toggle_masked(&mut self, _: &mut Window, cx: &mut Context<Self>) {
904 self.masked = !self.masked;
905 cx.notify();
906 }
907
908 pub fn on_context_menu(
909 &mut self,
910 handler: Rc<
911 dyn Fn(NativeMenu, InputContextMenuCapabilities, Point<Pixels>, &mut Window, &mut App),
912 >,
913 ) {
914 self.context_menu_handler = Some(handler);
915 }
916
917 pub fn set_selection_focus(&mut self, focus: Option<FocusHandle>, cx: &mut Context<Self>) {
922 self.selection_focus = focus.as_ref().map(FocusHandle::downgrade);
923 cx.notify();
924 }
925
926 pub fn has_selection_focus(&self, window: &Window, cx: &App) -> bool {
928 self.focus_handle.is_focused(window)
929 || self
930 .selection_focus
931 .as_ref()
932 .and_then(WeakFocusHandle::upgrade)
933 .is_some_and(|focus| focus.is_focused(window) || focus.contains_focused(window, cx))
934 }
935
936 fn new_in_mode(window: &mut Window, cx: &mut Context<Self>) -> Self {
938 let focus_handle = cx.focus_handle().tab_stop(true);
939 let blink_cursor = cx.new(BlinkCursor::new);
940 let undo_manager = UndoManager::new();
941
942 let _subscriptions = vec![
943 cx.intercept_keystrokes({
946 let focus_handle = focus_handle.clone();
947 let blink_cursor = blink_cursor.downgrade();
948 move |_, window, cx| {
949 if focus_handle.is_focused(window) {
950 _ = blink_cursor.update(cx, |cursor, cx| cursor.pause(cx));
951 }
952 }
953 }),
954 cx.observe(&blink_cursor, |_, _, cx| cx.notify()),
956 cx.observe_window_activation(window, |input, window, cx| {
958 if window.is_window_active() {
959 let focus_handle = input.focus_handle.clone();
960 if focus_handle.is_focused(window) {
961 input.blink_cursor.update(cx, |blink_cursor, cx| {
962 blink_cursor.start(cx);
963 });
964 }
965 }
966 }),
967 cx.on_focus(&focus_handle, window, Self::on_focus),
968 cx.on_blur(&focus_handle, window, Self::on_blur),
969 ];
970
971 let text_style = window.text_style();
972
973 Self {
974 extras: M::Extras::default(),
975 focus_handle: focus_handle.clone(),
976 selection_focus: None,
977 text: "".into(),
978 display_map: DisplayMap::new(text_style.font(), window.rem_size(), None),
979 search_session: super::SearchSession::default(),
980 search_activation_revision: 0,
981 searchable: false,
982 replaceable: true,
983 soft_wrap: true,
984 wrapping_indent: WrappingIndent::default(),
985 scroll_beyond_last_line: None,
986 cursor_surrounding_lines: None,
987 blink_cursor,
988 undo_manager,
989 inline_tokens: None,
990 pending_token: None,
991 replaying_history: false,
992 validated_token_edit: false,
993 document_revision: 0,
994 token_presentation: Default::default(),
995 token_bounds: Default::default(),
996 hovered_token: None,
997 pending_token_hover_exits: Vec::new(),
998 token_layout_cache: None,
999 pressed_token: None,
1000 selections: Selections::default(),
1001 selected_word_range: None,
1002 ime_marked_range: None,
1003 ime_original_text: None,
1004 input_bounds: Bounds::default(),
1005 selecting: false,
1006 touch_selection: Default::default(),
1007 disabled: false,
1008 readonly: false,
1009 text_align: TextAlign::Left,
1010 masked: false,
1011 clean_on_escape: false,
1012 submit_on_enter: false,
1013 input_action: None,
1014 autocorrect: None,
1015 autocapitalize: None,
1016 show_whitespaces: false,
1017 loading: false,
1018 pattern: None,
1019 validate: None,
1020 number_step: Some(NumberStep::Fixed(1.)),
1021 number_min: None,
1022 number_max: None,
1023 mode: LayoutMode::default(),
1024 last_layout: None,
1025 prepaint_caret_geometry: None,
1026 last_bounds: None,
1027 revealed_viewport: None,
1028 last_selected_range: None,
1029 last_input_edits: Vec::new(),
1030 column_select_start: None,
1031 last_cursor: None,
1032 ime_anchor: None,
1033 scroll_handle: ScrollHandle::new(),
1034 scroll_bounce: crate::ScrollBounce::enabled_by_default(),
1035 overscroll: Overscroll::default(),
1036 scroll_size: gpui::size(px(0.), px(0.)),
1037 editor_scrollbar_snapshot: Cell::new(None),
1038 overview_ruler_rows: Default::default(),
1039 longest_line_width: Cell::new(None),
1040 editor_paddings: Edges::default(),
1041 deferred_scroll_offset: None,
1042 placeholder: SharedString::default(),
1043 mask_pattern: MaskPattern::default(),
1044 mask_pattern_set: false,
1045 editor_style: InputEditorStyle::default(),
1046 projected_editor_style: InputEditorStyle::default(),
1047 fold_placeholder_renderer: None,
1048 fold_background: None,
1049 accessibility: None,
1050 scrollbar_renderer: None,
1051 diagnostic_popover: None,
1052 context_menu_handler: None,
1053 pending_context_menu: None,
1054 enable_context_menu: true,
1055 completion_inserting: false,
1056 overlay_action_handler: None,
1057 silent_replace_text: false,
1058 emit_events: true,
1059 _subscriptions,
1060 _pending_update: false,
1061 cursor_line_end_affinity: false,
1062 auto_scroll: AutoScroll::default(),
1063 }
1064 }
1065
1066 pub fn context_menu(mut self, enable: bool) -> Self {
1073 self.enable_context_menu = enable;
1074 self
1075 }
1076
1077 pub fn set_context_menu_enabled(&mut self, enabled: bool) {
1078 self.enable_context_menu = enabled;
1079 }
1080
1081 #[doc(hidden)]
1083 pub fn replaceable(mut self, allow: bool) -> Self {
1084 self.replaceable = allow;
1085 self
1086 }
1087
1088 pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
1090 self.placeholder = placeholder.into();
1091 self
1092 }
1093
1094 pub fn set_highlighter(
1096 &mut self,
1097 new_language: impl Into<SharedString>,
1098 cx: &mut Context<Self>,
1099 ) {
1100 match &mut self.mode {
1101 LayoutMode::CodeEditor {
1102 language,
1103 highlighter,
1104 ..
1105 } => {
1106 language.set_name(new_language.into());
1107 *highlighter.borrow_mut() = None;
1108 }
1109 _ => {}
1110 }
1111 self.refresh(cx);
1112 }
1113
1114 fn reset_highlighter(&mut self, cx: &mut Context<Self>) {
1115 match &mut self.mode {
1116 LayoutMode::CodeEditor { highlighter, .. } => {
1117 *highlighter.borrow_mut() = None;
1118 }
1119 _ => {}
1120 }
1121 cx.notify();
1122 }
1123
1124 pub fn set_highlighter_factory(
1126 &mut self,
1127 factory: InputHighlighterFactory,
1128 cx: &mut Context<Self>,
1129 ) {
1130 self.mode.set_highlighter_factory(factory);
1131 self._pending_update = true;
1132 self.token_layout_cache = None;
1133 cx.notify();
1134 }
1135
1136 pub fn ensure_highlighter_factory(&mut self, factory: InputHighlighterFactory) {
1138 self.mode.ensure_highlighter_factory(factory);
1139 }
1140
1141 pub fn set_editor_style(&mut self, style: InputEditorStyle) {
1142 self.editor_style = style.clone();
1143 self.projected_editor_style = style;
1144 }
1145
1146 pub fn set_fold_placeholder_renderer(
1149 &mut self,
1150 renderer: Option<FoldPlaceholderRenderer>,
1151 cx: &mut Context<Self>,
1152 ) {
1153 self.fold_placeholder_renderer = renderer;
1154 cx.notify();
1155 }
1156
1157 pub fn set_fold_background(&mut self, color: Option<Hsla>, cx: &mut Context<Self>) {
1160 self.fold_background = color;
1161 cx.notify();
1162 }
1163
1164 pub fn set_accessibility(&mut self, accessibility: InputAccessibility) {
1166 self.accessibility = Some(accessibility);
1167 }
1168
1169 fn handle_accessibility_focus(&self, window: &mut Window, cx: &mut Context<Self>) {
1170 if !self.disabled {
1171 self.focus(window, cx);
1172 }
1173 }
1174
1175 fn handle_accessibility_set_value(
1176 &mut self,
1177 data: Option<&gpui::accesskit::ActionData>,
1178 window: &mut Window,
1179 cx: &mut Context<Self>,
1180 ) {
1181 let Some(gpui::accesskit::ActionData::Value(value)) = data else {
1182 return;
1183 };
1184 if self.is_editable() {
1186 self.replace_all(value.to_string(), window, cx);
1187 }
1188 }
1189
1190 #[doc(hidden)]
1192 pub fn set_editor_paddings(&mut self, paddings: Edges<Pixels>) {
1193 self.editor_paddings = paddings;
1194 }
1195
1196 pub fn set_scrollbar_renderer(
1204 &mut self,
1205 renderer: Option<crate::input::EditorScrollbarRenderer>,
1206 ) {
1207 self.scrollbar_renderer = renderer;
1208 }
1209
1210 pub fn apply_highlighter_fold_candidates(
1211 &mut self,
1212 candidates: Vec<crate::input::FoldRange>,
1213 cx: &mut Context<Self>,
1214 ) {
1215 if self.mode.is_folding() {
1216 self.display_map.set_fold_candidates(candidates);
1217 }
1218 cx.notify();
1219 }
1220
1221 #[inline]
1222 pub fn diagnostics(&self) -> Option<&DiagnosticSet> {
1223 self.mode.diagnostics()
1224 }
1225
1226 #[inline]
1227 pub fn diagnostics_mut(&mut self) -> Option<&mut DiagnosticSet> {
1228 self.mode.diagnostics_mut()
1229 }
1230
1231 pub fn set_placeholder(
1233 &mut self,
1234 placeholder: impl Into<SharedString>,
1235 _: &mut Window,
1236 cx: &mut Context<Self>,
1237 ) {
1238 self.placeholder = placeholder.into();
1239 cx.notify();
1240 }
1241
1242 pub(super) fn line_and_position_for_offset(
1250 &self,
1251 offset: usize,
1252 ) -> (usize, usize, Option<Point<Pixels>>) {
1253 let Some(last_layout) = &self.last_layout else {
1254 return (0, 0, None);
1255 };
1256 let line_height = last_layout.line_height;
1257
1258 let mut y_offset = last_layout.visible_top;
1259 for (vi, line) in last_layout.lines.iter().enumerate() {
1260 let prev_lines_offset = last_layout.visible_line_byte_offsets[vi];
1261 let local_offset = offset.saturating_sub(prev_lines_offset);
1262 if let Some(pos) = line.position_for_index(local_offset, last_layout, false) {
1263 let sub_line_index = (pos.y / line_height) as usize;
1264 let adjusted_pos = point(pos.x + last_layout.line_number_width, pos.y + y_offset);
1265 return (vi, sub_line_index, Some(adjusted_pos));
1266 }
1267
1268 y_offset += line.size(line_height).height;
1269 }
1270 (0, 0, None)
1271 }
1272
1273 pub fn set_value(
1282 &mut self,
1283 value: impl Into<super::InputContent>,
1284 window: &mut Window,
1285 cx: &mut Context<Self>,
1286 ) {
1287 let content = value.into();
1288 self.inline_tokens = None;
1289 self.undo_manager.set_ignoring(true);
1290 self.emit_events = false;
1291 self.replace_text(content.text().clone(), window, cx);
1292 self.ime_original_text = None;
1293 self.install_tokens(content);
1294 self.undo_manager.set_ignoring(false);
1295 self.emit_events = true;
1296
1297 self.reset_selection();
1298 self.reset_lsp_state();
1299 self.reset_scroll_to_start();
1300
1301 self.undo_manager.clear();
1302 cx.notify();
1303 }
1304
1305 pub fn replace_all(
1315 &mut self,
1316 text: impl Into<SharedString>,
1317 window: &mut Window,
1318 cx: &mut Context<Self>,
1319 ) {
1320 self.replace_text(text, window, cx);
1321 self.reset_selection();
1322 self.reset_lsp_state();
1323 self.reset_scroll_to_start();
1324
1325 cx.notify();
1326 }
1327
1328 pub(super) fn with_edits_allowed(&mut self, f: impl FnOnce(&mut Self)) {
1333 let (was_disabled, was_readonly) = (self.disabled, self.readonly);
1334 (self.disabled, self.readonly) = (false, false);
1335 f(self);
1336 (self.disabled, self.readonly) = (was_disabled, was_readonly);
1337 }
1338
1339 pub fn insert(
1343 &mut self,
1344 text: impl Into<SharedString>,
1345 window: &mut Window,
1346 cx: &mut Context<Self>,
1347 ) {
1348 let text: SharedString = text.into();
1349 self.with_edits_allowed(|this| {
1350 this.undo_manager.set_pending_intent(EditIntent::Atomic);
1351 let range_utf16 = this.range_to_utf16(&(this.cursor()..this.cursor()));
1352 this.replace_text_in_range_silent(Some(range_utf16), &text, window, cx);
1353 let end = this.active_selection().end;
1354 this.set_cursor_to(end);
1355 });
1356 }
1357
1358 pub fn replace(
1362 &mut self,
1363 text: impl Into<SharedString>,
1364 window: &mut Window,
1365 cx: &mut Context<Self>,
1366 ) {
1367 let text: SharedString = text.into();
1368 self.with_edits_allowed(|this| {
1369 this.undo_manager.set_pending_intent(EditIntent::Atomic);
1370 this.replace_text_in_range_silent(None, &text, window, cx);
1371 let end = this.active_selection().end;
1372 this.set_cursor_to(end);
1373 });
1374 }
1375
1376 fn replace_text(
1377 &mut self,
1378 text: impl Into<SharedString>,
1379 window: &mut Window,
1380 cx: &mut Context<Self>,
1381 ) {
1382 let text: SharedString = text.into();
1383 self.with_edits_allowed(|this| {
1384 this.undo_manager.set_pending_intent(EditIntent::Atomic);
1385 let range = 0..this.text.chars().map(|c| c.len_utf16()).sum();
1386 this.replace_text_in_range_silent(Some(range), &text, window, cx);
1387 this.reset_highlighter(cx);
1388 });
1389 }
1390
1391 fn reset_selection(&mut self) {
1392 self.selections.remove_all_but_active();
1393
1394 if self.is_single_line() {
1398 let end = self.text.len();
1399 self.set_cursor_to(end);
1400 } else {
1401 self.active_selection_mut().clear();
1402 }
1403 }
1404
1405 fn reset_lsp_state(&mut self) {
1406 if self.is_code_editor() {
1407 self._pending_update = true;
1408 M::reset_language_features(self);
1409 }
1410 }
1411
1412 fn reset_scroll_to_start(&mut self) {
1413 self.scroll_handle.set_offset(point(px(0.), px(0.)));
1417 if self.is_single_line() {
1418 self.deferred_scroll_offset = Some(point(px(0.), px(0.)));
1419 }
1420 }
1421
1422 #[allow(unused)]
1426 pub(crate) fn disabled(mut self, disabled: bool) -> Self {
1427 self.disabled = disabled;
1428 self
1429 }
1430
1431 pub fn set_disabled(&mut self, disabled: bool, cx: &mut Context<Self>) {
1432 if self.disabled == disabled {
1433 return;
1434 }
1435
1436 self.disabled = disabled;
1437 if disabled {
1438 M::hide_context_menu(self, cx);
1439 M::clear_inline_completion(self, cx);
1440 }
1441 cx.notify();
1442 }
1443
1444 #[allow(unused)]
1452 pub(crate) fn readonly(mut self, readonly: bool) -> Self {
1453 self.readonly = readonly;
1454 self
1455 }
1456
1457 pub fn set_readonly(&mut self, readonly: bool, cx: &mut Context<Self>) {
1458 if self.readonly == readonly {
1459 return;
1460 }
1461
1462 self.readonly = readonly;
1463 if readonly {
1464 M::hide_context_menu(self, cx);
1465 M::clear_inline_completion(self, cx);
1466 self.search_session.replace_mode = false;
1467 }
1468 cx.notify();
1469 }
1470
1471 pub fn is_editable(&self) -> bool {
1476 !self.disabled && !self.readonly
1477 }
1478
1479 pub fn clean_on_escape(mut self) -> Self {
1483 self.clean_on_escape = true;
1484 self
1485 }
1486
1487 pub fn set_clean_on_escape(&mut self, clean: bool) {
1488 self.clean_on_escape = clean;
1489 }
1490
1491 #[doc(hidden)]
1496 pub fn submit_on_enter(mut self, submit: bool) -> Self {
1497 self.submit_on_enter = submit;
1498 self
1499 }
1500
1501 pub fn set_submit_on_enter(&mut self, submit: bool, cx: &mut Context<Self>) {
1502 self.submit_on_enter = submit;
1503 cx.notify();
1504 }
1505
1506 pub fn input_action(mut self, action: TextInputAction) -> Self {
1513 self.input_action = Some(action);
1514 self
1515 }
1516
1517 pub fn autocorrect(mut self, autocorrect: bool) -> Self {
1523 self.autocorrect = Some(autocorrect);
1524 self
1525 }
1526
1527 pub fn autocapitalize(mut self, autocapitalize: Autocapitalize) -> Self {
1532 self.autocapitalize = Some(autocapitalize);
1533 self
1534 }
1535
1536 pub fn text_input_configuration(&self) -> TextInputConfiguration {
1540 let autocorrect = self.autocorrect.unwrap_or(false);
1544 let input_action =
1545 self.input_action
1546 .unwrap_or(if self.is_multi_line() && !self.submit_on_enter {
1547 TextInputAction::Enter
1548 } else {
1549 TextInputAction::Done
1550 });
1551 TextInputConfiguration {
1552 autocorrect,
1553 autocapitalize: self.autocapitalize.unwrap_or(Autocapitalize::None),
1554 suggestions: autocorrect,
1555 input_action,
1556 }
1557 }
1558
1559 #[doc(hidden)]
1561 pub fn show_whitespaces(mut self, show: bool) -> Self {
1562 self.show_whitespaces = show;
1563 self
1564 }
1565
1566 pub fn set_show_whitespaces(&mut self, show: bool, _: &mut Window, cx: &mut Context<Self>) {
1568 self.show_whitespaces = show;
1569 cx.notify();
1570 }
1571
1572 pub fn scroll_beyond_last_line(mut self, rows: Option<usize>) -> Self {
1582 self.scroll_beyond_last_line = rows;
1583 self
1584 }
1585
1586 pub fn set_scroll_beyond_last_line(
1588 &mut self,
1589 rows: Option<usize>,
1590 _: &mut Window,
1591 cx: &mut Context<Self>,
1592 ) {
1593 if self.scroll_beyond_last_line == rows {
1594 return;
1595 }
1596 self.scroll_beyond_last_line = rows;
1597 cx.notify();
1598 }
1599
1600 pub fn cursor_surrounding_lines(mut self, lines: Option<usize>) -> Self {
1614 self.cursor_surrounding_lines = lines;
1615 self
1616 }
1617
1618 pub fn set_cursor_surrounding_lines(
1620 &mut self,
1621 lines: Option<usize>,
1622 _: &mut Window,
1623 cx: &mut Context<Self>,
1624 ) {
1625 if self.cursor_surrounding_lines == lines {
1626 return;
1627 }
1628 self.cursor_surrounding_lines = lines;
1629 cx.notify();
1630 }
1631
1632 pub fn default_value(mut self, value: impl Into<SharedString>) -> Self {
1634 let text: SharedString = value.into();
1635 self.text = Rope::from(self.normalize_input(&text).as_ref());
1636 if let Some(diagnostics) = self.mode.diagnostics_mut() {
1637 diagnostics.reset(&self.text)
1638 }
1639 self._pending_update = true;
1642 self
1643 }
1644
1645 pub fn value(&self) -> SharedString {
1650 SharedString::new(self.text.to_string())
1651 }
1652
1653 pub fn selected_value(&self) -> SharedString {
1659 SharedString::new(self.selected_text().to_string())
1660 }
1661
1662 pub fn unmask_value(&self) -> SharedString {
1664 self.mask_pattern.unmask(&self.text.to_string()).into()
1665 }
1666
1667 #[doc(hidden)]
1673 pub fn prepare(&mut self, _: &mut Window, _: &mut Context<Self>) {}
1674
1675 pub fn text(&self) -> &Rope {
1680 &self.text
1681 }
1682
1683 fn emit_change(&mut self, cx: &mut Context<Self>) {
1685 if let Some(original) = self.ime_original_text.take() {
1686 self.last_input_edits = vec![(
1687 Self::full_input_edit(&original, &self.text),
1688 self.text.clone(),
1689 )];
1690 }
1691 cx.emit(InputEditsEvent {
1692 edits: self.last_input_edits.clone(),
1693 });
1694 cx.emit(InputEvent::Change);
1695 }
1696
1697 fn full_input_edit(old_text: &Rope, new_text: &Rope) -> InputEdit {
1698 InputEdit {
1699 start_byte: 0,
1700 old_end_byte: old_text.len(),
1701 new_end_byte: new_text.len(),
1702 start_position: old_text.offset_to_point(0),
1703 old_end_position: old_text.offset_to_point(old_text.len()),
1704 new_end_position: new_text.offset_to_point(new_text.len()),
1705 }
1706 }
1707
1708 pub fn cursor_position(&self) -> Position {
1710 let offset = self.cursor();
1711 self.text.offset_to_position(offset)
1712 }
1713
1714 pub fn set_cursor_position(
1718 &mut self,
1719 position: impl Into<Position>,
1720 window: &mut Window,
1721 cx: &mut Context<Self>,
1722 ) {
1723 let position: Position = position.into();
1724 let offset = self.text.position_to_offset(&position);
1725
1726 self.move_to(offset, None, cx);
1727 self.update_preferred_column();
1728 self.focus(window, cx);
1729 }
1730
1731 pub fn focus(&self, window: &mut Window, cx: &mut Context<Self>) {
1733 self.focus_handle.focus(window, cx);
1734 self.blink_cursor.update(cx, |cursor, cx| {
1735 cursor.start(cx);
1736 });
1737 }
1738
1739 pub fn refresh(&mut self, cx: &mut Context<Self>) {
1753 self._pending_update = true;
1754 cx.notify();
1755 }
1756
1757 pub(super) fn select_left(&mut self, _: &SelectLeft, _: &mut Window, cx: &mut Context<Self>) {
1758 self.undo_manager.break_transaction_coalescing();
1759 self.select_all_cursors_to(|s, sel| s.previous_boundary(sel.cursor_offset()), cx);
1760 }
1761
1762 pub(super) fn select_right(&mut self, _: &SelectRight, _: &mut Window, cx: &mut Context<Self>) {
1763 self.select_all_cursors_to(|s, sel| s.next_boundary(sel.cursor_offset()), cx);
1764 }
1765
1766 pub(super) fn select_up(&mut self, _: &SelectUp, _: &mut Window, cx: &mut Context<Self>) {
1767 if self.is_single_line() {
1768 return;
1769 }
1770 self.select_all_cursors_to_with_affinity(
1771 |s, sel| {
1772 s.vertical_selection_target(
1773 sel.cursor_offset(),
1774 sel.column_anchor,
1775 s.line_end_affinity_for(sel),
1776 -1,
1777 )
1778 },
1779 true,
1780 cx,
1781 );
1782 }
1783
1784 pub(super) fn select_down(&mut self, _: &SelectDown, _: &mut Window, cx: &mut Context<Self>) {
1785 if self.is_single_line() {
1786 return;
1787 }
1788 self.select_all_cursors_to_with_affinity(
1789 |s, sel| {
1790 s.vertical_selection_target(
1791 sel.cursor_offset(),
1792 sel.column_anchor,
1793 s.line_end_affinity_for(sel),
1794 1,
1795 )
1796 },
1797 true,
1798 cx,
1799 );
1800 }
1801
1802 pub(super) fn on_action_select_all(
1803 &mut self,
1804 _: &SelectAll,
1805 window: &mut Window,
1806 cx: &mut Context<Self>,
1807 ) {
1808 self.select_all(window, cx);
1809 }
1810
1811 pub(super) fn select_to_start(
1812 &mut self,
1813 _: &SelectToStart,
1814 _: &mut Window,
1815 cx: &mut Context<Self>,
1816 ) {
1817 self.undo_manager.break_transaction_coalescing();
1818 self.select_all_cursors_to(|_, _| 0, cx);
1819 }
1820
1821 pub(super) fn select_to_end(
1822 &mut self,
1823 _: &SelectToEnd,
1824 _: &mut Window,
1825 cx: &mut Context<Self>,
1826 ) {
1827 self.undo_manager.break_transaction_coalescing();
1828 let end = self.text.len();
1829 self.select_all_cursors_to(move |_, _| end, cx);
1830 }
1831
1832 pub(super) fn select_to_start_of_line(
1833 &mut self,
1834 _: &SelectToStartOfLine,
1835 _: &mut Window,
1836 cx: &mut Context<Self>,
1837 ) {
1838 self.undo_manager.break_transaction_coalescing();
1839 self.select_all_cursors_to(
1840 |s, sel| s.start_of_line_at(sel.cursor_offset(), s.line_end_affinity_for(sel)),
1841 cx,
1842 );
1843 }
1844
1845 pub(super) fn select_to_end_of_line(
1846 &mut self,
1847 _: &SelectToEndOfLine,
1848 _: &mut Window,
1849 cx: &mut Context<Self>,
1850 ) {
1851 self.undo_manager.break_transaction_coalescing();
1852 self.select_all_cursors_to_with_affinity(
1853 |s, sel| {
1854 (
1855 s.end_of_line_at(sel.cursor_offset(), s.line_end_affinity_for(sel)),
1856 true,
1857 )
1858 },
1859 false,
1860 cx,
1861 );
1862 }
1863
1864 pub(super) fn select_to_previous_word(
1865 &mut self,
1866 _: &SelectToPreviousWordStart,
1867 _: &mut Window,
1868 cx: &mut Context<Self>,
1869 ) {
1870 self.undo_manager.break_transaction_coalescing();
1871 self.select_all_cursors_to(
1872 |s, sel| s.previous_start_of_word_at(sel.cursor_offset()),
1873 cx,
1874 );
1875 }
1876
1877 pub(super) fn select_to_next_word(
1878 &mut self,
1879 _: &SelectToNextWordEnd,
1880 _: &mut Window,
1881 cx: &mut Context<Self>,
1882 ) {
1883 self.undo_manager.break_transaction_coalescing();
1884 self.select_all_cursors_to(|s, sel| s.next_end_of_word_at(sel.cursor_offset()), cx);
1885 }
1886
1887 pub(super) fn previous_start_of_word_at(&self, offset: usize) -> usize {
1890 if self.masked {
1891 return 0;
1895 }
1896
1897 let offset = self.offset_from_utf16(self.offset_to_utf16(offset));
1898 let left_part = self.text.slice(0..offset).to_string();
1900
1901 let target = UnicodeSegmentation::split_word_bound_indices(left_part.as_str())
1902 .rfind(|(_, s)| !s.trim_start().is_empty())
1903 .map(|(i, _)| i)
1904 .unwrap_or(0);
1905 self.token_boundary(target, Bias::Left)
1906 }
1907
1908 pub(super) fn next_end_of_word_at(&self, offset: usize) -> usize {
1910 if self.masked {
1911 return self.text.len();
1913 }
1914
1915 let offset = self.offset_from_utf16(self.offset_to_utf16(offset));
1916 let right_part = self.text.slice(offset..self.text.len()).to_string();
1917
1918 let target = UnicodeSegmentation::split_word_bound_indices(right_part.as_str())
1919 .find(|(_, s)| !s.trim_start().is_empty())
1920 .map(|(i, s)| offset + i + s.len())
1921 .unwrap_or(self.text.len());
1922 self.token_boundary(target, Bias::Right)
1923 }
1924
1925 pub(super) fn start_of_line_at(&self, offset: usize, line_end_affinity: bool) -> usize {
1930 if self.is_single_line() {
1931 return 0;
1932 }
1933
1934 let row = self.text.offset_to_point(offset).row;
1935 let logical_start = self.text.line_start_offset(row);
1936
1937 if self.soft_wrap && self.is_code_editor() {
1938 let wrap_point = self
1939 .display_map
1940 .offset_to_wrap_display_point_with_affinity(offset, line_end_affinity);
1941 if let Some(line) = self.display_map.line(row)
1942 && let Some(range) = line.wrapped_lines.get(wrap_point.local_row)
1943 {
1944 let visual_start = logical_start + range.start;
1945 if offset != visual_start {
1946 return visual_start;
1947 }
1948 }
1949 }
1950
1951 logical_start
1952 }
1953
1954 pub(super) fn end_of_line_at(&self, offset: usize, line_end_affinity: bool) -> usize {
1959 if self.is_single_line() {
1960 return self.text.len();
1961 }
1962
1963 let row = self.text.offset_to_point(offset).row;
1964 let logical_start = self.text.line_start_offset(row);
1965 let logical_end = self.text.line_end_offset(row);
1966
1967 if self.soft_wrap && self.is_code_editor() {
1968 let wrap_point = self
1972 .display_map
1973 .offset_to_wrap_display_point_with_affinity(offset, line_end_affinity);
1974 if let Some(line) = self.display_map.line(row)
1975 && let Some(range) = line.wrapped_lines.get(wrap_point.local_row)
1976 {
1977 let visual_end = logical_start + range.end;
1978 if offset != visual_end {
1979 return visual_end;
1980 }
1981 }
1982 }
1983
1984 logical_end
1985 }
1986
1987 pub(super) fn indent_of_next_line(&mut self) -> String {
1991 self.indent_of_next_line_at(self.cursor())
1992 }
1993
1994 pub(super) fn indent_of_next_line_at(&mut self, offset: usize) -> String {
1996 if self.is_single_line() {
1997 return "".into();
1998 }
1999
2000 let mut current_indent = String::new();
2001 let mut next_indent = String::new();
2002 let line_end_affinity = self.line_end_affinity_at(offset);
2003 let current_line_start_pos = self.start_of_line_at(offset, line_end_affinity);
2004 let next_line_start_pos = self.end_of_line_at(offset, line_end_affinity);
2005 for c in self.text.slice(current_line_start_pos..).chars() {
2006 if !c.is_whitespace() {
2007 break;
2008 }
2009 if c == '\n' || c == '\r' {
2010 break;
2011 }
2012 current_indent.push(c);
2013 }
2014
2015 for c in self.text.slice(next_line_start_pos..).chars() {
2016 if !c.is_whitespace() {
2017 break;
2018 }
2019 if c == '\n' || c == '\r' {
2020 break;
2021 }
2022 next_indent.push(c);
2023 }
2024
2025 if next_indent.len() > current_indent.len() {
2026 return next_indent;
2027 }
2028
2029 if let Some(rules) = self.mode.language_config() {
2032 if self.mode.is_smart_indent()
2033 && M::editing_syntax_context(self, offset) == crate::input::SyntaxContext::Code
2034 {
2035 let line_before: String = self
2036 .text
2037 .slice(current_line_start_pos..offset)
2038 .chars()
2039 .collect();
2040 if rules.opens_indent(line_before.trim_end()) {
2041 let tab = self.mode.tab_size();
2042 if tab.hard_tabs {
2043 current_indent.push('\t');
2044 } else {
2045 for _ in 0..tab.tab_size {
2046 current_indent.push(' ');
2047 }
2048 }
2049 } else {
2050 let line_after = self.text.slice(offset..next_line_start_pos).to_string();
2051 if rules.closes_indent(&line_after) {
2052 let tab = self.mode.tab_size();
2053 if current_indent.ends_with('\t') {
2054 current_indent.pop();
2055 } else {
2056 for _ in 0..tab.tab_size {
2057 if !current_indent.ends_with(' ') {
2058 break;
2059 }
2060 current_indent.pop();
2061 }
2062 }
2063 }
2064 }
2065 }
2066 }
2067
2068 current_indent
2069 }
2070
2071 fn delete_selections(
2079 &mut self,
2080 silent: bool,
2081 collapsed_intent: EditIntent,
2082 mut collapsed_target: impl FnMut(&mut Self, usize) -> Range<usize>,
2083 window: &mut Window,
2084 cx: &mut Context<Self>,
2085 ) {
2086 if !self.is_editable() {
2087 return;
2088 }
2089 let cursors: Vec<CursorSelection> = self.selections.iter().copied().collect();
2090 let intent = if cursors.iter().all(|sel| sel.is_empty()) {
2091 collapsed_intent
2092 } else {
2093 EditIntent::Atomic
2094 };
2095 let mut new_selections: Vec<CursorSelection> = Vec::with_capacity(cursors.len());
2096 for sel in &cursors {
2097 let range = if sel.is_empty() {
2098 collapsed_target(self, sel.cursor_offset())
2099 } else {
2100 sel.start..sel.end
2101 };
2102 let (start, end) = (range.start.min(range.end), range.start.max(range.end));
2103 let mut selection = *sel;
2104 selection.start = start;
2105 selection.end = end;
2106 new_selections.push(selection);
2107 }
2108 self.undo_manager.begin_transaction_with(intent);
2111 self.undo_manager
2112 .record_selections(cursors.clone(), cursors.clone());
2113 self.selections.replace_all(new_selections);
2114 self.undo_manager.set_pending_intent(intent);
2115
2116 let was_silent = self.silent_replace_text;
2117 self.silent_replace_text = silent;
2118 self.replace_text_in_range(None, "", window, cx);
2119 self.silent_replace_text = was_silent;
2120 self.undo_manager
2121 .record_selections(cursors, self.selections.iter().copied().collect());
2122 self.undo_manager.commit_transaction();
2123 self.pause_blink_cursor(cx);
2124 }
2125
2126 fn auto_close_target(&self, range: &Range<usize>, text: &str) -> Option<(usize, SharedString)> {
2129 if self.silent_replace_text
2130 || self.ime_marked_range.is_some()
2131 || !self.selections.is_single()
2132 || !self.active_selection().is_empty()
2133 || !range.is_empty()
2134 || range.start != self.cursor()
2135 || text.chars().count() != 1
2136 || !self.mode.is_auto_close()
2137 {
2138 return None;
2139 }
2140 let rules = self.mode.language_config()?;
2141 if self
2142 .text
2143 .chars_at(range.start)
2144 .next()
2145 .is_some_and(|c| !c.is_whitespace() && !rules.auto_close_before.contains(c))
2146 {
2147 return None;
2148 }
2149 for (open, close, not_in) in rules.closing_pairs() {
2150 let Some(prefix) = open.strip_suffix(text) else {
2151 continue;
2152 };
2153 if !self.text_before_matches(range.start, prefix) {
2154 continue;
2155 }
2156 let start = range.start - prefix.len();
2157 if open == close
2158 && (self.is_escaped_at(start)
2159 || self
2160 .text
2161 .chars_at(start)
2162 .reversed()
2163 .next()
2164 .is_some_and(|c| c.is_alphanumeric() || c == '_'))
2165 {
2166 continue;
2167 }
2168 if not_in.contains(&M::editing_syntax_context(self, start)) {
2169 continue;
2170 }
2171 return Some((open.len(), close.into()));
2172 }
2173 None
2174 }
2175
2176 fn text_before_matches(&self, offset: usize, text: &str) -> bool {
2177 offset >= text.len()
2178 && self.text.is_char_boundary(offset - text.len())
2179 && self.text.slice(offset - text.len()..offset) == text
2180 }
2181
2182 fn text_after_matches(&self, offset: usize, text: &str) -> bool {
2183 offset + text.len() <= self.text.len()
2184 && self.text.is_char_boundary(offset + text.len())
2185 && self.text.slice(offset..offset + text.len()) == text
2186 }
2187
2188 fn is_escaped_at(&self, offset: usize) -> bool {
2189 self.text
2190 .chars_at(offset)
2191 .reversed()
2192 .take_while(|c| *c == '\\')
2193 .count()
2194 % 2
2195 == 1
2196 }
2197
2198 fn skip_over_target(&self, new_text: &str) -> Option<usize> {
2205 if !self.mode.is_auto_close() || new_text.chars().count() != 1 {
2206 return None;
2207 }
2208 let rules = self.mode.language_config()?;
2209 let cursor = self.cursor();
2210 for (open, close, not_in) in rules.closing_pairs() {
2211 for (index, _) in close.char_indices() {
2212 if !close[index..].starts_with(new_text)
2213 || !self.text_before_matches(cursor, &close[..index])
2214 || !self.text_after_matches(cursor, &close[index..])
2215 {
2216 continue;
2217 }
2218 let context = M::editing_syntax_context(self, cursor);
2219 let generated =
2220 self.mode
2221 .auto_closed_pairs()
2222 .contains_closer(cursor, index, close.len());
2223 if !generated
2224 && not_in.contains(&context)
2225 && !(context == crate::input::SyntaxContext::String && open == close)
2226 {
2227 continue;
2228 }
2229 if self.is_escaped_at(cursor - index) {
2230 continue;
2231 }
2232 return Some(cursor + new_text.len());
2233 }
2234 }
2235 None
2236 }
2237
2238 pub(super) fn backspace(&mut self, _: &Backspace, window: &mut Window, cx: &mut Context<Self>) {
2239 if self.selections.is_single() && self.active_selection().is_empty() && self.cursor() == 0 {
2245 cx.propagate();
2246 return;
2247 }
2248
2249 if self.mode.is_auto_close()
2252 && self.selections.is_single()
2253 && self.active_selection().is_empty()
2254 {
2255 let off = self.cursor();
2256 let deletion = self.mode.language_config().and_then(|rules| {
2257 rules.closing_pairs().find_map(|(open, close, not_in)| {
2258 if !self.text_before_matches(off, open) || !self.text_after_matches(off, close)
2259 {
2260 return None;
2261 }
2262 let start = off - open.len();
2263 let generated = self
2264 .mode
2265 .auto_closed_pairs()
2266 .contains(start..off, off..off + close.len());
2267 if self.is_escaped_at(start)
2268 || (!generated && not_in.contains(&M::editing_syntax_context(self, start)))
2269 {
2270 return None;
2271 }
2272 Some(start..off + close.len())
2273 })
2274 });
2275 if let Some(range) = deletion {
2276 let utf16 = self.range_to_utf16(&range);
2277 self.replace_text_in_range_silent(Some(utf16), "", window, cx);
2278 return;
2279 }
2280 }
2281
2282 self.delete_selections(
2283 false,
2284 EditIntent::Backspace,
2285 |s, offset| s.previous_boundary(offset)..offset,
2286 window,
2287 cx,
2288 );
2289 }
2290
2291 pub(super) fn delete(&mut self, _: &Delete, window: &mut Window, cx: &mut Context<Self>) {
2292 self.delete_selections(
2293 false,
2294 EditIntent::DeleteForward,
2295 |s, offset| offset..s.next_boundary(offset),
2296 window,
2297 cx,
2298 );
2299 }
2300
2301 pub(super) fn delete_to_beginning_of_line(
2302 &mut self,
2303 _: &DeleteToBeginningOfLine,
2304 window: &mut Window,
2305 cx: &mut Context<Self>,
2306 ) {
2307 self.delete_selections(
2308 true,
2309 EditIntent::Atomic,
2310 |s, offset| {
2311 let mut start = s.start_of_line_at(offset, s.line_end_affinity_at(offset));
2312 if start == offset {
2313 start = start.saturating_sub(1);
2314 }
2315 start..offset
2316 },
2317 window,
2318 cx,
2319 );
2320 }
2321
2322 pub(super) fn delete_to_end_of_line(
2323 &mut self,
2324 _: &DeleteToEndOfLine,
2325 window: &mut Window,
2326 cx: &mut Context<Self>,
2327 ) {
2328 self.delete_selections(
2329 true,
2330 EditIntent::Atomic,
2331 |s, offset| {
2332 let mut end = s.end_of_line_at(offset, s.line_end_affinity_at(offset));
2333 if end == offset {
2334 end = (end + 1).clamp(0, s.text.len());
2335 }
2336 offset..end
2337 },
2338 window,
2339 cx,
2340 );
2341 }
2342
2343 pub(super) fn delete_previous_word(
2344 &mut self,
2345 _: &DeleteToPreviousWordStart,
2346 window: &mut Window,
2347 cx: &mut Context<Self>,
2348 ) {
2349 self.delete_selections(
2350 true,
2351 EditIntent::Atomic,
2352 |s, offset| s.previous_start_of_word_at(offset)..offset,
2353 window,
2354 cx,
2355 );
2356 }
2357
2358 pub(super) fn delete_next_word(
2359 &mut self,
2360 _: &DeleteToNextWordEnd,
2361 window: &mut Window,
2362 cx: &mut Context<Self>,
2363 ) {
2364 self.delete_selections(
2365 true,
2366 EditIntent::Atomic,
2367 |s, offset| offset..s.next_end_of_word_at(offset),
2368 window,
2369 cx,
2370 );
2371 }
2372
2373 pub(super) fn enter(&mut self, action: &Enter, window: &mut Window, cx: &mut Context<Self>) {
2374 if M::handle_context_menu_action(self, Box::new(action.clone()), window, cx) {
2375 return;
2376 }
2377
2378 if M::has_inline_completion(self) {
2380 M::clear_inline_completion(self, cx);
2381 }
2382
2383 let insert_newline = self.is_multi_line() && (!self.submit_on_enter || action.shift);
2388
2389 if insert_newline {
2390 if !self.selections.is_single() {
2391 self.selections.merge_overlapping();
2393 let selections: Vec<CursorSelection> = self.selections.iter().copied().collect();
2394 let mut edits: Vec<(Range<usize>, String)> = Vec::with_capacity(selections.len());
2395 for sel in &selections {
2396 let indent = if self.is_code_editor() {
2397 self.indent_of_next_line_at(sel.cursor_offset())
2398 } else {
2399 String::new()
2400 };
2401 edits.push((sel.start..sel.end, format!("\n{}", indent)));
2402 }
2403 edits.sort_by_key(|(range, _)| range.start);
2404 self.replace_text_in_ranges(&edits, window, cx);
2405 self.pause_blink_cursor(cx);
2406 } else {
2407 let mut split = false;
2413 if self.is_code_editor() && self.active_selection().is_empty() {
2414 if let Some(rules) = self.mode.language_config() {
2415 if self.mode.is_smart_indent()
2416 && M::editing_syntax_context(self, self.cursor())
2417 == crate::input::SyntaxContext::Code
2418 {
2419 let off = self.cursor();
2420 let is_pair = rules.brackets.iter().any(|p| {
2421 !p.open.is_empty()
2422 && !p.close.is_empty()
2423 && p.open != p.close
2424 && self.text_before_matches(off, p.open.as_ref())
2425 && self.text_after_matches(off, p.close.as_ref())
2426 });
2427 if is_pair {
2428 let line_end_affinity = self.line_end_affinity_at(off);
2430 let line_start = self.start_of_line_at(off, line_end_affinity);
2431 let mut indent = String::new();
2432 for c in self.text.slice(line_start..).chars() {
2433 if !c.is_whitespace() || c == '\n' || c == '\r' {
2434 break;
2435 }
2436 indent.push(c);
2437 }
2438 let mut inner = indent.clone();
2440 if self.mode.is_smart_indent() {
2441 let tab = self.mode.tab_size();
2442 if tab.hard_tabs {
2443 inner.push('\t');
2444 } else {
2445 for _ in 0..tab.tab_size {
2446 inner.push(' ');
2447 }
2448 }
2449 }
2450 let new_line_text = format!("\n{inner}\n{indent}");
2451 self.replace_text_in_range_silent(None, &new_line_text, window, cx);
2452 self.set_cursor_to(off + 1 + inner.len());
2453 self.update_preferred_column();
2454 let cursors = self.selections.iter().copied().collect::<Vec<_>>();
2455 self.undo_manager
2456 .record_selections(cursors.clone(), cursors);
2457 self.pause_blink_cursor(cx);
2458 split = true;
2459 }
2460 }
2461 }
2462 }
2463 if !split {
2464 let indent = if self.is_code_editor() {
2466 self.indent_of_next_line()
2467 } else {
2468 "".to_string()
2469 };
2470
2471 let new_line_text = format!("\n{}", indent);
2473 self.replace_text_in_range_silent(None, &new_line_text, window, cx);
2474 self.pause_blink_cursor(cx);
2475 }
2476 }
2477 } else {
2478 self.undo_manager.break_transaction_coalescing();
2481 cx.propagate();
2482 }
2483
2484 cx.emit(InputEvent::PressEnter {
2485 secondary: action.secondary,
2486 shift: action.shift,
2487 });
2488 }
2489
2490 pub fn clean(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2491 self.replace_text("", window, cx);
2492 self.set_selection(0, 0);
2493 self.scroll_to(0, None, cx);
2494 }
2495
2496 pub(super) fn escape(&mut self, action: &Escape, window: &mut Window, cx: &mut Context<Self>) {
2497 if M::handle_context_menu_action(self, Box::new(action.clone()), window, cx) {
2498 return;
2499 }
2500
2501 M::hide_context_menu(self, cx);
2503
2504 if !self.selections.is_single() {
2506 self.undo_manager.break_transaction_coalescing();
2507 self.selections.remove_all_but_active();
2508 cx.notify();
2509 return;
2510 }
2511
2512 if M::has_inline_completion(self) {
2514 M::clear_inline_completion(self, cx);
2515 return; }
2517 M::clear_inline_completion(self, cx);
2518
2519 if self.touch_selection().is_some() {
2521 self.dismiss_touch_selection(cx);
2522 return;
2523 }
2524
2525 if self.ime_marked_range.is_some() {
2526 self.unmark_text(window, cx);
2527 }
2528
2529 if self.clean_on_escape && !cfg!(target_os = "android") {
2532 return self.clean(window, cx);
2533 }
2534
2535 cx.propagate();
2536 }
2537
2538 pub(crate) fn handle_right_click_menu(
2540 &mut self,
2541 position: Point<Pixels>,
2542 offset: usize,
2543 window: &mut Window,
2544 cx: &mut Context<Self>,
2545 ) {
2546 if self.disabled {
2547 return;
2548 }
2549 if crate::GlobalState::is_in_deferred_context(cx) {
2550 return;
2551 }
2552
2553 if !self.active_selection().contains(offset) {
2554 self.move_to(offset, None, cx);
2555 }
2556
2557 if self.is_code_editor() {
2558 M::on_hover_definition(self, offset, window, cx);
2559 }
2560
2561 if let Some(handler) = self.context_menu_handler.clone() {
2562 let capabilities = self.context_menu_capabilities();
2563 let input = cx.weak_entity();
2564 window.defer(cx, move |window, cx| {
2567 if input.upgrade().is_none() {
2568 return;
2569 }
2570 let previous_focus = window.focused(cx);
2571 handler(NativeMenu::new(), capabilities, position, window, cx);
2572 input
2575 .update(cx, |this, cx| {
2576 if window.focused(cx) == previous_focus
2577 && !this.focus_handle.is_focused(window)
2578 && this.has_selection_focus(window, cx)
2579 {
2580 return;
2582 }
2583 let menu_focus = window.focused(cx).filter(|focus| {
2584 Some(focus) != previous_focus.as_ref() && *focus != this.focus_handle
2585 });
2586 this.set_selection_focus(menu_focus, cx);
2587 })
2588 .ok();
2589 });
2590 }
2591 }
2592
2593 pub(super) fn add_cursor_above(
2594 &mut self,
2595 _: &AddCursorAbove,
2596 _: &mut Window,
2597 cx: &mut Context<Self>,
2598 ) {
2599 self.add_cursor_vertical(-1, cx);
2600 }
2601
2602 pub(super) fn add_cursor_below(
2603 &mut self,
2604 _: &AddCursorBelow,
2605 _: &mut Window,
2606 cx: &mut Context<Self>,
2607 ) {
2608 self.add_cursor_vertical(1, cx);
2609 }
2610
2611 fn add_cursor_vertical(&mut self, move_lines: isize, cx: &mut Context<Self>) {
2616 if !self.is_multi_line() {
2617 return;
2618 }
2619
2620 self.pause_blink_cursor(cx);
2621 self.undo_manager.break_transaction_coalescing();
2623
2624 let sources: Vec<(usize, Option<(Pixels, usize)>, bool)> = self
2625 .selections
2626 .iter()
2627 .map(|sel| {
2628 (
2629 sel.cursor_offset(),
2630 sel.column_anchor,
2631 self.line_end_affinity_for(sel),
2632 )
2633 })
2634 .collect();
2635 let mut offsets: std::collections::HashSet<usize> =
2636 sources.iter().map(|(offset, _, _)| *offset).collect();
2637
2638 let mut newest: Option<usize> = None;
2639 for (offset, anchor, line_end_affinity) in sources {
2640 let anchor = anchor.or_else(|| self.preferred_column_for(offset));
2641 let (target, _) = self.vertical_target(offset, anchor, line_end_affinity, move_lines);
2642 if target == offset || offsets.contains(&target) {
2643 continue;
2644 }
2645 offsets.insert(target);
2646 let id = self.selections.generate_id();
2647 let mut cursor = CursorSelection::new(id, target, target);
2648 cursor.column_anchor = anchor;
2649 self.selections.add(cursor);
2650 newest = Some(target);
2651 }
2652
2653 if let Some(newest) = newest {
2654 self.scroll_to(newest, None, cx);
2655 }
2656 cx.notify();
2657 }
2658
2659 pub(super) fn add_cursor_at(&mut self, offset: usize, cx: &mut Context<Self>) {
2664 if !self.is_multi_line() {
2665 return;
2666 }
2667
2668 for sel in self.selections.iter() {
2669 if sel.contains(offset) {
2670 return;
2671 }
2672 if sel.is_collapsed() && sel.cursor_offset() == offset {
2673 return;
2674 }
2675 }
2676
2677 self.undo_manager.break_transaction_coalescing();
2678 let id = self.selections.generate_id();
2679 let mut selection = CursorSelection::new(id, offset, offset);
2680 selection.column_anchor = self.preferred_column_for(offset);
2681 self.selections.add(selection);
2682 cx.notify();
2683 }
2684
2685 pub(super) fn build_columnar_selection(
2693 &mut self,
2694 start: ColumnarPoint,
2695 end: ColumnarPoint,
2696 cx: &mut Context<Self>,
2697 ) {
2698 if !self.is_multi_line() {
2699 return;
2700 }
2701
2702 self.undo_manager.break_transaction_coalescing();
2703
2704 let (start_row, start_col) = self.columnar_row_column(start);
2705 let (end_row, end_col) = self.columnar_row_column(end);
2706 let (start_row, end_row) = (start_row.min(end_row), start_row.max(end_row));
2707 let (start_col, end_col) = (start_col.min(end_col), start_col.max(end_col));
2708
2709 let mut new_selections = Vec::with_capacity(end_row - start_row + 1);
2710 for row in start_row..=end_row {
2711 let sel_start = self
2712 .display_map
2713 .display_row_column_to_offset(row, start_col);
2714 let sel_end = self.display_map.display_row_column_to_offset(row, end_col);
2715 let id = self.selections.generate_id();
2716 let sel_start = self.text.clip_offset(sel_start, Bias::Left);
2717 let sel_end = self.text.clip_offset(sel_end, Bias::Left);
2718 new_selections.push(CursorSelection::new(id, sel_start, sel_end));
2719 }
2720
2721 self.selections.replace_all(new_selections);
2722 cx.notify();
2723 }
2724
2725 fn columnar_row_column(&self, point: ColumnarPoint) -> (usize, usize) {
2728 let display_point = self.display_map.offset_to_wrap_display_point(point.offset);
2729 let row = self
2730 .display_map
2731 .wrap_row_to_display_row(display_point.row)
2732 .unwrap_or_else(|| {
2733 self.display_map
2734 .nearest_visible_display_row(display_point.row)
2735 });
2736
2737 (row, display_point.column + point.columns_past_line_end)
2738 }
2739
2740 pub(super) fn on_mouse_down(
2741 &mut self,
2742 event: &MouseDownEvent,
2743 window: &mut Window,
2744 cx: &mut Context<Self>,
2745 ) {
2746 if window.default_prevented() {
2749 return;
2750 }
2751 if event.button == MouseButton::Left
2754 && crate::GlobalState::is_touch_press(cx)
2755 && !self.disabled
2756 && !self.readonly
2757 {
2758 window.request_virtual_keyboard();
2759 }
2760 self.undo_manager.break_transaction_coalescing();
2761 if !self.disabled {
2765 self.focus(window, cx);
2766 self.pause_blink_cursor(cx);
2767 }
2768 crate::global_state::GlobalState::suppress_text_selection(cx);
2771
2772 M::clear_inline_completion(self, cx);
2774 if event.button == MouseButton::Left
2778 && event.click_count == 1
2779 && self.reopen_edit_menu_at(event.position, cx)
2780 {
2781 return;
2782 }
2783 self.dismiss_touch_selection(cx);
2784
2785 if let Some(ime_marked_range) = &self.ime_marked_range {
2788 if ime_marked_range.len() == 0 {
2789 self.ime_marked_range = None;
2790 }
2791 }
2792
2793 self.selecting = true;
2794 let (offset, line_end_affinity, columns_past_line_end) =
2795 self.resolve_mouse_position(event.position);
2796
2797 if M::on_click(self, event, offset, window, cx) {
2798 return;
2799 }
2800
2801 if event.button == MouseButton::Left && event.click_count >= 3 {
2803 self.select_line(offset, window, cx);
2804 return;
2805 }
2806
2807 if event.button == MouseButton::Left && event.click_count == 2 {
2809 self.select_word(offset, window, cx);
2810 if crate::GlobalState::is_touch_press(cx) {
2813 self.keep_touch_selection(cx);
2814 }
2815 return;
2816 }
2817
2818 if event.button == MouseButton::Right {
2820 if self.enable_context_menu {
2821 if !self.active_selection().contains(offset) {
2822 self.move_to(offset, None, cx);
2823 }
2824 self.pending_context_menu = Some((event.position, offset));
2825 }
2826 return;
2827 }
2828
2829 if self.is_multi_line() && event.button == MouseButton::Left {
2831 if event.modifiers.alt
2832 && (event.modifiers.shift || (cfg!(target_os = "linux") && event.modifiers.control))
2833 {
2834 self.column_select_start = Some(ColumnarPoint::new(offset, columns_past_line_end));
2837 self.selecting = true;
2838 self.move_to_with_affinity(offset, None, line_end_affinity, cx);
2839 return;
2840 } else if event.modifiers.alt {
2841 self.add_cursor_at(offset, cx);
2842 self.column_select_start = Some(ColumnarPoint::new(offset, columns_past_line_end));
2845 return;
2846 }
2847 }
2848
2849 if event.modifiers.shift {
2850 self.select_to_with_affinity(offset, line_end_affinity, cx);
2851 } else {
2852 self.move_to_with_affinity(offset, None, line_end_affinity, cx)
2853 }
2854 }
2855
2856 pub(super) fn on_mouse_up(
2857 &mut self,
2858 event: &MouseUpEvent,
2859 window: &mut Window,
2860 cx: &mut Context<Self>,
2861 ) {
2862 if event.button == MouseButton::Right {
2863 if let Some((position, offset)) = self.pending_context_menu.take() {
2864 self.handle_right_click_menu(position, offset, window, cx);
2865 }
2866 }
2867 if self.active_selection().is_empty() {
2868 self.active_selection_mut().reversed = false;
2869 }
2870 self.selecting = false;
2871 self.selected_word_range = None;
2872 self.column_select_start = None;
2873 self.auto_scroll.stop();
2874 }
2875
2876 pub(super) fn on_mouse_move(
2877 &mut self,
2878 event: &MouseMoveEvent,
2879 window: &mut Window,
2880 cx: &mut Context<Self>,
2881 ) {
2882 let within_bounds = self.input_bounds.contains(&event.position);
2884
2885 if !within_bounds {
2886 M::clear_hover_state(self, cx);
2888 return;
2889 }
2890
2891 let (offset, _) = self.index_for_mouse_position(event.position);
2893 M::on_mouse_move(self, offset, event, window, cx);
2894
2895 if self.is_code_editor() {
2896 match self
2900 .mode
2901 .diagnostics()
2902 .and_then(|set| set.for_offset(offset))
2903 {
2904 Some(diagnostic) => {
2905 if self.diagnostic_popover.as_deref() != Some(diagnostic) {
2906 self.diagnostic_popover = Some(Rc::new(diagnostic.clone()));
2907 cx.notify();
2908 }
2909 }
2910 None => {
2911 if self.diagnostic_popover.take().is_some() {
2912 cx.notify();
2913 }
2914 }
2915 }
2916 }
2917 }
2918
2919 pub(super) fn on_scroll_wheel(
2920 &mut self,
2921 event: &ScrollEvent,
2922 window: &mut Window,
2923 cx: &mut Context<Self>,
2924 ) {
2925 let line_height = self
2926 .last_layout
2927 .as_ref()
2928 .map(|layout| layout.line_height)
2929 .unwrap_or(window.line_height());
2930 let delta = event.delta.pixel_delta(line_height);
2931
2932 let old_offset = self.scroll_handle.offset();
2933 let precise = matches!(event.delta, gpui::ScrollDelta::Pixels(_));
2934 if !precise || !self.scroll_bounce || cx.reduce_motion() || !self.overflows_vertically() {
2935 let displaced = self.overscroll.offset() != 0.;
2936 self.overscroll.reset();
2937 if displaced {
2938 cx.notify();
2939 }
2940 }
2941 let bounces = self.scroll_bounce
2942 && precise
2943 && !cx.reduce_motion()
2944 && delta.y.abs() >= delta.x.abs()
2945 && self.overflows_vertically();
2946 if bounces {
2947 let mut overscroll = std::mem::take(&mut self.overscroll);
2948 let stretched = overscroll.packet(
2949 delta.y.as_f32(),
2950 event.touch_phase,
2951 self.input_bounds.size.height.as_f32(),
2952 cx.background_executor().now(),
2953 |dy| {
2954 let before = self.scroll_handle.offset();
2955 self.update_scroll_offset(
2956 Some(point(before.x + delta.x, before.y + px(dy))),
2957 cx,
2958 );
2959 (self.scroll_handle.offset().y - before.y).as_f32()
2960 },
2961 );
2962 self.overscroll = overscroll;
2963 if stretched {
2964 cx.notify();
2965 }
2966 } else {
2967 self.overscroll
2968 .touch_phase(event.touch_phase, cx.background_executor().now());
2969 if matches!(
2971 event.touch_phase,
2972 gpui::TouchPhase::Ended | gpui::TouchPhase::Cancelled
2973 ) && self.overscroll.offset() != 0.
2974 {
2975 self.overscroll.packet(
2976 0.,
2977 event.touch_phase,
2978 self.input_bounds.size.height.as_f32(),
2979 cx.background_executor().now(),
2980 |_| 0.,
2981 );
2982 cx.notify();
2983 }
2984 self.update_scroll_offset(Some(old_offset + delta), cx);
2985 }
2986
2987 if self.scroll_handle.offset() != old_offset || self.overflows_along(delta) {
2991 cx.stop_propagation();
2992 }
2993
2994 if self.diagnostic_popover.take().is_some() {
2995 cx.notify();
2996 }
2997 self.edit_menu_on_scroll(event.touch_phase, cx);
3000 }
3001
3002 pub(super) fn takes_scroll(&self, delta: Point<Pixels>) -> bool {
3008 if delta.y.abs() >= delta.x.abs() {
3009 self.overflows_vertically()
3010 } else {
3011 self.scroll_size.width > self.input_bounds.size.width
3012 }
3013 }
3014
3015 fn overflows_along(&self, delta: Point<Pixels>) -> bool {
3019 if delta.y.abs() >= delta.x.abs() {
3020 delta.y != px(0.) && self.overflows_vertically()
3021 } else {
3022 self.scroll_size.width > self.input_bounds.size.width
3023 }
3024 }
3025
3026 fn overflows_vertically(&self) -> bool {
3028 self.is_multi_line() && self.scroll_size.height > self.input_bounds.size.height
3029 }
3030
3031 pub(super) fn update_scroll_offset(
3032 &mut self,
3033 offset: Option<Point<Pixels>>,
3034 cx: &mut Context<Self>,
3035 ) {
3036 let mut offset = offset.unwrap_or(self.scroll_handle.offset());
3037 let safe_y_range =
3038 (-self.scroll_size.height + self.input_bounds.size.height).min(px(0.0))..px(0.);
3039
3040 offset.y = if self.is_single_line() {
3041 px(0.)
3042 } else {
3043 offset.y.clamp(safe_y_range.start, safe_y_range.end)
3044 };
3045 offset.x = clamp_horizontal_scroll_offset(
3046 offset.x,
3047 self.scroll_size.width,
3048 self.input_bounds.size.width,
3049 self.text_align,
3050 );
3051 if self.scroll_handle.offset() != offset {
3052 self.scroll_handle.set_offset(offset);
3053 cx.notify();
3054 }
3055 }
3056
3057 pub(crate) fn scroll_to(
3061 &mut self,
3062 offset: usize,
3063 direction: Option<MoveDirection>,
3064 cx: &mut Context<Self>,
3065 ) {
3066 let padding = if direction.is_some() {
3067 ScrollPadding::SurroundingLines
3068 } else {
3069 ScrollPadding::Minimal
3070 };
3071 self.scroll_to_with_padding(offset, direction, padding, cx);
3072 }
3073
3074 pub(crate) fn scroll_to_with_padding(
3077 &mut self,
3078 offset: usize,
3079 direction: Option<MoveDirection>,
3080 padding: ScrollPadding,
3081 cx: &mut Context<Self>,
3082 ) {
3083 let Some(last_layout) = self.last_layout.as_ref() else {
3084 return;
3085 };
3086 let Some(bounds) = self.last_bounds.as_ref() else {
3087 return;
3088 };
3089
3090 let mut scroll_offset = self.scroll_handle.offset();
3091 let was_offset = scroll_offset;
3092 let line_height = last_layout.line_height;
3093
3094 let point = self.text.offset_to_point(offset);
3095
3096 let row = point.row;
3097
3098 let display_row = self
3100 .display_map
3101 .offset_to_display_row(offset, self.line_end_affinity_at(offset));
3102 let row_offset_y = line_height * display_row;
3103
3104 let safety_margin = match last_layout.text_align {
3107 TextAlign::Left => RIGHT_MARGIN,
3108 TextAlign::Right => px(0.),
3109 TextAlign::Center => CURSOR_WIDTH,
3110 };
3111 if let Some(vi) = last_layout
3112 .visible_buffer_lines
3113 .iter()
3114 .position(|&line| line == row)
3115 {
3116 let line = &last_layout.lines[vi];
3117 let local_offset = offset.saturating_sub(last_layout.visible_line_byte_offsets[vi]);
3118 if let Some(pos) = line.position_for_index(local_offset, last_layout, false) {
3119 let bounds_width = bounds.size.width - last_layout.line_number_width;
3120 let col_offset_x = pos.x;
3121 if col_offset_x - safety_margin < -scroll_offset.x {
3122 scroll_offset.x = -col_offset_x + safety_margin;
3124 } else if col_offset_x + safety_margin > -scroll_offset.x + bounds_width {
3125 scroll_offset.x = -(col_offset_x - bounds_width + safety_margin);
3126 }
3127 }
3128 }
3129
3130 let edge_height =
3135 if matches!(padding, ScrollPadding::SurroundingLines) && self.is_code_editor() {
3136 super::element::cursor_surrounding_padding(
3137 self.mode.is_auto_grow(),
3138 self.cursor_surrounding_lines,
3139 super::element::viewport_visible_lines(bounds.size.height, line_height),
3140 line_height,
3141 )
3142 } else {
3143 px(0.)
3144 };
3145 scroll_offset.y = super::element::reveal_row(
3146 scroll_offset.y,
3147 row_offset_y,
3148 line_height,
3149 bounds.size.height,
3150 edge_height,
3151 );
3152
3153 if direction == Some(MoveDirection::Up) {
3155 scroll_offset.y = scroll_offset.y.max(was_offset.y);
3156 } else if direction == Some(MoveDirection::Down) {
3157 scroll_offset.y = scroll_offset.y.min(was_offset.y);
3158 }
3159
3160 let safe_y_min = (-self.scroll_size.height + self.input_bounds.size.height).min(px(0.));
3164 scroll_offset.x = scroll_offset.x.min(px(0.));
3165 scroll_offset.y = scroll_offset.y.clamp(safe_y_min, px(0.));
3166 self.deferred_scroll_offset = Some(scroll_offset);
3167 cx.notify();
3168 }
3169
3170 pub(super) fn show_character_palette(
3171 &mut self,
3172 _: &ShowCharacterPalette,
3173 window: &mut Window,
3174 _: &mut Context<Self>,
3175 ) {
3176 window.show_character_palette();
3177 }
3178
3179 fn selected_texts(&self) -> Vec<String> {
3181 let mut selections: Vec<CursorSelection> = self
3182 .selections
3183 .iter()
3184 .copied()
3185 .filter(|sel| !sel.is_empty())
3186 .collect();
3187 selections.sort_by_key(|sel| sel.start);
3188 selections
3189 .iter()
3190 .map(|sel| self.text.slice(*sel).to_string())
3191 .collect()
3192 }
3193
3194 pub(super) fn copy(&mut self, _: &Copy, _: &mut Window, cx: &mut Context<Self>) {
3195 if !self.is_copyable() {
3196 return;
3197 }
3198
3199 let texts = self.selected_texts();
3200
3201 cx.write_to_clipboard(ClipboardItem::new_string(texts.join("\n")));
3202 }
3203
3204 pub(super) fn cut(&mut self, _: &Cut, window: &mut Window, cx: &mut Context<Self>) {
3205 if !self.is_copyable() {
3206 return;
3207 }
3208
3209 let texts = self.selected_texts();
3210
3211 cx.write_to_clipboard(ClipboardItem::new_string(texts.join("\n")));
3212
3213 self.undo_manager.set_pending_intent(EditIntent::Atomic);
3214 self.replace_text_in_range_silent(None, "", window, cx);
3215 }
3216
3217 pub(super) fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
3218 if !self.is_editable() {
3219 return;
3220 }
3221 if let Some(clipboard) = cx.read_from_clipboard() {
3222 self.insert_clipboard(clipboard, window, cx);
3223 return;
3224 }
3225 let read = cx.read_from_clipboard_async();
3230 let target = self.paste_target();
3231 cx.spawn_in(window, async move |this, cx| match read.await {
3232 Ok(Some(clipboard)) => {
3233 this.update_in(cx, |this, window, cx| {
3234 if this.is_editable()
3241 && this.focus_handle.is_focused(window)
3242 && this.paste_target() == target
3243 {
3244 this.insert_clipboard(clipboard, window, cx);
3245 }
3246 })
3247 .ok();
3248 }
3249 Ok(None) => {}
3250 Err(error) => tracing::warn!("failed to read the clipboard for paste: {error}"),
3251 })
3252 .detach();
3253 }
3254
3255 fn paste_target(&self) -> PasteTarget {
3259 PasteTarget {
3260 document_revision: self.document_revision,
3261 selections: self.selections.iter().copied().collect(),
3262 }
3263 }
3264
3265 fn insert_clipboard(
3271 &mut self,
3272 clipboard: ClipboardItem,
3273 window: &mut Window,
3274 cx: &mut Context<Self>,
3275 ) {
3276 let Some(mut new_text) = clipboard.text().filter(|text| !text.is_empty()) else {
3277 return;
3278 };
3279 self.undo_manager.set_pending_intent(EditIntent::Atomic);
3281
3282 if !self.is_multi_line() {
3283 new_text = new_text.replace('\n', "");
3284 self.replace_text_in_range_silent(None, &new_text, window, cx);
3285 self.scroll_to(self.cursor(), None, cx);
3286 return;
3287 }
3288
3289 if !self.selections.is_single() {
3292 self.selections.merge_overlapping();
3293 }
3294 let lines: Vec<String> = new_text.split('\n').map(|s| s.to_string()).collect();
3295 let count = self.selections.len();
3296 if count > 1 && lines.len() == count {
3297 let mut selections: Vec<CursorSelection> = self.selections.iter().copied().collect();
3298 selections.sort_by_key(|sel| sel.start);
3299 let edits: Vec<(Range<usize>, String)> = selections
3300 .iter()
3301 .zip(lines)
3302 .map(|(sel, line)| (sel.start..sel.end, line))
3303 .collect();
3304 self.replace_text_in_ranges(&edits, window, cx);
3305 } else {
3306 self.replace_text_in_range_silent(None, &new_text, window, cx);
3307 }
3308 self.scroll_to(self.cursor(), None, cx);
3309 }
3310
3311 fn typing_intent(&self, edits: &[(Range<usize>, String)], new_text: &str) -> EditIntent {
3314 if !new_text.is_empty()
3315 && !new_text.contains(['\n', '\r'])
3316 && edits.iter().all(|(range, _)| range.is_empty())
3317 {
3318 EditIntent::Typing
3319 } else {
3320 EditIntent::Atomic
3321 }
3322 }
3323
3324 fn collapse_for_intent(
3330 intent: EditIntent,
3331 mut selection: CursorSelection,
3332 range: &Range<usize>,
3333 ) -> CursorSelection {
3334 match intent {
3335 EditIntent::Backspace => selection.place_at(range.end, None),
3336 EditIntent::DeleteForward => selection.place_at(range.start, None),
3337 EditIntent::Typing | EditIntent::Atomic => {}
3338 }
3339 selection
3340 }
3341
3342 fn rewrites_typed_char(
3345 &self,
3346 selection: CursorSelection,
3347 range: &Range<usize>,
3348 old_text: &str,
3349 new_text: &str,
3350 ) -> bool {
3351 !self.silent_replace_text
3352 && selection.is_collapsed()
3353 && selection.cursor_offset() == range.end
3354 && old_text.chars().count() == 1
3355 && !old_text.contains(['\n', '\r'])
3356 && !new_text.contains(['\n', '\r'])
3357 }
3358
3359 fn push_history(
3360 &mut self,
3361 text: &Rope,
3362 range: &Range<usize>,
3363 new_text: &str,
3364 requested_intent: Option<EditIntent>,
3365 selection_before: CursorSelection,
3366 selection_after: Option<CursorSelection>,
3367 ) -> bool {
3368 self.document_revision = self.document_revision.wrapping_add(1);
3369 let token_delta = self.edit_tokens(range, new_text.len());
3370 if self.undo_manager.is_ignoring() {
3371 return false;
3372 }
3373
3374 let range =
3375 text.clip_offset(range.start, Bias::Left)..text.clip_offset(range.end, Bias::Right);
3376 let old_text = text.slice(range.clone()).to_string();
3377 let new_range = range.start..range.start + new_text.len();
3378
3379 let intent = requested_intent.unwrap_or_else(|| {
3380 if range.is_empty()
3381 && old_text.is_empty()
3382 && !new_text.is_empty()
3383 && !new_text.contains(['\n', '\r'])
3384 || self.rewrites_typed_char(selection_before, &range, &old_text, new_text)
3385 {
3386 EditIntent::Typing
3387 } else {
3388 EditIntent::Atomic
3389 }
3390 });
3391
3392 let selection_before = Self::collapse_for_intent(intent, selection_before, &range);
3393 let selection_after =
3394 selection_after.unwrap_or_else(|| (new_range.end..new_range.end).into());
3395
3396 let open_transaction = self.undo_manager.has_open_transaction();
3397 let mut change = Change::new(range, &old_text, new_range, new_text);
3398 change.token_delta = token_delta;
3399 let recorded = self.undo_manager.record_transaction(change, intent);
3400 if recorded && !open_transaction {
3403 self.undo_manager
3404 .record_selections(vec![selection_before], vec![selection_after]);
3405 }
3406 recorded
3407 }
3408
3409 pub(super) fn undo(&mut self, _: &Undo, window: &mut Window, cx: &mut Context<Self>) {
3410 self.replay_history(true, window, cx);
3411 }
3412
3413 pub(super) fn redo(&mut self, _: &Redo, window: &mut Window, cx: &mut Context<Self>) {
3414 self.replay_history(false, window, cx);
3415 }
3416
3417 fn replay_history(&mut self, undo: bool, window: &mut Window, cx: &mut Context<Self>) {
3418 if !self.is_editable() {
3419 return;
3420 }
3421 let replay = if undo {
3422 self.undo_manager.undo()
3423 } else {
3424 self.undo_manager.redo()
3425 };
3426 let Some(replay) = replay else {
3427 return;
3428 };
3429 let token_aware = self.inline_tokens.is_some()
3430 || replay
3431 .changes
3432 .iter()
3433 .any(|change| change.token_delta.is_some());
3434 let emit_events = self.emit_events;
3435 if token_aware {
3436 self.emit_events = false;
3437 self.last_input_edits.clear();
3438 }
3439 self.replaying_history = token_aware;
3440 self.undo_manager.set_ignoring(true);
3441 for change in &replay.changes {
3442 let (range, text) = if undo {
3443 (change.new_range.into(), &change.old_text)
3444 } else {
3445 (change.old_range.into(), &change.new_text)
3446 };
3447 let range_utf16 = self.range_to_utf16(&range);
3448 self.replace_text_in_range_silent(Some(range_utf16), text, window, cx);
3449 self.replay_tokens(&range, text.len(), change.token_delta.as_deref(), undo);
3450 }
3451 self.restore_selections(replay.selections);
3452 self.mode
3453 .restore_auto_closed_pairs(replay.auto_closed_pairs.unwrap_or_default());
3454 if token_aware {
3455 self.ime_marked_range = None;
3458 }
3459 self.undo_manager.set_ignoring(false);
3460 self.replaying_history = false;
3461 self.emit_events = emit_events;
3462 if token_aware {
3463 if emit_events {
3464 self.emit_change(cx);
3465 } else {
3466 self.last_input_edits.clear();
3467 }
3468 }
3469 cx.notify();
3470 }
3471
3472 fn restore_selections(&mut self, selections: Option<Vec<CursorSelection>>) {
3476 let Some(selections) = selections else {
3477 return;
3478 };
3479
3480 let len = self.text.len();
3481 let restored: Vec<CursorSelection> = selections
3482 .into_iter()
3483 .map(|mut sel| {
3484 sel.start = sel.start.min(len);
3485 sel.end = sel.end.min(len);
3486 sel
3487 })
3488 .collect();
3489 self.selections.replace_all(restored);
3490 self.selections.merge_overlapping();
3491 }
3492
3493 pub fn cursor(&self) -> usize {
3497 if let Some(ime_marked_range) = &self.ime_marked_range {
3498 return ime_marked_range.end;
3499 }
3500
3501 self.selections.active().cursor_offset()
3502 }
3503
3504 pub(super) fn active_selection(&self) -> &CursorSelection {
3506 self.selections.active()
3507 }
3508
3509 pub(super) fn active_selection_mut(&mut self) -> &mut CursorSelection {
3511 self.selections.active_mut()
3512 }
3513
3514 pub(super) fn set_selection(&mut self, start: usize, end: usize) {
3517 let range = self.normalize_token_range(start..end);
3518 let (start, end) = (range.start, range.end);
3519 let active = self.active_selection_mut();
3520 active.start = start;
3521 active.end = end;
3522 }
3523
3524 pub(super) fn set_cursor_to(&mut self, offset: usize) {
3527 let offset = self.token_boundary(offset, Bias::Right);
3528 let active = self.active_selection_mut();
3529 active.start = offset;
3530 active.end = offset;
3531 active.reversed = false;
3532 }
3533
3534 pub fn visible_row_range(&self) -> Option<std::ops::Range<usize>> {
3536 self.last_layout.as_ref().map(|l| l.visible_range.clone())
3537 }
3538
3539 pub fn scroll_offset(&self) -> gpui::Point<gpui::Pixels> {
3541 self.scroll_handle.offset()
3542 }
3543
3544 pub fn set_scroll_offset(&mut self, offset: gpui::Point<gpui::Pixels>, cx: &mut Context<Self>) {
3548 self.deferred_scroll_offset = Some(offset);
3549 cx.notify();
3550 }
3551
3552 pub fn line_height(&self) -> Option<gpui::Pixels> {
3554 self.last_layout.as_ref().map(|l| l.line_height)
3555 }
3556
3557 pub fn selected_range(&self) -> std::ops::Range<usize> {
3565 (*self.selections.active()).into()
3566 }
3567
3568 pub fn is_selection_reversed(&self) -> bool {
3573 let selection = self.selections.active();
3574 !selection.is_empty() && selection.reversed
3575 }
3576
3577 pub fn select_all(&mut self, _: &mut Window, cx: &mut Context<Self>) {
3578 self.undo_manager.break_transaction_coalescing();
3579 self.selections.remove_all_but_active();
3580 self.set_selection(0, self.text.len());
3581 cx.notify();
3582 }
3583
3584 pub fn set_selected_range(&mut self, range: Range<usize>, cx: &mut Context<Self>) {
3589 self.set_selected_range_with_direction(range, false, cx);
3590 }
3591
3592 pub fn set_selected_range_with_direction(
3599 &mut self,
3600 range: Range<usize>,
3601 reversed: bool,
3602 cx: &mut Context<Self>,
3603 ) {
3604 let range = self.normalize_token_range(range);
3605 let end_bias = if range.start == range.end {
3606 Bias::Left
3607 } else {
3608 Bias::Right
3609 };
3610 let start = self.text.clip_offset(range.start, Bias::Left);
3611 let end = self.text.clip_offset(range.end, end_bias);
3612
3613 let (anchor, head) = if reversed { (end, start) } else { (start, end) };
3614 self.move_to(anchor, None, cx);
3615 self.active_selection_mut().reversed = false;
3616 self.selected_word_range = None;
3617 self.select_to(head, cx);
3618 }
3619
3620 pub fn add_selection(&mut self, range: Range<usize>, cx: &mut Context<Self>) {
3631 if !self.is_multi_line() {
3632 self.set_selected_range(range, cx);
3633 return;
3634 }
3635
3636 let range = self.normalize_token_range(range);
3637 let end_bias = if range.start == range.end {
3638 Bias::Left
3639 } else {
3640 Bias::Right
3641 };
3642 let start = self.cursor_boundary(range.start, Bias::Left);
3643 let end = self.cursor_boundary(self.text.clip_offset(range.end, end_bias), Bias::Left);
3646
3647 for sel in self.selections.iter() {
3648 if sel.start <= start && end <= sel.end {
3649 return;
3650 }
3651 }
3652
3653 self.undo_manager.break_transaction_coalescing();
3654 self.selected_word_range = None;
3655 self.pause_blink_cursor(cx);
3656 M::hide_context_menu(self, cx);
3657 M::clear_inline_completion(self, cx);
3658 let id = self.selections.generate_id();
3659 let mut selection = CursorSelection::new(id, start, end);
3660 selection.column_anchor = self.preferred_column_for(end);
3661 self.selections.add(selection);
3662 self.selections.merge_overlapping();
3663 self.scroll_to(end, None, cx);
3664 cx.notify();
3665 }
3666
3667 pub(crate) fn index_for_mouse_position(&self, position: Point<Pixels>) -> (usize, bool) {
3675 let (offset, line_end_affinity, _) = self.resolve_mouse_position(position);
3676 (offset, line_end_affinity)
3677 }
3678
3679 pub(super) fn resolve_mouse_position(&self, position: Point<Pixels>) -> (usize, bool, usize) {
3686 if self.text.len() == 0 {
3688 return (0, false, 0);
3689 }
3690
3691 let (Some(bounds), Some(last_layout)) =
3692 (self.last_bounds.as_ref(), self.last_layout.as_ref())
3693 else {
3694 return (0, false, 0);
3695 };
3696
3697 let line_height = last_layout.line_height;
3698 let line_number_width = last_layout.line_number_width;
3699
3700 let inner_position = position - bounds.origin - point(line_number_width, px(0.));
3711
3712 let mut y_offset = last_layout.visible_top;
3713 let mut last_line_pos = None;
3716
3717 for (vi, (line_layout, _buffer_line)) in last_layout
3719 .lines
3720 .iter()
3721 .zip(last_layout.visible_buffer_lines.iter())
3722 .enumerate()
3723 {
3724 let line_start_offset = last_layout.visible_line_byte_offsets[vi];
3725
3726 let line_origin = point(px(0.), y_offset);
3728 let pos = inner_position - line_origin;
3729
3730 if self.is_single_line() {
3732 let local_index = line_layout.closest_index_for_x(pos.x, last_layout);
3733 return (
3735 self.resolve_index(line_start_offset + local_index),
3736 false,
3737 0,
3738 );
3739 }
3740
3741 if let Some((local_index, line_end_affinity)) =
3743 line_layout.closest_index_for_position(pos, last_layout)
3744 {
3745 return (
3746 self.resolve_index(line_start_offset + local_index),
3747 line_end_affinity,
3748 line_layout.columns_past_line_end(pos, last_layout),
3749 );
3750 } else if pos.y < px(0.) {
3751 return (self.resolve_index(line_start_offset), false, 0);
3753 }
3754
3755 y_offset += line_layout.size(line_height).height;
3756 last_line_pos = Some(pos);
3757 }
3758
3759 let last_position = last_layout
3762 .lines
3763 .last()
3764 .zip(last_line_pos)
3765 .zip(last_layout.visible_line_byte_offsets.last())
3766 .map(|((line_layout, pos), line_start)| {
3767 let last_row_top = (line_layout.size(line_height).height - line_height).max(px(0.));
3768 let pos = point(pos.x, last_row_top);
3769 (
3770 self.resolve_index(line_start + line_layout.len()),
3771 false,
3772 line_layout.columns_past_line_end(pos, last_layout),
3773 )
3774 });
3775
3776 last_position.unwrap_or((self.text.len(), false, 0))
3777 }
3778
3779 fn resolve_index(&self, index: usize) -> usize {
3782 if self.masked {
3783 self.text.char_index_to_offset(index / MASK_CHAR.len_utf8())
3784 } else {
3785 index.min(self.text.len())
3786 }
3787 }
3788
3789 fn extend_selection(
3799 sel: &mut CursorSelection,
3800 offset: usize,
3801 word_range: Option<CursorSelection>,
3802 ) {
3803 if sel.reversed {
3804 sel.start = offset;
3805 } else {
3806 sel.end = offset;
3807 }
3808
3809 if sel.end < sel.start {
3810 sel.reversed = !sel.reversed;
3811 std::mem::swap(&mut sel.start, &mut sel.end);
3812 }
3813
3814 if let Some(word_range) = word_range {
3815 if sel.start > word_range.start {
3816 sel.start = word_range.start;
3817 }
3818 if sel.end < word_range.end {
3819 sel.end = word_range.end;
3820 }
3821 }
3822 }
3823
3824 pub(crate) fn select_to(&mut self, offset: usize, cx: &mut Context<Self>) {
3826 self.select_to_with_affinity(offset, false, cx);
3827 }
3828
3829 pub(crate) fn select_to_with_affinity(
3836 &mut self,
3837 offset: usize,
3838 line_end_affinity: bool,
3839 cx: &mut Context<Self>,
3840 ) {
3841 M::clear_inline_completion(self, cx);
3842
3843 self.cursor_line_end_affinity = line_end_affinity;
3844 let offset = self.cursor_boundary(offset, Bias::Left);
3845 let word_range = self.selected_word_range;
3846 Self::extend_selection(self.active_selection_mut(), offset, word_range);
3847 let range = self.normalize_token_range(self.selected_range());
3848 self.set_selection(range.start, range.end);
3849
3850 if self.active_selection().is_empty() {
3851 self.update_preferred_column();
3852 }
3853 cx.notify()
3854 }
3855
3856 fn select_all_cursors_to(
3859 &mut self,
3860 f: impl Fn(&Self, &CursorSelection) -> usize,
3861 cx: &mut Context<Self>,
3862 ) {
3863 self.select_all_cursors_to_with_affinity(|s, sel| (f(s, sel), false), false, cx);
3864 }
3865
3866 fn select_all_cursors_to_with_affinity(
3869 &mut self,
3870 f: impl Fn(&Self, &CursorSelection) -> (usize, bool),
3871 preserve_column: bool,
3872 cx: &mut Context<Self>,
3873 ) {
3874 self.pause_blink_cursor(cx);
3875 self.undo_manager.break_transaction_coalescing();
3876 M::clear_inline_completion(self, cx);
3877
3878 let mut active_affinity = false;
3879 let new_selections: Vec<CursorSelection> = self
3880 .selections
3881 .iter()
3882 .map(|sel| {
3883 let (offset, affinity) = f(self, sel);
3884 if sel.id == self.active_selection().id {
3885 active_affinity = affinity;
3886 }
3887 let offset = self.cursor_boundary(offset, Bias::Left);
3888 let mut new_sel = *sel;
3889 Self::extend_selection(&mut new_sel, offset, None);
3890 let range = self.normalize_token_range(new_sel.start..new_sel.end);
3891 new_sel.start = range.start;
3892 new_sel.end = range.end;
3893 if !preserve_column {
3894 new_sel.column_anchor =
3895 self.preferred_column_for_with_affinity(new_sel.cursor_offset(), affinity);
3896 } else if new_sel.column_anchor.is_none() {
3897 new_sel.column_anchor = self.preferred_column_for_with_affinity(
3898 sel.cursor_offset(),
3899 self.line_end_affinity_for(sel),
3900 );
3901 }
3902 new_sel
3903 })
3904 .collect();
3905 self.cursor_line_end_affinity = active_affinity;
3906 self.selections.replace_all(new_selections);
3907 self.selections.merge_overlapping();
3908
3909 self.scroll_to(self.cursor(), None, cx);
3910 cx.notify()
3911 }
3912
3913 pub fn unselect(&mut self, _: &mut Window, cx: &mut Context<Self>) {
3915 self.undo_manager.break_transaction_coalescing();
3916 let offset = self.cursor();
3917 self.set_cursor_to(offset);
3918 cx.notify()
3919 }
3920
3921 #[inline]
3922 pub(super) fn offset_from_utf16(&self, offset: usize) -> usize {
3923 self.text.offset_utf16_to_offset(offset)
3924 }
3925
3926 #[inline]
3927 pub(super) fn offset_to_utf16(&self, offset: usize) -> usize {
3928 self.text.offset_to_offset_utf16(offset)
3929 }
3930
3931 #[inline]
3932 pub(crate) fn range_to_utf16(&self, range: &Range<usize>) -> Range<usize> {
3933 self.offset_to_utf16(range.start)..self.offset_to_utf16(range.end)
3934 }
3935
3936 #[inline]
3937 pub(super) fn range_from_utf16(&self, range_utf16: &Range<usize>) -> Range<usize> {
3938 self.offset_from_utf16(range_utf16.start)..self.offset_from_utf16(range_utf16.end)
3939 }
3940
3941 fn clamp_offset_to_visible_backward(&self, offset: usize) -> usize {
3944 let line = self.text.offset_to_point(offset).row;
3945 if self.display_map.is_buffer_line_hidden(line) {
3946 for fold in self.display_map.folded_ranges() {
3947 if line > fold.start_line && line <= fold.end_line {
3948 return self.text.line_end_offset(fold.start_line);
3949 }
3950 }
3951 }
3952 offset
3953 }
3954
3955 fn clamp_offset_to_visible_forward(&self, offset: usize) -> usize {
3958 let line = self.text.offset_to_point(offset).row;
3959 if self.display_map.is_buffer_line_hidden(line) {
3960 for fold in self.display_map.folded_ranges() {
3961 if line > fold.start_line && line <= fold.end_line {
3962 return self.text.line_start_offset(fold.end_line);
3963 }
3964 }
3965 }
3966 offset
3967 }
3968
3969 pub(super) fn cursor_boundary(&self, offset: usize, bias: Bias) -> usize {
3972 let offset = self.token_boundary(self.text.clip_offset(offset, bias), bias);
3973 if offset > 0
3974 && self.text.char_at(offset - 1) == Some('\r')
3975 && self.text.char_at(offset) == Some('\n')
3976 {
3977 if bias == Bias::Left {
3978 offset - 1
3979 } else {
3980 offset + 1
3981 }
3982 } else {
3983 offset
3984 }
3985 }
3986
3987 pub(super) fn previous_boundary(&self, offset: usize) -> usize {
3988 let offset = self.cursor_boundary(offset.saturating_sub(1), Bias::Left);
3989 self.clamp_offset_to_visible_backward(offset)
3990 }
3991
3992 pub(super) fn next_boundary(&self, offset: usize) -> usize {
3993 let offset = self.cursor_boundary(offset.saturating_add(1), Bias::Right);
3994 self.clamp_offset_to_visible_forward(offset)
3995 }
3996
3997 pub(crate) fn show_cursor(&self, window: &Window, cx: &App) -> bool {
3999 (self.focus_handle.is_focused(window) || M::is_context_menu_open(self, cx))
4000 && !self.disabled
4001 && self.blink_cursor.read(cx).visible()
4002 && window.is_window_active()
4003 }
4004
4005 fn on_focus(&mut self, _: &mut Window, cx: &mut Context<Self>) {
4006 self.blink_cursor.update(cx, |cursor, cx| {
4007 cursor.start(cx);
4008 });
4009 cx.emit(InputEvent::Focus);
4010 }
4011
4012 fn on_blur(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4013 self.revealed_viewport = None;
4014 if M::is_context_menu_open(self, cx) {
4015 return;
4016 }
4017
4018 M::hide_context_menu(self, cx);
4019 self.undo_manager.break_transaction_coalescing();
4020
4021 M::clear_hover_state(self, cx);
4025 self.diagnostic_popover = None;
4026 M::clear_inline_completion(self, cx);
4027 self.dismiss_touch_selection(cx);
4028 self.blink_cursor.update(cx, |cursor, cx| {
4029 cursor.stop(cx);
4030 });
4031 self.clamp_number_value(window, cx);
4032 cx.emit(InputEvent::Blur);
4033 cx.notify();
4034 }
4035
4036 fn clamp_number_value(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4041 if !self.is_single_line() {
4042 return;
4043 }
4044 if !matches!(self.mask_pattern, MaskPattern::Number { .. }) {
4045 return;
4046 }
4047 if self.number_min.is_none() && self.number_max.is_none() {
4048 return;
4049 }
4050
4051 let Ok(value) = self.unmask_value().parse::<f64>() else {
4052 return;
4053 };
4054
4055 let clamped = match (self.number_min, self.number_max) {
4056 (Some(min), _) if value < min => min,
4057 (_, Some(max)) if value > max => max,
4058 _ => return,
4059 };
4060
4061 let new_text = clamped.to_string();
4064 if !self.is_valid_input(&new_text, cx) {
4065 return;
4066 }
4067
4068 let range = self.range_to_utf16(&(0..self.text.len()));
4069 self.replace_text_in_range_silent(Some(range), &new_text, window, cx);
4070 }
4071
4072 pub(super) fn pause_blink_cursor(&mut self, cx: &mut Context<Self>) {
4073 self.blink_cursor.update(cx, |cursor, cx| {
4074 cursor.pause(cx);
4075 });
4076 }
4077
4078 pub(super) fn on_drag_move(
4079 &mut self,
4080 event: &MouseMoveEvent,
4081 window: &mut Window,
4082 cx: &mut Context<Self>,
4083 ) {
4084 if self.text.len() == 0 {
4085 return;
4086 }
4087
4088 if self.last_layout.is_none() {
4089 return;
4090 }
4091
4092 if !self.focus_handle.is_focused(window) {
4093 return;
4094 }
4095
4096 if !self.selecting {
4097 return;
4098 }
4099
4100 self.auto_scroll.last_drag_position = Some(event.position);
4101 let (offset, line_end_affinity, columns_past_line_end) =
4102 self.resolve_mouse_position(event.position);
4103 if let Some(start) = self.column_select_start {
4104 let end = ColumnarPoint::new(offset, columns_past_line_end);
4105 self.build_columnar_selection(start, end, cx);
4106 } else {
4107 self.select_to_with_affinity(offset, line_end_affinity, cx);
4108 }
4109
4110 if !self.is_single_line() {
4111 let delta = AutoScroll::compute_delta(event.position.y, self.input_bounds);
4112 let scroll_delta = delta.map(|d| -d);
4114 self.auto_scroll.set(scroll_delta, cx, |delta, state, cx| {
4115 let current = state.scroll_handle.offset();
4116 state.update_scroll_offset(Some(point(current.x, current.y + delta)), cx);
4117 if let Some(pos) = state.auto_scroll.last_drag_position {
4118 let (offset, line_end_affinity, columns_past_line_end) =
4119 state.resolve_mouse_position(pos);
4120 if let Some(start) = state.column_select_start {
4121 let end = ColumnarPoint::new(offset, columns_past_line_end);
4122 state.build_columnar_selection(start, end, cx);
4123 } else {
4124 state.select_to_with_affinity(offset, line_end_affinity, cx);
4125 }
4126 }
4127 });
4128 }
4129 }
4130
4131 pub(super) fn normalize_input<'a>(&self, new_text: &'a str) -> Cow<'a, str> {
4137 let normalized = if matches!(self.mask_pattern, MaskPattern::Number { .. }) {
4138 normalize_number_input(new_text)
4139 } else {
4140 Cow::Borrowed(new_text)
4141 };
4142
4143 if self.is_single_line() && normalized.contains(['\n', '\r']) {
4144 Cow::Owned(normalized.replace(['\n', '\r'], ""))
4145 } else {
4146 normalized
4147 }
4148 }
4149
4150 pub(crate) fn is_valid_input(&self, new_text: &str, cx: &mut Context<Self>) -> bool {
4151 if new_text.is_empty() {
4152 return true;
4153 }
4154
4155 if let Some(validate) = &self.validate {
4156 if !validate(new_text, cx) {
4157 return false;
4158 }
4159 }
4160
4161 if !self.mask_pattern.is_valid(new_text) {
4162 return false;
4163 }
4164
4165 let Some(pattern) = &self.pattern else {
4166 return true;
4167 };
4168
4169 pattern.is_match(new_text)
4170 }
4171
4172 pub fn mask_pattern(mut self, pattern: impl Into<MaskPattern>) -> Self {
4182 self.mask_pattern = pattern.into();
4183 self.mask_pattern_set = true;
4184 if let Some(placeholder) = self.mask_pattern.placeholder() {
4185 self.placeholder = placeholder.into();
4186 }
4187 self
4188 }
4189
4190 pub fn set_mask_pattern(
4191 &mut self,
4192 pattern: impl Into<MaskPattern>,
4193 _: &mut Window,
4194 cx: &mut Context<Self>,
4195 ) {
4196 self.mask_pattern = pattern.into();
4197 self.mask_pattern_set = true;
4198 if let Some(placeholder) = self.mask_pattern.placeholder() {
4199 self.placeholder = placeholder.into();
4200 }
4201 cx.notify();
4202 }
4203
4204 pub fn ensure_number_mask(&mut self) {
4206 if self.mask_pattern_set {
4207 return;
4208 }
4209 self.mask_pattern = MaskPattern::Number {
4210 separator: None,
4211 fraction: None,
4212 };
4213 }
4214
4215 pub(super) fn set_input_bounds(&mut self, new_bounds: Bounds<Pixels>, cx: &mut Context<Self>) {
4216 let wrap_width_changed = self.input_bounds.size.width != new_bounds.size.width;
4217 self.input_bounds = new_bounds;
4218
4219 if let Some(last_layout) = self.last_layout.as_ref() {
4221 if wrap_width_changed {
4222 let wrap_width = if !self.soft_wrap {
4223 None
4225 } else {
4226 last_layout.wrap_width
4227 };
4228
4229 self.display_map.on_layout_changed(wrap_width, cx);
4230 if self.is_multi_line() {
4231 self.mode.update_auto_grow(&self.display_map);
4232 }
4233 cx.notify();
4234 }
4235 }
4236 }
4237
4238 pub fn selected_text(&self) -> RopeSlice<'_> {
4243 let range_utf16 = self.range_to_utf16(&self.selected_range());
4244 let range = self.range_from_utf16(&range_utf16);
4245 self.text.slice(range)
4246 }
4247
4248 pub fn range_to_bounds(&self, range: &Range<usize>) -> Option<Bounds<Pixels>> {
4252 let Some(last_layout) = self.last_layout.as_ref() else {
4253 return None;
4254 };
4255
4256 let Some(last_bounds) = self.last_bounds else {
4257 return None;
4258 };
4259
4260 let (_, _, start_pos) = self.line_and_position_for_offset(range.start);
4261 let (_, _, end_pos) = self.line_and_position_for_offset(range.end);
4262
4263 let Some(start_pos) = start_pos else {
4264 return None;
4265 };
4266 let Some(end_pos) = end_pos else {
4267 return None;
4268 };
4269
4270 Some(Bounds::from_corners(
4271 last_bounds.origin + start_pos,
4272 last_bounds.origin + end_pos + point(px(0.), last_layout.line_height),
4273 ))
4274 }
4275
4276 pub(crate) fn replace_text_in_range_silent(
4280 &mut self,
4281 range_utf16: Option<Range<usize>>,
4282 new_text: &str,
4283 window: &mut Window,
4284 cx: &mut Context<Self>,
4285 ) {
4286 self.silent_replace_text = true;
4287 self.replace_text_in_range(range_utf16, new_text, window, cx);
4288 self.silent_replace_text = false;
4289 }
4290
4291 pub(crate) fn replace_text_in_ranges(
4296 &mut self,
4297 edits: &[(Range<usize>, String)],
4298 window: &mut Window,
4299 cx: &mut Context<Self>,
4300 ) {
4301 if !self.is_editable() || edits.is_empty() {
4302 return;
4303 }
4304
4305 M::hide_context_menu(self, cx);
4308 M::clear_inline_completion(self, cx);
4309
4310 let mut sorted: Vec<(Range<usize>, &str)> = edits
4313 .iter()
4314 .map(|(range, text)| (self.normalize_token_range(range.clone()), text.as_str()))
4315 .collect();
4316 sorted.sort_by_key(|edit| std::cmp::Reverse(edit.0.start));
4317 if !self.token_spans().is_empty() {
4318 let mut merged: Vec<(Range<usize>, &str)> = Vec::with_capacity(sorted.len());
4319 for (range, text) in sorted {
4320 if let Some(last) = merged
4321 .last_mut()
4322 .filter(|last| range.end > last.0.start || range == last.0)
4323 {
4324 last.0.start = range.start.min(last.0.start);
4325 last.0.end = range.end.max(last.0.end);
4326 } else {
4327 merged.push((range, text));
4328 }
4329 }
4330 sorted = merged;
4331 }
4332
4333 #[cfg(debug_assertions)]
4334 for pair in sorted.windows(2) {
4335 debug_assert!(
4336 pair[1].0.end <= pair[0].0.start,
4337 "replace_text_in_ranges requires disjoint ranges"
4338 );
4339 }
4340
4341 let requested_intent = self.undo_manager.take_pending_intent();
4345 let selection_before = *self.active_selection();
4346 let original_selections: Vec<CursorSelection> = self.selections.iter().copied().collect();
4347 let selections_before: Vec<CursorSelection> = self
4351 .selections
4352 .iter()
4353 .map(|selection| {
4354 Self::collapse_for_intent(
4355 requested_intent.unwrap_or(EditIntent::Atomic),
4356 *selection,
4357 &(selection.start..selection.end),
4358 )
4359 })
4360 .collect();
4361 let group = sorted.len() > 1;
4362 if group {
4363 self.undo_manager.begin_transaction();
4364 }
4365
4366 let auto_closed_pairs_before = self.mode.auto_closed_pairs().clone();
4367 let mut recorded = false;
4368 let mut highlighter_edits = Vec::new();
4371 let accumulate_replay_edits = self.replaying_history && !self.emit_events;
4375 if !accumulate_replay_edits {
4376 self.last_input_edits.clear();
4377 }
4378 for (range, new_text) in &sorted {
4379 let old_text = self.text.clone();
4380 self.mode.adjust_auto_closed_pair(range, new_text.len());
4381 self.text.replace(range.clone(), new_text);
4382
4383 M::adjust_annotations(self, range, new_text.len());
4384 recorded |= self.push_history(
4385 &old_text,
4386 range,
4387 new_text,
4388 requested_intent,
4389 selection_before,
4390 None,
4391 );
4392
4393 self.display_map
4395 .adjust_folds_for_edit(&old_text, range, new_text);
4396 self.display_map
4397 .on_text_changed(&self.text, range, &Rope::from(*new_text), cx);
4398
4399 let edit = super::mode::replacement_input_edit(&old_text, &self.text, range, new_text);
4400 if self.emit_events || self.replaying_history {
4401 self.last_input_edits.push((edit, self.text.clone()));
4402 }
4403
4404 if group {
4405 if self.is_code_editor() {
4406 highlighter_edits.push((edit, self.text.clone()));
4407 }
4408 } else {
4409 self.mode.update_highlighter(
4410 super::mode::HighlighterUpdate {
4411 selected_range: range,
4412 old_text: &old_text,
4413 new_text: &self.text,
4414 change_text: new_text,
4415 force: true,
4416 input_edit: Some(edit),
4417 },
4418 window,
4419 cx,
4420 );
4421
4422 self.update_fold_candidates_incremental(range, new_text);
4423 }
4424 }
4425
4426 if group {
4427 self.mode
4428 .update_highlighter_batch::<M>(&highlighter_edits, window, cx);
4429
4430 let mut shift = 0isize;
4434 for (range, new_text) in sorted.iter().rev() {
4435 let start = (range.start as isize + shift) as usize;
4436 self.update_fold_candidates_incremental(&(start..start), new_text);
4437 shift += new_text.len() as isize - range.len() as isize;
4438 }
4439
4440 self.undo_manager.commit_transaction();
4441 }
4442
4443 if let Some(diagnostics) = self.mode.diagnostics_mut() {
4445 diagnostics.reset(&self.text)
4446 }
4447 M::refresh_language_features(self, window, cx);
4448 self.update_search(cx);
4449
4450 let mut ascending: Vec<(Range<usize>, &str)> = sorted.clone();
4453 ascending.sort_by_key(|edit| edit.0.start);
4454 let text_len = self.text.len();
4455 let mut delta: isize = 0;
4456 let mut edit_results = Vec::with_capacity(ascending.len());
4457 for (range, new_text) in &ascending {
4458 let offset = ((range.start as isize + delta) as usize + new_text.len()).min(text_len);
4459 edit_results.push((range.clone(), offset));
4460 delta += new_text.len() as isize - (range.end as isize - range.start as isize);
4461 }
4462
4463 let mut used = vec![false; edit_results.len()];
4464 let mut new_selections: Vec<CursorSelection> = Vec::with_capacity(ascending.len());
4465 for selection in original_selections {
4466 if let Ok(index) = edit_results
4467 .binary_search_by_key(&(selection.start, selection.end), |(range, _)| {
4468 (range.start, range.end)
4469 })
4470 && !used[index]
4471 {
4472 let offset = edit_results[index].1;
4473 used[index] = true;
4474 let mut selection = selection;
4475 selection.place_at(offset, None);
4476 new_selections.push(selection);
4477 }
4478 }
4479 for (index, (_, offset)) in edit_results.into_iter().enumerate() {
4480 if !used[index] {
4481 new_selections.push(CursorSelection::new(
4482 self.selections.generate_id(),
4483 offset,
4484 offset,
4485 ));
4486 }
4487 }
4488 self.selections.replace_all(new_selections);
4489 self.selections.merge_overlapping();
4490
4491 let selections_after: Vec<CursorSelection> = self.selections.iter().copied().collect();
4493 if recorded {
4494 self.undo_manager.record_auto_closed_pairs(
4495 auto_closed_pairs_before,
4496 self.mode.auto_closed_pairs().clone(),
4497 );
4498 self.undo_manager
4499 .record_selections(selections_before, selections_after);
4500 }
4501
4502 self.ime_marked_range.take();
4503 self.update_preferred_column();
4504 self.dismiss_touch_selection(cx);
4505 if self.is_multi_line() {
4506 self.mode.update_auto_grow(&self.display_map);
4507 }
4508 if self.emit_events {
4509 self.emit_change(cx);
4510 }
4511 cx.notify();
4512 }
4513
4514 fn update_fold_candidates(&mut self) {
4517 if !self.mode.is_folding() {
4518 return;
4519 }
4520
4521 let Some(highlighter_rc) = self.mode.highlighter() else {
4522 return;
4523 };
4524
4525 let highlighter = highlighter_rc.borrow();
4526 let Some(highlighter) = highlighter.as_ref() else {
4527 return;
4528 };
4529
4530 let fold_ranges = highlighter.fold_ranges(&self.text);
4531 self.display_map.set_fold_candidates(fold_ranges);
4532 }
4533
4534 fn update_fold_candidates_incremental(&mut self, edit_range: &Range<usize>, new_text: &str) {
4537 if !self.mode.is_folding() {
4538 return;
4539 }
4540
4541 let Some(highlighter_rc) = self.mode.highlighter() else {
4542 return;
4543 };
4544
4545 let highlighter = highlighter_rc.borrow();
4546 let Some(highlighter) = highlighter.as_ref() else {
4547 return;
4548 };
4549
4550 let new_end = edit_range.start + new_text.len();
4552 self.display_map.update_fold_candidates_for_edit(
4553 |range, text| highlighter.fold_ranges_for_edit(range, text),
4554 edit_range.start..new_end,
4555 &self.text,
4556 );
4557 }
4558}
4559
4560impl<M: InputModeKind> EntityInputHandler for InputBaseState<M> {
4561 fn text_for_range(
4562 &mut self,
4563 range_utf16: Range<usize>,
4564 adjusted_range: &mut Option<Range<usize>>,
4565 _window: &mut Window,
4566 _cx: &mut Context<Self>,
4567 ) -> Option<String> {
4568 let range = self.range_from_utf16(&range_utf16);
4569 adjusted_range.replace(self.range_to_utf16(&range));
4570 Some(self.text.slice(range).to_string())
4571 }
4572
4573 fn selected_text_range(
4574 &mut self,
4575 _ignore_disabled_input: bool,
4576 _window: &mut Window,
4577 _cx: &mut Context<Self>,
4578 ) -> Option<UTF16Selection> {
4579 Some(UTF16Selection {
4580 range: self.range_to_utf16(&self.selected_range()),
4581 reversed: self.active_selection().reversed,
4582 })
4583 }
4584
4585 fn set_selected_text_range(
4592 &mut self,
4593 range_utf16: Range<usize>,
4594 _window: &mut Window,
4595 cx: &mut Context<Self>,
4596 ) {
4597 let previous = self.selected_range();
4598 let was_reversed = self.active_selection().reversed;
4599 let range = self.range_from_utf16(&range_utf16);
4600 let range = range.start.min(range.end)..range.start.max(range.end);
4601 self.set_selected_range(range, cx);
4602
4603 let range = self.selected_range();
4604 let reversed = if range == previous {
4605 was_reversed
4606 } else if !range.is_empty() && range.end == previous.start {
4607 true
4608 } else if !range.is_empty() && range.start == previous.end {
4609 false
4610 } else {
4611 !range.is_empty() && range.start != previous.start && range.end == previous.end
4612 };
4613 self.active_selection_mut().reversed = reversed;
4614 self.scroll_to(self.cursor(), None, cx);
4615 cx.notify();
4616 }
4617
4618 fn marked_text_range(
4619 &self,
4620 _window: &mut Window,
4621 _cx: &mut Context<Self>,
4622 ) -> Option<Range<usize>> {
4623 self.ime_marked_range
4624 .map(|range| self.range_to_utf16(&range.into()))
4625 }
4626
4627 fn unmark_text(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {
4628 self.ime_marked_range = None;
4629 self.undo_manager.commit_transaction();
4630 }
4631
4632 fn replace_text_in_range(
4637 &mut self,
4638 range_utf16: Option<Range<usize>>,
4639 new_text: &str,
4640 window: &mut Window,
4641 cx: &mut Context<Self>,
4642 ) {
4643 let requested_intent = self.undo_manager.take_pending_intent();
4644 if !self.is_editable() {
4645 return;
4646 }
4647 let selection_before = *self.active_selection();
4648 let ends_composition = self.ime_marked_range.is_some();
4651
4652 self.pause_blink_cursor(cx);
4653
4654 let new_text = if self.replaying_history {
4658 Cow::Borrowed(new_text)
4659 } else {
4660 self.normalize_input(new_text)
4661 };
4662 let new_text: &str = &new_text;
4663
4664 let range = range_utf16
4665 .as_ref()
4666 .map(|range_utf16| self.range_from_utf16(range_utf16))
4667 .or(self.ime_marked_range.map(|range| {
4668 let range = self.range_to_utf16(&(range.start..range.end));
4669 self.range_from_utf16(&range)
4670 }))
4671 .unwrap_or(self.selected_range());
4672 let range = self.normalize_token_range(range);
4673
4674 if range.is_empty()
4680 && range.start == self.cursor()
4681 && self.ime_marked_range.is_none()
4682 && !self.silent_replace_text
4683 && self.is_code_editor()
4684 && self.selections.is_single()
4685 && self.active_selection().is_empty()
4686 {
4687 if let Some(target) = self.skip_over_target(new_text) {
4688 self.set_cursor_to(target);
4689 self.update_preferred_column();
4690 cx.notify();
4691 return;
4692 }
4693 }
4694
4695 if self.is_multi_line() {
4696 let multi_cursor = range_utf16.is_none()
4697 && self.ime_marked_range.is_none()
4698 && !self.selections.is_single();
4699 if multi_cursor {
4700 self.selections.merge_overlapping();
4701 let mut edits: Vec<(Range<usize>, String)> = self
4702 .selections
4703 .iter()
4704 .map(|sel| (sel.start..sel.end, new_text.to_string()))
4705 .collect();
4706 edits.sort_by_key(|(range, _)| range.start);
4707 let intent =
4711 requested_intent.unwrap_or_else(|| self.typing_intent(&edits, new_text));
4712 self.undo_manager.begin_transaction_with(intent);
4713 self.undo_manager.set_pending_intent(intent);
4714 self.replace_text_in_ranges(&edits, window, cx);
4715 self.undo_manager.commit_transaction();
4716 } else {
4717 if range_utf16.is_some() {
4718 self.selections.remove_all_but_active();
4719 }
4720 if let Some(intent) = requested_intent {
4721 self.undo_manager.set_pending_intent(intent);
4722 }
4723 if let Some((open_len, closer)) = self.auto_close_target(&range, new_text) {
4724 let replacement = format!("{new_text}{closer}");
4726 self.replace_text_in_ranges(&[(range.clone(), replacement)], window, cx);
4727 let cursor = range.start + new_text.len();
4728 self.mode.track_auto_closed_pair(
4729 cursor - open_len..cursor,
4730 cursor..cursor + closer.len(),
4731 );
4732 self.undo_manager
4733 .record_auto_closed_pairs_after(self.mode.auto_closed_pairs().clone());
4734 self.set_cursor_to(cursor);
4735 self.update_preferred_column();
4736 self.undo_manager.record_selections(
4737 vec![selection_before],
4738 self.selections.iter().copied().collect(),
4739 );
4740 } else {
4741 self.replace_text_in_ranges(
4742 &[(range.clone(), new_text.to_string())],
4743 window,
4744 cx,
4745 );
4746 }
4747 }
4748 if ends_composition {
4749 self.undo_manager.commit_transaction();
4750 }
4751
4752 if !self.silent_replace_text {
4753 M::on_text_typed(self, &range, new_text, window, cx);
4754 }
4755 return;
4756 }
4757
4758 let old_text = self.text.clone();
4760 let accumulate_replay_edits = self.replaying_history && !self.emit_events;
4764 if !accumulate_replay_edits {
4765 self.last_input_edits.clear();
4766 }
4767 self.mode.adjust_auto_closed_pair(&range, new_text.len());
4768 self.text.replace(range.clone(), new_text);
4769
4770 let mut new_offset = (range.start + new_text.len()).min(self.text.len());
4771
4772 let mut mask_changed = false;
4775
4776 if self.is_single_line() {
4777 let pending_text = self.text.to_string();
4778 if !self.replaying_history
4784 && !self.validated_token_edit
4785 && !self.is_valid_input(&pending_text, cx)
4786 && self.is_valid_input(&old_text.to_string(), cx)
4787 {
4788 self.text = old_text;
4789 return;
4790 }
4791
4792 if !self.replaying_history && !self.mask_pattern.is_none() {
4793 let mask_text = self.mask_pattern.mask(&pending_text);
4794 mask_changed = mask_text.as_str() != pending_text;
4795 self.text = Rope::from(mask_text.as_str());
4796 let new_text_len =
4797 (new_text.len() + mask_text.len()).saturating_sub(pending_text.len());
4798 new_offset = (range.start + new_text_len).min(mask_text.len());
4799 }
4800 }
4801
4802 if mask_changed {
4803 M::reset_annotations(self);
4806 } else {
4807 M::adjust_annotations(self, &range, new_text.len());
4808 }
4809 if mask_changed {
4810 self.push_history(
4814 &old_text,
4815 &(0..old_text.len()),
4816 &self.text.to_string(),
4817 Some(EditIntent::Atomic),
4818 selection_before,
4819 Some((new_offset..new_offset).into()),
4820 );
4821 } else {
4822 self.push_history(
4823 &old_text,
4824 &range,
4825 &new_text,
4826 requested_intent,
4827 selection_before,
4828 None,
4829 );
4830 }
4831 if let Some(diagnostics) = self.mode.diagnostics_mut() {
4832 diagnostics.reset(&self.text)
4833 }
4834 self.display_map
4836 .adjust_folds_for_edit(&old_text, &range, new_text);
4837 self.display_map
4838 .on_text_changed(&self.text, &range, &Rope::from(new_text), cx);
4839
4840 let edit = if mask_changed {
4841 Self::full_input_edit(&old_text, &self.text)
4842 } else {
4843 super::mode::replacement_input_edit(&old_text, &self.text, &range, new_text)
4844 };
4845 if self.emit_events || self.replaying_history {
4846 self.last_input_edits.push((edit, self.text.clone()));
4847 }
4848
4849 self.mode.update_highlighter::<M>(
4850 super::mode::HighlighterUpdate {
4851 selected_range: &range,
4852 old_text: &old_text,
4853 new_text: &self.text,
4854 change_text: &new_text,
4855 force: true,
4856 input_edit: Some(edit),
4857 },
4858 window,
4859 cx,
4860 );
4861
4862 self.update_fold_candidates_incremental(&range, new_text);
4863 M::refresh_language_features(self, window, cx);
4864 self.set_cursor_to(new_offset);
4865 self.ime_marked_range.take();
4866 if ends_composition {
4872 self.undo_manager
4873 .record_selections(vec![selection_before], vec![*self.active_selection()]);
4874 self.undo_manager.commit_transaction();
4875 }
4876 self.update_preferred_column();
4877 self.update_search(cx);
4878 self.dismiss_touch_selection(cx);
4879 if self.is_multi_line() {
4880 self.mode.update_auto_grow(&self.display_map);
4881 }
4882 if !self.silent_replace_text {
4883 M::on_text_typed(self, &range, &new_text, window, cx);
4884 }
4885 if self.emit_events {
4886 self.emit_change(cx);
4887 }
4888 cx.notify();
4889 }
4890
4891 fn replace_and_mark_text_in_range(
4893 &mut self,
4894 range_utf16: Option<Range<usize>>,
4895 new_text: &str,
4896 new_selected_range_utf16: Option<Range<usize>>,
4897 window: &mut Window,
4898 cx: &mut Context<Self>,
4899 ) {
4900 let requested_intent = self.undo_manager.take_pending_intent();
4901 if !self.is_editable() {
4902 return;
4903 }
4904 let selection_before = *self.active_selection();
4905
4906 let starts_composition = self.ime_marked_range.is_none();
4907 if starts_composition {
4908 self.undo_manager.break_transaction_coalescing();
4911 self.undo_manager.begin_transaction();
4912 }
4913
4914 self.selections.remove_all_but_active();
4916
4917 M::reset_language_features(self);
4918
4919 let new_text = if self.replaying_history {
4921 Cow::Borrowed(new_text)
4922 } else {
4923 self.normalize_input(new_text)
4924 };
4925 let new_text: &str = &new_text;
4926
4927 let range = range_utf16
4928 .as_ref()
4929 .map(|range_utf16| self.range_from_utf16(range_utf16))
4930 .or(self.ime_marked_range.map(|range| {
4931 let range = self.range_to_utf16(&(range.start..range.end));
4932 self.range_from_utf16(&range)
4933 }))
4934 .unwrap_or(self.selected_range());
4935 let range = self.normalize_token_range(range);
4936
4937 let auto_closed_pairs_before = self.mode.auto_closed_pairs().clone();
4938 let old_text = self.text.clone();
4939 self.last_input_edits.clear();
4942 self.mode.adjust_auto_closed_pair(&range, new_text.len());
4943 self.text.replace(range.clone(), new_text);
4944
4945 if self.is_single_line() {
4946 let pending_text = self.text.to_string();
4947 if !self.replaying_history
4949 && !self.validated_token_edit
4950 && !self.is_valid_input(&pending_text, cx)
4951 && self.is_valid_input(&old_text.to_string(), cx)
4952 {
4953 self.text = old_text;
4954 if starts_composition {
4955 self.undo_manager.commit_transaction();
4956 }
4957 return;
4958 }
4959 }
4960
4961 if self.ime_original_text.is_none() {
4962 self.ime_original_text = Some(old_text.clone());
4963 }
4964 M::adjust_annotations(self, &range, new_text.len());
4965 if let Some(diagnostics) = self.mode.diagnostics_mut() {
4966 diagnostics.reset(&self.text)
4967 }
4968 self.display_map
4970 .adjust_folds_for_edit(&old_text, &range, new_text);
4971 self.display_map
4972 .on_text_changed(&self.text, &range, &Rope::from(new_text), cx);
4973
4974 self.mode.update_highlighter::<M>(
4975 super::mode::HighlighterUpdate {
4976 selected_range: &range,
4977 old_text: &old_text,
4978 new_text: &self.text,
4979 change_text: &new_text,
4980 force: true,
4981 input_edit: None,
4982 },
4983 window,
4984 cx,
4985 );
4986
4987 self.update_fold_candidates_incremental(&range, new_text);
4988 M::refresh_language_features(self, window, cx);
4989 if new_text.is_empty() {
4990 self.set_cursor_to(range.start);
4992 self.ime_marked_range = None;
4993 } else {
4994 self.ime_marked_range = Some((range.start..range.start + new_text.len()).into());
4995 let new_range = new_selected_range_utf16
4996 .as_ref()
4997 .map(|range_utf16| {
4998 let new_text = Rope::from(new_text);
4999 range.start + new_text.offset_utf16_to_offset(range_utf16.start)
5000 ..range.start + new_text.offset_utf16_to_offset(range_utf16.end)
5001 })
5002 .unwrap_or_else(|| range.start + new_text.len()..range.start + new_text.len());
5003 self.set_selection(new_range.start, new_range.end);
5004 }
5005 if self.is_multi_line() {
5006 self.mode.update_auto_grow(&self.display_map);
5007 }
5008 if self.push_history(
5009 &old_text,
5010 &range,
5011 new_text,
5012 requested_intent,
5013 selection_before,
5014 Some(*self.active_selection()),
5015 ) {
5016 self.undo_manager
5017 .record_selections(vec![selection_before], vec![*self.active_selection()]);
5018 self.undo_manager.record_auto_closed_pairs(
5019 auto_closed_pairs_before,
5020 self.mode.auto_closed_pairs().clone(),
5021 );
5022 }
5023 if new_text.is_empty() {
5024 self.undo_manager.commit_transaction();
5025 }
5026 cx.notify();
5027 }
5028
5029 fn bounds_for_range(
5031 &mut self,
5032 range_utf16: Range<usize>,
5033 bounds: Bounds<Pixels>,
5034 _window: &mut Window,
5035 _cx: &mut Context<Self>,
5036 ) -> Option<Bounds<Pixels>> {
5037 let last_layout = self.last_layout.as_ref()?;
5038 let line_height = last_layout.line_height;
5039 let line_number_width = last_layout.line_number_width;
5040 let range = self.range_from_utf16(&range_utf16);
5041
5042 let mut start_origin = None;
5043 let mut end_origin = None;
5044 let line_number_origin = point(line_number_width, px(0.));
5045 let mut y_offset = last_layout.visible_top;
5046
5047 for (vi, line) in last_layout.lines.iter().enumerate() {
5048 if start_origin.is_some() && end_origin.is_some() {
5049 break;
5050 }
5051
5052 let index_offset = last_layout.visible_line_byte_offsets[vi];
5053
5054 if start_origin.is_none() {
5055 if let Some(p) = line.position_for_index(
5056 range.start.saturating_sub(index_offset),
5057 last_layout,
5058 false,
5059 ) {
5060 start_origin = Some(p + point(px(0.), y_offset));
5061 }
5062 }
5063
5064 if end_origin.is_none() {
5065 if let Some(p) = line.position_for_index(
5066 range.end.saturating_sub(index_offset),
5067 last_layout,
5068 false,
5069 ) {
5070 end_origin = Some(p + point(px(0.), y_offset));
5071 }
5072 }
5073
5074 y_offset += line.size(line_height).height;
5075 }
5076
5077 let start_origin = start_origin.or_else(|| {
5078 let offset = self.last_cursor.or(Some(self.cursor()))?;
5079 let (_, _, origin) = self.line_and_position_for_offset(offset);
5080 origin.map(|origin| origin - line_number_origin)
5081 })?;
5082 let mut end_origin = end_origin.unwrap_or(start_origin);
5083 end_origin.y = start_origin.y;
5085
5086 Some(Bounds::from_corners(
5087 bounds.origin + line_number_origin + start_origin,
5088 bounds.origin + line_number_origin + point(end_origin.x, end_origin.y + line_height),
5090 ))
5091 }
5092
5093 fn character_index_for_point(
5094 &mut self,
5095 point: gpui::Point<Pixels>,
5096 _window: &mut Window,
5097 _cx: &mut Context<Self>,
5098 ) -> Option<usize> {
5099 let last_layout = self.last_layout.as_ref()?;
5100 let line_point = self.last_bounds?.localize(&point)?;
5101
5102 for (vi, line) in last_layout.lines.iter().enumerate() {
5103 let offset = last_layout.visible_line_byte_offsets[vi];
5104 if let Some(utf8_index) = line.index_for_position(line_point, last_layout) {
5105 return Some(self.offset_to_utf16(offset + utf8_index));
5106 }
5107 }
5108
5109 None
5110 }
5111
5112 fn text_input_configuration(
5113 &mut self,
5114 _window: &mut Window,
5115 _cx: &mut Context<Self>,
5116 ) -> TextInputConfiguration {
5117 InputBaseState::text_input_configuration(self)
5118 }
5119}
5120
5121impl<M: InputModeKind> Focusable for InputBaseState<M> {
5122 fn focus_handle(&self, _cx: &App) -> FocusHandle {
5123 self.focus_handle.clone()
5124 }
5125}
5126
5127impl<M: InputModeKind> Render for InputBaseState<M> {
5128 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
5129 self.editor_style = self
5132 .projected_editor_style
5133 .resolved(&crate::Theme::global(cx).tokens);
5134 let entity = cx.entity();
5135 if self._pending_update {
5136 self.mode.update_highlighter::<M>(
5137 super::mode::HighlighterUpdate {
5138 selected_range: &(0..0),
5139 old_text: &self.text,
5140 new_text: &self.text,
5141 change_text: "",
5142 force: false,
5143 input_edit: None,
5144 },
5145 window,
5146 cx,
5147 );
5148
5149 self.update_fold_candidates();
5150 M::refresh_language_features(self, window, cx);
5151 self._pending_update = false;
5152 }
5153
5154 let element = div()
5155 .id(EDITOR_ELEMENT_ID)
5156 .key_context(CONTEXT)
5157 .when_some(self.accessibility.as_ref(), |this, accessibility| {
5158 this.when_some(accessibility.role, |this, role| this.role(role))
5159 .when_some(accessibility.label.clone(), |this, label| {
5160 this.aria_label(label)
5161 })
5162 .when_some(accessibility.placeholder.clone(), |this, placeholder| {
5163 this.aria_placeholder(placeholder)
5164 })
5165 .when_some(accessibility.accessibility_id.clone(), |this, id| {
5166 this.accessibility_id(id)
5167 })
5168 .when(
5169 accessibility.value_exposed
5170 && !self.masked
5171 && (window.is_a11y_active() || cfg!(feature = "test-support")),
5172 |this| this.aria_value(self.text.to_string()),
5173 )
5174 .on_a11y_action(gpui::AccessibleAction::Focus, {
5175 let entity = entity.clone();
5176 move |_, window, cx| {
5177 entity.update(cx, |state, cx| {
5178 state.handle_accessibility_focus(window, cx)
5179 });
5180 }
5181 })
5182 .when(self.is_editable(), |this| {
5183 this.on_a11y_action(gpui::AccessibleAction::SetValue, {
5184 let entity = entity.clone();
5185 move |data, window, cx| {
5186 entity.update(cx, |state, cx| {
5187 state.handle_accessibility_set_value(data, window, cx)
5188 });
5189 }
5190 })
5191 })
5192 })
5193 .when(self.is_editable(), |this| {
5194 this.on_action(window.listener_for(&entity, InputBaseState::backspace))
5195 .on_action(window.listener_for(&entity, InputBaseState::delete))
5196 .on_action(
5197 window.listener_for(&entity, InputBaseState::delete_to_beginning_of_line),
5198 )
5199 .on_action(window.listener_for(&entity, InputBaseState::delete_to_end_of_line))
5200 .on_action(window.listener_for(&entity, InputBaseState::delete_previous_word))
5201 .on_action(window.listener_for(&entity, InputBaseState::delete_next_word))
5202 .on_action(window.listener_for(&entity, InputBaseState::enter))
5203 .on_action(window.listener_for(&entity, InputBaseState::escape))
5204 .on_action(window.listener_for(&entity, InputBaseState::paste))
5205 .on_action(window.listener_for(&entity, InputBaseState::cut))
5206 .on_action(window.listener_for(&entity, InputBaseState::undo))
5207 .on_action(window.listener_for(&entity, InputBaseState::redo))
5208 .when(self.is_multi_line(), |this| {
5209 this.on_action(window.listener_for(&entity, InputBaseState::indent_inline))
5210 .on_action(window.listener_for(&entity, InputBaseState::outdent_inline))
5211 .on_action(window.listener_for(&entity, InputBaseState::indent_block))
5212 .on_action(window.listener_for(&entity, InputBaseState::outdent_block))
5213 })
5214 })
5215 .on_action(window.listener_for(&entity, InputBaseState::left))
5216 .on_action(window.listener_for(&entity, InputBaseState::right))
5217 .on_action(window.listener_for(&entity, InputBaseState::select_left))
5218 .on_action(window.listener_for(&entity, InputBaseState::select_right))
5219 .when(self.is_multi_line(), |this| {
5220 this.on_action(window.listener_for(&entity, InputBaseState::up))
5221 .on_action(window.listener_for(&entity, InputBaseState::down))
5222 .on_action(window.listener_for(&entity, InputBaseState::select_up))
5223 .on_action(window.listener_for(&entity, InputBaseState::select_down))
5224 .on_action(window.listener_for(&entity, InputBaseState::page_up))
5225 .on_action(window.listener_for(&entity, InputBaseState::page_down))
5226 .on_action(window.listener_for(&entity, InputBaseState::add_cursor_above))
5227 .on_action(window.listener_for(&entity, InputBaseState::add_cursor_below))
5228 })
5229 .on_action(window.listener_for(&entity, InputBaseState::on_action_select_all))
5230 .on_action(window.listener_for(&entity, InputBaseState::select_to_start_of_line))
5231 .on_action(window.listener_for(&entity, InputBaseState::select_to_end_of_line))
5232 .on_action(window.listener_for(&entity, InputBaseState::select_to_previous_word))
5233 .on_action(window.listener_for(&entity, InputBaseState::select_to_next_word))
5234 .on_action(window.listener_for(&entity, InputBaseState::home))
5235 .on_action(window.listener_for(&entity, InputBaseState::end))
5236 .on_action(window.listener_for(&entity, InputBaseState::move_to_start))
5237 .on_action(window.listener_for(&entity, InputBaseState::move_to_end))
5238 .on_action(window.listener_for(&entity, InputBaseState::move_to_previous_word))
5239 .on_action(window.listener_for(&entity, InputBaseState::move_to_next_word))
5240 .on_action(window.listener_for(&entity, InputBaseState::select_to_start))
5241 .on_action(window.listener_for(&entity, InputBaseState::select_to_end))
5242 .on_action(window.listener_for(&entity, InputBaseState::show_character_palette))
5243 .on_action({
5244 let entity = entity.clone();
5245 move |_: &ActivateToken, window, cx| {
5246 let state = entity.read(cx);
5247 let activation = state
5248 .token_spans()
5249 .iter()
5250 .find(|span| span.range() == state.selected_range())
5251 .and_then(|span| {
5252 state.range_to_bounds(&span.range()).and_then(|bounds| {
5253 state.token_activation(
5254 span.range().start,
5255 bounds,
5256 gpui::ClickEvent::Keyboard(gpui::KeyboardClickEvent {
5257 bounds,
5258 ..Default::default()
5259 }),
5260 )
5261 })
5262 });
5263 if let Some((listener, event)) = activation {
5264 listener(&event, window, cx);
5265 }
5266 }
5267 })
5268 .on_action(window.listener_for(&entity, InputBaseState::copy))
5269 .on_action(window.listener_for(&entity, InputBaseState::on_action_search))
5270 .on_action(window.listener_for(&entity, InputBaseState::on_action_replace))
5271 .on_mouse_down(
5272 MouseButton::Left,
5273 window.listener_for(&entity, InputBaseState::on_mouse_down),
5274 )
5275 .on_mouse_down(
5276 MouseButton::Right,
5277 window.listener_for(&entity, InputBaseState::on_mouse_down),
5278 )
5279 .on_mouse_up(
5280 MouseButton::Left,
5281 window.listener_for(&entity, InputBaseState::on_mouse_up),
5282 )
5283 .on_mouse_up(
5284 MouseButton::Right,
5285 window.listener_for(&entity, InputBaseState::on_mouse_up),
5286 )
5287 .on_mouse_move(window.listener_for(&entity, InputBaseState::on_mouse_move))
5288 .on_scroll(window.listener_for(&entity, InputBaseState::on_scroll_wheel))
5289 .when(self.is_multi_line() && !self.disabled, |this| {
5290 this.on_modifiers_changed(cx.listener(|_, _, _, cx| cx.notify()))
5291 })
5292 .when(!self.disabled, |this| {
5293 if self.is_multi_line() && window.modifiers().alt {
5294 this.cursor_crosshair()
5295 } else {
5296 this.cursor_text()
5297 }
5298 })
5299 .flex_1()
5300 .map(|this| {
5306 if self.is_multi_line() {
5307 this.h_full()
5308 } else {
5309 this.min_h_full().flex().items_center()
5310 }
5311 })
5312 .flex_grow_1()
5313 .overflow_x_hidden()
5314 .when(self.is_multi_line(), |this| {
5315 this.pt(self.editor_paddings.top)
5316 .pr(self.editor_paddings.right)
5317 .pb(self.editor_paddings.bottom)
5318 .pl(self.editor_paddings.left)
5319 })
5320 .child(TextElement::new(entity.clone()).placeholder(self.placeholder.clone()))
5321 .when(self.shows_scrollbar(), |this| {
5322 this.child(EditorScrollbar::new(entity.clone()))
5323 });
5324
5325 M::register_actions(element, &entity, window)
5328 .test_support()
5329 .track_focus(&self.focus_handle)
5330 }
5331}
5332
5333#[cfg(test)]
5334mod tests {
5335 use super::*;
5336
5337 #[gpui::test]
5338 fn test_set_value_waits_for_the_first_layout(cx: &mut TestAppContext) {
5339 cx.update(crate::init);
5340 let (state, cx) = cx.add_window_view(|window, cx| {
5341 let mut state = crate::input::InputState::new(window, cx);
5342 state.set_value("hello", window, cx);
5343 assert_eq!(state.display_map().wrap_row_count(), 0);
5345 state
5346 });
5347 cx.update(|window, cx| window.draw(cx).clear(cx));
5348 state.read_with(cx, |state, _| {
5349 assert_eq!(state.value(), "hello");
5350 assert_eq!(state.display_map().wrap_row_count(), 1);
5351 });
5352 }
5353
5354 #[gpui::test]
5355 fn accessibility_actions_recheck_live_editability(cx: &mut TestAppContext) {
5356 cx.update(crate::init);
5357 let (state, cx) = cx.add_window_view(|window, cx| {
5358 crate::input::InputState::new(window, cx).default_value("initial")
5359 });
5360 let changes = Rc::new(Cell::new(0));
5361 let observed = changes.clone();
5362 let _subscription = cx.update(|_, cx| {
5363 cx.subscribe(&state, move |_, event: &InputEvent, _| {
5364 if matches!(event, InputEvent::Change) {
5365 observed.set(observed.get() + 1);
5366 }
5367 })
5368 });
5369 cx.update(|window, cx| {
5370 state.update(cx, |state, cx| {
5371 state.handle_accessibility_set_value(None, window, cx);
5372 state.handle_accessibility_focus(window, cx);
5373 assert!(state.focus_handle.is_focused(window));
5374 });
5375 window.draw(cx).clear(cx);
5376 });
5377 assert_eq!(state.read_with(cx, |state, _| state.value()), "initial");
5378 assert_eq!(changes.get(), 0);
5379
5380 let action = gpui::accesskit::ActionData::Value("updated🦀".into());
5381 cx.update(|window, cx| {
5382 state.update(cx, |state, cx| {
5383 state.handle_accessibility_set_value(Some(&action), window, cx)
5384 });
5385 });
5386 assert_eq!(state.read_with(cx, |state, _| state.value()), "updated🦀");
5387 assert_eq!(changes.get(), 1);
5388 for disabled in [false, true] {
5389 cx.update(|window, cx| {
5390 window.blur(cx);
5391 state.update(cx, |state, cx| {
5392 state.set_disabled(disabled, cx);
5393 state.set_readonly(!disabled, cx);
5394 state.handle_accessibility_focus(window, cx);
5395 assert_eq!(state.focus_handle.is_focused(window), !disabled);
5396 let rejected = gpui::accesskit::ActionData::Value("rejected".into());
5397 state.handle_accessibility_set_value(Some(&rejected), window, cx);
5398 });
5399 });
5400 assert_eq!(state.read_with(cx, |state, _| state.value()), "updated🦀");
5401 assert_eq!(changes.get(), 1);
5402 }
5403 cx.update(|window, cx| {
5404 state.update(cx, |state, cx| {
5405 state.set_disabled(false, cx);
5406 state.set_readonly(false, cx);
5407 state.handle_accessibility_focus(window, cx);
5408 });
5409 window.draw(cx).clear(cx);
5410 window.dispatch_action(Box::new(Undo), cx);
5411 });
5412 assert_eq!(state.read_with(cx, |state, _| state.value()), "initial");
5413 cx.update(|window, cx| {
5414 state.update(cx, |state, cx| {
5415 state.set_masked(true, window, cx);
5416 state.handle_accessibility_set_value(Some(&action), window, cx);
5417 });
5418 });
5419 assert_eq!(state.read_with(cx, |state, _| state.value()), "updated🦀");
5420 }
5421
5422 #[gpui::test]
5423 fn accessibility_set_value_preserves_exact_editor_text(cx: &mut TestAppContext) {
5424 cx.update(crate::init);
5425 let (editor, cx) = cx.add_window_view(|window, cx| {
5426 crate::input::EditorState::new(window, cx).language("rust")
5427 });
5428 let action = gpui::accesskit::ActionData::Value("(".into());
5429 cx.update(|window, cx| {
5430 editor.update(cx, |editor, cx| {
5431 editor.handle_accessibility_set_value(Some(&action), window, cx)
5432 });
5433 });
5434 assert_eq!(editor.read_with(cx, |editor, _| editor.value()), "(");
5435 }
5436
5437 #[gpui::test]
5438 fn revealing_a_password_keeps_text_assistance_disabled(cx: &mut TestAppContext) {
5439 cx.update(crate::init);
5440 cx.open_window(size(px(400.), px(100.)), |window, cx| {
5441 let state = cx.new(|cx| crate::input::InputState::new(window, cx).masked(true));
5442 let hidden = state.read(cx).text_input_configuration();
5443 assert!(!hidden.autocorrect && !hidden.suggestions);
5444 assert_eq!(hidden.autocapitalize, Autocapitalize::None);
5445
5446 state.update(cx, |state, cx| state.toggle_masked(window, cx));
5448 assert_eq!(state.read(cx).text_input_configuration(), hidden);
5449 state.update(cx, |state, cx| state.toggle_masked(window, cx));
5450 assert_eq!(state.read(cx).text_input_configuration(), hidden);
5451 gpui::EmptyView
5452 });
5453 }
5454
5455 #[gpui::test]
5456 fn text_input_configuration_follows_the_input(cx: &mut TestAppContext) {
5457 use crate::input::{InputState, TextareaState};
5458
5459 cx.update(crate::init);
5460 let mut states = None;
5461 cx.open_window(size(px(400.), px(100.)), |window, cx| {
5462 states = Some((
5463 cx.new(|cx| InputState::new(window, cx)),
5464 cx.new(|cx| InputState::new(window, cx).masked(true)),
5465 cx.new(|cx| TextareaState::new(window, cx)),
5466 cx.new(|cx| {
5467 InputState::new(window, cx)
5468 .input_action(TextInputAction::Search)
5469 .autocorrect(true)
5470 .autocapitalize(Autocapitalize::Words)
5471 }),
5472 ));
5473 gpui::EmptyView
5474 });
5475 let (plain, masked, textarea, search) = states.unwrap();
5476
5477 cx.read(|cx| {
5478 assert_eq!(
5479 plain.read(cx).text_input_configuration(),
5480 TextInputConfiguration {
5481 input_action: TextInputAction::Done,
5482 ..Default::default()
5483 }
5484 );
5485 assert_eq!(
5486 masked.read(cx).text_input_configuration(),
5487 TextInputConfiguration {
5488 input_action: TextInputAction::Done,
5489 ..Default::default()
5490 }
5491 );
5492 assert_eq!(
5493 textarea.read(cx).text_input_configuration(),
5494 TextInputConfiguration {
5495 input_action: TextInputAction::Enter,
5496 ..Default::default()
5497 }
5498 );
5499 let search = search.read(cx).text_input_configuration();
5500 assert_eq!(search.input_action, TextInputAction::Search);
5501 assert_eq!(search.autocapitalize, Autocapitalize::Words);
5502 assert!(search.autocorrect);
5503 assert!(search.suggestions);
5504 });
5505 }
5506
5507 #[gpui::test]
5508 fn test_multicursor_unicode_completion_uses_post_edit_start(cx: &mut TestAppContext) {
5509 use crate::input::{CompletionProvider, EditorState};
5510 use gpui::Task;
5511 use lsp_types::{CompletionContext, CompletionResponse};
5512 use std::cell::RefCell;
5513
5514 #[derive(Default)]
5515 struct Provider {
5516 starts: RefCell<Vec<usize>>,
5517 queries: RefCell<Vec<(usize, String)>>,
5518 }
5519
5520 impl CompletionProvider for Provider {
5521 fn is_completion_trigger(&self, offset: usize, _: &str, _: &mut App) -> bool {
5522 self.starts.borrow_mut().push(offset);
5523 true
5524 }
5525
5526 fn completions(
5527 &self,
5528 _: &Rope,
5529 offset: usize,
5530 trigger: CompletionContext,
5531 _: &mut Window,
5532 _: &mut App,
5533 ) -> Task<anyhow::Result<CompletionResponse>> {
5534 self.queries
5535 .borrow_mut()
5536 .push((offset, trigger.trigger_character.unwrap()));
5537 Task::ready(Ok(CompletionResponse::Array(vec![])))
5538 }
5539 }
5540
5541 cx.update(crate::init);
5542 let mut editor = None;
5543 let window = cx.open_window(size(px(400.), px(100.)), |window, cx| {
5544 editor = Some(cx.new(|cx| EditorState::new(window, cx).default_value("aa")));
5545 gpui::EmptyView
5546 });
5547 let editor = editor.unwrap();
5548 let provider = Rc::new(Provider::default());
5549 window
5550 .update(cx, |_, window, cx| {
5551 editor.update(cx, |state, cx| {
5552 state.extras.lsp.completion_provider = Some(provider.clone());
5553 state
5554 .extras
5555 .context_menu_content
5556 .completion
5557 .trigger_start_offset = Some(0);
5558 state.extras.context_menu_content.completion.query = "aa".into();
5559 state.set_cursor_to(1);
5560 state.add_cursor_at(0, cx);
5561
5562 state.replace_text_in_range(None, "中", window, cx);
5563
5564 assert_eq!(state.value(), "中a中a");
5565 assert_eq!(provider.starts.borrow().as_slice(), &[4]);
5566 assert_eq!(provider.queries.borrow().as_slice(), &[(7, "中".into())]);
5567 });
5568 })
5569 .unwrap();
5570 }
5571
5572 #[gpui::test]
5573 fn selected_range_restores_unicode_direction_without_splitting_characters(
5574 cx: &mut TestAppContext,
5575 ) {
5576 use crate::input::EditorState;
5577 cx.update(crate::init);
5578 cx.open_window(size(px(400.), px(100.)), |window, cx| {
5579 let _state = cx.new(|cx| {
5580 let mut state = EditorState::new(window, cx).default_value("中😀a");
5581 state.set_selected_range_with_direction(3..8, true, cx);
5582 let selected = state.selected_text_range(false, window, cx).unwrap();
5583 assert_eq!(selected.range, 1..4);
5584 assert!(selected.reversed);
5585 assert_eq!(state.selected_range(), 3..8);
5586 assert!(state.is_selection_reversed());
5587 let range = state.selected_range();
5588 let reversed = state.is_selection_reversed();
5589 state.set_selected_range_with_direction(range, reversed, cx);
5590 assert!(state.is_selection_reversed());
5591 state.set_selected_range(3..8, cx);
5592 let selected = state.selected_text_range(false, window, cx).unwrap();
5593 assert_eq!(selected.range, 1..4);
5594 assert!(!selected.reversed);
5595 assert!(!state.is_selection_reversed());
5596 state.set_selected_range(Range { start: 8, end: 3 }, cx);
5598 assert_eq!(state.selected_range(), 8..8);
5599 assert!(!state.is_selection_reversed());
5600 state.set_selected_range_with_direction(4..4, true, cx);
5601 assert!(!state.is_selection_reversed());
5602 assert_eq!(
5603 state.selected_text_range(false, window, cx).unwrap().range,
5604 1..1
5605 );
5606 state
5607 });
5608 gpui::EmptyView
5609 });
5610 }
5611
5612 #[gpui::test]
5613 fn test_inline_token_wrap_and_size_refresh(cx: &mut TestAppContext) {
5614 use crate::input::{InlineToken, InlineTokenPresentation};
5615 let width = Rc::new(Cell::new(4000.));
5616 let render_width = width.clone();
5617 let view = InputView::build_textarea(cx, |state| state.rows(4).default_value("@a@b"));
5618 view.window_handle
5619 .update(cx, |_, window, cx| {
5620 view.input.update(cx, |state, cx| {
5621 state
5622 .replace_range_with_token(0..2, InlineToken::new("a", "@a"), window, cx)
5623 .unwrap();
5624 state
5625 .replace_range_with_token(2..4, InlineToken::new("b", "@b"), window, cx)
5626 .unwrap();
5627 state.set_token_presentation(
5628 InlineTokenPresentation::default()
5629 .token(move |_, _, _| div().w(px(render_width.get()))),
5630 );
5631 });
5632 })
5633 .unwrap();
5634 let mut visual = VisualTestContext::from_window(view.window_handle.into(), cx);
5635 visual.update(|window, cx| window.draw(cx).clear(cx));
5636 view.input.read_with(&visual, |state, _| {
5637 assert_eq!(state.display_map.wrap_row_count(), 2);
5638 let first = state.range_to_bounds(&(0..2)).unwrap();
5639 let second = state.range_to_bounds(&(2..4)).unwrap();
5640 assert!(second.origin.y > first.origin.y);
5641 assert_eq!(
5642 state.last_layout.as_ref().unwrap().lines[0].wrapped_lines[0].len,
5643 2
5644 );
5645 });
5646 width.set(100.);
5647 view.input.update(&mut visual, |_, cx| cx.notify());
5648 visual.update(|window, cx| window.draw(cx).clear(cx));
5649 view.input.read_with(&visual, |state, _| {
5650 assert_eq!(state.display_map.wrap_row_count(), 1)
5651 });
5652 }
5653
5654 #[gpui::test]
5655 fn test_inline_token_geometry_and_reentrant_activation(cx: &mut TestAppContext) {
5656 use crate::input::{InlineToken, InlineTokenPresentation};
5657 let view = InputView::build(cx, |state| state.default_value("before @alice after"));
5658 let target = view.input.clone();
5659 view.window_handle
5660 .update(cx, |_, window, cx| {
5661 view.input.update(cx, |state, cx| {
5662 state
5663 .replace_range_with_token(
5664 7..13,
5665 InlineToken::new("a", "@alice"),
5666 window,
5667 cx,
5668 )
5669 .unwrap();
5670 state.set_token_presentation(
5671 InlineTokenPresentation::default()
5672 .token(|_, _, _| div().w(px(100.)).h(px(20.)))
5673 .on_token_click(move |_, window, cx| {
5674 target.update(cx, |state, cx| {
5675 state.set_value("activated", window, cx)
5676 })
5677 }),
5678 );
5679 state.focus(window, cx);
5680 state.set_selected_range(7..13, cx);
5681 });
5682 })
5683 .unwrap();
5684 let mut visual = VisualTestContext::from_window(view.window_handle.into(), cx);
5685 visual.update(|window, cx| window.draw(cx).clear(cx));
5686 visual.update(|window, cx| {
5687 let state = view.input.read(cx);
5688 let bounds = state.range_to_bounds(&(7..13)).unwrap();
5689 assert!((bounds.size.width - px(100.)).abs() < px(1.), "{bounds:?}");
5690 let (left, _) = state.index_for_mouse_position(bounds.origin + point(px(10.), px(5.)));
5691 let (right, _) = state.index_for_mouse_position(bounds.origin + point(px(90.), px(5.)));
5692 assert_eq!((left, right), (7, 13));
5693 window.dispatch_action(Box::new(ActivateToken), cx);
5694 });
5695 visual.run_until_parked();
5696 view.input.read_with(&visual, |state, _| {
5697 assert_eq!(state.value().as_ref(), "activated")
5698 });
5699 }
5700
5701 #[gpui::test]
5702 fn test_inline_token_click_selects_it(cx: &mut TestAppContext) {
5703 use crate::input::InlineToken;
5704 cx.update(crate::init);
5705 let view = InputView::build(cx, |state| state.default_value("before @alice after"));
5706 view.window_handle
5707 .update(cx, |_, window, cx| {
5708 view.input.update(cx, |state, cx| {
5709 state
5710 .replace_range_with_token(
5711 7..13,
5712 InlineToken::new("a", "@alice"),
5713 window,
5714 cx,
5715 )
5716 .unwrap();
5717 state.set_selected_range(0..0, cx);
5718 });
5719 })
5720 .unwrap();
5721 let mut visual = VisualTestContext::from_window(view.window_handle.into(), cx);
5722 visual.update(|window, cx| window.draw(cx).clear(cx));
5723 let bounds = view
5724 .input
5725 .read_with(&visual, |state, _| state.range_to_bounds(&(7..13)).unwrap());
5726 visual.simulate_click(bounds.center(), gpui::Modifiers::default());
5727 visual.run_until_parked();
5728 view.input.read_with(&visual, |state, _| {
5729 assert_eq!(state.selected_range(), 7..13, "a click selects the token");
5730 });
5731 }
5732
5733 #[gpui::test]
5734 fn test_inline_token_hover_reports_presence_without_selecting(cx: &mut TestAppContext) {
5735 use crate::input::{InlineToken, InlineTokenPresentation};
5736 use gpui::{Bounds, point, px, size};
5737 cx.update(crate::init);
5738 let view = InputView::build(cx, |state| state.default_value("before @alice after"));
5739 let bounds = Bounds::new(point(px(10.), px(20.)), size(px(100.), px(20.)));
5740 view.window_handle
5741 .update(cx, |_, window, cx| {
5742 view.input.update(cx, |state, cx| {
5743 state
5744 .replace_range_with_token(
5745 7..13,
5746 InlineToken::new("a", "@alice"),
5747 window,
5748 cx,
5749 )
5750 .unwrap();
5751 state.set_token_presentation(
5752 InlineTokenPresentation::default().on_token_hover(|_, _, _| {}),
5753 );
5754 state.set_selected_range(0..0, cx);
5755 });
5756 })
5757 .unwrap();
5758 view.window_handle
5759 .update(cx, |_, _, cx| {
5760 view.input.update(cx, |state, _| {
5761 let (_, entered) = state
5762 .token_hover(7, bounds, true, None)
5763 .expect("hover enters an enabled token");
5764 assert!(entered.is_hovered());
5765 assert_eq!(entered.token().id().as_ref(), "a");
5766 assert_eq!(entered.range(), 7..13);
5767 assert_eq!(entered.bounds(), bounds);
5768 let (_, left) = state
5769 .token_hover(7, bounds, false, None)
5770 .expect("hover leaves the same token");
5771 assert!(!left.is_hovered());
5772 assert_eq!(state.selected_range(), 0..0, "hover never selects");
5773 assert!(state.token_hover(99, bounds, true, None).is_none());
5774 });
5775 })
5776 .unwrap();
5777
5778 view.window_handle
5780 .update(cx, |_, _, cx| {
5781 view.input.update(cx, |state, cx| {
5782 state.set_disabled(true, cx);
5783 });
5784 })
5785 .unwrap();
5786 view.window_handle
5787 .update(cx, |_, _, cx| {
5788 view.input.update(cx, |state, _| {
5789 assert!(state.token_hover(7, bounds, true, None).is_none())
5790 })
5791 })
5792 .unwrap();
5793 view.window_handle
5794 .update(cx, |_, _, cx| {
5795 view.input.update(cx, |state, cx| {
5796 state.set_disabled(false, cx);
5797 state.set_readonly(true, cx);
5798 });
5799 })
5800 .unwrap();
5801 view.window_handle
5802 .update(cx, |_, _, cx| {
5803 view.input.update(cx, |state, _| {
5804 assert!(state.token_hover(7, bounds, true, None).is_some())
5805 })
5806 })
5807 .unwrap();
5808
5809 view.window_handle
5811 .update(cx, |_, _, cx| {
5812 view.input.update(cx, |state, _| {
5813 state.set_token_presentation(InlineTokenPresentation::default());
5814 });
5815 })
5816 .unwrap();
5817 view.window_handle
5818 .update(cx, |_, _, cx| {
5819 view.input.update(cx, |state, _| {
5820 assert!(state.token_hover(7, bounds, true, None).is_none())
5821 })
5822 })
5823 .unwrap();
5824 }
5825
5826 #[gpui::test]
5827 fn test_inline_token_hover_exit_delivered_when_token_removed(cx: &mut TestAppContext) {
5828 use crate::input::{InlineToken, InlineTokenPresentation};
5829 use std::cell::RefCell;
5830 use std::rc::Rc;
5831 cx.update(crate::init);
5832 let view = InputView::build(cx, |state| state.default_value("before @alice after"));
5833 let events: Rc<RefCell<Vec<(SharedString, bool)>>> = Rc::new(RefCell::new(Vec::new()));
5834 let sink = events.clone();
5835 view.window_handle
5836 .update(cx, |_, window, cx| {
5837 view.input.update(cx, |state, cx| {
5838 state
5839 .replace_range_with_token(
5840 7..13,
5841 InlineToken::new("a", "@alice"),
5842 window,
5843 cx,
5844 )
5845 .unwrap();
5846 state.set_token_presentation(
5847 InlineTokenPresentation::default()
5848 .token(|_, _, _| div().w(px(100.)).h(px(20.)))
5849 .on_token_hover(move |event, _, _| {
5850 sink.borrow_mut()
5851 .push((event.token().id().clone(), event.is_hovered()));
5852 }),
5853 );
5854 });
5855 })
5856 .unwrap();
5857 let mut visual = VisualTestContext::from_window(view.window_handle.into(), cx);
5858 visual.update(|window, cx| window.draw(cx).clear(cx));
5859 let known_bounds = view.input.read_with(&visual, |state, _| {
5860 state
5861 .token_bounds
5862 .get(&7)
5863 .copied()
5864 .expect("placed token bounds")
5865 });
5866 visual.simulate_mouse_move(known_bounds.center(), None, gpui::Modifiers::default());
5867 visual.run_until_parked();
5868 visual.update(|window, cx| window.draw(cx).clear(cx));
5869 assert_eq!(events.borrow().as_slice(), &[("a".into(), true)]);
5870
5871 view.window_handle
5875 .update(cx, |_, window, cx| {
5876 view.input
5877 .update(cx, |state, cx| state.set_value("no tokens", window, cx));
5878 })
5879 .unwrap();
5880 visual.run_until_parked();
5881 visual.update(|window, cx| window.draw(cx).clear(cx));
5882 assert_eq!(
5883 events.borrow().as_slice(),
5884 &[("a".into(), true), ("a".into(), false)]
5885 );
5886 }
5887
5888 #[gpui::test]
5889 fn test_inline_token_hover_bounds_stay_single_row_at_wrap_boundary(cx: &mut TestAppContext) {
5890 use crate::input::{InlineToken, InlineTokenPresentation};
5891 cx.update(crate::init);
5892 let view = InputView::build_textarea(cx, |state| {
5893 state
5894 .rows(4)
5895 .default_value("aaaa bbbb cccc dddd @alice eeee ffff")
5896 });
5897 view.window_handle
5898 .update(cx, |_, window, cx| {
5899 view.input.update(cx, |state, cx| {
5900 state
5901 .replace_range_with_token(
5902 20..26,
5903 InlineToken::new("a", "@alice"),
5904 window,
5905 cx,
5906 )
5907 .unwrap();
5908 state.set_token_presentation(
5909 InlineTokenPresentation::default()
5910 .token(|_, _, _| div().w(px(60.)).h(px(20.))),
5911 );
5912 });
5913 })
5914 .unwrap();
5915 let mut visual = VisualTestContext::from_window(view.window_handle.into(), cx);
5916 visual.update(|window, cx| window.draw(cx).clear(cx));
5917 let (placed, ranged, line_height) = view.input.read_with(&visual, |state, _| {
5918 (
5919 state
5920 .token_bounds
5921 .get(&20)
5922 .copied()
5923 .expect("placed token bounds"),
5924 state.range_to_bounds(&(20..26)),
5925 state.last_layout.as_ref().unwrap().line_height,
5926 )
5927 });
5928 assert!(placed.size.height <= line_height + px(1.), "{placed:?}");
5932 assert!(
5933 placed.size.width <= px(61.) && placed.size.width > px(0.),
5934 "{placed:?}"
5935 );
5936 if let Some(ranged) = ranged {
5937 assert!(
5938 ranged.size.height >= placed.size.height - px(1.),
5939 "{ranged:?} vs {placed:?}"
5940 );
5941 }
5942 }
5943
5944 #[gpui::test]
5945 fn test_inline_token_hover_same_offset_replacement_exits_predecessor(cx: &mut TestAppContext) {
5946 use crate::input::{InlineToken, InlineTokenPresentation};
5947 use std::{cell::RefCell, rc::Rc};
5948 cx.update(crate::init);
5949 let view = InputView::build(cx, |state| state.default_value("@alice!"));
5950 let events = Rc::new(RefCell::new(Vec::new()));
5951 let sink = events.clone();
5952 view.window_handle
5953 .update(cx, |_, window, cx| {
5954 view.input.update(cx, |state, cx| {
5955 state
5956 .replace_range_with_token(0..6, InlineToken::new("a", "@alice"), window, cx)
5957 .unwrap();
5958 state.set_token_presentation(
5959 InlineTokenPresentation::default()
5960 .token(|_, _, _| div().w(px(100.)).h(px(20.)))
5961 .on_token_hover(move |event, _, _| {
5962 sink.borrow_mut()
5963 .push((event.token().id().clone(), event.is_hovered()));
5964 }),
5965 );
5966 });
5967 })
5968 .unwrap();
5969 let mut visual = VisualTestContext::from_window(view.window_handle.into(), cx);
5970 visual.update(|window, cx| window.draw(cx).clear(cx));
5971 let bounds = view
5972 .input
5973 .read_with(&visual, |state, _| state.token_bounds[&0]);
5974 visual.simulate_mouse_move(bounds.center(), None, gpui::Modifiers::default());
5975 visual.run_until_parked();
5976 visual.update(|window, cx| window.draw(cx).clear(cx));
5977 assert_eq!(events.borrow().as_slice(), &[("a".into(), true)]);
5978
5979 view.window_handle
5980 .update(cx, |_, window, cx| {
5981 view.input.update(cx, |state, cx| {
5982 state
5983 .replace_range_with_token(0..6, InlineToken::new("b", "@alice"), window, cx)
5984 .unwrap();
5985 });
5986 })
5987 .unwrap();
5988 visual.run_until_parked();
5989 visual.update(|window, cx| window.draw(cx).clear(cx));
5990 visual.simulate_mouse_move(
5991 bounds.center() + gpui::point(px(1.), px(0.)),
5992 None,
5993 gpui::Modifiers::default(),
5994 );
5995 visual.run_until_parked();
5996 visual.update(|window, cx| window.draw(cx).clear(cx));
5997 assert_eq!(
5998 events.borrow().as_slice(),
5999 &[("a".into(), true), ("a".into(), false), ("b".into(), true)]
6000 );
6001 view.input
6004 .update(&mut visual, |state, cx| state.set_disabled(true, cx));
6005 visual.update(|window, cx| window.draw(cx).clear(cx));
6006 visual.run_until_parked();
6007 assert_eq!(events.borrow().last(), Some(&("b".into(), false)));
6008 view.input
6009 .update(&mut visual, |state, cx| state.set_disabled(false, cx));
6010 visual.update(|window, cx| window.draw(cx).clear(cx));
6011 visual.run_until_parked();
6012 visual.simulate_mouse_move(bounds.center(), None, gpui::Modifiers::default());
6013 visual.run_until_parked();
6014 assert_eq!(
6015 events.borrow().as_slice(),
6016 &[
6017 ("a".into(), true),
6018 ("a".into(), false),
6019 ("b".into(), true),
6020 ("b".into(), false),
6021 ("b".into(), true)
6022 ]
6023 );
6024 visual.simulate_mouse_move(
6025 bounds.bottom_right() + gpui::point(px(10.), px(10.)),
6026 None,
6027 gpui::Modifiers::default(),
6028 );
6029 visual.run_until_parked();
6030 assert_eq!(
6031 events.borrow().as_slice(),
6032 &[
6033 ("a".into(), true),
6034 ("a".into(), false),
6035 ("b".into(), true),
6036 ("b".into(), false),
6037 ("b".into(), true),
6038 ("b".into(), false)
6039 ]
6040 );
6041 }
6042
6043 #[gpui::test]
6044 fn test_inline_token_hover_older_exit_keeps_newer_snapshot(cx: &mut TestAppContext) {
6045 use crate::input::{InlineToken, InlineTokenPresentation};
6046 use gpui::{Bounds, point, px, size};
6047 cx.update(crate::init);
6048 let view = InputView::build(cx, |state| state.default_value("@a @b"));
6049 let bounds = Bounds::new(point(px(10.), px(20.)), size(px(100.), px(20.)));
6050 view.window_handle
6051 .update(cx, |_, window, cx| {
6052 view.input.update(cx, |state, cx| {
6053 state
6054 .replace_range_with_token(0..2, InlineToken::new("a", "@a"), window, cx)
6055 .unwrap();
6056 state
6057 .replace_range_with_token(3..5, InlineToken::new("b", "@b"), window, cx)
6058 .unwrap();
6059 state.set_token_presentation(
6060 InlineTokenPresentation::default().on_token_hover(|_, _, _| {}),
6061 );
6062 state
6065 .token_hover(0, bounds, true, None)
6066 .expect("hover enters A");
6067 state
6068 .token_hover(3, bounds, true, None)
6069 .expect("hover enters B");
6070 let (_, exit_a) = state.token_hover(0, bounds, false, None).expect("A exits");
6071 assert!(!exit_a.is_hovered());
6072 assert_eq!(
6074 state
6075 .hovered_token
6076 .as_ref()
6077 .expect("B still hovered")
6078 .span
6079 .token()
6080 .id()
6081 .as_ref(),
6082 "b"
6083 );
6084 state.set_value("no tokens", window, cx);
6086 state.token_bounds.clear();
6087 let (listener, exit_b) = state.reconcile_token_hover().expect("B must exit");
6088 assert!(!exit_b.is_hovered());
6089 assert_eq!(exit_b.token().id().as_ref(), "b");
6090 drop(listener);
6091 });
6092 })
6093 .unwrap();
6094 }
6095
6096 #[gpui::test]
6097 fn test_inline_token_hover_late_exit_matches_replaced_token(cx: &mut TestAppContext) {
6098 use crate::input::{InlineToken, InlineTokenPresentation};
6099 cx.update(crate::init);
6100 let view = InputView::build(cx, |state| state.default_value("@a"));
6101 view.window_handle
6102 .update(cx, |_, window, cx| {
6103 view.input.update(cx, |state, cx| {
6104 let a = InlineToken::new("a", "@a");
6105 let b = InlineToken::new("b", "@a");
6106 state
6107 .replace_range_with_token(0..2, a.clone(), window, cx)
6108 .unwrap();
6109 state.set_token_presentation(
6110 InlineTokenPresentation::default().on_token_hover(|_, _, _| {}),
6111 );
6112 let bounds = gpui::Bounds::default();
6113 state.token_hover(0, bounds, true, Some(&a)).unwrap();
6114 state
6115 .replace_range_with_token(0..2, b.clone(), window, cx)
6116 .unwrap();
6117 state.token_hover(0, bounds, true, Some(&b)).unwrap();
6118 let (_, exit_a) = state.token_hover(0, bounds, false, Some(&a)).unwrap();
6119 assert_eq!(exit_a.token().id().as_ref(), "a");
6120 assert_eq!(state.hovered_token.as_ref().unwrap().span.token(), &b);
6121 let (_, exit_b) = state.token_hover(0, bounds, false, Some(&b)).unwrap();
6122 assert_eq!(exit_b.token().id().as_ref(), "b");
6123 assert!(state.pending_token_hover_exits.is_empty());
6124 });
6125 })
6126 .unwrap();
6127 }
6128
6129 #[gpui::test]
6130 fn test_inline_token_hover_exit_keeps_entry_utf16_after_edit(cx: &mut TestAppContext) {
6131 use crate::input::{InlineToken, InlineTokenPresentation};
6132 use gpui::EntityInputHandler as _;
6133 cx.update(crate::init);
6134 let view = InputView::build(cx, |state| state.default_value("🙂 @a"));
6135 view.window_handle
6136 .update(cx, |_, window, cx| {
6137 view.input.update(cx, |state, cx| {
6138 state
6139 .replace_range_with_token(5..7, InlineToken::new("a", "@a"), window, cx)
6140 .unwrap();
6141 state.set_token_presentation(
6142 InlineTokenPresentation::default().on_token_hover(|_, _, _| {}),
6143 );
6144 let bounds = gpui::Bounds::default();
6145 let (_, enter) = state.token_hover(5, bounds, true, None).unwrap();
6146 assert_eq!(enter.range_utf16(), (3, 5));
6147 state.replace_text_in_range(Some(0..2), "abcd", window, cx);
6148 assert_eq!(state.token_spans()[0].range(), 5..7);
6149 let (_, exit) = state.token_hover(5, bounds, false, None).unwrap();
6150 assert_eq!(exit.range_utf16(), (3, 5));
6151 });
6152 })
6153 .unwrap();
6154 }
6155
6156 #[gpui::test]
6157 fn test_set_selected_text_range(cx: &mut TestAppContext) {
6158 use gpui::EntityInputHandler as _;
6159 cx.update(crate::init);
6160 let view = InputView::build(cx, |state| state.default_value("🙂 hello world"));
6162 view.window_handle
6163 .update(cx, |_, window, cx| {
6164 view.input.update(cx, |state, cx| {
6165 fn selection(
6166 state: &mut InputBaseState<InputMode>,
6167 window: &mut Window,
6168 cx: &mut Context<InputBaseState<InputMode>>,
6169 ) -> (Range<usize>, bool) {
6170 let selection = state.selected_text_range(false, window, cx).unwrap();
6171 (selection.range, selection.reversed)
6172 }
6173
6174 state.set_selected_text_range(3..8, window, cx);
6175 assert_eq!(state.selected_range(), 5..10);
6176 assert_eq!(selection(state, window, cx), (3..8, false));
6177
6178 state.set_selected_text_range(3..14, window, cx);
6180 assert_eq!(selection(state, window, cx), (3..14, false));
6181 assert_eq!(state.cursor(), 16);
6182
6183 state.set_selected_text_range(9..14, window, cx);
6185 assert_eq!(selection(state, window, cx), (9..14, true));
6186 assert_eq!(state.cursor(), 11);
6187
6188 state.set_selected_text_range(9..14, window, cx);
6190 assert_eq!(selection(state, window, cx), (9..14, true));
6191
6192 state.set_selected_text_range(100..9, window, cx);
6194 assert_eq!(selection(state, window, cx), (9..14, true));
6195
6196 state.set_selected_text_range(3..8, window, cx);
6198 state.set_selected_text_range(2..3, window, cx);
6199 assert_eq!(selection(state, window, cx), (2..3, true));
6200 assert_eq!(state.cursor(), 4);
6201 state.set_selected_text_range(3..8, window, cx);
6202 state.set_selected_text_range(8..9, window, cx);
6203 assert_eq!(selection(state, window, cx), (8..9, false));
6204 assert_eq!(state.cursor(), 11);
6205
6206 state.set_selected_text_range(2..2, window, cx);
6208 assert_eq!(selection(state, window, cx), (2..2, false));
6209 assert_eq!(state.value(), "🙂 hello world");
6210 });
6211 })
6212 .unwrap();
6213 }
6214
6215 #[gpui::test]
6216 fn test_inline_token_hover_callback_edit_preserves_both_exits(cx: &mut TestAppContext) {
6217 use crate::input::{InlineToken, InlineTokenPresentation};
6218 cx.update(crate::init);
6219 let view = InputView::build(cx, |state| state.default_value("@a @b"));
6220 view.window_handle
6221 .update(cx, |_, window, cx| {
6222 view.input.update(cx, |state, cx| {
6223 state
6224 .replace_range_with_token(0..2, InlineToken::new("a", "@a"), window, cx)
6225 .unwrap();
6226 state
6227 .replace_range_with_token(3..5, InlineToken::new("b", "@b"), window, cx)
6228 .unwrap();
6229 state.set_token_presentation(
6230 InlineTokenPresentation::default().on_token_hover(|_, _, _| {}),
6231 );
6232 let bounds = gpui::Bounds::default();
6233 state.token_hover(0, bounds, true, None).unwrap();
6234 state.token_hover(3, bounds, true, None).unwrap();
6235 state.set_value("gone", window, cx);
6237 let (_, exit_a) = state.token_hover(0, bounds, false, None).unwrap();
6238 assert_eq!(exit_a.token().id().as_ref(), "a");
6239 let (_, exit_b) = state.reconcile_token_hover().unwrap();
6240 assert_eq!(exit_b.token().id().as_ref(), "b");
6241 assert!(state.token_hover(0, bounds, false, None).is_none());
6242 assert!(state.reconcile_token_hover().is_none());
6243 });
6244 })
6245 .unwrap();
6246 }
6247
6248 #[gpui::test]
6249 fn test_inline_token_edit_history_and_validation(cx: &mut TestAppContext) {
6250 let view = InputView::build(cx, |state| state.default_value("问 @alice!"));
6251 view.window_handle
6252 .update(cx, |_, window, cx| {
6253 view.input.update(cx, |state, cx| {
6254 let token =
6255 crate::input::InlineToken::new("alice-1", "@alice").with_label("Alice");
6256 state
6257 .replace_range_with_token(4..10, token.clone(), window, cx)
6258 .unwrap();
6259 assert_eq!(state.value().as_ref(), "问 @alice!");
6260 assert_eq!(state.tokens()[0].range(), 4..10);
6261 assert_eq!(state.next_end_of_word_at(4), 10);
6262 assert_eq!(state.previous_start_of_word_at(10), 4);
6263 state.undo(&Undo, window, cx);
6264 assert!(
6265 state.tokens().is_empty(),
6266 "identity-only association is undoable"
6267 );
6268 assert_eq!(state.value().as_ref(), "问 @alice!");
6269 state.redo(&Redo, window, cx);
6270 assert_eq!(state.tokens()[0].token(), &token);
6271 state.set_selected_range(6..7, cx);
6272 assert_eq!(state.selected_range(), 4..10);
6273 state.replace("", window, cx);
6274 assert_eq!(state.value().as_ref(), "问 !");
6275 assert!(state.tokens().is_empty());
6276 state.undo(&Undo, window, cx);
6277 assert_eq!(state.tokens()[0].token(), &token);
6278 state.set_selected_range(0..0, cx);
6279 state.replace("🙂", window, cx);
6280 assert_eq!(state.tokens()[0].range(), 8..14);
6281 state.undo(&Undo, window, cx);
6282 assert_eq!(state.tokens()[0].range(), 4..10);
6283 let before = state.content();
6284 state
6285 .replace_range_with_token(0..0, token.clone(), window, cx)
6286 .expect("the same reference may occur twice");
6287 assert_eq!(state.value().as_ref(), "@alice问 @alice!");
6288 assert_eq!(state.tokens().len(), 2);
6289 assert_eq!(state.tokens()[1].token(), &token);
6290 state.undo(&Undo, window, cx);
6291 assert_eq!(state.content(), before);
6292 state.set_value(before.text().clone(), window, cx);
6293 assert!(state.tokens().is_empty());
6294 assert!(!state.undo_manager.has_undos());
6295 });
6296 })
6297 .unwrap();
6298 }
6299
6300 #[gpui::test]
6301 fn test_inline_token_textarea_ime_and_modes(cx: &mut TestAppContext) {
6302 use crate::input::{InlineToken, InlineTokenError, InputContent};
6303 let view = InputView::build_textarea(cx, |state| state);
6304 view.window_handle
6305 .update(cx, |_, window, cx| {
6306 view.input.update(cx, |state, cx| {
6307 let content = InputContent::new("@a@b\n后面")
6308 .with_token(0..2, InlineToken::new("a", "@a"))
6309 .unwrap()
6310 .with_token(2..4, InlineToken::new("b", "@b"))
6311 .unwrap();
6312 state.set_value(content.clone(), window, cx);
6313 state.set_selected_range(2..2, cx);
6314 assert_eq!(state.previous_boundary(2), 0);
6315 assert_eq!(state.next_boundary(2), 4);
6316 state.replace_and_mark_text_in_range(Some(1..2), "中", Some(1..1), window, cx);
6317 assert_eq!(state.value().as_ref(), "中@b\n后面");
6318 assert_eq!(state.tokens().len(), 1);
6319 assert_eq!(
6320 state.replace_with_token(InlineToken::new("x", "x"), window, cx),
6321 Err(InlineTokenError::CompositionActive)
6322 );
6323 state.replace_text_in_range(None, "中文", window, cx);
6324 state.undo(&Undo, window, cx);
6325 assert_eq!(state.content(), content);
6326 state.redo(&Redo, window, cx);
6327 assert_eq!(state.value().as_ref(), "中文@b\n后面");
6328 state.undo(&Undo, window, cx);
6329 state.replace_all("plain", window, cx);
6330 assert!(state.tokens().is_empty());
6331 state.undo(&Undo, window, cx);
6332 assert_eq!(state.content(), content);
6333 });
6334 })
6335 .unwrap();
6336 let view = InputView::build(cx, |state| state);
6337 view.window_handle
6338 .update(cx, |_, window, cx| {
6339 view.input.update(cx, |state, cx| {
6340 assert_eq!(
6341 InputContent::new("a\u{301}")
6342 .with_token(0..1, InlineToken::new("bad", "a"))
6343 .err(),
6344 Some(InlineTokenError::InvalidBoundary)
6345 );
6346 assert_eq!(
6347 InputContent::new("@a@a")
6348 .with_token(0..2, InlineToken::new("a", "@a"))
6349 .unwrap()
6350 .with_token(1..3, InlineToken::new("x", "a@"))
6351 .err(),
6352 Some(InlineTokenError::OverlappingTokens)
6353 );
6354 state
6355 .replace_with_token(InlineToken::new("a", "@a"), window, cx)
6356 .unwrap();
6357 let before = state.content();
6358 state.set_masked(true, window, cx);
6359 assert!(!state.tokens_visible());
6360 assert_eq!(
6361 state.replace_with_token(InlineToken::new("b", "b"), window, cx),
6362 Err(InlineTokenError::UnsupportedMode)
6363 );
6364 assert_eq!(state.content(), before);
6365 state.undo(&Undo, window, cx);
6366 assert!(state.tokens().is_empty());
6367 state.redo(&Redo, window, cx);
6368 assert_eq!(state.content(), before);
6369 assert!(!state.tokens_visible());
6370 });
6371 })
6372 .unwrap();
6373 }
6374
6375 use crate::theme::Theme;
6376 use gpui::{TestAppContext, VisualTestContext, size};
6377
6378 use crate::input::{EditorMode, EditorState, InputMode, LanguageConfig, TabSize, TextareaMode};
6379
6380 fn set_test_syntax_provider(
6381 provider: Rc<dyn crate::input::SyntaxContextProvider>,
6382 cx: &mut App,
6383 ) {
6384 struct TestLanguages(Rc<dyn crate::input::SyntaxContextProvider>);
6385 impl crate::input::LanguageProvider for TestLanguages {
6386 fn syntax_context_provider(
6387 &self,
6388 _: &str,
6389 ) -> Option<Rc<dyn crate::input::SyntaxContextProvider>> {
6390 Some(self.0.clone())
6391 }
6392 }
6393 crate::input::set_language_provider(Rc::new(TestLanguages(provider)), cx);
6394 }
6395
6396 struct TestRoot<M: InputModeKind>(Entity<InputBaseState<M>>);
6397
6398 impl<M: InputModeKind> Render for TestRoot<M> {
6399 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
6400 div().size_full().child(self.0.clone())
6401 }
6402 }
6403
6404 struct InputView<M: InputModeKind> {
6405 input: Entity<InputBaseState<M>>,
6406 window_handle: gpui::WindowHandle<TestRoot<M>>,
6407 }
6408
6409 impl<M: InputModeKind> InputView<M> {
6411 fn build_with(
6412 cx: &mut TestAppContext,
6413 make: impl FnOnce(&mut Window, &mut Context<InputBaseState<M>>) -> InputBaseState<M>
6414 + 'static,
6415 ) -> Self {
6416 let mut input: Option<Entity<InputBaseState<M>>> = None;
6417
6418 let window = cx.update(|cx| {
6419 cx.open_window(Default::default(), |window, cx| {
6420 cx.set_global(Theme::default());
6422 super::super::init(cx);
6424
6425 input = Some(cx.new(|cx| make(window, cx)));
6426
6427 cx.new(|_| TestRoot(input.clone().unwrap()))
6428 })
6429 .unwrap()
6430 });
6431
6432 Self {
6433 input: input.clone().unwrap(),
6434 window_handle: window,
6435 }
6436 }
6437 }
6438
6439 impl InputView<EditorMode> {
6440 fn new(cx: &mut TestAppContext) -> Self {
6442 Self::build_editor(cx, |state| state)
6443 }
6444
6445 fn build_editor(
6446 cx: &mut TestAppContext,
6447 f: impl FnOnce(InputBaseState<EditorMode>) -> InputBaseState<EditorMode> + 'static,
6448 ) -> Self {
6449 Self::build_with(cx, move |window, cx| {
6450 f(crate::input::EditorState::new(window, cx).language("sql"))
6451 })
6452 }
6453 }
6454
6455 impl InputView<TextareaMode> {
6456 fn build_textarea(
6457 cx: &mut TestAppContext,
6458 f: impl FnOnce(InputBaseState<TextareaMode>) -> InputBaseState<TextareaMode> + 'static,
6459 ) -> Self {
6460 Self::build_with(cx, move |window, cx| {
6461 f(crate::input::TextareaState::new(window, cx))
6462 })
6463 }
6464 }
6465
6466 impl InputView<InputMode> {
6467 fn build(
6469 cx: &mut TestAppContext,
6470 f: impl FnOnce(InputBaseState<InputMode>) -> InputBaseState<InputMode> + 'static,
6471 ) -> Self {
6472 Self::build_with(cx, move |window, cx| {
6473 f(crate::input::InputState::new(window, cx))
6474 })
6475 }
6476 }
6477
6478 fn swipe<M: InputModeKind>(
6480 view: &InputView<M>,
6481 cx: &mut VisualTestContext,
6482 dy: f32,
6483 phase: gpui::TouchPhase,
6484 ) {
6485 cx.update(|window, cx| {
6486 view.input.update(cx, |state, cx| {
6487 state.on_scroll_wheel(
6488 &ScrollEvent {
6489 delta: gpui::ScrollDelta::Pixels(point(px(0.), px(dy))),
6490 touch_phase: phase,
6491 ..Default::default()
6492 },
6493 window,
6494 cx,
6495 )
6496 })
6497 });
6498 }
6499
6500 fn long_text() -> String {
6501 (0..60)
6502 .map(|n| format!("line {n}"))
6503 .collect::<Vec<_>>()
6504 .join("\n")
6505 }
6506
6507 #[gpui::test]
6508 fn test_scroll_bounce_stretches_past_the_end_and_springs_back(cx: &mut TestAppContext) {
6509 use gpui::TouchPhase::{Ended, Moved, Started};
6510
6511 let view = InputView::build_textarea(cx, |state| {
6512 state.scroll_bounce(true).default_value(long_text())
6513 });
6514 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
6515 cx.update(|window, cx| window.draw(cx).clear(cx));
6516 let drawn_at = |cx: &mut VisualTestContext| {
6517 view.input
6518 .read_with(cx, |state, _| state.last_bounds.unwrap().origin.y)
6519 };
6520
6521 swipe(&view, &mut cx, -20., Started);
6522 swipe(&view, &mut cx, -100_000., Moved);
6523 let (end, stretch) = view.input.read_with(&cx, |state, _| {
6524 (state.scroll_handle.offset().y, state.overscroll.offset())
6525 });
6526 assert!(end < px(0.), "the text scrolled to its end");
6527 assert!(stretch < 0., "the rest stretched the edge: {stretch}");
6528 cx.update(|window, cx| window.draw(cx).clear(cx));
6529 let stretched_at = drawn_at(&mut cx);
6530
6531 swipe(&view, &mut cx, 0., Ended);
6532 for _ in 0..120 {
6533 cx.executor()
6534 .advance_clock(std::time::Duration::from_millis(16));
6535 cx.update(|window, cx| window.draw(cx).clear(cx));
6536 }
6537 view.input.read_with(&cx, |state, _| {
6538 assert_eq!(state.overscroll.offset(), 0., "the edge sprang back");
6539 assert_eq!(
6540 state.scroll_handle.offset().y,
6541 end,
6542 "scrolling stayed exact"
6543 );
6544 });
6545 let moved = (stretched_at - drawn_at(&mut cx)).as_f32();
6546 assert!(
6547 (moved - stretch).abs() < 0.001,
6548 "the stretch only moved where the text was drawn: {moved} vs {stretch}"
6549 );
6550 }
6551
6552 fn swipe_past_the_end(
6555 view: &InputView<TextareaMode>,
6556 cx: &mut VisualTestContext,
6557 ) -> (Pixels, f32) {
6558 use gpui::TouchPhase::{Moved, Started};
6559
6560 cx.update(|window, cx| window.draw(cx).clear(cx));
6561 swipe(view, cx, -20., Started);
6562 swipe(view, cx, -100_000., Moved);
6563 view.input.read_with(cx, |state, _| {
6564 (state.scroll_handle.offset().y, state.overscroll.offset())
6565 })
6566 }
6567
6568 #[gpui::test]
6569 fn test_scroll_bounce_stays_put_under_reduced_motion(cx: &mut TestAppContext) {
6570 cx.update(|cx| cx.set_reduce_motion(true));
6571 let view = InputView::build_textarea(cx, |state| {
6572 state.scroll_bounce(true).default_value(long_text())
6573 });
6574 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
6575 let (end, stretch) = swipe_past_the_end(&view, &mut cx);
6576 assert!(end < px(0.), "the text still scrolls");
6577 assert_eq!(stretch, 0., "the edge does not stretch");
6578 }
6579
6580 #[gpui::test]
6581 fn test_scroll_bounce_follows_the_platform_by_default(cx: &mut TestAppContext) {
6582 let view = InputView::build_textarea(cx, |state| state.default_value(long_text()));
6583 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
6584 let (end, stretch) = swipe_past_the_end(&view, &mut cx);
6585 assert!(end < px(0.), "the text scrolls");
6586 assert_eq!(
6587 stretch != 0.,
6588 crate::ScrollBounce::enabled_by_default(),
6589 "the edge stretches where the platform's scroll views do"
6590 );
6591 }
6592
6593 #[gpui::test]
6594 fn test_scroll_bounce_leaves_text_that_fits(cx: &mut TestAppContext) {
6595 let view = InputView::build_textarea(cx, |state| {
6596 state.scroll_bounce(true).default_value("one line")
6597 });
6598 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
6599 let (end, stretch) = swipe_past_the_end(&view, &mut cx);
6600 assert_eq!((end, stretch), (px(0.), 0.));
6601 }
6602
6603 #[gpui::test]
6604 fn test_scroll_bounce_resets_when_replacement_text_fits(cx: &mut TestAppContext) {
6605 let view = InputView::build_textarea(cx, |state| {
6606 state.scroll_bounce(true).default_value(long_text())
6607 });
6608 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
6609 let (_, stretch) = swipe_past_the_end(&view, &mut cx);
6610 assert!(stretch < 0.);
6611 cx.update(|window, cx| {
6612 view.input
6613 .update(cx, |state, cx| state.set_value("short", window, cx));
6614 window.draw(cx).clear(cx);
6615 });
6616 swipe(&view, &mut cx, 0., gpui::TouchPhase::Ended);
6617 view.input.read_with(&cx, |state, _| {
6618 assert_eq!(state.overscroll.offset(), 0.);
6619 assert_eq!(state.scroll_handle.offset().y, px(0.));
6620 });
6621 }
6622
6623 #[gpui::test]
6624 fn test_scroll_bounce_releases_when_the_end_packet_is_horizontal(cx: &mut TestAppContext) {
6625 let view = InputView::build_textarea(cx, |state| {
6626 state.scroll_bounce(true).default_value(long_text())
6627 });
6628 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
6629 let (_, stretch) = swipe_past_the_end(&view, &mut cx);
6630 assert!(stretch < 0.);
6631 cx.update(|window, cx| {
6632 view.input.update(cx, |state, cx| {
6633 state.on_scroll_wheel(
6634 &ScrollEvent {
6635 delta: gpui::ScrollDelta::Pixels(point(px(1.), px(0.))),
6636 touch_phase: gpui::TouchPhase::Ended,
6637 ..Default::default()
6638 },
6639 window,
6640 cx,
6641 );
6642 });
6643 });
6644 for _ in 0..120 {
6645 cx.executor()
6646 .advance_clock(std::time::Duration::from_millis(16));
6647 cx.update(|window, cx| window.draw(cx).clear(cx));
6648 }
6649 assert_eq!(
6650 view.input
6651 .read_with(&cx, |state, _| state.overscroll.offset()),
6652 0.
6653 );
6654 }
6655
6656 #[gpui::test]
6657 fn test_scroll_bounce_ordinary_mouse_wheel_does_not_stretch(cx: &mut TestAppContext) {
6658 let view = InputView::build_textarea(cx, |state| {
6659 state.scroll_bounce(true).default_value(long_text())
6660 });
6661 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
6662 cx.update(|window, cx| {
6663 window.draw(cx).clear(cx);
6664 view.input.update(cx, |state, cx| {
6665 state.on_scroll_wheel(
6666 &ScrollEvent {
6667 delta: gpui::ScrollDelta::Lines(point(0., -100_000.)),
6668 touch_phase: gpui::TouchPhase::Moved,
6669 ..Default::default()
6670 },
6671 window,
6672 cx,
6673 );
6674 assert!(state.scroll_handle.offset().y < px(0.));
6675 assert_eq!(state.overscroll.offset(), 0.);
6676 });
6677 });
6678 }
6679
6680 #[gpui::test]
6681 fn test_scroll_bounce_new_wheel_input_interrupts_the_return(cx: &mut TestAppContext) {
6682 let view = InputView::build_textarea(cx, |state| {
6683 state.scroll_bounce(true).default_value(long_text())
6684 });
6685 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
6686 let (end, stretch) = swipe_past_the_end(&view, &mut cx);
6687 assert!(stretch < 0.);
6688 swipe(&view, &mut cx, 0., gpui::TouchPhase::Ended);
6689 cx.executor()
6690 .advance_clock(std::time::Duration::from_millis(16));
6691 cx.update(|window, cx| {
6692 window.draw(cx).clear(cx);
6693 view.input.update(cx, |state, cx| {
6694 state.on_scroll_wheel(
6695 &ScrollEvent {
6696 delta: gpui::ScrollDelta::Lines(point(0., 2.)),
6697 touch_phase: gpui::TouchPhase::Moved,
6698 ..Default::default()
6699 },
6700 window,
6701 cx,
6702 );
6703 assert!(state.scroll_handle.offset().y > end);
6704 assert_eq!(state.overscroll.offset(), 0.);
6705 });
6706 });
6707 }
6708
6709 #[gpui::test]
6710 fn test_turning_scroll_bounce_off_drops_the_stretch(cx: &mut TestAppContext) {
6711 let view = InputView::build_textarea(cx, |state| {
6712 state.scroll_bounce(true).default_value(long_text())
6713 });
6714 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
6715 let (_, stretch) = swipe_past_the_end(&view, &mut cx);
6716 assert!(stretch < 0., "the edge stretched: {stretch}");
6717 view.input
6718 .update(&mut cx, |state, cx| state.set_scroll_bounce(false, cx));
6719 assert_eq!(
6720 view.input
6721 .read_with(&cx, |state, _| state.overscroll.offset()),
6722 0.
6723 );
6724 }
6725
6726 #[gpui::test]
6729 fn single_line_is_centered_in_a_taller_frame(cx: &mut TestAppContext) {
6730 struct Frame(Entity<InputBaseState<InputMode>>);
6731 impl Render for Frame {
6732 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
6733 crate::input::InputBase::new("frame")
6734 .h(px(60.))
6735 .child(crate::input::Input::new(&self.0))
6736 }
6737 }
6738
6739 cx.update(crate::init);
6740 let mut input = None;
6741 let window = cx.open_window(size(px(400.), px(100.)), |window, cx| {
6742 let state = cx.new(|cx| crate::input::InputState::new(window, cx).default_value("a"));
6743 input = Some(state.clone());
6744 Frame(state)
6745 });
6746 let input = input.unwrap();
6747 let mut cx = VisualTestContext::from_window(window.into(), cx);
6748 cx.update(|window, cx| window.draw(cx).clear(cx));
6749
6750 input.read_with(&cx, |state, _| {
6751 let line = state.input_bounds;
6752 assert_eq!(line.center().y, px(30.), "line {line:?}");
6753 });
6754 }
6755
6756 #[gpui::test]
6757 fn test_noop_scroll_notifies_diagnostic_dismissal(cx: &mut TestAppContext) {
6758 use std::{cell::Cell, rc::Rc};
6759
6760 cx.update(crate::init);
6761 let mut input = None;
6762 let window = cx.open_window(size(px(400.), px(100.)), |window, cx| {
6763 input = Some(cx.new(|cx| crate::input::EditorState::new(window, cx)));
6764 gpui::EmptyView
6765 });
6766 let input = input.unwrap();
6767 input.update(cx, |state, cx| {
6768 state.input_bounds = Bounds::new(Point::default(), size(px(100.), px(20.)));
6769 state.scroll_size = size(px(100.), px(20.));
6770 state.present_diagnostic(crate::input::DiagnosticEntry::default(), cx);
6771 });
6772 let notifications = Rc::new(Cell::new(0));
6773 let count = notifications.clone();
6774 let _subscription =
6775 cx.update(|cx| cx.observe(&input, move |_, _| count.set(count.get() + 1)));
6776
6777 window
6778 .update(cx, |_, window, cx| {
6779 input.update(cx, |state, cx| {
6780 state.on_scroll_wheel(
6781 &ScrollEvent {
6782 delta: gpui::ScrollDelta::Pixels(point(px(0.), px(-40.))),
6783 ..Default::default()
6784 },
6785 window,
6786 cx,
6787 );
6788 assert_eq!(state.scroll_handle.offset(), Point::default());
6789 assert!(state.diagnostic_popover().is_none());
6790 });
6791 })
6792 .unwrap();
6793 assert_eq!(
6794 notifications.get(),
6795 1,
6796 "clearing a diagnostic must notify even when scrolling is clamped"
6797 );
6798 window
6799 .update(cx, |_, window, cx| {
6800 input.update(cx, |state, cx| {
6801 state.on_scroll_wheel(
6802 &ScrollEvent {
6803 delta: gpui::ScrollDelta::Pixels(point(px(0.), px(-40.))),
6804 ..Default::default()
6805 },
6806 window,
6807 cx,
6808 );
6809 });
6810 })
6811 .unwrap();
6812 assert_eq!(notifications.get(), 1, "an unchanged input must stay quiet");
6813 }
6814
6815 #[gpui::test]
6818 fn test_hover_reaches_bottom_of_scrolled_editor(cx: &mut TestAppContext) {
6819 use crate::input::{Diagnostic, Position};
6820 struct Frame(Entity<crate::input::EditorState>);
6821 impl Render for Frame {
6822 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
6823 crate::input::InputBase::new("frame")
6824 .h(px(100.))
6825 .child(crate::input::Editor::new(&self.0))
6826 }
6827 }
6828
6829 cx.update(crate::init);
6830 let text = (0..100).map(|i| format!("line {i}\n")).collect::<String>();
6831 let mut input = None;
6832 let window = cx.open_window(size(px(400.), px(100.)), |window, cx| {
6833 let state = cx.new(|cx| crate::input::EditorState::new(window, cx).default_value(text));
6834 input = Some(state.clone());
6835 Frame(state)
6836 });
6837 let input = input.unwrap();
6838 let mut cx = VisualTestContext::from_window(window.into(), cx);
6839 input.update(&mut cx, |state, _| {
6840 state.diagnostics_mut().unwrap().push(Diagnostic::new(
6841 Position::new(0, 0)..Position::new(100, 0),
6842 "everywhere",
6843 ));
6844 });
6845 cx.update(|window, cx| window.draw(cx).clear(cx));
6846 cx.update(|window, cx| {
6847 input.update(cx, |state, cx| {
6848 state.on_scroll_wheel(
6849 &ScrollEvent {
6850 delta: gpui::ScrollDelta::Pixels(point(px(0.), px(-400.))),
6851 ..Default::default()
6852 },
6853 window,
6854 cx,
6855 );
6856 })
6857 });
6858 cx.update(|window, cx| window.draw(cx).clear(cx));
6859
6860 let bounds = input.read_with(&cx, |state, _| state.input_bounds);
6861 cx.simulate_mouse_move(
6862 point(bounds.center().x, bounds.bottom() - px(5.)),
6863 None,
6864 gpui::Modifiers::default(),
6865 );
6866 assert!(
6867 input.read_with(&cx, |state, _| state.diagnostic_popover().is_some()),
6868 "no hover at the bottom of a scrolled editor, bounds {bounds:?}"
6869 );
6870 }
6871
6872 #[gpui::test]
6873 fn test_cursor_layout_consumer_updates_after_selection(cx: &mut TestAppContext) {
6874 use std::{cell::Cell, rc::Rc};
6875 struct Panel {
6876 input: Entity<crate::input::EditorState>,
6877 observed: Rc<Cell<Option<(Bounds<Pixels>, Pixels)>>>,
6878 }
6879 impl Render for Panel {
6880 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
6881 self.observed.set(self.input.read(cx).cursor_layout());
6882 div().size_full().child(self.input.clone())
6883 }
6884 }
6885 struct Root(Entity<Panel>);
6886 impl Render for Root {
6887 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
6888 div().size_full().child(
6889 self.0
6890 .clone()
6891 .cached(gpui::StyleRefinement::default().size_full()),
6892 )
6893 }
6894 }
6895 cx.update(crate::init);
6896 let observed = Rc::new(Cell::new(None));
6897 let mut input = None;
6898 let window = cx.open_window(size(px(400.), px(100.)), |window, cx| {
6899 let state = cx.new(|cx| {
6900 let mut state = crate::input::EditorState::new(window, cx);
6901 state.set_value("abcdefgh", window, cx);
6902 state
6903 });
6904 input = Some(state.clone());
6905 Root(cx.new(|_| Panel {
6906 input: state,
6907 observed: observed.clone(),
6908 }))
6909 });
6910 cx.run_until_parked();
6911 let input = input.unwrap();
6912 let before = input.read_with(cx, |state, _| state.cursor_layout());
6913 input.update(cx, |_, cx| cx.notify());
6918 cx.run_until_parked();
6919 assert_eq!(observed.get(), before);
6920 window
6921 .update(cx, |_, _, cx| {
6922 input.update(cx, |state, cx| state.select_to_with_affinity(3, false, cx));
6923 })
6924 .unwrap();
6925 cx.run_until_parked();
6926 window.update(cx, |_, _, cx| cx.notify()).unwrap();
6927 let after = input.read_with(cx, |state, _| state.cursor_layout());
6928 assert_ne!(after, before, "the caret must actually move");
6929 assert_eq!(
6930 observed.get(),
6931 after,
6932 "render consumers must receive the newly painted caret geometry"
6933 );
6934 }
6935
6936 #[gpui::test]
6937 fn test_input_scroll_offset_notifies_only_on_change(cx: &mut TestAppContext) {
6938 use std::{cell::Cell, rc::Rc};
6939
6940 cx.update(crate::init);
6941 let mut input = None;
6942 let _window = cx.open_window(size(px(400.), px(100.)), |window, cx| {
6943 input = Some(cx.new(|cx| crate::input::InputState::new(window, cx)));
6944 gpui::EmptyView
6945 });
6946 let input = input.unwrap();
6947 let notifications = Rc::new(Cell::new(0));
6948 let count = notifications.clone();
6949 let _subscription =
6950 cx.update(|cx| cx.observe(&input, move |_, _| count.set(count.get() + 1)));
6951
6952 input.update(cx, |state, _| {
6953 state.input_bounds = Bounds::new(Point::default(), size(px(100.), px(20.)));
6954 state.scroll_size = size(px(300.), px(20.));
6955 });
6956 for target in [
6957 None,
6958 Some(point(px(0.), px(0.))),
6959 Some(point(px(20.), px(50.))),
6960 ] {
6961 input.update(cx, |state, cx| state.update_scroll_offset(target, cx));
6962 assert_eq!(
6963 notifications.get(),
6964 0,
6965 "an unchanged clamped offset must stay quiet"
6966 );
6967 }
6968 input.update(cx, |state, cx| {
6969 state.update_scroll_offset(Some(point(px(-40.), px(0.))), cx);
6970 assert_eq!(state.scroll_handle.offset(), point(px(-40.), px(0.)));
6971 });
6972 assert_eq!(notifications.get(), 1, "a scroll must notify");
6973 input.update(cx, |state, cx| {
6974 state.update_scroll_offset(Some(point(px(-400.), px(50.))), cx);
6975 assert_eq!(state.scroll_handle.offset(), point(px(-200.), px(0.)));
6976 });
6977 assert_eq!(notifications.get(), 2);
6978 input.update(cx, |state, cx| {
6979 state.update_scroll_offset(Some(point(px(-500.), px(0.))), cx);
6980 });
6981 assert_eq!(
6982 notifications.get(),
6983 2,
6984 "clamping to the current offset must stay quiet"
6985 );
6986 input.update(cx, |state, cx| {
6987 state.scroll_size.width = px(110.);
6988 state.update_scroll_offset(None, cx);
6989 assert_eq!(state.scroll_handle.offset(), point(px(-10.), px(0.)));
6990 });
6991 assert_eq!(
6992 notifications.get(),
6993 3,
6994 "a smaller scroll range must clamp and notify"
6995 );
6996 }
6997
6998 #[gpui::test]
6999 fn test_set_line_number_notifies_only_on_change(cx: &mut TestAppContext) {
7000 use std::{cell::Cell, rc::Rc};
7001
7002 cx.update(crate::init);
7003 let mut input = None;
7004 let window = cx.open_window(size(px(400.), px(100.)), |window, cx| {
7005 input = Some(cx.new(|cx| crate::input::EditorState::new(window, cx)));
7006 gpui::EmptyView
7007 });
7008 let input = input.unwrap();
7009 let shown = input.read_with(cx, |state, _| state.mode.line_number());
7010 let notifications = Rc::new(Cell::new(0));
7011 let count = notifications.clone();
7012 let _subscription =
7013 cx.update(|cx| cx.observe(&input, move |_, _| count.set(count.get() + 1)));
7014
7015 window
7016 .update(cx, |_, window, cx| {
7017 input.update(cx, |state, cx| state.set_line_number(shown, window, cx))
7018 })
7019 .unwrap();
7020 assert_eq!(notifications.get(), 0);
7021
7022 window
7023 .update(cx, |_, window, cx| {
7024 input.update(cx, |state, cx| state.set_line_number(!shown, window, cx))
7025 })
7026 .unwrap();
7027 assert_eq!(notifications.get(), 1);
7028 assert_eq!(
7029 input.read_with(cx, |state, _| state.mode.line_number()),
7030 !shown
7031 );
7032 }
7033
7034 #[gpui::test]
7035 fn test_set_value_on_unfocused_input_stays_quiet(cx: &mut TestAppContext) {
7036 use std::{cell::Cell, rc::Rc};
7037
7038 cx.update(crate::init);
7039 let mut input = None;
7040 let window = cx.open_window(size(px(400.), px(100.)), |window, cx| {
7041 input = Some(cx.new(|cx| crate::input::InputState::new(window, cx)));
7042 gpui::EmptyView
7043 });
7044 let input = input.unwrap();
7045 cx.run_until_parked();
7046
7047 let notifications = Rc::new(Cell::new(0));
7048 let count = notifications.clone();
7049 let _subscription =
7050 cx.update(|cx| cx.observe(&input, move |_, _| count.set(count.get() + 1)));
7051 cx.run_until_parked();
7052
7053 window
7056 .update(cx, |_, window, cx| {
7057 input.update(cx, |state, cx| state.set_value("seeded", window, cx));
7058 })
7059 .unwrap();
7060 cx.run_until_parked();
7061 let settled = notifications.get();
7062
7063 cx.executor()
7064 .advance_clock(std::time::Duration::from_secs(3));
7065 cx.run_until_parked();
7066
7067 assert_eq!(
7068 notifications.get(),
7069 settled,
7070 "an unfocused input is blinking, and every blink repaints the view it is in"
7071 );
7072 }
7073
7074 #[gpui::test]
7075 fn test_input_does_not_invalidate_cached_parent_during_paint(cx: &mut TestAppContext) {
7076 use std::{cell::Cell, rc::Rc};
7077
7078 struct Panel {
7079 input: Entity<crate::input::InputState>,
7080 renders: Rc<Cell<usize>>,
7081 }
7082 impl Render for Panel {
7083 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
7084 self.renders.set(self.renders.get() + 1);
7085 div().size_full().child(self.input.clone())
7086 }
7087 }
7088 struct Root(Entity<Panel>);
7089 impl Render for Root {
7090 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
7091 div().size_full().child(
7092 self.0
7093 .clone()
7094 .cached(gpui::StyleRefinement::default().size_full()),
7095 )
7096 }
7097 }
7098
7099 cx.update(crate::init);
7100 let renders = Rc::new(Cell::new(0));
7101 let mut input = None;
7102 let window = cx.open_window(size(px(400.), px(100.)), |window, cx| {
7103 let state = cx.new(|cx| crate::input::InputState::new(window, cx));
7104 input = Some(state.clone());
7105 Root(cx.new(|_| Panel {
7106 input: state,
7107 renders: renders.clone(),
7108 }))
7109 });
7110 cx.run_until_parked();
7111 let before = renders.get();
7112 assert!(before > 0);
7113 for _ in 0..60 {
7114 window.update(cx, |_, _, cx| cx.notify()).unwrap();
7115 }
7116 assert_eq!(
7117 renders.get(),
7118 before,
7119 "an idle input must not invalidate its cached parent"
7120 );
7121
7122 let input = input.unwrap();
7123 window
7124 .update(cx, |_, window, cx| {
7125 input.update(cx, |state, cx| state.set_value("changed", window, cx));
7126 })
7127 .unwrap();
7128 assert!(
7129 renders.get() > before,
7130 "text changes must invalidate the parent"
7131 );
7132 window.update(cx, |_, _, cx| cx.notify()).unwrap();
7134 let before = renders.get();
7135 for _ in 0..60 {
7136 window.update(cx, |_, _, cx| cx.notify()).unwrap();
7137 }
7138 assert_eq!(renders.get(), before, "the edited input must settle again");
7139
7140 cx.simulate_window_resize(window.into(), size(px(240.), px(100.)));
7141 cx.run_until_parked();
7142 assert!(renders.get() > before, "resizing must invalidate layout");
7143 }
7144
7145 #[gpui::test]
7146 fn textarea_cursor_treats_crlf_as_one_newline(cx: &mut TestAppContext) {
7147 cx.update(crate::init);
7148 let view = InputView::build_textarea(cx, |state| state);
7149 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7150 cx.update(|window, cx| {
7151 view.input.update(cx, |state, cx| {
7152 for original in ["\r\nlast", "\u{feff}first\r\nlast\n", "first\nlast\r\n"] {
7153 state.set_value(original, window, cx);
7154 let newline = original.find('\n').unwrap();
7155 let end = if original.as_bytes()[newline.saturating_sub(1)] == b'\r' {
7156 newline - 1
7157 } else {
7158 newline
7159 };
7160 state.end(&MoveEnd, window, cx);
7161 assert_eq!(state.cursor(), end);
7162 state.right(&MoveRight, window, cx);
7163 assert_eq!(state.cursor(), newline + 1);
7164 state.left(&MoveLeft, window, cx);
7165 assert_eq!(state.cursor(), end);
7166 state.select_to_end_of_line(&SelectToEndOfLine, window, cx);
7167 assert_eq!(state.selected_range(), end..end);
7168 state.move_to_end(&MoveToEnd, window, cx);
7169 assert_eq!(state.cursor(), original.len());
7170 state.move_to_start(&MoveToStart, window, cx);
7171 assert_eq!(state.cursor(), 0);
7172 assert_eq!(state.value().as_bytes(), original.as_bytes());
7173 }
7174 state.set_value("a\rb", window, cx);
7176 state.right(&MoveRight, window, cx);
7177 assert_eq!(state.cursor(), 1);
7178 state.right(&MoveRight, window, cx);
7179 assert_eq!(state.cursor(), 2);
7180 })
7181 });
7182 }
7183
7184 #[gpui::test]
7185 fn only_a_multi_line_input_paints_scrollbars(cx: &mut TestAppContext) {
7186 cx.update(crate::init);
7187
7188 let single = InputView::build(cx, |state| state);
7192 single
7193 .input
7194 .update(cx, |state, _| assert!(!state.shows_scrollbar()));
7195
7196 let multi = InputView::build_textarea(cx, |state| state);
7197 multi
7198 .input
7199 .update(cx, |state, _| assert!(state.shows_scrollbar()));
7200 }
7201
7202 #[gpui::test]
7203 fn context_menu_handler_is_deferred_and_respects_disabled(cx: &mut TestAppContext) {
7204 use std::{cell::Cell, rc::Rc};
7205 cx.update(crate::init);
7206 let input_view = InputView::new(cx);
7207 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7208 let input = input_view.input;
7209 let calls = Rc::new(Cell::new(0usize));
7210 let items = Rc::new(Cell::new(0usize));
7211 let menu_focus = cx.update(|_, cx| cx.focus_handle());
7212
7213 cx.update(|window, cx| {
7214 input.update(cx, |state, cx| {
7215 let calls2 = calls.clone();
7216 let items2 = items.clone();
7217 let menu_focus = menu_focus.clone();
7218 state.on_context_menu(Rc::new(move |menu, _, _, window, cx| {
7219 calls2.set(calls2.get() + 1);
7220 items2.set(menu.items.len());
7221 menu_focus.focus(window, cx);
7222 }));
7223 state.handle_right_click_menu(point(px(0.), px(0.)), 0, window, cx);
7224 })
7225 });
7226 assert_eq!(calls.get(), 1);
7227 assert_eq!(items.get(), 0);
7228 input.read_with(&cx, |state, _| {
7229 assert_eq!(
7230 state
7231 .selection_focus
7232 .as_ref()
7233 .and_then(WeakFocusHandle::upgrade)
7234 .as_ref(),
7235 Some(&menu_focus),
7236 "the menu callback must retain selection without reborrowing its input"
7237 );
7238 });
7239
7240 cx.update(|window, cx| {
7242 input.update(cx, |state, cx| {
7243 state.handle_right_click_menu(point(px(0.), px(0.)), 0, window, cx);
7244 });
7245 });
7246 assert_eq!(calls.get(), 2);
7247 cx.update(|window, cx| {
7248 assert!(input.read(cx).has_selection_focus(window, cx));
7249 });
7250
7251 cx.update(|window, cx| {
7252 input.update(cx, |state, cx| {
7253 state.disabled = true;
7254 state.handle_right_click_menu(point(px(0.), px(0.)), 0, window, cx);
7255 })
7256 });
7257 assert_eq!(calls.get(), 2);
7258 }
7259
7260 #[gpui::test]
7261 fn context_menu_handler_can_read_its_input(cx: &mut TestAppContext) {
7262 use std::{cell::Cell, rc::Rc};
7263 cx.update(crate::init);
7264 let input_view = InputView::new(cx);
7265 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7266 let input = input_view.input;
7267 let cursor = Rc::new(Cell::new(None));
7268
7269 cx.update(|window, cx| {
7270 input.update(cx, |state, cx| {
7271 state.set_value("hello", window, cx);
7272 let weak = cx.weak_entity();
7273 let cursor = cursor.clone();
7274 state.on_context_menu(Rc::new(move |_, _, _, _, cx| {
7275 cursor.set(weak.upgrade().map(|input| input.read(cx).cursor()));
7276 }));
7277 state.handle_right_click_menu(point(px(0.), px(0.)), 2, window, cx);
7278 })
7279 });
7280 assert_eq!(cursor.get(), Some(2));
7281 }
7282
7283 #[gpui::test]
7284 fn test_readonly_rejects_user_edits_only(cx: &mut TestAppContext) {
7285 let input_view = InputView::new(cx);
7286 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7287 let input = input_view.input;
7288
7289 cx.update(|window, cx| {
7290 input.update(cx, |state, cx| {
7291 state.set_value("hello", window, cx);
7292 state.set_readonly(true, cx);
7293 });
7294 });
7295
7296 cx.update(|_, cx| {
7297 input.read_with(cx, |state, _| {
7298 assert!(!state.is_editable());
7299 assert!(!state.is_replaceable());
7300 });
7301 });
7302
7303 cx.update(|window, cx| {
7305 input.update(cx, |state, cx| {
7306 state.replace_text_in_range(None, " world", window, cx);
7307 state.replace_and_mark_text_in_range(None, "あ", None, window, cx);
7308 });
7309 });
7310 cx.update(|_, cx| {
7311 input.read_with(cx, |state, _| assert_eq!(state.value(), "hello"));
7312 });
7313
7314 cx.update(|window, cx| {
7316 input.update(cx, |state, cx| {
7317 state.insert(" world", window, cx);
7318 state.set_value("changed", window, cx);
7319 });
7320 });
7321 cx.update(|_, cx| {
7322 input.read_with(cx, |state, _| assert_eq!(state.value(), "changed"));
7323 });
7324
7325 cx.update(|window, cx| {
7328 input.update(cx, |state, cx| {
7329 state.set_readonly(false, cx);
7330 state.replace_text_in_range(None, "!", window, cx);
7331 });
7332 });
7333 cx.update(|_, cx| {
7334 input.read_with(cx, |state, _| {
7335 assert!(state.is_editable());
7336 assert_eq!(state.value(), "!changed");
7337 });
7338 });
7339 }
7340
7341 #[gpui::test]
7346 fn test_edit_reveals_far_offscreen_caret(cx: &mut TestAppContext) {
7347 let mut input = None;
7348 let window = cx.open_window(size(px(720.), px(400.)), |window, cx| {
7349 cx.set_global(Theme::default());
7350 super::super::init(cx);
7351 let state = cx.new(|cx| crate::input::TextareaState::new(window, cx).auto_grow(1, 6));
7352 input = Some(state.clone());
7353 TestRoot(state)
7354 });
7355 let mut cx = VisualTestContext::from_window(window.into(), cx);
7356 let input = input.unwrap();
7357
7358 let text: String = (1..=100)
7359 .map(|i| format!("line {i}"))
7360 .collect::<Vec<_>>()
7361 .join("\n");
7362 cx.update(|window, cx| {
7363 input.update(cx, |state, cx| {
7364 state.set_value(text, window, cx);
7365 let end = state.text.len();
7366 state.set_selection(end, end);
7367 });
7368 });
7369 cx.run_until_parked();
7370
7371 cx.update(|_, cx| {
7373 input.update(cx, |state, cx| {
7374 state.scroll_handle.set_offset(point(px(0.), px(0.)));
7375 cx.notify();
7376 });
7377 });
7378 cx.run_until_parked();
7379
7380 cx.update(|window, cx| {
7381 input.update(cx, |state, cx| {
7382 state.replace_text_in_range(None, "X", window, cx);
7383 });
7384 });
7385 cx.run_until_parked();
7386
7387 cx.update(|_, cx| {
7388 input.read_with(cx, |state, _| {
7389 assert!(state.value().ends_with("line 100X"));
7390 let (caret, _) = state.cursor_layout().expect("caret painted");
7391 let viewport = state.input_bounds();
7392 let scroll_y = state.scroll_handle.offset().y;
7393 let top = caret.origin.y - viewport.origin.y + scroll_y;
7394 assert!(
7395 top >= px(0.) && top + caret.size.height <= viewport.size.height,
7396 "caret top {top:?} outside viewport height {:?} (scroll {scroll_y:?})",
7397 viewport.size.height,
7398 );
7399 });
7400 });
7401 }
7402
7403 #[gpui::test]
7407 fn test_set_auto_grow_keeps_rows_fitted(cx: &mut TestAppContext) {
7408 let mut input = None;
7409 let window = cx.open_window(size(px(720.), px(400.)), |window, cx| {
7410 cx.set_global(Theme::default());
7411 super::super::init(cx);
7412 let state = cx.new(|cx| crate::input::TextareaState::new(window, cx).auto_grow(1, 10));
7413 input = Some(state.clone());
7414 TestRoot(state)
7415 });
7416 let mut cx = VisualTestContext::from_window(window.into(), cx);
7417 let input = input.unwrap();
7418
7419 let text: String = (1..=8)
7420 .map(|i| format!("line {i}"))
7421 .collect::<Vec<_>>()
7422 .join("\n");
7423 cx.update(|window, cx| {
7424 input.update(cx, |state, cx| state.set_value(text, window, cx));
7425 });
7426 cx.run_until_parked();
7427 cx.update(|_, cx| input.read_with(cx, |state, _| assert_eq!(state.mode.rows(), 8)));
7428
7429 cx.update(|_, cx| input.update(cx, |state, cx| state.set_auto_grow(1, 12, cx)));
7431 cx.run_until_parked();
7432 cx.update(|_, cx| input.read_with(cx, |state, _| assert_eq!(state.mode.rows(), 8)));
7433
7434 cx.update(|_, cx| input.update(cx, |state, cx| state.set_auto_grow(1, 4, cx)));
7436 cx.run_until_parked();
7437 cx.update(|_, cx| input.read_with(cx, |state, _| assert_eq!(state.mode.rows(), 4)));
7438 }
7439
7440 #[gpui::test]
7446 fn test_scroll_to_eob_does_not_overshoot_safe_range(cx: &mut TestAppContext) {
7447 let input_view = InputView::new(cx);
7448 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7449 let input = input_view.input;
7450
7451 cx.update(|window, cx| {
7453 input.update(cx, |state, cx| {
7454 state.set_scroll_beyond_last_line(Some(1), window, cx);
7455 state.set_cursor_surrounding_lines(Some(1), window, cx);
7456 let text: String = (1..=50)
7457 .map(|i| format!("line {i}"))
7458 .collect::<Vec<_>>()
7459 .join("\n");
7460 state.set_value(text, window, cx);
7461 });
7462 });
7463 cx.run_until_parked();
7464
7465 cx.update(|_, cx| {
7468 input.read_with(cx, |state, _| {
7469 assert!(
7470 state.scroll_size.height > px(0.),
7471 "scroll_size not populated by initial paint"
7472 );
7473 assert!(
7474 state.input_bounds.size.height > px(0.),
7475 "input_bounds not populated by initial paint"
7476 );
7477 });
7478 });
7479
7480 cx.update(|_, cx| {
7485 input.update(cx, |state, cx| {
7486 let end = state.text.len();
7487 state.move_to(end, Some(MoveDirection::Down), cx);
7488
7489 let deferred = state
7490 .deferred_scroll_offset
7491 .expect("scroll_to should populate deferred_scroll_offset");
7492 let safe_y_min =
7493 (-state.scroll_size.height + state.input_bounds.size.height).min(px(0.));
7494
7495 assert!(
7496 deferred.y >= safe_y_min,
7497 "deferred_scroll_offset.y = {:?} below safe_y_min = {:?} \
7498 — paint would jitter (Bug C regression)",
7499 deferred.y,
7500 safe_y_min,
7501 );
7502 });
7503 });
7504 }
7505
7506 #[gpui::test]
7507 fn test_next_search_match_reveals_with_padding_after_manual_scroll(cx: &mut TestAppContext) {
7508 assert_search_reveals_with_padding_after_manual_scroll(false, cx);
7509 }
7510
7511 #[gpui::test]
7512 fn test_previous_search_match_reveals_with_padding_after_manual_scroll(
7513 cx: &mut TestAppContext,
7514 ) {
7515 assert_search_reveals_with_padding_after_manual_scroll(true, cx);
7516 }
7517
7518 fn assert_search_reveals_with_padding_after_manual_scroll(
7519 previous: bool,
7520 cx: &mut TestAppContext,
7521 ) {
7522 let input_view = InputView::new(cx);
7523 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7524 let input = input_view.input;
7525 let text = (0..160)
7526 .map(|row| {
7527 if matches!(row, 20 | 60 | 100) {
7528 format!("match on row {row}")
7529 } else {
7530 format!("line {row}")
7531 }
7532 })
7533 .collect::<Vec<_>>()
7534 .join("\n");
7535 let start = text.match_indices("match").nth(1).unwrap().0;
7536 let expected_match = start..start + "match".len();
7537 cx.update(|window, cx| {
7538 input.update(cx, |state, cx| {
7539 state.set_cursor_surrounding_lines(Some(3), window, cx);
7540 state.set_value(text, window, cx);
7541 state.set_search_query("match", true, cx);
7542 if previous {
7543 state.search_session.matcher.next();
7544 state.search_session.matcher.next();
7545 }
7546 });
7547 });
7548 cx.run_until_parked();
7549 cx.update(|_, cx| {
7550 input.update(cx, |state, cx| {
7551 let line_height = state.last_layout.as_ref().unwrap().line_height;
7552 let y = if previous { px(0.) } else { -line_height * 80. };
7553 state.set_scroll_offset(point(px(0.), y), cx);
7554 });
7555 });
7556 cx.run_until_parked();
7557 input.read_with(&cx, |state, _| {
7558 let visible = state.visible_row_range().unwrap();
7559 if previous {
7560 assert!(visible.end <= 60, "target must be below the viewport");
7561 } else {
7562 assert!(visible.start > 60, "target must be above the viewport");
7563 }
7564 });
7565 cx.update(|_, cx| {
7566 input.update(cx, |state, cx| {
7567 let range = if previous {
7568 state.previous_search_match(cx)
7569 } else {
7570 state.next_search_match(cx)
7571 };
7572 assert_eq!(range, Some(expected_match));
7573 assert_eq!(state.search_session.matcher.label(), "2/3");
7574 });
7575 });
7576 cx.run_until_parked();
7577 input.read_with(&cx, |state, _| {
7578 assert!(state.visible_row_range().unwrap().contains(&60));
7579 let line_height = state.last_layout.as_ref().unwrap().line_height;
7580 let target_y = line_height * 60. + state.scroll_handle.offset().y;
7581 assert!(target_y >= line_height * 2. - px(0.1));
7583 assert!(
7584 target_y + line_height * 3.
7585 <= state.last_bounds.as_ref().unwrap().size.height + px(0.1),
7586 "search must preserve the configured surrounding-line padding"
7587 );
7588 });
7589 }
7590
7591 struct SearchHost {
7593 editor: Entity<InputBaseState<EditorMode>>,
7594 search_requests: Rc<Cell<usize>>,
7595 }
7596
7597 impl Render for SearchHost {
7598 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
7599 let search_requests = self.search_requests.clone();
7600 div()
7601 .size_full()
7602 .on_action(cx.listener(move |_, _: &Search, _, _| {
7603 search_requests.set(search_requests.get() + 1);
7604 }))
7605 .child(self.editor.clone())
7606 }
7607 }
7608
7609 fn press_search_shortcut(
7612 cx: &mut TestAppContext,
7613 searchable: bool,
7614 ) -> (Entity<InputBaseState<EditorMode>>, Rc<Cell<usize>>) {
7615 let search_requests = Rc::new(Cell::new(0));
7616 let mut editor = None;
7617 let window = cx.update(|cx| {
7618 cx.open_window(Default::default(), |window, cx| {
7619 cx.set_global(Theme::default());
7620 super::super::init(cx);
7621 let state =
7622 cx.new(|cx| crate::input::EditorState::new(window, cx).searchable(searchable));
7623 editor = Some(state.clone());
7624 cx.new(|_| SearchHost {
7625 editor: state,
7626 search_requests: search_requests.clone(),
7627 })
7628 })
7629 .unwrap()
7630 });
7631 let editor = editor.unwrap();
7632 let mut cx = VisualTestContext::from_window(window.into(), cx);
7633 cx.update(|window, cx| {
7634 editor.update(cx, |state, cx| state.focus(window, cx));
7635 });
7636 cx.run_until_parked();
7637 #[cfg(target_os = "macos")]
7638 cx.simulate_keystrokes("cmd-f");
7639 #[cfg(not(target_os = "macos"))]
7640 cx.simulate_keystrokes("ctrl-f");
7641 cx.run_until_parked();
7642 (editor, search_requests)
7643 }
7644
7645 #[gpui::test]
7646 fn test_search_shortcut_reaches_the_host_when_not_searchable(cx: &mut TestAppContext) {
7647 let (editor, search_requests) = press_search_shortcut(cx, false);
7648 assert_eq!(search_requests.get(), 1);
7649 editor.read_with(cx, |state, _| {
7650 assert!(!state.search_session().open);
7651 assert!(!state.search_session().is_active());
7652 });
7653 }
7654
7655 #[gpui::test]
7656 fn test_search_shortcut_opens_the_panel_when_searchable(cx: &mut TestAppContext) {
7657 let (editor, search_requests) = press_search_shortcut(cx, true);
7658 assert_eq!(search_requests.get(), 0);
7659 editor.read_with(cx, |state, _| {
7660 assert!(state.search_session().open);
7661 assert!(state.search_session().is_active());
7662 });
7663 }
7664
7665 #[gpui::test]
7666 fn test_set_search_query_highlights_without_the_panel(cx: &mut TestAppContext) {
7667 let input_view = InputView::build_editor(cx, |state| state.searchable(false));
7668 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7669 let input = input_view.input;
7670 cx.update(|window, cx| {
7671 input.update(cx, |state, cx| {
7672 state.set_value("foo bar foo", window, cx);
7673 state.set_search_query("foo", true, cx);
7674 });
7675 });
7676 cx.run_until_parked();
7677 input.read_with(&cx, |state, _| {
7678 let session = state.search_session();
7679 assert!(session.is_active());
7680 assert!(!session.open);
7681 assert_eq!(session.matcher.len(), 2);
7682 });
7683 cx.update(|_, cx| {
7684 input.update(cx, |state, cx| state.close_search(cx));
7685 });
7686 input.read_with(&cx, |state, _| {
7687 assert!(!state.search_session().is_active());
7688 });
7689 }
7690
7691 #[gpui::test]
7694 fn test_closed_search_resyncs_matches_after_edits(cx: &mut TestAppContext) {
7695 let input_view = InputView::build_editor(cx, |state| state.searchable(false));
7696 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7697 let input = input_view.input;
7698 cx.update(|window, cx| {
7699 input.update(cx, |state, cx| {
7700 state.set_value("foo bar foo", window, cx);
7701 state.set_search_query("foo", true, cx);
7702 state.close_search(cx);
7703
7704 state.set_value("bar foo", window, cx);
7705 assert_eq!(state.next_search_match(cx), Some(4..7));
7706
7707 state.set_value("foo foo foo", window, cx);
7708 state.set_search_query("foo", true, cx);
7709 assert_eq!(state.search_session().matcher.len(), 3);
7710 });
7711 });
7712 }
7713
7714 #[gpui::test]
7715 fn test_search_reveals_offscreen_wrapped_match(cx: &mut TestAppContext) {
7716 let input_view = InputView::new(cx);
7717 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7718 let input = input_view.input;
7719 let text = format!(
7720 "match\n{}\n{}match\n{}",
7721 "line\n".repeat(80),
7722 "wrapped text ".repeat(500),
7723 "line\n".repeat(80)
7724 );
7725 cx.update(|window, cx| {
7726 input.update(cx, |state, cx| {
7727 state.set_cursor_surrounding_lines(Some(3), window, cx);
7728 state.set_value(text, window, cx);
7729 state.set_search_query("match", true, cx);
7730 });
7731 });
7732 cx.run_until_parked();
7733 cx.update(|_, cx| {
7734 input.update(cx, |state, cx| {
7735 state.next_search_match(cx).unwrap();
7736 });
7737 });
7738 cx.run_until_parked();
7739 input.read_with(&cx, |state, _| {
7740 let range = state.search_session.matcher.matched_ranges()[1].clone();
7741 let layout = state.last_layout.as_ref().unwrap();
7742 let (_, _, position) = state.line_and_position_for_offset(range.end);
7743 let y = position.expect("wrapped match must be laid out").y
7744 + state.scroll_handle.offset().y;
7745 assert!(y >= layout.line_height * 2. - px(0.1));
7746 assert!(
7747 y + layout.line_height * 3. <= state.last_bounds.unwrap().size.height + px(0.1),
7748 "wrapped match must retain surrounding display rows"
7749 );
7750 });
7751 }
7752
7753 #[gpui::test]
7754 fn test_number_step(cx: &mut TestAppContext) {
7755 let input = InputView::build(cx, |state| state).input;
7756
7757 cx.update(|cx| {
7758 input.update(cx, |_state, cx| {
7759 assert_eq!(
7760 NumberStep::from(5.).value(123., StepAction::Increment, cx),
7761 5.
7762 );
7763
7764 let step = NumberStep::by_value(|value, action, _cx| {
7767 let below = match action {
7768 StepAction::Increment => value < 1.0,
7769 StepAction::Decrement => value <= 1.0,
7770 };
7771 if below { 0.1 } else { 0.5 }
7772 });
7773 assert_eq!(step.value(0.5, StepAction::Increment, cx), 0.1);
7774 assert_eq!(step.value(1.0, StepAction::Increment, cx), 0.5);
7775 assert_eq!(step.value(1.0, StepAction::Decrement, cx), 0.1);
7776 assert_eq!(step.value(2.0, StepAction::Decrement, cx), 0.5);
7777 });
7778 });
7779 }
7780
7781 #[gpui::test]
7782 fn test_number_input_normalization(cx: &mut TestAppContext) {
7783 let input_view = InputView::build(cx, |state| {
7784 state.mask_pattern(MaskPattern::Number {
7785 separator: None,
7786 fraction: None,
7787 })
7788 });
7789 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7790 let input = input_view.input;
7791
7792 cx.update(|window, cx| {
7796 input.update(cx, |state, cx| {
7797 state.replace_text_in_range(None, "12。5", window, cx);
7798 });
7799 });
7800 cx.run_until_parked();
7801 cx.update(|_, cx| {
7802 input.read_with(cx, |state, _| {
7803 assert_eq!(state.value(), "12.5");
7804 let cursor: Range<usize> = state.selected_range();
7805 assert_eq!(cursor, 4..4);
7806 });
7807 });
7808
7809 cx.update(|window, cx| {
7811 input.update(cx, |state, cx| {
7812 state.replace_text_in_range(None, "abc", window, cx);
7813 });
7814 });
7815 cx.run_until_parked();
7816 cx.update(|_, cx| {
7817 input.read_with(cx, |state, _| {
7818 assert_eq!(state.value(), "12.5");
7819 });
7820 });
7821
7822 cx.update(|window, cx| {
7825 input.update(cx, |state, cx| {
7826 let range = state.range_to_utf16(&(0..state.text.len()));
7827 state.replace_text_in_range(Some(range), "。", window, cx);
7828 });
7829 });
7830 cx.run_until_parked();
7831 cx.update(|_, cx| {
7832 input.read_with(cx, |state, _| {
7833 assert_eq!(state.value(), ".");
7834 let cursor: Range<usize> = state.selected_range();
7835 assert_eq!(cursor, 1..1);
7836 });
7837 });
7838 }
7839
7840 #[gpui::test]
7841 fn test_mask_pattern_accepts_non_ascii_text(cx: &mut TestAppContext) {
7842 let input_view = InputView::build(cx, |state| state.mask_pattern("9999年99月"));
7843 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7844 let input = input_view.input;
7845
7846 cx.update(|window, cx| {
7847 input.update(cx, |state, cx| {
7848 state.replace_text_in_range(None, "2024年12月", window, cx);
7849 });
7850 });
7851 cx.run_until_parked();
7852 cx.update(|_, cx| {
7853 input.read_with(cx, |state, _| {
7854 assert_eq!(state.value(), "2024年12月");
7855 });
7856 });
7857 }
7858
7859 #[gpui::test]
7860 fn test_number_input_normalization_with_separator(cx: &mut TestAppContext) {
7861 let input_view = InputView::build(cx, |state| {
7862 state.mask_pattern(MaskPattern::Number {
7863 separator: Some(','),
7864 fraction: Some(2),
7865 })
7866 });
7867 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7868 let input = input_view.input;
7869
7870 cx.update(|window, cx| {
7871 input.update(cx, |state, cx| {
7872 state.replace_text_in_range(None, "1234", window, cx);
7873 });
7874 });
7875 cx.run_until_parked();
7876 cx.update(|_, cx| {
7877 input.read_with(cx, |state, _| {
7878 assert_eq!(state.value(), "1,234");
7879 assert_eq!(state.unmask_value(), "1234");
7880 });
7881 });
7882 }
7883
7884 #[gpui::test]
7885 fn test_number_input_clamp_on_blur(cx: &mut TestAppContext) {
7886 let input_view = InputView::build(cx, |state| {
7887 state
7888 .mask_pattern(MaskPattern::Number {
7889 separator: None,
7890 fraction: None,
7891 })
7892 .min(10.)
7893 .max(100.)
7894 });
7895 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7896 let input = input_view.input;
7897
7898 cx.update(|window, cx| {
7900 input.update(cx, |state, cx| {
7901 state.replace_text_in_range(None, "1000", window, cx);
7902 assert_eq!(state.value(), "1000");
7903 state.clamp_number_value(window, cx);
7904 assert_eq!(state.value(), "100");
7905
7906 let range = state.range_to_utf16(&(0..state.text.len()));
7907 state.replace_text_in_range(Some(range), "1", window, cx);
7908 assert_eq!(state.value(), "1");
7909 state.clamp_number_value(window, cx);
7910 assert_eq!(state.value(), "10");
7911 });
7912 });
7913 }
7914
7915 #[gpui::test]
7916 fn test_number_input_undo_with_mask(cx: &mut TestAppContext) {
7917 let input_view = InputView::build(cx, |state| {
7918 state.mask_pattern(MaskPattern::Number {
7919 separator: Some(','),
7920 fraction: None,
7921 })
7922 });
7923 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7924 let input = input_view.input;
7925
7926 cx.update(|window, cx| {
7929 input.update(cx, |state, cx| {
7930 state.replace_text_in_range(None, "1234", window, cx);
7931 assert_eq!(state.value(), "1,234");
7932 state.replace_text_in_range(None, "5", window, cx);
7933 assert_eq!(state.value(), "12,345");
7934
7935 state.undo(&Undo, window, cx);
7939 assert_eq!(state.value(), "1,234");
7940 state.undo(&Undo, window, cx);
7941 assert_eq!(state.value(), "");
7942 state.redo(&Redo, window, cx);
7943 assert_eq!(state.value(), "1,234");
7944 state.redo(&Redo, window, cx);
7945 assert_eq!(state.value(), "12,345");
7946 });
7947 });
7948 }
7949
7950 #[gpui::test]
7951 fn test_undo_manager_coalesces_adjacent_typing_transactions(cx: &mut TestAppContext) {
7952 let input_view = InputView::build(cx, |state| state);
7953 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7954 let input = input_view.input;
7955
7956 cx.update(|window, cx| {
7957 input.update(cx, |state, cx| {
7958 state.replace_text_in_range(None, "a", window, cx);
7959 state.replace_text_in_range(None, "b", window, cx);
7960 assert_eq!(state.value(), "ab");
7961
7962 state.undo(&Undo, window, cx);
7963 assert_eq!(state.value(), "");
7964 });
7965 });
7966 }
7967
7968 #[gpui::test]
7969 fn test_undo_manager_cursor_movement_splits_typing(cx: &mut TestAppContext) {
7970 let input_view = InputView::build(cx, |state| state);
7971 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7972 let input = input_view.input;
7973
7974 cx.update(|window, cx| {
7975 input.update(cx, |state, cx| {
7976 state.replace_text_in_range(None, "a", window, cx);
7977 state.replace_text_in_range(None, "b", window, cx);
7978 state.left(&MoveLeft, window, cx);
7979 state.replace_text_in_range(None, "x", window, cx);
7980 assert_eq!(state.value(), "axb");
7981
7982 state.undo(&Undo, window, cx);
7983 assert_eq!(state.value(), "ab");
7984 state.undo(&Undo, window, cx);
7985 assert_eq!(state.value(), "");
7986 });
7987 });
7988 }
7989
7990 #[gpui::test]
7991 fn test_undo_manager_splits_backward_and_forward_delete(cx: &mut TestAppContext) {
7992 let input_view = InputView::build(cx, |state| state);
7993 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7994 let input = input_view.input;
7995
7996 cx.update(|window, cx| {
7997 input.update(cx, |state, cx| {
7998 state.set_value("abcd", window, cx);
7999 state.set_selected_range(2..2, cx);
8000 state.backspace(&Backspace, window, cx);
8001 state.delete(&Delete, window, cx);
8002 assert_eq!(state.value(), "ad");
8003
8004 state.undo(&Undo, window, cx);
8005 assert_eq!(state.value(), "acd");
8006 state.undo(&Undo, window, cx);
8007 assert_eq!(state.value(), "abcd");
8008 });
8009 });
8010 }
8011
8012 #[gpui::test]
8013 fn test_undo_manager_coalesces_directional_character_deletes(cx: &mut TestAppContext) {
8014 let input_view = InputView::build(cx, |state| state);
8015 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8016 let input = input_view.input;
8017
8018 cx.update(|window, cx| {
8019 input.update(cx, |state, cx| {
8020 state.set_value("abcd", window, cx);
8021 state.backspace(&Backspace, window, cx);
8022 state.backspace(&Backspace, window, cx);
8023 assert_eq!(state.value(), "ab");
8024 state.undo(&Undo, window, cx);
8025 assert_eq!(state.value(), "abcd");
8026 assert_eq!(state.selected_range(), 4..4);
8027
8028 state.set_value("abcd", window, cx);
8029 state.set_selected_range(1..1, cx);
8030 state.delete(&Delete, window, cx);
8031 state.delete(&Delete, window, cx);
8032 assert_eq!(state.value(), "ad");
8033 state.undo(&Undo, window, cx);
8034 assert_eq!(state.value(), "abcd");
8035 assert_eq!(state.selected_range(), 1..1);
8036 });
8037 });
8038 }
8039
8040 #[gpui::test]
8041 fn test_undo_manager_atomic_paste_isolated_from_typing(cx: &mut TestAppContext) {
8042 let input_view = InputView::build(cx, |state| state);
8043 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8044 let input = input_view.input;
8045
8046 cx.update(|window, cx| {
8047 cx.write_to_clipboard(ClipboardItem::new_string("P".to_string()));
8048 input.update(cx, |state, cx| {
8049 state.replace_text_in_range(None, "a", window, cx);
8050 state.paste(&Paste, window, cx);
8051 state.replace_text_in_range(None, "b", window, cx);
8052 assert_eq!(state.value(), "aPb");
8053
8054 state.undo(&Undo, window, cx);
8055 assert_eq!(state.value(), "aP");
8056 state.undo(&Undo, window, cx);
8057 assert_eq!(state.value(), "a");
8058 state.undo(&Undo, window, cx);
8059 assert_eq!(state.value(), "");
8060 });
8061 });
8062 }
8063
8064 #[gpui::test]
8065 fn test_undo_manager_programmatic_insert_is_atomic(cx: &mut TestAppContext) {
8066 let input_view = InputView::build(cx, |state| state);
8067 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8068 let input = input_view.input;
8069
8070 cx.update(|window, cx| {
8071 input.update(cx, |state, cx| {
8072 state.replace_text_in_range(None, "a", window, cx);
8073 state.insert("P", window, cx);
8074 state.replace_text_in_range(None, "b", window, cx);
8075 assert_eq!(state.value(), "aPb");
8076
8077 state.undo(&Undo, window, cx);
8078 assert_eq!(state.value(), "aP");
8079 state.undo(&Undo, window, cx);
8080 assert_eq!(state.value(), "a");
8081 });
8082 });
8083 }
8084
8085 #[gpui::test]
8086 fn test_undo_manager_selection_round_trip_splits_typing(cx: &mut TestAppContext) {
8087 let input_view = InputView::build(cx, |state| state);
8088 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8089 let input = input_view.input;
8090
8091 cx.update(|window, cx| {
8092 input.update(cx, |state, cx| {
8093 state.replace_text_in_range(None, "a", window, cx);
8094 state.replace_text_in_range(None, "b", window, cx);
8095 state.select_all(window, cx);
8096 state.unselect(window, cx);
8097 state.replace_text_in_range(None, "c", window, cx);
8098
8099 state.undo(&Undo, window, cx);
8100 assert_eq!(state.value(), "ab");
8101 state.undo(&Undo, window, cx);
8102 assert_eq!(state.value(), "");
8103 });
8104 });
8105 }
8106
8107 #[gpui::test]
8108 fn test_undo_manager_enter_is_atomic(cx: &mut TestAppContext) {
8109 let input_view = InputView::build_textarea(cx, |state| state);
8110 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8111 let input = input_view.input;
8112
8113 cx.update(|window, cx| {
8114 input.update(cx, |state, cx| {
8115 state.replace_text_in_range(None, "a", window, cx);
8116 state.enter(
8117 &Enter {
8118 secondary: false,
8119 shift: false,
8120 },
8121 window,
8122 cx,
8123 );
8124 state.replace_text_in_range(None, "b", window, cx);
8125
8126 state.undo(&Undo, window, cx);
8127 assert_eq!(state.value(), "a\n");
8128 state.undo(&Undo, window, cx);
8129 assert_eq!(state.value(), "a");
8130 state.undo(&Undo, window, cx);
8131 assert_eq!(state.value(), "");
8132 });
8133 });
8134 }
8135
8136 #[gpui::test]
8137 fn test_undo_manager_single_line_return_commits_the_typing_session(cx: &mut TestAppContext) {
8138 let input_view = InputView::build(cx, |state| state);
8139 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8140 let input = input_view.input;
8141
8142 cx.update(|window, cx| {
8143 input.update(cx, |state, cx| {
8144 for part in ["a", "b", "c"] {
8145 state.replace_text_in_range(None, part, window, cx);
8146 }
8147 state.enter(
8148 &Enter {
8149 secondary: false,
8150 shift: false,
8151 },
8152 window,
8153 cx,
8154 );
8155 for part in ["d", "e", "f"] {
8156 state.replace_text_in_range(None, part, window, cx);
8157 }
8158 assert_eq!(state.value(), "abcdef");
8159
8160 state.undo(&Undo, window, cx);
8161 assert_eq!(state.value(), "abc");
8162 state.undo(&Undo, window, cx);
8163 assert_eq!(state.value(), "");
8164 });
8165 });
8166 }
8167
8168 #[gpui::test]
8169 fn test_undo_manager_submit_on_enter_commits_the_textarea_session(cx: &mut TestAppContext) {
8170 let input_view = InputView::build_textarea(cx, |state| state.submit_on_enter(true));
8171 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8172 let input = input_view.input;
8173
8174 cx.update(|window, cx| {
8175 input.update(cx, |state, cx| {
8176 state.replace_text_in_range(None, "before submit", window, cx);
8177 state.enter(
8178 &Enter {
8179 secondary: false,
8180 shift: false,
8181 },
8182 window,
8183 cx,
8184 );
8185 state.replace_text_in_range(None, " after submit", window, cx);
8186 assert_eq!(state.value(), "before submit after submit");
8187
8188 state.undo(&Undo, window, cx);
8189 assert_eq!(state.value(), "before submit");
8190 state.undo(&Undo, window, cx);
8191 assert_eq!(state.value(), "");
8192 });
8193 });
8194 }
8195
8196 #[gpui::test]
8197 fn test_undo_manager_blur_commits_the_typing_session(cx: &mut TestAppContext) {
8198 let input_view = InputView::build(cx, |state| state);
8199 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8200 let input = input_view.input;
8201
8202 cx.update(|window, cx| {
8203 input.update(cx, |state, cx| {
8204 state.replace_text_in_range(None, "before blur", window, cx);
8205 state.on_blur(window, cx);
8206 state.on_focus(window, cx);
8207 state.replace_text_in_range(None, " after focus", window, cx);
8208 assert_eq!(state.value(), "before blur after focus");
8209
8210 state.undo(&Undo, window, cx);
8211 assert_eq!(state.value(), "before blur");
8212 state.undo(&Undo, window, cx);
8213 assert_eq!(state.value(), "");
8214 });
8215 });
8216 }
8217
8218 #[gpui::test]
8219 fn test_undo_manager_keeps_rapid_lines_in_distinct_transactions(cx: &mut TestAppContext) {
8220 let input_view = InputView::build_textarea(cx, |state| state);
8221 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8222 let input = input_view.input;
8223
8224 cx.update(|window, cx| {
8225 input.update(cx, |state, cx| {
8226 let enter = Enter {
8227 secondary: false,
8228 shift: false,
8229 };
8230 state.replace_text_in_range(None, "a", window, cx);
8231 state.enter(&enter, window, cx);
8232 state.replace_text_in_range(None, "b", window, cx);
8233 state.enter(&enter, window, cx);
8234 state.replace_text_in_range(None, "c", window, cx);
8235 assert_eq!(state.value(), "a\nb\nc");
8236
8237 for expected in ["a\nb\n", "a\nb", "a\n", "a", ""] {
8238 state.undo(&Undo, window, cx);
8239 assert_eq!(state.value(), expected);
8240 }
8241 });
8242 });
8243 }
8244
8245 #[gpui::test]
8246 fn test_undo_manager_coalesces_long_unicode_typing_without_a_timer(cx: &mut TestAppContext) {
8247 let input_view = InputView::build_textarea(cx, |state| state);
8248 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8249 let input = input_view.input;
8250 let parts = [
8251 "The ",
8252 "quick ",
8253 "brown fox, ",
8254 "你好,世界 ",
8255 "🦀 jumps over 13 lazy dogs.",
8256 ];
8257 let expected = parts.concat();
8258
8259 cx.update(|window, cx| {
8260 input.update(cx, |state, cx| {
8261 for part in parts {
8262 state.replace_text_in_range(None, part, window, cx);
8263 }
8264 assert_eq!(state.value(), expected);
8265
8266 state.undo(&Undo, window, cx);
8267 assert_eq!(state.value(), "");
8268 state.redo(&Redo, window, cx);
8269 assert_eq!(state.value(), expected);
8270 });
8271 });
8272 }
8273
8274 #[gpui::test]
8275 fn test_undo_manager_long_multiline_sequence_has_structural_boundaries(
8276 cx: &mut TestAppContext,
8277 ) {
8278 let input_view = InputView::build_textarea(cx, |state| state);
8279 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8280 let input = input_view.input;
8281 let enter = Enter {
8282 secondary: false,
8283 shift: false,
8284 };
8285
8286 cx.update(|window, cx| {
8287 input.update(cx, |state, cx| {
8288 for (index, line) in [
8289 "first line with punctuation!",
8290 "第二行包含 Unicode 🦀",
8291 "third line has several words",
8292 ]
8293 .into_iter()
8294 .enumerate()
8295 {
8296 for chunk in line.split_inclusive(' ') {
8297 state.replace_text_in_range(None, chunk, window, cx);
8298 }
8299 if index < 2 {
8300 state.enter(&enter, window, cx);
8301 }
8302 }
8303
8304 assert_eq!(
8305 state.value(),
8306 "first line with punctuation!\n第二行包含 Unicode 🦀\nthird line has several words"
8307 );
8308 state.undo(&Undo, window, cx);
8309 assert_eq!(
8310 state.value(),
8311 "first line with punctuation!\n第二行包含 Unicode 🦀\n"
8312 );
8313 state.undo(&Undo, window, cx);
8314 assert_eq!(
8315 state.value(),
8316 "first line with punctuation!\n第二行包含 Unicode 🦀"
8317 );
8318 state.undo(&Undo, window, cx);
8319 assert_eq!(state.value(), "first line with punctuation!\n");
8320 });
8321 });
8322 }
8323
8324 #[gpui::test]
8325 fn test_masked_input_keeps_its_value_out_of_the_clipboard(cx: &mut TestAppContext) {
8326 let input_view = InputView::build(cx, |state| state);
8327 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8328 let input = input_view.input;
8329
8330 cx.update(|window, cx| {
8331 input.update(cx, |state, cx| {
8332 state.set_value("hunter2", window, cx);
8333 state.set_masked(true, window, cx);
8334 state.select_all(window, cx);
8335 cx.write_to_clipboard(ClipboardItem::new_string("sentinel".into()));
8336
8337 state.copy(&Copy, window, cx);
8338 assert_eq!(
8339 cx.read_from_clipboard().and_then(|item| item.text()),
8340 Some("sentinel".to_string())
8341 );
8342
8343 state.cut(&Cut, window, cx);
8345 assert_eq!(state.value(), "hunter2");
8346 assert_eq!(
8347 cx.read_from_clipboard().and_then(|item| item.text()),
8348 Some("sentinel".to_string())
8349 );
8350
8351 state.set_masked(false, window, cx);
8353 state.copy(&Copy, window, cx);
8354 assert_eq!(
8355 cx.read_from_clipboard().and_then(|item| item.text()),
8356 Some("hunter2".to_string())
8357 );
8358 });
8359 });
8360 }
8361
8362 #[gpui::test]
8363 fn test_masked_input_collapses_word_boundaries(cx: &mut TestAppContext) {
8364 let input_view = InputView::build(cx, |state| state);
8365 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8366 let input = input_view.input;
8367
8368 cx.update(|window, cx| {
8369 input.update(cx, |state, cx| {
8370 state.set_value("aaa bbb ccc", window, cx);
8371 state.set_masked(true, window, cx);
8372 state.set_selected_range(7..7, cx);
8373
8374 state.delete_previous_word(&DeleteToPreviousWordStart, window, cx);
8377 assert_eq!(state.value(), " ccc");
8378 assert_eq!(state.selected_range(), 0..0);
8379
8380 state.delete_next_word(&DeleteToNextWordEnd, window, cx);
8381 assert_eq!(state.value(), "");
8382
8383 state.set_value("aaa bbb ccc", window, cx);
8385 state.select_word(9, window, cx);
8386 assert_eq!(state.selected_range(), 0..11);
8387
8388 state.set_masked(false, window, cx);
8390 state.set_value("aaa bbb ccc", window, cx);
8391 state.set_selected_range(11..11, cx);
8392 state.delete_previous_word(&DeleteToPreviousWordStart, window, cx);
8393 assert_eq!(state.value(), "aaa bbb ");
8394
8395 state.set_value("aaa bbb ccc", window, cx);
8396 state.select_word(9, window, cx);
8397 assert_eq!(state.selected_range(), 8..11);
8398 });
8399 });
8400 }
8401
8402 #[gpui::test]
8403 fn test_masked_input_disables_the_copy_context_menu_items(cx: &mut TestAppContext) {
8404 let input_view = InputView::build(cx, |state| state);
8405 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8406 let input = input_view.input;
8407
8408 cx.update(|window, cx| {
8409 input.update(cx, |state, cx| {
8410 state.set_value("hunter2", window, cx);
8411 state.select_all(window, cx);
8412 assert!(state.context_menu_capabilities().is_copyable());
8413
8414 state.set_masked(true, window, cx);
8415 let capabilities = state.context_menu_capabilities();
8416 assert!(capabilities.is_masked());
8417 assert!(capabilities.has_selection());
8418 assert!(!capabilities.is_copyable());
8419 });
8420 });
8421 }
8422
8423 #[gpui::test]
8424 fn test_undo_manager_cut_and_repeated_pastes_are_distinct_transactions(
8425 cx: &mut TestAppContext,
8426 ) {
8427 let input_view = InputView::build(cx, |state| state);
8428 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8429 let input = input_view.input;
8430
8431 cx.update(|window, cx| {
8432 input.update(cx, |state, cx| {
8433 state.replace_text_in_range(None, "alpha beta gamma", window, cx);
8434 state.set_selected_range(6..10, cx);
8435 state.cut(&Cut, window, cx);
8436 assert_eq!(state.value(), "alpha gamma");
8437
8438 state.paste(&Paste, window, cx);
8439 state.paste(&Paste, window, cx);
8440 assert_eq!(state.value(), "alpha betabeta gamma");
8441
8442 state.undo(&Undo, window, cx);
8443 assert_eq!(state.value(), "alpha beta gamma");
8444 state.undo(&Undo, window, cx);
8445 assert_eq!(state.value(), "alpha gamma");
8446 state.undo(&Undo, window, cx);
8447 assert_eq!(state.value(), "alpha beta gamma");
8448 });
8449 });
8450 }
8451
8452 #[gpui::test]
8453 fn test_undo_manager_word_and_line_deletes_do_not_coalesce(cx: &mut TestAppContext) {
8454 let input_view = InputView::build_textarea(cx, |state| state);
8455 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8456 let input = input_view.input;
8457
8458 cx.update(|window, cx| {
8459 input.update(cx, |state, cx| {
8460 state.set_value("one two three\nfour five", window, cx);
8461 state.set_selected_range(13..13, cx);
8462 state.delete_previous_word(&DeleteToPreviousWordStart, window, cx);
8463 assert_eq!(state.value(), "one two \nfour five");
8464 state.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
8465 assert_eq!(state.value(), "one two four five");
8466
8467 state.undo(&Undo, window, cx);
8468 assert_eq!(state.value(), "one two \nfour five");
8469 state.undo(&Undo, window, cx);
8470 assert_eq!(state.value(), "one two three\nfour five");
8471 });
8472 });
8473 }
8474
8475 #[gpui::test]
8476 fn test_undo_manager_multiline_replacement_is_one_atomic_transaction(cx: &mut TestAppContext) {
8477 let input_view = InputView::build_textarea(cx, |state| state);
8478 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8479 let input = input_view.input;
8480
8481 cx.update(|window, cx| {
8482 input.update(cx, |state, cx| {
8483 state.replace_text_in_range(None, "before", window, cx);
8484 state.set_selected_range(0..6, cx);
8485 state.replace_text_in_range(None, "line one\nline two\n第三行", window, cx);
8486 assert_eq!(state.value(), "line one\nline two\n第三行");
8487
8488 state.undo(&Undo, window, cx);
8489 assert_eq!(state.value(), "before");
8490 assert_eq!(state.selected_range(), 0..6);
8491 state.redo(&Redo, window, cx);
8492 assert_eq!(state.value(), "line one\nline two\n第三行");
8493 });
8494 });
8495 }
8496
8497 #[gpui::test]
8498 fn test_undo_manager_composition_isolated_from_long_typing(cx: &mut TestAppContext) {
8499 let input_view = InputView::build(cx, |state| state);
8500 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8501 let input = input_view.input;
8502
8503 cx.update(|window, cx| {
8504 input.update(cx, |state, cx| {
8505 state.replace_text_in_range(None, "prefix ", window, cx);
8506 state.replace_and_mark_text_in_range(None, "n", None, window, cx);
8507 state.replace_and_mark_text_in_range(None, "ni", None, window, cx);
8508 state.replace_and_mark_text_in_range(None, "你", None, window, cx);
8509 state.unmark_text(window, cx);
8510 state.replace_text_in_range(None, " suffix", window, cx);
8511 assert_eq!(state.value(), "prefix 你 suffix");
8512
8513 state.undo(&Undo, window, cx);
8514 assert_eq!(state.value(), "prefix 你");
8515 state.undo(&Undo, window, cx);
8516 assert_eq!(state.value(), "prefix ");
8517 state.undo(&Undo, window, cx);
8518 assert_eq!(state.value(), "");
8519 });
8520 });
8521 }
8522
8523 #[gpui::test]
8524 fn test_undo_manager_selected_replacement_is_atomic(cx: &mut TestAppContext) {
8525 let input_view = InputView::build(cx, |state| state);
8526 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8527 let input = input_view.input;
8528
8529 cx.update(|window, cx| {
8530 input.update(cx, |state, cx| {
8531 state.replace_text_in_range(None, "abc", window, cx);
8532 state.set_selected_range(1..2, cx);
8533 state.replace_text_in_range(None, "X", window, cx);
8534 state.replace_text_in_range(None, "z", window, cx);
8535 assert_eq!(state.value(), "aXzc");
8536
8537 state.undo(&Undo, window, cx);
8538 assert_eq!(state.value(), "aXc");
8539 state.undo(&Undo, window, cx);
8540 assert_eq!(state.value(), "abc");
8541 state.undo(&Undo, window, cx);
8542 assert_eq!(state.value(), "");
8543 });
8544 });
8545 }
8546
8547 #[gpui::test]
8548 fn test_number_input_leading_dot_editable(cx: &mut TestAppContext) {
8549 let input_view = InputView::build(cx, |state| {
8550 state.mask_pattern(MaskPattern::Number {
8551 separator: None,
8552 fraction: None,
8553 })
8554 });
8555 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8556 let input = input_view.input;
8557
8558 cx.update(|window, cx| {
8559 input.update(cx, |state, cx| {
8560 state.replace_text_in_range(None, "1.2", window, cx);
8561
8562 let range = state.range_to_utf16(&(0..1));
8566 state.replace_text_in_range(Some(range), "", window, cx);
8567 assert_eq!(state.value(), ".2");
8568 let cursor: Range<usize> = state.selected_range();
8569 assert_eq!(cursor, 0..0);
8570
8571 state.replace_text_in_range(Some(0..0), "3", window, cx);
8573 assert_eq!(state.value(), "3.2");
8574 });
8575 });
8576 }
8577
8578 #[gpui::test]
8579 fn test_number_input_escape_invalid_text(cx: &mut TestAppContext) {
8580 let input_view = InputView::build(cx, |state| {
8583 state
8584 .mask_pattern(MaskPattern::Number {
8585 separator: None,
8586 fraction: None,
8587 })
8588 .default_value("1,234")
8589 });
8590 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8591 let input = input_view.input;
8592
8593 cx.update(|window, cx| {
8594 input.update(cx, |state, cx| {
8595 let range = state.range_to_utf16(&(4..5));
8599 state.replace_text_in_range(Some(range), "", window, cx);
8600 assert_eq!(state.value(), "1,23");
8601
8602 let range = state.range_to_utf16(&(1..2));
8604 state.replace_text_in_range(Some(range), "", window, cx);
8605 assert_eq!(state.value(), "123");
8606 state.replace_text_in_range(None, "a", window, cx);
8607 assert_eq!(state.value(), "123");
8608 });
8609 });
8610 }
8611
8612 #[gpui::test]
8616 fn test_set_value_single_line_caret_at_end_view_at_start(cx: &mut TestAppContext) {
8617 let input_view = InputView::build(cx, |state| state);
8618 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8619 let input = input_view.input;
8620
8621 let value = format!("https://example.com/v1/users?{}", "x=1&".repeat(120));
8623 let len = value.len();
8624
8625 cx.update(|window, cx| {
8628 input.update(cx, |state, cx| {
8629 state.set_value(value.clone(), window, cx);
8630
8631 assert_eq!(
8632 state.selected_range(),
8633 len..len,
8634 "single-line caret should be at the end after set_value"
8635 );
8636 assert_eq!(
8637 state.deferred_scroll_offset,
8638 Some(point(px(0.), px(0.))),
8639 "the view should be forced back to the start"
8640 );
8641 });
8642 });
8643
8644 cx.run_until_parked();
8647 cx.update(|_, cx| {
8648 input.read_with(cx, |state, _| {
8649 assert!(
8650 state.scroll_size.width > state.input_bounds.size.width,
8651 "value must overflow the input width or this test is vacuous"
8652 );
8653 assert_eq!(
8654 state.scroll_handle.offset().x,
8655 px(0.),
8656 "long value should display from its start, not its tail"
8657 );
8658 });
8659 });
8660 }
8661
8662 #[gpui::test]
8666 fn test_replace_all_single_line(cx: &mut TestAppContext) {
8667 let input_view = InputView::build(cx, |state| state);
8668 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8669 let input = input_view.input;
8670
8671 let value = format!("https://example.com/v1/users?{}", "x=1&".repeat(120));
8673 let len = value.len();
8674
8675 cx.update(|window, cx| {
8679 input.update(cx, |state, cx| {
8680 state.set_value("hello", window, cx);
8681 state.replace_all(value.clone(), window, cx);
8682 assert_eq!(state.value(), value);
8683 assert_eq!(
8684 state.selected_range(),
8685 len..len,
8686 "single-line caret should be at the end after replace_all"
8687 );
8688 assert_eq!(
8689 state.scroll_handle.offset(),
8690 point(px(0.), px(0.)),
8691 "the scroll offset should be reset to the start"
8692 );
8693 assert_eq!(
8694 state.deferred_scroll_offset,
8695 Some(point(px(0.), px(0.))),
8696 "single-line should set a deferred scroll offset to keep the start visible"
8697 );
8698 });
8699 });
8700
8701 cx.run_until_parked();
8704 cx.update(|_, cx| {
8705 input.read_with(cx, |state, _| {
8706 assert!(
8707 state.scroll_size.width > state.input_bounds.size.width,
8708 "value must overflow the input width or this test is vacuous"
8709 );
8710 assert_eq!(
8711 state.scroll_handle.offset().x,
8712 px(0.),
8713 "long value should display from its start, not its tail"
8714 );
8715 });
8716 });
8717 }
8718
8719 #[gpui::test]
8720 fn test_single_line_removes_newlines(cx: &mut TestAppContext) {
8721 let input_view = InputView::build(cx, |state| state.default_value("default\nvalue"));
8722 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8723 let input = input_view.input;
8724
8725 cx.update(|window, cx| {
8726 input.update(cx, |state, cx| {
8727 assert_eq!(state.value(), "defaultvalue");
8728
8729 state.set_value("first\nsecond\r\nthird\rfourth", window, cx);
8730 assert_eq!(state.value(), "firstsecondthirdfourth");
8731
8732 state.set_value("", window, cx);
8733 state.insert("a\nb", window, cx);
8734 assert_eq!(state.value(), "ab");
8735 });
8736
8737 cx.write_to_clipboard(ClipboardItem::new_string("a\r\nb\nc\rd".to_string()));
8738 input.update(cx, |state, cx| {
8739 state.set_value("", window, cx);
8740 state.paste(&Paste, window, cx);
8741 assert_eq!(state.value(), "abcd");
8742 });
8743 });
8744
8745 cx.run_until_parked();
8746 }
8747
8748 #[gpui::test]
8752 fn test_replace_all_multi_line(cx: &mut TestAppContext) {
8753 let input_view = InputView::build_textarea(cx, |state| state);
8754 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8755 let input = input_view.input;
8756
8757 cx.update(|window, cx| {
8758 input.update(cx, |state, cx| {
8759 state.set_value("foo\nbar", window, cx);
8760 state.replace_all("baz\nqux", window, cx);
8761 assert_eq!(state.value(), "baz\nqux");
8762 assert_eq!(
8763 state.selected_range(),
8764 0..0,
8765 "multi-line selection should be cleared after replace_all"
8766 );
8767 assert_eq!(
8768 state.scroll_handle.offset(),
8769 point(px(0.), px(0.)),
8770 "the scroll offset should be reset to the start"
8771 );
8772 assert!(
8773 state.deferred_scroll_offset.is_none(),
8774 "multi-line should not set a deferred scroll offset"
8775 );
8776 });
8777 });
8778 }
8779
8780 #[gpui::test]
8783 fn test_replace_all_preserves_undo_history(cx: &mut TestAppContext) {
8784 let input_view = InputView::build(cx, |state| state);
8785 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8786 let input = input_view.input;
8787
8788 cx.update(|window, cx| {
8789 input.update(cx, |state, cx| {
8790 state.set_value("first", window, cx);
8792 assert!(
8793 !state.undo_manager.has_undos(),
8794 "history should be empty after set_value"
8795 );
8796
8797 state.replace_all("second", window, cx);
8799 assert_eq!(state.value(), "second");
8800 assert!(
8801 state.undo_manager.has_undos(),
8802 "replace_all should record an undo step"
8803 );
8804
8805 state.undo(&Undo, window, cx);
8807 assert_eq!(state.value(), "first");
8808
8809 state.redo(&Redo, window, cx);
8811 assert_eq!(state.value(), "second");
8812 });
8813 });
8814 }
8815
8816 #[gpui::test]
8819 fn test_replace_all_code_editor(cx: &mut TestAppContext) {
8820 let input_view = InputView::new(cx);
8821 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8822 let input = input_view.input;
8823
8824 cx.update(|window, cx| {
8825 input.update(cx, |state, cx| {
8826 state.set_value("select 1", window, cx);
8828 state._pending_update = false;
8829
8830 state.replace_all("select 2", window, cx);
8831 assert_eq!(state.value(), "select 2");
8832 assert!(
8833 state._pending_update,
8834 "replace_all on a code editor should request a pending update"
8835 );
8836 });
8837 });
8838 }
8839
8840 #[gpui::test]
8841 fn test_set_selected_range(cx: &mut TestAppContext) {
8842 let input_view = InputView::build(cx, |state| state.default_value("hello world"));
8843 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8844 let input = input_view.input;
8845
8846 cx.update(|_, cx| {
8847 input.update(cx, |s, cx| {
8848 s.set_selected_range(0..5, cx);
8849 assert_eq!(s.selected_range(), 0..5);
8850 assert_eq!(s.selected_text().to_string(), "hello");
8851
8852 s.set_selected_range(6..11, cx);
8853 assert_eq!(s.selected_text().to_string(), "world");
8854
8855 s.set_selected_range(100..100, cx);
8857 assert_eq!(s.selected_range(), 11..11);
8858 });
8859 });
8860 }
8861
8862 #[gpui::test]
8864 fn test_replace_text_in_ranges_single_edit(cx: &mut TestAppContext) {
8865 let input_view = InputView::build_textarea(cx, |state| state.default_value("hello world"));
8866 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8867 let input = input_view.input;
8868
8869 cx.update(|window, cx| {
8870 input.update(cx, |s, cx| {
8871 s.replace_text_in_ranges(&[(0..5, "HELLO".to_string())], window, cx);
8872 assert_eq!(s.value(), "HELLO world");
8873
8874 s.undo(&Undo, window, cx);
8875 assert_eq!(s.value(), "hello world");
8876
8877 s.redo(&Redo, window, cx);
8878 assert_eq!(s.value(), "HELLO world");
8879 });
8880 });
8881 }
8882
8883 #[gpui::test]
8886 fn test_replace_text_in_ranges_multi_edit_transaction(cx: &mut TestAppContext) {
8887 let input_view = InputView::build_textarea(cx, |state| state.default_value("aaa bbb ccc"));
8888 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8889 let input = input_view.input;
8890
8891 cx.update(|window, cx| {
8892 input.update(cx, |s, cx| {
8893 s.replace_text_in_ranges(
8896 &[(0..3, "X".to_string()), (8..11, "Y".to_string())],
8897 window,
8898 cx,
8899 );
8900 assert_eq!(s.value(), "X bbb Y");
8901
8902 let cursors: Vec<usize> =
8904 s.selections.iter().map(|sel| sel.cursor_offset()).collect();
8905 assert_eq!(cursors, vec![1, 7]);
8906
8907 assert_eq!(s.undo_manager.undo_count(), 1);
8909
8910 s.undo(&Undo, window, cx);
8912 assert_eq!(s.value(), "aaa bbb ccc");
8913
8914 s.redo(&Redo, window, cx);
8916 assert_eq!(s.value(), "X bbb Y");
8917 });
8918 });
8919 }
8920
8921 #[gpui::test]
8925 fn test_replace_text_in_ranges_drives_the_highlighter_once(cx: &mut TestAppContext) {
8926 use crate::input::{FoldRange, HighlightStyleResolver, InputEdit, InputHighlighter};
8927 use gpui::HighlightStyle;
8928 use std::cell::RefCell;
8929
8930 struct RecordingHighlighter(Rc<RefCell<Vec<Vec<String>>>>);
8931
8932 impl InputHighlighter for RecordingHighlighter {
8933 fn language(&self) -> SharedString {
8934 "recording".into()
8935 }
8936
8937 fn update(
8938 &mut self,
8939 _: Option<InputEdit>,
8940 text: &Rope,
8941 _: bool,
8942 _: &mut Window,
8943 _: &mut Context<EditorState>,
8944 ) {
8945 self.0.borrow_mut().push(vec![text.to_string()]);
8946 }
8947
8948 fn update_batch(
8949 &mut self,
8950 edits: &[(InputEdit, Rope)],
8951 _: bool,
8952 _: &mut Window,
8953 _: &mut Context<EditorState>,
8954 ) {
8955 let texts = edits.iter().map(|(_, text)| text.to_string()).collect();
8956 self.0.borrow_mut().push(texts);
8957 }
8958
8959 fn styles(
8960 &self,
8961 range: &Range<usize>,
8962 _: &dyn HighlightStyleResolver,
8963 ) -> Vec<(Range<usize>, HighlightStyle)> {
8964 vec![(range.clone(), HighlightStyle::default())]
8965 }
8966
8967 fn fold_ranges(&self, _: &Rope) -> Vec<FoldRange> {
8968 Vec::new()
8969 }
8970 }
8971
8972 let updates = Rc::new(RefCell::new(Vec::new()));
8973 let input_view = InputView::new(cx);
8974 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8975 let input = input_view.input;
8976
8977 cx.update(|window, cx| {
8978 input.update(cx, |s, cx| {
8979 s.set_value("aaa bbb ccc", window, cx);
8980 let recorded = updates.clone();
8981 s.set_highlighter_factory(
8982 Rc::new(move |_: &str| {
8983 let highlighter: Box<dyn InputHighlighter> =
8984 Box::new(RecordingHighlighter(recorded.clone()));
8985 Some(highlighter)
8986 }),
8987 cx,
8988 );
8989 s.replace_text_in_ranges(
8990 &[(0..3, "X".to_string()), (8..11, "Y".to_string())],
8991 window,
8992 cx,
8993 );
8994 });
8995 });
8996
8997 assert_eq!(
8998 *updates.borrow(),
8999 vec![vec!["aaa bbb Y".to_string(), "X bbb Y".to_string()]]
9000 );
9001 }
9002
9003 #[gpui::test]
9005 fn test_last_input_edits_single_insert(cx: &mut TestAppContext) {
9006 let input_view = InputView::build_textarea(cx, |state| state.default_value("hello world"));
9007 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
9008 let input = input_view.input;
9009
9010 cx.update(|window, cx| {
9011 input.update(cx, |state, cx| {
9012 state.replace_text_in_ranges(&[(5..5, " big".to_string())], window, cx);
9013
9014 let edits = state.last_input_edits.as_slice();
9015 assert_eq!(edits.len(), 1);
9016 let (edit, text_after) = &edits[0];
9017 assert_eq!(edit.start_byte, 5);
9018 assert_eq!(edit.old_end_byte, 5);
9019 assert_eq!(edit.new_end_byte, 9);
9020 assert_eq!(edit.start_position, crate::input::Point::new(0, 5));
9021 assert_eq!(edit.old_end_position, crate::input::Point::new(0, 5));
9022 assert_eq!(edit.new_end_position, crate::input::Point::new(0, 9));
9023 assert_eq!(text_after.to_string(), "hello big world");
9024 });
9025 });
9026 }
9027
9028 #[gpui::test]
9030 fn test_last_input_edits_delete_and_paste(cx: &mut TestAppContext) {
9031 let input_view = InputView::build_textarea(cx, |state| state.default_value("hello world"));
9032 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
9033 let input = input_view.input;
9034
9035 cx.update(|window, cx| {
9036 input.update(cx, |state, cx| {
9037 state.replace_text_in_ranges(&[(5..11, String::new())], window, cx);
9038 assert_eq!(state.value(), "hello");
9039
9040 let edits = state.last_input_edits.as_slice();
9041 assert_eq!(edits.len(), 1);
9042 let (edit, text_after) = &edits[0];
9043 assert_eq!(edit.start_byte, 5);
9044 assert_eq!(edit.old_end_byte, 11);
9045 assert_eq!(edit.new_end_byte, 5);
9046 assert_eq!(text_after.to_string(), "hello");
9047
9048 state.replace_text_in_ranges(&[(5..5, "\nnext".to_string())], window, cx);
9049 assert_eq!(state.value(), "hello\nnext");
9050
9051 let edits = state.last_input_edits.as_slice();
9052 assert_eq!(edits.len(), 1);
9053 let (edit, text_after) = &edits[0];
9054 assert_eq!(edit.start_byte, 5);
9055 assert_eq!(edit.old_end_byte, 5);
9056 assert_eq!(edit.new_end_byte, 10);
9057 assert_eq!(edit.start_position, crate::input::Point::new(0, 5));
9058 assert_eq!(edit.old_end_position, crate::input::Point::new(0, 5));
9059 assert_eq!(edit.new_end_position, crate::input::Point::new(1, 4));
9060 assert_eq!(text_after.to_string(), "hello\nnext");
9061 });
9062 });
9063 }
9064
9065 #[gpui::test]
9067 fn test_last_input_edits_grouped_change(cx: &mut TestAppContext) {
9068 let input_view = InputView::build_textarea(cx, |state| state.default_value("aaa bbb ccc"));
9069 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
9070 let input = input_view.input;
9071
9072 cx.update(|window, cx| {
9073 input.update(cx, |state, cx| {
9074 state.replace_text_in_ranges(
9075 &[(0..3, "X".to_string()), (8..11, "Y".to_string())],
9076 window,
9077 cx,
9078 );
9079 assert_eq!(state.value(), "X bbb Y");
9080
9081 let edits = state.last_input_edits.as_slice();
9082 assert_eq!(edits.len(), 2);
9083 assert_eq!(edits[0].0.start_byte, 8);
9085 assert_eq!(edits[0].0.old_end_byte, 11);
9086 assert_eq!(edits[0].0.new_end_byte, 9);
9087 assert_eq!(edits[0].0.start_position, crate::input::Point::new(0, 8));
9088 assert_eq!(edits[0].0.old_end_position, crate::input::Point::new(0, 11));
9089 assert_eq!(edits[0].0.new_end_position, crate::input::Point::new(0, 9));
9090 assert_eq!(edits[0].1.to_string(), "aaa bbb Y");
9091
9092 assert_eq!(edits[1].0.start_byte, 0);
9093 assert_eq!(edits[1].0.old_end_byte, 3);
9094 assert_eq!(edits[1].0.new_end_byte, 1);
9095 assert_eq!(edits[1].0.start_position, crate::input::Point::new(0, 0));
9096 assert_eq!(edits[1].0.old_end_position, crate::input::Point::new(0, 3));
9097 assert_eq!(edits[1].0.new_end_position, crate::input::Point::new(0, 1));
9098 assert_eq!(edits[1].1.to_string(), "X bbb Y");
9099 });
9100 });
9101 }
9102
9103 #[gpui::test]
9105 fn test_last_input_edits_without_a_highlighter(cx: &mut TestAppContext) {
9106 let input_view = InputView::new(cx);
9107 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
9108 let input = input_view.input;
9109
9110 cx.update(|window, cx| {
9111 input.update(cx, |state, cx| {
9112 state.set_value("fn main() {}", window, cx);
9113 let no_highlighter: InputHighlighterFactory = Rc::new(|_: &str| None);
9114 state.set_highlighter_factory(no_highlighter, cx);
9115 assert!(state.last_input_edits.as_slice().is_empty());
9116
9117 state.replace_text_in_ranges(&[(12..12, "\n".to_string())], window, cx);
9118 assert_eq!(state.value(), "fn main() {}\n");
9119
9120 let edits = state.last_input_edits.as_slice();
9121 assert_eq!(edits.len(), 1);
9122 let (edit, text_after) = &edits[0];
9123 assert_eq!(edit.start_byte, 12);
9124 assert_eq!(edit.old_end_byte, 12);
9125 assert_eq!(edit.new_end_byte, 13);
9126 assert_eq!(edit.start_position, crate::input::Point::new(0, 12));
9127 assert_eq!(edit.old_end_position, crate::input::Point::new(0, 12));
9128 assert_eq!(edit.new_end_position, crate::input::Point::new(1, 0));
9129 assert_eq!(text_after.to_string(), "fn main() {}\n");
9130 });
9131 });
9132 }
9133
9134 #[gpui::test]
9136 fn test_last_input_edits_cleared_by_set_value(cx: &mut TestAppContext) {
9137 let input_view = InputView::build_textarea(cx, |state| state.default_value("hello"));
9138 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
9139 let input = input_view.input;
9140
9141 cx.update(|window, cx| {
9142 input.update(cx, |state, cx| {
9143 state.replace_text_in_ranges(&[(5..5, "!".to_string())], window, cx);
9144 assert_eq!(state.last_input_edits.as_slice().len(), 1);
9145
9146 state.set_value("replaced", window, cx);
9147 assert!(state.last_input_edits.as_slice().is_empty());
9148
9149 state.replace_text_in_ranges(&[(8..8, "?".to_string())], window, cx);
9150 let edits = state.last_input_edits.as_slice();
9151 assert_eq!(edits.len(), 1);
9152 assert_eq!(edits[0].1.to_string(), "replaced?");
9153 });
9154 });
9155 }
9156
9157 fn replay_input_edits(text: &mut String, event: &InputEditsEvent) {
9159 for (edit, after) in event.edits() {
9160 let replacement = after.slice(edit.start_byte..edit.new_end_byte).to_string();
9161 text.replace_range(edit.start_byte..edit.old_end_byte, &replacement);
9162 assert_eq!(*text, after.to_string());
9163 }
9164 }
9165
9166 #[gpui::test]
9167 fn input_edits_events_preserve_queued_changes_and_undo(cx: &mut TestAppContext) {
9168 let view = InputView::build_textarea(cx, |state| state.default_value("hello"));
9169 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9170 let input = view.input;
9171 let observed = Rc::new(std::cell::RefCell::new(Vec::<InputEditsEvent>::new()));
9172 let seen = observed.clone();
9173 let _subscription = cx.update(|_, cx| {
9174 cx.subscribe(&input, move |_, event: &InputEditsEvent, _| {
9175 seen.borrow_mut().push(event.clone());
9176 })
9177 });
9178 cx.update(|window, cx| {
9179 input.update(cx, |state, cx| {
9180 state.replace_text_in_ranges(&[(5..5, "!".into())], window, cx);
9181 state.replace_text_in_ranges(&[(6..6, "?".into())], window, cx);
9182 state.undo(&Undo, window, cx);
9183 });
9184 });
9185 let events = observed.borrow();
9186 assert!(events.len() >= 3);
9187 let mut text = "hello".to_string();
9188 for event in events.iter() {
9189 replay_input_edits(&mut text, event);
9190 }
9191 assert_eq!(
9192 text,
9193 input.read_with(&cx, |state, _| state.value().to_string())
9194 );
9195 assert_eq!(events[0].edits()[0].1.to_string(), "hello!");
9196 assert_eq!(events[1].edits()[0].1.to_string(), "hello!?");
9197 }
9198
9199 #[gpui::test]
9200 fn input_edits_event_replays_ime_commit(cx: &mut TestAppContext) {
9201 let view = InputView::build_textarea(cx, |state| state.default_value(""));
9202 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9203 let input = view.input;
9204 let observed = Rc::new(std::cell::RefCell::new(Vec::<InputEditsEvent>::new()));
9205 let seen = observed.clone();
9206 let _subscription = cx.update(|_, cx| {
9207 cx.subscribe(&input, move |_, event: &InputEditsEvent, _| {
9208 seen.borrow_mut().push(event.clone());
9209 })
9210 });
9211 cx.update(|window, cx| {
9212 input.update(cx, |state, cx| {
9213 state.replace_and_mark_text_in_range(None, "n", Some(1..1), window, cx);
9214 state.replace_and_mark_text_in_range(None, "ni", Some(2..2), window, cx);
9215 state.replace_text_in_range(None, "你", window, cx);
9216 });
9217 });
9218 let events = observed.borrow();
9219 assert_eq!(events.len(), 1);
9220 let mut text = String::new();
9221 replay_input_edits(&mut text, &events[0]);
9222 assert_eq!(text, "你");
9223 }
9224
9225 #[gpui::test]
9226 fn input_edits_event_replays_after_cancelled_composition(cx: &mut TestAppContext) {
9227 let view = InputView::build_textarea(cx, |state| state.default_value(""));
9228 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9229 let input = view.input;
9230 let observed = Rc::new(std::cell::RefCell::new(Vec::<InputEditsEvent>::new()));
9231 let seen = observed.clone();
9232 let _subscription = cx.update(|_, cx| {
9233 cx.subscribe(&input, move |_, event: &InputEditsEvent, _| {
9234 seen.borrow_mut().push(event.clone());
9235 })
9236 });
9237 cx.update(|window, cx| {
9238 input.update(cx, |state, cx| {
9239 state.replace_and_mark_text_in_range(None, "n", Some(1..1), window, cx);
9240 state.replace_and_mark_text_in_range(None, "", None, window, cx);
9241 state.replace_text_in_range(None, "x", window, cx);
9242 });
9243 });
9244 let events = observed.borrow();
9245 assert_eq!(events.len(), 1);
9246 let mut text = String::new();
9247 replay_input_edits(&mut text, &events[0]);
9248 assert_eq!(text, "x");
9249 }
9250
9251 #[gpui::test]
9252 fn input_edits_event_replays_mask_rewrite(cx: &mut TestAppContext) {
9253 let view = InputView::build(cx, |state| {
9254 state
9255 .mask_pattern(MaskPattern::Number {
9256 separator: Some(','),
9257 fraction: None,
9258 })
9259 .default_value("999")
9260 });
9261 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9262 let input = view.input;
9263 let observed = Rc::new(std::cell::RefCell::new(Vec::<InputEditsEvent>::new()));
9264 let seen = observed.clone();
9265 let _subscription = cx.update(|_, cx| {
9266 cx.subscribe(&input, move |_, event: &InputEditsEvent, _| {
9267 seen.borrow_mut().push(event.clone());
9268 })
9269 });
9270 cx.update(|window, cx| {
9271 input.update(cx, |state, cx| {
9272 state.set_cursor_to(3);
9273 state.replace_text_in_range(None, "9", window, cx)
9274 });
9275 });
9276 let events = observed.borrow();
9277 assert_eq!(events.len(), 1);
9278 let mut text = "999".to_string();
9279 replay_input_edits(&mut text, &events[0]);
9280 assert_eq!(text, "9,999");
9281 }
9282
9283 #[gpui::test]
9285 fn test_ime_composition_undoes_as_one_unit(cx: &mut TestAppContext) {
9286 let input_view = InputView::build_textarea(cx, |state| state.default_value(""));
9287 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
9288 let input = input_view.input;
9289
9290 cx.update(|window, cx| {
9291 input.update(cx, |s, cx| {
9292 s.replace_and_mark_text_in_range(None, "n", Some(1..1), window, cx);
9294 s.replace_and_mark_text_in_range(None, "ni", Some(2..2), window, cx);
9295 s.replace_text_in_range(None, "你", window, cx);
9296 assert_eq!(s.value(), "你");
9297
9298 assert_eq!(s.undo_manager.undo_count(), 1);
9300
9301 s.undo(&Undo, window, cx);
9302 assert_eq!(s.value(), "");
9303
9304 s.redo(&Redo, window, cx);
9305 assert_eq!(s.value(), "你");
9306 });
9307 });
9308 }
9309
9310 #[gpui::test]
9313 fn test_edit_after_composition_is_separate_undo(cx: &mut TestAppContext) {
9314 let input_view = InputView::build_textarea(cx, |state| state.default_value(""));
9315 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
9316 let input = input_view.input;
9317
9318 cx.update(|window, cx| {
9319 input.update(cx, |s, cx| {
9320 s.replace_and_mark_text_in_range(None, "n", Some(1..1), window, cx);
9321 s.replace_text_in_range(None, "你", window, cx);
9322 assert_eq!(s.value(), "你");
9323 assert_eq!(s.undo_manager.undo_count(), 1);
9324
9325 s.replace_text_in_range(None, "x", window, cx);
9327 assert_eq!(s.value(), "你x");
9328 assert_eq!(s.undo_manager.undo_count(), 2);
9329
9330 s.undo(&Undo, window, cx);
9331 assert_eq!(s.value(), "你");
9332 s.undo(&Undo, window, cx);
9333 assert_eq!(s.value(), "");
9334 });
9335 });
9336 }
9337
9338 #[gpui::test]
9341 fn test_composition_cancel_via_unmark_does_not_leak(cx: &mut TestAppContext) {
9342 let input_view = InputView::build_textarea(cx, |state| state.default_value(""));
9343 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
9344 let input = input_view.input;
9345
9346 cx.update(|window, cx| {
9347 input.update(cx, |s, cx| {
9348 s.replace_and_mark_text_in_range(None, "n", Some(1..1), window, cx);
9350 s.unmark_text(window, cx);
9351 let after_cancel = s.undo_manager.undo_count();
9352
9353 s.replace_text_in_range(None, "x", window, cx);
9355 assert_eq!(s.undo_manager.undo_count(), after_cancel + 1);
9356 });
9357 });
9358 }
9359
9360 #[gpui::test]
9361 fn test_set_selected_range_clips_to_utf8_boundaries(cx: &mut TestAppContext) {
9362 let input_view = InputView::build(cx, |state| state.default_value("éx"));
9363 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
9364 let input = input_view.input;
9365
9366 cx.update(|window, cx| {
9367 input.update(cx, |state, cx| {
9368 state.set_selected_range(0..1, cx);
9369 assert_eq!(state.selected_range(), 0..2);
9370 state.copy(&Copy, window, cx);
9371
9372 state.set_selected_range(1..1, cx);
9373 assert_eq!(state.selected_range(), 0..0);
9374 });
9375 });
9376 }
9377
9378 #[gpui::test]
9379 fn test_ime_selection_is_relative_to_replacement_start(cx: &mut TestAppContext) {
9380 let input_view = InputView::build(cx, |state| state.default_value("你好 "));
9381 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
9382 let input = input_view.input;
9383
9384 cx.update(|window, cx| {
9385 input.update(cx, |state, cx| {
9386 state.set_selected_range(7..7, cx);
9387 state.replace_and_mark_text_in_range(None, "s", Some(1..1), window, cx);
9388 state.replace_and_mark_text_in_range(None, "sh", Some(2..2), window, cx);
9389
9390 assert_eq!(state.value(), "你好 sh");
9391 assert_eq!(state.selected_range(), 9..9);
9392 assert_eq!(state.ime_marked_range, Some((7..9).into()));
9393 });
9394 });
9395 }
9396
9397 #[gpui::test]
9398 fn test_undo_manager_composition_is_one_undo_group(cx: &mut TestAppContext) {
9399 let input_view = InputView::build(cx, |state| state);
9400 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
9401 let input = input_view.input;
9402
9403 cx.update(|window, cx| {
9404 input.update(cx, |state, cx| {
9405 state.set_value("a", window, cx);
9406 state.replace_and_mark_text_in_range(None, "s", None, window, cx);
9407 state.replace_and_mark_text_in_range(None, "sh", None, window, cx);
9408 state.replace_text_in_range(None, "是", window, cx);
9409 assert_eq!(state.value(), "a是");
9410
9411 state.undo(&Undo, window, cx);
9412 assert_eq!(state.value(), "a");
9413 state.redo(&Redo, window, cx);
9414 assert_eq!(state.value(), "a是");
9415 });
9416 });
9417 }
9418
9419 #[gpui::test]
9420 fn test_undo_manager_consecutive_compositions_are_separate_groups(cx: &mut TestAppContext) {
9421 let input_view = InputView::build(cx, |state| state);
9422 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
9423 let input = input_view.input;
9424
9425 cx.update(|window, cx| {
9426 input.update(cx, |state, cx| {
9427 state.replace_and_mark_text_in_range(None, "j", None, window, cx);
9429 state.replace_and_mark_text_in_range(None, "jin", None, window, cx);
9430 state.replace_text_in_range(None, "今天", window, cx);
9431 state.replace_and_mark_text_in_range(None, "w", None, window, cx);
9433 state.replace_and_mark_text_in_range(None, "wo", None, window, cx);
9434 state.replace_text_in_range(None, "我们", window, cx);
9435 assert_eq!(state.value(), "今天我们");
9436 assert_eq!(state.selected_range(), 12..12);
9437
9438 state.undo(&Undo, window, cx);
9439 assert_eq!(state.value(), "今天");
9440 assert_eq!(state.selected_range(), 6..6);
9441
9442 state.undo(&Undo, window, cx);
9443 assert_eq!(state.value(), "");
9444 assert_eq!(state.selected_range(), 0..0);
9445
9446 state.redo(&Redo, window, cx);
9447 assert_eq!(state.value(), "今天");
9448 assert_eq!(state.selected_range(), 6..6);
9449
9450 state.redo(&Redo, window, cx);
9451 assert_eq!(state.value(), "今天我们");
9452 assert_eq!(state.selected_range(), 12..12);
9453 });
9454 });
9455 }
9456
9457 #[gpui::test]
9458 fn test_undo_manager_typing_after_composition_is_a_separate_group(cx: &mut TestAppContext) {
9459 let input_view = InputView::build(cx, |state| state);
9460 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
9461 let input = input_view.input;
9462
9463 cx.update(|window, cx| {
9464 input.update(cx, |state, cx| {
9465 state.replace_and_mark_text_in_range(None, "n", None, window, cx);
9466 state.replace_text_in_range(None, "你", window, cx);
9467 state.undo_manager.set_pending_intent(EditIntent::Typing);
9468 state.replace_text_in_range(None, "a", window, cx);
9469 state.undo_manager.set_pending_intent(EditIntent::Typing);
9470 state.replace_text_in_range(None, "b", window, cx);
9471 assert_eq!(state.value(), "你ab");
9472
9473 state.undo(&Undo, window, cx);
9474 assert_eq!(state.value(), "你");
9475
9476 state.undo(&Undo, window, cx);
9477 assert_eq!(state.value(), "");
9478 });
9479 });
9480 }
9481
9482 #[gpui::test]
9483 fn test_undo_manager_composition_cancel_leaves_no_entry(cx: &mut TestAppContext) {
9484 let input_view = InputView::build(cx, |state| state);
9485 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
9486 let input = input_view.input;
9487
9488 cx.update(|window, cx| {
9489 input.update(cx, |state, cx| {
9490 state.set_value("a", window, cx);
9491 state.replace_and_mark_text_in_range(None, "s", None, window, cx);
9492 state.replace_and_mark_text_in_range(None, "", None, window, cx);
9493
9494 assert_eq!(state.value(), "a");
9495 assert!(!state.undo_manager.has_undos());
9496 });
9497 });
9498 }
9499
9500 #[gpui::test]
9502 fn test_undo_manager_ime_rewrites_of_typed_char_are_one_group(cx: &mut TestAppContext) {
9503 let input_view = InputView::build(cx, |state| state);
9504 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
9505 let input = input_view.input;
9506
9507 cx.update(|window, cx| {
9508 input.update(cx, |state, cx| {
9509 state.set_value("é ", window, cx);
9510 state.set_selected_range(3..3, cx);
9511 state.replace_text_in_range(None, "ㅎ", window, cx);
9513 state.replace_text_in_range(Some(2..3), "하", window, cx);
9514 state.replace_text_in_range(Some(2..3), "한", window, cx);
9515 state.replace_text_in_range(None, "ㄱ", window, cx);
9516 state.replace_text_in_range(Some(3..4), "그", window, cx);
9517 state.replace_text_in_range(Some(3..4), "글", window, cx);
9518 assert_eq!(state.value(), "é 한글");
9519 assert_eq!(state.undo_manager.undo_count(), 1);
9520
9521 state.undo(&Undo, window, cx);
9522 assert_eq!(state.value(), "é ");
9523 assert_eq!(state.selected_range(), 3..3);
9524
9525 state.redo(&Redo, window, cx);
9526 assert_eq!(state.value(), "é 한글");
9527 assert_eq!(state.selected_range(), 9..9);
9528 });
9529 });
9530 }
9531
9532 #[gpui::test]
9533 fn test_undo_manager_rewrite_after_cursor_movement_is_separate(cx: &mut TestAppContext) {
9534 let input_view = InputView::build(cx, |state| state);
9535 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
9536 let input = input_view.input;
9537
9538 cx.update(|window, cx| {
9539 input.update(cx, |state, cx| {
9540 state.replace_text_in_range(None, "c", window, cx);
9541 state.replace_text_in_range(None, "e", window, cx);
9542 state.left(&MoveLeft, window, cx);
9543 state.right(&MoveRight, window, cx);
9544 state.replace_text_in_range(Some(1..2), "é", window, cx);
9545 assert_eq!(state.value(), "cé");
9546
9547 state.undo(&Undo, window, cx);
9548 assert_eq!(state.value(), "ce");
9549 state.undo(&Undo, window, cx);
9550 assert_eq!(state.value(), "");
9551 });
9552 });
9553 }
9554
9555 #[gpui::test]
9556 fn test_undo_manager_selection_restored_by_undo_and_redo(cx: &mut TestAppContext) {
9557 let input_view = InputView::build(cx, |state| state);
9558 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
9559 let input = input_view.input;
9560
9561 cx.update(|window, cx| {
9562 input.update(cx, |state, cx| {
9563 state.set_value("abc", window, cx);
9564 state.set_selected_range(1..2, cx);
9565 state.replace_text_in_range(None, "X", window, cx);
9566
9567 state.undo(&Undo, window, cx);
9568 assert_eq!(state.value(), "abc");
9569 assert_eq!(state.selected_range(), 1..2);
9570
9571 state.redo(&Redo, window, cx);
9572 assert_eq!(state.value(), "aXc");
9573 assert_eq!(state.selected_range(), 2..2);
9574 });
9575 });
9576 }
9577
9578 #[gpui::test]
9579 fn test_undo_manager_forward_delete_restores_cursor(cx: &mut TestAppContext) {
9580 let input_view = InputView::build(cx, |state| state);
9581 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
9582 let input = input_view.input;
9583
9584 cx.update(|window, cx| {
9585 input.update(cx, |state, cx| {
9586 state.set_value("abc", window, cx);
9587 state.set_selected_range(1..1, cx);
9588 state.delete(&Delete, window, cx);
9589
9590 state.undo(&Undo, window, cx);
9591 assert_eq!(state.value(), "abc");
9592 assert_eq!(state.selected_range(), 1..1);
9593 });
9594 });
9595 }
9596
9597 #[gpui::test]
9598 fn test_undo_manager_selection_movement_preserves_redo(cx: &mut TestAppContext) {
9599 let input_view = InputView::build(cx, |state| state);
9600 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
9601 let input = input_view.input;
9602
9603 cx.update(|window, cx| {
9604 input.update(cx, |state, cx| {
9605 state.replace_text_in_range(None, "ab", window, cx);
9606 state.undo(&Undo, window, cx);
9607 state.set_selected_range(0..0, cx);
9608 state.redo(&Redo, window, cx);
9609 assert_eq!(state.value(), "ab");
9610
9611 state.undo(&Undo, window, cx);
9612 state.replace_text_in_range(None, "x", window, cx);
9613 state.redo(&Redo, window, cx);
9614 assert_eq!(state.value(), "x");
9615 });
9616 });
9617 }
9618
9619 #[gpui::test]
9620 fn test_undo_manager_noop_edit_preserves_redo(cx: &mut TestAppContext) {
9621 let input_view = InputView::build(cx, |state| state);
9622 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
9623 let input = input_view.input;
9624
9625 cx.update(|window, cx| {
9626 input.update(cx, |state, cx| {
9627 state.replace_text_in_range(None, "a", window, cx);
9628 state.move_to(0, None, cx);
9629 state.replace_text_in_range(None, "", window, cx);
9630 state.undo(&Undo, window, cx);
9631 assert_eq!(state.value(), "");
9632 state.redo(&Redo, window, cx);
9633 assert_eq!(state.value(), "a");
9634 assert_eq!(state.cursor(), 1);
9635 });
9636 });
9637 }
9638
9639 #[gpui::test]
9640 fn test_cursor_round_trip_stops_typing_coalescing(cx: &mut TestAppContext) {
9641 let input_view = InputView::build(cx, |state| state);
9642 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
9643 let input = input_view.input;
9644
9645 cx.update(|window, cx| {
9646 input.update(cx, |state, cx| {
9647 state.replace_text_in_range(None, "a", window, cx);
9648 state.left(&MoveLeft, window, cx);
9649 state.right(&MoveRight, window, cx);
9650 state.replace_text_in_range(None, "b", window, cx);
9651
9652 state.undo(&Undo, window, cx);
9653 assert_eq!(state.value(), "a");
9654 state.undo(&Undo, window, cx);
9655 assert_eq!(state.value(), "");
9656 });
9657 });
9658 }
9659
9660 #[gpui::test]
9661 fn test_undo_manager_noop_edit_breaks_coalescing_without_clearing_history(
9662 cx: &mut TestAppContext,
9663 ) {
9664 let input_view = InputView::build(cx, |state| state);
9665 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
9666 let input = input_view.input;
9667
9668 cx.update(|window, cx| {
9669 input.update(cx, |state, cx| {
9670 state.replace_text_in_range(None, "alpha", window, cx);
9671 state.replace_text_in_range(None, "", window, cx);
9672 state.replace_text_in_range(None, "beta", window, cx);
9673 assert_eq!(state.value(), "alphabeta");
9674
9675 state.undo(&Undo, window, cx);
9676 assert_eq!(state.value(), "alpha");
9677 state.undo(&Undo, window, cx);
9678 assert_eq!(state.value(), "");
9679 });
9680 });
9681 }
9682
9683 #[gpui::test]
9684 fn test_undo_manager_masked_redo_restores_actual_cursor(cx: &mut TestAppContext) {
9685 let input_view = InputView::build(cx, |state| {
9686 state.mask_pattern(MaskPattern::Number {
9687 separator: Some(','),
9688 fraction: None,
9689 })
9690 });
9691 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
9692 let input = input_view.input;
9693
9694 cx.update(|window, cx| {
9695 input.update(cx, |state, cx| {
9696 state.set_value("12345", window, cx);
9697 state.set_selected_range(2..2, cx);
9698 state.replace_text_in_range(None, "9", window, cx);
9699 let selection_after_edit = state.selected_range();
9700 assert_ne!(selection_after_edit.end, state.value().len());
9701
9702 state.undo(&Undo, window, cx);
9703 state.redo(&Redo, window, cx);
9704 assert_eq!(state.selected_range(), selection_after_edit);
9705 });
9706 });
9707 }
9708
9709 #[gpui::test]
9715 fn test_unfold_at(cx: &mut TestAppContext) {
9716 use crate::input::{FoldRange, Position};
9717
9718 let view = InputView::<EditorMode>::new(cx);
9719 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9720 let input = view.input;
9721
9722 cx.update(|window, cx| {
9725 input.update(cx, |state, cx| {
9726 state.set_value("a\nb\nc\nd\ne\nf\ng\nh\ni\nj\nk\nl", window, cx);
9727 state.apply_highlighter_fold_candidates(
9728 vec![
9729 FoldRange::new(0, 5),
9730 FoldRange::new(2, 4),
9731 FoldRange::new(7, 10),
9732 ],
9733 cx,
9734 );
9735 state.display_map.set_folded(0, true);
9736 state.display_map.set_folded(2, true);
9737 state.display_map.set_folded(7, true);
9738 });
9739 });
9740
9741 for line in [0, 5] {
9744 cx.update(|_, cx| {
9745 input.update(cx, |state, cx| {
9746 assert!(!state.display_map.is_buffer_line_hidden(line));
9747 assert!(!state.unfold_at(Position::new(line as u32, 0), cx));
9748 });
9749 input.read_with(cx, |state, _| {
9750 assert!(state.display_map.is_folded_at(0));
9751 assert!(state.display_map.is_folded_at(2));
9752 assert!(state.display_map.is_folded_at(7));
9753 });
9754 });
9755 }
9756
9757 cx.update(|_, cx| {
9760 input.update(cx, |state, cx| {
9761 assert!(state.unfold_at(Position::new(3, 0), cx));
9762 });
9763 input.read_with(cx, |state, _| {
9764 assert!(!state.display_map.is_buffer_line_hidden(3));
9765 assert!(!state.display_map.is_folded_at(0));
9766 assert!(!state.display_map.is_folded_at(2));
9767 assert!(state.display_map.is_folded_at(7));
9768 assert!(state.display_map.is_fold_candidate(0));
9770 assert!(state.display_map.is_fold_candidate(2));
9771 });
9772 });
9773
9774 cx.update(|_, cx| {
9776 input.update(cx, |state, cx| {
9777 assert!(!state.unfold_at(Position::new(3, 0), cx));
9778 });
9779 });
9780 }
9781
9782 #[gpui::test]
9787 fn test_blur_keeps_decorations(cx: &mut TestAppContext) {
9788 let view = InputView::<EditorMode>::new(cx);
9789 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9790 let input = view.input;
9791
9792 cx.update(|window, cx| {
9793 input.update(cx, |state, cx| {
9794 state.set_value("select 1", window, cx);
9795 let _collection = state.create_decorations_collection(
9796 vec![crate::input::TextDecoration::new(
9797 0..6,
9798 gpui::HighlightStyle {
9799 font_weight: Some(gpui::FontWeight::BOLD),
9800 ..Default::default()
9801 },
9802 )],
9803 cx,
9804 );
9805 state.present_hover(
9806 0..6,
9807 lsp_types::Hover {
9808 contents: lsp_types::HoverContents::Scalar(
9809 lsp_types::MarkedString::String("docs".into()),
9810 ),
9811 range: None,
9812 },
9813 cx,
9814 );
9815 assert!(state.hover_popover().is_some());
9816
9817 state.on_blur(window, cx);
9818
9819 assert!(
9820 state.hover_popover().is_none(),
9821 "blur should hide the hover popover"
9822 );
9823 let decorations = state.extras.decoration_layers();
9824 assert!(
9825 decorations.iter().any(|layer| !layer.is_empty()),
9826 "blur must not discard decorations"
9827 );
9828 });
9829 });
9830 }
9831
9832 #[gpui::test]
9837 fn test_kind_does_not_follow_the_row_count(cx: &mut TestAppContext) {
9838 let view = InputView::build_textarea(cx, |state| state.auto_grow(1, 1));
9839 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9840 view.input.read_with(&mut cx, |state, _| {
9841 assert!(state.is_multi_line());
9842 assert!(!state.is_single_line());
9843 assert!(!state.is_code_editor());
9844 });
9845 }
9846
9847 #[gpui::test]
9848 fn test_textarea_prewrap_prepares_content_rows_before_layout(cx: &mut TestAppContext) {
9849 cx.update(crate::init);
9850 for (value, expected_rows) in [("", 1), ("one\ntwo", 2)] {
9851 cx.open_window(size(px(400.), px(100.)), |window, cx| {
9852 let font = window.text_style().font();
9853 cx.new(|cx| {
9854 let mut state = crate::input::TextareaState::new(window, cx)
9855 .rows(12)
9856 .default_value(value);
9857 assert_eq!(state.display_map().wrap_row_count(), 0);
9858 state.prewrap(font, px(14.), px(120.), cx);
9859 assert_eq!(state.display_map().wrap_row_count(), expected_rows);
9860 assert_eq!(state.value(), value);
9861 state
9862 });
9863 gpui::EmptyView
9864 });
9865 }
9866 }
9867
9868 #[gpui::test]
9869 fn test_textarea_prewrap_respects_soft_wrap_before_layout(cx: &mut TestAppContext) {
9870 cx.update(crate::init);
9871 let value = "select id, name, email, phone from users where active\nlimit 10";
9872 for soft_wrap in [true, false] {
9873 cx.open_window(size(px(400.), px(100.)), |window, cx| {
9874 let font = window.text_style().font();
9875 cx.new(|cx| {
9876 let mut state = crate::input::TextareaState::new(window, cx)
9877 .rows(1)
9878 .soft_wrap(soft_wrap)
9879 .default_value(value);
9880 state.prewrap(font, px(14.), px(120.), cx);
9881 let rows = state.display_map().wrap_row_count();
9882 if soft_wrap {
9883 assert!(rows > 2, "the first logical line must wrap");
9884 } else {
9885 assert_eq!(rows, 2);
9886 }
9887 assert_eq!(state.value(), value);
9888 state
9889 });
9890 gpui::EmptyView
9891 });
9892 }
9893 }
9894
9895 #[gpui::test]
9896 fn test_prewrap_counts_wrapped_rows_before_the_first_paint(cx: &mut TestAppContext) {
9897 cx.update(crate::init);
9898 let text = "select id, name, email, phone from users where active\nlimit 10";
9899 for (soft_wrap, wraps) in [(true, true), (false, false)] {
9900 let mut input = None;
9901 cx.open_window(size(px(400.), px(100.)), |window, cx| {
9902 let font = window.text_style().font();
9903 let editor = cx.new(|cx| {
9904 let mut state = crate::input::EditorState::new(window, cx)
9905 .soft_wrap(soft_wrap)
9906 .default_value(text);
9907 state.prewrap(font, px(14.), px(120.), cx);
9908 state
9909 });
9910 input = Some(editor);
9911 gpui::EmptyView
9912 });
9913 let rows = input
9914 .unwrap()
9915 .read_with(cx, |state, _| state.display_map().wrap_row_count());
9916 assert_eq!(rows > 2, wraps, "soft_wrap({soft_wrap}): {rows} rows");
9918 if !wraps {
9919 assert_eq!(rows, 2);
9920 }
9921 }
9922 }
9923
9924 #[gpui::test]
9925 fn test_prewrap_uses_the_configured_tab_stops(cx: &mut TestAppContext) {
9926 cx.update(crate::init);
9927 cx.open_window(size(px(400.), px(100.)), |window, cx| {
9928 let font = window.text_style().font();
9929 let font_size = px(14.);
9930 let font_id = window.text_system().resolve_font(&font);
9931 let space_width = window.text_system().layout_width(font_id, font_size, ' ');
9932 cx.new(|cx| {
9933 let mut state = crate::input::EditorState::new(window, cx)
9934 .tab_size(TabSize {
9935 tab_size: 4,
9936 hard_tabs: true,
9937 })
9938 .default_value("\t\t");
9939 for (tab_size, expected_rows) in [(4, 2), (2, 1), (8, 2)] {
9940 state.set_tab_size(
9941 TabSize {
9942 tab_size,
9943 hard_tabs: true,
9944 },
9945 cx,
9946 );
9947 state.prewrap(font.clone(), font_size, space_width * 5. + RIGHT_MARGIN, cx);
9948 assert_eq!(state.display_map().wrap_row_count(), expected_rows);
9949 }
9950 state
9951 });
9952 gpui::EmptyView
9953 });
9954 }
9955
9956 #[gpui::test]
9959 fn test_prewrap_wraps_with_the_wrapping_indent(cx: &mut TestAppContext) {
9960 cx.update(crate::init);
9961 let text = " select id, name, email, phone from users where active";
9962 let mut rows = Vec::new();
9963 for indent in [WrappingIndent::Same, WrappingIndent::None] {
9964 let mut input = None;
9965 cx.open_window(size(px(400.), px(100.)), |window, cx| {
9966 let font = window.text_style().font();
9967 input = Some(cx.new(|cx| {
9968 let mut state = crate::input::EditorState::new(window, cx)
9969 .wrapping_indent(indent)
9970 .default_value(text);
9971 state.prewrap(font, px(14.), px(200.), cx);
9972 state
9973 }));
9974 gpui::EmptyView
9975 });
9976 rows.push(
9977 input
9978 .unwrap()
9979 .read_with(cx, |state, _| state.display_map().wrap_row_count()),
9980 );
9981 }
9982 assert!(rows[0] > rows[1], "rows with Same, None: {rows:?}");
9983 }
9984
9985 #[gpui::test]
9987 fn test_prewrap_does_nothing_once_laid_out(cx: &mut TestAppContext) {
9988 struct Frame(Entity<crate::input::EditorState>);
9989 impl Render for Frame {
9990 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
9991 crate::input::InputBase::new("frame")
9992 .h(px(100.))
9993 .child(crate::input::Editor::new(&self.0))
9994 }
9995 }
9996
9997 cx.update(crate::init);
9998 let text = "select id, name, email, phone from users where active\nlimit 10";
9999 let mut input = None;
10000 let window = cx.open_window(size(px(400.), px(100.)), |window, cx| {
10001 let state = cx.new(|cx| crate::input::EditorState::new(window, cx).default_value(text));
10002 input = Some(state.clone());
10003 Frame(state)
10004 });
10005 let input = input.unwrap();
10006 let mut cx = VisualTestContext::from_window(window.into(), cx);
10007 cx.update(|window, cx| window.draw(cx).clear(cx));
10008
10009 let laid_out = input.read_with(&cx, |state, _| state.display_map().wrap_row_count());
10010 assert!(laid_out >= 2, "laid out to {laid_out} rows");
10011 let font = cx.update(|window, _| window.text_style().font());
10012 input.update(&mut cx, |state, cx| {
10013 state.prewrap(font, px(14.), px(60.), cx)
10014 });
10015 assert_eq!(
10016 input.read_with(&cx, |state, _| state.display_map().wrap_row_count()),
10017 laid_out
10018 );
10019 }
10020
10021 #[gpui::test]
10026 fn test_soft_wrap_is_enabled_by_default(cx: &mut TestAppContext) {
10027 let textarea = InputView::build_textarea(cx, |state| state);
10028 let mut textarea_cx = VisualTestContext::from_window(textarea.window_handle.into(), cx);
10029 textarea
10030 .input
10031 .read_with(&mut textarea_cx, |state, _| assert!(state.soft_wrap));
10032
10033 let editor = InputView::<EditorMode>::new(cx);
10034 let mut editor_cx = VisualTestContext::from_window(editor.window_handle.into(), cx);
10035 editor
10036 .input
10037 .read_with(&mut editor_cx, |state, _| assert!(state.soft_wrap));
10038 }
10039
10040 fn parse_cursor_spec(input: &str) -> (String, Vec<usize>) {
10044 let mut full_text = String::new();
10045 let mut cursor_offsets = Vec::new();
10046 let non_empty_lines: Vec<&str> = input.lines().filter(|l| !l.is_empty()).collect();
10047
10048 for (line_idx, line) in non_empty_lines.iter().enumerate() {
10049 let mut positions = Vec::new();
10050 let mut text = String::new();
10051 for ch in line.chars() {
10052 if ch == '|' {
10053 positions.push(text.len());
10054 } else {
10055 text.push(ch);
10056 }
10057 }
10058
10059 if line_idx > 0 {
10060 full_text.push('\n');
10061 }
10062 let line_start = full_text.len();
10063 for pos in positions {
10064 cursor_offsets.push(line_start + pos);
10065 }
10066 full_text.push_str(&text);
10067 }
10068 full_text.push('\n');
10069
10070 (full_text, cursor_offsets)
10071 }
10072
10073 fn multi_line(cx: &mut TestAppContext) -> InputView<TextareaMode> {
10075 InputView::build_textarea(cx, |state| state)
10076 }
10077
10078 fn setup_cursors<M: InputModeKind>(
10080 cx: &mut VisualTestContext,
10081 input: &Entity<InputBaseState<M>>,
10082 spec: &str,
10083 ) {
10084 let (full_text, offsets) = parse_cursor_spec(spec);
10085 cx.update(|window, cx| {
10086 input.update(cx, |state, cx| {
10087 state.set_value(&full_text, window, cx);
10088 let selections = offsets
10089 .into_iter()
10090 .map(|offset| {
10091 CursorSelection::new(state.selections.generate_id(), offset, offset)
10092 })
10093 .collect();
10094 state.selections.replace_all(selections);
10095 cx.notify();
10096 });
10097 });
10098 }
10099
10100 #[track_caller]
10102 fn assert_cursors<M: InputModeKind>(
10103 cx: &mut VisualTestContext,
10104 input: &Entity<InputBaseState<M>>,
10105 spec: &str,
10106 ) {
10107 let (expected_text, mut expected_cursors) = parse_cursor_spec(spec);
10108 expected_cursors.sort();
10109
10110 let (actual_text, mut actual_cursors) = input.read_with(cx, |state, _| {
10111 (
10112 state.text.to_string(),
10113 state
10114 .selections
10115 .iter()
10116 .map(|s| s.cursor_offset())
10117 .collect::<Vec<_>>(),
10118 )
10119 });
10120 actual_cursors.sort();
10121
10122 assert_eq!(
10123 actual_text, expected_text,
10124 "Text mismatch:\nExpected: {expected_text:?}\nActual: {actual_text:?}"
10125 );
10126 assert_eq!(
10127 actual_cursors, expected_cursors,
10128 "Cursor mismatch:\nExpected: {expected_cursors:?}\nActual: {actual_cursors:?}"
10129 );
10130 }
10131
10132 #[gpui::test]
10133 fn test_alt_drag_selects_a_block_and_replaces_each_row(cx: &mut TestAppContext) {
10134 cx.update(crate::init);
10135 let view = InputView::<EditorMode>::new(cx);
10136 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10137 for modifiers in [
10138 gpui::Modifiers {
10139 alt: true,
10140 ..Default::default()
10141 },
10142 gpui::Modifiers {
10143 alt: true,
10144 shift: true,
10145 ..Default::default()
10146 },
10147 #[cfg(target_os = "linux")]
10148 gpui::Modifiers {
10149 alt: true,
10150 control: true,
10151 ..Default::default()
10152 },
10153 ] {
10154 setup_cursors(&mut cx, &view.input, "|abcd\nabcd\nabcd");
10155 cx.update(|window, cx| {
10156 view.input.update(cx, |state, cx| state.focus(window, cx));
10157 });
10158 let (start, end) = view.input.read_with(&cx, |state, _| {
10159 let layout = state.last_layout.as_ref().unwrap();
10160 let origin = state.last_bounds.unwrap().origin;
10161 let position = |row: usize, col| {
10162 let local = layout.lines[row]
10163 .position_for_index(col, layout, false)
10164 .unwrap();
10165 origin
10166 + point(
10167 layout.line_number_width + local.x,
10168 layout.line_height * (row as f32 + 0.5),
10169 )
10170 };
10171 (position(0, 1), position(2, 3))
10172 });
10173 let end = point(end.x - px(0.5), end.y);
10176 cx.update(|_, cx| {
10178 view.input.update(cx, |state, _| {
10179 state.extras.hover_definition.update(
10180 0..4,
10181 vec![lsp_types::LocationLink {
10182 origin_selection_range: None,
10183 target_uri: "file:///tmp/column-selection.rs".parse().unwrap(),
10184 target_range: Default::default(),
10185 target_selection_range: Default::default(),
10186 }],
10187 );
10188 });
10189 });
10190 cx.simulate_mouse_down(start, MouseButton::Left, modifiers);
10191 cx.simulate_mouse_move(end, MouseButton::Left, modifiers);
10192 cx.simulate_mouse_up(end, MouseButton::Left, modifiers);
10193 view.input.read_with(&cx, |state, _| {
10194 let ranges: Vec<_> = state
10195 .selections
10196 .iter()
10197 .map(|sel| sel.start..sel.end)
10198 .collect();
10199 assert_eq!(ranges, vec![1..3, 6..8, 11..13]);
10200 });
10201 cx.simulate_mouse_move(start, None, modifiers);
10203 cx.simulate_keystrokes("x");
10204 assert_cursors(&mut cx, &view.input, "ax|d\nax|d\nax|d");
10205 }
10206 }
10207
10208 #[gpui::test]
10209 fn test_alt_drag_extends_upward_from_an_existing_cursor(cx: &mut TestAppContext) {
10210 cx.update(crate::init);
10211 let view = InputView::<EditorMode>::new(cx);
10212 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10213 setup_cursors(&mut cx, &view.input, "abcd\nabcd\na|bcd");
10214 cx.update(|window, cx| {
10215 view.input.update(cx, |state, cx| state.focus(window, cx));
10216 });
10217 let (start, end) = view.input.read_with(&cx, |state, _| {
10218 let layout = state.last_layout.as_ref().unwrap();
10219 let origin = state.last_bounds.unwrap().origin;
10220 let local = layout.lines[0]
10221 .position_for_index(1, layout, false)
10222 .unwrap();
10223 let x = layout.line_number_width + local.x;
10224 (
10225 origin + point(x, layout.line_height * 2.5),
10226 origin + point(x, layout.line_height * 0.5),
10227 )
10228 });
10229 let modifiers = gpui::Modifiers {
10230 alt: true,
10231 ..Default::default()
10232 };
10233 cx.simulate_mouse_down(start, MouseButton::Left, modifiers);
10234 cx.simulate_mouse_move(end, MouseButton::Left, modifiers);
10235 cx.simulate_mouse_up(end, MouseButton::Left, modifiers);
10236 assert_cursors(&mut cx, &view.input, "a|bcd\na|bcd\na|bcd");
10237 }
10238
10239 #[gpui::test]
10240 fn test_alt_drag_over_a_short_row_keeps_the_block_width(cx: &mut TestAppContext) {
10241 cx.update(crate::init);
10242 let view = InputView::<EditorMode>::new(cx);
10243 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10244 setup_cursors(&mut cx, &view.input, "abcdef\nab\nabcdef");
10245 cx.update(|window, cx| {
10246 view.input.update(cx, |state, cx| state.focus(window, cx));
10247 });
10248 let (start, end) = view.input.read_with(&cx, |state, _| {
10251 let layout = state.last_layout.as_ref().unwrap();
10252 let origin = state.last_bounds.unwrap().origin;
10253 let x_of = |index| {
10254 layout.line_number_width
10255 + layout.lines[0]
10256 .position_for_index(index, layout, false)
10257 .unwrap()
10258 .x
10259 };
10260 (
10261 origin + point(x_of(1), layout.line_height * 0.5),
10262 origin + point(x_of(5), layout.line_height * 1.5),
10263 )
10264 });
10265 let modifiers = gpui::Modifiers {
10266 alt: true,
10267 shift: true,
10268 ..Default::default()
10269 };
10270 cx.simulate_mouse_down(start, MouseButton::Left, modifiers);
10271 cx.simulate_mouse_move(end, MouseButton::Left, modifiers);
10272 cx.simulate_mouse_up(end, MouseButton::Left, modifiers);
10273
10274 view.input.read_with(&cx, |state, _| {
10276 let ranges: Vec<_> = state.selections.iter().map(|s| (s.start, s.end)).collect();
10277 assert_eq!(ranges, vec![(1, 5), (8, 9)]);
10278 });
10279 }
10280
10281 #[gpui::test]
10282 fn test_alt_mouse_release_outside_editor_ends_column_selection(cx: &mut TestAppContext) {
10283 cx.update(crate::init);
10284 let view = InputView::<EditorMode>::new(cx);
10285 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10286 setup_cursors(&mut cx, &view.input, "|abcd\nabcd");
10287 let position = view.input.read_with(&cx, |state, _| {
10288 let layout = state.last_layout.as_ref().unwrap();
10289 state.last_bounds.unwrap().origin
10290 + point(layout.line_number_width + px(2.), layout.line_height * 0.5)
10291 });
10292 let modifiers = gpui::Modifiers {
10293 alt: true,
10294 ..Default::default()
10295 };
10296 cx.simulate_mouse_down(position, MouseButton::Left, modifiers);
10297 cx.simulate_mouse_up(point(px(-100.), px(-100.)), MouseButton::Left, modifiers);
10298 view.input.read_with(&cx, |state, _| {
10299 assert!(!state.selecting);
10300 assert!(state.column_select_start.is_none());
10301 });
10302 }
10303
10304 #[gpui::test]
10305 fn test_consumed_keystrokes_keep_cursor_visible(cx: &mut TestAppContext) {
10306 let view = InputView::<EditorMode>::new(cx);
10307 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10308 setup_cursors(&mut cx, &view.input, "a|b");
10309 cx.update(|window, cx| {
10310 view.input.update(cx, |state, cx| {
10311 state.focus(window, cx);
10312 state.pause_blink_cursor(cx);
10313 });
10314 });
10315 cx.run_until_parked();
10316 cx.executor()
10317 .advance_clock(std::time::Duration::from_millis(300));
10318 cx.run_until_parked();
10319 view.input.read_with(&cx, |state, cx| {
10320 assert!(!state.blink_cursor.read(cx).visible());
10321 });
10322 for _ in 0..5 {
10324 #[cfg(target_os = "macos")]
10325 cx.simulate_keystrokes("cmd-c");
10326 #[cfg(not(target_os = "macos"))]
10327 cx.simulate_keystrokes("ctrl-c");
10328 cx.run_until_parked();
10329 cx.executor()
10330 .advance_clock(std::time::Duration::from_millis(200));
10331 cx.run_until_parked();
10332 view.input.read_with(&cx, |state, cx| {
10333 assert!(state.blink_cursor.read(cx).visible());
10334 });
10335 }
10336 }
10337
10338 #[gpui::test]
10339 fn test_mouse_down_shows_cursor_like_a_cursor_move(cx: &mut TestAppContext) {
10340 cx.update(crate::init);
10341 let view = InputView::<EditorMode>::new(cx);
10342 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10343 setup_cursors(&mut cx, &view.input, "abcd|efgh");
10344 let position = |offset: Pixels, cx: &mut VisualTestContext| {
10345 view.input.read_with(cx, |state, _| {
10346 let layout = state.last_layout.as_ref().unwrap();
10347 state.last_bounds.unwrap().origin
10348 + point(layout.line_number_width + offset, layout.line_height * 0.5)
10349 })
10350 };
10351 let visible = |cx: &mut VisualTestContext| {
10352 view.input
10353 .read_with(cx, |state, cx| state.blink_cursor.read(cx).visible())
10354 };
10355
10356 let first = position(px(2.), &mut cx);
10359 cx.simulate_mouse_down(first, MouseButton::Left, Default::default());
10360 assert!(visible(&mut cx));
10361 cx.simulate_mouse_up(first, MouseButton::Left, Default::default());
10362 cx.run_until_parked();
10363
10364 cx.executor()
10367 .advance_clock(std::time::Duration::from_millis(400));
10368 cx.run_until_parked();
10369 assert!(!visible(&mut cx));
10370 let second = position(px(30.), &mut cx);
10371 cx.simulate_mouse_down(second, MouseButton::Left, Default::default());
10372 assert!(visible(&mut cx));
10373 cx.executor()
10374 .advance_clock(std::time::Duration::from_millis(250));
10375 cx.run_until_parked();
10376 assert!(visible(&mut cx));
10377 cx.simulate_mouse_up(second, MouseButton::Left, Default::default());
10378 }
10379
10380 #[gpui::test]
10381 fn test_multi_cursor_actions_reveal_hidden_carets(cx: &mut TestAppContext) {
10382 let view = InputView::<EditorMode>::new(cx);
10383 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10384 setup_cursors(&mut cx, &view.input, "ab\na|b\nab");
10385 for action in 0..6 {
10388 cx.update(|_, cx| {
10389 view.input.update(cx, |state, cx| {
10390 state.blink_cursor = cx.new(BlinkCursor::new);
10391 state.blink_cursor.update(cx, |cursor, cx| cursor.start(cx));
10392 });
10393 });
10394 cx.run_until_parked();
10395 cx.executor()
10396 .advance_clock(std::time::Duration::from_millis(500));
10397 cx.run_until_parked();
10398 view.input.read_with(&cx, |state, cx| {
10399 assert!(!state.blink_cursor.read(cx).visible(), "action {action}");
10400 });
10401
10402 cx.update(|window, cx| {
10403 view.input.update(cx, |state, cx| {
10404 match action {
10405 0 => state.add_cursor_above(&AddCursorAbove, window, cx),
10406 1 => state.add_cursor_below(&AddCursorBelow, window, cx),
10407 2 => state.select_up(&SelectUp, window, cx),
10408 3 => state.select_down(&SelectDown, window, cx),
10409 4 => state.replace_text_in_range(None, "x", window, cx),
10410 _ => state.backspace(&Backspace, window, cx),
10411 }
10412 assert!(state.blink_cursor.read(cx).visible(), "action {action}");
10413 });
10414 });
10415 }
10416 }
10417
10418 #[gpui::test]
10419 fn test_multi_cursor_keyboard_dispatch(cx: &mut TestAppContext) {
10420 let view = InputView::<EditorMode>::new(cx);
10421 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10422 setup_cursors(&mut cx, &view.input, "ab\na|b\nab");
10423 cx.update(|window, cx| {
10424 view.input.update(cx, |state, cx| state.focus(window, cx));
10425 });
10426 #[cfg(target_os = "macos")]
10427 cx.simulate_keystrokes("cmd-alt-up");
10428 #[cfg(target_os = "windows")]
10429 cx.simulate_keystrokes("ctrl-alt-up");
10430 #[cfg(not(any(target_os = "macos", target_os = "windows")))]
10431 cx.simulate_keystrokes("alt-shift-up");
10432 view.input
10433 .read_with(&cx, |state, _| assert_eq!(state.selections.len(), 2));
10434 #[cfg(target_os = "macos")]
10435 cx.simulate_keystrokes("cmd-alt-down");
10436 #[cfg(target_os = "windows")]
10437 cx.simulate_keystrokes("ctrl-alt-down");
10438 #[cfg(not(any(target_os = "macos", target_os = "windows")))]
10439 cx.simulate_keystrokes("alt-shift-down");
10440 view.input
10441 .read_with(&cx, |state, _| assert_eq!(state.selections.len(), 3));
10442 cx.simulate_keystrokes("x");
10443 assert_cursors(&mut cx, &view.input, "ax|b\nax|b\nax|b");
10444 }
10445
10446 #[gpui::test]
10447 fn test_multi_cursor_platform_word_selection_dispatch(cx: &mut TestAppContext) {
10448 let view = InputView::<EditorMode>::new(cx);
10449 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10450 setup_cursors(&mut cx, &view.input, "one |two\none |two");
10451 cx.update(|window, cx| {
10452 view.input.update(cx, |state, cx| state.focus(window, cx));
10453 });
10454 #[cfg(any(target_os = "macos", target_os = "linux"))]
10455 cx.simulate_keystrokes("alt-shift-right");
10456 #[cfg(not(any(target_os = "macos", target_os = "linux")))]
10457 cx.simulate_keystrokes("ctrl-shift-right");
10458 cx.simulate_keystrokes("x");
10459 assert_cursors(&mut cx, &view.input, "one x|\none x|");
10460 setup_cursors(&mut cx, &view.input, "one two|\none two|");
10461 #[cfg(any(target_os = "macos", target_os = "linux"))]
10462 cx.simulate_keystrokes("alt-shift-left");
10463 #[cfg(not(any(target_os = "macos", target_os = "linux")))]
10464 cx.simulate_keystrokes("ctrl-shift-left");
10465 cx.simulate_keystrokes("x");
10466 assert_cursors(&mut cx, &view.input, "one x|\none x|");
10467 }
10468
10469 #[cfg(not(target_os = "macos"))]
10470 #[gpui::test]
10471 fn test_multi_cursor_horizontal_selection_dispatch(cx: &mut TestAppContext) {
10472 let view = InputView::<EditorMode>::new(cx);
10473 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10474 setup_cursors(&mut cx, &view.input, "ab\na|b");
10475 cx.update(|window, cx| {
10476 view.input.update(cx, |state, cx| state.focus(window, cx));
10477 });
10478 #[cfg(target_os = "windows")]
10479 cx.simulate_keystrokes("ctrl-alt-up");
10480 #[cfg(not(target_os = "windows"))]
10481 cx.simulate_keystrokes("alt-shift-up");
10482 #[cfg(target_os = "linux")]
10483 cx.simulate_keystrokes("shift-right");
10484 #[cfg(not(target_os = "linux"))]
10485 cx.simulate_keystrokes("alt-shift-right");
10486 view.input.read_with(&cx, |state, _| {
10487 assert_eq!(
10488 state
10489 .selections
10490 .iter()
10491 .map(|s| s.start..s.end)
10492 .collect::<Vec<_>>(),
10493 vec![4..5, 1..2]
10494 );
10495 });
10496 #[cfg(target_os = "linux")]
10497 cx.simulate_keystrokes("shift-left shift-left");
10498 #[cfg(not(target_os = "linux"))]
10499 cx.simulate_keystrokes("alt-shift-left alt-shift-left");
10500 view.input.read_with(&cx, |state, _| {
10501 assert_eq!(
10502 state
10503 .selections
10504 .iter()
10505 .map(|s| s.start..s.end)
10506 .collect::<Vec<_>>(),
10507 vec![3..4, 0..1]
10508 );
10509 });
10510 cx.simulate_keystrokes("x");
10511 assert_cursors(&mut cx, &view.input, "x|b\nx|b");
10512 }
10513
10514 #[gpui::test]
10515 fn test_multi_cursor_alt_click_dispatch(cx: &mut TestAppContext) {
10516 cx.update(crate::init);
10517 let view = InputView::<EditorMode>::new(cx);
10518 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10519 setup_cursors(&mut cx, &view.input, "a|b\nab\nab");
10520 cx.update(|window, cx| {
10521 view.input.update(cx, |state, cx| state.focus(window, cx));
10522 });
10523 let position = view.input.read_with(&cx, |state, _| {
10524 let bounds = state.last_bounds.unwrap();
10525 let layout = state.last_layout.as_ref().unwrap();
10526 bounds.origin + point(layout.line_number_width + px(2.), layout.line_height * 1.5)
10527 });
10528 cx.simulate_click(
10529 position,
10530 gpui::Modifiers {
10531 alt: true,
10532 ..Default::default()
10533 },
10534 );
10535 view.input
10536 .read_with(&cx, |state, _| assert_eq!(state.selections.len(), 2));
10537 }
10538
10539 #[gpui::test]
10540 fn test_add_selection(cx: &mut TestAppContext) {
10541 let view = InputView::<EditorMode>::new(cx);
10542 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10543 cx.update(|window, cx| {
10544 view.input.update(cx, |state, cx| {
10545 state.focus(window, cx);
10546 state.set_value("foo bar foo", window, cx);
10547 state.set_selected_range(0..3, cx);
10548 state.add_selection(8..11, cx);
10550 state.add_selection(0..3, cx);
10552 });
10553 });
10554 view.input.read_with(&cx, |state, _| {
10555 let mut ranges: Vec<Range<usize>> =
10556 state.selections.iter().map(|s| s.start..s.end).collect();
10557 ranges.sort_by_key(|r| r.start);
10558 assert_eq!(ranges, vec![0..3, 8..11]);
10559 });
10560 cx.simulate_keystrokes("x");
10562 view.input.read_with(&cx, |state, _| {
10563 assert_eq!(state.text.to_string(), "x bar x")
10564 });
10565 }
10566
10567 #[gpui::test]
10568 fn test_add_selection_preserves_crlf_boundaries(cx: &mut TestAppContext) {
10569 let view = InputView::build_textarea(cx, |state| state.default_value("a\r\nb"));
10570 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10571 cx.update(|window, cx| {
10572 view.input.update(cx, |state, cx| {
10573 state.focus(window, cx);
10574 state.set_selected_range(4..4, cx);
10575 state.add_selection(2..2, cx);
10576 let ranges: Vec<_> = state.selections.iter().map(|s| s.start..s.end).collect();
10577 assert_eq!(ranges, vec![4..4, 1..1]);
10578 });
10579 });
10580 cx.simulate_keystrokes("x");
10581 view.input.read_with(&cx, |state, _| {
10582 assert_eq!(state.value(), "ax\r\nbx");
10583 });
10584 cx.update(|window, cx| {
10585 view.input.update(cx, |state, cx| {
10586 state.set_value("a\r\nb", window, cx);
10587 state.set_selected_range(4..4, cx);
10588 state.add_selection(2..3, cx);
10589 state.cut(&Cut, window, cx);
10590 assert_eq!(cx.read_from_clipboard().unwrap().text().unwrap(), "\r\n");
10591 assert_eq!(state.value(), "ab");
10592 });
10593 });
10594 }
10595
10596 #[gpui::test]
10597 fn test_add_selection_merges_overlaps_before_copy_and_cut(cx: &mut TestAppContext) {
10598 let view = InputView::<EditorMode>::new(cx);
10599 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10600 cx.update(|window, cx| {
10601 view.input.update(cx, |state, cx| {
10602 for additions in [vec![2..5], vec![0..5], vec![4..5, 2..4]] {
10605 state.set_value("abcdef", window, cx);
10606 state.set_selected_range(0..3, cx);
10607 let active_id = state.active_selection().id;
10608 for range in additions {
10609 state.add_selection(range, cx);
10610 }
10611 assert_eq!(state.active_selection().id, active_id);
10612 assert_eq!(state.selected_range(), 0..5);
10613 assert_eq!(state.selections.len(), 1);
10614 state.copy(&Copy, window, cx);
10615 assert_eq!(cx.read_from_clipboard().unwrap().text().unwrap(), "abcde");
10616 state.cut(&Cut, window, cx);
10617 assert_eq!(cx.read_from_clipboard().unwrap().text().unwrap(), "abcde");
10618 assert_eq!(state.value(), "f");
10619 state.undo(&Undo, window, cx);
10620 assert_eq!(state.value(), "abcdef");
10621 assert_eq!(state.selected_range(), 0..5);
10622 }
10623 });
10624 });
10625 }
10626
10627 #[gpui::test]
10628 fn test_add_selection_clears_inline_completion(cx: &mut TestAppContext) {
10629 use crate::input::CompletionProvider;
10630 use gpui::Task;
10631 use lsp_types::{
10632 CompletionContext, CompletionResponse, InlineCompletionContext, InlineCompletionItem,
10633 InlineCompletionResponse,
10634 };
10635 use std::time::Duration;
10636
10637 struct Provider(Rc<Cell<usize>>);
10638
10639 impl CompletionProvider for Provider {
10640 fn is_completion_trigger(&self, _: usize, _: &str, _: &mut App) -> bool {
10641 false
10642 }
10643
10644 fn completions(
10645 &self,
10646 _: &Rope,
10647 _: usize,
10648 _: CompletionContext,
10649 _: &mut Window,
10650 _: &mut App,
10651 ) -> Task<anyhow::Result<CompletionResponse>> {
10652 Task::ready(Ok(CompletionResponse::Array(vec![])))
10653 }
10654
10655 fn inline_completion(
10656 &self,
10657 _: &Rope,
10658 _: usize,
10659 _: InlineCompletionContext,
10660 _: &mut Window,
10661 _: &mut App,
10662 ) -> Task<anyhow::Result<InlineCompletionResponse>> {
10663 self.0.set(self.0.get() + 1);
10664 Task::ready(Ok(InlineCompletionResponse::Array(vec![])))
10665 }
10666 }
10667
10668 let view = InputView::<EditorMode>::new(cx);
10669 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10670 let calls = Rc::new(Cell::new(0));
10671 cx.update(|window, cx| {
10672 view.input.update(cx, |state, cx| {
10673 state.focus(window, cx);
10674 state.set_value("aa\naa", window, cx);
10675 state.set_selected_range(2..2, cx);
10676 state.extras.inline_completion.item = Some(InlineCompletionItem {
10677 insert_text: "suggestion".into(),
10678 filter_text: None,
10679 range: None,
10680 command: None,
10681 insert_text_format: None,
10682 });
10683 state.add_selection(5..5, cx);
10684 assert!(!state.has_inline_completion());
10685 state.indent_inline(&IndentInline, window, cx);
10686 assert_eq!(state.value(), "aa \naa ");
10687 assert_eq!(state.selections.len(), 2);
10688
10689 state.set_value("aa\naa", window, cx);
10692 state.set_selected_range(2..2, cx);
10693 state.extras.lsp.completion_provider = Some(Rc::new(Provider(calls.clone())));
10694 state.schedule_inline_completion(window, cx);
10695 });
10696 });
10697 cx.run_until_parked();
10698 cx.update(|_, cx| {
10699 view.input
10700 .update(cx, |state, cx| state.add_selection(5..5, cx));
10701 });
10702 cx.executor().advance_clock(Duration::from_secs(1));
10703 cx.run_until_parked();
10704 assert_eq!(calls.get(), 0);
10705 view.input.read_with(&cx, |state, _| {
10706 assert!(!state.has_inline_completion());
10707 assert_eq!(state.selections.len(), 2);
10708 assert_eq!(state.value(), "aa\naa");
10709 });
10710 }
10711
10712 #[gpui::test]
10713 fn test_word_delete_undo_restores_caret(cx: &mut TestAppContext) {
10714 let view = multi_line(cx);
10715 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10716 setup_cursors(&mut cx, &view.input, "hello|");
10717 cx.update(|window, cx| {
10718 view.input.update(cx, |state, cx| {
10719 state.delete_previous_word(&DeleteToPreviousWordStart, window, cx);
10720 state.undo(&Undo, window, cx);
10721 assert_eq!(state.selected_range(), 5..5);
10722 });
10723 });
10724 }
10725
10726 #[gpui::test]
10727 fn test_merged_delete_undo_restores_all_carets(cx: &mut TestAppContext) {
10728 let view = multi_line(cx);
10729 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10730 setup_cursors(&mut cx, &view.input, "a|b|c");
10731 cx.update(|window, cx| {
10732 view.input.update(cx, |state, cx| {
10733 state.backspace(&Backspace, window, cx);
10734 state.undo(&Undo, window, cx);
10735 });
10736 });
10737 assert_cursors(&mut cx, &view.input, "a|b|c");
10738 }
10739
10740 #[gpui::test]
10741 fn test_shift_end_respects_soft_wrap_end(cx: &mut TestAppContext) {
10742 let view = InputView::<EditorMode>::new(cx);
10743 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10744 cx.update(|window, cx| {
10745 view.input.update(cx, |state, cx| {
10746 state.set_value("abcdef ".repeat(30), window, cx);
10747 state.display_map.on_layout_changed(Some(px(60.)), cx);
10748 let line = state.display_map.line(0).unwrap();
10749 assert!(line.wrapped_lines.len() > 1);
10750 let boundary = line.wrapped_lines[0].end;
10751 state.move_to_with_affinity(boundary, None, true, cx);
10752 state.select_to_end_of_line(&SelectToEndOfLine, window, cx);
10753 assert_eq!(state.selected_range(), boundary..state.text.len());
10754 });
10755 });
10756 }
10757
10758 #[gpui::test]
10759 fn test_outdent_unindented_unicode_is_unchanged(cx: &mut TestAppContext) {
10760 let view = multi_line(cx);
10761 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10762 setup_cursors(&mut cx, &view.input, "|你好\n|世界");
10763 cx.update(|window, cx| {
10764 view.input.update(cx, |state, cx| {
10765 state.outdent(false, window, cx);
10766 state.outdent(true, window, cx);
10767 });
10768 });
10769 assert_cursors(&mut cx, &view.input, "|你好\n|世界");
10770 }
10771
10772 #[gpui::test]
10773 fn test_column_selection_stays_on_unicode_boundaries(cx: &mut TestAppContext) {
10774 let view = multi_line(cx);
10775 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10776 setup_cursors(&mut cx, &view.input, "ab\n你好\ncd");
10777 cx.update(|window, cx| {
10778 view.input.update(cx, |state, cx| {
10779 state.build_columnar_selection(
10780 ColumnarPoint::new(1, 0),
10781 ColumnarPoint::new(11, 0),
10782 cx,
10783 );
10784 let text = state.value();
10785 for sel in state.selections.iter() {
10786 assert!(text.is_char_boundary(sel.start));
10787 assert!(text.is_char_boundary(sel.end));
10788 }
10789 state.replace_text_in_range(None, "X", window, cx);
10790 });
10791 });
10792 }
10793
10794 #[gpui::test]
10795 fn test_editor_decorations_follow_typing(cx: &mut TestAppContext) {
10796 let view = InputView::<EditorMode>::new(cx);
10797 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10798 cx.update(|window, cx| {
10799 view.input.update(cx, |state, cx| {
10800 state.set_value("abc def", window, cx);
10801 state.create_decorations_collection(
10802 vec![crate::input::TextDecoration::new(
10803 4..7,
10804 gpui::HighlightStyle::default(),
10805 )],
10806 cx,
10807 );
10808 state.set_selected_range(0..0, cx);
10809 state.replace_text_in_range(None, "X", window, cx);
10810 let layers = state.extras.decoration_layers();
10811 assert_eq!(layers.into_iter().flatten().next().unwrap().range, 5..8);
10812 });
10813 });
10814 }
10815
10816 #[gpui::test]
10817 fn test_backspace_preserves_escaped_quote_terminator(cx: &mut TestAppContext) {
10818 let view = InputView::<EditorMode>::new(cx);
10819 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10820 setup_cursors(&mut cx, &view.input, r#"{"s":"\"|"}"#);
10821 cx.update(|window, cx| {
10822 view.input.update(cx, |state, cx| {
10823 set_test_syntax_provider(std::rc::Rc::new(StringAllProvider), cx);
10824 state.backspace(&Backspace, window, cx);
10825 });
10826 });
10827 assert_cursors(&mut cx, &view.input, r#"{"s":"\|"}"#);
10828 }
10829
10830 #[gpui::test]
10831 fn test_ordinary_typing_does_not_query_syntax(cx: &mut TestAppContext) {
10832 struct UnexpectedQuery;
10833 impl crate::input::SyntaxContextProvider for UnexpectedQuery {
10834 fn context_at(&self, _: &ropey::Rope, _: usize) -> crate::input::SyntaxContext {
10835 panic!("ordinary typing must not query syntax");
10836 }
10837 }
10838 let view = InputView::<EditorMode>::new(cx);
10839 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10840 setup_cursors(&mut cx, &view.input, "|");
10841 cx.update(|window, cx| {
10842 view.input.update(cx, |state, cx| {
10843 set_test_syntax_provider(std::rc::Rc::new(UnexpectedQuery), cx);
10844 for c in ["a", "b", "c"] {
10845 state.replace_text_in_range(None, c, window, cx);
10846 }
10847 });
10848 });
10849 assert_cursors(&mut cx, &view.input, "abc|");
10850 }
10851
10852 #[gpui::test]
10853 fn test_auto_close_is_atomic_at_history_limit(cx: &mut TestAppContext) {
10854 let view = InputView::<EditorMode>::new(cx);
10855 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10856 setup_cursors(&mut cx, &view.input, "|");
10857 cx.update(|window, cx| {
10858 view.input.update(cx, |state, cx| {
10859 for _ in 0..999 {
10860 state.replace_text_in_range(None, "a", window, cx);
10861 }
10862 state.replace_text_in_range(None, "(", window, cx);
10863 state.undo(&Undo, window, cx);
10864 assert!(!state.text.to_string().contains('('));
10865 assert!(!state.text.to_string().contains(')'));
10866 state.redo(&Redo, window, cx);
10867 });
10868 });
10869 assert_cursors(&mut cx, &view.input, &format!("{}(|)", "a".repeat(999)));
10870 }
10871
10872 #[gpui::test]
10873 fn test_comment_closer_is_inserted_literally(cx: &mut TestAppContext) {
10874 let view = InputView::<EditorMode>::new(cx);
10875 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10876 setup_cursors(&mut cx, &view.input, "// (|)");
10877 cx.update(|window, cx| {
10878 view.input.update(cx, |state, cx| {
10879 set_test_syntax_provider(std::rc::Rc::new(CommentAllProvider), cx);
10880 state.replace_text_in_range(None, ")", window, cx);
10881 });
10882 });
10883 assert_cursors(&mut cx, &view.input, "// ()|)");
10884 }
10885
10886 #[gpui::test]
10887 fn test_explicit_range_skip_does_not_delete_existing_closer_on_undo(cx: &mut TestAppContext) {
10888 let view = InputView::<EditorMode>::new(cx);
10889 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10890 setup_cursors(&mut cx, &view.input, "(a|)");
10891 let before = view.input.read_with(&cx, |state, _| state.text.to_string());
10892 cx.update(|window, cx| {
10893 view.input.update(cx, |state, cx| {
10894 state.replace_text_in_range(Some(2..2), ")", window, cx);
10895 });
10896 });
10897 assert_cursors(&mut cx, &view.input, "(a)|");
10898 cx.update(|window, cx| {
10899 view.input
10900 .update(cx, |state, cx| state.undo(&Undo, window, cx));
10901 });
10902 view.input.read_with(&cx, |state, _| {
10903 assert_eq!(
10904 state.text.to_string(),
10905 before,
10906 "skip must not remove an existing closer on undo"
10907 );
10908 });
10909 }
10910
10911 #[gpui::test]
10912 fn test_pair_redo_restores_interior_cursor(cx: &mut TestAppContext) {
10913 let view = InputView::<EditorMode>::new(cx);
10914 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10915 setup_cursors(&mut cx, &view.input, "|");
10916 cx.update(|window, cx| {
10917 view.input.update(cx, |state, cx| {
10918 state.replace_text_in_range(None, "(", window, cx);
10919 });
10920 });
10921 assert_cursors(&mut cx, &view.input, "(|)");
10922 cx.update(|window, cx| {
10923 view.input.update(cx, |state, cx| {
10924 state.undo(&Undo, window, cx);
10925 state.redo(&Redo, window, cx);
10926 });
10927 });
10928 assert_cursors(&mut cx, &view.input, "(|)");
10929 }
10930
10931 #[gpui::test]
10932 fn test_pair_enter_redo_restores_interior_cursor(cx: &mut TestAppContext) {
10933 let view = InputView::<EditorMode>::new(cx);
10934 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10935 setup_cursors(&mut cx, &view.input, "{|}");
10936 cx.update(|window, cx| {
10937 view.input.update(cx, |state, cx| {
10938 state.enter(
10939 &Enter {
10940 secondary: false,
10941 shift: false,
10942 },
10943 window,
10944 cx,
10945 );
10946 });
10947 });
10948 assert_cursors(&mut cx, &view.input, "{\n |\n}");
10949 cx.update(|window, cx| {
10950 view.input.update(cx, |state, cx| {
10951 state.undo(&Undo, window, cx);
10952 });
10953 });
10954 assert_cursors(&mut cx, &view.input, "{|}");
10955 cx.update(|window, cx| {
10956 view.input
10957 .update(cx, |state, cx| state.redo(&Redo, window, cx));
10958 });
10959 assert_cursors(&mut cx, &view.input, "{\n |\n}");
10960 }
10961
10962 #[gpui::test]
10963 fn test_auto_close_parens(cx: &mut TestAppContext) {
10964 let view = InputView::<EditorMode>::new(cx);
10965 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10966 setup_cursors(&mut cx, &view.input, "|");
10967 cx.update(|window, cx| {
10968 view.input.update(cx, |state, cx| {
10969 state.replace_text_in_range(None, "(", window, cx);
10970 });
10971 });
10972 assert_cursors(&mut cx, &view.input, "(|)");
10973 cx.update(|window, cx| {
10975 view.input.update(cx, |state, cx| {
10976 state.replace_text_in_range(None, "x", window, cx);
10977 });
10978 });
10979 assert_cursors(&mut cx, &view.input, "(x|)");
10980 }
10981
10982 #[gpui::test]
10983 fn test_auto_close_skip_over_closer(cx: &mut TestAppContext) {
10984 let view = InputView::<EditorMode>::new(cx);
10985 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10986 setup_cursors(&mut cx, &view.input, "(|)");
10987 cx.update(|window, cx| {
10988 view.input.update(cx, |state, cx| {
10989 state.replace_text_in_range(None, ")", window, cx);
10990 });
10991 });
10992 assert_cursors(&mut cx, &view.input, "()|");
10994 view.input.read_with(&cx, |state, _| {
10996 assert!(state.active_selection().is_empty());
10997 });
10998 }
10999
11000 #[gpui::test]
11001 fn test_auto_close_no_pair_inside_word(cx: &mut TestAppContext) {
11002 let view = InputView::<EditorMode>::new(cx);
11003 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11004 setup_cursors(&mut cx, &view.input, "don|");
11005 cx.update(|window, cx| {
11006 view.input.update(cx, |state, cx| {
11007 state.replace_text_in_range(None, "'", window, cx);
11008 });
11009 });
11010 assert_cursors(&mut cx, &view.input, "don'|");
11012 }
11013
11014 #[gpui::test]
11015 fn test_auto_close_quote_after_cjk(cx: &mut TestAppContext) {
11016 let view = InputView::<EditorMode>::new(cx);
11017 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11018 setup_cursors(&mut cx, &view.input, "中|");
11021 cx.update(|window, cx| {
11022 view.input.update(cx, |state, cx| {
11023 state.replace_text_in_range(None, "'", window, cx);
11024 });
11025 });
11026 assert_cursors(&mut cx, &view.input, "中'|");
11027 }
11028
11029 #[gpui::test]
11030 fn test_auto_close_closer_skips_at_string_end(cx: &mut TestAppContext) {
11031 let view = InputView::<EditorMode>::new(cx);
11032 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11033 setup_cursors(&mut cx, &view.input, "\"hello|\"");
11035 cx.update(|window, cx| {
11036 view.input.update(cx, |state, cx| {
11037 state.replace_text_in_range(None, "\"", window, cx);
11038 });
11039 });
11040 assert_cursors(&mut cx, &view.input, "\"hello\"|");
11041 view.input.read_with(&cx, |state, _| {
11042 assert!(state.active_selection().is_empty());
11043 });
11044 }
11045
11046 #[gpui::test]
11047 fn test_backspace_pair_with_cjk_prefix(cx: &mut TestAppContext) {
11048 let view = InputView::<EditorMode>::new(cx);
11049 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11050 setup_cursors(&mut cx, &view.input, "中(|)abc");
11052 cx.update(|window, cx| {
11053 view.input
11054 .update(cx, |state, cx| state.backspace(&Backspace, window, cx));
11055 });
11056 assert_cursors(&mut cx, &view.input, "中|abc");
11057 }
11058
11059 #[gpui::test]
11060 fn test_enter_split_respects_smart_indent_off(cx: &mut TestAppContext) {
11061 let view = InputView::<EditorMode>::new(cx);
11062 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11063 setup_cursors(&mut cx, &view.input, "{|}");
11064 cx.update(|window, cx| {
11065 view.input.update(cx, |state, cx| {
11066 state.set_smart_indent(false, window, cx);
11067 state.enter(
11068 &Enter {
11069 secondary: false,
11070 shift: false,
11071 },
11072 window,
11073 cx,
11074 );
11075 });
11076 });
11077 assert_cursors(&mut cx, &view.input, "{\n|}");
11079 }
11080
11081 #[gpui::test]
11082 fn test_enter_split_is_independent_of_auto_close(cx: &mut TestAppContext) {
11083 let view = InputView::<EditorMode>::new(cx);
11084 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11085 setup_cursors(&mut cx, &view.input, "{|}");
11086 cx.update(|window, cx| {
11087 view.input.update(cx, |state, cx| {
11088 state.set_auto_close(false, window, cx);
11089 state.enter(
11090 &Enter {
11091 secondary: false,
11092 shift: false,
11093 },
11094 window,
11095 cx,
11096 );
11097 });
11098 });
11099 assert_cursors(&mut cx, &view.input, "{\n |\n}");
11101 }
11102
11103 #[gpui::test]
11104 fn test_language_config_applies_before_render_and_on_language_change(cx: &mut TestAppContext) {
11105 use crate::input::{AutoClosingPair, set_language_config};
11106 let view = InputView::<EditorMode>::new(cx);
11107 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11108 cx.update(|window, cx| {
11109 set_language_config(
11110 "alpha",
11111 LanguageConfig::default().auto_closing_pairs([AutoClosingPair::new("«", "»")]),
11112 cx,
11113 );
11114 set_language_config(
11115 "beta",
11116 LanguageConfig::default().auto_closing_pairs([AutoClosingPair::new("‹", "›")]),
11117 cx,
11118 );
11119 let first = cx.new(|cx| EditorState::new(window, cx).language("ALPHA"));
11120 let second = cx.new(|cx| EditorState::new(window, cx).language("beta"));
11121 first.update(cx, |state, cx| {
11122 state.replace_text_in_range(None, "«", window, cx);
11123 assert_eq!(state.text().to_string(), "«»");
11124 state.set_value("", window, cx);
11125 state.set_smart_indent(false, window, cx);
11126 state.set_highlighter("beta", cx);
11127 assert!(!state.mode.is_smart_indent());
11128 state.replace_text_in_range(None, "‹", window, cx);
11129 assert_eq!(state.text().to_string(), "‹›");
11130 state.set_value("", window, cx);
11131 state.set_highlighter("unknown", cx);
11132 state.replace_text_in_range(None, "(", window, cx);
11133 assert_eq!(state.text().to_string(), "()");
11134 });
11135 second.update(cx, |state, cx| {
11136 state.replace_text_in_range(None, "‹", window, cx);
11137 assert_eq!(state.text().to_string(), "‹›");
11138 });
11139 });
11140 }
11141
11142 #[gpui::test]
11143 fn test_language_service_replacement_updates_syntax_without_render(cx: &mut TestAppContext) {
11144 use crate::input::{LanguageProvider, SyntaxContextProvider, set_language_provider};
11145 struct StringLanguages(Rc<Cell<usize>>);
11146 impl LanguageProvider for StringLanguages {
11147 fn syntax_context_provider(&self, _: &str) -> Option<Rc<dyn SyntaxContextProvider>> {
11148 self.0.set(self.0.get() + 1);
11149 Some(Rc::new(StringAllProvider))
11150 }
11151 }
11152 struct CodeLanguages;
11153 impl LanguageProvider for CodeLanguages {}
11154 let view = InputView::<EditorMode>::new(cx);
11155 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11156 cx.update(|window, cx| {
11157 let creations = Rc::new(Cell::new(0));
11158 set_language_provider(Rc::new(StringLanguages(creations.clone())), cx);
11159 view.input.update(cx, |state, cx| {
11160 state.replace_text_in_range(None, "(", window, cx);
11161 state.replace_text_in_range(None, "[", window, cx);
11162 assert_eq!(state.text().to_string(), "([");
11163 assert_eq!(
11164 creations.get(),
11165 1,
11166 "retain the document provider between edits"
11167 );
11168 });
11169 set_language_provider(Rc::new(CodeLanguages), cx);
11170 view.input.update(cx, |state, cx| {
11171 state.replace_text_in_range(None, "{", window, cx);
11172 assert_eq!(state.text().to_string(), "([{}");
11173 });
11174 });
11175 }
11176
11177 #[gpui::test]
11178 fn test_language_config_updates_existing_editors(cx: &mut TestAppContext) {
11179 use crate::input::{AutoClosingPair, set_language_config};
11180 let view = InputView::<EditorMode>::new(cx);
11181 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11182 setup_cursors(&mut cx, &view.input, "|");
11183 cx.update(|window, cx| {
11184 view.input.update(cx, |state, cx| {
11185 state.set_highlighter("CUSTOM", cx);
11186 state.set_auto_close(false, window, cx);
11187 state.set_smart_indent(false, window, cx);
11188 });
11189 set_language_config(
11190 "custom",
11191 LanguageConfig::default().auto_closing_pairs([AutoClosingPair::new("«", "»")]),
11192 cx,
11193 );
11194 set_language_config("other", LanguageConfig::default(), cx);
11196 view.input.update(cx, |state, cx| {
11197 assert!(!state.mode.is_auto_close());
11198 assert!(!state.mode.is_smart_indent());
11199 state.set_auto_close(true, window, cx);
11200 state.replace_text_in_range(None, "«", window, cx);
11201 });
11202 });
11203 assert_cursors(&mut cx, &view.input, "«|»");
11204 }
11205
11206 #[gpui::test]
11207 fn test_auto_close_before_is_language_configurable(cx: &mut TestAppContext) {
11208 let view = InputView::<EditorMode>::new(cx);
11209 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11210 setup_cursors(&mut cx, &view.input, "|word");
11211 cx.update(|window, cx| {
11212 view.input.update(cx, |state, cx| {
11213 state.replace_text_in_range(None, "(", window, cx)
11214 });
11215 });
11216 assert_cursors(&mut cx, &view.input, "(|word");
11217 setup_cursors(&mut cx, &view.input, "|word");
11218 cx.update(|window, cx| {
11219 view.input.update(cx, |state, cx| {
11220 crate::input::set_language_config(
11221 state.language_name(),
11222 LanguageConfig::default().auto_close_before("w"),
11223 cx,
11224 );
11225 state.replace_text_in_range(None, "(", window, cx);
11226 });
11227 });
11228 assert_cursors(&mut cx, &view.input, "(|)word");
11229 }
11230
11231 #[gpui::test]
11232 fn test_pair_context_restrictions_are_per_pair(cx: &mut TestAppContext) {
11233 use crate::input::{AutoClosingPair, SyntaxContext};
11234 let view = InputView::<EditorMode>::new(cx);
11235 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11236 setup_cursors(&mut cx, &view.input, "|");
11237 cx.update(|window, cx| {
11238 view.input.update(cx, |state, cx| {
11239 set_test_syntax_provider(std::rc::Rc::new(CommentAllProvider), cx);
11240 crate::input::set_language_config(
11241 state.language_name(),
11242 LanguageConfig::default().auto_closing_pairs([
11243 AutoClosingPair::new("(", ")").not_in([SyntaxContext::String]),
11244 AutoClosingPair::new("[", "]").not_in([SyntaxContext::Comment]),
11245 ]),
11246 cx,
11247 );
11248 state.replace_text_in_range(None, "(", window, cx);
11249 state.replace_text_in_range(None, "[", window, cx);
11250 });
11251 });
11252 assert_cursors(&mut cx, &view.input, "([|)");
11253 }
11254
11255 #[gpui::test]
11256 fn test_multichar_delimiters_insert_delete_and_skip(cx: &mut TestAppContext) {
11257 use crate::input::{AutoClosingPair, SyntaxContext};
11258 struct BlockComment;
11259 impl crate::input::SyntaxContextProvider for BlockComment {
11260 fn context_at(&self, text: &ropey::Rope, _: usize) -> SyntaxContext {
11261 if text.to_string().contains("/*") {
11262 SyntaxContext::Comment
11263 } else {
11264 SyntaxContext::Code
11265 }
11266 }
11267 }
11268 let view = InputView::<EditorMode>::new(cx);
11269 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11270 setup_cursors(&mut cx, &view.input, "|");
11271 cx.update(|window, cx| {
11272 view.input.update(cx, |state, cx| {
11273 set_test_syntax_provider(std::rc::Rc::new(BlockComment), cx);
11274 crate::input::set_language_config(
11275 state.language_name(),
11276 LanguageConfig::default()
11277 .auto_closing_pairs([AutoClosingPair::new("/*", "*/")
11278 .not_in([SyntaxContext::String, SyntaxContext::Comment])]),
11279 cx,
11280 );
11281 state.replace_text_in_range(None, "/", window, cx);
11282 state.replace_text_in_range(None, "*", window, cx);
11283 });
11284 });
11285 assert_cursors(&mut cx, &view.input, "/*|*/");
11286 cx.update(|window, cx| {
11287 view.input
11288 .update(cx, |state, cx| state.backspace(&Backspace, window, cx));
11289 });
11290 assert_cursors(&mut cx, &view.input, "|");
11291 cx.update(|window, cx| {
11292 view.input.update(cx, |state, cx| {
11293 state.undo(&Undo, window, cx);
11294 state.replace_text_in_range(None, "*", window, cx);
11295 state.replace_text_in_range(None, "/", window, cx);
11296 });
11297 });
11298 assert_cursors(&mut cx, &view.input, "/**/|");
11299 }
11300
11301 #[gpui::test]
11302 fn test_auto_closed_pairs_fallback_and_explicit_disable(cx: &mut TestAppContext) {
11303 use crate::input::BracketPair;
11304 let view = InputView::<EditorMode>::new(cx);
11305 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11306 setup_cursors(&mut cx, &view.input, "|");
11307 cx.update(|window, cx| {
11308 view.input.update(cx, |state, cx| {
11309 let mut rules = LanguageConfig::default().brackets([BracketPair::new("«", "»")]);
11310 rules.auto_closing_pairs = None;
11311 crate::input::set_language_config(state.language_name(), rules.clone(), cx);
11312 state.replace_text_in_range(None, "«", window, cx);
11313 crate::input::set_language_config(
11314 state.language_name(),
11315 rules.auto_closing_pairs([]),
11316 cx,
11317 );
11318 state.replace_text_in_range(None, "«", window, cx);
11319 });
11320 });
11321 assert_cursors(&mut cx, &view.input, "««|»");
11322 }
11323
11324 #[gpui::test]
11325 fn test_undo_open_composition_preserves_generated_pairs(cx: &mut TestAppContext) {
11326 use crate::input::{AutoClosingPair, SyntaxContext};
11327 let view = InputView::<EditorMode>::new(cx);
11328 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11329 setup_cursors(&mut cx, &view.input, "|");
11330 cx.update(|window, cx| {
11331 view.input.update(cx, |state, cx| {
11332 crate::input::set_language_config(
11333 state.language_name(),
11334 LanguageConfig::default()
11335 .auto_closing_pairs([
11336 AutoClosingPair::new("/*", "*/").not_in([SyntaxContext::Comment])
11337 ]),
11338 cx,
11339 );
11340 state.replace_text_in_range(None, "/", window, cx);
11341 state.replace_text_in_range(None, "*", window, cx);
11342 set_test_syntax_provider(std::rc::Rc::new(CommentAllProvider), cx);
11343 state.replace_and_mark_text_in_range(None, "x", Some(1..1), window, cx);
11344 state.undo(&Undo, window, cx);
11345 state.replace_text_in_range(None, "*", window, cx);
11346 state.replace_text_in_range(None, "/", window, cx);
11347 });
11348 });
11349 assert_cursors(&mut cx, &view.input, "/**/|");
11350 }
11351
11352 #[gpui::test]
11353 fn test_undo_does_not_promote_literal_comment_delimiters(cx: &mut TestAppContext) {
11354 use crate::input::{AutoClosingPair, SyntaxContext};
11355 let view = InputView::<EditorMode>::new(cx);
11356 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11357 setup_cursors(&mut cx, &view.input, "/*|*/");
11358 cx.update(|window, cx| {
11359 view.input.update(cx, |state, cx| {
11360 set_test_syntax_provider(std::rc::Rc::new(CommentAllProvider), cx);
11361 crate::input::set_language_config(
11362 state.language_name(),
11363 LanguageConfig::default()
11364 .auto_closing_pairs([
11365 AutoClosingPair::new("/*", "*/").not_in([SyntaxContext::Comment])
11366 ]),
11367 cx,
11368 );
11369 state.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
11370 state.undo(&Undo, window, cx);
11371 state.backspace(&Backspace, window, cx);
11372 });
11373 });
11374 assert_cursors(&mut cx, &view.input, "/|*/");
11375 }
11376
11377 #[gpui::test]
11378 fn test_multiple_generated_pairs_retain_their_identity(cx: &mut TestAppContext) {
11379 use crate::input::{AutoClosingPair, SyntaxContext};
11380 struct CommentScope;
11381 impl crate::input::SyntaxContextProvider for CommentScope {
11382 fn context_at(&self, text: &ropey::Rope, offset: usize) -> SyntaxContext {
11383 if text
11384 .to_string()
11385 .find("*/")
11386 .is_some_and(|end| offset < end + 2)
11387 {
11388 SyntaxContext::Comment
11389 } else {
11390 SyntaxContext::Code
11391 }
11392 }
11393 }
11394 let view = InputView::<EditorMode>::new(cx);
11395 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11396 setup_cursors(&mut cx, &view.input, "|");
11397 cx.update(|window, cx| {
11398 view.input.update(cx, |state, cx| {
11399 set_test_syntax_provider(std::rc::Rc::new(CommentScope), cx);
11400 crate::input::set_language_config(
11401 state.language_name(),
11402 LanguageConfig::default().auto_closing_pairs([
11403 AutoClosingPair::new("/*", "*/").not_in([SyntaxContext::Comment]),
11404 AutoClosingPair::new("(", ")").not_in([SyntaxContext::Comment]),
11405 ]),
11406 cx,
11407 );
11408 state.replace_text_in_range(None, "/", window, cx);
11409 state.replace_text_in_range(None, "*", window, cx);
11410 state.set_selected_range(4..4, cx);
11411 state.replace_text_in_range(None, "(", window, cx);
11412 state.set_selected_range(2..2, cx);
11413 state.replace_text_in_range(None, "*", window, cx);
11414 state.replace_text_in_range(None, "/", window, cx);
11415 });
11416 });
11417 assert_cursors(&mut cx, &view.input, "/**/|()");
11418 cx.update(|window, cx| {
11419 view.input.update(cx, |state, cx| {
11420 state.set_selected_range(2..2, cx);
11421 state.backspace(&Backspace, window, cx);
11422 state.undo(&Undo, window, cx);
11423 state.redo(&Redo, window, cx);
11424 });
11425 });
11426 assert_cursors(&mut cx, &view.input, "|()");
11427 }
11428
11429 #[gpui::test]
11430 fn test_replacing_generated_opener_invalidates_the_pair(cx: &mut TestAppContext) {
11431 use crate::input::{AutoClosingPair, SyntaxContext};
11432 let view = InputView::<EditorMode>::new(cx);
11433 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11434 setup_cursors(&mut cx, &view.input, "|");
11435 cx.update(|window, cx| {
11436 view.input.update(cx, |state, cx| {
11437 crate::input::set_language_config(
11438 state.language_name(),
11439 LanguageConfig::default()
11440 .auto_closing_pairs([
11441 AutoClosingPair::new("/*", "*/").not_in([SyntaxContext::Comment])
11442 ]),
11443 cx,
11444 );
11445 state.replace_text_in_range(None, "/", window, cx);
11446 state.replace_text_in_range(None, "*", window, cx);
11447 state.replace_text_in_range(Some(0..2), "//", window, cx);
11448 set_test_syntax_provider(std::rc::Rc::new(CommentAllProvider), cx);
11449 state.replace_text_in_range(None, "*", window, cx);
11450 });
11451 });
11452 assert_cursors(&mut cx, &view.input, "//*|*/");
11453 }
11454
11455 #[gpui::test]
11456 fn test_configuration_preserves_compiled_regex_options(cx: &mut TestAppContext) {
11457 use crate::input::IndentationRules;
11458 let view = InputView::<EditorMode>::new(cx);
11459 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11460 setup_cursors(&mut cx, &view.input, "BEGIN|");
11461 cx.update(|window, cx| {
11462 view.input.update(cx, |state, cx| {
11463 let rules = |insensitive| {
11464 LanguageConfig::default().indentation_rules(IndentationRules::new(
11465 regex::RegexBuilder::new("begin$")
11466 .case_insensitive(insensitive)
11467 .build()
11468 .unwrap(),
11469 regex::Regex::new("^end").unwrap(),
11470 ))
11471 };
11472 crate::input::set_language_config(state.language_name(), rules(false), cx);
11473 crate::input::set_language_config(state.language_name(), rules(true), cx);
11474 state.enter(
11475 &Enter {
11476 secondary: false,
11477 shift: false,
11478 },
11479 window,
11480 cx,
11481 );
11482 });
11483 });
11484 assert_cursors(&mut cx, &view.input, "BEGIN\n |");
11485 }
11486
11487 #[gpui::test]
11488 fn test_indent_patterns_are_applied_on_enter(cx: &mut TestAppContext) {
11489 use crate::input::IndentationRules;
11490 let view = InputView::<EditorMode>::new(cx);
11491 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11492 let rules = LanguageConfig::default().indentation_rules(IndentationRules::new(
11493 regex::Regex::new(r"\bbegin\s*$").unwrap(),
11494 regex::Regex::new(r"^\s*end\b").unwrap(),
11495 ));
11496 for (before, expected) in [("begin|", "begin\n |"), (" |end", " \n|end")] {
11497 setup_cursors(&mut cx, &view.input, before);
11498 cx.update(|window, cx| {
11499 view.input.update(cx, |state, cx| {
11500 crate::input::set_language_config(state.language_name(), rules.clone(), cx);
11501 state.enter(
11502 &Enter {
11503 secondary: false,
11504 shift: false,
11505 },
11506 window,
11507 cx,
11508 );
11509 });
11510 });
11511 assert_cursors(&mut cx, &view.input, expected);
11512 }
11513 }
11514
11515 #[gpui::test]
11516 fn test_custom_language_config(cx: &mut TestAppContext) {
11517 let view = InputView::<EditorMode>::new(cx);
11518 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11519 cx.update(|_window, cx| {
11520 view.input.update(cx, |state, cx| {
11521 crate::input::set_language_config(
11522 state.language_name(),
11523 crate::input::language_config::LanguageConfig::default()
11524 .brackets([crate::input::BracketPair::new("«", "»")])
11525 .auto_closing_pairs([crate::input::AutoClosingPair::new("«", "»")]),
11526 cx,
11527 );
11528 });
11529 });
11530 setup_cursors(&mut cx, &view.input, "|");
11532 cx.update(|window, cx| {
11533 view.input.update(cx, |state, cx| {
11534 state.replace_text_in_range(None, "«", window, cx);
11535 });
11536 });
11537 assert_cursors(&mut cx, &view.input, "«|»");
11538 cx.update(|window, cx| {
11540 view.input.update(cx, |state, cx| {
11541 state.set_value("\n", window, cx);
11542 state.set_selected_range(0..0, cx);
11543 state.replace_text_in_range(None, "(", window, cx);
11544 });
11545 });
11546 assert_cursors(&mut cx, &view.input, "(|");
11547 }
11548
11549 #[gpui::test]
11550 fn test_skip_records_no_history(cx: &mut TestAppContext) {
11551 let view = InputView::<EditorMode>::new(cx);
11552 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11553 setup_cursors(&mut cx, &view.input, "(a|)");
11554 cx.update(|window, cx| {
11555 view.input.update(cx, |state, cx| {
11556 state.replace_text_in_range(None, "b", window, cx);
11558 });
11559 });
11560 assert_cursors(&mut cx, &view.input, "(ab|)");
11561 cx.update(|window, cx| {
11562 view.input.update(cx, |state, cx| {
11563 state.replace_text_in_range(None, ")", window, cx);
11564 });
11565 });
11566 assert_cursors(&mut cx, &view.input, "(ab)|");
11568 cx.update(|window, cx| {
11570 view.input
11571 .update(cx, |state, cx| state.undo(&Undo, window, cx));
11572 });
11573 assert_cursors(&mut cx, &view.input, "(a|)");
11574 }
11575
11576 #[gpui::test]
11577 fn test_pair_insert_undo_removes_both(cx: &mut TestAppContext) {
11578 let view = InputView::<EditorMode>::new(cx);
11579 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11580 setup_cursors(&mut cx, &view.input, "|");
11581 cx.update(|window, cx| {
11582 view.input.update(cx, |state, cx| {
11583 state.replace_text_in_range(None, "(", window, cx);
11584 });
11585 });
11586 assert_cursors(&mut cx, &view.input, "(|)");
11587 cx.update(|window, cx| {
11589 view.input
11590 .update(cx, |state, cx| state.undo(&Undo, window, cx));
11591 });
11592 assert_cursors(&mut cx, &view.input, "|");
11593 }
11594
11595 #[gpui::test]
11596 fn test_escaped_quote_does_not_skip(cx: &mut TestAppContext) {
11597 let view = InputView::<EditorMode>::new(cx);
11598 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11599 setup_cursors(&mut cx, &view.input, "\"hello\\|\"");
11601 cx.update(|window, cx| {
11602 view.input.update(cx, |state, cx| {
11603 state.replace_text_in_range(None, "\"", window, cx);
11604 });
11605 });
11606 assert_cursors(&mut cx, &view.input, "\"hello\\\"|\"");
11607 }
11608
11609 #[gpui::test]
11610 fn test_even_backslashes_still_skip(cx: &mut TestAppContext) {
11611 let view = InputView::<EditorMode>::new(cx);
11612 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11613 setup_cursors(&mut cx, &view.input, "\"hello\\\\|\"");
11615 cx.update(|window, cx| {
11616 view.input.update(cx, |state, cx| {
11617 state.replace_text_in_range(None, "\"", window, cx);
11618 });
11619 });
11620 assert_cursors(&mut cx, &view.input, "\"hello\\\\\"|");
11621 }
11622
11623 #[gpui::test]
11624 fn test_editor_options_survive_configuration_replacement(cx: &mut TestAppContext) {
11625 use crate::input::{BracketPair, LanguageConfig};
11626
11627 let view = InputView::<EditorMode>::new(cx);
11628 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11629 cx.update(|window, cx| {
11630 view.input.update(cx, |state, cx| {
11631 state.set_smart_indent(false, window, cx);
11633 crate::input::set_language_config(
11634 state.language_name(),
11635 LanguageConfig::default().brackets([BracketPair::new("«", "»")]),
11636 cx,
11637 );
11638 });
11639 });
11640 view.input.read_with(&cx, |state, _| {
11641 assert!(!state.mode.is_smart_indent(), "editor option preserved");
11642 assert!(state.mode.is_auto_close());
11643 let pairs = &state.mode.language_config().unwrap().brackets;
11644 assert_eq!(pairs.len(), 1, "pairs follow configuration");
11645 assert_eq!(pairs[0].open.as_ref(), "«");
11646 });
11647 }
11648
11649 struct CommentAllProvider;
11650 impl crate::input::SyntaxContextProvider for CommentAllProvider {
11651 fn context_at(&self, _text: &ropey::Rope, _offset: usize) -> crate::input::SyntaxContext {
11652 crate::input::SyntaxContext::Comment
11653 }
11654 }
11655
11656 struct StringAllProvider;
11657 impl crate::input::SyntaxContextProvider for StringAllProvider {
11658 fn context_at(&self, _text: &ropey::Rope, _offset: usize) -> crate::input::SyntaxContext {
11659 crate::input::SyntaxContext::String
11660 }
11661 }
11662
11663 #[gpui::test]
11664 fn test_smart_indent_suppressed_in_strings(cx: &mut TestAppContext) {
11665 use std::rc::Rc;
11666
11667 let view = InputView::<EditorMode>::new(cx);
11668 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11669 cx.update(|_window, cx| {
11670 set_test_syntax_provider(Rc::new(StringAllProvider), cx);
11671 });
11672 setup_cursors(&mut cx, &view.input, " x = {|");
11674 cx.update(|window, cx| {
11675 view.input.update(cx, |state, cx| {
11676 state.enter(
11677 &Enter {
11678 secondary: false,
11679 shift: false,
11680 },
11681 window,
11682 cx,
11683 );
11684 });
11685 });
11686 assert_cursors(&mut cx, &view.input, " x = {\n |");
11687 }
11688
11689 #[gpui::test]
11690 fn test_split_suppressed_in_strings(cx: &mut TestAppContext) {
11691 use std::rc::Rc;
11692
11693 let view = InputView::<EditorMode>::new(cx);
11694 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11695 cx.update(|_window, cx| {
11696 set_test_syntax_provider(Rc::new(StringAllProvider), cx);
11697 });
11698 setup_cursors(&mut cx, &view.input, "{|}");
11700 cx.update(|window, cx| {
11701 view.input.update(cx, |state, cx| {
11702 state.enter(
11703 &Enter {
11704 secondary: false,
11705 shift: false,
11706 },
11707 window,
11708 cx,
11709 );
11710 });
11711 });
11712 assert_cursors(&mut cx, &view.input, "{\n|}");
11713 }
11714
11715 #[gpui::test]
11716 fn test_comment_context_disables_pairing(cx: &mut TestAppContext) {
11717 use std::rc::Rc;
11718
11719 let view = InputView::<EditorMode>::new(cx);
11720 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11721 cx.update(|_window, cx| {
11722 set_test_syntax_provider(Rc::new(CommentAllProvider), cx);
11723 });
11724 setup_cursors(&mut cx, &view.input, "|");
11725 cx.update(|window, cx| {
11726 view.input.update(cx, |state, cx| {
11727 state.replace_text_in_range(None, "(", window, cx);
11728 });
11729 });
11730 assert_cursors(&mut cx, &view.input, "(|");
11732 }
11733
11734 #[gpui::test]
11735 fn test_backspace_deletes_empty_pair(cx: &mut TestAppContext) {
11736 let view = InputView::<EditorMode>::new(cx);
11737 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11738 setup_cursors(&mut cx, &view.input, "{|}");
11739 cx.update(|window, cx| {
11740 view.input
11741 .update(cx, |state, cx| state.backspace(&Backspace, window, cx));
11742 });
11743 assert_cursors(&mut cx, &view.input, "|");
11744 }
11745
11746 #[gpui::test]
11747 fn test_backspace_keeps_closer_with_content(cx: &mut TestAppContext) {
11748 let view = InputView::<EditorMode>::new(cx);
11749 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11750 setup_cursors(&mut cx, &view.input, "{d|}");
11751 cx.update(|window, cx| {
11752 view.input
11753 .update(cx, |state, cx| state.backspace(&Backspace, window, cx));
11754 });
11755 assert_cursors(&mut cx, &view.input, "{|}");
11757 }
11758
11759 #[gpui::test]
11760 fn test_enter_splits_brackets(cx: &mut TestAppContext) {
11761 let view = InputView::<EditorMode>::new(cx);
11762 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11763 setup_cursors(&mut cx, &view.input, "{|}");
11764 cx.update(|window, cx| {
11765 view.input.update(cx, |state, cx| {
11766 state.enter(
11767 &Enter {
11768 secondary: false,
11769 shift: false,
11770 },
11771 window,
11772 cx,
11773 );
11774 });
11775 });
11776 assert_cursors(&mut cx, &view.input, "{\n |\n}");
11777 view.input.read_with(&cx, |state, _| {
11779 assert!(state.active_selection().is_empty());
11780 });
11781 cx.update(|window, cx| {
11782 view.input.update(cx, |state, cx| {
11783 state.replace_text_in_range(None, "x", window, cx);
11784 });
11785 });
11786 assert_cursors(&mut cx, &view.input, "{\n x|\n}");
11787 }
11788
11789 #[gpui::test]
11790 fn test_enter_smart_indent_after_brace(cx: &mut TestAppContext) {
11791 let view = InputView::<EditorMode>::new(cx);
11792 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11793 setup_cursors(&mut cx, &view.input, "fn main() {|");
11794 cx.update(|window, cx| {
11795 view.input.update(cx, |state, cx| {
11796 state.enter(
11797 &Enter {
11798 secondary: false,
11799 shift: false,
11800 },
11801 window,
11802 cx,
11803 );
11804 });
11805 });
11806 assert_cursors(&mut cx, &view.input, "fn main() {\n |");
11807 }
11808
11809 #[gpui::test]
11810 fn test_block_indent_tracks_all_preceding_edits(cx: &mut TestAppContext) {
11811 let view = multi_line(cx);
11812 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11813 setup_cursors(&mut cx, &view.input, "|ab\n|cd\n|ef");
11814 cx.update(|window, cx| {
11815 view.input
11816 .update(cx, |state, cx| state.indent(true, window, cx));
11817 });
11818 assert_cursors(&mut cx, &view.input, " |ab\n |cd\n |ef");
11819 cx.update(|window, cx| {
11820 view.input
11821 .update(cx, |state, cx| state.outdent(true, window, cx));
11822 });
11823 assert_cursors(&mut cx, &view.input, "|ab\n|cd\n|ef");
11824 }
11825
11826 #[gpui::test]
11827 fn test_block_outdent_clamps_cursor_inside_indent(cx: &mut TestAppContext) {
11828 let view = multi_line(cx);
11829 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11830 setup_cursors(&mut cx, &view.input, "ab\n | cd");
11831 cx.update(|window, cx| {
11832 view.input
11833 .update(cx, |state, cx| state.outdent(true, window, cx));
11834 });
11835 assert_cursors(&mut cx, &view.input, "ab\n|cd");
11836 }
11837
11838 #[gpui::test]
11839 fn test_ime_restores_original_selection(cx: &mut TestAppContext) {
11840 let view = InputView::build(cx, |state| state);
11841 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11842 cx.update(|window, cx| {
11843 view.input.update(cx, |state, cx| {
11844 state.set_value("abc", window, cx);
11845 state.set_selected_range(1..2, cx);
11846 state.replace_and_mark_text_in_range(None, "ni", None, window, cx);
11847 state.replace_text_in_range(None, "你", window, cx);
11848 state.undo(&Undo, window, cx);
11849 assert_eq!(state.value(), "abc");
11850 assert_eq!(state.selected_range(), 1..2);
11851 state.redo(&Redo, window, cx);
11852 assert_eq!(state.selected_range(), 4..4);
11853 });
11854 });
11855 }
11856
11857 #[gpui::test]
11858 fn test_noop_does_not_change_redo_selection(cx: &mut TestAppContext) {
11859 let view = multi_line(cx);
11860 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11861 cx.update(|window, cx| {
11862 view.input.update(cx, |state, cx| {
11863 state.set_value("abc", window, cx);
11864 state.set_selected_range(1..1, cx);
11865 state.replace_text_in_range(None, "X", window, cx);
11866 state.set_selected_range(0..0, cx);
11867 state.replace_text_in_range(None, "", window, cx);
11868 state.undo(&Undo, window, cx);
11869 state.redo(&Redo, window, cx);
11870 assert_eq!(state.value(), "aXbc");
11871 assert_eq!(state.selected_range(), 2..2);
11872 });
11873 });
11874 }
11875
11876 #[gpui::test]
11877 fn test_multi_cursor_insert_text(cx: &mut TestAppContext) {
11878 let view = multi_line(cx);
11879 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11880 let input = view.input;
11881
11882 setup_cursors(&mut cx, &input, "|hello |world|");
11883 cx.update(|window, cx| {
11884 input.update(cx, |state, cx| {
11885 state.replace_text_in_range(None, ">>>", window, cx);
11886 });
11887 });
11888 assert_cursors(&mut cx, &input, ">>>|hello >>>|world>>>|");
11889 }
11890
11891 #[gpui::test]
11892 fn test_multi_cursor_delete_backward(cx: &mut TestAppContext) {
11893 let view = multi_line(cx);
11894 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11895 let input = view.input;
11896
11897 setup_cursors(&mut cx, &input, "|islands| cars|");
11898 cx.update(|window, cx| {
11899 input.update(cx, |state, cx| {
11900 state.backspace(&Backspace, window, cx);
11901 });
11902 });
11903 assert_cursors(&mut cx, &input, "|island| car|");
11905 }
11906
11907 #[gpui::test]
11908 fn test_multi_cursor_delete_forward_merges(cx: &mut TestAppContext) {
11909 let view = multi_line(cx);
11910 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11911 let input = view.input;
11912
11913 setup_cursors(&mut cx, &input, "hello| |world");
11914 cx.update(|window, cx| {
11915 input.update(cx, |state, cx| {
11916 state.delete(&Delete, window, cx);
11917 });
11918 });
11919 assert_cursors(&mut cx, &input, "hello|orld");
11921 }
11922
11923 #[gpui::test]
11924 fn test_multi_cursor_multiline_insert_and_delete(cx: &mut TestAppContext) {
11925 let view = multi_line(cx);
11926 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11927 let input = view.input;
11928
11929 setup_cursors(&mut cx, &input, "|1\n|2\n|3");
11930 cx.update(|window, cx| {
11931 input.update(cx, |state, cx| {
11932 state.replace_text_in_range(None, "a", window, cx);
11933 });
11934 });
11935 assert_cursors(&mut cx, &input, "a|1\na|2\na|3");
11936
11937 input.read_with(&cx, |state, _| {
11939 assert_eq!(state.undo_manager.undo_count(), 1);
11940 });
11941
11942 cx.update(|window, cx| {
11943 input.update(cx, |state, cx| {
11944 state.backspace(&Backspace, window, cx);
11945 });
11946 });
11947 assert_cursors(&mut cx, &input, "|1\n|2\n|3");
11948 }
11949
11950 #[gpui::test]
11951 fn test_add_cursor_below_preserves_column(cx: &mut TestAppContext) {
11952 let view = multi_line(cx);
11953 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11954 let input = view.input;
11955
11956 setup_cursors(&mut cx, &input, "ab|cd\nabcd");
11957 cx.update(|window, cx| {
11958 input.update(cx, |state, cx| {
11959 state.add_cursor_below(&AddCursorBelow, window, cx);
11960 });
11961 });
11962 assert_cursors(&mut cx, &input, "ab|cd\nab|cd");
11963 }
11964
11965 #[gpui::test]
11966 fn test_add_cursor_at_rejects_duplicates(cx: &mut TestAppContext) {
11967 let view = multi_line(cx);
11968 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11969 let input = view.input;
11970
11971 setup_cursors(&mut cx, &input, "he|llo");
11972 cx.update(|_, cx| {
11973 input.update(cx, |state, cx| {
11974 state.add_cursor_at(2, cx);
11976 assert_eq!(state.selections.len(), 1);
11977 state.add_cursor_at(4, cx);
11979 assert_eq!(state.selections.len(), 2);
11980 });
11981 });
11982 }
11983
11984 #[gpui::test]
11985 fn test_multi_cursor_undo_redo_restores_selections(cx: &mut TestAppContext) {
11986 let view = multi_line(cx);
11987 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11988 let input = view.input;
11989
11990 setup_cursors(&mut cx, &input, "|1\n|2\n|3");
11991 cx.update(|window, cx| {
11992 input.update(cx, |state, cx| {
11993 state.replace_text_in_range(None, "a", window, cx);
11994 });
11995 });
11996 assert_cursors(&mut cx, &input, "a|1\na|2\na|3");
11997
11998 cx.update(|window, cx| {
11999 input.update(cx, |state, cx| {
12000 state.undo(&Undo, window, cx);
12001 });
12002 });
12003 assert_cursors(&mut cx, &input, "|1\n|2\n|3");
12004
12005 cx.update(|window, cx| {
12006 input.update(cx, |state, cx| {
12007 state.redo(&Redo, window, cx);
12008 });
12009 });
12010 assert_cursors(&mut cx, &input, "a|1\na|2\na|3");
12011 }
12012
12013 #[gpui::test]
12014 fn test_multi_cursor_undo_redo_different_line_lengths(cx: &mut TestAppContext) {
12015 let view = multi_line(cx);
12016 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
12017 let input = view.input;
12018
12019 setup_cursors(&mut cx, &input, "abc123|\nabc12345|\nabc1234567|");
12020 cx.update(|window, cx| {
12021 input.update(cx, |state, cx| {
12022 state.replace_text_in_range(None, "a", window, cx);
12023 });
12024 });
12025 assert_cursors(&mut cx, &input, "abc123a|\nabc12345a|\nabc1234567a|");
12026
12027 cx.update(|window, cx| {
12028 input.update(cx, |state, cx| {
12029 state.undo(&Undo, window, cx);
12030 });
12031 });
12032 assert_cursors(&mut cx, &input, "abc123|\nabc12345|\nabc1234567|");
12033
12034 cx.update(|window, cx| {
12035 input.update(cx, |state, cx| {
12036 state.redo(&Redo, window, cx);
12037 });
12038 });
12039 assert_cursors(&mut cx, &input, "abc123a|\nabc12345a|\nabc1234567a|");
12040 }
12041
12042 #[gpui::test]
12043 fn test_multi_cursor_undo_multiple_inserts(cx: &mut TestAppContext) {
12044 let view = multi_line(cx);
12045 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
12046 let input = view.input;
12047
12048 setup_cursors(&mut cx, &input, "|1\n|2\n|3");
12049 for ch in ['a', 'b', 'c'] {
12050 cx.update(|window, cx| {
12051 input.update(cx, |state, cx| {
12052 state.replace_text_in_range(None, &ch.to_string(), window, cx);
12053 });
12054 });
12055 }
12056 assert_cursors(&mut cx, &input, "abc|1\nabc|2\nabc|3");
12057
12058 cx.update(|window, cx| {
12060 input.update(cx, |state, cx| {
12061 assert_eq!(state.undo_manager.undo_count(), 1);
12062 state.undo(&Undo, window, cx);
12063 });
12064 });
12065 assert_cursors(&mut cx, &input, "|1\n|2\n|3");
12066
12067 cx.update(|window, cx| {
12068 input.update(cx, |state, cx| {
12069 state.redo(&Redo, window, cx);
12070 });
12071 });
12072 assert_cursors(&mut cx, &input, "abc|1\nabc|2\nabc|3");
12073 }
12074
12075 #[gpui::test]
12076 fn test_multi_cursor_backspace_run_is_one_undo(cx: &mut TestAppContext) {
12077 let view = multi_line(cx);
12078 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
12079 let input = view.input;
12080
12081 setup_cursors(&mut cx, &input, "abc|1\nabc|2\nabc|3");
12082 for _ in 0..3 {
12083 cx.update(|window, cx| {
12084 input.update(cx, |state, cx| {
12085 state.backspace(&Backspace, window, cx);
12086 });
12087 });
12088 }
12089 assert_cursors(&mut cx, &input, "|1\n|2\n|3");
12090
12091 cx.update(|window, cx| {
12092 input.update(cx, |state, cx| {
12093 assert_eq!(state.undo_manager.undo_count(), 1);
12094 state.undo(&Undo, window, cx);
12095 });
12096 });
12097 assert_cursors(&mut cx, &input, "abc|1\nabc|2\nabc|3");
12098 }
12099
12100 #[gpui::test]
12101 fn test_adding_a_cursor_splits_the_typing_gesture(cx: &mut TestAppContext) {
12102 let view = multi_line(cx);
12103 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
12104 let input = view.input;
12105
12106 setup_cursors(&mut cx, &input, "|1\n2\n3");
12107 cx.update(|window, cx| {
12108 input.update(cx, |state, cx| {
12109 state.replace_text_in_range(None, "a", window, cx);
12110 state.add_cursor_below(&AddCursorBelow, window, cx);
12111 state.replace_text_in_range(None, "b", window, cx);
12112 });
12113 });
12114 assert_cursors(&mut cx, &input, "ab|1\n2b|\n3");
12115
12116 cx.update(|window, cx| {
12118 input.update(cx, |state, cx| {
12119 state.undo(&Undo, window, cx);
12120 });
12121 });
12122 assert_cursors(&mut cx, &input, "a|1\n2|\n3");
12123 }
12124
12125 #[gpui::test]
12126 fn test_multi_cursor_indent_is_one_undo(cx: &mut TestAppContext) {
12127 let view = multi_line(cx);
12128 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
12129 let input = view.input;
12130
12131 setup_cursors(&mut cx, &input, "|1\n|2\n|3");
12132 cx.update(|window, cx| {
12133 input.update(cx, |state, cx| {
12134 state.indent_inline(&IndentInline, window, cx);
12135 assert_eq!(state.undo_manager.undo_count(), 1);
12136 state.undo(&Undo, window, cx);
12137 });
12138 });
12139 assert_cursors(&mut cx, &input, "|1\n|2\n|3");
12140 }
12141
12142 #[gpui::test]
12143 fn test_multi_cursor_indent_then_outdent_roundtrips(cx: &mut TestAppContext) {
12144 let view = multi_line(cx);
12145 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
12146 let input = view.input;
12147
12148 setup_cursors(&mut cx, &input, "|1\n|2\n|3");
12150 cx.update(|window, cx| {
12151 input.update(cx, |state, cx| {
12152 state.indent(false, window, cx);
12153 });
12154 });
12155 assert_cursors(&mut cx, &input, " |1\n |2\n |3");
12156
12157 cx.update(|window, cx| {
12158 input.update(cx, |state, cx| {
12159 state.outdent(false, window, cx);
12160 });
12161 });
12162 assert_cursors(&mut cx, &input, "|1\n|2\n|3");
12163 }
12164
12165 #[gpui::test]
12166 fn test_inline_outdent_only_removes_line_indentation(cx: &mut TestAppContext) {
12167 let view = multi_line(cx);
12168 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
12169 let input = view.input;
12170
12171 setup_cursors(&mut cx, &input, "1|2\n1|2");
12174 cx.update(|window, cx| {
12175 input.update(cx, |state, cx| {
12176 state.indent(false, window, cx);
12177 });
12178 });
12179 assert_cursors(&mut cx, &input, "1 |2\n1 |2");
12180
12181 cx.update(|window, cx| {
12182 input.update(cx, |state, cx| {
12183 state.outdent(false, window, cx);
12184 });
12185 });
12186 assert_cursors(&mut cx, &input, "1 |2\n1 |2");
12187
12188 setup_cursors(&mut cx, &input, " 1|2\n 1|2");
12190 cx.update(|window, cx| {
12191 input.update(cx, |state, cx| {
12192 state.outdent(false, window, cx);
12193 });
12194 });
12195 assert_cursors(&mut cx, &input, "1|2\n1|2");
12196 }
12197
12198 #[gpui::test]
12199 fn test_readonly_multi_cursor_commands_leave_state_unchanged(cx: &mut TestAppContext) {
12200 let view = multi_line(cx);
12201 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
12202 let input = view.input;
12203
12204 setup_cursors(&mut cx, &input, "a|b\nc|d");
12205 cx.update(|window, cx| {
12206 input.update(cx, |state, cx| {
12207 let before: Vec<_> = state.selections.iter().copied().collect();
12208 state.set_readonly(true, cx);
12209 state.backspace(&Backspace, window, cx);
12210 state.indent_inline(&IndentInline, window, cx);
12211
12212 assert_eq!(state.value(), "ab\ncd\n");
12213 assert_eq!(state.selections.iter().copied().collect::<Vec<_>>(), before);
12214 });
12215 });
12216 }
12217
12218 #[gpui::test]
12219 fn test_multi_cursor_edit_preserves_the_active_cursor(cx: &mut TestAppContext) {
12220 let view = multi_line(cx);
12221 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
12222 let input = view.input;
12223
12224 setup_cursors(&mut cx, &input, "x|\n|y");
12225 cx.update(|window, cx| {
12226 input.update(cx, |state, cx| {
12227 let mut selections: Vec<_> = state.selections.iter().copied().collect();
12228 selections.swap(0, 1);
12229 state.selections.replace_all(selections);
12230 let active_id = state.active_selection().id;
12231 state.replace_text_in_range(None, "!", window, cx);
12232 assert_eq!(state.active_selection().id, active_id);
12233 });
12234 });
12235 }
12236
12237 #[gpui::test]
12238 fn test_block_indent_outdent_with_selection(cx: &mut TestAppContext) {
12239 let view = multi_line(cx);
12240 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
12241 let input = view.input;
12242
12243 cx.update(|window, cx| {
12244 input.update(cx, |state, cx| {
12245 state.set_value("line1\nline2\nline3", window, cx);
12246 let id = state.selections.generate_id();
12247 state
12248 .selections
12249 .replace_all(vec![CursorSelection::new(id, 0, 17)]);
12250 cx.notify();
12251 });
12252 });
12253
12254 cx.update(|window, cx| {
12255 input.update(cx, |state, cx| {
12256 state.indent(true, window, cx);
12257 });
12258 });
12259 input.read_with(&cx, |state, _| {
12260 assert_eq!(state.text.to_string(), " line1\n line2\n line3");
12261 });
12262
12263 cx.update(|window, cx| {
12264 input.update(cx, |state, cx| {
12265 state.outdent(true, window, cx);
12266 });
12267 });
12268 input.read_with(&cx, |state, _| {
12269 assert_eq!(state.text.to_string(), "line1\nline2\nline3");
12270 });
12271 }
12272
12273 #[gpui::test]
12274 fn test_multi_cursor_word_movement(cx: &mut TestAppContext) {
12275 let view = multi_line(cx);
12276 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
12277 let input = view.input;
12278
12279 setup_cursors(
12280 &mut cx,
12281 &input,
12282 "on|e two three\none t|wo three\non|e two three",
12283 );
12284
12285 cx.update(|window, cx| {
12286 input.update(cx, |state, cx| {
12287 state.move_to_next_word(&MoveToNextWord, window, cx);
12288 });
12289 });
12290 assert_cursors(
12291 &mut cx,
12292 &input,
12293 "one| two three\none two| three\none| two three",
12294 );
12295
12296 cx.update(|window, cx| {
12297 input.update(cx, |state, cx| {
12298 state.move_to_previous_word(&MoveToPreviousWord, window, cx);
12299 });
12300 });
12301 assert_cursors(
12302 &mut cx,
12303 &input,
12304 "|one two three\none |two three\n|one two three",
12305 );
12306
12307 cx.update(|window, cx| {
12309 input.update(cx, |state, cx| {
12310 state.move_to_end(&MoveToEnd, window, cx);
12311 });
12312 });
12313 input.read_with(&cx, |state, _| {
12314 let cursors: Vec<usize> = state.selections.iter().map(|s| s.cursor_offset()).collect();
12315 assert_eq!(cursors, vec![state.text.len()]);
12316 });
12317
12318 cx.update(|window, cx| {
12319 input.update(cx, |state, cx| {
12320 state.move_to_start(&MoveToStart, window, cx);
12321 });
12322 });
12323 input.read_with(&cx, |state, _| {
12324 let cursors: Vec<usize> = state.selections.iter().map(|s| s.cursor_offset()).collect();
12325 assert_eq!(cursors, vec![0]);
12326 });
12327 }
12328
12329 #[gpui::test]
12330 fn test_multi_cursor_selection_commands(cx: &mut TestAppContext) {
12331 let view = multi_line(cx);
12332 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
12333 let input = view.input;
12334
12335 setup_cursors(
12336 &mut cx,
12337 &input,
12338 "on|e two three\none t|wo three\non|e two three",
12339 );
12340 cx.update(|window, cx| {
12341 input.update(cx, |state, cx| {
12342 state.select_to_start_of_line(&SelectToStartOfLine, window, cx);
12343 });
12344 });
12345 assert_cursors(
12346 &mut cx,
12347 &input,
12348 "|one two three\n|one two three\n|one two three",
12349 );
12350
12351 setup_cursors(
12353 &mut cx,
12354 &input,
12355 "on|e two three\none t|wo three\non|e two three",
12356 );
12357 cx.update(|window, cx| {
12358 input.update(cx, |state, cx| {
12359 state.select_to_start(&SelectToStart, window, cx);
12360 });
12361 });
12362 input.read_with(&cx, |state, _| {
12363 let cursors: Vec<usize> = state.selections.iter().map(|s| s.cursor_offset()).collect();
12364 assert_eq!(cursors, vec![0]);
12365 });
12366 }
12367
12368 #[gpui::test]
12369 fn test_multi_cursor_replace_selection(cx: &mut TestAppContext) {
12370 let view = multi_line(cx);
12371 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
12372 let input = view.input;
12373
12374 setup_cursors(&mut cx, &input, "|a\n|b\n|c");
12375 cx.update(|window, cx| {
12376 input.update(cx, |state, cx| {
12377 state.select_right(&SelectRight, window, cx);
12378 });
12379 });
12380 input.read_with(&cx, |state, _| {
12381 let ranges: Vec<_> = state.selections.iter().map(|s| (s.start, s.end)).collect();
12382 assert_eq!(ranges, vec![(0, 1), (2, 3), (4, 5)]);
12383 });
12384
12385 cx.update(|window, cx| {
12386 input.update(cx, |state, cx| {
12387 state.replace_text_in_range(None, "x", window, cx);
12388 });
12389 });
12390 assert_cursors(&mut cx, &input, "x|\nx|\nx|");
12391 }
12392
12393 #[gpui::test]
12394 fn test_multi_cursor_escape_collapses(cx: &mut TestAppContext) {
12395 let view = multi_line(cx);
12396 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
12397 let input = view.input;
12398
12399 setup_cursors(&mut cx, &input, "|a\n|b\n|c");
12400 cx.update(|window, cx| {
12401 input.update(cx, |state, cx| {
12402 state.escape(&Escape, window, cx);
12403 });
12404 });
12405 input.read_with(&cx, |state, _| {
12406 assert_eq!(state.selections.len(), 1);
12407 });
12408 }
12409
12410 #[gpui::test]
12411 fn test_build_columnar_selection(cx: &mut TestAppContext) {
12412 let view = multi_line(cx);
12413 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
12414 let input = view.input;
12415
12416 setup_cursors(&mut cx, &input, "abcd\nabcd\nabcd");
12417 cx.update(|_, cx| {
12418 input.update(cx, |state, cx| {
12419 state.build_columnar_selection(
12421 ColumnarPoint::new(1, 0),
12422 ColumnarPoint::new(13, 0),
12423 cx,
12424 );
12425 let ranges: Vec<_> = state.selections.iter().map(|s| (s.start, s.end)).collect();
12426 assert_eq!(ranges, vec![(1, 3), (6, 8), (11, 13)]);
12427 });
12428 });
12429 }
12430
12431 #[gpui::test]
12432 fn test_columnar_selection_keeps_width_over_short_rows(cx: &mut TestAppContext) {
12433 let view = multi_line(cx);
12434 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
12435 let input = view.input;
12436
12437 setup_cursors(&mut cx, &input, "abcdef\nab\nabcdef");
12438 cx.update(|_, cx| {
12439 input.update(cx, |state, cx| {
12440 state.build_columnar_selection(
12443 ColumnarPoint::new(1, 0),
12444 ColumnarPoint::new(9, 3),
12445 cx,
12446 );
12447 let ranges: Vec<_> = state.selections.iter().map(|s| (s.start, s.end)).collect();
12448 assert_eq!(ranges, vec![(1, 5), (8, 9)]);
12449 });
12450 });
12451 }
12452
12453 #[gpui::test]
12454 fn test_multi_cursor_paste_distributes_lines(cx: &mut TestAppContext) {
12455 let view = multi_line(cx);
12456 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
12457 let input = view.input;
12458
12459 setup_cursors(&mut cx, &input, "|1\n|2\n|3");
12460 cx.update(|_, cx| {
12461 cx.write_to_clipboard(ClipboardItem::new_string("x\ny\nz".to_string()));
12462 });
12463 cx.update(|window, cx| {
12464 input.update(cx, |state, cx| {
12465 state.paste(&Paste, window, cx);
12466 });
12467 });
12468 assert_cursors(&mut cx, &input, "x|1\ny|2\nz|3");
12470 }
12471
12472 #[gpui::test]
12473 fn test_paste_without_text_leaves_the_selection_alone(cx: &mut TestAppContext) {
12474 use gpui::{ClipboardEntry, Image, ImageFormat};
12475
12476 let input_view = InputView::build(cx, |state| state);
12477 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
12478 let input = input_view.input;
12479
12480 cx.update(|window, cx| {
12481 input.update(cx, |state, cx| {
12482 state.set_value("hello world", window, cx);
12483 state.select_all(window, cx);
12484 cx.write_to_clipboard(ClipboardItem {
12485 entries: vec![ClipboardEntry::Image(Image::from_bytes(
12486 ImageFormat::Png,
12487 vec![0x89, b'P', b'N', b'G'],
12488 ))],
12489 });
12490
12491 state.paste(&Paste, window, cx);
12494 assert_eq!(state.value(), "hello world");
12495 assert_eq!(state.selected_range(), 0..11);
12496 });
12497 });
12498 }
12499
12500 #[gpui::test]
12501 fn test_paste_target_tracks_edits_and_selections(cx: &mut TestAppContext) {
12502 let view = multi_line(cx);
12503 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
12504 let input = view.input;
12505
12506 setup_cursors(&mut cx, &input, "ab|c");
12507 let target = input.read_with(&cx, |state, _| state.paste_target());
12508 assert_eq!(
12510 input.read_with(&cx, |state, _| state.paste_target()),
12511 target
12512 );
12513
12514 cx.update(|_, cx| {
12516 input.update(cx, |state, cx| {
12517 let id = state.selections.generate_id();
12518 state
12519 .selections
12520 .replace_all(vec![CursorSelection::new(id, 1, 1)]);
12521 cx.notify();
12522 });
12523 });
12524 let moved = input.read_with(&cx, |state, _| state.paste_target());
12525 assert_ne!(moved, target);
12526
12527 cx.update(|window, cx| {
12529 input.update(cx, |state, cx| {
12530 state.replace_text_in_range_silent(None, "x", window, cx);
12531 let id = state.selections.generate_id();
12532 state
12533 .selections
12534 .replace_all(vec![CursorSelection::new(id, 1, 1)]);
12535 });
12536 });
12537 assert_ne!(input.read_with(&cx, |state, _| state.paste_target()), moved);
12538 }
12539
12540 #[gpui::test]
12541 fn test_ime_candidate_bounds_fallback_before_repaint(cx: &mut TestAppContext) {
12542 let input_view = InputView::build_textarea(cx, |state| {
12543 state.default_value("这是一段已经输入的中文文字")
12544 });
12545 let mut visual = VisualTestContext::from_window(input_view.window_handle.into(), cx);
12546 visual.update(|window, cx| window.draw(cx).clear(cx));
12547
12548 visual.update(|window, cx| {
12549 input_view.input.update(cx, |state, cx| {
12550 state.focus(window, cx);
12551 let len = state.text.len();
12552 state.set_cursor_to(len);
12553 });
12554 });
12555 visual.update(|window, cx| window.draw(cx).clear(cx));
12556
12557 visual.update(|window, cx| {
12558 input_view.input.update(cx, |state, cx| {
12559 let bounds = state.text_bounds().unwrap_or_default();
12560 let before = state.selected_text_range(false, window, cx).unwrap().range;
12561 let previous = state
12562 .bounds_for_range(before.end..before.end, bounds, window, cx)
12563 .unwrap();
12564
12565 state.replace_and_mark_text_in_range(None, "n", None, window, cx);
12566
12567 let after = state.selected_text_range(false, window, cx).unwrap().range;
12568 let pending = state
12569 .bounds_for_range(after.end..after.end, bounds, window, cx)
12570 .unwrap();
12571
12572 assert!(
12573 (pending.origin.x - previous.origin.x).abs() < px(40.0),
12574 "pending origin.x {:?} must remain close to previous origin.x {:?}",
12575 pending.origin.x,
12576 previous.origin.x
12577 );
12578
12579 let range_bounds = state
12580 .bounds_for_range(before.end..after.end, bounds, window, cx)
12581 .unwrap();
12582 assert!(
12583 (range_bounds.origin.x - previous.origin.x).abs() < px(40.0),
12584 "range_bounds origin.x {:?} must remain close to previous origin.x {:?}",
12585 range_bounds.origin.x,
12586 previous.origin.x
12587 );
12588 assert!(range_bounds.size.width >= px(0.0));
12589 });
12590 });
12591 }
12592}
12593
12594impl InputBaseState<crate::input::InputMode> {
12596 pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
12598 Self::new_in_mode(window, cx)
12599 }
12600
12601 pub fn step_by(mut self, f: impl Fn(f64, StepAction, &mut App) -> f64 + 'static) -> Self {
12616 self.number_step = Some(NumberStep::by_value(f));
12617 self
12618 }
12619
12620 pub fn masked(mut self, masked: bool) -> Self {
12622 self.masked = masked;
12623 self
12624 }
12625
12626 pub fn set_masked(&mut self, masked: bool, _: &mut Window, cx: &mut Context<Self>) {
12628 self.masked = masked;
12629 cx.notify();
12630 }
12631
12632 pub fn pattern(mut self, pattern: regex::Regex) -> Self {
12634 self.pattern = Some(pattern);
12635 self
12636 }
12637
12638 pub fn set_pattern(
12640 &mut self,
12641 pattern: regex::Regex,
12642 _window: &mut Window,
12643 _cx: &mut Context<Self>,
12644 ) {
12645 self.pattern = Some(pattern);
12646 }
12647
12648 pub fn validate(mut self, f: impl Fn(&str, &mut App) -> bool + 'static) -> Self {
12650 self.validate = Some(Box::new(f));
12651 self
12652 }
12653
12654 pub fn set_validator(
12655 &mut self,
12656 validate: impl Fn(&str, &mut App) -> bool + 'static,
12657 _cx: &mut Context<Self>,
12658 ) {
12659 self.validate = Some(Box::new(validate));
12660 }
12661
12662 pub fn step(mut self, step: impl Into<NumberStep>) -> Self {
12672 self.number_step = Some(step.into());
12673 self
12674 }
12675
12676 pub fn min(mut self, min: f64) -> Self {
12682 self.number_min = Some(min);
12683 self
12684 }
12685
12686 pub fn max(mut self, max: f64) -> Self {
12692 self.number_max = Some(max);
12693 self
12694 }
12695
12696 pub fn set_step(
12701 &mut self,
12702 step: impl Into<Option<NumberStep>>,
12703 _: &mut Window,
12704 _: &mut Context<Self>,
12705 ) {
12706 self.number_step = step.into();
12707 }
12708
12709 pub fn set_min(&mut self, min: Option<f64>, _: &mut Window, _: &mut Context<Self>) {
12711 self.number_min = min;
12712 }
12713
12714 pub fn set_max(&mut self, max: Option<f64>, _: &mut Window, _: &mut Context<Self>) {
12716 self.number_max = max;
12717 }
12718
12719 pub fn set_loading(&mut self, loading: bool, _: &mut Window, cx: &mut Context<Self>) {
12721 self.loading = loading;
12722 cx.notify();
12723 }
12724}
12725
12726impl<M: crate::input::MultiLineMode> InputBaseState<M> {
12729 pub fn scroll_bounce(mut self, scroll_bounce: bool) -> Self {
12737 self.scroll_bounce = scroll_bounce;
12738 self
12739 }
12740
12741 pub fn set_scroll_bounce(&mut self, scroll_bounce: bool, cx: &mut Context<Self>) {
12745 if self.scroll_bounce != scroll_bounce {
12746 self.scroll_bounce = scroll_bounce;
12747 self.overscroll.reset();
12748 cx.notify();
12749 }
12750 }
12751
12752 pub fn searchable(mut self, searchable: bool) -> Self {
12760 self.searchable = searchable;
12761 self
12762 }
12763
12764 pub fn set_searchable(&mut self, searchable: bool, cx: &mut Context<Self>) {
12766 self.searchable = searchable;
12767 cx.notify();
12768 }
12769
12770 #[doc(hidden)]
12772 pub fn soft_wrap(mut self, wrap: bool) -> Self {
12773 self.soft_wrap = wrap;
12774 self
12775 }
12776
12777 pub fn set_soft_wrap(&mut self, wrap: bool, _: &mut Window, cx: &mut Context<Self>) {
12779 self.soft_wrap = wrap;
12780 if wrap {
12781 let wrap_width = self
12782 .last_layout
12783 .as_ref()
12784 .and_then(|b| b.wrap_width)
12785 .unwrap_or(self.input_bounds.size.width);
12786
12787 self.display_map.on_layout_changed(Some(wrap_width), cx);
12788
12789 let mut offset = self.scroll_handle.offset();
12791 offset.x = px(0.);
12792 self.scroll_handle.set_offset(offset);
12793 } else {
12794 self.display_map.on_layout_changed(None, cx);
12795 }
12796 cx.notify();
12797 }
12798
12799 #[doc(hidden)]
12801 pub fn wrapping_indent(mut self, wrapping_indent: WrappingIndent) -> Self {
12802 self.wrapping_indent = wrapping_indent;
12803 self
12804 }
12805
12806 pub fn set_wrapping_indent(
12808 &mut self,
12809 wrapping_indent: WrappingIndent,
12810 _: &mut Window,
12811 cx: &mut Context<Self>,
12812 ) {
12813 self.wrapping_indent = wrapping_indent;
12814 self.display_map.set_wrapping_indent(wrapping_indent, cx);
12815 cx.notify();
12816 }
12817}
12818
12819impl InputBaseState<crate::input::TextareaMode> {
12821 pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
12826 Self::new_in_mode(window, cx)
12827 }
12828
12829 pub fn set_auto_grow(&mut self, min_rows: usize, max_rows: usize, cx: &mut Context<Self>) {
12830 self.mode = LayoutMode::auto_grow(min_rows, max_rows.max(min_rows));
12831 self.mode.update_auto_grow(&self.display_map);
12832 cx.notify();
12833 }
12834
12835 #[doc(hidden)]
12842 pub fn rows(mut self, rows: usize) -> Self {
12843 match &mut self.mode {
12844 LayoutMode::PlainText { rows: r, .. } | LayoutMode::CodeEditor { rows: r, .. } => {
12845 *r = rows
12846 }
12847 LayoutMode::AutoGrow {
12848 max_rows: max_r,
12849 rows: r,
12850 ..
12851 } => {
12852 *r = rows;
12853 *max_r = rows;
12854 }
12855 }
12856 self
12857 }
12858
12859 pub fn set_rows(&mut self, rows: usize, cx: &mut Context<Self>) {
12860 match &mut self.mode {
12861 LayoutMode::PlainText { rows: value, .. }
12862 | LayoutMode::CodeEditor { rows: value, .. } => *value = rows,
12863 LayoutMode::AutoGrow {
12864 rows: value,
12865 max_rows,
12866 ..
12867 } => {
12868 *value = rows;
12869 *max_rows = rows;
12870 }
12871 }
12872 cx.notify();
12873 }
12874
12875 pub fn auto_grow(mut self, min_rows: usize, max_rows: usize) -> Self {
12877 self.mode = LayoutMode::auto_grow(min_rows, max_rows);
12878 self
12879 }
12880}
12881
12882impl InputBaseState<crate::input::EditorMode> {
12884 pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
12895 let mut state = Self::new_in_mode(window, cx);
12896 state.mode = LayoutMode::code_editor(super::EditorLanguage::new(cx));
12897 state.searchable = true;
12898 state
12899 }
12900
12901 pub fn language(mut self, language: impl Into<SharedString>) -> Self {
12905 if let LayoutMode::CodeEditor {
12906 language: l,
12907 highlighter,
12908 ..
12909 } = &mut self.mode
12910 {
12911 l.set_name(language.into());
12912 *highlighter.borrow_mut() = None;
12913 }
12914 self
12915 }
12916
12917 pub fn language_name(&self) -> SharedString {
12919 match &self.mode {
12920 LayoutMode::CodeEditor { language, .. } => language.name(),
12921 _ => SharedString::default(),
12922 }
12923 }
12924
12925 #[doc(hidden)]
12929 pub fn folding(mut self, folding: bool) -> Self {
12930 if let LayoutMode::CodeEditor { folding: f, .. } = &mut self.mode {
12931 *f = folding;
12932 }
12933 self
12934 }
12935
12936 pub fn set_folding(&mut self, folding: bool, _: &mut Window, cx: &mut Context<Self>) {
12940 if let LayoutMode::CodeEditor { folding: f, .. } = &mut self.mode {
12941 *f = folding;
12942 }
12943 if !folding {
12944 self.display_map.clear_folds();
12945 }
12946 cx.notify();
12947 }
12948
12949 pub fn unfold_at(&mut self, position: impl Into<Position>, cx: &mut Context<Self>) -> bool {
12962 let offset = self.text.position_to_offset(&position.into());
12963 let line = self.text.offset_to_point(offset).row;
12964 let covering: Vec<usize> = self
12967 .display_map
12968 .folded_ranges()
12969 .iter()
12970 .filter(|fold| line > fold.start_line && line < fold.end_line)
12971 .map(|fold| fold.start_line)
12972 .collect();
12973 if covering.is_empty() {
12974 return false;
12975 }
12976
12977 for start_line in covering {
12978 self.display_map.set_folded(start_line, false);
12979 }
12980 cx.notify();
12981 true
12982 }
12983
12984 #[doc(hidden)]
12986 pub fn line_number(mut self, line_number: bool) -> Self {
12987 if let LayoutMode::CodeEditor { line_number: l, .. } = &mut self.mode {
12988 *l = line_number;
12989 }
12990 self
12991 }
12992
12993 pub fn set_line_number(&mut self, line_number: bool, _: &mut Window, cx: &mut Context<Self>) {
12995 if let LayoutMode::CodeEditor { line_number: l, .. } = &mut self.mode {
12996 if *l == line_number {
12997 return;
12998 }
12999 *l = line_number;
13000 }
13001 cx.notify();
13002 }
13003
13004 #[doc(hidden)]
13010 pub fn auto_close(mut self, auto_close: bool) -> Self {
13011 self.mode.set_auto_close(auto_close);
13012 self
13013 }
13014
13015 pub fn set_auto_close(&mut self, auto_close: bool, _: &mut Window, cx: &mut Context<Self>) {
13017 self.mode.set_auto_close(auto_close);
13018 cx.notify();
13019 }
13020
13021 #[doc(hidden)]
13027 pub fn smart_indent(mut self, smart_indent: bool) -> Self {
13028 self.mode.set_smart_indent(smart_indent);
13029 self
13030 }
13031
13032 pub fn set_smart_indent(&mut self, smart_indent: bool, _: &mut Window, cx: &mut Context<Self>) {
13034 self.mode.set_smart_indent(smart_indent);
13035 cx.notify();
13036 }
13037}