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"}}