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