|
| 1 | +use syntax::{ast, ast::Radix, AstToken}; |
| 2 | + |
| 3 | +use crate::{AssistContext, AssistId, AssistKind, Assists, GroupLabel}; |
| 4 | + |
| 5 | +const MIN_NUMBER_OF_DIGITS_TO_FORMAT: usize = 5; |
| 6 | + |
| 7 | +// Assist: reformat_number_literal |
| 8 | +// |
| 9 | +// Adds or removes seprators from integer literal. |
| 10 | +// |
| 11 | +// ``` |
| 12 | +// const _: i32 = 1012345$0; |
| 13 | +// ``` |
| 14 | +// -> |
| 15 | +// ``` |
| 16 | +// const _: i32 = 1_012_345; |
| 17 | +// ``` |
| 18 | +pub(crate) fn reformat_number_literal(acc: &mut Assists, ctx: &AssistContext) -> Option<()> { |
| 19 | + let literal = ctx.find_node_at_offset::<ast::Literal>()?; |
| 20 | + let literal = match literal.kind() { |
| 21 | + ast::LiteralKind::IntNumber(it) => it, |
| 22 | + _ => return None, |
| 23 | + }; |
| 24 | + |
| 25 | + let text = literal.text(); |
| 26 | + if text.contains('_') { |
| 27 | + return remove_separators(acc, literal); |
| 28 | + } |
| 29 | + |
| 30 | + let value = literal.str_value(); |
| 31 | + if value.len() < MIN_NUMBER_OF_DIGITS_TO_FORMAT { |
| 32 | + return None; |
| 33 | + } |
| 34 | + |
| 35 | + let radix = literal.radix(); |
| 36 | + let mut converted = literal.prefix().to_string(); |
| 37 | + converted.push_str(&add_group_separators(literal.str_value(), group_size(radix))); |
| 38 | + if let Some(suffix) = literal.suffix() { |
| 39 | + converted.push_str(suffix); |
| 40 | + } |
| 41 | + |
| 42 | + let group_id = GroupLabel("Reformat number literal".into()); |
| 43 | + let label = format!("Convert {} to {}", literal, converted); |
| 44 | + let range = literal.syntax().text_range(); |
| 45 | + acc.add_group( |
| 46 | + &group_id, |
| 47 | + AssistId("reformat_number_literal", AssistKind::RefactorInline), |
| 48 | + label, |
| 49 | + range, |
| 50 | + |builder| builder.replace(range, converted), |
| 51 | + ) |
| 52 | +} |
| 53 | + |
| 54 | +fn remove_separators(acc: &mut Assists, literal: ast::IntNumber) -> Option<()> { |
| 55 | + let group_id = GroupLabel("Reformat number literal".into()); |
| 56 | + let range = literal.syntax().text_range(); |
| 57 | + acc.add_group( |
| 58 | + &group_id, |
| 59 | + AssistId("reformat_number_literal", AssistKind::RefactorInline), |
| 60 | + "Remove digit seprators", |
| 61 | + range, |
| 62 | + |builder| builder.replace(range, literal.text().replace("_", "")), |
| 63 | + ) |
| 64 | +} |
| 65 | + |
| 66 | +const fn group_size(r: Radix) -> usize { |
| 67 | + match r { |
| 68 | + Radix::Binary => 4, |
| 69 | + Radix::Octal => 3, |
| 70 | + Radix::Decimal => 3, |
| 71 | + Radix::Hexadecimal => 4, |
| 72 | + } |
| 73 | +} |
| 74 | + |
| 75 | +fn add_group_separators(s: &str, group_size: usize) -> String { |
| 76 | + let mut chars = Vec::new(); |
| 77 | + for (i, ch) in s.chars().filter(|&ch| ch != '_').rev().enumerate() { |
| 78 | + if i > 0 && i % group_size == 0 { |
| 79 | + chars.push('_'); |
| 80 | + } |
| 81 | + chars.push(ch); |
| 82 | + } |
| 83 | + |
| 84 | + chars.into_iter().rev().collect() |
| 85 | +} |
| 86 | + |
| 87 | +#[cfg(test)] |
| 88 | +mod tests { |
| 89 | + use crate::tests::{check_assist_by_label, check_assist_not_applicable, check_assist_target}; |
| 90 | + |
| 91 | + use super::*; |
| 92 | + |
| 93 | + #[test] |
| 94 | + fn group_separators() { |
| 95 | + let cases = vec![ |
| 96 | + ("", 4, ""), |
| 97 | + ("1", 4, "1"), |
| 98 | + ("12", 4, "12"), |
| 99 | + ("123", 4, "123"), |
| 100 | + ("1234", 4, "1234"), |
| 101 | + ("12345", 4, "1_2345"), |
| 102 | + ("123456", 4, "12_3456"), |
| 103 | + ("1234567", 4, "123_4567"), |
| 104 | + ("12345678", 4, "1234_5678"), |
| 105 | + ("123456789", 4, "1_2345_6789"), |
| 106 | + ("1234567890", 4, "12_3456_7890"), |
| 107 | + ("1_2_3_4_5_6_7_8_9_0_", 4, "12_3456_7890"), |
| 108 | + ("1234567890", 3, "1_234_567_890"), |
| 109 | + ("1234567890", 2, "12_34_56_78_90"), |
| 110 | + ("1234567890", 1, "1_2_3_4_5_6_7_8_9_0"), |
| 111 | + ]; |
| 112 | + |
| 113 | + for case in cases { |
| 114 | + let (input, group_size, expected) = case; |
| 115 | + assert_eq!(add_group_separators(input, group_size), expected) |
| 116 | + } |
| 117 | + } |
| 118 | + |
| 119 | + #[test] |
| 120 | + fn good_targets() { |
| 121 | + let cases = vec![ |
| 122 | + ("const _: i32 = 0b11111$0", "0b11111"), |
| 123 | + ("const _: i32 = 0o77777$0;", "0o77777"), |
| 124 | + ("const _: i32 = 10000$0;", "10000"), |
| 125 | + ("const _: i32 = 0xFFFFF$0;", "0xFFFFF"), |
| 126 | + ("const _: i32 = 10000i32$0;", "10000i32"), |
| 127 | + ("const _: i32 = 0b_10_0i32$0;", "0b_10_0i32"), |
| 128 | + ]; |
| 129 | + |
| 130 | + for case in cases { |
| 131 | + check_assist_target(reformat_number_literal, case.0, case.1); |
| 132 | + } |
| 133 | + } |
| 134 | + |
| 135 | + #[test] |
| 136 | + fn bad_targets() { |
| 137 | + let cases = vec![ |
| 138 | + "const _: i32 = 0b111$0", |
| 139 | + "const _: i32 = 0b1111$0", |
| 140 | + "const _: i32 = 0o77$0;", |
| 141 | + "const _: i32 = 0o777$0;", |
| 142 | + "const _: i32 = 10$0;", |
| 143 | + "const _: i32 = 999$0;", |
| 144 | + "const _: i32 = 0xFF$0;", |
| 145 | + "const _: i32 = 0xFFFF$0;", |
| 146 | + ]; |
| 147 | + |
| 148 | + for case in cases { |
| 149 | + check_assist_not_applicable(reformat_number_literal, case); |
| 150 | + } |
| 151 | + } |
| 152 | + |
| 153 | + #[test] |
| 154 | + fn labels() { |
| 155 | + let cases = vec![ |
| 156 | + ("const _: i32 = 10000$0", "const _: i32 = 10_000", "Convert 10000 to 10_000"), |
| 157 | + ( |
| 158 | + "const _: i32 = 0xFF0000$0;", |
| 159 | + "const _: i32 = 0xFF_0000;", |
| 160 | + "Convert 0xFF0000 to 0xFF_0000", |
| 161 | + ), |
| 162 | + ( |
| 163 | + "const _: i32 = 0b11111111$0;", |
| 164 | + "const _: i32 = 0b1111_1111;", |
| 165 | + "Convert 0b11111111 to 0b1111_1111", |
| 166 | + ), |
| 167 | + ( |
| 168 | + "const _: i32 = 0o377211$0;", |
| 169 | + "const _: i32 = 0o377_211;", |
| 170 | + "Convert 0o377211 to 0o377_211", |
| 171 | + ), |
| 172 | + ( |
| 173 | + "const _: i32 = 10000i32$0;", |
| 174 | + "const _: i32 = 10_000i32;", |
| 175 | + "Convert 10000i32 to 10_000i32", |
| 176 | + ), |
| 177 | + ("const _: i32 = 1_0_0_0_i32$0;", "const _: i32 = 1000i32;", "Remove digit seprators"), |
| 178 | + ]; |
| 179 | + |
| 180 | + for case in cases { |
| 181 | + let (before, after, label) = case; |
| 182 | + check_assist_by_label(reformat_number_literal, before, after, label); |
| 183 | + } |
| 184 | + } |
| 185 | +} |
0 commit comments