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