Skip to content

Commit 4f1167d

Browse files
Merge #5969
5969: Propose module name completion options r=jonas-schievink a=SomeoneToIgnore <img width="539" alt="image" src="https://user-images.githubusercontent.com/2690773/92663009-cb0aec00-f308-11ea-9ef5-1faa91518031.png"> Currently traverses the whole file set every time we try to complete the module, as discussed in https://rust-lang.zulipchat.com/#narrow/stream/185405-t-compiler.2Fwg-rls-2.2E0/topic/mod.3C.7C.3E.20completion Co-authored-by: Kirill Bulatov <[email protected]>
2 parents eb7136f + ca698a0 commit 4f1167d

File tree

10 files changed

+431
-5
lines changed

10 files changed

+431
-5
lines changed

crates/base_db/src/input.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use cfg::CfgOptions;
1212
use rustc_hash::{FxHashMap, FxHashSet};
1313
use syntax::SmolStr;
1414
use tt::TokenExpander;
15-
use vfs::file_set::FileSet;
15+
use vfs::{file_set::FileSet, VfsPath};
1616

1717
pub use vfs::FileId;
1818

@@ -43,6 +43,12 @@ impl SourceRoot {
4343
pub fn new_library(file_set: FileSet) -> SourceRoot {
4444
SourceRoot { is_library: true, file_set }
4545
}
46+
pub fn path_for_file(&self, file: &FileId) -> Option<&VfsPath> {
47+
self.file_set.path_for_file(file)
48+
}
49+
pub fn file_for_path(&self, path: &VfsPath) -> Option<&FileId> {
50+
self.file_set.file_for_path(path)
51+
}
4652
pub fn iter(&self) -> impl Iterator<Item = FileId> + '_ {
4753
self.file_set.iter()
4854
}

crates/ide/src/completion.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ mod complete_unqualified_path;
1919
mod complete_postfix;
2020
mod complete_macro_in_item_position;
2121
mod complete_trait_impl;
22+
mod complete_mod;
2223

2324
use ide_db::RootDatabase;
2425

@@ -124,6 +125,7 @@ pub(crate) fn completions(
124125
complete_postfix::complete_postfix(&mut acc, &ctx);
125126
complete_macro_in_item_position::complete_macro_in_item_position(&mut acc, &ctx);
126127
complete_trait_impl::complete_trait_impl(&mut acc, &ctx);
128+
complete_mod::complete_mod(&mut acc, &ctx);
127129

128130
Some(acc)
129131
}

crates/ide/src/completion/complete_attribute.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ use crate::completion::{
1313
};
1414

1515
pub(super) fn complete_attribute(acc: &mut Completions, ctx: &CompletionContext) -> Option<()> {
16+
if ctx.mod_declaration_under_caret.is_some() {
17+
return None;
18+
}
19+
1620
let attribute = ctx.attribute_under_caret.as_ref()?;
1721
match (attribute.path(), attribute.token_tree()) {
1822
(Some(path), Some(token_tree)) if path.to_string() == "derive" => {
Lines changed: 324 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,324 @@
1+
//! Completes mod declarations.
2+
3+
use base_db::{SourceDatabaseExt, VfsPath};
4+
use hir::{Module, ModuleSource};
5+
use ide_db::RootDatabase;
6+
use rustc_hash::FxHashSet;
7+
8+
use crate::{CompletionItem, CompletionItemKind};
9+
10+
use super::{
11+
completion_context::CompletionContext, completion_item::CompletionKind,
12+
completion_item::Completions,
13+
};
14+
15+
/// Complete mod declaration, i.e. `mod <|> ;`
16+
pub(super) fn complete_mod(acc: &mut Completions, ctx: &CompletionContext) -> Option<()> {
17+
let mod_under_caret = match &ctx.mod_declaration_under_caret {
18+
Some(mod_under_caret) if mod_under_caret.item_list().is_some() => return None,
19+
Some(mod_under_caret) => mod_under_caret,
20+
None => 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+
match submodule_path.name_and_extension()? {
56+
("lib", Some("rs")) | ("main", Some("rs")) => None,
57+
("mod", Some("rs")) => {
58+
if directory_with_submodule.parent()? == directory_to_look_for_submodules {
59+
match directory_with_submodule.name_and_extension()? {
60+
(directory_name, None) => Some(directory_name.to_owned()),
61+
_ => None,
62+
}
63+
} else {
64+
None
65+
}
66+
}
67+
(file_name, Some("rs"))
68+
if directory_with_submodule == directory_to_look_for_submodules =>
69+
{
70+
Some(file_name.to_owned())
71+
}
72+
_ => None,
73+
}
74+
})
75+
.filter(|name| !existing_mod_declarations.contains(name))
76+
.for_each(|submodule_name| {
77+
let mut label = submodule_name;
78+
if mod_under_caret.semicolon_token().is_none() {
79+
label.push(';')
80+
}
81+
acc.add(
82+
CompletionItem::new(CompletionKind::Magic, ctx.source_range(), &label)
83+
.kind(CompletionItemKind::Module),
84+
)
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 base_directory = match module_file_path.name_and_extension()? {
97+
("mod", Some("rs")) | ("lib", Some("rs")) | ("main", Some("rs")) => {
98+
Some(directory_with_module_path)
99+
}
100+
(regular_rust_file_name, Some("rs")) => {
101+
if matches!(
102+
(
103+
directory_with_module_path
104+
.parent()
105+
.as_ref()
106+
.and_then(|path| path.name_and_extension()),
107+
directory_with_module_path.name_and_extension(),
108+
),
109+
(Some(("src", None)), Some(("bin", None)))
110+
) {
111+
// files in /src/bin/ can import each other directly
112+
Some(directory_with_module_path)
113+
} else {
114+
directory_with_module_path.join(regular_rust_file_name)
115+
}
116+
}
117+
_ => None,
118+
}?;
119+
120+
let mut resulting_path = base_directory;
121+
for module in module_chain_to_containing_module_file(module, db) {
122+
if let Some(name) = module.name(db) {
123+
resulting_path = resulting_path.join(&name.to_string())?;
124+
}
125+
}
126+
127+
Some(resulting_path)
128+
}
129+
130+
fn module_chain_to_containing_module_file(
131+
current_module: Module,
132+
db: &RootDatabase,
133+
) -> Vec<Module> {
134+
let mut path = Vec::new();
135+
136+
let mut current_module = Some(current_module);
137+
while let Some(ModuleSource::Module(_)) =
138+
current_module.map(|module| module.definition_source(db).value)
139+
{
140+
if let Some(module) = current_module {
141+
path.insert(0, module);
142+
current_module = module.parent(db);
143+
} else {
144+
current_module = None;
145+
}
146+
}
147+
148+
path
149+
}
150+
151+
#[cfg(test)]
152+
mod tests {
153+
use crate::completion::{test_utils::completion_list, CompletionKind};
154+
use expect_test::{expect, Expect};
155+
156+
fn check(ra_fixture: &str, expect: Expect) {
157+
let actual = completion_list(ra_fixture, CompletionKind::Magic);
158+
expect.assert_eq(&actual);
159+
}
160+
161+
#[test]
162+
fn lib_module_completion() {
163+
check(
164+
r#"
165+
//- /lib.rs
166+
mod <|>
167+
//- /foo.rs
168+
fn foo() {}
169+
//- /foo/ignored_foo.rs
170+
fn ignored_foo() {}
171+
//- /bar/mod.rs
172+
fn bar() {}
173+
//- /bar/ignored_bar.rs
174+
fn ignored_bar() {}
175+
"#,
176+
expect![[r#"
177+
md bar;
178+
md foo;
179+
"#]],
180+
);
181+
}
182+
183+
#[test]
184+
fn no_module_completion_with_module_body() {
185+
check(
186+
r#"
187+
//- /lib.rs
188+
mod <|> {
189+
190+
}
191+
//- /foo.rs
192+
fn foo() {}
193+
"#,
194+
expect![[r#""#]],
195+
);
196+
}
197+
198+
#[test]
199+
fn main_module_completion() {
200+
check(
201+
r#"
202+
//- /main.rs
203+
mod <|>
204+
//- /foo.rs
205+
fn foo() {}
206+
//- /foo/ignored_foo.rs
207+
fn ignored_foo() {}
208+
//- /bar/mod.rs
209+
fn bar() {}
210+
//- /bar/ignored_bar.rs
211+
fn ignored_bar() {}
212+
"#,
213+
expect![[r#"
214+
md bar;
215+
md foo;
216+
"#]],
217+
);
218+
}
219+
220+
#[test]
221+
fn main_test_module_completion() {
222+
check(
223+
r#"
224+
//- /main.rs
225+
mod tests {
226+
mod <|>;
227+
}
228+
//- /tests/foo.rs
229+
fn foo() {}
230+
"#,
231+
expect![[r#"
232+
md foo
233+
"#]],
234+
);
235+
}
236+
237+
#[test]
238+
fn directly_nested_module_completion() {
239+
check(
240+
r#"
241+
//- /lib.rs
242+
mod foo;
243+
//- /foo.rs
244+
mod <|>;
245+
//- /foo/bar.rs
246+
fn bar() {}
247+
//- /foo/bar/ignored_bar.rs
248+
fn ignored_bar() {}
249+
//- /foo/baz/mod.rs
250+
fn baz() {}
251+
//- /foo/moar/ignored_moar.rs
252+
fn ignored_moar() {}
253+
"#,
254+
expect![[r#"
255+
md bar
256+
md baz
257+
"#]],
258+
);
259+
}
260+
261+
#[test]
262+
fn nested_in_source_module_completion() {
263+
check(
264+
r#"
265+
//- /lib.rs
266+
mod foo;
267+
//- /foo.rs
268+
mod bar {
269+
mod <|>
270+
}
271+
//- /foo/bar/baz.rs
272+
fn baz() {}
273+
"#,
274+
expect![[r#"
275+
md baz;
276+
"#]],
277+
);
278+
}
279+
280+
// FIXME binary modules are not supported in tests properly
281+
// Binary modules are a bit special, they allow importing the modules from `/src/bin`
282+
// and that's why are good to test two things:
283+
// * no cycles are allowed in mod declarations
284+
// * no modules from the parent directory are proposed
285+
// Unfortunately, binary modules support is in cargo not rustc,
286+
// hence the test does not work now
287+
//
288+
// #[test]
289+
// fn regular_bin_module_completion() {
290+
// check(
291+
// r#"
292+
// //- /src/bin.rs
293+
// fn main() {}
294+
// //- /src/bin/foo.rs
295+
// mod <|>
296+
// //- /src/bin/bar.rs
297+
// fn bar() {}
298+
// //- /src/bin/bar/bar_ignored.rs
299+
// fn bar_ignored() {}
300+
// "#,
301+
// expect![[r#"
302+
// md bar;
303+
// "#]],
304+
// );
305+
// }
306+
307+
#[test]
308+
fn already_declared_bin_module_completion_omitted() {
309+
check(
310+
r#"
311+
//- /src/bin.rs
312+
fn main() {}
313+
//- /src/bin/foo.rs
314+
mod <|>
315+
//- /src/bin/bar.rs
316+
mod foo;
317+
fn bar() {}
318+
//- /src/bin/bar/bar_ignored.rs
319+
fn bar_ignored() {}
320+
"#,
321+
expect![[r#""#]],
322+
);
323+
}
324+
}

crates/ide/src/completion/complete_qualified_path.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ pub(super) fn complete_qualified_path(acc: &mut Completions, ctx: &CompletionCon
1313
None => return,
1414
};
1515

16-
if ctx.attribute_under_caret.is_some() {
16+
if ctx.attribute_under_caret.is_some() || ctx.mod_declaration_under_caret.is_some() {
1717
return;
1818
}
1919

crates/ide/src/completion/complete_unqualified_path.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ pub(super) fn complete_unqualified_path(acc: &mut Completions, ctx: &CompletionC
1313
if ctx.record_lit_syntax.is_some()
1414
|| ctx.record_pat_syntax.is_some()
1515
|| ctx.attribute_under_caret.is_some()
16+
|| ctx.mod_declaration_under_caret.is_some()
1617
{
1718
return;
1819
}

0 commit comments

Comments
 (0)