1use std::sync::Arc;
2
3use collections::HashMap;
4use editor::Editor;
5use gpui::{Action, App, Context, Keystroke, KeystrokeEvent, Window};
6use schemars::JsonSchema;
7use serde::Deserialize;
8use settings::Settings;
9use std::sync::LazyLock;
10
11use crate::{Vim, VimSettings, state::Operator};
12
13mod default;
14
15#[derive(Debug, Clone, Deserialize, JsonSchema, PartialEq, Action)]
16#[action(namespace = vim)]
17struct Literal(String, char);
18
19pub(crate) fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
20 Vim::action(editor, cx, Vim::literal)
21}
22
23static DEFAULT_DIGRAPHS_MAP: LazyLock<HashMap<String, Arc<str>>> = LazyLock::new(|| {
24 let mut map = HashMap::default();
25 for &(a, b, c) in default::DEFAULT_DIGRAPHS {
26 let key = format!("{a}{b}");
27 let value = char::from_u32(c).unwrap().to_string().into();
28 map.insert(key, value);
29 }
30 map
31});
32
33fn lookup_digraph(a: char, b: char, cx: &App) -> Arc<str> {
34 let custom_digraphs = &VimSettings::get_global(cx).custom_digraphs;
35 let input = format!("{a}{b}");
36 let reversed = format!("{b}{a}");
37
38 custom_digraphs
39 .get(&input)
40 .or_else(|| DEFAULT_DIGRAPHS_MAP.get(&input))
41 .or_else(|| custom_digraphs.get(&reversed))
42 .or_else(|| DEFAULT_DIGRAPHS_MAP.get(&reversed))
43 .cloned()
44 .unwrap_or_else(|| b.to_string().into())
45}
46
47impl Vim {
48 pub fn insert_digraph(
49 &mut self,
50 first_char: char,
51 second_char: char,
52 window: &mut Window,
53 cx: &mut Context<Self>,
54 ) {
55 let text = lookup_digraph(first_char, second_char, cx);
56
57 self.pop_operator(window, cx);
58 if self.editor_input_enabled() {
59 self.update_editor(cx, |_, editor, cx| editor.insert(&text, window, cx));
60 } else {
61 self.input_ignored(text, window, cx);
62 }
63 }
64
65 fn literal(&mut self, action: &Literal, window: &mut Window, cx: &mut Context<Self>) {
66 match self.active_operator() {
67 Some(Operator::Literal {
68 prefix: Some(prefix),
69 }) => {
70 if let Some(keystroke) = Keystroke::parse(&action.0).ok() {
71 window.defer(cx, |window, cx| {
72 window.dispatch_keystroke(keystroke, cx);
73 });
74 }
75 return self.handle_literal_input(prefix, "", window, cx);
76 }
77 Some(_) => self.insert_literal(Some(action.1), "", window, cx),
78 None => log::error!(
79 "Literal called when no operator was on the stack. This likely means there is an invalid keymap config"
80 ),
81 }
82 }
83
84 pub fn handle_literal_keystroke(
85 &mut self,
86 keystroke_event: &KeystrokeEvent,
87 prefix: String,
88 window: &mut Window,
89 cx: &mut Context<Self>,
90 ) {
91 // handled by handle_literal_input
92 if keystroke_event.keystroke.key_char.is_some() {
93 return;
94 };
95
96 if !prefix.is_empty() {
97 self.handle_literal_input(prefix, "", window, cx);
98 } else {
99 self.pop_operator(window, cx);
100 }
101
102 // give another chance to handle the binding outside
103 // of waiting mode.
104 if keystroke_event.action.is_none() {
105 let keystroke = keystroke_event.keystroke.clone();
106 window.defer(cx, |window, cx| {
107 window.dispatch_keystroke(keystroke, cx);
108 });
109 }
110 }
111
112 pub fn handle_literal_input(
113 &mut self,
114 mut prefix: String,
115 text: &str,
116 window: &mut Window,
117 cx: &mut Context<Self>,
118 ) {
119 let first = prefix.chars().next();
120 let next = text.chars().next().unwrap_or(' ');
121 match first {
122 Some('o' | 'O') => {
123 if next.is_digit(8) {
124 prefix.push(next);
125 if prefix.len() == 4 {
126 let ch: char = u8::from_str_radix(&prefix[1..], 8).unwrap_or(255).into();
127 return self.insert_literal(Some(ch), "", window, cx);
128 }
129 } else {
130 let ch = if prefix.len() > 1 {
131 Some(u8::from_str_radix(&prefix[1..], 8).unwrap_or(255).into())
132 } else {
133 None
134 };
135 return self.insert_literal(ch, text, window, cx);
136 }
137 }
138 Some('x' | 'X' | 'u' | 'U') => {
139 let max_len = match first.unwrap() {
140 'x' => 3,
141 'X' => 3,
142 'u' => 5,
143 'U' => 9,
144 _ => unreachable!(),
145 };
146 if next.is_ascii_hexdigit() {
147 prefix.push(next);
148 if prefix.len() == max_len {
149 let ch: char = u32::from_str_radix(&prefix[1..], 16)
150 .ok()
151 .and_then(|n| n.try_into().ok())
152 .unwrap_or('\u{FFFD}');
153 return self.insert_literal(Some(ch), "", window, cx);
154 }
155 } else {
156 let ch = if prefix.len() > 1 {
157 Some(
158 u32::from_str_radix(&prefix[1..], 16)
159 .ok()
160 .and_then(|n| n.try_into().ok())
161 .unwrap_or('\u{FFFD}'),
162 )
163 } else {
164 None
165 };
166 return self.insert_literal(ch, text, window, cx);
167 }
168 }
169 Some('0'..='9') => {
170 if next.is_ascii_hexdigit() {
171 prefix.push(next);
172 if prefix.len() == 3 {
173 let ch: char = u8::from_str_radix(&prefix, 10).unwrap_or(255).into();
174 return self.insert_literal(Some(ch), "", window, cx);
175 }
176 } else {
177 let ch: char = u8::from_str_radix(&prefix, 10).unwrap_or(255).into();
178 return self.insert_literal(Some(ch), "", window, cx);
179 }
180 }
181 None if matches!(next, 'o' | 'O' | 'x' | 'X' | 'u' | 'U' | '0'..='9') => {
182 prefix.push(next)
183 }
184 _ => {
185 return self.insert_literal(None, text, window, cx);
186 }
187 };
188
189 self.pop_operator(window, cx);
190 self.push_operator(
191 Operator::Literal {
192 prefix: Some(prefix),
193 },
194 window,
195 cx,
196 );
197 }
198
199 fn insert_literal(
200 &mut self,
201 ch: Option<char>,
202 suffix: &str,
203 window: &mut Window,
204 cx: &mut Context<Self>,
205 ) {
206 self.pop_operator(window, cx);
207 let mut text = String::new();
208 if let Some(c) = ch {
209 if c == '\n' {
210 text.push('\x00')
211 } else {
212 text.push(c)
213 }
214 }
215 text.push_str(suffix);
216
217 if self.editor_input_enabled() {
218 self.update_editor(cx, |_, editor, cx| editor.insert(&text, window, cx));
219 } else {
220 self.input_ignored(text.into(), window, cx);
221 }
222 }
223}
224
225#[cfg(test)]
226mod test {
227 use collections::HashMap;
228 use settings::SettingsStore;
229
230 use crate::{
231 state::Mode,
232 test::{NeovimBackedTestContext, VimTestContext},
233 };
234
235 #[gpui::test]
236 async fn test_digraph_insert_mode(cx: &mut gpui::TestAppContext) {
237 let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await;
238
239 cx.set_shared_state("Hellˇo").await;
240 cx.simulate_shared_keystrokes("a ctrl-k o : escape").await;
241 cx.shared_state().await.assert_eq("Helloˇö");
242
243 cx.set_shared_state("Hellˇo").await;
244 cx.simulate_shared_keystrokes("a ctrl-k : o escape").await;
245 cx.shared_state().await.assert_eq("Helloˇö");
246
247 cx.set_shared_state("Hellˇo").await;
248 cx.simulate_shared_keystrokes("i ctrl-k o : escape").await;
249 cx.shared_state().await.assert_eq("Hellˇöo");
250 }
251
252 #[gpui::test]
253 async fn test_digraph_insert_multicursor(cx: &mut gpui::TestAppContext) {
254 let mut cx: VimTestContext = VimTestContext::new(cx, true).await;
255
256 cx.set_state("Hellˇo wˇorld", Mode::Normal);
257 cx.simulate_keystrokes("a ctrl-k o : escape");
258 cx.assert_state("Helloˇö woˇörld", Mode::Normal);
259 }
260
261 #[gpui::test]
262 async fn test_digraph_replace(cx: &mut gpui::TestAppContext) {
263 let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await;
264
265 cx.set_shared_state("Hellˇo").await;
266 cx.simulate_shared_keystrokes("r ctrl-k o :").await;
267 cx.shared_state().await.assert_eq("Hellˇö");
268 }
269
270 #[gpui::test]
271 async fn test_digraph_find(cx: &mut gpui::TestAppContext) {
272 let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await;
273
274 cx.set_shared_state("ˇHellö world").await;
275 cx.simulate_shared_keystrokes("f ctrl-k o :").await;
276 cx.shared_state().await.assert_eq("Hellˇö world");
277
278 cx.set_shared_state("ˇHellö world").await;
279 cx.simulate_shared_keystrokes("t ctrl-k o :").await;
280 cx.shared_state().await.assert_eq("Helˇlö world");
281 }
282
283 #[gpui::test]
284 async fn test_digraph_replace_mode(cx: &mut gpui::TestAppContext) {
285 let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await;
286
287 cx.set_shared_state("ˇHello").await;
288 cx.simulate_shared_keystrokes(
289 "shift-r ctrl-k a ' ctrl-k e ` ctrl-k i : ctrl-k o ~ ctrl-k u - escape",
290 )
291 .await;
292 cx.shared_state().await.assert_eq("áèïõˇū");
293 }
294
295 #[gpui::test]
296 async fn test_digraph_custom(cx: &mut gpui::TestAppContext) {
297 let mut cx: VimTestContext = VimTestContext::new(cx, true).await;
298
299 cx.update_global(|store: &mut SettingsStore, cx| {
300 store.update_user_settings(cx, |s| {
301 let mut custom_digraphs = HashMap::default();
302 custom_digraphs.insert("|-".into(), "⊢".into());
303 custom_digraphs.insert(":)".into(), "👨💻".into());
304 s.vim.get_or_insert_default().custom_digraphs = Some(custom_digraphs);
305 });
306 });
307
308 cx.set_state("ˇ", Mode::Normal);
309 cx.simulate_keystrokes("a ctrl-k | - escape");
310 cx.assert_state("ˇ⊢", Mode::Normal);
311
312 // Test support for multi-codepoint mappings
313 cx.set_state("ˇ", Mode::Normal);
314 cx.simulate_keystrokes("a ctrl-k : ) escape");
315 cx.assert_state("ˇ👨💻", Mode::Normal);
316 }
317
318 #[gpui::test]
319 async fn test_digraph_keymap_conflict(cx: &mut gpui::TestAppContext) {
320 let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await;
321
322 cx.set_shared_state("Hellˇo").await;
323 cx.simulate_shared_keystrokes("a ctrl-k s , escape").await;
324 cx.shared_state().await.assert_eq("Helloˇş");
325 }
326
327 #[gpui::test]
328 async fn test_ctrl_v(cx: &mut gpui::TestAppContext) {
329 let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await;
330
331 cx.set_shared_state("ˇ").await;
332 cx.simulate_shared_keystrokes("i ctrl-v 0 0 0").await;
333 cx.shared_state().await.assert_eq("\x00ˇ");
334
335 cx.simulate_shared_keystrokes("ctrl-v j").await;
336 cx.shared_state().await.assert_eq("\x00jˇ");
337 cx.simulate_shared_keystrokes("ctrl-v x 6 5").await;
338 cx.shared_state().await.assert_eq("\x00jeˇ");
339 cx.simulate_shared_keystrokes("ctrl-v U 1 F 6 4 0 space")
340 .await;
341 cx.shared_state().await.assert_eq("\x00je🙀 ˇ");
342 }
343
344 #[gpui::test]
345 async fn test_ctrl_v_escape(cx: &mut gpui::TestAppContext) {
346 let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await;
347 cx.set_shared_state("ˇ").await;
348 cx.simulate_shared_keystrokes("i ctrl-v 9 escape").await;
349 cx.shared_state().await.assert_eq("ˇ\t");
350 cx.simulate_shared_keystrokes("i ctrl-v escape").await;
351 cx.shared_state().await.assert_eq("\x1bˇ\t");
352 }
353
354 #[gpui::test]
355 async fn test_ctrl_v_control(cx: &mut gpui::TestAppContext) {
356 let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await;
357 cx.set_shared_state("ˇ").await;
358 cx.simulate_shared_keystrokes("i ctrl-v ctrl-d").await;
359 cx.shared_state().await.assert_eq("\x04ˇ");
360 cx.simulate_shared_keystrokes("ctrl-v ctrl-j").await;
361 cx.shared_state().await.assert_eq("\x04\x00ˇ");
362 cx.simulate_shared_keystrokes("ctrl-v tab").await;
363 cx.shared_state().await.assert_eq("\x04\x00\x09ˇ");
364 }
365}