diff --git a/crates/vim/src/normal/increment.rs b/crates/vim/src/normal/increment.rs index 1d2a4e9b61..34ac4aab1f 100644 --- a/crates/vim/src/normal/increment.rs +++ b/crates/vim/src/normal/increment.rs @@ -70,8 +70,19 @@ impl Vim { } else { Point::new(row, 0) }; + let end = if row == selection.end.row { + selection.end + } else { + Point::new(row, snapshot.line_len(multi_buffer::MultiBufferRow(row))) + }; - if let Some((range, num, radix)) = find_number(&snapshot, start) { + let number_result = if !selection.is_empty() { + find_number_in_range(&snapshot, start, end) + } else { + find_number(&snapshot, start) + }; + + if let Some((range, num, radix)) = number_result { let replace = match radix { 10 => increment_decimal_string(&num, delta), 16 => increment_hex_string(&num, delta), @@ -189,6 +200,90 @@ fn increment_binary_string(num: &str, delta: i64) -> String { format!("{:0width$b}", result, width = num.len()) } +fn find_number_in_range( + snapshot: &MultiBufferSnapshot, + start: Point, + end: Point, +) -> Option<(Range, String, u32)> { + let start_offset = start.to_offset(snapshot); + let end_offset = end.to_offset(snapshot); + + let mut offset = start_offset; + + // Backward scan to find the start of the number, but stop at start_offset + for ch in snapshot.reversed_chars_at(offset) { + if ch.is_ascii_hexdigit() || ch == '-' || ch == 'b' || ch == 'x' { + if offset == 0 { + break; + } + offset -= ch.len_utf8(); + if offset < start_offset { + offset = start_offset; + break; + } + } else { + break; + } + } + + let mut begin = None; + let mut end_num = None; + let mut num = String::new(); + let mut radix = 10; + + let mut chars = snapshot.chars_at(offset).peekable(); + + while let Some(ch) = chars.next() { + if offset >= end_offset { + break; // stop at end of selection + } + + if num == "0" && ch == 'b' && chars.peek().is_some() && chars.peek().unwrap().is_digit(2) { + radix = 2; + begin = None; + num = String::new(); + } else if num == "0" + && ch == 'x' + && chars.peek().is_some() + && chars.peek().unwrap().is_ascii_hexdigit() + { + radix = 16; + begin = None; + num = String::new(); + } + + if ch.is_digit(radix) + || (begin.is_none() + && ch == '-' + && chars.peek().is_some() + && chars.peek().unwrap().is_digit(radix)) + { + if begin.is_none() { + begin = Some(offset); + } + num.push(ch); + } else if begin.is_some() { + end_num = Some(offset); + break; + } else if ch == '\n' { + break; + } + + offset += ch.len_utf8(); + } + + if let Some(begin) = begin { + let end_num = end_num.unwrap_or(offset); + Some(( + begin.to_point(snapshot)..end_num.to_point(snapshot), + num, + radix, + )) + } else { + None + } +} + fn find_number( snapshot: &MultiBufferSnapshot, start: Point, @@ -764,4 +859,18 @@ mod test { cx.simulate_keystrokes("v b ctrl-a"); cx.assert_state("let enabled = ˇOff;", Mode::Normal); } + + #[gpui::test] + async fn test_increment_visual_partial_number(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state("ˇ123").await; + cx.simulate_shared_keystrokes("v l ctrl-a").await; + cx.shared_state().await.assert_eq(indoc! {"ˇ133"}); + cx.simulate_shared_keystrokes("l v l ctrl-a").await; + cx.shared_state().await.assert_eq(indoc! {"1ˇ34"}); + cx.simulate_shared_keystrokes("shift-v y p p ctrl-v k k l ctrl-a") + .await; + cx.shared_state().await.assert_eq(indoc! {"ˇ144\n144\n144"}); + } } diff --git a/crates/vim/test_data/test_increment_visual_partial_number.json b/crates/vim/test_data/test_increment_visual_partial_number.json new file mode 100644 index 0000000000..ebb4eece78 --- /dev/null +++ b/crates/vim/test_data/test_increment_visual_partial_number.json @@ -0,0 +1,20 @@ +{"Put":{"state":"ˇ123"}} +{"Key":"v"} +{"Key":"l"} +{"Key":"ctrl-a"} +{"Get":{"state":"ˇ133","mode":"Normal"}} +{"Key":"l"} +{"Key":"v"} +{"Key":"l"} +{"Key":"ctrl-a"} +{"Get":{"state":"1ˇ34","mode":"Normal"}} +{"Key":"shift-v"} +{"Key":"y"} +{"Key":"p"} +{"Key":"p"} +{"Key":"ctrl-v"} +{"Key":"k"} +{"Key":"k"} +{"Key":"l"} +{"Key":"ctrl-a"} +{"Get":{"state":"ˇ144\n144\n144","mode":"Normal"}}