Skip to content

Commit 5a5e1cd

Browse files
committed
Add create unknown module code action
1 parent f1f45d1 commit 5a5e1cd

8 files changed

+509
-58
lines changed

CHANGELOG.md

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

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

294+
- The language server now offers a code action to create unknown modules
295+
when an import is added for a module that doesn't exist.
296+
297+
For example, if `import wobble/woo` is added to `src/wiggle.gleam`,
298+
then a code action to create `src/wobble/woo.gleam` will be presented
299+
when triggered over `import wobble/woo`.
300+
301+
([Cory Forsstrom](https://github.com/tarkah))
302+
294303
### Formatter
295304

296305
- 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
TypedPattern, TypedPipelineAssignment, TypedRecordConstructor, TypedStatement, TypedUse,
1111
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,
@@ -6970,7 +6984,7 @@ impl<'a> FixBinaryOperation<'a> {
69706984
self.edits.replace(location, replacement.name().into());
69716985

69726986
let mut action = Vec::with_capacity(1);
6973-
CodeActionBuilder::new(&format!("Use `{}`", replacement.name()))
6987+
CodeActionBuilder::new(format!("Use `{}`", replacement.name()))
69746988
.kind(CodeActionKind::REFACTOR_REWRITE)
69756989
.changes(self.params.text_document.uri.clone(), self.edits.edits)
69766990
.preferred(true)
@@ -7053,7 +7067,7 @@ impl<'a> FixTruncatedBitArraySegment<'a> {
70537067
.replace(truncation.value_location, replacement.clone());
70547068

70557069
let mut action = Vec::with_capacity(1);
7056-
CodeActionBuilder::new(&format!("Replace with `{replacement}`"))
7070+
CodeActionBuilder::new(format!("Replace with `{replacement}`"))
70577071
.kind(CodeActionKind::REFACTOR_REWRITE)
70587072
.changes(self.params.text_document.uri.clone(), self.edits.edits)
70597073
.preferred(true)
@@ -7895,3 +7909,105 @@ fn single_expression(expression: &TypedExpr) -> Option<&TypedExpr> {
78957909
expression => Some(expression),
78967910
}
78977911
}
7912+
7913+
/// Code action to create unknown modules when an import is added for a
7914+
/// module that doesn't exist.
7915+
///
7916+
/// For example, if `import wobble/woo` is added to `src/wiggle.gleam`,
7917+
/// then a code action to create `src/wobble/woo.gleam` will be presented
7918+
/// when triggered over `import wobble/woo`.
7919+
pub struct CreateUnknownModule<'a> {
7920+
module: &'a Module,
7921+
lines: &'a LineNumbers,
7922+
params: &'a CodeActionParams,
7923+
paths: &'a ProjectPaths,
7924+
error: &'a Option<Error>,
7925+
}
7926+
7927+
impl<'a> CreateUnknownModule<'a> {
7928+
pub fn new(
7929+
module: &'a Module,
7930+
lines: &'a LineNumbers,
7931+
params: &'a CodeActionParams,
7932+
paths: &'a ProjectPaths,
7933+
error: &'a Option<Error>,
7934+
) -> Self {
7935+
Self {
7936+
module,
7937+
lines,
7938+
params,
7939+
paths,
7940+
error,
7941+
}
7942+
}
7943+
7944+
pub fn code_actions(self) -> Vec<CodeAction> {
7945+
struct UnknownModule<'a> {
7946+
name: &'a EcoString,
7947+
location: &'a SrcSpan,
7948+
}
7949+
7950+
let mut actions = vec![];
7951+
7952+
// This code action can be derived from UnknownModule type errors. If those
7953+
// errors don't exist, there are no actions to add.
7954+
let Some(Error::Type { errors, .. }) = self.error else {
7955+
return actions;
7956+
};
7957+
7958+
// Span of the code action so we can check if it exists within the span of
7959+
// the UnkownModule type error
7960+
let code_action_span = lsp_range_to_src_span(self.params.range, self.lines);
7961+
7962+
// Origin directory we can build the new module path from
7963+
let origin_directory = match self.module.origin {
7964+
Origin::Src => self.paths.src_directory(),
7965+
Origin::Test => self.paths.test_directory(),
7966+
Origin::Dev => self.paths.dev_directory(),
7967+
};
7968+
7969+
// Filter for any UnknownModule type errors
7970+
let unknown_modules = errors.iter().filter_map(|error| {
7971+
if let TypeError::UnknownModule { name, location, .. } = error {
7972+
return Some(UnknownModule { name, location });
7973+
}
7974+
7975+
None
7976+
});
7977+
7978+
// For each UnknownModule type error, check to see if it contains the
7979+
// incoming code action & if so, add a document change to create the module
7980+
for unknown_module in unknown_modules {
7981+
// Was this code action triggered within the UnknownModule error?
7982+
let error_contains_action = unknown_module.location.contains(code_action_span.start)
7983+
&& unknown_module.location.contains(code_action_span.end);
7984+
7985+
if !error_contains_action {
7986+
continue;
7987+
}
7988+
7989+
let uri = url_from_path(&format!("{origin_directory}/{}.gleam", unknown_module.name))
7990+
.expect("origin directory is absolute");
7991+
7992+
CodeActionBuilder::new(format!(
7993+
"Create {}/{}.gleam",
7994+
self.module.origin.folder_name(),
7995+
unknown_module.name
7996+
))
7997+
.kind(CodeActionKind::QUICKFIX)
7998+
.document_changes(DocumentChanges::Operations(vec![
7999+
DocumentChangeOperation::Op(ResourceOp::Create(CreateFile {
8000+
uri,
8001+
options: Some(CreateFileOptions {
8002+
overwrite: Some(false),
8003+
ignore_if_exists: Some(true),
8004+
}),
8005+
annotation_id: None,
8006+
})),
8007+
]))
8008+
.push_to(&mut actions);
8009+
}
8010+
8011+
actions
8012+
}
8013+
}

compiler-core/src/language_server/engine.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ use super::{
4242
DownloadDependencies, MakeLocker,
4343
code_action::{
4444
AddAnnotations, CodeActionBuilder, ConvertFromUse, ConvertToFunctionCall, ConvertToPipe,
45-
ConvertToUse, ExpandFunctionCapture, ExtractConstant, ExtractVariable,
45+
ConvertToUse, CreateUnknownModule, ExpandFunctionCapture, ExtractConstant, ExtractVariable,
4646
FillInMissingLabelledArgs, FillUnusedFields, FixBinaryOperation,
4747
FixTruncatedBitArraySegment, GenerateDynamicDecoder, GenerateFunction, GenerateJsonEncoder,
4848
GenerateVariant, InlineVariable, InterpolateString, LetAssertToCase, PatternMatchOnValue,
@@ -451,6 +451,10 @@ where
451451
)
452452
.code_actions();
453453
AddAnnotations::new(module, &lines, &params).code_action(&mut actions);
454+
actions.extend(
455+
CreateUnknownModule::new(module, &lines, &params, &this.paths, &this.error)
456+
.code_actions(),
457+
);
454458
Ok(if actions.is_empty() {
455459
None
456460
} else {
@@ -1537,7 +1541,7 @@ fn code_action_fix_names(
15371541
new_text: correction.to_string(),
15381542
};
15391543

1540-
CodeActionBuilder::new(&format!("Rename to {correction}"))
1544+
CodeActionBuilder::new(format!("Rename to {correction}"))
15411545
.kind(lsp_types::CodeActionKind::QUICKFIX)
15421546
.changes(uri.clone(), vec![edit])
15431547
.preferred(true)

0 commit comments

Comments
 (0)