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        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("\x00");
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}