Skip to content

Commit 8b03b41

Browse files
committed
Add number representation assists
1 parent 755b668 commit 8b03b41

File tree

4 files changed

+233
-26
lines changed

4 files changed

+233
-26
lines changed
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
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+
}

crates/ide_assists/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ mod handlers {
158158
mod move_module_to_file;
159159
mod move_to_mod_rs;
160160
mod move_from_mod_rs;
161+
mod number_representation;
161162
mod promote_local_to_const;
162163
mod pull_assignment_up;
163164
mod qualify_path;
@@ -241,6 +242,7 @@ mod handlers {
241242
move_module_to_file::move_module_to_file,
242243
move_to_mod_rs::move_to_mod_rs,
243244
move_from_mod_rs::move_from_mod_rs,
245+
number_representation::reformat_number_literal,
244246
pull_assignment_up::pull_assignment_up,
245247
promote_local_to_const::promote_local_to_const,
246248
qualify_path::qualify_path,

crates/ide_assists/src/tests/generated.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1577,6 +1577,19 @@ pub mod std { pub mod collections { pub struct HashMap { } } }
15771577
)
15781578
}
15791579

1580+
#[test]
1581+
fn doctest_reformat_number_literal() {
1582+
check_doc_test(
1583+
"reformat_number_literal",
1584+
r#####"
1585+
const _: i32 = 1012345$0;
1586+
"#####,
1587+
r#####"
1588+
const _: i32 = 1_012_345;
1589+
"#####,
1590+
)
1591+
}
1592+
15801593
#[test]
15811594
fn doctest_remove_dbg() {
15821595
check_doc_test(

crates/syntax/src/ast/token_ext.rs

Lines changed: 33 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -613,6 +613,8 @@ impl HasFormatSpecifier for ast::String {
613613
}
614614
}
615615

616+
struct IntNumberParts<'a>(&'a str, &'a str, &'a str);
617+
616618
impl ast::IntNumber {
617619
pub fn radix(&self) -> Radix {
618620
match self.text().get(..2).unwrap_or_default() {
@@ -623,41 +625,46 @@ impl ast::IntNumber {
623625
}
624626
}
625627

626-
pub fn value(&self) -> Option<u128> {
627-
let token = self.syntax();
628-
629-
let mut text = token.text();
630-
if let Some(suffix) = self.suffix() {
631-
text = &text[..text.len() - suffix.len()];
632-
}
633-
628+
fn split_into_parts(&self) -> IntNumberParts {
634629
let radix = self.radix();
635-
text = &text[radix.prefix_len()..];
630+
let (prefix, mut text) = self.text().split_at(radix.prefix_len());
636631

637-
let buf;
638-
if text.contains('_') {
639-
buf = text.replace('_', "");
640-
text = buf.as_str();
632+
let is_suffix_start: fn(&(usize, char)) -> bool = match radix {
633+
Radix::Hexadecimal => |(_, c)| matches!(c, 'g'..='z' | 'G'..='Z'),
634+
_ => |(_, c)| c.is_ascii_alphabetic(),
635+
};
636+
637+
let mut suffix = "";
638+
if let Some((suffix_start, _)) = text.char_indices().find(is_suffix_start) {
639+
let (text2, suffix2) = text.split_at(suffix_start);
640+
text = text2;
641+
suffix = suffix2;
641642
};
642643

643-
let value = u128::from_str_radix(text, radix as u32).ok()?;
644+
IntNumberParts(prefix, text, suffix)
645+
}
646+
647+
pub fn prefix(&self) -> &str {
648+
self.split_into_parts().0
649+
}
650+
651+
pub fn str_value(&self) -> &str {
652+
self.split_into_parts().1
653+
}
654+
655+
pub fn value(&self) -> Option<u128> {
656+
let text = self.str_value().replace("_", "");
657+
let value = u128::from_str_radix(&text, self.radix() as u32).ok()?;
644658
Some(value)
645659
}
646660

647661
pub fn suffix(&self) -> Option<&str> {
648-
let text = self.text();
649-
let radix = self.radix();
650-
let mut indices = text.char_indices();
651-
if radix != Radix::Decimal {
652-
indices.next()?;
653-
indices.next()?;
662+
let suffix = self.split_into_parts().2;
663+
if suffix.is_empty() {
664+
None
665+
} else {
666+
Some(suffix)
654667
}
655-
let is_suffix_start: fn(&(usize, char)) -> bool = match radix {
656-
Radix::Hexadecimal => |(_, c)| matches!(c, 'g'..='z' | 'G'..='Z'),
657-
_ => |(_, c)| c.is_ascii_alphabetic(),
658-
};
659-
let (suffix_start, _) = indices.find(is_suffix_start)?;
660-
Some(&text[suffix_start..])
661668
}
662669
}
663670

0 commit comments

Comments
 (0)