1use crate::command::command_interceptor;
2use crate::normal::repeat::Replayer;
3use crate::surrounds::SurroundsType;
4use crate::{motion::Motion, object::Object};
5use crate::{ToggleRegistersView, UseSystemClipboard, Vim, VimSettings};
6use anyhow::Result;
7use collections::HashMap;
8use command_palette_hooks::{CommandPaletteFilter, CommandPaletteInterceptor};
9use db::define_connection;
10use db::sqlez_macros::sql;
11use editor::display_map::{is_invisible, replacement};
12use editor::{Anchor, ClipboardSelection, Editor, MultiBuffer};
13use gpui::{
14 Action, App, AppContext, BorrowAppContext, ClipboardEntry, ClipboardItem, Entity, EntityId,
15 Global, HighlightStyle, StyledText, Subscription, Task, TextStyle, WeakEntity,
16};
17use language::{Buffer, BufferEvent, BufferId, Point};
18use picker::{Picker, PickerDelegate};
19use project::{Project, ProjectItem, ProjectPath};
20use serde::{Deserialize, Serialize};
21use settings::{Settings, SettingsStore};
22use std::borrow::BorrowMut;
23use std::path::Path;
24use std::{fmt::Display, ops::Range, sync::Arc};
25use text::Bias;
26use theme::ThemeSettings;
27use ui::{
28 h_flex, rems, ActiveTheme, Context, Div, FluentBuilder, KeyBinding, ParentElement,
29 SharedString, Styled, StyledTypography, Window,
30};
31use util::ResultExt;
32use workspace::searchable::Direction;
33use workspace::{Workspace, WorkspaceDb, WorkspaceId};
34
35#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
36pub enum Mode {
37 Normal,
38 Insert,
39 Replace,
40 Visual,
41 VisualLine,
42 VisualBlock,
43 HelixNormal,
44}
45
46impl Display for Mode {
47 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48 match self {
49 Mode::Normal => write!(f, "NORMAL"),
50 Mode::Insert => write!(f, "INSERT"),
51 Mode::Replace => write!(f, "REPLACE"),
52 Mode::Visual => write!(f, "VISUAL"),
53 Mode::VisualLine => write!(f, "VISUAL LINE"),
54 Mode::VisualBlock => write!(f, "VISUAL BLOCK"),
55 Mode::HelixNormal => write!(f, "HELIX NORMAL"),
56 }
57 }
58}
59
60impl Mode {
61 pub fn is_visual(&self) -> bool {
62 match self {
63 Self::Visual | Self::VisualLine | Self::VisualBlock => true,
64 Self::Normal | Self::Insert | Self::Replace | Self::HelixNormal => false,
65 }
66 }
67}
68
69impl Default for Mode {
70 fn default() -> Self {
71 Self::Normal
72 }
73}
74
75#[derive(Clone, Debug, PartialEq)]
76pub enum Operator {
77 Change,
78 Delete,
79 Yank,
80 Replace,
81 Object {
82 around: bool,
83 },
84 FindForward {
85 before: bool,
86 },
87 FindBackward {
88 after: bool,
89 },
90 Sneak {
91 first_char: Option<char>,
92 },
93 SneakBackward {
94 first_char: Option<char>,
95 },
96 AddSurrounds {
97 // Typically no need to configure this as `SendKeystrokes` can be used - see #23088.
98 target: Option<SurroundsType>,
99 },
100 ChangeSurrounds {
101 target: Option<Object>,
102 },
103 DeleteSurrounds,
104 Mark,
105 Jump {
106 line: bool,
107 },
108 Indent,
109 Outdent,
110 AutoIndent,
111 Rewrap,
112 ShellCommand,
113 Lowercase,
114 Uppercase,
115 OppositeCase,
116 Digraph {
117 first_char: Option<char>,
118 },
119 Literal {
120 prefix: Option<String>,
121 },
122 Register,
123 RecordRegister,
124 ReplayRegister,
125 ToggleComments,
126 ReplaceWithRegister,
127 Exchange,
128}
129
130#[derive(Default, Clone, Debug)]
131pub enum RecordedSelection {
132 #[default]
133 None,
134 Visual {
135 rows: u32,
136 cols: u32,
137 },
138 SingleLine {
139 cols: u32,
140 },
141 VisualBlock {
142 rows: u32,
143 cols: u32,
144 },
145 VisualLine {
146 rows: u32,
147 },
148}
149
150#[derive(Default, Clone, Debug)]
151pub struct Register {
152 pub(crate) text: SharedString,
153 pub(crate) clipboard_selections: Option<Vec<ClipboardSelection>>,
154}
155
156impl From<Register> for ClipboardItem {
157 fn from(register: Register) -> Self {
158 if let Some(clipboard_selections) = register.clipboard_selections {
159 ClipboardItem::new_string_with_json_metadata(register.text.into(), clipboard_selections)
160 } else {
161 ClipboardItem::new_string(register.text.into())
162 }
163 }
164}
165
166impl From<ClipboardItem> for Register {
167 fn from(item: ClipboardItem) -> Self {
168 // For now, we don't store metadata for multiple entries.
169 match item.entries().first() {
170 Some(ClipboardEntry::String(value)) if item.entries().len() == 1 => Register {
171 text: value.text().to_owned().into(),
172 clipboard_selections: value.metadata_json::<Vec<ClipboardSelection>>(),
173 },
174 // For now, registers can't store images. This could change in the future.
175 _ => Register::default(),
176 }
177 }
178}
179
180impl From<String> for Register {
181 fn from(text: String) -> Self {
182 Register {
183 text: text.into(),
184 clipboard_selections: None,
185 }
186 }
187}
188
189#[derive(Default)]
190pub struct VimGlobals {
191 pub last_find: Option<Motion>,
192
193 pub dot_recording: bool,
194 pub dot_replaying: bool,
195
196 /// pre_count is the number before an operator is specified (3 in 3d2d)
197 pub pre_count: Option<usize>,
198 /// post_count is the number after an operator is specified (2 in 3d2d)
199 pub post_count: Option<usize>,
200
201 pub stop_recording_after_next_action: bool,
202 pub ignore_current_insertion: bool,
203 pub recorded_count: Option<usize>,
204 pub recording_actions: Vec<ReplayableAction>,
205 pub recorded_actions: Vec<ReplayableAction>,
206 pub recorded_selection: RecordedSelection,
207
208 pub recording_register: Option<char>,
209 pub last_recorded_register: Option<char>,
210 pub last_replayed_register: Option<char>,
211 pub replayer: Option<Replayer>,
212
213 pub last_yank: Option<SharedString>,
214 pub registers: HashMap<char, Register>,
215 pub recordings: HashMap<char, Vec<ReplayableAction>>,
216
217 pub focused_vim: Option<WeakEntity<Vim>>,
218
219 pub marks: HashMap<EntityId, Entity<MarksState>>,
220}
221
222pub struct MarksState {
223 workspace: WeakEntity<Workspace>,
224
225 multibuffer_marks: HashMap<EntityId, HashMap<String, Vec<Anchor>>>,
226 buffer_marks: HashMap<BufferId, HashMap<String, Vec<text::Anchor>>>,
227 watched_buffers: HashMap<BufferId, (MarkLocation, Subscription, Subscription)>,
228
229 serialized_marks: HashMap<Arc<Path>, HashMap<String, Vec<Point>>>,
230 global_marks: HashMap<String, MarkLocation>,
231
232 _subscription: Subscription,
233}
234
235#[derive(Debug, PartialEq, Eq, Clone)]
236pub enum MarkLocation {
237 Buffer(EntityId),
238 Path(Arc<Path>),
239}
240
241pub enum Mark {
242 Local(Vec<Anchor>),
243 Buffer(EntityId, Vec<Anchor>),
244 Path(Arc<Path>, Vec<Point>),
245}
246
247impl MarksState {
248 pub fn new(workspace: &Workspace, cx: &mut App) -> Entity<MarksState> {
249 cx.new(|cx| {
250 let buffer_store = workspace.project().read(cx).buffer_store().clone();
251 let subscription =
252 cx.subscribe(
253 &buffer_store,
254 move |this: &mut Self, _, event, cx| match event {
255 project::buffer_store::BufferStoreEvent::BufferAdded(buffer) => {
256 this.on_buffer_loaded(buffer, cx);
257 }
258 _ => {}
259 },
260 );
261
262 let mut this = Self {
263 workspace: workspace.weak_handle(),
264 multibuffer_marks: HashMap::default(),
265 buffer_marks: HashMap::default(),
266 watched_buffers: HashMap::default(),
267 serialized_marks: HashMap::default(),
268 global_marks: HashMap::default(),
269 _subscription: subscription,
270 };
271
272 this.load(cx);
273 this
274 })
275 }
276
277 fn workspace_id(&self, cx: &App) -> Option<WorkspaceId> {
278 self.workspace
279 .read_with(cx, |workspace, _| workspace.database_id())
280 .ok()
281 .flatten()
282 }
283
284 fn project(&self, cx: &App) -> Option<Entity<Project>> {
285 self.workspace
286 .read_with(cx, |workspace, _| workspace.project().clone())
287 .ok()
288 }
289
290 fn load(&mut self, cx: &mut Context<Self>) {
291 cx.spawn(async move |this, cx| {
292 let Some(workspace_id) = this.update(cx, |this, cx| this.workspace_id(cx))? else {
293 return Ok(());
294 };
295 let (marks, paths) = cx
296 .background_spawn(async move {
297 let marks = DB.get_marks(workspace_id)?;
298 let paths = DB.get_global_marks_paths(workspace_id)?;
299 anyhow::Ok((marks, paths))
300 })
301 .await?;
302 this.update(cx, |this, cx| this.loaded(marks, paths, cx))
303 })
304 .detach_and_log_err(cx);
305 }
306
307 fn loaded(
308 &mut self,
309 marks: Vec<SerializedMark>,
310 global_mark_paths: Vec<(String, Arc<Path>)>,
311 cx: &mut Context<Self>,
312 ) {
313 let Some(project) = self.project(cx) else {
314 return;
315 };
316
317 for mark in marks {
318 self.serialized_marks
319 .entry(mark.path)
320 .or_default()
321 .insert(mark.name, mark.points);
322 }
323
324 for (name, path) in global_mark_paths {
325 self.global_marks
326 .insert(name, MarkLocation::Path(path.clone()));
327
328 let project_path = project
329 .read(cx)
330 .worktrees(cx)
331 .filter_map(|worktree| {
332 let relative = path.strip_prefix(worktree.read(cx).abs_path()).ok()?;
333 Some(ProjectPath {
334 worktree_id: worktree.read(cx).id(),
335 path: relative.into(),
336 })
337 })
338 .next();
339 if let Some(buffer) = project_path
340 .and_then(|project_path| project.read(cx).get_open_buffer(&project_path, cx))
341 {
342 self.on_buffer_loaded(&buffer, cx)
343 }
344 }
345 }
346
347 pub fn on_buffer_loaded(&mut self, buffer_handle: &Entity<Buffer>, cx: &mut Context<Self>) {
348 let Some(project) = self.project(cx) else {
349 return;
350 };
351 let Some(project_path) = buffer_handle.read(cx).project_path(cx) else {
352 return;
353 };
354 let Some(abs_path) = project.read(cx).absolute_path(&project_path, cx) else {
355 return;
356 };
357 let abs_path: Arc<Path> = abs_path.into();
358
359 let Some(serialized_marks) = self.serialized_marks.get(&abs_path) else {
360 return;
361 };
362
363 let mut loaded_marks = HashMap::default();
364 let buffer = buffer_handle.read(cx);
365 for (name, points) in serialized_marks.iter() {
366 loaded_marks.insert(
367 name.clone(),
368 points
369 .iter()
370 .map(|point| buffer.anchor_before(buffer.clip_point(*point, Bias::Left)))
371 .collect(),
372 );
373 }
374 self.buffer_marks.insert(buffer.remote_id(), loaded_marks);
375 self.watch_buffer(MarkLocation::Path(abs_path), buffer_handle, cx)
376 }
377
378 fn serialize_buffer_marks(
379 &mut self,
380 path: Arc<Path>,
381 buffer: &Entity<Buffer>,
382 cx: &mut Context<Self>,
383 ) {
384 let new_points: HashMap<String, Vec<Point>> =
385 if let Some(anchors) = self.buffer_marks.get(&buffer.read(cx).remote_id()) {
386 anchors
387 .iter()
388 .map(|(name, anchors)| {
389 (
390 name.clone(),
391 buffer
392 .read(cx)
393 .summaries_for_anchors::<Point, _>(anchors)
394 .collect(),
395 )
396 })
397 .collect()
398 } else {
399 HashMap::default()
400 };
401 let old_points = self.serialized_marks.get(&path.clone());
402 if old_points == Some(&new_points) {
403 return;
404 }
405 let mut to_write = HashMap::default();
406
407 for (key, value) in &new_points {
408 if self.is_global_mark(key) {
409 if self.global_marks.get(key) != Some(&MarkLocation::Path(path.clone())) {
410 if let Some(workspace_id) = self.workspace_id(cx) {
411 let path = path.clone();
412 let key = key.clone();
413 cx.background_spawn(async move {
414 DB.set_global_mark_path(workspace_id, key, path).await
415 })
416 .detach_and_log_err(cx);
417 }
418
419 self.global_marks
420 .insert(key.clone(), MarkLocation::Path(path.clone()));
421 }
422 }
423 if old_points.and_then(|o| o.get(key)) != Some(value) {
424 to_write.insert(key.clone(), value.clone());
425 }
426 }
427
428 self.serialized_marks.insert(path.clone(), new_points);
429
430 if let Some(workspace_id) = self.workspace_id(cx) {
431 cx.background_spawn(async move {
432 DB.set_marks(workspace_id, path.clone(), to_write).await?;
433 anyhow::Ok(())
434 })
435 .detach_and_log_err(cx);
436 }
437 }
438
439 fn is_global_mark(&self, key: &str) -> bool {
440 key.chars()
441 .next()
442 .is_some_and(|c| c.is_uppercase() || c.is_digit(10))
443 }
444
445 fn rename_buffer(
446 &mut self,
447 old_path: MarkLocation,
448 new_path: Arc<Path>,
449 buffer: &Entity<Buffer>,
450 cx: &mut Context<Self>,
451 ) {
452 if let MarkLocation::Buffer(entity_id) = old_path {
453 if let Some(old_marks) = self.multibuffer_marks.remove(&entity_id) {
454 let buffer_marks = old_marks
455 .into_iter()
456 .map(|(k, v)| (k, v.into_iter().map(|anchor| anchor.text_anchor).collect()))
457 .collect();
458 self.buffer_marks
459 .insert(buffer.read(cx).remote_id(), buffer_marks);
460 }
461 }
462 self.watch_buffer(MarkLocation::Path(new_path.clone()), buffer, cx);
463 self.serialize_buffer_marks(new_path, buffer, cx);
464 }
465
466 fn path_for_buffer(&self, buffer: &Entity<Buffer>, cx: &App) -> Option<Arc<Path>> {
467 let project_path = buffer.read(cx).project_path(cx)?;
468 let project = self.project(cx)?;
469 let abs_path = project.read(cx).absolute_path(&project_path, cx)?;
470 Some(abs_path.into())
471 }
472
473 fn points_at(
474 &self,
475 location: &MarkLocation,
476 multi_buffer: &Entity<MultiBuffer>,
477 cx: &App,
478 ) -> bool {
479 match location {
480 MarkLocation::Buffer(entity_id) => entity_id == &multi_buffer.entity_id(),
481 MarkLocation::Path(path) => {
482 let Some(singleton) = multi_buffer.read(cx).as_singleton() else {
483 return false;
484 };
485 self.path_for_buffer(&singleton, cx).as_ref() == Some(path)
486 }
487 }
488 }
489
490 pub fn watch_buffer(
491 &mut self,
492 mark_location: MarkLocation,
493 buffer_handle: &Entity<Buffer>,
494 cx: &mut Context<Self>,
495 ) {
496 let on_change = cx.subscribe(buffer_handle, move |this, buffer, event, cx| match event {
497 BufferEvent::Edited => {
498 if let Some(path) = this.path_for_buffer(&buffer, cx) {
499 this.serialize_buffer_marks(path, &buffer, cx);
500 }
501 }
502 BufferEvent::FileHandleChanged => {
503 let buffer_id = buffer.read(cx).remote_id();
504 if let Some(old_path) = this
505 .watched_buffers
506 .get(&buffer_id.clone())
507 .map(|(path, _, _)| path.clone())
508 {
509 if let Some(new_path) = this.path_for_buffer(&buffer, cx) {
510 this.rename_buffer(old_path, new_path, &buffer, cx)
511 }
512 }
513 }
514 _ => {}
515 });
516
517 let on_release = cx.observe_release(buffer_handle, |this, buffer, _| {
518 this.watched_buffers.remove(&buffer.remote_id());
519 this.buffer_marks.remove(&buffer.remote_id());
520 });
521
522 self.watched_buffers.insert(
523 buffer_handle.read(cx).remote_id(),
524 (mark_location, on_change, on_release),
525 );
526 }
527
528 pub fn set_mark(
529 &mut self,
530 name: String,
531 multibuffer: &Entity<MultiBuffer>,
532 anchors: Vec<Anchor>,
533 cx: &mut Context<Self>,
534 ) {
535 let buffer = multibuffer.read(cx).as_singleton();
536 let abs_path = buffer.as_ref().and_then(|b| self.path_for_buffer(&b, cx));
537
538 let Some(abs_path) = abs_path else {
539 self.multibuffer_marks
540 .entry(multibuffer.entity_id())
541 .or_default()
542 .insert(name.clone(), anchors);
543 if self.is_global_mark(&name) {
544 self.global_marks
545 .insert(name.clone(), MarkLocation::Buffer(multibuffer.entity_id()));
546 }
547 if let Some(buffer) = buffer {
548 let buffer_id = buffer.read(cx).remote_id();
549 if !self.watched_buffers.contains_key(&buffer_id) {
550 self.watch_buffer(MarkLocation::Buffer(multibuffer.entity_id()), &buffer, cx)
551 }
552 }
553 return;
554 };
555 let buffer = buffer.unwrap();
556
557 let buffer_id = buffer.read(cx).remote_id();
558 self.buffer_marks.entry(buffer_id).or_default().insert(
559 name.clone(),
560 anchors
561 .into_iter()
562 .map(|anchor| anchor.text_anchor)
563 .collect(),
564 );
565 if !self.watched_buffers.contains_key(&buffer_id) {
566 self.watch_buffer(MarkLocation::Path(abs_path.clone()), &buffer, cx)
567 }
568 self.serialize_buffer_marks(abs_path, &buffer, cx)
569 }
570
571 pub fn get_mark(
572 &self,
573 name: &str,
574 multi_buffer: &Entity<MultiBuffer>,
575 cx: &App,
576 ) -> Option<Mark> {
577 let target = self.global_marks.get(name);
578
579 if !self.is_global_mark(name) || target.is_some_and(|t| self.points_at(t, multi_buffer, cx))
580 {
581 if let Some(anchors) = self.multibuffer_marks.get(&multi_buffer.entity_id()) {
582 return Some(Mark::Local(anchors.get(name)?.clone()));
583 }
584
585 let singleton = multi_buffer.read(cx).as_singleton()?;
586 let excerpt_id = *multi_buffer.read(cx).excerpt_ids().first().unwrap();
587 let buffer_id = singleton.read(cx).remote_id();
588 if let Some(anchors) = self.buffer_marks.get(&buffer_id) {
589 let text_anchors = anchors.get(name)?;
590 let anchors = text_anchors
591 .into_iter()
592 .map(|anchor| Anchor::in_buffer(excerpt_id, buffer_id, *anchor))
593 .collect();
594 return Some(Mark::Local(anchors));
595 }
596 }
597
598 match target? {
599 MarkLocation::Buffer(entity_id) => {
600 let anchors = self.multibuffer_marks.get(&entity_id)?;
601 return Some(Mark::Buffer(*entity_id, anchors.get(name)?.clone()));
602 }
603 MarkLocation::Path(path) => {
604 let points = self.serialized_marks.get(path)?;
605 return Some(Mark::Path(path.clone(), points.get(name)?.clone()));
606 }
607 }
608 }
609}
610
611impl Global for VimGlobals {}
612
613impl VimGlobals {
614 pub(crate) fn register(cx: &mut App) {
615 cx.set_global(VimGlobals::default());
616
617 cx.observe_keystrokes(|event, _, cx| {
618 let Some(action) = event.action.as_ref().map(|action| action.boxed_clone()) else {
619 return;
620 };
621 Vim::globals(cx).observe_action(action.boxed_clone())
622 })
623 .detach();
624
625 cx.observe_new(|workspace: &mut Workspace, window, _| {
626 RegistersView::register(workspace, window);
627 })
628 .detach();
629
630 let mut was_enabled = None;
631
632 cx.observe_global::<SettingsStore>(move |cx| {
633 let is_enabled = Vim::enabled(cx);
634 if was_enabled == Some(is_enabled) {
635 return;
636 }
637 was_enabled = Some(is_enabled);
638 if is_enabled {
639 KeyBinding::set_vim_mode(cx, true);
640 CommandPaletteFilter::update_global(cx, |filter, _| {
641 filter.show_namespace(Vim::NAMESPACE);
642 });
643 CommandPaletteInterceptor::update_global(cx, |interceptor, _| {
644 interceptor.set(Box::new(command_interceptor));
645 });
646 for window in cx.windows() {
647 if let Some(workspace) = window.downcast::<Workspace>() {
648 workspace
649 .update(cx, |workspace, _, cx| {
650 Vim::update_globals(cx, |globals, cx| {
651 globals.register_workspace(workspace, cx)
652 });
653 })
654 .ok();
655 }
656 }
657 } else {
658 KeyBinding::set_vim_mode(cx, false);
659 *Vim::globals(cx) = VimGlobals::default();
660 CommandPaletteInterceptor::update_global(cx, |interceptor, _| {
661 interceptor.clear();
662 });
663 CommandPaletteFilter::update_global(cx, |filter, _| {
664 filter.hide_namespace(Vim::NAMESPACE);
665 });
666 }
667 })
668 .detach();
669 cx.observe_new(|workspace: &mut Workspace, _, cx| {
670 Vim::update_globals(cx, |globals, cx| globals.register_workspace(workspace, cx));
671 })
672 .detach()
673 }
674
675 fn register_workspace(&mut self, workspace: &Workspace, cx: &mut Context<Workspace>) {
676 let entity_id = cx.entity_id();
677 self.marks.insert(entity_id, MarksState::new(workspace, cx));
678 cx.observe_release(&cx.entity(), move |_, _, cx| {
679 Vim::update_globals(cx, |globals, _| {
680 globals.marks.remove(&entity_id);
681 })
682 })
683 .detach();
684 }
685
686 pub(crate) fn write_registers(
687 &mut self,
688 content: Register,
689 register: Option<char>,
690 is_yank: bool,
691 linewise: bool,
692 cx: &mut Context<Editor>,
693 ) {
694 if let Some(register) = register {
695 let lower = register.to_lowercase().next().unwrap_or(register);
696 if lower != register {
697 let current = self.registers.entry(lower).or_default();
698 current.text = (current.text.to_string() + &content.text).into();
699 // not clear how to support appending to registers with multiple cursors
700 current.clipboard_selections.take();
701 let yanked = current.clone();
702 self.registers.insert('"', yanked);
703 } else {
704 match lower {
705 '_' | ':' | '.' | '%' | '#' | '=' | '/' => {}
706 '+' => {
707 self.registers.insert('"', content.clone());
708 cx.write_to_clipboard(content.into());
709 }
710 '*' => {
711 self.registers.insert('"', content.clone());
712 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
713 cx.write_to_primary(content.into());
714 #[cfg(not(any(target_os = "linux", target_os = "freebsd")))]
715 cx.write_to_clipboard(content.into());
716 }
717 '"' => {
718 self.registers.insert('"', content.clone());
719 self.registers.insert('0', content);
720 }
721 _ => {
722 self.registers.insert('"', content.clone());
723 self.registers.insert(lower, content);
724 }
725 }
726 }
727 } else {
728 let setting = VimSettings::get_global(cx).use_system_clipboard;
729 if setting == UseSystemClipboard::Always
730 || setting == UseSystemClipboard::OnYank && is_yank
731 {
732 self.last_yank.replace(content.text.clone());
733 cx.write_to_clipboard(content.clone().into());
734 } else {
735 self.last_yank = cx
736 .read_from_clipboard()
737 .and_then(|item| item.text().map(|string| string.into()));
738 }
739
740 self.registers.insert('"', content.clone());
741 if is_yank {
742 self.registers.insert('0', content);
743 } else {
744 let contains_newline = content.text.contains('\n');
745 if !contains_newline {
746 self.registers.insert('-', content.clone());
747 }
748 if linewise || contains_newline {
749 let mut content = content;
750 for i in '1'..'8' {
751 if let Some(moved) = self.registers.insert(i, content) {
752 content = moved;
753 } else {
754 break;
755 }
756 }
757 }
758 }
759 }
760 }
761
762 pub(crate) fn read_register(
763 &self,
764 register: Option<char>,
765 editor: Option<&mut Editor>,
766 cx: &mut App,
767 ) -> Option<Register> {
768 let Some(register) = register.filter(|reg| *reg != '"') else {
769 let setting = VimSettings::get_global(cx).use_system_clipboard;
770 return match setting {
771 UseSystemClipboard::Always => cx.read_from_clipboard().map(|item| item.into()),
772 UseSystemClipboard::OnYank if self.system_clipboard_is_newer(cx) => {
773 cx.read_from_clipboard().map(|item| item.into())
774 }
775 _ => self.registers.get(&'"').cloned(),
776 };
777 };
778 let lower = register.to_lowercase().next().unwrap_or(register);
779 match lower {
780 '_' | ':' | '.' | '#' | '=' => None,
781 '+' => cx.read_from_clipboard().map(|item| item.into()),
782 '*' => {
783 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
784 {
785 cx.read_from_primary().map(|item| item.into())
786 }
787 #[cfg(not(any(target_os = "linux", target_os = "freebsd")))]
788 {
789 cx.read_from_clipboard().map(|item| item.into())
790 }
791 }
792 '%' => editor.and_then(|editor| {
793 let selection = editor.selections.newest::<Point>(cx);
794 if let Some((_, buffer, _)) = editor
795 .buffer()
796 .read(cx)
797 .excerpt_containing(selection.head(), cx)
798 {
799 buffer
800 .read(cx)
801 .file()
802 .map(|file| file.path().to_string_lossy().to_string().into())
803 } else {
804 None
805 }
806 }),
807 _ => self.registers.get(&lower).cloned(),
808 }
809 }
810
811 fn system_clipboard_is_newer(&self, cx: &App) -> bool {
812 cx.read_from_clipboard().is_some_and(|item| {
813 if let Some(last_state) = &self.last_yank {
814 Some(last_state.as_ref()) != item.text().as_deref()
815 } else {
816 true
817 }
818 })
819 }
820
821 pub fn observe_action(&mut self, action: Box<dyn Action>) {
822 if self.dot_recording {
823 self.recording_actions
824 .push(ReplayableAction::Action(action.boxed_clone()));
825
826 if self.stop_recording_after_next_action {
827 self.dot_recording = false;
828 self.recorded_actions = std::mem::take(&mut self.recording_actions);
829 self.stop_recording_after_next_action = false;
830 }
831 }
832 if self.replayer.is_none() {
833 if let Some(recording_register) = self.recording_register {
834 self.recordings
835 .entry(recording_register)
836 .or_default()
837 .push(ReplayableAction::Action(action));
838 }
839 }
840 }
841
842 pub fn observe_insertion(&mut self, text: &Arc<str>, range_to_replace: Option<Range<isize>>) {
843 if self.ignore_current_insertion {
844 self.ignore_current_insertion = false;
845 return;
846 }
847 if self.dot_recording {
848 self.recording_actions.push(ReplayableAction::Insertion {
849 text: text.clone(),
850 utf16_range_to_replace: range_to_replace.clone(),
851 });
852 if self.stop_recording_after_next_action {
853 self.dot_recording = false;
854 self.recorded_actions = std::mem::take(&mut self.recording_actions);
855 self.stop_recording_after_next_action = false;
856 }
857 }
858 if let Some(recording_register) = self.recording_register {
859 self.recordings.entry(recording_register).or_default().push(
860 ReplayableAction::Insertion {
861 text: text.clone(),
862 utf16_range_to_replace: range_to_replace,
863 },
864 );
865 }
866 }
867
868 pub fn focused_vim(&self) -> Option<Entity<Vim>> {
869 self.focused_vim.as_ref().and_then(|vim| vim.upgrade())
870 }
871}
872
873impl Vim {
874 pub fn globals(cx: &mut App) -> &mut VimGlobals {
875 cx.global_mut::<VimGlobals>()
876 }
877
878 pub fn update_globals<C, R>(cx: &mut C, f: impl FnOnce(&mut VimGlobals, &mut C) -> R) -> R
879 where
880 C: BorrowMut<App>,
881 {
882 cx.update_global(f)
883 }
884}
885
886#[derive(Debug)]
887pub enum ReplayableAction {
888 Action(Box<dyn Action>),
889 Insertion {
890 text: Arc<str>,
891 utf16_range_to_replace: Option<Range<isize>>,
892 },
893}
894
895impl Clone for ReplayableAction {
896 fn clone(&self) -> Self {
897 match self {
898 Self::Action(action) => Self::Action(action.boxed_clone()),
899 Self::Insertion {
900 text,
901 utf16_range_to_replace,
902 } => Self::Insertion {
903 text: text.clone(),
904 utf16_range_to_replace: utf16_range_to_replace.clone(),
905 },
906 }
907 }
908}
909
910#[derive(Clone, Default, Debug)]
911pub struct SearchState {
912 pub direction: Direction,
913 pub count: usize,
914
915 pub prior_selections: Vec<Range<Anchor>>,
916 pub prior_operator: Option<Operator>,
917 pub prior_mode: Mode,
918}
919
920impl Operator {
921 pub fn id(&self) -> &'static str {
922 match self {
923 Operator::Object { around: false } => "i",
924 Operator::Object { around: true } => "a",
925 Operator::Change => "c",
926 Operator::Delete => "d",
927 Operator::Yank => "y",
928 Operator::Replace => "r",
929 Operator::Digraph { .. } => "^K",
930 Operator::Literal { .. } => "^V",
931 Operator::FindForward { before: false } => "f",
932 Operator::FindForward { before: true } => "t",
933 Operator::Sneak { .. } => "s",
934 Operator::SneakBackward { .. } => "S",
935 Operator::FindBackward { after: false } => "F",
936 Operator::FindBackward { after: true } => "T",
937 Operator::AddSurrounds { .. } => "ys",
938 Operator::ChangeSurrounds { .. } => "cs",
939 Operator::DeleteSurrounds => "ds",
940 Operator::Mark => "m",
941 Operator::Jump { line: true } => "'",
942 Operator::Jump { line: false } => "`",
943 Operator::Indent => ">",
944 Operator::AutoIndent => "eq",
945 Operator::ShellCommand => "sh",
946 Operator::Rewrap => "gq",
947 Operator::ReplaceWithRegister => "gr",
948 Operator::Exchange => "cx",
949 Operator::Outdent => "<",
950 Operator::Uppercase => "gU",
951 Operator::Lowercase => "gu",
952 Operator::OppositeCase => "g~",
953 Operator::Register => "\"",
954 Operator::RecordRegister => "q",
955 Operator::ReplayRegister => "@",
956 Operator::ToggleComments => "gc",
957 }
958 }
959
960 pub fn status(&self) -> String {
961 match self {
962 Operator::Digraph {
963 first_char: Some(first_char),
964 } => format!("^K{first_char}"),
965 Operator::Literal {
966 prefix: Some(prefix),
967 } => format!("^V{prefix}"),
968 Operator::AutoIndent => "=".to_string(),
969 Operator::ShellCommand => "=".to_string(),
970 _ => self.id().to_string(),
971 }
972 }
973
974 pub fn is_waiting(&self, mode: Mode) -> bool {
975 match self {
976 Operator::AddSurrounds { target } => target.is_some() || mode.is_visual(),
977 Operator::FindForward { .. }
978 | Operator::Mark
979 | Operator::Jump { .. }
980 | Operator::FindBackward { .. }
981 | Operator::Sneak { .. }
982 | Operator::SneakBackward { .. }
983 | Operator::Register
984 | Operator::RecordRegister
985 | Operator::ReplayRegister
986 | Operator::Replace
987 | Operator::Digraph { .. }
988 | Operator::Literal { .. }
989 | Operator::ChangeSurrounds { target: Some(_) }
990 | Operator::DeleteSurrounds => true,
991 Operator::Change
992 | Operator::Delete
993 | Operator::Yank
994 | Operator::Rewrap
995 | Operator::Indent
996 | Operator::Outdent
997 | Operator::AutoIndent
998 | Operator::ShellCommand
999 | Operator::Lowercase
1000 | Operator::Uppercase
1001 | Operator::ReplaceWithRegister
1002 | Operator::Exchange
1003 | Operator::Object { .. }
1004 | Operator::ChangeSurrounds { target: None }
1005 | Operator::OppositeCase
1006 | Operator::ToggleComments => false,
1007 }
1008 }
1009
1010 pub fn starts_dot_recording(&self) -> bool {
1011 match self {
1012 Operator::Change
1013 | Operator::Delete
1014 | Operator::Replace
1015 | Operator::Indent
1016 | Operator::Outdent
1017 | Operator::AutoIndent
1018 | Operator::Lowercase
1019 | Operator::Uppercase
1020 | Operator::OppositeCase
1021 | Operator::ToggleComments
1022 | Operator::ReplaceWithRegister
1023 | Operator::Rewrap
1024 | Operator::ShellCommand
1025 | Operator::AddSurrounds { target: None }
1026 | Operator::ChangeSurrounds { target: None }
1027 | Operator::DeleteSurrounds
1028 | Operator::Exchange => true,
1029 Operator::Yank
1030 | Operator::Object { .. }
1031 | Operator::FindForward { .. }
1032 | Operator::FindBackward { .. }
1033 | Operator::Sneak { .. }
1034 | Operator::SneakBackward { .. }
1035 | Operator::Mark
1036 | Operator::Digraph { .. }
1037 | Operator::Literal { .. }
1038 | Operator::AddSurrounds { .. }
1039 | Operator::ChangeSurrounds { .. }
1040 | Operator::Jump { .. }
1041 | Operator::Register
1042 | Operator::RecordRegister
1043 | Operator::ReplayRegister => false,
1044 }
1045 }
1046}
1047
1048struct RegisterMatch {
1049 name: char,
1050 contents: SharedString,
1051}
1052
1053pub struct RegistersViewDelegate {
1054 selected_index: usize,
1055 matches: Vec<RegisterMatch>,
1056}
1057
1058impl PickerDelegate for RegistersViewDelegate {
1059 type ListItem = Div;
1060
1061 fn match_count(&self) -> usize {
1062 self.matches.len()
1063 }
1064
1065 fn selected_index(&self) -> usize {
1066 self.selected_index
1067 }
1068
1069 fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context<Picker<Self>>) {
1070 self.selected_index = ix;
1071 cx.notify();
1072 }
1073
1074 fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
1075 Arc::default()
1076 }
1077
1078 fn update_matches(
1079 &mut self,
1080 _: String,
1081 _: &mut Window,
1082 _: &mut Context<Picker<Self>>,
1083 ) -> gpui::Task<()> {
1084 Task::ready(())
1085 }
1086
1087 fn confirm(&mut self, _: bool, _: &mut Window, _: &mut Context<Picker<Self>>) {}
1088
1089 fn dismissed(&mut self, _: &mut Window, _: &mut Context<Picker<Self>>) {}
1090
1091 fn render_match(
1092 &self,
1093 ix: usize,
1094 selected: bool,
1095 _: &mut Window,
1096 cx: &mut Context<Picker<Self>>,
1097 ) -> Option<Self::ListItem> {
1098 let register_match = self
1099 .matches
1100 .get(ix)
1101 .expect("Invalid matches state: no element for index {ix}");
1102
1103 let mut output = String::new();
1104 let mut runs = Vec::new();
1105 output.push('"');
1106 output.push(register_match.name);
1107 runs.push((
1108 0..output.len(),
1109 HighlightStyle::color(cx.theme().colors().text_accent),
1110 ));
1111 output.push(' ');
1112 output.push(' ');
1113 let mut base = output.len();
1114 for (ix, c) in register_match.contents.char_indices() {
1115 if ix > 100 {
1116 break;
1117 }
1118 let replace = match c {
1119 '\t' => Some("\\t".to_string()),
1120 '\n' => Some("\\n".to_string()),
1121 '\r' => Some("\\r".to_string()),
1122 c if is_invisible(c) => {
1123 if c <= '\x1f' {
1124 replacement(c).map(|s| s.to_string())
1125 } else {
1126 Some(format!("\\u{:04X}", c as u32))
1127 }
1128 }
1129 _ => None,
1130 };
1131 let Some(replace) = replace else {
1132 output.push(c);
1133 continue;
1134 };
1135 output.push_str(&replace);
1136 runs.push((
1137 base + ix..base + ix + replace.len(),
1138 HighlightStyle::color(cx.theme().colors().text_muted),
1139 ));
1140 base += replace.len() - c.len_utf8();
1141 }
1142
1143 let theme = ThemeSettings::get_global(cx);
1144 let text_style = TextStyle {
1145 color: cx.theme().colors().editor_foreground,
1146 font_family: theme.buffer_font.family.clone(),
1147 font_features: theme.buffer_font.features.clone(),
1148 font_fallbacks: theme.buffer_font.fallbacks.clone(),
1149 font_size: theme.buffer_font_size(cx).into(),
1150 line_height: (theme.line_height() * theme.buffer_font_size(cx)).into(),
1151 font_weight: theme.buffer_font.weight,
1152 font_style: theme.buffer_font.style,
1153 ..Default::default()
1154 };
1155
1156 Some(
1157 h_flex()
1158 .when(selected, |el| el.bg(cx.theme().colors().element_selected))
1159 .font_buffer(cx)
1160 .text_buffer(cx)
1161 .h(theme.buffer_font_size(cx) * theme.line_height())
1162 .px_2()
1163 .gap_1()
1164 .child(StyledText::new(output).with_default_highlights(&text_style, runs)),
1165 )
1166 }
1167}
1168
1169pub struct RegistersView {}
1170
1171impl RegistersView {
1172 fn register(workspace: &mut Workspace, _window: Option<&mut Window>) {
1173 workspace.register_action(|workspace, _: &ToggleRegistersView, window, cx| {
1174 Self::toggle(workspace, window, cx);
1175 });
1176 }
1177
1178 pub fn toggle(workspace: &mut Workspace, window: &mut Window, cx: &mut Context<Workspace>) {
1179 let editor = workspace
1180 .active_item(cx)
1181 .and_then(|item| item.act_as::<Editor>(cx));
1182 workspace.toggle_modal(window, cx, move |window, cx| {
1183 RegistersView::new(editor, window, cx)
1184 });
1185 }
1186
1187 fn new(
1188 editor: Option<Entity<Editor>>,
1189 window: &mut Window,
1190 cx: &mut Context<Picker<RegistersViewDelegate>>,
1191 ) -> Picker<RegistersViewDelegate> {
1192 let mut matches = Vec::default();
1193 cx.update_global(|globals: &mut VimGlobals, cx| {
1194 for name in ['"', '+', '*'] {
1195 if let Some(register) = globals.read_register(Some(name), None, cx) {
1196 matches.push(RegisterMatch {
1197 name,
1198 contents: register.text.clone(),
1199 })
1200 }
1201 }
1202 if let Some(editor) = editor {
1203 let register = editor.update(cx, |editor, cx| {
1204 globals.read_register(Some('%'), Some(editor), cx)
1205 });
1206 if let Some(register) = register {
1207 matches.push(RegisterMatch {
1208 name: '%',
1209 contents: register.text.clone(),
1210 })
1211 }
1212 }
1213 for (name, register) in globals.registers.iter() {
1214 if ['"', '+', '*', '%'].contains(name) {
1215 continue;
1216 };
1217 matches.push(RegisterMatch {
1218 name: *name,
1219 contents: register.text.clone(),
1220 })
1221 }
1222 });
1223 matches.sort_by(|a, b| a.name.cmp(&b.name));
1224 let delegate = RegistersViewDelegate {
1225 selected_index: 0,
1226 matches,
1227 };
1228
1229 Picker::nonsearchable_uniform_list(delegate, window, cx)
1230 .width(rems(36.))
1231 .modal(true)
1232 }
1233}
1234
1235define_connection! (
1236 pub static ref DB: VimDb<WorkspaceDb> = &[
1237 sql! (
1238 CREATE TABLE vim_marks (
1239 workspace_id INTEGER,
1240 mark_name TEXT,
1241 path BLOB,
1242 value TEXT
1243 );
1244 CREATE UNIQUE INDEX idx_vim_marks ON vim_marks (workspace_id, mark_name, path);
1245 ),
1246 sql! (
1247 CREATE TABLE vim_global_marks_paths(
1248 workspace_id INTEGER,
1249 mark_name TEXT,
1250 path BLOB
1251 );
1252 CREATE UNIQUE INDEX idx_vim_global_marks_paths
1253 ON vim_global_marks_paths(workspace_id, mark_name);
1254 ),
1255 ];
1256);
1257
1258struct SerializedMark {
1259 path: Arc<Path>,
1260 name: String,
1261 points: Vec<Point>,
1262}
1263
1264impl VimDb {
1265 pub(crate) async fn set_marks(
1266 &self,
1267 workspace_id: WorkspaceId,
1268 path: Arc<Path>,
1269 marks: HashMap<String, Vec<Point>>,
1270 ) -> Result<()> {
1271 let result = self
1272 .write(move |conn| {
1273 let mut query = conn.exec_bound(sql!(
1274 INSERT OR REPLACE INTO vim_marks
1275 (workspace_id, mark_name, path, value)
1276 VALUES
1277 (?, ?, ?, ?)
1278 ))?;
1279 for (mark_name, value) in marks {
1280 let pairs: Vec<(u32, u32)> = value
1281 .into_iter()
1282 .map(|point| (point.row, point.column))
1283 .collect();
1284 let serialized = serde_json::to_string(&pairs)?;
1285 query((workspace_id, mark_name, path.clone(), serialized))?;
1286 }
1287 Ok(())
1288 })
1289 .await;
1290 result
1291 }
1292
1293 fn get_marks(&self, workspace_id: WorkspaceId) -> Result<Vec<SerializedMark>> {
1294 let result: Vec<(Arc<Path>, String, String)> = self.select_bound(sql!(
1295 SELECT path, mark_name, value FROM vim_marks
1296 WHERE workspace_id = ?
1297 ))?(workspace_id)?;
1298
1299 Ok(result
1300 .into_iter()
1301 .filter_map(|(path, name, value)| {
1302 let pairs: Vec<(u32, u32)> = serde_json::from_str(&value).log_err()?;
1303 Some(SerializedMark {
1304 path,
1305 name,
1306 points: pairs
1307 .into_iter()
1308 .map(|(row, column)| Point { row, column })
1309 .collect(),
1310 })
1311 })
1312 .collect())
1313 }
1314
1315 pub(crate) async fn set_global_mark_path(
1316 &self,
1317 workspace_id: WorkspaceId,
1318 mark_name: String,
1319 path: Arc<Path>,
1320 ) -> Result<()> {
1321 self.write(move |conn| {
1322 conn.exec_bound(sql!(
1323 INSERT OR REPLACE INTO vim_global_marks_paths
1324 (workspace_id, mark_name, path)
1325 VALUES
1326 (?, ?, ?)
1327 ))?((workspace_id, mark_name, path))
1328 })
1329 .await
1330 }
1331
1332 pub fn get_global_marks_paths(
1333 &self,
1334 workspace_id: WorkspaceId,
1335 ) -> Result<Vec<(String, Arc<Path>)>> {
1336 self.select_bound(sql!(
1337 SELECT mark_name, path FROM vim_global_marks_paths
1338 WHERE workspace_id = ?
1339 ))?(workspace_id)
1340 }
1341}