Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T04:31:10.394Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

increment.rs

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