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