aboutsummaryrefslogtreecommitdiff
path: root/crates/ide
diff options
context:
space:
mode:
Diffstat (limited to 'crates/ide')
-rw-r--r--crates/ide/src/diagnostics.rs157
-rw-r--r--crates/ide/src/diagnostics/field_shorthand.rs8
-rw-r--r--crates/ide/src/diagnostics/fixes.rs60
-rw-r--r--crates/ide/src/diagnostics/unlinked_file.rs14
-rw-r--r--crates/ide/src/doc_links.rs190
-rw-r--r--crates/ide/src/expand_macro.rs67
-rw-r--r--crates/ide/src/folding_ranges.rs17
-rw-r--r--crates/ide/src/goto_definition.rs82
-rw-r--r--crates/ide/src/hover.rs42
-rw-r--r--crates/ide/src/lib.rs42
-rw-r--r--crates/ide/src/move_item.rs100
-rw-r--r--crates/ide/src/prime_caches.rs3
-rw-r--r--crates/ide/src/syntax_highlighting/highlight.rs73
-rw-r--r--crates/ide/src/syntax_highlighting/inject.rs104
-rw-r--r--crates/ide/src/syntax_highlighting/tags.rs24
-rw-r--r--crates/ide/src/syntax_highlighting/test_data/highlight_doctest.html12
-rw-r--r--crates/ide/src/syntax_highlighting/test_data/highlighting.html2
-rw-r--r--crates/ide/src/syntax_highlighting/tests.rs46
-rw-r--r--crates/ide/src/typing.rs368
-rw-r--r--crates/ide/src/typing/on_enter.rs230
20 files changed, 1148 insertions, 493 deletions
diff --git a/crates/ide/src/diagnostics.rs b/crates/ide/src/diagnostics.rs
index dd42116a7..1c911a8b2 100644
--- a/crates/ide/src/diagnostics.rs
+++ b/crates/ide/src/diagnostics.rs
@@ -20,12 +20,12 @@ use itertools::Itertools;
20use rustc_hash::FxHashSet; 20use rustc_hash::FxHashSet;
21use syntax::{ 21use syntax::{
22 ast::{self, AstNode}, 22 ast::{self, AstNode},
23 SyntaxNode, SyntaxNodePtr, TextRange, 23 SyntaxNode, SyntaxNodePtr, TextRange, TextSize,
24}; 24};
25use text_edit::TextEdit; 25use text_edit::TextEdit;
26use unlinked_file::UnlinkedFile; 26use unlinked_file::UnlinkedFile;
27 27
28use crate::{FileId, Label, SourceChange}; 28use crate::{Assist, AssistId, AssistKind, FileId, Label, SourceChange};
29 29
30use self::fixes::DiagnosticWithFix; 30use self::fixes::DiagnosticWithFix;
31 31
@@ -35,7 +35,7 @@ pub struct Diagnostic {
35 pub message: String, 35 pub message: String,
36 pub range: TextRange, 36 pub range: TextRange,
37 pub severity: Severity, 37 pub severity: Severity,
38 pub fix: Option<Fix>, 38 pub fix: Option<Assist>,
39 pub unused: bool, 39 pub unused: bool,
40 pub code: Option<DiagnosticCode>, 40 pub code: Option<DiagnosticCode>,
41} 41}
@@ -56,7 +56,7 @@ impl Diagnostic {
56 } 56 }
57 } 57 }
58 58
59 fn with_fix(self, fix: Option<Fix>) -> Self { 59 fn with_fix(self, fix: Option<Assist>) -> Self {
60 Self { fix, ..self } 60 Self { fix, ..self }
61 } 61 }
62 62
@@ -69,21 +69,6 @@ impl Diagnostic {
69 } 69 }
70} 70}
71 71
72#[derive(Debug)]
73pub struct Fix {
74 pub label: Label,
75 pub source_change: SourceChange,
76 /// Allows to trigger the fix only when the caret is in the range given
77 pub fix_trigger_range: TextRange,
78}
79
80impl Fix {
81 fn new(label: &str, source_change: SourceChange, fix_trigger_range: TextRange) -> Self {
82 let label = Label::new(label);
83 Self { label, source_change, fix_trigger_range }
84 }
85}
86
87#[derive(Debug, Copy, Clone)] 72#[derive(Debug, Copy, Clone)]
88pub enum Severity { 73pub enum Severity {
89 Error, 74 Error,
@@ -99,6 +84,7 @@ pub struct DiagnosticsConfig {
99pub(crate) fn diagnostics( 84pub(crate) fn diagnostics(
100 db: &RootDatabase, 85 db: &RootDatabase,
101 config: &DiagnosticsConfig, 86 config: &DiagnosticsConfig,
87 resolve: bool,
102 file_id: FileId, 88 file_id: FileId,
103) -> Vec<Diagnostic> { 89) -> Vec<Diagnostic> {
104 let _p = profile::span("diagnostics"); 90 let _p = profile::span("diagnostics");
@@ -122,25 +108,25 @@ pub(crate) fn diagnostics(
122 let res = RefCell::new(res); 108 let res = RefCell::new(res);
123 let sink_builder = DiagnosticSinkBuilder::new() 109 let sink_builder = DiagnosticSinkBuilder::new()
124 .on::<hir::diagnostics::UnresolvedModule, _>(|d| { 110 .on::<hir::diagnostics::UnresolvedModule, _>(|d| {
125 res.borrow_mut().push(diagnostic_with_fix(d, &sema)); 111 res.borrow_mut().push(diagnostic_with_fix(d, &sema, resolve));
126 }) 112 })
127 .on::<hir::diagnostics::MissingFields, _>(|d| { 113 .on::<hir::diagnostics::MissingFields, _>(|d| {
128 res.borrow_mut().push(diagnostic_with_fix(d, &sema)); 114 res.borrow_mut().push(diagnostic_with_fix(d, &sema, resolve));
129 }) 115 })
130 .on::<hir::diagnostics::MissingOkOrSomeInTailExpr, _>(|d| { 116 .on::<hir::diagnostics::MissingOkOrSomeInTailExpr, _>(|d| {
131 res.borrow_mut().push(diagnostic_with_fix(d, &sema)); 117 res.borrow_mut().push(diagnostic_with_fix(d, &sema, resolve));
132 }) 118 })
133 .on::<hir::diagnostics::NoSuchField, _>(|d| { 119 .on::<hir::diagnostics::NoSuchField, _>(|d| {
134 res.borrow_mut().push(diagnostic_with_fix(d, &sema)); 120 res.borrow_mut().push(diagnostic_with_fix(d, &sema, resolve));
135 }) 121 })
136 .on::<hir::diagnostics::RemoveThisSemicolon, _>(|d| { 122 .on::<hir::diagnostics::RemoveThisSemicolon, _>(|d| {
137 res.borrow_mut().push(diagnostic_with_fix(d, &sema)); 123 res.borrow_mut().push(diagnostic_with_fix(d, &sema, resolve));
138 }) 124 })
139 .on::<hir::diagnostics::IncorrectCase, _>(|d| { 125 .on::<hir::diagnostics::IncorrectCase, _>(|d| {
140 res.borrow_mut().push(warning_with_fix(d, &sema)); 126 res.borrow_mut().push(warning_with_fix(d, &sema, resolve));
141 }) 127 })
142 .on::<hir::diagnostics::ReplaceFilterMapNextWithFindMap, _>(|d| { 128 .on::<hir::diagnostics::ReplaceFilterMapNextWithFindMap, _>(|d| {
143 res.borrow_mut().push(warning_with_fix(d, &sema)); 129 res.borrow_mut().push(warning_with_fix(d, &sema, resolve));
144 }) 130 })
145 .on::<hir::diagnostics::InactiveCode, _>(|d| { 131 .on::<hir::diagnostics::InactiveCode, _>(|d| {
146 // If there's inactive code somewhere in a macro, don't propagate to the call-site. 132 // If there's inactive code somewhere in a macro, don't propagate to the call-site.
@@ -159,14 +145,16 @@ pub(crate) fn diagnostics(
159 ); 145 );
160 }) 146 })
161 .on::<UnlinkedFile, _>(|d| { 147 .on::<UnlinkedFile, _>(|d| {
148 // Limit diagnostic to the first few characters in the file. This matches how VS Code
149 // renders it with the full span, but on other editors, and is less invasive.
150 let range = sema.diagnostics_display_range(d.display_source()).range;
151 let range = range.intersect(TextRange::up_to(TextSize::of("..."))).unwrap_or(range);
152
162 // Override severity and mark as unused. 153 // Override severity and mark as unused.
163 res.borrow_mut().push( 154 res.borrow_mut().push(
164 Diagnostic::hint( 155 Diagnostic::hint(range, d.message())
165 sema.diagnostics_display_range(d.display_source()).range, 156 .with_fix(d.fix(&sema, resolve))
166 d.message(), 157 .with_code(Some(d.code())),
167 )
168 .with_fix(d.fix(&sema))
169 .with_code(Some(d.code())),
170 ); 158 );
171 }) 159 })
172 .on::<hir::diagnostics::UnresolvedProcMacro, _>(|d| { 160 .on::<hir::diagnostics::UnresolvedProcMacro, _>(|d| {
@@ -221,15 +209,23 @@ pub(crate) fn diagnostics(
221 res.into_inner() 209 res.into_inner()
222} 210}
223 211
224fn diagnostic_with_fix<D: DiagnosticWithFix>(d: &D, sema: &Semantics<RootDatabase>) -> Diagnostic { 212fn diagnostic_with_fix<D: DiagnosticWithFix>(
213 d: &D,
214 sema: &Semantics<RootDatabase>,
215 resolve: bool,
216) -> Diagnostic {
225 Diagnostic::error(sema.diagnostics_display_range(d.display_source()).range, d.message()) 217 Diagnostic::error(sema.diagnostics_display_range(d.display_source()).range, d.message())
226 .with_fix(d.fix(&sema)) 218 .with_fix(d.fix(&sema, resolve))
227 .with_code(Some(d.code())) 219 .with_code(Some(d.code()))
228} 220}
229 221
230fn warning_with_fix<D: DiagnosticWithFix>(d: &D, sema: &Semantics<RootDatabase>) -> Diagnostic { 222fn warning_with_fix<D: DiagnosticWithFix>(
223 d: &D,
224 sema: &Semantics<RootDatabase>,
225 resolve: bool,
226) -> Diagnostic {
231 Diagnostic::hint(sema.diagnostics_display_range(d.display_source()).range, d.message()) 227 Diagnostic::hint(sema.diagnostics_display_range(d.display_source()).range, d.message())
232 .with_fix(d.fix(&sema)) 228 .with_fix(d.fix(&sema, resolve))
233 .with_code(Some(d.code())) 229 .with_code(Some(d.code()))
234} 230}
235 231
@@ -259,7 +255,8 @@ fn check_unnecessary_braces_in_use_statement(
259 255
260 acc.push( 256 acc.push(
261 Diagnostic::hint(use_range, "Unnecessary braces in use statement".to_string()) 257 Diagnostic::hint(use_range, "Unnecessary braces in use statement".to_string())
262 .with_fix(Some(Fix::new( 258 .with_fix(Some(fix(
259 "remove_braces",
263 "Remove unnecessary braces", 260 "Remove unnecessary braces",
264 SourceChange::from_text_edit(file_id, edit), 261 SourceChange::from_text_edit(file_id, edit),
265 use_range, 262 use_range,
@@ -282,6 +279,23 @@ fn text_edit_for_remove_unnecessary_braces_with_self_in_use_statement(
282 None 279 None
283} 280}
284 281
282fn fix(id: &'static str, label: &str, source_change: SourceChange, target: TextRange) -> Assist {
283 let mut res = unresolved_fix(id, label, target);
284 res.source_change = Some(source_change);
285 res
286}
287
288fn unresolved_fix(id: &'static str, label: &str, target: TextRange) -> Assist {
289 assert!(!id.contains(' '));
290 Assist {
291 id: AssistId(id, AssistKind::QuickFix),
292 label: Label::new(label),
293 group: None,
294 target,
295 source_change: None,
296 }
297}
298
285#[cfg(test)] 299#[cfg(test)]
286mod tests { 300mod tests {
287 use expect_test::{expect, Expect}; 301 use expect_test::{expect, Expect};
@@ -300,16 +314,17 @@ mod tests {
300 314
301 let (analysis, file_position) = fixture::position(ra_fixture_before); 315 let (analysis, file_position) = fixture::position(ra_fixture_before);
302 let diagnostic = analysis 316 let diagnostic = analysis
303 .diagnostics(&DiagnosticsConfig::default(), file_position.file_id) 317 .diagnostics(&DiagnosticsConfig::default(), true, file_position.file_id)
304 .unwrap() 318 .unwrap()
305 .pop() 319 .pop()
306 .unwrap(); 320 .unwrap();
307 let fix = diagnostic.fix.unwrap(); 321 let fix = diagnostic.fix.unwrap();
308 let actual = { 322 let actual = {
309 let file_id = *fix.source_change.source_file_edits.keys().next().unwrap(); 323 let source_change = fix.source_change.unwrap();
324 let file_id = *source_change.source_file_edits.keys().next().unwrap();
310 let mut actual = analysis.file_text(file_id).unwrap().to_string(); 325 let mut actual = analysis.file_text(file_id).unwrap().to_string();
311 326
312 for edit in fix.source_change.source_file_edits.values() { 327 for edit in source_change.source_file_edits.values() {
313 edit.apply(&mut actual); 328 edit.apply(&mut actual);
314 } 329 }
315 actual 330 actual
@@ -317,9 +332,9 @@ mod tests {
317 332
318 assert_eq_text!(&after, &actual); 333 assert_eq_text!(&after, &actual);
319 assert!( 334 assert!(
320 fix.fix_trigger_range.contains_inclusive(file_position.offset), 335 fix.target.contains_inclusive(file_position.offset),
321 "diagnostic fix range {:?} does not touch cursor position {:?}", 336 "diagnostic fix range {:?} does not touch cursor position {:?}",
322 fix.fix_trigger_range, 337 fix.target,
323 file_position.offset 338 file_position.offset
324 ); 339 );
325 } 340 }
@@ -328,7 +343,7 @@ mod tests {
328 fn check_no_fix(ra_fixture: &str) { 343 fn check_no_fix(ra_fixture: &str) {
329 let (analysis, file_position) = fixture::position(ra_fixture); 344 let (analysis, file_position) = fixture::position(ra_fixture);
330 let diagnostic = analysis 345 let diagnostic = analysis
331 .diagnostics(&DiagnosticsConfig::default(), file_position.file_id) 346 .diagnostics(&DiagnosticsConfig::default(), true, file_position.file_id)
332 .unwrap() 347 .unwrap()
333 .pop() 348 .pop()
334 .unwrap(); 349 .unwrap();
@@ -342,7 +357,7 @@ mod tests {
342 let diagnostics = files 357 let diagnostics = files
343 .into_iter() 358 .into_iter()
344 .flat_map(|file_id| { 359 .flat_map(|file_id| {
345 analysis.diagnostics(&DiagnosticsConfig::default(), file_id).unwrap() 360 analysis.diagnostics(&DiagnosticsConfig::default(), true, file_id).unwrap()
346 }) 361 })
347 .collect::<Vec<_>>(); 362 .collect::<Vec<_>>();
348 assert_eq!(diagnostics.len(), 0, "unexpected diagnostics:\n{:#?}", diagnostics); 363 assert_eq!(diagnostics.len(), 0, "unexpected diagnostics:\n{:#?}", diagnostics);
@@ -350,7 +365,8 @@ mod tests {
350 365
351 fn check_expect(ra_fixture: &str, expect: Expect) { 366 fn check_expect(ra_fixture: &str, expect: Expect) {
352 let (analysis, file_id) = fixture::file(ra_fixture); 367 let (analysis, file_id) = fixture::file(ra_fixture);
353 let diagnostics = analysis.diagnostics(&DiagnosticsConfig::default(), file_id).unwrap(); 368 let diagnostics =
369 analysis.diagnostics(&DiagnosticsConfig::default(), true, file_id).unwrap();
354 expect.assert_debug_eq(&diagnostics) 370 expect.assert_debug_eq(&diagnostics)
355 } 371 }
356 372
@@ -663,24 +679,31 @@ fn test_fn() {
663 range: 0..8, 679 range: 0..8,
664 severity: Error, 680 severity: Error,
665 fix: Some( 681 fix: Some(
666 Fix { 682 Assist {
683 id: AssistId(
684 "create_module",
685 QuickFix,
686 ),
667 label: "Create module", 687 label: "Create module",
668 source_change: SourceChange { 688 group: None,
669 source_file_edits: {}, 689 target: 0..8,
670 file_system_edits: [ 690 source_change: Some(
671 CreateFile { 691 SourceChange {
672 dst: AnchoredPathBuf { 692 source_file_edits: {},
673 anchor: FileId( 693 file_system_edits: [
674 0, 694 CreateFile {
675 ), 695 dst: AnchoredPathBuf {
676 path: "foo.rs", 696 anchor: FileId(
697 0,
698 ),
699 path: "foo.rs",
700 },
701 initial_contents: "",
677 }, 702 },
678 initial_contents: "", 703 ],
679 }, 704 is_snippet: false,
680 ], 705 },
681 is_snippet: false, 706 ),
682 },
683 fix_trigger_range: 0..8,
684 }, 707 },
685 ), 708 ),
686 unused: false, 709 unused: false,
@@ -702,7 +725,7 @@ fn test_fn() {
702 expect![[r#" 725 expect![[r#"
703 [ 726 [
704 Diagnostic { 727 Diagnostic {
705 message: "unresolved macro call", 728 message: "unresolved macro `foo::bar!`",
706 range: 5..8, 729 range: 5..8,
707 severity: Error, 730 severity: Error,
708 fix: None, 731 fix: None,
@@ -888,10 +911,11 @@ struct Foo {
888 911
889 let (analysis, file_id) = fixture::file(r#"mod foo;"#); 912 let (analysis, file_id) = fixture::file(r#"mod foo;"#);
890 913
891 let diagnostics = analysis.diagnostics(&config, file_id).unwrap(); 914 let diagnostics = analysis.diagnostics(&config, true, file_id).unwrap();
892 assert!(diagnostics.is_empty()); 915 assert!(diagnostics.is_empty());
893 916
894 let diagnostics = analysis.diagnostics(&DiagnosticsConfig::default(), file_id).unwrap(); 917 let diagnostics =
918 analysis.diagnostics(&DiagnosticsConfig::default(), true, file_id).unwrap();
895 assert!(!diagnostics.is_empty()); 919 assert!(!diagnostics.is_empty());
896 } 920 }
897 921
@@ -997,8 +1021,9 @@ impl TestStruct {
997 let expected = r#"fn foo() {}"#; 1021 let expected = r#"fn foo() {}"#;
998 1022
999 let (analysis, file_position) = fixture::position(input); 1023 let (analysis, file_position) = fixture::position(input);
1000 let diagnostics = 1024 let diagnostics = analysis
1001 analysis.diagnostics(&DiagnosticsConfig::default(), file_position.file_id).unwrap(); 1025 .diagnostics(&DiagnosticsConfig::default(), true, file_position.file_id)
1026 .unwrap();
1002 assert_eq!(diagnostics.len(), 1); 1027 assert_eq!(diagnostics.len(), 1);
1003 1028
1004 check_fix(input, expected); 1029 check_fix(input, expected);
diff --git a/crates/ide/src/diagnostics/field_shorthand.rs b/crates/ide/src/diagnostics/field_shorthand.rs
index 5c89e2170..2b1787f9b 100644
--- a/crates/ide/src/diagnostics/field_shorthand.rs
+++ b/crates/ide/src/diagnostics/field_shorthand.rs
@@ -5,7 +5,7 @@ use ide_db::{base_db::FileId, source_change::SourceChange};
5use syntax::{ast, match_ast, AstNode, SyntaxNode}; 5use syntax::{ast, match_ast, AstNode, SyntaxNode};
6use text_edit::TextEdit; 6use text_edit::TextEdit;
7 7
8use crate::{Diagnostic, Fix}; 8use crate::{diagnostics::fix, Diagnostic};
9 9
10pub(super) fn check(acc: &mut Vec<Diagnostic>, file_id: FileId, node: &SyntaxNode) { 10pub(super) fn check(acc: &mut Vec<Diagnostic>, file_id: FileId, node: &SyntaxNode) {
11 match_ast! { 11 match_ast! {
@@ -47,7 +47,8 @@ fn check_expr_field_shorthand(
47 let field_range = record_field.syntax().text_range(); 47 let field_range = record_field.syntax().text_range();
48 acc.push( 48 acc.push(
49 Diagnostic::hint(field_range, "Shorthand struct initialization".to_string()).with_fix( 49 Diagnostic::hint(field_range, "Shorthand struct initialization".to_string()).with_fix(
50 Some(Fix::new( 50 Some(fix(
51 "use_expr_field_shorthand",
51 "Use struct shorthand initialization", 52 "Use struct shorthand initialization",
52 SourceChange::from_text_edit(file_id, edit), 53 SourceChange::from_text_edit(file_id, edit),
53 field_range, 54 field_range,
@@ -86,7 +87,8 @@ fn check_pat_field_shorthand(
86 87
87 let field_range = record_pat_field.syntax().text_range(); 88 let field_range = record_pat_field.syntax().text_range();
88 acc.push(Diagnostic::hint(field_range, "Shorthand struct pattern".to_string()).with_fix( 89 acc.push(Diagnostic::hint(field_range, "Shorthand struct pattern".to_string()).with_fix(
89 Some(Fix::new( 90 Some(fix(
91 "use_pat_field_shorthand",
90 "Use struct field shorthand", 92 "Use struct field shorthand",
91 SourceChange::from_text_edit(file_id, edit), 93 SourceChange::from_text_edit(file_id, edit),
92 field_range, 94 field_range,
diff --git a/crates/ide/src/diagnostics/fixes.rs b/crates/ide/src/diagnostics/fixes.rs
index 5fb3e2d91..7be8b3459 100644
--- a/crates/ide/src/diagnostics/fixes.rs
+++ b/crates/ide/src/diagnostics/fixes.rs
@@ -20,20 +20,30 @@ use syntax::{
20}; 20};
21use text_edit::TextEdit; 21use text_edit::TextEdit;
22 22
23use crate::{diagnostics::Fix, references::rename::rename_with_semantics, FilePosition}; 23use crate::{
24 diagnostics::{fix, unresolved_fix},
25 references::rename::rename_with_semantics,
26 Assist, FilePosition,
27};
24 28
25/// A [Diagnostic] that potentially has a fix available. 29/// A [Diagnostic] that potentially has a fix available.
26/// 30///
27/// [Diagnostic]: hir::diagnostics::Diagnostic 31/// [Diagnostic]: hir::diagnostics::Diagnostic
28pub(crate) trait DiagnosticWithFix: Diagnostic { 32pub(crate) trait DiagnosticWithFix: Diagnostic {
29 fn fix(&self, sema: &Semantics<RootDatabase>) -> Option<Fix>; 33 /// `resolve` determines if the diagnostic should fill in the `edit` field
34 /// of the assist.
35 ///
36 /// If `resolve` is false, the edit will be computed later, on demand, and
37 /// can be omitted.
38 fn fix(&self, sema: &Semantics<RootDatabase>, _resolve: bool) -> Option<Assist>;
30} 39}
31 40
32impl DiagnosticWithFix for UnresolvedModule { 41impl DiagnosticWithFix for UnresolvedModule {
33 fn fix(&self, sema: &Semantics<RootDatabase>) -> Option<Fix> { 42 fn fix(&self, sema: &Semantics<RootDatabase>, _resolve: bool) -> Option<Assist> {
34 let root = sema.db.parse_or_expand(self.file)?; 43 let root = sema.db.parse_or_expand(self.file)?;
35 let unresolved_module = self.decl.to_node(&root); 44 let unresolved_module = self.decl.to_node(&root);
36 Some(Fix::new( 45 Some(fix(
46 "create_module",
37 "Create module", 47 "Create module",
38 FileSystemEdit::CreateFile { 48 FileSystemEdit::CreateFile {
39 dst: AnchoredPathBuf { 49 dst: AnchoredPathBuf {
@@ -49,7 +59,7 @@ impl DiagnosticWithFix for UnresolvedModule {
49} 59}
50 60
51impl DiagnosticWithFix for NoSuchField { 61impl DiagnosticWithFix for NoSuchField {
52 fn fix(&self, sema: &Semantics<RootDatabase>) -> Option<Fix> { 62 fn fix(&self, sema: &Semantics<RootDatabase>, _resolve: bool) -> Option<Assist> {
53 let root = sema.db.parse_or_expand(self.file)?; 63 let root = sema.db.parse_or_expand(self.file)?;
54 missing_record_expr_field_fix( 64 missing_record_expr_field_fix(
55 &sema, 65 &sema,
@@ -60,7 +70,7 @@ impl DiagnosticWithFix for NoSuchField {
60} 70}
61 71
62impl DiagnosticWithFix for MissingFields { 72impl DiagnosticWithFix for MissingFields {
63 fn fix(&self, sema: &Semantics<RootDatabase>) -> Option<Fix> { 73 fn fix(&self, sema: &Semantics<RootDatabase>, _resolve: bool) -> Option<Assist> {
64 // Note that although we could add a diagnostics to 74 // Note that although we could add a diagnostics to
65 // fill the missing tuple field, e.g : 75 // fill the missing tuple field, e.g :
66 // `struct A(usize);` 76 // `struct A(usize);`
@@ -86,7 +96,8 @@ impl DiagnosticWithFix for MissingFields {
86 .into_text_edit(&mut builder); 96 .into_text_edit(&mut builder);
87 builder.finish() 97 builder.finish()
88 }; 98 };
89 Some(Fix::new( 99 Some(fix(
100 "fill_missing_fields",
90 "Fill struct fields", 101 "Fill struct fields",
91 SourceChange::from_text_edit(self.file.original_file(sema.db), edit), 102 SourceChange::from_text_edit(self.file.original_file(sema.db), edit),
92 sema.original_range(&field_list_parent.syntax()).range, 103 sema.original_range(&field_list_parent.syntax()).range,
@@ -95,7 +106,7 @@ impl DiagnosticWithFix for MissingFields {
95} 106}
96 107
97impl DiagnosticWithFix for MissingOkOrSomeInTailExpr { 108impl DiagnosticWithFix for MissingOkOrSomeInTailExpr {
98 fn fix(&self, sema: &Semantics<RootDatabase>) -> Option<Fix> { 109 fn fix(&self, sema: &Semantics<RootDatabase>, _resolve: bool) -> Option<Assist> {
99 let root = sema.db.parse_or_expand(self.file)?; 110 let root = sema.db.parse_or_expand(self.file)?;
100 let tail_expr = self.expr.to_node(&root); 111 let tail_expr = self.expr.to_node(&root);
101 let tail_expr_range = tail_expr.syntax().text_range(); 112 let tail_expr_range = tail_expr.syntax().text_range();
@@ -103,12 +114,12 @@ impl DiagnosticWithFix for MissingOkOrSomeInTailExpr {
103 let edit = TextEdit::replace(tail_expr_range, replacement); 114 let edit = TextEdit::replace(tail_expr_range, replacement);
104 let source_change = SourceChange::from_text_edit(self.file.original_file(sema.db), edit); 115 let source_change = SourceChange::from_text_edit(self.file.original_file(sema.db), edit);
105 let name = if self.required == "Ok" { "Wrap with Ok" } else { "Wrap with Some" }; 116 let name = if self.required == "Ok" { "Wrap with Ok" } else { "Wrap with Some" };
106 Some(Fix::new(name, source_change, tail_expr_range)) 117 Some(fix("wrap_tail_expr", name, source_change, tail_expr_range))
107 } 118 }
108} 119}
109 120
110impl DiagnosticWithFix for RemoveThisSemicolon { 121impl DiagnosticWithFix for RemoveThisSemicolon {
111 fn fix(&self, sema: &Semantics<RootDatabase>) -> Option<Fix> { 122 fn fix(&self, sema: &Semantics<RootDatabase>, _resolve: bool) -> Option<Assist> {
112 let root = sema.db.parse_or_expand(self.file)?; 123 let root = sema.db.parse_or_expand(self.file)?;
113 124
114 let semicolon = self 125 let semicolon = self
@@ -123,12 +134,12 @@ impl DiagnosticWithFix for RemoveThisSemicolon {
123 let edit = TextEdit::delete(semicolon); 134 let edit = TextEdit::delete(semicolon);
124 let source_change = SourceChange::from_text_edit(self.file.original_file(sema.db), edit); 135 let source_change = SourceChange::from_text_edit(self.file.original_file(sema.db), edit);
125 136
126 Some(Fix::new("Remove this semicolon", source_change, semicolon)) 137 Some(fix("remove_semicolon", "Remove this semicolon", source_change, semicolon))
127 } 138 }
128} 139}
129 140
130impl DiagnosticWithFix for IncorrectCase { 141impl DiagnosticWithFix for IncorrectCase {
131 fn fix(&self, sema: &Semantics<RootDatabase>) -> Option<Fix> { 142 fn fix(&self, sema: &Semantics<RootDatabase>, resolve: bool) -> Option<Assist> {
132 let root = sema.db.parse_or_expand(self.file)?; 143 let root = sema.db.parse_or_expand(self.file)?;
133 let name_node = self.ident.to_node(&root); 144 let name_node = self.ident.to_node(&root);
134 145
@@ -136,16 +147,19 @@ impl DiagnosticWithFix for IncorrectCase {
136 let frange = name_node.original_file_range(sema.db); 147 let frange = name_node.original_file_range(sema.db);
137 let file_position = FilePosition { file_id: frange.file_id, offset: frange.range.start() }; 148 let file_position = FilePosition { file_id: frange.file_id, offset: frange.range.start() };
138 149
139 let rename_changes =
140 rename_with_semantics(sema, file_position, &self.suggested_text).ok()?;
141
142 let label = format!("Rename to {}", self.suggested_text); 150 let label = format!("Rename to {}", self.suggested_text);
143 Some(Fix::new(&label, rename_changes, frange.range)) 151 let mut res = unresolved_fix("change_case", &label, frange.range);
152 if resolve {
153 let source_change = rename_with_semantics(sema, file_position, &self.suggested_text);
154 res.source_change = Some(source_change.ok().unwrap_or_default());
155 }
156
157 Some(res)
144 } 158 }
145} 159}
146 160
147impl DiagnosticWithFix for ReplaceFilterMapNextWithFindMap { 161impl DiagnosticWithFix for ReplaceFilterMapNextWithFindMap {
148 fn fix(&self, sema: &Semantics<RootDatabase>) -> Option<Fix> { 162 fn fix(&self, sema: &Semantics<RootDatabase>, _resolve: bool) -> Option<Assist> {
149 let root = sema.db.parse_or_expand(self.file)?; 163 let root = sema.db.parse_or_expand(self.file)?;
150 let next_expr = self.next_expr.to_node(&root); 164 let next_expr = self.next_expr.to_node(&root);
151 let next_call = ast::MethodCallExpr::cast(next_expr.syntax().clone())?; 165 let next_call = ast::MethodCallExpr::cast(next_expr.syntax().clone())?;
@@ -163,7 +177,8 @@ impl DiagnosticWithFix for ReplaceFilterMapNextWithFindMap {
163 177
164 let source_change = SourceChange::from_text_edit(self.file.original_file(sema.db), edit); 178 let source_change = SourceChange::from_text_edit(self.file.original_file(sema.db), edit);
165 179
166 Some(Fix::new( 180 Some(fix(
181 "replace_with_find_map",
167 "Replace filter_map(..).next() with find_map()", 182 "Replace filter_map(..).next() with find_map()",
168 source_change, 183 source_change,
169 trigger_range, 184 trigger_range,
@@ -175,7 +190,7 @@ fn missing_record_expr_field_fix(
175 sema: &Semantics<RootDatabase>, 190 sema: &Semantics<RootDatabase>,
176 usage_file_id: FileId, 191 usage_file_id: FileId,
177 record_expr_field: &ast::RecordExprField, 192 record_expr_field: &ast::RecordExprField,
178) -> Option<Fix> { 193) -> Option<Assist> {
179 let record_lit = ast::RecordExpr::cast(record_expr_field.syntax().parent()?.parent()?)?; 194 let record_lit = ast::RecordExpr::cast(record_expr_field.syntax().parent()?.parent()?)?;
180 let def_id = sema.resolve_variant(record_lit)?; 195 let def_id = sema.resolve_variant(record_lit)?;
181 let module; 196 let module;
@@ -233,7 +248,12 @@ fn missing_record_expr_field_fix(
233 def_file_id, 248 def_file_id,
234 TextEdit::insert(last_field_syntax.text_range().end(), new_field), 249 TextEdit::insert(last_field_syntax.text_range().end(), new_field),
235 ); 250 );
236 return Some(Fix::new("Create field", source_change, record_expr_field.syntax().text_range())); 251 return Some(fix(
252 "create_field",
253 "Create field",
254 source_change,
255 record_expr_field.syntax().text_range(),
256 ));
237 257
238 fn record_field_list(field_def_list: ast::FieldList) -> Option<ast::RecordFieldList> { 258 fn record_field_list(field_def_list: ast::FieldList) -> Option<ast::RecordFieldList> {
239 match field_def_list { 259 match field_def_list {
diff --git a/crates/ide/src/diagnostics/unlinked_file.rs b/crates/ide/src/diagnostics/unlinked_file.rs
index e174fb767..7d39f4fbe 100644
--- a/crates/ide/src/diagnostics/unlinked_file.rs
+++ b/crates/ide/src/diagnostics/unlinked_file.rs
@@ -16,9 +16,10 @@ use syntax::{
16}; 16};
17use text_edit::TextEdit; 17use text_edit::TextEdit;
18 18
19use crate::Fix; 19use crate::{
20 20 diagnostics::{fix, fixes::DiagnosticWithFix},
21use super::fixes::DiagnosticWithFix; 21 Assist,
22};
22 23
23// Diagnostic: unlinked-file 24// Diagnostic: unlinked-file
24// 25//
@@ -49,7 +50,7 @@ impl Diagnostic for UnlinkedFile {
49} 50}
50 51
51impl DiagnosticWithFix for UnlinkedFile { 52impl DiagnosticWithFix for UnlinkedFile {
52 fn fix(&self, sema: &hir::Semantics<RootDatabase>) -> Option<Fix> { 53 fn fix(&self, sema: &hir::Semantics<RootDatabase>, _resolve: bool) -> Option<Assist> {
53 // If there's an existing module that could add a `mod` item to include the unlinked file, 54 // If there's an existing module that could add a `mod` item to include the unlinked file,
54 // suggest that as a fix. 55 // suggest that as a fix.
55 56
@@ -100,7 +101,7 @@ fn make_fix(
100 parent_file_id: FileId, 101 parent_file_id: FileId,
101 new_mod_name: &str, 102 new_mod_name: &str,
102 added_file_id: FileId, 103 added_file_id: FileId,
103) -> Option<Fix> { 104) -> Option<Assist> {
104 fn is_outline_mod(item: &ast::Item) -> bool { 105 fn is_outline_mod(item: &ast::Item) -> bool {
105 matches!(item, ast::Item::Module(m) if m.item_list().is_none()) 106 matches!(item, ast::Item::Module(m) if m.item_list().is_none())
106 } 107 }
@@ -152,7 +153,8 @@ fn make_fix(
152 153
153 let edit = builder.finish(); 154 let edit = builder.finish();
154 let trigger_range = db.parse(added_file_id).tree().syntax().text_range(); 155 let trigger_range = db.parse(added_file_id).tree().syntax().text_range();
155 Some(Fix::new( 156 Some(fix(
157 "add_mod_declaration",
156 &format!("Insert `{}`", mod_decl), 158 &format!("Insert `{}`", mod_decl),
157 SourceChange::from_text_edit(parent_file_id, edit), 159 SourceChange::from_text_edit(parent_file_id, edit),
158 trigger_range, 160 trigger_range,
diff --git a/crates/ide/src/doc_links.rs b/crates/ide/src/doc_links.rs
index 67e2e5a1c..cb5a8e19a 100644
--- a/crates/ide/src/doc_links.rs
+++ b/crates/ide/src/doc_links.rs
@@ -1,6 +1,9 @@
1//! Extracts, resolves and rewrites links and intra-doc links in markdown documentation. 1//! Extracts, resolves and rewrites links and intra-doc links in markdown documentation.
2 2
3use std::{convert::TryFrom, iter::once, ops::Range}; 3use std::{
4 convert::{TryFrom, TryInto},
5 iter::once,
6};
4 7
5use itertools::Itertools; 8use itertools::Itertools;
6use pulldown_cmark::{BrokenLink, CowStr, Event, InlineStr, LinkType, Options, Parser, Tag}; 9use pulldown_cmark::{BrokenLink, CowStr, Event, InlineStr, LinkType, Options, Parser, Tag};
@@ -16,8 +19,7 @@ use ide_db::{
16 RootDatabase, 19 RootDatabase,
17}; 20};
18use syntax::{ 21use syntax::{
19 ast, match_ast, AstNode, AstToken, SyntaxKind::*, SyntaxNode, SyntaxToken, TextRange, TextSize, 22 ast, match_ast, AstNode, SyntaxKind::*, SyntaxNode, SyntaxToken, TextRange, TokenAtOffset, T,
20 TokenAtOffset, T,
21}; 23};
22 24
23use crate::{FilePosition, Semantics}; 25use crate::{FilePosition, Semantics};
@@ -26,12 +28,7 @@ pub(crate) type DocumentationLink = String;
26 28
27/// Rewrite documentation links in markdown to point to an online host (e.g. docs.rs) 29/// Rewrite documentation links in markdown to point to an online host (e.g. docs.rs)
28pub(crate) fn rewrite_links(db: &RootDatabase, markdown: &str, definition: &Definition) -> String { 30pub(crate) fn rewrite_links(db: &RootDatabase, markdown: &str, definition: &Definition) -> String {
29 let mut cb = |link: BrokenLink| { 31 let mut cb = broken_link_clone_cb;
30 Some((
31 /*url*/ link.reference.to_owned().into(),
32 /*title*/ link.reference.to_owned().into(),
33 ))
34 };
35 let doc = Parser::new_with_broken_link_callback(markdown, Options::empty(), Some(&mut cb)); 32 let doc = Parser::new_with_broken_link_callback(markdown, Options::empty(), Some(&mut cb));
36 33
37 let doc = map_links(doc, |target, title: &str| { 34 let doc = map_links(doc, |target, title: &str| {
@@ -111,86 +108,39 @@ pub(crate) fn external_docs(
111 let node = token.parent()?; 108 let node = token.parent()?;
112 let definition = match_ast! { 109 let definition = match_ast! {
113 match node { 110 match node {
114 ast::NameRef(name_ref) => NameRefClass::classify(&sema, &name_ref).map(|d| d.referenced(sema.db)), 111 ast::NameRef(name_ref) => NameRefClass::classify(&sema, &name_ref).map(|d| d.referenced(sema.db))?,
115 ast::Name(name) => NameClass::classify(&sema, &name).map(|d| d.referenced_or_defined(sema.db)), 112 ast::Name(name) => NameClass::classify(&sema, &name).map(|d| d.referenced_or_defined(sema.db))?,
116 _ => None, 113 _ => return None,
117 } 114 }
118 }; 115 };
119 116
120 get_doc_link(db, definition?) 117 get_doc_link(db, definition)
121} 118}
122 119
123/// Extracts all links from a given markdown text. 120/// Extracts all links from a given markdown text.
124pub(crate) fn extract_definitions_from_markdown( 121pub(crate) fn extract_definitions_from_markdown(
125 markdown: &str, 122 markdown: &str,
126) -> Vec<(Range<usize>, String, Option<hir::Namespace>)> { 123) -> Vec<(TextRange, String, Option<hir::Namespace>)> {
127 let mut res = vec![]; 124 Parser::new_with_broken_link_callback(
128 let mut cb = |link: BrokenLink| { 125 markdown,
129 // These allocations are actually unnecessary but the lifetimes on BrokenLinkCallback are wrong 126 Options::empty(),
130 // this is fixed in the repo but not on the crates.io release yet 127 Some(&mut broken_link_clone_cb),
131 Some(( 128 )
132 /*url*/ link.reference.to_owned().into(), 129 .into_offset_iter()
133 /*title*/ link.reference.to_owned().into(), 130 .filter_map(|(event, range)| {
134 ))
135 };
136 let doc = Parser::new_with_broken_link_callback(markdown, Options::empty(), Some(&mut cb));
137 for (event, range) in doc.into_offset_iter() {
138 if let Event::Start(Tag::Link(_, target, title)) = event { 131 if let Event::Start(Tag::Link(_, target, title)) = event {
139 let link = if target.is_empty() { title } else { target }; 132 let link = if target.is_empty() { title } else { target };
140 let (link, ns) = parse_intra_doc_link(&link); 133 let (link, ns) = parse_intra_doc_link(&link);
141 res.push((range, link.to_string(), ns)); 134 Some((
142 } 135 TextRange::new(range.start.try_into().ok()?, range.end.try_into().ok()?),
143 } 136 link.to_string(),
144 res 137 ns,
145} 138 ))
146 139 } else {
147/// Extracts a link from a comment at the given position returning the spanning range, link and 140 None
148/// optionally it's namespace.
149pub(crate) fn extract_positioned_link_from_comment(
150 position: TextSize,
151 comment: &ast::Comment,
152) -> Option<(TextRange, String, Option<hir::Namespace>)> {
153 let doc_comment = comment.doc_comment()?;
154 let comment_start =
155 comment.syntax().text_range().start() + TextSize::from(comment.prefix().len() as u32);
156 let def_links = extract_definitions_from_markdown(doc_comment);
157 let (range, def_link, ns) =
158 def_links.into_iter().find_map(|(Range { start, end }, def_link, ns)| {
159 let range = TextRange::at(
160 comment_start + TextSize::from(start as u32),
161 TextSize::from((end - start) as u32),
162 );
163 range.contains(position).then(|| (range, def_link, ns))
164 })?;
165 Some((range, def_link, ns))
166}
167
168/// Turns a syntax node into it's [`Definition`] if it can hold docs.
169pub(crate) fn doc_owner_to_def(
170 sema: &Semantics<RootDatabase>,
171 item: &SyntaxNode,
172) -> Option<Definition> {
173 let res: hir::ModuleDef = match_ast! {
174 match item {
175 ast::SourceFile(_it) => sema.scope(item).module()?.into(),
176 ast::Fn(it) => sema.to_def(&it)?.into(),
177 ast::Struct(it) => sema.to_def(&it)?.into(),
178 ast::Enum(it) => sema.to_def(&it)?.into(),
179 ast::Union(it) => sema.to_def(&it)?.into(),
180 ast::Trait(it) => sema.to_def(&it)?.into(),
181 ast::Const(it) => sema.to_def(&it)?.into(),
182 ast::Static(it) => sema.to_def(&it)?.into(),
183 ast::TypeAlias(it) => sema.to_def(&it)?.into(),
184 ast::Variant(it) => sema.to_def(&it)?.into(),
185 ast::Trait(it) => sema.to_def(&it)?.into(),
186 ast::Impl(it) => return sema.to_def(&it).map(Definition::SelfType),
187 ast::Macro(it) => return sema.to_def(&it).map(Definition::Macro),
188 ast::TupleField(it) => return sema.to_def(&it).map(Definition::Field),
189 ast::RecordField(it) => return sema.to_def(&it).map(Definition::Field),
190 _ => return None,
191 } 141 }
192 }; 142 })
193 Some(Definition::ModuleDef(res)) 143 .collect()
194} 144}
195 145
196pub(crate) fn resolve_doc_path_for_def( 146pub(crate) fn resolve_doc_path_for_def(
@@ -220,6 +170,42 @@ pub(crate) fn resolve_doc_path_for_def(
220 } 170 }
221} 171}
222 172
173pub(crate) fn doc_attributes(
174 sema: &Semantics<RootDatabase>,
175 node: &SyntaxNode,
176) -> Option<(hir::AttrsWithOwner, Definition)> {
177 match_ast! {
178 match node {
179 ast::SourceFile(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Module(def)))),
180 ast::Module(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Module(def)))),
181 ast::Fn(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Function(def)))),
182 ast::Struct(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Adt(hir::Adt::Struct(def))))),
183 ast::Union(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Adt(hir::Adt::Union(def))))),
184 ast::Enum(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Adt(hir::Adt::Enum(def))))),
185 ast::Variant(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Variant(def)))),
186 ast::Trait(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Trait(def)))),
187 ast::Static(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Static(def)))),
188 ast::Const(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Const(def)))),
189 ast::TypeAlias(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::TypeAlias(def)))),
190 ast::Impl(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::SelfType(def))),
191 ast::RecordField(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))),
194 // ast::Use(it) => sema.to_def(&it).map(|def| (Box::new(it) as _, def.attrs(sema.db))),
195 _ => return None
196 }
197 }
198}
199
200fn broken_link_clone_cb<'a, 'b>(link: BrokenLink<'a>) -> Option<(CowStr<'b>, CowStr<'b>)> {
201 // These allocations are actually unnecessary but the lifetimes on BrokenLinkCallback are wrong
202 // this is fixed in the repo but not on the crates.io release yet
203 Some((
204 /*url*/ link.reference.to_owned().into(),
205 /*title*/ link.reference.to_owned().into(),
206 ))
207}
208
223// FIXME: 209// FIXME:
224// BUG: For Option::Some 210// BUG: For Option::Some
225// Returns https://doc.rust-lang.org/nightly/core/prelude/v1/enum.Option.html#variant.Some 211// Returns https://doc.rust-lang.org/nightly/core/prelude/v1/enum.Option.html#variant.Some
@@ -228,20 +214,20 @@ pub(crate) fn resolve_doc_path_for_def(
228// This should cease to be a problem if RFC2988 (Stable Rustdoc URLs) is implemented 214// This should cease to be a problem if RFC2988 (Stable Rustdoc URLs) is implemented
229// https://github.com/rust-lang/rfcs/pull/2988 215// https://github.com/rust-lang/rfcs/pull/2988
230fn get_doc_link(db: &RootDatabase, definition: Definition) -> Option<String> { 216fn get_doc_link(db: &RootDatabase, definition: Definition) -> Option<String> {
231 // Get the outermost definition for the moduledef. This is used to resolve the public path to the type, 217 // Get the outermost definition for the module def. This is used to resolve the public path to the type,
232 // then we can join the method, field, etc onto it if required. 218 // then we can join the method, field, etc onto it if required.
233 let target_def: ModuleDef = match definition { 219 let target_def: ModuleDef = match definition {
234 Definition::ModuleDef(moddef) => match moddef { 220 Definition::ModuleDef(def) => match def {
235 ModuleDef::Function(f) => f 221 ModuleDef::Function(f) => f
236 .as_assoc_item(db) 222 .as_assoc_item(db)
237 .and_then(|assoc| match assoc.container(db) { 223 .and_then(|assoc| match assoc.container(db) {
238 AssocItemContainer::Trait(t) => Some(t.into()), 224 AssocItemContainer::Trait(t) => Some(t.into()),
239 AssocItemContainer::Impl(impld) => { 225 AssocItemContainer::Impl(impl_) => {
240 impld.self_ty(db).as_adt().map(|adt| adt.into()) 226 impl_.self_ty(db).as_adt().map(|adt| adt.into())
241 } 227 }
242 }) 228 })
243 .unwrap_or_else(|| f.clone().into()), 229 .unwrap_or_else(|| def),
244 moddef => moddef, 230 def => def,
245 }, 231 },
246 Definition::Field(f) => f.parent_def(db).into(), 232 Definition::Field(f) => f.parent_def(db).into(),
247 // FIXME: Handle macros 233 // FIXME: Handle macros
@@ -250,17 +236,28 @@ fn get_doc_link(db: &RootDatabase, definition: Definition) -> Option<String> {
250 236
251 let ns = ItemInNs::from(target_def); 237 let ns = ItemInNs::from(target_def);
252 238
253 let module = definition.module(db)?; 239 let krate = match definition {
254 let krate = module.krate(); 240 // Definition::module gives back the parent module, we don't want that as it fails for root modules
241 Definition::ModuleDef(ModuleDef::Module(module)) => module.krate(),
242 _ => definition.module(db)?.krate(),
243 };
255 let import_map = db.import_map(krate.into()); 244 let import_map = db.import_map(krate.into());
256 let base = once(krate.display_name(db)?.to_string()) 245
257 .chain(import_map.path_of(ns)?.segments.iter().map(|name| name.to_string())) 246 let mut base = krate.display_name(db)?.to_string();
258 .join("/") 247 let is_root_module = matches!(
259 + "/"; 248 definition,
249 Definition::ModuleDef(ModuleDef::Module(module)) if krate.root_module(db) == module
250 );
251 if !is_root_module {
252 base = once(base)
253 .chain(import_map.path_of(ns)?.segments.iter().map(|name| name.to_string()))
254 .join("/");
255 }
256 base += "/";
260 257
261 let filename = get_symbol_filename(db, &target_def); 258 let filename = get_symbol_filename(db, &target_def);
262 let fragment = match definition { 259 let fragment = match definition {
263 Definition::ModuleDef(moddef) => match moddef { 260 Definition::ModuleDef(def) => match def {
264 ModuleDef::Function(f) => { 261 ModuleDef::Function(f) => {
265 get_symbol_fragment(db, &FieldOrAssocItem::AssocItem(AssocItem::Function(f))) 262 get_symbol_fragment(db, &FieldOrAssocItem::AssocItem(AssocItem::Function(f)))
266 } 263 }
@@ -547,6 +544,19 @@ mod tests {
547 } 544 }
548 545
549 #[test] 546 #[test]
547 fn test_doc_url_crate() {
548 check(
549 r#"
550//- /main.rs crate:main deps:test
551use test$0::Foo;
552//- /lib.rs crate:test
553pub struct Foo;
554"#,
555 expect![[r#"https://docs.rs/test/*/test/index.html"#]],
556 );
557 }
558
559 #[test]
550 fn test_doc_url_struct() { 560 fn test_doc_url_struct() {
551 check( 561 check(
552 r#" 562 r#"
diff --git a/crates/ide/src/expand_macro.rs b/crates/ide/src/expand_macro.rs
index 9eeabbeda..eebae5ebe 100644
--- a/crates/ide/src/expand_macro.rs
+++ b/crates/ide/src/expand_macro.rs
@@ -1,9 +1,9 @@
1use std::iter;
2
1use hir::Semantics; 3use hir::Semantics;
2use ide_db::RootDatabase; 4use ide_db::RootDatabase;
3use syntax::{ 5use syntax::{
4 algo::{find_node_at_offset, SyntaxRewriter}, 6 algo::find_node_at_offset, ast, ted, AstNode, NodeOrToken, SyntaxKind, SyntaxKind::*,
5 ast, AstNode, NodeOrToken, SyntaxKind,
6 SyntaxKind::*,
7 SyntaxNode, WalkEvent, T, 7 SyntaxNode, WalkEvent, T,
8}; 8};
9 9
@@ -44,26 +44,23 @@ fn expand_macro_recur(
44 sema: &Semantics<RootDatabase>, 44 sema: &Semantics<RootDatabase>,
45 macro_call: &ast::MacroCall, 45 macro_call: &ast::MacroCall,
46) -> Option<SyntaxNode> { 46) -> Option<SyntaxNode> {
47 let mut expanded = sema.expand(macro_call)?; 47 let expanded = sema.expand(macro_call)?.clone_for_update();
48 48
49 let children = expanded.descendants().filter_map(ast::MacroCall::cast); 49 let children = expanded.descendants().filter_map(ast::MacroCall::cast);
50 let mut rewriter = SyntaxRewriter::default(); 50 let mut replacements = Vec::new();
51 51
52 for child in children.into_iter() { 52 for child in children {
53 if let Some(new_node) = expand_macro_recur(sema, &child) { 53 if let Some(new_node) = expand_macro_recur(sema, &child) {
54 // Replace the whole node if it is root 54 // check if the whole original syntax is replaced
55 // `replace_descendants` will not replace the parent node
56 // but `SyntaxNode::descendants include itself
57 if expanded == *child.syntax() { 55 if expanded == *child.syntax() {
58 expanded = new_node; 56 return Some(new_node);
59 } else {
60 rewriter.replace(child.syntax(), &new_node)
61 } 57 }
58 replacements.push((child, new_node));
62 } 59 }
63 } 60 }
64 61
65 let res = rewriter.rewrite(&expanded); 62 replacements.into_iter().rev().for_each(|(old, new)| ted::replace(old.syntax(), new));
66 Some(res) 63 Some(expanded)
67} 64}
68 65
69// FIXME: It would also be cool to share logic here and in the mbe tests, 66// FIXME: It would also be cool to share logic here and in the mbe tests,
@@ -91,24 +88,42 @@ fn insert_whitespaces(syn: SyntaxNode) -> String {
91 let is_last = 88 let is_last =
92 |f: fn(SyntaxKind) -> bool, default| -> bool { last.map(f).unwrap_or(default) }; 89 |f: fn(SyntaxKind) -> bool, default| -> bool { last.map(f).unwrap_or(default) };
93 90
94 res += &match token.kind() { 91 match token.kind() {
95 k if is_text(k) && is_next(|it| !it.is_punct(), true) => token.text().to_string() + " ", 92 k if is_text(k) && is_next(|it| !it.is_punct(), true) => {
93 res.push_str(token.text());
94 res.push(' ');
95 }
96 L_CURLY if is_next(|it| it != R_CURLY, true) => { 96 L_CURLY if is_next(|it| it != R_CURLY, true) => {
97 indent += 1; 97 indent += 1;
98 let leading_space = if is_last(is_text, false) { " " } else { "" }; 98 if is_last(is_text, false) {
99 format!("{}{{\n{}", leading_space, " ".repeat(indent)) 99 res.push(' ');
100 }
101 res.push_str("{\n");
102 res.extend(iter::repeat(" ").take(2 * indent));
100 } 103 }
101 R_CURLY if is_last(|it| it != L_CURLY, true) => { 104 R_CURLY if is_last(|it| it != L_CURLY, true) => {
102 indent = indent.saturating_sub(1); 105 indent = indent.saturating_sub(1);
103 format!("\n{}}}", " ".repeat(indent)) 106 res.push('\n');
107 res.extend(iter::repeat(" ").take(2 * indent));
108 res.push_str("}");
104 } 109 }
105 R_CURLY => format!("}}\n{}", " ".repeat(indent)), 110 R_CURLY => {
106 T![;] => format!(";\n{}", " ".repeat(indent)), 111 res.push_str("}\n");
107 T![->] => " -> ".to_string(), 112 res.extend(iter::repeat(" ").take(2 * indent));
108 T![=] => " = ".to_string(), 113 }
109 T![=>] => " => ".to_string(), 114 LIFETIME_IDENT if is_next(|it| it == IDENT, true) => {
110 _ => token.text().to_string(), 115 res.push_str(token.text());
111 }; 116 res.push(' ');
117 }
118 T![;] => {
119 res.push_str(";\n");
120 res.extend(iter::repeat(" ").take(2 * indent));
121 }
122 T![->] => res.push_str(" -> "),
123 T![=] => res.push_str(" = "),
124 T![=>] => res.push_str(" => "),
125 _ => res.push_str(token.text()),
126 }
112 127
113 last = Some(token.kind()); 128 last = Some(token.kind());
114 } 129 }
diff --git a/crates/ide/src/folding_ranges.rs b/crates/ide/src/folding_ranges.rs
index 153726ce8..2b9ed123c 100644
--- a/crates/ide/src/folding_ranges.rs
+++ b/crates/ide/src/folding_ranges.rs
@@ -19,6 +19,7 @@ pub enum FoldKind {
19 Region, 19 Region,
20 Consts, 20 Consts,
21 Statics, 21 Statics,
22 Array,
22} 23}
23 24
24#[derive(Debug)] 25#[derive(Debug)]
@@ -119,6 +120,7 @@ fn fold_kind(kind: SyntaxKind) -> Option<FoldKind> {
119 match kind { 120 match kind {
120 COMMENT => Some(FoldKind::Comment), 121 COMMENT => Some(FoldKind::Comment),
121 ARG_LIST | PARAM_LIST => Some(FoldKind::ArgList), 122 ARG_LIST | PARAM_LIST => Some(FoldKind::ArgList),
123 ARRAY_EXPR => Some(FoldKind::Array),
122 ASSOC_ITEM_LIST 124 ASSOC_ITEM_LIST
123 | RECORD_FIELD_LIST 125 | RECORD_FIELD_LIST
124 | RECORD_PAT_FIELD_LIST 126 | RECORD_PAT_FIELD_LIST
@@ -269,6 +271,7 @@ mod tests {
269 FoldKind::Region => "region", 271 FoldKind::Region => "region",
270 FoldKind::Consts => "consts", 272 FoldKind::Consts => "consts",
271 FoldKind::Statics => "statics", 273 FoldKind::Statics => "statics",
274 FoldKind::Array => "array",
272 }; 275 };
273 assert_eq!(kind, &attr.unwrap()); 276 assert_eq!(kind, &attr.unwrap());
274 } 277 }
@@ -465,6 +468,20 @@ fn foo<fold arglist>(
465 } 468 }
466 469
467 #[test] 470 #[test]
471 fn fold_multiline_array() {
472 check(
473 r#"
474const FOO: [usize; 4] = <fold array>[
475 1,
476 2,
477 3,
478 4,
479]</fold>;
480"#,
481 )
482 }
483
484 #[test]
468 fn fold_region() { 485 fn fold_region() {
469 check( 486 check(
470 r#" 487 r#"
diff --git a/crates/ide/src/goto_definition.rs b/crates/ide/src/goto_definition.rs
index 8574d1e3f..a04333e63 100644
--- a/crates/ide/src/goto_definition.rs
+++ b/crates/ide/src/goto_definition.rs
@@ -1,5 +1,5 @@
1use either::Either; 1use either::Either;
2use hir::Semantics; 2use hir::{InFile, Semantics};
3use ide_db::{ 3use ide_db::{
4 defs::{NameClass, NameRefClass}, 4 defs::{NameClass, NameRefClass},
5 RootDatabase, 5 RootDatabase,
@@ -8,7 +8,7 @@ use syntax::{ast, match_ast, AstNode, AstToken, SyntaxKind::*, SyntaxToken, Toke
8 8
9use crate::{ 9use crate::{
10 display::TryToNav, 10 display::TryToNav,
11 doc_links::{doc_owner_to_def, extract_positioned_link_from_comment, resolve_doc_path_for_def}, 11 doc_links::{doc_attributes, extract_definitions_from_markdown, resolve_doc_path_for_def},
12 FilePosition, NavigationTarget, RangeInfo, 12 FilePosition, NavigationTarget, RangeInfo,
13}; 13};
14 14
@@ -32,9 +32,16 @@ pub(crate) fn goto_definition(
32 let original_token = pick_best(file.token_at_offset(position.offset))?; 32 let original_token = pick_best(file.token_at_offset(position.offset))?;
33 let token = sema.descend_into_macros(original_token.clone()); 33 let token = sema.descend_into_macros(original_token.clone());
34 let parent = token.parent()?; 34 let parent = token.parent()?;
35 if let Some(comment) = ast::Comment::cast(token) { 35 if let Some(_) = ast::Comment::cast(token) {
36 let (_, link, ns) = extract_positioned_link_from_comment(position.offset, &comment)?; 36 let (attributes, def) = doc_attributes(&sema, &parent)?;
37 let def = doc_owner_to_def(&sema, &parent)?; 37
38 let (docs, doc_mapping) = attributes.docs_with_rangemap(db)?;
39 let (_, link, ns) =
40 extract_definitions_from_markdown(docs.as_str()).into_iter().find(|(range, ..)| {
41 doc_mapping.map(range.clone()).map_or(false, |InFile { file_id, value: range }| {
42 file_id == position.file_id.into() && range.contains(position.offset)
43 })
44 })?;
38 let nav = resolve_doc_path_for_def(db, def, &link, ns)?.try_to_nav(db)?; 45 let nav = resolve_doc_path_for_def(db, def, &link, ns)?.try_to_nav(db)?;
39 return Some(RangeInfo::new(original_token.text_range(), vec![nav])); 46 return Some(RangeInfo::new(original_token.text_range(), vec![nav]));
40 } 47 }
@@ -103,6 +110,13 @@ mod tests {
103 assert_eq!(expected, FileRange { file_id: nav.file_id, range: nav.focus_or_full_range() }); 110 assert_eq!(expected, FileRange { file_id: nav.file_id, range: nav.focus_or_full_range() });
104 } 111 }
105 112
113 fn check_unresolved(ra_fixture: &str) {
114 let (analysis, position) = fixture::position(ra_fixture);
115 let navs = analysis.goto_definition(position).unwrap().expect("no definition found").info;
116
117 assert!(navs.is_empty(), "didn't expect this to resolve anywhere: {:?}", navs)
118 }
119
106 #[test] 120 #[test]
107 fn goto_def_for_extern_crate() { 121 fn goto_def_for_extern_crate() {
108 check( 122 check(
@@ -920,17 +934,12 @@ fn f() -> impl Iterator<Item$0 = u8> {}
920 } 934 }
921 935
922 #[test] 936 #[test]
923 #[should_panic = "unresolved reference"]
924 fn unknown_assoc_ty() { 937 fn unknown_assoc_ty() {
925 check( 938 check_unresolved(
926 r#" 939 r#"
927trait Iterator { 940trait Iterator { type Item; }
928 type Item;
929 //^^^^
930}
931
932fn f() -> impl Iterator<Invalid$0 = u8> {} 941fn f() -> impl Iterator<Invalid$0 = u8> {}
933 "#, 942"#,
934 ) 943 )
935 } 944 }
936 945
@@ -1160,4 +1169,51 @@ fn fn_macro() {}
1160 "#, 1169 "#,
1161 ) 1170 )
1162 } 1171 }
1172
1173 #[test]
1174 fn goto_intra_doc_links() {
1175 check(
1176 r#"
1177
1178pub mod theitem {
1179 /// This is the item. Cool!
1180 pub struct TheItem;
1181 //^^^^^^^
1182}
1183
1184/// Gives you a [`TheItem$0`].
1185///
1186/// [`TheItem`]: theitem::TheItem
1187pub fn gimme() -> theitem::TheItem {
1188 theitem::TheItem
1189}
1190"#,
1191 );
1192 }
1193
1194 #[test]
1195 fn goto_ident_from_pat_macro() {
1196 check(
1197 r#"
1198macro_rules! pat {
1199 ($name:ident) => { Enum::Variant1($name) }
1200}
1201
1202enum Enum {
1203 Variant1(u8),
1204 Variant2,
1205}
1206
1207fn f(e: Enum) {
1208 match e {
1209 pat!(bind) => {
1210 //^^^^
1211 bind$0
1212 }
1213 Enum::Variant2 => {}
1214 }
1215}
1216"#,
1217 );
1218 }
1163} 1219}
diff --git a/crates/ide/src/hover.rs b/crates/ide/src/hover.rs
index 614433417..9de653739 100644
--- a/crates/ide/src/hover.rs
+++ b/crates/ide/src/hover.rs
@@ -1,6 +1,6 @@
1use either::Either; 1use either::Either;
2use hir::{ 2use hir::{
3 AsAssocItem, AssocItemContainer, GenericParam, HasAttrs, HasSource, HirDisplay, Module, 3 AsAssocItem, AssocItemContainer, GenericParam, HasAttrs, HasSource, HirDisplay, InFile, Module,
4 ModuleDef, Semantics, 4 ModuleDef, Semantics,
5}; 5};
6use ide_db::{ 6use ide_db::{
@@ -16,8 +16,8 @@ use syntax::{ast, match_ast, AstNode, AstToken, SyntaxKind::*, SyntaxToken, Toke
16use crate::{ 16use crate::{
17 display::{macro_label, TryToNav}, 17 display::{macro_label, TryToNav},
18 doc_links::{ 18 doc_links::{
19 doc_owner_to_def, extract_positioned_link_from_comment, remove_links, 19 doc_attributes, extract_definitions_from_markdown, remove_links, resolve_doc_path_for_def,
20 resolve_doc_path_for_def, rewrite_links, 20 rewrite_links,
21 }, 21 },
22 markdown_remove::remove_markdown, 22 markdown_remove::remove_markdown,
23 markup::Markup, 23 markup::Markup,
@@ -116,11 +116,19 @@ pub(crate) fn hover(
116 ), 116 ),
117 117
118 _ => ast::Comment::cast(token.clone()) 118 _ => ast::Comment::cast(token.clone())
119 .and_then(|comment| { 119 .and_then(|_| {
120 let (attributes, def) = doc_attributes(&sema, &node)?;
121 let (docs, doc_mapping) = attributes.docs_with_rangemap(db)?;
120 let (idl_range, link, ns) = 122 let (idl_range, link, ns) =
121 extract_positioned_link_from_comment(position.offset, &comment)?; 123 extract_definitions_from_markdown(docs.as_str()).into_iter().find_map(|(range, link, ns)| {
124 let InFile { file_id, value: range } = doc_mapping.map(range.clone())?;
125 if file_id == position.file_id.into() && range.contains(position.offset) {
126 Some((range, link, ns))
127 } else {
128 None
129 }
130 })?;
122 range = Some(idl_range); 131 range = Some(idl_range);
123 let def = doc_owner_to_def(&sema, &node)?;
124 resolve_doc_path_for_def(db, def, &link, ns) 132 resolve_doc_path_for_def(db, def, &link, ns)
125 }) 133 })
126 .map(Definition::ModuleDef), 134 .map(Definition::ModuleDef),
@@ -3814,23 +3822,33 @@ fn main() {
3814 fn hover_intra_doc_links() { 3822 fn hover_intra_doc_links() {
3815 check( 3823 check(
3816 r#" 3824 r#"
3817/// This is the [`foo`](foo$0) function. 3825
3818fn foo() {} 3826pub mod theitem {
3827 /// This is the item. Cool!
3828 pub struct TheItem;
3829}
3830
3831/// Gives you a [`TheItem$0`].
3832///
3833/// [`TheItem`]: theitem::TheItem
3834pub fn gimme() -> theitem::TheItem {
3835 theitem::TheItem
3836}
3819"#, 3837"#,
3820 expect![[r#" 3838 expect![[r#"
3821 *[`foo`](foo)* 3839 *[`TheItem`]*
3822 3840
3823 ```rust 3841 ```rust
3824 test 3842 test::theitem
3825 ``` 3843 ```
3826 3844
3827 ```rust 3845 ```rust
3828 fn foo() 3846 pub struct TheItem
3829 ``` 3847 ```
3830 3848
3831 --- 3849 ---
3832 3850
3833 This is the [`foo`](https://docs.rs/test/*/test/fn.foo.html) function. 3851 This is the item. Cool!
3834 "#]], 3852 "#]],
3835 ); 3853 );
3836 } 3854 }
diff --git a/crates/ide/src/lib.rs b/crates/ide/src/lib.rs
index 3f73c0632..99e45633e 100644
--- a/crates/ide/src/lib.rs
+++ b/crates/ide/src/lib.rs
@@ -69,7 +69,7 @@ use crate::display::ToNav;
69pub use crate::{ 69pub use crate::{
70 annotations::{Annotation, AnnotationConfig, AnnotationKind}, 70 annotations::{Annotation, AnnotationConfig, AnnotationKind},
71 call_hierarchy::CallItem, 71 call_hierarchy::CallItem,
72 diagnostics::{Diagnostic, DiagnosticsConfig, Fix, Severity}, 72 diagnostics::{Diagnostic, DiagnosticsConfig, Severity},
73 display::navigation_target::NavigationTarget, 73 display::navigation_target::NavigationTarget,
74 expand_macro::ExpandedMacro, 74 expand_macro::ExpandedMacro,
75 file_structure::{StructureNode, StructureNodeKind}, 75 file_structure::{StructureNode, StructureNodeKind},
@@ -82,7 +82,7 @@ pub use crate::{
82 references::{rename::RenameError, ReferenceSearchResult}, 82 references::{rename::RenameError, ReferenceSearchResult},
83 runnables::{Runnable, RunnableKind, TestId}, 83 runnables::{Runnable, RunnableKind, TestId},
84 syntax_highlighting::{ 84 syntax_highlighting::{
85 tags::{Highlight, HlMod, HlMods, HlPunct, HlTag}, 85 tags::{Highlight, HlMod, HlMods, HlOperator, HlPunct, HlTag},
86 HlRange, 86 HlRange,
87 }, 87 },
88}; 88};
@@ -244,6 +244,12 @@ impl Analysis {
244 self.with_db(|db| db.parse(file_id).tree()) 244 self.with_db(|db| db.parse(file_id).tree())
245 } 245 }
246 246
247 /// Returns true if this file belongs to an immutable library.
248 pub fn is_library_file(&self, file_id: FileId) -> Cancelable<bool> {
249 use ide_db::base_db::SourceDatabaseExt;
250 self.with_db(|db| db.source_root(db.file_source_root(file_id)).is_library)
251 }
252
247 /// Gets the file's `LineIndex`: data structure to convert between absolute 253 /// Gets the file's `LineIndex`: data structure to convert between absolute
248 /// offsets and line/column representation. 254 /// offsets and line/column representation.
249 pub fn file_line_index(&self, file_id: FileId) -> Cancelable<Arc<LineIndex>> { 255 pub fn file_line_index(&self, file_id: FileId) -> Cancelable<Arc<LineIndex>> {
@@ -526,9 +532,39 @@ impl Analysis {
526 pub fn diagnostics( 532 pub fn diagnostics(
527 &self, 533 &self,
528 config: &DiagnosticsConfig, 534 config: &DiagnosticsConfig,
535 resolve: bool,
529 file_id: FileId, 536 file_id: FileId,
530 ) -> Cancelable<Vec<Diagnostic>> { 537 ) -> Cancelable<Vec<Diagnostic>> {
531 self.with_db(|db| diagnostics::diagnostics(db, config, file_id)) 538 self.with_db(|db| diagnostics::diagnostics(db, config, resolve, file_id))
539 }
540
541 /// Convenience function to return assists + quick fixes for diagnostics
542 pub fn assists_with_fixes(
543 &self,
544 assist_config: &AssistConfig,
545 diagnostics_config: &DiagnosticsConfig,
546 resolve: bool,
547 frange: FileRange,
548 ) -> Cancelable<Vec<Assist>> {
549 let include_fixes = match &assist_config.allowed {
550 Some(it) => it.iter().any(|&it| it == AssistKind::None || it == AssistKind::QuickFix),
551 None => true,
552 };
553
554 self.with_db(|db| {
555 let mut res = Assist::get(db, assist_config, resolve, frange);
556 ssr::add_ssr_assist(db, &mut res, resolve, frange);
557
558 if include_fixes {
559 res.extend(
560 diagnostics::diagnostics(db, diagnostics_config, resolve, frange.file_id)
561 .into_iter()
562 .filter_map(|it| it.fix)
563 .filter(|it| it.target.intersect(frange.range).is_some()),
564 );
565 }
566 res
567 })
532 } 568 }
533 569
534 /// Returns the edit required to rename reference at the position to the new 570 /// Returns the edit required to rename reference at the position to the new
diff --git a/crates/ide/src/move_item.rs b/crates/ide/src/move_item.rs
index 8d37f4f92..246f10a0a 100644
--- a/crates/ide/src/move_item.rs
+++ b/crates/ide/src/move_item.rs
@@ -1,4 +1,4 @@
1use std::iter::once; 1use std::{iter::once, mem};
2 2
3use hir::Semantics; 3use hir::Semantics;
4use ide_db::{base_db::FileRange, RootDatabase}; 4use ide_db::{base_db::FileRange, RootDatabase};
@@ -102,7 +102,7 @@ fn move_in_direction(
102 ast::GenericArgList(it) => swap_sibling_in_list(node, it.generic_args(), range, direction), 102 ast::GenericArgList(it) => swap_sibling_in_list(node, it.generic_args(), range, direction),
103 ast::VariantList(it) => swap_sibling_in_list(node, it.variants(), range, direction), 103 ast::VariantList(it) => swap_sibling_in_list(node, it.variants(), range, direction),
104 ast::TypeBoundList(it) => swap_sibling_in_list(node, it.bounds(), range, direction), 104 ast::TypeBoundList(it) => swap_sibling_in_list(node, it.bounds(), range, direction),
105 _ => Some(replace_nodes(node, &match direction { 105 _ => Some(replace_nodes(range, node, &match direction {
106 Direction::Up => node.prev_sibling(), 106 Direction::Up => node.prev_sibling(),
107 Direction::Down => node.next_sibling(), 107 Direction::Down => node.next_sibling(),
108 }?)) 108 }?))
@@ -125,7 +125,7 @@ fn swap_sibling_in_list<A: AstNode + Clone, I: Iterator<Item = A>>(
125 .next(); 125 .next();
126 126
127 if let Some((l, r)) = list_lookup { 127 if let Some((l, r)) = list_lookup {
128 Some(replace_nodes(l.syntax(), r.syntax())) 128 Some(replace_nodes(range, l.syntax(), r.syntax()))
129 } else { 129 } else {
130 // Cursor is beyond any movable list item (for example, on curly brace in enum). 130 // Cursor is beyond any movable list item (for example, on curly brace in enum).
131 // It's not necessary, that parent of list is movable (arg list's parent is not, for example), 131 // It's not necessary, that parent of list is movable (arg list's parent is not, for example),
@@ -134,11 +134,38 @@ fn swap_sibling_in_list<A: AstNode + Clone, I: Iterator<Item = A>>(
134 } 134 }
135} 135}
136 136
137fn replace_nodes(first: &SyntaxNode, second: &SyntaxNode) -> TextEdit { 137fn replace_nodes<'a>(
138 range: TextRange,
139 mut first: &'a SyntaxNode,
140 mut second: &'a SyntaxNode,
141) -> TextEdit {
142 let cursor_offset = if range.is_empty() {
143 // FIXME: `applySnippetTextEdits` does not support non-empty selection ranges
144 if first.text_range().contains_range(range) {
145 Some(range.start() - first.text_range().start())
146 } else if second.text_range().contains_range(range) {
147 mem::swap(&mut first, &mut second);
148 Some(range.start() - first.text_range().start())
149 } else {
150 None
151 }
152 } else {
153 None
154 };
155
156 let first_with_cursor = match cursor_offset {
157 Some(offset) => {
158 let mut item_text = first.text().to_string();
159 item_text.insert_str(offset.into(), "$0");
160 item_text
161 }
162 None => first.text().to_string(),
163 };
164
138 let mut edit = TextEditBuilder::default(); 165 let mut edit = TextEditBuilder::default();
139 166
140 algo::diff(first, second).into_text_edit(&mut edit); 167 algo::diff(first, second).into_text_edit(&mut edit);
141 algo::diff(second, first).into_text_edit(&mut edit); 168 edit.replace(second.text_range(), first_with_cursor);
142 169
143 edit.finish() 170 edit.finish()
144} 171}
@@ -188,7 +215,7 @@ fn main() {
188 expect![[r#" 215 expect![[r#"
189fn main() { 216fn main() {
190 match true { 217 match true {
191 false => { 218 false =>$0 {
192 println!("Test"); 219 println!("Test");
193 }, 220 },
194 true => { 221 true => {
@@ -222,7 +249,7 @@ fn main() {
222 false => { 249 false => {
223 println!("Test"); 250 println!("Test");
224 }, 251 },
225 true => { 252 true =>$0 {
226 println!("Hello, world"); 253 println!("Hello, world");
227 } 254 }
228 }; 255 };
@@ -274,7 +301,7 @@ fn main() {
274 "#, 301 "#,
275 expect![[r#" 302 expect![[r#"
276fn main() { 303fn main() {
277 let test2 = 456; 304 let test2$0 = 456;
278 let test = 123; 305 let test = 123;
279} 306}
280 "#]], 307 "#]],
@@ -293,7 +320,7 @@ fn main() {
293 "#, 320 "#,
294 expect![[r#" 321 expect![[r#"
295fn main() { 322fn main() {
296 println!("All I want to say is..."); 323 println!("All I want to say is...");$0
297 println!("Hello, world"); 324 println!("Hello, world");
298} 325}
299 "#]], 326 "#]],
@@ -313,7 +340,7 @@ fn main() {
313fn main() { 340fn main() {
314 if true { 341 if true {
315 println!("Test"); 342 println!("Test");
316 } 343 }$0
317 344
318 println!("Hello, world"); 345 println!("Hello, world");
319} 346}
@@ -334,7 +361,7 @@ fn main() {
334fn main() { 361fn main() {
335 for i in 0..10 { 362 for i in 0..10 {
336 println!("Test"); 363 println!("Test");
337 } 364 }$0
338 365
339 println!("Hello, world"); 366 println!("Hello, world");
340} 367}
@@ -355,7 +382,7 @@ fn main() {
355fn main() { 382fn main() {
356 loop { 383 loop {
357 println!("Test"); 384 println!("Test");
358 } 385 }$0
359 386
360 println!("Hello, world"); 387 println!("Hello, world");
361} 388}
@@ -376,7 +403,7 @@ fn main() {
376fn main() { 403fn main() {
377 while true { 404 while true {
378 println!("Test"); 405 println!("Test");
379 } 406 }$0
380 407
381 println!("Hello, world"); 408 println!("Hello, world");
382} 409}
@@ -393,7 +420,7 @@ fn main() {
393 "#, 420 "#,
394 expect![[r#" 421 expect![[r#"
395fn main() { 422fn main() {
396 return 123; 423 return 123;$0
397 424
398 println!("Hello, world"); 425 println!("Hello, world");
399} 426}
@@ -430,7 +457,7 @@ fn main() {}
430fn foo() {}$0$0 457fn foo() {}$0$0
431 "#, 458 "#,
432 expect![[r#" 459 expect![[r#"
433fn foo() {} 460fn foo() {}$0
434 461
435fn main() {} 462fn main() {}
436 "#]], 463 "#]],
@@ -451,7 +478,7 @@ impl Wow for Yay $0$0{}
451 expect![[r#" 478 expect![[r#"
452struct Yay; 479struct Yay;
453 480
454impl Wow for Yay {} 481impl Wow for Yay $0{}
455 482
456trait Wow {} 483trait Wow {}
457 "#]], 484 "#]],
@@ -467,7 +494,7 @@ use std::vec::Vec;
467use std::collections::HashMap$0$0; 494use std::collections::HashMap$0$0;
468 "#, 495 "#,
469 expect![[r#" 496 expect![[r#"
470use std::collections::HashMap; 497use std::collections::HashMap$0;
471use std::vec::Vec; 498use std::vec::Vec;
472 "#]], 499 "#]],
473 Direction::Up, 500 Direction::Up,
@@ -502,7 +529,7 @@ fn main() {
502 } 529 }
503 530
504 #[test] 531 #[test]
505 fn test_moves_param_up() { 532 fn test_moves_param() {
506 check( 533 check(
507 r#" 534 r#"
508fn test(one: i32, two$0$0: u32) {} 535fn test(one: i32, two$0$0: u32) {}
@@ -512,7 +539,7 @@ fn main() {
512} 539}
513 "#, 540 "#,
514 expect![[r#" 541 expect![[r#"
515fn test(two: u32, one: i32) {} 542fn test(two$0: u32, one: i32) {}
516 543
517fn main() { 544fn main() {
518 test(123, 456); 545 test(123, 456);
@@ -520,6 +547,15 @@ fn main() {
520 "#]], 547 "#]],
521 Direction::Up, 548 Direction::Up,
522 ); 549 );
550 check(
551 r#"
552fn f($0$0arg: u8, arg2: u16) {}
553 "#,
554 expect![[r#"
555fn f(arg2: u16, $0arg: u8) {}
556 "#]],
557 Direction::Down,
558 );
523 } 559 }
524 560
525 #[test] 561 #[test]
@@ -536,7 +572,7 @@ fn main() {
536fn test(one: i32, two: u32) {} 572fn test(one: i32, two: u32) {}
537 573
538fn main() { 574fn main() {
539 test(456, 123); 575 test(456$0, 123);
540} 576}
541 "#]], 577 "#]],
542 Direction::Up, 578 Direction::Up,
@@ -557,7 +593,7 @@ fn main() {
557fn test(one: i32, two: u32) {} 593fn test(one: i32, two: u32) {}
558 594
559fn main() { 595fn main() {
560 test(456, 123); 596 test(456, 123$0);
561} 597}
562 "#]], 598 "#]],
563 Direction::Down, 599 Direction::Down,
@@ -594,7 +630,7 @@ struct Test<A, B$0$0>(A, B);
594fn main() {} 630fn main() {}
595 "#, 631 "#,
596 expect![[r#" 632 expect![[r#"
597struct Test<B, A>(A, B); 633struct Test<B$0, A>(A, B);
598 634
599fn main() {} 635fn main() {}
600 "#]], 636 "#]],
@@ -616,7 +652,7 @@ fn main() {
616struct Test<A, B>(A, B); 652struct Test<A, B>(A, B);
617 653
618fn main() { 654fn main() {
619 let t = Test::<&str, i32>(123, "yay"); 655 let t = Test::<&str$0, i32>(123, "yay");
620} 656}
621 "#]], 657 "#]],
622 Direction::Up, 658 Direction::Up,
@@ -636,7 +672,7 @@ fn main() {}
636 "#, 672 "#,
637 expect![[r#" 673 expect![[r#"
638enum Hello { 674enum Hello {
639 Two, 675 Two$0,
640 One 676 One
641} 677}
642 678
@@ -663,7 +699,7 @@ trait One {}
663 699
664trait Two {} 700trait Two {}
665 701
666fn test<T: Two + One>(t: T) {} 702fn test<T: Two$0 + One>(t: T) {}
667 703
668fn main() {} 704fn main() {}
669 "#]], 705 "#]],
@@ -709,7 +745,7 @@ trait Yay {
709impl Yay for Test { 745impl Yay for Test {
710 type One = i32; 746 type One = i32;
711 747
712 fn inner() { 748 fn inner() {$0
713 println!("Mmmm"); 749 println!("Mmmm");
714 } 750 }
715 751
@@ -736,7 +772,7 @@ fn test() {
736 "#, 772 "#,
737 expect![[r#" 773 expect![[r#"
738fn test() { 774fn test() {
739 mod hi { 775 mod hi {$0
740 fn inner() {} 776 fn inner() {}
741 } 777 }
742 778
@@ -764,7 +800,7 @@ fn main() {}
764 expect![[r#" 800 expect![[r#"
765fn main() {} 801fn main() {}
766 802
767#[derive(Debug)] 803$0#[derive(Debug)]
768enum FooBar { 804enum FooBar {
769 Foo, 805 Foo,
770 Bar, 806 Bar,
@@ -784,7 +820,7 @@ fn main() {}
784 expect![[r#" 820 expect![[r#"
785fn main() {} 821fn main() {}
786 822
787enum FooBar { 823$0enum FooBar {
788 Foo, 824 Foo,
789 Bar, 825 Bar,
790} 826}
@@ -804,7 +840,7 @@ fn main() {}
804 expect![[r#" 840 expect![[r#"
805struct Test; 841struct Test;
806 842
807impl SomeTrait for Test {} 843$0impl SomeTrait for Test {}
808 844
809trait SomeTrait {} 845trait SomeTrait {}
810 846
@@ -831,7 +867,7 @@ fn main() {}
831enum FooBar { 867enum FooBar {
832 Foo, 868 Foo,
833 Bar, 869 Bar,
834} 870}$0
835 "#]], 871 "#]],
836 Direction::Down, 872 Direction::Down,
837 ); 873 );
@@ -848,7 +884,7 @@ fn main() {}
848 expect![[r#" 884 expect![[r#"
849struct Test; 885struct Test;
850 886
851impl SomeTrait for Test {} 887impl SomeTrait for Test {}$0
852 888
853trait SomeTrait {} 889trait SomeTrait {}
854 890
diff --git a/crates/ide/src/prime_caches.rs b/crates/ide/src/prime_caches.rs
index ea0acfaa0..03597f507 100644
--- a/crates/ide/src/prime_caches.rs
+++ b/crates/ide/src/prime_caches.rs
@@ -27,6 +27,7 @@ pub(crate) fn prime_caches(db: &RootDatabase, cb: &(dyn Fn(PrimeCachesProgress)
27 let topo = &graph.crates_in_topological_order(); 27 let topo = &graph.crates_in_topological_order();
28 28
29 cb(PrimeCachesProgress::Started); 29 cb(PrimeCachesProgress::Started);
30 let _d = stdx::defer(|| cb(PrimeCachesProgress::Finished));
30 31
31 // FIXME: This would be easy to parallelize, since it's in the ideal ordering for that. 32 // FIXME: This would be easy to parallelize, since it's in the ideal ordering for that.
32 // Unfortunately rayon prevents panics from propagation out of a `scope`, which breaks 33 // Unfortunately rayon prevents panics from propagation out of a `scope`, which breaks
@@ -41,6 +42,4 @@ pub(crate) fn prime_caches(db: &RootDatabase, cb: &(dyn Fn(PrimeCachesProgress)
41 }); 42 });
42 db.crate_def_map(*krate); 43 db.crate_def_map(*krate);
43 } 44 }
44
45 cb(PrimeCachesProgress::Finished);
46} 45}
diff --git a/crates/ide/src/syntax_highlighting/highlight.rs b/crates/ide/src/syntax_highlighting/highlight.rs
index 5ccb84714..18552459b 100644
--- a/crates/ide/src/syntax_highlighting/highlight.rs
+++ b/crates/ide/src/syntax_highlighting/highlight.rs
@@ -12,7 +12,10 @@ use syntax::{
12 SyntaxNode, SyntaxToken, T, 12 SyntaxNode, SyntaxToken, T,
13}; 13};
14 14
15use crate::{syntax_highlighting::tags::HlPunct, Highlight, HlMod, HlTag}; 15use crate::{
16 syntax_highlighting::tags::{HlOperator, HlPunct},
17 Highlight, HlMod, HlTag,
18};
16 19
17pub(super) fn element( 20pub(super) fn element(
18 sema: &Semantics<RootDatabase>, 21 sema: &Semantics<RootDatabase>,
@@ -132,7 +135,7 @@ pub(super) fn element(
132 INT_NUMBER | FLOAT_NUMBER => HlTag::NumericLiteral.into(), 135 INT_NUMBER | FLOAT_NUMBER => HlTag::NumericLiteral.into(),
133 BYTE => HlTag::ByteLiteral.into(), 136 BYTE => HlTag::ByteLiteral.into(),
134 CHAR => HlTag::CharLiteral.into(), 137 CHAR => HlTag::CharLiteral.into(),
135 QUESTION => Highlight::new(HlTag::Operator) | HlMod::ControlFlow, 138 QUESTION => Highlight::new(HlTag::Operator(HlOperator::Other)) | HlMod::ControlFlow,
136 LIFETIME => { 139 LIFETIME => {
137 let lifetime = element.into_node().and_then(ast::Lifetime::cast).unwrap(); 140 let lifetime = element.into_node().and_then(ast::Lifetime::cast).unwrap();
138 141
@@ -146,8 +149,11 @@ pub(super) fn element(
146 } 149 }
147 } 150 }
148 p if p.is_punct() => match p { 151 p if p.is_punct() => match p {
152 T![&] if element.parent().and_then(ast::BinExpr::cast).is_some() => {
153 HlTag::Operator(HlOperator::Bitwise).into()
154 }
149 T![&] => { 155 T![&] => {
150 let h = HlTag::Operator.into(); 156 let h = HlTag::Operator(HlOperator::Other).into();
151 let is_unsafe = element 157 let is_unsafe = element
152 .parent() 158 .parent()
153 .and_then(ast::RefExpr::cast) 159 .and_then(ast::RefExpr::cast)
@@ -159,13 +165,18 @@ pub(super) fn element(
159 h 165 h
160 } 166 }
161 } 167 }
162 T![::] | T![->] | T![=>] | T![..] | T![=] | T![@] | T![.] => HlTag::Operator.into(), 168 T![::] | T![->] | T![=>] | T![..] | T![=] | T![@] | T![.] => {
169 HlTag::Operator(HlOperator::Other).into()
170 }
163 T![!] if element.parent().and_then(ast::MacroCall::cast).is_some() => { 171 T![!] if element.parent().and_then(ast::MacroCall::cast).is_some() => {
164 HlTag::Symbol(SymbolKind::Macro).into() 172 HlTag::Symbol(SymbolKind::Macro).into()
165 } 173 }
166 T![!] if element.parent().and_then(ast::NeverType::cast).is_some() => { 174 T![!] if element.parent().and_then(ast::NeverType::cast).is_some() => {
167 HlTag::BuiltinType.into() 175 HlTag::BuiltinType.into()
168 } 176 }
177 T![!] if element.parent().and_then(ast::PrefixExpr::cast).is_some() => {
178 HlTag::Operator(HlOperator::Logical).into()
179 }
169 T![*] if element.parent().and_then(ast::PtrType::cast).is_some() => { 180 T![*] if element.parent().and_then(ast::PtrType::cast).is_some() => {
170 HlTag::Keyword.into() 181 HlTag::Keyword.into()
171 } 182 }
@@ -175,9 +186,9 @@ pub(super) fn element(
175 let expr = prefix_expr.expr()?; 186 let expr = prefix_expr.expr()?;
176 let ty = sema.type_of_expr(&expr)?; 187 let ty = sema.type_of_expr(&expr)?;
177 if ty.is_raw_ptr() { 188 if ty.is_raw_ptr() {
178 HlTag::Operator | HlMod::Unsafe 189 HlTag::Operator(HlOperator::Other) | HlMod::Unsafe
179 } else if let Some(ast::PrefixOp::Deref) = prefix_expr.op_kind() { 190 } else if let Some(ast::PrefixOp::Deref) = prefix_expr.op_kind() {
180 HlTag::Operator.into() 191 HlTag::Operator(HlOperator::Other).into()
181 } else { 192 } else {
182 HlTag::Punctuation(HlPunct::Other).into() 193 HlTag::Punctuation(HlPunct::Other).into()
183 } 194 }
@@ -188,19 +199,43 @@ pub(super) fn element(
188 let expr = prefix_expr.expr()?; 199 let expr = prefix_expr.expr()?;
189 match expr { 200 match expr {
190 ast::Expr::Literal(_) => HlTag::NumericLiteral, 201 ast::Expr::Literal(_) => HlTag::NumericLiteral,
191 _ => HlTag::Operator, 202 _ => HlTag::Operator(HlOperator::Other),
192 } 203 }
193 .into() 204 .into()
194 } 205 }
195 _ if element.parent().and_then(ast::PrefixExpr::cast).is_some() => { 206 _ if element.parent().and_then(ast::PrefixExpr::cast).is_some() => {
196 HlTag::Operator.into() 207 HlTag::Operator(HlOperator::Other).into()
208 }
209 T![+] | T![-] | T![*] | T![/] | T![+=] | T![-=] | T![*=] | T![/=]
210 if element.parent().and_then(ast::BinExpr::cast).is_some() =>
211 {
212 HlTag::Operator(HlOperator::Arithmetic).into()
213 }
214 T![|] | T![&] | T![!] | T![^] | T![|=] | T![&=] | T![^=]
215 if element.parent().and_then(ast::BinExpr::cast).is_some() =>
216 {
217 HlTag::Operator(HlOperator::Bitwise).into()
218 }
219 T![&&] | T![||] if element.parent().and_then(ast::BinExpr::cast).is_some() => {
220 HlTag::Operator(HlOperator::Logical).into()
221 }
222 T![>] | T![<] | T![==] | T![>=] | T![<=] | T![!=]
223 if element.parent().and_then(ast::BinExpr::cast).is_some() =>
224 {
225 HlTag::Operator(HlOperator::Comparison).into()
226 }
227 _ if element.parent().and_then(ast::BinExpr::cast).is_some() => {
228 HlTag::Operator(HlOperator::Other).into()
197 } 229 }
198 _ if element.parent().and_then(ast::BinExpr::cast).is_some() => HlTag::Operator.into(),
199 _ if element.parent().and_then(ast::RangeExpr::cast).is_some() => { 230 _ if element.parent().and_then(ast::RangeExpr::cast).is_some() => {
200 HlTag::Operator.into() 231 HlTag::Operator(HlOperator::Other).into()
232 }
233 _ if element.parent().and_then(ast::RangePat::cast).is_some() => {
234 HlTag::Operator(HlOperator::Other).into()
235 }
236 _ if element.parent().and_then(ast::RestPat::cast).is_some() => {
237 HlTag::Operator(HlOperator::Other).into()
201 } 238 }
202 _ if element.parent().and_then(ast::RangePat::cast).is_some() => HlTag::Operator.into(),
203 _ if element.parent().and_then(ast::RestPat::cast).is_some() => HlTag::Operator.into(),
204 _ if element.parent().and_then(ast::Attr::cast).is_some() => HlTag::Attribute.into(), 239 _ if element.parent().and_then(ast::Attr::cast).is_some() => HlTag::Attribute.into(),
205 kind => HlTag::Punctuation(match kind { 240 kind => HlTag::Punctuation(match kind {
206 T!['['] | T![']'] => HlPunct::Bracket, 241 T!['['] | T![']'] => HlPunct::Bracket,
@@ -323,8 +358,18 @@ fn highlight_def(db: &RootDatabase, def: Definition) -> Highlight {
323 hir::ModuleDef::Trait(_) => HlTag::Symbol(SymbolKind::Trait), 358 hir::ModuleDef::Trait(_) => HlTag::Symbol(SymbolKind::Trait),
324 hir::ModuleDef::TypeAlias(type_) => { 359 hir::ModuleDef::TypeAlias(type_) => {
325 let mut h = Highlight::new(HlTag::Symbol(SymbolKind::TypeAlias)); 360 let mut h = Highlight::new(HlTag::Symbol(SymbolKind::TypeAlias));
326 if type_.as_assoc_item(db).is_some() { 361 if let Some(item) = type_.as_assoc_item(db) {
327 h |= HlMod::Associated 362 h |= HlMod::Associated;
363 match item.container(db) {
364 AssocItemContainer::Impl(i) => {
365 if i.trait_(db).is_some() {
366 h |= HlMod::Trait;
367 }
368 }
369 AssocItemContainer::Trait(_t) => {
370 h |= HlMod::Trait;
371 }
372 }
328 } 373 }
329 return h; 374 return h;
330 } 375 }
diff --git a/crates/ide/src/syntax_highlighting/inject.rs b/crates/ide/src/syntax_highlighting/inject.rs
index b62d43256..bc221d599 100644
--- a/crates/ide/src/syntax_highlighting/inject.rs
+++ b/crates/ide/src/syntax_highlighting/inject.rs
@@ -1,17 +1,17 @@
1//! "Recursive" Syntax highlighting for code in doctests and fixtures. 1//! "Recursive" Syntax highlighting for code in doctests and fixtures.
2 2
3use std::{mem, ops::Range}; 3use std::mem;
4 4
5use either::Either; 5use either::Either;
6use hir::{HasAttrs, InFile, Semantics}; 6use hir::{InFile, Semantics};
7use ide_db::{call_info::ActiveParameter, defs::Definition, SymbolKind}; 7use ide_db::{call_info::ActiveParameter, helpers::rust_doc::is_rust_fence, SymbolKind};
8use syntax::{ 8use syntax::{
9 ast::{self, AstNode}, 9 ast::{self, AstNode},
10 match_ast, AstToken, NodeOrToken, SyntaxNode, SyntaxToken, TextRange, TextSize, 10 AstToken, NodeOrToken, SyntaxNode, SyntaxToken, TextRange, TextSize,
11}; 11};
12 12
13use crate::{ 13use crate::{
14 doc_links::{extract_definitions_from_markdown, resolve_doc_path_for_def}, 14 doc_links::{doc_attributes, extract_definitions_from_markdown, resolve_doc_path_for_def},
15 Analysis, HlMod, HlRange, HlTag, RootDatabase, 15 Analysis, HlMod, HlRange, HlTag, RootDatabase,
16}; 16};
17 17
@@ -78,44 +78,6 @@ pub(super) fn ra_fixture(
78} 78}
79 79
80const RUSTDOC_FENCE: &'static str = "```"; 80const RUSTDOC_FENCE: &'static str = "```";
81const RUSTDOC_FENCE_TOKENS: &[&'static str] = &[
82 "",
83 "rust",
84 "should_panic",
85 "ignore",
86 "no_run",
87 "compile_fail",
88 "edition2015",
89 "edition2018",
90 "edition2021",
91];
92
93fn doc_attributes<'node>(
94 sema: &Semantics<RootDatabase>,
95 node: &'node SyntaxNode,
96) -> Option<(hir::AttrsWithOwner, Definition)> {
97 match_ast! {
98 match node {
99 ast::SourceFile(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Module(def)))),
100 ast::Module(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Module(def)))),
101 ast::Fn(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Function(def)))),
102 ast::Struct(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Adt(hir::Adt::Struct(def))))),
103 ast::Union(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Adt(hir::Adt::Union(def))))),
104 ast::Enum(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Adt(hir::Adt::Enum(def))))),
105 ast::Variant(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Variant(def)))),
106 ast::Trait(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Trait(def)))),
107 ast::Static(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Static(def)))),
108 ast::Const(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Const(def)))),
109 ast::TypeAlias(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::TypeAlias(def)))),
110 ast::Impl(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::SelfType(def))),
111 ast::RecordField(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::Field(def))),
112 ast::TupleField(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::Field(def))),
113 ast::Macro(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::Macro(def))),
114 // ast::Use(it) => sema.to_def(&it).map(|def| (Box::new(it) as _, def.attrs(sema.db))),
115 _ => return None
116 }
117 }
118}
119 81
120/// Injection of syntax highlighting of doctests. 82/// Injection of syntax highlighting of doctests.
121pub(super) fn doc_comment( 83pub(super) fn doc_comment(
@@ -139,8 +101,28 @@ pub(super) fn doc_comment(
139 // Replace the original, line-spanning comment ranges by new, only comment-prefix 101 // Replace the original, line-spanning comment ranges by new, only comment-prefix
140 // spanning comment ranges. 102 // spanning comment ranges.
141 let mut new_comments = Vec::new(); 103 let mut new_comments = Vec::new();
142 let mut intra_doc_links = Vec::new();
143 let mut string; 104 let mut string;
105
106 if let Some((docs, doc_mapping)) = attributes.docs_with_rangemap(sema.db) {
107 extract_definitions_from_markdown(docs.as_str())
108 .into_iter()
109 .filter_map(|(range, link, ns)| {
110 let def = resolve_doc_path_for_def(sema.db, def, &link, ns)?;
111 let InFile { file_id, value: range } = doc_mapping.map(range)?;
112 (file_id == node.file_id).then(|| (range, def))
113 })
114 .for_each(|(range, def)| {
115 hl.add(HlRange {
116 range,
117 highlight: module_def_to_hl_tag(def)
118 | HlMod::Documentation
119 | HlMod::Injected
120 | HlMod::IntraDocLink,
121 binding_hash: None,
122 })
123 });
124 }
125
144 for attr in attributes.by_key("doc").attrs() { 126 for attr in attributes.by_key("doc").attrs() {
145 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);
146 if file_id != node.file_id { 128 if file_id != node.file_id {
@@ -181,30 +163,11 @@ pub(super) fn doc_comment(
181 is_codeblock = !is_codeblock; 163 is_codeblock = !is_codeblock;
182 // Check whether code is rust by inspecting fence guards 164 // Check whether code is rust by inspecting fence guards
183 let guards = &line[idx + RUSTDOC_FENCE.len()..]; 165 let guards = &line[idx + RUSTDOC_FENCE.len()..];
184 let is_rust = 166 let is_rust = is_rust_fence(guards);
185 guards.split(',').all(|sub| RUSTDOC_FENCE_TOKENS.contains(&sub.trim()));
186 is_doctest = is_codeblock && is_rust; 167 is_doctest = is_codeblock && is_rust;
187 continue; 168 continue;
188 } 169 }
189 None if !is_doctest => { 170 None if !is_doctest => continue,
190 intra_doc_links.extend(
191 extract_definitions_from_markdown(line)
192 .into_iter()
193 .filter_map(|(range, link, ns)| {
194 Some(range).zip(resolve_doc_path_for_def(sema.db, def, &link, ns))
195 })
196 .map(|(Range { start, end }, def)| {
197 (
198 def,
199 TextRange::at(
200 prev_range_start + TextSize::from(start as u32),
201 TextSize::from((end - start) as u32),
202 ),
203 )
204 }),
205 );
206 continue;
207 }
208 None => (), 171 None => (),
209 } 172 }
210 173
@@ -223,17 +186,6 @@ pub(super) fn doc_comment(
223 } 186 }
224 } 187 }
225 188
226 for (def, range) in intra_doc_links {
227 hl.add(HlRange {
228 range,
229 highlight: module_def_to_hl_tag(def)
230 | HlMod::Documentation
231 | HlMod::Injected
232 | HlMod::IntraDocLink,
233 binding_hash: None,
234 });
235 }
236
237 if new_comments.is_empty() { 189 if new_comments.is_empty() {
238 return; // no need to run an analysis on an empty file 190 return; // no need to run an analysis on an empty file
239 } 191 }
diff --git a/crates/ide/src/syntax_highlighting/tags.rs b/crates/ide/src/syntax_highlighting/tags.rs
index 1cec991aa..e58392d67 100644
--- a/crates/ide/src/syntax_highlighting/tags.rs
+++ b/crates/ide/src/syntax_highlighting/tags.rs
@@ -28,7 +28,7 @@ pub enum HlTag {
28 FormatSpecifier, 28 FormatSpecifier,
29 Keyword, 29 Keyword,
30 NumericLiteral, 30 NumericLiteral,
31 Operator, 31 Operator(HlOperator),
32 Punctuation(HlPunct), 32 Punctuation(HlPunct),
33 StringLiteral, 33 StringLiteral,
34 UnresolvedReference, 34 UnresolvedReference,
@@ -87,6 +87,20 @@ pub enum HlPunct {
87 Other, 87 Other,
88} 88}
89 89
90#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
91pub enum HlOperator {
92 /// |, &, !, ^, |=, &=, ^=
93 Bitwise,
94 /// +, -, *, /, +=, -=, *=, /=
95 Arithmetic,
96 /// &&, ||, !
97 Logical,
98 /// >, <, ==, >=, <=, !=
99 Comparison,
100 ///
101 Other,
102}
103
90impl HlTag { 104impl HlTag {
91 fn as_str(self) -> &'static str { 105 fn as_str(self) -> &'static str {
92 match self { 106 match self {
@@ -133,7 +147,13 @@ impl HlTag {
133 HlPunct::Other => "punctuation", 147 HlPunct::Other => "punctuation",
134 }, 148 },
135 HlTag::NumericLiteral => "numeric_literal", 149 HlTag::NumericLiteral => "numeric_literal",
136 HlTag::Operator => "operator", 150 HlTag::Operator(op) => match op {
151 HlOperator::Bitwise => "bitwise",
152 HlOperator::Arithmetic => "arithmetic",
153 HlOperator::Logical => "logical",
154 HlOperator::Comparison => "comparison",
155 HlOperator::Other => "operator",
156 },
137 HlTag::StringLiteral => "string_literal", 157 HlTag::StringLiteral => "string_literal",
138 HlTag::UnresolvedReference => "unresolved_reference", 158 HlTag::UnresolvedReference => "unresolved_reference",
139 HlTag::None => "none", 159 HlTag::None => "none",
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 045162eb8..6ee6d85fb 100644
--- a/crates/ide/src/syntax_highlighting/test_data/highlight_doctest.html
+++ b/crates/ide/src/syntax_highlighting/test_data/highlight_doctest.html
@@ -76,7 +76,7 @@ pre { color: #DCDCCC; background: #3F3F3F; font-size: 22px; padd
76 <span class="comment documentation">/// </span><span class="comment injected">// calls bar on foo</span> 76 <span class="comment documentation">/// </span><span class="comment injected">// calls bar on foo</span>
77 <span class="comment documentation">/// </span><span class="macro injected">assert!</span><span class="parenthesis injected">(</span><span class="none injected">foo</span><span class="operator injected">.</span><span class="none injected">bar</span><span class="parenthesis injected">(</span><span class="parenthesis injected">)</span><span class="parenthesis injected">)</span><span class="semicolon injected">;</span> 77 <span class="comment documentation">/// </span><span class="macro injected">assert!</span><span class="parenthesis injected">(</span><span class="none injected">foo</span><span class="operator injected">.</span><span class="none injected">bar</span><span class="parenthesis injected">(</span><span class="parenthesis injected">)</span><span class="parenthesis injected">)</span><span class="semicolon injected">;</span>
78 <span class="comment documentation">///</span> 78 <span class="comment documentation">///</span>
79 <span class="comment documentation">/// </span><span class="keyword injected">let</span><span class="none injected"> </span><span class="variable declaration injected">bar</span><span class="none injected"> </span><span class="operator injected">=</span><span class="none injected"> </span><span class="variable injected">foo</span><span class="operator injected">.</span><span class="field injected">bar</span><span class="none injected"> </span><span class="operator injected">||</span><span class="none injected"> </span><span class="struct injected">Foo</span><span class="operator injected">::</span><span class="constant injected">bar</span><span class="semicolon injected">;</span> 79 <span class="comment documentation">/// </span><span class="keyword injected">let</span><span class="none injected"> </span><span class="variable declaration injected">bar</span><span class="none injected"> </span><span class="operator injected">=</span><span class="none injected"> </span><span class="variable injected">foo</span><span class="operator injected">.</span><span class="field injected">bar</span><span class="none injected"> </span><span class="logical injected">||</span><span class="none injected"> </span><span class="struct injected">Foo</span><span class="operator injected">::</span><span class="constant injected">bar</span><span class="semicolon injected">;</span>
80 <span class="comment documentation">///</span> 80 <span class="comment documentation">///</span>
81 <span class="comment documentation">/// </span><span class="comment injected">/* multi-line</span> 81 <span class="comment documentation">/// </span><span class="comment injected">/* multi-line</span>
82 <span class="comment documentation">/// </span><span class="comment injected"> comment */</span> 82 <span class="comment documentation">/// </span><span class="comment injected"> comment */</span>
@@ -100,10 +100,18 @@ pre { color: #DCDCCC; background: #3F3F3F; font-size: 22px; padd
100<span class="brace">}</span> 100<span class="brace">}</span>
101 101
102<span class="comment documentation">/// </span><span class="struct documentation intra_doc_link injected">[`Foo`](Foo)</span><span class="comment documentation"> is a struct</span> 102<span class="comment documentation">/// </span><span class="struct documentation intra_doc_link injected">[`Foo`](Foo)</span><span class="comment documentation"> is a struct</span>
103<span class="comment documentation">/// </span><span class="function documentation intra_doc_link injected">[`all_the_links`](all_the_links)</span><span class="comment documentation"> is this function</span> 103<span class="comment documentation">/// This function is &gt; </span><span class="function documentation intra_doc_link injected">[`all_the_links`](all_the_links)</span><span class="comment documentation"> &lt;</span>
104<span class="comment documentation">/// [`noop`](noop) is a macro below</span> 104<span class="comment documentation">/// [`noop`](noop) is a macro below</span>
105<span class="comment documentation">/// </span><span class="struct documentation intra_doc_link injected">[`Item`]</span><span class="comment documentation"> is a struct in the module </span><span class="module documentation intra_doc_link injected">[`module`]</span>
106<span class="comment documentation">///</span>
107<span class="comment documentation">/// [`Item`]: module::Item</span>
108<span class="comment documentation">/// [mix_and_match]: ThisShouldntResolve</span>
105<span class="keyword">pub</span> <span class="keyword">fn</span> <span class="function declaration">all_the_links</span><span class="parenthesis">(</span><span class="parenthesis">)</span> <span class="brace">{</span><span class="brace">}</span> 109<span class="keyword">pub</span> <span class="keyword">fn</span> <span class="function declaration">all_the_links</span><span class="parenthesis">(</span><span class="parenthesis">)</span> <span class="brace">{</span><span class="brace">}</span>
106 110
111<span class="keyword">pub</span> <span class="keyword">mod</span> <span class="module declaration">module</span> <span class="brace">{</span>
112 <span class="keyword">pub</span> <span class="keyword">struct</span> <span class="struct declaration">Item</span><span class="semicolon">;</span>
113<span class="brace">}</span>
114
107<span class="comment documentation">/// ```</span> 115<span class="comment documentation">/// ```</span>
108<span class="comment documentation">/// </span><span class="macro injected">noop!</span><span class="parenthesis injected">(</span><span class="numeric_literal injected">1</span><span class="parenthesis injected">)</span><span class="semicolon injected">;</span> 116<span class="comment documentation">/// </span><span class="macro injected">noop!</span><span class="parenthesis injected">(</span><span class="numeric_literal injected">1</span><span class="parenthesis injected">)</span><span class="semicolon injected">;</span>
109<span class="comment documentation">/// ```</span> 117<span class="comment documentation">/// ```</span>
diff --git a/crates/ide/src/syntax_highlighting/test_data/highlighting.html b/crates/ide/src/syntax_highlighting/test_data/highlighting.html
index 973173254..c43bcb691 100644
--- a/crates/ide/src/syntax_highlighting/test_data/highlighting.html
+++ b/crates/ide/src/syntax_highlighting/test_data/highlighting.html
@@ -213,7 +213,7 @@ pre { color: #DCDCCC; background: #3F3F3F; font-size: 22px; padd
213 <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> 213 <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>
214 <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> 214 <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>
215 215
216 <span class="keyword">let</span> <span class="punctuation">_</span> <span class="operator">=</span> <span class="operator">!</span><span class="bool_literal">true</span><span class="semicolon">;</span> 216 <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>
217 217
218 <span class="label declaration">'foo</span><span class="colon">:</span> <span class="keyword control">loop</span> <span class="brace">{</span> 218 <span class="label declaration">'foo</span><span class="colon">:</span> <span class="keyword control">loop</span> <span class="brace">{</span>
219 <span class="keyword control">break</span> <span class="label">'foo</span><span class="semicolon">;</span> 219 <span class="keyword control">break</span> <span class="label">'foo</span><span class="semicolon">;</span>
diff --git a/crates/ide/src/syntax_highlighting/tests.rs b/crates/ide/src/syntax_highlighting/tests.rs
index 369ae0972..17cc6334b 100644
--- a/crates/ide/src/syntax_highlighting/tests.rs
+++ b/crates/ide/src/syntax_highlighting/tests.rs
@@ -1,6 +1,8 @@
1use std::time::Instant;
2
1use expect_test::{expect_file, ExpectFile}; 3use expect_test::{expect_file, ExpectFile};
2use ide_db::SymbolKind; 4use ide_db::SymbolKind;
3use test_utils::{bench, bench_fixture, skip_slow_tests}; 5use test_utils::{bench, bench_fixture, skip_slow_tests, AssertLinear};
4 6
5use crate::{fixture, FileRange, HlTag, TextRange}; 7use crate::{fixture, FileRange, HlTag, TextRange};
6 8
@@ -258,6 +260,36 @@ fn benchmark_syntax_highlighting_long_struct() {
258} 260}
259 261
260#[test] 262#[test]
263fn syntax_highlighting_not_quadratic() {
264 if skip_slow_tests() {
265 return;
266 }
267
268 let mut al = AssertLinear::default();
269 while al.next_round() {
270 for i in 6..=10 {
271 let n = 1 << i;
272
273 let fixture = bench_fixture::big_struct_n(n);
274 let (analysis, file_id) = fixture::file(&fixture);
275
276 let time = Instant::now();
277
278 let hash = analysis
279 .highlight(file_id)
280 .unwrap()
281 .iter()
282 .filter(|it| it.highlight.tag == HlTag::Symbol(SymbolKind::Struct))
283 .count();
284 assert!(hash > n as usize);
285
286 let elapsed = time.elapsed();
287 al.sample(n as f64, elapsed.as_millis() as f64);
288 }
289 }
290}
291
292#[test]
261fn benchmark_syntax_highlighting_parser() { 293fn benchmark_syntax_highlighting_parser() {
262 if skip_slow_tests() { 294 if skip_slow_tests() {
263 return; 295 return;
@@ -275,7 +307,7 @@ fn benchmark_syntax_highlighting_parser() {
275 .filter(|it| it.highlight.tag == HlTag::Symbol(SymbolKind::Function)) 307 .filter(|it| it.highlight.tag == HlTag::Symbol(SymbolKind::Function))
276 .count() 308 .count()
277 }; 309 };
278 assert_eq!(hash, 1629); 310 assert_eq!(hash, 1632);
279} 311}
280 312
281#[test] 313#[test]
@@ -544,10 +576,18 @@ impl Foo {
544} 576}
545 577
546/// [`Foo`](Foo) is a struct 578/// [`Foo`](Foo) is a struct
547/// [`all_the_links`](all_the_links) is this function 579/// This function is > [`all_the_links`](all_the_links) <
548/// [`noop`](noop) is a macro below 580/// [`noop`](noop) is a macro below
581/// [`Item`] is a struct in the module [`module`]
582///
583/// [`Item`]: module::Item
584/// [mix_and_match]: ThisShouldntResolve
549pub fn all_the_links() {} 585pub fn all_the_links() {}
550 586
587pub mod module {
588 pub struct Item;
589}
590
551/// ``` 591/// ```
552/// noop!(1); 592/// noop!(1);
553/// ``` 593/// ```
diff --git a/crates/ide/src/typing.rs b/crates/ide/src/typing.rs
index 11408d445..82c732390 100644
--- a/crates/ide/src/typing.rs
+++ b/crates/ide/src/typing.rs
@@ -22,18 +22,19 @@ use ide_db::{
22use syntax::{ 22use syntax::{
23 algo::find_node_at_offset, 23 algo::find_node_at_offset,
24 ast::{self, edit::IndentLevel, AstToken}, 24 ast::{self, edit::IndentLevel, AstToken},
25 AstNode, SourceFile, 25 AstNode, Parse, SourceFile,
26 SyntaxKind::{FIELD_EXPR, METHOD_CALL_EXPR}, 26 SyntaxKind::{FIELD_EXPR, METHOD_CALL_EXPR},
27 TextRange, TextSize, 27 TextRange, TextSize,
28}; 28};
29 29
30use text_edit::TextEdit; 30use text_edit::{Indel, TextEdit};
31 31
32use crate::SourceChange; 32use crate::SourceChange;
33 33
34pub(crate) use on_enter::on_enter; 34pub(crate) use on_enter::on_enter;
35 35
36pub(crate) const TRIGGER_CHARS: &str = ".=>"; 36// Don't forget to add new trigger characters to `server_capabilities` in `caps.rs`.
37pub(crate) const TRIGGER_CHARS: &str = ".=>{";
37 38
38// Feature: On Typing Assists 39// Feature: On Typing Assists
39// 40//
@@ -41,6 +42,7 @@ pub(crate) const TRIGGER_CHARS: &str = ".=>";
41// 42//
42// - typing `let =` tries to smartly add `;` if `=` is followed by an existing expression 43// - typing `let =` tries to smartly add `;` if `=` is followed by an existing expression
43// - typing `.` in a chain method call auto-indents 44// - typing `.` in a chain method call auto-indents
45// - typing `{` in front of an expression inserts a closing `}` after the expression
44// 46//
45// VS Code:: 47// VS Code::
46// 48//
@@ -57,28 +59,79 @@ pub(crate) fn on_char_typed(
57 position: FilePosition, 59 position: FilePosition,
58 char_typed: char, 60 char_typed: char,
59) -> Option<SourceChange> { 61) -> Option<SourceChange> {
60 assert!(TRIGGER_CHARS.contains(char_typed)); 62 if !stdx::always!(TRIGGER_CHARS.contains(char_typed)) {
61 let file = &db.parse(position.file_id).tree(); 63 return None;
62 assert_eq!(file.syntax().text().char_at(position.offset), Some(char_typed)); 64 }
65 let file = &db.parse(position.file_id);
66 if !stdx::always!(file.tree().syntax().text().char_at(position.offset) == Some(char_typed)) {
67 return None;
68 }
63 let edit = on_char_typed_inner(file, position.offset, char_typed)?; 69 let edit = on_char_typed_inner(file, position.offset, char_typed)?;
64 Some(SourceChange::from_text_edit(position.file_id, edit)) 70 Some(SourceChange::from_text_edit(position.file_id, edit))
65} 71}
66 72
67fn on_char_typed_inner(file: &SourceFile, offset: TextSize, char_typed: char) -> Option<TextEdit> { 73fn on_char_typed_inner(
68 assert!(TRIGGER_CHARS.contains(char_typed)); 74 file: &Parse<SourceFile>,
75 offset: TextSize,
76 char_typed: char,
77) -> Option<TextEdit> {
78 if !stdx::always!(TRIGGER_CHARS.contains(char_typed)) {
79 return None;
80 }
69 match char_typed { 81 match char_typed {
70 '.' => on_dot_typed(file, offset), 82 '.' => on_dot_typed(&file.tree(), offset),
71 '=' => on_eq_typed(file, offset), 83 '=' => on_eq_typed(&file.tree(), offset),
72 '>' => on_arrow_typed(file, offset), 84 '>' => on_arrow_typed(&file.tree(), offset),
85 '{' => on_opening_brace_typed(file, offset),
73 _ => unreachable!(), 86 _ => unreachable!(),
74 } 87 }
75} 88}
76 89
90/// Inserts a closing `}` when the user types an opening `{`, wrapping an existing expression in a
91/// block.
92fn on_opening_brace_typed(file: &Parse<SourceFile>, offset: TextSize) -> Option<TextEdit> {
93 if !stdx::always!(file.tree().syntax().text().char_at(offset) == Some('{')) {
94 return None;
95 }
96
97 let brace_token = file.tree().syntax().token_at_offset(offset).right_biased()?;
98
99 // Remove the `{` to get a better parse tree, and reparse
100 let file = file.reparse(&Indel::delete(brace_token.text_range()));
101
102 let mut expr: ast::Expr = find_node_at_offset(file.tree().syntax(), offset)?;
103 if expr.syntax().text_range().start() != offset {
104 return None;
105 }
106
107 // Enclose the outermost expression starting at `offset`
108 while let Some(parent) = expr.syntax().parent() {
109 if parent.text_range().start() != expr.syntax().text_range().start() {
110 break;
111 }
112
113 match ast::Expr::cast(parent) {
114 Some(parent) => expr = parent,
115 None => break,
116 }
117 }
118
119 // If it's a statement in a block, we don't know how many statements should be included
120 if ast::ExprStmt::can_cast(expr.syntax().parent()?.kind()) {
121 return None;
122 }
123
124 // Insert `}` right after the expression.
125 Some(TextEdit::insert(expr.syntax().text_range().end() + TextSize::of("{"), "}".to_string()))
126}
127
77/// Returns an edit which should be applied after `=` was typed. Primarily, 128/// Returns an edit which should be applied after `=` was typed. Primarily,
78/// this works when adding `let =`. 129/// this works when adding `let =`.
79// FIXME: use a snippet completion instead of this hack here. 130// FIXME: use a snippet completion instead of this hack here.
80fn on_eq_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> { 131fn on_eq_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> {
81 assert_eq!(file.syntax().text().char_at(offset), Some('=')); 132 if !stdx::always!(file.syntax().text().char_at(offset) == Some('=')) {
133 return None;
134 }
82 let let_stmt: ast::LetStmt = find_node_at_offset(file.syntax(), offset)?; 135 let let_stmt: ast::LetStmt = find_node_at_offset(file.syntax(), offset)?;
83 if let_stmt.semicolon_token().is_some() { 136 if let_stmt.semicolon_token().is_some() {
84 return None; 137 return None;
@@ -100,7 +153,9 @@ fn on_eq_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> {
100 153
101/// Returns an edit which should be applied when a dot ('.') is typed on a blank line, indenting the line appropriately. 154/// Returns an edit which should be applied when a dot ('.') is typed on a blank line, indenting the line appropriately.
102fn on_dot_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> { 155fn on_dot_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> {
103 assert_eq!(file.syntax().text().char_at(offset), Some('.')); 156 if !stdx::always!(file.syntax().text().char_at(offset) == Some('.')) {
157 return None;
158 }
104 let whitespace = 159 let whitespace =
105 file.syntax().token_at_offset(offset).left_biased().and_then(ast::Whitespace::cast)?; 160 file.syntax().token_at_offset(offset).left_biased().and_then(ast::Whitespace::cast)?;
106 161
@@ -129,7 +184,9 @@ fn on_dot_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> {
129/// Adds a space after an arrow when `fn foo() { ... }` is turned into `fn foo() -> { ... }` 184/// Adds a space after an arrow when `fn foo() { ... }` is turned into `fn foo() -> { ... }`
130fn on_arrow_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> { 185fn on_arrow_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> {
131 let file_text = file.syntax().text(); 186 let file_text = file.syntax().text();
132 assert_eq!(file_text.char_at(offset), Some('>')); 187 if !stdx::always!(file_text.char_at(offset) == Some('>')) {
188 return None;
189 }
133 let after_arrow = offset + TextSize::of('>'); 190 let after_arrow = offset + TextSize::of('>');
134 if file_text.char_at(after_arrow) != Some('{') { 191 if file_text.char_at(after_arrow) != Some('{') {
135 return None; 192 return None;
@@ -152,7 +209,7 @@ mod tests {
152 let edit = TextEdit::insert(offset, char_typed.to_string()); 209 let edit = TextEdit::insert(offset, char_typed.to_string());
153 edit.apply(&mut before); 210 edit.apply(&mut before);
154 let parse = SourceFile::parse(&before); 211 let parse = SourceFile::parse(&before);
155 on_char_typed_inner(&parse.tree(), offset, char_typed).map(|it| { 212 on_char_typed_inner(&parse, offset, char_typed).map(|it| {
156 it.apply(&mut before); 213 it.apply(&mut before);
157 before.to_string() 214 before.to_string()
158 }) 215 })
@@ -165,8 +222,8 @@ mod tests {
165 assert_eq_text!(ra_fixture_after, &actual); 222 assert_eq_text!(ra_fixture_after, &actual);
166 } 223 }
167 224
168 fn type_char_noop(char_typed: char, before: &str) { 225 fn type_char_noop(char_typed: char, ra_fixture_before: &str) {
169 let file_change = do_type_char(char_typed, before); 226 let file_change = do_type_char(char_typed, ra_fixture_before);
170 assert!(file_change.is_none()) 227 assert!(file_change.is_none())
171 } 228 }
172 229
@@ -183,16 +240,16 @@ mod tests {
183 // "); 240 // ");
184 type_char( 241 type_char(
185 '=', 242 '=',
186 r" 243 r#"
187fn foo() { 244fn foo() {
188 let foo $0 1 + 1 245 let foo $0 1 + 1
189} 246}
190", 247"#,
191 r" 248 r#"
192fn foo() { 249fn foo() {
193 let foo = 1 + 1; 250 let foo = 1 + 1;
194} 251}
195", 252"#,
196 ); 253 );
197 // do_check(r" 254 // do_check(r"
198 // fn foo() { 255 // fn foo() {
@@ -211,27 +268,27 @@ fn foo() {
211 fn indents_new_chain_call() { 268 fn indents_new_chain_call() {
212 type_char( 269 type_char(
213 '.', 270 '.',
214 r" 271 r#"
215 fn main() { 272fn main() {
216 xs.foo() 273 xs.foo()
217 $0 274 $0
218 } 275}
219 ", 276 "#,
220 r" 277 r#"
221 fn main() { 278fn main() {
222 xs.foo() 279 xs.foo()
223 . 280 .
224 } 281}
225 ", 282 "#,
226 ); 283 );
227 type_char_noop( 284 type_char_noop(
228 '.', 285 '.',
229 r" 286 r#"
230 fn main() { 287fn main() {
231 xs.foo() 288 xs.foo()
232 $0 289 $0
233 } 290}
234 ", 291 "#,
235 ) 292 )
236 } 293 }
237 294
@@ -240,26 +297,26 @@ fn foo() {
240 type_char( 297 type_char(
241 '.', 298 '.',
242 r" 299 r"
243 fn main() { 300fn main() {
244 xs.foo() 301 xs.foo()
245 $0; 302 $0;
246 } 303}
247 ",
248 r"
249 fn main() {
250 xs.foo()
251 .;
252 }
253 ", 304 ",
305 r#"
306fn main() {
307 xs.foo()
308 .;
309}
310 "#,
254 ); 311 );
255 type_char_noop( 312 type_char_noop(
256 '.', 313 '.',
257 r" 314 r#"
258 fn main() { 315fn main() {
259 xs.foo() 316 xs.foo()
260 $0; 317 $0;
261 } 318}
262 ", 319 "#,
263 ) 320 )
264 } 321 }
265 322
@@ -288,30 +345,30 @@ fn main() {
288 fn indents_continued_chain_call() { 345 fn indents_continued_chain_call() {
289 type_char( 346 type_char(
290 '.', 347 '.',
291 r" 348 r#"
292 fn main() { 349fn main() {
293 xs.foo() 350 xs.foo()
294 .first() 351 .first()
295 $0 352 $0
296 } 353}
297 ", 354 "#,
298 r" 355 r#"
299 fn main() { 356fn main() {
300 xs.foo() 357 xs.foo()
301 .first() 358 .first()
302 . 359 .
303 } 360}
304 ", 361 "#,
305 ); 362 );
306 type_char_noop( 363 type_char_noop(
307 '.', 364 '.',
308 r" 365 r#"
309 fn main() { 366fn main() {
310 xs.foo() 367 xs.foo()
311 .first() 368 .first()
312 $0 369 $0
313 } 370}
314 ", 371 "#,
315 ); 372 );
316 } 373 }
317 374
@@ -319,33 +376,33 @@ fn main() {
319 fn indents_middle_of_chain_call() { 376 fn indents_middle_of_chain_call() {
320 type_char( 377 type_char(
321 '.', 378 '.',
322 r" 379 r#"
323 fn source_impl() { 380fn source_impl() {
324 let var = enum_defvariant_list().unwrap() 381 let var = enum_defvariant_list().unwrap()
325 $0 382 $0
326 .nth(92) 383 .nth(92)
327 .unwrap(); 384 .unwrap();
328 } 385}
329 ", 386 "#,
330 r" 387 r#"
331 fn source_impl() { 388fn source_impl() {
332 let var = enum_defvariant_list().unwrap() 389 let var = enum_defvariant_list().unwrap()
333 . 390 .
334 .nth(92) 391 .nth(92)
335 .unwrap(); 392 .unwrap();
336 } 393}
337 ", 394 "#,
338 ); 395 );
339 type_char_noop( 396 type_char_noop(
340 '.', 397 '.',
341 r" 398 r#"
342 fn source_impl() { 399fn source_impl() {
343 let var = enum_defvariant_list().unwrap() 400 let var = enum_defvariant_list().unwrap()
344 $0 401 $0
345 .nth(92) 402 .nth(92)
346 .unwrap(); 403 .unwrap();
347 } 404}
348 ", 405 "#,
349 ); 406 );
350 } 407 }
351 408
@@ -353,24 +410,113 @@ fn main() {
353 fn dont_indent_freestanding_dot() { 410 fn dont_indent_freestanding_dot() {
354 type_char_noop( 411 type_char_noop(
355 '.', 412 '.',
356 r" 413 r#"
357 fn main() { 414fn main() {
358 $0 415 $0
359 } 416}
360 ", 417 "#,
361 ); 418 );
362 type_char_noop( 419 type_char_noop(
363 '.', 420 '.',
364 r" 421 r#"
365 fn main() { 422fn main() {
366 $0 423$0
367 } 424}
368 ", 425 "#,
369 ); 426 );
370 } 427 }
371 428
372 #[test] 429 #[test]
373 fn adds_space_after_return_type() { 430 fn adds_space_after_return_type() {
374 type_char('>', "fn foo() -$0{ 92 }", "fn foo() -> { 92 }") 431 type_char(
432 '>',
433 r#"
434fn foo() -$0{ 92 }
435"#,
436 r#"
437fn foo() -> { 92 }
438"#,
439 );
440 }
441
442 #[test]
443 fn adds_closing_brace() {
444 type_char(
445 '{',
446 r#"
447fn f() { match () { _ => $0() } }
448 "#,
449 r#"
450fn f() { match () { _ => {()} } }
451 "#,
452 );
453 type_char(
454 '{',
455 r#"
456fn f() { $0() }
457 "#,
458 r#"
459fn f() { {()} }
460 "#,
461 );
462 type_char(
463 '{',
464 r#"
465fn f() { let x = $0(); }
466 "#,
467 r#"
468fn f() { let x = {()}; }
469 "#,
470 );
471 type_char(
472 '{',
473 r#"
474fn f() { let x = $0a.b(); }
475 "#,
476 r#"
477fn f() { let x = {a.b()}; }
478 "#,
479 );
480 type_char(
481 '{',
482 r#"
483const S: () = $0();
484fn f() {}
485 "#,
486 r#"
487const S: () = {()};
488fn f() {}
489 "#,
490 );
491 type_char(
492 '{',
493 r#"
494const S: () = $0a.b();
495fn f() {}
496 "#,
497 r#"
498const S: () = {a.b()};
499fn f() {}
500 "#,
501 );
502 type_char(
503 '{',
504 r#"
505fn f() {
506 match x {
507 0 => $0(),
508 1 => (),
509 }
510}
511 "#,
512 r#"
513fn f() {
514 match x {
515 0 => {()},
516 1 => (),
517 }
518}
519 "#,
520 );
375 } 521 }
376} 522}
diff --git a/crates/ide/src/typing/on_enter.rs b/crates/ide/src/typing/on_enter.rs
index 9144681bf..7d2db201a 100644
--- a/crates/ide/src/typing/on_enter.rs
+++ b/crates/ide/src/typing/on_enter.rs
@@ -4,10 +4,11 @@
4use ide_db::base_db::{FilePosition, SourceDatabase}; 4use ide_db::base_db::{FilePosition, SourceDatabase};
5use ide_db::RootDatabase; 5use ide_db::RootDatabase;
6use syntax::{ 6use syntax::{
7 ast::{self, AstToken}, 7 algo::find_node_at_offset,
8 ast::{self, edit::IndentLevel, AstToken},
8 AstNode, SmolStr, SourceFile, 9 AstNode, SmolStr, SourceFile,
9 SyntaxKind::*, 10 SyntaxKind::*,
10 SyntaxToken, TextRange, TextSize, TokenAtOffset, 11 SyntaxNode, SyntaxToken, TextRange, TextSize, TokenAtOffset,
11}; 12};
12 13
13use text_edit::TextEdit; 14use text_edit::TextEdit;
@@ -18,6 +19,8 @@ use text_edit::TextEdit;
18// 19//
19// - kbd:[Enter] inside triple-slash comments automatically inserts `///` 20// - kbd:[Enter] inside triple-slash comments automatically inserts `///`
20// - kbd:[Enter] in the middle or after a trailing space in `//` inserts `//` 21// - kbd:[Enter] in the middle or after a trailing space in `//` inserts `//`
22// - kbd:[Enter] inside `//!` doc comments automatically inserts `//!`
23// - kbd:[Enter] after `{` indents contents and closing `}` of single-line block
21// 24//
22// This action needs to be assigned to shortcut explicitly. 25// This action needs to be assigned to shortcut explicitly.
23// 26//
@@ -37,25 +40,43 @@ use text_edit::TextEdit;
37pub(crate) fn on_enter(db: &RootDatabase, position: FilePosition) -> Option<TextEdit> { 40pub(crate) fn on_enter(db: &RootDatabase, position: FilePosition) -> Option<TextEdit> {
38 let parse = db.parse(position.file_id); 41 let parse = db.parse(position.file_id);
39 let file = parse.tree(); 42 let file = parse.tree();
40 let comment = file 43 let token = file.syntax().token_at_offset(position.offset).left_biased()?;
41 .syntax()
42 .token_at_offset(position.offset)
43 .left_biased()
44 .and_then(ast::Comment::cast)?;
45 44
45 if let Some(comment) = ast::Comment::cast(token.clone()) {
46 return on_enter_in_comment(&comment, &file, position.offset);
47 }
48
49 if token.kind() == L_CURLY {
50 // Typing enter after the `{` of a block expression, where the `}` is on the same line
51 if let Some(edit) = find_node_at_offset(file.syntax(), position.offset - TextSize::of('{'))
52 .and_then(|block| on_enter_in_block(block, position))
53 {
54 cov_mark::hit!(indent_block_contents);
55 return Some(edit);
56 }
57 }
58
59 None
60}
61
62fn on_enter_in_comment(
63 comment: &ast::Comment,
64 file: &ast::SourceFile,
65 offset: TextSize,
66) -> Option<TextEdit> {
46 if comment.kind().shape.is_block() { 67 if comment.kind().shape.is_block() {
47 return None; 68 return None;
48 } 69 }
49 70
50 let prefix = comment.prefix(); 71 let prefix = comment.prefix();
51 let comment_range = comment.syntax().text_range(); 72 let comment_range = comment.syntax().text_range();
52 if position.offset < comment_range.start() + TextSize::of(prefix) { 73 if offset < comment_range.start() + TextSize::of(prefix) {
53 return None; 74 return None;
54 } 75 }
55 76
56 let mut remove_trailing_whitespace = false; 77 let mut remove_trailing_whitespace = false;
57 // Continuing single-line non-doc comments (like this one :) ) is annoying 78 // Continuing single-line non-doc comments (like this one :) ) is annoying
58 if prefix == "//" && comment_range.end() == position.offset { 79 if prefix == "//" && comment_range.end() == offset {
59 if comment.text().ends_with(' ') { 80 if comment.text().ends_with(' ') {
60 cov_mark::hit!(continues_end_of_line_comment_with_space); 81 cov_mark::hit!(continues_end_of_line_comment_with_space);
61 remove_trailing_whitespace = true; 82 remove_trailing_whitespace = true;
@@ -69,14 +90,42 @@ pub(crate) fn on_enter(db: &RootDatabase, position: FilePosition) -> Option<Text
69 let delete = if remove_trailing_whitespace { 90 let delete = if remove_trailing_whitespace {
70 let trimmed_len = comment.text().trim_end().len() as u32; 91 let trimmed_len = comment.text().trim_end().len() as u32;
71 let trailing_whitespace_len = comment.text().len() as u32 - trimmed_len; 92 let trailing_whitespace_len = comment.text().len() as u32 - trimmed_len;
72 TextRange::new(position.offset - TextSize::from(trailing_whitespace_len), position.offset) 93 TextRange::new(offset - TextSize::from(trailing_whitespace_len), offset)
73 } else { 94 } else {
74 TextRange::empty(position.offset) 95 TextRange::empty(offset)
75 }; 96 };
76 let edit = TextEdit::replace(delete, inserted); 97 let edit = TextEdit::replace(delete, inserted);
77 Some(edit) 98 Some(edit)
78} 99}
79 100
101fn on_enter_in_block(block: ast::BlockExpr, position: FilePosition) -> Option<TextEdit> {
102 let contents = block_contents(&block)?;
103
104 if block.syntax().text().contains_char('\n') {
105 return None;
106 }
107
108 let indent = IndentLevel::from_node(block.syntax());
109 let mut edit = TextEdit::insert(position.offset, format!("\n{}$0", indent + 1));
110 edit.union(TextEdit::insert(contents.text_range().end(), format!("\n{}", indent))).ok()?;
111 Some(edit)
112}
113
114fn block_contents(block: &ast::BlockExpr) -> Option<SyntaxNode> {
115 let mut node = block.tail_expr().map(|e| e.syntax().clone());
116
117 for stmt in block.statements() {
118 if node.is_some() {
119 // More than 1 node in the block
120 return None;
121 }
122
123 node = Some(stmt.syntax().clone());
124 }
125
126 node
127}
128
80fn followed_by_comment(comment: &ast::Comment) -> bool { 129fn followed_by_comment(comment: &ast::Comment) -> bool {
81 let ws = match comment.syntax().next_token().and_then(ast::Whitespace::cast) { 130 let ws = match comment.syntax().next_token().and_then(ast::Whitespace::cast) {
82 Some(it) => it, 131 Some(it) => it,
@@ -187,6 +236,25 @@ fn foo() {
187 } 236 }
188 237
189 #[test] 238 #[test]
239 fn continues_another_doc_comment() {
240 do_check(
241 r#"
242fn main() {
243 //! Documentation for$0 on enter
244 let x = 1 + 1;
245}
246"#,
247 r#"
248fn main() {
249 //! Documentation for
250 //! $0 on enter
251 let x = 1 + 1;
252}
253"#,
254 );
255 }
256
257 #[test]
190 fn continues_code_comment_in_the_middle_of_line() { 258 fn continues_code_comment_in_the_middle_of_line() {
191 do_check( 259 do_check(
192 r" 260 r"
@@ -276,4 +344,144 @@ fn main() {
276", 344",
277 ); 345 );
278 } 346 }
347
348 #[test]
349 fn indents_fn_body_block() {
350 cov_mark::check!(indent_block_contents);
351 do_check(
352 r#"
353fn f() {$0()}
354 "#,
355 r#"
356fn f() {
357 $0()
358}
359 "#,
360 );
361 }
362
363 #[test]
364 fn indents_block_expr() {
365 do_check(
366 r#"
367fn f() {
368 let x = {$0()};
369}
370 "#,
371 r#"
372fn f() {
373 let x = {
374 $0()
375 };
376}
377 "#,
378 );
379 }
380
381 #[test]
382 fn indents_match_arm() {
383 do_check(
384 r#"
385fn f() {
386 match 6 {
387 1 => {$0f()},
388 _ => (),
389 }
390}
391 "#,
392 r#"
393fn f() {
394 match 6 {
395 1 => {
396 $0f()
397 },
398 _ => (),
399 }
400}
401 "#,
402 );
403 }
404
405 #[test]
406 fn indents_block_with_statement() {
407 do_check(
408 r#"
409fn f() {$0a = b}
410 "#,
411 r#"
412fn f() {
413 $0a = b
414}
415 "#,
416 );
417 do_check(
418 r#"
419fn f() {$0fn f() {}}
420 "#,
421 r#"
422fn f() {
423 $0fn f() {}
424}
425 "#,
426 );
427 }
428
429 #[test]
430 fn indents_nested_blocks() {
431 do_check(
432 r#"
433fn f() {$0{}}
434 "#,
435 r#"
436fn f() {
437 $0{}
438}
439 "#,
440 );
441 }
442
443 #[test]
444 fn does_not_indent_empty_block() {
445 do_check_noop(
446 r#"
447fn f() {$0}
448 "#,
449 );
450 do_check_noop(
451 r#"
452fn f() {{$0}}
453 "#,
454 );
455 }
456
457 #[test]
458 fn does_not_indent_block_with_too_much_content() {
459 do_check_noop(
460 r#"
461fn f() {$0 a = b; ()}
462 "#,
463 );
464 do_check_noop(
465 r#"
466fn f() {$0 a = b; a = b; }
467 "#,
468 );
469 }
470
471 #[test]
472 fn does_not_indent_multiline_block() {
473 do_check_noop(
474 r#"
475fn f() {$0
476}
477 "#,
478 );
479 do_check_noop(
480 r#"
481fn f() {$0
482
483}
484 "#,
485 );
486 }
279} 487}