@@ -10,23 +10,28 @@ 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 ,
16- language_server:: { edits, reference:: FindVariableReferences } ,
16+ language_server:: { edits, lsp_range_to_src_span , reference:: FindVariableReferences } ,
1717 line_numbers:: LineNumbers ,
1818 parse:: { extra:: ModuleExtra , lexer:: str_to_keyword} ,
19+ paths:: ProjectPaths ,
1920 strings:: to_snake_case,
2021 type_:: {
21- self , FieldMap , ModuleValueConstructor , Type , TypeVar , TypedCallArg , ValueConstructor ,
22+ self , Error as TypeError , FieldMap , ModuleValueConstructor , Type , TypeVar , TypedCallArg ,
23+ ValueConstructor ,
2224 error:: { ModuleSuggestion , VariableDeclaration , VariableOrigin } ,
2325 printer:: Printer ,
2426 } ,
2527} ;
2628use ecow:: { EcoString , eco_format} ;
2729use im:: HashMap ;
2830use itertools:: Itertools ;
29- use lsp_types:: { CodeAction , CodeActionKind , CodeActionParams , Position , Range , TextEdit , Url } ;
31+ use lsp_types:: {
32+ CodeAction , CodeActionKind , CodeActionParams , CreateFile , CreateFileOptions ,
33+ DocumentChangeOperation , DocumentChanges , Position , Range , ResourceOp , TextEdit , Url ,
34+ } ;
3035use vec1:: { Vec1 , vec1} ;
3136
3237use super :: {
@@ -45,7 +50,7 @@ pub struct CodeActionBuilder {
4550}
4651
4752impl CodeActionBuilder {
48- pub fn new ( title : & str ) -> Self {
53+ pub fn new ( title : impl ToString ) -> Self {
4954 Self {
5055 action : CodeAction {
5156 title : title. to_string ( ) ,
@@ -75,6 +80,15 @@ impl CodeActionBuilder {
7580 self
7681 }
7782
83+ pub fn document_changes ( mut self , changes : DocumentChanges ) -> Self {
84+ let mut edit = self . action . edit . take ( ) . unwrap_or_default ( ) ;
85+
86+ edit. document_changes = Some ( changes) ;
87+
88+ self . action . edit = Some ( edit) ;
89+ self
90+ }
91+
7892 pub fn preferred ( mut self , is_preferred : bool ) -> Self {
7993 self . action . is_preferred = Some ( is_preferred) ;
8094 self
@@ -1623,7 +1637,7 @@ impl<'a> QualifiedToUnqualifiedImportSecondPass<'a> {
16231637 }
16241638 self . edit_import ( ) ;
16251639 let mut action = Vec :: with_capacity ( 1 ) ;
1626- CodeActionBuilder :: new ( & format ! (
1640+ CodeActionBuilder :: new ( format ! (
16271641 "Unqualify {}.{}" ,
16281642 self . qualified_constructor. used_name, self . qualified_constructor. constructor
16291643 ) )
@@ -2013,7 +2027,7 @@ impl<'a> UnqualifiedToQualifiedImportSecondPass<'a> {
20132027 constructor,
20142028 ..
20152029 } = self . unqualified_constructor ;
2016- CodeActionBuilder :: new ( & format ! (
2030+ CodeActionBuilder :: new ( format ! (
20172031 "Qualify {} as {}.{}" ,
20182032 constructor. used_name( ) ,
20192033 module_name,
@@ -7297,7 +7311,7 @@ impl<'a> FixBinaryOperation<'a> {
72977311 self . edits . replace ( location, replacement. name ( ) . into ( ) ) ;
72987312
72997313 let mut action = Vec :: with_capacity ( 1 ) ;
7300- CodeActionBuilder :: new ( & format ! ( "Use `{}`" , replacement. name( ) ) )
7314+ CodeActionBuilder :: new ( format ! ( "Use `{}`" , replacement. name( ) ) )
73017315 . kind ( CodeActionKind :: REFACTOR_REWRITE )
73027316 . changes ( self . params . text_document . uri . clone ( ) , self . edits . edits )
73037317 . preferred ( true )
@@ -7380,7 +7394,7 @@ impl<'a> FixTruncatedBitArraySegment<'a> {
73807394 . replace ( truncation. value_location , replacement. clone ( ) ) ;
73817395
73827396 let mut action = Vec :: with_capacity ( 1 ) ;
7383- CodeActionBuilder :: new ( & format ! ( "Replace with `{replacement}`" ) )
7397+ CodeActionBuilder :: new ( format ! ( "Replace with `{replacement}`" ) )
73847398 . kind ( CodeActionKind :: REFACTOR_REWRITE )
73857399 . changes ( self . params . text_document . uri . clone ( ) , self . edits . edits )
73867400 . preferred ( true )
@@ -9103,3 +9117,105 @@ impl<'ast> ast::visit::Visit<'ast> for ExtractFunction<'ast> {
91039117 }
91049118 }
91059119}
9120+
9121+ /// Code action to create unknown modules when an import is added for a
9122+ /// module that doesn't exist.
9123+ ///
9124+ /// For example, if `import wobble/woo` is added to `src/wiggle.gleam`,
9125+ /// then a code action to create `src/wobble/woo.gleam` will be presented
9126+ /// when triggered over `import wobble/woo`.
9127+ pub struct CreateUnknownModule < ' a > {
9128+ module : & ' a Module ,
9129+ lines : & ' a LineNumbers ,
9130+ params : & ' a CodeActionParams ,
9131+ paths : & ' a ProjectPaths ,
9132+ error : & ' a Option < Error > ,
9133+ }
9134+
9135+ impl < ' a > CreateUnknownModule < ' a > {
9136+ pub fn new (
9137+ module : & ' a Module ,
9138+ lines : & ' a LineNumbers ,
9139+ params : & ' a CodeActionParams ,
9140+ paths : & ' a ProjectPaths ,
9141+ error : & ' a Option < Error > ,
9142+ ) -> Self {
9143+ Self {
9144+ module,
9145+ lines,
9146+ params,
9147+ paths,
9148+ error,
9149+ }
9150+ }
9151+
9152+ pub fn code_actions ( self ) -> Vec < CodeAction > {
9153+ struct UnknownModule < ' a > {
9154+ name : & ' a EcoString ,
9155+ location : & ' a SrcSpan ,
9156+ }
9157+
9158+ let mut actions = vec ! [ ] ;
9159+
9160+ // This code action can be derived from UnknownModule type errors. If those
9161+ // errors don't exist, there are no actions to add.
9162+ let Some ( Error :: Type { errors, .. } ) = self . error else {
9163+ return actions;
9164+ } ;
9165+
9166+ // Span of the code action so we can check if it exists within the span of
9167+ // the UnkownModule type error
9168+ let code_action_span = lsp_range_to_src_span ( self . params . range , self . lines ) ;
9169+
9170+ // Origin directory we can build the new module path from
9171+ let origin_directory = match self . module . origin {
9172+ Origin :: Src => self . paths . src_directory ( ) ,
9173+ Origin :: Test => self . paths . test_directory ( ) ,
9174+ Origin :: Dev => self . paths . dev_directory ( ) ,
9175+ } ;
9176+
9177+ // Filter for any UnknownModule type errors
9178+ let unknown_modules = errors. iter ( ) . filter_map ( |error| {
9179+ if let TypeError :: UnknownModule { name, location, .. } = error {
9180+ return Some ( UnknownModule { name, location } ) ;
9181+ }
9182+
9183+ None
9184+ } ) ;
9185+
9186+ // For each UnknownModule type error, check to see if it contains the
9187+ // incoming code action & if so, add a document change to create the module
9188+ for unknown_module in unknown_modules {
9189+ // Was this code action triggered within the UnknownModule error?
9190+ let error_contains_action = unknown_module. location . contains ( code_action_span. start )
9191+ && unknown_module. location . contains ( code_action_span. end ) ;
9192+
9193+ if !error_contains_action {
9194+ continue ;
9195+ }
9196+
9197+ let uri = url_from_path ( & format ! ( "{origin_directory}/{}.gleam" , unknown_module. name) )
9198+ . expect ( "origin directory is absolute" ) ;
9199+
9200+ CodeActionBuilder :: new ( format ! (
9201+ "Create {}/{}.gleam" ,
9202+ self . module. origin. folder_name( ) ,
9203+ unknown_module. name
9204+ ) )
9205+ . kind ( CodeActionKind :: QUICKFIX )
9206+ . document_changes ( DocumentChanges :: Operations ( vec ! [
9207+ DocumentChangeOperation :: Op ( ResourceOp :: Create ( CreateFile {
9208+ uri,
9209+ options: Some ( CreateFileOptions {
9210+ overwrite: Some ( false ) ,
9211+ ignore_if_exists: Some ( true ) ,
9212+ } ) ,
9213+ annotation_id: None ,
9214+ } ) ) ,
9215+ ] ) )
9216+ . push_to ( & mut actions) ;
9217+ }
9218+
9219+ actions
9220+ }
9221+ }
0 commit comments