Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'crates/ide-assists/src/handlers/number_representation.rs')
| -rw-r--r-- | crates/ide-assists/src/handlers/number_representation.rs | 183 |
1 files changed, 183 insertions, 0 deletions
diff --git a/crates/ide-assists/src/handlers/number_representation.rs b/crates/ide-assists/src/handlers/number_representation.rs new file mode 100644 index 0000000000..f77471d80c --- /dev/null +++ b/crates/ide-assists/src/handlers/number_representation.rs @@ -0,0 +1,183 @@ +use syntax::{ast, ast::Radix, AstToken}; + +use crate::{AssistContext, AssistId, AssistKind, Assists, GroupLabel}; + +const MIN_NUMBER_OF_DIGITS_TO_FORMAT: usize = 5; + +// Assist: reformat_number_literal +// +// Adds or removes separators from integer literal. +// +// ``` +// const _: i32 = 1012345$0; +// ``` +// -> +// ``` +// const _: i32 = 1_012_345; +// ``` +pub(crate) fn reformat_number_literal(acc: &mut Assists, ctx: &AssistContext) -> Option<()> { + let literal = ctx.find_node_at_offset::<ast::Literal>()?; + let literal = match literal.kind() { + ast::LiteralKind::IntNumber(it) => it, + _ => return None, + }; + + let text = literal.text(); + if text.contains('_') { + return remove_separators(acc, literal); + } + + let (prefix, value, suffix) = literal.split_into_parts(); + if value.len() < MIN_NUMBER_OF_DIGITS_TO_FORMAT { + return None; + } + + let radix = literal.radix(); + let mut converted = prefix.to_string(); + converted.push_str(&add_group_separators(value, group_size(radix))); + converted.push_str(suffix); + + let group_id = GroupLabel("Reformat number literal".into()); + let label = format!("Convert {} to {}", literal, converted); + let range = literal.syntax().text_range(); + acc.add_group( + &group_id, + AssistId("reformat_number_literal", AssistKind::RefactorInline), + label, + range, + |builder| builder.replace(range, converted), + ) +} + +fn remove_separators(acc: &mut Assists, literal: ast::IntNumber) -> Option<()> { + let group_id = GroupLabel("Reformat number literal".into()); + let range = literal.syntax().text_range(); + acc.add_group( + &group_id, + AssistId("reformat_number_literal", AssistKind::RefactorInline), + "Remove digit separators", + range, + |builder| builder.replace(range, literal.text().replace('_', "")), + ) +} + +const fn group_size(r: Radix) -> usize { + match r { + Radix::Binary => 4, + Radix::Octal => 3, + Radix::Decimal => 3, + Radix::Hexadecimal => 4, + } +} + +fn add_group_separators(s: &str, group_size: usize) -> String { + let mut chars = Vec::new(); + for (i, ch) in s.chars().filter(|&ch| ch != '_').rev().enumerate() { + if i > 0 && i % group_size == 0 { + chars.push('_'); + } + chars.push(ch); + } + + chars.into_iter().rev().collect() +} + +#[cfg(test)] +mod tests { + use crate::tests::{check_assist_by_label, check_assist_not_applicable, check_assist_target}; + + use super::*; + + #[test] + fn group_separators() { + let cases = vec![ + ("", 4, ""), + ("1", 4, "1"), + ("12", 4, "12"), + ("123", 4, "123"), + ("1234", 4, "1234"), + ("12345", 4, "1_2345"), + ("123456", 4, "12_3456"), + ("1234567", 4, "123_4567"), + ("12345678", 4, "1234_5678"), + ("123456789", 4, "1_2345_6789"), + ("1234567890", 4, "12_3456_7890"), + ("1_2_3_4_5_6_7_8_9_0_", 4, "12_3456_7890"), + ("1234567890", 3, "1_234_567_890"), + ("1234567890", 2, "12_34_56_78_90"), + ("1234567890", 1, "1_2_3_4_5_6_7_8_9_0"), + ]; + + for case in cases { + let (input, group_size, expected) = case; + assert_eq!(add_group_separators(input, group_size), expected) + } + } + + #[test] + fn good_targets() { + let cases = vec![ + ("const _: i32 = 0b11111$0", "0b11111"), + ("const _: i32 = 0o77777$0;", "0o77777"), + ("const _: i32 = 10000$0;", "10000"), + ("const _: i32 = 0xFFFFF$0;", "0xFFFFF"), + ("const _: i32 = 10000i32$0;", "10000i32"), + ("const _: i32 = 0b_10_0i32$0;", "0b_10_0i32"), + ]; + + for case in cases { + check_assist_target(reformat_number_literal, case.0, case.1); + } + } + + #[test] + fn bad_targets() { + let cases = vec![ + "const _: i32 = 0b111$0", + "const _: i32 = 0b1111$0", + "const _: i32 = 0o77$0;", + "const _: i32 = 0o777$0;", + "const _: i32 = 10$0;", + "const _: i32 = 999$0;", + "const _: i32 = 0xFF$0;", + "const _: i32 = 0xFFFF$0;", + ]; + + for case in cases { + check_assist_not_applicable(reformat_number_literal, case); + } + } + + #[test] + fn labels() { + let cases = vec![ + ("const _: i32 = 10000$0", "const _: i32 = 10_000", "Convert 10000 to 10_000"), + ( + "const _: i32 = 0xFF0000$0;", + "const _: i32 = 0xFF_0000;", + "Convert 0xFF0000 to 0xFF_0000", + ), + ( + "const _: i32 = 0b11111111$0;", + "const _: i32 = 0b1111_1111;", + "Convert 0b11111111 to 0b1111_1111", + ), + ( + "const _: i32 = 0o377211$0;", + "const _: i32 = 0o377_211;", + "Convert 0o377211 to 0o377_211", + ), + ( + "const _: i32 = 10000i32$0;", + "const _: i32 = 10_000i32;", + "Convert 10000i32 to 10_000i32", + ), + ("const _: i32 = 1_0_0_0_i32$0;", "const _: i32 = 1000i32;", "Remove digit separators"), + ]; + + for case in cases { + let (before, after, label) = case; + check_assist_by_label(reformat_number_literal, before, after, label); + } + } +} |