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