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