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