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