Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -729,6 +729,15 @@

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

- The language server now offers a code action to create unknown modules
when an import is added for a module that doesn't exist.

For example, if `import wobble/woo` is added to `src/wiggle.gleam`,
then a code action to create `src/wobble/woo.gleam` will be presented
when triggered over `import wobble/woo`.

([Cory Forsstrom](https://github.com/tarkah))

### Formatter

- The formatter now removes needless multiple negations that are safe to remove.
Expand Down
134 changes: 125 additions & 9 deletions compiler-core/src/language_server/code_action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,28 @@ use crate::{
TypedExpr, TypedModuleConstant, TypedPattern, TypedPipelineAssignment,
TypedRecordConstructor, TypedStatement, TypedUse, visit::Visit as _,
},
build::{Located, Module},
build::{Located, Module, Origin},
config::PackageConfig,
exhaustiveness::CompiledCase,
language_server::{edits, reference::FindVariableReferences},
language_server::{edits, lsp_range_to_src_span, reference::FindVariableReferences},
line_numbers::LineNumbers,
parse::{extra::ModuleExtra, lexer::str_to_keyword},
paths::ProjectPaths,
strings::to_snake_case,
type_::{
self, FieldMap, ModuleValueConstructor, Type, TypeVar, TypedCallArg, ValueConstructor,
self, Error as TypeError, FieldMap, ModuleValueConstructor, Type, TypeVar, TypedCallArg,
ValueConstructor,
error::{ModuleSuggestion, VariableDeclaration, VariableOrigin},
printer::Printer,
},
};
use ecow::{EcoString, eco_format};
use im::HashMap;
use itertools::Itertools;
use lsp_types::{CodeAction, CodeActionKind, CodeActionParams, Position, Range, TextEdit, Url};
use lsp_types::{
CodeAction, CodeActionKind, CodeActionParams, CreateFile, CreateFileOptions,
DocumentChangeOperation, DocumentChanges, Position, Range, ResourceOp, TextEdit, Url,
};
use vec1::{Vec1, vec1};

use super::{
Expand All @@ -45,7 +50,7 @@ pub struct CodeActionBuilder {
}

impl CodeActionBuilder {
pub fn new(title: &str) -> Self {
pub fn new(title: impl ToString) -> Self {
Self {
action: CodeAction {
title: title.to_string(),
Expand Down Expand Up @@ -75,6 +80,15 @@ impl CodeActionBuilder {
self
}

pub fn document_changes(mut self, changes: DocumentChanges) -> Self {
let mut edit = self.action.edit.take().unwrap_or_default();

edit.document_changes = Some(changes);

self.action.edit = Some(edit);
self
}

pub fn preferred(mut self, is_preferred: bool) -> Self {
self.action.is_preferred = Some(is_preferred);
self
Expand Down Expand Up @@ -1623,7 +1637,7 @@ impl<'a> QualifiedToUnqualifiedImportSecondPass<'a> {
}
self.edit_import();
let mut action = Vec::with_capacity(1);
CodeActionBuilder::new(&format!(
CodeActionBuilder::new(format!(
"Unqualify {}.{}",
self.qualified_constructor.used_name, self.qualified_constructor.constructor
))
Expand Down Expand Up @@ -2013,7 +2027,7 @@ impl<'a> UnqualifiedToQualifiedImportSecondPass<'a> {
constructor,
..
} = self.unqualified_constructor;
CodeActionBuilder::new(&format!(
CodeActionBuilder::new(format!(
"Qualify {} as {}.{}",
constructor.used_name(),
module_name,
Expand Down Expand Up @@ -7297,7 +7311,7 @@ impl<'a> FixBinaryOperation<'a> {
self.edits.replace(location, replacement.name().into());

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

let mut action = Vec::with_capacity(1);
CodeActionBuilder::new(&format!("Replace with `{replacement}`"))
CodeActionBuilder::new(format!("Replace with `{replacement}`"))
.kind(CodeActionKind::REFACTOR_REWRITE)
.changes(self.params.text_document.uri.clone(), self.edits.edits)
.preferred(true)
Expand Down Expand Up @@ -9103,3 +9117,105 @@ impl<'ast> ast::visit::Visit<'ast> for ExtractFunction<'ast> {
}
}
}

/// Code action to create unknown modules when an import is added for a
/// module that doesn't exist.
///
/// For example, if `import wobble/woo` is added to `src/wiggle.gleam`,
/// then a code action to create `src/wobble/woo.gleam` will be presented
/// when triggered over `import wobble/woo`.
pub struct CreateUnknownModule<'a> {
module: &'a Module,
lines: &'a LineNumbers,
params: &'a CodeActionParams,
paths: &'a ProjectPaths,
error: &'a Option<Error>,
}

impl<'a> CreateUnknownModule<'a> {
pub fn new(
module: &'a Module,
lines: &'a LineNumbers,
params: &'a CodeActionParams,
paths: &'a ProjectPaths,
error: &'a Option<Error>,
) -> Self {
Self {
module,
lines,
params,
paths,
error,
}
}

pub fn code_actions(self) -> Vec<CodeAction> {
struct UnknownModule<'a> {
name: &'a EcoString,
location: &'a SrcSpan,
}

let mut actions = vec![];

// This code action can be derived from UnknownModule type errors. If those
// errors don't exist, there are no actions to add.
let Some(Error::Type { errors, .. }) = self.error else {
return actions;
};

// Span of the code action so we can check if it exists within the span of
// the UnkownModule type error
let code_action_span = lsp_range_to_src_span(self.params.range, self.lines);

// Origin directory we can build the new module path from
let origin_directory = match self.module.origin {
Origin::Src => self.paths.src_directory(),
Origin::Test => self.paths.test_directory(),
Origin::Dev => self.paths.dev_directory(),
};

// Filter for any UnknownModule type errors
let unknown_modules = errors.iter().filter_map(|error| {
if let TypeError::UnknownModule { name, location, .. } = error {
return Some(UnknownModule { name, location });
}

None
});

// For each UnknownModule type error, check to see if it contains the
// incoming code action & if so, add a document change to create the module
for unknown_module in unknown_modules {
// Was this code action triggered within the UnknownModule error?
let error_contains_action = unknown_module.location.contains(code_action_span.start)
&& unknown_module.location.contains(code_action_span.end);

if !error_contains_action {
continue;
}

let uri = url_from_path(&format!("{origin_directory}/{}.gleam", unknown_module.name))
.expect("origin directory is absolute");

CodeActionBuilder::new(format!(
"Create {}/{}.gleam",
self.module.origin.folder_name(),
unknown_module.name
))
.kind(CodeActionKind::QUICKFIX)
.document_changes(DocumentChanges::Operations(vec![
DocumentChangeOperation::Op(ResourceOp::Create(CreateFile {
uri,
options: Some(CreateFileOptions {
overwrite: Some(false),
ignore_if_exists: Some(true),
}),
annotation_id: None,
})),
]))
.push_to(&mut actions);
}

actions
}
}
8 changes: 6 additions & 2 deletions compiler-core/src/language_server/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ use super::{
DownloadDependencies, MakeLocker,
code_action::{
AddAnnotations, CodeActionBuilder, ConvertFromUse, ConvertToFunctionCall, ConvertToPipe,
ConvertToUse, ExpandFunctionCapture, ExtractConstant, ExtractVariable,
ConvertToUse, CreateUnknownModule, ExpandFunctionCapture, ExtractConstant, ExtractVariable,
FillInMissingLabelledArgs, FillUnusedFields, FixBinaryOperation,
FixTruncatedBitArraySegment, GenerateDynamicDecoder, GenerateFunction, GenerateJsonEncoder,
GenerateVariant, InlineVariable, InterpolateString, LetAssertToCase, PatternMatchOnValue,
Expand Down Expand Up @@ -461,6 +461,10 @@ where
)
.code_actions();
AddAnnotations::new(module, &lines, &params).code_action(&mut actions);
actions.extend(
CreateUnknownModule::new(module, &lines, &params, &this.paths, &this.error)
.code_actions(),
);
Ok(if actions.is_empty() {
None
} else {
Expand Down Expand Up @@ -1547,7 +1551,7 @@ fn code_action_fix_names(
new_text: correction.to_string(),
};

CodeActionBuilder::new(&format!("Rename to {correction}"))
CodeActionBuilder::new(format!("Rename to {correction}"))
.kind(lsp_types::CodeActionKind::QUICKFIX)
.changes(uri.clone(), vec![edit])
.preferred(true)
Expand Down
Loading