Skip to content

Commit 4d373c3

Browse files
committed
Add create unknown module code action
1 parent 9a7f74f commit 4d373c3

8 files changed

+507
-58
lines changed

CHANGELOG.md

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

325325
([Surya Rose](https://github.com/GearsDatapacks))
326326

327+
- The language server now offers a code action to create unknown modules
328+
when an import is added for a module that doesn't exist.
329+
330+
For example, if `import wobble/woo` is added to `src/wiggle.gleam`,
331+
then a code action to create `src/wobble/woo.gleam` will be presented
332+
when triggered over `import wobble/woo`.
333+
334+
([Cory Forsstrom](https://github.com/tarkah))
335+
327336
### Formatter
328337

329338
- 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,
@@ -7081,7 +7095,7 @@ impl<'a> FixBinaryOperation<'a> {
70817095
self.edits.replace(location, replacement.name().into());
70827096

70837097
let mut action = Vec::with_capacity(1);
7084-
CodeActionBuilder::new(&format!("Use `{}`", replacement.name()))
7098+
CodeActionBuilder::new(format!("Use `{}`", replacement.name()))
70857099
.kind(CodeActionKind::REFACTOR_REWRITE)
70867100
.changes(self.params.text_document.uri.clone(), self.edits.edits)
70877101
.preferred(true)
@@ -7164,7 +7178,7 @@ impl<'a> FixTruncatedBitArraySegment<'a> {
71647178
.replace(truncation.value_location, replacement.clone());
71657179

71667180
let mut action = Vec::with_capacity(1);
7167-
CodeActionBuilder::new(&format!("Replace with `{replacement}`"))
7181+
CodeActionBuilder::new(format!("Replace with `{replacement}`"))
71687182
.kind(CodeActionKind::REFACTOR_REWRITE)
71697183
.changes(self.params.text_document.uri.clone(), self.edits.edits)
71707184
.preferred(true)
@@ -8006,3 +8020,105 @@ fn single_expression(expression: &TypedExpr) -> Option<&TypedExpr> {
80068020
expression => Some(expression),
80078021
}
80088022
}
8023+
8024+
/// Code action to create unknown modules when an import is added for a
8025+
/// module that doesn't exist.
8026+
///
8027+
/// For example, if `import wobble/woo` is added to `src/wiggle.gleam`,
8028+
/// then a code action to create `src/wobble/woo.gleam` will be presented
8029+
/// when triggered over `import wobble/woo`.
8030+
pub struct CreateUnknownModule<'a> {
8031+
module: &'a Module,
8032+
lines: &'a LineNumbers,
8033+
params: &'a CodeActionParams,
8034+
paths: &'a ProjectPaths,
8035+
error: &'a Option<Error>,
8036+
}
8037+
8038+
impl<'a> CreateUnknownModule<'a> {
8039+
pub fn new(
8040+
module: &'a Module,
8041+
lines: &'a LineNumbers,
8042+
params: &'a CodeActionParams,
8043+
paths: &'a ProjectPaths,
8044+
error: &'a Option<Error>,
8045+
) -> Self {
8046+
Self {
8047+
module,
8048+
lines,
8049+
params,
8050+
paths,
8051+
error,
8052+
}
8053+
}
8054+
8055+
pub fn code_actions(self) -> Vec<CodeAction> {
8056+
struct UnknownModule<'a> {
8057+
name: &'a EcoString,
8058+
location: &'a SrcSpan,
8059+
}
8060+
8061+
let mut actions = vec![];
8062+
8063+
// This code action can be derived from UnknownModule type errors. If those
8064+
// errors don't exist, there are no actions to add.
8065+
let Some(Error::Type { errors, .. }) = self.error else {
8066+
return actions;
8067+
};
8068+
8069+
// Span of the code action so we can check if it exists within the span of
8070+
// the UnkownModule type error
8071+
let code_action_span = lsp_range_to_src_span(self.params.range, self.lines);
8072+
8073+
// Origin directory we can build the new module path from
8074+
let origin_directory = match self.module.origin {
8075+
Origin::Src => self.paths.src_directory(),
8076+
Origin::Test => self.paths.test_directory(),
8077+
Origin::Dev => self.paths.dev_directory(),
8078+
};
8079+
8080+
// Filter for any UnknownModule type errors
8081+
let unknown_modules = errors.iter().filter_map(|error| {
8082+
if let TypeError::UnknownModule { name, location, .. } = error {
8083+
return Some(UnknownModule { name, location });
8084+
}
8085+
8086+
None
8087+
});
8088+
8089+
// For each UnknownModule type error, check to see if it contains the
8090+
// incoming code action & if so, add a document change to create the module
8091+
for unknown_module in unknown_modules {
8092+
// Was this code action triggered within the UnknownModule error?
8093+
let error_contains_action = unknown_module.location.contains(code_action_span.start)
8094+
&& unknown_module.location.contains(code_action_span.end);
8095+
8096+
if !error_contains_action {
8097+
continue;
8098+
}
8099+
8100+
let uri = url_from_path(&format!("{origin_directory}/{}.gleam", unknown_module.name))
8101+
.expect("origin directory is absolute");
8102+
8103+
CodeActionBuilder::new(format!(
8104+
"Create {}/{}.gleam",
8105+
self.module.origin.folder_name(),
8106+
unknown_module.name
8107+
))
8108+
.kind(CodeActionKind::QUICKFIX)
8109+
.document_changes(DocumentChanges::Operations(vec![
8110+
DocumentChangeOperation::Op(ResourceOp::Create(CreateFile {
8111+
uri,
8112+
options: Some(CreateFileOptions {
8113+
overwrite: Some(false),
8114+
ignore_if_exists: Some(true),
8115+
}),
8116+
annotation_id: None,
8117+
})),
8118+
]))
8119+
.push_to(&mut actions);
8120+
}
8121+
8122+
actions
8123+
}
8124+
}

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,
@@ -454,6 +454,10 @@ where
454454
)
455455
.code_actions();
456456
AddAnnotations::new(module, &lines, &params).code_action(&mut actions);
457+
actions.extend(
458+
CreateUnknownModule::new(module, &lines, &params, &this.paths, &this.error)
459+
.code_actions(),
460+
);
457461
Ok(if actions.is_empty() {
458462
None
459463
} else {
@@ -1540,7 +1544,7 @@ fn code_action_fix_names(
15401544
new_text: correction.to_string(),
15411545
};
15421546

1543-
CodeActionBuilder::new(&format!("Rename to {correction}"))
1547+
CodeActionBuilder::new(format!("Rename to {correction}"))
15441548
.kind(lsp_types::CodeActionKind::QUICKFIX)
15451549
.changes(uri.clone(), vec![edit])
15461550
.preferred(true)

0 commit comments

Comments
 (0)