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_.rs320
1 files changed, 320 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..c96f84171
--- /dev/null
+++ b/crates/completion/src/completions/mod_.rs
@@ -0,0 +1,320 @@
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, 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 CompletionItem::new(CompletionKind::Magic, ctx.source_range(), &label)
79 .kind(CompletionItemKind::Module)
80 .add_to(acc)
81 });
82
83 Some(())
84}
85
86fn directory_to_look_for_submodules(
87 module: Module,
88 db: &RootDatabase,
89 module_file_path: &VfsPath,
90) -> Option<VfsPath> {
91 let directory_with_module_path = module_file_path.parent()?;
92 let base_directory = match module_file_path.name_and_extension()? {
93 ("mod", Some("rs")) | ("lib", Some("rs")) | ("main", Some("rs")) => {
94 Some(directory_with_module_path)
95 }
96 (regular_rust_file_name, Some("rs")) => {
97 if matches!(
98 (
99 directory_with_module_path
100 .parent()
101 .as_ref()
102 .and_then(|path| path.name_and_extension()),
103 directory_with_module_path.name_and_extension(),
104 ),
105 (Some(("src", None)), Some(("bin", None)))
106 ) {
107 // files in /src/bin/ can import each other directly
108 Some(directory_with_module_path)
109 } else {
110 directory_with_module_path.join(regular_rust_file_name)
111 }
112 }
113 _ => None,
114 }?;
115
116 let mut resulting_path = base_directory;
117 for module in module_chain_to_containing_module_file(module, db) {
118 if let Some(name) = module.name(db) {
119 resulting_path = resulting_path.join(&name.to_string())?;
120 }
121 }
122
123 Some(resulting_path)
124}
125
126fn module_chain_to_containing_module_file(
127 current_module: Module,
128 db: &RootDatabase,
129) -> Vec<Module> {
130 let mut path = Vec::new();
131
132 let mut current_module = Some(current_module);
133 while let Some(ModuleSource::Module(_)) =
134 current_module.map(|module| module.definition_source(db).value)
135 {
136 if let Some(module) = current_module {
137 path.insert(0, module);
138 current_module = module.parent(db);
139 } else {
140 current_module = None;
141 }
142 }
143
144 path
145}
146
147#[cfg(test)]
148mod tests {
149 use crate::{test_utils::completion_list, CompletionKind};
150 use expect_test::{expect, Expect};
151
152 fn check(ra_fixture: &str, expect: Expect) {
153 let actual = completion_list(ra_fixture, CompletionKind::Magic);
154 expect.assert_eq(&actual);
155 }
156
157 #[test]
158 fn lib_module_completion() {
159 check(
160 r#"
161 //- /lib.rs
162 mod <|>
163 //- /foo.rs
164 fn foo() {}
165 //- /foo/ignored_foo.rs
166 fn ignored_foo() {}
167 //- /bar/mod.rs
168 fn bar() {}
169 //- /bar/ignored_bar.rs
170 fn ignored_bar() {}
171 "#,
172 expect![[r#"
173 md bar;
174 md foo;
175 "#]],
176 );
177 }
178
179 #[test]
180 fn no_module_completion_with_module_body() {
181 check(
182 r#"
183 //- /lib.rs
184 mod <|> {
185
186 }
187 //- /foo.rs
188 fn foo() {}
189 "#,
190 expect![[r#""#]],
191 );
192 }
193
194 #[test]
195 fn main_module_completion() {
196 check(
197 r#"
198 //- /main.rs
199 mod <|>
200 //- /foo.rs
201 fn foo() {}
202 //- /foo/ignored_foo.rs
203 fn ignored_foo() {}
204 //- /bar/mod.rs
205 fn bar() {}
206 //- /bar/ignored_bar.rs
207 fn ignored_bar() {}
208 "#,
209 expect![[r#"
210 md bar;
211 md foo;
212 "#]],
213 );
214 }
215
216 #[test]
217 fn main_test_module_completion() {
218 check(
219 r#"
220 //- /main.rs
221 mod tests {
222 mod <|>;
223 }
224 //- /tests/foo.rs
225 fn foo() {}
226 "#,
227 expect![[r#"
228 md foo
229 "#]],
230 );
231 }
232
233 #[test]
234 fn directly_nested_module_completion() {
235 check(
236 r#"
237 //- /lib.rs
238 mod foo;
239 //- /foo.rs
240 mod <|>;
241 //- /foo/bar.rs
242 fn bar() {}
243 //- /foo/bar/ignored_bar.rs
244 fn ignored_bar() {}
245 //- /foo/baz/mod.rs
246 fn baz() {}
247 //- /foo/moar/ignored_moar.rs
248 fn ignored_moar() {}
249 "#,
250 expect![[r#"
251 md bar
252 md baz
253 "#]],
254 );
255 }
256
257 #[test]
258 fn nested_in_source_module_completion() {
259 check(
260 r#"
261 //- /lib.rs
262 mod foo;
263 //- /foo.rs
264 mod bar {
265 mod <|>
266 }
267 //- /foo/bar/baz.rs
268 fn baz() {}
269 "#,
270 expect![[r#"
271 md baz;
272 "#]],
273 );
274 }
275
276 // FIXME binary modules are not supported in tests properly
277 // Binary modules are a bit special, they allow importing the modules from `/src/bin`
278 // and that's why are good to test two things:
279 // * no cycles are allowed in mod declarations
280 // * no modules from the parent directory are proposed
281 // Unfortunately, binary modules support is in cargo not rustc,
282 // hence the test does not work now
283 //
284 // #[test]
285 // fn regular_bin_module_completion() {
286 // check(
287 // r#"
288 // //- /src/bin.rs
289 // fn main() {}
290 // //- /src/bin/foo.rs
291 // mod <|>
292 // //- /src/bin/bar.rs
293 // fn bar() {}
294 // //- /src/bin/bar/bar_ignored.rs
295 // fn bar_ignored() {}
296 // "#,
297 // expect![[r#"
298 // md bar;
299 // "#]],foo
300 // );
301 // }
302
303 #[test]
304 fn already_declared_bin_module_completion_omitted() {
305 check(
306 r#"
307 //- /src/bin.rs crate:main
308 fn main() {}
309 //- /src/bin/foo.rs
310 mod <|>
311 //- /src/bin/bar.rs
312 mod foo;
313 fn bar() {}
314 //- /src/bin/bar/bar_ignored.rs
315 fn bar_ignored() {}
316 "#,
317 expect![[r#""#]],
318 );
319 }
320}