Skip to main content

ide_assists/handlers/
generate_enum_projection_method.rs

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
14// Assist: generate_enum_try_into_method
15//
16// Generate a `try_into_` method for this enum variant.
17//
18// ```
19// enum Value {
20//  Number(i32),
21//  Text(String)$0,
22// }
23// ```
24// ->
25// ```
26// enum Value {
27//  Number(i32),
28//  Text(String),
29// }
30//
31// impl Value {
32//     fn try_into_text(self) -> Result<String, Self> {
33//         if let Self::Text(v) = self {
34//             Ok(v)
35//         } else {
36//             Err(self)
37//         }
38//     }
39// }
40// ```
41pub(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
61// Assist: generate_enum_as_method
62//
63// Generate an `as_` method for this enum variant.
64//
65// ```
66// enum Value {
67//  Number(i32),
68//  Text(String)$0,
69// }
70// ```
71// ->
72// ```
73// enum Value {
74//  Number(i32),
75//  Text(String),
76// }
77//
78// impl Value {
79//     fn as_text(&self) -> Option<&String> {
80//         if let Self::Text(v) = self {
81//             Some(v)
82//         } else {
83//             None
84//         }
85//     }
86// }
87// ```
88pub(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    // Return early if we've found an existing new fn
140    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}