diff options
author | greg-enbala | 2023-01-16 16:15:23 +0000 |
---|---|---|
committer | GitHub | 2023-01-16 16:15:23 +0000 |
commit | 60f84be40c1c488dacf823f791ca33f43b5d28d8 (patch) | |
tree | 0efb36c23780c8be4e5e7f6d26675da93091ec16 /helix-core/src/increment/integer.rs | |
parent | 97083f88364e1455f42023dadadfb410fd476505 (diff) |
Separate jump behavior from increment/decrement (#4123)
increment/decrement (C-a/C-x) had some buggy behavior where selections
could be offset incorrectly or the editor could panic with some edits
that changed the number of characters in a number or date. These stemmed
from the automatic jumping behavior which attempted to find the next
date or integer to increment. The jumping behavior also complicated the
code quite a bit and made the behavior somewhat difficult to predict
when using many cursors.
This change removes the automatic jumping behavior and only increments
or decrements when the full text in a range of a selection is a number
or date. This simplifies the code and fixes the panics and buggy
behaviors from changing the number of characters.
Diffstat (limited to 'helix-core/src/increment/integer.rs')
-rw-r--r-- | helix-core/src/increment/integer.rs | 235 |
1 files changed, 235 insertions, 0 deletions
diff --git a/helix-core/src/increment/integer.rs b/helix-core/src/increment/integer.rs new file mode 100644 index 00000000..30803e17 --- /dev/null +++ b/helix-core/src/increment/integer.rs @@ -0,0 +1,235 @@ +const SEPARATOR: char = '_'; + +/// Increment an integer. +/// +/// Supported bases: +/// 2 with prefix 0b +/// 8 with prefix 0o +/// 10 with no prefix +/// 16 with prefix 0x +/// +/// An integer can contain `_` as a separator but may not start or end with a separator. +/// Base 10 integers can go negative, but bases 2, 8, and 16 cannot. +/// All addition and subtraction is saturating. +pub fn increment(selected_text: &str, amount: i64) -> Option<String> { + if selected_text.is_empty() + || selected_text.ends_with(SEPARATOR) + || selected_text.starts_with(SEPARATOR) + { + return None; + } + + let radix = if selected_text.starts_with("0x") { + 16 + } else if selected_text.starts_with("0o") { + 8 + } else if selected_text.starts_with("0b") { + 2 + } else { + 10 + }; + + // Get separator indexes from right to left. + let separator_rtl_indexes: Vec<usize> = selected_text + .chars() + .rev() + .enumerate() + .filter_map(|(i, c)| if c == SEPARATOR { Some(i) } else { None }) + .collect(); + + let word: String = selected_text.chars().filter(|&c| c != SEPARATOR).collect(); + + let mut new_text = if radix == 10 { + let number = &word; + let value = i128::from_str_radix(number, radix).ok()?; + let new_value = value.saturating_add(amount as i128); + + let format_length = match (value.is_negative(), new_value.is_negative()) { + (true, false) => number.len() - 1, + (false, true) => number.len() + 1, + _ => number.len(), + } - separator_rtl_indexes.len(); + + if number.starts_with('0') || number.starts_with("-0") { + format!("{:01$}", new_value, format_length) + } else { + format!("{}", new_value) + } + } else { + let number = &word[2..]; + let value = u128::from_str_radix(number, radix).ok()?; + let new_value = (value as i128).saturating_add(amount as i128); + let new_value = if new_value < 0 { 0 } else { new_value }; + let format_length = selected_text.len() - 2 - separator_rtl_indexes.len(); + + match radix { + 2 => format!("0b{:01$b}", new_value, format_length), + 8 => format!("0o{:01$o}", new_value, format_length), + 16 => { + let (lower_count, upper_count): (usize, usize) = + number.chars().fold((0, 0), |(lower, upper), c| { + ( + lower + c.is_ascii_lowercase().then(|| 1).unwrap_or(0), + upper + c.is_ascii_uppercase().then(|| 1).unwrap_or(0), + ) + }); + if upper_count > lower_count { + format!("0x{:01$X}", new_value, format_length) + } else { + format!("0x{:01$x}", new_value, format_length) + } + } + _ => unimplemented!("radix not supported: {}", radix), + } + }; + + // Add separators from original number. + for &rtl_index in &separator_rtl_indexes { + if rtl_index < new_text.len() { + let new_index = new_text.len().saturating_sub(rtl_index); + if new_index > 0 { + new_text.insert(new_index, SEPARATOR); + } + } + } + + // Add in additional separators if necessary. + if new_text.len() > selected_text.len() && !separator_rtl_indexes.is_empty() { + let spacing = match separator_rtl_indexes.as_slice() { + [.., b, a] => a - b - 1, + _ => separator_rtl_indexes[0], + }; + + let prefix_length = if radix == 10 { 0 } else { 2 }; + if let Some(mut index) = new_text.find(SEPARATOR) { + while index - prefix_length > spacing { + index -= spacing; + new_text.insert(index, SEPARATOR); + } + } + } + + Some(new_text) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_increment_basic_decimal_numbers() { + let tests = [ + ("100", 1, "101"), + ("100", -1, "99"), + ("99", 1, "100"), + ("100", 1000, "1100"), + ("100", -1000, "-900"), + ("-1", 1, "0"), + ("-1", 2, "1"), + ("1", -1, "0"), + ("1", -2, "-1"), + ]; + + for (original, amount, expected) in tests { + assert_eq!(increment(original, amount).unwrap(), expected); + } + } + + #[test] + fn test_increment_basic_hexadecimal_numbers() { + let tests = [ + ("0x0100", 1, "0x0101"), + ("0x0100", -1, "0x00ff"), + ("0x0001", -1, "0x0000"), + ("0x0000", -1, "0x0000"), + ("0xffffffffffffffff", 1, "0x10000000000000000"), + ("0xffffffffffffffff", 2, "0x10000000000000001"), + ("0xffffffffffffffff", -1, "0xfffffffffffffffe"), + ("0xABCDEF1234567890", 1, "0xABCDEF1234567891"), + ("0xabcdef1234567890", 1, "0xabcdef1234567891"), + ]; + + for (original, amount, expected) in tests { + assert_eq!(increment(original, amount).unwrap(), expected); + } + } + + #[test] + fn test_increment_basic_octal_numbers() { + let tests = [ + ("0o0107", 1, "0o0110"), + ("0o0110", -1, "0o0107"), + ("0o0001", -1, "0o0000"), + ("0o7777", 1, "0o10000"), + ("0o1000", -1, "0o0777"), + ("0o0107", 10, "0o0121"), + ("0o0000", -1, "0o0000"), + ("0o1777777777777777777777", 1, "0o2000000000000000000000"), + ("0o1777777777777777777777", 2, "0o2000000000000000000001"), + ("0o1777777777777777777777", -1, "0o1777777777777777777776"), + ]; + + for (original, amount, expected) in tests { + assert_eq!(increment(original, amount).unwrap(), expected); + } + } + + #[test] + fn test_increment_basic_binary_numbers() { + let tests = [ + ("0b00000100", 1, "0b00000101"), + ("0b00000100", -1, "0b00000011"), + ("0b00000100", 2, "0b00000110"), + ("0b00000100", -2, "0b00000010"), + ("0b00000001", -1, "0b00000000"), + ("0b00111111", 10, "0b01001001"), + ("0b11111111", 1, "0b100000000"), + ("0b10000000", -1, "0b01111111"), + ("0b0000", -1, "0b0000"), + ( + "0b1111111111111111111111111111111111111111111111111111111111111111", + 1, + "0b10000000000000000000000000000000000000000000000000000000000000000", + ), + ( + "0b1111111111111111111111111111111111111111111111111111111111111111", + 2, + "0b10000000000000000000000000000000000000000000000000000000000000001", + ), + ( + "0b1111111111111111111111111111111111111111111111111111111111111111", + -1, + "0b1111111111111111111111111111111111111111111111111111111111111110", + ), + ]; + + for (original, amount, expected) in tests { + assert_eq!(increment(original, amount).unwrap(), expected); + } + } + + #[test] + fn test_increment_with_separators() { + let tests = [ + ("999_999", 1, "1_000_000"), + ("1_000_000", -1, "999_999"), + ("-999_999", -1, "-1_000_000"), + ("0x0000_0000_0001", 0x1_ffff_0000, "0x0001_ffff_0001"), + ("0x0000_0000", -1, "0x0000_0000"), + ("0x0000_0000_0000", -1, "0x0000_0000_0000"), + ("0b01111111_11111111", 1, "0b10000000_00000000"), + ("0b11111111_11111111", 1, "0b1_00000000_00000000"), + ]; + + for (original, amount, expected) in tests { + assert_eq!(increment(original, amount).unwrap(), expected); + } + } + + #[test] + fn test_leading_and_trailing_separators_arent_a_match() { + assert_eq!(increment("9_", 1), None); + assert_eq!(increment("_9", 1), None); + assert_eq!(increment("_9_", 1), None); + } +} |