Skip to content

Commit 655236a

Browse files
committed
Add create unknown module code action
1 parent 195c44a commit 655236a

8 files changed

+509
-58
lines changed

CHANGELOG.md

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

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

732+
- The language server now offers a code action to create unknown modules
733+
when an import is added for a module that doesn't exist.
734+
735+
For example, if `import wobble/woo` is added to `src/wiggle.gleam`,
736+
then a code action to create `src/wobble/woo.gleam` will be presented
737+
when triggered over `import wobble/woo`.
738+
739+
([Cory Forsstrom](https://github.com/tarkah))
740+
732741
### Formatter
733742

734743
- 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,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,
@@ -7297,7 +7311,7 @@ impl<'a> FixBinaryOperation<'a> {
72977311
self.edits.replace(location, replacement.name().into());
72987312

72997313
let mut action = Vec::with_capacity(1);
7300-
CodeActionBuilder::new(&format!("Use `{}`", replacement.name()))
7314+
CodeActionBuilder::new(format!("Use `{}`", replacement.name()))
73017315
.kind(CodeActionKind::REFACTOR_REWRITE)
73027316
.changes(self.params.text_document.uri.clone(), self.edits.edits)
73037317
.preferred(true)
@@ -7380,7 +7394,7 @@ impl<'a> FixTruncatedBitArraySegment<'a> {
73807394
.replace(truncation.value_location, replacement.clone());
73817395

73827396
let mut action = Vec::with_capacity(1);
7383-
CodeActionBuilder::new(&format!("Replace with `{replacement}`"))
7397+
CodeActionBuilder::new(format!("Replace with `{replacement}`"))
73847398
.kind(CodeActionKind::REFACTOR_REWRITE)
73857399
.changes(self.params.text_document.uri.clone(), self.edits.edits)
73867400
.preferred(true)
@@ -9103,3 +9117,105 @@ impl<'ast> ast::visit::Visit<'ast> for ExtractFunction<'ast> {
91039117
}
91049118
}
91059119
}
9120+
9121+
/// Code action to create unknown modules when an import is added for a
9122+
/// module that doesn't exist.
9123+
///
9124+
/// For example, if `import wobble/woo` is added to `src/wiggle.gleam`,
9125+
/// then a code action to create `src/wobble/woo.gleam` will be presented
9126+
/// when triggered over `import wobble/woo`.
9127+
pub struct CreateUnknownModule<'a> {
9128+
module: &'a Module,
9129+
lines: &'a LineNumbers,
9130+
params: &'a CodeActionParams,
9131+
paths: &'a ProjectPaths,
9132+
error: &'a Option<Error>,
9133+
}
9134+
9135+
impl<'a> CreateUnknownModule<'a> {
9136+
pub fn new(
9137+
module: &'a Module,
9138+
lines: &'a LineNumbers,
9139+
params: &'a CodeActionParams,
9140+
paths: &'a ProjectPaths,
9141+
error: &'a Option<Error>,
9142+
) -> Self {
9143+
Self {
9144+
module,
9145+
lines,
9146+
params,
9147+
paths,
9148+
error,
9149+
}
9150+
}
9151+
9152+
pub fn code_actions(self) -> Vec<CodeAction> {
9153+
struct UnknownModule<'a> {
9154+
name: &'a EcoString,
9155+
location: &'a SrcSpan,
9156+
}
9157+
9158+
let mut actions = vec![];
9159+
9160+
// This code action can be derived from UnknownModule type errors. If those
9161+
// errors don't exist, there are no actions to add.
9162+
let Some(Error::Type { errors, .. }) = self.error else {
9163+
return actions;
9164+
};
9165+
9166+
// Span of the code action so we can check if it exists within the span of
9167+
// the UnkownModule type error
9168+
let code_action_span = lsp_range_to_src_span(self.params.range, self.lines);
9169+
9170+
// Origin directory we can build the new module path from
9171+
let origin_directory = match self.module.origin {
9172+
Origin::Src => self.paths.src_directory(),
9173+
Origin::Test => self.paths.test_directory(),
9174+
Origin::Dev => self.paths.dev_directory(),
9175+
};
9176+
9177+
// Filter for any UnknownModule type errors
9178+
let unknown_modules = errors.iter().filter_map(|error| {
9179+
if let TypeError::UnknownModule { name, location, .. } = error {
9180+
return Some(UnknownModule { name, location });
9181+
}
9182+
9183+
None
9184+
});
9185+
9186+
// For each UnknownModule type error, check to see if it contains the
9187+
// incoming code action & if so, add a document change to create the module
9188+
for unknown_module in unknown_modules {
9189+
// Was this code action triggered within the UnknownModule error?
9190+
let error_contains_action = unknown_module.location.contains(code_action_span.start)
9191+
&& unknown_module.location.contains(code_action_span.end);
9192+
9193+
if !error_contains_action {
9194+
continue;
9195+
}
9196+
9197+
let uri = url_from_path(&format!("{origin_directory}/{}.gleam", unknown_module.name))
9198+
.expect("origin directory is absolute");
9199+
9200+
CodeActionBuilder::new(format!(
9201+
"Create {}/{}.gleam",
9202+
self.module.origin.folder_name(),
9203+
unknown_module.name
9204+
))
9205+
.kind(CodeActionKind::QUICKFIX)
9206+
.document_changes(DocumentChanges::Operations(vec![
9207+
DocumentChangeOperation::Op(ResourceOp::Create(CreateFile {
9208+
uri,
9209+
options: Some(CreateFileOptions {
9210+
overwrite: Some(false),
9211+
ignore_if_exists: Some(true),
9212+
}),
9213+
annotation_id: None,
9214+
})),
9215+
]))
9216+
.push_to(&mut actions);
9217+
}
9218+
9219+
actions
9220+
}
9221+
}

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,
@@ -461,6 +461,10 @@ where
461461
)
462462
.code_actions();
463463
AddAnnotations::new(module, &lines, &params).code_action(&mut actions);
464+
actions.extend(
465+
CreateUnknownModule::new(module, &lines, &params, &this.paths, &this.error)
466+
.code_actions(),
467+
);
464468
Ok(if actions.is_empty() {
465469
None
466470
} else {
@@ -1547,7 +1551,7 @@ fn code_action_fix_names(
15471551
new_text: correction.to_string(),
15481552
};
15491553

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

0 commit comments

Comments
 (0)