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