]> git.lizzy.rs Git - rust.git/blob - crates/ide_completion/src/completions/mod_.rs
28be83f196aded7896ef1b4d8a6fa32f84eacf00
[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::{patterns::ImmediateLocation, CompletionItem};
13
14 use crate::{context::CompletionContext, 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.completion_location {
19         Some(ImmediateLocation::ModDeclaration(mod_under_caret)) => 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             let item = CompletionItem::new(SymbolKind::Module, ctx.source_range(), &label);
84             item.add_to(acc)
85         });
86
87     Some(())
88 }
89
90 fn directory_to_look_for_submodules(
91     module: Module,
92     db: &RootDatabase,
93     module_file_path: &VfsPath,
94 ) -> Option<VfsPath> {
95     let directory_with_module_path = module_file_path.parent()?;
96     let (name, ext) = module_file_path.name_and_extension()?;
97     if ext != Some("rs") {
98         return None;
99     }
100     let base_directory = match name {
101         "mod" | "lib" | "main" => Some(directory_with_module_path),
102         regular_rust_file_name => {
103             if matches!(
104                 (
105                     directory_with_module_path
106                         .parent()
107                         .as_ref()
108                         .and_then(|path| path.name_and_extension()),
109                     directory_with_module_path.name_and_extension(),
110                 ),
111                 (Some(("src", None)), Some(("bin", None)))
112             ) {
113                 // files in /src/bin/ can import each other directly
114                 Some(directory_with_module_path)
115             } else {
116                 directory_with_module_path.join(regular_rust_file_name)
117             }
118         }
119     }?;
120
121     module_chain_to_containing_module_file(module, db)
122         .into_iter()
123         .filter_map(|module| module.name(db))
124         .try_fold(base_directory, |path, name| path.join(&name.to_string()))
125 }
126
127 fn module_chain_to_containing_module_file(
128     current_module: Module,
129     db: &RootDatabase,
130 ) -> Vec<Module> {
131     let mut path =
132         iter::successors(Some(current_module), |current_module| current_module.parent(db))
133             .take_while(|current_module| {
134                 matches!(current_module.definition_source(db).value, ModuleSource::Module(_))
135             })
136             .collect::<Vec<_>>();
137     path.reverse();
138     path
139 }
140
141 #[cfg(test)]
142 mod tests {
143     use expect_test::{expect, Expect};
144
145     use crate::tests::completion_list;
146
147     fn check(ra_fixture: &str, expect: Expect) {
148         let actual = completion_list(ra_fixture);
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 }