ide_assists/handlers/
generate_enum_projection_method.rs

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