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