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