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