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