ide_assists/handlers/
move_module_to_file.rs1use std::iter;
2
3use ast::edit::IndentLevel;
4use ide_db::base_db::AnchoredPathBuf;
5use itertools::Itertools;
6use stdx::format_to;
7use syntax::{
8 AstNode, SmolStr, TextRange,
9 ast::{self, HasName, edit::AstNodeEdit},
10};
11
12use crate::{AssistContext, AssistId, Assists};
13
14pub(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 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::refactor_extract("move_module_to_file"),
48 "Extract module to file",
49 target,
50 |builder| {
51 let path = {
52 let mut buf = String::from("./");
53 let db = ctx.db();
54 match parent_module.name(db) {
55 Some(name) if !parent_module.is_mod_rs(db) && !parent_module.has_path(db) => {
56 format_to!(buf, "{}/", name.as_str())
57 }
58 _ => (),
59 }
60 let segments = iter::successors(Some(module_ast.clone()), |module| module.parent())
61 .filter_map(|it| it.name())
62 .map(|name| SmolStr::from(name.text().trim_start_matches("r#")))
63 .collect::<Vec<_>>();
64
65 format_to!(buf, "{}", segments.into_iter().rev().format("/"));
66
67 if module_name.text() == "r#mod" {
70 format_to!(buf, "/mod.rs");
71 } else {
72 format_to!(buf, ".rs");
73 }
74 buf
75 };
76 let contents = {
77 let items = module_items.dedent(IndentLevel(1)).to_string();
78 let mut items =
79 items.trim_start_matches('{').trim_end_matches('}').trim().to_owned();
80 if !items.is_empty() {
81 items.push('\n');
82 }
83 items
84 };
85
86 let buf = format!("mod {module_name};");
87
88 let replacement_start = match module_ast.mod_token() {
89 Some(mod_token) => mod_token.text_range(),
90 None => module_ast.syntax().text_range(),
91 }
92 .start();
93
94 builder.replace(
95 TextRange::new(replacement_start, module_ast.syntax().text_range().end()),
96 buf,
97 );
98
99 let dst = AnchoredPathBuf { anchor: ctx.vfs_file_id(), path };
100 builder.create_file(dst, contents);
101 },
102 )
103}
104
105#[cfg(test)]
106mod tests {
107 use crate::tests::{check_assist, check_assist_not_applicable};
108
109 use super::*;
110
111 #[test]
112 fn extract_with_specified_path_attr() {
113 check_assist(
114 move_module_to_file,
115 r#"
116//- /main.rs
117#[path="parser/__mod.rs"]
118mod parser;
119//- /parser/__mod.rs
120fn test() {}
121mod $0expr {
122 struct A {}
123}
124"#,
125 r#"
126//- /parser/__mod.rs
127fn test() {}
128mod expr;
129//- /parser/expr.rs
130struct A {}
131"#,
132 );
133
134 check_assist(
135 move_module_to_file,
136 r#"
137//- /main.rs
138#[path="parser/a/__mod.rs"]
139mod parser;
140//- /parser/a/__mod.rs
141fn test() {}
142mod $0expr {
143 struct A {}
144}
145"#,
146 r#"
147//- /parser/a/__mod.rs
148fn test() {}
149mod expr;
150//- /parser/a/expr.rs
151struct A {}
152"#,
153 );
154
155 check_assist(
156 move_module_to_file,
157 r#"
158//- /main.rs
159#[path="a.rs"]
160mod parser;
161//- /a.rs
162fn test() {}
163mod $0expr {
164 struct A {}
165}
166"#,
167 r#"
168//- /a.rs
169fn test() {}
170mod expr;
171//- /expr.rs
172struct A {}
173"#,
174 );
175 }
176
177 #[test]
178 fn extract_from_root() {
179 check_assist(
180 move_module_to_file,
181 r#"
182mod $0tests {
183 #[test] fn t() {}
184}
185"#,
186 r#"
187//- /main.rs
188mod tests;
189//- /tests.rs
190#[test] fn t() {}
191"#,
192 );
193 }
194
195 #[test]
196 fn extract_from_submodule() {
197 check_assist(
198 move_module_to_file,
199 r#"
200//- /main.rs
201mod submod;
202//- /submod.rs
203$0mod inner {
204 fn f() {}
205}
206fn g() {}
207"#,
208 r#"
209//- /submod.rs
210mod inner;
211fn g() {}
212//- /submod/inner.rs
213fn f() {}
214"#,
215 );
216 }
217
218 #[test]
219 fn extract_from_mod_rs() {
220 check_assist(
221 move_module_to_file,
222 r#"
223//- /main.rs
224mod submodule;
225//- /submodule/mod.rs
226mod inner$0 {
227 fn f() {}
228}
229fn g() {}
230"#,
231 r#"
232//- /submodule/mod.rs
233mod inner;
234fn g() {}
235//- /submodule/inner.rs
236fn f() {}
237"#,
238 );
239 }
240
241 #[test]
242 fn extract_public() {
243 check_assist(
244 move_module_to_file,
245 r#"
246pub mod $0tests {
247 #[test] fn t() {}
248}
249"#,
250 r#"
251//- /main.rs
252pub mod tests;
253//- /tests.rs
254#[test] fn t() {}
255"#,
256 );
257 }
258
259 #[test]
260 fn extract_public_crate() {
261 check_assist(
262 move_module_to_file,
263 r#"
264pub(crate) mod $0tests {
265 #[test] fn t() {}
266}
267"#,
268 r#"
269//- /main.rs
270pub(crate) mod tests;
271//- /tests.rs
272#[test] fn t() {}
273"#,
274 );
275 }
276
277 #[test]
278 fn available_before_curly() {
279 cov_mark::check!(available_before_curly);
280 check_assist_not_applicable(move_module_to_file, r#"mod m { $0 }"#);
281 }
282
283 #[test]
284 fn keep_outer_comments_and_attributes() {
285 check_assist(
286 move_module_to_file,
287 r#"
288/// doc comment
289#[attribute]
290mod $0tests {
291 #[test] fn t() {}
292}
293"#,
294 r#"
295//- /main.rs
296/// doc comment
297#[attribute]
298mod tests;
299//- /tests.rs
300#[test] fn t() {}
301"#,
302 );
303 }
304
305 #[test]
306 fn extract_nested() {
307 check_assist(
308 move_module_to_file,
309 r#"
310//- /lib.rs
311mod foo;
312//- /foo.rs
313mod bar {
314 mod baz {
315 mod qux$0 {}
316 }
317}
318"#,
319 r#"
320//- /foo.rs
321mod bar {
322 mod baz {
323 mod qux;
324 }
325}
326//- /foo/bar/baz/qux.rs
327"#,
328 );
329 }
330
331 #[test]
332 fn extract_mod_with_raw_ident() {
333 check_assist(
334 move_module_to_file,
335 r#"
336//- /main.rs
337mod $0r#static {}
338"#,
339 r#"
340//- /main.rs
341mod r#static;
342//- /static.rs
343"#,
344 )
345 }
346
347 #[test]
348 fn extract_r_mod() {
349 check_assist(
350 move_module_to_file,
351 r#"
352//- /main.rs
353mod $0r#mod {}
354"#,
355 r#"
356//- /main.rs
357mod r#mod;
358//- /mod/mod.rs
359"#,
360 )
361 }
362
363 #[test]
364 fn extract_r_mod_from_mod_rs() {
365 check_assist(
366 move_module_to_file,
367 r#"
368//- /main.rs
369mod foo;
370//- /foo/mod.rs
371mod $0r#mod {}
372"#,
373 r#"
374//- /foo/mod.rs
375mod r#mod;
376//- /foo/mod/mod.rs
377"#,
378 )
379 }
380
381 #[test]
382 fn extract_nested_r_mod() {
383 check_assist(
384 move_module_to_file,
385 r#"
386//- /main.rs
387mod r#mod {
388 mod foo {
389 mod $0r#mod {}
390 }
391}
392"#,
393 r#"
394//- /main.rs
395mod r#mod {
396 mod foo {
397 mod r#mod;
398 }
399}
400//- /mod/foo/mod/mod.rs
401"#,
402 )
403 }
404}