1//! Vim support for Zed.
2
3#[cfg(test)]
4mod test;
5
6mod change_list;
7mod command;
8mod editor_events;
9mod insert;
10mod mode_indicator;
11mod motion;
12mod normal;
13mod object;
14mod replace;
15mod state;
16mod surrounds;
17mod visual;
18
19use anyhow::Result;
20use change_list::push_to_change_list;
21use collections::HashMap;
22use command_palette_hooks::{CommandPaletteFilter, CommandPaletteInterceptor};
23use editor::{
24 movement::{self, FindRange},
25 Anchor, Bias, Editor, EditorEvent, EditorMode, ToPoint,
26};
27use gpui::{
28 actions, impl_actions, Action, AppContext, EntityId, FocusableView, Global, KeystrokeEvent,
29 Subscription, UpdateGlobal, View, ViewContext, WeakView, WindowContext,
30};
31use language::{CursorShape, Point, SelectionGoal, TransactionId};
32pub use mode_indicator::ModeIndicator;
33use motion::Motion;
34use normal::{
35 mark::create_visual_marks,
36 normal_replace,
37 repeat::{observe_action, observe_insertion, record_register, replay_register},
38};
39use replace::multi_replace;
40use schemars::JsonSchema;
41use serde::Deserialize;
42use serde_derive::Serialize;
43use settings::{update_settings_file, Settings, SettingsSources, SettingsStore};
44use state::{EditorState, Mode, Operator, RecordedSelection, Register, WorkspaceState};
45use std::{ops::Range, sync::Arc};
46use surrounds::{add_surrounds, change_surrounds, delete_surrounds, SurroundsType};
47use ui::BorrowAppContext;
48use visual::{visual_block_motion, visual_replace};
49use workspace::{self, Workspace};
50
51use crate::state::ReplayableAction;
52
53/// Whether or not to enable Vim mode (work in progress).
54///
55/// Default: false
56pub struct VimModeSetting(pub bool);
57
58/// An Action to Switch between modes
59#[derive(Clone, Deserialize, PartialEq)]
60pub struct SwitchMode(pub Mode);
61
62/// PushOperator is used to put vim into a "minor" mode,
63/// where it's waiting for a specific next set of keystrokes.
64/// For example 'd' needs a motion to complete.
65#[derive(Clone, Deserialize, PartialEq)]
66pub struct PushOperator(pub Operator);
67
68/// Number is used to manage vim's count. Pushing a digit
69/// multiplis the current value by 10 and adds the digit.
70#[derive(Clone, Deserialize, PartialEq)]
71struct Number(usize);
72
73#[derive(Clone, Deserialize, PartialEq)]
74struct SelectRegister(String);
75
76actions!(
77 vim,
78 [
79 Tab,
80 Enter,
81 Object,
82 InnerObject,
83 FindForward,
84 FindBackward,
85 OpenDefaultKeymap
86 ]
87);
88
89// in the workspace namespace so it's not filtered out when vim is disabled.
90actions!(workspace, [ToggleVimMode]);
91
92impl_actions!(vim, [SwitchMode, PushOperator, Number, SelectRegister]);
93
94/// Initializes the `vim` crate.
95pub fn init(cx: &mut AppContext) {
96 cx.set_global(Vim::default());
97 VimModeSetting::register(cx);
98 VimSettings::register(cx);
99
100 cx.observe_keystrokes(observe_keystrokes).detach();
101 editor_events::init(cx);
102
103 cx.observe_new_views(|workspace: &mut Workspace, cx| register(workspace, cx))
104 .detach();
105
106 // Any time settings change, update vim mode to match. The Vim struct
107 // will be initialized as disabled by default, so we filter its commands
108 // out when starting up.
109 CommandPaletteFilter::update_global(cx, |filter, _| {
110 filter.hide_namespace(Vim::NAMESPACE);
111 });
112 Vim::update_global(cx, |vim, cx| {
113 vim.set_enabled(VimModeSetting::get_global(cx).0, cx)
114 });
115 cx.observe_global::<SettingsStore>(|cx| {
116 Vim::update_global(cx, |vim, cx| {
117 vim.set_enabled(VimModeSetting::get_global(cx).0, cx)
118 });
119 })
120 .detach();
121}
122
123fn register(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
124 workspace.register_action(|_: &mut Workspace, &SwitchMode(mode): &SwitchMode, cx| {
125 Vim::update(cx, |vim, cx| vim.switch_mode(mode, false, cx))
126 });
127 workspace.register_action(
128 |_: &mut Workspace, PushOperator(operator): &PushOperator, cx| {
129 Vim::update(cx, |vim, cx| vim.push_operator(operator.clone(), cx))
130 },
131 );
132 workspace.register_action(|_: &mut Workspace, n: &Number, cx: _| {
133 Vim::update(cx, |vim, cx| vim.push_count_digit(n.0, cx));
134 });
135 workspace.register_action(|_: &mut Workspace, _: &Tab, cx| {
136 Vim::active_editor_input_ignored(" ".into(), cx)
137 });
138
139 workspace.register_action(|_: &mut Workspace, _: &Enter, cx| {
140 Vim::active_editor_input_ignored("\n".into(), cx)
141 });
142
143 workspace.register_action(|workspace: &mut Workspace, _: &ToggleVimMode, cx| {
144 let fs = workspace.app_state().fs.clone();
145 let currently_enabled = VimModeSetting::get_global(cx).0;
146 update_settings_file::<VimModeSetting>(fs, cx, move |setting| {
147 *setting = Some(!currently_enabled)
148 })
149 });
150
151 workspace.register_action(|_: &mut Workspace, _: &OpenDefaultKeymap, cx| {
152 cx.emit(workspace::Event::OpenBundledFile {
153 text: settings::vim_keymap(),
154 title: "Default Vim Bindings",
155 language: "JSON",
156 });
157 });
158
159 normal::register(workspace, cx);
160 insert::register(workspace, cx);
161 motion::register(workspace, cx);
162 command::register(workspace, cx);
163 replace::register(workspace, cx);
164 object::register(workspace, cx);
165 visual::register(workspace, cx);
166 change_list::register(workspace, cx);
167}
168
169/// Called whenever an keystroke is typed so vim can observe all actions
170/// and keystrokes accordingly.
171fn observe_keystrokes(keystroke_event: &KeystrokeEvent, cx: &mut WindowContext) {
172 if let Some(action) = keystroke_event
173 .action
174 .as_ref()
175 .map(|action| action.boxed_clone())
176 {
177 observe_action(action.boxed_clone(), cx);
178
179 // Keystroke is handled by the vim system, so continue forward
180 if action.name().starts_with("vim::") {
181 return;
182 }
183 } else if cx.has_pending_keystrokes() || keystroke_event.keystroke.is_ime_in_progress() {
184 return;
185 }
186
187 Vim::update(cx, |vim, cx| match vim.active_operator() {
188 Some(
189 Operator::FindForward { .. }
190 | Operator::FindBackward { .. }
191 | Operator::Replace
192 | Operator::AddSurrounds { .. }
193 | Operator::ChangeSurrounds { .. }
194 | Operator::DeleteSurrounds
195 | Operator::Mark
196 | Operator::Jump { .. }
197 | Operator::Register
198 | Operator::RecordRegister
199 | Operator::ReplayRegister,
200 ) => {}
201 Some(_) => {
202 vim.clear_operator(cx);
203 }
204 _ => {}
205 });
206}
207
208/// The state pertaining to Vim mode.
209#[derive(Default)]
210struct Vim {
211 active_editor: Option<WeakView<Editor>>,
212 editor_subscription: Option<Subscription>,
213 enabled: bool,
214 editor_states: HashMap<EntityId, EditorState>,
215 workspace_state: WorkspaceState,
216 default_state: EditorState,
217}
218
219impl Global for Vim {}
220
221impl Vim {
222 /// The namespace for Vim actions.
223 const NAMESPACE: &'static str = "vim";
224
225 fn read(cx: &mut AppContext) -> &Self {
226 cx.global::<Self>()
227 }
228
229 fn update<F, S>(cx: &mut WindowContext, update: F) -> S
230 where
231 F: FnOnce(&mut Self, &mut WindowContext) -> S,
232 {
233 cx.update_global(update)
234 }
235
236 fn activate_editor(&mut self, editor: View<Editor>, cx: &mut WindowContext) {
237 if !editor.read(cx).use_modal_editing() {
238 return;
239 }
240
241 self.active_editor = Some(editor.clone().downgrade());
242 self.editor_subscription = Some(cx.subscribe(&editor, |editor, event, cx| match event {
243 EditorEvent::SelectionsChanged { local: true } => {
244 if editor.read(cx).leader_peer_id().is_none() {
245 Vim::update(cx, |vim, cx| {
246 vim.local_selections_changed(editor, cx);
247 })
248 }
249 }
250 EditorEvent::InputIgnored { text } => {
251 Vim::active_editor_input_ignored(text.clone(), cx);
252 observe_insertion(text, None, cx)
253 }
254 EditorEvent::InputHandled {
255 text,
256 utf16_range_to_replace: range_to_replace,
257 } => observe_insertion(text, range_to_replace.clone(), cx),
258 EditorEvent::TransactionBegun { transaction_id } => Vim::update(cx, |vim, cx| {
259 vim.transaction_begun(*transaction_id, cx);
260 }),
261 EditorEvent::TransactionUndone { transaction_id } => Vim::update(cx, |vim, cx| {
262 vim.transaction_undone(transaction_id, cx);
263 }),
264 EditorEvent::Edited { .. } => {
265 Vim::update(cx, |vim, cx| vim.transaction_ended(editor, cx))
266 }
267 _ => {}
268 }));
269
270 let editor = editor.read(cx);
271 let editor_mode = editor.mode();
272 let newest_selection_empty = editor.selections.newest::<usize>(cx).is_empty();
273
274 if editor_mode == EditorMode::Full
275 && !newest_selection_empty
276 && self.state().mode == Mode::Normal
277 // When following someone, don't switch vim mode.
278 && editor.leader_peer_id().is_none()
279 {
280 self.switch_mode(Mode::Visual, true, cx);
281 }
282
283 self.sync_vim_settings(cx);
284 }
285
286 fn update_active_editor<S>(
287 &mut self,
288 cx: &mut WindowContext,
289 update: impl FnOnce(&mut Vim, &mut Editor, &mut ViewContext<Editor>) -> S,
290 ) -> Option<S> {
291 let editor = self.active_editor.clone()?.upgrade()?;
292 Some(editor.update(cx, |editor, cx| update(self, editor, cx)))
293 }
294
295 fn editor_selections(&mut self, cx: &mut WindowContext) -> Vec<Range<Anchor>> {
296 self.update_active_editor(cx, |_, editor, _| {
297 editor
298 .selections
299 .disjoint_anchors()
300 .iter()
301 .map(|selection| selection.tail()..selection.head())
302 .collect()
303 })
304 .unwrap_or_default()
305 }
306
307 /// When doing an action that modifies the buffer, we start recording so that `.`
308 /// will replay the action.
309 pub fn start_recording(&mut self, cx: &mut WindowContext) {
310 if !self.workspace_state.dot_replaying {
311 self.workspace_state.dot_recording = true;
312 self.workspace_state.recorded_actions = Default::default();
313 self.workspace_state.recorded_count = None;
314
315 let selections = self
316 .active_editor
317 .as_ref()
318 .and_then(|editor| editor.upgrade())
319 .map(|editor| {
320 let editor = editor.read(cx);
321 (
322 editor.selections.oldest::<Point>(cx),
323 editor.selections.newest::<Point>(cx),
324 )
325 });
326
327 if let Some((oldest, newest)) = selections {
328 self.workspace_state.recorded_selection = match self.state().mode {
329 Mode::Visual if newest.end.row == newest.start.row => {
330 RecordedSelection::SingleLine {
331 cols: newest.end.column - newest.start.column,
332 }
333 }
334 Mode::Visual => RecordedSelection::Visual {
335 rows: newest.end.row - newest.start.row,
336 cols: newest.end.column,
337 },
338 Mode::VisualLine => RecordedSelection::VisualLine {
339 rows: newest.end.row - newest.start.row,
340 },
341 Mode::VisualBlock => RecordedSelection::VisualBlock {
342 rows: newest.end.row.abs_diff(oldest.start.row),
343 cols: newest.end.column.abs_diff(oldest.start.column),
344 },
345 _ => RecordedSelection::None,
346 }
347 } else {
348 self.workspace_state.recorded_selection = RecordedSelection::None;
349 }
350 }
351 }
352
353 pub fn stop_replaying(&mut self, _: &mut WindowContext) {
354 self.workspace_state.dot_replaying = false;
355 if let Some(replayer) = self.workspace_state.replayer.take() {
356 replayer.stop();
357 }
358 }
359
360 /// When finishing an action that modifies the buffer, stop recording.
361 /// as you usually call this within a keystroke handler we also ensure that
362 /// the current action is recorded.
363 pub fn stop_recording(&mut self) {
364 if self.workspace_state.dot_recording {
365 self.workspace_state.stop_recording_after_next_action = true;
366 }
367 }
368
369 /// Stops recording actions immediately rather than waiting until after the
370 /// next action to stop recording.
371 ///
372 /// This doesn't include the current action.
373 pub fn stop_recording_immediately(&mut self, action: Box<dyn Action>) {
374 if self.workspace_state.dot_recording {
375 self.workspace_state
376 .recorded_actions
377 .push(ReplayableAction::Action(action.boxed_clone()));
378 self.workspace_state.dot_recording = false;
379 self.workspace_state.stop_recording_after_next_action = false;
380 }
381 }
382
383 /// Explicitly record one action (equivalents to start_recording and stop_recording)
384 pub fn record_current_action(&mut self, cx: &mut WindowContext) {
385 self.start_recording(cx);
386 self.stop_recording();
387 }
388
389 fn switch_mode(&mut self, mode: Mode, leave_selections: bool, cx: &mut WindowContext) {
390 let state = self.state();
391 let last_mode = state.mode;
392 let prior_mode = state.last_mode;
393 let prior_tx = state.current_tx;
394 self.update_state(|state| {
395 state.last_mode = last_mode;
396 state.mode = mode;
397 state.operator_stack.clear();
398 if mode == Mode::Normal || mode != last_mode {
399 state.current_tx.take();
400 state.current_anchor.take();
401 }
402 });
403 if mode != Mode::Insert && mode != Mode::Replace {
404 self.take_count(cx);
405 }
406
407 // Sync editor settings like clip mode
408 self.sync_vim_settings(cx);
409
410 if !mode.is_visual() && last_mode.is_visual() {
411 create_visual_marks(self, last_mode, cx);
412 }
413
414 if leave_selections {
415 return;
416 }
417
418 // Adjust selections
419 self.update_active_editor(cx, |_, editor, cx| {
420 if last_mode != Mode::VisualBlock && last_mode.is_visual() && mode == Mode::VisualBlock
421 {
422 visual_block_motion(true, editor, cx, |_, point, goal| Some((point, goal)))
423 }
424 if last_mode == Mode::Insert || last_mode == Mode::Replace {
425 if let Some(prior_tx) = prior_tx {
426 editor.group_until_transaction(prior_tx, cx)
427 }
428 }
429
430 editor.change_selections(None, cx, |s| {
431 // we cheat with visual block mode and use multiple cursors.
432 // the cost of this cheat is we need to convert back to a single
433 // cursor whenever vim would.
434 if last_mode == Mode::VisualBlock
435 && (mode != Mode::VisualBlock && mode != Mode::Insert)
436 {
437 let tail = s.oldest_anchor().tail();
438 let head = s.newest_anchor().head();
439 s.select_anchor_ranges(vec![tail..head]);
440 } else if last_mode == Mode::Insert
441 && prior_mode == Mode::VisualBlock
442 && mode != Mode::VisualBlock
443 {
444 let pos = s.first_anchor().head();
445 s.select_anchor_ranges(vec![pos..pos])
446 }
447
448 let snapshot = s.display_map();
449 if let Some(pending) = s.pending.as_mut() {
450 if pending.selection.reversed && mode.is_visual() && !last_mode.is_visual() {
451 let mut end = pending.selection.end.to_point(&snapshot.buffer_snapshot);
452 end = snapshot
453 .buffer_snapshot
454 .clip_point(end + Point::new(0, 1), Bias::Right);
455 pending.selection.end = snapshot.buffer_snapshot.anchor_before(end);
456 }
457 }
458
459 s.move_with(|map, selection| {
460 if last_mode.is_visual() && !mode.is_visual() {
461 let mut point = selection.head();
462 if !selection.reversed && !selection.is_empty() {
463 point = movement::left(map, selection.head());
464 }
465 selection.collapse_to(point, selection.goal)
466 } else if !last_mode.is_visual() && mode.is_visual() {
467 if selection.is_empty() {
468 selection.end = movement::right(map, selection.start);
469 }
470 }
471 });
472 })
473 });
474 }
475
476 fn push_count_digit(&mut self, number: usize, cx: &mut WindowContext) {
477 if self.active_operator().is_some() {
478 self.update_state(|state| {
479 state.post_count = Some(state.post_count.unwrap_or(0) * 10 + number)
480 })
481 } else {
482 self.update_state(|state| {
483 state.pre_count = Some(state.pre_count.unwrap_or(0) * 10 + number)
484 })
485 }
486 // update the keymap so that 0 works
487 self.sync_vim_settings(cx)
488 }
489
490 fn take_count(&mut self, cx: &mut WindowContext) -> Option<usize> {
491 if self.workspace_state.dot_replaying {
492 return self.workspace_state.recorded_count;
493 }
494
495 let count = if self.state().post_count == None && self.state().pre_count == None {
496 return None;
497 } else {
498 Some(self.update_state(|state| {
499 state.post_count.take().unwrap_or(1) * state.pre_count.take().unwrap_or(1)
500 }))
501 };
502 if self.workspace_state.dot_recording {
503 self.workspace_state.recorded_count = count;
504 }
505 self.sync_vim_settings(cx);
506 count
507 }
508
509 fn select_register(&mut self, register: Arc<str>, cx: &mut WindowContext) {
510 self.update_state(|state| {
511 if register.chars().count() == 1 {
512 state
513 .selected_register
514 .replace(register.chars().next().unwrap());
515 }
516 state.operator_stack.clear();
517 });
518 self.sync_vim_settings(cx);
519 }
520
521 fn write_registers(
522 &mut self,
523 content: Register,
524 register: Option<char>,
525 is_yank: bool,
526 linewise: bool,
527 cx: &mut ViewContext<Editor>,
528 ) {
529 if let Some(register) = register {
530 let lower = register.to_lowercase().next().unwrap_or(register);
531 if lower != register {
532 let current = self.workspace_state.registers.entry(lower).or_default();
533 current.text = (current.text.to_string() + &content.text).into();
534 // not clear how to support appending to registers with multiple cursors
535 current.clipboard_selections.take();
536 let yanked = current.clone();
537 self.workspace_state.registers.insert('"', yanked);
538 } else {
539 self.workspace_state.registers.insert('"', content.clone());
540 match lower {
541 '_' | ':' | '.' | '%' | '#' | '=' | '/' => {}
542 '+' => {
543 cx.write_to_clipboard(content.into());
544 }
545 '*' => {
546 #[cfg(target_os = "linux")]
547 cx.write_to_primary(content.into());
548 #[cfg(not(target_os = "linux"))]
549 cx.write_to_clipboard(content.into());
550 }
551 '"' => {
552 self.workspace_state.registers.insert('0', content.clone());
553 self.workspace_state.registers.insert('"', content);
554 }
555 _ => {
556 self.workspace_state.registers.insert(lower, content);
557 }
558 }
559 }
560 } else {
561 let setting = VimSettings::get_global(cx).use_system_clipboard;
562 if setting == UseSystemClipboard::Always
563 || setting == UseSystemClipboard::OnYank && is_yank
564 {
565 self.workspace_state.last_yank.replace(content.text.clone());
566 cx.write_to_clipboard(content.clone().into());
567 } else {
568 self.workspace_state.last_yank = cx
569 .read_from_clipboard()
570 .map(|item| item.text().to_owned().into());
571 }
572
573 self.workspace_state.registers.insert('"', content.clone());
574 if is_yank {
575 self.workspace_state.registers.insert('0', content);
576 } else {
577 let contains_newline = content.text.contains('\n');
578 if !contains_newline {
579 self.workspace_state.registers.insert('-', content.clone());
580 }
581 if linewise || contains_newline {
582 let mut content = content;
583 for i in '1'..'8' {
584 if let Some(moved) = self.workspace_state.registers.insert(i, content) {
585 content = moved;
586 } else {
587 break;
588 }
589 }
590 }
591 }
592 }
593 }
594
595 fn read_register(
596 &mut self,
597 register: Option<char>,
598 editor: Option<&mut Editor>,
599 cx: &mut WindowContext,
600 ) -> Option<Register> {
601 let Some(register) = register.filter(|reg| *reg != '"') else {
602 let setting = VimSettings::get_global(cx).use_system_clipboard;
603 return match setting {
604 UseSystemClipboard::Always => cx.read_from_clipboard().map(|item| item.into()),
605 UseSystemClipboard::OnYank if self.system_clipboard_is_newer(cx) => {
606 cx.read_from_clipboard().map(|item| item.into())
607 }
608 _ => self.workspace_state.registers.get(&'"').cloned(),
609 };
610 };
611 let lower = register.to_lowercase().next().unwrap_or(register);
612 match lower {
613 '_' | ':' | '.' | '#' | '=' => None,
614 '+' => cx.read_from_clipboard().map(|item| item.into()),
615 '*' => {
616 #[cfg(target_os = "linux")]
617 {
618 cx.read_from_primary().map(|item| item.into())
619 }
620 #[cfg(not(target_os = "linux"))]
621 {
622 cx.read_from_clipboard().map(|item| item.into())
623 }
624 }
625 '%' => editor.and_then(|editor| {
626 let selection = editor.selections.newest::<Point>(cx);
627 if let Some((_, buffer, _)) = editor
628 .buffer()
629 .read(cx)
630 .excerpt_containing(selection.head(), cx)
631 {
632 buffer
633 .read(cx)
634 .file()
635 .map(|file| file.path().to_string_lossy().to_string().into())
636 } else {
637 None
638 }
639 }),
640 _ => self.workspace_state.registers.get(&lower).cloned(),
641 }
642 }
643
644 fn system_clipboard_is_newer(&self, cx: &mut AppContext) -> bool {
645 cx.read_from_clipboard().is_some_and(|item| {
646 if let Some(last_state) = &self.workspace_state.last_yank {
647 last_state != item.text()
648 } else {
649 true
650 }
651 })
652 }
653
654 fn push_operator(&mut self, operator: Operator, cx: &mut WindowContext) {
655 if matches!(
656 operator,
657 Operator::Change
658 | Operator::Delete
659 | Operator::Replace
660 | Operator::Indent
661 | Operator::Outdent
662 | Operator::Lowercase
663 | Operator::Uppercase
664 | Operator::OppositeCase
665 ) {
666 self.start_recording(cx)
667 };
668 // Since these operations can only be entered with pre-operators,
669 // we need to clear the previous operators when pushing,
670 // so that the current stack is the most correct
671 if matches!(
672 operator,
673 Operator::AddSurrounds { .. }
674 | Operator::ChangeSurrounds { .. }
675 | Operator::DeleteSurrounds
676 ) {
677 self.update_state(|state| state.operator_stack.clear());
678 };
679 self.update_state(|state| state.operator_stack.push(operator));
680 self.sync_vim_settings(cx);
681 }
682
683 fn maybe_pop_operator(&mut self) -> Option<Operator> {
684 self.update_state(|state| state.operator_stack.pop())
685 }
686
687 fn pop_operator(&mut self, cx: &mut WindowContext) -> Operator {
688 let popped_operator = self.update_state(|state| state.operator_stack.pop())
689 .expect("Operator popped when no operator was on the stack. This likely means there is an invalid keymap config");
690 self.sync_vim_settings(cx);
691 popped_operator
692 }
693
694 fn clear_operator(&mut self, cx: &mut WindowContext) {
695 self.take_count(cx);
696 self.update_state(|state| {
697 state.selected_register.take();
698 state.operator_stack.clear()
699 });
700 self.sync_vim_settings(cx);
701 }
702
703 fn active_operator(&self) -> Option<Operator> {
704 self.state().operator_stack.last().cloned()
705 }
706
707 fn transaction_begun(&mut self, transaction_id: TransactionId, _: &mut WindowContext) {
708 self.update_state(|state| {
709 let mode = if (state.mode == Mode::Insert
710 || state.mode == Mode::Replace
711 || state.mode == Mode::Normal)
712 && state.current_tx.is_none()
713 {
714 state.current_tx = Some(transaction_id);
715 state.last_mode
716 } else {
717 state.mode
718 };
719 if mode == Mode::VisualLine || mode == Mode::VisualBlock {
720 state.undo_modes.insert(transaction_id, mode);
721 }
722 });
723 }
724
725 fn transaction_undone(&mut self, transaction_id: &TransactionId, cx: &mut WindowContext) {
726 match self.state().mode {
727 Mode::VisualLine | Mode::VisualBlock | Mode::Visual => {
728 self.update_active_editor(cx, |vim, editor, cx| {
729 let original_mode = vim.state().undo_modes.get(transaction_id);
730 editor.change_selections(None, cx, |s| match original_mode {
731 Some(Mode::VisualLine) => {
732 s.move_with(|map, selection| {
733 selection.collapse_to(
734 map.prev_line_boundary(selection.start.to_point(map)).1,
735 SelectionGoal::None,
736 )
737 });
738 }
739 Some(Mode::VisualBlock) => {
740 let mut first = s.first_anchor();
741 first.collapse_to(first.start, first.goal);
742 s.select_anchors(vec![first]);
743 }
744 _ => {
745 s.move_with(|map, selection| {
746 selection.collapse_to(
747 map.clip_at_line_end(selection.start),
748 selection.goal,
749 );
750 });
751 }
752 });
753 });
754 self.switch_mode(Mode::Normal, true, cx)
755 }
756 Mode::Normal => {
757 self.update_active_editor(cx, |_, editor, cx| {
758 editor.change_selections(None, cx, |s| {
759 s.move_with(|map, selection| {
760 selection
761 .collapse_to(map.clip_at_line_end(selection.end), selection.goal)
762 })
763 })
764 });
765 }
766 Mode::Insert | Mode::Replace => {}
767 }
768 }
769
770 fn transaction_ended(&mut self, editor: View<Editor>, cx: &mut WindowContext) {
771 push_to_change_list(self, editor, cx)
772 }
773
774 fn local_selections_changed(&mut self, editor: View<Editor>, cx: &mut WindowContext) {
775 let newest = editor.read(cx).selections.newest_anchor().clone();
776 let is_multicursor = editor.read(cx).selections.count() > 1;
777
778 let state = self.state();
779 if state.mode == Mode::Insert && state.current_tx.is_some() {
780 if state.current_anchor.is_none() {
781 self.update_state(|state| state.current_anchor = Some(newest));
782 } else if state.current_anchor.as_ref().unwrap() != &newest {
783 if let Some(tx_id) = self.update_state(|state| state.current_tx.take()) {
784 self.update_active_editor(cx, |_, editor, cx| {
785 editor.group_until_transaction(tx_id, cx)
786 });
787 }
788 }
789 } else if state.mode == Mode::Normal && newest.start != newest.end {
790 if matches!(newest.goal, SelectionGoal::HorizontalRange { .. }) {
791 self.switch_mode(Mode::VisualBlock, false, cx);
792 } else {
793 self.switch_mode(Mode::Visual, false, cx)
794 }
795 } else if newest.start == newest.end
796 && !is_multicursor
797 && [Mode::Visual, Mode::VisualLine, Mode::VisualBlock].contains(&state.mode)
798 {
799 self.switch_mode(Mode::Normal, true, cx);
800 }
801 }
802
803 fn active_editor_input_ignored(text: Arc<str>, cx: &mut WindowContext) {
804 if text.is_empty() {
805 return;
806 }
807
808 match Vim::read(cx).active_operator() {
809 Some(Operator::FindForward { before }) => {
810 let find = Motion::FindForward {
811 before,
812 char: text.chars().next().unwrap(),
813 mode: if VimSettings::get_global(cx).use_multiline_find {
814 FindRange::MultiLine
815 } else {
816 FindRange::SingleLine
817 },
818 smartcase: VimSettings::get_global(cx).use_smartcase_find,
819 };
820 Vim::update(cx, |vim, _| {
821 vim.workspace_state.last_find = Some(find.clone())
822 });
823 motion::motion(find, cx)
824 }
825 Some(Operator::FindBackward { after }) => {
826 let find = Motion::FindBackward {
827 after,
828 char: text.chars().next().unwrap(),
829 mode: if VimSettings::get_global(cx).use_multiline_find {
830 FindRange::MultiLine
831 } else {
832 FindRange::SingleLine
833 },
834 smartcase: VimSettings::get_global(cx).use_smartcase_find,
835 };
836 Vim::update(cx, |vim, _| {
837 vim.workspace_state.last_find = Some(find.clone())
838 });
839 motion::motion(find, cx)
840 }
841 Some(Operator::Replace) => match Vim::read(cx).state().mode {
842 Mode::Normal => normal_replace(text, cx),
843 Mode::Visual | Mode::VisualLine | Mode::VisualBlock => visual_replace(text, cx),
844 _ => Vim::update(cx, |vim, cx| vim.clear_operator(cx)),
845 },
846 Some(Operator::AddSurrounds { target }) => match Vim::read(cx).state().mode {
847 Mode::Normal => {
848 if let Some(target) = target {
849 add_surrounds(text, target, cx);
850 Vim::update(cx, |vim, cx| vim.clear_operator(cx));
851 }
852 }
853 Mode::Visual | Mode::VisualLine | Mode::VisualBlock => {
854 add_surrounds(text, SurroundsType::Selection, cx);
855 Vim::update(cx, |vim, cx| vim.clear_operator(cx));
856 }
857 _ => Vim::update(cx, |vim, cx| vim.clear_operator(cx)),
858 },
859 Some(Operator::ChangeSurrounds { target }) => match Vim::read(cx).state().mode {
860 Mode::Normal => {
861 if let Some(target) = target {
862 change_surrounds(text, target, cx);
863 Vim::update(cx, |vim, cx| vim.clear_operator(cx));
864 }
865 }
866 _ => Vim::update(cx, |vim, cx| vim.clear_operator(cx)),
867 },
868 Some(Operator::DeleteSurrounds) => match Vim::read(cx).state().mode {
869 Mode::Normal => {
870 delete_surrounds(text, cx);
871 Vim::update(cx, |vim, cx| vim.clear_operator(cx));
872 }
873 _ => Vim::update(cx, |vim, cx| vim.clear_operator(cx)),
874 },
875 Some(Operator::Mark) => Vim::update(cx, |vim, cx| {
876 normal::mark::create_mark(vim, text, false, cx)
877 }),
878 Some(Operator::RecordRegister) => record_register(text.chars().next().unwrap(), cx),
879 Some(Operator::ReplayRegister) => replay_register(text.chars().next().unwrap(), cx),
880 Some(Operator::Register) => Vim::update(cx, |vim, cx| match vim.state().mode {
881 Mode::Insert => {
882 vim.update_active_editor(cx, |vim, editor, cx| {
883 if let Some(register) =
884 vim.read_register(text.chars().next(), Some(editor), cx)
885 {
886 editor.do_paste(
887 ®ister.text.to_string(),
888 register.clipboard_selections.clone(),
889 false,
890 cx,
891 )
892 }
893 });
894 vim.clear_operator(cx);
895 }
896 _ => {
897 vim.select_register(text, cx);
898 }
899 }),
900 Some(Operator::Jump { line }) => normal::mark::jump(text, line, cx),
901 _ => match Vim::read(cx).state().mode {
902 Mode::Replace => multi_replace(text, cx),
903 _ => {}
904 },
905 }
906 }
907
908 fn set_enabled(&mut self, enabled: bool, cx: &mut AppContext) {
909 if self.enabled == enabled {
910 return;
911 }
912 if !enabled {
913 CommandPaletteInterceptor::update_global(cx, |interceptor, _| {
914 interceptor.clear();
915 });
916 CommandPaletteFilter::update_global(cx, |filter, _| {
917 filter.hide_namespace(Self::NAMESPACE);
918 });
919 *self = Default::default();
920 return;
921 }
922
923 self.enabled = true;
924 CommandPaletteFilter::update_global(cx, |filter, _| {
925 filter.show_namespace(Self::NAMESPACE);
926 });
927 CommandPaletteInterceptor::update_global(cx, |interceptor, _| {
928 interceptor.set(Box::new(command::command_interceptor));
929 });
930
931 if let Some(active_window) = cx
932 .active_window()
933 .and_then(|window| window.downcast::<Workspace>())
934 {
935 active_window
936 .update(cx, |workspace, cx| {
937 let active_editor = workspace.active_item_as::<Editor>(cx);
938 if let Some(active_editor) = active_editor {
939 self.activate_editor(active_editor, cx);
940 self.switch_mode(Mode::Normal, false, cx);
941 }
942 })
943 .ok();
944 }
945 }
946
947 /// Returns the state of the active editor.
948 pub fn state(&self) -> &EditorState {
949 if let Some(active_editor) = self.active_editor.as_ref() {
950 if let Some(state) = self.editor_states.get(&active_editor.entity_id()) {
951 return state;
952 }
953 }
954
955 &self.default_state
956 }
957
958 /// Updates the state of the active editor.
959 pub fn update_state<T>(&mut self, func: impl FnOnce(&mut EditorState) -> T) -> T {
960 let mut state = self.state().clone();
961 let ret = func(&mut state);
962
963 if let Some(active_editor) = self.active_editor.as_ref() {
964 self.editor_states.insert(active_editor.entity_id(), state);
965 }
966
967 ret
968 }
969
970 fn sync_vim_settings(&mut self, cx: &mut WindowContext) {
971 self.update_active_editor(cx, |vim, editor, cx| {
972 let state = vim.state();
973 editor.set_cursor_shape(state.cursor_shape(), cx);
974 editor.set_clip_at_line_ends(state.clip_at_line_ends(), cx);
975 editor.set_collapse_matches(true);
976 editor.set_input_enabled(!state.vim_controlled());
977 editor.set_autoindent(state.should_autoindent());
978 editor.selections.line_mode = matches!(state.mode, Mode::VisualLine);
979 if editor.is_focused(cx) || editor.mouse_menu_is_focused(cx) {
980 editor.set_keymap_context_layer::<Self>(state.keymap_context_layer(), cx);
981 // disable vim mode if a sub-editor (inline assist, rename, etc.) is focused
982 } else if editor.focus_handle(cx).contains_focused(cx) {
983 editor.remove_keymap_context_layer::<Self>(cx);
984 }
985 });
986 }
987
988 fn unhook_vim_settings(editor: &mut Editor, cx: &mut ViewContext<Editor>) {
989 if editor.mode() == EditorMode::Full {
990 editor.set_cursor_shape(CursorShape::Bar, cx);
991 editor.set_clip_at_line_ends(false, cx);
992 editor.set_collapse_matches(false);
993 editor.set_input_enabled(true);
994 editor.set_autoindent(true);
995 editor.selections.line_mode = false;
996 }
997 editor.remove_keymap_context_layer::<Self>(cx)
998 }
999}
1000
1001impl Settings for VimModeSetting {
1002 const KEY: Option<&'static str> = Some("vim_mode");
1003
1004 type FileContent = Option<bool>;
1005
1006 fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
1007 Ok(Self(sources.user.copied().flatten().unwrap_or(
1008 sources.default.ok_or_else(Self::missing_default)?,
1009 )))
1010 }
1011}
1012
1013/// Controls when to use system clipboard.
1014#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
1015#[serde(rename_all = "snake_case")]
1016pub enum UseSystemClipboard {
1017 /// Don't use system clipboard.
1018 Never,
1019 /// Use system clipboard.
1020 Always,
1021 /// Use system clipboard for yank operations.
1022 OnYank,
1023}
1024
1025#[derive(Deserialize)]
1026struct VimSettings {
1027 // all vim uses vim clipboard
1028 // vim always uses system cliupbaord
1029 // some magic where yy is system and dd is not.
1030 pub use_system_clipboard: UseSystemClipboard,
1031 pub use_multiline_find: bool,
1032 pub use_smartcase_find: bool,
1033}
1034
1035#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
1036struct VimSettingsContent {
1037 pub use_system_clipboard: Option<UseSystemClipboard>,
1038 pub use_multiline_find: Option<bool>,
1039 pub use_smartcase_find: Option<bool>,
1040}
1041
1042impl Settings for VimSettings {
1043 const KEY: Option<&'static str> = Some("vim");
1044
1045 type FileContent = VimSettingsContent;
1046
1047 fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
1048 sources.json_merge()
1049 }
1050}