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