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