1use editor::{Editor, MultiBufferSnapshot, ToOffset, ToPoint};
2use gpui::{Action, Context, Window};
3use language::{Bias, Point};
4use schemars::JsonSchema;
5use serde::Deserialize;
6use std::ops::Range;
7
8use crate::{Vim, state::Mode};
9
10const BOOLEAN_PAIRS: &[(&str, &str)] = &[("true", "false"), ("yes", "no"), ("on", "off")];
11
12/// Increments the number under the cursor or toggles boolean values.
13#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
14#[action(namespace = vim)]
15#[serde(deny_unknown_fields)]
16struct Increment {
17 #[serde(default)]
18 step: bool,
19}
20
21/// Decrements the number under the cursor or toggles boolean values.
22#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
23#[action(namespace = vim)]
24#[serde(deny_unknown_fields)]
25struct Decrement {
26 #[serde(default)]
27 step: bool,
28}
29
30pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
31 Vim::action(editor, cx, |vim, action: &Increment, window, cx| {
32 vim.record_current_action(cx);
33 let count = Vim::take_count(cx).unwrap_or(1);
34 Vim::take_forced_motion(cx);
35 let step = if action.step { count as i32 } else { 0 };
36 vim.increment(count as i64, step, window, cx)
37 });
38 Vim::action(editor, cx, |vim, action: &Decrement, window, cx| {
39 vim.record_current_action(cx);
40 let count = Vim::take_count(cx).unwrap_or(1);
41 Vim::take_forced_motion(cx);
42 let step = if action.step { -1 * (count as i32) } else { 0 };
43 vim.increment(-(count as i64), step, window, cx)
44 });
45}
46
47impl Vim {
48 fn increment(
49 &mut self,
50 mut delta: i64,
51 step: i32,
52 window: &mut Window,
53 cx: &mut Context<Self>,
54 ) {
55 self.store_visual_marks(window, cx);
56 self.update_editor(cx, |vim, editor, cx| {
57 let mut edits = Vec::new();
58 let mut new_anchors = Vec::new();
59
60 let snapshot = editor.buffer().read(cx).snapshot(cx);
61 for selection in editor.selections.all_adjusted(&editor.display_snapshot(cx)) {
62 if !selection.is_empty()
63 && (vim.mode != Mode::VisualBlock || new_anchors.is_empty())
64 {
65 new_anchors.push((true, snapshot.anchor_before(selection.start)))
66 }
67 for row in selection.start.row..=selection.end.row {
68 let start = if row == selection.start.row {
69 selection.start
70 } else {
71 Point::new(row, 0)
72 };
73 let end = if row == selection.end.row {
74 selection.end
75 } else {
76 Point::new(row, snapshot.line_len(multi_buffer::MultiBufferRow(row)))
77 };
78
79 let find_result = if !selection.is_empty() {
80 find_target(&snapshot, start, end, true)
81 } else {
82 find_target(&snapshot, start, end, false)
83 };
84
85 if let Some((range, target, radix)) = find_result {
86 let replace = match radix {
87 10 => increment_decimal_string(&target, delta),
88 16 => increment_hex_string(&target, delta),
89 2 => increment_binary_string(&target, delta),
90 0 => increment_toggle_string(&target),
91 _ => unreachable!(),
92 };
93 delta += step as i64;
94 edits.push((range.clone(), replace));
95 if selection.is_empty() {
96 new_anchors.push((false, snapshot.anchor_after(range.end)))
97 }
98 } else if selection.is_empty() {
99 new_anchors.push((true, snapshot.anchor_after(start)))
100 }
101 }
102 }
103 editor.transact(window, cx, |editor, window, cx| {
104 editor.edit(edits, cx);
105
106 let snapshot = editor.buffer().read(cx).snapshot(cx);
107 editor.change_selections(Default::default(), window, cx, |s| {
108 let mut new_ranges = Vec::new();
109 for (visual, anchor) in new_anchors.iter() {
110 let mut point = anchor.to_point(&snapshot);
111 if !*visual && point.column > 0 {
112 point.column -= 1;
113 point = snapshot.clip_point(point, Bias::Left)
114 }
115 new_ranges.push(point..point);
116 }
117 s.select_ranges(new_ranges)
118 })
119 });
120 });
121 self.switch_mode(Mode::Normal, true, window, cx)
122 }
123}
124
125fn increment_decimal_string(num: &str, delta: i64) -> String {
126 let (negative, delta, num_str) = match num.strip_prefix('-') {
127 Some(n) => (true, -delta, n),
128 None => (false, delta, num),
129 };
130 let num_length = num_str.len();
131 let leading_zero = num_str.starts_with('0');
132
133 let (result, new_negative) = match u64::from_str_radix(num_str, 10) {
134 Ok(value) => {
135 let wrapped = value.wrapping_add_signed(delta);
136 if delta < 0 && wrapped > value {
137 ((u64::MAX - wrapped).wrapping_add(1), !negative)
138 } else if delta > 0 && wrapped < value {
139 (u64::MAX - wrapped, !negative)
140 } else {
141 (wrapped, negative)
142 }
143 }
144 Err(_) => (u64::MAX, negative),
145 };
146
147 let formatted = format!("{}", result);
148 let new_significant_digits = formatted.len();
149 let padding = if leading_zero {
150 num_length.saturating_sub(new_significant_digits)
151 } else {
152 0
153 };
154
155 if new_negative && result != 0 {
156 format!("-{}{}", "0".repeat(padding), formatted)
157 } else {
158 format!("{}{}", "0".repeat(padding), formatted)
159 }
160}
161
162fn increment_hex_string(num: &str, delta: i64) -> String {
163 let result = if let Ok(val) = u64::from_str_radix(num, 16) {
164 val.wrapping_add_signed(delta)
165 } else {
166 u64::MAX
167 };
168 if should_use_lowercase(num) {
169 format!("{:0width$x}", result, width = num.len())
170 } else {
171 format!("{:0width$X}", result, width = num.len())
172 }
173}
174
175fn should_use_lowercase(num: &str) -> bool {
176 let mut use_uppercase = false;
177 for ch in num.chars() {
178 if ch.is_ascii_lowercase() {
179 return true;
180 }
181 if ch.is_ascii_uppercase() {
182 use_uppercase = true;
183 }
184 }
185 !use_uppercase
186}
187
188fn increment_binary_string(num: &str, delta: i64) -> String {
189 let result = if let Ok(val) = u64::from_str_radix(num, 2) {
190 val.wrapping_add_signed(delta)
191 } else {
192 u64::MAX
193 };
194 format!("{:0width$b}", result, width = num.len())
195}
196
197fn find_target(
198 snapshot: &MultiBufferSnapshot,
199 start: Point,
200 end: Point,
201 need_range: bool,
202) -> Option<(Range<Point>, String, u32)> {
203 let start_offset = start.to_offset(snapshot);
204 let end_offset = end.to_offset(snapshot);
205
206 let mut offset = start_offset;
207 let mut first_char_is_num = snapshot
208 .chars_at(offset)
209 .next()
210 .map_or(false, |ch| ch.is_ascii_hexdigit());
211 let mut pre_char = String::new();
212
213 // Backward scan to find the start of the number, but stop at start_offset
214 for ch in snapshot.reversed_chars_at(offset + 1) {
215 // Search boundaries
216 if offset == 0 || ch.is_whitespace() || (need_range && offset <= start_offset) {
217 break;
218 }
219
220 // Avoid the influence of hexadecimal letters
221 if first_char_is_num
222 && !ch.is_ascii_hexdigit()
223 && (ch != 'b' && ch != 'B')
224 && (ch != 'x' && ch != 'X')
225 && ch != '-'
226 {
227 // Used to determine if the initial character is a number.
228 if is_numeric_string(&pre_char) {
229 break;
230 } else {
231 first_char_is_num = false;
232 }
233 }
234
235 pre_char.insert(0, ch);
236 offset -= ch.len_utf8();
237 }
238
239 let mut begin = None;
240 let mut end = None;
241 let mut target = String::new();
242 let mut radix = 10;
243 let mut is_num = false;
244
245 let mut chars = snapshot.chars_at(offset).peekable();
246
247 while let Some(ch) = chars.next() {
248 if need_range && offset >= end_offset {
249 break; // stop at end of selection
250 }
251
252 if target == "0"
253 && (ch == 'b' || ch == 'B')
254 && chars.peek().is_some()
255 && chars.peek().unwrap().is_digit(2)
256 {
257 radix = 2;
258 begin = None;
259 target = String::new();
260 } else if target == "0"
261 && (ch == 'x' || ch == 'X')
262 && chars.peek().is_some()
263 && chars.peek().unwrap().is_ascii_hexdigit()
264 {
265 radix = 16;
266 begin = None;
267 target = String::new();
268 } else if ch == '.' {
269 is_num = false;
270 begin = None;
271 target = String::new();
272 } else if ch.is_digit(radix)
273 || ((begin.is_none() || !is_num)
274 && ch == '-'
275 && chars.peek().is_some()
276 && chars.peek().unwrap().is_digit(radix))
277 {
278 if !is_num {
279 is_num = true;
280 begin = Some(offset);
281 target = String::new();
282 } else if begin.is_none() {
283 begin = Some(offset);
284 }
285 target.push(ch);
286 } else if ch.is_ascii_alphabetic() && !is_num {
287 if begin.is_none() {
288 begin = Some(offset);
289 }
290 target.push(ch);
291 } else if begin.is_some() && (is_num || !is_num && is_toggle_word(&target)) {
292 // End of matching
293 end = Some(offset);
294 break;
295 } else if ch == '\n' {
296 break;
297 } else {
298 // To match the next word
299 is_num = false;
300 begin = None;
301 target = String::new();
302 }
303
304 offset += ch.len_utf8();
305 }
306
307 if let Some(begin) = begin
308 && (is_num || !is_num && is_toggle_word(&target))
309 {
310 if !is_num {
311 radix = 0;
312 }
313
314 let end = end.unwrap_or(offset);
315 Some((
316 begin.to_point(snapshot)..end.to_point(snapshot),
317 target,
318 radix,
319 ))
320 } else {
321 None
322 }
323}
324
325fn is_numeric_string(s: &str) -> bool {
326 if s.is_empty() {
327 return false;
328 }
329
330 let (_, rest) = if let Some(r) = s.strip_prefix('-') {
331 (true, r)
332 } else {
333 (false, s)
334 };
335
336 if rest.is_empty() {
337 return false;
338 }
339
340 if let Some(digits) = rest.strip_prefix("0b").or_else(|| rest.strip_prefix("0B")) {
341 digits.is_empty() || digits.chars().all(|c| c == '0' || c == '1')
342 } else if let Some(digits) = rest.strip_prefix("0x").or_else(|| rest.strip_prefix("0X")) {
343 digits.is_empty() || digits.chars().all(|c| c.is_ascii_hexdigit())
344 } else {
345 !rest.is_empty() && rest.chars().all(|c| c.is_ascii_digit())
346 }
347}
348
349fn is_toggle_word(word: &str) -> bool {
350 let lower = word.to_lowercase();
351 BOOLEAN_PAIRS
352 .iter()
353 .any(|(a, b)| lower == *a || lower == *b)
354}
355
356fn increment_toggle_string(boolean: &str) -> String {
357 let lower = boolean.to_lowercase();
358
359 let target = BOOLEAN_PAIRS
360 .iter()
361 .find_map(|(a, b)| {
362 if lower == *a {
363 Some(b)
364 } else if lower == *b {
365 Some(a)
366 } else {
367 None
368 }
369 })
370 .unwrap_or(&boolean);
371
372 if boolean.chars().all(|c| c.is_uppercase()) {
373 // Upper case
374 target.to_uppercase()
375 } else if boolean.chars().next().unwrap_or(' ').is_uppercase() {
376 // Title case
377 let mut chars = target.chars();
378 match chars.next() {
379 None => String::new(),
380 Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
381 }
382 } else {
383 target.to_string()
384 }
385}
386
387#[cfg(test)]
388mod test {
389 use indoc::indoc;
390
391 use crate::{
392 state::Mode,
393 test::{NeovimBackedTestContext, VimTestContext},
394 };
395
396 #[gpui::test]
397 async fn test_increment(cx: &mut gpui::TestAppContext) {
398 let mut cx = NeovimBackedTestContext::new(cx).await;
399
400 cx.set_shared_state(indoc! {"
401 1ˇ2
402 "})
403 .await;
404
405 cx.simulate_shared_keystrokes("ctrl-a").await;
406 cx.shared_state().await.assert_eq(indoc! {"
407 1ˇ3
408 "});
409 cx.simulate_shared_keystrokes("ctrl-x").await;
410 cx.shared_state().await.assert_eq(indoc! {"
411 1ˇ2
412 "});
413
414 cx.simulate_shared_keystrokes("9 9 ctrl-a").await;
415 cx.shared_state().await.assert_eq(indoc! {"
416 11ˇ1
417 "});
418 cx.simulate_shared_keystrokes("1 1 1 ctrl-x").await;
419 cx.shared_state().await.assert_eq(indoc! {"
420 ˇ0
421 "});
422 cx.simulate_shared_keystrokes(".").await;
423 cx.shared_state().await.assert_eq(indoc! {"
424 -11ˇ1
425 "});
426 }
427
428 #[gpui::test]
429 async fn test_increment_with_dot(cx: &mut gpui::TestAppContext) {
430 let mut cx = NeovimBackedTestContext::new(cx).await;
431
432 cx.set_shared_state(indoc! {"
433 1ˇ.2
434 "})
435 .await;
436
437 cx.simulate_shared_keystrokes("ctrl-a").await;
438 cx.shared_state().await.assert_eq(indoc! {"
439 1.ˇ3
440 "});
441 cx.simulate_shared_keystrokes("ctrl-x").await;
442 cx.shared_state().await.assert_eq(indoc! {"
443 1.ˇ2
444 "});
445 }
446
447 #[gpui::test]
448 async fn test_increment_with_leading_zeros(cx: &mut gpui::TestAppContext) {
449 let mut cx = NeovimBackedTestContext::new(cx).await;
450
451 cx.set_shared_state(indoc! {"
452 000ˇ9
453 "})
454 .await;
455
456 cx.simulate_shared_keystrokes("ctrl-a").await;
457 cx.shared_state().await.assert_eq(indoc! {"
458 001ˇ0
459 "});
460 cx.simulate_shared_keystrokes("2 ctrl-x").await;
461 cx.shared_state().await.assert_eq(indoc! {"
462 000ˇ8
463 "});
464 }
465
466 #[gpui::test]
467 async fn test_increment_with_leading_zeros_and_zero(cx: &mut gpui::TestAppContext) {
468 let mut cx = NeovimBackedTestContext::new(cx).await;
469
470 cx.set_shared_state(indoc! {"
471 01ˇ1
472 "})
473 .await;
474
475 cx.simulate_shared_keystrokes("ctrl-a").await;
476 cx.shared_state().await.assert_eq(indoc! {"
477 01ˇ2
478 "});
479 cx.simulate_shared_keystrokes("1 2 ctrl-x").await;
480 cx.shared_state().await.assert_eq(indoc! {"
481 00ˇ0
482 "});
483 }
484
485 #[gpui::test]
486 async fn test_increment_with_changing_leading_zeros(cx: &mut gpui::TestAppContext) {
487 let mut cx = NeovimBackedTestContext::new(cx).await;
488
489 cx.set_shared_state(indoc! {"
490 099ˇ9
491 "})
492 .await;
493
494 cx.simulate_shared_keystrokes("ctrl-a").await;
495 cx.shared_state().await.assert_eq(indoc! {"
496 100ˇ0
497 "});
498 cx.simulate_shared_keystrokes("2 ctrl-x").await;
499 cx.shared_state().await.assert_eq(indoc! {"
500 99ˇ8
501 "});
502 }
503
504 #[gpui::test]
505 async fn test_increment_with_two_dots(cx: &mut gpui::TestAppContext) {
506 let mut cx = NeovimBackedTestContext::new(cx).await;
507
508 cx.set_shared_state(indoc! {"
509 111.ˇ.2
510 "})
511 .await;
512
513 cx.simulate_shared_keystrokes("ctrl-a").await;
514 cx.shared_state().await.assert_eq(indoc! {"
515 111..ˇ3
516 "});
517 cx.simulate_shared_keystrokes("ctrl-x").await;
518 cx.shared_state().await.assert_eq(indoc! {"
519 111..ˇ2
520 "});
521 }
522
523 #[gpui::test]
524 async fn test_increment_sign_change(cx: &mut gpui::TestAppContext) {
525 let mut cx = NeovimBackedTestContext::new(cx).await;
526 cx.set_shared_state(indoc! {"
527 ˇ0
528 "})
529 .await;
530 cx.simulate_shared_keystrokes("ctrl-x").await;
531 cx.shared_state().await.assert_eq(indoc! {"
532 -ˇ1
533 "});
534 cx.simulate_shared_keystrokes("2 ctrl-a").await;
535 cx.shared_state().await.assert_eq(indoc! {"
536 ˇ1
537 "});
538 }
539
540 #[gpui::test]
541 async fn test_increment_sign_change_with_leading_zeros(cx: &mut gpui::TestAppContext) {
542 let mut cx = NeovimBackedTestContext::new(cx).await;
543 cx.set_shared_state(indoc! {"
544 00ˇ1
545 "})
546 .await;
547 cx.simulate_shared_keystrokes("ctrl-x").await;
548 cx.shared_state().await.assert_eq(indoc! {"
549 00ˇ0
550 "});
551 cx.simulate_shared_keystrokes("ctrl-x").await;
552 cx.shared_state().await.assert_eq(indoc! {"
553 -00ˇ1
554 "});
555 cx.simulate_shared_keystrokes("2 ctrl-a").await;
556 cx.shared_state().await.assert_eq(indoc! {"
557 00ˇ1
558 "});
559 }
560
561 #[gpui::test]
562 async fn test_increment_bin_wrapping_and_padding(cx: &mut gpui::TestAppContext) {
563 let mut cx = NeovimBackedTestContext::new(cx).await;
564 cx.set_shared_state(indoc! {"
565 0b111111111111111111111111111111111111111111111111111111111111111111111ˇ1
566 "})
567 .await;
568
569 cx.simulate_shared_keystrokes("ctrl-a").await;
570 cx.shared_state().await.assert_eq(indoc! {"
571 0b000000111111111111111111111111111111111111111111111111111111111111111ˇ1
572 "});
573 cx.simulate_shared_keystrokes("ctrl-a").await;
574 cx.shared_state().await.assert_eq(indoc! {"
575 0b000000000000000000000000000000000000000000000000000000000000000000000ˇ0
576 "});
577
578 cx.simulate_shared_keystrokes("ctrl-a").await;
579 cx.shared_state().await.assert_eq(indoc! {"
580 0b000000000000000000000000000000000000000000000000000000000000000000000ˇ1
581 "});
582 cx.simulate_shared_keystrokes("2 ctrl-x").await;
583 cx.shared_state().await.assert_eq(indoc! {"
584 0b000000111111111111111111111111111111111111111111111111111111111111111ˇ1
585 "});
586 }
587
588 #[gpui::test]
589 async fn test_increment_hex_wrapping_and_padding(cx: &mut gpui::TestAppContext) {
590 let mut cx = NeovimBackedTestContext::new(cx).await;
591 cx.set_shared_state(indoc! {"
592 0xfffffffffffffffffffˇf
593 "})
594 .await;
595
596 cx.simulate_shared_keystrokes("ctrl-a").await;
597 cx.shared_state().await.assert_eq(indoc! {"
598 0x0000fffffffffffffffˇf
599 "});
600 cx.simulate_shared_keystrokes("ctrl-a").await;
601 cx.shared_state().await.assert_eq(indoc! {"
602 0x0000000000000000000ˇ0
603 "});
604 cx.simulate_shared_keystrokes("ctrl-a").await;
605 cx.shared_state().await.assert_eq(indoc! {"
606 0x0000000000000000000ˇ1
607 "});
608 cx.simulate_shared_keystrokes("2 ctrl-x").await;
609 cx.shared_state().await.assert_eq(indoc! {"
610 0x0000fffffffffffffffˇf
611 "});
612 }
613
614 #[gpui::test]
615 async fn test_increment_wrapping(cx: &mut gpui::TestAppContext) {
616 let mut cx = NeovimBackedTestContext::new(cx).await;
617 cx.set_shared_state(indoc! {"
618 1844674407370955161ˇ9
619 "})
620 .await;
621
622 cx.simulate_shared_keystrokes("ctrl-a").await;
623 cx.shared_state().await.assert_eq(indoc! {"
624 1844674407370955161ˇ5
625 "});
626 cx.simulate_shared_keystrokes("ctrl-a").await;
627 cx.shared_state().await.assert_eq(indoc! {"
628 -1844674407370955161ˇ5
629 "});
630 cx.simulate_shared_keystrokes("ctrl-a").await;
631 cx.shared_state().await.assert_eq(indoc! {"
632 -1844674407370955161ˇ4
633 "});
634 cx.simulate_shared_keystrokes("3 ctrl-x").await;
635 cx.shared_state().await.assert_eq(indoc! {"
636 1844674407370955161ˇ4
637 "});
638 cx.simulate_shared_keystrokes("2 ctrl-a").await;
639 cx.shared_state().await.assert_eq(indoc! {"
640 -1844674407370955161ˇ5
641 "});
642 }
643
644 #[gpui::test]
645 async fn test_increment_inline(cx: &mut gpui::TestAppContext) {
646 let mut cx = NeovimBackedTestContext::new(cx).await;
647 cx.set_shared_state(indoc! {"
648 inline0x3ˇ9u32
649 "})
650 .await;
651
652 cx.simulate_shared_keystrokes("ctrl-a").await;
653 cx.shared_state().await.assert_eq(indoc! {"
654 inline0x3ˇau32
655 "});
656 cx.simulate_shared_keystrokes("ctrl-a").await;
657 cx.shared_state().await.assert_eq(indoc! {"
658 inline0x3ˇbu32
659 "});
660 cx.simulate_shared_keystrokes("l l l ctrl-a").await;
661 cx.shared_state().await.assert_eq(indoc! {"
662 inline0x3bu3ˇ3
663 "});
664 }
665
666 #[gpui::test]
667 async fn test_increment_hex_casing(cx: &mut gpui::TestAppContext) {
668 let mut cx = NeovimBackedTestContext::new(cx).await;
669 cx.set_shared_state(indoc! {"
670 0xFˇa
671 "})
672 .await;
673
674 cx.simulate_shared_keystrokes("ctrl-a").await;
675 cx.shared_state().await.assert_eq(indoc! {"
676 0xfˇb
677 "});
678 cx.simulate_shared_keystrokes("ctrl-a").await;
679 cx.shared_state().await.assert_eq(indoc! {"
680 0xfˇc
681 "});
682 }
683
684 #[gpui::test]
685 async fn test_increment_radix(cx: &mut gpui::TestAppContext) {
686 let mut cx = NeovimBackedTestContext::new(cx).await;
687
688 cx.simulate("ctrl-a", "ˇ total: 0xff")
689 .await
690 .assert_matches();
691 cx.simulate("ctrl-x", "ˇ total: 0xff")
692 .await
693 .assert_matches();
694 cx.simulate("ctrl-x", "ˇ total: 0xFF")
695 .await
696 .assert_matches();
697 cx.simulate("ctrl-a", "(ˇ0b10f)").await.assert_matches();
698 cx.simulate("ctrl-a", "ˇ-1").await.assert_matches();
699 cx.simulate("ctrl-a", "banˇana").await.assert_matches();
700 }
701
702 #[gpui::test]
703 async fn test_increment_steps(cx: &mut gpui::TestAppContext) {
704 let mut cx = NeovimBackedTestContext::new(cx).await;
705
706 cx.set_shared_state(indoc! {"
707 ˇ1
708 1
709 1 2
710 1
711 1"})
712 .await;
713
714 cx.simulate_shared_keystrokes("j v shift-g g ctrl-a").await;
715 cx.shared_state().await.assert_eq(indoc! {"
716 1
717 ˇ2
718 3 2
719 4
720 5"});
721
722 cx.simulate_shared_keystrokes("shift-g ctrl-v g g").await;
723 cx.shared_state().await.assert_eq(indoc! {"
724 «1ˇ»
725 «2ˇ»
726 «3ˇ» 2
727 «4ˇ»
728 «5ˇ»"});
729
730 cx.simulate_shared_keystrokes("g ctrl-x").await;
731 cx.shared_state().await.assert_eq(indoc! {"
732 ˇ0
733 0
734 0 2
735 0
736 0"});
737 cx.simulate_shared_keystrokes("v shift-g g ctrl-a").await;
738 cx.simulate_shared_keystrokes("v shift-g 5 g ctrl-a").await;
739 cx.shared_state().await.assert_eq(indoc! {"
740 ˇ6
741 12
742 18 2
743 24
744 30"});
745 }
746
747 #[gpui::test]
748 async fn test_increment_toggle(cx: &mut gpui::TestAppContext) {
749 let mut cx = VimTestContext::new(cx, true).await;
750
751 cx.set_state("let enabled = trˇue;", Mode::Normal);
752 cx.simulate_keystrokes("ctrl-a");
753 cx.assert_state("let enabled = falsˇe;", Mode::Normal);
754
755 cx.simulate_keystrokes("0 ctrl-a");
756 cx.assert_state("let enabled = truˇe;", Mode::Normal);
757
758 cx.set_state(
759 indoc! {"
760 ˇlet enabled = TRUE;
761 let enabled = TRUE;
762 let enabled = TRUE;
763 "},
764 Mode::Normal,
765 );
766 cx.simulate_keystrokes("shift-v j j ctrl-x");
767 cx.assert_state(
768 indoc! {"
769 ˇlet enabled = FALSE;
770 let enabled = FALSE;
771 let enabled = FALSE;
772 "},
773 Mode::Normal,
774 );
775
776 cx.set_state(
777 indoc! {"
778 let enabled = ˇYes;
779 let enabled = Yes;
780 let enabled = Yes;
781 "},
782 Mode::Normal,
783 );
784 cx.simulate_keystrokes("ctrl-v j j e ctrl-x");
785 cx.assert_state(
786 indoc! {"
787 let enabled = ˇNo;
788 let enabled = No;
789 let enabled = No;
790 "},
791 Mode::Normal,
792 );
793
794 cx.set_state("ˇlet enabled = True;", Mode::Normal);
795 cx.simulate_keystrokes("ctrl-a");
796 cx.assert_state("let enabled = Falsˇe;", Mode::Normal);
797
798 cx.simulate_keystrokes("ctrl-a");
799 cx.assert_state("let enabled = Truˇe;", Mode::Normal);
800
801 cx.set_state("let enabled = Onˇ;", Mode::Normal);
802 cx.simulate_keystrokes("v b ctrl-a");
803 cx.assert_state("let enabled = ˇOff;", Mode::Normal);
804 }
805
806 #[gpui::test]
807 async fn test_increment_order(cx: &mut gpui::TestAppContext) {
808 let mut cx = VimTestContext::new(cx, true).await;
809
810 cx.set_state("aaˇa false 1 2 3", Mode::Normal);
811 cx.simulate_keystrokes("ctrl-a");
812 cx.assert_state("aaa truˇe 1 2 3", Mode::Normal);
813
814 cx.set_state("aaˇa 1 false 2 3", Mode::Normal);
815 cx.simulate_keystrokes("ctrl-a");
816 cx.assert_state("aaa ˇ2 false 2 3", Mode::Normal);
817
818 cx.set_state("trueˇ 1 2 3", Mode::Normal);
819 cx.simulate_keystrokes("ctrl-a");
820 cx.assert_state("true ˇ2 2 3", Mode::Normal);
821 }
822
823 #[gpui::test]
824 async fn test_increment_visual_partial_number(cx: &mut gpui::TestAppContext) {
825 let mut cx = NeovimBackedTestContext::new(cx).await;
826
827 cx.set_shared_state("ˇ123").await;
828 cx.simulate_shared_keystrokes("v l ctrl-a").await;
829 cx.shared_state().await.assert_eq(indoc! {"ˇ133"});
830 cx.simulate_shared_keystrokes("l v l ctrl-a").await;
831 cx.shared_state().await.assert_eq(indoc! {"1ˇ34"});
832 cx.simulate_shared_keystrokes("shift-v y p p ctrl-v k k l ctrl-a")
833 .await;
834 cx.shared_state().await.assert_eq(indoc! {"ˇ144\n144\n144"});
835 }
836}