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