diff --git a/CHANGELOG.md b/CHANGELOG.md index 1be6a42ecdf..59eecc34a76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/compiler-core/src/language_server/code_action.rs b/compiler-core/src/language_server/code_action.rs index 26c819807cc..51598bc1169 100644 --- a/compiler-core/src/language_server/code_action.rs +++ b/compiler-core/src/language_server/code_action.rs @@ -10,15 +10,17 @@ 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, }, @@ -26,7 +28,10 @@ use crate::{ 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::{ @@ -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(), @@ -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 @@ -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 )) @@ -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, @@ -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) @@ -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) @@ -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, +} + +impl<'a> CreateUnknownModule<'a> { + pub fn new( + module: &'a Module, + lines: &'a LineNumbers, + params: &'a CodeActionParams, + paths: &'a ProjectPaths, + error: &'a Option, + ) -> Self { + Self { + module, + lines, + params, + paths, + error, + } + } + + pub fn code_actions(self) -> Vec { + 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 + } +} diff --git a/compiler-core/src/language_server/engine.rs b/compiler-core/src/language_server/engine.rs index ef30ce7b317..a5690efe208 100644 --- a/compiler-core/src/language_server/engine.rs +++ b/compiler-core/src/language_server/engine.rs @@ -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, @@ -461,6 +461,10 @@ where ) .code_actions(); AddAnnotations::new(module, &lines, ¶ms).code_action(&mut actions); + actions.extend( + CreateUnknownModule::new(module, &lines, ¶ms, &this.paths, &this.error) + .code_actions(), + ); Ok(if actions.is_empty() { None } else { @@ -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) diff --git a/compiler-core/src/language_server/tests.rs b/compiler-core/src/language_server/tests.rs index c3246091b0c..0a76b5c1619 100644 --- a/compiler-core/src/language_server/tests.rs +++ b/compiler-core/src/language_server/tests.rs @@ -24,6 +24,7 @@ use lsp_types::{Position, TextDocumentIdentifier, TextDocumentPositionParams, Ur use crate::{ Result, + build::Origin, config::PackageConfig, io::{ BeamCompiler, Command, CommandExecutor, FileSystemReader, FileSystemWriter, ReadDir, @@ -438,7 +439,7 @@ impl<'a> TestProject<'a> { pub fn src_from_module_url(&self, url: &Url) -> Option<&str> { let module_name: EcoString = self.module_name_from_url(url)?.into(); - if module_name == "app" { + if module_name == LSP_TEST_ROOT_PACKAGE_NAME { return Some(self.src); } @@ -457,6 +458,31 @@ impl<'a> TestProject<'a> { .or_else(|| find_module(&self.indirect_hex_modules)) } + pub fn src_from_origin_and_module_name( + &self, + origin: Origin, + module_name: &str, + ) -> Option<&str> { + let find_module = |modules: &Vec<(&'a str, &'a str)>| { + modules + .iter() + .find(|(name, _)| *name == module_name) + .map(|(_, src)| *src) + }; + + match origin { + Origin::Src => { + if module_name == LSP_TEST_ROOT_PACKAGE_NAME { + return Some(self.src); + } + + find_module(&self.root_package_modules) + } + Origin::Test => find_module(&self.test_modules), + Origin::Dev => find_module(&self.dev_modules), + } + } + pub fn add_module(mut self, name: &'a str, src: &'a str) -> Self { self.root_package_modules.push((name, src)); self @@ -597,6 +623,18 @@ impl<'a> TestProject<'a> { TextDocumentPositionParams::new(TextDocumentIdentifier::new(url), position) } + pub fn build_src_path(&self, position: Position, module: &str) -> TextDocumentPositionParams { + let path = Utf8PathBuf::from(if cfg!(target_family = "windows") { + format!(r"\\?\C:\src\{module}.gleam") + } else { + format!("/src/{module}.gleam") + }); + + let url = Url::from_file_path(path).unwrap(); + + TextDocumentPositionParams::new(TextDocumentIdentifier::new(url), position) + } + pub fn build_test_path( &self, position: Position, @@ -640,7 +678,7 @@ impl<'a> TestProject<'a> { let mut engine = self.build_engine(&mut io); // Add the final module we're going to be positioning the cursor in. - _ = io.src_module("app", self.src); + _ = io.src_module(LSP_TEST_ROOT_PACKAGE_NAME, self.src); let _response = engine.compile_please(); @@ -649,6 +687,27 @@ impl<'a> TestProject<'a> { (engine, param) } + pub fn positioned_with_io_in_src( + &self, + position: Position, + module: &str, + ) -> ( + LanguageServerEngine, + TextDocumentPositionParams, + ) { + let mut io = LanguageServerTestIO::new(); + let mut engine = self.build_engine(&mut io); + + // Add the final module we're going to be positioning the cursor in. + _ = io.src_module(LSP_TEST_ROOT_PACKAGE_NAME, self.src); + + let _response = engine.compile_please(); + + let param = self.build_src_path(position, module); + + (engine, param) + } + pub fn positioned_with_io_in_test( &self, position: Position, @@ -661,10 +720,9 @@ impl<'a> TestProject<'a> { let mut engine = self.build_engine(&mut io); // Add the final module we're going to be positioning the cursor in. - _ = io.src_module("app", self.src); + _ = io.src_module(LSP_TEST_ROOT_PACKAGE_NAME, self.src); - let response = engine.compile_please(); - assert!(response.result.is_ok()); + let _response = engine.compile_please(); let param = self.build_test_path(position, test_name); @@ -683,10 +741,9 @@ impl<'a> TestProject<'a> { let mut engine = self.build_engine(&mut io); // Add the final module we're going to be positioning the cursor in. - _ = io.src_module("app", self.src); + _ = io.src_module(LSP_TEST_ROOT_PACKAGE_NAME, self.src); - let response = engine.compile_please(); - assert!(response.result.is_ok()); + let _response = engine.compile_please(); let param = self.build_dev_path(position, test_name); @@ -706,6 +763,30 @@ impl<'a> TestProject<'a> { executor(&mut engine, params, self.src.into()) } + + pub fn in_module_at( + &self, + origin: Origin, + module: &str, + position: Position, + executor: impl FnOnce( + &mut LanguageServerEngine, + TextDocumentPositionParams, + EcoString, + ) -> T, + ) -> T { + let (mut engine, params) = match origin { + Origin::Src => self.positioned_with_io_in_src(position, module), + Origin::Test => self.positioned_with_io_in_test(position, module), + Origin::Dev => self.positioned_with_io_in_dev(position, module), + }; + + let code = self + .src_from_origin_and_module_name(origin, module) + .expect("Module doesn't exist"); + + executor(&mut engine, params, code.into()) + } } #[derive(Clone)] diff --git a/compiler-core/src/language_server/tests/action.rs b/compiler-core/src/language_server/tests/action.rs index 7466c7a1af4..4623287a3f5 100644 --- a/compiler-core/src/language_server/tests/action.rs +++ b/compiler-core/src/language_server/tests/action.rs @@ -1,18 +1,26 @@ use itertools::Itertools; use lsp_types::{ - CodeActionContext, CodeActionParams, PartialResultParams, Position, Range, Url, - WorkDoneProgressParams, + CodeActionContext, CodeActionParams, DocumentChangeOperation, DocumentChanges, + PartialResultParams, Position, ResourceOp, Url, WorkDoneProgressParams, }; +use crate::language_server::path; + use super::*; -fn code_actions(tester: &TestProject<'_>, range: Range) -> Option> { +fn code_actions( + tester: &TestProject<'_>, + origin: Origin, + module: &str, + range_selector: RangeSelector, +) -> Vec { let position = Position { line: 0, character: 0, }; - tester.at(position, |engine, params, _| { + tester.in_module_at(origin, module, position, |engine, params, code| { + let range = range_selector.find_range(&code); let params = CodeActionParams { text_document: params.text_document, range, @@ -20,44 +28,51 @@ fn code_actions(tester: &TestProject<'_>, range: Range) -> Option, tester: &TestProject<'_>, - range: Range, + origin: Origin, + module: &str, + range_selector: RangeSelector, ) -> Vec { - code_actions(tester, range) + code_actions(tester, origin, module, range_selector) .into_iter() - .flatten() .filter(|action| titles.contains(&action.title.as_str())) .collect_vec() } -fn owned_actions_with_title( - titles: Vec<&str>, - tester: TestProject<'_>, - range: Range, -) -> Vec { - actions_with_title(titles, &tester, range) -} - -fn apply_code_action(title: &str, tester: TestProject<'_>, range: Range) -> String { +fn apply_code_action( + title: &str, + tester: &TestProject<'_>, + origin: Origin, + module: &str, + range_selector: RangeSelector, +) -> (String, String) { let titles = vec![title]; - let changes = actions_with_title(titles, &tester, range) - .pop() + let actions = actions_with_title(titles, &tester, origin, module, range_selector); + let changes = actions + .last() .expect("No action with the given title") .edit - .expect("No workspace edit found") - .changes - .expect("No text edit found"); - apply_code_edit(tester, changes) + .as_ref() + .and_then(|edit| edit.changes.as_ref()) + .cloned() + .unwrap_or_default(); + let code_change = apply_code_edit(tester, changes); + let file_operations = format_code_action_file_operations(&actions); + (code_change, file_operations) } fn apply_code_edit( - tester: TestProject<'_>, + tester: &TestProject<'_>, changes: HashMap>, ) -> String { let mut changed_files: HashMap = HashMap::new(); @@ -75,7 +90,7 @@ fn apply_code_edit( show_code_edits(tester, changed_files) } -fn show_code_edits(tester: TestProject<'_>, changed_files: HashMap) -> String { +fn show_code_edits(tester: &TestProject<'_>, changed_files: HashMap) -> String { let format_code = |url: &Url, code: &String| { format!( "// --- Edits applied to module '{}'\n{}", @@ -103,6 +118,44 @@ fn show_code_edits(tester: TestProject<'_>, changed_files: HashMap) } } +fn format_code_action_file_operations<'a>(actions: &[lsp_types::CodeAction]) -> String { + actions + .iter() + .filter_map(|action| { + if let Some(DocumentChanges::Operations(operations)) = action + .edit + .as_ref() + .and_then(|edit| edit.document_changes.as_ref()) + { + Some(operations) + } else { + None + } + }) + .flat_map(|operations| { + operations.into_iter().filter_map(|op| match op { + DocumentChangeOperation::Op(op) => Some(op), + DocumentChangeOperation::Edit(_) => None, + }) + }) + .map(|op| match op { + ResourceOp::Create(create) => { + format!("- Create {}", path(&create.uri)) + } + ResourceOp::Rename(rename) => { + format!( + "- Rename {} to {}", + path(&rename.old_uri), + path(&rename.new_uri) + ) + } + ResourceOp::Delete(delete) => { + format!("- Delete {}", path(&delete.uri)) + } + }) + .join("\n") +} + const REMOVE_UNUSED_IMPORTS: &str = "Remove unused imports"; const REMOVE_REDUNDANT_TUPLES: &str = "Remove redundant tuples"; const CONVERT_TO_CASE: &str = "Convert to case"; @@ -138,36 +191,83 @@ const ADD_OMITTED_LABELS: &str = "Add omitted labels"; const EXTRACT_FUNCTION: &str = "Extract function"; macro_rules! assert_code_action { - ($title:expr, $code:literal, $range:expr $(,)?) => { + ($title:expr, $code:literal, $range_selector:expr $(,)?) => { let project = TestProject::for_source($code); - assert_code_action!($title, project, $range); + assert_code_action!($title, project, $range_selector); }; - ($title:expr, $project:expr, $range:expr $(,)?) => { - let src = $project.src; - let range = $range.find_range(src); - let result = apply_code_action($title, $project, range); - let output = format!( + ($title:expr, $project:expr, $range_selector:expr $(,)?) => { + assert_code_action!( + $title, + $project, + Origin::Src, + LSP_TEST_ROOT_PACKAGE_NAME, + $range_selector + ); + }; + + ($title:expr, $code:literal, $origin:expr, $module:expr, $range_selector:expr $(,)?) => { + let project = TestProject::for_source($code); + assert_code_action!($title, project, $origin, $module, $range_selector); + }; + + ($title:expr, $project:expr, $origin:expr, $module:expr, $range_selector:expr $(,)?) => { + let project = &$project; + let src = project + .src_from_origin_and_module_name($origin, $module) + .unwrap(); + let range = $range_selector.find_range(src); + let (updated_src, file_operations) = + apply_code_action($title, project, $origin, $module, $range_selector); + + let mut output = format!( "----- BEFORE ACTION\n{}\n\n----- AFTER ACTION\n{}", hover::show_hover(src, range, range.end), - result + updated_src ); + + if !file_operations.is_empty() { + output.push_str(&format!("\n----- FILE OPERATIONS -----\n{file_operations}")); + } + insta::assert_snapshot!(insta::internals::AutoName, output, src); }; } macro_rules! assert_no_code_actions { - ($title:ident $(| $titles:ident)*, $code:literal, $range:expr $(,)?) => { + ($title:ident $(| $titles:ident)*, $code:literal, $range_selector:expr $(,)?) => { let project = TestProject::for_source($code); - assert_no_code_actions!($title $(| $titles)*, project, $range); + assert_no_code_actions!($title $(| $titles)*, project, $range_selector); }; - ($title:ident $(| $titles:ident)*, $project:expr, $range:expr $(,)?) => { - let src = $project.src; - let range = $range.find_range(src); + ($title:ident $(| $titles:ident)*, $project:expr, $range_selector:expr $(,)?) => { let all_titles = vec![$title $(, $titles)*]; let expected: Vec = vec![]; - let result = owned_actions_with_title(all_titles, $project, range); + let result = actions_with_title( + all_titles, + &$project, + Origin::Src, + LSP_TEST_ROOT_PACKAGE_NAME, + $range_selector + ); + assert_eq!(expected, result); + }; + + ($title:literal $(| $titles:literal)*, $code:literal, $range_selector:expr $(,)?) => { + let project = TestProject::for_source($code); + assert_no_code_actions!($title $(| $titles)*, project, $range_selector); + }; + + ($title:literal $(| $titles:literal)*, $project:expr, $range_selector:expr $(,)?) => { + let all_titles = vec![$title $(, $titles)*]; + let expected: Vec = vec![]; + let result = actions_with_title( + all_titles, + &$project, + Origin::Src, + LSP_TEST_ROOT_PACKAGE_NAME, + $range_selector + ); assert_eq!(expected, result); }; } @@ -10686,3 +10786,98 @@ pub fn main() -> Nil { find_position_of("function").to_selection() ); } + +#[test] +fn create_unknown_module_under_src() { + assert_code_action!( + "Create src/wibble/wobble.gleam", + " +import wibble/wobble + +pub fn main() { + Nil +}", + find_position_of("wobble").to_selection() + ); +} + +#[test] +fn create_unknown_module_under_dev() { + let project = TestProject::for_source( + " +pub fn main() { + Nil +}", + ) + .add_dev_module( + "wibble", + " +import wobble/woo", + ); + + assert_code_action!( + "Create dev/wobble/woo.gleam", + project, + Origin::Dev, + "wibble", + find_position_of("woo").to_selection() + ); +} + +#[test] +fn create_unknown_module_under_test() { + let project = TestProject::for_source( + " +pub fn main() { + Nil +}", + ) + .add_test_module( + "wibble", + " +import wobble/woo", + ); + + assert_code_action!( + "Create test/wobble/woo.gleam", + project, + Origin::Test, + "wibble", + find_position_of("woo").to_selection() + ); +} + +#[test] +fn create_unknown_module_doesnt_trigger_when_module_exists() { + let code = " +import wibble/wobble + +pub fn main() { + Nil +} +"; + + assert_no_code_actions!( + "Create src/wibble/wobble.gleam", + TestProject::for_source(code) + .add_module("wibble/wobble", "pub type Wibble { Wobble(String) }"), + find_position_of("wobble").to_selection() + ); +} + +#[test] +fn create_unknown_module_doesnt_trigger_when_import_not_selected() { + let code = " +import wibble/wobble + +pub fn main() { + Nil +} +"; + + assert_no_code_actions!( + "Create src/wibble/wobble.gleam", + TestProject::for_source(code), + find_position_of("main").to_selection() + ); +} diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__create_unknown_module_under_dev.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__create_unknown_module_under_dev.snap new file mode 100644 index 00000000000..7cd4f092561 --- /dev/null +++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__create_unknown_module_under_dev.snap @@ -0,0 +1,14 @@ +--- +source: compiler-core/src/language_server/tests/action.rs +expression: "\nimport wobble/woo" +--- +----- BEFORE ACTION + +import wobble/woo + ↑ + + +----- AFTER ACTION + +----- FILE OPERATIONS ----- +- Create /dev/wobble/woo.gleam diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__create_unknown_module_under_src.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__create_unknown_module_under_src.snap new file mode 100644 index 00000000000..62d2a2e4209 --- /dev/null +++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__create_unknown_module_under_src.snap @@ -0,0 +1,18 @@ +--- +source: compiler-core/src/language_server/tests/action.rs +expression: "\nimport wibble/wobble\n\npub fn main() {\n Nil\n}" +--- +----- BEFORE ACTION + +import wibble/wobble + ↑ + +pub fn main() { + Nil +} + + +----- AFTER ACTION + +----- FILE OPERATIONS ----- +- Create /src/wibble/wobble.gleam diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__create_unknown_module_under_test.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__create_unknown_module_under_test.snap new file mode 100644 index 00000000000..7386c3c7015 --- /dev/null +++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__create_unknown_module_under_test.snap @@ -0,0 +1,14 @@ +--- +source: compiler-core/src/language_server/tests/action.rs +expression: "\nimport wobble/woo" +--- +----- BEFORE ACTION + +import wobble/woo + ↑ + + +----- AFTER ACTION + +----- FILE OPERATIONS ----- +- Create /test/wobble/woo.gleam