increment.rs

  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}