1use gpui::TextAlign;
6use gpui::{
7 Action, App, AppContext, Bounds, ClipboardItem, Context, Edges, Entity, EntityInputHandler,
8 EventEmitter, FocusHandle, Focusable, InteractiveElement as _, IntoElement, KeyBinding,
9 MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels, Point,
10 Render, ScrollEvent, ScrollHandle, SharedString, StatefulInteractiveElement as _, Styled as _,
11 Subscription, UTF16Selection, WeakFocusHandle, Window, actions, div, point,
12 prelude::FluentBuilder as _, px,
13};
14use gpui::{Autocapitalize, TextInputAction, TextInputConfiguration};
15use ropey::{Rope, RopeSlice};
16use serde::Deserialize;
17use std::borrow::Cow;
18use std::cell::Cell;
19use std::ops::Range;
20use std::rc::Rc;
21use sum_tree::Bias;
22use unicode_segmentation::*;
23
24use super::{
25 DiagnosticSet, DisplayMap, InputContextMenuCapabilities, InputEditorStyle,
26 InputHighlighterFactory, MASK_CHAR, MaskPattern, NativeMenu, NumberStep, WrappingIndent,
27 blink_cursor::BlinkCursor,
28 change::Change,
29 cursor::{CursorSelection, Selections},
30 element::{
31 EditorScrollbar, EditorScrollbarSnapshot, LongestLineKey, OverviewRulerRows, TextElement,
32 clamp_horizontal_scroll_offset,
33 },
34 kind::InputModeKind,
35 mask_pattern::normalize_number_input,
36 mode::LayoutMode,
37 undo_manager::{EditIntent, UndoManager},
38};
39use crate::actions::{SelectDown, SelectLeft, SelectRight, SelectUp};
40use crate::input::blink_cursor::CURSOR_WIDTH;
41use crate::input::movement::MoveDirection;
42use crate::input::{
43 InputEdit, InputExtras as _, Position, RopeExt as _, element::RIGHT_MARGIN, layout::LastLayout,
44};
45use crate::overscroll::Overscroll;
46use crate::{AutoScroll, StepAction, TestSupportExt as _};
47
48pub(super) const EDITOR_ELEMENT_ID: &str = "input-state";
49
50pub(crate) enum ScrollPadding {
52 Minimal,
53 SurroundingLines,
54}
55
56#[derive(Action, Clone, PartialEq, Eq, Deserialize)]
57#[action(namespace = input, no_json)]
58pub struct Enter {
59 pub secondary: bool,
61 pub shift: bool,
63}
64
65impl Enter {
66 pub fn is_primary(action: &dyn Action) -> bool {
69 action.partial_eq(&Enter {
70 secondary: false,
71 shift: false,
72 }) || action.partial_eq(&Enter {
73 secondary: false,
74 shift: true,
75 })
76 }
77}
78
79actions!(
80 input,
81 [
82 ActivateToken,
83 Backspace,
84 Delete,
85 DeleteToBeginningOfLine,
86 DeleteToEndOfLine,
87 DeleteToPreviousWordStart,
88 DeleteToNextWordEnd,
89 Indent,
90 Outdent,
91 IndentInline,
92 OutdentInline,
93 MoveUp,
94 MoveDown,
95 MoveLeft,
96 MoveRight,
97 MoveHome,
98 MoveEnd,
99 MovePageUp,
100 MovePageDown,
101 AddCursorAbove,
102 AddCursorBelow,
103 SelectAll,
104 SelectToStartOfLine,
105 SelectToEndOfLine,
106 SelectToStart,
107 SelectToEnd,
108 SelectToPreviousWordStart,
109 SelectToNextWordEnd,
110 ShowCharacterPalette,
111 Copy,
112 Cut,
113 Paste,
114 Undo,
115 Redo,
116 MoveToStartOfLine,
117 MoveToEndOfLine,
118 MoveToStart,
119 MoveToEnd,
120 MoveToPreviousWord,
121 MoveToNextWord,
122 Escape,
123 ToggleCodeActions,
124 ShowCompletions,
125 Search,
126 Replace,
127 GoToDefinition,
128 ]
129);
130
131#[derive(Clone)]
137pub enum InputEvent {
138 Change,
139 PressEnter { secondary: bool, shift: bool },
140 Focus,
141 Blur,
142}
143
144#[derive(Clone)]
150pub struct InputEditsEvent {
151 edits: Vec<(InputEdit, Rope)>,
152}
153
154impl InputEditsEvent {
155 pub fn edits(&self) -> &[(InputEdit, Rope)] {
157 &self.edits
158 }
159}
160
161pub(super) const CONTEXT: &str = "Input";
162
163pub(crate) fn init(cx: &mut App) {
164 cx.bind_keys([
165 KeyBinding::new("backspace", Backspace, Some(CONTEXT)),
166 KeyBinding::new("shift-backspace", Backspace, Some(CONTEXT)),
167 #[cfg(target_os = "macos")]
168 KeyBinding::new("ctrl-backspace", Backspace, Some(CONTEXT)),
169 KeyBinding::new("delete", Delete, Some(CONTEXT)),
170 KeyBinding::new("shift-delete", Delete, Some(CONTEXT)),
171 #[cfg(target_os = "macos")]
172 KeyBinding::new("cmd-backspace", DeleteToBeginningOfLine, Some(CONTEXT)),
173 #[cfg(target_os = "macos")]
174 KeyBinding::new("cmd-delete", DeleteToEndOfLine, Some(CONTEXT)),
175 #[cfg(target_os = "macos")]
176 KeyBinding::new("alt-backspace", DeleteToPreviousWordStart, Some(CONTEXT)),
177 #[cfg(not(target_os = "macos"))]
178 KeyBinding::new("ctrl-backspace", DeleteToPreviousWordStart, Some(CONTEXT)),
179 #[cfg(target_os = "macos")]
180 KeyBinding::new("alt-delete", DeleteToNextWordEnd, Some(CONTEXT)),
181 #[cfg(not(target_os = "macos"))]
182 KeyBinding::new("ctrl-delete", DeleteToNextWordEnd, Some(CONTEXT)),
183 KeyBinding::new(
184 "enter",
185 Enter {
186 secondary: false,
187 shift: false,
188 },
189 Some(CONTEXT),
190 ),
191 KeyBinding::new(
192 "shift-enter",
193 Enter {
194 secondary: false,
195 shift: true,
196 },
197 Some(CONTEXT),
198 ),
199 KeyBinding::new(
200 if cx.platform_info().is_mac() {
201 "cmd-enter"
202 } else {
203 "ctrl-enter"
204 },
205 Enter {
206 secondary: true,
207 shift: false,
208 },
209 Some(CONTEXT),
210 ),
211 KeyBinding::new("escape", Escape, Some(CONTEXT)),
212 KeyBinding::new("up", MoveUp, Some(CONTEXT)),
213 KeyBinding::new("down", MoveDown, Some(CONTEXT)),
214 KeyBinding::new("left", MoveLeft, Some(CONTEXT)),
215 KeyBinding::new("right", MoveRight, Some(CONTEXT)),
216 KeyBinding::new("pageup", MovePageUp, Some(CONTEXT)),
217 KeyBinding::new("pagedown", MovePageDown, Some(CONTEXT)),
218 KeyBinding::new("tab", IndentInline, Some(CONTEXT)),
219 KeyBinding::new("shift-tab", OutdentInline, Some(CONTEXT)),
220 #[cfg(target_os = "macos")]
221 KeyBinding::new("cmd-]", Indent, Some(CONTEXT)),
222 #[cfg(not(target_os = "macos"))]
223 KeyBinding::new("ctrl-]", Indent, Some(CONTEXT)),
224 #[cfg(target_os = "macos")]
225 KeyBinding::new("cmd-[", Outdent, Some(CONTEXT)),
226 #[cfg(not(target_os = "macos"))]
227 KeyBinding::new("ctrl-[", Outdent, Some(CONTEXT)),
228 KeyBinding::new("shift-left", SelectLeft, Some(CONTEXT)),
229 KeyBinding::new("shift-right", SelectRight, Some(CONTEXT)),
230 KeyBinding::new("shift-up", SelectUp, Some(CONTEXT)),
231 KeyBinding::new("shift-down", SelectDown, Some(CONTEXT)),
232 #[cfg(not(any(target_os = "macos", target_os = "linux")))]
233 KeyBinding::new("shift-alt-left", SelectLeft, Some(CONTEXT)),
234 #[cfg(not(any(target_os = "macos", target_os = "linux")))]
235 KeyBinding::new("shift-alt-right", SelectRight, Some(CONTEXT)),
236 #[cfg(target_os = "macos")]
238 KeyBinding::new("cmd-alt-up", AddCursorAbove, Some(CONTEXT)),
239 #[cfg(target_os = "macos")]
240 KeyBinding::new("cmd-alt-down", AddCursorBelow, Some(CONTEXT)),
241 #[cfg(target_os = "windows")]
242 KeyBinding::new("ctrl-alt-up", AddCursorAbove, Some(CONTEXT)),
243 #[cfg(not(any(target_os = "macos", target_os = "windows")))]
244 KeyBinding::new("shift-alt-up", AddCursorAbove, Some(CONTEXT)),
245 #[cfg(target_os = "windows")]
246 KeyBinding::new("ctrl-alt-down", AddCursorBelow, Some(CONTEXT)),
247 #[cfg(not(any(target_os = "macos", target_os = "windows")))]
248 KeyBinding::new("shift-alt-down", AddCursorBelow, Some(CONTEXT)),
249 KeyBinding::new("home", MoveHome, Some(CONTEXT)),
250 KeyBinding::new("end", MoveEnd, Some(CONTEXT)),
251 #[cfg(not(target_os = "macos"))]
252 KeyBinding::new("ctrl-home", MoveToStart, Some(CONTEXT)),
253 #[cfg(not(target_os = "macos"))]
254 KeyBinding::new("ctrl-end", MoveToEnd, Some(CONTEXT)),
255 #[cfg(not(target_os = "macos"))]
256 KeyBinding::new("ctrl-shift-home", SelectToStart, Some(CONTEXT)),
257 #[cfg(not(target_os = "macos"))]
258 KeyBinding::new("ctrl-shift-end", SelectToEnd, Some(CONTEXT)),
259 KeyBinding::new("shift-home", SelectToStartOfLine, Some(CONTEXT)),
260 KeyBinding::new("shift-end", SelectToEndOfLine, Some(CONTEXT)),
261 #[cfg(target_os = "macos")]
262 KeyBinding::new("ctrl-shift-a", SelectToStartOfLine, Some(CONTEXT)),
263 #[cfg(target_os = "macos")]
264 KeyBinding::new("ctrl-shift-e", SelectToEndOfLine, Some(CONTEXT)),
265 #[cfg(target_os = "macos")]
266 KeyBinding::new("shift-cmd-left", SelectToStartOfLine, Some(CONTEXT)),
267 #[cfg(target_os = "macos")]
268 KeyBinding::new("shift-cmd-right", SelectToEndOfLine, Some(CONTEXT)),
269 #[cfg(any(target_os = "macos", target_os = "linux"))]
270 KeyBinding::new("alt-shift-left", SelectToPreviousWordStart, Some(CONTEXT)),
271 #[cfg(not(target_os = "macos"))]
272 KeyBinding::new("ctrl-shift-left", SelectToPreviousWordStart, Some(CONTEXT)),
273 #[cfg(any(target_os = "macos", target_os = "linux"))]
274 KeyBinding::new("alt-shift-right", SelectToNextWordEnd, Some(CONTEXT)),
275 #[cfg(not(target_os = "macos"))]
276 KeyBinding::new("ctrl-shift-right", SelectToNextWordEnd, Some(CONTEXT)),
277 #[cfg(target_os = "macos")]
278 KeyBinding::new("ctrl-cmd-space", ShowCharacterPalette, Some(CONTEXT)),
279 #[cfg(target_os = "macos")]
280 KeyBinding::new("cmd-a", SelectAll, Some(CONTEXT)),
281 #[cfg(not(target_os = "macos"))]
282 KeyBinding::new("ctrl-a", SelectAll, Some(CONTEXT)),
283 #[cfg(target_os = "macos")]
284 KeyBinding::new("cmd-c", Copy, Some(CONTEXT)),
285 #[cfg(not(target_os = "macos"))]
286 KeyBinding::new("ctrl-c", Copy, Some(CONTEXT)),
287 #[cfg(target_os = "macos")]
288 KeyBinding::new("cmd-x", Cut, Some(CONTEXT)),
289 #[cfg(not(target_os = "macos"))]
290 KeyBinding::new("ctrl-x", Cut, Some(CONTEXT)),
291 #[cfg(target_os = "macos")]
292 KeyBinding::new("cmd-v", Paste, Some(CONTEXT)),
293 #[cfg(not(target_os = "macos"))]
294 KeyBinding::new("ctrl-v", Paste, Some(CONTEXT)),
295 #[cfg(target_os = "macos")]
296 KeyBinding::new("ctrl-a", MoveHome, Some(CONTEXT)),
297 #[cfg(target_os = "macos")]
298 KeyBinding::new("cmd-left", MoveHome, Some(CONTEXT)),
299 #[cfg(target_os = "macos")]
300 KeyBinding::new("ctrl-e", MoveEnd, Some(CONTEXT)),
301 #[cfg(target_os = "macos")]
302 KeyBinding::new("cmd-right", MoveEnd, Some(CONTEXT)),
303 #[cfg(target_os = "macos")]
304 KeyBinding::new("cmd-z", Undo, Some(CONTEXT)),
305 #[cfg(target_os = "macos")]
306 KeyBinding::new("cmd-shift-z", Redo, Some(CONTEXT)),
307 #[cfg(target_os = "macos")]
308 KeyBinding::new("cmd-up", MoveToStart, Some(CONTEXT)),
309 #[cfg(target_os = "macos")]
310 KeyBinding::new("cmd-down", MoveToEnd, Some(CONTEXT)),
311 #[cfg(target_os = "macos")]
312 KeyBinding::new("alt-left", MoveToPreviousWord, Some(CONTEXT)),
313 #[cfg(target_os = "macos")]
314 KeyBinding::new("alt-right", MoveToNextWord, Some(CONTEXT)),
315 #[cfg(not(target_os = "macos"))]
316 KeyBinding::new("ctrl-left", MoveToPreviousWord, Some(CONTEXT)),
317 #[cfg(not(target_os = "macos"))]
318 KeyBinding::new("ctrl-right", MoveToNextWord, Some(CONTEXT)),
319 #[cfg(target_os = "macos")]
320 KeyBinding::new("cmd-shift-up", SelectToStart, Some(CONTEXT)),
321 #[cfg(target_os = "macos")]
322 KeyBinding::new("cmd-shift-down", SelectToEnd, Some(CONTEXT)),
323 #[cfg(not(target_os = "macos"))]
324 KeyBinding::new("ctrl-z", Undo, Some(CONTEXT)),
325 #[cfg(not(target_os = "macos"))]
326 KeyBinding::new("ctrl-y", Redo, Some(CONTEXT)),
327 #[cfg(target_os = "macos")]
328 KeyBinding::new("cmd-.", ToggleCodeActions, Some(CONTEXT)),
329 #[cfg(not(target_os = "macos"))]
330 KeyBinding::new("ctrl-.", ToggleCodeActions, Some(CONTEXT)),
331 #[cfg(target_os = "macos")]
332 KeyBinding::new("cmd-f", Search, Some(CONTEXT)),
333 #[cfg(not(target_os = "macos"))]
334 KeyBinding::new("ctrl-f", Search, Some(CONTEXT)),
335 #[cfg(target_os = "macos")]
336 KeyBinding::new("cmd-shift-f", Replace, Some(CONTEXT)),
337 #[cfg(not(target_os = "macos"))]
338 KeyBinding::new("ctrl-h", Replace, Some(CONTEXT)),
339 ]);
340}
341
342#[derive(Debug, Clone, Copy, PartialEq, Eq)]
349pub(super) struct ColumnarPoint {
350 offset: usize,
351 columns_past_line_end: usize,
352}
353
354impl ColumnarPoint {
355 pub(super) fn new(offset: usize, columns_past_line_end: usize) -> Self {
356 Self {
357 offset,
358 columns_past_line_end,
359 }
360 }
361}
362
363#[derive(Debug, PartialEq)]
367struct PasteTarget {
368 document_revision: u64,
369 selections: Vec<CursorSelection>,
370}
371
372#[derive(Clone, Copy, Debug)]
379#[doc(hidden)]
380pub struct PrepaintCaretGeometry {
381 cursor_bounds: Bounds<Pixels>,
382 line_height: Pixels,
383 input_bounds: Bounds<Pixels>,
384 scroll_offset: Point<Pixels>,
385 pub(super) resize_reveal_owned: bool,
387}
388
389impl PrepaintCaretGeometry {
390 pub(super) fn new(
391 cursor_bounds: Bounds<Pixels>,
392 line_height: Pixels,
393 input_bounds: Bounds<Pixels>,
394 scroll_offset: Point<Pixels>,
395 ) -> Self {
396 Self {
397 cursor_bounds,
398 line_height,
399 input_bounds,
400 scroll_offset,
401 resize_reveal_owned: false,
402 }
403 }
404
405 pub fn cursor_bounds(&self) -> Bounds<Pixels> {
407 self.cursor_bounds
408 }
409
410 pub fn line_height(&self) -> Pixels {
412 self.line_height
413 }
414
415 pub fn input_bounds(&self) -> Bounds<Pixels> {
417 self.input_bounds
418 }
419
420 pub fn scroll_offset(&self) -> Point<Pixels> {
423 self.scroll_offset
424 }
425}
426
427pub struct InputBaseState<M: InputModeKind> {
438 pub(crate) extras: M::Extras,
440 pub(super) focus_handle: FocusHandle,
441 selection_focus: Option<WeakFocusHandle>,
442 pub(super) mode: LayoutMode,
443 pub(super) text: Rope,
444 pub(super) display_map: DisplayMap,
445 pub(super) undo_manager: UndoManager,
446 pub(super) inline_tokens: Option<Box<super::inline_tokens::InlineTokenStore>>,
447 pub(super) pending_token: Option<super::InlineToken>,
448 pub(super) replaying_history: bool,
449 pub(super) validated_token_edit: bool,
450 pub(super) document_revision: u64,
451 pub(super) token_presentation: super::InlineTokenPresentation,
452 pub(super) token_layout_cache: Option<Box<super::token_presentation::TokenLayoutCache>>,
453 pub(super) token_bounds: std::collections::HashMap<usize, Bounds<Pixels>>,
456 pub(super) hovered_token: Option<super::token_presentation::HoverSnapshot>,
461 pub(super) pending_token_hover_exits: Vec<super::token_presentation::HoverSnapshot>,
462 pub(super) pressed_token: Option<(usize, u64, Point<Pixels>)>,
465 pub(super) search_session: super::SearchSession,
466 pub(super) search_activation_revision: u64,
469 pub(super) searchable: bool,
470 pub(super) replaceable: bool,
471 pub(super) soft_wrap: bool,
472 pub(super) wrapping_indent: WrappingIndent,
473 pub(super) scroll_beyond_last_line: Option<usize>,
474 pub(super) cursor_surrounding_lines: Option<usize>,
475 pub(super) blink_cursor: Entity<BlinkCursor>,
476 pub(super) loading: bool,
477 pub(super) selections: Selections,
481 pub(super) selected_word_range: Option<CursorSelection>,
483 pub(super) ime_marked_range: Option<CursorSelection>,
485 ime_original_text: Option<Rope>,
487 pub(super) last_layout: Option<LastLayout>,
488 pub(crate) prepaint_caret_geometry: Option<PrepaintCaretGeometry>,
494 pub(super) last_cursor: Option<usize>,
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 accessibility: Option<InputAccessibility>,
558 pub(super) scrollbar_renderer: Option<crate::input::EditorScrollbarRenderer>,
561
562 pub(crate) mask_pattern: MaskPattern,
564 pub(super) mask_pattern_set: bool,
568 pub(super) placeholder: SharedString,
569
570 pub(super) diagnostic_popover: Option<Rc<crate::input::DiagnosticEntry>>,
572
573 context_menu_handler: Option<
574 Rc<dyn Fn(NativeMenu, InputContextMenuCapabilities, Point<Pixels>, &mut Window, &mut App)>,
575 >,
576 pending_context_menu: Option<(Point<Pixels>, usize)>,
577
578 pub(super) enable_context_menu: bool,
581
582 pub(super) completion_inserting: bool,
584 pub(super) overlay_action_handler: Option<
585 Rc<
586 dyn Fn(
587 super::InputOverlayKind,
588 Box<dyn Action>,
589 &mut Window,
590 &mut Context<InputBaseState<M>>,
591 ) -> bool,
592 >,
593 >,
594
595 _pending_update: bool,
599 pub(super) silent_replace_text: bool,
601 pub(super) emit_events: bool,
603
604 _subscriptions: Vec<Subscription>,
605
606 pub(super) auto_scroll: AutoScroll,
607}
608
609#[derive(Clone, Debug)]
616pub struct InputAccessibility {
617 role: Option<gpui::Role>,
618 label: Option<SharedString>,
619 placeholder: Option<SharedString>,
620 accessibility_id: Option<SharedString>,
621 value_exposed: bool,
622}
623
624impl InputAccessibility {
625 pub fn new(role: Option<gpui::Role>) -> Self {
626 Self {
627 role,
628 label: None,
629 placeholder: None,
630 accessibility_id: None,
631 value_exposed: true,
632 }
633 }
634
635 pub fn with_label(mut self, label: Option<SharedString>) -> Self {
636 self.label = label;
637 self
638 }
639
640 pub fn with_placeholder(mut self, placeholder: Option<SharedString>) -> Self {
641 self.placeholder = placeholder;
642 self
643 }
644
645 pub fn with_accessibility_id(mut self, id: Option<SharedString>) -> Self {
646 self.accessibility_id = id;
647 self
648 }
649
650 pub fn with_value_exposed(mut self, exposed: bool) -> Self {
652 self.value_exposed = exposed;
653 self
654 }
655
656 pub fn role(&self) -> Option<gpui::Role> {
657 self.role
658 }
659
660 pub fn label(&self) -> Option<&SharedString> {
661 self.label.as_ref()
662 }
663
664 pub fn placeholder(&self) -> Option<&SharedString> {
665 self.placeholder.as_ref()
666 }
667
668 pub fn accessibility_id(&self) -> Option<&SharedString> {
669 self.accessibility_id.as_ref()
670 }
671
672 pub fn value_exposed(&self) -> bool {
673 self.value_exposed
674 }
675}
676
677#[derive(Clone)]
682pub struct InputPresentation {
683 focus_handle: FocusHandle,
684 disabled: bool,
685 readonly: bool,
686 loading: bool,
687 masked: bool,
688 multi_line: bool,
689 code_editor: bool,
690 text_align: TextAlign,
691 placeholder: SharedString,
692 mask_placeholder: Option<String>,
693}
694
695impl InputPresentation {
696 pub fn focus_handle(&self) -> &FocusHandle {
697 &self.focus_handle
698 }
699
700 pub fn is_disabled(&self) -> bool {
701 self.disabled
702 }
703
704 pub fn is_readonly(&self) -> bool {
705 self.readonly
706 }
707
708 pub fn is_editable(&self) -> bool {
712 !self.disabled && !self.readonly
713 }
714
715 pub fn is_loading(&self) -> bool {
716 self.loading
717 }
718
719 pub fn is_masked(&self) -> bool {
720 self.masked
721 }
722
723 pub fn is_multi_line(&self) -> bool {
724 self.multi_line
725 }
726
727 pub fn is_code_editor(&self) -> bool {
728 self.code_editor
729 }
730
731 pub fn text_align(&self) -> TextAlign {
732 self.text_align
733 }
734
735 pub fn placeholder(&self) -> &SharedString {
736 &self.placeholder
737 }
738
739 pub fn mask_placeholder(&self) -> Option<&str> {
741 self.mask_placeholder.as_deref()
742 }
743}
744
745impl<M: InputModeKind> EventEmitter<InputEvent> for InputBaseState<M> {}
746impl<M: InputModeKind> EventEmitter<InputEditsEvent> for InputBaseState<M> {}
747
748impl<M: InputModeKind> InputBaseState<M> {
749 #[doc(hidden)]
750 pub fn cursor_layout(&self) -> Option<(Bounds<Pixels>, Pixels)> {
751 let layout = self.last_layout.as_ref()?;
752 Some((layout.cursor_bounds?, layout.line_height))
753 }
754
755 #[doc(hidden)]
760 pub fn prepaint_caret_geometry(&self) -> Option<PrepaintCaretGeometry> {
761 self.prepaint_caret_geometry
762 }
763
764 pub fn input_bounds(&self) -> Bounds<Pixels> {
765 self.input_bounds
766 }
767
768 pub fn text_bounds(&self) -> Option<Bounds<Pixels>> {
769 self.last_bounds
770 }
771
772 pub fn display_map(&self) -> &DisplayMap {
783 &self.display_map
784 }
785
786 pub fn prewrap(
797 &mut self,
798 font: gpui::Font,
799 font_size: Pixels,
800 width: Pixels,
801 cx: &mut Context<Self>,
802 ) {
803 if self.last_layout.is_some() {
804 return;
805 }
806 let wrap_width = (width - RIGHT_MARGIN).max(px(1.));
807 self.display_map.set_font(font, font_size, cx);
808 self.display_map
809 .set_tab_size(self.mode.tab_size().tab_size, cx);
810 self.display_map
811 .on_layout_changed(self.soft_wrap.then_some(wrap_width), cx);
812 self.display_map
813 .set_wrapping_indent(self.wrapping_indent, cx);
814 self.display_map.ensure_text_prepared(&self.text, cx);
815 }
816
817 pub fn diagnostic_popover(&self) -> Option<Rc<crate::input::DiagnosticEntry>> {
818 self.diagnostic_popover.clone()
819 }
820
821 pub fn presentation(&self) -> InputPresentation {
822 InputPresentation {
823 focus_handle: self.focus_handle.clone(),
824 disabled: self.disabled,
825 readonly: self.readonly,
826 loading: self.loading,
827 masked: self.masked,
828 multi_line: self.is_multi_line(),
829 code_editor: self.is_code_editor(),
830 text_align: self.text_align,
831 placeholder: self.placeholder.clone(),
832 mask_placeholder: self.mask_pattern.placeholder(),
833 }
834 }
835
836 #[inline]
841 pub(crate) fn shows_scrollbar(&self) -> bool {
849 self.is_multi_line()
850 }
851
852 pub fn is_multi_line(&self) -> bool {
853 M::MULTI_LINE
854 }
855
856 #[inline]
858 pub fn is_single_line(&self) -> bool {
859 !M::MULTI_LINE
860 }
861
862 #[inline]
864 pub fn is_code_editor(&self) -> bool {
865 M::CODE_EDITOR
866 }
867
868 pub fn is_copyable(&self) -> bool {
872 self.selections.iter().any(|sel| !sel.is_empty()) && !self.masked
873 }
874
875 pub fn context_menu_capabilities(&self) -> InputContextMenuCapabilities {
876 let (go_to_definition, code_actions) = self.extras.context_menu_capabilities();
877 InputContextMenuCapabilities::new()
878 .disabled(self.disabled)
879 .readonly(self.readonly)
880 .code_editor(self.is_code_editor())
881 .selection(!self.active_selection().is_empty())
882 .masked(self.masked)
883 .go_to_definition(go_to_definition)
884 .code_actions(code_actions)
885 }
886
887 pub fn set_text_align(&mut self, text_align: TextAlign, cx: &mut Context<Self>) {
888 if !self.is_single_line() || self.text_align == text_align {
889 return;
890 }
891
892 self.text_align = text_align;
893 cx.notify();
894 }
895
896 pub fn toggle_masked(&mut self, _: &mut Window, cx: &mut Context<Self>) {
902 self.masked = !self.masked;
903 cx.notify();
904 }
905
906 pub fn on_context_menu(
907 &mut self,
908 handler: Rc<
909 dyn Fn(NativeMenu, InputContextMenuCapabilities, Point<Pixels>, &mut Window, &mut App),
910 >,
911 ) {
912 self.context_menu_handler = Some(handler);
913 }
914
915 pub fn set_selection_focus(&mut self, focus: Option<FocusHandle>, cx: &mut Context<Self>) {
920 self.selection_focus = focus.as_ref().map(FocusHandle::downgrade);
921 cx.notify();
922 }
923
924 pub fn has_selection_focus(&self, window: &Window, cx: &App) -> bool {
926 self.focus_handle.is_focused(window)
927 || self
928 .selection_focus
929 .as_ref()
930 .and_then(WeakFocusHandle::upgrade)
931 .is_some_and(|focus| focus.is_focused(window) || focus.contains_focused(window, cx))
932 }
933
934 fn new_in_mode(window: &mut Window, cx: &mut Context<Self>) -> Self {
936 let focus_handle = cx.focus_handle().tab_stop(true);
937 let blink_cursor = cx.new(BlinkCursor::new);
938 let undo_manager = UndoManager::new();
939
940 let _subscriptions = vec![
941 cx.intercept_keystrokes({
944 let focus_handle = focus_handle.clone();
945 let blink_cursor = blink_cursor.downgrade();
946 move |_, window, cx| {
947 if focus_handle.is_focused(window) {
948 _ = blink_cursor.update(cx, |cursor, cx| cursor.pause(cx));
949 }
950 }
951 }),
952 cx.observe(&blink_cursor, |_, _, cx| cx.notify()),
954 cx.observe_window_activation(window, |input, window, cx| {
956 if window.is_window_active() {
957 let focus_handle = input.focus_handle.clone();
958 if focus_handle.is_focused(window) {
959 input.blink_cursor.update(cx, |blink_cursor, cx| {
960 blink_cursor.start(cx);
961 });
962 }
963 }
964 }),
965 cx.on_focus(&focus_handle, window, Self::on_focus),
966 cx.on_blur(&focus_handle, window, Self::on_blur),
967 ];
968
969 let text_style = window.text_style();
970
971 Self {
972 extras: M::Extras::default(),
973 focus_handle: focus_handle.clone(),
974 selection_focus: None,
975 text: "".into(),
976 display_map: DisplayMap::new(text_style.font(), window.rem_size(), None),
977 search_session: super::SearchSession::default(),
978 search_activation_revision: 0,
979 searchable: false,
980 replaceable: true,
981 soft_wrap: true,
982 wrapping_indent: WrappingIndent::default(),
983 scroll_beyond_last_line: None,
984 cursor_surrounding_lines: None,
985 blink_cursor,
986 undo_manager,
987 inline_tokens: None,
988 pending_token: None,
989 replaying_history: false,
990 validated_token_edit: false,
991 document_revision: 0,
992 token_presentation: Default::default(),
993 token_bounds: Default::default(),
994 hovered_token: None,
995 pending_token_hover_exits: Vec::new(),
996 token_layout_cache: None,
997 pressed_token: None,
998 selections: Selections::default(),
999 selected_word_range: None,
1000 ime_marked_range: None,
1001 ime_original_text: None,
1002 input_bounds: Bounds::default(),
1003 selecting: false,
1004 touch_selection: Default::default(),
1005 disabled: false,
1006 readonly: false,
1007 text_align: TextAlign::Left,
1008 masked: false,
1009 clean_on_escape: false,
1010 submit_on_enter: false,
1011 input_action: None,
1012 autocorrect: None,
1013 autocapitalize: None,
1014 show_whitespaces: false,
1015 loading: false,
1016 pattern: None,
1017 validate: None,
1018 number_step: Some(NumberStep::Fixed(1.)),
1019 number_min: None,
1020 number_max: None,
1021 mode: LayoutMode::default(),
1022 last_layout: None,
1023 prepaint_caret_geometry: None,
1024 last_bounds: None,
1025 revealed_viewport: None,
1026 last_selected_range: None,
1027 last_input_edits: Vec::new(),
1028 column_select_start: None,
1029 last_cursor: None,
1030 scroll_handle: ScrollHandle::new(),
1031 scroll_bounce: crate::ScrollBounce::enabled_by_default(),
1032 overscroll: Overscroll::default(),
1033 scroll_size: gpui::size(px(0.), px(0.)),
1034 editor_scrollbar_snapshot: Cell::new(None),
1035 overview_ruler_rows: Default::default(),
1036 longest_line_width: Cell::new(None),
1037 editor_paddings: Edges::default(),
1038 deferred_scroll_offset: None,
1039 placeholder: SharedString::default(),
1040 mask_pattern: MaskPattern::default(),
1041 mask_pattern_set: false,
1042 editor_style: InputEditorStyle::default(),
1043 projected_editor_style: InputEditorStyle::default(),
1044 accessibility: None,
1045 scrollbar_renderer: None,
1046 diagnostic_popover: None,
1047 context_menu_handler: None,
1048 pending_context_menu: None,
1049 enable_context_menu: true,
1050 completion_inserting: false,
1051 overlay_action_handler: None,
1052 silent_replace_text: false,
1053 emit_events: true,
1054 _subscriptions,
1055 _pending_update: false,
1056 cursor_line_end_affinity: false,
1057 auto_scroll: AutoScroll::default(),
1058 }
1059 }
1060
1061 pub fn context_menu(mut self, enable: bool) -> Self {
1068 self.enable_context_menu = enable;
1069 self
1070 }
1071
1072 pub fn set_context_menu_enabled(&mut self, enabled: bool) {
1073 self.enable_context_menu = enabled;
1074 }
1075
1076 #[doc(hidden)]
1078 pub fn replaceable(mut self, allow: bool) -> Self {
1079 self.replaceable = allow;
1080 self
1081 }
1082
1083 pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
1085 self.placeholder = placeholder.into();
1086 self
1087 }
1088
1089 pub fn set_highlighter(
1091 &mut self,
1092 new_language: impl Into<SharedString>,
1093 cx: &mut Context<Self>,
1094 ) {
1095 match &mut self.mode {
1096 LayoutMode::CodeEditor {
1097 language,
1098 highlighter,
1099 ..
1100 } => {
1101 language.set_name(new_language.into());
1102 *highlighter.borrow_mut() = None;
1103 }
1104 _ => {}
1105 }
1106 self.refresh(cx);
1107 }
1108
1109 fn reset_highlighter(&mut self, cx: &mut Context<Self>) {
1110 match &mut self.mode {
1111 LayoutMode::CodeEditor { highlighter, .. } => {
1112 *highlighter.borrow_mut() = None;
1113 }
1114 _ => {}
1115 }
1116 cx.notify();
1117 }
1118
1119 pub fn set_highlighter_factory(
1121 &mut self,
1122 factory: InputHighlighterFactory,
1123 cx: &mut Context<Self>,
1124 ) {
1125 self.mode.set_highlighter_factory(factory);
1126 self._pending_update = true;
1127 self.token_layout_cache = None;
1128 cx.notify();
1129 }
1130
1131 pub fn ensure_highlighter_factory(&mut self, factory: InputHighlighterFactory) {
1133 self.mode.ensure_highlighter_factory(factory);
1134 }
1135
1136 pub fn set_editor_style(&mut self, style: InputEditorStyle) {
1137 self.editor_style = style.clone();
1138 self.projected_editor_style = style;
1139 }
1140
1141 pub fn set_accessibility(&mut self, accessibility: InputAccessibility) {
1143 self.accessibility = Some(accessibility);
1144 }
1145
1146 fn handle_accessibility_focus(&self, window: &mut Window, cx: &mut Context<Self>) {
1147 if !self.disabled {
1148 self.focus(window, cx);
1149 }
1150 }
1151
1152 fn handle_accessibility_set_value(
1153 &mut self,
1154 data: Option<&gpui::accesskit::ActionData>,
1155 window: &mut Window,
1156 cx: &mut Context<Self>,
1157 ) {
1158 let Some(gpui::accesskit::ActionData::Value(value)) = data else {
1159 return;
1160 };
1161 if self.is_editable() {
1163 self.replace_all(value.to_string(), window, cx);
1164 }
1165 }
1166
1167 #[doc(hidden)]
1169 pub fn set_editor_paddings(&mut self, paddings: Edges<Pixels>) {
1170 self.editor_paddings = paddings;
1171 }
1172
1173 pub fn set_scrollbar_renderer(
1181 &mut self,
1182 renderer: Option<crate::input::EditorScrollbarRenderer>,
1183 ) {
1184 self.scrollbar_renderer = renderer;
1185 }
1186
1187 pub fn apply_highlighter_fold_candidates(
1188 &mut self,
1189 candidates: Vec<crate::input::FoldRange>,
1190 cx: &mut Context<Self>,
1191 ) {
1192 if self.mode.is_folding() {
1193 self.display_map.set_fold_candidates(candidates);
1194 }
1195 cx.notify();
1196 }
1197
1198 #[inline]
1199 pub fn diagnostics(&self) -> Option<&DiagnosticSet> {
1200 self.mode.diagnostics()
1201 }
1202
1203 #[inline]
1204 pub fn diagnostics_mut(&mut self) -> Option<&mut DiagnosticSet> {
1205 self.mode.diagnostics_mut()
1206 }
1207
1208 pub fn set_placeholder(
1210 &mut self,
1211 placeholder: impl Into<SharedString>,
1212 _: &mut Window,
1213 cx: &mut Context<Self>,
1214 ) {
1215 self.placeholder = placeholder.into();
1216 cx.notify();
1217 }
1218
1219 pub(super) fn line_and_position_for_offset(
1227 &self,
1228 offset: usize,
1229 ) -> (usize, usize, Option<Point<Pixels>>) {
1230 let Some(last_layout) = &self.last_layout else {
1231 return (0, 0, None);
1232 };
1233 let line_height = last_layout.line_height;
1234
1235 let mut y_offset = last_layout.visible_top;
1236 for (vi, line) in last_layout.lines.iter().enumerate() {
1237 let prev_lines_offset = last_layout.visible_line_byte_offsets[vi];
1238 let local_offset = offset.saturating_sub(prev_lines_offset);
1239 if let Some(pos) = line.position_for_index(local_offset, last_layout, false) {
1240 let sub_line_index = (pos.y / line_height) as usize;
1241 let adjusted_pos = point(pos.x + last_layout.line_number_width, pos.y + y_offset);
1242 return (vi, sub_line_index, Some(adjusted_pos));
1243 }
1244
1245 y_offset += line.size(line_height).height;
1246 }
1247 (0, 0, None)
1248 }
1249
1250 pub fn set_value(
1259 &mut self,
1260 value: impl Into<super::InputContent>,
1261 window: &mut Window,
1262 cx: &mut Context<Self>,
1263 ) {
1264 let content = value.into();
1265 self.inline_tokens = None;
1266 self.undo_manager.set_ignoring(true);
1267 self.emit_events = false;
1268 self.replace_text(content.text().clone(), window, cx);
1269 self.ime_original_text = None;
1270 self.install_tokens(content);
1271 self.undo_manager.set_ignoring(false);
1272 self.emit_events = true;
1273
1274 self.reset_selection();
1275 self.reset_lsp_state();
1276 self.reset_scroll_to_start();
1277
1278 self.undo_manager.clear();
1279 cx.notify();
1280 }
1281
1282 pub fn replace_all(
1292 &mut self,
1293 text: impl Into<SharedString>,
1294 window: &mut Window,
1295 cx: &mut Context<Self>,
1296 ) {
1297 self.replace_text(text, window, cx);
1298 self.reset_selection();
1299 self.reset_lsp_state();
1300 self.reset_scroll_to_start();
1301
1302 cx.notify();
1303 }
1304
1305 pub(super) fn with_edits_allowed(&mut self, f: impl FnOnce(&mut Self)) {
1310 let (was_disabled, was_readonly) = (self.disabled, self.readonly);
1311 (self.disabled, self.readonly) = (false, false);
1312 f(self);
1313 (self.disabled, self.readonly) = (was_disabled, was_readonly);
1314 }
1315
1316 pub fn insert(
1320 &mut self,
1321 text: impl Into<SharedString>,
1322 window: &mut Window,
1323 cx: &mut Context<Self>,
1324 ) {
1325 let text: SharedString = text.into();
1326 self.with_edits_allowed(|this| {
1327 this.undo_manager.set_pending_intent(EditIntent::Atomic);
1328 let range_utf16 = this.range_to_utf16(&(this.cursor()..this.cursor()));
1329 this.replace_text_in_range_silent(Some(range_utf16), &text, window, cx);
1330 let end = this.active_selection().end;
1331 this.set_cursor_to(end);
1332 });
1333 }
1334
1335 pub fn replace(
1339 &mut self,
1340 text: impl Into<SharedString>,
1341 window: &mut Window,
1342 cx: &mut Context<Self>,
1343 ) {
1344 let text: SharedString = text.into();
1345 self.with_edits_allowed(|this| {
1346 this.undo_manager.set_pending_intent(EditIntent::Atomic);
1347 this.replace_text_in_range_silent(None, &text, window, cx);
1348 let end = this.active_selection().end;
1349 this.set_cursor_to(end);
1350 });
1351 }
1352
1353 fn replace_text(
1354 &mut self,
1355 text: impl Into<SharedString>,
1356 window: &mut Window,
1357 cx: &mut Context<Self>,
1358 ) {
1359 let text: SharedString = text.into();
1360 self.with_edits_allowed(|this| {
1361 this.undo_manager.set_pending_intent(EditIntent::Atomic);
1362 let range = 0..this.text.chars().map(|c| c.len_utf16()).sum();
1363 this.replace_text_in_range_silent(Some(range), &text, window, cx);
1364 this.reset_highlighter(cx);
1365 });
1366 }
1367
1368 fn reset_selection(&mut self) {
1369 self.selections.remove_all_but_active();
1370
1371 if self.is_single_line() {
1375 let end = self.text.len();
1376 self.set_cursor_to(end);
1377 } else {
1378 self.active_selection_mut().clear();
1379 }
1380 }
1381
1382 fn reset_lsp_state(&mut self) {
1383 if self.is_code_editor() {
1384 self._pending_update = true;
1385 M::reset_language_features(self);
1386 }
1387 }
1388
1389 fn reset_scroll_to_start(&mut self) {
1390 self.scroll_handle.set_offset(point(px(0.), px(0.)));
1394 if self.is_single_line() {
1395 self.deferred_scroll_offset = Some(point(px(0.), px(0.)));
1396 }
1397 }
1398
1399 #[allow(unused)]
1403 pub(crate) fn disabled(mut self, disabled: bool) -> Self {
1404 self.disabled = disabled;
1405 self
1406 }
1407
1408 pub fn set_disabled(&mut self, disabled: bool, cx: &mut Context<Self>) {
1409 if self.disabled == disabled {
1410 return;
1411 }
1412
1413 self.disabled = disabled;
1414 if disabled {
1415 M::hide_context_menu(self, cx);
1416 M::clear_inline_completion(self, cx);
1417 }
1418 cx.notify();
1419 }
1420
1421 #[allow(unused)]
1429 pub(crate) fn readonly(mut self, readonly: bool) -> Self {
1430 self.readonly = readonly;
1431 self
1432 }
1433
1434 pub fn set_readonly(&mut self, readonly: bool, cx: &mut Context<Self>) {
1435 if self.readonly == readonly {
1436 return;
1437 }
1438
1439 self.readonly = readonly;
1440 if readonly {
1441 M::hide_context_menu(self, cx);
1442 M::clear_inline_completion(self, cx);
1443 self.search_session.replace_mode = false;
1444 }
1445 cx.notify();
1446 }
1447
1448 pub fn is_editable(&self) -> bool {
1453 !self.disabled && !self.readonly
1454 }
1455
1456 pub fn clean_on_escape(mut self) -> Self {
1460 self.clean_on_escape = true;
1461 self
1462 }
1463
1464 pub fn set_clean_on_escape(&mut self, clean: bool) {
1465 self.clean_on_escape = clean;
1466 }
1467
1468 #[doc(hidden)]
1473 pub fn submit_on_enter(mut self, submit: bool) -> Self {
1474 self.submit_on_enter = submit;
1475 self
1476 }
1477
1478 pub fn set_submit_on_enter(&mut self, submit: bool, cx: &mut Context<Self>) {
1479 self.submit_on_enter = submit;
1480 cx.notify();
1481 }
1482
1483 pub fn input_action(mut self, action: TextInputAction) -> Self {
1490 self.input_action = Some(action);
1491 self
1492 }
1493
1494 pub fn autocorrect(mut self, autocorrect: bool) -> Self {
1500 self.autocorrect = Some(autocorrect);
1501 self
1502 }
1503
1504 pub fn autocapitalize(mut self, autocapitalize: Autocapitalize) -> Self {
1509 self.autocapitalize = Some(autocapitalize);
1510 self
1511 }
1512
1513 pub fn text_input_configuration(&self) -> TextInputConfiguration {
1517 let autocorrect = self.autocorrect.unwrap_or(false);
1521 let input_action =
1522 self.input_action
1523 .unwrap_or(if self.is_multi_line() && !self.submit_on_enter {
1524 TextInputAction::Enter
1525 } else {
1526 TextInputAction::Done
1527 });
1528 TextInputConfiguration {
1529 autocorrect,
1530 autocapitalize: self.autocapitalize.unwrap_or(Autocapitalize::None),
1531 suggestions: autocorrect,
1532 input_action,
1533 }
1534 }
1535
1536 #[doc(hidden)]
1538 pub fn show_whitespaces(mut self, show: bool) -> Self {
1539 self.show_whitespaces = show;
1540 self
1541 }
1542
1543 pub fn set_show_whitespaces(&mut self, show: bool, _: &mut Window, cx: &mut Context<Self>) {
1545 self.show_whitespaces = show;
1546 cx.notify();
1547 }
1548
1549 pub fn scroll_beyond_last_line(mut self, rows: Option<usize>) -> Self {
1559 self.scroll_beyond_last_line = rows;
1560 self
1561 }
1562
1563 pub fn set_scroll_beyond_last_line(
1565 &mut self,
1566 rows: Option<usize>,
1567 _: &mut Window,
1568 cx: &mut Context<Self>,
1569 ) {
1570 if self.scroll_beyond_last_line == rows {
1571 return;
1572 }
1573 self.scroll_beyond_last_line = rows;
1574 cx.notify();
1575 }
1576
1577 pub fn cursor_surrounding_lines(mut self, lines: Option<usize>) -> Self {
1591 self.cursor_surrounding_lines = lines;
1592 self
1593 }
1594
1595 pub fn set_cursor_surrounding_lines(
1597 &mut self,
1598 lines: Option<usize>,
1599 _: &mut Window,
1600 cx: &mut Context<Self>,
1601 ) {
1602 if self.cursor_surrounding_lines == lines {
1603 return;
1604 }
1605 self.cursor_surrounding_lines = lines;
1606 cx.notify();
1607 }
1608
1609 pub fn default_value(mut self, value: impl Into<SharedString>) -> Self {
1611 let text: SharedString = value.into();
1612 self.text = Rope::from(self.normalize_input(&text).as_ref());
1613 if let Some(diagnostics) = self.mode.diagnostics_mut() {
1614 diagnostics.reset(&self.text)
1615 }
1616 self._pending_update = true;
1619 self
1620 }
1621
1622 pub fn value(&self) -> SharedString {
1627 SharedString::new(self.text.to_string())
1628 }
1629
1630 pub fn selected_value(&self) -> SharedString {
1636 SharedString::new(self.selected_text().to_string())
1637 }
1638
1639 pub fn unmask_value(&self) -> SharedString {
1641 self.mask_pattern.unmask(&self.text.to_string()).into()
1642 }
1643
1644 #[doc(hidden)]
1650 pub fn prepare(&mut self, _: &mut Window, _: &mut Context<Self>) {}
1651
1652 pub fn text(&self) -> &Rope {
1657 &self.text
1658 }
1659
1660 fn emit_change(&mut self, cx: &mut Context<Self>) {
1662 if let Some(original) = self.ime_original_text.take() {
1663 self.last_input_edits = vec![(
1664 Self::full_input_edit(&original, &self.text),
1665 self.text.clone(),
1666 )];
1667 }
1668 cx.emit(InputEditsEvent {
1669 edits: self.last_input_edits.clone(),
1670 });
1671 cx.emit(InputEvent::Change);
1672 }
1673
1674 fn full_input_edit(old_text: &Rope, new_text: &Rope) -> InputEdit {
1675 InputEdit {
1676 start_byte: 0,
1677 old_end_byte: old_text.len(),
1678 new_end_byte: new_text.len(),
1679 start_position: old_text.offset_to_point(0),
1680 old_end_position: old_text.offset_to_point(old_text.len()),
1681 new_end_position: new_text.offset_to_point(new_text.len()),
1682 }
1683 }
1684
1685 pub fn cursor_position(&self) -> Position {
1687 let offset = self.cursor();
1688 self.text.offset_to_position(offset)
1689 }
1690
1691 pub fn set_cursor_position(
1695 &mut self,
1696 position: impl Into<Position>,
1697 window: &mut Window,
1698 cx: &mut Context<Self>,
1699 ) {
1700 let position: Position = position.into();
1701 let offset = self.text.position_to_offset(&position);
1702
1703 self.move_to(offset, None, cx);
1704 self.update_preferred_column();
1705 self.focus(window, cx);
1706 }
1707
1708 pub fn focus(&self, window: &mut Window, cx: &mut Context<Self>) {
1710 self.focus_handle.focus(window, cx);
1711 self.blink_cursor.update(cx, |cursor, cx| {
1712 cursor.start(cx);
1713 });
1714 }
1715
1716 pub fn refresh(&mut self, cx: &mut Context<Self>) {
1730 self._pending_update = true;
1731 cx.notify();
1732 }
1733
1734 pub(super) fn select_left(&mut self, _: &SelectLeft, _: &mut Window, cx: &mut Context<Self>) {
1735 self.undo_manager.break_transaction_coalescing();
1736 self.select_all_cursors_to(|s, sel| s.previous_boundary(sel.cursor_offset()), cx);
1737 }
1738
1739 pub(super) fn select_right(&mut self, _: &SelectRight, _: &mut Window, cx: &mut Context<Self>) {
1740 self.select_all_cursors_to(|s, sel| s.next_boundary(sel.cursor_offset()), cx);
1741 }
1742
1743 pub(super) fn select_up(&mut self, _: &SelectUp, _: &mut Window, cx: &mut Context<Self>) {
1744 if self.is_single_line() {
1745 return;
1746 }
1747 self.select_all_cursors_to_with_affinity(
1748 |s, sel| {
1749 s.vertical_selection_target(
1750 sel.cursor_offset(),
1751 sel.column_anchor,
1752 s.line_end_affinity_for(sel),
1753 -1,
1754 )
1755 },
1756 true,
1757 cx,
1758 );
1759 }
1760
1761 pub(super) fn select_down(&mut self, _: &SelectDown, _: &mut Window, cx: &mut Context<Self>) {
1762 if self.is_single_line() {
1763 return;
1764 }
1765 self.select_all_cursors_to_with_affinity(
1766 |s, sel| {
1767 s.vertical_selection_target(
1768 sel.cursor_offset(),
1769 sel.column_anchor,
1770 s.line_end_affinity_for(sel),
1771 1,
1772 )
1773 },
1774 true,
1775 cx,
1776 );
1777 }
1778
1779 pub(super) fn on_action_select_all(
1780 &mut self,
1781 _: &SelectAll,
1782 window: &mut Window,
1783 cx: &mut Context<Self>,
1784 ) {
1785 self.select_all(window, cx);
1786 }
1787
1788 pub(super) fn select_to_start(
1789 &mut self,
1790 _: &SelectToStart,
1791 _: &mut Window,
1792 cx: &mut Context<Self>,
1793 ) {
1794 self.undo_manager.break_transaction_coalescing();
1795 self.select_all_cursors_to(|_, _| 0, cx);
1796 }
1797
1798 pub(super) fn select_to_end(
1799 &mut self,
1800 _: &SelectToEnd,
1801 _: &mut Window,
1802 cx: &mut Context<Self>,
1803 ) {
1804 self.undo_manager.break_transaction_coalescing();
1805 let end = self.text.len();
1806 self.select_all_cursors_to(move |_, _| end, cx);
1807 }
1808
1809 pub(super) fn select_to_start_of_line(
1810 &mut self,
1811 _: &SelectToStartOfLine,
1812 _: &mut Window,
1813 cx: &mut Context<Self>,
1814 ) {
1815 self.undo_manager.break_transaction_coalescing();
1816 self.select_all_cursors_to(
1817 |s, sel| s.start_of_line_at(sel.cursor_offset(), s.line_end_affinity_for(sel)),
1818 cx,
1819 );
1820 }
1821
1822 pub(super) fn select_to_end_of_line(
1823 &mut self,
1824 _: &SelectToEndOfLine,
1825 _: &mut Window,
1826 cx: &mut Context<Self>,
1827 ) {
1828 self.undo_manager.break_transaction_coalescing();
1829 self.select_all_cursors_to_with_affinity(
1830 |s, sel| {
1831 (
1832 s.end_of_line_at(sel.cursor_offset(), s.line_end_affinity_for(sel)),
1833 true,
1834 )
1835 },
1836 false,
1837 cx,
1838 );
1839 }
1840
1841 pub(super) fn select_to_previous_word(
1842 &mut self,
1843 _: &SelectToPreviousWordStart,
1844 _: &mut Window,
1845 cx: &mut Context<Self>,
1846 ) {
1847 self.undo_manager.break_transaction_coalescing();
1848 self.select_all_cursors_to(
1849 |s, sel| s.previous_start_of_word_at(sel.cursor_offset()),
1850 cx,
1851 );
1852 }
1853
1854 pub(super) fn select_to_next_word(
1855 &mut self,
1856 _: &SelectToNextWordEnd,
1857 _: &mut Window,
1858 cx: &mut Context<Self>,
1859 ) {
1860 self.undo_manager.break_transaction_coalescing();
1861 self.select_all_cursors_to(|s, sel| s.next_end_of_word_at(sel.cursor_offset()), cx);
1862 }
1863
1864 pub(super) fn previous_start_of_word_at(&self, offset: usize) -> usize {
1867 if self.masked {
1868 return 0;
1872 }
1873
1874 let offset = self.offset_from_utf16(self.offset_to_utf16(offset));
1875 let left_part = self.text.slice(0..offset).to_string();
1877
1878 let target = UnicodeSegmentation::split_word_bound_indices(left_part.as_str())
1879 .rfind(|(_, s)| !s.trim_start().is_empty())
1880 .map(|(i, _)| i)
1881 .unwrap_or(0);
1882 self.token_boundary(target, Bias::Left)
1883 }
1884
1885 pub(super) fn next_end_of_word_at(&self, offset: usize) -> usize {
1887 if self.masked {
1888 return self.text.len();
1890 }
1891
1892 let offset = self.offset_from_utf16(self.offset_to_utf16(offset));
1893 let right_part = self.text.slice(offset..self.text.len()).to_string();
1894
1895 let target = UnicodeSegmentation::split_word_bound_indices(right_part.as_str())
1896 .find(|(_, s)| !s.trim_start().is_empty())
1897 .map(|(i, s)| offset + i + s.len())
1898 .unwrap_or(self.text.len());
1899 self.token_boundary(target, Bias::Right)
1900 }
1901
1902 pub(super) fn start_of_line_at(&self, offset: usize, line_end_affinity: bool) -> usize {
1907 if self.is_single_line() {
1908 return 0;
1909 }
1910
1911 let row = self.text.offset_to_point(offset).row;
1912 let logical_start = self.text.line_start_offset(row);
1913
1914 if self.soft_wrap && self.is_code_editor() {
1915 let wrap_point = self
1916 .display_map
1917 .offset_to_wrap_display_point_with_affinity(offset, line_end_affinity);
1918 if let Some(line) = self.display_map.line(row)
1919 && let Some(range) = line.wrapped_lines.get(wrap_point.local_row)
1920 {
1921 let visual_start = logical_start + range.start;
1922 if offset != visual_start {
1923 return visual_start;
1924 }
1925 }
1926 }
1927
1928 logical_start
1929 }
1930
1931 pub(super) fn end_of_line_at(&self, offset: usize, line_end_affinity: bool) -> usize {
1936 if self.is_single_line() {
1937 return self.text.len();
1938 }
1939
1940 let row = self.text.offset_to_point(offset).row;
1941 let logical_start = self.text.line_start_offset(row);
1942 let logical_end = self.text.line_end_offset(row);
1943
1944 if self.soft_wrap && self.is_code_editor() {
1945 let wrap_point = self
1949 .display_map
1950 .offset_to_wrap_display_point_with_affinity(offset, line_end_affinity);
1951 if let Some(line) = self.display_map.line(row)
1952 && let Some(range) = line.wrapped_lines.get(wrap_point.local_row)
1953 {
1954 let visual_end = logical_start + range.end;
1955 if offset != visual_end {
1956 return visual_end;
1957 }
1958 }
1959 }
1960
1961 logical_end
1962 }
1963
1964 pub(super) fn indent_of_next_line(&mut self) -> String {
1968 self.indent_of_next_line_at(self.cursor())
1969 }
1970
1971 pub(super) fn indent_of_next_line_at(&mut self, offset: usize) -> String {
1973 if self.is_single_line() {
1974 return "".into();
1975 }
1976
1977 let mut current_indent = String::new();
1978 let mut next_indent = String::new();
1979 let line_end_affinity = self.line_end_affinity_at(offset);
1980 let current_line_start_pos = self.start_of_line_at(offset, line_end_affinity);
1981 let next_line_start_pos = self.end_of_line_at(offset, line_end_affinity);
1982 for c in self.text.slice(current_line_start_pos..).chars() {
1983 if !c.is_whitespace() {
1984 break;
1985 }
1986 if c == '\n' || c == '\r' {
1987 break;
1988 }
1989 current_indent.push(c);
1990 }
1991
1992 for c in self.text.slice(next_line_start_pos..).chars() {
1993 if !c.is_whitespace() {
1994 break;
1995 }
1996 if c == '\n' || c == '\r' {
1997 break;
1998 }
1999 next_indent.push(c);
2000 }
2001
2002 if next_indent.len() > current_indent.len() {
2003 return next_indent;
2004 }
2005
2006 if let Some(rules) = self.mode.language_config() {
2009 if self.mode.is_smart_indent()
2010 && M::editing_syntax_context(self, offset) == crate::input::SyntaxContext::Code
2011 {
2012 let line_before: String = self
2013 .text
2014 .slice(current_line_start_pos..offset)
2015 .chars()
2016 .collect();
2017 if rules.opens_indent(line_before.trim_end()) {
2018 let tab = self.mode.tab_size();
2019 if tab.hard_tabs {
2020 current_indent.push('\t');
2021 } else {
2022 for _ in 0..tab.tab_size {
2023 current_indent.push(' ');
2024 }
2025 }
2026 } else {
2027 let line_after = self.text.slice(offset..next_line_start_pos).to_string();
2028 if rules.closes_indent(&line_after) {
2029 let tab = self.mode.tab_size();
2030 if current_indent.ends_with('\t') {
2031 current_indent.pop();
2032 } else {
2033 for _ in 0..tab.tab_size {
2034 if !current_indent.ends_with(' ') {
2035 break;
2036 }
2037 current_indent.pop();
2038 }
2039 }
2040 }
2041 }
2042 }
2043 }
2044
2045 current_indent
2046 }
2047
2048 fn delete_selections(
2056 &mut self,
2057 silent: bool,
2058 collapsed_intent: EditIntent,
2059 mut collapsed_target: impl FnMut(&mut Self, usize) -> Range<usize>,
2060 window: &mut Window,
2061 cx: &mut Context<Self>,
2062 ) {
2063 if !self.is_editable() {
2064 return;
2065 }
2066 let cursors: Vec<CursorSelection> = self.selections.iter().copied().collect();
2067 let intent = if cursors.iter().all(|sel| sel.is_empty()) {
2068 collapsed_intent
2069 } else {
2070 EditIntent::Atomic
2071 };
2072 let mut new_selections: Vec<CursorSelection> = Vec::with_capacity(cursors.len());
2073 for sel in &cursors {
2074 let range = if sel.is_empty() {
2075 collapsed_target(self, sel.cursor_offset())
2076 } else {
2077 sel.start..sel.end
2078 };
2079 let (start, end) = (range.start.min(range.end), range.start.max(range.end));
2080 let mut selection = *sel;
2081 selection.start = start;
2082 selection.end = end;
2083 new_selections.push(selection);
2084 }
2085 self.undo_manager.begin_transaction_with(intent);
2088 self.undo_manager
2089 .record_selections(cursors.clone(), cursors.clone());
2090 self.selections.replace_all(new_selections);
2091 self.undo_manager.set_pending_intent(intent);
2092
2093 let was_silent = self.silent_replace_text;
2094 self.silent_replace_text = silent;
2095 self.replace_text_in_range(None, "", window, cx);
2096 self.silent_replace_text = was_silent;
2097 self.undo_manager
2098 .record_selections(cursors, self.selections.iter().copied().collect());
2099 self.undo_manager.commit_transaction();
2100 self.pause_blink_cursor(cx);
2101 }
2102
2103 fn auto_close_target(&self, range: &Range<usize>, text: &str) -> Option<(usize, SharedString)> {
2106 if self.silent_replace_text
2107 || self.ime_marked_range.is_some()
2108 || !self.selections.is_single()
2109 || !self.active_selection().is_empty()
2110 || !range.is_empty()
2111 || range.start != self.cursor()
2112 || text.chars().count() != 1
2113 || !self.mode.is_auto_close()
2114 {
2115 return None;
2116 }
2117 let rules = self.mode.language_config()?;
2118 if self
2119 .text
2120 .chars_at(range.start)
2121 .next()
2122 .is_some_and(|c| !c.is_whitespace() && !rules.auto_close_before.contains(c))
2123 {
2124 return None;
2125 }
2126 for (open, close, not_in) in rules.closing_pairs() {
2127 let Some(prefix) = open.strip_suffix(text) else {
2128 continue;
2129 };
2130 if !self.text_before_matches(range.start, prefix) {
2131 continue;
2132 }
2133 let start = range.start - prefix.len();
2134 if open == close
2135 && (self.is_escaped_at(start)
2136 || self
2137 .text
2138 .chars_at(start)
2139 .reversed()
2140 .next()
2141 .is_some_and(|c| c.is_alphanumeric() || c == '_'))
2142 {
2143 continue;
2144 }
2145 if not_in.contains(&M::editing_syntax_context(self, start)) {
2146 continue;
2147 }
2148 return Some((open.len(), close.into()));
2149 }
2150 None
2151 }
2152
2153 fn text_before_matches(&self, offset: usize, text: &str) -> bool {
2154 offset >= text.len()
2155 && self.text.is_char_boundary(offset - text.len())
2156 && self.text.slice(offset - text.len()..offset) == text
2157 }
2158
2159 fn text_after_matches(&self, offset: usize, text: &str) -> bool {
2160 offset + text.len() <= self.text.len()
2161 && self.text.is_char_boundary(offset + text.len())
2162 && self.text.slice(offset..offset + text.len()) == text
2163 }
2164
2165 fn is_escaped_at(&self, offset: usize) -> bool {
2166 self.text
2167 .chars_at(offset)
2168 .reversed()
2169 .take_while(|c| *c == '\\')
2170 .count()
2171 % 2
2172 == 1
2173 }
2174
2175 fn skip_over_target(&self, new_text: &str) -> Option<usize> {
2182 if !self.mode.is_auto_close() || new_text.chars().count() != 1 {
2183 return None;
2184 }
2185 let rules = self.mode.language_config()?;
2186 let cursor = self.cursor();
2187 for (open, close, not_in) in rules.closing_pairs() {
2188 for (index, _) in close.char_indices() {
2189 if !close[index..].starts_with(new_text)
2190 || !self.text_before_matches(cursor, &close[..index])
2191 || !self.text_after_matches(cursor, &close[index..])
2192 {
2193 continue;
2194 }
2195 let context = M::editing_syntax_context(self, cursor);
2196 let generated =
2197 self.mode
2198 .auto_closed_pairs()
2199 .contains_closer(cursor, index, close.len());
2200 if !generated
2201 && not_in.contains(&context)
2202 && !(context == crate::input::SyntaxContext::String && open == close)
2203 {
2204 continue;
2205 }
2206 if self.is_escaped_at(cursor - index) {
2207 continue;
2208 }
2209 return Some(cursor + new_text.len());
2210 }
2211 }
2212 None
2213 }
2214
2215 pub(super) fn backspace(&mut self, _: &Backspace, window: &mut Window, cx: &mut Context<Self>) {
2216 if self.selections.is_single() && self.active_selection().is_empty() && self.cursor() == 0 {
2222 cx.propagate();
2223 return;
2224 }
2225
2226 if self.mode.is_auto_close()
2229 && self.selections.is_single()
2230 && self.active_selection().is_empty()
2231 {
2232 let off = self.cursor();
2233 let deletion = self.mode.language_config().and_then(|rules| {
2234 rules.closing_pairs().find_map(|(open, close, not_in)| {
2235 if !self.text_before_matches(off, open) || !self.text_after_matches(off, close)
2236 {
2237 return None;
2238 }
2239 let start = off - open.len();
2240 let generated = self
2241 .mode
2242 .auto_closed_pairs()
2243 .contains(start..off, off..off + close.len());
2244 if self.is_escaped_at(start)
2245 || (!generated && not_in.contains(&M::editing_syntax_context(self, start)))
2246 {
2247 return None;
2248 }
2249 Some(start..off + close.len())
2250 })
2251 });
2252 if let Some(range) = deletion {
2253 let utf16 = self.range_to_utf16(&range);
2254 self.replace_text_in_range_silent(Some(utf16), "", window, cx);
2255 return;
2256 }
2257 }
2258
2259 self.delete_selections(
2260 false,
2261 EditIntent::Backspace,
2262 |s, offset| s.previous_boundary(offset)..offset,
2263 window,
2264 cx,
2265 );
2266 }
2267
2268 pub(super) fn delete(&mut self, _: &Delete, window: &mut Window, cx: &mut Context<Self>) {
2269 self.delete_selections(
2270 false,
2271 EditIntent::DeleteForward,
2272 |s, offset| offset..s.next_boundary(offset),
2273 window,
2274 cx,
2275 );
2276 }
2277
2278 pub(super) fn delete_to_beginning_of_line(
2279 &mut self,
2280 _: &DeleteToBeginningOfLine,
2281 window: &mut Window,
2282 cx: &mut Context<Self>,
2283 ) {
2284 self.delete_selections(
2285 true,
2286 EditIntent::Atomic,
2287 |s, offset| {
2288 let mut start = s.start_of_line_at(offset, s.line_end_affinity_at(offset));
2289 if start == offset {
2290 start = start.saturating_sub(1);
2291 }
2292 start..offset
2293 },
2294 window,
2295 cx,
2296 );
2297 }
2298
2299 pub(super) fn delete_to_end_of_line(
2300 &mut self,
2301 _: &DeleteToEndOfLine,
2302 window: &mut Window,
2303 cx: &mut Context<Self>,
2304 ) {
2305 self.delete_selections(
2306 true,
2307 EditIntent::Atomic,
2308 |s, offset| {
2309 let mut end = s.end_of_line_at(offset, s.line_end_affinity_at(offset));
2310 if end == offset {
2311 end = (end + 1).clamp(0, s.text.len());
2312 }
2313 offset..end
2314 },
2315 window,
2316 cx,
2317 );
2318 }
2319
2320 pub(super) fn delete_previous_word(
2321 &mut self,
2322 _: &DeleteToPreviousWordStart,
2323 window: &mut Window,
2324 cx: &mut Context<Self>,
2325 ) {
2326 self.delete_selections(
2327 true,
2328 EditIntent::Atomic,
2329 |s, offset| s.previous_start_of_word_at(offset)..offset,
2330 window,
2331 cx,
2332 );
2333 }
2334
2335 pub(super) fn delete_next_word(
2336 &mut self,
2337 _: &DeleteToNextWordEnd,
2338 window: &mut Window,
2339 cx: &mut Context<Self>,
2340 ) {
2341 self.delete_selections(
2342 true,
2343 EditIntent::Atomic,
2344 |s, offset| offset..s.next_end_of_word_at(offset),
2345 window,
2346 cx,
2347 );
2348 }
2349
2350 pub(super) fn enter(&mut self, action: &Enter, window: &mut Window, cx: &mut Context<Self>) {
2351 if M::handle_context_menu_action(self, Box::new(action.clone()), window, cx) {
2352 return;
2353 }
2354
2355 if M::has_inline_completion(self) {
2357 M::clear_inline_completion(self, cx);
2358 }
2359
2360 let insert_newline = self.is_multi_line() && (!self.submit_on_enter || action.shift);
2365
2366 if insert_newline {
2367 if !self.selections.is_single() {
2368 self.selections.merge_overlapping();
2370 let selections: Vec<CursorSelection> = self.selections.iter().copied().collect();
2371 let mut edits: Vec<(Range<usize>, String)> = Vec::with_capacity(selections.len());
2372 for sel in &selections {
2373 let indent = if self.is_code_editor() {
2374 self.indent_of_next_line_at(sel.cursor_offset())
2375 } else {
2376 String::new()
2377 };
2378 edits.push((sel.start..sel.end, format!("\n{}", indent)));
2379 }
2380 edits.sort_by_key(|(range, _)| range.start);
2381 self.replace_text_in_ranges(&edits, window, cx);
2382 self.pause_blink_cursor(cx);
2383 } else {
2384 let mut split = false;
2390 if self.is_code_editor() && self.active_selection().is_empty() {
2391 if let Some(rules) = self.mode.language_config() {
2392 if self.mode.is_smart_indent()
2393 && M::editing_syntax_context(self, self.cursor())
2394 == crate::input::SyntaxContext::Code
2395 {
2396 let off = self.cursor();
2397 let is_pair = rules.brackets.iter().any(|p| {
2398 !p.open.is_empty()
2399 && !p.close.is_empty()
2400 && p.open != p.close
2401 && self.text_before_matches(off, p.open.as_ref())
2402 && self.text_after_matches(off, p.close.as_ref())
2403 });
2404 if is_pair {
2405 let line_end_affinity = self.line_end_affinity_at(off);
2407 let line_start = self.start_of_line_at(off, line_end_affinity);
2408 let mut indent = String::new();
2409 for c in self.text.slice(line_start..).chars() {
2410 if !c.is_whitespace() || c == '\n' || c == '\r' {
2411 break;
2412 }
2413 indent.push(c);
2414 }
2415 let mut inner = indent.clone();
2417 if self.mode.is_smart_indent() {
2418 let tab = self.mode.tab_size();
2419 if tab.hard_tabs {
2420 inner.push('\t');
2421 } else {
2422 for _ in 0..tab.tab_size {
2423 inner.push(' ');
2424 }
2425 }
2426 }
2427 let new_line_text = format!("\n{inner}\n{indent}");
2428 self.replace_text_in_range_silent(None, &new_line_text, window, cx);
2429 self.set_cursor_to(off + 1 + inner.len());
2430 self.update_preferred_column();
2431 let cursors = self.selections.iter().copied().collect::<Vec<_>>();
2432 self.undo_manager
2433 .record_selections(cursors.clone(), cursors);
2434 self.pause_blink_cursor(cx);
2435 split = true;
2436 }
2437 }
2438 }
2439 }
2440 if !split {
2441 let indent = if self.is_code_editor() {
2443 self.indent_of_next_line()
2444 } else {
2445 "".to_string()
2446 };
2447
2448 let new_line_text = format!("\n{}", indent);
2450 self.replace_text_in_range_silent(None, &new_line_text, window, cx);
2451 self.pause_blink_cursor(cx);
2452 }
2453 }
2454 } else {
2455 self.undo_manager.break_transaction_coalescing();
2458 cx.propagate();
2459 }
2460
2461 cx.emit(InputEvent::PressEnter {
2462 secondary: action.secondary,
2463 shift: action.shift,
2464 });
2465 }
2466
2467 pub fn clean(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2468 self.replace_text("", window, cx);
2469 self.set_selection(0, 0);
2470 self.scroll_to(0, None, cx);
2471 }
2472
2473 pub(super) fn escape(&mut self, action: &Escape, window: &mut Window, cx: &mut Context<Self>) {
2474 if M::handle_context_menu_action(self, Box::new(action.clone()), window, cx) {
2475 return;
2476 }
2477
2478 M::hide_context_menu(self, cx);
2480
2481 if !self.selections.is_single() {
2483 self.undo_manager.break_transaction_coalescing();
2484 self.selections.remove_all_but_active();
2485 cx.notify();
2486 return;
2487 }
2488
2489 if M::has_inline_completion(self) {
2491 M::clear_inline_completion(self, cx);
2492 return; }
2494 M::clear_inline_completion(self, cx);
2495
2496 if self.touch_selection().is_some() {
2498 self.dismiss_touch_selection(cx);
2499 return;
2500 }
2501
2502 if self.ime_marked_range.is_some() {
2503 self.unmark_text(window, cx);
2504 }
2505
2506 if self.clean_on_escape && !cfg!(target_os = "android") {
2509 return self.clean(window, cx);
2510 }
2511
2512 cx.propagate();
2513 }
2514
2515 pub(crate) fn handle_right_click_menu(
2517 &mut self,
2518 position: Point<Pixels>,
2519 offset: usize,
2520 window: &mut Window,
2521 cx: &mut Context<Self>,
2522 ) {
2523 if self.disabled {
2524 return;
2525 }
2526 if crate::GlobalState::is_in_deferred_context(cx) {
2527 return;
2528 }
2529
2530 if !self.active_selection().contains(offset) {
2531 self.move_to(offset, None, cx);
2532 }
2533
2534 if self.is_code_editor() {
2535 M::on_hover_definition(self, offset, window, cx);
2536 }
2537
2538 if let Some(handler) = self.context_menu_handler.clone() {
2539 let capabilities = self.context_menu_capabilities();
2540 let input = cx.weak_entity();
2541 window.defer(cx, move |window, cx| {
2544 if input.upgrade().is_none() {
2545 return;
2546 }
2547 let previous_focus = window.focused(cx);
2548 handler(NativeMenu::new(), capabilities, position, window, cx);
2549 input
2552 .update(cx, |this, cx| {
2553 if window.focused(cx) == previous_focus
2554 && !this.focus_handle.is_focused(window)
2555 && this.has_selection_focus(window, cx)
2556 {
2557 return;
2559 }
2560 let menu_focus = window.focused(cx).filter(|focus| {
2561 Some(focus) != previous_focus.as_ref() && *focus != this.focus_handle
2562 });
2563 this.set_selection_focus(menu_focus, cx);
2564 })
2565 .ok();
2566 });
2567 }
2568 }
2569
2570 pub(super) fn add_cursor_above(
2571 &mut self,
2572 _: &AddCursorAbove,
2573 _: &mut Window,
2574 cx: &mut Context<Self>,
2575 ) {
2576 self.add_cursor_vertical(-1, cx);
2577 }
2578
2579 pub(super) fn add_cursor_below(
2580 &mut self,
2581 _: &AddCursorBelow,
2582 _: &mut Window,
2583 cx: &mut Context<Self>,
2584 ) {
2585 self.add_cursor_vertical(1, cx);
2586 }
2587
2588 fn add_cursor_vertical(&mut self, move_lines: isize, cx: &mut Context<Self>) {
2593 if !self.is_multi_line() {
2594 return;
2595 }
2596
2597 self.pause_blink_cursor(cx);
2598 self.undo_manager.break_transaction_coalescing();
2600
2601 let sources: Vec<(usize, Option<(Pixels, usize)>, bool)> = self
2602 .selections
2603 .iter()
2604 .map(|sel| {
2605 (
2606 sel.cursor_offset(),
2607 sel.column_anchor,
2608 self.line_end_affinity_for(sel),
2609 )
2610 })
2611 .collect();
2612 let mut offsets: std::collections::HashSet<usize> =
2613 sources.iter().map(|(offset, _, _)| *offset).collect();
2614
2615 let mut newest: Option<usize> = None;
2616 for (offset, anchor, line_end_affinity) in sources {
2617 let anchor = anchor.or_else(|| self.preferred_column_for(offset));
2618 let (target, _) = self.vertical_target(offset, anchor, line_end_affinity, move_lines);
2619 if target == offset || offsets.contains(&target) {
2620 continue;
2621 }
2622 offsets.insert(target);
2623 let id = self.selections.generate_id();
2624 let mut cursor = CursorSelection::new(id, target, target);
2625 cursor.column_anchor = anchor;
2626 self.selections.add(cursor);
2627 newest = Some(target);
2628 }
2629
2630 if let Some(newest) = newest {
2631 self.scroll_to(newest, None, cx);
2632 }
2633 cx.notify();
2634 }
2635
2636 pub(super) fn add_cursor_at(&mut self, offset: usize, cx: &mut Context<Self>) {
2641 if !self.is_multi_line() {
2642 return;
2643 }
2644
2645 for sel in self.selections.iter() {
2646 if sel.contains(offset) {
2647 return;
2648 }
2649 if sel.is_collapsed() && sel.cursor_offset() == offset {
2650 return;
2651 }
2652 }
2653
2654 self.undo_manager.break_transaction_coalescing();
2655 let id = self.selections.generate_id();
2656 let mut selection = CursorSelection::new(id, offset, offset);
2657 selection.column_anchor = self.preferred_column_for(offset);
2658 self.selections.add(selection);
2659 cx.notify();
2660 }
2661
2662 pub(super) fn build_columnar_selection(
2670 &mut self,
2671 start: ColumnarPoint,
2672 end: ColumnarPoint,
2673 cx: &mut Context<Self>,
2674 ) {
2675 if !self.is_multi_line() {
2676 return;
2677 }
2678
2679 self.undo_manager.break_transaction_coalescing();
2680
2681 let (start_row, start_col) = self.columnar_row_column(start);
2682 let (end_row, end_col) = self.columnar_row_column(end);
2683 let (start_row, end_row) = (start_row.min(end_row), start_row.max(end_row));
2684 let (start_col, end_col) = (start_col.min(end_col), start_col.max(end_col));
2685
2686 let mut new_selections = Vec::with_capacity(end_row - start_row + 1);
2687 for row in start_row..=end_row {
2688 let sel_start = self
2689 .display_map
2690 .display_row_column_to_offset(row, start_col);
2691 let sel_end = self.display_map.display_row_column_to_offset(row, end_col);
2692 let id = self.selections.generate_id();
2693 let sel_start = self.text.clip_offset(sel_start, Bias::Left);
2694 let sel_end = self.text.clip_offset(sel_end, Bias::Left);
2695 new_selections.push(CursorSelection::new(id, sel_start, sel_end));
2696 }
2697
2698 self.selections.replace_all(new_selections);
2699 cx.notify();
2700 }
2701
2702 fn columnar_row_column(&self, point: ColumnarPoint) -> (usize, usize) {
2705 let display_point = self.display_map.offset_to_wrap_display_point(point.offset);
2706 let row = self
2707 .display_map
2708 .wrap_row_to_display_row(display_point.row)
2709 .unwrap_or_else(|| {
2710 self.display_map
2711 .nearest_visible_display_row(display_point.row)
2712 });
2713
2714 (row, display_point.column + point.columns_past_line_end)
2715 }
2716
2717 pub(super) fn on_mouse_down(
2718 &mut self,
2719 event: &MouseDownEvent,
2720 window: &mut Window,
2721 cx: &mut Context<Self>,
2722 ) {
2723 if window.default_prevented() {
2726 return;
2727 }
2728 if event.button == MouseButton::Left
2731 && crate::GlobalState::is_touch_press(cx)
2732 && !self.disabled
2733 && !self.readonly
2734 {
2735 window.request_virtual_keyboard();
2736 }
2737 self.undo_manager.break_transaction_coalescing();
2738 if !self.disabled {
2742 self.focus(window, cx);
2743 self.pause_blink_cursor(cx);
2744 }
2745 crate::global_state::GlobalState::suppress_text_selection(cx);
2748
2749 M::clear_inline_completion(self, cx);
2751 if event.button == MouseButton::Left
2755 && event.click_count == 1
2756 && self.reopen_edit_menu_at(event.position, cx)
2757 {
2758 return;
2759 }
2760 self.dismiss_touch_selection(cx);
2761
2762 if let Some(ime_marked_range) = &self.ime_marked_range {
2765 if ime_marked_range.len() == 0 {
2766 self.ime_marked_range = None;
2767 }
2768 }
2769
2770 self.selecting = true;
2771 let (offset, line_end_affinity, columns_past_line_end) =
2772 self.resolve_mouse_position(event.position);
2773
2774 if M::on_click(self, event, offset, window, cx) {
2775 return;
2776 }
2777
2778 if event.button == MouseButton::Left && event.click_count >= 3 {
2780 self.select_line(offset, window, cx);
2781 return;
2782 }
2783
2784 if event.button == MouseButton::Left && event.click_count == 2 {
2786 self.select_word(offset, window, cx);
2787 if crate::GlobalState::is_touch_press(cx) {
2790 self.keep_touch_selection(cx);
2791 }
2792 return;
2793 }
2794
2795 if event.button == MouseButton::Right {
2797 if self.enable_context_menu {
2798 if !self.active_selection().contains(offset) {
2799 self.move_to(offset, None, cx);
2800 }
2801 self.pending_context_menu = Some((event.position, offset));
2802 }
2803 return;
2804 }
2805
2806 if self.is_multi_line() && event.button == MouseButton::Left {
2808 if event.modifiers.alt
2809 && (event.modifiers.shift || (cfg!(target_os = "linux") && event.modifiers.control))
2810 {
2811 self.column_select_start = Some(ColumnarPoint::new(offset, columns_past_line_end));
2814 self.selecting = true;
2815 self.move_to_with_affinity(offset, None, line_end_affinity, cx);
2816 return;
2817 } else if event.modifiers.alt {
2818 self.add_cursor_at(offset, cx);
2819 self.column_select_start = Some(ColumnarPoint::new(offset, columns_past_line_end));
2822 return;
2823 }
2824 }
2825
2826 if event.modifiers.shift {
2827 self.select_to_with_affinity(offset, line_end_affinity, cx);
2828 } else {
2829 self.move_to_with_affinity(offset, None, line_end_affinity, cx)
2830 }
2831 }
2832
2833 pub(super) fn on_mouse_up(
2834 &mut self,
2835 event: &MouseUpEvent,
2836 window: &mut Window,
2837 cx: &mut Context<Self>,
2838 ) {
2839 if event.button == MouseButton::Right {
2840 if let Some((position, offset)) = self.pending_context_menu.take() {
2841 self.handle_right_click_menu(position, offset, window, cx);
2842 }
2843 }
2844 if self.active_selection().is_empty() {
2845 self.active_selection_mut().reversed = false;
2846 }
2847 self.selecting = false;
2848 self.selected_word_range = None;
2849 self.column_select_start = None;
2850 self.auto_scroll.stop();
2851 }
2852
2853 pub(super) fn on_mouse_move(
2854 &mut self,
2855 event: &MouseMoveEvent,
2856 window: &mut Window,
2857 cx: &mut Context<Self>,
2858 ) {
2859 let within_bounds = self.input_bounds.contains(&event.position);
2861
2862 if !within_bounds {
2863 M::clear_hover_state(self, cx);
2865 return;
2866 }
2867
2868 let (offset, _) = self.index_for_mouse_position(event.position);
2870 M::on_mouse_move(self, offset, event, window, cx);
2871
2872 if self.is_code_editor() {
2873 match self
2877 .mode
2878 .diagnostics()
2879 .and_then(|set| set.for_offset(offset))
2880 {
2881 Some(diagnostic) => {
2882 if self.diagnostic_popover.as_deref() != Some(diagnostic) {
2883 self.diagnostic_popover = Some(Rc::new(diagnostic.clone()));
2884 cx.notify();
2885 }
2886 }
2887 None => {
2888 if self.diagnostic_popover.take().is_some() {
2889 cx.notify();
2890 }
2891 }
2892 }
2893 }
2894 }
2895
2896 pub(super) fn on_scroll_wheel(
2897 &mut self,
2898 event: &ScrollEvent,
2899 window: &mut Window,
2900 cx: &mut Context<Self>,
2901 ) {
2902 let line_height = self
2903 .last_layout
2904 .as_ref()
2905 .map(|layout| layout.line_height)
2906 .unwrap_or(window.line_height());
2907 let delta = event.delta.pixel_delta(line_height);
2908
2909 let old_offset = self.scroll_handle.offset();
2910 let precise = matches!(event.delta, gpui::ScrollDelta::Pixels(_));
2911 if !precise || !self.scroll_bounce || cx.reduce_motion() || !self.overflows_vertically() {
2912 let displaced = self.overscroll.offset() != 0.;
2913 self.overscroll.reset();
2914 if displaced {
2915 cx.notify();
2916 }
2917 }
2918 let bounces = self.scroll_bounce
2919 && precise
2920 && !cx.reduce_motion()
2921 && delta.y.abs() >= delta.x.abs()
2922 && self.overflows_vertically();
2923 if bounces {
2924 let mut overscroll = std::mem::take(&mut self.overscroll);
2925 let stretched = overscroll.packet(
2926 delta.y.as_f32(),
2927 event.touch_phase,
2928 self.input_bounds.size.height.as_f32(),
2929 cx.background_executor().now(),
2930 |dy| {
2931 let before = self.scroll_handle.offset();
2932 self.update_scroll_offset(
2933 Some(point(before.x + delta.x, before.y + px(dy))),
2934 cx,
2935 );
2936 (self.scroll_handle.offset().y - before.y).as_f32()
2937 },
2938 );
2939 self.overscroll = overscroll;
2940 if stretched {
2941 cx.notify();
2942 }
2943 } else {
2944 if matches!(
2946 event.touch_phase,
2947 gpui::TouchPhase::Ended | gpui::TouchPhase::Cancelled
2948 ) && self.overscroll.offset() != 0.
2949 {
2950 self.overscroll.packet(
2951 0.,
2952 event.touch_phase,
2953 self.input_bounds.size.height.as_f32(),
2954 cx.background_executor().now(),
2955 |_| 0.,
2956 );
2957 cx.notify();
2958 }
2959 self.update_scroll_offset(Some(old_offset + delta), cx);
2960 }
2961
2962 if self.scroll_handle.offset() != old_offset || self.overflows_along(delta) {
2966 cx.stop_propagation();
2967 }
2968
2969 if self.diagnostic_popover.take().is_some() {
2970 cx.notify();
2971 }
2972 self.edit_menu_on_scroll(event.touch_phase, cx);
2975 }
2976
2977 pub(super) fn takes_scroll(&self, delta: Point<Pixels>) -> bool {
2983 if delta.y.abs() >= delta.x.abs() {
2984 self.overflows_vertically()
2985 } else {
2986 self.scroll_size.width > self.input_bounds.size.width
2987 }
2988 }
2989
2990 fn overflows_along(&self, delta: Point<Pixels>) -> bool {
2994 if delta.y.abs() >= delta.x.abs() {
2995 delta.y != px(0.) && self.overflows_vertically()
2996 } else {
2997 self.scroll_size.width > self.input_bounds.size.width
2998 }
2999 }
3000
3001 fn overflows_vertically(&self) -> bool {
3003 self.is_multi_line() && self.scroll_size.height > self.input_bounds.size.height
3004 }
3005
3006 pub(super) fn update_scroll_offset(
3007 &mut self,
3008 offset: Option<Point<Pixels>>,
3009 cx: &mut Context<Self>,
3010 ) {
3011 let mut offset = offset.unwrap_or(self.scroll_handle.offset());
3012 let safe_y_range =
3013 (-self.scroll_size.height + self.input_bounds.size.height).min(px(0.0))..px(0.);
3014
3015 offset.y = if self.is_single_line() {
3016 px(0.)
3017 } else {
3018 offset.y.clamp(safe_y_range.start, safe_y_range.end)
3019 };
3020 offset.x = clamp_horizontal_scroll_offset(
3021 offset.x,
3022 self.scroll_size.width,
3023 self.input_bounds.size.width,
3024 self.text_align,
3025 );
3026 if self.scroll_handle.offset() != offset {
3027 self.scroll_handle.set_offset(offset);
3028 cx.notify();
3029 }
3030 }
3031
3032 pub(crate) fn scroll_to(
3036 &mut self,
3037 offset: usize,
3038 direction: Option<MoveDirection>,
3039 cx: &mut Context<Self>,
3040 ) {
3041 let padding = if direction.is_some() {
3042 ScrollPadding::SurroundingLines
3043 } else {
3044 ScrollPadding::Minimal
3045 };
3046 self.scroll_to_with_padding(offset, direction, padding, cx);
3047 }
3048
3049 pub(crate) fn scroll_to_with_padding(
3052 &mut self,
3053 offset: usize,
3054 direction: Option<MoveDirection>,
3055 padding: ScrollPadding,
3056 cx: &mut Context<Self>,
3057 ) {
3058 let Some(last_layout) = self.last_layout.as_ref() else {
3059 return;
3060 };
3061 let Some(bounds) = self.last_bounds.as_ref() else {
3062 return;
3063 };
3064
3065 let mut scroll_offset = self.scroll_handle.offset();
3066 let was_offset = scroll_offset;
3067 let line_height = last_layout.line_height;
3068
3069 let point = self.text.offset_to_point(offset);
3070
3071 let row = point.row;
3072
3073 let display_row = self
3075 .display_map
3076 .offset_to_display_row(offset, self.line_end_affinity_at(offset));
3077 let row_offset_y = line_height * display_row;
3078
3079 let safety_margin = match last_layout.text_align {
3082 TextAlign::Left => RIGHT_MARGIN,
3083 TextAlign::Right => px(0.),
3084 TextAlign::Center => CURSOR_WIDTH,
3085 };
3086 if let Some(vi) = last_layout
3087 .visible_buffer_lines
3088 .iter()
3089 .position(|&line| line == row)
3090 {
3091 let line = &last_layout.lines[vi];
3092 let local_offset = offset.saturating_sub(last_layout.visible_line_byte_offsets[vi]);
3093 if let Some(pos) = line.position_for_index(local_offset, last_layout, false) {
3094 let bounds_width = bounds.size.width - last_layout.line_number_width;
3095 let col_offset_x = pos.x;
3096 if col_offset_x - safety_margin < -scroll_offset.x {
3097 scroll_offset.x = -col_offset_x + safety_margin;
3099 } else if col_offset_x + safety_margin > -scroll_offset.x + bounds_width {
3100 scroll_offset.x = -(col_offset_x - bounds_width + safety_margin);
3101 }
3102 }
3103 }
3104
3105 let edge_height =
3110 if matches!(padding, ScrollPadding::SurroundingLines) && self.is_code_editor() {
3111 super::element::cursor_surrounding_padding(
3112 self.mode.is_auto_grow(),
3113 self.cursor_surrounding_lines,
3114 super::element::viewport_visible_lines(bounds.size.height, line_height),
3115 line_height,
3116 )
3117 } else {
3118 px(0.)
3119 };
3120 scroll_offset.y = super::element::reveal_row(
3121 scroll_offset.y,
3122 row_offset_y,
3123 line_height,
3124 bounds.size.height,
3125 edge_height,
3126 );
3127
3128 if direction == Some(MoveDirection::Up) {
3130 scroll_offset.y = scroll_offset.y.max(was_offset.y);
3131 } else if direction == Some(MoveDirection::Down) {
3132 scroll_offset.y = scroll_offset.y.min(was_offset.y);
3133 }
3134
3135 let safe_y_min = (-self.scroll_size.height + self.input_bounds.size.height).min(px(0.));
3139 scroll_offset.x = scroll_offset.x.min(px(0.));
3140 scroll_offset.y = scroll_offset.y.clamp(safe_y_min, px(0.));
3141 self.deferred_scroll_offset = Some(scroll_offset);
3142 cx.notify();
3143 }
3144
3145 pub(super) fn show_character_palette(
3146 &mut self,
3147 _: &ShowCharacterPalette,
3148 window: &mut Window,
3149 _: &mut Context<Self>,
3150 ) {
3151 window.show_character_palette();
3152 }
3153
3154 fn selected_texts(&self) -> Vec<String> {
3156 let mut selections: Vec<CursorSelection> = self
3157 .selections
3158 .iter()
3159 .copied()
3160 .filter(|sel| !sel.is_empty())
3161 .collect();
3162 selections.sort_by_key(|sel| sel.start);
3163 selections
3164 .iter()
3165 .map(|sel| self.text.slice(*sel).to_string())
3166 .collect()
3167 }
3168
3169 pub(super) fn copy(&mut self, _: &Copy, _: &mut Window, cx: &mut Context<Self>) {
3170 if !self.is_copyable() {
3171 return;
3172 }
3173
3174 let texts = self.selected_texts();
3175
3176 cx.write_to_clipboard(ClipboardItem::new_string(texts.join("\n")));
3177 }
3178
3179 pub(super) fn cut(&mut self, _: &Cut, window: &mut Window, cx: &mut Context<Self>) {
3180 if !self.is_copyable() {
3181 return;
3182 }
3183
3184 let texts = self.selected_texts();
3185
3186 cx.write_to_clipboard(ClipboardItem::new_string(texts.join("\n")));
3187
3188 self.undo_manager.set_pending_intent(EditIntent::Atomic);
3189 self.replace_text_in_range_silent(None, "", window, cx);
3190 }
3191
3192 pub(super) fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
3193 if !self.is_editable() {
3194 return;
3195 }
3196 if let Some(clipboard) = cx.read_from_clipboard() {
3197 self.insert_clipboard(clipboard, window, cx);
3198 return;
3199 }
3200 let read = cx.read_from_clipboard_async();
3205 let target = self.paste_target();
3206 cx.spawn_in(window, async move |this, cx| match read.await {
3207 Ok(Some(clipboard)) => {
3208 this.update_in(cx, |this, window, cx| {
3209 if this.is_editable()
3216 && this.focus_handle.is_focused(window)
3217 && this.paste_target() == target
3218 {
3219 this.insert_clipboard(clipboard, window, cx);
3220 }
3221 })
3222 .ok();
3223 }
3224 Ok(None) => {}
3225 Err(error) => tracing::warn!("failed to read the clipboard for paste: {error}"),
3226 })
3227 .detach();
3228 }
3229
3230 fn paste_target(&self) -> PasteTarget {
3234 PasteTarget {
3235 document_revision: self.document_revision,
3236 selections: self.selections.iter().copied().collect(),
3237 }
3238 }
3239
3240 fn insert_clipboard(
3246 &mut self,
3247 clipboard: ClipboardItem,
3248 window: &mut Window,
3249 cx: &mut Context<Self>,
3250 ) {
3251 let Some(mut new_text) = clipboard.text().filter(|text| !text.is_empty()) else {
3252 return;
3253 };
3254 self.undo_manager.set_pending_intent(EditIntent::Atomic);
3256
3257 if !self.is_multi_line() {
3258 new_text = new_text.replace('\n', "");
3259 self.replace_text_in_range_silent(None, &new_text, window, cx);
3260 self.scroll_to(self.cursor(), None, cx);
3261 return;
3262 }
3263
3264 if !self.selections.is_single() {
3267 self.selections.merge_overlapping();
3268 }
3269 let lines: Vec<String> = new_text.split('\n').map(|s| s.to_string()).collect();
3270 let count = self.selections.len();
3271 if count > 1 && lines.len() == count {
3272 let mut selections: Vec<CursorSelection> = self.selections.iter().copied().collect();
3273 selections.sort_by_key(|sel| sel.start);
3274 let edits: Vec<(Range<usize>, String)> = selections
3275 .iter()
3276 .zip(lines)
3277 .map(|(sel, line)| (sel.start..sel.end, line))
3278 .collect();
3279 self.replace_text_in_ranges(&edits, window, cx);
3280 } else {
3281 self.replace_text_in_range_silent(None, &new_text, window, cx);
3282 }
3283 self.scroll_to(self.cursor(), None, cx);
3284 }
3285
3286 fn typing_intent(&self, edits: &[(Range<usize>, String)], new_text: &str) -> EditIntent {
3289 if !new_text.is_empty()
3290 && !new_text.contains(['\n', '\r'])
3291 && edits.iter().all(|(range, _)| range.is_empty())
3292 {
3293 EditIntent::Typing
3294 } else {
3295 EditIntent::Atomic
3296 }
3297 }
3298
3299 fn collapse_for_intent(
3305 intent: EditIntent,
3306 mut selection: CursorSelection,
3307 range: &Range<usize>,
3308 ) -> CursorSelection {
3309 match intent {
3310 EditIntent::Backspace => selection.place_at(range.end, None),
3311 EditIntent::DeleteForward => selection.place_at(range.start, None),
3312 EditIntent::Typing | EditIntent::Atomic => {}
3313 }
3314 selection
3315 }
3316
3317 fn rewrites_typed_char(
3320 &self,
3321 selection: CursorSelection,
3322 range: &Range<usize>,
3323 old_text: &str,
3324 new_text: &str,
3325 ) -> bool {
3326 !self.silent_replace_text
3327 && selection.is_collapsed()
3328 && selection.cursor_offset() == range.end
3329 && old_text.chars().count() == 1
3330 && !old_text.contains(['\n', '\r'])
3331 && !new_text.contains(['\n', '\r'])
3332 }
3333
3334 fn push_history(
3335 &mut self,
3336 text: &Rope,
3337 range: &Range<usize>,
3338 new_text: &str,
3339 requested_intent: Option<EditIntent>,
3340 selection_before: CursorSelection,
3341 selection_after: Option<CursorSelection>,
3342 ) -> bool {
3343 self.document_revision = self.document_revision.wrapping_add(1);
3344 let token_delta = self.edit_tokens(range, new_text.len());
3345 if self.undo_manager.is_ignoring() {
3346 return false;
3347 }
3348
3349 let range =
3350 text.clip_offset(range.start, Bias::Left)..text.clip_offset(range.end, Bias::Right);
3351 let old_text = text.slice(range.clone()).to_string();
3352 let new_range = range.start..range.start + new_text.len();
3353
3354 let intent = requested_intent.unwrap_or_else(|| {
3355 if range.is_empty()
3356 && old_text.is_empty()
3357 && !new_text.is_empty()
3358 && !new_text.contains(['\n', '\r'])
3359 || self.rewrites_typed_char(selection_before, &range, &old_text, new_text)
3360 {
3361 EditIntent::Typing
3362 } else {
3363 EditIntent::Atomic
3364 }
3365 });
3366
3367 let selection_before = Self::collapse_for_intent(intent, selection_before, &range);
3368 let selection_after =
3369 selection_after.unwrap_or_else(|| (new_range.end..new_range.end).into());
3370
3371 let open_transaction = self.undo_manager.has_open_transaction();
3372 let mut change = Change::new(range, &old_text, new_range, new_text);
3373 change.token_delta = token_delta;
3374 let recorded = self.undo_manager.record_transaction(change, intent);
3375 if recorded && !open_transaction {
3378 self.undo_manager
3379 .record_selections(vec![selection_before], vec![selection_after]);
3380 }
3381 recorded
3382 }
3383
3384 pub(super) fn undo(&mut self, _: &Undo, window: &mut Window, cx: &mut Context<Self>) {
3385 self.replay_history(true, window, cx);
3386 }
3387
3388 pub(super) fn redo(&mut self, _: &Redo, window: &mut Window, cx: &mut Context<Self>) {
3389 self.replay_history(false, window, cx);
3390 }
3391
3392 fn replay_history(&mut self, undo: bool, window: &mut Window, cx: &mut Context<Self>) {
3393 if !self.is_editable() {
3394 return;
3395 }
3396 let replay = if undo {
3397 self.undo_manager.undo()
3398 } else {
3399 self.undo_manager.redo()
3400 };
3401 let Some(replay) = replay else {
3402 return;
3403 };
3404 let token_aware = self.inline_tokens.is_some()
3405 || replay
3406 .changes
3407 .iter()
3408 .any(|change| change.token_delta.is_some());
3409 let emit_events = self.emit_events;
3410 if token_aware {
3411 self.emit_events = false;
3412 self.last_input_edits.clear();
3413 }
3414 self.replaying_history = token_aware;
3415 self.undo_manager.set_ignoring(true);
3416 for change in &replay.changes {
3417 let (range, text) = if undo {
3418 (change.new_range.into(), &change.old_text)
3419 } else {
3420 (change.old_range.into(), &change.new_text)
3421 };
3422 let range_utf16 = self.range_to_utf16(&range);
3423 self.replace_text_in_range_silent(Some(range_utf16), text, window, cx);
3424 self.replay_tokens(&range, text.len(), change.token_delta.as_deref(), undo);
3425 }
3426 self.restore_selections(replay.selections);
3427 self.mode
3428 .restore_auto_closed_pairs(replay.auto_closed_pairs.unwrap_or_default());
3429 if token_aware {
3430 self.ime_marked_range = None;
3433 }
3434 self.undo_manager.set_ignoring(false);
3435 self.replaying_history = false;
3436 self.emit_events = emit_events;
3437 if token_aware {
3438 if emit_events {
3439 self.emit_change(cx);
3440 } else {
3441 self.last_input_edits.clear();
3442 }
3443 }
3444 cx.notify();
3445 }
3446
3447 fn restore_selections(&mut self, selections: Option<Vec<CursorSelection>>) {
3451 let Some(selections) = selections else {
3452 return;
3453 };
3454
3455 let len = self.text.len();
3456 let restored: Vec<CursorSelection> = selections
3457 .into_iter()
3458 .map(|mut sel| {
3459 sel.start = sel.start.min(len);
3460 sel.end = sel.end.min(len);
3461 sel
3462 })
3463 .collect();
3464 self.selections.replace_all(restored);
3465 self.selections.merge_overlapping();
3466 }
3467
3468 pub fn cursor(&self) -> usize {
3472 if let Some(ime_marked_range) = &self.ime_marked_range {
3473 return ime_marked_range.end;
3474 }
3475
3476 self.selections.active().cursor_offset()
3477 }
3478
3479 pub(super) fn active_selection(&self) -> &CursorSelection {
3481 self.selections.active()
3482 }
3483
3484 pub(super) fn active_selection_mut(&mut self) -> &mut CursorSelection {
3486 self.selections.active_mut()
3487 }
3488
3489 pub(super) fn set_selection(&mut self, start: usize, end: usize) {
3492 let range = self.normalize_token_range(start..end);
3493 let (start, end) = (range.start, range.end);
3494 let active = self.active_selection_mut();
3495 active.start = start;
3496 active.end = end;
3497 }
3498
3499 pub(super) fn set_cursor_to(&mut self, offset: usize) {
3502 let offset = self.token_boundary(offset, Bias::Right);
3503 let active = self.active_selection_mut();
3504 active.start = offset;
3505 active.end = offset;
3506 active.reversed = false;
3507 }
3508
3509 pub fn visible_row_range(&self) -> Option<std::ops::Range<usize>> {
3511 self.last_layout.as_ref().map(|l| l.visible_range.clone())
3512 }
3513
3514 pub fn scroll_offset(&self) -> gpui::Point<gpui::Pixels> {
3516 self.scroll_handle.offset()
3517 }
3518
3519 pub fn set_scroll_offset(&mut self, offset: gpui::Point<gpui::Pixels>, cx: &mut Context<Self>) {
3523 self.deferred_scroll_offset = Some(offset);
3524 cx.notify();
3525 }
3526
3527 pub fn line_height(&self) -> Option<gpui::Pixels> {
3529 self.last_layout.as_ref().map(|l| l.line_height)
3530 }
3531
3532 pub fn selected_range(&self) -> std::ops::Range<usize> {
3540 (*self.selections.active()).into()
3541 }
3542
3543 pub fn is_selection_reversed(&self) -> bool {
3548 let selection = self.selections.active();
3549 !selection.is_empty() && selection.reversed
3550 }
3551
3552 pub fn select_all(&mut self, _: &mut Window, cx: &mut Context<Self>) {
3553 self.undo_manager.break_transaction_coalescing();
3554 self.selections.remove_all_but_active();
3555 self.set_selection(0, self.text.len());
3556 cx.notify();
3557 }
3558
3559 pub fn set_selected_range(&mut self, range: Range<usize>, cx: &mut Context<Self>) {
3564 self.set_selected_range_with_direction(range, false, cx);
3565 }
3566
3567 pub fn set_selected_range_with_direction(
3574 &mut self,
3575 range: Range<usize>,
3576 reversed: bool,
3577 cx: &mut Context<Self>,
3578 ) {
3579 let range = self.normalize_token_range(range);
3580 let end_bias = if range.start == range.end {
3581 Bias::Left
3582 } else {
3583 Bias::Right
3584 };
3585 let start = self.text.clip_offset(range.start, Bias::Left);
3586 let end = self.text.clip_offset(range.end, end_bias);
3587
3588 let (anchor, head) = if reversed { (end, start) } else { (start, end) };
3589 self.move_to(anchor, None, cx);
3590 self.active_selection_mut().reversed = false;
3591 self.selected_word_range = None;
3592 self.select_to(head, cx);
3593 }
3594
3595 pub fn add_selection(&mut self, range: Range<usize>, cx: &mut Context<Self>) {
3606 if !self.is_multi_line() {
3607 self.set_selected_range(range, cx);
3608 return;
3609 }
3610
3611 let range = self.normalize_token_range(range);
3612 let end_bias = if range.start == range.end {
3613 Bias::Left
3614 } else {
3615 Bias::Right
3616 };
3617 let start = self.cursor_boundary(range.start, Bias::Left);
3618 let end = self.cursor_boundary(self.text.clip_offset(range.end, end_bias), Bias::Left);
3621
3622 for sel in self.selections.iter() {
3623 if sel.start <= start && end <= sel.end {
3624 return;
3625 }
3626 }
3627
3628 self.undo_manager.break_transaction_coalescing();
3629 self.selected_word_range = None;
3630 self.pause_blink_cursor(cx);
3631 M::hide_context_menu(self, cx);
3632 M::clear_inline_completion(self, cx);
3633 let id = self.selections.generate_id();
3634 let mut selection = CursorSelection::new(id, start, end);
3635 selection.column_anchor = self.preferred_column_for(end);
3636 self.selections.add(selection);
3637 self.selections.merge_overlapping();
3638 self.scroll_to(end, None, cx);
3639 cx.notify();
3640 }
3641
3642 pub(crate) fn index_for_mouse_position(&self, position: Point<Pixels>) -> (usize, bool) {
3650 let (offset, line_end_affinity, _) = self.resolve_mouse_position(position);
3651 (offset, line_end_affinity)
3652 }
3653
3654 pub(super) fn resolve_mouse_position(&self, position: Point<Pixels>) -> (usize, bool, usize) {
3661 if self.text.len() == 0 {
3663 return (0, false, 0);
3664 }
3665
3666 let (Some(bounds), Some(last_layout)) =
3667 (self.last_bounds.as_ref(), self.last_layout.as_ref())
3668 else {
3669 return (0, false, 0);
3670 };
3671
3672 let line_height = last_layout.line_height;
3673 let line_number_width = last_layout.line_number_width;
3674
3675 let inner_position = position - bounds.origin - point(line_number_width, px(0.));
3686
3687 let mut y_offset = last_layout.visible_top;
3688 let mut last_line_pos = None;
3691
3692 for (vi, (line_layout, _buffer_line)) in last_layout
3694 .lines
3695 .iter()
3696 .zip(last_layout.visible_buffer_lines.iter())
3697 .enumerate()
3698 {
3699 let line_start_offset = last_layout.visible_line_byte_offsets[vi];
3700
3701 let line_origin = point(px(0.), y_offset);
3703 let pos = inner_position - line_origin;
3704
3705 if self.is_single_line() {
3707 let local_index = line_layout.closest_index_for_x(pos.x, last_layout);
3708 return (
3710 self.resolve_index(line_start_offset + local_index),
3711 false,
3712 0,
3713 );
3714 }
3715
3716 if let Some((local_index, line_end_affinity)) =
3718 line_layout.closest_index_for_position(pos, last_layout)
3719 {
3720 return (
3721 self.resolve_index(line_start_offset + local_index),
3722 line_end_affinity,
3723 line_layout.columns_past_line_end(pos, last_layout),
3724 );
3725 } else if pos.y < px(0.) {
3726 return (self.resolve_index(line_start_offset), false, 0);
3728 }
3729
3730 y_offset += line_layout.size(line_height).height;
3731 last_line_pos = Some(pos);
3732 }
3733
3734 let last_position = last_layout
3737 .lines
3738 .last()
3739 .zip(last_line_pos)
3740 .zip(last_layout.visible_line_byte_offsets.last())
3741 .map(|((line_layout, pos), line_start)| {
3742 let last_row_top = (line_layout.size(line_height).height - line_height).max(px(0.));
3743 let pos = point(pos.x, last_row_top);
3744 (
3745 self.resolve_index(line_start + line_layout.len()),
3746 false,
3747 line_layout.columns_past_line_end(pos, last_layout),
3748 )
3749 });
3750
3751 last_position.unwrap_or((self.text.len(), false, 0))
3752 }
3753
3754 fn resolve_index(&self, index: usize) -> usize {
3757 if self.masked {
3758 self.text.char_index_to_offset(index / MASK_CHAR.len_utf8())
3759 } else {
3760 index.min(self.text.len())
3761 }
3762 }
3763
3764 fn extend_selection(
3774 sel: &mut CursorSelection,
3775 offset: usize,
3776 word_range: Option<CursorSelection>,
3777 ) {
3778 if sel.reversed {
3779 sel.start = offset;
3780 } else {
3781 sel.end = offset;
3782 }
3783
3784 if sel.end < sel.start {
3785 sel.reversed = !sel.reversed;
3786 std::mem::swap(&mut sel.start, &mut sel.end);
3787 }
3788
3789 if let Some(word_range) = word_range {
3790 if sel.start > word_range.start {
3791 sel.start = word_range.start;
3792 }
3793 if sel.end < word_range.end {
3794 sel.end = word_range.end;
3795 }
3796 }
3797 }
3798
3799 pub(crate) fn select_to(&mut self, offset: usize, cx: &mut Context<Self>) {
3801 self.select_to_with_affinity(offset, false, cx);
3802 }
3803
3804 pub(crate) fn select_to_with_affinity(
3811 &mut self,
3812 offset: usize,
3813 line_end_affinity: bool,
3814 cx: &mut Context<Self>,
3815 ) {
3816 M::clear_inline_completion(self, cx);
3817
3818 self.cursor_line_end_affinity = line_end_affinity;
3819 let offset = self.cursor_boundary(offset, Bias::Left);
3820 let word_range = self.selected_word_range;
3821 Self::extend_selection(self.active_selection_mut(), offset, word_range);
3822 let range = self.normalize_token_range(self.selected_range());
3823 self.set_selection(range.start, range.end);
3824
3825 if self.active_selection().is_empty() {
3826 self.update_preferred_column();
3827 }
3828 cx.notify()
3829 }
3830
3831 fn select_all_cursors_to(
3834 &mut self,
3835 f: impl Fn(&Self, &CursorSelection) -> usize,
3836 cx: &mut Context<Self>,
3837 ) {
3838 self.select_all_cursors_to_with_affinity(|s, sel| (f(s, sel), false), false, cx);
3839 }
3840
3841 fn select_all_cursors_to_with_affinity(
3844 &mut self,
3845 f: impl Fn(&Self, &CursorSelection) -> (usize, bool),
3846 preserve_column: bool,
3847 cx: &mut Context<Self>,
3848 ) {
3849 self.pause_blink_cursor(cx);
3850 self.undo_manager.break_transaction_coalescing();
3851 M::clear_inline_completion(self, cx);
3852
3853 let mut active_affinity = false;
3854 let new_selections: Vec<CursorSelection> = self
3855 .selections
3856 .iter()
3857 .map(|sel| {
3858 let (offset, affinity) = f(self, sel);
3859 if sel.id == self.active_selection().id {
3860 active_affinity = affinity;
3861 }
3862 let offset = self.cursor_boundary(offset, Bias::Left);
3863 let mut new_sel = *sel;
3864 Self::extend_selection(&mut new_sel, offset, None);
3865 let range = self.normalize_token_range(new_sel.start..new_sel.end);
3866 new_sel.start = range.start;
3867 new_sel.end = range.end;
3868 if !preserve_column {
3869 new_sel.column_anchor =
3870 self.preferred_column_for_with_affinity(new_sel.cursor_offset(), affinity);
3871 } else if new_sel.column_anchor.is_none() {
3872 new_sel.column_anchor = self.preferred_column_for_with_affinity(
3873 sel.cursor_offset(),
3874 self.line_end_affinity_for(sel),
3875 );
3876 }
3877 new_sel
3878 })
3879 .collect();
3880 self.cursor_line_end_affinity = active_affinity;
3881 self.selections.replace_all(new_selections);
3882 self.selections.merge_overlapping();
3883
3884 self.scroll_to(self.cursor(), None, cx);
3885 cx.notify()
3886 }
3887
3888 pub fn unselect(&mut self, _: &mut Window, cx: &mut Context<Self>) {
3890 self.undo_manager.break_transaction_coalescing();
3891 let offset = self.cursor();
3892 self.set_cursor_to(offset);
3893 cx.notify()
3894 }
3895
3896 #[inline]
3897 pub(super) fn offset_from_utf16(&self, offset: usize) -> usize {
3898 self.text.offset_utf16_to_offset(offset)
3899 }
3900
3901 #[inline]
3902 pub(super) fn offset_to_utf16(&self, offset: usize) -> usize {
3903 self.text.offset_to_offset_utf16(offset)
3904 }
3905
3906 #[inline]
3907 pub(crate) fn range_to_utf16(&self, range: &Range<usize>) -> Range<usize> {
3908 self.offset_to_utf16(range.start)..self.offset_to_utf16(range.end)
3909 }
3910
3911 #[inline]
3912 pub(super) fn range_from_utf16(&self, range_utf16: &Range<usize>) -> Range<usize> {
3913 self.offset_from_utf16(range_utf16.start)..self.offset_from_utf16(range_utf16.end)
3914 }
3915
3916 fn clamp_offset_to_visible_backward(&self, offset: usize) -> usize {
3919 let line = self.text.offset_to_point(offset).row;
3920 if self.display_map.is_buffer_line_hidden(line) {
3921 for fold in self.display_map.folded_ranges() {
3922 if line > fold.start_line && line <= fold.end_line {
3923 return self.text.line_end_offset(fold.start_line);
3924 }
3925 }
3926 }
3927 offset
3928 }
3929
3930 fn clamp_offset_to_visible_forward(&self, offset: usize) -> usize {
3933 let line = self.text.offset_to_point(offset).row;
3934 if self.display_map.is_buffer_line_hidden(line) {
3935 for fold in self.display_map.folded_ranges() {
3936 if line > fold.start_line && line <= fold.end_line {
3937 return self.text.line_start_offset(fold.end_line);
3938 }
3939 }
3940 }
3941 offset
3942 }
3943
3944 pub(super) fn cursor_boundary(&self, offset: usize, bias: Bias) -> usize {
3947 let offset = self.token_boundary(self.text.clip_offset(offset, bias), bias);
3948 if offset > 0
3949 && self.text.char_at(offset - 1) == Some('\r')
3950 && self.text.char_at(offset) == Some('\n')
3951 {
3952 if bias == Bias::Left {
3953 offset - 1
3954 } else {
3955 offset + 1
3956 }
3957 } else {
3958 offset
3959 }
3960 }
3961
3962 pub(super) fn previous_boundary(&self, offset: usize) -> usize {
3963 let offset = self.cursor_boundary(offset.saturating_sub(1), Bias::Left);
3964 self.clamp_offset_to_visible_backward(offset)
3965 }
3966
3967 pub(super) fn next_boundary(&self, offset: usize) -> usize {
3968 let offset = self.cursor_boundary(offset.saturating_add(1), Bias::Right);
3969 self.clamp_offset_to_visible_forward(offset)
3970 }
3971
3972 pub(crate) fn show_cursor(&self, window: &Window, cx: &App) -> bool {
3974 (self.focus_handle.is_focused(window) || M::is_context_menu_open(self, cx))
3975 && !self.disabled
3976 && self.blink_cursor.read(cx).visible()
3977 && window.is_window_active()
3978 }
3979
3980 fn on_focus(&mut self, _: &mut Window, cx: &mut Context<Self>) {
3981 self.blink_cursor.update(cx, |cursor, cx| {
3982 cursor.start(cx);
3983 });
3984 cx.emit(InputEvent::Focus);
3985 }
3986
3987 fn on_blur(&mut self, window: &mut Window, cx: &mut Context<Self>) {
3988 self.revealed_viewport = None;
3989 if M::is_context_menu_open(self, cx) {
3990 return;
3991 }
3992
3993 M::hide_context_menu(self, cx);
3994 self.undo_manager.break_transaction_coalescing();
3995
3996 M::clear_hover_state(self, cx);
4000 self.diagnostic_popover = None;
4001 M::clear_inline_completion(self, cx);
4002 self.dismiss_touch_selection(cx);
4003 self.blink_cursor.update(cx, |cursor, cx| {
4004 cursor.stop(cx);
4005 });
4006 self.clamp_number_value(window, cx);
4007 cx.emit(InputEvent::Blur);
4008 cx.notify();
4009 }
4010
4011 fn clamp_number_value(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4016 if !self.is_single_line() {
4017 return;
4018 }
4019 if !matches!(self.mask_pattern, MaskPattern::Number { .. }) {
4020 return;
4021 }
4022 if self.number_min.is_none() && self.number_max.is_none() {
4023 return;
4024 }
4025
4026 let Ok(value) = self.unmask_value().parse::<f64>() else {
4027 return;
4028 };
4029
4030 let clamped = match (self.number_min, self.number_max) {
4031 (Some(min), _) if value < min => min,
4032 (_, Some(max)) if value > max => max,
4033 _ => return,
4034 };
4035
4036 let new_text = clamped.to_string();
4039 if !self.is_valid_input(&new_text, cx) {
4040 return;
4041 }
4042
4043 let range = self.range_to_utf16(&(0..self.text.len()));
4044 self.replace_text_in_range_silent(Some(range), &new_text, window, cx);
4045 }
4046
4047 pub(super) fn pause_blink_cursor(&mut self, cx: &mut Context<Self>) {
4048 self.blink_cursor.update(cx, |cursor, cx| {
4049 cursor.pause(cx);
4050 });
4051 }
4052
4053 pub(super) fn on_drag_move(
4054 &mut self,
4055 event: &MouseMoveEvent,
4056 window: &mut Window,
4057 cx: &mut Context<Self>,
4058 ) {
4059 if self.text.len() == 0 {
4060 return;
4061 }
4062
4063 if self.last_layout.is_none() {
4064 return;
4065 }
4066
4067 if !self.focus_handle.is_focused(window) {
4068 return;
4069 }
4070
4071 if !self.selecting {
4072 return;
4073 }
4074
4075 self.auto_scroll.last_drag_position = Some(event.position);
4076 let (offset, line_end_affinity, columns_past_line_end) =
4077 self.resolve_mouse_position(event.position);
4078 if let Some(start) = self.column_select_start {
4079 let end = ColumnarPoint::new(offset, columns_past_line_end);
4080 self.build_columnar_selection(start, end, cx);
4081 } else {
4082 self.select_to_with_affinity(offset, line_end_affinity, cx);
4083 }
4084
4085 if !self.is_single_line() {
4086 let delta = AutoScroll::compute_delta(event.position.y, self.input_bounds);
4087 let scroll_delta = delta.map(|d| -d);
4089 self.auto_scroll.set(scroll_delta, cx, |delta, state, cx| {
4090 let current = state.scroll_handle.offset();
4091 state.update_scroll_offset(Some(point(current.x, current.y + delta)), cx);
4092 if let Some(pos) = state.auto_scroll.last_drag_position {
4093 let (offset, line_end_affinity, columns_past_line_end) =
4094 state.resolve_mouse_position(pos);
4095 if let Some(start) = state.column_select_start {
4096 let end = ColumnarPoint::new(offset, columns_past_line_end);
4097 state.build_columnar_selection(start, end, cx);
4098 } else {
4099 state.select_to_with_affinity(offset, line_end_affinity, cx);
4100 }
4101 }
4102 });
4103 }
4104 }
4105
4106 pub(super) fn normalize_input<'a>(&self, new_text: &'a str) -> Cow<'a, str> {
4112 let normalized = if matches!(self.mask_pattern, MaskPattern::Number { .. }) {
4113 normalize_number_input(new_text)
4114 } else {
4115 Cow::Borrowed(new_text)
4116 };
4117
4118 if self.is_single_line() && normalized.contains(['\n', '\r']) {
4119 Cow::Owned(normalized.replace(['\n', '\r'], ""))
4120 } else {
4121 normalized
4122 }
4123 }
4124
4125 pub(crate) fn is_valid_input(&self, new_text: &str, cx: &mut Context<Self>) -> bool {
4126 if new_text.is_empty() {
4127 return true;
4128 }
4129
4130 if let Some(validate) = &self.validate {
4131 if !validate(new_text, cx) {
4132 return false;
4133 }
4134 }
4135
4136 if !self.mask_pattern.is_valid(new_text) {
4137 return false;
4138 }
4139
4140 let Some(pattern) = &self.pattern else {
4141 return true;
4142 };
4143
4144 pattern.is_match(new_text)
4145 }
4146
4147 pub fn mask_pattern(mut self, pattern: impl Into<MaskPattern>) -> Self {
4157 self.mask_pattern = pattern.into();
4158 self.mask_pattern_set = true;
4159 if let Some(placeholder) = self.mask_pattern.placeholder() {
4160 self.placeholder = placeholder.into();
4161 }
4162 self
4163 }
4164
4165 pub fn set_mask_pattern(
4166 &mut self,
4167 pattern: impl Into<MaskPattern>,
4168 _: &mut Window,
4169 cx: &mut Context<Self>,
4170 ) {
4171 self.mask_pattern = pattern.into();
4172 self.mask_pattern_set = true;
4173 if let Some(placeholder) = self.mask_pattern.placeholder() {
4174 self.placeholder = placeholder.into();
4175 }
4176 cx.notify();
4177 }
4178
4179 pub fn ensure_number_mask(&mut self) {
4181 if self.mask_pattern_set {
4182 return;
4183 }
4184 self.mask_pattern = MaskPattern::Number {
4185 separator: None,
4186 fraction: None,
4187 };
4188 }
4189
4190 pub(super) fn set_input_bounds(&mut self, new_bounds: Bounds<Pixels>, cx: &mut Context<Self>) {
4191 let wrap_width_changed = self.input_bounds.size.width != new_bounds.size.width;
4192 self.input_bounds = new_bounds;
4193
4194 if let Some(last_layout) = self.last_layout.as_ref() {
4196 if wrap_width_changed {
4197 let wrap_width = if !self.soft_wrap {
4198 None
4200 } else {
4201 last_layout.wrap_width
4202 };
4203
4204 self.display_map.on_layout_changed(wrap_width, cx);
4205 if self.is_multi_line() {
4206 self.mode.update_auto_grow(&self.display_map);
4207 }
4208 cx.notify();
4209 }
4210 }
4211 }
4212
4213 pub fn selected_text(&self) -> RopeSlice<'_> {
4218 let range_utf16 = self.range_to_utf16(&self.selected_range());
4219 let range = self.range_from_utf16(&range_utf16);
4220 self.text.slice(range)
4221 }
4222
4223 pub fn range_to_bounds(&self, range: &Range<usize>) -> Option<Bounds<Pixels>> {
4227 let Some(last_layout) = self.last_layout.as_ref() else {
4228 return None;
4229 };
4230
4231 let Some(last_bounds) = self.last_bounds else {
4232 return None;
4233 };
4234
4235 let (_, _, start_pos) = self.line_and_position_for_offset(range.start);
4236 let (_, _, end_pos) = self.line_and_position_for_offset(range.end);
4237
4238 let Some(start_pos) = start_pos else {
4239 return None;
4240 };
4241 let Some(end_pos) = end_pos else {
4242 return None;
4243 };
4244
4245 Some(Bounds::from_corners(
4246 last_bounds.origin + start_pos,
4247 last_bounds.origin + end_pos + point(px(0.), last_layout.line_height),
4248 ))
4249 }
4250
4251 pub(crate) fn replace_text_in_range_silent(
4255 &mut self,
4256 range_utf16: Option<Range<usize>>,
4257 new_text: &str,
4258 window: &mut Window,
4259 cx: &mut Context<Self>,
4260 ) {
4261 self.silent_replace_text = true;
4262 self.replace_text_in_range(range_utf16, new_text, window, cx);
4263 self.silent_replace_text = false;
4264 }
4265
4266 pub(crate) fn replace_text_in_ranges(
4271 &mut self,
4272 edits: &[(Range<usize>, String)],
4273 window: &mut Window,
4274 cx: &mut Context<Self>,
4275 ) {
4276 if !self.is_editable() || edits.is_empty() {
4277 return;
4278 }
4279
4280 M::hide_context_menu(self, cx);
4283 M::clear_inline_completion(self, cx);
4284
4285 let mut sorted: Vec<(Range<usize>, &str)> = edits
4288 .iter()
4289 .map(|(range, text)| (self.normalize_token_range(range.clone()), text.as_str()))
4290 .collect();
4291 sorted.sort_by_key(|edit| std::cmp::Reverse(edit.0.start));
4292 if !self.token_spans().is_empty() {
4293 let mut merged: Vec<(Range<usize>, &str)> = Vec::with_capacity(sorted.len());
4294 for (range, text) in sorted {
4295 if let Some(last) = merged
4296 .last_mut()
4297 .filter(|last| range.end > last.0.start || range == last.0)
4298 {
4299 last.0.start = range.start.min(last.0.start);
4300 last.0.end = range.end.max(last.0.end);
4301 } else {
4302 merged.push((range, text));
4303 }
4304 }
4305 sorted = merged;
4306 }
4307
4308 #[cfg(debug_assertions)]
4309 for pair in sorted.windows(2) {
4310 debug_assert!(
4311 pair[1].0.end <= pair[0].0.start,
4312 "replace_text_in_ranges requires disjoint ranges"
4313 );
4314 }
4315
4316 let requested_intent = self.undo_manager.take_pending_intent();
4320 let selection_before = *self.active_selection();
4321 let original_selections: Vec<CursorSelection> = self.selections.iter().copied().collect();
4322 let selections_before: Vec<CursorSelection> = self
4326 .selections
4327 .iter()
4328 .map(|selection| {
4329 Self::collapse_for_intent(
4330 requested_intent.unwrap_or(EditIntent::Atomic),
4331 *selection,
4332 &(selection.start..selection.end),
4333 )
4334 })
4335 .collect();
4336 let group = sorted.len() > 1;
4337 if group {
4338 self.undo_manager.begin_transaction();
4339 }
4340
4341 let auto_closed_pairs_before = self.mode.auto_closed_pairs().clone();
4342 let mut recorded = false;
4343 let mut highlighter_edits = Vec::new();
4346 let accumulate_replay_edits = self.replaying_history && !self.emit_events;
4350 if !accumulate_replay_edits {
4351 self.last_input_edits.clear();
4352 }
4353 for (range, new_text) in &sorted {
4354 let old_text = self.text.clone();
4355 self.mode.adjust_auto_closed_pair(range, new_text.len());
4356 self.text.replace(range.clone(), new_text);
4357
4358 M::adjust_annotations(self, range, new_text.len());
4359 recorded |= self.push_history(
4360 &old_text,
4361 range,
4362 new_text,
4363 requested_intent,
4364 selection_before,
4365 None,
4366 );
4367
4368 self.display_map
4370 .adjust_folds_for_edit(&old_text, range, new_text);
4371 self.display_map
4372 .on_text_changed(&self.text, range, &Rope::from(*new_text), cx);
4373
4374 let edit = super::mode::replacement_input_edit(&old_text, &self.text, range, new_text);
4375 if self.emit_events || self.replaying_history {
4376 self.last_input_edits.push((edit, self.text.clone()));
4377 }
4378
4379 if group {
4380 if self.is_code_editor() {
4381 highlighter_edits.push((edit, self.text.clone()));
4382 }
4383 } else {
4384 self.mode.update_highlighter(
4385 super::mode::HighlighterUpdate {
4386 selected_range: range,
4387 old_text: &old_text,
4388 new_text: &self.text,
4389 change_text: new_text,
4390 force: true,
4391 input_edit: Some(edit),
4392 },
4393 window,
4394 cx,
4395 );
4396
4397 self.update_fold_candidates_incremental(range, new_text);
4398 }
4399 }
4400
4401 if group {
4402 self.mode
4403 .update_highlighter_batch::<M>(&highlighter_edits, window, cx);
4404
4405 let mut shift = 0isize;
4409 for (range, new_text) in sorted.iter().rev() {
4410 let start = (range.start as isize + shift) as usize;
4411 self.update_fold_candidates_incremental(&(start..start), new_text);
4412 shift += new_text.len() as isize - range.len() as isize;
4413 }
4414
4415 self.undo_manager.commit_transaction();
4416 }
4417
4418 if let Some(diagnostics) = self.mode.diagnostics_mut() {
4420 diagnostics.reset(&self.text)
4421 }
4422 M::refresh_language_features(self, window, cx);
4423 self.update_search(cx);
4424
4425 let mut ascending: Vec<(Range<usize>, &str)> = sorted.clone();
4428 ascending.sort_by_key(|edit| edit.0.start);
4429 let text_len = self.text.len();
4430 let mut delta: isize = 0;
4431 let mut edit_results = Vec::with_capacity(ascending.len());
4432 for (range, new_text) in &ascending {
4433 let offset = ((range.start as isize + delta) as usize + new_text.len()).min(text_len);
4434 edit_results.push((range.clone(), offset));
4435 delta += new_text.len() as isize - (range.end as isize - range.start as isize);
4436 }
4437
4438 let mut used = vec![false; edit_results.len()];
4439 let mut new_selections: Vec<CursorSelection> = Vec::with_capacity(ascending.len());
4440 for selection in original_selections {
4441 if let Ok(index) = edit_results
4442 .binary_search_by_key(&(selection.start, selection.end), |(range, _)| {
4443 (range.start, range.end)
4444 })
4445 && !used[index]
4446 {
4447 let offset = edit_results[index].1;
4448 used[index] = true;
4449 let mut selection = selection;
4450 selection.place_at(offset, None);
4451 new_selections.push(selection);
4452 }
4453 }
4454 for (index, (_, offset)) in edit_results.into_iter().enumerate() {
4455 if !used[index] {
4456 new_selections.push(CursorSelection::new(
4457 self.selections.generate_id(),
4458 offset,
4459 offset,
4460 ));
4461 }
4462 }
4463 self.selections.replace_all(new_selections);
4464 self.selections.merge_overlapping();
4465
4466 let selections_after: Vec<CursorSelection> = self.selections.iter().copied().collect();
4468 if recorded {
4469 self.undo_manager.record_auto_closed_pairs(
4470 auto_closed_pairs_before,
4471 self.mode.auto_closed_pairs().clone(),
4472 );
4473 self.undo_manager
4474 .record_selections(selections_before, selections_after);
4475 }
4476
4477 self.ime_marked_range.take();
4478 self.update_preferred_column();
4479 self.dismiss_touch_selection(cx);
4480 if self.is_multi_line() {
4481 self.mode.update_auto_grow(&self.display_map);
4482 }
4483 if self.emit_events {
4484 self.emit_change(cx);
4485 }
4486 cx.notify();
4487 }
4488
4489 fn update_fold_candidates(&mut self) {
4492 if !self.mode.is_folding() {
4493 return;
4494 }
4495
4496 let Some(highlighter_rc) = self.mode.highlighter() else {
4497 return;
4498 };
4499
4500 let highlighter = highlighter_rc.borrow();
4501 let Some(highlighter) = highlighter.as_ref() else {
4502 return;
4503 };
4504
4505 let fold_ranges = highlighter.fold_ranges(&self.text);
4506 self.display_map.set_fold_candidates(fold_ranges);
4507 }
4508
4509 fn update_fold_candidates_incremental(&mut self, edit_range: &Range<usize>, new_text: &str) {
4512 if !self.mode.is_folding() {
4513 return;
4514 }
4515
4516 let Some(highlighter_rc) = self.mode.highlighter() else {
4517 return;
4518 };
4519
4520 let highlighter = highlighter_rc.borrow();
4521 let Some(highlighter) = highlighter.as_ref() else {
4522 return;
4523 };
4524
4525 let new_end = edit_range.start + new_text.len();
4527 self.display_map.update_fold_candidates_for_edit(
4528 |range, text| highlighter.fold_ranges_for_edit(range, text),
4529 edit_range.start..new_end,
4530 &self.text,
4531 );
4532 }
4533}
4534
4535impl<M: InputModeKind> EntityInputHandler for InputBaseState<M> {
4536 fn text_for_range(
4537 &mut self,
4538 range_utf16: Range<usize>,
4539 adjusted_range: &mut Option<Range<usize>>,
4540 _window: &mut Window,
4541 _cx: &mut Context<Self>,
4542 ) -> Option<String> {
4543 let range = self.range_from_utf16(&range_utf16);
4544 adjusted_range.replace(self.range_to_utf16(&range));
4545 Some(self.text.slice(range).to_string())
4546 }
4547
4548 fn selected_text_range(
4549 &mut self,
4550 _ignore_disabled_input: bool,
4551 _window: &mut Window,
4552 _cx: &mut Context<Self>,
4553 ) -> Option<UTF16Selection> {
4554 Some(UTF16Selection {
4555 range: self.range_to_utf16(&self.selected_range()),
4556 reversed: self.active_selection().reversed,
4557 })
4558 }
4559
4560 fn set_selected_text_range(
4567 &mut self,
4568 range_utf16: Range<usize>,
4569 _window: &mut Window,
4570 cx: &mut Context<Self>,
4571 ) {
4572 let previous = self.selected_range();
4573 let was_reversed = self.active_selection().reversed;
4574 let range = self.range_from_utf16(&range_utf16);
4575 let range = range.start.min(range.end)..range.start.max(range.end);
4576 self.set_selected_range(range, cx);
4577
4578 let range = self.selected_range();
4579 let reversed = if range == previous {
4580 was_reversed
4581 } else if !range.is_empty() && range.end == previous.start {
4582 true
4583 } else if !range.is_empty() && range.start == previous.end {
4584 false
4585 } else {
4586 !range.is_empty() && range.start != previous.start && range.end == previous.end
4587 };
4588 self.active_selection_mut().reversed = reversed;
4589 self.scroll_to(self.cursor(), None, cx);
4590 cx.notify();
4591 }
4592
4593 fn marked_text_range(
4594 &self,
4595 _window: &mut Window,
4596 _cx: &mut Context<Self>,
4597 ) -> Option<Range<usize>> {
4598 self.ime_marked_range
4599 .map(|range| self.range_to_utf16(&range.into()))
4600 }
4601
4602 fn unmark_text(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {
4603 self.ime_marked_range = None;
4604 self.undo_manager.commit_transaction();
4605 }
4606
4607 fn replace_text_in_range(
4612 &mut self,
4613 range_utf16: Option<Range<usize>>,
4614 new_text: &str,
4615 window: &mut Window,
4616 cx: &mut Context<Self>,
4617 ) {
4618 let requested_intent = self.undo_manager.take_pending_intent();
4619 if !self.is_editable() {
4620 return;
4621 }
4622 let selection_before = *self.active_selection();
4623 let ends_composition = self.ime_marked_range.is_some();
4626
4627 self.pause_blink_cursor(cx);
4628
4629 let new_text = if self.replaying_history {
4633 Cow::Borrowed(new_text)
4634 } else {
4635 self.normalize_input(new_text)
4636 };
4637 let new_text: &str = &new_text;
4638
4639 let range = range_utf16
4640 .as_ref()
4641 .map(|range_utf16| self.range_from_utf16(range_utf16))
4642 .or(self.ime_marked_range.map(|range| {
4643 let range = self.range_to_utf16(&(range.start..range.end));
4644 self.range_from_utf16(&range)
4645 }))
4646 .unwrap_or(self.selected_range());
4647 let range = self.normalize_token_range(range);
4648
4649 if range.is_empty()
4655 && range.start == self.cursor()
4656 && self.ime_marked_range.is_none()
4657 && !self.silent_replace_text
4658 && self.is_code_editor()
4659 && self.selections.is_single()
4660 && self.active_selection().is_empty()
4661 {
4662 if let Some(target) = self.skip_over_target(new_text) {
4663 self.set_cursor_to(target);
4664 self.update_preferred_column();
4665 cx.notify();
4666 return;
4667 }
4668 }
4669
4670 if self.is_multi_line() {
4671 let multi_cursor = range_utf16.is_none()
4672 && self.ime_marked_range.is_none()
4673 && !self.selections.is_single();
4674 if multi_cursor {
4675 self.selections.merge_overlapping();
4676 let mut edits: Vec<(Range<usize>, String)> = self
4677 .selections
4678 .iter()
4679 .map(|sel| (sel.start..sel.end, new_text.to_string()))
4680 .collect();
4681 edits.sort_by_key(|(range, _)| range.start);
4682 let intent =
4686 requested_intent.unwrap_or_else(|| self.typing_intent(&edits, new_text));
4687 self.undo_manager.begin_transaction_with(intent);
4688 self.undo_manager.set_pending_intent(intent);
4689 self.replace_text_in_ranges(&edits, window, cx);
4690 self.undo_manager.commit_transaction();
4691 } else {
4692 if range_utf16.is_some() {
4693 self.selections.remove_all_but_active();
4694 }
4695 if let Some(intent) = requested_intent {
4696 self.undo_manager.set_pending_intent(intent);
4697 }
4698 if let Some((open_len, closer)) = self.auto_close_target(&range, new_text) {
4699 let replacement = format!("{new_text}{closer}");
4701 self.replace_text_in_ranges(&[(range.clone(), replacement)], window, cx);
4702 let cursor = range.start + new_text.len();
4703 self.mode.track_auto_closed_pair(
4704 cursor - open_len..cursor,
4705 cursor..cursor + closer.len(),
4706 );
4707 self.undo_manager
4708 .record_auto_closed_pairs_after(self.mode.auto_closed_pairs().clone());
4709 self.set_cursor_to(cursor);
4710 self.update_preferred_column();
4711 self.undo_manager.record_selections(
4712 vec![selection_before],
4713 self.selections.iter().copied().collect(),
4714 );
4715 } else {
4716 self.replace_text_in_ranges(
4717 &[(range.clone(), new_text.to_string())],
4718 window,
4719 cx,
4720 );
4721 }
4722 }
4723 if ends_composition {
4724 self.undo_manager.commit_transaction();
4725 }
4726
4727 if !self.silent_replace_text {
4728 M::on_text_typed(self, &range, new_text, window, cx);
4729 }
4730 return;
4731 }
4732
4733 let old_text = self.text.clone();
4735 let accumulate_replay_edits = self.replaying_history && !self.emit_events;
4739 if !accumulate_replay_edits {
4740 self.last_input_edits.clear();
4741 }
4742 self.mode.adjust_auto_closed_pair(&range, new_text.len());
4743 self.text.replace(range.clone(), new_text);
4744
4745 let mut new_offset = (range.start + new_text.len()).min(self.text.len());
4746
4747 let mut mask_changed = false;
4750
4751 if self.is_single_line() {
4752 let pending_text = self.text.to_string();
4753 if !self.replaying_history
4759 && !self.validated_token_edit
4760 && !self.is_valid_input(&pending_text, cx)
4761 && self.is_valid_input(&old_text.to_string(), cx)
4762 {
4763 self.text = old_text;
4764 return;
4765 }
4766
4767 if !self.replaying_history && !self.mask_pattern.is_none() {
4768 let mask_text = self.mask_pattern.mask(&pending_text);
4769 mask_changed = mask_text.as_str() != pending_text;
4770 self.text = Rope::from(mask_text.as_str());
4771 let new_text_len =
4772 (new_text.len() + mask_text.len()).saturating_sub(pending_text.len());
4773 new_offset = (range.start + new_text_len).min(mask_text.len());
4774 }
4775 }
4776
4777 if mask_changed {
4778 M::reset_annotations(self);
4781 } else {
4782 M::adjust_annotations(self, &range, new_text.len());
4783 }
4784 if mask_changed {
4785 self.push_history(
4789 &old_text,
4790 &(0..old_text.len()),
4791 &self.text.to_string(),
4792 Some(EditIntent::Atomic),
4793 selection_before,
4794 Some((new_offset..new_offset).into()),
4795 );
4796 } else {
4797 self.push_history(
4798 &old_text,
4799 &range,
4800 &new_text,
4801 requested_intent,
4802 selection_before,
4803 None,
4804 );
4805 }
4806 if let Some(diagnostics) = self.mode.diagnostics_mut() {
4807 diagnostics.reset(&self.text)
4808 }
4809 self.display_map
4811 .adjust_folds_for_edit(&old_text, &range, new_text);
4812 self.display_map
4813 .on_text_changed(&self.text, &range, &Rope::from(new_text), cx);
4814
4815 let edit = if mask_changed {
4816 Self::full_input_edit(&old_text, &self.text)
4817 } else {
4818 super::mode::replacement_input_edit(&old_text, &self.text, &range, new_text)
4819 };
4820 if self.emit_events || self.replaying_history {
4821 self.last_input_edits.push((edit, self.text.clone()));
4822 }
4823
4824 self.mode.update_highlighter::<M>(
4825 super::mode::HighlighterUpdate {
4826 selected_range: &range,
4827 old_text: &old_text,
4828 new_text: &self.text,
4829 change_text: &new_text,
4830 force: true,
4831 input_edit: Some(edit),
4832 },
4833 window,
4834 cx,
4835 );
4836
4837 self.update_fold_candidates_incremental(&range, new_text);
4838 M::refresh_language_features(self, window, cx);
4839 self.set_cursor_to(new_offset);
4840 self.ime_marked_range.take();
4841 if ends_composition {
4847 self.undo_manager
4848 .record_selections(vec![selection_before], vec![*self.active_selection()]);
4849 self.undo_manager.commit_transaction();
4850 }
4851 self.update_preferred_column();
4852 self.update_search(cx);
4853 self.dismiss_touch_selection(cx);
4854 if self.is_multi_line() {
4855 self.mode.update_auto_grow(&self.display_map);
4856 }
4857 if !self.silent_replace_text {
4858 M::on_text_typed(self, &range, &new_text, window, cx);
4859 }
4860 if self.emit_events {
4861 self.emit_change(cx);
4862 }
4863 cx.notify();
4864 }
4865
4866 fn replace_and_mark_text_in_range(
4868 &mut self,
4869 range_utf16: Option<Range<usize>>,
4870 new_text: &str,
4871 new_selected_range_utf16: Option<Range<usize>>,
4872 window: &mut Window,
4873 cx: &mut Context<Self>,
4874 ) {
4875 let requested_intent = self.undo_manager.take_pending_intent();
4876 if !self.is_editable() {
4877 return;
4878 }
4879 let selection_before = *self.active_selection();
4880
4881 let starts_composition = self.ime_marked_range.is_none();
4882 if starts_composition {
4883 self.undo_manager.break_transaction_coalescing();
4886 self.undo_manager.begin_transaction();
4887 }
4888
4889 self.selections.remove_all_but_active();
4891
4892 M::reset_language_features(self);
4893
4894 let new_text = if self.replaying_history {
4896 Cow::Borrowed(new_text)
4897 } else {
4898 self.normalize_input(new_text)
4899 };
4900 let new_text: &str = &new_text;
4901
4902 let range = range_utf16
4903 .as_ref()
4904 .map(|range_utf16| self.range_from_utf16(range_utf16))
4905 .or(self.ime_marked_range.map(|range| {
4906 let range = self.range_to_utf16(&(range.start..range.end));
4907 self.range_from_utf16(&range)
4908 }))
4909 .unwrap_or(self.selected_range());
4910 let range = self.normalize_token_range(range);
4911
4912 let auto_closed_pairs_before = self.mode.auto_closed_pairs().clone();
4913 let old_text = self.text.clone();
4914 self.last_input_edits.clear();
4917 self.mode.adjust_auto_closed_pair(&range, new_text.len());
4918 self.text.replace(range.clone(), new_text);
4919
4920 if self.is_single_line() {
4921 let pending_text = self.text.to_string();
4922 if !self.replaying_history
4924 && !self.validated_token_edit
4925 && !self.is_valid_input(&pending_text, cx)
4926 && self.is_valid_input(&old_text.to_string(), cx)
4927 {
4928 self.text = old_text;
4929 if starts_composition {
4930 self.undo_manager.commit_transaction();
4931 }
4932 return;
4933 }
4934 }
4935
4936 if self.ime_original_text.is_none() {
4937 self.ime_original_text = Some(old_text.clone());
4938 }
4939 M::adjust_annotations(self, &range, new_text.len());
4940 if let Some(diagnostics) = self.mode.diagnostics_mut() {
4941 diagnostics.reset(&self.text)
4942 }
4943 self.display_map
4945 .adjust_folds_for_edit(&old_text, &range, new_text);
4946 self.display_map
4947 .on_text_changed(&self.text, &range, &Rope::from(new_text), cx);
4948
4949 self.mode.update_highlighter::<M>(
4950 super::mode::HighlighterUpdate {
4951 selected_range: &range,
4952 old_text: &old_text,
4953 new_text: &self.text,
4954 change_text: &new_text,
4955 force: true,
4956 input_edit: None,
4957 },
4958 window,
4959 cx,
4960 );
4961
4962 self.update_fold_candidates_incremental(&range, new_text);
4963 M::refresh_language_features(self, window, cx);
4964 if new_text.is_empty() {
4965 self.set_cursor_to(range.start);
4967 self.ime_marked_range = None;
4968 } else {
4969 self.ime_marked_range = Some((range.start..range.start + new_text.len()).into());
4970 let new_range = new_selected_range_utf16
4971 .as_ref()
4972 .map(|range_utf16| {
4973 let new_text = Rope::from(new_text);
4974 range.start + new_text.offset_utf16_to_offset(range_utf16.start)
4975 ..range.start + new_text.offset_utf16_to_offset(range_utf16.end)
4976 })
4977 .unwrap_or_else(|| range.start + new_text.len()..range.start + new_text.len());
4978 self.set_selection(new_range.start, new_range.end);
4979 }
4980 if self.is_multi_line() {
4981 self.mode.update_auto_grow(&self.display_map);
4982 }
4983 if self.push_history(
4984 &old_text,
4985 &range,
4986 new_text,
4987 requested_intent,
4988 selection_before,
4989 Some(*self.active_selection()),
4990 ) {
4991 self.undo_manager
4992 .record_selections(vec![selection_before], vec![*self.active_selection()]);
4993 self.undo_manager.record_auto_closed_pairs(
4994 auto_closed_pairs_before,
4995 self.mode.auto_closed_pairs().clone(),
4996 );
4997 }
4998 if new_text.is_empty() {
4999 self.undo_manager.commit_transaction();
5000 }
5001 cx.notify();
5002 }
5003
5004 fn bounds_for_range(
5006 &mut self,
5007 range_utf16: Range<usize>,
5008 bounds: Bounds<Pixels>,
5009 _window: &mut Window,
5010 _cx: &mut Context<Self>,
5011 ) -> Option<Bounds<Pixels>> {
5012 let last_layout = self.last_layout.as_ref()?;
5013 let line_height = last_layout.line_height;
5014 let line_number_width = last_layout.line_number_width;
5015 let range = self.range_from_utf16(&range_utf16);
5016
5017 let mut start_origin = None;
5018 let mut end_origin = None;
5019 let line_number_origin = point(line_number_width, px(0.));
5020 let mut y_offset = last_layout.visible_top;
5021
5022 for (vi, line) in last_layout.lines.iter().enumerate() {
5023 if start_origin.is_some() && end_origin.is_some() {
5024 break;
5025 }
5026
5027 let index_offset = last_layout.visible_line_byte_offsets[vi];
5028
5029 if start_origin.is_none() {
5030 if let Some(p) = line.position_for_index(
5031 range.start.saturating_sub(index_offset),
5032 last_layout,
5033 false,
5034 ) {
5035 start_origin = Some(p + point(px(0.), y_offset));
5036 }
5037 }
5038
5039 if end_origin.is_none() {
5040 if let Some(p) = line.position_for_index(
5041 range.end.saturating_sub(index_offset),
5042 last_layout,
5043 false,
5044 ) {
5045 end_origin = Some(p + point(px(0.), y_offset));
5046 }
5047 }
5048
5049 y_offset += line.size(line_height).height;
5050 }
5051
5052 let start_origin = start_origin.or_else(|| {
5053 let offset = self.last_cursor.or(Some(self.cursor()))?;
5054 let (_, _, origin) = self.line_and_position_for_offset(offset);
5055 origin.map(|origin| origin - line_number_origin)
5056 })?;
5057 let mut end_origin = end_origin.unwrap_or(start_origin);
5058 end_origin.y = start_origin.y;
5060
5061 Some(Bounds::from_corners(
5062 bounds.origin + line_number_origin + start_origin,
5063 bounds.origin + line_number_origin + point(end_origin.x, end_origin.y + line_height),
5065 ))
5066 }
5067
5068 fn character_index_for_point(
5069 &mut self,
5070 point: gpui::Point<Pixels>,
5071 _window: &mut Window,
5072 _cx: &mut Context<Self>,
5073 ) -> Option<usize> {
5074 let last_layout = self.last_layout.as_ref()?;
5075 let line_point = self.last_bounds?.localize(&point)?;
5076
5077 for (vi, line) in last_layout.lines.iter().enumerate() {
5078 let offset = last_layout.visible_line_byte_offsets[vi];
5079 if let Some(utf8_index) = line.index_for_position(line_point, last_layout) {
5080 return Some(self.offset_to_utf16(offset + utf8_index));
5081 }
5082 }
5083
5084 None
5085 }
5086
5087 fn text_input_configuration(
5088 &mut self,
5089 _window: &mut Window,
5090 _cx: &mut Context<Self>,
5091 ) -> TextInputConfiguration {
5092 InputBaseState::text_input_configuration(self)
5093 }
5094}
5095
5096impl<M: InputModeKind> Focusable for InputBaseState<M> {
5097 fn focus_handle(&self, _cx: &App) -> FocusHandle {
5098 self.focus_handle.clone()
5099 }
5100}
5101
5102impl<M: InputModeKind> Render for InputBaseState<M> {
5103 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
5104 self.editor_style = self
5107 .projected_editor_style
5108 .resolved(&crate::Theme::global(cx).tokens);
5109 let entity = cx.entity();
5110 if self._pending_update {
5111 self.mode.update_highlighter::<M>(
5112 super::mode::HighlighterUpdate {
5113 selected_range: &(0..0),
5114 old_text: &self.text,
5115 new_text: &self.text,
5116 change_text: "",
5117 force: false,
5118 input_edit: None,
5119 },
5120 window,
5121 cx,
5122 );
5123
5124 self.update_fold_candidates();
5125 M::refresh_language_features(self, window, cx);
5126 self._pending_update = false;
5127 }
5128
5129 let element = div()
5130 .id(EDITOR_ELEMENT_ID)
5131 .key_context(CONTEXT)
5132 .when_some(self.accessibility.as_ref(), |this, accessibility| {
5133 this.when_some(accessibility.role, |this, role| this.role(role))
5134 .when_some(accessibility.label.clone(), |this, label| {
5135 this.aria_label(label)
5136 })
5137 .when_some(accessibility.placeholder.clone(), |this, placeholder| {
5138 this.aria_placeholder(placeholder)
5139 })
5140 .when_some(accessibility.accessibility_id.clone(), |this, id| {
5141 this.accessibility_id(id)
5142 })
5143 .when(
5144 accessibility.value_exposed
5145 && !self.masked
5146 && (window.is_a11y_active() || cfg!(feature = "test-support")),
5147 |this| this.aria_value(self.text.to_string()),
5148 )
5149 .on_a11y_action(gpui::AccessibleAction::Focus, {
5150 let entity = entity.clone();
5151 move |_, window, cx| {
5152 entity.update(cx, |state, cx| {
5153 state.handle_accessibility_focus(window, cx)
5154 });
5155 }
5156 })
5157 .when(self.is_editable(), |this| {
5158 this.on_a11y_action(gpui::AccessibleAction::SetValue, {
5159 let entity = entity.clone();
5160 move |data, window, cx| {
5161 entity.update(cx, |state, cx| {
5162 state.handle_accessibility_set_value(data, window, cx)
5163 });
5164 }
5165 })
5166 })
5167 })
5168 .when(self.is_editable(), |this| {
5169 this.on_action(window.listener_for(&entity, InputBaseState::backspace))
5170 .on_action(window.listener_for(&entity, InputBaseState::delete))
5171 .on_action(
5172 window.listener_for(&entity, InputBaseState::delete_to_beginning_of_line),
5173 )
5174 .on_action(window.listener_for(&entity, InputBaseState::delete_to_end_of_line))
5175 .on_action(window.listener_for(&entity, InputBaseState::delete_previous_word))
5176 .on_action(window.listener_for(&entity, InputBaseState::delete_next_word))
5177 .on_action(window.listener_for(&entity, InputBaseState::enter))
5178 .on_action(window.listener_for(&entity, InputBaseState::escape))
5179 .on_action(window.listener_for(&entity, InputBaseState::paste))
5180 .on_action(window.listener_for(&entity, InputBaseState::cut))
5181 .on_action(window.listener_for(&entity, InputBaseState::undo))
5182 .on_action(window.listener_for(&entity, InputBaseState::redo))
5183 .when(self.is_multi_line(), |this| {
5184 this.on_action(window.listener_for(&entity, InputBaseState::indent_inline))
5185 .on_action(window.listener_for(&entity, InputBaseState::outdent_inline))
5186 .on_action(window.listener_for(&entity, InputBaseState::indent_block))
5187 .on_action(window.listener_for(&entity, InputBaseState::outdent_block))
5188 })
5189 })
5190 .on_action(window.listener_for(&entity, InputBaseState::left))
5191 .on_action(window.listener_for(&entity, InputBaseState::right))
5192 .on_action(window.listener_for(&entity, InputBaseState::select_left))
5193 .on_action(window.listener_for(&entity, InputBaseState::select_right))
5194 .when(self.is_multi_line(), |this| {
5195 this.on_action(window.listener_for(&entity, InputBaseState::up))
5196 .on_action(window.listener_for(&entity, InputBaseState::down))
5197 .on_action(window.listener_for(&entity, InputBaseState::select_up))
5198 .on_action(window.listener_for(&entity, InputBaseState::select_down))
5199 .on_action(window.listener_for(&entity, InputBaseState::page_up))
5200 .on_action(window.listener_for(&entity, InputBaseState::page_down))
5201 .on_action(window.listener_for(&entity, InputBaseState::add_cursor_above))
5202 .on_action(window.listener_for(&entity, InputBaseState::add_cursor_below))
5203 })
5204 .on_action(window.listener_for(&entity, InputBaseState::on_action_select_all))
5205 .on_action(window.listener_for(&entity, InputBaseState::select_to_start_of_line))
5206 .on_action(window.listener_for(&entity, InputBaseState::select_to_end_of_line))
5207 .on_action(window.listener_for(&entity, InputBaseState::select_to_previous_word))
5208 .on_action(window.listener_for(&entity, InputBaseState::select_to_next_word))
5209 .on_action(window.listener_for(&entity, InputBaseState::home))
5210 .on_action(window.listener_for(&entity, InputBaseState::end))
5211 .on_action(window.listener_for(&entity, InputBaseState::move_to_start))
5212 .on_action(window.listener_for(&entity, InputBaseState::move_to_end))
5213 .on_action(window.listener_for(&entity, InputBaseState::move_to_previous_word))
5214 .on_action(window.listener_for(&entity, InputBaseState::move_to_next_word))
5215 .on_action(window.listener_for(&entity, InputBaseState::select_to_start))
5216 .on_action(window.listener_for(&entity, InputBaseState::select_to_end))
5217 .on_action(window.listener_for(&entity, InputBaseState::show_character_palette))
5218 .on_action({
5219 let entity = entity.clone();
5220 move |_: &ActivateToken, window, cx| {
5221 let state = entity.read(cx);
5222 let activation = state
5223 .token_spans()
5224 .iter()
5225 .find(|span| span.range() == state.selected_range())
5226 .and_then(|span| {
5227 state.range_to_bounds(&span.range()).and_then(|bounds| {
5228 state.token_activation(
5229 span.range().start,
5230 bounds,
5231 gpui::ClickEvent::Keyboard(gpui::KeyboardClickEvent {
5232 bounds,
5233 ..Default::default()
5234 }),
5235 )
5236 })
5237 });
5238 if let Some((listener, event)) = activation {
5239 listener(&event, window, cx);
5240 }
5241 }
5242 })
5243 .on_action(window.listener_for(&entity, InputBaseState::copy))
5244 .on_action(window.listener_for(&entity, InputBaseState::on_action_search))
5245 .on_action(window.listener_for(&entity, InputBaseState::on_action_replace))
5246 .on_mouse_down(
5247 MouseButton::Left,
5248 window.listener_for(&entity, InputBaseState::on_mouse_down),
5249 )
5250 .on_mouse_down(
5251 MouseButton::Right,
5252 window.listener_for(&entity, InputBaseState::on_mouse_down),
5253 )
5254 .on_mouse_up(
5255 MouseButton::Left,
5256 window.listener_for(&entity, InputBaseState::on_mouse_up),
5257 )
5258 .on_mouse_up(
5259 MouseButton::Right,
5260 window.listener_for(&entity, InputBaseState::on_mouse_up),
5261 )
5262 .on_mouse_move(window.listener_for(&entity, InputBaseState::on_mouse_move))
5263 .on_scroll(window.listener_for(&entity, InputBaseState::on_scroll_wheel))
5264 .when(self.is_multi_line() && !self.disabled, |this| {
5265 this.on_modifiers_changed(cx.listener(|_, _, _, cx| cx.notify()))
5266 })
5267 .when(!self.disabled, |this| {
5268 if self.is_multi_line() && window.modifiers().alt {
5269 this.cursor_crosshair()
5270 } else {
5271 this.cursor_text()
5272 }
5273 })
5274 .flex_1()
5275 .map(|this| {
5281 if self.is_multi_line() {
5282 this.h_full()
5283 } else {
5284 this.min_h_full().flex().items_center()
5285 }
5286 })
5287 .flex_grow_1()
5288 .overflow_x_hidden()
5289 .when(self.is_multi_line(), |this| {
5290 this.pt(self.editor_paddings.top)
5291 .pr(self.editor_paddings.right)
5292 .pb(self.editor_paddings.bottom)
5293 .pl(self.editor_paddings.left)
5294 })
5295 .child(TextElement::new(entity.clone()).placeholder(self.placeholder.clone()))
5296 .when(self.shows_scrollbar(), |this| {
5297 this.child(EditorScrollbar::new(entity.clone()))
5298 });
5299
5300 M::register_actions(element, &entity, window)
5303 .test_support()
5304 .track_focus(&self.focus_handle)
5305 }
5306}
5307
5308#[cfg(test)]
5309mod tests {
5310 use super::*;
5311
5312 #[gpui::test]
5313 fn accessibility_actions_recheck_live_editability(cx: &mut TestAppContext) {
5314 cx.update(crate::init);
5315 let (state, cx) = cx.add_window_view(|window, cx| {
5316 crate::input::InputState::new(window, cx).default_value("initial")
5317 });
5318 let changes = Rc::new(Cell::new(0));
5319 let observed = changes.clone();
5320 let _subscription = cx.update(|_, cx| {
5321 cx.subscribe(&state, move |_, event: &InputEvent, _| {
5322 if matches!(event, InputEvent::Change) {
5323 observed.set(observed.get() + 1);
5324 }
5325 })
5326 });
5327 cx.update(|window, cx| {
5328 state.update(cx, |state, cx| {
5329 state.handle_accessibility_set_value(None, window, cx);
5330 state.handle_accessibility_focus(window, cx);
5331 assert!(state.focus_handle.is_focused(window));
5332 });
5333 window.draw(cx).clear(cx);
5334 });
5335 assert_eq!(state.read_with(cx, |state, _| state.value()), "initial");
5336 assert_eq!(changes.get(), 0);
5337
5338 let action = gpui::accesskit::ActionData::Value("updated🦀".into());
5339 cx.update(|window, cx| {
5340 state.update(cx, |state, cx| {
5341 state.handle_accessibility_set_value(Some(&action), window, cx)
5342 });
5343 });
5344 assert_eq!(state.read_with(cx, |state, _| state.value()), "updated🦀");
5345 assert_eq!(changes.get(), 1);
5346 for disabled in [false, true] {
5347 cx.update(|window, cx| {
5348 window.blur(cx);
5349 state.update(cx, |state, cx| {
5350 state.set_disabled(disabled, cx);
5351 state.set_readonly(!disabled, cx);
5352 state.handle_accessibility_focus(window, cx);
5353 assert_eq!(state.focus_handle.is_focused(window), !disabled);
5354 let rejected = gpui::accesskit::ActionData::Value("rejected".into());
5355 state.handle_accessibility_set_value(Some(&rejected), window, cx);
5356 });
5357 });
5358 assert_eq!(state.read_with(cx, |state, _| state.value()), "updated🦀");
5359 assert_eq!(changes.get(), 1);
5360 }
5361 cx.update(|window, cx| {
5362 state.update(cx, |state, cx| {
5363 state.set_disabled(false, cx);
5364 state.set_readonly(false, cx);
5365 state.handle_accessibility_focus(window, cx);
5366 });
5367 window.draw(cx).clear(cx);
5368 window.dispatch_action(Box::new(Undo), cx);
5369 });
5370 assert_eq!(state.read_with(cx, |state, _| state.value()), "initial");
5371 cx.update(|window, cx| {
5372 state.update(cx, |state, cx| {
5373 state.set_masked(true, window, cx);
5374 state.handle_accessibility_set_value(Some(&action), window, cx);
5375 });
5376 });
5377 assert_eq!(state.read_with(cx, |state, _| state.value()), "updated🦀");
5378 }
5379
5380 #[gpui::test]
5381 fn accessibility_set_value_preserves_exact_editor_text(cx: &mut TestAppContext) {
5382 cx.update(crate::init);
5383 let (editor, cx) = cx.add_window_view(|window, cx| {
5384 crate::input::EditorState::new(window, cx).language("rust")
5385 });
5386 let action = gpui::accesskit::ActionData::Value("(".into());
5387 cx.update(|window, cx| {
5388 editor.update(cx, |editor, cx| {
5389 editor.handle_accessibility_set_value(Some(&action), window, cx)
5390 });
5391 });
5392 assert_eq!(editor.read_with(cx, |editor, _| editor.value()), "(");
5393 }
5394
5395 #[gpui::test]
5396 fn revealing_a_password_keeps_text_assistance_disabled(cx: &mut TestAppContext) {
5397 cx.update(crate::init);
5398 cx.open_window(size(px(400.), px(100.)), |window, cx| {
5399 let state = cx.new(|cx| crate::input::InputState::new(window, cx).masked(true));
5400 let hidden = state.read(cx).text_input_configuration();
5401 assert!(!hidden.autocorrect && !hidden.suggestions);
5402 assert_eq!(hidden.autocapitalize, Autocapitalize::None);
5403
5404 state.update(cx, |state, cx| state.toggle_masked(window, cx));
5406 assert_eq!(state.read(cx).text_input_configuration(), hidden);
5407 state.update(cx, |state, cx| state.toggle_masked(window, cx));
5408 assert_eq!(state.read(cx).text_input_configuration(), hidden);
5409 gpui::EmptyView
5410 });
5411 }
5412
5413 #[gpui::test]
5414 fn text_input_configuration_follows_the_input(cx: &mut TestAppContext) {
5415 use crate::input::{InputState, TextareaState};
5416
5417 cx.update(crate::init);
5418 let mut states = None;
5419 cx.open_window(size(px(400.), px(100.)), |window, cx| {
5420 states = Some((
5421 cx.new(|cx| InputState::new(window, cx)),
5422 cx.new(|cx| InputState::new(window, cx).masked(true)),
5423 cx.new(|cx| TextareaState::new(window, cx)),
5424 cx.new(|cx| {
5425 InputState::new(window, cx)
5426 .input_action(TextInputAction::Search)
5427 .autocorrect(true)
5428 .autocapitalize(Autocapitalize::Words)
5429 }),
5430 ));
5431 gpui::EmptyView
5432 });
5433 let (plain, masked, textarea, search) = states.unwrap();
5434
5435 cx.read(|cx| {
5436 assert_eq!(
5437 plain.read(cx).text_input_configuration(),
5438 TextInputConfiguration {
5439 input_action: TextInputAction::Done,
5440 ..Default::default()
5441 }
5442 );
5443 assert_eq!(
5444 masked.read(cx).text_input_configuration(),
5445 TextInputConfiguration {
5446 input_action: TextInputAction::Done,
5447 ..Default::default()
5448 }
5449 );
5450 assert_eq!(
5451 textarea.read(cx).text_input_configuration(),
5452 TextInputConfiguration {
5453 input_action: TextInputAction::Enter,
5454 ..Default::default()
5455 }
5456 );
5457 let search = search.read(cx).text_input_configuration();
5458 assert_eq!(search.input_action, TextInputAction::Search);
5459 assert_eq!(search.autocapitalize, Autocapitalize::Words);
5460 assert!(search.autocorrect);
5461 assert!(search.suggestions);
5462 });
5463 }
5464
5465 #[gpui::test]
5466 fn test_multicursor_unicode_completion_uses_post_edit_start(cx: &mut TestAppContext) {
5467 use crate::input::{CompletionProvider, EditorState};
5468 use gpui::Task;
5469 use lsp_types::{CompletionContext, CompletionResponse};
5470 use std::cell::RefCell;
5471
5472 #[derive(Default)]
5473 struct Provider {
5474 starts: RefCell<Vec<usize>>,
5475 queries: RefCell<Vec<(usize, String)>>,
5476 }
5477
5478 impl CompletionProvider for Provider {
5479 fn is_completion_trigger(&self, offset: usize, _: &str, _: &mut App) -> bool {
5480 self.starts.borrow_mut().push(offset);
5481 true
5482 }
5483
5484 fn completions(
5485 &self,
5486 _: &Rope,
5487 offset: usize,
5488 trigger: CompletionContext,
5489 _: &mut Window,
5490 _: &mut App,
5491 ) -> Task<anyhow::Result<CompletionResponse>> {
5492 self.queries
5493 .borrow_mut()
5494 .push((offset, trigger.trigger_character.unwrap()));
5495 Task::ready(Ok(CompletionResponse::Array(vec![])))
5496 }
5497 }
5498
5499 cx.update(crate::init);
5500 let mut editor = None;
5501 let window = cx.open_window(size(px(400.), px(100.)), |window, cx| {
5502 editor = Some(cx.new(|cx| EditorState::new(window, cx).default_value("aa")));
5503 gpui::EmptyView
5504 });
5505 let editor = editor.unwrap();
5506 let provider = Rc::new(Provider::default());
5507 window
5508 .update(cx, |_, window, cx| {
5509 editor.update(cx, |state, cx| {
5510 state.extras.lsp.completion_provider = Some(provider.clone());
5511 state
5512 .extras
5513 .context_menu_content
5514 .completion
5515 .trigger_start_offset = Some(0);
5516 state.extras.context_menu_content.completion.query = "aa".into();
5517 state.set_cursor_to(1);
5518 state.add_cursor_at(0, cx);
5519
5520 state.replace_text_in_range(None, "中", window, cx);
5521
5522 assert_eq!(state.value(), "中a中a");
5523 assert_eq!(provider.starts.borrow().as_slice(), &[4]);
5524 assert_eq!(provider.queries.borrow().as_slice(), &[(7, "中".into())]);
5525 });
5526 })
5527 .unwrap();
5528 }
5529
5530 #[gpui::test]
5531 fn selected_range_restores_unicode_direction_without_splitting_characters(
5532 cx: &mut TestAppContext,
5533 ) {
5534 use crate::input::EditorState;
5535 cx.update(crate::init);
5536 cx.open_window(size(px(400.), px(100.)), |window, cx| {
5537 let _state = cx.new(|cx| {
5538 let mut state = EditorState::new(window, cx).default_value("中😀a");
5539 state.set_selected_range_with_direction(3..8, true, cx);
5540 let selected = state.selected_text_range(false, window, cx).unwrap();
5541 assert_eq!(selected.range, 1..4);
5542 assert!(selected.reversed);
5543 assert_eq!(state.selected_range(), 3..8);
5544 assert!(state.is_selection_reversed());
5545 let range = state.selected_range();
5546 let reversed = state.is_selection_reversed();
5547 state.set_selected_range_with_direction(range, reversed, cx);
5548 assert!(state.is_selection_reversed());
5549 state.set_selected_range(3..8, cx);
5550 let selected = state.selected_text_range(false, window, cx).unwrap();
5551 assert_eq!(selected.range, 1..4);
5552 assert!(!selected.reversed);
5553 assert!(!state.is_selection_reversed());
5554 state.set_selected_range(Range { start: 8, end: 3 }, cx);
5556 assert_eq!(state.selected_range(), 8..8);
5557 assert!(!state.is_selection_reversed());
5558 state.set_selected_range_with_direction(4..4, true, cx);
5559 assert!(!state.is_selection_reversed());
5560 assert_eq!(
5561 state.selected_text_range(false, window, cx).unwrap().range,
5562 1..1
5563 );
5564 state
5565 });
5566 gpui::EmptyView
5567 });
5568 }
5569
5570 #[gpui::test]
5571 fn test_inline_token_wrap_and_size_refresh(cx: &mut TestAppContext) {
5572 use crate::input::{InlineToken, InlineTokenPresentation};
5573 let width = Rc::new(Cell::new(4000.));
5574 let render_width = width.clone();
5575 let view = InputView::build_textarea(cx, |state| state.rows(4).default_value("@a@b"));
5576 view.window_handle
5577 .update(cx, |_, window, cx| {
5578 view.input.update(cx, |state, cx| {
5579 state
5580 .replace_range_with_token(0..2, InlineToken::new("a", "@a"), window, cx)
5581 .unwrap();
5582 state
5583 .replace_range_with_token(2..4, InlineToken::new("b", "@b"), window, cx)
5584 .unwrap();
5585 state.set_token_presentation(
5586 InlineTokenPresentation::default()
5587 .token(move |_, _, _| div().w(px(render_width.get()))),
5588 );
5589 });
5590 })
5591 .unwrap();
5592 let mut visual = VisualTestContext::from_window(view.window_handle.into(), cx);
5593 visual.update(|window, cx| window.draw(cx).clear(cx));
5594 view.input.read_with(&visual, |state, _| {
5595 assert_eq!(state.display_map.wrap_row_count(), 2);
5596 let first = state.range_to_bounds(&(0..2)).unwrap();
5597 let second = state.range_to_bounds(&(2..4)).unwrap();
5598 assert!(second.origin.y > first.origin.y);
5599 assert_eq!(
5600 state.last_layout.as_ref().unwrap().lines[0].wrapped_lines[0].len,
5601 2
5602 );
5603 });
5604 width.set(100.);
5605 view.input.update(&mut visual, |_, cx| cx.notify());
5606 visual.update(|window, cx| window.draw(cx).clear(cx));
5607 view.input.read_with(&visual, |state, _| {
5608 assert_eq!(state.display_map.wrap_row_count(), 1)
5609 });
5610 }
5611
5612 #[gpui::test]
5613 fn test_inline_token_geometry_and_reentrant_activation(cx: &mut TestAppContext) {
5614 use crate::input::{InlineToken, InlineTokenPresentation};
5615 let view = InputView::build(cx, |state| state.default_value("before @alice after"));
5616 let target = view.input.clone();
5617 view.window_handle
5618 .update(cx, |_, window, cx| {
5619 view.input.update(cx, |state, cx| {
5620 state
5621 .replace_range_with_token(
5622 7..13,
5623 InlineToken::new("a", "@alice"),
5624 window,
5625 cx,
5626 )
5627 .unwrap();
5628 state.set_token_presentation(
5629 InlineTokenPresentation::default()
5630 .token(|_, _, _| div().w(px(100.)).h(px(20.)))
5631 .on_token_click(move |_, window, cx| {
5632 target.update(cx, |state, cx| {
5633 state.set_value("activated", window, cx)
5634 })
5635 }),
5636 );
5637 state.focus(window, cx);
5638 state.set_selected_range(7..13, cx);
5639 });
5640 })
5641 .unwrap();
5642 let mut visual = VisualTestContext::from_window(view.window_handle.into(), cx);
5643 visual.update(|window, cx| window.draw(cx).clear(cx));
5644 visual.update(|window, cx| {
5645 let state = view.input.read(cx);
5646 let bounds = state.range_to_bounds(&(7..13)).unwrap();
5647 assert!((bounds.size.width - px(100.)).abs() < px(1.), "{bounds:?}");
5648 let (left, _) = state.index_for_mouse_position(bounds.origin + point(px(10.), px(5.)));
5649 let (right, _) = state.index_for_mouse_position(bounds.origin + point(px(90.), px(5.)));
5650 assert_eq!((left, right), (7, 13));
5651 window.dispatch_action(Box::new(ActivateToken), cx);
5652 });
5653 visual.run_until_parked();
5654 view.input.read_with(&visual, |state, _| {
5655 assert_eq!(state.value().as_ref(), "activated")
5656 });
5657 }
5658
5659 #[gpui::test]
5660 fn test_inline_token_click_selects_it(cx: &mut TestAppContext) {
5661 use crate::input::InlineToken;
5662 cx.update(crate::init);
5663 let view = InputView::build(cx, |state| state.default_value("before @alice after"));
5664 view.window_handle
5665 .update(cx, |_, window, cx| {
5666 view.input.update(cx, |state, cx| {
5667 state
5668 .replace_range_with_token(
5669 7..13,
5670 InlineToken::new("a", "@alice"),
5671 window,
5672 cx,
5673 )
5674 .unwrap();
5675 state.set_selected_range(0..0, cx);
5676 });
5677 })
5678 .unwrap();
5679 let mut visual = VisualTestContext::from_window(view.window_handle.into(), cx);
5680 visual.update(|window, cx| window.draw(cx).clear(cx));
5681 let bounds = view
5682 .input
5683 .read_with(&visual, |state, _| state.range_to_bounds(&(7..13)).unwrap());
5684 visual.simulate_click(bounds.center(), gpui::Modifiers::default());
5685 visual.run_until_parked();
5686 view.input.read_with(&visual, |state, _| {
5687 assert_eq!(state.selected_range(), 7..13, "a click selects the token");
5688 });
5689 }
5690
5691 #[gpui::test]
5692 fn test_inline_token_hover_reports_presence_without_selecting(cx: &mut TestAppContext) {
5693 use crate::input::{InlineToken, InlineTokenPresentation};
5694 use gpui::{Bounds, point, px, size};
5695 cx.update(crate::init);
5696 let view = InputView::build(cx, |state| state.default_value("before @alice after"));
5697 let bounds = Bounds::new(point(px(10.), px(20.)), size(px(100.), px(20.)));
5698 view.window_handle
5699 .update(cx, |_, window, cx| {
5700 view.input.update(cx, |state, cx| {
5701 state
5702 .replace_range_with_token(
5703 7..13,
5704 InlineToken::new("a", "@alice"),
5705 window,
5706 cx,
5707 )
5708 .unwrap();
5709 state.set_token_presentation(
5710 InlineTokenPresentation::default().on_token_hover(|_, _, _| {}),
5711 );
5712 state.set_selected_range(0..0, cx);
5713 });
5714 })
5715 .unwrap();
5716 view.window_handle
5717 .update(cx, |_, _, cx| {
5718 view.input.update(cx, |state, _| {
5719 let (_, entered) = state
5720 .token_hover(7, bounds, true, None)
5721 .expect("hover enters an enabled token");
5722 assert!(entered.is_hovered());
5723 assert_eq!(entered.token().id().as_ref(), "a");
5724 assert_eq!(entered.range(), 7..13);
5725 assert_eq!(entered.bounds(), bounds);
5726 let (_, left) = state
5727 .token_hover(7, bounds, false, None)
5728 .expect("hover leaves the same token");
5729 assert!(!left.is_hovered());
5730 assert_eq!(state.selected_range(), 0..0, "hover never selects");
5731 assert!(state.token_hover(99, bounds, true, None).is_none());
5732 });
5733 })
5734 .unwrap();
5735
5736 view.window_handle
5738 .update(cx, |_, _, cx| {
5739 view.input.update(cx, |state, cx| {
5740 state.set_disabled(true, cx);
5741 });
5742 })
5743 .unwrap();
5744 view.window_handle
5745 .update(cx, |_, _, cx| {
5746 view.input.update(cx, |state, _| {
5747 assert!(state.token_hover(7, bounds, true, None).is_none())
5748 })
5749 })
5750 .unwrap();
5751 view.window_handle
5752 .update(cx, |_, _, cx| {
5753 view.input.update(cx, |state, cx| {
5754 state.set_disabled(false, cx);
5755 state.set_readonly(true, cx);
5756 });
5757 })
5758 .unwrap();
5759 view.window_handle
5760 .update(cx, |_, _, cx| {
5761 view.input.update(cx, |state, _| {
5762 assert!(state.token_hover(7, bounds, true, None).is_some())
5763 })
5764 })
5765 .unwrap();
5766
5767 view.window_handle
5769 .update(cx, |_, _, cx| {
5770 view.input.update(cx, |state, _| {
5771 state.set_token_presentation(InlineTokenPresentation::default());
5772 });
5773 })
5774 .unwrap();
5775 view.window_handle
5776 .update(cx, |_, _, cx| {
5777 view.input.update(cx, |state, _| {
5778 assert!(state.token_hover(7, bounds, true, None).is_none())
5779 })
5780 })
5781 .unwrap();
5782 }
5783
5784 #[gpui::test]
5785 fn test_inline_token_hover_exit_delivered_when_token_removed(cx: &mut TestAppContext) {
5786 use crate::input::{InlineToken, InlineTokenPresentation};
5787 use std::cell::RefCell;
5788 use std::rc::Rc;
5789 cx.update(crate::init);
5790 let view = InputView::build(cx, |state| state.default_value("before @alice after"));
5791 let events: Rc<RefCell<Vec<(SharedString, bool)>>> = Rc::new(RefCell::new(Vec::new()));
5792 let sink = events.clone();
5793 view.window_handle
5794 .update(cx, |_, window, cx| {
5795 view.input.update(cx, |state, cx| {
5796 state
5797 .replace_range_with_token(
5798 7..13,
5799 InlineToken::new("a", "@alice"),
5800 window,
5801 cx,
5802 )
5803 .unwrap();
5804 state.set_token_presentation(
5805 InlineTokenPresentation::default()
5806 .token(|_, _, _| div().w(px(100.)).h(px(20.)))
5807 .on_token_hover(move |event, _, _| {
5808 sink.borrow_mut()
5809 .push((event.token().id().clone(), event.is_hovered()));
5810 }),
5811 );
5812 });
5813 })
5814 .unwrap();
5815 let mut visual = VisualTestContext::from_window(view.window_handle.into(), cx);
5816 visual.update(|window, cx| window.draw(cx).clear(cx));
5817 let known_bounds = view.input.read_with(&visual, |state, _| {
5818 state
5819 .token_bounds
5820 .get(&7)
5821 .copied()
5822 .expect("placed token bounds")
5823 });
5824 visual.simulate_mouse_move(known_bounds.center(), None, gpui::Modifiers::default());
5825 visual.run_until_parked();
5826 visual.update(|window, cx| window.draw(cx).clear(cx));
5827 assert_eq!(events.borrow().as_slice(), &[("a".into(), true)]);
5828
5829 view.window_handle
5833 .update(cx, |_, window, cx| {
5834 view.input
5835 .update(cx, |state, cx| state.set_value("no tokens", window, cx));
5836 })
5837 .unwrap();
5838 visual.run_until_parked();
5839 visual.update(|window, cx| window.draw(cx).clear(cx));
5840 assert_eq!(
5841 events.borrow().as_slice(),
5842 &[("a".into(), true), ("a".into(), false)]
5843 );
5844 }
5845
5846 #[gpui::test]
5847 fn test_inline_token_hover_bounds_stay_single_row_at_wrap_boundary(cx: &mut TestAppContext) {
5848 use crate::input::{InlineToken, InlineTokenPresentation};
5849 cx.update(crate::init);
5850 let view = InputView::build_textarea(cx, |state| {
5851 state
5852 .rows(4)
5853 .default_value("aaaa bbbb cccc dddd @alice eeee ffff")
5854 });
5855 view.window_handle
5856 .update(cx, |_, window, cx| {
5857 view.input.update(cx, |state, cx| {
5858 state
5859 .replace_range_with_token(
5860 20..26,
5861 InlineToken::new("a", "@alice"),
5862 window,
5863 cx,
5864 )
5865 .unwrap();
5866 state.set_token_presentation(
5867 InlineTokenPresentation::default()
5868 .token(|_, _, _| div().w(px(60.)).h(px(20.))),
5869 );
5870 });
5871 })
5872 .unwrap();
5873 let mut visual = VisualTestContext::from_window(view.window_handle.into(), cx);
5874 visual.update(|window, cx| window.draw(cx).clear(cx));
5875 let (placed, ranged, line_height) = view.input.read_with(&visual, |state, _| {
5876 (
5877 state
5878 .token_bounds
5879 .get(&20)
5880 .copied()
5881 .expect("placed token bounds"),
5882 state.range_to_bounds(&(20..26)),
5883 state.last_layout.as_ref().unwrap().line_height,
5884 )
5885 });
5886 assert!(placed.size.height <= line_height + px(1.), "{placed:?}");
5890 assert!(
5891 placed.size.width <= px(61.) && placed.size.width > px(0.),
5892 "{placed:?}"
5893 );
5894 if let Some(ranged) = ranged {
5895 assert!(
5896 ranged.size.height >= placed.size.height - px(1.),
5897 "{ranged:?} vs {placed:?}"
5898 );
5899 }
5900 }
5901
5902 #[gpui::test]
5903 fn test_inline_token_hover_same_offset_replacement_exits_predecessor(cx: &mut TestAppContext) {
5904 use crate::input::{InlineToken, InlineTokenPresentation};
5905 use std::{cell::RefCell, rc::Rc};
5906 cx.update(crate::init);
5907 let view = InputView::build(cx, |state| state.default_value("@alice!"));
5908 let events = Rc::new(RefCell::new(Vec::new()));
5909 let sink = events.clone();
5910 view.window_handle
5911 .update(cx, |_, window, cx| {
5912 view.input.update(cx, |state, cx| {
5913 state
5914 .replace_range_with_token(0..6, InlineToken::new("a", "@alice"), window, cx)
5915 .unwrap();
5916 state.set_token_presentation(
5917 InlineTokenPresentation::default()
5918 .token(|_, _, _| div().w(px(100.)).h(px(20.)))
5919 .on_token_hover(move |event, _, _| {
5920 sink.borrow_mut()
5921 .push((event.token().id().clone(), event.is_hovered()));
5922 }),
5923 );
5924 });
5925 })
5926 .unwrap();
5927 let mut visual = VisualTestContext::from_window(view.window_handle.into(), cx);
5928 visual.update(|window, cx| window.draw(cx).clear(cx));
5929 let bounds = view
5930 .input
5931 .read_with(&visual, |state, _| state.token_bounds[&0]);
5932 visual.simulate_mouse_move(bounds.center(), None, gpui::Modifiers::default());
5933 visual.run_until_parked();
5934 visual.update(|window, cx| window.draw(cx).clear(cx));
5935 assert_eq!(events.borrow().as_slice(), &[("a".into(), true)]);
5936
5937 view.window_handle
5938 .update(cx, |_, window, cx| {
5939 view.input.update(cx, |state, cx| {
5940 state
5941 .replace_range_with_token(0..6, InlineToken::new("b", "@alice"), window, cx)
5942 .unwrap();
5943 });
5944 })
5945 .unwrap();
5946 visual.run_until_parked();
5947 visual.update(|window, cx| window.draw(cx).clear(cx));
5948 visual.simulate_mouse_move(
5949 bounds.center() + gpui::point(px(1.), px(0.)),
5950 None,
5951 gpui::Modifiers::default(),
5952 );
5953 visual.run_until_parked();
5954 visual.update(|window, cx| window.draw(cx).clear(cx));
5955 assert_eq!(
5956 events.borrow().as_slice(),
5957 &[("a".into(), true), ("a".into(), false), ("b".into(), true)]
5958 );
5959 view.input
5962 .update(&mut visual, |state, cx| state.set_disabled(true, cx));
5963 visual.update(|window, cx| window.draw(cx).clear(cx));
5964 visual.run_until_parked();
5965 assert_eq!(events.borrow().last(), Some(&("b".into(), false)));
5966 view.input
5967 .update(&mut visual, |state, cx| state.set_disabled(false, cx));
5968 visual.update(|window, cx| window.draw(cx).clear(cx));
5969 visual.run_until_parked();
5970 visual.simulate_mouse_move(bounds.center(), None, gpui::Modifiers::default());
5971 visual.run_until_parked();
5972 assert_eq!(
5973 events.borrow().as_slice(),
5974 &[
5975 ("a".into(), true),
5976 ("a".into(), false),
5977 ("b".into(), true),
5978 ("b".into(), false),
5979 ("b".into(), true)
5980 ]
5981 );
5982 visual.simulate_mouse_move(
5983 bounds.bottom_right() + gpui::point(px(10.), px(10.)),
5984 None,
5985 gpui::Modifiers::default(),
5986 );
5987 visual.run_until_parked();
5988 assert_eq!(
5989 events.borrow().as_slice(),
5990 &[
5991 ("a".into(), true),
5992 ("a".into(), false),
5993 ("b".into(), true),
5994 ("b".into(), false),
5995 ("b".into(), true),
5996 ("b".into(), false)
5997 ]
5998 );
5999 }
6000
6001 #[gpui::test]
6002 fn test_inline_token_hover_older_exit_keeps_newer_snapshot(cx: &mut TestAppContext) {
6003 use crate::input::{InlineToken, InlineTokenPresentation};
6004 use gpui::{Bounds, point, px, size};
6005 cx.update(crate::init);
6006 let view = InputView::build(cx, |state| state.default_value("@a @b"));
6007 let bounds = Bounds::new(point(px(10.), px(20.)), size(px(100.), px(20.)));
6008 view.window_handle
6009 .update(cx, |_, window, cx| {
6010 view.input.update(cx, |state, cx| {
6011 state
6012 .replace_range_with_token(0..2, InlineToken::new("a", "@a"), window, cx)
6013 .unwrap();
6014 state
6015 .replace_range_with_token(3..5, InlineToken::new("b", "@b"), window, cx)
6016 .unwrap();
6017 state.set_token_presentation(
6018 InlineTokenPresentation::default().on_token_hover(|_, _, _| {}),
6019 );
6020 state
6023 .token_hover(0, bounds, true, None)
6024 .expect("hover enters A");
6025 state
6026 .token_hover(3, bounds, true, None)
6027 .expect("hover enters B");
6028 let (_, exit_a) = state.token_hover(0, bounds, false, None).expect("A exits");
6029 assert!(!exit_a.is_hovered());
6030 assert_eq!(
6032 state
6033 .hovered_token
6034 .as_ref()
6035 .expect("B still hovered")
6036 .span
6037 .token()
6038 .id()
6039 .as_ref(),
6040 "b"
6041 );
6042 state.set_value("no tokens", window, cx);
6044 state.token_bounds.clear();
6045 let (listener, exit_b) = state.reconcile_token_hover().expect("B must exit");
6046 assert!(!exit_b.is_hovered());
6047 assert_eq!(exit_b.token().id().as_ref(), "b");
6048 drop(listener);
6049 });
6050 })
6051 .unwrap();
6052 }
6053
6054 #[gpui::test]
6055 fn test_inline_token_hover_late_exit_matches_replaced_token(cx: &mut TestAppContext) {
6056 use crate::input::{InlineToken, InlineTokenPresentation};
6057 cx.update(crate::init);
6058 let view = InputView::build(cx, |state| state.default_value("@a"));
6059 view.window_handle
6060 .update(cx, |_, window, cx| {
6061 view.input.update(cx, |state, cx| {
6062 let a = InlineToken::new("a", "@a");
6063 let b = InlineToken::new("b", "@a");
6064 state
6065 .replace_range_with_token(0..2, a.clone(), window, cx)
6066 .unwrap();
6067 state.set_token_presentation(
6068 InlineTokenPresentation::default().on_token_hover(|_, _, _| {}),
6069 );
6070 let bounds = gpui::Bounds::default();
6071 state.token_hover(0, bounds, true, Some(&a)).unwrap();
6072 state
6073 .replace_range_with_token(0..2, b.clone(), window, cx)
6074 .unwrap();
6075 state.token_hover(0, bounds, true, Some(&b)).unwrap();
6076 let (_, exit_a) = state.token_hover(0, bounds, false, Some(&a)).unwrap();
6077 assert_eq!(exit_a.token().id().as_ref(), "a");
6078 assert_eq!(state.hovered_token.as_ref().unwrap().span.token(), &b);
6079 let (_, exit_b) = state.token_hover(0, bounds, false, Some(&b)).unwrap();
6080 assert_eq!(exit_b.token().id().as_ref(), "b");
6081 assert!(state.pending_token_hover_exits.is_empty());
6082 });
6083 })
6084 .unwrap();
6085 }
6086
6087 #[gpui::test]
6088 fn test_inline_token_hover_exit_keeps_entry_utf16_after_edit(cx: &mut TestAppContext) {
6089 use crate::input::{InlineToken, InlineTokenPresentation};
6090 use gpui::EntityInputHandler as _;
6091 cx.update(crate::init);
6092 let view = InputView::build(cx, |state| state.default_value("🙂 @a"));
6093 view.window_handle
6094 .update(cx, |_, window, cx| {
6095 view.input.update(cx, |state, cx| {
6096 state
6097 .replace_range_with_token(5..7, InlineToken::new("a", "@a"), window, cx)
6098 .unwrap();
6099 state.set_token_presentation(
6100 InlineTokenPresentation::default().on_token_hover(|_, _, _| {}),
6101 );
6102 let bounds = gpui::Bounds::default();
6103 let (_, enter) = state.token_hover(5, bounds, true, None).unwrap();
6104 assert_eq!(enter.range_utf16(), (3, 5));
6105 state.replace_text_in_range(Some(0..2), "abcd", window, cx);
6106 assert_eq!(state.token_spans()[0].range(), 5..7);
6107 let (_, exit) = state.token_hover(5, bounds, false, None).unwrap();
6108 assert_eq!(exit.range_utf16(), (3, 5));
6109 });
6110 })
6111 .unwrap();
6112 }
6113
6114 #[gpui::test]
6115 fn test_set_selected_text_range(cx: &mut TestAppContext) {
6116 use gpui::EntityInputHandler as _;
6117 cx.update(crate::init);
6118 let view = InputView::build(cx, |state| state.default_value("🙂 hello world"));
6120 view.window_handle
6121 .update(cx, |_, window, cx| {
6122 view.input.update(cx, |state, cx| {
6123 fn selection(
6124 state: &mut InputBaseState<InputMode>,
6125 window: &mut Window,
6126 cx: &mut Context<InputBaseState<InputMode>>,
6127 ) -> (Range<usize>, bool) {
6128 let selection = state.selected_text_range(false, window, cx).unwrap();
6129 (selection.range, selection.reversed)
6130 }
6131
6132 state.set_selected_text_range(3..8, window, cx);
6133 assert_eq!(state.selected_range(), 5..10);
6134 assert_eq!(selection(state, window, cx), (3..8, false));
6135
6136 state.set_selected_text_range(3..14, window, cx);
6138 assert_eq!(selection(state, window, cx), (3..14, false));
6139 assert_eq!(state.cursor(), 16);
6140
6141 state.set_selected_text_range(9..14, window, cx);
6143 assert_eq!(selection(state, window, cx), (9..14, true));
6144 assert_eq!(state.cursor(), 11);
6145
6146 state.set_selected_text_range(9..14, window, cx);
6148 assert_eq!(selection(state, window, cx), (9..14, true));
6149
6150 state.set_selected_text_range(100..9, window, cx);
6152 assert_eq!(selection(state, window, cx), (9..14, true));
6153
6154 state.set_selected_text_range(3..8, window, cx);
6156 state.set_selected_text_range(2..3, window, cx);
6157 assert_eq!(selection(state, window, cx), (2..3, true));
6158 assert_eq!(state.cursor(), 4);
6159 state.set_selected_text_range(3..8, window, cx);
6160 state.set_selected_text_range(8..9, window, cx);
6161 assert_eq!(selection(state, window, cx), (8..9, false));
6162 assert_eq!(state.cursor(), 11);
6163
6164 state.set_selected_text_range(2..2, window, cx);
6166 assert_eq!(selection(state, window, cx), (2..2, false));
6167 assert_eq!(state.value(), "🙂 hello world");
6168 });
6169 })
6170 .unwrap();
6171 }
6172
6173 #[gpui::test]
6174 fn test_inline_token_hover_callback_edit_preserves_both_exits(cx: &mut TestAppContext) {
6175 use crate::input::{InlineToken, InlineTokenPresentation};
6176 cx.update(crate::init);
6177 let view = InputView::build(cx, |state| state.default_value("@a @b"));
6178 view.window_handle
6179 .update(cx, |_, window, cx| {
6180 view.input.update(cx, |state, cx| {
6181 state
6182 .replace_range_with_token(0..2, InlineToken::new("a", "@a"), window, cx)
6183 .unwrap();
6184 state
6185 .replace_range_with_token(3..5, InlineToken::new("b", "@b"), window, cx)
6186 .unwrap();
6187 state.set_token_presentation(
6188 InlineTokenPresentation::default().on_token_hover(|_, _, _| {}),
6189 );
6190 let bounds = gpui::Bounds::default();
6191 state.token_hover(0, bounds, true, None).unwrap();
6192 state.token_hover(3, bounds, true, None).unwrap();
6193 state.set_value("gone", window, cx);
6195 let (_, exit_a) = state.token_hover(0, bounds, false, None).unwrap();
6196 assert_eq!(exit_a.token().id().as_ref(), "a");
6197 let (_, exit_b) = state.reconcile_token_hover().unwrap();
6198 assert_eq!(exit_b.token().id().as_ref(), "b");
6199 assert!(state.token_hover(0, bounds, false, None).is_none());
6200 assert!(state.reconcile_token_hover().is_none());
6201 });
6202 })
6203 .unwrap();
6204 }
6205
6206 #[gpui::test]
6207 fn test_inline_token_edit_history_and_validation(cx: &mut TestAppContext) {
6208 let view = InputView::build(cx, |state| state.default_value("问 @alice!"));
6209 view.window_handle
6210 .update(cx, |_, window, cx| {
6211 view.input.update(cx, |state, cx| {
6212 let token =
6213 crate::input::InlineToken::new("alice-1", "@alice").with_label("Alice");
6214 state
6215 .replace_range_with_token(4..10, token.clone(), window, cx)
6216 .unwrap();
6217 assert_eq!(state.value().as_ref(), "问 @alice!");
6218 assert_eq!(state.tokens()[0].range(), 4..10);
6219 assert_eq!(state.next_end_of_word_at(4), 10);
6220 assert_eq!(state.previous_start_of_word_at(10), 4);
6221 state.undo(&Undo, window, cx);
6222 assert!(
6223 state.tokens().is_empty(),
6224 "identity-only association is undoable"
6225 );
6226 assert_eq!(state.value().as_ref(), "问 @alice!");
6227 state.redo(&Redo, window, cx);
6228 assert_eq!(state.tokens()[0].token(), &token);
6229 state.set_selected_range(6..7, cx);
6230 assert_eq!(state.selected_range(), 4..10);
6231 state.replace("", window, cx);
6232 assert_eq!(state.value().as_ref(), "问 !");
6233 assert!(state.tokens().is_empty());
6234 state.undo(&Undo, window, cx);
6235 assert_eq!(state.tokens()[0].token(), &token);
6236 state.set_selected_range(0..0, cx);
6237 state.replace("🙂", window, cx);
6238 assert_eq!(state.tokens()[0].range(), 8..14);
6239 state.undo(&Undo, window, cx);
6240 assert_eq!(state.tokens()[0].range(), 4..10);
6241 let before = state.content();
6242 state
6243 .replace_range_with_token(0..0, token.clone(), window, cx)
6244 .expect("the same reference may occur twice");
6245 assert_eq!(state.value().as_ref(), "@alice问 @alice!");
6246 assert_eq!(state.tokens().len(), 2);
6247 assert_eq!(state.tokens()[1].token(), &token);
6248 state.undo(&Undo, window, cx);
6249 assert_eq!(state.content(), before);
6250 state.set_value(before.text().clone(), window, cx);
6251 assert!(state.tokens().is_empty());
6252 assert!(!state.undo_manager.has_undos());
6253 });
6254 })
6255 .unwrap();
6256 }
6257
6258 #[gpui::test]
6259 fn test_inline_token_textarea_ime_and_modes(cx: &mut TestAppContext) {
6260 use crate::input::{InlineToken, InlineTokenError, InputContent};
6261 let view = InputView::build_textarea(cx, |state| state);
6262 view.window_handle
6263 .update(cx, |_, window, cx| {
6264 view.input.update(cx, |state, cx| {
6265 let content = InputContent::new("@a@b\n后面")
6266 .with_token(0..2, InlineToken::new("a", "@a"))
6267 .unwrap()
6268 .with_token(2..4, InlineToken::new("b", "@b"))
6269 .unwrap();
6270 state.set_value(content.clone(), window, cx);
6271 state.set_selected_range(2..2, cx);
6272 assert_eq!(state.previous_boundary(2), 0);
6273 assert_eq!(state.next_boundary(2), 4);
6274 state.replace_and_mark_text_in_range(Some(1..2), "中", Some(1..1), window, cx);
6275 assert_eq!(state.value().as_ref(), "中@b\n后面");
6276 assert_eq!(state.tokens().len(), 1);
6277 assert_eq!(
6278 state.replace_with_token(InlineToken::new("x", "x"), window, cx),
6279 Err(InlineTokenError::CompositionActive)
6280 );
6281 state.replace_text_in_range(None, "中文", window, cx);
6282 state.undo(&Undo, window, cx);
6283 assert_eq!(state.content(), content);
6284 state.redo(&Redo, window, cx);
6285 assert_eq!(state.value().as_ref(), "中文@b\n后面");
6286 state.undo(&Undo, window, cx);
6287 state.replace_all("plain", window, cx);
6288 assert!(state.tokens().is_empty());
6289 state.undo(&Undo, window, cx);
6290 assert_eq!(state.content(), content);
6291 });
6292 })
6293 .unwrap();
6294 let view = InputView::build(cx, |state| state);
6295 view.window_handle
6296 .update(cx, |_, window, cx| {
6297 view.input.update(cx, |state, cx| {
6298 assert_eq!(
6299 InputContent::new("a\u{301}")
6300 .with_token(0..1, InlineToken::new("bad", "a"))
6301 .err(),
6302 Some(InlineTokenError::InvalidBoundary)
6303 );
6304 assert_eq!(
6305 InputContent::new("@a@a")
6306 .with_token(0..2, InlineToken::new("a", "@a"))
6307 .unwrap()
6308 .with_token(1..3, InlineToken::new("x", "a@"))
6309 .err(),
6310 Some(InlineTokenError::OverlappingTokens)
6311 );
6312 state
6313 .replace_with_token(InlineToken::new("a", "@a"), window, cx)
6314 .unwrap();
6315 let before = state.content();
6316 state.set_masked(true, window, cx);
6317 assert!(!state.tokens_visible());
6318 assert_eq!(
6319 state.replace_with_token(InlineToken::new("b", "b"), window, cx),
6320 Err(InlineTokenError::UnsupportedMode)
6321 );
6322 assert_eq!(state.content(), before);
6323 state.undo(&Undo, window, cx);
6324 assert!(state.tokens().is_empty());
6325 state.redo(&Redo, window, cx);
6326 assert_eq!(state.content(), before);
6327 assert!(!state.tokens_visible());
6328 });
6329 })
6330 .unwrap();
6331 }
6332
6333 use crate::theme::Theme;
6334 use gpui::{TestAppContext, VisualTestContext, size};
6335
6336 use crate::input::{EditorMode, EditorState, InputMode, LanguageConfig, TabSize, TextareaMode};
6337
6338 fn set_test_syntax_provider(
6339 provider: Rc<dyn crate::input::SyntaxContextProvider>,
6340 cx: &mut App,
6341 ) {
6342 struct TestLanguages(Rc<dyn crate::input::SyntaxContextProvider>);
6343 impl crate::input::LanguageProvider for TestLanguages {
6344 fn syntax_context_provider(
6345 &self,
6346 _: &str,
6347 ) -> Option<Rc<dyn crate::input::SyntaxContextProvider>> {
6348 Some(self.0.clone())
6349 }
6350 }
6351 crate::input::set_language_provider(Rc::new(TestLanguages(provider)), cx);
6352 }
6353
6354 struct TestRoot<M: InputModeKind>(Entity<InputBaseState<M>>);
6355
6356 impl<M: InputModeKind> Render for TestRoot<M> {
6357 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
6358 div().size_full().child(self.0.clone())
6359 }
6360 }
6361
6362 struct InputView<M: InputModeKind> {
6363 input: Entity<InputBaseState<M>>,
6364 window_handle: gpui::WindowHandle<TestRoot<M>>,
6365 }
6366
6367 impl<M: InputModeKind> InputView<M> {
6369 fn build_with(
6370 cx: &mut TestAppContext,
6371 make: impl FnOnce(&mut Window, &mut Context<InputBaseState<M>>) -> InputBaseState<M>
6372 + 'static,
6373 ) -> Self {
6374 let mut input: Option<Entity<InputBaseState<M>>> = None;
6375
6376 let window = cx.update(|cx| {
6377 cx.open_window(Default::default(), |window, cx| {
6378 cx.set_global(Theme::default());
6380 super::super::init(cx);
6382
6383 input = Some(cx.new(|cx| make(window, cx)));
6384
6385 cx.new(|_| TestRoot(input.clone().unwrap()))
6386 })
6387 .unwrap()
6388 });
6389
6390 Self {
6391 input: input.clone().unwrap(),
6392 window_handle: window,
6393 }
6394 }
6395 }
6396
6397 impl InputView<EditorMode> {
6398 fn new(cx: &mut TestAppContext) -> Self {
6400 Self::build_editor(cx, |state| state)
6401 }
6402
6403 fn build_editor(
6404 cx: &mut TestAppContext,
6405 f: impl FnOnce(InputBaseState<EditorMode>) -> InputBaseState<EditorMode> + 'static,
6406 ) -> Self {
6407 Self::build_with(cx, move |window, cx| {
6408 f(crate::input::EditorState::new(window, cx).language("sql"))
6409 })
6410 }
6411 }
6412
6413 impl InputView<TextareaMode> {
6414 fn build_textarea(
6415 cx: &mut TestAppContext,
6416 f: impl FnOnce(InputBaseState<TextareaMode>) -> InputBaseState<TextareaMode> + 'static,
6417 ) -> Self {
6418 Self::build_with(cx, move |window, cx| {
6419 f(crate::input::TextareaState::new(window, cx))
6420 })
6421 }
6422 }
6423
6424 impl InputView<InputMode> {
6425 fn build(
6427 cx: &mut TestAppContext,
6428 f: impl FnOnce(InputBaseState<InputMode>) -> InputBaseState<InputMode> + 'static,
6429 ) -> Self {
6430 Self::build_with(cx, move |window, cx| {
6431 f(crate::input::InputState::new(window, cx))
6432 })
6433 }
6434 }
6435
6436 fn swipe<M: InputModeKind>(
6438 view: &InputView<M>,
6439 cx: &mut VisualTestContext,
6440 dy: f32,
6441 phase: gpui::TouchPhase,
6442 ) {
6443 cx.update(|window, cx| {
6444 view.input.update(cx, |state, cx| {
6445 state.on_scroll_wheel(
6446 &ScrollEvent {
6447 delta: gpui::ScrollDelta::Pixels(point(px(0.), px(dy))),
6448 touch_phase: phase,
6449 ..Default::default()
6450 },
6451 window,
6452 cx,
6453 )
6454 })
6455 });
6456 }
6457
6458 fn long_text() -> String {
6459 (0..60)
6460 .map(|n| format!("line {n}"))
6461 .collect::<Vec<_>>()
6462 .join("\n")
6463 }
6464
6465 #[gpui::test]
6466 fn test_scroll_bounce_stretches_past_the_end_and_springs_back(cx: &mut TestAppContext) {
6467 use gpui::TouchPhase::{Ended, Moved, Started};
6468
6469 let view = InputView::build_textarea(cx, |state| {
6470 state.scroll_bounce(true).default_value(long_text())
6471 });
6472 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
6473 cx.update(|window, cx| window.draw(cx).clear(cx));
6474 let drawn_at = |cx: &mut VisualTestContext| {
6475 view.input
6476 .read_with(cx, |state, _| state.last_bounds.unwrap().origin.y)
6477 };
6478
6479 swipe(&view, &mut cx, -20., Started);
6480 swipe(&view, &mut cx, -100_000., Moved);
6481 let (end, stretch) = view.input.read_with(&cx, |state, _| {
6482 (state.scroll_handle.offset().y, state.overscroll.offset())
6483 });
6484 assert!(end < px(0.), "the text scrolled to its end");
6485 assert!(stretch < 0., "the rest stretched the edge: {stretch}");
6486 cx.update(|window, cx| window.draw(cx).clear(cx));
6487 let stretched_at = drawn_at(&mut cx);
6488
6489 swipe(&view, &mut cx, 0., Ended);
6490 for _ in 0..120 {
6491 cx.executor()
6492 .advance_clock(std::time::Duration::from_millis(16));
6493 cx.update(|window, cx| window.draw(cx).clear(cx));
6494 }
6495 view.input.read_with(&cx, |state, _| {
6496 assert_eq!(state.overscroll.offset(), 0., "the edge sprang back");
6497 assert_eq!(
6498 state.scroll_handle.offset().y,
6499 end,
6500 "scrolling stayed exact"
6501 );
6502 });
6503 let moved = (stretched_at - drawn_at(&mut cx)).as_f32();
6504 assert!(
6505 (moved - stretch).abs() < 0.001,
6506 "the stretch only moved where the text was drawn: {moved} vs {stretch}"
6507 );
6508 }
6509
6510 fn swipe_past_the_end(
6513 view: &InputView<TextareaMode>,
6514 cx: &mut VisualTestContext,
6515 ) -> (Pixels, f32) {
6516 use gpui::TouchPhase::{Moved, Started};
6517
6518 cx.update(|window, cx| window.draw(cx).clear(cx));
6519 swipe(view, cx, -20., Started);
6520 swipe(view, cx, -100_000., Moved);
6521 view.input.read_with(cx, |state, _| {
6522 (state.scroll_handle.offset().y, state.overscroll.offset())
6523 })
6524 }
6525
6526 #[gpui::test]
6527 fn test_scroll_bounce_stays_put_under_reduced_motion(cx: &mut TestAppContext) {
6528 cx.update(|cx| cx.set_reduce_motion(true));
6529 let view = InputView::build_textarea(cx, |state| {
6530 state.scroll_bounce(true).default_value(long_text())
6531 });
6532 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
6533 let (end, stretch) = swipe_past_the_end(&view, &mut cx);
6534 assert!(end < px(0.), "the text still scrolls");
6535 assert_eq!(stretch, 0., "the edge does not stretch");
6536 }
6537
6538 #[gpui::test]
6539 fn test_scroll_bounce_follows_the_platform_by_default(cx: &mut TestAppContext) {
6540 let view = InputView::build_textarea(cx, |state| state.default_value(long_text()));
6541 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
6542 let (end, stretch) = swipe_past_the_end(&view, &mut cx);
6543 assert!(end < px(0.), "the text scrolls");
6544 assert_eq!(
6545 stretch != 0.,
6546 crate::ScrollBounce::enabled_by_default(),
6547 "the edge stretches where the platform's scroll views do"
6548 );
6549 }
6550
6551 #[gpui::test]
6552 fn test_scroll_bounce_leaves_text_that_fits(cx: &mut TestAppContext) {
6553 let view = InputView::build_textarea(cx, |state| {
6554 state.scroll_bounce(true).default_value("one line")
6555 });
6556 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
6557 let (end, stretch) = swipe_past_the_end(&view, &mut cx);
6558 assert_eq!((end, stretch), (px(0.), 0.));
6559 }
6560
6561 #[gpui::test]
6562 fn test_scroll_bounce_resets_when_replacement_text_fits(cx: &mut TestAppContext) {
6563 let view = InputView::build_textarea(cx, |state| {
6564 state.scroll_bounce(true).default_value(long_text())
6565 });
6566 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
6567 let (_, stretch) = swipe_past_the_end(&view, &mut cx);
6568 assert!(stretch < 0.);
6569 cx.update(|window, cx| {
6570 view.input
6571 .update(cx, |state, cx| state.set_value("short", window, cx));
6572 window.draw(cx).clear(cx);
6573 });
6574 swipe(&view, &mut cx, 0., gpui::TouchPhase::Ended);
6575 view.input.read_with(&cx, |state, _| {
6576 assert_eq!(state.overscroll.offset(), 0.);
6577 assert_eq!(state.scroll_handle.offset().y, px(0.));
6578 });
6579 }
6580
6581 #[gpui::test]
6582 fn test_scroll_bounce_releases_when_the_end_packet_is_horizontal(cx: &mut TestAppContext) {
6583 let view = InputView::build_textarea(cx, |state| {
6584 state.scroll_bounce(true).default_value(long_text())
6585 });
6586 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
6587 let (_, stretch) = swipe_past_the_end(&view, &mut cx);
6588 assert!(stretch < 0.);
6589 cx.update(|window, cx| {
6590 view.input.update(cx, |state, cx| {
6591 state.on_scroll_wheel(
6592 &ScrollEvent {
6593 delta: gpui::ScrollDelta::Pixels(point(px(1.), px(0.))),
6594 touch_phase: gpui::TouchPhase::Ended,
6595 ..Default::default()
6596 },
6597 window,
6598 cx,
6599 );
6600 });
6601 });
6602 for _ in 0..120 {
6603 cx.executor()
6604 .advance_clock(std::time::Duration::from_millis(16));
6605 cx.update(|window, cx| window.draw(cx).clear(cx));
6606 }
6607 assert_eq!(
6608 view.input
6609 .read_with(&cx, |state, _| state.overscroll.offset()),
6610 0.
6611 );
6612 }
6613
6614 #[gpui::test]
6615 fn test_scroll_bounce_ordinary_mouse_wheel_does_not_stretch(cx: &mut TestAppContext) {
6616 let view = InputView::build_textarea(cx, |state| {
6617 state.scroll_bounce(true).default_value(long_text())
6618 });
6619 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
6620 cx.update(|window, cx| {
6621 window.draw(cx).clear(cx);
6622 view.input.update(cx, |state, cx| {
6623 state.on_scroll_wheel(
6624 &ScrollEvent {
6625 delta: gpui::ScrollDelta::Lines(point(0., -100_000.)),
6626 touch_phase: gpui::TouchPhase::Moved,
6627 ..Default::default()
6628 },
6629 window,
6630 cx,
6631 );
6632 assert!(state.scroll_handle.offset().y < px(0.));
6633 assert_eq!(state.overscroll.offset(), 0.);
6634 });
6635 });
6636 }
6637
6638 #[gpui::test]
6639 fn test_scroll_bounce_new_wheel_input_interrupts_the_return(cx: &mut TestAppContext) {
6640 let view = InputView::build_textarea(cx, |state| {
6641 state.scroll_bounce(true).default_value(long_text())
6642 });
6643 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
6644 let (end, stretch) = swipe_past_the_end(&view, &mut cx);
6645 assert!(stretch < 0.);
6646 swipe(&view, &mut cx, 0., gpui::TouchPhase::Ended);
6647 cx.executor()
6648 .advance_clock(std::time::Duration::from_millis(16));
6649 cx.update(|window, cx| {
6650 window.draw(cx).clear(cx);
6651 view.input.update(cx, |state, cx| {
6652 state.on_scroll_wheel(
6653 &ScrollEvent {
6654 delta: gpui::ScrollDelta::Lines(point(0., 2.)),
6655 touch_phase: gpui::TouchPhase::Moved,
6656 ..Default::default()
6657 },
6658 window,
6659 cx,
6660 );
6661 assert!(state.scroll_handle.offset().y > end);
6662 assert_eq!(state.overscroll.offset(), 0.);
6663 });
6664 });
6665 }
6666
6667 #[gpui::test]
6668 fn test_turning_scroll_bounce_off_drops_the_stretch(cx: &mut TestAppContext) {
6669 let view = InputView::build_textarea(cx, |state| {
6670 state.scroll_bounce(true).default_value(long_text())
6671 });
6672 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
6673 let (_, stretch) = swipe_past_the_end(&view, &mut cx);
6674 assert!(stretch < 0., "the edge stretched: {stretch}");
6675 view.input
6676 .update(&mut cx, |state, cx| state.set_scroll_bounce(false, cx));
6677 assert_eq!(
6678 view.input
6679 .read_with(&cx, |state, _| state.overscroll.offset()),
6680 0.
6681 );
6682 }
6683
6684 #[gpui::test]
6687 fn single_line_is_centered_in_a_taller_frame(cx: &mut TestAppContext) {
6688 struct Frame(Entity<InputBaseState<InputMode>>);
6689 impl Render for Frame {
6690 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
6691 crate::input::InputBase::new("frame")
6692 .h(px(60.))
6693 .child(crate::input::Input::new(&self.0))
6694 }
6695 }
6696
6697 cx.update(crate::init);
6698 let mut input = None;
6699 let window = cx.open_window(size(px(400.), px(100.)), |window, cx| {
6700 let state = cx.new(|cx| crate::input::InputState::new(window, cx).default_value("a"));
6701 input = Some(state.clone());
6702 Frame(state)
6703 });
6704 let input = input.unwrap();
6705 let mut cx = VisualTestContext::from_window(window.into(), cx);
6706 cx.update(|window, cx| window.draw(cx).clear(cx));
6707
6708 input.read_with(&cx, |state, _| {
6709 let line = state.input_bounds;
6710 assert_eq!(line.center().y, px(30.), "line {line:?}");
6711 });
6712 }
6713
6714 #[gpui::test]
6715 fn test_noop_scroll_notifies_diagnostic_dismissal(cx: &mut TestAppContext) {
6716 use std::{cell::Cell, rc::Rc};
6717
6718 cx.update(crate::init);
6719 let mut input = None;
6720 let window = cx.open_window(size(px(400.), px(100.)), |window, cx| {
6721 input = Some(cx.new(|cx| crate::input::EditorState::new(window, cx)));
6722 gpui::EmptyView
6723 });
6724 let input = input.unwrap();
6725 input.update(cx, |state, cx| {
6726 state.input_bounds = Bounds::new(Point::default(), size(px(100.), px(20.)));
6727 state.scroll_size = size(px(100.), px(20.));
6728 state.present_diagnostic(crate::input::DiagnosticEntry::default(), cx);
6729 });
6730 let notifications = Rc::new(Cell::new(0));
6731 let count = notifications.clone();
6732 let _subscription =
6733 cx.update(|cx| cx.observe(&input, move |_, _| count.set(count.get() + 1)));
6734
6735 window
6736 .update(cx, |_, window, cx| {
6737 input.update(cx, |state, cx| {
6738 state.on_scroll_wheel(
6739 &ScrollEvent {
6740 delta: gpui::ScrollDelta::Pixels(point(px(0.), px(-40.))),
6741 ..Default::default()
6742 },
6743 window,
6744 cx,
6745 );
6746 assert_eq!(state.scroll_handle.offset(), Point::default());
6747 assert!(state.diagnostic_popover().is_none());
6748 });
6749 })
6750 .unwrap();
6751 assert_eq!(
6752 notifications.get(),
6753 1,
6754 "clearing a diagnostic must notify even when scrolling is clamped"
6755 );
6756 window
6757 .update(cx, |_, window, cx| {
6758 input.update(cx, |state, cx| {
6759 state.on_scroll_wheel(
6760 &ScrollEvent {
6761 delta: gpui::ScrollDelta::Pixels(point(px(0.), px(-40.))),
6762 ..Default::default()
6763 },
6764 window,
6765 cx,
6766 );
6767 });
6768 })
6769 .unwrap();
6770 assert_eq!(notifications.get(), 1, "an unchanged input must stay quiet");
6771 }
6772
6773 #[gpui::test]
6776 fn test_hover_reaches_bottom_of_scrolled_editor(cx: &mut TestAppContext) {
6777 use crate::input::{Diagnostic, Position};
6778 struct Frame(Entity<crate::input::EditorState>);
6779 impl Render for Frame {
6780 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
6781 crate::input::InputBase::new("frame")
6782 .h(px(100.))
6783 .child(crate::input::Editor::new(&self.0))
6784 }
6785 }
6786
6787 cx.update(crate::init);
6788 let text = (0..100).map(|i| format!("line {i}\n")).collect::<String>();
6789 let mut input = None;
6790 let window = cx.open_window(size(px(400.), px(100.)), |window, cx| {
6791 let state = cx.new(|cx| crate::input::EditorState::new(window, cx).default_value(text));
6792 input = Some(state.clone());
6793 Frame(state)
6794 });
6795 let input = input.unwrap();
6796 let mut cx = VisualTestContext::from_window(window.into(), cx);
6797 input.update(&mut cx, |state, _| {
6798 state.diagnostics_mut().unwrap().push(Diagnostic::new(
6799 Position::new(0, 0)..Position::new(100, 0),
6800 "everywhere",
6801 ));
6802 });
6803 cx.update(|window, cx| window.draw(cx).clear(cx));
6804 cx.update(|window, cx| {
6805 input.update(cx, |state, cx| {
6806 state.on_scroll_wheel(
6807 &ScrollEvent {
6808 delta: gpui::ScrollDelta::Pixels(point(px(0.), px(-400.))),
6809 ..Default::default()
6810 },
6811 window,
6812 cx,
6813 );
6814 })
6815 });
6816 cx.update(|window, cx| window.draw(cx).clear(cx));
6817
6818 let bounds = input.read_with(&cx, |state, _| state.input_bounds);
6819 cx.simulate_mouse_move(
6820 point(bounds.center().x, bounds.bottom() - px(5.)),
6821 None,
6822 gpui::Modifiers::default(),
6823 );
6824 assert!(
6825 input.read_with(&cx, |state, _| state.diagnostic_popover().is_some()),
6826 "no hover at the bottom of a scrolled editor, bounds {bounds:?}"
6827 );
6828 }
6829
6830 #[gpui::test]
6831 fn test_cursor_layout_consumer_updates_after_selection(cx: &mut TestAppContext) {
6832 use std::{cell::Cell, rc::Rc};
6833 struct Panel {
6834 input: Entity<crate::input::EditorState>,
6835 observed: Rc<Cell<Option<(Bounds<Pixels>, Pixels)>>>,
6836 }
6837 impl Render for Panel {
6838 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
6839 self.observed.set(self.input.read(cx).cursor_layout());
6840 div().size_full().child(self.input.clone())
6841 }
6842 }
6843 struct Root(Entity<Panel>);
6844 impl Render for Root {
6845 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
6846 div().size_full().child(
6847 self.0
6848 .clone()
6849 .cached(gpui::StyleRefinement::default().size_full()),
6850 )
6851 }
6852 }
6853 cx.update(crate::init);
6854 let observed = Rc::new(Cell::new(None));
6855 let mut input = None;
6856 let window = cx.open_window(size(px(400.), px(100.)), |window, cx| {
6857 let state = cx.new(|cx| {
6858 let mut state = crate::input::EditorState::new(window, cx);
6859 state.set_value("abcdefgh", window, cx);
6860 state
6861 });
6862 input = Some(state.clone());
6863 Root(cx.new(|_| Panel {
6864 input: state,
6865 observed: observed.clone(),
6866 }))
6867 });
6868 cx.run_until_parked();
6869 let input = input.unwrap();
6870 let before = input.read_with(cx, |state, _| state.cursor_layout());
6871 input.update(cx, |_, cx| cx.notify());
6876 cx.run_until_parked();
6877 assert_eq!(observed.get(), before);
6878 window
6879 .update(cx, |_, _, cx| {
6880 input.update(cx, |state, cx| state.select_to_with_affinity(3, false, cx));
6881 })
6882 .unwrap();
6883 cx.run_until_parked();
6884 window.update(cx, |_, _, cx| cx.notify()).unwrap();
6885 let after = input.read_with(cx, |state, _| state.cursor_layout());
6886 assert_ne!(after, before, "the caret must actually move");
6887 assert_eq!(
6888 observed.get(),
6889 after,
6890 "render consumers must receive the newly painted caret geometry"
6891 );
6892 }
6893
6894 #[gpui::test]
6895 fn test_input_scroll_offset_notifies_only_on_change(cx: &mut TestAppContext) {
6896 use std::{cell::Cell, rc::Rc};
6897
6898 cx.update(crate::init);
6899 let mut input = None;
6900 let _window = cx.open_window(size(px(400.), px(100.)), |window, cx| {
6901 input = Some(cx.new(|cx| crate::input::InputState::new(window, cx)));
6902 gpui::EmptyView
6903 });
6904 let input = input.unwrap();
6905 let notifications = Rc::new(Cell::new(0));
6906 let count = notifications.clone();
6907 let _subscription =
6908 cx.update(|cx| cx.observe(&input, move |_, _| count.set(count.get() + 1)));
6909
6910 input.update(cx, |state, _| {
6911 state.input_bounds = Bounds::new(Point::default(), size(px(100.), px(20.)));
6912 state.scroll_size = size(px(300.), px(20.));
6913 });
6914 for target in [
6915 None,
6916 Some(point(px(0.), px(0.))),
6917 Some(point(px(20.), px(50.))),
6918 ] {
6919 input.update(cx, |state, cx| state.update_scroll_offset(target, cx));
6920 assert_eq!(
6921 notifications.get(),
6922 0,
6923 "an unchanged clamped offset must stay quiet"
6924 );
6925 }
6926 input.update(cx, |state, cx| {
6927 state.update_scroll_offset(Some(point(px(-40.), px(0.))), cx);
6928 assert_eq!(state.scroll_handle.offset(), point(px(-40.), px(0.)));
6929 });
6930 assert_eq!(notifications.get(), 1, "a scroll must notify");
6931 input.update(cx, |state, cx| {
6932 state.update_scroll_offset(Some(point(px(-400.), px(50.))), cx);
6933 assert_eq!(state.scroll_handle.offset(), point(px(-200.), px(0.)));
6934 });
6935 assert_eq!(notifications.get(), 2);
6936 input.update(cx, |state, cx| {
6937 state.update_scroll_offset(Some(point(px(-500.), px(0.))), cx);
6938 });
6939 assert_eq!(
6940 notifications.get(),
6941 2,
6942 "clamping to the current offset must stay quiet"
6943 );
6944 input.update(cx, |state, cx| {
6945 state.scroll_size.width = px(110.);
6946 state.update_scroll_offset(None, cx);
6947 assert_eq!(state.scroll_handle.offset(), point(px(-10.), px(0.)));
6948 });
6949 assert_eq!(
6950 notifications.get(),
6951 3,
6952 "a smaller scroll range must clamp and notify"
6953 );
6954 }
6955
6956 #[gpui::test]
6957 fn test_set_line_number_notifies_only_on_change(cx: &mut TestAppContext) {
6958 use std::{cell::Cell, rc::Rc};
6959
6960 cx.update(crate::init);
6961 let mut input = None;
6962 let window = cx.open_window(size(px(400.), px(100.)), |window, cx| {
6963 input = Some(cx.new(|cx| crate::input::EditorState::new(window, cx)));
6964 gpui::EmptyView
6965 });
6966 let input = input.unwrap();
6967 let shown = input.read_with(cx, |state, _| state.mode.line_number());
6968 let notifications = Rc::new(Cell::new(0));
6969 let count = notifications.clone();
6970 let _subscription =
6971 cx.update(|cx| cx.observe(&input, move |_, _| count.set(count.get() + 1)));
6972
6973 window
6974 .update(cx, |_, window, cx| {
6975 input.update(cx, |state, cx| state.set_line_number(shown, window, cx))
6976 })
6977 .unwrap();
6978 assert_eq!(notifications.get(), 0);
6979
6980 window
6981 .update(cx, |_, window, cx| {
6982 input.update(cx, |state, cx| state.set_line_number(!shown, window, cx))
6983 })
6984 .unwrap();
6985 assert_eq!(notifications.get(), 1);
6986 assert_eq!(
6987 input.read_with(cx, |state, _| state.mode.line_number()),
6988 !shown
6989 );
6990 }
6991
6992 #[gpui::test]
6993 fn test_set_value_on_unfocused_input_stays_quiet(cx: &mut TestAppContext) {
6994 use std::{cell::Cell, rc::Rc};
6995
6996 cx.update(crate::init);
6997 let mut input = None;
6998 let window = cx.open_window(size(px(400.), px(100.)), |window, cx| {
6999 input = Some(cx.new(|cx| crate::input::InputState::new(window, cx)));
7000 gpui::EmptyView
7001 });
7002 let input = input.unwrap();
7003 cx.run_until_parked();
7004
7005 let notifications = Rc::new(Cell::new(0));
7006 let count = notifications.clone();
7007 let _subscription =
7008 cx.update(|cx| cx.observe(&input, move |_, _| count.set(count.get() + 1)));
7009 cx.run_until_parked();
7010
7011 window
7014 .update(cx, |_, window, cx| {
7015 input.update(cx, |state, cx| state.set_value("seeded", window, cx));
7016 })
7017 .unwrap();
7018 cx.run_until_parked();
7019 let settled = notifications.get();
7020
7021 cx.executor()
7022 .advance_clock(std::time::Duration::from_secs(3));
7023 cx.run_until_parked();
7024
7025 assert_eq!(
7026 notifications.get(),
7027 settled,
7028 "an unfocused input is blinking, and every blink repaints the view it is in"
7029 );
7030 }
7031
7032 #[gpui::test]
7033 fn test_input_does_not_invalidate_cached_parent_during_paint(cx: &mut TestAppContext) {
7034 use std::{cell::Cell, rc::Rc};
7035
7036 struct Panel {
7037 input: Entity<crate::input::InputState>,
7038 renders: Rc<Cell<usize>>,
7039 }
7040 impl Render for Panel {
7041 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
7042 self.renders.set(self.renders.get() + 1);
7043 div().size_full().child(self.input.clone())
7044 }
7045 }
7046 struct Root(Entity<Panel>);
7047 impl Render for Root {
7048 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
7049 div().size_full().child(
7050 self.0
7051 .clone()
7052 .cached(gpui::StyleRefinement::default().size_full()),
7053 )
7054 }
7055 }
7056
7057 cx.update(crate::init);
7058 let renders = Rc::new(Cell::new(0));
7059 let mut input = None;
7060 let window = cx.open_window(size(px(400.), px(100.)), |window, cx| {
7061 let state = cx.new(|cx| crate::input::InputState::new(window, cx));
7062 input = Some(state.clone());
7063 Root(cx.new(|_| Panel {
7064 input: state,
7065 renders: renders.clone(),
7066 }))
7067 });
7068 cx.run_until_parked();
7069 let before = renders.get();
7070 assert!(before > 0);
7071 for _ in 0..60 {
7072 window.update(cx, |_, _, cx| cx.notify()).unwrap();
7073 }
7074 assert_eq!(
7075 renders.get(),
7076 before,
7077 "an idle input must not invalidate its cached parent"
7078 );
7079
7080 let input = input.unwrap();
7081 window
7082 .update(cx, |_, window, cx| {
7083 input.update(cx, |state, cx| state.set_value("changed", window, cx));
7084 })
7085 .unwrap();
7086 assert!(
7087 renders.get() > before,
7088 "text changes must invalidate the parent"
7089 );
7090 window.update(cx, |_, _, cx| cx.notify()).unwrap();
7092 let before = renders.get();
7093 for _ in 0..60 {
7094 window.update(cx, |_, _, cx| cx.notify()).unwrap();
7095 }
7096 assert_eq!(renders.get(), before, "the edited input must settle again");
7097
7098 cx.simulate_window_resize(window.into(), size(px(240.), px(100.)));
7099 cx.run_until_parked();
7100 assert!(renders.get() > before, "resizing must invalidate layout");
7101 }
7102
7103 #[gpui::test]
7104 fn textarea_cursor_treats_crlf_as_one_newline(cx: &mut TestAppContext) {
7105 cx.update(crate::init);
7106 let view = InputView::build_textarea(cx, |state| state);
7107 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7108 cx.update(|window, cx| {
7109 view.input.update(cx, |state, cx| {
7110 for original in ["\r\nlast", "\u{feff}first\r\nlast\n", "first\nlast\r\n"] {
7111 state.set_value(original, window, cx);
7112 let newline = original.find('\n').unwrap();
7113 let end = if original.as_bytes()[newline.saturating_sub(1)] == b'\r' {
7114 newline - 1
7115 } else {
7116 newline
7117 };
7118 state.end(&MoveEnd, window, cx);
7119 assert_eq!(state.cursor(), end);
7120 state.right(&MoveRight, window, cx);
7121 assert_eq!(state.cursor(), newline + 1);
7122 state.left(&MoveLeft, window, cx);
7123 assert_eq!(state.cursor(), end);
7124 state.select_to_end_of_line(&SelectToEndOfLine, window, cx);
7125 assert_eq!(state.selected_range(), end..end);
7126 state.move_to_end(&MoveToEnd, window, cx);
7127 assert_eq!(state.cursor(), original.len());
7128 state.move_to_start(&MoveToStart, window, cx);
7129 assert_eq!(state.cursor(), 0);
7130 assert_eq!(state.value().as_bytes(), original.as_bytes());
7131 }
7132 state.set_value("a\rb", window, cx);
7134 state.right(&MoveRight, window, cx);
7135 assert_eq!(state.cursor(), 1);
7136 state.right(&MoveRight, window, cx);
7137 assert_eq!(state.cursor(), 2);
7138 })
7139 });
7140 }
7141
7142 #[gpui::test]
7143 fn only_a_multi_line_input_paints_scrollbars(cx: &mut TestAppContext) {
7144 cx.update(crate::init);
7145
7146 let single = InputView::build(cx, |state| state);
7150 single
7151 .input
7152 .update(cx, |state, _| assert!(!state.shows_scrollbar()));
7153
7154 let multi = InputView::build_textarea(cx, |state| state);
7155 multi
7156 .input
7157 .update(cx, |state, _| assert!(state.shows_scrollbar()));
7158 }
7159
7160 #[gpui::test]
7161 fn context_menu_handler_is_deferred_and_respects_disabled(cx: &mut TestAppContext) {
7162 use std::{cell::Cell, rc::Rc};
7163 cx.update(crate::init);
7164 let input_view = InputView::new(cx);
7165 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7166 let input = input_view.input;
7167 let calls = Rc::new(Cell::new(0usize));
7168 let items = Rc::new(Cell::new(0usize));
7169 let menu_focus = cx.update(|_, cx| cx.focus_handle());
7170
7171 cx.update(|window, cx| {
7172 input.update(cx, |state, cx| {
7173 let calls2 = calls.clone();
7174 let items2 = items.clone();
7175 let menu_focus = menu_focus.clone();
7176 state.on_context_menu(Rc::new(move |menu, _, _, window, cx| {
7177 calls2.set(calls2.get() + 1);
7178 items2.set(menu.items.len());
7179 menu_focus.focus(window, cx);
7180 }));
7181 state.handle_right_click_menu(point(px(0.), px(0.)), 0, window, cx);
7182 })
7183 });
7184 assert_eq!(calls.get(), 1);
7185 assert_eq!(items.get(), 0);
7186 input.read_with(&cx, |state, _| {
7187 assert_eq!(
7188 state
7189 .selection_focus
7190 .as_ref()
7191 .and_then(WeakFocusHandle::upgrade)
7192 .as_ref(),
7193 Some(&menu_focus),
7194 "the menu callback must retain selection without reborrowing its input"
7195 );
7196 });
7197
7198 cx.update(|window, cx| {
7200 input.update(cx, |state, cx| {
7201 state.handle_right_click_menu(point(px(0.), px(0.)), 0, window, cx);
7202 });
7203 });
7204 assert_eq!(calls.get(), 2);
7205 cx.update(|window, cx| {
7206 assert!(input.read(cx).has_selection_focus(window, cx));
7207 });
7208
7209 cx.update(|window, cx| {
7210 input.update(cx, |state, cx| {
7211 state.disabled = true;
7212 state.handle_right_click_menu(point(px(0.), px(0.)), 0, window, cx);
7213 })
7214 });
7215 assert_eq!(calls.get(), 2);
7216 }
7217
7218 #[gpui::test]
7219 fn context_menu_handler_can_read_its_input(cx: &mut TestAppContext) {
7220 use std::{cell::Cell, rc::Rc};
7221 cx.update(crate::init);
7222 let input_view = InputView::new(cx);
7223 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7224 let input = input_view.input;
7225 let cursor = Rc::new(Cell::new(None));
7226
7227 cx.update(|window, cx| {
7228 input.update(cx, |state, cx| {
7229 state.set_value("hello", window, cx);
7230 let weak = cx.weak_entity();
7231 let cursor = cursor.clone();
7232 state.on_context_menu(Rc::new(move |_, _, _, _, cx| {
7233 cursor.set(weak.upgrade().map(|input| input.read(cx).cursor()));
7234 }));
7235 state.handle_right_click_menu(point(px(0.), px(0.)), 2, window, cx);
7236 })
7237 });
7238 assert_eq!(cursor.get(), Some(2));
7239 }
7240
7241 #[gpui::test]
7242 fn test_readonly_rejects_user_edits_only(cx: &mut TestAppContext) {
7243 let input_view = InputView::new(cx);
7244 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7245 let input = input_view.input;
7246
7247 cx.update(|window, cx| {
7248 input.update(cx, |state, cx| {
7249 state.set_value("hello", window, cx);
7250 state.set_readonly(true, cx);
7251 });
7252 });
7253
7254 cx.update(|_, cx| {
7255 input.read_with(cx, |state, _| {
7256 assert!(!state.is_editable());
7257 assert!(!state.is_replaceable());
7258 });
7259 });
7260
7261 cx.update(|window, cx| {
7263 input.update(cx, |state, cx| {
7264 state.replace_text_in_range(None, " world", window, cx);
7265 state.replace_and_mark_text_in_range(None, "あ", None, window, cx);
7266 });
7267 });
7268 cx.update(|_, cx| {
7269 input.read_with(cx, |state, _| assert_eq!(state.value(), "hello"));
7270 });
7271
7272 cx.update(|window, cx| {
7274 input.update(cx, |state, cx| {
7275 state.insert(" world", window, cx);
7276 state.set_value("changed", window, cx);
7277 });
7278 });
7279 cx.update(|_, cx| {
7280 input.read_with(cx, |state, _| assert_eq!(state.value(), "changed"));
7281 });
7282
7283 cx.update(|window, cx| {
7286 input.update(cx, |state, cx| {
7287 state.set_readonly(false, cx);
7288 state.replace_text_in_range(None, "!", window, cx);
7289 });
7290 });
7291 cx.update(|_, cx| {
7292 input.read_with(cx, |state, _| {
7293 assert!(state.is_editable());
7294 assert_eq!(state.value(), "!changed");
7295 });
7296 });
7297 }
7298
7299 #[gpui::test]
7304 fn test_edit_reveals_far_offscreen_caret(cx: &mut TestAppContext) {
7305 let mut input = None;
7306 let window = cx.open_window(size(px(720.), px(400.)), |window, cx| {
7307 cx.set_global(Theme::default());
7308 super::super::init(cx);
7309 let state = cx.new(|cx| crate::input::TextareaState::new(window, cx).auto_grow(1, 6));
7310 input = Some(state.clone());
7311 TestRoot(state)
7312 });
7313 let mut cx = VisualTestContext::from_window(window.into(), cx);
7314 let input = input.unwrap();
7315
7316 let text: String = (1..=100)
7317 .map(|i| format!("line {i}"))
7318 .collect::<Vec<_>>()
7319 .join("\n");
7320 cx.update(|window, cx| {
7321 input.update(cx, |state, cx| {
7322 state.set_value(text, window, cx);
7323 let end = state.text.len();
7324 state.set_selection(end, end);
7325 });
7326 });
7327 cx.run_until_parked();
7328
7329 cx.update(|_, cx| {
7331 input.update(cx, |state, cx| {
7332 state.scroll_handle.set_offset(point(px(0.), px(0.)));
7333 cx.notify();
7334 });
7335 });
7336 cx.run_until_parked();
7337
7338 cx.update(|window, cx| {
7339 input.update(cx, |state, cx| {
7340 state.replace_text_in_range(None, "X", window, cx);
7341 });
7342 });
7343 cx.run_until_parked();
7344
7345 cx.update(|_, cx| {
7346 input.read_with(cx, |state, _| {
7347 assert!(state.value().ends_with("line 100X"));
7348 let (caret, _) = state.cursor_layout().expect("caret painted");
7349 let viewport = state.input_bounds();
7350 let scroll_y = state.scroll_handle.offset().y;
7351 let top = caret.origin.y - viewport.origin.y + scroll_y;
7352 assert!(
7353 top >= px(0.) && top + caret.size.height <= viewport.size.height,
7354 "caret top {top:?} outside viewport height {:?} (scroll {scroll_y:?})",
7355 viewport.size.height,
7356 );
7357 });
7358 });
7359 }
7360
7361 #[gpui::test]
7365 fn test_set_auto_grow_keeps_rows_fitted(cx: &mut TestAppContext) {
7366 let mut input = None;
7367 let window = cx.open_window(size(px(720.), px(400.)), |window, cx| {
7368 cx.set_global(Theme::default());
7369 super::super::init(cx);
7370 let state = cx.new(|cx| crate::input::TextareaState::new(window, cx).auto_grow(1, 10));
7371 input = Some(state.clone());
7372 TestRoot(state)
7373 });
7374 let mut cx = VisualTestContext::from_window(window.into(), cx);
7375 let input = input.unwrap();
7376
7377 let text: String = (1..=8)
7378 .map(|i| format!("line {i}"))
7379 .collect::<Vec<_>>()
7380 .join("\n");
7381 cx.update(|window, cx| {
7382 input.update(cx, |state, cx| state.set_value(text, window, cx));
7383 });
7384 cx.run_until_parked();
7385 cx.update(|_, cx| input.read_with(cx, |state, _| assert_eq!(state.mode.rows(), 8)));
7386
7387 cx.update(|_, cx| input.update(cx, |state, cx| state.set_auto_grow(1, 12, cx)));
7389 cx.run_until_parked();
7390 cx.update(|_, cx| input.read_with(cx, |state, _| assert_eq!(state.mode.rows(), 8)));
7391
7392 cx.update(|_, cx| input.update(cx, |state, cx| state.set_auto_grow(1, 4, cx)));
7394 cx.run_until_parked();
7395 cx.update(|_, cx| input.read_with(cx, |state, _| assert_eq!(state.mode.rows(), 4)));
7396 }
7397
7398 #[gpui::test]
7404 fn test_scroll_to_eob_does_not_overshoot_safe_range(cx: &mut TestAppContext) {
7405 let input_view = InputView::new(cx);
7406 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7407 let input = input_view.input;
7408
7409 cx.update(|window, cx| {
7411 input.update(cx, |state, cx| {
7412 state.set_scroll_beyond_last_line(Some(1), window, cx);
7413 state.set_cursor_surrounding_lines(Some(1), window, cx);
7414 let text: String = (1..=50)
7415 .map(|i| format!("line {i}"))
7416 .collect::<Vec<_>>()
7417 .join("\n");
7418 state.set_value(text, window, cx);
7419 });
7420 });
7421 cx.run_until_parked();
7422
7423 cx.update(|_, cx| {
7426 input.read_with(cx, |state, _| {
7427 assert!(
7428 state.scroll_size.height > px(0.),
7429 "scroll_size not populated by initial paint"
7430 );
7431 assert!(
7432 state.input_bounds.size.height > px(0.),
7433 "input_bounds not populated by initial paint"
7434 );
7435 });
7436 });
7437
7438 cx.update(|_, cx| {
7443 input.update(cx, |state, cx| {
7444 let end = state.text.len();
7445 state.move_to(end, Some(MoveDirection::Down), cx);
7446
7447 let deferred = state
7448 .deferred_scroll_offset
7449 .expect("scroll_to should populate deferred_scroll_offset");
7450 let safe_y_min =
7451 (-state.scroll_size.height + state.input_bounds.size.height).min(px(0.));
7452
7453 assert!(
7454 deferred.y >= safe_y_min,
7455 "deferred_scroll_offset.y = {:?} below safe_y_min = {:?} \
7456 — paint would jitter (Bug C regression)",
7457 deferred.y,
7458 safe_y_min,
7459 );
7460 });
7461 });
7462 }
7463
7464 #[gpui::test]
7465 fn test_next_search_match_reveals_with_padding_after_manual_scroll(cx: &mut TestAppContext) {
7466 assert_search_reveals_with_padding_after_manual_scroll(false, cx);
7467 }
7468
7469 #[gpui::test]
7470 fn test_previous_search_match_reveals_with_padding_after_manual_scroll(
7471 cx: &mut TestAppContext,
7472 ) {
7473 assert_search_reveals_with_padding_after_manual_scroll(true, cx);
7474 }
7475
7476 fn assert_search_reveals_with_padding_after_manual_scroll(
7477 previous: bool,
7478 cx: &mut TestAppContext,
7479 ) {
7480 let input_view = InputView::new(cx);
7481 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7482 let input = input_view.input;
7483 let text = (0..160)
7484 .map(|row| {
7485 if matches!(row, 20 | 60 | 100) {
7486 format!("match on row {row}")
7487 } else {
7488 format!("line {row}")
7489 }
7490 })
7491 .collect::<Vec<_>>()
7492 .join("\n");
7493 let start = text.match_indices("match").nth(1).unwrap().0;
7494 let expected_match = start..start + "match".len();
7495 cx.update(|window, cx| {
7496 input.update(cx, |state, cx| {
7497 state.set_cursor_surrounding_lines(Some(3), window, cx);
7498 state.set_value(text, window, cx);
7499 state.set_search_query("match", true, cx);
7500 if previous {
7501 state.search_session.matcher.next();
7502 state.search_session.matcher.next();
7503 }
7504 });
7505 });
7506 cx.run_until_parked();
7507 cx.update(|_, cx| {
7508 input.update(cx, |state, cx| {
7509 let line_height = state.last_layout.as_ref().unwrap().line_height;
7510 let y = if previous { px(0.) } else { -line_height * 80. };
7511 state.set_scroll_offset(point(px(0.), y), cx);
7512 });
7513 });
7514 cx.run_until_parked();
7515 input.read_with(&cx, |state, _| {
7516 let visible = state.visible_row_range().unwrap();
7517 if previous {
7518 assert!(visible.end <= 60, "target must be below the viewport");
7519 } else {
7520 assert!(visible.start > 60, "target must be above the viewport");
7521 }
7522 });
7523 cx.update(|_, cx| {
7524 input.update(cx, |state, cx| {
7525 let range = if previous {
7526 state.previous_search_match(cx)
7527 } else {
7528 state.next_search_match(cx)
7529 };
7530 assert_eq!(range, Some(expected_match));
7531 assert_eq!(state.search_session.matcher.label(), "2/3");
7532 });
7533 });
7534 cx.run_until_parked();
7535 input.read_with(&cx, |state, _| {
7536 assert!(state.visible_row_range().unwrap().contains(&60));
7537 let line_height = state.last_layout.as_ref().unwrap().line_height;
7538 let target_y = line_height * 60. + state.scroll_handle.offset().y;
7539 assert!(target_y >= line_height * 2. - px(0.1));
7541 assert!(
7542 target_y + line_height * 3.
7543 <= state.last_bounds.as_ref().unwrap().size.height + px(0.1),
7544 "search must preserve the configured surrounding-line padding"
7545 );
7546 });
7547 }
7548
7549 struct SearchHost {
7551 editor: Entity<InputBaseState<EditorMode>>,
7552 search_requests: Rc<Cell<usize>>,
7553 }
7554
7555 impl Render for SearchHost {
7556 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
7557 let search_requests = self.search_requests.clone();
7558 div()
7559 .size_full()
7560 .on_action(cx.listener(move |_, _: &Search, _, _| {
7561 search_requests.set(search_requests.get() + 1);
7562 }))
7563 .child(self.editor.clone())
7564 }
7565 }
7566
7567 fn press_search_shortcut(
7570 cx: &mut TestAppContext,
7571 searchable: bool,
7572 ) -> (Entity<InputBaseState<EditorMode>>, Rc<Cell<usize>>) {
7573 let search_requests = Rc::new(Cell::new(0));
7574 let mut editor = None;
7575 let window = cx.update(|cx| {
7576 cx.open_window(Default::default(), |window, cx| {
7577 cx.set_global(Theme::default());
7578 super::super::init(cx);
7579 let state =
7580 cx.new(|cx| crate::input::EditorState::new(window, cx).searchable(searchable));
7581 editor = Some(state.clone());
7582 cx.new(|_| SearchHost {
7583 editor: state,
7584 search_requests: search_requests.clone(),
7585 })
7586 })
7587 .unwrap()
7588 });
7589 let editor = editor.unwrap();
7590 let mut cx = VisualTestContext::from_window(window.into(), cx);
7591 cx.update(|window, cx| {
7592 editor.update(cx, |state, cx| state.focus(window, cx));
7593 });
7594 cx.run_until_parked();
7595 #[cfg(target_os = "macos")]
7596 cx.simulate_keystrokes("cmd-f");
7597 #[cfg(not(target_os = "macos"))]
7598 cx.simulate_keystrokes("ctrl-f");
7599 cx.run_until_parked();
7600 (editor, search_requests)
7601 }
7602
7603 #[gpui::test]
7604 fn test_search_shortcut_reaches_the_host_when_not_searchable(cx: &mut TestAppContext) {
7605 let (editor, search_requests) = press_search_shortcut(cx, false);
7606 assert_eq!(search_requests.get(), 1);
7607 editor.read_with(cx, |state, _| {
7608 assert!(!state.search_session().open);
7609 assert!(!state.search_session().is_active());
7610 });
7611 }
7612
7613 #[gpui::test]
7614 fn test_search_shortcut_opens_the_panel_when_searchable(cx: &mut TestAppContext) {
7615 let (editor, search_requests) = press_search_shortcut(cx, true);
7616 assert_eq!(search_requests.get(), 0);
7617 editor.read_with(cx, |state, _| {
7618 assert!(state.search_session().open);
7619 assert!(state.search_session().is_active());
7620 });
7621 }
7622
7623 #[gpui::test]
7624 fn test_set_search_query_highlights_without_the_panel(cx: &mut TestAppContext) {
7625 let input_view = InputView::build_editor(cx, |state| state.searchable(false));
7626 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7627 let input = input_view.input;
7628 cx.update(|window, cx| {
7629 input.update(cx, |state, cx| {
7630 state.set_value("foo bar foo", window, cx);
7631 state.set_search_query("foo", true, cx);
7632 });
7633 });
7634 cx.run_until_parked();
7635 input.read_with(&cx, |state, _| {
7636 let session = state.search_session();
7637 assert!(session.is_active());
7638 assert!(!session.open);
7639 assert_eq!(session.matcher.len(), 2);
7640 });
7641 cx.update(|_, cx| {
7642 input.update(cx, |state, cx| state.close_search(cx));
7643 });
7644 input.read_with(&cx, |state, _| {
7645 assert!(!state.search_session().is_active());
7646 });
7647 }
7648
7649 #[gpui::test]
7652 fn test_closed_search_resyncs_matches_after_edits(cx: &mut TestAppContext) {
7653 let input_view = InputView::build_editor(cx, |state| state.searchable(false));
7654 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7655 let input = input_view.input;
7656 cx.update(|window, cx| {
7657 input.update(cx, |state, cx| {
7658 state.set_value("foo bar foo", window, cx);
7659 state.set_search_query("foo", true, cx);
7660 state.close_search(cx);
7661
7662 state.set_value("bar foo", window, cx);
7663 assert_eq!(state.next_search_match(cx), Some(4..7));
7664
7665 state.set_value("foo foo foo", window, cx);
7666 state.set_search_query("foo", true, cx);
7667 assert_eq!(state.search_session().matcher.len(), 3);
7668 });
7669 });
7670 }
7671
7672 #[gpui::test]
7673 fn test_search_reveals_offscreen_wrapped_match(cx: &mut TestAppContext) {
7674 let input_view = InputView::new(cx);
7675 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7676 let input = input_view.input;
7677 let text = format!(
7678 "match\n{}\n{}match\n{}",
7679 "line\n".repeat(80),
7680 "wrapped text ".repeat(500),
7681 "line\n".repeat(80)
7682 );
7683 cx.update(|window, cx| {
7684 input.update(cx, |state, cx| {
7685 state.set_cursor_surrounding_lines(Some(3), window, cx);
7686 state.set_value(text, window, cx);
7687 state.set_search_query("match", true, cx);
7688 });
7689 });
7690 cx.run_until_parked();
7691 cx.update(|_, cx| {
7692 input.update(cx, |state, cx| {
7693 state.next_search_match(cx).unwrap();
7694 });
7695 });
7696 cx.run_until_parked();
7697 input.read_with(&cx, |state, _| {
7698 let range = state.search_session.matcher.matched_ranges()[1].clone();
7699 let layout = state.last_layout.as_ref().unwrap();
7700 let (_, _, position) = state.line_and_position_for_offset(range.end);
7701 let y = position.expect("wrapped match must be laid out").y
7702 + state.scroll_handle.offset().y;
7703 assert!(y >= layout.line_height * 2. - px(0.1));
7704 assert!(
7705 y + layout.line_height * 3. <= state.last_bounds.unwrap().size.height + px(0.1),
7706 "wrapped match must retain surrounding display rows"
7707 );
7708 });
7709 }
7710
7711 #[gpui::test]
7712 fn test_number_step(cx: &mut TestAppContext) {
7713 let input = InputView::build(cx, |state| state).input;
7714
7715 cx.update(|cx| {
7716 input.update(cx, |_state, cx| {
7717 assert_eq!(
7718 NumberStep::from(5.).value(123., StepAction::Increment, cx),
7719 5.
7720 );
7721
7722 let step = NumberStep::by_value(|value, action, _cx| {
7725 let below = match action {
7726 StepAction::Increment => value < 1.0,
7727 StepAction::Decrement => value <= 1.0,
7728 };
7729 if below { 0.1 } else { 0.5 }
7730 });
7731 assert_eq!(step.value(0.5, StepAction::Increment, cx), 0.1);
7732 assert_eq!(step.value(1.0, StepAction::Increment, cx), 0.5);
7733 assert_eq!(step.value(1.0, StepAction::Decrement, cx), 0.1);
7734 assert_eq!(step.value(2.0, StepAction::Decrement, cx), 0.5);
7735 });
7736 });
7737 }
7738
7739 #[gpui::test]
7740 fn test_number_input_normalization(cx: &mut TestAppContext) {
7741 let input_view = InputView::build(cx, |state| {
7742 state.mask_pattern(MaskPattern::Number {
7743 separator: None,
7744 fraction: None,
7745 })
7746 });
7747 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7748 let input = input_view.input;
7749
7750 cx.update(|window, cx| {
7754 input.update(cx, |state, cx| {
7755 state.replace_text_in_range(None, "12。5", window, cx);
7756 });
7757 });
7758 cx.run_until_parked();
7759 cx.update(|_, cx| {
7760 input.read_with(cx, |state, _| {
7761 assert_eq!(state.value(), "12.5");
7762 let cursor: Range<usize> = state.selected_range();
7763 assert_eq!(cursor, 4..4);
7764 });
7765 });
7766
7767 cx.update(|window, cx| {
7769 input.update(cx, |state, cx| {
7770 state.replace_text_in_range(None, "abc", window, cx);
7771 });
7772 });
7773 cx.run_until_parked();
7774 cx.update(|_, cx| {
7775 input.read_with(cx, |state, _| {
7776 assert_eq!(state.value(), "12.5");
7777 });
7778 });
7779
7780 cx.update(|window, cx| {
7783 input.update(cx, |state, cx| {
7784 let range = state.range_to_utf16(&(0..state.text.len()));
7785 state.replace_text_in_range(Some(range), "。", window, cx);
7786 });
7787 });
7788 cx.run_until_parked();
7789 cx.update(|_, cx| {
7790 input.read_with(cx, |state, _| {
7791 assert_eq!(state.value(), ".");
7792 let cursor: Range<usize> = state.selected_range();
7793 assert_eq!(cursor, 1..1);
7794 });
7795 });
7796 }
7797
7798 #[gpui::test]
7799 fn test_mask_pattern_accepts_non_ascii_text(cx: &mut TestAppContext) {
7800 let input_view = InputView::build(cx, |state| state.mask_pattern("9999年99月"));
7801 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7802 let input = input_view.input;
7803
7804 cx.update(|window, cx| {
7805 input.update(cx, |state, cx| {
7806 state.replace_text_in_range(None, "2024年12月", window, cx);
7807 });
7808 });
7809 cx.run_until_parked();
7810 cx.update(|_, cx| {
7811 input.read_with(cx, |state, _| {
7812 assert_eq!(state.value(), "2024年12月");
7813 });
7814 });
7815 }
7816
7817 #[gpui::test]
7818 fn test_number_input_normalization_with_separator(cx: &mut TestAppContext) {
7819 let input_view = InputView::build(cx, |state| {
7820 state.mask_pattern(MaskPattern::Number {
7821 separator: Some(','),
7822 fraction: Some(2),
7823 })
7824 });
7825 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7826 let input = input_view.input;
7827
7828 cx.update(|window, cx| {
7829 input.update(cx, |state, cx| {
7830 state.replace_text_in_range(None, "1234", window, cx);
7831 });
7832 });
7833 cx.run_until_parked();
7834 cx.update(|_, cx| {
7835 input.read_with(cx, |state, _| {
7836 assert_eq!(state.value(), "1,234");
7837 assert_eq!(state.unmask_value(), "1234");
7838 });
7839 });
7840 }
7841
7842 #[gpui::test]
7843 fn test_number_input_clamp_on_blur(cx: &mut TestAppContext) {
7844 let input_view = InputView::build(cx, |state| {
7845 state
7846 .mask_pattern(MaskPattern::Number {
7847 separator: None,
7848 fraction: None,
7849 })
7850 .min(10.)
7851 .max(100.)
7852 });
7853 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7854 let input = input_view.input;
7855
7856 cx.update(|window, cx| {
7858 input.update(cx, |state, cx| {
7859 state.replace_text_in_range(None, "1000", window, cx);
7860 assert_eq!(state.value(), "1000");
7861 state.clamp_number_value(window, cx);
7862 assert_eq!(state.value(), "100");
7863
7864 let range = state.range_to_utf16(&(0..state.text.len()));
7865 state.replace_text_in_range(Some(range), "1", window, cx);
7866 assert_eq!(state.value(), "1");
7867 state.clamp_number_value(window, cx);
7868 assert_eq!(state.value(), "10");
7869 });
7870 });
7871 }
7872
7873 #[gpui::test]
7874 fn test_number_input_undo_with_mask(cx: &mut TestAppContext) {
7875 let input_view = InputView::build(cx, |state| {
7876 state.mask_pattern(MaskPattern::Number {
7877 separator: Some(','),
7878 fraction: None,
7879 })
7880 });
7881 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7882 let input = input_view.input;
7883
7884 cx.update(|window, cx| {
7887 input.update(cx, |state, cx| {
7888 state.replace_text_in_range(None, "1234", window, cx);
7889 assert_eq!(state.value(), "1,234");
7890 state.replace_text_in_range(None, "5", window, cx);
7891 assert_eq!(state.value(), "12,345");
7892
7893 state.undo(&Undo, window, cx);
7897 assert_eq!(state.value(), "1,234");
7898 state.undo(&Undo, window, cx);
7899 assert_eq!(state.value(), "");
7900 state.redo(&Redo, window, cx);
7901 assert_eq!(state.value(), "1,234");
7902 state.redo(&Redo, window, cx);
7903 assert_eq!(state.value(), "12,345");
7904 });
7905 });
7906 }
7907
7908 #[gpui::test]
7909 fn test_undo_manager_coalesces_adjacent_typing_transactions(cx: &mut TestAppContext) {
7910 let input_view = InputView::build(cx, |state| state);
7911 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7912 let input = input_view.input;
7913
7914 cx.update(|window, cx| {
7915 input.update(cx, |state, cx| {
7916 state.replace_text_in_range(None, "a", window, cx);
7917 state.replace_text_in_range(None, "b", window, cx);
7918 assert_eq!(state.value(), "ab");
7919
7920 state.undo(&Undo, window, cx);
7921 assert_eq!(state.value(), "");
7922 });
7923 });
7924 }
7925
7926 #[gpui::test]
7927 fn test_undo_manager_cursor_movement_splits_typing(cx: &mut TestAppContext) {
7928 let input_view = InputView::build(cx, |state| state);
7929 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7930 let input = input_view.input;
7931
7932 cx.update(|window, cx| {
7933 input.update(cx, |state, cx| {
7934 state.replace_text_in_range(None, "a", window, cx);
7935 state.replace_text_in_range(None, "b", window, cx);
7936 state.left(&MoveLeft, window, cx);
7937 state.replace_text_in_range(None, "x", window, cx);
7938 assert_eq!(state.value(), "axb");
7939
7940 state.undo(&Undo, window, cx);
7941 assert_eq!(state.value(), "ab");
7942 state.undo(&Undo, window, cx);
7943 assert_eq!(state.value(), "");
7944 });
7945 });
7946 }
7947
7948 #[gpui::test]
7949 fn test_undo_manager_splits_backward_and_forward_delete(cx: &mut TestAppContext) {
7950 let input_view = InputView::build(cx, |state| state);
7951 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7952 let input = input_view.input;
7953
7954 cx.update(|window, cx| {
7955 input.update(cx, |state, cx| {
7956 state.set_value("abcd", window, cx);
7957 state.set_selected_range(2..2, cx);
7958 state.backspace(&Backspace, window, cx);
7959 state.delete(&Delete, window, cx);
7960 assert_eq!(state.value(), "ad");
7961
7962 state.undo(&Undo, window, cx);
7963 assert_eq!(state.value(), "acd");
7964 state.undo(&Undo, window, cx);
7965 assert_eq!(state.value(), "abcd");
7966 });
7967 });
7968 }
7969
7970 #[gpui::test]
7971 fn test_undo_manager_coalesces_directional_character_deletes(cx: &mut TestAppContext) {
7972 let input_view = InputView::build(cx, |state| state);
7973 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7974 let input = input_view.input;
7975
7976 cx.update(|window, cx| {
7977 input.update(cx, |state, cx| {
7978 state.set_value("abcd", window, cx);
7979 state.backspace(&Backspace, window, cx);
7980 state.backspace(&Backspace, window, cx);
7981 assert_eq!(state.value(), "ab");
7982 state.undo(&Undo, window, cx);
7983 assert_eq!(state.value(), "abcd");
7984 assert_eq!(state.selected_range(), 4..4);
7985
7986 state.set_value("abcd", window, cx);
7987 state.set_selected_range(1..1, cx);
7988 state.delete(&Delete, window, cx);
7989 state.delete(&Delete, window, cx);
7990 assert_eq!(state.value(), "ad");
7991 state.undo(&Undo, window, cx);
7992 assert_eq!(state.value(), "abcd");
7993 assert_eq!(state.selected_range(), 1..1);
7994 });
7995 });
7996 }
7997
7998 #[gpui::test]
7999 fn test_undo_manager_atomic_paste_isolated_from_typing(cx: &mut TestAppContext) {
8000 let input_view = InputView::build(cx, |state| state);
8001 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8002 let input = input_view.input;
8003
8004 cx.update(|window, cx| {
8005 cx.write_to_clipboard(ClipboardItem::new_string("P".to_string()));
8006 input.update(cx, |state, cx| {
8007 state.replace_text_in_range(None, "a", window, cx);
8008 state.paste(&Paste, window, cx);
8009 state.replace_text_in_range(None, "b", window, cx);
8010 assert_eq!(state.value(), "aPb");
8011
8012 state.undo(&Undo, window, cx);
8013 assert_eq!(state.value(), "aP");
8014 state.undo(&Undo, window, cx);
8015 assert_eq!(state.value(), "a");
8016 state.undo(&Undo, window, cx);
8017 assert_eq!(state.value(), "");
8018 });
8019 });
8020 }
8021
8022 #[gpui::test]
8023 fn test_undo_manager_programmatic_insert_is_atomic(cx: &mut TestAppContext) {
8024 let input_view = InputView::build(cx, |state| state);
8025 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8026 let input = input_view.input;
8027
8028 cx.update(|window, cx| {
8029 input.update(cx, |state, cx| {
8030 state.replace_text_in_range(None, "a", window, cx);
8031 state.insert("P", window, cx);
8032 state.replace_text_in_range(None, "b", window, cx);
8033 assert_eq!(state.value(), "aPb");
8034
8035 state.undo(&Undo, window, cx);
8036 assert_eq!(state.value(), "aP");
8037 state.undo(&Undo, window, cx);
8038 assert_eq!(state.value(), "a");
8039 });
8040 });
8041 }
8042
8043 #[gpui::test]
8044 fn test_undo_manager_selection_round_trip_splits_typing(cx: &mut TestAppContext) {
8045 let input_view = InputView::build(cx, |state| state);
8046 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8047 let input = input_view.input;
8048
8049 cx.update(|window, cx| {
8050 input.update(cx, |state, cx| {
8051 state.replace_text_in_range(None, "a", window, cx);
8052 state.replace_text_in_range(None, "b", window, cx);
8053 state.select_all(window, cx);
8054 state.unselect(window, cx);
8055 state.replace_text_in_range(None, "c", window, cx);
8056
8057 state.undo(&Undo, window, cx);
8058 assert_eq!(state.value(), "ab");
8059 state.undo(&Undo, window, cx);
8060 assert_eq!(state.value(), "");
8061 });
8062 });
8063 }
8064
8065 #[gpui::test]
8066 fn test_undo_manager_enter_is_atomic(cx: &mut TestAppContext) {
8067 let input_view = InputView::build_textarea(cx, |state| state);
8068 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8069 let input = input_view.input;
8070
8071 cx.update(|window, cx| {
8072 input.update(cx, |state, cx| {
8073 state.replace_text_in_range(None, "a", window, cx);
8074 state.enter(
8075 &Enter {
8076 secondary: false,
8077 shift: false,
8078 },
8079 window,
8080 cx,
8081 );
8082 state.replace_text_in_range(None, "b", window, cx);
8083
8084 state.undo(&Undo, window, cx);
8085 assert_eq!(state.value(), "a\n");
8086 state.undo(&Undo, window, cx);
8087 assert_eq!(state.value(), "a");
8088 state.undo(&Undo, window, cx);
8089 assert_eq!(state.value(), "");
8090 });
8091 });
8092 }
8093
8094 #[gpui::test]
8095 fn test_undo_manager_single_line_return_commits_the_typing_session(cx: &mut TestAppContext) {
8096 let input_view = InputView::build(cx, |state| state);
8097 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8098 let input = input_view.input;
8099
8100 cx.update(|window, cx| {
8101 input.update(cx, |state, cx| {
8102 for part in ["a", "b", "c"] {
8103 state.replace_text_in_range(None, part, window, cx);
8104 }
8105 state.enter(
8106 &Enter {
8107 secondary: false,
8108 shift: false,
8109 },
8110 window,
8111 cx,
8112 );
8113 for part in ["d", "e", "f"] {
8114 state.replace_text_in_range(None, part, window, cx);
8115 }
8116 assert_eq!(state.value(), "abcdef");
8117
8118 state.undo(&Undo, window, cx);
8119 assert_eq!(state.value(), "abc");
8120 state.undo(&Undo, window, cx);
8121 assert_eq!(state.value(), "");
8122 });
8123 });
8124 }
8125
8126 #[gpui::test]
8127 fn test_undo_manager_submit_on_enter_commits_the_textarea_session(cx: &mut TestAppContext) {
8128 let input_view = InputView::build_textarea(cx, |state| state.submit_on_enter(true));
8129 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8130 let input = input_view.input;
8131
8132 cx.update(|window, cx| {
8133 input.update(cx, |state, cx| {
8134 state.replace_text_in_range(None, "before submit", window, cx);
8135 state.enter(
8136 &Enter {
8137 secondary: false,
8138 shift: false,
8139 },
8140 window,
8141 cx,
8142 );
8143 state.replace_text_in_range(None, " after submit", window, cx);
8144 assert_eq!(state.value(), "before submit after submit");
8145
8146 state.undo(&Undo, window, cx);
8147 assert_eq!(state.value(), "before submit");
8148 state.undo(&Undo, window, cx);
8149 assert_eq!(state.value(), "");
8150 });
8151 });
8152 }
8153
8154 #[gpui::test]
8155 fn test_undo_manager_blur_commits_the_typing_session(cx: &mut TestAppContext) {
8156 let input_view = InputView::build(cx, |state| state);
8157 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8158 let input = input_view.input;
8159
8160 cx.update(|window, cx| {
8161 input.update(cx, |state, cx| {
8162 state.replace_text_in_range(None, "before blur", window, cx);
8163 state.on_blur(window, cx);
8164 state.on_focus(window, cx);
8165 state.replace_text_in_range(None, " after focus", window, cx);
8166 assert_eq!(state.value(), "before blur after focus");
8167
8168 state.undo(&Undo, window, cx);
8169 assert_eq!(state.value(), "before blur");
8170 state.undo(&Undo, window, cx);
8171 assert_eq!(state.value(), "");
8172 });
8173 });
8174 }
8175
8176 #[gpui::test]
8177 fn test_undo_manager_keeps_rapid_lines_in_distinct_transactions(cx: &mut TestAppContext) {
8178 let input_view = InputView::build_textarea(cx, |state| state);
8179 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8180 let input = input_view.input;
8181
8182 cx.update(|window, cx| {
8183 input.update(cx, |state, cx| {
8184 let enter = Enter {
8185 secondary: false,
8186 shift: false,
8187 };
8188 state.replace_text_in_range(None, "a", window, cx);
8189 state.enter(&enter, window, cx);
8190 state.replace_text_in_range(None, "b", window, cx);
8191 state.enter(&enter, window, cx);
8192 state.replace_text_in_range(None, "c", window, cx);
8193 assert_eq!(state.value(), "a\nb\nc");
8194
8195 for expected in ["a\nb\n", "a\nb", "a\n", "a", ""] {
8196 state.undo(&Undo, window, cx);
8197 assert_eq!(state.value(), expected);
8198 }
8199 });
8200 });
8201 }
8202
8203 #[gpui::test]
8204 fn test_undo_manager_coalesces_long_unicode_typing_without_a_timer(cx: &mut TestAppContext) {
8205 let input_view = InputView::build_textarea(cx, |state| state);
8206 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8207 let input = input_view.input;
8208 let parts = [
8209 "The ",
8210 "quick ",
8211 "brown fox, ",
8212 "你好,世界 ",
8213 "🦀 jumps over 13 lazy dogs.",
8214 ];
8215 let expected = parts.concat();
8216
8217 cx.update(|window, cx| {
8218 input.update(cx, |state, cx| {
8219 for part in parts {
8220 state.replace_text_in_range(None, part, window, cx);
8221 }
8222 assert_eq!(state.value(), expected);
8223
8224 state.undo(&Undo, window, cx);
8225 assert_eq!(state.value(), "");
8226 state.redo(&Redo, window, cx);
8227 assert_eq!(state.value(), expected);
8228 });
8229 });
8230 }
8231
8232 #[gpui::test]
8233 fn test_undo_manager_long_multiline_sequence_has_structural_boundaries(
8234 cx: &mut TestAppContext,
8235 ) {
8236 let input_view = InputView::build_textarea(cx, |state| state);
8237 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8238 let input = input_view.input;
8239 let enter = Enter {
8240 secondary: false,
8241 shift: false,
8242 };
8243
8244 cx.update(|window, cx| {
8245 input.update(cx, |state, cx| {
8246 for (index, line) in [
8247 "first line with punctuation!",
8248 "第二行包含 Unicode 🦀",
8249 "third line has several words",
8250 ]
8251 .into_iter()
8252 .enumerate()
8253 {
8254 for chunk in line.split_inclusive(' ') {
8255 state.replace_text_in_range(None, chunk, window, cx);
8256 }
8257 if index < 2 {
8258 state.enter(&enter, window, cx);
8259 }
8260 }
8261
8262 assert_eq!(
8263 state.value(),
8264 "first line with punctuation!\n第二行包含 Unicode 🦀\nthird line has several words"
8265 );
8266 state.undo(&Undo, window, cx);
8267 assert_eq!(
8268 state.value(),
8269 "first line with punctuation!\n第二行包含 Unicode 🦀\n"
8270 );
8271 state.undo(&Undo, window, cx);
8272 assert_eq!(
8273 state.value(),
8274 "first line with punctuation!\n第二行包含 Unicode 🦀"
8275 );
8276 state.undo(&Undo, window, cx);
8277 assert_eq!(state.value(), "first line with punctuation!\n");
8278 });
8279 });
8280 }
8281
8282 #[gpui::test]
8283 fn test_masked_input_keeps_its_value_out_of_the_clipboard(cx: &mut TestAppContext) {
8284 let input_view = InputView::build(cx, |state| state);
8285 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8286 let input = input_view.input;
8287
8288 cx.update(|window, cx| {
8289 input.update(cx, |state, cx| {
8290 state.set_value("hunter2", window, cx);
8291 state.set_masked(true, window, cx);
8292 state.select_all(window, cx);
8293 cx.write_to_clipboard(ClipboardItem::new_string("sentinel".into()));
8294
8295 state.copy(&Copy, window, cx);
8296 assert_eq!(
8297 cx.read_from_clipboard().and_then(|item| item.text()),
8298 Some("sentinel".to_string())
8299 );
8300
8301 state.cut(&Cut, window, cx);
8303 assert_eq!(state.value(), "hunter2");
8304 assert_eq!(
8305 cx.read_from_clipboard().and_then(|item| item.text()),
8306 Some("sentinel".to_string())
8307 );
8308
8309 state.set_masked(false, window, cx);
8311 state.copy(&Copy, window, cx);
8312 assert_eq!(
8313 cx.read_from_clipboard().and_then(|item| item.text()),
8314 Some("hunter2".to_string())
8315 );
8316 });
8317 });
8318 }
8319
8320 #[gpui::test]
8321 fn test_masked_input_collapses_word_boundaries(cx: &mut TestAppContext) {
8322 let input_view = InputView::build(cx, |state| state);
8323 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8324 let input = input_view.input;
8325
8326 cx.update(|window, cx| {
8327 input.update(cx, |state, cx| {
8328 state.set_value("aaa bbb ccc", window, cx);
8329 state.set_masked(true, window, cx);
8330 state.set_selected_range(7..7, cx);
8331
8332 state.delete_previous_word(&DeleteToPreviousWordStart, window, cx);
8335 assert_eq!(state.value(), " ccc");
8336 assert_eq!(state.selected_range(), 0..0);
8337
8338 state.delete_next_word(&DeleteToNextWordEnd, window, cx);
8339 assert_eq!(state.value(), "");
8340
8341 state.set_value("aaa bbb ccc", window, cx);
8343 state.select_word(9, window, cx);
8344 assert_eq!(state.selected_range(), 0..11);
8345
8346 state.set_masked(false, window, cx);
8348 state.set_value("aaa bbb ccc", window, cx);
8349 state.set_selected_range(11..11, cx);
8350 state.delete_previous_word(&DeleteToPreviousWordStart, window, cx);
8351 assert_eq!(state.value(), "aaa bbb ");
8352
8353 state.set_value("aaa bbb ccc", window, cx);
8354 state.select_word(9, window, cx);
8355 assert_eq!(state.selected_range(), 8..11);
8356 });
8357 });
8358 }
8359
8360 #[gpui::test]
8361 fn test_masked_input_disables_the_copy_context_menu_items(cx: &mut TestAppContext) {
8362 let input_view = InputView::build(cx, |state| state);
8363 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8364 let input = input_view.input;
8365
8366 cx.update(|window, cx| {
8367 input.update(cx, |state, cx| {
8368 state.set_value("hunter2", window, cx);
8369 state.select_all(window, cx);
8370 assert!(state.context_menu_capabilities().is_copyable());
8371
8372 state.set_masked(true, window, cx);
8373 let capabilities = state.context_menu_capabilities();
8374 assert!(capabilities.is_masked());
8375 assert!(capabilities.has_selection());
8376 assert!(!capabilities.is_copyable());
8377 });
8378 });
8379 }
8380
8381 #[gpui::test]
8382 fn test_undo_manager_cut_and_repeated_pastes_are_distinct_transactions(
8383 cx: &mut TestAppContext,
8384 ) {
8385 let input_view = InputView::build(cx, |state| state);
8386 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8387 let input = input_view.input;
8388
8389 cx.update(|window, cx| {
8390 input.update(cx, |state, cx| {
8391 state.replace_text_in_range(None, "alpha beta gamma", window, cx);
8392 state.set_selected_range(6..10, cx);
8393 state.cut(&Cut, window, cx);
8394 assert_eq!(state.value(), "alpha gamma");
8395
8396 state.paste(&Paste, window, cx);
8397 state.paste(&Paste, window, cx);
8398 assert_eq!(state.value(), "alpha betabeta gamma");
8399
8400 state.undo(&Undo, window, cx);
8401 assert_eq!(state.value(), "alpha beta gamma");
8402 state.undo(&Undo, window, cx);
8403 assert_eq!(state.value(), "alpha gamma");
8404 state.undo(&Undo, window, cx);
8405 assert_eq!(state.value(), "alpha beta gamma");
8406 });
8407 });
8408 }
8409
8410 #[gpui::test]
8411 fn test_undo_manager_word_and_line_deletes_do_not_coalesce(cx: &mut TestAppContext) {
8412 let input_view = InputView::build_textarea(cx, |state| state);
8413 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8414 let input = input_view.input;
8415
8416 cx.update(|window, cx| {
8417 input.update(cx, |state, cx| {
8418 state.set_value("one two three\nfour five", window, cx);
8419 state.set_selected_range(13..13, cx);
8420 state.delete_previous_word(&DeleteToPreviousWordStart, window, cx);
8421 assert_eq!(state.value(), "one two \nfour five");
8422 state.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
8423 assert_eq!(state.value(), "one two four five");
8424
8425 state.undo(&Undo, window, cx);
8426 assert_eq!(state.value(), "one two \nfour five");
8427 state.undo(&Undo, window, cx);
8428 assert_eq!(state.value(), "one two three\nfour five");
8429 });
8430 });
8431 }
8432
8433 #[gpui::test]
8434 fn test_undo_manager_multiline_replacement_is_one_atomic_transaction(cx: &mut TestAppContext) {
8435 let input_view = InputView::build_textarea(cx, |state| state);
8436 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8437 let input = input_view.input;
8438
8439 cx.update(|window, cx| {
8440 input.update(cx, |state, cx| {
8441 state.replace_text_in_range(None, "before", window, cx);
8442 state.set_selected_range(0..6, cx);
8443 state.replace_text_in_range(None, "line one\nline two\n第三行", window, cx);
8444 assert_eq!(state.value(), "line one\nline two\n第三行");
8445
8446 state.undo(&Undo, window, cx);
8447 assert_eq!(state.value(), "before");
8448 assert_eq!(state.selected_range(), 0..6);
8449 state.redo(&Redo, window, cx);
8450 assert_eq!(state.value(), "line one\nline two\n第三行");
8451 });
8452 });
8453 }
8454
8455 #[gpui::test]
8456 fn test_undo_manager_composition_isolated_from_long_typing(cx: &mut TestAppContext) {
8457 let input_view = InputView::build(cx, |state| state);
8458 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8459 let input = input_view.input;
8460
8461 cx.update(|window, cx| {
8462 input.update(cx, |state, cx| {
8463 state.replace_text_in_range(None, "prefix ", window, cx);
8464 state.replace_and_mark_text_in_range(None, "n", None, window, cx);
8465 state.replace_and_mark_text_in_range(None, "ni", None, window, cx);
8466 state.replace_and_mark_text_in_range(None, "你", None, window, cx);
8467 state.unmark_text(window, cx);
8468 state.replace_text_in_range(None, " suffix", window, cx);
8469 assert_eq!(state.value(), "prefix 你 suffix");
8470
8471 state.undo(&Undo, window, cx);
8472 assert_eq!(state.value(), "prefix 你");
8473 state.undo(&Undo, window, cx);
8474 assert_eq!(state.value(), "prefix ");
8475 state.undo(&Undo, window, cx);
8476 assert_eq!(state.value(), "");
8477 });
8478 });
8479 }
8480
8481 #[gpui::test]
8482 fn test_undo_manager_selected_replacement_is_atomic(cx: &mut TestAppContext) {
8483 let input_view = InputView::build(cx, |state| state);
8484 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8485 let input = input_view.input;
8486
8487 cx.update(|window, cx| {
8488 input.update(cx, |state, cx| {
8489 state.replace_text_in_range(None, "abc", window, cx);
8490 state.set_selected_range(1..2, cx);
8491 state.replace_text_in_range(None, "X", window, cx);
8492 state.replace_text_in_range(None, "z", window, cx);
8493 assert_eq!(state.value(), "aXzc");
8494
8495 state.undo(&Undo, window, cx);
8496 assert_eq!(state.value(), "aXc");
8497 state.undo(&Undo, window, cx);
8498 assert_eq!(state.value(), "abc");
8499 state.undo(&Undo, window, cx);
8500 assert_eq!(state.value(), "");
8501 });
8502 });
8503 }
8504
8505 #[gpui::test]
8506 fn test_number_input_leading_dot_editable(cx: &mut TestAppContext) {
8507 let input_view = InputView::build(cx, |state| {
8508 state.mask_pattern(MaskPattern::Number {
8509 separator: None,
8510 fraction: None,
8511 })
8512 });
8513 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8514 let input = input_view.input;
8515
8516 cx.update(|window, cx| {
8517 input.update(cx, |state, cx| {
8518 state.replace_text_in_range(None, "1.2", window, cx);
8519
8520 let range = state.range_to_utf16(&(0..1));
8524 state.replace_text_in_range(Some(range), "", window, cx);
8525 assert_eq!(state.value(), ".2");
8526 let cursor: Range<usize> = state.selected_range();
8527 assert_eq!(cursor, 0..0);
8528
8529 state.replace_text_in_range(Some(0..0), "3", window, cx);
8531 assert_eq!(state.value(), "3.2");
8532 });
8533 });
8534 }
8535
8536 #[gpui::test]
8537 fn test_number_input_escape_invalid_text(cx: &mut TestAppContext) {
8538 let input_view = InputView::build(cx, |state| {
8541 state
8542 .mask_pattern(MaskPattern::Number {
8543 separator: None,
8544 fraction: None,
8545 })
8546 .default_value("1,234")
8547 });
8548 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8549 let input = input_view.input;
8550
8551 cx.update(|window, cx| {
8552 input.update(cx, |state, cx| {
8553 let range = state.range_to_utf16(&(4..5));
8557 state.replace_text_in_range(Some(range), "", window, cx);
8558 assert_eq!(state.value(), "1,23");
8559
8560 let range = state.range_to_utf16(&(1..2));
8562 state.replace_text_in_range(Some(range), "", window, cx);
8563 assert_eq!(state.value(), "123");
8564 state.replace_text_in_range(None, "a", window, cx);
8565 assert_eq!(state.value(), "123");
8566 });
8567 });
8568 }
8569
8570 #[gpui::test]
8574 fn test_set_value_single_line_caret_at_end_view_at_start(cx: &mut TestAppContext) {
8575 let input_view = InputView::build(cx, |state| state);
8576 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8577 let input = input_view.input;
8578
8579 let value = format!("https://example.com/v1/users?{}", "x=1&".repeat(120));
8581 let len = value.len();
8582
8583 cx.update(|window, cx| {
8586 input.update(cx, |state, cx| {
8587 state.set_value(value.clone(), window, cx);
8588
8589 assert_eq!(
8590 state.selected_range(),
8591 len..len,
8592 "single-line caret should be at the end after set_value"
8593 );
8594 assert_eq!(
8595 state.deferred_scroll_offset,
8596 Some(point(px(0.), px(0.))),
8597 "the view should be forced back to the start"
8598 );
8599 });
8600 });
8601
8602 cx.run_until_parked();
8605 cx.update(|_, cx| {
8606 input.read_with(cx, |state, _| {
8607 assert!(
8608 state.scroll_size.width > state.input_bounds.size.width,
8609 "value must overflow the input width or this test is vacuous"
8610 );
8611 assert_eq!(
8612 state.scroll_handle.offset().x,
8613 px(0.),
8614 "long value should display from its start, not its tail"
8615 );
8616 });
8617 });
8618 }
8619
8620 #[gpui::test]
8624 fn test_replace_all_single_line(cx: &mut TestAppContext) {
8625 let input_view = InputView::build(cx, |state| state);
8626 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8627 let input = input_view.input;
8628
8629 let value = format!("https://example.com/v1/users?{}", "x=1&".repeat(120));
8631 let len = value.len();
8632
8633 cx.update(|window, cx| {
8637 input.update(cx, |state, cx| {
8638 state.set_value("hello", window, cx);
8639 state.replace_all(value.clone(), window, cx);
8640 assert_eq!(state.value(), value);
8641 assert_eq!(
8642 state.selected_range(),
8643 len..len,
8644 "single-line caret should be at the end after replace_all"
8645 );
8646 assert_eq!(
8647 state.scroll_handle.offset(),
8648 point(px(0.), px(0.)),
8649 "the scroll offset should be reset to the start"
8650 );
8651 assert_eq!(
8652 state.deferred_scroll_offset,
8653 Some(point(px(0.), px(0.))),
8654 "single-line should set a deferred scroll offset to keep the start visible"
8655 );
8656 });
8657 });
8658
8659 cx.run_until_parked();
8662 cx.update(|_, cx| {
8663 input.read_with(cx, |state, _| {
8664 assert!(
8665 state.scroll_size.width > state.input_bounds.size.width,
8666 "value must overflow the input width or this test is vacuous"
8667 );
8668 assert_eq!(
8669 state.scroll_handle.offset().x,
8670 px(0.),
8671 "long value should display from its start, not its tail"
8672 );
8673 });
8674 });
8675 }
8676
8677 #[gpui::test]
8678 fn test_single_line_removes_newlines(cx: &mut TestAppContext) {
8679 let input_view = InputView::build(cx, |state| state.default_value("default\nvalue"));
8680 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8681 let input = input_view.input;
8682
8683 cx.update(|window, cx| {
8684 input.update(cx, |state, cx| {
8685 assert_eq!(state.value(), "defaultvalue");
8686
8687 state.set_value("first\nsecond\r\nthird\rfourth", window, cx);
8688 assert_eq!(state.value(), "firstsecondthirdfourth");
8689
8690 state.set_value("", window, cx);
8691 state.insert("a\nb", window, cx);
8692 assert_eq!(state.value(), "ab");
8693 });
8694
8695 cx.write_to_clipboard(ClipboardItem::new_string("a\r\nb\nc\rd".to_string()));
8696 input.update(cx, |state, cx| {
8697 state.set_value("", window, cx);
8698 state.paste(&Paste, window, cx);
8699 assert_eq!(state.value(), "abcd");
8700 });
8701 });
8702
8703 cx.run_until_parked();
8704 }
8705
8706 #[gpui::test]
8710 fn test_replace_all_multi_line(cx: &mut TestAppContext) {
8711 let input_view = InputView::build_textarea(cx, |state| state);
8712 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8713 let input = input_view.input;
8714
8715 cx.update(|window, cx| {
8716 input.update(cx, |state, cx| {
8717 state.set_value("foo\nbar", window, cx);
8718 state.replace_all("baz\nqux", window, cx);
8719 assert_eq!(state.value(), "baz\nqux");
8720 assert_eq!(
8721 state.selected_range(),
8722 0..0,
8723 "multi-line selection should be cleared after replace_all"
8724 );
8725 assert_eq!(
8726 state.scroll_handle.offset(),
8727 point(px(0.), px(0.)),
8728 "the scroll offset should be reset to the start"
8729 );
8730 assert!(
8731 state.deferred_scroll_offset.is_none(),
8732 "multi-line should not set a deferred scroll offset"
8733 );
8734 });
8735 });
8736 }
8737
8738 #[gpui::test]
8741 fn test_replace_all_preserves_undo_history(cx: &mut TestAppContext) {
8742 let input_view = InputView::build(cx, |state| state);
8743 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8744 let input = input_view.input;
8745
8746 cx.update(|window, cx| {
8747 input.update(cx, |state, cx| {
8748 state.set_value("first", window, cx);
8750 assert!(
8751 !state.undo_manager.has_undos(),
8752 "history should be empty after set_value"
8753 );
8754
8755 state.replace_all("second", window, cx);
8757 assert_eq!(state.value(), "second");
8758 assert!(
8759 state.undo_manager.has_undos(),
8760 "replace_all should record an undo step"
8761 );
8762
8763 state.undo(&Undo, window, cx);
8765 assert_eq!(state.value(), "first");
8766
8767 state.redo(&Redo, window, cx);
8769 assert_eq!(state.value(), "second");
8770 });
8771 });
8772 }
8773
8774 #[gpui::test]
8777 fn test_replace_all_code_editor(cx: &mut TestAppContext) {
8778 let input_view = InputView::new(cx);
8779 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8780 let input = input_view.input;
8781
8782 cx.update(|window, cx| {
8783 input.update(cx, |state, cx| {
8784 state.set_value("select 1", window, cx);
8786 state._pending_update = false;
8787
8788 state.replace_all("select 2", window, cx);
8789 assert_eq!(state.value(), "select 2");
8790 assert!(
8791 state._pending_update,
8792 "replace_all on a code editor should request a pending update"
8793 );
8794 });
8795 });
8796 }
8797
8798 #[gpui::test]
8799 fn test_set_selected_range(cx: &mut TestAppContext) {
8800 let input_view = InputView::build(cx, |state| state.default_value("hello world"));
8801 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8802 let input = input_view.input;
8803
8804 cx.update(|_, cx| {
8805 input.update(cx, |s, cx| {
8806 s.set_selected_range(0..5, cx);
8807 assert_eq!(s.selected_range(), 0..5);
8808 assert_eq!(s.selected_text().to_string(), "hello");
8809
8810 s.set_selected_range(6..11, cx);
8811 assert_eq!(s.selected_text().to_string(), "world");
8812
8813 s.set_selected_range(100..100, cx);
8815 assert_eq!(s.selected_range(), 11..11);
8816 });
8817 });
8818 }
8819
8820 #[gpui::test]
8822 fn test_replace_text_in_ranges_single_edit(cx: &mut TestAppContext) {
8823 let input_view = InputView::build_textarea(cx, |state| state.default_value("hello world"));
8824 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8825 let input = input_view.input;
8826
8827 cx.update(|window, cx| {
8828 input.update(cx, |s, cx| {
8829 s.replace_text_in_ranges(&[(0..5, "HELLO".to_string())], window, cx);
8830 assert_eq!(s.value(), "HELLO world");
8831
8832 s.undo(&Undo, window, cx);
8833 assert_eq!(s.value(), "hello world");
8834
8835 s.redo(&Redo, window, cx);
8836 assert_eq!(s.value(), "HELLO world");
8837 });
8838 });
8839 }
8840
8841 #[gpui::test]
8844 fn test_replace_text_in_ranges_multi_edit_transaction(cx: &mut TestAppContext) {
8845 let input_view = InputView::build_textarea(cx, |state| state.default_value("aaa bbb ccc"));
8846 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8847 let input = input_view.input;
8848
8849 cx.update(|window, cx| {
8850 input.update(cx, |s, cx| {
8851 s.replace_text_in_ranges(
8854 &[(0..3, "X".to_string()), (8..11, "Y".to_string())],
8855 window,
8856 cx,
8857 );
8858 assert_eq!(s.value(), "X bbb Y");
8859
8860 let cursors: Vec<usize> =
8862 s.selections.iter().map(|sel| sel.cursor_offset()).collect();
8863 assert_eq!(cursors, vec![1, 7]);
8864
8865 assert_eq!(s.undo_manager.undo_count(), 1);
8867
8868 s.undo(&Undo, window, cx);
8870 assert_eq!(s.value(), "aaa bbb ccc");
8871
8872 s.redo(&Redo, window, cx);
8874 assert_eq!(s.value(), "X bbb Y");
8875 });
8876 });
8877 }
8878
8879 #[gpui::test]
8883 fn test_replace_text_in_ranges_drives_the_highlighter_once(cx: &mut TestAppContext) {
8884 use crate::input::{FoldRange, HighlightStyleResolver, InputEdit, InputHighlighter};
8885 use gpui::HighlightStyle;
8886 use std::cell::RefCell;
8887
8888 struct RecordingHighlighter(Rc<RefCell<Vec<Vec<String>>>>);
8889
8890 impl InputHighlighter for RecordingHighlighter {
8891 fn language(&self) -> SharedString {
8892 "recording".into()
8893 }
8894
8895 fn update(
8896 &mut self,
8897 _: Option<InputEdit>,
8898 text: &Rope,
8899 _: bool,
8900 _: &mut Window,
8901 _: &mut Context<EditorState>,
8902 ) {
8903 self.0.borrow_mut().push(vec![text.to_string()]);
8904 }
8905
8906 fn update_batch(
8907 &mut self,
8908 edits: &[(InputEdit, Rope)],
8909 _: bool,
8910 _: &mut Window,
8911 _: &mut Context<EditorState>,
8912 ) {
8913 let texts = edits.iter().map(|(_, text)| text.to_string()).collect();
8914 self.0.borrow_mut().push(texts);
8915 }
8916
8917 fn styles(
8918 &self,
8919 range: &Range<usize>,
8920 _: &dyn HighlightStyleResolver,
8921 ) -> Vec<(Range<usize>, HighlightStyle)> {
8922 vec![(range.clone(), HighlightStyle::default())]
8923 }
8924
8925 fn fold_ranges(&self, _: &Rope) -> Vec<FoldRange> {
8926 Vec::new()
8927 }
8928 }
8929
8930 let updates = Rc::new(RefCell::new(Vec::new()));
8931 let input_view = InputView::new(cx);
8932 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8933 let input = input_view.input;
8934
8935 cx.update(|window, cx| {
8936 input.update(cx, |s, cx| {
8937 s.set_value("aaa bbb ccc", window, cx);
8938 let recorded = updates.clone();
8939 s.set_highlighter_factory(
8940 Rc::new(move |_: &str| {
8941 let highlighter: Box<dyn InputHighlighter> =
8942 Box::new(RecordingHighlighter(recorded.clone()));
8943 Some(highlighter)
8944 }),
8945 cx,
8946 );
8947 s.replace_text_in_ranges(
8948 &[(0..3, "X".to_string()), (8..11, "Y".to_string())],
8949 window,
8950 cx,
8951 );
8952 });
8953 });
8954
8955 assert_eq!(
8956 *updates.borrow(),
8957 vec![vec!["aaa bbb Y".to_string(), "X bbb Y".to_string()]]
8958 );
8959 }
8960
8961 #[gpui::test]
8963 fn test_last_input_edits_single_insert(cx: &mut TestAppContext) {
8964 let input_view = InputView::build_textarea(cx, |state| state.default_value("hello world"));
8965 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8966 let input = input_view.input;
8967
8968 cx.update(|window, cx| {
8969 input.update(cx, |state, cx| {
8970 state.replace_text_in_ranges(&[(5..5, " big".to_string())], window, cx);
8971
8972 let edits = state.last_input_edits.as_slice();
8973 assert_eq!(edits.len(), 1);
8974 let (edit, text_after) = &edits[0];
8975 assert_eq!(edit.start_byte, 5);
8976 assert_eq!(edit.old_end_byte, 5);
8977 assert_eq!(edit.new_end_byte, 9);
8978 assert_eq!(edit.start_position, crate::input::Point::new(0, 5));
8979 assert_eq!(edit.old_end_position, crate::input::Point::new(0, 5));
8980 assert_eq!(edit.new_end_position, crate::input::Point::new(0, 9));
8981 assert_eq!(text_after.to_string(), "hello big world");
8982 });
8983 });
8984 }
8985
8986 #[gpui::test]
8988 fn test_last_input_edits_delete_and_paste(cx: &mut TestAppContext) {
8989 let input_view = InputView::build_textarea(cx, |state| state.default_value("hello world"));
8990 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8991 let input = input_view.input;
8992
8993 cx.update(|window, cx| {
8994 input.update(cx, |state, cx| {
8995 state.replace_text_in_ranges(&[(5..11, String::new())], window, cx);
8996 assert_eq!(state.value(), "hello");
8997
8998 let edits = state.last_input_edits.as_slice();
8999 assert_eq!(edits.len(), 1);
9000 let (edit, text_after) = &edits[0];
9001 assert_eq!(edit.start_byte, 5);
9002 assert_eq!(edit.old_end_byte, 11);
9003 assert_eq!(edit.new_end_byte, 5);
9004 assert_eq!(text_after.to_string(), "hello");
9005
9006 state.replace_text_in_ranges(&[(5..5, "\nnext".to_string())], window, cx);
9007 assert_eq!(state.value(), "hello\nnext");
9008
9009 let edits = state.last_input_edits.as_slice();
9010 assert_eq!(edits.len(), 1);
9011 let (edit, text_after) = &edits[0];
9012 assert_eq!(edit.start_byte, 5);
9013 assert_eq!(edit.old_end_byte, 5);
9014 assert_eq!(edit.new_end_byte, 10);
9015 assert_eq!(edit.start_position, crate::input::Point::new(0, 5));
9016 assert_eq!(edit.old_end_position, crate::input::Point::new(0, 5));
9017 assert_eq!(edit.new_end_position, crate::input::Point::new(1, 4));
9018 assert_eq!(text_after.to_string(), "hello\nnext");
9019 });
9020 });
9021 }
9022
9023 #[gpui::test]
9025 fn test_last_input_edits_grouped_change(cx: &mut TestAppContext) {
9026 let input_view = InputView::build_textarea(cx, |state| state.default_value("aaa bbb ccc"));
9027 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
9028 let input = input_view.input;
9029
9030 cx.update(|window, cx| {
9031 input.update(cx, |state, cx| {
9032 state.replace_text_in_ranges(
9033 &[(0..3, "X".to_string()), (8..11, "Y".to_string())],
9034 window,
9035 cx,
9036 );
9037 assert_eq!(state.value(), "X bbb Y");
9038
9039 let edits = state.last_input_edits.as_slice();
9040 assert_eq!(edits.len(), 2);
9041 assert_eq!(edits[0].0.start_byte, 8);
9043 assert_eq!(edits[0].0.old_end_byte, 11);
9044 assert_eq!(edits[0].0.new_end_byte, 9);
9045 assert_eq!(edits[0].0.start_position, crate::input::Point::new(0, 8));
9046 assert_eq!(edits[0].0.old_end_position, crate::input::Point::new(0, 11));
9047 assert_eq!(edits[0].0.new_end_position, crate::input::Point::new(0, 9));
9048 assert_eq!(edits[0].1.to_string(), "aaa bbb Y");
9049
9050 assert_eq!(edits[1].0.start_byte, 0);
9051 assert_eq!(edits[1].0.old_end_byte, 3);
9052 assert_eq!(edits[1].0.new_end_byte, 1);
9053 assert_eq!(edits[1].0.start_position, crate::input::Point::new(0, 0));
9054 assert_eq!(edits[1].0.old_end_position, crate::input::Point::new(0, 3));
9055 assert_eq!(edits[1].0.new_end_position, crate::input::Point::new(0, 1));
9056 assert_eq!(edits[1].1.to_string(), "X bbb Y");
9057 });
9058 });
9059 }
9060
9061 #[gpui::test]
9063 fn test_last_input_edits_without_a_highlighter(cx: &mut TestAppContext) {
9064 let input_view = InputView::new(cx);
9065 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
9066 let input = input_view.input;
9067
9068 cx.update(|window, cx| {
9069 input.update(cx, |state, cx| {
9070 state.set_value("fn main() {}", window, cx);
9071 let no_highlighter: InputHighlighterFactory = Rc::new(|_: &str| None);
9072 state.set_highlighter_factory(no_highlighter, cx);
9073 assert!(state.last_input_edits.as_slice().is_empty());
9074
9075 state.replace_text_in_ranges(&[(12..12, "\n".to_string())], window, cx);
9076 assert_eq!(state.value(), "fn main() {}\n");
9077
9078 let edits = state.last_input_edits.as_slice();
9079 assert_eq!(edits.len(), 1);
9080 let (edit, text_after) = &edits[0];
9081 assert_eq!(edit.start_byte, 12);
9082 assert_eq!(edit.old_end_byte, 12);
9083 assert_eq!(edit.new_end_byte, 13);
9084 assert_eq!(edit.start_position, crate::input::Point::new(0, 12));
9085 assert_eq!(edit.old_end_position, crate::input::Point::new(0, 12));
9086 assert_eq!(edit.new_end_position, crate::input::Point::new(1, 0));
9087 assert_eq!(text_after.to_string(), "fn main() {}\n");
9088 });
9089 });
9090 }
9091
9092 #[gpui::test]
9094 fn test_last_input_edits_cleared_by_set_value(cx: &mut TestAppContext) {
9095 let input_view = InputView::build_textarea(cx, |state| state.default_value("hello"));
9096 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
9097 let input = input_view.input;
9098
9099 cx.update(|window, cx| {
9100 input.update(cx, |state, cx| {
9101 state.replace_text_in_ranges(&[(5..5, "!".to_string())], window, cx);
9102 assert_eq!(state.last_input_edits.as_slice().len(), 1);
9103
9104 state.set_value("replaced", window, cx);
9105 assert!(state.last_input_edits.as_slice().is_empty());
9106
9107 state.replace_text_in_ranges(&[(8..8, "?".to_string())], window, cx);
9108 let edits = state.last_input_edits.as_slice();
9109 assert_eq!(edits.len(), 1);
9110 assert_eq!(edits[0].1.to_string(), "replaced?");
9111 });
9112 });
9113 }
9114
9115 fn replay_input_edits(text: &mut String, event: &InputEditsEvent) {
9117 for (edit, after) in event.edits() {
9118 let replacement = after.slice(edit.start_byte..edit.new_end_byte).to_string();
9119 text.replace_range(edit.start_byte..edit.old_end_byte, &replacement);
9120 assert_eq!(*text, after.to_string());
9121 }
9122 }
9123
9124 #[gpui::test]
9125 fn input_edits_events_preserve_queued_changes_and_undo(cx: &mut TestAppContext) {
9126 let view = InputView::build_textarea(cx, |state| state.default_value("hello"));
9127 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9128 let input = view.input;
9129 let observed = Rc::new(std::cell::RefCell::new(Vec::<InputEditsEvent>::new()));
9130 let seen = observed.clone();
9131 let _subscription = cx.update(|_, cx| {
9132 cx.subscribe(&input, move |_, event: &InputEditsEvent, _| {
9133 seen.borrow_mut().push(event.clone());
9134 })
9135 });
9136 cx.update(|window, cx| {
9137 input.update(cx, |state, cx| {
9138 state.replace_text_in_ranges(&[(5..5, "!".into())], window, cx);
9139 state.replace_text_in_ranges(&[(6..6, "?".into())], window, cx);
9140 state.undo(&Undo, window, cx);
9141 });
9142 });
9143 let events = observed.borrow();
9144 assert!(events.len() >= 3);
9145 let mut text = "hello".to_string();
9146 for event in events.iter() {
9147 replay_input_edits(&mut text, event);
9148 }
9149 assert_eq!(
9150 text,
9151 input.read_with(&cx, |state, _| state.value().to_string())
9152 );
9153 assert_eq!(events[0].edits()[0].1.to_string(), "hello!");
9154 assert_eq!(events[1].edits()[0].1.to_string(), "hello!?");
9155 }
9156
9157 #[gpui::test]
9158 fn input_edits_event_replays_ime_commit(cx: &mut TestAppContext) {
9159 let view = InputView::build_textarea(cx, |state| state.default_value(""));
9160 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9161 let input = view.input;
9162 let observed = Rc::new(std::cell::RefCell::new(Vec::<InputEditsEvent>::new()));
9163 let seen = observed.clone();
9164 let _subscription = cx.update(|_, cx| {
9165 cx.subscribe(&input, move |_, event: &InputEditsEvent, _| {
9166 seen.borrow_mut().push(event.clone());
9167 })
9168 });
9169 cx.update(|window, cx| {
9170 input.update(cx, |state, cx| {
9171 state.replace_and_mark_text_in_range(None, "n", Some(1..1), window, cx);
9172 state.replace_and_mark_text_in_range(None, "ni", Some(2..2), window, cx);
9173 state.replace_text_in_range(None, "你", window, cx);
9174 });
9175 });
9176 let events = observed.borrow();
9177 assert_eq!(events.len(), 1);
9178 let mut text = String::new();
9179 replay_input_edits(&mut text, &events[0]);
9180 assert_eq!(text, "你");
9181 }
9182
9183 #[gpui::test]
9184 fn input_edits_event_replays_after_cancelled_composition(cx: &mut TestAppContext) {
9185 let view = InputView::build_textarea(cx, |state| state.default_value(""));
9186 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9187 let input = view.input;
9188 let observed = Rc::new(std::cell::RefCell::new(Vec::<InputEditsEvent>::new()));
9189 let seen = observed.clone();
9190 let _subscription = cx.update(|_, cx| {
9191 cx.subscribe(&input, move |_, event: &InputEditsEvent, _| {
9192 seen.borrow_mut().push(event.clone());
9193 })
9194 });
9195 cx.update(|window, cx| {
9196 input.update(cx, |state, cx| {
9197 state.replace_and_mark_text_in_range(None, "n", Some(1..1), window, cx);
9198 state.replace_and_mark_text_in_range(None, "", None, window, cx);
9199 state.replace_text_in_range(None, "x", window, cx);
9200 });
9201 });
9202 let events = observed.borrow();
9203 assert_eq!(events.len(), 1);
9204 let mut text = String::new();
9205 replay_input_edits(&mut text, &events[0]);
9206 assert_eq!(text, "x");
9207 }
9208
9209 #[gpui::test]
9210 fn input_edits_event_replays_mask_rewrite(cx: &mut TestAppContext) {
9211 let view = InputView::build(cx, |state| {
9212 state
9213 .mask_pattern(MaskPattern::Number {
9214 separator: Some(','),
9215 fraction: None,
9216 })
9217 .default_value("999")
9218 });
9219 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9220 let input = view.input;
9221 let observed = Rc::new(std::cell::RefCell::new(Vec::<InputEditsEvent>::new()));
9222 let seen = observed.clone();
9223 let _subscription = cx.update(|_, cx| {
9224 cx.subscribe(&input, move |_, event: &InputEditsEvent, _| {
9225 seen.borrow_mut().push(event.clone());
9226 })
9227 });
9228 cx.update(|window, cx| {
9229 input.update(cx, |state, cx| {
9230 state.set_cursor_to(3);
9231 state.replace_text_in_range(None, "9", window, cx)
9232 });
9233 });
9234 let events = observed.borrow();
9235 assert_eq!(events.len(), 1);
9236 let mut text = "999".to_string();
9237 replay_input_edits(&mut text, &events[0]);
9238 assert_eq!(text, "9,999");
9239 }
9240
9241 #[gpui::test]
9243 fn test_ime_composition_undoes_as_one_unit(cx: &mut TestAppContext) {
9244 let input_view = InputView::build_textarea(cx, |state| state.default_value(""));
9245 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
9246 let input = input_view.input;
9247
9248 cx.update(|window, cx| {
9249 input.update(cx, |s, cx| {
9250 s.replace_and_mark_text_in_range(None, "n", Some(1..1), window, cx);
9252 s.replace_and_mark_text_in_range(None, "ni", Some(2..2), window, cx);
9253 s.replace_text_in_range(None, "你", window, cx);
9254 assert_eq!(s.value(), "你");
9255
9256 assert_eq!(s.undo_manager.undo_count(), 1);
9258
9259 s.undo(&Undo, window, cx);
9260 assert_eq!(s.value(), "");
9261
9262 s.redo(&Redo, window, cx);
9263 assert_eq!(s.value(), "你");
9264 });
9265 });
9266 }
9267
9268 #[gpui::test]
9271 fn test_edit_after_composition_is_separate_undo(cx: &mut TestAppContext) {
9272 let input_view = InputView::build_textarea(cx, |state| state.default_value(""));
9273 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
9274 let input = input_view.input;
9275
9276 cx.update(|window, cx| {
9277 input.update(cx, |s, cx| {
9278 s.replace_and_mark_text_in_range(None, "n", Some(1..1), window, cx);
9279 s.replace_text_in_range(None, "你", window, cx);
9280 assert_eq!(s.value(), "你");
9281 assert_eq!(s.undo_manager.undo_count(), 1);
9282
9283 s.replace_text_in_range(None, "x", window, cx);
9285 assert_eq!(s.value(), "你x");
9286 assert_eq!(s.undo_manager.undo_count(), 2);
9287
9288 s.undo(&Undo, window, cx);
9289 assert_eq!(s.value(), "你");
9290 s.undo(&Undo, window, cx);
9291 assert_eq!(s.value(), "");
9292 });
9293 });
9294 }
9295
9296 #[gpui::test]
9299 fn test_composition_cancel_via_unmark_does_not_leak(cx: &mut TestAppContext) {
9300 let input_view = InputView::build_textarea(cx, |state| state.default_value(""));
9301 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
9302 let input = input_view.input;
9303
9304 cx.update(|window, cx| {
9305 input.update(cx, |s, cx| {
9306 s.replace_and_mark_text_in_range(None, "n", Some(1..1), window, cx);
9308 s.unmark_text(window, cx);
9309 let after_cancel = s.undo_manager.undo_count();
9310
9311 s.replace_text_in_range(None, "x", window, cx);
9313 assert_eq!(s.undo_manager.undo_count(), after_cancel + 1);
9314 });
9315 });
9316 }
9317
9318 #[gpui::test]
9319 fn test_set_selected_range_clips_to_utf8_boundaries(cx: &mut TestAppContext) {
9320 let input_view = InputView::build(cx, |state| state.default_value("éx"));
9321 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
9322 let input = input_view.input;
9323
9324 cx.update(|window, cx| {
9325 input.update(cx, |state, cx| {
9326 state.set_selected_range(0..1, cx);
9327 assert_eq!(state.selected_range(), 0..2);
9328 state.copy(&Copy, window, cx);
9329
9330 state.set_selected_range(1..1, cx);
9331 assert_eq!(state.selected_range(), 0..0);
9332 });
9333 });
9334 }
9335
9336 #[gpui::test]
9337 fn test_ime_selection_is_relative_to_replacement_start(cx: &mut TestAppContext) {
9338 let input_view = InputView::build(cx, |state| state.default_value("你好 "));
9339 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
9340 let input = input_view.input;
9341
9342 cx.update(|window, cx| {
9343 input.update(cx, |state, cx| {
9344 state.set_selected_range(7..7, cx);
9345 state.replace_and_mark_text_in_range(None, "s", Some(1..1), window, cx);
9346 state.replace_and_mark_text_in_range(None, "sh", Some(2..2), window, cx);
9347
9348 assert_eq!(state.value(), "你好 sh");
9349 assert_eq!(state.selected_range(), 9..9);
9350 assert_eq!(state.ime_marked_range, Some((7..9).into()));
9351 });
9352 });
9353 }
9354
9355 #[gpui::test]
9356 fn test_undo_manager_composition_is_one_undo_group(cx: &mut TestAppContext) {
9357 let input_view = InputView::build(cx, |state| state);
9358 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
9359 let input = input_view.input;
9360
9361 cx.update(|window, cx| {
9362 input.update(cx, |state, cx| {
9363 state.set_value("a", window, cx);
9364 state.replace_and_mark_text_in_range(None, "s", None, window, cx);
9365 state.replace_and_mark_text_in_range(None, "sh", None, window, cx);
9366 state.replace_text_in_range(None, "是", window, cx);
9367 assert_eq!(state.value(), "a是");
9368
9369 state.undo(&Undo, window, cx);
9370 assert_eq!(state.value(), "a");
9371 state.redo(&Redo, window, cx);
9372 assert_eq!(state.value(), "a是");
9373 });
9374 });
9375 }
9376
9377 #[gpui::test]
9378 fn test_undo_manager_consecutive_compositions_are_separate_groups(cx: &mut TestAppContext) {
9379 let input_view = InputView::build(cx, |state| state);
9380 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
9381 let input = input_view.input;
9382
9383 cx.update(|window, cx| {
9384 input.update(cx, |state, cx| {
9385 state.replace_and_mark_text_in_range(None, "j", None, window, cx);
9387 state.replace_and_mark_text_in_range(None, "jin", None, window, cx);
9388 state.replace_text_in_range(None, "今天", window, cx);
9389 state.replace_and_mark_text_in_range(None, "w", None, window, cx);
9391 state.replace_and_mark_text_in_range(None, "wo", None, window, cx);
9392 state.replace_text_in_range(None, "我们", window, cx);
9393 assert_eq!(state.value(), "今天我们");
9394 assert_eq!(state.selected_range(), 12..12);
9395
9396 state.undo(&Undo, window, cx);
9397 assert_eq!(state.value(), "今天");
9398 assert_eq!(state.selected_range(), 6..6);
9399
9400 state.undo(&Undo, window, cx);
9401 assert_eq!(state.value(), "");
9402 assert_eq!(state.selected_range(), 0..0);
9403
9404 state.redo(&Redo, window, cx);
9405 assert_eq!(state.value(), "今天");
9406 assert_eq!(state.selected_range(), 6..6);
9407
9408 state.redo(&Redo, window, cx);
9409 assert_eq!(state.value(), "今天我们");
9410 assert_eq!(state.selected_range(), 12..12);
9411 });
9412 });
9413 }
9414
9415 #[gpui::test]
9416 fn test_undo_manager_typing_after_composition_is_a_separate_group(cx: &mut TestAppContext) {
9417 let input_view = InputView::build(cx, |state| state);
9418 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
9419 let input = input_view.input;
9420
9421 cx.update(|window, cx| {
9422 input.update(cx, |state, cx| {
9423 state.replace_and_mark_text_in_range(None, "n", None, window, cx);
9424 state.replace_text_in_range(None, "你", window, cx);
9425 state.undo_manager.set_pending_intent(EditIntent::Typing);
9426 state.replace_text_in_range(None, "a", window, cx);
9427 state.undo_manager.set_pending_intent(EditIntent::Typing);
9428 state.replace_text_in_range(None, "b", window, cx);
9429 assert_eq!(state.value(), "你ab");
9430
9431 state.undo(&Undo, window, cx);
9432 assert_eq!(state.value(), "你");
9433
9434 state.undo(&Undo, window, cx);
9435 assert_eq!(state.value(), "");
9436 });
9437 });
9438 }
9439
9440 #[gpui::test]
9441 fn test_undo_manager_composition_cancel_leaves_no_entry(cx: &mut TestAppContext) {
9442 let input_view = InputView::build(cx, |state| state);
9443 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
9444 let input = input_view.input;
9445
9446 cx.update(|window, cx| {
9447 input.update(cx, |state, cx| {
9448 state.set_value("a", window, cx);
9449 state.replace_and_mark_text_in_range(None, "s", None, window, cx);
9450 state.replace_and_mark_text_in_range(None, "", None, window, cx);
9451
9452 assert_eq!(state.value(), "a");
9453 assert!(!state.undo_manager.has_undos());
9454 });
9455 });
9456 }
9457
9458 #[gpui::test]
9460 fn test_undo_manager_ime_rewrites_of_typed_char_are_one_group(cx: &mut TestAppContext) {
9461 let input_view = InputView::build(cx, |state| state);
9462 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
9463 let input = input_view.input;
9464
9465 cx.update(|window, cx| {
9466 input.update(cx, |state, cx| {
9467 state.set_value("é ", window, cx);
9468 state.set_selected_range(3..3, cx);
9469 state.replace_text_in_range(None, "ㅎ", window, cx);
9471 state.replace_text_in_range(Some(2..3), "하", window, cx);
9472 state.replace_text_in_range(Some(2..3), "한", window, cx);
9473 state.replace_text_in_range(None, "ㄱ", window, cx);
9474 state.replace_text_in_range(Some(3..4), "그", window, cx);
9475 state.replace_text_in_range(Some(3..4), "글", window, cx);
9476 assert_eq!(state.value(), "é 한글");
9477 assert_eq!(state.undo_manager.undo_count(), 1);
9478
9479 state.undo(&Undo, window, cx);
9480 assert_eq!(state.value(), "é ");
9481 assert_eq!(state.selected_range(), 3..3);
9482
9483 state.redo(&Redo, window, cx);
9484 assert_eq!(state.value(), "é 한글");
9485 assert_eq!(state.selected_range(), 9..9);
9486 });
9487 });
9488 }
9489
9490 #[gpui::test]
9491 fn test_undo_manager_rewrite_after_cursor_movement_is_separate(cx: &mut TestAppContext) {
9492 let input_view = InputView::build(cx, |state| state);
9493 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
9494 let input = input_view.input;
9495
9496 cx.update(|window, cx| {
9497 input.update(cx, |state, cx| {
9498 state.replace_text_in_range(None, "c", window, cx);
9499 state.replace_text_in_range(None, "e", window, cx);
9500 state.left(&MoveLeft, window, cx);
9501 state.right(&MoveRight, window, cx);
9502 state.replace_text_in_range(Some(1..2), "é", window, cx);
9503 assert_eq!(state.value(), "cé");
9504
9505 state.undo(&Undo, window, cx);
9506 assert_eq!(state.value(), "ce");
9507 state.undo(&Undo, window, cx);
9508 assert_eq!(state.value(), "");
9509 });
9510 });
9511 }
9512
9513 #[gpui::test]
9514 fn test_undo_manager_selection_restored_by_undo_and_redo(cx: &mut TestAppContext) {
9515 let input_view = InputView::build(cx, |state| state);
9516 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
9517 let input = input_view.input;
9518
9519 cx.update(|window, cx| {
9520 input.update(cx, |state, cx| {
9521 state.set_value("abc", window, cx);
9522 state.set_selected_range(1..2, cx);
9523 state.replace_text_in_range(None, "X", window, cx);
9524
9525 state.undo(&Undo, window, cx);
9526 assert_eq!(state.value(), "abc");
9527 assert_eq!(state.selected_range(), 1..2);
9528
9529 state.redo(&Redo, window, cx);
9530 assert_eq!(state.value(), "aXc");
9531 assert_eq!(state.selected_range(), 2..2);
9532 });
9533 });
9534 }
9535
9536 #[gpui::test]
9537 fn test_undo_manager_forward_delete_restores_cursor(cx: &mut TestAppContext) {
9538 let input_view = InputView::build(cx, |state| state);
9539 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
9540 let input = input_view.input;
9541
9542 cx.update(|window, cx| {
9543 input.update(cx, |state, cx| {
9544 state.set_value("abc", window, cx);
9545 state.set_selected_range(1..1, cx);
9546 state.delete(&Delete, window, cx);
9547
9548 state.undo(&Undo, window, cx);
9549 assert_eq!(state.value(), "abc");
9550 assert_eq!(state.selected_range(), 1..1);
9551 });
9552 });
9553 }
9554
9555 #[gpui::test]
9556 fn test_undo_manager_selection_movement_preserves_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.replace_text_in_range(None, "ab", window, cx);
9564 state.undo(&Undo, window, cx);
9565 state.set_selected_range(0..0, cx);
9566 state.redo(&Redo, window, cx);
9567 assert_eq!(state.value(), "ab");
9568
9569 state.undo(&Undo, window, cx);
9570 state.replace_text_in_range(None, "x", window, cx);
9571 state.redo(&Redo, window, cx);
9572 assert_eq!(state.value(), "x");
9573 });
9574 });
9575 }
9576
9577 #[gpui::test]
9578 fn test_undo_manager_noop_edit_preserves_redo(cx: &mut TestAppContext) {
9579 let input_view = InputView::build(cx, |state| state);
9580 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
9581 let input = input_view.input;
9582
9583 cx.update(|window, cx| {
9584 input.update(cx, |state, cx| {
9585 state.replace_text_in_range(None, "a", window, cx);
9586 state.move_to(0, None, cx);
9587 state.replace_text_in_range(None, "", window, cx);
9588 state.undo(&Undo, window, cx);
9589 assert_eq!(state.value(), "");
9590 state.redo(&Redo, window, cx);
9591 assert_eq!(state.value(), "a");
9592 assert_eq!(state.cursor(), 1);
9593 });
9594 });
9595 }
9596
9597 #[gpui::test]
9598 fn test_cursor_round_trip_stops_typing_coalescing(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, "a", window, cx);
9606 state.left(&MoveLeft, window, cx);
9607 state.right(&MoveRight, window, cx);
9608 state.replace_text_in_range(None, "b", window, cx);
9609
9610 state.undo(&Undo, window, cx);
9611 assert_eq!(state.value(), "a");
9612 state.undo(&Undo, window, cx);
9613 assert_eq!(state.value(), "");
9614 });
9615 });
9616 }
9617
9618 #[gpui::test]
9619 fn test_undo_manager_noop_edit_breaks_coalescing_without_clearing_history(
9620 cx: &mut TestAppContext,
9621 ) {
9622 let input_view = InputView::build(cx, |state| state);
9623 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
9624 let input = input_view.input;
9625
9626 cx.update(|window, cx| {
9627 input.update(cx, |state, cx| {
9628 state.replace_text_in_range(None, "alpha", window, cx);
9629 state.replace_text_in_range(None, "", window, cx);
9630 state.replace_text_in_range(None, "beta", window, cx);
9631 assert_eq!(state.value(), "alphabeta");
9632
9633 state.undo(&Undo, window, cx);
9634 assert_eq!(state.value(), "alpha");
9635 state.undo(&Undo, window, cx);
9636 assert_eq!(state.value(), "");
9637 });
9638 });
9639 }
9640
9641 #[gpui::test]
9642 fn test_undo_manager_masked_redo_restores_actual_cursor(cx: &mut TestAppContext) {
9643 let input_view = InputView::build(cx, |state| {
9644 state.mask_pattern(MaskPattern::Number {
9645 separator: Some(','),
9646 fraction: None,
9647 })
9648 });
9649 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
9650 let input = input_view.input;
9651
9652 cx.update(|window, cx| {
9653 input.update(cx, |state, cx| {
9654 state.set_value("12345", window, cx);
9655 state.set_selected_range(2..2, cx);
9656 state.replace_text_in_range(None, "9", window, cx);
9657 let selection_after_edit = state.selected_range();
9658 assert_ne!(selection_after_edit.end, state.value().len());
9659
9660 state.undo(&Undo, window, cx);
9661 state.redo(&Redo, window, cx);
9662 assert_eq!(state.selected_range(), selection_after_edit);
9663 });
9664 });
9665 }
9666
9667 #[gpui::test]
9673 fn test_unfold_at(cx: &mut TestAppContext) {
9674 use crate::input::{FoldRange, Position};
9675
9676 let view = InputView::<EditorMode>::new(cx);
9677 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9678 let input = view.input;
9679
9680 cx.update(|window, cx| {
9683 input.update(cx, |state, cx| {
9684 state.set_value("a\nb\nc\nd\ne\nf\ng\nh\ni\nj\nk\nl", window, cx);
9685 state.apply_highlighter_fold_candidates(
9686 vec![
9687 FoldRange::new(0, 5),
9688 FoldRange::new(2, 4),
9689 FoldRange::new(7, 10),
9690 ],
9691 cx,
9692 );
9693 state.display_map.set_folded(0, true);
9694 state.display_map.set_folded(2, true);
9695 state.display_map.set_folded(7, true);
9696 });
9697 });
9698
9699 for line in [0, 5] {
9702 cx.update(|_, cx| {
9703 input.update(cx, |state, cx| {
9704 assert!(!state.display_map.is_buffer_line_hidden(line));
9705 assert!(!state.unfold_at(Position::new(line as u32, 0), cx));
9706 });
9707 input.read_with(cx, |state, _| {
9708 assert!(state.display_map.is_folded_at(0));
9709 assert!(state.display_map.is_folded_at(2));
9710 assert!(state.display_map.is_folded_at(7));
9711 });
9712 });
9713 }
9714
9715 cx.update(|_, cx| {
9718 input.update(cx, |state, cx| {
9719 assert!(state.unfold_at(Position::new(3, 0), cx));
9720 });
9721 input.read_with(cx, |state, _| {
9722 assert!(!state.display_map.is_buffer_line_hidden(3));
9723 assert!(!state.display_map.is_folded_at(0));
9724 assert!(!state.display_map.is_folded_at(2));
9725 assert!(state.display_map.is_folded_at(7));
9726 assert!(state.display_map.is_fold_candidate(0));
9728 assert!(state.display_map.is_fold_candidate(2));
9729 });
9730 });
9731
9732 cx.update(|_, cx| {
9734 input.update(cx, |state, cx| {
9735 assert!(!state.unfold_at(Position::new(3, 0), cx));
9736 });
9737 });
9738 }
9739
9740 #[gpui::test]
9745 fn test_blur_keeps_decorations(cx: &mut TestAppContext) {
9746 let view = InputView::<EditorMode>::new(cx);
9747 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9748 let input = view.input;
9749
9750 cx.update(|window, cx| {
9751 input.update(cx, |state, cx| {
9752 state.set_value("select 1", window, cx);
9753 let _collection = state.create_decorations_collection(
9754 vec![crate::input::TextDecoration::new(
9755 0..6,
9756 gpui::HighlightStyle {
9757 font_weight: Some(gpui::FontWeight::BOLD),
9758 ..Default::default()
9759 },
9760 )],
9761 cx,
9762 );
9763 state.present_hover(
9764 0..6,
9765 lsp_types::Hover {
9766 contents: lsp_types::HoverContents::Scalar(
9767 lsp_types::MarkedString::String("docs".into()),
9768 ),
9769 range: None,
9770 },
9771 cx,
9772 );
9773 assert!(state.hover_popover().is_some());
9774
9775 state.on_blur(window, cx);
9776
9777 assert!(
9778 state.hover_popover().is_none(),
9779 "blur should hide the hover popover"
9780 );
9781 let decorations = state.extras.decoration_layers();
9782 assert!(
9783 decorations.iter().any(|layer| !layer.is_empty()),
9784 "blur must not discard decorations"
9785 );
9786 });
9787 });
9788 }
9789
9790 #[gpui::test]
9795 fn test_kind_does_not_follow_the_row_count(cx: &mut TestAppContext) {
9796 let view = InputView::build_textarea(cx, |state| state.auto_grow(1, 1));
9797 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9798 view.input.read_with(&mut cx, |state, _| {
9799 assert!(state.is_multi_line());
9800 assert!(!state.is_single_line());
9801 assert!(!state.is_code_editor());
9802 });
9803 }
9804
9805 #[gpui::test]
9806 fn test_textarea_prewrap_prepares_content_rows_before_layout(cx: &mut TestAppContext) {
9807 cx.update(crate::init);
9808 for (value, expected_rows) in [("", 1), ("one\ntwo", 2)] {
9809 cx.open_window(size(px(400.), px(100.)), |window, cx| {
9810 let font = window.text_style().font();
9811 cx.new(|cx| {
9812 let mut state = crate::input::TextareaState::new(window, cx)
9813 .rows(12)
9814 .default_value(value);
9815 assert_eq!(state.display_map().wrap_row_count(), 0);
9816 state.prewrap(font, px(14.), px(120.), cx);
9817 assert_eq!(state.display_map().wrap_row_count(), expected_rows);
9818 assert_eq!(state.value(), value);
9819 state
9820 });
9821 gpui::EmptyView
9822 });
9823 }
9824 }
9825
9826 #[gpui::test]
9827 fn test_textarea_prewrap_respects_soft_wrap_before_layout(cx: &mut TestAppContext) {
9828 cx.update(crate::init);
9829 let value = "select id, name, email, phone from users where active\nlimit 10";
9830 for soft_wrap in [true, false] {
9831 cx.open_window(size(px(400.), px(100.)), |window, cx| {
9832 let font = window.text_style().font();
9833 cx.new(|cx| {
9834 let mut state = crate::input::TextareaState::new(window, cx)
9835 .rows(1)
9836 .soft_wrap(soft_wrap)
9837 .default_value(value);
9838 state.prewrap(font, px(14.), px(120.), cx);
9839 let rows = state.display_map().wrap_row_count();
9840 if soft_wrap {
9841 assert!(rows > 2, "the first logical line must wrap");
9842 } else {
9843 assert_eq!(rows, 2);
9844 }
9845 assert_eq!(state.value(), value);
9846 state
9847 });
9848 gpui::EmptyView
9849 });
9850 }
9851 }
9852
9853 #[gpui::test]
9854 fn test_prewrap_counts_wrapped_rows_before_the_first_paint(cx: &mut TestAppContext) {
9855 cx.update(crate::init);
9856 let text = "select id, name, email, phone from users where active\nlimit 10";
9857 for (soft_wrap, wraps) in [(true, true), (false, false)] {
9858 let mut input = None;
9859 cx.open_window(size(px(400.), px(100.)), |window, cx| {
9860 let font = window.text_style().font();
9861 let editor = cx.new(|cx| {
9862 let mut state = crate::input::EditorState::new(window, cx)
9863 .soft_wrap(soft_wrap)
9864 .default_value(text);
9865 state.prewrap(font, px(14.), px(120.), cx);
9866 state
9867 });
9868 input = Some(editor);
9869 gpui::EmptyView
9870 });
9871 let rows = input
9872 .unwrap()
9873 .read_with(cx, |state, _| state.display_map().wrap_row_count());
9874 assert_eq!(rows > 2, wraps, "soft_wrap({soft_wrap}): {rows} rows");
9876 if !wraps {
9877 assert_eq!(rows, 2);
9878 }
9879 }
9880 }
9881
9882 #[gpui::test]
9883 fn test_prewrap_uses_the_configured_tab_stops(cx: &mut TestAppContext) {
9884 cx.update(crate::init);
9885 cx.open_window(size(px(400.), px(100.)), |window, cx| {
9886 let font = window.text_style().font();
9887 let font_size = px(14.);
9888 let font_id = window.text_system().resolve_font(&font);
9889 let space_width = window.text_system().layout_width(font_id, font_size, ' ');
9890 cx.new(|cx| {
9891 let mut state = crate::input::EditorState::new(window, cx)
9892 .tab_size(TabSize {
9893 tab_size: 4,
9894 hard_tabs: true,
9895 })
9896 .default_value("\t\t");
9897 for (tab_size, expected_rows) in [(4, 2), (2, 1), (8, 2)] {
9898 state.set_tab_size(
9899 TabSize {
9900 tab_size,
9901 hard_tabs: true,
9902 },
9903 cx,
9904 );
9905 state.prewrap(font.clone(), font_size, space_width * 5. + RIGHT_MARGIN, cx);
9906 assert_eq!(state.display_map().wrap_row_count(), expected_rows);
9907 }
9908 state
9909 });
9910 gpui::EmptyView
9911 });
9912 }
9913
9914 #[gpui::test]
9917 fn test_prewrap_wraps_with_the_wrapping_indent(cx: &mut TestAppContext) {
9918 cx.update(crate::init);
9919 let text = " select id, name, email, phone from users where active";
9920 let mut rows = Vec::new();
9921 for indent in [WrappingIndent::Same, WrappingIndent::None] {
9922 let mut input = None;
9923 cx.open_window(size(px(400.), px(100.)), |window, cx| {
9924 let font = window.text_style().font();
9925 input = Some(cx.new(|cx| {
9926 let mut state = crate::input::EditorState::new(window, cx)
9927 .wrapping_indent(indent)
9928 .default_value(text);
9929 state.prewrap(font, px(14.), px(200.), cx);
9930 state
9931 }));
9932 gpui::EmptyView
9933 });
9934 rows.push(
9935 input
9936 .unwrap()
9937 .read_with(cx, |state, _| state.display_map().wrap_row_count()),
9938 );
9939 }
9940 assert!(rows[0] > rows[1], "rows with Same, None: {rows:?}");
9941 }
9942
9943 #[gpui::test]
9945 fn test_prewrap_does_nothing_once_laid_out(cx: &mut TestAppContext) {
9946 struct Frame(Entity<crate::input::EditorState>);
9947 impl Render for Frame {
9948 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
9949 crate::input::InputBase::new("frame")
9950 .h(px(100.))
9951 .child(crate::input::Editor::new(&self.0))
9952 }
9953 }
9954
9955 cx.update(crate::init);
9956 let text = "select id, name, email, phone from users where active\nlimit 10";
9957 let mut input = None;
9958 let window = cx.open_window(size(px(400.), px(100.)), |window, cx| {
9959 let state = cx.new(|cx| crate::input::EditorState::new(window, cx).default_value(text));
9960 input = Some(state.clone());
9961 Frame(state)
9962 });
9963 let input = input.unwrap();
9964 let mut cx = VisualTestContext::from_window(window.into(), cx);
9965 cx.update(|window, cx| window.draw(cx).clear(cx));
9966
9967 let laid_out = input.read_with(&cx, |state, _| state.display_map().wrap_row_count());
9968 assert!(laid_out >= 2, "laid out to {laid_out} rows");
9969 let font = cx.update(|window, _| window.text_style().font());
9970 input.update(&mut cx, |state, cx| {
9971 state.prewrap(font, px(14.), px(60.), cx)
9972 });
9973 assert_eq!(
9974 input.read_with(&cx, |state, _| state.display_map().wrap_row_count()),
9975 laid_out
9976 );
9977 }
9978
9979 #[gpui::test]
9984 fn test_soft_wrap_is_enabled_by_default(cx: &mut TestAppContext) {
9985 let textarea = InputView::build_textarea(cx, |state| state);
9986 let mut textarea_cx = VisualTestContext::from_window(textarea.window_handle.into(), cx);
9987 textarea
9988 .input
9989 .read_with(&mut textarea_cx, |state, _| assert!(state.soft_wrap));
9990
9991 let editor = InputView::<EditorMode>::new(cx);
9992 let mut editor_cx = VisualTestContext::from_window(editor.window_handle.into(), cx);
9993 editor
9994 .input
9995 .read_with(&mut editor_cx, |state, _| assert!(state.soft_wrap));
9996 }
9997
9998 fn parse_cursor_spec(input: &str) -> (String, Vec<usize>) {
10002 let mut full_text = String::new();
10003 let mut cursor_offsets = Vec::new();
10004 let non_empty_lines: Vec<&str> = input.lines().filter(|l| !l.is_empty()).collect();
10005
10006 for (line_idx, line) in non_empty_lines.iter().enumerate() {
10007 let mut positions = Vec::new();
10008 let mut text = String::new();
10009 for ch in line.chars() {
10010 if ch == '|' {
10011 positions.push(text.len());
10012 } else {
10013 text.push(ch);
10014 }
10015 }
10016
10017 if line_idx > 0 {
10018 full_text.push('\n');
10019 }
10020 let line_start = full_text.len();
10021 for pos in positions {
10022 cursor_offsets.push(line_start + pos);
10023 }
10024 full_text.push_str(&text);
10025 }
10026 full_text.push('\n');
10027
10028 (full_text, cursor_offsets)
10029 }
10030
10031 fn multi_line(cx: &mut TestAppContext) -> InputView<TextareaMode> {
10033 InputView::build_textarea(cx, |state| state)
10034 }
10035
10036 fn setup_cursors<M: InputModeKind>(
10038 cx: &mut VisualTestContext,
10039 input: &Entity<InputBaseState<M>>,
10040 spec: &str,
10041 ) {
10042 let (full_text, offsets) = parse_cursor_spec(spec);
10043 cx.update(|window, cx| {
10044 input.update(cx, |state, cx| {
10045 state.set_value(&full_text, window, cx);
10046 let selections = offsets
10047 .into_iter()
10048 .map(|offset| {
10049 CursorSelection::new(state.selections.generate_id(), offset, offset)
10050 })
10051 .collect();
10052 state.selections.replace_all(selections);
10053 cx.notify();
10054 });
10055 });
10056 }
10057
10058 #[track_caller]
10060 fn assert_cursors<M: InputModeKind>(
10061 cx: &mut VisualTestContext,
10062 input: &Entity<InputBaseState<M>>,
10063 spec: &str,
10064 ) {
10065 let (expected_text, mut expected_cursors) = parse_cursor_spec(spec);
10066 expected_cursors.sort();
10067
10068 let (actual_text, mut actual_cursors) = input.read_with(cx, |state, _| {
10069 (
10070 state.text.to_string(),
10071 state
10072 .selections
10073 .iter()
10074 .map(|s| s.cursor_offset())
10075 .collect::<Vec<_>>(),
10076 )
10077 });
10078 actual_cursors.sort();
10079
10080 assert_eq!(
10081 actual_text, expected_text,
10082 "Text mismatch:\nExpected: {expected_text:?}\nActual: {actual_text:?}"
10083 );
10084 assert_eq!(
10085 actual_cursors, expected_cursors,
10086 "Cursor mismatch:\nExpected: {expected_cursors:?}\nActual: {actual_cursors:?}"
10087 );
10088 }
10089
10090 #[gpui::test]
10091 fn test_alt_drag_selects_a_block_and_replaces_each_row(cx: &mut TestAppContext) {
10092 cx.update(crate::init);
10093 let view = InputView::<EditorMode>::new(cx);
10094 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10095 for modifiers in [
10096 gpui::Modifiers {
10097 alt: true,
10098 ..Default::default()
10099 },
10100 gpui::Modifiers {
10101 alt: true,
10102 shift: true,
10103 ..Default::default()
10104 },
10105 #[cfg(target_os = "linux")]
10106 gpui::Modifiers {
10107 alt: true,
10108 control: true,
10109 ..Default::default()
10110 },
10111 ] {
10112 setup_cursors(&mut cx, &view.input, "|abcd\nabcd\nabcd");
10113 cx.update(|window, cx| {
10114 view.input.update(cx, |state, cx| state.focus(window, cx));
10115 });
10116 let (start, end) = view.input.read_with(&cx, |state, _| {
10117 let layout = state.last_layout.as_ref().unwrap();
10118 let origin = state.last_bounds.unwrap().origin;
10119 let position = |row: usize, col| {
10120 let local = layout.lines[row]
10121 .position_for_index(col, layout, false)
10122 .unwrap();
10123 origin
10124 + point(
10125 layout.line_number_width + local.x,
10126 layout.line_height * (row as f32 + 0.5),
10127 )
10128 };
10129 (position(0, 1), position(2, 3))
10130 });
10131 let end = point(end.x - px(0.5), end.y);
10134 cx.update(|_, cx| {
10136 view.input.update(cx, |state, _| {
10137 state.extras.hover_definition.update(
10138 0..4,
10139 vec![lsp_types::LocationLink {
10140 origin_selection_range: None,
10141 target_uri: "file:///tmp/column-selection.rs".parse().unwrap(),
10142 target_range: Default::default(),
10143 target_selection_range: Default::default(),
10144 }],
10145 );
10146 });
10147 });
10148 cx.simulate_mouse_down(start, MouseButton::Left, modifiers);
10149 cx.simulate_mouse_move(end, MouseButton::Left, modifiers);
10150 cx.simulate_mouse_up(end, MouseButton::Left, modifiers);
10151 view.input.read_with(&cx, |state, _| {
10152 let ranges: Vec<_> = state
10153 .selections
10154 .iter()
10155 .map(|sel| sel.start..sel.end)
10156 .collect();
10157 assert_eq!(ranges, vec![1..3, 6..8, 11..13]);
10158 });
10159 cx.simulate_mouse_move(start, None, modifiers);
10161 cx.simulate_keystrokes("x");
10162 assert_cursors(&mut cx, &view.input, "ax|d\nax|d\nax|d");
10163 }
10164 }
10165
10166 #[gpui::test]
10167 fn test_alt_drag_extends_upward_from_an_existing_cursor(cx: &mut TestAppContext) {
10168 cx.update(crate::init);
10169 let view = InputView::<EditorMode>::new(cx);
10170 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10171 setup_cursors(&mut cx, &view.input, "abcd\nabcd\na|bcd");
10172 cx.update(|window, cx| {
10173 view.input.update(cx, |state, cx| state.focus(window, cx));
10174 });
10175 let (start, end) = view.input.read_with(&cx, |state, _| {
10176 let layout = state.last_layout.as_ref().unwrap();
10177 let origin = state.last_bounds.unwrap().origin;
10178 let local = layout.lines[0]
10179 .position_for_index(1, layout, false)
10180 .unwrap();
10181 let x = layout.line_number_width + local.x;
10182 (
10183 origin + point(x, layout.line_height * 2.5),
10184 origin + point(x, layout.line_height * 0.5),
10185 )
10186 });
10187 let modifiers = gpui::Modifiers {
10188 alt: true,
10189 ..Default::default()
10190 };
10191 cx.simulate_mouse_down(start, MouseButton::Left, modifiers);
10192 cx.simulate_mouse_move(end, MouseButton::Left, modifiers);
10193 cx.simulate_mouse_up(end, MouseButton::Left, modifiers);
10194 assert_cursors(&mut cx, &view.input, "a|bcd\na|bcd\na|bcd");
10195 }
10196
10197 #[gpui::test]
10198 fn test_alt_drag_over_a_short_row_keeps_the_block_width(cx: &mut TestAppContext) {
10199 cx.update(crate::init);
10200 let view = InputView::<EditorMode>::new(cx);
10201 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10202 setup_cursors(&mut cx, &view.input, "abcdef\nab\nabcdef");
10203 cx.update(|window, cx| {
10204 view.input.update(cx, |state, cx| state.focus(window, cx));
10205 });
10206 let (start, end) = view.input.read_with(&cx, |state, _| {
10209 let layout = state.last_layout.as_ref().unwrap();
10210 let origin = state.last_bounds.unwrap().origin;
10211 let x_of = |index| {
10212 layout.line_number_width
10213 + layout.lines[0]
10214 .position_for_index(index, layout, false)
10215 .unwrap()
10216 .x
10217 };
10218 (
10219 origin + point(x_of(1), layout.line_height * 0.5),
10220 origin + point(x_of(5), layout.line_height * 1.5),
10221 )
10222 });
10223 let modifiers = gpui::Modifiers {
10224 alt: true,
10225 shift: true,
10226 ..Default::default()
10227 };
10228 cx.simulate_mouse_down(start, MouseButton::Left, modifiers);
10229 cx.simulate_mouse_move(end, MouseButton::Left, modifiers);
10230 cx.simulate_mouse_up(end, MouseButton::Left, modifiers);
10231
10232 view.input.read_with(&cx, |state, _| {
10234 let ranges: Vec<_> = state.selections.iter().map(|s| (s.start, s.end)).collect();
10235 assert_eq!(ranges, vec![(1, 5), (8, 9)]);
10236 });
10237 }
10238
10239 #[gpui::test]
10240 fn test_alt_mouse_release_outside_editor_ends_column_selection(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, "|abcd\nabcd");
10245 let position = view.input.read_with(&cx, |state, _| {
10246 let layout = state.last_layout.as_ref().unwrap();
10247 state.last_bounds.unwrap().origin
10248 + point(layout.line_number_width + px(2.), layout.line_height * 0.5)
10249 });
10250 let modifiers = gpui::Modifiers {
10251 alt: true,
10252 ..Default::default()
10253 };
10254 cx.simulate_mouse_down(position, MouseButton::Left, modifiers);
10255 cx.simulate_mouse_up(point(px(-100.), px(-100.)), MouseButton::Left, modifiers);
10256 view.input.read_with(&cx, |state, _| {
10257 assert!(!state.selecting);
10258 assert!(state.column_select_start.is_none());
10259 });
10260 }
10261
10262 #[gpui::test]
10263 fn test_consumed_keystrokes_keep_cursor_visible(cx: &mut TestAppContext) {
10264 let view = InputView::<EditorMode>::new(cx);
10265 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10266 setup_cursors(&mut cx, &view.input, "a|b");
10267 cx.update(|window, cx| {
10268 view.input.update(cx, |state, cx| {
10269 state.focus(window, cx);
10270 state.pause_blink_cursor(cx);
10271 });
10272 });
10273 cx.run_until_parked();
10274 cx.executor()
10275 .advance_clock(std::time::Duration::from_millis(300));
10276 cx.run_until_parked();
10277 view.input.read_with(&cx, |state, cx| {
10278 assert!(!state.blink_cursor.read(cx).visible());
10279 });
10280 for _ in 0..5 {
10282 #[cfg(target_os = "macos")]
10283 cx.simulate_keystrokes("cmd-c");
10284 #[cfg(not(target_os = "macos"))]
10285 cx.simulate_keystrokes("ctrl-c");
10286 cx.run_until_parked();
10287 cx.executor()
10288 .advance_clock(std::time::Duration::from_millis(200));
10289 cx.run_until_parked();
10290 view.input.read_with(&cx, |state, cx| {
10291 assert!(state.blink_cursor.read(cx).visible());
10292 });
10293 }
10294 }
10295
10296 #[gpui::test]
10297 fn test_mouse_down_shows_cursor_like_a_cursor_move(cx: &mut TestAppContext) {
10298 cx.update(crate::init);
10299 let view = InputView::<EditorMode>::new(cx);
10300 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10301 setup_cursors(&mut cx, &view.input, "abcd|efgh");
10302 let position = |offset: Pixels, cx: &mut VisualTestContext| {
10303 view.input.read_with(cx, |state, _| {
10304 let layout = state.last_layout.as_ref().unwrap();
10305 state.last_bounds.unwrap().origin
10306 + point(layout.line_number_width + offset, layout.line_height * 0.5)
10307 })
10308 };
10309 let visible = |cx: &mut VisualTestContext| {
10310 view.input
10311 .read_with(cx, |state, cx| state.blink_cursor.read(cx).visible())
10312 };
10313
10314 let first = position(px(2.), &mut cx);
10317 cx.simulate_mouse_down(first, MouseButton::Left, Default::default());
10318 assert!(visible(&mut cx));
10319 cx.simulate_mouse_up(first, MouseButton::Left, Default::default());
10320 cx.run_until_parked();
10321
10322 cx.executor()
10325 .advance_clock(std::time::Duration::from_millis(400));
10326 cx.run_until_parked();
10327 assert!(!visible(&mut cx));
10328 let second = position(px(30.), &mut cx);
10329 cx.simulate_mouse_down(second, MouseButton::Left, Default::default());
10330 assert!(visible(&mut cx));
10331 cx.executor()
10332 .advance_clock(std::time::Duration::from_millis(250));
10333 cx.run_until_parked();
10334 assert!(visible(&mut cx));
10335 cx.simulate_mouse_up(second, MouseButton::Left, Default::default());
10336 }
10337
10338 #[gpui::test]
10339 fn test_multi_cursor_actions_reveal_hidden_carets(cx: &mut TestAppContext) {
10340 let view = InputView::<EditorMode>::new(cx);
10341 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10342 setup_cursors(&mut cx, &view.input, "ab\na|b\nab");
10343 for action in 0..6 {
10346 cx.update(|_, cx| {
10347 view.input.update(cx, |state, cx| {
10348 state.blink_cursor = cx.new(BlinkCursor::new);
10349 state.blink_cursor.update(cx, |cursor, cx| cursor.start(cx));
10350 });
10351 });
10352 cx.run_until_parked();
10353 cx.executor()
10354 .advance_clock(std::time::Duration::from_millis(500));
10355 cx.run_until_parked();
10356 view.input.read_with(&cx, |state, cx| {
10357 assert!(!state.blink_cursor.read(cx).visible(), "action {action}");
10358 });
10359
10360 cx.update(|window, cx| {
10361 view.input.update(cx, |state, cx| {
10362 match action {
10363 0 => state.add_cursor_above(&AddCursorAbove, window, cx),
10364 1 => state.add_cursor_below(&AddCursorBelow, window, cx),
10365 2 => state.select_up(&SelectUp, window, cx),
10366 3 => state.select_down(&SelectDown, window, cx),
10367 4 => state.replace_text_in_range(None, "x", window, cx),
10368 _ => state.backspace(&Backspace, window, cx),
10369 }
10370 assert!(state.blink_cursor.read(cx).visible(), "action {action}");
10371 });
10372 });
10373 }
10374 }
10375
10376 #[gpui::test]
10377 fn test_multi_cursor_keyboard_dispatch(cx: &mut TestAppContext) {
10378 let view = InputView::<EditorMode>::new(cx);
10379 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10380 setup_cursors(&mut cx, &view.input, "ab\na|b\nab");
10381 cx.update(|window, cx| {
10382 view.input.update(cx, |state, cx| state.focus(window, cx));
10383 });
10384 #[cfg(target_os = "macos")]
10385 cx.simulate_keystrokes("cmd-alt-up");
10386 #[cfg(target_os = "windows")]
10387 cx.simulate_keystrokes("ctrl-alt-up");
10388 #[cfg(not(any(target_os = "macos", target_os = "windows")))]
10389 cx.simulate_keystrokes("alt-shift-up");
10390 view.input
10391 .read_with(&cx, |state, _| assert_eq!(state.selections.len(), 2));
10392 #[cfg(target_os = "macos")]
10393 cx.simulate_keystrokes("cmd-alt-down");
10394 #[cfg(target_os = "windows")]
10395 cx.simulate_keystrokes("ctrl-alt-down");
10396 #[cfg(not(any(target_os = "macos", target_os = "windows")))]
10397 cx.simulate_keystrokes("alt-shift-down");
10398 view.input
10399 .read_with(&cx, |state, _| assert_eq!(state.selections.len(), 3));
10400 cx.simulate_keystrokes("x");
10401 assert_cursors(&mut cx, &view.input, "ax|b\nax|b\nax|b");
10402 }
10403
10404 #[gpui::test]
10405 fn test_multi_cursor_platform_word_selection_dispatch(cx: &mut TestAppContext) {
10406 let view = InputView::<EditorMode>::new(cx);
10407 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10408 setup_cursors(&mut cx, &view.input, "one |two\none |two");
10409 cx.update(|window, cx| {
10410 view.input.update(cx, |state, cx| state.focus(window, cx));
10411 });
10412 #[cfg(any(target_os = "macos", target_os = "linux"))]
10413 cx.simulate_keystrokes("alt-shift-right");
10414 #[cfg(not(any(target_os = "macos", target_os = "linux")))]
10415 cx.simulate_keystrokes("ctrl-shift-right");
10416 cx.simulate_keystrokes("x");
10417 assert_cursors(&mut cx, &view.input, "one x|\none x|");
10418 setup_cursors(&mut cx, &view.input, "one two|\none two|");
10419 #[cfg(any(target_os = "macos", target_os = "linux"))]
10420 cx.simulate_keystrokes("alt-shift-left");
10421 #[cfg(not(any(target_os = "macos", target_os = "linux")))]
10422 cx.simulate_keystrokes("ctrl-shift-left");
10423 cx.simulate_keystrokes("x");
10424 assert_cursors(&mut cx, &view.input, "one x|\none x|");
10425 }
10426
10427 #[cfg(not(target_os = "macos"))]
10428 #[gpui::test]
10429 fn test_multi_cursor_horizontal_selection_dispatch(cx: &mut TestAppContext) {
10430 let view = InputView::<EditorMode>::new(cx);
10431 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10432 setup_cursors(&mut cx, &view.input, "ab\na|b");
10433 cx.update(|window, cx| {
10434 view.input.update(cx, |state, cx| state.focus(window, cx));
10435 });
10436 #[cfg(target_os = "windows")]
10437 cx.simulate_keystrokes("ctrl-alt-up");
10438 #[cfg(not(target_os = "windows"))]
10439 cx.simulate_keystrokes("alt-shift-up");
10440 #[cfg(target_os = "linux")]
10441 cx.simulate_keystrokes("shift-right");
10442 #[cfg(not(target_os = "linux"))]
10443 cx.simulate_keystrokes("alt-shift-right");
10444 view.input.read_with(&cx, |state, _| {
10445 assert_eq!(
10446 state
10447 .selections
10448 .iter()
10449 .map(|s| s.start..s.end)
10450 .collect::<Vec<_>>(),
10451 vec![4..5, 1..2]
10452 );
10453 });
10454 #[cfg(target_os = "linux")]
10455 cx.simulate_keystrokes("shift-left shift-left");
10456 #[cfg(not(target_os = "linux"))]
10457 cx.simulate_keystrokes("alt-shift-left alt-shift-left");
10458 view.input.read_with(&cx, |state, _| {
10459 assert_eq!(
10460 state
10461 .selections
10462 .iter()
10463 .map(|s| s.start..s.end)
10464 .collect::<Vec<_>>(),
10465 vec![3..4, 0..1]
10466 );
10467 });
10468 cx.simulate_keystrokes("x");
10469 assert_cursors(&mut cx, &view.input, "x|b\nx|b");
10470 }
10471
10472 #[gpui::test]
10473 fn test_multi_cursor_alt_click_dispatch(cx: &mut TestAppContext) {
10474 cx.update(crate::init);
10475 let view = InputView::<EditorMode>::new(cx);
10476 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10477 setup_cursors(&mut cx, &view.input, "a|b\nab\nab");
10478 cx.update(|window, cx| {
10479 view.input.update(cx, |state, cx| state.focus(window, cx));
10480 });
10481 let position = view.input.read_with(&cx, |state, _| {
10482 let bounds = state.last_bounds.unwrap();
10483 let layout = state.last_layout.as_ref().unwrap();
10484 bounds.origin + point(layout.line_number_width + px(2.), layout.line_height * 1.5)
10485 });
10486 cx.simulate_click(
10487 position,
10488 gpui::Modifiers {
10489 alt: true,
10490 ..Default::default()
10491 },
10492 );
10493 view.input
10494 .read_with(&cx, |state, _| assert_eq!(state.selections.len(), 2));
10495 }
10496
10497 #[gpui::test]
10498 fn test_add_selection(cx: &mut TestAppContext) {
10499 let view = InputView::<EditorMode>::new(cx);
10500 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10501 cx.update(|window, cx| {
10502 view.input.update(cx, |state, cx| {
10503 state.focus(window, cx);
10504 state.set_value("foo bar foo", window, cx);
10505 state.set_selected_range(0..3, cx);
10506 state.add_selection(8..11, cx);
10508 state.add_selection(0..3, cx);
10510 });
10511 });
10512 view.input.read_with(&cx, |state, _| {
10513 let mut ranges: Vec<Range<usize>> =
10514 state.selections.iter().map(|s| s.start..s.end).collect();
10515 ranges.sort_by_key(|r| r.start);
10516 assert_eq!(ranges, vec![0..3, 8..11]);
10517 });
10518 cx.simulate_keystrokes("x");
10520 view.input.read_with(&cx, |state, _| {
10521 assert_eq!(state.text.to_string(), "x bar x")
10522 });
10523 }
10524
10525 #[gpui::test]
10526 fn test_add_selection_preserves_crlf_boundaries(cx: &mut TestAppContext) {
10527 let view = InputView::build_textarea(cx, |state| state.default_value("a\r\nb"));
10528 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10529 cx.update(|window, cx| {
10530 view.input.update(cx, |state, cx| {
10531 state.focus(window, cx);
10532 state.set_selected_range(4..4, cx);
10533 state.add_selection(2..2, cx);
10534 let ranges: Vec<_> = state.selections.iter().map(|s| s.start..s.end).collect();
10535 assert_eq!(ranges, vec![4..4, 1..1]);
10536 });
10537 });
10538 cx.simulate_keystrokes("x");
10539 view.input.read_with(&cx, |state, _| {
10540 assert_eq!(state.value(), "ax\r\nbx");
10541 });
10542 cx.update(|window, cx| {
10543 view.input.update(cx, |state, cx| {
10544 state.set_value("a\r\nb", window, cx);
10545 state.set_selected_range(4..4, cx);
10546 state.add_selection(2..3, cx);
10547 state.cut(&Cut, window, cx);
10548 assert_eq!(cx.read_from_clipboard().unwrap().text().unwrap(), "\r\n");
10549 assert_eq!(state.value(), "ab");
10550 });
10551 });
10552 }
10553
10554 #[gpui::test]
10555 fn test_add_selection_merges_overlaps_before_copy_and_cut(cx: &mut TestAppContext) {
10556 let view = InputView::<EditorMode>::new(cx);
10557 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10558 cx.update(|window, cx| {
10559 view.input.update(cx, |state, cx| {
10560 for additions in [vec![2..5], vec![0..5], vec![4..5, 2..4]] {
10563 state.set_value("abcdef", window, cx);
10564 state.set_selected_range(0..3, cx);
10565 let active_id = state.active_selection().id;
10566 for range in additions {
10567 state.add_selection(range, cx);
10568 }
10569 assert_eq!(state.active_selection().id, active_id);
10570 assert_eq!(state.selected_range(), 0..5);
10571 assert_eq!(state.selections.len(), 1);
10572 state.copy(&Copy, window, cx);
10573 assert_eq!(cx.read_from_clipboard().unwrap().text().unwrap(), "abcde");
10574 state.cut(&Cut, window, cx);
10575 assert_eq!(cx.read_from_clipboard().unwrap().text().unwrap(), "abcde");
10576 assert_eq!(state.value(), "f");
10577 state.undo(&Undo, window, cx);
10578 assert_eq!(state.value(), "abcdef");
10579 assert_eq!(state.selected_range(), 0..5);
10580 }
10581 });
10582 });
10583 }
10584
10585 #[gpui::test]
10586 fn test_add_selection_clears_inline_completion(cx: &mut TestAppContext) {
10587 use crate::input::CompletionProvider;
10588 use gpui::Task;
10589 use lsp_types::{
10590 CompletionContext, CompletionResponse, InlineCompletionContext, InlineCompletionItem,
10591 InlineCompletionResponse,
10592 };
10593 use std::time::Duration;
10594
10595 struct Provider(Rc<Cell<usize>>);
10596
10597 impl CompletionProvider for Provider {
10598 fn is_completion_trigger(&self, _: usize, _: &str, _: &mut App) -> bool {
10599 false
10600 }
10601
10602 fn completions(
10603 &self,
10604 _: &Rope,
10605 _: usize,
10606 _: CompletionContext,
10607 _: &mut Window,
10608 _: &mut App,
10609 ) -> Task<anyhow::Result<CompletionResponse>> {
10610 Task::ready(Ok(CompletionResponse::Array(vec![])))
10611 }
10612
10613 fn inline_completion(
10614 &self,
10615 _: &Rope,
10616 _: usize,
10617 _: InlineCompletionContext,
10618 _: &mut Window,
10619 _: &mut App,
10620 ) -> Task<anyhow::Result<InlineCompletionResponse>> {
10621 self.0.set(self.0.get() + 1);
10622 Task::ready(Ok(InlineCompletionResponse::Array(vec![])))
10623 }
10624 }
10625
10626 let view = InputView::<EditorMode>::new(cx);
10627 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10628 let calls = Rc::new(Cell::new(0));
10629 cx.update(|window, cx| {
10630 view.input.update(cx, |state, cx| {
10631 state.focus(window, cx);
10632 state.set_value("aa\naa", window, cx);
10633 state.set_selected_range(2..2, cx);
10634 state.extras.inline_completion.item = Some(InlineCompletionItem {
10635 insert_text: "suggestion".into(),
10636 filter_text: None,
10637 range: None,
10638 command: None,
10639 insert_text_format: None,
10640 });
10641 state.add_selection(5..5, cx);
10642 assert!(!state.has_inline_completion());
10643 state.indent_inline(&IndentInline, window, cx);
10644 assert_eq!(state.value(), "aa \naa ");
10645 assert_eq!(state.selections.len(), 2);
10646
10647 state.set_value("aa\naa", window, cx);
10650 state.set_selected_range(2..2, cx);
10651 state.extras.lsp.completion_provider = Some(Rc::new(Provider(calls.clone())));
10652 state.schedule_inline_completion(window, cx);
10653 });
10654 });
10655 cx.run_until_parked();
10656 cx.update(|_, cx| {
10657 view.input
10658 .update(cx, |state, cx| state.add_selection(5..5, cx));
10659 });
10660 cx.executor().advance_clock(Duration::from_secs(1));
10661 cx.run_until_parked();
10662 assert_eq!(calls.get(), 0);
10663 view.input.read_with(&cx, |state, _| {
10664 assert!(!state.has_inline_completion());
10665 assert_eq!(state.selections.len(), 2);
10666 assert_eq!(state.value(), "aa\naa");
10667 });
10668 }
10669
10670 #[gpui::test]
10671 fn test_word_delete_undo_restores_caret(cx: &mut TestAppContext) {
10672 let view = multi_line(cx);
10673 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10674 setup_cursors(&mut cx, &view.input, "hello|");
10675 cx.update(|window, cx| {
10676 view.input.update(cx, |state, cx| {
10677 state.delete_previous_word(&DeleteToPreviousWordStart, window, cx);
10678 state.undo(&Undo, window, cx);
10679 assert_eq!(state.selected_range(), 5..5);
10680 });
10681 });
10682 }
10683
10684 #[gpui::test]
10685 fn test_merged_delete_undo_restores_all_carets(cx: &mut TestAppContext) {
10686 let view = multi_line(cx);
10687 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10688 setup_cursors(&mut cx, &view.input, "a|b|c");
10689 cx.update(|window, cx| {
10690 view.input.update(cx, |state, cx| {
10691 state.backspace(&Backspace, window, cx);
10692 state.undo(&Undo, window, cx);
10693 });
10694 });
10695 assert_cursors(&mut cx, &view.input, "a|b|c");
10696 }
10697
10698 #[gpui::test]
10699 fn test_shift_end_respects_soft_wrap_end(cx: &mut TestAppContext) {
10700 let view = InputView::<EditorMode>::new(cx);
10701 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10702 cx.update(|window, cx| {
10703 view.input.update(cx, |state, cx| {
10704 state.set_value("abcdef ".repeat(30), window, cx);
10705 state.display_map.on_layout_changed(Some(px(60.)), cx);
10706 let line = state.display_map.line(0).unwrap();
10707 assert!(line.wrapped_lines.len() > 1);
10708 let boundary = line.wrapped_lines[0].end;
10709 state.move_to_with_affinity(boundary, None, true, cx);
10710 state.select_to_end_of_line(&SelectToEndOfLine, window, cx);
10711 assert_eq!(state.selected_range(), boundary..state.text.len());
10712 });
10713 });
10714 }
10715
10716 #[gpui::test]
10717 fn test_outdent_unindented_unicode_is_unchanged(cx: &mut TestAppContext) {
10718 let view = multi_line(cx);
10719 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10720 setup_cursors(&mut cx, &view.input, "|你好\n|世界");
10721 cx.update(|window, cx| {
10722 view.input.update(cx, |state, cx| {
10723 state.outdent(false, window, cx);
10724 state.outdent(true, window, cx);
10725 });
10726 });
10727 assert_cursors(&mut cx, &view.input, "|你好\n|世界");
10728 }
10729
10730 #[gpui::test]
10731 fn test_column_selection_stays_on_unicode_boundaries(cx: &mut TestAppContext) {
10732 let view = multi_line(cx);
10733 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10734 setup_cursors(&mut cx, &view.input, "ab\n你好\ncd");
10735 cx.update(|window, cx| {
10736 view.input.update(cx, |state, cx| {
10737 state.build_columnar_selection(
10738 ColumnarPoint::new(1, 0),
10739 ColumnarPoint::new(11, 0),
10740 cx,
10741 );
10742 let text = state.value();
10743 for sel in state.selections.iter() {
10744 assert!(text.is_char_boundary(sel.start));
10745 assert!(text.is_char_boundary(sel.end));
10746 }
10747 state.replace_text_in_range(None, "X", window, cx);
10748 });
10749 });
10750 }
10751
10752 #[gpui::test]
10753 fn test_editor_decorations_follow_typing(cx: &mut TestAppContext) {
10754 let view = InputView::<EditorMode>::new(cx);
10755 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10756 cx.update(|window, cx| {
10757 view.input.update(cx, |state, cx| {
10758 state.set_value("abc def", window, cx);
10759 state.create_decorations_collection(
10760 vec![crate::input::TextDecoration::new(
10761 4..7,
10762 gpui::HighlightStyle::default(),
10763 )],
10764 cx,
10765 );
10766 state.set_selected_range(0..0, cx);
10767 state.replace_text_in_range(None, "X", window, cx);
10768 let layers = state.extras.decoration_layers();
10769 assert_eq!(layers.into_iter().flatten().next().unwrap().range, 5..8);
10770 });
10771 });
10772 }
10773
10774 #[gpui::test]
10775 fn test_backspace_preserves_escaped_quote_terminator(cx: &mut TestAppContext) {
10776 let view = InputView::<EditorMode>::new(cx);
10777 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10778 setup_cursors(&mut cx, &view.input, r#"{"s":"\"|"}"#);
10779 cx.update(|window, cx| {
10780 view.input.update(cx, |state, cx| {
10781 set_test_syntax_provider(std::rc::Rc::new(StringAllProvider), cx);
10782 state.backspace(&Backspace, window, cx);
10783 });
10784 });
10785 assert_cursors(&mut cx, &view.input, r#"{"s":"\|"}"#);
10786 }
10787
10788 #[gpui::test]
10789 fn test_ordinary_typing_does_not_query_syntax(cx: &mut TestAppContext) {
10790 struct UnexpectedQuery;
10791 impl crate::input::SyntaxContextProvider for UnexpectedQuery {
10792 fn context_at(&self, _: &ropey::Rope, _: usize) -> crate::input::SyntaxContext {
10793 panic!("ordinary typing must not query syntax");
10794 }
10795 }
10796 let view = InputView::<EditorMode>::new(cx);
10797 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10798 setup_cursors(&mut cx, &view.input, "|");
10799 cx.update(|window, cx| {
10800 view.input.update(cx, |state, cx| {
10801 set_test_syntax_provider(std::rc::Rc::new(UnexpectedQuery), cx);
10802 for c in ["a", "b", "c"] {
10803 state.replace_text_in_range(None, c, window, cx);
10804 }
10805 });
10806 });
10807 assert_cursors(&mut cx, &view.input, "abc|");
10808 }
10809
10810 #[gpui::test]
10811 fn test_auto_close_is_atomic_at_history_limit(cx: &mut TestAppContext) {
10812 let view = InputView::<EditorMode>::new(cx);
10813 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10814 setup_cursors(&mut cx, &view.input, "|");
10815 cx.update(|window, cx| {
10816 view.input.update(cx, |state, cx| {
10817 for _ in 0..999 {
10818 state.replace_text_in_range(None, "a", window, cx);
10819 }
10820 state.replace_text_in_range(None, "(", window, cx);
10821 state.undo(&Undo, window, cx);
10822 assert!(!state.text.to_string().contains('('));
10823 assert!(!state.text.to_string().contains(')'));
10824 state.redo(&Redo, window, cx);
10825 });
10826 });
10827 assert_cursors(&mut cx, &view.input, &format!("{}(|)", "a".repeat(999)));
10828 }
10829
10830 #[gpui::test]
10831 fn test_comment_closer_is_inserted_literally(cx: &mut TestAppContext) {
10832 let view = InputView::<EditorMode>::new(cx);
10833 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10834 setup_cursors(&mut cx, &view.input, "// (|)");
10835 cx.update(|window, cx| {
10836 view.input.update(cx, |state, cx| {
10837 set_test_syntax_provider(std::rc::Rc::new(CommentAllProvider), cx);
10838 state.replace_text_in_range(None, ")", window, cx);
10839 });
10840 });
10841 assert_cursors(&mut cx, &view.input, "// ()|)");
10842 }
10843
10844 #[gpui::test]
10845 fn test_explicit_range_skip_does_not_delete_existing_closer_on_undo(cx: &mut TestAppContext) {
10846 let view = InputView::<EditorMode>::new(cx);
10847 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10848 setup_cursors(&mut cx, &view.input, "(a|)");
10849 let before = view.input.read_with(&cx, |state, _| state.text.to_string());
10850 cx.update(|window, cx| {
10851 view.input.update(cx, |state, cx| {
10852 state.replace_text_in_range(Some(2..2), ")", window, cx);
10853 });
10854 });
10855 assert_cursors(&mut cx, &view.input, "(a)|");
10856 cx.update(|window, cx| {
10857 view.input
10858 .update(cx, |state, cx| state.undo(&Undo, window, cx));
10859 });
10860 view.input.read_with(&cx, |state, _| {
10861 assert_eq!(
10862 state.text.to_string(),
10863 before,
10864 "skip must not remove an existing closer on undo"
10865 );
10866 });
10867 }
10868
10869 #[gpui::test]
10870 fn test_pair_redo_restores_interior_cursor(cx: &mut TestAppContext) {
10871 let view = InputView::<EditorMode>::new(cx);
10872 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10873 setup_cursors(&mut cx, &view.input, "|");
10874 cx.update(|window, cx| {
10875 view.input.update(cx, |state, cx| {
10876 state.replace_text_in_range(None, "(", window, cx);
10877 });
10878 });
10879 assert_cursors(&mut cx, &view.input, "(|)");
10880 cx.update(|window, cx| {
10881 view.input.update(cx, |state, cx| {
10882 state.undo(&Undo, window, cx);
10883 state.redo(&Redo, window, cx);
10884 });
10885 });
10886 assert_cursors(&mut cx, &view.input, "(|)");
10887 }
10888
10889 #[gpui::test]
10890 fn test_pair_enter_redo_restores_interior_cursor(cx: &mut TestAppContext) {
10891 let view = InputView::<EditorMode>::new(cx);
10892 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10893 setup_cursors(&mut cx, &view.input, "{|}");
10894 cx.update(|window, cx| {
10895 view.input.update(cx, |state, cx| {
10896 state.enter(
10897 &Enter {
10898 secondary: false,
10899 shift: false,
10900 },
10901 window,
10902 cx,
10903 );
10904 });
10905 });
10906 assert_cursors(&mut cx, &view.input, "{\n |\n}");
10907 cx.update(|window, cx| {
10908 view.input.update(cx, |state, cx| {
10909 state.undo(&Undo, window, cx);
10910 });
10911 });
10912 assert_cursors(&mut cx, &view.input, "{|}");
10913 cx.update(|window, cx| {
10914 view.input
10915 .update(cx, |state, cx| state.redo(&Redo, window, cx));
10916 });
10917 assert_cursors(&mut cx, &view.input, "{\n |\n}");
10918 }
10919
10920 #[gpui::test]
10921 fn test_auto_close_parens(cx: &mut TestAppContext) {
10922 let view = InputView::<EditorMode>::new(cx);
10923 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10924 setup_cursors(&mut cx, &view.input, "|");
10925 cx.update(|window, cx| {
10926 view.input.update(cx, |state, cx| {
10927 state.replace_text_in_range(None, "(", window, cx);
10928 });
10929 });
10930 assert_cursors(&mut cx, &view.input, "(|)");
10931 cx.update(|window, cx| {
10933 view.input.update(cx, |state, cx| {
10934 state.replace_text_in_range(None, "x", window, cx);
10935 });
10936 });
10937 assert_cursors(&mut cx, &view.input, "(x|)");
10938 }
10939
10940 #[gpui::test]
10941 fn test_auto_close_skip_over_closer(cx: &mut TestAppContext) {
10942 let view = InputView::<EditorMode>::new(cx);
10943 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10944 setup_cursors(&mut cx, &view.input, "(|)");
10945 cx.update(|window, cx| {
10946 view.input.update(cx, |state, cx| {
10947 state.replace_text_in_range(None, ")", window, cx);
10948 });
10949 });
10950 assert_cursors(&mut cx, &view.input, "()|");
10952 view.input.read_with(&cx, |state, _| {
10954 assert!(state.active_selection().is_empty());
10955 });
10956 }
10957
10958 #[gpui::test]
10959 fn test_auto_close_no_pair_inside_word(cx: &mut TestAppContext) {
10960 let view = InputView::<EditorMode>::new(cx);
10961 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10962 setup_cursors(&mut cx, &view.input, "don|");
10963 cx.update(|window, cx| {
10964 view.input.update(cx, |state, cx| {
10965 state.replace_text_in_range(None, "'", window, cx);
10966 });
10967 });
10968 assert_cursors(&mut cx, &view.input, "don'|");
10970 }
10971
10972 #[gpui::test]
10973 fn test_auto_close_quote_after_cjk(cx: &mut TestAppContext) {
10974 let view = InputView::<EditorMode>::new(cx);
10975 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10976 setup_cursors(&mut cx, &view.input, "中|");
10979 cx.update(|window, cx| {
10980 view.input.update(cx, |state, cx| {
10981 state.replace_text_in_range(None, "'", window, cx);
10982 });
10983 });
10984 assert_cursors(&mut cx, &view.input, "中'|");
10985 }
10986
10987 #[gpui::test]
10988 fn test_auto_close_closer_skips_at_string_end(cx: &mut TestAppContext) {
10989 let view = InputView::<EditorMode>::new(cx);
10990 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10991 setup_cursors(&mut cx, &view.input, "\"hello|\"");
10993 cx.update(|window, cx| {
10994 view.input.update(cx, |state, cx| {
10995 state.replace_text_in_range(None, "\"", window, cx);
10996 });
10997 });
10998 assert_cursors(&mut cx, &view.input, "\"hello\"|");
10999 view.input.read_with(&cx, |state, _| {
11000 assert!(state.active_selection().is_empty());
11001 });
11002 }
11003
11004 #[gpui::test]
11005 fn test_backspace_pair_with_cjk_prefix(cx: &mut TestAppContext) {
11006 let view = InputView::<EditorMode>::new(cx);
11007 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11008 setup_cursors(&mut cx, &view.input, "中(|)abc");
11010 cx.update(|window, cx| {
11011 view.input
11012 .update(cx, |state, cx| state.backspace(&Backspace, window, cx));
11013 });
11014 assert_cursors(&mut cx, &view.input, "中|abc");
11015 }
11016
11017 #[gpui::test]
11018 fn test_enter_split_respects_smart_indent_off(cx: &mut TestAppContext) {
11019 let view = InputView::<EditorMode>::new(cx);
11020 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11021 setup_cursors(&mut cx, &view.input, "{|}");
11022 cx.update(|window, cx| {
11023 view.input.update(cx, |state, cx| {
11024 state.set_smart_indent(false, window, cx);
11025 state.enter(
11026 &Enter {
11027 secondary: false,
11028 shift: false,
11029 },
11030 window,
11031 cx,
11032 );
11033 });
11034 });
11035 assert_cursors(&mut cx, &view.input, "{\n|}");
11037 }
11038
11039 #[gpui::test]
11040 fn test_enter_split_is_independent_of_auto_close(cx: &mut TestAppContext) {
11041 let view = InputView::<EditorMode>::new(cx);
11042 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11043 setup_cursors(&mut cx, &view.input, "{|}");
11044 cx.update(|window, cx| {
11045 view.input.update(cx, |state, cx| {
11046 state.set_auto_close(false, window, cx);
11047 state.enter(
11048 &Enter {
11049 secondary: false,
11050 shift: false,
11051 },
11052 window,
11053 cx,
11054 );
11055 });
11056 });
11057 assert_cursors(&mut cx, &view.input, "{\n |\n}");
11059 }
11060
11061 #[gpui::test]
11062 fn test_language_config_applies_before_render_and_on_language_change(cx: &mut TestAppContext) {
11063 use crate::input::{AutoClosingPair, set_language_config};
11064 let view = InputView::<EditorMode>::new(cx);
11065 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11066 cx.update(|window, cx| {
11067 set_language_config(
11068 "alpha",
11069 LanguageConfig::default().auto_closing_pairs([AutoClosingPair::new("«", "»")]),
11070 cx,
11071 );
11072 set_language_config(
11073 "beta",
11074 LanguageConfig::default().auto_closing_pairs([AutoClosingPair::new("‹", "›")]),
11075 cx,
11076 );
11077 let first = cx.new(|cx| EditorState::new(window, cx).language("ALPHA"));
11078 let second = cx.new(|cx| EditorState::new(window, cx).language("beta"));
11079 first.update(cx, |state, cx| {
11080 state.replace_text_in_range(None, "«", window, cx);
11081 assert_eq!(state.text().to_string(), "«»");
11082 state.set_value("", window, cx);
11083 state.set_smart_indent(false, window, cx);
11084 state.set_highlighter("beta", cx);
11085 assert!(!state.mode.is_smart_indent());
11086 state.replace_text_in_range(None, "‹", window, cx);
11087 assert_eq!(state.text().to_string(), "‹›");
11088 state.set_value("", window, cx);
11089 state.set_highlighter("unknown", cx);
11090 state.replace_text_in_range(None, "(", window, cx);
11091 assert_eq!(state.text().to_string(), "()");
11092 });
11093 second.update(cx, |state, cx| {
11094 state.replace_text_in_range(None, "‹", window, cx);
11095 assert_eq!(state.text().to_string(), "‹›");
11096 });
11097 });
11098 }
11099
11100 #[gpui::test]
11101 fn test_language_service_replacement_updates_syntax_without_render(cx: &mut TestAppContext) {
11102 use crate::input::{LanguageProvider, SyntaxContextProvider, set_language_provider};
11103 struct StringLanguages(Rc<Cell<usize>>);
11104 impl LanguageProvider for StringLanguages {
11105 fn syntax_context_provider(&self, _: &str) -> Option<Rc<dyn SyntaxContextProvider>> {
11106 self.0.set(self.0.get() + 1);
11107 Some(Rc::new(StringAllProvider))
11108 }
11109 }
11110 struct CodeLanguages;
11111 impl LanguageProvider for CodeLanguages {}
11112 let view = InputView::<EditorMode>::new(cx);
11113 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11114 cx.update(|window, cx| {
11115 let creations = Rc::new(Cell::new(0));
11116 set_language_provider(Rc::new(StringLanguages(creations.clone())), cx);
11117 view.input.update(cx, |state, cx| {
11118 state.replace_text_in_range(None, "(", window, cx);
11119 state.replace_text_in_range(None, "[", window, cx);
11120 assert_eq!(state.text().to_string(), "([");
11121 assert_eq!(
11122 creations.get(),
11123 1,
11124 "retain the document provider between edits"
11125 );
11126 });
11127 set_language_provider(Rc::new(CodeLanguages), cx);
11128 view.input.update(cx, |state, cx| {
11129 state.replace_text_in_range(None, "{", window, cx);
11130 assert_eq!(state.text().to_string(), "([{}");
11131 });
11132 });
11133 }
11134
11135 #[gpui::test]
11136 fn test_language_config_updates_existing_editors(cx: &mut TestAppContext) {
11137 use crate::input::{AutoClosingPair, set_language_config};
11138 let view = InputView::<EditorMode>::new(cx);
11139 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11140 setup_cursors(&mut cx, &view.input, "|");
11141 cx.update(|window, cx| {
11142 view.input.update(cx, |state, cx| {
11143 state.set_highlighter("CUSTOM", cx);
11144 state.set_auto_close(false, window, cx);
11145 state.set_smart_indent(false, window, cx);
11146 });
11147 set_language_config(
11148 "custom",
11149 LanguageConfig::default().auto_closing_pairs([AutoClosingPair::new("«", "»")]),
11150 cx,
11151 );
11152 set_language_config("other", LanguageConfig::default(), cx);
11154 view.input.update(cx, |state, cx| {
11155 assert!(!state.mode.is_auto_close());
11156 assert!(!state.mode.is_smart_indent());
11157 state.set_auto_close(true, window, cx);
11158 state.replace_text_in_range(None, "«", window, cx);
11159 });
11160 });
11161 assert_cursors(&mut cx, &view.input, "«|»");
11162 }
11163
11164 #[gpui::test]
11165 fn test_auto_close_before_is_language_configurable(cx: &mut TestAppContext) {
11166 let view = InputView::<EditorMode>::new(cx);
11167 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11168 setup_cursors(&mut cx, &view.input, "|word");
11169 cx.update(|window, cx| {
11170 view.input.update(cx, |state, cx| {
11171 state.replace_text_in_range(None, "(", window, cx)
11172 });
11173 });
11174 assert_cursors(&mut cx, &view.input, "(|word");
11175 setup_cursors(&mut cx, &view.input, "|word");
11176 cx.update(|window, cx| {
11177 view.input.update(cx, |state, cx| {
11178 crate::input::set_language_config(
11179 state.language_name(),
11180 LanguageConfig::default().auto_close_before("w"),
11181 cx,
11182 );
11183 state.replace_text_in_range(None, "(", window, cx);
11184 });
11185 });
11186 assert_cursors(&mut cx, &view.input, "(|)word");
11187 }
11188
11189 #[gpui::test]
11190 fn test_pair_context_restrictions_are_per_pair(cx: &mut TestAppContext) {
11191 use crate::input::{AutoClosingPair, SyntaxContext};
11192 let view = InputView::<EditorMode>::new(cx);
11193 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11194 setup_cursors(&mut cx, &view.input, "|");
11195 cx.update(|window, cx| {
11196 view.input.update(cx, |state, cx| {
11197 set_test_syntax_provider(std::rc::Rc::new(CommentAllProvider), cx);
11198 crate::input::set_language_config(
11199 state.language_name(),
11200 LanguageConfig::default().auto_closing_pairs([
11201 AutoClosingPair::new("(", ")").not_in([SyntaxContext::String]),
11202 AutoClosingPair::new("[", "]").not_in([SyntaxContext::Comment]),
11203 ]),
11204 cx,
11205 );
11206 state.replace_text_in_range(None, "(", window, cx);
11207 state.replace_text_in_range(None, "[", window, cx);
11208 });
11209 });
11210 assert_cursors(&mut cx, &view.input, "([|)");
11211 }
11212
11213 #[gpui::test]
11214 fn test_multichar_delimiters_insert_delete_and_skip(cx: &mut TestAppContext) {
11215 use crate::input::{AutoClosingPair, SyntaxContext};
11216 struct BlockComment;
11217 impl crate::input::SyntaxContextProvider for BlockComment {
11218 fn context_at(&self, text: &ropey::Rope, _: usize) -> SyntaxContext {
11219 if text.to_string().contains("/*") {
11220 SyntaxContext::Comment
11221 } else {
11222 SyntaxContext::Code
11223 }
11224 }
11225 }
11226 let view = InputView::<EditorMode>::new(cx);
11227 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11228 setup_cursors(&mut cx, &view.input, "|");
11229 cx.update(|window, cx| {
11230 view.input.update(cx, |state, cx| {
11231 set_test_syntax_provider(std::rc::Rc::new(BlockComment), cx);
11232 crate::input::set_language_config(
11233 state.language_name(),
11234 LanguageConfig::default()
11235 .auto_closing_pairs([AutoClosingPair::new("/*", "*/")
11236 .not_in([SyntaxContext::String, SyntaxContext::Comment])]),
11237 cx,
11238 );
11239 state.replace_text_in_range(None, "/", window, cx);
11240 state.replace_text_in_range(None, "*", window, cx);
11241 });
11242 });
11243 assert_cursors(&mut cx, &view.input, "/*|*/");
11244 cx.update(|window, cx| {
11245 view.input
11246 .update(cx, |state, cx| state.backspace(&Backspace, window, cx));
11247 });
11248 assert_cursors(&mut cx, &view.input, "|");
11249 cx.update(|window, cx| {
11250 view.input.update(cx, |state, cx| {
11251 state.undo(&Undo, window, cx);
11252 state.replace_text_in_range(None, "*", window, cx);
11253 state.replace_text_in_range(None, "/", window, cx);
11254 });
11255 });
11256 assert_cursors(&mut cx, &view.input, "/**/|");
11257 }
11258
11259 #[gpui::test]
11260 fn test_auto_closed_pairs_fallback_and_explicit_disable(cx: &mut TestAppContext) {
11261 use crate::input::BracketPair;
11262 let view = InputView::<EditorMode>::new(cx);
11263 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11264 setup_cursors(&mut cx, &view.input, "|");
11265 cx.update(|window, cx| {
11266 view.input.update(cx, |state, cx| {
11267 let mut rules = LanguageConfig::default().brackets([BracketPair::new("«", "»")]);
11268 rules.auto_closing_pairs = None;
11269 crate::input::set_language_config(state.language_name(), rules.clone(), cx);
11270 state.replace_text_in_range(None, "«", window, cx);
11271 crate::input::set_language_config(
11272 state.language_name(),
11273 rules.auto_closing_pairs([]),
11274 cx,
11275 );
11276 state.replace_text_in_range(None, "«", window, cx);
11277 });
11278 });
11279 assert_cursors(&mut cx, &view.input, "««|»");
11280 }
11281
11282 #[gpui::test]
11283 fn test_undo_open_composition_preserves_generated_pairs(cx: &mut TestAppContext) {
11284 use crate::input::{AutoClosingPair, SyntaxContext};
11285 let view = InputView::<EditorMode>::new(cx);
11286 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11287 setup_cursors(&mut cx, &view.input, "|");
11288 cx.update(|window, cx| {
11289 view.input.update(cx, |state, cx| {
11290 crate::input::set_language_config(
11291 state.language_name(),
11292 LanguageConfig::default()
11293 .auto_closing_pairs([
11294 AutoClosingPair::new("/*", "*/").not_in([SyntaxContext::Comment])
11295 ]),
11296 cx,
11297 );
11298 state.replace_text_in_range(None, "/", window, cx);
11299 state.replace_text_in_range(None, "*", window, cx);
11300 set_test_syntax_provider(std::rc::Rc::new(CommentAllProvider), cx);
11301 state.replace_and_mark_text_in_range(None, "x", Some(1..1), window, cx);
11302 state.undo(&Undo, window, cx);
11303 state.replace_text_in_range(None, "*", window, cx);
11304 state.replace_text_in_range(None, "/", window, cx);
11305 });
11306 });
11307 assert_cursors(&mut cx, &view.input, "/**/|");
11308 }
11309
11310 #[gpui::test]
11311 fn test_undo_does_not_promote_literal_comment_delimiters(cx: &mut TestAppContext) {
11312 use crate::input::{AutoClosingPair, SyntaxContext};
11313 let view = InputView::<EditorMode>::new(cx);
11314 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11315 setup_cursors(&mut cx, &view.input, "/*|*/");
11316 cx.update(|window, cx| {
11317 view.input.update(cx, |state, cx| {
11318 set_test_syntax_provider(std::rc::Rc::new(CommentAllProvider), cx);
11319 crate::input::set_language_config(
11320 state.language_name(),
11321 LanguageConfig::default()
11322 .auto_closing_pairs([
11323 AutoClosingPair::new("/*", "*/").not_in([SyntaxContext::Comment])
11324 ]),
11325 cx,
11326 );
11327 state.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
11328 state.undo(&Undo, window, cx);
11329 state.backspace(&Backspace, window, cx);
11330 });
11331 });
11332 assert_cursors(&mut cx, &view.input, "/|*/");
11333 }
11334
11335 #[gpui::test]
11336 fn test_multiple_generated_pairs_retain_their_identity(cx: &mut TestAppContext) {
11337 use crate::input::{AutoClosingPair, SyntaxContext};
11338 struct CommentScope;
11339 impl crate::input::SyntaxContextProvider for CommentScope {
11340 fn context_at(&self, text: &ropey::Rope, offset: usize) -> SyntaxContext {
11341 if text
11342 .to_string()
11343 .find("*/")
11344 .is_some_and(|end| offset < end + 2)
11345 {
11346 SyntaxContext::Comment
11347 } else {
11348 SyntaxContext::Code
11349 }
11350 }
11351 }
11352 let view = InputView::<EditorMode>::new(cx);
11353 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11354 setup_cursors(&mut cx, &view.input, "|");
11355 cx.update(|window, cx| {
11356 view.input.update(cx, |state, cx| {
11357 set_test_syntax_provider(std::rc::Rc::new(CommentScope), cx);
11358 crate::input::set_language_config(
11359 state.language_name(),
11360 LanguageConfig::default().auto_closing_pairs([
11361 AutoClosingPair::new("/*", "*/").not_in([SyntaxContext::Comment]),
11362 AutoClosingPair::new("(", ")").not_in([SyntaxContext::Comment]),
11363 ]),
11364 cx,
11365 );
11366 state.replace_text_in_range(None, "/", window, cx);
11367 state.replace_text_in_range(None, "*", window, cx);
11368 state.set_selected_range(4..4, cx);
11369 state.replace_text_in_range(None, "(", window, cx);
11370 state.set_selected_range(2..2, cx);
11371 state.replace_text_in_range(None, "*", window, cx);
11372 state.replace_text_in_range(None, "/", window, cx);
11373 });
11374 });
11375 assert_cursors(&mut cx, &view.input, "/**/|()");
11376 cx.update(|window, cx| {
11377 view.input.update(cx, |state, cx| {
11378 state.set_selected_range(2..2, cx);
11379 state.backspace(&Backspace, window, cx);
11380 state.undo(&Undo, window, cx);
11381 state.redo(&Redo, window, cx);
11382 });
11383 });
11384 assert_cursors(&mut cx, &view.input, "|()");
11385 }
11386
11387 #[gpui::test]
11388 fn test_replacing_generated_opener_invalidates_the_pair(cx: &mut TestAppContext) {
11389 use crate::input::{AutoClosingPair, SyntaxContext};
11390 let view = InputView::<EditorMode>::new(cx);
11391 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11392 setup_cursors(&mut cx, &view.input, "|");
11393 cx.update(|window, cx| {
11394 view.input.update(cx, |state, cx| {
11395 crate::input::set_language_config(
11396 state.language_name(),
11397 LanguageConfig::default()
11398 .auto_closing_pairs([
11399 AutoClosingPair::new("/*", "*/").not_in([SyntaxContext::Comment])
11400 ]),
11401 cx,
11402 );
11403 state.replace_text_in_range(None, "/", window, cx);
11404 state.replace_text_in_range(None, "*", window, cx);
11405 state.replace_text_in_range(Some(0..2), "//", window, cx);
11406 set_test_syntax_provider(std::rc::Rc::new(CommentAllProvider), cx);
11407 state.replace_text_in_range(None, "*", window, cx);
11408 });
11409 });
11410 assert_cursors(&mut cx, &view.input, "//*|*/");
11411 }
11412
11413 #[gpui::test]
11414 fn test_configuration_preserves_compiled_regex_options(cx: &mut TestAppContext) {
11415 use crate::input::IndentationRules;
11416 let view = InputView::<EditorMode>::new(cx);
11417 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11418 setup_cursors(&mut cx, &view.input, "BEGIN|");
11419 cx.update(|window, cx| {
11420 view.input.update(cx, |state, cx| {
11421 let rules = |insensitive| {
11422 LanguageConfig::default().indentation_rules(IndentationRules::new(
11423 regex::RegexBuilder::new("begin$")
11424 .case_insensitive(insensitive)
11425 .build()
11426 .unwrap(),
11427 regex::Regex::new("^end").unwrap(),
11428 ))
11429 };
11430 crate::input::set_language_config(state.language_name(), rules(false), cx);
11431 crate::input::set_language_config(state.language_name(), rules(true), cx);
11432 state.enter(
11433 &Enter {
11434 secondary: false,
11435 shift: false,
11436 },
11437 window,
11438 cx,
11439 );
11440 });
11441 });
11442 assert_cursors(&mut cx, &view.input, "BEGIN\n |");
11443 }
11444
11445 #[gpui::test]
11446 fn test_indent_patterns_are_applied_on_enter(cx: &mut TestAppContext) {
11447 use crate::input::IndentationRules;
11448 let view = InputView::<EditorMode>::new(cx);
11449 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11450 let rules = LanguageConfig::default().indentation_rules(IndentationRules::new(
11451 regex::Regex::new(r"\bbegin\s*$").unwrap(),
11452 regex::Regex::new(r"^\s*end\b").unwrap(),
11453 ));
11454 for (before, expected) in [("begin|", "begin\n |"), (" |end", " \n|end")] {
11455 setup_cursors(&mut cx, &view.input, before);
11456 cx.update(|window, cx| {
11457 view.input.update(cx, |state, cx| {
11458 crate::input::set_language_config(state.language_name(), rules.clone(), cx);
11459 state.enter(
11460 &Enter {
11461 secondary: false,
11462 shift: false,
11463 },
11464 window,
11465 cx,
11466 );
11467 });
11468 });
11469 assert_cursors(&mut cx, &view.input, expected);
11470 }
11471 }
11472
11473 #[gpui::test]
11474 fn test_custom_language_config(cx: &mut TestAppContext) {
11475 let view = InputView::<EditorMode>::new(cx);
11476 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11477 cx.update(|_window, cx| {
11478 view.input.update(cx, |state, cx| {
11479 crate::input::set_language_config(
11480 state.language_name(),
11481 crate::input::language_config::LanguageConfig::default()
11482 .brackets([crate::input::BracketPair::new("«", "»")])
11483 .auto_closing_pairs([crate::input::AutoClosingPair::new("«", "»")]),
11484 cx,
11485 );
11486 });
11487 });
11488 setup_cursors(&mut cx, &view.input, "|");
11490 cx.update(|window, cx| {
11491 view.input.update(cx, |state, cx| {
11492 state.replace_text_in_range(None, "«", window, cx);
11493 });
11494 });
11495 assert_cursors(&mut cx, &view.input, "«|»");
11496 cx.update(|window, cx| {
11498 view.input.update(cx, |state, cx| {
11499 state.set_value("\n", window, cx);
11500 state.set_selected_range(0..0, cx);
11501 state.replace_text_in_range(None, "(", window, cx);
11502 });
11503 });
11504 assert_cursors(&mut cx, &view.input, "(|");
11505 }
11506
11507 #[gpui::test]
11508 fn test_skip_records_no_history(cx: &mut TestAppContext) {
11509 let view = InputView::<EditorMode>::new(cx);
11510 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11511 setup_cursors(&mut cx, &view.input, "(a|)");
11512 cx.update(|window, cx| {
11513 view.input.update(cx, |state, cx| {
11514 state.replace_text_in_range(None, "b", window, cx);
11516 });
11517 });
11518 assert_cursors(&mut cx, &view.input, "(ab|)");
11519 cx.update(|window, cx| {
11520 view.input.update(cx, |state, cx| {
11521 state.replace_text_in_range(None, ")", window, cx);
11522 });
11523 });
11524 assert_cursors(&mut cx, &view.input, "(ab)|");
11526 cx.update(|window, cx| {
11528 view.input
11529 .update(cx, |state, cx| state.undo(&Undo, window, cx));
11530 });
11531 assert_cursors(&mut cx, &view.input, "(a|)");
11532 }
11533
11534 #[gpui::test]
11535 fn test_pair_insert_undo_removes_both(cx: &mut TestAppContext) {
11536 let view = InputView::<EditorMode>::new(cx);
11537 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11538 setup_cursors(&mut cx, &view.input, "|");
11539 cx.update(|window, cx| {
11540 view.input.update(cx, |state, cx| {
11541 state.replace_text_in_range(None, "(", window, cx);
11542 });
11543 });
11544 assert_cursors(&mut cx, &view.input, "(|)");
11545 cx.update(|window, cx| {
11547 view.input
11548 .update(cx, |state, cx| state.undo(&Undo, window, cx));
11549 });
11550 assert_cursors(&mut cx, &view.input, "|");
11551 }
11552
11553 #[gpui::test]
11554 fn test_escaped_quote_does_not_skip(cx: &mut TestAppContext) {
11555 let view = InputView::<EditorMode>::new(cx);
11556 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11557 setup_cursors(&mut cx, &view.input, "\"hello\\|\"");
11559 cx.update(|window, cx| {
11560 view.input.update(cx, |state, cx| {
11561 state.replace_text_in_range(None, "\"", window, cx);
11562 });
11563 });
11564 assert_cursors(&mut cx, &view.input, "\"hello\\\"|\"");
11565 }
11566
11567 #[gpui::test]
11568 fn test_even_backslashes_still_skip(cx: &mut TestAppContext) {
11569 let view = InputView::<EditorMode>::new(cx);
11570 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11571 setup_cursors(&mut cx, &view.input, "\"hello\\\\|\"");
11573 cx.update(|window, cx| {
11574 view.input.update(cx, |state, cx| {
11575 state.replace_text_in_range(None, "\"", window, cx);
11576 });
11577 });
11578 assert_cursors(&mut cx, &view.input, "\"hello\\\\\"|");
11579 }
11580
11581 #[gpui::test]
11582 fn test_editor_options_survive_configuration_replacement(cx: &mut TestAppContext) {
11583 use crate::input::{BracketPair, LanguageConfig};
11584
11585 let view = InputView::<EditorMode>::new(cx);
11586 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11587 cx.update(|window, cx| {
11588 view.input.update(cx, |state, cx| {
11589 state.set_smart_indent(false, window, cx);
11591 crate::input::set_language_config(
11592 state.language_name(),
11593 LanguageConfig::default().brackets([BracketPair::new("«", "»")]),
11594 cx,
11595 );
11596 });
11597 });
11598 view.input.read_with(&cx, |state, _| {
11599 assert!(!state.mode.is_smart_indent(), "editor option preserved");
11600 assert!(state.mode.is_auto_close());
11601 let pairs = &state.mode.language_config().unwrap().brackets;
11602 assert_eq!(pairs.len(), 1, "pairs follow configuration");
11603 assert_eq!(pairs[0].open.as_ref(), "«");
11604 });
11605 }
11606
11607 struct CommentAllProvider;
11608 impl crate::input::SyntaxContextProvider for CommentAllProvider {
11609 fn context_at(&self, _text: &ropey::Rope, _offset: usize) -> crate::input::SyntaxContext {
11610 crate::input::SyntaxContext::Comment
11611 }
11612 }
11613
11614 struct StringAllProvider;
11615 impl crate::input::SyntaxContextProvider for StringAllProvider {
11616 fn context_at(&self, _text: &ropey::Rope, _offset: usize) -> crate::input::SyntaxContext {
11617 crate::input::SyntaxContext::String
11618 }
11619 }
11620
11621 #[gpui::test]
11622 fn test_smart_indent_suppressed_in_strings(cx: &mut TestAppContext) {
11623 use std::rc::Rc;
11624
11625 let view = InputView::<EditorMode>::new(cx);
11626 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11627 cx.update(|_window, cx| {
11628 set_test_syntax_provider(Rc::new(StringAllProvider), cx);
11629 });
11630 setup_cursors(&mut cx, &view.input, " x = {|");
11632 cx.update(|window, cx| {
11633 view.input.update(cx, |state, cx| {
11634 state.enter(
11635 &Enter {
11636 secondary: false,
11637 shift: false,
11638 },
11639 window,
11640 cx,
11641 );
11642 });
11643 });
11644 assert_cursors(&mut cx, &view.input, " x = {\n |");
11645 }
11646
11647 #[gpui::test]
11648 fn test_split_suppressed_in_strings(cx: &mut TestAppContext) {
11649 use std::rc::Rc;
11650
11651 let view = InputView::<EditorMode>::new(cx);
11652 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11653 cx.update(|_window, cx| {
11654 set_test_syntax_provider(Rc::new(StringAllProvider), cx);
11655 });
11656 setup_cursors(&mut cx, &view.input, "{|}");
11658 cx.update(|window, cx| {
11659 view.input.update(cx, |state, cx| {
11660 state.enter(
11661 &Enter {
11662 secondary: false,
11663 shift: false,
11664 },
11665 window,
11666 cx,
11667 );
11668 });
11669 });
11670 assert_cursors(&mut cx, &view.input, "{\n|}");
11671 }
11672
11673 #[gpui::test]
11674 fn test_comment_context_disables_pairing(cx: &mut TestAppContext) {
11675 use std::rc::Rc;
11676
11677 let view = InputView::<EditorMode>::new(cx);
11678 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11679 cx.update(|_window, cx| {
11680 set_test_syntax_provider(Rc::new(CommentAllProvider), cx);
11681 });
11682 setup_cursors(&mut cx, &view.input, "|");
11683 cx.update(|window, cx| {
11684 view.input.update(cx, |state, cx| {
11685 state.replace_text_in_range(None, "(", window, cx);
11686 });
11687 });
11688 assert_cursors(&mut cx, &view.input, "(|");
11690 }
11691
11692 #[gpui::test]
11693 fn test_backspace_deletes_empty_pair(cx: &mut TestAppContext) {
11694 let view = InputView::<EditorMode>::new(cx);
11695 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11696 setup_cursors(&mut cx, &view.input, "{|}");
11697 cx.update(|window, cx| {
11698 view.input
11699 .update(cx, |state, cx| state.backspace(&Backspace, window, cx));
11700 });
11701 assert_cursors(&mut cx, &view.input, "|");
11702 }
11703
11704 #[gpui::test]
11705 fn test_backspace_keeps_closer_with_content(cx: &mut TestAppContext) {
11706 let view = InputView::<EditorMode>::new(cx);
11707 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11708 setup_cursors(&mut cx, &view.input, "{d|}");
11709 cx.update(|window, cx| {
11710 view.input
11711 .update(cx, |state, cx| state.backspace(&Backspace, window, cx));
11712 });
11713 assert_cursors(&mut cx, &view.input, "{|}");
11715 }
11716
11717 #[gpui::test]
11718 fn test_enter_splits_brackets(cx: &mut TestAppContext) {
11719 let view = InputView::<EditorMode>::new(cx);
11720 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11721 setup_cursors(&mut cx, &view.input, "{|}");
11722 cx.update(|window, cx| {
11723 view.input.update(cx, |state, cx| {
11724 state.enter(
11725 &Enter {
11726 secondary: false,
11727 shift: false,
11728 },
11729 window,
11730 cx,
11731 );
11732 });
11733 });
11734 assert_cursors(&mut cx, &view.input, "{\n |\n}");
11735 view.input.read_with(&cx, |state, _| {
11737 assert!(state.active_selection().is_empty());
11738 });
11739 cx.update(|window, cx| {
11740 view.input.update(cx, |state, cx| {
11741 state.replace_text_in_range(None, "x", window, cx);
11742 });
11743 });
11744 assert_cursors(&mut cx, &view.input, "{\n x|\n}");
11745 }
11746
11747 #[gpui::test]
11748 fn test_enter_smart_indent_after_brace(cx: &mut TestAppContext) {
11749 let view = InputView::<EditorMode>::new(cx);
11750 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11751 setup_cursors(&mut cx, &view.input, "fn main() {|");
11752 cx.update(|window, cx| {
11753 view.input.update(cx, |state, cx| {
11754 state.enter(
11755 &Enter {
11756 secondary: false,
11757 shift: false,
11758 },
11759 window,
11760 cx,
11761 );
11762 });
11763 });
11764 assert_cursors(&mut cx, &view.input, "fn main() {\n |");
11765 }
11766
11767 #[gpui::test]
11768 fn test_block_indent_tracks_all_preceding_edits(cx: &mut TestAppContext) {
11769 let view = multi_line(cx);
11770 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11771 setup_cursors(&mut cx, &view.input, "|ab\n|cd\n|ef");
11772 cx.update(|window, cx| {
11773 view.input
11774 .update(cx, |state, cx| state.indent(true, window, cx));
11775 });
11776 assert_cursors(&mut cx, &view.input, " |ab\n |cd\n |ef");
11777 cx.update(|window, cx| {
11778 view.input
11779 .update(cx, |state, cx| state.outdent(true, window, cx));
11780 });
11781 assert_cursors(&mut cx, &view.input, "|ab\n|cd\n|ef");
11782 }
11783
11784 #[gpui::test]
11785 fn test_block_outdent_clamps_cursor_inside_indent(cx: &mut TestAppContext) {
11786 let view = multi_line(cx);
11787 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11788 setup_cursors(&mut cx, &view.input, "ab\n | cd");
11789 cx.update(|window, cx| {
11790 view.input
11791 .update(cx, |state, cx| state.outdent(true, window, cx));
11792 });
11793 assert_cursors(&mut cx, &view.input, "ab\n|cd");
11794 }
11795
11796 #[gpui::test]
11797 fn test_ime_restores_original_selection(cx: &mut TestAppContext) {
11798 let view = InputView::build(cx, |state| state);
11799 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11800 cx.update(|window, cx| {
11801 view.input.update(cx, |state, cx| {
11802 state.set_value("abc", window, cx);
11803 state.set_selected_range(1..2, cx);
11804 state.replace_and_mark_text_in_range(None, "ni", None, window, cx);
11805 state.replace_text_in_range(None, "你", window, cx);
11806 state.undo(&Undo, window, cx);
11807 assert_eq!(state.value(), "abc");
11808 assert_eq!(state.selected_range(), 1..2);
11809 state.redo(&Redo, window, cx);
11810 assert_eq!(state.selected_range(), 4..4);
11811 });
11812 });
11813 }
11814
11815 #[gpui::test]
11816 fn test_noop_does_not_change_redo_selection(cx: &mut TestAppContext) {
11817 let view = multi_line(cx);
11818 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11819 cx.update(|window, cx| {
11820 view.input.update(cx, |state, cx| {
11821 state.set_value("abc", window, cx);
11822 state.set_selected_range(1..1, cx);
11823 state.replace_text_in_range(None, "X", window, cx);
11824 state.set_selected_range(0..0, cx);
11825 state.replace_text_in_range(None, "", window, cx);
11826 state.undo(&Undo, window, cx);
11827 state.redo(&Redo, window, cx);
11828 assert_eq!(state.value(), "aXbc");
11829 assert_eq!(state.selected_range(), 2..2);
11830 });
11831 });
11832 }
11833
11834 #[gpui::test]
11835 fn test_multi_cursor_insert_text(cx: &mut TestAppContext) {
11836 let view = multi_line(cx);
11837 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11838 let input = view.input;
11839
11840 setup_cursors(&mut cx, &input, "|hello |world|");
11841 cx.update(|window, cx| {
11842 input.update(cx, |state, cx| {
11843 state.replace_text_in_range(None, ">>>", window, cx);
11844 });
11845 });
11846 assert_cursors(&mut cx, &input, ">>>|hello >>>|world>>>|");
11847 }
11848
11849 #[gpui::test]
11850 fn test_multi_cursor_delete_backward(cx: &mut TestAppContext) {
11851 let view = multi_line(cx);
11852 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11853 let input = view.input;
11854
11855 setup_cursors(&mut cx, &input, "|islands| cars|");
11856 cx.update(|window, cx| {
11857 input.update(cx, |state, cx| {
11858 state.backspace(&Backspace, window, cx);
11859 });
11860 });
11861 assert_cursors(&mut cx, &input, "|island| car|");
11863 }
11864
11865 #[gpui::test]
11866 fn test_multi_cursor_delete_forward_merges(cx: &mut TestAppContext) {
11867 let view = multi_line(cx);
11868 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11869 let input = view.input;
11870
11871 setup_cursors(&mut cx, &input, "hello| |world");
11872 cx.update(|window, cx| {
11873 input.update(cx, |state, cx| {
11874 state.delete(&Delete, window, cx);
11875 });
11876 });
11877 assert_cursors(&mut cx, &input, "hello|orld");
11879 }
11880
11881 #[gpui::test]
11882 fn test_multi_cursor_multiline_insert_and_delete(cx: &mut TestAppContext) {
11883 let view = multi_line(cx);
11884 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11885 let input = view.input;
11886
11887 setup_cursors(&mut cx, &input, "|1\n|2\n|3");
11888 cx.update(|window, cx| {
11889 input.update(cx, |state, cx| {
11890 state.replace_text_in_range(None, "a", window, cx);
11891 });
11892 });
11893 assert_cursors(&mut cx, &input, "a|1\na|2\na|3");
11894
11895 input.read_with(&cx, |state, _| {
11897 assert_eq!(state.undo_manager.undo_count(), 1);
11898 });
11899
11900 cx.update(|window, cx| {
11901 input.update(cx, |state, cx| {
11902 state.backspace(&Backspace, window, cx);
11903 });
11904 });
11905 assert_cursors(&mut cx, &input, "|1\n|2\n|3");
11906 }
11907
11908 #[gpui::test]
11909 fn test_add_cursor_below_preserves_column(cx: &mut TestAppContext) {
11910 let view = multi_line(cx);
11911 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11912 let input = view.input;
11913
11914 setup_cursors(&mut cx, &input, "ab|cd\nabcd");
11915 cx.update(|window, cx| {
11916 input.update(cx, |state, cx| {
11917 state.add_cursor_below(&AddCursorBelow, window, cx);
11918 });
11919 });
11920 assert_cursors(&mut cx, &input, "ab|cd\nab|cd");
11921 }
11922
11923 #[gpui::test]
11924 fn test_add_cursor_at_rejects_duplicates(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, "he|llo");
11930 cx.update(|_, cx| {
11931 input.update(cx, |state, cx| {
11932 state.add_cursor_at(2, cx);
11934 assert_eq!(state.selections.len(), 1);
11935 state.add_cursor_at(4, cx);
11937 assert_eq!(state.selections.len(), 2);
11938 });
11939 });
11940 }
11941
11942 #[gpui::test]
11943 fn test_multi_cursor_undo_redo_restores_selections(cx: &mut TestAppContext) {
11944 let view = multi_line(cx);
11945 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11946 let input = view.input;
11947
11948 setup_cursors(&mut cx, &input, "|1\n|2\n|3");
11949 cx.update(|window, cx| {
11950 input.update(cx, |state, cx| {
11951 state.replace_text_in_range(None, "a", window, cx);
11952 });
11953 });
11954 assert_cursors(&mut cx, &input, "a|1\na|2\na|3");
11955
11956 cx.update(|window, cx| {
11957 input.update(cx, |state, cx| {
11958 state.undo(&Undo, window, cx);
11959 });
11960 });
11961 assert_cursors(&mut cx, &input, "|1\n|2\n|3");
11962
11963 cx.update(|window, cx| {
11964 input.update(cx, |state, cx| {
11965 state.redo(&Redo, window, cx);
11966 });
11967 });
11968 assert_cursors(&mut cx, &input, "a|1\na|2\na|3");
11969 }
11970
11971 #[gpui::test]
11972 fn test_multi_cursor_undo_redo_different_line_lengths(cx: &mut TestAppContext) {
11973 let view = multi_line(cx);
11974 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
11975 let input = view.input;
11976
11977 setup_cursors(&mut cx, &input, "abc123|\nabc12345|\nabc1234567|");
11978 cx.update(|window, cx| {
11979 input.update(cx, |state, cx| {
11980 state.replace_text_in_range(None, "a", window, cx);
11981 });
11982 });
11983 assert_cursors(&mut cx, &input, "abc123a|\nabc12345a|\nabc1234567a|");
11984
11985 cx.update(|window, cx| {
11986 input.update(cx, |state, cx| {
11987 state.undo(&Undo, window, cx);
11988 });
11989 });
11990 assert_cursors(&mut cx, &input, "abc123|\nabc12345|\nabc1234567|");
11991
11992 cx.update(|window, cx| {
11993 input.update(cx, |state, cx| {
11994 state.redo(&Redo, window, cx);
11995 });
11996 });
11997 assert_cursors(&mut cx, &input, "abc123a|\nabc12345a|\nabc1234567a|");
11998 }
11999
12000 #[gpui::test]
12001 fn test_multi_cursor_undo_multiple_inserts(cx: &mut TestAppContext) {
12002 let view = multi_line(cx);
12003 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
12004 let input = view.input;
12005
12006 setup_cursors(&mut cx, &input, "|1\n|2\n|3");
12007 for ch in ['a', 'b', 'c'] {
12008 cx.update(|window, cx| {
12009 input.update(cx, |state, cx| {
12010 state.replace_text_in_range(None, &ch.to_string(), window, cx);
12011 });
12012 });
12013 }
12014 assert_cursors(&mut cx, &input, "abc|1\nabc|2\nabc|3");
12015
12016 cx.update(|window, cx| {
12018 input.update(cx, |state, cx| {
12019 assert_eq!(state.undo_manager.undo_count(), 1);
12020 state.undo(&Undo, window, cx);
12021 });
12022 });
12023 assert_cursors(&mut cx, &input, "|1\n|2\n|3");
12024
12025 cx.update(|window, cx| {
12026 input.update(cx, |state, cx| {
12027 state.redo(&Redo, window, cx);
12028 });
12029 });
12030 assert_cursors(&mut cx, &input, "abc|1\nabc|2\nabc|3");
12031 }
12032
12033 #[gpui::test]
12034 fn test_multi_cursor_backspace_run_is_one_undo(cx: &mut TestAppContext) {
12035 let view = multi_line(cx);
12036 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
12037 let input = view.input;
12038
12039 setup_cursors(&mut cx, &input, "abc|1\nabc|2\nabc|3");
12040 for _ in 0..3 {
12041 cx.update(|window, cx| {
12042 input.update(cx, |state, cx| {
12043 state.backspace(&Backspace, window, cx);
12044 });
12045 });
12046 }
12047 assert_cursors(&mut cx, &input, "|1\n|2\n|3");
12048
12049 cx.update(|window, cx| {
12050 input.update(cx, |state, cx| {
12051 assert_eq!(state.undo_manager.undo_count(), 1);
12052 state.undo(&Undo, window, cx);
12053 });
12054 });
12055 assert_cursors(&mut cx, &input, "abc|1\nabc|2\nabc|3");
12056 }
12057
12058 #[gpui::test]
12059 fn test_adding_a_cursor_splits_the_typing_gesture(cx: &mut TestAppContext) {
12060 let view = multi_line(cx);
12061 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
12062 let input = view.input;
12063
12064 setup_cursors(&mut cx, &input, "|1\n2\n3");
12065 cx.update(|window, cx| {
12066 input.update(cx, |state, cx| {
12067 state.replace_text_in_range(None, "a", window, cx);
12068 state.add_cursor_below(&AddCursorBelow, window, cx);
12069 state.replace_text_in_range(None, "b", window, cx);
12070 });
12071 });
12072 assert_cursors(&mut cx, &input, "ab|1\n2b|\n3");
12073
12074 cx.update(|window, cx| {
12076 input.update(cx, |state, cx| {
12077 state.undo(&Undo, window, cx);
12078 });
12079 });
12080 assert_cursors(&mut cx, &input, "a|1\n2|\n3");
12081 }
12082
12083 #[gpui::test]
12084 fn test_multi_cursor_indent_is_one_undo(cx: &mut TestAppContext) {
12085 let view = multi_line(cx);
12086 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
12087 let input = view.input;
12088
12089 setup_cursors(&mut cx, &input, "|1\n|2\n|3");
12090 cx.update(|window, cx| {
12091 input.update(cx, |state, cx| {
12092 state.indent_inline(&IndentInline, window, cx);
12093 assert_eq!(state.undo_manager.undo_count(), 1);
12094 state.undo(&Undo, window, cx);
12095 });
12096 });
12097 assert_cursors(&mut cx, &input, "|1\n|2\n|3");
12098 }
12099
12100 #[gpui::test]
12101 fn test_multi_cursor_indent_then_outdent_roundtrips(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\n|2\n|3");
12108 cx.update(|window, cx| {
12109 input.update(cx, |state, cx| {
12110 state.indent(false, window, cx);
12111 });
12112 });
12113 assert_cursors(&mut cx, &input, " |1\n |2\n |3");
12114
12115 cx.update(|window, cx| {
12116 input.update(cx, |state, cx| {
12117 state.outdent(false, window, cx);
12118 });
12119 });
12120 assert_cursors(&mut cx, &input, "|1\n|2\n|3");
12121 }
12122
12123 #[gpui::test]
12124 fn test_inline_outdent_only_removes_line_indentation(cx: &mut TestAppContext) {
12125 let view = multi_line(cx);
12126 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
12127 let input = view.input;
12128
12129 setup_cursors(&mut cx, &input, "1|2\n1|2");
12132 cx.update(|window, cx| {
12133 input.update(cx, |state, cx| {
12134 state.indent(false, window, cx);
12135 });
12136 });
12137 assert_cursors(&mut cx, &input, "1 |2\n1 |2");
12138
12139 cx.update(|window, cx| {
12140 input.update(cx, |state, cx| {
12141 state.outdent(false, window, cx);
12142 });
12143 });
12144 assert_cursors(&mut cx, &input, "1 |2\n1 |2");
12145
12146 setup_cursors(&mut cx, &input, " 1|2\n 1|2");
12148 cx.update(|window, cx| {
12149 input.update(cx, |state, cx| {
12150 state.outdent(false, window, cx);
12151 });
12152 });
12153 assert_cursors(&mut cx, &input, "1|2\n1|2");
12154 }
12155
12156 #[gpui::test]
12157 fn test_readonly_multi_cursor_commands_leave_state_unchanged(cx: &mut TestAppContext) {
12158 let view = multi_line(cx);
12159 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
12160 let input = view.input;
12161
12162 setup_cursors(&mut cx, &input, "a|b\nc|d");
12163 cx.update(|window, cx| {
12164 input.update(cx, |state, cx| {
12165 let before: Vec<_> = state.selections.iter().copied().collect();
12166 state.set_readonly(true, cx);
12167 state.backspace(&Backspace, window, cx);
12168 state.indent_inline(&IndentInline, window, cx);
12169
12170 assert_eq!(state.value(), "ab\ncd\n");
12171 assert_eq!(state.selections.iter().copied().collect::<Vec<_>>(), before);
12172 });
12173 });
12174 }
12175
12176 #[gpui::test]
12177 fn test_multi_cursor_edit_preserves_the_active_cursor(cx: &mut TestAppContext) {
12178 let view = multi_line(cx);
12179 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
12180 let input = view.input;
12181
12182 setup_cursors(&mut cx, &input, "x|\n|y");
12183 cx.update(|window, cx| {
12184 input.update(cx, |state, cx| {
12185 let mut selections: Vec<_> = state.selections.iter().copied().collect();
12186 selections.swap(0, 1);
12187 state.selections.replace_all(selections);
12188 let active_id = state.active_selection().id;
12189 state.replace_text_in_range(None, "!", window, cx);
12190 assert_eq!(state.active_selection().id, active_id);
12191 });
12192 });
12193 }
12194
12195 #[gpui::test]
12196 fn test_block_indent_outdent_with_selection(cx: &mut TestAppContext) {
12197 let view = multi_line(cx);
12198 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
12199 let input = view.input;
12200
12201 cx.update(|window, cx| {
12202 input.update(cx, |state, cx| {
12203 state.set_value("line1\nline2\nline3", window, cx);
12204 let id = state.selections.generate_id();
12205 state
12206 .selections
12207 .replace_all(vec![CursorSelection::new(id, 0, 17)]);
12208 cx.notify();
12209 });
12210 });
12211
12212 cx.update(|window, cx| {
12213 input.update(cx, |state, cx| {
12214 state.indent(true, window, cx);
12215 });
12216 });
12217 input.read_with(&cx, |state, _| {
12218 assert_eq!(state.text.to_string(), " line1\n line2\n line3");
12219 });
12220
12221 cx.update(|window, cx| {
12222 input.update(cx, |state, cx| {
12223 state.outdent(true, window, cx);
12224 });
12225 });
12226 input.read_with(&cx, |state, _| {
12227 assert_eq!(state.text.to_string(), "line1\nline2\nline3");
12228 });
12229 }
12230
12231 #[gpui::test]
12232 fn test_multi_cursor_word_movement(cx: &mut TestAppContext) {
12233 let view = multi_line(cx);
12234 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
12235 let input = view.input;
12236
12237 setup_cursors(
12238 &mut cx,
12239 &input,
12240 "on|e two three\none t|wo three\non|e two three",
12241 );
12242
12243 cx.update(|window, cx| {
12244 input.update(cx, |state, cx| {
12245 state.move_to_next_word(&MoveToNextWord, window, cx);
12246 });
12247 });
12248 assert_cursors(
12249 &mut cx,
12250 &input,
12251 "one| two three\none two| three\none| two three",
12252 );
12253
12254 cx.update(|window, cx| {
12255 input.update(cx, |state, cx| {
12256 state.move_to_previous_word(&MoveToPreviousWord, window, cx);
12257 });
12258 });
12259 assert_cursors(
12260 &mut cx,
12261 &input,
12262 "|one two three\none |two three\n|one two three",
12263 );
12264
12265 cx.update(|window, cx| {
12267 input.update(cx, |state, cx| {
12268 state.move_to_end(&MoveToEnd, window, cx);
12269 });
12270 });
12271 input.read_with(&cx, |state, _| {
12272 let cursors: Vec<usize> = state.selections.iter().map(|s| s.cursor_offset()).collect();
12273 assert_eq!(cursors, vec![state.text.len()]);
12274 });
12275
12276 cx.update(|window, cx| {
12277 input.update(cx, |state, cx| {
12278 state.move_to_start(&MoveToStart, window, cx);
12279 });
12280 });
12281 input.read_with(&cx, |state, _| {
12282 let cursors: Vec<usize> = state.selections.iter().map(|s| s.cursor_offset()).collect();
12283 assert_eq!(cursors, vec![0]);
12284 });
12285 }
12286
12287 #[gpui::test]
12288 fn test_multi_cursor_selection_commands(cx: &mut TestAppContext) {
12289 let view = multi_line(cx);
12290 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
12291 let input = view.input;
12292
12293 setup_cursors(
12294 &mut cx,
12295 &input,
12296 "on|e two three\none t|wo three\non|e two three",
12297 );
12298 cx.update(|window, cx| {
12299 input.update(cx, |state, cx| {
12300 state.select_to_start_of_line(&SelectToStartOfLine, window, cx);
12301 });
12302 });
12303 assert_cursors(
12304 &mut cx,
12305 &input,
12306 "|one two three\n|one two three\n|one two three",
12307 );
12308
12309 setup_cursors(
12311 &mut cx,
12312 &input,
12313 "on|e two three\none t|wo three\non|e two three",
12314 );
12315 cx.update(|window, cx| {
12316 input.update(cx, |state, cx| {
12317 state.select_to_start(&SelectToStart, window, cx);
12318 });
12319 });
12320 input.read_with(&cx, |state, _| {
12321 let cursors: Vec<usize> = state.selections.iter().map(|s| s.cursor_offset()).collect();
12322 assert_eq!(cursors, vec![0]);
12323 });
12324 }
12325
12326 #[gpui::test]
12327 fn test_multi_cursor_replace_selection(cx: &mut TestAppContext) {
12328 let view = multi_line(cx);
12329 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
12330 let input = view.input;
12331
12332 setup_cursors(&mut cx, &input, "|a\n|b\n|c");
12333 cx.update(|window, cx| {
12334 input.update(cx, |state, cx| {
12335 state.select_right(&SelectRight, window, cx);
12336 });
12337 });
12338 input.read_with(&cx, |state, _| {
12339 let ranges: Vec<_> = state.selections.iter().map(|s| (s.start, s.end)).collect();
12340 assert_eq!(ranges, vec![(0, 1), (2, 3), (4, 5)]);
12341 });
12342
12343 cx.update(|window, cx| {
12344 input.update(cx, |state, cx| {
12345 state.replace_text_in_range(None, "x", window, cx);
12346 });
12347 });
12348 assert_cursors(&mut cx, &input, "x|\nx|\nx|");
12349 }
12350
12351 #[gpui::test]
12352 fn test_multi_cursor_escape_collapses(cx: &mut TestAppContext) {
12353 let view = multi_line(cx);
12354 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
12355 let input = view.input;
12356
12357 setup_cursors(&mut cx, &input, "|a\n|b\n|c");
12358 cx.update(|window, cx| {
12359 input.update(cx, |state, cx| {
12360 state.escape(&Escape, window, cx);
12361 });
12362 });
12363 input.read_with(&cx, |state, _| {
12364 assert_eq!(state.selections.len(), 1);
12365 });
12366 }
12367
12368 #[gpui::test]
12369 fn test_build_columnar_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, "abcd\nabcd\nabcd");
12375 cx.update(|_, cx| {
12376 input.update(cx, |state, cx| {
12377 state.build_columnar_selection(
12379 ColumnarPoint::new(1, 0),
12380 ColumnarPoint::new(13, 0),
12381 cx,
12382 );
12383 let ranges: Vec<_> = state.selections.iter().map(|s| (s.start, s.end)).collect();
12384 assert_eq!(ranges, vec![(1, 3), (6, 8), (11, 13)]);
12385 });
12386 });
12387 }
12388
12389 #[gpui::test]
12390 fn test_columnar_selection_keeps_width_over_short_rows(cx: &mut TestAppContext) {
12391 let view = multi_line(cx);
12392 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
12393 let input = view.input;
12394
12395 setup_cursors(&mut cx, &input, "abcdef\nab\nabcdef");
12396 cx.update(|_, cx| {
12397 input.update(cx, |state, cx| {
12398 state.build_columnar_selection(
12401 ColumnarPoint::new(1, 0),
12402 ColumnarPoint::new(9, 3),
12403 cx,
12404 );
12405 let ranges: Vec<_> = state.selections.iter().map(|s| (s.start, s.end)).collect();
12406 assert_eq!(ranges, vec![(1, 5), (8, 9)]);
12407 });
12408 });
12409 }
12410
12411 #[gpui::test]
12412 fn test_multi_cursor_paste_distributes_lines(cx: &mut TestAppContext) {
12413 let view = multi_line(cx);
12414 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
12415 let input = view.input;
12416
12417 setup_cursors(&mut cx, &input, "|1\n|2\n|3");
12418 cx.update(|_, cx| {
12419 cx.write_to_clipboard(ClipboardItem::new_string("x\ny\nz".to_string()));
12420 });
12421 cx.update(|window, cx| {
12422 input.update(cx, |state, cx| {
12423 state.paste(&Paste, window, cx);
12424 });
12425 });
12426 assert_cursors(&mut cx, &input, "x|1\ny|2\nz|3");
12428 }
12429
12430 #[gpui::test]
12431 fn test_paste_without_text_leaves_the_selection_alone(cx: &mut TestAppContext) {
12432 use gpui::{ClipboardEntry, Image, ImageFormat};
12433
12434 let input_view = InputView::build(cx, |state| state);
12435 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
12436 let input = input_view.input;
12437
12438 cx.update(|window, cx| {
12439 input.update(cx, |state, cx| {
12440 state.set_value("hello world", window, cx);
12441 state.select_all(window, cx);
12442 cx.write_to_clipboard(ClipboardItem {
12443 entries: vec![ClipboardEntry::Image(Image::from_bytes(
12444 ImageFormat::Png,
12445 vec![0x89, b'P', b'N', b'G'],
12446 ))],
12447 });
12448
12449 state.paste(&Paste, window, cx);
12452 assert_eq!(state.value(), "hello world");
12453 assert_eq!(state.selected_range(), 0..11);
12454 });
12455 });
12456 }
12457
12458 #[gpui::test]
12459 fn test_paste_target_tracks_edits_and_selections(cx: &mut TestAppContext) {
12460 let view = multi_line(cx);
12461 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
12462 let input = view.input;
12463
12464 setup_cursors(&mut cx, &input, "ab|c");
12465 let target = input.read_with(&cx, |state, _| state.paste_target());
12466 assert_eq!(
12468 input.read_with(&cx, |state, _| state.paste_target()),
12469 target
12470 );
12471
12472 cx.update(|_, cx| {
12474 input.update(cx, |state, cx| {
12475 let id = state.selections.generate_id();
12476 state
12477 .selections
12478 .replace_all(vec![CursorSelection::new(id, 1, 1)]);
12479 cx.notify();
12480 });
12481 });
12482 let moved = input.read_with(&cx, |state, _| state.paste_target());
12483 assert_ne!(moved, target);
12484
12485 cx.update(|window, cx| {
12487 input.update(cx, |state, cx| {
12488 state.replace_text_in_range_silent(None, "x", window, cx);
12489 let id = state.selections.generate_id();
12490 state
12491 .selections
12492 .replace_all(vec![CursorSelection::new(id, 1, 1)]);
12493 });
12494 });
12495 assert_ne!(input.read_with(&cx, |state, _| state.paste_target()), moved);
12496 }
12497
12498 #[gpui::test]
12499 fn test_ime_candidate_bounds_fallback_before_repaint(cx: &mut TestAppContext) {
12500 let input_view = InputView::build_textarea(cx, |state| {
12501 state.default_value("这是一段已经输入的中文文字")
12502 });
12503 let mut visual = VisualTestContext::from_window(input_view.window_handle.into(), cx);
12504 visual.update(|window, cx| window.draw(cx).clear(cx));
12505
12506 visual.update(|window, cx| {
12507 input_view.input.update(cx, |state, cx| {
12508 state.focus(window, cx);
12509 let len = state.text.len();
12510 state.set_cursor_to(len);
12511 });
12512 });
12513 visual.update(|window, cx| window.draw(cx).clear(cx));
12514
12515 visual.update(|window, cx| {
12516 input_view.input.update(cx, |state, cx| {
12517 let bounds = state.text_bounds().unwrap_or_default();
12518 let before = state.selected_text_range(false, window, cx).unwrap().range;
12519 let previous = state
12520 .bounds_for_range(before.end..before.end, bounds, window, cx)
12521 .unwrap();
12522
12523 state.replace_and_mark_text_in_range(None, "n", None, window, cx);
12524
12525 let after = state.selected_text_range(false, window, cx).unwrap().range;
12526 let pending = state
12527 .bounds_for_range(after.end..after.end, bounds, window, cx)
12528 .unwrap();
12529
12530 assert!(
12531 (pending.origin.x - previous.origin.x).abs() < px(40.0),
12532 "pending origin.x {:?} must remain close to previous origin.x {:?}",
12533 pending.origin.x,
12534 previous.origin.x
12535 );
12536
12537 let range_bounds = state
12538 .bounds_for_range(before.end..after.end, bounds, window, cx)
12539 .unwrap();
12540 assert!(
12541 (range_bounds.origin.x - previous.origin.x).abs() < px(40.0),
12542 "range_bounds origin.x {:?} must remain close to previous origin.x {:?}",
12543 range_bounds.origin.x,
12544 previous.origin.x
12545 );
12546 assert!(range_bounds.size.width >= px(0.0));
12547 });
12548 });
12549 }
12550}
12551
12552impl InputBaseState<crate::input::InputMode> {
12554 pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
12556 Self::new_in_mode(window, cx)
12557 }
12558
12559 pub fn step_by(mut self, f: impl Fn(f64, StepAction, &mut App) -> f64 + 'static) -> Self {
12574 self.number_step = Some(NumberStep::by_value(f));
12575 self
12576 }
12577
12578 pub fn masked(mut self, masked: bool) -> Self {
12580 self.masked = masked;
12581 self
12582 }
12583
12584 pub fn set_masked(&mut self, masked: bool, _: &mut Window, cx: &mut Context<Self>) {
12586 self.masked = masked;
12587 cx.notify();
12588 }
12589
12590 pub fn pattern(mut self, pattern: regex::Regex) -> Self {
12592 self.pattern = Some(pattern);
12593 self
12594 }
12595
12596 pub fn set_pattern(
12598 &mut self,
12599 pattern: regex::Regex,
12600 _window: &mut Window,
12601 _cx: &mut Context<Self>,
12602 ) {
12603 self.pattern = Some(pattern);
12604 }
12605
12606 pub fn validate(mut self, f: impl Fn(&str, &mut App) -> bool + 'static) -> Self {
12608 self.validate = Some(Box::new(f));
12609 self
12610 }
12611
12612 pub fn set_validator(
12613 &mut self,
12614 validate: impl Fn(&str, &mut App) -> bool + 'static,
12615 _cx: &mut Context<Self>,
12616 ) {
12617 self.validate = Some(Box::new(validate));
12618 }
12619
12620 pub fn step(mut self, step: impl Into<NumberStep>) -> Self {
12630 self.number_step = Some(step.into());
12631 self
12632 }
12633
12634 pub fn min(mut self, min: f64) -> Self {
12640 self.number_min = Some(min);
12641 self
12642 }
12643
12644 pub fn max(mut self, max: f64) -> Self {
12650 self.number_max = Some(max);
12651 self
12652 }
12653
12654 pub fn set_step(
12659 &mut self,
12660 step: impl Into<Option<NumberStep>>,
12661 _: &mut Window,
12662 _: &mut Context<Self>,
12663 ) {
12664 self.number_step = step.into();
12665 }
12666
12667 pub fn set_min(&mut self, min: Option<f64>, _: &mut Window, _: &mut Context<Self>) {
12669 self.number_min = min;
12670 }
12671
12672 pub fn set_max(&mut self, max: Option<f64>, _: &mut Window, _: &mut Context<Self>) {
12674 self.number_max = max;
12675 }
12676
12677 pub fn set_loading(&mut self, loading: bool, _: &mut Window, cx: &mut Context<Self>) {
12679 self.loading = loading;
12680 cx.notify();
12681 }
12682}
12683
12684impl<M: crate::input::MultiLineMode> InputBaseState<M> {
12687 pub fn scroll_bounce(mut self, scroll_bounce: bool) -> Self {
12695 self.scroll_bounce = scroll_bounce;
12696 self
12697 }
12698
12699 pub fn set_scroll_bounce(&mut self, scroll_bounce: bool, cx: &mut Context<Self>) {
12703 if self.scroll_bounce != scroll_bounce {
12704 self.scroll_bounce = scroll_bounce;
12705 self.overscroll.reset();
12706 cx.notify();
12707 }
12708 }
12709
12710 pub fn searchable(mut self, searchable: bool) -> Self {
12718 self.searchable = searchable;
12719 self
12720 }
12721
12722 pub fn set_searchable(&mut self, searchable: bool, cx: &mut Context<Self>) {
12724 self.searchable = searchable;
12725 cx.notify();
12726 }
12727
12728 #[doc(hidden)]
12730 pub fn soft_wrap(mut self, wrap: bool) -> Self {
12731 self.soft_wrap = wrap;
12732 self
12733 }
12734
12735 pub fn set_soft_wrap(&mut self, wrap: bool, _: &mut Window, cx: &mut Context<Self>) {
12737 self.soft_wrap = wrap;
12738 if wrap {
12739 let wrap_width = self
12740 .last_layout
12741 .as_ref()
12742 .and_then(|b| b.wrap_width)
12743 .unwrap_or(self.input_bounds.size.width);
12744
12745 self.display_map.on_layout_changed(Some(wrap_width), cx);
12746
12747 let mut offset = self.scroll_handle.offset();
12749 offset.x = px(0.);
12750 self.scroll_handle.set_offset(offset);
12751 } else {
12752 self.display_map.on_layout_changed(None, cx);
12753 }
12754 cx.notify();
12755 }
12756
12757 #[doc(hidden)]
12759 pub fn wrapping_indent(mut self, wrapping_indent: WrappingIndent) -> Self {
12760 self.wrapping_indent = wrapping_indent;
12761 self
12762 }
12763
12764 pub fn set_wrapping_indent(
12766 &mut self,
12767 wrapping_indent: WrappingIndent,
12768 _: &mut Window,
12769 cx: &mut Context<Self>,
12770 ) {
12771 self.wrapping_indent = wrapping_indent;
12772 self.display_map.set_wrapping_indent(wrapping_indent, cx);
12773 cx.notify();
12774 }
12775}
12776
12777impl InputBaseState<crate::input::TextareaMode> {
12779 pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
12784 Self::new_in_mode(window, cx)
12785 }
12786
12787 pub fn set_auto_grow(&mut self, min_rows: usize, max_rows: usize, cx: &mut Context<Self>) {
12788 self.mode = LayoutMode::auto_grow(min_rows, max_rows.max(min_rows));
12789 self.mode.update_auto_grow(&self.display_map);
12790 cx.notify();
12791 }
12792
12793 #[doc(hidden)]
12800 pub fn rows(mut self, rows: usize) -> Self {
12801 match &mut self.mode {
12802 LayoutMode::PlainText { rows: r, .. } | LayoutMode::CodeEditor { rows: r, .. } => {
12803 *r = rows
12804 }
12805 LayoutMode::AutoGrow {
12806 max_rows: max_r,
12807 rows: r,
12808 ..
12809 } => {
12810 *r = rows;
12811 *max_r = rows;
12812 }
12813 }
12814 self
12815 }
12816
12817 pub fn set_rows(&mut self, rows: usize, cx: &mut Context<Self>) {
12818 match &mut self.mode {
12819 LayoutMode::PlainText { rows: value, .. }
12820 | LayoutMode::CodeEditor { rows: value, .. } => *value = rows,
12821 LayoutMode::AutoGrow {
12822 rows: value,
12823 max_rows,
12824 ..
12825 } => {
12826 *value = rows;
12827 *max_rows = rows;
12828 }
12829 }
12830 cx.notify();
12831 }
12832
12833 pub fn auto_grow(mut self, min_rows: usize, max_rows: usize) -> Self {
12835 self.mode = LayoutMode::auto_grow(min_rows, max_rows);
12836 self
12837 }
12838}
12839
12840impl InputBaseState<crate::input::EditorMode> {
12842 pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
12853 let mut state = Self::new_in_mode(window, cx);
12854 state.mode = LayoutMode::code_editor(super::EditorLanguage::new(cx));
12855 state.searchable = true;
12856 state
12857 }
12858
12859 pub fn language(mut self, language: impl Into<SharedString>) -> Self {
12863 if let LayoutMode::CodeEditor {
12864 language: l,
12865 highlighter,
12866 ..
12867 } = &mut self.mode
12868 {
12869 l.set_name(language.into());
12870 *highlighter.borrow_mut() = None;
12871 }
12872 self
12873 }
12874
12875 pub fn language_name(&self) -> SharedString {
12877 match &self.mode {
12878 LayoutMode::CodeEditor { language, .. } => language.name(),
12879 _ => SharedString::default(),
12880 }
12881 }
12882
12883 #[doc(hidden)]
12887 pub fn folding(mut self, folding: bool) -> Self {
12888 if let LayoutMode::CodeEditor { folding: f, .. } = &mut self.mode {
12889 *f = folding;
12890 }
12891 self
12892 }
12893
12894 pub fn set_folding(&mut self, folding: bool, _: &mut Window, cx: &mut Context<Self>) {
12898 if let LayoutMode::CodeEditor { folding: f, .. } = &mut self.mode {
12899 *f = folding;
12900 }
12901 if !folding {
12902 self.display_map.clear_folds();
12903 }
12904 cx.notify();
12905 }
12906
12907 pub fn unfold_at(&mut self, position: impl Into<Position>, cx: &mut Context<Self>) -> bool {
12920 let offset = self.text.position_to_offset(&position.into());
12921 let line = self.text.offset_to_point(offset).row;
12922 let covering: Vec<usize> = self
12925 .display_map
12926 .folded_ranges()
12927 .iter()
12928 .filter(|fold| line > fold.start_line && line < fold.end_line)
12929 .map(|fold| fold.start_line)
12930 .collect();
12931 if covering.is_empty() {
12932 return false;
12933 }
12934
12935 for start_line in covering {
12936 self.display_map.set_folded(start_line, false);
12937 }
12938 cx.notify();
12939 true
12940 }
12941
12942 #[doc(hidden)]
12944 pub fn line_number(mut self, line_number: bool) -> Self {
12945 if let LayoutMode::CodeEditor { line_number: l, .. } = &mut self.mode {
12946 *l = line_number;
12947 }
12948 self
12949 }
12950
12951 pub fn set_line_number(&mut self, line_number: bool, _: &mut Window, cx: &mut Context<Self>) {
12953 if let LayoutMode::CodeEditor { line_number: l, .. } = &mut self.mode {
12954 if *l == line_number {
12955 return;
12956 }
12957 *l = line_number;
12958 }
12959 cx.notify();
12960 }
12961
12962 #[doc(hidden)]
12968 pub fn auto_close(mut self, auto_close: bool) -> Self {
12969 self.mode.set_auto_close(auto_close);
12970 self
12971 }
12972
12973 pub fn set_auto_close(&mut self, auto_close: bool, _: &mut Window, cx: &mut Context<Self>) {
12975 self.mode.set_auto_close(auto_close);
12976 cx.notify();
12977 }
12978
12979 #[doc(hidden)]
12985 pub fn smart_indent(mut self, smart_indent: bool) -> Self {
12986 self.mode.set_smart_indent(smart_indent);
12987 self
12988 }
12989
12990 pub fn set_smart_indent(&mut self, smart_indent: bool, _: &mut Window, cx: &mut Context<Self>) {
12992 self.mode.set_smart_indent(smart_indent);
12993 cx.notify();
12994 }
12995}