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