digraph.rs

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