|
1 | 1 | // SPDX-License-Identifier: Apache-2.0 |
2 | 2 | // SPDX-FileCopyrightText: 2023 The Gleam contributors |
3 | 3 |
|
4 | | -use std::{collections::HashSet, iter, sync::Arc}; |
| 4 | +use std::{collections::HashSet, iter, ops::Neg, sync::Arc}; |
5 | 5 |
|
6 | 6 | use ecow::{EcoString, eco_format}; |
7 | 7 | use gleam_core::{ |
@@ -36,6 +36,7 @@ use lsp_types::{ |
36 | 36 | CodeAction, CodeActionKind, CodeActionParams, CreateFile, CreateFileOptions, DocumentChange, |
37 | 37 | Position, Range, TextEdit, Uri as Url, |
38 | 38 | }; |
| 39 | +use num_bigint::BigInt; |
39 | 40 | use vec1::{Vec1, vec1}; |
40 | 41 |
|
41 | 42 | use crate::engine::{completely_within, position_within}; |
@@ -13099,3 +13100,123 @@ impl<'a> ConvertBetweenDocAndRegularComment<'a> { |
13099 | 13100 | }) |
13100 | 13101 | } |
13101 | 13102 | } |
| 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 | +} |
0 commit comments