1use gpui::TextAlign;
6use gpui::{
7 Action, App, AppContext, Bounds, ClipboardItem, Context, Edges, Entity, EntityInputHandler,
8 EventEmitter, FocusHandle, Focusable, InteractiveElement as _, IntoElement, KeyBinding,
9 MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels, Point,
10 Render, ScrollHandle, ScrollWheelEvent, SharedString, Styled as _, Subscription,
11 UTF16Selection, WeakFocusHandle, Window, actions, div, point, prelude::FluentBuilder as _, px,
12};
13use ropey::{Rope, RopeSlice};
14use serde::Deserialize;
15use std::borrow::Cow;
16use std::cell::Cell;
17use std::ops::Range;
18use std::rc::Rc;
19use sum_tree::Bias;
20use unicode_segmentation::*;
21
22use super::{
23 DiagnosticSet, DisplayMap, InputContextMenuCapabilities, InputEditorStyle,
24 InputHighlighterFactory, MASK_CHAR, MaskPattern, NativeMenu, NumberStep, WrappingIndent,
25 blink_cursor::BlinkCursor,
26 change::Change,
27 cursor::{CursorSelection, Selections},
28 element::{
29 EditorScrollbar, EditorScrollbarSnapshot, LongestLineKey, TextElement,
30 clamp_horizontal_scroll_offset,
31 },
32 kind::InputModeKind,
33 mask_pattern::normalize_number_input,
34 mode::LayoutMode,
35 undo_manager::{EditIntent, UndoManager},
36};
37use crate::actions::{SelectDown, SelectLeft, SelectRight, SelectUp};
38use crate::input::blink_cursor::CURSOR_WIDTH;
39use crate::input::movement::MoveDirection;
40use crate::input::{
41 InputExtras as _, Position, RopeExt as _, element::RIGHT_MARGIN, layout::LastLayout,
42};
43use crate::{AutoScroll, StepAction};
44
45pub(crate) enum ScrollPadding {
47 Minimal,
48 SurroundingLines,
49}
50
51#[derive(Action, Clone, PartialEq, Eq, Deserialize)]
52#[action(namespace = input, no_json)]
53pub struct Enter {
54 pub secondary: bool,
56 pub shift: bool,
58}
59
60impl Enter {
61 pub fn is_primary(action: &dyn Action) -> bool {
64 action.partial_eq(&Enter {
65 secondary: false,
66 shift: false,
67 }) || action.partial_eq(&Enter {
68 secondary: false,
69 shift: true,
70 })
71 }
72}
73
74actions!(
75 input,
76 [
77 ActivateToken,
78 Backspace,
79 Delete,
80 DeleteToBeginningOfLine,
81 DeleteToEndOfLine,
82 DeleteToPreviousWordStart,
83 DeleteToNextWordEnd,
84 Indent,
85 Outdent,
86 IndentInline,
87 OutdentInline,
88 MoveUp,
89 MoveDown,
90 MoveLeft,
91 MoveRight,
92 MoveHome,
93 MoveEnd,
94 MovePageUp,
95 MovePageDown,
96 AddCursorAbove,
97 AddCursorBelow,
98 SelectAll,
99 SelectToStartOfLine,
100 SelectToEndOfLine,
101 SelectToStart,
102 SelectToEnd,
103 SelectToPreviousWordStart,
104 SelectToNextWordEnd,
105 ShowCharacterPalette,
106 Copy,
107 Cut,
108 Paste,
109 Undo,
110 Redo,
111 MoveToStartOfLine,
112 MoveToEndOfLine,
113 MoveToStart,
114 MoveToEnd,
115 MoveToPreviousWord,
116 MoveToNextWord,
117 Escape,
118 ToggleCodeActions,
119 ShowCompletions,
120 Search,
121 Replace,
122 GoToDefinition,
123 ]
124);
125
126#[derive(Clone)]
127pub enum InputEvent {
128 Change,
129 PressEnter { secondary: bool, shift: bool },
130 Focus,
131 Blur,
132}
133
134pub(super) const CONTEXT: &str = "Input";
135
136pub(crate) fn init(cx: &mut App) {
137 cx.bind_keys([
138 KeyBinding::new("backspace", Backspace, Some(CONTEXT)),
139 KeyBinding::new("shift-backspace", Backspace, Some(CONTEXT)),
140 #[cfg(target_os = "macos")]
141 KeyBinding::new("ctrl-backspace", Backspace, Some(CONTEXT)),
142 KeyBinding::new("delete", Delete, Some(CONTEXT)),
143 KeyBinding::new("shift-delete", Delete, Some(CONTEXT)),
144 #[cfg(target_os = "macos")]
145 KeyBinding::new("cmd-backspace", DeleteToBeginningOfLine, Some(CONTEXT)),
146 #[cfg(target_os = "macos")]
147 KeyBinding::new("cmd-delete", DeleteToEndOfLine, Some(CONTEXT)),
148 #[cfg(target_os = "macos")]
149 KeyBinding::new("alt-backspace", DeleteToPreviousWordStart, Some(CONTEXT)),
150 #[cfg(not(target_os = "macos"))]
151 KeyBinding::new("ctrl-backspace", DeleteToPreviousWordStart, Some(CONTEXT)),
152 #[cfg(target_os = "macos")]
153 KeyBinding::new("alt-delete", DeleteToNextWordEnd, Some(CONTEXT)),
154 #[cfg(not(target_os = "macos"))]
155 KeyBinding::new("ctrl-delete", DeleteToNextWordEnd, Some(CONTEXT)),
156 KeyBinding::new(
157 "enter",
158 Enter {
159 secondary: false,
160 shift: false,
161 },
162 Some(CONTEXT),
163 ),
164 KeyBinding::new(
165 "shift-enter",
166 Enter {
167 secondary: false,
168 shift: true,
169 },
170 Some(CONTEXT),
171 ),
172 KeyBinding::new(
173 "secondary-enter",
174 Enter {
175 secondary: true,
176 shift: false,
177 },
178 Some(CONTEXT),
179 ),
180 KeyBinding::new("escape", Escape, Some(CONTEXT)),
181 KeyBinding::new("up", MoveUp, Some(CONTEXT)),
182 KeyBinding::new("down", MoveDown, Some(CONTEXT)),
183 KeyBinding::new("left", MoveLeft, Some(CONTEXT)),
184 KeyBinding::new("right", MoveRight, Some(CONTEXT)),
185 KeyBinding::new("pageup", MovePageUp, Some(CONTEXT)),
186 KeyBinding::new("pagedown", MovePageDown, Some(CONTEXT)),
187 KeyBinding::new("tab", IndentInline, Some(CONTEXT)),
188 KeyBinding::new("shift-tab", OutdentInline, Some(CONTEXT)),
189 #[cfg(target_os = "macos")]
190 KeyBinding::new("cmd-]", Indent, Some(CONTEXT)),
191 #[cfg(not(target_os = "macos"))]
192 KeyBinding::new("ctrl-]", Indent, Some(CONTEXT)),
193 #[cfg(target_os = "macos")]
194 KeyBinding::new("cmd-[", Outdent, Some(CONTEXT)),
195 #[cfg(not(target_os = "macos"))]
196 KeyBinding::new("ctrl-[", Outdent, Some(CONTEXT)),
197 KeyBinding::new("shift-left", SelectLeft, Some(CONTEXT)),
198 KeyBinding::new("shift-right", SelectRight, Some(CONTEXT)),
199 KeyBinding::new("shift-up", SelectUp, Some(CONTEXT)),
200 KeyBinding::new("shift-down", SelectDown, Some(CONTEXT)),
201 #[cfg(not(any(target_os = "macos", target_os = "linux")))]
202 KeyBinding::new("shift-alt-left", SelectLeft, Some(CONTEXT)),
203 #[cfg(not(any(target_os = "macos", target_os = "linux")))]
204 KeyBinding::new("shift-alt-right", SelectRight, Some(CONTEXT)),
205 #[cfg(target_os = "macos")]
207 KeyBinding::new("cmd-alt-up", AddCursorAbove, Some(CONTEXT)),
208 #[cfg(target_os = "macos")]
209 KeyBinding::new("cmd-alt-down", AddCursorBelow, Some(CONTEXT)),
210 #[cfg(target_os = "windows")]
211 KeyBinding::new("ctrl-alt-up", AddCursorAbove, Some(CONTEXT)),
212 #[cfg(not(any(target_os = "macos", target_os = "windows")))]
213 KeyBinding::new("shift-alt-up", AddCursorAbove, Some(CONTEXT)),
214 #[cfg(target_os = "windows")]
215 KeyBinding::new("ctrl-alt-down", AddCursorBelow, Some(CONTEXT)),
216 #[cfg(not(any(target_os = "macos", target_os = "windows")))]
217 KeyBinding::new("shift-alt-down", AddCursorBelow, Some(CONTEXT)),
218 KeyBinding::new("home", MoveHome, Some(CONTEXT)),
219 KeyBinding::new("end", MoveEnd, Some(CONTEXT)),
220 #[cfg(not(target_os = "macos"))]
221 KeyBinding::new("ctrl-home", MoveToStart, Some(CONTEXT)),
222 #[cfg(not(target_os = "macos"))]
223 KeyBinding::new("ctrl-end", MoveToEnd, Some(CONTEXT)),
224 #[cfg(not(target_os = "macos"))]
225 KeyBinding::new("ctrl-shift-home", SelectToStart, Some(CONTEXT)),
226 #[cfg(not(target_os = "macos"))]
227 KeyBinding::new("ctrl-shift-end", SelectToEnd, Some(CONTEXT)),
228 KeyBinding::new("shift-home", SelectToStartOfLine, Some(CONTEXT)),
229 KeyBinding::new("shift-end", SelectToEndOfLine, Some(CONTEXT)),
230 #[cfg(target_os = "macos")]
231 KeyBinding::new("ctrl-shift-a", SelectToStartOfLine, Some(CONTEXT)),
232 #[cfg(target_os = "macos")]
233 KeyBinding::new("ctrl-shift-e", SelectToEndOfLine, Some(CONTEXT)),
234 #[cfg(target_os = "macos")]
235 KeyBinding::new("shift-cmd-left", SelectToStartOfLine, Some(CONTEXT)),
236 #[cfg(target_os = "macos")]
237 KeyBinding::new("shift-cmd-right", SelectToEndOfLine, Some(CONTEXT)),
238 #[cfg(any(target_os = "macos", target_os = "linux"))]
239 KeyBinding::new("alt-shift-left", SelectToPreviousWordStart, Some(CONTEXT)),
240 #[cfg(not(target_os = "macos"))]
241 KeyBinding::new("ctrl-shift-left", SelectToPreviousWordStart, Some(CONTEXT)),
242 #[cfg(any(target_os = "macos", target_os = "linux"))]
243 KeyBinding::new("alt-shift-right", SelectToNextWordEnd, Some(CONTEXT)),
244 #[cfg(not(target_os = "macos"))]
245 KeyBinding::new("ctrl-shift-right", SelectToNextWordEnd, Some(CONTEXT)),
246 #[cfg(target_os = "macos")]
247 KeyBinding::new("ctrl-cmd-space", ShowCharacterPalette, Some(CONTEXT)),
248 #[cfg(target_os = "macos")]
249 KeyBinding::new("cmd-a", SelectAll, Some(CONTEXT)),
250 #[cfg(not(target_os = "macos"))]
251 KeyBinding::new("ctrl-a", SelectAll, Some(CONTEXT)),
252 #[cfg(target_os = "macos")]
253 KeyBinding::new("cmd-c", Copy, Some(CONTEXT)),
254 #[cfg(not(target_os = "macos"))]
255 KeyBinding::new("ctrl-c", Copy, Some(CONTEXT)),
256 #[cfg(target_os = "macos")]
257 KeyBinding::new("cmd-x", Cut, Some(CONTEXT)),
258 #[cfg(not(target_os = "macos"))]
259 KeyBinding::new("ctrl-x", Cut, Some(CONTEXT)),
260 #[cfg(target_os = "macos")]
261 KeyBinding::new("cmd-v", Paste, Some(CONTEXT)),
262 #[cfg(not(target_os = "macos"))]
263 KeyBinding::new("ctrl-v", Paste, Some(CONTEXT)),
264 #[cfg(target_os = "macos")]
265 KeyBinding::new("ctrl-a", MoveHome, Some(CONTEXT)),
266 #[cfg(target_os = "macos")]
267 KeyBinding::new("cmd-left", MoveHome, Some(CONTEXT)),
268 #[cfg(target_os = "macos")]
269 KeyBinding::new("ctrl-e", MoveEnd, Some(CONTEXT)),
270 #[cfg(target_os = "macos")]
271 KeyBinding::new("cmd-right", MoveEnd, Some(CONTEXT)),
272 #[cfg(target_os = "macos")]
273 KeyBinding::new("cmd-z", Undo, Some(CONTEXT)),
274 #[cfg(target_os = "macos")]
275 KeyBinding::new("cmd-shift-z", Redo, Some(CONTEXT)),
276 #[cfg(target_os = "macos")]
277 KeyBinding::new("cmd-up", MoveToStart, Some(CONTEXT)),
278 #[cfg(target_os = "macos")]
279 KeyBinding::new("cmd-down", MoveToEnd, Some(CONTEXT)),
280 #[cfg(target_os = "macos")]
281 KeyBinding::new("alt-left", MoveToPreviousWord, Some(CONTEXT)),
282 #[cfg(target_os = "macos")]
283 KeyBinding::new("alt-right", MoveToNextWord, Some(CONTEXT)),
284 #[cfg(not(target_os = "macos"))]
285 KeyBinding::new("ctrl-left", MoveToPreviousWord, Some(CONTEXT)),
286 #[cfg(not(target_os = "macos"))]
287 KeyBinding::new("ctrl-right", MoveToNextWord, Some(CONTEXT)),
288 #[cfg(target_os = "macos")]
289 KeyBinding::new("cmd-shift-up", SelectToStart, Some(CONTEXT)),
290 #[cfg(target_os = "macos")]
291 KeyBinding::new("cmd-shift-down", SelectToEnd, Some(CONTEXT)),
292 #[cfg(not(target_os = "macos"))]
293 KeyBinding::new("ctrl-z", Undo, Some(CONTEXT)),
294 #[cfg(not(target_os = "macos"))]
295 KeyBinding::new("ctrl-y", Redo, Some(CONTEXT)),
296 #[cfg(target_os = "macos")]
297 KeyBinding::new("cmd-.", ToggleCodeActions, Some(CONTEXT)),
298 #[cfg(not(target_os = "macos"))]
299 KeyBinding::new("ctrl-.", ToggleCodeActions, Some(CONTEXT)),
300 #[cfg(target_os = "macos")]
301 KeyBinding::new("cmd-f", Search, Some(CONTEXT)),
302 #[cfg(not(target_os = "macos"))]
303 KeyBinding::new("ctrl-f", Search, Some(CONTEXT)),
304 #[cfg(target_os = "macos")]
305 KeyBinding::new("cmd-shift-f", Replace, Some(CONTEXT)),
306 #[cfg(not(target_os = "macos"))]
307 KeyBinding::new("ctrl-h", Replace, Some(CONTEXT)),
308 ]);
309}
310
311#[derive(Debug, Clone, Copy, PartialEq, Eq)]
318pub(super) struct ColumnarPoint {
319 offset: usize,
320 columns_past_line_end: usize,
321}
322
323impl ColumnarPoint {
324 pub(super) fn new(offset: usize, columns_past_line_end: usize) -> Self {
325 Self {
326 offset,
327 columns_past_line_end,
328 }
329 }
330}
331
332#[derive(Debug, PartialEq)]
336struct PasteTarget {
337 document_revision: u64,
338 selections: Vec<CursorSelection>,
339}
340
341#[derive(Clone, Copy, Debug)]
348#[doc(hidden)]
349pub struct PrepaintCaretGeometry {
350 cursor_bounds: Bounds<Pixels>,
351 line_height: Pixels,
352 input_bounds: Bounds<Pixels>,
353 scroll_offset: Point<Pixels>,
354}
355
356impl PrepaintCaretGeometry {
357 pub(super) fn new(
358 cursor_bounds: Bounds<Pixels>,
359 line_height: Pixels,
360 input_bounds: Bounds<Pixels>,
361 scroll_offset: Point<Pixels>,
362 ) -> Self {
363 Self {
364 cursor_bounds,
365 line_height,
366 input_bounds,
367 scroll_offset,
368 }
369 }
370
371 pub fn cursor_bounds(&self) -> Bounds<Pixels> {
373 self.cursor_bounds
374 }
375
376 pub fn line_height(&self) -> Pixels {
378 self.line_height
379 }
380
381 pub fn input_bounds(&self) -> Bounds<Pixels> {
383 self.input_bounds
384 }
385
386 pub fn scroll_offset(&self) -> Point<Pixels> {
389 self.scroll_offset
390 }
391}
392
393pub struct InputBaseState<M: InputModeKind> {
404 pub(crate) extras: M::Extras,
406 pub(super) focus_handle: FocusHandle,
407 selection_focus: Option<WeakFocusHandle>,
408 pub(super) mode: LayoutMode,
409 pub(super) text: Rope,
410 pub(super) display_map: DisplayMap,
411 pub(super) undo_manager: UndoManager,
412 pub(super) inline_tokens: Option<Box<super::inline_tokens::InlineTokenStore>>,
413 pub(super) pending_token: Option<super::InlineToken>,
414 pub(super) replaying_history: bool,
415 pub(super) validated_token_edit: bool,
416 pub(super) document_revision: u64,
417 pub(super) token_presentation: super::InlineTokenPresentation,
418 pub(super) token_layout_cache: Option<Box<super::token_presentation::TokenLayoutCache>>,
419 pub(super) token_bounds: std::collections::HashMap<usize, Bounds<Pixels>>,
422 pub(super) hovered_token: Option<super::token_presentation::HoverSnapshot>,
427 pub(super) pending_token_hover_exits: Vec<super::token_presentation::HoverSnapshot>,
428 pub(super) pressed_token: Option<(usize, u64, Point<Pixels>)>,
431 pub(super) search_session: super::SearchSession,
432 pub(super) search_activation_revision: u64,
435 pub(super) searchable: bool,
436 pub(super) replaceable: bool,
437 pub(super) soft_wrap: bool,
438 pub(super) wrapping_indent: WrappingIndent,
439 pub(super) scroll_beyond_last_line: Option<usize>,
440 pub(super) cursor_surrounding_lines: Option<usize>,
441 pub(super) blink_cursor: Entity<BlinkCursor>,
442 pub(super) loading: bool,
443 pub(super) selections: Selections,
447 pub(super) selected_word_range: Option<CursorSelection>,
449 pub(super) ime_marked_range: Option<CursorSelection>,
451 pub(super) last_layout: Option<LastLayout>,
452 pub(crate) prepaint_caret_geometry: Option<PrepaintCaretGeometry>,
458 pub(super) last_cursor: Option<usize>,
459 pub(super) input_bounds: Bounds<Pixels>,
461 pub(super) last_bounds: Option<Bounds<Pixels>>,
463 pub(super) last_selected_range: Option<CursorSelection>,
464 pub(super) selecting: bool,
465 pub(super) column_select_start: Option<ColumnarPoint>,
467 pub(super) touch_selection: super::touch::TouchSelection,
469 pub(crate) disabled: bool,
470 pub(crate) readonly: bool,
471 pub(crate) text_align: TextAlign,
472 pub(super) masked: bool,
473 pub(super) clean_on_escape: bool,
474 pub(super) submit_on_enter: bool,
475 pub(super) show_whitespaces: bool,
476 pub(crate) cursor_line_end_affinity: bool,
478 pub(super) pattern: Option<regex::Regex>,
479 pub(super) validate: Option<Box<dyn Fn(&str, &mut App) -> bool + 'static>>,
480 pub(crate) number_step: Option<NumberStep>,
483 pub(crate) number_min: Option<f64>,
485 pub(crate) number_max: Option<f64>,
487 pub(crate) scroll_handle: ScrollHandle,
488 pub(crate) deferred_scroll_offset: Option<Point<Pixels>>,
490 pub(crate) scroll_size: gpui::Size<Pixels>,
492 pub(super) editor_scrollbar_snapshot: Cell<Option<EditorScrollbarSnapshot>>,
493 pub(super) longest_line_width: Cell<Option<(LongestLineKey, Pixels)>>,
495 pub(super) editor_paddings: Edges<Pixels>,
496 pub(super) editor_style: InputEditorStyle,
501 projected_editor_style: InputEditorStyle,
506
507 pub(crate) mask_pattern: MaskPattern,
509 pub(super) mask_pattern_set: bool,
513 pub(super) placeholder: SharedString,
514
515 pub(super) diagnostic_popover: Option<Rc<crate::input::DiagnosticEntry>>,
517
518 context_menu_handler: Option<
519 Rc<dyn Fn(NativeMenu, InputContextMenuCapabilities, Point<Pixels>, &mut Window, &mut App)>,
520 >,
521 pending_context_menu: Option<(Point<Pixels>, usize)>,
522
523 pub(super) enable_context_menu: bool,
526
527 pub(super) completion_inserting: bool,
529 pub(super) overlay_action_handler: Option<
530 Rc<
531 dyn Fn(
532 super::InputOverlayKind,
533 Box<dyn Action>,
534 &mut Window,
535 &mut Context<InputBaseState<M>>,
536 ) -> bool,
537 >,
538 >,
539
540 _pending_update: bool,
544 pub(super) silent_replace_text: bool,
546 pub(super) emit_events: bool,
548
549 _subscriptions: Vec<Subscription>,
550
551 pub(super) auto_scroll: AutoScroll,
552}
553
554#[derive(Clone)]
559pub struct InputPresentation {
560 focus_handle: FocusHandle,
561 disabled: bool,
562 readonly: bool,
563 loading: bool,
564 masked: bool,
565 multi_line: bool,
566 code_editor: bool,
567 text_align: TextAlign,
568 placeholder: SharedString,
569 mask_placeholder: Option<String>,
570}
571
572impl InputPresentation {
573 pub fn focus_handle(&self) -> &FocusHandle {
574 &self.focus_handle
575 }
576
577 pub fn is_disabled(&self) -> bool {
578 self.disabled
579 }
580
581 pub fn is_readonly(&self) -> bool {
582 self.readonly
583 }
584
585 pub fn is_editable(&self) -> bool {
589 !self.disabled && !self.readonly
590 }
591
592 pub fn is_loading(&self) -> bool {
593 self.loading
594 }
595
596 pub fn is_masked(&self) -> bool {
597 self.masked
598 }
599
600 pub fn is_multi_line(&self) -> bool {
601 self.multi_line
602 }
603
604 pub fn is_code_editor(&self) -> bool {
605 self.code_editor
606 }
607
608 pub fn text_align(&self) -> TextAlign {
609 self.text_align
610 }
611
612 pub fn placeholder(&self) -> &SharedString {
613 &self.placeholder
614 }
615
616 pub fn mask_placeholder(&self) -> Option<&str> {
618 self.mask_placeholder.as_deref()
619 }
620}
621
622impl<M: InputModeKind> EventEmitter<InputEvent> for InputBaseState<M> {}
623
624impl<M: InputModeKind> InputBaseState<M> {
625 #[doc(hidden)]
626 pub fn cursor_layout(&self) -> Option<(Bounds<Pixels>, Pixels)> {
627 let layout = self.last_layout.as_ref()?;
628 Some((layout.cursor_bounds?, layout.line_height))
629 }
630
631 #[doc(hidden)]
636 pub fn prepaint_caret_geometry(&self) -> Option<PrepaintCaretGeometry> {
637 self.prepaint_caret_geometry
638 }
639
640 pub fn input_bounds(&self) -> Bounds<Pixels> {
641 self.input_bounds
642 }
643
644 pub fn text_bounds(&self) -> Option<Bounds<Pixels>> {
645 self.last_bounds
646 }
647
648 pub fn diagnostic_popover(&self) -> Option<Rc<crate::input::DiagnosticEntry>> {
649 self.diagnostic_popover.clone()
650 }
651
652 pub fn presentation(&self) -> InputPresentation {
653 InputPresentation {
654 focus_handle: self.focus_handle.clone(),
655 disabled: self.disabled,
656 readonly: self.readonly,
657 loading: self.loading,
658 masked: self.masked,
659 multi_line: self.is_multi_line(),
660 code_editor: self.is_code_editor(),
661 text_align: self.text_align,
662 placeholder: self.placeholder.clone(),
663 mask_placeholder: self.mask_pattern.placeholder(),
664 }
665 }
666
667 #[inline]
672 pub(crate) fn shows_scrollbar(&self) -> bool {
680 self.is_multi_line()
681 }
682
683 pub fn is_multi_line(&self) -> bool {
684 M::MULTI_LINE
685 }
686
687 #[inline]
689 pub fn is_single_line(&self) -> bool {
690 !M::MULTI_LINE
691 }
692
693 #[inline]
695 pub fn is_code_editor(&self) -> bool {
696 M::CODE_EDITOR
697 }
698
699 pub fn is_copyable(&self) -> bool {
703 self.selections.iter().any(|sel| !sel.is_empty()) && !self.masked
704 }
705
706 pub fn context_menu_capabilities(&self) -> InputContextMenuCapabilities {
707 let (go_to_definition, code_actions) = self.extras.context_menu_capabilities();
708 InputContextMenuCapabilities::new()
709 .disabled(self.disabled)
710 .readonly(self.readonly)
711 .code_editor(self.is_code_editor())
712 .selection(!self.active_selection().is_empty())
713 .masked(self.masked)
714 .go_to_definition(go_to_definition)
715 .code_actions(code_actions)
716 }
717
718 pub fn set_text_align(&mut self, text_align: TextAlign, cx: &mut Context<Self>) {
719 if !self.is_single_line() || self.text_align == text_align {
720 return;
721 }
722
723 self.text_align = text_align;
724 cx.notify();
725 }
726
727 pub fn toggle_masked(&mut self, _: &mut Window, cx: &mut Context<Self>) {
733 self.masked = !self.masked;
734 cx.notify();
735 }
736
737 pub fn on_context_menu(
738 &mut self,
739 handler: Rc<
740 dyn Fn(NativeMenu, InputContextMenuCapabilities, Point<Pixels>, &mut Window, &mut App),
741 >,
742 ) {
743 self.context_menu_handler = Some(handler);
744 }
745
746 pub fn set_selection_focus(&mut self, focus: Option<FocusHandle>, cx: &mut Context<Self>) {
751 self.selection_focus = focus.as_ref().map(FocusHandle::downgrade);
752 cx.notify();
753 }
754
755 pub fn has_selection_focus(&self, window: &Window, cx: &App) -> bool {
757 self.focus_handle.is_focused(window)
758 || self
759 .selection_focus
760 .as_ref()
761 .and_then(WeakFocusHandle::upgrade)
762 .is_some_and(|focus| focus.is_focused(window) || focus.contains_focused(window, cx))
763 }
764
765 fn new_in_mode(window: &mut Window, cx: &mut Context<Self>) -> Self {
767 let focus_handle = cx.focus_handle().tab_stop(true);
768 let blink_cursor = cx.new(|_| BlinkCursor::new());
769 let undo_manager = UndoManager::new();
770
771 let _subscriptions = vec![
772 cx.intercept_keystrokes({
775 let focus_handle = focus_handle.clone();
776 let blink_cursor = blink_cursor.downgrade();
777 move |_, window, cx| {
778 if focus_handle.is_focused(window) {
779 _ = blink_cursor.update(cx, |cursor, cx| cursor.pause(cx));
780 }
781 }
782 }),
783 cx.observe(&blink_cursor, |_, _, cx| cx.notify()),
785 cx.observe_window_activation(window, |input, window, cx| {
787 if window.is_window_active() {
788 let focus_handle = input.focus_handle.clone();
789 if focus_handle.is_focused(window) {
790 input.blink_cursor.update(cx, |blink_cursor, cx| {
791 blink_cursor.start(cx);
792 });
793 }
794 }
795 }),
796 cx.on_focus(&focus_handle, window, Self::on_focus),
797 cx.on_blur(&focus_handle, window, Self::on_blur),
798 ];
799
800 let text_style = window.text_style();
801
802 Self {
803 extras: M::Extras::default(),
804 focus_handle: focus_handle.clone(),
805 selection_focus: None,
806 text: "".into(),
807 display_map: DisplayMap::new(text_style.font(), window.rem_size(), None),
808 search_session: super::SearchSession::default(),
809 search_activation_revision: 0,
810 searchable: false,
811 replaceable: true,
812 soft_wrap: true,
813 wrapping_indent: WrappingIndent::default(),
814 scroll_beyond_last_line: None,
815 cursor_surrounding_lines: None,
816 blink_cursor,
817 undo_manager,
818 inline_tokens: None,
819 pending_token: None,
820 replaying_history: false,
821 validated_token_edit: false,
822 document_revision: 0,
823 token_presentation: Default::default(),
824 token_bounds: Default::default(),
825 hovered_token: None,
826 pending_token_hover_exits: Vec::new(),
827 token_layout_cache: None,
828 pressed_token: None,
829 selections: Selections::default(),
830 selected_word_range: None,
831 ime_marked_range: None,
832 input_bounds: Bounds::default(),
833 selecting: false,
834 touch_selection: Default::default(),
835 disabled: false,
836 readonly: false,
837 text_align: TextAlign::Left,
838 masked: false,
839 clean_on_escape: false,
840 submit_on_enter: false,
841 show_whitespaces: false,
842 loading: false,
843 pattern: None,
844 validate: None,
845 number_step: Some(NumberStep::Fixed(1.)),
846 number_min: None,
847 number_max: None,
848 mode: LayoutMode::default(),
849 last_layout: None,
850 prepaint_caret_geometry: None,
851 last_bounds: None,
852 last_selected_range: None,
853 column_select_start: None,
854 last_cursor: None,
855 scroll_handle: ScrollHandle::new(),
856 scroll_size: gpui::size(px(0.), px(0.)),
857 editor_scrollbar_snapshot: Cell::new(None),
858 longest_line_width: Cell::new(None),
859 editor_paddings: Edges::default(),
860 deferred_scroll_offset: None,
861 placeholder: SharedString::default(),
862 mask_pattern: MaskPattern::default(),
863 mask_pattern_set: false,
864 editor_style: InputEditorStyle::default(),
865 projected_editor_style: InputEditorStyle::default(),
866 diagnostic_popover: None,
867 context_menu_handler: None,
868 pending_context_menu: None,
869 enable_context_menu: true,
870 completion_inserting: false,
871 overlay_action_handler: None,
872 silent_replace_text: false,
873 emit_events: true,
874 _subscriptions,
875 _pending_update: false,
876 cursor_line_end_affinity: false,
877 auto_scroll: AutoScroll::default(),
878 }
879 }
880
881 pub fn context_menu(mut self, enable: bool) -> Self {
888 self.enable_context_menu = enable;
889 self
890 }
891
892 pub fn set_context_menu_enabled(&mut self, enabled: bool) {
893 self.enable_context_menu = enabled;
894 }
895
896 #[doc(hidden)]
898 pub fn replaceable(mut self, allow: bool) -> Self {
899 self.replaceable = allow;
900 self
901 }
902
903 pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
905 self.placeholder = placeholder.into();
906 self
907 }
908
909 pub fn set_highlighter(
911 &mut self,
912 new_language: impl Into<SharedString>,
913 cx: &mut Context<Self>,
914 ) {
915 match &mut self.mode {
916 LayoutMode::CodeEditor {
917 language,
918 highlighter,
919 ..
920 } => {
921 language.set_name(new_language.into());
922 *highlighter.borrow_mut() = None;
923 }
924 _ => {}
925 }
926 self.refresh(cx);
927 }
928
929 fn reset_highlighter(&mut self, cx: &mut Context<Self>) {
930 match &mut self.mode {
931 LayoutMode::CodeEditor { highlighter, .. } => {
932 *highlighter.borrow_mut() = None;
933 }
934 _ => {}
935 }
936 cx.notify();
937 }
938
939 pub fn set_highlighter_factory(
941 &mut self,
942 factory: InputHighlighterFactory,
943 cx: &mut Context<Self>,
944 ) {
945 self.mode.set_highlighter_factory(factory);
946 self._pending_update = true;
947 self.token_layout_cache = None;
948 cx.notify();
949 }
950
951 pub fn ensure_highlighter_factory(&mut self, factory: InputHighlighterFactory) {
953 self.mode.ensure_highlighter_factory(factory);
954 }
955
956 pub fn set_editor_style(&mut self, style: InputEditorStyle) {
957 self.editor_style = style.clone();
958 self.projected_editor_style = style;
959 }
960
961 #[doc(hidden)]
963 pub fn set_editor_paddings(&mut self, paddings: Edges<Pixels>) {
964 self.editor_paddings = paddings;
965 }
966
967 pub fn apply_highlighter_fold_candidates(
968 &mut self,
969 candidates: Vec<crate::input::FoldRange>,
970 cx: &mut Context<Self>,
971 ) {
972 if self.mode.is_folding() {
973 self.display_map.set_fold_candidates(candidates);
974 }
975 cx.notify();
976 }
977
978 #[inline]
979 pub fn diagnostics(&self) -> Option<&DiagnosticSet> {
980 self.mode.diagnostics()
981 }
982
983 #[inline]
984 pub fn diagnostics_mut(&mut self) -> Option<&mut DiagnosticSet> {
985 self.mode.diagnostics_mut()
986 }
987
988 pub fn set_placeholder(
990 &mut self,
991 placeholder: impl Into<SharedString>,
992 _: &mut Window,
993 cx: &mut Context<Self>,
994 ) {
995 self.placeholder = placeholder.into();
996 cx.notify();
997 }
998
999 pub(super) fn line_and_position_for_offset(
1007 &self,
1008 offset: usize,
1009 ) -> (usize, usize, Option<Point<Pixels>>) {
1010 let Some(last_layout) = &self.last_layout else {
1011 return (0, 0, None);
1012 };
1013 let line_height = last_layout.line_height;
1014
1015 let mut y_offset = last_layout.visible_top;
1016 for (vi, line) in last_layout.lines.iter().enumerate() {
1017 let prev_lines_offset = last_layout.visible_line_byte_offsets[vi];
1018 let local_offset = offset.saturating_sub(prev_lines_offset);
1019 if let Some(pos) = line.position_for_index(local_offset, last_layout, false) {
1020 let sub_line_index = (pos.y / line_height) as usize;
1021 let adjusted_pos = point(pos.x + last_layout.line_number_width, pos.y + y_offset);
1022 return (vi, sub_line_index, Some(adjusted_pos));
1023 }
1024
1025 y_offset += line.size(line_height).height;
1026 }
1027 (0, 0, None)
1028 }
1029
1030 pub fn set_value(
1039 &mut self,
1040 value: impl Into<super::InputContent>,
1041 window: &mut Window,
1042 cx: &mut Context<Self>,
1043 ) {
1044 let content = value.into();
1045 self.inline_tokens = None;
1046 self.undo_manager.set_ignoring(true);
1047 self.emit_events = false;
1048 self.replace_text(content.text().clone(), window, cx);
1049 self.install_tokens(content);
1050 self.undo_manager.set_ignoring(false);
1051 self.emit_events = true;
1052
1053 self.reset_selection();
1054 self.reset_lsp_state();
1055 self.reset_scroll_to_start();
1056
1057 self.undo_manager.clear();
1058 cx.notify();
1059 }
1060
1061 pub fn replace_all(
1071 &mut self,
1072 text: impl Into<SharedString>,
1073 window: &mut Window,
1074 cx: &mut Context<Self>,
1075 ) {
1076 self.replace_text(text, window, cx);
1077 self.reset_selection();
1078 self.reset_lsp_state();
1079 self.reset_scroll_to_start();
1080
1081 cx.notify();
1082 }
1083
1084 pub(super) fn with_edits_allowed(&mut self, f: impl FnOnce(&mut Self)) {
1089 let (was_disabled, was_readonly) = (self.disabled, self.readonly);
1090 (self.disabled, self.readonly) = (false, false);
1091 f(self);
1092 (self.disabled, self.readonly) = (was_disabled, was_readonly);
1093 }
1094
1095 pub fn insert(
1099 &mut self,
1100 text: impl Into<SharedString>,
1101 window: &mut Window,
1102 cx: &mut Context<Self>,
1103 ) {
1104 let text: SharedString = text.into();
1105 self.with_edits_allowed(|this| {
1106 this.undo_manager.set_pending_intent(EditIntent::Atomic);
1107 let range_utf16 = this.range_to_utf16(&(this.cursor()..this.cursor()));
1108 this.replace_text_in_range_silent(Some(range_utf16), &text, window, cx);
1109 let end = this.active_selection().end;
1110 this.set_cursor_to(end);
1111 });
1112 }
1113
1114 pub fn replace(
1118 &mut self,
1119 text: impl Into<SharedString>,
1120 window: &mut Window,
1121 cx: &mut Context<Self>,
1122 ) {
1123 let text: SharedString = text.into();
1124 self.with_edits_allowed(|this| {
1125 this.undo_manager.set_pending_intent(EditIntent::Atomic);
1126 this.replace_text_in_range_silent(None, &text, window, cx);
1127 let end = this.active_selection().end;
1128 this.set_cursor_to(end);
1129 });
1130 }
1131
1132 fn replace_text(
1133 &mut self,
1134 text: impl Into<SharedString>,
1135 window: &mut Window,
1136 cx: &mut Context<Self>,
1137 ) {
1138 let text: SharedString = text.into();
1139 self.with_edits_allowed(|this| {
1140 this.undo_manager.set_pending_intent(EditIntent::Atomic);
1141 let range = 0..this.text.chars().map(|c| c.len_utf16()).sum();
1142 this.replace_text_in_range_silent(Some(range), &text, window, cx);
1143 this.reset_highlighter(cx);
1144 });
1145 }
1146
1147 fn reset_selection(&mut self) {
1148 self.selections.remove_all_but_active();
1149
1150 if self.is_single_line() {
1154 let end = self.text.len();
1155 self.set_cursor_to(end);
1156 } else {
1157 self.active_selection_mut().clear();
1158 }
1159 }
1160
1161 fn reset_lsp_state(&mut self) {
1162 if self.is_code_editor() {
1163 self._pending_update = true;
1164 M::reset_language_features(self);
1165 }
1166 }
1167
1168 fn reset_scroll_to_start(&mut self) {
1169 self.scroll_handle.set_offset(point(px(0.), px(0.)));
1173 if self.is_single_line() {
1174 self.deferred_scroll_offset = Some(point(px(0.), px(0.)));
1175 }
1176 }
1177
1178 #[allow(unused)]
1182 pub(crate) fn disabled(mut self, disabled: bool) -> Self {
1183 self.disabled = disabled;
1184 self
1185 }
1186
1187 pub fn set_disabled(&mut self, disabled: bool, cx: &mut Context<Self>) {
1188 if self.disabled == disabled {
1189 return;
1190 }
1191
1192 self.disabled = disabled;
1193 if disabled {
1194 M::hide_context_menu(self, cx);
1195 M::clear_inline_completion(self, cx);
1196 }
1197 cx.notify();
1198 }
1199
1200 #[allow(unused)]
1208 pub(crate) fn readonly(mut self, readonly: bool) -> Self {
1209 self.readonly = readonly;
1210 self
1211 }
1212
1213 pub fn set_readonly(&mut self, readonly: bool, cx: &mut Context<Self>) {
1214 if self.readonly == readonly {
1215 return;
1216 }
1217
1218 self.readonly = readonly;
1219 if readonly {
1220 M::hide_context_menu(self, cx);
1221 M::clear_inline_completion(self, cx);
1222 self.search_session.replace_mode = false;
1223 }
1224 cx.notify();
1225 }
1226
1227 pub fn is_editable(&self) -> bool {
1232 !self.disabled && !self.readonly
1233 }
1234
1235 pub fn clean_on_escape(mut self) -> Self {
1237 self.clean_on_escape = true;
1238 self
1239 }
1240
1241 pub fn set_clean_on_escape(&mut self, clean: bool) {
1242 self.clean_on_escape = clean;
1243 }
1244
1245 #[doc(hidden)]
1250 pub fn submit_on_enter(mut self, submit: bool) -> Self {
1251 self.submit_on_enter = submit;
1252 self
1253 }
1254
1255 pub fn set_submit_on_enter(&mut self, submit: bool, cx: &mut Context<Self>) {
1256 self.submit_on_enter = submit;
1257 cx.notify();
1258 }
1259
1260 #[doc(hidden)]
1262 pub fn show_whitespaces(mut self, show: bool) -> Self {
1263 self.show_whitespaces = show;
1264 self
1265 }
1266
1267 pub fn set_show_whitespaces(&mut self, show: bool, _: &mut Window, cx: &mut Context<Self>) {
1269 self.show_whitespaces = show;
1270 cx.notify();
1271 }
1272
1273 pub fn scroll_beyond_last_line(mut self, rows: Option<usize>) -> Self {
1283 self.scroll_beyond_last_line = rows;
1284 self
1285 }
1286
1287 pub fn set_scroll_beyond_last_line(
1289 &mut self,
1290 rows: Option<usize>,
1291 _: &mut Window,
1292 cx: &mut Context<Self>,
1293 ) {
1294 if self.scroll_beyond_last_line == rows {
1295 return;
1296 }
1297 self.scroll_beyond_last_line = rows;
1298 cx.notify();
1299 }
1300
1301 pub fn cursor_surrounding_lines(mut self, lines: Option<usize>) -> Self {
1312 self.cursor_surrounding_lines = lines;
1313 self
1314 }
1315
1316 pub fn set_cursor_surrounding_lines(
1318 &mut self,
1319 lines: Option<usize>,
1320 _: &mut Window,
1321 cx: &mut Context<Self>,
1322 ) {
1323 if self.cursor_surrounding_lines == lines {
1324 return;
1325 }
1326 self.cursor_surrounding_lines = lines;
1327 cx.notify();
1328 }
1329
1330 pub fn default_value(mut self, value: impl Into<SharedString>) -> Self {
1332 let text: SharedString = value.into();
1333 self.text = Rope::from(self.normalize_input(&text).as_ref());
1334 if let Some(diagnostics) = self.mode.diagnostics_mut() {
1335 diagnostics.reset(&self.text)
1336 }
1337 self._pending_update = true;
1340 self
1341 }
1342
1343 pub fn value(&self) -> SharedString {
1348 SharedString::new(self.text.to_string())
1349 }
1350
1351 pub fn selected_value(&self) -> SharedString {
1357 SharedString::new(self.selected_text().to_string())
1358 }
1359
1360 pub fn unmask_value(&self) -> SharedString {
1362 self.mask_pattern.unmask(&self.text.to_string()).into()
1363 }
1364
1365 #[doc(hidden)]
1371 pub fn prepare(&mut self, _: &mut Window, _: &mut Context<Self>) {}
1372
1373 pub fn text(&self) -> &Rope {
1378 &self.text
1379 }
1380
1381 pub fn cursor_position(&self) -> Position {
1383 let offset = self.cursor();
1384 self.text.offset_to_position(offset)
1385 }
1386
1387 pub fn set_cursor_position(
1391 &mut self,
1392 position: impl Into<Position>,
1393 window: &mut Window,
1394 cx: &mut Context<Self>,
1395 ) {
1396 let position: Position = position.into();
1397 let offset = self.text.position_to_offset(&position);
1398
1399 self.move_to(offset, None, cx);
1400 self.update_preferred_column();
1401 self.focus(window, cx);
1402 }
1403
1404 pub fn focus(&self, window: &mut Window, cx: &mut Context<Self>) {
1406 self.focus_handle.focus(window, cx);
1407 self.blink_cursor.update(cx, |cursor, cx| {
1408 cursor.start(cx);
1409 });
1410 }
1411
1412 pub fn refresh(&mut self, cx: &mut Context<Self>) {
1426 self._pending_update = true;
1427 cx.notify();
1428 }
1429
1430 pub(super) fn select_left(&mut self, _: &SelectLeft, _: &mut Window, cx: &mut Context<Self>) {
1431 self.undo_manager.break_transaction_coalescing();
1432 self.select_all_cursors_to(|s, sel| s.previous_boundary(sel.cursor_offset()), cx);
1433 }
1434
1435 pub(super) fn select_right(&mut self, _: &SelectRight, _: &mut Window, cx: &mut Context<Self>) {
1436 self.select_all_cursors_to(|s, sel| s.next_boundary(sel.cursor_offset()), cx);
1437 }
1438
1439 pub(super) fn select_up(&mut self, _: &SelectUp, _: &mut Window, cx: &mut Context<Self>) {
1440 if self.is_single_line() {
1441 return;
1442 }
1443 self.select_all_cursors_to_with_affinity(
1444 |s, sel| {
1445 s.vertical_selection_target(
1446 sel.cursor_offset(),
1447 sel.column_anchor,
1448 s.line_end_affinity_for(sel),
1449 -1,
1450 )
1451 },
1452 true,
1453 cx,
1454 );
1455 }
1456
1457 pub(super) fn select_down(&mut self, _: &SelectDown, _: &mut Window, cx: &mut Context<Self>) {
1458 if self.is_single_line() {
1459 return;
1460 }
1461 self.select_all_cursors_to_with_affinity(
1462 |s, sel| {
1463 s.vertical_selection_target(
1464 sel.cursor_offset(),
1465 sel.column_anchor,
1466 s.line_end_affinity_for(sel),
1467 1,
1468 )
1469 },
1470 true,
1471 cx,
1472 );
1473 }
1474
1475 pub(super) fn on_action_select_all(
1476 &mut self,
1477 _: &SelectAll,
1478 window: &mut Window,
1479 cx: &mut Context<Self>,
1480 ) {
1481 self.select_all(window, cx);
1482 }
1483
1484 pub(super) fn select_to_start(
1485 &mut self,
1486 _: &SelectToStart,
1487 _: &mut Window,
1488 cx: &mut Context<Self>,
1489 ) {
1490 self.undo_manager.break_transaction_coalescing();
1491 self.select_all_cursors_to(|_, _| 0, cx);
1492 }
1493
1494 pub(super) fn select_to_end(
1495 &mut self,
1496 _: &SelectToEnd,
1497 _: &mut Window,
1498 cx: &mut Context<Self>,
1499 ) {
1500 self.undo_manager.break_transaction_coalescing();
1501 let end = self.text.len();
1502 self.select_all_cursors_to(move |_, _| end, cx);
1503 }
1504
1505 pub(super) fn select_to_start_of_line(
1506 &mut self,
1507 _: &SelectToStartOfLine,
1508 _: &mut Window,
1509 cx: &mut Context<Self>,
1510 ) {
1511 self.undo_manager.break_transaction_coalescing();
1512 self.select_all_cursors_to(
1513 |s, sel| s.start_of_line_at(sel.cursor_offset(), s.line_end_affinity_for(sel)),
1514 cx,
1515 );
1516 }
1517
1518 pub(super) fn select_to_end_of_line(
1519 &mut self,
1520 _: &SelectToEndOfLine,
1521 _: &mut Window,
1522 cx: &mut Context<Self>,
1523 ) {
1524 self.undo_manager.break_transaction_coalescing();
1525 self.select_all_cursors_to_with_affinity(
1526 |s, sel| {
1527 (
1528 s.end_of_line_at(sel.cursor_offset(), s.line_end_affinity_for(sel)),
1529 true,
1530 )
1531 },
1532 false,
1533 cx,
1534 );
1535 }
1536
1537 pub(super) fn select_to_previous_word(
1538 &mut self,
1539 _: &SelectToPreviousWordStart,
1540 _: &mut Window,
1541 cx: &mut Context<Self>,
1542 ) {
1543 self.undo_manager.break_transaction_coalescing();
1544 self.select_all_cursors_to(
1545 |s, sel| s.previous_start_of_word_at(sel.cursor_offset()),
1546 cx,
1547 );
1548 }
1549
1550 pub(super) fn select_to_next_word(
1551 &mut self,
1552 _: &SelectToNextWordEnd,
1553 _: &mut Window,
1554 cx: &mut Context<Self>,
1555 ) {
1556 self.undo_manager.break_transaction_coalescing();
1557 self.select_all_cursors_to(|s, sel| s.next_end_of_word_at(sel.cursor_offset()), cx);
1558 }
1559
1560 pub(super) fn previous_start_of_word_at(&self, offset: usize) -> usize {
1563 if self.masked {
1564 return 0;
1568 }
1569
1570 let offset = self.offset_from_utf16(self.offset_to_utf16(offset));
1571 let left_part = self.text.slice(0..offset).to_string();
1573
1574 let target = UnicodeSegmentation::split_word_bound_indices(left_part.as_str())
1575 .rfind(|(_, s)| !s.trim_start().is_empty())
1576 .map(|(i, _)| i)
1577 .unwrap_or(0);
1578 self.token_boundary(target, Bias::Left)
1579 }
1580
1581 pub(super) fn next_end_of_word_at(&self, offset: usize) -> usize {
1583 if self.masked {
1584 return self.text.len();
1586 }
1587
1588 let offset = self.offset_from_utf16(self.offset_to_utf16(offset));
1589 let right_part = self.text.slice(offset..self.text.len()).to_string();
1590
1591 let target = UnicodeSegmentation::split_word_bound_indices(right_part.as_str())
1592 .find(|(_, s)| !s.trim_start().is_empty())
1593 .map(|(i, s)| offset + i + s.len())
1594 .unwrap_or(self.text.len());
1595 self.token_boundary(target, Bias::Right)
1596 }
1597
1598 pub(super) fn start_of_line_at(&self, offset: usize, line_end_affinity: bool) -> usize {
1603 if self.is_single_line() {
1604 return 0;
1605 }
1606
1607 let row = self.text.offset_to_point(offset).row;
1608 let logical_start = self.text.line_start_offset(row);
1609
1610 if self.soft_wrap && self.is_code_editor() {
1611 let wrap_point = self
1612 .display_map
1613 .offset_to_wrap_display_point_with_affinity(offset, line_end_affinity);
1614 if let Some(line) = self.display_map.line(row)
1615 && let Some(range) = line.wrapped_lines.get(wrap_point.local_row)
1616 {
1617 let visual_start = logical_start + range.start;
1618 if offset != visual_start {
1619 return visual_start;
1620 }
1621 }
1622 }
1623
1624 logical_start
1625 }
1626
1627 pub(super) fn end_of_line_at(&self, offset: usize, line_end_affinity: bool) -> usize {
1632 if self.is_single_line() {
1633 return self.text.len();
1634 }
1635
1636 let row = self.text.offset_to_point(offset).row;
1637 let logical_start = self.text.line_start_offset(row);
1638 let logical_end = self.text.line_end_offset(row);
1639
1640 if self.soft_wrap && self.is_code_editor() {
1641 let wrap_point = self
1645 .display_map
1646 .offset_to_wrap_display_point_with_affinity(offset, line_end_affinity);
1647 if let Some(line) = self.display_map.line(row)
1648 && let Some(range) = line.wrapped_lines.get(wrap_point.local_row)
1649 {
1650 let visual_end = logical_start + range.end;
1651 if offset != visual_end {
1652 return visual_end;
1653 }
1654 }
1655 }
1656
1657 logical_end
1658 }
1659
1660 pub(super) fn indent_of_next_line(&mut self) -> String {
1664 self.indent_of_next_line_at(self.cursor())
1665 }
1666
1667 pub(super) fn indent_of_next_line_at(&mut self, offset: usize) -> String {
1669 if self.is_single_line() {
1670 return "".into();
1671 }
1672
1673 let mut current_indent = String::new();
1674 let mut next_indent = String::new();
1675 let line_end_affinity = self.line_end_affinity_at(offset);
1676 let current_line_start_pos = self.start_of_line_at(offset, line_end_affinity);
1677 let next_line_start_pos = self.end_of_line_at(offset, line_end_affinity);
1678 for c in self.text.slice(current_line_start_pos..).chars() {
1679 if !c.is_whitespace() {
1680 break;
1681 }
1682 if c == '\n' || c == '\r' {
1683 break;
1684 }
1685 current_indent.push(c);
1686 }
1687
1688 for c in self.text.slice(next_line_start_pos..).chars() {
1689 if !c.is_whitespace() {
1690 break;
1691 }
1692 if c == '\n' || c == '\r' {
1693 break;
1694 }
1695 next_indent.push(c);
1696 }
1697
1698 if next_indent.len() > current_indent.len() {
1699 return next_indent;
1700 }
1701
1702 if let Some(rules) = self.mode.language_config() {
1705 if self.mode.is_smart_indent()
1706 && M::editing_syntax_context(self, offset) == crate::input::SyntaxContext::Code
1707 {
1708 let line_before: String = self
1709 .text
1710 .slice(current_line_start_pos..offset)
1711 .chars()
1712 .collect();
1713 if rules.opens_indent(line_before.trim_end()) {
1714 let tab = self.mode.tab_size();
1715 if tab.hard_tabs {
1716 current_indent.push('\t');
1717 } else {
1718 for _ in 0..tab.tab_size {
1719 current_indent.push(' ');
1720 }
1721 }
1722 } else {
1723 let line_after = self.text.slice(offset..next_line_start_pos).to_string();
1724 if rules.closes_indent(&line_after) {
1725 let tab = self.mode.tab_size();
1726 if current_indent.ends_with('\t') {
1727 current_indent.pop();
1728 } else {
1729 for _ in 0..tab.tab_size {
1730 if !current_indent.ends_with(' ') {
1731 break;
1732 }
1733 current_indent.pop();
1734 }
1735 }
1736 }
1737 }
1738 }
1739 }
1740
1741 current_indent
1742 }
1743
1744 fn delete_selections(
1752 &mut self,
1753 silent: bool,
1754 collapsed_intent: EditIntent,
1755 mut collapsed_target: impl FnMut(&mut Self, usize) -> Range<usize>,
1756 window: &mut Window,
1757 cx: &mut Context<Self>,
1758 ) {
1759 if !self.is_editable() {
1760 return;
1761 }
1762 let cursors: Vec<CursorSelection> = self.selections.iter().copied().collect();
1763 let intent = if cursors.iter().all(|sel| sel.is_empty()) {
1764 collapsed_intent
1765 } else {
1766 EditIntent::Atomic
1767 };
1768 let mut new_selections: Vec<CursorSelection> = Vec::with_capacity(cursors.len());
1769 for sel in &cursors {
1770 let range = if sel.is_empty() {
1771 collapsed_target(self, sel.cursor_offset())
1772 } else {
1773 sel.start..sel.end
1774 };
1775 let (start, end) = (range.start.min(range.end), range.start.max(range.end));
1776 let mut selection = *sel;
1777 selection.start = start;
1778 selection.end = end;
1779 new_selections.push(selection);
1780 }
1781 self.undo_manager.begin_transaction_with(intent);
1784 self.undo_manager
1785 .record_selections(cursors.clone(), cursors.clone());
1786 self.selections.replace_all(new_selections);
1787 self.undo_manager.set_pending_intent(intent);
1788
1789 let was_silent = self.silent_replace_text;
1790 self.silent_replace_text = silent;
1791 self.replace_text_in_range(None, "", window, cx);
1792 self.silent_replace_text = was_silent;
1793 self.undo_manager
1794 .record_selections(cursors, self.selections.iter().copied().collect());
1795 self.undo_manager.commit_transaction();
1796 self.pause_blink_cursor(cx);
1797 }
1798
1799 fn auto_close_target(&self, range: &Range<usize>, text: &str) -> Option<(usize, SharedString)> {
1802 if self.silent_replace_text
1803 || self.ime_marked_range.is_some()
1804 || !self.selections.is_single()
1805 || !self.active_selection().is_empty()
1806 || !range.is_empty()
1807 || range.start != self.cursor()
1808 || text.chars().count() != 1
1809 || !self.mode.is_auto_close()
1810 {
1811 return None;
1812 }
1813 let rules = self.mode.language_config()?;
1814 if self
1815 .text
1816 .chars_at(range.start)
1817 .next()
1818 .is_some_and(|c| !c.is_whitespace() && !rules.auto_close_before.contains(c))
1819 {
1820 return None;
1821 }
1822 for (open, close, not_in) in rules.closing_pairs() {
1823 let Some(prefix) = open.strip_suffix(text) else {
1824 continue;
1825 };
1826 if !self.text_before_matches(range.start, prefix) {
1827 continue;
1828 }
1829 let start = range.start - prefix.len();
1830 if open == close
1831 && (self.is_escaped_at(start)
1832 || self
1833 .text
1834 .chars_at(start)
1835 .reversed()
1836 .next()
1837 .is_some_and(|c| c.is_alphanumeric() || c == '_'))
1838 {
1839 continue;
1840 }
1841 if not_in.contains(&M::editing_syntax_context(self, start)) {
1842 continue;
1843 }
1844 return Some((open.len(), close.into()));
1845 }
1846 None
1847 }
1848
1849 fn text_before_matches(&self, offset: usize, text: &str) -> bool {
1850 offset >= text.len()
1851 && self.text.is_char_boundary(offset - text.len())
1852 && self.text.slice(offset - text.len()..offset) == text
1853 }
1854
1855 fn text_after_matches(&self, offset: usize, text: &str) -> bool {
1856 offset + text.len() <= self.text.len()
1857 && self.text.is_char_boundary(offset + text.len())
1858 && self.text.slice(offset..offset + text.len()) == text
1859 }
1860
1861 fn is_escaped_at(&self, offset: usize) -> bool {
1862 self.text
1863 .chars_at(offset)
1864 .reversed()
1865 .take_while(|c| *c == '\\')
1866 .count()
1867 % 2
1868 == 1
1869 }
1870
1871 fn skip_over_target(&self, new_text: &str) -> Option<usize> {
1878 if !self.mode.is_auto_close() || new_text.chars().count() != 1 {
1879 return None;
1880 }
1881 let rules = self.mode.language_config()?;
1882 let cursor = self.cursor();
1883 for (open, close, not_in) in rules.closing_pairs() {
1884 for (index, _) in close.char_indices() {
1885 if !close[index..].starts_with(new_text)
1886 || !self.text_before_matches(cursor, &close[..index])
1887 || !self.text_after_matches(cursor, &close[index..])
1888 {
1889 continue;
1890 }
1891 let context = M::editing_syntax_context(self, cursor);
1892 let generated =
1893 self.mode
1894 .auto_closed_pairs()
1895 .contains_closer(cursor, index, close.len());
1896 if !generated
1897 && not_in.contains(&context)
1898 && !(context == crate::input::SyntaxContext::String && open == close)
1899 {
1900 continue;
1901 }
1902 if self.is_escaped_at(cursor - index) {
1903 continue;
1904 }
1905 return Some(cursor + new_text.len());
1906 }
1907 }
1908 None
1909 }
1910
1911 pub(super) fn backspace(&mut self, _: &Backspace, window: &mut Window, cx: &mut Context<Self>) {
1912 if self.selections.is_single() && self.active_selection().is_empty() && self.cursor() == 0 {
1918 cx.propagate();
1919 return;
1920 }
1921
1922 if self.mode.is_auto_close()
1925 && self.selections.is_single()
1926 && self.active_selection().is_empty()
1927 {
1928 let off = self.cursor();
1929 let deletion = self.mode.language_config().and_then(|rules| {
1930 rules.closing_pairs().find_map(|(open, close, not_in)| {
1931 if !self.text_before_matches(off, open) || !self.text_after_matches(off, close)
1932 {
1933 return None;
1934 }
1935 let start = off - open.len();
1936 let generated = self
1937 .mode
1938 .auto_closed_pairs()
1939 .contains(start..off, off..off + close.len());
1940 if self.is_escaped_at(start)
1941 || (!generated && not_in.contains(&M::editing_syntax_context(self, start)))
1942 {
1943 return None;
1944 }
1945 Some(start..off + close.len())
1946 })
1947 });
1948 if let Some(range) = deletion {
1949 let utf16 = self.range_to_utf16(&range);
1950 self.replace_text_in_range_silent(Some(utf16), "", window, cx);
1951 return;
1952 }
1953 }
1954
1955 self.delete_selections(
1956 false,
1957 EditIntent::Backspace,
1958 |s, offset| s.previous_boundary(offset)..offset,
1959 window,
1960 cx,
1961 );
1962 }
1963
1964 pub(super) fn delete(&mut self, _: &Delete, window: &mut Window, cx: &mut Context<Self>) {
1965 self.delete_selections(
1966 false,
1967 EditIntent::DeleteForward,
1968 |s, offset| offset..s.next_boundary(offset),
1969 window,
1970 cx,
1971 );
1972 }
1973
1974 pub(super) fn delete_to_beginning_of_line(
1975 &mut self,
1976 _: &DeleteToBeginningOfLine,
1977 window: &mut Window,
1978 cx: &mut Context<Self>,
1979 ) {
1980 self.delete_selections(
1981 true,
1982 EditIntent::Atomic,
1983 |s, offset| {
1984 let mut start = s.start_of_line_at(offset, s.line_end_affinity_at(offset));
1985 if start == offset {
1986 start = start.saturating_sub(1);
1987 }
1988 start..offset
1989 },
1990 window,
1991 cx,
1992 );
1993 }
1994
1995 pub(super) fn delete_to_end_of_line(
1996 &mut self,
1997 _: &DeleteToEndOfLine,
1998 window: &mut Window,
1999 cx: &mut Context<Self>,
2000 ) {
2001 self.delete_selections(
2002 true,
2003 EditIntent::Atomic,
2004 |s, offset| {
2005 let mut end = s.end_of_line_at(offset, s.line_end_affinity_at(offset));
2006 if end == offset {
2007 end = (end + 1).clamp(0, s.text.len());
2008 }
2009 offset..end
2010 },
2011 window,
2012 cx,
2013 );
2014 }
2015
2016 pub(super) fn delete_previous_word(
2017 &mut self,
2018 _: &DeleteToPreviousWordStart,
2019 window: &mut Window,
2020 cx: &mut Context<Self>,
2021 ) {
2022 self.delete_selections(
2023 true,
2024 EditIntent::Atomic,
2025 |s, offset| s.previous_start_of_word_at(offset)..offset,
2026 window,
2027 cx,
2028 );
2029 }
2030
2031 pub(super) fn delete_next_word(
2032 &mut self,
2033 _: &DeleteToNextWordEnd,
2034 window: &mut Window,
2035 cx: &mut Context<Self>,
2036 ) {
2037 self.delete_selections(
2038 true,
2039 EditIntent::Atomic,
2040 |s, offset| offset..s.next_end_of_word_at(offset),
2041 window,
2042 cx,
2043 );
2044 }
2045
2046 pub(super) fn enter(&mut self, action: &Enter, window: &mut Window, cx: &mut Context<Self>) {
2047 if M::handle_context_menu_action(self, Box::new(action.clone()), window, cx) {
2048 return;
2049 }
2050
2051 if M::has_inline_completion(self) {
2053 M::clear_inline_completion(self, cx);
2054 }
2055
2056 let insert_newline = self.is_multi_line() && (!self.submit_on_enter || action.shift);
2061
2062 if insert_newline {
2063 if !self.selections.is_single() {
2064 self.selections.merge_overlapping();
2066 let selections: Vec<CursorSelection> = self.selections.iter().copied().collect();
2067 let mut edits: Vec<(Range<usize>, String)> = Vec::with_capacity(selections.len());
2068 for sel in &selections {
2069 let indent = if self.is_code_editor() {
2070 self.indent_of_next_line_at(sel.cursor_offset())
2071 } else {
2072 String::new()
2073 };
2074 edits.push((sel.start..sel.end, format!("\n{}", indent)));
2075 }
2076 edits.sort_by_key(|(range, _)| range.start);
2077 self.replace_text_in_ranges(&edits, window, cx);
2078 self.pause_blink_cursor(cx);
2079 } else {
2080 let mut split = false;
2086 if self.is_code_editor() && self.active_selection().is_empty() {
2087 if let Some(rules) = self.mode.language_config() {
2088 if self.mode.is_smart_indent()
2089 && M::editing_syntax_context(self, self.cursor())
2090 == crate::input::SyntaxContext::Code
2091 {
2092 let off = self.cursor();
2093 let is_pair = rules.brackets.iter().any(|p| {
2094 !p.open.is_empty()
2095 && !p.close.is_empty()
2096 && p.open != p.close
2097 && self.text_before_matches(off, p.open.as_ref())
2098 && self.text_after_matches(off, p.close.as_ref())
2099 });
2100 if is_pair {
2101 let line_end_affinity = self.line_end_affinity_at(off);
2103 let line_start = self.start_of_line_at(off, line_end_affinity);
2104 let mut indent = String::new();
2105 for c in self.text.slice(line_start..).chars() {
2106 if !c.is_whitespace() || c == '\n' || c == '\r' {
2107 break;
2108 }
2109 indent.push(c);
2110 }
2111 let mut inner = indent.clone();
2113 if self.mode.is_smart_indent() {
2114 let tab = self.mode.tab_size();
2115 if tab.hard_tabs {
2116 inner.push('\t');
2117 } else {
2118 for _ in 0..tab.tab_size {
2119 inner.push(' ');
2120 }
2121 }
2122 }
2123 let new_line_text = format!("\n{inner}\n{indent}");
2124 self.replace_text_in_range_silent(None, &new_line_text, window, cx);
2125 self.set_cursor_to(off + 1 + inner.len());
2126 self.update_preferred_column();
2127 let cursors = self.selections.iter().copied().collect::<Vec<_>>();
2128 self.undo_manager
2129 .record_selections(cursors.clone(), cursors);
2130 self.pause_blink_cursor(cx);
2131 split = true;
2132 }
2133 }
2134 }
2135 }
2136 if !split {
2137 let indent = if self.is_code_editor() {
2139 self.indent_of_next_line()
2140 } else {
2141 "".to_string()
2142 };
2143
2144 let new_line_text = format!("\n{}", indent);
2146 self.replace_text_in_range_silent(None, &new_line_text, window, cx);
2147 self.pause_blink_cursor(cx);
2148 }
2149 }
2150 } else {
2151 self.undo_manager.break_transaction_coalescing();
2154 cx.propagate();
2155 }
2156
2157 cx.emit(InputEvent::PressEnter {
2158 secondary: action.secondary,
2159 shift: action.shift,
2160 });
2161 }
2162
2163 pub fn clean(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2164 self.replace_text("", window, cx);
2165 self.set_selection(0, 0);
2166 self.scroll_to(0, None, cx);
2167 }
2168
2169 pub(super) fn escape(&mut self, action: &Escape, window: &mut Window, cx: &mut Context<Self>) {
2170 if M::handle_context_menu_action(self, Box::new(action.clone()), window, cx) {
2171 return;
2172 }
2173
2174 M::hide_context_menu(self, cx);
2176
2177 if !self.selections.is_single() {
2179 self.undo_manager.break_transaction_coalescing();
2180 self.selections.remove_all_but_active();
2181 cx.notify();
2182 return;
2183 }
2184
2185 if M::has_inline_completion(self) {
2187 M::clear_inline_completion(self, cx);
2188 return; }
2190 M::clear_inline_completion(self, cx);
2191
2192 if self.touch_selection().is_some() {
2194 self.dismiss_touch_selection(cx);
2195 return;
2196 }
2197
2198 if self.ime_marked_range.is_some() {
2199 self.unmark_text(window, cx);
2200 }
2201
2202 if self.clean_on_escape {
2203 return self.clean(window, cx);
2204 }
2205
2206 cx.propagate();
2207 }
2208
2209 pub(crate) fn handle_right_click_menu(
2211 &mut self,
2212 position: Point<Pixels>,
2213 offset: usize,
2214 window: &mut Window,
2215 cx: &mut Context<Self>,
2216 ) {
2217 if self.disabled {
2218 return;
2219 }
2220 if crate::GlobalState::is_in_deferred_context(cx) {
2221 return;
2222 }
2223
2224 if !self.active_selection().contains(offset) {
2225 self.move_to(offset, None, cx);
2226 }
2227
2228 if self.is_code_editor() {
2229 M::on_hover_definition(self, offset, window, cx);
2230 }
2231
2232 if let Some(handler) = self.context_menu_handler.clone() {
2233 let capabilities = self.context_menu_capabilities();
2234 let input = cx.weak_entity();
2235 window.defer(cx, move |window, cx| {
2238 if input.upgrade().is_none() {
2239 return;
2240 }
2241 let previous_focus = window.focused(cx);
2242 handler(NativeMenu::new(), capabilities, position, window, cx);
2243 input
2246 .update(cx, |this, cx| {
2247 if window.focused(cx) == previous_focus
2248 && !this.focus_handle.is_focused(window)
2249 && this.has_selection_focus(window, cx)
2250 {
2251 return;
2253 }
2254 let menu_focus = window.focused(cx).filter(|focus| {
2255 Some(focus) != previous_focus.as_ref() && *focus != this.focus_handle
2256 });
2257 this.set_selection_focus(menu_focus, cx);
2258 })
2259 .ok();
2260 });
2261 }
2262 }
2263
2264 pub(super) fn add_cursor_above(
2265 &mut self,
2266 _: &AddCursorAbove,
2267 _: &mut Window,
2268 cx: &mut Context<Self>,
2269 ) {
2270 self.add_cursor_vertical(-1, cx);
2271 }
2272
2273 pub(super) fn add_cursor_below(
2274 &mut self,
2275 _: &AddCursorBelow,
2276 _: &mut Window,
2277 cx: &mut Context<Self>,
2278 ) {
2279 self.add_cursor_vertical(1, cx);
2280 }
2281
2282 fn add_cursor_vertical(&mut self, move_lines: isize, cx: &mut Context<Self>) {
2287 if !self.is_multi_line() {
2288 return;
2289 }
2290
2291 self.pause_blink_cursor(cx);
2292 self.undo_manager.break_transaction_coalescing();
2294
2295 let sources: Vec<(usize, Option<(Pixels, usize)>, bool)> = self
2296 .selections
2297 .iter()
2298 .map(|sel| {
2299 (
2300 sel.cursor_offset(),
2301 sel.column_anchor,
2302 self.line_end_affinity_for(sel),
2303 )
2304 })
2305 .collect();
2306 let mut offsets: std::collections::HashSet<usize> =
2307 sources.iter().map(|(offset, _, _)| *offset).collect();
2308
2309 let mut newest: Option<usize> = None;
2310 for (offset, anchor, line_end_affinity) in sources {
2311 let anchor = anchor.or_else(|| self.preferred_column_for(offset));
2312 let (target, _) = self.vertical_target(offset, anchor, line_end_affinity, move_lines);
2313 if target == offset || offsets.contains(&target) {
2314 continue;
2315 }
2316 offsets.insert(target);
2317 let id = self.selections.generate_id();
2318 let mut cursor = CursorSelection::new(id, target, target);
2319 cursor.column_anchor = anchor;
2320 self.selections.add(cursor);
2321 newest = Some(target);
2322 }
2323
2324 if let Some(newest) = newest {
2325 self.scroll_to(newest, None, cx);
2326 }
2327 cx.notify();
2328 }
2329
2330 pub(super) fn add_cursor_at(&mut self, offset: usize, cx: &mut Context<Self>) {
2335 if !self.is_multi_line() {
2336 return;
2337 }
2338
2339 for sel in self.selections.iter() {
2340 if sel.contains(offset) {
2341 return;
2342 }
2343 if sel.is_collapsed() && sel.cursor_offset() == offset {
2344 return;
2345 }
2346 }
2347
2348 self.undo_manager.break_transaction_coalescing();
2349 let id = self.selections.generate_id();
2350 let mut selection = CursorSelection::new(id, offset, offset);
2351 selection.column_anchor = self.preferred_column_for(offset);
2352 self.selections.add(selection);
2353 cx.notify();
2354 }
2355
2356 pub(super) fn build_columnar_selection(
2364 &mut self,
2365 start: ColumnarPoint,
2366 end: ColumnarPoint,
2367 cx: &mut Context<Self>,
2368 ) {
2369 if !self.is_multi_line() {
2370 return;
2371 }
2372
2373 self.undo_manager.break_transaction_coalescing();
2374
2375 let (start_row, start_col) = self.columnar_row_column(start);
2376 let (end_row, end_col) = self.columnar_row_column(end);
2377 let (start_row, end_row) = (start_row.min(end_row), start_row.max(end_row));
2378 let (start_col, end_col) = (start_col.min(end_col), start_col.max(end_col));
2379
2380 let mut new_selections = Vec::with_capacity(end_row - start_row + 1);
2381 for row in start_row..=end_row {
2382 let sel_start = self
2383 .display_map
2384 .display_row_column_to_offset(row, start_col);
2385 let sel_end = self.display_map.display_row_column_to_offset(row, end_col);
2386 let id = self.selections.generate_id();
2387 let sel_start = self.text.clip_offset(sel_start, Bias::Left);
2388 let sel_end = self.text.clip_offset(sel_end, Bias::Left);
2389 new_selections.push(CursorSelection::new(id, sel_start, sel_end));
2390 }
2391
2392 self.selections.replace_all(new_selections);
2393 cx.notify();
2394 }
2395
2396 fn columnar_row_column(&self, point: ColumnarPoint) -> (usize, usize) {
2399 let display_point = self.display_map.offset_to_wrap_display_point(point.offset);
2400 let row = self
2401 .display_map
2402 .wrap_row_to_display_row(display_point.row)
2403 .unwrap_or_else(|| {
2404 self.display_map
2405 .nearest_visible_display_row(display_point.row)
2406 });
2407
2408 (row, display_point.column + point.columns_past_line_end)
2409 }
2410
2411 pub(super) fn on_mouse_down(
2412 &mut self,
2413 event: &MouseDownEvent,
2414 window: &mut Window,
2415 cx: &mut Context<Self>,
2416 ) {
2417 if window.default_prevented() {
2420 return;
2421 }
2422 self.undo_manager.break_transaction_coalescing();
2423 crate::global_state::GlobalState::suppress_text_selection(cx);
2426
2427 M::clear_inline_completion(self, cx);
2429 if event.button == MouseButton::Left
2433 && event.click_count == 1
2434 && self.reopen_edit_menu_at(event.position, cx)
2435 {
2436 return;
2437 }
2438 self.dismiss_touch_selection(cx);
2439
2440 if let Some(ime_marked_range) = &self.ime_marked_range {
2443 if ime_marked_range.len() == 0 {
2444 self.ime_marked_range = None;
2445 }
2446 }
2447
2448 self.selecting = true;
2449 let (offset, line_end_affinity, columns_past_line_end) =
2450 self.resolve_mouse_position(event.position);
2451
2452 if M::on_click(self, event, offset, window, cx) {
2453 return;
2454 }
2455
2456 if event.button == MouseButton::Left && event.click_count >= 3 {
2458 self.select_line(offset, window, cx);
2459 return;
2460 }
2461
2462 if event.button == MouseButton::Left && event.click_count == 2 {
2464 self.select_word(offset, window, cx);
2465 if crate::GlobalState::is_touch_press(cx) {
2468 self.keep_touch_selection(cx);
2469 }
2470 return;
2471 }
2472
2473 if event.button == MouseButton::Right {
2475 if self.enable_context_menu {
2476 if !self.active_selection().contains(offset) {
2477 self.move_to(offset, None, cx);
2478 }
2479 self.pending_context_menu = Some((event.position, offset));
2480 }
2481 return;
2482 }
2483
2484 if self.is_multi_line() && event.button == MouseButton::Left {
2486 if event.modifiers.alt
2487 && (event.modifiers.shift || (cfg!(target_os = "linux") && event.modifiers.control))
2488 {
2489 self.column_select_start = Some(ColumnarPoint::new(offset, columns_past_line_end));
2492 self.selecting = true;
2493 self.move_to_with_affinity(offset, None, line_end_affinity, cx);
2494 return;
2495 } else if event.modifiers.alt {
2496 self.add_cursor_at(offset, cx);
2497 self.column_select_start = Some(ColumnarPoint::new(offset, columns_past_line_end));
2500 return;
2501 }
2502 }
2503
2504 if event.modifiers.shift {
2505 self.select_to_with_affinity(offset, line_end_affinity, cx);
2506 } else {
2507 self.move_to_with_affinity(offset, None, line_end_affinity, cx)
2508 }
2509 }
2510
2511 pub(super) fn on_mouse_up(
2512 &mut self,
2513 event: &MouseUpEvent,
2514 window: &mut Window,
2515 cx: &mut Context<Self>,
2516 ) {
2517 if event.button == MouseButton::Right {
2518 if let Some((position, offset)) = self.pending_context_menu.take() {
2519 self.handle_right_click_menu(position, offset, window, cx);
2520 }
2521 }
2522 if self.active_selection().is_empty() {
2523 self.active_selection_mut().reversed = false;
2524 }
2525 self.selecting = false;
2526 self.selected_word_range = None;
2527 self.column_select_start = None;
2528 self.auto_scroll.stop();
2529 }
2530
2531 pub(super) fn on_mouse_move(
2532 &mut self,
2533 event: &MouseMoveEvent,
2534 window: &mut Window,
2535 cx: &mut Context<Self>,
2536 ) {
2537 let within_bounds = self
2539 .last_bounds
2540 .as_ref()
2541 .map(|bounds| bounds.contains(&event.position))
2542 .unwrap_or(false);
2543
2544 if !within_bounds {
2545 M::clear_hover_state(self, cx);
2547 return;
2548 }
2549
2550 let (offset, _) = self.index_for_mouse_position(event.position);
2552 M::on_mouse_move(self, offset, event, window, cx);
2553
2554 if self.is_code_editor() {
2555 match self
2559 .mode
2560 .diagnostics()
2561 .and_then(|set| set.for_offset(offset))
2562 {
2563 Some(diagnostic) => {
2564 if self.diagnostic_popover.as_deref() != Some(diagnostic) {
2565 self.diagnostic_popover = Some(Rc::new(diagnostic.clone()));
2566 cx.notify();
2567 }
2568 }
2569 None => {
2570 if self.diagnostic_popover.take().is_some() {
2571 cx.notify();
2572 }
2573 }
2574 }
2575 }
2576 }
2577
2578 pub(super) fn on_scroll_wheel(
2579 &mut self,
2580 event: &ScrollWheelEvent,
2581 window: &mut Window,
2582 cx: &mut Context<Self>,
2583 ) {
2584 let line_height = self
2585 .last_layout
2586 .as_ref()
2587 .map(|layout| layout.line_height)
2588 .unwrap_or(window.line_height());
2589 let delta = event.delta.pixel_delta(line_height);
2590
2591 let old_offset = self.scroll_handle.offset();
2592 self.update_scroll_offset(Some(old_offset + delta), cx);
2593
2594 if self.scroll_handle.offset() != old_offset {
2596 cx.stop_propagation();
2597 }
2598
2599 if self.diagnostic_popover.take().is_some() {
2600 cx.notify();
2601 }
2602 self.edit_menu_on_scroll(event.touch_phase, cx);
2605 }
2606
2607 pub(super) fn update_scroll_offset(
2608 &mut self,
2609 offset: Option<Point<Pixels>>,
2610 cx: &mut Context<Self>,
2611 ) {
2612 let mut offset = offset.unwrap_or(self.scroll_handle.offset());
2613 let safe_y_range =
2614 (-self.scroll_size.height + self.input_bounds.size.height).min(px(0.0))..px(0.);
2615
2616 offset.y = if self.is_single_line() {
2617 px(0.)
2618 } else {
2619 offset.y.clamp(safe_y_range.start, safe_y_range.end)
2620 };
2621 offset.x = clamp_horizontal_scroll_offset(
2622 offset.x,
2623 self.scroll_size.width,
2624 self.input_bounds.size.width,
2625 self.text_align,
2626 );
2627 if self.scroll_handle.offset() != offset {
2628 self.scroll_handle.set_offset(offset);
2629 cx.notify();
2630 }
2631 }
2632
2633 pub(crate) fn scroll_to(
2637 &mut self,
2638 offset: usize,
2639 direction: Option<MoveDirection>,
2640 cx: &mut Context<Self>,
2641 ) {
2642 let padding = if direction.is_some() {
2643 ScrollPadding::SurroundingLines
2644 } else {
2645 ScrollPadding::Minimal
2646 };
2647 self.scroll_to_with_padding(offset, direction, padding, cx);
2648 }
2649
2650 pub(crate) fn scroll_to_with_padding(
2653 &mut self,
2654 offset: usize,
2655 direction: Option<MoveDirection>,
2656 padding: ScrollPadding,
2657 cx: &mut Context<Self>,
2658 ) {
2659 let Some(last_layout) = self.last_layout.as_ref() else {
2660 return;
2661 };
2662 let Some(bounds) = self.last_bounds.as_ref() else {
2663 return;
2664 };
2665
2666 let mut scroll_offset = self.scroll_handle.offset();
2667 let was_offset = scroll_offset;
2668 let line_height = last_layout.line_height;
2669
2670 let point = self.text.offset_to_point(offset);
2671
2672 let row = point.row;
2673
2674 let display_pos = self
2676 .display_map
2677 .buffer_pos_to_display_pos(crate::input::BufferPoint::new(row, point.column));
2678 let row_offset_y = line_height * display_pos.row;
2679
2680 let safety_margin = match last_layout.text_align {
2683 TextAlign::Left => RIGHT_MARGIN,
2684 TextAlign::Right => px(0.),
2685 TextAlign::Center => CURSOR_WIDTH,
2686 };
2687 if let Some(vi) = last_layout
2688 .visible_buffer_lines
2689 .iter()
2690 .position(|&line| line == row)
2691 {
2692 let line = &last_layout.lines[vi];
2693 let local_offset = offset.saturating_sub(last_layout.visible_line_byte_offsets[vi]);
2694 if let Some(pos) = line.position_for_index(local_offset, last_layout, false) {
2695 let bounds_width = bounds.size.width - last_layout.line_number_width;
2696 let col_offset_x = pos.x;
2697 if col_offset_x - safety_margin < -scroll_offset.x {
2698 scroll_offset.x = -col_offset_x + safety_margin;
2700 } else if col_offset_x + safety_margin > -scroll_offset.x + bounds_width {
2701 scroll_offset.x = -(col_offset_x - bounds_width + safety_margin);
2702 }
2703 }
2704 }
2705
2706 let edge_height =
2711 if matches!(padding, ScrollPadding::SurroundingLines) && self.is_code_editor() {
2712 super::element::cursor_surrounding_padding(
2713 self.mode.is_auto_grow(),
2714 self.cursor_surrounding_lines,
2715 super::element::viewport_visible_lines(bounds.size.height, line_height),
2716 line_height,
2717 )
2718 } else {
2719 line_height
2720 };
2721 if row_offset_y - edge_height + line_height < -scroll_offset.y {
2722 scroll_offset.y = -row_offset_y + edge_height - line_height;
2724 } else if row_offset_y + edge_height > -scroll_offset.y + bounds.size.height {
2725 scroll_offset.y = -(row_offset_y - bounds.size.height + edge_height);
2727 }
2728
2729 if direction == Some(MoveDirection::Up) {
2731 scroll_offset.y = scroll_offset.y.max(was_offset.y);
2732 } else if direction == Some(MoveDirection::Down) {
2733 scroll_offset.y = scroll_offset.y.min(was_offset.y);
2734 }
2735
2736 let safe_y_min = (-self.scroll_size.height + self.input_bounds.size.height).min(px(0.));
2740 scroll_offset.x = scroll_offset.x.min(px(0.));
2741 scroll_offset.y = scroll_offset.y.clamp(safe_y_min, px(0.));
2742 self.deferred_scroll_offset = Some(scroll_offset);
2743 cx.notify();
2744 }
2745
2746 pub(super) fn show_character_palette(
2747 &mut self,
2748 _: &ShowCharacterPalette,
2749 window: &mut Window,
2750 _: &mut Context<Self>,
2751 ) {
2752 window.show_character_palette();
2753 }
2754
2755 fn selected_texts(&self) -> Vec<String> {
2757 let mut selections: Vec<CursorSelection> = self
2758 .selections
2759 .iter()
2760 .copied()
2761 .filter(|sel| !sel.is_empty())
2762 .collect();
2763 selections.sort_by_key(|sel| sel.start);
2764 selections
2765 .iter()
2766 .map(|sel| self.text.slice(*sel).to_string())
2767 .collect()
2768 }
2769
2770 pub(super) fn copy(&mut self, _: &Copy, _: &mut Window, cx: &mut Context<Self>) {
2771 if !self.is_copyable() {
2772 return;
2773 }
2774
2775 let texts = self.selected_texts();
2776
2777 cx.write_to_clipboard(ClipboardItem::new_string(texts.join("\n")));
2778 }
2779
2780 pub(super) fn cut(&mut self, _: &Cut, window: &mut Window, cx: &mut Context<Self>) {
2781 if !self.is_copyable() {
2782 return;
2783 }
2784
2785 let texts = self.selected_texts();
2786
2787 cx.write_to_clipboard(ClipboardItem::new_string(texts.join("\n")));
2788
2789 self.undo_manager.set_pending_intent(EditIntent::Atomic);
2790 self.replace_text_in_range_silent(None, "", window, cx);
2791 }
2792
2793 pub(super) fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
2794 if !self.is_editable() {
2795 return;
2796 }
2797 if let Some(clipboard) = cx.read_from_clipboard() {
2798 self.insert_clipboard(clipboard, window, cx);
2799 return;
2800 }
2801 let read = cx.read_from_clipboard_async();
2806 let target = self.paste_target();
2807 cx.spawn_in(window, async move |this, cx| match read.await {
2808 Ok(Some(clipboard)) => {
2809 this.update_in(cx, |this, window, cx| {
2810 if this.is_editable()
2817 && this.focus_handle.is_focused(window)
2818 && this.paste_target() == target
2819 {
2820 this.insert_clipboard(clipboard, window, cx);
2821 }
2822 })
2823 .ok();
2824 }
2825 Ok(None) => {}
2826 Err(error) => tracing::warn!("failed to read the clipboard for paste: {error}"),
2827 })
2828 .detach();
2829 }
2830
2831 fn paste_target(&self) -> PasteTarget {
2835 PasteTarget {
2836 document_revision: self.document_revision,
2837 selections: self.selections.iter().copied().collect(),
2838 }
2839 }
2840
2841 fn insert_clipboard(
2847 &mut self,
2848 clipboard: ClipboardItem,
2849 window: &mut Window,
2850 cx: &mut Context<Self>,
2851 ) {
2852 let Some(mut new_text) = clipboard.text().filter(|text| !text.is_empty()) else {
2853 return;
2854 };
2855 self.undo_manager.set_pending_intent(EditIntent::Atomic);
2857
2858 if !self.is_multi_line() {
2859 new_text = new_text.replace('\n', "");
2860 self.replace_text_in_range_silent(None, &new_text, window, cx);
2861 self.scroll_to(self.cursor(), None, cx);
2862 return;
2863 }
2864
2865 if !self.selections.is_single() {
2868 self.selections.merge_overlapping();
2869 }
2870 let lines: Vec<String> = new_text.split('\n').map(|s| s.to_string()).collect();
2871 let count = self.selections.len();
2872 if count > 1 && lines.len() == count {
2873 let mut selections: Vec<CursorSelection> = self.selections.iter().copied().collect();
2874 selections.sort_by_key(|sel| sel.start);
2875 let edits: Vec<(Range<usize>, String)> = selections
2876 .iter()
2877 .zip(lines)
2878 .map(|(sel, line)| (sel.start..sel.end, line))
2879 .collect();
2880 self.replace_text_in_ranges(&edits, window, cx);
2881 } else {
2882 self.replace_text_in_range_silent(None, &new_text, window, cx);
2883 }
2884 self.scroll_to(self.cursor(), None, cx);
2885 }
2886
2887 fn typing_intent(&self, edits: &[(Range<usize>, String)], new_text: &str) -> EditIntent {
2890 if !new_text.is_empty()
2891 && !new_text.contains(['\n', '\r'])
2892 && edits.iter().all(|(range, _)| range.is_empty())
2893 {
2894 EditIntent::Typing
2895 } else {
2896 EditIntent::Atomic
2897 }
2898 }
2899
2900 fn collapse_for_intent(
2906 intent: EditIntent,
2907 mut selection: CursorSelection,
2908 range: &Range<usize>,
2909 ) -> CursorSelection {
2910 match intent {
2911 EditIntent::Backspace => selection.place_at(range.end, None),
2912 EditIntent::DeleteForward => selection.place_at(range.start, None),
2913 EditIntent::Typing | EditIntent::Atomic => {}
2914 }
2915 selection
2916 }
2917
2918 fn rewrites_typed_char(
2921 &self,
2922 selection: CursorSelection,
2923 range: &Range<usize>,
2924 old_text: &str,
2925 new_text: &str,
2926 ) -> bool {
2927 !self.silent_replace_text
2928 && selection.is_collapsed()
2929 && selection.cursor_offset() == range.end
2930 && old_text.chars().count() == 1
2931 && !old_text.contains(['\n', '\r'])
2932 && !new_text.contains(['\n', '\r'])
2933 }
2934
2935 fn push_history(
2936 &mut self,
2937 text: &Rope,
2938 range: &Range<usize>,
2939 new_text: &str,
2940 requested_intent: Option<EditIntent>,
2941 selection_before: CursorSelection,
2942 selection_after: Option<CursorSelection>,
2943 ) -> bool {
2944 self.document_revision = self.document_revision.wrapping_add(1);
2945 let token_delta = self.edit_tokens(range, new_text.len());
2946 if self.undo_manager.is_ignoring() {
2947 return false;
2948 }
2949
2950 let range =
2951 text.clip_offset(range.start, Bias::Left)..text.clip_offset(range.end, Bias::Right);
2952 let old_text = text.slice(range.clone()).to_string();
2953 let new_range = range.start..range.start + new_text.len();
2954
2955 let intent = requested_intent.unwrap_or_else(|| {
2956 if range.is_empty()
2957 && old_text.is_empty()
2958 && !new_text.is_empty()
2959 && !new_text.contains(['\n', '\r'])
2960 || self.rewrites_typed_char(selection_before, &range, &old_text, new_text)
2961 {
2962 EditIntent::Typing
2963 } else {
2964 EditIntent::Atomic
2965 }
2966 });
2967
2968 let selection_before = Self::collapse_for_intent(intent, selection_before, &range);
2969 let selection_after =
2970 selection_after.unwrap_or_else(|| (new_range.end..new_range.end).into());
2971
2972 let open_transaction = self.undo_manager.has_open_transaction();
2973 let mut change = Change::new(range, &old_text, new_range, new_text);
2974 change.token_delta = token_delta;
2975 let recorded = self.undo_manager.record_transaction(change, intent);
2976 if recorded && !open_transaction {
2979 self.undo_manager
2980 .record_selections(vec![selection_before], vec![selection_after]);
2981 }
2982 recorded
2983 }
2984
2985 pub(super) fn undo(&mut self, _: &Undo, window: &mut Window, cx: &mut Context<Self>) {
2986 self.replay_history(true, window, cx);
2987 }
2988
2989 pub(super) fn redo(&mut self, _: &Redo, window: &mut Window, cx: &mut Context<Self>) {
2990 self.replay_history(false, window, cx);
2991 }
2992
2993 fn replay_history(&mut self, undo: bool, window: &mut Window, cx: &mut Context<Self>) {
2994 if !self.is_editable() {
2995 return;
2996 }
2997 let replay = if undo {
2998 self.undo_manager.undo()
2999 } else {
3000 self.undo_manager.redo()
3001 };
3002 let Some(replay) = replay else {
3003 return;
3004 };
3005 let token_aware = self.inline_tokens.is_some()
3006 || replay
3007 .changes
3008 .iter()
3009 .any(|change| change.token_delta.is_some());
3010 let emit_events = self.emit_events;
3011 if token_aware {
3012 self.emit_events = false;
3013 }
3014 self.replaying_history = token_aware;
3015 self.undo_manager.set_ignoring(true);
3016 for change in &replay.changes {
3017 let (range, text) = if undo {
3018 (change.new_range.into(), &change.old_text)
3019 } else {
3020 (change.old_range.into(), &change.new_text)
3021 };
3022 let range_utf16 = self.range_to_utf16(&range);
3023 self.replace_text_in_range_silent(Some(range_utf16), text, window, cx);
3024 self.replay_tokens(&range, text.len(), change.token_delta.as_deref(), undo);
3025 }
3026 self.restore_selections(replay.selections);
3027 self.mode
3028 .restore_auto_closed_pairs(replay.auto_closed_pairs.unwrap_or_default());
3029 if token_aware {
3030 self.ime_marked_range = None;
3033 }
3034 self.undo_manager.set_ignoring(false);
3035 self.replaying_history = false;
3036 self.emit_events = emit_events;
3037 if token_aware && emit_events {
3038 cx.emit(InputEvent::Change);
3039 }
3040 cx.notify();
3041 }
3042
3043 fn restore_selections(&mut self, selections: Option<Vec<CursorSelection>>) {
3047 let Some(selections) = selections else {
3048 return;
3049 };
3050
3051 let len = self.text.len();
3052 let restored: Vec<CursorSelection> = selections
3053 .into_iter()
3054 .map(|mut sel| {
3055 sel.start = sel.start.min(len);
3056 sel.end = sel.end.min(len);
3057 sel
3058 })
3059 .collect();
3060 self.selections.replace_all(restored);
3061 self.selections.merge_overlapping();
3062 }
3063
3064 pub fn cursor(&self) -> usize {
3068 if let Some(ime_marked_range) = &self.ime_marked_range {
3069 return ime_marked_range.end;
3070 }
3071
3072 self.selections.active().cursor_offset()
3073 }
3074
3075 pub(super) fn active_selection(&self) -> &CursorSelection {
3077 self.selections.active()
3078 }
3079
3080 pub(super) fn active_selection_mut(&mut self) -> &mut CursorSelection {
3082 self.selections.active_mut()
3083 }
3084
3085 pub(super) fn set_selection(&mut self, start: usize, end: usize) {
3088 let range = self.normalize_token_range(start..end);
3089 let (start, end) = (range.start, range.end);
3090 let active = self.active_selection_mut();
3091 active.start = start;
3092 active.end = end;
3093 }
3094
3095 pub(super) fn set_cursor_to(&mut self, offset: usize) {
3098 let offset = self.token_boundary(offset, Bias::Right);
3099 let active = self.active_selection_mut();
3100 active.start = offset;
3101 active.end = offset;
3102 active.reversed = false;
3103 }
3104
3105 pub fn visible_row_range(&self) -> Option<std::ops::Range<usize>> {
3107 self.last_layout.as_ref().map(|l| l.visible_range.clone())
3108 }
3109
3110 pub fn scroll_offset(&self) -> gpui::Point<gpui::Pixels> {
3112 self.scroll_handle.offset()
3113 }
3114
3115 pub fn set_scroll_offset(&mut self, offset: gpui::Point<gpui::Pixels>, cx: &mut Context<Self>) {
3119 self.deferred_scroll_offset = Some(offset);
3120 cx.notify();
3121 }
3122
3123 pub fn line_height(&self) -> Option<gpui::Pixels> {
3125 self.last_layout.as_ref().map(|l| l.line_height)
3126 }
3127
3128 pub fn selected_range(&self) -> std::ops::Range<usize> {
3136 (*self.selections.active()).into()
3137 }
3138
3139 pub fn select_all(&mut self, _: &mut Window, cx: &mut Context<Self>) {
3140 self.undo_manager.break_transaction_coalescing();
3141 self.selections.remove_all_but_active();
3142 self.set_selection(0, self.text.len());
3143 cx.notify();
3144 }
3145
3146 pub fn set_selected_range(&mut self, range: Range<usize>, cx: &mut Context<Self>) {
3151 let range = self.normalize_token_range(range);
3152 let end_bias = if range.start == range.end {
3153 Bias::Left
3154 } else {
3155 Bias::Right
3156 };
3157 let start = self.text.clip_offset(range.start, Bias::Left);
3158 let end = self.text.clip_offset(range.end, end_bias);
3159
3160 self.move_to(start, None, cx);
3161 self.active_selection_mut().reversed = false;
3162 self.selected_word_range = None;
3163 self.select_to(end, cx);
3164 }
3165
3166 pub fn add_selection(&mut self, range: Range<usize>, cx: &mut Context<Self>) {
3177 if !self.is_multi_line() {
3178 self.set_selected_range(range, cx);
3179 return;
3180 }
3181
3182 let range = self.normalize_token_range(range);
3183 let end_bias = if range.start == range.end {
3184 Bias::Left
3185 } else {
3186 Bias::Right
3187 };
3188 let start = self.cursor_boundary(range.start, Bias::Left);
3189 let end = self.cursor_boundary(self.text.clip_offset(range.end, end_bias), Bias::Left);
3192
3193 for sel in self.selections.iter() {
3194 if sel.start <= start && end <= sel.end {
3195 return;
3196 }
3197 }
3198
3199 self.undo_manager.break_transaction_coalescing();
3200 self.selected_word_range = None;
3201 self.pause_blink_cursor(cx);
3202 M::hide_context_menu(self, cx);
3203 M::clear_inline_completion(self, cx);
3204 let id = self.selections.generate_id();
3205 let mut selection = CursorSelection::new(id, start, end);
3206 selection.column_anchor = self.preferred_column_for(end);
3207 self.selections.add(selection);
3208 self.selections.merge_overlapping();
3209 self.scroll_to(end, None, cx);
3210 cx.notify();
3211 }
3212
3213 pub(crate) fn index_for_mouse_position(&self, position: Point<Pixels>) -> (usize, bool) {
3221 let (offset, line_end_affinity, _) = self.resolve_mouse_position(position);
3222 (offset, line_end_affinity)
3223 }
3224
3225 pub(super) fn resolve_mouse_position(&self, position: Point<Pixels>) -> (usize, bool, usize) {
3232 if self.text.len() == 0 {
3234 return (0, false, 0);
3235 }
3236
3237 let (Some(bounds), Some(last_layout)) =
3238 (self.last_bounds.as_ref(), self.last_layout.as_ref())
3239 else {
3240 return (0, false, 0);
3241 };
3242
3243 let line_height = last_layout.line_height;
3244 let line_number_width = last_layout.line_number_width;
3245
3246 let inner_position = position - bounds.origin - point(line_number_width, px(0.));
3257
3258 let mut y_offset = last_layout.visible_top;
3259 let mut last_line_pos = None;
3262
3263 for (vi, (line_layout, _buffer_line)) in last_layout
3265 .lines
3266 .iter()
3267 .zip(last_layout.visible_buffer_lines.iter())
3268 .enumerate()
3269 {
3270 let line_start_offset = last_layout.visible_line_byte_offsets[vi];
3271
3272 let line_origin = point(px(0.), y_offset);
3274 let pos = inner_position - line_origin;
3275
3276 if self.is_single_line() {
3278 let local_index = line_layout.closest_index_for_x(pos.x, last_layout);
3279 return (
3281 self.resolve_index(line_start_offset + local_index),
3282 false,
3283 0,
3284 );
3285 }
3286
3287 if let Some((local_index, line_end_affinity)) =
3289 line_layout.closest_index_for_position(pos, last_layout)
3290 {
3291 return (
3292 self.resolve_index(line_start_offset + local_index),
3293 line_end_affinity,
3294 line_layout.columns_past_line_end(pos, last_layout),
3295 );
3296 } else if pos.y < px(0.) {
3297 return (self.resolve_index(line_start_offset), false, 0);
3299 }
3300
3301 y_offset += line_layout.size(line_height).height;
3302 last_line_pos = Some(pos);
3303 }
3304
3305 let last_position = last_layout
3308 .lines
3309 .last()
3310 .zip(last_line_pos)
3311 .zip(last_layout.visible_line_byte_offsets.last())
3312 .map(|((line_layout, pos), line_start)| {
3313 let last_row_top = (line_layout.size(line_height).height - line_height).max(px(0.));
3314 let pos = point(pos.x, last_row_top);
3315 (
3316 self.resolve_index(line_start + line_layout.len()),
3317 false,
3318 line_layout.columns_past_line_end(pos, last_layout),
3319 )
3320 });
3321
3322 last_position.unwrap_or((self.text.len(), false, 0))
3323 }
3324
3325 fn resolve_index(&self, index: usize) -> usize {
3328 if self.masked {
3329 self.text.char_index_to_offset(index / MASK_CHAR.len_utf8())
3330 } else {
3331 index.min(self.text.len())
3332 }
3333 }
3334
3335 fn extend_selection(
3345 sel: &mut CursorSelection,
3346 offset: usize,
3347 word_range: Option<CursorSelection>,
3348 ) {
3349 if sel.reversed {
3350 sel.start = offset;
3351 } else {
3352 sel.end = offset;
3353 }
3354
3355 if sel.end < sel.start {
3356 sel.reversed = !sel.reversed;
3357 std::mem::swap(&mut sel.start, &mut sel.end);
3358 }
3359
3360 if let Some(word_range) = word_range {
3361 if sel.start > word_range.start {
3362 sel.start = word_range.start;
3363 }
3364 if sel.end < word_range.end {
3365 sel.end = word_range.end;
3366 }
3367 }
3368 }
3369
3370 pub(crate) fn select_to(&mut self, offset: usize, cx: &mut Context<Self>) {
3372 self.select_to_with_affinity(offset, false, cx);
3373 }
3374
3375 pub(crate) fn select_to_with_affinity(
3382 &mut self,
3383 offset: usize,
3384 line_end_affinity: bool,
3385 cx: &mut Context<Self>,
3386 ) {
3387 M::clear_inline_completion(self, cx);
3388
3389 self.cursor_line_end_affinity = line_end_affinity;
3390 let offset = self.cursor_boundary(offset, Bias::Left);
3391 let word_range = self.selected_word_range;
3392 Self::extend_selection(self.active_selection_mut(), offset, word_range);
3393 let range = self.normalize_token_range(self.selected_range());
3394 self.set_selection(range.start, range.end);
3395
3396 if self.active_selection().is_empty() {
3397 self.update_preferred_column();
3398 }
3399 cx.notify()
3400 }
3401
3402 fn select_all_cursors_to(
3405 &mut self,
3406 f: impl Fn(&Self, &CursorSelection) -> usize,
3407 cx: &mut Context<Self>,
3408 ) {
3409 self.select_all_cursors_to_with_affinity(|s, sel| (f(s, sel), false), false, cx);
3410 }
3411
3412 fn select_all_cursors_to_with_affinity(
3415 &mut self,
3416 f: impl Fn(&Self, &CursorSelection) -> (usize, bool),
3417 preserve_column: bool,
3418 cx: &mut Context<Self>,
3419 ) {
3420 self.pause_blink_cursor(cx);
3421 self.undo_manager.break_transaction_coalescing();
3422 M::clear_inline_completion(self, cx);
3423
3424 let mut active_affinity = false;
3425 let new_selections: Vec<CursorSelection> = self
3426 .selections
3427 .iter()
3428 .map(|sel| {
3429 let (offset, affinity) = f(self, sel);
3430 if sel.id == self.active_selection().id {
3431 active_affinity = affinity;
3432 }
3433 let offset = self.cursor_boundary(offset, Bias::Left);
3434 let mut new_sel = *sel;
3435 Self::extend_selection(&mut new_sel, offset, None);
3436 let range = self.normalize_token_range(new_sel.start..new_sel.end);
3437 new_sel.start = range.start;
3438 new_sel.end = range.end;
3439 if !preserve_column {
3440 new_sel.column_anchor =
3441 self.preferred_column_for_with_affinity(new_sel.cursor_offset(), affinity);
3442 } else if new_sel.column_anchor.is_none() {
3443 new_sel.column_anchor = self.preferred_column_for_with_affinity(
3444 sel.cursor_offset(),
3445 self.line_end_affinity_for(sel),
3446 );
3447 }
3448 new_sel
3449 })
3450 .collect();
3451 self.cursor_line_end_affinity = active_affinity;
3452 self.selections.replace_all(new_selections);
3453 self.selections.merge_overlapping();
3454
3455 self.scroll_to(self.cursor(), None, cx);
3456 cx.notify()
3457 }
3458
3459 pub fn unselect(&mut self, _: &mut Window, cx: &mut Context<Self>) {
3461 self.undo_manager.break_transaction_coalescing();
3462 let offset = self.cursor();
3463 self.set_cursor_to(offset);
3464 cx.notify()
3465 }
3466
3467 #[inline]
3468 pub(super) fn offset_from_utf16(&self, offset: usize) -> usize {
3469 self.text.offset_utf16_to_offset(offset)
3470 }
3471
3472 #[inline]
3473 pub(super) fn offset_to_utf16(&self, offset: usize) -> usize {
3474 self.text.offset_to_offset_utf16(offset)
3475 }
3476
3477 #[inline]
3478 pub(crate) fn range_to_utf16(&self, range: &Range<usize>) -> Range<usize> {
3479 self.offset_to_utf16(range.start)..self.offset_to_utf16(range.end)
3480 }
3481
3482 #[inline]
3483 pub(super) fn range_from_utf16(&self, range_utf16: &Range<usize>) -> Range<usize> {
3484 self.offset_from_utf16(range_utf16.start)..self.offset_from_utf16(range_utf16.end)
3485 }
3486
3487 fn clamp_offset_to_visible_backward(&self, offset: usize) -> usize {
3490 let line = self.text.offset_to_point(offset).row;
3491 if self.display_map.is_buffer_line_hidden(line) {
3492 for fold in self.display_map.folded_ranges() {
3493 if line > fold.start_line && line <= fold.end_line {
3494 return self.text.line_end_offset(fold.start_line);
3495 }
3496 }
3497 }
3498 offset
3499 }
3500
3501 fn clamp_offset_to_visible_forward(&self, offset: usize) -> usize {
3504 let line = self.text.offset_to_point(offset).row;
3505 if self.display_map.is_buffer_line_hidden(line) {
3506 for fold in self.display_map.folded_ranges() {
3507 if line > fold.start_line && line <= fold.end_line {
3508 return self.text.line_start_offset(fold.end_line);
3509 }
3510 }
3511 }
3512 offset
3513 }
3514
3515 pub(super) fn cursor_boundary(&self, offset: usize, bias: Bias) -> usize {
3518 let offset = self.token_boundary(self.text.clip_offset(offset, bias), bias);
3519 if offset > 0
3520 && self.text.char_at(offset - 1) == Some('\r')
3521 && self.text.char_at(offset) == Some('\n')
3522 {
3523 if bias == Bias::Left {
3524 offset - 1
3525 } else {
3526 offset + 1
3527 }
3528 } else {
3529 offset
3530 }
3531 }
3532
3533 pub(super) fn previous_boundary(&self, offset: usize) -> usize {
3534 let offset = self.cursor_boundary(offset.saturating_sub(1), Bias::Left);
3535 self.clamp_offset_to_visible_backward(offset)
3536 }
3537
3538 pub(super) fn next_boundary(&self, offset: usize) -> usize {
3539 let offset = self.cursor_boundary(offset.saturating_add(1), Bias::Right);
3540 self.clamp_offset_to_visible_forward(offset)
3541 }
3542
3543 pub(crate) fn show_cursor(&self, window: &Window, cx: &App) -> bool {
3545 (self.focus_handle.is_focused(window) || M::is_context_menu_open(self, cx))
3546 && !self.disabled
3547 && self.blink_cursor.read(cx).visible()
3548 && window.is_window_active()
3549 }
3550
3551 fn on_focus(&mut self, _: &mut Window, cx: &mut Context<Self>) {
3552 self.blink_cursor.update(cx, |cursor, cx| {
3553 cursor.start(cx);
3554 });
3555 cx.emit(InputEvent::Focus);
3556 }
3557
3558 fn on_blur(&mut self, window: &mut Window, cx: &mut Context<Self>) {
3559 if M::is_context_menu_open(self, cx) {
3560 return;
3561 }
3562
3563 M::hide_context_menu(self, cx);
3564 self.undo_manager.break_transaction_coalescing();
3565
3566 M::clear_hover_state(self, cx);
3570 self.diagnostic_popover = None;
3571 M::clear_inline_completion(self, cx);
3572 self.dismiss_touch_selection(cx);
3573 self.blink_cursor.update(cx, |cursor, cx| {
3574 cursor.stop(cx);
3575 });
3576 self.clamp_number_value(window, cx);
3577 cx.emit(InputEvent::Blur);
3578 cx.notify();
3579 }
3580
3581 fn clamp_number_value(&mut self, window: &mut Window, cx: &mut Context<Self>) {
3586 if !self.is_single_line() {
3587 return;
3588 }
3589 if !matches!(self.mask_pattern, MaskPattern::Number { .. }) {
3590 return;
3591 }
3592 if self.number_min.is_none() && self.number_max.is_none() {
3593 return;
3594 }
3595
3596 let Ok(value) = self.unmask_value().parse::<f64>() else {
3597 return;
3598 };
3599
3600 let clamped = match (self.number_min, self.number_max) {
3601 (Some(min), _) if value < min => min,
3602 (_, Some(max)) if value > max => max,
3603 _ => return,
3604 };
3605
3606 let new_text = clamped.to_string();
3609 if !self.is_valid_input(&new_text, cx) {
3610 return;
3611 }
3612
3613 let range = self.range_to_utf16(&(0..self.text.len()));
3614 self.replace_text_in_range_silent(Some(range), &new_text, window, cx);
3615 }
3616
3617 pub(super) fn pause_blink_cursor(&mut self, cx: &mut Context<Self>) {
3618 self.blink_cursor.update(cx, |cursor, cx| {
3619 cursor.pause(cx);
3620 });
3621 }
3622
3623 pub(super) fn on_drag_move(
3624 &mut self,
3625 event: &MouseMoveEvent,
3626 window: &mut Window,
3627 cx: &mut Context<Self>,
3628 ) {
3629 if self.text.len() == 0 {
3630 return;
3631 }
3632
3633 if self.last_layout.is_none() {
3634 return;
3635 }
3636
3637 if !self.focus_handle.is_focused(window) {
3638 return;
3639 }
3640
3641 if !self.selecting {
3642 return;
3643 }
3644
3645 self.auto_scroll.last_drag_position = Some(event.position);
3646 let (offset, line_end_affinity, columns_past_line_end) =
3647 self.resolve_mouse_position(event.position);
3648 if let Some(start) = self.column_select_start {
3649 let end = ColumnarPoint::new(offset, columns_past_line_end);
3650 self.build_columnar_selection(start, end, cx);
3651 } else {
3652 self.select_to_with_affinity(offset, line_end_affinity, cx);
3653 }
3654
3655 if !self.is_single_line() {
3656 let delta = AutoScroll::compute_delta(event.position.y, self.input_bounds);
3657 let scroll_delta = delta.map(|d| -d);
3659 self.auto_scroll.set(scroll_delta, cx, |delta, state, cx| {
3660 let current = state.scroll_handle.offset();
3661 state.update_scroll_offset(Some(point(current.x, current.y + delta)), cx);
3662 if let Some(pos) = state.auto_scroll.last_drag_position {
3663 let (offset, line_end_affinity, columns_past_line_end) =
3664 state.resolve_mouse_position(pos);
3665 if let Some(start) = state.column_select_start {
3666 let end = ColumnarPoint::new(offset, columns_past_line_end);
3667 state.build_columnar_selection(start, end, cx);
3668 } else {
3669 state.select_to_with_affinity(offset, line_end_affinity, cx);
3670 }
3671 }
3672 });
3673 }
3674 }
3675
3676 pub(super) fn normalize_input<'a>(&self, new_text: &'a str) -> Cow<'a, str> {
3682 let normalized = if matches!(self.mask_pattern, MaskPattern::Number { .. }) {
3683 normalize_number_input(new_text)
3684 } else {
3685 Cow::Borrowed(new_text)
3686 };
3687
3688 if self.is_single_line() && normalized.contains(['\n', '\r']) {
3689 Cow::Owned(normalized.replace(['\n', '\r'], ""))
3690 } else {
3691 normalized
3692 }
3693 }
3694
3695 pub(crate) fn is_valid_input(&self, new_text: &str, cx: &mut Context<Self>) -> bool {
3696 if new_text.is_empty() {
3697 return true;
3698 }
3699
3700 if let Some(validate) = &self.validate {
3701 if !validate(new_text, cx) {
3702 return false;
3703 }
3704 }
3705
3706 if !self.mask_pattern.is_valid(new_text) {
3707 return false;
3708 }
3709
3710 let Some(pattern) = &self.pattern else {
3711 return true;
3712 };
3713
3714 pattern.is_match(new_text)
3715 }
3716
3717 pub fn mask_pattern(mut self, pattern: impl Into<MaskPattern>) -> Self {
3727 self.mask_pattern = pattern.into();
3728 self.mask_pattern_set = true;
3729 if let Some(placeholder) = self.mask_pattern.placeholder() {
3730 self.placeholder = placeholder.into();
3731 }
3732 self
3733 }
3734
3735 pub fn set_mask_pattern(
3736 &mut self,
3737 pattern: impl Into<MaskPattern>,
3738 _: &mut Window,
3739 cx: &mut Context<Self>,
3740 ) {
3741 self.mask_pattern = pattern.into();
3742 self.mask_pattern_set = true;
3743 if let Some(placeholder) = self.mask_pattern.placeholder() {
3744 self.placeholder = placeholder.into();
3745 }
3746 cx.notify();
3747 }
3748
3749 pub fn ensure_number_mask(&mut self) {
3751 if self.mask_pattern_set {
3752 return;
3753 }
3754 self.mask_pattern = MaskPattern::Number {
3755 separator: None,
3756 fraction: None,
3757 };
3758 }
3759
3760 pub(super) fn set_input_bounds(&mut self, new_bounds: Bounds<Pixels>, cx: &mut Context<Self>) {
3761 let wrap_width_changed = self.input_bounds.size.width != new_bounds.size.width;
3762 self.input_bounds = new_bounds;
3763
3764 if let Some(last_layout) = self.last_layout.as_ref() {
3766 if wrap_width_changed {
3767 let wrap_width = if !self.soft_wrap {
3768 None
3770 } else {
3771 last_layout.wrap_width
3772 };
3773
3774 self.display_map.on_layout_changed(wrap_width, cx);
3775 if self.is_multi_line() {
3776 self.mode.update_auto_grow(&self.display_map);
3777 }
3778 cx.notify();
3779 }
3780 }
3781 }
3782
3783 pub fn selected_text(&self) -> RopeSlice<'_> {
3788 let range_utf16 = self.range_to_utf16(&self.selected_range());
3789 let range = self.range_from_utf16(&range_utf16);
3790 self.text.slice(range)
3791 }
3792
3793 pub fn range_to_bounds(&self, range: &Range<usize>) -> Option<Bounds<Pixels>> {
3797 let Some(last_layout) = self.last_layout.as_ref() else {
3798 return None;
3799 };
3800
3801 let Some(last_bounds) = self.last_bounds else {
3802 return None;
3803 };
3804
3805 let (_, _, start_pos) = self.line_and_position_for_offset(range.start);
3806 let (_, _, end_pos) = self.line_and_position_for_offset(range.end);
3807
3808 let Some(start_pos) = start_pos else {
3809 return None;
3810 };
3811 let Some(end_pos) = end_pos else {
3812 return None;
3813 };
3814
3815 Some(Bounds::from_corners(
3816 last_bounds.origin + start_pos,
3817 last_bounds.origin + end_pos + point(px(0.), last_layout.line_height),
3818 ))
3819 }
3820
3821 pub(crate) fn replace_text_in_range_silent(
3825 &mut self,
3826 range_utf16: Option<Range<usize>>,
3827 new_text: &str,
3828 window: &mut Window,
3829 cx: &mut Context<Self>,
3830 ) {
3831 self.silent_replace_text = true;
3832 self.replace_text_in_range(range_utf16, new_text, window, cx);
3833 self.silent_replace_text = false;
3834 }
3835
3836 pub(crate) fn replace_text_in_ranges(
3841 &mut self,
3842 edits: &[(Range<usize>, String)],
3843 window: &mut Window,
3844 cx: &mut Context<Self>,
3845 ) {
3846 if !self.is_editable() || edits.is_empty() {
3847 return;
3848 }
3849
3850 M::hide_context_menu(self, cx);
3853 M::clear_inline_completion(self, cx);
3854
3855 let mut sorted: Vec<(Range<usize>, &str)> = edits
3858 .iter()
3859 .map(|(range, text)| (self.normalize_token_range(range.clone()), text.as_str()))
3860 .collect();
3861 sorted.sort_by_key(|edit| std::cmp::Reverse(edit.0.start));
3862 if !self.token_spans().is_empty() {
3863 let mut merged: Vec<(Range<usize>, &str)> = Vec::with_capacity(sorted.len());
3864 for (range, text) in sorted {
3865 if let Some(last) = merged
3866 .last_mut()
3867 .filter(|last| range.end > last.0.start || range == last.0)
3868 {
3869 last.0.start = range.start.min(last.0.start);
3870 last.0.end = range.end.max(last.0.end);
3871 } else {
3872 merged.push((range, text));
3873 }
3874 }
3875 sorted = merged;
3876 }
3877
3878 #[cfg(debug_assertions)]
3879 for pair in sorted.windows(2) {
3880 debug_assert!(
3881 pair[1].0.end <= pair[0].0.start,
3882 "replace_text_in_ranges requires disjoint ranges"
3883 );
3884 }
3885
3886 let requested_intent = self.undo_manager.take_pending_intent();
3890 let selection_before = *self.active_selection();
3891 let original_selections: Vec<CursorSelection> = self.selections.iter().copied().collect();
3892 let selections_before: Vec<CursorSelection> = self
3896 .selections
3897 .iter()
3898 .map(|selection| {
3899 Self::collapse_for_intent(
3900 requested_intent.unwrap_or(EditIntent::Atomic),
3901 *selection,
3902 &(selection.start..selection.end),
3903 )
3904 })
3905 .collect();
3906 let group = sorted.len() > 1;
3907 if group {
3908 self.undo_manager.begin_transaction();
3909 }
3910
3911 let auto_closed_pairs_before = self.mode.auto_closed_pairs().clone();
3912 let mut recorded = false;
3913 let mut highlighter_edits = Vec::new();
3916 for (range, new_text) in &sorted {
3917 let old_text = self.text.clone();
3918 self.mode.adjust_auto_closed_pair(range, new_text.len());
3919 self.text.replace(range.clone(), new_text);
3920
3921 M::adjust_annotations(self, range, new_text.len());
3922 recorded |= self.push_history(
3923 &old_text,
3924 range,
3925 new_text,
3926 requested_intent,
3927 selection_before,
3928 None,
3929 );
3930
3931 self.display_map
3933 .adjust_folds_for_edit(&old_text, range, new_text);
3934 self.display_map
3935 .on_text_changed(&self.text, range, &Rope::from(*new_text), cx);
3936
3937 if group {
3938 if self.is_code_editor() {
3939 let edit =
3940 super::mode::replacement_input_edit(&old_text, &self.text, range, new_text);
3941 highlighter_edits.push((edit, self.text.clone()));
3942 }
3943 } else {
3944 self.mode.update_highlighter(
3945 super::mode::HighlighterUpdate {
3946 selected_range: range,
3947 old_text: &old_text,
3948 new_text: &self.text,
3949 change_text: new_text,
3950 force: true,
3951 },
3952 window,
3953 cx,
3954 );
3955
3956 self.update_fold_candidates_incremental(range, new_text);
3957 }
3958 }
3959
3960 if group {
3961 self.mode
3962 .update_highlighter_batch::<M>(&highlighter_edits, window, cx);
3963
3964 let mut shift = 0isize;
3968 for (range, new_text) in sorted.iter().rev() {
3969 let start = (range.start as isize + shift) as usize;
3970 self.update_fold_candidates_incremental(&(start..start), new_text);
3971 shift += new_text.len() as isize - range.len() as isize;
3972 }
3973
3974 self.undo_manager.commit_transaction();
3975 }
3976
3977 if let Some(diagnostics) = self.mode.diagnostics_mut() {
3979 diagnostics.reset(&self.text)
3980 }
3981 M::refresh_language_features(self, window, cx);
3982 self.update_search(cx);
3983
3984 let mut ascending: Vec<(Range<usize>, &str)> = sorted.clone();
3987 ascending.sort_by_key(|edit| edit.0.start);
3988 let text_len = self.text.len();
3989 let mut delta: isize = 0;
3990 let mut edit_results = Vec::with_capacity(ascending.len());
3991 for (range, new_text) in &ascending {
3992 let offset = ((range.start as isize + delta) as usize + new_text.len()).min(text_len);
3993 edit_results.push((range.clone(), offset));
3994 delta += new_text.len() as isize - (range.end as isize - range.start as isize);
3995 }
3996
3997 let mut used = vec![false; edit_results.len()];
3998 let mut new_selections: Vec<CursorSelection> = Vec::with_capacity(ascending.len());
3999 for selection in original_selections {
4000 if let Ok(index) = edit_results
4001 .binary_search_by_key(&(selection.start, selection.end), |(range, _)| {
4002 (range.start, range.end)
4003 })
4004 && !used[index]
4005 {
4006 let offset = edit_results[index].1;
4007 used[index] = true;
4008 let mut selection = selection;
4009 selection.place_at(offset, None);
4010 new_selections.push(selection);
4011 }
4012 }
4013 for (index, (_, offset)) in edit_results.into_iter().enumerate() {
4014 if !used[index] {
4015 new_selections.push(CursorSelection::new(
4016 self.selections.generate_id(),
4017 offset,
4018 offset,
4019 ));
4020 }
4021 }
4022 self.selections.replace_all(new_selections);
4023 self.selections.merge_overlapping();
4024
4025 let selections_after: Vec<CursorSelection> = self.selections.iter().copied().collect();
4027 if recorded {
4028 self.undo_manager.record_auto_closed_pairs(
4029 auto_closed_pairs_before,
4030 self.mode.auto_closed_pairs().clone(),
4031 );
4032 self.undo_manager
4033 .record_selections(selections_before, selections_after);
4034 }
4035
4036 self.ime_marked_range.take();
4037 self.update_preferred_column();
4038 self.dismiss_touch_selection(cx);
4039 if self.is_multi_line() {
4040 self.mode.update_auto_grow(&self.display_map);
4041 }
4042 if self.emit_events {
4043 cx.emit(InputEvent::Change);
4044 }
4045 cx.notify();
4046 }
4047
4048 fn update_fold_candidates(&mut self) {
4051 if !self.mode.is_folding() {
4052 return;
4053 }
4054
4055 let Some(highlighter_rc) = self.mode.highlighter() else {
4056 return;
4057 };
4058
4059 let highlighter = highlighter_rc.borrow();
4060 let Some(highlighter) = highlighter.as_ref() else {
4061 return;
4062 };
4063
4064 let fold_ranges = highlighter.fold_ranges(&self.text);
4065 self.display_map.set_fold_candidates(fold_ranges);
4066 }
4067
4068 fn update_fold_candidates_incremental(&mut self, edit_range: &Range<usize>, new_text: &str) {
4071 if !self.mode.is_folding() {
4072 return;
4073 }
4074
4075 let Some(highlighter_rc) = self.mode.highlighter() else {
4076 return;
4077 };
4078
4079 let highlighter = highlighter_rc.borrow();
4080 let Some(highlighter) = highlighter.as_ref() else {
4081 return;
4082 };
4083
4084 let new_end = edit_range.start + new_text.len();
4086 self.display_map.update_fold_candidates_for_edit(
4087 |range, text| highlighter.fold_ranges_for_edit(range, text),
4088 edit_range.start..new_end,
4089 &self.text,
4090 );
4091 }
4092}
4093
4094impl<M: InputModeKind> EntityInputHandler for InputBaseState<M> {
4095 fn text_for_range(
4096 &mut self,
4097 range_utf16: Range<usize>,
4098 adjusted_range: &mut Option<Range<usize>>,
4099 _window: &mut Window,
4100 _cx: &mut Context<Self>,
4101 ) -> Option<String> {
4102 let range = self.range_from_utf16(&range_utf16);
4103 adjusted_range.replace(self.range_to_utf16(&range));
4104 Some(self.text.slice(range).to_string())
4105 }
4106
4107 fn selected_text_range(
4108 &mut self,
4109 _ignore_disabled_input: bool,
4110 _window: &mut Window,
4111 _cx: &mut Context<Self>,
4112 ) -> Option<UTF16Selection> {
4113 Some(UTF16Selection {
4114 range: self.range_to_utf16(&self.selected_range()),
4115 reversed: self.active_selection().reversed,
4116 })
4117 }
4118
4119 fn marked_text_range(
4120 &self,
4121 _window: &mut Window,
4122 _cx: &mut Context<Self>,
4123 ) -> Option<Range<usize>> {
4124 self.ime_marked_range
4125 .map(|range| self.range_to_utf16(&range.into()))
4126 }
4127
4128 fn unmark_text(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {
4129 self.ime_marked_range = None;
4130 self.undo_manager.commit_transaction();
4131 }
4132
4133 fn replace_text_in_range(
4138 &mut self,
4139 range_utf16: Option<Range<usize>>,
4140 new_text: &str,
4141 window: &mut Window,
4142 cx: &mut Context<Self>,
4143 ) {
4144 let requested_intent = self.undo_manager.take_pending_intent();
4145 if !self.is_editable() {
4146 return;
4147 }
4148 let selection_before = *self.active_selection();
4149 let ends_composition = self.ime_marked_range.is_some();
4152
4153 self.pause_blink_cursor(cx);
4154
4155 let new_text = if self.replaying_history {
4159 Cow::Borrowed(new_text)
4160 } else {
4161 self.normalize_input(new_text)
4162 };
4163 let new_text: &str = &new_text;
4164
4165 let range = range_utf16
4166 .as_ref()
4167 .map(|range_utf16| self.range_from_utf16(range_utf16))
4168 .or(self.ime_marked_range.map(|range| {
4169 let range = self.range_to_utf16(&(range.start..range.end));
4170 self.range_from_utf16(&range)
4171 }))
4172 .unwrap_or(self.selected_range());
4173 let range = self.normalize_token_range(range);
4174
4175 if range.is_empty()
4181 && range.start == self.cursor()
4182 && self.ime_marked_range.is_none()
4183 && !self.silent_replace_text
4184 && self.is_code_editor()
4185 && self.selections.is_single()
4186 && self.active_selection().is_empty()
4187 {
4188 if let Some(target) = self.skip_over_target(new_text) {
4189 self.set_cursor_to(target);
4190 self.update_preferred_column();
4191 cx.notify();
4192 return;
4193 }
4194 }
4195
4196 if self.is_multi_line() {
4197 let multi_cursor = range_utf16.is_none()
4198 && self.ime_marked_range.is_none()
4199 && !self.selections.is_single();
4200 if multi_cursor {
4201 self.selections.merge_overlapping();
4202 let mut edits: Vec<(Range<usize>, String)> = self
4203 .selections
4204 .iter()
4205 .map(|sel| (sel.start..sel.end, new_text.to_string()))
4206 .collect();
4207 edits.sort_by_key(|(range, _)| range.start);
4208 let intent =
4212 requested_intent.unwrap_or_else(|| self.typing_intent(&edits, new_text));
4213 self.undo_manager.begin_transaction_with(intent);
4214 self.undo_manager.set_pending_intent(intent);
4215 self.replace_text_in_ranges(&edits, window, cx);
4216 self.undo_manager.commit_transaction();
4217 } else {
4218 if range_utf16.is_some() {
4219 self.selections.remove_all_but_active();
4220 }
4221 if let Some(intent) = requested_intent {
4222 self.undo_manager.set_pending_intent(intent);
4223 }
4224 if let Some((open_len, closer)) = self.auto_close_target(&range, new_text) {
4225 let replacement = format!("{new_text}{closer}");
4227 self.replace_text_in_ranges(&[(range.clone(), replacement)], window, cx);
4228 let cursor = range.start + new_text.len();
4229 self.mode.track_auto_closed_pair(
4230 cursor - open_len..cursor,
4231 cursor..cursor + closer.len(),
4232 );
4233 self.undo_manager
4234 .record_auto_closed_pairs_after(self.mode.auto_closed_pairs().clone());
4235 self.set_cursor_to(cursor);
4236 self.update_preferred_column();
4237 self.undo_manager.record_selections(
4238 vec![selection_before],
4239 self.selections.iter().copied().collect(),
4240 );
4241 } else {
4242 self.replace_text_in_ranges(
4243 &[(range.clone(), new_text.to_string())],
4244 window,
4245 cx,
4246 );
4247 }
4248 }
4249 if ends_composition {
4250 self.undo_manager.commit_transaction();
4251 }
4252
4253 if !self.silent_replace_text {
4254 M::on_text_typed(self, &range, new_text, window, cx);
4255 }
4256 return;
4257 }
4258
4259 let old_text = self.text.clone();
4261 self.mode.adjust_auto_closed_pair(&range, new_text.len());
4262 self.text.replace(range.clone(), new_text);
4263
4264 let mut new_offset = (range.start + new_text.len()).min(self.text.len());
4265
4266 let mut mask_changed = false;
4269
4270 if self.is_single_line() {
4271 let pending_text = self.text.to_string();
4272 if !self.replaying_history
4278 && !self.validated_token_edit
4279 && !self.is_valid_input(&pending_text, cx)
4280 && self.is_valid_input(&old_text.to_string(), cx)
4281 {
4282 self.text = old_text;
4283 return;
4284 }
4285
4286 if !self.replaying_history && !self.mask_pattern.is_none() {
4287 let mask_text = self.mask_pattern.mask(&pending_text);
4288 mask_changed = mask_text.as_str() != pending_text;
4289 self.text = Rope::from(mask_text.as_str());
4290 let new_text_len =
4291 (new_text.len() + mask_text.len()).saturating_sub(pending_text.len());
4292 new_offset = (range.start + new_text_len).min(mask_text.len());
4293 }
4294 }
4295
4296 if mask_changed {
4297 M::reset_annotations(self);
4300 } else {
4301 M::adjust_annotations(self, &range, new_text.len());
4302 }
4303 if mask_changed {
4304 self.push_history(
4308 &old_text,
4309 &(0..old_text.len()),
4310 &self.text.to_string(),
4311 Some(EditIntent::Atomic),
4312 selection_before,
4313 Some((new_offset..new_offset).into()),
4314 );
4315 } else {
4316 self.push_history(
4317 &old_text,
4318 &range,
4319 &new_text,
4320 requested_intent,
4321 selection_before,
4322 None,
4323 );
4324 }
4325 if let Some(diagnostics) = self.mode.diagnostics_mut() {
4326 diagnostics.reset(&self.text)
4327 }
4328 self.display_map
4330 .adjust_folds_for_edit(&old_text, &range, new_text);
4331 self.display_map
4332 .on_text_changed(&self.text, &range, &Rope::from(new_text), cx);
4333
4334 self.mode.update_highlighter::<M>(
4335 super::mode::HighlighterUpdate {
4336 selected_range: &range,
4337 old_text: &old_text,
4338 new_text: &self.text,
4339 change_text: &new_text,
4340 force: true,
4341 },
4342 window,
4343 cx,
4344 );
4345
4346 self.update_fold_candidates_incremental(&range, new_text);
4347 M::refresh_language_features(self, window, cx);
4348 self.set_cursor_to(new_offset);
4349 self.ime_marked_range.take();
4350 if ends_composition {
4356 self.undo_manager
4357 .record_selections(vec![selection_before], vec![*self.active_selection()]);
4358 self.undo_manager.commit_transaction();
4359 }
4360 self.update_preferred_column();
4361 self.update_search(cx);
4362 self.dismiss_touch_selection(cx);
4363 if self.is_multi_line() {
4364 self.mode.update_auto_grow(&self.display_map);
4365 }
4366 if !self.silent_replace_text {
4367 M::on_text_typed(self, &range, &new_text, window, cx);
4368 }
4369 if self.emit_events {
4370 cx.emit(InputEvent::Change);
4371 }
4372 cx.notify();
4373 }
4374
4375 fn replace_and_mark_text_in_range(
4377 &mut self,
4378 range_utf16: Option<Range<usize>>,
4379 new_text: &str,
4380 new_selected_range_utf16: Option<Range<usize>>,
4381 window: &mut Window,
4382 cx: &mut Context<Self>,
4383 ) {
4384 let requested_intent = self.undo_manager.take_pending_intent();
4385 if !self.is_editable() {
4386 return;
4387 }
4388 let selection_before = *self.active_selection();
4389
4390 let starts_composition = self.ime_marked_range.is_none();
4391 if starts_composition {
4392 self.undo_manager.break_transaction_coalescing();
4395 self.undo_manager.begin_transaction();
4396 }
4397
4398 self.selections.remove_all_but_active();
4400
4401 M::reset_language_features(self);
4402
4403 let new_text = if self.replaying_history {
4405 Cow::Borrowed(new_text)
4406 } else {
4407 self.normalize_input(new_text)
4408 };
4409 let new_text: &str = &new_text;
4410
4411 let range = range_utf16
4412 .as_ref()
4413 .map(|range_utf16| self.range_from_utf16(range_utf16))
4414 .or(self.ime_marked_range.map(|range| {
4415 let range = self.range_to_utf16(&(range.start..range.end));
4416 self.range_from_utf16(&range)
4417 }))
4418 .unwrap_or(self.selected_range());
4419 let range = self.normalize_token_range(range);
4420
4421 let auto_closed_pairs_before = self.mode.auto_closed_pairs().clone();
4422 let old_text = self.text.clone();
4423 self.mode.adjust_auto_closed_pair(&range, new_text.len());
4424 self.text.replace(range.clone(), new_text);
4425
4426 if self.is_single_line() {
4427 let pending_text = self.text.to_string();
4428 if !self.replaying_history
4430 && !self.validated_token_edit
4431 && !self.is_valid_input(&pending_text, cx)
4432 && self.is_valid_input(&old_text.to_string(), cx)
4433 {
4434 self.text = old_text;
4435 if starts_composition {
4436 self.undo_manager.commit_transaction();
4437 }
4438 return;
4439 }
4440 }
4441
4442 M::adjust_annotations(self, &range, new_text.len());
4443 if let Some(diagnostics) = self.mode.diagnostics_mut() {
4444 diagnostics.reset(&self.text)
4445 }
4446 self.display_map
4448 .adjust_folds_for_edit(&old_text, &range, new_text);
4449 self.display_map
4450 .on_text_changed(&self.text, &range, &Rope::from(new_text), cx);
4451
4452 self.mode.update_highlighter::<M>(
4453 super::mode::HighlighterUpdate {
4454 selected_range: &range,
4455 old_text: &old_text,
4456 new_text: &self.text,
4457 change_text: &new_text,
4458 force: true,
4459 },
4460 window,
4461 cx,
4462 );
4463
4464 self.update_fold_candidates_incremental(&range, new_text);
4465 M::refresh_language_features(self, window, cx);
4466 if new_text.is_empty() {
4467 self.set_cursor_to(range.start);
4469 self.ime_marked_range = None;
4470 } else {
4471 self.ime_marked_range = Some((range.start..range.start + new_text.len()).into());
4472 let new_range = new_selected_range_utf16
4473 .as_ref()
4474 .map(|range_utf16| {
4475 let new_text = Rope::from(new_text);
4476 range.start + new_text.offset_utf16_to_offset(range_utf16.start)
4477 ..range.start + new_text.offset_utf16_to_offset(range_utf16.end)
4478 })
4479 .unwrap_or_else(|| range.start + new_text.len()..range.start + new_text.len());
4480 self.set_selection(new_range.start, new_range.end);
4481 }
4482 if self.is_multi_line() {
4483 self.mode.update_auto_grow(&self.display_map);
4484 }
4485 if self.push_history(
4486 &old_text,
4487 &range,
4488 new_text,
4489 requested_intent,
4490 selection_before,
4491 Some(*self.active_selection()),
4492 ) {
4493 self.undo_manager
4494 .record_selections(vec![selection_before], vec![*self.active_selection()]);
4495 self.undo_manager.record_auto_closed_pairs(
4496 auto_closed_pairs_before,
4497 self.mode.auto_closed_pairs().clone(),
4498 );
4499 }
4500 if new_text.is_empty() {
4501 self.undo_manager.commit_transaction();
4502 }
4503 cx.notify();
4504 }
4505
4506 fn bounds_for_range(
4508 &mut self,
4509 range_utf16: Range<usize>,
4510 bounds: Bounds<Pixels>,
4511 _window: &mut Window,
4512 _cx: &mut Context<Self>,
4513 ) -> Option<Bounds<Pixels>> {
4514 let last_layout = self.last_layout.as_ref()?;
4515 let line_height = last_layout.line_height;
4516 let line_number_width = last_layout.line_number_width;
4517 let range = self.range_from_utf16(&range_utf16);
4518
4519 let mut start_origin = None;
4520 let mut end_origin = None;
4521 let line_number_origin = point(line_number_width, px(0.));
4522 let mut y_offset = last_layout.visible_top;
4523
4524 for (vi, line) in last_layout.lines.iter().enumerate() {
4525 if start_origin.is_some() && end_origin.is_some() {
4526 break;
4527 }
4528
4529 let index_offset = last_layout.visible_line_byte_offsets[vi];
4530
4531 if start_origin.is_none() {
4532 if let Some(p) = line.position_for_index(
4533 range.start.saturating_sub(index_offset),
4534 last_layout,
4535 false,
4536 ) {
4537 start_origin = Some(p + point(px(0.), y_offset));
4538 }
4539 }
4540
4541 if end_origin.is_none() {
4542 if let Some(p) = line.position_for_index(
4543 range.end.saturating_sub(index_offset),
4544 last_layout,
4545 false,
4546 ) {
4547 end_origin = Some(p + point(px(0.), y_offset));
4548 }
4549 }
4550
4551 y_offset += line.size(line_height).height;
4552 }
4553
4554 let start_origin = start_origin.or_else(|| {
4555 let offset = self.last_cursor.or(Some(self.cursor()))?;
4556 let (_, _, origin) = self.line_and_position_for_offset(offset);
4557 origin.map(|origin| origin - line_number_origin)
4558 })?;
4559 let mut end_origin = end_origin.unwrap_or(start_origin);
4560 end_origin.y = start_origin.y;
4562
4563 Some(Bounds::from_corners(
4564 bounds.origin + line_number_origin + start_origin,
4565 bounds.origin + line_number_origin + point(end_origin.x, end_origin.y + line_height),
4567 ))
4568 }
4569
4570 fn character_index_for_point(
4571 &mut self,
4572 point: gpui::Point<Pixels>,
4573 _window: &mut Window,
4574 _cx: &mut Context<Self>,
4575 ) -> Option<usize> {
4576 let last_layout = self.last_layout.as_ref()?;
4577 let line_point = self.last_bounds?.localize(&point)?;
4578
4579 for (vi, line) in last_layout.lines.iter().enumerate() {
4580 let offset = last_layout.visible_line_byte_offsets[vi];
4581 if let Some(utf8_index) = line.index_for_position(line_point, last_layout) {
4582 return Some(self.offset_to_utf16(offset + utf8_index));
4583 }
4584 }
4585
4586 None
4587 }
4588}
4589
4590impl<M: InputModeKind> Focusable for InputBaseState<M> {
4591 fn focus_handle(&self, _cx: &App) -> FocusHandle {
4592 self.focus_handle.clone()
4593 }
4594}
4595
4596impl<M: InputModeKind> Render for InputBaseState<M> {
4597 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
4598 self.editor_style = self
4601 .projected_editor_style
4602 .resolved(&crate::Theme::global(cx).tokens);
4603 let entity = cx.entity();
4604 if self._pending_update {
4605 self.mode.update_highlighter::<M>(
4606 super::mode::HighlighterUpdate {
4607 selected_range: &(0..0),
4608 old_text: &self.text,
4609 new_text: &self.text,
4610 change_text: "",
4611 force: false,
4612 },
4613 window,
4614 cx,
4615 );
4616
4617 self.update_fold_candidates();
4618 M::refresh_language_features(self, window, cx);
4619 self._pending_update = false;
4620 }
4621
4622 let element = div()
4623 .id("input-state")
4624 .key_context(CONTEXT)
4625 .track_focus(&self.focus_handle)
4626 .when(self.is_editable(), |this| {
4627 this.on_action(window.listener_for(&entity, InputBaseState::backspace))
4628 .on_action(window.listener_for(&entity, InputBaseState::delete))
4629 .on_action(
4630 window.listener_for(&entity, InputBaseState::delete_to_beginning_of_line),
4631 )
4632 .on_action(window.listener_for(&entity, InputBaseState::delete_to_end_of_line))
4633 .on_action(window.listener_for(&entity, InputBaseState::delete_previous_word))
4634 .on_action(window.listener_for(&entity, InputBaseState::delete_next_word))
4635 .on_action(window.listener_for(&entity, InputBaseState::enter))
4636 .on_action(window.listener_for(&entity, InputBaseState::escape))
4637 .on_action(window.listener_for(&entity, InputBaseState::paste))
4638 .on_action(window.listener_for(&entity, InputBaseState::cut))
4639 .on_action(window.listener_for(&entity, InputBaseState::undo))
4640 .on_action(window.listener_for(&entity, InputBaseState::redo))
4641 .when(self.is_multi_line(), |this| {
4642 this.on_action(window.listener_for(&entity, InputBaseState::indent_inline))
4643 .on_action(window.listener_for(&entity, InputBaseState::outdent_inline))
4644 .on_action(window.listener_for(&entity, InputBaseState::indent_block))
4645 .on_action(window.listener_for(&entity, InputBaseState::outdent_block))
4646 })
4647 })
4648 .on_action(window.listener_for(&entity, InputBaseState::left))
4649 .on_action(window.listener_for(&entity, InputBaseState::right))
4650 .on_action(window.listener_for(&entity, InputBaseState::select_left))
4651 .on_action(window.listener_for(&entity, InputBaseState::select_right))
4652 .when(self.is_multi_line(), |this| {
4653 this.on_action(window.listener_for(&entity, InputBaseState::up))
4654 .on_action(window.listener_for(&entity, InputBaseState::down))
4655 .on_action(window.listener_for(&entity, InputBaseState::select_up))
4656 .on_action(window.listener_for(&entity, InputBaseState::select_down))
4657 .on_action(window.listener_for(&entity, InputBaseState::page_up))
4658 .on_action(window.listener_for(&entity, InputBaseState::page_down))
4659 .on_action(window.listener_for(&entity, InputBaseState::add_cursor_above))
4660 .on_action(window.listener_for(&entity, InputBaseState::add_cursor_below))
4661 })
4662 .on_action(window.listener_for(&entity, InputBaseState::on_action_select_all))
4663 .on_action(window.listener_for(&entity, InputBaseState::select_to_start_of_line))
4664 .on_action(window.listener_for(&entity, InputBaseState::select_to_end_of_line))
4665 .on_action(window.listener_for(&entity, InputBaseState::select_to_previous_word))
4666 .on_action(window.listener_for(&entity, InputBaseState::select_to_next_word))
4667 .on_action(window.listener_for(&entity, InputBaseState::home))
4668 .on_action(window.listener_for(&entity, InputBaseState::end))
4669 .on_action(window.listener_for(&entity, InputBaseState::move_to_start))
4670 .on_action(window.listener_for(&entity, InputBaseState::move_to_end))
4671 .on_action(window.listener_for(&entity, InputBaseState::move_to_previous_word))
4672 .on_action(window.listener_for(&entity, InputBaseState::move_to_next_word))
4673 .on_action(window.listener_for(&entity, InputBaseState::select_to_start))
4674 .on_action(window.listener_for(&entity, InputBaseState::select_to_end))
4675 .on_action(window.listener_for(&entity, InputBaseState::show_character_palette))
4676 .on_action({
4677 let entity = entity.clone();
4678 move |_: &ActivateToken, window, cx| {
4679 let state = entity.read(cx);
4680 let activation = state
4681 .token_spans()
4682 .iter()
4683 .find(|span| span.range() == state.selected_range())
4684 .and_then(|span| {
4685 state.range_to_bounds(&span.range()).and_then(|bounds| {
4686 state.token_activation(
4687 span.range().start,
4688 bounds,
4689 gpui::ClickEvent::Keyboard(gpui::KeyboardClickEvent {
4690 bounds,
4691 ..Default::default()
4692 }),
4693 )
4694 })
4695 });
4696 if let Some((listener, event)) = activation {
4697 listener(&event, window, cx);
4698 }
4699 }
4700 })
4701 .on_action(window.listener_for(&entity, InputBaseState::copy))
4702 .on_action(window.listener_for(&entity, InputBaseState::on_action_search))
4703 .on_action(window.listener_for(&entity, InputBaseState::on_action_replace))
4704 .on_mouse_down(
4705 MouseButton::Left,
4706 window.listener_for(&entity, InputBaseState::on_mouse_down),
4707 )
4708 .on_mouse_down(
4709 MouseButton::Right,
4710 window.listener_for(&entity, InputBaseState::on_mouse_down),
4711 )
4712 .on_mouse_up(
4713 MouseButton::Left,
4714 window.listener_for(&entity, InputBaseState::on_mouse_up),
4715 )
4716 .on_mouse_up(
4717 MouseButton::Right,
4718 window.listener_for(&entity, InputBaseState::on_mouse_up),
4719 )
4720 .on_mouse_move(window.listener_for(&entity, InputBaseState::on_mouse_move))
4721 .on_scroll_wheel(window.listener_for(&entity, InputBaseState::on_scroll_wheel))
4722 .when(self.is_multi_line() && !self.disabled, |this| {
4723 this.on_modifiers_changed(cx.listener(|_, _, _, cx| cx.notify()))
4724 })
4725 .when(!self.disabled, |this| {
4726 if self.is_multi_line() && window.modifiers().alt {
4727 this.cursor_crosshair()
4728 } else {
4729 this.cursor_text()
4730 }
4731 })
4732 .flex_1()
4733 .map(|this| {
4739 if self.is_multi_line() {
4740 this.h_full()
4741 } else {
4742 this.min_h_full().flex().items_center()
4743 }
4744 })
4745 .flex_grow_1()
4746 .overflow_x_hidden()
4747 .when(self.is_multi_line(), |this| {
4748 this.pt(self.editor_paddings.top)
4749 .pr(self.editor_paddings.right)
4750 .pb(self.editor_paddings.bottom)
4751 .pl(self.editor_paddings.left)
4752 })
4753 .child(TextElement::new(entity.clone()).placeholder(self.placeholder.clone()))
4754 .when(self.shows_scrollbar(), |this| {
4755 this.child(EditorScrollbar::new(entity.clone()))
4756 });
4757
4758 M::register_actions(element, &entity, window)
4761 }
4762}
4763
4764#[cfg(test)]
4765mod tests {
4766 use super::*;
4767
4768 #[gpui::test]
4769 fn test_multicursor_unicode_completion_uses_post_edit_start(cx: &mut TestAppContext) {
4770 use crate::input::{CompletionProvider, EditorState};
4771 use gpui::Task;
4772 use lsp_types::{CompletionContext, CompletionResponse};
4773 use std::cell::RefCell;
4774
4775 #[derive(Default)]
4776 struct Provider {
4777 starts: RefCell<Vec<usize>>,
4778 queries: RefCell<Vec<(usize, String)>>,
4779 }
4780
4781 impl CompletionProvider for Provider {
4782 fn is_completion_trigger(&self, offset: usize, _: &str, _: &mut App) -> bool {
4783 self.starts.borrow_mut().push(offset);
4784 true
4785 }
4786
4787 fn completions(
4788 &self,
4789 _: &Rope,
4790 offset: usize,
4791 trigger: CompletionContext,
4792 _: &mut Window,
4793 _: &mut App,
4794 ) -> Task<anyhow::Result<CompletionResponse>> {
4795 self.queries
4796 .borrow_mut()
4797 .push((offset, trigger.trigger_character.unwrap()));
4798 Task::ready(Ok(CompletionResponse::Array(vec![])))
4799 }
4800 }
4801
4802 cx.update(crate::init);
4803 let mut editor = None;
4804 let window = cx.open_window(size(px(400.), px(100.)), |window, cx| {
4805 editor = Some(cx.new(|cx| EditorState::new(window, cx).default_value("aa")));
4806 gpui::EmptyView
4807 });
4808 let editor = editor.unwrap();
4809 let provider = Rc::new(Provider::default());
4810 window
4811 .update(cx, |_, window, cx| {
4812 editor.update(cx, |state, cx| {
4813 state.extras.lsp.completion_provider = Some(provider.clone());
4814 state
4815 .extras
4816 .context_menu_content
4817 .completion
4818 .trigger_start_offset = Some(0);
4819 state.extras.context_menu_content.completion.query = "aa".into();
4820 state.set_cursor_to(1);
4821 state.add_cursor_at(0, cx);
4822
4823 state.replace_text_in_range(None, "中", window, cx);
4824
4825 assert_eq!(state.value(), "中a中a");
4826 assert_eq!(provider.starts.borrow().as_slice(), &[4]);
4827 assert_eq!(provider.queries.borrow().as_slice(), &[(7, "中".into())]);
4828 });
4829 })
4830 .unwrap();
4831 }
4832
4833 #[gpui::test]
4834 fn test_inline_token_wrap_and_size_refresh(cx: &mut TestAppContext) {
4835 use crate::input::{InlineToken, InlineTokenPresentation};
4836 let width = Rc::new(Cell::new(4000.));
4837 let render_width = width.clone();
4838 let view = InputView::build_textarea(cx, |state| state.rows(4).default_value("@a@b"));
4839 view.window_handle
4840 .update(cx, |_, window, cx| {
4841 view.input.update(cx, |state, cx| {
4842 state
4843 .replace_range_with_token(0..2, InlineToken::new("a", "@a"), window, cx)
4844 .unwrap();
4845 state
4846 .replace_range_with_token(2..4, InlineToken::new("b", "@b"), window, cx)
4847 .unwrap();
4848 state.set_token_presentation(
4849 InlineTokenPresentation::default()
4850 .token(move |_, _, _| div().w(px(render_width.get()))),
4851 );
4852 });
4853 })
4854 .unwrap();
4855 let mut visual = VisualTestContext::from_window(view.window_handle.into(), cx);
4856 visual.update(|window, cx| window.draw(cx).clear(cx));
4857 view.input.read_with(&visual, |state, _| {
4858 assert_eq!(state.display_map.wrap_row_count(), 2);
4859 let first = state.range_to_bounds(&(0..2)).unwrap();
4860 let second = state.range_to_bounds(&(2..4)).unwrap();
4861 assert!(second.origin.y > first.origin.y);
4862 assert_eq!(
4863 state.last_layout.as_ref().unwrap().lines[0].wrapped_lines[0].len,
4864 2
4865 );
4866 });
4867 width.set(100.);
4868 view.input.update(&mut visual, |_, cx| cx.notify());
4869 visual.update(|window, cx| window.draw(cx).clear(cx));
4870 view.input.read_with(&visual, |state, _| {
4871 assert_eq!(state.display_map.wrap_row_count(), 1)
4872 });
4873 }
4874
4875 #[gpui::test]
4876 fn test_inline_token_geometry_and_reentrant_activation(cx: &mut TestAppContext) {
4877 use crate::input::{InlineToken, InlineTokenPresentation};
4878 let view = InputView::build(cx, |state| state.default_value("before @alice after"));
4879 let target = view.input.clone();
4880 view.window_handle
4881 .update(cx, |_, window, cx| {
4882 view.input.update(cx, |state, cx| {
4883 state
4884 .replace_range_with_token(
4885 7..13,
4886 InlineToken::new("a", "@alice"),
4887 window,
4888 cx,
4889 )
4890 .unwrap();
4891 state.set_token_presentation(
4892 InlineTokenPresentation::default()
4893 .token(|_, _, _| div().w(px(100.)).h(px(20.)))
4894 .on_token_click(move |_, window, cx| {
4895 target.update(cx, |state, cx| {
4896 state.set_value("activated", window, cx)
4897 })
4898 }),
4899 );
4900 state.focus(window, cx);
4901 state.set_selected_range(7..13, cx);
4902 });
4903 })
4904 .unwrap();
4905 let mut visual = VisualTestContext::from_window(view.window_handle.into(), cx);
4906 visual.update(|window, cx| window.draw(cx).clear(cx));
4907 visual.update(|window, cx| {
4908 let state = view.input.read(cx);
4909 let bounds = state.range_to_bounds(&(7..13)).unwrap();
4910 assert!((bounds.size.width - px(100.)).abs() < px(1.), "{bounds:?}");
4911 let (left, _) = state.index_for_mouse_position(bounds.origin + point(px(10.), px(5.)));
4912 let (right, _) = state.index_for_mouse_position(bounds.origin + point(px(90.), px(5.)));
4913 assert_eq!((left, right), (7, 13));
4914 window.dispatch_action(Box::new(ActivateToken), cx);
4915 });
4916 visual.run_until_parked();
4917 view.input.read_with(&visual, |state, _| {
4918 assert_eq!(state.value().as_ref(), "activated")
4919 });
4920 }
4921
4922 #[gpui::test]
4923 fn test_inline_token_click_selects_it(cx: &mut TestAppContext) {
4924 use crate::input::InlineToken;
4925 cx.update(crate::init);
4926 let view = InputView::build(cx, |state| state.default_value("before @alice after"));
4927 view.window_handle
4928 .update(cx, |_, window, cx| {
4929 view.input.update(cx, |state, cx| {
4930 state
4931 .replace_range_with_token(
4932 7..13,
4933 InlineToken::new("a", "@alice"),
4934 window,
4935 cx,
4936 )
4937 .unwrap();
4938 state.set_selected_range(0..0, cx);
4939 });
4940 })
4941 .unwrap();
4942 let mut visual = VisualTestContext::from_window(view.window_handle.into(), cx);
4943 visual.update(|window, cx| window.draw(cx).clear(cx));
4944 let bounds = view
4945 .input
4946 .read_with(&visual, |state, _| state.range_to_bounds(&(7..13)).unwrap());
4947 visual.simulate_click(bounds.center(), gpui::Modifiers::default());
4948 visual.run_until_parked();
4949 view.input.read_with(&visual, |state, _| {
4950 assert_eq!(state.selected_range(), 7..13, "a click selects the token");
4951 });
4952 }
4953
4954 #[gpui::test]
4955 fn test_inline_token_hover_reports_presence_without_selecting(cx: &mut TestAppContext) {
4956 use crate::input::{InlineToken, InlineTokenPresentation};
4957 use gpui::{Bounds, point, px, size};
4958 cx.update(crate::init);
4959 let view = InputView::build(cx, |state| state.default_value("before @alice after"));
4960 let bounds = Bounds::new(point(px(10.), px(20.)), size(px(100.), px(20.)));
4961 view.window_handle
4962 .update(cx, |_, window, cx| {
4963 view.input.update(cx, |state, cx| {
4964 state
4965 .replace_range_with_token(
4966 7..13,
4967 InlineToken::new("a", "@alice"),
4968 window,
4969 cx,
4970 )
4971 .unwrap();
4972 state.set_token_presentation(
4973 InlineTokenPresentation::default().on_token_hover(|_, _, _| {}),
4974 );
4975 state.set_selected_range(0..0, cx);
4976 });
4977 })
4978 .unwrap();
4979 view.window_handle
4980 .update(cx, |_, _, cx| {
4981 view.input.update(cx, |state, _| {
4982 let (_, entered) = state
4983 .token_hover(7, bounds, true, None)
4984 .expect("hover enters an enabled token");
4985 assert!(entered.is_hovered());
4986 assert_eq!(entered.token().id().as_ref(), "a");
4987 assert_eq!(entered.range(), 7..13);
4988 assert_eq!(entered.bounds(), bounds);
4989 let (_, left) = state
4990 .token_hover(7, bounds, false, None)
4991 .expect("hover leaves the same token");
4992 assert!(!left.is_hovered());
4993 assert_eq!(state.selected_range(), 0..0, "hover never selects");
4994 assert!(state.token_hover(99, bounds, true, None).is_none());
4995 });
4996 })
4997 .unwrap();
4998
4999 view.window_handle
5001 .update(cx, |_, _, cx| {
5002 view.input.update(cx, |state, cx| {
5003 state.set_disabled(true, cx);
5004 });
5005 })
5006 .unwrap();
5007 view.window_handle
5008 .update(cx, |_, _, cx| {
5009 view.input.update(cx, |state, _| {
5010 assert!(state.token_hover(7, bounds, true, None).is_none())
5011 })
5012 })
5013 .unwrap();
5014 view.window_handle
5015 .update(cx, |_, _, cx| {
5016 view.input.update(cx, |state, cx| {
5017 state.set_disabled(false, cx);
5018 state.set_readonly(true, cx);
5019 });
5020 })
5021 .unwrap();
5022 view.window_handle
5023 .update(cx, |_, _, cx| {
5024 view.input.update(cx, |state, _| {
5025 assert!(state.token_hover(7, bounds, true, None).is_some())
5026 })
5027 })
5028 .unwrap();
5029
5030 view.window_handle
5032 .update(cx, |_, _, cx| {
5033 view.input.update(cx, |state, _| {
5034 state.set_token_presentation(InlineTokenPresentation::default());
5035 });
5036 })
5037 .unwrap();
5038 view.window_handle
5039 .update(cx, |_, _, cx| {
5040 view.input.update(cx, |state, _| {
5041 assert!(state.token_hover(7, bounds, true, None).is_none())
5042 })
5043 })
5044 .unwrap();
5045 }
5046
5047 #[gpui::test]
5048 fn test_inline_token_hover_exit_delivered_when_token_removed(cx: &mut TestAppContext) {
5049 use crate::input::{InlineToken, InlineTokenPresentation};
5050 use std::cell::RefCell;
5051 use std::rc::Rc;
5052 cx.update(crate::init);
5053 let view = InputView::build(cx, |state| state.default_value("before @alice after"));
5054 let events: Rc<RefCell<Vec<(SharedString, bool)>>> = Rc::new(RefCell::new(Vec::new()));
5055 let sink = events.clone();
5056 view.window_handle
5057 .update(cx, |_, window, cx| {
5058 view.input.update(cx, |state, cx| {
5059 state
5060 .replace_range_with_token(
5061 7..13,
5062 InlineToken::new("a", "@alice"),
5063 window,
5064 cx,
5065 )
5066 .unwrap();
5067 state.set_token_presentation(
5068 InlineTokenPresentation::default()
5069 .token(|_, _, _| div().w(px(100.)).h(px(20.)))
5070 .on_token_hover(move |event, _, _| {
5071 sink.borrow_mut()
5072 .push((event.token().id().clone(), event.is_hovered()));
5073 }),
5074 );
5075 });
5076 })
5077 .unwrap();
5078 let mut visual = VisualTestContext::from_window(view.window_handle.into(), cx);
5079 visual.update(|window, cx| window.draw(cx).clear(cx));
5080 let known_bounds = view.input.read_with(&visual, |state, _| {
5081 state
5082 .token_bounds
5083 .get(&7)
5084 .copied()
5085 .expect("placed token bounds")
5086 });
5087 visual.simulate_mouse_move(known_bounds.center(), None, gpui::Modifiers::default());
5088 visual.run_until_parked();
5089 visual.update(|window, cx| window.draw(cx).clear(cx));
5090 assert_eq!(events.borrow().as_slice(), &[("a".into(), true)]);
5091
5092 view.window_handle
5096 .update(cx, |_, window, cx| {
5097 view.input
5098 .update(cx, |state, cx| state.set_value("no tokens", window, cx));
5099 })
5100 .unwrap();
5101 visual.run_until_parked();
5102 visual.update(|window, cx| window.draw(cx).clear(cx));
5103 assert_eq!(
5104 events.borrow().as_slice(),
5105 &[("a".into(), true), ("a".into(), false)]
5106 );
5107 }
5108
5109 #[gpui::test]
5110 fn test_inline_token_hover_bounds_stay_single_row_at_wrap_boundary(cx: &mut TestAppContext) {
5111 use crate::input::{InlineToken, InlineTokenPresentation};
5112 cx.update(crate::init);
5113 let view = InputView::build_textarea(cx, |state| {
5114 state
5115 .rows(4)
5116 .default_value("aaaa bbbb cccc dddd @alice eeee ffff")
5117 });
5118 view.window_handle
5119 .update(cx, |_, window, cx| {
5120 view.input.update(cx, |state, cx| {
5121 state
5122 .replace_range_with_token(
5123 20..26,
5124 InlineToken::new("a", "@alice"),
5125 window,
5126 cx,
5127 )
5128 .unwrap();
5129 state.set_token_presentation(
5130 InlineTokenPresentation::default()
5131 .token(|_, _, _| div().w(px(60.)).h(px(20.))),
5132 );
5133 });
5134 })
5135 .unwrap();
5136 let mut visual = VisualTestContext::from_window(view.window_handle.into(), cx);
5137 visual.update(|window, cx| window.draw(cx).clear(cx));
5138 let (placed, ranged, line_height) = view.input.read_with(&visual, |state, _| {
5139 (
5140 state
5141 .token_bounds
5142 .get(&20)
5143 .copied()
5144 .expect("placed token bounds"),
5145 state.range_to_bounds(&(20..26)),
5146 state.last_layout.as_ref().unwrap().line_height,
5147 )
5148 });
5149 assert!(placed.size.height <= line_height + px(1.), "{placed:?}");
5153 assert!(
5154 placed.size.width <= px(61.) && placed.size.width > px(0.),
5155 "{placed:?}"
5156 );
5157 if let Some(ranged) = ranged {
5158 assert!(
5159 ranged.size.height >= placed.size.height - px(1.),
5160 "{ranged:?} vs {placed:?}"
5161 );
5162 }
5163 }
5164
5165 #[gpui::test]
5166 fn test_inline_token_hover_same_offset_replacement_exits_predecessor(cx: &mut TestAppContext) {
5167 use crate::input::{InlineToken, InlineTokenPresentation};
5168 use std::{cell::RefCell, rc::Rc};
5169 cx.update(crate::init);
5170 let view = InputView::build(cx, |state| state.default_value("@alice!"));
5171 let events = Rc::new(RefCell::new(Vec::new()));
5172 let sink = events.clone();
5173 view.window_handle
5174 .update(cx, |_, window, cx| {
5175 view.input.update(cx, |state, cx| {
5176 state
5177 .replace_range_with_token(0..6, InlineToken::new("a", "@alice"), window, cx)
5178 .unwrap();
5179 state.set_token_presentation(
5180 InlineTokenPresentation::default()
5181 .token(|_, _, _| div().w(px(100.)).h(px(20.)))
5182 .on_token_hover(move |event, _, _| {
5183 sink.borrow_mut()
5184 .push((event.token().id().clone(), event.is_hovered()));
5185 }),
5186 );
5187 });
5188 })
5189 .unwrap();
5190 let mut visual = VisualTestContext::from_window(view.window_handle.into(), cx);
5191 visual.update(|window, cx| window.draw(cx).clear(cx));
5192 let bounds = view
5193 .input
5194 .read_with(&visual, |state, _| state.token_bounds[&0]);
5195 visual.simulate_mouse_move(bounds.center(), None, gpui::Modifiers::default());
5196 visual.run_until_parked();
5197 visual.update(|window, cx| window.draw(cx).clear(cx));
5198 assert_eq!(events.borrow().as_slice(), &[("a".into(), true)]);
5199
5200 view.window_handle
5201 .update(cx, |_, window, cx| {
5202 view.input.update(cx, |state, cx| {
5203 state
5204 .replace_range_with_token(0..6, InlineToken::new("b", "@alice"), window, cx)
5205 .unwrap();
5206 });
5207 })
5208 .unwrap();
5209 visual.run_until_parked();
5210 visual.update(|window, cx| window.draw(cx).clear(cx));
5211 visual.simulate_mouse_move(
5212 bounds.center() + gpui::point(px(1.), px(0.)),
5213 None,
5214 gpui::Modifiers::default(),
5215 );
5216 visual.run_until_parked();
5217 visual.update(|window, cx| window.draw(cx).clear(cx));
5218 assert_eq!(
5219 events.borrow().as_slice(),
5220 &[("a".into(), true), ("a".into(), false), ("b".into(), true)]
5221 );
5222 view.input
5225 .update(&mut visual, |state, cx| state.set_disabled(true, cx));
5226 visual.update(|window, cx| window.draw(cx).clear(cx));
5227 visual.run_until_parked();
5228 assert_eq!(events.borrow().last(), Some(&("b".into(), false)));
5229 view.input
5230 .update(&mut visual, |state, cx| state.set_disabled(false, cx));
5231 visual.update(|window, cx| window.draw(cx).clear(cx));
5232 visual.run_until_parked();
5233 visual.simulate_mouse_move(bounds.center(), None, gpui::Modifiers::default());
5234 visual.run_until_parked();
5235 assert_eq!(
5236 events.borrow().as_slice(),
5237 &[
5238 ("a".into(), true),
5239 ("a".into(), false),
5240 ("b".into(), true),
5241 ("b".into(), false),
5242 ("b".into(), true)
5243 ]
5244 );
5245 visual.simulate_mouse_move(
5246 bounds.bottom_right() + gpui::point(px(10.), px(10.)),
5247 None,
5248 gpui::Modifiers::default(),
5249 );
5250 visual.run_until_parked();
5251 assert_eq!(
5252 events.borrow().as_slice(),
5253 &[
5254 ("a".into(), true),
5255 ("a".into(), false),
5256 ("b".into(), true),
5257 ("b".into(), false),
5258 ("b".into(), true),
5259 ("b".into(), false)
5260 ]
5261 );
5262 }
5263
5264 #[gpui::test]
5265 fn test_inline_token_hover_older_exit_keeps_newer_snapshot(cx: &mut TestAppContext) {
5266 use crate::input::{InlineToken, InlineTokenPresentation};
5267 use gpui::{Bounds, point, px, size};
5268 cx.update(crate::init);
5269 let view = InputView::build(cx, |state| state.default_value("@a @b"));
5270 let bounds = Bounds::new(point(px(10.), px(20.)), size(px(100.), px(20.)));
5271 view.window_handle
5272 .update(cx, |_, window, cx| {
5273 view.input.update(cx, |state, cx| {
5274 state
5275 .replace_range_with_token(0..2, InlineToken::new("a", "@a"), window, cx)
5276 .unwrap();
5277 state
5278 .replace_range_with_token(3..5, InlineToken::new("b", "@b"), window, cx)
5279 .unwrap();
5280 state.set_token_presentation(
5281 InlineTokenPresentation::default().on_token_hover(|_, _, _| {}),
5282 );
5283 state
5286 .token_hover(0, bounds, true, None)
5287 .expect("hover enters A");
5288 state
5289 .token_hover(3, bounds, true, None)
5290 .expect("hover enters B");
5291 let (_, exit_a) = state.token_hover(0, bounds, false, None).expect("A exits");
5292 assert!(!exit_a.is_hovered());
5293 assert_eq!(
5295 state
5296 .hovered_token
5297 .as_ref()
5298 .expect("B still hovered")
5299 .span
5300 .token()
5301 .id()
5302 .as_ref(),
5303 "b"
5304 );
5305 state.set_value("no tokens", window, cx);
5307 state.token_bounds.clear();
5308 let (listener, exit_b) = state.reconcile_token_hover().expect("B must exit");
5309 assert!(!exit_b.is_hovered());
5310 assert_eq!(exit_b.token().id().as_ref(), "b");
5311 drop(listener);
5312 });
5313 })
5314 .unwrap();
5315 }
5316
5317 #[gpui::test]
5318 fn test_inline_token_hover_late_exit_matches_replaced_token(cx: &mut TestAppContext) {
5319 use crate::input::{InlineToken, InlineTokenPresentation};
5320 cx.update(crate::init);
5321 let view = InputView::build(cx, |state| state.default_value("@a"));
5322 view.window_handle
5323 .update(cx, |_, window, cx| {
5324 view.input.update(cx, |state, cx| {
5325 let a = InlineToken::new("a", "@a");
5326 let b = InlineToken::new("b", "@a");
5327 state
5328 .replace_range_with_token(0..2, a.clone(), window, cx)
5329 .unwrap();
5330 state.set_token_presentation(
5331 InlineTokenPresentation::default().on_token_hover(|_, _, _| {}),
5332 );
5333 let bounds = gpui::Bounds::default();
5334 state.token_hover(0, bounds, true, Some(&a)).unwrap();
5335 state
5336 .replace_range_with_token(0..2, b.clone(), window, cx)
5337 .unwrap();
5338 state.token_hover(0, bounds, true, Some(&b)).unwrap();
5339 let (_, exit_a) = state.token_hover(0, bounds, false, Some(&a)).unwrap();
5340 assert_eq!(exit_a.token().id().as_ref(), "a");
5341 assert_eq!(state.hovered_token.as_ref().unwrap().span.token(), &b);
5342 let (_, exit_b) = state.token_hover(0, bounds, false, Some(&b)).unwrap();
5343 assert_eq!(exit_b.token().id().as_ref(), "b");
5344 assert!(state.pending_token_hover_exits.is_empty());
5345 });
5346 })
5347 .unwrap();
5348 }
5349
5350 #[gpui::test]
5351 fn test_inline_token_hover_exit_keeps_entry_utf16_after_edit(cx: &mut TestAppContext) {
5352 use crate::input::{InlineToken, InlineTokenPresentation};
5353 use gpui::EntityInputHandler as _;
5354 cx.update(crate::init);
5355 let view = InputView::build(cx, |state| state.default_value("🙂 @a"));
5356 view.window_handle
5357 .update(cx, |_, window, cx| {
5358 view.input.update(cx, |state, cx| {
5359 state
5360 .replace_range_with_token(5..7, InlineToken::new("a", "@a"), window, cx)
5361 .unwrap();
5362 state.set_token_presentation(
5363 InlineTokenPresentation::default().on_token_hover(|_, _, _| {}),
5364 );
5365 let bounds = gpui::Bounds::default();
5366 let (_, enter) = state.token_hover(5, bounds, true, None).unwrap();
5367 assert_eq!(enter.range_utf16(), (3, 5));
5368 state.replace_text_in_range(Some(0..2), "abcd", window, cx);
5369 assert_eq!(state.token_spans()[0].range(), 5..7);
5370 let (_, exit) = state.token_hover(5, bounds, false, None).unwrap();
5371 assert_eq!(exit.range_utf16(), (3, 5));
5372 });
5373 })
5374 .unwrap();
5375 }
5376
5377 #[gpui::test]
5378 fn test_inline_token_hover_callback_edit_preserves_both_exits(cx: &mut TestAppContext) {
5379 use crate::input::{InlineToken, InlineTokenPresentation};
5380 cx.update(crate::init);
5381 let view = InputView::build(cx, |state| state.default_value("@a @b"));
5382 view.window_handle
5383 .update(cx, |_, window, cx| {
5384 view.input.update(cx, |state, cx| {
5385 state
5386 .replace_range_with_token(0..2, InlineToken::new("a", "@a"), window, cx)
5387 .unwrap();
5388 state
5389 .replace_range_with_token(3..5, InlineToken::new("b", "@b"), window, cx)
5390 .unwrap();
5391 state.set_token_presentation(
5392 InlineTokenPresentation::default().on_token_hover(|_, _, _| {}),
5393 );
5394 let bounds = gpui::Bounds::default();
5395 state.token_hover(0, bounds, true, None).unwrap();
5396 state.token_hover(3, bounds, true, None).unwrap();
5397 state.set_value("gone", window, cx);
5399 let (_, exit_a) = state.token_hover(0, bounds, false, None).unwrap();
5400 assert_eq!(exit_a.token().id().as_ref(), "a");
5401 let (_, exit_b) = state.reconcile_token_hover().unwrap();
5402 assert_eq!(exit_b.token().id().as_ref(), "b");
5403 assert!(state.token_hover(0, bounds, false, None).is_none());
5404 assert!(state.reconcile_token_hover().is_none());
5405 });
5406 })
5407 .unwrap();
5408 }
5409
5410 #[gpui::test]
5411 fn test_inline_token_edit_history_and_validation(cx: &mut TestAppContext) {
5412 let view = InputView::build(cx, |state| state.default_value("问 @alice!"));
5413 view.window_handle
5414 .update(cx, |_, window, cx| {
5415 view.input.update(cx, |state, cx| {
5416 let token =
5417 crate::input::InlineToken::new("alice-1", "@alice").with_label("Alice");
5418 state
5419 .replace_range_with_token(4..10, token.clone(), window, cx)
5420 .unwrap();
5421 assert_eq!(state.value().as_ref(), "问 @alice!");
5422 assert_eq!(state.tokens()[0].range(), 4..10);
5423 assert_eq!(state.next_end_of_word_at(4), 10);
5424 assert_eq!(state.previous_start_of_word_at(10), 4);
5425 state.undo(&Undo, window, cx);
5426 assert!(
5427 state.tokens().is_empty(),
5428 "identity-only association is undoable"
5429 );
5430 assert_eq!(state.value().as_ref(), "问 @alice!");
5431 state.redo(&Redo, window, cx);
5432 assert_eq!(state.tokens()[0].token(), &token);
5433 state.set_selected_range(6..7, cx);
5434 assert_eq!(state.selected_range(), 4..10);
5435 state.replace("", window, cx);
5436 assert_eq!(state.value().as_ref(), "问 !");
5437 assert!(state.tokens().is_empty());
5438 state.undo(&Undo, window, cx);
5439 assert_eq!(state.tokens()[0].token(), &token);
5440 state.set_selected_range(0..0, cx);
5441 state.replace("🙂", window, cx);
5442 assert_eq!(state.tokens()[0].range(), 8..14);
5443 state.undo(&Undo, window, cx);
5444 assert_eq!(state.tokens()[0].range(), 4..10);
5445 let before = state.content();
5446 state
5447 .replace_range_with_token(0..0, token.clone(), window, cx)
5448 .expect("the same reference may occur twice");
5449 assert_eq!(state.value().as_ref(), "@alice问 @alice!");
5450 assert_eq!(state.tokens().len(), 2);
5451 assert_eq!(state.tokens()[1].token(), &token);
5452 state.undo(&Undo, window, cx);
5453 assert_eq!(state.content(), before);
5454 state.set_value(before.text().clone(), window, cx);
5455 assert!(state.tokens().is_empty());
5456 assert!(!state.undo_manager.has_undos());
5457 });
5458 })
5459 .unwrap();
5460 }
5461
5462 #[gpui::test]
5463 fn test_inline_token_textarea_ime_and_modes(cx: &mut TestAppContext) {
5464 use crate::input::{InlineToken, InlineTokenError, InputContent};
5465 let view = InputView::build_textarea(cx, |state| state);
5466 view.window_handle
5467 .update(cx, |_, window, cx| {
5468 view.input.update(cx, |state, cx| {
5469 let content = InputContent::new("@a@b\n后面")
5470 .with_token(0..2, InlineToken::new("a", "@a"))
5471 .unwrap()
5472 .with_token(2..4, InlineToken::new("b", "@b"))
5473 .unwrap();
5474 state.set_value(content.clone(), window, cx);
5475 state.set_selected_range(2..2, cx);
5476 assert_eq!(state.previous_boundary(2), 0);
5477 assert_eq!(state.next_boundary(2), 4);
5478 state.replace_and_mark_text_in_range(Some(1..2), "中", Some(1..1), window, cx);
5479 assert_eq!(state.value().as_ref(), "中@b\n后面");
5480 assert_eq!(state.tokens().len(), 1);
5481 assert_eq!(
5482 state.replace_with_token(InlineToken::new("x", "x"), window, cx),
5483 Err(InlineTokenError::CompositionActive)
5484 );
5485 state.replace_text_in_range(None, "中文", window, cx);
5486 state.undo(&Undo, window, cx);
5487 assert_eq!(state.content(), content);
5488 state.redo(&Redo, window, cx);
5489 assert_eq!(state.value().as_ref(), "中文@b\n后面");
5490 state.undo(&Undo, window, cx);
5491 state.replace_all("plain", window, cx);
5492 assert!(state.tokens().is_empty());
5493 state.undo(&Undo, window, cx);
5494 assert_eq!(state.content(), content);
5495 });
5496 })
5497 .unwrap();
5498 let view = InputView::build(cx, |state| state);
5499 view.window_handle
5500 .update(cx, |_, window, cx| {
5501 view.input.update(cx, |state, cx| {
5502 assert_eq!(
5503 InputContent::new("a\u{301}")
5504 .with_token(0..1, InlineToken::new("bad", "a"))
5505 .err(),
5506 Some(InlineTokenError::InvalidBoundary)
5507 );
5508 assert_eq!(
5509 InputContent::new("@a@a")
5510 .with_token(0..2, InlineToken::new("a", "@a"))
5511 .unwrap()
5512 .with_token(1..3, InlineToken::new("x", "a@"))
5513 .err(),
5514 Some(InlineTokenError::OverlappingTokens)
5515 );
5516 state
5517 .replace_with_token(InlineToken::new("a", "@a"), window, cx)
5518 .unwrap();
5519 let before = state.content();
5520 state.set_masked(true, window, cx);
5521 assert!(!state.tokens_visible());
5522 assert_eq!(
5523 state.replace_with_token(InlineToken::new("b", "b"), window, cx),
5524 Err(InlineTokenError::UnsupportedMode)
5525 );
5526 assert_eq!(state.content(), before);
5527 state.undo(&Undo, window, cx);
5528 assert!(state.tokens().is_empty());
5529 state.redo(&Redo, window, cx);
5530 assert_eq!(state.content(), before);
5531 assert!(!state.tokens_visible());
5532 });
5533 })
5534 .unwrap();
5535 }
5536
5537 use crate::theme::Theme;
5538 use gpui::{TestAppContext, VisualTestContext, size};
5539
5540 use crate::input::{EditorMode, EditorState, InputMode, LanguageConfig, TextareaMode};
5541
5542 fn set_test_syntax_provider(
5543 provider: Rc<dyn crate::input::SyntaxContextProvider>,
5544 cx: &mut App,
5545 ) {
5546 struct TestLanguages(Rc<dyn crate::input::SyntaxContextProvider>);
5547 impl crate::input::LanguageProvider for TestLanguages {
5548 fn syntax_context_provider(
5549 &self,
5550 _: &str,
5551 ) -> Option<Rc<dyn crate::input::SyntaxContextProvider>> {
5552 Some(self.0.clone())
5553 }
5554 }
5555 crate::input::set_language_provider(Rc::new(TestLanguages(provider)), cx);
5556 }
5557
5558 struct TestRoot<M: InputModeKind>(Entity<InputBaseState<M>>);
5559
5560 impl<M: InputModeKind> Render for TestRoot<M> {
5561 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
5562 div().size_full().child(self.0.clone())
5563 }
5564 }
5565
5566 struct InputView<M: InputModeKind> {
5567 input: Entity<InputBaseState<M>>,
5568 window_handle: gpui::WindowHandle<TestRoot<M>>,
5569 }
5570
5571 impl<M: InputModeKind> InputView<M> {
5573 fn build_with(
5574 cx: &mut TestAppContext,
5575 make: impl FnOnce(&mut Window, &mut Context<InputBaseState<M>>) -> InputBaseState<M>
5576 + 'static,
5577 ) -> Self {
5578 let mut input: Option<Entity<InputBaseState<M>>> = None;
5579
5580 let window = cx.update(|cx| {
5581 cx.open_window(Default::default(), |window, cx| {
5582 cx.set_global(Theme::default());
5584 super::super::init(cx);
5586
5587 input = Some(cx.new(|cx| make(window, cx)));
5588
5589 cx.new(|_| TestRoot(input.clone().unwrap()))
5590 })
5591 .unwrap()
5592 });
5593
5594 Self {
5595 input: input.clone().unwrap(),
5596 window_handle: window,
5597 }
5598 }
5599 }
5600
5601 impl InputView<EditorMode> {
5602 fn new(cx: &mut TestAppContext) -> Self {
5604 Self::build_editor(cx, |state| state)
5605 }
5606
5607 fn build_editor(
5608 cx: &mut TestAppContext,
5609 f: impl FnOnce(InputBaseState<EditorMode>) -> InputBaseState<EditorMode> + 'static,
5610 ) -> Self {
5611 Self::build_with(cx, move |window, cx| {
5612 f(crate::input::EditorState::new(window, cx).language("sql"))
5613 })
5614 }
5615 }
5616
5617 impl InputView<TextareaMode> {
5618 fn build_textarea(
5619 cx: &mut TestAppContext,
5620 f: impl FnOnce(InputBaseState<TextareaMode>) -> InputBaseState<TextareaMode> + 'static,
5621 ) -> Self {
5622 Self::build_with(cx, move |window, cx| {
5623 f(crate::input::TextareaState::new(window, cx))
5624 })
5625 }
5626 }
5627
5628 impl InputView<InputMode> {
5629 fn build(
5631 cx: &mut TestAppContext,
5632 f: impl FnOnce(InputBaseState<InputMode>) -> InputBaseState<InputMode> + 'static,
5633 ) -> Self {
5634 Self::build_with(cx, move |window, cx| {
5635 f(crate::input::InputState::new(window, cx))
5636 })
5637 }
5638 }
5639
5640 #[gpui::test]
5643 fn single_line_is_centered_in_a_taller_frame(cx: &mut TestAppContext) {
5644 struct Frame(Entity<InputBaseState<InputMode>>);
5645 impl Render for Frame {
5646 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
5647 crate::input::InputBase::new("frame")
5648 .h(px(60.))
5649 .child(crate::input::Input::new(&self.0))
5650 }
5651 }
5652
5653 cx.update(crate::init);
5654 let mut input = None;
5655 let window = cx.open_window(size(px(400.), px(100.)), |window, cx| {
5656 let state = cx.new(|cx| crate::input::InputState::new(window, cx).default_value("a"));
5657 input = Some(state.clone());
5658 Frame(state)
5659 });
5660 let input = input.unwrap();
5661 let mut cx = VisualTestContext::from_window(window.into(), cx);
5662 cx.update(|window, cx| window.draw(cx).clear(cx));
5663
5664 input.read_with(&cx, |state, _| {
5665 let line = state.input_bounds;
5666 assert_eq!(line.center().y, px(30.), "line {line:?}");
5667 });
5668 }
5669
5670 #[gpui::test]
5671 fn test_noop_scroll_notifies_diagnostic_dismissal(cx: &mut TestAppContext) {
5672 use std::{cell::Cell, rc::Rc};
5673
5674 cx.update(crate::init);
5675 let mut input = None;
5676 let window = cx.open_window(size(px(400.), px(100.)), |window, cx| {
5677 input = Some(cx.new(|cx| crate::input::EditorState::new(window, cx)));
5678 gpui::EmptyView
5679 });
5680 let input = input.unwrap();
5681 input.update(cx, |state, cx| {
5682 state.input_bounds = Bounds::new(Point::default(), size(px(100.), px(20.)));
5683 state.scroll_size = size(px(100.), px(20.));
5684 state.present_diagnostic(crate::input::DiagnosticEntry::default(), cx);
5685 });
5686 let notifications = Rc::new(Cell::new(0));
5687 let count = notifications.clone();
5688 let _subscription =
5689 cx.update(|cx| cx.observe(&input, move |_, _| count.set(count.get() + 1)));
5690
5691 window
5692 .update(cx, |_, window, cx| {
5693 input.update(cx, |state, cx| {
5694 state.on_scroll_wheel(
5695 &ScrollWheelEvent {
5696 delta: gpui::ScrollDelta::Pixels(point(px(0.), px(-40.))),
5697 ..Default::default()
5698 },
5699 window,
5700 cx,
5701 );
5702 assert_eq!(state.scroll_handle.offset(), Point::default());
5703 assert!(state.diagnostic_popover().is_none());
5704 });
5705 })
5706 .unwrap();
5707 assert_eq!(
5708 notifications.get(),
5709 1,
5710 "clearing a diagnostic must notify even when scrolling is clamped"
5711 );
5712 window
5713 .update(cx, |_, window, cx| {
5714 input.update(cx, |state, cx| {
5715 state.on_scroll_wheel(
5716 &ScrollWheelEvent {
5717 delta: gpui::ScrollDelta::Pixels(point(px(0.), px(-40.))),
5718 ..Default::default()
5719 },
5720 window,
5721 cx,
5722 );
5723 });
5724 })
5725 .unwrap();
5726 assert_eq!(notifications.get(), 1, "an unchanged input must stay quiet");
5727 }
5728
5729 #[gpui::test]
5730 fn test_cursor_layout_consumer_updates_after_selection(cx: &mut TestAppContext) {
5731 use std::{cell::Cell, rc::Rc};
5732 struct Panel {
5733 input: Entity<crate::input::EditorState>,
5734 observed: Rc<Cell<Option<(Bounds<Pixels>, Pixels)>>>,
5735 }
5736 impl Render for Panel {
5737 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
5738 self.observed.set(self.input.read(cx).cursor_layout());
5739 div().size_full().child(self.input.clone())
5740 }
5741 }
5742 struct Root(Entity<Panel>);
5743 impl Render for Root {
5744 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
5745 div().size_full().child(
5746 self.0
5747 .clone()
5748 .cached(gpui::StyleRefinement::default().size_full()),
5749 )
5750 }
5751 }
5752 cx.update(crate::init);
5753 let observed = Rc::new(Cell::new(None));
5754 let mut input = None;
5755 let window = cx.open_window(size(px(400.), px(100.)), |window, cx| {
5756 let state = cx.new(|cx| {
5757 let mut state = crate::input::EditorState::new(window, cx);
5758 state.set_value("abcdefgh", window, cx);
5759 state
5760 });
5761 input = Some(state.clone());
5762 Root(cx.new(|_| Panel {
5763 input: state,
5764 observed: observed.clone(),
5765 }))
5766 });
5767 cx.run_until_parked();
5768 let input = input.unwrap();
5769 let before = input.read_with(cx, |state, _| state.cursor_layout());
5770 input.update(cx, |_, cx| cx.notify());
5775 cx.run_until_parked();
5776 assert_eq!(observed.get(), before);
5777 window
5778 .update(cx, |_, _, cx| {
5779 input.update(cx, |state, cx| state.select_to_with_affinity(3, false, cx));
5780 })
5781 .unwrap();
5782 cx.run_until_parked();
5783 window.update(cx, |_, _, cx| cx.notify()).unwrap();
5784 let after = input.read_with(cx, |state, _| state.cursor_layout());
5785 assert_ne!(after, before, "the caret must actually move");
5786 assert_eq!(
5787 observed.get(),
5788 after,
5789 "render consumers must receive the newly painted caret geometry"
5790 );
5791 }
5792
5793 #[gpui::test]
5794 fn test_input_scroll_offset_notifies_only_on_change(cx: &mut TestAppContext) {
5795 use std::{cell::Cell, rc::Rc};
5796
5797 cx.update(crate::init);
5798 let mut input = None;
5799 let _window = cx.open_window(size(px(400.), px(100.)), |window, cx| {
5800 input = Some(cx.new(|cx| crate::input::InputState::new(window, cx)));
5801 gpui::EmptyView
5802 });
5803 let input = input.unwrap();
5804 let notifications = Rc::new(Cell::new(0));
5805 let count = notifications.clone();
5806 let _subscription =
5807 cx.update(|cx| cx.observe(&input, move |_, _| count.set(count.get() + 1)));
5808
5809 input.update(cx, |state, _| {
5810 state.input_bounds = Bounds::new(Point::default(), size(px(100.), px(20.)));
5811 state.scroll_size = size(px(300.), px(20.));
5812 });
5813 for target in [
5814 None,
5815 Some(point(px(0.), px(0.))),
5816 Some(point(px(20.), px(50.))),
5817 ] {
5818 input.update(cx, |state, cx| state.update_scroll_offset(target, cx));
5819 assert_eq!(
5820 notifications.get(),
5821 0,
5822 "an unchanged clamped offset must stay quiet"
5823 );
5824 }
5825 input.update(cx, |state, cx| {
5826 state.update_scroll_offset(Some(point(px(-40.), px(0.))), cx);
5827 assert_eq!(state.scroll_handle.offset(), point(px(-40.), px(0.)));
5828 });
5829 assert_eq!(notifications.get(), 1, "a scroll must notify");
5830 input.update(cx, |state, cx| {
5831 state.update_scroll_offset(Some(point(px(-400.), px(50.))), cx);
5832 assert_eq!(state.scroll_handle.offset(), point(px(-200.), px(0.)));
5833 });
5834 assert_eq!(notifications.get(), 2);
5835 input.update(cx, |state, cx| {
5836 state.update_scroll_offset(Some(point(px(-500.), px(0.))), cx);
5837 });
5838 assert_eq!(
5839 notifications.get(),
5840 2,
5841 "clamping to the current offset must stay quiet"
5842 );
5843 input.update(cx, |state, cx| {
5844 state.scroll_size.width = px(110.);
5845 state.update_scroll_offset(None, cx);
5846 assert_eq!(state.scroll_handle.offset(), point(px(-10.), px(0.)));
5847 });
5848 assert_eq!(
5849 notifications.get(),
5850 3,
5851 "a smaller scroll range must clamp and notify"
5852 );
5853 }
5854
5855 #[gpui::test]
5856 fn test_set_value_on_unfocused_input_stays_quiet(cx: &mut TestAppContext) {
5857 use std::{cell::Cell, rc::Rc};
5858
5859 cx.update(crate::init);
5860 let mut input = None;
5861 let window = cx.open_window(size(px(400.), px(100.)), |window, cx| {
5862 input = Some(cx.new(|cx| crate::input::InputState::new(window, cx)));
5863 gpui::EmptyView
5864 });
5865 let input = input.unwrap();
5866 cx.run_until_parked();
5867
5868 let notifications = Rc::new(Cell::new(0));
5869 let count = notifications.clone();
5870 let _subscription =
5871 cx.update(|cx| cx.observe(&input, move |_, _| count.set(count.get() + 1)));
5872 cx.run_until_parked();
5873
5874 window
5877 .update(cx, |_, window, cx| {
5878 input.update(cx, |state, cx| state.set_value("seeded", window, cx));
5879 })
5880 .unwrap();
5881 cx.run_until_parked();
5882 let settled = notifications.get();
5883
5884 cx.executor()
5885 .advance_clock(std::time::Duration::from_secs(3));
5886 cx.run_until_parked();
5887
5888 assert_eq!(
5889 notifications.get(),
5890 settled,
5891 "an unfocused input is blinking, and every blink repaints the view it is in"
5892 );
5893 }
5894
5895 #[gpui::test]
5896 fn test_input_does_not_invalidate_cached_parent_during_paint(cx: &mut TestAppContext) {
5897 use std::{cell::Cell, rc::Rc};
5898
5899 struct Panel {
5900 input: Entity<crate::input::InputState>,
5901 renders: Rc<Cell<usize>>,
5902 }
5903 impl Render for Panel {
5904 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
5905 self.renders.set(self.renders.get() + 1);
5906 div().size_full().child(self.input.clone())
5907 }
5908 }
5909 struct Root(Entity<Panel>);
5910 impl Render for Root {
5911 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
5912 div().size_full().child(
5913 self.0
5914 .clone()
5915 .cached(gpui::StyleRefinement::default().size_full()),
5916 )
5917 }
5918 }
5919
5920 cx.update(crate::init);
5921 let renders = Rc::new(Cell::new(0));
5922 let mut input = None;
5923 let window = cx.open_window(size(px(400.), px(100.)), |window, cx| {
5924 let state = cx.new(|cx| crate::input::InputState::new(window, cx));
5925 input = Some(state.clone());
5926 Root(cx.new(|_| Panel {
5927 input: state,
5928 renders: renders.clone(),
5929 }))
5930 });
5931 cx.run_until_parked();
5932 let before = renders.get();
5933 assert!(before > 0);
5934 for _ in 0..60 {
5935 window.update(cx, |_, _, cx| cx.notify()).unwrap();
5936 }
5937 assert_eq!(
5938 renders.get(),
5939 before,
5940 "an idle input must not invalidate its cached parent"
5941 );
5942
5943 let input = input.unwrap();
5944 window
5945 .update(cx, |_, window, cx| {
5946 input.update(cx, |state, cx| state.set_value("changed", window, cx));
5947 })
5948 .unwrap();
5949 assert!(
5950 renders.get() > before,
5951 "text changes must invalidate the parent"
5952 );
5953 window.update(cx, |_, _, cx| cx.notify()).unwrap();
5955 let before = renders.get();
5956 for _ in 0..60 {
5957 window.update(cx, |_, _, cx| cx.notify()).unwrap();
5958 }
5959 assert_eq!(renders.get(), before, "the edited input must settle again");
5960
5961 cx.simulate_window_resize(window.into(), size(px(240.), px(100.)));
5962 cx.run_until_parked();
5963 assert!(renders.get() > before, "resizing must invalidate layout");
5964 }
5965
5966 #[gpui::test]
5967 fn textarea_cursor_treats_crlf_as_one_newline(cx: &mut TestAppContext) {
5968 cx.update(crate::init);
5969 let view = InputView::build_textarea(cx, |state| state);
5970 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
5971 cx.update(|window, cx| {
5972 view.input.update(cx, |state, cx| {
5973 for original in ["\r\nlast", "\u{feff}first\r\nlast\n", "first\nlast\r\n"] {
5974 state.set_value(original, window, cx);
5975 let newline = original.find('\n').unwrap();
5976 let end = if original.as_bytes()[newline.saturating_sub(1)] == b'\r' {
5977 newline - 1
5978 } else {
5979 newline
5980 };
5981 state.end(&MoveEnd, window, cx);
5982 assert_eq!(state.cursor(), end);
5983 state.right(&MoveRight, window, cx);
5984 assert_eq!(state.cursor(), newline + 1);
5985 state.left(&MoveLeft, window, cx);
5986 assert_eq!(state.cursor(), end);
5987 state.select_to_end_of_line(&SelectToEndOfLine, window, cx);
5988 assert_eq!(state.selected_range(), end..end);
5989 state.move_to_end(&MoveToEnd, window, cx);
5990 assert_eq!(state.cursor(), original.len());
5991 state.move_to_start(&MoveToStart, window, cx);
5992 assert_eq!(state.cursor(), 0);
5993 assert_eq!(state.value().as_bytes(), original.as_bytes());
5994 }
5995 state.set_value("a\rb", window, cx);
5997 state.right(&MoveRight, window, cx);
5998 assert_eq!(state.cursor(), 1);
5999 state.right(&MoveRight, window, cx);
6000 assert_eq!(state.cursor(), 2);
6001 })
6002 });
6003 }
6004
6005 #[gpui::test]
6006 fn only_a_multi_line_input_paints_scrollbars(cx: &mut TestAppContext) {
6007 cx.update(crate::init);
6008
6009 let single = InputView::build(cx, |state| state);
6013 single
6014 .input
6015 .update(cx, |state, _| assert!(!state.shows_scrollbar()));
6016
6017 let multi = InputView::build_textarea(cx, |state| state);
6018 multi
6019 .input
6020 .update(cx, |state, _| assert!(state.shows_scrollbar()));
6021 }
6022
6023 #[gpui::test]
6024 fn context_menu_handler_is_deferred_and_respects_disabled(cx: &mut TestAppContext) {
6025 use std::{cell::Cell, rc::Rc};
6026 cx.update(crate::init);
6027 let input_view = InputView::new(cx);
6028 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6029 let input = input_view.input;
6030 let calls = Rc::new(Cell::new(0usize));
6031 let items = Rc::new(Cell::new(0usize));
6032 let menu_focus = cx.update(|_, cx| cx.focus_handle());
6033
6034 cx.update(|window, cx| {
6035 input.update(cx, |state, cx| {
6036 let calls2 = calls.clone();
6037 let items2 = items.clone();
6038 let menu_focus = menu_focus.clone();
6039 state.on_context_menu(Rc::new(move |menu, _, _, window, cx| {
6040 calls2.set(calls2.get() + 1);
6041 items2.set(menu.items.len());
6042 menu_focus.focus(window, cx);
6043 }));
6044 state.handle_right_click_menu(point(px(0.), px(0.)), 0, window, cx);
6045 })
6046 });
6047 assert_eq!(calls.get(), 1);
6048 assert_eq!(items.get(), 0);
6049 input.read_with(&cx, |state, _| {
6050 assert_eq!(
6051 state
6052 .selection_focus
6053 .as_ref()
6054 .and_then(WeakFocusHandle::upgrade)
6055 .as_ref(),
6056 Some(&menu_focus),
6057 "the menu callback must retain selection without reborrowing its input"
6058 );
6059 });
6060
6061 cx.update(|window, cx| {
6063 input.update(cx, |state, cx| {
6064 state.handle_right_click_menu(point(px(0.), px(0.)), 0, window, cx);
6065 });
6066 });
6067 assert_eq!(calls.get(), 2);
6068 cx.update(|window, cx| {
6069 assert!(input.read(cx).has_selection_focus(window, cx));
6070 });
6071
6072 cx.update(|window, cx| {
6073 input.update(cx, |state, cx| {
6074 state.disabled = true;
6075 state.handle_right_click_menu(point(px(0.), px(0.)), 0, window, cx);
6076 })
6077 });
6078 assert_eq!(calls.get(), 2);
6079 }
6080
6081 #[gpui::test]
6082 fn context_menu_handler_can_read_its_input(cx: &mut TestAppContext) {
6083 use std::{cell::Cell, rc::Rc};
6084 cx.update(crate::init);
6085 let input_view = InputView::new(cx);
6086 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6087 let input = input_view.input;
6088 let cursor = Rc::new(Cell::new(None));
6089
6090 cx.update(|window, cx| {
6091 input.update(cx, |state, cx| {
6092 state.set_value("hello", window, cx);
6093 let weak = cx.weak_entity();
6094 let cursor = cursor.clone();
6095 state.on_context_menu(Rc::new(move |_, _, _, _, cx| {
6096 cursor.set(weak.upgrade().map(|input| input.read(cx).cursor()));
6097 }));
6098 state.handle_right_click_menu(point(px(0.), px(0.)), 2, window, cx);
6099 })
6100 });
6101 assert_eq!(cursor.get(), Some(2));
6102 }
6103
6104 #[gpui::test]
6105 fn test_readonly_rejects_user_edits_only(cx: &mut TestAppContext) {
6106 let input_view = InputView::new(cx);
6107 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6108 let input = input_view.input;
6109
6110 cx.update(|window, cx| {
6111 input.update(cx, |state, cx| {
6112 state.set_value("hello", window, cx);
6113 state.set_readonly(true, cx);
6114 });
6115 });
6116
6117 cx.update(|_, cx| {
6118 input.read_with(cx, |state, _| {
6119 assert!(!state.is_editable());
6120 assert!(!state.is_replaceable());
6121 });
6122 });
6123
6124 cx.update(|window, cx| {
6126 input.update(cx, |state, cx| {
6127 state.replace_text_in_range(None, " world", window, cx);
6128 state.replace_and_mark_text_in_range(None, "あ", None, window, cx);
6129 });
6130 });
6131 cx.update(|_, cx| {
6132 input.read_with(cx, |state, _| assert_eq!(state.value(), "hello"));
6133 });
6134
6135 cx.update(|window, cx| {
6137 input.update(cx, |state, cx| {
6138 state.insert(" world", window, cx);
6139 state.set_value("changed", window, cx);
6140 });
6141 });
6142 cx.update(|_, cx| {
6143 input.read_with(cx, |state, _| assert_eq!(state.value(), "changed"));
6144 });
6145
6146 cx.update(|window, cx| {
6149 input.update(cx, |state, cx| {
6150 state.set_readonly(false, cx);
6151 state.replace_text_in_range(None, "!", window, cx);
6152 });
6153 });
6154 cx.update(|_, cx| {
6155 input.read_with(cx, |state, _| {
6156 assert!(state.is_editable());
6157 assert_eq!(state.value(), "!changed");
6158 });
6159 });
6160 }
6161
6162 #[gpui::test]
6167 fn test_edit_reveals_far_offscreen_caret(cx: &mut TestAppContext) {
6168 let mut input = None;
6169 let window = cx.open_window(size(px(720.), px(400.)), |window, cx| {
6170 cx.set_global(Theme::default());
6171 super::super::init(cx);
6172 let state = cx.new(|cx| crate::input::TextareaState::new(window, cx).auto_grow(1, 6));
6173 input = Some(state.clone());
6174 TestRoot(state)
6175 });
6176 let mut cx = VisualTestContext::from_window(window.into(), cx);
6177 let input = input.unwrap();
6178
6179 let text: String = (1..=100)
6180 .map(|i| format!("line {i}"))
6181 .collect::<Vec<_>>()
6182 .join("\n");
6183 cx.update(|window, cx| {
6184 input.update(cx, |state, cx| {
6185 state.set_value(text, window, cx);
6186 let end = state.text.len();
6187 state.set_selection(end, end);
6188 });
6189 });
6190 cx.run_until_parked();
6191
6192 cx.update(|_, cx| {
6194 input.update(cx, |state, cx| {
6195 state.scroll_handle.set_offset(point(px(0.), px(0.)));
6196 cx.notify();
6197 });
6198 });
6199 cx.run_until_parked();
6200
6201 cx.update(|window, cx| {
6202 input.update(cx, |state, cx| {
6203 state.replace_text_in_range(None, "X", window, cx);
6204 });
6205 });
6206 cx.run_until_parked();
6207
6208 cx.update(|_, cx| {
6209 input.read_with(cx, |state, _| {
6210 assert!(state.value().ends_with("line 100X"));
6211 let (caret, _) = state.cursor_layout().expect("caret painted");
6212 let viewport = state.input_bounds();
6213 let scroll_y = state.scroll_handle.offset().y;
6214 let top = caret.origin.y - viewport.origin.y + scroll_y;
6215 assert!(
6216 top >= px(0.) && top + caret.size.height <= viewport.size.height,
6217 "caret top {top:?} outside viewport height {:?} (scroll {scroll_y:?})",
6218 viewport.size.height,
6219 );
6220 });
6221 });
6222 }
6223
6224 #[gpui::test]
6228 fn test_set_auto_grow_keeps_rows_fitted(cx: &mut TestAppContext) {
6229 let mut input = None;
6230 let window = cx.open_window(size(px(720.), px(400.)), |window, cx| {
6231 cx.set_global(Theme::default());
6232 super::super::init(cx);
6233 let state = cx.new(|cx| crate::input::TextareaState::new(window, cx).auto_grow(1, 10));
6234 input = Some(state.clone());
6235 TestRoot(state)
6236 });
6237 let mut cx = VisualTestContext::from_window(window.into(), cx);
6238 let input = input.unwrap();
6239
6240 let text: String = (1..=8)
6241 .map(|i| format!("line {i}"))
6242 .collect::<Vec<_>>()
6243 .join("\n");
6244 cx.update(|window, cx| {
6245 input.update(cx, |state, cx| state.set_value(text, window, cx));
6246 });
6247 cx.run_until_parked();
6248 cx.update(|_, cx| input.read_with(cx, |state, _| assert_eq!(state.mode.rows(), 8)));
6249
6250 cx.update(|_, cx| input.update(cx, |state, cx| state.set_auto_grow(1, 12, cx)));
6252 cx.run_until_parked();
6253 cx.update(|_, cx| input.read_with(cx, |state, _| assert_eq!(state.mode.rows(), 8)));
6254
6255 cx.update(|_, cx| input.update(cx, |state, cx| state.set_auto_grow(1, 4, cx)));
6257 cx.run_until_parked();
6258 cx.update(|_, cx| input.read_with(cx, |state, _| assert_eq!(state.mode.rows(), 4)));
6259 }
6260
6261 #[gpui::test]
6267 fn test_scroll_to_eob_does_not_overshoot_safe_range(cx: &mut TestAppContext) {
6268 let input_view = InputView::new(cx);
6269 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6270 let input = input_view.input;
6271
6272 cx.update(|window, cx| {
6274 input.update(cx, |state, cx| {
6275 state.set_scroll_beyond_last_line(Some(1), window, cx);
6276 state.set_cursor_surrounding_lines(Some(1), window, cx);
6277 let text: String = (1..=50)
6278 .map(|i| format!("line {i}"))
6279 .collect::<Vec<_>>()
6280 .join("\n");
6281 state.set_value(text, window, cx);
6282 });
6283 });
6284 cx.run_until_parked();
6285
6286 cx.update(|_, cx| {
6289 input.read_with(cx, |state, _| {
6290 assert!(
6291 state.scroll_size.height > px(0.),
6292 "scroll_size not populated by initial paint"
6293 );
6294 assert!(
6295 state.input_bounds.size.height > px(0.),
6296 "input_bounds not populated by initial paint"
6297 );
6298 });
6299 });
6300
6301 cx.update(|_, cx| {
6306 input.update(cx, |state, cx| {
6307 let end = state.text.len();
6308 state.move_to(end, Some(MoveDirection::Down), cx);
6309
6310 let deferred = state
6311 .deferred_scroll_offset
6312 .expect("scroll_to should populate deferred_scroll_offset");
6313 let safe_y_min =
6314 (-state.scroll_size.height + state.input_bounds.size.height).min(px(0.));
6315
6316 assert!(
6317 deferred.y >= safe_y_min,
6318 "deferred_scroll_offset.y = {:?} below safe_y_min = {:?} \
6319 — paint would jitter (Bug C regression)",
6320 deferred.y,
6321 safe_y_min,
6322 );
6323 });
6324 });
6325 }
6326
6327 #[gpui::test]
6328 fn test_next_search_match_reveals_with_padding_after_manual_scroll(cx: &mut TestAppContext) {
6329 assert_search_reveals_with_padding_after_manual_scroll(false, cx);
6330 }
6331
6332 #[gpui::test]
6333 fn test_previous_search_match_reveals_with_padding_after_manual_scroll(
6334 cx: &mut TestAppContext,
6335 ) {
6336 assert_search_reveals_with_padding_after_manual_scroll(true, cx);
6337 }
6338
6339 fn assert_search_reveals_with_padding_after_manual_scroll(
6340 previous: bool,
6341 cx: &mut TestAppContext,
6342 ) {
6343 let input_view = InputView::new(cx);
6344 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6345 let input = input_view.input;
6346 let text = (0..160)
6347 .map(|row| {
6348 if matches!(row, 20 | 60 | 100) {
6349 format!("match on row {row}")
6350 } else {
6351 format!("line {row}")
6352 }
6353 })
6354 .collect::<Vec<_>>()
6355 .join("\n");
6356 let start = text.match_indices("match").nth(1).unwrap().0;
6357 let expected_match = start..start + "match".len();
6358 cx.update(|window, cx| {
6359 input.update(cx, |state, cx| {
6360 state.set_cursor_surrounding_lines(Some(3), window, cx);
6361 state.set_value(text, window, cx);
6362 state.set_search_query("match", true, cx);
6363 if previous {
6364 state.search_session.matcher.next();
6365 state.search_session.matcher.next();
6366 }
6367 });
6368 });
6369 cx.run_until_parked();
6370 cx.update(|_, cx| {
6371 input.update(cx, |state, cx| {
6372 let line_height = state.last_layout.as_ref().unwrap().line_height;
6373 let y = if previous { px(0.) } else { -line_height * 80. };
6374 state.set_scroll_offset(point(px(0.), y), cx);
6375 });
6376 });
6377 cx.run_until_parked();
6378 input.read_with(&cx, |state, _| {
6379 let visible = state.visible_row_range().unwrap();
6380 if previous {
6381 assert!(visible.end <= 60, "target must be below the viewport");
6382 } else {
6383 assert!(visible.start > 60, "target must be above the viewport");
6384 }
6385 });
6386 cx.update(|_, cx| {
6387 input.update(cx, |state, cx| {
6388 let range = if previous {
6389 state.previous_search_match(cx)
6390 } else {
6391 state.next_search_match(cx)
6392 };
6393 assert_eq!(range, Some(expected_match));
6394 assert_eq!(state.search_session.matcher.label(), "2/3");
6395 });
6396 });
6397 cx.run_until_parked();
6398 input.read_with(&cx, |state, _| {
6399 assert!(state.visible_row_range().unwrap().contains(&60));
6400 let line_height = state.last_layout.as_ref().unwrap().line_height;
6401 let target_y = line_height * 60. + state.scroll_handle.offset().y;
6402 assert!(target_y >= line_height * 2. - px(0.1));
6404 assert!(
6405 target_y + line_height * 3.
6406 <= state.last_bounds.as_ref().unwrap().size.height + px(0.1),
6407 "search must preserve the configured surrounding-line padding"
6408 );
6409 });
6410 }
6411
6412 struct SearchHost {
6414 editor: Entity<InputBaseState<EditorMode>>,
6415 search_requests: Rc<Cell<usize>>,
6416 }
6417
6418 impl Render for SearchHost {
6419 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
6420 let search_requests = self.search_requests.clone();
6421 div()
6422 .size_full()
6423 .on_action(cx.listener(move |_, _: &Search, _, _| {
6424 search_requests.set(search_requests.get() + 1);
6425 }))
6426 .child(self.editor.clone())
6427 }
6428 }
6429
6430 fn press_search_shortcut(
6433 cx: &mut TestAppContext,
6434 searchable: bool,
6435 ) -> (Entity<InputBaseState<EditorMode>>, Rc<Cell<usize>>) {
6436 let search_requests = Rc::new(Cell::new(0));
6437 let mut editor = None;
6438 let window = cx.update(|cx| {
6439 cx.open_window(Default::default(), |window, cx| {
6440 cx.set_global(Theme::default());
6441 super::super::init(cx);
6442 let state =
6443 cx.new(|cx| crate::input::EditorState::new(window, cx).searchable(searchable));
6444 editor = Some(state.clone());
6445 cx.new(|_| SearchHost {
6446 editor: state,
6447 search_requests: search_requests.clone(),
6448 })
6449 })
6450 .unwrap()
6451 });
6452 let editor = editor.unwrap();
6453 let mut cx = VisualTestContext::from_window(window.into(), cx);
6454 cx.update(|window, cx| {
6455 editor.update(cx, |state, cx| state.focus(window, cx));
6456 });
6457 cx.run_until_parked();
6458 #[cfg(target_os = "macos")]
6459 cx.simulate_keystrokes("cmd-f");
6460 #[cfg(not(target_os = "macos"))]
6461 cx.simulate_keystrokes("ctrl-f");
6462 cx.run_until_parked();
6463 (editor, search_requests)
6464 }
6465
6466 #[gpui::test]
6467 fn test_search_shortcut_reaches_the_host_when_not_searchable(cx: &mut TestAppContext) {
6468 let (editor, search_requests) = press_search_shortcut(cx, false);
6469 assert_eq!(search_requests.get(), 1);
6470 editor.read_with(cx, |state, _| {
6471 assert!(!state.search_session().open);
6472 assert!(!state.search_session().is_active());
6473 });
6474 }
6475
6476 #[gpui::test]
6477 fn test_search_shortcut_opens_the_panel_when_searchable(cx: &mut TestAppContext) {
6478 let (editor, search_requests) = press_search_shortcut(cx, true);
6479 assert_eq!(search_requests.get(), 0);
6480 editor.read_with(cx, |state, _| {
6481 assert!(state.search_session().open);
6482 assert!(state.search_session().is_active());
6483 });
6484 }
6485
6486 #[gpui::test]
6487 fn test_set_search_query_highlights_without_the_panel(cx: &mut TestAppContext) {
6488 let input_view = InputView::build_editor(cx, |state| state.searchable(false));
6489 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6490 let input = input_view.input;
6491 cx.update(|window, cx| {
6492 input.update(cx, |state, cx| {
6493 state.set_value("foo bar foo", window, cx);
6494 state.set_search_query("foo", true, cx);
6495 });
6496 });
6497 cx.run_until_parked();
6498 input.read_with(&cx, |state, _| {
6499 let session = state.search_session();
6500 assert!(session.is_active());
6501 assert!(!session.open);
6502 assert_eq!(session.matcher.len(), 2);
6503 });
6504 cx.update(|_, cx| {
6505 input.update(cx, |state, cx| state.close_search(cx));
6506 });
6507 input.read_with(&cx, |state, _| {
6508 assert!(!state.search_session().is_active());
6509 });
6510 }
6511
6512 #[gpui::test]
6515 fn test_closed_search_resyncs_matches_after_edits(cx: &mut TestAppContext) {
6516 let input_view = InputView::build_editor(cx, |state| state.searchable(false));
6517 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6518 let input = input_view.input;
6519 cx.update(|window, cx| {
6520 input.update(cx, |state, cx| {
6521 state.set_value("foo bar foo", window, cx);
6522 state.set_search_query("foo", true, cx);
6523 state.close_search(cx);
6524
6525 state.set_value("bar foo", window, cx);
6526 assert_eq!(state.next_search_match(cx), Some(4..7));
6527
6528 state.set_value("foo foo foo", window, cx);
6529 state.set_search_query("foo", true, cx);
6530 assert_eq!(state.search_session().matcher.len(), 3);
6531 });
6532 });
6533 }
6534
6535 #[gpui::test]
6536 fn test_search_reveals_offscreen_wrapped_match(cx: &mut TestAppContext) {
6537 let input_view = InputView::new(cx);
6538 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6539 let input = input_view.input;
6540 let text = format!(
6541 "match\n{}\n{}match\n{}",
6542 "line\n".repeat(80),
6543 "wrapped text ".repeat(500),
6544 "line\n".repeat(80)
6545 );
6546 cx.update(|window, cx| {
6547 input.update(cx, |state, cx| {
6548 state.set_cursor_surrounding_lines(Some(3), window, cx);
6549 state.set_value(text, window, cx);
6550 state.set_search_query("match", true, cx);
6551 });
6552 });
6553 cx.run_until_parked();
6554 cx.update(|_, cx| {
6555 input.update(cx, |state, cx| {
6556 state.next_search_match(cx).unwrap();
6557 });
6558 });
6559 cx.run_until_parked();
6560 input.read_with(&cx, |state, _| {
6561 let range = state.search_session.matcher.matched_ranges()[1].clone();
6562 let layout = state.last_layout.as_ref().unwrap();
6563 let (_, _, position) = state.line_and_position_for_offset(range.end);
6564 let y = position.expect("wrapped match must be laid out").y
6565 + state.scroll_handle.offset().y;
6566 assert!(y >= layout.line_height * 2. - px(0.1));
6567 assert!(
6568 y + layout.line_height * 3. <= state.last_bounds.unwrap().size.height + px(0.1),
6569 "wrapped match must retain surrounding display rows"
6570 );
6571 });
6572 }
6573
6574 #[gpui::test]
6575 fn test_number_step(cx: &mut TestAppContext) {
6576 let input = InputView::build(cx, |state| state).input;
6577
6578 cx.update(|cx| {
6579 input.update(cx, |_state, cx| {
6580 assert_eq!(
6581 NumberStep::from(5.).value(123., StepAction::Increment, cx),
6582 5.
6583 );
6584
6585 let step = NumberStep::by_value(|value, action, _cx| {
6588 let below = match action {
6589 StepAction::Increment => value < 1.0,
6590 StepAction::Decrement => value <= 1.0,
6591 };
6592 if below { 0.1 } else { 0.5 }
6593 });
6594 assert_eq!(step.value(0.5, StepAction::Increment, cx), 0.1);
6595 assert_eq!(step.value(1.0, StepAction::Increment, cx), 0.5);
6596 assert_eq!(step.value(1.0, StepAction::Decrement, cx), 0.1);
6597 assert_eq!(step.value(2.0, StepAction::Decrement, cx), 0.5);
6598 });
6599 });
6600 }
6601
6602 #[gpui::test]
6603 fn test_number_input_normalization(cx: &mut TestAppContext) {
6604 let input_view = InputView::build(cx, |state| {
6605 state.mask_pattern(MaskPattern::Number {
6606 separator: None,
6607 fraction: None,
6608 })
6609 });
6610 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6611 let input = input_view.input;
6612
6613 cx.update(|window, cx| {
6617 input.update(cx, |state, cx| {
6618 state.replace_text_in_range(None, "12。5", window, cx);
6619 });
6620 });
6621 cx.run_until_parked();
6622 cx.update(|_, cx| {
6623 input.read_with(cx, |state, _| {
6624 assert_eq!(state.value(), "12.5");
6625 let cursor: Range<usize> = state.selected_range();
6626 assert_eq!(cursor, 4..4);
6627 });
6628 });
6629
6630 cx.update(|window, cx| {
6632 input.update(cx, |state, cx| {
6633 state.replace_text_in_range(None, "abc", window, cx);
6634 });
6635 });
6636 cx.run_until_parked();
6637 cx.update(|_, cx| {
6638 input.read_with(cx, |state, _| {
6639 assert_eq!(state.value(), "12.5");
6640 });
6641 });
6642
6643 cx.update(|window, cx| {
6646 input.update(cx, |state, cx| {
6647 let range = state.range_to_utf16(&(0..state.text.len()));
6648 state.replace_text_in_range(Some(range), "。", window, cx);
6649 });
6650 });
6651 cx.run_until_parked();
6652 cx.update(|_, cx| {
6653 input.read_with(cx, |state, _| {
6654 assert_eq!(state.value(), ".");
6655 let cursor: Range<usize> = state.selected_range();
6656 assert_eq!(cursor, 1..1);
6657 });
6658 });
6659 }
6660
6661 #[gpui::test]
6662 fn test_mask_pattern_accepts_non_ascii_text(cx: &mut TestAppContext) {
6663 let input_view = InputView::build(cx, |state| state.mask_pattern("9999年99月"));
6664 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6665 let input = input_view.input;
6666
6667 cx.update(|window, cx| {
6668 input.update(cx, |state, cx| {
6669 state.replace_text_in_range(None, "2024年12月", window, cx);
6670 });
6671 });
6672 cx.run_until_parked();
6673 cx.update(|_, cx| {
6674 input.read_with(cx, |state, _| {
6675 assert_eq!(state.value(), "2024年12月");
6676 });
6677 });
6678 }
6679
6680 #[gpui::test]
6681 fn test_number_input_normalization_with_separator(cx: &mut TestAppContext) {
6682 let input_view = InputView::build(cx, |state| {
6683 state.mask_pattern(MaskPattern::Number {
6684 separator: Some(','),
6685 fraction: Some(2),
6686 })
6687 });
6688 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6689 let input = input_view.input;
6690
6691 cx.update(|window, cx| {
6692 input.update(cx, |state, cx| {
6693 state.replace_text_in_range(None, "1234", window, cx);
6694 });
6695 });
6696 cx.run_until_parked();
6697 cx.update(|_, cx| {
6698 input.read_with(cx, |state, _| {
6699 assert_eq!(state.value(), "1,234");
6700 assert_eq!(state.unmask_value(), "1234");
6701 });
6702 });
6703 }
6704
6705 #[gpui::test]
6706 fn test_number_input_clamp_on_blur(cx: &mut TestAppContext) {
6707 let input_view = InputView::build(cx, |state| {
6708 state
6709 .mask_pattern(MaskPattern::Number {
6710 separator: None,
6711 fraction: None,
6712 })
6713 .min(10.)
6714 .max(100.)
6715 });
6716 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6717 let input = input_view.input;
6718
6719 cx.update(|window, cx| {
6721 input.update(cx, |state, cx| {
6722 state.replace_text_in_range(None, "1000", window, cx);
6723 assert_eq!(state.value(), "1000");
6724 state.clamp_number_value(window, cx);
6725 assert_eq!(state.value(), "100");
6726
6727 let range = state.range_to_utf16(&(0..state.text.len()));
6728 state.replace_text_in_range(Some(range), "1", window, cx);
6729 assert_eq!(state.value(), "1");
6730 state.clamp_number_value(window, cx);
6731 assert_eq!(state.value(), "10");
6732 });
6733 });
6734 }
6735
6736 #[gpui::test]
6737 fn test_number_input_undo_with_mask(cx: &mut TestAppContext) {
6738 let input_view = InputView::build(cx, |state| {
6739 state.mask_pattern(MaskPattern::Number {
6740 separator: Some(','),
6741 fraction: None,
6742 })
6743 });
6744 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6745 let input = input_view.input;
6746
6747 cx.update(|window, cx| {
6750 input.update(cx, |state, cx| {
6751 state.replace_text_in_range(None, "1234", window, cx);
6752 assert_eq!(state.value(), "1,234");
6753 state.replace_text_in_range(None, "5", window, cx);
6754 assert_eq!(state.value(), "12,345");
6755
6756 state.undo(&Undo, window, cx);
6760 assert_eq!(state.value(), "1,234");
6761 state.undo(&Undo, window, cx);
6762 assert_eq!(state.value(), "");
6763 state.redo(&Redo, window, cx);
6764 assert_eq!(state.value(), "1,234");
6765 state.redo(&Redo, window, cx);
6766 assert_eq!(state.value(), "12,345");
6767 });
6768 });
6769 }
6770
6771 #[gpui::test]
6772 fn test_undo_manager_coalesces_adjacent_typing_transactions(cx: &mut TestAppContext) {
6773 let input_view = InputView::build(cx, |state| state);
6774 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6775 let input = input_view.input;
6776
6777 cx.update(|window, cx| {
6778 input.update(cx, |state, cx| {
6779 state.replace_text_in_range(None, "a", window, cx);
6780 state.replace_text_in_range(None, "b", window, cx);
6781 assert_eq!(state.value(), "ab");
6782
6783 state.undo(&Undo, window, cx);
6784 assert_eq!(state.value(), "");
6785 });
6786 });
6787 }
6788
6789 #[gpui::test]
6790 fn test_undo_manager_cursor_movement_splits_typing(cx: &mut TestAppContext) {
6791 let input_view = InputView::build(cx, |state| state);
6792 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6793 let input = input_view.input;
6794
6795 cx.update(|window, cx| {
6796 input.update(cx, |state, cx| {
6797 state.replace_text_in_range(None, "a", window, cx);
6798 state.replace_text_in_range(None, "b", window, cx);
6799 state.left(&MoveLeft, window, cx);
6800 state.replace_text_in_range(None, "x", window, cx);
6801 assert_eq!(state.value(), "axb");
6802
6803 state.undo(&Undo, window, cx);
6804 assert_eq!(state.value(), "ab");
6805 state.undo(&Undo, window, cx);
6806 assert_eq!(state.value(), "");
6807 });
6808 });
6809 }
6810
6811 #[gpui::test]
6812 fn test_undo_manager_splits_backward_and_forward_delete(cx: &mut TestAppContext) {
6813 let input_view = InputView::build(cx, |state| state);
6814 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6815 let input = input_view.input;
6816
6817 cx.update(|window, cx| {
6818 input.update(cx, |state, cx| {
6819 state.set_value("abcd", window, cx);
6820 state.set_selected_range(2..2, cx);
6821 state.backspace(&Backspace, window, cx);
6822 state.delete(&Delete, window, cx);
6823 assert_eq!(state.value(), "ad");
6824
6825 state.undo(&Undo, window, cx);
6826 assert_eq!(state.value(), "acd");
6827 state.undo(&Undo, window, cx);
6828 assert_eq!(state.value(), "abcd");
6829 });
6830 });
6831 }
6832
6833 #[gpui::test]
6834 fn test_undo_manager_coalesces_directional_character_deletes(cx: &mut TestAppContext) {
6835 let input_view = InputView::build(cx, |state| state);
6836 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6837 let input = input_view.input;
6838
6839 cx.update(|window, cx| {
6840 input.update(cx, |state, cx| {
6841 state.set_value("abcd", window, cx);
6842 state.backspace(&Backspace, window, cx);
6843 state.backspace(&Backspace, window, cx);
6844 assert_eq!(state.value(), "ab");
6845 state.undo(&Undo, window, cx);
6846 assert_eq!(state.value(), "abcd");
6847 assert_eq!(state.selected_range(), 4..4);
6848
6849 state.set_value("abcd", window, cx);
6850 state.set_selected_range(1..1, cx);
6851 state.delete(&Delete, window, cx);
6852 state.delete(&Delete, window, cx);
6853 assert_eq!(state.value(), "ad");
6854 state.undo(&Undo, window, cx);
6855 assert_eq!(state.value(), "abcd");
6856 assert_eq!(state.selected_range(), 1..1);
6857 });
6858 });
6859 }
6860
6861 #[gpui::test]
6862 fn test_undo_manager_atomic_paste_isolated_from_typing(cx: &mut TestAppContext) {
6863 let input_view = InputView::build(cx, |state| state);
6864 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6865 let input = input_view.input;
6866
6867 cx.update(|window, cx| {
6868 cx.write_to_clipboard(ClipboardItem::new_string("P".to_string()));
6869 input.update(cx, |state, cx| {
6870 state.replace_text_in_range(None, "a", window, cx);
6871 state.paste(&Paste, window, cx);
6872 state.replace_text_in_range(None, "b", window, cx);
6873 assert_eq!(state.value(), "aPb");
6874
6875 state.undo(&Undo, window, cx);
6876 assert_eq!(state.value(), "aP");
6877 state.undo(&Undo, window, cx);
6878 assert_eq!(state.value(), "a");
6879 state.undo(&Undo, window, cx);
6880 assert_eq!(state.value(), "");
6881 });
6882 });
6883 }
6884
6885 #[gpui::test]
6886 fn test_undo_manager_programmatic_insert_is_atomic(cx: &mut TestAppContext) {
6887 let input_view = InputView::build(cx, |state| state);
6888 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6889 let input = input_view.input;
6890
6891 cx.update(|window, cx| {
6892 input.update(cx, |state, cx| {
6893 state.replace_text_in_range(None, "a", window, cx);
6894 state.insert("P", window, cx);
6895 state.replace_text_in_range(None, "b", window, cx);
6896 assert_eq!(state.value(), "aPb");
6897
6898 state.undo(&Undo, window, cx);
6899 assert_eq!(state.value(), "aP");
6900 state.undo(&Undo, window, cx);
6901 assert_eq!(state.value(), "a");
6902 });
6903 });
6904 }
6905
6906 #[gpui::test]
6907 fn test_undo_manager_selection_round_trip_splits_typing(cx: &mut TestAppContext) {
6908 let input_view = InputView::build(cx, |state| state);
6909 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6910 let input = input_view.input;
6911
6912 cx.update(|window, cx| {
6913 input.update(cx, |state, cx| {
6914 state.replace_text_in_range(None, "a", window, cx);
6915 state.replace_text_in_range(None, "b", window, cx);
6916 state.select_all(window, cx);
6917 state.unselect(window, cx);
6918 state.replace_text_in_range(None, "c", window, cx);
6919
6920 state.undo(&Undo, window, cx);
6921 assert_eq!(state.value(), "ab");
6922 state.undo(&Undo, window, cx);
6923 assert_eq!(state.value(), "");
6924 });
6925 });
6926 }
6927
6928 #[gpui::test]
6929 fn test_undo_manager_enter_is_atomic(cx: &mut TestAppContext) {
6930 let input_view = InputView::build_textarea(cx, |state| state);
6931 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6932 let input = input_view.input;
6933
6934 cx.update(|window, cx| {
6935 input.update(cx, |state, cx| {
6936 state.replace_text_in_range(None, "a", window, cx);
6937 state.enter(
6938 &Enter {
6939 secondary: false,
6940 shift: false,
6941 },
6942 window,
6943 cx,
6944 );
6945 state.replace_text_in_range(None, "b", window, cx);
6946
6947 state.undo(&Undo, window, cx);
6948 assert_eq!(state.value(), "a\n");
6949 state.undo(&Undo, window, cx);
6950 assert_eq!(state.value(), "a");
6951 state.undo(&Undo, window, cx);
6952 assert_eq!(state.value(), "");
6953 });
6954 });
6955 }
6956
6957 #[gpui::test]
6958 fn test_undo_manager_single_line_return_commits_the_typing_session(cx: &mut TestAppContext) {
6959 let input_view = InputView::build(cx, |state| state);
6960 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6961 let input = input_view.input;
6962
6963 cx.update(|window, cx| {
6964 input.update(cx, |state, cx| {
6965 for part in ["a", "b", "c"] {
6966 state.replace_text_in_range(None, part, window, cx);
6967 }
6968 state.enter(
6969 &Enter {
6970 secondary: false,
6971 shift: false,
6972 },
6973 window,
6974 cx,
6975 );
6976 for part in ["d", "e", "f"] {
6977 state.replace_text_in_range(None, part, window, cx);
6978 }
6979 assert_eq!(state.value(), "abcdef");
6980
6981 state.undo(&Undo, window, cx);
6982 assert_eq!(state.value(), "abc");
6983 state.undo(&Undo, window, cx);
6984 assert_eq!(state.value(), "");
6985 });
6986 });
6987 }
6988
6989 #[gpui::test]
6990 fn test_undo_manager_submit_on_enter_commits_the_textarea_session(cx: &mut TestAppContext) {
6991 let input_view = InputView::build_textarea(cx, |state| state.submit_on_enter(true));
6992 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6993 let input = input_view.input;
6994
6995 cx.update(|window, cx| {
6996 input.update(cx, |state, cx| {
6997 state.replace_text_in_range(None, "before submit", window, cx);
6998 state.enter(
6999 &Enter {
7000 secondary: false,
7001 shift: false,
7002 },
7003 window,
7004 cx,
7005 );
7006 state.replace_text_in_range(None, " after submit", window, cx);
7007 assert_eq!(state.value(), "before submit after submit");
7008
7009 state.undo(&Undo, window, cx);
7010 assert_eq!(state.value(), "before submit");
7011 state.undo(&Undo, window, cx);
7012 assert_eq!(state.value(), "");
7013 });
7014 });
7015 }
7016
7017 #[gpui::test]
7018 fn test_undo_manager_blur_commits_the_typing_session(cx: &mut TestAppContext) {
7019 let input_view = InputView::build(cx, |state| state);
7020 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7021 let input = input_view.input;
7022
7023 cx.update(|window, cx| {
7024 input.update(cx, |state, cx| {
7025 state.replace_text_in_range(None, "before blur", window, cx);
7026 state.on_blur(window, cx);
7027 state.on_focus(window, cx);
7028 state.replace_text_in_range(None, " after focus", window, cx);
7029 assert_eq!(state.value(), "before blur after focus");
7030
7031 state.undo(&Undo, window, cx);
7032 assert_eq!(state.value(), "before blur");
7033 state.undo(&Undo, window, cx);
7034 assert_eq!(state.value(), "");
7035 });
7036 });
7037 }
7038
7039 #[gpui::test]
7040 fn test_undo_manager_keeps_rapid_lines_in_distinct_transactions(cx: &mut TestAppContext) {
7041 let input_view = InputView::build_textarea(cx, |state| state);
7042 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7043 let input = input_view.input;
7044
7045 cx.update(|window, cx| {
7046 input.update(cx, |state, cx| {
7047 let enter = Enter {
7048 secondary: false,
7049 shift: false,
7050 };
7051 state.replace_text_in_range(None, "a", window, cx);
7052 state.enter(&enter, window, cx);
7053 state.replace_text_in_range(None, "b", window, cx);
7054 state.enter(&enter, window, cx);
7055 state.replace_text_in_range(None, "c", window, cx);
7056 assert_eq!(state.value(), "a\nb\nc");
7057
7058 for expected in ["a\nb\n", "a\nb", "a\n", "a", ""] {
7059 state.undo(&Undo, window, cx);
7060 assert_eq!(state.value(), expected);
7061 }
7062 });
7063 });
7064 }
7065
7066 #[gpui::test]
7067 fn test_undo_manager_coalesces_long_unicode_typing_without_a_timer(cx: &mut TestAppContext) {
7068 let input_view = InputView::build_textarea(cx, |state| state);
7069 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7070 let input = input_view.input;
7071 let parts = [
7072 "The ",
7073 "quick ",
7074 "brown fox, ",
7075 "你好,世界 ",
7076 "🦀 jumps over 13 lazy dogs.",
7077 ];
7078 let expected = parts.concat();
7079
7080 cx.update(|window, cx| {
7081 input.update(cx, |state, cx| {
7082 for part in parts {
7083 state.replace_text_in_range(None, part, window, cx);
7084 }
7085 assert_eq!(state.value(), expected);
7086
7087 state.undo(&Undo, window, cx);
7088 assert_eq!(state.value(), "");
7089 state.redo(&Redo, window, cx);
7090 assert_eq!(state.value(), expected);
7091 });
7092 });
7093 }
7094
7095 #[gpui::test]
7096 fn test_undo_manager_long_multiline_sequence_has_structural_boundaries(
7097 cx: &mut TestAppContext,
7098 ) {
7099 let input_view = InputView::build_textarea(cx, |state| state);
7100 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7101 let input = input_view.input;
7102 let enter = Enter {
7103 secondary: false,
7104 shift: false,
7105 };
7106
7107 cx.update(|window, cx| {
7108 input.update(cx, |state, cx| {
7109 for (index, line) in [
7110 "first line with punctuation!",
7111 "第二行包含 Unicode 🦀",
7112 "third line has several words",
7113 ]
7114 .into_iter()
7115 .enumerate()
7116 {
7117 for chunk in line.split_inclusive(' ') {
7118 state.replace_text_in_range(None, chunk, window, cx);
7119 }
7120 if index < 2 {
7121 state.enter(&enter, window, cx);
7122 }
7123 }
7124
7125 assert_eq!(
7126 state.value(),
7127 "first line with punctuation!\n第二行包含 Unicode 🦀\nthird line has several words"
7128 );
7129 state.undo(&Undo, window, cx);
7130 assert_eq!(
7131 state.value(),
7132 "first line with punctuation!\n第二行包含 Unicode 🦀\n"
7133 );
7134 state.undo(&Undo, window, cx);
7135 assert_eq!(
7136 state.value(),
7137 "first line with punctuation!\n第二行包含 Unicode 🦀"
7138 );
7139 state.undo(&Undo, window, cx);
7140 assert_eq!(state.value(), "first line with punctuation!\n");
7141 });
7142 });
7143 }
7144
7145 #[gpui::test]
7146 fn test_masked_input_keeps_its_value_out_of_the_clipboard(cx: &mut TestAppContext) {
7147 let input_view = InputView::build(cx, |state| state);
7148 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7149 let input = input_view.input;
7150
7151 cx.update(|window, cx| {
7152 input.update(cx, |state, cx| {
7153 state.set_value("hunter2", window, cx);
7154 state.set_masked(true, window, cx);
7155 state.select_all(window, cx);
7156 cx.write_to_clipboard(ClipboardItem::new_string("sentinel".into()));
7157
7158 state.copy(&Copy, window, cx);
7159 assert_eq!(
7160 cx.read_from_clipboard().and_then(|item| item.text()),
7161 Some("sentinel".to_string())
7162 );
7163
7164 state.cut(&Cut, window, cx);
7166 assert_eq!(state.value(), "hunter2");
7167 assert_eq!(
7168 cx.read_from_clipboard().and_then(|item| item.text()),
7169 Some("sentinel".to_string())
7170 );
7171
7172 state.set_masked(false, window, cx);
7174 state.copy(&Copy, window, cx);
7175 assert_eq!(
7176 cx.read_from_clipboard().and_then(|item| item.text()),
7177 Some("hunter2".to_string())
7178 );
7179 });
7180 });
7181 }
7182
7183 #[gpui::test]
7184 fn test_masked_input_collapses_word_boundaries(cx: &mut TestAppContext) {
7185 let input_view = InputView::build(cx, |state| state);
7186 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7187 let input = input_view.input;
7188
7189 cx.update(|window, cx| {
7190 input.update(cx, |state, cx| {
7191 state.set_value("aaa bbb ccc", window, cx);
7192 state.set_masked(true, window, cx);
7193 state.set_selected_range(7..7, cx);
7194
7195 state.delete_previous_word(&DeleteToPreviousWordStart, window, cx);
7198 assert_eq!(state.value(), " ccc");
7199 assert_eq!(state.selected_range(), 0..0);
7200
7201 state.delete_next_word(&DeleteToNextWordEnd, window, cx);
7202 assert_eq!(state.value(), "");
7203
7204 state.set_value("aaa bbb ccc", window, cx);
7206 state.select_word(9, window, cx);
7207 assert_eq!(state.selected_range(), 0..11);
7208
7209 state.set_masked(false, window, cx);
7211 state.set_value("aaa bbb ccc", window, cx);
7212 state.set_selected_range(11..11, cx);
7213 state.delete_previous_word(&DeleteToPreviousWordStart, window, cx);
7214 assert_eq!(state.value(), "aaa bbb ");
7215
7216 state.set_value("aaa bbb ccc", window, cx);
7217 state.select_word(9, window, cx);
7218 assert_eq!(state.selected_range(), 8..11);
7219 });
7220 });
7221 }
7222
7223 #[gpui::test]
7224 fn test_masked_input_disables_the_copy_context_menu_items(cx: &mut TestAppContext) {
7225 let input_view = InputView::build(cx, |state| state);
7226 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7227 let input = input_view.input;
7228
7229 cx.update(|window, cx| {
7230 input.update(cx, |state, cx| {
7231 state.set_value("hunter2", window, cx);
7232 state.select_all(window, cx);
7233 assert!(state.context_menu_capabilities().is_copyable());
7234
7235 state.set_masked(true, window, cx);
7236 let capabilities = state.context_menu_capabilities();
7237 assert!(capabilities.is_masked());
7238 assert!(capabilities.has_selection());
7239 assert!(!capabilities.is_copyable());
7240 });
7241 });
7242 }
7243
7244 #[gpui::test]
7245 fn test_undo_manager_cut_and_repeated_pastes_are_distinct_transactions(
7246 cx: &mut TestAppContext,
7247 ) {
7248 let input_view = InputView::build(cx, |state| state);
7249 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7250 let input = input_view.input;
7251
7252 cx.update(|window, cx| {
7253 input.update(cx, |state, cx| {
7254 state.replace_text_in_range(None, "alpha beta gamma", window, cx);
7255 state.set_selected_range(6..10, cx);
7256 state.cut(&Cut, window, cx);
7257 assert_eq!(state.value(), "alpha gamma");
7258
7259 state.paste(&Paste, window, cx);
7260 state.paste(&Paste, window, cx);
7261 assert_eq!(state.value(), "alpha betabeta gamma");
7262
7263 state.undo(&Undo, window, cx);
7264 assert_eq!(state.value(), "alpha beta gamma");
7265 state.undo(&Undo, window, cx);
7266 assert_eq!(state.value(), "alpha gamma");
7267 state.undo(&Undo, window, cx);
7268 assert_eq!(state.value(), "alpha beta gamma");
7269 });
7270 });
7271 }
7272
7273 #[gpui::test]
7274 fn test_undo_manager_word_and_line_deletes_do_not_coalesce(cx: &mut TestAppContext) {
7275 let input_view = InputView::build_textarea(cx, |state| state);
7276 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7277 let input = input_view.input;
7278
7279 cx.update(|window, cx| {
7280 input.update(cx, |state, cx| {
7281 state.set_value("one two three\nfour five", window, cx);
7282 state.set_selected_range(13..13, cx);
7283 state.delete_previous_word(&DeleteToPreviousWordStart, window, cx);
7284 assert_eq!(state.value(), "one two \nfour five");
7285 state.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
7286 assert_eq!(state.value(), "one two four five");
7287
7288 state.undo(&Undo, window, cx);
7289 assert_eq!(state.value(), "one two \nfour five");
7290 state.undo(&Undo, window, cx);
7291 assert_eq!(state.value(), "one two three\nfour five");
7292 });
7293 });
7294 }
7295
7296 #[gpui::test]
7297 fn test_undo_manager_multiline_replacement_is_one_atomic_transaction(cx: &mut TestAppContext) {
7298 let input_view = InputView::build_textarea(cx, |state| state);
7299 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7300 let input = input_view.input;
7301
7302 cx.update(|window, cx| {
7303 input.update(cx, |state, cx| {
7304 state.replace_text_in_range(None, "before", window, cx);
7305 state.set_selected_range(0..6, cx);
7306 state.replace_text_in_range(None, "line one\nline two\n第三行", window, cx);
7307 assert_eq!(state.value(), "line one\nline two\n第三行");
7308
7309 state.undo(&Undo, window, cx);
7310 assert_eq!(state.value(), "before");
7311 assert_eq!(state.selected_range(), 0..6);
7312 state.redo(&Redo, window, cx);
7313 assert_eq!(state.value(), "line one\nline two\n第三行");
7314 });
7315 });
7316 }
7317
7318 #[gpui::test]
7319 fn test_undo_manager_composition_isolated_from_long_typing(cx: &mut TestAppContext) {
7320 let input_view = InputView::build(cx, |state| state);
7321 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7322 let input = input_view.input;
7323
7324 cx.update(|window, cx| {
7325 input.update(cx, |state, cx| {
7326 state.replace_text_in_range(None, "prefix ", window, cx);
7327 state.replace_and_mark_text_in_range(None, "n", None, window, cx);
7328 state.replace_and_mark_text_in_range(None, "ni", None, window, cx);
7329 state.replace_and_mark_text_in_range(None, "你", None, window, cx);
7330 state.unmark_text(window, cx);
7331 state.replace_text_in_range(None, " suffix", window, cx);
7332 assert_eq!(state.value(), "prefix 你 suffix");
7333
7334 state.undo(&Undo, window, cx);
7335 assert_eq!(state.value(), "prefix 你");
7336 state.undo(&Undo, window, cx);
7337 assert_eq!(state.value(), "prefix ");
7338 state.undo(&Undo, window, cx);
7339 assert_eq!(state.value(), "");
7340 });
7341 });
7342 }
7343
7344 #[gpui::test]
7345 fn test_undo_manager_selected_replacement_is_atomic(cx: &mut TestAppContext) {
7346 let input_view = InputView::build(cx, |state| state);
7347 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7348 let input = input_view.input;
7349
7350 cx.update(|window, cx| {
7351 input.update(cx, |state, cx| {
7352 state.replace_text_in_range(None, "abc", window, cx);
7353 state.set_selected_range(1..2, cx);
7354 state.replace_text_in_range(None, "X", window, cx);
7355 state.replace_text_in_range(None, "z", window, cx);
7356 assert_eq!(state.value(), "aXzc");
7357
7358 state.undo(&Undo, window, cx);
7359 assert_eq!(state.value(), "aXc");
7360 state.undo(&Undo, window, cx);
7361 assert_eq!(state.value(), "abc");
7362 state.undo(&Undo, window, cx);
7363 assert_eq!(state.value(), "");
7364 });
7365 });
7366 }
7367
7368 #[gpui::test]
7369 fn test_number_input_leading_dot_editable(cx: &mut TestAppContext) {
7370 let input_view = InputView::build(cx, |state| {
7371 state.mask_pattern(MaskPattern::Number {
7372 separator: None,
7373 fraction: None,
7374 })
7375 });
7376 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7377 let input = input_view.input;
7378
7379 cx.update(|window, cx| {
7380 input.update(cx, |state, cx| {
7381 state.replace_text_in_range(None, "1.2", window, cx);
7382
7383 let range = state.range_to_utf16(&(0..1));
7387 state.replace_text_in_range(Some(range), "", window, cx);
7388 assert_eq!(state.value(), ".2");
7389 let cursor: Range<usize> = state.selected_range();
7390 assert_eq!(cursor, 0..0);
7391
7392 state.replace_text_in_range(Some(0..0), "3", window, cx);
7394 assert_eq!(state.value(), "3.2");
7395 });
7396 });
7397 }
7398
7399 #[gpui::test]
7400 fn test_number_input_escape_invalid_text(cx: &mut TestAppContext) {
7401 let input_view = InputView::build(cx, |state| {
7404 state
7405 .mask_pattern(MaskPattern::Number {
7406 separator: None,
7407 fraction: None,
7408 })
7409 .default_value("1,234")
7410 });
7411 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7412 let input = input_view.input;
7413
7414 cx.update(|window, cx| {
7415 input.update(cx, |state, cx| {
7416 let range = state.range_to_utf16(&(4..5));
7420 state.replace_text_in_range(Some(range), "", window, cx);
7421 assert_eq!(state.value(), "1,23");
7422
7423 let range = state.range_to_utf16(&(1..2));
7425 state.replace_text_in_range(Some(range), "", window, cx);
7426 assert_eq!(state.value(), "123");
7427 state.replace_text_in_range(None, "a", window, cx);
7428 assert_eq!(state.value(), "123");
7429 });
7430 });
7431 }
7432
7433 #[gpui::test]
7437 fn test_set_value_single_line_caret_at_end_view_at_start(cx: &mut TestAppContext) {
7438 let input_view = InputView::build(cx, |state| state);
7439 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7440 let input = input_view.input;
7441
7442 let value = format!("https://example.com/v1/users?{}", "x=1&".repeat(120));
7444 let len = value.len();
7445
7446 cx.update(|window, cx| {
7449 input.update(cx, |state, cx| {
7450 state.set_value(value.clone(), window, cx);
7451
7452 assert_eq!(
7453 state.selected_range(),
7454 len..len,
7455 "single-line caret should be at the end after set_value"
7456 );
7457 assert_eq!(
7458 state.deferred_scroll_offset,
7459 Some(point(px(0.), px(0.))),
7460 "the view should be forced back to the start"
7461 );
7462 });
7463 });
7464
7465 cx.run_until_parked();
7468 cx.update(|_, cx| {
7469 input.read_with(cx, |state, _| {
7470 assert!(
7471 state.scroll_size.width > state.input_bounds.size.width,
7472 "value must overflow the input width or this test is vacuous"
7473 );
7474 assert_eq!(
7475 state.scroll_handle.offset().x,
7476 px(0.),
7477 "long value should display from its start, not its tail"
7478 );
7479 });
7480 });
7481 }
7482
7483 #[gpui::test]
7487 fn test_replace_all_single_line(cx: &mut TestAppContext) {
7488 let input_view = InputView::build(cx, |state| state);
7489 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7490 let input = input_view.input;
7491
7492 let value = format!("https://example.com/v1/users?{}", "x=1&".repeat(120));
7494 let len = value.len();
7495
7496 cx.update(|window, cx| {
7500 input.update(cx, |state, cx| {
7501 state.set_value("hello", window, cx);
7502 state.replace_all(value.clone(), window, cx);
7503 assert_eq!(state.value(), value);
7504 assert_eq!(
7505 state.selected_range(),
7506 len..len,
7507 "single-line caret should be at the end after replace_all"
7508 );
7509 assert_eq!(
7510 state.scroll_handle.offset(),
7511 point(px(0.), px(0.)),
7512 "the scroll offset should be reset to the start"
7513 );
7514 assert_eq!(
7515 state.deferred_scroll_offset,
7516 Some(point(px(0.), px(0.))),
7517 "single-line should set a deferred scroll offset to keep the start visible"
7518 );
7519 });
7520 });
7521
7522 cx.run_until_parked();
7525 cx.update(|_, cx| {
7526 input.read_with(cx, |state, _| {
7527 assert!(
7528 state.scroll_size.width > state.input_bounds.size.width,
7529 "value must overflow the input width or this test is vacuous"
7530 );
7531 assert_eq!(
7532 state.scroll_handle.offset().x,
7533 px(0.),
7534 "long value should display from its start, not its tail"
7535 );
7536 });
7537 });
7538 }
7539
7540 #[gpui::test]
7541 fn test_single_line_removes_newlines(cx: &mut TestAppContext) {
7542 let input_view = InputView::build(cx, |state| state.default_value("default\nvalue"));
7543 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7544 let input = input_view.input;
7545
7546 cx.update(|window, cx| {
7547 input.update(cx, |state, cx| {
7548 assert_eq!(state.value(), "defaultvalue");
7549
7550 state.set_value("first\nsecond\r\nthird\rfourth", window, cx);
7551 assert_eq!(state.value(), "firstsecondthirdfourth");
7552
7553 state.set_value("", window, cx);
7554 state.insert("a\nb", window, cx);
7555 assert_eq!(state.value(), "ab");
7556 });
7557
7558 cx.write_to_clipboard(ClipboardItem::new_string("a\r\nb\nc\rd".to_string()));
7559 input.update(cx, |state, cx| {
7560 state.set_value("", window, cx);
7561 state.paste(&Paste, window, cx);
7562 assert_eq!(state.value(), "abcd");
7563 });
7564 });
7565
7566 cx.run_until_parked();
7567 }
7568
7569 #[gpui::test]
7573 fn test_replace_all_multi_line(cx: &mut TestAppContext) {
7574 let input_view = InputView::build_textarea(cx, |state| state);
7575 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7576 let input = input_view.input;
7577
7578 cx.update(|window, cx| {
7579 input.update(cx, |state, cx| {
7580 state.set_value("foo\nbar", window, cx);
7581 state.replace_all("baz\nqux", window, cx);
7582 assert_eq!(state.value(), "baz\nqux");
7583 assert_eq!(
7584 state.selected_range(),
7585 0..0,
7586 "multi-line selection should be cleared after replace_all"
7587 );
7588 assert_eq!(
7589 state.scroll_handle.offset(),
7590 point(px(0.), px(0.)),
7591 "the scroll offset should be reset to the start"
7592 );
7593 assert!(
7594 state.deferred_scroll_offset.is_none(),
7595 "multi-line should not set a deferred scroll offset"
7596 );
7597 });
7598 });
7599 }
7600
7601 #[gpui::test]
7604 fn test_replace_all_preserves_undo_history(cx: &mut TestAppContext) {
7605 let input_view = InputView::build(cx, |state| state);
7606 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7607 let input = input_view.input;
7608
7609 cx.update(|window, cx| {
7610 input.update(cx, |state, cx| {
7611 state.set_value("first", window, cx);
7613 assert!(
7614 !state.undo_manager.has_undos(),
7615 "history should be empty after set_value"
7616 );
7617
7618 state.replace_all("second", window, cx);
7620 assert_eq!(state.value(), "second");
7621 assert!(
7622 state.undo_manager.has_undos(),
7623 "replace_all should record an undo step"
7624 );
7625
7626 state.undo(&Undo, window, cx);
7628 assert_eq!(state.value(), "first");
7629
7630 state.redo(&Redo, window, cx);
7632 assert_eq!(state.value(), "second");
7633 });
7634 });
7635 }
7636
7637 #[gpui::test]
7640 fn test_replace_all_code_editor(cx: &mut TestAppContext) {
7641 let input_view = InputView::new(cx);
7642 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7643 let input = input_view.input;
7644
7645 cx.update(|window, cx| {
7646 input.update(cx, |state, cx| {
7647 state.set_value("select 1", window, cx);
7649 state._pending_update = false;
7650
7651 state.replace_all("select 2", window, cx);
7652 assert_eq!(state.value(), "select 2");
7653 assert!(
7654 state._pending_update,
7655 "replace_all on a code editor should request a pending update"
7656 );
7657 });
7658 });
7659 }
7660
7661 #[gpui::test]
7662 fn test_set_selected_range(cx: &mut TestAppContext) {
7663 let input_view = InputView::build(cx, |state| state.default_value("hello world"));
7664 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7665 let input = input_view.input;
7666
7667 cx.update(|_, cx| {
7668 input.update(cx, |s, cx| {
7669 s.set_selected_range(0..5, cx);
7670 assert_eq!(s.selected_range(), 0..5);
7671 assert_eq!(s.selected_text().to_string(), "hello");
7672
7673 s.set_selected_range(6..11, cx);
7674 assert_eq!(s.selected_text().to_string(), "world");
7675
7676 s.set_selected_range(100..100, cx);
7678 assert_eq!(s.selected_range(), 11..11);
7679 });
7680 });
7681 }
7682
7683 #[gpui::test]
7685 fn test_replace_text_in_ranges_single_edit(cx: &mut TestAppContext) {
7686 let input_view = InputView::build_textarea(cx, |state| state.default_value("hello world"));
7687 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7688 let input = input_view.input;
7689
7690 cx.update(|window, cx| {
7691 input.update(cx, |s, cx| {
7692 s.replace_text_in_ranges(&[(0..5, "HELLO".to_string())], window, cx);
7693 assert_eq!(s.value(), "HELLO world");
7694
7695 s.undo(&Undo, window, cx);
7696 assert_eq!(s.value(), "hello world");
7697
7698 s.redo(&Redo, window, cx);
7699 assert_eq!(s.value(), "HELLO world");
7700 });
7701 });
7702 }
7703
7704 #[gpui::test]
7707 fn test_replace_text_in_ranges_multi_edit_transaction(cx: &mut TestAppContext) {
7708 let input_view = InputView::build_textarea(cx, |state| state.default_value("aaa bbb ccc"));
7709 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7710 let input = input_view.input;
7711
7712 cx.update(|window, cx| {
7713 input.update(cx, |s, cx| {
7714 s.replace_text_in_ranges(
7717 &[(0..3, "X".to_string()), (8..11, "Y".to_string())],
7718 window,
7719 cx,
7720 );
7721 assert_eq!(s.value(), "X bbb Y");
7722
7723 let cursors: Vec<usize> =
7725 s.selections.iter().map(|sel| sel.cursor_offset()).collect();
7726 assert_eq!(cursors, vec![1, 7]);
7727
7728 assert_eq!(s.undo_manager.undo_count(), 1);
7730
7731 s.undo(&Undo, window, cx);
7733 assert_eq!(s.value(), "aaa bbb ccc");
7734
7735 s.redo(&Redo, window, cx);
7737 assert_eq!(s.value(), "X bbb Y");
7738 });
7739 });
7740 }
7741
7742 #[gpui::test]
7746 fn test_replace_text_in_ranges_drives_the_highlighter_once(cx: &mut TestAppContext) {
7747 use crate::input::{FoldRange, HighlightStyleResolver, InputEdit, InputHighlighter};
7748 use gpui::HighlightStyle;
7749 use std::cell::RefCell;
7750
7751 struct RecordingHighlighter(Rc<RefCell<Vec<Vec<String>>>>);
7752
7753 impl InputHighlighter for RecordingHighlighter {
7754 fn language(&self) -> SharedString {
7755 "recording".into()
7756 }
7757
7758 fn update(
7759 &mut self,
7760 _: Option<InputEdit>,
7761 text: &Rope,
7762 _: bool,
7763 _: &mut Window,
7764 _: &mut Context<EditorState>,
7765 ) {
7766 self.0.borrow_mut().push(vec![text.to_string()]);
7767 }
7768
7769 fn update_batch(
7770 &mut self,
7771 edits: &[(InputEdit, Rope)],
7772 _: bool,
7773 _: &mut Window,
7774 _: &mut Context<EditorState>,
7775 ) {
7776 let texts = edits.iter().map(|(_, text)| text.to_string()).collect();
7777 self.0.borrow_mut().push(texts);
7778 }
7779
7780 fn styles(
7781 &self,
7782 range: &Range<usize>,
7783 _: &dyn HighlightStyleResolver,
7784 ) -> Vec<(Range<usize>, HighlightStyle)> {
7785 vec![(range.clone(), HighlightStyle::default())]
7786 }
7787
7788 fn fold_ranges(&self, _: &Rope) -> Vec<FoldRange> {
7789 Vec::new()
7790 }
7791 }
7792
7793 let updates = Rc::new(RefCell::new(Vec::new()));
7794 let input_view = InputView::new(cx);
7795 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7796 let input = input_view.input;
7797
7798 cx.update(|window, cx| {
7799 input.update(cx, |s, cx| {
7800 s.set_value("aaa bbb ccc", window, cx);
7801 let recorded = updates.clone();
7802 s.set_highlighter_factory(
7803 Rc::new(move |_: &str| {
7804 let highlighter: Box<dyn InputHighlighter> =
7805 Box::new(RecordingHighlighter(recorded.clone()));
7806 Some(highlighter)
7807 }),
7808 cx,
7809 );
7810 s.replace_text_in_ranges(
7811 &[(0..3, "X".to_string()), (8..11, "Y".to_string())],
7812 window,
7813 cx,
7814 );
7815 });
7816 });
7817
7818 assert_eq!(
7819 *updates.borrow(),
7820 vec![vec!["aaa bbb Y".to_string(), "X bbb Y".to_string()]]
7821 );
7822 }
7823
7824 #[gpui::test]
7826 fn test_ime_composition_undoes_as_one_unit(cx: &mut TestAppContext) {
7827 let input_view = InputView::build_textarea(cx, |state| state.default_value(""));
7828 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7829 let input = input_view.input;
7830
7831 cx.update(|window, cx| {
7832 input.update(cx, |s, cx| {
7833 s.replace_and_mark_text_in_range(None, "n", Some(1..1), window, cx);
7835 s.replace_and_mark_text_in_range(None, "ni", Some(2..2), window, cx);
7836 s.replace_text_in_range(None, "你", window, cx);
7837 assert_eq!(s.value(), "你");
7838
7839 assert_eq!(s.undo_manager.undo_count(), 1);
7841
7842 s.undo(&Undo, window, cx);
7843 assert_eq!(s.value(), "");
7844
7845 s.redo(&Redo, window, cx);
7846 assert_eq!(s.value(), "你");
7847 });
7848 });
7849 }
7850
7851 #[gpui::test]
7854 fn test_edit_after_composition_is_separate_undo(cx: &mut TestAppContext) {
7855 let input_view = InputView::build_textarea(cx, |state| state.default_value(""));
7856 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7857 let input = input_view.input;
7858
7859 cx.update(|window, cx| {
7860 input.update(cx, |s, cx| {
7861 s.replace_and_mark_text_in_range(None, "n", Some(1..1), window, cx);
7862 s.replace_text_in_range(None, "你", window, cx);
7863 assert_eq!(s.value(), "你");
7864 assert_eq!(s.undo_manager.undo_count(), 1);
7865
7866 s.replace_text_in_range(None, "x", window, cx);
7868 assert_eq!(s.value(), "你x");
7869 assert_eq!(s.undo_manager.undo_count(), 2);
7870
7871 s.undo(&Undo, window, cx);
7872 assert_eq!(s.value(), "你");
7873 s.undo(&Undo, window, cx);
7874 assert_eq!(s.value(), "");
7875 });
7876 });
7877 }
7878
7879 #[gpui::test]
7882 fn test_composition_cancel_via_unmark_does_not_leak(cx: &mut TestAppContext) {
7883 let input_view = InputView::build_textarea(cx, |state| state.default_value(""));
7884 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7885 let input = input_view.input;
7886
7887 cx.update(|window, cx| {
7888 input.update(cx, |s, cx| {
7889 s.replace_and_mark_text_in_range(None, "n", Some(1..1), window, cx);
7891 s.unmark_text(window, cx);
7892 let after_cancel = s.undo_manager.undo_count();
7893
7894 s.replace_text_in_range(None, "x", window, cx);
7896 assert_eq!(s.undo_manager.undo_count(), after_cancel + 1);
7897 });
7898 });
7899 }
7900
7901 #[gpui::test]
7902 fn test_set_selected_range_clips_to_utf8_boundaries(cx: &mut TestAppContext) {
7903 let input_view = InputView::build(cx, |state| state.default_value("éx"));
7904 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7905 let input = input_view.input;
7906
7907 cx.update(|window, cx| {
7908 input.update(cx, |state, cx| {
7909 state.set_selected_range(0..1, cx);
7910 assert_eq!(state.selected_range(), 0..2);
7911 state.copy(&Copy, window, cx);
7912
7913 state.set_selected_range(1..1, cx);
7914 assert_eq!(state.selected_range(), 0..0);
7915 });
7916 });
7917 }
7918
7919 #[gpui::test]
7920 fn test_ime_selection_is_relative_to_replacement_start(cx: &mut TestAppContext) {
7921 let input_view = InputView::build(cx, |state| state.default_value("你好 "));
7922 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7923 let input = input_view.input;
7924
7925 cx.update(|window, cx| {
7926 input.update(cx, |state, cx| {
7927 state.set_selected_range(7..7, cx);
7928 state.replace_and_mark_text_in_range(None, "s", Some(1..1), window, cx);
7929 state.replace_and_mark_text_in_range(None, "sh", Some(2..2), window, cx);
7930
7931 assert_eq!(state.value(), "你好 sh");
7932 assert_eq!(state.selected_range(), 9..9);
7933 assert_eq!(state.ime_marked_range, Some((7..9).into()));
7934 });
7935 });
7936 }
7937
7938 #[gpui::test]
7939 fn test_undo_manager_composition_is_one_undo_group(cx: &mut TestAppContext) {
7940 let input_view = InputView::build(cx, |state| state);
7941 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7942 let input = input_view.input;
7943
7944 cx.update(|window, cx| {
7945 input.update(cx, |state, cx| {
7946 state.set_value("a", window, cx);
7947 state.replace_and_mark_text_in_range(None, "s", None, window, cx);
7948 state.replace_and_mark_text_in_range(None, "sh", None, window, cx);
7949 state.replace_text_in_range(None, "是", window, cx);
7950 assert_eq!(state.value(), "a是");
7951
7952 state.undo(&Undo, window, cx);
7953 assert_eq!(state.value(), "a");
7954 state.redo(&Redo, window, cx);
7955 assert_eq!(state.value(), "a是");
7956 });
7957 });
7958 }
7959
7960 #[gpui::test]
7961 fn test_undo_manager_consecutive_compositions_are_separate_groups(cx: &mut TestAppContext) {
7962 let input_view = InputView::build(cx, |state| state);
7963 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
7964 let input = input_view.input;
7965
7966 cx.update(|window, cx| {
7967 input.update(cx, |state, cx| {
7968 state.replace_and_mark_text_in_range(None, "j", None, window, cx);
7970 state.replace_and_mark_text_in_range(None, "jin", None, window, cx);
7971 state.replace_text_in_range(None, "今天", window, cx);
7972 state.replace_and_mark_text_in_range(None, "w", None, window, cx);
7974 state.replace_and_mark_text_in_range(None, "wo", None, window, cx);
7975 state.replace_text_in_range(None, "我们", window, cx);
7976 assert_eq!(state.value(), "今天我们");
7977 assert_eq!(state.selected_range(), 12..12);
7978
7979 state.undo(&Undo, window, cx);
7980 assert_eq!(state.value(), "今天");
7981 assert_eq!(state.selected_range(), 6..6);
7982
7983 state.undo(&Undo, window, cx);
7984 assert_eq!(state.value(), "");
7985 assert_eq!(state.selected_range(), 0..0);
7986
7987 state.redo(&Redo, window, cx);
7988 assert_eq!(state.value(), "今天");
7989 assert_eq!(state.selected_range(), 6..6);
7990
7991 state.redo(&Redo, window, cx);
7992 assert_eq!(state.value(), "今天我们");
7993 assert_eq!(state.selected_range(), 12..12);
7994 });
7995 });
7996 }
7997
7998 #[gpui::test]
7999 fn test_undo_manager_typing_after_composition_is_a_separate_group(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 input.update(cx, |state, cx| {
8006 state.replace_and_mark_text_in_range(None, "n", None, window, cx);
8007 state.replace_text_in_range(None, "你", window, cx);
8008 state.undo_manager.set_pending_intent(EditIntent::Typing);
8009 state.replace_text_in_range(None, "a", window, cx);
8010 state.undo_manager.set_pending_intent(EditIntent::Typing);
8011 state.replace_text_in_range(None, "b", window, cx);
8012 assert_eq!(state.value(), "你ab");
8013
8014 state.undo(&Undo, window, cx);
8015 assert_eq!(state.value(), "你");
8016
8017 state.undo(&Undo, window, cx);
8018 assert_eq!(state.value(), "");
8019 });
8020 });
8021 }
8022
8023 #[gpui::test]
8024 fn test_undo_manager_composition_cancel_leaves_no_entry(cx: &mut TestAppContext) {
8025 let input_view = InputView::build(cx, |state| state);
8026 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8027 let input = input_view.input;
8028
8029 cx.update(|window, cx| {
8030 input.update(cx, |state, cx| {
8031 state.set_value("a", window, cx);
8032 state.replace_and_mark_text_in_range(None, "s", None, window, cx);
8033 state.replace_and_mark_text_in_range(None, "", None, window, cx);
8034
8035 assert_eq!(state.value(), "a");
8036 assert!(!state.undo_manager.has_undos());
8037 });
8038 });
8039 }
8040
8041 #[gpui::test]
8043 fn test_undo_manager_ime_rewrites_of_typed_char_are_one_group(cx: &mut TestAppContext) {
8044 let input_view = InputView::build(cx, |state| state);
8045 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8046 let input = input_view.input;
8047
8048 cx.update(|window, cx| {
8049 input.update(cx, |state, cx| {
8050 state.set_value("é ", window, cx);
8051 state.set_selected_range(3..3, cx);
8052 state.replace_text_in_range(None, "ㅎ", window, cx);
8054 state.replace_text_in_range(Some(2..3), "하", window, cx);
8055 state.replace_text_in_range(Some(2..3), "한", window, cx);
8056 state.replace_text_in_range(None, "ㄱ", window, cx);
8057 state.replace_text_in_range(Some(3..4), "그", window, cx);
8058 state.replace_text_in_range(Some(3..4), "글", window, cx);
8059 assert_eq!(state.value(), "é 한글");
8060 assert_eq!(state.undo_manager.undo_count(), 1);
8061
8062 state.undo(&Undo, window, cx);
8063 assert_eq!(state.value(), "é ");
8064 assert_eq!(state.selected_range(), 3..3);
8065
8066 state.redo(&Redo, window, cx);
8067 assert_eq!(state.value(), "é 한글");
8068 assert_eq!(state.selected_range(), 9..9);
8069 });
8070 });
8071 }
8072
8073 #[gpui::test]
8074 fn test_undo_manager_rewrite_after_cursor_movement_is_separate(cx: &mut TestAppContext) {
8075 let input_view = InputView::build(cx, |state| state);
8076 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8077 let input = input_view.input;
8078
8079 cx.update(|window, cx| {
8080 input.update(cx, |state, cx| {
8081 state.replace_text_in_range(None, "c", window, cx);
8082 state.replace_text_in_range(None, "e", window, cx);
8083 state.left(&MoveLeft, window, cx);
8084 state.right(&MoveRight, window, cx);
8085 state.replace_text_in_range(Some(1..2), "é", window, cx);
8086 assert_eq!(state.value(), "cé");
8087
8088 state.undo(&Undo, window, cx);
8089 assert_eq!(state.value(), "ce");
8090 state.undo(&Undo, window, cx);
8091 assert_eq!(state.value(), "");
8092 });
8093 });
8094 }
8095
8096 #[gpui::test]
8097 fn test_undo_manager_selection_restored_by_undo_and_redo(cx: &mut TestAppContext) {
8098 let input_view = InputView::build(cx, |state| state);
8099 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8100 let input = input_view.input;
8101
8102 cx.update(|window, cx| {
8103 input.update(cx, |state, cx| {
8104 state.set_value("abc", window, cx);
8105 state.set_selected_range(1..2, cx);
8106 state.replace_text_in_range(None, "X", window, cx);
8107
8108 state.undo(&Undo, window, cx);
8109 assert_eq!(state.value(), "abc");
8110 assert_eq!(state.selected_range(), 1..2);
8111
8112 state.redo(&Redo, window, cx);
8113 assert_eq!(state.value(), "aXc");
8114 assert_eq!(state.selected_range(), 2..2);
8115 });
8116 });
8117 }
8118
8119 #[gpui::test]
8120 fn test_undo_manager_forward_delete_restores_cursor(cx: &mut TestAppContext) {
8121 let input_view = InputView::build(cx, |state| state);
8122 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8123 let input = input_view.input;
8124
8125 cx.update(|window, cx| {
8126 input.update(cx, |state, cx| {
8127 state.set_value("abc", window, cx);
8128 state.set_selected_range(1..1, cx);
8129 state.delete(&Delete, window, cx);
8130
8131 state.undo(&Undo, window, cx);
8132 assert_eq!(state.value(), "abc");
8133 assert_eq!(state.selected_range(), 1..1);
8134 });
8135 });
8136 }
8137
8138 #[gpui::test]
8139 fn test_undo_manager_selection_movement_preserves_redo(cx: &mut TestAppContext) {
8140 let input_view = InputView::build(cx, |state| state);
8141 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8142 let input = input_view.input;
8143
8144 cx.update(|window, cx| {
8145 input.update(cx, |state, cx| {
8146 state.replace_text_in_range(None, "ab", window, cx);
8147 state.undo(&Undo, window, cx);
8148 state.set_selected_range(0..0, cx);
8149 state.redo(&Redo, window, cx);
8150 assert_eq!(state.value(), "ab");
8151
8152 state.undo(&Undo, window, cx);
8153 state.replace_text_in_range(None, "x", window, cx);
8154 state.redo(&Redo, window, cx);
8155 assert_eq!(state.value(), "x");
8156 });
8157 });
8158 }
8159
8160 #[gpui::test]
8161 fn test_undo_manager_noop_edit_preserves_redo(cx: &mut TestAppContext) {
8162 let input_view = InputView::build(cx, |state| state);
8163 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8164 let input = input_view.input;
8165
8166 cx.update(|window, cx| {
8167 input.update(cx, |state, cx| {
8168 state.replace_text_in_range(None, "a", window, cx);
8169 state.move_to(0, None, cx);
8170 state.replace_text_in_range(None, "", window, cx);
8171 state.undo(&Undo, window, cx);
8172 assert_eq!(state.value(), "");
8173 state.redo(&Redo, window, cx);
8174 assert_eq!(state.value(), "a");
8175 assert_eq!(state.cursor(), 1);
8176 });
8177 });
8178 }
8179
8180 #[gpui::test]
8181 fn test_cursor_round_trip_stops_typing_coalescing(cx: &mut TestAppContext) {
8182 let input_view = InputView::build(cx, |state| state);
8183 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8184 let input = input_view.input;
8185
8186 cx.update(|window, cx| {
8187 input.update(cx, |state, cx| {
8188 state.replace_text_in_range(None, "a", window, cx);
8189 state.left(&MoveLeft, window, cx);
8190 state.right(&MoveRight, window, cx);
8191 state.replace_text_in_range(None, "b", window, cx);
8192
8193 state.undo(&Undo, window, cx);
8194 assert_eq!(state.value(), "a");
8195 state.undo(&Undo, window, cx);
8196 assert_eq!(state.value(), "");
8197 });
8198 });
8199 }
8200
8201 #[gpui::test]
8202 fn test_undo_manager_noop_edit_breaks_coalescing_without_clearing_history(
8203 cx: &mut TestAppContext,
8204 ) {
8205 let input_view = InputView::build(cx, |state| state);
8206 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8207 let input = input_view.input;
8208
8209 cx.update(|window, cx| {
8210 input.update(cx, |state, cx| {
8211 state.replace_text_in_range(None, "alpha", window, cx);
8212 state.replace_text_in_range(None, "", window, cx);
8213 state.replace_text_in_range(None, "beta", window, cx);
8214 assert_eq!(state.value(), "alphabeta");
8215
8216 state.undo(&Undo, window, cx);
8217 assert_eq!(state.value(), "alpha");
8218 state.undo(&Undo, window, cx);
8219 assert_eq!(state.value(), "");
8220 });
8221 });
8222 }
8223
8224 #[gpui::test]
8225 fn test_undo_manager_masked_redo_restores_actual_cursor(cx: &mut TestAppContext) {
8226 let input_view = InputView::build(cx, |state| {
8227 state.mask_pattern(MaskPattern::Number {
8228 separator: Some(','),
8229 fraction: None,
8230 })
8231 });
8232 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
8233 let input = input_view.input;
8234
8235 cx.update(|window, cx| {
8236 input.update(cx, |state, cx| {
8237 state.set_value("12345", window, cx);
8238 state.set_selected_range(2..2, cx);
8239 state.replace_text_in_range(None, "9", window, cx);
8240 let selection_after_edit = state.selected_range();
8241 assert_ne!(selection_after_edit.end, state.value().len());
8242
8243 state.undo(&Undo, window, cx);
8244 state.redo(&Redo, window, cx);
8245 assert_eq!(state.selected_range(), selection_after_edit);
8246 });
8247 });
8248 }
8249
8250 #[gpui::test]
8256 fn test_unfold_at(cx: &mut TestAppContext) {
8257 use crate::input::{FoldRange, Position};
8258
8259 let view = InputView::<EditorMode>::new(cx);
8260 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8261 let input = view.input;
8262
8263 cx.update(|window, cx| {
8266 input.update(cx, |state, cx| {
8267 state.set_value("a\nb\nc\nd\ne\nf\ng\nh\ni\nj\nk\nl", window, cx);
8268 state.apply_highlighter_fold_candidates(
8269 vec![
8270 FoldRange::new(0, 5),
8271 FoldRange::new(2, 4),
8272 FoldRange::new(7, 10),
8273 ],
8274 cx,
8275 );
8276 state.display_map.set_folded(0, true);
8277 state.display_map.set_folded(2, true);
8278 state.display_map.set_folded(7, true);
8279 });
8280 });
8281
8282 for line in [0, 5] {
8285 cx.update(|_, cx| {
8286 input.update(cx, |state, cx| {
8287 assert!(!state.display_map.is_buffer_line_hidden(line));
8288 assert!(!state.unfold_at(Position::new(line as u32, 0), cx));
8289 });
8290 input.read_with(cx, |state, _| {
8291 assert!(state.display_map.is_folded_at(0));
8292 assert!(state.display_map.is_folded_at(2));
8293 assert!(state.display_map.is_folded_at(7));
8294 });
8295 });
8296 }
8297
8298 cx.update(|_, cx| {
8301 input.update(cx, |state, cx| {
8302 assert!(state.unfold_at(Position::new(3, 0), cx));
8303 });
8304 input.read_with(cx, |state, _| {
8305 assert!(!state.display_map.is_buffer_line_hidden(3));
8306 assert!(!state.display_map.is_folded_at(0));
8307 assert!(!state.display_map.is_folded_at(2));
8308 assert!(state.display_map.is_folded_at(7));
8309 assert!(state.display_map.is_fold_candidate(0));
8311 assert!(state.display_map.is_fold_candidate(2));
8312 });
8313 });
8314
8315 cx.update(|_, cx| {
8317 input.update(cx, |state, cx| {
8318 assert!(!state.unfold_at(Position::new(3, 0), cx));
8319 });
8320 });
8321 }
8322
8323 #[gpui::test]
8328 fn test_blur_keeps_decorations(cx: &mut TestAppContext) {
8329 let view = InputView::<EditorMode>::new(cx);
8330 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8331 let input = view.input;
8332
8333 cx.update(|window, cx| {
8334 input.update(cx, |state, cx| {
8335 state.set_value("select 1", window, cx);
8336 let _collection = state.create_decorations_collection(
8337 vec![crate::input::TextDecoration::new(
8338 0..6,
8339 gpui::HighlightStyle {
8340 font_weight: Some(gpui::FontWeight::BOLD),
8341 ..Default::default()
8342 },
8343 )],
8344 cx,
8345 );
8346 state.present_hover(
8347 0..6,
8348 lsp_types::Hover {
8349 contents: lsp_types::HoverContents::Scalar(
8350 lsp_types::MarkedString::String("docs".into()),
8351 ),
8352 range: None,
8353 },
8354 cx,
8355 );
8356 assert!(state.hover_popover().is_some());
8357
8358 state.on_blur(window, cx);
8359
8360 assert!(
8361 state.hover_popover().is_none(),
8362 "blur should hide the hover popover"
8363 );
8364 let decorations = state.extras.decoration_layers();
8365 assert!(
8366 decorations.iter().any(|layer| !layer.is_empty()),
8367 "blur must not discard decorations"
8368 );
8369 });
8370 });
8371 }
8372
8373 #[gpui::test]
8378 fn test_kind_does_not_follow_the_row_count(cx: &mut TestAppContext) {
8379 let view = InputView::build_textarea(cx, |state| state.auto_grow(1, 1));
8380 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8381 view.input.read_with(&mut cx, |state, _| {
8382 assert!(state.is_multi_line());
8383 assert!(!state.is_single_line());
8384 assert!(!state.is_code_editor());
8385 });
8386 }
8387
8388 #[gpui::test]
8393 fn test_soft_wrap_is_enabled_by_default(cx: &mut TestAppContext) {
8394 let textarea = InputView::build_textarea(cx, |state| state);
8395 let mut textarea_cx = VisualTestContext::from_window(textarea.window_handle.into(), cx);
8396 textarea
8397 .input
8398 .read_with(&mut textarea_cx, |state, _| assert!(state.soft_wrap));
8399
8400 let editor = InputView::<EditorMode>::new(cx);
8401 let mut editor_cx = VisualTestContext::from_window(editor.window_handle.into(), cx);
8402 editor
8403 .input
8404 .read_with(&mut editor_cx, |state, _| assert!(state.soft_wrap));
8405 }
8406
8407 fn parse_cursor_spec(input: &str) -> (String, Vec<usize>) {
8411 let mut full_text = String::new();
8412 let mut cursor_offsets = Vec::new();
8413 let non_empty_lines: Vec<&str> = input.lines().filter(|l| !l.is_empty()).collect();
8414
8415 for (line_idx, line) in non_empty_lines.iter().enumerate() {
8416 let mut positions = Vec::new();
8417 let mut text = String::new();
8418 for ch in line.chars() {
8419 if ch == '|' {
8420 positions.push(text.len());
8421 } else {
8422 text.push(ch);
8423 }
8424 }
8425
8426 if line_idx > 0 {
8427 full_text.push('\n');
8428 }
8429 let line_start = full_text.len();
8430 for pos in positions {
8431 cursor_offsets.push(line_start + pos);
8432 }
8433 full_text.push_str(&text);
8434 }
8435 full_text.push('\n');
8436
8437 (full_text, cursor_offsets)
8438 }
8439
8440 fn multi_line(cx: &mut TestAppContext) -> InputView<TextareaMode> {
8442 InputView::build_textarea(cx, |state| state)
8443 }
8444
8445 fn setup_cursors<M: InputModeKind>(
8447 cx: &mut VisualTestContext,
8448 input: &Entity<InputBaseState<M>>,
8449 spec: &str,
8450 ) {
8451 let (full_text, offsets) = parse_cursor_spec(spec);
8452 cx.update(|window, cx| {
8453 input.update(cx, |state, cx| {
8454 state.set_value(&full_text, window, cx);
8455 let selections = offsets
8456 .into_iter()
8457 .map(|offset| {
8458 CursorSelection::new(state.selections.generate_id(), offset, offset)
8459 })
8460 .collect();
8461 state.selections.replace_all(selections);
8462 cx.notify();
8463 });
8464 });
8465 }
8466
8467 #[track_caller]
8469 fn assert_cursors<M: InputModeKind>(
8470 cx: &mut VisualTestContext,
8471 input: &Entity<InputBaseState<M>>,
8472 spec: &str,
8473 ) {
8474 let (expected_text, mut expected_cursors) = parse_cursor_spec(spec);
8475 expected_cursors.sort();
8476
8477 let (actual_text, mut actual_cursors) = input.read_with(cx, |state, _| {
8478 (
8479 state.text.to_string(),
8480 state
8481 .selections
8482 .iter()
8483 .map(|s| s.cursor_offset())
8484 .collect::<Vec<_>>(),
8485 )
8486 });
8487 actual_cursors.sort();
8488
8489 assert_eq!(
8490 actual_text, expected_text,
8491 "Text mismatch:\nExpected: {expected_text:?}\nActual: {actual_text:?}"
8492 );
8493 assert_eq!(
8494 actual_cursors, expected_cursors,
8495 "Cursor mismatch:\nExpected: {expected_cursors:?}\nActual: {actual_cursors:?}"
8496 );
8497 }
8498
8499 #[gpui::test]
8500 fn test_alt_drag_selects_a_block_and_replaces_each_row(cx: &mut TestAppContext) {
8501 cx.update(crate::init);
8502 let view = InputView::<EditorMode>::new(cx);
8503 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8504 for modifiers in [
8505 gpui::Modifiers {
8506 alt: true,
8507 ..Default::default()
8508 },
8509 gpui::Modifiers {
8510 alt: true,
8511 shift: true,
8512 ..Default::default()
8513 },
8514 #[cfg(target_os = "linux")]
8515 gpui::Modifiers {
8516 alt: true,
8517 control: true,
8518 ..Default::default()
8519 },
8520 ] {
8521 setup_cursors(&mut cx, &view.input, "|abcd\nabcd\nabcd");
8522 cx.update(|window, cx| {
8523 view.input.update(cx, |state, cx| state.focus(window, cx));
8524 });
8525 let (start, end) = view.input.read_with(&cx, |state, _| {
8526 let layout = state.last_layout.as_ref().unwrap();
8527 let origin = state.last_bounds.unwrap().origin;
8528 let position = |row: usize, col| {
8529 let local = layout.lines[row]
8530 .position_for_index(col, layout, false)
8531 .unwrap();
8532 origin
8533 + point(
8534 layout.line_number_width + local.x,
8535 layout.line_height * (row as f32 + 0.5),
8536 )
8537 };
8538 (position(0, 1), position(2, 3))
8539 });
8540 let end = point(end.x - px(0.5), end.y);
8543 cx.update(|_, cx| {
8545 view.input.update(cx, |state, _| {
8546 state.extras.hover_definition.update(
8547 0..4,
8548 vec![lsp_types::LocationLink {
8549 origin_selection_range: None,
8550 target_uri: "file:///tmp/column-selection.rs".parse().unwrap(),
8551 target_range: Default::default(),
8552 target_selection_range: Default::default(),
8553 }],
8554 );
8555 });
8556 });
8557 cx.simulate_mouse_down(start, MouseButton::Left, modifiers);
8558 cx.simulate_mouse_move(end, MouseButton::Left, modifiers);
8559 cx.simulate_mouse_up(end, MouseButton::Left, modifiers);
8560 view.input.read_with(&cx, |state, _| {
8561 let ranges: Vec<_> = state
8562 .selections
8563 .iter()
8564 .map(|sel| sel.start..sel.end)
8565 .collect();
8566 assert_eq!(ranges, vec![1..3, 6..8, 11..13]);
8567 });
8568 cx.simulate_mouse_move(start, None, modifiers);
8570 cx.simulate_keystrokes("x");
8571 assert_cursors(&mut cx, &view.input, "ax|d\nax|d\nax|d");
8572 }
8573 }
8574
8575 #[gpui::test]
8576 fn test_alt_drag_extends_upward_from_an_existing_cursor(cx: &mut TestAppContext) {
8577 cx.update(crate::init);
8578 let view = InputView::<EditorMode>::new(cx);
8579 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8580 setup_cursors(&mut cx, &view.input, "abcd\nabcd\na|bcd");
8581 cx.update(|window, cx| {
8582 view.input.update(cx, |state, cx| state.focus(window, cx));
8583 });
8584 let (start, end) = view.input.read_with(&cx, |state, _| {
8585 let layout = state.last_layout.as_ref().unwrap();
8586 let origin = state.last_bounds.unwrap().origin;
8587 let local = layout.lines[0]
8588 .position_for_index(1, layout, false)
8589 .unwrap();
8590 let x = layout.line_number_width + local.x;
8591 (
8592 origin + point(x, layout.line_height * 2.5),
8593 origin + point(x, layout.line_height * 0.5),
8594 )
8595 });
8596 let modifiers = gpui::Modifiers {
8597 alt: true,
8598 ..Default::default()
8599 };
8600 cx.simulate_mouse_down(start, MouseButton::Left, modifiers);
8601 cx.simulate_mouse_move(end, MouseButton::Left, modifiers);
8602 cx.simulate_mouse_up(end, MouseButton::Left, modifiers);
8603 assert_cursors(&mut cx, &view.input, "a|bcd\na|bcd\na|bcd");
8604 }
8605
8606 #[gpui::test]
8607 fn test_alt_drag_over_a_short_row_keeps_the_block_width(cx: &mut TestAppContext) {
8608 cx.update(crate::init);
8609 let view = InputView::<EditorMode>::new(cx);
8610 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8611 setup_cursors(&mut cx, &view.input, "abcdef\nab\nabcdef");
8612 cx.update(|window, cx| {
8613 view.input.update(cx, |state, cx| state.focus(window, cx));
8614 });
8615 let (start, end) = view.input.read_with(&cx, |state, _| {
8618 let layout = state.last_layout.as_ref().unwrap();
8619 let origin = state.last_bounds.unwrap().origin;
8620 let x_of = |index| {
8621 layout.line_number_width
8622 + layout.lines[0]
8623 .position_for_index(index, layout, false)
8624 .unwrap()
8625 .x
8626 };
8627 (
8628 origin + point(x_of(1), layout.line_height * 0.5),
8629 origin + point(x_of(5), layout.line_height * 1.5),
8630 )
8631 });
8632 let modifiers = gpui::Modifiers {
8633 alt: true,
8634 shift: true,
8635 ..Default::default()
8636 };
8637 cx.simulate_mouse_down(start, MouseButton::Left, modifiers);
8638 cx.simulate_mouse_move(end, MouseButton::Left, modifiers);
8639 cx.simulate_mouse_up(end, MouseButton::Left, modifiers);
8640
8641 view.input.read_with(&cx, |state, _| {
8643 let ranges: Vec<_> = state.selections.iter().map(|s| (s.start, s.end)).collect();
8644 assert_eq!(ranges, vec![(1, 5), (8, 9)]);
8645 });
8646 }
8647
8648 #[gpui::test]
8649 fn test_alt_mouse_release_outside_editor_ends_column_selection(cx: &mut TestAppContext) {
8650 cx.update(crate::init);
8651 let view = InputView::<EditorMode>::new(cx);
8652 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8653 setup_cursors(&mut cx, &view.input, "|abcd\nabcd");
8654 let position = view.input.read_with(&cx, |state, _| {
8655 let layout = state.last_layout.as_ref().unwrap();
8656 state.last_bounds.unwrap().origin
8657 + point(layout.line_number_width + px(2.), layout.line_height * 0.5)
8658 });
8659 let modifiers = gpui::Modifiers {
8660 alt: true,
8661 ..Default::default()
8662 };
8663 cx.simulate_mouse_down(position, MouseButton::Left, modifiers);
8664 cx.simulate_mouse_up(point(px(-100.), px(-100.)), MouseButton::Left, modifiers);
8665 view.input.read_with(&cx, |state, _| {
8666 assert!(!state.selecting);
8667 assert!(state.column_select_start.is_none());
8668 });
8669 }
8670
8671 #[gpui::test]
8672 fn test_consumed_keystrokes_keep_cursor_visible(cx: &mut TestAppContext) {
8673 let view = InputView::<EditorMode>::new(cx);
8674 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8675 setup_cursors(&mut cx, &view.input, "a|b");
8676 cx.update(|window, cx| {
8677 view.input.update(cx, |state, cx| {
8678 state.focus(window, cx);
8679 state.pause_blink_cursor(cx);
8680 });
8681 });
8682 cx.run_until_parked();
8683 cx.executor()
8684 .advance_clock(std::time::Duration::from_millis(300));
8685 cx.run_until_parked();
8686 view.input.read_with(&cx, |state, cx| {
8687 assert!(!state.blink_cursor.read(cx).visible());
8688 });
8689 for _ in 0..5 {
8691 #[cfg(target_os = "macos")]
8692 cx.simulate_keystrokes("cmd-c");
8693 #[cfg(not(target_os = "macos"))]
8694 cx.simulate_keystrokes("ctrl-c");
8695 cx.run_until_parked();
8696 cx.executor()
8697 .advance_clock(std::time::Duration::from_millis(200));
8698 cx.run_until_parked();
8699 view.input.read_with(&cx, |state, cx| {
8700 assert!(state.blink_cursor.read(cx).visible());
8701 });
8702 }
8703 }
8704
8705 #[gpui::test]
8706 fn test_multi_cursor_actions_reveal_hidden_carets(cx: &mut TestAppContext) {
8707 let view = InputView::<EditorMode>::new(cx);
8708 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8709 setup_cursors(&mut cx, &view.input, "ab\na|b\nab");
8710 for action in 0..6 {
8713 cx.update(|_, cx| {
8714 view.input.update(cx, |state, cx| {
8715 state.blink_cursor = cx.new(|_| BlinkCursor::new());
8716 state.blink_cursor.update(cx, |cursor, cx| cursor.start(cx));
8717 });
8718 });
8719 cx.run_until_parked();
8720 cx.executor()
8721 .advance_clock(std::time::Duration::from_millis(500));
8722 cx.run_until_parked();
8723 view.input.read_with(&cx, |state, cx| {
8724 assert!(!state.blink_cursor.read(cx).visible(), "action {action}");
8725 });
8726
8727 cx.update(|window, cx| {
8728 view.input.update(cx, |state, cx| {
8729 match action {
8730 0 => state.add_cursor_above(&AddCursorAbove, window, cx),
8731 1 => state.add_cursor_below(&AddCursorBelow, window, cx),
8732 2 => state.select_up(&SelectUp, window, cx),
8733 3 => state.select_down(&SelectDown, window, cx),
8734 4 => state.replace_text_in_range(None, "x", window, cx),
8735 _ => state.backspace(&Backspace, window, cx),
8736 }
8737 assert!(state.blink_cursor.read(cx).visible(), "action {action}");
8738 });
8739 });
8740 }
8741 }
8742
8743 #[gpui::test]
8744 fn test_multi_cursor_keyboard_dispatch(cx: &mut TestAppContext) {
8745 let view = InputView::<EditorMode>::new(cx);
8746 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8747 setup_cursors(&mut cx, &view.input, "ab\na|b\nab");
8748 cx.update(|window, cx| {
8749 view.input.update(cx, |state, cx| state.focus(window, cx));
8750 });
8751 #[cfg(target_os = "macos")]
8752 cx.simulate_keystrokes("cmd-alt-up");
8753 #[cfg(target_os = "windows")]
8754 cx.simulate_keystrokes("ctrl-alt-up");
8755 #[cfg(not(any(target_os = "macos", target_os = "windows")))]
8756 cx.simulate_keystrokes("alt-shift-up");
8757 view.input
8758 .read_with(&cx, |state, _| assert_eq!(state.selections.len(), 2));
8759 #[cfg(target_os = "macos")]
8760 cx.simulate_keystrokes("cmd-alt-down");
8761 #[cfg(target_os = "windows")]
8762 cx.simulate_keystrokes("ctrl-alt-down");
8763 #[cfg(not(any(target_os = "macos", target_os = "windows")))]
8764 cx.simulate_keystrokes("alt-shift-down");
8765 view.input
8766 .read_with(&cx, |state, _| assert_eq!(state.selections.len(), 3));
8767 cx.simulate_keystrokes("x");
8768 assert_cursors(&mut cx, &view.input, "ax|b\nax|b\nax|b");
8769 }
8770
8771 #[gpui::test]
8772 fn test_multi_cursor_platform_word_selection_dispatch(cx: &mut TestAppContext) {
8773 let view = InputView::<EditorMode>::new(cx);
8774 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8775 setup_cursors(&mut cx, &view.input, "one |two\none |two");
8776 cx.update(|window, cx| {
8777 view.input.update(cx, |state, cx| state.focus(window, cx));
8778 });
8779 #[cfg(any(target_os = "macos", target_os = "linux"))]
8780 cx.simulate_keystrokes("alt-shift-right");
8781 #[cfg(not(any(target_os = "macos", target_os = "linux")))]
8782 cx.simulate_keystrokes("ctrl-shift-right");
8783 cx.simulate_keystrokes("x");
8784 assert_cursors(&mut cx, &view.input, "one x|\none x|");
8785 setup_cursors(&mut cx, &view.input, "one two|\none two|");
8786 #[cfg(any(target_os = "macos", target_os = "linux"))]
8787 cx.simulate_keystrokes("alt-shift-left");
8788 #[cfg(not(any(target_os = "macos", target_os = "linux")))]
8789 cx.simulate_keystrokes("ctrl-shift-left");
8790 cx.simulate_keystrokes("x");
8791 assert_cursors(&mut cx, &view.input, "one x|\none x|");
8792 }
8793
8794 #[cfg(not(target_os = "macos"))]
8795 #[gpui::test]
8796 fn test_multi_cursor_horizontal_selection_dispatch(cx: &mut TestAppContext) {
8797 let view = InputView::<EditorMode>::new(cx);
8798 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8799 setup_cursors(&mut cx, &view.input, "ab\na|b");
8800 cx.update(|window, cx| {
8801 view.input.update(cx, |state, cx| state.focus(window, cx));
8802 });
8803 #[cfg(target_os = "windows")]
8804 cx.simulate_keystrokes("ctrl-alt-up");
8805 #[cfg(not(target_os = "windows"))]
8806 cx.simulate_keystrokes("alt-shift-up");
8807 #[cfg(target_os = "linux")]
8808 cx.simulate_keystrokes("shift-right");
8809 #[cfg(not(target_os = "linux"))]
8810 cx.simulate_keystrokes("alt-shift-right");
8811 view.input.read_with(&cx, |state, _| {
8812 assert_eq!(
8813 state
8814 .selections
8815 .iter()
8816 .map(|s| s.start..s.end)
8817 .collect::<Vec<_>>(),
8818 vec![4..5, 1..2]
8819 );
8820 });
8821 #[cfg(target_os = "linux")]
8822 cx.simulate_keystrokes("shift-left shift-left");
8823 #[cfg(not(target_os = "linux"))]
8824 cx.simulate_keystrokes("alt-shift-left alt-shift-left");
8825 view.input.read_with(&cx, |state, _| {
8826 assert_eq!(
8827 state
8828 .selections
8829 .iter()
8830 .map(|s| s.start..s.end)
8831 .collect::<Vec<_>>(),
8832 vec![3..4, 0..1]
8833 );
8834 });
8835 cx.simulate_keystrokes("x");
8836 assert_cursors(&mut cx, &view.input, "x|b\nx|b");
8837 }
8838
8839 #[gpui::test]
8840 fn test_multi_cursor_alt_click_dispatch(cx: &mut TestAppContext) {
8841 cx.update(crate::init);
8842 let view = InputView::<EditorMode>::new(cx);
8843 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8844 setup_cursors(&mut cx, &view.input, "a|b\nab\nab");
8845 cx.update(|window, cx| {
8846 view.input.update(cx, |state, cx| state.focus(window, cx));
8847 });
8848 let position = view.input.read_with(&cx, |state, _| {
8849 let bounds = state.last_bounds.unwrap();
8850 let layout = state.last_layout.as_ref().unwrap();
8851 bounds.origin + point(layout.line_number_width + px(2.), layout.line_height * 1.5)
8852 });
8853 cx.simulate_click(
8854 position,
8855 gpui::Modifiers {
8856 alt: true,
8857 ..Default::default()
8858 },
8859 );
8860 view.input
8861 .read_with(&cx, |state, _| assert_eq!(state.selections.len(), 2));
8862 }
8863
8864 #[gpui::test]
8865 fn test_add_selection(cx: &mut TestAppContext) {
8866 let view = InputView::<EditorMode>::new(cx);
8867 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8868 cx.update(|window, cx| {
8869 view.input.update(cx, |state, cx| {
8870 state.focus(window, cx);
8871 state.set_value("foo bar foo", window, cx);
8872 state.set_selected_range(0..3, cx);
8873 state.add_selection(8..11, cx);
8875 state.add_selection(0..3, cx);
8877 });
8878 });
8879 view.input.read_with(&cx, |state, _| {
8880 let mut ranges: Vec<Range<usize>> =
8881 state.selections.iter().map(|s| s.start..s.end).collect();
8882 ranges.sort_by_key(|r| r.start);
8883 assert_eq!(ranges, vec![0..3, 8..11]);
8884 });
8885 cx.simulate_keystrokes("x");
8887 view.input.read_with(&cx, |state, _| {
8888 assert_eq!(state.text.to_string(), "x bar x")
8889 });
8890 }
8891
8892 #[gpui::test]
8893 fn test_add_selection_preserves_crlf_boundaries(cx: &mut TestAppContext) {
8894 let view = InputView::build_textarea(cx, |state| state.default_value("a\r\nb"));
8895 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8896 cx.update(|window, cx| {
8897 view.input.update(cx, |state, cx| {
8898 state.focus(window, cx);
8899 state.set_selected_range(4..4, cx);
8900 state.add_selection(2..2, cx);
8901 let ranges: Vec<_> = state.selections.iter().map(|s| s.start..s.end).collect();
8902 assert_eq!(ranges, vec![4..4, 1..1]);
8903 });
8904 });
8905 cx.simulate_keystrokes("x");
8906 view.input.read_with(&cx, |state, _| {
8907 assert_eq!(state.value(), "ax\r\nbx");
8908 });
8909 cx.update(|window, cx| {
8910 view.input.update(cx, |state, cx| {
8911 state.set_value("a\r\nb", window, cx);
8912 state.set_selected_range(4..4, cx);
8913 state.add_selection(2..3, cx);
8914 state.cut(&Cut, window, cx);
8915 assert_eq!(cx.read_from_clipboard().unwrap().text().unwrap(), "\r\n");
8916 assert_eq!(state.value(), "ab");
8917 });
8918 });
8919 }
8920
8921 #[gpui::test]
8922 fn test_add_selection_merges_overlaps_before_copy_and_cut(cx: &mut TestAppContext) {
8923 let view = InputView::<EditorMode>::new(cx);
8924 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8925 cx.update(|window, cx| {
8926 view.input.update(cx, |state, cx| {
8927 for additions in [vec![2..5], vec![0..5], vec![4..5, 2..4]] {
8930 state.set_value("abcdef", window, cx);
8931 state.set_selected_range(0..3, cx);
8932 let active_id = state.active_selection().id;
8933 for range in additions {
8934 state.add_selection(range, cx);
8935 }
8936 assert_eq!(state.active_selection().id, active_id);
8937 assert_eq!(state.selected_range(), 0..5);
8938 assert_eq!(state.selections.len(), 1);
8939 state.copy(&Copy, window, cx);
8940 assert_eq!(cx.read_from_clipboard().unwrap().text().unwrap(), "abcde");
8941 state.cut(&Cut, window, cx);
8942 assert_eq!(cx.read_from_clipboard().unwrap().text().unwrap(), "abcde");
8943 assert_eq!(state.value(), "f");
8944 state.undo(&Undo, window, cx);
8945 assert_eq!(state.value(), "abcdef");
8946 assert_eq!(state.selected_range(), 0..5);
8947 }
8948 });
8949 });
8950 }
8951
8952 #[gpui::test]
8953 fn test_add_selection_clears_inline_completion(cx: &mut TestAppContext) {
8954 use crate::input::CompletionProvider;
8955 use gpui::Task;
8956 use lsp_types::{
8957 CompletionContext, CompletionResponse, InlineCompletionContext, InlineCompletionItem,
8958 InlineCompletionResponse,
8959 };
8960 use std::time::Duration;
8961
8962 struct Provider(Rc<Cell<usize>>);
8963
8964 impl CompletionProvider for Provider {
8965 fn is_completion_trigger(&self, _: usize, _: &str, _: &mut App) -> bool {
8966 false
8967 }
8968
8969 fn completions(
8970 &self,
8971 _: &Rope,
8972 _: usize,
8973 _: CompletionContext,
8974 _: &mut Window,
8975 _: &mut App,
8976 ) -> Task<anyhow::Result<CompletionResponse>> {
8977 Task::ready(Ok(CompletionResponse::Array(vec![])))
8978 }
8979
8980 fn inline_completion(
8981 &self,
8982 _: &Rope,
8983 _: usize,
8984 _: InlineCompletionContext,
8985 _: &mut Window,
8986 _: &mut App,
8987 ) -> Task<anyhow::Result<InlineCompletionResponse>> {
8988 self.0.set(self.0.get() + 1);
8989 Task::ready(Ok(InlineCompletionResponse::Array(vec![])))
8990 }
8991 }
8992
8993 let view = InputView::<EditorMode>::new(cx);
8994 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8995 let calls = Rc::new(Cell::new(0));
8996 cx.update(|window, cx| {
8997 view.input.update(cx, |state, cx| {
8998 state.focus(window, cx);
8999 state.set_value("aa\naa", window, cx);
9000 state.set_selected_range(2..2, cx);
9001 state.extras.inline_completion.item = Some(InlineCompletionItem {
9002 insert_text: "suggestion".into(),
9003 filter_text: None,
9004 range: None,
9005 command: None,
9006 insert_text_format: None,
9007 });
9008 state.add_selection(5..5, cx);
9009 assert!(!state.has_inline_completion());
9010 state.indent_inline(&IndentInline, window, cx);
9011 assert_eq!(state.value(), "aa \naa ");
9012 assert_eq!(state.selections.len(), 2);
9013
9014 state.set_value("aa\naa", window, cx);
9017 state.set_selected_range(2..2, cx);
9018 state.extras.lsp.completion_provider = Some(Rc::new(Provider(calls.clone())));
9019 state.schedule_inline_completion(window, cx);
9020 });
9021 });
9022 cx.run_until_parked();
9023 cx.update(|_, cx| {
9024 view.input
9025 .update(cx, |state, cx| state.add_selection(5..5, cx));
9026 });
9027 cx.executor().advance_clock(Duration::from_secs(1));
9028 cx.run_until_parked();
9029 assert_eq!(calls.get(), 0);
9030 view.input.read_with(&cx, |state, _| {
9031 assert!(!state.has_inline_completion());
9032 assert_eq!(state.selections.len(), 2);
9033 assert_eq!(state.value(), "aa\naa");
9034 });
9035 }
9036
9037 #[gpui::test]
9038 fn test_word_delete_undo_restores_caret(cx: &mut TestAppContext) {
9039 let view = multi_line(cx);
9040 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9041 setup_cursors(&mut cx, &view.input, "hello|");
9042 cx.update(|window, cx| {
9043 view.input.update(cx, |state, cx| {
9044 state.delete_previous_word(&DeleteToPreviousWordStart, window, cx);
9045 state.undo(&Undo, window, cx);
9046 assert_eq!(state.selected_range(), 5..5);
9047 });
9048 });
9049 }
9050
9051 #[gpui::test]
9052 fn test_merged_delete_undo_restores_all_carets(cx: &mut TestAppContext) {
9053 let view = multi_line(cx);
9054 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9055 setup_cursors(&mut cx, &view.input, "a|b|c");
9056 cx.update(|window, cx| {
9057 view.input.update(cx, |state, cx| {
9058 state.backspace(&Backspace, window, cx);
9059 state.undo(&Undo, window, cx);
9060 });
9061 });
9062 assert_cursors(&mut cx, &view.input, "a|b|c");
9063 }
9064
9065 #[gpui::test]
9066 fn test_shift_end_respects_soft_wrap_end(cx: &mut TestAppContext) {
9067 let view = InputView::<EditorMode>::new(cx);
9068 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9069 cx.update(|window, cx| {
9070 view.input.update(cx, |state, cx| {
9071 state.set_value("abcdef ".repeat(30), window, cx);
9072 state.display_map.on_layout_changed(Some(px(60.)), cx);
9073 let line = state.display_map.line(0).unwrap();
9074 assert!(line.wrapped_lines.len() > 1);
9075 let boundary = line.wrapped_lines[0].end;
9076 state.move_to_with_affinity(boundary, None, true, cx);
9077 state.select_to_end_of_line(&SelectToEndOfLine, window, cx);
9078 assert_eq!(state.selected_range(), boundary..state.text.len());
9079 });
9080 });
9081 }
9082
9083 #[gpui::test]
9084 fn test_outdent_unindented_unicode_is_unchanged(cx: &mut TestAppContext) {
9085 let view = multi_line(cx);
9086 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9087 setup_cursors(&mut cx, &view.input, "|你好\n|世界");
9088 cx.update(|window, cx| {
9089 view.input.update(cx, |state, cx| {
9090 state.outdent(false, window, cx);
9091 state.outdent(true, window, cx);
9092 });
9093 });
9094 assert_cursors(&mut cx, &view.input, "|你好\n|世界");
9095 }
9096
9097 #[gpui::test]
9098 fn test_column_selection_stays_on_unicode_boundaries(cx: &mut TestAppContext) {
9099 let view = multi_line(cx);
9100 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9101 setup_cursors(&mut cx, &view.input, "ab\n你好\ncd");
9102 cx.update(|window, cx| {
9103 view.input.update(cx, |state, cx| {
9104 state.build_columnar_selection(
9105 ColumnarPoint::new(1, 0),
9106 ColumnarPoint::new(11, 0),
9107 cx,
9108 );
9109 let text = state.value();
9110 for sel in state.selections.iter() {
9111 assert!(text.is_char_boundary(sel.start));
9112 assert!(text.is_char_boundary(sel.end));
9113 }
9114 state.replace_text_in_range(None, "X", window, cx);
9115 });
9116 });
9117 }
9118
9119 #[gpui::test]
9120 fn test_editor_decorations_follow_typing(cx: &mut TestAppContext) {
9121 let view = InputView::<EditorMode>::new(cx);
9122 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9123 cx.update(|window, cx| {
9124 view.input.update(cx, |state, cx| {
9125 state.set_value("abc def", window, cx);
9126 state.create_decorations_collection(
9127 vec![crate::input::TextDecoration::new(
9128 4..7,
9129 gpui::HighlightStyle::default(),
9130 )],
9131 cx,
9132 );
9133 state.set_selected_range(0..0, cx);
9134 state.replace_text_in_range(None, "X", window, cx);
9135 let layers = state.extras.decoration_layers();
9136 assert_eq!(layers.into_iter().flatten().next().unwrap().range, 5..8);
9137 });
9138 });
9139 }
9140
9141 #[gpui::test]
9142 fn test_backspace_preserves_escaped_quote_terminator(cx: &mut TestAppContext) {
9143 let view = InputView::<EditorMode>::new(cx);
9144 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9145 setup_cursors(&mut cx, &view.input, r#"{"s":"\"|"}"#);
9146 cx.update(|window, cx| {
9147 view.input.update(cx, |state, cx| {
9148 set_test_syntax_provider(std::rc::Rc::new(StringAllProvider), cx);
9149 state.backspace(&Backspace, window, cx);
9150 });
9151 });
9152 assert_cursors(&mut cx, &view.input, r#"{"s":"\|"}"#);
9153 }
9154
9155 #[gpui::test]
9156 fn test_ordinary_typing_does_not_query_syntax(cx: &mut TestAppContext) {
9157 struct UnexpectedQuery;
9158 impl crate::input::SyntaxContextProvider for UnexpectedQuery {
9159 fn context_at(&self, _: &ropey::Rope, _: usize) -> crate::input::SyntaxContext {
9160 panic!("ordinary typing must not query syntax");
9161 }
9162 }
9163 let view = InputView::<EditorMode>::new(cx);
9164 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9165 setup_cursors(&mut cx, &view.input, "|");
9166 cx.update(|window, cx| {
9167 view.input.update(cx, |state, cx| {
9168 set_test_syntax_provider(std::rc::Rc::new(UnexpectedQuery), cx);
9169 for c in ["a", "b", "c"] {
9170 state.replace_text_in_range(None, c, window, cx);
9171 }
9172 });
9173 });
9174 assert_cursors(&mut cx, &view.input, "abc|");
9175 }
9176
9177 #[gpui::test]
9178 fn test_auto_close_is_atomic_at_history_limit(cx: &mut TestAppContext) {
9179 let view = InputView::<EditorMode>::new(cx);
9180 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9181 setup_cursors(&mut cx, &view.input, "|");
9182 cx.update(|window, cx| {
9183 view.input.update(cx, |state, cx| {
9184 for _ in 0..999 {
9185 state.replace_text_in_range(None, "a", window, cx);
9186 }
9187 state.replace_text_in_range(None, "(", window, cx);
9188 state.undo(&Undo, window, cx);
9189 assert!(!state.text.to_string().contains('('));
9190 assert!(!state.text.to_string().contains(')'));
9191 state.redo(&Redo, window, cx);
9192 });
9193 });
9194 assert_cursors(&mut cx, &view.input, &format!("{}(|)", "a".repeat(999)));
9195 }
9196
9197 #[gpui::test]
9198 fn test_comment_closer_is_inserted_literally(cx: &mut TestAppContext) {
9199 let view = InputView::<EditorMode>::new(cx);
9200 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9201 setup_cursors(&mut cx, &view.input, "// (|)");
9202 cx.update(|window, cx| {
9203 view.input.update(cx, |state, cx| {
9204 set_test_syntax_provider(std::rc::Rc::new(CommentAllProvider), cx);
9205 state.replace_text_in_range(None, ")", window, cx);
9206 });
9207 });
9208 assert_cursors(&mut cx, &view.input, "// ()|)");
9209 }
9210
9211 #[gpui::test]
9212 fn test_explicit_range_skip_does_not_delete_existing_closer_on_undo(cx: &mut TestAppContext) {
9213 let view = InputView::<EditorMode>::new(cx);
9214 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9215 setup_cursors(&mut cx, &view.input, "(a|)");
9216 let before = view.input.read_with(&cx, |state, _| state.text.to_string());
9217 cx.update(|window, cx| {
9218 view.input.update(cx, |state, cx| {
9219 state.replace_text_in_range(Some(2..2), ")", window, cx);
9220 });
9221 });
9222 assert_cursors(&mut cx, &view.input, "(a)|");
9223 cx.update(|window, cx| {
9224 view.input
9225 .update(cx, |state, cx| state.undo(&Undo, window, cx));
9226 });
9227 view.input.read_with(&cx, |state, _| {
9228 assert_eq!(
9229 state.text.to_string(),
9230 before,
9231 "skip must not remove an existing closer on undo"
9232 );
9233 });
9234 }
9235
9236 #[gpui::test]
9237 fn test_pair_redo_restores_interior_cursor(cx: &mut TestAppContext) {
9238 let view = InputView::<EditorMode>::new(cx);
9239 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9240 setup_cursors(&mut cx, &view.input, "|");
9241 cx.update(|window, cx| {
9242 view.input.update(cx, |state, cx| {
9243 state.replace_text_in_range(None, "(", window, cx);
9244 });
9245 });
9246 assert_cursors(&mut cx, &view.input, "(|)");
9247 cx.update(|window, cx| {
9248 view.input.update(cx, |state, cx| {
9249 state.undo(&Undo, window, cx);
9250 state.redo(&Redo, window, cx);
9251 });
9252 });
9253 assert_cursors(&mut cx, &view.input, "(|)");
9254 }
9255
9256 #[gpui::test]
9257 fn test_pair_enter_redo_restores_interior_cursor(cx: &mut TestAppContext) {
9258 let view = InputView::<EditorMode>::new(cx);
9259 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9260 setup_cursors(&mut cx, &view.input, "{|}");
9261 cx.update(|window, cx| {
9262 view.input.update(cx, |state, cx| {
9263 state.enter(
9264 &Enter {
9265 secondary: false,
9266 shift: false,
9267 },
9268 window,
9269 cx,
9270 );
9271 });
9272 });
9273 assert_cursors(&mut cx, &view.input, "{\n |\n}");
9274 cx.update(|window, cx| {
9275 view.input.update(cx, |state, cx| {
9276 state.undo(&Undo, window, cx);
9277 });
9278 });
9279 assert_cursors(&mut cx, &view.input, "{|}");
9280 cx.update(|window, cx| {
9281 view.input
9282 .update(cx, |state, cx| state.redo(&Redo, window, cx));
9283 });
9284 assert_cursors(&mut cx, &view.input, "{\n |\n}");
9285 }
9286
9287 #[gpui::test]
9288 fn test_auto_close_parens(cx: &mut TestAppContext) {
9289 let view = InputView::<EditorMode>::new(cx);
9290 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9291 setup_cursors(&mut cx, &view.input, "|");
9292 cx.update(|window, cx| {
9293 view.input.update(cx, |state, cx| {
9294 state.replace_text_in_range(None, "(", window, cx);
9295 });
9296 });
9297 assert_cursors(&mut cx, &view.input, "(|)");
9298 cx.update(|window, cx| {
9300 view.input.update(cx, |state, cx| {
9301 state.replace_text_in_range(None, "x", window, cx);
9302 });
9303 });
9304 assert_cursors(&mut cx, &view.input, "(x|)");
9305 }
9306
9307 #[gpui::test]
9308 fn test_auto_close_skip_over_closer(cx: &mut TestAppContext) {
9309 let view = InputView::<EditorMode>::new(cx);
9310 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9311 setup_cursors(&mut cx, &view.input, "(|)");
9312 cx.update(|window, cx| {
9313 view.input.update(cx, |state, cx| {
9314 state.replace_text_in_range(None, ")", window, cx);
9315 });
9316 });
9317 assert_cursors(&mut cx, &view.input, "()|");
9319 view.input.read_with(&cx, |state, _| {
9321 assert!(state.active_selection().is_empty());
9322 });
9323 }
9324
9325 #[gpui::test]
9326 fn test_auto_close_no_pair_inside_word(cx: &mut TestAppContext) {
9327 let view = InputView::<EditorMode>::new(cx);
9328 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9329 setup_cursors(&mut cx, &view.input, "don|");
9330 cx.update(|window, cx| {
9331 view.input.update(cx, |state, cx| {
9332 state.replace_text_in_range(None, "'", window, cx);
9333 });
9334 });
9335 assert_cursors(&mut cx, &view.input, "don'|");
9337 }
9338
9339 #[gpui::test]
9340 fn test_auto_close_quote_after_cjk(cx: &mut TestAppContext) {
9341 let view = InputView::<EditorMode>::new(cx);
9342 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9343 setup_cursors(&mut cx, &view.input, "中|");
9346 cx.update(|window, cx| {
9347 view.input.update(cx, |state, cx| {
9348 state.replace_text_in_range(None, "'", window, cx);
9349 });
9350 });
9351 assert_cursors(&mut cx, &view.input, "中'|");
9352 }
9353
9354 #[gpui::test]
9355 fn test_auto_close_closer_skips_at_string_end(cx: &mut TestAppContext) {
9356 let view = InputView::<EditorMode>::new(cx);
9357 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9358 setup_cursors(&mut cx, &view.input, "\"hello|\"");
9360 cx.update(|window, cx| {
9361 view.input.update(cx, |state, cx| {
9362 state.replace_text_in_range(None, "\"", window, cx);
9363 });
9364 });
9365 assert_cursors(&mut cx, &view.input, "\"hello\"|");
9366 view.input.read_with(&cx, |state, _| {
9367 assert!(state.active_selection().is_empty());
9368 });
9369 }
9370
9371 #[gpui::test]
9372 fn test_backspace_pair_with_cjk_prefix(cx: &mut TestAppContext) {
9373 let view = InputView::<EditorMode>::new(cx);
9374 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9375 setup_cursors(&mut cx, &view.input, "中(|)abc");
9377 cx.update(|window, cx| {
9378 view.input
9379 .update(cx, |state, cx| state.backspace(&Backspace, window, cx));
9380 });
9381 assert_cursors(&mut cx, &view.input, "中|abc");
9382 }
9383
9384 #[gpui::test]
9385 fn test_enter_split_respects_smart_indent_off(cx: &mut TestAppContext) {
9386 let view = InputView::<EditorMode>::new(cx);
9387 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9388 setup_cursors(&mut cx, &view.input, "{|}");
9389 cx.update(|window, cx| {
9390 view.input.update(cx, |state, cx| {
9391 state.set_smart_indent(false, window, cx);
9392 state.enter(
9393 &Enter {
9394 secondary: false,
9395 shift: false,
9396 },
9397 window,
9398 cx,
9399 );
9400 });
9401 });
9402 assert_cursors(&mut cx, &view.input, "{\n|}");
9404 }
9405
9406 #[gpui::test]
9407 fn test_enter_split_is_independent_of_auto_close(cx: &mut TestAppContext) {
9408 let view = InputView::<EditorMode>::new(cx);
9409 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9410 setup_cursors(&mut cx, &view.input, "{|}");
9411 cx.update(|window, cx| {
9412 view.input.update(cx, |state, cx| {
9413 state.set_auto_close(false, window, cx);
9414 state.enter(
9415 &Enter {
9416 secondary: false,
9417 shift: false,
9418 },
9419 window,
9420 cx,
9421 );
9422 });
9423 });
9424 assert_cursors(&mut cx, &view.input, "{\n |\n}");
9426 }
9427
9428 #[gpui::test]
9429 fn test_language_config_applies_before_render_and_on_language_change(cx: &mut TestAppContext) {
9430 use crate::input::{AutoClosingPair, set_language_config};
9431 let view = InputView::<EditorMode>::new(cx);
9432 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9433 cx.update(|window, cx| {
9434 set_language_config(
9435 "alpha",
9436 LanguageConfig::default().auto_closing_pairs([AutoClosingPair::new("«", "»")]),
9437 cx,
9438 );
9439 set_language_config(
9440 "beta",
9441 LanguageConfig::default().auto_closing_pairs([AutoClosingPair::new("‹", "›")]),
9442 cx,
9443 );
9444 let first = cx.new(|cx| EditorState::new(window, cx).language("ALPHA"));
9445 let second = cx.new(|cx| EditorState::new(window, cx).language("beta"));
9446 first.update(cx, |state, cx| {
9447 state.replace_text_in_range(None, "«", window, cx);
9448 assert_eq!(state.text().to_string(), "«»");
9449 state.set_value("", window, cx);
9450 state.set_smart_indent(false, window, cx);
9451 state.set_highlighter("beta", cx);
9452 assert!(!state.mode.is_smart_indent());
9453 state.replace_text_in_range(None, "‹", window, cx);
9454 assert_eq!(state.text().to_string(), "‹›");
9455 state.set_value("", window, cx);
9456 state.set_highlighter("unknown", cx);
9457 state.replace_text_in_range(None, "(", window, cx);
9458 assert_eq!(state.text().to_string(), "()");
9459 });
9460 second.update(cx, |state, cx| {
9461 state.replace_text_in_range(None, "‹", window, cx);
9462 assert_eq!(state.text().to_string(), "‹›");
9463 });
9464 });
9465 }
9466
9467 #[gpui::test]
9468 fn test_language_service_replacement_updates_syntax_without_render(cx: &mut TestAppContext) {
9469 use crate::input::{LanguageProvider, SyntaxContextProvider, set_language_provider};
9470 struct StringLanguages(Rc<Cell<usize>>);
9471 impl LanguageProvider for StringLanguages {
9472 fn syntax_context_provider(&self, _: &str) -> Option<Rc<dyn SyntaxContextProvider>> {
9473 self.0.set(self.0.get() + 1);
9474 Some(Rc::new(StringAllProvider))
9475 }
9476 }
9477 struct CodeLanguages;
9478 impl LanguageProvider for CodeLanguages {}
9479 let view = InputView::<EditorMode>::new(cx);
9480 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9481 cx.update(|window, cx| {
9482 let creations = Rc::new(Cell::new(0));
9483 set_language_provider(Rc::new(StringLanguages(creations.clone())), cx);
9484 view.input.update(cx, |state, cx| {
9485 state.replace_text_in_range(None, "(", window, cx);
9486 state.replace_text_in_range(None, "[", window, cx);
9487 assert_eq!(state.text().to_string(), "([");
9488 assert_eq!(
9489 creations.get(),
9490 1,
9491 "retain the document provider between edits"
9492 );
9493 });
9494 set_language_provider(Rc::new(CodeLanguages), cx);
9495 view.input.update(cx, |state, cx| {
9496 state.replace_text_in_range(None, "{", window, cx);
9497 assert_eq!(state.text().to_string(), "([{}");
9498 });
9499 });
9500 }
9501
9502 #[gpui::test]
9503 fn test_language_config_updates_existing_editors(cx: &mut TestAppContext) {
9504 use crate::input::{AutoClosingPair, set_language_config};
9505 let view = InputView::<EditorMode>::new(cx);
9506 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9507 setup_cursors(&mut cx, &view.input, "|");
9508 cx.update(|window, cx| {
9509 view.input.update(cx, |state, cx| {
9510 state.set_highlighter("CUSTOM", cx);
9511 state.set_auto_close(false, window, cx);
9512 state.set_smart_indent(false, window, cx);
9513 });
9514 set_language_config(
9515 "custom",
9516 LanguageConfig::default().auto_closing_pairs([AutoClosingPair::new("«", "»")]),
9517 cx,
9518 );
9519 set_language_config("other", LanguageConfig::default(), cx);
9521 view.input.update(cx, |state, cx| {
9522 assert!(!state.mode.is_auto_close());
9523 assert!(!state.mode.is_smart_indent());
9524 state.set_auto_close(true, window, cx);
9525 state.replace_text_in_range(None, "«", window, cx);
9526 });
9527 });
9528 assert_cursors(&mut cx, &view.input, "«|»");
9529 }
9530
9531 #[gpui::test]
9532 fn test_auto_close_before_is_language_configurable(cx: &mut TestAppContext) {
9533 let view = InputView::<EditorMode>::new(cx);
9534 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9535 setup_cursors(&mut cx, &view.input, "|word");
9536 cx.update(|window, cx| {
9537 view.input.update(cx, |state, cx| {
9538 state.replace_text_in_range(None, "(", window, cx)
9539 });
9540 });
9541 assert_cursors(&mut cx, &view.input, "(|word");
9542 setup_cursors(&mut cx, &view.input, "|word");
9543 cx.update(|window, cx| {
9544 view.input.update(cx, |state, cx| {
9545 crate::input::set_language_config(
9546 state.language_name(),
9547 LanguageConfig::default().auto_close_before("w"),
9548 cx,
9549 );
9550 state.replace_text_in_range(None, "(", window, cx);
9551 });
9552 });
9553 assert_cursors(&mut cx, &view.input, "(|)word");
9554 }
9555
9556 #[gpui::test]
9557 fn test_pair_context_restrictions_are_per_pair(cx: &mut TestAppContext) {
9558 use crate::input::{AutoClosingPair, SyntaxContext};
9559 let view = InputView::<EditorMode>::new(cx);
9560 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9561 setup_cursors(&mut cx, &view.input, "|");
9562 cx.update(|window, cx| {
9563 view.input.update(cx, |state, cx| {
9564 set_test_syntax_provider(std::rc::Rc::new(CommentAllProvider), cx);
9565 crate::input::set_language_config(
9566 state.language_name(),
9567 LanguageConfig::default().auto_closing_pairs([
9568 AutoClosingPair::new("(", ")").not_in([SyntaxContext::String]),
9569 AutoClosingPair::new("[", "]").not_in([SyntaxContext::Comment]),
9570 ]),
9571 cx,
9572 );
9573 state.replace_text_in_range(None, "(", window, cx);
9574 state.replace_text_in_range(None, "[", window, cx);
9575 });
9576 });
9577 assert_cursors(&mut cx, &view.input, "([|)");
9578 }
9579
9580 #[gpui::test]
9581 fn test_multichar_delimiters_insert_delete_and_skip(cx: &mut TestAppContext) {
9582 use crate::input::{AutoClosingPair, SyntaxContext};
9583 struct BlockComment;
9584 impl crate::input::SyntaxContextProvider for BlockComment {
9585 fn context_at(&self, text: &ropey::Rope, _: usize) -> SyntaxContext {
9586 if text.to_string().contains("/*") {
9587 SyntaxContext::Comment
9588 } else {
9589 SyntaxContext::Code
9590 }
9591 }
9592 }
9593 let view = InputView::<EditorMode>::new(cx);
9594 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9595 setup_cursors(&mut cx, &view.input, "|");
9596 cx.update(|window, cx| {
9597 view.input.update(cx, |state, cx| {
9598 set_test_syntax_provider(std::rc::Rc::new(BlockComment), cx);
9599 crate::input::set_language_config(
9600 state.language_name(),
9601 LanguageConfig::default()
9602 .auto_closing_pairs([AutoClosingPair::new("/*", "*/")
9603 .not_in([SyntaxContext::String, SyntaxContext::Comment])]),
9604 cx,
9605 );
9606 state.replace_text_in_range(None, "/", window, cx);
9607 state.replace_text_in_range(None, "*", window, cx);
9608 });
9609 });
9610 assert_cursors(&mut cx, &view.input, "/*|*/");
9611 cx.update(|window, cx| {
9612 view.input
9613 .update(cx, |state, cx| state.backspace(&Backspace, window, cx));
9614 });
9615 assert_cursors(&mut cx, &view.input, "|");
9616 cx.update(|window, cx| {
9617 view.input.update(cx, |state, cx| {
9618 state.undo(&Undo, window, cx);
9619 state.replace_text_in_range(None, "*", window, cx);
9620 state.replace_text_in_range(None, "/", window, cx);
9621 });
9622 });
9623 assert_cursors(&mut cx, &view.input, "/**/|");
9624 }
9625
9626 #[gpui::test]
9627 fn test_auto_closed_pairs_fallback_and_explicit_disable(cx: &mut TestAppContext) {
9628 use crate::input::BracketPair;
9629 let view = InputView::<EditorMode>::new(cx);
9630 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9631 setup_cursors(&mut cx, &view.input, "|");
9632 cx.update(|window, cx| {
9633 view.input.update(cx, |state, cx| {
9634 let mut rules = LanguageConfig::default().brackets([BracketPair::new("«", "»")]);
9635 rules.auto_closing_pairs = None;
9636 crate::input::set_language_config(state.language_name(), rules.clone(), cx);
9637 state.replace_text_in_range(None, "«", window, cx);
9638 crate::input::set_language_config(
9639 state.language_name(),
9640 rules.auto_closing_pairs([]),
9641 cx,
9642 );
9643 state.replace_text_in_range(None, "«", window, cx);
9644 });
9645 });
9646 assert_cursors(&mut cx, &view.input, "««|»");
9647 }
9648
9649 #[gpui::test]
9650 fn test_undo_open_composition_preserves_generated_pairs(cx: &mut TestAppContext) {
9651 use crate::input::{AutoClosingPair, SyntaxContext};
9652 let view = InputView::<EditorMode>::new(cx);
9653 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9654 setup_cursors(&mut cx, &view.input, "|");
9655 cx.update(|window, cx| {
9656 view.input.update(cx, |state, cx| {
9657 crate::input::set_language_config(
9658 state.language_name(),
9659 LanguageConfig::default()
9660 .auto_closing_pairs([
9661 AutoClosingPair::new("/*", "*/").not_in([SyntaxContext::Comment])
9662 ]),
9663 cx,
9664 );
9665 state.replace_text_in_range(None, "/", window, cx);
9666 state.replace_text_in_range(None, "*", window, cx);
9667 set_test_syntax_provider(std::rc::Rc::new(CommentAllProvider), cx);
9668 state.replace_and_mark_text_in_range(None, "x", Some(1..1), window, cx);
9669 state.undo(&Undo, window, cx);
9670 state.replace_text_in_range(None, "*", window, cx);
9671 state.replace_text_in_range(None, "/", window, cx);
9672 });
9673 });
9674 assert_cursors(&mut cx, &view.input, "/**/|");
9675 }
9676
9677 #[gpui::test]
9678 fn test_undo_does_not_promote_literal_comment_delimiters(cx: &mut TestAppContext) {
9679 use crate::input::{AutoClosingPair, SyntaxContext};
9680 let view = InputView::<EditorMode>::new(cx);
9681 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9682 setup_cursors(&mut cx, &view.input, "/*|*/");
9683 cx.update(|window, cx| {
9684 view.input.update(cx, |state, cx| {
9685 set_test_syntax_provider(std::rc::Rc::new(CommentAllProvider), cx);
9686 crate::input::set_language_config(
9687 state.language_name(),
9688 LanguageConfig::default()
9689 .auto_closing_pairs([
9690 AutoClosingPair::new("/*", "*/").not_in([SyntaxContext::Comment])
9691 ]),
9692 cx,
9693 );
9694 state.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
9695 state.undo(&Undo, window, cx);
9696 state.backspace(&Backspace, window, cx);
9697 });
9698 });
9699 assert_cursors(&mut cx, &view.input, "/|*/");
9700 }
9701
9702 #[gpui::test]
9703 fn test_multiple_generated_pairs_retain_their_identity(cx: &mut TestAppContext) {
9704 use crate::input::{AutoClosingPair, SyntaxContext};
9705 struct CommentScope;
9706 impl crate::input::SyntaxContextProvider for CommentScope {
9707 fn context_at(&self, text: &ropey::Rope, offset: usize) -> SyntaxContext {
9708 if text
9709 .to_string()
9710 .find("*/")
9711 .is_some_and(|end| offset < end + 2)
9712 {
9713 SyntaxContext::Comment
9714 } else {
9715 SyntaxContext::Code
9716 }
9717 }
9718 }
9719 let view = InputView::<EditorMode>::new(cx);
9720 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9721 setup_cursors(&mut cx, &view.input, "|");
9722 cx.update(|window, cx| {
9723 view.input.update(cx, |state, cx| {
9724 set_test_syntax_provider(std::rc::Rc::new(CommentScope), cx);
9725 crate::input::set_language_config(
9726 state.language_name(),
9727 LanguageConfig::default().auto_closing_pairs([
9728 AutoClosingPair::new("/*", "*/").not_in([SyntaxContext::Comment]),
9729 AutoClosingPair::new("(", ")").not_in([SyntaxContext::Comment]),
9730 ]),
9731 cx,
9732 );
9733 state.replace_text_in_range(None, "/", window, cx);
9734 state.replace_text_in_range(None, "*", window, cx);
9735 state.set_selected_range(4..4, cx);
9736 state.replace_text_in_range(None, "(", window, cx);
9737 state.set_selected_range(2..2, cx);
9738 state.replace_text_in_range(None, "*", window, cx);
9739 state.replace_text_in_range(None, "/", window, cx);
9740 });
9741 });
9742 assert_cursors(&mut cx, &view.input, "/**/|()");
9743 cx.update(|window, cx| {
9744 view.input.update(cx, |state, cx| {
9745 state.set_selected_range(2..2, cx);
9746 state.backspace(&Backspace, window, cx);
9747 state.undo(&Undo, window, cx);
9748 state.redo(&Redo, window, cx);
9749 });
9750 });
9751 assert_cursors(&mut cx, &view.input, "|()");
9752 }
9753
9754 #[gpui::test]
9755 fn test_replacing_generated_opener_invalidates_the_pair(cx: &mut TestAppContext) {
9756 use crate::input::{AutoClosingPair, SyntaxContext};
9757 let view = InputView::<EditorMode>::new(cx);
9758 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9759 setup_cursors(&mut cx, &view.input, "|");
9760 cx.update(|window, cx| {
9761 view.input.update(cx, |state, cx| {
9762 crate::input::set_language_config(
9763 state.language_name(),
9764 LanguageConfig::default()
9765 .auto_closing_pairs([
9766 AutoClosingPair::new("/*", "*/").not_in([SyntaxContext::Comment])
9767 ]),
9768 cx,
9769 );
9770 state.replace_text_in_range(None, "/", window, cx);
9771 state.replace_text_in_range(None, "*", window, cx);
9772 state.replace_text_in_range(Some(0..2), "//", window, cx);
9773 set_test_syntax_provider(std::rc::Rc::new(CommentAllProvider), cx);
9774 state.replace_text_in_range(None, "*", window, cx);
9775 });
9776 });
9777 assert_cursors(&mut cx, &view.input, "//*|*/");
9778 }
9779
9780 #[gpui::test]
9781 fn test_configuration_preserves_compiled_regex_options(cx: &mut TestAppContext) {
9782 use crate::input::IndentationRules;
9783 let view = InputView::<EditorMode>::new(cx);
9784 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9785 setup_cursors(&mut cx, &view.input, "BEGIN|");
9786 cx.update(|window, cx| {
9787 view.input.update(cx, |state, cx| {
9788 let rules = |insensitive| {
9789 LanguageConfig::default().indentation_rules(IndentationRules::new(
9790 regex::RegexBuilder::new("begin$")
9791 .case_insensitive(insensitive)
9792 .build()
9793 .unwrap(),
9794 regex::Regex::new("^end").unwrap(),
9795 ))
9796 };
9797 crate::input::set_language_config(state.language_name(), rules(false), cx);
9798 crate::input::set_language_config(state.language_name(), rules(true), cx);
9799 state.enter(
9800 &Enter {
9801 secondary: false,
9802 shift: false,
9803 },
9804 window,
9805 cx,
9806 );
9807 });
9808 });
9809 assert_cursors(&mut cx, &view.input, "BEGIN\n |");
9810 }
9811
9812 #[gpui::test]
9813 fn test_indent_patterns_are_applied_on_enter(cx: &mut TestAppContext) {
9814 use crate::input::IndentationRules;
9815 let view = InputView::<EditorMode>::new(cx);
9816 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9817 let rules = LanguageConfig::default().indentation_rules(IndentationRules::new(
9818 regex::Regex::new(r"\bbegin\s*$").unwrap(),
9819 regex::Regex::new(r"^\s*end\b").unwrap(),
9820 ));
9821 for (before, expected) in [("begin|", "begin\n |"), (" |end", " \n|end")] {
9822 setup_cursors(&mut cx, &view.input, before);
9823 cx.update(|window, cx| {
9824 view.input.update(cx, |state, cx| {
9825 crate::input::set_language_config(state.language_name(), rules.clone(), cx);
9826 state.enter(
9827 &Enter {
9828 secondary: false,
9829 shift: false,
9830 },
9831 window,
9832 cx,
9833 );
9834 });
9835 });
9836 assert_cursors(&mut cx, &view.input, expected);
9837 }
9838 }
9839
9840 #[gpui::test]
9841 fn test_custom_language_config(cx: &mut TestAppContext) {
9842 let view = InputView::<EditorMode>::new(cx);
9843 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9844 cx.update(|_window, cx| {
9845 view.input.update(cx, |state, cx| {
9846 crate::input::set_language_config(
9847 state.language_name(),
9848 crate::input::language_config::LanguageConfig::default()
9849 .brackets([crate::input::BracketPair::new("«", "»")])
9850 .auto_closing_pairs([crate::input::AutoClosingPair::new("«", "»")]),
9851 cx,
9852 );
9853 });
9854 });
9855 setup_cursors(&mut cx, &view.input, "|");
9857 cx.update(|window, cx| {
9858 view.input.update(cx, |state, cx| {
9859 state.replace_text_in_range(None, "«", window, cx);
9860 });
9861 });
9862 assert_cursors(&mut cx, &view.input, "«|»");
9863 cx.update(|window, cx| {
9865 view.input.update(cx, |state, cx| {
9866 state.set_value("\n", window, cx);
9867 state.set_selected_range(0..0, cx);
9868 state.replace_text_in_range(None, "(", window, cx);
9869 });
9870 });
9871 assert_cursors(&mut cx, &view.input, "(|");
9872 }
9873
9874 #[gpui::test]
9875 fn test_skip_records_no_history(cx: &mut TestAppContext) {
9876 let view = InputView::<EditorMode>::new(cx);
9877 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9878 setup_cursors(&mut cx, &view.input, "(a|)");
9879 cx.update(|window, cx| {
9880 view.input.update(cx, |state, cx| {
9881 state.replace_text_in_range(None, "b", window, cx);
9883 });
9884 });
9885 assert_cursors(&mut cx, &view.input, "(ab|)");
9886 cx.update(|window, cx| {
9887 view.input.update(cx, |state, cx| {
9888 state.replace_text_in_range(None, ")", window, cx);
9889 });
9890 });
9891 assert_cursors(&mut cx, &view.input, "(ab)|");
9893 cx.update(|window, cx| {
9895 view.input
9896 .update(cx, |state, cx| state.undo(&Undo, window, cx));
9897 });
9898 assert_cursors(&mut cx, &view.input, "(a|)");
9899 }
9900
9901 #[gpui::test]
9902 fn test_pair_insert_undo_removes_both(cx: &mut TestAppContext) {
9903 let view = InputView::<EditorMode>::new(cx);
9904 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9905 setup_cursors(&mut cx, &view.input, "|");
9906 cx.update(|window, cx| {
9907 view.input.update(cx, |state, cx| {
9908 state.replace_text_in_range(None, "(", window, cx);
9909 });
9910 });
9911 assert_cursors(&mut cx, &view.input, "(|)");
9912 cx.update(|window, cx| {
9914 view.input
9915 .update(cx, |state, cx| state.undo(&Undo, window, cx));
9916 });
9917 assert_cursors(&mut cx, &view.input, "|");
9918 }
9919
9920 #[gpui::test]
9921 fn test_escaped_quote_does_not_skip(cx: &mut TestAppContext) {
9922 let view = InputView::<EditorMode>::new(cx);
9923 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9924 setup_cursors(&mut cx, &view.input, "\"hello\\|\"");
9926 cx.update(|window, cx| {
9927 view.input.update(cx, |state, cx| {
9928 state.replace_text_in_range(None, "\"", window, cx);
9929 });
9930 });
9931 assert_cursors(&mut cx, &view.input, "\"hello\\\"|\"");
9932 }
9933
9934 #[gpui::test]
9935 fn test_even_backslashes_still_skip(cx: &mut TestAppContext) {
9936 let view = InputView::<EditorMode>::new(cx);
9937 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9938 setup_cursors(&mut cx, &view.input, "\"hello\\\\|\"");
9940 cx.update(|window, cx| {
9941 view.input.update(cx, |state, cx| {
9942 state.replace_text_in_range(None, "\"", window, cx);
9943 });
9944 });
9945 assert_cursors(&mut cx, &view.input, "\"hello\\\\\"|");
9946 }
9947
9948 #[gpui::test]
9949 fn test_editor_options_survive_configuration_replacement(cx: &mut TestAppContext) {
9950 use crate::input::{BracketPair, LanguageConfig};
9951
9952 let view = InputView::<EditorMode>::new(cx);
9953 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9954 cx.update(|window, cx| {
9955 view.input.update(cx, |state, cx| {
9956 state.set_smart_indent(false, window, cx);
9958 crate::input::set_language_config(
9959 state.language_name(),
9960 LanguageConfig::default().brackets([BracketPair::new("«", "»")]),
9961 cx,
9962 );
9963 });
9964 });
9965 view.input.read_with(&cx, |state, _| {
9966 assert!(!state.mode.is_smart_indent(), "editor option preserved");
9967 assert!(state.mode.is_auto_close());
9968 let pairs = &state.mode.language_config().unwrap().brackets;
9969 assert_eq!(pairs.len(), 1, "pairs follow configuration");
9970 assert_eq!(pairs[0].open.as_ref(), "«");
9971 });
9972 }
9973
9974 struct CommentAllProvider;
9975 impl crate::input::SyntaxContextProvider for CommentAllProvider {
9976 fn context_at(&self, _text: &ropey::Rope, _offset: usize) -> crate::input::SyntaxContext {
9977 crate::input::SyntaxContext::Comment
9978 }
9979 }
9980
9981 struct StringAllProvider;
9982 impl crate::input::SyntaxContextProvider for StringAllProvider {
9983 fn context_at(&self, _text: &ropey::Rope, _offset: usize) -> crate::input::SyntaxContext {
9984 crate::input::SyntaxContext::String
9985 }
9986 }
9987
9988 #[gpui::test]
9989 fn test_smart_indent_suppressed_in_strings(cx: &mut TestAppContext) {
9990 use std::rc::Rc;
9991
9992 let view = InputView::<EditorMode>::new(cx);
9993 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9994 cx.update(|_window, cx| {
9995 set_test_syntax_provider(Rc::new(StringAllProvider), cx);
9996 });
9997 setup_cursors(&mut cx, &view.input, " x = {|");
9999 cx.update(|window, cx| {
10000 view.input.update(cx, |state, cx| {
10001 state.enter(
10002 &Enter {
10003 secondary: false,
10004 shift: false,
10005 },
10006 window,
10007 cx,
10008 );
10009 });
10010 });
10011 assert_cursors(&mut cx, &view.input, " x = {\n |");
10012 }
10013
10014 #[gpui::test]
10015 fn test_split_suppressed_in_strings(cx: &mut TestAppContext) {
10016 use std::rc::Rc;
10017
10018 let view = InputView::<EditorMode>::new(cx);
10019 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10020 cx.update(|_window, cx| {
10021 set_test_syntax_provider(Rc::new(StringAllProvider), cx);
10022 });
10023 setup_cursors(&mut cx, &view.input, "{|}");
10025 cx.update(|window, cx| {
10026 view.input.update(cx, |state, cx| {
10027 state.enter(
10028 &Enter {
10029 secondary: false,
10030 shift: false,
10031 },
10032 window,
10033 cx,
10034 );
10035 });
10036 });
10037 assert_cursors(&mut cx, &view.input, "{\n|}");
10038 }
10039
10040 #[gpui::test]
10041 fn test_comment_context_disables_pairing(cx: &mut TestAppContext) {
10042 use std::rc::Rc;
10043
10044 let view = InputView::<EditorMode>::new(cx);
10045 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10046 cx.update(|_window, cx| {
10047 set_test_syntax_provider(Rc::new(CommentAllProvider), cx);
10048 });
10049 setup_cursors(&mut cx, &view.input, "|");
10050 cx.update(|window, cx| {
10051 view.input.update(cx, |state, cx| {
10052 state.replace_text_in_range(None, "(", window, cx);
10053 });
10054 });
10055 assert_cursors(&mut cx, &view.input, "(|");
10057 }
10058
10059 #[gpui::test]
10060 fn test_backspace_deletes_empty_pair(cx: &mut TestAppContext) {
10061 let view = InputView::<EditorMode>::new(cx);
10062 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10063 setup_cursors(&mut cx, &view.input, "{|}");
10064 cx.update(|window, cx| {
10065 view.input
10066 .update(cx, |state, cx| state.backspace(&Backspace, window, cx));
10067 });
10068 assert_cursors(&mut cx, &view.input, "|");
10069 }
10070
10071 #[gpui::test]
10072 fn test_backspace_keeps_closer_with_content(cx: &mut TestAppContext) {
10073 let view = InputView::<EditorMode>::new(cx);
10074 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10075 setup_cursors(&mut cx, &view.input, "{d|}");
10076 cx.update(|window, cx| {
10077 view.input
10078 .update(cx, |state, cx| state.backspace(&Backspace, window, cx));
10079 });
10080 assert_cursors(&mut cx, &view.input, "{|}");
10082 }
10083
10084 #[gpui::test]
10085 fn test_enter_splits_brackets(cx: &mut TestAppContext) {
10086 let view = InputView::<EditorMode>::new(cx);
10087 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10088 setup_cursors(&mut cx, &view.input, "{|}");
10089 cx.update(|window, cx| {
10090 view.input.update(cx, |state, cx| {
10091 state.enter(
10092 &Enter {
10093 secondary: false,
10094 shift: false,
10095 },
10096 window,
10097 cx,
10098 );
10099 });
10100 });
10101 assert_cursors(&mut cx, &view.input, "{\n |\n}");
10102 view.input.read_with(&cx, |state, _| {
10104 assert!(state.active_selection().is_empty());
10105 });
10106 cx.update(|window, cx| {
10107 view.input.update(cx, |state, cx| {
10108 state.replace_text_in_range(None, "x", window, cx);
10109 });
10110 });
10111 assert_cursors(&mut cx, &view.input, "{\n x|\n}");
10112 }
10113
10114 #[gpui::test]
10115 fn test_enter_smart_indent_after_brace(cx: &mut TestAppContext) {
10116 let view = InputView::<EditorMode>::new(cx);
10117 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10118 setup_cursors(&mut cx, &view.input, "fn main() {|");
10119 cx.update(|window, cx| {
10120 view.input.update(cx, |state, cx| {
10121 state.enter(
10122 &Enter {
10123 secondary: false,
10124 shift: false,
10125 },
10126 window,
10127 cx,
10128 );
10129 });
10130 });
10131 assert_cursors(&mut cx, &view.input, "fn main() {\n |");
10132 }
10133
10134 #[gpui::test]
10135 fn test_block_indent_tracks_all_preceding_edits(cx: &mut TestAppContext) {
10136 let view = multi_line(cx);
10137 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10138 setup_cursors(&mut cx, &view.input, "|ab\n|cd\n|ef");
10139 cx.update(|window, cx| {
10140 view.input
10141 .update(cx, |state, cx| state.indent(true, window, cx));
10142 });
10143 assert_cursors(&mut cx, &view.input, " |ab\n |cd\n |ef");
10144 cx.update(|window, cx| {
10145 view.input
10146 .update(cx, |state, cx| state.outdent(true, window, cx));
10147 });
10148 assert_cursors(&mut cx, &view.input, "|ab\n|cd\n|ef");
10149 }
10150
10151 #[gpui::test]
10152 fn test_block_outdent_clamps_cursor_inside_indent(cx: &mut TestAppContext) {
10153 let view = multi_line(cx);
10154 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10155 setup_cursors(&mut cx, &view.input, "ab\n | cd");
10156 cx.update(|window, cx| {
10157 view.input
10158 .update(cx, |state, cx| state.outdent(true, window, cx));
10159 });
10160 assert_cursors(&mut cx, &view.input, "ab\n|cd");
10161 }
10162
10163 #[gpui::test]
10164 fn test_ime_restores_original_selection(cx: &mut TestAppContext) {
10165 let view = InputView::build(cx, |state| state);
10166 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10167 cx.update(|window, cx| {
10168 view.input.update(cx, |state, cx| {
10169 state.set_value("abc", window, cx);
10170 state.set_selected_range(1..2, cx);
10171 state.replace_and_mark_text_in_range(None, "ni", None, window, cx);
10172 state.replace_text_in_range(None, "你", window, cx);
10173 state.undo(&Undo, window, cx);
10174 assert_eq!(state.value(), "abc");
10175 assert_eq!(state.selected_range(), 1..2);
10176 state.redo(&Redo, window, cx);
10177 assert_eq!(state.selected_range(), 4..4);
10178 });
10179 });
10180 }
10181
10182 #[gpui::test]
10183 fn test_noop_does_not_change_redo_selection(cx: &mut TestAppContext) {
10184 let view = multi_line(cx);
10185 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10186 cx.update(|window, cx| {
10187 view.input.update(cx, |state, cx| {
10188 state.set_value("abc", window, cx);
10189 state.set_selected_range(1..1, cx);
10190 state.replace_text_in_range(None, "X", window, cx);
10191 state.set_selected_range(0..0, cx);
10192 state.replace_text_in_range(None, "", window, cx);
10193 state.undo(&Undo, window, cx);
10194 state.redo(&Redo, window, cx);
10195 assert_eq!(state.value(), "aXbc");
10196 assert_eq!(state.selected_range(), 2..2);
10197 });
10198 });
10199 }
10200
10201 #[gpui::test]
10202 fn test_multi_cursor_insert_text(cx: &mut TestAppContext) {
10203 let view = multi_line(cx);
10204 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10205 let input = view.input;
10206
10207 setup_cursors(&mut cx, &input, "|hello |world|");
10208 cx.update(|window, cx| {
10209 input.update(cx, |state, cx| {
10210 state.replace_text_in_range(None, ">>>", window, cx);
10211 });
10212 });
10213 assert_cursors(&mut cx, &input, ">>>|hello >>>|world>>>|");
10214 }
10215
10216 #[gpui::test]
10217 fn test_multi_cursor_delete_backward(cx: &mut TestAppContext) {
10218 let view = multi_line(cx);
10219 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10220 let input = view.input;
10221
10222 setup_cursors(&mut cx, &input, "|islands| cars|");
10223 cx.update(|window, cx| {
10224 input.update(cx, |state, cx| {
10225 state.backspace(&Backspace, window, cx);
10226 });
10227 });
10228 assert_cursors(&mut cx, &input, "|island| car|");
10230 }
10231
10232 #[gpui::test]
10233 fn test_multi_cursor_delete_forward_merges(cx: &mut TestAppContext) {
10234 let view = multi_line(cx);
10235 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10236 let input = view.input;
10237
10238 setup_cursors(&mut cx, &input, "hello| |world");
10239 cx.update(|window, cx| {
10240 input.update(cx, |state, cx| {
10241 state.delete(&Delete, window, cx);
10242 });
10243 });
10244 assert_cursors(&mut cx, &input, "hello|orld");
10246 }
10247
10248 #[gpui::test]
10249 fn test_multi_cursor_multiline_insert_and_delete(cx: &mut TestAppContext) {
10250 let view = multi_line(cx);
10251 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10252 let input = view.input;
10253
10254 setup_cursors(&mut cx, &input, "|1\n|2\n|3");
10255 cx.update(|window, cx| {
10256 input.update(cx, |state, cx| {
10257 state.replace_text_in_range(None, "a", window, cx);
10258 });
10259 });
10260 assert_cursors(&mut cx, &input, "a|1\na|2\na|3");
10261
10262 input.read_with(&cx, |state, _| {
10264 assert_eq!(state.undo_manager.undo_count(), 1);
10265 });
10266
10267 cx.update(|window, cx| {
10268 input.update(cx, |state, cx| {
10269 state.backspace(&Backspace, window, cx);
10270 });
10271 });
10272 assert_cursors(&mut cx, &input, "|1\n|2\n|3");
10273 }
10274
10275 #[gpui::test]
10276 fn test_add_cursor_below_preserves_column(cx: &mut TestAppContext) {
10277 let view = multi_line(cx);
10278 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10279 let input = view.input;
10280
10281 setup_cursors(&mut cx, &input, "ab|cd\nabcd");
10282 cx.update(|window, cx| {
10283 input.update(cx, |state, cx| {
10284 state.add_cursor_below(&AddCursorBelow, window, cx);
10285 });
10286 });
10287 assert_cursors(&mut cx, &input, "ab|cd\nab|cd");
10288 }
10289
10290 #[gpui::test]
10291 fn test_add_cursor_at_rejects_duplicates(cx: &mut TestAppContext) {
10292 let view = multi_line(cx);
10293 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10294 let input = view.input;
10295
10296 setup_cursors(&mut cx, &input, "he|llo");
10297 cx.update(|_, cx| {
10298 input.update(cx, |state, cx| {
10299 state.add_cursor_at(2, cx);
10301 assert_eq!(state.selections.len(), 1);
10302 state.add_cursor_at(4, cx);
10304 assert_eq!(state.selections.len(), 2);
10305 });
10306 });
10307 }
10308
10309 #[gpui::test]
10310 fn test_multi_cursor_undo_redo_restores_selections(cx: &mut TestAppContext) {
10311 let view = multi_line(cx);
10312 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10313 let input = view.input;
10314
10315 setup_cursors(&mut cx, &input, "|1\n|2\n|3");
10316 cx.update(|window, cx| {
10317 input.update(cx, |state, cx| {
10318 state.replace_text_in_range(None, "a", window, cx);
10319 });
10320 });
10321 assert_cursors(&mut cx, &input, "a|1\na|2\na|3");
10322
10323 cx.update(|window, cx| {
10324 input.update(cx, |state, cx| {
10325 state.undo(&Undo, window, cx);
10326 });
10327 });
10328 assert_cursors(&mut cx, &input, "|1\n|2\n|3");
10329
10330 cx.update(|window, cx| {
10331 input.update(cx, |state, cx| {
10332 state.redo(&Redo, window, cx);
10333 });
10334 });
10335 assert_cursors(&mut cx, &input, "a|1\na|2\na|3");
10336 }
10337
10338 #[gpui::test]
10339 fn test_multi_cursor_undo_redo_different_line_lengths(cx: &mut TestAppContext) {
10340 let view = multi_line(cx);
10341 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10342 let input = view.input;
10343
10344 setup_cursors(&mut cx, &input, "abc123|\nabc12345|\nabc1234567|");
10345 cx.update(|window, cx| {
10346 input.update(cx, |state, cx| {
10347 state.replace_text_in_range(None, "a", window, cx);
10348 });
10349 });
10350 assert_cursors(&mut cx, &input, "abc123a|\nabc12345a|\nabc1234567a|");
10351
10352 cx.update(|window, cx| {
10353 input.update(cx, |state, cx| {
10354 state.undo(&Undo, window, cx);
10355 });
10356 });
10357 assert_cursors(&mut cx, &input, "abc123|\nabc12345|\nabc1234567|");
10358
10359 cx.update(|window, cx| {
10360 input.update(cx, |state, cx| {
10361 state.redo(&Redo, window, cx);
10362 });
10363 });
10364 assert_cursors(&mut cx, &input, "abc123a|\nabc12345a|\nabc1234567a|");
10365 }
10366
10367 #[gpui::test]
10368 fn test_multi_cursor_undo_multiple_inserts(cx: &mut TestAppContext) {
10369 let view = multi_line(cx);
10370 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10371 let input = view.input;
10372
10373 setup_cursors(&mut cx, &input, "|1\n|2\n|3");
10374 for ch in ['a', 'b', 'c'] {
10375 cx.update(|window, cx| {
10376 input.update(cx, |state, cx| {
10377 state.replace_text_in_range(None, &ch.to_string(), window, cx);
10378 });
10379 });
10380 }
10381 assert_cursors(&mut cx, &input, "abc|1\nabc|2\nabc|3");
10382
10383 cx.update(|window, cx| {
10385 input.update(cx, |state, cx| {
10386 assert_eq!(state.undo_manager.undo_count(), 1);
10387 state.undo(&Undo, window, cx);
10388 });
10389 });
10390 assert_cursors(&mut cx, &input, "|1\n|2\n|3");
10391
10392 cx.update(|window, cx| {
10393 input.update(cx, |state, cx| {
10394 state.redo(&Redo, window, cx);
10395 });
10396 });
10397 assert_cursors(&mut cx, &input, "abc|1\nabc|2\nabc|3");
10398 }
10399
10400 #[gpui::test]
10401 fn test_multi_cursor_backspace_run_is_one_undo(cx: &mut TestAppContext) {
10402 let view = multi_line(cx);
10403 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10404 let input = view.input;
10405
10406 setup_cursors(&mut cx, &input, "abc|1\nabc|2\nabc|3");
10407 for _ in 0..3 {
10408 cx.update(|window, cx| {
10409 input.update(cx, |state, cx| {
10410 state.backspace(&Backspace, window, cx);
10411 });
10412 });
10413 }
10414 assert_cursors(&mut cx, &input, "|1\n|2\n|3");
10415
10416 cx.update(|window, cx| {
10417 input.update(cx, |state, cx| {
10418 assert_eq!(state.undo_manager.undo_count(), 1);
10419 state.undo(&Undo, window, cx);
10420 });
10421 });
10422 assert_cursors(&mut cx, &input, "abc|1\nabc|2\nabc|3");
10423 }
10424
10425 #[gpui::test]
10426 fn test_adding_a_cursor_splits_the_typing_gesture(cx: &mut TestAppContext) {
10427 let view = multi_line(cx);
10428 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10429 let input = view.input;
10430
10431 setup_cursors(&mut cx, &input, "|1\n2\n3");
10432 cx.update(|window, cx| {
10433 input.update(cx, |state, cx| {
10434 state.replace_text_in_range(None, "a", window, cx);
10435 state.add_cursor_below(&AddCursorBelow, window, cx);
10436 state.replace_text_in_range(None, "b", window, cx);
10437 });
10438 });
10439 assert_cursors(&mut cx, &input, "ab|1\n2b|\n3");
10440
10441 cx.update(|window, cx| {
10443 input.update(cx, |state, cx| {
10444 state.undo(&Undo, window, cx);
10445 });
10446 });
10447 assert_cursors(&mut cx, &input, "a|1\n2|\n3");
10448 }
10449
10450 #[gpui::test]
10451 fn test_multi_cursor_indent_is_one_undo(cx: &mut TestAppContext) {
10452 let view = multi_line(cx);
10453 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10454 let input = view.input;
10455
10456 setup_cursors(&mut cx, &input, "|1\n|2\n|3");
10457 cx.update(|window, cx| {
10458 input.update(cx, |state, cx| {
10459 state.indent_inline(&IndentInline, window, cx);
10460 assert_eq!(state.undo_manager.undo_count(), 1);
10461 state.undo(&Undo, window, cx);
10462 });
10463 });
10464 assert_cursors(&mut cx, &input, "|1\n|2\n|3");
10465 }
10466
10467 #[gpui::test]
10468 fn test_multi_cursor_indent_then_outdent_roundtrips(cx: &mut TestAppContext) {
10469 let view = multi_line(cx);
10470 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10471 let input = view.input;
10472
10473 setup_cursors(&mut cx, &input, "|1\n|2\n|3");
10475 cx.update(|window, cx| {
10476 input.update(cx, |state, cx| {
10477 state.indent(false, window, cx);
10478 });
10479 });
10480 assert_cursors(&mut cx, &input, " |1\n |2\n |3");
10481
10482 cx.update(|window, cx| {
10483 input.update(cx, |state, cx| {
10484 state.outdent(false, window, cx);
10485 });
10486 });
10487 assert_cursors(&mut cx, &input, "|1\n|2\n|3");
10488 }
10489
10490 #[gpui::test]
10491 fn test_inline_outdent_only_removes_line_indentation(cx: &mut TestAppContext) {
10492 let view = multi_line(cx);
10493 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10494 let input = view.input;
10495
10496 setup_cursors(&mut cx, &input, "1|2\n1|2");
10499 cx.update(|window, cx| {
10500 input.update(cx, |state, cx| {
10501 state.indent(false, window, cx);
10502 });
10503 });
10504 assert_cursors(&mut cx, &input, "1 |2\n1 |2");
10505
10506 cx.update(|window, cx| {
10507 input.update(cx, |state, cx| {
10508 state.outdent(false, window, cx);
10509 });
10510 });
10511 assert_cursors(&mut cx, &input, "1 |2\n1 |2");
10512
10513 setup_cursors(&mut cx, &input, " 1|2\n 1|2");
10515 cx.update(|window, cx| {
10516 input.update(cx, |state, cx| {
10517 state.outdent(false, window, cx);
10518 });
10519 });
10520 assert_cursors(&mut cx, &input, "1|2\n1|2");
10521 }
10522
10523 #[gpui::test]
10524 fn test_readonly_multi_cursor_commands_leave_state_unchanged(cx: &mut TestAppContext) {
10525 let view = multi_line(cx);
10526 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10527 let input = view.input;
10528
10529 setup_cursors(&mut cx, &input, "a|b\nc|d");
10530 cx.update(|window, cx| {
10531 input.update(cx, |state, cx| {
10532 let before: Vec<_> = state.selections.iter().copied().collect();
10533 state.set_readonly(true, cx);
10534 state.backspace(&Backspace, window, cx);
10535 state.indent_inline(&IndentInline, window, cx);
10536
10537 assert_eq!(state.value(), "ab\ncd\n");
10538 assert_eq!(state.selections.iter().copied().collect::<Vec<_>>(), before);
10539 });
10540 });
10541 }
10542
10543 #[gpui::test]
10544 fn test_multi_cursor_edit_preserves_the_active_cursor(cx: &mut TestAppContext) {
10545 let view = multi_line(cx);
10546 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10547 let input = view.input;
10548
10549 setup_cursors(&mut cx, &input, "x|\n|y");
10550 cx.update(|window, cx| {
10551 input.update(cx, |state, cx| {
10552 let mut selections: Vec<_> = state.selections.iter().copied().collect();
10553 selections.swap(0, 1);
10554 state.selections.replace_all(selections);
10555 let active_id = state.active_selection().id;
10556 state.replace_text_in_range(None, "!", window, cx);
10557 assert_eq!(state.active_selection().id, active_id);
10558 });
10559 });
10560 }
10561
10562 #[gpui::test]
10563 fn test_block_indent_outdent_with_selection(cx: &mut TestAppContext) {
10564 let view = multi_line(cx);
10565 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10566 let input = view.input;
10567
10568 cx.update(|window, cx| {
10569 input.update(cx, |state, cx| {
10570 state.set_value("line1\nline2\nline3", window, cx);
10571 let id = state.selections.generate_id();
10572 state
10573 .selections
10574 .replace_all(vec![CursorSelection::new(id, 0, 17)]);
10575 cx.notify();
10576 });
10577 });
10578
10579 cx.update(|window, cx| {
10580 input.update(cx, |state, cx| {
10581 state.indent(true, window, cx);
10582 });
10583 });
10584 input.read_with(&cx, |state, _| {
10585 assert_eq!(state.text.to_string(), " line1\n line2\n line3");
10586 });
10587
10588 cx.update(|window, cx| {
10589 input.update(cx, |state, cx| {
10590 state.outdent(true, window, cx);
10591 });
10592 });
10593 input.read_with(&cx, |state, _| {
10594 assert_eq!(state.text.to_string(), "line1\nline2\nline3");
10595 });
10596 }
10597
10598 #[gpui::test]
10599 fn test_multi_cursor_word_movement(cx: &mut TestAppContext) {
10600 let view = multi_line(cx);
10601 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10602 let input = view.input;
10603
10604 setup_cursors(
10605 &mut cx,
10606 &input,
10607 "on|e two three\none t|wo three\non|e two three",
10608 );
10609
10610 cx.update(|window, cx| {
10611 input.update(cx, |state, cx| {
10612 state.move_to_next_word(&MoveToNextWord, window, cx);
10613 });
10614 });
10615 assert_cursors(
10616 &mut cx,
10617 &input,
10618 "one| two three\none two| three\none| two three",
10619 );
10620
10621 cx.update(|window, cx| {
10622 input.update(cx, |state, cx| {
10623 state.move_to_previous_word(&MoveToPreviousWord, window, cx);
10624 });
10625 });
10626 assert_cursors(
10627 &mut cx,
10628 &input,
10629 "|one two three\none |two three\n|one two three",
10630 );
10631
10632 cx.update(|window, cx| {
10634 input.update(cx, |state, cx| {
10635 state.move_to_end(&MoveToEnd, window, cx);
10636 });
10637 });
10638 input.read_with(&cx, |state, _| {
10639 let cursors: Vec<usize> = state.selections.iter().map(|s| s.cursor_offset()).collect();
10640 assert_eq!(cursors, vec![state.text.len()]);
10641 });
10642
10643 cx.update(|window, cx| {
10644 input.update(cx, |state, cx| {
10645 state.move_to_start(&MoveToStart, window, cx);
10646 });
10647 });
10648 input.read_with(&cx, |state, _| {
10649 let cursors: Vec<usize> = state.selections.iter().map(|s| s.cursor_offset()).collect();
10650 assert_eq!(cursors, vec![0]);
10651 });
10652 }
10653
10654 #[gpui::test]
10655 fn test_multi_cursor_selection_commands(cx: &mut TestAppContext) {
10656 let view = multi_line(cx);
10657 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10658 let input = view.input;
10659
10660 setup_cursors(
10661 &mut cx,
10662 &input,
10663 "on|e two three\none t|wo three\non|e two three",
10664 );
10665 cx.update(|window, cx| {
10666 input.update(cx, |state, cx| {
10667 state.select_to_start_of_line(&SelectToStartOfLine, window, cx);
10668 });
10669 });
10670 assert_cursors(
10671 &mut cx,
10672 &input,
10673 "|one two three\n|one two three\n|one two three",
10674 );
10675
10676 setup_cursors(
10678 &mut cx,
10679 &input,
10680 "on|e two three\none t|wo three\non|e two three",
10681 );
10682 cx.update(|window, cx| {
10683 input.update(cx, |state, cx| {
10684 state.select_to_start(&SelectToStart, window, cx);
10685 });
10686 });
10687 input.read_with(&cx, |state, _| {
10688 let cursors: Vec<usize> = state.selections.iter().map(|s| s.cursor_offset()).collect();
10689 assert_eq!(cursors, vec![0]);
10690 });
10691 }
10692
10693 #[gpui::test]
10694 fn test_multi_cursor_replace_selection(cx: &mut TestAppContext) {
10695 let view = multi_line(cx);
10696 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10697 let input = view.input;
10698
10699 setup_cursors(&mut cx, &input, "|a\n|b\n|c");
10700 cx.update(|window, cx| {
10701 input.update(cx, |state, cx| {
10702 state.select_right(&SelectRight, window, cx);
10703 });
10704 });
10705 input.read_with(&cx, |state, _| {
10706 let ranges: Vec<_> = state.selections.iter().map(|s| (s.start, s.end)).collect();
10707 assert_eq!(ranges, vec![(0, 1), (2, 3), (4, 5)]);
10708 });
10709
10710 cx.update(|window, cx| {
10711 input.update(cx, |state, cx| {
10712 state.replace_text_in_range(None, "x", window, cx);
10713 });
10714 });
10715 assert_cursors(&mut cx, &input, "x|\nx|\nx|");
10716 }
10717
10718 #[gpui::test]
10719 fn test_multi_cursor_escape_collapses(cx: &mut TestAppContext) {
10720 let view = multi_line(cx);
10721 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10722 let input = view.input;
10723
10724 setup_cursors(&mut cx, &input, "|a\n|b\n|c");
10725 cx.update(|window, cx| {
10726 input.update(cx, |state, cx| {
10727 state.escape(&Escape, window, cx);
10728 });
10729 });
10730 input.read_with(&cx, |state, _| {
10731 assert_eq!(state.selections.len(), 1);
10732 });
10733 }
10734
10735 #[gpui::test]
10736 fn test_build_columnar_selection(cx: &mut TestAppContext) {
10737 let view = multi_line(cx);
10738 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10739 let input = view.input;
10740
10741 setup_cursors(&mut cx, &input, "abcd\nabcd\nabcd");
10742 cx.update(|_, cx| {
10743 input.update(cx, |state, cx| {
10744 state.build_columnar_selection(
10746 ColumnarPoint::new(1, 0),
10747 ColumnarPoint::new(13, 0),
10748 cx,
10749 );
10750 let ranges: Vec<_> = state.selections.iter().map(|s| (s.start, s.end)).collect();
10751 assert_eq!(ranges, vec![(1, 3), (6, 8), (11, 13)]);
10752 });
10753 });
10754 }
10755
10756 #[gpui::test]
10757 fn test_columnar_selection_keeps_width_over_short_rows(cx: &mut TestAppContext) {
10758 let view = multi_line(cx);
10759 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10760 let input = view.input;
10761
10762 setup_cursors(&mut cx, &input, "abcdef\nab\nabcdef");
10763 cx.update(|_, cx| {
10764 input.update(cx, |state, cx| {
10765 state.build_columnar_selection(
10768 ColumnarPoint::new(1, 0),
10769 ColumnarPoint::new(9, 3),
10770 cx,
10771 );
10772 let ranges: Vec<_> = state.selections.iter().map(|s| (s.start, s.end)).collect();
10773 assert_eq!(ranges, vec![(1, 5), (8, 9)]);
10774 });
10775 });
10776 }
10777
10778 #[gpui::test]
10779 fn test_multi_cursor_paste_distributes_lines(cx: &mut TestAppContext) {
10780 let view = multi_line(cx);
10781 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10782 let input = view.input;
10783
10784 setup_cursors(&mut cx, &input, "|1\n|2\n|3");
10785 cx.update(|_, cx| {
10786 cx.write_to_clipboard(ClipboardItem::new_string("x\ny\nz".to_string()));
10787 });
10788 cx.update(|window, cx| {
10789 input.update(cx, |state, cx| {
10790 state.paste(&Paste, window, cx);
10791 });
10792 });
10793 assert_cursors(&mut cx, &input, "x|1\ny|2\nz|3");
10795 }
10796
10797 #[gpui::test]
10798 fn test_paste_without_text_leaves_the_selection_alone(cx: &mut TestAppContext) {
10799 use gpui::{ClipboardEntry, Image, ImageFormat};
10800
10801 let input_view = InputView::build(cx, |state| state);
10802 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
10803 let input = input_view.input;
10804
10805 cx.update(|window, cx| {
10806 input.update(cx, |state, cx| {
10807 state.set_value("hello world", window, cx);
10808 state.select_all(window, cx);
10809 cx.write_to_clipboard(ClipboardItem {
10810 entries: vec![ClipboardEntry::Image(Image::from_bytes(
10811 ImageFormat::Png,
10812 vec![0x89, b'P', b'N', b'G'],
10813 ))],
10814 });
10815
10816 state.paste(&Paste, window, cx);
10819 assert_eq!(state.value(), "hello world");
10820 assert_eq!(state.selected_range(), 0..11);
10821 });
10822 });
10823 }
10824
10825 #[gpui::test]
10826 fn test_paste_target_tracks_edits_and_selections(cx: &mut TestAppContext) {
10827 let view = multi_line(cx);
10828 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
10829 let input = view.input;
10830
10831 setup_cursors(&mut cx, &input, "ab|c");
10832 let target = input.read_with(&cx, |state, _| state.paste_target());
10833 assert_eq!(
10835 input.read_with(&cx, |state, _| state.paste_target()),
10836 target
10837 );
10838
10839 cx.update(|_, cx| {
10841 input.update(cx, |state, cx| {
10842 let id = state.selections.generate_id();
10843 state
10844 .selections
10845 .replace_all(vec![CursorSelection::new(id, 1, 1)]);
10846 cx.notify();
10847 });
10848 });
10849 let moved = input.read_with(&cx, |state, _| state.paste_target());
10850 assert_ne!(moved, target);
10851
10852 cx.update(|window, cx| {
10854 input.update(cx, |state, cx| {
10855 state.replace_text_in_range_silent(None, "x", window, cx);
10856 let id = state.selections.generate_id();
10857 state
10858 .selections
10859 .replace_all(vec![CursorSelection::new(id, 1, 1)]);
10860 });
10861 });
10862 assert_ne!(input.read_with(&cx, |state, _| state.paste_target()), moved);
10863 }
10864
10865 #[gpui::test]
10866 fn test_ime_candidate_bounds_fallback_before_repaint(cx: &mut TestAppContext) {
10867 let input_view = InputView::build_textarea(cx, |state| {
10868 state.default_value("这是一段已经输入的中文文字")
10869 });
10870 let mut visual = VisualTestContext::from_window(input_view.window_handle.into(), cx);
10871 visual.update(|window, cx| window.draw(cx).clear(cx));
10872
10873 visual.update(|window, cx| {
10874 input_view.input.update(cx, |state, cx| {
10875 state.focus(window, cx);
10876 let len = state.text.len();
10877 state.set_cursor_to(len);
10878 });
10879 });
10880 visual.update(|window, cx| window.draw(cx).clear(cx));
10881
10882 visual.update(|window, cx| {
10883 input_view.input.update(cx, |state, cx| {
10884 let bounds = state.text_bounds().unwrap_or_default();
10885 let before = state.selected_text_range(false, window, cx).unwrap().range;
10886 let previous = state
10887 .bounds_for_range(before.end..before.end, bounds, window, cx)
10888 .unwrap();
10889
10890 state.replace_and_mark_text_in_range(None, "n", None, window, cx);
10891
10892 let after = state.selected_text_range(false, window, cx).unwrap().range;
10893 let pending = state
10894 .bounds_for_range(after.end..after.end, bounds, window, cx)
10895 .unwrap();
10896
10897 assert!(
10898 (pending.origin.x - previous.origin.x).abs() < px(40.0),
10899 "pending origin.x {:?} must remain close to previous origin.x {:?}",
10900 pending.origin.x,
10901 previous.origin.x
10902 );
10903
10904 let range_bounds = state
10905 .bounds_for_range(before.end..after.end, bounds, window, cx)
10906 .unwrap();
10907 assert!(
10908 (range_bounds.origin.x - previous.origin.x).abs() < px(40.0),
10909 "range_bounds origin.x {:?} must remain close to previous origin.x {:?}",
10910 range_bounds.origin.x,
10911 previous.origin.x
10912 );
10913 assert!(range_bounds.size.width >= px(0.0));
10914 });
10915 });
10916 }
10917}
10918
10919impl InputBaseState<crate::input::InputMode> {
10921 pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
10923 Self::new_in_mode(window, cx)
10924 }
10925
10926 pub fn step_by(mut self, f: impl Fn(f64, StepAction, &mut App) -> f64 + 'static) -> Self {
10941 self.number_step = Some(NumberStep::by_value(f));
10942 self
10943 }
10944
10945 pub fn masked(mut self, masked: bool) -> Self {
10947 self.masked = masked;
10948 self
10949 }
10950
10951 pub fn set_masked(&mut self, masked: bool, _: &mut Window, cx: &mut Context<Self>) {
10953 self.masked = masked;
10954 cx.notify();
10955 }
10956
10957 pub fn pattern(mut self, pattern: regex::Regex) -> Self {
10959 self.pattern = Some(pattern);
10960 self
10961 }
10962
10963 pub fn set_pattern(
10965 &mut self,
10966 pattern: regex::Regex,
10967 _window: &mut Window,
10968 _cx: &mut Context<Self>,
10969 ) {
10970 self.pattern = Some(pattern);
10971 }
10972
10973 pub fn validate(mut self, f: impl Fn(&str, &mut App) -> bool + 'static) -> Self {
10975 self.validate = Some(Box::new(f));
10976 self
10977 }
10978
10979 pub fn set_validator(
10980 &mut self,
10981 validate: impl Fn(&str, &mut App) -> bool + 'static,
10982 _cx: &mut Context<Self>,
10983 ) {
10984 self.validate = Some(Box::new(validate));
10985 }
10986
10987 pub fn step(mut self, step: impl Into<NumberStep>) -> Self {
10997 self.number_step = Some(step.into());
10998 self
10999 }
11000
11001 pub fn min(mut self, min: f64) -> Self {
11007 self.number_min = Some(min);
11008 self
11009 }
11010
11011 pub fn max(mut self, max: f64) -> Self {
11017 self.number_max = Some(max);
11018 self
11019 }
11020
11021 pub fn set_step(
11026 &mut self,
11027 step: impl Into<Option<NumberStep>>,
11028 _: &mut Window,
11029 _: &mut Context<Self>,
11030 ) {
11031 self.number_step = step.into();
11032 }
11033
11034 pub fn set_min(&mut self, min: Option<f64>, _: &mut Window, _: &mut Context<Self>) {
11036 self.number_min = min;
11037 }
11038
11039 pub fn set_max(&mut self, max: Option<f64>, _: &mut Window, _: &mut Context<Self>) {
11041 self.number_max = max;
11042 }
11043
11044 pub fn set_loading(&mut self, loading: bool, _: &mut Window, cx: &mut Context<Self>) {
11046 self.loading = loading;
11047 cx.notify();
11048 }
11049}
11050
11051impl<M: crate::input::MultiLineMode> InputBaseState<M> {
11054 pub fn searchable(mut self, searchable: bool) -> Self {
11062 self.searchable = searchable;
11063 self
11064 }
11065
11066 pub fn set_searchable(&mut self, searchable: bool, cx: &mut Context<Self>) {
11068 self.searchable = searchable;
11069 cx.notify();
11070 }
11071
11072 #[doc(hidden)]
11074 pub fn soft_wrap(mut self, wrap: bool) -> Self {
11075 self.soft_wrap = wrap;
11076 self
11077 }
11078
11079 pub fn set_soft_wrap(&mut self, wrap: bool, _: &mut Window, cx: &mut Context<Self>) {
11081 self.soft_wrap = wrap;
11082 if wrap {
11083 let wrap_width = self
11084 .last_layout
11085 .as_ref()
11086 .and_then(|b| b.wrap_width)
11087 .unwrap_or(self.input_bounds.size.width);
11088
11089 self.display_map.on_layout_changed(Some(wrap_width), cx);
11090
11091 let mut offset = self.scroll_handle.offset();
11093 offset.x = px(0.);
11094 self.scroll_handle.set_offset(offset);
11095 } else {
11096 self.display_map.on_layout_changed(None, cx);
11097 }
11098 cx.notify();
11099 }
11100
11101 #[doc(hidden)]
11103 pub fn wrapping_indent(mut self, wrapping_indent: WrappingIndent) -> Self {
11104 self.wrapping_indent = wrapping_indent;
11105 self
11106 }
11107
11108 pub fn set_wrapping_indent(
11110 &mut self,
11111 wrapping_indent: WrappingIndent,
11112 _: &mut Window,
11113 cx: &mut Context<Self>,
11114 ) {
11115 self.wrapping_indent = wrapping_indent;
11116 self.display_map.set_wrapping_indent(wrapping_indent, cx);
11117 cx.notify();
11118 }
11119}
11120
11121impl InputBaseState<crate::input::TextareaMode> {
11123 pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
11128 Self::new_in_mode(window, cx)
11129 }
11130
11131 pub fn set_auto_grow(&mut self, min_rows: usize, max_rows: usize, cx: &mut Context<Self>) {
11132 self.mode = LayoutMode::auto_grow(min_rows, max_rows.max(min_rows));
11133 self.mode.update_auto_grow(&self.display_map);
11134 cx.notify();
11135 }
11136
11137 #[doc(hidden)]
11144 pub fn rows(mut self, rows: usize) -> Self {
11145 match &mut self.mode {
11146 LayoutMode::PlainText { rows: r, .. } | LayoutMode::CodeEditor { rows: r, .. } => {
11147 *r = rows
11148 }
11149 LayoutMode::AutoGrow {
11150 max_rows: max_r,
11151 rows: r,
11152 ..
11153 } => {
11154 *r = rows;
11155 *max_r = rows;
11156 }
11157 }
11158 self
11159 }
11160
11161 pub fn set_rows(&mut self, rows: usize, cx: &mut Context<Self>) {
11162 match &mut self.mode {
11163 LayoutMode::PlainText { rows: value, .. }
11164 | LayoutMode::CodeEditor { rows: value, .. } => *value = rows,
11165 LayoutMode::AutoGrow {
11166 rows: value,
11167 max_rows,
11168 ..
11169 } => {
11170 *value = rows;
11171 *max_rows = rows;
11172 }
11173 }
11174 cx.notify();
11175 }
11176
11177 pub fn auto_grow(mut self, min_rows: usize, max_rows: usize) -> Self {
11179 self.mode = LayoutMode::auto_grow(min_rows, max_rows);
11180 self
11181 }
11182}
11183
11184impl InputBaseState<crate::input::EditorMode> {
11186 pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
11197 let mut state = Self::new_in_mode(window, cx);
11198 state.mode = LayoutMode::code_editor(super::EditorLanguage::new(cx));
11199 state.searchable = true;
11200 state
11201 }
11202
11203 pub fn language(mut self, language: impl Into<SharedString>) -> Self {
11207 if let LayoutMode::CodeEditor {
11208 language: l,
11209 highlighter,
11210 ..
11211 } = &mut self.mode
11212 {
11213 l.set_name(language.into());
11214 *highlighter.borrow_mut() = None;
11215 }
11216 self
11217 }
11218
11219 pub fn language_name(&self) -> SharedString {
11221 match &self.mode {
11222 LayoutMode::CodeEditor { language, .. } => language.name(),
11223 _ => SharedString::default(),
11224 }
11225 }
11226
11227 #[doc(hidden)]
11231 pub fn folding(mut self, folding: bool) -> Self {
11232 if let LayoutMode::CodeEditor { folding: f, .. } = &mut self.mode {
11233 *f = folding;
11234 }
11235 self
11236 }
11237
11238 pub fn set_folding(&mut self, folding: bool, _: &mut Window, cx: &mut Context<Self>) {
11242 if let LayoutMode::CodeEditor { folding: f, .. } = &mut self.mode {
11243 *f = folding;
11244 }
11245 if !folding {
11246 self.display_map.clear_folds();
11247 }
11248 cx.notify();
11249 }
11250
11251 pub fn unfold_at(&mut self, position: impl Into<Position>, cx: &mut Context<Self>) -> bool {
11264 let offset = self.text.position_to_offset(&position.into());
11265 let line = self.text.offset_to_point(offset).row;
11266 let covering: Vec<usize> = self
11269 .display_map
11270 .folded_ranges()
11271 .iter()
11272 .filter(|fold| line > fold.start_line && line < fold.end_line)
11273 .map(|fold| fold.start_line)
11274 .collect();
11275 if covering.is_empty() {
11276 return false;
11277 }
11278
11279 for start_line in covering {
11280 self.display_map.set_folded(start_line, false);
11281 }
11282 cx.notify();
11283 true
11284 }
11285
11286 #[doc(hidden)]
11288 pub fn line_number(mut self, line_number: bool) -> Self {
11289 if let LayoutMode::CodeEditor { line_number: l, .. } = &mut self.mode {
11290 *l = line_number;
11291 }
11292 self
11293 }
11294
11295 pub fn set_line_number(&mut self, line_number: bool, _: &mut Window, cx: &mut Context<Self>) {
11297 if let LayoutMode::CodeEditor { line_number: l, .. } = &mut self.mode {
11298 *l = line_number;
11299 }
11300 cx.notify();
11301 }
11302
11303 #[doc(hidden)]
11309 pub fn auto_close(mut self, auto_close: bool) -> Self {
11310 self.mode.set_auto_close(auto_close);
11311 self
11312 }
11313
11314 pub fn set_auto_close(&mut self, auto_close: bool, _: &mut Window, cx: &mut Context<Self>) {
11316 self.mode.set_auto_close(auto_close);
11317 cx.notify();
11318 }
11319
11320 #[doc(hidden)]
11326 pub fn smart_indent(mut self, smart_indent: bool) -> Self {
11327 self.mode.set_smart_indent(smart_indent);
11328 self
11329 }
11330
11331 pub fn set_smart_indent(&mut self, smart_indent: bool, _: &mut Window, cx: &mut Context<Self>) {
11333 self.mode.set_smart_indent(smart_indent);
11334 cx.notify();
11335 }
11336}