vim: Fix increment/decrement for negative numbers without gaps (#58327)

Finally continuing from PR #35200:

I adapted the fix to the changed find_target() logic and added more
tests for version strings like `0.82.46`.
This is a different but similar fix compared to #47356 for helix mode -
not sure if this can and should be unified?

Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
(should not affect UI at all)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Closes #35193 

Release Notes:

- Fixed vim's increment (`ctrl-a`) and decrement (`ctrl-x`) commands
skipping the number under the cursor in dotted strings like version
numbers (e.g. `0.81.46`) and hyphened date strings (e.g. `2015-02-01`)

---------

Co-authored-by: Stefan Bethge <kjyv@users.noreply.github.com>
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
Co-authored-by: dino <dinojoaocosta@gmail.com>
This commit is contained in:
authored and GitHub committed 2026-06-03 11:31:38 +00:00
1 parent 8ed0f5f05a
commit 95dfe8ca6d
3 files changed
+53 -8

No files matched your search

+38 -8
View File
@@ -227,6 +227,15 @@ fn find_target(
break;
}
// vim's ctrl-a/ctrl-x operate on the number at or after the cursor and
// do not require it to be whitespace-separated. Stop the backward scan
// at a '-' so we keep the number the cursor is on (e.g. `05` in
// `2025-05-10`) instead of scanning past the '-' to an earlier number on
// the line. vim folds a leading '-' into the number, making it negative.
if ch == '-' {
break;
}
// Avoid the influence of hexadecimal letters
if first_char_is_num
&& !ch.is_ascii_hexdigit()
@@ -284,17 +293,17 @@ fn find_target(
begin = None;
target = String::new();
} else if ch == '.' {
// When the cursor is on a number followed by a dot and a non-digit
// (`ˇ1. item`), terminate the match so the number is incrementable.
// Without this, the dot unconditionally resets the scan and the
// number is skipped. We only do this when the cursor is on the
// number, when it's past (`111.ˇ.2`), we still reset so the forward
// scan can find the number after the dots.
let next_is_non_digit = chars.peek().map_or(true, |char| !char.is_digit(radix));
// vim treats '.' as a separator, not a decimal point: ctrl-a/ctrl-x
// act on the whole digit run, not a float. So when the cursor is on
// the current number, terminate the match at the dot regardless of
// what follows it, so `ˇ1. item` and version strings like `0.8ˇ1.46`
// (-> `0.82.46`) both increment the number under the cursor. When the
// cursor is past the number (`111.ˇ.2`), `on_number` is false and we
// still reset so the forward scan finds the number after the dots.
let on_number =
is_num && begin.is_some_and(|begin| begin >= start_offset || start_offset < offset);
if on_number && next_is_non_digit {
if on_number {
end = Some(offset);
break;
}
@@ -475,6 +484,11 @@ mod test {
cx.shared_state().await.assert_eq(indoc! {"
1.ˇ2
"});
// '.' is a separator, not a decimal point, so the number the cursor is
// on is incremented even without surrounding whitespace.
cx.simulate("ctrl-a", "0.8ˇ1.46").await.assert_matches();
cx.simulate("ctrl-x", "0.8ˇ1.46").await.assert_matches();
}
#[gpui::test]
@@ -778,6 +792,22 @@ mod test {
30"});
}
#[gpui::test]
async fn test_increment_negative_numbers(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
// vim folds a leading '-' into the number, so ctrl-a on the `05` here
// operates on `-05` and decrements the visible digits to `04`.
cx.simulate("ctrl-a", "2025-0ˇ5-10").await.assert_matches();
// Cursor on or just before a trailing '-' (with or without a following
// number) must not scan past the '-' into the earlier number.
cx.simulate("ctrl-a", "2025-05ˇ-").await.assert_matches();
cx.simulate("ctrl-a", "2025-05ˇ- 345")
.await
.assert_matches();
}
#[gpui::test]
async fn test_increment_toggle(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, true).await;
@@ -0,0 +1,9 @@
{"Put":{"state":"2025-0ˇ5-10"}}
{"Key":"ctrl-a"}
{"Get":{"state":"2025-0ˇ4-10","mode":"Normal"}}
{"Put":{"state":"2025-05ˇ-"}}
{"Key":"ctrl-a"}
{"Get":{"state":"2025-05ˇ-","mode":"Normal"}}
{"Put":{"state":"2025-05ˇ- 345"}}
{"Key":"ctrl-a"}
{"Get":{"state":"2025-05- 34ˇ6","mode":"Normal"}}
@@ -3,3 +3,9 @@
{"Get":{"state":"1.ˇ3\n","mode":"Normal"}}
{"Key":"ctrl-x"}
{"Get":{"state":"1.ˇ2\n","mode":"Normal"}}
{"Put":{"state":"0.8ˇ1.46"}}
{"Key":"ctrl-a"}
{"Get":{"state":"0.8ˇ2.46","mode":"Normal"}}
{"Put":{"state":"0.8ˇ1.46"}}
{"Key":"ctrl-x"}
{"Get":{"state":"0.8ˇ0.46","mode":"Normal"}}