aboutsummaryrefslogtreecommitdiff
path: root/crates/ide
diff options
context:
space:
mode:
Diffstat (limited to 'crates/ide')
-rw-r--r--crates/ide/Cargo.toml4
-rw-r--r--crates/ide/src/diagnostics.rs722
-rw-r--r--crates/ide/src/diagnostics/field_shorthand.rs200
-rw-r--r--crates/ide/src/diagnostics/fixes.rs31
-rw-r--r--crates/ide/src/diagnostics/fixes/change_case.rs155
-rw-r--r--crates/ide/src/diagnostics/fixes/create_field.rs156
-rw-r--r--crates/ide/src/diagnostics/fixes/fill_missing_fields.rs217
-rw-r--r--crates/ide/src/diagnostics/fixes/remove_semicolon.rs41
-rw-r--r--crates/ide/src/diagnostics/fixes/replace_with_find_map.rs84
-rw-r--r--crates/ide/src/diagnostics/fixes/unresolved_module.rs89
-rw-r--r--crates/ide/src/diagnostics/fixes/wrap_tail_expr.rs211
-rw-r--r--crates/ide/src/diagnostics/unlinked_file.rs183
-rw-r--r--crates/ide/src/display/navigation_target.rs6
-rw-r--r--crates/ide/src/doc_links.rs40
-rw-r--r--crates/ide/src/extend_selection.rs2
-rw-r--r--crates/ide/src/fixture.rs20
-rw-r--r--crates/ide/src/goto_definition.rs26
-rw-r--r--crates/ide/src/goto_implementation.rs2
-rw-r--r--crates/ide/src/goto_type_definition.rs62
-rw-r--r--crates/ide/src/hover.rs30
-rw-r--r--crates/ide/src/inlay_hints.rs6
-rw-r--r--crates/ide/src/join_lines.rs2
-rw-r--r--crates/ide/src/lib.rs38
-rw-r--r--crates/ide/src/references.rs4
-rw-r--r--crates/ide/src/rename.rs (renamed from crates/ide/src/references/rename.rs)434
-rw-r--r--crates/ide/src/runnables.rs8
-rw-r--r--crates/ide/src/syntax_highlighting.rs4
-rw-r--r--crates/ide/src/syntax_highlighting/highlight.rs65
-rw-r--r--crates/ide/src/syntax_highlighting/html.rs3
-rw-r--r--crates/ide/src/syntax_highlighting/inject.rs6
-rw-r--r--crates/ide/src/syntax_highlighting/test_data/highlight_assoc_functions.html1
-rw-r--r--crates/ide/src/syntax_highlighting/test_data/highlight_doctest.html1
-rw-r--r--crates/ide/src/syntax_highlighting/test_data/highlight_extern_crate.html1
-rw-r--r--crates/ide/src/syntax_highlighting/test_data/highlight_injection.html1
-rw-r--r--crates/ide/src/syntax_highlighting/test_data/highlight_strings.html1
-rw-r--r--crates/ide/src/syntax_highlighting/test_data/highlight_unsafe.html6
-rw-r--r--crates/ide/src/syntax_highlighting/test_data/highlighting.html5
-rw-r--r--crates/ide/src/syntax_highlighting/test_data/injection.html1
-rw-r--r--crates/ide/src/syntax_highlighting/test_data/rainbow_highlighting.html1
-rw-r--r--crates/ide/src/syntax_highlighting/tests.rs9
-rw-r--r--crates/ide/src/typing/on_enter.rs6
41 files changed, 279 insertions, 2605 deletions
diff --git a/crates/ide/Cargo.toml b/crates/ide/Cargo.toml
index 88f3d09d3..0e8447394 100644
--- a/crates/ide/Cargo.toml
+++ b/crates/ide/Cargo.toml
@@ -10,7 +10,7 @@ edition = "2018"
10doctest = false 10doctest = false
11 11
12[dependencies] 12[dependencies]
13cov-mark = { version = "1.1", features = ["thread-local"] } 13cov-mark = "2.0.0-pre.1"
14either = "1.5.3" 14either = "1.5.3"
15indexmap = "1.4.0" 15indexmap = "1.4.0"
16itertools = "0.10.0" 16itertools = "0.10.0"
@@ -29,6 +29,7 @@ ide_db = { path = "../ide_db", version = "0.0.0" }
29cfg = { path = "../cfg", version = "0.0.0" } 29cfg = { path = "../cfg", version = "0.0.0" }
30profile = { path = "../profile", version = "0.0.0" } 30profile = { path = "../profile", version = "0.0.0" }
31ide_assists = { path = "../ide_assists", version = "0.0.0" } 31ide_assists = { path = "../ide_assists", version = "0.0.0" }
32ide_diagnostics = { path = "../ide_diagnostics", version = "0.0.0" }
32ide_ssr = { path = "../ide_ssr", version = "0.0.0" } 33ide_ssr = { path = "../ide_ssr", version = "0.0.0" }
33ide_completion = { path = "../ide_completion", version = "0.0.0" } 34ide_completion = { path = "../ide_completion", version = "0.0.0" }
34 35
@@ -39,4 +40,3 @@ hir = { path = "../hir", version = "0.0.0" }
39[dev-dependencies] 40[dev-dependencies]
40test_utils = { path = "../test_utils" } 41test_utils = { path = "../test_utils" }
41expect-test = "1.1" 42expect-test = "1.1"
42cov-mark = "1.1.0"
diff --git a/crates/ide/src/diagnostics.rs b/crates/ide/src/diagnostics.rs
deleted file mode 100644
index d5c954b8b..000000000
--- a/crates/ide/src/diagnostics.rs
+++ /dev/null
@@ -1,722 +0,0 @@
1//! Collects diagnostics & fixits for a single file.
2//!
3//! The tricky bit here is that diagnostics are produced by hir in terms of
4//! macro-expanded files, but we need to present them to the users in terms of
5//! original files. So we need to map the ranges.
6
7mod fixes;
8mod field_shorthand;
9mod unlinked_file;
10
11use std::cell::RefCell;
12
13use hir::{
14 db::AstDatabase,
15 diagnostics::{Diagnostic as _, DiagnosticCode, DiagnosticSinkBuilder},
16 InFile, Semantics,
17};
18use ide_assists::AssistResolveStrategy;
19use ide_db::{base_db::SourceDatabase, RootDatabase};
20use itertools::Itertools;
21use rustc_hash::FxHashSet;
22use syntax::{
23 ast::{self, AstNode},
24 SyntaxNode, SyntaxNodePtr, TextRange, TextSize,
25};
26use text_edit::TextEdit;
27use unlinked_file::UnlinkedFile;
28
29use crate::{Assist, AssistId, AssistKind, FileId, Label, SourceChange};
30
31use self::fixes::DiagnosticWithFixes;
32
33#[derive(Debug)]
34pub struct Diagnostic {
35 // pub name: Option<String>,
36 pub message: String,
37 pub range: TextRange,
38 pub severity: Severity,
39 pub fixes: Option<Vec<Assist>>,
40 pub unused: bool,
41 pub code: Option<DiagnosticCode>,
42}
43
44impl Diagnostic {
45 fn error(range: TextRange, message: String) -> Self {
46 Self { message, range, severity: Severity::Error, fixes: None, unused: false, code: None }
47 }
48
49 fn hint(range: TextRange, message: String) -> Self {
50 Self {
51 message,
52 range,
53 severity: Severity::WeakWarning,
54 fixes: None,
55 unused: false,
56 code: None,
57 }
58 }
59
60 fn with_fixes(self, fixes: Option<Vec<Assist>>) -> Self {
61 Self { fixes, ..self }
62 }
63
64 fn with_unused(self, unused: bool) -> Self {
65 Self { unused, ..self }
66 }
67
68 fn with_code(self, code: Option<DiagnosticCode>) -> Self {
69 Self { code, ..self }
70 }
71}
72
73#[derive(Debug, Copy, Clone)]
74pub enum Severity {
75 Error,
76 WeakWarning,
77}
78
79#[derive(Default, Debug, Clone)]
80pub struct DiagnosticsConfig {
81 pub disable_experimental: bool,
82 pub disabled: FxHashSet<String>,
83}
84
85pub(crate) fn diagnostics(
86 db: &RootDatabase,
87 config: &DiagnosticsConfig,
88 resolve: &AssistResolveStrategy,
89 file_id: FileId,
90) -> Vec<Diagnostic> {
91 let _p = profile::span("diagnostics");
92 let sema = Semantics::new(db);
93 let parse = db.parse(file_id);
94 let mut res = Vec::new();
95
96 // [#34344] Only take first 128 errors to prevent slowing down editor/ide, the number 128 is chosen arbitrarily.
97 res.extend(
98 parse
99 .errors()
100 .iter()
101 .take(128)
102 .map(|err| Diagnostic::error(err.range(), format!("Syntax Error: {}", err))),
103 );
104
105 for node in parse.tree().syntax().descendants() {
106 check_unnecessary_braces_in_use_statement(&mut res, file_id, &node);
107 field_shorthand::check(&mut res, file_id, &node);
108 }
109 let res = RefCell::new(res);
110 let sink_builder = DiagnosticSinkBuilder::new()
111 .on::<hir::diagnostics::UnresolvedModule, _>(|d| {
112 res.borrow_mut().push(diagnostic_with_fix(d, &sema, resolve));
113 })
114 .on::<hir::diagnostics::MissingFields, _>(|d| {
115 res.borrow_mut().push(diagnostic_with_fix(d, &sema, resolve));
116 })
117 .on::<hir::diagnostics::MissingOkOrSomeInTailExpr, _>(|d| {
118 res.borrow_mut().push(diagnostic_with_fix(d, &sema, resolve));
119 })
120 .on::<hir::diagnostics::NoSuchField, _>(|d| {
121 res.borrow_mut().push(diagnostic_with_fix(d, &sema, resolve));
122 })
123 .on::<hir::diagnostics::RemoveThisSemicolon, _>(|d| {
124 res.borrow_mut().push(diagnostic_with_fix(d, &sema, resolve));
125 })
126 .on::<hir::diagnostics::IncorrectCase, _>(|d| {
127 res.borrow_mut().push(warning_with_fix(d, &sema, resolve));
128 })
129 .on::<hir::diagnostics::ReplaceFilterMapNextWithFindMap, _>(|d| {
130 res.borrow_mut().push(warning_with_fix(d, &sema, resolve));
131 })
132 .on::<hir::diagnostics::InactiveCode, _>(|d| {
133 // If there's inactive code somewhere in a macro, don't propagate to the call-site.
134 if d.display_source().file_id.expansion_info(db).is_some() {
135 return;
136 }
137
138 // Override severity and mark as unused.
139 res.borrow_mut().push(
140 Diagnostic::hint(
141 sema.diagnostics_display_range(d.display_source()).range,
142 d.message(),
143 )
144 .with_unused(true)
145 .with_code(Some(d.code())),
146 );
147 })
148 .on::<UnlinkedFile, _>(|d| {
149 // Limit diagnostic to the first few characters in the file. This matches how VS Code
150 // renders it with the full span, but on other editors, and is less invasive.
151 let range = sema.diagnostics_display_range(d.display_source()).range;
152 let range = range.intersect(TextRange::up_to(TextSize::of("..."))).unwrap_or(range);
153
154 // Override severity and mark as unused.
155 res.borrow_mut().push(
156 Diagnostic::hint(range, d.message())
157 .with_fixes(d.fixes(&sema, resolve))
158 .with_code(Some(d.code())),
159 );
160 })
161 .on::<hir::diagnostics::UnresolvedProcMacro, _>(|d| {
162 // Use more accurate position if available.
163 let display_range = d
164 .precise_location
165 .unwrap_or_else(|| sema.diagnostics_display_range(d.display_source()).range);
166
167 // FIXME: it would be nice to tell the user whether proc macros are currently disabled
168 res.borrow_mut()
169 .push(Diagnostic::hint(display_range, d.message()).with_code(Some(d.code())));
170 })
171 .on::<hir::diagnostics::UnresolvedMacroCall, _>(|d| {
172 let last_path_segment = sema.db.parse_or_expand(d.file).and_then(|root| {
173 d.node
174 .to_node(&root)
175 .path()
176 .and_then(|it| it.segment())
177 .and_then(|it| it.name_ref())
178 .map(|it| InFile::new(d.file, SyntaxNodePtr::new(it.syntax())))
179 });
180 let diagnostics = last_path_segment.unwrap_or_else(|| d.display_source());
181 let display_range = sema.diagnostics_display_range(diagnostics).range;
182 res.borrow_mut()
183 .push(Diagnostic::error(display_range, d.message()).with_code(Some(d.code())));
184 })
185 .on::<hir::diagnostics::UnimplementedBuiltinMacro, _>(|d| {
186 let display_range = sema.diagnostics_display_range(d.display_source()).range;
187 res.borrow_mut()
188 .push(Diagnostic::hint(display_range, d.message()).with_code(Some(d.code())));
189 })
190 // Only collect experimental diagnostics when they're enabled.
191 .filter(|diag| !(diag.is_experimental() && config.disable_experimental))
192 .filter(|diag| !config.disabled.contains(diag.code().as_str()));
193
194 // Finalize the `DiagnosticSink` building process.
195 let mut sink = sink_builder
196 // Diagnostics not handled above get no fix and default treatment.
197 .build(|d| {
198 res.borrow_mut().push(
199 Diagnostic::error(
200 sema.diagnostics_display_range(d.display_source()).range,
201 d.message(),
202 )
203 .with_code(Some(d.code())),
204 );
205 });
206
207 match sema.to_module_def(file_id) {
208 Some(m) => m.diagnostics(db, &mut sink),
209 None => {
210 sink.push(UnlinkedFile { file_id, node: SyntaxNodePtr::new(&parse.tree().syntax()) });
211 }
212 }
213
214 drop(sink);
215 res.into_inner()
216}
217
218fn diagnostic_with_fix<D: DiagnosticWithFixes>(
219 d: &D,
220 sema: &Semantics<RootDatabase>,
221 resolve: &AssistResolveStrategy,
222) -> Diagnostic {
223 Diagnostic::error(sema.diagnostics_display_range(d.display_source()).range, d.message())
224 .with_fixes(d.fixes(&sema, resolve))
225 .with_code(Some(d.code()))
226}
227
228fn warning_with_fix<D: DiagnosticWithFixes>(
229 d: &D,
230 sema: &Semantics<RootDatabase>,
231 resolve: &AssistResolveStrategy,
232) -> Diagnostic {
233 Diagnostic::hint(sema.diagnostics_display_range(d.display_source()).range, d.message())
234 .with_fixes(d.fixes(&sema, resolve))
235 .with_code(Some(d.code()))
236}
237
238fn check_unnecessary_braces_in_use_statement(
239 acc: &mut Vec<Diagnostic>,
240 file_id: FileId,
241 node: &SyntaxNode,
242) -> Option<()> {
243 let use_tree_list = ast::UseTreeList::cast(node.clone())?;
244 if let Some((single_use_tree,)) = use_tree_list.use_trees().collect_tuple() {
245 // If there is a comment inside the bracketed `use`,
246 // assume it is a commented out module path and don't show diagnostic.
247 if use_tree_list.has_inner_comment() {
248 return Some(());
249 }
250
251 let use_range = use_tree_list.syntax().text_range();
252 let edit =
253 text_edit_for_remove_unnecessary_braces_with_self_in_use_statement(&single_use_tree)
254 .unwrap_or_else(|| {
255 let to_replace = single_use_tree.syntax().text().to_string();
256 let mut edit_builder = TextEdit::builder();
257 edit_builder.delete(use_range);
258 edit_builder.insert(use_range.start(), to_replace);
259 edit_builder.finish()
260 });
261
262 acc.push(
263 Diagnostic::hint(use_range, "Unnecessary braces in use statement".to_string())
264 .with_fixes(Some(vec![fix(
265 "remove_braces",
266 "Remove unnecessary braces",
267 SourceChange::from_text_edit(file_id, edit),
268 use_range,
269 )])),
270 );
271 }
272
273 Some(())
274}
275
276fn text_edit_for_remove_unnecessary_braces_with_self_in_use_statement(
277 single_use_tree: &ast::UseTree,
278) -> Option<TextEdit> {
279 let use_tree_list_node = single_use_tree.syntax().parent()?;
280 if single_use_tree.path()?.segment()?.self_token().is_some() {
281 let start = use_tree_list_node.prev_sibling_or_token()?.text_range().start();
282 let end = use_tree_list_node.text_range().end();
283 return Some(TextEdit::delete(TextRange::new(start, end)));
284 }
285 None
286}
287
288fn fix(id: &'static str, label: &str, source_change: SourceChange, target: TextRange) -> Assist {
289 let mut res = unresolved_fix(id, label, target);
290 res.source_change = Some(source_change);
291 res
292}
293
294fn unresolved_fix(id: &'static str, label: &str, target: TextRange) -> Assist {
295 assert!(!id.contains(' '));
296 Assist {
297 id: AssistId(id, AssistKind::QuickFix),
298 label: Label::new(label),
299 group: None,
300 target,
301 source_change: None,
302 }
303}
304
305#[cfg(test)]
306mod tests {
307 use expect_test::Expect;
308 use ide_assists::AssistResolveStrategy;
309 use stdx::trim_indent;
310 use test_utils::{assert_eq_text, extract_annotations};
311
312 use crate::{fixture, DiagnosticsConfig};
313
314 /// Takes a multi-file input fixture with annotated cursor positions,
315 /// and checks that:
316 /// * a diagnostic is produced
317 /// * the first diagnostic fix trigger range touches the input cursor position
318 /// * that the contents of the file containing the cursor match `after` after the diagnostic fix is applied
319 #[track_caller]
320 pub(crate) fn check_fix(ra_fixture_before: &str, ra_fixture_after: &str) {
321 check_nth_fix(0, ra_fixture_before, ra_fixture_after);
322 }
323 /// Takes a multi-file input fixture with annotated cursor positions,
324 /// and checks that:
325 /// * a diagnostic is produced
326 /// * every diagnostic fixes trigger range touches the input cursor position
327 /// * that the contents of the file containing the cursor match `after` after each diagnostic fix is applied
328 pub(crate) fn check_fixes(ra_fixture_before: &str, ra_fixtures_after: Vec<&str>) {
329 for (i, ra_fixture_after) in ra_fixtures_after.iter().enumerate() {
330 check_nth_fix(i, ra_fixture_before, ra_fixture_after)
331 }
332 }
333
334 #[track_caller]
335 fn check_nth_fix(nth: usize, ra_fixture_before: &str, ra_fixture_after: &str) {
336 let after = trim_indent(ra_fixture_after);
337
338 let (analysis, file_position) = fixture::position(ra_fixture_before);
339 let diagnostic = analysis
340 .diagnostics(
341 &DiagnosticsConfig::default(),
342 AssistResolveStrategy::All,
343 file_position.file_id,
344 )
345 .unwrap()
346 .pop()
347 .unwrap();
348 let fix = &diagnostic.fixes.unwrap()[nth];
349 let actual = {
350 let source_change = fix.source_change.as_ref().unwrap();
351 let file_id = *source_change.source_file_edits.keys().next().unwrap();
352 let mut actual = analysis.file_text(file_id).unwrap().to_string();
353
354 for edit in source_change.source_file_edits.values() {
355 edit.apply(&mut actual);
356 }
357 actual
358 };
359
360 assert_eq_text!(&after, &actual);
361 assert!(
362 fix.target.contains_inclusive(file_position.offset),
363 "diagnostic fix range {:?} does not touch cursor position {:?}",
364 fix.target,
365 file_position.offset
366 );
367 }
368 /// Checks that there's a diagnostic *without* fix at `$0`.
369 fn check_no_fix(ra_fixture: &str) {
370 let (analysis, file_position) = fixture::position(ra_fixture);
371 let diagnostic = analysis
372 .diagnostics(
373 &DiagnosticsConfig::default(),
374 AssistResolveStrategy::All,
375 file_position.file_id,
376 )
377 .unwrap()
378 .pop()
379 .unwrap();
380 assert!(diagnostic.fixes.is_none(), "got a fix when none was expected: {:?}", diagnostic);
381 }
382
383 /// Takes a multi-file input fixture with annotated cursor position and checks that no diagnostics
384 /// apply to the file containing the cursor.
385 pub(crate) fn check_no_diagnostics(ra_fixture: &str) {
386 let (analysis, files) = fixture::files(ra_fixture);
387 let diagnostics = files
388 .into_iter()
389 .flat_map(|file_id| {
390 analysis
391 .diagnostics(&DiagnosticsConfig::default(), AssistResolveStrategy::All, file_id)
392 .unwrap()
393 })
394 .collect::<Vec<_>>();
395 assert_eq!(diagnostics.len(), 0, "unexpected diagnostics:\n{:#?}", diagnostics);
396 }
397
398 pub(crate) fn check_expect(ra_fixture: &str, expect: Expect) {
399 let (analysis, file_id) = fixture::file(ra_fixture);
400 let diagnostics = analysis
401 .diagnostics(&DiagnosticsConfig::default(), AssistResolveStrategy::All, file_id)
402 .unwrap();
403 expect.assert_debug_eq(&diagnostics)
404 }
405
406 pub(crate) fn check_diagnostics(ra_fixture: &str) {
407 let (analysis, file_id) = fixture::file(ra_fixture);
408 let diagnostics = analysis
409 .diagnostics(&DiagnosticsConfig::default(), AssistResolveStrategy::All, file_id)
410 .unwrap();
411
412 let expected = extract_annotations(&*analysis.file_text(file_id).unwrap());
413 let actual = diagnostics.into_iter().map(|d| (d.range, d.message)).collect::<Vec<_>>();
414 assert_eq!(expected, actual);
415 }
416
417 #[test]
418 fn test_unresolved_macro_range() {
419 check_diagnostics(
420 r#"
421foo::bar!(92);
422 //^^^ unresolved macro `foo::bar!`
423"#,
424 );
425 }
426
427 #[test]
428 fn unresolved_import_in_use_tree() {
429 // Only the relevant part of a nested `use` item should be highlighted.
430 check_diagnostics(
431 r#"
432use does_exist::{Exists, DoesntExist};
433 //^^^^^^^^^^^ unresolved import
434
435use {does_not_exist::*, does_exist};
436 //^^^^^^^^^^^^^^^^^ unresolved import
437
438use does_not_exist::{
439 a,
440 //^ unresolved import
441 b,
442 //^ unresolved import
443 c,
444 //^ unresolved import
445};
446
447mod does_exist {
448 pub struct Exists;
449}
450"#,
451 );
452 }
453
454 #[test]
455 fn range_mapping_out_of_macros() {
456 // FIXME: this is very wrong, but somewhat tricky to fix.
457 check_fix(
458 r#"
459fn some() {}
460fn items() {}
461fn here() {}
462
463macro_rules! id { ($($tt:tt)*) => { $($tt)*}; }
464
465fn main() {
466 let _x = id![Foo { a: $042 }];
467}
468
469pub struct Foo { pub a: i32, pub b: i32 }
470"#,
471 r#"
472fn some(, b: () ) {}
473fn items() {}
474fn here() {}
475
476macro_rules! id { ($($tt:tt)*) => { $($tt)*}; }
477
478fn main() {
479 let _x = id![Foo { a: 42 }];
480}
481
482pub struct Foo { pub a: i32, pub b: i32 }
483"#,
484 );
485 }
486
487 #[test]
488 fn test_check_unnecessary_braces_in_use_statement() {
489 check_no_diagnostics(
490 r#"
491use a;
492use a::{c, d::e};
493
494mod a {
495 mod c {}
496 mod d {
497 mod e {}
498 }
499}
500"#,
501 );
502 check_no_diagnostics(
503 r#"
504use a;
505use a::{
506 c,
507 // d::e
508};
509
510mod a {
511 mod c {}
512 mod d {
513 mod e {}
514 }
515}
516"#,
517 );
518 check_fix(
519 r"
520 mod b {}
521 use {$0b};
522 ",
523 r"
524 mod b {}
525 use b;
526 ",
527 );
528 check_fix(
529 r"
530 mod b {}
531 use {b$0};
532 ",
533 r"
534 mod b {}
535 use b;
536 ",
537 );
538 check_fix(
539 r"
540 mod a { mod c {} }
541 use a::{c$0};
542 ",
543 r"
544 mod a { mod c {} }
545 use a::c;
546 ",
547 );
548 check_fix(
549 r"
550 mod a {}
551 use a::{self$0};
552 ",
553 r"
554 mod a {}
555 use a;
556 ",
557 );
558 check_fix(
559 r"
560 mod a { mod c {} mod d { mod e {} } }
561 use a::{c, d::{e$0}};
562 ",
563 r"
564 mod a { mod c {} mod d { mod e {} } }
565 use a::{c, d::e};
566 ",
567 );
568 }
569
570 #[test]
571 fn test_disabled_diagnostics() {
572 let mut config = DiagnosticsConfig::default();
573 config.disabled.insert("unresolved-module".into());
574
575 let (analysis, file_id) = fixture::file(r#"mod foo;"#);
576
577 let diagnostics =
578 analysis.diagnostics(&config, AssistResolveStrategy::All, file_id).unwrap();
579 assert!(diagnostics.is_empty());
580
581 let diagnostics = analysis
582 .diagnostics(&DiagnosticsConfig::default(), AssistResolveStrategy::All, file_id)
583 .unwrap();
584 assert!(!diagnostics.is_empty());
585 }
586
587 #[test]
588 fn unlinked_file_prepend_first_item() {
589 cov_mark::check!(unlinked_file_prepend_before_first_item);
590 // Only tests the first one for `pub mod` since the rest are the same
591 check_fixes(
592 r#"
593//- /main.rs
594fn f() {}
595//- /foo.rs
596$0
597"#,
598 vec![
599 r#"
600mod foo;
601
602fn f() {}
603"#,
604 r#"
605pub mod foo;
606
607fn f() {}
608"#,
609 ],
610 );
611 }
612
613 #[test]
614 fn unlinked_file_append_mod() {
615 cov_mark::check!(unlinked_file_append_to_existing_mods);
616 check_fix(
617 r#"
618//- /main.rs
619//! Comment on top
620
621mod preexisting;
622
623mod preexisting2;
624
625struct S;
626
627mod preexisting_bottom;)
628//- /foo.rs
629$0
630"#,
631 r#"
632//! Comment on top
633
634mod preexisting;
635
636mod preexisting2;
637mod foo;
638
639struct S;
640
641mod preexisting_bottom;)
642"#,
643 );
644 }
645
646 #[test]
647 fn unlinked_file_insert_in_empty_file() {
648 cov_mark::check!(unlinked_file_empty_file);
649 check_fix(
650 r#"
651//- /main.rs
652//- /foo.rs
653$0
654"#,
655 r#"
656mod foo;
657"#,
658 );
659 }
660
661 #[test]
662 fn unlinked_file_old_style_modrs() {
663 check_fix(
664 r#"
665//- /main.rs
666mod submod;
667//- /submod/mod.rs
668// in mod.rs
669//- /submod/foo.rs
670$0
671"#,
672 r#"
673// in mod.rs
674mod foo;
675"#,
676 );
677 }
678
679 #[test]
680 fn unlinked_file_new_style_mod() {
681 check_fix(
682 r#"
683//- /main.rs
684mod submod;
685//- /submod.rs
686//- /submod/foo.rs
687$0
688"#,
689 r#"
690mod foo;
691"#,
692 );
693 }
694
695 #[test]
696 fn unlinked_file_with_cfg_off() {
697 cov_mark::check!(unlinked_file_skip_fix_when_mod_already_exists);
698 check_no_fix(
699 r#"
700//- /main.rs
701#[cfg(never)]
702mod foo;
703
704//- /foo.rs
705$0
706"#,
707 );
708 }
709
710 #[test]
711 fn unlinked_file_with_cfg_on() {
712 check_no_diagnostics(
713 r#"
714//- /main.rs
715#[cfg(not(never))]
716mod foo;
717
718//- /foo.rs
719"#,
720 );
721 }
722}
diff --git a/crates/ide/src/diagnostics/field_shorthand.rs b/crates/ide/src/diagnostics/field_shorthand.rs
deleted file mode 100644
index 01bd2dba6..000000000
--- a/crates/ide/src/diagnostics/field_shorthand.rs
+++ /dev/null
@@ -1,200 +0,0 @@
1//! Suggests shortening `Foo { field: field }` to `Foo { field }` in both
2//! expressions and patterns.
3
4use ide_db::{base_db::FileId, source_change::SourceChange};
5use syntax::{ast, match_ast, AstNode, SyntaxNode};
6use text_edit::TextEdit;
7
8use crate::{diagnostics::fix, Diagnostic};
9
10pub(super) fn check(acc: &mut Vec<Diagnostic>, file_id: FileId, node: &SyntaxNode) {
11 match_ast! {
12 match node {
13 ast::RecordExpr(it) => check_expr_field_shorthand(acc, file_id, it),
14 ast::RecordPat(it) => check_pat_field_shorthand(acc, file_id, it),
15 _ => ()
16 }
17 };
18}
19
20fn check_expr_field_shorthand(
21 acc: &mut Vec<Diagnostic>,
22 file_id: FileId,
23 record_expr: ast::RecordExpr,
24) {
25 let record_field_list = match record_expr.record_expr_field_list() {
26 Some(it) => it,
27 None => return,
28 };
29 for record_field in record_field_list.fields() {
30 let (name_ref, expr) = match record_field.name_ref().zip(record_field.expr()) {
31 Some(it) => it,
32 None => continue,
33 };
34
35 let field_name = name_ref.syntax().text().to_string();
36 let field_expr = expr.syntax().text().to_string();
37 let field_name_is_tup_index = name_ref.as_tuple_field().is_some();
38 if field_name != field_expr || field_name_is_tup_index {
39 continue;
40 }
41
42 let mut edit_builder = TextEdit::builder();
43 edit_builder.delete(record_field.syntax().text_range());
44 edit_builder.insert(record_field.syntax().text_range().start(), field_name);
45 let edit = edit_builder.finish();
46
47 let field_range = record_field.syntax().text_range();
48 acc.push(
49 Diagnostic::hint(field_range, "Shorthand struct initialization".to_string())
50 .with_fixes(Some(vec![fix(
51 "use_expr_field_shorthand",
52 "Use struct shorthand initialization",
53 SourceChange::from_text_edit(file_id, edit),
54 field_range,
55 )])),
56 );
57 }
58}
59
60fn check_pat_field_shorthand(
61 acc: &mut Vec<Diagnostic>,
62 file_id: FileId,
63 record_pat: ast::RecordPat,
64) {
65 let record_pat_field_list = match record_pat.record_pat_field_list() {
66 Some(it) => it,
67 None => return,
68 };
69 for record_pat_field in record_pat_field_list.fields() {
70 let (name_ref, pat) = match record_pat_field.name_ref().zip(record_pat_field.pat()) {
71 Some(it) => it,
72 None => continue,
73 };
74
75 let field_name = name_ref.syntax().text().to_string();
76 let field_pat = pat.syntax().text().to_string();
77 let field_name_is_tup_index = name_ref.as_tuple_field().is_some();
78 if field_name != field_pat || field_name_is_tup_index {
79 continue;
80 }
81
82 let mut edit_builder = TextEdit::builder();
83 edit_builder.delete(record_pat_field.syntax().text_range());
84 edit_builder.insert(record_pat_field.syntax().text_range().start(), field_name);
85 let edit = edit_builder.finish();
86
87 let field_range = record_pat_field.syntax().text_range();
88 acc.push(Diagnostic::hint(field_range, "Shorthand struct pattern".to_string()).with_fixes(
89 Some(vec![fix(
90 "use_pat_field_shorthand",
91 "Use struct field shorthand",
92 SourceChange::from_text_edit(file_id, edit),
93 field_range,
94 )]),
95 ));
96 }
97}
98
99#[cfg(test)]
100mod tests {
101 use crate::diagnostics::tests::{check_fix, check_no_diagnostics};
102
103 #[test]
104 fn test_check_expr_field_shorthand() {
105 check_no_diagnostics(
106 r#"
107struct A { a: &'static str }
108fn main() { A { a: "hello" } }
109"#,
110 );
111 check_no_diagnostics(
112 r#"
113struct A(usize);
114fn main() { A { 0: 0 } }
115"#,
116 );
117
118 check_fix(
119 r#"
120struct A { a: &'static str }
121fn main() {
122 let a = "haha";
123 A { a$0: a }
124}
125"#,
126 r#"
127struct A { a: &'static str }
128fn main() {
129 let a = "haha";
130 A { a }
131}
132"#,
133 );
134
135 check_fix(
136 r#"
137struct A { a: &'static str, b: &'static str }
138fn main() {
139 let a = "haha";
140 let b = "bb";
141 A { a$0: a, b }
142}
143"#,
144 r#"
145struct A { a: &'static str, b: &'static str }
146fn main() {
147 let a = "haha";
148 let b = "bb";
149 A { a, b }
150}
151"#,
152 );
153 }
154
155 #[test]
156 fn test_check_pat_field_shorthand() {
157 check_no_diagnostics(
158 r#"
159struct A { a: &'static str }
160fn f(a: A) { let A { a: hello } = a; }
161"#,
162 );
163 check_no_diagnostics(
164 r#"
165struct A(usize);
166fn f(a: A) { let A { 0: 0 } = a; }
167"#,
168 );
169
170 check_fix(
171 r#"
172struct A { a: &'static str }
173fn f(a: A) {
174 let A { a$0: a } = a;
175}
176"#,
177 r#"
178struct A { a: &'static str }
179fn f(a: A) {
180 let A { a } = a;
181}
182"#,
183 );
184
185 check_fix(
186 r#"
187struct A { a: &'static str, b: &'static str }
188fn f(a: A) {
189 let A { a$0: a, b } = a;
190}
191"#,
192 r#"
193struct A { a: &'static str, b: &'static str }
194fn f(a: A) {
195 let A { a, b } = a;
196}
197"#,
198 );
199 }
200}
diff --git a/crates/ide/src/diagnostics/fixes.rs b/crates/ide/src/diagnostics/fixes.rs
deleted file mode 100644
index 258ac6974..000000000
--- a/crates/ide/src/diagnostics/fixes.rs
+++ /dev/null
@@ -1,31 +0,0 @@
1//! Provides a way to attach fixes to the diagnostics.
2//! The same module also has all curret custom fixes for the diagnostics implemented.
3mod change_case;
4mod create_field;
5mod fill_missing_fields;
6mod remove_semicolon;
7mod replace_with_find_map;
8mod unresolved_module;
9mod wrap_tail_expr;
10
11use hir::{diagnostics::Diagnostic, Semantics};
12use ide_assists::AssistResolveStrategy;
13use ide_db::RootDatabase;
14
15use crate::Assist;
16
17/// A [Diagnostic] that potentially has some fixes available.
18///
19/// [Diagnostic]: hir::diagnostics::Diagnostic
20pub(crate) trait DiagnosticWithFixes: Diagnostic {
21 /// `resolve` determines if the diagnostic should fill in the `edit` field
22 /// of the assist.
23 ///
24 /// If `resolve` is false, the edit will be computed later, on demand, and
25 /// can be omitted.
26 fn fixes(
27 &self,
28 sema: &Semantics<RootDatabase>,
29 _resolve: &AssistResolveStrategy,
30 ) -> Option<Vec<Assist>>;
31}
diff --git a/crates/ide/src/diagnostics/fixes/change_case.rs b/crates/ide/src/diagnostics/fixes/change_case.rs
deleted file mode 100644
index 42be3375f..000000000
--- a/crates/ide/src/diagnostics/fixes/change_case.rs
+++ /dev/null
@@ -1,155 +0,0 @@
1use hir::{db::AstDatabase, diagnostics::IncorrectCase, InFile, Semantics};
2use ide_assists::{Assist, AssistResolveStrategy};
3use ide_db::{base_db::FilePosition, RootDatabase};
4use syntax::AstNode;
5
6use crate::{
7 diagnostics::{unresolved_fix, DiagnosticWithFixes},
8 references::rename::rename_with_semantics,
9};
10
11impl DiagnosticWithFixes for IncorrectCase {
12 fn fixes(
13 &self,
14 sema: &Semantics<RootDatabase>,
15 resolve: &AssistResolveStrategy,
16 ) -> Option<Vec<Assist>> {
17 let root = sema.db.parse_or_expand(self.file)?;
18 let name_node = self.ident.to_node(&root);
19
20 let name_node = InFile::new(self.file, name_node.syntax());
21 let frange = name_node.original_file_range(sema.db);
22 let file_position = FilePosition { file_id: frange.file_id, offset: frange.range.start() };
23
24 let label = format!("Rename to {}", self.suggested_text);
25 let mut res = unresolved_fix("change_case", &label, frange.range);
26 if resolve.should_resolve(&res.id) {
27 let source_change = rename_with_semantics(sema, file_position, &self.suggested_text);
28 res.source_change = Some(source_change.ok().unwrap_or_default());
29 }
30
31 Some(vec![res])
32 }
33}
34
35#[cfg(test)]
36mod change_case {
37 use crate::{
38 diagnostics::tests::{check_fix, check_no_diagnostics},
39 fixture, AssistResolveStrategy, DiagnosticsConfig,
40 };
41
42 #[test]
43 fn test_rename_incorrect_case() {
44 check_fix(
45 r#"
46pub struct test_struct$0 { one: i32 }
47
48pub fn some_fn(val: test_struct) -> test_struct {
49 test_struct { one: val.one + 1 }
50}
51"#,
52 r#"
53pub struct TestStruct { one: i32 }
54
55pub fn some_fn(val: TestStruct) -> TestStruct {
56 TestStruct { one: val.one + 1 }
57}
58"#,
59 );
60
61 check_fix(
62 r#"
63pub fn some_fn(NonSnakeCase$0: u8) -> u8 {
64 NonSnakeCase
65}
66"#,
67 r#"
68pub fn some_fn(non_snake_case: u8) -> u8 {
69 non_snake_case
70}
71"#,
72 );
73
74 check_fix(
75 r#"
76pub fn SomeFn$0(val: u8) -> u8 {
77 if val != 0 { SomeFn(val - 1) } else { val }
78}
79"#,
80 r#"
81pub fn some_fn(val: u8) -> u8 {
82 if val != 0 { some_fn(val - 1) } else { val }
83}
84"#,
85 );
86
87 check_fix(
88 r#"
89fn some_fn() {
90 let whatAWeird_Formatting$0 = 10;
91 another_func(whatAWeird_Formatting);
92}
93"#,
94 r#"
95fn some_fn() {
96 let what_a_weird_formatting = 10;
97 another_func(what_a_weird_formatting);
98}
99"#,
100 );
101 }
102
103 #[test]
104 fn test_uppercase_const_no_diagnostics() {
105 check_no_diagnostics(
106 r#"
107fn foo() {
108 const ANOTHER_ITEM$0: &str = "some_item";
109}
110"#,
111 );
112 }
113
114 #[test]
115 fn test_rename_incorrect_case_struct_method() {
116 check_fix(
117 r#"
118pub struct TestStruct;
119
120impl TestStruct {
121 pub fn SomeFn$0() -> TestStruct {
122 TestStruct
123 }
124}
125"#,
126 r#"
127pub struct TestStruct;
128
129impl TestStruct {
130 pub fn some_fn() -> TestStruct {
131 TestStruct
132 }
133}
134"#,
135 );
136 }
137
138 #[test]
139 fn test_single_incorrect_case_diagnostic_in_function_name_issue_6970() {
140 let input = r#"fn FOO$0() {}"#;
141 let expected = r#"fn foo() {}"#;
142
143 let (analysis, file_position) = fixture::position(input);
144 let diagnostics = analysis
145 .diagnostics(
146 &DiagnosticsConfig::default(),
147 AssistResolveStrategy::All,
148 file_position.file_id,
149 )
150 .unwrap();
151 assert_eq!(diagnostics.len(), 1);
152
153 check_fix(input, expected);
154 }
155}
diff --git a/crates/ide/src/diagnostics/fixes/create_field.rs b/crates/ide/src/diagnostics/fixes/create_field.rs
deleted file mode 100644
index a5f457dce..000000000
--- a/crates/ide/src/diagnostics/fixes/create_field.rs
+++ /dev/null
@@ -1,156 +0,0 @@
1use hir::{db::AstDatabase, diagnostics::NoSuchField, HasSource, HirDisplay, Semantics};
2use ide_db::{base_db::FileId, source_change::SourceChange, RootDatabase};
3use syntax::{
4 ast::{self, edit::IndentLevel, make},
5 AstNode,
6};
7use text_edit::TextEdit;
8
9use crate::{
10 diagnostics::{fix, DiagnosticWithFixes},
11 Assist, AssistResolveStrategy,
12};
13impl DiagnosticWithFixes for NoSuchField {
14 fn fixes(
15 &self,
16 sema: &Semantics<RootDatabase>,
17 _resolve: &AssistResolveStrategy,
18 ) -> Option<Vec<Assist>> {
19 let root = sema.db.parse_or_expand(self.file)?;
20 missing_record_expr_field_fixes(
21 &sema,
22 self.file.original_file(sema.db),
23 &self.field.to_node(&root),
24 )
25 }
26}
27
28fn missing_record_expr_field_fixes(
29 sema: &Semantics<RootDatabase>,
30 usage_file_id: FileId,
31 record_expr_field: &ast::RecordExprField,
32) -> Option<Vec<Assist>> {
33 let record_lit = ast::RecordExpr::cast(record_expr_field.syntax().parent()?.parent()?)?;
34 let def_id = sema.resolve_variant(record_lit)?;
35 let module;
36 let def_file_id;
37 let record_fields = match def_id {
38 hir::VariantDef::Struct(s) => {
39 module = s.module(sema.db);
40 let source = s.source(sema.db)?;
41 def_file_id = source.file_id;
42 let fields = source.value.field_list()?;
43 record_field_list(fields)?
44 }
45 hir::VariantDef::Union(u) => {
46 module = u.module(sema.db);
47 let source = u.source(sema.db)?;
48 def_file_id = source.file_id;
49 source.value.record_field_list()?
50 }
51 hir::VariantDef::Variant(e) => {
52 module = e.module(sema.db);
53 let source = e.source(sema.db)?;
54 def_file_id = source.file_id;
55 let fields = source.value.field_list()?;
56 record_field_list(fields)?
57 }
58 };
59 let def_file_id = def_file_id.original_file(sema.db);
60
61 let new_field_type = sema.type_of_expr(&record_expr_field.expr()?)?;
62 if new_field_type.is_unknown() {
63 return None;
64 }
65 let new_field = make::record_field(
66 None,
67 make::name(&record_expr_field.field_name()?.text()),
68 make::ty(&new_field_type.display_source_code(sema.db, module.into()).ok()?),
69 );
70
71 let last_field = record_fields.fields().last()?;
72 let last_field_syntax = last_field.syntax();
73 let indent = IndentLevel::from_node(last_field_syntax);
74
75 let mut new_field = new_field.to_string();
76 if usage_file_id != def_file_id {
77 new_field = format!("pub(crate) {}", new_field);
78 }
79 new_field = format!("\n{}{}", indent, new_field);
80
81 let needs_comma = !last_field_syntax.to_string().ends_with(',');
82 if needs_comma {
83 new_field = format!(",{}", new_field);
84 }
85
86 let source_change = SourceChange::from_text_edit(
87 def_file_id,
88 TextEdit::insert(last_field_syntax.text_range().end(), new_field),
89 );
90
91 return Some(vec![fix(
92 "create_field",
93 "Create field",
94 source_change,
95 record_expr_field.syntax().text_range(),
96 )]);
97
98 fn record_field_list(field_def_list: ast::FieldList) -> Option<ast::RecordFieldList> {
99 match field_def_list {
100 ast::FieldList::RecordFieldList(it) => Some(it),
101 ast::FieldList::TupleFieldList(_) => None,
102 }
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use crate::diagnostics::tests::check_fix;
109
110 #[test]
111 fn test_add_field_from_usage() {
112 check_fix(
113 r"
114fn main() {
115 Foo { bar: 3, baz$0: false};
116}
117struct Foo {
118 bar: i32
119}
120",
121 r"
122fn main() {
123 Foo { bar: 3, baz: false};
124}
125struct Foo {
126 bar: i32,
127 baz: bool
128}
129",
130 )
131 }
132
133 #[test]
134 fn test_add_field_in_other_file_from_usage() {
135 check_fix(
136 r#"
137//- /main.rs
138mod foo;
139
140fn main() {
141 foo::Foo { bar: 3, $0baz: false};
142}
143//- /foo.rs
144struct Foo {
145 bar: i32
146}
147"#,
148 r#"
149struct Foo {
150 bar: i32,
151 pub(crate) baz: bool
152}
153"#,
154 )
155 }
156}
diff --git a/crates/ide/src/diagnostics/fixes/fill_missing_fields.rs b/crates/ide/src/diagnostics/fixes/fill_missing_fields.rs
deleted file mode 100644
index b5dd64c08..000000000
--- a/crates/ide/src/diagnostics/fixes/fill_missing_fields.rs
+++ /dev/null
@@ -1,217 +0,0 @@
1use hir::{db::AstDatabase, diagnostics::MissingFields, Semantics};
2use ide_assists::AssistResolveStrategy;
3use ide_db::{source_change::SourceChange, RootDatabase};
4use syntax::{algo, ast::make, AstNode};
5use text_edit::TextEdit;
6
7use crate::{
8 diagnostics::{fix, fixes::DiagnosticWithFixes},
9 Assist,
10};
11
12impl DiagnosticWithFixes for MissingFields {
13 fn fixes(
14 &self,
15 sema: &Semantics<RootDatabase>,
16 _resolve: &AssistResolveStrategy,
17 ) -> Option<Vec<Assist>> {
18 // Note that although we could add a diagnostics to
19 // fill the missing tuple field, e.g :
20 // `struct A(usize);`
21 // `let a = A { 0: () }`
22 // but it is uncommon usage and it should not be encouraged.
23 if self.missed_fields.iter().any(|it| it.as_tuple_index().is_some()) {
24 return None;
25 }
26
27 let root = sema.db.parse_or_expand(self.file)?;
28 let field_list_parent = self.field_list_parent.to_node(&root);
29 let old_field_list = field_list_parent.record_expr_field_list()?;
30 let new_field_list = old_field_list.clone_for_update();
31 for f in self.missed_fields.iter() {
32 let field =
33 make::record_expr_field(make::name_ref(&f.to_string()), Some(make::expr_unit()))
34 .clone_for_update();
35 new_field_list.add_field(field);
36 }
37
38 let edit = {
39 let mut builder = TextEdit::builder();
40 algo::diff(&old_field_list.syntax(), &new_field_list.syntax())
41 .into_text_edit(&mut builder);
42 builder.finish()
43 };
44 Some(vec![fix(
45 "fill_missing_fields",
46 "Fill struct fields",
47 SourceChange::from_text_edit(self.file.original_file(sema.db), edit),
48 sema.original_range(&field_list_parent.syntax()).range,
49 )])
50 }
51}
52
53#[cfg(test)]
54mod tests {
55 use crate::diagnostics::tests::{check_fix, check_no_diagnostics};
56
57 #[test]
58 fn test_fill_struct_fields_empty() {
59 check_fix(
60 r#"
61struct TestStruct { one: i32, two: i64 }
62
63fn test_fn() {
64 let s = TestStruct {$0};
65}
66"#,
67 r#"
68struct TestStruct { one: i32, two: i64 }
69
70fn test_fn() {
71 let s = TestStruct { one: (), two: () };
72}
73"#,
74 );
75 }
76
77 #[test]
78 fn test_fill_struct_fields_self() {
79 check_fix(
80 r#"
81struct TestStruct { one: i32 }
82
83impl TestStruct {
84 fn test_fn() { let s = Self {$0}; }
85}
86"#,
87 r#"
88struct TestStruct { one: i32 }
89
90impl TestStruct {
91 fn test_fn() { let s = Self { one: () }; }
92}
93"#,
94 );
95 }
96
97 #[test]
98 fn test_fill_struct_fields_enum() {
99 check_fix(
100 r#"
101enum Expr {
102 Bin { lhs: Box<Expr>, rhs: Box<Expr> }
103}
104
105impl Expr {
106 fn new_bin(lhs: Box<Expr>, rhs: Box<Expr>) -> Expr {
107 Expr::Bin {$0 }
108 }
109}
110"#,
111 r#"
112enum Expr {
113 Bin { lhs: Box<Expr>, rhs: Box<Expr> }
114}
115
116impl Expr {
117 fn new_bin(lhs: Box<Expr>, rhs: Box<Expr>) -> Expr {
118 Expr::Bin { lhs: (), rhs: () }
119 }
120}
121"#,
122 );
123 }
124
125 #[test]
126 fn test_fill_struct_fields_partial() {
127 check_fix(
128 r#"
129struct TestStruct { one: i32, two: i64 }
130
131fn test_fn() {
132 let s = TestStruct{ two: 2$0 };
133}
134"#,
135 r"
136struct TestStruct { one: i32, two: i64 }
137
138fn test_fn() {
139 let s = TestStruct{ two: 2, one: () };
140}
141",
142 );
143 }
144
145 #[test]
146 fn test_fill_struct_fields_raw_ident() {
147 check_fix(
148 r#"
149struct TestStruct { r#type: u8 }
150
151fn test_fn() {
152 TestStruct { $0 };
153}
154"#,
155 r"
156struct TestStruct { r#type: u8 }
157
158fn test_fn() {
159 TestStruct { r#type: () };
160}
161",
162 );
163 }
164
165 #[test]
166 fn test_fill_struct_fields_no_diagnostic() {
167 check_no_diagnostics(
168 r#"
169struct TestStruct { one: i32, two: i64 }
170
171fn test_fn() {
172 let one = 1;
173 let s = TestStruct{ one, two: 2 };
174}
175 "#,
176 );
177 }
178
179 #[test]
180 fn test_fill_struct_fields_no_diagnostic_on_spread() {
181 check_no_diagnostics(
182 r#"
183struct TestStruct { one: i32, two: i64 }
184
185fn test_fn() {
186 let one = 1;
187 let s = TestStruct{ ..a };
188}
189"#,
190 );
191 }
192
193 #[test]
194 fn test_fill_struct_fields_blank_line() {
195 check_fix(
196 r#"
197struct S { a: (), b: () }
198
199fn f() {
200 S {
201 $0
202 };
203}
204"#,
205 r#"
206struct S { a: (), b: () }
207
208fn f() {
209 S {
210 a: (),
211 b: (),
212 };
213}
214"#,
215 );
216 }
217}
diff --git a/crates/ide/src/diagnostics/fixes/remove_semicolon.rs b/crates/ide/src/diagnostics/fixes/remove_semicolon.rs
deleted file mode 100644
index f1724d479..000000000
--- a/crates/ide/src/diagnostics/fixes/remove_semicolon.rs
+++ /dev/null
@@ -1,41 +0,0 @@
1use hir::{db::AstDatabase, diagnostics::RemoveThisSemicolon, Semantics};
2use ide_assists::{Assist, AssistResolveStrategy};
3use ide_db::{source_change::SourceChange, RootDatabase};
4use syntax::{ast, AstNode};
5use text_edit::TextEdit;
6
7use crate::diagnostics::{fix, DiagnosticWithFixes};
8
9impl DiagnosticWithFixes for RemoveThisSemicolon {
10 fn fixes(
11 &self,
12 sema: &Semantics<RootDatabase>,
13 _resolve: &AssistResolveStrategy,
14 ) -> Option<Vec<Assist>> {
15 let root = sema.db.parse_or_expand(self.file)?;
16
17 let semicolon = self
18 .expr
19 .to_node(&root)
20 .syntax()
21 .parent()
22 .and_then(ast::ExprStmt::cast)
23 .and_then(|expr| expr.semicolon_token())?
24 .text_range();
25
26 let edit = TextEdit::delete(semicolon);
27 let source_change = SourceChange::from_text_edit(self.file.original_file(sema.db), edit);
28
29 Some(vec![fix("remove_semicolon", "Remove this semicolon", source_change, semicolon)])
30 }
31}
32
33#[cfg(test)]
34mod tests {
35 use crate::diagnostics::tests::check_fix;
36
37 #[test]
38 fn remove_semicolon() {
39 check_fix(r#"fn f() -> i32 { 92$0; }"#, r#"fn f() -> i32 { 92 }"#);
40 }
41}
diff --git a/crates/ide/src/diagnostics/fixes/replace_with_find_map.rs b/crates/ide/src/diagnostics/fixes/replace_with_find_map.rs
deleted file mode 100644
index 444bf563b..000000000
--- a/crates/ide/src/diagnostics/fixes/replace_with_find_map.rs
+++ /dev/null
@@ -1,84 +0,0 @@
1use hir::{db::AstDatabase, diagnostics::ReplaceFilterMapNextWithFindMap, Semantics};
2use ide_assists::{Assist, AssistResolveStrategy};
3use ide_db::{source_change::SourceChange, RootDatabase};
4use syntax::{
5 ast::{self, ArgListOwner},
6 AstNode, TextRange,
7};
8use text_edit::TextEdit;
9
10use crate::diagnostics::{fix, DiagnosticWithFixes};
11
12impl DiagnosticWithFixes for ReplaceFilterMapNextWithFindMap {
13 fn fixes(
14 &self,
15 sema: &Semantics<RootDatabase>,
16 _resolve: &AssistResolveStrategy,
17 ) -> Option<Vec<Assist>> {
18 let root = sema.db.parse_or_expand(self.file)?;
19 let next_expr = self.next_expr.to_node(&root);
20 let next_call = ast::MethodCallExpr::cast(next_expr.syntax().clone())?;
21
22 let filter_map_call = ast::MethodCallExpr::cast(next_call.receiver()?.syntax().clone())?;
23 let filter_map_name_range = filter_map_call.name_ref()?.ident_token()?.text_range();
24 let filter_map_args = filter_map_call.arg_list()?;
25
26 let range_to_replace =
27 TextRange::new(filter_map_name_range.start(), next_expr.syntax().text_range().end());
28 let replacement = format!("find_map{}", filter_map_args.syntax().text());
29 let trigger_range = next_expr.syntax().text_range();
30
31 let edit = TextEdit::replace(range_to_replace, replacement);
32
33 let source_change = SourceChange::from_text_edit(self.file.original_file(sema.db), edit);
34
35 Some(vec![fix(
36 "replace_with_find_map",
37 "Replace filter_map(..).next() with find_map()",
38 source_change,
39 trigger_range,
40 )])
41 }
42}
43
44#[cfg(test)]
45mod tests {
46 use crate::diagnostics::tests::check_fix;
47
48 #[test]
49 fn replace_with_wind_map() {
50 check_fix(
51 r#"
52//- /main.rs crate:main deps:core
53use core::iter::Iterator;
54use core::option::Option::{self, Some, None};
55fn foo() {
56 let m = [1, 2, 3].iter().$0filter_map(|x| if *x == 2 { Some (4) } else { None }).next();
57}
58//- /core/lib.rs crate:core
59pub mod option {
60 pub enum Option<T> { Some(T), None }
61}
62pub mod iter {
63 pub trait Iterator {
64 type Item;
65 fn filter_map<B, F>(self, f: F) -> FilterMap where F: FnMut(Self::Item) -> Option<B> { FilterMap }
66 fn next(&mut self) -> Option<Self::Item>;
67 }
68 pub struct FilterMap {}
69 impl Iterator for FilterMap {
70 type Item = i32;
71 fn next(&mut self) -> i32 { 7 }
72 }
73}
74"#,
75 r#"
76use core::iter::Iterator;
77use core::option::Option::{self, Some, None};
78fn foo() {
79 let m = [1, 2, 3].iter().find_map(|x| if *x == 2 { Some (4) } else { None });
80}
81"#,
82 )
83 }
84}
diff --git a/crates/ide/src/diagnostics/fixes/unresolved_module.rs b/crates/ide/src/diagnostics/fixes/unresolved_module.rs
deleted file mode 100644
index b3d0283bb..000000000
--- a/crates/ide/src/diagnostics/fixes/unresolved_module.rs
+++ /dev/null
@@ -1,89 +0,0 @@
1use hir::{db::AstDatabase, diagnostics::UnresolvedModule, Semantics};
2use ide_assists::{Assist, AssistResolveStrategy};
3use ide_db::{base_db::AnchoredPathBuf, source_change::FileSystemEdit, RootDatabase};
4use syntax::AstNode;
5
6use crate::diagnostics::{fix, DiagnosticWithFixes};
7
8impl DiagnosticWithFixes for UnresolvedModule {
9 fn fixes(
10 &self,
11 sema: &Semantics<RootDatabase>,
12 _resolve: &AssistResolveStrategy,
13 ) -> Option<Vec<Assist>> {
14 let root = sema.db.parse_or_expand(self.file)?;
15 let unresolved_module = self.decl.to_node(&root);
16 Some(vec![fix(
17 "create_module",
18 "Create module",
19 FileSystemEdit::CreateFile {
20 dst: AnchoredPathBuf {
21 anchor: self.file.original_file(sema.db),
22 path: self.candidate.clone(),
23 },
24 initial_contents: "".to_string(),
25 }
26 .into(),
27 unresolved_module.syntax().text_range(),
28 )])
29 }
30}
31
32#[cfg(test)]
33mod tests {
34 use expect_test::expect;
35
36 use crate::diagnostics::tests::check_expect;
37
38 #[test]
39 fn test_unresolved_module_diagnostic() {
40 check_expect(
41 r#"mod foo;"#,
42 expect![[r#"
43 [
44 Diagnostic {
45 message: "unresolved module",
46 range: 0..8,
47 severity: Error,
48 fixes: Some(
49 [
50 Assist {
51 id: AssistId(
52 "create_module",
53 QuickFix,
54 ),
55 label: "Create module",
56 group: None,
57 target: 0..8,
58 source_change: Some(
59 SourceChange {
60 source_file_edits: {},
61 file_system_edits: [
62 CreateFile {
63 dst: AnchoredPathBuf {
64 anchor: FileId(
65 0,
66 ),
67 path: "foo.rs",
68 },
69 initial_contents: "",
70 },
71 ],
72 is_snippet: false,
73 },
74 ),
75 },
76 ],
77 ),
78 unused: false,
79 code: Some(
80 DiagnosticCode(
81 "unresolved-module",
82 ),
83 ),
84 },
85 ]
86 "#]],
87 );
88 }
89}
diff --git a/crates/ide/src/diagnostics/fixes/wrap_tail_expr.rs b/crates/ide/src/diagnostics/fixes/wrap_tail_expr.rs
deleted file mode 100644
index 715a403b9..000000000
--- a/crates/ide/src/diagnostics/fixes/wrap_tail_expr.rs
+++ /dev/null
@@ -1,211 +0,0 @@
1use hir::{db::AstDatabase, diagnostics::MissingOkOrSomeInTailExpr, Semantics};
2use ide_assists::{Assist, AssistResolveStrategy};
3use ide_db::{source_change::SourceChange, RootDatabase};
4use syntax::AstNode;
5use text_edit::TextEdit;
6
7use crate::diagnostics::{fix, DiagnosticWithFixes};
8
9impl DiagnosticWithFixes for MissingOkOrSomeInTailExpr {
10 fn fixes(
11 &self,
12 sema: &Semantics<RootDatabase>,
13 _resolve: &AssistResolveStrategy,
14 ) -> Option<Vec<Assist>> {
15 let root = sema.db.parse_or_expand(self.file)?;
16 let tail_expr = self.expr.to_node(&root);
17 let tail_expr_range = tail_expr.syntax().text_range();
18 let replacement = format!("{}({})", self.required, tail_expr.syntax());
19 let edit = TextEdit::replace(tail_expr_range, replacement);
20 let source_change = SourceChange::from_text_edit(self.file.original_file(sema.db), edit);
21 let name = if self.required == "Ok" { "Wrap with Ok" } else { "Wrap with Some" };
22 Some(vec![fix("wrap_tail_expr", name, source_change, tail_expr_range)])
23 }
24}
25
26#[cfg(test)]
27mod tests {
28 use crate::diagnostics::tests::{check_fix, check_no_diagnostics};
29
30 #[test]
31 fn test_wrap_return_type_option() {
32 check_fix(
33 r#"
34//- /main.rs crate:main deps:core
35use core::option::Option::{self, Some, None};
36
37fn div(x: i32, y: i32) -> Option<i32> {
38 if y == 0 {
39 return None;
40 }
41 x / y$0
42}
43//- /core/lib.rs crate:core
44pub mod result {
45 pub enum Result<T, E> { Ok(T), Err(E) }
46}
47pub mod option {
48 pub enum Option<T> { Some(T), None }
49}
50"#,
51 r#"
52use core::option::Option::{self, Some, None};
53
54fn div(x: i32, y: i32) -> Option<i32> {
55 if y == 0 {
56 return None;
57 }
58 Some(x / y)
59}
60"#,
61 );
62 }
63
64 #[test]
65 fn test_wrap_return_type() {
66 check_fix(
67 r#"
68//- /main.rs crate:main deps:core
69use core::result::Result::{self, Ok, Err};
70
71fn div(x: i32, y: i32) -> Result<i32, ()> {
72 if y == 0 {
73 return Err(());
74 }
75 x / y$0
76}
77//- /core/lib.rs crate:core
78pub mod result {
79 pub enum Result<T, E> { Ok(T), Err(E) }
80}
81pub mod option {
82 pub enum Option<T> { Some(T), None }
83}
84"#,
85 r#"
86use core::result::Result::{self, Ok, Err};
87
88fn div(x: i32, y: i32) -> Result<i32, ()> {
89 if y == 0 {
90 return Err(());
91 }
92 Ok(x / y)
93}
94"#,
95 );
96 }
97
98 #[test]
99 fn test_wrap_return_type_handles_generic_functions() {
100 check_fix(
101 r#"
102//- /main.rs crate:main deps:core
103use core::result::Result::{self, Ok, Err};
104
105fn div<T>(x: T) -> Result<T, i32> {
106 if x == 0 {
107 return Err(7);
108 }
109 $0x
110}
111//- /core/lib.rs crate:core
112pub mod result {
113 pub enum Result<T, E> { Ok(T), Err(E) }
114}
115pub mod option {
116 pub enum Option<T> { Some(T), None }
117}
118"#,
119 r#"
120use core::result::Result::{self, Ok, Err};
121
122fn div<T>(x: T) -> Result<T, i32> {
123 if x == 0 {
124 return Err(7);
125 }
126 Ok(x)
127}
128"#,
129 );
130 }
131
132 #[test]
133 fn test_wrap_return_type_handles_type_aliases() {
134 check_fix(
135 r#"
136//- /main.rs crate:main deps:core
137use core::result::Result::{self, Ok, Err};
138
139type MyResult<T> = Result<T, ()>;
140
141fn div(x: i32, y: i32) -> MyResult<i32> {
142 if y == 0 {
143 return Err(());
144 }
145 x $0/ y
146}
147//- /core/lib.rs crate:core
148pub mod result {
149 pub enum Result<T, E> { Ok(T), Err(E) }
150}
151pub mod option {
152 pub enum Option<T> { Some(T), None }
153}
154"#,
155 r#"
156use core::result::Result::{self, Ok, Err};
157
158type MyResult<T> = Result<T, ()>;
159
160fn div(x: i32, y: i32) -> MyResult<i32> {
161 if y == 0 {
162 return Err(());
163 }
164 Ok(x / y)
165}
166"#,
167 );
168 }
169
170 #[test]
171 fn test_wrap_return_type_not_applicable_when_expr_type_does_not_match_ok_type() {
172 check_no_diagnostics(
173 r#"
174//- /main.rs crate:main deps:core
175use core::result::Result::{self, Ok, Err};
176
177fn foo() -> Result<(), i32> { 0 }
178
179//- /core/lib.rs crate:core
180pub mod result {
181 pub enum Result<T, E> { Ok(T), Err(E) }
182}
183pub mod option {
184 pub enum Option<T> { Some(T), None }
185}
186"#,
187 );
188 }
189
190 #[test]
191 fn test_wrap_return_type_not_applicable_when_return_type_is_not_result_or_option() {
192 check_no_diagnostics(
193 r#"
194//- /main.rs crate:main deps:core
195use core::result::Result::{self, Ok, Err};
196
197enum SomeOtherEnum { Ok(i32), Err(String) }
198
199fn foo() -> SomeOtherEnum { 0 }
200
201//- /core/lib.rs crate:core
202pub mod result {
203 pub enum Result<T, E> { Ok(T), Err(E) }
204}
205pub mod option {
206 pub enum Option<T> { Some(T), None }
207}
208"#,
209 );
210 }
211}
diff --git a/crates/ide/src/diagnostics/unlinked_file.rs b/crates/ide/src/diagnostics/unlinked_file.rs
deleted file mode 100644
index 51fe0f360..000000000
--- a/crates/ide/src/diagnostics/unlinked_file.rs
+++ /dev/null
@@ -1,183 +0,0 @@
1//! Diagnostic emitted for files that aren't part of any crate.
2
3use hir::{
4 db::DefDatabase,
5 diagnostics::{Diagnostic, DiagnosticCode},
6 InFile,
7};
8use ide_assists::AssistResolveStrategy;
9use ide_db::{
10 base_db::{FileId, FileLoader, SourceDatabase, SourceDatabaseExt},
11 source_change::SourceChange,
12 RootDatabase,
13};
14use syntax::{
15 ast::{self, ModuleItemOwner, NameOwner},
16 AstNode, SyntaxNodePtr,
17};
18use text_edit::TextEdit;
19
20use crate::{
21 diagnostics::{fix, fixes::DiagnosticWithFixes},
22 Assist,
23};
24
25// Diagnostic: unlinked-file
26//
27// This diagnostic is shown for files that are not included in any crate, or files that are part of
28// crates rust-analyzer failed to discover. The file will not have IDE features available.
29#[derive(Debug)]
30pub(crate) struct UnlinkedFile {
31 pub(crate) file_id: FileId,
32 pub(crate) node: SyntaxNodePtr,
33}
34
35impl Diagnostic for UnlinkedFile {
36 fn code(&self) -> DiagnosticCode {
37 DiagnosticCode("unlinked-file")
38 }
39
40 fn message(&self) -> String {
41 "file not included in module tree".to_string()
42 }
43
44 fn display_source(&self) -> InFile<SyntaxNodePtr> {
45 InFile::new(self.file_id.into(), self.node.clone())
46 }
47
48 fn as_any(&self) -> &(dyn std::any::Any + Send + 'static) {
49 self
50 }
51}
52
53impl DiagnosticWithFixes for UnlinkedFile {
54 fn fixes(
55 &self,
56 sema: &hir::Semantics<RootDatabase>,
57 _resolve: &AssistResolveStrategy,
58 ) -> Option<Vec<Assist>> {
59 // If there's an existing module that could add `mod` or `pub mod` items to include the unlinked file,
60 // suggest that as a fix.
61
62 let source_root = sema.db.source_root(sema.db.file_source_root(self.file_id));
63 let our_path = source_root.path_for_file(&self.file_id)?;
64 let module_name = our_path.name_and_extension()?.0;
65
66 // Candidates to look for:
67 // - `mod.rs` in the same folder
68 // - we also check `main.rs` and `lib.rs`
69 // - `$dir.rs` in the parent folder, where `$dir` is the directory containing `self.file_id`
70 let parent = our_path.parent()?;
71 let mut paths =
72 vec![parent.join("mod.rs")?, parent.join("lib.rs")?, parent.join("main.rs")?];
73
74 // `submod/bla.rs` -> `submod.rs`
75 if let Some(newmod) = (|| {
76 let name = parent.name_and_extension()?.0;
77 parent.parent()?.join(&format!("{}.rs", name))
78 })() {
79 paths.push(newmod);
80 }
81
82 for path in &paths {
83 if let Some(parent_id) = source_root.file_for_path(path) {
84 for krate in sema.db.relevant_crates(*parent_id).iter() {
85 let crate_def_map = sema.db.crate_def_map(*krate);
86 for (_, module) in crate_def_map.modules() {
87 if module.origin.is_inline() {
88 // We don't handle inline `mod parent {}`s, they use different paths.
89 continue;
90 }
91
92 if module.origin.file_id() == Some(*parent_id) {
93 return make_fixes(sema.db, *parent_id, module_name, self.file_id);
94 }
95 }
96 }
97 }
98 }
99
100 None
101 }
102}
103
104fn make_fixes(
105 db: &RootDatabase,
106 parent_file_id: FileId,
107 new_mod_name: &str,
108 added_file_id: FileId,
109) -> Option<Vec<Assist>> {
110 fn is_outline_mod(item: &ast::Item) -> bool {
111 matches!(item, ast::Item::Module(m) if m.item_list().is_none())
112 }
113
114 let mod_decl = format!("mod {};", new_mod_name);
115 let pub_mod_decl = format!("pub mod {};", new_mod_name);
116
117 let ast: ast::SourceFile = db.parse(parent_file_id).tree();
118
119 let mut mod_decl_builder = TextEdit::builder();
120 let mut pub_mod_decl_builder = TextEdit::builder();
121
122 // If there's an existing `mod m;` statement matching the new one, don't emit a fix (it's
123 // probably `#[cfg]`d out).
124 for item in ast.items() {
125 if let ast::Item::Module(m) = item {
126 if let Some(name) = m.name() {
127 if m.item_list().is_none() && name.to_string() == new_mod_name {
128 cov_mark::hit!(unlinked_file_skip_fix_when_mod_already_exists);
129 return None;
130 }
131 }
132 }
133 }
134
135 // If there are existing `mod m;` items, append after them (after the first group of them, rather).
136 match ast
137 .items()
138 .skip_while(|item| !is_outline_mod(item))
139 .take_while(|item| is_outline_mod(item))
140 .last()
141 {
142 Some(last) => {
143 cov_mark::hit!(unlinked_file_append_to_existing_mods);
144 let offset = last.syntax().text_range().end();
145 mod_decl_builder.insert(offset, format!("\n{}", mod_decl));
146 pub_mod_decl_builder.insert(offset, format!("\n{}", pub_mod_decl));
147 }
148 None => {
149 // Prepend before the first item in the file.
150 match ast.items().next() {
151 Some(item) => {
152 cov_mark::hit!(unlinked_file_prepend_before_first_item);
153 let offset = item.syntax().text_range().start();
154 mod_decl_builder.insert(offset, format!("{}\n\n", mod_decl));
155 pub_mod_decl_builder.insert(offset, format!("{}\n\n", pub_mod_decl));
156 }
157 None => {
158 // No items in the file, so just append at the end.
159 cov_mark::hit!(unlinked_file_empty_file);
160 let offset = ast.syntax().text_range().end();
161 mod_decl_builder.insert(offset, format!("{}\n", mod_decl));
162 pub_mod_decl_builder.insert(offset, format!("{}\n", pub_mod_decl));
163 }
164 }
165 }
166 }
167
168 let trigger_range = db.parse(added_file_id).tree().syntax().text_range();
169 Some(vec![
170 fix(
171 "add_mod_declaration",
172 &format!("Insert `{}`", mod_decl),
173 SourceChange::from_text_edit(parent_file_id, mod_decl_builder.finish()),
174 trigger_range,
175 ),
176 fix(
177 "add_pub_mod_declaration",
178 &format!("Insert `{}`", pub_mod_decl),
179 SourceChange::from_text_edit(parent_file_id, pub_mod_decl_builder.finish()),
180 trigger_range,
181 ),
182 ])
183}
diff --git a/crates/ide/src/display/navigation_target.rs b/crates/ide/src/display/navigation_target.rs
index b75ec411c..455b32456 100644
--- a/crates/ide/src/display/navigation_target.rs
+++ b/crates/ide/src/display/navigation_target.rs
@@ -442,10 +442,10 @@ impl TryToNav for hir::TypeParam {
442 fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget> { 442 fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget> {
443 let src = self.source(db)?; 443 let src = self.source(db)?;
444 let full_range = match &src.value { 444 let full_range = match &src.value {
445 Either::Left(it) => it 445 Either::Left(type_param) => type_param.syntax().text_range(),
446 Either::Right(trait_) => trait_
446 .name() 447 .name()
447 .map_or_else(|| it.syntax().text_range(), |name| name.syntax().text_range()), 448 .map_or_else(|| trait_.syntax().text_range(), |name| name.syntax().text_range()),
448 Either::Right(it) => it.syntax().text_range(),
449 }; 449 };
450 let focus_range = match &src.value { 450 let focus_range = match &src.value {
451 Either::Left(it) => it.name(), 451 Either::Left(it) => it.name(),
diff --git a/crates/ide/src/doc_links.rs b/crates/ide/src/doc_links.rs
index ec3828ab2..7ac0118fe 100644
--- a/crates/ide/src/doc_links.rs
+++ b/crates/ide/src/doc_links.rs
@@ -151,18 +151,18 @@ pub(crate) fn resolve_doc_path_for_def(
151) -> Option<hir::ModuleDef> { 151) -> Option<hir::ModuleDef> {
152 match def { 152 match def {
153 Definition::ModuleDef(def) => match def { 153 Definition::ModuleDef(def) => match def {
154 hir::ModuleDef::Module(it) => it.resolve_doc_path(db, &link, ns), 154 hir::ModuleDef::Module(it) => it.resolve_doc_path(db, link, ns),
155 hir::ModuleDef::Function(it) => it.resolve_doc_path(db, &link, ns), 155 hir::ModuleDef::Function(it) => it.resolve_doc_path(db, link, ns),
156 hir::ModuleDef::Adt(it) => it.resolve_doc_path(db, &link, ns), 156 hir::ModuleDef::Adt(it) => it.resolve_doc_path(db, link, ns),
157 hir::ModuleDef::Variant(it) => it.resolve_doc_path(db, &link, ns), 157 hir::ModuleDef::Variant(it) => it.resolve_doc_path(db, link, ns),
158 hir::ModuleDef::Const(it) => it.resolve_doc_path(db, &link, ns), 158 hir::ModuleDef::Const(it) => it.resolve_doc_path(db, link, ns),
159 hir::ModuleDef::Static(it) => it.resolve_doc_path(db, &link, ns), 159 hir::ModuleDef::Static(it) => it.resolve_doc_path(db, link, ns),
160 hir::ModuleDef::Trait(it) => it.resolve_doc_path(db, &link, ns), 160 hir::ModuleDef::Trait(it) => it.resolve_doc_path(db, link, ns),
161 hir::ModuleDef::TypeAlias(it) => it.resolve_doc_path(db, &link, ns), 161 hir::ModuleDef::TypeAlias(it) => it.resolve_doc_path(db, link, ns),
162 hir::ModuleDef::BuiltinType(_) => None, 162 hir::ModuleDef::BuiltinType(_) => None,
163 }, 163 },
164 Definition::Macro(it) => it.resolve_doc_path(db, &link, ns), 164 Definition::Macro(it) => it.resolve_doc_path(db, link, ns),
165 Definition::Field(it) => it.resolve_doc_path(db, &link, ns), 165 Definition::Field(it) => it.resolve_doc_path(db, link, ns),
166 Definition::SelfType(_) 166 Definition::SelfType(_)
167 | Definition::Local(_) 167 | Definition::Local(_)
168 | Definition::GenericParam(_) 168 | Definition::GenericParam(_)
@@ -192,7 +192,7 @@ pub(crate) fn doc_attributes(
192 ast::TupleField(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::Field(def))), 192 ast::TupleField(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::Field(def))),
193 ast::Macro(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::Macro(def))), 193 ast::Macro(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::Macro(def))),
194 // ast::Use(it) => sema.to_def(&it).map(|def| (Box::new(it) as _, def.attrs(sema.db))), 194 // ast::Use(it) => sema.to_def(&it).map(|def| (Box::new(it) as _, def.attrs(sema.db))),
195 _ => return None 195 _ => None
196 } 196 }
197 } 197 }
198} 198}
@@ -241,6 +241,10 @@ fn get_doc_link(db: &RootDatabase, definition: Definition) -> Option<String> {
241 Definition::ModuleDef(ModuleDef::Module(module)) => module.krate(), 241 Definition::ModuleDef(ModuleDef::Module(module)) => module.krate(),
242 _ => definition.module(db)?.krate(), 242 _ => definition.module(db)?.krate(),
243 }; 243 };
244 // FIXME: using import map doesn't make sense here. What we want here is
245 // canonical path. What import map returns is the shortest path suitable for
246 // import. See this test:
247 cov_mark::hit!(test_reexport_order);
244 let import_map = db.import_map(krate.into()); 248 let import_map = db.import_map(krate.into());
245 249
246 let mut base = krate.display_name(db)?.to_string(); 250 let mut base = krate.display_name(db)?.to_string();
@@ -642,13 +646,15 @@ pub mod foo {
642 ) 646 )
643 } 647 }
644 648
645 // FIXME: ImportMap will return re-export paths instead of public module
646 // paths. The correct path to documentation will never be a re-export.
647 // This problem stops us from resolving stdlib items included in the prelude
648 // such as `Option::Some` correctly.
649 #[ignore = "ImportMap may return re-exports"]
650 #[test] 649 #[test]
651 fn test_reexport_order() { 650 fn test_reexport_order() {
651 cov_mark::check!(test_reexport_order);
652 // FIXME: This should return
653 //
654 // https://docs.rs/test/*/test/wrapper/modulestruct.Item.html
655 //
656 // That is, we should point inside the module, rather than at the
657 // re-export.
652 check( 658 check(
653 r#" 659 r#"
654pub mod wrapper { 660pub mod wrapper {
@@ -663,7 +669,7 @@ fn foo() {
663 let bar: wrapper::It$0em; 669 let bar: wrapper::It$0em;
664} 670}
665 "#, 671 "#,
666 expect![[r#"https://docs.rs/test/*/test/wrapper/module/struct.Item.html"#]], 672 expect![[r#"https://docs.rs/test/*/test/wrapper/struct.Item.html"#]],
667 ) 673 )
668 } 674 }
669} 675}
diff --git a/crates/ide/src/extend_selection.rs b/crates/ide/src/extend_selection.rs
index 7032889ac..c7ec87edf 100644
--- a/crates/ide/src/extend_selection.rs
+++ b/crates/ide/src/extend_selection.rs
@@ -328,7 +328,7 @@ mod tests {
328 use super::*; 328 use super::*;
329 329
330 fn do_check(before: &str, afters: &[&str]) { 330 fn do_check(before: &str, afters: &[&str]) {
331 let (analysis, position) = fixture::position(&before); 331 let (analysis, position) = fixture::position(before);
332 let before = analysis.file_text(position.file_id).unwrap(); 332 let before = analysis.file_text(position.file_id).unwrap();
333 let range = TextRange::empty(position.offset); 333 let range = TextRange::empty(position.offset);
334 let mut frange = FileRange { file_id: position.file_id, range }; 334 let mut frange = FileRange { file_id: position.file_id, range };
diff --git a/crates/ide/src/fixture.rs b/crates/ide/src/fixture.rs
index 6780af617..cf679edd3 100644
--- a/crates/ide/src/fixture.rs
+++ b/crates/ide/src/fixture.rs
@@ -1,6 +1,5 @@
1//! Utilities for creating `Analysis` instances for tests. 1//! Utilities for creating `Analysis` instances for tests.
2use ide_db::base_db::fixture::ChangeFixture; 2use ide_db::base_db::fixture::ChangeFixture;
3use syntax::{TextRange, TextSize};
4use test_utils::extract_annotations; 3use test_utils::extract_annotations;
5 4
6use crate::{Analysis, AnalysisHost, FileId, FilePosition, FileRange}; 5use crate::{Analysis, AnalysisHost, FileId, FilePosition, FileRange};
@@ -13,14 +12,6 @@ pub(crate) fn file(ra_fixture: &str) -> (Analysis, FileId) {
13 (host.analysis(), change_fixture.files[0]) 12 (host.analysis(), change_fixture.files[0])
14} 13}
15 14
16/// Creates analysis for many files.
17pub(crate) fn files(ra_fixture: &str) -> (Analysis, Vec<FileId>) {
18 let mut host = AnalysisHost::default();
19 let change_fixture = ChangeFixture::parse(ra_fixture);
20 host.db.apply_change(change_fixture.change);
21 (host.analysis(), change_fixture.files)
22}
23
24/// Creates analysis from a multi-file fixture, returns positions marked with $0. 15/// Creates analysis from a multi-file fixture, returns positions marked with $0.
25pub(crate) fn position(ra_fixture: &str) -> (Analysis, FilePosition) { 16pub(crate) fn position(ra_fixture: &str) -> (Analysis, FilePosition) {
26 let mut host = AnalysisHost::default(); 17 let mut host = AnalysisHost::default();
@@ -63,15 +54,8 @@ pub(crate) fn annotations(ra_fixture: &str) -> (Analysis, FilePosition, Vec<(Fil
63 54
64pub(crate) fn nav_target_annotation(ra_fixture: &str) -> (Analysis, FilePosition, FileRange) { 55pub(crate) fn nav_target_annotation(ra_fixture: &str) -> (Analysis, FilePosition, FileRange) {
65 let (analysis, position, mut annotations) = annotations(ra_fixture); 56 let (analysis, position, mut annotations) = annotations(ra_fixture);
66 let (mut expected, data) = annotations.pop().unwrap(); 57 let (expected, data) = annotations.pop().unwrap();
67 assert!(annotations.is_empty()); 58 assert!(annotations.is_empty());
68 match data.as_str() { 59 assert_eq!(data, "");
69 "" => (),
70 "file" => {
71 expected.range =
72 TextRange::up_to(TextSize::of(&*analysis.file_text(expected.file_id).unwrap()))
73 }
74 data => panic!("bad data: {}", data),
75 }
76 (analysis, position, expected) 60 (analysis, position, expected)
77} 61}
diff --git a/crates/ide/src/goto_definition.rs b/crates/ide/src/goto_definition.rs
index 2d36c34e9..d8e0dc4d5 100644
--- a/crates/ide/src/goto_definition.rs
+++ b/crates/ide/src/goto_definition.rs
@@ -43,7 +43,7 @@ pub(crate) fn goto_definition(
43 let (docs, doc_mapping) = attributes.docs_with_rangemap(db)?; 43 let (docs, doc_mapping) = attributes.docs_with_rangemap(db)?;
44 let (_, link, ns) = 44 let (_, link, ns) =
45 extract_definitions_from_markdown(docs.as_str()).into_iter().find(|(range, ..)| { 45 extract_definitions_from_markdown(docs.as_str()).into_iter().find(|(range, ..)| {
46 doc_mapping.map(range.clone()).map_or(false, |InFile { file_id, value: range }| { 46 doc_mapping.map(*range).map_or(false, |InFile { file_id, value: range }| {
47 file_id == position.file_id.into() && range.contains(position.offset) 47 file_id == position.file_id.into() && range.contains(position.offset)
48 }) 48 })
49 })?; 49 })?;
@@ -57,7 +57,7 @@ pub(crate) fn goto_definition(
57 }, 57 },
58 ast::Name(name) => { 58 ast::Name(name) => {
59 let def = NameClass::classify(&sema, &name)?.referenced_or_defined(sema.db); 59 let def = NameClass::classify(&sema, &name)?.referenced_or_defined(sema.db);
60 try_find_trait_item_definition(&sema.db, &def) 60 try_find_trait_item_definition(sema.db, &def)
61 .or_else(|| def.try_to_nav(sema.db)) 61 .or_else(|| def.try_to_nav(sema.db))
62 }, 62 },
63 ast::Lifetime(lt) => if let Some(name_class) = NameClass::classify_lifetime(&sema, &lt) { 63 ast::Lifetime(lt) => if let Some(name_class) = NameClass::classify_lifetime(&sema, &lt) {
@@ -185,7 +185,7 @@ mod tests {
185extern crate std$0; 185extern crate std$0;
186//- /std/lib.rs crate:std 186//- /std/lib.rs crate:std
187// empty 187// empty
188//^ file 188//^file
189"#, 189"#,
190 ) 190 )
191 } 191 }
@@ -198,7 +198,7 @@ extern crate std$0;
198extern crate std as abc$0; 198extern crate std as abc$0;
199//- /std/lib.rs crate:std 199//- /std/lib.rs crate:std
200// empty 200// empty
201//^ file 201//^file
202"#, 202"#,
203 ) 203 )
204 } 204 }
@@ -253,7 +253,7 @@ mod $0foo;
253 253
254//- /foo.rs 254//- /foo.rs
255// empty 255// empty
256//^ file 256//^file
257"#, 257"#,
258 ); 258 );
259 259
@@ -264,7 +264,7 @@ mod $0foo;
264 264
265//- /foo/mod.rs 265//- /foo/mod.rs
266// empty 266// empty
267//^ file 267//^file
268"#, 268"#,
269 ); 269 );
270 } 270 }
@@ -395,7 +395,7 @@ use foo as bar$0;
395 395
396//- /foo/lib.rs crate:foo 396//- /foo/lib.rs crate:foo
397// empty 397// empty
398//^ file 398//^file
399"#, 399"#,
400 ); 400 );
401 } 401 }
@@ -1130,15 +1130,15 @@ fn foo<'foobar>(_: &'foobar ()) {
1130 } 1130 }
1131 1131
1132 #[test] 1132 #[test]
1133 #[ignore] // requires the HIR to somehow track these hrtb lifetimes
1134 fn goto_lifetime_hrtb() { 1133 fn goto_lifetime_hrtb() {
1135 check( 1134 // FIXME: requires the HIR to somehow track these hrtb lifetimes
1135 check_unresolved(
1136 r#"trait Foo<T> {} 1136 r#"trait Foo<T> {}
1137fn foo<T>() where for<'a> T: Foo<&'a$0 (u8, u16)>, {} 1137fn foo<T>() where for<'a> T: Foo<&'a$0 (u8, u16)>, {}
1138 //^^ 1138 //^^
1139"#, 1139"#,
1140 ); 1140 );
1141 check( 1141 check_unresolved(
1142 r#"trait Foo<T> {} 1142 r#"trait Foo<T> {}
1143fn foo<T>() where for<'a$0> T: Foo<&'a (u8, u16)>, {} 1143fn foo<T>() where for<'a$0> T: Foo<&'a (u8, u16)>, {}
1144 //^^ 1144 //^^
@@ -1147,9 +1147,9 @@ fn foo<T>() where for<'a$0> T: Foo<&'a (u8, u16)>, {}
1147 } 1147 }
1148 1148
1149 #[test] 1149 #[test]
1150 #[ignore] // requires ForTypes to be implemented
1151 fn goto_lifetime_hrtb_for_type() { 1150 fn goto_lifetime_hrtb_for_type() {
1152 check( 1151 // FIXME: requires ForTypes to be implemented
1152 check_unresolved(
1153 r#"trait Foo<T> {} 1153 r#"trait Foo<T> {}
1154fn foo<T>() where T: for<'a> Foo<&'a$0 (u8, u16)>, {} 1154fn foo<T>() where T: for<'a> Foo<&'a$0 (u8, u16)>, {}
1155 //^^ 1155 //^^
@@ -1287,7 +1287,7 @@ fn main() {
1287} 1287}
1288//- /foo.txt 1288//- /foo.txt
1289// empty 1289// empty
1290//^ file 1290//^file
1291"#, 1291"#,
1292 ); 1292 );
1293 } 1293 }
diff --git a/crates/ide/src/goto_implementation.rs b/crates/ide/src/goto_implementation.rs
index 95fd39850..0013820b4 100644
--- a/crates/ide/src/goto_implementation.rs
+++ b/crates/ide/src/goto_implementation.rs
@@ -87,7 +87,7 @@ fn impls_for_trait_item(
87 .filter_map(|imp| { 87 .filter_map(|imp| {
88 let item = imp.items(sema.db).iter().find_map(|itm| { 88 let item = imp.items(sema.db).iter().find_map(|itm| {
89 let itm_name = itm.name(sema.db)?; 89 let itm_name = itm.name(sema.db)?;
90 (itm_name == fun_name).then(|| itm.clone()) 90 (itm_name == fun_name).then(|| *itm)
91 })?; 91 })?;
92 item.try_to_nav(sema.db) 92 item.try_to_nav(sema.db)
93 }) 93 })
diff --git a/crates/ide/src/goto_type_definition.rs b/crates/ide/src/goto_type_definition.rs
index 004d9cb68..ca3c02bf6 100644
--- a/crates/ide/src/goto_type_definition.rs
+++ b/crates/ide/src/goto_type_definition.rs
@@ -25,7 +25,7 @@ pub(crate) fn goto_type_definition(
25 let token: SyntaxToken = pick_best(file.syntax().token_at_offset(position.offset))?; 25 let token: SyntaxToken = pick_best(file.syntax().token_at_offset(position.offset))?;
26 let token: SyntaxToken = sema.descend_into_macros(token); 26 let token: SyntaxToken = sema.descend_into_macros(token);
27 27
28 let (ty, node) = sema.token_ancestors_with_macros(token).find_map(|node| { 28 let (ty, node) = sema.token_ancestors_with_macros(token.clone()).find_map(|node| {
29 let ty = match_ast! { 29 let ty = match_ast! {
30 match node { 30 match node {
31 ast::Expr(it) => sema.type_of_expr(&it)?, 31 ast::Expr(it) => sema.type_of_expr(&it)?,
@@ -33,13 +33,23 @@ pub(crate) fn goto_type_definition(
33 ast::SelfParam(it) => sema.type_of_self(&it)?, 33 ast::SelfParam(it) => sema.type_of_self(&it)?,
34 ast::Type(it) => sema.resolve_type(&it)?, 34 ast::Type(it) => sema.resolve_type(&it)?,
35 ast::RecordField(it) => sema.to_def(&it).map(|d| d.ty(db.upcast()))?, 35 ast::RecordField(it) => sema.to_def(&it).map(|d| d.ty(db.upcast()))?,
36 ast::RecordField(it) => sema.to_def(&it).map(|d| d.ty(db.upcast()))?,
37 // can't match on RecordExprField directly as `ast::Expr` will match an iteration too early otherwise
38 ast::NameRef(it) => {
39 if let Some(record_field) = ast::RecordExprField::for_name_ref(&it) {
40 let (_, _, ty) = sema.resolve_record_field(&record_field)?;
41 ty
42 } else {
43 let record_field = ast::RecordPatField::for_field_name_ref(&it)?;
44 sema.resolve_record_pat_field(&record_field)?.ty(db)
45 }
46 },
36 _ => return None, 47 _ => return None,
37 } 48 }
38 }; 49 };
39 50
40 Some((ty, node)) 51 Some((ty, node))
41 })?; 52 })?;
42
43 let adt_def = ty.autoderef(db).filter_map(|ty| ty.as_adt()).last()?; 53 let adt_def = ty.autoderef(db).filter_map(|ty| ty.as_adt()).last()?;
44 54
45 let nav = adt_def.try_to_nav(db)?; 55 let nav = adt_def.try_to_nav(db)?;
@@ -88,6 +98,54 @@ fn foo() {
88 } 98 }
89 99
90 #[test] 100 #[test]
101 fn goto_type_definition_record_expr_field() {
102 check(
103 r#"
104struct Bar;
105 // ^^^
106struct Foo { foo: Bar }
107fn foo() {
108 Foo { foo$0 }
109}
110"#,
111 );
112 check(
113 r#"
114struct Bar;
115 // ^^^
116struct Foo { foo: Bar }
117fn foo() {
118 Foo { foo$0: Bar }
119}
120"#,
121 );
122 }
123
124 #[test]
125 fn goto_type_definition_record_pat_field() {
126 check(
127 r#"
128struct Bar;
129 // ^^^
130struct Foo { foo: Bar }
131fn foo() {
132 let Foo { foo$0 };
133}
134"#,
135 );
136 check(
137 r#"
138struct Bar;
139 // ^^^
140struct Foo { foo: Bar }
141fn foo() {
142 let Foo { foo$0: bar };
143}
144"#,
145 );
146 }
147
148 #[test]
91 fn goto_type_definition_works_simple_ref() { 149 fn goto_type_definition_works_simple_ref() {
92 check( 150 check(
93 r#" 151 r#"
diff --git a/crates/ide/src/hover.rs b/crates/ide/src/hover.rs
index 1c6d36939..529cf5f33 100644
--- a/crates/ide/src/hover.rs
+++ b/crates/ide/src/hover.rs
@@ -131,7 +131,7 @@ pub(crate) fn hover(
131 let (docs, doc_mapping) = attributes.docs_with_rangemap(db)?; 131 let (docs, doc_mapping) = attributes.docs_with_rangemap(db)?;
132 let (idl_range, link, ns) = 132 let (idl_range, link, ns) =
133 extract_definitions_from_markdown(docs.as_str()).into_iter().find_map(|(range, link, ns)| { 133 extract_definitions_from_markdown(docs.as_str()).into_iter().find_map(|(range, link, ns)| {
134 let InFile { file_id, value: range } = doc_mapping.map(range.clone())?; 134 let InFile { file_id, value: range } = doc_mapping.map(range)?;
135 if file_id == position.file_id.into() && range.contains(position.offset) { 135 if file_id == position.file_id.into() && range.contains(position.offset) {
136 Some((range, link, ns)) 136 Some((range, link, ns))
137 } else { 137 } else {
@@ -288,7 +288,7 @@ fn runnable_action(
288) -> Option<HoverAction> { 288) -> Option<HoverAction> {
289 match def { 289 match def {
290 Definition::ModuleDef(it) => match it { 290 Definition::ModuleDef(it) => match it {
291 ModuleDef::Module(it) => runnable_mod(&sema, it).map(|it| HoverAction::Runnable(it)), 291 ModuleDef::Module(it) => runnable_mod(sema, it).map(HoverAction::Runnable),
292 ModuleDef::Function(func) => { 292 ModuleDef::Function(func) => {
293 let src = func.source(sema.db)?; 293 let src = func.source(sema.db)?;
294 if src.file_id != file_id.into() { 294 if src.file_id != file_id.into() {
@@ -297,7 +297,7 @@ fn runnable_action(
297 return None; 297 return None;
298 } 298 }
299 299
300 runnable_fn(&sema, func).map(HoverAction::Runnable) 300 runnable_fn(sema, func).map(HoverAction::Runnable)
301 } 301 }
302 _ => None, 302 _ => None,
303 }, 303 },
@@ -432,7 +432,7 @@ fn hover_for_definition(
432 return match def { 432 return match def {
433 Definition::Macro(it) => match &it.source(db)?.value { 433 Definition::Macro(it) => match &it.source(db)?.value {
434 Either::Left(mac) => { 434 Either::Left(mac) => {
435 let label = macro_label(&mac); 435 let label = macro_label(mac);
436 from_def_source_labeled(db, it, Some(label), mod_path) 436 from_def_source_labeled(db, it, Some(label), mod_path)
437 } 437 }
438 Either::Right(_) => { 438 Either::Right(_) => {
@@ -516,7 +516,7 @@ fn hover_for_keyword(
516 if !token.kind().is_keyword() { 516 if !token.kind().is_keyword() {
517 return None; 517 return None;
518 } 518 }
519 let famous_defs = FamousDefs(&sema, sema.scope(&token.parent()?).krate()); 519 let famous_defs = FamousDefs(sema, sema.scope(&token.parent()?).krate());
520 // std exposes {}_keyword modules with docstrings on the root to document keywords 520 // std exposes {}_keyword modules with docstrings on the root to document keywords
521 let keyword_mod = format!("{}_keyword", token.text()); 521 let keyword_mod = format!("{}_keyword", token.text());
522 let doc_owner = find_std_module(&famous_defs, &keyword_mod)?; 522 let doc_owner = find_std_module(&famous_defs, &keyword_mod)?;
@@ -1821,9 +1821,10 @@ pub struct B$0ar
1821 ); 1821 );
1822 } 1822 }
1823 1823
1824 #[ignore = "path based links currently only support documentation on ModuleDef items"]
1825 #[test] 1824 #[test]
1826 fn test_hover_path_link_field() { 1825 fn test_hover_path_link_field() {
1826 // FIXME: Should be
1827 // [Foo](https://docs.rs/test/*/test/struct.Foo.html)
1827 check( 1828 check(
1828 r#" 1829 r#"
1829pub struct Foo; 1830pub struct Foo;
@@ -1845,7 +1846,7 @@ pub struct Bar {
1845 1846
1846 --- 1847 ---
1847 1848
1848 [Foo](https://docs.rs/test/*/test/struct.Foo.html) 1849 [Foo](struct.Foo.html)
1849 "#]], 1850 "#]],
1850 ); 1851 );
1851 } 1852 }
@@ -2999,29 +3000,24 @@ fn foo(ar$0g: &impl Foo + Bar<S>) {}
2999 fn test_hover_async_block_impl_trait_has_goto_type_action() { 3000 fn test_hover_async_block_impl_trait_has_goto_type_action() {
3000 check_actions( 3001 check_actions(
3001 r#" 3002 r#"
3003//- minicore: future
3002struct S; 3004struct S;
3003fn foo() { 3005fn foo() {
3004 let fo$0o = async { S }; 3006 let fo$0o = async { S };
3005} 3007}
3006
3007#[prelude_import] use future::*;
3008mod future {
3009 #[lang = "future_trait"]
3010 pub trait Future { type Output; }
3011}
3012"#, 3008"#,
3013 expect![[r#" 3009 expect![[r#"
3014 [ 3010 [
3015 GoToType( 3011 GoToType(
3016 [ 3012 [
3017 HoverGotoTypeData { 3013 HoverGotoTypeData {
3018 mod_path: "test::future::Future", 3014 mod_path: "core::future::Future",
3019 nav: NavigationTarget { 3015 nav: NavigationTarget {
3020 file_id: FileId( 3016 file_id: FileId(
3021 0, 3017 1,
3022 ), 3018 ),
3023 full_range: 101..163, 3019 full_range: 245..427,
3024 focus_range: 140..146, 3020 focus_range: 284..290,
3025 name: "Future", 3021 name: "Future",
3026 kind: Trait, 3022 kind: Trait,
3027 description: "pub trait Future", 3023 description: "pub trait Future",
diff --git a/crates/ide/src/inlay_hints.rs b/crates/ide/src/inlay_hints.rs
index 821c61403..9cd33d0e4 100644
--- a/crates/ide/src/inlay_hints.rs
+++ b/crates/ide/src/inlay_hints.rs
@@ -96,7 +96,7 @@ fn get_chaining_hints(
96 } 96 }
97 97
98 let krate = sema.scope(expr.syntax()).module().map(|it| it.krate()); 98 let krate = sema.scope(expr.syntax()).module().map(|it| it.krate());
99 let famous_defs = FamousDefs(&sema, krate); 99 let famous_defs = FamousDefs(sema, krate);
100 100
101 let mut tokens = expr 101 let mut tokens = expr
102 .syntax() 102 .syntax()
@@ -165,7 +165,7 @@ fn get_param_name_hints(
165 }; 165 };
166 Some((param_name, arg)) 166 Some((param_name, arg))
167 }) 167 })
168 .filter(|(param_name, arg)| !should_hide_param_name_hint(sema, &callable, param_name, &arg)) 168 .filter(|(param_name, arg)| !should_hide_param_name_hint(sema, &callable, param_name, arg))
169 .map(|(param_name, arg)| InlayHint { 169 .map(|(param_name, arg)| InlayHint {
170 range: arg.syntax().text_range(), 170 range: arg.syntax().text_range(),
171 kind: InlayKind::ParameterHint, 171 kind: InlayKind::ParameterHint,
@@ -187,7 +187,7 @@ fn get_bind_pat_hints(
187 } 187 }
188 188
189 let krate = sema.scope(pat.syntax()).module().map(|it| it.krate()); 189 let krate = sema.scope(pat.syntax()).module().map(|it| it.krate());
190 let famous_defs = FamousDefs(&sema, krate); 190 let famous_defs = FamousDefs(sema, krate);
191 191
192 let ty = sema.type_of_pat(&pat.clone().into())?; 192 let ty = sema.type_of_pat(&pat.clone().into())?;
193 193
diff --git a/crates/ide/src/join_lines.rs b/crates/ide/src/join_lines.rs
index c67ccd1a9..93d3760bf 100644
--- a/crates/ide/src/join_lines.rs
+++ b/crates/ide/src/join_lines.rs
@@ -60,7 +60,7 @@ fn remove_newlines(edit: &mut TextEditBuilder, token: &SyntaxToken, range: TextR
60 let pos: TextSize = (pos as u32).into(); 60 let pos: TextSize = (pos as u32).into();
61 let offset = token.text_range().start() + range.start() + pos; 61 let offset = token.text_range().start() + range.start() + pos;
62 if !edit.invalidates_offset(offset) { 62 if !edit.invalidates_offset(offset) {
63 remove_newline(edit, &token, offset); 63 remove_newline(edit, token, offset);
64 } 64 }
65 } 65 }
66} 66}
diff --git a/crates/ide/src/lib.rs b/crates/ide/src/lib.rs
index 97c9e5d2b..4bd073cc3 100644
--- a/crates/ide/src/lib.rs
+++ b/crates/ide/src/lib.rs
@@ -24,7 +24,6 @@ mod display;
24 24
25mod annotations; 25mod annotations;
26mod call_hierarchy; 26mod call_hierarchy;
27mod diagnostics;
28mod expand_macro; 27mod expand_macro;
29mod extend_selection; 28mod extend_selection;
30mod file_structure; 29mod file_structure;
@@ -40,6 +39,7 @@ mod matching_brace;
40mod move_item; 39mod move_item;
41mod parent_module; 40mod parent_module;
42mod references; 41mod references;
42mod rename;
43mod fn_references; 43mod fn_references;
44mod runnables; 44mod runnables;
45mod ssr; 45mod ssr;
@@ -71,7 +71,6 @@ use crate::display::ToNav;
71pub use crate::{ 71pub use crate::{
72 annotations::{Annotation, AnnotationConfig, AnnotationKind}, 72 annotations::{Annotation, AnnotationConfig, AnnotationKind},
73 call_hierarchy::CallItem, 73 call_hierarchy::CallItem,
74 diagnostics::{Diagnostic, DiagnosticsConfig, Severity},
75 display::navigation_target::NavigationTarget, 74 display::navigation_target::NavigationTarget,
76 expand_macro::ExpandedMacro, 75 expand_macro::ExpandedMacro,
77 file_structure::{StructureNode, StructureNodeKind}, 76 file_structure::{StructureNode, StructureNodeKind},
@@ -81,7 +80,8 @@ pub use crate::{
81 markup::Markup, 80 markup::Markup,
82 move_item::Direction, 81 move_item::Direction,
83 prime_caches::PrimeCachesProgress, 82 prime_caches::PrimeCachesProgress,
84 references::{rename::RenameError, ReferenceSearchResult}, 83 references::ReferenceSearchResult,
84 rename::RenameError,
85 runnables::{Runnable, RunnableKind, TestId}, 85 runnables::{Runnable, RunnableKind, TestId},
86 syntax_highlighting::{ 86 syntax_highlighting::{
87 tags::{Highlight, HlMod, HlMods, HlOperator, HlPunct, HlTag}, 87 tags::{Highlight, HlMod, HlMods, HlOperator, HlPunct, HlTag},
@@ -109,6 +109,7 @@ pub use ide_db::{
109 symbol_index::Query, 109 symbol_index::Query,
110 RootDatabase, SymbolKind, 110 RootDatabase, SymbolKind,
111}; 111};
112pub use ide_diagnostics::{Diagnostic, DiagnosticsConfig, Severity};
112pub use ide_ssr::SsrError; 113pub use ide_ssr::SsrError;
113pub use syntax::{TextRange, TextSize}; 114pub use syntax::{TextRange, TextSize};
114pub use text_edit::{Indel, TextEdit}; 115pub use text_edit::{Indel, TextEdit};
@@ -282,20 +283,20 @@ impl Analysis {
282 file_id: FileId, 283 file_id: FileId,
283 text_range: Option<TextRange>, 284 text_range: Option<TextRange>,
284 ) -> Cancellable<String> { 285 ) -> Cancellable<String> {
285 self.with_db(|db| syntax_tree::syntax_tree(&db, file_id, text_range)) 286 self.with_db(|db| syntax_tree::syntax_tree(db, file_id, text_range))
286 } 287 }
287 288
288 pub fn view_hir(&self, position: FilePosition) -> Cancellable<String> { 289 pub fn view_hir(&self, position: FilePosition) -> Cancellable<String> {
289 self.with_db(|db| view_hir::view_hir(&db, position)) 290 self.with_db(|db| view_hir::view_hir(db, position))
290 } 291 }
291 292
292 pub fn view_item_tree(&self, file_id: FileId) -> Cancellable<String> { 293 pub fn view_item_tree(&self, file_id: FileId) -> Cancellable<String> {
293 self.with_db(|db| view_item_tree::view_item_tree(&db, file_id)) 294 self.with_db(|db| view_item_tree::view_item_tree(db, file_id))
294 } 295 }
295 296
296 /// Renders the crate graph to GraphViz "dot" syntax. 297 /// Renders the crate graph to GraphViz "dot" syntax.
297 pub fn view_crate_graph(&self) -> Cancellable<Result<String, String>> { 298 pub fn view_crate_graph(&self) -> Cancellable<Result<String, String>> {
298 self.with_db(|db| view_crate_graph::view_crate_graph(&db)) 299 self.with_db(|db| view_crate_graph::view_crate_graph(db))
299 } 300 }
300 301
301 pub fn expand_macro(&self, position: FilePosition) -> Cancellable<Option<ExpandedMacro>> { 302 pub fn expand_macro(&self, position: FilePosition) -> Cancellable<Option<ExpandedMacro>> {
@@ -315,7 +316,7 @@ impl Analysis {
315 /// up minor stuff like continuing the comment. 316 /// up minor stuff like continuing the comment.
316 /// The edit will be a snippet (with `$0`). 317 /// The edit will be a snippet (with `$0`).
317 pub fn on_enter(&self, position: FilePosition) -> Cancellable<Option<TextEdit>> { 318 pub fn on_enter(&self, position: FilePosition) -> Cancellable<Option<TextEdit>> {
318 self.with_db(|db| typing::on_enter(&db, position)) 319 self.with_db(|db| typing::on_enter(db, position))
319 } 320 }
320 321
321 /// Returns an edit which should be applied after a character was typed. 322 /// Returns an edit which should be applied after a character was typed.
@@ -331,7 +332,7 @@ impl Analysis {
331 if !typing::TRIGGER_CHARS.contains(char_typed) { 332 if !typing::TRIGGER_CHARS.contains(char_typed) {
332 return Ok(None); 333 return Ok(None);
333 } 334 }
334 self.with_db(|db| typing::on_char_typed(&db, position, char_typed)) 335 self.with_db(|db| typing::on_char_typed(db, position, char_typed))
335 } 336 }
336 337
337 /// Returns a tree representation of symbols in the file. Useful to draw a 338 /// Returns a tree representation of symbols in the file. Useful to draw a
@@ -536,7 +537,7 @@ impl Analysis {
536 ) -> Cancellable<Vec<Assist>> { 537 ) -> Cancellable<Vec<Assist>> {
537 self.with_db(|db| { 538 self.with_db(|db| {
538 let ssr_assists = ssr::ssr_assists(db, &resolve, frange); 539 let ssr_assists = ssr::ssr_assists(db, &resolve, frange);
539 let mut acc = Assist::get(db, config, resolve, frange); 540 let mut acc = ide_assists::assists(db, config, resolve, frange);
540 acc.extend(ssr_assists.into_iter()); 541 acc.extend(ssr_assists.into_iter());
541 acc 542 acc
542 }) 543 })
@@ -549,7 +550,7 @@ impl Analysis {
549 resolve: AssistResolveStrategy, 550 resolve: AssistResolveStrategy,
550 file_id: FileId, 551 file_id: FileId,
551 ) -> Cancellable<Vec<Diagnostic>> { 552 ) -> Cancellable<Vec<Diagnostic>> {
552 self.with_db(|db| diagnostics::diagnostics(db, config, &resolve, file_id)) 553 self.with_db(|db| ide_diagnostics::diagnostics(db, config, &resolve, file_id))
553 } 554 }
554 555
555 /// Convenience function to return assists + quick fixes for diagnostics 556 /// Convenience function to return assists + quick fixes for diagnostics
@@ -566,9 +567,8 @@ impl Analysis {
566 }; 567 };
567 568
568 self.with_db(|db| { 569 self.with_db(|db| {
569 let ssr_assists = ssr::ssr_assists(db, &resolve, frange);
570 let diagnostic_assists = if include_fixes { 570 let diagnostic_assists = if include_fixes {
571 diagnostics::diagnostics(db, diagnostics_config, &resolve, frange.file_id) 571 ide_diagnostics::diagnostics(db, diagnostics_config, &resolve, frange.file_id)
572 .into_iter() 572 .into_iter()
573 .flat_map(|it| it.fixes.unwrap_or_default()) 573 .flat_map(|it| it.fixes.unwrap_or_default())
574 .filter(|it| it.target.intersect(frange.range).is_some()) 574 .filter(|it| it.target.intersect(frange.range).is_some())
@@ -576,10 +576,12 @@ impl Analysis {
576 } else { 576 } else {
577 Vec::new() 577 Vec::new()
578 }; 578 };
579 let ssr_assists = ssr::ssr_assists(db, &resolve, frange);
580 let assists = ide_assists::assists(db, assist_config, resolve, frange);
579 581
580 let mut res = Assist::get(db, assist_config, resolve, frange); 582 let mut res = diagnostic_assists;
581 res.extend(ssr_assists.into_iter()); 583 res.extend(ssr_assists.into_iter());
582 res.extend(diagnostic_assists.into_iter()); 584 res.extend(assists.into_iter());
583 585
584 res 586 res
585 }) 587 })
@@ -592,14 +594,14 @@ impl Analysis {
592 position: FilePosition, 594 position: FilePosition,
593 new_name: &str, 595 new_name: &str,
594 ) -> Cancellable<Result<SourceChange, RenameError>> { 596 ) -> Cancellable<Result<SourceChange, RenameError>> {
595 self.with_db(|db| references::rename::rename(db, position, new_name)) 597 self.with_db(|db| rename::rename(db, position, new_name))
596 } 598 }
597 599
598 pub fn prepare_rename( 600 pub fn prepare_rename(
599 &self, 601 &self,
600 position: FilePosition, 602 position: FilePosition,
601 ) -> Cancellable<Result<RangeInfo<()>, RenameError>> { 603 ) -> Cancellable<Result<RangeInfo<()>, RenameError>> {
602 self.with_db(|db| references::rename::prepare_rename(db, position)) 604 self.with_db(|db| rename::prepare_rename(db, position))
603 } 605 }
604 606
605 pub fn will_rename_file( 607 pub fn will_rename_file(
@@ -607,7 +609,7 @@ impl Analysis {
607 file_id: FileId, 609 file_id: FileId,
608 new_name_stem: &str, 610 new_name_stem: &str,
609 ) -> Cancellable<Option<SourceChange>> { 611 ) -> Cancellable<Option<SourceChange>> {
610 self.with_db(|db| references::rename::will_rename_file(db, file_id, new_name_stem)) 612 self.with_db(|db| rename::will_rename_file(db, file_id, new_name_stem))
611 } 613 }
612 614
613 pub fn structural_search_replace( 615 pub fn structural_search_replace(
diff --git a/crates/ide/src/references.rs b/crates/ide/src/references.rs
index f8b64a669..945c9b9e1 100644
--- a/crates/ide/src/references.rs
+++ b/crates/ide/src/references.rs
@@ -9,8 +9,6 @@
9//! at the index that the match starts at and its tree parent is 9//! at the index that the match starts at and its tree parent is
10//! resolved to the search element definition, we get a reference. 10//! resolved to the search element definition, we get a reference.
11 11
12pub(crate) mod rename;
13
14use hir::{PathResolution, Semantics}; 12use hir::{PathResolution, Semantics};
15use ide_db::{ 13use ide_db::{
16 base_db::FileId, 14 base_db::FileId,
@@ -62,7 +60,7 @@ pub(crate) fn find_all_refs(
62 if let Some(name) = get_name_of_item_declaration(&syntax, position) { 60 if let Some(name) = get_name_of_item_declaration(&syntax, position) {
63 (NameClass::classify(sema, &name)?.referenced_or_defined(sema.db), true) 61 (NameClass::classify(sema, &name)?.referenced_or_defined(sema.db), true)
64 } else { 62 } else {
65 (find_def(&sema, &syntax, position)?, false) 63 (find_def(sema, &syntax, position)?, false)
66 }; 64 };
67 65
68 let mut usages = def.usages(sema).set_scope(search_scope).include_self_refs().all(); 66 let mut usages = def.usages(sema).set_scope(search_scope).include_self_refs().all();
diff --git a/crates/ide/src/references/rename.rs b/crates/ide/src/rename.rs
index 7dfc5043e..8096dfa0e 100644
--- a/crates/ide/src/references/rename.rs
+++ b/crates/ide/src/rename.rs
@@ -1,45 +1,25 @@
1//! Renaming functionality 1//! Renaming functionality.
2//! 2//!
3//! All reference and file rename requests go through here where the corresponding [`SourceChange`]s 3//! This is mostly front-end for [`ide_db::rename`], but it also includes the
4//! will be calculated. 4//! tests. This module also implements a couple of magic tricks, like renaming
5use std::fmt::{self, Display}; 5//! `self` and to `self` (to switch between associated function and method).
6 6use hir::{AsAssocItem, InFile, Semantics};
7use either::Either;
8use hir::{AsAssocItem, InFile, Module, ModuleDef, ModuleSource, Semantics};
9use ide_db::{ 7use ide_db::{
10 base_db::{AnchoredPathBuf, FileId}, 8 base_db::FileId,
11 defs::{Definition, NameClass, NameRefClass}, 9 defs::{Definition, NameClass, NameRefClass},
12 search::FileReference, 10 rename::{bail, format_err, source_edit_from_references, IdentifierKind},
13 RootDatabase, 11 RootDatabase,
14}; 12};
15use stdx::never; 13use stdx::never;
16use syntax::{ 14use syntax::{ast, AstNode, SyntaxNode};
17 ast::{self, NameOwner},
18 lex_single_syntax_kind, AstNode, SyntaxKind, SyntaxNode, T,
19};
20 15
21use text_edit::TextEdit; 16use text_edit::TextEdit;
22 17
23use crate::{display::TryToNav, FilePosition, FileSystemEdit, RangeInfo, SourceChange, TextRange}; 18use crate::{FilePosition, RangeInfo, SourceChange};
24
25type RenameResult<T> = Result<T, RenameError>;
26#[derive(Debug)]
27pub struct RenameError(String);
28
29impl fmt::Display for RenameError {
30 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31 Display::fmt(&self.0, f)
32 }
33}
34 19
35macro_rules! format_err { 20pub use ide_db::rename::RenameError;
36 ($fmt:expr) => {RenameError(format!($fmt))};
37 ($fmt:expr, $($arg:tt)+) => {RenameError(format!($fmt, $($arg)+))}
38}
39 21
40macro_rules! bail { 22type RenameResult<T> = Result<T, RenameError>;
41 ($($tokens:tt)*) => {return Err(format_err!($($tokens)*))}
42}
43 23
44/// Prepares a rename. The sole job of this function is to return the TextRange of the thing that is 24/// Prepares a rename. The sole job of this function is to return the TextRange of the thing that is
45/// being targeted for a rename. 25/// being targeted for a rename.
@@ -52,26 +32,10 @@ pub(crate) fn prepare_rename(
52 let syntax = source_file.syntax(); 32 let syntax = source_file.syntax();
53 33
54 let def = find_definition(&sema, syntax, position)?; 34 let def = find_definition(&sema, syntax, position)?;
55 match def { 35 let frange = def
56 Definition::SelfType(_) => bail!("Cannot rename `Self`"), 36 .range_for_rename(&sema)
57 Definition::ModuleDef(ModuleDef::BuiltinType(_)) => bail!("Cannot rename builtin type"),
58 Definition::ModuleDef(ModuleDef::Module(_)) => (),
59 _ => {
60 let nav = def
61 .try_to_nav(sema.db)
62 .ok_or_else(|| format_err!("No references found at position"))?;
63 nav.focus_range.ok_or_else(|| format_err!("No identifier available to rename"))?;
64 }
65 };
66 let name_like = sema
67 .find_node_at_offset_with_descend(&syntax, position.offset)
68 .ok_or_else(|| format_err!("No references found at position"))?; 37 .ok_or_else(|| format_err!("No references found at position"))?;
69 let node = match &name_like { 38 Ok(RangeInfo::new(frange.range, ()))
70 ast::NameLike::Name(it) => it.syntax(),
71 ast::NameLike::NameRef(it) => it.syntax(),
72 ast::NameLike::Lifetime(it) => it.syntax(),
73 };
74 Ok(RangeInfo::new(sema.original_range(node).range, ()))
75} 39}
76 40
77// Feature: Rename 41// Feature: Rename
@@ -103,12 +67,19 @@ pub(crate) fn rename_with_semantics(
103 let syntax = source_file.syntax(); 67 let syntax = source_file.syntax();
104 68
105 let def = find_definition(sema, syntax, position)?; 69 let def = find_definition(sema, syntax, position)?;
106 match def { 70
107 Definition::ModuleDef(ModuleDef::Module(module)) => rename_mod(&sema, module, new_name), 71 if let Definition::Local(local) = def {
108 Definition::SelfType(_) => bail!("Cannot rename `Self`"), 72 if let Some(self_param) = local.as_self_param(sema.db) {
109 Definition::ModuleDef(ModuleDef::BuiltinType(_)) => bail!("Cannot rename builtin type"), 73 cov_mark::hit!(rename_self_to_param);
110 def => rename_reference(sema, def, new_name), 74 return rename_self_to_param(sema, local, self_param, new_name);
75 }
76 if new_name == "self" {
77 cov_mark::hit!(rename_to_self);
78 return rename_to_self(sema, local);
79 }
111 } 80 }
81
82 def.rename(sema, new_name)
112} 83}
113 84
114/// Called by the client when it is about to rename a file. 85/// Called by the client when it is about to rename a file.
@@ -119,38 +90,12 @@ pub(crate) fn will_rename_file(
119) -> Option<SourceChange> { 90) -> Option<SourceChange> {
120 let sema = Semantics::new(db); 91 let sema = Semantics::new(db);
121 let module = sema.to_module_def(file_id)?; 92 let module = sema.to_module_def(file_id)?;
122 let mut change = rename_mod(&sema, module, new_name_stem).ok()?; 93 let def = Definition::ModuleDef(module.into());
94 let mut change = def.rename(&sema, new_name_stem).ok()?;
123 change.file_system_edits.clear(); 95 change.file_system_edits.clear();
124 Some(change) 96 Some(change)
125} 97}
126 98
127#[derive(Copy, Clone, Debug, PartialEq)]
128enum IdentifierKind {
129 Ident,
130 Lifetime,
131 ToSelf,
132 Underscore,
133}
134
135fn check_identifier(new_name: &str) -> RenameResult<IdentifierKind> {
136 match lex_single_syntax_kind(new_name) {
137 Some(res) => match res {
138 (SyntaxKind::IDENT, _) => Ok(IdentifierKind::Ident),
139 (T![_], _) => Ok(IdentifierKind::Underscore),
140 (T![self], _) => Ok(IdentifierKind::ToSelf),
141 (SyntaxKind::LIFETIME_IDENT, _) if new_name != "'static" && new_name != "'_" => {
142 Ok(IdentifierKind::Lifetime)
143 }
144 (SyntaxKind::LIFETIME_IDENT, _) => {
145 bail!("Invalid name `{}`: not a lifetime identifier", new_name)
146 }
147 (_, Some(syntax_error)) => bail!("Invalid name `{}`: {}", new_name, syntax_error),
148 (_, None) => bail!("Invalid name `{}`: not an identifier", new_name),
149 },
150 None => bail!("Invalid name `{}`: not an identifier", new_name),
151 }
152}
153
154fn find_definition( 99fn find_definition(
155 sema: &Semantics<RootDatabase>, 100 sema: &Semantics<RootDatabase>,
156 syntax: &SyntaxNode, 101 syntax: &SyntaxNode,
@@ -192,145 +137,6 @@ fn find_definition(
192 .ok_or_else(|| format_err!("No references found at position")) 137 .ok_or_else(|| format_err!("No references found at position"))
193} 138}
194 139
195fn rename_mod(
196 sema: &Semantics<RootDatabase>,
197 module: Module,
198 new_name: &str,
199) -> RenameResult<SourceChange> {
200 if IdentifierKind::Ident != check_identifier(new_name)? {
201 bail!("Invalid name `{0}`: cannot rename module to {0}", new_name);
202 }
203
204 let mut source_change = SourceChange::default();
205
206 let InFile { file_id, value: def_source } = module.definition_source(sema.db);
207 let file_id = file_id.original_file(sema.db);
208 if let ModuleSource::SourceFile(..) = def_source {
209 // mod is defined in path/to/dir/mod.rs
210 let path = if module.is_mod_rs(sema.db) {
211 format!("../{}/mod.rs", new_name)
212 } else {
213 format!("{}.rs", new_name)
214 };
215 let dst = AnchoredPathBuf { anchor: file_id, path };
216 let move_file = FileSystemEdit::MoveFile { src: file_id, dst };
217 source_change.push_file_system_edit(move_file);
218 }
219
220 if let Some(InFile { file_id, value: decl_source }) = module.declaration_source(sema.db) {
221 let file_id = file_id.original_file(sema.db);
222 match decl_source.name() {
223 Some(name) => source_change.insert_source_edit(
224 file_id,
225 TextEdit::replace(name.syntax().text_range(), new_name.to_string()),
226 ),
227 _ => never!("Module source node is missing a name"),
228 }
229 }
230 let def = Definition::ModuleDef(ModuleDef::Module(module));
231 let usages = def.usages(sema).all();
232 let ref_edits = usages.iter().map(|(&file_id, references)| {
233 (file_id, source_edit_from_references(references, def, new_name))
234 });
235 source_change.extend(ref_edits);
236
237 Ok(source_change)
238}
239
240fn rename_reference(
241 sema: &Semantics<RootDatabase>,
242 mut def: Definition,
243 new_name: &str,
244) -> RenameResult<SourceChange> {
245 let ident_kind = check_identifier(new_name)?;
246
247 if matches!(
248 def, // is target a lifetime?
249 Definition::GenericParam(hir::GenericParam::LifetimeParam(_)) | Definition::Label(_)
250 ) {
251 match ident_kind {
252 IdentifierKind::Ident | IdentifierKind::ToSelf | IdentifierKind::Underscore => {
253 cov_mark::hit!(rename_not_a_lifetime_ident_ref);
254 bail!("Invalid name `{}`: not a lifetime identifier", new_name);
255 }
256 IdentifierKind::Lifetime => cov_mark::hit!(rename_lifetime),
257 }
258 } else {
259 match (ident_kind, def) {
260 (IdentifierKind::Lifetime, _) => {
261 cov_mark::hit!(rename_not_an_ident_ref);
262 bail!("Invalid name `{}`: not an identifier", new_name);
263 }
264 (IdentifierKind::ToSelf, Definition::Local(local)) => {
265 if local.is_self(sema.db) {
266 // no-op
267 cov_mark::hit!(rename_self_to_self);
268 return Ok(SourceChange::default());
269 } else {
270 cov_mark::hit!(rename_to_self);
271 return rename_to_self(sema, local);
272 }
273 }
274 (ident_kind, Definition::Local(local)) => {
275 if let Some(self_param) = local.as_self_param(sema.db) {
276 cov_mark::hit!(rename_self_to_param);
277 return rename_self_to_param(sema, local, self_param, new_name, ident_kind);
278 } else {
279 cov_mark::hit!(rename_local);
280 }
281 }
282 (IdentifierKind::ToSelf, _) => bail!("Invalid name `{}`: not an identifier", new_name),
283 (IdentifierKind::Ident, _) => cov_mark::hit!(rename_non_local),
284 (IdentifierKind::Underscore, _) => (),
285 }
286 }
287
288 def = match def {
289 // HACK: resolve trait impl items to the item def of the trait definition
290 // so that we properly resolve all trait item references
291 Definition::ModuleDef(mod_def) => mod_def
292 .as_assoc_item(sema.db)
293 .and_then(|it| it.containing_trait_impl(sema.db))
294 .and_then(|it| {
295 it.items(sema.db).into_iter().find_map(|it| match (it, mod_def) {
296 (hir::AssocItem::Function(trait_func), ModuleDef::Function(func))
297 if trait_func.name(sema.db) == func.name(sema.db) =>
298 {
299 Some(Definition::ModuleDef(ModuleDef::Function(trait_func)))
300 }
301 (hir::AssocItem::Const(trait_konst), ModuleDef::Const(konst))
302 if trait_konst.name(sema.db) == konst.name(sema.db) =>
303 {
304 Some(Definition::ModuleDef(ModuleDef::Const(trait_konst)))
305 }
306 (
307 hir::AssocItem::TypeAlias(trait_type_alias),
308 ModuleDef::TypeAlias(type_alias),
309 ) if trait_type_alias.name(sema.db) == type_alias.name(sema.db) => {
310 Some(Definition::ModuleDef(ModuleDef::TypeAlias(trait_type_alias)))
311 }
312 _ => None,
313 })
314 })
315 .unwrap_or(def),
316 _ => def,
317 };
318 let usages = def.usages(sema).all();
319
320 if !usages.is_empty() && ident_kind == IdentifierKind::Underscore {
321 cov_mark::hit!(rename_underscore_multiple);
322 bail!("Cannot rename reference to `_` as it is being referenced multiple times");
323 }
324 let mut source_change = SourceChange::default();
325 source_change.extend(usages.iter().map(|(&file_id, references)| {
326 (file_id, source_edit_from_references(&references, def, new_name))
327 }));
328
329 let (file_id, edit) = source_edit_from_def(sema, def, new_name)?;
330 source_change.insert_source_edit(file_id, edit);
331 Ok(source_change)
332}
333
334fn rename_to_self(sema: &Semantics<RootDatabase>, local: hir::Local) -> RenameResult<SourceChange> { 140fn rename_to_self(sema: &Semantics<RootDatabase>, local: hir::Local) -> RenameResult<SourceChange> {
335 if never!(local.is_self(sema.db)) { 141 if never!(local.is_self(sema.db)) {
336 bail!("rename_to_self invoked on self"); 142 bail!("rename_to_self invoked on self");
@@ -398,8 +204,15 @@ fn rename_self_to_param(
398 local: hir::Local, 204 local: hir::Local,
399 self_param: hir::SelfParam, 205 self_param: hir::SelfParam,
400 new_name: &str, 206 new_name: &str,
401 identifier_kind: IdentifierKind,
402) -> RenameResult<SourceChange> { 207) -> RenameResult<SourceChange> {
208 if new_name == "self" {
209 // Let's do nothing rather than complain.
210 cov_mark::hit!(rename_self_to_self);
211 return Ok(SourceChange::default());
212 }
213
214 let identifier_kind = IdentifierKind::classify(new_name)?;
215
403 let InFile { file_id, value: self_param } = 216 let InFile { file_id, value: self_param } =
404 self_param.source(sema.db).ok_or_else(|| format_err!("cannot find function source"))?; 217 self_param.source(sema.db).ok_or_else(|| format_err!("cannot find function source"))?;
405 218
@@ -413,7 +226,7 @@ fn rename_self_to_param(
413 let mut source_change = SourceChange::default(); 226 let mut source_change = SourceChange::default();
414 source_change.insert_source_edit(file_id.original_file(sema.db), edit); 227 source_change.insert_source_edit(file_id.original_file(sema.db), edit);
415 source_change.extend(usages.iter().map(|(&file_id, references)| { 228 source_change.extend(usages.iter().map(|(&file_id, references)| {
416 (file_id, source_edit_from_references(&references, def, new_name)) 229 (file_id, source_edit_from_references(references, def, new_name))
417 })); 230 }));
418 Ok(source_change) 231 Ok(source_change)
419} 232}
@@ -426,7 +239,7 @@ fn text_edit_from_self_param(self_param: &ast::SelfParam, new_name: &str) -> Opt
426 None 239 None
427 } 240 }
428 241
429 let impl_def = self_param.syntax().ancestors().find_map(|it| ast::Impl::cast(it))?; 242 let impl_def = self_param.syntax().ancestors().find_map(ast::Impl::cast)?;
430 let type_name = target_type_name(&impl_def)?; 243 let type_name = target_type_name(&impl_def)?;
431 244
432 let mut replacement_text = String::from(new_name); 245 let mut replacement_text = String::from(new_name);
@@ -441,150 +254,6 @@ fn text_edit_from_self_param(self_param: &ast::SelfParam, new_name: &str) -> Opt
441 Some(TextEdit::replace(self_param.syntax().text_range(), replacement_text)) 254 Some(TextEdit::replace(self_param.syntax().text_range(), replacement_text))
442} 255}
443 256
444fn source_edit_from_references(
445 references: &[FileReference],
446 def: Definition,
447 new_name: &str,
448) -> TextEdit {
449 let mut edit = TextEdit::builder();
450 for reference in references {
451 let (range, replacement) = match &reference.name {
452 // if the ranges differ then the node is inside a macro call, we can't really attempt
453 // to make special rewrites like shorthand syntax and such, so just rename the node in
454 // the macro input
455 ast::NameLike::NameRef(name_ref)
456 if name_ref.syntax().text_range() == reference.range =>
457 {
458 source_edit_from_name_ref(name_ref, new_name, def)
459 }
460 ast::NameLike::Name(name) if name.syntax().text_range() == reference.range => {
461 source_edit_from_name(name, new_name)
462 }
463 _ => None,
464 }
465 .unwrap_or_else(|| (reference.range, new_name.to_string()));
466 edit.replace(range, replacement);
467 }
468 edit.finish()
469}
470
471fn source_edit_from_name(name: &ast::Name, new_name: &str) -> Option<(TextRange, String)> {
472 if let Some(_) = ast::RecordPatField::for_field_name(name) {
473 if let Some(ident_pat) = name.syntax().parent().and_then(ast::IdentPat::cast) {
474 return Some((
475 TextRange::empty(ident_pat.syntax().text_range().start()),
476 [new_name, ": "].concat(),
477 ));
478 }
479 }
480 None
481}
482
483fn source_edit_from_name_ref(
484 name_ref: &ast::NameRef,
485 new_name: &str,
486 def: Definition,
487) -> Option<(TextRange, String)> {
488 if let Some(record_field) = ast::RecordExprField::for_name_ref(name_ref) {
489 let rcf_name_ref = record_field.name_ref();
490 let rcf_expr = record_field.expr();
491 match (rcf_name_ref, rcf_expr.and_then(|it| it.name_ref())) {
492 // field: init-expr, check if we can use a field init shorthand
493 (Some(field_name), Some(init)) => {
494 if field_name == *name_ref {
495 if init.text() == new_name {
496 cov_mark::hit!(test_rename_field_put_init_shorthand);
497 // same names, we can use a shorthand here instead.
498 // we do not want to erase attributes hence this range start
499 let s = field_name.syntax().text_range().start();
500 let e = record_field.syntax().text_range().end();
501 return Some((TextRange::new(s, e), new_name.to_owned()));
502 }
503 } else if init == *name_ref {
504 if field_name.text() == new_name {
505 cov_mark::hit!(test_rename_local_put_init_shorthand);
506 // same names, we can use a shorthand here instead.
507 // we do not want to erase attributes hence this range start
508 let s = field_name.syntax().text_range().start();
509 let e = record_field.syntax().text_range().end();
510 return Some((TextRange::new(s, e), new_name.to_owned()));
511 }
512 }
513 None
514 }
515 // init shorthand
516 // FIXME: instead of splitting the shorthand, recursively trigger a rename of the
517 // other name https://github.com/rust-analyzer/rust-analyzer/issues/6547
518 (None, Some(_)) if matches!(def, Definition::Field(_)) => {
519 cov_mark::hit!(test_rename_field_in_field_shorthand);
520 let s = name_ref.syntax().text_range().start();
521 Some((TextRange::empty(s), format!("{}: ", new_name)))
522 }
523 (None, Some(_)) if matches!(def, Definition::Local(_)) => {
524 cov_mark::hit!(test_rename_local_in_field_shorthand);
525 let s = name_ref.syntax().text_range().end();
526 Some((TextRange::empty(s), format!(": {}", new_name)))
527 }
528 _ => None,
529 }
530 } else if let Some(record_field) = ast::RecordPatField::for_field_name_ref(name_ref) {
531 let rcf_name_ref = record_field.name_ref();
532 let rcf_pat = record_field.pat();
533 match (rcf_name_ref, rcf_pat) {
534 // field: rename
535 (Some(field_name), Some(ast::Pat::IdentPat(pat))) if field_name == *name_ref => {
536 // field name is being renamed
537 if pat.name().map_or(false, |it| it.text() == new_name) {
538 cov_mark::hit!(test_rename_field_put_init_shorthand_pat);
539 // same names, we can use a shorthand here instead/
540 // we do not want to erase attributes hence this range start
541 let s = field_name.syntax().text_range().start();
542 let e = record_field.syntax().text_range().end();
543 Some((TextRange::new(s, e), pat.to_string()))
544 } else {
545 None
546 }
547 }
548 _ => None,
549 }
550 } else {
551 None
552 }
553}
554
555fn source_edit_from_def(
556 sema: &Semantics<RootDatabase>,
557 def: Definition,
558 new_name: &str,
559) -> RenameResult<(FileId, TextEdit)> {
560 let nav =
561 def.try_to_nav(sema.db).ok_or_else(|| format_err!("No references found at position"))?;
562
563 let mut replacement_text = String::new();
564 let mut repl_range =
565 nav.focus_range.ok_or_else(|| format_err!("No identifier available to rename"))?;
566 if let Definition::Local(local) = def {
567 if let Either::Left(pat) = local.source(sema.db).value {
568 if matches!(
569 pat.syntax().parent().and_then(ast::RecordPatField::cast),
570 Some(pat_field) if pat_field.name_ref().is_none()
571 ) {
572 replacement_text.push_str(": ");
573 replacement_text.push_str(new_name);
574 repl_range = TextRange::new(
575 pat.syntax().text_range().end(),
576 pat.syntax().text_range().end(),
577 );
578 }
579 }
580 }
581 if replacement_text.is_empty() {
582 replacement_text.push_str(new_name);
583 }
584 let edit = TextEdit::replace(repl_range, replacement_text);
585 Ok((nav.file_id, edit))
586}
587
588#[cfg(test)] 257#[cfg(test)]
589mod tests { 258mod tests {
590 use expect_test::{expect, Expect}; 259 use expect_test::{expect, Expect};
@@ -659,7 +328,7 @@ mod tests {
659 fn test_prepare_rename_namelikes() { 328 fn test_prepare_rename_namelikes() {
660 check_prepare(r"fn name$0<'lifetime>() {}", expect![[r#"3..7: name"#]]); 329 check_prepare(r"fn name$0<'lifetime>() {}", expect![[r#"3..7: name"#]]);
661 check_prepare(r"fn name<'lifetime$0>() {}", expect![[r#"8..17: 'lifetime"#]]); 330 check_prepare(r"fn name<'lifetime$0>() {}", expect![[r#"8..17: 'lifetime"#]]);
662 check_prepare(r"fn name<'lifetime>() { name$0(); }", expect![[r#"23..27: name"#]]); 331 check_prepare(r"fn name<'lifetime>() { name$0(); }", expect![[r#"3..7: name"#]]);
663 } 332 }
664 333
665 #[test] 334 #[test]
@@ -691,7 +360,7 @@ fn baz() {
691 x.0$0 = 5; 360 x.0$0 = 5;
692} 361}
693"#, 362"#,
694 expect![[r#"No identifier available to rename"#]], 363 expect![[r#"No references found at position"#]],
695 ); 364 );
696 } 365 }
697 366
@@ -703,7 +372,7 @@ fn foo() {
703 let x: i32$0 = 0; 372 let x: i32$0 = 0;
704} 373}
705"#, 374"#,
706 expect![[r#"Cannot rename builtin type"#]], 375 expect![[r#"No references found at position"#]],
707 ); 376 );
708 } 377 }
709 378
@@ -719,7 +388,7 @@ impl Foo {
719 } 388 }
720} 389}
721"#, 390"#,
722 expect![[r#"Cannot rename `Self`"#]], 391 expect![[r#"No references found at position"#]],
723 ); 392 );
724 } 393 }
725 394
@@ -801,7 +470,6 @@ impl Foo {
801 470
802 #[test] 471 #[test]
803 fn test_rename_for_local() { 472 fn test_rename_for_local() {
804 cov_mark::check!(rename_local);
805 check( 473 check(
806 "k", 474 "k",
807 r#" 475 r#"
@@ -2101,4 +1769,22 @@ fn f() { <()>::BAR$0; }"#,
2101 res, 1769 res,
2102 ); 1770 );
2103 } 1771 }
1772
1773 #[test]
1774 fn macros_are_broken_lol() {
1775 cov_mark::check!(macros_are_broken_lol);
1776 check(
1777 "lol",
1778 r#"
1779macro_rules! m { () => { fn f() {} } }
1780m!();
1781fn main() { f$0() }
1782"#,
1783 r#"
1784macro_rules! m { () => { fn f() {} } }
1785lol
1786fn main() { lol() }
1787"#,
1788 )
1789 }
2104} 1790}
diff --git a/crates/ide/src/runnables.rs b/crates/ide/src/runnables.rs
index 552054951..03faabadc 100644
--- a/crates/ide/src/runnables.rs
+++ b/crates/ide/src/runnables.rs
@@ -158,7 +158,7 @@ fn find_related_tests(
158 search_scope: Option<SearchScope>, 158 search_scope: Option<SearchScope>,
159 tests: &mut FxHashSet<Runnable>, 159 tests: &mut FxHashSet<Runnable>,
160) { 160) {
161 if let Some(refs) = references::find_all_refs(&sema, position, search_scope) { 161 if let Some(refs) = references::find_all_refs(sema, position, search_scope) {
162 for (file_id, refs) in refs.references { 162 for (file_id, refs) in refs.references {
163 let file = sema.parse(file_id); 163 let file = sema.parse(file_id);
164 let file = file.syntax(); 164 let file = file.syntax();
@@ -169,10 +169,10 @@ fn find_related_tests(
169 }); 169 });
170 170
171 for fn_def in functions { 171 for fn_def in functions {
172 if let Some(runnable) = as_test_runnable(&sema, &fn_def) { 172 if let Some(runnable) = as_test_runnable(sema, &fn_def) {
173 // direct test 173 // direct test
174 tests.insert(runnable); 174 tests.insert(runnable);
175 } else if let Some(module) = parent_test_module(&sema, &fn_def) { 175 } else if let Some(module) = parent_test_module(sema, &fn_def) {
176 // indirect test 176 // indirect test
177 find_related_tests_in_module(sema, &fn_def, &module, tests); 177 find_related_tests_in_module(sema, &fn_def, &module, tests);
178 } 178 }
@@ -203,7 +203,7 @@ fn find_related_tests_in_module(
203} 203}
204 204
205fn as_test_runnable(sema: &Semantics<RootDatabase>, fn_def: &ast::Fn) -> Option<Runnable> { 205fn as_test_runnable(sema: &Semantics<RootDatabase>, fn_def: &ast::Fn) -> Option<Runnable> {
206 if test_related_attribute(&fn_def).is_some() { 206 if test_related_attribute(fn_def).is_some() {
207 let function = sema.to_def(fn_def)?; 207 let function = sema.to_def(fn_def)?;
208 runnable_fn(sema, function) 208 runnable_fn(sema, function)
209 } else { 209 } else {
diff --git a/crates/ide/src/syntax_highlighting.rs b/crates/ide/src/syntax_highlighting.rs
index b03f1c71f..e186b82b7 100644
--- a/crates/ide/src/syntax_highlighting.rs
+++ b/crates/ide/src/syntax_highlighting.rs
@@ -323,7 +323,7 @@ fn traverse(
323 if let Some(token) = element.as_token().cloned().and_then(ast::String::cast) { 323 if let Some(token) = element.as_token().cloned().and_then(ast::String::cast) {
324 if token.is_raw() { 324 if token.is_raw() {
325 let expanded = element_to_highlight.as_token().unwrap().clone(); 325 let expanded = element_to_highlight.as_token().unwrap().clone();
326 if inject::ra_fixture(hl, &sema, token, expanded).is_some() { 326 if inject::ra_fixture(hl, sema, token, expanded).is_some() {
327 continue; 327 continue;
328 } 328 }
329 } 329 }
@@ -334,7 +334,7 @@ fn traverse(
334 } 334 }
335 335
336 if let Some((mut highlight, binding_hash)) = highlight::element( 336 if let Some((mut highlight, binding_hash)) = highlight::element(
337 &sema, 337 sema,
338 krate, 338 krate,
339 &mut bindings_shadow_count, 339 &mut bindings_shadow_count,
340 syntactic_name_ref_highlighting, 340 syntactic_name_ref_highlighting,
diff --git a/crates/ide/src/syntax_highlighting/highlight.rs b/crates/ide/src/syntax_highlighting/highlight.rs
index 9503c936d..6834fe11a 100644
--- a/crates/ide/src/syntax_highlighting/highlight.rs
+++ b/crates/ide/src/syntax_highlighting/highlight.rs
@@ -48,7 +48,13 @@ pub(super) fn element(
48 match name_kind { 48 match name_kind {
49 Some(NameClass::ExternCrate(_)) => SymbolKind::Module.into(), 49 Some(NameClass::ExternCrate(_)) => SymbolKind::Module.into(),
50 Some(NameClass::Definition(def)) => { 50 Some(NameClass::Definition(def)) => {
51 highlight_def(db, krate, def) | HlMod::Definition 51 let mut h = highlight_def(db, krate, def) | HlMod::Definition;
52 if let Definition::ModuleDef(hir::ModuleDef::Trait(trait_)) = &def {
53 if trait_.is_unsafe(db) {
54 h |= HlMod::Unsafe;
55 }
56 }
57 h
52 } 58 }
53 Some(NameClass::ConstReference(def)) => highlight_def(db, krate, def), 59 Some(NameClass::ConstReference(def)) => highlight_def(db, krate, def),
54 Some(NameClass::PatFieldShorthand { field_ref, .. }) => { 60 Some(NameClass::PatFieldShorthand { field_ref, .. }) => {
@@ -87,20 +93,34 @@ pub(super) fn element(
87 93
88 let mut h = highlight_def(db, krate, def); 94 let mut h = highlight_def(db, krate, def);
89 95
90 if let Definition::Local(local) = &def { 96 match def {
91 if is_consumed_lvalue(name_ref.syntax().clone().into(), local, db) { 97 Definition::Local(local)
98 if is_consumed_lvalue(
99 name_ref.syntax().clone().into(),
100 &local,
101 db,
102 ) =>
103 {
92 h |= HlMod::Consuming; 104 h |= HlMod::Consuming;
93 } 105 }
94 } 106 Definition::ModuleDef(hir::ModuleDef::Trait(trait_))
95 107 if trait_.is_unsafe(db) =>
96 if let Some(parent) = name_ref.syntax().parent() { 108 {
97 if matches!(parent.kind(), FIELD_EXPR | RECORD_PAT_FIELD) { 109 if ast::Impl::for_trait_name_ref(&name_ref).is_some() {
98 if let Definition::Field(field) = def { 110 h |= HlMod::Unsafe;
99 if let hir::VariantDef::Union(_) = field.parent_def(db) { 111 }
100 h |= HlMod::Unsafe; 112 }
113 Definition::Field(field) => {
114 if let Some(parent) = name_ref.syntax().parent() {
115 if matches!(parent.kind(), FIELD_EXPR | RECORD_PAT_FIELD) {
116 if let hir::VariantDef::Union(_) = field.parent_def(db)
117 {
118 h |= HlMod::Unsafe;
119 }
101 } 120 }
102 } 121 }
103 } 122 }
123 _ => (),
104 } 124 }
105 125
106 h 126 h
@@ -131,6 +151,9 @@ pub(super) fn element(
131 } 151 }
132 STRING | BYTE_STRING => HlTag::StringLiteral.into(), 152 STRING | BYTE_STRING => HlTag::StringLiteral.into(),
133 ATTR => HlTag::Attribute.into(), 153 ATTR => HlTag::Attribute.into(),
154 INT_NUMBER if element.ancestors().nth(1).map_or(false, |it| it.kind() == FIELD_EXPR) => {
155 SymbolKind::Field.into()
156 }
134 INT_NUMBER | FLOAT_NUMBER => HlTag::NumericLiteral.into(), 157 INT_NUMBER | FLOAT_NUMBER => HlTag::NumericLiteral.into(),
135 BYTE => HlTag::ByteLiteral.into(), 158 BYTE => HlTag::ByteLiteral.into(),
136 CHAR => HlTag::CharLiteral.into(), 159 CHAR => HlTag::CharLiteral.into(),
@@ -351,15 +374,7 @@ fn highlight_def(db: &RootDatabase, krate: Option<hir::Crate>, def: Definition)
351 374
352 h 375 h
353 } 376 }
354 hir::ModuleDef::Trait(trait_) => { 377 hir::ModuleDef::Trait(_) => Highlight::new(HlTag::Symbol(SymbolKind::Trait)),
355 let mut h = Highlight::new(HlTag::Symbol(SymbolKind::Trait));
356
357 if trait_.is_unsafe(db) {
358 h |= HlMod::Unsafe;
359 }
360
361 h
362 }
363 hir::ModuleDef::TypeAlias(type_) => { 378 hir::ModuleDef::TypeAlias(type_) => {
364 let mut h = Highlight::new(HlTag::Symbol(SymbolKind::TypeAlias)); 379 let mut h = Highlight::new(HlTag::Symbol(SymbolKind::TypeAlias));
365 380
@@ -446,12 +461,12 @@ fn highlight_method_call(
446 krate: Option<hir::Crate>, 461 krate: Option<hir::Crate>,
447 method_call: &ast::MethodCallExpr, 462 method_call: &ast::MethodCallExpr,
448) -> Option<Highlight> { 463) -> Option<Highlight> {
449 let func = sema.resolve_method_call(&method_call)?; 464 let func = sema.resolve_method_call(method_call)?;
450 465
451 let mut h = SymbolKind::Function.into(); 466 let mut h = SymbolKind::Function.into();
452 h |= HlMod::Associated; 467 h |= HlMod::Associated;
453 468
454 if func.is_unsafe(sema.db) || sema.is_unsafe_method_call(&method_call) { 469 if func.is_unsafe(sema.db) || sema.is_unsafe_method_call(method_call) {
455 h |= HlMod::Unsafe; 470 h |= HlMod::Unsafe;
456 } 471 }
457 if func.is_async(sema.db) { 472 if func.is_async(sema.db) {
@@ -523,11 +538,9 @@ fn highlight_name_ref_by_syntax(
523 }; 538 };
524 539
525 match parent.kind() { 540 match parent.kind() {
526 METHOD_CALL_EXPR => { 541 METHOD_CALL_EXPR => ast::MethodCallExpr::cast(parent)
527 return ast::MethodCallExpr::cast(parent) 542 .and_then(|it| highlight_method_call(sema, krate, &it))
528 .and_then(|it| highlight_method_call(sema, krate, &it)) 543 .unwrap_or_else(|| SymbolKind::Function.into()),
529 .unwrap_or_else(|| SymbolKind::Function.into());
530 }
531 FIELD_EXPR => { 544 FIELD_EXPR => {
532 let h = HlTag::Symbol(SymbolKind::Field); 545 let h = HlTag::Symbol(SymbolKind::Field);
533 let is_union = ast::FieldExpr::cast(parent) 546 let is_union = ast::FieldExpr::cast(parent)
diff --git a/crates/ide/src/syntax_highlighting/html.rs b/crates/ide/src/syntax_highlighting/html.rs
index 5327af845..21376a7ae 100644
--- a/crates/ide/src/syntax_highlighting/html.rs
+++ b/crates/ide/src/syntax_highlighting/html.rs
@@ -23,7 +23,7 @@ pub(crate) fn highlight_as_html(db: &RootDatabase, file_id: FileId, rainbow: boo
23 let hl_ranges = highlight(db, file_id, None, false); 23 let hl_ranges = highlight(db, file_id, None, false);
24 let text = parse.tree().syntax().to_string(); 24 let text = parse.tree().syntax().to_string();
25 let mut buf = String::new(); 25 let mut buf = String::new();
26 buf.push_str(&STYLE); 26 buf.push_str(STYLE);
27 buf.push_str("<pre><code>"); 27 buf.push_str("<pre><code>");
28 for r in &hl_ranges { 28 for r in &hl_ranges {
29 let chunk = html_escape(&text[r.range]); 29 let chunk = html_escape(&text[r.range]);
@@ -67,6 +67,7 @@ pre { color: #DCDCCC; background: #3F3F3F; font-size: 22px; padd
67.field { color: #94BFF3; } 67.field { color: #94BFF3; }
68.function { color: #93E0E3; } 68.function { color: #93E0E3; }
69.function.unsafe { color: #BC8383; } 69.function.unsafe { color: #BC8383; }
70.trait.unsafe { color: #BC8383; }
70.operator.unsafe { color: #BC8383; } 71.operator.unsafe { color: #BC8383; }
71.parameter { color: #94BFF3; } 72.parameter { color: #94BFF3; }
72.text { color: #DCDCCC; } 73.text { color: #DCDCCC; }
diff --git a/crates/ide/src/syntax_highlighting/inject.rs b/crates/ide/src/syntax_highlighting/inject.rs
index 4269d339e..ec43c8579 100644
--- a/crates/ide/src/syntax_highlighting/inject.rs
+++ b/crates/ide/src/syntax_highlighting/inject.rs
@@ -23,7 +23,7 @@ pub(super) fn ra_fixture(
23 literal: ast::String, 23 literal: ast::String,
24 expanded: SyntaxToken, 24 expanded: SyntaxToken,
25) -> Option<()> { 25) -> Option<()> {
26 let active_parameter = ActiveParameter::at_token(&sema, expanded)?; 26 let active_parameter = ActiveParameter::at_token(sema, expanded)?;
27 if !active_parameter.ident().map_or(false, |name| name.text().starts_with("ra_fixture")) { 27 if !active_parameter.ident().map_or(false, |name| name.text().starts_with("ra_fixture")) {
28 return None; 28 return None;
29 } 29 }
@@ -124,7 +124,7 @@ pub(super) fn doc_comment(
124 } 124 }
125 125
126 for attr in attributes.by_key("doc").attrs() { 126 for attr in attributes.by_key("doc").attrs() {
127 let InFile { file_id, value: src } = attrs_source_map.source_of(&attr); 127 let InFile { file_id, value: src } = attrs_source_map.source_of(attr);
128 if file_id != node.file_id { 128 if file_id != node.file_id {
129 continue; 129 continue;
130 } 130 }
@@ -232,7 +232,7 @@ fn find_doc_string_in_attr(attr: &hir::Attr, it: &ast::Attr) -> Option<ast::Stri
232 string.text().get(1..string.text().len() - 1).map_or(false, |it| it == text) 232 string.text().get(1..string.text().len() - 1).map_or(false, |it| it == text)
233 }) 233 })
234 } 234 }
235 _ => return None, 235 _ => None,
236 } 236 }
237} 237}
238 238
diff --git a/crates/ide/src/syntax_highlighting/test_data/highlight_assoc_functions.html b/crates/ide/src/syntax_highlighting/test_data/highlight_assoc_functions.html
index a0ea1db34..4e85f7c0b 100644
--- a/crates/ide/src/syntax_highlighting/test_data/highlight_assoc_functions.html
+++ b/crates/ide/src/syntax_highlighting/test_data/highlight_assoc_functions.html
@@ -15,6 +15,7 @@ pre { color: #DCDCCC; background: #3F3F3F; font-size: 22px; padd
15.field { color: #94BFF3; } 15.field { color: #94BFF3; }
16.function { color: #93E0E3; } 16.function { color: #93E0E3; }
17.function.unsafe { color: #BC8383; } 17.function.unsafe { color: #BC8383; }
18.trait.unsafe { color: #BC8383; }
18.operator.unsafe { color: #BC8383; } 19.operator.unsafe { color: #BC8383; }
19.parameter { color: #94BFF3; } 20.parameter { color: #94BFF3; }
20.text { color: #DCDCCC; } 21.text { color: #DCDCCC; }
diff --git a/crates/ide/src/syntax_highlighting/test_data/highlight_doctest.html b/crates/ide/src/syntax_highlighting/test_data/highlight_doctest.html
index 921a956e6..79a285107 100644
--- a/crates/ide/src/syntax_highlighting/test_data/highlight_doctest.html
+++ b/crates/ide/src/syntax_highlighting/test_data/highlight_doctest.html
@@ -15,6 +15,7 @@ pre { color: #DCDCCC; background: #3F3F3F; font-size: 22px; padd
15.field { color: #94BFF3; } 15.field { color: #94BFF3; }
16.function { color: #93E0E3; } 16.function { color: #93E0E3; }
17.function.unsafe { color: #BC8383; } 17.function.unsafe { color: #BC8383; }
18.trait.unsafe { color: #BC8383; }
18.operator.unsafe { color: #BC8383; } 19.operator.unsafe { color: #BC8383; }
19.parameter { color: #94BFF3; } 20.parameter { color: #94BFF3; }
20.text { color: #DCDCCC; } 21.text { color: #DCDCCC; }
diff --git a/crates/ide/src/syntax_highlighting/test_data/highlight_extern_crate.html b/crates/ide/src/syntax_highlighting/test_data/highlight_extern_crate.html
index ca9bb1e7d..13f589cc0 100644
--- a/crates/ide/src/syntax_highlighting/test_data/highlight_extern_crate.html
+++ b/crates/ide/src/syntax_highlighting/test_data/highlight_extern_crate.html
@@ -15,6 +15,7 @@ pre { color: #DCDCCC; background: #3F3F3F; font-size: 22px; padd
15.field { color: #94BFF3; } 15.field { color: #94BFF3; }
16.function { color: #93E0E3; } 16.function { color: #93E0E3; }
17.function.unsafe { color: #BC8383; } 17.function.unsafe { color: #BC8383; }
18.trait.unsafe { color: #BC8383; }
18.operator.unsafe { color: #BC8383; } 19.operator.unsafe { color: #BC8383; }
19.parameter { color: #94BFF3; } 20.parameter { color: #94BFF3; }
20.text { color: #DCDCCC; } 21.text { color: #DCDCCC; }
diff --git a/crates/ide/src/syntax_highlighting/test_data/highlight_injection.html b/crates/ide/src/syntax_highlighting/test_data/highlight_injection.html
index 6202a03ce..50df376ae 100644
--- a/crates/ide/src/syntax_highlighting/test_data/highlight_injection.html
+++ b/crates/ide/src/syntax_highlighting/test_data/highlight_injection.html
@@ -15,6 +15,7 @@ pre { color: #DCDCCC; background: #3F3F3F; font-size: 22px; padd
15.field { color: #94BFF3; } 15.field { color: #94BFF3; }
16.function { color: #93E0E3; } 16.function { color: #93E0E3; }
17.function.unsafe { color: #BC8383; } 17.function.unsafe { color: #BC8383; }
18.trait.unsafe { color: #BC8383; }
18.operator.unsafe { color: #BC8383; } 19.operator.unsafe { color: #BC8383; }
19.parameter { color: #94BFF3; } 20.parameter { color: #94BFF3; }
20.text { color: #DCDCCC; } 21.text { color: #DCDCCC; }
diff --git a/crates/ide/src/syntax_highlighting/test_data/highlight_strings.html b/crates/ide/src/syntax_highlighting/test_data/highlight_strings.html
index e860d713e..96cb09642 100644
--- a/crates/ide/src/syntax_highlighting/test_data/highlight_strings.html
+++ b/crates/ide/src/syntax_highlighting/test_data/highlight_strings.html
@@ -15,6 +15,7 @@ pre { color: #DCDCCC; background: #3F3F3F; font-size: 22px; padd
15.field { color: #94BFF3; } 15.field { color: #94BFF3; }
16.function { color: #93E0E3; } 16.function { color: #93E0E3; }
17.function.unsafe { color: #BC8383; } 17.function.unsafe { color: #BC8383; }
18.trait.unsafe { color: #BC8383; }
18.operator.unsafe { color: #BC8383; } 19.operator.unsafe { color: #BC8383; }
19.parameter { color: #94BFF3; } 20.parameter { color: #94BFF3; }
20.text { color: #DCDCCC; } 21.text { color: #DCDCCC; }
diff --git a/crates/ide/src/syntax_highlighting/test_data/highlight_unsafe.html b/crates/ide/src/syntax_highlighting/test_data/highlight_unsafe.html
index 68165bdbf..55453468b 100644
--- a/crates/ide/src/syntax_highlighting/test_data/highlight_unsafe.html
+++ b/crates/ide/src/syntax_highlighting/test_data/highlight_unsafe.html
@@ -15,6 +15,7 @@ pre { color: #DCDCCC; background: #3F3F3F; font-size: 22px; padd
15.field { color: #94BFF3; } 15.field { color: #94BFF3; }
16.function { color: #93E0E3; } 16.function { color: #93E0E3; }
17.function.unsafe { color: #BC8383; } 17.function.unsafe { color: #BC8383; }
18.trait.unsafe { color: #BC8383; }
18.operator.unsafe { color: #BC8383; } 19.operator.unsafe { color: #BC8383; }
19.parameter { color: #94BFF3; } 20.parameter { color: #94BFF3; }
20.text { color: #DCDCCC; } 21.text { color: #DCDCCC; }
@@ -61,6 +62,11 @@ pre { color: #DCDCCC; background: #3F3F3F; font-size: 22px; padd
61 <span class="field declaration">a</span><span class="colon">:</span> <span class="builtin_type">u16</span><span class="comma">,</span> 62 <span class="field declaration">a</span><span class="colon">:</span> <span class="builtin_type">u16</span><span class="comma">,</span>
62<span class="brace">}</span> 63<span class="brace">}</span>
63 64
65<span class="keyword unsafe">unsafe</span> <span class="keyword">trait</span> <span class="trait declaration unsafe">UnsafeTrait</span> <span class="brace">{</span><span class="brace">}</span>
66<span class="keyword unsafe">unsafe</span> <span class="keyword">impl</span> <span class="trait unsafe">UnsafeTrait</span> <span class="keyword">for</span> <span class="struct">Packed</span> <span class="brace">{</span><span class="brace">}</span>
67
68<span class="keyword">fn</span> <span class="function declaration">require_unsafe_trait</span><span class="angle">&lt;</span><span class="type_param declaration">T</span><span class="colon">:</span> <span class="trait">UnsafeTrait</span><span class="angle">&gt;</span><span class="parenthesis">(</span><span class="punctuation">_</span><span class="colon">:</span> <span class="type_param">T</span><span class="parenthesis">)</span> <span class="brace">{</span><span class="brace">}</span>
69
64<span class="keyword">trait</span> <span class="trait declaration">DoTheAutoref</span> <span class="brace">{</span> 70<span class="keyword">trait</span> <span class="trait declaration">DoTheAutoref</span> <span class="brace">{</span>
65 <span class="keyword">fn</span> <span class="function associated declaration trait">calls_autoref</span><span class="parenthesis">(</span><span class="operator">&</span><span class="self_keyword declaration">self</span><span class="parenthesis">)</span><span class="semicolon">;</span> 71 <span class="keyword">fn</span> <span class="function associated declaration trait">calls_autoref</span><span class="parenthesis">(</span><span class="operator">&</span><span class="self_keyword declaration">self</span><span class="parenthesis">)</span><span class="semicolon">;</span>
66<span class="brace">}</span> 72<span class="brace">}</span>
diff --git a/crates/ide/src/syntax_highlighting/test_data/highlighting.html b/crates/ide/src/syntax_highlighting/test_data/highlighting.html
index a7b5c3b89..9232cf905 100644
--- a/crates/ide/src/syntax_highlighting/test_data/highlighting.html
+++ b/crates/ide/src/syntax_highlighting/test_data/highlighting.html
@@ -15,6 +15,7 @@ pre { color: #DCDCCC; background: #3F3F3F; font-size: 22px; padd
15.field { color: #94BFF3; } 15.field { color: #94BFF3; }
16.function { color: #93E0E3; } 16.function { color: #93E0E3; }
17.function.unsafe { color: #BC8383; } 17.function.unsafe { color: #BC8383; }
18.trait.unsafe { color: #BC8383; }
18.operator.unsafe { color: #BC8383; } 19.operator.unsafe { color: #BC8383; }
19.parameter { color: #94BFF3; } 20.parameter { color: #94BFF3; }
20.text { color: #DCDCCC; } 21.text { color: #DCDCCC; }
@@ -215,8 +216,8 @@ pre { color: #DCDCCC; background: #3F3F3F; font-size: 22px; padd
215 <span class="keyword">let</span> <span class="variable callable declaration">a</span> <span class="operator">=</span> <span class="punctuation">|</span><span class="value_param declaration">x</span><span class="punctuation">|</span> <span class="value_param">x</span><span class="semicolon">;</span> 216 <span class="keyword">let</span> <span class="variable callable declaration">a</span> <span class="operator">=</span> <span class="punctuation">|</span><span class="value_param declaration">x</span><span class="punctuation">|</span> <span class="value_param">x</span><span class="semicolon">;</span>
216 <span class="keyword">let</span> <span class="variable callable declaration">bar</span> <span class="operator">=</span> <span class="struct">Foo</span><span class="operator">::</span><span class="function associated">baz</span><span class="semicolon">;</span> 217 <span class="keyword">let</span> <span class="variable callable declaration">bar</span> <span class="operator">=</span> <span class="struct">Foo</span><span class="operator">::</span><span class="function associated">baz</span><span class="semicolon">;</span>
217 218
218 <span class="keyword">let</span> <span class="variable declaration">baz</span> <span class="operator">=</span> <span class="numeric_literal">-</span><span class="numeric_literal">42</span><span class="semicolon">;</span> 219 <span class="keyword">let</span> <span class="variable declaration">baz</span> <span class="operator">=</span> <span class="parenthesis">(</span><span class="numeric_literal">-</span><span class="numeric_literal">42</span><span class="comma">,</span><span class="parenthesis">)</span><span class="semicolon">;</span>
219 <span class="keyword">let</span> <span class="variable declaration">baz</span> <span class="operator">=</span> <span class="operator">-</span><span class="variable">baz</span><span class="semicolon">;</span> 220 <span class="keyword">let</span> <span class="variable declaration">baz</span> <span class="operator">=</span> <span class="operator">-</span><span class="variable">baz</span><span class="operator">.</span><span class="field">0</span><span class="semicolon">;</span>
220 221
221 <span class="keyword">let</span> <span class="punctuation">_</span> <span class="operator">=</span> <span class="logical">!</span><span class="bool_literal">true</span><span class="semicolon">;</span> 222 <span class="keyword">let</span> <span class="punctuation">_</span> <span class="operator">=</span> <span class="logical">!</span><span class="bool_literal">true</span><span class="semicolon">;</span>
222 223
diff --git a/crates/ide/src/syntax_highlighting/test_data/injection.html b/crates/ide/src/syntax_highlighting/test_data/injection.html
index 9ab46d05c..082837328 100644
--- a/crates/ide/src/syntax_highlighting/test_data/injection.html
+++ b/crates/ide/src/syntax_highlighting/test_data/injection.html
@@ -15,6 +15,7 @@ pre { color: #DCDCCC; background: #3F3F3F; font-size: 22px; padd
15.field { color: #94BFF3; } 15.field { color: #94BFF3; }
16.function { color: #93E0E3; } 16.function { color: #93E0E3; }
17.function.unsafe { color: #BC8383; } 17.function.unsafe { color: #BC8383; }
18.trait.unsafe { color: #BC8383; }
18.operator.unsafe { color: #BC8383; } 19.operator.unsafe { color: #BC8383; }
19.parameter { color: #94BFF3; } 20.parameter { color: #94BFF3; }
20.text { color: #DCDCCC; } 21.text { color: #DCDCCC; }
diff --git a/crates/ide/src/syntax_highlighting/test_data/rainbow_highlighting.html b/crates/ide/src/syntax_highlighting/test_data/rainbow_highlighting.html
index 666b0b228..763917714 100644
--- a/crates/ide/src/syntax_highlighting/test_data/rainbow_highlighting.html
+++ b/crates/ide/src/syntax_highlighting/test_data/rainbow_highlighting.html
@@ -15,6 +15,7 @@ pre { color: #DCDCCC; background: #3F3F3F; font-size: 22px; padd
15.field { color: #94BFF3; } 15.field { color: #94BFF3; }
16.function { color: #93E0E3; } 16.function { color: #93E0E3; }
17.function.unsafe { color: #BC8383; } 17.function.unsafe { color: #BC8383; }
18.trait.unsafe { color: #BC8383; }
18.operator.unsafe { color: #BC8383; } 19.operator.unsafe { color: #BC8383; }
19.parameter { color: #94BFF3; } 20.parameter { color: #94BFF3; }
20.text { color: #DCDCCC; } 21.text { color: #DCDCCC; }
diff --git a/crates/ide/src/syntax_highlighting/tests.rs b/crates/ide/src/syntax_highlighting/tests.rs
index 6ad2a362a..4f0b1ce85 100644
--- a/crates/ide/src/syntax_highlighting/tests.rs
+++ b/crates/ide/src/syntax_highlighting/tests.rs
@@ -189,8 +189,8 @@ fn main() {
189 let a = |x| x; 189 let a = |x| x;
190 let bar = Foo::baz; 190 let bar = Foo::baz;
191 191
192 let baz = -42; 192 let baz = (-42,);
193 let baz = -baz; 193 let baz = -baz.0;
194 194
195 let _ = !true; 195 let _ = !true;
196 196
@@ -527,6 +527,11 @@ struct Packed {
527 a: u16, 527 a: u16,
528} 528}
529 529
530unsafe trait UnsafeTrait {}
531unsafe impl UnsafeTrait for Packed {}
532
533fn require_unsafe_trait<T: UnsafeTrait>(_: T) {}
534
530trait DoTheAutoref { 535trait DoTheAutoref {
531 fn calls_autoref(&self); 536 fn calls_autoref(&self);
532} 537}
diff --git a/crates/ide/src/typing/on_enter.rs b/crates/ide/src/typing/on_enter.rs
index 81c4d95b1..5cba9d11d 100644
--- a/crates/ide/src/typing/on_enter.rs
+++ b/crates/ide/src/typing/on_enter.rs
@@ -88,12 +88,12 @@ fn on_enter_in_comment(
88 if comment.text().ends_with(' ') { 88 if comment.text().ends_with(' ') {
89 cov_mark::hit!(continues_end_of_line_comment_with_space); 89 cov_mark::hit!(continues_end_of_line_comment_with_space);
90 remove_trailing_whitespace = true; 90 remove_trailing_whitespace = true;
91 } else if !followed_by_comment(&comment) { 91 } else if !followed_by_comment(comment) {
92 return None; 92 return None;
93 } 93 }
94 } 94 }
95 95
96 let indent = node_indent(&file, comment.syntax())?; 96 let indent = node_indent(file, comment.syntax())?;
97 let inserted = format!("\n{}{} $0", indent, prefix); 97 let inserted = format!("\n{}{} $0", indent, prefix);
98 let delete = if remove_trailing_whitespace { 98 let delete = if remove_trailing_whitespace {
99 let trimmed_len = comment.text().trim_end().len() as u32; 99 let trimmed_len = comment.text().trim_end().len() as u32;
@@ -188,7 +188,7 @@ mod tests {
188 use crate::fixture; 188 use crate::fixture;
189 189
190 fn apply_on_enter(before: &str) -> Option<String> { 190 fn apply_on_enter(before: &str) -> Option<String> {
191 let (analysis, position) = fixture::position(&before); 191 let (analysis, position) = fixture::position(before);
192 let result = analysis.on_enter(position).unwrap()?; 192 let result = analysis.on_enter(position).unwrap()?;
193 193
194 let mut actual = analysis.file_text(position.file_id).unwrap().to_string(); 194 let mut actual = analysis.file_text(position.file_id).unwrap().to_string();