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