Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
e42fc33
feat: add 'Annotate all top level type definitions' code action
ankddev Sep 30, 2025
c965a3e
chore: write tests + snapshots
ankddev Sep 30, 2025
eb0e350
chore: CHANGELOG entry
ankddev Sep 30, 2025
7bcbb8b
chore: format code sample in CHANGELOG
ankddev Sep 30, 2025
b1f03fb
chore: remove test and snapshot
ankddev Sep 30, 2025
464a0df
fix: typo in CHANGELOG
ankddev Sep 30, 2025
db55d88
refactor: don't visit expr_fn's in code action
ankddev Sep 30, 2025
a2d65a6
refactor: rename code action
ankddev Sep 30, 2025
8222962
test: write more tests for code action
ankddev Sep 30, 2025
0748109
fix(test): forgot to change selection text
ankddev Sep 30, 2025
17ddcad
fix(LS): make visiters ignore type variables from other definitions
ankddev Oct 1, 2025
600f776
refactor: don't visit entire AST of function
ankddev Oct 1, 2025
d6df4c1
fix(test): syntax error
ankddev Oct 1, 2025
9feaa08
test: add new test with partially annotated generic function
ankddev Oct 1, 2025
5586521
style: reformat code
ankddev Oct 1, 2025
d538983
fix: code action showed up for annotated functions
ankddev Oct 1, 2025
db9c434
fix(test): some errors while resolving conflicts
ankddev Oct 2, 2025
a41a1b2
feat: offer action on constant only if one doesn't fully annotated
ankddev Oct 23, 2025
f34c78f
feat: trigger code action on function only if some annotation omitted
ankddev Oct 23, 2025
fa0684f
fix: arguments is field and not method :)
ankddev Oct 23, 2025
c856e86
fix: forgot to collect filter result
ankddev Oct 23, 2025
9023167
refactor: use simpler method to get head location
ankddev Oct 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,41 @@
used on tuples.
([Giacomo Cavalieri](https://github.com/giacomocavalieri))

- The language server now offers code action to add type annotations to all
functions and constants. For example,

```gleam
pub const answer = 42

pub fn add(x, y) {
x + y
}

pub fn add_one(thing) {
// ^ Triggering "Annotate all top level definitions" code action here
let result = add(thing, 1)
result
}
```

Triggering the "Annotate all top level definitions" code action over
the name of function `add_one` would result in following code:

```gleam
pub const answer: Int = 42

pub fn add(x: Int, y: Int) -> Int {
x + y
}

pub fn add_one(thing: Int) -> Int {
let result = add(thing, 1)
result
}
```

([Andrey Kozhev](https://github.com/ankddev))

### Formatter

### Bug fixes
Expand Down
108 changes: 108 additions & 0 deletions compiler-core/src/language_server/code_action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1360,6 +1360,114 @@ impl<'a> AddAnnotations<'a> {
}
}

/// Code action to add type annotations to all top level definitions
///
pub struct AnnotateTopLevelDefinitions<'a> {
module: &'a Module,
params: &'a CodeActionParams,
edits: TextEdits<'a>,
is_hovering_definition: bool,
}

impl<'a> AnnotateTopLevelDefinitions<'a> {
pub fn new(
module: &'a Module,
line_numbers: &'a LineNumbers,
params: &'a CodeActionParams,
) -> Self {
Self {
module,
params,
edits: TextEdits::new(line_numbers),
is_hovering_definition: false,
}
}

pub fn code_actions(mut self) -> Vec<CodeAction> {
self.visit_typed_module(&self.module.ast);

// We only want to trigger the action if we're over one of the definition
// which is lacking some annotations in the module
if !self.is_hovering_definition || self.edits.edits.is_empty() {
return vec![];
};

let mut action = Vec::with_capacity(1);
CodeActionBuilder::new("Annotate all top level definitions")
.kind(CodeActionKind::REFACTOR_REWRITE)
.changes(self.params.text_document.uri.clone(), self.edits.edits)
.preferred(false)
.push_to(&mut action);
action
}
}

impl<'ast> ast::visit::Visit<'ast> for AnnotateTopLevelDefinitions<'_> {
fn visit_typed_module_constant(&mut self, constant: &'ast TypedModuleConstant) {
let code_action_range = self.edits.src_span_to_lsp_range(constant.location);

// We don't need to add an annotation if there already is one
if constant.annotation.is_some() {
return;
}

// We're hovering definition which needs some annotations
if overlaps(code_action_range, self.params.range) {
self.is_hovering_definition = true;
}

self.edits.insert(
constant.name_location.end,
format!(
": {}",
// Create new printer to ignore type variables from other definitions
Printer::new_without_type_variables(&self.module.ast.names)
.print_type(&constant.type_)
),
);
}

fn visit_typed_function(&mut self, fun: &'ast ast::TypedFunction) {
// Don't annotate already annotated arguments
let arguments_to_annotate = fun
.arguments
.iter()
.filter(|argument| argument.annotation.is_none())
.collect::<Vec<_>>();
let needs_return_annotation = fun.return_annotation.is_none();

if arguments_to_annotate.is_empty() && !needs_return_annotation {
return;
}

// Create new printer to ignore type variables from other definitions
let mut printer = Printer::new_without_type_variables(&self.module.ast.names);
collect_type_variables(&mut printer, fun);

let code_action_range = self.edits.src_span_to_lsp_range(fun.location);

if overlaps(code_action_range, self.params.range) {
self.is_hovering_definition = true;
}

// Annotate each argument separately
for argument in arguments_to_annotate {
self.edits.insert(
argument.location.end,
format!(": {}", printer.print_type(&argument.type_)),
);
}

// Annotate the return type if it isn't already annotated
if needs_return_annotation {
self.edits.insert(
fun.location.end,
format!(" -> {}", printer.print_type(&fun.return_type)),
);
}
}
}

struct TypeVariableCollector<'a, 'b> {
printer: &'a mut Printer<'b>,
}
Expand Down
8 changes: 5 additions & 3 deletions compiler-core/src/language_server/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,9 @@ use std::{collections::HashSet, sync::Arc};
use super::{
DownloadDependencies, MakeLocker,
code_action::{
AddAnnotations, CodeActionBuilder, ConvertFromUse, ConvertToFunctionCall, ConvertToPipe,
ConvertToUse, ExpandFunctionCapture, ExtractConstant, ExtractVariable,
FillInMissingLabelledArgs, FillUnusedFields, FixBinaryOperation,
AddAnnotations, AnnotateTopLevelDefinitions, CodeActionBuilder, ConvertFromUse,
ConvertToFunctionCall, ConvertToPipe, ConvertToUse, ExpandFunctionCapture, ExtractConstant,
ExtractVariable, FillInMissingLabelledArgs, FillUnusedFields, FixBinaryOperation,
FixTruncatedBitArraySegment, GenerateDynamicDecoder, GenerateFunction, GenerateJsonEncoder,
GenerateVariant, InlineVariable, InterpolateString, LetAssertToCase, PatternMatchOnValue,
RedundantTupleInCaseSubject, RemoveEchos, RemoveUnusedImports, UseLabelShorthandSyntax,
Expand Down Expand Up @@ -462,6 +462,8 @@ where
)
.code_actions();
AddAnnotations::new(module, &lines, &params).code_action(&mut actions);
actions
.extend(AnnotateTopLevelDefinitions::new(module, &lines, &params).code_actions());
Ok(if actions.is_empty() {
None
} else {
Expand Down
130 changes: 130 additions & 0 deletions compiler-core/src/language_server/tests/action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ const ASSIGN_UNUSED_RESULT: &str = "Assign unused Result value to `_`";
const ADD_MISSING_PATTERNS: &str = "Add missing patterns";
const ADD_ANNOTATION: &str = "Add type annotation";
const ADD_ANNOTATIONS: &str = "Add type annotations";
const ANNOTATE_TOP_LEVEL_DEFINITIONS: &str = "Annotate all top level definitions";
const CONVERT_FROM_USE: &str = "Convert from `use`";
const CONVERT_TO_USE: &str = "Convert to `use`";
const EXTRACT_VARIABLE: &str = "Extract variable";
Expand Down Expand Up @@ -10800,3 +10801,132 @@ fn wibble() -> Nil
find_position_of("wibble").to_selection()
);
}

#[test]
fn annotate_all_top_level_definitions_constant() {
assert_code_action!(
ANNOTATE_TOP_LEVEL_DEFINITIONS,
r#"
pub const answer = 42

pub fn add_two(thing) {
thing + 2
}

pub fn add_one(thing) {
thing + 1
}
"#,
find_position_of("const").select_until(find_position_of("="))
);
}

#[test]
fn annotate_all_top_level_definitions_function() {
assert_code_action!(
ANNOTATE_TOP_LEVEL_DEFINITIONS,
r#"
pub fn add_two(thing) {
thing + 2
}

pub fn add_one(thing) {
thing + 1
}
"#,
find_position_of("fn").select_until(find_position_of("("))
);
}

#[test]
fn annotate_all_top_level_definitions_already_annotated() {
assert_no_code_actions!(
ANNOTATE_TOP_LEVEL_DEFINITIONS,
r#"
pub const answer: Int = 42

pub fn add_two(thing: Int) -> Int {
thing + 2
}

pub fn add_one(thing: Int) -> Int {
thing + 1
}
"#,
find_position_of("fn").select_until(find_position_of("("))
);
}

#[test]
fn annotate_all_top_level_definitions_inside_body() {
assert_no_code_actions!(
ANNOTATE_TOP_LEVEL_DEFINITIONS,
r#"
pub fn add_one(thing) {
thing + 1
}
"#,
find_position_of("thing + 1").to_selection()
);
}

#[test]
fn annotate_all_top_level_definitions_partially_annotated() {
assert_code_action!(
ANNOTATE_TOP_LEVEL_DEFINITIONS,
r#"
pub const answer: Int = 42
pub const another_answer = 43

pub fn add_two(thing) -> Int {
thing + 2
}

pub fn add_one(thing: Int) {
thing + 1
}
"#,
find_position_of("fn").select_until(find_position_of("("))
);
}

#[test]
fn annotate_all_top_level_definitions_with_partially_annotated_generic_function() {
assert_code_action!(
ANNOTATE_TOP_LEVEL_DEFINITIONS,
r#"
pub fn wibble(a: a, b, c: c, d) {
todo
}
"#,
find_position_of("wibble").to_selection()
);
}

#[test]
fn annotate_all_top_level_definitions_with_two_generic_functions() {
assert_code_action!(
ANNOTATE_TOP_LEVEL_DEFINITIONS,
r#"
fn wibble(one) { todo }

fn wobble(other) { todo }
"#,
find_position_of("wobble").to_selection()
);
}

#[test]
fn annotate_all_top_level_definitions_with_constant_and_generic_functions() {
assert_code_action!(
ANNOTATE_TOP_LEVEL_DEFINITIONS,
r#"
const answer = 42

fn wibble(one) { todo }

fn wobble(other) { todo }
"#,
find_position_of("wobble").to_selection()
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
source: compiler-core/src/language_server/tests/action.rs
assertion_line: 2928
expression: "\npub const my_constant = 20\n\npub fn add_my_constant(value) {\n let result = value + my_constant\n result\n}\n"
snapshot_kind: text
---
----- BEFORE ACTION

pub const my_constant = 20
▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔

pub fn add_my_constant(value) {
▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔
let result = value + my_constant
▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔
result
▔▔▔▔▔▔▔▔
}


----- AFTER ACTION

pub const my_constant: Int = 20

pub fn add_my_constant(value: Int) -> Int {
let result = value + my_constant
result
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
source: compiler-core/src/language_server/tests/action.rs
expression: "\npub const answer = 42\n\npub fn add_two(thing) {\n thing + 2\n}\n\npub fn add_one(thing) {\n thing + 1\n}\n"
---
----- BEFORE ACTION

pub const answer = 42
▔▔▔▔▔▔▔▔▔▔▔▔▔↑

pub fn add_two(thing) {
thing + 2
}

pub fn add_one(thing) {
thing + 1
}


----- AFTER ACTION

pub const answer: Int = 42

pub fn add_two(thing: Int) -> Int {
thing + 2
}

pub fn add_one(thing: Int) -> Int {
thing + 1
}
Loading
Loading