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    // 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}