diff --git a/crates/vim/src/normal/increment.rs b/crates/vim/src/normal/increment.rs index 8786eae872..6d66e380c3 100644 --- a/crates/vim/src/normal/increment.rs +++ b/crates/vim/src/normal/increment.rs @@ -28,18 +28,18 @@ pub fn register(editor: &mut Editor, cx: &mut ViewContext) { vim.record_current_action(cx); let count = vim.take_count(cx).unwrap_or(1); let step = if action.step { 1 } else { 0 }; - vim.increment(count as i32, step, cx) + vim.increment(count as i64, step, cx) }); Vim::action(editor, cx, |vim, action: &Decrement, cx| { vim.record_current_action(cx); let count = vim.take_count(cx).unwrap_or(1); let step = if action.step { -1 } else { 0 }; - vim.increment(-(count as i32), step, cx) + vim.increment(-(count as i64), step, cx) }); } impl Vim { - fn increment(&mut self, mut delta: i32, step: i32, cx: &mut ViewContext) { + fn increment(&mut self, mut delta: i64, step: i32, cx: &mut ViewContext) { self.store_visual_marks(cx); self.update_editor(cx, |vim, editor, cx| { let mut edits = Vec::new(); @@ -60,23 +60,14 @@ impl Vim { }; if let Some((range, num, radix)) = find_number(&snapshot, start) { - if let Ok(val) = i32::from_str_radix(&num, radix) { - let result = val + delta; - delta += step; - let replace = match radix { - 10 => format!("{}", result), - 16 => { - if num.to_ascii_lowercase() == num { - format!("{:x}", result) - } else { - format!("{:X}", result) - } - } - 2 => format!("{:b}", result), - _ => unreachable!(), - }; - edits.push((range.clone(), replace)); - } + let replace = match radix { + 10 => increment_decimal_string(&num, delta), + 16 => increment_hex_string(&num, delta), + 2 => increment_binary_string(&num, delta), + _ => unreachable!(), + }; + delta += step as i64; + edits.push((range.clone(), replace)); if selection.is_empty() { new_anchors.push((false, snapshot.anchor_after(range.end))) } @@ -107,6 +98,70 @@ impl Vim { } } +fn increment_decimal_string(mut num: &str, mut delta: i64) -> String { + let mut negative = false; + if num.chars().next() == Some('-') { + negative = true; + delta = 0 - delta; + num = &num[1..]; + } + let result = if let Ok(value) = u64::from_str_radix(num, 10) { + let wrapped = value.wrapping_add_signed(delta); + if delta < 0 && wrapped > value { + negative = !negative; + (u64::MAX - wrapped).wrapping_add(1) + } else if delta > 0 && wrapped < value { + negative = !negative; + u64::MAX - wrapped + } else { + wrapped + } + } else { + u64::MAX + }; + + if result == 0 || !negative { + format!("{}", result) + } else { + format!("-{}", result) + } +} + +fn increment_hex_string(num: &str, delta: i64) -> String { + let result = if let Ok(val) = u64::from_str_radix(&num, 16) { + val.wrapping_add_signed(delta) + } else { + u64::MAX + }; + if should_use_lowercase(num) { + format!("{:0width$x}", result, width = num.len()) + } else { + format!("{:0width$X}", result, width = num.len()) + } +} + +fn should_use_lowercase(num: &str) -> bool { + let mut use_uppercase = false; + for ch in num.chars() { + if ch.is_ascii_lowercase() { + return true; + } + if ch.is_ascii_uppercase() { + use_uppercase = true; + } + } + !use_uppercase +} + +fn increment_binary_string(num: &str, delta: i64) -> String { + let result = if let Ok(val) = u64::from_str_radix(&num, 2) { + val.wrapping_add_signed(delta) + } else { + u64::MAX + }; + format!("{:0width$b}", result, width = num.len()) +} + fn find_number( snapshot: &MultiBufferSnapshot, start: Point, @@ -114,10 +169,10 @@ fn find_number( let mut offset = start.to_offset(snapshot); let ch0 = snapshot.chars_at(offset).next(); - if ch0.as_ref().is_some_and(char::is_ascii_digit) || matches!(ch0, Some('-' | 'b' | 'x')) { + if ch0.as_ref().is_some_and(char::is_ascii_hexdigit) || matches!(ch0, Some('-' | 'b' | 'x')) { // go backwards to the start of any number the selection is within for ch in snapshot.reversed_chars_at(offset) { - if ch.is_ascii_digit() || ch == '-' || ch == 'b' || ch == 'x' { + if ch.is_ascii_hexdigit() || ch == '-' || ch == 'b' || ch == 'x' { offset -= ch.len_utf8(); continue; } @@ -158,6 +213,8 @@ fn find_number( begin = Some(offset); } num.push(ch); + println!("pushing {}", ch); + println!(); } else if begin.is_some() { end = Some(offset); break; @@ -250,6 +307,146 @@ mod test { "}); } + #[gpui::test] + async fn test_increment_sign_change(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + cx.set_shared_state(indoc! {" + ˇ0 + "}) + .await; + cx.simulate_shared_keystrokes("ctrl-x").await; + cx.shared_state().await.assert_eq(indoc! {" + -ˇ1 + "}); + cx.simulate_shared_keystrokes("2 ctrl-a").await; + cx.shared_state().await.assert_eq(indoc! {" + ˇ1 + "}); + } + + #[gpui::test] + async fn test_increment_bin_wrapping_and_padding(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + cx.set_shared_state(indoc! {" + 0b111111111111111111111111111111111111111111111111111111111111111111111ˇ1 + "}) + .await; + + cx.simulate_shared_keystrokes("ctrl-a").await; + cx.shared_state().await.assert_eq(indoc! {" + 0b000000111111111111111111111111111111111111111111111111111111111111111ˇ1 + "}); + cx.simulate_shared_keystrokes("ctrl-a").await; + cx.shared_state().await.assert_eq(indoc! {" + 0b000000000000000000000000000000000000000000000000000000000000000000000ˇ0 + "}); + + cx.simulate_shared_keystrokes("ctrl-a").await; + cx.shared_state().await.assert_eq(indoc! {" + 0b000000000000000000000000000000000000000000000000000000000000000000000ˇ1 + "}); + cx.simulate_shared_keystrokes("2 ctrl-x").await; + cx.shared_state().await.assert_eq(indoc! {" + 0b000000111111111111111111111111111111111111111111111111111111111111111ˇ1 + "}); + } + + #[gpui::test] + async fn test_increment_hex_wrapping_and_padding(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + cx.set_shared_state(indoc! {" + 0xfffffffffffffffffffˇf + "}) + .await; + + cx.simulate_shared_keystrokes("ctrl-a").await; + cx.shared_state().await.assert_eq(indoc! {" + 0x0000fffffffffffffffˇf + "}); + cx.simulate_shared_keystrokes("ctrl-a").await; + cx.shared_state().await.assert_eq(indoc! {" + 0x0000000000000000000ˇ0 + "}); + cx.simulate_shared_keystrokes("ctrl-a").await; + cx.shared_state().await.assert_eq(indoc! {" + 0x0000000000000000000ˇ1 + "}); + cx.simulate_shared_keystrokes("2 ctrl-x").await; + cx.shared_state().await.assert_eq(indoc! {" + 0x0000fffffffffffffffˇf + "}); + } + + #[gpui::test] + async fn test_increment_wrapping(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + cx.set_shared_state(indoc! {" + 1844674407370955161ˇ9 + "}) + .await; + + cx.simulate_shared_keystrokes("ctrl-a").await; + cx.shared_state().await.assert_eq(indoc! {" + 1844674407370955161ˇ5 + "}); + cx.simulate_shared_keystrokes("ctrl-a").await; + cx.shared_state().await.assert_eq(indoc! {" + -1844674407370955161ˇ5 + "}); + cx.simulate_shared_keystrokes("ctrl-a").await; + cx.shared_state().await.assert_eq(indoc! {" + -1844674407370955161ˇ4 + "}); + cx.simulate_shared_keystrokes("3 ctrl-x").await; + cx.shared_state().await.assert_eq(indoc! {" + 1844674407370955161ˇ4 + "}); + cx.simulate_shared_keystrokes("2 ctrl-a").await; + cx.shared_state().await.assert_eq(indoc! {" + -1844674407370955161ˇ5 + "}); + } + + #[gpui::test] + async fn test_increment_inline(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + cx.set_shared_state(indoc! {" + inline0x3ˇ9u32 + "}) + .await; + + cx.simulate_shared_keystrokes("ctrl-a").await; + cx.shared_state().await.assert_eq(indoc! {" + inline0x3ˇau32 + "}); + cx.simulate_shared_keystrokes("ctrl-a").await; + cx.shared_state().await.assert_eq(indoc! {" + inline0x3ˇbu32 + "}); + cx.simulate_shared_keystrokes("l l l ctrl-a").await; + cx.shared_state().await.assert_eq(indoc! {" + inline0x3bu3ˇ3 + "}); + } + + #[gpui::test] + async fn test_increment_hex_casing(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + cx.set_shared_state(indoc! {" + 0xFˇa + "}) + .await; + + cx.simulate_shared_keystrokes("ctrl-a").await; + cx.shared_state().await.assert_eq(indoc! {" + 0xfˇb + "}); + cx.simulate_shared_keystrokes("ctrl-a").await; + cx.shared_state().await.assert_eq(indoc! {" + 0xfˇc + "}); + } + #[gpui::test] async fn test_increment_radix(cx: &mut gpui::TestAppContext) { let mut cx = NeovimBackedTestContext::new(cx).await; diff --git a/crates/vim/test_data/test_increment_bin_wrapping_and_padding.json b/crates/vim/test_data/test_increment_bin_wrapping_and_padding.json new file mode 100644 index 0000000000..4f1a6aa1d3 --- /dev/null +++ b/crates/vim/test_data/test_increment_bin_wrapping_and_padding.json @@ -0,0 +1,10 @@ +{"Put":{"state":"0b111111111111111111111111111111111111111111111111111111111111111111111ˇ1\n"}} +{"Key":"ctrl-a"} +{"Get":{"state":"0b000000111111111111111111111111111111111111111111111111111111111111111ˇ1\n", "mode":"Normal"}} +{"Key":"ctrl-a"} +{"Get":{"state":"0b000000000000000000000000000000000000000000000000000000000000000000000ˇ0\n","mode":"Normal"}} +{"Key":"ctrl-a"} +{"Get":{"state":"0b000000000000000000000000000000000000000000000000000000000000000000000ˇ1\n","mode":"Normal"}} +{"Key":"2"} +{"Key":"ctrl-x"} +{"Get":{"state":"0b000000111111111111111111111111111111111111111111111111111111111111111ˇ1\n", "mode":"Normal"}} diff --git a/crates/vim/test_data/test_increment_hex_casing.json b/crates/vim/test_data/test_increment_hex_casing.json new file mode 100644 index 0000000000..951906fa25 --- /dev/null +++ b/crates/vim/test_data/test_increment_hex_casing.json @@ -0,0 +1,5 @@ +{"Put":{"state":"0xFˇa\n"}} +{"Key":"ctrl-a"} +{"Get":{"state":"0xfˇb\n","mode":"Normal"}} +{"Key":"ctrl-a"} +{"Get":{"state":"0xfˇc\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_increment_hex_wrapping_and_padding.json b/crates/vim/test_data/test_increment_hex_wrapping_and_padding.json new file mode 100644 index 0000000000..23a5611264 --- /dev/null +++ b/crates/vim/test_data/test_increment_hex_wrapping_and_padding.json @@ -0,0 +1,10 @@ +{"Put":{"state":"0xfffffffffffffffffffˇf\n"}} +{"Key":"ctrl-a"} +{"Get":{"state":"0x0000fffffffffffffffˇf\n", "mode":"Normal"}} +{"Key":"ctrl-a"} +{"Get":{"state":"0x0000000000000000000ˇ0\n","mode":"Normal"}} +{"Key":"ctrl-a"} +{"Get":{"state":"0x0000000000000000000ˇ1\n","mode":"Normal"}} +{"Key":"2"} +{"Key":"ctrl-x"} +{"Get":{"state":"0x0000fffffffffffffffˇf\n", "mode":"Normal"}} diff --git a/crates/vim/test_data/test_increment_inline.json b/crates/vim/test_data/test_increment_inline.json new file mode 100644 index 0000000000..98c4fc2805 --- /dev/null +++ b/crates/vim/test_data/test_increment_inline.json @@ -0,0 +1,10 @@ +{"Put":{"state":"inline0x3ˇ9u32\n"}} +{"Key":"ctrl-a"} +{"Get":{"state":"inline0x3ˇau32\n","mode":"Normal"}} +{"Key":"ctrl-a"} +{"Get":{"state":"inline0x3ˇbu32\n", "mode":"Normal"}} +{"Key":"l"} +{"Key":"l"} +{"Key":"l"} +{"Key":"ctrl-a"} +{"Get":{"state":"inline0x3bu3ˇ3\n", "mode":"Normal"}} diff --git a/crates/vim/test_data/test_increment_sign_change.json b/crates/vim/test_data/test_increment_sign_change.json new file mode 100644 index 0000000000..1f4edd57b4 --- /dev/null +++ b/crates/vim/test_data/test_increment_sign_change.json @@ -0,0 +1,6 @@ +{"Put":{"state":"ˇ0\n"}} +{"Key":"ctrl-x"} +{"Get":{"state":"-ˇ1\n","mode":"Normal"}} +{"Key":"2"} +{"Key":"ctrl-a"} +{"Get":{"state":"ˇ1\n", "mode":"Normal"}} diff --git a/crates/vim/test_data/test_increment_wrapping.json b/crates/vim/test_data/test_increment_wrapping.json new file mode 100644 index 0000000000..9f84c8cb11 --- /dev/null +++ b/crates/vim/test_data/test_increment_wrapping.json @@ -0,0 +1,13 @@ +{"Put":{"state":"1844674407370955161ˇ9\n"}} +{"Key":"ctrl-a"} +{"Get":{"state":"1844674407370955161ˇ5\n","mode":"Normal"}} +{"Key":"ctrl-a"} +{"Get":{"state":"-1844674407370955161ˇ5\n", "mode":"Normal"}} +{"Key":"ctrl-a"} +{"Get":{"state":"-1844674407370955161ˇ4\n", "mode":"Normal"}} +{"Key":"3"} +{"Key":"ctrl-x"} +{"Get":{"state":"1844674407370955161ˇ4\n", "mode":"Normal"}} +{"Key":"2"} +{"Key":"ctrl-a"} +{"Get":{"state":"-1844674407370955161ˇ5\n", "mode":"Normal"}}