From 2b5386b4386254101e8260ba11dfe2812d7742dd Mon Sep 17 00:00:00 2001 From: lex00 <121451605+lex00@users.noreply.github.com> Date: Wed, 4 Feb 2026 16:52:43 -0700 Subject: [PATCH] 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 Co-authored-by: dino --- crates/vim/src/normal/increment.rs | 49 +++++++++++++++++++ ...ement_markdown_list_markers_multiline.json | 9 ++++ .../vim/test_data/test_increment_radix.json | 3 ++ 3 files changed, 61 insertions(+) create mode 100644 crates/vim/test_data/test_increment_markdown_list_markers_multiline.json diff --git a/crates/vim/src/normal/increment.rs b/crates/vim/src/normal/increment.rs index d9ef32deba5a3beb530d9ee42e2a6254df8c253b..9b6707fdb92520e95e874a5be143024beb21b873 100644 --- a/crates/vim/src/normal/increment.rs +++ b/crates/vim/src/normal/increment.rs @@ -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"); + } } diff --git a/crates/vim/test_data/test_increment_markdown_list_markers_multiline.json b/crates/vim/test_data/test_increment_markdown_list_markers_multiline.json new file mode 100644 index 0000000000000000000000000000000000000000..cecd514a9024e0be2014876b2c5fb67da46949da --- /dev/null +++ b/crates/vim/test_data/test_increment_markdown_list_markers_multiline.json @@ -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"}} diff --git a/crates/vim/test_data/test_increment_radix.json b/crates/vim/test_data/test_increment_radix.json index 0f41c01599cde39a4528a30be94d7c1fab0ff385..d2df7a9c7f13bf2263608218c9d40d9ce5084649 100644 --- a/crates/vim/test_data/test_increment_radix.json +++ b/crates/vim/test_data/test_increment_radix.json @@ -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"}}