mirror of
https://github.com/zed-industries/zed.git
synced 2024-11-08 07:35:01 +03:00
vim: Add (half of) ctrl-v/ctrl-q (#19585)
Release Notes: - vim: Add `ctrl-v`/`ctrl-q` to type any unicode code point. For example `ctrl-v escape` inserts an escape character(U+001B), or `ctrl-v u 1 0 E 2` types ტ (U+10E2). As in vim `ctrl-v ctrl-j` inserts U+0000 not U+000A. Zed does not yet implement insertion of the vim-specific representation of the typed keystroke for other keystrokes. - vim: Add `ctrl-shift-v` as an alias for paste on Linux
This commit is contained in:
parent
f8ab86f930
commit
75f1862268
@ -339,6 +339,10 @@
|
||||
"ctrl-t": "vim::Indent",
|
||||
"ctrl-d": "vim::Outdent",
|
||||
"ctrl-k": ["vim::PushOperator", { "Digraph": {} }],
|
||||
"ctrl-v": ["vim::PushOperator", { "Literal": {} }],
|
||||
"ctrl-shift-v": "editor::Paste", // note: this is *very* similar to ctrl-v in vim, but ctrl-shift-v on linux is the typical shortcut for paste when ctrl-v is already in use.
|
||||
"ctrl-q": ["vim::PushOperator", { "Literal": {} }],
|
||||
"ctrl-shift-q": ["vim::PushOperator", { "Literal": {} }],
|
||||
"ctrl-r": ["vim::PushOperator", "Register"],
|
||||
"insert": "vim::ToggleReplace"
|
||||
}
|
||||
@ -357,6 +361,10 @@
|
||||
"ctrl-c": "vim::NormalBefore",
|
||||
"ctrl-[": "vim::NormalBefore",
|
||||
"ctrl-k": ["vim::PushOperator", { "Digraph": {} }],
|
||||
"ctrl-v": ["vim::PushOperator", { "Literal": {} }],
|
||||
"ctrl-shift-v": "editor::Paste", // note: this is *very* similar to ctrl-v in vim, but ctrl-shift-v on linux is the typical shortcut for paste when ctrl-v is already in use.
|
||||
"ctrl-q": ["vim::PushOperator", { "Literal": {} }],
|
||||
"ctrl-shift-q": ["vim::PushOperator", { "Literal": {} }],
|
||||
"backspace": "vim::UndoReplace",
|
||||
"tab": "vim::Tab",
|
||||
"enter": "vim::Enter",
|
||||
@ -371,7 +379,9 @@
|
||||
"escape": "vim::ClearOperators",
|
||||
"ctrl-c": "vim::ClearOperators",
|
||||
"ctrl-[": "vim::ClearOperators",
|
||||
"ctrl-k": ["vim::PushOperator", { "Digraph": {} }]
|
||||
"ctrl-k": ["vim::PushOperator", { "Digraph": {} }],
|
||||
"ctrl-v": ["vim::PushOperator", { "Literal": {} }],
|
||||
"ctrl-q": ["vim::PushOperator", { "Literal": {} }]
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -485,6 +495,49 @@
|
||||
"c": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_mode == literal",
|
||||
"bindings": {
|
||||
"ctrl-@": ["vim::Literal", ["ctrl-@", "\u0000"]],
|
||||
"ctrl-a": ["vim::Literal", ["ctrl-a", "\u0001"]],
|
||||
"ctrl-b": ["vim::Literal", ["ctrl-b", "\u0002"]],
|
||||
"ctrl-c": ["vim::Literal", ["ctrl-c", "\u0003"]],
|
||||
"ctrl-d": ["vim::Literal", ["ctrl-d", "\u0004"]],
|
||||
"ctrl-e": ["vim::Literal", ["ctrl-e", "\u0005"]],
|
||||
"ctrl-f": ["vim::Literal", ["ctrl-f", "\u0006"]],
|
||||
"ctrl-g": ["vim::Literal", ["ctrl-g", "\u0007"]],
|
||||
"ctrl-h": ["vim::Literal", ["ctrl-h", "\u0008"]],
|
||||
"ctrl-i": ["vim::Literal", ["ctrl-i", "\u0009"]],
|
||||
"ctrl-j": ["vim::Literal", ["ctrl-j", "\u000A"]],
|
||||
"ctrl-k": ["vim::Literal", ["ctrl-k", "\u000B"]],
|
||||
"ctrl-l": ["vim::Literal", ["ctrl-l", "\u000C"]],
|
||||
"ctrl-m": ["vim::Literal", ["ctrl-m", "\u000D"]],
|
||||
"ctrl-n": ["vim::Literal", ["ctrl-n", "\u000E"]],
|
||||
"ctrl-o": ["vim::Literal", ["ctrl-o", "\u000F"]],
|
||||
"ctrl-p": ["vim::Literal", ["ctrl-p", "\u0010"]],
|
||||
"ctrl-q": ["vim::Literal", ["ctrl-q", "\u0011"]],
|
||||
"ctrl-r": ["vim::Literal", ["ctrl-r", "\u0012"]],
|
||||
"ctrl-s": ["vim::Literal", ["ctrl-s", "\u0013"]],
|
||||
"ctrl-t": ["vim::Literal", ["ctrl-t", "\u0014"]],
|
||||
"ctrl-u": ["vim::Literal", ["ctrl-u", "\u0015"]],
|
||||
"ctrl-v": ["vim::Literal", ["ctrl-v", "\u0016"]],
|
||||
"ctrl-w": ["vim::Literal", ["ctrl-w", "\u0017"]],
|
||||
"ctrl-x": ["vim::Literal", ["ctrl-x", "\u0018"]],
|
||||
"ctrl-y": ["vim::Literal", ["ctrl-y", "\u0019"]],
|
||||
"ctrl-z": ["vim::Literal", ["ctrl-z", "\u001A"]],
|
||||
"ctrl-[": ["vim::Literal", ["ctrl-[", "\u001B"]],
|
||||
"ctrl-\\": ["vim::Literal", ["ctrl-\\", "\u001C"]],
|
||||
"ctrl-]": ["vim::Literal", ["ctrl-]", "\u001D"]],
|
||||
"ctrl-^": ["vim::Literal", ["ctrl-^", "\u001E"]],
|
||||
"ctrl-_": ["vim::Literal", ["ctrl-_", "\u001F"]],
|
||||
"escape": ["vim::Literal", ["escape", "\u001B"]],
|
||||
"enter": ["vim::Literal", ["enter", "\u000D"]],
|
||||
"tab": ["vim::Literal", ["tab", "\u0009"]],
|
||||
// zed extensions:
|
||||
"backspace": ["vim::Literal", ["backspace", "\u0008"]],
|
||||
"delete": ["vim::Literal", ["delete", "\u007F"]]
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "BufferSearchBar && !in_replace",
|
||||
"bindings": {
|
||||
|
@ -1,15 +1,25 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use collections::HashMap;
|
||||
use gpui::AppContext;
|
||||
use editor::Editor;
|
||||
use gpui::{impl_actions, AppContext, Keystroke, KeystrokeEvent};
|
||||
use serde::Deserialize;
|
||||
use settings::Settings;
|
||||
use std::sync::LazyLock;
|
||||
use ui::ViewContext;
|
||||
|
||||
use crate::{Vim, VimSettings};
|
||||
use crate::{state::Operator, Vim, VimSettings};
|
||||
|
||||
mod default;
|
||||
|
||||
#[derive(PartialEq, Clone, Deserialize)]
|
||||
struct Literal(String, char);
|
||||
impl_actions!(vim, [Literal]);
|
||||
|
||||
pub(crate) fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
|
||||
Vim::action(editor, cx, Vim::literal)
|
||||
}
|
||||
|
||||
static DEFAULT_DIGRAPHS_MAP: LazyLock<HashMap<String, Arc<str>>> = LazyLock::new(|| {
|
||||
let mut map = HashMap::default();
|
||||
for &(a, b, c) in default::DEFAULT_DIGRAPHS {
|
||||
@ -50,6 +60,153 @@ impl Vim {
|
||||
self.input_ignored(text, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn literal(&mut self, action: &Literal, cx: &mut ViewContext<Self>) {
|
||||
if let Some(Operator::Literal { prefix }) = self.active_operator() {
|
||||
if let Some(prefix) = prefix {
|
||||
if let Some(keystroke) = Keystroke::parse(&action.0).ok() {
|
||||
cx.window_context().defer(|cx| {
|
||||
cx.dispatch_keystroke(keystroke);
|
||||
});
|
||||
}
|
||||
return self.handle_literal_input(prefix, "", cx);
|
||||
}
|
||||
}
|
||||
|
||||
self.insert_literal(Some(action.1), "", cx);
|
||||
}
|
||||
|
||||
pub fn handle_literal_keystroke(
|
||||
&mut self,
|
||||
keystroke_event: &KeystrokeEvent,
|
||||
prefix: String,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
// handled by handle_literal_input
|
||||
if keystroke_event.keystroke.ime_key.is_some() {
|
||||
return;
|
||||
};
|
||||
|
||||
if prefix.len() > 0 {
|
||||
self.handle_literal_input(prefix, "", cx);
|
||||
} else {
|
||||
self.pop_operator(cx);
|
||||
}
|
||||
|
||||
// give another chance to handle the binding outside
|
||||
// of waiting mode.
|
||||
if keystroke_event.action.is_none() {
|
||||
let keystroke = keystroke_event.keystroke.clone();
|
||||
cx.window_context().defer(|cx| {
|
||||
cx.dispatch_keystroke(keystroke);
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
pub fn handle_literal_input(
|
||||
&mut self,
|
||||
mut prefix: String,
|
||||
text: &str,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let first = prefix.chars().next();
|
||||
let next = text.chars().next().unwrap_or(' ');
|
||||
match first {
|
||||
Some('o' | 'O') => {
|
||||
if next.is_digit(8) {
|
||||
prefix.push(next);
|
||||
if prefix.len() == 4 {
|
||||
let ch: char = u8::from_str_radix(&prefix[1..], 8).unwrap_or(255).into();
|
||||
return self.insert_literal(Some(ch), "", cx);
|
||||
}
|
||||
} else {
|
||||
let ch = if prefix.len() > 1 {
|
||||
Some(u8::from_str_radix(&prefix[1..], 8).unwrap_or(255).into())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
return self.insert_literal(ch, text, cx);
|
||||
}
|
||||
}
|
||||
Some('x' | 'X' | 'u' | 'U') => {
|
||||
let max_len = match first.unwrap() {
|
||||
'x' => 3,
|
||||
'X' => 3,
|
||||
'u' => 5,
|
||||
'U' => 9,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
if next.is_ascii_hexdigit() {
|
||||
prefix.push(next);
|
||||
if prefix.len() == max_len {
|
||||
let ch: char = u32::from_str_radix(&prefix[1..], 16)
|
||||
.ok()
|
||||
.and_then(|n| n.try_into().ok())
|
||||
.unwrap_or('\u{FFFD}');
|
||||
return self.insert_literal(Some(ch), "", cx);
|
||||
}
|
||||
} else {
|
||||
let ch = if prefix.len() > 1 {
|
||||
Some(
|
||||
u32::from_str_radix(&prefix[1..], 16)
|
||||
.ok()
|
||||
.and_then(|n| n.try_into().ok())
|
||||
.unwrap_or('\u{FFFD}'),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
return self.insert_literal(ch, text, cx);
|
||||
}
|
||||
}
|
||||
Some('0'..='9') => {
|
||||
if next.is_ascii_hexdigit() {
|
||||
prefix.push(next);
|
||||
if prefix.len() == 3 {
|
||||
let ch: char = u8::from_str_radix(&prefix, 10).unwrap_or(255).into();
|
||||
return self.insert_literal(Some(ch), "", cx);
|
||||
}
|
||||
} else {
|
||||
let ch: char = u8::from_str_radix(&prefix, 10).unwrap_or(255).into();
|
||||
return self.insert_literal(Some(ch), "", cx);
|
||||
}
|
||||
}
|
||||
None if matches!(next, 'o' | 'O' | 'x' | 'X' | 'u' | 'U' | '0'..='9') => {
|
||||
prefix.push(next)
|
||||
}
|
||||
_ => {
|
||||
return self.insert_literal(None, text, cx);
|
||||
}
|
||||
};
|
||||
|
||||
self.pop_operator(cx);
|
||||
self.push_operator(
|
||||
Operator::Literal {
|
||||
prefix: Some(prefix),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
fn insert_literal(&mut self, ch: Option<char>, suffix: &str, cx: &mut ViewContext<Self>) {
|
||||
self.pop_operator(cx);
|
||||
let mut text = String::new();
|
||||
if let Some(c) = ch {
|
||||
if c == '\n' {
|
||||
text.push('\x00')
|
||||
} else {
|
||||
text.push(c)
|
||||
}
|
||||
}
|
||||
text.push_str(suffix);
|
||||
|
||||
if self.editor_input_enabled() {
|
||||
self.update_editor(cx, |_, editor, cx| editor.insert(&text, cx));
|
||||
} else {
|
||||
self.input_ignored(text.into(), cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@ -154,4 +311,43 @@ mod test {
|
||||
cx.simulate_shared_keystrokes("a ctrl-k s , escape").await;
|
||||
cx.shared_state().await.assert_eq("Helloˇş");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_ctrl_v(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state("ˇ").await;
|
||||
cx.simulate_shared_keystrokes("i ctrl-v 0 0 0").await;
|
||||
cx.shared_state().await.assert_eq("\x00ˇ");
|
||||
|
||||
cx.simulate_shared_keystrokes("ctrl-v j").await;
|
||||
cx.shared_state().await.assert_eq("\x00jˇ");
|
||||
cx.simulate_shared_keystrokes("ctrl-v x 6 5").await;
|
||||
cx.shared_state().await.assert_eq("\x00jeˇ");
|
||||
cx.simulate_shared_keystrokes("ctrl-v U 1 F 6 4 0 space")
|
||||
.await;
|
||||
cx.shared_state().await.assert_eq("\x00je🙀 ˇ");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_ctrl_v_escape(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await;
|
||||
cx.set_shared_state("ˇ").await;
|
||||
cx.simulate_shared_keystrokes("i ctrl-v 9 escape").await;
|
||||
cx.shared_state().await.assert_eq("ˇ\t");
|
||||
cx.simulate_shared_keystrokes("i ctrl-v escape").await;
|
||||
cx.shared_state().await.assert_eq("\x1bˇ\t");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_ctrl_v_control(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await;
|
||||
cx.set_shared_state("ˇ").await;
|
||||
cx.simulate_shared_keystrokes("i ctrl-v ctrl-d").await;
|
||||
cx.shared_state().await.assert_eq("\x04ˇ");
|
||||
cx.simulate_shared_keystrokes("ctrl-v ctrl-j").await;
|
||||
cx.shared_state().await.assert_eq("\x04\x00ˇ");
|
||||
cx.simulate_shared_keystrokes("ctrl-v tab").await;
|
||||
cx.shared_state().await.assert_eq("\x04\x00\x09ˇ");
|
||||
}
|
||||
}
|
||||
|
@ -70,7 +70,11 @@ impl ModeIndicator {
|
||||
recording
|
||||
.chain(vim.pre_count.map(|count| format!("{}", count)))
|
||||
.chain(vim.selected_register.map(|reg| format!("\"{reg}")))
|
||||
.chain(vim.operator_stack.iter().map(|item| item.id().to_string()))
|
||||
.chain(
|
||||
vim.operator_stack
|
||||
.iter()
|
||||
.map(|item| item.status().to_string()),
|
||||
)
|
||||
.chain(vim.post_count.map(|count| format!("{}", count)))
|
||||
.collect::<Vec<_>>()
|
||||
.join("")
|
||||
|
@ -77,6 +77,7 @@ pub enum Operator {
|
||||
Uppercase,
|
||||
OppositeCase,
|
||||
Digraph { first_char: Option<char> },
|
||||
Literal { prefix: Option<String> },
|
||||
Register,
|
||||
RecordRegister,
|
||||
ReplayRegister,
|
||||
@ -444,6 +445,7 @@ impl Operator {
|
||||
Operator::Yank => "y",
|
||||
Operator::Replace => "r",
|
||||
Operator::Digraph { .. } => "^K",
|
||||
Operator::Literal { .. } => "^V",
|
||||
Operator::FindForward { before: false } => "f",
|
||||
Operator::FindForward { before: true } => "t",
|
||||
Operator::FindBackward { after: false } => "F",
|
||||
@ -467,6 +469,18 @@ impl Operator {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn status(&self) -> String {
|
||||
match self {
|
||||
Operator::Digraph {
|
||||
first_char: Some(first_char),
|
||||
} => format!("^K{first_char}"),
|
||||
Operator::Literal {
|
||||
prefix: Some(prefix),
|
||||
} => format!("^V{prefix}"),
|
||||
_ => self.id().to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_waiting(&self, mode: Mode) -> bool {
|
||||
match self {
|
||||
Operator::AddSurrounds { target } => target.is_some() || mode.is_visual(),
|
||||
@ -479,6 +493,7 @@ impl Operator {
|
||||
| Operator::ReplayRegister
|
||||
| Operator::Replace
|
||||
| Operator::Digraph { .. }
|
||||
| Operator::Literal { .. }
|
||||
| Operator::ChangeSurrounds { target: Some(_) }
|
||||
| Operator::DeleteSurrounds => true,
|
||||
Operator::Change
|
||||
|
@ -296,6 +296,7 @@ impl Vim {
|
||||
object::register(editor, cx);
|
||||
visual::register(editor, cx);
|
||||
change_list::register(editor, cx);
|
||||
digraph::register(editor, cx);
|
||||
|
||||
cx.defer(|vim, cx| {
|
||||
vim.focused(false, cx);
|
||||
@ -359,9 +360,15 @@ impl Vim {
|
||||
}
|
||||
|
||||
if let Some(operator) = self.active_operator() {
|
||||
if !operator.is_waiting(self.mode) {
|
||||
self.clear_operator(cx);
|
||||
self.stop_recording_immediately(Box::new(ClearOperators), cx)
|
||||
match operator {
|
||||
Operator::Literal { prefix } => {
|
||||
self.handle_literal_keystroke(keystroke_event, prefix.unwrap_or_default(), cx);
|
||||
}
|
||||
_ if !operator.is_waiting(self.mode) => {
|
||||
self.clear_operator(cx);
|
||||
self.stop_recording_immediately(Box::new(ClearOperators), cx)
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -602,14 +609,18 @@ impl Vim {
|
||||
|
||||
if let Some(active_operator) = active_operator {
|
||||
if active_operator.is_waiting(self.mode) {
|
||||
mode = "waiting".to_string();
|
||||
if matches!(active_operator, Operator::Literal { .. }) {
|
||||
mode = "literal".to_string();
|
||||
} else {
|
||||
mode = "waiting".to_string();
|
||||
}
|
||||
} else {
|
||||
mode = "operator".to_string();
|
||||
operator_id = active_operator.id();
|
||||
mode = "operator".to_string();
|
||||
}
|
||||
}
|
||||
|
||||
if mode != "waiting" && mode != "insert" && mode != "replace" {
|
||||
if mode == "normal" || mode == "visual" || mode == "operator" {
|
||||
context.add("VimControl");
|
||||
}
|
||||
context.set("vim_mode", mode);
|
||||
@ -998,6 +1009,9 @@ impl Vim {
|
||||
self.push_operator(Operator::Digraph { first_char }, cx);
|
||||
}
|
||||
}
|
||||
Some(Operator::Literal { prefix }) => {
|
||||
self.handle_literal_input(prefix.unwrap_or_default(), &text, cx)
|
||||
}
|
||||
Some(Operator::AddSurrounds { target }) => match self.mode {
|
||||
Mode::Normal => {
|
||||
if let Some(target) = target {
|
||||
|
24
crates/vim/test_data/test_ctrl_v.json
Normal file
24
crates/vim/test_data/test_ctrl_v.json
Normal file
@ -0,0 +1,24 @@
|
||||
{"Put":{"state":"ˇ"}}
|
||||
{"Key":"i"}
|
||||
{"Key":"ctrl-v"}
|
||||
{"Key":"0"}
|
||||
{"Key":"0"}
|
||||
{"Key":"0"}
|
||||
{"Get":{"state":"\u0000ˇ","mode":"Insert"}}
|
||||
{"Key":"ctrl-v"}
|
||||
{"Key":"j"}
|
||||
{"Get":{"state":"\u0000jˇ","mode":"Insert"}}
|
||||
{"Key":"ctrl-v"}
|
||||
{"Key":"x"}
|
||||
{"Key":"6"}
|
||||
{"Key":"5"}
|
||||
{"Get":{"state":"\u0000jeˇ","mode":"Insert"}}
|
||||
{"Key":"ctrl-v"}
|
||||
{"Key":"U"}
|
||||
{"Key":"1"}
|
||||
{"Key":"F"}
|
||||
{"Key":"6"}
|
||||
{"Key":"4"}
|
||||
{"Key":"0"}
|
||||
{"Key":"space"}
|
||||
{"Get":{"state":"\u0000je🙀 ˇ","mode":"Insert"}}
|
11
crates/vim/test_data/test_ctrl_v_control.json
Normal file
11
crates/vim/test_data/test_ctrl_v_control.json
Normal file
@ -0,0 +1,11 @@
|
||||
{"Put":{"state":"ˇ"}}
|
||||
{"Key":"i"}
|
||||
{"Key":"ctrl-v"}
|
||||
{"Key":"ctrl-d"}
|
||||
{"Get":{"state":"\u0004ˇ","mode":"Insert"}}
|
||||
{"Key":"ctrl-v"}
|
||||
{"Key":"ctrl-j"}
|
||||
{"Get":{"state":"\u0004\u0000ˇ","mode":"Insert"}}
|
||||
{"Key":"ctrl-v"}
|
||||
{"Key":"tab"}
|
||||
{"Get":{"state":"\u0004\u0000\tˇ","mode":"Insert"}}
|
10
crates/vim/test_data/test_ctrl_v_escape.json
Normal file
10
crates/vim/test_data/test_ctrl_v_escape.json
Normal file
@ -0,0 +1,10 @@
|
||||
{"Put":{"state":"ˇ"}}
|
||||
{"Key":"i"}
|
||||
{"Key":"ctrl-v"}
|
||||
{"Key":"9"}
|
||||
{"Key":"escape"}
|
||||
{"Get":{"state":"ˇ\t","mode":"Normal"}}
|
||||
{"Key":"i"}
|
||||
{"Key":"ctrl-v"}
|
||||
{"Key":"escape"}
|
||||
{"Get":{"state":"\u001bˇ\t","mode":"Insert"}}
|
Loading…
Reference in New Issue
Block a user