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