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