From a5af5b2883130bb4ff5c2c180bd0e67e07f05442 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 13 Jun 2024 20:32:58 -0600 Subject: [PATCH] Multicursor vim registers (#13025) Release Notes: - vim: Added support for multicursor registers (#11687) - vim: Added support for the `"/` register --- crates/editor/src/editor.rs | 2 +- crates/vim/src/motion.rs | 9 +- crates/vim/src/normal.rs | 2 +- crates/vim/src/normal/change.rs | 2 +- crates/vim/src/normal/delete.rs | 2 +- crates/vim/src/normal/paste.rs | 112 +++++++------- crates/vim/src/normal/search.rs | 3 + crates/vim/src/normal/substitute.rs | 2 +- crates/vim/src/normal/yank.rs | 140 +++++++++++++++++- crates/vim/src/object.rs | 7 +- crates/vim/src/state.rs | 43 +++++- .../src/test/neovim_backed_test_context.rs | 4 +- crates/vim/src/utils.rs | 135 ----------------- crates/vim/src/vim.rs | 105 +++++++------ crates/vim/src/visual.rs | 2 +- .../vim/test_data/test_named_registers.json | 2 + .../vim/test_data/test_special_registers.json | 11 ++ 17 files changed, 333 insertions(+), 250 deletions(-) delete mode 100644 crates/vim/src/utils.rs diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 1583c1e227..0bc3a63a57 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1522,7 +1522,7 @@ struct ActiveDiagnosticGroup { is_valid: bool, } -#[derive(Serialize, Deserialize, Clone)] +#[derive(Serialize, Deserialize, Clone, Debug)] pub struct ClipboardSelection { pub len: usize, pub is_entire_line: bool, diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index 295112e881..2b430a0e48 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -17,7 +17,6 @@ use crate::{ normal::{mark, normal_motion}, state::{Mode, Operator}, surrounds::SurroundsType, - utils::coerce_punctuation, visual::visual_motion, Vim, }; @@ -1764,6 +1763,14 @@ fn window_bottom( } } +pub fn coerce_punctuation(kind: CharKind, treat_punctuation_as_word: bool) -> CharKind { + if treat_punctuation_as_word && kind == CharKind::Punctuation { + CharKind::Word + } else { + kind + } +} + #[cfg(test)] mod test { diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 1bd7040ed5..02729e04bb 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -9,7 +9,7 @@ pub(crate) mod repeat; mod scroll; pub(crate) mod search; pub mod substitute; -mod yank; +pub(crate) mod yank; use std::collections::HashMap; use std::sync::Arc; diff --git a/crates/vim/src/normal/change.rs b/crates/vim/src/normal/change.rs index c52cb22e8a..07486c6d93 100644 --- a/crates/vim/src/normal/change.rs +++ b/crates/vim/src/normal/change.rs @@ -1,8 +1,8 @@ use crate::{ motion::{self, Motion}, + normal::yank::copy_selections_content, object::Object, state::Mode, - utils::copy_selections_content, Vim, }; use editor::{ diff --git a/crates/vim/src/normal/delete.rs b/crates/vim/src/normal/delete.rs index 4136f9e5c6..e439981888 100644 --- a/crates/vim/src/normal/delete.rs +++ b/crates/vim/src/normal/delete.rs @@ -1,4 +1,4 @@ -use crate::{motion::Motion, object::Object, utils::copy_selections_content, Vim}; +use crate::{motion::Motion, normal::yank::copy_selections_content, object::Object, Vim}; use collections::{HashMap, HashSet}; use editor::{ display_map::{DisplaySnapshot, ToDisplayPoint}, diff --git a/crates/vim/src/normal/paste.rs b/crates/vim/src/normal/paste.rs index fa5360b96a..dfc0ff84c3 100644 --- a/crates/vim/src/normal/paste.rs +++ b/crates/vim/src/normal/paste.rs @@ -1,19 +1,15 @@ use std::cmp; -use editor::{ - display_map::ToDisplayPoint, movement, scroll::Autoscroll, ClipboardSelection, DisplayPoint, - RowExt, -}; -use gpui::{impl_actions, AppContext, ViewContext}; +use editor::{display_map::ToDisplayPoint, movement, scroll::Autoscroll, DisplayPoint, RowExt}; +use gpui::{impl_actions, ViewContext}; use language::{Bias, SelectionGoal}; use serde::Deserialize; -use settings::Settings; use workspace::Workspace; use crate::{ - state::Mode, - utils::{copy_selections_content, SYSTEM_CLIPBOARD}, - UseSystemClipboard, Vim, VimSettings, + normal::yank::copy_selections_content, + state::{Mode, Register}, + Vim, }; #[derive(Clone, Deserialize, PartialEq)] @@ -31,16 +27,6 @@ pub(crate) fn register(workspace: &mut Workspace, _: &mut ViewContext workspace.register_action(paste); } -fn system_clipboard_is_newer(vim: &Vim, cx: &mut AppContext) -> bool { - cx.read_from_clipboard().is_some_and(|item| { - if let Some(last_state) = vim.workspace_state.registers.get(&SYSTEM_CLIPBOARD) { - last_state != item.text() - } else { - true - } - }) -} - fn paste(_: &mut Workspace, action: &Paste, cx: &mut ViewContext) { Vim::update(cx, |vim, cx| { vim.record_current_action(cx); @@ -50,40 +36,19 @@ fn paste(_: &mut Workspace, action: &Paste, cx: &mut ViewContext) { editor.transact(cx, |editor, cx| { editor.set_clip_at_line_ends(false, cx); - let (clipboard_text, clipboard_selections): (String, Option<_>) = if let Some( - register, - ) = - vim.update_state(|state| state.selected_register.take()) - { - ( - vim.read_register(register, Some(editor), cx) - .unwrap_or_default(), - None, - ) - } else if VimSettings::get_global(cx).use_system_clipboard - == UseSystemClipboard::Never - || VimSettings::get_global(cx).use_system_clipboard - == UseSystemClipboard::OnYank - && !system_clipboard_is_newer(vim, cx) - { - (vim.read_register('"', None, cx).unwrap_or_default(), None) - } else { - if let Some(item) = cx.read_from_clipboard() { - let clipboard_selections = item - .metadata::>() - .filter(|clipboard_selections| { - clipboard_selections.len() > 1 - && vim.state().mode != Mode::VisualLine - }); - (item.text().clone(), clipboard_selections) - } else { - ("".into(), None) - } - }; + let selected_register = vim.update_state(|state| state.selected_register.take()); - if clipboard_text.is_empty() { + let Some(Register { + text, + clipboard_selections, + }) = vim + .read_register(selected_register, Some(editor), cx) + .filter(|reg| !reg.text.is_empty()) + else { return; - } + }; + let clipboard_selections = clipboard_selections + .filter(|sel| sel.len() > 1 && vim.state().mode != Mode::VisualLine); if !action.preserve_clipboard && vim.state().mode.is_visual() { copy_selections_content(vim, editor, vim.state().mode == Mode::VisualLine, cx); @@ -135,14 +100,14 @@ fn paste(_: &mut Workspace, action: &Paste, cx: &mut ViewContext) { if let Some(clipboard_selections) = &clipboard_selections { if let Some(clipboard_selection) = clipboard_selections.get(ix) { let end_offset = start_offset + clipboard_selection.len; - let text = clipboard_text[start_offset..end_offset].to_string(); + let text = text[start_offset..end_offset].to_string(); start_offset = end_offset + 1; (text, Some(clipboard_selection.first_line_indent)) } else { ("".to_string(), first_selection_indent_column) } } else { - (clipboard_text.to_string(), first_selection_indent_column) + (text.to_string(), first_selection_indent_column) }; let line_mode = to_insert.ends_with('\n'); let is_multiline = to_insert.contains('\n'); @@ -679,6 +644,7 @@ mod test { cx.shared_register('a').await.assert_eq("jumps "); cx.simulate_shared_keystrokes("\" shift-a d i w").await; cx.shared_register('a').await.assert_eq("jumps over"); + cx.shared_register('"').await.assert_eq("jumps over"); cx.simulate_shared_keystrokes("\" a p").await; cx.shared_state().await.assert_eq(indoc! {" The quick brown @@ -719,12 +685,50 @@ mod test { cx.shared_clipboard().await.assert_eq("lazy dog"); cx.shared_register('"').await.assert_eq("lazy dog"); + cx.simulate_shared_keystrokes("/ d o g enter").await; + cx.shared_register('/').await.assert_eq("dog"); + cx.simulate_shared_keystrokes("\" / shift-p").await; + cx.shared_state().await.assert_eq(indoc! {" + The quick brown + doˇg"}); + // not testing nvim as it doesn't have a filename cx.simulate_keystrokes("\" % p"); cx.assert_state( indoc! {" The quick brown - dir/file.rˇs"}, + dogdir/file.rˇs"}, + Mode::Normal, + ); + } + + #[gpui::test] + async fn test_multicursor_paste(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + + cx.update_global(|store: &mut SettingsStore, cx| { + store.update_user_settings::(cx, |s| { + s.use_system_clipboard = Some(UseSystemClipboard::Never) + }); + }); + + cx.set_state( + indoc! {" + ˇfish one + fish two + fish red + fish blue + "}, + Mode::Normal, + ); + cx.simulate_keystrokes("4 g l w escape d i w 0 shift-p"); + cx.assert_state( + indoc! {" + onˇefish• + twˇofish• + reˇdfish• + bluˇefish• + "}, Mode::Normal, ); } diff --git a/crates/vim/src/normal/search.rs b/crates/vim/src/normal/search.rs index 6d2a0c749b..b802b28360 100644 --- a/crates/vim/src/normal/search.rs +++ b/crates/vim/src/normal/search.rs @@ -165,6 +165,9 @@ fn search_submit(workspace: &mut Workspace, _: &SearchSubmit, cx: &mut ViewConte { count = count.saturating_sub(1) } + vim.workspace_state + .registers + .insert('/', search_bar.query(cx).into()); state.count = 1; search_bar.select_match(direction, count, cx); search_bar.focus_editor(&Default::default(), cx); diff --git a/crates/vim/src/normal/substitute.rs b/crates/vim/src/normal/substitute.rs index 0e8f4afb51..1d9f9b3066 100644 --- a/crates/vim/src/normal/substitute.rs +++ b/crates/vim/src/normal/substitute.rs @@ -3,7 +3,7 @@ use gpui::{actions, ViewContext, WindowContext}; use language::Point; use workspace::Workspace; -use crate::{motion::Motion, utils::copy_selections_content, Mode, Vim}; +use crate::{motion::Motion, normal::yank::copy_selections_content, Mode, Vim}; actions!(vim, [Substitute, SubstituteLine]); diff --git a/crates/vim/src/normal/yank.rs b/crates/vim/src/normal/yank.rs index 1220460d29..922bb7c7f2 100644 --- a/crates/vim/src/normal/yank.rs +++ b/crates/vim/src/normal/yank.rs @@ -1,6 +1,17 @@ -use crate::{motion::Motion, object::Object, utils::yank_selections_content, Vim}; +use std::time::Duration; + +use crate::{ + motion::Motion, + object::Object, + state::{Mode, Register}, + Vim, +}; use collections::HashMap; +use editor::{ClipboardSelection, Editor}; use gpui::WindowContext; +use language::Point; +use multi_buffer::MultiBufferRow; +use ui::ViewContext; pub fn yank_motion(vim: &mut Vim, motion: Motion, times: Option, cx: &mut WindowContext) { vim.update_active_editor(cx, |vim, editor, cx| { @@ -48,3 +59,130 @@ pub fn yank_object(vim: &mut Vim, object: Object, around: bool, cx: &mut WindowC }); }); } + +pub fn yank_selections_content( + vim: &mut Vim, + editor: &mut Editor, + linewise: bool, + cx: &mut ViewContext, +) { + copy_selections_content_internal(vim, editor, linewise, true, cx); +} + +pub fn copy_selections_content( + vim: &mut Vim, + editor: &mut Editor, + linewise: bool, + cx: &mut ViewContext, +) { + copy_selections_content_internal(vim, editor, linewise, false, cx); +} + +struct HighlightOnYank; + +fn copy_selections_content_internal( + vim: &mut Vim, + editor: &mut Editor, + linewise: bool, + is_yank: bool, + cx: &mut ViewContext, +) { + let selections = editor.selections.all_adjusted(cx); + 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(); + + vim.update_state(|state| { + state.marks.insert( + "[".to_string(), + selections + .iter() + .map(|s| buffer.anchor_before(s.start)) + .collect(), + ); + state.marks.insert( + "]".to_string(), + selections + .iter() + .map(|s| buffer.anchor_after(s.end)) + .collect(), + ) + }); + + { + let mut is_first = true; + for selection in selections.iter() { + let mut start = selection.start; + let end = selection.end; + if is_first { + is_first = false; + } else { + text.push_str("\n"); + } + let initial_len = text.len(); + + // if the file does not end with \n, and our line-mode selection ends on + // that line, we will have expanded the start of the selection to ensure it + // contains a newline (so that delete works as expected). We undo that change + // here. + let is_last_line = linewise + && end.row == buffer.max_buffer_row().0 + && buffer.max_point().column > 0 + && start.row < buffer.max_buffer_row().0 + && start == Point::new(start.row, buffer.line_len(MultiBufferRow(start.row))); + + if is_last_line { + start = Point::new(start.row + 1, 0); + } + + let start_anchor = buffer.anchor_after(start); + let end_anchor = buffer.anchor_before(end); + ranges_to_highlight.push(start_anchor..end_anchor); + + for chunk in buffer.text_for_range(start..end) { + text.push_str(chunk); + } + if is_last_line { + text.push_str("\n"); + } + clipboard_selections.push(ClipboardSelection { + len: text.len() - initial_len, + is_entire_line: linewise, + first_line_indent: buffer.indent_size_for_line(MultiBufferRow(start.row)).len, + }); + } + } + + let selected_register = vim.update_state(|state| state.selected_register.take()); + vim.write_registers( + Register { + text: text.into(), + clipboard_selections: Some(clipboard_selections), + }, + selected_register, + is_yank, + linewise, + cx, + ); + + if !is_yank || vim.state().mode == Mode::Visual { + return; + } + + editor.highlight_background::( + &ranges_to_highlight, + |colors| colors.editor_document_highlight_read_background, + cx, + ); + cx.spawn(|this, mut cx| async move { + cx.background_executor() + .timer(Duration::from_millis(200)) + .await; + this.update(&mut cx, |editor, cx| { + editor.clear_background_highlights::(cx) + }) + .ok(); + }) + .detach(); +} diff --git a/crates/vim/src/object.rs b/crates/vim/src/object.rs index 458c0b25c4..2392c4a025 100644 --- a/crates/vim/src/object.rs +++ b/crates/vim/src/object.rs @@ -1,8 +1,11 @@ use std::ops::Range; use crate::{ - motion::right, normal::normal_object, state::Mode, utils::coerce_punctuation, - visual::visual_object, Vim, + motion::{coerce_punctuation, right}, + normal::normal_object, + state::Mode, + visual::visual_object, + Vim, }; use editor::{ display_map::{DisplaySnapshot, ToDisplayPoint}, diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index 6d18f94b2e..b822626dfa 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -3,10 +3,11 @@ use std::{fmt::Display, ops::Range, sync::Arc}; use crate::surrounds::SurroundsType; use crate::{motion::Motion, object::Object}; use collections::HashMap; -use editor::Anchor; -use gpui::{Action, KeyContext}; +use editor::{Anchor, ClipboardSelection}; +use gpui::{Action, ClipboardItem, KeyContext}; use language::{CursorShape, Selection, TransactionId}; use serde::{Deserialize, Serialize}; +use ui::SharedString; use workspace::searchable::Direction; #[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)] @@ -113,6 +114,41 @@ pub enum RecordedSelection { }, } +#[derive(Default, Clone, Debug)] +pub struct Register { + pub(crate) text: SharedString, + pub(crate) clipboard_selections: Option>, +} + +impl From for ClipboardItem { + fn from(register: Register) -> Self { + let item = ClipboardItem::new(register.text.into()); + if let Some(clipboard_selections) = register.clipboard_selections { + item.with_metadata(clipboard_selections) + } else { + item + } + } +} + +impl From for Register { + fn from(value: ClipboardItem) -> Self { + Register { + text: value.text().to_owned().into(), + clipboard_selections: value.metadata::>(), + } + } +} + +impl From for Register { + fn from(text: String) -> Self { + Register { + text: text.into(), + clipboard_selections: None, + } + } +} + #[derive(Default, Clone)] pub struct WorkspaceState { pub search: SearchState, @@ -125,7 +161,8 @@ pub struct WorkspaceState { pub recorded_actions: Vec, pub recorded_selection: RecordedSelection, - pub registers: HashMap, + pub last_yank: Option, + pub registers: HashMap, } #[derive(Debug)] diff --git a/crates/vim/src/test/neovim_backed_test_context.rs b/crates/vim/src/test/neovim_backed_test_context.rs index f7e4e28702..babee53505 100644 --- a/crates/vim/src/test/neovim_backed_test_context.rs +++ b/crates/vim/src/test/neovim_backed_test_context.rs @@ -254,7 +254,7 @@ impl NeovimBackedTestContext { #[must_use] pub async fn shared_register(&mut self, register: char) -> SharedClipboard { SharedClipboard { - register: register, + register, state: self.shared_state().await, neovim: self.neovim.read_register(register).await, editor: self.update(|cx| { @@ -264,6 +264,8 @@ impl NeovimBackedTestContext { .get(®ister) .cloned() .unwrap_or_default() + .text + .into() }), } } diff --git a/crates/vim/src/utils.rs b/crates/vim/src/utils.rs deleted file mode 100644 index b9bb30fe86..0000000000 --- a/crates/vim/src/utils.rs +++ /dev/null @@ -1,135 +0,0 @@ -use std::time::Duration; - -use editor::{ClipboardSelection, Editor}; -use gpui::ViewContext; -use language::{CharKind, Point}; -use multi_buffer::MultiBufferRow; - -use crate::{state::Mode, Vim}; - -pub const SYSTEM_CLIPBOARD: char = '\0'; - -pub struct HighlightOnYank; - -pub fn yank_selections_content( - vim: &mut Vim, - editor: &mut Editor, - linewise: bool, - cx: &mut ViewContext, -) { - copy_selections_content_internal(vim, editor, linewise, true, cx); -} - -pub fn copy_selections_content( - vim: &mut Vim, - editor: &mut Editor, - linewise: bool, - cx: &mut ViewContext, -) { - copy_selections_content_internal(vim, editor, linewise, false, cx); -} - -fn copy_selections_content_internal( - vim: &mut Vim, - editor: &mut Editor, - linewise: bool, - is_yank: bool, - cx: &mut ViewContext, -) { - let selections = editor.selections.all_adjusted(cx); - 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(); - - vim.update_state(|state| { - state.marks.insert( - "[".to_string(), - selections - .iter() - .map(|s| buffer.anchor_before(s.start)) - .collect(), - ); - state.marks.insert( - "]".to_string(), - selections - .iter() - .map(|s| buffer.anchor_after(s.end)) - .collect(), - ) - }); - - { - let mut is_first = true; - for selection in selections.iter() { - let mut start = selection.start; - let end = selection.end; - if is_first { - is_first = false; - } else { - text.push_str("\n"); - } - let initial_len = text.len(); - - // if the file does not end with \n, and our line-mode selection ends on - // that line, we will have expanded the start of the selection to ensure it - // contains a newline (so that delete works as expected). We undo that change - // here. - let is_last_line = linewise - && end.row == buffer.max_buffer_row().0 - && buffer.max_point().column > 0 - && start.row < buffer.max_buffer_row().0 - && start == Point::new(start.row, buffer.line_len(MultiBufferRow(start.row))); - - if is_last_line { - start = Point::new(start.row + 1, 0); - } - - let start_anchor = buffer.anchor_after(start); - let end_anchor = buffer.anchor_before(end); - ranges_to_highlight.push(start_anchor..end_anchor); - - for chunk in buffer.text_for_range(start..end) { - text.push_str(chunk); - } - if is_last_line { - text.push_str("\n"); - } - clipboard_selections.push(ClipboardSelection { - len: text.len() - initial_len, - is_entire_line: linewise, - first_line_indent: buffer.indent_size_for_line(MultiBufferRow(start.row)).len, - }); - } - } - - vim.write_registers(is_yank, linewise, text, clipboard_selections, cx); - - if !is_yank || vim.state().mode == Mode::Visual { - return; - } - - editor.highlight_background::( - &ranges_to_highlight, - |colors| colors.editor_document_highlight_read_background, - cx, - ); - cx.spawn(|this, mut cx| async move { - cx.background_executor() - .timer(Duration::from_millis(200)) - .await; - this.update(&mut cx, |editor, cx| { - editor.clear_background_highlights::(cx) - }) - .ok(); - }) - .detach(); -} - -pub fn coerce_punctuation(kind: CharKind, treat_punctuation_as_word: bool) -> CharKind { - if treat_punctuation_as_word && kind == CharKind::Punctuation { - CharKind::Word - } else { - kind - } -} diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 090cddfc3d..8eb3ba8626 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -14,7 +14,6 @@ mod object; mod replace; mod state; mod surrounds; -mod utils; mod visual; use anyhow::Result; @@ -23,11 +22,11 @@ use collections::HashMap; use command_palette_hooks::{CommandPaletteFilter, CommandPaletteInterceptor}; use editor::{ movement::{self, FindRange}, - Anchor, Bias, ClipboardSelection, Editor, EditorEvent, EditorMode, ToPoint, + Anchor, Bias, Editor, EditorEvent, EditorMode, ToPoint, }; use gpui::{ - actions, impl_actions, Action, AppContext, ClipboardItem, EntityId, FocusableView, Global, - KeystrokeEvent, Subscription, UpdateGlobal, View, ViewContext, WeakView, WindowContext, + actions, impl_actions, Action, AppContext, EntityId, FocusableView, Global, KeystrokeEvent, + Subscription, UpdateGlobal, View, ViewContext, WeakView, WindowContext, }; use language::{CursorShape, Point, SelectionGoal, TransactionId}; pub use mode_indicator::ModeIndicator; @@ -41,11 +40,10 @@ use schemars::JsonSchema; use serde::Deserialize; use serde_derive::Serialize; use settings::{update_settings_file, Settings, SettingsSources, SettingsStore}; -use state::{EditorState, Mode, Operator, RecordedSelection, WorkspaceState}; +use state::{EditorState, Mode, Operator, RecordedSelection, Register, WorkspaceState}; use std::{ops::Range, sync::Arc}; use surrounds::{add_surrounds, change_surrounds, delete_surrounds}; use ui::BorrowAppContext; -use utils::SYSTEM_CLIPBOARD; use visual::{visual_block_motion, visual_replace}; use workspace::{self, Workspace}; @@ -551,42 +549,40 @@ impl Vim { fn write_registers( &mut self, + content: Register, + register: Option, is_yank: bool, linewise: bool, - text: String, - clipboard_selections: Vec, cx: &mut ViewContext, ) { - self.workspace_state.registers.insert('"', text.clone()); - if let Some(register) = self.update_state(|vim| vim.selected_register.take()) { + if let Some(register) = register { let lower = register.to_lowercase().next().unwrap_or(register); if lower != register { let current = self.workspace_state.registers.entry(lower).or_default(); - *current += &text; + current.text = (current.text.to_string() + &content.text).into(); + // not clear how to support appending to registers with multiple cursors + current.clipboard_selections.take(); + let yanked = current.clone(); + self.workspace_state.registers.insert('"', yanked); } else { + self.workspace_state.registers.insert('"', content.clone()); match lower { '_' | ':' | '.' | '%' | '#' | '=' | '/' => {} '+' => { - cx.write_to_clipboard( - ClipboardItem::new(text.clone()).with_metadata(clipboard_selections), - ); + cx.write_to_clipboard(content.into()); } '*' => { #[cfg(target_os = "linux")] - cx.write_to_primary( - ClipboardItem::new(text.clone()).with_metadata(clipboard_selections), - ); + cx.write_to_primary(content.into()); #[cfg(not(target_os = "linux"))] - cx.write_to_clipboard( - ClipboardItem::new(text.clone()).with_metadata(clipboard_selections), - ); + cx.write_to_clipboard(content.into()); } '"' => { - self.workspace_state.registers.insert('0', text.clone()); - self.workspace_state.registers.insert('"', text); + self.workspace_state.registers.insert('0', content.clone()); + self.workspace_state.registers.insert('"', content); } _ => { - self.workspace_state.registers.insert(lower, text); + self.workspace_state.registers.insert(lower, content); } } } @@ -595,29 +591,24 @@ impl Vim { if setting == UseSystemClipboard::Always || setting == UseSystemClipboard::OnYank && is_yank { - cx.write_to_clipboard( - ClipboardItem::new(text.clone()).with_metadata(clipboard_selections.clone()), - ); - self.workspace_state - .registers - .insert(SYSTEM_CLIPBOARD, text.clone()); + self.workspace_state.last_yank.replace(content.text.clone()); + cx.write_to_clipboard(content.clone().into()); } else { - self.workspace_state.registers.insert( - SYSTEM_CLIPBOARD, - cx.read_from_clipboard() - .map(|item| item.text().clone()) - .unwrap_or_default(), - ); + self.workspace_state.last_yank = cx + .read_from_clipboard() + .map(|item| item.text().to_owned().into()); } + self.workspace_state.registers.insert('"', content.clone()); if is_yank { - self.workspace_state.registers.insert('0', text); + self.workspace_state.registers.insert('0', content); } else { - if !text.contains('\n') { - self.workspace_state.registers.insert('-', text.clone()); + let contains_newline = content.text.contains('\n'); + if !contains_newline { + self.workspace_state.registers.insert('-', content.clone()); } - if linewise || text.contains('\n') { - let mut content = text; + if linewise || contains_newline { + let mut content = content; for i in '1'..'8' { if let Some(moved) = self.workspace_state.registers.insert(i, content) { content = moved; @@ -632,22 +623,32 @@ impl Vim { fn read_register( &mut self, - register: char, + register: Option, editor: Option<&mut Editor>, cx: &mut WindowContext, - ) -> Option { + ) -> Option { + let Some(register) = register else { + let setting = VimSettings::get_global(cx).use_system_clipboard; + return match setting { + UseSystemClipboard::Always => cx.read_from_clipboard().map(|item| item.into()), + UseSystemClipboard::OnYank if self.system_clipboard_is_newer(cx) => { + cx.read_from_clipboard().map(|item| item.into()) + } + _ => self.workspace_state.registers.get(&'"').cloned(), + }; + }; let lower = register.to_lowercase().next().unwrap_or(register); match lower { - '_' | ':' | '.' | '#' | '=' | '/' => None, - '+' => cx.read_from_clipboard().map(|item| item.text().clone()), + '_' | ':' | '.' | '#' | '=' => None, + '+' => cx.read_from_clipboard().map(|item| item.into()), '*' => { #[cfg(target_os = "linux")] { - cx.read_from_primary().map(|item| item.text().clone()) + cx.read_from_primary().map(|item| item.into()) } #[cfg(not(target_os = "linux"))] { - cx.read_from_clipboard().map(|item| item.text().clone()) + cx.read_from_clipboard().map(|item| item.into()) } } '%' => editor.and_then(|editor| { @@ -660,7 +661,7 @@ impl Vim { buffer .read(cx) .file() - .map(|file| file.path().to_string_lossy().to_string()) + .map(|file| file.path().to_string_lossy().to_string().into()) } else { None } @@ -669,6 +670,16 @@ impl Vim { } } + fn system_clipboard_is_newer(&self, cx: &mut AppContext) -> bool { + cx.read_from_clipboard().is_some_and(|item| { + if let Some(last_state) = &self.workspace_state.last_yank { + last_state != item.text() + } else { + true + } + }) + } + fn push_operator(&mut self, operator: Operator, cx: &mut WindowContext) { if matches!( operator, diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index 8436df2f9e..cc320bc207 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -17,9 +17,9 @@ use workspace::{searchable::Direction, Workspace}; use crate::{ motion::{start_of_line, Motion}, normal::substitute::substitute, + normal::yank::{copy_selections_content, yank_selections_content}, object::Object, state::{Mode, Operator}, - utils::{copy_selections_content, yank_selections_content}, Vim, }; diff --git a/crates/vim/test_data/test_named_registers.json b/crates/vim/test_data/test_named_registers.json index 886a19ad9e..789f17f93f 100644 --- a/crates/vim/test_data/test_named_registers.json +++ b/crates/vim/test_data/test_named_registers.json @@ -13,6 +13,8 @@ {"Key":"w"} {"Get":{"state":"The quick brown\nfoxˇ \nthe lazy dog","mode":"Normal"}} {"ReadRegister":{"name":"a","value":"jumps over"}} +{"Get":{"state":"The quick brown\nfoxˇ \nthe lazy dog","mode":"Normal"}} +{"ReadRegister":{"name":"\"","value":"jumps over"}} {"Key":"\""} {"Key":"a"} {"Key":"p"} diff --git a/crates/vim/test_data/test_special_registers.json b/crates/vim/test_data/test_special_registers.json index 29f27cc688..8b6b098af6 100644 --- a/crates/vim/test_data/test_special_registers.json +++ b/crates/vim/test_data/test_special_registers.json @@ -28,3 +28,14 @@ {"ReadRegister":{"name":"\"","value":"lazy dog"}} {"Get":{"state":"The quick brown\nˇ","mode":"Normal"}} {"ReadRegister":{"name":"\"","value":"lazy dog"}} +{"Key":"/"} +{"Key":"d"} +{"Key":"o"} +{"Key":"g"} +{"Key":"enter"} +{"Get":{"state":"The quick brown\nˇ","mode":"Normal"}} +{"ReadRegister":{"name":"/","value":"dog"}} +{"Key":"\""} +{"Key":"/"} +{"Key":"shift-p"} +{"Get":{"state":"The quick brown\ndoˇg","mode":"Normal"}}