Skip to content

Commit 383f986

Browse files
committed
Add create unknown module code action
1 parent 994915e commit 383f986

8 files changed

+509
-58
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -540,6 +540,15 @@
540540

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

543+
- The language server now offers a code action to create unknown modules
544+
when an import is added for a module that doesn't exist.
545+
546+
For example, if `import wobble/woo` is added to `src/wiggle.gleam`,
547+
then a code action to create `src/wobble/woo.gleam` will be presented
548+
when triggered over `import wobble/woo`.
549+
550+
([Cory Forsstrom](https://github.com/tarkah))
551+
543552
### Formatter
544553

545554
- The formatter now removes needless multiple negations that are safe to remove.

compiler-core/src/language_server/code_action.rs

Lines changed: 125 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,24 +10,29 @@ use crate::{
1010
TypedExpr, TypedModuleConstant, TypedPattern, TypedPipelineAssignment,
1111
TypedRecordConstructor, TypedStatement, TypedUse, visit::Visit as _,
1212
},
13-
build::{Located, Module},
13+
build::{Located, Module, Origin},
1414
config::PackageConfig,
1515
exhaustiveness::CompiledCase,
1616
io::{BeamCompiler, CommandExecutor, FileSystemReader, FileSystemWriter},
17-
language_server::{edits, reference::FindVariableReferences},
17+
language_server::{edits, lsp_range_to_src_span, reference::FindVariableReferences},
1818
line_numbers::LineNumbers,
1919
parse::{extra::ModuleExtra, lexer::str_to_keyword},
20+
paths::ProjectPaths,
2021
strings::to_snake_case,
2122
type_::{
22-
self, FieldMap, ModuleValueConstructor, Type, TypeVar, TypedCallArg, ValueConstructor,
23+
self, Error as TypeError, FieldMap, ModuleValueConstructor, Type, TypeVar, TypedCallArg,
24+
ValueConstructor,
2325
error::{ModuleSuggestion, VariableDeclaration, VariableOrigin},
2426
printer::Printer,
2527
},
2628
};
2729
use ecow::{EcoString, eco_format};
2830
use im::HashMap;
2931
use itertools::Itertools;
30-
use lsp_types::{CodeAction, CodeActionKind, CodeActionParams, Position, Range, TextEdit, Url};
32+
use lsp_types::{
33+
CodeAction, CodeActionKind, CodeActionParams, CreateFile, CreateFileOptions,
34+
DocumentChangeOperation, DocumentChanges, Position, Range, ResourceOp, TextEdit, Url,
35+
};
3136
use vec1::{Vec1, vec1};
3237

3338
use super::{
@@ -46,7 +51,7 @@ pub struct CodeActionBuilder {
4651
}
4752

4853
impl CodeActionBuilder {
49-
pub fn new(title: &str) -> Self {
54+
pub fn new(title: impl ToString) -> Self {
5055
Self {
5156
action: CodeAction {
5257
title: title.to_string(),
@@ -76,6 +81,15 @@ impl CodeActionBuilder {
7681
self
7782
}
7883

84+
pub fn document_changes(mut self, changes: DocumentChanges) -> Self {
85+
let mut edit = self.action.edit.take().unwrap_or_default();
86+
87+
edit.document_changes = Some(changes);
88+
89+
self.action.edit = Some(edit);
90+
self
91+
}
92+
7993
pub fn preferred(mut self, is_preferred: bool) -> Self {
8094
self.action.is_preferred = Some(is_preferred);
8195
self
@@ -1571,7 +1585,7 @@ impl<'a> QualifiedToUnqualifiedImportSecondPass<'a> {
15711585
}
15721586
self.edit_import();
15731587
let mut action = Vec::with_capacity(1);
1574-
CodeActionBuilder::new(&format!(
1588+
CodeActionBuilder::new(format!(
15751589
"Unqualify {}.{}",
15761590
self.qualified_constructor.used_name, self.qualified_constructor.constructor
15771591
))
@@ -1959,7 +1973,7 @@ impl<'a> UnqualifiedToQualifiedImportSecondPass<'a> {
19591973
constructor,
19601974
..
19611975
} = self.unqualified_constructor;
1962-
CodeActionBuilder::new(&format!(
1976+
CodeActionBuilder::new(format!(
19631977
"Qualify {} as {}.{}",
19641978
constructor.used_name(),
19651979
module_name,
@@ -7188,7 +7202,7 @@ impl<'a> FixBinaryOperation<'a> {
71887202
self.edits.replace(location, replacement.name().into());
71897203

71907204
let mut action = Vec::with_capacity(1);
7191-
CodeActionBuilder::new(&format!("Use `{}`", replacement.name()))
7205+
CodeActionBuilder::new(format!("Use `{}`", replacement.name()))
71927206
.kind(CodeActionKind::REFACTOR_REWRITE)
71937207
.changes(self.params.text_document.uri.clone(), self.edits.edits)
71947208
.preferred(true)
@@ -7271,7 +7285,7 @@ impl<'a> FixTruncatedBitArraySegment<'a> {
72717285
.replace(truncation.value_location, replacement.clone());
72727286

72737287
let mut action = Vec::with_capacity(1);
7274-
CodeActionBuilder::new(&format!("Replace with `{replacement}`"))
7288+
CodeActionBuilder::new(format!("Replace with `{replacement}`"))
72757289
.kind(CodeActionKind::REFACTOR_REWRITE)
72767290
.changes(self.params.text_document.uri.clone(), self.edits.edits)
72777291
.preferred(true)
@@ -8196,3 +8210,105 @@ impl<'ast> ast::visit::Visit<'ast> for RemoveUnreachableBranches<'ast> {
81968210
ast::visit::visit_typed_expr_case(self, location, type_, subjects, clauses, compiled_case);
81978211
}
81988212
}
8213+
8214+
/// Code action to create unknown modules when an import is added for a
8215+
/// module that doesn't exist.
8216+
///
8217+
/// For example, if `import wobble/woo` is added to `src/wiggle.gleam`,
8218+
/// then a code action to create `src/wobble/woo.gleam` will be presented
8219+
/// when triggered over `import wobble/woo`.
8220+
pub struct CreateUnknownModule<'a> {
8221+
module: &'a Module,
8222+
lines: &'a LineNumbers,
8223+
params: &'a CodeActionParams,
8224+
paths: &'a ProjectPaths,
8225+
error: &'a Option<Error>,
8226+
}
8227+
8228+
impl<'a> CreateUnknownModule<'a> {
8229+
pub fn new(
8230+
module: &'a Module,
8231+
lines: &'a LineNumbers,
8232+
params: &'a CodeActionParams,
8233+
paths: &'a ProjectPaths,
8234+
error: &'a Option<Error>,
8235+
) -> Self {
8236+
Self {
8237+
module,
8238+
lines,
8239+
params,
8240+
paths,
8241+
error,
8242+
}
8243+
}
8244+
8245+
pub fn code_actions(self) -> Vec<CodeAction> {
8246+
struct UnknownModule<'a> {
8247+
name: &'a EcoString,
8248+
location: &'a SrcSpan,
8249+
}
8250+
8251+
let mut actions = vec![];
8252+
8253+
// This code action can be derived from UnknownModule type errors. If those
8254+
// errors don't exist, there are no actions to add.
8255+
let Some(Error::Type { errors, .. }) = self.error else {
8256+
return actions;
8257+
};
8258+
8259+
// Span of the code action so we can check if it exists within the span of
8260+
// the UnkownModule type error
8261+
let code_action_span = lsp_range_to_src_span(self.params.range, self.lines);
8262+
8263+
// Origin directory we can build the new module path from
8264+
let origin_directory = match self.module.origin {
8265+
Origin::Src => self.paths.src_directory(),
8266+
Origin::Test => self.paths.test_directory(),
8267+
Origin::Dev => self.paths.dev_directory(),
8268+
};
8269+
8270+
// Filter for any UnknownModule type errors
8271+
let unknown_modules = errors.iter().filter_map(|error| {
8272+
if let TypeError::UnknownModule { name, location, .. } = error {
8273+
return Some(UnknownModule { name, location });
8274+
}
8275+
8276+
None
8277+
});
8278+
8279+
// For each UnknownModule type error, check to see if it contains the
8280+
// incoming code action & if so, add a document change to create the module
8281+
for unknown_module in unknown_modules {
8282+
// Was this code action triggered within the UnknownModule error?
8283+
let error_contains_action = unknown_module.location.contains(code_action_span.start)
8284+
&& unknown_module.location.contains(code_action_span.end);
8285+
8286+
if !error_contains_action {
8287+
continue;
8288+
}
8289+
8290+
let uri = url_from_path(&format!("{origin_directory}/{}.gleam", unknown_module.name))
8291+
.expect("origin directory is absolute");
8292+
8293+
CodeActionBuilder::new(format!(
8294+
"Create {}/{}.gleam",
8295+
self.module.origin.folder_name(),
8296+
unknown_module.name
8297+
))
8298+
.kind(CodeActionKind::QUICKFIX)
8299+
.document_changes(DocumentChanges::Operations(vec![
8300+
DocumentChangeOperation::Op(ResourceOp::Create(CreateFile {
8301+
uri,
8302+
options: Some(CreateFileOptions {
8303+
overwrite: Some(false),
8304+
ignore_if_exists: Some(true),
8305+
}),
8306+
annotation_id: None,
8307+
})),
8308+
]))
8309+
.push_to(&mut actions);
8310+
}
8311+
8312+
actions
8313+
}
8314+
}

compiler-core/src/language_server/engine.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ use super::{
4444
DownloadDependencies, MakeLocker,
4545
code_action::{
4646
AddAnnotations, CodeActionBuilder, ConvertFromUse, ConvertToFunctionCall, ConvertToPipe,
47-
ConvertToUse, ExpandFunctionCapture, ExtractConstant, ExtractVariable,
47+
ConvertToUse, CreateUnknownModule, ExpandFunctionCapture, ExtractConstant, ExtractVariable,
4848
FillInMissingLabelledArgs, FillUnusedFields, FixBinaryOperation,
4949
FixTruncatedBitArraySegment, GenerateDynamicDecoder, GenerateFunction, GenerateJsonEncoder,
5050
GenerateVariant, InlineVariable, InterpolateString, LetAssertToCase, PatternMatchOnValue,
@@ -457,6 +457,10 @@ where
457457
)
458458
.code_actions();
459459
AddAnnotations::new(module, &lines, &params).code_action(&mut actions);
460+
actions.extend(
461+
CreateUnknownModule::new(module, &lines, &params, &this.paths, &this.error)
462+
.code_actions(),
463+
);
460464
Ok(if actions.is_empty() {
461465
None
462466
} else {
@@ -1543,7 +1547,7 @@ fn code_action_fix_names(
15431547
new_text: correction.to_string(),
15441548
};
15451549

1546-
CodeActionBuilder::new(&format!("Rename to {correction}"))
1550+
CodeActionBuilder::new(format!("Rename to {correction}"))
15471551
.kind(lsp_types::CodeActionKind::QUICKFIX)
15481552
.changes(uri.clone(), vec![edit])
15491553
.preferred(true)

0 commit comments

Comments
 (0)