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, 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 // ```
move_module_to_file(acc: &mut Assists, ctx: &AssistContext) -> Option<()>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                     .collect::<Vec<_>>();
62                 format_to!(buf, "{}", segments.into_iter().rev().format("/"));
63                 format_to!(buf, ".rs");
64                 buf
65             };
66             let contents = {
67                 let items = module_items.dedent(IndentLevel(1)).to_string();
68                 let mut items =
69                     items.trim_start_matches('{').trim_end_matches('}').trim().to_string();
70                 if !items.is_empty() {
71                     items.push('\n');
72                 }
73                 items
74             };
75 
76             let buf = format!("mod {};", module_name);
77 
78             let replacement_start = match module_ast.mod_token() {
79                 Some(mod_token) => mod_token.text_range(),
80                 None => module_ast.syntax().text_range(),
81             }
82             .start();
83 
84             builder.replace(
85                 TextRange::new(replacement_start, module_ast.syntax().text_range().end()),
86                 buf,
87             );
88 
89             let dst = AnchoredPathBuf { anchor: ctx.file_id(), path };
90             builder.create_file(dst, contents);
91         },
92     )
93 }
94 
95 #[cfg(test)]
96 mod tests {
97     use crate::tests::{check_assist, check_assist_not_applicable};
98 
99     use super::*;
100 
101     #[test]
extract_from_root()102     fn extract_from_root() {
103         check_assist(
104             move_module_to_file,
105             r#"
106 mod $0tests {
107     #[test] fn t() {}
108 }
109 "#,
110             r#"
111 //- /main.rs
112 mod tests;
113 //- /tests.rs
114 #[test] fn t() {}
115 "#,
116         );
117     }
118 
119     #[test]
extract_from_submodule()120     fn extract_from_submodule() {
121         check_assist(
122             move_module_to_file,
123             r#"
124 //- /main.rs
125 mod submod;
126 //- /submod.rs
127 $0mod inner {
128     fn f() {}
129 }
130 fn g() {}
131 "#,
132             r#"
133 //- /submod.rs
134 mod inner;
135 fn g() {}
136 //- /submod/inner.rs
137 fn f() {}
138 "#,
139         );
140     }
141 
142     #[test]
extract_from_mod_rs()143     fn extract_from_mod_rs() {
144         check_assist(
145             move_module_to_file,
146             r#"
147 //- /main.rs
148 mod submodule;
149 //- /submodule/mod.rs
150 mod inner$0 {
151     fn f() {}
152 }
153 fn g() {}
154 "#,
155             r#"
156 //- /submodule/mod.rs
157 mod inner;
158 fn g() {}
159 //- /submodule/inner.rs
160 fn f() {}
161 "#,
162         );
163     }
164 
165     #[test]
extract_public()166     fn extract_public() {
167         check_assist(
168             move_module_to_file,
169             r#"
170 pub mod $0tests {
171     #[test] fn t() {}
172 }
173 "#,
174             r#"
175 //- /main.rs
176 pub mod tests;
177 //- /tests.rs
178 #[test] fn t() {}
179 "#,
180         );
181     }
182 
183     #[test]
extract_public_crate()184     fn extract_public_crate() {
185         check_assist(
186             move_module_to_file,
187             r#"
188 pub(crate) mod $0tests {
189     #[test] fn t() {}
190 }
191 "#,
192             r#"
193 //- /main.rs
194 pub(crate) mod tests;
195 //- /tests.rs
196 #[test] fn t() {}
197 "#,
198         );
199     }
200 
201     #[test]
available_before_curly()202     fn available_before_curly() {
203         cov_mark::check!(available_before_curly);
204         check_assist_not_applicable(move_module_to_file, r#"mod m { $0 }"#);
205     }
206 
207     #[test]
keep_outer_comments_and_attributes()208     fn keep_outer_comments_and_attributes() {
209         check_assist(
210             move_module_to_file,
211             r#"
212 /// doc comment
213 #[attribute]
214 mod $0tests {
215     #[test] fn t() {}
216 }
217 "#,
218             r#"
219 //- /main.rs
220 /// doc comment
221 #[attribute]
222 mod tests;
223 //- /tests.rs
224 #[test] fn t() {}
225 "#,
226         );
227     }
228 
229     #[test]
extract_nested()230     fn extract_nested() {
231         check_assist(
232             move_module_to_file,
233             r#"
234 //- /lib.rs
235 mod foo;
236 //- /foo.rs
237 mod bar {
238     mod baz {
239         mod qux$0 {}
240     }
241 }
242 "#,
243             r#"
244 //- /foo.rs
245 mod bar {
246     mod baz {
247         mod qux;
248     }
249 }
250 //- /foo/bar/baz/qux.rs
251 "#,
252         );
253     }
254 }
255