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
12pub(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
59pub(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 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}