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