1
1
//! Diagnostic emitted for files that aren't part of any crate.
2
2
3
- use hir:: db:: DefDatabase ;
3
+ use std:: iter;
4
+
5
+ use hir:: { db:: DefDatabase , InFile , ModuleSource } ;
4
6
use ide_db:: {
5
7
base_db:: { FileId , FileLoader , SourceDatabase , SourceDatabaseExt } ,
6
8
source_change:: SourceChange ,
7
9
RootDatabase ,
8
10
} ;
9
11
use syntax:: {
10
- ast:: { self , HasModuleItem , HasName } ,
12
+ ast:: { self , edit :: IndentLevel , HasModuleItem , HasName } ,
11
13
AstNode , TextRange , TextSize ,
12
14
} ;
13
15
use text_edit:: TextEdit ;
@@ -42,47 +44,99 @@ fn fixes(ctx: &DiagnosticsContext<'_>, file_id: FileId) -> Option<Vec<Assist>> {
42
44
43
45
let source_root = ctx. sema . db . source_root ( ctx. sema . db . file_source_root ( file_id) ) ;
44
46
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`
50
47
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" => {
66
53
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 ( ) ) ,
71
57
} ;
72
58
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
+ }
81
91
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;
84
134
}
85
135
}
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) ;
86
140
}
87
141
}
88
142
@@ -92,6 +146,7 @@ fn fixes(ctx: &DiagnosticsContext<'_>, file_id: FileId) -> Option<Vec<Assist>> {
92
146
fn make_fixes (
93
147
db : & RootDatabase ,
94
148
parent_file_id : FileId ,
149
+ source : ModuleSource ,
95
150
new_mod_name : & str ,
96
151
added_file_id : FileId ,
97
152
) -> Option < Vec < Assist > > {
@@ -102,14 +157,18 @@ fn make_fixes(
102
157
let mod_decl = format ! ( "mod {new_mod_name};" ) ;
103
158
let pub_mod_decl = format ! ( "pub mod {new_mod_name};" ) ;
104
159
105
- let ast: ast:: SourceFile = db. parse ( parent_file_id) . tree ( ) ;
106
-
107
160
let mut mod_decl_builder = TextEdit :: builder ( ) ;
108
161
let mut pub_mod_decl_builder = TextEdit :: builder ( ) ;
109
162
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
+
110
169
// If there's an existing `mod m;` statement matching the new one, don't emit a fix (it's
111
170
// probably `#[cfg]`d out).
112
- for item in ast . items ( ) {
171
+ for item in items. clone ( ) {
113
172
if let ast:: Item :: Module ( m) = item {
114
173
if let Some ( name) = m. name ( ) {
115
174
if m. item_list ( ) . is_none ( ) && name. to_string ( ) == new_mod_name {
@@ -121,28 +180,40 @@ fn make_fixes(
121
180
}
122
181
123
182
// 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 ( ) {
125
184
Some ( last) => {
126
185
cov_mark:: hit!( unlinked_file_append_to_existing_mods) ;
127
186
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}" ) ) ;
130
190
}
131
191
None => {
132
192
// Prepend before the first item in the file.
133
- match ast . items ( ) . next ( ) {
134
- Some ( item ) => {
193
+ match items. next ( ) {
194
+ Some ( first ) => {
135
195
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}" ) ) ;
139
200
}
140
201
None => {
141
202
// No items in the file, so just append at the end.
142
203
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 " ) ) ;
146
217
}
147
218
}
148
219
}
@@ -167,7 +238,6 @@ fn make_fixes(
167
238
168
239
#[ cfg( test) ]
169
240
mod tests {
170
-
171
241
use crate :: tests:: { check_diagnostics, check_fix, check_fixes, check_no_fix} ;
172
242
173
243
#[ test]
330
400
mod foo;
331
401
332
402
//- /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
+ }
333
461
"# ,
334
462
) ;
335
463
}
0 commit comments