Fix vim increment/decrement on Markdown list markers (#47978)
lex00
,
Claude Opus 4.5
, and
dino
created
`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>
@@ -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");
+ }
}