Cargo.lock 🔗
@@ -10903,6 +10903,7 @@ dependencies = [
"project",
"regex",
"release_channel",
+ "schemars",
"search",
"serde",
"serde_derive",
Mahdy M. Karam and Conrad Irwin created
Release Notes:
- vim: Added a setting to control default clipboard behaviour. `{"vim":
{"use_system_clipboard": "never"}}` disables writing to the clipboard.
`"on_yank"` writes to the system clipboard only on yank, and `"always"`
preserves the current behavior. ([#4390
](https://github.com/zed-industries/zed/issues/4390))
---------
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
Cargo.lock | 1
assets/keymaps/vim.json | 220 ++++++++++++++++++++++++------
assets/settings/default.json | 15 +
crates/vim/Cargo.toml | 1
crates/vim/src/insert.rs | 2
crates/vim/src/normal.rs | 16 +-
crates/vim/src/normal/case.rs | 2
crates/vim/src/normal/change.rs | 8
crates/vim/src/normal/delete.rs | 8
crates/vim/src/normal/increment.rs | 2
crates/vim/src/normal/paste.rs | 161 ++++++++++++++++++++--
crates/vim/src/normal/scroll.rs | 2
crates/vim/src/normal/substitute.rs | 4
crates/vim/src/normal/yank.rs | 10
crates/vim/src/state.rs | 3
crates/vim/src/utils.rs | 38 ++++
crates/vim/src/vim.rs | 54 ++++++-
crates/vim/src/visual.rs | 22 +-
18 files changed, 453 insertions(+), 116 deletions(-)
@@ -10903,6 +10903,7 @@ dependencies = [
"project",
"regex",
"release_channel",
+ "schemars",
"search",
"serde",
"serde_derive",
@@ -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"
+ ]
}
},
{
@@ -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",
@@ -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"] }
@@ -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, _| {
@@ -119,7 +119,7 @@ pub(crate) fn register(workspace: &mut Workspace, cx: &mut ViewContext<Workspace
times -= 1;
}
- vim.update_active_editor(cx, |editor, cx| {
+ vim.update_active_editor(cx, |_, editor, cx| {
editor.transact(cx, |editor, cx| {
for _ in 0..times {
editor.join_lines(&Default::default(), cx)
@@ -182,7 +182,7 @@ pub(crate) fn move_cursor(
times: Option<usize>,
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<Workspa
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| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_cursors_with(|map, cursor, _| (right(map, cursor, 1), SelectionGoal::None));
});
@@ -221,7 +221,7 @@ fn insert_first_non_whitespace(
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| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_cursors_with(|map, cursor, _| {
(
@@ -238,7 +238,7 @@ fn insert_end_of_line(_: &mut Workspace, _: &InsertEndOfLine, cx: &mut ViewConte
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| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_cursors_with(|map, cursor, _| {
(next_line_end(map, cursor, 1), SelectionGoal::None)
@@ -252,7 +252,7 @@ fn insert_line_above(_: &mut Workspace, _: &InsertLineAbove, 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| {
editor.transact(cx, |editor, cx| {
let (map, old_selections) = editor.selections.all_display(cx);
let selection_start_rows: HashSet<u32> = 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<Workspace>) {
pub(crate) fn normal_replace(text: Arc<str>, 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);
@@ -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);
@@ -24,7 +24,7 @@ pub fn change_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, 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<usize>, 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<usize>, 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);
}
});
@@ -6,7 +6,7 @@ use language::Point;
pub fn delete_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, 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<usize>, 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<usize>, 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
@@ -44,7 +44,7 @@ pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
}
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();
@@ -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>
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<Workspace>) {
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::<Vec<ClipboardSelection>>()
+ .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::<Vec<ClipboardSelection>>()
- .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::<VimSettings>(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::<VimSettings>(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;
@@ -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)
});
})
@@ -29,7 +29,7 @@ pub(crate) fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>
}
pub fn substitute(vim: &mut Vim, count: Option<usize>, 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<usize>, 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::<Point>(cx).into_iter();
let edits = selections.map(|selection| (selection.start..selection.end, ""));
editor.edit(edits, cx);
@@ -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<usize>, 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<usize>, 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<usize>, 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();
@@ -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<usize>,
pub recorded_actions: Vec<ReplayableAction>,
pub recorded_selection: RecordedSelection,
+
+ pub registers: HashMap<String, String>,
}
#[derive(Debug)]
@@ -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<Editor>,
) {
- 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<Editor>) {
- copy_selections_content_internal(editor, linewise, false, cx);
+pub fn copy_selections_content(
+ vim: &mut Vim,
+ editor: &mut Editor,
+ linewise: bool,
+ cx: &mut ViewContext<Editor>,
+) {
+ 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<Editor>,
) {
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;
}
@@ -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<S>(
- &self,
+ &mut self,
cx: &mut WindowContext,
- update: impl FnOnce(&mut Editor, &mut ViewContext<Editor>) -> S,
+ update: impl FnOnce(&mut Vim, &mut Editor, &mut ViewContext<Editor>) -> S,
) -> Option<S> {
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<UseSystemClipboard>,
+}
+
+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> {
+ Self::load_via_json_merge(default_value, user_values)
+ }
+}
+
fn local_selections_changed(
newest: Selection<usize>,
is_multicursor: bool,
@@ -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<Workspace>) {
pub fn visual_motion(motion: Motion, times: Option<usize>, 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<Workspace>) {
pub fn other_end(_: &mut Workspace, _: &OtherEnd, cx: &mut ViewContext<Workspace>) {
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<Workspace
pub fn delete(_: &mut Workspace, _: &VisualDelete, cx: &mut ViewContext<Workspace>) {
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<Workspac
selection.goal = SelectionGoal::None;
});
});
- copy_selections_content(editor, line_mode, cx);
+ copy_selections_content(vim, editor, line_mode, cx);
editor.insert("", cx);
// Fixup cursor position after the deletion
@@ -355,9 +355,9 @@ pub fn delete(_: &mut Workspace, _: &VisualDelete, cx: &mut ViewContext<Workspac
pub fn yank(_: &mut Workspace, _: &VisualYank, cx: &mut ViewContext<Workspace>) {
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<Workspace>)
pub(crate) fn visual_replace(text: Arc<str>, 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),