digraph.rs

  1use std::sync::Arc;
  2
  3use collections::HashMap;
  4use editor::Editor;
  5use gpui::{impl_actions, AppContext, Keystroke, KeystrokeEvent};
  6use schemars::JsonSchema;
  7use serde::Deserialize;
  8use settings::Settings;
  9use std::sync::LazyLock;
 10use ui::ViewContext;
 11
 12use crate::{state::Operator, Vim, VimSettings};
 13
 14mod default;
 15
 16#[derive(Debug, Clone, Deserialize, JsonSchema, PartialEq)]
 17struct Literal(String, char);
 18impl_actions!(vim, [Literal]);
 19
 20pub(crate) fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
 21    Vim::action(editor, cx, Vim::literal)
 22}
 23
 24static DEFAULT_DIGRAPHS_MAP: LazyLock<HashMap<String, Arc<str>>> = LazyLock::new(|| {
 25    let mut map = HashMap::default();
 26    for &(a, b, c) in default::DEFAULT_DIGRAPHS {
 27        let key = format!("{a}{b}");
 28        let value = char::from_u32(c).unwrap().to_string().into();
 29        map.insert(key, value);
 30    }
 31    map
 32});
 33
 34fn lookup_digraph(a: char, b: char, cx: &AppContext) -> Arc<str> {
 35    let custom_digraphs = &VimSettings::get_global(cx).custom_digraphs;
 36    let input = format!("{a}{b}");
 37    let reversed = format!("{b}{a}");
 38
 39    custom_digraphs
 40        .get(&input)
 41        .or_else(|| DEFAULT_DIGRAPHS_MAP.get(&input))
 42        .or_else(|| custom_digraphs.get(&reversed))
 43        .or_else(|| DEFAULT_DIGRAPHS_MAP.get(&reversed))
 44        .cloned()
 45        .unwrap_or_else(|| b.to_string().into())
 46}
 47
 48impl Vim {
 49    pub fn insert_digraph(
 50        &mut self,
 51        first_char: char,
 52        second_char: char,
 53        cx: &mut ViewContext<Self>,
 54    ) {
 55        let text = lookup_digraph(first_char, second_char, cx);
 56
 57        self.pop_operator(cx);
 58        if self.editor_input_enabled() {
 59            self.update_editor(cx, |_, editor, cx| editor.insert(&text, cx));
 60        } else {
 61            self.input_ignored(text, cx);
 62        }
 63    }
 64
 65    fn literal(&mut self, action: &Literal, cx: &mut ViewContext<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                    cx.window_context().defer(|cx| {
 70                        cx.dispatch_keystroke(keystroke);
 71                    });
 72                }
 73                return self.handle_literal_input(prefix, "", cx);
 74            }
 75        }
 76
 77        self.insert_literal(Some(action.1), "", cx);
 78    }
 79
 80    pub fn handle_literal_keystroke(
 81        &mut self,
 82        keystroke_event: &KeystrokeEvent,
 83        prefix: String,
 84        cx: &mut ViewContext<Self>,
 85    ) {
 86        // handled by handle_literal_input
 87        if keystroke_event.keystroke.key_char.is_some() {
 88            return;
 89        };
 90
 91        if prefix.len() > 0 {
 92            self.handle_literal_input(prefix, "", cx);
 93        } else {
 94            self.pop_operator(cx);
 95        }
 96
 97        // give another chance to handle the binding outside
 98        // of waiting mode.
 99        if keystroke_event.action.is_none() {
100            let keystroke = keystroke_event.keystroke.clone();
101            cx.window_context().defer(|cx| {
102                cx.dispatch_keystroke(keystroke);
103            });
104        }
105        return;
106    }
107
108    pub fn handle_literal_input(
109        &mut self,
110        mut prefix: String,
111        text: &str,
112        cx: &mut ViewContext<Self>,
113    ) {
114        let first = prefix.chars().next();
115        let next = text.chars().next().unwrap_or(' ');
116        match first {
117            Some('o' | 'O') => {
118                if next.is_digit(8) {
119                    prefix.push(next);
120                    if prefix.len() == 4 {
121                        let ch: char = u8::from_str_radix(&prefix[1..], 8).unwrap_or(255).into();
122                        return self.insert_literal(Some(ch), "", cx);
123                    }
124                } else {
125                    let ch = if prefix.len() > 1 {
126                        Some(u8::from_str_radix(&prefix[1..], 8).unwrap_or(255).into())
127                    } else {
128                        None
129                    };
130                    return self.insert_literal(ch, text, cx);
131                }
132            }
133            Some('x' | 'X' | 'u' | 'U') => {
134                let max_len = match first.unwrap() {
135                    'x' => 3,
136                    'X' => 3,
137                    'u' => 5,
138                    'U' => 9,
139                    _ => unreachable!(),
140                };
141                if next.is_ascii_hexdigit() {
142                    prefix.push(next);
143                    if prefix.len() == max_len {
144                        let ch: char = u32::from_str_radix(&prefix[1..], 16)
145                            .ok()
146                            .and_then(|n| n.try_into().ok())
147                            .unwrap_or('\u{FFFD}');
148                        return self.insert_literal(Some(ch), "", cx);
149                    }
150                } else {
151                    let ch = if prefix.len() > 1 {
152                        Some(
153                            u32::from_str_radix(&prefix[1..], 16)
154                                .ok()
155                                .and_then(|n| n.try_into().ok())
156                                .unwrap_or('\u{FFFD}'),
157                        )
158                    } else {
159                        None
160                    };
161                    return self.insert_literal(ch, text, cx);
162                }
163            }
164            Some('0'..='9') => {
165                if next.is_ascii_hexdigit() {
166                    prefix.push(next);
167                    if prefix.len() == 3 {
168                        let ch: char = u8::from_str_radix(&prefix, 10).unwrap_or(255).into();
169                        return self.insert_literal(Some(ch), "", cx);
170                    }
171                } else {
172                    let ch: char = u8::from_str_radix(&prefix, 10).unwrap_or(255).into();
173                    return self.insert_literal(Some(ch), "", cx);
174                }
175            }
176            None if matches!(next, 'o' | 'O' | 'x' | 'X' | 'u' | 'U' | '0'..='9') => {
177                prefix.push(next)
178            }
179            _ => {
180                return self.insert_literal(None, text, cx);
181            }
182        };
183
184        self.pop_operator(cx);
185        self.push_operator(
186            Operator::Literal {
187                prefix: Some(prefix),
188            },
189            cx,
190        );
191    }
192
193    fn insert_literal(&mut self, ch: Option<char>, suffix: &str, cx: &mut ViewContext<Self>) {
194        self.pop_operator(cx);
195        let mut text = String::new();
196        if let Some(c) = ch {
197            if c == '\n' {
198                text.push('\x00')
199            } else {
200                text.push(c)
201            }
202        }
203        text.push_str(suffix);
204
205        if self.editor_input_enabled() {
206            self.update_editor(cx, |_, editor, cx| editor.insert(&text, cx));
207        } else {
208            self.input_ignored(text.into(), cx);
209        }
210    }
211}
212
213#[cfg(test)]
214mod test {
215    use collections::HashMap;
216    use settings::SettingsStore;
217
218    use crate::{
219        state::Mode,
220        test::{NeovimBackedTestContext, VimTestContext},
221        VimSettings,
222    };
223
224    #[gpui::test]
225    async fn test_digraph_insert_mode(cx: &mut gpui::TestAppContext) {
226        let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await;
227
228        cx.set_shared_state("Hellˇo").await;
229        cx.simulate_shared_keystrokes("a ctrl-k o : escape").await;
230        cx.shared_state().await.assert_eq("Helloˇö");
231
232        cx.set_shared_state("Hellˇo").await;
233        cx.simulate_shared_keystrokes("a ctrl-k : o escape").await;
234        cx.shared_state().await.assert_eq("Helloˇö");
235
236        cx.set_shared_state("Hellˇo").await;
237        cx.simulate_shared_keystrokes("i ctrl-k o : escape").await;
238        cx.shared_state().await.assert_eq("Hellˇöo");
239    }
240
241    #[gpui::test]
242    async fn test_digraph_insert_multicursor(cx: &mut gpui::TestAppContext) {
243        let mut cx: VimTestContext = VimTestContext::new(cx, true).await;
244
245        cx.set_state("Hellˇo wˇorld", Mode::Normal);
246        cx.simulate_keystrokes("a ctrl-k o : escape");
247        cx.assert_state("Helloˇö woˇörld", Mode::Normal);
248    }
249
250    #[gpui::test]
251    async fn test_digraph_replace(cx: &mut gpui::TestAppContext) {
252        let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await;
253
254        cx.set_shared_state("Hellˇo").await;
255        cx.simulate_shared_keystrokes("r ctrl-k o :").await;
256        cx.shared_state().await.assert_eq("Hellˇö");
257    }
258
259    #[gpui::test]
260    async fn test_digraph_find(cx: &mut gpui::TestAppContext) {
261        let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await;
262
263        cx.set_shared_state("ˇHellö world").await;
264        cx.simulate_shared_keystrokes("f ctrl-k o :").await;
265        cx.shared_state().await.assert_eq("Hellˇö world");
266
267        cx.set_shared_state("ˇHellö world").await;
268        cx.simulate_shared_keystrokes("t ctrl-k o :").await;
269        cx.shared_state().await.assert_eq("Helˇlö world");
270    }
271
272    #[gpui::test]
273    async fn test_digraph_replace_mode(cx: &mut gpui::TestAppContext) {
274        let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await;
275
276        cx.set_shared_state("ˇHello").await;
277        cx.simulate_shared_keystrokes(
278            "shift-r ctrl-k a ' ctrl-k e ` ctrl-k i : ctrl-k o ~ ctrl-k u - escape",
279        )
280        .await;
281        cx.shared_state().await.assert_eq("áèïõˇū");
282    }
283
284    #[gpui::test]
285    async fn test_digraph_custom(cx: &mut gpui::TestAppContext) {
286        let mut cx: VimTestContext = VimTestContext::new(cx, true).await;
287
288        cx.update_global(|store: &mut SettingsStore, cx| {
289            store.update_user_settings::<VimSettings>(cx, |s| {
290                let mut custom_digraphs = HashMap::default();
291                custom_digraphs.insert("|-".into(), "".into());
292                custom_digraphs.insert(":)".into(), "👨‍💻".into());
293                s.custom_digraphs = Some(custom_digraphs);
294            });
295        });
296
297        cx.set_state("ˇ", Mode::Normal);
298        cx.simulate_keystrokes("a ctrl-k | - escape");
299        cx.assert_state("ˇ⊢", Mode::Normal);
300
301        // Test support for multi-codepoint mappings
302        cx.set_state("ˇ", Mode::Normal);
303        cx.simulate_keystrokes("a ctrl-k : ) escape");
304        cx.assert_state("ˇ👨‍💻", Mode::Normal);
305    }
306
307    #[gpui::test]
308    async fn test_digraph_keymap_conflict(cx: &mut gpui::TestAppContext) {
309        let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await;
310
311        cx.set_shared_state("Hellˇo").await;
312        cx.simulate_shared_keystrokes("a ctrl-k s , escape").await;
313        cx.shared_state().await.assert_eq("Helloˇş");
314    }
315
316    #[gpui::test]
317    async fn test_ctrl_v(cx: &mut gpui::TestAppContext) {
318        let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await;
319
320        cx.set_shared_state("ˇ").await;
321        cx.simulate_shared_keystrokes("i ctrl-v 0 0 0").await;
322        cx.shared_state().await.assert_eq("\x00ˇ");
323
324        cx.simulate_shared_keystrokes("ctrl-v j").await;
325        cx.shared_state().await.assert_eq("\x00");
326        cx.simulate_shared_keystrokes("ctrl-v x 6 5").await;
327        cx.shared_state().await.assert_eq("\x00jeˇ");
328        cx.simulate_shared_keystrokes("ctrl-v U 1 F 6 4 0 space")
329            .await;
330        cx.shared_state().await.assert_eq("\x00je🙀 ˇ");
331    }
332
333    #[gpui::test]
334    async fn test_ctrl_v_escape(cx: &mut gpui::TestAppContext) {
335        let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await;
336        cx.set_shared_state("ˇ").await;
337        cx.simulate_shared_keystrokes("i ctrl-v 9 escape").await;
338        cx.shared_state().await.assert_eq("ˇ\t");
339        cx.simulate_shared_keystrokes("i ctrl-v escape").await;
340        cx.shared_state().await.assert_eq("\x1bˇ\t");
341    }
342
343    #[gpui::test]
344    async fn test_ctrl_v_control(cx: &mut gpui::TestAppContext) {
345        let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await;
346        cx.set_shared_state("ˇ").await;
347        cx.simulate_shared_keystrokes("i ctrl-v ctrl-d").await;
348        cx.shared_state().await.assert_eq("\x04ˇ");
349        cx.simulate_shared_keystrokes("ctrl-v ctrl-j").await;
350        cx.shared_state().await.assert_eq("\x04\x00ˇ");
351        cx.simulate_shared_keystrokes("ctrl-v tab").await;
352        cx.shared_state().await.assert_eq("\x04\x00\x09ˇ");
353    }
354}