aboutsummaryrefslogtreecommitdiff
path: root/crates/ide/src
diff options
context:
space:
mode:
Diffstat (limited to 'crates/ide/src')
-rw-r--r--crates/ide/src/diagnostics.rs67
-rw-r--r--crates/ide/src/display/short_label.rs6
-rw-r--r--crates/ide/src/folding_ranges.rs55
-rw-r--r--crates/ide/src/hover.rs235
-rw-r--r--crates/ide/src/join_lines.rs72
-rw-r--r--crates/ide/src/lib.rs12
-rw-r--r--crates/ide/src/matching_brace.rs5
-rw-r--r--crates/ide/src/parent_module.rs6
-rw-r--r--crates/ide/src/references.rs9
-rw-r--r--crates/ide/src/references/rename.rs478
-rw-r--r--crates/ide/src/runnables.rs10
-rw-r--r--crates/ide/src/ssr.rs259
-rw-r--r--crates/ide/src/syntax_highlighting/highlight.rs5
-rw-r--r--crates/ide/src/typing/on_enter.rs8
14 files changed, 920 insertions, 307 deletions
diff --git a/crates/ide/src/diagnostics.rs b/crates/ide/src/diagnostics.rs
index 8607139ba..fe32f39b6 100644
--- a/crates/ide/src/diagnostics.rs
+++ b/crates/ide/src/diagnostics.rs
@@ -10,15 +10,16 @@ mod field_shorthand;
10use std::cell::RefCell; 10use std::cell::RefCell;
11 11
12use hir::{ 12use hir::{
13 db::AstDatabase,
13 diagnostics::{Diagnostic as _, DiagnosticCode, DiagnosticSinkBuilder}, 14 diagnostics::{Diagnostic as _, DiagnosticCode, DiagnosticSinkBuilder},
14 Semantics, 15 InFile, Semantics,
15}; 16};
16use ide_db::{base_db::SourceDatabase, RootDatabase}; 17use ide_db::{base_db::SourceDatabase, RootDatabase};
17use itertools::Itertools; 18use itertools::Itertools;
18use rustc_hash::FxHashSet; 19use rustc_hash::FxHashSet;
19use syntax::{ 20use syntax::{
20 ast::{self, AstNode}, 21 ast::{self, AstNode},
21 SyntaxNode, TextRange, 22 SyntaxNode, SyntaxNodePtr, TextRange,
22}; 23};
23use text_edit::TextEdit; 24use text_edit::TextEdit;
24 25
@@ -147,20 +148,38 @@ pub(crate) fn diagnostics(
147 148
148 // Override severity and mark as unused. 149 // Override severity and mark as unused.
149 res.borrow_mut().push( 150 res.borrow_mut().push(
150 Diagnostic::hint(sema.diagnostics_display_range(d).range, d.message()) 151 Diagnostic::hint(
151 .with_unused(true) 152 sema.diagnostics_display_range(d.display_source()).range,
152 .with_code(Some(d.code())), 153 d.message(),
154 )
155 .with_unused(true)
156 .with_code(Some(d.code())),
153 ); 157 );
154 }) 158 })
155 .on::<hir::diagnostics::UnresolvedProcMacro, _>(|d| { 159 .on::<hir::diagnostics::UnresolvedProcMacro, _>(|d| {
156 // Use more accurate position if available. 160 // Use more accurate position if available.
157 let display_range = 161 let display_range = d
158 d.precise_location.unwrap_or_else(|| sema.diagnostics_display_range(d).range); 162 .precise_location
163 .unwrap_or_else(|| sema.diagnostics_display_range(d.display_source()).range);
159 164
160 // FIXME: it would be nice to tell the user whether proc macros are currently disabled 165 // FIXME: it would be nice to tell the user whether proc macros are currently disabled
161 res.borrow_mut() 166 res.borrow_mut()
162 .push(Diagnostic::hint(display_range, d.message()).with_code(Some(d.code()))); 167 .push(Diagnostic::hint(display_range, d.message()).with_code(Some(d.code())));
163 }) 168 })
169 .on::<hir::diagnostics::UnresolvedMacroCall, _>(|d| {
170 let last_path_segment = sema.db.parse_or_expand(d.file).and_then(|root| {
171 d.node
172 .to_node(&root)
173 .path()
174 .and_then(|it| it.segment())
175 .and_then(|it| it.name_ref())
176 .map(|it| InFile::new(d.file, SyntaxNodePtr::new(it.syntax())))
177 });
178 let diagnostics = last_path_segment.unwrap_or_else(|| d.display_source());
179 let display_range = sema.diagnostics_display_range(diagnostics).range;
180 res.borrow_mut()
181 .push(Diagnostic::error(display_range, d.message()).with_code(Some(d.code())));
182 })
164 // Only collect experimental diagnostics when they're enabled. 183 // Only collect experimental diagnostics when they're enabled.
165 .filter(|diag| !(diag.is_experimental() && config.disable_experimental)) 184 .filter(|diag| !(diag.is_experimental() && config.disable_experimental))
166 .filter(|diag| !config.disabled.contains(diag.code().as_str())); 185 .filter(|diag| !config.disabled.contains(diag.code().as_str()));
@@ -170,8 +189,11 @@ pub(crate) fn diagnostics(
170 // Diagnostics not handled above get no fix and default treatment. 189 // Diagnostics not handled above get no fix and default treatment.
171 .build(|d| { 190 .build(|d| {
172 res.borrow_mut().push( 191 res.borrow_mut().push(
173 Diagnostic::error(sema.diagnostics_display_range(d).range, d.message()) 192 Diagnostic::error(
174 .with_code(Some(d.code())), 193 sema.diagnostics_display_range(d.display_source()).range,
194 d.message(),
195 )
196 .with_code(Some(d.code())),
175 ); 197 );
176 }); 198 });
177 199
@@ -183,13 +205,13 @@ pub(crate) fn diagnostics(
183} 205}
184 206
185fn diagnostic_with_fix<D: DiagnosticWithFix>(d: &D, sema: &Semantics<RootDatabase>) -> Diagnostic { 207fn diagnostic_with_fix<D: DiagnosticWithFix>(d: &D, sema: &Semantics<RootDatabase>) -> Diagnostic {
186 Diagnostic::error(sema.diagnostics_display_range(d).range, d.message()) 208 Diagnostic::error(sema.diagnostics_display_range(d.display_source()).range, d.message())
187 .with_fix(d.fix(&sema)) 209 .with_fix(d.fix(&sema))
188 .with_code(Some(d.code())) 210 .with_code(Some(d.code()))
189} 211}
190 212
191fn warning_with_fix<D: DiagnosticWithFix>(d: &D, sema: &Semantics<RootDatabase>) -> Diagnostic { 213fn warning_with_fix<D: DiagnosticWithFix>(d: &D, sema: &Semantics<RootDatabase>) -> Diagnostic {
192 Diagnostic::hint(sema.diagnostics_display_range(d).range, d.message()) 214 Diagnostic::hint(sema.diagnostics_display_range(d.display_source()).range, d.message())
193 .with_fix(d.fix(&sema)) 215 .with_fix(d.fix(&sema))
194 .with_code(Some(d.code())) 216 .with_code(Some(d.code()))
195} 217}
@@ -646,6 +668,29 @@ fn test_fn() {
646 } 668 }
647 669
648 #[test] 670 #[test]
671 fn test_unresolved_macro_range() {
672 check_expect(
673 r#"foo::bar!(92);"#,
674 expect![[r#"
675 [
676 Diagnostic {
677 message: "unresolved macro call",
678 range: 5..8,
679 severity: Error,
680 fix: None,
681 unused: false,
682 code: Some(
683 DiagnosticCode(
684 "unresolved-macro-call",
685 ),
686 ),
687 },
688 ]
689 "#]],
690 );
691 }
692
693 #[test]
649 fn range_mapping_out_of_macros() { 694 fn range_mapping_out_of_macros() {
650 // FIXME: this is very wrong, but somewhat tricky to fix. 695 // FIXME: this is very wrong, but somewhat tricky to fix.
651 check_fix( 696 check_fix(
diff --git a/crates/ide/src/display/short_label.rs b/crates/ide/src/display/short_label.rs
index 84b8883de..2df9266b4 100644
--- a/crates/ide/src/display/short_label.rs
+++ b/crates/ide/src/display/short_label.rs
@@ -71,11 +71,7 @@ impl ShortLabel for ast::TypeAlias {
71 71
72impl ShortLabel for ast::Const { 72impl ShortLabel for ast::Const {
73 fn short_label(&self) -> Option<String> { 73 fn short_label(&self) -> Option<String> {
74 let mut new_buf = short_label_from_ty(self, self.ty(), "const ")?; 74 short_label_from_ty(self, self.ty(), "const ")
75 if let Some(expr) = self.body() {
76 format_to!(new_buf, " = {}", expr.syntax());
77 }
78 Some(new_buf)
79 } 75 }
80} 76}
81 77
diff --git a/crates/ide/src/folding_ranges.rs b/crates/ide/src/folding_ranges.rs
index 45170dd29..4b1b24562 100644
--- a/crates/ide/src/folding_ranges.rs
+++ b/crates/ide/src/folding_ranges.rs
@@ -6,7 +6,7 @@ use syntax::{
6 ast::{self, AstNode, AstToken, VisibilityOwner}, 6 ast::{self, AstNode, AstToken, VisibilityOwner},
7 Direction, NodeOrToken, SourceFile, 7 Direction, NodeOrToken, SourceFile,
8 SyntaxKind::{self, *}, 8 SyntaxKind::{self, *},
9 SyntaxNode, TextRange, 9 SyntaxNode, TextRange, TextSize,
10}; 10};
11 11
12#[derive(Debug, PartialEq, Eq)] 12#[derive(Debug, PartialEq, Eq)]
@@ -16,6 +16,7 @@ pub enum FoldKind {
16 Mods, 16 Mods,
17 Block, 17 Block,
18 ArgList, 18 ArgList,
19 Region,
19} 20}
20 21
21#[derive(Debug)] 22#[derive(Debug)]
@@ -29,6 +30,8 @@ pub(crate) fn folding_ranges(file: &SourceFile) -> Vec<Fold> {
29 let mut visited_comments = FxHashSet::default(); 30 let mut visited_comments = FxHashSet::default();
30 let mut visited_imports = FxHashSet::default(); 31 let mut visited_imports = FxHashSet::default();
31 let mut visited_mods = FxHashSet::default(); 32 let mut visited_mods = FxHashSet::default();
33 // regions can be nested, here is a LIFO buffer
34 let mut regions_starts: Vec<TextSize> = vec![];
32 35
33 for element in file.syntax().descendants_with_tokens() { 36 for element in file.syntax().descendants_with_tokens() {
34 // Fold items that span multiple lines 37 // Fold items that span multiple lines
@@ -48,10 +51,25 @@ pub(crate) fn folding_ranges(file: &SourceFile) -> Vec<Fold> {
48 // Fold groups of comments 51 // Fold groups of comments
49 if let Some(comment) = ast::Comment::cast(token) { 52 if let Some(comment) = ast::Comment::cast(token) {
50 if !visited_comments.contains(&comment) { 53 if !visited_comments.contains(&comment) {
51 if let Some(range) = 54 // regions are not real comments
52 contiguous_range_for_comment(comment, &mut visited_comments) 55 if comment.text().trim().starts_with("// region:") {
53 { 56 regions_starts.push(comment.syntax().text_range().start());
54 res.push(Fold { range, kind: FoldKind::Comment }) 57 } else if comment.text().trim().starts_with("// endregion") {
58 if let Some(region) = regions_starts.pop() {
59 res.push(Fold {
60 range: TextRange::new(
61 region,
62 comment.syntax().text_range().end(),
63 ),
64 kind: FoldKind::Region,
65 })
66 }
67 } else {
68 if let Some(range) =
69 contiguous_range_for_comment(comment, &mut visited_comments)
70 {
71 res.push(Fold { range, kind: FoldKind::Comment })
72 }
55 } 73 }
56 } 74 }
57 } 75 }
@@ -175,9 +193,16 @@ fn contiguous_range_for_comment(
175 } 193 }
176 if let Some(c) = ast::Comment::cast(token) { 194 if let Some(c) = ast::Comment::cast(token) {
177 if c.kind() == group_kind { 195 if c.kind() == group_kind {
178 visited.insert(c.clone()); 196 // regions are not real comments
179 last = c; 197 if c.text().trim().starts_with("// region:")
180 continue; 198 || c.text().trim().starts_with("// endregion")
199 {
200 break;
201 } else {
202 visited.insert(c.clone());
203 last = c;
204 continue;
205 }
181 } 206 }
182 } 207 }
183 // The comment group ends because either: 208 // The comment group ends because either:
@@ -224,6 +249,7 @@ mod tests {
224 FoldKind::Mods => "mods", 249 FoldKind::Mods => "mods",
225 FoldKind::Block => "block", 250 FoldKind::Block => "block",
226 FoldKind::ArgList => "arglist", 251 FoldKind::ArgList => "arglist",
252 FoldKind::Region => "region",
227 }; 253 };
228 assert_eq!(kind, &attr.unwrap()); 254 assert_eq!(kind, &attr.unwrap());
229 } 255 }
@@ -418,4 +444,17 @@ fn foo<fold arglist>(
418"#, 444"#,
419 ) 445 )
420 } 446 }
447
448 #[test]
449 fn fold_region() {
450 check(
451 r#"
452// 1. some normal comment
453<fold region>// region: test
454// 2. some normal comment
455calling_function(x,y);
456// endregion: test</fold>
457"#,
458 )
459 }
421} 460}
diff --git a/crates/ide/src/hover.rs b/crates/ide/src/hover.rs
index 9a605b09d..ea45086ce 100644
--- a/crates/ide/src/hover.rs
+++ b/crates/ide/src/hover.rs
@@ -1,3 +1,4 @@
1use either::Either;
1use hir::{ 2use hir::{
2 Adt, AsAssocItem, AssocItemContainer, FieldSource, GenericParam, HasAttrs, HasSource, 3 Adt, AsAssocItem, AssocItemContainer, FieldSource, GenericParam, HasAttrs, HasSource,
3 HirDisplay, Module, ModuleDef, ModuleSource, Semantics, 4 HirDisplay, Module, ModuleDef, ModuleSource, Semantics,
@@ -5,12 +6,12 @@ use hir::{
5use ide_db::{ 6use ide_db::{
6 base_db::SourceDatabase, 7 base_db::SourceDatabase,
7 defs::{Definition, NameClass, NameRefClass}, 8 defs::{Definition, NameClass, NameRefClass},
9 helpers::FamousDefs,
8 RootDatabase, 10 RootDatabase,
9}; 11};
10use itertools::Itertools; 12use itertools::Itertools;
11use stdx::format_to; 13use stdx::format_to;
12use syntax::{ast, match_ast, AstNode, SyntaxKind::*, SyntaxToken, TokenAtOffset, T}; 14use syntax::{ast, match_ast, AstNode, SyntaxKind::*, SyntaxToken, TokenAtOffset, T};
13use test_utils::mark;
14 15
15use crate::{ 16use crate::{
16 display::{macro_label, ShortLabel, TryToNav}, 17 display::{macro_label, ShortLabel, TryToNav},
@@ -107,16 +108,14 @@ pub(crate) fn hover(
107 } 108 }
108 }; 109 };
109 if let Some(definition) = definition { 110 if let Some(definition) = definition {
110 if let Some(markup) = hover_for_definition(db, definition) { 111 let famous_defs = match &definition {
111 let markup = markup.as_str(); 112 Definition::ModuleDef(ModuleDef::BuiltinType(_)) => {
112 let markup = if !markdown { 113 Some(FamousDefs(&sema, sema.scope(&node).krate()))
113 remove_markdown(markup) 114 }
114 } else if links_in_hover { 115 _ => None,
115 rewrite_links(db, markup, &definition) 116 };
116 } else { 117 if let Some(markup) = hover_for_definition(db, definition, famous_defs.as_ref()) {
117 remove_links(markup) 118 res.markup = process_markup(sema.db, definition, &markup, links_in_hover, markdown);
118 };
119 res.markup = Markup::from(markup);
120 if let Some(action) = show_implementations_action(db, definition) { 119 if let Some(action) = show_implementations_action(db, definition) {
121 res.actions.push(action); 120 res.actions.push(action);
122 } 121 }
@@ -138,6 +137,9 @@ pub(crate) fn hover(
138 // don't highlight the entire parent node on comment hover 137 // don't highlight the entire parent node on comment hover
139 return None; 138 return None;
140 } 139 }
140 if let res @ Some(_) = hover_for_keyword(&sema, links_in_hover, markdown, &token) {
141 return res;
142 }
141 143
142 let node = token 144 let node = token
143 .ancestors() 145 .ancestors()
@@ -191,8 +193,8 @@ fn runnable_action(
191 ModuleDef::Function(func) => { 193 ModuleDef::Function(func) => {
192 let src = func.source(sema.db)?; 194 let src = func.source(sema.db)?;
193 if src.file_id != file_id.into() { 195 if src.file_id != file_id.into() {
194 mark::hit!(hover_macro_generated_struct_fn_doc_comment); 196 cov_mark::hit!(hover_macro_generated_struct_fn_doc_comment);
195 mark::hit!(hover_macro_generated_struct_fn_doc_attr); 197 cov_mark::hit!(hover_macro_generated_struct_fn_doc_attr);
196 return None; 198 return None;
197 } 199 }
198 200
@@ -272,6 +274,24 @@ fn hover_markup(
272 } 274 }
273} 275}
274 276
277fn process_markup(
278 db: &RootDatabase,
279 def: Definition,
280 markup: &Markup,
281 links_in_hover: bool,
282 markdown: bool,
283) -> Markup {
284 let markup = markup.as_str();
285 let markup = if !markdown {
286 remove_markdown(markup)
287 } else if links_in_hover {
288 rewrite_links(db, markup, &def)
289 } else {
290 remove_links(markup)
291 };
292 Markup::from(markup)
293}
294
275fn definition_owner_name(db: &RootDatabase, def: &Definition) -> Option<String> { 295fn definition_owner_name(db: &RootDatabase, def: &Definition) -> Option<String> {
276 match def { 296 match def {
277 Definition::Field(f) => Some(f.parent_def(db).name(db)), 297 Definition::Field(f) => Some(f.parent_def(db).name(db)),
@@ -304,7 +324,11 @@ fn definition_mod_path(db: &RootDatabase, def: &Definition) -> Option<String> {
304 def.module(db).map(|module| render_path(db, module, definition_owner_name(db, def))) 324 def.module(db).map(|module| render_path(db, module, definition_owner_name(db, def)))
305} 325}
306 326
307fn hover_for_definition(db: &RootDatabase, def: Definition) -> Option<Markup> { 327fn hover_for_definition(
328 db: &RootDatabase,
329 def: Definition,
330 famous_defs: Option<&FamousDefs>,
331) -> Option<Markup> {
308 let mod_path = definition_mod_path(db, &def); 332 let mod_path = definition_mod_path(db, &def);
309 return match def { 333 return match def {
310 Definition::Macro(it) => { 334 Definition::Macro(it) => {
@@ -339,9 +363,11 @@ fn hover_for_definition(db: &RootDatabase, def: Definition) -> Option<Markup> {
339 ModuleDef::Static(it) => from_def_source(db, it, mod_path), 363 ModuleDef::Static(it) => from_def_source(db, it, mod_path),
340 ModuleDef::Trait(it) => from_def_source(db, it, mod_path), 364 ModuleDef::Trait(it) => from_def_source(db, it, mod_path),
341 ModuleDef::TypeAlias(it) => from_def_source(db, it, mod_path), 365 ModuleDef::TypeAlias(it) => from_def_source(db, it, mod_path),
342 ModuleDef::BuiltinType(it) => Some(Markup::fenced_block(&it.name())), 366 ModuleDef::BuiltinType(it) => famous_defs
367 .and_then(|fd| hover_for_builtin(fd, it))
368 .or_else(|| Some(Markup::fenced_block(&it.name()))),
343 }, 369 },
344 Definition::Local(it) => Some(Markup::fenced_block(&it.ty(db).display(db))), 370 Definition::Local(it) => hover_for_local(it, db),
345 Definition::SelfType(impl_def) => { 371 Definition::SelfType(impl_def) => {
346 impl_def.target_ty(db).as_adt().and_then(|adt| match adt { 372 impl_def.target_ty(db).as_adt().and_then(|adt| match adt {
347 Adt::Struct(it) => from_def_source(db, it, mod_path), 373 Adt::Struct(it) => from_def_source(db, it, mod_path),
@@ -380,11 +406,75 @@ fn hover_for_definition(db: &RootDatabase, def: Definition) -> Option<Markup> {
380 } 406 }
381} 407}
382 408
409fn hover_for_local(it: hir::Local, db: &RootDatabase) -> Option<Markup> {
410 let ty = it.ty(db);
411 let ty = ty.display(db);
412 let is_mut = if it.is_mut(db) { "mut " } else { "" };
413 let desc = match it.source(db).value {
414 Either::Left(ident) => {
415 let name = it.name(db).unwrap();
416 let let_kw = if ident
417 .syntax()
418 .parent()
419 .map_or(false, |p| p.kind() == LET_STMT || p.kind() == CONDITION)
420 {
421 "let "
422 } else {
423 ""
424 };
425 format!("{}{}{}: {}", let_kw, is_mut, name, ty)
426 }
427 Either::Right(_) => format!("{}self: {}", is_mut, ty),
428 };
429 hover_markup(None, Some(desc), None)
430}
431
432fn hover_for_keyword(
433 sema: &Semantics<RootDatabase>,
434 links_in_hover: bool,
435 markdown: bool,
436 token: &SyntaxToken,
437) -> Option<RangeInfo<HoverResult>> {
438 if !token.kind().is_keyword() {
439 return None;
440 }
441 let famous_defs = FamousDefs(&sema, sema.scope(&token.parent()).krate());
442 // std exposes {}_keyword modules with docstrings on the root to document keywords
443 let keyword_mod = format!("{}_keyword", token.text());
444 let doc_owner = find_std_module(&famous_defs, &keyword_mod)?;
445 let docs = doc_owner.attrs(sema.db).docs()?;
446 let markup = process_markup(
447 sema.db,
448 Definition::ModuleDef(doc_owner.into()),
449 &hover_markup(Some(docs.into()), Some(token.text().into()), None)?,
450 links_in_hover,
451 markdown,
452 );
453 Some(RangeInfo::new(token.text_range(), HoverResult { markup, actions: Default::default() }))
454}
455
456fn hover_for_builtin(famous_defs: &FamousDefs, builtin: hir::BuiltinType) -> Option<Markup> {
457 // std exposes prim_{} modules with docstrings on the root to document the builtins
458 let primitive_mod = format!("prim_{}", builtin.name());
459 let doc_owner = find_std_module(famous_defs, &primitive_mod)?;
460 let docs = doc_owner.attrs(famous_defs.0.db).docs()?;
461 hover_markup(Some(docs.into()), Some(builtin.name().to_string()), None)
462}
463
464fn find_std_module(famous_defs: &FamousDefs, name: &str) -> Option<hir::Module> {
465 let db = famous_defs.0.db;
466 let std_crate = famous_defs.std()?;
467 let std_root_module = std_crate.root_module(db);
468 std_root_module
469 .children(db)
470 .find(|module| module.name(db).map_or(false, |module| module.to_string() == name))
471}
472
383fn pick_best(tokens: TokenAtOffset<SyntaxToken>) -> Option<SyntaxToken> { 473fn pick_best(tokens: TokenAtOffset<SyntaxToken>) -> Option<SyntaxToken> {
384 return tokens.max_by_key(priority); 474 return tokens.max_by_key(priority);
385 fn priority(n: &SyntaxToken) -> usize { 475 fn priority(n: &SyntaxToken) -> usize {
386 match n.kind() { 476 match n.kind() {
387 IDENT | INT_NUMBER | LIFETIME_IDENT | T![self] => 3, 477 IDENT | INT_NUMBER | LIFETIME_IDENT | T![self] | T![super] | T![crate] => 3,
388 T!['('] | T![')'] => 2, 478 T!['('] | T![')'] => 2,
389 kind if kind.is_trivia() => 0, 479 kind if kind.is_trivia() => 0,
390 _ => 1, 480 _ => 1,
@@ -508,7 +598,7 @@ fn main() {
508 *iter* 598 *iter*
509 599
510 ```rust 600 ```rust
511 Iter<Scan<OtherStruct<OtherStruct<i32>>, |&mut u32, &u32, &mut u32| -> Option<u32>, u32>> 601 let mut iter: Iter<Scan<OtherStruct<OtherStruct<i32>>, |&mut u32, &u32, &mut u32| -> Option<u32>, u32>>
512 ``` 602 ```
513 "#]], 603 "#]],
514 ); 604 );
@@ -732,7 +822,7 @@ fn main() {
732 ``` 822 ```
733 823
734 ```rust 824 ```rust
735 const foo: u32 = 123 825 const foo: u32
736 ``` 826 ```
737 "#]], 827 "#]],
738 ); 828 );
@@ -765,7 +855,7 @@ fn main() {
765 *zz* 855 *zz*
766 856
767 ```rust 857 ```rust
768 Test<i32, u8> 858 let zz: Test<i32, u8>
769 ``` 859 ```
770 "#]], 860 "#]],
771 ); 861 );
@@ -804,7 +894,7 @@ fn main() { let b$0ar = Some(12); }
804 *bar* 894 *bar*
805 895
806 ```rust 896 ```rust
807 Option<i32> 897 let bar: Option<i32>
808 ``` 898 ```
809 "#]], 899 "#]],
810 ); 900 );
@@ -872,7 +962,7 @@ fn main() {
872 *foo* 962 *foo*
873 963
874 ```rust 964 ```rust
875 i32 965 foo: i32
876 ``` 966 ```
877 "#]], 967 "#]],
878 ) 968 )
@@ -886,7 +976,7 @@ fn main() {
886 *foo* 976 *foo*
887 977
888 ```rust 978 ```rust
889 i32 979 foo: i32
890 ``` 980 ```
891 "#]], 981 "#]],
892 ) 982 )
@@ -900,7 +990,7 @@ fn main() {
900 *foo* 990 *foo*
901 991
902 ```rust 992 ```rust
903 i32 993 foo: i32
904 ``` 994 ```
905 "#]], 995 "#]],
906 ) 996 )
@@ -914,7 +1004,7 @@ fn main() {
914 *foo* 1004 *foo*
915 1005
916 ```rust 1006 ```rust
917 i32 1007 foo: i32
918 ``` 1008 ```
919 "#]], 1009 "#]],
920 ) 1010 )
@@ -934,7 +1024,7 @@ fn main() {
934 *_x* 1024 *_x*
935 1025
936 ```rust 1026 ```rust
937 impl Deref<Target = u8> + DerefMut<Target = u8> 1027 _x: impl Deref<Target = u8> + DerefMut<Target = u8>
938 ``` 1028 ```
939 "#]], 1029 "#]],
940 ) 1030 )
@@ -956,7 +1046,7 @@ fn main() { let foo_$0test = Thing::new(); }
956 *foo_test* 1046 *foo_test*
957 1047
958 ```rust 1048 ```rust
959 Thing 1049 let foo_test: Thing
960 ``` 1050 ```
961 "#]], 1051 "#]],
962 ) 1052 )
@@ -1015,7 +1105,7 @@ fn main() {
1015 ``` 1105 ```
1016 1106
1017 ```rust 1107 ```rust
1018 const C: u32 = 1 1108 const C: u32
1019 ``` 1109 ```
1020 "#]], 1110 "#]],
1021 ) 1111 )
@@ -1116,7 +1206,7 @@ fn y() {
1116 *x* 1206 *x*
1117 1207
1118 ```rust 1208 ```rust
1119 i32 1209 let x: i32
1120 ``` 1210 ```
1121 "#]], 1211 "#]],
1122 ) 1212 )
@@ -1193,7 +1283,7 @@ fn foo(bar:u32) { let a = id!(ba$0r); }
1193 *bar* 1283 *bar*
1194 1284
1195 ```rust 1285 ```rust
1196 u32 1286 bar: u32
1197 ``` 1287 ```
1198 "#]], 1288 "#]],
1199 ); 1289 );
@@ -1211,7 +1301,7 @@ fn foo(bar:u32) { let a = id!(ba$0r); }
1211 *bar* 1301 *bar*
1212 1302
1213 ```rust 1303 ```rust
1214 u32 1304 bar: u32
1215 ``` 1305 ```
1216 "#]], 1306 "#]],
1217 ); 1307 );
@@ -2034,7 +2124,7 @@ pub fn fo$0o() {}
2034 2124
2035 #[test] 2125 #[test]
2036 fn test_hover_macro_generated_struct_fn_doc_comment() { 2126 fn test_hover_macro_generated_struct_fn_doc_comment() {
2037 mark::check!(hover_macro_generated_struct_fn_doc_comment); 2127 cov_mark::check!(hover_macro_generated_struct_fn_doc_comment);
2038 2128
2039 check( 2129 check(
2040 r#" 2130 r#"
@@ -2072,7 +2162,7 @@ fn foo() { let bar = Bar; bar.fo$0o(); }
2072 2162
2073 #[test] 2163 #[test]
2074 fn test_hover_macro_generated_struct_fn_doc_attr() { 2164 fn test_hover_macro_generated_struct_fn_doc_attr() {
2075 mark::check!(hover_macro_generated_struct_fn_doc_attr); 2165 cov_mark::check!(hover_macro_generated_struct_fn_doc_attr);
2076 2166
2077 check( 2167 check(
2078 r#" 2168 r#"
@@ -3236,7 +3326,7 @@ fn main() {
3236 *f* 3326 *f*
3237 3327
3238 ```rust 3328 ```rust
3239 &i32 3329 f: &i32
3240 ``` 3330 ```
3241 "#]], 3331 "#]],
3242 ); 3332 );
@@ -3255,7 +3345,7 @@ impl Foo {
3255 *self* 3345 *self*
3256 3346
3257 ```rust 3347 ```rust
3258 &Foo 3348 self: &Foo
3259 ``` 3349 ```
3260 "#]], 3350 "#]],
3261 ); 3351 );
@@ -3275,7 +3365,7 @@ impl Foo {
3275 *self* 3365 *self*
3276 3366
3277 ```rust 3367 ```rust
3278 Arc<Foo> 3368 self: Arc<Foo>
3279 ``` 3369 ```
3280 "#]], 3370 "#]],
3281 ); 3371 );
@@ -3471,7 +3561,7 @@ fn foo() {
3471 ``` 3561 ```
3472 3562
3473 ```rust 3563 ```rust
3474 const FOO: usize = 3 3564 const FOO: usize
3475 ``` 3565 ```
3476 3566
3477 --- 3567 ---
@@ -3496,4 +3586,75 @@ mod foo$0;
3496 "#]], 3586 "#]],
3497 ); 3587 );
3498 } 3588 }
3589
3590 #[test]
3591 fn hover_self_in_use() {
3592 check(
3593 r#"
3594//! This should not appear
3595mod foo {
3596 /// But this should appear
3597 pub mod bar {}
3598}
3599use foo::bar::{self$0};
3600"#,
3601 expect![[r#"
3602 *self*
3603
3604 ```rust
3605 test::foo
3606 ```
3607
3608 ```rust
3609 pub mod bar
3610 ```
3611
3612 ---
3613
3614 But this should appear
3615 "#]],
3616 )
3617 }
3618
3619 #[test]
3620 fn hover_keyword() {
3621 let ra_fixture = r#"//- /main.rs crate:main deps:std
3622fn f() { retur$0n; }"#;
3623 let fixture = format!("{}\n{}", ra_fixture, FamousDefs::FIXTURE);
3624 check(
3625 &fixture,
3626 expect![[r#"
3627 *return*
3628
3629 ```rust
3630 return
3631 ```
3632
3633 ---
3634
3635 Docs for return_keyword
3636 "#]],
3637 );
3638 }
3639
3640 #[test]
3641 fn hover_builtin() {
3642 let ra_fixture = r#"//- /main.rs crate:main deps:std
3643cosnt _: &str$0 = ""; }"#;
3644 let fixture = format!("{}\n{}", ra_fixture, FamousDefs::FIXTURE);
3645 check(
3646 &fixture,
3647 expect![[r#"
3648 *str*
3649
3650 ```rust
3651 str
3652 ```
3653
3654 ---
3655
3656 Docs for prim_str
3657 "#]],
3658 );
3659 }
3499} 3660}
diff --git a/crates/ide/src/join_lines.rs b/crates/ide/src/join_lines.rs
index 2c077ed1f..20a920ddb 100644
--- a/crates/ide/src/join_lines.rs
+++ b/crates/ide/src/join_lines.rs
@@ -7,6 +7,7 @@ use syntax::{
7 SyntaxKind::{self, USE_TREE, WHITESPACE}, 7 SyntaxKind::{self, USE_TREE, WHITESPACE},
8 SyntaxNode, SyntaxToken, TextRange, TextSize, T, 8 SyntaxNode, SyntaxToken, TextRange, TextSize, T,
9}; 9};
10
10use text_edit::{TextEdit, TextEditBuilder}; 11use text_edit::{TextEdit, TextEditBuilder};
11 12
12// Feature: Join Lines 13// Feature: Join Lines
@@ -44,9 +45,9 @@ pub(crate) fn join_lines(file: &SourceFile, range: TextRange) -> TextEdit {
44 let text = token.text(); 45 let text = token.text();
45 for (pos, _) in text[range].bytes().enumerate().filter(|&(_, b)| b == b'\n') { 46 for (pos, _) in text[range].bytes().enumerate().filter(|&(_, b)| b == b'\n') {
46 let pos: TextSize = (pos as u32).into(); 47 let pos: TextSize = (pos as u32).into();
47 let off = token.text_range().start() + range.start() + pos; 48 let offset = token.text_range().start() + range.start() + pos;
48 if !edit.invalidates_offset(off) { 49 if !edit.invalidates_offset(offset) {
49 remove_newline(&mut edit, &token, off); 50 remove_newline(&mut edit, &token, offset);
50 } 51 }
51 } 52 }
52 } 53 }
@@ -56,14 +57,25 @@ pub(crate) fn join_lines(file: &SourceFile, range: TextRange) -> TextEdit {
56 57
57fn remove_newline(edit: &mut TextEditBuilder, token: &SyntaxToken, offset: TextSize) { 58fn remove_newline(edit: &mut TextEditBuilder, token: &SyntaxToken, offset: TextSize) {
58 if token.kind() != WHITESPACE || token.text().bytes().filter(|&b| b == b'\n').count() != 1 { 59 if token.kind() != WHITESPACE || token.text().bytes().filter(|&b| b == b'\n').count() != 1 {
59 // The node is either the first or the last in the file 60 let mut string_open_quote = false;
60 let suff = &token.text()[TextRange::new( 61 if let Some(string) = ast::String::cast(token.clone()) {
61 offset - token.text_range().start() + TextSize::of('\n'), 62 if let Some(range) = string.open_quote_text_range() {
62 TextSize::of(token.text()), 63 cov_mark::hit!(join_string_literal);
63 )]; 64 string_open_quote = range.end() == offset;
64 let spaces = suff.bytes().take_while(|&b| b == b' ').count(); 65 }
65 66 }
66 edit.replace(TextRange::at(offset, ((spaces + 1) as u32).into()), " ".to_string()); 67
68 let n_spaces_after_line_break = {
69 let suff = &token.text()[TextRange::new(
70 offset - token.text_range().start() + TextSize::of('\n'),
71 TextSize::of(token.text()),
72 )];
73 suff.bytes().take_while(|&b| b == b' ').count()
74 };
75
76 let range = TextRange::at(offset, ((n_spaces_after_line_break + 1) as u32).into());
77 let replace_with = if string_open_quote { "" } else { " " };
78 edit.replace(range, replace_with.to_string());
67 return; 79 return;
68 } 80 }
69 81
@@ -771,4 +783,42 @@ fn foo() {
771 ", 783 ",
772 ); 784 );
773 } 785 }
786
787 #[test]
788 fn join_string_literal() {
789 cov_mark::check!(join_string_literal);
790 check_join_lines(
791 r#"
792fn main() {
793 $0"
794hello
795";
796}
797"#,
798 r#"
799fn main() {
800 $0"hello
801";
802}
803"#,
804 );
805
806 check_join_lines(
807 r#"
808fn main() {
809 "
810$0hello
811world
812";
813}
814"#,
815 r#"
816fn main() {
817 "
818$0hello world
819";
820}
821"#,
822 );
823 }
774} 824}
diff --git a/crates/ide/src/lib.rs b/crates/ide/src/lib.rs
index baa80cf43..a8b169e87 100644
--- a/crates/ide/src/lib.rs
+++ b/crates/ide/src/lib.rs
@@ -41,6 +41,7 @@ mod parent_module;
41mod references; 41mod references;
42mod fn_references; 42mod fn_references;
43mod runnables; 43mod runnables;
44mod ssr;
44mod status; 45mod status;
45mod syntax_highlighting; 46mod syntax_highlighting;
46mod syntax_tree; 47mod syntax_tree;
@@ -51,6 +52,7 @@ mod doc_links;
51use std::sync::Arc; 52use std::sync::Arc;
52 53
53use cfg::CfgOptions; 54use cfg::CfgOptions;
55
54use ide_db::base_db::{ 56use ide_db::base_db::{
55 salsa::{self, ParallelDatabase}, 57 salsa::{self, ParallelDatabase},
56 CheckCanceled, Env, FileLoader, FileSet, SourceDatabase, VfsPath, 58 CheckCanceled, Env, FileLoader, FileSet, SourceDatabase, VfsPath,
@@ -85,7 +87,7 @@ pub use crate::{
85pub use hir::{Documentation, Semantics}; 87pub use hir::{Documentation, Semantics};
86pub use ide_assists::{Assist, AssistConfig, AssistId, AssistKind}; 88pub use ide_assists::{Assist, AssistConfig, AssistId, AssistKind};
87pub use ide_completion::{ 89pub use ide_completion::{
88 CompletionConfig, CompletionItem, CompletionItemKind, CompletionScore, ImportEdit, 90 CompletionConfig, CompletionItem, CompletionItemKind, CompletionRelevance, ImportEdit,
89 InsertTextFormat, 91 InsertTextFormat,
90}; 92};
91pub use ide_db::{ 93pub use ide_db::{
@@ -487,7 +489,6 @@ impl Analysis {
487 position: FilePosition, 489 position: FilePosition,
488 full_import_path: &str, 490 full_import_path: &str,
489 imported_name: String, 491 imported_name: String,
490 import_for_trait_assoc_item: bool,
491 ) -> Cancelable<Vec<TextEdit>> { 492 ) -> Cancelable<Vec<TextEdit>> {
492 Ok(self 493 Ok(self
493 .with_db(|db| { 494 .with_db(|db| {
@@ -497,7 +498,6 @@ impl Analysis {
497 position, 498 position,
498 full_import_path, 499 full_import_path,
499 imported_name, 500 imported_name,
500 import_for_trait_assoc_item,
501 ) 501 )
502 })? 502 })?
503 .unwrap_or_default()) 503 .unwrap_or_default())
@@ -513,7 +513,11 @@ impl Analysis {
513 resolve: bool, 513 resolve: bool,
514 frange: FileRange, 514 frange: FileRange,
515 ) -> Cancelable<Vec<Assist>> { 515 ) -> Cancelable<Vec<Assist>> {
516 self.with_db(|db| Assist::get(db, config, resolve, frange)) 516 self.with_db(|db| {
517 let mut acc = Assist::get(db, config, resolve, frange);
518 ssr::add_ssr_assist(db, &mut acc, resolve, frange);
519 acc
520 })
517 } 521 }
518 522
519 /// Computes the set of diagnostics for the given file. 523 /// Computes the set of diagnostics for the given file.
diff --git a/crates/ide/src/matching_brace.rs b/crates/ide/src/matching_brace.rs
index 1bfa1439d..000c412d9 100644
--- a/crates/ide/src/matching_brace.rs
+++ b/crates/ide/src/matching_brace.rs
@@ -2,7 +2,6 @@ use syntax::{
2 ast::{self, AstNode}, 2 ast::{self, AstNode},
3 SourceFile, SyntaxKind, TextSize, T, 3 SourceFile, SyntaxKind, TextSize, T,
4}; 4};
5use test_utils::mark;
6 5
7// Feature: Matching Brace 6// Feature: Matching Brace
8// 7//
@@ -28,7 +27,7 @@ pub(crate) fn matching_brace(file: &SourceFile, offset: TextSize) -> Option<Text
28 .next()?; 27 .next()?;
29 let parent = brace_token.parent(); 28 let parent = brace_token.parent();
30 if brace_token.kind() == T![|] && !ast::ParamList::can_cast(parent.kind()) { 29 if brace_token.kind() == T![|] && !ast::ParamList::can_cast(parent.kind()) {
31 mark::hit!(pipes_not_braces); 30 cov_mark::hit!(pipes_not_braces);
32 return None; 31 return None;
33 } 32 }
34 let matching_kind = BRACES[brace_idx ^ 1]; 33 let matching_kind = BRACES[brace_idx ^ 1];
@@ -63,7 +62,7 @@ mod tests {
63 do_check("fn main() { $0|x: i32| x * 2;}", "fn main() { |x: i32$0| x * 2;}"); 62 do_check("fn main() { $0|x: i32| x * 2;}", "fn main() { |x: i32$0| x * 2;}");
64 63
65 { 64 {
66 mark::check!(pipes_not_braces); 65 cov_mark::check!(pipes_not_braces);
67 do_check( 66 do_check(
68 "fn main() { match 92 { 1 | 2 |$0 3 => 92 } }", 67 "fn main() { match 92 { 1 | 2 |$0 3 => 92 } }",
69 "fn main() { match 92 { 1 | 2 |$0 3 => 92 } }", 68 "fn main() { match 92 { 1 | 2 |$0 3 => 92 } }",
diff --git a/crates/ide/src/parent_module.rs b/crates/ide/src/parent_module.rs
index ddbaf22b7..03d71b380 100644
--- a/crates/ide/src/parent_module.rs
+++ b/crates/ide/src/parent_module.rs
@@ -5,7 +5,6 @@ use syntax::{
5 algo::find_node_at_offset, 5 algo::find_node_at_offset,
6 ast::{self, AstNode}, 6 ast::{self, AstNode},
7}; 7};
8use test_utils::mark;
9 8
10use crate::NavigationTarget; 9use crate::NavigationTarget;
11 10
@@ -33,7 +32,7 @@ pub(crate) fn parent_module(db: &RootDatabase, position: FilePosition) -> Vec<Na
33 .item_list() 32 .item_list()
34 .map_or(false, |it| it.syntax().text_range().contains_inclusive(position.offset)) 33 .map_or(false, |it| it.syntax().text_range().contains_inclusive(position.offset))
35 { 34 {
36 mark::hit!(test_resolve_parent_module_on_module_decl); 35 cov_mark::hit!(test_resolve_parent_module_on_module_decl);
37 module = m.syntax().ancestors().skip(1).find_map(ast::Module::cast); 36 module = m.syntax().ancestors().skip(1).find_map(ast::Module::cast);
38 } 37 }
39 } 38 }
@@ -64,7 +63,6 @@ pub(crate) fn crate_for(db: &RootDatabase, file_id: FileId) -> Vec<CrateId> {
64#[cfg(test)] 63#[cfg(test)]
65mod tests { 64mod tests {
66 use ide_db::base_db::FileRange; 65 use ide_db::base_db::FileRange;
67 use test_utils::mark;
68 66
69 use crate::fixture; 67 use crate::fixture;
70 68
@@ -92,7 +90,7 @@ $0// empty
92 90
93 #[test] 91 #[test]
94 fn test_resolve_parent_module_on_module_decl() { 92 fn test_resolve_parent_module_on_module_decl() {
95 mark::check!(test_resolve_parent_module_on_module_decl); 93 cov_mark::check!(test_resolve_parent_module_on_module_decl);
96 check( 94 check(
97 r#" 95 r#"
98//- /lib.rs 96//- /lib.rs
diff --git a/crates/ide/src/references.rs b/crates/ide/src/references.rs
index 5d0449e56..fef70533d 100644
--- a/crates/ide/src/references.rs
+++ b/crates/ide/src/references.rs
@@ -39,6 +39,15 @@ pub struct Declaration {
39 pub access: Option<ReferenceAccess>, 39 pub access: Option<ReferenceAccess>,
40} 40}
41 41
42// Feature: Find All References
43//
44// Shows all references of the item at the cursor location
45//
46// |===
47// | Editor | Shortcut
48//
49// | VS Code | kbd:[Shift+Alt+F12]
50// |===
42pub(crate) fn find_all_refs( 51pub(crate) fn find_all_refs(
43 sema: &Semantics<RootDatabase>, 52 sema: &Semantics<RootDatabase>,
44 position: FilePosition, 53 position: FilePosition,
diff --git a/crates/ide/src/references/rename.rs b/crates/ide/src/references/rename.rs
index 175ddd759..1e378279d 100644
--- a/crates/ide/src/references/rename.rs
+++ b/crates/ide/src/references/rename.rs
@@ -14,14 +14,14 @@ use syntax::{
14 ast::{self, NameOwner}, 14 ast::{self, NameOwner},
15 lex_single_syntax_kind, AstNode, SyntaxKind, SyntaxNode, T, 15 lex_single_syntax_kind, AstNode, SyntaxKind, SyntaxNode, T,
16}; 16};
17use test_utils::mark; 17
18use text_edit::TextEdit; 18use text_edit::TextEdit;
19 19
20use crate::{display::TryToNav, FilePosition, FileSystemEdit, RangeInfo, SourceChange, TextRange}; 20use crate::{display::TryToNav, FilePosition, FileSystemEdit, RangeInfo, SourceChange, TextRange};
21 21
22type RenameResult<T> = Result<T, RenameError>; 22type RenameResult<T> = Result<T, RenameError>;
23#[derive(Debug)] 23#[derive(Debug)]
24pub struct RenameError(pub(crate) String); 24pub struct RenameError(String);
25 25
26impl fmt::Display for RenameError { 26impl fmt::Display for RenameError {
27 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 27 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
@@ -47,18 +47,26 @@ pub(crate) fn prepare_rename(
47 let sema = Semantics::new(db); 47 let sema = Semantics::new(db);
48 let source_file = sema.parse(position.file_id); 48 let source_file = sema.parse(position.file_id);
49 let syntax = source_file.syntax(); 49 let syntax = source_file.syntax();
50 let range = match &sema 50 let name_like = sema
51 .find_node_at_offset_with_descend(&syntax, position.offset) 51 .find_node_at_offset_with_descend(&syntax, position.offset)
52 .ok_or_else(|| format_err!("No references found at position"))? 52 .ok_or_else(|| format_err!("No references found at position"))?;
53 { 53 let node = match &name_like {
54 ast::NameLike::Name(it) => it.syntax(), 54 ast::NameLike::Name(it) => it.syntax(),
55 ast::NameLike::NameRef(it) => it.syntax(), 55 ast::NameLike::NameRef(it) => it.syntax(),
56 ast::NameLike::Lifetime(it) => it.syntax(), 56 ast::NameLike::Lifetime(it) => it.syntax(),
57 } 57 };
58 .text_range(); 58 Ok(RangeInfo::new(sema.original_range(node).range, ()))
59 Ok(RangeInfo::new(range, ())) 59}
60} 60
61 61// Feature: Rename
62//
63// Renames the item below the cursor and all of its references
64//
65// |===
66// | Editor | Shortcut
67//
68// | VS Code | kbd:[F2]
69// |===
62pub(crate) fn rename( 70pub(crate) fn rename(
63 db: &RootDatabase, 71 db: &RootDatabase,
64 position: FilePosition, 72 position: FilePosition,
@@ -79,10 +87,13 @@ pub(crate) fn rename_with_semantics(
79 let def = find_definition(sema, syntax, position)?; 87 let def = find_definition(sema, syntax, position)?;
80 match def { 88 match def {
81 Definition::ModuleDef(ModuleDef::Module(module)) => rename_mod(&sema, module, new_name), 89 Definition::ModuleDef(ModuleDef::Module(module)) => rename_mod(&sema, module, new_name),
90 Definition::SelfType(_) => bail!("Cannot rename `Self`"),
91 Definition::ModuleDef(ModuleDef::BuiltinType(_)) => bail!("Cannot rename builtin type"),
82 def => rename_reference(sema, def, new_name), 92 def => rename_reference(sema, def, new_name),
83 } 93 }
84} 94}
85 95
96/// Called by the client when it is about to rename a file.
86pub(crate) fn will_rename_file( 97pub(crate) fn will_rename_file(
87 db: &RootDatabase, 98 db: &RootDatabase,
88 file_id: FileId, 99 file_id: FileId,
@@ -113,7 +124,7 @@ fn check_identifier(new_name: &str) -> RenameResult<IdentifierKind> {
113 Ok(IdentifierKind::Lifetime) 124 Ok(IdentifierKind::Lifetime)
114 } 125 }
115 (SyntaxKind::LIFETIME_IDENT, _) => { 126 (SyntaxKind::LIFETIME_IDENT, _) => {
116 bail!("Invalid name `{0}`: Cannot rename lifetime to {0}", new_name) 127 bail!("Invalid name `{}`: not a lifetime identifier", new_name)
117 } 128 }
118 (_, Some(syntax_error)) => bail!("Invalid name `{}`: {}", new_name, syntax_error), 129 (_, Some(syntax_error)) => bail!("Invalid name `{}`: {}", new_name, syntax_error),
119 (_, None) => bail!("Invalid name `{}`: not an identifier", new_name), 130 (_, None) => bail!("Invalid name `{}`: not an identifier", new_name),
@@ -153,119 +164,6 @@ fn find_definition(
153 .ok_or_else(|| format_err!("No references found at position")) 164 .ok_or_else(|| format_err!("No references found at position"))
154} 165}
155 166
156fn source_edit_from_references(
157 _sema: &Semantics<RootDatabase>,
158 file_id: FileId,
159 references: &[FileReference],
160 def: Definition,
161 new_name: &str,
162) -> (FileId, TextEdit) {
163 let mut edit = TextEdit::builder();
164 for reference in references {
165 let (range, replacement) = match &reference.name {
166 // if the ranges differ then the node is inside a macro call, we can't really attempt
167 // to make special rewrites like shorthand syntax and such, so just rename the node in
168 // the macro input
169 ast::NameLike::NameRef(name_ref)
170 if name_ref.syntax().text_range() == reference.range =>
171 {
172 source_edit_from_name_ref(name_ref, new_name, def)
173 }
174 ast::NameLike::Name(name) if name.syntax().text_range() == reference.range => {
175 source_edit_from_name(name, new_name)
176 }
177 _ => None,
178 }
179 .unwrap_or_else(|| (reference.range, new_name.to_string()));
180 edit.replace(range, replacement);
181 }
182 (file_id, edit.finish())
183}
184
185fn source_edit_from_name(name: &ast::Name, new_name: &str) -> Option<(TextRange, String)> {
186 if let Some(_) = ast::RecordPatField::for_field_name(name) {
187 if let Some(ident_pat) = name.syntax().parent().and_then(ast::IdentPat::cast) {
188 return Some((
189 TextRange::empty(ident_pat.syntax().text_range().start()),
190 format!("{}: ", new_name),
191 ));
192 }
193 }
194 None
195}
196
197fn source_edit_from_name_ref(
198 name_ref: &ast::NameRef,
199 new_name: &str,
200 def: Definition,
201) -> Option<(TextRange, String)> {
202 if let Some(record_field) = ast::RecordExprField::for_name_ref(name_ref) {
203 let rcf_name_ref = record_field.name_ref();
204 let rcf_expr = record_field.expr();
205 match (rcf_name_ref, rcf_expr.and_then(|it| it.name_ref())) {
206 // field: init-expr, check if we can use a field init shorthand
207 (Some(field_name), Some(init)) => {
208 if field_name == *name_ref {
209 if init.text() == new_name {
210 mark::hit!(test_rename_field_put_init_shorthand);
211 // same names, we can use a shorthand here instead.
212 // we do not want to erase attributes hence this range start
213 let s = field_name.syntax().text_range().start();
214 let e = record_field.syntax().text_range().end();
215 return Some((TextRange::new(s, e), new_name.to_owned()));
216 }
217 } else if init == *name_ref {
218 if field_name.text() == new_name {
219 mark::hit!(test_rename_local_put_init_shorthand);
220 // same names, we can use a shorthand here instead.
221 // we do not want to erase attributes hence this range start
222 let s = field_name.syntax().text_range().start();
223 let e = record_field.syntax().text_range().end();
224 return Some((TextRange::new(s, e), new_name.to_owned()));
225 }
226 }
227 None
228 }
229 // init shorthand
230 // FIXME: instead of splitting the shorthand, recursively trigger a rename of the
231 // other name https://github.com/rust-analyzer/rust-analyzer/issues/6547
232 (None, Some(_)) if matches!(def, Definition::Field(_)) => {
233 mark::hit!(test_rename_field_in_field_shorthand);
234 let s = name_ref.syntax().text_range().start();
235 Some((TextRange::empty(s), format!("{}: ", new_name)))
236 }
237 (None, Some(_)) if matches!(def, Definition::Local(_)) => {
238 mark::hit!(test_rename_local_in_field_shorthand);
239 let s = name_ref.syntax().text_range().end();
240 Some((TextRange::empty(s), format!(": {}", new_name)))
241 }
242 _ => None,
243 }
244 } else if let Some(record_field) = ast::RecordPatField::for_field_name_ref(name_ref) {
245 let rcf_name_ref = record_field.name_ref();
246 let rcf_pat = record_field.pat();
247 match (rcf_name_ref, rcf_pat) {
248 // field: rename
249 (Some(field_name), Some(ast::Pat::IdentPat(pat))) if field_name == *name_ref => {
250 // field name is being renamed
251 if pat.name().map_or(false, |it| it.text() == new_name) {
252 mark::hit!(test_rename_field_put_init_shorthand_pat);
253 // same names, we can use a shorthand here instead/
254 // we do not want to erase attributes hence this range start
255 let s = field_name.syntax().text_range().start();
256 let e = record_field.syntax().text_range().end();
257 Some((TextRange::new(s, e), pat.to_string()))
258 } else {
259 None
260 }
261 }
262 _ => None,
263 }
264 } else {
265 None
266 }
267}
268
269fn rename_mod( 167fn rename_mod(
270 sema: &Semantics<RootDatabase>, 168 sema: &Semantics<RootDatabase>,
271 module: Module, 169 module: Module,
@@ -299,18 +197,77 @@ fn rename_mod(
299 TextEdit::replace(name.syntax().text_range(), new_name.to_string()), 197 TextEdit::replace(name.syntax().text_range(), new_name.to_string()),
300 ), 198 ),
301 _ => unreachable!(), 199 _ => unreachable!(),
302 }; 200 }
303 } 201 }
304 let def = Definition::ModuleDef(ModuleDef::Module(module)); 202 let def = Definition::ModuleDef(ModuleDef::Module(module));
305 let usages = def.usages(sema).all(); 203 let usages = def.usages(sema).all();
306 let ref_edits = usages.iter().map(|(&file_id, references)| { 204 let ref_edits = usages.iter().map(|(&file_id, references)| {
307 source_edit_from_references(sema, file_id, references, def, new_name) 205 (file_id, source_edit_from_references(references, def, new_name))
308 }); 206 });
309 source_change.extend(ref_edits); 207 source_change.extend(ref_edits);
310 208
311 Ok(source_change) 209 Ok(source_change)
312} 210}
313 211
212fn rename_reference(
213 sema: &Semantics<RootDatabase>,
214 def: Definition,
215 new_name: &str,
216) -> RenameResult<SourceChange> {
217 let ident_kind = check_identifier(new_name)?;
218
219 let def_is_lbl_or_lt = matches!(
220 def,
221 Definition::GenericParam(hir::GenericParam::LifetimeParam(_)) | Definition::Label(_)
222 );
223 match (ident_kind, def) {
224 (IdentifierKind::ToSelf, _)
225 | (IdentifierKind::Underscore, _)
226 | (IdentifierKind::Ident, _)
227 if def_is_lbl_or_lt =>
228 {
229 cov_mark::hit!(rename_not_a_lifetime_ident_ref);
230 bail!("Invalid name `{}`: not a lifetime identifier", new_name)
231 }
232 (IdentifierKind::Lifetime, _) if def_is_lbl_or_lt => cov_mark::hit!(rename_lifetime),
233 (IdentifierKind::Lifetime, _) => {
234 cov_mark::hit!(rename_not_an_ident_ref);
235 bail!("Invalid name `{}`: not an identifier", new_name)
236 }
237 (IdentifierKind::ToSelf, Definition::Local(local)) if local.is_self(sema.db) => {
238 // no-op
239 cov_mark::hit!(rename_self_to_self);
240 return Ok(SourceChange::default());
241 }
242 (ident_kind, Definition::Local(local)) if local.is_self(sema.db) => {
243 cov_mark::hit!(rename_self_to_param);
244 return rename_self_to_param(sema, local, new_name, ident_kind);
245 }
246 (IdentifierKind::ToSelf, Definition::Local(local)) => {
247 cov_mark::hit!(rename_to_self);
248 return rename_to_self(sema, local);
249 }
250 (IdentifierKind::ToSelf, _) => bail!("Invalid name `{}`: not an identifier", new_name),
251 (IdentifierKind::Ident, _) | (IdentifierKind::Underscore, _) => {
252 cov_mark::hit!(rename_ident)
253 }
254 }
255
256 let usages = def.usages(sema).all();
257 if !usages.is_empty() && ident_kind == IdentifierKind::Underscore {
258 cov_mark::hit!(rename_underscore_multiple);
259 bail!("Cannot rename reference to `_` as it is being referenced multiple times");
260 }
261 let mut source_change = SourceChange::default();
262 source_change.extend(usages.iter().map(|(&file_id, references)| {
263 (file_id, source_edit_from_references(&references, def, new_name))
264 }));
265
266 let (file_id, edit) = source_edit_from_def(sema, def, new_name)?;
267 source_change.insert_source_edit(file_id, edit);
268 Ok(source_change)
269}
270
314fn rename_to_self(sema: &Semantics<RootDatabase>, local: hir::Local) -> RenameResult<SourceChange> { 271fn rename_to_self(sema: &Semantics<RootDatabase>, local: hir::Local) -> RenameResult<SourceChange> {
315 if never!(local.is_self(sema.db)) { 272 if never!(local.is_self(sema.db)) {
316 bail!("rename_to_self invoked on self"); 273 bail!("rename_to_self invoked on self");
@@ -375,7 +332,7 @@ fn rename_to_self(sema: &Semantics<RootDatabase>, local: hir::Local) -> RenameRe
375 let usages = def.usages(sema).all(); 332 let usages = def.usages(sema).all();
376 let mut source_change = SourceChange::default(); 333 let mut source_change = SourceChange::default();
377 source_change.extend(usages.iter().map(|(&file_id, references)| { 334 source_change.extend(usages.iter().map(|(&file_id, references)| {
378 source_edit_from_references(sema, file_id, references, def, "self") 335 (file_id, source_edit_from_references(references, def, "self"))
379 })); 336 }));
380 source_change.insert_source_edit( 337 source_change.insert_source_edit(
381 file_id.original_file(sema.db), 338 file_id.original_file(sema.db),
@@ -385,29 +342,6 @@ fn rename_to_self(sema: &Semantics<RootDatabase>, local: hir::Local) -> RenameRe
385 Ok(source_change) 342 Ok(source_change)
386} 343}
387 344
388fn text_edit_from_self_param(self_param: &ast::SelfParam, new_name: &str) -> Option<TextEdit> {
389 fn target_type_name(impl_def: &ast::Impl) -> Option<String> {
390 if let Some(ast::Type::PathType(p)) = impl_def.self_ty() {
391 return Some(p.path()?.segment()?.name_ref()?.text().to_string());
392 }
393 None
394 }
395
396 let impl_def = self_param.syntax().ancestors().find_map(|it| ast::Impl::cast(it))?;
397 let type_name = target_type_name(&impl_def)?;
398
399 let mut replacement_text = String::from(new_name);
400 replacement_text.push_str(": ");
401 match (self_param.amp_token(), self_param.mut_token()) {
402 (None, None) => (),
403 (Some(_), None) => replacement_text.push('&'),
404 (_, Some(_)) => replacement_text.push_str("&mut "),
405 };
406 replacement_text.push_str(type_name.as_str());
407
408 Some(TextEdit::replace(self_param.syntax().text_range(), replacement_text))
409}
410
411fn rename_self_to_param( 345fn rename_self_to_param(
412 sema: &Semantics<RootDatabase>, 346 sema: &Semantics<RootDatabase>,
413 local: hir::Local, 347 local: hir::Local,
@@ -432,66 +366,143 @@ fn rename_self_to_param(
432 let mut source_change = SourceChange::default(); 366 let mut source_change = SourceChange::default();
433 source_change.insert_source_edit(file_id.original_file(sema.db), edit); 367 source_change.insert_source_edit(file_id.original_file(sema.db), edit);
434 source_change.extend(usages.iter().map(|(&file_id, references)| { 368 source_change.extend(usages.iter().map(|(&file_id, references)| {
435 source_edit_from_references(sema, file_id, &references, def, new_name) 369 (file_id, source_edit_from_references(&references, def, new_name))
436 })); 370 }));
437 Ok(source_change) 371 Ok(source_change)
438} 372}
439 373
440fn rename_reference( 374fn text_edit_from_self_param(self_param: &ast::SelfParam, new_name: &str) -> Option<TextEdit> {
441 sema: &Semantics<RootDatabase>, 375 fn target_type_name(impl_def: &ast::Impl) -> Option<String> {
376 if let Some(ast::Type::PathType(p)) = impl_def.self_ty() {
377 return Some(p.path()?.segment()?.name_ref()?.text().to_string());
378 }
379 None
380 }
381
382 let impl_def = self_param.syntax().ancestors().find_map(|it| ast::Impl::cast(it))?;
383 let type_name = target_type_name(&impl_def)?;
384
385 let mut replacement_text = String::from(new_name);
386 replacement_text.push_str(": ");
387 match (self_param.amp_token(), self_param.mut_token()) {
388 (Some(_), None) => replacement_text.push('&'),
389 (Some(_), Some(_)) => replacement_text.push_str("&mut "),
390 (_, _) => (),
391 };
392 replacement_text.push_str(type_name.as_str());
393
394 Some(TextEdit::replace(self_param.syntax().text_range(), replacement_text))
395}
396
397fn source_edit_from_references(
398 references: &[FileReference],
442 def: Definition, 399 def: Definition,
443 new_name: &str, 400 new_name: &str,
444) -> RenameResult<SourceChange> { 401) -> TextEdit {
445 let ident_kind = check_identifier(new_name)?; 402 let mut edit = TextEdit::builder();
446 403 for reference in references {
447 let def_is_lbl_or_lt = matches!( 404 let (range, replacement) = match &reference.name {
448 def, 405 // if the ranges differ then the node is inside a macro call, we can't really attempt
449 Definition::GenericParam(hir::GenericParam::LifetimeParam(_)) | Definition::Label(_) 406 // to make special rewrites like shorthand syntax and such, so just rename the node in
450 ); 407 // the macro input
451 match (ident_kind, def) { 408 ast::NameLike::NameRef(name_ref)
452 (IdentifierKind::ToSelf, _) 409 if name_ref.syntax().text_range() == reference.range =>
453 | (IdentifierKind::Underscore, _) 410 {
454 | (IdentifierKind::Ident, _) 411 source_edit_from_name_ref(name_ref, new_name, def)
455 if def_is_lbl_or_lt => 412 }
456 { 413 ast::NameLike::Name(name) if name.syntax().text_range() == reference.range => {
457 mark::hit!(rename_not_a_lifetime_ident_ref); 414 source_edit_from_name(name, new_name)
458 bail!("Invalid name `{}`: not a lifetime identifier", new_name) 415 }
459 } 416 _ => None,
460 (IdentifierKind::Lifetime, _) if def_is_lbl_or_lt => mark::hit!(rename_lifetime),
461 (IdentifierKind::Lifetime, _) => {
462 mark::hit!(rename_not_an_ident_ref);
463 bail!("Invalid name `{}`: not an identifier", new_name)
464 }
465 (IdentifierKind::ToSelf, Definition::Local(local)) if local.is_self(sema.db) => {
466 // no-op
467 mark::hit!(rename_self_to_self);
468 return Ok(SourceChange::default());
469 }
470 (ident_kind, Definition::Local(local)) if local.is_self(sema.db) => {
471 mark::hit!(rename_self_to_param);
472 return rename_self_to_param(sema, local, new_name, ident_kind);
473 }
474 (IdentifierKind::ToSelf, Definition::Local(local)) => {
475 mark::hit!(rename_to_self);
476 return rename_to_self(sema, local);
477 } 417 }
478 (IdentifierKind::ToSelf, _) => bail!("Invalid name `{}`: not an identifier", new_name), 418 .unwrap_or_else(|| (reference.range, new_name.to_string()));
479 (IdentifierKind::Ident, _) | (IdentifierKind::Underscore, _) => mark::hit!(rename_ident), 419 edit.replace(range, replacement);
480 } 420 }
421 edit.finish()
422}
481 423
482 let usages = def.usages(sema).all(); 424fn source_edit_from_name(name: &ast::Name, new_name: &str) -> Option<(TextRange, String)> {
483 if !usages.is_empty() && ident_kind == IdentifierKind::Underscore { 425 if let Some(_) = ast::RecordPatField::for_field_name(name) {
484 mark::hit!(rename_underscore_multiple); 426 if let Some(ident_pat) = name.syntax().parent().and_then(ast::IdentPat::cast) {
485 bail!("Cannot rename reference to `_` as it is being referenced multiple times"); 427 return Some((
428 TextRange::empty(ident_pat.syntax().text_range().start()),
429 [new_name, ": "].concat(),
430 ));
431 }
486 } 432 }
487 let mut source_change = SourceChange::default(); 433 None
488 source_change.extend(usages.iter().map(|(&file_id, references)| { 434}
489 source_edit_from_references(sema, file_id, &references, def, new_name)
490 }));
491 435
492 let (file_id, edit) = source_edit_from_def(sema, def, new_name)?; 436fn source_edit_from_name_ref(
493 source_change.insert_source_edit(file_id, edit); 437 name_ref: &ast::NameRef,
494 Ok(source_change) 438 new_name: &str,
439 def: Definition,
440) -> Option<(TextRange, String)> {
441 if let Some(record_field) = ast::RecordExprField::for_name_ref(name_ref) {
442 let rcf_name_ref = record_field.name_ref();
443 let rcf_expr = record_field.expr();
444 match (rcf_name_ref, rcf_expr.and_then(|it| it.name_ref())) {
445 // field: init-expr, check if we can use a field init shorthand
446 (Some(field_name), Some(init)) => {
447 if field_name == *name_ref {
448 if init.text() == new_name {
449 cov_mark::hit!(test_rename_field_put_init_shorthand);
450 // same names, we can use a shorthand here instead.
451 // we do not want to erase attributes hence this range start
452 let s = field_name.syntax().text_range().start();
453 let e = record_field.syntax().text_range().end();
454 return Some((TextRange::new(s, e), new_name.to_owned()));
455 }
456 } else if init == *name_ref {
457 if field_name.text() == new_name {
458 cov_mark::hit!(test_rename_local_put_init_shorthand);
459 // same names, we can use a shorthand here instead.
460 // we do not want to erase attributes hence this range start
461 let s = field_name.syntax().text_range().start();
462 let e = record_field.syntax().text_range().end();
463 return Some((TextRange::new(s, e), new_name.to_owned()));
464 }
465 }
466 None
467 }
468 // init shorthand
469 // FIXME: instead of splitting the shorthand, recursively trigger a rename of the
470 // other name https://github.com/rust-analyzer/rust-analyzer/issues/6547
471 (None, Some(_)) if matches!(def, Definition::Field(_)) => {
472 cov_mark::hit!(test_rename_field_in_field_shorthand);
473 let s = name_ref.syntax().text_range().start();
474 Some((TextRange::empty(s), format!("{}: ", new_name)))
475 }
476 (None, Some(_)) if matches!(def, Definition::Local(_)) => {
477 cov_mark::hit!(test_rename_local_in_field_shorthand);
478 let s = name_ref.syntax().text_range().end();
479 Some((TextRange::empty(s), format!(": {}", new_name)))
480 }
481 _ => None,
482 }
483 } else if let Some(record_field) = ast::RecordPatField::for_field_name_ref(name_ref) {
484 let rcf_name_ref = record_field.name_ref();
485 let rcf_pat = record_field.pat();
486 match (rcf_name_ref, rcf_pat) {
487 // field: rename
488 (Some(field_name), Some(ast::Pat::IdentPat(pat))) if field_name == *name_ref => {
489 // field name is being renamed
490 if pat.name().map_or(false, |it| it.text() == new_name) {
491 cov_mark::hit!(test_rename_field_put_init_shorthand_pat);
492 // same names, we can use a shorthand here instead/
493 // we do not want to erase attributes hence this range start
494 let s = field_name.syntax().text_range().start();
495 let e = record_field.syntax().text_range().end();
496 Some((TextRange::new(s, e), pat.to_string()))
497 } else {
498 None
499 }
500 }
501 _ => None,
502 }
503 } else {
504 None
505 }
495} 506}
496 507
497fn source_edit_from_def( 508fn source_edit_from_def(
@@ -529,11 +540,13 @@ fn source_edit_from_def(
529mod tests { 540mod tests {
530 use expect_test::{expect, Expect}; 541 use expect_test::{expect, Expect};
531 use stdx::trim_indent; 542 use stdx::trim_indent;
532 use test_utils::{assert_eq_text, mark}; 543 use test_utils::assert_eq_text;
533 use text_edit::TextEdit; 544 use text_edit::TextEdit;
534 545
535 use crate::{fixture, FileId}; 546 use crate::{fixture, FileId};
536 547
548 use super::{RangeInfo, RenameError};
549
537 fn check(new_name: &str, ra_fixture_before: &str, ra_fixture_after: &str) { 550 fn check(new_name: &str, ra_fixture_before: &str, ra_fixture_after: &str) {
538 let ra_fixture_after = &trim_indent(ra_fixture_after); 551 let ra_fixture_after = &trim_indent(ra_fixture_after);
539 let (analysis, position) = fixture::position(ra_fixture_before); 552 let (analysis, position) = fixture::position(ra_fixture_before);
@@ -579,6 +592,45 @@ mod tests {
579 expect.assert_debug_eq(&source_change) 592 expect.assert_debug_eq(&source_change)
580 } 593 }
581 594
595 fn check_prepare(ra_fixture: &str, expect: Expect) {
596 let (analysis, position) = fixture::position(ra_fixture);
597 let result = analysis
598 .prepare_rename(position)
599 .unwrap_or_else(|err| panic!("PrepareRename was cancelled: {}", err));
600 match result {
601 Ok(RangeInfo { range, info: () }) => {
602 let source = analysis.file_text(position.file_id).unwrap();
603 expect.assert_eq(&format!("{:?}: {}", range, &source[range]))
604 }
605 Err(RenameError(err)) => expect.assert_eq(&err),
606 };
607 }
608
609 #[test]
610 fn test_prepare_rename_namelikes() {
611 check_prepare(r"fn name$0<'lifetime>() {}", expect![[r#"3..7: name"#]]);
612 check_prepare(r"fn name<'lifetime$0>() {}", expect![[r#"8..17: 'lifetime"#]]);
613 check_prepare(r"fn name<'lifetime>() { name$0(); }", expect![[r#"23..27: name"#]]);
614 }
615
616 #[test]
617 fn test_prepare_rename_in_macro() {
618 check_prepare(
619 r"macro_rules! foo {
620 ($ident:ident) => {
621 pub struct $ident;
622 }
623}
624foo!(Foo$0);",
625 expect![[r#"83..86: Foo"#]],
626 );
627 }
628
629 #[test]
630 fn test_prepare_rename_keyword() {
631 check_prepare(r"struct$0 Foo;", expect![[r#"No references found at position"#]]);
632 }
633
582 #[test] 634 #[test]
583 fn test_rename_to_underscore() { 635 fn test_rename_to_underscore() {
584 check("_", r#"fn main() { let i$0 = 1; }"#, r#"fn main() { let _ = 1; }"#); 636 check("_", r#"fn main() { let i$0 = 1; }"#, r#"fn main() { let _ = 1; }"#);
@@ -618,7 +670,7 @@ mod tests {
618 670
619 #[test] 671 #[test]
620 fn test_rename_to_invalid_identifier_lifetime() { 672 fn test_rename_to_invalid_identifier_lifetime() {
621 mark::check!(rename_not_an_ident_ref); 673 cov_mark::check!(rename_not_an_ident_ref);
622 check( 674 check(
623 "'foo", 675 "'foo",
624 r#"fn main() { let i$0 = 1; }"#, 676 r#"fn main() { let i$0 = 1; }"#,
@@ -628,7 +680,7 @@ mod tests {
628 680
629 #[test] 681 #[test]
630 fn test_rename_to_invalid_identifier_lifetime2() { 682 fn test_rename_to_invalid_identifier_lifetime2() {
631 mark::check!(rename_not_a_lifetime_ident_ref); 683 cov_mark::check!(rename_not_a_lifetime_ident_ref);
632 check( 684 check(
633 "foo", 685 "foo",
634 r#"fn main<'a>(_: &'a$0 ()) {}"#, 686 r#"fn main<'a>(_: &'a$0 ()) {}"#,
@@ -638,7 +690,7 @@ mod tests {
638 690
639 #[test] 691 #[test]
640 fn test_rename_to_underscore_invalid() { 692 fn test_rename_to_underscore_invalid() {
641 mark::check!(rename_underscore_multiple); 693 cov_mark::check!(rename_underscore_multiple);
642 check( 694 check(
643 "_", 695 "_",
644 r#"fn main(foo$0: ()) {foo;}"#, 696 r#"fn main(foo$0: ()) {foo;}"#,
@@ -657,7 +709,7 @@ mod tests {
657 709
658 #[test] 710 #[test]
659 fn test_rename_for_local() { 711 fn test_rename_for_local() {
660 mark::check!(rename_ident); 712 cov_mark::check!(rename_ident);
661 check( 713 check(
662 "k", 714 "k",
663 r#" 715 r#"
@@ -820,7 +872,7 @@ impl Foo {
820 872
821 #[test] 873 #[test]
822 fn test_rename_field_in_field_shorthand() { 874 fn test_rename_field_in_field_shorthand() {
823 mark::check!(test_rename_field_in_field_shorthand); 875 cov_mark::check!(test_rename_field_in_field_shorthand);
824 check( 876 check(
825 "j", 877 "j",
826 r#" 878 r#"
@@ -846,7 +898,7 @@ impl Foo {
846 898
847 #[test] 899 #[test]
848 fn test_rename_local_in_field_shorthand() { 900 fn test_rename_local_in_field_shorthand() {
849 mark::check!(test_rename_local_in_field_shorthand); 901 cov_mark::check!(test_rename_local_in_field_shorthand);
850 check( 902 check(
851 "j", 903 "j",
852 r#" 904 r#"
@@ -1252,7 +1304,7 @@ fn foo(f: foo::Foo) {
1252 1304
1253 #[test] 1305 #[test]
1254 fn test_parameter_to_self() { 1306 fn test_parameter_to_self() {
1255 mark::check!(rename_to_self); 1307 cov_mark::check!(rename_to_self);
1256 check( 1308 check(
1257 "self", 1309 "self",
1258 r#" 1310 r#"
@@ -1392,7 +1444,7 @@ impl Foo {
1392 1444
1393 #[test] 1445 #[test]
1394 fn test_owned_self_to_parameter() { 1446 fn test_owned_self_to_parameter() {
1395 mark::check!(rename_self_to_param); 1447 cov_mark::check!(rename_self_to_param);
1396 check( 1448 check(
1397 "foo", 1449 "foo",
1398 r#" 1450 r#"
@@ -1445,7 +1497,7 @@ impl Foo {
1445 1497
1446 #[test] 1498 #[test]
1447 fn test_rename_field_put_init_shorthand() { 1499 fn test_rename_field_put_init_shorthand() {
1448 mark::check!(test_rename_field_put_init_shorthand); 1500 cov_mark::check!(test_rename_field_put_init_shorthand);
1449 check( 1501 check(
1450 "bar", 1502 "bar",
1451 r#" 1503 r#"
@@ -1467,7 +1519,7 @@ fn foo(bar: i32) -> Foo {
1467 1519
1468 #[test] 1520 #[test]
1469 fn test_rename_local_put_init_shorthand() { 1521 fn test_rename_local_put_init_shorthand() {
1470 mark::check!(test_rename_local_put_init_shorthand); 1522 cov_mark::check!(test_rename_local_put_init_shorthand);
1471 check( 1523 check(
1472 "i", 1524 "i",
1473 r#" 1525 r#"
@@ -1489,7 +1541,7 @@ fn foo(i: i32) -> Foo {
1489 1541
1490 #[test] 1542 #[test]
1491 fn test_struct_field_pat_into_shorthand() { 1543 fn test_struct_field_pat_into_shorthand() {
1492 mark::check!(test_rename_field_put_init_shorthand_pat); 1544 cov_mark::check!(test_rename_field_put_init_shorthand_pat);
1493 check( 1545 check(
1494 "baz", 1546 "baz",
1495 r#" 1547 r#"
@@ -1601,7 +1653,7 @@ fn foo(foo: Foo) {
1601 1653
1602 #[test] 1654 #[test]
1603 fn test_rename_lifetimes() { 1655 fn test_rename_lifetimes() {
1604 mark::check!(rename_lifetime); 1656 cov_mark::check!(rename_lifetime);
1605 check( 1657 check(
1606 "'yeeee", 1658 "'yeeee",
1607 r#" 1659 r#"
@@ -1689,7 +1741,7 @@ fn foo<'a>() -> &'a () {
1689 1741
1690 #[test] 1742 #[test]
1691 fn test_self_to_self() { 1743 fn test_self_to_self() {
1692 mark::check!(rename_self_to_self); 1744 cov_mark::check!(rename_self_to_self);
1693 check( 1745 check(
1694 "self", 1746 "self",
1695 r#" 1747 r#"
diff --git a/crates/ide/src/runnables.rs b/crates/ide/src/runnables.rs
index 368bf9aa5..faa91541e 100644
--- a/crates/ide/src/runnables.rs
+++ b/crates/ide/src/runnables.rs
@@ -16,7 +16,6 @@ use syntax::{
16 ast::{self, AstNode, AttrsOwner}, 16 ast::{self, AstNode, AttrsOwner},
17 match_ast, SyntaxNode, 17 match_ast, SyntaxNode,
18}; 18};
19use test_utils::mark;
20 19
21use crate::{ 20use crate::{
22 display::{ToNav, TryToNav}, 21 display::{ToNav, TryToNav},
@@ -236,7 +235,9 @@ fn runnables_mod(sema: &Semantics<RootDatabase>, acc: &mut Vec<Runnable>, module
236 if let hir::ModuleDef::Module(submodule) = def { 235 if let hir::ModuleDef::Module(submodule) = def {
237 match submodule.definition_source(sema.db).value { 236 match submodule.definition_source(sema.db).value {
238 hir::ModuleSource::Module(_) => runnables_mod(sema, acc, submodule), 237 hir::ModuleSource::Module(_) => runnables_mod(sema, acc, submodule),
239 hir::ModuleSource::SourceFile(_) => mark::hit!(dont_recurse_in_outline_submodules), 238 hir::ModuleSource::SourceFile(_) => {
239 cov_mark::hit!(dont_recurse_in_outline_submodules)
240 }
240 hir::ModuleSource::BlockExpr(_) => {} // inner items aren't runnable 241 hir::ModuleSource::BlockExpr(_) => {} // inner items aren't runnable
241 } 242 }
242 } 243 }
@@ -295,7 +296,7 @@ pub(crate) fn doc_owner_to_def(
295) -> Option<Definition> { 296) -> Option<Definition> {
296 let res: hir::ModuleDef = match_ast! { 297 let res: hir::ModuleDef = match_ast! {
297 match item { 298 match item {
298 ast::SourceFile(it) => sema.scope(&item).module()?.into(), 299 ast::SourceFile(_it) => sema.scope(&item).module()?.into(),
299 ast::Fn(it) => sema.to_def(&it)?.into(), 300 ast::Fn(it) => sema.to_def(&it)?.into(),
300 ast::Struct(it) => sema.to_def(&it)?.into(), 301 ast::Struct(it) => sema.to_def(&it)?.into(),
301 ast::Enum(it) => sema.to_def(&it)?.into(), 302 ast::Enum(it) => sema.to_def(&it)?.into(),
@@ -434,7 +435,6 @@ fn has_test_function_or_multiple_test_submodules(
434#[cfg(test)] 435#[cfg(test)]
435mod tests { 436mod tests {
436 use expect_test::{expect, Expect}; 437 use expect_test::{expect, Expect};
437 use test_utils::mark;
438 438
439 use crate::fixture; 439 use crate::fixture;
440 440
@@ -1168,7 +1168,7 @@ mod tests {
1168 1168
1169 #[test] 1169 #[test]
1170 fn dont_recurse_in_outline_submodules() { 1170 fn dont_recurse_in_outline_submodules() {
1171 mark::check!(dont_recurse_in_outline_submodules); 1171 cov_mark::check!(dont_recurse_in_outline_submodules);
1172 check( 1172 check(
1173 r#" 1173 r#"
1174//- /lib.rs 1174//- /lib.rs
diff --git a/crates/ide/src/ssr.rs b/crates/ide/src/ssr.rs
new file mode 100644
index 000000000..f3638d928
--- /dev/null
+++ b/crates/ide/src/ssr.rs
@@ -0,0 +1,259 @@
1//! This module provides an SSR assist. It is not desirable to include this
2//! assist in ide_assists because that would require the ide_assists crate
3//! depend on the ide_ssr crate.
4
5use ide_assists::{Assist, AssistId, AssistKind, GroupLabel};
6use ide_db::{base_db::FileRange, label::Label, source_change::SourceChange, RootDatabase};
7
8pub(crate) fn add_ssr_assist(
9 db: &RootDatabase,
10 base: &mut Vec<Assist>,
11 resolve: bool,
12 frange: FileRange,
13) -> Option<()> {
14 let (match_finder, comment_range) = ide_ssr::ssr_from_comment(db, frange)?;
15
16 let (source_change_for_file, source_change_for_workspace) = if resolve {
17 let edits = match_finder.edits();
18
19 let source_change_for_file = {
20 let text_edit_for_file = edits.get(&frange.file_id).cloned().unwrap_or_default();
21 SourceChange::from_text_edit(frange.file_id, text_edit_for_file)
22 };
23
24 let source_change_for_workspace = SourceChange::from(match_finder.edits());
25
26 (Some(source_change_for_file), Some(source_change_for_workspace))
27 } else {
28 (None, None)
29 };
30
31 let assists = vec![
32 ("Apply SSR in file", source_change_for_file),
33 ("Apply SSR in workspace", source_change_for_workspace),
34 ];
35
36 for (label, source_change) in assists.into_iter() {
37 let assist = Assist {
38 id: AssistId("ssr", AssistKind::RefactorRewrite),
39 label: Label::new(label),
40 group: Some(GroupLabel("Apply SSR".into())),
41 target: comment_range,
42 source_change,
43 };
44
45 base.push(assist);
46 }
47 Some(())
48}
49
50#[cfg(test)]
51mod tests {
52 use std::sync::Arc;
53
54 use expect_test::expect;
55 use ide_assists::Assist;
56 use ide_db::{
57 base_db::{fixture::WithFixture, salsa::Durability, FileRange},
58 symbol_index::SymbolsDatabase,
59 RootDatabase,
60 };
61 use rustc_hash::FxHashSet;
62
63 use super::add_ssr_assist;
64
65 fn get_assists(ra_fixture: &str, resolve: bool) -> Vec<Assist> {
66 let (mut db, file_id, range_or_offset) = RootDatabase::with_range_or_offset(ra_fixture);
67 let mut local_roots = FxHashSet::default();
68 local_roots.insert(ide_db::base_db::fixture::WORKSPACE);
69 db.set_local_roots_with_durability(Arc::new(local_roots), Durability::HIGH);
70
71 let mut assists = vec![];
72
73 add_ssr_assist(
74 &db,
75 &mut assists,
76 resolve,
77 FileRange { file_id, range: range_or_offset.into() },
78 );
79
80 assists
81 }
82
83 #[test]
84 fn not_applicable_comment_not_ssr() {
85 let ra_fixture = r#"
86 //- /lib.rs
87
88 // This is foo $0
89 fn foo() {}
90 "#;
91 let resolve = true;
92
93 let assists = get_assists(ra_fixture, resolve);
94
95 assert_eq!(0, assists.len());
96 }
97
98 #[test]
99 fn resolve_edits_true() {
100 let resolve = true;
101 let assists = get_assists(
102 r#"
103 //- /lib.rs
104 mod bar;
105
106 // 2 ==>> 3$0
107 fn foo() { 2 }
108
109 //- /bar.rs
110 fn bar() { 2 }
111 "#,
112 resolve,
113 );
114
115 assert_eq!(2, assists.len());
116 let mut assists = assists.into_iter();
117
118 let apply_in_file_assist = assists.next().unwrap();
119 expect![[r#"
120 Assist {
121 id: AssistId(
122 "ssr",
123 RefactorRewrite,
124 ),
125 label: "Apply SSR in file",
126 group: Some(
127 GroupLabel(
128 "Apply SSR",
129 ),
130 ),
131 target: 10..21,
132 source_change: Some(
133 SourceChange {
134 source_file_edits: {
135 FileId(
136 0,
137 ): TextEdit {
138 indels: [
139 Indel {
140 insert: "3",
141 delete: 33..34,
142 },
143 ],
144 },
145 },
146 file_system_edits: [],
147 is_snippet: false,
148 },
149 ),
150 }
151 "#]]
152 .assert_debug_eq(&apply_in_file_assist);
153
154 let apply_in_workspace_assist = assists.next().unwrap();
155 expect![[r#"
156 Assist {
157 id: AssistId(
158 "ssr",
159 RefactorRewrite,
160 ),
161 label: "Apply SSR in workspace",
162 group: Some(
163 GroupLabel(
164 "Apply SSR",
165 ),
166 ),
167 target: 10..21,
168 source_change: Some(
169 SourceChange {
170 source_file_edits: {
171 FileId(
172 0,
173 ): TextEdit {
174 indels: [
175 Indel {
176 insert: "3",
177 delete: 33..34,
178 },
179 ],
180 },
181 FileId(
182 1,
183 ): TextEdit {
184 indels: [
185 Indel {
186 insert: "3",
187 delete: 11..12,
188 },
189 ],
190 },
191 },
192 file_system_edits: [],
193 is_snippet: false,
194 },
195 ),
196 }
197 "#]]
198 .assert_debug_eq(&apply_in_workspace_assist);
199 }
200
201 #[test]
202 fn resolve_edits_false() {
203 let resolve = false;
204 let assists = get_assists(
205 r#"
206 //- /lib.rs
207 mod bar;
208
209 // 2 ==>> 3$0
210 fn foo() { 2 }
211
212 //- /bar.rs
213 fn bar() { 2 }
214 "#,
215 resolve,
216 );
217
218 assert_eq!(2, assists.len());
219 let mut assists = assists.into_iter();
220
221 let apply_in_file_assist = assists.next().unwrap();
222 expect![[r#"
223 Assist {
224 id: AssistId(
225 "ssr",
226 RefactorRewrite,
227 ),
228 label: "Apply SSR in file",
229 group: Some(
230 GroupLabel(
231 "Apply SSR",
232 ),
233 ),
234 target: 10..21,
235 source_change: None,
236 }
237 "#]]
238 .assert_debug_eq(&apply_in_file_assist);
239
240 let apply_in_workspace_assist = assists.next().unwrap();
241 expect![[r#"
242 Assist {
243 id: AssistId(
244 "ssr",
245 RefactorRewrite,
246 ),
247 label: "Apply SSR in workspace",
248 group: Some(
249 GroupLabel(
250 "Apply SSR",
251 ),
252 ),
253 target: 10..21,
254 source_change: None,
255 }
256 "#]]
257 .assert_debug_eq(&apply_in_workspace_assist);
258 }
259}
diff --git a/crates/ide/src/syntax_highlighting/highlight.rs b/crates/ide/src/syntax_highlighting/highlight.rs
index 24fcbb584..b0cfdd8b7 100644
--- a/crates/ide/src/syntax_highlighting/highlight.rs
+++ b/crates/ide/src/syntax_highlighting/highlight.rs
@@ -330,10 +330,11 @@ fn highlight_def(db: &RootDatabase, def: Definition) -> Highlight {
330 HlTag::Symbol(SymbolKind::Local) 330 HlTag::Symbol(SymbolKind::Local)
331 }; 331 };
332 let mut h = Highlight::new(tag); 332 let mut h = Highlight::new(tag);
333 if local.is_mut(db) || local.ty(db).is_mutable_reference() { 333 let ty = local.ty(db);
334 if local.is_mut(db) || ty.is_mutable_reference() {
334 h |= HlMod::Mutable; 335 h |= HlMod::Mutable;
335 } 336 }
336 if local.ty(db).as_callable(db).is_some() || local.ty(db).impls_fnonce(db) { 337 if ty.as_callable(db).is_some() || ty.impls_fnonce(db) {
337 h |= HlMod::Callable; 338 h |= HlMod::Callable;
338 } 339 }
339 return h; 340 return h;
diff --git a/crates/ide/src/typing/on_enter.rs b/crates/ide/src/typing/on_enter.rs
index 63cd51b69..978c479de 100644
--- a/crates/ide/src/typing/on_enter.rs
+++ b/crates/ide/src/typing/on_enter.rs
@@ -9,7 +9,7 @@ use syntax::{
9 SyntaxKind::*, 9 SyntaxKind::*,
10 SyntaxToken, TextRange, TextSize, TokenAtOffset, 10 SyntaxToken, TextRange, TextSize, TokenAtOffset,
11}; 11};
12use test_utils::mark; 12
13use text_edit::TextEdit; 13use text_edit::TextEdit;
14 14
15// Feature: On Enter 15// Feature: On Enter
@@ -55,7 +55,7 @@ pub(crate) fn on_enter(db: &RootDatabase, position: FilePosition) -> Option<Text
55 // Continuing single-line non-doc comments (like this one :) ) is annoying 55 // Continuing single-line non-doc comments (like this one :) ) is annoying
56 if prefix == "//" && comment_range.end() == position.offset { 56 if prefix == "//" && comment_range.end() == position.offset {
57 if comment.text().ends_with(' ') { 57 if comment.text().ends_with(' ') {
58 mark::hit!(continues_end_of_line_comment_with_space); 58 cov_mark::hit!(continues_end_of_line_comment_with_space);
59 remove_trailing_whitespace = true; 59 remove_trailing_whitespace = true;
60 } else if !followed_by_comment(&comment) { 60 } else if !followed_by_comment(&comment) {
61 return None; 61 return None;
@@ -109,7 +109,7 @@ fn node_indent(file: &SourceFile, token: &SyntaxToken) -> Option<SmolStr> {
109#[cfg(test)] 109#[cfg(test)]
110mod tests { 110mod tests {
111 use stdx::trim_indent; 111 use stdx::trim_indent;
112 use test_utils::{assert_eq_text, mark}; 112 use test_utils::assert_eq_text;
113 113
114 use crate::fixture; 114 use crate::fixture;
115 115
@@ -238,7 +238,7 @@ fn main() {
238 238
239 #[test] 239 #[test]
240 fn continues_end_of_line_comment_with_space() { 240 fn continues_end_of_line_comment_with_space() {
241 mark::check!(continues_end_of_line_comment_with_space); 241 cov_mark::check!(continues_end_of_line_comment_with_space);
242 do_check( 242 do_check(
243 r#" 243 r#"
244fn main() { 244fn main() {