vim: View Marks (#26885)
Closes #26884 Release Notes: - vim: Added `:marks` which brings up list of current marks - confirming on selected mark in the view jumps to that mark --------- Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
This commit is contained in:
parent
4c86cda909
commit
d82b547596
4 changed files with 381 additions and 9 deletions
|
@ -39,7 +39,7 @@ use crate::{
|
|||
object::Object,
|
||||
state::{Mark, Mode},
|
||||
visual::VisualDeleteLine,
|
||||
ToggleRegistersView, Vim,
|
||||
ToggleMarksView, ToggleRegistersView, Vim,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
|
@ -860,6 +860,7 @@ fn generate_commands(_: &App) -> Vec<VimCommand> {
|
|||
)
|
||||
}),
|
||||
VimCommand::new(("reg", "isters"), ToggleRegistersView).bang(ToggleRegistersView),
|
||||
VimCommand::new(("marks", ""), ToggleMarksView).bang(ToggleMarksView),
|
||||
VimCommand::new(("sor", "t"), SortLinesCaseSensitive).range(select_range),
|
||||
VimCommand::new(("sort i", ""), SortLinesCaseInsensitive).range(select_range),
|
||||
VimCommand::str(("E", "xplore"), "project_panel::ToggleFocus"),
|
||||
|
|
|
@ -183,10 +183,13 @@ impl Vim {
|
|||
&mut self,
|
||||
text: Arc<str>,
|
||||
line: bool,
|
||||
should_pop_operator: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.pop_operator(window, cx);
|
||||
if should_pop_operator {
|
||||
self.pop_operator(window, cx);
|
||||
}
|
||||
let mark = self
|
||||
.update_editor(window, cx, |vim, editor, window, cx| {
|
||||
vim.get_mark(&text, editor, window, cx)
|
||||
|
|
|
@ -2,27 +2,29 @@ use crate::command::command_interceptor;
|
|||
use crate::normal::repeat::Replayer;
|
||||
use crate::surrounds::SurroundsType;
|
||||
use crate::{motion::Motion, object::Object};
|
||||
use crate::{ToggleRegistersView, UseSystemClipboard, Vim, VimSettings};
|
||||
use crate::{ToggleMarksView, ToggleRegistersView, UseSystemClipboard, Vim, VimAddon, VimSettings};
|
||||
use anyhow::Result;
|
||||
use collections::HashMap;
|
||||
use command_palette_hooks::{CommandPaletteFilter, CommandPaletteInterceptor};
|
||||
use db::define_connection;
|
||||
use db::sqlez_macros::sql;
|
||||
use editor::display_map::{is_invisible, replacement};
|
||||
use editor::{Anchor, ClipboardSelection, Editor, MultiBuffer};
|
||||
use editor::{Anchor, ClipboardSelection, Editor, MultiBuffer, ToPoint as EditorToPoint};
|
||||
use gpui::{
|
||||
Action, App, AppContext, BorrowAppContext, ClipboardEntry, ClipboardItem, Entity, EntityId,
|
||||
Global, HighlightStyle, StyledText, Subscription, Task, TextStyle, WeakEntity,
|
||||
Action, App, AppContext, BorrowAppContext, ClipboardEntry, ClipboardItem, DismissEvent, Entity,
|
||||
EntityId, Global, HighlightStyle, StyledText, Subscription, Task, TextStyle, WeakEntity,
|
||||
};
|
||||
use language::{Buffer, BufferEvent, BufferId, Point};
|
||||
use language::{Buffer, BufferEvent, BufferId, Chunk, Point};
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use project::{Project, ProjectItem, ProjectPath};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsStore};
|
||||
use std::borrow::BorrowMut;
|
||||
use std::collections::HashSet;
|
||||
use std::path::Path;
|
||||
use std::{fmt::Display, ops::Range, sync::Arc};
|
||||
use text::Bias;
|
||||
use text::{Bias, ToPoint};
|
||||
use theme::ThemeSettings;
|
||||
use ui::{
|
||||
h_flex, rems, ActiveTheme, Context, Div, FluentBuilder, KeyBinding, ParentElement,
|
||||
|
@ -627,6 +629,11 @@ impl VimGlobals {
|
|||
})
|
||||
.detach();
|
||||
|
||||
cx.observe_new(move |workspace: &mut Workspace, window, _| {
|
||||
MarksView::register(workspace, window);
|
||||
})
|
||||
.detach();
|
||||
|
||||
let mut was_enabled = None;
|
||||
|
||||
cx.observe_global::<SettingsStore>(move |cx| {
|
||||
|
@ -1232,6 +1239,366 @@ impl RegistersView {
|
|||
}
|
||||
}
|
||||
|
||||
enum MarksMatchInfo {
|
||||
Path(Arc<Path>),
|
||||
Title(String),
|
||||
Content {
|
||||
line: String,
|
||||
highlights: Vec<(Range<usize>, HighlightStyle)>,
|
||||
},
|
||||
}
|
||||
|
||||
impl MarksMatchInfo {
|
||||
fn from_chunks<'a>(chunks: impl Iterator<Item = Chunk<'a>>, cx: &App) -> Self {
|
||||
let mut line = String::new();
|
||||
let mut highlights = Vec::new();
|
||||
let mut offset = 0;
|
||||
for chunk in chunks {
|
||||
line.push_str(chunk.text);
|
||||
if let Some(highlight_style) = chunk.syntax_highlight_id {
|
||||
if let Some(highlight) = highlight_style.style(cx.theme().syntax()) {
|
||||
highlights.push((offset..offset + chunk.text.len(), highlight))
|
||||
}
|
||||
}
|
||||
offset += chunk.text.len();
|
||||
}
|
||||
MarksMatchInfo::Content { line, highlights }
|
||||
}
|
||||
}
|
||||
|
||||
struct MarksMatch {
|
||||
name: String,
|
||||
position: Point,
|
||||
info: MarksMatchInfo,
|
||||
}
|
||||
|
||||
pub struct MarksViewDelegate {
|
||||
selected_index: usize,
|
||||
matches: Vec<MarksMatch>,
|
||||
point_column_width: usize,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
}
|
||||
|
||||
impl PickerDelegate for MarksViewDelegate {
|
||||
type ListItem = Div;
|
||||
|
||||
fn match_count(&self) -> usize {
|
||||
self.matches.len()
|
||||
}
|
||||
|
||||
fn selected_index(&self) -> usize {
|
||||
self.selected_index
|
||||
}
|
||||
|
||||
fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||
self.selected_index = ix;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
|
||||
Arc::default()
|
||||
}
|
||||
|
||||
fn update_matches(
|
||||
&mut self,
|
||||
_: String,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> gpui::Task<()> {
|
||||
let Some(workspace) = self.workspace.upgrade().clone() else {
|
||||
return Task::ready(());
|
||||
};
|
||||
cx.spawn(async move |picker, cx| {
|
||||
let mut matches = Vec::new();
|
||||
let _ = workspace.update(cx, |workspace, cx| {
|
||||
let entity_id = cx.entity_id();
|
||||
let Some(editor) = workspace
|
||||
.active_item(cx)
|
||||
.and_then(|item| item.act_as::<Editor>(cx))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let editor = editor.read(cx);
|
||||
let mut has_seen = HashSet::new();
|
||||
let Some(marks_state) = cx.global::<VimGlobals>().marks.get(&entity_id) else {
|
||||
return;
|
||||
};
|
||||
let marks_state = marks_state.read(cx);
|
||||
|
||||
if let Some(map) = marks_state
|
||||
.multibuffer_marks
|
||||
.get(&editor.buffer().entity_id())
|
||||
{
|
||||
for (name, anchors) in map {
|
||||
if has_seen.contains(name) {
|
||||
continue;
|
||||
}
|
||||
has_seen.insert(name.clone());
|
||||
let Some(anchor) = anchors.first() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
let position = anchor.to_point(&snapshot);
|
||||
|
||||
let chunks = snapshot.chunks(
|
||||
Point::new(position.row, 0)
|
||||
..Point::new(
|
||||
position.row,
|
||||
snapshot.line_len(MultiBufferRow(position.row)),
|
||||
),
|
||||
true,
|
||||
);
|
||||
matches.push(MarksMatch {
|
||||
name: name.clone(),
|
||||
position,
|
||||
info: MarksMatchInfo::from_chunks(chunks, cx),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(buffer) = editor.buffer().read(cx).as_singleton() {
|
||||
let buffer = buffer.read(cx);
|
||||
if let Some(map) = marks_state.buffer_marks.get(&buffer.remote_id()) {
|
||||
for (name, anchors) in map {
|
||||
if has_seen.contains(name) {
|
||||
continue;
|
||||
}
|
||||
has_seen.insert(name.clone());
|
||||
let Some(anchor) = anchors.first() else {
|
||||
continue;
|
||||
};
|
||||
let snapshot = buffer.snapshot();
|
||||
let position = anchor.to_point(&snapshot);
|
||||
let chunks = snapshot.chunks(
|
||||
Point::new(position.row, 0)
|
||||
..Point::new(position.row, snapshot.line_len(position.row)),
|
||||
true,
|
||||
);
|
||||
|
||||
matches.push(MarksMatch {
|
||||
name: name.clone(),
|
||||
position,
|
||||
info: MarksMatchInfo::from_chunks(chunks, cx),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (name, mark_location) in marks_state.global_marks.iter() {
|
||||
if has_seen.contains(name) {
|
||||
continue;
|
||||
}
|
||||
has_seen.insert(name.clone());
|
||||
|
||||
match mark_location {
|
||||
MarkLocation::Buffer(entity_id) => {
|
||||
if let Some(&anchor) = marks_state
|
||||
.multibuffer_marks
|
||||
.get(entity_id)
|
||||
.and_then(|map| map.get(name))
|
||||
.and_then(|anchors| anchors.first())
|
||||
{
|
||||
let Some((info, snapshot)) = workspace
|
||||
.items(cx)
|
||||
.filter_map(|item| item.act_as::<Editor>(cx))
|
||||
.map(|entity| entity.read(cx).buffer())
|
||||
.find(|buffer| buffer.entity_id().eq(entity_id))
|
||||
.map(|buffer| {
|
||||
(
|
||||
MarksMatchInfo::Title(
|
||||
buffer.read(cx).title(cx).to_string(),
|
||||
),
|
||||
buffer.read(cx).snapshot(cx),
|
||||
)
|
||||
})
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
matches.push(MarksMatch {
|
||||
name: name.clone(),
|
||||
position: anchor.to_point(&snapshot),
|
||||
info,
|
||||
});
|
||||
}
|
||||
}
|
||||
MarkLocation::Path(path) => {
|
||||
if let Some(&position) = marks_state
|
||||
.serialized_marks
|
||||
.get(path.as_ref())
|
||||
.and_then(|map| map.get(name))
|
||||
.and_then(|points| points.first())
|
||||
{
|
||||
let info = MarksMatchInfo::Path(path.clone());
|
||||
matches.push(MarksMatch {
|
||||
name: name.clone(),
|
||||
position,
|
||||
info,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
let _ = picker.update(cx, |picker, cx| {
|
||||
matches.sort_by_key(|a| {
|
||||
(
|
||||
a.name.chars().next().map(|c| c.is_ascii_uppercase()),
|
||||
a.name.clone(),
|
||||
)
|
||||
});
|
||||
let digits = matches
|
||||
.iter()
|
||||
.map(|m| (m.position.row + 1).ilog10() + (m.position.column + 1).ilog10())
|
||||
.max()
|
||||
.unwrap_or_default();
|
||||
picker.delegate.matches = matches;
|
||||
picker.delegate.point_column_width = (digits + 4) as usize;
|
||||
cx.notify();
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||
let Some(vim) = self
|
||||
.workspace
|
||||
.upgrade()
|
||||
.map(|w| w.read(cx))
|
||||
.and_then(|w| w.focused_pane(window, cx).read(cx).active_item())
|
||||
.and_then(|item| item.act_as::<Editor>(cx))
|
||||
.and_then(|editor| editor.read(cx).addon::<VimAddon>().cloned())
|
||||
.map(|addon| addon.entity)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let Some(text): Option<Arc<str>> = self
|
||||
.matches
|
||||
.get(self.selected_index)
|
||||
.map(|m| Arc::from(m.name.to_string().into_boxed_str()))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
vim.update(cx, |vim, cx| {
|
||||
vim.jump(text, false, false, window, cx);
|
||||
});
|
||||
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, _: &mut Window, _: &mut Context<Picker<Self>>) {}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let mark_match = self
|
||||
.matches
|
||||
.get(ix)
|
||||
.expect("Invalid matches state: no element for index {ix}");
|
||||
|
||||
let mut left_output = String::new();
|
||||
let mut left_runs = Vec::new();
|
||||
left_output.push('`');
|
||||
left_output.push_str(&mark_match.name);
|
||||
left_runs.push((
|
||||
0..left_output.len(),
|
||||
HighlightStyle::color(cx.theme().colors().text_accent),
|
||||
));
|
||||
left_output.push(' ');
|
||||
left_output.push(' ');
|
||||
let point_column = format!(
|
||||
"{},{}",
|
||||
mark_match.position.row + 1,
|
||||
mark_match.position.column + 1
|
||||
);
|
||||
left_output.push_str(&point_column);
|
||||
if let Some(padding) = self.point_column_width.checked_sub(point_column.len()) {
|
||||
left_output.push_str(&" ".repeat(padding));
|
||||
}
|
||||
|
||||
let (right_output, right_runs): (String, Vec<_>) = match &mark_match.info {
|
||||
MarksMatchInfo::Path(path) => {
|
||||
let s = path.to_string_lossy().to_string();
|
||||
(
|
||||
s.clone(),
|
||||
vec![(0..s.len(), HighlightStyle::color(cx.theme().colors().text))],
|
||||
)
|
||||
}
|
||||
MarksMatchInfo::Title(title) => (
|
||||
title.clone(),
|
||||
vec![(
|
||||
0..title.len(),
|
||||
HighlightStyle::color(cx.theme().colors().text),
|
||||
)],
|
||||
),
|
||||
MarksMatchInfo::Content { line, highlights } => (line.clone(), highlights.clone()),
|
||||
};
|
||||
|
||||
let theme = ThemeSettings::get_global(cx);
|
||||
let text_style = TextStyle {
|
||||
color: cx.theme().colors().editor_foreground,
|
||||
font_family: theme.buffer_font.family.clone(),
|
||||
font_features: theme.buffer_font.features.clone(),
|
||||
font_fallbacks: theme.buffer_font.fallbacks.clone(),
|
||||
font_size: theme.buffer_font_size(cx).into(),
|
||||
line_height: (theme.line_height() * theme.buffer_font_size(cx)).into(),
|
||||
font_weight: theme.buffer_font.weight,
|
||||
font_style: theme.buffer_font.style,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
Some(
|
||||
h_flex()
|
||||
.when(selected, |el| el.bg(cx.theme().colors().element_selected))
|
||||
.font_buffer(cx)
|
||||
.text_buffer(cx)
|
||||
.h(theme.buffer_font_size(cx) * theme.line_height())
|
||||
.px_2()
|
||||
.child(StyledText::new(left_output).with_default_highlights(&text_style, left_runs))
|
||||
.child(
|
||||
StyledText::new(right_output).with_default_highlights(&text_style, right_runs),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MarksView {}
|
||||
|
||||
impl MarksView {
|
||||
fn register(workspace: &mut Workspace, _window: Option<&mut Window>) {
|
||||
workspace.register_action(|workspace, _: &ToggleMarksView, window, cx| {
|
||||
Self::toggle(workspace, window, cx);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn toggle(workspace: &mut Workspace, window: &mut Window, cx: &mut Context<Workspace>) {
|
||||
let handle = cx.weak_entity();
|
||||
workspace.toggle_modal(window, cx, move |window, cx| {
|
||||
MarksView::new(handle, window, cx)
|
||||
});
|
||||
}
|
||||
|
||||
fn new(
|
||||
workspace: WeakEntity<Workspace>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Picker<MarksViewDelegate>>,
|
||||
) -> Picker<MarksViewDelegate> {
|
||||
let matches = Vec::default();
|
||||
let delegate = MarksViewDelegate {
|
||||
selected_index: 0,
|
||||
point_column_width: 0,
|
||||
matches,
|
||||
workspace,
|
||||
};
|
||||
Picker::nonsearchable_uniform_list(delegate, window, cx)
|
||||
.width(rems(36.))
|
||||
.modal(true)
|
||||
}
|
||||
}
|
||||
|
||||
define_connection! (
|
||||
pub static ref DB: VimDb<WorkspaceDb> = &[
|
||||
sql! (
|
||||
|
|
|
@ -144,6 +144,7 @@ actions!(
|
|||
PushReplace,
|
||||
PushDeleteSurrounds,
|
||||
PushMark,
|
||||
ToggleMarksView,
|
||||
PushIndent,
|
||||
PushOutdent,
|
||||
PushAutoIndent,
|
||||
|
@ -1599,7 +1600,7 @@ impl Vim {
|
|||
self.select_register(text, window, cx);
|
||||
}
|
||||
},
|
||||
Some(Operator::Jump { line }) => self.jump(text, line, window, cx),
|
||||
Some(Operator::Jump { line }) => self.jump(text, line, true, window, cx),
|
||||
_ => {
|
||||
if self.mode == Mode::Replace {
|
||||
self.multi_replace(text, window, cx)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue