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, 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::{EditorScrollbar, EditorScrollbarSnapshot, TextElement},
29 kind::InputModeKind,
30 mask_pattern::normalize_number_input,
31 mode::LayoutMode,
32 undo_manager::{EditIntent, UndoManager},
33};
34use crate::actions::{SelectDown, SelectLeft, SelectRight, SelectUp};
35use crate::input::blink_cursor::CURSOR_WIDTH;
36use crate::input::movement::MoveDirection;
37use crate::input::{
38 InputExtras as _, Position, RopeExt as _, element::RIGHT_MARGIN, layout::LastLayout,
39};
40use crate::{AutoScroll, StepAction};
41
42pub(crate) enum ScrollPadding {
44 Minimal,
45 SurroundingLines,
46}
47
48#[derive(Action, Clone, PartialEq, Eq, Deserialize)]
49#[action(namespace = input, no_json)]
50pub struct Enter {
51 pub secondary: bool,
53 pub shift: bool,
55}
56
57impl Enter {
58 pub fn is_primary(action: &dyn Action) -> bool {
61 action.partial_eq(&Enter {
62 secondary: false,
63 shift: false,
64 }) || action.partial_eq(&Enter {
65 secondary: false,
66 shift: true,
67 })
68 }
69}
70
71actions!(
72 input,
73 [
74 ActivateToken,
75 Backspace,
76 Delete,
77 DeleteToBeginningOfLine,
78 DeleteToEndOfLine,
79 DeleteToPreviousWordStart,
80 DeleteToNextWordEnd,
81 Indent,
82 Outdent,
83 IndentInline,
84 OutdentInline,
85 MoveUp,
86 MoveDown,
87 MoveLeft,
88 MoveRight,
89 MoveHome,
90 MoveEnd,
91 MovePageUp,
92 MovePageDown,
93 AddCursorAbove,
94 AddCursorBelow,
95 SelectAll,
96 SelectToStartOfLine,
97 SelectToEndOfLine,
98 SelectToStart,
99 SelectToEnd,
100 SelectToPreviousWordStart,
101 SelectToNextWordEnd,
102 ShowCharacterPalette,
103 Copy,
104 Cut,
105 Paste,
106 Undo,
107 Redo,
108 MoveToStartOfLine,
109 MoveToEndOfLine,
110 MoveToStart,
111 MoveToEnd,
112 MoveToPreviousWord,
113 MoveToNextWord,
114 Escape,
115 ToggleCodeActions,
116 Search,
117 Replace,
118 GoToDefinition,
119 ]
120);
121
122#[derive(Clone)]
123pub enum InputEvent {
124 Change,
125 PressEnter { secondary: bool, shift: bool },
126 Focus,
127 Blur,
128}
129
130pub(super) const CONTEXT: &str = "Input";
131
132pub(crate) fn init(cx: &mut App) {
133 cx.bind_keys([
134 KeyBinding::new("backspace", Backspace, Some(CONTEXT)),
135 KeyBinding::new("shift-backspace", Backspace, Some(CONTEXT)),
136 #[cfg(target_os = "macos")]
137 KeyBinding::new("ctrl-backspace", Backspace, Some(CONTEXT)),
138 KeyBinding::new("delete", Delete, Some(CONTEXT)),
139 KeyBinding::new("shift-delete", Delete, Some(CONTEXT)),
140 #[cfg(target_os = "macos")]
141 KeyBinding::new("cmd-backspace", DeleteToBeginningOfLine, Some(CONTEXT)),
142 #[cfg(target_os = "macos")]
143 KeyBinding::new("cmd-delete", DeleteToEndOfLine, Some(CONTEXT)),
144 #[cfg(target_os = "macos")]
145 KeyBinding::new("alt-backspace", DeleteToPreviousWordStart, Some(CONTEXT)),
146 #[cfg(not(target_os = "macos"))]
147 KeyBinding::new("ctrl-backspace", DeleteToPreviousWordStart, Some(CONTEXT)),
148 #[cfg(target_os = "macos")]
149 KeyBinding::new("alt-delete", DeleteToNextWordEnd, Some(CONTEXT)),
150 #[cfg(not(target_os = "macos"))]
151 KeyBinding::new("ctrl-delete", DeleteToNextWordEnd, Some(CONTEXT)),
152 KeyBinding::new(
153 "enter",
154 Enter {
155 secondary: false,
156 shift: false,
157 },
158 Some(CONTEXT),
159 ),
160 KeyBinding::new(
161 "shift-enter",
162 Enter {
163 secondary: false,
164 shift: true,
165 },
166 Some(CONTEXT),
167 ),
168 KeyBinding::new(
169 "secondary-enter",
170 Enter {
171 secondary: true,
172 shift: false,
173 },
174 Some(CONTEXT),
175 ),
176 KeyBinding::new("escape", Escape, Some(CONTEXT)),
177 KeyBinding::new("up", MoveUp, Some(CONTEXT)),
178 KeyBinding::new("down", MoveDown, Some(CONTEXT)),
179 KeyBinding::new("left", MoveLeft, Some(CONTEXT)),
180 KeyBinding::new("right", MoveRight, Some(CONTEXT)),
181 KeyBinding::new("pageup", MovePageUp, Some(CONTEXT)),
182 KeyBinding::new("pagedown", MovePageDown, Some(CONTEXT)),
183 KeyBinding::new("tab", IndentInline, Some(CONTEXT)),
184 KeyBinding::new("shift-tab", OutdentInline, Some(CONTEXT)),
185 #[cfg(target_os = "macos")]
186 KeyBinding::new("cmd-]", Indent, Some(CONTEXT)),
187 #[cfg(not(target_os = "macos"))]
188 KeyBinding::new("ctrl-]", Indent, Some(CONTEXT)),
189 #[cfg(target_os = "macos")]
190 KeyBinding::new("cmd-[", Outdent, Some(CONTEXT)),
191 #[cfg(not(target_os = "macos"))]
192 KeyBinding::new("ctrl-[", Outdent, Some(CONTEXT)),
193 KeyBinding::new("shift-left", SelectLeft, Some(CONTEXT)),
194 KeyBinding::new("shift-right", SelectRight, Some(CONTEXT)),
195 KeyBinding::new("shift-up", SelectUp, Some(CONTEXT)),
196 KeyBinding::new("shift-down", SelectDown, Some(CONTEXT)),
197 #[cfg(not(any(target_os = "macos", target_os = "linux")))]
198 KeyBinding::new("shift-alt-left", SelectLeft, Some(CONTEXT)),
199 #[cfg(not(any(target_os = "macos", target_os = "linux")))]
200 KeyBinding::new("shift-alt-right", SelectRight, Some(CONTEXT)),
201 #[cfg(target_os = "macos")]
203 KeyBinding::new("cmd-alt-up", AddCursorAbove, Some(CONTEXT)),
204 #[cfg(target_os = "macos")]
205 KeyBinding::new("cmd-alt-down", AddCursorBelow, Some(CONTEXT)),
206 #[cfg(target_os = "windows")]
207 KeyBinding::new("ctrl-alt-up", AddCursorAbove, Some(CONTEXT)),
208 #[cfg(not(any(target_os = "macos", target_os = "windows")))]
209 KeyBinding::new("shift-alt-up", AddCursorAbove, Some(CONTEXT)),
210 #[cfg(target_os = "windows")]
211 KeyBinding::new("ctrl-alt-down", AddCursorBelow, Some(CONTEXT)),
212 #[cfg(not(any(target_os = "macos", target_os = "windows")))]
213 KeyBinding::new("shift-alt-down", AddCursorBelow, Some(CONTEXT)),
214 KeyBinding::new("home", MoveHome, Some(CONTEXT)),
215 KeyBinding::new("end", MoveEnd, Some(CONTEXT)),
216 #[cfg(not(target_os = "macos"))]
217 KeyBinding::new("ctrl-home", MoveToStart, Some(CONTEXT)),
218 #[cfg(not(target_os = "macos"))]
219 KeyBinding::new("ctrl-end", MoveToEnd, Some(CONTEXT)),
220 #[cfg(not(target_os = "macos"))]
221 KeyBinding::new("ctrl-shift-home", SelectToStart, Some(CONTEXT)),
222 #[cfg(not(target_os = "macos"))]
223 KeyBinding::new("ctrl-shift-end", SelectToEnd, Some(CONTEXT)),
224 KeyBinding::new("shift-home", SelectToStartOfLine, Some(CONTEXT)),
225 KeyBinding::new("shift-end", SelectToEndOfLine, Some(CONTEXT)),
226 #[cfg(target_os = "macos")]
227 KeyBinding::new("ctrl-shift-a", SelectToStartOfLine, Some(CONTEXT)),
228 #[cfg(target_os = "macos")]
229 KeyBinding::new("ctrl-shift-e", SelectToEndOfLine, Some(CONTEXT)),
230 #[cfg(target_os = "macos")]
231 KeyBinding::new("shift-cmd-left", SelectToStartOfLine, Some(CONTEXT)),
232 #[cfg(target_os = "macos")]
233 KeyBinding::new("shift-cmd-right", SelectToEndOfLine, Some(CONTEXT)),
234 #[cfg(any(target_os = "macos", target_os = "linux"))]
235 KeyBinding::new("alt-shift-left", SelectToPreviousWordStart, Some(CONTEXT)),
236 #[cfg(not(target_os = "macos"))]
237 KeyBinding::new("ctrl-shift-left", SelectToPreviousWordStart, Some(CONTEXT)),
238 #[cfg(any(target_os = "macos", target_os = "linux"))]
239 KeyBinding::new("alt-shift-right", SelectToNextWordEnd, Some(CONTEXT)),
240 #[cfg(not(target_os = "macos"))]
241 KeyBinding::new("ctrl-shift-right", SelectToNextWordEnd, Some(CONTEXT)),
242 #[cfg(target_os = "macos")]
243 KeyBinding::new("ctrl-cmd-space", ShowCharacterPalette, Some(CONTEXT)),
244 #[cfg(target_os = "macos")]
245 KeyBinding::new("cmd-a", SelectAll, Some(CONTEXT)),
246 #[cfg(not(target_os = "macos"))]
247 KeyBinding::new("ctrl-a", SelectAll, Some(CONTEXT)),
248 #[cfg(target_os = "macos")]
249 KeyBinding::new("cmd-c", Copy, Some(CONTEXT)),
250 #[cfg(not(target_os = "macos"))]
251 KeyBinding::new("ctrl-c", Copy, Some(CONTEXT)),
252 #[cfg(target_os = "macos")]
253 KeyBinding::new("cmd-x", Cut, Some(CONTEXT)),
254 #[cfg(not(target_os = "macos"))]
255 KeyBinding::new("ctrl-x", Cut, Some(CONTEXT)),
256 #[cfg(target_os = "macos")]
257 KeyBinding::new("cmd-v", Paste, Some(CONTEXT)),
258 #[cfg(not(target_os = "macos"))]
259 KeyBinding::new("ctrl-v", Paste, Some(CONTEXT)),
260 #[cfg(target_os = "macos")]
261 KeyBinding::new("ctrl-a", MoveHome, Some(CONTEXT)),
262 #[cfg(target_os = "macos")]
263 KeyBinding::new("cmd-left", MoveHome, Some(CONTEXT)),
264 #[cfg(target_os = "macos")]
265 KeyBinding::new("ctrl-e", MoveEnd, Some(CONTEXT)),
266 #[cfg(target_os = "macos")]
267 KeyBinding::new("cmd-right", MoveEnd, Some(CONTEXT)),
268 #[cfg(target_os = "macos")]
269 KeyBinding::new("cmd-z", Undo, Some(CONTEXT)),
270 #[cfg(target_os = "macos")]
271 KeyBinding::new("cmd-shift-z", Redo, Some(CONTEXT)),
272 #[cfg(target_os = "macos")]
273 KeyBinding::new("cmd-up", MoveToStart, Some(CONTEXT)),
274 #[cfg(target_os = "macos")]
275 KeyBinding::new("cmd-down", MoveToEnd, Some(CONTEXT)),
276 #[cfg(target_os = "macos")]
277 KeyBinding::new("alt-left", MoveToPreviousWord, Some(CONTEXT)),
278 #[cfg(target_os = "macos")]
279 KeyBinding::new("alt-right", MoveToNextWord, Some(CONTEXT)),
280 #[cfg(not(target_os = "macos"))]
281 KeyBinding::new("ctrl-left", MoveToPreviousWord, Some(CONTEXT)),
282 #[cfg(not(target_os = "macos"))]
283 KeyBinding::new("ctrl-right", MoveToNextWord, Some(CONTEXT)),
284 #[cfg(target_os = "macos")]
285 KeyBinding::new("cmd-shift-up", SelectToStart, Some(CONTEXT)),
286 #[cfg(target_os = "macos")]
287 KeyBinding::new("cmd-shift-down", SelectToEnd, Some(CONTEXT)),
288 #[cfg(not(target_os = "macos"))]
289 KeyBinding::new("ctrl-z", Undo, Some(CONTEXT)),
290 #[cfg(not(target_os = "macos"))]
291 KeyBinding::new("ctrl-y", Redo, Some(CONTEXT)),
292 #[cfg(target_os = "macos")]
293 KeyBinding::new("cmd-.", ToggleCodeActions, Some(CONTEXT)),
294 #[cfg(not(target_os = "macos"))]
295 KeyBinding::new("ctrl-.", ToggleCodeActions, Some(CONTEXT)),
296 #[cfg(target_os = "macos")]
297 KeyBinding::new("cmd-f", Search, Some(CONTEXT)),
298 #[cfg(not(target_os = "macos"))]
299 KeyBinding::new("ctrl-f", Search, Some(CONTEXT)),
300 #[cfg(target_os = "macos")]
301 KeyBinding::new("cmd-shift-f", Replace, Some(CONTEXT)),
302 #[cfg(not(target_os = "macos"))]
303 KeyBinding::new("ctrl-h", Replace, Some(CONTEXT)),
304 ]);
305}
306
307#[derive(Debug, Clone, Copy, PartialEq, Eq)]
314pub(super) struct ColumnarPoint {
315 offset: usize,
316 columns_past_line_end: usize,
317}
318
319impl ColumnarPoint {
320 pub(super) fn new(offset: usize, columns_past_line_end: usize) -> Self {
321 Self {
322 offset,
323 columns_past_line_end,
324 }
325 }
326}
327
328pub struct InputBaseState<M: InputModeKind> {
339 pub(crate) extras: M::Extras,
341 pub(super) focus_handle: FocusHandle,
342 pub(super) mode: LayoutMode,
343 pub(super) text: Rope,
344 pub(super) display_map: DisplayMap,
345 pub(super) undo_manager: UndoManager,
346 pub(super) inline_tokens: Option<Box<super::inline_tokens::InlineTokenStore>>,
347 pub(super) pending_token: Option<super::InlineToken>,
348 pub(super) replaying_history: bool,
349 pub(super) validated_token_edit: bool,
350 pub(super) document_revision: u64,
351 pub(super) token_presentation: super::InlineTokenPresentation,
352 pub(super) token_layout_cache: Option<Box<super::token_presentation::TokenLayoutCache>>,
353 pub(super) pressed_token: Option<(usize, u64, Point<Pixels>)>,
356 pub(super) search_session: super::SearchSession,
357 pub(super) search_activation_revision: u64,
360 pub(super) searchable: bool,
361 pub(super) replaceable: bool,
362 pub(super) soft_wrap: bool,
363 pub(super) wrapping_indent: WrappingIndent,
364 pub(super) scroll_beyond_last_line: Option<usize>,
365 pub(super) cursor_surrounding_lines: Option<usize>,
366 pub(super) blink_cursor: Entity<BlinkCursor>,
367 pub(super) loading: bool,
368 pub(super) selections: Selections,
372 pub(super) selected_word_range: Option<CursorSelection>,
374 pub(super) ime_marked_range: Option<CursorSelection>,
376 pub(super) last_layout: Option<LastLayout>,
377 pub(super) last_cursor: Option<usize>,
378 pub(super) input_bounds: Bounds<Pixels>,
380 pub(super) last_bounds: Option<Bounds<Pixels>>,
382 pub(super) last_selected_range: Option<CursorSelection>,
383 pub(super) selecting: bool,
384 pub(super) column_select_start: Option<ColumnarPoint>,
386 pub(super) touch_selection: super::touch::TouchSelection,
388 pub(crate) disabled: bool,
389 pub(crate) readonly: bool,
390 pub(crate) text_align: TextAlign,
391 pub(super) masked: bool,
392 pub(super) clean_on_escape: bool,
393 pub(super) submit_on_enter: bool,
394 pub(super) show_whitespaces: bool,
395 pub(crate) cursor_line_end_affinity: bool,
397 pub(super) pattern: Option<regex::Regex>,
398 pub(super) validate: Option<Box<dyn Fn(&str, &mut App) -> bool + 'static>>,
399 pub(crate) number_step: Option<NumberStep>,
402 pub(crate) number_min: Option<f64>,
404 pub(crate) number_max: Option<f64>,
406 pub(crate) scroll_handle: ScrollHandle,
407 pub(crate) deferred_scroll_offset: Option<Point<Pixels>>,
409 pub(crate) scroll_size: gpui::Size<Pixels>,
411 pub(super) editor_scrollbar_snapshot: Cell<Option<EditorScrollbarSnapshot>>,
412 pub(super) editor_paddings: Edges<Pixels>,
413 pub(super) editor_style: InputEditorStyle,
418 projected_editor_style: InputEditorStyle,
423
424 pub(crate) mask_pattern: MaskPattern,
426 pub(super) mask_pattern_set: bool,
430 pub(super) placeholder: SharedString,
431
432 pub(super) diagnostic_popover: Option<Rc<crate::input::DiagnosticEntry>>,
434
435 context_menu_handler: Option<
436 Rc<dyn Fn(NativeMenu, InputContextMenuCapabilities, Point<Pixels>, &mut Window, &mut App)>,
437 >,
438 pending_context_menu: Option<(Point<Pixels>, usize)>,
439
440 pub(super) enable_context_menu: bool,
443
444 pub(super) completion_inserting: bool,
446 pub(super) overlay_action_handler: Option<
447 Rc<
448 dyn Fn(
449 super::InputOverlayKind,
450 Box<dyn Action>,
451 &mut Window,
452 &mut Context<InputBaseState<M>>,
453 ) -> bool,
454 >,
455 >,
456
457 _pending_update: bool,
461 pub(super) silent_replace_text: bool,
463 pub(super) emit_events: bool,
465
466 _subscriptions: Vec<Subscription>,
467
468 pub(super) auto_scroll: AutoScroll,
469}
470
471#[derive(Clone)]
476pub struct InputPresentation {
477 focus_handle: FocusHandle,
478 disabled: bool,
479 readonly: bool,
480 loading: bool,
481 masked: bool,
482 multi_line: bool,
483 code_editor: bool,
484 text_align: TextAlign,
485 placeholder: SharedString,
486 mask_placeholder: Option<String>,
487}
488
489impl InputPresentation {
490 pub fn focus_handle(&self) -> &FocusHandle {
491 &self.focus_handle
492 }
493
494 pub fn is_disabled(&self) -> bool {
495 self.disabled
496 }
497
498 pub fn is_readonly(&self) -> bool {
499 self.readonly
500 }
501
502 pub fn is_editable(&self) -> bool {
506 !self.disabled && !self.readonly
507 }
508
509 pub fn is_loading(&self) -> bool {
510 self.loading
511 }
512
513 pub fn is_masked(&self) -> bool {
514 self.masked
515 }
516
517 pub fn is_multi_line(&self) -> bool {
518 self.multi_line
519 }
520
521 pub fn is_code_editor(&self) -> bool {
522 self.code_editor
523 }
524
525 pub fn text_align(&self) -> TextAlign {
526 self.text_align
527 }
528
529 pub fn placeholder(&self) -> &SharedString {
530 &self.placeholder
531 }
532
533 pub fn mask_placeholder(&self) -> Option<&str> {
535 self.mask_placeholder.as_deref()
536 }
537}
538
539impl<M: InputModeKind> EventEmitter<InputEvent> for InputBaseState<M> {}
540
541impl<M: InputModeKind> InputBaseState<M> {
542 #[doc(hidden)]
543 pub fn cursor_layout(&self) -> Option<(Bounds<Pixels>, Pixels)> {
544 let layout = self.last_layout.as_ref()?;
545 Some((layout.cursor_bounds?, layout.line_height))
546 }
547
548 pub fn input_bounds(&self) -> Bounds<Pixels> {
549 self.input_bounds
550 }
551
552 pub fn text_bounds(&self) -> Option<Bounds<Pixels>> {
553 self.last_bounds
554 }
555
556 pub fn diagnostic_popover(&self) -> Option<Rc<crate::input::DiagnosticEntry>> {
557 self.diagnostic_popover.clone()
558 }
559
560 pub fn presentation(&self) -> InputPresentation {
561 InputPresentation {
562 focus_handle: self.focus_handle.clone(),
563 disabled: self.disabled,
564 readonly: self.readonly,
565 loading: self.loading,
566 masked: self.masked,
567 multi_line: self.is_multi_line(),
568 code_editor: self.is_code_editor(),
569 text_align: self.text_align,
570 placeholder: self.placeholder.clone(),
571 mask_placeholder: self.mask_pattern.placeholder(),
572 }
573 }
574
575 #[inline]
580 pub(crate) fn shows_scrollbar(&self) -> bool {
588 self.is_multi_line()
589 }
590
591 pub fn is_multi_line(&self) -> bool {
592 M::MULTI_LINE
593 }
594
595 #[inline]
597 pub fn is_single_line(&self) -> bool {
598 !M::MULTI_LINE
599 }
600
601 #[inline]
603 pub fn is_code_editor(&self) -> bool {
604 M::CODE_EDITOR
605 }
606
607 pub fn is_copyable(&self) -> bool {
611 self.selections.iter().any(|sel| !sel.is_empty()) && !self.masked
612 }
613
614 pub fn context_menu_capabilities(&self) -> InputContextMenuCapabilities {
615 let (go_to_definition, code_actions) = self.extras.context_menu_capabilities();
616 InputContextMenuCapabilities::new()
617 .disabled(self.disabled)
618 .readonly(self.readonly)
619 .code_editor(self.is_code_editor())
620 .selection(!self.active_selection().is_empty())
621 .masked(self.masked)
622 .go_to_definition(go_to_definition)
623 .code_actions(code_actions)
624 }
625
626 pub fn set_text_align(&mut self, text_align: TextAlign, cx: &mut Context<Self>) {
627 if !self.is_single_line() || self.text_align == text_align {
628 return;
629 }
630
631 self.text_align = text_align;
632 cx.notify();
633 }
634
635 pub fn toggle_masked(&mut self, _: &mut Window, cx: &mut Context<Self>) {
641 self.masked = !self.masked;
642 cx.notify();
643 }
644
645 pub fn on_context_menu(
646 &mut self,
647 handler: Rc<
648 dyn Fn(NativeMenu, InputContextMenuCapabilities, Point<Pixels>, &mut Window, &mut App),
649 >,
650 ) {
651 self.context_menu_handler = Some(handler);
652 }
653
654 fn new_in_mode(window: &mut Window, cx: &mut Context<Self>) -> Self {
656 let focus_handle = cx.focus_handle().tab_stop(true);
657 let blink_cursor = cx.new(|_| BlinkCursor::new());
658 let undo_manager = UndoManager::new();
659
660 let _subscriptions = vec![
661 cx.intercept_keystrokes({
664 let focus_handle = focus_handle.clone();
665 let blink_cursor = blink_cursor.downgrade();
666 move |_, window, cx| {
667 if focus_handle.is_focused(window) {
668 _ = blink_cursor.update(cx, |cursor, cx| cursor.pause(cx));
669 }
670 }
671 }),
672 cx.observe(&blink_cursor, |_, _, cx| cx.notify()),
674 cx.observe_window_activation(window, |input, window, cx| {
676 if window.is_window_active() {
677 let focus_handle = input.focus_handle.clone();
678 if focus_handle.is_focused(window) {
679 input.blink_cursor.update(cx, |blink_cursor, cx| {
680 blink_cursor.start(cx);
681 });
682 }
683 }
684 }),
685 cx.on_focus(&focus_handle, window, Self::on_focus),
686 cx.on_blur(&focus_handle, window, Self::on_blur),
687 ];
688
689 let text_style = window.text_style();
690
691 Self {
692 extras: M::Extras::default(),
693 focus_handle: focus_handle.clone(),
694 text: "".into(),
695 display_map: DisplayMap::new(text_style.font(), window.rem_size(), None),
696 search_session: super::SearchSession::default(),
697 search_activation_revision: 0,
698 searchable: false,
699 replaceable: true,
700 soft_wrap: true,
701 wrapping_indent: WrappingIndent::default(),
702 scroll_beyond_last_line: None,
703 cursor_surrounding_lines: None,
704 blink_cursor,
705 undo_manager,
706 inline_tokens: None,
707 pending_token: None,
708 replaying_history: false,
709 validated_token_edit: false,
710 document_revision: 0,
711 token_presentation: Default::default(),
712 token_layout_cache: None,
713 pressed_token: None,
714 selections: Selections::default(),
715 selected_word_range: None,
716 ime_marked_range: None,
717 input_bounds: Bounds::default(),
718 selecting: false,
719 touch_selection: Default::default(),
720 disabled: false,
721 readonly: false,
722 text_align: TextAlign::Left,
723 masked: false,
724 clean_on_escape: false,
725 submit_on_enter: false,
726 show_whitespaces: false,
727 loading: false,
728 pattern: None,
729 validate: None,
730 number_step: Some(NumberStep::Fixed(1.)),
731 number_min: None,
732 number_max: None,
733 mode: LayoutMode::default(),
734 last_layout: None,
735 last_bounds: None,
736 last_selected_range: None,
737 column_select_start: None,
738 last_cursor: None,
739 scroll_handle: ScrollHandle::new(),
740 scroll_size: gpui::size(px(0.), px(0.)),
741 editor_scrollbar_snapshot: Cell::new(None),
742 editor_paddings: Edges::default(),
743 deferred_scroll_offset: None,
744 placeholder: SharedString::default(),
745 mask_pattern: MaskPattern::default(),
746 mask_pattern_set: false,
747 editor_style: InputEditorStyle::default(),
748 projected_editor_style: InputEditorStyle::default(),
749 diagnostic_popover: None,
750 context_menu_handler: None,
751 pending_context_menu: None,
752 enable_context_menu: true,
753 completion_inserting: false,
754 overlay_action_handler: None,
755 silent_replace_text: false,
756 emit_events: true,
757 _subscriptions,
758 _pending_update: false,
759 cursor_line_end_affinity: false,
760 auto_scroll: AutoScroll::default(),
761 }
762 }
763
764 pub fn context_menu(mut self, enable: bool) -> Self {
769 self.enable_context_menu = enable;
770 self
771 }
772
773 pub fn set_context_menu_enabled(&mut self, enabled: bool) {
774 self.enable_context_menu = enabled;
775 }
776
777 #[doc(hidden)]
779 pub fn replaceable(mut self, allow: bool) -> Self {
780 self.replaceable = allow;
781 self
782 }
783
784 pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
786 self.placeholder = placeholder.into();
787 self
788 }
789
790 pub fn set_highlighter(
792 &mut self,
793 new_language: impl Into<SharedString>,
794 cx: &mut Context<Self>,
795 ) {
796 match &mut self.mode {
797 LayoutMode::CodeEditor {
798 language,
799 highlighter,
800 ..
801 } => {
802 language.set_name(new_language.into());
803 *highlighter.borrow_mut() = None;
804 }
805 _ => {}
806 }
807 self.refresh(cx);
808 }
809
810 fn reset_highlighter(&mut self, cx: &mut Context<Self>) {
811 match &mut self.mode {
812 LayoutMode::CodeEditor { highlighter, .. } => {
813 *highlighter.borrow_mut() = None;
814 }
815 _ => {}
816 }
817 cx.notify();
818 }
819
820 pub fn set_highlighter_factory(
822 &mut self,
823 factory: InputHighlighterFactory,
824 cx: &mut Context<Self>,
825 ) {
826 self.mode.set_highlighter_factory(factory);
827 self._pending_update = true;
828 self.token_layout_cache = None;
829 cx.notify();
830 }
831
832 pub fn ensure_highlighter_factory(&mut self, factory: InputHighlighterFactory) {
834 self.mode.ensure_highlighter_factory(factory);
835 }
836
837 pub fn set_editor_style(&mut self, style: InputEditorStyle) {
838 self.editor_style = style.clone();
839 self.projected_editor_style = style;
840 }
841
842 #[doc(hidden)]
844 pub fn set_editor_paddings(&mut self, paddings: Edges<Pixels>) {
845 self.editor_paddings = paddings;
846 }
847
848 pub fn apply_highlighter_fold_candidates(
849 &mut self,
850 candidates: Vec<crate::input::FoldRange>,
851 cx: &mut Context<Self>,
852 ) {
853 if self.mode.is_folding() {
854 self.display_map.set_fold_candidates(candidates);
855 }
856 cx.notify();
857 }
858
859 #[inline]
860 pub fn diagnostics(&self) -> Option<&DiagnosticSet> {
861 self.mode.diagnostics()
862 }
863
864 #[inline]
865 pub fn diagnostics_mut(&mut self) -> Option<&mut DiagnosticSet> {
866 self.mode.diagnostics_mut()
867 }
868
869 pub fn set_placeholder(
871 &mut self,
872 placeholder: impl Into<SharedString>,
873 _: &mut Window,
874 cx: &mut Context<Self>,
875 ) {
876 self.placeholder = placeholder.into();
877 cx.notify();
878 }
879
880 pub(super) fn line_and_position_for_offset(
888 &self,
889 offset: usize,
890 ) -> (usize, usize, Option<Point<Pixels>>) {
891 let Some(last_layout) = &self.last_layout else {
892 return (0, 0, None);
893 };
894 let line_height = last_layout.line_height;
895
896 let mut y_offset = last_layout.visible_top;
897 for (vi, line) in last_layout.lines.iter().enumerate() {
898 let prev_lines_offset = last_layout.visible_line_byte_offsets[vi];
899 let local_offset = offset.saturating_sub(prev_lines_offset);
900 if let Some(pos) = line.position_for_index(local_offset, last_layout, false) {
901 let sub_line_index = (pos.y / line_height) as usize;
902 let adjusted_pos = point(pos.x + last_layout.line_number_width, pos.y + y_offset);
903 return (vi, sub_line_index, Some(adjusted_pos));
904 }
905
906 y_offset += line.size(line_height).height;
907 }
908 (0, 0, None)
909 }
910
911 pub fn set_value(
920 &mut self,
921 value: impl Into<super::InputContent>,
922 window: &mut Window,
923 cx: &mut Context<Self>,
924 ) {
925 let content = value.into();
926 self.inline_tokens = None;
927 self.undo_manager.set_ignoring(true);
928 self.emit_events = false;
929 self.replace_text(content.text().clone(), window, cx);
930 self.install_tokens(content);
931 self.undo_manager.set_ignoring(false);
932 self.emit_events = true;
933
934 self.reset_selection();
935 self.reset_lsp_state();
936 self.reset_scroll_to_start();
937
938 self.undo_manager.clear();
939 cx.notify();
940 }
941
942 pub fn replace_all(
952 &mut self,
953 text: impl Into<SharedString>,
954 window: &mut Window,
955 cx: &mut Context<Self>,
956 ) {
957 self.replace_text(text, window, cx);
958 self.reset_selection();
959 self.reset_lsp_state();
960 self.reset_scroll_to_start();
961
962 cx.notify();
963 }
964
965 pub(super) fn with_edits_allowed(&mut self, f: impl FnOnce(&mut Self)) {
970 let (was_disabled, was_readonly) = (self.disabled, self.readonly);
971 (self.disabled, self.readonly) = (false, false);
972 f(self);
973 (self.disabled, self.readonly) = (was_disabled, was_readonly);
974 }
975
976 pub fn insert(
980 &mut self,
981 text: impl Into<SharedString>,
982 window: &mut Window,
983 cx: &mut Context<Self>,
984 ) {
985 let text: SharedString = text.into();
986 self.with_edits_allowed(|this| {
987 this.undo_manager.set_pending_intent(EditIntent::Atomic);
988 let range_utf16 = this.range_to_utf16(&(this.cursor()..this.cursor()));
989 this.replace_text_in_range_silent(Some(range_utf16), &text, window, cx);
990 let end = this.active_selection().end;
991 this.set_cursor_to(end);
992 });
993 }
994
995 pub fn replace(
999 &mut self,
1000 text: impl Into<SharedString>,
1001 window: &mut Window,
1002 cx: &mut Context<Self>,
1003 ) {
1004 let text: SharedString = text.into();
1005 self.with_edits_allowed(|this| {
1006 this.undo_manager.set_pending_intent(EditIntent::Atomic);
1007 this.replace_text_in_range_silent(None, &text, window, cx);
1008 let end = this.active_selection().end;
1009 this.set_cursor_to(end);
1010 });
1011 }
1012
1013 fn replace_text(
1014 &mut self,
1015 text: impl Into<SharedString>,
1016 window: &mut Window,
1017 cx: &mut Context<Self>,
1018 ) {
1019 let text: SharedString = text.into();
1020 self.with_edits_allowed(|this| {
1021 this.undo_manager.set_pending_intent(EditIntent::Atomic);
1022 let range = 0..this.text.chars().map(|c| c.len_utf16()).sum();
1023 this.replace_text_in_range_silent(Some(range), &text, window, cx);
1024 this.reset_highlighter(cx);
1025 });
1026 }
1027
1028 fn reset_selection(&mut self) {
1029 self.selections.remove_all_but_active();
1030
1031 if self.is_single_line() {
1035 let end = self.text.len();
1036 self.set_cursor_to(end);
1037 } else {
1038 self.active_selection_mut().clear();
1039 }
1040 }
1041
1042 fn reset_lsp_state(&mut self) {
1043 if self.is_code_editor() {
1044 self._pending_update = true;
1045 M::reset_language_features(self);
1046 }
1047 }
1048
1049 fn reset_scroll_to_start(&mut self) {
1050 self.scroll_handle.set_offset(point(px(0.), px(0.)));
1054 if self.is_single_line() {
1055 self.deferred_scroll_offset = Some(point(px(0.), px(0.)));
1056 }
1057 }
1058
1059 #[allow(unused)]
1063 pub(crate) fn disabled(mut self, disabled: bool) -> Self {
1064 self.disabled = disabled;
1065 self
1066 }
1067
1068 pub fn set_disabled(&mut self, disabled: bool, cx: &mut Context<Self>) {
1069 if self.disabled == disabled {
1070 return;
1071 }
1072
1073 self.disabled = disabled;
1074 cx.notify();
1075 }
1076
1077 #[allow(unused)]
1085 pub(crate) fn readonly(mut self, readonly: bool) -> Self {
1086 self.readonly = readonly;
1087 self
1088 }
1089
1090 pub fn set_readonly(&mut self, readonly: bool, cx: &mut Context<Self>) {
1091 if self.readonly == readonly {
1092 return;
1093 }
1094
1095 self.readonly = readonly;
1096 if readonly {
1097 self.search_session.replace_mode = false;
1098 }
1099 cx.notify();
1100 }
1101
1102 pub fn is_editable(&self) -> bool {
1107 !self.disabled && !self.readonly
1108 }
1109
1110 pub fn clean_on_escape(mut self) -> Self {
1112 self.clean_on_escape = true;
1113 self
1114 }
1115
1116 pub fn set_clean_on_escape(&mut self, clean: bool) {
1117 self.clean_on_escape = clean;
1118 }
1119
1120 #[doc(hidden)]
1125 pub fn submit_on_enter(mut self, submit: bool) -> Self {
1126 self.submit_on_enter = submit;
1127 self
1128 }
1129
1130 pub fn set_submit_on_enter(&mut self, submit: bool, cx: &mut Context<Self>) {
1131 self.submit_on_enter = submit;
1132 cx.notify();
1133 }
1134
1135 #[doc(hidden)]
1137 pub fn show_whitespaces(mut self, show: bool) -> Self {
1138 self.show_whitespaces = show;
1139 self
1140 }
1141
1142 pub fn set_show_whitespaces(&mut self, show: bool, _: &mut Window, cx: &mut Context<Self>) {
1144 self.show_whitespaces = show;
1145 cx.notify();
1146 }
1147
1148 pub fn scroll_beyond_last_line(mut self, rows: Option<usize>) -> Self {
1158 self.scroll_beyond_last_line = rows;
1159 self
1160 }
1161
1162 pub fn set_scroll_beyond_last_line(
1164 &mut self,
1165 rows: Option<usize>,
1166 _: &mut Window,
1167 cx: &mut Context<Self>,
1168 ) {
1169 if self.scroll_beyond_last_line == rows {
1170 return;
1171 }
1172 self.scroll_beyond_last_line = rows;
1173 cx.notify();
1174 }
1175
1176 pub fn cursor_surrounding_lines(mut self, lines: Option<usize>) -> Self {
1187 self.cursor_surrounding_lines = lines;
1188 self
1189 }
1190
1191 pub fn set_cursor_surrounding_lines(
1193 &mut self,
1194 lines: Option<usize>,
1195 _: &mut Window,
1196 cx: &mut Context<Self>,
1197 ) {
1198 if self.cursor_surrounding_lines == lines {
1199 return;
1200 }
1201 self.cursor_surrounding_lines = lines;
1202 cx.notify();
1203 }
1204
1205 pub fn default_value(mut self, value: impl Into<SharedString>) -> Self {
1207 let text: SharedString = value.into();
1208 self.text = Rope::from(self.normalize_input(&text).as_ref());
1209 if let Some(diagnostics) = self.mode.diagnostics_mut() {
1210 diagnostics.reset(&self.text)
1211 }
1212 self._pending_update = true;
1215 self
1216 }
1217
1218 pub fn value(&self) -> SharedString {
1223 SharedString::new(self.text.to_string())
1224 }
1225
1226 pub fn selected_value(&self) -> SharedString {
1232 SharedString::new(self.selected_text().to_string())
1233 }
1234
1235 pub fn unmask_value(&self) -> SharedString {
1237 self.mask_pattern.unmask(&self.text.to_string()).into()
1238 }
1239
1240 #[doc(hidden)]
1246 pub fn prepare(&mut self, _: &mut Window, _: &mut Context<Self>) {}
1247
1248 pub fn text(&self) -> &Rope {
1253 &self.text
1254 }
1255
1256 pub fn cursor_position(&self) -> Position {
1258 let offset = self.cursor();
1259 self.text.offset_to_position(offset)
1260 }
1261
1262 pub fn set_cursor_position(
1266 &mut self,
1267 position: impl Into<Position>,
1268 window: &mut Window,
1269 cx: &mut Context<Self>,
1270 ) {
1271 let position: Position = position.into();
1272 let offset = self.text.position_to_offset(&position);
1273
1274 self.move_to(offset, None, cx);
1275 self.update_preferred_column();
1276 self.focus(window, cx);
1277 }
1278
1279 pub fn focus(&self, window: &mut Window, cx: &mut Context<Self>) {
1281 self.focus_handle.focus(window, cx);
1282 self.blink_cursor.update(cx, |cursor, cx| {
1283 cursor.start(cx);
1284 });
1285 }
1286
1287 pub fn refresh(&mut self, cx: &mut Context<Self>) {
1301 self._pending_update = true;
1302 cx.notify();
1303 }
1304
1305 pub(super) fn select_left(&mut self, _: &SelectLeft, _: &mut Window, cx: &mut Context<Self>) {
1306 self.undo_manager.break_transaction_coalescing();
1307 self.select_all_cursors_to(|s, sel| s.previous_boundary(sel.cursor_offset()), cx);
1308 }
1309
1310 pub(super) fn select_right(&mut self, _: &SelectRight, _: &mut Window, cx: &mut Context<Self>) {
1311 self.select_all_cursors_to(|s, sel| s.next_boundary(sel.cursor_offset()), cx);
1312 }
1313
1314 pub(super) fn select_up(&mut self, _: &SelectUp, _: &mut Window, cx: &mut Context<Self>) {
1315 if self.is_single_line() {
1316 return;
1317 }
1318 self.undo_manager.break_transaction_coalescing();
1319 self.select_all_cursors_to(
1320 |s, sel| {
1321 let offset = s
1322 .start_of_line_at(sel.cursor_offset(), s.line_end_affinity_for(sel))
1323 .saturating_sub(1);
1324 s.previous_boundary(offset)
1325 },
1326 cx,
1327 );
1328 }
1329
1330 pub(super) fn select_down(&mut self, _: &SelectDown, _: &mut Window, cx: &mut Context<Self>) {
1331 if self.is_single_line() {
1332 return;
1333 }
1334 self.undo_manager.break_transaction_coalescing();
1335 let len = self.text.len();
1336 self.select_all_cursors_to(
1337 |s, sel| {
1338 let offset = (s.end_of_line_at(sel.cursor_offset(), s.line_end_affinity_for(sel))
1339 + 1)
1340 .min(len);
1341 s.next_boundary(offset)
1342 },
1343 cx,
1344 );
1345 }
1346
1347 pub(super) fn on_action_select_all(
1348 &mut self,
1349 _: &SelectAll,
1350 window: &mut Window,
1351 cx: &mut Context<Self>,
1352 ) {
1353 self.select_all(window, cx);
1354 }
1355
1356 pub(super) fn select_to_start(
1357 &mut self,
1358 _: &SelectToStart,
1359 _: &mut Window,
1360 cx: &mut Context<Self>,
1361 ) {
1362 self.undo_manager.break_transaction_coalescing();
1363 self.select_all_cursors_to(|_, _| 0, cx);
1364 }
1365
1366 pub(super) fn select_to_end(
1367 &mut self,
1368 _: &SelectToEnd,
1369 _: &mut Window,
1370 cx: &mut Context<Self>,
1371 ) {
1372 self.undo_manager.break_transaction_coalescing();
1373 let end = self.text.len();
1374 self.select_all_cursors_to(move |_, _| end, cx);
1375 }
1376
1377 pub(super) fn select_to_start_of_line(
1378 &mut self,
1379 _: &SelectToStartOfLine,
1380 _: &mut Window,
1381 cx: &mut Context<Self>,
1382 ) {
1383 self.undo_manager.break_transaction_coalescing();
1384 self.select_all_cursors_to(
1385 |s, sel| s.start_of_line_at(sel.cursor_offset(), s.line_end_affinity_for(sel)),
1386 cx,
1387 );
1388 }
1389
1390 pub(super) fn select_to_end_of_line(
1391 &mut self,
1392 _: &SelectToEndOfLine,
1393 _: &mut Window,
1394 cx: &mut Context<Self>,
1395 ) {
1396 self.undo_manager.break_transaction_coalescing();
1397 self.select_all_cursors_to(
1398 |s, sel| s.end_of_line_at(sel.cursor_offset(), s.line_end_affinity_for(sel)),
1399 cx,
1400 );
1401 self.cursor_line_end_affinity = true;
1403 }
1404
1405 pub(super) fn select_to_previous_word(
1406 &mut self,
1407 _: &SelectToPreviousWordStart,
1408 _: &mut Window,
1409 cx: &mut Context<Self>,
1410 ) {
1411 self.undo_manager.break_transaction_coalescing();
1412 self.select_all_cursors_to(
1413 |s, sel| s.previous_start_of_word_at(sel.cursor_offset()),
1414 cx,
1415 );
1416 }
1417
1418 pub(super) fn select_to_next_word(
1419 &mut self,
1420 _: &SelectToNextWordEnd,
1421 _: &mut Window,
1422 cx: &mut Context<Self>,
1423 ) {
1424 self.undo_manager.break_transaction_coalescing();
1425 self.select_all_cursors_to(|s, sel| s.next_end_of_word_at(sel.cursor_offset()), cx);
1426 }
1427
1428 pub(super) fn previous_start_of_word_at(&self, offset: usize) -> usize {
1431 if self.masked {
1432 return 0;
1436 }
1437
1438 let offset = self.offset_from_utf16(self.offset_to_utf16(offset));
1439 let left_part = self.text.slice(0..offset).to_string();
1441
1442 let target = UnicodeSegmentation::split_word_bound_indices(left_part.as_str())
1443 .rfind(|(_, s)| !s.trim_start().is_empty())
1444 .map(|(i, _)| i)
1445 .unwrap_or(0);
1446 self.token_boundary(target, Bias::Left)
1447 }
1448
1449 pub(super) fn next_end_of_word_at(&self, offset: usize) -> usize {
1451 if self.masked {
1452 return self.text.len();
1454 }
1455
1456 let offset = self.offset_from_utf16(self.offset_to_utf16(offset));
1457 let right_part = self.text.slice(offset..self.text.len()).to_string();
1458
1459 let target = UnicodeSegmentation::split_word_bound_indices(right_part.as_str())
1460 .find(|(_, s)| !s.trim_start().is_empty())
1461 .map(|(i, s)| offset + i + s.len())
1462 .unwrap_or(self.text.len());
1463 self.token_boundary(target, Bias::Right)
1464 }
1465
1466 pub(super) fn start_of_line_at(&self, offset: usize, line_end_affinity: bool) -> usize {
1471 if self.is_single_line() {
1472 return 0;
1473 }
1474
1475 let row = self.text.offset_to_point(offset).row;
1476 let logical_start = self.text.line_start_offset(row);
1477
1478 if self.soft_wrap && self.is_code_editor() {
1479 let wrap_point = self
1480 .display_map
1481 .offset_to_wrap_display_point_with_affinity(offset, line_end_affinity);
1482 if let Some(line) = self.display_map.line(row)
1483 && let Some(range) = line.wrapped_lines.get(wrap_point.local_row)
1484 {
1485 let visual_start = logical_start + range.start;
1486 if offset != visual_start {
1487 return visual_start;
1488 }
1489 }
1490 }
1491
1492 logical_start
1493 }
1494
1495 pub(super) fn end_of_line_at(&self, offset: usize, line_end_affinity: bool) -> usize {
1500 if self.is_single_line() {
1501 return self.text.len();
1502 }
1503
1504 let row = self.text.offset_to_point(offset).row;
1505 let logical_start = self.text.line_start_offset(row);
1506 let logical_end = self.text.line_end_offset(row);
1507
1508 if self.soft_wrap && self.is_code_editor() {
1509 let wrap_point = self
1513 .display_map
1514 .offset_to_wrap_display_point_with_affinity(offset, line_end_affinity);
1515 if let Some(line) = self.display_map.line(row)
1516 && let Some(range) = line.wrapped_lines.get(wrap_point.local_row)
1517 {
1518 let visual_end = logical_start + range.end;
1519 if offset != visual_end {
1520 return visual_end;
1521 }
1522 }
1523 }
1524
1525 logical_end
1526 }
1527
1528 pub(super) fn indent_of_next_line(&mut self) -> String {
1532 self.indent_of_next_line_at(self.cursor())
1533 }
1534
1535 pub(super) fn indent_of_next_line_at(&mut self, offset: usize) -> String {
1537 if self.is_single_line() {
1538 return "".into();
1539 }
1540
1541 let mut current_indent = String::new();
1542 let mut next_indent = String::new();
1543 let line_end_affinity = self.line_end_affinity_at(offset);
1544 let current_line_start_pos = self.start_of_line_at(offset, line_end_affinity);
1545 let next_line_start_pos = self.end_of_line_at(offset, line_end_affinity);
1546 for c in self.text.slice(current_line_start_pos..).chars() {
1547 if !c.is_whitespace() {
1548 break;
1549 }
1550 if c == '\n' || c == '\r' {
1551 break;
1552 }
1553 current_indent.push(c);
1554 }
1555
1556 for c in self.text.slice(next_line_start_pos..).chars() {
1557 if !c.is_whitespace() {
1558 break;
1559 }
1560 if c == '\n' || c == '\r' {
1561 break;
1562 }
1563 next_indent.push(c);
1564 }
1565
1566 if next_indent.len() > current_indent.len() {
1567 return next_indent;
1568 }
1569
1570 if let Some(rules) = self.mode.language_config() {
1573 if self.mode.is_smart_indent()
1574 && M::editing_syntax_context(self, offset) == crate::input::SyntaxContext::Code
1575 {
1576 let line_before: String = self
1577 .text
1578 .slice(current_line_start_pos..offset)
1579 .chars()
1580 .collect();
1581 if rules.opens_indent(line_before.trim_end()) {
1582 let tab = self.mode.tab_size();
1583 if tab.hard_tabs {
1584 current_indent.push('\t');
1585 } else {
1586 for _ in 0..tab.tab_size {
1587 current_indent.push(' ');
1588 }
1589 }
1590 } else {
1591 let line_after = self.text.slice(offset..next_line_start_pos).to_string();
1592 if rules.closes_indent(&line_after) {
1593 let tab = self.mode.tab_size();
1594 if current_indent.ends_with('\t') {
1595 current_indent.pop();
1596 } else {
1597 for _ in 0..tab.tab_size {
1598 if !current_indent.ends_with(' ') {
1599 break;
1600 }
1601 current_indent.pop();
1602 }
1603 }
1604 }
1605 }
1606 }
1607 }
1608
1609 current_indent
1610 }
1611
1612 fn delete_selections(
1620 &mut self,
1621 silent: bool,
1622 collapsed_intent: EditIntent,
1623 mut collapsed_target: impl FnMut(&mut Self, usize) -> Range<usize>,
1624 window: &mut Window,
1625 cx: &mut Context<Self>,
1626 ) {
1627 if !self.is_editable() {
1628 return;
1629 }
1630 let cursors: Vec<CursorSelection> = self.selections.iter().copied().collect();
1631 let intent = if cursors.iter().all(|sel| sel.is_empty()) {
1632 collapsed_intent
1633 } else {
1634 EditIntent::Atomic
1635 };
1636 let mut new_selections: Vec<CursorSelection> = Vec::with_capacity(cursors.len());
1637 for sel in &cursors {
1638 let range = if sel.is_empty() {
1639 collapsed_target(self, sel.cursor_offset())
1640 } else {
1641 sel.start..sel.end
1642 };
1643 let (start, end) = (range.start.min(range.end), range.start.max(range.end));
1644 let mut selection = *sel;
1645 selection.start = start;
1646 selection.end = end;
1647 new_selections.push(selection);
1648 }
1649 self.undo_manager.begin_transaction_with(intent);
1652 self.undo_manager
1653 .record_selections(cursors.clone(), cursors.clone());
1654 self.selections.replace_all(new_selections);
1655 self.undo_manager.set_pending_intent(intent);
1656
1657 let was_silent = self.silent_replace_text;
1658 self.silent_replace_text = silent;
1659 self.replace_text_in_range(None, "", window, cx);
1660 self.silent_replace_text = was_silent;
1661 self.undo_manager
1662 .record_selections(cursors, self.selections.iter().copied().collect());
1663 self.undo_manager.commit_transaction();
1664 self.pause_blink_cursor(cx);
1665 }
1666
1667 fn auto_close_target(&self, range: &Range<usize>, text: &str) -> Option<(usize, SharedString)> {
1670 if self.silent_replace_text
1671 || self.ime_marked_range.is_some()
1672 || !self.selections.is_single()
1673 || !self.active_selection().is_empty()
1674 || !range.is_empty()
1675 || range.start != self.cursor()
1676 || text.chars().count() != 1
1677 || !self.mode.is_auto_close()
1678 {
1679 return None;
1680 }
1681 let rules = self.mode.language_config()?;
1682 if self
1683 .text
1684 .chars_at(range.start)
1685 .next()
1686 .is_some_and(|c| !c.is_whitespace() && !rules.auto_close_before.contains(c))
1687 {
1688 return None;
1689 }
1690 for (open, close, not_in) in rules.closing_pairs() {
1691 let Some(prefix) = open.strip_suffix(text) else {
1692 continue;
1693 };
1694 if !self.text_before_matches(range.start, prefix) {
1695 continue;
1696 }
1697 let start = range.start - prefix.len();
1698 if open == close
1699 && (self.is_escaped_at(start)
1700 || self
1701 .text
1702 .chars_at(start)
1703 .reversed()
1704 .next()
1705 .is_some_and(|c| c.is_alphanumeric() || c == '_'))
1706 {
1707 continue;
1708 }
1709 if not_in.contains(&M::editing_syntax_context(self, start)) {
1710 continue;
1711 }
1712 return Some((open.len(), close.into()));
1713 }
1714 None
1715 }
1716
1717 fn text_before_matches(&self, offset: usize, text: &str) -> bool {
1718 offset >= text.len()
1719 && self.text.is_char_boundary(offset - text.len())
1720 && self.text.slice(offset - text.len()..offset) == text
1721 }
1722
1723 fn text_after_matches(&self, offset: usize, text: &str) -> bool {
1724 offset + text.len() <= self.text.len()
1725 && self.text.is_char_boundary(offset + text.len())
1726 && self.text.slice(offset..offset + text.len()) == text
1727 }
1728
1729 fn is_escaped_at(&self, offset: usize) -> bool {
1730 self.text
1731 .chars_at(offset)
1732 .reversed()
1733 .take_while(|c| *c == '\\')
1734 .count()
1735 % 2
1736 == 1
1737 }
1738
1739 fn skip_over_target(&self, new_text: &str) -> Option<usize> {
1746 if !self.mode.is_auto_close() || new_text.chars().count() != 1 {
1747 return None;
1748 }
1749 let rules = self.mode.language_config()?;
1750 let cursor = self.cursor();
1751 for (open, close, not_in) in rules.closing_pairs() {
1752 for (index, _) in close.char_indices() {
1753 if !close[index..].starts_with(new_text)
1754 || !self.text_before_matches(cursor, &close[..index])
1755 || !self.text_after_matches(cursor, &close[index..])
1756 {
1757 continue;
1758 }
1759 let context = M::editing_syntax_context(self, cursor);
1760 let generated =
1761 self.mode
1762 .auto_closed_pairs()
1763 .contains_closer(cursor, index, close.len());
1764 if !generated
1765 && not_in.contains(&context)
1766 && !(context == crate::input::SyntaxContext::String && open == close)
1767 {
1768 continue;
1769 }
1770 if self.is_escaped_at(cursor - index) {
1771 continue;
1772 }
1773 return Some(cursor + new_text.len());
1774 }
1775 }
1776 None
1777 }
1778
1779 pub(super) fn backspace(&mut self, _: &Backspace, window: &mut Window, cx: &mut Context<Self>) {
1780 if self.selections.is_single() && self.active_selection().is_empty() && self.cursor() == 0 {
1786 cx.propagate();
1787 return;
1788 }
1789
1790 if self.mode.is_auto_close()
1793 && self.selections.is_single()
1794 && self.active_selection().is_empty()
1795 {
1796 let off = self.cursor();
1797 let deletion = self.mode.language_config().and_then(|rules| {
1798 rules.closing_pairs().find_map(|(open, close, not_in)| {
1799 if !self.text_before_matches(off, open) || !self.text_after_matches(off, close)
1800 {
1801 return None;
1802 }
1803 let start = off - open.len();
1804 let generated = self
1805 .mode
1806 .auto_closed_pairs()
1807 .contains(start..off, off..off + close.len());
1808 if self.is_escaped_at(start)
1809 || (!generated && not_in.contains(&M::editing_syntax_context(self, start)))
1810 {
1811 return None;
1812 }
1813 Some(start..off + close.len())
1814 })
1815 });
1816 if let Some(range) = deletion {
1817 let utf16 = self.range_to_utf16(&range);
1818 self.replace_text_in_range_silent(Some(utf16), "", window, cx);
1819 return;
1820 }
1821 }
1822
1823 self.delete_selections(
1824 false,
1825 EditIntent::Backspace,
1826 |s, offset| s.previous_boundary(offset)..offset,
1827 window,
1828 cx,
1829 );
1830 }
1831
1832 pub(super) fn delete(&mut self, _: &Delete, window: &mut Window, cx: &mut Context<Self>) {
1833 self.delete_selections(
1834 false,
1835 EditIntent::DeleteForward,
1836 |s, offset| offset..s.next_boundary(offset),
1837 window,
1838 cx,
1839 );
1840 }
1841
1842 pub(super) fn delete_to_beginning_of_line(
1843 &mut self,
1844 _: &DeleteToBeginningOfLine,
1845 window: &mut Window,
1846 cx: &mut Context<Self>,
1847 ) {
1848 self.delete_selections(
1849 true,
1850 EditIntent::Atomic,
1851 |s, offset| {
1852 let mut start = s.start_of_line_at(offset, s.line_end_affinity_at(offset));
1853 if start == offset {
1854 start = start.saturating_sub(1);
1855 }
1856 start..offset
1857 },
1858 window,
1859 cx,
1860 );
1861 }
1862
1863 pub(super) fn delete_to_end_of_line(
1864 &mut self,
1865 _: &DeleteToEndOfLine,
1866 window: &mut Window,
1867 cx: &mut Context<Self>,
1868 ) {
1869 self.delete_selections(
1870 true,
1871 EditIntent::Atomic,
1872 |s, offset| {
1873 let mut end = s.end_of_line_at(offset, s.line_end_affinity_at(offset));
1874 if end == offset {
1875 end = (end + 1).clamp(0, s.text.len());
1876 }
1877 offset..end
1878 },
1879 window,
1880 cx,
1881 );
1882 }
1883
1884 pub(super) fn delete_previous_word(
1885 &mut self,
1886 _: &DeleteToPreviousWordStart,
1887 window: &mut Window,
1888 cx: &mut Context<Self>,
1889 ) {
1890 self.delete_selections(
1891 true,
1892 EditIntent::Atomic,
1893 |s, offset| s.previous_start_of_word_at(offset)..offset,
1894 window,
1895 cx,
1896 );
1897 }
1898
1899 pub(super) fn delete_next_word(
1900 &mut self,
1901 _: &DeleteToNextWordEnd,
1902 window: &mut Window,
1903 cx: &mut Context<Self>,
1904 ) {
1905 self.delete_selections(
1906 true,
1907 EditIntent::Atomic,
1908 |s, offset| offset..s.next_end_of_word_at(offset),
1909 window,
1910 cx,
1911 );
1912 }
1913
1914 pub(super) fn enter(&mut self, action: &Enter, window: &mut Window, cx: &mut Context<Self>) {
1915 if M::handle_context_menu_action(self, Box::new(action.clone()), window, cx) {
1916 return;
1917 }
1918
1919 if M::has_inline_completion(self) {
1921 M::clear_inline_completion(self, cx);
1922 }
1923
1924 let insert_newline = self.is_multi_line() && (!self.submit_on_enter || action.shift);
1929
1930 if insert_newline {
1931 if !self.selections.is_single() {
1932 self.selections.merge_overlapping();
1934 let selections: Vec<CursorSelection> = self.selections.iter().copied().collect();
1935 let mut edits: Vec<(Range<usize>, String)> = Vec::with_capacity(selections.len());
1936 for sel in &selections {
1937 let indent = if self.is_code_editor() {
1938 self.indent_of_next_line_at(sel.cursor_offset())
1939 } else {
1940 String::new()
1941 };
1942 edits.push((sel.start..sel.end, format!("\n{}", indent)));
1943 }
1944 edits.sort_by_key(|(range, _)| range.start);
1945 self.replace_text_in_ranges(&edits, window, cx);
1946 self.pause_blink_cursor(cx);
1947 } else {
1948 let mut split = false;
1954 if self.is_code_editor() && self.active_selection().is_empty() {
1955 if let Some(rules) = self.mode.language_config() {
1956 if self.mode.is_smart_indent()
1957 && M::editing_syntax_context(self, self.cursor())
1958 == crate::input::SyntaxContext::Code
1959 {
1960 let off = self.cursor();
1961 let is_pair = rules.brackets.iter().any(|p| {
1962 !p.open.is_empty()
1963 && !p.close.is_empty()
1964 && p.open != p.close
1965 && self.text_before_matches(off, p.open.as_ref())
1966 && self.text_after_matches(off, p.close.as_ref())
1967 });
1968 if is_pair {
1969 let line_end_affinity = self.line_end_affinity_at(off);
1971 let line_start = self.start_of_line_at(off, line_end_affinity);
1972 let mut indent = String::new();
1973 for c in self.text.slice(line_start..).chars() {
1974 if !c.is_whitespace() || c == '\n' || c == '\r' {
1975 break;
1976 }
1977 indent.push(c);
1978 }
1979 let mut inner = indent.clone();
1981 if self.mode.is_smart_indent() {
1982 let tab = self.mode.tab_size();
1983 if tab.hard_tabs {
1984 inner.push('\t');
1985 } else {
1986 for _ in 0..tab.tab_size {
1987 inner.push(' ');
1988 }
1989 }
1990 }
1991 let new_line_text = format!("\n{inner}\n{indent}");
1992 self.replace_text_in_range_silent(None, &new_line_text, window, cx);
1993 self.set_cursor_to(off + 1 + inner.len());
1994 self.update_preferred_column();
1995 let cursors = self.selections.iter().copied().collect::<Vec<_>>();
1996 self.undo_manager
1997 .record_selections(cursors.clone(), cursors);
1998 self.pause_blink_cursor(cx);
1999 split = true;
2000 }
2001 }
2002 }
2003 }
2004 if !split {
2005 let indent = if self.is_code_editor() {
2007 self.indent_of_next_line()
2008 } else {
2009 "".to_string()
2010 };
2011
2012 let new_line_text = format!("\n{}", indent);
2014 self.replace_text_in_range_silent(None, &new_line_text, window, cx);
2015 self.pause_blink_cursor(cx);
2016 }
2017 }
2018 } else {
2019 self.undo_manager.break_transaction_coalescing();
2022 cx.propagate();
2023 }
2024
2025 cx.emit(InputEvent::PressEnter {
2026 secondary: action.secondary,
2027 shift: action.shift,
2028 });
2029 }
2030
2031 pub fn clean(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2032 self.replace_text("", window, cx);
2033 self.set_selection(0, 0);
2034 self.scroll_to(0, None, cx);
2035 }
2036
2037 pub(super) fn escape(&mut self, action: &Escape, window: &mut Window, cx: &mut Context<Self>) {
2038 if M::handle_context_menu_action(self, Box::new(action.clone()), window, cx) {
2039 return;
2040 }
2041
2042 if !self.selections.is_single() {
2044 self.undo_manager.break_transaction_coalescing();
2045 self.selections.remove_all_but_active();
2046 cx.notify();
2047 return;
2048 }
2049
2050 if M::has_inline_completion(self) {
2052 M::clear_inline_completion(self, cx);
2053 return; }
2055
2056 if self.touch_selection().is_some() {
2058 self.dismiss_touch_selection(cx);
2059 return;
2060 }
2061
2062 if self.ime_marked_range.is_some() {
2063 self.unmark_text(window, cx);
2064 }
2065
2066 if self.clean_on_escape {
2067 return self.clean(window, cx);
2068 }
2069
2070 cx.propagate();
2071 }
2072
2073 pub(crate) fn handle_right_click_menu(
2075 &mut self,
2076 position: Point<Pixels>,
2077 offset: usize,
2078 window: &mut Window,
2079 cx: &mut Context<Self>,
2080 ) {
2081 if self.disabled {
2082 return;
2083 }
2084 if crate::GlobalState::is_in_deferred_context(cx) {
2085 return;
2086 }
2087
2088 if !self.active_selection().contains(offset) {
2089 self.move_to(offset, None, cx);
2090 }
2091
2092 if self.is_code_editor() {
2093 M::on_hover_definition(self, offset, window, cx);
2094 }
2095
2096 if let Some(handler) = self.context_menu_handler.clone() {
2097 let capabilities = self.context_menu_capabilities();
2098 cx.defer_in(window, move |_, window, cx| {
2099 handler(NativeMenu::new(), capabilities, position, window, cx);
2100 });
2101 }
2102 }
2103
2104 pub(super) fn add_cursor_above(
2105 &mut self,
2106 _: &AddCursorAbove,
2107 _: &mut Window,
2108 cx: &mut Context<Self>,
2109 ) {
2110 self.add_cursor_vertical(-1, cx);
2111 }
2112
2113 pub(super) fn add_cursor_below(
2114 &mut self,
2115 _: &AddCursorBelow,
2116 _: &mut Window,
2117 cx: &mut Context<Self>,
2118 ) {
2119 self.add_cursor_vertical(1, cx);
2120 }
2121
2122 fn add_cursor_vertical(&mut self, move_lines: isize, cx: &mut Context<Self>) {
2127 if !self.is_multi_line() {
2128 return;
2129 }
2130
2131 self.pause_blink_cursor(cx);
2132 self.undo_manager.break_transaction_coalescing();
2134
2135 let sources: Vec<(usize, Option<(Pixels, usize)>, bool)> = self
2136 .selections
2137 .iter()
2138 .map(|sel| {
2139 (
2140 sel.cursor_offset(),
2141 sel.column_anchor,
2142 self.line_end_affinity_for(sel),
2143 )
2144 })
2145 .collect();
2146 let mut offsets: std::collections::HashSet<usize> =
2147 sources.iter().map(|(offset, _, _)| *offset).collect();
2148
2149 let mut newest: Option<usize> = None;
2150 for (offset, anchor, line_end_affinity) in sources {
2151 let anchor = anchor.or_else(|| self.preferred_column_for(offset));
2152 let (target, _) = self.vertical_target(offset, anchor, line_end_affinity, move_lines);
2153 if target == offset || offsets.contains(&target) {
2154 continue;
2155 }
2156 offsets.insert(target);
2157 let id = self.selections.generate_id();
2158 let mut cursor = CursorSelection::new(id, target, target);
2159 cursor.column_anchor = anchor;
2160 self.selections.add(cursor);
2161 newest = Some(target);
2162 }
2163
2164 if let Some(newest) = newest {
2165 self.scroll_to(newest, None, cx);
2166 }
2167 cx.notify();
2168 }
2169
2170 pub(super) fn add_cursor_at(&mut self, offset: usize, cx: &mut Context<Self>) {
2175 if !self.is_multi_line() {
2176 return;
2177 }
2178
2179 for sel in self.selections.iter() {
2180 if sel.contains(offset) {
2181 return;
2182 }
2183 if sel.is_collapsed() && sel.cursor_offset() == offset {
2184 return;
2185 }
2186 }
2187
2188 self.undo_manager.break_transaction_coalescing();
2189 let id = self.selections.generate_id();
2190 self.selections
2191 .add(CursorSelection::new(id, offset, offset));
2192 cx.notify();
2193 }
2194
2195 pub(super) fn build_columnar_selection(
2203 &mut self,
2204 start: ColumnarPoint,
2205 end: ColumnarPoint,
2206 cx: &mut Context<Self>,
2207 ) {
2208 if !self.is_multi_line() {
2209 return;
2210 }
2211
2212 self.undo_manager.break_transaction_coalescing();
2213
2214 let (start_row, start_col) = self.columnar_row_column(start);
2215 let (end_row, end_col) = self.columnar_row_column(end);
2216 let (start_row, end_row) = (start_row.min(end_row), start_row.max(end_row));
2217 let (start_col, end_col) = (start_col.min(end_col), start_col.max(end_col));
2218
2219 let mut new_selections = Vec::with_capacity(end_row - start_row + 1);
2220 for row in start_row..=end_row {
2221 let sel_start = self
2222 .display_map
2223 .display_row_column_to_offset(row, start_col);
2224 let sel_end = self.display_map.display_row_column_to_offset(row, end_col);
2225 let id = self.selections.generate_id();
2226 let sel_start = self.text.clip_offset(sel_start, Bias::Left);
2227 let sel_end = self.text.clip_offset(sel_end, Bias::Left);
2228 new_selections.push(CursorSelection::new(id, sel_start, sel_end));
2229 }
2230
2231 self.selections.replace_all(new_selections);
2232 cx.notify();
2233 }
2234
2235 fn columnar_row_column(&self, point: ColumnarPoint) -> (usize, usize) {
2238 let display_point = self.display_map.offset_to_wrap_display_point(point.offset);
2239 let row = self
2240 .display_map
2241 .wrap_row_to_display_row(display_point.row)
2242 .unwrap_or_else(|| {
2243 self.display_map
2244 .nearest_visible_display_row(display_point.row)
2245 });
2246
2247 (row, display_point.column + point.columns_past_line_end)
2248 }
2249
2250 pub(super) fn on_mouse_down(
2251 &mut self,
2252 event: &MouseDownEvent,
2253 window: &mut Window,
2254 cx: &mut Context<Self>,
2255 ) {
2256 if window.default_prevented() {
2259 return;
2260 }
2261 self.undo_manager.break_transaction_coalescing();
2262 crate::global_state::GlobalState::suppress_text_selection(cx);
2265
2266 M::clear_inline_completion(self, cx);
2268 if event.button == MouseButton::Left
2272 && event.click_count == 1
2273 && self.reopen_edit_menu_at(event.position, cx)
2274 {
2275 return;
2276 }
2277 self.dismiss_touch_selection(cx);
2278
2279 if let Some(ime_marked_range) = &self.ime_marked_range {
2282 if ime_marked_range.len() == 0 {
2283 self.ime_marked_range = None;
2284 }
2285 }
2286
2287 self.selecting = true;
2288 let (offset, line_end_affinity, columns_past_line_end) =
2289 self.resolve_mouse_position(event.position);
2290
2291 if M::on_click(self, event, offset, window, cx) {
2292 return;
2293 }
2294
2295 if event.button == MouseButton::Left && event.click_count >= 3 {
2297 self.select_line(offset, window, cx);
2298 return;
2299 }
2300
2301 if event.button == MouseButton::Left && event.click_count == 2 {
2303 self.select_word(offset, window, cx);
2304 if crate::GlobalState::is_touch_press(cx) {
2307 self.keep_touch_selection(cx);
2308 }
2309 return;
2310 }
2311
2312 if event.button == MouseButton::Right {
2314 if self.enable_context_menu {
2315 if !self.active_selection().contains(offset) {
2316 self.move_to(offset, None, cx);
2317 }
2318 self.pending_context_menu = Some((event.position, offset));
2319 }
2320 return;
2321 }
2322
2323 if self.is_multi_line() && event.button == MouseButton::Left {
2325 if event.modifiers.alt
2326 && (event.modifiers.shift || (cfg!(target_os = "linux") && event.modifiers.control))
2327 {
2328 self.column_select_start = Some(ColumnarPoint::new(offset, columns_past_line_end));
2331 self.selecting = true;
2332 self.move_to_with_affinity(offset, None, line_end_affinity, cx);
2333 return;
2334 } else if event.modifiers.alt {
2335 self.add_cursor_at(offset, cx);
2336 self.column_select_start = Some(ColumnarPoint::new(offset, columns_past_line_end));
2339 return;
2340 }
2341 }
2342
2343 if event.modifiers.shift {
2344 self.select_to_with_affinity(offset, line_end_affinity, cx);
2345 } else {
2346 self.move_to_with_affinity(offset, None, line_end_affinity, cx)
2347 }
2348 }
2349
2350 pub(super) fn on_mouse_up(
2351 &mut self,
2352 event: &MouseUpEvent,
2353 window: &mut Window,
2354 cx: &mut Context<Self>,
2355 ) {
2356 if event.button == MouseButton::Right {
2357 if let Some((position, offset)) = self.pending_context_menu.take() {
2358 self.handle_right_click_menu(position, offset, window, cx);
2359 }
2360 }
2361 if self.active_selection().is_empty() {
2362 self.active_selection_mut().reversed = false;
2363 }
2364 self.selecting = false;
2365 self.selected_word_range = None;
2366 self.column_select_start = None;
2367 self.auto_scroll.stop();
2368 }
2369
2370 pub(super) fn on_mouse_move(
2371 &mut self,
2372 event: &MouseMoveEvent,
2373 window: &mut Window,
2374 cx: &mut Context<Self>,
2375 ) {
2376 let within_bounds = self
2378 .last_bounds
2379 .as_ref()
2380 .map(|bounds| bounds.contains(&event.position))
2381 .unwrap_or(false);
2382
2383 if !within_bounds {
2384 M::clear_hover_state(self, cx);
2386 return;
2387 }
2388
2389 let (offset, _) = self.index_for_mouse_position(event.position);
2391 M::on_mouse_move(self, offset, event, window, cx);
2392
2393 if self.is_code_editor() {
2394 if let Some(diagnostic) = self
2395 .mode
2396 .diagnostics()
2397 .and_then(|set| set.for_offset(offset))
2398 {
2399 self.diagnostic_popover = Some(Rc::new(diagnostic.clone()));
2400 cx.notify();
2401 } else {
2402 self.diagnostic_popover = None;
2403 }
2404 }
2405 }
2406
2407 pub(super) fn on_scroll_wheel(
2408 &mut self,
2409 event: &ScrollWheelEvent,
2410 window: &mut Window,
2411 cx: &mut Context<Self>,
2412 ) {
2413 let line_height = self
2414 .last_layout
2415 .as_ref()
2416 .map(|layout| layout.line_height)
2417 .unwrap_or(window.line_height());
2418 let delta = event.delta.pixel_delta(line_height);
2419
2420 let old_offset = self.scroll_handle.offset();
2421 self.update_scroll_offset(Some(old_offset + delta), cx);
2422
2423 if self.scroll_handle.offset() != old_offset {
2425 cx.stop_propagation();
2426 }
2427
2428 if self.diagnostic_popover.take().is_some() {
2429 cx.notify();
2430 }
2431 self.edit_menu_on_scroll(event.touch_phase, cx);
2434 }
2435
2436 pub(super) fn update_scroll_offset(
2437 &mut self,
2438 offset: Option<Point<Pixels>>,
2439 cx: &mut Context<Self>,
2440 ) {
2441 let mut offset = offset.unwrap_or(self.scroll_handle.offset());
2442 let safe_x_offset = if self.text_align == TextAlign::Left {
2444 px(0.)
2445 } else {
2446 -CURSOR_WIDTH
2447 };
2448
2449 let safe_y_range =
2450 (-self.scroll_size.height + self.input_bounds.size.height).min(px(0.0))..px(0.);
2451 let safe_x_range = (-self.scroll_size.width + self.input_bounds.size.width + safe_x_offset)
2452 .min(safe_x_offset)..px(0.);
2453
2454 offset.y = if self.is_single_line() {
2455 px(0.)
2456 } else {
2457 offset.y.clamp(safe_y_range.start, safe_y_range.end)
2458 };
2459 offset.x = offset.x.clamp(safe_x_range.start, safe_x_range.end);
2460 if self.scroll_handle.offset() != offset {
2461 self.scroll_handle.set_offset(offset);
2462 cx.notify();
2463 }
2464 }
2465
2466 pub(crate) fn scroll_to(
2470 &mut self,
2471 offset: usize,
2472 direction: Option<MoveDirection>,
2473 cx: &mut Context<Self>,
2474 ) {
2475 let padding = if direction.is_some() {
2476 ScrollPadding::SurroundingLines
2477 } else {
2478 ScrollPadding::Minimal
2479 };
2480 self.scroll_to_with_padding(offset, direction, padding, cx);
2481 }
2482
2483 pub(crate) fn scroll_to_with_padding(
2486 &mut self,
2487 offset: usize,
2488 direction: Option<MoveDirection>,
2489 padding: ScrollPadding,
2490 cx: &mut Context<Self>,
2491 ) {
2492 let Some(last_layout) = self.last_layout.as_ref() else {
2493 return;
2494 };
2495 let Some(bounds) = self.last_bounds.as_ref() else {
2496 return;
2497 };
2498
2499 let mut scroll_offset = self.scroll_handle.offset();
2500 let was_offset = scroll_offset;
2501 let line_height = last_layout.line_height;
2502
2503 let point = self.text.offset_to_point(offset);
2504
2505 let row = point.row;
2506
2507 let display_pos = self
2509 .display_map
2510 .buffer_pos_to_display_pos(crate::input::BufferPoint::new(row, point.column));
2511 let row_offset_y = line_height * display_pos.row;
2512
2513 let safety_margin = match last_layout.text_align {
2516 TextAlign::Left => RIGHT_MARGIN,
2517 TextAlign::Right => px(0.),
2518 TextAlign::Center => CURSOR_WIDTH,
2519 };
2520 if let Some(vi) = last_layout
2521 .visible_buffer_lines
2522 .iter()
2523 .position(|&line| line == row)
2524 {
2525 let line = &last_layout.lines[vi];
2526 let local_offset = offset.saturating_sub(last_layout.visible_line_byte_offsets[vi]);
2527 if let Some(pos) = line.position_for_index(local_offset, last_layout, false) {
2528 let bounds_width = bounds.size.width - last_layout.line_number_width;
2529 let col_offset_x = pos.x;
2530 if col_offset_x - safety_margin < -scroll_offset.x {
2531 scroll_offset.x = -col_offset_x + safety_margin;
2533 } else if col_offset_x + safety_margin > -scroll_offset.x + bounds_width {
2534 scroll_offset.x = -(col_offset_x - bounds_width + safety_margin);
2535 }
2536 }
2537 }
2538
2539 let edge_height =
2544 if matches!(padding, ScrollPadding::SurroundingLines) && self.is_code_editor() {
2545 super::element::cursor_surrounding_padding(
2546 self.mode.is_auto_grow(),
2547 self.cursor_surrounding_lines,
2548 super::element::viewport_visible_lines(bounds.size.height, line_height),
2549 line_height,
2550 )
2551 } else {
2552 line_height
2553 };
2554 if row_offset_y - edge_height + line_height < -scroll_offset.y {
2555 scroll_offset.y = -row_offset_y + edge_height - line_height;
2557 } else if row_offset_y + edge_height > -scroll_offset.y + bounds.size.height {
2558 scroll_offset.y = -(row_offset_y - bounds.size.height + edge_height);
2560 }
2561
2562 if direction == Some(MoveDirection::Up) {
2564 scroll_offset.y = scroll_offset.y.max(was_offset.y);
2565 } else if direction == Some(MoveDirection::Down) {
2566 scroll_offset.y = scroll_offset.y.min(was_offset.y);
2567 }
2568
2569 let safe_y_min = (-self.scroll_size.height + self.input_bounds.size.height).min(px(0.));
2573 scroll_offset.x = scroll_offset.x.min(px(0.));
2574 scroll_offset.y = scroll_offset.y.clamp(safe_y_min, px(0.));
2575 self.deferred_scroll_offset = Some(scroll_offset);
2576 cx.notify();
2577 }
2578
2579 pub(super) fn show_character_palette(
2580 &mut self,
2581 _: &ShowCharacterPalette,
2582 window: &mut Window,
2583 _: &mut Context<Self>,
2584 ) {
2585 window.show_character_palette();
2586 }
2587
2588 fn selected_texts(&self) -> Vec<String> {
2590 let mut selections: Vec<CursorSelection> = self
2591 .selections
2592 .iter()
2593 .copied()
2594 .filter(|sel| !sel.is_empty())
2595 .collect();
2596 selections.sort_by_key(|sel| sel.start);
2597 selections
2598 .iter()
2599 .map(|sel| self.text.slice(*sel).to_string())
2600 .collect()
2601 }
2602
2603 pub(super) fn copy(&mut self, _: &Copy, _: &mut Window, cx: &mut Context<Self>) {
2604 if !self.is_copyable() {
2605 return;
2606 }
2607
2608 let texts = self.selected_texts();
2609
2610 cx.write_to_clipboard(ClipboardItem::new_string(texts.join("\n")));
2611 }
2612
2613 pub(super) fn cut(&mut self, _: &Cut, window: &mut Window, cx: &mut Context<Self>) {
2614 if !self.is_copyable() {
2615 return;
2616 }
2617
2618 let texts = self.selected_texts();
2619
2620 cx.write_to_clipboard(ClipboardItem::new_string(texts.join("\n")));
2621
2622 self.undo_manager.set_pending_intent(EditIntent::Atomic);
2623 self.replace_text_in_range_silent(None, "", window, cx);
2624 }
2625
2626 pub(super) fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
2627 if !self.is_editable() {
2628 return;
2629 }
2630 let Some(clipboard) = cx.read_from_clipboard() else {
2631 return;
2632 };
2633 let mut new_text = clipboard.text().unwrap_or_default();
2634 self.undo_manager.set_pending_intent(EditIntent::Atomic);
2636
2637 if !self.is_multi_line() {
2638 new_text = new_text.replace('\n', "");
2639 self.replace_text_in_range_silent(None, &new_text, window, cx);
2640 self.scroll_to(self.cursor(), None, cx);
2641 return;
2642 }
2643
2644 if !self.selections.is_single() {
2647 self.selections.merge_overlapping();
2648 }
2649 let lines: Vec<String> = new_text.split('\n').map(|s| s.to_string()).collect();
2650 let count = self.selections.len();
2651 if count > 1 && lines.len() == count {
2652 let mut selections: Vec<CursorSelection> = self.selections.iter().copied().collect();
2653 selections.sort_by_key(|sel| sel.start);
2654 let edits: Vec<(Range<usize>, String)> = selections
2655 .iter()
2656 .zip(lines)
2657 .map(|(sel, line)| (sel.start..sel.end, line))
2658 .collect();
2659 self.replace_text_in_ranges(&edits, window, cx);
2660 } else {
2661 self.replace_text_in_range_silent(None, &new_text, window, cx);
2662 }
2663 self.scroll_to(self.cursor(), None, cx);
2664 }
2665
2666 fn typing_intent(&self, edits: &[(Range<usize>, String)], new_text: &str) -> EditIntent {
2669 if !new_text.is_empty()
2670 && !new_text.contains(['\n', '\r'])
2671 && edits.iter().all(|(range, _)| range.is_empty())
2672 {
2673 EditIntent::Typing
2674 } else {
2675 EditIntent::Atomic
2676 }
2677 }
2678
2679 fn collapse_for_intent(
2685 intent: EditIntent,
2686 mut selection: CursorSelection,
2687 range: &Range<usize>,
2688 ) -> CursorSelection {
2689 match intent {
2690 EditIntent::Backspace => selection.place_at(range.end, None),
2691 EditIntent::DeleteForward => selection.place_at(range.start, None),
2692 EditIntent::Typing | EditIntent::Atomic => {}
2693 }
2694 selection
2695 }
2696
2697 fn push_history(
2698 &mut self,
2699 text: &Rope,
2700 range: &Range<usize>,
2701 new_text: &str,
2702 requested_intent: Option<EditIntent>,
2703 selection_before: CursorSelection,
2704 selection_after: Option<CursorSelection>,
2705 ) -> bool {
2706 self.document_revision = self.document_revision.wrapping_add(1);
2707 let token_delta = self.edit_tokens(range, new_text.len());
2708 if self.undo_manager.is_ignoring() {
2709 return false;
2710 }
2711
2712 let range =
2713 text.clip_offset(range.start, Bias::Left)..text.clip_offset(range.end, Bias::Right);
2714 let old_text = text.slice(range.clone()).to_string();
2715 let new_range = range.start..range.start + new_text.len();
2716
2717 let intent = requested_intent.unwrap_or_else(|| {
2718 if range.is_empty()
2719 && old_text.is_empty()
2720 && !new_text.is_empty()
2721 && !new_text.contains(['\n', '\r'])
2722 {
2723 EditIntent::Typing
2724 } else {
2725 EditIntent::Atomic
2726 }
2727 });
2728
2729 let selection_before = Self::collapse_for_intent(intent, selection_before, &range);
2730 let selection_after =
2731 selection_after.unwrap_or_else(|| (new_range.end..new_range.end).into());
2732
2733 let open_transaction = self.undo_manager.has_open_transaction();
2734 let mut change = Change::new(range, &old_text, new_range, new_text);
2735 change.token_delta = token_delta;
2736 let recorded = self.undo_manager.record_transaction(change, intent);
2737 if recorded && !open_transaction {
2740 self.undo_manager
2741 .record_selections(vec![selection_before], vec![selection_after]);
2742 }
2743 recorded
2744 }
2745
2746 pub(super) fn undo(&mut self, _: &Undo, window: &mut Window, cx: &mut Context<Self>) {
2747 self.replay_history(true, window, cx);
2748 }
2749
2750 pub(super) fn redo(&mut self, _: &Redo, window: &mut Window, cx: &mut Context<Self>) {
2751 self.replay_history(false, window, cx);
2752 }
2753
2754 fn replay_history(&mut self, undo: bool, window: &mut Window, cx: &mut Context<Self>) {
2755 if !self.is_editable() {
2756 return;
2757 }
2758 let replay = if undo {
2759 self.undo_manager.undo()
2760 } else {
2761 self.undo_manager.redo()
2762 };
2763 let Some(replay) = replay else {
2764 return;
2765 };
2766 let token_aware = self.inline_tokens.is_some()
2767 || replay
2768 .changes
2769 .iter()
2770 .any(|change| change.token_delta.is_some());
2771 let emit_events = self.emit_events;
2772 if token_aware {
2773 self.emit_events = false;
2774 }
2775 self.replaying_history = token_aware;
2776 self.undo_manager.set_ignoring(true);
2777 for change in &replay.changes {
2778 let (range, text) = if undo {
2779 (change.new_range.into(), &change.old_text)
2780 } else {
2781 (change.old_range.into(), &change.new_text)
2782 };
2783 let range_utf16 = self.range_to_utf16(&range);
2784 self.replace_text_in_range_silent(Some(range_utf16), text, window, cx);
2785 self.replay_tokens(&range, text.len(), change.token_delta.as_deref(), undo);
2786 }
2787 self.restore_selections(replay.selections);
2788 self.mode
2789 .restore_auto_closed_pairs(replay.auto_closed_pairs.unwrap_or_default());
2790 if token_aware {
2791 self.ime_marked_range = None;
2794 }
2795 self.undo_manager.set_ignoring(false);
2796 self.replaying_history = false;
2797 self.emit_events = emit_events;
2798 if token_aware && emit_events {
2799 cx.emit(InputEvent::Change);
2800 }
2801 cx.notify();
2802 }
2803
2804 fn restore_selections(&mut self, selections: Option<Vec<CursorSelection>>) {
2808 let Some(selections) = selections else {
2809 return;
2810 };
2811
2812 let len = self.text.len();
2813 let restored: Vec<CursorSelection> = selections
2814 .into_iter()
2815 .map(|mut sel| {
2816 sel.start = sel.start.min(len);
2817 sel.end = sel.end.min(len);
2818 sel
2819 })
2820 .collect();
2821 self.selections.replace_all(restored);
2822 self.selections.merge_overlapping();
2823 }
2824
2825 pub fn cursor(&self) -> usize {
2829 if let Some(ime_marked_range) = &self.ime_marked_range {
2830 return ime_marked_range.end;
2831 }
2832
2833 self.selections.active().cursor_offset()
2834 }
2835
2836 pub(super) fn active_selection(&self) -> &CursorSelection {
2838 self.selections.active()
2839 }
2840
2841 pub(super) fn active_selection_mut(&mut self) -> &mut CursorSelection {
2843 self.selections.active_mut()
2844 }
2845
2846 pub(super) fn set_selection(&mut self, start: usize, end: usize) {
2849 let range = self.normalize_token_range(start..end);
2850 let (start, end) = (range.start, range.end);
2851 let active = self.active_selection_mut();
2852 active.start = start;
2853 active.end = end;
2854 }
2855
2856 pub(super) fn set_cursor_to(&mut self, offset: usize) {
2859 let offset = self.token_boundary(offset, Bias::Right);
2860 let active = self.active_selection_mut();
2861 active.start = offset;
2862 active.end = offset;
2863 active.reversed = false;
2864 }
2865
2866 pub fn visible_row_range(&self) -> Option<std::ops::Range<usize>> {
2868 self.last_layout.as_ref().map(|l| l.visible_range.clone())
2869 }
2870
2871 pub fn scroll_offset(&self) -> gpui::Point<gpui::Pixels> {
2873 self.scroll_handle.offset()
2874 }
2875
2876 pub fn set_scroll_offset(&mut self, offset: gpui::Point<gpui::Pixels>, cx: &mut Context<Self>) {
2880 self.deferred_scroll_offset = Some(offset);
2881 cx.notify();
2882 }
2883
2884 pub fn line_height(&self) -> Option<gpui::Pixels> {
2886 self.last_layout.as_ref().map(|l| l.line_height)
2887 }
2888
2889 pub fn selected_range(&self) -> std::ops::Range<usize> {
2897 (*self.selections.active()).into()
2898 }
2899
2900 pub fn select_all(&mut self, _: &mut Window, cx: &mut Context<Self>) {
2901 self.undo_manager.break_transaction_coalescing();
2902 self.selections.remove_all_but_active();
2903 self.set_selection(0, self.text.len());
2904 cx.notify();
2905 }
2906
2907 pub fn set_selected_range(&mut self, range: Range<usize>, cx: &mut Context<Self>) {
2912 let range = self.normalize_token_range(range);
2913 let end_bias = if range.start == range.end {
2914 Bias::Left
2915 } else {
2916 Bias::Right
2917 };
2918 let start = self.text.clip_offset(range.start, Bias::Left);
2919 let end = self.text.clip_offset(range.end, end_bias);
2920
2921 self.move_to(start, None, cx);
2922 self.active_selection_mut().reversed = false;
2923 self.selected_word_range = None;
2924 self.select_to(end, cx);
2925 }
2926
2927 pub(crate) fn index_for_mouse_position(&self, position: Point<Pixels>) -> (usize, bool) {
2935 let (offset, line_end_affinity, _) = self.resolve_mouse_position(position);
2936 (offset, line_end_affinity)
2937 }
2938
2939 pub(super) fn resolve_mouse_position(&self, position: Point<Pixels>) -> (usize, bool, usize) {
2946 if self.text.len() == 0 {
2948 return (0, false, 0);
2949 }
2950
2951 let (Some(bounds), Some(last_layout)) =
2952 (self.last_bounds.as_ref(), self.last_layout.as_ref())
2953 else {
2954 return (0, false, 0);
2955 };
2956
2957 let line_height = last_layout.line_height;
2958 let line_number_width = last_layout.line_number_width;
2959
2960 let inner_position = position - bounds.origin - point(line_number_width, px(0.));
2971
2972 let mut y_offset = last_layout.visible_top;
2973 let mut last_line_pos = None;
2976
2977 for (vi, (line_layout, _buffer_line)) in last_layout
2979 .lines
2980 .iter()
2981 .zip(last_layout.visible_buffer_lines.iter())
2982 .enumerate()
2983 {
2984 let line_start_offset = last_layout.visible_line_byte_offsets[vi];
2985
2986 let line_origin = point(px(0.), y_offset);
2988 let pos = inner_position - line_origin;
2989
2990 if self.is_single_line() {
2992 let local_index = line_layout.closest_index_for_x(pos.x, last_layout);
2993 return (
2995 self.resolve_index(line_start_offset + local_index),
2996 false,
2997 0,
2998 );
2999 }
3000
3001 if let Some((local_index, line_end_affinity)) =
3003 line_layout.closest_index_for_position(pos, last_layout)
3004 {
3005 return (
3006 self.resolve_index(line_start_offset + local_index),
3007 line_end_affinity,
3008 line_layout.columns_past_line_end(pos, last_layout),
3009 );
3010 } else if pos.y < px(0.) {
3011 return (self.resolve_index(line_start_offset), false, 0);
3013 }
3014
3015 y_offset += line_layout.size(line_height).height;
3016 last_line_pos = Some(pos);
3017 }
3018
3019 let columns_past_line_end = last_layout
3023 .lines
3024 .last()
3025 .zip(last_line_pos)
3026 .map(|(line_layout, pos)| {
3027 let last_row_top = (line_layout.size(line_height).height - line_height).max(px(0.));
3028 line_layout.columns_past_line_end(point(pos.x, last_row_top), last_layout)
3029 })
3030 .unwrap_or(0);
3031
3032 (self.text.len(), false, columns_past_line_end)
3033 }
3034
3035 fn resolve_index(&self, index: usize) -> usize {
3038 if self.masked {
3039 self.text.char_index_to_offset(index / MASK_CHAR.len_utf8())
3040 } else {
3041 index.min(self.text.len())
3042 }
3043 }
3044
3045 fn extend_selection(
3055 sel: &mut CursorSelection,
3056 offset: usize,
3057 word_range: Option<CursorSelection>,
3058 ) {
3059 if sel.reversed {
3060 sel.start = offset;
3061 } else {
3062 sel.end = offset;
3063 }
3064
3065 if sel.end < sel.start {
3066 sel.reversed = !sel.reversed;
3067 std::mem::swap(&mut sel.start, &mut sel.end);
3068 }
3069
3070 if let Some(word_range) = word_range {
3071 if sel.start > word_range.start {
3072 sel.start = word_range.start;
3073 }
3074 if sel.end < word_range.end {
3075 sel.end = word_range.end;
3076 }
3077 }
3078 }
3079
3080 pub(crate) fn select_to(&mut self, offset: usize, cx: &mut Context<Self>) {
3082 self.select_to_with_affinity(offset, false, cx);
3083 }
3084
3085 pub(crate) fn select_to_with_affinity(
3092 &mut self,
3093 offset: usize,
3094 line_end_affinity: bool,
3095 cx: &mut Context<Self>,
3096 ) {
3097 M::clear_inline_completion(self, cx);
3098
3099 self.cursor_line_end_affinity = line_end_affinity;
3100 let offset = self.cursor_boundary(offset, Bias::Left);
3101 let word_range = self.selected_word_range;
3102 Self::extend_selection(self.active_selection_mut(), offset, word_range);
3103 let range = self.normalize_token_range(self.selected_range());
3104 self.set_selection(range.start, range.end);
3105
3106 if self.active_selection().is_empty() {
3107 self.update_preferred_column();
3108 }
3109 cx.notify()
3110 }
3111
3112 fn select_all_cursors_to(
3115 &mut self,
3116 f: impl Fn(&Self, &CursorSelection) -> usize,
3117 cx: &mut Context<Self>,
3118 ) {
3119 self.pause_blink_cursor(cx);
3120 self.undo_manager.break_transaction_coalescing();
3121 M::clear_inline_completion(self, cx);
3122
3123 let new_selections: Vec<CursorSelection> = self
3124 .selections
3125 .iter()
3126 .map(|sel| {
3127 let offset = self.cursor_boundary(f(self, sel), Bias::Left);
3128 let mut new_sel = *sel;
3129 Self::extend_selection(&mut new_sel, offset, None);
3130 let range = self.normalize_token_range(new_sel.start..new_sel.end);
3131 new_sel.start = range.start;
3132 new_sel.end = range.end;
3133 new_sel
3134 })
3135 .collect();
3136 self.cursor_line_end_affinity = false;
3138 self.selections.replace_all(new_selections);
3139 self.selections.merge_overlapping();
3140
3141 if self.active_selection().is_empty() {
3142 self.update_preferred_column();
3143 }
3144 self.scroll_to(self.cursor(), None, cx);
3145 cx.notify()
3146 }
3147
3148 pub fn unselect(&mut self, _: &mut Window, cx: &mut Context<Self>) {
3150 self.undo_manager.break_transaction_coalescing();
3151 let offset = self.cursor();
3152 self.set_cursor_to(offset);
3153 cx.notify()
3154 }
3155
3156 #[inline]
3157 pub(super) fn offset_from_utf16(&self, offset: usize) -> usize {
3158 self.text.offset_utf16_to_offset(offset)
3159 }
3160
3161 #[inline]
3162 pub(super) fn offset_to_utf16(&self, offset: usize) -> usize {
3163 self.text.offset_to_offset_utf16(offset)
3164 }
3165
3166 #[inline]
3167 pub(crate) fn range_to_utf16(&self, range: &Range<usize>) -> Range<usize> {
3168 self.offset_to_utf16(range.start)..self.offset_to_utf16(range.end)
3169 }
3170
3171 #[inline]
3172 pub(super) fn range_from_utf16(&self, range_utf16: &Range<usize>) -> Range<usize> {
3173 self.offset_from_utf16(range_utf16.start)..self.offset_from_utf16(range_utf16.end)
3174 }
3175
3176 fn clamp_offset_to_visible_backward(&self, offset: usize) -> usize {
3179 let line = self.text.offset_to_point(offset).row;
3180 if self.display_map.is_buffer_line_hidden(line) {
3181 for fold in self.display_map.folded_ranges() {
3182 if line > fold.start_line && line <= fold.end_line {
3183 return self.text.line_end_offset(fold.start_line);
3184 }
3185 }
3186 }
3187 offset
3188 }
3189
3190 fn clamp_offset_to_visible_forward(&self, offset: usize) -> usize {
3193 let line = self.text.offset_to_point(offset).row;
3194 if self.display_map.is_buffer_line_hidden(line) {
3195 for fold in self.display_map.folded_ranges() {
3196 if line > fold.start_line && line <= fold.end_line {
3197 return self.text.line_start_offset(fold.end_line);
3198 }
3199 }
3200 }
3201 offset
3202 }
3203
3204 pub(super) fn cursor_boundary(&self, offset: usize, bias: Bias) -> usize {
3207 let offset = self.token_boundary(self.text.clip_offset(offset, bias), bias);
3208 if offset > 0
3209 && self.text.char_at(offset - 1) == Some('\r')
3210 && self.text.char_at(offset) == Some('\n')
3211 {
3212 if bias == Bias::Left {
3213 offset - 1
3214 } else {
3215 offset + 1
3216 }
3217 } else {
3218 offset
3219 }
3220 }
3221
3222 pub(super) fn previous_boundary(&self, offset: usize) -> usize {
3223 let offset = self.cursor_boundary(offset.saturating_sub(1), Bias::Left);
3224 self.clamp_offset_to_visible_backward(offset)
3225 }
3226
3227 pub(super) fn next_boundary(&self, offset: usize) -> usize {
3228 let offset = self.cursor_boundary(offset.saturating_add(1), Bias::Right);
3229 self.clamp_offset_to_visible_forward(offset)
3230 }
3231
3232 pub(crate) fn show_cursor(&self, window: &Window, cx: &App) -> bool {
3234 (self.focus_handle.is_focused(window) || M::is_context_menu_open(self, cx))
3235 && !self.disabled
3236 && self.blink_cursor.read(cx).visible()
3237 && window.is_window_active()
3238 }
3239
3240 fn on_focus(&mut self, _: &mut Window, cx: &mut Context<Self>) {
3241 self.blink_cursor.update(cx, |cursor, cx| {
3242 cursor.start(cx);
3243 });
3244 cx.emit(InputEvent::Focus);
3245 }
3246
3247 fn on_blur(&mut self, window: &mut Window, cx: &mut Context<Self>) {
3248 if M::is_context_menu_open(self, cx) {
3249 return;
3250 }
3251
3252 self.undo_manager.break_transaction_coalescing();
3253
3254 M::clear_hover_state(self, cx);
3258 self.diagnostic_popover = None;
3259 M::clear_inline_completion(self, cx);
3260 self.dismiss_touch_selection(cx);
3261 self.blink_cursor.update(cx, |cursor, cx| {
3262 cursor.stop(cx);
3263 });
3264 self.clamp_number_value(window, cx);
3265 cx.emit(InputEvent::Blur);
3266 cx.notify();
3267 }
3268
3269 fn clamp_number_value(&mut self, window: &mut Window, cx: &mut Context<Self>) {
3274 if !self.is_single_line() {
3275 return;
3276 }
3277 if !matches!(self.mask_pattern, MaskPattern::Number { .. }) {
3278 return;
3279 }
3280 if self.number_min.is_none() && self.number_max.is_none() {
3281 return;
3282 }
3283
3284 let Ok(value) = self.unmask_value().parse::<f64>() else {
3285 return;
3286 };
3287
3288 let clamped = match (self.number_min, self.number_max) {
3289 (Some(min), _) if value < min => min,
3290 (_, Some(max)) if value > max => max,
3291 _ => return,
3292 };
3293
3294 let new_text = clamped.to_string();
3297 if !self.is_valid_input(&new_text, cx) {
3298 return;
3299 }
3300
3301 let range = self.range_to_utf16(&(0..self.text.len()));
3302 self.replace_text_in_range_silent(Some(range), &new_text, window, cx);
3303 }
3304
3305 pub(super) fn pause_blink_cursor(&mut self, cx: &mut Context<Self>) {
3306 self.blink_cursor.update(cx, |cursor, cx| {
3307 cursor.pause(cx);
3308 });
3309 }
3310
3311 pub(super) fn on_drag_move(
3312 &mut self,
3313 event: &MouseMoveEvent,
3314 window: &mut Window,
3315 cx: &mut Context<Self>,
3316 ) {
3317 if self.text.len() == 0 {
3318 return;
3319 }
3320
3321 if self.last_layout.is_none() {
3322 return;
3323 }
3324
3325 if !self.focus_handle.is_focused(window) {
3326 return;
3327 }
3328
3329 if !self.selecting {
3330 return;
3331 }
3332
3333 self.auto_scroll.last_drag_position = Some(event.position);
3334 let (offset, line_end_affinity, columns_past_line_end) =
3335 self.resolve_mouse_position(event.position);
3336 if let Some(start) = self.column_select_start {
3337 let end = ColumnarPoint::new(offset, columns_past_line_end);
3338 self.build_columnar_selection(start, end, cx);
3339 } else {
3340 self.select_to_with_affinity(offset, line_end_affinity, cx);
3341 }
3342
3343 if !self.is_single_line() {
3344 let delta = AutoScroll::compute_delta(event.position.y, self.input_bounds);
3345 let scroll_delta = delta.map(|d| -d);
3347 self.auto_scroll.set(scroll_delta, cx, |delta, state, cx| {
3348 let current = state.scroll_handle.offset();
3349 state.update_scroll_offset(Some(point(current.x, current.y + delta)), cx);
3350 if let Some(pos) = state.auto_scroll.last_drag_position {
3351 let (offset, line_end_affinity, columns_past_line_end) =
3352 state.resolve_mouse_position(pos);
3353 if let Some(start) = state.column_select_start {
3354 let end = ColumnarPoint::new(offset, columns_past_line_end);
3355 state.build_columnar_selection(start, end, cx);
3356 } else {
3357 state.select_to_with_affinity(offset, line_end_affinity, cx);
3358 }
3359 }
3360 });
3361 }
3362 }
3363
3364 pub(super) fn normalize_input<'a>(&self, new_text: &'a str) -> Cow<'a, str> {
3370 let normalized = if matches!(self.mask_pattern, MaskPattern::Number { .. }) {
3371 normalize_number_input(new_text)
3372 } else {
3373 Cow::Borrowed(new_text)
3374 };
3375
3376 if self.is_single_line() && normalized.contains(['\n', '\r']) {
3377 Cow::Owned(normalized.replace(['\n', '\r'], ""))
3378 } else {
3379 normalized
3380 }
3381 }
3382
3383 pub(crate) fn is_valid_input(&self, new_text: &str, cx: &mut Context<Self>) -> bool {
3384 if new_text.is_empty() {
3385 return true;
3386 }
3387
3388 if let Some(validate) = &self.validate {
3389 if !validate(new_text, cx) {
3390 return false;
3391 }
3392 }
3393
3394 if !self.mask_pattern.is_valid(new_text) {
3395 return false;
3396 }
3397
3398 let Some(pattern) = &self.pattern else {
3399 return true;
3400 };
3401
3402 pattern.is_match(new_text)
3403 }
3404
3405 pub fn mask_pattern(mut self, pattern: impl Into<MaskPattern>) -> Self {
3415 self.mask_pattern = pattern.into();
3416 self.mask_pattern_set = true;
3417 if let Some(placeholder) = self.mask_pattern.placeholder() {
3418 self.placeholder = placeholder.into();
3419 }
3420 self
3421 }
3422
3423 pub fn set_mask_pattern(
3424 &mut self,
3425 pattern: impl Into<MaskPattern>,
3426 _: &mut Window,
3427 cx: &mut Context<Self>,
3428 ) {
3429 self.mask_pattern = pattern.into();
3430 self.mask_pattern_set = true;
3431 if let Some(placeholder) = self.mask_pattern.placeholder() {
3432 self.placeholder = placeholder.into();
3433 }
3434 cx.notify();
3435 }
3436
3437 pub fn ensure_number_mask(&mut self) {
3439 if self.mask_pattern_set {
3440 return;
3441 }
3442 self.mask_pattern = MaskPattern::Number {
3443 separator: None,
3444 fraction: None,
3445 };
3446 }
3447
3448 pub(super) fn set_input_bounds(&mut self, new_bounds: Bounds<Pixels>, cx: &mut Context<Self>) {
3449 let wrap_width_changed = self.input_bounds.size.width != new_bounds.size.width;
3450 self.input_bounds = new_bounds;
3451
3452 if let Some(last_layout) = self.last_layout.as_ref() {
3454 if wrap_width_changed {
3455 let wrap_width = if !self.soft_wrap {
3456 None
3458 } else {
3459 last_layout.wrap_width
3460 };
3461
3462 self.display_map.on_layout_changed(wrap_width, cx);
3463 if self.is_multi_line() {
3464 self.mode.update_auto_grow(&self.display_map);
3465 }
3466 cx.notify();
3467 }
3468 }
3469 }
3470
3471 pub fn selected_text(&self) -> RopeSlice<'_> {
3476 let range_utf16 = self.range_to_utf16(&self.selected_range());
3477 let range = self.range_from_utf16(&range_utf16);
3478 self.text.slice(range)
3479 }
3480
3481 pub fn range_to_bounds(&self, range: &Range<usize>) -> Option<Bounds<Pixels>> {
3485 let Some(last_layout) = self.last_layout.as_ref() else {
3486 return None;
3487 };
3488
3489 let Some(last_bounds) = self.last_bounds else {
3490 return None;
3491 };
3492
3493 let (_, _, start_pos) = self.line_and_position_for_offset(range.start);
3494 let (_, _, end_pos) = self.line_and_position_for_offset(range.end);
3495
3496 let Some(start_pos) = start_pos else {
3497 return None;
3498 };
3499 let Some(end_pos) = end_pos else {
3500 return None;
3501 };
3502
3503 Some(Bounds::from_corners(
3504 last_bounds.origin + start_pos,
3505 last_bounds.origin + end_pos + point(px(0.), last_layout.line_height),
3506 ))
3507 }
3508
3509 pub(crate) fn replace_text_in_range_silent(
3513 &mut self,
3514 range_utf16: Option<Range<usize>>,
3515 new_text: &str,
3516 window: &mut Window,
3517 cx: &mut Context<Self>,
3518 ) {
3519 self.silent_replace_text = true;
3520 self.replace_text_in_range(range_utf16, new_text, window, cx);
3521 self.silent_replace_text = false;
3522 }
3523
3524 pub(crate) fn replace_text_in_ranges(
3529 &mut self,
3530 edits: &[(Range<usize>, String)],
3531 window: &mut Window,
3532 cx: &mut Context<Self>,
3533 ) {
3534 if !self.is_editable() || edits.is_empty() {
3535 return;
3536 }
3537
3538 let mut sorted: Vec<(Range<usize>, &str)> = edits
3541 .iter()
3542 .map(|(range, text)| (self.normalize_token_range(range.clone()), text.as_str()))
3543 .collect();
3544 sorted.sort_by_key(|edit| std::cmp::Reverse(edit.0.start));
3545 if !self.token_spans().is_empty() {
3546 let mut merged: Vec<(Range<usize>, &str)> = Vec::with_capacity(sorted.len());
3547 for (range, text) in sorted {
3548 if let Some(last) = merged
3549 .last_mut()
3550 .filter(|last| range.end > last.0.start || range == last.0)
3551 {
3552 last.0.start = range.start.min(last.0.start);
3553 last.0.end = range.end.max(last.0.end);
3554 } else {
3555 merged.push((range, text));
3556 }
3557 }
3558 sorted = merged;
3559 }
3560
3561 #[cfg(debug_assertions)]
3562 for pair in sorted.windows(2) {
3563 debug_assert!(
3564 pair[1].0.end <= pair[0].0.start,
3565 "replace_text_in_ranges requires disjoint ranges"
3566 );
3567 }
3568
3569 let requested_intent = self.undo_manager.take_pending_intent();
3573 let selection_before = *self.active_selection();
3574 let original_selections: Vec<CursorSelection> = self.selections.iter().copied().collect();
3575 let selections_before: Vec<CursorSelection> = self
3579 .selections
3580 .iter()
3581 .map(|selection| {
3582 Self::collapse_for_intent(
3583 requested_intent.unwrap_or(EditIntent::Atomic),
3584 *selection,
3585 &(selection.start..selection.end),
3586 )
3587 })
3588 .collect();
3589 let group = sorted.len() > 1;
3590 if group {
3591 self.undo_manager.begin_transaction();
3592 }
3593
3594 let auto_closed_pairs_before = self.mode.auto_closed_pairs().clone();
3595 let mut recorded = false;
3596 for (range, new_text) in &sorted {
3597 let old_text = self.text.clone();
3598 self.mode.adjust_auto_closed_pair(range, new_text.len());
3599 self.text.replace(range.clone(), new_text);
3600
3601 M::adjust_annotations(self, range, new_text.len());
3602 recorded |= self.push_history(
3603 &old_text,
3604 range,
3605 new_text,
3606 requested_intent,
3607 selection_before,
3608 None,
3609 );
3610
3611 self.display_map
3613 .adjust_folds_for_edit(&old_text, range, new_text);
3614 self.display_map
3615 .on_text_changed(&self.text, range, &Rope::from(*new_text), cx);
3616
3617 self.mode.update_highlighter(
3618 super::mode::HighlighterUpdate {
3619 selected_range: range,
3620 old_text: &old_text,
3621 new_text: &self.text,
3622 change_text: new_text,
3623 force: true,
3624 },
3625 window,
3626 cx,
3627 );
3628
3629 self.update_fold_candidates_incremental(range, new_text);
3630 }
3631
3632 if group {
3633 self.undo_manager.commit_transaction();
3634 }
3635
3636 if let Some(diagnostics) = self.mode.diagnostics_mut() {
3638 diagnostics.reset(&self.text)
3639 }
3640 M::refresh_language_features(self, window, cx);
3641 self.update_search(cx);
3642
3643 let mut ascending: Vec<(Range<usize>, &str)> = sorted.clone();
3646 ascending.sort_by_key(|edit| edit.0.start);
3647 let text_len = self.text.len();
3648 let mut delta: isize = 0;
3649 let mut edit_results = Vec::with_capacity(ascending.len());
3650 for (range, new_text) in &ascending {
3651 let offset = ((range.start as isize + delta) as usize + new_text.len()).min(text_len);
3652 edit_results.push((range.clone(), offset));
3653 delta += new_text.len() as isize - (range.end as isize - range.start as isize);
3654 }
3655
3656 let mut used = vec![false; edit_results.len()];
3657 let mut new_selections: Vec<CursorSelection> = Vec::with_capacity(ascending.len());
3658 for selection in original_selections {
3659 if let Ok(index) = edit_results
3660 .binary_search_by_key(&(selection.start, selection.end), |(range, _)| {
3661 (range.start, range.end)
3662 })
3663 && !used[index]
3664 {
3665 let offset = edit_results[index].1;
3666 used[index] = true;
3667 let mut selection = selection;
3668 selection.place_at(offset, None);
3669 new_selections.push(selection);
3670 }
3671 }
3672 for (index, (_, offset)) in edit_results.into_iter().enumerate() {
3673 if !used[index] {
3674 new_selections.push(CursorSelection::new(
3675 self.selections.generate_id(),
3676 offset,
3677 offset,
3678 ));
3679 }
3680 }
3681 self.selections.replace_all(new_selections);
3682 self.selections.merge_overlapping();
3683
3684 let selections_after: Vec<CursorSelection> = self.selections.iter().copied().collect();
3686 if recorded {
3687 self.undo_manager.record_auto_closed_pairs(
3688 auto_closed_pairs_before,
3689 self.mode.auto_closed_pairs().clone(),
3690 );
3691 self.undo_manager
3692 .record_selections(selections_before, selections_after);
3693 }
3694
3695 self.ime_marked_range.take();
3696 self.update_preferred_column();
3697 self.dismiss_touch_selection(cx);
3698 if self.is_multi_line() {
3699 self.mode.update_auto_grow(&self.display_map);
3700 }
3701 if self.emit_events {
3702 cx.emit(InputEvent::Change);
3703 }
3704 cx.notify();
3705 }
3706
3707 fn update_fold_candidates(&mut self) {
3710 if !self.mode.is_folding() {
3711 return;
3712 }
3713
3714 let Some(highlighter_rc) = self.mode.highlighter() else {
3715 return;
3716 };
3717
3718 let highlighter = highlighter_rc.borrow();
3719 let Some(highlighter) = highlighter.as_ref() else {
3720 return;
3721 };
3722
3723 let fold_ranges = highlighter.fold_ranges(&self.text);
3724 self.display_map.set_fold_candidates(fold_ranges);
3725 }
3726
3727 fn update_fold_candidates_incremental(&mut self, edit_range: &Range<usize>, new_text: &str) {
3730 if !self.mode.is_folding() {
3731 return;
3732 }
3733
3734 let Some(highlighter_rc) = self.mode.highlighter() else {
3735 return;
3736 };
3737
3738 let highlighter = highlighter_rc.borrow();
3739 let Some(highlighter) = highlighter.as_ref() else {
3740 return;
3741 };
3742
3743 let new_end = edit_range.start + new_text.len();
3745 self.display_map.update_fold_candidates_for_edit(
3746 |range, text| highlighter.fold_ranges_for_edit(range, text),
3747 edit_range.start..new_end,
3748 &self.text,
3749 );
3750 }
3751}
3752
3753impl<M: InputModeKind> EntityInputHandler for InputBaseState<M> {
3754 fn text_for_range(
3755 &mut self,
3756 range_utf16: Range<usize>,
3757 adjusted_range: &mut Option<Range<usize>>,
3758 _window: &mut Window,
3759 _cx: &mut Context<Self>,
3760 ) -> Option<String> {
3761 let range = self.range_from_utf16(&range_utf16);
3762 adjusted_range.replace(self.range_to_utf16(&range));
3763 Some(self.text.slice(range).to_string())
3764 }
3765
3766 fn selected_text_range(
3767 &mut self,
3768 _ignore_disabled_input: bool,
3769 _window: &mut Window,
3770 _cx: &mut Context<Self>,
3771 ) -> Option<UTF16Selection> {
3772 Some(UTF16Selection {
3773 range: self.range_to_utf16(&self.selected_range()),
3774 reversed: false,
3775 })
3776 }
3777
3778 fn marked_text_range(
3779 &self,
3780 _window: &mut Window,
3781 _cx: &mut Context<Self>,
3782 ) -> Option<Range<usize>> {
3783 self.ime_marked_range
3784 .map(|range| self.range_to_utf16(&range.into()))
3785 }
3786
3787 fn unmark_text(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {
3788 self.ime_marked_range = None;
3789 self.undo_manager.commit_transaction();
3790 }
3791
3792 fn replace_text_in_range(
3797 &mut self,
3798 range_utf16: Option<Range<usize>>,
3799 new_text: &str,
3800 window: &mut Window,
3801 cx: &mut Context<Self>,
3802 ) {
3803 let requested_intent = self.undo_manager.take_pending_intent();
3804 if !self.is_editable() {
3805 return;
3806 }
3807 let selection_before = *self.active_selection();
3808 let ends_composition = self.ime_marked_range.is_some();
3811
3812 self.pause_blink_cursor(cx);
3813
3814 let new_text = if self.replaying_history {
3818 Cow::Borrowed(new_text)
3819 } else {
3820 self.normalize_input(new_text)
3821 };
3822 let new_text: &str = &new_text;
3823
3824 let range = range_utf16
3825 .as_ref()
3826 .map(|range_utf16| self.range_from_utf16(range_utf16))
3827 .or(self.ime_marked_range.map(|range| {
3828 let range = self.range_to_utf16(&(range.start..range.end));
3829 self.range_from_utf16(&range)
3830 }))
3831 .unwrap_or(self.selected_range());
3832 let range = self.normalize_token_range(range);
3833
3834 if range.is_empty()
3840 && range.start == self.cursor()
3841 && self.ime_marked_range.is_none()
3842 && !self.silent_replace_text
3843 && self.is_code_editor()
3844 && self.selections.is_single()
3845 && self.active_selection().is_empty()
3846 {
3847 if let Some(target) = self.skip_over_target(new_text) {
3848 self.set_cursor_to(target);
3849 self.update_preferred_column();
3850 cx.notify();
3851 return;
3852 }
3853 }
3854
3855 if self.is_multi_line() {
3856 let multi_cursor = range_utf16.is_none()
3857 && self.ime_marked_range.is_none()
3858 && !self.selections.is_single();
3859 if multi_cursor {
3860 self.selections.merge_overlapping();
3861 let mut edits: Vec<(Range<usize>, String)> = self
3862 .selections
3863 .iter()
3864 .map(|sel| (sel.start..sel.end, new_text.to_string()))
3865 .collect();
3866 edits.sort_by_key(|(range, _)| range.start);
3867 let intent =
3871 requested_intent.unwrap_or_else(|| self.typing_intent(&edits, new_text));
3872 self.undo_manager.begin_transaction_with(intent);
3873 self.undo_manager.set_pending_intent(intent);
3874 self.replace_text_in_ranges(&edits, window, cx);
3875 self.undo_manager.commit_transaction();
3876 } else {
3877 if range_utf16.is_some() {
3878 self.selections.remove_all_but_active();
3879 }
3880 if let Some(intent) = requested_intent {
3881 self.undo_manager.set_pending_intent(intent);
3882 }
3883 if let Some((open_len, closer)) = self.auto_close_target(&range, new_text) {
3884 let replacement = format!("{new_text}{closer}");
3886 self.replace_text_in_ranges(&[(range.clone(), replacement)], window, cx);
3887 let cursor = range.start + new_text.len();
3888 self.mode.track_auto_closed_pair(
3889 cursor - open_len..cursor,
3890 cursor..cursor + closer.len(),
3891 );
3892 self.undo_manager
3893 .record_auto_closed_pairs_after(self.mode.auto_closed_pairs().clone());
3894 self.set_cursor_to(cursor);
3895 self.update_preferred_column();
3896 self.undo_manager.record_selections(
3897 vec![selection_before],
3898 self.selections.iter().copied().collect(),
3899 );
3900 } else {
3901 self.replace_text_in_ranges(
3902 &[(range.clone(), new_text.to_string())],
3903 window,
3904 cx,
3905 );
3906 }
3907 }
3908 if ends_composition {
3909 self.undo_manager.commit_transaction();
3910 }
3911
3912 if !self.silent_replace_text {
3913 M::on_text_typed(self, &range, new_text, window, cx);
3914 }
3915 return;
3916 }
3917
3918 let old_text = self.text.clone();
3920 self.mode.adjust_auto_closed_pair(&range, new_text.len());
3921 self.text.replace(range.clone(), new_text);
3922
3923 let mut new_offset = (range.start + new_text.len()).min(self.text.len());
3924
3925 let mut mask_changed = false;
3928
3929 if self.is_single_line() {
3930 let pending_text = self.text.to_string();
3931 if !self.replaying_history
3937 && !self.validated_token_edit
3938 && !self.is_valid_input(&pending_text, cx)
3939 && self.is_valid_input(&old_text.to_string(), cx)
3940 {
3941 self.text = old_text;
3942 return;
3943 }
3944
3945 if !self.replaying_history && !self.mask_pattern.is_none() {
3946 let mask_text = self.mask_pattern.mask(&pending_text);
3947 mask_changed = mask_text.as_str() != pending_text;
3948 self.text = Rope::from(mask_text.as_str());
3949 let new_text_len =
3950 (new_text.len() + mask_text.len()).saturating_sub(pending_text.len());
3951 new_offset = (range.start + new_text_len).min(mask_text.len());
3952 }
3953 }
3954
3955 if mask_changed {
3956 M::reset_annotations(self);
3959 } else {
3960 M::adjust_annotations(self, &range, new_text.len());
3961 }
3962 if mask_changed {
3963 self.push_history(
3967 &old_text,
3968 &(0..old_text.len()),
3969 &self.text.to_string(),
3970 Some(EditIntent::Atomic),
3971 selection_before,
3972 Some((new_offset..new_offset).into()),
3973 );
3974 } else {
3975 self.push_history(
3976 &old_text,
3977 &range,
3978 &new_text,
3979 requested_intent,
3980 selection_before,
3981 None,
3982 );
3983 }
3984 if let Some(diagnostics) = self.mode.diagnostics_mut() {
3985 diagnostics.reset(&self.text)
3986 }
3987 self.display_map
3989 .adjust_folds_for_edit(&old_text, &range, new_text);
3990 self.display_map
3991 .on_text_changed(&self.text, &range, &Rope::from(new_text), cx);
3992
3993 self.mode.update_highlighter::<M>(
3994 super::mode::HighlighterUpdate {
3995 selected_range: &range,
3996 old_text: &old_text,
3997 new_text: &self.text,
3998 change_text: &new_text,
3999 force: true,
4000 },
4001 window,
4002 cx,
4003 );
4004
4005 self.update_fold_candidates_incremental(&range, new_text);
4006 M::refresh_language_features(self, window, cx);
4007 self.set_cursor_to(new_offset);
4008 self.ime_marked_range.take();
4009 if ends_composition {
4015 self.undo_manager
4016 .record_selections(vec![selection_before], vec![*self.active_selection()]);
4017 self.undo_manager.commit_transaction();
4018 }
4019 self.update_preferred_column();
4020 self.update_search(cx);
4021 self.dismiss_touch_selection(cx);
4022 if self.is_multi_line() {
4023 self.mode.update_auto_grow(&self.display_map);
4024 }
4025 if !self.silent_replace_text {
4026 M::on_text_typed(self, &range, &new_text, window, cx);
4027 }
4028 if self.emit_events {
4029 cx.emit(InputEvent::Change);
4030 }
4031 cx.notify();
4032 }
4033
4034 fn replace_and_mark_text_in_range(
4036 &mut self,
4037 range_utf16: Option<Range<usize>>,
4038 new_text: &str,
4039 new_selected_range_utf16: Option<Range<usize>>,
4040 window: &mut Window,
4041 cx: &mut Context<Self>,
4042 ) {
4043 let requested_intent = self.undo_manager.take_pending_intent();
4044 if !self.is_editable() {
4045 return;
4046 }
4047 let selection_before = *self.active_selection();
4048
4049 let starts_composition = self.ime_marked_range.is_none();
4050 if starts_composition {
4051 self.undo_manager.begin_transaction();
4052 }
4053
4054 self.selections.remove_all_but_active();
4056
4057 M::reset_language_features(self);
4058
4059 let new_text = if self.replaying_history {
4061 Cow::Borrowed(new_text)
4062 } else {
4063 self.normalize_input(new_text)
4064 };
4065 let new_text: &str = &new_text;
4066
4067 let range = range_utf16
4068 .as_ref()
4069 .map(|range_utf16| self.range_from_utf16(range_utf16))
4070 .or(self.ime_marked_range.map(|range| {
4071 let range = self.range_to_utf16(&(range.start..range.end));
4072 self.range_from_utf16(&range)
4073 }))
4074 .unwrap_or(self.selected_range());
4075 let range = self.normalize_token_range(range);
4076
4077 let auto_closed_pairs_before = self.mode.auto_closed_pairs().clone();
4078 let old_text = self.text.clone();
4079 self.mode.adjust_auto_closed_pair(&range, new_text.len());
4080 self.text.replace(range.clone(), new_text);
4081
4082 if self.is_single_line() {
4083 let pending_text = self.text.to_string();
4084 if !self.replaying_history
4086 && !self.validated_token_edit
4087 && !self.is_valid_input(&pending_text, cx)
4088 && self.is_valid_input(&old_text.to_string(), cx)
4089 {
4090 self.text = old_text;
4091 if starts_composition {
4092 self.undo_manager.commit_transaction();
4093 }
4094 return;
4095 }
4096 }
4097
4098 M::adjust_annotations(self, &range, new_text.len());
4099 if let Some(diagnostics) = self.mode.diagnostics_mut() {
4100 diagnostics.reset(&self.text)
4101 }
4102 self.display_map
4104 .adjust_folds_for_edit(&old_text, &range, new_text);
4105 self.display_map
4106 .on_text_changed(&self.text, &range, &Rope::from(new_text), cx);
4107
4108 self.mode.update_highlighter::<M>(
4109 super::mode::HighlighterUpdate {
4110 selected_range: &range,
4111 old_text: &old_text,
4112 new_text: &self.text,
4113 change_text: &new_text,
4114 force: true,
4115 },
4116 window,
4117 cx,
4118 );
4119
4120 self.update_fold_candidates_incremental(&range, new_text);
4121 M::refresh_language_features(self, window, cx);
4122 if new_text.is_empty() {
4123 self.set_cursor_to(range.start);
4125 self.ime_marked_range = None;
4126 } else {
4127 self.ime_marked_range = Some((range.start..range.start + new_text.len()).into());
4128 let new_range = new_selected_range_utf16
4129 .as_ref()
4130 .map(|range_utf16| {
4131 let new_text = Rope::from(new_text);
4132 range.start + new_text.offset_utf16_to_offset(range_utf16.start)
4133 ..range.start + new_text.offset_utf16_to_offset(range_utf16.end)
4134 })
4135 .unwrap_or_else(|| range.start + new_text.len()..range.start + new_text.len());
4136 self.set_selection(new_range.start, new_range.end);
4137 }
4138 if self.is_multi_line() {
4139 self.mode.update_auto_grow(&self.display_map);
4140 }
4141 if self.push_history(
4142 &old_text,
4143 &range,
4144 new_text,
4145 requested_intent,
4146 selection_before,
4147 Some(*self.active_selection()),
4148 ) {
4149 self.undo_manager
4150 .record_selections(vec![selection_before], vec![*self.active_selection()]);
4151 self.undo_manager.record_auto_closed_pairs(
4152 auto_closed_pairs_before,
4153 self.mode.auto_closed_pairs().clone(),
4154 );
4155 }
4156 if new_text.is_empty() {
4157 self.undo_manager.commit_transaction();
4158 }
4159 cx.notify();
4160 }
4161
4162 fn bounds_for_range(
4164 &mut self,
4165 range_utf16: Range<usize>,
4166 bounds: Bounds<Pixels>,
4167 _window: &mut Window,
4168 _cx: &mut Context<Self>,
4169 ) -> Option<Bounds<Pixels>> {
4170 let last_layout = self.last_layout.as_ref()?;
4171 let line_height = last_layout.line_height;
4172 let line_number_width = last_layout.line_number_width;
4173 let range = self.range_from_utf16(&range_utf16);
4174
4175 let mut start_origin = None;
4176 let mut end_origin = None;
4177 let line_number_origin = point(line_number_width, px(0.));
4178 let mut y_offset = last_layout.visible_top;
4179
4180 for (vi, line) in last_layout.lines.iter().enumerate() {
4181 if start_origin.is_some() && end_origin.is_some() {
4182 break;
4183 }
4184
4185 let index_offset = last_layout.visible_line_byte_offsets[vi];
4186
4187 if start_origin.is_none() {
4188 if let Some(p) = line.position_for_index(
4189 range.start.saturating_sub(index_offset),
4190 last_layout,
4191 false,
4192 ) {
4193 start_origin = Some(p + point(px(0.), y_offset));
4194 }
4195 }
4196
4197 if end_origin.is_none() {
4198 if let Some(p) = line.position_for_index(
4199 range.end.saturating_sub(index_offset),
4200 last_layout,
4201 false,
4202 ) {
4203 end_origin = Some(p + point(px(0.), y_offset));
4204 }
4205 }
4206
4207 y_offset += line.size(line_height).height;
4208 }
4209
4210 let start_origin = start_origin.unwrap_or_default();
4211 let mut end_origin = end_origin.unwrap_or_default();
4212 end_origin.y = start_origin.y;
4214
4215 Some(Bounds::from_corners(
4216 bounds.origin + line_number_origin + start_origin,
4217 bounds.origin + line_number_origin + point(end_origin.x, end_origin.y + line_height),
4219 ))
4220 }
4221
4222 fn character_index_for_point(
4223 &mut self,
4224 point: gpui::Point<Pixels>,
4225 _window: &mut Window,
4226 _cx: &mut Context<Self>,
4227 ) -> Option<usize> {
4228 let last_layout = self.last_layout.as_ref()?;
4229 let line_point = self.last_bounds?.localize(&point)?;
4230
4231 for (vi, line) in last_layout.lines.iter().enumerate() {
4232 let offset = last_layout.visible_line_byte_offsets[vi];
4233 if let Some(utf8_index) = line.index_for_position(line_point, last_layout) {
4234 return Some(self.offset_to_utf16(offset + utf8_index));
4235 }
4236 }
4237
4238 None
4239 }
4240}
4241
4242impl<M: InputModeKind> Focusable for InputBaseState<M> {
4243 fn focus_handle(&self, _cx: &App) -> FocusHandle {
4244 self.focus_handle.clone()
4245 }
4246}
4247
4248impl<M: InputModeKind> Render for InputBaseState<M> {
4249 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
4250 self.editor_style = self
4253 .projected_editor_style
4254 .resolved(&crate::Theme::global(cx).tokens);
4255 let entity = cx.entity();
4256 if self._pending_update {
4257 self.mode.update_highlighter::<M>(
4258 super::mode::HighlighterUpdate {
4259 selected_range: &(0..0),
4260 old_text: &self.text,
4261 new_text: &self.text,
4262 change_text: "",
4263 force: false,
4264 },
4265 window,
4266 cx,
4267 );
4268
4269 self.update_fold_candidates();
4270 M::refresh_language_features(self, window, cx);
4271 self._pending_update = false;
4272 }
4273
4274 let element = div()
4275 .id("input-state")
4276 .key_context(CONTEXT)
4277 .track_focus(&self.focus_handle)
4278 .when(self.is_editable(), |this| {
4279 this.on_action(window.listener_for(&entity, InputBaseState::backspace))
4280 .on_action(window.listener_for(&entity, InputBaseState::delete))
4281 .on_action(
4282 window.listener_for(&entity, InputBaseState::delete_to_beginning_of_line),
4283 )
4284 .on_action(window.listener_for(&entity, InputBaseState::delete_to_end_of_line))
4285 .on_action(window.listener_for(&entity, InputBaseState::delete_previous_word))
4286 .on_action(window.listener_for(&entity, InputBaseState::delete_next_word))
4287 .on_action(window.listener_for(&entity, InputBaseState::enter))
4288 .on_action(window.listener_for(&entity, InputBaseState::escape))
4289 .on_action(window.listener_for(&entity, InputBaseState::paste))
4290 .on_action(window.listener_for(&entity, InputBaseState::cut))
4291 .on_action(window.listener_for(&entity, InputBaseState::undo))
4292 .on_action(window.listener_for(&entity, InputBaseState::redo))
4293 .when(self.is_multi_line(), |this| {
4294 this.on_action(window.listener_for(&entity, InputBaseState::indent_inline))
4295 .on_action(window.listener_for(&entity, InputBaseState::outdent_inline))
4296 .on_action(window.listener_for(&entity, InputBaseState::indent_block))
4297 .on_action(window.listener_for(&entity, InputBaseState::outdent_block))
4298 })
4299 })
4300 .on_action(window.listener_for(&entity, InputBaseState::left))
4301 .on_action(window.listener_for(&entity, InputBaseState::right))
4302 .on_action(window.listener_for(&entity, InputBaseState::select_left))
4303 .on_action(window.listener_for(&entity, InputBaseState::select_right))
4304 .when(self.is_multi_line(), |this| {
4305 this.on_action(window.listener_for(&entity, InputBaseState::up))
4306 .on_action(window.listener_for(&entity, InputBaseState::down))
4307 .on_action(window.listener_for(&entity, InputBaseState::select_up))
4308 .on_action(window.listener_for(&entity, InputBaseState::select_down))
4309 .on_action(window.listener_for(&entity, InputBaseState::page_up))
4310 .on_action(window.listener_for(&entity, InputBaseState::page_down))
4311 .on_action(window.listener_for(&entity, InputBaseState::add_cursor_above))
4312 .on_action(window.listener_for(&entity, InputBaseState::add_cursor_below))
4313 })
4314 .on_action(window.listener_for(&entity, InputBaseState::on_action_select_all))
4315 .on_action(window.listener_for(&entity, InputBaseState::select_to_start_of_line))
4316 .on_action(window.listener_for(&entity, InputBaseState::select_to_end_of_line))
4317 .on_action(window.listener_for(&entity, InputBaseState::select_to_previous_word))
4318 .on_action(window.listener_for(&entity, InputBaseState::select_to_next_word))
4319 .on_action(window.listener_for(&entity, InputBaseState::home))
4320 .on_action(window.listener_for(&entity, InputBaseState::end))
4321 .on_action(window.listener_for(&entity, InputBaseState::move_to_start))
4322 .on_action(window.listener_for(&entity, InputBaseState::move_to_end))
4323 .on_action(window.listener_for(&entity, InputBaseState::move_to_previous_word))
4324 .on_action(window.listener_for(&entity, InputBaseState::move_to_next_word))
4325 .on_action(window.listener_for(&entity, InputBaseState::select_to_start))
4326 .on_action(window.listener_for(&entity, InputBaseState::select_to_end))
4327 .on_action(window.listener_for(&entity, InputBaseState::show_character_palette))
4328 .on_action({
4329 let entity = entity.clone();
4330 move |_: &ActivateToken, window, cx| {
4331 let state = entity.read(cx);
4332 let activation = state
4333 .token_spans()
4334 .iter()
4335 .find(|span| span.range() == state.selected_range())
4336 .and_then(|span| {
4337 state.range_to_bounds(&span.range()).and_then(|bounds| {
4338 state.token_activation(
4339 span.range().start,
4340 bounds,
4341 gpui::ClickEvent::Keyboard(gpui::KeyboardClickEvent {
4342 bounds,
4343 ..Default::default()
4344 }),
4345 )
4346 })
4347 });
4348 if let Some((listener, event)) = activation {
4349 listener(&event, window, cx);
4350 }
4351 }
4352 })
4353 .on_action(window.listener_for(&entity, InputBaseState::copy))
4354 .on_action(window.listener_for(&entity, InputBaseState::on_action_search))
4355 .on_action(window.listener_for(&entity, InputBaseState::on_action_replace))
4356 .on_mouse_down(
4357 MouseButton::Left,
4358 window.listener_for(&entity, InputBaseState::on_mouse_down),
4359 )
4360 .on_mouse_down(
4361 MouseButton::Right,
4362 window.listener_for(&entity, InputBaseState::on_mouse_down),
4363 )
4364 .on_mouse_up(
4365 MouseButton::Left,
4366 window.listener_for(&entity, InputBaseState::on_mouse_up),
4367 )
4368 .on_mouse_up(
4369 MouseButton::Right,
4370 window.listener_for(&entity, InputBaseState::on_mouse_up),
4371 )
4372 .on_mouse_move(window.listener_for(&entity, InputBaseState::on_mouse_move))
4373 .on_scroll_wheel(window.listener_for(&entity, InputBaseState::on_scroll_wheel))
4374 .when(self.is_multi_line() && !self.disabled, |this| {
4375 this.on_modifiers_changed(cx.listener(|_, _, _, cx| cx.notify()))
4376 })
4377 .when(!self.disabled, |this| {
4378 if self.is_multi_line() && window.modifiers().alt {
4379 this.cursor_crosshair()
4380 } else {
4381 this.cursor_text()
4382 }
4383 })
4384 .flex_1()
4385 .when(self.is_multi_line(), |this| this.h_full())
4386 .flex_grow_1()
4387 .overflow_x_hidden()
4388 .when(self.is_multi_line(), |this| {
4389 this.pt(self.editor_paddings.top)
4390 .pr(self.editor_paddings.right)
4391 .pb(self.editor_paddings.bottom)
4392 .pl(self.editor_paddings.left)
4393 })
4394 .child(TextElement::new(entity.clone()).placeholder(self.placeholder.clone()))
4395 .when(self.shows_scrollbar(), |this| {
4396 this.child(EditorScrollbar::new(entity.clone()))
4397 });
4398
4399 M::register_actions(element, &entity, window)
4402 }
4403}
4404
4405#[cfg(test)]
4406mod tests {
4407 use super::*;
4408 #[gpui::test]
4409 fn test_inline_token_wrap_and_size_refresh(cx: &mut TestAppContext) {
4410 use crate::input::{InlineToken, InlineTokenPresentation};
4411 let width = Rc::new(Cell::new(4000.));
4412 let render_width = width.clone();
4413 let view = InputView::build_textarea(cx, |state| state.rows(4).default_value("@a@b"));
4414 view.window_handle
4415 .update(cx, |_, window, cx| {
4416 view.input.update(cx, |state, cx| {
4417 state
4418 .replace_range_with_token(0..2, InlineToken::new("a", "@a"), window, cx)
4419 .unwrap();
4420 state
4421 .replace_range_with_token(2..4, InlineToken::new("b", "@b"), window, cx)
4422 .unwrap();
4423 state.set_token_presentation(
4424 InlineTokenPresentation::default()
4425 .token(move |_, _, _| div().w(px(render_width.get()))),
4426 );
4427 });
4428 })
4429 .unwrap();
4430 let mut visual = VisualTestContext::from_window(view.window_handle.into(), cx);
4431 visual.update(|window, cx| window.draw(cx).clear(cx));
4432 view.input.read_with(&visual, |state, _| {
4433 assert_eq!(state.display_map.wrap_row_count(), 2);
4434 let first = state.range_to_bounds(&(0..2)).unwrap();
4435 let second = state.range_to_bounds(&(2..4)).unwrap();
4436 assert!(second.origin.y > first.origin.y);
4437 assert_eq!(
4438 state.last_layout.as_ref().unwrap().lines[0].wrapped_lines[0].len,
4439 2
4440 );
4441 });
4442 width.set(100.);
4443 view.input.update(&mut visual, |_, cx| cx.notify());
4444 visual.update(|window, cx| window.draw(cx).clear(cx));
4445 view.input.read_with(&visual, |state, _| {
4446 assert_eq!(state.display_map.wrap_row_count(), 1)
4447 });
4448 }
4449
4450 #[gpui::test]
4451 fn test_inline_token_geometry_and_reentrant_activation(cx: &mut TestAppContext) {
4452 use crate::input::{InlineToken, InlineTokenPresentation};
4453 let view = InputView::build(cx, |state| state.default_value("before @alice after"));
4454 let target = view.input.clone();
4455 view.window_handle
4456 .update(cx, |_, window, cx| {
4457 view.input.update(cx, |state, cx| {
4458 state
4459 .replace_range_with_token(
4460 7..13,
4461 InlineToken::new("a", "@alice"),
4462 window,
4463 cx,
4464 )
4465 .unwrap();
4466 state.set_token_presentation(
4467 InlineTokenPresentation::default()
4468 .token(|_, _, _| div().w(px(100.)).h(px(20.)))
4469 .on_token_click(move |_, window, cx| {
4470 target.update(cx, |state, cx| {
4471 state.set_value("activated", window, cx)
4472 })
4473 }),
4474 );
4475 state.focus(window, cx);
4476 state.set_selected_range(7..13, cx);
4477 });
4478 })
4479 .unwrap();
4480 let mut visual = VisualTestContext::from_window(view.window_handle.into(), cx);
4481 visual.update(|window, cx| window.draw(cx).clear(cx));
4482 visual.update(|window, cx| {
4483 let state = view.input.read(cx);
4484 let bounds = state.range_to_bounds(&(7..13)).unwrap();
4485 assert!((bounds.size.width - px(100.)).abs() < px(1.), "{bounds:?}");
4486 let (left, _) = state.index_for_mouse_position(bounds.origin + point(px(10.), px(5.)));
4487 let (right, _) = state.index_for_mouse_position(bounds.origin + point(px(90.), px(5.)));
4488 assert_eq!((left, right), (7, 13));
4489 window.dispatch_action(Box::new(ActivateToken), cx);
4490 });
4491 visual.run_until_parked();
4492 view.input.read_with(&visual, |state, _| {
4493 assert_eq!(state.value().as_ref(), "activated")
4494 });
4495 }
4496
4497 #[gpui::test]
4498 fn test_inline_token_click_selects_it(cx: &mut TestAppContext) {
4499 use crate::input::InlineToken;
4500 cx.update(crate::init);
4501 let view = InputView::build(cx, |state| state.default_value("before @alice after"));
4502 view.window_handle
4503 .update(cx, |_, window, cx| {
4504 view.input.update(cx, |state, cx| {
4505 state
4506 .replace_range_with_token(
4507 7..13,
4508 InlineToken::new("a", "@alice"),
4509 window,
4510 cx,
4511 )
4512 .unwrap();
4513 state.set_selected_range(0..0, cx);
4514 });
4515 })
4516 .unwrap();
4517 let mut visual = VisualTestContext::from_window(view.window_handle.into(), cx);
4518 visual.update(|window, cx| window.draw(cx).clear(cx));
4519 let bounds = view
4520 .input
4521 .read_with(&visual, |state, _| state.range_to_bounds(&(7..13)).unwrap());
4522 visual.simulate_click(bounds.center(), gpui::Modifiers::default());
4523 visual.run_until_parked();
4524 view.input.read_with(&visual, |state, _| {
4525 assert_eq!(state.selected_range(), 7..13, "a click selects the token");
4526 });
4527 }
4528
4529 #[gpui::test]
4530 fn test_inline_token_edit_history_and_validation(cx: &mut TestAppContext) {
4531 let view = InputView::build(cx, |state| state.default_value("问 @alice!"));
4532 view.window_handle
4533 .update(cx, |_, window, cx| {
4534 view.input.update(cx, |state, cx| {
4535 let token =
4536 crate::input::InlineToken::new("alice-1", "@alice").with_label("Alice");
4537 state
4538 .replace_range_with_token(4..10, token.clone(), window, cx)
4539 .unwrap();
4540 assert_eq!(state.value().as_ref(), "问 @alice!");
4541 assert_eq!(state.tokens()[0].range(), 4..10);
4542 assert_eq!(state.next_end_of_word_at(4), 10);
4543 assert_eq!(state.previous_start_of_word_at(10), 4);
4544 state.undo(&Undo, window, cx);
4545 assert!(
4546 state.tokens().is_empty(),
4547 "identity-only association is undoable"
4548 );
4549 assert_eq!(state.value().as_ref(), "问 @alice!");
4550 state.redo(&Redo, window, cx);
4551 assert_eq!(state.tokens()[0].token(), &token);
4552 state.set_selected_range(6..7, cx);
4553 assert_eq!(state.selected_range(), 4..10);
4554 state.replace("", window, cx);
4555 assert_eq!(state.value().as_ref(), "问 !");
4556 assert!(state.tokens().is_empty());
4557 state.undo(&Undo, window, cx);
4558 assert_eq!(state.tokens()[0].token(), &token);
4559 state.set_selected_range(0..0, cx);
4560 state.replace("🙂", window, cx);
4561 assert_eq!(state.tokens()[0].range(), 8..14);
4562 state.undo(&Undo, window, cx);
4563 assert_eq!(state.tokens()[0].range(), 4..10);
4564 let before = state.content();
4565 state
4566 .replace_range_with_token(0..0, token.clone(), window, cx)
4567 .expect("the same reference may occur twice");
4568 assert_eq!(state.value().as_ref(), "@alice问 @alice!");
4569 assert_eq!(state.tokens().len(), 2);
4570 assert_eq!(state.tokens()[1].token(), &token);
4571 state.undo(&Undo, window, cx);
4572 assert_eq!(state.content(), before);
4573 state.set_value(before.text().clone(), window, cx);
4574 assert!(state.tokens().is_empty());
4575 assert!(!state.undo_manager.has_undos());
4576 });
4577 })
4578 .unwrap();
4579 }
4580
4581 #[gpui::test]
4582 fn test_inline_token_textarea_ime_and_modes(cx: &mut TestAppContext) {
4583 use crate::input::{InlineToken, InlineTokenError, InputContent};
4584 let view = InputView::build_textarea(cx, |state| state);
4585 view.window_handle
4586 .update(cx, |_, window, cx| {
4587 view.input.update(cx, |state, cx| {
4588 let content = InputContent::new("@a@b\n后面")
4589 .with_token(0..2, InlineToken::new("a", "@a"))
4590 .unwrap()
4591 .with_token(2..4, InlineToken::new("b", "@b"))
4592 .unwrap();
4593 state.set_value(content.clone(), window, cx);
4594 state.set_selected_range(2..2, cx);
4595 assert_eq!(state.previous_boundary(2), 0);
4596 assert_eq!(state.next_boundary(2), 4);
4597 state.replace_and_mark_text_in_range(Some(1..2), "中", Some(1..1), window, cx);
4598 assert_eq!(state.value().as_ref(), "中@b\n后面");
4599 assert_eq!(state.tokens().len(), 1);
4600 assert_eq!(
4601 state.replace_with_token(InlineToken::new("x", "x"), window, cx),
4602 Err(InlineTokenError::CompositionActive)
4603 );
4604 state.replace_text_in_range(None, "中文", window, cx);
4605 state.undo(&Undo, window, cx);
4606 assert_eq!(state.content(), content);
4607 state.redo(&Redo, window, cx);
4608 assert_eq!(state.value().as_ref(), "中文@b\n后面");
4609 state.undo(&Undo, window, cx);
4610 state.replace_all("plain", window, cx);
4611 assert!(state.tokens().is_empty());
4612 state.undo(&Undo, window, cx);
4613 assert_eq!(state.content(), content);
4614 });
4615 })
4616 .unwrap();
4617 let view = InputView::build(cx, |state| state);
4618 view.window_handle
4619 .update(cx, |_, window, cx| {
4620 view.input.update(cx, |state, cx| {
4621 assert_eq!(
4622 InputContent::new("a\u{301}")
4623 .with_token(0..1, InlineToken::new("bad", "a"))
4624 .err(),
4625 Some(InlineTokenError::InvalidBoundary)
4626 );
4627 assert_eq!(
4628 InputContent::new("@a@a")
4629 .with_token(0..2, InlineToken::new("a", "@a"))
4630 .unwrap()
4631 .with_token(1..3, InlineToken::new("x", "a@"))
4632 .err(),
4633 Some(InlineTokenError::OverlappingTokens)
4634 );
4635 state
4636 .replace_with_token(InlineToken::new("a", "@a"), window, cx)
4637 .unwrap();
4638 let before = state.content();
4639 state.set_masked(true, window, cx);
4640 assert!(!state.tokens_visible());
4641 assert_eq!(
4642 state.replace_with_token(InlineToken::new("b", "b"), window, cx),
4643 Err(InlineTokenError::UnsupportedMode)
4644 );
4645 assert_eq!(state.content(), before);
4646 state.undo(&Undo, window, cx);
4647 assert!(state.tokens().is_empty());
4648 state.redo(&Redo, window, cx);
4649 assert_eq!(state.content(), before);
4650 assert!(!state.tokens_visible());
4651 });
4652 })
4653 .unwrap();
4654 }
4655
4656 use crate::theme::Theme;
4657 use gpui::{TestAppContext, VisualTestContext, size};
4658
4659 use crate::input::{EditorMode, EditorState, InputMode, LanguageConfig, TextareaMode};
4660
4661 fn set_test_syntax_provider(
4662 provider: Rc<dyn crate::input::SyntaxContextProvider>,
4663 cx: &mut App,
4664 ) {
4665 struct TestLanguages(Rc<dyn crate::input::SyntaxContextProvider>);
4666 impl crate::input::LanguageProvider for TestLanguages {
4667 fn syntax_context_provider(
4668 &self,
4669 _: &str,
4670 ) -> Option<Rc<dyn crate::input::SyntaxContextProvider>> {
4671 Some(self.0.clone())
4672 }
4673 }
4674 crate::input::set_language_provider(Rc::new(TestLanguages(provider)), cx);
4675 }
4676
4677 struct TestRoot<M: InputModeKind>(Entity<InputBaseState<M>>);
4678
4679 impl<M: InputModeKind> Render for TestRoot<M> {
4680 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
4681 div().size_full().child(self.0.clone())
4682 }
4683 }
4684
4685 struct InputView<M: InputModeKind> {
4686 input: Entity<InputBaseState<M>>,
4687 window_handle: gpui::WindowHandle<TestRoot<M>>,
4688 }
4689
4690 impl<M: InputModeKind> InputView<M> {
4692 fn build_with(
4693 cx: &mut TestAppContext,
4694 make: impl FnOnce(&mut Window, &mut Context<InputBaseState<M>>) -> InputBaseState<M>
4695 + 'static,
4696 ) -> Self {
4697 let mut input: Option<Entity<InputBaseState<M>>> = None;
4698
4699 let window = cx.update(|cx| {
4700 cx.open_window(Default::default(), |window, cx| {
4701 cx.set_global(Theme::default());
4703 super::super::init(cx);
4705
4706 input = Some(cx.new(|cx| make(window, cx)));
4707
4708 cx.new(|_| TestRoot(input.clone().unwrap()))
4709 })
4710 .unwrap()
4711 });
4712
4713 Self {
4714 input: input.clone().unwrap(),
4715 window_handle: window,
4716 }
4717 }
4718 }
4719
4720 impl InputView<EditorMode> {
4721 fn new(cx: &mut TestAppContext) -> Self {
4723 Self::build_editor(cx, |state| state)
4724 }
4725
4726 fn build_editor(
4727 cx: &mut TestAppContext,
4728 f: impl FnOnce(InputBaseState<EditorMode>) -> InputBaseState<EditorMode> + 'static,
4729 ) -> Self {
4730 Self::build_with(cx, move |window, cx| {
4731 f(crate::input::EditorState::new(window, cx).language("sql"))
4732 })
4733 }
4734 }
4735
4736 impl InputView<TextareaMode> {
4737 fn build_textarea(
4738 cx: &mut TestAppContext,
4739 f: impl FnOnce(InputBaseState<TextareaMode>) -> InputBaseState<TextareaMode> + 'static,
4740 ) -> Self {
4741 Self::build_with(cx, move |window, cx| {
4742 f(crate::input::TextareaState::new(window, cx))
4743 })
4744 }
4745 }
4746
4747 impl InputView<InputMode> {
4748 fn build(
4750 cx: &mut TestAppContext,
4751 f: impl FnOnce(InputBaseState<InputMode>) -> InputBaseState<InputMode> + 'static,
4752 ) -> Self {
4753 Self::build_with(cx, move |window, cx| {
4754 f(crate::input::InputState::new(window, cx))
4755 })
4756 }
4757 }
4758
4759 #[gpui::test]
4760 fn test_noop_scroll_notifies_diagnostic_dismissal(cx: &mut TestAppContext) {
4761 use std::{cell::Cell, rc::Rc};
4762
4763 cx.update(crate::init);
4764 let mut input = None;
4765 let window = cx.open_window(size(px(400.), px(100.)), |window, cx| {
4766 input = Some(cx.new(|cx| crate::input::EditorState::new(window, cx)));
4767 gpui::EmptyView
4768 });
4769 let input = input.unwrap();
4770 input.update(cx, |state, cx| {
4771 state.input_bounds = Bounds::new(Point::default(), size(px(100.), px(20.)));
4772 state.scroll_size = size(px(100.), px(20.));
4773 state.present_diagnostic(crate::input::DiagnosticEntry::default(), cx);
4774 });
4775 let notifications = Rc::new(Cell::new(0));
4776 let count = notifications.clone();
4777 let _subscription =
4778 cx.update(|cx| cx.observe(&input, move |_, _| count.set(count.get() + 1)));
4779
4780 window
4781 .update(cx, |_, window, cx| {
4782 input.update(cx, |state, cx| {
4783 state.on_scroll_wheel(
4784 &ScrollWheelEvent {
4785 delta: gpui::ScrollDelta::Pixels(point(px(0.), px(-40.))),
4786 ..Default::default()
4787 },
4788 window,
4789 cx,
4790 );
4791 assert_eq!(state.scroll_handle.offset(), Point::default());
4792 assert!(state.diagnostic_popover().is_none());
4793 });
4794 })
4795 .unwrap();
4796 assert_eq!(
4797 notifications.get(),
4798 1,
4799 "clearing a diagnostic must notify even when scrolling is clamped"
4800 );
4801 window
4802 .update(cx, |_, window, cx| {
4803 input.update(cx, |state, cx| {
4804 state.on_scroll_wheel(
4805 &ScrollWheelEvent {
4806 delta: gpui::ScrollDelta::Pixels(point(px(0.), px(-40.))),
4807 ..Default::default()
4808 },
4809 window,
4810 cx,
4811 );
4812 });
4813 })
4814 .unwrap();
4815 assert_eq!(notifications.get(), 1, "an unchanged input must stay quiet");
4816 }
4817
4818 #[gpui::test]
4819 fn test_cursor_layout_consumer_updates_after_selection(cx: &mut TestAppContext) {
4820 use std::{cell::Cell, rc::Rc};
4821 struct Panel {
4822 input: Entity<crate::input::EditorState>,
4823 observed: Rc<Cell<Option<(Bounds<Pixels>, Pixels)>>>,
4824 }
4825 impl Render for Panel {
4826 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
4827 self.observed.set(self.input.read(cx).cursor_layout());
4828 div().size_full().child(self.input.clone())
4829 }
4830 }
4831 struct Root(Entity<Panel>);
4832 impl Render for Root {
4833 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
4834 div().size_full().child(
4835 self.0
4836 .clone()
4837 .cached(gpui::StyleRefinement::default().size_full()),
4838 )
4839 }
4840 }
4841 cx.update(crate::init);
4842 let observed = Rc::new(Cell::new(None));
4843 let mut input = None;
4844 let window = cx.open_window(size(px(400.), px(100.)), |window, cx| {
4845 let state = cx.new(|cx| {
4846 let mut state = crate::input::EditorState::new(window, cx);
4847 state.set_value("abcdefgh", window, cx);
4848 state
4849 });
4850 input = Some(state.clone());
4851 Root(cx.new(|_| Panel {
4852 input: state,
4853 observed: observed.clone(),
4854 }))
4855 });
4856 cx.run_until_parked();
4857 let input = input.unwrap();
4858 let before = input.read_with(cx, |state, _| state.cursor_layout());
4859 input.update(cx, |_, cx| cx.notify());
4864 cx.run_until_parked();
4865 assert_eq!(observed.get(), before);
4866 window
4867 .update(cx, |_, _, cx| {
4868 input.update(cx, |state, cx| state.select_to_with_affinity(3, false, cx));
4869 })
4870 .unwrap();
4871 cx.run_until_parked();
4872 window.update(cx, |_, _, cx| cx.notify()).unwrap();
4873 let after = input.read_with(cx, |state, _| state.cursor_layout());
4874 assert_ne!(after, before, "the caret must actually move");
4875 assert_eq!(
4876 observed.get(),
4877 after,
4878 "render consumers must receive the newly painted caret geometry"
4879 );
4880 }
4881
4882 #[gpui::test]
4883 fn test_input_scroll_offset_notifies_only_on_change(cx: &mut TestAppContext) {
4884 use std::{cell::Cell, rc::Rc};
4885
4886 cx.update(crate::init);
4887 let mut input = None;
4888 let _window = cx.open_window(size(px(400.), px(100.)), |window, cx| {
4889 input = Some(cx.new(|cx| crate::input::InputState::new(window, cx)));
4890 gpui::EmptyView
4891 });
4892 let input = input.unwrap();
4893 let notifications = Rc::new(Cell::new(0));
4894 let count = notifications.clone();
4895 let _subscription =
4896 cx.update(|cx| cx.observe(&input, move |_, _| count.set(count.get() + 1)));
4897
4898 input.update(cx, |state, _| {
4899 state.input_bounds = Bounds::new(Point::default(), size(px(100.), px(20.)));
4900 state.scroll_size = size(px(300.), px(20.));
4901 });
4902 for target in [
4903 None,
4904 Some(point(px(0.), px(0.))),
4905 Some(point(px(20.), px(50.))),
4906 ] {
4907 input.update(cx, |state, cx| state.update_scroll_offset(target, cx));
4908 assert_eq!(
4909 notifications.get(),
4910 0,
4911 "an unchanged clamped offset must stay quiet"
4912 );
4913 }
4914 input.update(cx, |state, cx| {
4915 state.update_scroll_offset(Some(point(px(-40.), px(0.))), cx);
4916 assert_eq!(state.scroll_handle.offset(), point(px(-40.), px(0.)));
4917 });
4918 assert_eq!(notifications.get(), 1, "a scroll must notify");
4919 input.update(cx, |state, cx| {
4920 state.update_scroll_offset(Some(point(px(-400.), px(50.))), cx);
4921 assert_eq!(state.scroll_handle.offset(), point(px(-200.), px(0.)));
4922 });
4923 assert_eq!(notifications.get(), 2);
4924 input.update(cx, |state, cx| {
4925 state.update_scroll_offset(Some(point(px(-500.), px(0.))), cx);
4926 });
4927 assert_eq!(
4928 notifications.get(),
4929 2,
4930 "clamping to the current offset must stay quiet"
4931 );
4932 input.update(cx, |state, cx| {
4933 state.scroll_size.width = px(110.);
4934 state.update_scroll_offset(None, cx);
4935 assert_eq!(state.scroll_handle.offset(), point(px(-10.), px(0.)));
4936 });
4937 assert_eq!(
4938 notifications.get(),
4939 3,
4940 "a smaller scroll range must clamp and notify"
4941 );
4942 }
4943
4944 #[gpui::test]
4945 fn test_set_value_on_unfocused_input_stays_quiet(cx: &mut TestAppContext) {
4946 use std::{cell::Cell, rc::Rc};
4947
4948 cx.update(crate::init);
4949 let mut input = None;
4950 let window = cx.open_window(size(px(400.), px(100.)), |window, cx| {
4951 input = Some(cx.new(|cx| crate::input::InputState::new(window, cx)));
4952 gpui::EmptyView
4953 });
4954 let input = input.unwrap();
4955 cx.run_until_parked();
4956
4957 let notifications = Rc::new(Cell::new(0));
4958 let count = notifications.clone();
4959 let _subscription =
4960 cx.update(|cx| cx.observe(&input, move |_, _| count.set(count.get() + 1)));
4961 cx.run_until_parked();
4962
4963 window
4966 .update(cx, |_, window, cx| {
4967 input.update(cx, |state, cx| state.set_value("seeded", window, cx));
4968 })
4969 .unwrap();
4970 cx.run_until_parked();
4971 let settled = notifications.get();
4972
4973 cx.executor()
4974 .advance_clock(std::time::Duration::from_secs(3));
4975 cx.run_until_parked();
4976
4977 assert_eq!(
4978 notifications.get(),
4979 settled,
4980 "an unfocused input is blinking, and every blink repaints the view it is in"
4981 );
4982 }
4983
4984 #[gpui::test]
4985 fn test_input_does_not_invalidate_cached_parent_during_paint(cx: &mut TestAppContext) {
4986 use std::{cell::Cell, rc::Rc};
4987
4988 struct Panel {
4989 input: Entity<crate::input::InputState>,
4990 renders: Rc<Cell<usize>>,
4991 }
4992 impl Render for Panel {
4993 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
4994 self.renders.set(self.renders.get() + 1);
4995 div().size_full().child(self.input.clone())
4996 }
4997 }
4998 struct Root(Entity<Panel>);
4999 impl Render for Root {
5000 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
5001 div().size_full().child(
5002 self.0
5003 .clone()
5004 .cached(gpui::StyleRefinement::default().size_full()),
5005 )
5006 }
5007 }
5008
5009 cx.update(crate::init);
5010 let renders = Rc::new(Cell::new(0));
5011 let mut input = None;
5012 let window = cx.open_window(size(px(400.), px(100.)), |window, cx| {
5013 let state = cx.new(|cx| crate::input::InputState::new(window, cx));
5014 input = Some(state.clone());
5015 Root(cx.new(|_| Panel {
5016 input: state,
5017 renders: renders.clone(),
5018 }))
5019 });
5020 cx.run_until_parked();
5021 let before = renders.get();
5022 assert!(before > 0);
5023 for _ in 0..60 {
5024 window.update(cx, |_, _, cx| cx.notify()).unwrap();
5025 }
5026 assert_eq!(
5027 renders.get(),
5028 before,
5029 "an idle input must not invalidate its cached parent"
5030 );
5031
5032 let input = input.unwrap();
5033 window
5034 .update(cx, |_, window, cx| {
5035 input.update(cx, |state, cx| state.set_value("changed", window, cx));
5036 })
5037 .unwrap();
5038 assert!(
5039 renders.get() > before,
5040 "text changes must invalidate the parent"
5041 );
5042 window.update(cx, |_, _, cx| cx.notify()).unwrap();
5044 let before = renders.get();
5045 for _ in 0..60 {
5046 window.update(cx, |_, _, cx| cx.notify()).unwrap();
5047 }
5048 assert_eq!(renders.get(), before, "the edited input must settle again");
5049
5050 cx.simulate_window_resize(window.into(), size(px(240.), px(100.)));
5051 cx.run_until_parked();
5052 assert!(renders.get() > before, "resizing must invalidate layout");
5053 }
5054
5055 #[gpui::test]
5056 fn textarea_cursor_treats_crlf_as_one_newline(cx: &mut TestAppContext) {
5057 cx.update(crate::init);
5058 let view = InputView::build_textarea(cx, |state| state);
5059 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
5060 cx.update(|window, cx| {
5061 view.input.update(cx, |state, cx| {
5062 for original in ["\r\nlast", "\u{feff}first\r\nlast\n", "first\nlast\r\n"] {
5063 state.set_value(original, window, cx);
5064 let newline = original.find('\n').unwrap();
5065 let end = if original.as_bytes()[newline.saturating_sub(1)] == b'\r' {
5066 newline - 1
5067 } else {
5068 newline
5069 };
5070 state.end(&MoveEnd, window, cx);
5071 assert_eq!(state.cursor(), end);
5072 state.right(&MoveRight, window, cx);
5073 assert_eq!(state.cursor(), newline + 1);
5074 state.left(&MoveLeft, window, cx);
5075 assert_eq!(state.cursor(), end);
5076 state.select_to_end_of_line(&SelectToEndOfLine, window, cx);
5077 assert_eq!(state.selected_range(), end..end);
5078 state.move_to_end(&MoveToEnd, window, cx);
5079 assert_eq!(state.cursor(), original.len());
5080 state.move_to_start(&MoveToStart, window, cx);
5081 assert_eq!(state.cursor(), 0);
5082 assert_eq!(state.value().as_bytes(), original.as_bytes());
5083 }
5084 state.set_value("a\rb", window, cx);
5086 state.right(&MoveRight, window, cx);
5087 assert_eq!(state.cursor(), 1);
5088 state.right(&MoveRight, window, cx);
5089 assert_eq!(state.cursor(), 2);
5090 })
5091 });
5092 }
5093
5094 #[gpui::test]
5095 fn only_a_multi_line_input_paints_scrollbars(cx: &mut TestAppContext) {
5096 cx.update(crate::init);
5097
5098 let single = InputView::build(cx, |state| state);
5102 single
5103 .input
5104 .update(cx, |state, _| assert!(!state.shows_scrollbar()));
5105
5106 let multi = InputView::build_textarea(cx, |state| state);
5107 multi
5108 .input
5109 .update(cx, |state, _| assert!(state.shows_scrollbar()));
5110 }
5111
5112 #[gpui::test]
5113 fn context_menu_handler_is_deferred_and_respects_disabled(cx: &mut TestAppContext) {
5114 use std::{cell::Cell, rc::Rc};
5115 cx.update(crate::init);
5116 let input_view = InputView::new(cx);
5117 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
5118 let input = input_view.input;
5119 let calls = Rc::new(Cell::new(0usize));
5120 let items = Rc::new(Cell::new(0usize));
5121
5122 cx.update(|window, cx| {
5123 input.update(cx, |state, cx| {
5124 let calls2 = calls.clone();
5125 let items2 = items.clone();
5126 state.on_context_menu(Rc::new(move |menu, _, _, _, _| {
5127 calls2.set(calls2.get() + 1);
5128 items2.set(menu.items.len());
5129 }));
5130 state.handle_right_click_menu(point(px(0.), px(0.)), 0, window, cx);
5131 })
5132 });
5133 assert_eq!(calls.get(), 1);
5134 assert_eq!(items.get(), 0);
5135
5136 cx.update(|window, cx| {
5137 input.update(cx, |state, cx| {
5138 state.disabled = true;
5139 state.handle_right_click_menu(point(px(0.), px(0.)), 0, window, cx);
5140 })
5141 });
5142 assert_eq!(calls.get(), 1);
5143 }
5144
5145 #[gpui::test]
5146 fn test_readonly_rejects_user_edits_only(cx: &mut TestAppContext) {
5147 let input_view = InputView::new(cx);
5148 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
5149 let input = input_view.input;
5150
5151 cx.update(|window, cx| {
5152 input.update(cx, |state, cx| {
5153 state.set_value("hello", window, cx);
5154 state.set_readonly(true, cx);
5155 });
5156 });
5157
5158 cx.update(|_, cx| {
5159 input.read_with(cx, |state, _| {
5160 assert!(!state.is_editable());
5161 assert!(!state.is_replaceable());
5162 });
5163 });
5164
5165 cx.update(|window, cx| {
5167 input.update(cx, |state, cx| {
5168 state.replace_text_in_range(None, " world", window, cx);
5169 state.replace_and_mark_text_in_range(None, "あ", None, window, cx);
5170 });
5171 });
5172 cx.update(|_, cx| {
5173 input.read_with(cx, |state, _| assert_eq!(state.value(), "hello"));
5174 });
5175
5176 cx.update(|window, cx| {
5178 input.update(cx, |state, cx| {
5179 state.insert(" world", window, cx);
5180 state.set_value("changed", window, cx);
5181 });
5182 });
5183 cx.update(|_, cx| {
5184 input.read_with(cx, |state, _| assert_eq!(state.value(), "changed"));
5185 });
5186
5187 cx.update(|window, cx| {
5190 input.update(cx, |state, cx| {
5191 state.set_readonly(false, cx);
5192 state.replace_text_in_range(None, "!", window, cx);
5193 });
5194 });
5195 cx.update(|_, cx| {
5196 input.read_with(cx, |state, _| {
5197 assert!(state.is_editable());
5198 assert_eq!(state.value(), "!changed");
5199 });
5200 });
5201 }
5202
5203 #[gpui::test]
5209 fn test_scroll_to_eob_does_not_overshoot_safe_range(cx: &mut TestAppContext) {
5210 let input_view = InputView::new(cx);
5211 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
5212 let input = input_view.input;
5213
5214 cx.update(|window, cx| {
5216 input.update(cx, |state, cx| {
5217 state.set_scroll_beyond_last_line(Some(1), window, cx);
5218 state.set_cursor_surrounding_lines(Some(1), window, cx);
5219 let text: String = (1..=50)
5220 .map(|i| format!("line {i}"))
5221 .collect::<Vec<_>>()
5222 .join("\n");
5223 state.set_value(text, window, cx);
5224 });
5225 });
5226 cx.run_until_parked();
5227
5228 cx.update(|_, cx| {
5231 input.read_with(cx, |state, _| {
5232 assert!(
5233 state.scroll_size.height > px(0.),
5234 "scroll_size not populated by initial paint"
5235 );
5236 assert!(
5237 state.input_bounds.size.height > px(0.),
5238 "input_bounds not populated by initial paint"
5239 );
5240 });
5241 });
5242
5243 cx.update(|_, cx| {
5248 input.update(cx, |state, cx| {
5249 let end = state.text.len();
5250 state.move_to(end, Some(MoveDirection::Down), cx);
5251
5252 let deferred = state
5253 .deferred_scroll_offset
5254 .expect("scroll_to should populate deferred_scroll_offset");
5255 let safe_y_min =
5256 (-state.scroll_size.height + state.input_bounds.size.height).min(px(0.));
5257
5258 assert!(
5259 deferred.y >= safe_y_min,
5260 "deferred_scroll_offset.y = {:?} below safe_y_min = {:?} \
5261 — paint would jitter (Bug C regression)",
5262 deferred.y,
5263 safe_y_min,
5264 );
5265 });
5266 });
5267 }
5268
5269 #[gpui::test]
5270 fn test_next_search_match_reveals_with_padding_after_manual_scroll(cx: &mut TestAppContext) {
5271 assert_search_reveals_with_padding_after_manual_scroll(false, cx);
5272 }
5273
5274 #[gpui::test]
5275 fn test_previous_search_match_reveals_with_padding_after_manual_scroll(
5276 cx: &mut TestAppContext,
5277 ) {
5278 assert_search_reveals_with_padding_after_manual_scroll(true, cx);
5279 }
5280
5281 fn assert_search_reveals_with_padding_after_manual_scroll(
5282 previous: bool,
5283 cx: &mut TestAppContext,
5284 ) {
5285 let input_view = InputView::new(cx);
5286 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
5287 let input = input_view.input;
5288 let text = (0..160)
5289 .map(|row| {
5290 if matches!(row, 20 | 60 | 100) {
5291 format!("match on row {row}")
5292 } else {
5293 format!("line {row}")
5294 }
5295 })
5296 .collect::<Vec<_>>()
5297 .join("\n");
5298 let start = text.match_indices("match").nth(1).unwrap().0;
5299 let expected_match = start..start + "match".len();
5300 cx.update(|window, cx| {
5301 input.update(cx, |state, cx| {
5302 state.set_cursor_surrounding_lines(Some(3), window, cx);
5303 state.set_value(text, window, cx);
5304 state.set_search_query("match", true, cx);
5305 if previous {
5306 state.search_session.matcher.next();
5307 state.search_session.matcher.next();
5308 }
5309 });
5310 });
5311 cx.run_until_parked();
5312 cx.update(|_, cx| {
5313 input.update(cx, |state, cx| {
5314 let line_height = state.last_layout.as_ref().unwrap().line_height;
5315 let y = if previous { px(0.) } else { -line_height * 80. };
5316 state.set_scroll_offset(point(px(0.), y), cx);
5317 });
5318 });
5319 cx.run_until_parked();
5320 input.read_with(&cx, |state, _| {
5321 let visible = state.visible_row_range().unwrap();
5322 if previous {
5323 assert!(visible.end <= 60, "target must be below the viewport");
5324 } else {
5325 assert!(visible.start > 60, "target must be above the viewport");
5326 }
5327 });
5328 cx.update(|_, cx| {
5329 input.update(cx, |state, cx| {
5330 let range = if previous {
5331 state.previous_search_match(cx)
5332 } else {
5333 state.next_search_match(cx)
5334 };
5335 assert_eq!(range, Some(expected_match));
5336 assert_eq!(state.search_session.matcher.label(), "2/3");
5337 });
5338 });
5339 cx.run_until_parked();
5340 input.read_with(&cx, |state, _| {
5341 assert!(state.visible_row_range().unwrap().contains(&60));
5342 let line_height = state.last_layout.as_ref().unwrap().line_height;
5343 let target_y = line_height * 60. + state.scroll_handle.offset().y;
5344 assert!(target_y >= line_height * 2. - px(0.1));
5346 assert!(
5347 target_y + line_height * 3.
5348 <= state.last_bounds.as_ref().unwrap().size.height + px(0.1),
5349 "search must preserve the configured surrounding-line padding"
5350 );
5351 });
5352 }
5353
5354 struct SearchHost {
5356 editor: Entity<InputBaseState<EditorMode>>,
5357 search_requests: Rc<Cell<usize>>,
5358 }
5359
5360 impl Render for SearchHost {
5361 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
5362 let search_requests = self.search_requests.clone();
5363 div()
5364 .size_full()
5365 .on_action(cx.listener(move |_, _: &Search, _, _| {
5366 search_requests.set(search_requests.get() + 1);
5367 }))
5368 .child(self.editor.clone())
5369 }
5370 }
5371
5372 fn press_search_shortcut(
5375 cx: &mut TestAppContext,
5376 searchable: bool,
5377 ) -> (Entity<InputBaseState<EditorMode>>, Rc<Cell<usize>>) {
5378 let search_requests = Rc::new(Cell::new(0));
5379 let mut editor = None;
5380 let window = cx.update(|cx| {
5381 cx.open_window(Default::default(), |window, cx| {
5382 cx.set_global(Theme::default());
5383 super::super::init(cx);
5384 let state =
5385 cx.new(|cx| crate::input::EditorState::new(window, cx).searchable(searchable));
5386 editor = Some(state.clone());
5387 cx.new(|_| SearchHost {
5388 editor: state,
5389 search_requests: search_requests.clone(),
5390 })
5391 })
5392 .unwrap()
5393 });
5394 let editor = editor.unwrap();
5395 let mut cx = VisualTestContext::from_window(window.into(), cx);
5396 cx.update(|window, cx| {
5397 editor.update(cx, |state, cx| state.focus(window, cx));
5398 });
5399 cx.run_until_parked();
5400 #[cfg(target_os = "macos")]
5401 cx.simulate_keystrokes("cmd-f");
5402 #[cfg(not(target_os = "macos"))]
5403 cx.simulate_keystrokes("ctrl-f");
5404 cx.run_until_parked();
5405 (editor, search_requests)
5406 }
5407
5408 #[gpui::test]
5409 fn test_search_shortcut_reaches_the_host_when_not_searchable(cx: &mut TestAppContext) {
5410 let (editor, search_requests) = press_search_shortcut(cx, false);
5411 assert_eq!(search_requests.get(), 1);
5412 editor.read_with(cx, |state, _| {
5413 assert!(!state.search_session().open);
5414 assert!(!state.search_session().is_active());
5415 });
5416 }
5417
5418 #[gpui::test]
5419 fn test_search_shortcut_opens_the_panel_when_searchable(cx: &mut TestAppContext) {
5420 let (editor, search_requests) = press_search_shortcut(cx, true);
5421 assert_eq!(search_requests.get(), 0);
5422 editor.read_with(cx, |state, _| {
5423 assert!(state.search_session().open);
5424 assert!(state.search_session().is_active());
5425 });
5426 }
5427
5428 #[gpui::test]
5429 fn test_set_search_query_highlights_without_the_panel(cx: &mut TestAppContext) {
5430 let input_view = InputView::build_editor(cx, |state| state.searchable(false));
5431 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
5432 let input = input_view.input;
5433 cx.update(|window, cx| {
5434 input.update(cx, |state, cx| {
5435 state.set_value("foo bar foo", window, cx);
5436 state.set_search_query("foo", true, cx);
5437 });
5438 });
5439 cx.run_until_parked();
5440 input.read_with(&cx, |state, _| {
5441 let session = state.search_session();
5442 assert!(session.is_active());
5443 assert!(!session.open);
5444 assert_eq!(session.matcher.len(), 2);
5445 });
5446 cx.update(|_, cx| {
5447 input.update(cx, |state, cx| state.close_search(cx));
5448 });
5449 input.read_with(&cx, |state, _| {
5450 assert!(!state.search_session().is_active());
5451 });
5452 }
5453
5454 #[gpui::test]
5455 fn test_search_reveals_offscreen_wrapped_match(cx: &mut TestAppContext) {
5456 let input_view = InputView::new(cx);
5457 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
5458 let input = input_view.input;
5459 let text = format!(
5460 "match\n{}\n{}match\n{}",
5461 "line\n".repeat(80),
5462 "wrapped text ".repeat(500),
5463 "line\n".repeat(80)
5464 );
5465 cx.update(|window, cx| {
5466 input.update(cx, |state, cx| {
5467 state.set_cursor_surrounding_lines(Some(3), window, cx);
5468 state.set_value(text, window, cx);
5469 state.set_search_query("match", true, cx);
5470 });
5471 });
5472 cx.run_until_parked();
5473 cx.update(|_, cx| {
5474 input.update(cx, |state, cx| {
5475 state.next_search_match(cx).unwrap();
5476 });
5477 });
5478 cx.run_until_parked();
5479 input.read_with(&cx, |state, _| {
5480 let range = state.search_session.matcher.matched_ranges()[1].clone();
5481 let layout = state.last_layout.as_ref().unwrap();
5482 let (_, _, position) = state.line_and_position_for_offset(range.end);
5483 let y = position.expect("wrapped match must be laid out").y
5484 + state.scroll_handle.offset().y;
5485 assert!(y >= layout.line_height * 2. - px(0.1));
5486 assert!(
5487 y + layout.line_height * 3. <= state.last_bounds.unwrap().size.height + px(0.1),
5488 "wrapped match must retain surrounding display rows"
5489 );
5490 });
5491 }
5492
5493 #[gpui::test]
5494 fn test_number_step(cx: &mut TestAppContext) {
5495 let input = InputView::build(cx, |state| state).input;
5496
5497 cx.update(|cx| {
5498 input.update(cx, |_state, cx| {
5499 assert_eq!(
5500 NumberStep::from(5.).value(123., StepAction::Increment, cx),
5501 5.
5502 );
5503
5504 let step = NumberStep::by_value(|value, action, _cx| {
5507 let below = match action {
5508 StepAction::Increment => value < 1.0,
5509 StepAction::Decrement => value <= 1.0,
5510 };
5511 if below { 0.1 } else { 0.5 }
5512 });
5513 assert_eq!(step.value(0.5, StepAction::Increment, cx), 0.1);
5514 assert_eq!(step.value(1.0, StepAction::Increment, cx), 0.5);
5515 assert_eq!(step.value(1.0, StepAction::Decrement, cx), 0.1);
5516 assert_eq!(step.value(2.0, StepAction::Decrement, cx), 0.5);
5517 });
5518 });
5519 }
5520
5521 #[gpui::test]
5522 fn test_number_input_normalization(cx: &mut TestAppContext) {
5523 let input_view = InputView::build(cx, |state| {
5524 state.mask_pattern(MaskPattern::Number {
5525 separator: None,
5526 fraction: None,
5527 })
5528 });
5529 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
5530 let input = input_view.input;
5531
5532 cx.update(|window, cx| {
5536 input.update(cx, |state, cx| {
5537 state.replace_text_in_range(None, "12。5", window, cx);
5538 });
5539 });
5540 cx.run_until_parked();
5541 cx.update(|_, cx| {
5542 input.read_with(cx, |state, _| {
5543 assert_eq!(state.value(), "12.5");
5544 let cursor: Range<usize> = state.selected_range();
5545 assert_eq!(cursor, 4..4);
5546 });
5547 });
5548
5549 cx.update(|window, cx| {
5551 input.update(cx, |state, cx| {
5552 state.replace_text_in_range(None, "abc", window, cx);
5553 });
5554 });
5555 cx.run_until_parked();
5556 cx.update(|_, cx| {
5557 input.read_with(cx, |state, _| {
5558 assert_eq!(state.value(), "12.5");
5559 });
5560 });
5561
5562 cx.update(|window, cx| {
5565 input.update(cx, |state, cx| {
5566 let range = state.range_to_utf16(&(0..state.text.len()));
5567 state.replace_text_in_range(Some(range), "。", window, cx);
5568 });
5569 });
5570 cx.run_until_parked();
5571 cx.update(|_, cx| {
5572 input.read_with(cx, |state, _| {
5573 assert_eq!(state.value(), ".");
5574 let cursor: Range<usize> = state.selected_range();
5575 assert_eq!(cursor, 1..1);
5576 });
5577 });
5578 }
5579
5580 #[gpui::test]
5581 fn test_number_input_normalization_with_separator(cx: &mut TestAppContext) {
5582 let input_view = InputView::build(cx, |state| {
5583 state.mask_pattern(MaskPattern::Number {
5584 separator: Some(','),
5585 fraction: Some(2),
5586 })
5587 });
5588 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
5589 let input = input_view.input;
5590
5591 cx.update(|window, cx| {
5592 input.update(cx, |state, cx| {
5593 state.replace_text_in_range(None, "1234", window, cx);
5594 });
5595 });
5596 cx.run_until_parked();
5597 cx.update(|_, cx| {
5598 input.read_with(cx, |state, _| {
5599 assert_eq!(state.value(), "1,234");
5600 assert_eq!(state.unmask_value(), "1234");
5601 });
5602 });
5603 }
5604
5605 #[gpui::test]
5606 fn test_number_input_clamp_on_blur(cx: &mut TestAppContext) {
5607 let input_view = InputView::build(cx, |state| {
5608 state
5609 .mask_pattern(MaskPattern::Number {
5610 separator: None,
5611 fraction: None,
5612 })
5613 .min(10.)
5614 .max(100.)
5615 });
5616 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
5617 let input = input_view.input;
5618
5619 cx.update(|window, cx| {
5621 input.update(cx, |state, cx| {
5622 state.replace_text_in_range(None, "1000", window, cx);
5623 assert_eq!(state.value(), "1000");
5624 state.clamp_number_value(window, cx);
5625 assert_eq!(state.value(), "100");
5626
5627 let range = state.range_to_utf16(&(0..state.text.len()));
5628 state.replace_text_in_range(Some(range), "1", window, cx);
5629 assert_eq!(state.value(), "1");
5630 state.clamp_number_value(window, cx);
5631 assert_eq!(state.value(), "10");
5632 });
5633 });
5634 }
5635
5636 #[gpui::test]
5637 fn test_number_input_undo_with_mask(cx: &mut TestAppContext) {
5638 let input_view = InputView::build(cx, |state| {
5639 state.mask_pattern(MaskPattern::Number {
5640 separator: Some(','),
5641 fraction: None,
5642 })
5643 });
5644 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
5645 let input = input_view.input;
5646
5647 cx.update(|window, cx| {
5650 input.update(cx, |state, cx| {
5651 state.replace_text_in_range(None, "1234", window, cx);
5652 assert_eq!(state.value(), "1,234");
5653 state.replace_text_in_range(None, "5", window, cx);
5654 assert_eq!(state.value(), "12,345");
5655
5656 state.undo(&Undo, window, cx);
5660 assert_eq!(state.value(), "1,234");
5661 state.undo(&Undo, window, cx);
5662 assert_eq!(state.value(), "");
5663 state.redo(&Redo, window, cx);
5664 assert_eq!(state.value(), "1,234");
5665 state.redo(&Redo, window, cx);
5666 assert_eq!(state.value(), "12,345");
5667 });
5668 });
5669 }
5670
5671 #[gpui::test]
5672 fn test_undo_manager_coalesces_adjacent_typing_transactions(cx: &mut TestAppContext) {
5673 let input_view = InputView::build(cx, |state| state);
5674 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
5675 let input = input_view.input;
5676
5677 cx.update(|window, cx| {
5678 input.update(cx, |state, cx| {
5679 state.replace_text_in_range(None, "a", window, cx);
5680 state.replace_text_in_range(None, "b", window, cx);
5681 assert_eq!(state.value(), "ab");
5682
5683 state.undo(&Undo, window, cx);
5684 assert_eq!(state.value(), "");
5685 });
5686 });
5687 }
5688
5689 #[gpui::test]
5690 fn test_undo_manager_cursor_movement_splits_typing(cx: &mut TestAppContext) {
5691 let input_view = InputView::build(cx, |state| state);
5692 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
5693 let input = input_view.input;
5694
5695 cx.update(|window, cx| {
5696 input.update(cx, |state, cx| {
5697 state.replace_text_in_range(None, "a", window, cx);
5698 state.replace_text_in_range(None, "b", window, cx);
5699 state.left(&MoveLeft, window, cx);
5700 state.replace_text_in_range(None, "x", window, cx);
5701 assert_eq!(state.value(), "axb");
5702
5703 state.undo(&Undo, window, cx);
5704 assert_eq!(state.value(), "ab");
5705 state.undo(&Undo, window, cx);
5706 assert_eq!(state.value(), "");
5707 });
5708 });
5709 }
5710
5711 #[gpui::test]
5712 fn test_undo_manager_splits_backward_and_forward_delete(cx: &mut TestAppContext) {
5713 let input_view = InputView::build(cx, |state| state);
5714 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
5715 let input = input_view.input;
5716
5717 cx.update(|window, cx| {
5718 input.update(cx, |state, cx| {
5719 state.set_value("abcd", window, cx);
5720 state.set_selected_range(2..2, cx);
5721 state.backspace(&Backspace, window, cx);
5722 state.delete(&Delete, window, cx);
5723 assert_eq!(state.value(), "ad");
5724
5725 state.undo(&Undo, window, cx);
5726 assert_eq!(state.value(), "acd");
5727 state.undo(&Undo, window, cx);
5728 assert_eq!(state.value(), "abcd");
5729 });
5730 });
5731 }
5732
5733 #[gpui::test]
5734 fn test_undo_manager_coalesces_directional_character_deletes(cx: &mut TestAppContext) {
5735 let input_view = InputView::build(cx, |state| state);
5736 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
5737 let input = input_view.input;
5738
5739 cx.update(|window, cx| {
5740 input.update(cx, |state, cx| {
5741 state.set_value("abcd", window, cx);
5742 state.backspace(&Backspace, window, cx);
5743 state.backspace(&Backspace, window, cx);
5744 assert_eq!(state.value(), "ab");
5745 state.undo(&Undo, window, cx);
5746 assert_eq!(state.value(), "abcd");
5747 assert_eq!(state.selected_range(), 4..4);
5748
5749 state.set_value("abcd", window, cx);
5750 state.set_selected_range(1..1, cx);
5751 state.delete(&Delete, window, cx);
5752 state.delete(&Delete, window, cx);
5753 assert_eq!(state.value(), "ad");
5754 state.undo(&Undo, window, cx);
5755 assert_eq!(state.value(), "abcd");
5756 assert_eq!(state.selected_range(), 1..1);
5757 });
5758 });
5759 }
5760
5761 #[gpui::test]
5762 fn test_undo_manager_atomic_paste_isolated_from_typing(cx: &mut TestAppContext) {
5763 let input_view = InputView::build(cx, |state| state);
5764 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
5765 let input = input_view.input;
5766
5767 cx.update(|window, cx| {
5768 cx.write_to_clipboard(ClipboardItem::new_string("P".to_string()));
5769 input.update(cx, |state, cx| {
5770 state.replace_text_in_range(None, "a", window, cx);
5771 state.paste(&Paste, window, cx);
5772 state.replace_text_in_range(None, "b", window, cx);
5773 assert_eq!(state.value(), "aPb");
5774
5775 state.undo(&Undo, window, cx);
5776 assert_eq!(state.value(), "aP");
5777 state.undo(&Undo, window, cx);
5778 assert_eq!(state.value(), "a");
5779 state.undo(&Undo, window, cx);
5780 assert_eq!(state.value(), "");
5781 });
5782 });
5783 }
5784
5785 #[gpui::test]
5786 fn test_undo_manager_programmatic_insert_is_atomic(cx: &mut TestAppContext) {
5787 let input_view = InputView::build(cx, |state| state);
5788 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
5789 let input = input_view.input;
5790
5791 cx.update(|window, cx| {
5792 input.update(cx, |state, cx| {
5793 state.replace_text_in_range(None, "a", window, cx);
5794 state.insert("P", window, cx);
5795 state.replace_text_in_range(None, "b", window, cx);
5796 assert_eq!(state.value(), "aPb");
5797
5798 state.undo(&Undo, window, cx);
5799 assert_eq!(state.value(), "aP");
5800 state.undo(&Undo, window, cx);
5801 assert_eq!(state.value(), "a");
5802 });
5803 });
5804 }
5805
5806 #[gpui::test]
5807 fn test_undo_manager_selection_round_trip_splits_typing(cx: &mut TestAppContext) {
5808 let input_view = InputView::build(cx, |state| state);
5809 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
5810 let input = input_view.input;
5811
5812 cx.update(|window, cx| {
5813 input.update(cx, |state, cx| {
5814 state.replace_text_in_range(None, "a", window, cx);
5815 state.replace_text_in_range(None, "b", window, cx);
5816 state.select_all(window, cx);
5817 state.unselect(window, cx);
5818 state.replace_text_in_range(None, "c", window, cx);
5819
5820 state.undo(&Undo, window, cx);
5821 assert_eq!(state.value(), "ab");
5822 state.undo(&Undo, window, cx);
5823 assert_eq!(state.value(), "");
5824 });
5825 });
5826 }
5827
5828 #[gpui::test]
5829 fn test_undo_manager_enter_is_atomic(cx: &mut TestAppContext) {
5830 let input_view = InputView::build_textarea(cx, |state| state);
5831 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
5832 let input = input_view.input;
5833
5834 cx.update(|window, cx| {
5835 input.update(cx, |state, cx| {
5836 state.replace_text_in_range(None, "a", window, cx);
5837 state.enter(
5838 &Enter {
5839 secondary: false,
5840 shift: false,
5841 },
5842 window,
5843 cx,
5844 );
5845 state.replace_text_in_range(None, "b", window, cx);
5846
5847 state.undo(&Undo, window, cx);
5848 assert_eq!(state.value(), "a\n");
5849 state.undo(&Undo, window, cx);
5850 assert_eq!(state.value(), "a");
5851 state.undo(&Undo, window, cx);
5852 assert_eq!(state.value(), "");
5853 });
5854 });
5855 }
5856
5857 #[gpui::test]
5858 fn test_undo_manager_single_line_return_commits_the_typing_session(cx: &mut TestAppContext) {
5859 let input_view = InputView::build(cx, |state| state);
5860 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
5861 let input = input_view.input;
5862
5863 cx.update(|window, cx| {
5864 input.update(cx, |state, cx| {
5865 for part in ["a", "b", "c"] {
5866 state.replace_text_in_range(None, part, window, cx);
5867 }
5868 state.enter(
5869 &Enter {
5870 secondary: false,
5871 shift: false,
5872 },
5873 window,
5874 cx,
5875 );
5876 for part in ["d", "e", "f"] {
5877 state.replace_text_in_range(None, part, window, cx);
5878 }
5879 assert_eq!(state.value(), "abcdef");
5880
5881 state.undo(&Undo, window, cx);
5882 assert_eq!(state.value(), "abc");
5883 state.undo(&Undo, window, cx);
5884 assert_eq!(state.value(), "");
5885 });
5886 });
5887 }
5888
5889 #[gpui::test]
5890 fn test_undo_manager_submit_on_enter_commits_the_textarea_session(cx: &mut TestAppContext) {
5891 let input_view = InputView::build_textarea(cx, |state| state.submit_on_enter(true));
5892 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
5893 let input = input_view.input;
5894
5895 cx.update(|window, cx| {
5896 input.update(cx, |state, cx| {
5897 state.replace_text_in_range(None, "before submit", window, cx);
5898 state.enter(
5899 &Enter {
5900 secondary: false,
5901 shift: false,
5902 },
5903 window,
5904 cx,
5905 );
5906 state.replace_text_in_range(None, " after submit", window, cx);
5907 assert_eq!(state.value(), "before submit after submit");
5908
5909 state.undo(&Undo, window, cx);
5910 assert_eq!(state.value(), "before submit");
5911 state.undo(&Undo, window, cx);
5912 assert_eq!(state.value(), "");
5913 });
5914 });
5915 }
5916
5917 #[gpui::test]
5918 fn test_undo_manager_blur_commits_the_typing_session(cx: &mut TestAppContext) {
5919 let input_view = InputView::build(cx, |state| state);
5920 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
5921 let input = input_view.input;
5922
5923 cx.update(|window, cx| {
5924 input.update(cx, |state, cx| {
5925 state.replace_text_in_range(None, "before blur", window, cx);
5926 state.on_blur(window, cx);
5927 state.on_focus(window, cx);
5928 state.replace_text_in_range(None, " after focus", window, cx);
5929 assert_eq!(state.value(), "before blur after focus");
5930
5931 state.undo(&Undo, window, cx);
5932 assert_eq!(state.value(), "before blur");
5933 state.undo(&Undo, window, cx);
5934 assert_eq!(state.value(), "");
5935 });
5936 });
5937 }
5938
5939 #[gpui::test]
5940 fn test_undo_manager_keeps_rapid_lines_in_distinct_transactions(cx: &mut TestAppContext) {
5941 let input_view = InputView::build_textarea(cx, |state| state);
5942 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
5943 let input = input_view.input;
5944
5945 cx.update(|window, cx| {
5946 input.update(cx, |state, cx| {
5947 let enter = Enter {
5948 secondary: false,
5949 shift: false,
5950 };
5951 state.replace_text_in_range(None, "a", window, cx);
5952 state.enter(&enter, window, cx);
5953 state.replace_text_in_range(None, "b", window, cx);
5954 state.enter(&enter, window, cx);
5955 state.replace_text_in_range(None, "c", window, cx);
5956 assert_eq!(state.value(), "a\nb\nc");
5957
5958 for expected in ["a\nb\n", "a\nb", "a\n", "a", ""] {
5959 state.undo(&Undo, window, cx);
5960 assert_eq!(state.value(), expected);
5961 }
5962 });
5963 });
5964 }
5965
5966 #[gpui::test]
5967 fn test_undo_manager_coalesces_long_unicode_typing_without_a_timer(cx: &mut TestAppContext) {
5968 let input_view = InputView::build_textarea(cx, |state| state);
5969 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
5970 let input = input_view.input;
5971 let parts = [
5972 "The ",
5973 "quick ",
5974 "brown fox, ",
5975 "你好,世界 ",
5976 "🦀 jumps over 13 lazy dogs.",
5977 ];
5978 let expected = parts.concat();
5979
5980 cx.update(|window, cx| {
5981 input.update(cx, |state, cx| {
5982 for part in parts {
5983 state.replace_text_in_range(None, part, window, cx);
5984 }
5985 assert_eq!(state.value(), expected);
5986
5987 state.undo(&Undo, window, cx);
5988 assert_eq!(state.value(), "");
5989 state.redo(&Redo, window, cx);
5990 assert_eq!(state.value(), expected);
5991 });
5992 });
5993 }
5994
5995 #[gpui::test]
5996 fn test_undo_manager_long_multiline_sequence_has_structural_boundaries(
5997 cx: &mut TestAppContext,
5998 ) {
5999 let input_view = InputView::build_textarea(cx, |state| state);
6000 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6001 let input = input_view.input;
6002 let enter = Enter {
6003 secondary: false,
6004 shift: false,
6005 };
6006
6007 cx.update(|window, cx| {
6008 input.update(cx, |state, cx| {
6009 for (index, line) in [
6010 "first line with punctuation!",
6011 "第二行包含 Unicode 🦀",
6012 "third line has several words",
6013 ]
6014 .into_iter()
6015 .enumerate()
6016 {
6017 for chunk in line.split_inclusive(' ') {
6018 state.replace_text_in_range(None, chunk, window, cx);
6019 }
6020 if index < 2 {
6021 state.enter(&enter, window, cx);
6022 }
6023 }
6024
6025 assert_eq!(
6026 state.value(),
6027 "first line with punctuation!\n第二行包含 Unicode 🦀\nthird line has several words"
6028 );
6029 state.undo(&Undo, window, cx);
6030 assert_eq!(
6031 state.value(),
6032 "first line with punctuation!\n第二行包含 Unicode 🦀\n"
6033 );
6034 state.undo(&Undo, window, cx);
6035 assert_eq!(
6036 state.value(),
6037 "first line with punctuation!\n第二行包含 Unicode 🦀"
6038 );
6039 state.undo(&Undo, window, cx);
6040 assert_eq!(state.value(), "first line with punctuation!\n");
6041 });
6042 });
6043 }
6044
6045 #[gpui::test]
6046 fn test_masked_input_keeps_its_value_out_of_the_clipboard(cx: &mut TestAppContext) {
6047 let input_view = InputView::build(cx, |state| state);
6048 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6049 let input = input_view.input;
6050
6051 cx.update(|window, cx| {
6052 input.update(cx, |state, cx| {
6053 state.set_value("hunter2", window, cx);
6054 state.set_masked(true, window, cx);
6055 state.select_all(window, cx);
6056 cx.write_to_clipboard(ClipboardItem::new_string("sentinel".into()));
6057
6058 state.copy(&Copy, window, cx);
6059 assert_eq!(
6060 cx.read_from_clipboard().and_then(|item| item.text()),
6061 Some("sentinel".to_string())
6062 );
6063
6064 state.cut(&Cut, window, cx);
6066 assert_eq!(state.value(), "hunter2");
6067 assert_eq!(
6068 cx.read_from_clipboard().and_then(|item| item.text()),
6069 Some("sentinel".to_string())
6070 );
6071
6072 state.set_masked(false, window, cx);
6074 state.copy(&Copy, window, cx);
6075 assert_eq!(
6076 cx.read_from_clipboard().and_then(|item| item.text()),
6077 Some("hunter2".to_string())
6078 );
6079 });
6080 });
6081 }
6082
6083 #[gpui::test]
6084 fn test_masked_input_collapses_word_boundaries(cx: &mut TestAppContext) {
6085 let input_view = InputView::build(cx, |state| state);
6086 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6087 let input = input_view.input;
6088
6089 cx.update(|window, cx| {
6090 input.update(cx, |state, cx| {
6091 state.set_value("aaa bbb ccc", window, cx);
6092 state.set_masked(true, window, cx);
6093 state.set_selected_range(7..7, cx);
6094
6095 state.delete_previous_word(&DeleteToPreviousWordStart, window, cx);
6098 assert_eq!(state.value(), " ccc");
6099 assert_eq!(state.selected_range(), 0..0);
6100
6101 state.delete_next_word(&DeleteToNextWordEnd, window, cx);
6102 assert_eq!(state.value(), "");
6103
6104 state.set_value("aaa bbb ccc", window, cx);
6106 state.select_word(9, window, cx);
6107 assert_eq!(state.selected_range(), 0..11);
6108
6109 state.set_masked(false, window, cx);
6111 state.set_value("aaa bbb ccc", window, cx);
6112 state.set_selected_range(11..11, cx);
6113 state.delete_previous_word(&DeleteToPreviousWordStart, window, cx);
6114 assert_eq!(state.value(), "aaa bbb ");
6115
6116 state.set_value("aaa bbb ccc", window, cx);
6117 state.select_word(9, window, cx);
6118 assert_eq!(state.selected_range(), 8..11);
6119 });
6120 });
6121 }
6122
6123 #[gpui::test]
6124 fn test_masked_input_disables_the_copy_context_menu_items(cx: &mut TestAppContext) {
6125 let input_view = InputView::build(cx, |state| state);
6126 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6127 let input = input_view.input;
6128
6129 cx.update(|window, cx| {
6130 input.update(cx, |state, cx| {
6131 state.set_value("hunter2", window, cx);
6132 state.select_all(window, cx);
6133 assert!(state.context_menu_capabilities().is_copyable());
6134
6135 state.set_masked(true, window, cx);
6136 let capabilities = state.context_menu_capabilities();
6137 assert!(capabilities.is_masked());
6138 assert!(capabilities.has_selection());
6139 assert!(!capabilities.is_copyable());
6140 });
6141 });
6142 }
6143
6144 #[gpui::test]
6145 fn test_undo_manager_cut_and_repeated_pastes_are_distinct_transactions(
6146 cx: &mut TestAppContext,
6147 ) {
6148 let input_view = InputView::build(cx, |state| state);
6149 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6150 let input = input_view.input;
6151
6152 cx.update(|window, cx| {
6153 input.update(cx, |state, cx| {
6154 state.replace_text_in_range(None, "alpha beta gamma", window, cx);
6155 state.set_selected_range(6..10, cx);
6156 state.cut(&Cut, window, cx);
6157 assert_eq!(state.value(), "alpha gamma");
6158
6159 state.paste(&Paste, window, cx);
6160 state.paste(&Paste, window, cx);
6161 assert_eq!(state.value(), "alpha betabeta gamma");
6162
6163 state.undo(&Undo, window, cx);
6164 assert_eq!(state.value(), "alpha beta gamma");
6165 state.undo(&Undo, window, cx);
6166 assert_eq!(state.value(), "alpha gamma");
6167 state.undo(&Undo, window, cx);
6168 assert_eq!(state.value(), "alpha beta gamma");
6169 });
6170 });
6171 }
6172
6173 #[gpui::test]
6174 fn test_undo_manager_word_and_line_deletes_do_not_coalesce(cx: &mut TestAppContext) {
6175 let input_view = InputView::build_textarea(cx, |state| state);
6176 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6177 let input = input_view.input;
6178
6179 cx.update(|window, cx| {
6180 input.update(cx, |state, cx| {
6181 state.set_value("one two three\nfour five", window, cx);
6182 state.set_selected_range(13..13, cx);
6183 state.delete_previous_word(&DeleteToPreviousWordStart, window, cx);
6184 assert_eq!(state.value(), "one two \nfour five");
6185 state.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
6186 assert_eq!(state.value(), "one two four five");
6187
6188 state.undo(&Undo, window, cx);
6189 assert_eq!(state.value(), "one two \nfour five");
6190 state.undo(&Undo, window, cx);
6191 assert_eq!(state.value(), "one two three\nfour five");
6192 });
6193 });
6194 }
6195
6196 #[gpui::test]
6197 fn test_undo_manager_multiline_replacement_is_one_atomic_transaction(cx: &mut TestAppContext) {
6198 let input_view = InputView::build_textarea(cx, |state| state);
6199 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6200 let input = input_view.input;
6201
6202 cx.update(|window, cx| {
6203 input.update(cx, |state, cx| {
6204 state.replace_text_in_range(None, "before", window, cx);
6205 state.set_selected_range(0..6, cx);
6206 state.replace_text_in_range(None, "line one\nline two\n第三行", window, cx);
6207 assert_eq!(state.value(), "line one\nline two\n第三行");
6208
6209 state.undo(&Undo, window, cx);
6210 assert_eq!(state.value(), "before");
6211 assert_eq!(state.selected_range(), 0..6);
6212 state.redo(&Redo, window, cx);
6213 assert_eq!(state.value(), "line one\nline two\n第三行");
6214 });
6215 });
6216 }
6217
6218 #[gpui::test]
6219 fn test_undo_manager_composition_isolated_from_long_typing(cx: &mut TestAppContext) {
6220 let input_view = InputView::build(cx, |state| state);
6221 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6222 let input = input_view.input;
6223
6224 cx.update(|window, cx| {
6225 input.update(cx, |state, cx| {
6226 state.replace_text_in_range(None, "prefix ", window, cx);
6227 state.replace_and_mark_text_in_range(None, "n", None, window, cx);
6228 state.replace_and_mark_text_in_range(None, "ni", None, window, cx);
6229 state.replace_and_mark_text_in_range(None, "你", None, window, cx);
6230 state.unmark_text(window, cx);
6231 state.replace_text_in_range(None, " suffix", window, cx);
6232 assert_eq!(state.value(), "prefix 你 suffix");
6233
6234 state.undo(&Undo, window, cx);
6235 assert_eq!(state.value(), "prefix 你");
6236 state.undo(&Undo, window, cx);
6237 assert_eq!(state.value(), "prefix ");
6238 state.undo(&Undo, window, cx);
6239 assert_eq!(state.value(), "");
6240 });
6241 });
6242 }
6243
6244 #[gpui::test]
6245 fn test_undo_manager_selected_replacement_is_atomic(cx: &mut TestAppContext) {
6246 let input_view = InputView::build(cx, |state| state);
6247 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6248 let input = input_view.input;
6249
6250 cx.update(|window, cx| {
6251 input.update(cx, |state, cx| {
6252 state.replace_text_in_range(None, "abc", window, cx);
6253 state.set_selected_range(1..2, cx);
6254 state.replace_text_in_range(None, "X", window, cx);
6255 state.replace_text_in_range(None, "z", window, cx);
6256 assert_eq!(state.value(), "aXzc");
6257
6258 state.undo(&Undo, window, cx);
6259 assert_eq!(state.value(), "aXc");
6260 state.undo(&Undo, window, cx);
6261 assert_eq!(state.value(), "abc");
6262 state.undo(&Undo, window, cx);
6263 assert_eq!(state.value(), "");
6264 });
6265 });
6266 }
6267
6268 #[gpui::test]
6269 fn test_number_input_leading_dot_editable(cx: &mut TestAppContext) {
6270 let input_view = InputView::build(cx, |state| {
6271 state.mask_pattern(MaskPattern::Number {
6272 separator: None,
6273 fraction: None,
6274 })
6275 });
6276 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6277 let input = input_view.input;
6278
6279 cx.update(|window, cx| {
6280 input.update(cx, |state, cx| {
6281 state.replace_text_in_range(None, "1.2", window, cx);
6282
6283 let range = state.range_to_utf16(&(0..1));
6287 state.replace_text_in_range(Some(range), "", window, cx);
6288 assert_eq!(state.value(), ".2");
6289 let cursor: Range<usize> = state.selected_range();
6290 assert_eq!(cursor, 0..0);
6291
6292 state.replace_text_in_range(Some(0..0), "3", window, cx);
6294 assert_eq!(state.value(), "3.2");
6295 });
6296 });
6297 }
6298
6299 #[gpui::test]
6300 fn test_number_input_escape_invalid_text(cx: &mut TestAppContext) {
6301 let input_view = InputView::build(cx, |state| {
6304 state
6305 .mask_pattern(MaskPattern::Number {
6306 separator: None,
6307 fraction: None,
6308 })
6309 .default_value("1,234")
6310 });
6311 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6312 let input = input_view.input;
6313
6314 cx.update(|window, cx| {
6315 input.update(cx, |state, cx| {
6316 let range = state.range_to_utf16(&(4..5));
6320 state.replace_text_in_range(Some(range), "", window, cx);
6321 assert_eq!(state.value(), "1,23");
6322
6323 let range = state.range_to_utf16(&(1..2));
6325 state.replace_text_in_range(Some(range), "", window, cx);
6326 assert_eq!(state.value(), "123");
6327 state.replace_text_in_range(None, "a", window, cx);
6328 assert_eq!(state.value(), "123");
6329 });
6330 });
6331 }
6332
6333 #[gpui::test]
6337 fn test_set_value_single_line_caret_at_end_view_at_start(cx: &mut TestAppContext) {
6338 let input_view = InputView::build(cx, |state| state);
6339 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6340 let input = input_view.input;
6341
6342 let value = format!("https://example.com/v1/users?{}", "x=1&".repeat(120));
6344 let len = value.len();
6345
6346 cx.update(|window, cx| {
6349 input.update(cx, |state, cx| {
6350 state.set_value(value.clone(), window, cx);
6351
6352 assert_eq!(
6353 state.selected_range(),
6354 len..len,
6355 "single-line caret should be at the end after set_value"
6356 );
6357 assert_eq!(
6358 state.deferred_scroll_offset,
6359 Some(point(px(0.), px(0.))),
6360 "the view should be forced back to the start"
6361 );
6362 });
6363 });
6364
6365 cx.run_until_parked();
6368 cx.update(|_, cx| {
6369 input.read_with(cx, |state, _| {
6370 assert!(
6371 state.scroll_size.width > state.input_bounds.size.width,
6372 "value must overflow the input width or this test is vacuous"
6373 );
6374 assert_eq!(
6375 state.scroll_handle.offset().x,
6376 px(0.),
6377 "long value should display from its start, not its tail"
6378 );
6379 });
6380 });
6381 }
6382
6383 #[gpui::test]
6387 fn test_replace_all_single_line(cx: &mut TestAppContext) {
6388 let input_view = InputView::build(cx, |state| state);
6389 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6390 let input = input_view.input;
6391
6392 let value = format!("https://example.com/v1/users?{}", "x=1&".repeat(120));
6394 let len = value.len();
6395
6396 cx.update(|window, cx| {
6400 input.update(cx, |state, cx| {
6401 state.set_value("hello", window, cx);
6402 state.replace_all(value.clone(), window, cx);
6403 assert_eq!(state.value(), value);
6404 assert_eq!(
6405 state.selected_range(),
6406 len..len,
6407 "single-line caret should be at the end after replace_all"
6408 );
6409 assert_eq!(
6410 state.scroll_handle.offset(),
6411 point(px(0.), px(0.)),
6412 "the scroll offset should be reset to the start"
6413 );
6414 assert_eq!(
6415 state.deferred_scroll_offset,
6416 Some(point(px(0.), px(0.))),
6417 "single-line should set a deferred scroll offset to keep the start visible"
6418 );
6419 });
6420 });
6421
6422 cx.run_until_parked();
6425 cx.update(|_, cx| {
6426 input.read_with(cx, |state, _| {
6427 assert!(
6428 state.scroll_size.width > state.input_bounds.size.width,
6429 "value must overflow the input width or this test is vacuous"
6430 );
6431 assert_eq!(
6432 state.scroll_handle.offset().x,
6433 px(0.),
6434 "long value should display from its start, not its tail"
6435 );
6436 });
6437 });
6438 }
6439
6440 #[gpui::test]
6441 fn test_single_line_removes_newlines(cx: &mut TestAppContext) {
6442 let input_view = InputView::build(cx, |state| state.default_value("default\nvalue"));
6443 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6444 let input = input_view.input;
6445
6446 cx.update(|window, cx| {
6447 input.update(cx, |state, cx| {
6448 assert_eq!(state.value(), "defaultvalue");
6449
6450 state.set_value("first\nsecond\r\nthird\rfourth", window, cx);
6451 assert_eq!(state.value(), "firstsecondthirdfourth");
6452
6453 state.set_value("", window, cx);
6454 state.insert("a\nb", window, cx);
6455 assert_eq!(state.value(), "ab");
6456 });
6457
6458 cx.write_to_clipboard(ClipboardItem::new_string("a\r\nb\nc\rd".to_string()));
6459 input.update(cx, |state, cx| {
6460 state.set_value("", window, cx);
6461 state.paste(&Paste, window, cx);
6462 assert_eq!(state.value(), "abcd");
6463 });
6464 });
6465
6466 cx.run_until_parked();
6467 }
6468
6469 #[gpui::test]
6473 fn test_replace_all_multi_line(cx: &mut TestAppContext) {
6474 let input_view = InputView::build_textarea(cx, |state| state);
6475 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6476 let input = input_view.input;
6477
6478 cx.update(|window, cx| {
6479 input.update(cx, |state, cx| {
6480 state.set_value("foo\nbar", window, cx);
6481 state.replace_all("baz\nqux", window, cx);
6482 assert_eq!(state.value(), "baz\nqux");
6483 assert_eq!(
6484 state.selected_range(),
6485 0..0,
6486 "multi-line selection should be cleared after replace_all"
6487 );
6488 assert_eq!(
6489 state.scroll_handle.offset(),
6490 point(px(0.), px(0.)),
6491 "the scroll offset should be reset to the start"
6492 );
6493 assert!(
6494 state.deferred_scroll_offset.is_none(),
6495 "multi-line should not set a deferred scroll offset"
6496 );
6497 });
6498 });
6499 }
6500
6501 #[gpui::test]
6504 fn test_replace_all_preserves_undo_history(cx: &mut TestAppContext) {
6505 let input_view = InputView::build(cx, |state| state);
6506 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6507 let input = input_view.input;
6508
6509 cx.update(|window, cx| {
6510 input.update(cx, |state, cx| {
6511 state.set_value("first", window, cx);
6513 assert!(
6514 !state.undo_manager.has_undos(),
6515 "history should be empty after set_value"
6516 );
6517
6518 state.replace_all("second", window, cx);
6520 assert_eq!(state.value(), "second");
6521 assert!(
6522 state.undo_manager.has_undos(),
6523 "replace_all should record an undo step"
6524 );
6525
6526 state.undo(&Undo, window, cx);
6528 assert_eq!(state.value(), "first");
6529
6530 state.redo(&Redo, window, cx);
6532 assert_eq!(state.value(), "second");
6533 });
6534 });
6535 }
6536
6537 #[gpui::test]
6540 fn test_replace_all_code_editor(cx: &mut TestAppContext) {
6541 let input_view = InputView::new(cx);
6542 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6543 let input = input_view.input;
6544
6545 cx.update(|window, cx| {
6546 input.update(cx, |state, cx| {
6547 state.set_value("select 1", window, cx);
6549 state._pending_update = false;
6550
6551 state.replace_all("select 2", window, cx);
6552 assert_eq!(state.value(), "select 2");
6553 assert!(
6554 state._pending_update,
6555 "replace_all on a code editor should request a pending update"
6556 );
6557 });
6558 });
6559 }
6560
6561 #[gpui::test]
6562 fn test_set_selected_range(cx: &mut TestAppContext) {
6563 let input_view = InputView::build(cx, |state| state.default_value("hello world"));
6564 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6565 let input = input_view.input;
6566
6567 cx.update(|_, cx| {
6568 input.update(cx, |s, cx| {
6569 s.set_selected_range(0..5, cx);
6570 assert_eq!(s.selected_range(), 0..5);
6571 assert_eq!(s.selected_text().to_string(), "hello");
6572
6573 s.set_selected_range(6..11, cx);
6574 assert_eq!(s.selected_text().to_string(), "world");
6575
6576 s.set_selected_range(100..100, cx);
6578 assert_eq!(s.selected_range(), 11..11);
6579 });
6580 });
6581 }
6582
6583 #[gpui::test]
6585 fn test_replace_text_in_ranges_single_edit(cx: &mut TestAppContext) {
6586 let input_view = InputView::build_textarea(cx, |state| state.default_value("hello world"));
6587 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6588 let input = input_view.input;
6589
6590 cx.update(|window, cx| {
6591 input.update(cx, |s, cx| {
6592 s.replace_text_in_ranges(&[(0..5, "HELLO".to_string())], window, cx);
6593 assert_eq!(s.value(), "HELLO world");
6594
6595 s.undo(&Undo, window, cx);
6596 assert_eq!(s.value(), "hello world");
6597
6598 s.redo(&Redo, window, cx);
6599 assert_eq!(s.value(), "HELLO world");
6600 });
6601 });
6602 }
6603
6604 #[gpui::test]
6607 fn test_replace_text_in_ranges_multi_edit_transaction(cx: &mut TestAppContext) {
6608 let input_view = InputView::build_textarea(cx, |state| state.default_value("aaa bbb ccc"));
6609 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6610 let input = input_view.input;
6611
6612 cx.update(|window, cx| {
6613 input.update(cx, |s, cx| {
6614 s.replace_text_in_ranges(
6617 &[(0..3, "X".to_string()), (8..11, "Y".to_string())],
6618 window,
6619 cx,
6620 );
6621 assert_eq!(s.value(), "X bbb Y");
6622
6623 let cursors: Vec<usize> =
6625 s.selections.iter().map(|sel| sel.cursor_offset()).collect();
6626 assert_eq!(cursors, vec![1, 7]);
6627
6628 assert_eq!(s.undo_manager.undo_count(), 1);
6630
6631 s.undo(&Undo, window, cx);
6633 assert_eq!(s.value(), "aaa bbb ccc");
6634
6635 s.redo(&Redo, window, cx);
6637 assert_eq!(s.value(), "X bbb Y");
6638 });
6639 });
6640 }
6641
6642 #[gpui::test]
6644 fn test_ime_composition_undoes_as_one_unit(cx: &mut TestAppContext) {
6645 let input_view = InputView::build_textarea(cx, |state| state.default_value(""));
6646 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6647 let input = input_view.input;
6648
6649 cx.update(|window, cx| {
6650 input.update(cx, |s, cx| {
6651 s.replace_and_mark_text_in_range(None, "n", Some(1..1), window, cx);
6653 s.replace_and_mark_text_in_range(None, "ni", Some(2..2), window, cx);
6654 s.replace_text_in_range(None, "你", window, cx);
6655 assert_eq!(s.value(), "你");
6656
6657 assert_eq!(s.undo_manager.undo_count(), 1);
6659
6660 s.undo(&Undo, window, cx);
6661 assert_eq!(s.value(), "");
6662
6663 s.redo(&Redo, window, cx);
6664 assert_eq!(s.value(), "你");
6665 });
6666 });
6667 }
6668
6669 #[gpui::test]
6672 fn test_edit_after_composition_is_separate_undo(cx: &mut TestAppContext) {
6673 let input_view = InputView::build_textarea(cx, |state| state.default_value(""));
6674 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6675 let input = input_view.input;
6676
6677 cx.update(|window, cx| {
6678 input.update(cx, |s, cx| {
6679 s.replace_and_mark_text_in_range(None, "n", Some(1..1), window, cx);
6680 s.replace_text_in_range(None, "你", window, cx);
6681 assert_eq!(s.value(), "你");
6682 assert_eq!(s.undo_manager.undo_count(), 1);
6683
6684 s.replace_text_in_range(None, "x", window, cx);
6686 assert_eq!(s.value(), "你x");
6687 assert_eq!(s.undo_manager.undo_count(), 2);
6688
6689 s.undo(&Undo, window, cx);
6690 assert_eq!(s.value(), "你");
6691 s.undo(&Undo, window, cx);
6692 assert_eq!(s.value(), "");
6693 });
6694 });
6695 }
6696
6697 #[gpui::test]
6700 fn test_composition_cancel_via_unmark_does_not_leak(cx: &mut TestAppContext) {
6701 let input_view = InputView::build_textarea(cx, |state| state.default_value(""));
6702 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6703 let input = input_view.input;
6704
6705 cx.update(|window, cx| {
6706 input.update(cx, |s, cx| {
6707 s.replace_and_mark_text_in_range(None, "n", Some(1..1), window, cx);
6709 s.unmark_text(window, cx);
6710 let after_cancel = s.undo_manager.undo_count();
6711
6712 s.replace_text_in_range(None, "x", window, cx);
6714 assert_eq!(s.undo_manager.undo_count(), after_cancel + 1);
6715 });
6716 });
6717 }
6718
6719 #[gpui::test]
6720 fn test_set_selected_range_clips_to_utf8_boundaries(cx: &mut TestAppContext) {
6721 let input_view = InputView::build(cx, |state| state.default_value("éx"));
6722 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6723 let input = input_view.input;
6724
6725 cx.update(|window, cx| {
6726 input.update(cx, |state, cx| {
6727 state.set_selected_range(0..1, cx);
6728 assert_eq!(state.selected_range(), 0..2);
6729 state.copy(&Copy, window, cx);
6730
6731 state.set_selected_range(1..1, cx);
6732 assert_eq!(state.selected_range(), 0..0);
6733 });
6734 });
6735 }
6736
6737 #[gpui::test]
6738 fn test_ime_selection_is_relative_to_replacement_start(cx: &mut TestAppContext) {
6739 let input_view = InputView::build(cx, |state| state.default_value("你好 "));
6740 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6741 let input = input_view.input;
6742
6743 cx.update(|window, cx| {
6744 input.update(cx, |state, cx| {
6745 state.set_selected_range(7..7, cx);
6746 state.replace_and_mark_text_in_range(None, "s", Some(1..1), window, cx);
6747 state.replace_and_mark_text_in_range(None, "sh", Some(2..2), window, cx);
6748
6749 assert_eq!(state.value(), "你好 sh");
6750 assert_eq!(state.selected_range(), 9..9);
6751 assert_eq!(state.ime_marked_range, Some((7..9).into()));
6752 });
6753 });
6754 }
6755
6756 #[gpui::test]
6757 fn test_undo_manager_composition_is_one_undo_group(cx: &mut TestAppContext) {
6758 let input_view = InputView::build(cx, |state| state);
6759 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6760 let input = input_view.input;
6761
6762 cx.update(|window, cx| {
6763 input.update(cx, |state, cx| {
6764 state.set_value("a", window, cx);
6765 state.replace_and_mark_text_in_range(None, "s", None, window, cx);
6766 state.replace_and_mark_text_in_range(None, "sh", None, window, cx);
6767 state.replace_text_in_range(None, "是", window, cx);
6768 assert_eq!(state.value(), "a是");
6769
6770 state.undo(&Undo, window, cx);
6771 assert_eq!(state.value(), "a");
6772 state.redo(&Redo, window, cx);
6773 assert_eq!(state.value(), "a是");
6774 });
6775 });
6776 }
6777
6778 #[gpui::test]
6779 fn test_undo_manager_consecutive_compositions_are_separate_groups(cx: &mut TestAppContext) {
6780 let input_view = InputView::build(cx, |state| state);
6781 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6782 let input = input_view.input;
6783
6784 cx.update(|window, cx| {
6785 input.update(cx, |state, cx| {
6786 state.replace_and_mark_text_in_range(None, "j", None, window, cx);
6788 state.replace_and_mark_text_in_range(None, "jin", None, window, cx);
6789 state.replace_text_in_range(None, "今天", window, cx);
6790 state.replace_and_mark_text_in_range(None, "w", None, window, cx);
6792 state.replace_and_mark_text_in_range(None, "wo", None, window, cx);
6793 state.replace_text_in_range(None, "我们", window, cx);
6794 assert_eq!(state.value(), "今天我们");
6795 assert_eq!(state.selected_range(), 12..12);
6796
6797 state.undo(&Undo, window, cx);
6798 assert_eq!(state.value(), "今天");
6799 assert_eq!(state.selected_range(), 6..6);
6800
6801 state.undo(&Undo, window, cx);
6802 assert_eq!(state.value(), "");
6803 assert_eq!(state.selected_range(), 0..0);
6804
6805 state.redo(&Redo, window, cx);
6806 assert_eq!(state.value(), "今天");
6807 assert_eq!(state.selected_range(), 6..6);
6808
6809 state.redo(&Redo, window, cx);
6810 assert_eq!(state.value(), "今天我们");
6811 assert_eq!(state.selected_range(), 12..12);
6812 });
6813 });
6814 }
6815
6816 #[gpui::test]
6817 fn test_undo_manager_typing_after_composition_is_a_separate_group(cx: &mut TestAppContext) {
6818 let input_view = InputView::build(cx, |state| state);
6819 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6820 let input = input_view.input;
6821
6822 cx.update(|window, cx| {
6823 input.update(cx, |state, cx| {
6824 state.replace_and_mark_text_in_range(None, "n", None, window, cx);
6825 state.replace_text_in_range(None, "你", window, cx);
6826 state.undo_manager.set_pending_intent(EditIntent::Typing);
6827 state.replace_text_in_range(None, "a", window, cx);
6828 state.undo_manager.set_pending_intent(EditIntent::Typing);
6829 state.replace_text_in_range(None, "b", window, cx);
6830 assert_eq!(state.value(), "你ab");
6831
6832 state.undo(&Undo, window, cx);
6833 assert_eq!(state.value(), "你");
6834
6835 state.undo(&Undo, window, cx);
6836 assert_eq!(state.value(), "");
6837 });
6838 });
6839 }
6840
6841 #[gpui::test]
6842 fn test_undo_manager_composition_cancel_leaves_no_entry(cx: &mut TestAppContext) {
6843 let input_view = InputView::build(cx, |state| state);
6844 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6845 let input = input_view.input;
6846
6847 cx.update(|window, cx| {
6848 input.update(cx, |state, cx| {
6849 state.set_value("a", window, cx);
6850 state.replace_and_mark_text_in_range(None, "s", None, window, cx);
6851 state.replace_and_mark_text_in_range(None, "", None, window, cx);
6852
6853 assert_eq!(state.value(), "a");
6854 assert!(!state.undo_manager.has_undos());
6855 });
6856 });
6857 }
6858
6859 #[gpui::test]
6860 fn test_undo_manager_selection_restored_by_undo_and_redo(cx: &mut TestAppContext) {
6861 let input_view = InputView::build(cx, |state| state);
6862 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6863 let input = input_view.input;
6864
6865 cx.update(|window, cx| {
6866 input.update(cx, |state, cx| {
6867 state.set_value("abc", window, cx);
6868 state.set_selected_range(1..2, cx);
6869 state.replace_text_in_range(None, "X", window, cx);
6870
6871 state.undo(&Undo, window, cx);
6872 assert_eq!(state.value(), "abc");
6873 assert_eq!(state.selected_range(), 1..2);
6874
6875 state.redo(&Redo, window, cx);
6876 assert_eq!(state.value(), "aXc");
6877 assert_eq!(state.selected_range(), 2..2);
6878 });
6879 });
6880 }
6881
6882 #[gpui::test]
6883 fn test_undo_manager_forward_delete_restores_cursor(cx: &mut TestAppContext) {
6884 let input_view = InputView::build(cx, |state| state);
6885 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6886 let input = input_view.input;
6887
6888 cx.update(|window, cx| {
6889 input.update(cx, |state, cx| {
6890 state.set_value("abc", window, cx);
6891 state.set_selected_range(1..1, cx);
6892 state.delete(&Delete, window, cx);
6893
6894 state.undo(&Undo, window, cx);
6895 assert_eq!(state.value(), "abc");
6896 assert_eq!(state.selected_range(), 1..1);
6897 });
6898 });
6899 }
6900
6901 #[gpui::test]
6902 fn test_undo_manager_selection_movement_preserves_redo(cx: &mut TestAppContext) {
6903 let input_view = InputView::build(cx, |state| state);
6904 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6905 let input = input_view.input;
6906
6907 cx.update(|window, cx| {
6908 input.update(cx, |state, cx| {
6909 state.replace_text_in_range(None, "ab", window, cx);
6910 state.undo(&Undo, window, cx);
6911 state.set_selected_range(0..0, cx);
6912 state.redo(&Redo, window, cx);
6913 assert_eq!(state.value(), "ab");
6914
6915 state.undo(&Undo, window, cx);
6916 state.replace_text_in_range(None, "x", window, cx);
6917 state.redo(&Redo, window, cx);
6918 assert_eq!(state.value(), "x");
6919 });
6920 });
6921 }
6922
6923 #[gpui::test]
6924 fn test_undo_manager_noop_edit_preserves_redo(cx: &mut TestAppContext) {
6925 let input_view = InputView::build(cx, |state| state);
6926 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6927 let input = input_view.input;
6928
6929 cx.update(|window, cx| {
6930 input.update(cx, |state, cx| {
6931 state.replace_text_in_range(None, "a", window, cx);
6932 state.move_to(0, None, cx);
6933 state.replace_text_in_range(None, "", window, cx);
6934 state.undo(&Undo, window, cx);
6935 assert_eq!(state.value(), "");
6936 state.redo(&Redo, window, cx);
6937 assert_eq!(state.value(), "a");
6938 assert_eq!(state.cursor(), 1);
6939 });
6940 });
6941 }
6942
6943 #[gpui::test]
6944 fn test_cursor_round_trip_stops_typing_coalescing(cx: &mut TestAppContext) {
6945 let input_view = InputView::build(cx, |state| state);
6946 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6947 let input = input_view.input;
6948
6949 cx.update(|window, cx| {
6950 input.update(cx, |state, cx| {
6951 state.replace_text_in_range(None, "a", window, cx);
6952 state.left(&MoveLeft, window, cx);
6953 state.right(&MoveRight, window, cx);
6954 state.replace_text_in_range(None, "b", window, cx);
6955
6956 state.undo(&Undo, window, cx);
6957 assert_eq!(state.value(), "a");
6958 state.undo(&Undo, window, cx);
6959 assert_eq!(state.value(), "");
6960 });
6961 });
6962 }
6963
6964 #[gpui::test]
6965 fn test_undo_manager_noop_edit_breaks_coalescing_without_clearing_history(
6966 cx: &mut TestAppContext,
6967 ) {
6968 let input_view = InputView::build(cx, |state| state);
6969 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6970 let input = input_view.input;
6971
6972 cx.update(|window, cx| {
6973 input.update(cx, |state, cx| {
6974 state.replace_text_in_range(None, "alpha", window, cx);
6975 state.replace_text_in_range(None, "", window, cx);
6976 state.replace_text_in_range(None, "beta", window, cx);
6977 assert_eq!(state.value(), "alphabeta");
6978
6979 state.undo(&Undo, window, cx);
6980 assert_eq!(state.value(), "alpha");
6981 state.undo(&Undo, window, cx);
6982 assert_eq!(state.value(), "");
6983 });
6984 });
6985 }
6986
6987 #[gpui::test]
6988 fn test_undo_manager_masked_redo_restores_actual_cursor(cx: &mut TestAppContext) {
6989 let input_view = InputView::build(cx, |state| {
6990 state.mask_pattern(MaskPattern::Number {
6991 separator: Some(','),
6992 fraction: None,
6993 })
6994 });
6995 let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
6996 let input = input_view.input;
6997
6998 cx.update(|window, cx| {
6999 input.update(cx, |state, cx| {
7000 state.set_value("12345", window, cx);
7001 state.set_selected_range(2..2, cx);
7002 state.replace_text_in_range(None, "9", window, cx);
7003 let selection_after_edit = state.selected_range();
7004 assert_ne!(selection_after_edit.end, state.value().len());
7005
7006 state.undo(&Undo, window, cx);
7007 state.redo(&Redo, window, cx);
7008 assert_eq!(state.selected_range(), selection_after_edit);
7009 });
7010 });
7011 }
7012
7013 #[gpui::test]
7019 fn test_unfold_at(cx: &mut TestAppContext) {
7020 use crate::input::{FoldRange, Position};
7021
7022 let view = InputView::<EditorMode>::new(cx);
7023 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7024 let input = view.input;
7025
7026 cx.update(|window, cx| {
7029 input.update(cx, |state, cx| {
7030 state.set_value("a\nb\nc\nd\ne\nf\ng\nh\ni\nj\nk\nl", window, cx);
7031 state.apply_highlighter_fold_candidates(
7032 vec![
7033 FoldRange::new(0, 5),
7034 FoldRange::new(2, 4),
7035 FoldRange::new(7, 10),
7036 ],
7037 cx,
7038 );
7039 state.display_map.set_folded(0, true);
7040 state.display_map.set_folded(2, true);
7041 state.display_map.set_folded(7, true);
7042 });
7043 });
7044
7045 for line in [0, 5] {
7048 cx.update(|_, cx| {
7049 input.update(cx, |state, cx| {
7050 assert!(!state.display_map.is_buffer_line_hidden(line));
7051 assert!(!state.unfold_at(Position::new(line as u32, 0), cx));
7052 });
7053 input.read_with(cx, |state, _| {
7054 assert!(state.display_map.is_folded_at(0));
7055 assert!(state.display_map.is_folded_at(2));
7056 assert!(state.display_map.is_folded_at(7));
7057 });
7058 });
7059 }
7060
7061 cx.update(|_, cx| {
7064 input.update(cx, |state, cx| {
7065 assert!(state.unfold_at(Position::new(3, 0), cx));
7066 });
7067 input.read_with(cx, |state, _| {
7068 assert!(!state.display_map.is_buffer_line_hidden(3));
7069 assert!(!state.display_map.is_folded_at(0));
7070 assert!(!state.display_map.is_folded_at(2));
7071 assert!(state.display_map.is_folded_at(7));
7072 assert!(state.display_map.is_fold_candidate(0));
7074 assert!(state.display_map.is_fold_candidate(2));
7075 });
7076 });
7077
7078 cx.update(|_, cx| {
7080 input.update(cx, |state, cx| {
7081 assert!(!state.unfold_at(Position::new(3, 0), cx));
7082 });
7083 });
7084 }
7085
7086 #[gpui::test]
7091 fn test_blur_keeps_decorations(cx: &mut TestAppContext) {
7092 let view = InputView::<EditorMode>::new(cx);
7093 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7094 let input = view.input;
7095
7096 cx.update(|window, cx| {
7097 input.update(cx, |state, cx| {
7098 state.set_value("select 1", window, cx);
7099 let _collection = state.create_decorations_collection(
7100 vec![crate::input::TextDecoration::new(
7101 0..6,
7102 gpui::HighlightStyle {
7103 font_weight: Some(gpui::FontWeight::BOLD),
7104 ..Default::default()
7105 },
7106 )],
7107 cx,
7108 );
7109 state.present_hover(
7110 0..6,
7111 lsp_types::Hover {
7112 contents: lsp_types::HoverContents::Scalar(
7113 lsp_types::MarkedString::String("docs".into()),
7114 ),
7115 range: None,
7116 },
7117 cx,
7118 );
7119 assert!(state.hover_popover().is_some());
7120
7121 state.on_blur(window, cx);
7122
7123 assert!(
7124 state.hover_popover().is_none(),
7125 "blur should hide the hover popover"
7126 );
7127 let decorations = state.extras.decoration_layers();
7128 assert!(
7129 decorations.iter().any(|layer| !layer.is_empty()),
7130 "blur must not discard decorations"
7131 );
7132 });
7133 });
7134 }
7135
7136 #[gpui::test]
7141 fn test_kind_does_not_follow_the_row_count(cx: &mut TestAppContext) {
7142 let view = InputView::build_textarea(cx, |state| state.auto_grow(1, 1));
7143 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7144 view.input.read_with(&mut cx, |state, _| {
7145 assert!(state.is_multi_line());
7146 assert!(!state.is_single_line());
7147 assert!(!state.is_code_editor());
7148 });
7149 }
7150
7151 #[gpui::test]
7156 fn test_soft_wrap_is_enabled_by_default(cx: &mut TestAppContext) {
7157 let textarea = InputView::build_textarea(cx, |state| state);
7158 let mut textarea_cx = VisualTestContext::from_window(textarea.window_handle.into(), cx);
7159 textarea
7160 .input
7161 .read_with(&mut textarea_cx, |state, _| assert!(state.soft_wrap));
7162
7163 let editor = InputView::<EditorMode>::new(cx);
7164 let mut editor_cx = VisualTestContext::from_window(editor.window_handle.into(), cx);
7165 editor
7166 .input
7167 .read_with(&mut editor_cx, |state, _| assert!(state.soft_wrap));
7168 }
7169
7170 fn parse_cursor_spec(input: &str) -> (String, Vec<usize>) {
7174 let mut full_text = String::new();
7175 let mut cursor_offsets = Vec::new();
7176 let non_empty_lines: Vec<&str> = input.lines().filter(|l| !l.is_empty()).collect();
7177
7178 for (line_idx, line) in non_empty_lines.iter().enumerate() {
7179 let mut positions = Vec::new();
7180 let mut text = String::new();
7181 for ch in line.chars() {
7182 if ch == '|' {
7183 positions.push(text.len());
7184 } else {
7185 text.push(ch);
7186 }
7187 }
7188
7189 if line_idx > 0 {
7190 full_text.push('\n');
7191 }
7192 let line_start = full_text.len();
7193 for pos in positions {
7194 cursor_offsets.push(line_start + pos);
7195 }
7196 full_text.push_str(&text);
7197 }
7198 full_text.push('\n');
7199
7200 (full_text, cursor_offsets)
7201 }
7202
7203 fn multi_line(cx: &mut TestAppContext) -> InputView<TextareaMode> {
7205 InputView::build_textarea(cx, |state| state)
7206 }
7207
7208 fn setup_cursors<M: InputModeKind>(
7210 cx: &mut VisualTestContext,
7211 input: &Entity<InputBaseState<M>>,
7212 spec: &str,
7213 ) {
7214 let (full_text, offsets) = parse_cursor_spec(spec);
7215 cx.update(|window, cx| {
7216 input.update(cx, |state, cx| {
7217 state.set_value(&full_text, window, cx);
7218 let selections = offsets
7219 .into_iter()
7220 .map(|offset| {
7221 CursorSelection::new(state.selections.generate_id(), offset, offset)
7222 })
7223 .collect();
7224 state.selections.replace_all(selections);
7225 cx.notify();
7226 });
7227 });
7228 }
7229
7230 #[track_caller]
7232 fn assert_cursors<M: InputModeKind>(
7233 cx: &mut VisualTestContext,
7234 input: &Entity<InputBaseState<M>>,
7235 spec: &str,
7236 ) {
7237 let (expected_text, mut expected_cursors) = parse_cursor_spec(spec);
7238 expected_cursors.sort();
7239
7240 let (actual_text, mut actual_cursors) = input.read_with(cx, |state, _| {
7241 (
7242 state.text.to_string(),
7243 state
7244 .selections
7245 .iter()
7246 .map(|s| s.cursor_offset())
7247 .collect::<Vec<_>>(),
7248 )
7249 });
7250 actual_cursors.sort();
7251
7252 assert_eq!(
7253 actual_text, expected_text,
7254 "Text mismatch:\nExpected: {expected_text:?}\nActual: {actual_text:?}"
7255 );
7256 assert_eq!(
7257 actual_cursors, expected_cursors,
7258 "Cursor mismatch:\nExpected: {expected_cursors:?}\nActual: {actual_cursors:?}"
7259 );
7260 }
7261
7262 #[gpui::test]
7263 fn test_alt_drag_selects_a_block_and_replaces_each_row(cx: &mut TestAppContext) {
7264 cx.update(crate::init);
7265 let view = InputView::<EditorMode>::new(cx);
7266 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7267 for modifiers in [
7268 gpui::Modifiers {
7269 alt: true,
7270 ..Default::default()
7271 },
7272 gpui::Modifiers {
7273 alt: true,
7274 shift: true,
7275 ..Default::default()
7276 },
7277 #[cfg(target_os = "linux")]
7278 gpui::Modifiers {
7279 alt: true,
7280 control: true,
7281 ..Default::default()
7282 },
7283 ] {
7284 setup_cursors(&mut cx, &view.input, "|abcd\nabcd\nabcd");
7285 cx.update(|window, cx| {
7286 view.input.update(cx, |state, cx| state.focus(window, cx));
7287 });
7288 let (start, end) = view.input.read_with(&cx, |state, _| {
7289 let layout = state.last_layout.as_ref().unwrap();
7290 let origin = state.last_bounds.unwrap().origin;
7291 let position = |row: usize, col| {
7292 let local = layout.lines[row]
7293 .position_for_index(col, layout, false)
7294 .unwrap();
7295 origin
7296 + point(
7297 layout.line_number_width + local.x,
7298 layout.line_height * (row as f32 + 0.5),
7299 )
7300 };
7301 (position(0, 1), position(2, 3))
7302 });
7303 cx.update(|_, cx| {
7305 view.input.update(cx, |state, _| {
7306 state.extras.hover_definition.update(
7307 0..4,
7308 vec![lsp_types::LocationLink {
7309 origin_selection_range: None,
7310 target_uri: "file:///tmp/column-selection.rs".parse().unwrap(),
7311 target_range: Default::default(),
7312 target_selection_range: Default::default(),
7313 }],
7314 );
7315 });
7316 });
7317 cx.simulate_mouse_down(start, MouseButton::Left, modifiers);
7318 cx.simulate_mouse_move(end, MouseButton::Left, modifiers);
7319 cx.simulate_mouse_up(end, MouseButton::Left, modifiers);
7320 view.input.read_with(&cx, |state, _| {
7321 let ranges: Vec<_> = state
7322 .selections
7323 .iter()
7324 .map(|sel| sel.start..sel.end)
7325 .collect();
7326 assert_eq!(ranges, vec![1..3, 6..8, 11..13]);
7327 });
7328 cx.simulate_mouse_move(start, None, modifiers);
7330 cx.simulate_keystrokes("x");
7331 assert_cursors(&mut cx, &view.input, "ax|d\nax|d\nax|d");
7332 }
7333 }
7334
7335 #[gpui::test]
7336 fn test_alt_drag_extends_upward_from_an_existing_cursor(cx: &mut TestAppContext) {
7337 cx.update(crate::init);
7338 let view = InputView::<EditorMode>::new(cx);
7339 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7340 setup_cursors(&mut cx, &view.input, "abcd\nabcd\na|bcd");
7341 cx.update(|window, cx| {
7342 view.input.update(cx, |state, cx| state.focus(window, cx));
7343 });
7344 let (start, end) = view.input.read_with(&cx, |state, _| {
7345 let layout = state.last_layout.as_ref().unwrap();
7346 let origin = state.last_bounds.unwrap().origin;
7347 let local = layout.lines[0]
7348 .position_for_index(1, layout, false)
7349 .unwrap();
7350 let x = layout.line_number_width + local.x;
7351 (
7352 origin + point(x, layout.line_height * 2.5),
7353 origin + point(x, layout.line_height * 0.5),
7354 )
7355 });
7356 let modifiers = gpui::Modifiers {
7357 alt: true,
7358 ..Default::default()
7359 };
7360 cx.simulate_mouse_down(start, MouseButton::Left, modifiers);
7361 cx.simulate_mouse_move(end, MouseButton::Left, modifiers);
7362 cx.simulate_mouse_up(end, MouseButton::Left, modifiers);
7363 assert_cursors(&mut cx, &view.input, "a|bcd\na|bcd\na|bcd");
7364 }
7365
7366 #[gpui::test]
7367 fn test_alt_drag_over_a_short_row_keeps_the_block_width(cx: &mut TestAppContext) {
7368 cx.update(crate::init);
7369 let view = InputView::<EditorMode>::new(cx);
7370 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7371 setup_cursors(&mut cx, &view.input, "abcdef\nab\nabcdef");
7372 cx.update(|window, cx| {
7373 view.input.update(cx, |state, cx| state.focus(window, cx));
7374 });
7375 let (start, end) = view.input.read_with(&cx, |state, _| {
7378 let layout = state.last_layout.as_ref().unwrap();
7379 let origin = state.last_bounds.unwrap().origin;
7380 let x_of = |index| {
7381 layout.line_number_width
7382 + layout.lines[0]
7383 .position_for_index(index, layout, false)
7384 .unwrap()
7385 .x
7386 };
7387 (
7388 origin + point(x_of(1), layout.line_height * 0.5),
7389 origin + point(x_of(5), layout.line_height * 1.5),
7390 )
7391 });
7392 let modifiers = gpui::Modifiers {
7393 alt: true,
7394 shift: true,
7395 ..Default::default()
7396 };
7397 cx.simulate_mouse_down(start, MouseButton::Left, modifiers);
7398 cx.simulate_mouse_move(end, MouseButton::Left, modifiers);
7399 cx.simulate_mouse_up(end, MouseButton::Left, modifiers);
7400
7401 view.input.read_with(&cx, |state, _| {
7403 let ranges: Vec<_> = state.selections.iter().map(|s| (s.start, s.end)).collect();
7404 assert_eq!(ranges, vec![(1, 5), (8, 9)]);
7405 });
7406 }
7407
7408 #[gpui::test]
7409 fn test_alt_mouse_release_outside_editor_ends_column_selection(cx: &mut TestAppContext) {
7410 cx.update(crate::init);
7411 let view = InputView::<EditorMode>::new(cx);
7412 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7413 setup_cursors(&mut cx, &view.input, "|abcd\nabcd");
7414 let position = view.input.read_with(&cx, |state, _| {
7415 let layout = state.last_layout.as_ref().unwrap();
7416 state.last_bounds.unwrap().origin
7417 + point(layout.line_number_width + px(2.), layout.line_height * 0.5)
7418 });
7419 let modifiers = gpui::Modifiers {
7420 alt: true,
7421 ..Default::default()
7422 };
7423 cx.simulate_mouse_down(position, MouseButton::Left, modifiers);
7424 cx.simulate_mouse_up(point(px(-100.), px(-100.)), MouseButton::Left, modifiers);
7425 view.input.read_with(&cx, |state, _| {
7426 assert!(!state.selecting);
7427 assert!(state.column_select_start.is_none());
7428 });
7429 }
7430
7431 #[gpui::test]
7432 fn test_consumed_keystrokes_keep_cursor_visible(cx: &mut TestAppContext) {
7433 let view = InputView::<EditorMode>::new(cx);
7434 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7435 setup_cursors(&mut cx, &view.input, "a|b");
7436 cx.update(|window, cx| {
7437 view.input.update(cx, |state, cx| {
7438 state.focus(window, cx);
7439 state.pause_blink_cursor(cx);
7440 });
7441 });
7442 cx.run_until_parked();
7443 cx.executor()
7444 .advance_clock(std::time::Duration::from_millis(300));
7445 cx.run_until_parked();
7446 view.input.read_with(&cx, |state, cx| {
7447 assert!(!state.blink_cursor.read(cx).visible());
7448 });
7449 for _ in 0..5 {
7451 #[cfg(target_os = "macos")]
7452 cx.simulate_keystrokes("cmd-c");
7453 #[cfg(not(target_os = "macos"))]
7454 cx.simulate_keystrokes("ctrl-c");
7455 cx.run_until_parked();
7456 cx.executor()
7457 .advance_clock(std::time::Duration::from_millis(200));
7458 cx.run_until_parked();
7459 view.input.read_with(&cx, |state, cx| {
7460 assert!(state.blink_cursor.read(cx).visible());
7461 });
7462 }
7463 }
7464
7465 #[gpui::test]
7466 fn test_multi_cursor_actions_reveal_hidden_carets(cx: &mut TestAppContext) {
7467 let view = InputView::<EditorMode>::new(cx);
7468 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7469 setup_cursors(&mut cx, &view.input, "ab\na|b\nab");
7470 for action in 0..6 {
7473 cx.update(|_, cx| {
7474 view.input.update(cx, |state, cx| {
7475 state.blink_cursor = cx.new(|_| BlinkCursor::new());
7476 state.blink_cursor.update(cx, |cursor, cx| cursor.start(cx));
7477 });
7478 });
7479 cx.run_until_parked();
7480 cx.executor()
7481 .advance_clock(std::time::Duration::from_millis(500));
7482 cx.run_until_parked();
7483 view.input.read_with(&cx, |state, cx| {
7484 assert!(!state.blink_cursor.read(cx).visible(), "action {action}");
7485 });
7486
7487 cx.update(|window, cx| {
7488 view.input.update(cx, |state, cx| {
7489 match action {
7490 0 => state.add_cursor_above(&AddCursorAbove, window, cx),
7491 1 => state.add_cursor_below(&AddCursorBelow, window, cx),
7492 2 => state.select_up(&SelectUp, window, cx),
7493 3 => state.select_down(&SelectDown, window, cx),
7494 4 => state.replace_text_in_range(None, "x", window, cx),
7495 _ => state.backspace(&Backspace, window, cx),
7496 }
7497 assert!(state.blink_cursor.read(cx).visible(), "action {action}");
7498 });
7499 });
7500 }
7501 }
7502
7503 #[gpui::test]
7504 fn test_multi_cursor_keyboard_dispatch(cx: &mut TestAppContext) {
7505 let view = InputView::<EditorMode>::new(cx);
7506 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7507 setup_cursors(&mut cx, &view.input, "ab\na|b\nab");
7508 cx.update(|window, cx| {
7509 view.input.update(cx, |state, cx| state.focus(window, cx));
7510 });
7511 #[cfg(target_os = "macos")]
7512 cx.simulate_keystrokes("cmd-alt-up");
7513 #[cfg(target_os = "windows")]
7514 cx.simulate_keystrokes("ctrl-alt-up");
7515 #[cfg(not(any(target_os = "macos", target_os = "windows")))]
7516 cx.simulate_keystrokes("alt-shift-up");
7517 view.input
7518 .read_with(&cx, |state, _| assert_eq!(state.selections.len(), 2));
7519 #[cfg(target_os = "macos")]
7520 cx.simulate_keystrokes("cmd-alt-down");
7521 #[cfg(target_os = "windows")]
7522 cx.simulate_keystrokes("ctrl-alt-down");
7523 #[cfg(not(any(target_os = "macos", target_os = "windows")))]
7524 cx.simulate_keystrokes("alt-shift-down");
7525 view.input
7526 .read_with(&cx, |state, _| assert_eq!(state.selections.len(), 3));
7527 cx.simulate_keystrokes("x");
7528 assert_cursors(&mut cx, &view.input, "ax|b\nax|b\nax|b");
7529 }
7530
7531 #[gpui::test]
7532 fn test_multi_cursor_platform_word_selection_dispatch(cx: &mut TestAppContext) {
7533 let view = InputView::<EditorMode>::new(cx);
7534 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7535 setup_cursors(&mut cx, &view.input, "one |two\none |two");
7536 cx.update(|window, cx| {
7537 view.input.update(cx, |state, cx| state.focus(window, cx));
7538 });
7539 #[cfg(any(target_os = "macos", target_os = "linux"))]
7540 cx.simulate_keystrokes("alt-shift-right");
7541 #[cfg(not(any(target_os = "macos", target_os = "linux")))]
7542 cx.simulate_keystrokes("ctrl-shift-right");
7543 cx.simulate_keystrokes("x");
7544 assert_cursors(&mut cx, &view.input, "one x|\none x|");
7545 setup_cursors(&mut cx, &view.input, "one two|\none two|");
7546 #[cfg(any(target_os = "macos", target_os = "linux"))]
7547 cx.simulate_keystrokes("alt-shift-left");
7548 #[cfg(not(any(target_os = "macos", target_os = "linux")))]
7549 cx.simulate_keystrokes("ctrl-shift-left");
7550 cx.simulate_keystrokes("x");
7551 assert_cursors(&mut cx, &view.input, "one x|\none x|");
7552 }
7553
7554 #[cfg(not(target_os = "macos"))]
7555 #[gpui::test]
7556 fn test_multi_cursor_horizontal_selection_dispatch(cx: &mut TestAppContext) {
7557 let view = InputView::<EditorMode>::new(cx);
7558 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7559 setup_cursors(&mut cx, &view.input, "ab\na|b");
7560 cx.update(|window, cx| {
7561 view.input.update(cx, |state, cx| state.focus(window, cx));
7562 });
7563 #[cfg(target_os = "windows")]
7564 cx.simulate_keystrokes("ctrl-alt-up");
7565 #[cfg(not(target_os = "windows"))]
7566 cx.simulate_keystrokes("alt-shift-up");
7567 #[cfg(target_os = "linux")]
7568 cx.simulate_keystrokes("shift-right");
7569 #[cfg(not(target_os = "linux"))]
7570 cx.simulate_keystrokes("alt-shift-right");
7571 view.input.read_with(&cx, |state, _| {
7572 assert_eq!(
7573 state
7574 .selections
7575 .iter()
7576 .map(|s| s.start..s.end)
7577 .collect::<Vec<_>>(),
7578 vec![4..5, 1..2]
7579 );
7580 });
7581 #[cfg(target_os = "linux")]
7582 cx.simulate_keystrokes("shift-left shift-left");
7583 #[cfg(not(target_os = "linux"))]
7584 cx.simulate_keystrokes("alt-shift-left alt-shift-left");
7585 view.input.read_with(&cx, |state, _| {
7586 assert_eq!(
7587 state
7588 .selections
7589 .iter()
7590 .map(|s| s.start..s.end)
7591 .collect::<Vec<_>>(),
7592 vec![3..4, 0..1]
7593 );
7594 });
7595 cx.simulate_keystrokes("x");
7596 assert_cursors(&mut cx, &view.input, "x|b\nx|b");
7597 }
7598
7599 #[gpui::test]
7600 fn test_multi_cursor_alt_click_dispatch(cx: &mut TestAppContext) {
7601 cx.update(crate::init);
7602 let view = InputView::<EditorMode>::new(cx);
7603 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7604 setup_cursors(&mut cx, &view.input, "a|b\nab\nab");
7605 cx.update(|window, cx| {
7606 view.input.update(cx, |state, cx| state.focus(window, cx));
7607 });
7608 let position = view.input.read_with(&cx, |state, _| {
7609 let bounds = state.last_bounds.unwrap();
7610 let layout = state.last_layout.as_ref().unwrap();
7611 bounds.origin + point(layout.line_number_width + px(2.), layout.line_height * 1.5)
7612 });
7613 cx.simulate_click(
7614 position,
7615 gpui::Modifiers {
7616 alt: true,
7617 ..Default::default()
7618 },
7619 );
7620 view.input
7621 .read_with(&cx, |state, _| assert_eq!(state.selections.len(), 2));
7622 }
7623
7624 #[gpui::test]
7625 fn test_word_delete_undo_restores_caret(cx: &mut TestAppContext) {
7626 let view = multi_line(cx);
7627 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7628 setup_cursors(&mut cx, &view.input, "hello|");
7629 cx.update(|window, cx| {
7630 view.input.update(cx, |state, cx| {
7631 state.delete_previous_word(&DeleteToPreviousWordStart, window, cx);
7632 state.undo(&Undo, window, cx);
7633 assert_eq!(state.selected_range(), 5..5);
7634 });
7635 });
7636 }
7637
7638 #[gpui::test]
7639 fn test_merged_delete_undo_restores_all_carets(cx: &mut TestAppContext) {
7640 let view = multi_line(cx);
7641 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7642 setup_cursors(&mut cx, &view.input, "a|b|c");
7643 cx.update(|window, cx| {
7644 view.input.update(cx, |state, cx| {
7645 state.backspace(&Backspace, window, cx);
7646 state.undo(&Undo, window, cx);
7647 });
7648 });
7649 assert_cursors(&mut cx, &view.input, "a|b|c");
7650 }
7651
7652 #[gpui::test]
7653 fn test_shift_end_respects_soft_wrap_end(cx: &mut TestAppContext) {
7654 let view = InputView::<EditorMode>::new(cx);
7655 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7656 cx.update(|window, cx| {
7657 view.input.update(cx, |state, cx| {
7658 state.set_value("abcdef ".repeat(30), window, cx);
7659 state.display_map.on_layout_changed(Some(px(60.)), cx);
7660 let line = state.display_map.line(0).unwrap();
7661 assert!(line.wrapped_lines.len() > 1);
7662 let boundary = line.wrapped_lines[0].end;
7663 state.move_to_with_affinity(boundary, None, true, cx);
7664 state.select_to_end_of_line(&SelectToEndOfLine, window, cx);
7665 assert_eq!(state.selected_range(), boundary..state.text.len());
7666 });
7667 });
7668 }
7669
7670 #[gpui::test]
7671 fn test_outdent_unindented_unicode_is_unchanged(cx: &mut TestAppContext) {
7672 let view = multi_line(cx);
7673 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7674 setup_cursors(&mut cx, &view.input, "|你好\n|世界");
7675 cx.update(|window, cx| {
7676 view.input.update(cx, |state, cx| {
7677 state.outdent(false, window, cx);
7678 state.outdent(true, window, cx);
7679 });
7680 });
7681 assert_cursors(&mut cx, &view.input, "|你好\n|世界");
7682 }
7683
7684 #[gpui::test]
7685 fn test_column_selection_stays_on_unicode_boundaries(cx: &mut TestAppContext) {
7686 let view = multi_line(cx);
7687 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7688 setup_cursors(&mut cx, &view.input, "ab\n你好\ncd");
7689 cx.update(|window, cx| {
7690 view.input.update(cx, |state, cx| {
7691 state.build_columnar_selection(
7692 ColumnarPoint::new(1, 0),
7693 ColumnarPoint::new(11, 0),
7694 cx,
7695 );
7696 let text = state.value();
7697 for sel in state.selections.iter() {
7698 assert!(text.is_char_boundary(sel.start));
7699 assert!(text.is_char_boundary(sel.end));
7700 }
7701 state.replace_text_in_range(None, "X", window, cx);
7702 });
7703 });
7704 }
7705
7706 #[gpui::test]
7707 fn test_editor_decorations_follow_typing(cx: &mut TestAppContext) {
7708 let view = InputView::<EditorMode>::new(cx);
7709 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7710 cx.update(|window, cx| {
7711 view.input.update(cx, |state, cx| {
7712 state.set_value("abc def", window, cx);
7713 state.create_decorations_collection(
7714 vec![crate::input::TextDecoration::new(
7715 4..7,
7716 gpui::HighlightStyle::default(),
7717 )],
7718 cx,
7719 );
7720 state.set_selected_range(0..0, cx);
7721 state.replace_text_in_range(None, "X", window, cx);
7722 let layers = state.extras.decoration_layers();
7723 assert_eq!(layers.into_iter().flatten().next().unwrap().range, 5..8);
7724 });
7725 });
7726 }
7727
7728 #[gpui::test]
7729 fn test_backspace_preserves_escaped_quote_terminator(cx: &mut TestAppContext) {
7730 let view = InputView::<EditorMode>::new(cx);
7731 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7732 setup_cursors(&mut cx, &view.input, r#"{"s":"\"|"}"#);
7733 cx.update(|window, cx| {
7734 view.input.update(cx, |state, cx| {
7735 set_test_syntax_provider(std::rc::Rc::new(StringAllProvider), cx);
7736 state.backspace(&Backspace, window, cx);
7737 });
7738 });
7739 assert_cursors(&mut cx, &view.input, r#"{"s":"\|"}"#);
7740 }
7741
7742 #[gpui::test]
7743 fn test_ordinary_typing_does_not_query_syntax(cx: &mut TestAppContext) {
7744 struct UnexpectedQuery;
7745 impl crate::input::SyntaxContextProvider for UnexpectedQuery {
7746 fn context_at(&self, _: &ropey::Rope, _: usize) -> crate::input::SyntaxContext {
7747 panic!("ordinary typing must not query syntax");
7748 }
7749 }
7750 let view = InputView::<EditorMode>::new(cx);
7751 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7752 setup_cursors(&mut cx, &view.input, "|");
7753 cx.update(|window, cx| {
7754 view.input.update(cx, |state, cx| {
7755 set_test_syntax_provider(std::rc::Rc::new(UnexpectedQuery), cx);
7756 for c in ["a", "b", "c"] {
7757 state.replace_text_in_range(None, c, window, cx);
7758 }
7759 });
7760 });
7761 assert_cursors(&mut cx, &view.input, "abc|");
7762 }
7763
7764 #[gpui::test]
7765 fn test_auto_close_is_atomic_at_history_limit(cx: &mut TestAppContext) {
7766 let view = InputView::<EditorMode>::new(cx);
7767 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7768 setup_cursors(&mut cx, &view.input, "|");
7769 cx.update(|window, cx| {
7770 view.input.update(cx, |state, cx| {
7771 for _ in 0..999 {
7772 state.replace_text_in_range(None, "a", window, cx);
7773 }
7774 state.replace_text_in_range(None, "(", window, cx);
7775 state.undo(&Undo, window, cx);
7776 assert!(!state.text.to_string().contains('('));
7777 assert!(!state.text.to_string().contains(')'));
7778 state.redo(&Redo, window, cx);
7779 });
7780 });
7781 assert_cursors(&mut cx, &view.input, &format!("{}(|)", "a".repeat(999)));
7782 }
7783
7784 #[gpui::test]
7785 fn test_comment_closer_is_inserted_literally(cx: &mut TestAppContext) {
7786 let view = InputView::<EditorMode>::new(cx);
7787 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7788 setup_cursors(&mut cx, &view.input, "// (|)");
7789 cx.update(|window, cx| {
7790 view.input.update(cx, |state, cx| {
7791 set_test_syntax_provider(std::rc::Rc::new(CommentAllProvider), cx);
7792 state.replace_text_in_range(None, ")", window, cx);
7793 });
7794 });
7795 assert_cursors(&mut cx, &view.input, "// ()|)");
7796 }
7797
7798 #[gpui::test]
7799 fn test_explicit_range_skip_does_not_delete_existing_closer_on_undo(cx: &mut TestAppContext) {
7800 let view = InputView::<EditorMode>::new(cx);
7801 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7802 setup_cursors(&mut cx, &view.input, "(a|)");
7803 let before = view.input.read_with(&cx, |state, _| state.text.to_string());
7804 cx.update(|window, cx| {
7805 view.input.update(cx, |state, cx| {
7806 state.replace_text_in_range(Some(2..2), ")", window, cx);
7807 });
7808 });
7809 assert_cursors(&mut cx, &view.input, "(a)|");
7810 cx.update(|window, cx| {
7811 view.input
7812 .update(cx, |state, cx| state.undo(&Undo, window, cx));
7813 });
7814 view.input.read_with(&cx, |state, _| {
7815 assert_eq!(
7816 state.text.to_string(),
7817 before,
7818 "skip must not remove an existing closer on undo"
7819 );
7820 });
7821 }
7822
7823 #[gpui::test]
7824 fn test_pair_redo_restores_interior_cursor(cx: &mut TestAppContext) {
7825 let view = InputView::<EditorMode>::new(cx);
7826 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7827 setup_cursors(&mut cx, &view.input, "|");
7828 cx.update(|window, cx| {
7829 view.input.update(cx, |state, cx| {
7830 state.replace_text_in_range(None, "(", window, cx);
7831 });
7832 });
7833 assert_cursors(&mut cx, &view.input, "(|)");
7834 cx.update(|window, cx| {
7835 view.input.update(cx, |state, cx| {
7836 state.undo(&Undo, window, cx);
7837 state.redo(&Redo, window, cx);
7838 });
7839 });
7840 assert_cursors(&mut cx, &view.input, "(|)");
7841 }
7842
7843 #[gpui::test]
7844 fn test_pair_enter_redo_restores_interior_cursor(cx: &mut TestAppContext) {
7845 let view = InputView::<EditorMode>::new(cx);
7846 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7847 setup_cursors(&mut cx, &view.input, "{|}");
7848 cx.update(|window, cx| {
7849 view.input.update(cx, |state, cx| {
7850 state.enter(
7851 &Enter {
7852 secondary: false,
7853 shift: false,
7854 },
7855 window,
7856 cx,
7857 );
7858 });
7859 });
7860 assert_cursors(&mut cx, &view.input, "{\n |\n}");
7861 cx.update(|window, cx| {
7862 view.input.update(cx, |state, cx| {
7863 state.undo(&Undo, window, cx);
7864 });
7865 });
7866 assert_cursors(&mut cx, &view.input, "{|}");
7867 cx.update(|window, cx| {
7868 view.input
7869 .update(cx, |state, cx| state.redo(&Redo, window, cx));
7870 });
7871 assert_cursors(&mut cx, &view.input, "{\n |\n}");
7872 }
7873
7874 #[gpui::test]
7875 fn test_auto_close_parens(cx: &mut TestAppContext) {
7876 let view = InputView::<EditorMode>::new(cx);
7877 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7878 setup_cursors(&mut cx, &view.input, "|");
7879 cx.update(|window, cx| {
7880 view.input.update(cx, |state, cx| {
7881 state.replace_text_in_range(None, "(", window, cx);
7882 });
7883 });
7884 assert_cursors(&mut cx, &view.input, "(|)");
7885 cx.update(|window, cx| {
7887 view.input.update(cx, |state, cx| {
7888 state.replace_text_in_range(None, "x", window, cx);
7889 });
7890 });
7891 assert_cursors(&mut cx, &view.input, "(x|)");
7892 }
7893
7894 #[gpui::test]
7895 fn test_auto_close_skip_over_closer(cx: &mut TestAppContext) {
7896 let view = InputView::<EditorMode>::new(cx);
7897 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7898 setup_cursors(&mut cx, &view.input, "(|)");
7899 cx.update(|window, cx| {
7900 view.input.update(cx, |state, cx| {
7901 state.replace_text_in_range(None, ")", window, cx);
7902 });
7903 });
7904 assert_cursors(&mut cx, &view.input, "()|");
7906 view.input.read_with(&cx, |state, _| {
7908 assert!(state.active_selection().is_empty());
7909 });
7910 }
7911
7912 #[gpui::test]
7913 fn test_auto_close_no_pair_inside_word(cx: &mut TestAppContext) {
7914 let view = InputView::<EditorMode>::new(cx);
7915 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7916 setup_cursors(&mut cx, &view.input, "don|");
7917 cx.update(|window, cx| {
7918 view.input.update(cx, |state, cx| {
7919 state.replace_text_in_range(None, "'", window, cx);
7920 });
7921 });
7922 assert_cursors(&mut cx, &view.input, "don'|");
7924 }
7925
7926 #[gpui::test]
7927 fn test_auto_close_quote_after_cjk(cx: &mut TestAppContext) {
7928 let view = InputView::<EditorMode>::new(cx);
7929 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7930 setup_cursors(&mut cx, &view.input, "中|");
7933 cx.update(|window, cx| {
7934 view.input.update(cx, |state, cx| {
7935 state.replace_text_in_range(None, "'", window, cx);
7936 });
7937 });
7938 assert_cursors(&mut cx, &view.input, "中'|");
7939 }
7940
7941 #[gpui::test]
7942 fn test_auto_close_closer_skips_at_string_end(cx: &mut TestAppContext) {
7943 let view = InputView::<EditorMode>::new(cx);
7944 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7945 setup_cursors(&mut cx, &view.input, "\"hello|\"");
7947 cx.update(|window, cx| {
7948 view.input.update(cx, |state, cx| {
7949 state.replace_text_in_range(None, "\"", window, cx);
7950 });
7951 });
7952 assert_cursors(&mut cx, &view.input, "\"hello\"|");
7953 view.input.read_with(&cx, |state, _| {
7954 assert!(state.active_selection().is_empty());
7955 });
7956 }
7957
7958 #[gpui::test]
7959 fn test_backspace_pair_with_cjk_prefix(cx: &mut TestAppContext) {
7960 let view = InputView::<EditorMode>::new(cx);
7961 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7962 setup_cursors(&mut cx, &view.input, "中(|)abc");
7964 cx.update(|window, cx| {
7965 view.input
7966 .update(cx, |state, cx| state.backspace(&Backspace, window, cx));
7967 });
7968 assert_cursors(&mut cx, &view.input, "中|abc");
7969 }
7970
7971 #[gpui::test]
7972 fn test_enter_split_respects_smart_indent_off(cx: &mut TestAppContext) {
7973 let view = InputView::<EditorMode>::new(cx);
7974 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7975 setup_cursors(&mut cx, &view.input, "{|}");
7976 cx.update(|window, cx| {
7977 view.input.update(cx, |state, cx| {
7978 state.set_smart_indent(false, window, cx);
7979 state.enter(
7980 &Enter {
7981 secondary: false,
7982 shift: false,
7983 },
7984 window,
7985 cx,
7986 );
7987 });
7988 });
7989 assert_cursors(&mut cx, &view.input, "{\n|}");
7991 }
7992
7993 #[gpui::test]
7994 fn test_enter_split_is_independent_of_auto_close(cx: &mut TestAppContext) {
7995 let view = InputView::<EditorMode>::new(cx);
7996 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
7997 setup_cursors(&mut cx, &view.input, "{|}");
7998 cx.update(|window, cx| {
7999 view.input.update(cx, |state, cx| {
8000 state.set_auto_close(false, window, cx);
8001 state.enter(
8002 &Enter {
8003 secondary: false,
8004 shift: false,
8005 },
8006 window,
8007 cx,
8008 );
8009 });
8010 });
8011 assert_cursors(&mut cx, &view.input, "{\n |\n}");
8013 }
8014
8015 #[gpui::test]
8016 fn test_language_config_applies_before_render_and_on_language_change(cx: &mut TestAppContext) {
8017 use crate::input::{AutoClosingPair, set_language_config};
8018 let view = InputView::<EditorMode>::new(cx);
8019 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8020 cx.update(|window, cx| {
8021 set_language_config(
8022 "alpha",
8023 LanguageConfig::default().auto_closing_pairs([AutoClosingPair::new("«", "»")]),
8024 cx,
8025 );
8026 set_language_config(
8027 "beta",
8028 LanguageConfig::default().auto_closing_pairs([AutoClosingPair::new("‹", "›")]),
8029 cx,
8030 );
8031 let first = cx.new(|cx| EditorState::new(window, cx).language("ALPHA"));
8032 let second = cx.new(|cx| EditorState::new(window, cx).language("beta"));
8033 first.update(cx, |state, cx| {
8034 state.replace_text_in_range(None, "«", window, cx);
8035 assert_eq!(state.text().to_string(), "«»");
8036 state.set_value("", window, cx);
8037 state.set_smart_indent(false, window, cx);
8038 state.set_highlighter("beta", cx);
8039 assert!(!state.mode.is_smart_indent());
8040 state.replace_text_in_range(None, "‹", window, cx);
8041 assert_eq!(state.text().to_string(), "‹›");
8042 state.set_value("", window, cx);
8043 state.set_highlighter("unknown", cx);
8044 state.replace_text_in_range(None, "(", window, cx);
8045 assert_eq!(state.text().to_string(), "()");
8046 });
8047 second.update(cx, |state, cx| {
8048 state.replace_text_in_range(None, "‹", window, cx);
8049 assert_eq!(state.text().to_string(), "‹›");
8050 });
8051 });
8052 }
8053
8054 #[gpui::test]
8055 fn test_language_service_replacement_updates_syntax_without_render(cx: &mut TestAppContext) {
8056 use crate::input::{LanguageProvider, SyntaxContextProvider, set_language_provider};
8057 struct StringLanguages(Rc<Cell<usize>>);
8058 impl LanguageProvider for StringLanguages {
8059 fn syntax_context_provider(&self, _: &str) -> Option<Rc<dyn SyntaxContextProvider>> {
8060 self.0.set(self.0.get() + 1);
8061 Some(Rc::new(StringAllProvider))
8062 }
8063 }
8064 struct CodeLanguages;
8065 impl LanguageProvider for CodeLanguages {}
8066 let view = InputView::<EditorMode>::new(cx);
8067 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8068 cx.update(|window, cx| {
8069 let creations = Rc::new(Cell::new(0));
8070 set_language_provider(Rc::new(StringLanguages(creations.clone())), cx);
8071 view.input.update(cx, |state, cx| {
8072 state.replace_text_in_range(None, "(", window, cx);
8073 state.replace_text_in_range(None, "[", window, cx);
8074 assert_eq!(state.text().to_string(), "([");
8075 assert_eq!(
8076 creations.get(),
8077 1,
8078 "retain the document provider between edits"
8079 );
8080 });
8081 set_language_provider(Rc::new(CodeLanguages), cx);
8082 view.input.update(cx, |state, cx| {
8083 state.replace_text_in_range(None, "{", window, cx);
8084 assert_eq!(state.text().to_string(), "([{}");
8085 });
8086 });
8087 }
8088
8089 #[gpui::test]
8090 fn test_language_config_updates_existing_editors(cx: &mut TestAppContext) {
8091 use crate::input::{AutoClosingPair, set_language_config};
8092 let view = InputView::<EditorMode>::new(cx);
8093 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8094 setup_cursors(&mut cx, &view.input, "|");
8095 cx.update(|window, cx| {
8096 view.input.update(cx, |state, cx| {
8097 state.set_highlighter("CUSTOM", cx);
8098 state.set_auto_close(false, window, cx);
8099 state.set_smart_indent(false, window, cx);
8100 });
8101 set_language_config(
8102 "custom",
8103 LanguageConfig::default().auto_closing_pairs([AutoClosingPair::new("«", "»")]),
8104 cx,
8105 );
8106 set_language_config("other", LanguageConfig::default(), cx);
8108 view.input.update(cx, |state, cx| {
8109 assert!(!state.mode.is_auto_close());
8110 assert!(!state.mode.is_smart_indent());
8111 state.set_auto_close(true, window, cx);
8112 state.replace_text_in_range(None, "«", window, cx);
8113 });
8114 });
8115 assert_cursors(&mut cx, &view.input, "«|»");
8116 }
8117
8118 #[gpui::test]
8119 fn test_auto_close_before_is_language_configurable(cx: &mut TestAppContext) {
8120 let view = InputView::<EditorMode>::new(cx);
8121 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8122 setup_cursors(&mut cx, &view.input, "|word");
8123 cx.update(|window, cx| {
8124 view.input.update(cx, |state, cx| {
8125 state.replace_text_in_range(None, "(", window, cx)
8126 });
8127 });
8128 assert_cursors(&mut cx, &view.input, "(|word");
8129 setup_cursors(&mut cx, &view.input, "|word");
8130 cx.update(|window, cx| {
8131 view.input.update(cx, |state, cx| {
8132 crate::input::set_language_config(
8133 state.language_name(),
8134 LanguageConfig::default().auto_close_before("w"),
8135 cx,
8136 );
8137 state.replace_text_in_range(None, "(", window, cx);
8138 });
8139 });
8140 assert_cursors(&mut cx, &view.input, "(|)word");
8141 }
8142
8143 #[gpui::test]
8144 fn test_pair_context_restrictions_are_per_pair(cx: &mut TestAppContext) {
8145 use crate::input::{AutoClosingPair, SyntaxContext};
8146 let view = InputView::<EditorMode>::new(cx);
8147 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8148 setup_cursors(&mut cx, &view.input, "|");
8149 cx.update(|window, cx| {
8150 view.input.update(cx, |state, cx| {
8151 set_test_syntax_provider(std::rc::Rc::new(CommentAllProvider), cx);
8152 crate::input::set_language_config(
8153 state.language_name(),
8154 LanguageConfig::default().auto_closing_pairs([
8155 AutoClosingPair::new("(", ")").not_in([SyntaxContext::String]),
8156 AutoClosingPair::new("[", "]").not_in([SyntaxContext::Comment]),
8157 ]),
8158 cx,
8159 );
8160 state.replace_text_in_range(None, "(", window, cx);
8161 state.replace_text_in_range(None, "[", window, cx);
8162 });
8163 });
8164 assert_cursors(&mut cx, &view.input, "([|)");
8165 }
8166
8167 #[gpui::test]
8168 fn test_multichar_delimiters_insert_delete_and_skip(cx: &mut TestAppContext) {
8169 use crate::input::{AutoClosingPair, SyntaxContext};
8170 struct BlockComment;
8171 impl crate::input::SyntaxContextProvider for BlockComment {
8172 fn context_at(&self, text: &ropey::Rope, _: usize) -> SyntaxContext {
8173 if text.to_string().contains("/*") {
8174 SyntaxContext::Comment
8175 } else {
8176 SyntaxContext::Code
8177 }
8178 }
8179 }
8180 let view = InputView::<EditorMode>::new(cx);
8181 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8182 setup_cursors(&mut cx, &view.input, "|");
8183 cx.update(|window, cx| {
8184 view.input.update(cx, |state, cx| {
8185 set_test_syntax_provider(std::rc::Rc::new(BlockComment), cx);
8186 crate::input::set_language_config(
8187 state.language_name(),
8188 LanguageConfig::default()
8189 .auto_closing_pairs([AutoClosingPair::new("/*", "*/")
8190 .not_in([SyntaxContext::String, SyntaxContext::Comment])]),
8191 cx,
8192 );
8193 state.replace_text_in_range(None, "/", window, cx);
8194 state.replace_text_in_range(None, "*", window, cx);
8195 });
8196 });
8197 assert_cursors(&mut cx, &view.input, "/*|*/");
8198 cx.update(|window, cx| {
8199 view.input
8200 .update(cx, |state, cx| state.backspace(&Backspace, window, cx));
8201 });
8202 assert_cursors(&mut cx, &view.input, "|");
8203 cx.update(|window, cx| {
8204 view.input.update(cx, |state, cx| {
8205 state.undo(&Undo, window, cx);
8206 state.replace_text_in_range(None, "*", window, cx);
8207 state.replace_text_in_range(None, "/", window, cx);
8208 });
8209 });
8210 assert_cursors(&mut cx, &view.input, "/**/|");
8211 }
8212
8213 #[gpui::test]
8214 fn test_auto_closed_pairs_fallback_and_explicit_disable(cx: &mut TestAppContext) {
8215 use crate::input::BracketPair;
8216 let view = InputView::<EditorMode>::new(cx);
8217 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8218 setup_cursors(&mut cx, &view.input, "|");
8219 cx.update(|window, cx| {
8220 view.input.update(cx, |state, cx| {
8221 let mut rules = LanguageConfig::default().brackets([BracketPair::new("«", "»")]);
8222 rules.auto_closing_pairs = None;
8223 crate::input::set_language_config(state.language_name(), rules.clone(), cx);
8224 state.replace_text_in_range(None, "«", window, cx);
8225 crate::input::set_language_config(
8226 state.language_name(),
8227 rules.auto_closing_pairs([]),
8228 cx,
8229 );
8230 state.replace_text_in_range(None, "«", window, cx);
8231 });
8232 });
8233 assert_cursors(&mut cx, &view.input, "««|»");
8234 }
8235
8236 #[gpui::test]
8237 fn test_undo_open_composition_preserves_generated_pairs(cx: &mut TestAppContext) {
8238 use crate::input::{AutoClosingPair, SyntaxContext};
8239 let view = InputView::<EditorMode>::new(cx);
8240 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8241 setup_cursors(&mut cx, &view.input, "|");
8242 cx.update(|window, cx| {
8243 view.input.update(cx, |state, cx| {
8244 crate::input::set_language_config(
8245 state.language_name(),
8246 LanguageConfig::default()
8247 .auto_closing_pairs([
8248 AutoClosingPair::new("/*", "*/").not_in([SyntaxContext::Comment])
8249 ]),
8250 cx,
8251 );
8252 state.replace_text_in_range(None, "/", window, cx);
8253 state.replace_text_in_range(None, "*", window, cx);
8254 set_test_syntax_provider(std::rc::Rc::new(CommentAllProvider), cx);
8255 state.replace_and_mark_text_in_range(None, "x", Some(1..1), window, cx);
8256 state.undo(&Undo, window, cx);
8257 state.replace_text_in_range(None, "*", window, cx);
8258 state.replace_text_in_range(None, "/", window, cx);
8259 });
8260 });
8261 assert_cursors(&mut cx, &view.input, "/**/|");
8262 }
8263
8264 #[gpui::test]
8265 fn test_undo_does_not_promote_literal_comment_delimiters(cx: &mut TestAppContext) {
8266 use crate::input::{AutoClosingPair, SyntaxContext};
8267 let view = InputView::<EditorMode>::new(cx);
8268 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8269 setup_cursors(&mut cx, &view.input, "/*|*/");
8270 cx.update(|window, cx| {
8271 view.input.update(cx, |state, cx| {
8272 set_test_syntax_provider(std::rc::Rc::new(CommentAllProvider), cx);
8273 crate::input::set_language_config(
8274 state.language_name(),
8275 LanguageConfig::default()
8276 .auto_closing_pairs([
8277 AutoClosingPair::new("/*", "*/").not_in([SyntaxContext::Comment])
8278 ]),
8279 cx,
8280 );
8281 state.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
8282 state.undo(&Undo, window, cx);
8283 state.backspace(&Backspace, window, cx);
8284 });
8285 });
8286 assert_cursors(&mut cx, &view.input, "/|*/");
8287 }
8288
8289 #[gpui::test]
8290 fn test_multiple_generated_pairs_retain_their_identity(cx: &mut TestAppContext) {
8291 use crate::input::{AutoClosingPair, SyntaxContext};
8292 struct CommentScope;
8293 impl crate::input::SyntaxContextProvider for CommentScope {
8294 fn context_at(&self, text: &ropey::Rope, offset: usize) -> SyntaxContext {
8295 if text
8296 .to_string()
8297 .find("*/")
8298 .is_some_and(|end| offset < end + 2)
8299 {
8300 SyntaxContext::Comment
8301 } else {
8302 SyntaxContext::Code
8303 }
8304 }
8305 }
8306 let view = InputView::<EditorMode>::new(cx);
8307 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8308 setup_cursors(&mut cx, &view.input, "|");
8309 cx.update(|window, cx| {
8310 view.input.update(cx, |state, cx| {
8311 set_test_syntax_provider(std::rc::Rc::new(CommentScope), cx);
8312 crate::input::set_language_config(
8313 state.language_name(),
8314 LanguageConfig::default().auto_closing_pairs([
8315 AutoClosingPair::new("/*", "*/").not_in([SyntaxContext::Comment]),
8316 AutoClosingPair::new("(", ")").not_in([SyntaxContext::Comment]),
8317 ]),
8318 cx,
8319 );
8320 state.replace_text_in_range(None, "/", window, cx);
8321 state.replace_text_in_range(None, "*", window, cx);
8322 state.set_selected_range(4..4, cx);
8323 state.replace_text_in_range(None, "(", window, cx);
8324 state.set_selected_range(2..2, cx);
8325 state.replace_text_in_range(None, "*", window, cx);
8326 state.replace_text_in_range(None, "/", window, cx);
8327 });
8328 });
8329 assert_cursors(&mut cx, &view.input, "/**/|()");
8330 cx.update(|window, cx| {
8331 view.input.update(cx, |state, cx| {
8332 state.set_selected_range(2..2, cx);
8333 state.backspace(&Backspace, window, cx);
8334 state.undo(&Undo, window, cx);
8335 state.redo(&Redo, window, cx);
8336 });
8337 });
8338 assert_cursors(&mut cx, &view.input, "|()");
8339 }
8340
8341 #[gpui::test]
8342 fn test_replacing_generated_opener_invalidates_the_pair(cx: &mut TestAppContext) {
8343 use crate::input::{AutoClosingPair, SyntaxContext};
8344 let view = InputView::<EditorMode>::new(cx);
8345 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8346 setup_cursors(&mut cx, &view.input, "|");
8347 cx.update(|window, cx| {
8348 view.input.update(cx, |state, cx| {
8349 crate::input::set_language_config(
8350 state.language_name(),
8351 LanguageConfig::default()
8352 .auto_closing_pairs([
8353 AutoClosingPair::new("/*", "*/").not_in([SyntaxContext::Comment])
8354 ]),
8355 cx,
8356 );
8357 state.replace_text_in_range(None, "/", window, cx);
8358 state.replace_text_in_range(None, "*", window, cx);
8359 state.replace_text_in_range(Some(0..2), "//", window, cx);
8360 set_test_syntax_provider(std::rc::Rc::new(CommentAllProvider), cx);
8361 state.replace_text_in_range(None, "*", window, cx);
8362 });
8363 });
8364 assert_cursors(&mut cx, &view.input, "//*|*/");
8365 }
8366
8367 #[gpui::test]
8368 fn test_configuration_preserves_compiled_regex_options(cx: &mut TestAppContext) {
8369 use crate::input::IndentationRules;
8370 let view = InputView::<EditorMode>::new(cx);
8371 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8372 setup_cursors(&mut cx, &view.input, "BEGIN|");
8373 cx.update(|window, cx| {
8374 view.input.update(cx, |state, cx| {
8375 let rules = |insensitive| {
8376 LanguageConfig::default().indentation_rules(IndentationRules::new(
8377 regex::RegexBuilder::new("begin$")
8378 .case_insensitive(insensitive)
8379 .build()
8380 .unwrap(),
8381 regex::Regex::new("^end").unwrap(),
8382 ))
8383 };
8384 crate::input::set_language_config(state.language_name(), rules(false), cx);
8385 crate::input::set_language_config(state.language_name(), rules(true), cx);
8386 state.enter(
8387 &Enter {
8388 secondary: false,
8389 shift: false,
8390 },
8391 window,
8392 cx,
8393 );
8394 });
8395 });
8396 assert_cursors(&mut cx, &view.input, "BEGIN\n |");
8397 }
8398
8399 #[gpui::test]
8400 fn test_indent_patterns_are_applied_on_enter(cx: &mut TestAppContext) {
8401 use crate::input::IndentationRules;
8402 let view = InputView::<EditorMode>::new(cx);
8403 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8404 let rules = LanguageConfig::default().indentation_rules(IndentationRules::new(
8405 regex::Regex::new(r"\bbegin\s*$").unwrap(),
8406 regex::Regex::new(r"^\s*end\b").unwrap(),
8407 ));
8408 for (before, expected) in [("begin|", "begin\n |"), (" |end", " \n|end")] {
8409 setup_cursors(&mut cx, &view.input, before);
8410 cx.update(|window, cx| {
8411 view.input.update(cx, |state, cx| {
8412 crate::input::set_language_config(state.language_name(), rules.clone(), cx);
8413 state.enter(
8414 &Enter {
8415 secondary: false,
8416 shift: false,
8417 },
8418 window,
8419 cx,
8420 );
8421 });
8422 });
8423 assert_cursors(&mut cx, &view.input, expected);
8424 }
8425 }
8426
8427 #[gpui::test]
8428 fn test_custom_language_config(cx: &mut TestAppContext) {
8429 let view = InputView::<EditorMode>::new(cx);
8430 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8431 cx.update(|_window, cx| {
8432 view.input.update(cx, |state, cx| {
8433 crate::input::set_language_config(
8434 state.language_name(),
8435 crate::input::language_config::LanguageConfig::default()
8436 .brackets([crate::input::BracketPair::new("«", "»")])
8437 .auto_closing_pairs([crate::input::AutoClosingPair::new("«", "»")]),
8438 cx,
8439 );
8440 });
8441 });
8442 setup_cursors(&mut cx, &view.input, "|");
8444 cx.update(|window, cx| {
8445 view.input.update(cx, |state, cx| {
8446 state.replace_text_in_range(None, "«", window, cx);
8447 });
8448 });
8449 assert_cursors(&mut cx, &view.input, "«|»");
8450 cx.update(|window, cx| {
8452 view.input.update(cx, |state, cx| {
8453 state.set_value("\n", window, cx);
8454 state.set_selected_range(0..0, cx);
8455 state.replace_text_in_range(None, "(", window, cx);
8456 });
8457 });
8458 assert_cursors(&mut cx, &view.input, "(|");
8459 }
8460
8461 #[gpui::test]
8462 fn test_skip_records_no_history(cx: &mut TestAppContext) {
8463 let view = InputView::<EditorMode>::new(cx);
8464 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8465 setup_cursors(&mut cx, &view.input, "(a|)");
8466 cx.update(|window, cx| {
8467 view.input.update(cx, |state, cx| {
8468 state.replace_text_in_range(None, "b", window, cx);
8470 });
8471 });
8472 assert_cursors(&mut cx, &view.input, "(ab|)");
8473 cx.update(|window, cx| {
8474 view.input.update(cx, |state, cx| {
8475 state.replace_text_in_range(None, ")", window, cx);
8476 });
8477 });
8478 assert_cursors(&mut cx, &view.input, "(ab)|");
8480 cx.update(|window, cx| {
8482 view.input
8483 .update(cx, |state, cx| state.undo(&Undo, window, cx));
8484 });
8485 assert_cursors(&mut cx, &view.input, "(a|)");
8486 }
8487
8488 #[gpui::test]
8489 fn test_pair_insert_undo_removes_both(cx: &mut TestAppContext) {
8490 let view = InputView::<EditorMode>::new(cx);
8491 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8492 setup_cursors(&mut cx, &view.input, "|");
8493 cx.update(|window, cx| {
8494 view.input.update(cx, |state, cx| {
8495 state.replace_text_in_range(None, "(", window, cx);
8496 });
8497 });
8498 assert_cursors(&mut cx, &view.input, "(|)");
8499 cx.update(|window, cx| {
8501 view.input
8502 .update(cx, |state, cx| state.undo(&Undo, window, cx));
8503 });
8504 assert_cursors(&mut cx, &view.input, "|");
8505 }
8506
8507 #[gpui::test]
8508 fn test_escaped_quote_does_not_skip(cx: &mut TestAppContext) {
8509 let view = InputView::<EditorMode>::new(cx);
8510 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8511 setup_cursors(&mut cx, &view.input, "\"hello\\|\"");
8513 cx.update(|window, cx| {
8514 view.input.update(cx, |state, cx| {
8515 state.replace_text_in_range(None, "\"", window, cx);
8516 });
8517 });
8518 assert_cursors(&mut cx, &view.input, "\"hello\\\"|\"");
8519 }
8520
8521 #[gpui::test]
8522 fn test_even_backslashes_still_skip(cx: &mut TestAppContext) {
8523 let view = InputView::<EditorMode>::new(cx);
8524 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8525 setup_cursors(&mut cx, &view.input, "\"hello\\\\|\"");
8527 cx.update(|window, cx| {
8528 view.input.update(cx, |state, cx| {
8529 state.replace_text_in_range(None, "\"", window, cx);
8530 });
8531 });
8532 assert_cursors(&mut cx, &view.input, "\"hello\\\\\"|");
8533 }
8534
8535 #[gpui::test]
8536 fn test_editor_options_survive_configuration_replacement(cx: &mut TestAppContext) {
8537 use crate::input::{BracketPair, LanguageConfig};
8538
8539 let view = InputView::<EditorMode>::new(cx);
8540 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8541 cx.update(|window, cx| {
8542 view.input.update(cx, |state, cx| {
8543 state.set_smart_indent(false, window, cx);
8545 crate::input::set_language_config(
8546 state.language_name(),
8547 LanguageConfig::default().brackets([BracketPair::new("«", "»")]),
8548 cx,
8549 );
8550 });
8551 });
8552 view.input.read_with(&cx, |state, _| {
8553 assert!(!state.mode.is_smart_indent(), "editor option preserved");
8554 assert!(state.mode.is_auto_close());
8555 let pairs = &state.mode.language_config().unwrap().brackets;
8556 assert_eq!(pairs.len(), 1, "pairs follow configuration");
8557 assert_eq!(pairs[0].open.as_ref(), "«");
8558 });
8559 }
8560
8561 struct CommentAllProvider;
8562 impl crate::input::SyntaxContextProvider for CommentAllProvider {
8563 fn context_at(&self, _text: &ropey::Rope, _offset: usize) -> crate::input::SyntaxContext {
8564 crate::input::SyntaxContext::Comment
8565 }
8566 }
8567
8568 struct StringAllProvider;
8569 impl crate::input::SyntaxContextProvider for StringAllProvider {
8570 fn context_at(&self, _text: &ropey::Rope, _offset: usize) -> crate::input::SyntaxContext {
8571 crate::input::SyntaxContext::String
8572 }
8573 }
8574
8575 #[gpui::test]
8576 fn test_smart_indent_suppressed_in_strings(cx: &mut TestAppContext) {
8577 use std::rc::Rc;
8578
8579 let view = InputView::<EditorMode>::new(cx);
8580 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8581 cx.update(|_window, cx| {
8582 set_test_syntax_provider(Rc::new(StringAllProvider), cx);
8583 });
8584 setup_cursors(&mut cx, &view.input, " x = {|");
8586 cx.update(|window, cx| {
8587 view.input.update(cx, |state, cx| {
8588 state.enter(
8589 &Enter {
8590 secondary: false,
8591 shift: false,
8592 },
8593 window,
8594 cx,
8595 );
8596 });
8597 });
8598 assert_cursors(&mut cx, &view.input, " x = {\n |");
8599 }
8600
8601 #[gpui::test]
8602 fn test_split_suppressed_in_strings(cx: &mut TestAppContext) {
8603 use std::rc::Rc;
8604
8605 let view = InputView::<EditorMode>::new(cx);
8606 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8607 cx.update(|_window, cx| {
8608 set_test_syntax_provider(Rc::new(StringAllProvider), cx);
8609 });
8610 setup_cursors(&mut cx, &view.input, "{|}");
8612 cx.update(|window, cx| {
8613 view.input.update(cx, |state, cx| {
8614 state.enter(
8615 &Enter {
8616 secondary: false,
8617 shift: false,
8618 },
8619 window,
8620 cx,
8621 );
8622 });
8623 });
8624 assert_cursors(&mut cx, &view.input, "{\n|}");
8625 }
8626
8627 #[gpui::test]
8628 fn test_comment_context_disables_pairing(cx: &mut TestAppContext) {
8629 use std::rc::Rc;
8630
8631 let view = InputView::<EditorMode>::new(cx);
8632 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8633 cx.update(|_window, cx| {
8634 set_test_syntax_provider(Rc::new(CommentAllProvider), cx);
8635 });
8636 setup_cursors(&mut cx, &view.input, "|");
8637 cx.update(|window, cx| {
8638 view.input.update(cx, |state, cx| {
8639 state.replace_text_in_range(None, "(", window, cx);
8640 });
8641 });
8642 assert_cursors(&mut cx, &view.input, "(|");
8644 }
8645
8646 #[gpui::test]
8647 fn test_backspace_deletes_empty_pair(cx: &mut TestAppContext) {
8648 let view = InputView::<EditorMode>::new(cx);
8649 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8650 setup_cursors(&mut cx, &view.input, "{|}");
8651 cx.update(|window, cx| {
8652 view.input
8653 .update(cx, |state, cx| state.backspace(&Backspace, window, cx));
8654 });
8655 assert_cursors(&mut cx, &view.input, "|");
8656 }
8657
8658 #[gpui::test]
8659 fn test_backspace_keeps_closer_with_content(cx: &mut TestAppContext) {
8660 let view = InputView::<EditorMode>::new(cx);
8661 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8662 setup_cursors(&mut cx, &view.input, "{d|}");
8663 cx.update(|window, cx| {
8664 view.input
8665 .update(cx, |state, cx| state.backspace(&Backspace, window, cx));
8666 });
8667 assert_cursors(&mut cx, &view.input, "{|}");
8669 }
8670
8671 #[gpui::test]
8672 fn test_enter_splits_brackets(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, "{|}");
8676 cx.update(|window, cx| {
8677 view.input.update(cx, |state, cx| {
8678 state.enter(
8679 &Enter {
8680 secondary: false,
8681 shift: false,
8682 },
8683 window,
8684 cx,
8685 );
8686 });
8687 });
8688 assert_cursors(&mut cx, &view.input, "{\n |\n}");
8689 view.input.read_with(&cx, |state, _| {
8691 assert!(state.active_selection().is_empty());
8692 });
8693 cx.update(|window, cx| {
8694 view.input.update(cx, |state, cx| {
8695 state.replace_text_in_range(None, "x", window, cx);
8696 });
8697 });
8698 assert_cursors(&mut cx, &view.input, "{\n x|\n}");
8699 }
8700
8701 #[gpui::test]
8702 fn test_enter_smart_indent_after_brace(cx: &mut TestAppContext) {
8703 let view = InputView::<EditorMode>::new(cx);
8704 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8705 setup_cursors(&mut cx, &view.input, "fn main() {|");
8706 cx.update(|window, cx| {
8707 view.input.update(cx, |state, cx| {
8708 state.enter(
8709 &Enter {
8710 secondary: false,
8711 shift: false,
8712 },
8713 window,
8714 cx,
8715 );
8716 });
8717 });
8718 assert_cursors(&mut cx, &view.input, "fn main() {\n |");
8719 }
8720
8721 #[gpui::test]
8722 fn test_block_indent_tracks_all_preceding_edits(cx: &mut TestAppContext) {
8723 let view = multi_line(cx);
8724 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8725 setup_cursors(&mut cx, &view.input, "|ab\n|cd\n|ef");
8726 cx.update(|window, cx| {
8727 view.input
8728 .update(cx, |state, cx| state.indent(true, window, cx));
8729 });
8730 assert_cursors(&mut cx, &view.input, " |ab\n |cd\n |ef");
8731 cx.update(|window, cx| {
8732 view.input
8733 .update(cx, |state, cx| state.outdent(true, window, cx));
8734 });
8735 assert_cursors(&mut cx, &view.input, "|ab\n|cd\n|ef");
8736 }
8737
8738 #[gpui::test]
8739 fn test_block_outdent_clamps_cursor_inside_indent(cx: &mut TestAppContext) {
8740 let view = multi_line(cx);
8741 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8742 setup_cursors(&mut cx, &view.input, "ab\n | cd");
8743 cx.update(|window, cx| {
8744 view.input
8745 .update(cx, |state, cx| state.outdent(true, window, cx));
8746 });
8747 assert_cursors(&mut cx, &view.input, "ab\n|cd");
8748 }
8749
8750 #[gpui::test]
8751 fn test_ime_restores_original_selection(cx: &mut TestAppContext) {
8752 let view = InputView::build(cx, |state| state);
8753 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8754 cx.update(|window, cx| {
8755 view.input.update(cx, |state, cx| {
8756 state.set_value("abc", window, cx);
8757 state.set_selected_range(1..2, cx);
8758 state.replace_and_mark_text_in_range(None, "ni", None, window, cx);
8759 state.replace_text_in_range(None, "你", window, cx);
8760 state.undo(&Undo, window, cx);
8761 assert_eq!(state.value(), "abc");
8762 assert_eq!(state.selected_range(), 1..2);
8763 state.redo(&Redo, window, cx);
8764 assert_eq!(state.selected_range(), 4..4);
8765 });
8766 });
8767 }
8768
8769 #[gpui::test]
8770 fn test_noop_does_not_change_redo_selection(cx: &mut TestAppContext) {
8771 let view = multi_line(cx);
8772 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8773 cx.update(|window, cx| {
8774 view.input.update(cx, |state, cx| {
8775 state.set_value("abc", window, cx);
8776 state.set_selected_range(1..1, cx);
8777 state.replace_text_in_range(None, "X", window, cx);
8778 state.set_selected_range(0..0, cx);
8779 state.replace_text_in_range(None, "", window, cx);
8780 state.undo(&Undo, window, cx);
8781 state.redo(&Redo, window, cx);
8782 assert_eq!(state.value(), "aXbc");
8783 assert_eq!(state.selected_range(), 2..2);
8784 });
8785 });
8786 }
8787
8788 #[gpui::test]
8789 fn test_multi_cursor_insert_text(cx: &mut TestAppContext) {
8790 let view = multi_line(cx);
8791 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8792 let input = view.input;
8793
8794 setup_cursors(&mut cx, &input, "|hello |world|");
8795 cx.update(|window, cx| {
8796 input.update(cx, |state, cx| {
8797 state.replace_text_in_range(None, ">>>", window, cx);
8798 });
8799 });
8800 assert_cursors(&mut cx, &input, ">>>|hello >>>|world>>>|");
8801 }
8802
8803 #[gpui::test]
8804 fn test_multi_cursor_delete_backward(cx: &mut TestAppContext) {
8805 let view = multi_line(cx);
8806 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8807 let input = view.input;
8808
8809 setup_cursors(&mut cx, &input, "|islands| cars|");
8810 cx.update(|window, cx| {
8811 input.update(cx, |state, cx| {
8812 state.backspace(&Backspace, window, cx);
8813 });
8814 });
8815 assert_cursors(&mut cx, &input, "|island| car|");
8817 }
8818
8819 #[gpui::test]
8820 fn test_multi_cursor_delete_forward_merges(cx: &mut TestAppContext) {
8821 let view = multi_line(cx);
8822 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8823 let input = view.input;
8824
8825 setup_cursors(&mut cx, &input, "hello| |world");
8826 cx.update(|window, cx| {
8827 input.update(cx, |state, cx| {
8828 state.delete(&Delete, window, cx);
8829 });
8830 });
8831 assert_cursors(&mut cx, &input, "hello|orld");
8833 }
8834
8835 #[gpui::test]
8836 fn test_multi_cursor_multiline_insert_and_delete(cx: &mut TestAppContext) {
8837 let view = multi_line(cx);
8838 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8839 let input = view.input;
8840
8841 setup_cursors(&mut cx, &input, "|1\n|2\n|3");
8842 cx.update(|window, cx| {
8843 input.update(cx, |state, cx| {
8844 state.replace_text_in_range(None, "a", window, cx);
8845 });
8846 });
8847 assert_cursors(&mut cx, &input, "a|1\na|2\na|3");
8848
8849 input.read_with(&cx, |state, _| {
8851 assert_eq!(state.undo_manager.undo_count(), 1);
8852 });
8853
8854 cx.update(|window, cx| {
8855 input.update(cx, |state, cx| {
8856 state.backspace(&Backspace, window, cx);
8857 });
8858 });
8859 assert_cursors(&mut cx, &input, "|1\n|2\n|3");
8860 }
8861
8862 #[gpui::test]
8863 fn test_add_cursor_below_preserves_column(cx: &mut TestAppContext) {
8864 let view = multi_line(cx);
8865 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8866 let input = view.input;
8867
8868 setup_cursors(&mut cx, &input, "ab|cd\nabcd");
8869 cx.update(|window, cx| {
8870 input.update(cx, |state, cx| {
8871 state.add_cursor_below(&AddCursorBelow, window, cx);
8872 });
8873 });
8874 assert_cursors(&mut cx, &input, "ab|cd\nab|cd");
8875 }
8876
8877 #[gpui::test]
8878 fn test_add_cursor_at_rejects_duplicates(cx: &mut TestAppContext) {
8879 let view = multi_line(cx);
8880 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8881 let input = view.input;
8882
8883 setup_cursors(&mut cx, &input, "he|llo");
8884 cx.update(|_, cx| {
8885 input.update(cx, |state, cx| {
8886 state.add_cursor_at(2, cx);
8888 assert_eq!(state.selections.len(), 1);
8889 state.add_cursor_at(4, cx);
8891 assert_eq!(state.selections.len(), 2);
8892 });
8893 });
8894 }
8895
8896 #[gpui::test]
8897 fn test_multi_cursor_undo_redo_restores_selections(cx: &mut TestAppContext) {
8898 let view = multi_line(cx);
8899 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8900 let input = view.input;
8901
8902 setup_cursors(&mut cx, &input, "|1\n|2\n|3");
8903 cx.update(|window, cx| {
8904 input.update(cx, |state, cx| {
8905 state.replace_text_in_range(None, "a", window, cx);
8906 });
8907 });
8908 assert_cursors(&mut cx, &input, "a|1\na|2\na|3");
8909
8910 cx.update(|window, cx| {
8911 input.update(cx, |state, cx| {
8912 state.undo(&Undo, window, cx);
8913 });
8914 });
8915 assert_cursors(&mut cx, &input, "|1\n|2\n|3");
8916
8917 cx.update(|window, cx| {
8918 input.update(cx, |state, cx| {
8919 state.redo(&Redo, window, cx);
8920 });
8921 });
8922 assert_cursors(&mut cx, &input, "a|1\na|2\na|3");
8923 }
8924
8925 #[gpui::test]
8926 fn test_multi_cursor_undo_redo_different_line_lengths(cx: &mut TestAppContext) {
8927 let view = multi_line(cx);
8928 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8929 let input = view.input;
8930
8931 setup_cursors(&mut cx, &input, "abc123|\nabc12345|\nabc1234567|");
8932 cx.update(|window, cx| {
8933 input.update(cx, |state, cx| {
8934 state.replace_text_in_range(None, "a", window, cx);
8935 });
8936 });
8937 assert_cursors(&mut cx, &input, "abc123a|\nabc12345a|\nabc1234567a|");
8938
8939 cx.update(|window, cx| {
8940 input.update(cx, |state, cx| {
8941 state.undo(&Undo, window, cx);
8942 });
8943 });
8944 assert_cursors(&mut cx, &input, "abc123|\nabc12345|\nabc1234567|");
8945
8946 cx.update(|window, cx| {
8947 input.update(cx, |state, cx| {
8948 state.redo(&Redo, window, cx);
8949 });
8950 });
8951 assert_cursors(&mut cx, &input, "abc123a|\nabc12345a|\nabc1234567a|");
8952 }
8953
8954 #[gpui::test]
8955 fn test_multi_cursor_undo_multiple_inserts(cx: &mut TestAppContext) {
8956 let view = multi_line(cx);
8957 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8958 let input = view.input;
8959
8960 setup_cursors(&mut cx, &input, "|1\n|2\n|3");
8961 for ch in ['a', 'b', 'c'] {
8962 cx.update(|window, cx| {
8963 input.update(cx, |state, cx| {
8964 state.replace_text_in_range(None, &ch.to_string(), window, cx);
8965 });
8966 });
8967 }
8968 assert_cursors(&mut cx, &input, "abc|1\nabc|2\nabc|3");
8969
8970 cx.update(|window, cx| {
8972 input.update(cx, |state, cx| {
8973 assert_eq!(state.undo_manager.undo_count(), 1);
8974 state.undo(&Undo, window, cx);
8975 });
8976 });
8977 assert_cursors(&mut cx, &input, "|1\n|2\n|3");
8978
8979 cx.update(|window, cx| {
8980 input.update(cx, |state, cx| {
8981 state.redo(&Redo, window, cx);
8982 });
8983 });
8984 assert_cursors(&mut cx, &input, "abc|1\nabc|2\nabc|3");
8985 }
8986
8987 #[gpui::test]
8988 fn test_multi_cursor_backspace_run_is_one_undo(cx: &mut TestAppContext) {
8989 let view = multi_line(cx);
8990 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
8991 let input = view.input;
8992
8993 setup_cursors(&mut cx, &input, "abc|1\nabc|2\nabc|3");
8994 for _ in 0..3 {
8995 cx.update(|window, cx| {
8996 input.update(cx, |state, cx| {
8997 state.backspace(&Backspace, window, cx);
8998 });
8999 });
9000 }
9001 assert_cursors(&mut cx, &input, "|1\n|2\n|3");
9002
9003 cx.update(|window, cx| {
9004 input.update(cx, |state, cx| {
9005 assert_eq!(state.undo_manager.undo_count(), 1);
9006 state.undo(&Undo, window, cx);
9007 });
9008 });
9009 assert_cursors(&mut cx, &input, "abc|1\nabc|2\nabc|3");
9010 }
9011
9012 #[gpui::test]
9013 fn test_adding_a_cursor_splits_the_typing_gesture(cx: &mut TestAppContext) {
9014 let view = multi_line(cx);
9015 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9016 let input = view.input;
9017
9018 setup_cursors(&mut cx, &input, "|1\n2\n3");
9019 cx.update(|window, cx| {
9020 input.update(cx, |state, cx| {
9021 state.replace_text_in_range(None, "a", window, cx);
9022 state.add_cursor_below(&AddCursorBelow, window, cx);
9023 state.replace_text_in_range(None, "b", window, cx);
9024 });
9025 });
9026 assert_cursors(&mut cx, &input, "ab|1\n2b|\n3");
9027
9028 cx.update(|window, cx| {
9030 input.update(cx, |state, cx| {
9031 state.undo(&Undo, window, cx);
9032 });
9033 });
9034 assert_cursors(&mut cx, &input, "a|1\n2|\n3");
9035 }
9036
9037 #[gpui::test]
9038 fn test_multi_cursor_indent_is_one_undo(cx: &mut TestAppContext) {
9039 let view = multi_line(cx);
9040 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9041 let input = view.input;
9042
9043 setup_cursors(&mut cx, &input, "|1\n|2\n|3");
9044 cx.update(|window, cx| {
9045 input.update(cx, |state, cx| {
9046 state.indent_inline(&IndentInline, window, cx);
9047 assert_eq!(state.undo_manager.undo_count(), 1);
9048 state.undo(&Undo, window, cx);
9049 });
9050 });
9051 assert_cursors(&mut cx, &input, "|1\n|2\n|3");
9052 }
9053
9054 #[gpui::test]
9055 fn test_multi_cursor_indent_then_outdent_roundtrips(cx: &mut TestAppContext) {
9056 let view = multi_line(cx);
9057 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9058 let input = view.input;
9059
9060 setup_cursors(&mut cx, &input, "|1\n|2\n|3");
9062 cx.update(|window, cx| {
9063 input.update(cx, |state, cx| {
9064 state.indent(false, window, cx);
9065 });
9066 });
9067 assert_cursors(&mut cx, &input, " |1\n |2\n |3");
9068
9069 cx.update(|window, cx| {
9070 input.update(cx, |state, cx| {
9071 state.outdent(false, window, cx);
9072 });
9073 });
9074 assert_cursors(&mut cx, &input, "|1\n|2\n|3");
9075 }
9076
9077 #[gpui::test]
9078 fn test_inline_outdent_only_removes_line_indentation(cx: &mut TestAppContext) {
9079 let view = multi_line(cx);
9080 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9081 let input = view.input;
9082
9083 setup_cursors(&mut cx, &input, "1|2\n1|2");
9086 cx.update(|window, cx| {
9087 input.update(cx, |state, cx| {
9088 state.indent(false, window, cx);
9089 });
9090 });
9091 assert_cursors(&mut cx, &input, "1 |2\n1 |2");
9092
9093 cx.update(|window, cx| {
9094 input.update(cx, |state, cx| {
9095 state.outdent(false, window, cx);
9096 });
9097 });
9098 assert_cursors(&mut cx, &input, "1 |2\n1 |2");
9099
9100 setup_cursors(&mut cx, &input, " 1|2\n 1|2");
9102 cx.update(|window, cx| {
9103 input.update(cx, |state, cx| {
9104 state.outdent(false, window, cx);
9105 });
9106 });
9107 assert_cursors(&mut cx, &input, "1|2\n1|2");
9108 }
9109
9110 #[gpui::test]
9111 fn test_readonly_multi_cursor_commands_leave_state_unchanged(cx: &mut TestAppContext) {
9112 let view = multi_line(cx);
9113 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9114 let input = view.input;
9115
9116 setup_cursors(&mut cx, &input, "a|b\nc|d");
9117 cx.update(|window, cx| {
9118 input.update(cx, |state, cx| {
9119 let before: Vec<_> = state.selections.iter().copied().collect();
9120 state.set_readonly(true, cx);
9121 state.backspace(&Backspace, window, cx);
9122 state.indent_inline(&IndentInline, window, cx);
9123
9124 assert_eq!(state.value(), "ab\ncd\n");
9125 assert_eq!(state.selections.iter().copied().collect::<Vec<_>>(), before);
9126 });
9127 });
9128 }
9129
9130 #[gpui::test]
9131 fn test_multi_cursor_edit_preserves_the_active_cursor(cx: &mut TestAppContext) {
9132 let view = multi_line(cx);
9133 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9134 let input = view.input;
9135
9136 setup_cursors(&mut cx, &input, "x|\n|y");
9137 cx.update(|window, cx| {
9138 input.update(cx, |state, cx| {
9139 let mut selections: Vec<_> = state.selections.iter().copied().collect();
9140 selections.swap(0, 1);
9141 state.selections.replace_all(selections);
9142 let active_id = state.active_selection().id;
9143 state.replace_text_in_range(None, "!", window, cx);
9144 assert_eq!(state.active_selection().id, active_id);
9145 });
9146 });
9147 }
9148
9149 #[gpui::test]
9150 fn test_block_indent_outdent_with_selection(cx: &mut TestAppContext) {
9151 let view = multi_line(cx);
9152 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9153 let input = view.input;
9154
9155 cx.update(|window, cx| {
9156 input.update(cx, |state, cx| {
9157 state.set_value("line1\nline2\nline3", window, cx);
9158 let id = state.selections.generate_id();
9159 state
9160 .selections
9161 .replace_all(vec![CursorSelection::new(id, 0, 17)]);
9162 cx.notify();
9163 });
9164 });
9165
9166 cx.update(|window, cx| {
9167 input.update(cx, |state, cx| {
9168 state.indent(true, window, cx);
9169 });
9170 });
9171 input.read_with(&cx, |state, _| {
9172 assert_eq!(state.text.to_string(), " line1\n line2\n line3");
9173 });
9174
9175 cx.update(|window, cx| {
9176 input.update(cx, |state, cx| {
9177 state.outdent(true, window, cx);
9178 });
9179 });
9180 input.read_with(&cx, |state, _| {
9181 assert_eq!(state.text.to_string(), "line1\nline2\nline3");
9182 });
9183 }
9184
9185 #[gpui::test]
9186 fn test_multi_cursor_word_movement(cx: &mut TestAppContext) {
9187 let view = multi_line(cx);
9188 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9189 let input = view.input;
9190
9191 setup_cursors(
9192 &mut cx,
9193 &input,
9194 "on|e two three\none t|wo three\non|e two three",
9195 );
9196
9197 cx.update(|window, cx| {
9198 input.update(cx, |state, cx| {
9199 state.move_to_next_word(&MoveToNextWord, window, cx);
9200 });
9201 });
9202 assert_cursors(
9203 &mut cx,
9204 &input,
9205 "one| two three\none two| three\none| two three",
9206 );
9207
9208 cx.update(|window, cx| {
9209 input.update(cx, |state, cx| {
9210 state.move_to_previous_word(&MoveToPreviousWord, window, cx);
9211 });
9212 });
9213 assert_cursors(
9214 &mut cx,
9215 &input,
9216 "|one two three\none |two three\n|one two three",
9217 );
9218
9219 cx.update(|window, cx| {
9221 input.update(cx, |state, cx| {
9222 state.move_to_end(&MoveToEnd, window, cx);
9223 });
9224 });
9225 input.read_with(&cx, |state, _| {
9226 let cursors: Vec<usize> = state.selections.iter().map(|s| s.cursor_offset()).collect();
9227 assert_eq!(cursors, vec![state.text.len()]);
9228 });
9229
9230 cx.update(|window, cx| {
9231 input.update(cx, |state, cx| {
9232 state.move_to_start(&MoveToStart, window, cx);
9233 });
9234 });
9235 input.read_with(&cx, |state, _| {
9236 let cursors: Vec<usize> = state.selections.iter().map(|s| s.cursor_offset()).collect();
9237 assert_eq!(cursors, vec![0]);
9238 });
9239 }
9240
9241 #[gpui::test]
9242 fn test_multi_cursor_selection_commands(cx: &mut TestAppContext) {
9243 let view = multi_line(cx);
9244 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9245 let input = view.input;
9246
9247 setup_cursors(
9248 &mut cx,
9249 &input,
9250 "on|e two three\none t|wo three\non|e two three",
9251 );
9252 cx.update(|window, cx| {
9253 input.update(cx, |state, cx| {
9254 state.select_to_start_of_line(&SelectToStartOfLine, window, cx);
9255 });
9256 });
9257 assert_cursors(
9258 &mut cx,
9259 &input,
9260 "|one two three\n|one two three\n|one two three",
9261 );
9262
9263 setup_cursors(
9265 &mut cx,
9266 &input,
9267 "on|e two three\none t|wo three\non|e two three",
9268 );
9269 cx.update(|window, cx| {
9270 input.update(cx, |state, cx| {
9271 state.select_to_start(&SelectToStart, window, cx);
9272 });
9273 });
9274 input.read_with(&cx, |state, _| {
9275 let cursors: Vec<usize> = state.selections.iter().map(|s| s.cursor_offset()).collect();
9276 assert_eq!(cursors, vec![0]);
9277 });
9278 }
9279
9280 #[gpui::test]
9281 fn test_multi_cursor_replace_selection(cx: &mut TestAppContext) {
9282 let view = multi_line(cx);
9283 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9284 let input = view.input;
9285
9286 setup_cursors(&mut cx, &input, "|a\n|b\n|c");
9287 cx.update(|window, cx| {
9288 input.update(cx, |state, cx| {
9289 state.select_right(&SelectRight, window, cx);
9290 });
9291 });
9292 input.read_with(&cx, |state, _| {
9293 let ranges: Vec<_> = state.selections.iter().map(|s| (s.start, s.end)).collect();
9294 assert_eq!(ranges, vec![(0, 1), (2, 3), (4, 5)]);
9295 });
9296
9297 cx.update(|window, cx| {
9298 input.update(cx, |state, cx| {
9299 state.replace_text_in_range(None, "x", window, cx);
9300 });
9301 });
9302 assert_cursors(&mut cx, &input, "x|\nx|\nx|");
9303 }
9304
9305 #[gpui::test]
9306 fn test_multi_cursor_escape_collapses(cx: &mut TestAppContext) {
9307 let view = multi_line(cx);
9308 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9309 let input = view.input;
9310
9311 setup_cursors(&mut cx, &input, "|a\n|b\n|c");
9312 cx.update(|window, cx| {
9313 input.update(cx, |state, cx| {
9314 state.escape(&Escape, window, cx);
9315 });
9316 });
9317 input.read_with(&cx, |state, _| {
9318 assert_eq!(state.selections.len(), 1);
9319 });
9320 }
9321
9322 #[gpui::test]
9323 fn test_build_columnar_selection(cx: &mut TestAppContext) {
9324 let view = multi_line(cx);
9325 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9326 let input = view.input;
9327
9328 setup_cursors(&mut cx, &input, "abcd\nabcd\nabcd");
9329 cx.update(|_, cx| {
9330 input.update(cx, |state, cx| {
9331 state.build_columnar_selection(
9333 ColumnarPoint::new(1, 0),
9334 ColumnarPoint::new(13, 0),
9335 cx,
9336 );
9337 let ranges: Vec<_> = state.selections.iter().map(|s| (s.start, s.end)).collect();
9338 assert_eq!(ranges, vec![(1, 3), (6, 8), (11, 13)]);
9339 });
9340 });
9341 }
9342
9343 #[gpui::test]
9344 fn test_columnar_selection_keeps_width_over_short_rows(cx: &mut TestAppContext) {
9345 let view = multi_line(cx);
9346 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9347 let input = view.input;
9348
9349 setup_cursors(&mut cx, &input, "abcdef\nab\nabcdef");
9350 cx.update(|_, cx| {
9351 input.update(cx, |state, cx| {
9352 state.build_columnar_selection(
9355 ColumnarPoint::new(1, 0),
9356 ColumnarPoint::new(9, 3),
9357 cx,
9358 );
9359 let ranges: Vec<_> = state.selections.iter().map(|s| (s.start, s.end)).collect();
9360 assert_eq!(ranges, vec![(1, 5), (8, 9)]);
9361 });
9362 });
9363 }
9364
9365 #[gpui::test]
9366 fn test_multi_cursor_paste_distributes_lines(cx: &mut TestAppContext) {
9367 let view = multi_line(cx);
9368 let mut cx = VisualTestContext::from_window(view.window_handle.into(), cx);
9369 let input = view.input;
9370
9371 setup_cursors(&mut cx, &input, "|1\n|2\n|3");
9372 cx.update(|_, cx| {
9373 cx.write_to_clipboard(ClipboardItem::new_string("x\ny\nz".to_string()));
9374 });
9375 cx.update(|window, cx| {
9376 input.update(cx, |state, cx| {
9377 state.paste(&Paste, window, cx);
9378 });
9379 });
9380 assert_cursors(&mut cx, &input, "x|1\ny|2\nz|3");
9382 }
9383}
9384
9385impl InputBaseState<crate::input::InputMode> {
9387 pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
9389 Self::new_in_mode(window, cx)
9390 }
9391
9392 pub fn step_by(mut self, f: impl Fn(f64, StepAction, &mut App) -> f64 + 'static) -> Self {
9407 self.number_step = Some(NumberStep::by_value(f));
9408 self
9409 }
9410
9411 pub fn masked(mut self, masked: bool) -> Self {
9413 self.masked = masked;
9414 self
9415 }
9416
9417 pub fn set_masked(&mut self, masked: bool, _: &mut Window, cx: &mut Context<Self>) {
9419 self.masked = masked;
9420 cx.notify();
9421 }
9422
9423 pub fn pattern(mut self, pattern: regex::Regex) -> Self {
9425 self.pattern = Some(pattern);
9426 self
9427 }
9428
9429 pub fn set_pattern(
9431 &mut self,
9432 pattern: regex::Regex,
9433 _window: &mut Window,
9434 _cx: &mut Context<Self>,
9435 ) {
9436 self.pattern = Some(pattern);
9437 }
9438
9439 pub fn validate(mut self, f: impl Fn(&str, &mut App) -> bool + 'static) -> Self {
9441 self.validate = Some(Box::new(f));
9442 self
9443 }
9444
9445 pub fn set_validator(
9446 &mut self,
9447 validate: impl Fn(&str, &mut App) -> bool + 'static,
9448 _cx: &mut Context<Self>,
9449 ) {
9450 self.validate = Some(Box::new(validate));
9451 }
9452
9453 pub fn step(mut self, step: impl Into<NumberStep>) -> Self {
9463 self.number_step = Some(step.into());
9464 self
9465 }
9466
9467 pub fn min(mut self, min: f64) -> Self {
9473 self.number_min = Some(min);
9474 self
9475 }
9476
9477 pub fn max(mut self, max: f64) -> Self {
9483 self.number_max = Some(max);
9484 self
9485 }
9486
9487 pub fn set_step(
9492 &mut self,
9493 step: impl Into<Option<NumberStep>>,
9494 _: &mut Window,
9495 _: &mut Context<Self>,
9496 ) {
9497 self.number_step = step.into();
9498 }
9499
9500 pub fn set_min(&mut self, min: Option<f64>, _: &mut Window, _: &mut Context<Self>) {
9502 self.number_min = min;
9503 }
9504
9505 pub fn set_max(&mut self, max: Option<f64>, _: &mut Window, _: &mut Context<Self>) {
9507 self.number_max = max;
9508 }
9509
9510 pub fn set_loading(&mut self, loading: bool, _: &mut Window, cx: &mut Context<Self>) {
9512 self.loading = loading;
9513 cx.notify();
9514 }
9515}
9516
9517impl<M: crate::input::MultiLineMode> InputBaseState<M> {
9520 pub fn searchable(mut self, searchable: bool) -> Self {
9528 self.searchable = searchable;
9529 self
9530 }
9531
9532 pub fn set_searchable(&mut self, searchable: bool, cx: &mut Context<Self>) {
9534 self.searchable = searchable;
9535 cx.notify();
9536 }
9537
9538 #[doc(hidden)]
9540 pub fn soft_wrap(mut self, wrap: bool) -> Self {
9541 self.soft_wrap = wrap;
9542 self
9543 }
9544
9545 pub fn set_soft_wrap(&mut self, wrap: bool, _: &mut Window, cx: &mut Context<Self>) {
9547 self.soft_wrap = wrap;
9548 if wrap {
9549 let wrap_width = self
9550 .last_layout
9551 .as_ref()
9552 .and_then(|b| b.wrap_width)
9553 .unwrap_or(self.input_bounds.size.width);
9554
9555 self.display_map.on_layout_changed(Some(wrap_width), cx);
9556
9557 let mut offset = self.scroll_handle.offset();
9559 offset.x = px(0.);
9560 self.scroll_handle.set_offset(offset);
9561 } else {
9562 self.display_map.on_layout_changed(None, cx);
9563 }
9564 cx.notify();
9565 }
9566
9567 #[doc(hidden)]
9569 pub fn wrapping_indent(mut self, wrapping_indent: WrappingIndent) -> Self {
9570 self.wrapping_indent = wrapping_indent;
9571 self
9572 }
9573
9574 pub fn set_wrapping_indent(
9576 &mut self,
9577 wrapping_indent: WrappingIndent,
9578 _: &mut Window,
9579 cx: &mut Context<Self>,
9580 ) {
9581 self.wrapping_indent = wrapping_indent;
9582 self.display_map.set_wrapping_indent(wrapping_indent, cx);
9583 cx.notify();
9584 }
9585}
9586
9587impl InputBaseState<crate::input::TextareaMode> {
9589 pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
9594 Self::new_in_mode(window, cx)
9595 }
9596
9597 pub fn set_auto_grow(&mut self, min_rows: usize, max_rows: usize, cx: &mut Context<Self>) {
9598 self.mode = LayoutMode::auto_grow(min_rows, max_rows.max(min_rows));
9599 cx.notify();
9600 }
9601
9602 #[doc(hidden)]
9608 pub fn rows(mut self, rows: usize) -> Self {
9609 match &mut self.mode {
9610 LayoutMode::PlainText { rows: r, .. } | LayoutMode::CodeEditor { rows: r, .. } => {
9611 *r = rows
9612 }
9613 LayoutMode::AutoGrow {
9614 max_rows: max_r,
9615 rows: r,
9616 ..
9617 } => {
9618 *r = rows;
9619 *max_r = rows;
9620 }
9621 }
9622 self
9623 }
9624
9625 pub fn set_rows(&mut self, rows: usize, cx: &mut Context<Self>) {
9626 match &mut self.mode {
9627 LayoutMode::PlainText { rows: value, .. }
9628 | LayoutMode::CodeEditor { rows: value, .. } => *value = rows,
9629 LayoutMode::AutoGrow {
9630 rows: value,
9631 max_rows,
9632 ..
9633 } => {
9634 *value = rows;
9635 *max_rows = rows;
9636 }
9637 }
9638 cx.notify();
9639 }
9640
9641 pub fn auto_grow(mut self, min_rows: usize, max_rows: usize) -> Self {
9643 self.mode = LayoutMode::auto_grow(min_rows, max_rows);
9644 self
9645 }
9646}
9647
9648impl InputBaseState<crate::input::EditorMode> {
9650 pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
9661 let mut state = Self::new_in_mode(window, cx);
9662 state.mode = LayoutMode::code_editor(super::EditorLanguage::new(cx));
9663 state.searchable = true;
9664 state
9665 }
9666
9667 pub fn language(mut self, language: impl Into<SharedString>) -> Self {
9671 if let LayoutMode::CodeEditor {
9672 language: l,
9673 highlighter,
9674 ..
9675 } = &mut self.mode
9676 {
9677 l.set_name(language.into());
9678 *highlighter.borrow_mut() = None;
9679 }
9680 self
9681 }
9682
9683 pub fn language_name(&self) -> SharedString {
9685 match &self.mode {
9686 LayoutMode::CodeEditor { language, .. } => language.name(),
9687 _ => SharedString::default(),
9688 }
9689 }
9690
9691 #[doc(hidden)]
9695 pub fn folding(mut self, folding: bool) -> Self {
9696 if let LayoutMode::CodeEditor { folding: f, .. } = &mut self.mode {
9697 *f = folding;
9698 }
9699 self
9700 }
9701
9702 pub fn set_folding(&mut self, folding: bool, _: &mut Window, cx: &mut Context<Self>) {
9706 if let LayoutMode::CodeEditor { folding: f, .. } = &mut self.mode {
9707 *f = folding;
9708 }
9709 if !folding {
9710 self.display_map.clear_folds();
9711 }
9712 cx.notify();
9713 }
9714
9715 pub fn unfold_at(&mut self, position: impl Into<Position>, cx: &mut Context<Self>) -> bool {
9728 let offset = self.text.position_to_offset(&position.into());
9729 let line = self.text.offset_to_point(offset).row;
9730 let covering: Vec<usize> = self
9733 .display_map
9734 .folded_ranges()
9735 .iter()
9736 .filter(|fold| line > fold.start_line && line < fold.end_line)
9737 .map(|fold| fold.start_line)
9738 .collect();
9739 if covering.is_empty() {
9740 return false;
9741 }
9742
9743 for start_line in covering {
9744 self.display_map.set_folded(start_line, false);
9745 }
9746 cx.notify();
9747 true
9748 }
9749
9750 #[doc(hidden)]
9752 pub fn line_number(mut self, line_number: bool) -> Self {
9753 if let LayoutMode::CodeEditor { line_number: l, .. } = &mut self.mode {
9754 *l = line_number;
9755 }
9756 self
9757 }
9758
9759 pub fn set_line_number(&mut self, line_number: bool, _: &mut Window, cx: &mut Context<Self>) {
9761 if let LayoutMode::CodeEditor { line_number: l, .. } = &mut self.mode {
9762 *l = line_number;
9763 }
9764 cx.notify();
9765 }
9766
9767 #[doc(hidden)]
9773 pub fn auto_close(mut self, auto_close: bool) -> Self {
9774 self.mode.set_auto_close(auto_close);
9775 self
9776 }
9777
9778 pub fn set_auto_close(&mut self, auto_close: bool, _: &mut Window, cx: &mut Context<Self>) {
9780 self.mode.set_auto_close(auto_close);
9781 cx.notify();
9782 }
9783
9784 #[doc(hidden)]
9790 pub fn smart_indent(mut self, smart_indent: bool) -> Self {
9791 self.mode.set_smart_indent(smart_indent);
9792 self
9793 }
9794
9795 pub fn set_smart_indent(&mut self, smart_indent: bool, _: &mut Window, cx: &mut Context<Self>) {
9797 self.mode.set_smart_indent(smart_indent);
9798 cx.notify();
9799 }
9800}