Detailed changes
@@ -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 {
@@ -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"}}
@@ -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"}}
@@ -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"}}