diff --git a/crates/vim/src/command.rs b/crates/vim/src/command.rs index f01bb78dc9..8ad2994480 100644 --- a/crates/vim/src/command.rs +++ b/crates/vim/src/command.rs @@ -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::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"), diff --git a/crates/vim/src/normal/mark.rs b/crates/vim/src/normal/mark.rs index 783e918edd..3fffa5f583 100644 --- a/crates/vim/src/normal/mark.rs +++ b/crates/vim/src/normal/mark.rs @@ -183,10 +183,13 @@ impl Vim { &mut self, text: Arc, line: bool, + should_pop_operator: bool, window: &mut Window, cx: &mut Context, ) { - 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) diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index cf68798ffe..f367863bf6 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -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::(move |cx| { @@ -1232,6 +1239,366 @@ impl RegistersView { } } +enum MarksMatchInfo { + Path(Arc), + Title(String), + Content { + line: String, + highlights: Vec<(Range, HighlightStyle)>, + }, +} + +impl MarksMatchInfo { + fn from_chunks<'a>(chunks: impl Iterator>, 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, + point_column_width: usize, + workspace: WeakEntity, +} + +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>) { + self.selected_index = ix; + cx.notify(); + } + + fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc { + Arc::default() + } + + fn update_matches( + &mut self, + _: String, + _: &mut Window, + cx: &mut Context>, + ) -> 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::(cx)) + else { + return; + }; + let editor = editor.read(cx); + let mut has_seen = HashSet::new(); + let Some(marks_state) = cx.global::().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::(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>) { + 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::(cx)) + .and_then(|editor| editor.read(cx).addon::().cloned()) + .map(|addon| addon.entity) + else { + return; + }; + let Some(text): Option> = 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>) {} + + fn render_match( + &self, + ix: usize, + selected: bool, + _: &mut Window, + cx: &mut Context>, + ) -> Option { + 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) { + let handle = cx.weak_entity(); + workspace.toggle_modal(window, cx, move |window, cx| { + MarksView::new(handle, window, cx) + }); + } + + fn new( + workspace: WeakEntity, + window: &mut Window, + cx: &mut Context>, + ) -> Picker { + 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 = &[ sql! ( diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 79900e1a36..4743347b37 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -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)