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