Skip to content

Commit 7fcc8f7

Browse files
committed
Add create unknown module code action
1 parent 4869b5d commit 7fcc8f7

File tree

3 files changed

+136
-6
lines changed

3 files changed

+136
-6
lines changed

CHANGELOG.md

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

296296
([Surya Rose](https://github.com/GearsDatapacks))
297297

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

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

compiler-core/src/language_server/code_action.rs

Lines changed: 122 additions & 5 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
@@ -7895,3 +7909,106 @@ 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 =
7990+
Url::from_file_path(format!("{origin_directory}/{}.gleam", unknown_module.name))
7991+
.expect("origin directory is absolute");
7992+
7993+
CodeActionBuilder::new(format!(
7994+
"Create module {}/{}.gleam",
7995+
self.module.origin.folder_name(),
7996+
unknown_module.name
7997+
))
7998+
.kind(CodeActionKind::QUICKFIX)
7999+
.document_changes(DocumentChanges::Operations(vec![
8000+
DocumentChangeOperation::Op(ResourceOp::Create(CreateFile {
8001+
uri,
8002+
options: Some(CreateFileOptions {
8003+
overwrite: Some(false),
8004+
ignore_if_exists: Some(true),
8005+
}),
8006+
annotation_id: None,
8007+
})),
8008+
]))
8009+
.push_to(&mut actions);
8010+
}
8011+
8012+
actions
8013+
}
8014+
}

compiler-core/src/language_server/engine.rs

Lines changed: 5 additions & 1 deletion
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 {

0 commit comments

Comments
 (0)