From d6b2d001d4fba266003e3646a8a38ce6146721d2 Mon Sep 17 00:00:00 2001 From: Jason Rodney Hansen Date: Sun, 7 Nov 2021 20:41:50 -0700 Subject: [PATCH 01/18] Add command to inc/dec number under cursor With the cursor over a number in normal mode, Ctrl + A will increment the number and Ctrl + X will decrement the number. It works with binary, octal, decimal, and hexidecimal numbers. Here are some examples. 0b01110100 0o1734 -24234 0x1F245 If the number isn't over a number it will try to find a number after the cursor on the same line. --- book/src/keymap.md | 2 + helix-term/src/commands.rs | 239 +++++++++++++++++++++++++++++++++++++ helix-term/src/keymap.rs | 3 + 3 files changed, 244 insertions(+) diff --git a/book/src/keymap.md b/book/src/keymap.md index 5a6aee411001..d5754a69b347 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -67,6 +67,8 @@ | `=` | Format selection | `format_selections` | | `d` | Delete selection | `delete_selection` | | `c` | Change selection (delete and enter insert mode) | `change_selection` | +| `Ctrl-a` | Increment number under cursor | `increment_number` | +| `Ctrl-x` | Decrement number under cursor | `decrement_number` | #### Shell diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 245fbe4ee44b..6fb4bc2f655b 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -340,6 +340,8 @@ impl Command { shell_keep_pipe, "Filter selections with shell predicate", suspend, "Suspend", rename_symbol, "Rename symbol", + increment_number, "Increment number under cursor", + decrement_number, "Decrement number under cursor", ); } @@ -5100,3 +5102,240 @@ fn rename_symbol(cx: &mut Context) { ); cx.push_layer(Box::new(prompt)); } + +/// If there is a number at the cursor, increment it by the count. +fn increment_number(cx: &mut Context) { + increment_number_impl(cx, cx.count() as i64); +} + +/// If there is a number at the cursor, decrement it by the count. +fn decrement_number(cx: &mut Context) { + increment_number_impl(cx, -(cx.count() as i64)); +} + +/// If there is a number at the cursor, increment it by `amount`. +fn increment_number_impl(cx: &mut Context, amount: i64) { + let (view, doc) = current!(cx.editor); + + let selection = doc.selection(view.id); + if selection.len() != 1 { + return; + } + + let primary_selection = selection.primary(); + if primary_selection.to() - primary_selection.from() != 1 { + return; + } + + let text = doc.text(); + let cursor = primary_selection.cursor(text.slice(..)); + + if let Some(NumberInfo { + start, + end, + value: old_value, + radix, + }) = number_at(text, cursor) + { + let number_text: Cow = text.slice(start..end).into(); + let number_text = number_text.strip_prefix('-').unwrap_or(&number_text); + + let new_value = old_value.wrapping_add(amount); + let old_length = end - start; + + let (replacement, new_length) = { + let mut replacement = match radix { + 2 => format!("{:b}", new_value), + 8 => format!("{:o}", new_value), + 10 => format!("{}", new_value.abs()), + 16 => { + let lower_count = number_text.chars().filter(char::is_ascii_lowercase).count(); + let upper_count = number_text.chars().filter(char::is_ascii_uppercase).count(); + if upper_count > lower_count { + format!("{:X}", new_value) + } else { + format!("{:x}", new_value) + } + } + _ => unimplemented!("radix not supported: {}", radix), + }; + + let mut new_length = replacement.chars().count(); + + let old_length_no_sign = number_text.chars().count(); + if new_length < old_length_no_sign && (radix != 10 || number_text.starts_with('0')) { + replacement = "0".repeat(old_length_no_sign - new_length) + &replacement; + new_length = old_length_no_sign; + } + + if radix == 10 && new_value.is_negative() { + replacement = format!("-{}", replacement); + new_length += 1; + } + + (replacement, new_length) + }; + + let changes = std::iter::once((start, end, Some(replacement.into()))); + let mut transaction = Transaction::change(text, changes); + + // Move cursor to the last character of the number. + let mut selection = doc.selection(view.id).clone(); + let mut primary = selection.primary_mut(); + primary.anchor = if new_length < old_length { + end - 1 - old_length + new_length + } else { + end - 1 + new_length - old_length + }; + primary.head = primary.anchor; + transaction = transaction.with_selection(selection); + + doc.apply(&transaction, view.id); + doc.append_changes_to_history(view.id); + } +} + +struct NumberInfo { + start: usize, + end: usize, + value: i64, + radix: u32, +} + +/// If there is a number at `char_idx`, return the text range, value and radix. +fn number_at(text: &Rope, char_idx: usize) -> Option { + let line = text.char_to_line(char_idx); + let line_start = text.line_to_char(line); + let line = text.line(line); + let line_len = line.len_chars(); + + let mut pos = char_idx - line_start; + let mut range_and_radix = None; + + // Search from the cursor until a number is found or we reach the end of the line. + while range_and_radix.is_none() && pos < line_len { + pos += line.chars_at(pos).take_while(|c| !c.is_digit(16)).count(); + + range_and_radix = if let Some((start, end)) = hex_number_range(&line, pos) { + Some((start, end, 16)) + } else if let Some((start, end)) = octal_number_range(&line, pos) { + Some((start, end, 8)) + } else if let Some((start, end)) = binary_number_range(&line, pos) { + Some((start, end, 2)) + } else if let Some((start, end)) = decimal_number_range(&line, pos) { + // We don't want to treat the '0' of the prefixes "0x", "0o", and "0b" as a number itself, so check for that here. + if end - start == 1 + && line.char(start) == '0' + && start + 2 < line_len + && ((line.char(start + 1) == 'x' && line.char(start + 2).is_digit(16)) + || (line.char(start + 1) == 'o' && line.char(start + 2).is_digit(8)) + || (line.char(start + 1) == 'b' && line.char(start + 2).is_digit(2))) + { + pos += 2; + None + } else { + Some((start, end, 10)) + } + } else { + pos += 1; + None + }; + } + + if let Some((start, end, radix)) = range_and_radix { + let number_text: Cow = line.slice(start..end).into(); + let value = i128::from_str_radix(&number_text, radix).ok()?; + if (value.is_positive() && value.leading_zeros() < 64) + || (value.is_negative() && value.leading_ones() < 64) + { + return None; + } + let value = value as i64; + Some(NumberInfo { + start: line_start + start, + end: line_start + end, + value, + radix, + }) + } else { + None + } +} + +/// Return the start and end of the decimal number at `pos` if there is one. +fn decimal_number_range(text: &RopeSlice, pos: usize) -> Option<(usize, usize)> { + if pos >= text.len_chars() { + return None; + } + let pos = pos + 1; + let mut chars = text.chars_at(pos); + chars.reverse(); + let decimal_start = pos - chars.take_while(|c| c.is_digit(10)).count(); + + if decimal_start < pos { + let decimal_end = decimal_start + + text + .chars_at(decimal_start) + .take_while(|c| c.is_digit(10)) + .count(); + + // Handle negative numbers + if decimal_start > 0 && text.char(decimal_start - 1) == '-' { + Some((decimal_start - 1, decimal_end)) + } else { + Some((decimal_start, decimal_end)) + } + } else { + None + } +} + +/// Return the start and end of the hexidecimal number at `pos` if there is one. +/// Hexidecimal numbers must be prefixed with "0x". The prefix will not be included in the range. +fn hex_number_range(text: &RopeSlice, pos: usize) -> Option<(usize, usize)> { + prefixed_number_range(text, pos, 16, 'x') +} + +/// Return the start and end of the octal number at `pos` if there is one. +/// Octal numbers must be prefixed with "0o". The prefix will not be included in the range. +fn octal_number_range(text: &RopeSlice, pos: usize) -> Option<(usize, usize)> { + prefixed_number_range(text, pos, 8, 'o') +} + +/// Return the start and end of the binary number at `pos` if there is one. +/// Binary numbers must be prefixed with "0b". The prefix will not be included in the range. +fn binary_number_range(text: &RopeSlice, pos: usize) -> Option<(usize, usize)> { + prefixed_number_range(text, pos, 2, 'b') +} + +/// Return the start and end of the number at `pos` if there is one with the given `radix` and `prefix_char`. +/// The number must be prefixed with `'0' + prefix_char`. The prefix will not be included in the range. +fn prefixed_number_range( + text: &RopeSlice, + pos: usize, + radix: u32, + prefix_char: char, +) -> Option<(usize, usize)> { + if pos >= text.len_chars() { + return None; + } + let pos = pos + 1; + let mut chars = text.chars_at(pos); + chars.reverse(); + let start = pos - chars.take_while(|c| c.is_digit(radix)).count(); + let is_num = start < pos + && start >= 2 + && text.char(start - 2) == '0' + && text.char(start - 1) == prefix_char; + + if is_num { + let end = start + + text + .chars_at(start) + .take_while(|c| c.is_digit(radix)) + .count(); + Some((start, end)) + } else { + None + } +} diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index d497401f99f9..1887e99da4f0 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -618,6 +618,9 @@ impl Default for Keymaps { "A-!" => shell_append_output, "$" => shell_keep_pipe, "C-z" => suspend, + + "C-a" => increment_number, + "C-x" => decrement_number, }); let mut select = normal.clone(); select.merge_nodes(keymap!({ "Select mode" From 2bd252f5357f2de980575b5a5a0c84fb52ed2aa0 Mon Sep 17 00:00:00 2001 From: Jason Rodney Hansen Date: Wed, 10 Nov 2021 14:07:23 -0700 Subject: [PATCH 02/18] Move several functions to helix-core --- helix-core/src/lib.rs | 1 + helix-core/src/numbers.rs | 144 ++++++++++++++++++++++++++++++++++++ helix-term/src/commands.rs | 146 +------------------------------------ 3 files changed, 146 insertions(+), 145 deletions(-) create mode 100644 helix-core/src/numbers.rs diff --git a/helix-core/src/lib.rs b/helix-core/src/lib.rs index 6168d02c2cb4..7dd3b32607c5 100644 --- a/helix-core/src/lib.rs +++ b/helix-core/src/lib.rs @@ -10,6 +10,7 @@ pub mod line_ending; pub mod macros; pub mod match_brackets; pub mod movement; +pub mod numbers; pub mod object; pub mod path; mod position; diff --git a/helix-core/src/numbers.rs b/helix-core/src/numbers.rs new file mode 100644 index 000000000000..7beac7192f01 --- /dev/null +++ b/helix-core/src/numbers.rs @@ -0,0 +1,144 @@ +use std::borrow::Cow; + +use ropey::{Rope, RopeSlice}; + +pub struct NumberInfo { + pub start: usize, + pub end: usize, + pub value: i64, + pub radix: u32, +} + +/// If there is a number at `char_idx`, return the text range, value and radix. +pub fn number_at(text: &Rope, char_idx: usize) -> Option { + let line = text.char_to_line(char_idx); + let line_start = text.line_to_char(line); + let line = text.line(line); + let line_len = line.len_chars(); + + let mut pos = char_idx - line_start; + let mut range_and_radix = None; + + // Search from the cursor until a number is found or we reach the end of the line. + while range_and_radix.is_none() && pos < line_len { + pos += line.chars_at(pos).take_while(|c| !c.is_digit(16)).count(); + + range_and_radix = if let Some((start, end)) = hex_number_range(&line, pos) { + Some((start, end, 16)) + } else if let Some((start, end)) = octal_number_range(&line, pos) { + Some((start, end, 8)) + } else if let Some((start, end)) = binary_number_range(&line, pos) { + Some((start, end, 2)) + } else if let Some((start, end)) = decimal_number_range(&line, pos) { + // We don't want to treat the '0' of the prefixes "0x", "0o", and "0b" as a number itself, so check for that here. + if end - start == 1 && line.char(start) == '0' && start + 2 < line_len { + let (c1, c2) = (line.char(start + 1), line.char(start + 2)); + if c1 == 'x' && c2.is_digit(16) + || c1 == 'o' && c2.is_digit(8) + || c1 == 'b' && c2.is_digit(2) + { + pos += 2; + continue; + } + } + + Some((start, end, 10)) + } else { + pos += 1; + None + }; + } + + if let Some((start, end, radix)) = range_and_radix { + let number_text: Cow = line.slice(start..end).into(); + let value = i128::from_str_radix(&number_text, radix).ok()?; + if (value.is_positive() && value.leading_zeros() < 64) + || (value.is_negative() && value.leading_ones() < 64) + { + return None; + } + let value = value as i64; + Some(NumberInfo { + start: line_start + start, + end: line_start + end, + value, + radix, + }) + } else { + None + } +} + +/// Return the start and end of the decimal number at `pos` if there is one. +fn decimal_number_range(text: &RopeSlice, pos: usize) -> Option<(usize, usize)> { + if pos >= text.len_chars() { + return None; + } + let pos = pos + 1; + let mut chars = text.chars_at(pos); + chars.reverse(); + let decimal_start = pos - chars.take_while(|c| c.is_digit(10)).count(); + + if decimal_start < pos { + let decimal_end = decimal_start + + text + .chars_at(decimal_start) + .take_while(|c| c.is_digit(10)) + .count(); + + // Handle negative numbers + if decimal_start > 0 && text.char(decimal_start - 1) == '-' { + Some((decimal_start - 1, decimal_end)) + } else { + Some((decimal_start, decimal_end)) + } + } else { + None + } +} + +/// Return the start and end of the hexidecimal number at `pos` if there is one. +/// Hexidecimal numbers must be prefixed with "0x". The prefix will not be included in the range. +fn hex_number_range(text: &RopeSlice, pos: usize) -> Option<(usize, usize)> { + prefixed_number_range(text, pos, 16, 'x') +} + +/// Return the start and end of the octal number at `pos` if there is one. +/// Octal numbers must be prefixed with "0o". The prefix will not be included in the range. +fn octal_number_range(text: &RopeSlice, pos: usize) -> Option<(usize, usize)> { + prefixed_number_range(text, pos, 8, 'o') +} + +/// Return the start and end of the binary number at `pos` if there is one. +/// Binary numbers must be prefixed with "0b". The prefix will not be included in the range. +fn binary_number_range(text: &RopeSlice, pos: usize) -> Option<(usize, usize)> { + prefixed_number_range(text, pos, 2, 'b') +} + +/// Return the start and end of the number at `pos` if there is one with the given `radix` and `prefix_char`. +/// The number must be prefixed with `'0' + prefix_char`. The prefix will not be included in the range. +fn prefixed_number_range( + text: &RopeSlice, + pos: usize, + radix: u32, + prefix_char: char, +) -> Option<(usize, usize)> { + if pos >= text.len_chars() { + return None; + } + let pos = pos + 1; + let mut chars = text.chars_at(pos); + chars.reverse(); + let start = pos - chars.take_while(|c| c.is_digit(radix)).count(); + let is_num = start < pos + && start >= 2 + && text.char(start - 2) == '0' + && text.char(start - 1) == prefix_char; + + if is_num { + let end = pos + text.chars_at(pos).take_while(|c| c.is_digit(radix)).count(); + Some((start, end)) + } else { + None + } +} diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 6fb4bc2f655b..b03bf84047c7 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -4,6 +4,7 @@ use helix_core::{ line_ending::{get_line_ending_of_str, line_end_char_index, str_is_line_ending}, match_brackets, movement::{self, Direction}, + numbers::{number_at, NumberInfo}, object, pos_at_coords, regex::{self, Regex, RegexBuilder}, register::Register, @@ -5194,148 +5195,3 @@ fn increment_number_impl(cx: &mut Context, amount: i64) { doc.append_changes_to_history(view.id); } } - -struct NumberInfo { - start: usize, - end: usize, - value: i64, - radix: u32, -} - -/// If there is a number at `char_idx`, return the text range, value and radix. -fn number_at(text: &Rope, char_idx: usize) -> Option { - let line = text.char_to_line(char_idx); - let line_start = text.line_to_char(line); - let line = text.line(line); - let line_len = line.len_chars(); - - let mut pos = char_idx - line_start; - let mut range_and_radix = None; - - // Search from the cursor until a number is found or we reach the end of the line. - while range_and_radix.is_none() && pos < line_len { - pos += line.chars_at(pos).take_while(|c| !c.is_digit(16)).count(); - - range_and_radix = if let Some((start, end)) = hex_number_range(&line, pos) { - Some((start, end, 16)) - } else if let Some((start, end)) = octal_number_range(&line, pos) { - Some((start, end, 8)) - } else if let Some((start, end)) = binary_number_range(&line, pos) { - Some((start, end, 2)) - } else if let Some((start, end)) = decimal_number_range(&line, pos) { - // We don't want to treat the '0' of the prefixes "0x", "0o", and "0b" as a number itself, so check for that here. - if end - start == 1 - && line.char(start) == '0' - && start + 2 < line_len - && ((line.char(start + 1) == 'x' && line.char(start + 2).is_digit(16)) - || (line.char(start + 1) == 'o' && line.char(start + 2).is_digit(8)) - || (line.char(start + 1) == 'b' && line.char(start + 2).is_digit(2))) - { - pos += 2; - None - } else { - Some((start, end, 10)) - } - } else { - pos += 1; - None - }; - } - - if let Some((start, end, radix)) = range_and_radix { - let number_text: Cow = line.slice(start..end).into(); - let value = i128::from_str_radix(&number_text, radix).ok()?; - if (value.is_positive() && value.leading_zeros() < 64) - || (value.is_negative() && value.leading_ones() < 64) - { - return None; - } - let value = value as i64; - Some(NumberInfo { - start: line_start + start, - end: line_start + end, - value, - radix, - }) - } else { - None - } -} - -/// Return the start and end of the decimal number at `pos` if there is one. -fn decimal_number_range(text: &RopeSlice, pos: usize) -> Option<(usize, usize)> { - if pos >= text.len_chars() { - return None; - } - let pos = pos + 1; - let mut chars = text.chars_at(pos); - chars.reverse(); - let decimal_start = pos - chars.take_while(|c| c.is_digit(10)).count(); - - if decimal_start < pos { - let decimal_end = decimal_start - + text - .chars_at(decimal_start) - .take_while(|c| c.is_digit(10)) - .count(); - - // Handle negative numbers - if decimal_start > 0 && text.char(decimal_start - 1) == '-' { - Some((decimal_start - 1, decimal_end)) - } else { - Some((decimal_start, decimal_end)) - } - } else { - None - } -} - -/// Return the start and end of the hexidecimal number at `pos` if there is one. -/// Hexidecimal numbers must be prefixed with "0x". The prefix will not be included in the range. -fn hex_number_range(text: &RopeSlice, pos: usize) -> Option<(usize, usize)> { - prefixed_number_range(text, pos, 16, 'x') -} - -/// Return the start and end of the octal number at `pos` if there is one. -/// Octal numbers must be prefixed with "0o". The prefix will not be included in the range. -fn octal_number_range(text: &RopeSlice, pos: usize) -> Option<(usize, usize)> { - prefixed_number_range(text, pos, 8, 'o') -} - -/// Return the start and end of the binary number at `pos` if there is one. -/// Binary numbers must be prefixed with "0b". The prefix will not be included in the range. -fn binary_number_range(text: &RopeSlice, pos: usize) -> Option<(usize, usize)> { - prefixed_number_range(text, pos, 2, 'b') -} - -/// Return the start and end of the number at `pos` if there is one with the given `radix` and `prefix_char`. -/// The number must be prefixed with `'0' + prefix_char`. The prefix will not be included in the range. -fn prefixed_number_range( - text: &RopeSlice, - pos: usize, - radix: u32, - prefix_char: char, -) -> Option<(usize, usize)> { - if pos >= text.len_chars() { - return None; - } - let pos = pos + 1; - let mut chars = text.chars_at(pos); - chars.reverse(); - let start = pos - chars.take_while(|c| c.is_digit(radix)).count(); - let is_num = start < pos - && start >= 2 - && text.char(start - 2) == '0' - && text.char(start - 1) == prefix_char; - - if is_num { - let end = start - + text - .chars_at(start) - .take_while(|c| c.is_digit(radix)) - .count(); - Some((start, end)) - } else { - None - } -} From f78bb73ae39f744918c3a67e6ee5e9220932ff67 Mon Sep 17 00:00:00 2001 From: Jason Rodney Hansen Date: Wed, 10 Nov 2021 18:08:01 -0700 Subject: [PATCH 03/18] Change to work based on word under selection * It no longer finds the next number if the cursor isn't already over a number. * It only matches numbers that are part of words with other characters like "foo123bar". * It now works with multiple selections. --- helix-core/src/numbers.rs | 159 +++++++------------------------------ helix-term/src/commands.rs | 102 ++++++++++++------------ 2 files changed, 80 insertions(+), 181 deletions(-) diff --git a/helix-core/src/numbers.rs b/helix-core/src/numbers.rs index 7beac7192f01..05ed79e60477 100644 --- a/helix-core/src/numbers.rs +++ b/helix-core/src/numbers.rs @@ -1,144 +1,45 @@ use std::borrow::Cow; -use ropey::{Rope, RopeSlice}; +use ropey::RopeSlice; + +use crate::{ + textobject::{textobject_word, TextObject}, + Range, +}; pub struct NumberInfo { - pub start: usize, - pub end: usize, + pub range: Range, pub value: i64, pub radix: u32, } -/// If there is a number at `char_idx`, return the text range, value and radix. -pub fn number_at(text: &Rope, char_idx: usize) -> Option { - let line = text.char_to_line(char_idx); - let line_start = text.line_to_char(line); - let line = text.line(line); - let line_len = line.len_chars(); - - let mut pos = char_idx - line_start; - let mut range_and_radix = None; - - // Search from the cursor until a number is found or we reach the end of the line. - while range_and_radix.is_none() && pos < line_len { - pos += line.chars_at(pos).take_while(|c| !c.is_digit(16)).count(); - - range_and_radix = if let Some((start, end)) = hex_number_range(&line, pos) { - Some((start, end, 16)) - } else if let Some((start, end)) = octal_number_range(&line, pos) { - Some((start, end, 8)) - } else if let Some((start, end)) = binary_number_range(&line, pos) { - Some((start, end, 2)) - } else if let Some((start, end)) = decimal_number_range(&line, pos) { - // We don't want to treat the '0' of the prefixes "0x", "0o", and "0b" as a number itself, so check for that here. - if end - start == 1 && line.char(start) == '0' && start + 2 < line_len { - let (c1, c2) = (line.char(start + 1), line.char(start + 2)); - if c1 == 'x' && c2.is_digit(16) - || c1 == 'o' && c2.is_digit(8) - || c1 == 'b' && c2.is_digit(2) - { - pos += 2; - continue; - } - } - - Some((start, end, 10)) - } else { - pos += 1; - None - }; - } - - if let Some((start, end, radix)) = range_and_radix { - let number_text: Cow = line.slice(start..end).into(); - let value = i128::from_str_radix(&number_text, radix).ok()?; - if (value.is_positive() && value.leading_zeros() < 64) - || (value.is_negative() && value.leading_ones() < 64) - { - return None; - } - let value = value as i64; - Some(NumberInfo { - start: line_start + start, - end: line_start + end, - value, - radix, - }) - } else { - None - } -} - -/// Return the start and end of the decimal number at `pos` if there is one. -fn decimal_number_range(text: &RopeSlice, pos: usize) -> Option<(usize, usize)> { - if pos >= text.len_chars() { - return None; - } - let pos = pos + 1; - let mut chars = text.chars_at(pos); - chars.reverse(); - let decimal_start = pos - chars.take_while(|c| c.is_digit(10)).count(); - - if decimal_start < pos { - let decimal_end = decimal_start - + text - .chars_at(decimal_start) - .take_while(|c| c.is_digit(10)) - .count(); - - // Handle negative numbers - if decimal_start > 0 && text.char(decimal_start - 1) == '-' { - Some((decimal_start - 1, decimal_end)) - } else { - Some((decimal_start, decimal_end)) - } +/// Return information about number under cursor if there is one. +pub fn number_at(text: RopeSlice, range: Range) -> Option { + let word_range = textobject_word(text, range, TextObject::Inside, 1, true); + let word: Cow = text.slice(word_range.from()..word_range.to()).into(); + let (radix, prefixed) = if word.starts_with("0x") { + (16, true) + } else if word.starts_with("0o") { + (8, true) + } else if word.starts_with("0b") { + (2, true) } else { - None - } -} - -/// Return the start and end of the hexidecimal number at `pos` if there is one. -/// Hexidecimal numbers must be prefixed with "0x". The prefix will not be included in the range. -fn hex_number_range(text: &RopeSlice, pos: usize) -> Option<(usize, usize)> { - prefixed_number_range(text, pos, 16, 'x') -} - -/// Return the start and end of the octal number at `pos` if there is one. -/// Octal numbers must be prefixed with "0o". The prefix will not be included in the range. -fn octal_number_range(text: &RopeSlice, pos: usize) -> Option<(usize, usize)> { - prefixed_number_range(text, pos, 8, 'o') -} + (10, false) + }; -/// Return the start and end of the binary number at `pos` if there is one. -/// Binary numbers must be prefixed with "0b". The prefix will not be included in the range. -fn binary_number_range(text: &RopeSlice, pos: usize) -> Option<(usize, usize)> { - prefixed_number_range(text, pos, 2, 'b') -} + let number = if prefixed { &word[2..] } else { &word }; -/// Return the start and end of the number at `pos` if there is one with the given `radix` and `prefix_char`. -/// The number must be prefixed with `'0' + prefix_char`. The prefix will not be included in the range. -fn prefixed_number_range( - text: &RopeSlice, - pos: usize, - radix: u32, - prefix_char: char, -) -> Option<(usize, usize)> { - if pos >= text.len_chars() { + let value = i128::from_str_radix(&number, radix).ok()?; + if (value.is_positive() && value.leading_zeros() < 64) + || (value.is_negative() && value.leading_ones() < 64) + { return None; } - let pos = pos + 1; - let mut chars = text.chars_at(pos); - chars.reverse(); - let start = pos - chars.take_while(|c| c.is_digit(radix)).count(); - let is_num = start < pos - && start >= 2 - && text.char(start - 2) == '0' - && text.char(start - 1) == prefix_char; - if is_num { - let end = pos + text.chars_at(pos).take_while(|c| c.is_digit(radix)).count(); - Some((start, end)) - } else { - None - } + let value = value as i64; + Some(NumberInfo { + range: word_range, + value, + radix, + }) } diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index b03bf84047c7..048fad4225ad 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -5117,41 +5117,35 @@ fn decrement_number(cx: &mut Context) { /// If there is a number at the cursor, increment it by `amount`. fn increment_number_impl(cx: &mut Context, amount: i64) { let (view, doc) = current!(cx.editor); - let selection = doc.selection(view.id); - if selection.len() != 1 { - return; - } - - let primary_selection = selection.primary(); - if primary_selection.to() - primary_selection.from() != 1 { - return; - } - let text = doc.text(); - let cursor = primary_selection.cursor(text.slice(..)); - - if let Some(NumberInfo { - start, - end, - value: old_value, - radix, - }) = number_at(text, cursor) - { - let number_text: Cow = text.slice(start..end).into(); - let number_text = number_text.strip_prefix('-').unwrap_or(&number_text); - let new_value = old_value.wrapping_add(amount); - let old_length = end - start; + let changes = selection.ranges().iter().filter_map(|range| { + if let Some(NumberInfo { + range, + value: old_value, + radix, + }) = number_at(text.slice(..), *range) + { + let new_value = old_value.wrapping_add(amount); + let old_text: Cow = text.slice(range.from()..range.to()).into(); + let prefix = if radix == 10 { "" } else { &old_text[..2] }; - let (replacement, new_length) = { - let mut replacement = match radix { + let mut new_text = match radix { 2 => format!("{:b}", new_value), 8 => format!("{:o}", new_value), 10 => format!("{}", new_value.abs()), 16 => { - let lower_count = number_text.chars().filter(char::is_ascii_lowercase).count(); - let upper_count = number_text.chars().filter(char::is_ascii_uppercase).count(); + let lower_count = old_text + .chars() + .skip(2) + .filter(char::is_ascii_lowercase) + .count(); + let upper_count = old_text + .chars() + .skip(2) + .filter(char::is_ascii_uppercase) + .count(); if upper_count > lower_count { format!("{:X}", new_value) } else { @@ -5161,37 +5155,41 @@ fn increment_number_impl(cx: &mut Context, amount: i64) { _ => unimplemented!("radix not supported: {}", radix), }; - let mut new_length = replacement.chars().count(); - - let old_length_no_sign = number_text.chars().count(); - if new_length < old_length_no_sign && (radix != 10 || number_text.starts_with('0')) { - replacement = "0".repeat(old_length_no_sign - new_length) + &replacement; - new_length = old_length_no_sign; + // Pad with leading zeros if necessary. + // * For non-decimal numbers, we want to keep at least as many digits as before the change. + // * For decimal numbers we do this if there are leading zeros, like 000145, before the change. + let new_length_no_prefix = new_text.len(); + let old_text_no_prefix = { + let mut stripped: &str = old_text[prefix.len()..].as_ref(); + if stripped.starts_with('-') { + stripped = &stripped[1..]; + } + stripped + }; + let old_length_no_prefix = old_text_no_prefix.len(); + if new_length_no_prefix < old_length_no_prefix + && (radix != 10 || old_text_no_prefix.starts_with('0')) + { + new_text = "0".repeat(old_length_no_prefix - new_length_no_prefix) + &new_text; } + // Add prefix or sign if needed if radix == 10 && new_value.is_negative() { - replacement = format!("-{}", replacement); - new_length += 1; + new_text = format!("-{}", new_text); + } else { + new_text = prefix.to_owned() + &new_text; } - (replacement, new_length) - }; - - let changes = std::iter::once((start, end, Some(replacement.into()))); - let mut transaction = Transaction::change(text, changes); + let new_text: Tendril = new_text.into(); - // Move cursor to the last character of the number. - let mut selection = doc.selection(view.id).clone(); - let mut primary = selection.primary_mut(); - primary.anchor = if new_length < old_length { - end - 1 - old_length + new_length + Some((range.from(), range.to(), Some(new_text))) } else { - end - 1 + new_length - old_length - }; - primary.head = primary.anchor; - transaction = transaction.with_selection(selection); + None + } + }); - doc.apply(&transaction, view.id); - doc.append_changes_to_history(view.id); - } + let transaction = Transaction::change(doc.text(), changes); + + doc.apply(&transaction, view.id); + doc.append_changes_to_history(view.id); } From 6484ff9b9bbc7d6bcc6f7354f1ea8af5bc6ca48d Mon Sep 17 00:00:00 2001 From: Jason Rodney Hansen Date: Wed, 10 Nov 2021 18:57:21 -0700 Subject: [PATCH 04/18] Add some unit tests --- helix-core/src/numbers.rs | 119 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) diff --git a/helix-core/src/numbers.rs b/helix-core/src/numbers.rs index 05ed79e60477..cb9d01949149 100644 --- a/helix-core/src/numbers.rs +++ b/helix-core/src/numbers.rs @@ -7,6 +7,7 @@ use crate::{ Range, }; +#[derive(Debug, PartialEq, Eq)] pub struct NumberInfo { pub range: Range, pub value: i64, @@ -43,3 +44,121 @@ pub fn number_at(text: RopeSlice, range: Range) -> Option { radix, }) } + +#[cfg(test)] +mod test { + use super::*; + use crate::Rope; + + #[test] + fn test_decimal_at_point() { + let rope = Rope::from_str("Test text 12345 more text."); + let range = Range::point(12); + assert_eq!( + number_at(rope.slice(..), range), + Some(NumberInfo { + range: Range::new(10, 15), + value: 12345, + radix: 10, + }) + ); + } + + #[test] + fn test_uppercase_hexadecimal_at_point() { + let rope = Rope::from_str("Test text 0x123ABCDEF more text."); + let range = Range::point(12); + assert_eq!( + number_at(rope.slice(..), range), + Some(NumberInfo { + range: Range::new(10, 21), + value: 0x123ABCDEF, + radix: 16, + }) + ); + } + + #[test] + fn test_lowercase_hexadecimal_at_point() { + let rope = Rope::from_str("Test text 0xfa3b4e more text."); + let range = Range::point(12); + assert_eq!( + number_at(rope.slice(..), range), + Some(NumberInfo { + range: Range::new(10, 18), + value: 0xfa3b4e, + radix: 16, + }) + ); + } + + #[test] + fn test_octal_at_point() { + let rope = Rope::from_str("Test text 0o1074312 more text."); + let range = Range::point(12); + assert_eq!( + number_at(rope.slice(..), range), + Some(NumberInfo { + range: Range::new(10, 19), + value: 0o1074312, + radix: 8, + }) + ); + } + + #[test] + fn test_binary_at_point() { + let rope = Rope::from_str("Test text 0b10111010010101 more text."); + let range = Range::point(12); + assert_eq!( + number_at(rope.slice(..), range), + Some(NumberInfo { + range: Range::new(10, 26), + value: 0b10111010010101, + radix: 2, + }) + ); + } + + #[test] + fn test_negative_decimal_at_point() { + let rope = Rope::from_str("Test text -54321 more text."); + let range = Range::point(12); + assert_eq!( + number_at(rope.slice(..), range), + Some(NumberInfo { + range: Range::new(10, 16), + value: -54321, + radix: 10, + }) + ); + } + + #[test] + fn test_decimal_with_leading_zeroes_at_point() { + let rope = Rope::from_str("Test text 000045326 more text."); + let range = Range::point(12); + assert_eq!( + number_at(rope.slice(..), range), + Some(NumberInfo { + range: Range::new(10, 19), + value: 45326, + radix: 10, + }) + ); + } + + #[test] + fn test_not_a_number_point() { + let rope = Rope::from_str("Test text 45326 more text."); + let range = Range::point(6); + assert_eq!(number_at(rope.slice(..), range), None); + } + + #[test] + fn test_number_too_large_at_point() { + let rope = Rope::from_str("Test text 0xFFFFFFFFFFFFFFFFF more text."); + let range = Range::point(12); + assert_eq!(number_at(rope.slice(..), range), None); + } +} From 82a65d51c12042a79b13bda7463d768680566da9 Mon Sep 17 00:00:00 2001 From: Jason Rodney Hansen Date: Wed, 10 Nov 2021 19:24:16 -0700 Subject: [PATCH 05/18] Fix for clippy --- helix-core/src/numbers.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helix-core/src/numbers.rs b/helix-core/src/numbers.rs index cb9d01949149..84907c0bde19 100644 --- a/helix-core/src/numbers.rs +++ b/helix-core/src/numbers.rs @@ -30,7 +30,7 @@ pub fn number_at(text: RopeSlice, range: Range) -> Option { let number = if prefixed { &word[2..] } else { &word }; - let value = i128::from_str_radix(&number, radix).ok()?; + let value = i128::from_str_radix(number, radix).ok()?; if (value.is_positive() && value.leading_zeros() < 64) || (value.is_negative() && value.leading_ones() < 64) { From e2571fb4bdd7cc62ed1e6acb9e4e5b691df07d91 Mon Sep 17 00:00:00 2001 From: Jason Rodney Hansen Date: Thu, 11 Nov 2021 18:49:42 -0700 Subject: [PATCH 06/18] Simplify some things --- helix-term/src/commands.rs | 66 +++++++++++++------------------------- 1 file changed, 23 insertions(+), 43 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 048fad4225ad..fd2aae5ae6bb 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -5127,59 +5127,39 @@ fn increment_number_impl(cx: &mut Context, amount: i64) { radix, }) = number_at(text.slice(..), *range) { - let new_value = old_value.wrapping_add(amount); let old_text: Cow = text.slice(range.from()..range.to()).into(); - let prefix = if radix == 10 { "" } else { &old_text[..2] }; + let new_value = old_value.wrapping_add(amount); + let new_length = if radix == 10 { + match (old_value.is_negative(), new_value.is_negative()) { + (true, false) => old_text.len() - 1, + (false, true) => old_text.len() + 1, + _ => old_text.len(), + } + } else { + old_text.len() - 2 + }; - let mut new_text = match radix { - 2 => format!("{:b}", new_value), - 8 => format!("{:o}", new_value), - 10 => format!("{}", new_value.abs()), + let new_text = match radix { + 2 => format!("0b{:01$b}", new_value, new_length), + 8 => format!("0o{:01$o}", new_value, new_length), + 10 => format!("{:01$}", new_value, new_length), 16 => { - let lower_count = old_text - .chars() - .skip(2) - .filter(char::is_ascii_lowercase) - .count(); - let upper_count = old_text - .chars() - .skip(2) - .filter(char::is_ascii_uppercase) - .count(); + let (lower_count, upper_count): (usize, usize) = + old_text.chars().skip(2).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!("{:X}", new_value) + format!("0x{:01$X}", new_value, new_length) } else { - format!("{:x}", new_value) + format!("0x{:01$x}", new_value, new_length) } } _ => unimplemented!("radix not supported: {}", radix), }; - // Pad with leading zeros if necessary. - // * For non-decimal numbers, we want to keep at least as many digits as before the change. - // * For decimal numbers we do this if there are leading zeros, like 000145, before the change. - let new_length_no_prefix = new_text.len(); - let old_text_no_prefix = { - let mut stripped: &str = old_text[prefix.len()..].as_ref(); - if stripped.starts_with('-') { - stripped = &stripped[1..]; - } - stripped - }; - let old_length_no_prefix = old_text_no_prefix.len(); - if new_length_no_prefix < old_length_no_prefix - && (radix != 10 || old_text_no_prefix.starts_with('0')) - { - new_text = "0".repeat(old_length_no_prefix - new_length_no_prefix) + &new_text; - } - - // Add prefix or sign if needed - if radix == 10 && new_value.is_negative() { - new_text = format!("-{}", new_text); - } else { - new_text = prefix.to_owned() + &new_text; - } - let new_text: Tendril = new_text.into(); Some((range.from(), range.to(), Some(new_text))) From f6c8fba79f68f8c9e229cf4ae283da5de92e9577 Mon Sep 17 00:00:00 2001 From: Jason Rodney Hansen Date: Fri, 12 Nov 2021 18:59:51 -0700 Subject: [PATCH 07/18] Keep previous selection after incrementing --- helix-term/src/commands.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index fd2aae5ae6bb..d602a85bf603 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -5168,8 +5168,11 @@ fn increment_number_impl(cx: &mut Context, amount: i64) { } }); - let transaction = Transaction::change(doc.text(), changes); + if changes.clone().count() > 0 { + let transaction = Transaction::change(doc.text(), changes); + let transaction = transaction.with_selection(selection.clone()); - doc.apply(&transaction, view.id); - doc.append_changes_to_history(view.id); + doc.apply(&transaction, view.id); + doc.append_changes_to_history(view.id); + } } From e4241806e3a2d89e74f4c9bcbd4c9faa2999d7d1 Mon Sep 17 00:00:00 2001 From: Jason Rodney Hansen Date: Fri, 12 Nov 2021 19:00:59 -0700 Subject: [PATCH 08/18] Use short word instead of long word This change requires us to manually handle minus sign. --- helix-core/src/numbers.rs | 95 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 92 insertions(+), 3 deletions(-) diff --git a/helix-core/src/numbers.rs b/helix-core/src/numbers.rs index 84907c0bde19..982835f0b8ed 100644 --- a/helix-core/src/numbers.rs +++ b/helix-core/src/numbers.rs @@ -16,8 +16,27 @@ pub struct NumberInfo { /// Return information about number under cursor if there is one. pub fn number_at(text: RopeSlice, range: Range) -> Option { - let word_range = textobject_word(text, range, TextObject::Inside, 1, true); - let word: Cow = text.slice(word_range.from()..word_range.to()).into(); + // If the cursor is on the minus sign of a number we want to get the word textobject to the + // right of it. + let range = if range.to() < text.len_chars() + && range.to() - range.from() <= 1 + && text.char(range.from()) == '-' + { + Range::new(range.from() + 1, range.to() + 1) + } else { + range + }; + + let range = textobject_word(text, range, TextObject::Inside, 1, false); + + // If there is a minus sign to the left of the word object, we want to include it in the range. + let range = if range.from() > 0 && text.char(range.from() - 1) == '-' { + range.extend(range.from() - 1, range.from()) + } else { + range + }; + + let word: Cow = text.slice(range.from()..range.to()).into(); let (radix, prefixed) = if word.starts_with("0x") { (16, true) } else if word.starts_with("0o") { @@ -39,7 +58,7 @@ pub fn number_at(text: RopeSlice, range: Range) -> Option { let value = value as i64; Some(NumberInfo { - range: word_range, + range, value, radix, }) @@ -148,6 +167,62 @@ mod test { ); } + #[test] + fn test_negative_decimal_cursor_on_minus_sign() { + let rope = Rope::from_str("Test text -54321 more text."); + let range = Range::point(10); + assert_eq!( + number_at(rope.slice(..), range), + Some(NumberInfo { + range: Range::new(10, 16), + value: -54321, + radix: 10, + }) + ); + } + + #[test] + fn test_number_at_start_of_rope() { + let rope = Rope::from_str("100"); + let range = Range::point(0); + assert_eq!( + number_at(rope.slice(..), range), + Some(NumberInfo { + range: Range::new(0, 3), + value: 100, + radix: 10, + }) + ); + } + + #[test] + fn test_number_at_end_of_rope() { + let rope = Rope::from_str("100"); + let range = Range::point(2); + assert_eq!( + number_at(rope.slice(..), range), + Some(NumberInfo { + range: Range::new(0, 3), + value: 100, + radix: 10, + }) + ); + } + + #[test] + fn test_number_surrounded_by_punctuation() { + let rope = Rope::from_str(",100;"); + let range = Range::point(1); + assert_eq!( + number_at(rope.slice(..), range), + Some(NumberInfo { + range: Range::new(1, 4), + value: 100, + radix: 10, + }) + ); + } + #[test] fn test_not_a_number_point() { let rope = Rope::from_str("Test text 45326 more text."); @@ -161,4 +236,18 @@ mod test { let range = Range::point(12); assert_eq!(number_at(rope.slice(..), range), None); } + + #[test] + fn test_number_cursor_one_right_of_number() { + let rope = Rope::from_str("100 "); + let range = Range::point(3); + assert_eq!(number_at(rope.slice(..), range), None); + } + + #[test] + fn test_number_cursor_one_left_of_number() { + let rope = Rope::from_str(" 100"); + let range = Range::point(0); + assert_eq!(number_at(rope.slice(..), range), None); + } } From f3d88cd291d9907ede6fec418dd101b58521eb0b Mon Sep 17 00:00:00 2001 From: Jason Rodney Hansen Date: Fri, 12 Nov 2021 19:27:02 -0700 Subject: [PATCH 09/18] Don't pad decimal numbers if no leading zeros --- helix-term/src/commands.rs | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index d602a85bf603..b7fc343e054a 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -5129,20 +5129,19 @@ fn increment_number_impl(cx: &mut Context, amount: i64) { { let old_text: Cow = text.slice(range.from()..range.to()).into(); let new_value = old_value.wrapping_add(amount); - let new_length = if radix == 10 { - match (old_value.is_negative(), new_value.is_negative()) { - (true, false) => old_text.len() - 1, - (false, true) => old_text.len() + 1, - _ => old_text.len(), - } - } else { - old_text.len() - 2 - }; let new_text = match radix { - 2 => format!("0b{:01$b}", new_value, new_length), - 8 => format!("0o{:01$o}", new_value, new_length), - 10 => format!("{:01$}", new_value, new_length), + 2 => format!("0b{:01$b}", new_value, old_text.len() - 2), + 8 => format!("0o{:01$o}", new_value, old_text.len() - 2), + 10 if old_text.starts_with('0') || old_text.starts_with("-0") => { + let length = match (old_value.is_negative(), new_value.is_negative()) { + (true, false) => old_text.len() - 1, + (false, true) => old_text.len() + 1, + _ => old_text.len(), + }; + format!("{:01$}", new_value, length) + } + 10 => format!("{}", new_value), 16 => { let (lower_count, upper_count): (usize, usize) = old_text.chars().skip(2).fold((0, 0), |(lower, upper), c| { @@ -5152,9 +5151,9 @@ fn increment_number_impl(cx: &mut Context, amount: i64) { ) }); if upper_count > lower_count { - format!("0x{:01$X}", new_value, new_length) + format!("0x{:01$X}", new_value, old_text.len() - 2) } else { - format!("0x{:01$x}", new_value, new_length) + format!("0x{:01$x}", new_value, old_text.len() - 2) } } _ => unimplemented!("radix not supported: {}", radix), From 483c0d582701c1253b6165a4fa816952e6c52f35 Mon Sep 17 00:00:00 2001 From: Jason Rodney Hansen Date: Fri, 12 Nov 2021 21:43:21 -0700 Subject: [PATCH 10/18] Handle numbers with `_` separators --- helix-core/src/numbers.rs | 8 +++-- helix-term/src/commands.rs | 61 +++++++++++++++++++++++++++++++------- 2 files changed, 55 insertions(+), 14 deletions(-) diff --git a/helix-core/src/numbers.rs b/helix-core/src/numbers.rs index 982835f0b8ed..240574929cb4 100644 --- a/helix-core/src/numbers.rs +++ b/helix-core/src/numbers.rs @@ -1,5 +1,3 @@ -use std::borrow::Cow; - use ropey::RopeSlice; use crate::{ @@ -36,7 +34,11 @@ pub fn number_at(text: RopeSlice, range: Range) -> Option { range }; - let word: Cow = text.slice(range.from()..range.to()).into(); + let word: String = text + .slice(range.from()..range.to()) + .chars() + .filter(|&c| c != '_') + .collect(); let (radix, prefixed) = if word.starts_with("0x") { (16, true) } else if word.starts_with("0o") { diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index b7fc343e054a..0d2c5db7097c 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -5128,18 +5128,32 @@ fn increment_number_impl(cx: &mut Context, amount: i64) { }) = number_at(text.slice(..), *range) { let old_text: Cow = text.slice(range.from()..range.to()).into(); + let old_length = old_text.len(); let new_value = old_value.wrapping_add(amount); - let new_text = match radix { - 2 => format!("0b{:01$b}", new_value, old_text.len() - 2), - 8 => format!("0o{:01$o}", new_value, old_text.len() - 2), + // Get separator indexes from right to left. + let separator_rtl_indexes: Vec = old_text + .chars() + .rev() + .enumerate() + .filter_map(|(i, c)| if c == '_' { Some(i) } else { None }) + .collect(); + + let format_length = if radix == 10 { + match (old_value.is_negative(), new_value.is_negative()) { + (true, false) => old_length - 1, + (false, true) => old_length + 1, + _ => old_text.len(), + } + } else { + old_text.len() - 2 + } - separator_rtl_indexes.len(); + + let mut new_text = match radix { + 2 => format!("0b{:01$b}", new_value, format_length), + 8 => format!("0o{:01$o}", new_value, format_length), 10 if old_text.starts_with('0') || old_text.starts_with("-0") => { - let length = match (old_value.is_negative(), new_value.is_negative()) { - (true, false) => old_text.len() - 1, - (false, true) => old_text.len() + 1, - _ => old_text.len(), - }; - format!("{:01$}", new_value, length) + format!("{:01$}", new_value, format_length) } 10 => format!("{}", new_value), 16 => { @@ -5151,14 +5165,39 @@ fn increment_number_impl(cx: &mut Context, amount: i64) { ) }); if upper_count > lower_count { - format!("0x{:01$X}", new_value, old_text.len() - 2) + format!("0x{:01$X}", new_value, format_length) } else { - format!("0x{:01$x}", new_value, old_text.len() - 2) + 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() - rtl_index; + new_text.insert(new_index, '_'); + } + } + + // Add in additional separators if necessary. + if new_text.len() > old_length && !separator_rtl_indexes.is_empty() { + let spacing = if separator_rtl_indexes.len() > 1 { + separator_rtl_indexes[separator_rtl_indexes.len() - 1] + - separator_rtl_indexes[separator_rtl_indexes.len() - 2] + } else { + separator_rtl_indexes[0] + }; + let prefix_length = if radix == 10 { 0 } else { 2 }; + if let Some(mut index) = new_text.find('_') { + while index - prefix_length > spacing { + index -= spacing; + new_text.insert(index, '_'); + } + } + } + let new_text: Tendril = new_text.into(); Some((range.from(), range.to(), Some(new_text))) From 95a02b785f2f3bf4b8c784aa18f1de447430f0e0 Mon Sep 17 00:00:00 2001 From: Jason Rodney Hansen Date: Sat, 13 Nov 2021 12:51:39 -0700 Subject: [PATCH 11/18] Refactor and add tests * Move most of the code into core * Add tests for the incremented output --- helix-core/src/numbers.rs | 411 +++++++++++++++++++++++++++++-------- helix-term/src/commands.rs | 89 +------- 2 files changed, 334 insertions(+), 166 deletions(-) diff --git a/helix-core/src/numbers.rs b/helix-core/src/numbers.rs index 240574929cb4..7b0fb3fbdc95 100644 --- a/helix-core/src/numbers.rs +++ b/helix-core/src/numbers.rs @@ -1,69 +1,154 @@ +use std::borrow::Cow; + use ropey::RopeSlice; use crate::{ textobject::{textobject_word, TextObject}, - Range, + Range, Tendril, }; #[derive(Debug, PartialEq, Eq)] -pub struct NumberInfo { +pub struct NumberIncrementor<'a> { pub range: Range, pub value: i64, pub radix: u32, + + text: RopeSlice<'a>, } -/// Return information about number under cursor if there is one. -pub fn number_at(text: RopeSlice, range: Range) -> Option { - // If the cursor is on the minus sign of a number we want to get the word textobject to the - // right of it. - let range = if range.to() < text.len_chars() - && range.to() - range.from() <= 1 - && text.char(range.from()) == '-' - { - Range::new(range.from() + 1, range.to() + 1) - } else { - range - }; - - let range = textobject_word(text, range, TextObject::Inside, 1, false); - - // If there is a minus sign to the left of the word object, we want to include it in the range. - let range = if range.from() > 0 && text.char(range.from() - 1) == '-' { - range.extend(range.from() - 1, range.from()) - } else { - range - }; - - let word: String = text - .slice(range.from()..range.to()) - .chars() - .filter(|&c| c != '_') - .collect(); - let (radix, prefixed) = if word.starts_with("0x") { - (16, true) - } else if word.starts_with("0o") { - (8, true) - } else if word.starts_with("0b") { - (2, true) - } else { - (10, false) - }; - - let number = if prefixed { &word[2..] } else { &word }; - - let value = i128::from_str_radix(number, radix).ok()?; - if (value.is_positive() && value.leading_zeros() < 64) - || (value.is_negative() && value.leading_ones() < 64) - { - return None; - } - - let value = value as i64; - Some(NumberInfo { - range, - value, - radix, - }) +impl<'a> NumberIncrementor<'a> { + /// Return information about number under rang if there is one. + pub fn from_range(text: RopeSlice, range: Range) -> Option { + // If the cursor is on the minus sign of a number we want to get the word textobject to the + // right of it. + let range = if range.to() < text.len_chars() + && range.to() - range.from() <= 1 + && text.char(range.from()) == '-' + { + Range::new(range.from() + 1, range.to() + 1) + } else { + range + }; + + let range = textobject_word(text, range, TextObject::Inside, 1, false); + + // If there is a minus sign to the left of the word object, we want to include it in the range. + let range = if range.from() > 0 && text.char(range.from() - 1) == '-' { + range.extend(range.from() - 1, range.from()) + } else { + range + }; + + let word: String = text + .slice(range.from()..range.to()) + .chars() + .filter(|&c| c != '_') + .collect(); + let (radix, prefixed) = if word.starts_with("0x") { + (16, true) + } else if word.starts_with("0o") { + (8, true) + } else if word.starts_with("0b") { + (2, true) + } else { + (10, false) + }; + + let number = if prefixed { &word[2..] } else { &word }; + + let value = i128::from_str_radix(number, radix).ok()?; + if (value.is_positive() && value.leading_zeros() < 64) + || (value.is_negative() && value.leading_ones() < 64) + { + return None; + } + + let value = value as i64; + Some(NumberIncrementor { + range, + value, + radix, + text, + }) + } + + /// Add `amount` to the number and return the formatted text. + pub fn incremented_text(&self, amount: i64) -> Tendril { + let old_text: Cow = self.text.slice(self.range.from()..self.range.to()).into(); + let old_length = old_text.len(); + let new_value = self.value.wrapping_add(amount); + + // Get separator indexes from right to left. + let separator_rtl_indexes: Vec = old_text + .chars() + .rev() + .enumerate() + .filter_map(|(i, c)| if c == '_' { Some(i) } else { None }) + .collect(); + + let format_length = if self.radix == 10 { + match (self.value.is_negative(), new_value.is_negative()) { + (true, false) => old_length - 1, + (false, true) => old_length + 1, + _ => old_text.len(), + } + } else { + old_text.len() - 2 + } - separator_rtl_indexes.len(); + + let mut new_text = match self.radix { + 2 => format!("0b{:01$b}", new_value, format_length), + 8 => format!("0o{:01$o}", new_value, format_length), + 10 if old_text.starts_with('0') || old_text.starts_with("-0") => { + format!("{:01$}", new_value, format_length) + } + 10 => format!("{}", new_value), + 16 => { + let (lower_count, upper_count): (usize, usize) = + old_text.chars().skip(2).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: {}", self.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() - rtl_index; + new_text.insert(new_index, '_'); + } + } + + // Add in additional separators if necessary. + if new_text.len() > old_length && !separator_rtl_indexes.is_empty() { + let spacing = if separator_rtl_indexes.len() > 1 { + separator_rtl_indexes[separator_rtl_indexes.len() - 1] + - separator_rtl_indexes[separator_rtl_indexes.len() - 2] + - 1 + } else { + separator_rtl_indexes[0] + }; + let prefix_length = if self.radix == 10 { 0 } else { 2 }; + if let Some(mut index) = new_text.find('_') { + while index - prefix_length > spacing { + index -= spacing; + new_text.insert(index, '_'); + } + } + } + + let new_text: Tendril = new_text.into(); + new_text + } } #[cfg(test)] @@ -76,11 +161,12 @@ mod test { let rope = Rope::from_str("Test text 12345 more text."); let range = Range::point(12); assert_eq!( - number_at(rope.slice(..), range), - Some(NumberInfo { + NumberIncrementor::from_range(rope.slice(..), range), + Some(NumberIncrementor { range: Range::new(10, 15), value: 12345, radix: 10, + text: rope.slice(..), }) ); } @@ -90,11 +176,12 @@ mod test { let rope = Rope::from_str("Test text 0x123ABCDEF more text."); let range = Range::point(12); assert_eq!( - number_at(rope.slice(..), range), - Some(NumberInfo { + NumberIncrementor::from_range(rope.slice(..), range), + Some(NumberIncrementor { range: Range::new(10, 21), value: 0x123ABCDEF, radix: 16, + text: rope.slice(..), }) ); } @@ -104,11 +191,12 @@ mod test { let rope = Rope::from_str("Test text 0xfa3b4e more text."); let range = Range::point(12); assert_eq!( - number_at(rope.slice(..), range), - Some(NumberInfo { + NumberIncrementor::from_range(rope.slice(..), range), + Some(NumberIncrementor { range: Range::new(10, 18), value: 0xfa3b4e, radix: 16, + text: rope.slice(..), }) ); } @@ -118,11 +206,12 @@ mod test { let rope = Rope::from_str("Test text 0o1074312 more text."); let range = Range::point(12); assert_eq!( - number_at(rope.slice(..), range), - Some(NumberInfo { + NumberIncrementor::from_range(rope.slice(..), range), + Some(NumberIncrementor { range: Range::new(10, 19), value: 0o1074312, radix: 8, + text: rope.slice(..), }) ); } @@ -132,11 +221,12 @@ mod test { let rope = Rope::from_str("Test text 0b10111010010101 more text."); let range = Range::point(12); assert_eq!( - number_at(rope.slice(..), range), - Some(NumberInfo { + NumberIncrementor::from_range(rope.slice(..), range), + Some(NumberIncrementor { range: Range::new(10, 26), value: 0b10111010010101, radix: 2, + text: rope.slice(..), }) ); } @@ -146,11 +236,12 @@ mod test { let rope = Rope::from_str("Test text -54321 more text."); let range = Range::point(12); assert_eq!( - number_at(rope.slice(..), range), - Some(NumberInfo { + NumberIncrementor::from_range(rope.slice(..), range), + Some(NumberIncrementor { range: Range::new(10, 16), value: -54321, radix: 10, + text: rope.slice(..), }) ); } @@ -160,11 +251,12 @@ mod test { let rope = Rope::from_str("Test text 000045326 more text."); let range = Range::point(12); assert_eq!( - number_at(rope.slice(..), range), - Some(NumberInfo { + NumberIncrementor::from_range(rope.slice(..), range), + Some(NumberIncrementor { range: Range::new(10, 19), value: 45326, radix: 10, + text: rope.slice(..), }) ); } @@ -174,39 +266,42 @@ mod test { let rope = Rope::from_str("Test text -54321 more text."); let range = Range::point(10); assert_eq!( - number_at(rope.slice(..), range), - Some(NumberInfo { + NumberIncrementor::from_range(rope.slice(..), range), + Some(NumberIncrementor { range: Range::new(10, 16), value: -54321, radix: 10, + text: rope.slice(..), }) ); } #[test] - fn test_number_at_start_of_rope() { + fn test_number_under_range_start_of_rope() { let rope = Rope::from_str("100"); let range = Range::point(0); assert_eq!( - number_at(rope.slice(..), range), - Some(NumberInfo { + NumberIncrementor::from_range(rope.slice(..), range), + Some(NumberIncrementor { range: Range::new(0, 3), value: 100, radix: 10, + text: rope.slice(..), }) ); } #[test] - fn test_number_at_end_of_rope() { + fn test_number_under_range_end_of_rope() { let rope = Rope::from_str("100"); let range = Range::point(2); assert_eq!( - number_at(rope.slice(..), range), - Some(NumberInfo { + NumberIncrementor::from_range(rope.slice(..), range), + Some(NumberIncrementor { range: Range::new(0, 3), value: 100, radix: 10, + text: rope.slice(..), }) ); } @@ -216,11 +311,12 @@ mod test { let rope = Rope::from_str(",100;"); let range = Range::point(1); assert_eq!( - number_at(rope.slice(..), range), - Some(NumberInfo { + NumberIncrementor::from_range(rope.slice(..), range), + Some(NumberIncrementor { range: Range::new(1, 4), value: 100, radix: 10, + text: rope.slice(..), }) ); } @@ -229,27 +325,178 @@ mod test { fn test_not_a_number_point() { let rope = Rope::from_str("Test text 45326 more text."); let range = Range::point(6); - assert_eq!(number_at(rope.slice(..), range), None); + assert_eq!(NumberIncrementor::from_range(rope.slice(..), range), None); } #[test] fn test_number_too_large_at_point() { let rope = Rope::from_str("Test text 0xFFFFFFFFFFFFFFFFF more text."); let range = Range::point(12); - assert_eq!(number_at(rope.slice(..), range), None); + assert_eq!(NumberIncrementor::from_range(rope.slice(..), range), None); } #[test] fn test_number_cursor_one_right_of_number() { let rope = Rope::from_str("100 "); let range = Range::point(3); - assert_eq!(number_at(rope.slice(..), range), None); + assert_eq!(NumberIncrementor::from_range(rope.slice(..), range), None); } #[test] fn test_number_cursor_one_left_of_number() { let rope = Rope::from_str(" 100"); let range = Range::point(0); - assert_eq!(number_at(rope.slice(..), range), None); + assert_eq!(NumberIncrementor::from_range(rope.slice(..), range), None); + } + + #[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 { + let rope = Rope::from_str(original); + let range = Range::point(0); + assert_eq!( + NumberIncrementor::from_range(rope.slice(..), range) + .unwrap() + .incremented_text(amount), + expected.into() + ); + } + } + + #[test] + fn test_increment_basic_hexadedimal_numbers() { + let tests = [ + ("0x0100", 1, "0x0101"), + ("0x0100", -1, "0x00ff"), + ("0x0001", -1, "0x0000"), + ("0x0000", -1, "0xffffffffffffffff"), + ("0xffffffffffffffff", 1, "0x0000000000000000"), + ("0xffffffffffffffff", 2, "0x0000000000000001"), + ("0xffffffffffffffff", -1, "0xfffffffffffffffe"), + ("0xABCDEF1234567890", 1, "0xABCDEF1234567891"), + ("0xabcdef1234567890", 1, "0xabcdef1234567891"), + ]; + + for (original, amount, expected) in tests { + let rope = Rope::from_str(original); + let range = Range::point(0); + assert_eq!( + NumberIncrementor::from_range(rope.slice(..), range) + .unwrap() + .incremented_text(amount), + expected.into() + ); + } + } + + #[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, "0o1777777777777777777777"), + ("0o1777777777777777777777", 1, "0o0000000000000000000000"), + ("0o1777777777777777777777", 2, "0o0000000000000000000001"), + ("0o1777777777777777777777", -1, "0o1777777777777777777776"), + ]; + + for (original, amount, expected) in tests { + let rope = Rope::from_str(original); + let range = Range::point(0); + assert_eq!( + NumberIncrementor::from_range(rope.slice(..), range) + .unwrap() + .incremented_text(amount), + expected.into() + ); + } + } + + #[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, + "0b1111111111111111111111111111111111111111111111111111111111111111", + ), + ( + "0b1111111111111111111111111111111111111111111111111111111111111111", + 1, + "0b0000000000000000000000000000000000000000000000000000000000000000", + ), + ( + "0b1111111111111111111111111111111111111111111111111111111111111111", + 2, + "0b0000000000000000000000000000000000000000000000000000000000000001", + ), + ( + "0b1111111111111111111111111111111111111111111111111111111111111111", + -1, + "0b1111111111111111111111111111111111111111111111111111111111111110", + ), + ]; + + for (original, amount, expected) in tests { + let rope = Rope::from_str(original); + let range = Range::point(0); + assert_eq!( + NumberIncrementor::from_range(rope.slice(..), range) + .unwrap() + .incremented_text(amount), + expected.into() + ); + } + } + + #[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_0001", 0x1_ffff_0000, "0x0001_ffff_0001"), + ("0x0000_0000_0001", 0x1_ffff_0000, "0x0001_ffff_0001"), + ("0x0000_0000", -1, "0xffff_ffff_ffff_ffff"), + ("0x0000_0000_0000", -1, "0xffff_ffff_ffff_ffff"), + ("0b01111111_11111111", 1, "0b10000000_00000000"), + ("0b11111111_11111111", 1, "0b1_00000000_00000000"), + ]; + + for (original, amount, expected) in tests { + let rope = Rope::from_str(original); + let range = Range::point(0); + assert_eq!( + NumberIncrementor::from_range(rope.slice(..), range) + .unwrap() + .incremented_text(amount), + expected.into() + ); + } } } diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 0d2c5db7097c..b92a75046945 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -4,7 +4,7 @@ use helix_core::{ line_ending::{get_line_ending_of_str, line_end_char_index, str_is_line_ending}, match_brackets, movement::{self, Direction}, - numbers::{number_at, NumberInfo}, + numbers::NumberIncrementor, object, pos_at_coords, regex::{self, Regex, RegexBuilder}, register::Register, @@ -5121,89 +5121,10 @@ fn increment_number_impl(cx: &mut Context, amount: i64) { let text = doc.text(); let changes = selection.ranges().iter().filter_map(|range| { - if let Some(NumberInfo { - range, - value: old_value, - radix, - }) = number_at(text.slice(..), *range) - { - let old_text: Cow = text.slice(range.from()..range.to()).into(); - let old_length = old_text.len(); - let new_value = old_value.wrapping_add(amount); - - // Get separator indexes from right to left. - let separator_rtl_indexes: Vec = old_text - .chars() - .rev() - .enumerate() - .filter_map(|(i, c)| if c == '_' { Some(i) } else { None }) - .collect(); - - let format_length = if radix == 10 { - match (old_value.is_negative(), new_value.is_negative()) { - (true, false) => old_length - 1, - (false, true) => old_length + 1, - _ => old_text.len(), - } - } else { - old_text.len() - 2 - } - separator_rtl_indexes.len(); - - let mut new_text = match radix { - 2 => format!("0b{:01$b}", new_value, format_length), - 8 => format!("0o{:01$o}", new_value, format_length), - 10 if old_text.starts_with('0') || old_text.starts_with("-0") => { - format!("{:01$}", new_value, format_length) - } - 10 => format!("{}", new_value), - 16 => { - let (lower_count, upper_count): (usize, usize) = - old_text.chars().skip(2).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() - rtl_index; - new_text.insert(new_index, '_'); - } - } - - // Add in additional separators if necessary. - if new_text.len() > old_length && !separator_rtl_indexes.is_empty() { - let spacing = if separator_rtl_indexes.len() > 1 { - separator_rtl_indexes[separator_rtl_indexes.len() - 1] - - separator_rtl_indexes[separator_rtl_indexes.len() - 2] - } else { - separator_rtl_indexes[0] - }; - let prefix_length = if radix == 10 { 0 } else { 2 }; - if let Some(mut index) = new_text.find('_') { - while index - prefix_length > spacing { - index -= spacing; - new_text.insert(index, '_'); - } - } - } - - let new_text: Tendril = new_text.into(); - - Some((range.from(), range.to(), Some(new_text))) - } else { - None - } + NumberIncrementor::from_range(text.slice(..), *range).map(|incrementor| { + let new_text = incrementor.incremented_text(amount); + (range.from(), range.to(), Some(new_text)) + }) }); if changes.clone().count() > 0 { From 52e3de3102a65baa5a782678daa0a8b1a7531436 Mon Sep 17 00:00:00 2001 From: Jason Rodney Hansen Date: Sat, 13 Nov 2021 16:23:05 -0700 Subject: [PATCH 12/18] Use correct range --- helix-term/src/commands.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index b92a75046945..be63f730da08 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -5123,7 +5123,7 @@ fn increment_number_impl(cx: &mut Context, amount: i64) { let changes = selection.ranges().iter().filter_map(|range| { NumberIncrementor::from_range(text.slice(..), *range).map(|incrementor| { let new_text = incrementor.incremented_text(amount); - (range.from(), range.to(), Some(new_text)) + (incrementor.range.from(), incrementor.range.to(), Some(new_text)) }) }); From 66c4920c9e27cd5125985f17139d6b5142c4e1b2 Mon Sep 17 00:00:00 2001 From: Jason Rodney Hansen Date: Sat, 13 Nov 2021 16:32:20 -0700 Subject: [PATCH 13/18] Formatting --- helix-term/src/commands.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index be63f730da08..a742de261357 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -5123,7 +5123,11 @@ fn increment_number_impl(cx: &mut Context, amount: i64) { let changes = selection.ranges().iter().filter_map(|range| { NumberIncrementor::from_range(text.slice(..), *range).map(|incrementor| { let new_text = incrementor.incremented_text(amount); - (incrementor.range.from(), incrementor.range.to(), Some(new_text)) + ( + incrementor.range.from(), + incrementor.range.to(), + Some(new_text), + ) }) }); From d3d77292d3ebab692cdb5ef53ab6076304fb67c5 Mon Sep 17 00:00:00 2001 From: Jason Rodney Hansen Date: Sat, 13 Nov 2021 20:38:56 -0700 Subject: [PATCH 14/18] Rename increment functions --- book/src/keymap.md | 4 ++-- helix-term/src/commands.rs | 20 ++++++++++---------- helix-term/src/keymap.rs | 4 ++-- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/book/src/keymap.md b/book/src/keymap.md index d5754a69b347..97d70bfae8fa 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -67,8 +67,8 @@ | `=` | Format selection | `format_selections` | | `d` | Delete selection | `delete_selection` | | `c` | Change selection (delete and enter insert mode) | `change_selection` | -| `Ctrl-a` | Increment number under cursor | `increment_number` | -| `Ctrl-x` | Decrement number under cursor | `decrement_number` | +| `Ctrl-a` | Increment object under cursor | `increment` | +| `Ctrl-x` | Decrement object under cursor | `decrement` | #### Shell diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index a742de261357..c630bd9a8286 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -341,8 +341,8 @@ impl Command { shell_keep_pipe, "Filter selections with shell predicate", suspend, "Suspend", rename_symbol, "Rename symbol", - increment_number, "Increment number under cursor", - decrement_number, "Decrement number under cursor", + increment, "Increment", + decrement, "Decrement", ); } @@ -5104,18 +5104,18 @@ fn rename_symbol(cx: &mut Context) { cx.push_layer(Box::new(prompt)); } -/// If there is a number at the cursor, increment it by the count. -fn increment_number(cx: &mut Context) { - increment_number_impl(cx, cx.count() as i64); +/// Increment object under cursor by count. +fn increment(cx: &mut Context) { + increment_impl(cx, cx.count() as i64); } -/// If there is a number at the cursor, decrement it by the count. -fn decrement_number(cx: &mut Context) { - increment_number_impl(cx, -(cx.count() as i64)); +/// Decrement object under cursor by count. +fn decrement(cx: &mut Context) { + increment_impl(cx, -(cx.count() as i64)); } -/// If there is a number at the cursor, increment it by `amount`. -fn increment_number_impl(cx: &mut Context, amount: i64) { +/// Decrement object under cursor by `amount`. +fn increment_impl(cx: &mut Context, amount: i64) { let (view, doc) = current!(cx.editor); let selection = doc.selection(view.id); let text = doc.text(); diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 1887e99da4f0..a44adefbff6e 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -619,8 +619,8 @@ impl Default for Keymaps { "$" => shell_keep_pipe, "C-z" => suspend, - "C-a" => increment_number, - "C-x" => decrement_number, + "C-a" => increment, + "C-x" => decrement, }); let mut select = normal.clone(); select.merge_nodes(keymap!({ "Select mode" From 4754b492ad952ed8937821318cce08159eb6ced8 Mon Sep 17 00:00:00 2001 From: Jason Rodney Hansen Date: Sun, 14 Nov 2021 09:19:24 -0700 Subject: [PATCH 15/18] Make docs more specific --- book/src/keymap.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/book/src/keymap.md b/book/src/keymap.md index 97d70bfae8fa..88014fb32c6a 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -67,8 +67,8 @@ | `=` | Format selection | `format_selections` | | `d` | Delete selection | `delete_selection` | | `c` | Change selection (delete and enter insert mode) | `change_selection` | -| `Ctrl-a` | Increment object under cursor | `increment` | -| `Ctrl-x` | Decrement object under cursor | `decrement` | +| `Ctrl-a` | Increment object (number) under cursor | `increment` | +| `Ctrl-x` | Decrement object (number) under cursor | `decrement` | #### Shell From 7f18e9ac992621929e0dda1ca059802e7cc1aec0 Mon Sep 17 00:00:00 2001 From: Jason Rodney Hansen Date: Sun, 14 Nov 2021 10:07:40 -0700 Subject: [PATCH 16/18] This is easier to read --- helix-term/src/commands.rs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index c630bd9a8286..7d9ea7ba30d3 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -5121,14 +5121,13 @@ fn increment_impl(cx: &mut Context, amount: i64) { let text = doc.text(); let changes = selection.ranges().iter().filter_map(|range| { - NumberIncrementor::from_range(text.slice(..), *range).map(|incrementor| { - let new_text = incrementor.incremented_text(amount); - ( - incrementor.range.from(), - incrementor.range.to(), - Some(new_text), - ) - }) + let incrementor = NumberIncrementor::from_range(text.slice(..), *range)?; + let new_text = incrementor.incremented_text(amount); + Some(( + incrementor.range.from(), + incrementor.range.to(), + Some(new_text), + )) }); if changes.clone().count() > 0 { From df30836793c286ba4827f6f8a6dabea5aab2d852 Mon Sep 17 00:00:00 2001 From: Jason Rodney Hansen Date: Sun, 14 Nov 2021 10:12:08 -0700 Subject: [PATCH 17/18] This is clearer --- helix-core/src/numbers.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/helix-core/src/numbers.rs b/helix-core/src/numbers.rs index 7b0fb3fbdc95..2766faf926e5 100644 --- a/helix-core/src/numbers.rs +++ b/helix-core/src/numbers.rs @@ -130,13 +130,11 @@ impl<'a> NumberIncrementor<'a> { // Add in additional separators if necessary. if new_text.len() > old_length && !separator_rtl_indexes.is_empty() { - let spacing = if separator_rtl_indexes.len() > 1 { - separator_rtl_indexes[separator_rtl_indexes.len() - 1] - - separator_rtl_indexes[separator_rtl_indexes.len() - 2] - - 1 - } else { - separator_rtl_indexes[0] + let spacing = match separator_rtl_indexes.as_slice() { + [.., b, a] => a - b - 1, + _ => separator_rtl_indexes[0], }; + let prefix_length = if self.radix == 10 { 0 } else { 2 }; if let Some(mut index) = new_text.find('_') { while index - prefix_length > spacing { From f4825e4dfe9625adce9113d2c7ab531e711e83c5 Mon Sep 17 00:00:00 2001 From: Jason Rodney Hansen Date: Sun, 14 Nov 2021 10:17:49 -0700 Subject: [PATCH 18/18] Type can be inferred --- helix-core/src/numbers.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/helix-core/src/numbers.rs b/helix-core/src/numbers.rs index 2766faf926e5..e9f3c898dc82 100644 --- a/helix-core/src/numbers.rs +++ b/helix-core/src/numbers.rs @@ -144,8 +144,7 @@ impl<'a> NumberIncrementor<'a> { } } - let new_text: Tendril = new_text.into(); - new_text + new_text.into() } }