Skip to content

Commit 48c4bc7

Browse files
add code action to convert between bases
1 parent 4e817c2 commit 48c4bc7

31 files changed

Lines changed: 627 additions & 8 deletions

File tree

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,20 @@
134134

135135
([Giacomo Cavalieri](https://github.com/giacomocavalieri))
136136

137+
- The language server now offers a code action to rewrite integers in a
138+
different base. For example:
139+
140+
```gleam
141+
pub fn lucky_number() {
142+
0b1011
143+
//^^^^^^ Hovering this
144+
}
145+
```
146+
147+
The language server is going to show code actions to rewrite it as `11`,
148+
`0o13`, or `0xB`.
149+
([Giacomo Cavalieri](https://github.com/giacomocavalieri))
150+
137151
- The "remove unreachable patterns" code action can now be triggered on
138152
unreachable alternative patterns of a case expression.
139153
([Giacomo Cavalieri](https://github.com/giacomocavalieri))

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

compiler-core/src/ast/visit.rs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -114,9 +114,10 @@ pub trait Visit<'ast> {
114114
&mut self,
115115
location: &'ast SrcSpan,
116116
type_: &'ast Arc<Type>,
117-
value: &'ast EcoString,
117+
string_value: &'ast EcoString,
118+
int_value: &'ast BigInt,
118119
) {
119-
visit_typed_expr_int(self, location, type_, value);
120+
visit_typed_expr_int(self, location, type_, string_value, int_value);
120121
}
121122

122123
fn visit_typed_expr_float(
@@ -1261,8 +1262,8 @@ where
12611262
location,
12621263
type_,
12631264
value,
1264-
int_value: _,
1265-
} => v.visit_typed_expr_int(location, type_, value),
1265+
int_value,
1266+
} => v.visit_typed_expr_int(location, type_, value, int_value),
12661267
TypedExpr::Float {
12671268
location,
12681269
type_,
@@ -1428,7 +1429,8 @@ pub fn visit_typed_expr_int<'a, V>(
14281429
_v: &mut V,
14291430
_location: &'a SrcSpan,
14301431
_type_: &'a Arc<Type>,
1431-
_value: &'a EcoString,
1432+
_string_value: &'a EcoString,
1433+
_int_value: &'a BigInt,
14321434
) where
14331435
V: Visit<'a> + ?Sized,
14341436
{

language-server/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ vec1.workspace = true
2626
serde.workspace = true
2727
tracing.workspace = true
2828
toml.workspace = true
29+
num-bigint.workspace = true
2930

3031
[dev-dependencies]
3132
insta.workspace = true

language-server/src/code_action.rs

Lines changed: 122 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// SPDX-License-Identifier: Apache-2.0
22
// SPDX-FileCopyrightText: 2023 The Gleam contributors
33

4-
use std::{collections::HashSet, iter, sync::Arc};
4+
use std::{collections::HashSet, iter, ops::Neg, sync::Arc};
55

66
use ecow::{EcoString, eco_format};
77
use gleam_core::{
@@ -36,6 +36,7 @@ use lsp_types::{
3636
CodeAction, CodeActionKind, CodeActionParams, CreateFile, CreateFileOptions, DocumentChange,
3737
Position, Range, TextEdit, Uri as Url,
3838
};
39+
use num_bigint::BigInt;
3940
use vec1::{Vec1, vec1};
4041

4142
use crate::engine::{completely_within, position_within};
@@ -13099,3 +13100,123 @@ impl<'a> ConvertBetweenDocAndRegularComment<'a> {
1309913100
})
1310013101
}
1310113102
}
13103+
13104+
/// Code action to convert integers to a different base from the current one.
13105+
///
13106+
pub struct ConvertIntToDifferentBase<'a> {
13107+
module: &'a Module,
13108+
params: &'a CodeActionParams,
13109+
edits: TextEdits<'a>,
13110+
/// This is gonna hold the base the hovered integer is written in, and its
13111+
/// decimal value. And its position in the source code.
13112+
int: Option<(SrcSpan, Base, BigInt)>,
13113+
}
13114+
13115+
enum Base {
13116+
Binary,
13117+
Octal,
13118+
Decimal,
13119+
Hexadecimal,
13120+
}
13121+
13122+
impl<'a> ConvertIntToDifferentBase<'a> {
13123+
pub fn new(
13124+
module: &'a Module,
13125+
line_numbers: &'a LineNumbers,
13126+
params: &'a CodeActionParams,
13127+
) -> Self {
13128+
Self {
13129+
module,
13130+
params,
13131+
edits: TextEdits::new(line_numbers),
13132+
int: None,
13133+
}
13134+
}
13135+
13136+
pub fn code_actions(mut self) -> Vec<CodeAction> {
13137+
self.visit_typed_module(&self.module.ast);
13138+
13139+
let Some((location, base, int)) = self.int else {
13140+
return vec![];
13141+
};
13142+
13143+
let missing_bases = match base {
13144+
Base::Binary => [Base::Decimal, Base::Octal, Base::Hexadecimal],
13145+
Base::Octal => [Base::Decimal, Base::Binary, Base::Hexadecimal],
13146+
Base::Decimal => [Base::Binary, Base::Octal, Base::Hexadecimal],
13147+
Base::Hexadecimal => [Base::Decimal, Base::Binary, Base::Octal],
13148+
};
13149+
13150+
let is_negative = int < BigInt::ZERO;
13151+
let int = if is_negative { int.neg() } else { int };
13152+
13153+
let mut action = Vec::with_capacity(3);
13154+
for base in missing_bases {
13155+
let minus = if is_negative { "-" } else { "" };
13156+
let converted_number = match base {
13157+
Base::Binary => format!("{minus}0b{:b}", int),
13158+
Base::Octal => format!("{minus}0o{:o}", int),
13159+
Base::Decimal => format!("{minus}{}", int),
13160+
Base::Hexadecimal => format!("{minus}0x{:x}", int),
13161+
};
13162+
let title = format!("Convert to `{converted_number}`");
13163+
self.edits.replace(location, converted_number);
13164+
13165+
CodeActionBuilder::new(&title)
13166+
.kind(CodeActionKind::RefactorRewrite)
13167+
.changes(
13168+
self.params.text_document.uri.clone(),
13169+
self.edits.edits.drain(..).collect(),
13170+
)
13171+
.preferred(false)
13172+
.push_to(&mut action);
13173+
}
13174+
action
13175+
}
13176+
}
13177+
13178+
impl<'ast> ast::visit::Visit<'ast> for ConvertIntToDifferentBase<'ast> {
13179+
fn visit_typed_function(&mut self, fun: &'ast TypedFunction) {
13180+
// We skip all the functions the cursor is not inside of.
13181+
// This is gonna make it faster to find the int we're hovering, if any.
13182+
let fun_range = self.edits.src_span_to_lsp_range(fun.full_location());
13183+
if within(self.params.range, fun_range) {
13184+
ast::visit::visit_typed_function(self, fun);
13185+
}
13186+
}
13187+
13188+
fn visit_typed_expr(&mut self, expr: &'ast TypedExpr) {
13189+
// We skip all the expression's the cursor is not inside of.
13190+
// This is gonna make it faster to find the int we're hovering, if any.
13191+
let expression_range = self.edits.src_span_to_lsp_range(expr.location());
13192+
if within(self.params.range, expression_range) {
13193+
ast::visit::visit_typed_expr(self, expr);
13194+
}
13195+
}
13196+
13197+
fn visit_typed_expr_int(
13198+
&mut self,
13199+
location: &'ast SrcSpan,
13200+
_type_: &'ast Arc<Type>,
13201+
string_value: &'ast EcoString,
13202+
int_value: &'ast BigInt,
13203+
) {
13204+
let int_range = self.edits.src_span_to_lsp_range(*location);
13205+
if !within(self.params.range, int_range) {
13206+
return;
13207+
}
13208+
13209+
let string_value = string_value.trim_start_matches('-');
13210+
let base = if string_value.starts_with("0b") {
13211+
Base::Binary
13212+
} else if string_value.starts_with("0o") {
13213+
Base::Octal
13214+
} else if string_value.starts_with("0x") {
13215+
Base::Hexadecimal
13216+
} else {
13217+
Base::Decimal
13218+
};
13219+
13220+
self.int = Some((*location, base, int_value.clone()))
13221+
}
13222+
}

language-server/src/engine.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@ use std::{
4040

4141
use crate::{
4242
code_action::{
43-
DiscardUnusedVariable, RemoveRedundantRecordUpdate, ReplaceUnderscoreWithType,
44-
code_action_fix_deprecated_pipe, type_errors_for_module,
43+
ConvertIntToDifferentBase, DiscardUnusedVariable, RemoveRedundantRecordUpdate,
44+
ReplaceUnderscoreWithType, code_action_fix_deprecated_pipe, type_errors_for_module,
4545
},
4646
reference::find_module_references_in_module,
4747
rename::{rename_module_alias, rename_module_occurrences, rename_type_variable},
@@ -559,6 +559,7 @@ where
559559
actions.extend(
560560
ConvertBetweenDocAndRegularComment::new(module, &lines, &params).code_actions(),
561561
);
562+
actions.extend(ConvertIntToDifferentBase::new(module, &lines, &params).code_actions());
562563

563564
actions.sort_by_key(|one| {
564565
let preferred_key = if one.is_preferred == Some(true) { 0 } else { 1 };

0 commit comments

Comments
 (0)