1use ide_db::assists::GroupLabel;
2use stdx::to_lower_snake_case;
3use syntax::{
4 AstNode, Edition,
5 ast::{self, HasName, HasVisibility, edit::AstNodeEdit},
6 syntax_editor::Position,
7};
8
9use crate::{
10 AssistContext, AssistId, Assists,
11 utils::{find_struct_impl, generate_impl_with_item, is_selected},
12};
13
14pub(crate) fn generate_enum_try_into_method(
42 acc: &mut Assists,
43 ctx: &AssistContext<'_, '_>,
44) -> Option<()> {
45 generate_enum_projection_method(
46 acc,
47 ctx,
48 "generate_enum_try_into_method",
49 "Generate a `try_into_` method for this enum variant",
50 ProjectionProps {
51 fn_name_prefix: "try_into",
52 self_param: "self",
53 return_prefix: "Result<",
54 return_suffix: ", Self>",
55 happy_case: "Ok",
56 sad_case: "Err(self)",
57 },
58 )
59}
60
61pub(crate) fn generate_enum_as_method(
89 acc: &mut Assists,
90 ctx: &AssistContext<'_, '_>,
91) -> Option<()> {
92 generate_enum_projection_method(
93 acc,
94 ctx,
95 "generate_enum_as_method",
96 "Generate an `as_` method for this enum variant",
97 ProjectionProps {
98 fn_name_prefix: "as",
99 self_param: "&self",
100 return_prefix: "Option<&",
101 return_suffix: ">",
102 happy_case: "Some",
103 sad_case: "None",
104 },
105 )
106}
107
108struct ProjectionProps {
109 fn_name_prefix: &'static str,
110 self_param: &'static str,
111 return_prefix: &'static str,
112 return_suffix: &'static str,
113 happy_case: &'static str,
114 sad_case: &'static str,
115}
116
117fn generate_enum_projection_method(
118 acc: &mut Assists,
119 ctx: &AssistContext<'_, '_>,
120 assist_id: &'static str,
121 assist_description: &str,
122 props: ProjectionProps,
123) -> Option<()> {
124 let variant = ctx.find_node_at_offset::<ast::Variant>()?;
125 let parent_enum = ast::Adt::Enum(variant.parent_enum());
126 let variants = variant
127 .parent_enum()
128 .variant_list()?
129 .variants()
130 .filter(|it| is_selected(it, ctx.selection_trimmed(), true))
131 .collect::<Vec<_>>();
132 let methods = variants
133 .iter()
134 .map(|variant| Method::new(variant, props.fn_name_prefix))
135 .collect::<Option<Vec<_>>>()?;
136 let fn_names = methods.iter().map(|it| it.fn_name.clone()).collect::<Vec<_>>();
137 stdx::never!(variants.is_empty());
138
139 let impl_def = find_struct_impl(ctx, &parent_enum, &fn_names)?;
141
142 let target = variant.syntax().text_range();
143 acc.add_group(
144 &GroupLabel("Generate an `is_`,`as_`, or `try_into_` for this enum variant".to_owned()),
145 AssistId::generate(assist_id),
146 assist_description,
147 target,
148 |builder| {
149 let vis = parent_enum.visibility().map_or(String::new(), |v| format!("{v} "));
150 let must_use = if ctx.config.assist_emit_must_use { "#[must_use]\n" } else { "" };
151
152 let fn_items: Vec<ast::AssocItem> = methods
153 .iter()
154 .map(|method| build_fn_item(method, &vis, must_use, &props))
155 .collect();
156
157 if let Some(impl_def) = &impl_def {
158 let editor = builder.make_editor(impl_def.syntax());
159 impl_def.assoc_item_list().unwrap().add_items(&editor, fn_items);
160 builder.add_file_edits(ctx.vfs_file_id(), editor);
161 return;
162 }
163
164 let editor = builder.make_editor(parent_enum.syntax());
165 let make = editor.make();
166 let indent = parent_enum.indent_level();
167 let assoc_list = make.assoc_item_list(fn_items);
168 let new_impl = generate_impl_with_item(make, &parent_enum, Some(assoc_list));
169 editor.insert_all(
170 Position::after(parent_enum.syntax()),
171 vec![
172 make.whitespace(&format!("\n\n{indent}")).into(),
173 new_impl.syntax().clone().into(),
174 ],
175 );
176 builder.add_file_edits(ctx.vfs_file_id(), editor);
177 },
178 )
179}
180
181fn build_fn_item(
182 method: &Method,
183 vis: &str,
184 must_use: &str,
185 props: &ProjectionProps,
186) -> ast::AssocItem {
187 let Method { pattern_suffix, field_type, bound_name, fn_name, variant_name } = method;
188 let ProjectionProps { self_param, return_prefix, return_suffix, happy_case, sad_case, .. } =
189 props;
190 let fn_text = format!(
191 "{must_use}{vis}fn {fn_name}({self_param}) -> {return_prefix}{field_type}{return_suffix} {{
192 if let Self::{variant_name}{pattern_suffix} = self {{
193 {happy_case}({bound_name})
194 }} else {{
195 {sad_case}
196 }}
197}}"
198 );
199 let wrapped = format!("impl X {{ {fn_text} }}");
200 let parse = syntax::SourceFile::parse(&wrapped, Edition::CURRENT);
201 let fn_ = parse
202 .tree()
203 .syntax()
204 .descendants()
205 .find_map(ast::Fn::cast)
206 .expect("fn text must produce a valid fn node");
207 ast::AssocItem::Fn(fn_.indent(1.into()))
208}
209
210struct Method {
211 pattern_suffix: String,
212 field_type: ast::Type,
213 bound_name: String,
214 fn_name: String,
215 variant_name: ast::Name,
216}
217
218impl Method {
219 fn new(variant: &ast::Variant, fn_name_prefix: &str) -> Option<Self> {
220 use itertools::Itertools as _;
221 let variant_name = variant.name()?;
222 let fn_name = format!("{fn_name_prefix}_{}", &to_lower_snake_case(&variant_name.text()));
223
224 match variant.kind() {
225 ast::StructKind::Record(record) => {
226 let (field,) = record.fields().collect_tuple()?;
227 let name = field.name()?.to_string();
228 let field_type = field.ty()?;
229 let pattern_suffix = format!(" {{ {name} }}");
230 Some(Method { pattern_suffix, field_type, bound_name: name, fn_name, variant_name })
231 }
232 ast::StructKind::Tuple(tuple) => {
233 let (field,) = tuple.fields().collect_tuple()?;
234 let field_type = field.ty()?;
235 Some(Method {
236 pattern_suffix: "(v)".to_owned(),
237 field_type,
238 bound_name: "v".to_owned(),
239 variant_name,
240 fn_name,
241 })
242 }
243 ast::StructKind::Unit => None,
244 }
245 }
246}
247
248#[cfg(test)]
249mod tests {
250 use crate::tests::{check_assist, check_assist_not_applicable};
251
252 use super::*;
253
254 #[test]
255 fn test_generate_enum_try_into_tuple_variant() {
256 check_assist(
257 generate_enum_try_into_method,
258 r#"
259enum Value {
260 Number(i32),
261 Text(String)$0,
262}"#,
263 r#"enum Value {
264 Number(i32),
265 Text(String),
266}
267
268impl Value {
269 fn try_into_text(self) -> Result<String, Self> {
270 if let Self::Text(v) = self {
271 Ok(v)
272 } else {
273 Err(self)
274 }
275 }
276}"#,
277 );
278 }
279
280 #[test]
281 fn test_generate_enum_multiple_try_into_tuple_variant() {
282 check_assist(
283 generate_enum_try_into_method,
284 r#"
285enum Value {
286 Unit(()),
287 $0Number(i32),
288 Text(String)$0,
289}"#,
290 r#"enum Value {
291 Unit(()),
292 Number(i32),
293 Text(String),
294}
295
296impl Value {
297 fn try_into_number(self) -> Result<i32, Self> {
298 if let Self::Number(v) = self {
299 Ok(v)
300 } else {
301 Err(self)
302 }
303 }
304
305 fn try_into_text(self) -> Result<String, Self> {
306 if let Self::Text(v) = self {
307 Ok(v)
308 } else {
309 Err(self)
310 }
311 }
312}"#,
313 );
314 }
315
316 #[test]
317 fn test_generate_enum_try_into_already_implemented() {
318 check_assist_not_applicable(
319 generate_enum_try_into_method,
320 r#"enum Value {
321 Number(i32),
322 Text(String)$0,
323}
324
325impl Value {
326 fn try_into_text(self) -> Result<String, Self> {
327 if let Self::Text(v) = self {
328 Ok(v)
329 } else {
330 Err(self)
331 }
332 }
333}"#,
334 );
335 }
336
337 #[test]
338 fn test_generate_enum_try_into_unit_variant() {
339 check_assist_not_applicable(
340 generate_enum_try_into_method,
341 r#"enum Value {
342 Number(i32),
343 Text(String),
344 Unit$0,
345}"#,
346 );
347 }
348
349 #[test]
350 fn test_generate_enum_try_into_record_with_multiple_fields() {
351 check_assist_not_applicable(
352 generate_enum_try_into_method,
353 r#"enum Value {
354 Number(i32),
355 Text(String),
356 Both { first: i32, second: String }$0,
357}"#,
358 );
359 }
360
361 #[test]
362 fn test_generate_enum_try_into_tuple_with_multiple_fields() {
363 check_assist_not_applicable(
364 generate_enum_try_into_method,
365 r#"enum Value {
366 Number(i32),
367 Text(String, String)$0,
368}"#,
369 );
370 }
371
372 #[test]
373 fn test_generate_enum_try_into_record_variant() {
374 check_assist(
375 generate_enum_try_into_method,
376 r#"enum Value {
377 Number(i32),
378 Text { text: String }$0,
379}"#,
380 r#"enum Value {
381 Number(i32),
382 Text { text: String },
383}
384
385impl Value {
386 fn try_into_text(self) -> Result<String, Self> {
387 if let Self::Text { text } = self {
388 Ok(text)
389 } else {
390 Err(self)
391 }
392 }
393}"#,
394 );
395 }
396
397 #[test]
398 fn test_generate_enum_as_tuple_variant() {
399 check_assist(
400 generate_enum_as_method,
401 r#"
402enum Value {
403 Number(i32),
404 Text(String)$0,
405}"#,
406 r#"enum Value {
407 Number(i32),
408 Text(String),
409}
410
411impl Value {
412 fn as_text(&self) -> Option<&String> {
413 if let Self::Text(v) = self {
414 Some(v)
415 } else {
416 None
417 }
418 }
419}"#,
420 );
421 }
422
423 #[test]
424 fn test_generate_enum_as_multiple_tuple_variant() {
425 check_assist(
426 generate_enum_as_method,
427 r#"
428enum Value {
429 Unit(()),
430 $0Number(i32),
431 Text(String)$0,
432}"#,
433 r#"enum Value {
434 Unit(()),
435 Number(i32),
436 Text(String),
437}
438
439impl Value {
440 fn as_number(&self) -> Option<&i32> {
441 if let Self::Number(v) = self {
442 Some(v)
443 } else {
444 None
445 }
446 }
447
448 fn as_text(&self) -> Option<&String> {
449 if let Self::Text(v) = self {
450 Some(v)
451 } else {
452 None
453 }
454 }
455}"#,
456 );
457 }
458
459 #[test]
460 fn test_generate_enum_as_record_variant() {
461 check_assist(
462 generate_enum_as_method,
463 r#"enum Value {
464 Number(i32),
465 Text { text: String }$0,
466}"#,
467 r#"enum Value {
468 Number(i32),
469 Text { text: String },
470}
471
472impl Value {
473 fn as_text(&self) -> Option<&String> {
474 if let Self::Text { text } = self {
475 Some(text)
476 } else {
477 None
478 }
479 }
480}"#,
481 );
482 }
483}