digraph.rs

  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        VimSettings,
228        state::Mode,
229        test::{NeovimBackedTestContext, VimTestContext},
230    };
231
232    #[gpui::test]
233    async fn test_digraph_insert_mode(cx: &mut gpui::TestAppContext) {
234        let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await;
235
236        cx.set_shared_state("Hellˇo").await;
237        cx.simulate_shared_keystrokes("a ctrl-k o : escape").await;
238        cx.shared_state().await.assert_eq("Helloˇö");
239
240        cx.set_shared_state("Hellˇo").await;
241        cx.simulate_shared_keystrokes("a ctrl-k : o escape").await;
242        cx.shared_state().await.assert_eq("Helloˇö");
243
244        cx.set_shared_state("Hellˇo").await;
245        cx.simulate_shared_keystrokes("i ctrl-k o : escape").await;
246        cx.shared_state().await.assert_eq("Hellˇöo");
247    }
248
249    #[gpui::test]
250    async fn test_digraph_insert_multicursor(cx: &mut gpui::TestAppContext) {
251        let mut cx: VimTestContext = VimTestContext::new(cx, true).await;
252
253        cx.set_state("Hellˇo wˇorld", Mode::Normal);
254        cx.simulate_keystrokes("a ctrl-k o : escape");
255        cx.assert_state("Helloˇö woˇörld", Mode::Normal);
256    }
257
258    #[gpui::test]
259    async fn test_digraph_replace(cx: &mut gpui::TestAppContext) {
260        let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await;
261
262        cx.set_shared_state("Hellˇo").await;
263        cx.simulate_shared_keystrokes("r ctrl-k o :").await;
264        cx.shared_state().await.assert_eq("Hellˇö");
265    }
266
267    #[gpui::test]
268    async fn test_digraph_find(cx: &mut gpui::TestAppContext) {
269        let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await;
270
271        cx.set_shared_state("ˇHellö world").await;
272        cx.simulate_shared_keystrokes("f ctrl-k o :").await;
273        cx.shared_state().await.assert_eq("Hellˇö world");
274
275        cx.set_shared_state("ˇHellö world").await;
276        cx.simulate_shared_keystrokes("t ctrl-k o :").await;
277        cx.shared_state().await.assert_eq("Helˇlö world");
278    }
279
280    #[gpui::test]
281    async fn test_digraph_replace_mode(cx: &mut gpui::TestAppContext) {
282        let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await;
283
284        cx.set_shared_state("ˇHello").await;
285        cx.simulate_shared_keystrokes(
286            "shift-r ctrl-k a ' ctrl-k e ` ctrl-k i : ctrl-k o ~ ctrl-k u - escape",
287        )
288        .await;
289        cx.shared_state().await.assert_eq("áèïõˇū");
290    }
291
292    #[gpui::test]
293    async fn test_digraph_custom(cx: &mut gpui::TestAppContext) {
294        let mut cx: VimTestContext = VimTestContext::new(cx, true).await;
295
296        cx.update_global(|store: &mut SettingsStore, cx| {
297            store.update_user_settings::<VimSettings>(cx, |s| {
298                let mut custom_digraphs = HashMap::default();
299                custom_digraphs.insert("|-".into(), "".into());
300                custom_digraphs.insert(":)".into(), "👨‍💻".into());
301                s.custom_digraphs = Some(custom_digraphs);
302            });
303        });
304
305        cx.set_state("ˇ", Mode::Normal);
306        cx.simulate_keystrokes("a ctrl-k | - escape");
307        cx.assert_state("ˇ⊢", Mode::Normal);
308
309        // Test support for multi-codepoint mappings
310        cx.set_state("ˇ", Mode::Normal);
311        cx.simulate_keystrokes("a ctrl-k : ) escape");
312        cx.assert_state("ˇ👨‍💻", Mode::Normal);
313    }
314
315    #[gpui::test]
316    async fn test_digraph_keymap_conflict(cx: &mut gpui::TestAppContext) {
317        let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await;
318
319        cx.set_shared_state("Hellˇo").await;
320        cx.simulate_shared_keystrokes("a ctrl-k s , escape").await;
321        cx.shared_state().await.assert_eq("Helloˇş");
322    }
323
324    #[gpui::test]
325    async fn test_ctrl_v(cx: &mut gpui::TestAppContext) {
326        let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await;
327
328        cx.set_shared_state("ˇ").await;
329        cx.simulate_shared_keystrokes("i ctrl-v 0 0 0").await;
330        cx.shared_state().await.assert_eq("\x00ˇ");
331
332        cx.simulate_shared_keystrokes("ctrl-v j").await;
333        cx.shared_state().await.assert_eq("\x00");
334        cx.simulate_shared_keystrokes("ctrl-v x 6 5").await;
335        cx.shared_state().await.assert_eq("\x00jeˇ");
336        cx.simulate_shared_keystrokes("ctrl-v U 1 F 6 4 0 space")
337            .await;
338        cx.shared_state().await.assert_eq("\x00je🙀 ˇ");
339    }
340
341    #[gpui::test]
342    async fn test_ctrl_v_escape(cx: &mut gpui::TestAppContext) {
343        let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await;
344        cx.set_shared_state("ˇ").await;
345        cx.simulate_shared_keystrokes("i ctrl-v 9 escape").await;
346        cx.shared_state().await.assert_eq("ˇ\t");
347        cx.simulate_shared_keystrokes("i ctrl-v escape").await;
348        cx.shared_state().await.assert_eq("\x1bˇ\t");
349    }
350
351    #[gpui::test]
352    async fn test_ctrl_v_control(cx: &mut gpui::TestAppContext) {
353        let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await;
354        cx.set_shared_state("ˇ").await;
355        cx.simulate_shared_keystrokes("i ctrl-v ctrl-d").await;
356        cx.shared_state().await.assert_eq("\x04ˇ");
357        cx.simulate_shared_keystrokes("ctrl-v ctrl-j").await;
358        cx.shared_state().await.assert_eq("\x04\x00ˇ");
359        cx.simulate_shared_keystrokes("ctrl-v tab").await;
360        cx.shared_state().await.assert_eq("\x04\x00\x09ˇ");
361    }
362}