aboutsummaryrefslogtreecommitdiff
path: root/crates/ide
diff options
context:
space:
mode:
Diffstat (limited to 'crates/ide')
-rw-r--r--crates/ide/src/diagnostics.rs143
-rw-r--r--crates/ide/src/diagnostics/field_shorthand.rs8
-rw-r--r--crates/ide/src/diagnostics/fixes.rs60
-rw-r--r--crates/ide/src/diagnostics/unlinked_file.rs14
-rw-r--r--crates/ide/src/goto_definition.rs18
-rw-r--r--crates/ide/src/lib.rs36
-rw-r--r--crates/ide/src/move_item.rs100
-rw-r--r--crates/ide/src/syntax_highlighting/highlight.rs59
-rw-r--r--crates/ide/src/syntax_highlighting/inject.rs10
-rw-r--r--crates/ide/src/syntax_highlighting/tags.rs24
-rw-r--r--crates/ide/src/syntax_highlighting/test_data/highlight_doctest.html2
-rw-r--r--crates/ide/src/syntax_highlighting/test_data/highlighting.html2
-rw-r--r--crates/ide/src/syntax_highlighting/tests.rs98
-rw-r--r--crates/ide/src/typing.rs200
-rw-r--r--crates/ide/src/typing/on_enter.rs210
15 files changed, 646 insertions, 338 deletions
diff --git a/crates/ide/src/diagnostics.rs b/crates/ide/src/diagnostics.rs
index 0ace80a1e..1c911a8b2 100644
--- a/crates/ide/src/diagnostics.rs
+++ b/crates/ide/src/diagnostics.rs
@@ -25,7 +25,7 @@ use syntax::{
25use text_edit::TextEdit; 25use text_edit::TextEdit;
26use unlinked_file::UnlinkedFile; 26use unlinked_file::UnlinkedFile;
27 27
28use crate::{FileId, Label, SourceChange}; 28use crate::{Assist, AssistId, AssistKind, FileId, Label, SourceChange};
29 29
30use self::fixes::DiagnosticWithFix; 30use self::fixes::DiagnosticWithFix;
31 31
@@ -35,7 +35,7 @@ pub struct Diagnostic {
35 pub message: String, 35 pub message: String,
36 pub range: TextRange, 36 pub range: TextRange,
37 pub severity: Severity, 37 pub severity: Severity,
38 pub fix: Option<Fix>, 38 pub fix: Option<Assist>,
39 pub unused: bool, 39 pub unused: bool,
40 pub code: Option<DiagnosticCode>, 40 pub code: Option<DiagnosticCode>,
41} 41}
@@ -56,7 +56,7 @@ impl Diagnostic {
56 } 56 }
57 } 57 }
58 58
59 fn with_fix(self, fix: Option<Fix>) -> Self { 59 fn with_fix(self, fix: Option<Assist>) -> Self {
60 Self { fix, ..self } 60 Self { fix, ..self }
61 } 61 }
62 62
@@ -69,21 +69,6 @@ impl Diagnostic {
69 } 69 }
70} 70}
71 71
72#[derive(Debug)]
73pub struct Fix {
74 pub label: Label,
75 pub source_change: SourceChange,
76 /// Allows to trigger the fix only when the caret is in the range given
77 pub fix_trigger_range: TextRange,
78}
79
80impl Fix {
81 fn new(label: &str, source_change: SourceChange, fix_trigger_range: TextRange) -> Self {
82 let label = Label::new(label);
83 Self { label, source_change, fix_trigger_range }
84 }
85}
86
87#[derive(Debug, Copy, Clone)] 72#[derive(Debug, Copy, Clone)]
88pub enum Severity { 73pub enum Severity {
89 Error, 74 Error,
@@ -99,6 +84,7 @@ pub struct DiagnosticsConfig {
99pub(crate) fn diagnostics( 84pub(crate) fn diagnostics(
100 db: &RootDatabase, 85 db: &RootDatabase,
101 config: &DiagnosticsConfig, 86 config: &DiagnosticsConfig,
87 resolve: bool,
102 file_id: FileId, 88 file_id: FileId,
103) -> Vec<Diagnostic> { 89) -> Vec<Diagnostic> {
104 let _p = profile::span("diagnostics"); 90 let _p = profile::span("diagnostics");
@@ -122,25 +108,25 @@ pub(crate) fn diagnostics(
122 let res = RefCell::new(res); 108 let res = RefCell::new(res);
123 let sink_builder = DiagnosticSinkBuilder::new() 109 let sink_builder = DiagnosticSinkBuilder::new()
124 .on::<hir::diagnostics::UnresolvedModule, _>(|d| { 110 .on::<hir::diagnostics::UnresolvedModule, _>(|d| {
125 res.borrow_mut().push(diagnostic_with_fix(d, &sema)); 111 res.borrow_mut().push(diagnostic_with_fix(d, &sema, resolve));
126 }) 112 })
127 .on::<hir::diagnostics::MissingFields, _>(|d| { 113 .on::<hir::diagnostics::MissingFields, _>(|d| {
128 res.borrow_mut().push(diagnostic_with_fix(d, &sema)); 114 res.borrow_mut().push(diagnostic_with_fix(d, &sema, resolve));
129 }) 115 })
130 .on::<hir::diagnostics::MissingOkOrSomeInTailExpr, _>(|d| { 116 .on::<hir::diagnostics::MissingOkOrSomeInTailExpr, _>(|d| {
131 res.borrow_mut().push(diagnostic_with_fix(d, &sema)); 117 res.borrow_mut().push(diagnostic_with_fix(d, &sema, resolve));
132 }) 118 })
133 .on::<hir::diagnostics::NoSuchField, _>(|d| { 119 .on::<hir::diagnostics::NoSuchField, _>(|d| {
134 res.borrow_mut().push(diagnostic_with_fix(d, &sema)); 120 res.borrow_mut().push(diagnostic_with_fix(d, &sema, resolve));
135 }) 121 })
136 .on::<hir::diagnostics::RemoveThisSemicolon, _>(|d| { 122 .on::<hir::diagnostics::RemoveThisSemicolon, _>(|d| {
137 res.borrow_mut().push(diagnostic_with_fix(d, &sema)); 123 res.borrow_mut().push(diagnostic_with_fix(d, &sema, resolve));
138 }) 124 })
139 .on::<hir::diagnostics::IncorrectCase, _>(|d| { 125 .on::<hir::diagnostics::IncorrectCase, _>(|d| {
140 res.borrow_mut().push(warning_with_fix(d, &sema)); 126 res.borrow_mut().push(warning_with_fix(d, &sema, resolve));
141 }) 127 })
142 .on::<hir::diagnostics::ReplaceFilterMapNextWithFindMap, _>(|d| { 128 .on::<hir::diagnostics::ReplaceFilterMapNextWithFindMap, _>(|d| {
143 res.borrow_mut().push(warning_with_fix(d, &sema)); 129 res.borrow_mut().push(warning_with_fix(d, &sema, resolve));
144 }) 130 })
145 .on::<hir::diagnostics::InactiveCode, _>(|d| { 131 .on::<hir::diagnostics::InactiveCode, _>(|d| {
146 // If there's inactive code somewhere in a macro, don't propagate to the call-site. 132 // If there's inactive code somewhere in a macro, don't propagate to the call-site.
@@ -167,7 +153,7 @@ pub(crate) fn diagnostics(
167 // Override severity and mark as unused. 153 // Override severity and mark as unused.
168 res.borrow_mut().push( 154 res.borrow_mut().push(
169 Diagnostic::hint(range, d.message()) 155 Diagnostic::hint(range, d.message())
170 .with_fix(d.fix(&sema)) 156 .with_fix(d.fix(&sema, resolve))
171 .with_code(Some(d.code())), 157 .with_code(Some(d.code())),
172 ); 158 );
173 }) 159 })
@@ -223,15 +209,23 @@ pub(crate) fn diagnostics(
223 res.into_inner() 209 res.into_inner()
224} 210}
225 211
226fn diagnostic_with_fix<D: DiagnosticWithFix>(d: &D, sema: &Semantics<RootDatabase>) -> Diagnostic { 212fn diagnostic_with_fix<D: DiagnosticWithFix>(
213 d: &D,
214 sema: &Semantics<RootDatabase>,
215 resolve: bool,
216) -> Diagnostic {
227 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())
228 .with_fix(d.fix(&sema)) 218 .with_fix(d.fix(&sema, resolve))
229 .with_code(Some(d.code())) 219 .with_code(Some(d.code()))
230} 220}
231 221
232fn warning_with_fix<D: DiagnosticWithFix>(d: &D, sema: &Semantics<RootDatabase>) -> Diagnostic { 222fn warning_with_fix<D: DiagnosticWithFix>(
223 d: &D,
224 sema: &Semantics<RootDatabase>,
225 resolve: bool,
226) -> Diagnostic {
233 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())
234 .with_fix(d.fix(&sema)) 228 .with_fix(d.fix(&sema, resolve))
235 .with_code(Some(d.code())) 229 .with_code(Some(d.code()))
236} 230}
237 231
@@ -261,7 +255,8 @@ fn check_unnecessary_braces_in_use_statement(
261 255
262 acc.push( 256 acc.push(
263 Diagnostic::hint(use_range, "Unnecessary braces in use statement".to_string()) 257 Diagnostic::hint(use_range, "Unnecessary braces in use statement".to_string())
264 .with_fix(Some(Fix::new( 258 .with_fix(Some(fix(
259 "remove_braces",
265 "Remove unnecessary braces", 260 "Remove unnecessary braces",
266 SourceChange::from_text_edit(file_id, edit), 261 SourceChange::from_text_edit(file_id, edit),
267 use_range, 262 use_range,
@@ -284,6 +279,23 @@ fn text_edit_for_remove_unnecessary_braces_with_self_in_use_statement(
284 None 279 None
285} 280}
286 281
282fn fix(id: &'static str, label: &str, source_change: SourceChange, target: TextRange) -> Assist {
283 let mut res = unresolved_fix(id, label, target);
284 res.source_change = Some(source_change);
285 res
286}
287
288fn unresolved_fix(id: &'static str, label: &str, target: TextRange) -> Assist {
289 assert!(!id.contains(' '));
290 Assist {
291 id: AssistId(id, AssistKind::QuickFix),
292 label: Label::new(label),
293 group: None,
294 target,
295 source_change: None,
296 }
297}
298
287#[cfg(test)] 299#[cfg(test)]
288mod tests { 300mod tests {
289 use expect_test::{expect, Expect}; 301 use expect_test::{expect, Expect};
@@ -302,16 +314,17 @@ mod tests {
302 314
303 let (analysis, file_position) = fixture::position(ra_fixture_before); 315 let (analysis, file_position) = fixture::position(ra_fixture_before);
304 let diagnostic = analysis 316 let diagnostic = analysis
305 .diagnostics(&DiagnosticsConfig::default(), file_position.file_id) 317 .diagnostics(&DiagnosticsConfig::default(), true, file_position.file_id)
306 .unwrap() 318 .unwrap()
307 .pop() 319 .pop()
308 .unwrap(); 320 .unwrap();
309 let fix = diagnostic.fix.unwrap(); 321 let fix = diagnostic.fix.unwrap();
310 let actual = { 322 let actual = {
311 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();
312 let mut actual = analysis.file_text(file_id).unwrap().to_string(); 325 let mut actual = analysis.file_text(file_id).unwrap().to_string();
313 326
314 for edit in fix.source_change.source_file_edits.values() { 327 for edit in source_change.source_file_edits.values() {
315 edit.apply(&mut actual); 328 edit.apply(&mut actual);
316 } 329 }
317 actual 330 actual
@@ -319,9 +332,9 @@ mod tests {
319 332
320 assert_eq_text!(&after, &actual); 333 assert_eq_text!(&after, &actual);
321 assert!( 334 assert!(
322 fix.fix_trigger_range.contains_inclusive(file_position.offset), 335 fix.target.contains_inclusive(file_position.offset),
323 "diagnostic fix range {:?} does not touch cursor position {:?}", 336 "diagnostic fix range {:?} does not touch cursor position {:?}",
324 fix.fix_trigger_range, 337 fix.target,
325 file_position.offset 338 file_position.offset
326 ); 339 );
327 } 340 }
@@ -330,7 +343,7 @@ mod tests {
330 fn check_no_fix(ra_fixture: &str) { 343 fn check_no_fix(ra_fixture: &str) {
331 let (analysis, file_position) = fixture::position(ra_fixture); 344 let (analysis, file_position) = fixture::position(ra_fixture);
332 let diagnostic = analysis 345 let diagnostic = analysis
333 .diagnostics(&DiagnosticsConfig::default(), file_position.file_id) 346 .diagnostics(&DiagnosticsConfig::default(), true, file_position.file_id)
334 .unwrap() 347 .unwrap()
335 .pop() 348 .pop()
336 .unwrap(); 349 .unwrap();
@@ -344,7 +357,7 @@ mod tests {
344 let diagnostics = files 357 let diagnostics = files
345 .into_iter() 358 .into_iter()
346 .flat_map(|file_id| { 359 .flat_map(|file_id| {
347 analysis.diagnostics(&DiagnosticsConfig::default(), file_id).unwrap() 360 analysis.diagnostics(&DiagnosticsConfig::default(), true, file_id).unwrap()
348 }) 361 })
349 .collect::<Vec<_>>(); 362 .collect::<Vec<_>>();
350 assert_eq!(diagnostics.len(), 0, "unexpected diagnostics:\n{:#?}", diagnostics); 363 assert_eq!(diagnostics.len(), 0, "unexpected diagnostics:\n{:#?}", diagnostics);
@@ -352,7 +365,8 @@ mod tests {
352 365
353 fn check_expect(ra_fixture: &str, expect: Expect) { 366 fn check_expect(ra_fixture: &str, expect: Expect) {
354 let (analysis, file_id) = fixture::file(ra_fixture); 367 let (analysis, file_id) = fixture::file(ra_fixture);
355 let diagnostics = analysis.diagnostics(&DiagnosticsConfig::default(), file_id).unwrap(); 368 let diagnostics =
369 analysis.diagnostics(&DiagnosticsConfig::default(), true, file_id).unwrap();
356 expect.assert_debug_eq(&diagnostics) 370 expect.assert_debug_eq(&diagnostics)
357 } 371 }
358 372
@@ -665,24 +679,31 @@ fn test_fn() {
665 range: 0..8, 679 range: 0..8,
666 severity: Error, 680 severity: Error,
667 fix: Some( 681 fix: Some(
668 Fix { 682 Assist {
683 id: AssistId(
684 "create_module",
685 QuickFix,
686 ),
669 label: "Create module", 687 label: "Create module",
670 source_change: SourceChange { 688 group: None,
671 source_file_edits: {}, 689 target: 0..8,
672 file_system_edits: [ 690 source_change: Some(
673 CreateFile { 691 SourceChange {
674 dst: AnchoredPathBuf { 692 source_file_edits: {},
675 anchor: FileId( 693 file_system_edits: [
676 0, 694 CreateFile {
677 ), 695 dst: AnchoredPathBuf {
678 path: "foo.rs", 696 anchor: FileId(
697 0,
698 ),
699 path: "foo.rs",
700 },
701 initial_contents: "",
679 }, 702 },
680 initial_contents: "", 703 ],
681 }, 704 is_snippet: false,
682 ], 705 },
683 is_snippet: false, 706 ),
684 },
685 fix_trigger_range: 0..8,
686 }, 707 },
687 ), 708 ),
688 unused: false, 709 unused: false,
@@ -704,7 +725,7 @@ fn test_fn() {
704 expect![[r#" 725 expect![[r#"
705 [ 726 [
706 Diagnostic { 727 Diagnostic {
707 message: "unresolved macro call", 728 message: "unresolved macro `foo::bar!`",
708 range: 5..8, 729 range: 5..8,
709 severity: Error, 730 severity: Error,
710 fix: None, 731 fix: None,
@@ -890,10 +911,11 @@ struct Foo {
890 911
891 let (analysis, file_id) = fixture::file(r#"mod foo;"#); 912 let (analysis, file_id) = fixture::file(r#"mod foo;"#);
892 913
893 let diagnostics = analysis.diagnostics(&config, file_id).unwrap(); 914 let diagnostics = analysis.diagnostics(&config, true, file_id).unwrap();
894 assert!(diagnostics.is_empty()); 915 assert!(diagnostics.is_empty());
895 916
896 let diagnostics = analysis.diagnostics(&DiagnosticsConfig::default(), file_id).unwrap(); 917 let diagnostics =
918 analysis.diagnostics(&DiagnosticsConfig::default(), true, file_id).unwrap();
897 assert!(!diagnostics.is_empty()); 919 assert!(!diagnostics.is_empty());
898 } 920 }
899 921
@@ -999,8 +1021,9 @@ impl TestStruct {
999 let expected = r#"fn foo() {}"#; 1021 let expected = r#"fn foo() {}"#;
1000 1022
1001 let (analysis, file_position) = fixture::position(input); 1023 let (analysis, file_position) = fixture::position(input);
1002 let diagnostics = 1024 let diagnostics = analysis
1003 analysis.diagnostics(&DiagnosticsConfig::default(), file_position.file_id).unwrap(); 1025 .diagnostics(&DiagnosticsConfig::default(), true, file_position.file_id)
1026 .unwrap();
1004 assert_eq!(diagnostics.len(), 1); 1027 assert_eq!(diagnostics.len(), 1);
1005 1028
1006 check_fix(input, expected); 1029 check_fix(input, expected);
diff --git a/crates/ide/src/diagnostics/field_shorthand.rs b/crates/ide/src/diagnostics/field_shorthand.rs
index 5c89e2170..2b1787f9b 100644
--- a/crates/ide/src/diagnostics/field_shorthand.rs
+++ b/crates/ide/src/diagnostics/field_shorthand.rs
@@ -5,7 +5,7 @@ use ide_db::{base_db::FileId, source_change::SourceChange};
5use syntax::{ast, match_ast, AstNode, SyntaxNode}; 5use syntax::{ast, match_ast, AstNode, SyntaxNode};
6use text_edit::TextEdit; 6use text_edit::TextEdit;
7 7
8use crate::{Diagnostic, Fix}; 8use crate::{diagnostics::fix, Diagnostic};
9 9
10pub(super) fn check(acc: &mut Vec<Diagnostic>, file_id: FileId, node: &SyntaxNode) { 10pub(super) fn check(acc: &mut Vec<Diagnostic>, file_id: FileId, node: &SyntaxNode) {
11 match_ast! { 11 match_ast! {
@@ -47,7 +47,8 @@ fn check_expr_field_shorthand(
47 let field_range = record_field.syntax().text_range(); 47 let field_range = record_field.syntax().text_range();
48 acc.push( 48 acc.push(
49 Diagnostic::hint(field_range, "Shorthand struct initialization".to_string()).with_fix( 49 Diagnostic::hint(field_range, "Shorthand struct initialization".to_string()).with_fix(
50 Some(Fix::new( 50 Some(fix(
51 "use_expr_field_shorthand",
51 "Use struct shorthand initialization", 52 "Use struct shorthand initialization",
52 SourceChange::from_text_edit(file_id, edit), 53 SourceChange::from_text_edit(file_id, edit),
53 field_range, 54 field_range,
@@ -86,7 +87,8 @@ fn check_pat_field_shorthand(
86 87
87 let field_range = record_pat_field.syntax().text_range(); 88 let field_range = record_pat_field.syntax().text_range();
88 acc.push(Diagnostic::hint(field_range, "Shorthand struct pattern".to_string()).with_fix( 89 acc.push(Diagnostic::hint(field_range, "Shorthand struct pattern".to_string()).with_fix(
89 Some(Fix::new( 90 Some(fix(
91 "use_pat_field_shorthand",
90 "Use struct field shorthand", 92 "Use struct field shorthand",
91 SourceChange::from_text_edit(file_id, edit), 93 SourceChange::from_text_edit(file_id, edit),
92 field_range, 94 field_range,
diff --git a/crates/ide/src/diagnostics/fixes.rs b/crates/ide/src/diagnostics/fixes.rs
index 5fb3e2d91..7be8b3459 100644
--- a/crates/ide/src/diagnostics/fixes.rs
+++ b/crates/ide/src/diagnostics/fixes.rs
@@ -20,20 +20,30 @@ use syntax::{
20}; 20};
21use text_edit::TextEdit; 21use text_edit::TextEdit;
22 22
23use crate::{diagnostics::Fix, references::rename::rename_with_semantics, FilePosition}; 23use crate::{
24 diagnostics::{fix, unresolved_fix},
25 references::rename::rename_with_semantics,
26 Assist, FilePosition,
27};
24 28
25/// A [Diagnostic] that potentially has a fix available. 29/// A [Diagnostic] that potentially has a fix available.
26/// 30///
27/// [Diagnostic]: hir::diagnostics::Diagnostic 31/// [Diagnostic]: hir::diagnostics::Diagnostic
28pub(crate) trait DiagnosticWithFix: Diagnostic { 32pub(crate) trait DiagnosticWithFix: Diagnostic {
29 fn fix(&self, sema: &Semantics<RootDatabase>) -> Option<Fix>; 33 /// `resolve` determines if the diagnostic should fill in the `edit` field
34 /// of the assist.
35 ///
36 /// If `resolve` is false, the edit will be computed later, on demand, and
37 /// can be omitted.
38 fn fix(&self, sema: &Semantics<RootDatabase>, _resolve: bool) -> Option<Assist>;
30} 39}
31 40
32impl DiagnosticWithFix for UnresolvedModule { 41impl DiagnosticWithFix for UnresolvedModule {
33 fn fix(&self, sema: &Semantics<RootDatabase>) -> Option<Fix> { 42 fn fix(&self, sema: &Semantics<RootDatabase>, _resolve: bool) -> Option<Assist> {
34 let root = sema.db.parse_or_expand(self.file)?; 43 let root = sema.db.parse_or_expand(self.file)?;
35 let unresolved_module = self.decl.to_node(&root); 44 let unresolved_module = self.decl.to_node(&root);
36 Some(Fix::new( 45 Some(fix(
46 "create_module",
37 "Create module", 47 "Create module",
38 FileSystemEdit::CreateFile { 48 FileSystemEdit::CreateFile {
39 dst: AnchoredPathBuf { 49 dst: AnchoredPathBuf {
@@ -49,7 +59,7 @@ impl DiagnosticWithFix for UnresolvedModule {
49} 59}
50 60
51impl DiagnosticWithFix for NoSuchField { 61impl DiagnosticWithFix for NoSuchField {
52 fn fix(&self, sema: &Semantics<RootDatabase>) -> Option<Fix> { 62 fn fix(&self, sema: &Semantics<RootDatabase>, _resolve: bool) -> Option<Assist> {
53 let root = sema.db.parse_or_expand(self.file)?; 63 let root = sema.db.parse_or_expand(self.file)?;
54 missing_record_expr_field_fix( 64 missing_record_expr_field_fix(
55 &sema, 65 &sema,
@@ -60,7 +70,7 @@ impl DiagnosticWithFix for NoSuchField {
60} 70}
61 71
62impl DiagnosticWithFix for MissingFields { 72impl DiagnosticWithFix for MissingFields {
63 fn fix(&self, sema: &Semantics<RootDatabase>) -> Option<Fix> { 73 fn fix(&self, sema: &Semantics<RootDatabase>, _resolve: bool) -> Option<Assist> {
64 // Note that although we could add a diagnostics to 74 // Note that although we could add a diagnostics to
65 // fill the missing tuple field, e.g : 75 // fill the missing tuple field, e.g :
66 // `struct A(usize);` 76 // `struct A(usize);`
@@ -86,7 +96,8 @@ impl DiagnosticWithFix for MissingFields {
86 .into_text_edit(&mut builder); 96 .into_text_edit(&mut builder);
87 builder.finish() 97 builder.finish()
88 }; 98 };
89 Some(Fix::new( 99 Some(fix(
100 "fill_missing_fields",
90 "Fill struct fields", 101 "Fill struct fields",
91 SourceChange::from_text_edit(self.file.original_file(sema.db), edit), 102 SourceChange::from_text_edit(self.file.original_file(sema.db), edit),
92 sema.original_range(&field_list_parent.syntax()).range, 103 sema.original_range(&field_list_parent.syntax()).range,
@@ -95,7 +106,7 @@ impl DiagnosticWithFix for MissingFields {
95} 106}
96 107
97impl DiagnosticWithFix for MissingOkOrSomeInTailExpr { 108impl DiagnosticWithFix for MissingOkOrSomeInTailExpr {
98 fn fix(&self, sema: &Semantics<RootDatabase>) -> Option<Fix> { 109 fn fix(&self, sema: &Semantics<RootDatabase>, _resolve: bool) -> Option<Assist> {
99 let root = sema.db.parse_or_expand(self.file)?; 110 let root = sema.db.parse_or_expand(self.file)?;
100 let tail_expr = self.expr.to_node(&root); 111 let tail_expr = self.expr.to_node(&root);
101 let tail_expr_range = tail_expr.syntax().text_range(); 112 let tail_expr_range = tail_expr.syntax().text_range();
@@ -103,12 +114,12 @@ impl DiagnosticWithFix for MissingOkOrSomeInTailExpr {
103 let edit = TextEdit::replace(tail_expr_range, replacement); 114 let edit = TextEdit::replace(tail_expr_range, replacement);
104 let source_change = SourceChange::from_text_edit(self.file.original_file(sema.db), edit); 115 let source_change = SourceChange::from_text_edit(self.file.original_file(sema.db), edit);
105 let name = if self.required == "Ok" { "Wrap with Ok" } else { "Wrap with Some" }; 116 let name = if self.required == "Ok" { "Wrap with Ok" } else { "Wrap with Some" };
106 Some(Fix::new(name, source_change, tail_expr_range)) 117 Some(fix("wrap_tail_expr", name, source_change, tail_expr_range))
107 } 118 }
108} 119}
109 120
110impl DiagnosticWithFix for RemoveThisSemicolon { 121impl DiagnosticWithFix for RemoveThisSemicolon {
111 fn fix(&self, sema: &Semantics<RootDatabase>) -> Option<Fix> { 122 fn fix(&self, sema: &Semantics<RootDatabase>, _resolve: bool) -> Option<Assist> {
112 let root = sema.db.parse_or_expand(self.file)?; 123 let root = sema.db.parse_or_expand(self.file)?;
113 124
114 let semicolon = self 125 let semicolon = self
@@ -123,12 +134,12 @@ impl DiagnosticWithFix for RemoveThisSemicolon {
123 let edit = TextEdit::delete(semicolon); 134 let edit = TextEdit::delete(semicolon);
124 let source_change = SourceChange::from_text_edit(self.file.original_file(sema.db), edit); 135 let source_change = SourceChange::from_text_edit(self.file.original_file(sema.db), edit);
125 136
126 Some(Fix::new("Remove this semicolon", source_change, semicolon)) 137 Some(fix("remove_semicolon", "Remove this semicolon", source_change, semicolon))
127 } 138 }
128} 139}
129 140
130impl DiagnosticWithFix for IncorrectCase { 141impl DiagnosticWithFix for IncorrectCase {
131 fn fix(&self, sema: &Semantics<RootDatabase>) -> Option<Fix> { 142 fn fix(&self, sema: &Semantics<RootDatabase>, resolve: bool) -> Option<Assist> {
132 let root = sema.db.parse_or_expand(self.file)?; 143 let root = sema.db.parse_or_expand(self.file)?;
133 let name_node = self.ident.to_node(&root); 144 let name_node = self.ident.to_node(&root);
134 145
@@ -136,16 +147,19 @@ impl DiagnosticWithFix for IncorrectCase {
136 let frange = name_node.original_file_range(sema.db); 147 let frange = name_node.original_file_range(sema.db);
137 let file_position = FilePosition { file_id: frange.file_id, offset: frange.range.start() }; 148 let file_position = FilePosition { file_id: frange.file_id, offset: frange.range.start() };
138 149
139 let rename_changes =
140 rename_with_semantics(sema, file_position, &self.suggested_text).ok()?;
141
142 let label = format!("Rename to {}", self.suggested_text); 150 let label = format!("Rename to {}", self.suggested_text);
143 Some(Fix::new(&label, rename_changes, frange.range)) 151 let mut res = unresolved_fix("change_case", &label, frange.range);
152 if resolve {
153 let source_change = rename_with_semantics(sema, file_position, &self.suggested_text);
154 res.source_change = Some(source_change.ok().unwrap_or_default());
155 }
156
157 Some(res)
144 } 158 }
145} 159}
146 160
147impl DiagnosticWithFix for ReplaceFilterMapNextWithFindMap { 161impl DiagnosticWithFix for ReplaceFilterMapNextWithFindMap {
148 fn fix(&self, sema: &Semantics<RootDatabase>) -> Option<Fix> { 162 fn fix(&self, sema: &Semantics<RootDatabase>, _resolve: bool) -> Option<Assist> {
149 let root = sema.db.parse_or_expand(self.file)?; 163 let root = sema.db.parse_or_expand(self.file)?;
150 let next_expr = self.next_expr.to_node(&root); 164 let next_expr = self.next_expr.to_node(&root);
151 let next_call = ast::MethodCallExpr::cast(next_expr.syntax().clone())?; 165 let next_call = ast::MethodCallExpr::cast(next_expr.syntax().clone())?;
@@ -163,7 +177,8 @@ impl DiagnosticWithFix for ReplaceFilterMapNextWithFindMap {
163 177
164 let source_change = SourceChange::from_text_edit(self.file.original_file(sema.db), edit); 178 let source_change = SourceChange::from_text_edit(self.file.original_file(sema.db), edit);
165 179
166 Some(Fix::new( 180 Some(fix(
181 "replace_with_find_map",
167 "Replace filter_map(..).next() with find_map()", 182 "Replace filter_map(..).next() with find_map()",
168 source_change, 183 source_change,
169 trigger_range, 184 trigger_range,
@@ -175,7 +190,7 @@ fn missing_record_expr_field_fix(
175 sema: &Semantics<RootDatabase>, 190 sema: &Semantics<RootDatabase>,
176 usage_file_id: FileId, 191 usage_file_id: FileId,
177 record_expr_field: &ast::RecordExprField, 192 record_expr_field: &ast::RecordExprField,
178) -> Option<Fix> { 193) -> Option<Assist> {
179 let record_lit = ast::RecordExpr::cast(record_expr_field.syntax().parent()?.parent()?)?; 194 let record_lit = ast::RecordExpr::cast(record_expr_field.syntax().parent()?.parent()?)?;
180 let def_id = sema.resolve_variant(record_lit)?; 195 let def_id = sema.resolve_variant(record_lit)?;
181 let module; 196 let module;
@@ -233,7 +248,12 @@ fn missing_record_expr_field_fix(
233 def_file_id, 248 def_file_id,
234 TextEdit::insert(last_field_syntax.text_range().end(), new_field), 249 TextEdit::insert(last_field_syntax.text_range().end(), new_field),
235 ); 250 );
236 return Some(Fix::new("Create field", source_change, record_expr_field.syntax().text_range())); 251 return Some(fix(
252 "create_field",
253 "Create field",
254 source_change,
255 record_expr_field.syntax().text_range(),
256 ));
237 257
238 fn record_field_list(field_def_list: ast::FieldList) -> Option<ast::RecordFieldList> { 258 fn record_field_list(field_def_list: ast::FieldList) -> Option<ast::RecordFieldList> {
239 match field_def_list { 259 match field_def_list {
diff --git a/crates/ide/src/diagnostics/unlinked_file.rs b/crates/ide/src/diagnostics/unlinked_file.rs
index e174fb767..7d39f4fbe 100644
--- a/crates/ide/src/diagnostics/unlinked_file.rs
+++ b/crates/ide/src/diagnostics/unlinked_file.rs
@@ -16,9 +16,10 @@ use syntax::{
16}; 16};
17use text_edit::TextEdit; 17use text_edit::TextEdit;
18 18
19use crate::Fix; 19use crate::{
20 20 diagnostics::{fix, fixes::DiagnosticWithFix},
21use super::fixes::DiagnosticWithFix; 21 Assist,
22};
22 23
23// Diagnostic: unlinked-file 24// Diagnostic: unlinked-file
24// 25//
@@ -49,7 +50,7 @@ impl Diagnostic for UnlinkedFile {
49} 50}
50 51
51impl DiagnosticWithFix for UnlinkedFile { 52impl DiagnosticWithFix for UnlinkedFile {
52 fn fix(&self, sema: &hir::Semantics<RootDatabase>) -> Option<Fix> { 53 fn fix(&self, sema: &hir::Semantics<RootDatabase>, _resolve: bool) -> Option<Assist> {
53 // If there's an existing module that could add a `mod` item to include the unlinked file, 54 // If there's an existing module that could add a `mod` item to include the unlinked file,
54 // suggest that as a fix. 55 // suggest that as a fix.
55 56
@@ -100,7 +101,7 @@ fn make_fix(
100 parent_file_id: FileId, 101 parent_file_id: FileId,
101 new_mod_name: &str, 102 new_mod_name: &str,
102 added_file_id: FileId, 103 added_file_id: FileId,
103) -> Option<Fix> { 104) -> Option<Assist> {
104 fn is_outline_mod(item: &ast::Item) -> bool { 105 fn is_outline_mod(item: &ast::Item) -> bool {
105 matches!(item, ast::Item::Module(m) if m.item_list().is_none()) 106 matches!(item, ast::Item::Module(m) if m.item_list().is_none())
106 } 107 }
@@ -152,7 +153,8 @@ fn make_fix(
152 153
153 let edit = builder.finish(); 154 let edit = builder.finish();
154 let trigger_range = db.parse(added_file_id).tree().syntax().text_range(); 155 let trigger_range = db.parse(added_file_id).tree().syntax().text_range();
155 Some(Fix::new( 156 Some(fix(
157 "add_mod_declaration",
156 &format!("Insert `{}`", mod_decl), 158 &format!("Insert `{}`", mod_decl),
157 SourceChange::from_text_edit(parent_file_id, edit), 159 SourceChange::from_text_edit(parent_file_id, edit),
158 trigger_range, 160 trigger_range,
diff --git a/crates/ide/src/goto_definition.rs b/crates/ide/src/goto_definition.rs
index d057d5402..a04333e63 100644
--- a/crates/ide/src/goto_definition.rs
+++ b/crates/ide/src/goto_definition.rs
@@ -110,6 +110,13 @@ mod tests {
110 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() });
111 } 111 }
112 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
113 #[test] 120 #[test]
114 fn goto_def_for_extern_crate() { 121 fn goto_def_for_extern_crate() {
115 check( 122 check(
@@ -927,17 +934,12 @@ fn f() -> impl Iterator<Item$0 = u8> {}
927 } 934 }
928 935
929 #[test] 936 #[test]
930 #[should_panic = "unresolved reference"]
931 fn unknown_assoc_ty() { 937 fn unknown_assoc_ty() {
932 check( 938 check_unresolved(
933 r#" 939 r#"
934trait Iterator { 940trait Iterator { type Item; }
935 type Item;
936 //^^^^
937}
938
939fn f() -> impl Iterator<Invalid$0 = u8> {} 941fn f() -> impl Iterator<Invalid$0 = u8> {}
940 "#, 942"#,
941 ) 943 )
942 } 944 }
943 945
diff --git a/crates/ide/src/lib.rs b/crates/ide/src/lib.rs
index 3f73c0632..b24c664ba 100644
--- a/crates/ide/src/lib.rs
+++ b/crates/ide/src/lib.rs
@@ -69,7 +69,7 @@ use crate::display::ToNav;
69pub use crate::{ 69pub use crate::{
70 annotations::{Annotation, AnnotationConfig, AnnotationKind}, 70 annotations::{Annotation, AnnotationConfig, AnnotationKind},
71 call_hierarchy::CallItem, 71 call_hierarchy::CallItem,
72 diagnostics::{Diagnostic, DiagnosticsConfig, Fix, Severity}, 72 diagnostics::{Diagnostic, DiagnosticsConfig, Severity},
73 display::navigation_target::NavigationTarget, 73 display::navigation_target::NavigationTarget,
74 expand_macro::ExpandedMacro, 74 expand_macro::ExpandedMacro,
75 file_structure::{StructureNode, StructureNodeKind}, 75 file_structure::{StructureNode, StructureNodeKind},
@@ -82,7 +82,7 @@ pub use crate::{
82 references::{rename::RenameError, ReferenceSearchResult}, 82 references::{rename::RenameError, ReferenceSearchResult},
83 runnables::{Runnable, RunnableKind, TestId}, 83 runnables::{Runnable, RunnableKind, TestId},
84 syntax_highlighting::{ 84 syntax_highlighting::{
85 tags::{Highlight, HlMod, HlMods, HlPunct, HlTag}, 85 tags::{Highlight, HlMod, HlMods, HlOperator, HlPunct, HlTag},
86 HlRange, 86 HlRange,
87 }, 87 },
88}; 88};
@@ -526,9 +526,39 @@ impl Analysis {
526 pub fn diagnostics( 526 pub fn diagnostics(
527 &self, 527 &self,
528 config: &DiagnosticsConfig, 528 config: &DiagnosticsConfig,
529 resolve: bool,
529 file_id: FileId, 530 file_id: FileId,
530 ) -> Cancelable<Vec<Diagnostic>> { 531 ) -> Cancelable<Vec<Diagnostic>> {
531 self.with_db(|db| diagnostics::diagnostics(db, config, file_id)) 532 self.with_db(|db| diagnostics::diagnostics(db, config, resolve, file_id))
533 }
534
535 /// Convenience function to return assists + quick fixes for diagnostics
536 pub fn assists_with_fixes(
537 &self,
538 assist_config: &AssistConfig,
539 diagnostics_config: &DiagnosticsConfig,
540 resolve: bool,
541 frange: FileRange,
542 ) -> Cancelable<Vec<Assist>> {
543 let include_fixes = match &assist_config.allowed {
544 Some(it) => it.iter().any(|&it| it == AssistKind::None || it == AssistKind::QuickFix),
545 None => true,
546 };
547
548 self.with_db(|db| {
549 let mut res = Assist::get(db, assist_config, resolve, frange);
550 ssr::add_ssr_assist(db, &mut res, resolve, frange);
551
552 if include_fixes {
553 res.extend(
554 diagnostics::diagnostics(db, diagnostics_config, resolve, frange.file_id)
555 .into_iter()
556 .filter_map(|it| it.fix)
557 .filter(|it| it.target.intersect(frange.range).is_some()),
558 );
559 }
560 res
561 })
532 } 562 }
533 563
534 /// Returns the edit required to rename reference at the position to the new 564 /// Returns the edit required to rename reference at the position to the new
diff --git a/crates/ide/src/move_item.rs b/crates/ide/src/move_item.rs
index 8d37f4f92..246f10a0a 100644
--- a/crates/ide/src/move_item.rs
+++ b/crates/ide/src/move_item.rs
@@ -1,4 +1,4 @@
1use std::iter::once; 1use std::{iter::once, mem};
2 2
3use hir::Semantics; 3use hir::Semantics;
4use ide_db::{base_db::FileRange, RootDatabase}; 4use ide_db::{base_db::FileRange, RootDatabase};
@@ -102,7 +102,7 @@ fn move_in_direction(
102 ast::GenericArgList(it) => swap_sibling_in_list(node, it.generic_args(), range, direction), 102 ast::GenericArgList(it) => swap_sibling_in_list(node, it.generic_args(), range, direction),
103 ast::VariantList(it) => swap_sibling_in_list(node, it.variants(), range, direction), 103 ast::VariantList(it) => swap_sibling_in_list(node, it.variants(), range, direction),
104 ast::TypeBoundList(it) => swap_sibling_in_list(node, it.bounds(), range, direction), 104 ast::TypeBoundList(it) => swap_sibling_in_list(node, it.bounds(), range, direction),
105 _ => Some(replace_nodes(node, &match direction { 105 _ => Some(replace_nodes(range, node, &match direction {
106 Direction::Up => node.prev_sibling(), 106 Direction::Up => node.prev_sibling(),
107 Direction::Down => node.next_sibling(), 107 Direction::Down => node.next_sibling(),
108 }?)) 108 }?))
@@ -125,7 +125,7 @@ fn swap_sibling_in_list<A: AstNode + Clone, I: Iterator<Item = A>>(
125 .next(); 125 .next();
126 126
127 if let Some((l, r)) = list_lookup { 127 if let Some((l, r)) = list_lookup {
128 Some(replace_nodes(l.syntax(), r.syntax())) 128 Some(replace_nodes(range, l.syntax(), r.syntax()))
129 } else { 129 } else {
130 // Cursor is beyond any movable list item (for example, on curly brace in enum). 130 // Cursor is beyond any movable list item (for example, on curly brace in enum).
131 // It's not necessary, that parent of list is movable (arg list's parent is not, for example), 131 // It's not necessary, that parent of list is movable (arg list's parent is not, for example),
@@ -134,11 +134,38 @@ fn swap_sibling_in_list<A: AstNode + Clone, I: Iterator<Item = A>>(
134 } 134 }
135} 135}
136 136
137fn replace_nodes(first: &SyntaxNode, second: &SyntaxNode) -> TextEdit { 137fn replace_nodes<'a>(
138 range: TextRange,
139 mut first: &'a SyntaxNode,
140 mut second: &'a SyntaxNode,
141) -> TextEdit {
142 let cursor_offset = if range.is_empty() {
143 // FIXME: `applySnippetTextEdits` does not support non-empty selection ranges
144 if first.text_range().contains_range(range) {
145 Some(range.start() - first.text_range().start())
146 } else if second.text_range().contains_range(range) {
147 mem::swap(&mut first, &mut second);
148 Some(range.start() - first.text_range().start())
149 } else {
150 None
151 }
152 } else {
153 None
154 };
155
156 let first_with_cursor = match cursor_offset {
157 Some(offset) => {
158 let mut item_text = first.text().to_string();
159 item_text.insert_str(offset.into(), "$0");
160 item_text
161 }
162 None => first.text().to_string(),
163 };
164
138 let mut edit = TextEditBuilder::default(); 165 let mut edit = TextEditBuilder::default();
139 166
140 algo::diff(first, second).into_text_edit(&mut edit); 167 algo::diff(first, second).into_text_edit(&mut edit);
141 algo::diff(second, first).into_text_edit(&mut edit); 168 edit.replace(second.text_range(), first_with_cursor);
142 169
143 edit.finish() 170 edit.finish()
144} 171}
@@ -188,7 +215,7 @@ fn main() {
188 expect![[r#" 215 expect![[r#"
189fn main() { 216fn main() {
190 match true { 217 match true {
191 false => { 218 false =>$0 {
192 println!("Test"); 219 println!("Test");
193 }, 220 },
194 true => { 221 true => {
@@ -222,7 +249,7 @@ fn main() {
222 false => { 249 false => {
223 println!("Test"); 250 println!("Test");
224 }, 251 },
225 true => { 252 true =>$0 {
226 println!("Hello, world"); 253 println!("Hello, world");
227 } 254 }
228 }; 255 };
@@ -274,7 +301,7 @@ fn main() {
274 "#, 301 "#,
275 expect![[r#" 302 expect![[r#"
276fn main() { 303fn main() {
277 let test2 = 456; 304 let test2$0 = 456;
278 let test = 123; 305 let test = 123;
279} 306}
280 "#]], 307 "#]],
@@ -293,7 +320,7 @@ fn main() {
293 "#, 320 "#,
294 expect![[r#" 321 expect![[r#"
295fn main() { 322fn main() {
296 println!("All I want to say is..."); 323 println!("All I want to say is...");$0
297 println!("Hello, world"); 324 println!("Hello, world");
298} 325}
299 "#]], 326 "#]],
@@ -313,7 +340,7 @@ fn main() {
313fn main() { 340fn main() {
314 if true { 341 if true {
315 println!("Test"); 342 println!("Test");
316 } 343 }$0
317 344
318 println!("Hello, world"); 345 println!("Hello, world");
319} 346}
@@ -334,7 +361,7 @@ fn main() {
334fn main() { 361fn main() {
335 for i in 0..10 { 362 for i in 0..10 {
336 println!("Test"); 363 println!("Test");
337 } 364 }$0
338 365
339 println!("Hello, world"); 366 println!("Hello, world");
340} 367}
@@ -355,7 +382,7 @@ fn main() {
355fn main() { 382fn main() {
356 loop { 383 loop {
357 println!("Test"); 384 println!("Test");
358 } 385 }$0
359 386
360 println!("Hello, world"); 387 println!("Hello, world");
361} 388}
@@ -376,7 +403,7 @@ fn main() {
376fn main() { 403fn main() {
377 while true { 404 while true {
378 println!("Test"); 405 println!("Test");
379 } 406 }$0
380 407
381 println!("Hello, world"); 408 println!("Hello, world");
382} 409}
@@ -393,7 +420,7 @@ fn main() {
393 "#, 420 "#,
394 expect![[r#" 421 expect![[r#"
395fn main() { 422fn main() {
396 return 123; 423 return 123;$0
397 424
398 println!("Hello, world"); 425 println!("Hello, world");
399} 426}
@@ -430,7 +457,7 @@ fn main() {}
430fn foo() {}$0$0 457fn foo() {}$0$0
431 "#, 458 "#,
432 expect![[r#" 459 expect![[r#"
433fn foo() {} 460fn foo() {}$0
434 461
435fn main() {} 462fn main() {}
436 "#]], 463 "#]],
@@ -451,7 +478,7 @@ impl Wow for Yay $0$0{}
451 expect![[r#" 478 expect![[r#"
452struct Yay; 479struct Yay;
453 480
454impl Wow for Yay {} 481impl Wow for Yay $0{}
455 482
456trait Wow {} 483trait Wow {}
457 "#]], 484 "#]],
@@ -467,7 +494,7 @@ use std::vec::Vec;
467use std::collections::HashMap$0$0; 494use std::collections::HashMap$0$0;
468 "#, 495 "#,
469 expect![[r#" 496 expect![[r#"
470use std::collections::HashMap; 497use std::collections::HashMap$0;
471use std::vec::Vec; 498use std::vec::Vec;
472 "#]], 499 "#]],
473 Direction::Up, 500 Direction::Up,
@@ -502,7 +529,7 @@ fn main() {
502 } 529 }
503 530
504 #[test] 531 #[test]
505 fn test_moves_param_up() { 532 fn test_moves_param() {
506 check( 533 check(
507 r#" 534 r#"
508fn test(one: i32, two$0$0: u32) {} 535fn test(one: i32, two$0$0: u32) {}
@@ -512,7 +539,7 @@ fn main() {
512} 539}
513 "#, 540 "#,
514 expect![[r#" 541 expect![[r#"
515fn test(two: u32, one: i32) {} 542fn test(two$0: u32, one: i32) {}
516 543
517fn main() { 544fn main() {
518 test(123, 456); 545 test(123, 456);
@@ -520,6 +547,15 @@ fn main() {
520 "#]], 547 "#]],
521 Direction::Up, 548 Direction::Up,
522 ); 549 );
550 check(
551 r#"
552fn f($0$0arg: u8, arg2: u16) {}
553 "#,
554 expect![[r#"
555fn f(arg2: u16, $0arg: u8) {}
556 "#]],
557 Direction::Down,
558 );
523 } 559 }
524 560
525 #[test] 561 #[test]
@@ -536,7 +572,7 @@ fn main() {
536fn test(one: i32, two: u32) {} 572fn test(one: i32, two: u32) {}
537 573
538fn main() { 574fn main() {
539 test(456, 123); 575 test(456$0, 123);
540} 576}
541 "#]], 577 "#]],
542 Direction::Up, 578 Direction::Up,
@@ -557,7 +593,7 @@ fn main() {
557fn test(one: i32, two: u32) {} 593fn test(one: i32, two: u32) {}
558 594
559fn main() { 595fn main() {
560 test(456, 123); 596 test(456, 123$0);
561} 597}
562 "#]], 598 "#]],
563 Direction::Down, 599 Direction::Down,
@@ -594,7 +630,7 @@ struct Test<A, B$0$0>(A, B);
594fn main() {} 630fn main() {}
595 "#, 631 "#,
596 expect![[r#" 632 expect![[r#"
597struct Test<B, A>(A, B); 633struct Test<B$0, A>(A, B);
598 634
599fn main() {} 635fn main() {}
600 "#]], 636 "#]],
@@ -616,7 +652,7 @@ fn main() {
616struct Test<A, B>(A, B); 652struct Test<A, B>(A, B);
617 653
618fn main() { 654fn main() {
619 let t = Test::<&str, i32>(123, "yay"); 655 let t = Test::<&str$0, i32>(123, "yay");
620} 656}
621 "#]], 657 "#]],
622 Direction::Up, 658 Direction::Up,
@@ -636,7 +672,7 @@ fn main() {}
636 "#, 672 "#,
637 expect![[r#" 673 expect![[r#"
638enum Hello { 674enum Hello {
639 Two, 675 Two$0,
640 One 676 One
641} 677}
642 678
@@ -663,7 +699,7 @@ trait One {}
663 699
664trait Two {} 700trait Two {}
665 701
666fn test<T: Two + One>(t: T) {} 702fn test<T: Two$0 + One>(t: T) {}
667 703
668fn main() {} 704fn main() {}
669 "#]], 705 "#]],
@@ -709,7 +745,7 @@ trait Yay {
709impl Yay for Test { 745impl Yay for Test {
710 type One = i32; 746 type One = i32;
711 747
712 fn inner() { 748 fn inner() {$0
713 println!("Mmmm"); 749 println!("Mmmm");
714 } 750 }
715 751
@@ -736,7 +772,7 @@ fn test() {
736 "#, 772 "#,
737 expect![[r#" 773 expect![[r#"
738fn test() { 774fn test() {
739 mod hi { 775 mod hi {$0
740 fn inner() {} 776 fn inner() {}
741 } 777 }
742 778
@@ -764,7 +800,7 @@ fn main() {}
764 expect![[r#" 800 expect![[r#"
765fn main() {} 801fn main() {}
766 802
767#[derive(Debug)] 803$0#[derive(Debug)]
768enum FooBar { 804enum FooBar {
769 Foo, 805 Foo,
770 Bar, 806 Bar,
@@ -784,7 +820,7 @@ fn main() {}
784 expect![[r#" 820 expect![[r#"
785fn main() {} 821fn main() {}
786 822
787enum FooBar { 823$0enum FooBar {
788 Foo, 824 Foo,
789 Bar, 825 Bar,
790} 826}
@@ -804,7 +840,7 @@ fn main() {}
804 expect![[r#" 840 expect![[r#"
805struct Test; 841struct Test;
806 842
807impl SomeTrait for Test {} 843$0impl SomeTrait for Test {}
808 844
809trait SomeTrait {} 845trait SomeTrait {}
810 846
@@ -831,7 +867,7 @@ fn main() {}
831enum FooBar { 867enum FooBar {
832 Foo, 868 Foo,
833 Bar, 869 Bar,
834} 870}$0
835 "#]], 871 "#]],
836 Direction::Down, 872 Direction::Down,
837 ); 873 );
@@ -848,7 +884,7 @@ fn main() {}
848 expect![[r#" 884 expect![[r#"
849struct Test; 885struct Test;
850 886
851impl SomeTrait for Test {} 887impl SomeTrait for Test {}$0
852 888
853trait SomeTrait {} 889trait SomeTrait {}
854 890
diff --git a/crates/ide/src/syntax_highlighting/highlight.rs b/crates/ide/src/syntax_highlighting/highlight.rs
index e921784bf..8cc877c1c 100644
--- a/crates/ide/src/syntax_highlighting/highlight.rs
+++ b/crates/ide/src/syntax_highlighting/highlight.rs
@@ -12,7 +12,10 @@ use syntax::{
12 SyntaxNode, SyntaxToken, T, 12 SyntaxNode, SyntaxToken, T,
13}; 13};
14 14
15use crate::{syntax_highlighting::tags::HlPunct, Highlight, HlMod, HlTag}; 15use crate::{
16 syntax_highlighting::tags::{HlOperator, HlPunct},
17 Highlight, HlMod, HlTag,
18};
16 19
17pub(super) fn element( 20pub(super) fn element(
18 sema: &Semantics<RootDatabase>, 21 sema: &Semantics<RootDatabase>,
@@ -132,7 +135,7 @@ pub(super) fn element(
132 INT_NUMBER | FLOAT_NUMBER => HlTag::NumericLiteral.into(), 135 INT_NUMBER | FLOAT_NUMBER => HlTag::NumericLiteral.into(),
133 BYTE => HlTag::ByteLiteral.into(), 136 BYTE => HlTag::ByteLiteral.into(),
134 CHAR => HlTag::CharLiteral.into(), 137 CHAR => HlTag::CharLiteral.into(),
135 QUESTION => Highlight::new(HlTag::Operator) | HlMod::ControlFlow, 138 QUESTION => Highlight::new(HlTag::Operator(HlOperator::Other)) | HlMod::ControlFlow,
136 LIFETIME => { 139 LIFETIME => {
137 let lifetime = element.into_node().and_then(ast::Lifetime::cast).unwrap(); 140 let lifetime = element.into_node().and_then(ast::Lifetime::cast).unwrap();
138 141
@@ -146,8 +149,11 @@ pub(super) fn element(
146 } 149 }
147 } 150 }
148 p if p.is_punct() => match p { 151 p if p.is_punct() => match p {
152 T![&] if element.parent().and_then(ast::BinExpr::cast).is_some() => {
153 HlTag::Operator(HlOperator::Bitwise).into()
154 }
149 T![&] => { 155 T![&] => {
150 let h = HlTag::Operator.into(); 156 let h = HlTag::Operator(HlOperator::Other).into();
151 let is_unsafe = element 157 let is_unsafe = element
152 .parent() 158 .parent()
153 .and_then(ast::RefExpr::cast) 159 .and_then(ast::RefExpr::cast)
@@ -159,13 +165,18 @@ pub(super) fn element(
159 h 165 h
160 } 166 }
161 } 167 }
162 T![::] | T![->] | T![=>] | T![..] | T![=] | T![@] | T![.] => HlTag::Operator.into(), 168 T![::] | T![->] | T![=>] | T![..] | T![=] | T![@] | T![.] => {
169 HlTag::Operator(HlOperator::Other).into()
170 }
163 T![!] if element.parent().and_then(ast::MacroCall::cast).is_some() => { 171 T![!] if element.parent().and_then(ast::MacroCall::cast).is_some() => {
164 HlTag::Symbol(SymbolKind::Macro).into() 172 HlTag::Symbol(SymbolKind::Macro).into()
165 } 173 }
166 T![!] if element.parent().and_then(ast::NeverType::cast).is_some() => { 174 T![!] if element.parent().and_then(ast::NeverType::cast).is_some() => {
167 HlTag::BuiltinType.into() 175 HlTag::BuiltinType.into()
168 } 176 }
177 T![!] if element.parent().and_then(ast::PrefixExpr::cast).is_some() => {
178 HlTag::Operator(HlOperator::Logical).into()
179 }
169 T![*] if element.parent().and_then(ast::PtrType::cast).is_some() => { 180 T![*] if element.parent().and_then(ast::PtrType::cast).is_some() => {
170 HlTag::Keyword.into() 181 HlTag::Keyword.into()
171 } 182 }
@@ -175,9 +186,9 @@ pub(super) fn element(
175 let expr = prefix_expr.expr()?; 186 let expr = prefix_expr.expr()?;
176 let ty = sema.type_of_expr(&expr)?; 187 let ty = sema.type_of_expr(&expr)?;
177 if ty.is_raw_ptr() { 188 if ty.is_raw_ptr() {
178 HlTag::Operator | HlMod::Unsafe 189 HlTag::Operator(HlOperator::Other) | HlMod::Unsafe
179 } else if let Some(ast::PrefixOp::Deref) = prefix_expr.op_kind() { 190 } else if let Some(ast::PrefixOp::Deref) = prefix_expr.op_kind() {
180 HlTag::Operator.into() 191 HlTag::Operator(HlOperator::Other).into()
181 } else { 192 } else {
182 HlTag::Punctuation(HlPunct::Other).into() 193 HlTag::Punctuation(HlPunct::Other).into()
183 } 194 }
@@ -188,19 +199,43 @@ pub(super) fn element(
188 let expr = prefix_expr.expr()?; 199 let expr = prefix_expr.expr()?;
189 match expr { 200 match expr {
190 ast::Expr::Literal(_) => HlTag::NumericLiteral, 201 ast::Expr::Literal(_) => HlTag::NumericLiteral,
191 _ => HlTag::Operator, 202 _ => HlTag::Operator(HlOperator::Other),
192 } 203 }
193 .into() 204 .into()
194 } 205 }
195 _ if element.parent().and_then(ast::PrefixExpr::cast).is_some() => { 206 _ if element.parent().and_then(ast::PrefixExpr::cast).is_some() => {
196 HlTag::Operator.into() 207 HlTag::Operator(HlOperator::Other).into()
208 }
209 T![+] | T![-] | T![*] | T![/] | T![+=] | T![-=] | T![*=] | T![/=]
210 if element.parent().and_then(ast::BinExpr::cast).is_some() =>
211 {
212 HlTag::Operator(HlOperator::Arithmetic).into()
213 }
214 T![|] | T![&] | T![!] | T![^] | T![|=] | T![&=] | T![^=]
215 if element.parent().and_then(ast::BinExpr::cast).is_some() =>
216 {
217 HlTag::Operator(HlOperator::Bitwise).into()
218 }
219 T![&&] | T![||] if element.parent().and_then(ast::BinExpr::cast).is_some() => {
220 HlTag::Operator(HlOperator::Logical).into()
221 }
222 T![>] | T![<] | T![==] | T![>=] | T![<=] | T![!=]
223 if element.parent().and_then(ast::BinExpr::cast).is_some() =>
224 {
225 HlTag::Operator(HlOperator::Comparision).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,
diff --git a/crates/ide/src/syntax_highlighting/inject.rs b/crates/ide/src/syntax_highlighting/inject.rs
index 04fafd244..855c7fba8 100644
--- a/crates/ide/src/syntax_highlighting/inject.rs
+++ b/crates/ide/src/syntax_highlighting/inject.rs
@@ -90,6 +90,13 @@ const RUSTDOC_FENCE_TOKENS: &[&'static str] = &[
90 "edition2021", 90 "edition2021",
91]; 91];
92 92
93fn is_rustdoc_fence_token(token: &str) -> bool {
94 if RUSTDOC_FENCE_TOKENS.contains(&token) {
95 return true;
96 }
97 token.starts_with('E') && token.len() == 5 && token[1..].parse::<u32>().is_ok()
98}
99
93/// Injection of syntax highlighting of doctests. 100/// Injection of syntax highlighting of doctests.
94pub(super) fn doc_comment( 101pub(super) fn doc_comment(
95 hl: &mut Highlights, 102 hl: &mut Highlights,
@@ -174,8 +181,7 @@ pub(super) fn doc_comment(
174 is_codeblock = !is_codeblock; 181 is_codeblock = !is_codeblock;
175 // Check whether code is rust by inspecting fence guards 182 // Check whether code is rust by inspecting fence guards
176 let guards = &line[idx + RUSTDOC_FENCE.len()..]; 183 let guards = &line[idx + RUSTDOC_FENCE.len()..];
177 let is_rust = 184 let is_rust = guards.split(',').all(|sub| is_rustdoc_fence_token(sub.trim()));
178 guards.split(',').all(|sub| RUSTDOC_FENCE_TOKENS.contains(&sub.trim()));
179 is_doctest = is_codeblock && is_rust; 185 is_doctest = is_codeblock && is_rust;
180 continue; 186 continue;
181 } 187 }
diff --git a/crates/ide/src/syntax_highlighting/tags.rs b/crates/ide/src/syntax_highlighting/tags.rs
index 1cec991aa..8128d231d 100644
--- a/crates/ide/src/syntax_highlighting/tags.rs
+++ b/crates/ide/src/syntax_highlighting/tags.rs
@@ -28,7 +28,7 @@ pub enum HlTag {
28 FormatSpecifier, 28 FormatSpecifier,
29 Keyword, 29 Keyword,
30 NumericLiteral, 30 NumericLiteral,
31 Operator, 31 Operator(HlOperator),
32 Punctuation(HlPunct), 32 Punctuation(HlPunct),
33 StringLiteral, 33 StringLiteral,
34 UnresolvedReference, 34 UnresolvedReference,
@@ -87,6 +87,20 @@ pub enum HlPunct {
87 Other, 87 Other,
88} 88}
89 89
90#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
91pub enum HlOperator {
92 /// |, &, !, ^, |=, &=, ^=
93 Bitwise,
94 /// +, -, *, /, +=, -=, *=, /=
95 Arithmetic,
96 /// &&, ||, !
97 Logical,
98 /// >, <, ==, >=, <=, !=
99 Comparision,
100 ///
101 Other,
102}
103
90impl HlTag { 104impl HlTag {
91 fn as_str(self) -> &'static str { 105 fn as_str(self) -> &'static str {
92 match self { 106 match self {
@@ -133,7 +147,13 @@ impl HlTag {
133 HlPunct::Other => "punctuation", 147 HlPunct::Other => "punctuation",
134 }, 148 },
135 HlTag::NumericLiteral => "numeric_literal", 149 HlTag::NumericLiteral => "numeric_literal",
136 HlTag::Operator => "operator", 150 HlTag::Operator(op) => match op {
151 HlOperator::Bitwise => "bitwise",
152 HlOperator::Arithmetic => "arithmetic",
153 HlOperator::Logical => "logical",
154 HlOperator::Comparision => "comparision",
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 b6d1cac4e..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>
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 de2d22ac7..933cfa6f3 100644
--- a/crates/ide/src/syntax_highlighting/tests.rs
+++ b/crates/ide/src/syntax_highlighting/tests.rs
@@ -2,8 +2,7 @@ use std::time::Instant;
2 2
3use expect_test::{expect_file, ExpectFile}; 3use expect_test::{expect_file, ExpectFile};
4use ide_db::SymbolKind; 4use ide_db::SymbolKind;
5use stdx::format_to; 5use test_utils::{bench, bench_fixture, skip_slow_tests, AssertLinear};
6use test_utils::{bench, bench_fixture, skip_slow_tests};
7 6
8use crate::{fixture, FileRange, HlTag, TextRange}; 7use crate::{fixture, FileRange, HlTag, TextRange};
9 8
@@ -266,90 +265,27 @@ fn syntax_highlighting_not_quadratic() {
266 return; 265 return;
267 } 266 }
268 267
269 let mut measures = Vec::new(); 268 let mut al = AssertLinear::default();
270 for i in 6..=10 { 269 while al.next_round() {
271 let n = 1 << i; 270 for i in 6..=10 {
272 let fixture = bench_fixture::big_struct_n(n); 271 let n = 1 << i;
273 let (analysis, file_id) = fixture::file(&fixture);
274 272
275 let time = Instant::now(); 273 let fixture = bench_fixture::big_struct_n(n);
274 let (analysis, file_id) = fixture::file(&fixture);
276 275
277 let hash = analysis 276 let time = Instant::now();
278 .highlight(file_id)
279 .unwrap()
280 .iter()
281 .filter(|it| it.highlight.tag == HlTag::Symbol(SymbolKind::Struct))
282 .count();
283 assert!(hash > n as usize);
284 277
285 let elapsed = time.elapsed(); 278 let hash = analysis
286 measures.push((n as f64, elapsed.as_millis() as f64)) 279 .highlight(file_id)
287 } 280 .unwrap()
281 .iter()
282 .filter(|it| it.highlight.tag == HlTag::Symbol(SymbolKind::Struct))
283 .count();
284 assert!(hash > n as usize);
288 285
289 assert_linear(&measures) 286 let elapsed = time.elapsed();
290} 287 al.sample(n as f64, elapsed.as_millis() as f64);
291
292/// Checks that a set of measurements looks like a liner function rather than
293/// like a quadratic function. Algorithm:
294///
295/// 1. Linearly scale input to be in [0; 1)
296/// 2. Using linear regression, compute the best linear function approximating
297/// the input.
298/// 3. Compute RMSE and maximal absolute error.
299/// 4. Check that errors are within tolerances and that the constant term is not
300/// too negative.
301///
302/// Ideally, we should use a proper "model selection" to directly compare
303/// quadratic and linear models, but that sounds rather complicated:
304///
305/// https://stats.stackexchange.com/questions/21844/selecting-best-model-based-on-linear-quadratic-and-cubic-fit-of-data
306fn assert_linear(xy: &[(f64, f64)]) {
307 let (mut xs, mut ys): (Vec<_>, Vec<_>) = xy.iter().copied().unzip();
308 normalize(&mut xs);
309 normalize(&mut ys);
310 let xy = xs.iter().copied().zip(ys.iter().copied());
311
312 // Linear regression: finding a and b to fit y = a + b*x.
313
314 let mean_x = mean(&xs);
315 let mean_y = mean(&ys);
316
317 let b = {
318 let mut num = 0.0;
319 let mut denom = 0.0;
320 for (x, y) in xy.clone() {
321 num += (x - mean_x) * (y - mean_y);
322 denom += (x - mean_x).powi(2);
323 } 288 }
324 num / denom
325 };
326
327 let a = mean_y - b * mean_x;
328
329 let mut plot = format!("y_pred = {:.3} + {:.3} * x\n\nx y y_pred\n", a, b);
330
331 let mut se = 0.0;
332 let mut max_error = 0.0f64;
333 for (x, y) in xy {
334 let y_pred = a + b * x;
335 se += (y - y_pred).powi(2);
336 max_error = max_error.max((y_pred - y).abs());
337
338 format_to!(plot, "{:.3} {:.3} {:.3}\n", x, y, y_pred);
339 }
340
341 let rmse = (se / xs.len() as f64).sqrt();
342 format_to!(plot, "\nrmse = {:.3} max error = {:.3}", rmse, max_error);
343
344 assert!(rmse < 0.05 && max_error < 0.1 && a > -0.1, "\nLooks quadratic\n{}", plot);
345
346 fn normalize(xs: &mut Vec<f64>) {
347 let max = xs.iter().copied().max_by(|a, b| a.partial_cmp(b).unwrap()).unwrap();
348 xs.iter_mut().for_each(|it| *it /= max);
349 }
350
351 fn mean(xs: &[f64]) -> f64 {
352 xs.iter().copied().sum::<f64>() / (xs.len() as f64)
353 } 289 }
354} 290}
355 291
diff --git a/crates/ide/src/typing.rs b/crates/ide/src/typing.rs
index 1378048e5..82c732390 100644
--- a/crates/ide/src/typing.rs
+++ b/crates/ide/src/typing.rs
@@ -222,8 +222,8 @@ mod tests {
222 assert_eq_text!(ra_fixture_after, &actual); 222 assert_eq_text!(ra_fixture_after, &actual);
223 } 223 }
224 224
225 fn type_char_noop(char_typed: char, before: &str) { 225 fn type_char_noop(char_typed: char, ra_fixture_before: &str) {
226 let file_change = do_type_char(char_typed, before); 226 let file_change = do_type_char(char_typed, ra_fixture_before);
227 assert!(file_change.is_none()) 227 assert!(file_change.is_none())
228 } 228 }
229 229
@@ -240,16 +240,16 @@ mod tests {
240 // "); 240 // ");
241 type_char( 241 type_char(
242 '=', 242 '=',
243 r" 243 r#"
244fn foo() { 244fn foo() {
245 let foo $0 1 + 1 245 let foo $0 1 + 1
246} 246}
247", 247"#,
248 r" 248 r#"
249fn foo() { 249fn foo() {
250 let foo = 1 + 1; 250 let foo = 1 + 1;
251} 251}
252", 252"#,
253 ); 253 );
254 // do_check(r" 254 // do_check(r"
255 // fn foo() { 255 // fn foo() {
@@ -268,27 +268,27 @@ fn foo() {
268 fn indents_new_chain_call() { 268 fn indents_new_chain_call() {
269 type_char( 269 type_char(
270 '.', 270 '.',
271 r" 271 r#"
272 fn main() { 272fn main() {
273 xs.foo() 273 xs.foo()
274 $0 274 $0
275 } 275}
276 ", 276 "#,
277 r" 277 r#"
278 fn main() { 278fn main() {
279 xs.foo() 279 xs.foo()
280 . 280 .
281 } 281}
282 ", 282 "#,
283 ); 283 );
284 type_char_noop( 284 type_char_noop(
285 '.', 285 '.',
286 r" 286 r#"
287 fn main() { 287fn main() {
288 xs.foo() 288 xs.foo()
289 $0 289 $0
290 } 290}
291 ", 291 "#,
292 ) 292 )
293 } 293 }
294 294
@@ -297,26 +297,26 @@ fn foo() {
297 type_char( 297 type_char(
298 '.', 298 '.',
299 r" 299 r"
300 fn main() { 300fn main() {
301 xs.foo() 301 xs.foo()
302 $0; 302 $0;
303 } 303}
304 ",
305 r"
306 fn main() {
307 xs.foo()
308 .;
309 }
310 ", 304 ",
305 r#"
306fn main() {
307 xs.foo()
308 .;
309}
310 "#,
311 ); 311 );
312 type_char_noop( 312 type_char_noop(
313 '.', 313 '.',
314 r" 314 r#"
315 fn main() { 315fn main() {
316 xs.foo() 316 xs.foo()
317 $0; 317 $0;
318 } 318}
319 ", 319 "#,
320 ) 320 )
321 } 321 }
322 322
@@ -345,30 +345,30 @@ fn main() {
345 fn indents_continued_chain_call() { 345 fn indents_continued_chain_call() {
346 type_char( 346 type_char(
347 '.', 347 '.',
348 r" 348 r#"
349 fn main() { 349fn main() {
350 xs.foo() 350 xs.foo()
351 .first() 351 .first()
352 $0 352 $0
353 } 353}
354 ", 354 "#,
355 r" 355 r#"
356 fn main() { 356fn main() {
357 xs.foo() 357 xs.foo()
358 .first() 358 .first()
359 . 359 .
360 } 360}
361 ", 361 "#,
362 ); 362 );
363 type_char_noop( 363 type_char_noop(
364 '.', 364 '.',
365 r" 365 r#"
366 fn main() { 366fn main() {
367 xs.foo() 367 xs.foo()
368 .first() 368 .first()
369 $0 369 $0
370 } 370}
371 ", 371 "#,
372 ); 372 );
373 } 373 }
374 374
@@ -376,33 +376,33 @@ fn main() {
376 fn indents_middle_of_chain_call() { 376 fn indents_middle_of_chain_call() {
377 type_char( 377 type_char(
378 '.', 378 '.',
379 r" 379 r#"
380 fn source_impl() { 380fn source_impl() {
381 let var = enum_defvariant_list().unwrap() 381 let var = enum_defvariant_list().unwrap()
382 $0 382 $0
383 .nth(92) 383 .nth(92)
384 .unwrap(); 384 .unwrap();
385 } 385}
386 ", 386 "#,
387 r" 387 r#"
388 fn source_impl() { 388fn source_impl() {
389 let var = enum_defvariant_list().unwrap() 389 let var = enum_defvariant_list().unwrap()
390 . 390 .
391 .nth(92) 391 .nth(92)
392 .unwrap(); 392 .unwrap();
393 } 393}
394 ", 394 "#,
395 ); 395 );
396 type_char_noop( 396 type_char_noop(
397 '.', 397 '.',
398 r" 398 r#"
399 fn source_impl() { 399fn source_impl() {
400 let var = enum_defvariant_list().unwrap() 400 let var = enum_defvariant_list().unwrap()
401 $0 401 $0
402 .nth(92) 402 .nth(92)
403 .unwrap(); 403 .unwrap();
404 } 404}
405 ", 405 "#,
406 ); 406 );
407 } 407 }
408 408
@@ -410,25 +410,33 @@ fn main() {
410 fn dont_indent_freestanding_dot() { 410 fn dont_indent_freestanding_dot() {
411 type_char_noop( 411 type_char_noop(
412 '.', 412 '.',
413 r" 413 r#"
414 fn main() { 414fn main() {
415 $0 415 $0
416 } 416}
417 ", 417 "#,
418 ); 418 );
419 type_char_noop( 419 type_char_noop(
420 '.', 420 '.',
421 r" 421 r#"
422 fn main() { 422fn main() {
423 $0 423$0
424 } 424}
425 ", 425 "#,
426 ); 426 );
427 } 427 }
428 428
429 #[test] 429 #[test]
430 fn adds_space_after_return_type() { 430 fn adds_space_after_return_type() {
431 type_char('>', "fn foo() -$0{ 92 }", "fn foo() -> { 92 }") 431 type_char(
432 '>',
433 r#"
434fn foo() -$0{ 92 }
435"#,
436 r#"
437fn foo() -> { 92 }
438"#,
439 );
432 } 440 }
433 441
434 #[test] 442 #[test]
diff --git a/crates/ide/src/typing/on_enter.rs b/crates/ide/src/typing/on_enter.rs
index 6f1ce3689..7d2db201a 100644
--- a/crates/ide/src/typing/on_enter.rs
+++ b/crates/ide/src/typing/on_enter.rs
@@ -4,10 +4,11 @@
4use ide_db::base_db::{FilePosition, SourceDatabase}; 4use ide_db::base_db::{FilePosition, SourceDatabase};
5use ide_db::RootDatabase; 5use ide_db::RootDatabase;
6use syntax::{ 6use syntax::{
7 ast::{self, AstToken}, 7 algo::find_node_at_offset,
8 ast::{self, edit::IndentLevel, AstToken},
8 AstNode, SmolStr, SourceFile, 9 AstNode, SmolStr, SourceFile,
9 SyntaxKind::*, 10 SyntaxKind::*,
10 SyntaxToken, TextRange, TextSize, TokenAtOffset, 11 SyntaxNode, SyntaxToken, TextRange, TextSize, TokenAtOffset,
11}; 12};
12 13
13use text_edit::TextEdit; 14use text_edit::TextEdit;
@@ -19,6 +20,7 @@ use text_edit::TextEdit;
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 `//`
21// - kbd:[Enter] inside `//!` doc comments automatically inserts `//!` 22// - kbd:[Enter] inside `//!` doc comments automatically inserts `//!`
23// - kbd:[Enter] after `{` indents contents and closing `}` of single-line block
22// 24//
23// This action needs to be assigned to shortcut explicitly. 25// This action needs to be assigned to shortcut explicitly.
24// 26//
@@ -38,25 +40,43 @@ use text_edit::TextEdit;
38pub(crate) fn on_enter(db: &RootDatabase, position: FilePosition) -> Option<TextEdit> { 40pub(crate) fn on_enter(db: &RootDatabase, position: FilePosition) -> Option<TextEdit> {
39 let parse = db.parse(position.file_id); 41 let parse = db.parse(position.file_id);
40 let file = parse.tree(); 42 let file = parse.tree();
41 let comment = file 43 let token = file.syntax().token_at_offset(position.offset).left_biased()?;
42 .syntax()
43 .token_at_offset(position.offset)
44 .left_biased()
45 .and_then(ast::Comment::cast)?;
46 44
45 if let Some(comment) = ast::Comment::cast(token.clone()) {
46 return on_enter_in_comment(&comment, &file, position.offset);
47 }
48
49 if token.kind() == L_CURLY {
50 // Typing enter after the `{` of a block expression, where the `}` is on the same line
51 if let Some(edit) = find_node_at_offset(file.syntax(), position.offset - TextSize::of('{'))
52 .and_then(|block| on_enter_in_block(block, position))
53 {
54 cov_mark::hit!(indent_block_contents);
55 return Some(edit);
56 }
57 }
58
59 None
60}
61
62fn on_enter_in_comment(
63 comment: &ast::Comment,
64 file: &ast::SourceFile,
65 offset: TextSize,
66) -> Option<TextEdit> {
47 if comment.kind().shape.is_block() { 67 if comment.kind().shape.is_block() {
48 return None; 68 return None;
49 } 69 }
50 70
51 let prefix = comment.prefix(); 71 let prefix = comment.prefix();
52 let comment_range = comment.syntax().text_range(); 72 let comment_range = comment.syntax().text_range();
53 if position.offset < comment_range.start() + TextSize::of(prefix) { 73 if offset < comment_range.start() + TextSize::of(prefix) {
54 return None; 74 return None;
55 } 75 }
56 76
57 let mut remove_trailing_whitespace = false; 77 let mut remove_trailing_whitespace = false;
58 // Continuing single-line non-doc comments (like this one :) ) is annoying 78 // Continuing single-line non-doc comments (like this one :) ) is annoying
59 if prefix == "//" && comment_range.end() == position.offset { 79 if prefix == "//" && comment_range.end() == offset {
60 if comment.text().ends_with(' ') { 80 if comment.text().ends_with(' ') {
61 cov_mark::hit!(continues_end_of_line_comment_with_space); 81 cov_mark::hit!(continues_end_of_line_comment_with_space);
62 remove_trailing_whitespace = true; 82 remove_trailing_whitespace = true;
@@ -70,14 +90,42 @@ pub(crate) fn on_enter(db: &RootDatabase, position: FilePosition) -> Option<Text
70 let delete = if remove_trailing_whitespace { 90 let delete = if remove_trailing_whitespace {
71 let trimmed_len = comment.text().trim_end().len() as u32; 91 let trimmed_len = comment.text().trim_end().len() as u32;
72 let trailing_whitespace_len = comment.text().len() as u32 - trimmed_len; 92 let trailing_whitespace_len = comment.text().len() as u32 - trimmed_len;
73 TextRange::new(position.offset - TextSize::from(trailing_whitespace_len), position.offset) 93 TextRange::new(offset - TextSize::from(trailing_whitespace_len), offset)
74 } else { 94 } else {
75 TextRange::empty(position.offset) 95 TextRange::empty(offset)
76 }; 96 };
77 let edit = TextEdit::replace(delete, inserted); 97 let edit = TextEdit::replace(delete, inserted);
78 Some(edit) 98 Some(edit)
79} 99}
80 100
101fn on_enter_in_block(block: ast::BlockExpr, position: FilePosition) -> Option<TextEdit> {
102 let contents = block_contents(&block)?;
103
104 if block.syntax().text().contains_char('\n') {
105 return None;
106 }
107
108 let indent = IndentLevel::from_node(block.syntax());
109 let mut edit = TextEdit::insert(position.offset, format!("\n{}$0", indent + 1));
110 edit.union(TextEdit::insert(contents.text_range().end(), format!("\n{}", indent))).ok()?;
111 Some(edit)
112}
113
114fn block_contents(block: &ast::BlockExpr) -> Option<SyntaxNode> {
115 let mut node = block.tail_expr().map(|e| e.syntax().clone());
116
117 for stmt in block.statements() {
118 if node.is_some() {
119 // More than 1 node in the block
120 return None;
121 }
122
123 node = Some(stmt.syntax().clone());
124 }
125
126 node
127}
128
81fn followed_by_comment(comment: &ast::Comment) -> bool { 129fn followed_by_comment(comment: &ast::Comment) -> bool {
82 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) {
83 Some(it) => it, 131 Some(it) => it,
@@ -296,4 +344,144 @@ fn main() {
296", 344",
297 ); 345 );
298 } 346 }
347
348 #[test]
349 fn indents_fn_body_block() {
350 cov_mark::check!(indent_block_contents);
351 do_check(
352 r#"
353fn f() {$0()}
354 "#,
355 r#"
356fn f() {
357 $0()
358}
359 "#,
360 );
361 }
362
363 #[test]
364 fn indents_block_expr() {
365 do_check(
366 r#"
367fn f() {
368 let x = {$0()};
369}
370 "#,
371 r#"
372fn f() {
373 let x = {
374 $0()
375 };
376}
377 "#,
378 );
379 }
380
381 #[test]
382 fn indents_match_arm() {
383 do_check(
384 r#"
385fn f() {
386 match 6 {
387 1 => {$0f()},
388 _ => (),
389 }
390}
391 "#,
392 r#"
393fn f() {
394 match 6 {
395 1 => {
396 $0f()
397 },
398 _ => (),
399 }
400}
401 "#,
402 );
403 }
404
405 #[test]
406 fn indents_block_with_statement() {
407 do_check(
408 r#"
409fn f() {$0a = b}
410 "#,
411 r#"
412fn f() {
413 $0a = b
414}
415 "#,
416 );
417 do_check(
418 r#"
419fn f() {$0fn f() {}}
420 "#,
421 r#"
422fn f() {
423 $0fn f() {}
424}
425 "#,
426 );
427 }
428
429 #[test]
430 fn indents_nested_blocks() {
431 do_check(
432 r#"
433fn f() {$0{}}
434 "#,
435 r#"
436fn f() {
437 $0{}
438}
439 "#,
440 );
441 }
442
443 #[test]
444 fn does_not_indent_empty_block() {
445 do_check_noop(
446 r#"
447fn f() {$0}
448 "#,
449 );
450 do_check_noop(
451 r#"
452fn f() {{$0}}
453 "#,
454 );
455 }
456
457 #[test]
458 fn does_not_indent_block_with_too_much_content() {
459 do_check_noop(
460 r#"
461fn f() {$0 a = b; ()}
462 "#,
463 );
464 do_check_noop(
465 r#"
466fn f() {$0 a = b; a = b; }
467 "#,
468 );
469 }
470
471 #[test]
472 fn does_not_indent_multiline_block() {
473 do_check_noop(
474 r#"
475fn f() {$0
476}
477 "#,
478 );
479 do_check_noop(
480 r#"
481fn f() {$0
482
483}
484 "#,
485 );
486 }
299} 487}