Skip to content

Commit cff104a

Browse files
committed
Add create unknown module code action
1 parent 6b36ea4 commit cff104a

8 files changed

+509
-58
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,15 @@
2525
keyword of a variable to inline.
2626
([Giacomo Cavalieri](https://github.com/giacomocavalieri))
2727

28+
- The language server now offers a code action to create unknown modules
29+
when an import is added for a module that doesn't exist.
30+
31+
For example, if `import wobble/woo` is added to `src/wiggle.gleam`,
32+
then a code action to create `src/wobble/woo.gleam` will be presented
33+
when triggered over `import wobble/woo`.
34+
35+
([Cory Forsstrom](https://github.com/tarkah))
36+
2837
### Formatter
2938

3039
### Bug fixes

compiler-core/src/language_server/code_action.rs

Lines changed: 125 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,23 +10,28 @@ 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,
16-
language_server::{edits, reference::FindVariableReferences},
16+
language_server::{edits, lsp_range_to_src_span, reference::FindVariableReferences},
1717
line_numbers::LineNumbers,
1818
parse::{extra::ModuleExtra, lexer::str_to_keyword},
19+
paths::ProjectPaths,
1920
strings::to_snake_case,
2021
type_::{
21-
self, FieldMap, ModuleValueConstructor, Type, TypeVar, TypedCallArg, ValueConstructor,
22+
self, Error as TypeError, FieldMap, ModuleValueConstructor, Type, TypeVar, TypedCallArg,
23+
ValueConstructor,
2224
error::{ModuleSuggestion, VariableDeclaration, VariableOrigin},
2325
printer::Printer,
2426
},
2527
};
2628
use ecow::{EcoString, eco_format};
2729
use im::HashMap;
2830
use itertools::Itertools;
29-
use lsp_types::{CodeAction, CodeActionKind, CodeActionParams, Position, Range, TextEdit, Url};
31+
use lsp_types::{
32+
CodeAction, CodeActionKind, CodeActionParams, CreateFile, CreateFileOptions,
33+
DocumentChangeOperation, DocumentChanges, Position, Range, ResourceOp, TextEdit, Url,
34+
};
3035
use vec1::{Vec1, vec1};
3136

3237
use super::{
@@ -45,7 +50,7 @@ pub struct CodeActionBuilder {
4550
}
4651

4752
impl CodeActionBuilder {
48-
pub fn new(title: &str) -> Self {
53+
pub fn new(title: impl ToString) -> Self {
4954
Self {
5055
action: CodeAction {
5156
title: title.to_string(),
@@ -75,6 +80,15 @@ impl CodeActionBuilder {
7580
self
7681
}
7782

83+
pub fn document_changes(mut self, changes: DocumentChanges) -> Self {
84+
let mut edit = self.action.edit.take().unwrap_or_default();
85+
86+
edit.document_changes = Some(changes);
87+
88+
self.action.edit = Some(edit);
89+
self
90+
}
91+
7892
pub fn preferred(mut self, is_preferred: bool) -> Self {
7993
self.action.is_preferred = Some(is_preferred);
8094
self
@@ -1623,7 +1637,7 @@ impl<'a> QualifiedToUnqualifiedImportSecondPass<'a> {
16231637
}
16241638
self.edit_import();
16251639
let mut action = Vec::with_capacity(1);
1626-
CodeActionBuilder::new(&format!(
1640+
CodeActionBuilder::new(format!(
16271641
"Unqualify {}.{}",
16281642
self.qualified_constructor.used_name, self.qualified_constructor.constructor
16291643
))
@@ -2013,7 +2027,7 @@ impl<'a> UnqualifiedToQualifiedImportSecondPass<'a> {
20132027
constructor,
20142028
..
20152029
} = self.unqualified_constructor;
2016-
CodeActionBuilder::new(&format!(
2030+
CodeActionBuilder::new(format!(
20172031
"Qualify {} as {}.{}",
20182032
constructor.used_name(),
20192033
module_name,
@@ -7337,7 +7351,7 @@ impl<'a> FixBinaryOperation<'a> {
73377351
self.edits.replace(location, replacement.name().into());
73387352

73397353
let mut action = Vec::with_capacity(1);
7340-
CodeActionBuilder::new(&format!("Use `{}`", replacement.name()))
7354+
CodeActionBuilder::new(format!("Use `{}`", replacement.name()))
73417355
.kind(CodeActionKind::REFACTOR_REWRITE)
73427356
.changes(self.params.text_document.uri.clone(), self.edits.edits)
73437357
.preferred(true)
@@ -7420,7 +7434,7 @@ impl<'a> FixTruncatedBitArraySegment<'a> {
74207434
.replace(truncation.value_location, replacement.clone());
74217435

74227436
let mut action = Vec::with_capacity(1);
7423-
CodeActionBuilder::new(&format!("Replace with `{replacement}`"))
7437+
CodeActionBuilder::new(format!("Replace with `{replacement}`"))
74247438
.kind(CodeActionKind::REFACTOR_REWRITE)
74257439
.changes(self.params.text_document.uri.clone(), self.edits.edits)
74267440
.preferred(true)
@@ -9143,3 +9157,105 @@ impl<'ast> ast::visit::Visit<'ast> for ExtractFunction<'ast> {
91439157
}
91449158
}
91459159
}
9160+
9161+
/// Code action to create unknown modules when an import is added for a
9162+
/// module that doesn't exist.
9163+
///
9164+
/// For example, if `import wobble/woo` is added to `src/wiggle.gleam`,
9165+
/// then a code action to create `src/wobble/woo.gleam` will be presented
9166+
/// when triggered over `import wobble/woo`.
9167+
pub struct CreateUnknownModule<'a> {
9168+
module: &'a Module,
9169+
lines: &'a LineNumbers,
9170+
params: &'a CodeActionParams,
9171+
paths: &'a ProjectPaths,
9172+
error: &'a Option<Error>,
9173+
}
9174+
9175+
impl<'a> CreateUnknownModule<'a> {
9176+
pub fn new(
9177+
module: &'a Module,
9178+
lines: &'a LineNumbers,
9179+
params: &'a CodeActionParams,
9180+
paths: &'a ProjectPaths,
9181+
error: &'a Option<Error>,
9182+
) -> Self {
9183+
Self {
9184+
module,
9185+
lines,
9186+
params,
9187+
paths,
9188+
error,
9189+
}
9190+
}
9191+
9192+
pub fn code_actions(self) -> Vec<CodeAction> {
9193+
struct UnknownModule<'a> {
9194+
name: &'a EcoString,
9195+
location: &'a SrcSpan,
9196+
}
9197+
9198+
let mut actions = vec![];
9199+
9200+
// This code action can be derived from UnknownModule type errors. If those
9201+
// errors don't exist, there are no actions to add.
9202+
let Some(Error::Type { errors, .. }) = self.error else {
9203+
return actions;
9204+
};
9205+
9206+
// Span of the code action so we can check if it exists within the span of
9207+
// the UnkownModule type error
9208+
let code_action_span = lsp_range_to_src_span(self.params.range, self.lines);
9209+
9210+
// Origin directory we can build the new module path from
9211+
let origin_directory = match self.module.origin {
9212+
Origin::Src => self.paths.src_directory(),
9213+
Origin::Test => self.paths.test_directory(),
9214+
Origin::Dev => self.paths.dev_directory(),
9215+
};
9216+
9217+
// Filter for any UnknownModule type errors
9218+
let unknown_modules = errors.iter().filter_map(|error| {
9219+
if let TypeError::UnknownModule { name, location, .. } = error {
9220+
return Some(UnknownModule { name, location });
9221+
}
9222+
9223+
None
9224+
});
9225+
9226+
// For each UnknownModule type error, check to see if it contains the
9227+
// incoming code action & if so, add a document change to create the module
9228+
for unknown_module in unknown_modules {
9229+
// Was this code action triggered within the UnknownModule error?
9230+
let error_contains_action = unknown_module.location.contains(code_action_span.start)
9231+
&& unknown_module.location.contains(code_action_span.end);
9232+
9233+
if !error_contains_action {
9234+
continue;
9235+
}
9236+
9237+
let uri = url_from_path(&format!("{origin_directory}/{}.gleam", unknown_module.name))
9238+
.expect("origin directory is absolute");
9239+
9240+
CodeActionBuilder::new(format!(
9241+
"Create {}/{}.gleam",
9242+
self.module.origin.folder_name(),
9243+
unknown_module.name
9244+
))
9245+
.kind(CodeActionKind::QUICKFIX)
9246+
.document_changes(DocumentChanges::Operations(vec![
9247+
DocumentChangeOperation::Op(ResourceOp::Create(CreateFile {
9248+
uri,
9249+
options: Some(CreateFileOptions {
9250+
overwrite: Some(false),
9251+
ignore_if_exists: Some(true),
9252+
}),
9253+
annotation_id: None,
9254+
})),
9255+
]))
9256+
.push_to(&mut actions);
9257+
}
9258+
9259+
actions
9260+
}
9261+
}

compiler-core/src/language_server/engine.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ use super::{
4545
DownloadDependencies, MakeLocker,
4646
code_action::{
4747
AddAnnotations, CodeActionBuilder, ConvertFromUse, ConvertToFunctionCall, ConvertToPipe,
48-
ConvertToUse, ExpandFunctionCapture, ExtractConstant, ExtractVariable,
48+
ConvertToUse, CreateUnknownModule, ExpandFunctionCapture, ExtractConstant, ExtractVariable,
4949
FillInMissingLabelledArgs, FillUnusedFields, FixBinaryOperation,
5050
FixTruncatedBitArraySegment, GenerateDynamicDecoder, GenerateFunction, GenerateJsonEncoder,
5151
GenerateVariant, InlineVariable, InterpolateString, LetAssertToCase, PatternMatchOnValue,
@@ -462,6 +462,10 @@ where
462462
)
463463
.code_actions();
464464
AddAnnotations::new(module, &lines, &params).code_action(&mut actions);
465+
actions.extend(
466+
CreateUnknownModule::new(module, &lines, &params, &this.paths, &this.error)
467+
.code_actions(),
468+
);
465469
Ok(if actions.is_empty() {
466470
None
467471
} else {
@@ -1548,7 +1552,7 @@ fn code_action_fix_names(
15481552
new_text: correction.to_string(),
15491553
};
15501554

1551-
CodeActionBuilder::new(&format!("Rename to {correction}"))
1555+
CodeActionBuilder::new(format!("Rename to {correction}"))
15521556
.kind(lsp_types::CodeActionKind::QUICKFIX)
15531557
.changes(uri.clone(), vec![edit])
15541558
.preferred(true)

0 commit comments

Comments
 (0)