vim: Add global marks (#25702)

Closes https://github.com/zed-industries/zed/issues/13111

Release Notes:

- vim: Added global marks `'[A-Z]`
- vim: Added persistence for global (and local) marks. When re-opening
the same workspace your previous marks will be available.

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
This commit is contained in:
AidanV 2025-03-14 22:58:34 -07:00 committed by GitHub
parent 148131786f
commit 265caed15e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 982 additions and 281 deletions

View file

@ -76,7 +76,7 @@ impl Vim {
}
});
});
vim.copy_selections_content(editor, motion.linewise(), cx);
vim.copy_selections_content(editor, motion.linewise(), window, cx);
editor.insert("", window, cx);
editor.refresh_inline_completion(true, false, window, cx);
});
@ -107,7 +107,7 @@ impl Vim {
});
});
if objects_found {
vim.copy_selections_content(editor, false, cx);
vim.copy_selections_content(editor, false, window, cx);
editor.insert("", window, cx);
editor.refresh_inline_completion(true, false, window, cx);
}

View file

@ -60,7 +60,7 @@ impl Vim {
}
});
});
vim.copy_selections_content(editor, motion.linewise(), cx);
vim.copy_selections_content(editor, motion.linewise(), window, cx);
editor.insert("", window, cx);
// Fixup cursor position after the deletion
@ -148,7 +148,7 @@ impl Vim {
}
});
});
vim.copy_selections_content(editor, false, cx);
vim.copy_selections_content(editor, false, window, cx);
editor.insert("", window, cx);
// Fixup cursor position after the deletion

View file

@ -1,39 +1,34 @@
use std::{ops::Range, sync::Arc};
use std::{ops::Range, path::Path, sync::Arc};
use editor::{
display_map::{DisplaySnapshot, ToDisplayPoint},
movement,
scroll::Autoscroll,
Anchor, Bias, DisplayPoint,
Anchor, Bias, DisplayPoint, Editor, MultiBuffer,
};
use gpui::{Context, Window};
use gpui::{Context, Entity, EntityId, UpdateGlobal, Window};
use language::SelectionGoal;
use text::Point;
use ui::App;
use workspace::OpenOptions;
use crate::{
motion::{self, Motion},
state::Mode,
state::{Mark, Mode, VimGlobals},
Vim,
};
impl Vim {
pub fn create_mark(
&mut self,
text: Arc<str>,
tail: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(anchors) = self.update_editor(window, cx, |_, editor, _, _| {
editor
pub fn create_mark(&mut self, text: Arc<str>, window: &mut Window, cx: &mut Context<Self>) {
self.update_editor(window, cx, |vim, editor, window, cx| {
let anchors = editor
.selections
.disjoint_anchors()
.iter()
.map(|s| if tail { s.tail() } else { s.head() })
.collect::<Vec<_>>()
}) else {
return;
};
self.marks.insert(text.to_string(), anchors);
.map(|s| s.head())
.collect::<Vec<_>>();
vim.set_mark(text.to_string(), anchors, editor.buffer(), window, cx);
});
self.clear_operator(window, cx);
}
@ -55,7 +50,7 @@ impl Vim {
let mut ends = vec![];
let mut reversed = vec![];
self.update_editor(window, cx, |_, editor, _, cx| {
self.update_editor(window, cx, |vim, editor, window, cx| {
let (map, selections) = editor.selections.all_display(cx);
for selection in selections {
let end = movement::saturating_left(&map, selection.end);
@ -69,13 +64,121 @@ impl Vim {
);
reversed.push(selection.reversed)
}
vim.set_mark("<".to_string(), starts, editor.buffer(), window, cx);
vim.set_mark(">".to_string(), ends, editor.buffer(), window, cx);
});
self.marks.insert("<".to_string(), starts);
self.marks.insert(">".to_string(), ends);
self.stored_visual_mode.replace((mode, reversed));
}
fn open_buffer_mark(
&mut self,
line: bool,
entity_id: EntityId,
anchors: Vec<Anchor>,
window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(workspace) = self.workspace(window) else {
return;
};
workspace.update(cx, |workspace, cx| {
let item = workspace.items(cx).find(|item| {
item.act_as::<Editor>(cx)
.is_some_and(|editor| editor.read(cx).buffer().entity_id() == entity_id)
});
let Some(item) = item.cloned() else {
return;
};
if let Some(pane) = workspace.pane_for(item.as_ref()) {
pane.update(cx, |pane, cx| {
if let Some(index) = pane.index_for_item(item.as_ref()) {
pane.activate_item(index, true, true, window, cx);
}
});
};
item.act_as::<Editor>(cx).unwrap().update(cx, |editor, cx| {
let map = editor.snapshot(window, cx);
let mut ranges: Vec<Range<Anchor>> = Vec::new();
for mut anchor in anchors {
if line {
let mut point = anchor.to_display_point(&map.display_snapshot);
point = motion::first_non_whitespace(&map.display_snapshot, false, point);
anchor = map
.display_snapshot
.buffer_snapshot
.anchor_before(point.to_point(&map.display_snapshot));
}
if ranges.last() != Some(&(anchor..anchor)) {
ranges.push(anchor..anchor);
}
}
editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
s.select_anchor_ranges(ranges)
});
})
});
return;
}
fn open_path_mark(
&mut self,
line: bool,
path: Arc<Path>,
points: Vec<Point>,
window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(workspace) = self.workspace(window) else {
return;
};
let task = workspace.update(cx, |workspace, cx| {
workspace.open_abs_path(
path.to_path_buf(),
OpenOptions {
visible: Some(workspace::OpenVisible::All),
focus: Some(true),
..Default::default()
},
window,
cx,
)
});
cx.spawn_in(window, |this, mut cx| async move {
let editor = task.await?;
this.update_in(&mut cx, |_, window, cx| {
if let Some(editor) = editor.act_as::<Editor>(cx) {
editor.update(cx, |editor, cx| {
let map = editor.snapshot(window, cx);
let points: Vec<_> = points
.into_iter()
.map(|p| {
if line {
let point = p.to_display_point(&map.display_snapshot);
motion::first_non_whitespace(
&map.display_snapshot,
false,
point,
)
.to_point(&map.display_snapshot)
} else {
p
}
})
.collect();
editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
s.select_ranges(points.into_iter().map(|p| p..p))
})
})
}
})
})
.detach_and_log_err(cx);
}
pub fn jump(
&mut self,
text: Arc<str>,
@ -84,25 +187,22 @@ impl Vim {
cx: &mut Context<Self>,
) {
self.pop_operator(window, cx);
let anchors = match &*text {
"{" | "}" => self.update_editor(window, cx, |_, editor, _, cx| {
let (map, selections) = editor.selections.all_display(cx);
selections
.into_iter()
.map(|selection| {
let point = if &*text == "{" {
movement::start_of_paragraph(&map, selection.head(), 1)
} else {
movement::end_of_paragraph(&map, selection.head(), 1)
};
map.buffer_snapshot
.anchor_before(point.to_offset(&map, Bias::Left))
})
.collect::<Vec<Anchor>>()
}),
"." => self.change_list.last().cloned(),
_ => self.marks.get(&*text).cloned(),
let mark = self
.update_editor(window, cx, |vim, editor, window, cx| {
vim.get_mark(&text, editor, window, cx)
})
.flatten();
let anchors = match mark {
None => None,
Some(Mark::Local(anchors)) => Some(anchors),
Some(Mark::Buffer(entity_id, anchors)) => {
self.open_buffer_mark(line, entity_id, anchors, window, cx);
return;
}
Some(Mark::Path(path, points)) => {
self.open_path_mark(line, path, points, window, cx);
return;
}
};
let Some(mut anchors) = anchors else { return };
@ -144,7 +244,7 @@ impl Vim {
}
}
if !should_jump {
if !should_jump && !ranges.is_empty() {
editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
s.select_anchor_ranges(ranges)
});
@ -158,6 +258,62 @@ impl Vim {
}
}
}
pub fn set_mark(
&mut self,
name: String,
anchors: Vec<Anchor>,
buffer_entity: &Entity<MultiBuffer>,
window: &mut Window,
cx: &mut App,
) {
let Some(workspace) = self.workspace(window) else {
return;
};
let entity_id = workspace.entity_id();
Vim::update_globals(cx, |vim_globals, cx| {
let Some(marks_state) = vim_globals.marks.get(&entity_id) else {
return;
};
marks_state.update(cx, |ms, cx| {
ms.set_mark(name.clone(), buffer_entity, anchors, cx);
});
});
}
pub fn get_mark(
&self,
name: &str,
editor: &mut Editor,
window: &mut Window,
cx: &mut App,
) -> Option<Mark> {
if matches!(name, "{" | "}" | "(" | ")") {
let (map, selections) = editor.selections.all_display(cx);
let anchors = selections
.into_iter()
.map(|selection| {
let point = match name {
"{" => movement::start_of_paragraph(&map, selection.head(), 1),
"}" => movement::end_of_paragraph(&map, selection.head(), 1),
"(" => motion::sentence_backwards(&map, selection.head(), 1),
")" => motion::sentence_forwards(&map, selection.head(), 1),
_ => unreachable!(),
};
map.buffer_snapshot
.anchor_before(point.to_offset(&map, Bias::Left))
})
.collect::<Vec<Anchor>>();
return Some(Mark::Local(anchors));
}
VimGlobals::update_global(cx, |globals, cx| {
let workspace_id = self.workspace(window)?.entity_id();
globals
.marks
.get_mut(&workspace_id)?
.update(cx, |ms, cx| ms.get_mark(name, editor.buffer(), cx))
})
}
}
pub fn jump_motion(

View file

@ -50,7 +50,7 @@ impl Vim {
.filter(|sel| sel.len() > 1 && vim.mode != Mode::VisualLine);
if !action.preserve_clipboard && vim.mode.is_visual() {
vim.copy_selections_content(editor, vim.mode == Mode::VisualLine, cx);
vim.copy_selections_content(editor, vim.mode == Mode::VisualLine, window, cx);
}
let (display_map, current_selections) = editor.selections.all_adjusted_display(cx);

View file

@ -75,7 +75,7 @@ impl Vim {
}
})
});
vim.copy_selections_content(editor, line_mode, cx);
vim.copy_selections_content(editor, line_mode, window, cx);
let selections = editor.selections.all::<Point>(cx).into_iter();
let edits = selections.map(|selection| (selection.start..selection.end, ""));
editor.edit(edits, cx);

View file

@ -36,7 +36,7 @@ impl Vim {
motion.expand_selection(map, selection, times, true, &text_layout_details);
});
});
vim.yank_selections_content(editor, motion.linewise(), cx);
vim.yank_selections_content(editor, motion.linewise(), window, cx);
editor.change_selections(None, window, cx, |s| {
s.move_with(|_, selection| {
let (head, goal) = original_positions.remove(&selection.id).unwrap();
@ -66,7 +66,7 @@ impl Vim {
start_positions.insert(selection.id, start_position);
});
});
vim.yank_selections_content(editor, false, cx);
vim.yank_selections_content(editor, false, window, cx);
editor.change_selections(None, window, cx, |s| {
s.move_with(|_, selection| {
let (head, goal) = start_positions.remove(&selection.id).unwrap();
@ -82,6 +82,7 @@ impl Vim {
&mut self,
editor: &mut Editor,
linewise: bool,
window: &mut Window,
cx: &mut Context<Editor>,
) {
self.copy_ranges(
@ -94,6 +95,7 @@ impl Vim {
.iter()
.map(|s| s.range())
.collect(),
window,
cx,
)
}
@ -102,6 +104,7 @@ impl Vim {
&mut self,
editor: &mut Editor,
linewise: bool,
window: &mut Window,
cx: &mut Context<Editor>,
) {
self.copy_ranges(
@ -114,6 +117,7 @@ impl Vim {
.iter()
.map(|s| s.range())
.collect(),
window,
cx,
)
}
@ -124,28 +128,35 @@ impl Vim {
linewise: bool,
is_yank: bool,
selections: Vec<Range<Point>>,
window: &mut Window,
cx: &mut Context<Editor>,
) {
let buffer = editor.buffer().read(cx).snapshot(cx);
let mut text = String::new();
let mut clipboard_selections = Vec::with_capacity(selections.len());
let mut ranges_to_highlight = Vec::new();
self.marks.insert(
self.set_mark(
"[".to_string(),
selections
.iter()
.map(|s| buffer.anchor_before(s.start))
.collect(),
editor.buffer(),
window,
cx,
);
self.marks.insert(
self.set_mark(
"]".to_string(),
selections
.iter()
.map(|s| buffer.anchor_after(s.end))
.collect(),
editor.buffer(),
window,
cx,
);
let mut text = String::new();
let mut clipboard_selections = Vec::with_capacity(selections.len());
let mut ranges_to_highlight = Vec::new();
{
let mut is_first = true;
for selection in selections.iter() {