diff --git a/Cargo.lock b/Cargo.lock index 104a15769d..62f9781cee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10903,6 +10903,7 @@ dependencies = [ "project", "regex", "release_channel", + "schemars", "search", "serde", "serde_derive", diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 2af71b607f..d94adb95a3 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -101,8 +101,14 @@ "ctrl-o": "pane::GoBack", "ctrl-i": "pane::GoForward", "ctrl-]": "editor::GoToDefinition", - "escape": ["vim::SwitchMode", "Normal"], - "ctrl-[": ["vim::SwitchMode", "Normal"], + "escape": [ + "vim::SwitchMode", + "Normal" + ], + "ctrl-[": [ + "vim::SwitchMode", + "Normal" + ], "v": "vim::ToggleVisual", "shift-v": "vim::ToggleVisualLine", "ctrl-v": "vim::ToggleVisualBlock", @@ -235,36 +241,123 @@ } ], // Count support - "1": ["vim::Number", 1], - "2": ["vim::Number", 2], - "3": ["vim::Number", 3], - "4": ["vim::Number", 4], - "5": ["vim::Number", 5], - "6": ["vim::Number", 6], - "7": ["vim::Number", 7], - "8": ["vim::Number", 8], - "9": ["vim::Number", 9], + "1": [ + "vim::Number", + 1 + ], + "2": [ + "vim::Number", + 2 + ], + "3": [ + "vim::Number", + 3 + ], + "4": [ + "vim::Number", + 4 + ], + "5": [ + "vim::Number", + 5 + ], + "6": [ + "vim::Number", + 6 + ], + "7": [ + "vim::Number", + 7 + ], + "8": [ + "vim::Number", + 8 + ], + "9": [ + "vim::Number", + 9 + ], // window related commands (ctrl-w X) - "ctrl-w left": ["workspace::ActivatePaneInDirection", "Left"], - "ctrl-w right": ["workspace::ActivatePaneInDirection", "Right"], - "ctrl-w up": ["workspace::ActivatePaneInDirection", "Up"], - "ctrl-w down": ["workspace::ActivatePaneInDirection", "Down"], - "ctrl-w h": ["workspace::ActivatePaneInDirection", "Left"], - "ctrl-w l": ["workspace::ActivatePaneInDirection", "Right"], - "ctrl-w k": ["workspace::ActivatePaneInDirection", "Up"], - "ctrl-w j": ["workspace::ActivatePaneInDirection", "Down"], - "ctrl-w ctrl-h": ["workspace::ActivatePaneInDirection", "Left"], - "ctrl-w ctrl-l": ["workspace::ActivatePaneInDirection", "Right"], - "ctrl-w ctrl-k": ["workspace::ActivatePaneInDirection", "Up"], - "ctrl-w ctrl-j": ["workspace::ActivatePaneInDirection", "Down"], - "ctrl-w shift-left": ["workspace::SwapPaneInDirection", "Left"], - "ctrl-w shift-right": ["workspace::SwapPaneInDirection", "Right"], - "ctrl-w shift-up": ["workspace::SwapPaneInDirection", "Up"], - "ctrl-w shift-down": ["workspace::SwapPaneInDirection", "Down"], - "ctrl-w shift-h": ["workspace::SwapPaneInDirection", "Left"], - "ctrl-w shift-l": ["workspace::SwapPaneInDirection", "Right"], - "ctrl-w shift-k": ["workspace::SwapPaneInDirection", "Up"], - "ctrl-w shift-j": ["workspace::SwapPaneInDirection", "Down"], + "ctrl-w left": [ + "workspace::ActivatePaneInDirection", + "Left" + ], + "ctrl-w right": [ + "workspace::ActivatePaneInDirection", + "Right" + ], + "ctrl-w up": [ + "workspace::ActivatePaneInDirection", + "Up" + ], + "ctrl-w down": [ + "workspace::ActivatePaneInDirection", + "Down" + ], + "ctrl-w h": [ + "workspace::ActivatePaneInDirection", + "Left" + ], + "ctrl-w l": [ + "workspace::ActivatePaneInDirection", + "Right" + ], + "ctrl-w k": [ + "workspace::ActivatePaneInDirection", + "Up" + ], + "ctrl-w j": [ + "workspace::ActivatePaneInDirection", + "Down" + ], + "ctrl-w ctrl-h": [ + "workspace::ActivatePaneInDirection", + "Left" + ], + "ctrl-w ctrl-l": [ + "workspace::ActivatePaneInDirection", + "Right" + ], + "ctrl-w ctrl-k": [ + "workspace::ActivatePaneInDirection", + "Up" + ], + "ctrl-w ctrl-j": [ + "workspace::ActivatePaneInDirection", + "Down" + ], + "ctrl-w shift-left": [ + "workspace::SwapPaneInDirection", + "Left" + ], + "ctrl-w shift-right": [ + "workspace::SwapPaneInDirection", + "Right" + ], + "ctrl-w shift-up": [ + "workspace::SwapPaneInDirection", + "Up" + ], + "ctrl-w shift-down": [ + "workspace::SwapPaneInDirection", + "Down" + ], + "ctrl-w shift-h": [ + "workspace::SwapPaneInDirection", + "Left" + ], + "ctrl-w shift-l": [ + "workspace::SwapPaneInDirection", + "Right" + ], + "ctrl-w shift-k": [ + "workspace::SwapPaneInDirection", + "Up" + ], + "ctrl-w shift-j": [ + "workspace::SwapPaneInDirection", + "Down" + ], "ctrl-w g t": "pane::ActivateNextItem", "ctrl-w ctrl-g t": "pane::ActivateNextItem", "ctrl-w g shift-t": "pane::ActivatePrevItem", @@ -286,8 +379,14 @@ "ctrl-w ctrl-q": "pane::CloseAllItems", "ctrl-w o": "workspace::CloseInactiveTabsAndPanes", "ctrl-w ctrl-o": "workspace::CloseInactiveTabsAndPanes", - "ctrl-w n": ["workspace::NewFileInDirection", "Up"], - "ctrl-w ctrl-n": ["workspace::NewFileInDirection", "Up"], + "ctrl-w n": [ + "workspace::NewFileInDirection", + "Up" + ], + "ctrl-w ctrl-n": [ + "workspace::NewFileInDirection", + "Up" + ], "-": "pane::RevealInProjectPanel" } }, @@ -303,12 +402,21 @@ "context": "Editor && vim_mode == normal && vim_operator == none && !VimWaiting", "bindings": { ".": "vim::Repeat", - "c": ["vim::PushOperator", "Change"], + "c": [ + "vim::PushOperator", + "Change" + ], "shift-c": "vim::ChangeToEndOfLine", - "d": ["vim::PushOperator", "Delete"], + "d": [ + "vim::PushOperator", + "Delete" + ], "shift-d": "vim::DeleteToEndOfLine", "shift-j": "vim::JoinLines", - "y": ["vim::PushOperator", "Yank"], + "y": [ + "vim::PushOperator", + "Yank" + ], "shift-y": "vim::YankLine", "i": "vim::InsertBefore", "shift-i": "vim::InsertFirstNonWhitespace", @@ -339,7 +447,10 @@ ], "*": "vim::MoveToNext", "#": "vim::MoveToPrev", - "r": ["vim::PushOperator", "Replace"], + "r": [ + "vim::PushOperator", + "Replace" + ], "s": "vim::Substitute", "shift-s": "vim::SubstituteLine", "> >": "editor::Indent", @@ -351,7 +462,10 @@ { "context": "Editor && VimCount", "bindings": { - "0": ["vim::Number", 0] + "0": [ + "vim::Number", + 0 + ] } }, { @@ -454,10 +568,22 @@ "shift-i": "vim::InsertBefore", "shift-a": "vim::InsertAfter", "shift-j": "vim::JoinLines", - "r": ["vim::PushOperator", "Replace"], - "ctrl-c": ["vim::SwitchMode", "Normal"], - "escape": ["vim::SwitchMode", "Normal"], - "ctrl-[": ["vim::SwitchMode", "Normal"], + "r": [ + "vim::PushOperator", + "Replace" + ], + "ctrl-c": [ + "vim::SwitchMode", + "Normal" + ], + "escape": [ + "vim::SwitchMode", + "Normal" + ], + "ctrl-[": [ + "vim::SwitchMode", + "Normal" + ], ">": "editor::Indent", "<": "editor::Outdent", "i": [ @@ -498,8 +624,14 @@ "bindings": { "tab": "vim::Tab", "enter": "vim::Enter", - "escape": ["vim::SwitchMode", "Normal"], - "ctrl-[": ["vim::SwitchMode", "Normal"] + "escape": [ + "vim::SwitchMode", + "Normal" + ], + "ctrl-[": [ + "vim::SwitchMode", + "Normal" + ] } }, { diff --git a/assets/settings/default.json b/assets/settings/default.json index 1df60279db..f8c5e4b15e 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -331,7 +331,9 @@ "copilot": { // The set of glob patterns for which copilot should be disabled // in any matching file. - "disabled_globs": [".env"] + "disabled_globs": [ + ".env" + ] }, // Settings specific to journaling "journal": { @@ -440,7 +442,12 @@ // Default directories to search for virtual environments, relative // to the current working directory. We recommend overriding this // in your project's settings, rather than globally. - "directories": [".env", "env", ".venv", "venv"], + "directories": [ + ".env", + "env", + ".venv", + "venv" + ], // Can also be 'csh', 'fish', and `nushell` "activate_script": "default" } @@ -555,6 +562,10 @@ // } // } }, + // Vim settings + "vim": { + "use_system_clipboard": "always" + }, // The server to connect to. If the environment variable // ZED_SERVER_URL is set, it will override this setting. "server_url": "https://zed.dev", diff --git a/crates/vim/Cargo.toml b/crates/vim/Cargo.toml index 05f8dafe3d..682403ba5c 100644 --- a/crates/vim/Cargo.toml +++ b/crates/vim/Cargo.toml @@ -39,6 +39,7 @@ tokio = { version = "1.15", "optional" = true } ui.workspace = true workspace.workspace = true zed_actions.workspace = true +schemars.workspace = true [dev-dependencies] editor = { workspace = true, features = ["test-support"] } diff --git a/crates/vim/src/insert.rs b/crates/vim/src/insert.rs index a063d37475..6ac3f28695 100644 --- a/crates/vim/src/insert.rs +++ b/crates/vim/src/insert.rs @@ -15,7 +15,7 @@ fn normal_before(_: &mut Workspace, action: &NormalBefore, cx: &mut ViewContext< let count = vim.take_count(cx).unwrap_or(1); vim.stop_recording_immediately(action.boxed_clone()); if count <= 1 || vim.workspace_state.replaying { - vim.update_active_editor(cx, |editor, cx| { + vim.update_active_editor(cx, |_, editor, cx| { editor.cancel(&Default::default(), cx); editor.change_selections(Some(Autoscroll::fit()), cx, |s| { s.move_cursors_with(|map, mut cursor, _| { diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 399180fea4..e90eba95d0 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -119,7 +119,7 @@ pub(crate) fn register(workspace: &mut Workspace, cx: &mut ViewContext, cx: &mut WindowContext, ) { - vim.update_active_editor(cx, |editor, cx| { + vim.update_active_editor(cx, |_, editor, cx| { let text_layout_details = editor.text_layout_details(cx); editor.change_selections(Some(Autoscroll::fit()), cx, |s| { s.move_cursors_with(|map, cursor, goal| { @@ -198,7 +198,7 @@ fn insert_after(_: &mut Workspace, _: &InsertAfter, cx: &mut ViewContext = old_selections @@ -285,7 +285,7 @@ fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContex Vim::update(cx, |vim, cx| { vim.start_recording(cx); vim.switch_mode(Mode::Insert, false, cx); - vim.update_active_editor(cx, |editor, cx| { + vim.update_active_editor(cx, |_, editor, cx| { let text_layout_details = editor.text_layout_details(cx); editor.transact(cx, |editor, cx| { let (map, old_selections) = editor.selections.all_display(cx); @@ -330,7 +330,7 @@ fn yank_line(_: &mut Workspace, _: &YankLine, cx: &mut ViewContext) { pub(crate) fn normal_replace(text: Arc, cx: &mut WindowContext) { Vim::update(cx, |vim, cx| { vim.stop_recording(); - vim.update_active_editor(cx, |editor, cx| { + vim.update_active_editor(cx, |_, editor, cx| { editor.transact(cx, |editor, cx| { editor.set_clip_at_line_ends(false, cx); let (map, display_selections) = editor.selections.all_display(cx); diff --git a/crates/vim/src/normal/case.rs b/crates/vim/src/normal/case.rs index db33e48c34..d8111312f9 100644 --- a/crates/vim/src/normal/case.rs +++ b/crates/vim/src/normal/case.rs @@ -40,7 +40,7 @@ where Vim::update(cx, |vim, cx| { vim.record_current_action(cx); let count = vim.take_count(cx).unwrap_or(1) as u32; - vim.update_active_editor(cx, |editor, cx| { + vim.update_active_editor(cx, |vim, editor, cx| { let mut ranges = Vec::new(); let mut cursor_positions = Vec::new(); let snapshot = editor.buffer().read(cx).snapshot(cx); diff --git a/crates/vim/src/normal/change.rs b/crates/vim/src/normal/change.rs index 52de1f7e0a..08f3202e79 100644 --- a/crates/vim/src/normal/change.rs +++ b/crates/vim/src/normal/change.rs @@ -24,7 +24,7 @@ pub fn change_motion(vim: &mut Vim, motion: Motion, times: Option, cx: &m | Motion::Backspace | Motion::StartOfLine { .. } ); - vim.update_active_editor(cx, |editor, cx| { + vim.update_active_editor(cx, |vim, editor, cx| { let text_layout_details = editor.text_layout_details(cx); editor.transact(cx, |editor, cx| { // We are swapping to insert mode anyway. Just set the line end clipping behavior now @@ -45,7 +45,7 @@ pub fn change_motion(vim: &mut Vim, motion: Motion, times: Option, cx: &m }; }); }); - copy_selections_content(editor, motion.linewise(), cx); + copy_selections_content(vim, editor, motion.linewise(), cx); editor.insert("", cx); }); }); @@ -59,7 +59,7 @@ pub fn change_motion(vim: &mut Vim, motion: Motion, times: Option, cx: &m pub fn change_object(vim: &mut Vim, object: Object, around: bool, cx: &mut WindowContext) { let mut objects_found = false; - vim.update_active_editor(cx, |editor, cx| { + vim.update_active_editor(cx, |vim, editor, cx| { // We are swapping to insert mode anyway. Just set the line end clipping behavior now editor.set_clip_at_line_ends(false, cx); editor.transact(cx, |editor, cx| { @@ -69,7 +69,7 @@ pub fn change_object(vim: &mut Vim, object: Object, around: bool, cx: &mut Windo }); }); if objects_found { - copy_selections_content(editor, false, cx); + copy_selections_content(vim, editor, false, cx); editor.insert("", cx); } }); diff --git a/crates/vim/src/normal/delete.rs b/crates/vim/src/normal/delete.rs index cbcdcadca9..80e87ccaf9 100644 --- a/crates/vim/src/normal/delete.rs +++ b/crates/vim/src/normal/delete.rs @@ -6,7 +6,7 @@ use language::Point; pub fn delete_motion(vim: &mut Vim, motion: Motion, times: Option, cx: &mut WindowContext) { vim.stop_recording(); - vim.update_active_editor(cx, |editor, cx| { + vim.update_active_editor(cx, |vim, editor, cx| { let text_layout_details = editor.text_layout_details(cx); editor.transact(cx, |editor, cx| { editor.set_clip_at_line_ends(false, cx); @@ -39,7 +39,7 @@ pub fn delete_motion(vim: &mut Vim, motion: Motion, times: Option, cx: &m } }); }); - copy_selections_content(editor, motion.linewise(), cx); + copy_selections_content(vim, editor, motion.linewise(), cx); editor.insert("", cx); // Fixup cursor position after the deletion @@ -62,7 +62,7 @@ pub fn delete_motion(vim: &mut Vim, motion: Motion, times: Option, cx: &m pub fn delete_object(vim: &mut Vim, object: Object, around: bool, cx: &mut WindowContext) { vim.stop_recording(); - vim.update_active_editor(cx, |editor, cx| { + vim.update_active_editor(cx, |vim, editor, cx| { editor.transact(cx, |editor, cx| { editor.set_clip_at_line_ends(false, cx); // Emulates behavior in vim where if we expanded backwards to include a newline @@ -98,7 +98,7 @@ pub fn delete_object(vim: &mut Vim, object: Object, around: bool, cx: &mut Windo } }); }); - copy_selections_content(editor, false, cx); + copy_selections_content(vim, editor, false, cx); editor.insert("", cx); // Fixup cursor position after the deletion diff --git a/crates/vim/src/normal/increment.rs b/crates/vim/src/normal/increment.rs index 6353a881ed..e70fce99e1 100644 --- a/crates/vim/src/normal/increment.rs +++ b/crates/vim/src/normal/increment.rs @@ -44,7 +44,7 @@ pub fn register(workspace: &mut Workspace, _: &mut ViewContext) { } fn increment(vim: &mut Vim, mut delta: i32, step: i32, cx: &mut WindowContext) { - vim.update_active_editor(cx, |editor, cx| { + vim.update_active_editor(cx, |vim, editor, cx| { let mut edits = Vec::new(); let mut new_anchors = Vec::new(); diff --git a/crates/vim/src/normal/paste.rs b/crates/vim/src/normal/paste.rs index a65a816654..9c15cc0cf4 100644 --- a/crates/vim/src/normal/paste.rs +++ b/crates/vim/src/normal/paste.rs @@ -1,14 +1,15 @@ -use std::{borrow::Cow, cmp}; +use std::cmp; use editor::{ display_map::ToDisplayPoint, movement, scroll::Autoscroll, ClipboardSelection, DisplayPoint, }; -use gpui::{impl_actions, ViewContext}; +use gpui::{impl_actions, AppContext, ViewContext}; use language::{Bias, SelectionGoal}; use serde::Deserialize; +use settings::Settings; use workspace::Workspace; -use crate::{state::Mode, utils::copy_selections_content, Vim}; +use crate::{state::Mode, utils::copy_selections_content, UseSystemClipboard, Vim, VimSettings}; #[derive(Clone, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] @@ -25,34 +26,60 @@ 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.") { + 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); - vim.update_active_editor(cx, |editor, cx| { + vim.update_active_editor(cx, |vim, editor, cx| { let text_layout_details = editor.text_layout_details(cx); editor.transact(cx, |editor, cx| { editor.set_clip_at_line_ends(false, cx); - let Some(item) = cx.read_from_clipboard() else { - return; - }; - let clipboard_text = Cow::Borrowed(item.text()); + let (clipboard_text, clipboard_selections): (String, Option<_>) = + 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.workspace_state + .registers + .get("\"") + .cloned() + .unwrap_or_else(|| "".to_string()), + 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) + } + }; + if clipboard_text.is_empty() { return; } if !action.preserve_clipboard && vim.state().mode.is_visual() { - copy_selections_content(editor, vim.state().mode == Mode::VisualLine, cx); + copy_selections_content(vim, editor, vim.state().mode == Mode::VisualLine, cx); } - // if we are copying from multi-cursor (of visual block mode), we want - // to - let clipboard_selections = - item.metadata::>() - .filter(|clipboard_selections| { - clipboard_selections.len() > 1 && vim.state().mode != Mode::VisualLine - }); - let (display_map, current_selections) = editor.selections.all_adjusted_display(cx); // unlike zed, if you have a multi-cursor selection from vim block mode, @@ -201,8 +228,11 @@ mod test { use crate::{ state::Mode, test::{NeovimBackedTestContext, VimTestContext}, + UseSystemClipboard, VimSettings, }; + use gpui::ClipboardItem; use indoc::indoc; + use settings::SettingsStore; #[gpui::test] async fn test_paste(cx: &mut gpui::TestAppContext) { @@ -291,6 +321,103 @@ mod test { .await; } + #[gpui::test] + async fn test_yank_system_clipboard_never(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! {" + The quick brown + fox jˇumps over + the lazy dog"}, + Mode::Normal, + ); + cx.simulate_keystrokes(["v", "i", "w", "y"]); + cx.assert_state( + indoc! {" + The quick brown + fox ˇjumps over + the lazy dog"}, + Mode::Normal, + ); + cx.simulate_keystroke("p"); + cx.assert_state( + indoc! {" + The quick brown + fox jjumpˇsumps over + the lazy dog"}, + Mode::Normal, + ); + assert_eq!(cx.read_from_clipboard(), None); + } + + #[gpui::test] + async fn test_yank_system_clipboard_on_yank(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::OnYank) + }); + }); + + // copy in visual mode + cx.set_state( + indoc! {" + The quick brown + fox jˇumps over + the lazy dog"}, + Mode::Normal, + ); + cx.simulate_keystrokes(["v", "i", "w", "y"]); + cx.assert_state( + indoc! {" + The quick brown + fox ˇjumps over + the lazy dog"}, + Mode::Normal, + ); + cx.simulate_keystroke("p"); + cx.assert_state( + indoc! {" + The quick brown + fox jjumpˇsumps over + the lazy dog"}, + Mode::Normal, + ); + assert_eq!( + cx.read_from_clipboard().map(|item| item.text().clone()), + Some("jumps".into()) + ); + cx.simulate_keystrokes(["d", "d", "p"]); + cx.assert_state( + indoc! {" + The quick brown + the lazy dog + ˇfox jjumpsumps over"}, + Mode::Normal, + ); + assert_eq!( + cx.read_from_clipboard().map(|item| item.text().clone()), + Some("jumps".into()) + ); + cx.write_to_clipboard(ClipboardItem::new("test-copy".to_string())); + cx.simulate_keystroke("shift-p"); + cx.assert_state( + indoc! {" + The quick brown + the lazy dog + test-copˇyfox jjumpsumps over"}, + Mode::Normal, + ); + } + #[gpui::test] async fn test_paste_visual(cx: &mut gpui::TestAppContext) { let mut cx = NeovimBackedTestContext::new(cx).await; diff --git a/crates/vim/src/normal/scroll.rs b/crates/vim/src/normal/scroll.rs index 0bccf24977..2b8b192225 100644 --- a/crates/vim/src/normal/scroll.rs +++ b/crates/vim/src/normal/scroll.rs @@ -52,7 +52,7 @@ fn scroll( ) { Vim::update(cx, |vim, cx| { let amount = by(vim.take_count(cx).map(|c| c as f32)); - vim.update_active_editor(cx, |editor, cx| { + vim.update_active_editor(cx, |_, editor, cx| { scroll_editor(editor, move_cursor, &amount, cx) }); }) diff --git a/crates/vim/src/normal/substitute.rs b/crates/vim/src/normal/substitute.rs index 63fa58dbed..7c138b2faf 100644 --- a/crates/vim/src/normal/substitute.rs +++ b/crates/vim/src/normal/substitute.rs @@ -29,7 +29,7 @@ pub(crate) fn register(workspace: &mut Workspace, _: &mut ViewContext } pub fn substitute(vim: &mut Vim, count: Option, line_mode: bool, cx: &mut WindowContext) { - vim.update_active_editor(cx, |editor, cx| { + vim.update_active_editor(cx, |vim, editor, cx| { editor.set_clip_at_line_ends(false, cx); editor.transact(cx, |editor, cx| { let text_layout_details = editor.text_layout_details(cx); @@ -72,7 +72,7 @@ pub fn substitute(vim: &mut Vim, count: Option, line_mode: bool, cx: &mut } }) }); - copy_selections_content(editor, line_mode, cx); + copy_selections_content(vim, editor, line_mode, cx); let selections = editor.selections.all::(cx).into_iter(); let edits = selections.map(|selection| (selection.start..selection.end, "")); editor.edit(edits, cx); diff --git a/crates/vim/src/normal/yank.rs b/crates/vim/src/normal/yank.rs index 7792fc4dd7..1220460d29 100644 --- a/crates/vim/src/normal/yank.rs +++ b/crates/vim/src/normal/yank.rs @@ -1,9 +1,9 @@ -use crate::{motion::Motion, object::Object, utils::copy_and_flash_selections_content, Vim}; +use crate::{motion::Motion, object::Object, utils::yank_selections_content, Vim}; use collections::HashMap; use gpui::WindowContext; pub fn yank_motion(vim: &mut Vim, motion: Motion, times: Option, cx: &mut WindowContext) { - vim.update_active_editor(cx, |editor, cx| { + vim.update_active_editor(cx, |vim, editor, cx| { let text_layout_details = editor.text_layout_details(cx); editor.transact(cx, |editor, cx| { editor.set_clip_at_line_ends(false, cx); @@ -15,7 +15,7 @@ pub fn yank_motion(vim: &mut Vim, motion: Motion, times: Option, cx: &mut motion.expand_selection(map, selection, times, true, &text_layout_details); }); }); - copy_and_flash_selections_content(editor, motion.linewise(), cx); + yank_selections_content(vim, editor, motion.linewise(), cx); editor.change_selections(None, cx, |s| { s.move_with(|_, selection| { let (head, goal) = original_positions.remove(&selection.id).unwrap(); @@ -27,7 +27,7 @@ pub fn yank_motion(vim: &mut Vim, motion: Motion, times: Option, cx: &mut } pub fn yank_object(vim: &mut Vim, object: Object, around: bool, cx: &mut WindowContext) { - vim.update_active_editor(cx, |editor, cx| { + vim.update_active_editor(cx, |vim, editor, cx| { editor.transact(cx, |editor, cx| { editor.set_clip_at_line_ends(false, cx); let mut original_positions: HashMap<_, _> = Default::default(); @@ -38,7 +38,7 @@ pub fn yank_object(vim: &mut Vim, object: Object, around: bool, cx: &mut WindowC original_positions.insert(selection.id, original_position); }); }); - copy_and_flash_selections_content(editor, false, cx); + yank_selections_content(vim, editor, false, cx); editor.change_selections(None, cx, |s| { s.move_with(|_, selection| { let (head, goal) = original_positions.remove(&selection.id).unwrap(); diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index 6dcb9c3ac3..7a610f8820 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -1,5 +1,6 @@ use std::{ops::Range, sync::Arc}; +use collections::HashMap; use gpui::{Action, KeyContext}; use language::CursorShape; use serde::{Deserialize, Serialize}; @@ -86,6 +87,8 @@ pub struct WorkspaceState { pub recorded_count: Option, pub recorded_actions: Vec, pub recorded_selection: RecordedSelection, + + pub registers: HashMap, } #[derive(Debug)] diff --git a/crates/vim/src/utils.rs b/crates/vim/src/utils.rs index 8b913fabbd..d0c099f64d 100644 --- a/crates/vim/src/utils.rs +++ b/crates/vim/src/utils.rs @@ -3,25 +3,35 @@ use std::time::Duration; use editor::{ClipboardSelection, Editor}; use gpui::{ClipboardItem, ViewContext}; use language::{CharKind, Point}; +use settings::Settings; + +use crate::{state::Mode, UseSystemClipboard, Vim, VimSettings}; pub struct HighlightOnYank; -pub fn copy_and_flash_selections_content( +pub fn yank_selections_content( + vim: &mut Vim, editor: &mut Editor, linewise: bool, cx: &mut ViewContext, ) { - copy_selections_content_internal(editor, linewise, true, cx); + copy_selections_content_internal(vim, editor, linewise, true, cx); } -pub fn copy_selections_content(editor: &mut Editor, linewise: bool, cx: &mut ViewContext) { - copy_selections_content_internal(editor, linewise, false, 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, - highlight: bool, + is_yank: bool, cx: &mut ViewContext, ) { let selections = editor.selections.all_adjusted(cx); @@ -73,8 +83,22 @@ fn copy_selections_content_internal( } } - cx.write_to_clipboard(ClipboardItem::new(text).with_metadata(clipboard_selections)); - if !highlight { + let setting = VimSettings::get_global(cx).use_system_clipboard; + if setting == UseSystemClipboard::Always || setting == UseSystemClipboard::OnYank && is_yank { + cx.write_to_clipboard(ClipboardItem::new(text.clone()).with_metadata(clipboard_selections)); + vim.workspace_state + .registers + .insert(".system.".to_string(), text.clone()); + } else { + vim.workspace_state.registers.insert( + ".system.".to_string(), + cx.read_from_clipboard() + .map(|item| item.text().clone()) + .unwrap_or_default(), + ); + } + vim.workspace_state.registers.insert("\"".to_string(), text); + if !is_yank || vim.state().mode == Mode::Visual { return; } diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index e8cca7b03c..d6cb6c056e 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -27,7 +27,9 @@ use language::{CursorShape, Point, Selection, SelectionGoal}; pub use mode_indicator::ModeIndicator; use motion::Motion; use normal::normal_replace; +use schemars::JsonSchema; use serde::Deserialize; +use serde_derive::Serialize; use settings::{update_settings_file, Settings, SettingsStore}; use state::{EditorState, Mode, Operator, RecordedSelection, WorkspaceState}; use std::{ops::Range, sync::Arc}; @@ -70,6 +72,7 @@ impl_actions!(vim, [SwitchMode, PushOperator, Number]); pub fn init(cx: &mut AppContext) { cx.set_global(Vim::default()); VimModeSetting::register(cx); + VimSettings::register(cx); editor_events::init(cx); @@ -261,12 +264,12 @@ impl Vim { } fn update_active_editor( - &self, + &mut self, cx: &mut WindowContext, - update: impl FnOnce(&mut Editor, &mut ViewContext) -> S, + update: impl FnOnce(&mut Vim, &mut Editor, &mut ViewContext) -> S, ) -> Option { let editor = self.active_editor.clone()?.upgrade()?; - Some(editor.update(cx, update)) + Some(editor.update(cx, |editor, cx| update(self, editor, cx))) } /// When doing an action that modifies the buffer, we start recording so that `.` @@ -365,7 +368,7 @@ impl Vim { } // Adjust selections - self.update_active_editor(cx, |editor, cx| { + self.update_active_editor(cx, |_, editor, cx| { if last_mode != Mode::VisualBlock && last_mode.is_visual() && mode == Mode::VisualBlock { visual_block_motion(true, editor, cx, |_, point, goal| Some((point, goal))) @@ -565,10 +568,9 @@ impl Vim { ret } - fn sync_vim_settings(&self, cx: &mut WindowContext) { - let state = self.state(); - - self.update_active_editor(cx, |editor, cx| { + fn sync_vim_settings(&mut self, cx: &mut WindowContext) { + self.update_active_editor(cx, |vim, editor, cx| { + let state = vim.state(); editor.set_cursor_shape(state.cursor_shape(), cx); editor.set_clip_at_line_ends(state.clip_at_line_ends(), cx); editor.set_collapse_matches(true); @@ -612,6 +614,42 @@ impl Settings for VimModeSetting { } } +/// Controls the soft-wrapping behavior in the editor. +#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum UseSystemClipboard { + Never, + Always, + OnYank, +} + +#[derive(Deserialize)] +struct VimSettings { + // all vim uses vim clipboard + // vim always uses system cliupbaord + // some magic where yy is system and dd is not. + pub use_system_clipboard: UseSystemClipboard, +} + +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)] +struct VimSettingsContent { + pub use_system_clipboard: Option, +} + +impl Settings for VimSettings { + const KEY: Option<&'static str> = Some("vim"); + + type FileContent = VimSettingsContent; + + fn load( + default_value: &Self::FileContent, + user_values: &[&Self::FileContent], + _: &mut AppContext, + ) -> Result { + Self::load_via_json_merge(default_value, user_values) + } +} + fn local_selections_changed( newest: Selection, is_multicursor: bool, diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index 09b8e2c1b8..c2e1f0106f 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -16,7 +16,7 @@ use crate::{ motion::{start_of_line, Motion}, object::Object, state::{Mode, Operator}, - utils::copy_selections_content, + utils::{copy_selections_content, yank_selections_content}, Vim, }; @@ -60,7 +60,7 @@ pub fn register(workspace: &mut Workspace, _: &mut ViewContext) { pub fn visual_motion(motion: Motion, times: Option, cx: &mut WindowContext) { Vim::update(cx, |vim, cx| { - vim.update_active_editor(cx, |editor, cx| { + vim.update_active_editor(cx, |vim, editor, cx| { let text_layout_details = editor.text_layout_details(cx); if vim.state().mode == Mode::VisualBlock && !matches!( @@ -251,7 +251,7 @@ pub fn visual_object(object: Object, cx: &mut WindowContext) { vim.switch_mode(target_mode, true, cx); } - vim.update_active_editor(cx, |editor, cx| { + vim.update_active_editor(cx, |_, editor, cx| { editor.change_selections(Some(Autoscroll::fit()), cx, |s| { s.move_with(|map, selection| { let mut head = selection.head(); @@ -298,7 +298,7 @@ fn toggle_mode(mode: Mode, cx: &mut ViewContext) { pub fn other_end(_: &mut Workspace, _: &OtherEnd, cx: &mut ViewContext) { Vim::update(cx, |vim, cx| { - vim.update_active_editor(cx, |editor, cx| { + vim.update_active_editor(cx, |_, editor, cx| { editor.change_selections(None, cx, |s| { s.move_with(|_, selection| { selection.reversed = !selection.reversed; @@ -311,7 +311,7 @@ pub fn other_end(_: &mut Workspace, _: &OtherEnd, cx: &mut ViewContext) { Vim::update(cx, |vim, cx| { vim.record_current_action(cx); - vim.update_active_editor(cx, |editor, cx| { + vim.update_active_editor(cx, |vim, editor, cx| { let mut original_columns: HashMap<_, _> = Default::default(); let line_mode = editor.selections.line_mode; @@ -328,7 +328,7 @@ pub fn delete(_: &mut Workspace, _: &VisualDelete, cx: &mut ViewContext) { Vim::update(cx, |vim, cx| { - vim.update_active_editor(cx, |editor, cx| { + vim.update_active_editor(cx, |vim, editor, cx| { let line_mode = editor.selections.line_mode; - copy_selections_content(editor, line_mode, cx); + yank_selections_content(vim, editor, line_mode, cx); editor.change_selections(None, cx, |s| { s.move_with(|map, selection| { if line_mode { @@ -377,7 +377,7 @@ pub fn yank(_: &mut Workspace, _: &VisualYank, cx: &mut ViewContext) pub(crate) fn visual_replace(text: Arc, cx: &mut WindowContext) { Vim::update(cx, |vim, cx| { vim.stop_recording(); - vim.update_active_editor(cx, |editor, cx| { + vim.update_active_editor(cx, |_, editor, cx| { editor.transact(cx, |editor, cx| { let (display_map, selections) = editor.selections.all_adjusted_display(cx); @@ -426,7 +426,7 @@ pub fn select_next( let count = vim.take_count(cx) .unwrap_or_else(|| if vim.state().mode.is_visual() { 1 } else { 2 }); - vim.update_active_editor(cx, |editor, cx| { + vim.update_active_editor(cx, |_, editor, cx| { for _ in 0..count { match editor.select_next(&Default::default(), cx) { Err(a) => return Err(a), @@ -448,7 +448,7 @@ pub fn select_previous( let count = vim.take_count(cx) .unwrap_or_else(|| if vim.state().mode.is_visual() { 1 } else { 2 }); - vim.update_active_editor(cx, |editor, cx| { + vim.update_active_editor(cx, |_, editor, cx| { for _ in 0..count { match editor.select_previous(&Default::default(), cx) { Err(a) => return Err(a),