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 let next_offset = offset
214 + snapshot
215 .chars_at(start_offset)
216 .next()
217 .map_or(0, |ch| ch.len_utf8());
218 // Backward scan to find the start of the number, but stop at start_offset
219 for ch in snapshot.reversed_chars_at(next_offset) {
220 // Search boundaries
221 if offset.0 == 0 || ch.is_whitespace() || (need_range && offset <= start_offset) {
222 break;
223 }
224
225 // Avoid the influence of hexadecimal letters
226 if first_char_is_num
227 && !ch.is_ascii_hexdigit()
228 && (ch != 'b' && ch != 'B')
229 && (ch != 'x' && ch != 'X')
230 && ch != '-'
231 {
232 // Used to determine if the initial character is a number.
233 if is_numeric_string(&pre_char) {
234 break;
235 } else {
236 first_char_is_num = false;
237 }
238 }
239
240 pre_char.insert(0, ch);
241 offset -= ch.len_utf8();
242 }
243
244 let mut begin = None;
245 let mut end = None;
246 let mut target = String::new();
247 let mut radix = 10;
248 let mut is_num = false;
249
250 let mut chars = snapshot.chars_at(offset).peekable();
251
252 while let Some(ch) = chars.next() {
253 if need_range && offset >= end_offset {
254 break; // stop at end of selection
255 }
256
257 if target == "0"
258 && (ch == 'b' || ch == 'B')
259 && chars.peek().is_some()
260 && chars.peek().unwrap().is_digit(2)
261 {
262 radix = 2;
263 begin = None;
264 target = String::new();
265 } else if target == "0"
266 && (ch == 'x' || ch == 'X')
267 && chars.peek().is_some()
268 && chars.peek().unwrap().is_ascii_hexdigit()
269 {
270 radix = 16;
271 begin = None;
272 target = String::new();
273 } else if ch == '.' {
274 is_num = false;
275 begin = None;
276 target = String::new();
277 } else if ch.is_digit(radix)
278 || ((begin.is_none() || !is_num)
279 && ch == '-'
280 && chars.peek().is_some()
281 && chars.peek().unwrap().is_digit(radix))
282 {
283 if !is_num {
284 is_num = true;
285 begin = Some(offset);
286 target = String::new();
287 } else if begin.is_none() {
288 begin = Some(offset);
289 }
290 target.push(ch);
291 } else if ch.is_ascii_alphabetic() && !is_num {
292 if begin.is_none() {
293 begin = Some(offset);
294 }
295 target.push(ch);
296 } else if begin.is_some() && (is_num || !is_num && is_toggle_word(&target)) {
297 // End of matching
298 end = Some(offset);
299 break;
300 } else if ch == '\n' {
301 break;
302 } else {
303 // To match the next word
304 is_num = false;
305 begin = None;
306 target = String::new();
307 }
308
309 offset += ch.len_utf8();
310 }
311
312 if let Some(begin) = begin
313 && (is_num || !is_num && is_toggle_word(&target))
314 {
315 if !is_num {
316 radix = 0;
317 }
318
319 let end = end.unwrap_or(offset);
320 Some((
321 begin.to_point(snapshot)..end.to_point(snapshot),
322 target,
323 radix,
324 ))
325 } else {
326 None
327 }
328}
329
330fn is_numeric_string(s: &str) -> bool {
331 if s.is_empty() {
332 return false;
333 }
334
335 let (_, rest) = if let Some(r) = s.strip_prefix('-') {
336 (true, r)
337 } else {
338 (false, s)
339 };
340
341 if rest.is_empty() {
342 return false;
343 }
344
345 if let Some(digits) = rest.strip_prefix("0b").or_else(|| rest.strip_prefix("0B")) {
346 digits.is_empty() || digits.chars().all(|c| c == '0' || c == '1')
347 } else if let Some(digits) = rest.strip_prefix("0x").or_else(|| rest.strip_prefix("0X")) {
348 digits.is_empty() || digits.chars().all(|c| c.is_ascii_hexdigit())
349 } else {
350 !rest.is_empty() && rest.chars().all(|c| c.is_ascii_digit())
351 }
352}
353
354fn is_toggle_word(word: &str) -> bool {
355 let lower = word.to_lowercase();
356 BOOLEAN_PAIRS
357 .iter()
358 .any(|(a, b)| lower == *a || lower == *b)
359}
360
361fn increment_toggle_string(boolean: &str) -> String {
362 let lower = boolean.to_lowercase();
363
364 let target = BOOLEAN_PAIRS
365 .iter()
366 .find_map(|(a, b)| {
367 if lower == *a {
368 Some(b)
369 } else if lower == *b {
370 Some(a)
371 } else {
372 None
373 }
374 })
375 .unwrap_or(&boolean);
376
377 if boolean.chars().all(|c| c.is_uppercase()) {
378 // Upper case
379 target.to_uppercase()
380 } else if boolean.chars().next().unwrap_or(' ').is_uppercase() {
381 // Title case
382 let mut chars = target.chars();
383 match chars.next() {
384 None => String::new(),
385 Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
386 }
387 } else {
388 target.to_string()
389 }
390}
391
392#[cfg(test)]
393mod test {
394 use indoc::indoc;
395
396 use crate::{
397 state::Mode,
398 test::{NeovimBackedTestContext, VimTestContext},
399 };
400
401 #[gpui::test]
402 async fn test_increment(cx: &mut gpui::TestAppContext) {
403 let mut cx = NeovimBackedTestContext::new(cx).await;
404
405 cx.set_shared_state(indoc! {"
406 1ˇ2
407 "})
408 .await;
409
410 cx.simulate_shared_keystrokes("ctrl-a").await;
411 cx.shared_state().await.assert_eq(indoc! {"
412 1ˇ3
413 "});
414 cx.simulate_shared_keystrokes("ctrl-x").await;
415 cx.shared_state().await.assert_eq(indoc! {"
416 1ˇ2
417 "});
418
419 cx.simulate_shared_keystrokes("9 9 ctrl-a").await;
420 cx.shared_state().await.assert_eq(indoc! {"
421 11ˇ1
422 "});
423 cx.simulate_shared_keystrokes("1 1 1 ctrl-x").await;
424 cx.shared_state().await.assert_eq(indoc! {"
425 ˇ0
426 "});
427 cx.simulate_shared_keystrokes(".").await;
428 cx.shared_state().await.assert_eq(indoc! {"
429 -11ˇ1
430 "});
431 }
432
433 #[gpui::test]
434 async fn test_increment_with_dot(cx: &mut gpui::TestAppContext) {
435 let mut cx = NeovimBackedTestContext::new(cx).await;
436
437 cx.set_shared_state(indoc! {"
438 1ˇ.2
439 "})
440 .await;
441
442 cx.simulate_shared_keystrokes("ctrl-a").await;
443 cx.shared_state().await.assert_eq(indoc! {"
444 1.ˇ3
445 "});
446 cx.simulate_shared_keystrokes("ctrl-x").await;
447 cx.shared_state().await.assert_eq(indoc! {"
448 1.ˇ2
449 "});
450 }
451
452 #[gpui::test]
453 async fn test_increment_with_leading_zeros(cx: &mut gpui::TestAppContext) {
454 let mut cx = NeovimBackedTestContext::new(cx).await;
455
456 cx.set_shared_state(indoc! {"
457 000ˇ9
458 "})
459 .await;
460
461 cx.simulate_shared_keystrokes("ctrl-a").await;
462 cx.shared_state().await.assert_eq(indoc! {"
463 001ˇ0
464 "});
465 cx.simulate_shared_keystrokes("2 ctrl-x").await;
466 cx.shared_state().await.assert_eq(indoc! {"
467 000ˇ8
468 "});
469 }
470
471 #[gpui::test]
472 async fn test_increment_with_leading_zeros_and_zero(cx: &mut gpui::TestAppContext) {
473 let mut cx = NeovimBackedTestContext::new(cx).await;
474
475 cx.set_shared_state(indoc! {"
476 01ˇ1
477 "})
478 .await;
479
480 cx.simulate_shared_keystrokes("ctrl-a").await;
481 cx.shared_state().await.assert_eq(indoc! {"
482 01ˇ2
483 "});
484 cx.simulate_shared_keystrokes("1 2 ctrl-x").await;
485 cx.shared_state().await.assert_eq(indoc! {"
486 00ˇ0
487 "});
488 }
489
490 #[gpui::test]
491 async fn test_increment_with_changing_leading_zeros(cx: &mut gpui::TestAppContext) {
492 let mut cx = NeovimBackedTestContext::new(cx).await;
493
494 cx.set_shared_state(indoc! {"
495 099ˇ9
496 "})
497 .await;
498
499 cx.simulate_shared_keystrokes("ctrl-a").await;
500 cx.shared_state().await.assert_eq(indoc! {"
501 100ˇ0
502 "});
503 cx.simulate_shared_keystrokes("2 ctrl-x").await;
504 cx.shared_state().await.assert_eq(indoc! {"
505 99ˇ8
506 "});
507 }
508
509 #[gpui::test]
510 async fn test_increment_with_two_dots(cx: &mut gpui::TestAppContext) {
511 let mut cx = NeovimBackedTestContext::new(cx).await;
512
513 cx.set_shared_state(indoc! {"
514 111.ˇ.2
515 "})
516 .await;
517
518 cx.simulate_shared_keystrokes("ctrl-a").await;
519 cx.shared_state().await.assert_eq(indoc! {"
520 111..ˇ3
521 "});
522 cx.simulate_shared_keystrokes("ctrl-x").await;
523 cx.shared_state().await.assert_eq(indoc! {"
524 111..ˇ2
525 "});
526 }
527
528 #[gpui::test]
529 async fn test_increment_sign_change(cx: &mut gpui::TestAppContext) {
530 let mut cx = NeovimBackedTestContext::new(cx).await;
531 cx.set_shared_state(indoc! {"
532 ˇ0
533 "})
534 .await;
535 cx.simulate_shared_keystrokes("ctrl-x").await;
536 cx.shared_state().await.assert_eq(indoc! {"
537 -ˇ1
538 "});
539 cx.simulate_shared_keystrokes("2 ctrl-a").await;
540 cx.shared_state().await.assert_eq(indoc! {"
541 ˇ1
542 "});
543 }
544
545 #[gpui::test]
546 async fn test_increment_sign_change_with_leading_zeros(cx: &mut gpui::TestAppContext) {
547 let mut cx = NeovimBackedTestContext::new(cx).await;
548 cx.set_shared_state(indoc! {"
549 00ˇ1
550 "})
551 .await;
552 cx.simulate_shared_keystrokes("ctrl-x").await;
553 cx.shared_state().await.assert_eq(indoc! {"
554 00ˇ0
555 "});
556 cx.simulate_shared_keystrokes("ctrl-x").await;
557 cx.shared_state().await.assert_eq(indoc! {"
558 -00ˇ1
559 "});
560 cx.simulate_shared_keystrokes("2 ctrl-a").await;
561 cx.shared_state().await.assert_eq(indoc! {"
562 00ˇ1
563 "});
564 }
565
566 #[gpui::test]
567 async fn test_increment_bin_wrapping_and_padding(cx: &mut gpui::TestAppContext) {
568 let mut cx = NeovimBackedTestContext::new(cx).await;
569 cx.set_shared_state(indoc! {"
570 0b111111111111111111111111111111111111111111111111111111111111111111111ˇ1
571 "})
572 .await;
573
574 cx.simulate_shared_keystrokes("ctrl-a").await;
575 cx.shared_state().await.assert_eq(indoc! {"
576 0b000000111111111111111111111111111111111111111111111111111111111111111ˇ1
577 "});
578 cx.simulate_shared_keystrokes("ctrl-a").await;
579 cx.shared_state().await.assert_eq(indoc! {"
580 0b000000000000000000000000000000000000000000000000000000000000000000000ˇ0
581 "});
582
583 cx.simulate_shared_keystrokes("ctrl-a").await;
584 cx.shared_state().await.assert_eq(indoc! {"
585 0b000000000000000000000000000000000000000000000000000000000000000000000ˇ1
586 "});
587 cx.simulate_shared_keystrokes("2 ctrl-x").await;
588 cx.shared_state().await.assert_eq(indoc! {"
589 0b000000111111111111111111111111111111111111111111111111111111111111111ˇ1
590 "});
591 }
592
593 #[gpui::test]
594 async fn test_increment_hex_wrapping_and_padding(cx: &mut gpui::TestAppContext) {
595 let mut cx = NeovimBackedTestContext::new(cx).await;
596 cx.set_shared_state(indoc! {"
597 0xfffffffffffffffffffˇf
598 "})
599 .await;
600
601 cx.simulate_shared_keystrokes("ctrl-a").await;
602 cx.shared_state().await.assert_eq(indoc! {"
603 0x0000fffffffffffffffˇf
604 "});
605 cx.simulate_shared_keystrokes("ctrl-a").await;
606 cx.shared_state().await.assert_eq(indoc! {"
607 0x0000000000000000000ˇ0
608 "});
609 cx.simulate_shared_keystrokes("ctrl-a").await;
610 cx.shared_state().await.assert_eq(indoc! {"
611 0x0000000000000000000ˇ1
612 "});
613 cx.simulate_shared_keystrokes("2 ctrl-x").await;
614 cx.shared_state().await.assert_eq(indoc! {"
615 0x0000fffffffffffffffˇf
616 "});
617 }
618
619 #[gpui::test]
620 async fn test_increment_wrapping(cx: &mut gpui::TestAppContext) {
621 let mut cx = NeovimBackedTestContext::new(cx).await;
622 cx.set_shared_state(indoc! {"
623 1844674407370955161ˇ9
624 "})
625 .await;
626
627 cx.simulate_shared_keystrokes("ctrl-a").await;
628 cx.shared_state().await.assert_eq(indoc! {"
629 1844674407370955161ˇ5
630 "});
631 cx.simulate_shared_keystrokes("ctrl-a").await;
632 cx.shared_state().await.assert_eq(indoc! {"
633 -1844674407370955161ˇ5
634 "});
635 cx.simulate_shared_keystrokes("ctrl-a").await;
636 cx.shared_state().await.assert_eq(indoc! {"
637 -1844674407370955161ˇ4
638 "});
639 cx.simulate_shared_keystrokes("3 ctrl-x").await;
640 cx.shared_state().await.assert_eq(indoc! {"
641 1844674407370955161ˇ4
642 "});
643 cx.simulate_shared_keystrokes("2 ctrl-a").await;
644 cx.shared_state().await.assert_eq(indoc! {"
645 -1844674407370955161ˇ5
646 "});
647 }
648
649 #[gpui::test]
650 async fn test_increment_inline(cx: &mut gpui::TestAppContext) {
651 let mut cx = NeovimBackedTestContext::new(cx).await;
652 cx.set_shared_state(indoc! {"
653 inline0x3ˇ9u32
654 "})
655 .await;
656
657 cx.simulate_shared_keystrokes("ctrl-a").await;
658 cx.shared_state().await.assert_eq(indoc! {"
659 inline0x3ˇau32
660 "});
661 cx.simulate_shared_keystrokes("ctrl-a").await;
662 cx.shared_state().await.assert_eq(indoc! {"
663 inline0x3ˇbu32
664 "});
665 cx.simulate_shared_keystrokes("l l l ctrl-a").await;
666 cx.shared_state().await.assert_eq(indoc! {"
667 inline0x3bu3ˇ3
668 "});
669 }
670
671 #[gpui::test]
672 async fn test_increment_hex_casing(cx: &mut gpui::TestAppContext) {
673 let mut cx = NeovimBackedTestContext::new(cx).await;
674 cx.set_shared_state(indoc! {"
675 0xFˇa
676 "})
677 .await;
678
679 cx.simulate_shared_keystrokes("ctrl-a").await;
680 cx.shared_state().await.assert_eq(indoc! {"
681 0xfˇb
682 "});
683 cx.simulate_shared_keystrokes("ctrl-a").await;
684 cx.shared_state().await.assert_eq(indoc! {"
685 0xfˇc
686 "});
687 }
688
689 #[gpui::test]
690 async fn test_increment_radix(cx: &mut gpui::TestAppContext) {
691 let mut cx = NeovimBackedTestContext::new(cx).await;
692
693 cx.simulate("ctrl-a", "ˇ total: 0xff")
694 .await
695 .assert_matches();
696 cx.simulate("ctrl-x", "ˇ total: 0xff")
697 .await
698 .assert_matches();
699 cx.simulate("ctrl-x", "ˇ total: 0xFF")
700 .await
701 .assert_matches();
702 cx.simulate("ctrl-a", "(ˇ0b10f)").await.assert_matches();
703 cx.simulate("ctrl-a", "ˇ-1").await.assert_matches();
704 cx.simulate("ctrl-a", "banˇana").await.assert_matches();
705 }
706
707 #[gpui::test]
708 async fn test_increment_steps(cx: &mut gpui::TestAppContext) {
709 let mut cx = NeovimBackedTestContext::new(cx).await;
710
711 cx.set_shared_state(indoc! {"
712 ˇ1
713 1
714 1 2
715 1
716 1"})
717 .await;
718
719 cx.simulate_shared_keystrokes("j v shift-g g ctrl-a").await;
720 cx.shared_state().await.assert_eq(indoc! {"
721 1
722 ˇ2
723 3 2
724 4
725 5"});
726
727 cx.simulate_shared_keystrokes("shift-g ctrl-v g g").await;
728 cx.shared_state().await.assert_eq(indoc! {"
729 «1ˇ»
730 «2ˇ»
731 «3ˇ» 2
732 «4ˇ»
733 «5ˇ»"});
734
735 cx.simulate_shared_keystrokes("g ctrl-x").await;
736 cx.shared_state().await.assert_eq(indoc! {"
737 ˇ0
738 0
739 0 2
740 0
741 0"});
742 cx.simulate_shared_keystrokes("v shift-g g ctrl-a").await;
743 cx.simulate_shared_keystrokes("v shift-g 5 g ctrl-a").await;
744 cx.shared_state().await.assert_eq(indoc! {"
745 ˇ6
746 12
747 18 2
748 24
749 30"});
750 }
751
752 #[gpui::test]
753 async fn test_increment_toggle(cx: &mut gpui::TestAppContext) {
754 let mut cx = VimTestContext::new(cx, true).await;
755
756 cx.set_state("let enabled = trˇue;", Mode::Normal);
757 cx.simulate_keystrokes("ctrl-a");
758 cx.assert_state("let enabled = falsˇe;", Mode::Normal);
759
760 cx.simulate_keystrokes("0 ctrl-a");
761 cx.assert_state("let enabled = truˇe;", Mode::Normal);
762
763 cx.set_state(
764 indoc! {"
765 ˇlet enabled = TRUE;
766 let enabled = TRUE;
767 let enabled = TRUE;
768 "},
769 Mode::Normal,
770 );
771 cx.simulate_keystrokes("shift-v j j ctrl-x");
772 cx.assert_state(
773 indoc! {"
774 ˇlet enabled = FALSE;
775 let enabled = FALSE;
776 let enabled = FALSE;
777 "},
778 Mode::Normal,
779 );
780
781 cx.set_state(
782 indoc! {"
783 let enabled = ˇYes;
784 let enabled = Yes;
785 let enabled = Yes;
786 "},
787 Mode::Normal,
788 );
789 cx.simulate_keystrokes("ctrl-v j j e ctrl-x");
790 cx.assert_state(
791 indoc! {"
792 let enabled = ˇNo;
793 let enabled = No;
794 let enabled = No;
795 "},
796 Mode::Normal,
797 );
798
799 cx.set_state("ˇlet enabled = True;", Mode::Normal);
800 cx.simulate_keystrokes("ctrl-a");
801 cx.assert_state("let enabled = Falsˇe;", Mode::Normal);
802
803 cx.simulate_keystrokes("ctrl-a");
804 cx.assert_state("let enabled = Truˇe;", Mode::Normal);
805
806 cx.set_state("let enabled = Onˇ;", Mode::Normal);
807 cx.simulate_keystrokes("v b ctrl-a");
808 cx.assert_state("let enabled = ˇOff;", Mode::Normal);
809 }
810
811 #[gpui::test]
812 async fn test_increment_order(cx: &mut gpui::TestAppContext) {
813 let mut cx = VimTestContext::new(cx, true).await;
814
815 cx.set_state("aaˇa false 1 2 3", Mode::Normal);
816 cx.simulate_keystrokes("ctrl-a");
817 cx.assert_state("aaa truˇe 1 2 3", Mode::Normal);
818
819 cx.set_state("aaˇa 1 false 2 3", Mode::Normal);
820 cx.simulate_keystrokes("ctrl-a");
821 cx.assert_state("aaa ˇ2 false 2 3", Mode::Normal);
822
823 cx.set_state("trueˇ 1 2 3", Mode::Normal);
824 cx.simulate_keystrokes("ctrl-a");
825 cx.assert_state("true ˇ2 2 3", Mode::Normal);
826
827 cx.set_state("falseˇ", Mode::Normal);
828 cx.simulate_keystrokes("ctrl-a");
829 cx.assert_state("truˇe", Mode::Normal);
830
831 cx.set_state("⚡️ˇ⚡️", Mode::Normal);
832 cx.simulate_keystrokes("ctrl-a");
833 cx.assert_state("⚡️ˇ⚡️", Mode::Normal);
834 }
835
836 #[gpui::test]
837 async fn test_increment_visual_partial_number(cx: &mut gpui::TestAppContext) {
838 let mut cx = NeovimBackedTestContext::new(cx).await;
839
840 cx.set_shared_state("ˇ123").await;
841 cx.simulate_shared_keystrokes("v l ctrl-a").await;
842 cx.shared_state().await.assert_eq(indoc! {"ˇ133"});
843 cx.simulate_shared_keystrokes("l v l ctrl-a").await;
844 cx.shared_state().await.assert_eq(indoc! {"1ˇ34"});
845 cx.simulate_shared_keystrokes("shift-v y p p ctrl-v k k l ctrl-a")
846 .await;
847 cx.shared_state().await.assert_eq(indoc! {"ˇ144\n144\n144"});
848 }
849}