1use crate::command::command_interceptor;
2use crate::motion::MotionKind;
3use crate::normal::repeat::Replayer;
4use crate::surrounds::SurroundsType;
5use crate::{ToggleMarksView, ToggleRegistersView, UseSystemClipboard, Vim, VimAddon, VimSettings};
6use crate::{motion::Motion, object::Object};
7use anyhow::Result;
8use collections::HashMap;
9use command_palette_hooks::{CommandPaletteFilter, GlobalCommandPaletteInterceptor};
10use db::{
11 sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection},
12 sqlez_macros::sql,
13};
14use editor::display_map::{is_invisible, replacement};
15use editor::{Anchor, ClipboardSelection, Editor, MultiBuffer, ToPoint as EditorToPoint};
16use gpui::{
17 Action, App, AppContext, BorrowAppContext, ClipboardEntry, ClipboardItem, DismissEvent, Entity,
18 EntityId, Global, HighlightStyle, StyledText, Subscription, Task, TextStyle, WeakEntity,
19};
20use language::{Buffer, BufferEvent, BufferId, Chunk, Point};
21use multi_buffer::MultiBufferRow;
22use picker::{Picker, PickerDelegate};
23use project::{Project, ProjectItem, ProjectPath};
24use serde::{Deserialize, Serialize};
25use settings::{Settings, SettingsStore};
26use std::borrow::BorrowMut;
27use std::collections::HashSet;
28use std::path::Path;
29use std::{fmt::Display, ops::Range, sync::Arc};
30use text::{Bias, ToPoint};
31use theme::ThemeSettings;
32use ui::{
33 ActiveTheme, Context, Div, FluentBuilder, KeyBinding, ParentElement, SharedString, Styled,
34 StyledTypography, Window, h_flex, rems,
35};
36use util::ResultExt;
37use util::rel_path::RelPath;
38use workspace::searchable::Direction;
39use workspace::{Workspace, WorkspaceDb, WorkspaceId};
40
41#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
42pub enum Mode {
43 Normal,
44 Insert,
45 Replace,
46 Visual,
47 VisualLine,
48 VisualBlock,
49 HelixNormal,
50 HelixSelect,
51}
52
53impl Display for Mode {
54 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55 match self {
56 Mode::Normal => write!(f, "NORMAL"),
57 Mode::Insert => write!(f, "INSERT"),
58 Mode::Replace => write!(f, "REPLACE"),
59 Mode::Visual => write!(f, "VISUAL"),
60 Mode::VisualLine => write!(f, "VISUAL LINE"),
61 Mode::VisualBlock => write!(f, "VISUAL BLOCK"),
62 Mode::HelixNormal => write!(f, "NORMAL"),
63 Mode::HelixSelect => write!(f, "SELECT"),
64 }
65 }
66}
67
68impl Mode {
69 pub fn is_visual(&self) -> bool {
70 match self {
71 Self::Visual | Self::VisualLine | Self::VisualBlock | Self::HelixSelect => true,
72 Self::Normal | Self::Insert | Self::Replace | Self::HelixNormal => false,
73 }
74 }
75}
76
77impl Default for Mode {
78 fn default() -> Self {
79 Self::Normal
80 }
81}
82
83#[derive(Clone, Debug, PartialEq)]
84pub enum Operator {
85 Change,
86 Delete,
87 Yank,
88 Replace,
89 Object {
90 scope: ObjectScope,
91 },
92 FindForward {
93 before: bool,
94 multiline: bool,
95 },
96 FindBackward {
97 after: bool,
98 multiline: bool,
99 },
100 Sneak {
101 first_char: Option<char>,
102 },
103 SneakBackward {
104 first_char: Option<char>,
105 },
106 AddSurrounds {
107 // Typically no need to configure this as `SendKeystrokes` can be used - see #23088.
108 target: Option<SurroundsType>,
109 },
110 ChangeSurrounds {
111 target: Option<Object>,
112 /// Represents whether the opening bracket was used for the target
113 /// object.
114 opening: bool,
115 },
116 DeleteSurrounds,
117 Mark,
118 Jump {
119 line: bool,
120 },
121 Indent,
122 Outdent,
123 AutoIndent,
124 Rewrap,
125 ShellCommand,
126 Lowercase,
127 Uppercase,
128 OppositeCase,
129 Rot13,
130 Rot47,
131 Digraph {
132 first_char: Option<char>,
133 },
134 Literal {
135 prefix: Option<String>,
136 },
137 Register,
138 RecordRegister,
139 ReplayRegister,
140 ToggleComments,
141 ReplaceWithRegister,
142 Exchange,
143 HelixMatch,
144 HelixNext {
145 around: bool,
146 },
147 HelixPrevious {
148 around: bool,
149 },
150}
151
152/// Controls how the object interacts with its delimiters and the surrounding
153/// whitespace.
154#[derive(Clone, Debug, PartialEq)]
155pub(crate) enum ObjectScope {
156 /// Inside the delimiters, excluding whitespace.
157 ///
158 /// Used by the `i` operator (e.g., `diw` for "delete inner word").
159 /// Selects only the content between delimiters without including
160 /// the delimiters themselves or surrounding whitespace.
161 Inside,
162 /// Around the delimiters, including surrounding whitespace.
163 ///
164 /// Used by the `a` operator (e.g., `daw` for "delete a word").
165 /// Selects the content, the delimiters, and any surrounding whitespace.
166 Around,
167 /// Around the delimiters, excluding surrounding whitespace.
168 ///
169 /// Similar to `Around`, but does not include whitespace adjacent to
170 /// the delimiters.
171 AroundTrimmed,
172}
173
174#[derive(Default, Clone, Debug)]
175pub enum RecordedSelection {
176 #[default]
177 None,
178 Visual {
179 rows: u32,
180 cols: u32,
181 },
182 SingleLine {
183 cols: u32,
184 },
185 VisualBlock {
186 rows: u32,
187 cols: u32,
188 },
189 VisualLine {
190 rows: u32,
191 },
192}
193
194#[derive(Default, Clone, Debug)]
195pub struct Register {
196 pub(crate) text: SharedString,
197 pub(crate) clipboard_selections: Option<Vec<ClipboardSelection>>,
198}
199
200impl From<Register> for ClipboardItem {
201 fn from(register: Register) -> Self {
202 if let Some(clipboard_selections) = register.clipboard_selections {
203 ClipboardItem::new_string_with_json_metadata(register.text.into(), clipboard_selections)
204 } else {
205 ClipboardItem::new_string(register.text.into())
206 }
207 }
208}
209
210impl From<ClipboardItem> for Register {
211 fn from(item: ClipboardItem) -> Self {
212 // For now, we don't store metadata for multiple entries.
213 match item.entries().first() {
214 Some(ClipboardEntry::String(value)) if item.entries().len() == 1 => Register {
215 text: value.text().to_owned().into(),
216 clipboard_selections: value.metadata_json::<Vec<ClipboardSelection>>(),
217 },
218 // For now, registers can't store images. This could change in the future.
219 _ => Register::default(),
220 }
221 }
222}
223
224impl From<String> for Register {
225 fn from(text: String) -> Self {
226 Register {
227 text: text.into(),
228 clipboard_selections: None,
229 }
230 }
231}
232
233#[derive(Default)]
234pub struct VimGlobals {
235 pub last_find: Option<Motion>,
236
237 pub dot_recording: bool,
238 pub dot_replaying: bool,
239
240 /// pre_count is the number before an operator is specified (3 in 3d2d)
241 pub pre_count: Option<usize>,
242 /// post_count is the number after an operator is specified (2 in 3d2d)
243 pub post_count: Option<usize>,
244 pub forced_motion: bool,
245 pub stop_recording_after_next_action: bool,
246 pub ignore_current_insertion: bool,
247 pub recorded_count: Option<usize>,
248 pub recording_actions: Vec<ReplayableAction>,
249 pub recorded_actions: Vec<ReplayableAction>,
250 pub recorded_selection: RecordedSelection,
251
252 pub recording_register: Option<char>,
253 pub last_recorded_register: Option<char>,
254 pub last_replayed_register: Option<char>,
255 pub replayer: Option<Replayer>,
256
257 pub last_yank: Option<SharedString>,
258 pub registers: HashMap<char, Register>,
259 pub recordings: HashMap<char, Vec<ReplayableAction>>,
260
261 pub focused_vim: Option<WeakEntity<Vim>>,
262
263 pub marks: HashMap<EntityId, Entity<MarksState>>,
264}
265
266pub struct MarksState {
267 workspace: WeakEntity<Workspace>,
268
269 multibuffer_marks: HashMap<EntityId, HashMap<String, Vec<Anchor>>>,
270 buffer_marks: HashMap<BufferId, HashMap<String, Vec<text::Anchor>>>,
271 watched_buffers: HashMap<BufferId, (MarkLocation, Subscription, Subscription)>,
272
273 serialized_marks: HashMap<Arc<Path>, HashMap<String, Vec<Point>>>,
274 global_marks: HashMap<String, MarkLocation>,
275
276 _subscription: Subscription,
277}
278
279#[derive(Debug, PartialEq, Eq, Clone)]
280pub enum MarkLocation {
281 Buffer(EntityId),
282 Path(Arc<Path>),
283}
284
285pub enum Mark {
286 Local(Vec<Anchor>),
287 Buffer(EntityId, Vec<Anchor>),
288 Path(Arc<Path>, Vec<Point>),
289}
290
291impl MarksState {
292 pub fn new(workspace: &Workspace, cx: &mut App) -> Entity<MarksState> {
293 cx.new(|cx| {
294 let buffer_store = workspace.project().read(cx).buffer_store().clone();
295 let subscription = cx.subscribe(&buffer_store, move |this: &mut Self, _, event, cx| {
296 if let project::buffer_store::BufferStoreEvent::BufferAdded(buffer) = event {
297 this.on_buffer_loaded(buffer, cx);
298 }
299 });
300
301 let mut this = Self {
302 workspace: workspace.weak_handle(),
303 multibuffer_marks: HashMap::default(),
304 buffer_marks: HashMap::default(),
305 watched_buffers: HashMap::default(),
306 serialized_marks: HashMap::default(),
307 global_marks: HashMap::default(),
308 _subscription: subscription,
309 };
310
311 this.load(cx);
312 this
313 })
314 }
315
316 fn workspace_id(&self, cx: &App) -> Option<WorkspaceId> {
317 self.workspace
318 .read_with(cx, |workspace, _| workspace.database_id())
319 .ok()
320 .flatten()
321 }
322
323 fn project(&self, cx: &App) -> Option<Entity<Project>> {
324 self.workspace
325 .read_with(cx, |workspace, _| workspace.project().clone())
326 .ok()
327 }
328
329 fn load(&mut self, cx: &mut Context<Self>) {
330 cx.spawn(async move |this, cx| {
331 let Some(workspace_id) = this.update(cx, |this, cx| this.workspace_id(cx))? else {
332 return Ok(());
333 };
334 let (marks, paths) = cx
335 .background_spawn(async move {
336 let marks = DB.get_marks(workspace_id)?;
337 let paths = DB.get_global_marks_paths(workspace_id)?;
338 anyhow::Ok((marks, paths))
339 })
340 .await?;
341 this.update(cx, |this, cx| this.loaded(marks, paths, cx))
342 })
343 .detach_and_log_err(cx);
344 }
345
346 fn loaded(
347 &mut self,
348 marks: Vec<SerializedMark>,
349 global_mark_paths: Vec<(String, Arc<Path>)>,
350 cx: &mut Context<Self>,
351 ) {
352 let Some(project) = self.project(cx) else {
353 return;
354 };
355
356 for mark in marks {
357 self.serialized_marks
358 .entry(mark.path)
359 .or_default()
360 .insert(mark.name, mark.points);
361 }
362
363 for (name, path) in global_mark_paths {
364 self.global_marks
365 .insert(name, MarkLocation::Path(path.clone()));
366
367 let project_path = project
368 .read(cx)
369 .worktrees(cx)
370 .filter_map(|worktree| {
371 let relative = path.strip_prefix(worktree.read(cx).abs_path()).ok()?;
372 let path = RelPath::new(relative, worktree.read(cx).path_style()).log_err()?;
373 Some(ProjectPath {
374 worktree_id: worktree.read(cx).id(),
375 path: path.into_arc(),
376 })
377 })
378 .next();
379 if let Some(buffer) = project_path
380 .and_then(|project_path| project.read(cx).get_open_buffer(&project_path, cx))
381 {
382 self.on_buffer_loaded(&buffer, cx)
383 }
384 }
385 }
386
387 pub fn on_buffer_loaded(&mut self, buffer_handle: &Entity<Buffer>, cx: &mut Context<Self>) {
388 let Some(project) = self.project(cx) else {
389 return;
390 };
391 let Some(project_path) = buffer_handle.read(cx).project_path(cx) else {
392 return;
393 };
394 let Some(abs_path) = project.read(cx).absolute_path(&project_path, cx) else {
395 return;
396 };
397 let abs_path: Arc<Path> = abs_path.into();
398
399 let Some(serialized_marks) = self.serialized_marks.get(&abs_path) else {
400 return;
401 };
402
403 let mut loaded_marks = HashMap::default();
404 let buffer = buffer_handle.read(cx);
405 for (name, points) in serialized_marks.iter() {
406 loaded_marks.insert(
407 name.clone(),
408 points
409 .iter()
410 .map(|point| buffer.anchor_before(buffer.clip_point(*point, Bias::Left)))
411 .collect(),
412 );
413 }
414 self.buffer_marks.insert(buffer.remote_id(), loaded_marks);
415 self.watch_buffer(MarkLocation::Path(abs_path), buffer_handle, cx)
416 }
417
418 fn serialize_buffer_marks(
419 &mut self,
420 path: Arc<Path>,
421 buffer: &Entity<Buffer>,
422 cx: &mut Context<Self>,
423 ) {
424 let new_points: HashMap<String, Vec<Point>> =
425 if let Some(anchors) = self.buffer_marks.get(&buffer.read(cx).remote_id()) {
426 anchors
427 .iter()
428 .map(|(name, anchors)| {
429 (
430 name.clone(),
431 buffer
432 .read(cx)
433 .summaries_for_anchors::<Point, _>(anchors)
434 .collect(),
435 )
436 })
437 .collect()
438 } else {
439 HashMap::default()
440 };
441 let old_points = self.serialized_marks.get(&path);
442 if old_points == Some(&new_points) {
443 return;
444 }
445 let mut to_write = HashMap::default();
446
447 for (key, value) in &new_points {
448 if self.is_global_mark(key)
449 && self.global_marks.get(key) != Some(&MarkLocation::Path(path.clone()))
450 {
451 if let Some(workspace_id) = self.workspace_id(cx) {
452 let path = path.clone();
453 let key = key.clone();
454 cx.background_spawn(async move {
455 DB.set_global_mark_path(workspace_id, key, path).await
456 })
457 .detach_and_log_err(cx);
458 }
459
460 self.global_marks
461 .insert(key.clone(), MarkLocation::Path(path.clone()));
462 }
463 if old_points.and_then(|o| o.get(key)) != Some(value) {
464 to_write.insert(key.clone(), value.clone());
465 }
466 }
467
468 self.serialized_marks.insert(path.clone(), new_points);
469
470 if let Some(workspace_id) = self.workspace_id(cx) {
471 cx.background_spawn(async move {
472 DB.set_marks(workspace_id, path.clone(), to_write).await?;
473 anyhow::Ok(())
474 })
475 .detach_and_log_err(cx);
476 }
477 }
478
479 fn is_global_mark(&self, key: &str) -> bool {
480 key.chars()
481 .next()
482 .is_some_and(|c| c.is_uppercase() || c.is_digit(10))
483 }
484
485 fn rename_buffer(
486 &mut self,
487 old_path: MarkLocation,
488 new_path: Arc<Path>,
489 buffer: &Entity<Buffer>,
490 cx: &mut Context<Self>,
491 ) {
492 if let MarkLocation::Buffer(entity_id) = old_path
493 && let Some(old_marks) = self.multibuffer_marks.remove(&entity_id)
494 {
495 let buffer_marks = old_marks
496 .into_iter()
497 .map(|(k, v)| (k, v.into_iter().map(|anchor| anchor.text_anchor).collect()))
498 .collect();
499 self.buffer_marks
500 .insert(buffer.read(cx).remote_id(), buffer_marks);
501 }
502 self.watch_buffer(MarkLocation::Path(new_path.clone()), buffer, cx);
503 self.serialize_buffer_marks(new_path, buffer, cx);
504 }
505
506 fn path_for_buffer(&self, buffer: &Entity<Buffer>, cx: &App) -> Option<Arc<Path>> {
507 let project_path = buffer.read(cx).project_path(cx)?;
508 let project = self.project(cx)?;
509 let abs_path = project.read(cx).absolute_path(&project_path, cx)?;
510 Some(abs_path.into())
511 }
512
513 fn points_at(
514 &self,
515 location: &MarkLocation,
516 multi_buffer: &Entity<MultiBuffer>,
517 cx: &App,
518 ) -> bool {
519 match location {
520 MarkLocation::Buffer(entity_id) => entity_id == &multi_buffer.entity_id(),
521 MarkLocation::Path(path) => {
522 let Some(singleton) = multi_buffer.read(cx).as_singleton() else {
523 return false;
524 };
525 self.path_for_buffer(&singleton, cx).as_ref() == Some(path)
526 }
527 }
528 }
529
530 pub fn watch_buffer(
531 &mut self,
532 mark_location: MarkLocation,
533 buffer_handle: &Entity<Buffer>,
534 cx: &mut Context<Self>,
535 ) {
536 let on_change = cx.subscribe(buffer_handle, move |this, buffer, event, cx| match event {
537 BufferEvent::Edited => {
538 if let Some(path) = this.path_for_buffer(&buffer, cx) {
539 this.serialize_buffer_marks(path, &buffer, cx);
540 }
541 }
542 BufferEvent::FileHandleChanged => {
543 let buffer_id = buffer.read(cx).remote_id();
544 if let Some(old_path) = this
545 .watched_buffers
546 .get(&buffer_id.clone())
547 .map(|(path, _, _)| path.clone())
548 && let Some(new_path) = this.path_for_buffer(&buffer, cx)
549 {
550 this.rename_buffer(old_path, new_path, &buffer, cx)
551 }
552 }
553 _ => {}
554 });
555
556 let on_release = cx.observe_release(buffer_handle, |this, buffer, _| {
557 this.watched_buffers.remove(&buffer.remote_id());
558 this.buffer_marks.remove(&buffer.remote_id());
559 });
560
561 self.watched_buffers.insert(
562 buffer_handle.read(cx).remote_id(),
563 (mark_location, on_change, on_release),
564 );
565 }
566
567 pub fn set_mark(
568 &mut self,
569 name: String,
570 multibuffer: &Entity<MultiBuffer>,
571 anchors: Vec<Anchor>,
572 cx: &mut Context<Self>,
573 ) {
574 let buffer = multibuffer.read(cx).as_singleton();
575 let abs_path = buffer.as_ref().and_then(|b| self.path_for_buffer(b, cx));
576
577 let Some(abs_path) = abs_path else {
578 self.multibuffer_marks
579 .entry(multibuffer.entity_id())
580 .or_default()
581 .insert(name.clone(), anchors);
582 if self.is_global_mark(&name) {
583 self.global_marks
584 .insert(name, MarkLocation::Buffer(multibuffer.entity_id()));
585 }
586 if let Some(buffer) = buffer {
587 let buffer_id = buffer.read(cx).remote_id();
588 if !self.watched_buffers.contains_key(&buffer_id) {
589 self.watch_buffer(MarkLocation::Buffer(multibuffer.entity_id()), &buffer, cx)
590 }
591 }
592 return;
593 };
594 let Some(buffer) = buffer else {
595 return;
596 };
597
598 let buffer_id = buffer.read(cx).remote_id();
599 self.buffer_marks.entry(buffer_id).or_default().insert(
600 name,
601 anchors
602 .into_iter()
603 .map(|anchor| anchor.text_anchor)
604 .collect(),
605 );
606 if !self.watched_buffers.contains_key(&buffer_id) {
607 self.watch_buffer(MarkLocation::Path(abs_path.clone()), &buffer, cx)
608 }
609 self.serialize_buffer_marks(abs_path, &buffer, cx)
610 }
611
612 pub fn get_mark(
613 &self,
614 name: &str,
615 multi_buffer: &Entity<MultiBuffer>,
616 cx: &App,
617 ) -> Option<Mark> {
618 let target = self.global_marks.get(name);
619
620 if !self.is_global_mark(name) || target.is_some_and(|t| self.points_at(t, multi_buffer, cx))
621 {
622 if let Some(anchors) = self.multibuffer_marks.get(&multi_buffer.entity_id()) {
623 return Some(Mark::Local(anchors.get(name)?.clone()));
624 }
625
626 let singleton = multi_buffer.read(cx).as_singleton()?;
627 let excerpt_id = *multi_buffer.read(cx).excerpt_ids().first()?;
628 let buffer_id = singleton.read(cx).remote_id();
629 if let Some(anchors) = self.buffer_marks.get(&buffer_id) {
630 let text_anchors = anchors.get(name)?;
631 let anchors = text_anchors
632 .iter()
633 .map(|anchor| Anchor::in_buffer(excerpt_id, buffer_id, *anchor))
634 .collect();
635 return Some(Mark::Local(anchors));
636 }
637 }
638
639 match target? {
640 MarkLocation::Buffer(entity_id) => {
641 let anchors = self.multibuffer_marks.get(entity_id)?;
642 Some(Mark::Buffer(*entity_id, anchors.get(name)?.clone()))
643 }
644 MarkLocation::Path(path) => {
645 let points = self.serialized_marks.get(path)?;
646 Some(Mark::Path(path.clone(), points.get(name)?.clone()))
647 }
648 }
649 }
650 pub fn delete_mark(
651 &mut self,
652 mark_name: String,
653 multi_buffer: &Entity<MultiBuffer>,
654 cx: &mut Context<Self>,
655 ) {
656 let path = if let Some(target) = self.global_marks.get(&mark_name.clone()) {
657 let name = mark_name.clone();
658 if let Some(workspace_id) = self.workspace_id(cx) {
659 cx.background_spawn(async move {
660 DB.delete_global_marks_path(workspace_id, name).await
661 })
662 .detach_and_log_err(cx);
663 }
664 self.buffer_marks.iter_mut().for_each(|(_, m)| {
665 m.remove(&mark_name.clone());
666 });
667
668 match target {
669 MarkLocation::Buffer(entity_id) => {
670 self.multibuffer_marks
671 .get_mut(entity_id)
672 .map(|m| m.remove(&mark_name.clone()));
673 return;
674 }
675 MarkLocation::Path(path) => path.clone(),
676 }
677 } else {
678 self.multibuffer_marks
679 .get_mut(&multi_buffer.entity_id())
680 .map(|m| m.remove(&mark_name.clone()));
681
682 if let Some(singleton) = multi_buffer.read(cx).as_singleton() {
683 let buffer_id = singleton.read(cx).remote_id();
684 self.buffer_marks
685 .get_mut(&buffer_id)
686 .map(|m| m.remove(&mark_name.clone()));
687 let Some(path) = self.path_for_buffer(&singleton, cx) else {
688 return;
689 };
690 path
691 } else {
692 return;
693 }
694 };
695 self.global_marks.remove(&mark_name);
696 self.serialized_marks
697 .get_mut(&path)
698 .map(|m| m.remove(&mark_name.clone()));
699 if let Some(workspace_id) = self.workspace_id(cx) {
700 cx.background_spawn(async move { DB.delete_mark(workspace_id, path, mark_name).await })
701 .detach_and_log_err(cx);
702 }
703 }
704}
705
706impl Global for VimGlobals {}
707
708impl VimGlobals {
709 pub(crate) fn register(cx: &mut App) {
710 cx.set_global(VimGlobals::default());
711
712 cx.observe_keystrokes(|event, _, cx| {
713 let Some(action) = event.action.as_ref().map(|action| action.boxed_clone()) else {
714 return;
715 };
716 Vim::globals(cx).observe_action(action.boxed_clone())
717 })
718 .detach();
719
720 cx.observe_new(|workspace: &mut Workspace, window, _| {
721 RegistersView::register(workspace, window);
722 })
723 .detach();
724
725 cx.observe_new(move |workspace: &mut Workspace, window, _| {
726 MarksView::register(workspace, window);
727 })
728 .detach();
729
730 let mut was_enabled = None;
731
732 cx.observe_global::<SettingsStore>(move |cx| {
733 let is_enabled = Vim::enabled(cx);
734 if was_enabled == Some(is_enabled) {
735 return;
736 }
737 was_enabled = Some(is_enabled);
738 if is_enabled {
739 KeyBinding::set_vim_mode(cx, true);
740 CommandPaletteFilter::update_global(cx, |filter, _| {
741 filter.show_namespace(Vim::NAMESPACE);
742 });
743 GlobalCommandPaletteInterceptor::set(cx, command_interceptor);
744 for window in cx.windows() {
745 if let Some(workspace) = window.downcast::<Workspace>() {
746 workspace
747 .update(cx, |workspace, _, cx| {
748 Vim::update_globals(cx, |globals, cx| {
749 globals.register_workspace(workspace, cx)
750 });
751 })
752 .ok();
753 }
754 }
755 } else {
756 KeyBinding::set_vim_mode(cx, false);
757 *Vim::globals(cx) = VimGlobals::default();
758 GlobalCommandPaletteInterceptor::clear(cx);
759 CommandPaletteFilter::update_global(cx, |filter, _| {
760 filter.hide_namespace(Vim::NAMESPACE);
761 });
762 }
763 })
764 .detach();
765 cx.observe_new(|workspace: &mut Workspace, _, cx| {
766 Vim::update_globals(cx, |globals, cx| globals.register_workspace(workspace, cx));
767 })
768 .detach()
769 }
770
771 fn register_workspace(&mut self, workspace: &Workspace, cx: &mut Context<Workspace>) {
772 let entity_id = cx.entity_id();
773 self.marks.insert(entity_id, MarksState::new(workspace, cx));
774 cx.observe_release(&cx.entity(), move |_, _, cx| {
775 Vim::update_globals(cx, |globals, _| {
776 globals.marks.remove(&entity_id);
777 })
778 })
779 .detach();
780 }
781
782 pub(crate) fn write_registers(
783 &mut self,
784 content: Register,
785 register: Option<char>,
786 is_yank: bool,
787 kind: MotionKind,
788 cx: &mut Context<Editor>,
789 ) {
790 if let Some(register) = register {
791 let lower = register.to_lowercase().next().unwrap_or(register);
792 if lower != register {
793 let current = self.registers.entry(lower).or_default();
794 current.text = (current.text.to_string() + &content.text).into();
795 // not clear how to support appending to registers with multiple cursors
796 current.clipboard_selections.take();
797 let yanked = current.clone();
798 self.registers.insert('"', yanked);
799 } else {
800 match lower {
801 '_' | ':' | '.' | '%' | '#' | '=' | '/' => {}
802 '+' => {
803 self.registers.insert('"', content.clone());
804 cx.write_to_clipboard(content.into());
805 }
806 '*' => {
807 self.registers.insert('"', content.clone());
808 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
809 cx.write_to_primary(content.into());
810 #[cfg(not(any(target_os = "linux", target_os = "freebsd")))]
811 cx.write_to_clipboard(content.into());
812 }
813 '"' => {
814 self.registers.insert('"', content.clone());
815 self.registers.insert('0', content);
816 }
817 _ => {
818 self.registers.insert('"', content.clone());
819 self.registers.insert(lower, content);
820 }
821 }
822 }
823 } else {
824 let setting = VimSettings::get_global(cx).use_system_clipboard;
825 if setting == UseSystemClipboard::Always
826 || setting == UseSystemClipboard::OnYank && is_yank
827 {
828 self.last_yank.replace(content.text.clone());
829 cx.write_to_clipboard(content.clone().into());
830 } else {
831 if let Some(text) = cx.read_from_clipboard().and_then(|i| i.text()) {
832 self.last_yank.replace(text.into());
833 }
834 }
835 self.registers.insert('"', content.clone());
836 if is_yank {
837 self.registers.insert('0', content);
838 } else {
839 let contains_newline = content.text.contains('\n');
840 if !contains_newline {
841 self.registers.insert('-', content.clone());
842 }
843 if kind.linewise() || contains_newline {
844 let mut content = content;
845 for i in '1'..='9' {
846 if let Some(moved) = self.registers.insert(i, content) {
847 content = moved;
848 } else {
849 break;
850 }
851 }
852 }
853 }
854 }
855 }
856
857 pub(crate) fn read_register(
858 &self,
859 register: Option<char>,
860 editor: Option<&mut Editor>,
861 cx: &mut App,
862 ) -> Option<Register> {
863 let Some(register) = register.filter(|reg| *reg != '"') else {
864 let setting = VimSettings::get_global(cx).use_system_clipboard;
865 return match setting {
866 UseSystemClipboard::Always => cx.read_from_clipboard().map(|item| item.into()),
867 UseSystemClipboard::OnYank if self.system_clipboard_is_newer(cx) => {
868 cx.read_from_clipboard().map(|item| item.into())
869 }
870 _ => self.registers.get(&'"').cloned(),
871 };
872 };
873 let lower = register.to_lowercase().next().unwrap_or(register);
874 match lower {
875 '_' | ':' | '.' | '#' | '=' => None,
876 '+' => cx.read_from_clipboard().map(|item| item.into()),
877 '*' => {
878 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
879 {
880 cx.read_from_primary().map(|item| item.into())
881 }
882 #[cfg(not(any(target_os = "linux", target_os = "freebsd")))]
883 {
884 cx.read_from_clipboard().map(|item| item.into())
885 }
886 }
887 '%' => editor.and_then(|editor| {
888 let selection = editor
889 .selections
890 .newest::<Point>(&editor.display_snapshot(cx));
891 if let Some((_, buffer, _)) = editor
892 .buffer()
893 .read(cx)
894 .excerpt_containing(selection.head(), cx)
895 {
896 buffer
897 .read(cx)
898 .file()
899 .map(|file| file.path().display(file.path_style(cx)).into_owned().into())
900 } else {
901 None
902 }
903 }),
904 _ => self.registers.get(&lower).cloned(),
905 }
906 }
907
908 fn system_clipboard_is_newer(&self, cx: &App) -> bool {
909 cx.read_from_clipboard().is_some_and(|item| {
910 match (item.text().as_deref(), &self.last_yank) {
911 (Some(new), Some(last)) => last.as_ref() != new,
912 (Some(_), None) => true,
913 (None, _) => false,
914 }
915 })
916 }
917
918 pub fn observe_action(&mut self, action: Box<dyn Action>) {
919 if self.dot_recording {
920 self.recording_actions
921 .push(ReplayableAction::Action(action.boxed_clone()));
922
923 if self.stop_recording_after_next_action {
924 self.dot_recording = false;
925 self.recorded_actions = std::mem::take(&mut self.recording_actions);
926 self.stop_recording_after_next_action = false;
927 }
928 }
929 if self.replayer.is_none()
930 && let Some(recording_register) = self.recording_register
931 {
932 self.recordings
933 .entry(recording_register)
934 .or_default()
935 .push(ReplayableAction::Action(action));
936 }
937 }
938
939 pub fn observe_insertion(&mut self, text: &Arc<str>, range_to_replace: Option<Range<isize>>) {
940 if self.ignore_current_insertion {
941 self.ignore_current_insertion = false;
942 return;
943 }
944 if self.dot_recording {
945 self.recording_actions.push(ReplayableAction::Insertion {
946 text: text.clone(),
947 utf16_range_to_replace: range_to_replace.clone(),
948 });
949 if self.stop_recording_after_next_action {
950 self.dot_recording = false;
951 self.recorded_actions = std::mem::take(&mut self.recording_actions);
952 self.stop_recording_after_next_action = false;
953 }
954 }
955 if let Some(recording_register) = self.recording_register {
956 self.recordings.entry(recording_register).or_default().push(
957 ReplayableAction::Insertion {
958 text: text.clone(),
959 utf16_range_to_replace: range_to_replace,
960 },
961 );
962 }
963 }
964
965 pub fn focused_vim(&self) -> Option<Entity<Vim>> {
966 self.focused_vim.as_ref().and_then(|vim| vim.upgrade())
967 }
968}
969
970impl Vim {
971 pub fn globals(cx: &mut App) -> &mut VimGlobals {
972 cx.global_mut::<VimGlobals>()
973 }
974
975 pub fn update_globals<C, R>(cx: &mut C, f: impl FnOnce(&mut VimGlobals, &mut C) -> R) -> R
976 where
977 C: BorrowMut<App>,
978 {
979 cx.update_global(f)
980 }
981}
982
983#[derive(Debug)]
984pub enum ReplayableAction {
985 Action(Box<dyn Action>),
986 Insertion {
987 text: Arc<str>,
988 utf16_range_to_replace: Option<Range<isize>>,
989 },
990}
991
992impl Clone for ReplayableAction {
993 fn clone(&self) -> Self {
994 match self {
995 Self::Action(action) => Self::Action(action.boxed_clone()),
996 Self::Insertion {
997 text,
998 utf16_range_to_replace,
999 } => Self::Insertion {
1000 text: text.clone(),
1001 utf16_range_to_replace: utf16_range_to_replace.clone(),
1002 },
1003 }
1004 }
1005}
1006
1007#[derive(Clone, Default, Debug)]
1008pub struct SearchState {
1009 pub direction: Direction,
1010 pub count: usize,
1011
1012 pub prior_selections: Vec<Range<Anchor>>,
1013 pub prior_operator: Option<Operator>,
1014 pub prior_mode: Mode,
1015 pub helix_select: bool,
1016}
1017
1018impl Operator {
1019 pub fn id(&self) -> &'static str {
1020 match self {
1021 Operator::Object {
1022 scope: ObjectScope::Inside,
1023 } => "i",
1024 Operator::Object {
1025 scope: ObjectScope::Around | ObjectScope::AroundTrimmed,
1026 } => "a",
1027 Operator::Change => "c",
1028 Operator::Delete => "d",
1029 Operator::Yank => "y",
1030 Operator::Replace => "r",
1031 Operator::Digraph { .. } => "^K",
1032 Operator::Literal { .. } => "^V",
1033 Operator::FindForward { before: false, .. } => "f",
1034 Operator::FindForward { before: true, .. } => "t",
1035 Operator::Sneak { .. } => "s",
1036 Operator::SneakBackward { .. } => "S",
1037 Operator::FindBackward { after: false, .. } => "F",
1038 Operator::FindBackward { after: true, .. } => "T",
1039 Operator::AddSurrounds { .. } => "ys",
1040 Operator::ChangeSurrounds { .. } => "cs",
1041 Operator::DeleteSurrounds => "ds",
1042 Operator::Mark => "m",
1043 Operator::Jump { line: true } => "'",
1044 Operator::Jump { line: false } => "`",
1045 Operator::Indent => ">",
1046 Operator::AutoIndent => "eq",
1047 Operator::ShellCommand => "sh",
1048 Operator::Rewrap => "gq",
1049 Operator::ReplaceWithRegister => "gR",
1050 Operator::Exchange => "cx",
1051 Operator::Outdent => "<",
1052 Operator::Uppercase => "gU",
1053 Operator::Lowercase => "gu",
1054 Operator::OppositeCase => "g~",
1055 Operator::Rot13 => "g?",
1056 Operator::Rot47 => "g?",
1057 Operator::Register => "\"",
1058 Operator::RecordRegister => "q",
1059 Operator::ReplayRegister => "@",
1060 Operator::ToggleComments => "gc",
1061 Operator::HelixMatch => "helix_m",
1062 Operator::HelixNext { .. } => "helix_next",
1063 Operator::HelixPrevious { .. } => "helix_previous",
1064 }
1065 }
1066
1067 pub fn status(&self) -> String {
1068 fn make_visible(c: &str) -> &str {
1069 match c {
1070 "\n" => "enter",
1071 "\t" => "tab",
1072 " " => "space",
1073 c => c,
1074 }
1075 }
1076 match self {
1077 Operator::Digraph {
1078 first_char: Some(first_char),
1079 } => format!("^K{}", make_visible(&first_char.to_string())),
1080 Operator::Literal {
1081 prefix: Some(prefix),
1082 } => format!("^V{}", make_visible(prefix)),
1083 Operator::AutoIndent => "=".to_string(),
1084 Operator::ShellCommand => "=".to_string(),
1085 Operator::HelixMatch => "m".to_string(),
1086 Operator::HelixNext { .. } => "]".to_string(),
1087 Operator::HelixPrevious { .. } => "[".to_string(),
1088 _ => self.id().to_string(),
1089 }
1090 }
1091
1092 pub fn is_waiting(&self, mode: Mode) -> bool {
1093 match self {
1094 Operator::AddSurrounds { target } => target.is_some() || mode.is_visual(),
1095 Operator::FindForward { .. }
1096 | Operator::Mark
1097 | Operator::Jump { .. }
1098 | Operator::FindBackward { .. }
1099 | Operator::Sneak { .. }
1100 | Operator::SneakBackward { .. }
1101 | Operator::Register
1102 | Operator::RecordRegister
1103 | Operator::ReplayRegister
1104 | Operator::Replace
1105 | Operator::Digraph { .. }
1106 | Operator::Literal { .. }
1107 | Operator::ChangeSurrounds {
1108 target: Some(_), ..
1109 }
1110 | Operator::DeleteSurrounds => true,
1111 Operator::Change
1112 | Operator::Delete
1113 | Operator::Yank
1114 | Operator::Rewrap
1115 | Operator::Indent
1116 | Operator::Outdent
1117 | Operator::AutoIndent
1118 | Operator::ShellCommand
1119 | Operator::Lowercase
1120 | Operator::Uppercase
1121 | Operator::Rot13
1122 | Operator::Rot47
1123 | Operator::ReplaceWithRegister
1124 | Operator::Exchange
1125 | Operator::Object { .. }
1126 | Operator::ChangeSurrounds { target: None, .. }
1127 | Operator::OppositeCase
1128 | Operator::ToggleComments
1129 | Operator::HelixMatch
1130 | Operator::HelixNext { .. }
1131 | Operator::HelixPrevious { .. } => false,
1132 }
1133 }
1134
1135 pub fn starts_dot_recording(&self) -> bool {
1136 match self {
1137 Operator::Change
1138 | Operator::Delete
1139 | Operator::Replace
1140 | Operator::Indent
1141 | Operator::Outdent
1142 | Operator::AutoIndent
1143 | Operator::Lowercase
1144 | Operator::Uppercase
1145 | Operator::OppositeCase
1146 | Operator::Rot13
1147 | Operator::Rot47
1148 | Operator::ToggleComments
1149 | Operator::ReplaceWithRegister
1150 | Operator::Rewrap
1151 | Operator::ShellCommand
1152 | Operator::AddSurrounds { target: None }
1153 | Operator::ChangeSurrounds { target: None, .. }
1154 | Operator::DeleteSurrounds
1155 | Operator::Exchange
1156 | Operator::HelixNext { .. }
1157 | Operator::HelixPrevious { .. } => true,
1158 Operator::Yank
1159 | Operator::Object { .. }
1160 | Operator::FindForward { .. }
1161 | Operator::FindBackward { .. }
1162 | Operator::Sneak { .. }
1163 | Operator::SneakBackward { .. }
1164 | Operator::Mark
1165 | Operator::Digraph { .. }
1166 | Operator::Literal { .. }
1167 | Operator::AddSurrounds { .. }
1168 | Operator::ChangeSurrounds { .. }
1169 | Operator::Jump { .. }
1170 | Operator::Register
1171 | Operator::RecordRegister
1172 | Operator::ReplayRegister
1173 | Operator::HelixMatch => false,
1174 }
1175 }
1176}
1177
1178struct RegisterMatch {
1179 name: char,
1180 contents: SharedString,
1181}
1182
1183pub struct RegistersViewDelegate {
1184 selected_index: usize,
1185 matches: Vec<RegisterMatch>,
1186}
1187
1188impl PickerDelegate for RegistersViewDelegate {
1189 type ListItem = Div;
1190
1191 fn match_count(&self) -> usize {
1192 self.matches.len()
1193 }
1194
1195 fn selected_index(&self) -> usize {
1196 self.selected_index
1197 }
1198
1199 fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context<Picker<Self>>) {
1200 self.selected_index = ix;
1201 cx.notify();
1202 }
1203
1204 fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
1205 Arc::default()
1206 }
1207
1208 fn update_matches(
1209 &mut self,
1210 _: String,
1211 _: &mut Window,
1212 _: &mut Context<Picker<Self>>,
1213 ) -> gpui::Task<()> {
1214 Task::ready(())
1215 }
1216
1217 fn confirm(&mut self, _: bool, _: &mut Window, _: &mut Context<Picker<Self>>) {}
1218
1219 fn dismissed(&mut self, _: &mut Window, _: &mut Context<Picker<Self>>) {}
1220
1221 fn render_match(
1222 &self,
1223 ix: usize,
1224 selected: bool,
1225 _: &mut Window,
1226 cx: &mut Context<Picker<Self>>,
1227 ) -> Option<Self::ListItem> {
1228 let register_match = self.matches.get(ix)?;
1229
1230 let mut output = String::new();
1231 let mut runs = Vec::new();
1232 output.push('"');
1233 output.push(register_match.name);
1234 runs.push((
1235 0..output.len(),
1236 HighlightStyle::color(cx.theme().colors().text_accent),
1237 ));
1238 output.push(' ');
1239 output.push(' ');
1240 let mut base = output.len();
1241 for (ix, c) in register_match.contents.char_indices() {
1242 if ix > 100 {
1243 break;
1244 }
1245 let replace = match c {
1246 '\t' => Some("\\t".to_string()),
1247 '\n' => Some("\\n".to_string()),
1248 '\r' => Some("\\r".to_string()),
1249 c if is_invisible(c) => {
1250 if c <= '\x1f' {
1251 replacement(c).map(|s| s.to_string())
1252 } else {
1253 Some(format!("\\u{:04X}", c as u32))
1254 }
1255 }
1256 _ => None,
1257 };
1258 let Some(replace) = replace else {
1259 output.push(c);
1260 continue;
1261 };
1262 output.push_str(&replace);
1263 runs.push((
1264 base + ix..base + ix + replace.len(),
1265 HighlightStyle::color(cx.theme().colors().text_muted),
1266 ));
1267 base += replace.len() - c.len_utf8();
1268 }
1269
1270 let theme = ThemeSettings::get_global(cx);
1271 let text_style = TextStyle {
1272 color: cx.theme().colors().editor_foreground,
1273 font_family: theme.buffer_font.family.clone(),
1274 font_features: theme.buffer_font.features.clone(),
1275 font_fallbacks: theme.buffer_font.fallbacks.clone(),
1276 font_size: theme.buffer_font_size(cx).into(),
1277 line_height: (theme.line_height() * theme.buffer_font_size(cx)).into(),
1278 font_weight: theme.buffer_font.weight,
1279 font_style: theme.buffer_font.style,
1280 ..Default::default()
1281 };
1282
1283 Some(
1284 h_flex()
1285 .when(selected, |el| el.bg(cx.theme().colors().element_selected))
1286 .font_buffer(cx)
1287 .text_buffer(cx)
1288 .h(theme.buffer_font_size(cx) * theme.line_height())
1289 .px_2()
1290 .gap_1()
1291 .child(StyledText::new(output).with_default_highlights(&text_style, runs)),
1292 )
1293 }
1294}
1295
1296pub struct RegistersView {}
1297
1298impl RegistersView {
1299 fn register(workspace: &mut Workspace, _window: Option<&mut Window>) {
1300 workspace.register_action(|workspace, _: &ToggleRegistersView, window, cx| {
1301 Self::toggle(workspace, window, cx);
1302 });
1303 }
1304
1305 pub fn toggle(workspace: &mut Workspace, window: &mut Window, cx: &mut Context<Workspace>) {
1306 let editor = workspace
1307 .active_item(cx)
1308 .and_then(|item| item.act_as::<Editor>(cx));
1309 workspace.toggle_modal(window, cx, move |window, cx| {
1310 RegistersView::new(editor, window, cx)
1311 });
1312 }
1313
1314 fn new(
1315 editor: Option<Entity<Editor>>,
1316 window: &mut Window,
1317 cx: &mut Context<Picker<RegistersViewDelegate>>,
1318 ) -> Picker<RegistersViewDelegate> {
1319 let mut matches = Vec::default();
1320 cx.update_global(|globals: &mut VimGlobals, cx| {
1321 for name in ['"', '+', '*'] {
1322 if let Some(register) = globals.read_register(Some(name), None, cx) {
1323 matches.push(RegisterMatch {
1324 name,
1325 contents: register.text.clone(),
1326 })
1327 }
1328 }
1329 if let Some(editor) = editor {
1330 let register = editor.update(cx, |editor, cx| {
1331 globals.read_register(Some('%'), Some(editor), cx)
1332 });
1333 if let Some(register) = register {
1334 matches.push(RegisterMatch {
1335 name: '%',
1336 contents: register.text,
1337 })
1338 }
1339 }
1340 for (name, register) in globals.registers.iter() {
1341 if ['"', '+', '*', '%'].contains(name) {
1342 continue;
1343 };
1344 matches.push(RegisterMatch {
1345 name: *name,
1346 contents: register.text.clone(),
1347 })
1348 }
1349 });
1350 matches.sort_by(|a, b| a.name.cmp(&b.name));
1351 let delegate = RegistersViewDelegate {
1352 selected_index: 0,
1353 matches,
1354 };
1355
1356 Picker::nonsearchable_uniform_list(delegate, window, cx)
1357 .width(rems(36.))
1358 .modal(true)
1359 }
1360}
1361
1362enum MarksMatchInfo {
1363 Path(Arc<Path>),
1364 Title(String),
1365 Content {
1366 line: String,
1367 highlights: Vec<(Range<usize>, HighlightStyle)>,
1368 },
1369}
1370
1371impl MarksMatchInfo {
1372 fn from_chunks<'a>(chunks: impl Iterator<Item = Chunk<'a>>, cx: &App) -> Self {
1373 let mut line = String::new();
1374 let mut highlights = Vec::new();
1375 let mut offset = 0;
1376 for chunk in chunks {
1377 line.push_str(chunk.text);
1378 if let Some(highlight_style) = chunk.syntax_highlight_id
1379 && let Some(highlight) = highlight_style.style(cx.theme().syntax())
1380 {
1381 highlights.push((offset..offset + chunk.text.len(), highlight))
1382 }
1383 offset += chunk.text.len();
1384 }
1385 MarksMatchInfo::Content { line, highlights }
1386 }
1387}
1388
1389struct MarksMatch {
1390 name: String,
1391 position: Point,
1392 info: MarksMatchInfo,
1393}
1394
1395pub struct MarksViewDelegate {
1396 selected_index: usize,
1397 matches: Vec<MarksMatch>,
1398 point_column_width: usize,
1399 workspace: WeakEntity<Workspace>,
1400}
1401
1402impl PickerDelegate for MarksViewDelegate {
1403 type ListItem = Div;
1404
1405 fn match_count(&self) -> usize {
1406 self.matches.len()
1407 }
1408
1409 fn selected_index(&self) -> usize {
1410 self.selected_index
1411 }
1412
1413 fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context<Picker<Self>>) {
1414 self.selected_index = ix;
1415 cx.notify();
1416 }
1417
1418 fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
1419 Arc::default()
1420 }
1421
1422 fn update_matches(
1423 &mut self,
1424 _: String,
1425 _: &mut Window,
1426 cx: &mut Context<Picker<Self>>,
1427 ) -> gpui::Task<()> {
1428 let Some(workspace) = self.workspace.upgrade() else {
1429 return Task::ready(());
1430 };
1431 cx.spawn(async move |picker, cx| {
1432 let mut matches = Vec::new();
1433 let _ = workspace.update(cx, |workspace, cx| {
1434 let entity_id = cx.entity_id();
1435 let Some(editor) = workspace
1436 .active_item(cx)
1437 .and_then(|item| item.act_as::<Editor>(cx))
1438 else {
1439 return;
1440 };
1441 let editor = editor.read(cx);
1442 let mut has_seen = HashSet::new();
1443 let Some(marks_state) = cx.global::<VimGlobals>().marks.get(&entity_id) else {
1444 return;
1445 };
1446 let marks_state = marks_state.read(cx);
1447
1448 if let Some(map) = marks_state
1449 .multibuffer_marks
1450 .get(&editor.buffer().entity_id())
1451 {
1452 for (name, anchors) in map {
1453 if has_seen.contains(name) {
1454 continue;
1455 }
1456 has_seen.insert(name.clone());
1457 let Some(anchor) = anchors.first() else {
1458 continue;
1459 };
1460
1461 let snapshot = editor.buffer().read(cx).snapshot(cx);
1462 let position = anchor.to_point(&snapshot);
1463
1464 let chunks = snapshot.chunks(
1465 Point::new(position.row, 0)
1466 ..Point::new(
1467 position.row,
1468 snapshot.line_len(MultiBufferRow(position.row)),
1469 ),
1470 true,
1471 );
1472 matches.push(MarksMatch {
1473 name: name.clone(),
1474 position,
1475 info: MarksMatchInfo::from_chunks(chunks, cx),
1476 })
1477 }
1478 }
1479
1480 if let Some(buffer) = editor.buffer().read(cx).as_singleton() {
1481 let buffer = buffer.read(cx);
1482 if let Some(map) = marks_state.buffer_marks.get(&buffer.remote_id()) {
1483 for (name, anchors) in map {
1484 if has_seen.contains(name) {
1485 continue;
1486 }
1487 has_seen.insert(name.clone());
1488 let Some(anchor) = anchors.first() else {
1489 continue;
1490 };
1491 let snapshot = buffer.snapshot();
1492 let position = anchor.to_point(&snapshot);
1493 let chunks = snapshot.chunks(
1494 Point::new(position.row, 0)
1495 ..Point::new(position.row, snapshot.line_len(position.row)),
1496 true,
1497 );
1498
1499 matches.push(MarksMatch {
1500 name: name.clone(),
1501 position,
1502 info: MarksMatchInfo::from_chunks(chunks, cx),
1503 })
1504 }
1505 }
1506 }
1507
1508 for (name, mark_location) in marks_state.global_marks.iter() {
1509 if has_seen.contains(name) {
1510 continue;
1511 }
1512 has_seen.insert(name.clone());
1513
1514 match mark_location {
1515 MarkLocation::Buffer(entity_id) => {
1516 if let Some(&anchor) = marks_state
1517 .multibuffer_marks
1518 .get(entity_id)
1519 .and_then(|map| map.get(name))
1520 .and_then(|anchors| anchors.first())
1521 {
1522 let Some((info, snapshot)) = workspace
1523 .items(cx)
1524 .filter_map(|item| item.act_as::<Editor>(cx))
1525 .map(|entity| entity.read(cx).buffer())
1526 .find(|buffer| buffer.entity_id().eq(entity_id))
1527 .map(|buffer| {
1528 (
1529 MarksMatchInfo::Title(
1530 buffer.read(cx).title(cx).to_string(),
1531 ),
1532 buffer.read(cx).snapshot(cx),
1533 )
1534 })
1535 else {
1536 continue;
1537 };
1538 matches.push(MarksMatch {
1539 name: name.clone(),
1540 position: anchor.to_point(&snapshot),
1541 info,
1542 });
1543 }
1544 }
1545 MarkLocation::Path(path) => {
1546 if let Some(&position) = marks_state
1547 .serialized_marks
1548 .get(path.as_ref())
1549 .and_then(|map| map.get(name))
1550 .and_then(|points| points.first())
1551 {
1552 let info = MarksMatchInfo::Path(path.clone());
1553 matches.push(MarksMatch {
1554 name: name.clone(),
1555 position,
1556 info,
1557 });
1558 }
1559 }
1560 }
1561 }
1562 });
1563 let _ = picker.update(cx, |picker, cx| {
1564 matches.sort_by_key(|a| {
1565 (
1566 a.name.chars().next().map(|c| c.is_ascii_uppercase()),
1567 a.name.clone(),
1568 )
1569 });
1570 let digits = matches
1571 .iter()
1572 .map(|m| (m.position.row + 1).ilog10() + (m.position.column + 1).ilog10())
1573 .max()
1574 .unwrap_or_default();
1575 picker.delegate.matches = matches;
1576 picker.delegate.point_column_width = (digits + 4) as usize;
1577 cx.notify();
1578 });
1579 })
1580 }
1581
1582 fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
1583 let Some(vim) = self
1584 .workspace
1585 .upgrade()
1586 .map(|w| w.read(cx))
1587 .and_then(|w| w.focused_pane(window, cx).read(cx).active_item())
1588 .and_then(|item| item.act_as::<Editor>(cx))
1589 .and_then(|editor| editor.read(cx).addon::<VimAddon>().cloned())
1590 .map(|addon| addon.entity)
1591 else {
1592 return;
1593 };
1594 let Some(text): Option<Arc<str>> = self
1595 .matches
1596 .get(self.selected_index)
1597 .map(|m| Arc::from(m.name.to_string().into_boxed_str()))
1598 else {
1599 return;
1600 };
1601 vim.update(cx, |vim, cx| {
1602 vim.jump(text, false, false, window, cx);
1603 });
1604
1605 cx.emit(DismissEvent);
1606 }
1607
1608 fn dismissed(&mut self, _: &mut Window, _: &mut Context<Picker<Self>>) {}
1609
1610 fn render_match(
1611 &self,
1612 ix: usize,
1613 selected: bool,
1614 _: &mut Window,
1615 cx: &mut Context<Picker<Self>>,
1616 ) -> Option<Self::ListItem> {
1617 let mark_match = self.matches.get(ix)?;
1618
1619 let mut left_output = String::new();
1620 let mut left_runs = Vec::new();
1621 left_output.push('`');
1622 left_output.push_str(&mark_match.name);
1623 left_runs.push((
1624 0..left_output.len(),
1625 HighlightStyle::color(cx.theme().colors().text_accent),
1626 ));
1627 left_output.push(' ');
1628 left_output.push(' ');
1629 let point_column = format!(
1630 "{},{}",
1631 mark_match.position.row + 1,
1632 mark_match.position.column + 1
1633 );
1634 left_output.push_str(&point_column);
1635 if let Some(padding) = self.point_column_width.checked_sub(point_column.len()) {
1636 left_output.push_str(&" ".repeat(padding));
1637 }
1638
1639 let (right_output, right_runs): (String, Vec<_>) = match &mark_match.info {
1640 MarksMatchInfo::Path(path) => {
1641 let s = path.to_string_lossy().into_owned();
1642 (
1643 s.clone(),
1644 vec![(0..s.len(), HighlightStyle::color(cx.theme().colors().text))],
1645 )
1646 }
1647 MarksMatchInfo::Title(title) => (
1648 title.clone(),
1649 vec![(
1650 0..title.len(),
1651 HighlightStyle::color(cx.theme().colors().text),
1652 )],
1653 ),
1654 MarksMatchInfo::Content { line, highlights } => (line.clone(), highlights.clone()),
1655 };
1656
1657 let theme = ThemeSettings::get_global(cx);
1658 let text_style = TextStyle {
1659 color: cx.theme().colors().editor_foreground,
1660 font_family: theme.buffer_font.family.clone(),
1661 font_features: theme.buffer_font.features.clone(),
1662 font_fallbacks: theme.buffer_font.fallbacks.clone(),
1663 font_size: theme.buffer_font_size(cx).into(),
1664 line_height: (theme.line_height() * theme.buffer_font_size(cx)).into(),
1665 font_weight: theme.buffer_font.weight,
1666 font_style: theme.buffer_font.style,
1667 ..Default::default()
1668 };
1669
1670 Some(
1671 h_flex()
1672 .when(selected, |el| el.bg(cx.theme().colors().element_selected))
1673 .font_buffer(cx)
1674 .text_buffer(cx)
1675 .h(theme.buffer_font_size(cx) * theme.line_height())
1676 .px_2()
1677 .child(StyledText::new(left_output).with_default_highlights(&text_style, left_runs))
1678 .child(
1679 StyledText::new(right_output).with_default_highlights(&text_style, right_runs),
1680 ),
1681 )
1682 }
1683}
1684
1685pub struct MarksView {}
1686
1687impl MarksView {
1688 fn register(workspace: &mut Workspace, _window: Option<&mut Window>) {
1689 workspace.register_action(|workspace, _: &ToggleMarksView, window, cx| {
1690 Self::toggle(workspace, window, cx);
1691 });
1692 }
1693
1694 pub fn toggle(workspace: &mut Workspace, window: &mut Window, cx: &mut Context<Workspace>) {
1695 let handle = cx.weak_entity();
1696 workspace.toggle_modal(window, cx, move |window, cx| {
1697 MarksView::new(handle, window, cx)
1698 });
1699 }
1700
1701 fn new(
1702 workspace: WeakEntity<Workspace>,
1703 window: &mut Window,
1704 cx: &mut Context<Picker<MarksViewDelegate>>,
1705 ) -> Picker<MarksViewDelegate> {
1706 let matches = Vec::default();
1707 let delegate = MarksViewDelegate {
1708 selected_index: 0,
1709 point_column_width: 0,
1710 matches,
1711 workspace,
1712 };
1713 Picker::nonsearchable_uniform_list(delegate, window, cx)
1714 .width(rems(36.))
1715 .modal(true)
1716 }
1717}
1718
1719pub struct VimDb(ThreadSafeConnection);
1720
1721impl Domain for VimDb {
1722 const NAME: &str = stringify!(VimDb);
1723
1724 const MIGRATIONS: &[&str] = &[
1725 sql! (
1726 CREATE TABLE vim_marks (
1727 workspace_id INTEGER,
1728 mark_name TEXT,
1729 path BLOB,
1730 value TEXT
1731 );
1732 CREATE UNIQUE INDEX idx_vim_marks ON vim_marks (workspace_id, mark_name, path);
1733 ),
1734 sql! (
1735 CREATE TABLE vim_global_marks_paths(
1736 workspace_id INTEGER,
1737 mark_name TEXT,
1738 path BLOB
1739 );
1740 CREATE UNIQUE INDEX idx_vim_global_marks_paths
1741 ON vim_global_marks_paths(workspace_id, mark_name);
1742 ),
1743 ];
1744}
1745
1746db::static_connection!(DB, VimDb, [WorkspaceDb]);
1747
1748struct SerializedMark {
1749 path: Arc<Path>,
1750 name: String,
1751 points: Vec<Point>,
1752}
1753
1754impl VimDb {
1755 pub(crate) async fn set_marks(
1756 &self,
1757 workspace_id: WorkspaceId,
1758 path: Arc<Path>,
1759 marks: HashMap<String, Vec<Point>>,
1760 ) -> Result<()> {
1761 log::debug!("Setting path {path:?} for {} marks", marks.len());
1762
1763 self.write(move |conn| {
1764 let mut query = conn.exec_bound(sql!(
1765 INSERT OR REPLACE INTO vim_marks
1766 (workspace_id, mark_name, path, value)
1767 VALUES
1768 (?, ?, ?, ?)
1769 ))?;
1770 for (mark_name, value) in marks {
1771 let pairs: Vec<(u32, u32)> = value
1772 .into_iter()
1773 .map(|point| (point.row, point.column))
1774 .collect();
1775 let serialized = serde_json::to_string(&pairs)?;
1776 query((workspace_id, mark_name, path.clone(), serialized))?;
1777 }
1778 Ok(())
1779 })
1780 .await
1781 }
1782
1783 fn get_marks(&self, workspace_id: WorkspaceId) -> Result<Vec<SerializedMark>> {
1784 let result: Vec<(Arc<Path>, String, String)> = self.select_bound(sql!(
1785 SELECT path, mark_name, value FROM vim_marks
1786 WHERE workspace_id = ?
1787 ))?(workspace_id)?;
1788
1789 Ok(result
1790 .into_iter()
1791 .filter_map(|(path, name, value)| {
1792 let pairs: Vec<(u32, u32)> = serde_json::from_str(&value).log_err()?;
1793 Some(SerializedMark {
1794 path,
1795 name,
1796 points: pairs
1797 .into_iter()
1798 .map(|(row, column)| Point { row, column })
1799 .collect(),
1800 })
1801 })
1802 .collect())
1803 }
1804
1805 pub(crate) async fn delete_mark(
1806 &self,
1807 workspace_id: WorkspaceId,
1808 path: Arc<Path>,
1809 mark_name: String,
1810 ) -> Result<()> {
1811 self.write(move |conn| {
1812 conn.exec_bound(sql!(
1813 DELETE FROM vim_marks
1814 WHERE workspace_id = ? AND mark_name = ? AND path = ?
1815 ))?((workspace_id, mark_name, path))
1816 })
1817 .await
1818 }
1819
1820 pub(crate) async fn set_global_mark_path(
1821 &self,
1822 workspace_id: WorkspaceId,
1823 mark_name: String,
1824 path: Arc<Path>,
1825 ) -> Result<()> {
1826 log::debug!("Setting global mark path {path:?} for {mark_name}");
1827 self.write(move |conn| {
1828 conn.exec_bound(sql!(
1829 INSERT OR REPLACE INTO vim_global_marks_paths
1830 (workspace_id, mark_name, path)
1831 VALUES
1832 (?, ?, ?)
1833 ))?((workspace_id, mark_name, path))
1834 })
1835 .await
1836 }
1837
1838 pub fn get_global_marks_paths(
1839 &self,
1840 workspace_id: WorkspaceId,
1841 ) -> Result<Vec<(String, Arc<Path>)>> {
1842 self.select_bound(sql!(
1843 SELECT mark_name, path FROM vim_global_marks_paths
1844 WHERE workspace_id = ?
1845 ))?(workspace_id)
1846 }
1847
1848 pub(crate) async fn delete_global_marks_path(
1849 &self,
1850 workspace_id: WorkspaceId,
1851 mark_name: String,
1852 ) -> Result<()> {
1853 self.write(move |conn| {
1854 conn.exec_bound(sql!(
1855 DELETE FROM vim_global_marks_paths
1856 WHERE workspace_id = ? AND mark_name = ?
1857 ))?((workspace_id, mark_name))
1858 })
1859 .await
1860 }
1861}