]> git.lizzy.rs Git - rust.git/blob - src/tools/rust-analyzer/crates/ide-assists/src/handlers/move_module_to_file.rs
Rollup merge of #101420 - kraktus:doc_hir_local, r=cjgillot
[rust.git] / src / tools / rust-analyzer / crates / ide-assists / src / handlers / move_module_to_file.rs
1 use std::iter;
2
3 use ast::edit::IndentLevel;
4 use ide_db::base_db::AnchoredPathBuf;
5 use itertools::Itertools;
6 use stdx::format_to;
7 use syntax::{
8     ast::{self, edit::AstNodeEdit, HasName},
9     AstNode, SmolStr, TextRange,
10 };
11
12 use crate::{AssistContext, AssistId, AssistKind, Assists};
13
14 // Assist: move_module_to_file
15 //
16 // Moves inline module's contents to a separate file.
17 //
18 // ```
19 // mod $0foo {
20 //     fn t() {}
21 // }
22 // ```
23 // ->
24 // ```
25 // mod foo;
26 // ```
27 pub(crate) fn move_module_to_file(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
28     let module_ast = ctx.find_node_at_offset::<ast::Module>()?;
29     let module_items = module_ast.item_list()?;
30
31     let l_curly_offset = module_items.syntax().text_range().start();
32     if l_curly_offset <= ctx.offset() {
33         cov_mark::hit!(available_before_curly);
34         return None;
35     }
36     let target = TextRange::new(module_ast.syntax().text_range().start(), l_curly_offset);
37
38     let module_name = module_ast.name()?;
39
40     // get to the outermost module syntax so we can grab the module of file we are in
41     let outermost_mod_decl =
42         iter::successors(Some(module_ast.clone()), |module| module.parent()).last()?;
43     let module_def = ctx.sema.to_def(&outermost_mod_decl)?;
44     let parent_module = module_def.parent(ctx.db())?;
45
46     acc.add(
47         AssistId("move_module_to_file", AssistKind::RefactorExtract),
48         "Extract module to file",
49         target,
50         |builder| {
51             let path = {
52                 let mut buf = String::from("./");
53                 match parent_module.name(ctx.db()) {
54                     Some(name) if !parent_module.is_mod_rs(ctx.db()) => {
55                         format_to!(buf, "{}/", name)
56                     }
57                     _ => (),
58                 }
59                 let segments = iter::successors(Some(module_ast.clone()), |module| module.parent())
60                     .filter_map(|it| it.name())
61                     .map(|name| SmolStr::from(name.text().trim_start_matches("r#")))
62                     .collect::<Vec<_>>();
63
64                 format_to!(buf, "{}", segments.into_iter().rev().format("/"));
65
66                 // We need to special case mod named `r#mod` and place the file in a
67                 // subdirectory as "mod.rs" would be of its parent module otherwise.
68                 if module_name.text() == "r#mod" {
69                     format_to!(buf, "/mod.rs");
70                 } else {
71                     format_to!(buf, ".rs");
72                 }
73                 buf
74             };
75             let contents = {
76                 let items = module_items.dedent(IndentLevel(1)).to_string();
77                 let mut items =
78                     items.trim_start_matches('{').trim_end_matches('}').trim().to_string();
79                 if !items.is_empty() {
80                     items.push('\n');
81                 }
82                 items
83             };
84
85             let buf = format!("mod {};", module_name);
86
87             let replacement_start = match module_ast.mod_token() {
88                 Some(mod_token) => mod_token.text_range(),
89                 None => module_ast.syntax().text_range(),
90             }
91             .start();
92
93             builder.replace(
94                 TextRange::new(replacement_start, module_ast.syntax().text_range().end()),
95                 buf,
96             );
97
98             let dst = AnchoredPathBuf { anchor: ctx.file_id(), path };
99             builder.create_file(dst, contents);
100         },
101     )
102 }
103
104 #[cfg(test)]
105 mod tests {
106     use crate::tests::{check_assist, check_assist_not_applicable};
107
108     use super::*;
109
110     #[test]
111     fn extract_from_root() {
112         check_assist(
113             move_module_to_file,
114             r#"
115 mod $0tests {
116     #[test] fn t() {}
117 }
118 "#,
119             r#"
120 //- /main.rs
121 mod tests;
122 //- /tests.rs
123 #[test] fn t() {}
124 "#,
125         );
126     }
127
128     #[test]
129     fn extract_from_submodule() {
130         check_assist(
131             move_module_to_file,
132             r#"
133 //- /main.rs
134 mod submod;
135 //- /submod.rs
136 $0mod inner {
137     fn f() {}
138 }
139 fn g() {}
140 "#,
141             r#"
142 //- /submod.rs
143 mod inner;
144 fn g() {}
145 //- /submod/inner.rs
146 fn f() {}
147 "#,
148         );
149     }
150
151     #[test]
152     fn extract_from_mod_rs() {
153         check_assist(
154             move_module_to_file,
155             r#"
156 //- /main.rs
157 mod submodule;
158 //- /submodule/mod.rs
159 mod inner$0 {
160     fn f() {}
161 }
162 fn g() {}
163 "#,
164             r#"
165 //- /submodule/mod.rs
166 mod inner;
167 fn g() {}
168 //- /submodule/inner.rs
169 fn f() {}
170 "#,
171         );
172     }
173
174     #[test]
175     fn extract_public() {
176         check_assist(
177             move_module_to_file,
178             r#"
179 pub mod $0tests {
180     #[test] fn t() {}
181 }
182 "#,
183             r#"
184 //- /main.rs
185 pub mod tests;
186 //- /tests.rs
187 #[test] fn t() {}
188 "#,
189         );
190     }
191
192     #[test]
193     fn extract_public_crate() {
194         check_assist(
195             move_module_to_file,
196             r#"
197 pub(crate) mod $0tests {
198     #[test] fn t() {}
199 }
200 "#,
201             r#"
202 //- /main.rs
203 pub(crate) mod tests;
204 //- /tests.rs
205 #[test] fn t() {}
206 "#,
207         );
208     }
209
210     #[test]
211     fn available_before_curly() {
212         cov_mark::check!(available_before_curly);
213         check_assist_not_applicable(move_module_to_file, r#"mod m { $0 }"#);
214     }
215
216     #[test]
217     fn keep_outer_comments_and_attributes() {
218         check_assist(
219             move_module_to_file,
220             r#"
221 /// doc comment
222 #[attribute]
223 mod $0tests {
224     #[test] fn t() {}
225 }
226 "#,
227             r#"
228 //- /main.rs
229 /// doc comment
230 #[attribute]
231 mod tests;
232 //- /tests.rs
233 #[test] fn t() {}
234 "#,
235         );
236     }
237
238     #[test]
239     fn extract_nested() {
240         check_assist(
241             move_module_to_file,
242             r#"
243 //- /lib.rs
244 mod foo;
245 //- /foo.rs
246 mod bar {
247     mod baz {
248         mod qux$0 {}
249     }
250 }
251 "#,
252             r#"
253 //- /foo.rs
254 mod bar {
255     mod baz {
256         mod qux;
257     }
258 }
259 //- /foo/bar/baz/qux.rs
260 "#,
261         );
262     }
263
264     #[test]
265     fn extract_mod_with_raw_ident() {
266         check_assist(
267             move_module_to_file,
268             r#"
269 //- /main.rs
270 mod $0r#static {}
271 "#,
272             r#"
273 //- /main.rs
274 mod r#static;
275 //- /static.rs
276 "#,
277         )
278     }
279
280     #[test]
281     fn extract_r_mod() {
282         check_assist(
283             move_module_to_file,
284             r#"
285 //- /main.rs
286 mod $0r#mod {}
287 "#,
288             r#"
289 //- /main.rs
290 mod r#mod;
291 //- /mod/mod.rs
292 "#,
293         )
294     }
295
296     #[test]
297     fn extract_r_mod_from_mod_rs() {
298         check_assist(
299             move_module_to_file,
300             r#"
301 //- /main.rs
302 mod foo;
303 //- /foo/mod.rs
304 mod $0r#mod {}
305 "#,
306             r#"
307 //- /foo/mod.rs
308 mod r#mod;
309 //- /foo/mod/mod.rs
310 "#,
311         )
312     }
313
314     #[test]
315     fn extract_nested_r_mod() {
316         check_assist(
317             move_module_to_file,
318             r#"
319 //- /main.rs
320 mod r#mod {
321     mod foo {
322         mod $0r#mod {}
323     }
324 }
325 "#,
326             r#"
327 //- /main.rs
328 mod r#mod {
329     mod foo {
330         mod r#mod;
331     }
332 }
333 //- /mod/foo/mod/mod.rs
334 "#,
335         )
336     }
337 }