From f07ae541addc693eb017b05d41f1d4fcc194b5e1 Mon Sep 17 00:00:00 2001 From: AidanV <84053180+AidanV@users.noreply.github.com> Date: Tue, 4 Mar 2025 20:59:19 -0800 Subject: [PATCH] vim: Add registers view (#25945) Closes #18157 Release Notes: - vim: Added `:reg[isters]` to show the current values of registers --------- Co-authored-by: Conrad Irwin --- Cargo.lock | 1 + crates/editor/src/display_map.rs | 2 +- crates/editor/src/display_map/invisibles.rs | 2 +- crates/vim/Cargo.toml | 1 + crates/vim/src/command.rs | 3 +- crates/vim/src/state.rs | 212 +++++++++++++++++++- crates/vim/src/vim.rs | 1 + 7 files changed, 213 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 95612ffccf..d9a731b371 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14987,6 +14987,7 @@ dependencies = [ "multi_buffer", "nvim-rs", "parking_lot", + "picker", "project", "project_panel", "regex", diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index f3cbf2dd96..5a5df832fb 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -43,7 +43,7 @@ use gpui::{App, Context, Entity, Font, HighlightStyle, LineLayout, Pixels, Under pub use inlay_map::Inlay; use inlay_map::{InlayMap, InlaySnapshot}; pub use inlay_map::{InlayOffset, InlayPoint}; -use invisibles::{is_invisible, replacement}; +pub use invisibles::{is_invisible, replacement}; use language::{ language_settings::language_settings, ChunkRenderer, OffsetUtf16, Point, Subscription as BufferSubscription, diff --git a/crates/editor/src/display_map/invisibles.rs b/crates/editor/src/display_map/invisibles.rs index 794b897603..199986f2a4 100644 --- a/crates/editor/src/display_map/invisibles.rs +++ b/crates/editor/src/display_map/invisibles.rs @@ -45,7 +45,7 @@ pub fn is_invisible(c: char) -> bool { // ASCII control characters have fancy unicode glyphs, everything else // is replaced by a space - unless it is used in combining characters in // which case we need to leave it in the string. -pub(crate) fn replacement(c: char) -> Option<&'static str> { +pub fn replacement(c: char) -> Option<&'static str> { if c <= '\x1f' { Some(C0_SYMBOLS[c as usize]) } else if c == '\x7f' { diff --git a/crates/vim/Cargo.toml b/crates/vim/Cargo.toml index cd6ea3a082..9732e51f47 100644 --- a/crates/vim/Cargo.toml +++ b/crates/vim/Cargo.toml @@ -31,6 +31,7 @@ libc.workspace = true log.workspace = true multi_buffer.workspace = true nvim-rs = { git = "https://github.com/KillTheMule/nvim-rs", branch = "master", features = ["use_tokio"], optional = true } +picker.workspace = true regex.workspace = true schemars.workspace = true search.workspace = true diff --git a/crates/vim/src/command.rs b/crates/vim/src/command.rs index 9d3fbbe8bb..2c3b028f56 100644 --- a/crates/vim/src/command.rs +++ b/crates/vim/src/command.rs @@ -39,7 +39,7 @@ use crate::{ object::Object, state::Mode, visual::VisualDeleteLine, - Vim, + ToggleRegistersView, Vim, }; #[derive(Clone, Debug, PartialEq)] @@ -853,6 +853,7 @@ fn generate_commands(_: &App) -> Vec { .boxed_clone(), ) }), + VimCommand::new(("reg", "isters"), ToggleRegistersView).bang(ToggleRegistersView), 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/state.rs b/crates/vim/src/state.rs index bb0a3aebdb..f51e479c6e 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -2,20 +2,28 @@ use crate::command::command_interceptor; use crate::normal::repeat::Replayer; use crate::surrounds::SurroundsType; use crate::{motion::Motion, object::Object}; -use crate::{UseSystemClipboard, Vim, VimSettings}; +use crate::{ToggleRegistersView, UseSystemClipboard, Vim, VimSettings}; use collections::HashMap; use command_palette_hooks::{CommandPaletteFilter, CommandPaletteInterceptor}; +use editor::display_map::{is_invisible, replacement}; use editor::{Anchor, ClipboardSelection, Editor}; use gpui::{ - Action, App, BorrowAppContext, ClipboardEntry, ClipboardItem, Entity, Global, WeakEntity, + Action, App, BorrowAppContext, ClipboardEntry, ClipboardItem, Entity, Global, HighlightStyle, + StyledText, Task, TextStyle, WeakEntity, }; use language::Point; +use picker::{Picker, PickerDelegate}; use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsStore}; use std::borrow::BorrowMut; use std::{fmt::Display, ops::Range, sync::Arc}; -use ui::{Context, KeyBinding, SharedString}; +use theme::ThemeSettings; +use ui::{ + h_flex, rems, ActiveTheme, Context, Div, FluentBuilder, KeyBinding, ParentElement, + SharedString, Styled, StyledTypography, Window, +}; use workspace::searchable::Direction; +use workspace::Workspace; #[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] pub enum Mode { @@ -215,6 +223,11 @@ impl VimGlobals { }) .detach(); + cx.observe_new(|workspace: &mut Workspace, window, _| { + RegistersView::register(workspace, window); + }) + .detach(); + cx.observe_global::(move |cx| { if Vim::enabled(cx) { KeyBinding::set_vim_mode(cx, true); @@ -315,10 +328,10 @@ impl VimGlobals { } pub(crate) fn read_register( - &mut self, + &self, register: Option, editor: Option<&mut Editor>, - cx: &mut Context, + cx: &mut App, ) -> Option { let Some(register) = register.filter(|reg| *reg != '"') else { let setting = VimSettings::get_global(cx).use_system_clipboard; @@ -363,7 +376,7 @@ impl VimGlobals { } } - fn system_clipboard_is_newer(&self, cx: &mut Context) -> bool { + fn system_clipboard_is_newer(&self, cx: &App) -> bool { cx.read_from_clipboard().is_some_and(|item| { if let Some(last_state) = &self.last_yank { Some(last_state.as_ref()) != item.text().as_deref() @@ -599,3 +612,190 @@ impl Operator { } } } + +struct RegisterMatch { + name: char, + contents: SharedString, +} + +pub struct RegistersViewDelegate { + selected_index: usize, + matches: Vec, +} + +impl PickerDelegate for RegistersViewDelegate { + 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, + _: &mut Context>, + ) -> gpui::Task<()> { + Task::ready(()) + } + + fn confirm(&mut self, _: bool, _: &mut Window, _: &mut Context>) {} + + fn dismissed(&mut self, _: &mut Window, _: &mut Context>) {} + + fn render_match( + &self, + ix: usize, + selected: bool, + _: &mut Window, + cx: &mut Context>, + ) -> Option { + let register_match = self + .matches + .get(ix) + .expect("Invalid matches state: no element for index {ix}"); + + let mut output = String::new(); + let mut runs = Vec::new(); + output.push('"'); + output.push(register_match.name); + runs.push(( + 0..output.len(), + HighlightStyle::color(cx.theme().colors().text_accent), + )); + output.push(' '); + output.push(' '); + let mut base = output.len(); + for (ix, c) in register_match.contents.char_indices() { + if ix > 100 { + break; + } + let replace = match c { + '\t' => Some("\\t".to_string()), + '\n' => Some("\\n".to_string()), + '\r' => Some("\\r".to_string()), + c if is_invisible(c) => { + if c <= '\x1f' { + replacement(c).map(|s| s.to_string()) + } else { + Some(format!("\\u{:04X}", c as u32)) + } + } + _ => None, + }; + let Some(replace) = replace else { + output.push(c); + continue; + }; + output.push_str(&replace); + runs.push(( + base + ix..base + ix + replace.len(), + HighlightStyle::color(cx.theme().colors().text_muted), + )); + base += replace.len() - c.len_utf8(); + } + + 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() + .gap_1() + .child(StyledText::new(output).with_default_highlights(&text_style, runs)), + ) + } +} + +pub struct RegistersView {} + +impl RegistersView { + fn register(workspace: &mut Workspace, _window: Option<&mut Window>) { + workspace.register_action(|workspace, _: &ToggleRegistersView, window, cx| { + Self::toggle(workspace, window, cx); + }); + } + + pub fn toggle(workspace: &mut Workspace, window: &mut Window, cx: &mut Context) { + let editor = workspace + .active_item(cx) + .and_then(|item| item.act_as::(cx)); + workspace.toggle_modal(window, cx, move |window, cx| { + RegistersView::new(editor, window, cx) + }); + } + + fn new( + editor: Option>, + window: &mut Window, + cx: &mut Context>, + ) -> Picker { + let mut matches = Vec::default(); + cx.update_global(|globals: &mut VimGlobals, cx| { + for name in ['"', '+', '*'] { + if let Some(register) = globals.read_register(Some(name), None, cx) { + matches.push(RegisterMatch { + name, + contents: register.text.clone(), + }) + } + } + if let Some(editor) = editor { + let register = editor.update(cx, |editor, cx| { + globals.read_register(Some('%'), Some(editor), cx) + }); + if let Some(register) = register { + matches.push(RegisterMatch { + name: '%', + contents: register.text.clone(), + }) + } + } + for (name, register) in globals.registers.iter() { + if ['"', '+', '*', '%'].contains(name) { + continue; + }; + matches.push(RegisterMatch { + name: *name, + contents: register.text.clone(), + }) + } + }); + matches.sort_by(|a, b| a.name.cmp(&b.name)); + let delegate = RegistersViewDelegate { + selected_index: 0, + matches, + }; + + Picker::nonsearchable_uniform_list(delegate, window, cx) + .width(rems(36.)) + .modal(true) + } +} diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 2a10029488..be4acca290 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -152,6 +152,7 @@ actions!( PushLowercase, PushUppercase, PushOppositeCase, + ToggleRegistersView, PushRegister, PushRecordRegister, PushReplayRegister,