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