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