Skip to content

Commit c84fdb8

Browse files
committed
Add convert integer literal assist
1 parent e315fd9 commit c84fdb8

File tree

3 files changed

+150
-0
lines changed

3 files changed

+150
-0
lines changed
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
use syntax::{ast, AstNode, SmolStr};
2+
3+
use crate::{AssistContext, AssistId, AssistKind, Assists, GroupLabel};
4+
5+
// Assist: convert_integer_literal
6+
//
7+
// Converts the base of integer literals to other bases.
8+
//
9+
// ```
10+
// const _: i32 = 10<|>;
11+
// ```
12+
// ->
13+
// ```
14+
// const _: i32 = 0b1010;
15+
// ```
16+
pub(crate) fn convert_integer_literal(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
17+
let literal = ctx.find_node_at_offset::<ast::Literal>()?;
18+
let range = literal.syntax().text_range();
19+
let group_id = GroupLabel("Convert integer base".into());
20+
21+
let suffix = match literal.kind() {
22+
ast::LiteralKind::IntNumber { suffix } => suffix,
23+
_ => return None,
24+
};
25+
let suffix_len = suffix.as_ref().map(|s| s.len()).unwrap_or(0);
26+
let raw_literal_text = literal.syntax().to_string();
27+
28+
// Gets the literal's text without the type suffix and without underscores.
29+
let literal_text = raw_literal_text
30+
.chars()
31+
.take(raw_literal_text.len() - suffix_len)
32+
.filter(|c| *c != '_')
33+
.collect::<SmolStr>();
34+
let literal_base = IntegerLiteralBase::identify(&literal_text)?;
35+
36+
for base in IntegerLiteralBase::bases() {
37+
if *base == literal_base {
38+
continue;
39+
}
40+
41+
let mut converted = literal_base.convert(&literal_text, base);
42+
43+
let label = if let Some(suffix) = &suffix {
44+
format!("Convert {} ({}) to {}", &literal_text, suffix, &converted)
45+
} else {
46+
format!("Convert {} to {}", &literal_text, &converted)
47+
};
48+
49+
// Appends the type suffix back into the new literal if it exists.
50+
if let Some(suffix) = &suffix {
51+
converted.push_str(&suffix);
52+
}
53+
54+
acc.add_group(
55+
&group_id,
56+
AssistId("convert_integer_literal", AssistKind::RefactorInline),
57+
label,
58+
range,
59+
|builder| builder.replace(range, converted),
60+
);
61+
}
62+
63+
Some(())
64+
}
65+
66+
#[derive(Debug, PartialEq, Eq)]
67+
enum IntegerLiteralBase {
68+
Binary,
69+
Octal,
70+
Decimal,
71+
Hexadecimal,
72+
}
73+
74+
impl IntegerLiteralBase {
75+
fn identify(literal_text: &str) -> Option<Self> {
76+
// We cannot express a literal in anything other than decimal in under 3 characters, so we return here if possible.
77+
if literal_text.len() < 3 && literal_text.chars().all(|c| c.is_digit(10)) {
78+
return Some(Self::Decimal);
79+
}
80+
81+
let base = match &literal_text[..2] {
82+
"0b" => Self::Binary,
83+
"0o" => Self::Octal,
84+
"0x" => Self::Hexadecimal,
85+
_ => Self::Decimal,
86+
};
87+
88+
// Checks that all characters after the base prefix are all valid digits for that base.
89+
if literal_text[base.prefix_len()..]
90+
.chars()
91+
.all(|c| c.is_digit(base.base()))
92+
{
93+
Some(base)
94+
} else {
95+
None
96+
}
97+
}
98+
99+
fn convert(&self, literal_text: &str, to: &IntegerLiteralBase) -> String {
100+
let digits = &literal_text[self.prefix_len()..];
101+
let value = u128::from_str_radix(digits, self.base()).unwrap();
102+
103+
match to {
104+
Self::Binary => format!("0b{:b}", value),
105+
Self::Octal => format!("0o{:o}", value),
106+
Self::Decimal => value.to_string(),
107+
Self::Hexadecimal => format!("0x{:X}", value),
108+
}
109+
}
110+
111+
const fn base(&self) -> u32 {
112+
match self {
113+
Self::Binary => 2,
114+
Self::Octal => 8,
115+
Self::Decimal => 10,
116+
Self::Hexadecimal => 16,
117+
}
118+
}
119+
120+
const fn prefix_len(&self) -> usize {
121+
match self {
122+
Self::Decimal => 0,
123+
_ => 2,
124+
}
125+
}
126+
127+
const fn bases() -> &'static [IntegerLiteralBase] {
128+
&[
129+
IntegerLiteralBase::Binary,
130+
IntegerLiteralBase::Octal,
131+
IntegerLiteralBase::Decimal,
132+
IntegerLiteralBase::Hexadecimal,
133+
]
134+
}
135+
}

crates/assists/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ mod handlers {
128128
mod auto_import;
129129
mod change_return_type_to_result;
130130
mod change_visibility;
131+
mod convert_integer_literal;
131132
mod early_return;
132133
mod expand_glob_import;
133134
mod extract_struct_from_enum_variant;
@@ -172,6 +173,7 @@ mod handlers {
172173
auto_import::auto_import,
173174
change_return_type_to_result::change_return_type_to_result,
174175
change_visibility::change_visibility,
176+
convert_integer_literal::convert_integer_literal,
175177
early_return::convert_to_guarded_return,
176178
expand_glob_import::expand_glob_import,
177179
extract_struct_from_enum_variant::extract_struct_from_enum_variant,

crates/assists/src/tests/generated.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,19 @@ pub(crate) fn frobnicate() {}
203203
)
204204
}
205205

206+
#[test]
207+
fn doctest_convert_integer_literal() {
208+
check_doc_test(
209+
"convert_integer_literal",
210+
r#####"
211+
const _: i32 = 10<|>;
212+
"#####,
213+
r#####"
214+
const _: i32 = 0b1010;
215+
"#####,
216+
)
217+
}
218+
206219
#[test]
207220
fn doctest_convert_to_guarded_return() {
208221
check_doc_test(

0 commit comments

Comments
 (0)