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
8 changed files with 337 additions and 10 deletions
|
@ -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ˇ");
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue