]> git.lizzy.rs Git - rust.git/blob - crates/ide_completion/src/completions/mod_.rs
Merge #7707
[rust.git] / crates / ide_completion / src / completions / mod_.rs
1 //! Completes mod declarations.
2
3 use std::iter;
4
5 use hir::{Module, ModuleSource};
6 use ide_db::{
7     base_db::{SourceDatabaseExt, VfsPath},
8     RootDatabase, SymbolKind,
9 };
10 use rustc_hash::FxHashSet;
11
12 use crate::CompletionItem;
13
14 use crate::{context::CompletionContext, item::CompletionKind, Completions};
15
16 /// Complete mod declaration, i.e. `mod $0 ;`
17 pub(crate) fn complete_mod(acc: &mut Completions, ctx: &CompletionContext) -> Option<()> {
18     let mod_under_caret = match &ctx.mod_declaration_under_caret {
19         Some(mod_under_caret) if mod_under_caret.item_list().is_none() => mod_under_caret,
20         _ => return None,
21     };
22
23     let _p = profile::span("completion::complete_mod");
24
25     let current_module = ctx.scope.module()?;
26
27     let module_definition_file =
28         current_module.definition_source(ctx.db).file_id.original_file(ctx.db);
29     let source_root = ctx.db.source_root(ctx.db.file_source_root(module_definition_file));
30     let directory_to_look_for_submodules = directory_to_look_for_submodules(
31         current_module,
32         ctx.db,
33         source_root.path_for_file(&module_definition_file)?,
34     )?;
35
36     let existing_mod_declarations = current_module
37         .children(ctx.db)
38         .filter_map(|module| Some(module.name(ctx.db)?.to_string()))
39         .collect::<FxHashSet<_>>();
40
41     let module_declaration_file =
42         current_module.declaration_source(ctx.db).map(|module_declaration_source_file| {
43             module_declaration_source_file.file_id.original_file(ctx.db)
44         });
45
46     source_root
47         .iter()
48         .filter(|submodule_candidate_file| submodule_candidate_file != &module_definition_file)
49         .filter(|submodule_candidate_file| {
50             Some(submodule_candidate_file) != module_declaration_file.as_ref()
51         })
52         .filter_map(|submodule_file| {
53             let submodule_path = source_root.path_for_file(&submodule_file)?;
54             let directory_with_submodule = submodule_path.parent()?;
55             let (name, ext) = submodule_path.name_and_extension()?;
56             if ext != Some("rs") {
57                 return None;
58             }
59             match name {
60                 "lib" | "main" => None,
61                 "mod" => {
62                     if directory_with_submodule.parent()? == directory_to_look_for_submodules {
63                         match directory_with_submodule.name_and_extension()? {
64                             (directory_name, None) => Some(directory_name.to_owned()),
65                             _ => None,
66                         }
67                     } else {
68                         None
69                     }
70                 }
71                 file_name if directory_with_submodule == directory_to_look_for_submodules => {
72                     Some(file_name.to_owned())
73                 }
74                 _ => None,
75             }
76         })
77         .filter(|name| !existing_mod_declarations.contains(name))
78         .for_each(|submodule_name| {
79             let mut label = submodule_name;
80             if mod_under_caret.semicolon_token().is_none() {
81                 label.push(';');
82             }
83             CompletionItem::new(CompletionKind::Magic, ctx.source_range(), &label)
84                 .kind(SymbolKind::Module)
85                 .add_to(acc)
86         });
87
88     Some(())
89 }
90
91 fn directory_to_look_for_submodules(
92     module: Module,
93     db: &RootDatabase,
94     module_file_path: &VfsPath,
95 ) -> Option<VfsPath> {
96     let directory_with_module_path = module_file_path.parent()?;
97     let (name, ext) = module_file_path.name_and_extension()?;
98     if ext != Some("rs") {
99         return None;
100     }
101     let base_directory = match name {
102         "mod" | "lib" | "main" => Some(directory_with_module_path),
103         regular_rust_file_name => {
104             if matches!(
105                 (
106                     directory_with_module_path
107                         .parent()
108                         .as_ref()
109                         .and_then(|path| path.name_and_extension()),
110                     directory_with_module_path.name_and_extension(),
111                 ),
112                 (Some(("src", None)), Some(("bin", None)))
113             ) {
114                 // files in /src/bin/ can import each other directly
115                 Some(directory_with_module_path)
116             } else {
117                 directory_with_module_path.join(regular_rust_file_name)
118             }
119         }
120     }?;
121
122     module_chain_to_containing_module_file(module, db)
123         .into_iter()
124         .filter_map(|module| module.name(db))
125         .try_fold(base_directory, |path, name| path.join(&name.to_string()))
126 }
127
128 fn module_chain_to_containing_module_file(
129     current_module: Module,
130     db: &RootDatabase,
131 ) -> Vec<Module> {
132     let mut path =
133         iter::successors(Some(current_module), |current_module| current_module.parent(db))
134             .take_while(|current_module| {
135                 matches!(current_module.definition_source(db).value, ModuleSource::Module(_))
136             })
137             .collect::<Vec<_>>();
138     path.reverse();
139     path
140 }
141
142 #[cfg(test)]
143 mod tests {
144     use crate::{test_utils::completion_list, CompletionKind};
145     use expect_test::{expect, Expect};
146
147     fn check(ra_fixture: &str, expect: Expect) {
148         let actual = completion_list(ra_fixture, CompletionKind::Magic);
149         expect.assert_eq(&actual);
150     }
151
152     #[test]
153     fn lib_module_completion() {
154         check(
155             r#"
156             //- /lib.rs
157             mod $0
158             //- /foo.rs
159             fn foo() {}
160             //- /foo/ignored_foo.rs
161             fn ignored_foo() {}
162             //- /bar/mod.rs
163             fn bar() {}
164             //- /bar/ignored_bar.rs
165             fn ignored_bar() {}
166         "#,
167             expect![[r#"
168                 md foo;
169                 md bar;
170             "#]],
171         );
172     }
173
174     #[test]
175     fn no_module_completion_with_module_body() {
176         check(
177             r#"
178             //- /lib.rs
179             mod $0 {
180
181             }
182             //- /foo.rs
183             fn foo() {}
184         "#,
185             expect![[r#""#]],
186         );
187     }
188
189     #[test]
190     fn main_module_completion() {
191         check(
192             r#"
193             //- /main.rs
194             mod $0
195             //- /foo.rs
196             fn foo() {}
197             //- /foo/ignored_foo.rs
198             fn ignored_foo() {}
199             //- /bar/mod.rs
200             fn bar() {}
201             //- /bar/ignored_bar.rs
202             fn ignored_bar() {}
203         "#,
204             expect![[r#"
205                 md foo;
206                 md bar;
207             "#]],
208         );
209     }
210
211     #[test]
212     fn main_test_module_completion() {
213         check(
214             r#"
215             //- /main.rs
216             mod tests {
217                 mod $0;
218             }
219             //- /tests/foo.rs
220             fn foo() {}
221         "#,
222             expect![[r#"
223                 md foo
224             "#]],
225         );
226     }
227
228     #[test]
229     fn directly_nested_module_completion() {
230         check(
231             r#"
232             //- /lib.rs
233             mod foo;
234             //- /foo.rs
235             mod $0;
236             //- /foo/bar.rs
237             fn bar() {}
238             //- /foo/bar/ignored_bar.rs
239             fn ignored_bar() {}
240             //- /foo/baz/mod.rs
241             fn baz() {}
242             //- /foo/moar/ignored_moar.rs
243             fn ignored_moar() {}
244         "#,
245             expect![[r#"
246                 md bar
247                 md baz
248             "#]],
249         );
250     }
251
252     #[test]
253     fn nested_in_source_module_completion() {
254         check(
255             r#"
256             //- /lib.rs
257             mod foo;
258             //- /foo.rs
259             mod bar {
260                 mod $0
261             }
262             //- /foo/bar/baz.rs
263             fn baz() {}
264         "#,
265             expect![[r#"
266                 md baz;
267             "#]],
268         );
269     }
270
271     // FIXME binary modules are not supported in tests properly
272     // Binary modules are a bit special, they allow importing the modules from `/src/bin`
273     // and that's why are good to test two things:
274     // * no cycles are allowed in mod declarations
275     // * no modules from the parent directory are proposed
276     // Unfortunately, binary modules support is in cargo not rustc,
277     // hence the test does not work now
278     //
279     // #[test]
280     // fn regular_bin_module_completion() {
281     //     check(
282     //         r#"
283     //         //- /src/bin.rs
284     //         fn main() {}
285     //         //- /src/bin/foo.rs
286     //         mod $0
287     //         //- /src/bin/bar.rs
288     //         fn bar() {}
289     //         //- /src/bin/bar/bar_ignored.rs
290     //         fn bar_ignored() {}
291     //     "#,
292     //         expect![[r#"
293     //             md bar;
294     //         "#]],foo
295     //     );
296     // }
297
298     #[test]
299     fn already_declared_bin_module_completion_omitted() {
300         check(
301             r#"
302             //- /src/bin.rs crate:main
303             fn main() {}
304             //- /src/bin/foo.rs
305             mod $0
306             //- /src/bin/bar.rs
307             mod foo;
308             fn bar() {}
309             //- /src/bin/bar/bar_ignored.rs
310             fn bar_ignored() {}
311         "#,
312             expect![[r#""#]],
313         );
314     }
315 }