Skip to main content

ide_completion/completions/
mod_.rs

1//! Completes mod declarations.
2
3use std::iter;
4
5use hir::Module;
6use ide_db::{
7    FxHashSet, RootDatabase, SymbolKind,
8    base_db::{SourceDatabase, VfsPath},
9};
10use syntax::{AstNode, SyntaxKind, ast};
11
12use crate::{CompletionItem, Completions, context::CompletionContext};
13
14/// Complete mod declaration, i.e. `mod $0;`
15pub(crate) fn complete_mod(
16    acc: &mut Completions,
17    ctx: &CompletionContext<'_, '_>,
18    mod_under_caret: &ast::Module,
19) -> Option<()> {
20    if mod_under_caret.item_list().is_some() {
21        return None;
22    }
23
24    let _p = tracing::info_span!("completion::complete_mod").entered();
25
26    let mut current_module = ctx.module;
27    // For `mod $0`, `ctx.module` is its parent, but for `mod f$0`, it's `mod f` itself, but we're
28    // interested in its parent.
29    if ctx.original_token.kind() == SyntaxKind::IDENT
30        && let Some(module) =
31            ctx.original_token.parent_ancestors().nth(1).and_then(ast::Module::cast)
32    {
33        match ctx.sema.to_def(&module) {
34            Some(module) if module == current_module => {
35                if let Some(parent) = current_module.parent(ctx.db) {
36                    current_module = parent;
37                }
38            }
39            _ => {}
40        }
41    }
42
43    let module_definition_file =
44        current_module.definition_source_file_id(ctx.db).original_file(ctx.db).file_id(ctx.db);
45    let source_root_id = ctx.db.file_source_root(module_definition_file).source_root_id(ctx.db);
46    let source_root = ctx.db.source_root(source_root_id).source_root(ctx.db);
47    let crate_roots = base_db::all_crates(ctx.db)
48        .iter()
49        .map(|krate| krate.root_file_id(ctx.db).file_id(ctx.db))
50        .collect::<FxHashSet<_>>();
51
52    let directory_to_look_for_submodules = directory_to_look_for_submodules(
53        current_module,
54        ctx.db,
55        crate_roots.contains(&module_definition_file),
56        source_root.path_for_file(&module_definition_file)?,
57    )?;
58
59    let existing_mod_declarations = current_module
60        .children(ctx.db)
61        .filter_map(|module| Some(module.name(ctx.db)?.as_str().to_owned()))
62        .filter(|module| module != ctx.original_token.text())
63        .collect::<FxHashSet<_>>();
64
65    let module_declaration_file =
66        current_module.declaration_source_range(ctx.db).map(|module_declaration_source_file| {
67            module_declaration_source_file.file_id.original_file(ctx.db)
68        });
69
70    source_root
71        .iter()
72        .filter(|&submodule_candidate_file| submodule_candidate_file != module_definition_file)
73        .filter(|&submodule_candidate_file| {
74            module_declaration_file.is_none_or(|it| it.file_id(ctx.db) != submodule_candidate_file)
75        })
76        .filter_map(|submodule_file| {
77            let submodule_path = source_root.path_for_file(&submodule_file)?;
78            let directory_with_submodule = submodule_path.parent()?;
79            let (name, ext) = submodule_path.name_and_extension()?;
80            if ext != Some("rs") || crate_roots.contains(&submodule_file) {
81                return None;
82            }
83            match name {
84                "mod" => {
85                    if directory_with_submodule.parent()? == directory_to_look_for_submodules {
86                        match directory_with_submodule.name_and_extension()? {
87                            (directory_name, None) => Some(directory_name.to_owned()),
88                            _ => None,
89                        }
90                    } else {
91                        None
92                    }
93                }
94                file_name if directory_with_submodule == directory_to_look_for_submodules => {
95                    Some(file_name.to_owned())
96                }
97                _ => None,
98            }
99        })
100        .filter(|name| !existing_mod_declarations.contains(name))
101        .for_each(|name| {
102            let mut item =
103                CompletionItem::new(SymbolKind::Module, ctx.source_range(), &name, ctx.edition);
104            let mut insert_text = name.clone();
105            if mod_under_caret.semicolon_token().is_none() {
106                item.lookup_by(&name).label(format!("{name};"));
107                insert_text.push(';');
108            }
109            if syntax::utils::is_raw_identifier(&name, ctx.edition) {
110                insert_text.insert_str(0, "r#");
111            }
112            item.insert_text(insert_text);
113            item.add_to(acc, ctx.db)
114        });
115
116    Some(())
117}
118
119fn directory_to_look_for_submodules(
120    module: Module,
121    db: &RootDatabase,
122    is_root: bool,
123    module_file_path: &VfsPath,
124) -> Option<VfsPath> {
125    let directory_with_module_path = module_file_path.parent()?;
126    let (name, ext) = module_file_path.name_and_extension()?;
127    if ext != Some("rs") {
128        return None;
129    }
130    let base_directory = match name {
131        "mod" => Some(directory_with_module_path),
132        _ if is_root => Some(directory_with_module_path),
133        regular_rust_file_name => {
134            if matches!(
135                (
136                    directory_with_module_path
137                        .parent()
138                        .as_ref()
139                        .and_then(|path| path.name_and_extension()),
140                    directory_with_module_path.name_and_extension(),
141                ),
142                (Some(("src", None)), Some(("bin", None)))
143            ) {
144                // files in /src/bin/ can import each other directly
145                Some(directory_with_module_path)
146            } else {
147                directory_with_module_path.join(regular_rust_file_name)
148            }
149        }
150    }?;
151
152    module_chain_to_containing_module_file(module, db)
153        .into_iter()
154        .filter_map(|module| module.name(db))
155        .try_fold(base_directory, |path, name| path.join(name.as_str()))
156}
157
158fn module_chain_to_containing_module_file(
159    current_module: Module,
160    db: &RootDatabase,
161) -> Vec<Module> {
162    let mut path =
163        iter::successors(Some(current_module), |current_module| current_module.parent(db))
164            .take_while(|current_module| current_module.is_inline(db))
165            .collect::<Vec<_>>();
166    path.reverse();
167    path
168}
169
170#[cfg(test)]
171mod tests {
172    use expect_test::expect;
173
174    use crate::tests::{check, check_edit};
175
176    #[test]
177    fn lib_module_completion() {
178        check(
179            r#"
180//- /lib.rs
181mod $0
182//- /foo.rs
183fn foo() {}
184//- /foo/ignored_foo.rs
185fn ignored_foo() {}
186//- /bar/mod.rs
187fn bar() {}
188//- /bar/ignored_bar.rs
189fn ignored_bar() {}
190"#,
191            expect![[r#"
192                md bar;
193                md foo;
194            "#]],
195        );
196    }
197
198    #[test]
199    fn no_module_completion_with_module_body() {
200        check(
201            r#"
202//- /lib.rs
203mod $0 {
204
205}
206//- /foo.rs
207fn foo() {}
208"#,
209            expect![[r#""#]],
210        );
211    }
212
213    #[test]
214    fn exclude_exists_module_declarations() {
215        check(
216            r#"
217//- /lib.rs
218mod foo;
219mod r#struct;
220mod $0
221//- /foo.rs
222//- /bar.rs
223//- /struct.rs
224"#,
225            expect![[r#"
226                md bar;
227            "#]],
228        );
229    }
230
231    #[test]
232    fn exclude_crate_roots() {
233        check(
234            r#"
235//- /lib.rs crate:lib
236mod $0
237//- /main.rs crate:main
238//- /sub.rs
239"#,
240            expect![[r#"
241                md sub;
242            "#]],
243        );
244        check(
245            r#"
246//- /main.rs crate:main
247mod $0
248//- /lib.rs crate:lib
249//- /sub.rs
250"#,
251            expect![[r#"
252                md sub;
253            "#]],
254        );
255    }
256
257    #[test]
258    fn contained_submodule_name_same_as_root() {
259        check(
260            r#"
261//- /lib.rs
262mod sub {
263    mod $0
264}
265//- /sub/lib.rs
266//- /sub/main.rs
267"#,
268            expect![[r#"
269                md lib;
270                md main;
271            "#]],
272        );
273    }
274
275    #[test]
276    fn inside_name_same_as_root() {
277        check(
278            r#"
279//- /foo/lib.rs
280mod $0
281//- /foo/lib/loader.rs
282//- /lib.rs
283mod foo {
284    mod lib;
285}
286"#,
287            expect![[r#"
288                md loader;
289            "#]],
290        );
291    }
292
293    #[test]
294    fn main_module_completion() {
295        check(
296            r#"
297//- /main.rs
298mod $0
299//- /foo.rs
300fn foo() {}
301//- /foo/ignored_foo.rs
302fn ignored_foo() {}
303//- /bar/mod.rs
304fn bar() {}
305//- /bar/ignored_bar.rs
306fn ignored_bar() {}
307"#,
308            expect![[r#"
309                md bar;
310                md foo;
311            "#]],
312        );
313    }
314
315    #[test]
316    fn main_test_module_completion() {
317        check(
318            r#"
319//- /main.rs
320mod tests {
321    mod $0;
322}
323//- /tests/foo.rs
324fn foo() {}
325"#,
326            expect![[r#"
327                md foo
328            "#]],
329        );
330    }
331
332    #[test]
333    fn directly_nested_module_completion() {
334        check(
335            r#"
336//- /lib.rs
337mod foo;
338//- /foo.rs
339mod $0;
340//- /foo/bar.rs
341fn bar() {}
342//- /foo/bar/ignored_bar.rs
343fn ignored_bar() {}
344//- /foo/baz/mod.rs
345fn baz() {}
346//- /foo/moar/ignored_moar.rs
347fn ignored_moar() {}
348"#,
349            expect![[r#"
350                md bar
351                md baz
352            "#]],
353        );
354    }
355
356    #[test]
357    fn nested_in_source_module_completion() {
358        check(
359            r#"
360//- /lib.rs
361mod foo;
362//- /foo.rs
363mod bar {
364    mod $0
365}
366//- /foo/bar/baz.rs
367fn baz() {}
368"#,
369            expect![[r#"
370                md baz;
371            "#]],
372        );
373    }
374
375    // FIXME binary modules are not supported in tests properly
376    // Binary modules are a bit special, they allow importing the modules from `/src/bin`
377    // and that's why are good to test two things:
378    // * no cycles are allowed in mod declarations
379    // * no modules from the parent directory are proposed
380    // Unfortunately, binary modules support is in cargo not rustc,
381    // hence the test does not work now
382    //
383    // #[test]
384    // fn regular_bin_module_completion() {
385    //     check(
386    //         r#"
387    //         //- /src/bin.rs
388    //         fn main() {}
389    //         //- /src/bin/foo.rs
390    //         mod $0
391    //         //- /src/bin/bar.rs
392    //         fn bar() {}
393    //         //- /src/bin/bar/bar_ignored.rs
394    //         fn bar_ignored() {}
395    //     "#,
396    //         expect![[r#"
397    //             md bar;
398    //         "#]],foo
399    //     );
400    // }
401
402    #[test]
403    fn already_declared_bin_module_completion_omitted() {
404        check(
405            r#"
406//- /src/bin.rs crate:main
407fn main() {}
408//- /src/bin/foo.rs
409mod $0
410//- /src/bin/bar.rs
411mod foo;
412fn bar() {}
413//- /src/bin/bar/bar_ignored.rs
414fn bar_ignored() {}
415"#,
416            expect![[r#""#]],
417        );
418    }
419
420    #[test]
421    fn name_partially_typed() {
422        check(
423            r#"
424//- /lib.rs
425mod f$0
426//- /foo.rs
427fn foo() {}
428//- /foo/ignored_foo.rs
429fn ignored_foo() {}
430//- /bar/mod.rs
431fn bar() {}
432//- /bar/ignored_bar.rs
433fn ignored_bar() {}
434"#,
435            expect![[r#"
436                md bar;
437                md foo;
438            "#]],
439        );
440    }
441
442    #[test]
443    fn semi_colon_completion() {
444        check(
445            r#"
446//- /lib.rs
447mod foo;
448//- /foo.rs
449mod bar {
450    mod baz$0
451}
452//- /foo/bar/baz.rs
453fn baz() {}
454"#,
455            expect![[r#"
456                md baz;
457            "#]],
458        );
459        check(
460            r#"
461//- /lib.rs
462mod foo;
463//- /foo.rs
464mod bar {
465    mod struct$0
466}
467//- /foo/bar/struct.rs
468fn baz() {}
469"#,
470            expect![[r#"
471                md struct;
472            "#]],
473        );
474    }
475
476    #[test]
477    fn insert_escaped_name() {
478        check(
479            r#"
480//- /lib.rs
481mod s$0
482//- /struct.rs
483fn baz() {}
484//- /normal.rs
485fn baz() {}
486"#,
487            expect![[r#"
488                md normal;
489                md struct;
490            "#]],
491        );
492        check_edit(
493            "normal",
494            r#"
495//- /lib.rs
496mod s$0
497//- /struct.rs
498fn baz() {}
499//- /normal.rs
500fn baz() {}
501"#,
502            r#"
503mod normal;
504"#,
505        );
506        check_edit(
507            "struct",
508            r#"
509//- /lib.rs
510mod s$0
511//- /struct.rs
512fn baz() {}
513//- /normal.rs
514fn baz() {}
515"#,
516            r#"
517mod r#struct;
518"#,
519        );
520    }
521}