diff options
Diffstat (limited to 'crates/ide/src')
-rw-r--r-- | crates/ide/src/diagnostics.rs | 157 | ||||
-rw-r--r-- | crates/ide/src/diagnostics/field_shorthand.rs | 8 | ||||
-rw-r--r-- | crates/ide/src/diagnostics/fixes.rs | 60 | ||||
-rw-r--r-- | crates/ide/src/diagnostics/unlinked_file.rs | 14 | ||||
-rw-r--r-- | crates/ide/src/doc_links.rs | 190 | ||||
-rw-r--r-- | crates/ide/src/expand_macro.rs | 67 | ||||
-rw-r--r-- | crates/ide/src/folding_ranges.rs | 17 | ||||
-rw-r--r-- | crates/ide/src/goto_definition.rs | 82 | ||||
-rw-r--r-- | crates/ide/src/hover.rs | 42 | ||||
-rw-r--r-- | crates/ide/src/lib.rs | 42 | ||||
-rw-r--r-- | crates/ide/src/move_item.rs | 100 | ||||
-rw-r--r-- | crates/ide/src/prime_caches.rs | 3 | ||||
-rw-r--r-- | crates/ide/src/syntax_highlighting/highlight.rs | 73 | ||||
-rw-r--r-- | crates/ide/src/syntax_highlighting/inject.rs | 104 | ||||
-rw-r--r-- | crates/ide/src/syntax_highlighting/tags.rs | 24 | ||||
-rw-r--r-- | crates/ide/src/syntax_highlighting/test_data/highlight_doctest.html | 12 | ||||
-rw-r--r-- | crates/ide/src/syntax_highlighting/test_data/highlighting.html | 2 | ||||
-rw-r--r-- | crates/ide/src/syntax_highlighting/tests.rs | 46 | ||||
-rw-r--r-- | crates/ide/src/typing.rs | 368 | ||||
-rw-r--r-- | crates/ide/src/typing/on_enter.rs | 230 |
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; | |||
20 | use rustc_hash::FxHashSet; | 20 | use rustc_hash::FxHashSet; |
21 | use syntax::{ | 21 | use syntax::{ |
22 | ast::{self, AstNode}, | 22 | ast::{self, AstNode}, |
23 | SyntaxNode, SyntaxNodePtr, TextRange, | 23 | SyntaxNode, SyntaxNodePtr, TextRange, TextSize, |
24 | }; | 24 | }; |
25 | use text_edit::TextEdit; | 25 | use text_edit::TextEdit; |
26 | use unlinked_file::UnlinkedFile; | 26 | use unlinked_file::UnlinkedFile; |
27 | 27 | ||
28 | use crate::{FileId, Label, SourceChange}; | 28 | use crate::{Assist, AssistId, AssistKind, FileId, Label, SourceChange}; |
29 | 29 | ||
30 | use self::fixes::DiagnosticWithFix; | 30 | use 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)] | ||
73 | pub 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 | |||
80 | impl 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)] |
88 | pub enum Severity { | 73 | pub enum Severity { |
89 | Error, | 74 | Error, |
@@ -99,6 +84,7 @@ pub struct DiagnosticsConfig { | |||
99 | pub(crate) fn diagnostics( | 84 | pub(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 | ||
224 | fn diagnostic_with_fix<D: DiagnosticWithFix>(d: &D, sema: &Semantics<RootDatabase>) -> Diagnostic { | 212 | fn 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 | ||
230 | fn warning_with_fix<D: DiagnosticWithFix>(d: &D, sema: &Semantics<RootDatabase>) -> Diagnostic { | 222 | fn 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 | ||
282 | fn 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 | |||
288 | fn 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)] |
286 | mod tests { | 300 | mod 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}; | |||
5 | use syntax::{ast, match_ast, AstNode, SyntaxNode}; | 5 | use syntax::{ast, match_ast, AstNode, SyntaxNode}; |
6 | use text_edit::TextEdit; | 6 | use text_edit::TextEdit; |
7 | 7 | ||
8 | use crate::{Diagnostic, Fix}; | 8 | use crate::{diagnostics::fix, Diagnostic}; |
9 | 9 | ||
10 | pub(super) fn check(acc: &mut Vec<Diagnostic>, file_id: FileId, node: &SyntaxNode) { | 10 | pub(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 | }; |
21 | use text_edit::TextEdit; | 21 | use text_edit::TextEdit; |
22 | 22 | ||
23 | use crate::{diagnostics::Fix, references::rename::rename_with_semantics, FilePosition}; | 23 | use 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 |
28 | pub(crate) trait DiagnosticWithFix: Diagnostic { | 32 | pub(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 | ||
32 | impl DiagnosticWithFix for UnresolvedModule { | 41 | impl 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 | ||
51 | impl DiagnosticWithFix for NoSuchField { | 61 | impl 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 | ||
62 | impl DiagnosticWithFix for MissingFields { | 72 | impl 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 | ||
97 | impl DiagnosticWithFix for MissingOkOrSomeInTailExpr { | 108 | impl 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 | ||
110 | impl DiagnosticWithFix for RemoveThisSemicolon { | 121 | impl 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 | ||
130 | impl DiagnosticWithFix for IncorrectCase { | 141 | impl 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 | ||
147 | impl DiagnosticWithFix for ReplaceFilterMapNextWithFindMap { | 161 | impl 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 | }; |
17 | use text_edit::TextEdit; | 17 | use text_edit::TextEdit; |
18 | 18 | ||
19 | use crate::Fix; | 19 | use crate::{ |
20 | 20 | diagnostics::{fix, fixes::DiagnosticWithFix}, | |
21 | use 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 | ||
51 | impl DiagnosticWithFix for UnlinkedFile { | 52 | impl 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 | ||
3 | use std::{convert::TryFrom, iter::once, ops::Range}; | 3 | use std::{ |
4 | convert::{TryFrom, TryInto}, | ||
5 | iter::once, | ||
6 | }; | ||
4 | 7 | ||
5 | use itertools::Itertools; | 8 | use itertools::Itertools; |
6 | use pulldown_cmark::{BrokenLink, CowStr, Event, InlineStr, LinkType, Options, Parser, Tag}; | 9 | use pulldown_cmark::{BrokenLink, CowStr, Event, InlineStr, LinkType, Options, Parser, Tag}; |
@@ -16,8 +19,7 @@ use ide_db::{ | |||
16 | RootDatabase, | 19 | RootDatabase, |
17 | }; | 20 | }; |
18 | use syntax::{ | 21 | use 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 | ||
23 | use crate::{FilePosition, Semantics}; | 25 | use 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) |
28 | pub(crate) fn rewrite_links(db: &RootDatabase, markdown: &str, definition: &Definition) -> String { | 30 | pub(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. |
124 | pub(crate) fn extract_definitions_from_markdown( | 121 | pub(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. | ||
149 | pub(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. | ||
169 | pub(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 | ||
196 | pub(crate) fn resolve_doc_path_for_def( | 146 | pub(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 | ||
173 | pub(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 | |||
200 | fn 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 |
230 | fn get_doc_link(db: &RootDatabase, definition: Definition) -> Option<String> { | 216 | fn 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 | ||
551 | use test$0::Foo; | ||
552 | //- /lib.rs crate:test | ||
553 | pub 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 @@ | |||
1 | use std::iter; | ||
2 | |||
1 | use hir::Semantics; | 3 | use hir::Semantics; |
2 | use ide_db::RootDatabase; | 4 | use ide_db::RootDatabase; |
3 | use syntax::{ | 5 | use 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#" | ||
474 | const 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 @@ | |||
1 | use either::Either; | 1 | use either::Either; |
2 | use hir::Semantics; | 2 | use hir::{InFile, Semantics}; |
3 | use ide_db::{ | 3 | use 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 | ||
9 | use crate::{ | 9 | use 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#" |
927 | trait Iterator { | 940 | trait Iterator { type Item; } |
928 | type Item; | ||
929 | //^^^^ | ||
930 | } | ||
931 | |||
932 | fn f() -> impl Iterator<Invalid$0 = u8> {} | 941 | fn 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 | |||
1178 | pub 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 | ||
1187 | pub 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#" | ||
1198 | macro_rules! pat { | ||
1199 | ($name:ident) => { Enum::Variant1($name) } | ||
1200 | } | ||
1201 | |||
1202 | enum Enum { | ||
1203 | Variant1(u8), | ||
1204 | Variant2, | ||
1205 | } | ||
1206 | |||
1207 | fn 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 @@ | |||
1 | use either::Either; | 1 | use either::Either; |
2 | use hir::{ | 2 | use 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 | }; |
6 | use ide_db::{ | 6 | use ide_db::{ |
@@ -16,8 +16,8 @@ use syntax::{ast, match_ast, AstNode, AstToken, SyntaxKind::*, SyntaxToken, Toke | |||
16 | use crate::{ | 16 | use 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 | |
3818 | fn foo() {} | 3826 | pub 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 | ||
3834 | pub 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; | |||
69 | pub use crate::{ | 69 | pub 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 @@ | |||
1 | use std::iter::once; | 1 | use std::{iter::once, mem}; |
2 | 2 | ||
3 | use hir::Semantics; | 3 | use hir::Semantics; |
4 | use ide_db::{base_db::FileRange, RootDatabase}; | 4 | use 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 | ||
137 | fn replace_nodes(first: &SyntaxNode, second: &SyntaxNode) -> TextEdit { | 137 | fn 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#" |
189 | fn main() { | 216 | fn 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#" |
276 | fn main() { | 303 | fn 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#" |
295 | fn main() { | 322 | fn 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() { | |||
313 | fn main() { | 340 | fn 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() { | |||
334 | fn main() { | 361 | fn 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() { | |||
355 | fn main() { | 382 | fn 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() { | |||
376 | fn main() { | 403 | fn 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#" |
395 | fn main() { | 422 | fn 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() {} | |||
430 | fn foo() {}$0$0 | 457 | fn foo() {}$0$0 |
431 | "#, | 458 | "#, |
432 | expect![[r#" | 459 | expect![[r#" |
433 | fn foo() {} | 460 | fn foo() {}$0 |
434 | 461 | ||
435 | fn main() {} | 462 | fn main() {} |
436 | "#]], | 463 | "#]], |
@@ -451,7 +478,7 @@ impl Wow for Yay $0$0{} | |||
451 | expect![[r#" | 478 | expect![[r#" |
452 | struct Yay; | 479 | struct Yay; |
453 | 480 | ||
454 | impl Wow for Yay {} | 481 | impl Wow for Yay $0{} |
455 | 482 | ||
456 | trait Wow {} | 483 | trait Wow {} |
457 | "#]], | 484 | "#]], |
@@ -467,7 +494,7 @@ use std::vec::Vec; | |||
467 | use std::collections::HashMap$0$0; | 494 | use std::collections::HashMap$0$0; |
468 | "#, | 495 | "#, |
469 | expect![[r#" | 496 | expect![[r#" |
470 | use std::collections::HashMap; | 497 | use std::collections::HashMap$0; |
471 | use std::vec::Vec; | 498 | use 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#" |
508 | fn test(one: i32, two$0$0: u32) {} | 535 | fn test(one: i32, two$0$0: u32) {} |
@@ -512,7 +539,7 @@ fn main() { | |||
512 | } | 539 | } |
513 | "#, | 540 | "#, |
514 | expect![[r#" | 541 | expect![[r#" |
515 | fn test(two: u32, one: i32) {} | 542 | fn test(two$0: u32, one: i32) {} |
516 | 543 | ||
517 | fn main() { | 544 | fn 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#" | ||
552 | fn f($0$0arg: u8, arg2: u16) {} | ||
553 | "#, | ||
554 | expect![[r#" | ||
555 | fn f(arg2: u16, $0arg: u8) {} | ||
556 | "#]], | ||
557 | Direction::Down, | ||
558 | ); | ||
523 | } | 559 | } |
524 | 560 | ||
525 | #[test] | 561 | #[test] |
@@ -536,7 +572,7 @@ fn main() { | |||
536 | fn test(one: i32, two: u32) {} | 572 | fn test(one: i32, two: u32) {} |
537 | 573 | ||
538 | fn main() { | 574 | fn 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() { | |||
557 | fn test(one: i32, two: u32) {} | 593 | fn test(one: i32, two: u32) {} |
558 | 594 | ||
559 | fn main() { | 595 | fn 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); | |||
594 | fn main() {} | 630 | fn main() {} |
595 | "#, | 631 | "#, |
596 | expect![[r#" | 632 | expect![[r#" |
597 | struct Test<B, A>(A, B); | 633 | struct Test<B$0, A>(A, B); |
598 | 634 | ||
599 | fn main() {} | 635 | fn main() {} |
600 | "#]], | 636 | "#]], |
@@ -616,7 +652,7 @@ fn main() { | |||
616 | struct Test<A, B>(A, B); | 652 | struct Test<A, B>(A, B); |
617 | 653 | ||
618 | fn main() { | 654 | fn 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#" |
638 | enum Hello { | 674 | enum Hello { |
639 | Two, | 675 | Two$0, |
640 | One | 676 | One |
641 | } | 677 | } |
642 | 678 | ||
@@ -663,7 +699,7 @@ trait One {} | |||
663 | 699 | ||
664 | trait Two {} | 700 | trait Two {} |
665 | 701 | ||
666 | fn test<T: Two + One>(t: T) {} | 702 | fn test<T: Two$0 + One>(t: T) {} |
667 | 703 | ||
668 | fn main() {} | 704 | fn main() {} |
669 | "#]], | 705 | "#]], |
@@ -709,7 +745,7 @@ trait Yay { | |||
709 | impl Yay for Test { | 745 | impl 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#" |
738 | fn test() { | 774 | fn 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#" |
765 | fn main() {} | 801 | fn main() {} |
766 | 802 | ||
767 | #[derive(Debug)] | 803 | $0#[derive(Debug)] |
768 | enum FooBar { | 804 | enum FooBar { |
769 | Foo, | 805 | Foo, |
770 | Bar, | 806 | Bar, |
@@ -784,7 +820,7 @@ fn main() {} | |||
784 | expect![[r#" | 820 | expect![[r#" |
785 | fn main() {} | 821 | fn main() {} |
786 | 822 | ||
787 | enum 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#" |
805 | struct Test; | 841 | struct Test; |
806 | 842 | ||
807 | impl SomeTrait for Test {} | 843 | $0impl SomeTrait for Test {} |
808 | 844 | ||
809 | trait SomeTrait {} | 845 | trait SomeTrait {} |
810 | 846 | ||
@@ -831,7 +867,7 @@ fn main() {} | |||
831 | enum FooBar { | 867 | enum 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#" |
849 | struct Test; | 885 | struct Test; |
850 | 886 | ||
851 | impl SomeTrait for Test {} | 887 | impl SomeTrait for Test {}$0 |
852 | 888 | ||
853 | trait SomeTrait {} | 889 | trait 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 | ||
15 | use crate::{syntax_highlighting::tags::HlPunct, Highlight, HlMod, HlTag}; | 15 | use crate::{ |
16 | syntax_highlighting::tags::{HlOperator, HlPunct}, | ||
17 | Highlight, HlMod, HlTag, | ||
18 | }; | ||
16 | 19 | ||
17 | pub(super) fn element( | 20 | pub(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 | ||
3 | use std::{mem, ops::Range}; | 3 | use std::mem; |
4 | 4 | ||
5 | use either::Either; | 5 | use either::Either; |
6 | use hir::{HasAttrs, InFile, Semantics}; | 6 | use hir::{InFile, Semantics}; |
7 | use ide_db::{call_info::ActiveParameter, defs::Definition, SymbolKind}; | 7 | use ide_db::{call_info::ActiveParameter, helpers::rust_doc::is_rust_fence, SymbolKind}; |
8 | use syntax::{ | 8 | use 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 | ||
13 | use crate::{ | 13 | use 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 | ||
80 | const RUSTDOC_FENCE: &'static str = "```"; | 80 | const RUSTDOC_FENCE: &'static str = "```"; |
81 | const 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 | |||
93 | fn 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. |
121 | pub(super) fn doc_comment( | 83 | pub(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)] | ||
91 | pub enum HlOperator { | ||
92 | /// |, &, !, ^, |=, &=, ^= | ||
93 | Bitwise, | ||
94 | /// +, -, *, /, +=, -=, *=, /= | ||
95 | Arithmetic, | ||
96 | /// &&, ||, ! | ||
97 | Logical, | ||
98 | /// >, <, ==, >=, <=, != | ||
99 | Comparison, | ||
100 | /// | ||
101 | Other, | ||
102 | } | ||
103 | |||
90 | impl HlTag { | 104 | impl 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 > </span><span class="function documentation intra_doc_link injected">[`all_the_links`](all_the_links)</span><span class="comment documentation"> <</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 @@ | |||
1 | use std::time::Instant; | ||
2 | |||
1 | use expect_test::{expect_file, ExpectFile}; | 3 | use expect_test::{expect_file, ExpectFile}; |
2 | use ide_db::SymbolKind; | 4 | use ide_db::SymbolKind; |
3 | use test_utils::{bench, bench_fixture, skip_slow_tests}; | 5 | use test_utils::{bench, bench_fixture, skip_slow_tests, AssertLinear}; |
4 | 6 | ||
5 | use crate::{fixture, FileRange, HlTag, TextRange}; | 7 | use 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] |
263 | fn 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] | ||
261 | fn benchmark_syntax_highlighting_parser() { | 293 | fn 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 | ||
549 | pub fn all_the_links() {} | 585 | pub fn all_the_links() {} |
550 | 586 | ||
587 | pub 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::{ | |||
22 | use syntax::{ | 22 | use 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 | ||
30 | use text_edit::TextEdit; | 30 | use text_edit::{Indel, TextEdit}; |
31 | 31 | ||
32 | use crate::SourceChange; | 32 | use crate::SourceChange; |
33 | 33 | ||
34 | pub(crate) use on_enter::on_enter; | 34 | pub(crate) use on_enter::on_enter; |
35 | 35 | ||
36 | pub(crate) const TRIGGER_CHARS: &str = ".=>"; | 36 | // Don't forget to add new trigger characters to `server_capabilities` in `caps.rs`. |
37 | pub(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 | ||
67 | fn on_char_typed_inner(file: &SourceFile, offset: TextSize, char_typed: char) -> Option<TextEdit> { | 73 | fn 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. | ||
92 | fn 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. |
80 | fn on_eq_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> { | 131 | fn 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. |
102 | fn on_dot_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> { | 155 | fn 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() -> { ... }` |
130 | fn on_arrow_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> { | 185 | fn 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#" |
187 | fn foo() { | 244 | fn foo() { |
188 | let foo $0 1 + 1 | 245 | let foo $0 1 + 1 |
189 | } | 246 | } |
190 | ", | 247 | "#, |
191 | r" | 248 | r#" |
192 | fn foo() { | 249 | fn 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() { | 272 | fn main() { |
216 | xs.foo() | 273 | xs.foo() |
217 | $0 | 274 | $0 |
218 | } | 275 | } |
219 | ", | 276 | "#, |
220 | r" | 277 | r#" |
221 | fn main() { | 278 | fn 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() { | 287 | fn 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() { | 300 | fn 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#" | ||
306 | fn 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() { | 315 | fn 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() { | 349 | fn 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() { | 356 | fn 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() { | 366 | fn 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() { | 380 | fn 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() { | 388 | fn 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() { | 399 | fn 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() { | 414 | fn 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() { | 422 | fn 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#" | ||
434 | fn foo() -$0{ 92 } | ||
435 | "#, | ||
436 | r#" | ||
437 | fn foo() -> { 92 } | ||
438 | "#, | ||
439 | ); | ||
440 | } | ||
441 | |||
442 | #[test] | ||
443 | fn adds_closing_brace() { | ||
444 | type_char( | ||
445 | '{', | ||
446 | r#" | ||
447 | fn f() { match () { _ => $0() } } | ||
448 | "#, | ||
449 | r#" | ||
450 | fn f() { match () { _ => {()} } } | ||
451 | "#, | ||
452 | ); | ||
453 | type_char( | ||
454 | '{', | ||
455 | r#" | ||
456 | fn f() { $0() } | ||
457 | "#, | ||
458 | r#" | ||
459 | fn f() { {()} } | ||
460 | "#, | ||
461 | ); | ||
462 | type_char( | ||
463 | '{', | ||
464 | r#" | ||
465 | fn f() { let x = $0(); } | ||
466 | "#, | ||
467 | r#" | ||
468 | fn f() { let x = {()}; } | ||
469 | "#, | ||
470 | ); | ||
471 | type_char( | ||
472 | '{', | ||
473 | r#" | ||
474 | fn f() { let x = $0a.b(); } | ||
475 | "#, | ||
476 | r#" | ||
477 | fn f() { let x = {a.b()}; } | ||
478 | "#, | ||
479 | ); | ||
480 | type_char( | ||
481 | '{', | ||
482 | r#" | ||
483 | const S: () = $0(); | ||
484 | fn f() {} | ||
485 | "#, | ||
486 | r#" | ||
487 | const S: () = {()}; | ||
488 | fn f() {} | ||
489 | "#, | ||
490 | ); | ||
491 | type_char( | ||
492 | '{', | ||
493 | r#" | ||
494 | const S: () = $0a.b(); | ||
495 | fn f() {} | ||
496 | "#, | ||
497 | r#" | ||
498 | const S: () = {a.b()}; | ||
499 | fn f() {} | ||
500 | "#, | ||
501 | ); | ||
502 | type_char( | ||
503 | '{', | ||
504 | r#" | ||
505 | fn f() { | ||
506 | match x { | ||
507 | 0 => $0(), | ||
508 | 1 => (), | ||
509 | } | ||
510 | } | ||
511 | "#, | ||
512 | r#" | ||
513 | fn 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 @@ | |||
4 | use ide_db::base_db::{FilePosition, SourceDatabase}; | 4 | use ide_db::base_db::{FilePosition, SourceDatabase}; |
5 | use ide_db::RootDatabase; | 5 | use ide_db::RootDatabase; |
6 | use syntax::{ | 6 | use 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 | ||
13 | use text_edit::TextEdit; | 14 | use 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; | |||
37 | pub(crate) fn on_enter(db: &RootDatabase, position: FilePosition) -> Option<TextEdit> { | 40 | pub(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 | |||
62 | fn 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 | ||
101 | fn 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 | |||
114 | fn 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 | |||
80 | fn followed_by_comment(comment: &ast::Comment) -> bool { | 129 | fn 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#" | ||
242 | fn main() { | ||
243 | //! Documentation for$0 on enter | ||
244 | let x = 1 + 1; | ||
245 | } | ||
246 | "#, | ||
247 | r#" | ||
248 | fn 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#" | ||
353 | fn f() {$0()} | ||
354 | "#, | ||
355 | r#" | ||
356 | fn f() { | ||
357 | $0() | ||
358 | } | ||
359 | "#, | ||
360 | ); | ||
361 | } | ||
362 | |||
363 | #[test] | ||
364 | fn indents_block_expr() { | ||
365 | do_check( | ||
366 | r#" | ||
367 | fn f() { | ||
368 | let x = {$0()}; | ||
369 | } | ||
370 | "#, | ||
371 | r#" | ||
372 | fn f() { | ||
373 | let x = { | ||
374 | $0() | ||
375 | }; | ||
376 | } | ||
377 | "#, | ||
378 | ); | ||
379 | } | ||
380 | |||
381 | #[test] | ||
382 | fn indents_match_arm() { | ||
383 | do_check( | ||
384 | r#" | ||
385 | fn f() { | ||
386 | match 6 { | ||
387 | 1 => {$0f()}, | ||
388 | _ => (), | ||
389 | } | ||
390 | } | ||
391 | "#, | ||
392 | r#" | ||
393 | fn 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#" | ||
409 | fn f() {$0a = b} | ||
410 | "#, | ||
411 | r#" | ||
412 | fn f() { | ||
413 | $0a = b | ||
414 | } | ||
415 | "#, | ||
416 | ); | ||
417 | do_check( | ||
418 | r#" | ||
419 | fn f() {$0fn f() {}} | ||
420 | "#, | ||
421 | r#" | ||
422 | fn f() { | ||
423 | $0fn f() {} | ||
424 | } | ||
425 | "#, | ||
426 | ); | ||
427 | } | ||
428 | |||
429 | #[test] | ||
430 | fn indents_nested_blocks() { | ||
431 | do_check( | ||
432 | r#" | ||
433 | fn f() {$0{}} | ||
434 | "#, | ||
435 | r#" | ||
436 | fn f() { | ||
437 | $0{} | ||
438 | } | ||
439 | "#, | ||
440 | ); | ||
441 | } | ||
442 | |||
443 | #[test] | ||
444 | fn does_not_indent_empty_block() { | ||
445 | do_check_noop( | ||
446 | r#" | ||
447 | fn f() {$0} | ||
448 | "#, | ||
449 | ); | ||
450 | do_check_noop( | ||
451 | r#" | ||
452 | fn f() {{$0}} | ||
453 | "#, | ||
454 | ); | ||
455 | } | ||
456 | |||
457 | #[test] | ||
458 | fn does_not_indent_block_with_too_much_content() { | ||
459 | do_check_noop( | ||
460 | r#" | ||
461 | fn f() {$0 a = b; ()} | ||
462 | "#, | ||
463 | ); | ||
464 | do_check_noop( | ||
465 | r#" | ||
466 | fn 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#" | ||
475 | fn f() {$0 | ||
476 | } | ||
477 | "#, | ||
478 | ); | ||
479 | do_check_noop( | ||
480 | r#" | ||
481 | fn f() {$0 | ||
482 | |||
483 | } | ||
484 | "#, | ||
485 | ); | ||
486 | } | ||
279 | } | 487 | } |