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