ide_assists/handlers/
move_module_to_file.rs

1use 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
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// ```
27pub(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::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                // We need to special case mod named `r#mod` and place the file in a
68                // subdirectory as "mod.rs" would be of its parent module otherwise.
69                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}