11//! Diagnostic emitted for files that aren't part of any crate.
22
3- use hir:: db:: DefDatabase ;
3+ use std:: iter;
4+
5+ use hir:: { db:: DefDatabase , InFile , ModuleSource } ;
46use ide_db:: {
57 base_db:: { FileId , FileLoader , SourceDatabase , SourceDatabaseExt } ,
68 source_change:: SourceChange ,
79 RootDatabase ,
810} ;
911use syntax:: {
10- ast:: { self , HasModuleItem , HasName } ,
12+ ast:: { self , edit :: IndentLevel , HasModuleItem , HasName } ,
1113 AstNode , TextRange , TextSize ,
1214} ;
1315use text_edit:: TextEdit ;
@@ -42,47 +44,99 @@ fn fixes(ctx: &DiagnosticsContext<'_>, file_id: FileId) -> Option<Vec<Assist>> {
4244
4345 let source_root = ctx. sema . db . source_root ( ctx. sema . db . file_source_root ( file_id) ) ;
4446 let our_path = source_root. path_for_file ( & file_id) ?;
45- let ( mut module_name, _) = our_path. name_and_extension ( ) ?;
46-
47- // Candidates to look for:
48- // - `mod.rs`, `main.rs` and `lib.rs` in the same folder
49- // - `$dir.rs` in the parent folder, where `$dir` is the directory containing `self.file_id`
5047 let parent = our_path. parent ( ) ?;
51- let paths = {
52- let parent = if module_name == "mod" {
53- // for mod.rs we need to actually look up one higher
54- // and take the parent as our to be module name
55- let ( name, _) = parent. name_and_extension ( ) ?;
56- module_name = name;
57- parent. parent ( ) ?
58- } else {
59- parent
60- } ;
61- let mut paths =
62- vec ! [ parent. join( "mod.rs" ) ?, parent. join( "lib.rs" ) ?, parent. join( "main.rs" ) ?] ;
63-
64- // `submod/bla.rs` -> `submod.rs`
65- let parent_mod = ( || {
48+ let ( module_name, _) = our_path. name_and_extension ( ) ?;
49+ let ( parent, module_name) = match module_name {
50+ // for mod.rs we need to actually look up one higher
51+ // and take the parent as our to be module name
52+ "mod" => {
6653 let ( name, _) = parent. name_and_extension ( ) ?;
67- parent. parent ( ) ?. join ( & format ! ( "{name}.rs" ) )
68- } ) ( ) ;
69- paths. extend ( parent_mod) ;
70- paths
54+ ( parent. parent ( ) ?, name. to_owned ( ) )
55+ }
56+ _ => ( parent, module_name. to_owned ( ) ) ,
7157 } ;
7258
73- for & parent_id in paths. iter ( ) . filter_map ( |path| source_root. file_for_path ( path) ) {
74- for & krate in ctx. sema . db . relevant_crates ( parent_id) . iter ( ) {
75- let crate_def_map = ctx. sema . db . crate_def_map ( krate) ;
76- for ( _, module) in crate_def_map. modules ( ) {
77- if module. origin . is_inline ( ) {
78- // We don't handle inline `mod parent {}`s, they use different paths.
79- continue ;
80- }
59+ // check crate roots, i.e. main.rs, lib.rs, ...
60+ ' crates: for & krate in & * ctx. sema . db . relevant_crates ( file_id) {
61+ let crate_def_map = ctx. sema . db . crate_def_map ( krate) ;
62+
63+ let root_module = & crate_def_map[ crate_def_map. root ( ) ] ;
64+ let Some ( root_file_id) = root_module. origin . file_id ( ) else { continue } ;
65+ let Some ( crate_root_path) = source_root. path_for_file ( & root_file_id) else { continue } ;
66+ let Some ( rel) = parent. strip_prefix ( & crate_root_path. parent ( ) ?) else { continue } ;
67+
68+ // try resolving the relative difference of the paths as inline modules
69+ let mut current = root_module;
70+ for ele in rel. as_ref ( ) . components ( ) {
71+ let seg = match ele {
72+ std:: path:: Component :: Normal ( seg) => seg. to_str ( ) ?,
73+ std:: path:: Component :: RootDir => continue ,
74+ // shouldn't occur
75+ _ => continue ' crates,
76+ } ;
77+ match current. children . iter ( ) . find ( |( name, _) | name. to_smol_str ( ) == seg) {
78+ Some ( ( _, & child) ) => current = & crate_def_map[ child] ,
79+ None => continue ' crates,
80+ }
81+ if !current. origin . is_inline ( ) {
82+ continue ' crates;
83+ }
84+ }
85+
86+ let InFile { file_id : parent_file_id, value : source } =
87+ current. definition_source ( ctx. sema . db ) ;
88+ let parent_file_id = parent_file_id. file_id ( ) ?;
89+ return make_fixes ( ctx. sema . db , parent_file_id, source, & module_name, file_id) ;
90+ }
8191
82- if module. origin . file_id ( ) == Some ( parent_id) {
83- return make_fixes ( ctx. sema . db , parent_id, module_name, file_id) ;
92+ // if we aren't adding to a crate root, walk backwards such that we support `#[path = ...]` overrides if possible
93+
94+ // build all parent paths of the form `../module_name/mod.rs` and `../module_name.rs`
95+ let paths = iter:: successors ( Some ( parent. clone ( ) ) , |prev| prev. parent ( ) ) . filter_map ( |path| {
96+ let parent = path. parent ( ) ?;
97+ let ( name, _) = path. name_and_extension ( ) ?;
98+ Some ( ( [ parent. join ( & format ! ( "{name}.rs" ) ) ?, path. join ( "mod.rs" ) ?] , name. to_owned ( ) ) )
99+ } ) ;
100+ let mut stack = vec ! [ ] ;
101+ let & parent_id =
102+ paths. inspect ( |( _, name) | stack. push ( name. clone ( ) ) ) . find_map ( |( paths, _) | {
103+ paths. into_iter ( ) . find_map ( |path| source_root. file_for_path ( & path) )
104+ } ) ?;
105+ stack. pop ( ) ;
106+ ' crates: for & krate in ctx. sema . db . relevant_crates ( parent_id) . iter ( ) {
107+ let crate_def_map = ctx. sema . db . crate_def_map ( krate) ;
108+ let Some ( ( _, module) ) =
109+ crate_def_map. modules ( )
110+ . find ( |( _, module) | module. origin . file_id ( ) == Some ( parent_id) && !module. origin . is_inline ( ) )
111+ else { continue } ;
112+
113+ if stack. is_empty ( ) {
114+ return make_fixes (
115+ ctx. sema . db ,
116+ parent_id,
117+ module. definition_source ( ctx. sema . db ) . value ,
118+ & module_name,
119+ file_id,
120+ ) ;
121+ } else {
122+ // direct parent file is missing,
123+ // try finding a parent that has an inline tree from here on
124+ let mut current = module;
125+ for s in stack. iter ( ) . rev ( ) {
126+ match module. children . iter ( ) . find ( |( name, _) | name. to_smol_str ( ) == s) {
127+ Some ( ( _, child) ) => {
128+ current = & crate_def_map[ * child] ;
129+ }
130+ None => continue ' crates,
131+ }
132+ if !current. origin . is_inline ( ) {
133+ continue ' crates;
84134 }
85135 }
136+ let InFile { file_id : parent_file_id, value : source } =
137+ current. definition_source ( ctx. sema . db ) ;
138+ let parent_file_id = parent_file_id. file_id ( ) ?;
139+ return make_fixes ( ctx. sema . db , parent_file_id, source, & module_name, file_id) ;
86140 }
87141 }
88142
@@ -92,6 +146,7 @@ fn fixes(ctx: &DiagnosticsContext<'_>, file_id: FileId) -> Option<Vec<Assist>> {
92146fn make_fixes (
93147 db : & RootDatabase ,
94148 parent_file_id : FileId ,
149+ source : ModuleSource ,
95150 new_mod_name : & str ,
96151 added_file_id : FileId ,
97152) -> Option < Vec < Assist > > {
@@ -102,14 +157,18 @@ fn make_fixes(
102157 let mod_decl = format ! ( "mod {new_mod_name};" ) ;
103158 let pub_mod_decl = format ! ( "pub mod {new_mod_name};" ) ;
104159
105- let ast: ast:: SourceFile = db. parse ( parent_file_id) . tree ( ) ;
106-
107160 let mut mod_decl_builder = TextEdit :: builder ( ) ;
108161 let mut pub_mod_decl_builder = TextEdit :: builder ( ) ;
109162
163+ let mut items = match & source {
164+ ModuleSource :: SourceFile ( it) => it. items ( ) ,
165+ ModuleSource :: Module ( it) => it. item_list ( ) ?. items ( ) ,
166+ ModuleSource :: BlockExpr ( _) => return None ,
167+ } ;
168+
110169 // If there's an existing `mod m;` statement matching the new one, don't emit a fix (it's
111170 // probably `#[cfg]`d out).
112- for item in ast . items ( ) {
171+ for item in items. clone ( ) {
113172 if let ast:: Item :: Module ( m) = item {
114173 if let Some ( name) = m. name ( ) {
115174 if m. item_list ( ) . is_none ( ) && name. to_string ( ) == new_mod_name {
@@ -121,28 +180,40 @@ fn make_fixes(
121180 }
122181
123182 // If there are existing `mod m;` items, append after them (after the first group of them, rather).
124- match ast . items ( ) . skip_while ( |item| !is_outline_mod ( item) ) . take_while ( is_outline_mod) . last ( ) {
183+ match items. clone ( ) . skip_while ( |item| !is_outline_mod ( item) ) . take_while ( is_outline_mod) . last ( ) {
125184 Some ( last) => {
126185 cov_mark:: hit!( unlinked_file_append_to_existing_mods) ;
127186 let offset = last. syntax ( ) . text_range ( ) . end ( ) ;
128- mod_decl_builder. insert ( offset, format ! ( "\n {mod_decl}" ) ) ;
129- pub_mod_decl_builder. insert ( offset, format ! ( "\n {pub_mod_decl}" ) ) ;
187+ let indent = IndentLevel :: from_node ( last. syntax ( ) ) ;
188+ mod_decl_builder. insert ( offset, format ! ( "\n {indent}{mod_decl}" ) ) ;
189+ pub_mod_decl_builder. insert ( offset, format ! ( "\n {indent}{pub_mod_decl}" ) ) ;
130190 }
131191 None => {
132192 // Prepend before the first item in the file.
133- match ast . items ( ) . next ( ) {
134- Some ( item ) => {
193+ match items. next ( ) {
194+ Some ( first ) => {
135195 cov_mark:: hit!( unlinked_file_prepend_before_first_item) ;
136- let offset = item. syntax ( ) . text_range ( ) . start ( ) ;
137- mod_decl_builder. insert ( offset, format ! ( "{mod_decl}\n \n " ) ) ;
138- pub_mod_decl_builder. insert ( offset, format ! ( "{pub_mod_decl}\n \n " ) ) ;
196+ let offset = first. syntax ( ) . text_range ( ) . start ( ) ;
197+ let indent = IndentLevel :: from_node ( first. syntax ( ) ) ;
198+ mod_decl_builder. insert ( offset, format ! ( "{mod_decl}\n \n {indent}" ) ) ;
199+ pub_mod_decl_builder. insert ( offset, format ! ( "{pub_mod_decl}\n \n {indent}" ) ) ;
139200 }
140201 None => {
141202 // No items in the file, so just append at the end.
142203 cov_mark:: hit!( unlinked_file_empty_file) ;
143- let offset = ast. syntax ( ) . text_range ( ) . end ( ) ;
144- mod_decl_builder. insert ( offset, format ! ( "{mod_decl}\n " ) ) ;
145- pub_mod_decl_builder. insert ( offset, format ! ( "{pub_mod_decl}\n " ) ) ;
204+ let mut indent = IndentLevel :: from ( 0 ) ;
205+ let offset = match & source {
206+ ModuleSource :: SourceFile ( it) => it. syntax ( ) . text_range ( ) . end ( ) ,
207+ ModuleSource :: Module ( it) => {
208+ indent = IndentLevel :: from_node ( it. syntax ( ) ) + 1 ;
209+ it. item_list ( ) ?. r_curly_token ( ) ?. text_range ( ) . start ( )
210+ }
211+ ModuleSource :: BlockExpr ( it) => {
212+ it. stmt_list ( ) ?. r_curly_token ( ) ?. text_range ( ) . start ( )
213+ }
214+ } ;
215+ mod_decl_builder. insert ( offset, format ! ( "{indent}{mod_decl}\n " ) ) ;
216+ pub_mod_decl_builder. insert ( offset, format ! ( "{indent}{pub_mod_decl}\n " ) ) ;
146217 }
147218 }
148219 }
@@ -167,7 +238,6 @@ fn make_fixes(
167238
168239#[ cfg( test) ]
169240mod tests {
170-
171241 use crate :: tests:: { check_diagnostics, check_fix, check_fixes, check_no_fix} ;
172242
173243 #[ test]
330400mod foo;
331401
332402//- /foo.rs
403+ "# ,
404+ ) ;
405+ }
406+
407+ #[ test]
408+ fn unlinked_file_insert_into_inline_simple ( ) {
409+ check_fix (
410+ r#"
411+ //- /main.rs
412+ mod bar;
413+ //- /bar.rs
414+ mod foo {
415+ }
416+ //- /bar/foo/baz.rs
417+ $0
418+ "# ,
419+ r#"
420+ mod foo {
421+ mod baz;
422+ }
423+ "# ,
424+ ) ;
425+ }
426+
427+ #[ test]
428+ fn unlinked_file_insert_into_inline_simple_modrs ( ) {
429+ check_fix (
430+ r#"
431+ //- /main.rs
432+ mod bar;
433+ //- /bar.rs
434+ mod baz {
435+ }
436+ //- /bar/baz/foo/mod.rs
437+ $0
438+ "# ,
439+ r#"
440+ mod baz {
441+ mod foo;
442+ }
443+ "# ,
444+ ) ;
445+ }
446+
447+ #[ test]
448+ fn unlinked_file_insert_into_inline_simple_modrs_main ( ) {
449+ check_fix (
450+ r#"
451+ //- /main.rs
452+ mod bar {
453+ }
454+ //- /bar/foo/mod.rs
455+ $0
456+ "# ,
457+ r#"
458+ mod bar {
459+ mod foo;
460+ }
333461"# ,
334462 ) ;
335463 }
0 commit comments