Skip to main content

ide_assists/handlers/
number_representation.rs

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
7// Assist: reformat_number_literal
8//
9// Adds or removes separators from integer literal.
10//
11// ```
12// const _: i32 = 1012345$0;
13// ```
14// ->
15// ```
16// const _: i32 = 1_012_345;
17// ```
18pub(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}