state.rs

  1use crate::command::command_interceptor;
  2use crate::normal::repeat::Replayer;
  3use crate::surrounds::SurroundsType;
  4use crate::{motion::Motion, object::Object};
  5use crate::{UseSystemClipboard, Vim, VimSettings};
  6use collections::HashMap;
  7use command_palette_hooks::{CommandPaletteFilter, CommandPaletteInterceptor};
  8use editor::{Anchor, ClipboardSelection, Editor};
  9use gpui::{
 10    Action, App, BorrowAppContext, ClipboardEntry, ClipboardItem, Entity, Global, WeakEntity,
 11};
 12use language::Point;
 13use serde::{Deserialize, Serialize};
 14use settings::{Settings, SettingsStore};
 15use std::borrow::BorrowMut;
 16use std::{fmt::Display, ops::Range, sync::Arc};
 17use ui::{Context, KeyBinding, SharedString};
 18use workspace::searchable::Direction;
 19
 20#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
 21pub enum Mode {
 22    Normal,
 23    Insert,
 24    Replace,
 25    Visual,
 26    VisualLine,
 27    VisualBlock,
 28    HelixNormal,
 29}
 30
 31impl Display for Mode {
 32    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 33        match self {
 34            Mode::Normal => write!(f, "NORMAL"),
 35            Mode::Insert => write!(f, "INSERT"),
 36            Mode::Replace => write!(f, "REPLACE"),
 37            Mode::Visual => write!(f, "VISUAL"),
 38            Mode::VisualLine => write!(f, "VISUAL LINE"),
 39            Mode::VisualBlock => write!(f, "VISUAL BLOCK"),
 40            Mode::HelixNormal => write!(f, "HELIX NORMAL"),
 41        }
 42    }
 43}
 44
 45impl Mode {
 46    pub fn is_visual(&self) -> bool {
 47        match self {
 48            Mode::Normal | Mode::Insert | Mode::Replace => false,
 49            Mode::Visual | Mode::VisualLine | Mode::VisualBlock => true,
 50            Mode::HelixNormal => false,
 51        }
 52    }
 53}
 54
 55impl Default for Mode {
 56    fn default() -> Self {
 57        Self::Normal
 58    }
 59}
 60
 61#[derive(Clone, Debug, PartialEq)]
 62pub enum Operator {
 63    Change,
 64    Delete,
 65    Yank,
 66    Replace,
 67    Object {
 68        around: bool,
 69    },
 70    FindForward {
 71        before: bool,
 72    },
 73    FindBackward {
 74        after: bool,
 75    },
 76    Sneak {
 77        first_char: Option<char>,
 78    },
 79    SneakBackward {
 80        first_char: Option<char>,
 81    },
 82    AddSurrounds {
 83        // Typically no need to configure this as `SendKeystrokes` can be used - see #23088.
 84        target: Option<SurroundsType>,
 85    },
 86    ChangeSurrounds {
 87        target: Option<Object>,
 88    },
 89    DeleteSurrounds,
 90    Mark,
 91    Jump {
 92        line: bool,
 93    },
 94    Indent,
 95    Outdent,
 96    AutoIndent,
 97    Rewrap,
 98    ShellCommand,
 99    Lowercase,
100    Uppercase,
101    OppositeCase,
102    Digraph {
103        first_char: Option<char>,
104    },
105    Literal {
106        prefix: Option<String>,
107    },
108    Register,
109    RecordRegister,
110    ReplayRegister,
111    ToggleComments,
112    ReplaceWithRegister,
113    Exchange,
114}
115
116#[derive(Default, Clone, Debug)]
117pub enum RecordedSelection {
118    #[default]
119    None,
120    Visual {
121        rows: u32,
122        cols: u32,
123    },
124    SingleLine {
125        cols: u32,
126    },
127    VisualBlock {
128        rows: u32,
129        cols: u32,
130    },
131    VisualLine {
132        rows: u32,
133    },
134}
135
136#[derive(Default, Clone, Debug)]
137pub struct Register {
138    pub(crate) text: SharedString,
139    pub(crate) clipboard_selections: Option<Vec<ClipboardSelection>>,
140}
141
142impl From<Register> for ClipboardItem {
143    fn from(register: Register) -> Self {
144        if let Some(clipboard_selections) = register.clipboard_selections {
145            ClipboardItem::new_string_with_json_metadata(register.text.into(), clipboard_selections)
146        } else {
147            ClipboardItem::new_string(register.text.into())
148        }
149    }
150}
151
152impl From<ClipboardItem> for Register {
153    fn from(item: ClipboardItem) -> Self {
154        // For now, we don't store metadata for multiple entries.
155        match item.entries().first() {
156            Some(ClipboardEntry::String(value)) if item.entries().len() == 1 => Register {
157                text: value.text().to_owned().into(),
158                clipboard_selections: value.metadata_json::<Vec<ClipboardSelection>>(),
159            },
160            // For now, registers can't store images. This could change in the future.
161            _ => Register::default(),
162        }
163    }
164}
165
166impl From<String> for Register {
167    fn from(text: String) -> Self {
168        Register {
169            text: text.into(),
170            clipboard_selections: None,
171        }
172    }
173}
174
175#[derive(Default, Clone)]
176pub struct VimGlobals {
177    pub last_find: Option<Motion>,
178
179    pub dot_recording: bool,
180    pub dot_replaying: bool,
181
182    /// pre_count is the number before an operator is specified (3 in 3d2d)
183    pub pre_count: Option<usize>,
184    /// post_count is the number after an operator is specified (2 in 3d2d)
185    pub post_count: Option<usize>,
186
187    pub stop_recording_after_next_action: bool,
188    pub ignore_current_insertion: bool,
189    pub recorded_count: Option<usize>,
190    pub recording_actions: Vec<ReplayableAction>,
191    pub recorded_actions: Vec<ReplayableAction>,
192    pub recorded_selection: RecordedSelection,
193
194    pub recording_register: Option<char>,
195    pub last_recorded_register: Option<char>,
196    pub last_replayed_register: Option<char>,
197    pub replayer: Option<Replayer>,
198
199    pub last_yank: Option<SharedString>,
200    pub registers: HashMap<char, Register>,
201    pub recordings: HashMap<char, Vec<ReplayableAction>>,
202
203    pub focused_vim: Option<WeakEntity<Vim>>,
204}
205impl Global for VimGlobals {}
206
207impl VimGlobals {
208    pub(crate) fn register(cx: &mut App) {
209        cx.set_global(VimGlobals::default());
210
211        cx.observe_keystrokes(|event, _, cx| {
212            let Some(action) = event.action.as_ref().map(|action| action.boxed_clone()) else {
213                return;
214            };
215            Vim::globals(cx).observe_action(action.boxed_clone())
216        })
217        .detach();
218
219        cx.observe_global::<SettingsStore>(move |cx| {
220            if Vim::enabled(cx) {
221                KeyBinding::set_vim_mode(cx, true);
222                CommandPaletteFilter::update_global(cx, |filter, _| {
223                    filter.show_namespace(Vim::NAMESPACE);
224                });
225                CommandPaletteInterceptor::update_global(cx, |interceptor, _| {
226                    interceptor.set(Box::new(command_interceptor));
227                });
228            } else {
229                KeyBinding::set_vim_mode(cx, false);
230                *Vim::globals(cx) = VimGlobals::default();
231                CommandPaletteInterceptor::update_global(cx, |interceptor, _| {
232                    interceptor.clear();
233                });
234                CommandPaletteFilter::update_global(cx, |filter, _| {
235                    filter.hide_namespace(Vim::NAMESPACE);
236                });
237            }
238        })
239        .detach();
240    }
241
242    pub(crate) fn write_registers(
243        &mut self,
244        content: Register,
245        register: Option<char>,
246        is_yank: bool,
247        linewise: bool,
248        cx: &mut Context<Editor>,
249    ) {
250        if let Some(register) = register {
251            let lower = register.to_lowercase().next().unwrap_or(register);
252            if lower != register {
253                let current = self.registers.entry(lower).or_default();
254                current.text = (current.text.to_string() + &content.text).into();
255                // not clear how to support appending to registers with multiple cursors
256                current.clipboard_selections.take();
257                let yanked = current.clone();
258                self.registers.insert('"', yanked);
259            } else {
260                match lower {
261                    '_' | ':' | '.' | '%' | '#' | '=' | '/' => {}
262                    '+' => {
263                        self.registers.insert('"', content.clone());
264                        cx.write_to_clipboard(content.into());
265                    }
266                    '*' => {
267                        self.registers.insert('"', content.clone());
268                        #[cfg(any(target_os = "linux", target_os = "freebsd"))]
269                        cx.write_to_primary(content.into());
270                        #[cfg(not(any(target_os = "linux", target_os = "freebsd")))]
271                        cx.write_to_clipboard(content.into());
272                    }
273                    '"' => {
274                        self.registers.insert('"', content.clone());
275                        self.registers.insert('0', content);
276                    }
277                    _ => {
278                        self.registers.insert('"', content.clone());
279                        self.registers.insert(lower, content);
280                    }
281                }
282            }
283        } else {
284            let setting = VimSettings::get_global(cx).use_system_clipboard;
285            if setting == UseSystemClipboard::Always
286                || setting == UseSystemClipboard::OnYank && is_yank
287            {
288                self.last_yank.replace(content.text.clone());
289                cx.write_to_clipboard(content.clone().into());
290            } else {
291                self.last_yank = cx
292                    .read_from_clipboard()
293                    .and_then(|item| item.text().map(|string| string.into()));
294            }
295
296            self.registers.insert('"', content.clone());
297            if is_yank {
298                self.registers.insert('0', content);
299            } else {
300                let contains_newline = content.text.contains('\n');
301                if !contains_newline {
302                    self.registers.insert('-', content.clone());
303                }
304                if linewise || contains_newline {
305                    let mut content = content;
306                    for i in '1'..'8' {
307                        if let Some(moved) = self.registers.insert(i, content) {
308                            content = moved;
309                        } else {
310                            break;
311                        }
312                    }
313                }
314            }
315        }
316    }
317
318    pub(crate) fn read_register(
319        &mut self,
320        register: Option<char>,
321        editor: Option<&mut Editor>,
322        cx: &mut Context<Editor>,
323    ) -> Option<Register> {
324        let Some(register) = register.filter(|reg| *reg != '"') else {
325            let setting = VimSettings::get_global(cx).use_system_clipboard;
326            return match setting {
327                UseSystemClipboard::Always => cx.read_from_clipboard().map(|item| item.into()),
328                UseSystemClipboard::OnYank if self.system_clipboard_is_newer(cx) => {
329                    cx.read_from_clipboard().map(|item| item.into())
330                }
331                _ => self.registers.get(&'"').cloned(),
332            };
333        };
334        let lower = register.to_lowercase().next().unwrap_or(register);
335        match lower {
336            '_' | ':' | '.' | '#' | '=' => None,
337            '+' => cx.read_from_clipboard().map(|item| item.into()),
338            '*' => {
339                #[cfg(any(target_os = "linux", target_os = "freebsd"))]
340                {
341                    cx.read_from_primary().map(|item| item.into())
342                }
343                #[cfg(not(any(target_os = "linux", target_os = "freebsd")))]
344                {
345                    cx.read_from_clipboard().map(|item| item.into())
346                }
347            }
348            '%' => editor.and_then(|editor| {
349                let selection = editor.selections.newest::<Point>(cx);
350                if let Some((_, buffer, _)) = editor
351                    .buffer()
352                    .read(cx)
353                    .excerpt_containing(selection.head(), cx)
354                {
355                    buffer
356                        .read(cx)
357                        .file()
358                        .map(|file| file.path().to_string_lossy().to_string().into())
359                } else {
360                    None
361                }
362            }),
363            _ => self.registers.get(&lower).cloned(),
364        }
365    }
366
367    fn system_clipboard_is_newer(&self, cx: &mut Context<Editor>) -> bool {
368        cx.read_from_clipboard().is_some_and(|item| {
369            if let Some(last_state) = &self.last_yank {
370                Some(last_state.as_ref()) != item.text().as_deref()
371            } else {
372                true
373            }
374        })
375    }
376
377    pub fn observe_action(&mut self, action: Box<dyn Action>) {
378        if self.dot_recording {
379            self.recording_actions
380                .push(ReplayableAction::Action(action.boxed_clone()));
381
382            if self.stop_recording_after_next_action {
383                self.dot_recording = false;
384                self.recorded_actions = std::mem::take(&mut self.recording_actions);
385                self.stop_recording_after_next_action = false;
386            }
387        }
388        if self.replayer.is_none() {
389            if let Some(recording_register) = self.recording_register {
390                self.recordings
391                    .entry(recording_register)
392                    .or_default()
393                    .push(ReplayableAction::Action(action));
394            }
395        }
396    }
397
398    pub fn observe_insertion(&mut self, text: &Arc<str>, range_to_replace: Option<Range<isize>>) {
399        if self.ignore_current_insertion {
400            self.ignore_current_insertion = false;
401            return;
402        }
403        if self.dot_recording {
404            self.recording_actions.push(ReplayableAction::Insertion {
405                text: text.clone(),
406                utf16_range_to_replace: range_to_replace.clone(),
407            });
408            if self.stop_recording_after_next_action {
409                self.dot_recording = false;
410                self.recorded_actions = std::mem::take(&mut self.recording_actions);
411                self.stop_recording_after_next_action = false;
412            }
413        }
414        if let Some(recording_register) = self.recording_register {
415            self.recordings.entry(recording_register).or_default().push(
416                ReplayableAction::Insertion {
417                    text: text.clone(),
418                    utf16_range_to_replace: range_to_replace,
419                },
420            );
421        }
422    }
423
424    pub fn focused_vim(&self) -> Option<Entity<Vim>> {
425        self.focused_vim.as_ref().and_then(|vim| vim.upgrade())
426    }
427}
428
429impl Vim {
430    pub fn globals(cx: &mut App) -> &mut VimGlobals {
431        cx.global_mut::<VimGlobals>()
432    }
433
434    pub fn update_globals<C, R>(cx: &mut C, f: impl FnOnce(&mut VimGlobals, &mut C) -> R) -> R
435    where
436        C: BorrowMut<App>,
437    {
438        cx.update_global(f)
439    }
440}
441
442#[derive(Debug)]
443pub enum ReplayableAction {
444    Action(Box<dyn Action>),
445    Insertion {
446        text: Arc<str>,
447        utf16_range_to_replace: Option<Range<isize>>,
448    },
449}
450
451impl Clone for ReplayableAction {
452    fn clone(&self) -> Self {
453        match self {
454            Self::Action(action) => Self::Action(action.boxed_clone()),
455            Self::Insertion {
456                text,
457                utf16_range_to_replace,
458            } => Self::Insertion {
459                text: text.clone(),
460                utf16_range_to_replace: utf16_range_to_replace.clone(),
461            },
462        }
463    }
464}
465
466#[derive(Clone, Default, Debug)]
467pub struct SearchState {
468    pub direction: Direction,
469    pub count: usize,
470
471    pub prior_selections: Vec<Range<Anchor>>,
472    pub prior_operator: Option<Operator>,
473    pub prior_mode: Mode,
474}
475
476impl Operator {
477    pub fn id(&self) -> &'static str {
478        match self {
479            Operator::Object { around: false } => "i",
480            Operator::Object { around: true } => "a",
481            Operator::Change => "c",
482            Operator::Delete => "d",
483            Operator::Yank => "y",
484            Operator::Replace => "r",
485            Operator::Digraph { .. } => "^K",
486            Operator::Literal { .. } => "^V",
487            Operator::FindForward { before: false } => "f",
488            Operator::FindForward { before: true } => "t",
489            Operator::Sneak { .. } => "s",
490            Operator::SneakBackward { .. } => "S",
491            Operator::FindBackward { after: false } => "F",
492            Operator::FindBackward { after: true } => "T",
493            Operator::AddSurrounds { .. } => "ys",
494            Operator::ChangeSurrounds { .. } => "cs",
495            Operator::DeleteSurrounds => "ds",
496            Operator::Mark => "m",
497            Operator::Jump { line: true } => "'",
498            Operator::Jump { line: false } => "`",
499            Operator::Indent => ">",
500            Operator::AutoIndent => "eq",
501            Operator::ShellCommand => "sh",
502            Operator::Rewrap => "gq",
503            Operator::ReplaceWithRegister => "gr",
504            Operator::Exchange => "cx",
505            Operator::Outdent => "<",
506            Operator::Uppercase => "gU",
507            Operator::Lowercase => "gu",
508            Operator::OppositeCase => "g~",
509            Operator::Register => "\"",
510            Operator::RecordRegister => "q",
511            Operator::ReplayRegister => "@",
512            Operator::ToggleComments => "gc",
513        }
514    }
515
516    pub fn status(&self) -> String {
517        match self {
518            Operator::Digraph {
519                first_char: Some(first_char),
520            } => format!("^K{first_char}"),
521            Operator::Literal {
522                prefix: Some(prefix),
523            } => format!("^V{prefix}"),
524            Operator::AutoIndent => "=".to_string(),
525            Operator::ShellCommand => "=".to_string(),
526            _ => self.id().to_string(),
527        }
528    }
529
530    pub fn is_waiting(&self, mode: Mode) -> bool {
531        match self {
532            Operator::AddSurrounds { target } => target.is_some() || mode.is_visual(),
533            Operator::FindForward { .. }
534            | Operator::Mark
535            | Operator::Jump { .. }
536            | Operator::FindBackward { .. }
537            | Operator::Sneak { .. }
538            | Operator::SneakBackward { .. }
539            | Operator::Register
540            | Operator::RecordRegister
541            | Operator::ReplayRegister
542            | Operator::Replace
543            | Operator::Digraph { .. }
544            | Operator::Literal { .. }
545            | Operator::ChangeSurrounds { target: Some(_) }
546            | Operator::DeleteSurrounds => true,
547            Operator::Change
548            | Operator::Delete
549            | Operator::Yank
550            | Operator::Rewrap
551            | Operator::Indent
552            | Operator::Outdent
553            | Operator::AutoIndent
554            | Operator::ShellCommand
555            | Operator::Lowercase
556            | Operator::Uppercase
557            | Operator::ReplaceWithRegister
558            | Operator::Exchange
559            | Operator::Object { .. }
560            | Operator::ChangeSurrounds { target: None }
561            | Operator::OppositeCase
562            | Operator::ToggleComments => false,
563        }
564    }
565}