@@ -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} ;
2729use ecow:: { EcoString , eco_format} ;
2830use im:: HashMap ;
2931use 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+ } ;
3136use vec1:: { Vec1 , vec1} ;
3237
3338use super :: {
@@ -46,7 +51,7 @@ pub struct CodeActionBuilder {
4651}
4752
4853impl 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,
@@ -7188,7 +7202,7 @@ impl<'a> FixBinaryOperation<'a> {
71887202 self . edits . replace ( location, replacement. name ( ) . into ( ) ) ;
71897203
71907204 let mut action = Vec :: with_capacity ( 1 ) ;
7191- CodeActionBuilder :: new ( & format ! ( "Use `{}`" , replacement. name( ) ) )
7205+ CodeActionBuilder :: new ( format ! ( "Use `{}`" , replacement. name( ) ) )
71927206 . kind ( CodeActionKind :: REFACTOR_REWRITE )
71937207 . changes ( self . params . text_document . uri . clone ( ) , self . edits . edits )
71947208 . preferred ( true )
@@ -7271,7 +7285,7 @@ impl<'a> FixTruncatedBitArraySegment<'a> {
72717285 . replace ( truncation. value_location , replacement. clone ( ) ) ;
72727286
72737287 let mut action = Vec :: with_capacity ( 1 ) ;
7274- CodeActionBuilder :: new ( & format ! ( "Replace with `{replacement}`" ) )
7288+ CodeActionBuilder :: new ( format ! ( "Replace with `{replacement}`" ) )
72757289 . kind ( CodeActionKind :: REFACTOR_REWRITE )
72767290 . changes ( self . params . text_document . uri . clone ( ) , self . edits . edits )
72777291 . preferred ( true )
@@ -8196,3 +8210,105 @@ impl<'ast> ast::visit::Visit<'ast> for RemoveUnreachableBranches<'ast> {
81968210 ast:: visit:: visit_typed_expr_case ( self , location, type_, subjects, clauses, compiled_case) ;
81978211 }
81988212}
8213+
8214+ /// Code action to create unknown modules when an import is added for a
8215+ /// module that doesn't exist.
8216+ ///
8217+ /// For example, if `import wobble/woo` is added to `src/wiggle.gleam`,
8218+ /// then a code action to create `src/wobble/woo.gleam` will be presented
8219+ /// when triggered over `import wobble/woo`.
8220+ pub struct CreateUnknownModule < ' a > {
8221+ module : & ' a Module ,
8222+ lines : & ' a LineNumbers ,
8223+ params : & ' a CodeActionParams ,
8224+ paths : & ' a ProjectPaths ,
8225+ error : & ' a Option < Error > ,
8226+ }
8227+
8228+ impl < ' a > CreateUnknownModule < ' a > {
8229+ pub fn new (
8230+ module : & ' a Module ,
8231+ lines : & ' a LineNumbers ,
8232+ params : & ' a CodeActionParams ,
8233+ paths : & ' a ProjectPaths ,
8234+ error : & ' a Option < Error > ,
8235+ ) -> Self {
8236+ Self {
8237+ module,
8238+ lines,
8239+ params,
8240+ paths,
8241+ error,
8242+ }
8243+ }
8244+
8245+ pub fn code_actions ( self ) -> Vec < CodeAction > {
8246+ struct UnknownModule < ' a > {
8247+ name : & ' a EcoString ,
8248+ location : & ' a SrcSpan ,
8249+ }
8250+
8251+ let mut actions = vec ! [ ] ;
8252+
8253+ // This code action can be derived from UnknownModule type errors. If those
8254+ // errors don't exist, there are no actions to add.
8255+ let Some ( Error :: Type { errors, .. } ) = self . error else {
8256+ return actions;
8257+ } ;
8258+
8259+ // Span of the code action so we can check if it exists within the span of
8260+ // the UnkownModule type error
8261+ let code_action_span = lsp_range_to_src_span ( self . params . range , self . lines ) ;
8262+
8263+ // Origin directory we can build the new module path from
8264+ let origin_directory = match self . module . origin {
8265+ Origin :: Src => self . paths . src_directory ( ) ,
8266+ Origin :: Test => self . paths . test_directory ( ) ,
8267+ Origin :: Dev => self . paths . dev_directory ( ) ,
8268+ } ;
8269+
8270+ // Filter for any UnknownModule type errors
8271+ let unknown_modules = errors. iter ( ) . filter_map ( |error| {
8272+ if let TypeError :: UnknownModule { name, location, .. } = error {
8273+ return Some ( UnknownModule { name, location } ) ;
8274+ }
8275+
8276+ None
8277+ } ) ;
8278+
8279+ // For each UnknownModule type error, check to see if it contains the
8280+ // incoming code action & if so, add a document change to create the module
8281+ for unknown_module in unknown_modules {
8282+ // Was this code action triggered within the UnknownModule error?
8283+ let error_contains_action = unknown_module. location . contains ( code_action_span. start )
8284+ && unknown_module. location . contains ( code_action_span. end ) ;
8285+
8286+ if !error_contains_action {
8287+ continue ;
8288+ }
8289+
8290+ let uri = url_from_path ( & format ! ( "{origin_directory}/{}.gleam" , unknown_module. name) )
8291+ . expect ( "origin directory is absolute" ) ;
8292+
8293+ CodeActionBuilder :: new ( format ! (
8294+ "Create {}/{}.gleam" ,
8295+ self . module. origin. folder_name( ) ,
8296+ unknown_module. name
8297+ ) )
8298+ . kind ( CodeActionKind :: QUICKFIX )
8299+ . document_changes ( DocumentChanges :: Operations ( vec ! [
8300+ DocumentChangeOperation :: Op ( ResourceOp :: Create ( CreateFile {
8301+ uri,
8302+ options: Some ( CreateFileOptions {
8303+ overwrite: Some ( false ) ,
8304+ ignore_if_exists: Some ( true ) ,
8305+ } ) ,
8306+ annotation_id: None ,
8307+ } ) ) ,
8308+ ] ) )
8309+ . push_to ( & mut actions) ;
8310+ }
8311+
8312+ actions
8313+ }
8314+ }
0 commit comments