Add option to either use system clipboard or vim clipboard (#7936)

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>
This commit is contained in:
Mahdy M. Karam 2024-02-22 09:12:29 -08:00 committed by GitHub
parent c6826a61a0
commit 5c4f3c0cea
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 453 additions and 116 deletions

1
Cargo.lock generated
View file

@ -10903,6 +10903,7 @@ dependencies = [
"project",
"regex",
"release_channel",
"schemars",
"search",
"serde",
"serde_derive",

View file

@ -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"
]
}
},
{

View file

@ -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",

View file

@ -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"] }

View file

@ -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, _| {

View file

@ -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);

View file

@ -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);

View file

@ -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);
}
});

View file

@ -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

View file

@ -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();

View file

@ -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;

View file

@ -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)
});
})

View file

@ -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);

View file

@ -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();

View file

@ -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)]

View file

@ -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;
}

View file

@ -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,

View file

@ -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),