aboutsummaryrefslogtreecommitdiff
path: root/crates/ide_assists/src
diff options
context:
space:
mode:
Diffstat (limited to 'crates/ide_assists/src')
-rw-r--r--crates/ide_assists/src/handlers/add_turbo_fish.rs108
-rw-r--r--crates/ide_assists/src/handlers/apply_demorgan.rs45
-rw-r--r--crates/ide_assists/src/handlers/convert_comment_block.rs419
-rw-r--r--crates/ide_assists/src/handlers/extract_variable.rs208
-rw-r--r--crates/ide_assists/src/lib.rs2
-rw-r--r--crates/ide_assists/src/utils.rs2
-rw-r--r--crates/ide_assists/src/utils/suggest_name.rs729
7 files changed, 1504 insertions, 9 deletions
diff --git a/crates/ide_assists/src/handlers/add_turbo_fish.rs b/crates/ide_assists/src/handlers/add_turbo_fish.rs
index 8e9ea4fad..a08b55ebb 100644
--- a/crates/ide_assists/src/handlers/add_turbo_fish.rs
+++ b/crates/ide_assists/src/handlers/add_turbo_fish.rs
@@ -31,6 +31,7 @@ pub(crate) fn add_turbo_fish(acc: &mut Assists, ctx: &AssistContext) -> Option<(
31 return None; 31 return None;
32 } 32 }
33 mark::hit!(add_turbo_fish_after_call); 33 mark::hit!(add_turbo_fish_after_call);
34 mark::hit!(add_type_ascription_after_call);
34 arg_list.l_paren_token()?.prev_token().filter(|it| it.kind() == SyntaxKind::IDENT) 35 arg_list.l_paren_token()?.prev_token().filter(|it| it.kind() == SyntaxKind::IDENT)
35 })?; 36 })?;
36 let next_token = ident.next_token()?; 37 let next_token = ident.next_token()?;
@@ -52,6 +53,24 @@ pub(crate) fn add_turbo_fish(acc: &mut Assists, ctx: &AssistContext) -> Option<(
52 mark::hit!(add_turbo_fish_non_generic); 53 mark::hit!(add_turbo_fish_non_generic);
53 return None; 54 return None;
54 } 55 }
56
57 if let Some(let_stmt) = ctx.find_node_at_offset::<ast::LetStmt>() {
58 if let_stmt.colon_token().is_none() {
59 let type_pos = let_stmt.pat()?.syntax().last_token()?.text_range().end();
60 acc.add(
61 AssistId("add_type_ascription", AssistKind::RefactorRewrite),
62 "Add `: _` before assignment operator",
63 ident.text_range(),
64 |builder| match ctx.config.snippet_cap {
65 Some(cap) => builder.insert_snippet(cap, type_pos, ": ${0:_}"),
66 None => builder.insert(type_pos, ": _"),
67 },
68 )?
69 } else {
70 mark::hit!(add_type_ascription_already_typed);
71 }
72 }
73
55 acc.add( 74 acc.add(
56 AssistId("add_turbo_fish", AssistKind::RefactorRewrite), 75 AssistId("add_turbo_fish", AssistKind::RefactorRewrite),
57 "Add `::<>`", 76 "Add `::<>`",
@@ -65,7 +84,7 @@ pub(crate) fn add_turbo_fish(acc: &mut Assists, ctx: &AssistContext) -> Option<(
65 84
66#[cfg(test)] 85#[cfg(test)]
67mod tests { 86mod tests {
68 use crate::tests::{check_assist, check_assist_not_applicable}; 87 use crate::tests::{check_assist, check_assist_by_label, check_assist_not_applicable};
69 88
70 use super::*; 89 use super::*;
71 use test_utils::mark; 90 use test_utils::mark;
@@ -161,4 +180,91 @@ fn main() {
161"#, 180"#,
162 ); 181 );
163 } 182 }
183
184 #[test]
185 fn add_type_ascription_function() {
186 check_assist_by_label(
187 add_turbo_fish,
188 r#"
189fn make<T>() -> T {}
190fn main() {
191 let x = make$0();
192}
193"#,
194 r#"
195fn make<T>() -> T {}
196fn main() {
197 let x: ${0:_} = make();
198}
199"#,
200 "Add `: _` before assignment operator",
201 );
202 }
203
204 #[test]
205 fn add_type_ascription_after_call() {
206 mark::check!(add_type_ascription_after_call);
207 check_assist_by_label(
208 add_turbo_fish,
209 r#"
210fn make<T>() -> T {}
211fn main() {
212 let x = make()$0;
213}
214"#,
215 r#"
216fn make<T>() -> T {}
217fn main() {
218 let x: ${0:_} = make();
219}
220"#,
221 "Add `: _` before assignment operator",
222 );
223 }
224
225 #[test]
226 fn add_type_ascription_method() {
227 check_assist_by_label(
228 add_turbo_fish,
229 r#"
230struct S;
231impl S {
232 fn make<T>(&self) -> T {}
233}
234fn main() {
235 let x = S.make$0();
236}
237"#,
238 r#"
239struct S;
240impl S {
241 fn make<T>(&self) -> T {}
242}
243fn main() {
244 let x: ${0:_} = S.make();
245}
246"#,
247 "Add `: _` before assignment operator",
248 );
249 }
250
251 #[test]
252 fn add_type_ascription_already_typed() {
253 mark::check!(add_type_ascription_already_typed);
254 check_assist(
255 add_turbo_fish,
256 r#"
257fn make<T>() -> T {}
258fn main() {
259 let x: () = make$0();
260}
261"#,
262 r#"
263fn make<T>() -> T {}
264fn main() {
265 let x: () = make::<${0:_}>();
266}
267"#,
268 );
269 }
164} 270}
diff --git a/crates/ide_assists/src/handlers/apply_demorgan.rs b/crates/ide_assists/src/handlers/apply_demorgan.rs
index 6997ea048..128b1eb56 100644
--- a/crates/ide_assists/src/handlers/apply_demorgan.rs
+++ b/crates/ide_assists/src/handlers/apply_demorgan.rs
@@ -1,4 +1,5 @@
1use syntax::ast::{self, AstNode}; 1use syntax::ast::{self, AstNode};
2use test_utils::mark;
2 3
3use crate::{utils::invert_boolean_expression, AssistContext, AssistId, AssistKind, Assists}; 4use crate::{utils::invert_boolean_expression, AssistContext, AssistId, AssistKind, Assists};
4 5
@@ -43,9 +44,36 @@ pub(crate) fn apply_demorgan(acc: &mut Assists, ctx: &AssistContext) -> Option<(
43 "Apply De Morgan's law", 44 "Apply De Morgan's law",
44 op_range, 45 op_range,
45 |edit| { 46 |edit| {
47 let paren_expr = expr.syntax().parent().and_then(|parent| ast::ParenExpr::cast(parent));
48
49 let neg_expr = paren_expr
50 .clone()
51 .and_then(|paren_expr| paren_expr.syntax().parent())
52 .and_then(|parent| ast::PrefixExpr::cast(parent))
53 .and_then(|prefix_expr| {
54 if prefix_expr.op_kind().unwrap() == ast::PrefixOp::Not {
55 Some(prefix_expr)
56 } else {
57 None
58 }
59 });
60
46 edit.replace(op_range, opposite_op); 61 edit.replace(op_range, opposite_op);
47 edit.replace(lhs_range, format!("!({}", not_lhs.syntax().text())); 62
48 edit.replace(rhs_range, format!("{})", not_rhs.syntax().text())); 63 if let Some(paren_expr) = paren_expr {
64 edit.replace(lhs_range, not_lhs.syntax().text());
65 edit.replace(rhs_range, not_rhs.syntax().text());
66 if let Some(neg_expr) = neg_expr {
67 mark::hit!(demorgan_double_negation);
68 edit.replace(neg_expr.op_token().unwrap().text_range(), "");
69 } else {
70 mark::hit!(demorgan_double_parens);
71 edit.replace(paren_expr.l_paren_token().unwrap().text_range(), "!(");
72 }
73 } else {
74 edit.replace(lhs_range, format!("!({}", not_lhs.syntax().text()));
75 edit.replace(rhs_range, format!("{})", not_rhs.syntax().text()));
76 }
49 }, 77 },
50 ) 78 )
51} 79}
@@ -62,6 +90,7 @@ fn opposite_logic_op(kind: ast::BinOp) -> Option<&'static str> {
62#[cfg(test)] 90#[cfg(test)]
63mod tests { 91mod tests {
64 use ide_db::helpers::FamousDefs; 92 use ide_db::helpers::FamousDefs;
93 use test_utils::mark;
65 94
66 use super::*; 95 use super::*;
67 96
@@ -156,4 +185,16 @@ fn f() {
156 fn demorgan_doesnt_apply_with_cursor_not_on_op() { 185 fn demorgan_doesnt_apply_with_cursor_not_on_op() {
157 check_assist_not_applicable(apply_demorgan, "fn f() { $0 !x || !x }") 186 check_assist_not_applicable(apply_demorgan, "fn f() { $0 !x || !x }")
158 } 187 }
188
189 #[test]
190 fn demorgan_doesnt_double_negation() {
191 mark::check!(demorgan_double_negation);
192 check_assist(apply_demorgan, "fn f() { !(x ||$0 x) }", "fn f() { (!x && !x) }")
193 }
194
195 #[test]
196 fn demorgan_doesnt_double_parens() {
197 mark::check!(demorgan_double_parens);
198 check_assist(apply_demorgan, "fn f() { (x ||$0 x) }", "fn f() { !(!x && !x) }")
199 }
159} 200}
diff --git a/crates/ide_assists/src/handlers/convert_comment_block.rs b/crates/ide_assists/src/handlers/convert_comment_block.rs
new file mode 100644
index 000000000..cdc45fc42
--- /dev/null
+++ b/crates/ide_assists/src/handlers/convert_comment_block.rs
@@ -0,0 +1,419 @@
1use itertools::Itertools;
2use std::convert::identity;
3use syntax::{
4 ast::{
5 self,
6 edit::IndentLevel,
7 Comment, CommentKind,
8 CommentPlacement::{Inner, Outer},
9 CommentShape::{self, Block, Line},
10 Whitespace,
11 },
12 AstToken, Direction, SyntaxElement, TextRange,
13};
14
15use crate::{AssistContext, AssistId, AssistKind, Assists};
16
17/// Assist: line_to_block
18///
19/// Converts comments between block and single-line form
20///
21/// ```
22/// // Multi-line
23/// // comment
24/// ```
25/// ->
26/// ```
27/// /**
28/// Multi-line
29/// comment
30/// */
31/// ```
32pub(crate) fn convert_comment_block(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
33 if let Some(comment) = ctx.find_token_at_offset::<ast::Comment>() {
34 // Only allow comments which are alone on their line
35 if let Some(prev) = comment.syntax().prev_token() {
36 if Whitespace::cast(prev).filter(|w| w.text().contains('\n')).is_none() {
37 return None;
38 }
39 }
40
41 return match comment.kind().shape {
42 ast::CommentShape::Block => block_to_line(acc, comment),
43 ast::CommentShape::Line => line_to_block(acc, comment),
44 };
45 }
46
47 return None;
48}
49
50fn block_to_line(acc: &mut Assists, comment: ast::Comment) -> Option<()> {
51 let target = comment.syntax().text_range();
52
53 acc.add(
54 AssistId("block_to_line", AssistKind::RefactorRewrite),
55 "Replace block comment with line comments",
56 target,
57 |edit| {
58 let indentation = IndentLevel::from_token(comment.syntax());
59 let line_prefix =
60 comment_kind_prefix(CommentKind { shape: CommentShape::Line, ..comment.kind() });
61
62 let text = comment.text();
63 let text = &text[comment.prefix().len()..(text.len() - "*/".len())].trim();
64
65 let lines = text.lines().peekable();
66
67 let indent_spaces = indentation.to_string();
68 let output = lines
69 .map(|l| l.trim_start_matches(&indent_spaces))
70 .map(|l| {
71 // Don't introduce trailing whitespace
72 if l.is_empty() {
73 line_prefix.to_string()
74 } else {
75 format!("{} {}", line_prefix, l.trim_start_matches(&indent_spaces))
76 }
77 })
78 .join(&format!("\n{}", indent_spaces));
79
80 edit.replace(target, output)
81 },
82 )
83}
84
85fn line_to_block(acc: &mut Assists, comment: ast::Comment) -> Option<()> {
86 // Find all the comments we'll be collapsing into a block
87 let comments = relevant_line_comments(&comment);
88
89 // Establish the target of our edit based on the comments we found
90 let target = TextRange::new(
91 comments[0].syntax().text_range().start(),
92 comments.last().unwrap().syntax().text_range().end(),
93 );
94
95 acc.add(
96 AssistId("line_to_block", AssistKind::RefactorRewrite),
97 "Replace line comments with a single block comment",
98 target,
99 |edit| {
100 // We pick a single indentation level for the whole block comment based on the
101 // comment where the assist was invoked. This will be prepended to the
102 // contents of each line comment when they're put into the block comment.
103 let indentation = IndentLevel::from_token(&comment.syntax());
104
105 let block_comment_body =
106 comments.into_iter().map(|c| line_comment_text(indentation, c)).join("\n");
107
108 let block_prefix =
109 comment_kind_prefix(CommentKind { shape: CommentShape::Block, ..comment.kind() });
110
111 let output =
112 format!("{}\n{}\n{}*/", block_prefix, block_comment_body, indentation.to_string());
113
114 edit.replace(target, output)
115 },
116 )
117}
118
119/// The line -> block assist can be invoked from anywhere within a sequence of line comments.
120/// relevant_line_comments crawls backwards and forwards finding the complete sequence of comments that will
121/// be joined.
122fn relevant_line_comments(comment: &ast::Comment) -> Vec<Comment> {
123 // The prefix identifies the kind of comment we're dealing with
124 let prefix = comment.prefix();
125 let same_prefix = |c: &ast::Comment| c.prefix() == prefix;
126
127 // These tokens are allowed to exist between comments
128 let skippable = |not: &SyntaxElement| {
129 not.clone()
130 .into_token()
131 .and_then(Whitespace::cast)
132 .map(|w| !w.spans_multiple_lines())
133 .unwrap_or(false)
134 };
135
136 // Find all preceding comments (in reverse order) that have the same prefix
137 let prev_comments = comment
138 .syntax()
139 .siblings_with_tokens(Direction::Prev)
140 .filter(|s| !skippable(s))
141 .map(|not| not.into_token().and_then(Comment::cast).filter(same_prefix))
142 .take_while(|opt_com| opt_com.is_some())
143 .filter_map(identity)
144 .skip(1); // skip the first element so we don't duplicate it in next_comments
145
146 let next_comments = comment
147 .syntax()
148 .siblings_with_tokens(Direction::Next)
149 .filter(|s| !skippable(s))
150 .map(|not| not.into_token().and_then(Comment::cast).filter(same_prefix))
151 .take_while(|opt_com| opt_com.is_some())
152 .filter_map(identity);
153
154 let mut comments: Vec<_> = prev_comments.collect();
155 comments.reverse();
156 comments.extend(next_comments);
157 comments
158}
159
160// Line comments usually begin with a single space character following the prefix as seen here:
161//^
162// But comments can also include indented text:
163// > Hello there
164//
165// We handle this by stripping *AT MOST* one space character from the start of the line
166// This has its own problems because it can cause alignment issues:
167//
168// /*
169// a ----> a
170//b ----> b
171// */
172//
173// But since such comments aren't idiomatic we're okay with this.
174fn line_comment_text(indentation: IndentLevel, comm: ast::Comment) -> String {
175 let contents_without_prefix = comm.text().strip_prefix(comm.prefix()).unwrap();
176 let contents = contents_without_prefix.strip_prefix(' ').unwrap_or(contents_without_prefix);
177
178 // Don't add the indentation if the line is empty
179 if contents.is_empty() {
180 contents.to_owned()
181 } else {
182 indentation.to_string() + &contents
183 }
184}
185
186fn comment_kind_prefix(ck: ast::CommentKind) -> &'static str {
187 match (ck.shape, ck.doc) {
188 (Line, Some(Inner)) => "//!",
189 (Line, Some(Outer)) => "///",
190 (Line, None) => "//",
191 (Block, Some(Inner)) => "/*!",
192 (Block, Some(Outer)) => "/**",
193 (Block, None) => "/*",
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use crate::tests::{check_assist, check_assist_not_applicable};
200
201 use super::*;
202
203 #[test]
204 fn single_line_to_block() {
205 check_assist(
206 convert_comment_block,
207 r#"
208// line$0 comment
209fn main() {
210 foo();
211}
212"#,
213 r#"
214/*
215line comment
216*/
217fn main() {
218 foo();
219}
220"#,
221 );
222 }
223
224 #[test]
225 fn single_line_to_block_indented() {
226 check_assist(
227 convert_comment_block,
228 r#"
229fn main() {
230 // line$0 comment
231 foo();
232}
233"#,
234 r#"
235fn main() {
236 /*
237 line comment
238 */
239 foo();
240}
241"#,
242 );
243 }
244
245 #[test]
246 fn multiline_to_block() {
247 check_assist(
248 convert_comment_block,
249 r#"
250fn main() {
251 // above
252 // line$0 comment
253 //
254 // below
255 foo();
256}
257"#,
258 r#"
259fn main() {
260 /*
261 above
262 line comment
263
264 below
265 */
266 foo();
267}
268"#,
269 );
270 }
271
272 #[test]
273 fn end_of_line_to_block() {
274 check_assist_not_applicable(
275 convert_comment_block,
276 r#"
277fn main() {
278 foo(); // end-of-line$0 comment
279}
280"#,
281 );
282 }
283
284 #[test]
285 fn single_line_different_kinds() {
286 check_assist(
287 convert_comment_block,
288 r#"
289fn main() {
290 /// different prefix
291 // line$0 comment
292 // below
293 foo();
294}
295"#,
296 r#"
297fn main() {
298 /// different prefix
299 /*
300 line comment
301 below
302 */
303 foo();
304}
305"#,
306 );
307 }
308
309 #[test]
310 fn single_line_separate_chunks() {
311 check_assist(
312 convert_comment_block,
313 r#"
314fn main() {
315 // different chunk
316
317 // line$0 comment
318 // below
319 foo();
320}
321"#,
322 r#"
323fn main() {
324 // different chunk
325
326 /*
327 line comment
328 below
329 */
330 foo();
331}
332"#,
333 );
334 }
335
336 #[test]
337 fn doc_block_comment_to_lines() {
338 check_assist(
339 convert_comment_block,
340 r#"
341/**
342 hi$0 there
343*/
344"#,
345 r#"
346/// hi there
347"#,
348 );
349 }
350
351 #[test]
352 fn block_comment_to_lines() {
353 check_assist(
354 convert_comment_block,
355 r#"
356/*
357 hi$0 there
358*/
359"#,
360 r#"
361// hi there
362"#,
363 );
364 }
365
366 #[test]
367 fn inner_doc_block_to_lines() {
368 check_assist(
369 convert_comment_block,
370 r#"
371/*!
372 hi$0 there
373*/
374"#,
375 r#"
376//! hi there
377"#,
378 );
379 }
380
381 #[test]
382 fn block_to_lines_indent() {
383 check_assist(
384 convert_comment_block,
385 r#"
386fn main() {
387 /*!
388 hi$0 there
389
390 ```
391 code_sample
392 ```
393 */
394}
395"#,
396 r#"
397fn main() {
398 //! hi there
399 //!
400 //! ```
401 //! code_sample
402 //! ```
403}
404"#,
405 );
406 }
407
408 #[test]
409 fn end_of_line_block_to_line() {
410 check_assist_not_applicable(
411 convert_comment_block,
412 r#"
413fn main() {
414 foo(); /* end-of-line$0 comment */
415}
416"#,
417 );
418 }
419}
diff --git a/crates/ide_assists/src/handlers/extract_variable.rs b/crates/ide_assists/src/handlers/extract_variable.rs
index 98f3dc6ca..312ac7ac4 100644
--- a/crates/ide_assists/src/handlers/extract_variable.rs
+++ b/crates/ide_assists/src/handlers/extract_variable.rs
@@ -8,7 +8,7 @@ use syntax::{
8}; 8};
9use test_utils::mark; 9use test_utils::mark;
10 10
11use crate::{AssistContext, AssistId, AssistKind, Assists}; 11use crate::{utils::suggest_name, AssistContext, AssistId, AssistKind, Assists};
12 12
13// Assist: extract_variable 13// Assist: extract_variable
14// 14//
@@ -54,7 +54,7 @@ pub(crate) fn extract_variable(acc: &mut Assists, ctx: &AssistContext) -> Option
54 54
55 let var_name = match &field_shorthand { 55 let var_name = match &field_shorthand {
56 Some(it) => it.to_string(), 56 Some(it) => it.to_string(),
57 None => "var_name".to_string(), 57 None => suggest_name::variable(&to_extract, &ctx.sema),
58 }; 58 };
59 let expr_range = match &field_shorthand { 59 let expr_range = match &field_shorthand {
60 Some(it) => it.syntax().text_range().cover(to_extract.syntax().text_range()), 60 Some(it) => it.syntax().text_range().cover(to_extract.syntax().text_range()),
@@ -274,8 +274,8 @@ fn foo() {
274"#, 274"#,
275 r#" 275 r#"
276fn foo() { 276fn foo() {
277 let $0var_name = bar(1 + 1); 277 let $0bar = bar(1 + 1);
278 var_name 278 bar
279} 279}
280"#, 280"#,
281 ) 281 )
@@ -401,8 +401,8 @@ fn main() {
401", 401",
402 " 402 "
403fn main() { 403fn main() {
404 let $0var_name = bar.foo(); 404 let $0foo = bar.foo();
405 let v = var_name; 405 let v = foo;
406} 406}
407", 407",
408 ); 408 );
@@ -557,6 +557,202 @@ fn main() {
557 } 557 }
558 558
559 #[test] 559 #[test]
560 fn extract_var_name_from_type() {
561 check_assist(
562 extract_variable,
563 r#"
564struct Test(i32);
565
566fn foo() -> Test {
567 $0{ Test(10) }$0
568}
569"#,
570 r#"
571struct Test(i32);
572
573fn foo() -> Test {
574 let $0test = { Test(10) };
575 test
576}
577"#,
578 )
579 }
580
581 #[test]
582 fn extract_var_name_from_parameter() {
583 check_assist(
584 extract_variable,
585 r#"
586fn bar(test: u32, size: u32)
587
588fn foo() {
589 bar(1, $01+1$0);
590}
591"#,
592 r#"
593fn bar(test: u32, size: u32)
594
595fn foo() {
596 let $0size = 1+1;
597 bar(1, size);
598}
599"#,
600 )
601 }
602
603 #[test]
604 fn extract_var_parameter_name_has_precedence_over_type() {
605 check_assist(
606 extract_variable,
607 r#"
608struct TextSize(u32);
609fn bar(test: u32, size: TextSize)
610
611fn foo() {
612 bar(1, $0{ TextSize(1+1) }$0);
613}
614"#,
615 r#"
616struct TextSize(u32);
617fn bar(test: u32, size: TextSize)
618
619fn foo() {
620 let $0size = { TextSize(1+1) };
621 bar(1, size);
622}
623"#,
624 )
625 }
626
627 #[test]
628 fn extract_var_name_from_function() {
629 check_assist(
630 extract_variable,
631 r#"
632fn is_required(test: u32, size: u32) -> bool
633
634fn foo() -> bool {
635 $0is_required(1, 2)$0
636}
637"#,
638 r#"
639fn is_required(test: u32, size: u32) -> bool
640
641fn foo() -> bool {
642 let $0is_required = is_required(1, 2);
643 is_required
644}
645"#,
646 )
647 }
648
649 #[test]
650 fn extract_var_name_from_method() {
651 check_assist(
652 extract_variable,
653 r#"
654struct S;
655impl S {
656 fn bar(&self, n: u32) -> u32 { n }
657}
658
659fn foo() -> u32 {
660 $0S.bar(1)$0
661}
662"#,
663 r#"
664struct S;
665impl S {
666 fn bar(&self, n: u32) -> u32 { n }
667}
668
669fn foo() -> u32 {
670 let $0bar = S.bar(1);
671 bar
672}
673"#,
674 )
675 }
676
677 #[test]
678 fn extract_var_name_from_method_param() {
679 check_assist(
680 extract_variable,
681 r#"
682struct S;
683impl S {
684 fn bar(&self, n: u32, size: u32) { n }
685}
686
687fn foo() {
688 S.bar($01 + 1$0, 2)
689}
690"#,
691 r#"
692struct S;
693impl S {
694 fn bar(&self, n: u32, size: u32) { n }
695}
696
697fn foo() {
698 let $0n = 1 + 1;
699 S.bar(n, 2)
700}
701"#,
702 )
703 }
704
705 #[test]
706 fn extract_var_name_from_ufcs_method_param() {
707 check_assist(
708 extract_variable,
709 r#"
710struct S;
711impl S {
712 fn bar(&self, n: u32, size: u32) { n }
713}
714
715fn foo() {
716 S::bar(&S, $01 + 1$0, 2)
717}
718"#,
719 r#"
720struct S;
721impl S {
722 fn bar(&self, n: u32, size: u32) { n }
723}
724
725fn foo() {
726 let $0n = 1 + 1;
727 S::bar(&S, n, 2)
728}
729"#,
730 )
731 }
732
733 #[test]
734 fn extract_var_parameter_name_has_precedence_over_function() {
735 check_assist(
736 extract_variable,
737 r#"
738fn bar(test: u32, size: u32)
739
740fn foo() {
741 bar(1, $0symbol_size(1, 2)$0);
742}
743"#,
744 r#"
745fn bar(test: u32, size: u32)
746
747fn foo() {
748 let $0size = symbol_size(1, 2);
749 bar(1, size);
750}
751"#,
752 )
753 }
754
755 #[test]
560 fn test_extract_var_for_return_not_applicable() { 756 fn test_extract_var_for_return_not_applicable() {
561 check_assist_not_applicable(extract_variable, "fn foo() { $0return$0; } "); 757 check_assist_not_applicable(extract_variable, "fn foo() { $0return$0; } ");
562 } 758 }
diff --git a/crates/ide_assists/src/lib.rs b/crates/ide_assists/src/lib.rs
index 53542d433..9c8148462 100644
--- a/crates/ide_assists/src/lib.rs
+++ b/crates/ide_assists/src/lib.rs
@@ -115,6 +115,7 @@ mod handlers {
115 mod auto_import; 115 mod auto_import;
116 mod change_visibility; 116 mod change_visibility;
117 mod convert_integer_literal; 117 mod convert_integer_literal;
118 mod convert_comment_block;
118 mod early_return; 119 mod early_return;
119 mod expand_glob_import; 120 mod expand_glob_import;
120 mod extract_function; 121 mod extract_function;
@@ -178,6 +179,7 @@ mod handlers {
178 auto_import::auto_import, 179 auto_import::auto_import,
179 change_visibility::change_visibility, 180 change_visibility::change_visibility,
180 convert_integer_literal::convert_integer_literal, 181 convert_integer_literal::convert_integer_literal,
182 convert_comment_block::convert_comment_block,
181 early_return::convert_to_guarded_return, 183 early_return::convert_to_guarded_return,
182 expand_glob_import::expand_glob_import, 184 expand_glob_import::expand_glob_import,
183 extract_struct_from_enum_variant::extract_struct_from_enum_variant, 185 extract_struct_from_enum_variant::extract_struct_from_enum_variant,
diff --git a/crates/ide_assists/src/utils.rs b/crates/ide_assists/src/utils.rs
index 880ab6fe3..62f959082 100644
--- a/crates/ide_assists/src/utils.rs
+++ b/crates/ide_assists/src/utils.rs
@@ -1,5 +1,7 @@
1//! Assorted functions shared by several assists. 1//! Assorted functions shared by several assists.
2 2
3pub(crate) mod suggest_name;
4
3use std::ops; 5use std::ops;
4 6
5use ast::TypeBoundsOwner; 7use ast::TypeBoundsOwner;
diff --git a/crates/ide_assists/src/utils/suggest_name.rs b/crates/ide_assists/src/utils/suggest_name.rs
new file mode 100644
index 000000000..533624c1f
--- /dev/null
+++ b/crates/ide_assists/src/utils/suggest_name.rs
@@ -0,0 +1,729 @@
1//! This module contains functions to suggest names for expressions, functions and other items
2
3use hir::Semantics;
4use ide_db::RootDatabase;
5use itertools::Itertools;
6use stdx::to_lower_snake_case;
7use syntax::{
8 ast::{self, NameOwner},
9 match_ast, AstNode,
10};
11
12/// Trait names, that will be ignored when in `impl Trait` and `dyn Trait`
13const USELESS_TRAITS: &[&str] = &["Send", "Sync", "Copy", "Clone", "Eq", "PartialEq"];
14
15/// Identifier names that won't be suggested, ever
16///
17/// **NOTE**: they all must be snake lower case
18const USELESS_NAMES: &[&str] =
19 &["new", "default", "option", "some", "none", "ok", "err", "str", "string"];
20
21/// Generic types replaced by their first argument
22///
23/// # Examples
24/// `Option<Name>` -> `Name`
25/// `Result<User, Error>` -> `User`
26const WRAPPER_TYPES: &[&str] = &["Box", "Option", "Result"];
27
28/// Prefixes to strip from methods names
29///
30/// # Examples
31/// `vec.as_slice()` -> `slice`
32/// `args.into_config()` -> `config`
33/// `bytes.to_vec()` -> `vec`
34const USELESS_METHOD_PREFIXES: &[&str] = &["into_", "as_", "to_"];
35
36/// Useless methods that are stripped from expression
37///
38/// # Examples
39/// `var.name().to_string()` -> `var.name()`
40const USELESS_METHODS: &[&str] = &[
41 "to_string",
42 "as_str",
43 "to_owned",
44 "as_ref",
45 "clone",
46 "cloned",
47 "expect",
48 "expect_none",
49 "unwrap",
50 "unwrap_none",
51 "unwrap_or",
52 "unwrap_or_default",
53 "unwrap_or_else",
54 "unwrap_unchecked",
55 "iter",
56 "into_iter",
57 "iter_mut",
58];
59
60/// Suggest name of variable for given expression
61///
62/// **NOTE**: it is caller's responsibility to guarantee uniqueness of the name.
63/// I.e. it doesn't look for names in scope.
64///
65/// # Current implementation
66///
67/// In current implementation, the function tries to get the name from
68/// the following sources:
69///
70/// * if expr is an argument to function/method, use paramter name
71/// * if expr is a function/method call, use function name
72/// * expression type name if it exists (E.g. `()`, `fn() -> ()` or `!` do not have names)
73/// * fallback: `var_name`
74///
75/// It also applies heuristics to filter out less informative names
76///
77/// Currently it sticks to the first name found.
78pub(crate) fn variable(expr: &ast::Expr, sema: &Semantics<'_, RootDatabase>) -> String {
79 // `from_param` does not benifit from stripping
80 // it need the largest context possible
81 // so we check firstmost
82 if let Some(name) = from_param(expr, sema) {
83 return name;
84 }
85
86 let mut next_expr = Some(expr.clone());
87 while let Some(expr) = next_expr {
88 let name = from_call(&expr).or_else(|| from_type(&expr, sema));
89 if let Some(name) = name {
90 return name;
91 }
92
93 match expr {
94 ast::Expr::RefExpr(inner) => next_expr = inner.expr(),
95 ast::Expr::BoxExpr(inner) => next_expr = inner.expr(),
96 ast::Expr::AwaitExpr(inner) => next_expr = inner.expr(),
97 // ast::Expr::BlockExpr(block) => expr = block.tail_expr(),
98 ast::Expr::CastExpr(inner) => next_expr = inner.expr(),
99 ast::Expr::MethodCallExpr(method) if is_useless_method(&method) => {
100 next_expr = method.receiver();
101 }
102 ast::Expr::ParenExpr(inner) => next_expr = inner.expr(),
103 ast::Expr::TryExpr(inner) => next_expr = inner.expr(),
104 ast::Expr::PrefixExpr(prefix) if prefix.op_kind() == Some(ast::PrefixOp::Deref) => {
105 next_expr = prefix.expr()
106 }
107 _ => break,
108 }
109 }
110
111 "var_name".to_string()
112}
113
114fn normalize(name: &str) -> Option<String> {
115 let name = to_lower_snake_case(name);
116
117 if USELESS_NAMES.contains(&name.as_str()) {
118 return None;
119 }
120
121 if !is_valid_name(&name) {
122 return None;
123 }
124
125 Some(name)
126}
127
128fn is_valid_name(name: &str) -> bool {
129 match syntax::lex_single_syntax_kind(name) {
130 Some((syntax::SyntaxKind::IDENT, _error)) => true,
131 _ => false,
132 }
133}
134
135fn is_useless_method(method: &ast::MethodCallExpr) -> bool {
136 let ident = method.name_ref().and_then(|it| it.ident_token());
137
138 if let Some(ident) = ident {
139 USELESS_METHODS.contains(&ident.text())
140 } else {
141 false
142 }
143}
144
145fn from_call(expr: &ast::Expr) -> Option<String> {
146 from_func_call(expr).or_else(|| from_method_call(expr))
147}
148
149fn from_func_call(expr: &ast::Expr) -> Option<String> {
150 let call = match expr {
151 ast::Expr::CallExpr(call) => call,
152 _ => return None,
153 };
154 let func = match call.expr()? {
155 ast::Expr::PathExpr(path) => path,
156 _ => return None,
157 };
158 let ident = func.path()?.segment()?.name_ref()?.ident_token()?;
159 normalize(ident.text())
160}
161
162fn from_method_call(expr: &ast::Expr) -> Option<String> {
163 let method = match expr {
164 ast::Expr::MethodCallExpr(call) => call,
165 _ => return None,
166 };
167 let ident = method.name_ref()?.ident_token()?;
168 let mut name = ident.text();
169
170 if USELESS_METHODS.contains(&name) {
171 return None;
172 }
173
174 for prefix in USELESS_METHOD_PREFIXES {
175 if let Some(suffix) = name.strip_prefix(prefix) {
176 name = suffix;
177 break;
178 }
179 }
180
181 normalize(&name)
182}
183
184fn from_param(expr: &ast::Expr, sema: &Semantics<'_, RootDatabase>) -> Option<String> {
185 let arg_list = expr.syntax().parent().and_then(ast::ArgList::cast)?;
186 let args_parent = arg_list.syntax().parent()?;
187 let func = match_ast! {
188 match args_parent {
189 ast::CallExpr(call) => {
190 let func = call.expr()?;
191 let func_ty = sema.type_of_expr(&func)?;
192 func_ty.as_callable(sema.db)?
193 },
194 ast::MethodCallExpr(method) => sema.resolve_method_call_as_callable(&method)?,
195 _ => return None,
196 }
197 };
198
199 let (idx, _) = arg_list.args().find_position(|it| it == expr).unwrap();
200 let (pat, _) = func.params(sema.db).into_iter().nth(idx)?;
201 let pat = match pat? {
202 either::Either::Right(pat) => pat,
203 _ => return None,
204 };
205 let name = var_name_from_pat(&pat)?;
206 normalize(&name.to_string())
207}
208
209fn var_name_from_pat(pat: &ast::Pat) -> Option<ast::Name> {
210 match pat {
211 ast::Pat::IdentPat(var) => var.name(),
212 ast::Pat::RefPat(ref_pat) => var_name_from_pat(&ref_pat.pat()?),
213 ast::Pat::BoxPat(box_pat) => var_name_from_pat(&box_pat.pat()?),
214 _ => None,
215 }
216}
217
218fn from_type(expr: &ast::Expr, sema: &Semantics<'_, RootDatabase>) -> Option<String> {
219 let ty = sema.type_of_expr(expr)?;
220 let ty = ty.remove_ref().unwrap_or(ty);
221
222 name_of_type(&ty, sema.db)
223}
224
225fn name_of_type(ty: &hir::Type, db: &RootDatabase) -> Option<String> {
226 let name = if let Some(adt) = ty.as_adt() {
227 let name = adt.name(db).to_string();
228
229 if WRAPPER_TYPES.contains(&name.as_str()) {
230 let inner_ty = ty.type_parameters().next()?;
231 return name_of_type(&inner_ty, db);
232 }
233
234 name
235 } else if let Some(trait_) = ty.as_dyn_trait() {
236 trait_name(&trait_, db)?
237 } else if let Some(traits) = ty.as_impl_traits(db) {
238 let mut iter = traits.into_iter().filter_map(|t| trait_name(&t, db));
239 let name = iter.next()?;
240 if iter.next().is_some() {
241 return None;
242 }
243 name
244 } else {
245 return None;
246 };
247 normalize(&name)
248}
249
250fn trait_name(trait_: &hir::Trait, db: &RootDatabase) -> Option<String> {
251 let name = trait_.name(db).to_string();
252 if USELESS_TRAITS.contains(&name.as_str()) {
253 return None;
254 }
255 Some(name)
256}
257
258#[cfg(test)]
259mod tests {
260 use ide_db::base_db::{fixture::WithFixture, FileRange};
261
262 use super::*;
263
264 #[track_caller]
265 fn check(ra_fixture: &str, expected: &str) {
266 let (db, file_id, range_or_offset) = RootDatabase::with_range_or_offset(ra_fixture);
267 let frange = FileRange { file_id, range: range_or_offset.into() };
268
269 let sema = Semantics::new(&db);
270 let source_file = sema.parse(frange.file_id);
271 let element = source_file.syntax().covering_element(frange.range);
272 let expr =
273 element.ancestors().find_map(ast::Expr::cast).expect("selection is not an expression");
274 assert_eq!(
275 expr.syntax().text_range(),
276 frange.range,
277 "selection is not an expression(yet contained in one)"
278 );
279 let name = variable(&expr, &sema);
280 assert_eq!(&name, expected);
281 }
282
283 #[test]
284 fn no_args() {
285 check(r#"fn foo() { $0bar()$0 }"#, "bar");
286 check(r#"fn foo() { $0bar.frobnicate()$0 }"#, "frobnicate");
287 }
288
289 #[test]
290 fn single_arg() {
291 check(r#"fn foo() { $0bar(1)$0 }"#, "bar");
292 }
293
294 #[test]
295 fn many_args() {
296 check(r#"fn foo() { $0bar(1, 2, 3)$0 }"#, "bar");
297 }
298
299 #[test]
300 fn path() {
301 check(r#"fn foo() { $0i32::bar(1, 2, 3)$0 }"#, "bar");
302 }
303
304 #[test]
305 fn generic_params() {
306 check(r#"fn foo() { $0bar::<i32>(1, 2, 3)$0 }"#, "bar");
307 check(r#"fn foo() { $0bar.frobnicate::<i32, u32>()$0 }"#, "frobnicate");
308 }
309
310 #[test]
311 fn to_name() {
312 check(
313 r#"
314struct Args;
315struct Config;
316impl Args {
317 fn to_config(&self) -> Config {}
318}
319fn foo() {
320 $0Args.to_config()$0;
321}
322"#,
323 "config",
324 );
325 }
326
327 #[test]
328 fn plain_func() {
329 check(
330 r#"
331fn bar(n: i32, m: u32);
332fn foo() { bar($01$0, 2) }
333"#,
334 "n",
335 );
336 }
337
338 #[test]
339 fn mut_param() {
340 check(
341 r#"
342fn bar(mut n: i32, m: u32);
343fn foo() { bar($01$0, 2) }
344"#,
345 "n",
346 );
347 }
348
349 #[test]
350 fn func_does_not_exist() {
351 check(r#"fn foo() { bar($01$0, 2) }"#, "var_name");
352 }
353
354 #[test]
355 fn unnamed_param() {
356 check(
357 r#"
358fn bar(_: i32, m: u32);
359fn foo() { bar($01$0, 2) }
360"#,
361 "var_name",
362 );
363 }
364
365 #[test]
366 fn tuple_pat() {
367 check(
368 r#"
369fn bar((n, k): (i32, i32), m: u32);
370fn foo() {
371 bar($0(1, 2)$0, 3)
372}
373"#,
374 "var_name",
375 );
376 }
377
378 #[test]
379 fn ref_pat() {
380 check(
381 r#"
382fn bar(&n: &i32, m: u32);
383fn foo() { bar($0&1$0, 3) }
384"#,
385 "n",
386 );
387 }
388
389 #[test]
390 fn box_pat() {
391 check(
392 r#"
393fn bar(box n: &i32, m: u32);
394fn foo() { bar($01$0, 3) }
395"#,
396 "n",
397 );
398 }
399
400 #[test]
401 fn param_out_of_index() {
402 check(
403 r#"
404fn bar(n: i32, m: u32);
405fn foo() { bar(1, 2, $03$0) }
406"#,
407 "var_name",
408 );
409 }
410
411 #[test]
412 fn generic_param_resolved() {
413 check(
414 r#"
415fn bar<T>(n: T, m: u32);
416fn foo() { bar($01$0, 2) }
417"#,
418 "n",
419 );
420 }
421
422 #[test]
423 fn generic_param_unresolved() {
424 check(
425 r#"
426fn bar<T>(n: T, m: u32);
427fn foo<T>(x: T) { bar($0x$0, 2) }
428"#,
429 "n",
430 );
431 }
432
433 #[test]
434 fn method() {
435 check(
436 r#"
437struct S;
438impl S { fn bar(&self, n: i32, m: u32); }
439fn foo() { S.bar($01$0, 2) }
440"#,
441 "n",
442 );
443 }
444
445 #[test]
446 fn method_ufcs() {
447 check(
448 r#"
449struct S;
450impl S { fn bar(&self, n: i32, m: u32); }
451fn foo() { S::bar(&S, $01$0, 2) }
452"#,
453 "n",
454 );
455 }
456
457 #[test]
458 fn method_self() {
459 check(
460 r#"
461struct S;
462impl S { fn bar(&self, n: i32, m: u32); }
463fn foo() { S::bar($0&S$0, 1, 2) }
464"#,
465 "s",
466 );
467 }
468
469 #[test]
470 fn method_self_named() {
471 check(
472 r#"
473struct S;
474impl S { fn bar(strukt: &Self, n: i32, m: u32); }
475fn foo() { S::bar($0&S$0, 1, 2) }
476"#,
477 "strukt",
478 );
479 }
480
481 #[test]
482 fn i32() {
483 check(r#"fn foo() { let _: i32 = $01$0; }"#, "var_name");
484 }
485
486 #[test]
487 fn u64() {
488 check(r#"fn foo() { let _: u64 = $01$0; }"#, "var_name");
489 }
490
491 #[test]
492 fn bool() {
493 check(r#"fn foo() { let _: bool = $0true$0; }"#, "var_name");
494 }
495
496 #[test]
497 fn struct_unit() {
498 check(
499 r#"
500struct Seed;
501fn foo() { let _ = $0Seed$0; }
502"#,
503 "seed",
504 );
505 }
506
507 #[test]
508 fn struct_unit_to_snake() {
509 check(
510 r#"
511struct SeedState;
512fn foo() { let _ = $0SeedState$0; }
513"#,
514 "seed_state",
515 );
516 }
517
518 #[test]
519 fn struct_single_arg() {
520 check(
521 r#"
522struct Seed(u32);
523fn foo() { let _ = $0Seed(0)$0; }
524"#,
525 "seed",
526 );
527 }
528
529 #[test]
530 fn struct_with_fields() {
531 check(
532 r#"
533struct Seed { value: u32 }
534fn foo() { let _ = $0Seed { value: 0 }$0; }
535"#,
536 "seed",
537 );
538 }
539
540 #[test]
541 fn enum_() {
542 check(
543 r#"
544enum Kind { A, B }
545fn foo() { let _ = $0Kind::A$0; }
546"#,
547 "kind",
548 );
549 }
550
551 #[test]
552 fn enum_generic_resolved() {
553 check(
554 r#"
555enum Kind<T> { A { x: T }, B }
556fn foo() { let _ = $0Kind::A { x:1 }$0; }
557"#,
558 "kind",
559 );
560 }
561
562 #[test]
563 fn enum_generic_unresolved() {
564 check(
565 r#"
566enum Kind<T> { A { x: T }, B }
567fn foo<T>(x: T) { let _ = $0Kind::A { x }$0; }
568"#,
569 "kind",
570 );
571 }
572
573 #[test]
574 fn dyn_trait() {
575 check(
576 r#"
577trait DynHandler {}
578fn bar() -> dyn DynHandler {}
579fn foo() { $0(bar())$0; }
580"#,
581 "dyn_handler",
582 );
583 }
584
585 #[test]
586 fn impl_trait() {
587 check(
588 r#"
589trait StaticHandler {}
590fn bar() -> impl StaticHandler {}
591fn foo() { $0(bar())$0; }
592"#,
593 "static_handler",
594 );
595 }
596
597 #[test]
598 fn impl_trait_plus_clone() {
599 check(
600 r#"
601trait StaticHandler {}
602trait Clone {}
603fn bar() -> impl StaticHandler + Clone {}
604fn foo() { $0(bar())$0; }
605"#,
606 "static_handler",
607 );
608 }
609
610 #[test]
611 fn impl_trait_plus_lifetime() {
612 check(
613 r#"
614trait StaticHandler {}
615trait Clone {}
616fn bar<'a>(&'a i32) -> impl StaticHandler + 'a {}
617fn foo() { $0(bar(&1))$0; }
618"#,
619 "static_handler",
620 );
621 }
622
623 #[test]
624 fn impl_trait_plus_trait() {
625 check(
626 r#"
627trait Handler {}
628trait StaticHandler {}
629fn bar() -> impl StaticHandler + Handler {}
630fn foo() { $0(bar())$0; }
631"#,
632 "bar",
633 );
634 }
635
636 #[test]
637 fn ref_value() {
638 check(
639 r#"
640struct Seed;
641fn bar() -> &Seed {}
642fn foo() { $0(bar())$0; }
643"#,
644 "seed",
645 );
646 }
647
648 #[test]
649 fn box_value() {
650 check(
651 r#"
652struct Box<T>(*const T);
653struct Seed;
654fn bar() -> Box<Seed> {}
655fn foo() { $0(bar())$0; }
656"#,
657 "seed",
658 );
659 }
660
661 #[test]
662 fn box_generic() {
663 check(
664 r#"
665struct Box<T>(*const T);
666fn bar<T>() -> Box<T> {}
667fn foo<T>() { $0(bar::<T>())$0; }
668"#,
669 "bar",
670 );
671 }
672
673 #[test]
674 fn option_value() {
675 check(
676 r#"
677enum Option<T> { Some(T) }
678struct Seed;
679fn bar() -> Option<Seed> {}
680fn foo() { $0(bar())$0; }
681"#,
682 "seed",
683 );
684 }
685
686 #[test]
687 fn result_value() {
688 check(
689 r#"
690enum Result<T, E> { Ok(T), Err(E) }
691struct Seed;
692struct Error;
693fn bar() -> Result<Seed, Error> {}
694fn foo() { $0(bar())$0; }
695"#,
696 "seed",
697 );
698 }
699
700 #[test]
701 fn ref_call() {
702 check(
703 r#"
704fn foo() { $0&bar(1, 3)$0 }
705"#,
706 "bar",
707 );
708 }
709
710 #[test]
711 fn name_to_string() {
712 check(
713 r#"
714fn foo() { $0function.name().to_string()$0 }
715"#,
716 "name",
717 );
718 }
719
720 #[test]
721 fn nested_useless_method() {
722 check(
723 r#"
724fn foo() { $0function.name().as_ref().unwrap().to_string()$0 }
725"#,
726 "name",
727 );
728 }
729}