Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LSP Code Action: Generate dynamic decoder #4106

Merged
merged 10 commits into from
Dec 28, 2024
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,34 @@

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

- The language server now suggests a code action to generate a dynamic decoder
for a custom type. For example, this code:

```gleam
pub type Person {
Person(name: String, age: Int)
}
```

Will become:

```gleam
import gleam/dynamic/decode

pub type Person {
Person(name: String, age: Int)
}

fn person_decoder() -> decode.Decoder(Person) {
use name <- decode.field("name", decode.string)
use age <- decode.field("age", decode.int)

decode.success(Person(name:, age:))
}
```

([Surya Rose](https://github.com/GearsDatapacks))

### Formatter

- The formatter now adds a `todo` inside empty blocks.
Expand Down
1 change: 1 addition & 0 deletions compiler-core/src/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -729,6 +729,7 @@ pub struct ModuleConstant<T, ConstantRecordTag> {
}

pub type UntypedCustomType = CustomType<()>;
pub type TypedCustomType = CustomType<Arc<Type>>;

#[derive(Debug, Clone, PartialEq, Eq)]
/// A newly defined type with one or more constructors.
Expand Down
18 changes: 14 additions & 4 deletions compiler-core/src/ast/visit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,9 @@ use crate::type_::Type;

use super::{
untyped::FunctionLiteralKind, AssignName, BinOp, BitArrayOption, CallArg, Definition, Pattern,
SrcSpan, Statement, TodoKind, TypeAst, TypedArg, TypedAssignment, TypedClause, TypedDefinition,
TypedExpr, TypedExprBitArraySegment, TypedFunction, TypedModule, TypedModuleConstant,
TypedPattern, TypedPatternBitArraySegment, TypedStatement, TypedUse,
SrcSpan, Statement, TodoKind, TypeAst, TypedArg, TypedAssignment, TypedClause, TypedCustomType,
TypedDefinition, TypedExpr, TypedExprBitArraySegment, TypedFunction, TypedModule,
TypedModuleConstant, TypedPattern, TypedPatternBitArraySegment, TypedStatement, TypedUse,
};

pub trait Visit<'ast> {
Expand All @@ -75,6 +75,10 @@ pub trait Visit<'ast> {
visit_typed_module_constant(self, constant);
}

fn visit_typed_custom_type(&mut self, custom_type: &'ast TypedCustomType) {
visit_typed_custom_type(self, custom_type);
}

fn visit_typed_expr(&mut self, expr: &'ast TypedExpr) {
visit_typed_expr(self, expr);
}
Expand Down Expand Up @@ -493,7 +497,7 @@ where
match def {
Definition::Function(fun) => v.visit_typed_function(fun),
Definition::TypeAlias(_typealias) => { /* TODO */ }
Definition::CustomType(_custom_type) => { /* TODO */ }
Definition::CustomType(custom_type) => v.visit_typed_custom_type(custom_type),
Definition::Import(_import) => { /* TODO */ }
Definition::ModuleConstant(constant) => v.visit_typed_module_constant(constant),
}
Expand Down Expand Up @@ -596,6 +600,12 @@ where
{
}

pub fn visit_typed_custom_type<'a, V>(_v: &mut V, _custom_type: &'a TypedCustomType)
where
V: Visit<'a> + ?Sized,
{
}

pub fn visit_typed_expr<'a, V>(v: &mut V, node: &'a TypedExpr)
where
V: Visit<'a> + ?Sized,
Expand Down
294 changes: 292 additions & 2 deletions compiler-core/src/language_server/code_action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,13 @@ use crate::{
type_::{
self,
error::{ModuleSuggestion, VariableOrigin},
printer::Printer,
printer::{Names, Printer},
FieldMap, ModuleValueConstructor, Type, TypedCallArg,
},
Error,
Error, STDLIB_PACKAGE_NAME,
};
use ecow::{eco_format, EcoString};
use heck::ToSnakeCase;
use im::HashMap;
use itertools::Itertools;
use lsp_types::{CodeAction, CodeActionKind, CodeActionParams, Position, Range, TextEdit, Url};
Expand Down Expand Up @@ -2875,3 +2876,292 @@ impl<'ast> ast::visit::Visit<'ast> for VariablesNames {
let _ = self.names.insert(name.clone());
}
}

/// Builder for code action to apply the "generate dynamic decoder action.
///
pub struct GenerateDynamicDecoder<'a> {
module: &'a Module,
params: &'a CodeActionParams,
edits: TextEdits<'a>,
printer: Printer<'a>,
actions: &'a mut Vec<CodeAction>,
}

const DECODE_MODULE: &str = "gleam/dynamic/decode";

impl<'a> GenerateDynamicDecoder<'a> {
pub fn new(
module: &'a Module,
line_numbers: &'a LineNumbers,
params: &'a CodeActionParams,
actions: &'a mut Vec<CodeAction>,
) -> Self {
let printer = Printer::new(&module.ast.names);
Self {
module,
params,
edits: TextEdits::new(line_numbers),
printer,
actions,
}
}

pub fn code_actions(&mut self) {
self.visit_typed_module(&self.module.ast);
}
}

impl<'ast> ast::visit::Visit<'ast> for GenerateDynamicDecoder<'ast> {
fn visit_typed_custom_type(&mut self, custom_type: &'ast ast::TypedCustomType) {
let range = self.edits.src_span_to_lsp_range(custom_type.location);
if !overlaps(self.params.range, range) {
return;
}

// For now, we only generate dynamic decoders for types with one variant.
let constructor = match custom_type.constructors.as_slice() {
[constructor] => constructor,
_ => return,
};

let name = eco_format!("{}_decoder", custom_type.name.to_snake_case());

let Some(fields): Option<Vec<_>> = constructor
.arguments
.iter()
.map(|argument| {
Some(RecordField {
label: RecordLabel::Labeled(
argument.label.as_ref().map(|(_, name)| name.as_str())?,
),
type_: &argument.type_,
})
})
.collect()
else {
return;
};

let mut decoder_printer = DecoderPrinter::new(
&self.module.ast.names,
custom_type.name.clone(),
self.module.name.clone(),
);

let decoders = fields
.iter()
.map(|field| decoder_printer.decode_field(field, 2))
.join("\n");

let decoder_type = self.printer.print_type(&Type::Named {
publicity: ast::Publicity::Public,
package: STDLIB_PACKAGE_NAME.into(),
module: DECODE_MODULE.into(),
name: "Decoder".into(),
args: vec![],
inferred_variant: None,
});
let decode_module = self.printer.print_module(DECODE_MODULE);

let mut field_names = fields.iter().map(|field| field.label.variable_name());
let parameters = match custom_type.parameters.len() {
0 => EcoString::new(),
_ => eco_format!(
"({})",
custom_type
.parameters
.iter()
.map(|(_, name)| name)
.join(", ")
),
};

let function = format!(
"

fn {name}() -> {decoder_type}({type_name}{parameters}) {{
{decoders}

{decode_module}.success({constructor_name}({fields}:))
}}",
type_name = custom_type.name,
constructor_name = constructor.name,
fields = field_names.join(":, ")
);

self.edits.insert(custom_type.end_position, function);
maybe_import(&mut self.edits, self.module, DECODE_MODULE);

CodeActionBuilder::new("Generate dynamic decoder")
.kind(CodeActionKind::REFACTOR)
.preferred(false)
.changes(
self.params.text_document.uri.clone(),
std::mem::take(&mut self.edits.edits),
)
.push_to(self.actions);
}
}

fn maybe_import(edits: &mut TextEdits<'_>, module: &Module, module_name: &str) {
if module.ast.names.is_imported(module_name) {
return;
}

let first_import_pos = position_of_first_definition_if_import(module, edits.line_numbers);
let first_is_import = first_import_pos.is_some();
let import_location = first_import_pos.unwrap_or_default();
let after_import_newlines = add_newlines_after_import(
import_location,
first_is_import,
edits.line_numbers,
&module.code,
);

edits.edits.push(get_import_edit(
import_location,
module_name,
&after_import_newlines,
));
}

struct DecoderPrinter<'a> {
printer: Printer<'a>,
/// The name of the root type we are printing a decoder for
type_name: EcoString,
/// The module name of the root type we are printing a decoder for
type_module: EcoString,
}

struct RecordField<'a> {
label: RecordLabel<'a>,
type_: &'a Type,
}

enum RecordLabel<'a> {
Labeled(&'a str),
Unlabeled(usize),
}

impl RecordLabel<'_> {
fn field_key(&self) -> EcoString {
match self {
RecordLabel::Labeled(label) => eco_format!("\"{label}\""),
RecordLabel::Unlabeled(index) => {
eco_format!("{index}")
}
}
}

fn variable_name(&self) -> EcoString {
match self {
RecordLabel::Labeled(label) => (*label).into(),
RecordLabel::Unlabeled(mut index) => {
let mut characters = Vec::new();
let alphabet_length = 26;
let alphabet_offset = b'a';
loop {
let alphabet_index = (index % alphabet_length) as u8;
characters.push((alphabet_offset + alphabet_index) as char);
index /= alphabet_length;

if index == 0 {
break;
}
index -= 1;
}
characters.into_iter().rev().collect()
}
}
}
}

impl<'a> DecoderPrinter<'a> {
fn new(names: &'a Names, type_name: EcoString, type_module: EcoString) -> Self {
Self {
type_name,
type_module,
printer: Printer::new(names),
}
}

fn decoder_for(&mut self, type_: &Type, indent: usize) -> EcoString {
let module_name = self.printer.print_module(DECODE_MODULE);
if type_.is_bit_array() {
eco_format!("{module_name}.bit_array")
} else if type_.is_bool() {
eco_format!("{module_name}.bool")
} else if type_.is_float() {
eco_format!("{module_name}.float")
} else if type_.is_int() {
eco_format!("{module_name}.int")
} else if type_.is_string() {
eco_format!("{module_name}.string")
} else if let Some(types) = type_.tuple_types() {
let fields = types
.iter()
.enumerate()
.map(|(index, type_)| RecordField {
type_,
label: RecordLabel::Unlabeled(index),
})
.collect_vec();
let decoders = fields
.iter()
.map(|field| self.decode_field(field, indent + 2))
.join("\n");
let mut field_names = fields.iter().map(|field| field.label.variable_name());

eco_format!(
"{{
{decoders}

{indent} {module_name}.success(#({fields}))
{indent}}}",
fields = field_names.join(", "),
indent = " ".repeat(indent)
)
} else {
let type_information = type_.named_type_information();
let type_information = type_information.as_ref().map(|(module, name, arguments)| {
(module.as_str(), name.as_str(), arguments.as_slice())
});

match type_information {
Some(("gleam/dynamic", "Dynamic", _)) => eco_format!("{module_name}.dynamic"),
Some(("gleam", "List", [element])) => {
eco_format!("{module_name}.list({})", self.decoder_for(element, indent))
}
Some(("gleam/option", "Option", [some])) => {
eco_format!("{module_name}.optional({})", self.decoder_for(some, indent))
}
Some(("gleam/dict", "Dict", [key, value])) => {
eco_format!(
"{module_name}.dict({}, {})",
self.decoder_for(key, indent),
self.decoder_for(value, indent)
)
}
Some((module, name, _)) if module == self.type_module && name == self.type_name => {
eco_format!("{}_decoder()", name.to_snake_case())
}
_ => eco_format!(
r#"todo as "Decoder for {}""#,
self.printer.print_type(type_)
),
}
}
}

fn decode_field(&mut self, field: &RecordField<'_>, indent: usize) -> EcoString {
let decoder = self.decoder_for(field.type_, indent);

eco_format!(
r#"{indent}use {variable} <- {module}.field({field}, {decoder})"#,
indent = " ".repeat(indent),
variable = field.label.variable_name(),
field = field.label.field_key(),
module = self.printer.print_module(DECODE_MODULE)
)
}
}
Loading
Loading