Fix vim increment/decrement on Markdown list markers (#47978)

`find_target()` failed to match numbers followed by a dot and a
non-digit (e.g. `1. item`), because the dot unconditionally reset
the scan state, discarding the number. Additionally, numbers at the
start of non-first lines were missed because the backward scan
stopped on the preceding newline and the forward scan immediately
broke on it.

Closes #47761

Release Notes:

- Fixed vim increment (`ctrl-a`) and decrement (`ctrl-x`) not working on Markdown ordered list markers like `1.`, `2.`, etc.

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: dino <dinojoaocosta@gmail.com>
This commit is contained in:
authored and GitHub committed 2026-02-04 23:52:43 +00:00
1 parent 8c944f4337
commit bee1edcc4e
3 files changed
+61

No files matched your search

+49
View File
@@ -241,6 +241,15 @@ fn find_target(
offset -= ch.len_utf8();
}
// The backward scan breaks on whitespace, including newlines. Without this
// skip, the forward scan would start on the newline and immediately break
// (since it also breaks on newlines), finding nothing on the current line.
if let Some(ch) = snapshot.chars_at(offset).next() {
if ch == '\n' {
offset += ch.len_utf8();
}
}
let mut begin = None;
let mut end = None;
let mut target = String::new();
@@ -271,6 +280,21 @@ 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));
let on_number =
is_num && begin.is_some_and(|begin| begin >= start_offset || start_offset < offset);
if on_number && next_is_non_digit {
end = Some(offset);
break;
}
is_num = false;
begin = None;
target = String::new();
@@ -701,6 +725,7 @@ mod test {
.assert_matches();
cx.simulate("ctrl-a", "(ˇ0b10f)").await.assert_matches();
cx.simulate("ctrl-a", "ˇ-1").await.assert_matches();
cx.simulate("ctrl-a", "-ˇ1").await.assert_matches();
cx.simulate("ctrl-a", "banˇana").await.assert_matches();
}
@@ -846,4 +871,28 @@ mod test {
.await;
cx.shared_state().await.assert_eq(indoc! {"ˇ144\n144\n144"});
}
#[gpui::test]
async fn test_increment_markdown_list_markers_multiline(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
cx.set_shared_state("# Title\nˇ1. item\n2. item\n3. item")
.await;
cx.simulate_shared_keystrokes("ctrl-a").await;
cx.shared_state()
.await
.assert_eq("# Title\nˇ2. item\n2. item\n3. item");
cx.simulate_shared_keystrokes("j").await;
cx.shared_state()
.await
.assert_eq("# Title\n2. item\nˇ2. item\n3. item");
cx.simulate_shared_keystrokes("ctrl-a").await;
cx.shared_state()
.await
.assert_eq("# Title\n2. item\nˇ3. item\n3. item");
cx.simulate_shared_keystrokes("ctrl-x").await;
cx.shared_state()
.await
.assert_eq("# Title\n2. item\nˇ2. item\n3. item");
}
}
@@ -0,0 +1,9 @@
{"Put":{"state":"# Title\nˇ1. item\n2. item\n3. item"}}
{"Key":"ctrl-a"}
{"Get":{"state":"# Title\nˇ2. item\n2. item\n3. item","mode":"Normal"}}
{"Key":"j"}
{"Get":{"state":"# Title\n2. item\nˇ2. item\n3. item","mode":"Normal"}}
{"Key":"ctrl-a"}
{"Get":{"state":"# Title\n2. item\nˇ3. item\n3. item","mode":"Normal"}}
{"Key":"ctrl-x"}
{"Get":{"state":"# Title\n2. item\nˇ2. item\n3. item","mode":"Normal"}}
@@ -13,6 +13,9 @@
{"Put":{"state":"ˇ-1"}}
{"Key":"ctrl-a"}
{"Get":{"state":"ˇ0","mode":"Normal"}}
{"Put":{"state":"-ˇ1"}}
{"Key":"ctrl-a"}
{"Get":{"state":"ˇ0","mode":"Normal"}}
{"Put":{"state":"banˇana"}}
{"Key":"ctrl-a"}
{"Get":{"state":"banˇana","mode":"Normal"}}