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