aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--crates/ide/src/diagnostics.rs60
-rw-r--r--crates/ide/src/diagnostics/fixes.rs38
-rw-r--r--crates/ide/src/diagnostics/unlinked_file.rs2
-rw-r--r--crates/ide/src/goto_definition.rs18
-rw-r--r--crates/ide/src/lib.rs32
-rw-r--r--crates/ide/src/syntax_highlighting/tests.rs98
-rw-r--r--crates/ide_assists/src/handlers/flip_comma.rs18
-rw-r--r--crates/ide_assists/src/lib.rs2
-rw-r--r--crates/rust-analyzer/build.rs3
-rw-r--r--crates/rust-analyzer/src/cli/diagnostics.rs3
-rw-r--r--crates/rust-analyzer/src/from_proto.rs30
-rw-r--r--crates/rust-analyzer/src/handlers.rs102
-rw-r--r--crates/rust-analyzer/src/to_proto.rs39
-rw-r--r--crates/rust-analyzer/tests/rust-analyzer/main.rs2
-rw-r--r--crates/rust-analyzer/tests/rust-analyzer/support.rs1
-rw-r--r--crates/test_utils/src/assert_linear.rs112
-rw-r--r--crates/test_utils/src/lib.rs3
-rw-r--r--docs/dev/style.md10
18 files changed, 323 insertions, 250 deletions
diff --git a/crates/ide/src/diagnostics.rs b/crates/ide/src/diagnostics.rs
index 4f0b4a62e..9a883acb9 100644
--- a/crates/ide/src/diagnostics.rs
+++ b/crates/ide/src/diagnostics.rs
@@ -84,6 +84,7 @@ pub struct DiagnosticsConfig {
84pub(crate) fn diagnostics( 84pub(crate) fn diagnostics(
85 db: &RootDatabase, 85 db: &RootDatabase,
86 config: &DiagnosticsConfig, 86 config: &DiagnosticsConfig,
87 resolve: bool,
87 file_id: FileId, 88 file_id: FileId,
88) -> Vec<Diagnostic> { 89) -> Vec<Diagnostic> {
89 let _p = profile::span("diagnostics"); 90 let _p = profile::span("diagnostics");
@@ -107,25 +108,25 @@ pub(crate) fn diagnostics(
107 let res = RefCell::new(res); 108 let res = RefCell::new(res);
108 let sink_builder = DiagnosticSinkBuilder::new() 109 let sink_builder = DiagnosticSinkBuilder::new()
109 .on::<hir::diagnostics::UnresolvedModule, _>(|d| { 110 .on::<hir::diagnostics::UnresolvedModule, _>(|d| {
110 res.borrow_mut().push(diagnostic_with_fix(d, &sema)); 111 res.borrow_mut().push(diagnostic_with_fix(d, &sema, resolve));
111 }) 112 })
112 .on::<hir::diagnostics::MissingFields, _>(|d| { 113 .on::<hir::diagnostics::MissingFields, _>(|d| {
113 res.borrow_mut().push(diagnostic_with_fix(d, &sema)); 114 res.borrow_mut().push(diagnostic_with_fix(d, &sema, resolve));
114 }) 115 })
115 .on::<hir::diagnostics::MissingOkOrSomeInTailExpr, _>(|d| { 116 .on::<hir::diagnostics::MissingOkOrSomeInTailExpr, _>(|d| {
116 res.borrow_mut().push(diagnostic_with_fix(d, &sema)); 117 res.borrow_mut().push(diagnostic_with_fix(d, &sema, resolve));
117 }) 118 })
118 .on::<hir::diagnostics::NoSuchField, _>(|d| { 119 .on::<hir::diagnostics::NoSuchField, _>(|d| {
119 res.borrow_mut().push(diagnostic_with_fix(d, &sema)); 120 res.borrow_mut().push(diagnostic_with_fix(d, &sema, resolve));
120 }) 121 })
121 .on::<hir::diagnostics::RemoveThisSemicolon, _>(|d| { 122 .on::<hir::diagnostics::RemoveThisSemicolon, _>(|d| {
122 res.borrow_mut().push(diagnostic_with_fix(d, &sema)); 123 res.borrow_mut().push(diagnostic_with_fix(d, &sema, resolve));
123 }) 124 })
124 .on::<hir::diagnostics::IncorrectCase, _>(|d| { 125 .on::<hir::diagnostics::IncorrectCase, _>(|d| {
125 res.borrow_mut().push(warning_with_fix(d, &sema)); 126 res.borrow_mut().push(warning_with_fix(d, &sema, resolve));
126 }) 127 })
127 .on::<hir::diagnostics::ReplaceFilterMapNextWithFindMap, _>(|d| { 128 .on::<hir::diagnostics::ReplaceFilterMapNextWithFindMap, _>(|d| {
128 res.borrow_mut().push(warning_with_fix(d, &sema)); 129 res.borrow_mut().push(warning_with_fix(d, &sema, resolve));
129 }) 130 })
130 .on::<hir::diagnostics::InactiveCode, _>(|d| { 131 .on::<hir::diagnostics::InactiveCode, _>(|d| {
131 // 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.
@@ -152,7 +153,7 @@ pub(crate) fn diagnostics(
152 // Override severity and mark as unused. 153 // Override severity and mark as unused.
153 res.borrow_mut().push( 154 res.borrow_mut().push(
154 Diagnostic::hint(range, d.message()) 155 Diagnostic::hint(range, d.message())
155 .with_fix(d.fix(&sema)) 156 .with_fix(d.fix(&sema, resolve))
156 .with_code(Some(d.code())), 157 .with_code(Some(d.code())),
157 ); 158 );
158 }) 159 })
@@ -208,15 +209,23 @@ pub(crate) fn diagnostics(
208 res.into_inner() 209 res.into_inner()
209} 210}
210 211
211fn 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 {
212 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())
213 .with_fix(d.fix(&sema)) 218 .with_fix(d.fix(&sema, resolve))
214 .with_code(Some(d.code())) 219 .with_code(Some(d.code()))
215} 220}
216 221
217fn 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 {
218 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())
219 .with_fix(d.fix(&sema)) 228 .with_fix(d.fix(&sema, resolve))
220 .with_code(Some(d.code())) 229 .with_code(Some(d.code()))
221} 230}
222 231
@@ -271,13 +280,19 @@ fn text_edit_for_remove_unnecessary_braces_with_self_in_use_statement(
271} 280}
272 281
273fn fix(id: &'static str, label: &str, source_change: SourceChange, target: TextRange) -> Assist { 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 {
274 assert!(!id.contains(' ')); 289 assert!(!id.contains(' '));
275 Assist { 290 Assist {
276 id: AssistId(id, AssistKind::QuickFix), 291 id: AssistId(id, AssistKind::QuickFix),
277 label: Label::new(label), 292 label: Label::new(label),
278 group: None, 293 group: None,
279 target, 294 target,
280 source_change: Some(source_change), 295 source_change: None,
281 } 296 }
282} 297}
283 298
@@ -299,7 +314,7 @@ mod tests {
299 314
300 let (analysis, file_position) = fixture::position(ra_fixture_before); 315 let (analysis, file_position) = fixture::position(ra_fixture_before);
301 let diagnostic = analysis 316 let diagnostic = analysis
302 .diagnostics(&DiagnosticsConfig::default(), file_position.file_id) 317 .diagnostics(&DiagnosticsConfig::default(), true, file_position.file_id)
303 .unwrap() 318 .unwrap()
304 .pop() 319 .pop()
305 .unwrap(); 320 .unwrap();
@@ -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
@@ -895,10 +911,11 @@ struct Foo {
895 911
896 let (analysis, file_id) = fixture::file(r#"mod foo;"#); 912 let (analysis, file_id) = fixture::file(r#"mod foo;"#);
897 913
898 let diagnostics = analysis.diagnostics(&config, file_id).unwrap(); 914 let diagnostics = analysis.diagnostics(&config, true, file_id).unwrap();
899 assert!(diagnostics.is_empty()); 915 assert!(diagnostics.is_empty());
900 916
901 let diagnostics = analysis.diagnostics(&DiagnosticsConfig::default(), file_id).unwrap(); 917 let diagnostics =
918 analysis.diagnostics(&DiagnosticsConfig::default(), true, file_id).unwrap();
902 assert!(!diagnostics.is_empty()); 919 assert!(!diagnostics.is_empty());
903 } 920 }
904 921
@@ -1004,8 +1021,9 @@ impl TestStruct {
1004 let expected = r#"fn foo() {}"#; 1021 let expected = r#"fn foo() {}"#;
1005 1022
1006 let (analysis, file_position) = fixture::position(input); 1023 let (analysis, file_position) = fixture::position(input);
1007 let diagnostics = 1024 let diagnostics = analysis
1008 analysis.diagnostics(&DiagnosticsConfig::default(), file_position.file_id).unwrap(); 1025 .diagnostics(&DiagnosticsConfig::default(), true, file_position.file_id)
1026 .unwrap();
1009 assert_eq!(diagnostics.len(), 1); 1027 assert_eq!(diagnostics.len(), 1);
1010 1028
1011 check_fix(input, expected); 1029 check_fix(input, expected);
diff --git a/crates/ide/src/diagnostics/fixes.rs b/crates/ide/src/diagnostics/fixes.rs
index 69cf5288c..7be8b3459 100644
--- a/crates/ide/src/diagnostics/fixes.rs
+++ b/crates/ide/src/diagnostics/fixes.rs
@@ -20,17 +20,26 @@ use syntax::{
20}; 20};
21use text_edit::TextEdit; 21use text_edit::TextEdit;
22 22
23use crate::{diagnostics::fix, references::rename::rename_with_semantics, Assist, 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<Assist>; 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<Assist> { 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( 45 Some(fix(
@@ -50,7 +59,7 @@ impl DiagnosticWithFix for UnresolvedModule {
50} 59}
51 60
52impl DiagnosticWithFix for NoSuchField { 61impl DiagnosticWithFix for NoSuchField {
53 fn fix(&self, sema: &Semantics<RootDatabase>) -> Option<Assist> { 62 fn fix(&self, sema: &Semantics<RootDatabase>, _resolve: bool) -> Option<Assist> {
54 let root = sema.db.parse_or_expand(self.file)?; 63 let root = sema.db.parse_or_expand(self.file)?;
55 missing_record_expr_field_fix( 64 missing_record_expr_field_fix(
56 &sema, 65 &sema,
@@ -61,7 +70,7 @@ impl DiagnosticWithFix for NoSuchField {
61} 70}
62 71
63impl DiagnosticWithFix for MissingFields { 72impl DiagnosticWithFix for MissingFields {
64 fn fix(&self, sema: &Semantics<RootDatabase>) -> Option<Assist> { 73 fn fix(&self, sema: &Semantics<RootDatabase>, _resolve: bool) -> Option<Assist> {
65 // Note that although we could add a diagnostics to 74 // Note that although we could add a diagnostics to
66 // fill the missing tuple field, e.g : 75 // fill the missing tuple field, e.g :
67 // `struct A(usize);` 76 // `struct A(usize);`
@@ -97,7 +106,7 @@ impl DiagnosticWithFix for MissingFields {
97} 106}
98 107
99impl DiagnosticWithFix for MissingOkOrSomeInTailExpr { 108impl DiagnosticWithFix for MissingOkOrSomeInTailExpr {
100 fn fix(&self, sema: &Semantics<RootDatabase>) -> Option<Assist> { 109 fn fix(&self, sema: &Semantics<RootDatabase>, _resolve: bool) -> Option<Assist> {
101 let root = sema.db.parse_or_expand(self.file)?; 110 let root = sema.db.parse_or_expand(self.file)?;
102 let tail_expr = self.expr.to_node(&root); 111 let tail_expr = self.expr.to_node(&root);
103 let tail_expr_range = tail_expr.syntax().text_range(); 112 let tail_expr_range = tail_expr.syntax().text_range();
@@ -110,7 +119,7 @@ impl DiagnosticWithFix for MissingOkOrSomeInTailExpr {
110} 119}
111 120
112impl DiagnosticWithFix for RemoveThisSemicolon { 121impl DiagnosticWithFix for RemoveThisSemicolon {
113 fn fix(&self, sema: &Semantics<RootDatabase>) -> Option<Assist> { 122 fn fix(&self, sema: &Semantics<RootDatabase>, _resolve: bool) -> Option<Assist> {
114 let root = sema.db.parse_or_expand(self.file)?; 123 let root = sema.db.parse_or_expand(self.file)?;
115 124
116 let semicolon = self 125 let semicolon = self
@@ -130,7 +139,7 @@ impl DiagnosticWithFix for RemoveThisSemicolon {
130} 139}
131 140
132impl DiagnosticWithFix for IncorrectCase { 141impl DiagnosticWithFix for IncorrectCase {
133 fn fix(&self, sema: &Semantics<RootDatabase>) -> Option<Assist> { 142 fn fix(&self, sema: &Semantics<RootDatabase>, resolve: bool) -> Option<Assist> {
134 let root = sema.db.parse_or_expand(self.file)?; 143 let root = sema.db.parse_or_expand(self.file)?;
135 let name_node = self.ident.to_node(&root); 144 let name_node = self.ident.to_node(&root);
136 145
@@ -138,16 +147,19 @@ impl DiagnosticWithFix for IncorrectCase {
138 let frange = name_node.original_file_range(sema.db); 147 let frange = name_node.original_file_range(sema.db);
139 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() };
140 149
141 let rename_changes =
142 rename_with_semantics(sema, file_position, &self.suggested_text).ok()?;
143
144 let label = format!("Rename to {}", self.suggested_text); 150 let label = format!("Rename to {}", self.suggested_text);
145 Some(fix("change_case", &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)
146 } 158 }
147} 159}
148 160
149impl DiagnosticWithFix for ReplaceFilterMapNextWithFindMap { 161impl DiagnosticWithFix for ReplaceFilterMapNextWithFindMap {
150 fn fix(&self, sema: &Semantics<RootDatabase>) -> Option<Assist> { 162 fn fix(&self, sema: &Semantics<RootDatabase>, _resolve: bool) -> Option<Assist> {
151 let root = sema.db.parse_or_expand(self.file)?; 163 let root = sema.db.parse_or_expand(self.file)?;
152 let next_expr = self.next_expr.to_node(&root); 164 let next_expr = self.next_expr.to_node(&root);
153 let next_call = ast::MethodCallExpr::cast(next_expr.syntax().clone())?; 165 let next_call = ast::MethodCallExpr::cast(next_expr.syntax().clone())?;
diff --git a/crates/ide/src/diagnostics/unlinked_file.rs b/crates/ide/src/diagnostics/unlinked_file.rs
index 5482b7287..7d39f4fbe 100644
--- a/crates/ide/src/diagnostics/unlinked_file.rs
+++ b/crates/ide/src/diagnostics/unlinked_file.rs
@@ -50,7 +50,7 @@ impl Diagnostic for UnlinkedFile {
50} 50}
51 51
52impl DiagnosticWithFix for UnlinkedFile { 52impl DiagnosticWithFix for UnlinkedFile {
53 fn fix(&self, sema: &hir::Semantics<RootDatabase>) -> Option<Assist> { 53 fn fix(&self, sema: &hir::Semantics<RootDatabase>, _resolve: bool) -> Option<Assist> {
54 // 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,
55 // suggest that as a fix. 55 // suggest that as a fix.
56 56
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 0615b26d3..d481be09d 100644
--- a/crates/ide/src/lib.rs
+++ b/crates/ide/src/lib.rs
@@ -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/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_assists/src/handlers/flip_comma.rs b/crates/ide_assists/src/handlers/flip_comma.rs
index 7f5e75d34..99be5e090 100644
--- a/crates/ide_assists/src/handlers/flip_comma.rs
+++ b/crates/ide_assists/src/handlers/flip_comma.rs
@@ -66,26 +66,12 @@ mod tests {
66 } 66 }
67 67
68 #[test] 68 #[test]
69 #[should_panic]
70 fn flip_comma_before_punct() { 69 fn flip_comma_before_punct() {
71 // See https://github.com/rust-analyzer/rust-analyzer/issues/1619 70 // See https://github.com/rust-analyzer/rust-analyzer/issues/1619
72 // "Flip comma" assist shouldn't be applicable to the last comma in enum or struct 71 // "Flip comma" assist shouldn't be applicable to the last comma in enum or struct
73 // declaration body. 72 // declaration body.
74 check_assist_target( 73 check_assist_not_applicable(flip_comma, "pub enum Test { A,$0 }");
75 flip_comma, 74 check_assist_not_applicable(flip_comma, "pub struct Test { foo: usize,$0 }");
76 "pub enum Test { \
77 A,$0 \
78 }",
79 ",",
80 );
81
82 check_assist_target(
83 flip_comma,
84 "pub struct Test { \
85 foo: usize,$0 \
86 }",
87 ",",
88 );
89 } 75 }
90 76
91 #[test] 77 #[test]
diff --git a/crates/ide_assists/src/lib.rs b/crates/ide_assists/src/lib.rs
index 3e2c82dac..3694f468f 100644
--- a/crates/ide_assists/src/lib.rs
+++ b/crates/ide_assists/src/lib.rs
@@ -28,7 +28,9 @@ pub use assist_config::AssistConfig;
28 28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)] 29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum AssistKind { 30pub enum AssistKind {
31 // FIXME: does the None variant make sense? Probably not.
31 None, 32 None,
33
32 QuickFix, 34 QuickFix,
33 Generate, 35 Generate,
34 Refactor, 36 Refactor,
diff --git a/crates/rust-analyzer/build.rs b/crates/rust-analyzer/build.rs
index 999dc5928..13b903891 100644
--- a/crates/rust-analyzer/build.rs
+++ b/crates/rust-analyzer/build.rs
@@ -39,7 +39,8 @@ fn set_rerun() {
39} 39}
40 40
41fn rev() -> Option<String> { 41fn rev() -> Option<String> {
42 let output = Command::new("git").args(&["describe", "--tags"]).output().ok()?; 42 let output =
43 Command::new("git").args(&["describe", "--tags", "--exclude", "nightly"]).output().ok()?;
43 let stdout = String::from_utf8(output.stdout).ok()?; 44 let stdout = String::from_utf8(output.stdout).ok()?;
44 Some(stdout) 45 Some(stdout)
45} 46}
diff --git a/crates/rust-analyzer/src/cli/diagnostics.rs b/crates/rust-analyzer/src/cli/diagnostics.rs
index 0085d0e4d..74f784338 100644
--- a/crates/rust-analyzer/src/cli/diagnostics.rs
+++ b/crates/rust-analyzer/src/cli/diagnostics.rs
@@ -57,7 +57,8 @@ pub fn diagnostics(
57 let crate_name = 57 let crate_name =
58 module.krate().display_name(db).as_deref().unwrap_or("unknown").to_string(); 58 module.krate().display_name(db).as_deref().unwrap_or("unknown").to_string();
59 println!("processing crate: {}, module: {}", crate_name, _vfs.file_path(file_id)); 59 println!("processing crate: {}, module: {}", crate_name, _vfs.file_path(file_id));
60 for diagnostic in analysis.diagnostics(&DiagnosticsConfig::default(), file_id).unwrap() 60 for diagnostic in
61 analysis.diagnostics(&DiagnosticsConfig::default(), false, file_id).unwrap()
61 { 62 {
62 if matches!(diagnostic.severity, Severity::Error) { 63 if matches!(diagnostic.severity, Severity::Error) {
63 found_error = true; 64 found_error = true;
diff --git a/crates/rust-analyzer/src/from_proto.rs b/crates/rust-analyzer/src/from_proto.rs
index 5b02b2598..712d5a9c2 100644
--- a/crates/rust-analyzer/src/from_proto.rs
+++ b/crates/rust-analyzer/src/from_proto.rs
@@ -42,27 +42,27 @@ pub(crate) fn text_range(line_index: &LineIndex, range: lsp_types::Range) -> Tex
42 TextRange::new(start, end) 42 TextRange::new(start, end)
43} 43}
44 44
45pub(crate) fn file_id(world: &GlobalStateSnapshot, url: &lsp_types::Url) -> Result<FileId> { 45pub(crate) fn file_id(snap: &GlobalStateSnapshot, url: &lsp_types::Url) -> Result<FileId> {
46 world.url_to_file_id(url) 46 snap.url_to_file_id(url)
47} 47}
48 48
49pub(crate) fn file_position( 49pub(crate) fn file_position(
50 world: &GlobalStateSnapshot, 50 snap: &GlobalStateSnapshot,
51 tdpp: lsp_types::TextDocumentPositionParams, 51 tdpp: lsp_types::TextDocumentPositionParams,
52) -> Result<FilePosition> { 52) -> Result<FilePosition> {
53 let file_id = file_id(world, &tdpp.text_document.uri)?; 53 let file_id = file_id(snap, &tdpp.text_document.uri)?;
54 let line_index = world.file_line_index(file_id)?; 54 let line_index = snap.file_line_index(file_id)?;
55 let offset = offset(&line_index, tdpp.position); 55 let offset = offset(&line_index, tdpp.position);
56 Ok(FilePosition { file_id, offset }) 56 Ok(FilePosition { file_id, offset })
57} 57}
58 58
59pub(crate) fn file_range( 59pub(crate) fn file_range(
60 world: &GlobalStateSnapshot, 60 snap: &GlobalStateSnapshot,
61 text_document_identifier: lsp_types::TextDocumentIdentifier, 61 text_document_identifier: lsp_types::TextDocumentIdentifier,
62 range: lsp_types::Range, 62 range: lsp_types::Range,
63) -> Result<FileRange> { 63) -> Result<FileRange> {
64 let file_id = file_id(world, &text_document_identifier.uri)?; 64 let file_id = file_id(snap, &text_document_identifier.uri)?;
65 let line_index = world.file_line_index(file_id)?; 65 let line_index = snap.file_line_index(file_id)?;
66 let range = text_range(&line_index, range); 66 let range = text_range(&line_index, range);
67 Ok(FileRange { file_id, range }) 67 Ok(FileRange { file_id, range })
68} 68}
@@ -82,7 +82,7 @@ pub(crate) fn assist_kind(kind: lsp_types::CodeActionKind) -> Option<AssistKind>
82} 82}
83 83
84pub(crate) fn annotation( 84pub(crate) fn annotation(
85 world: &GlobalStateSnapshot, 85 snap: &GlobalStateSnapshot,
86 code_lens: lsp_types::CodeLens, 86 code_lens: lsp_types::CodeLens,
87) -> Result<Annotation> { 87) -> Result<Annotation> {
88 let data = code_lens.data.unwrap(); 88 let data = code_lens.data.unwrap();
@@ -91,25 +91,25 @@ pub(crate) fn annotation(
91 match resolve { 91 match resolve {
92 lsp_ext::CodeLensResolveData::Impls(params) => { 92 lsp_ext::CodeLensResolveData::Impls(params) => {
93 let file_id = 93 let file_id =
94 world.url_to_file_id(&params.text_document_position_params.text_document.uri)?; 94 snap.url_to_file_id(&params.text_document_position_params.text_document.uri)?;
95 let line_index = world.file_line_index(file_id)?; 95 let line_index = snap.file_line_index(file_id)?;
96 96
97 Ok(Annotation { 97 Ok(Annotation {
98 range: text_range(&line_index, code_lens.range), 98 range: text_range(&line_index, code_lens.range),
99 kind: AnnotationKind::HasImpls { 99 kind: AnnotationKind::HasImpls {
100 position: file_position(world, params.text_document_position_params)?, 100 position: file_position(snap, params.text_document_position_params)?,
101 data: None, 101 data: None,
102 }, 102 },
103 }) 103 })
104 } 104 }
105 lsp_ext::CodeLensResolveData::References(params) => { 105 lsp_ext::CodeLensResolveData::References(params) => {
106 let file_id = world.url_to_file_id(&params.text_document.uri)?; 106 let file_id = snap.url_to_file_id(&params.text_document.uri)?;
107 let line_index = world.file_line_index(file_id)?; 107 let line_index = snap.file_line_index(file_id)?;
108 108
109 Ok(Annotation { 109 Ok(Annotation {
110 range: text_range(&line_index, code_lens.range), 110 range: text_range(&line_index, code_lens.range),
111 kind: AnnotationKind::HasReferences { 111 kind: AnnotationKind::HasReferences {
112 position: file_position(world, params)?, 112 position: file_position(snap, params)?,
113 data: None, 113 data: None,
114 }, 114 },
115 }) 115 })
diff --git a/crates/rust-analyzer/src/handlers.rs b/crates/rust-analyzer/src/handlers.rs
index 107685c63..4f0c9d23c 100644
--- a/crates/rust-analyzer/src/handlers.rs
+++ b/crates/rust-analyzer/src/handlers.rs
@@ -17,7 +17,7 @@ use lsp_server::ErrorCode;
17use lsp_types::{ 17use lsp_types::{
18 CallHierarchyIncomingCall, CallHierarchyIncomingCallsParams, CallHierarchyItem, 18 CallHierarchyIncomingCall, CallHierarchyIncomingCallsParams, CallHierarchyItem,
19 CallHierarchyOutgoingCall, CallHierarchyOutgoingCallsParams, CallHierarchyPrepareParams, 19 CallHierarchyOutgoingCall, CallHierarchyOutgoingCallsParams, CallHierarchyPrepareParams,
20 CodeActionKind, CodeLens, CompletionItem, Diagnostic, DiagnosticTag, DocumentFormattingParams, 20 CodeLens, CompletionItem, Diagnostic, DiagnosticTag, DocumentFormattingParams,
21 DocumentHighlight, FoldingRange, FoldingRangeParams, HoverContents, Location, NumberOrString, 21 DocumentHighlight, FoldingRange, FoldingRangeParams, HoverContents, Location, NumberOrString,
22 Position, PrepareRenameResponse, Range, RenameParams, SemanticTokensDeltaParams, 22 Position, PrepareRenameResponse, Range, RenameParams, SemanticTokensDeltaParams,
23 SemanticTokensFullDeltaResult, SemanticTokensParams, SemanticTokensRangeParams, 23 SemanticTokensFullDeltaResult, SemanticTokensParams, SemanticTokensRangeParams,
@@ -36,7 +36,7 @@ use crate::{
36 diff::diff, 36 diff::diff,
37 from_proto, 37 from_proto,
38 global_state::{GlobalState, GlobalStateSnapshot}, 38 global_state::{GlobalState, GlobalStateSnapshot},
39 line_index::{LineEndings, LineIndex}, 39 line_index::LineEndings,
40 lsp_ext::{self, InlayHint, InlayHintsParams}, 40 lsp_ext::{self, InlayHint, InlayHintsParams},
41 lsp_utils::all_edits_are_disjoint, 41 lsp_utils::all_edits_are_disjoint,
42 to_proto, LspError, Result, 42 to_proto, LspError, Result,
@@ -982,86 +982,52 @@ pub(crate) fn handle_code_action(
982 params: lsp_types::CodeActionParams, 982 params: lsp_types::CodeActionParams,
983) -> Result<Option<Vec<lsp_ext::CodeAction>>> { 983) -> Result<Option<Vec<lsp_ext::CodeAction>>> {
984 let _p = profile::span("handle_code_action"); 984 let _p = profile::span("handle_code_action");
985 // We intentionally don't support command-based actions, as those either 985
986 // requires custom client-code anyway, or requires server-initiated edits.
987 // Server initiated edits break causality, so we avoid those as well.
988 if !snap.config.code_action_literals() { 986 if !snap.config.code_action_literals() {
987 // We intentionally don't support command-based actions, as those either
988 // require either custom client-code or server-initiated edits. Server
989 // initiated edits break causality, so we avoid those.
989 return Ok(None); 990 return Ok(None);
990 } 991 }
991 992
992 let file_id = from_proto::file_id(&snap, &params.text_document.uri)?; 993 let line_index =
993 let line_index = snap.file_line_index(file_id)?; 994 snap.file_line_index(from_proto::file_id(&snap, &params.text_document.uri)?)?;
994 let range = from_proto::text_range(&line_index, params.range); 995 let frange = from_proto::file_range(&snap, params.text_document.clone(), params.range)?;
995 let frange = FileRange { file_id, range };
996 996
997 let mut assists_config = snap.config.assist(); 997 let mut assists_config = snap.config.assist();
998 assists_config.allowed = params 998 assists_config.allowed = params
999 .clone()
1000 .context 999 .context
1001 .only 1000 .only
1001 .clone()
1002 .map(|it| it.into_iter().filter_map(from_proto::assist_kind).collect()); 1002 .map(|it| it.into_iter().filter_map(from_proto::assist_kind).collect());
1003 1003
1004 let mut res: Vec<lsp_ext::CodeAction> = Vec::new(); 1004 let mut res: Vec<lsp_ext::CodeAction> = Vec::new();
1005 1005
1006 let include_quick_fixes = match &params.context.only { 1006 let code_action_resolve_cap = snap.config.code_action_resolve();
1007 Some(v) => v.iter().any(|it| { 1007 let assists = snap.analysis.assists_with_fixes(
1008 it == &lsp_types::CodeActionKind::EMPTY || it == &lsp_types::CodeActionKind::QUICKFIX 1008 &assists_config,
1009 }), 1009 &snap.config.diagnostics(),
1010 None => true, 1010 !code_action_resolve_cap,
1011 }; 1011 frange,
1012 if include_quick_fixes { 1012 )?;
1013 add_quick_fixes(&snap, frange, &line_index, &mut res)?; 1013 for (index, assist) in assists.into_iter().enumerate() {
1014 } 1014 let resolve_data =
1015 1015 if code_action_resolve_cap { Some((index, params.clone())) } else { None };
1016 if snap.config.code_action_resolve() { 1016 let code_action = to_proto::code_action(&snap, assist, resolve_data)?;
1017 for (index, assist) in 1017 res.push(code_action)
1018 snap.analysis.assists(&assists_config, false, frange)?.into_iter().enumerate()
1019 {
1020 res.push(to_proto::unresolved_code_action(&snap, params.clone(), assist, index)?);
1021 }
1022 } else {
1023 for assist in snap.analysis.assists(&assists_config, true, frange)?.into_iter() {
1024 res.push(to_proto::resolved_code_action(&snap, assist)?);
1025 }
1026 }
1027
1028 Ok(Some(res))
1029}
1030
1031fn add_quick_fixes(
1032 snap: &GlobalStateSnapshot,
1033 frange: FileRange,
1034 line_index: &LineIndex,
1035 acc: &mut Vec<lsp_ext::CodeAction>,
1036) -> Result<()> {
1037 let diagnostics = snap.analysis.diagnostics(&snap.config.diagnostics(), frange.file_id)?;
1038
1039 for fix in diagnostics
1040 .into_iter()
1041 .filter_map(|d| d.fix)
1042 .filter(|fix| fix.target.intersect(frange.range).is_some())
1043 {
1044 if let Some(source_change) = fix.source_change {
1045 let edit = to_proto::snippet_workspace_edit(&snap, source_change)?;
1046 let action = lsp_ext::CodeAction {
1047 title: fix.label.to_string(),
1048 group: None,
1049 kind: Some(CodeActionKind::QUICKFIX),
1050 edit: Some(edit),
1051 is_preferred: Some(false),
1052 data: None,
1053 };
1054 acc.push(action);
1055 }
1056 } 1018 }
1057 1019
1020 // Fixes from `cargo check`.
1058 for fix in snap.check_fixes.get(&frange.file_id).into_iter().flatten() { 1021 for fix in snap.check_fixes.get(&frange.file_id).into_iter().flatten() {
1022 // FIXME: this mapping is awkward and shouldn't exist. Refactor
1023 // `snap.check_fixes` to not convert to LSP prematurely.
1059 let fix_range = from_proto::text_range(&line_index, fix.range); 1024 let fix_range = from_proto::text_range(&line_index, fix.range);
1060 if fix_range.intersect(frange.range).is_some() { 1025 if fix_range.intersect(frange.range).is_some() {
1061 acc.push(fix.action.clone()); 1026 res.push(fix.action.clone());
1062 } 1027 }
1063 } 1028 }
1064 Ok(()) 1029
1030 Ok(Some(res))
1065} 1031}
1066 1032
1067pub(crate) fn handle_code_action_resolve( 1033pub(crate) fn handle_code_action_resolve(
@@ -1086,12 +1052,18 @@ pub(crate) fn handle_code_action_resolve(
1086 .only 1052 .only
1087 .map(|it| it.into_iter().filter_map(from_proto::assist_kind).collect()); 1053 .map(|it| it.into_iter().filter_map(from_proto::assist_kind).collect());
1088 1054
1089 let assists = snap.analysis.assists(&assists_config, true, frange)?; 1055 let assists = snap.analysis.assists_with_fixes(
1056 &assists_config,
1057 &snap.config.diagnostics(),
1058 true,
1059 frange,
1060 )?;
1061
1090 let (id, index) = split_once(&params.id, ':').unwrap(); 1062 let (id, index) = split_once(&params.id, ':').unwrap();
1091 let index = index.parse::<usize>().unwrap(); 1063 let index = index.parse::<usize>().unwrap();
1092 let assist = &assists[index]; 1064 let assist = &assists[index];
1093 assert!(assist.id.0 == id); 1065 assert!(assist.id.0 == id);
1094 let edit = to_proto::resolved_code_action(&snap, assist.clone())?.edit; 1066 let edit = to_proto::code_action(&snap, assist.clone(), None)?.edit;
1095 code_action.edit = edit; 1067 code_action.edit = edit;
1096 Ok(code_action) 1068 Ok(code_action)
1097} 1069}
@@ -1210,7 +1182,7 @@ pub(crate) fn publish_diagnostics(
1210 1182
1211 let diagnostics: Vec<Diagnostic> = snap 1183 let diagnostics: Vec<Diagnostic> = snap
1212 .analysis 1184 .analysis
1213 .diagnostics(&snap.config.diagnostics(), file_id)? 1185 .diagnostics(&snap.config.diagnostics(), false, file_id)?
1214 .into_iter() 1186 .into_iter()
1215 .map(|d| Diagnostic { 1187 .map(|d| Diagnostic {
1216 range: to_proto::range(&line_index, d.range), 1188 range: to_proto::range(&line_index, d.range),
diff --git a/crates/rust-analyzer/src/to_proto.rs b/crates/rust-analyzer/src/to_proto.rs
index 9fac562ff..53852bfdc 100644
--- a/crates/rust-analyzer/src/to_proto.rs
+++ b/crates/rust-analyzer/src/to_proto.rs
@@ -843,40 +843,31 @@ pub(crate) fn code_action_kind(kind: AssistKind) -> lsp_types::CodeActionKind {
843 } 843 }
844} 844}
845 845
846pub(crate) fn unresolved_code_action( 846pub(crate) fn code_action(
847 snap: &GlobalStateSnapshot, 847 snap: &GlobalStateSnapshot,
848 code_action_params: lsp_types::CodeActionParams,
849 assist: Assist, 848 assist: Assist,
850 index: usize, 849 resolve_data: Option<(usize, lsp_types::CodeActionParams)>,
851) -> Result<lsp_ext::CodeAction> { 850) -> Result<lsp_ext::CodeAction> {
852 assert!(assist.source_change.is_none()); 851 let mut res = lsp_ext::CodeAction {
853 let res = lsp_ext::CodeAction {
854 title: assist.label.to_string(), 852 title: assist.label.to_string(),
855 group: assist.group.filter(|_| snap.config.code_action_group()).map(|gr| gr.0), 853 group: assist.group.filter(|_| snap.config.code_action_group()).map(|gr| gr.0),
856 kind: Some(code_action_kind(assist.id.1)), 854 kind: Some(code_action_kind(assist.id.1)),
857 edit: None, 855 edit: None,
858 is_preferred: None, 856 is_preferred: None,
859 data: Some(lsp_ext::CodeActionData {
860 id: format!("{}:{}", assist.id.0, index.to_string()),
861 code_action_params,
862 }),
863 };
864 Ok(res)
865}
866
867pub(crate) fn resolved_code_action(
868 snap: &GlobalStateSnapshot,
869 assist: Assist,
870) -> Result<lsp_ext::CodeAction> {
871 let change = assist.source_change.unwrap();
872 let res = lsp_ext::CodeAction {
873 edit: Some(snippet_workspace_edit(snap, change)?),
874 title: assist.label.to_string(),
875 group: assist.group.filter(|_| snap.config.code_action_group()).map(|gr| gr.0),
876 kind: Some(code_action_kind(assist.id.1)),
877 is_preferred: None,
878 data: None, 857 data: None,
879 }; 858 };
859 match (assist.source_change, resolve_data) {
860 (Some(it), _) => res.edit = Some(snippet_workspace_edit(snap, it)?),
861 (None, Some((index, code_action_params))) => {
862 res.data = Some(lsp_ext::CodeActionData {
863 id: format!("{}:{}", assist.id.0, index.to_string()),
864 code_action_params,
865 });
866 }
867 (None, None) => {
868 stdx::never!("assist should always be resolved if client can't do lazy resolving")
869 }
870 };
880 Ok(res) 871 Ok(res)
881} 872}
882 873
diff --git a/crates/rust-analyzer/tests/rust-analyzer/main.rs b/crates/rust-analyzer/tests/rust-analyzer/main.rs
index 1e4c04bbf..52a2674d5 100644
--- a/crates/rust-analyzer/tests/rust-analyzer/main.rs
+++ b/crates/rust-analyzer/tests/rust-analyzer/main.rs
@@ -340,7 +340,6 @@ fn main() {}
340 } 340 }
341 ] 341 ]
342 }, 342 },
343 "isPreferred": false,
344 "kind": "quickfix", 343 "kind": "quickfix",
345 "title": "Create module" 344 "title": "Create module"
346 }]), 345 }]),
@@ -411,7 +410,6 @@ fn main() {{}}
411 } 410 }
412 ] 411 ]
413 }, 412 },
414 "isPreferred": false,
415 "kind": "quickfix", 413 "kind": "quickfix",
416 "title": "Create module" 414 "title": "Create module"
417 }]), 415 }]),
diff --git a/crates/rust-analyzer/tests/rust-analyzer/support.rs b/crates/rust-analyzer/tests/rust-analyzer/support.rs
index 5e388c0f0..75e677762 100644
--- a/crates/rust-analyzer/tests/rust-analyzer/support.rs
+++ b/crates/rust-analyzer/tests/rust-analyzer/support.rs
@@ -168,6 +168,7 @@ impl Server {
168 self.send_notification(r) 168 self.send_notification(r)
169 } 169 }
170 170
171 #[track_caller]
171 pub(crate) fn request<R>(&self, params: R::Params, expected_resp: Value) 172 pub(crate) fn request<R>(&self, params: R::Params, expected_resp: Value)
172 where 173 where
173 R: lsp_types::request::Request, 174 R: lsp_types::request::Request,
diff --git a/crates/test_utils/src/assert_linear.rs b/crates/test_utils/src/assert_linear.rs
new file mode 100644
index 000000000..6ecc232e1
--- /dev/null
+++ b/crates/test_utils/src/assert_linear.rs
@@ -0,0 +1,112 @@
1//! Checks that a set of measurements looks like a linear function rather than
2//! like a quadratic function. Algorithm:
3//!
4//! 1. Linearly scale input to be in [0; 1)
5//! 2. Using linear regression, compute the best linear function approximating
6//! the input.
7//! 3. Compute RMSE and maximal absolute error.
8//! 4. Check that errors are within tolerances and that the constant term is not
9//! too negative.
10//!
11//! Ideally, we should use a proper "model selection" to directly compare
12//! quadratic and linear models, but that sounds rather complicated:
13//!
14//! https://stats.stackexchange.com/questions/21844/selecting-best-model-based-on-linear-quadratic-and-cubic-fit-of-data
15//!
16//! We might get false positives on a VM, but never false negatives. So, if the
17//! first round fails, we repeat the ordeal three more times and fail only if
18//! every time there's a fault.
19use stdx::format_to;
20
21#[derive(Default)]
22pub struct AssertLinear {
23 rounds: Vec<Round>,
24}
25
26#[derive(Default)]
27struct Round {
28 samples: Vec<(f64, f64)>,
29 plot: String,
30 linear: bool,
31}
32
33impl AssertLinear {
34 pub fn next_round(&mut self) -> bool {
35 if let Some(round) = self.rounds.last_mut() {
36 round.finish();
37 }
38 if self.rounds.iter().any(|it| it.linear) || self.rounds.len() == 4 {
39 return false;
40 }
41 self.rounds.push(Round::default());
42 true
43 }
44
45 pub fn sample(&mut self, x: f64, y: f64) {
46 self.rounds.last_mut().unwrap().samples.push((x, y))
47 }
48}
49
50impl Drop for AssertLinear {
51 fn drop(&mut self) {
52 assert!(!self.rounds.is_empty());
53 if self.rounds.iter().all(|it| !it.linear) {
54 for round in &self.rounds {
55 eprintln!("\n{}", round.plot);
56 }
57 panic!("Doesn't look linear!")
58 }
59 }
60}
61
62impl Round {
63 fn finish(&mut self) {
64 let (mut xs, mut ys): (Vec<_>, Vec<_>) = self.samples.iter().copied().unzip();
65 normalize(&mut xs);
66 normalize(&mut ys);
67 let xy = xs.iter().copied().zip(ys.iter().copied());
68
69 // Linear regression: finding a and b to fit y = a + b*x.
70
71 let mean_x = mean(&xs);
72 let mean_y = mean(&ys);
73
74 let b = {
75 let mut num = 0.0;
76 let mut denom = 0.0;
77 for (x, y) in xy.clone() {
78 num += (x - mean_x) * (y - mean_y);
79 denom += (x - mean_x).powi(2);
80 }
81 num / denom
82 };
83
84 let a = mean_y - b * mean_x;
85
86 self.plot = format!("y_pred = {:.3} + {:.3} * x\n\nx y y_pred\n", a, b);
87
88 let mut se = 0.0;
89 let mut max_error = 0.0f64;
90 for (x, y) in xy {
91 let y_pred = a + b * x;
92 se += (y - y_pred).powi(2);
93 max_error = max_error.max((y_pred - y).abs());
94
95 format_to!(self.plot, "{:.3} {:.3} {:.3}\n", x, y, y_pred);
96 }
97
98 let rmse = (se / xs.len() as f64).sqrt();
99 format_to!(self.plot, "\nrmse = {:.3} max error = {:.3}", rmse, max_error);
100
101 self.linear = rmse < 0.05 && max_error < 0.1 && a > -0.1;
102
103 fn normalize(xs: &mut Vec<f64>) {
104 let max = xs.iter().copied().max_by(|a, b| a.partial_cmp(b).unwrap()).unwrap();
105 xs.iter_mut().for_each(|it| *it /= max);
106 }
107
108 fn mean(xs: &[f64]) -> f64 {
109 xs.iter().copied().sum::<f64>() / (xs.len() as f64)
110 }
111 }
112}
diff --git a/crates/test_utils/src/lib.rs b/crates/test_utils/src/lib.rs
index c5f859790..72466c957 100644
--- a/crates/test_utils/src/lib.rs
+++ b/crates/test_utils/src/lib.rs
@@ -8,6 +8,7 @@
8 8
9pub mod bench_fixture; 9pub mod bench_fixture;
10mod fixture; 10mod fixture;
11mod assert_linear;
11 12
12use std::{ 13use std::{
13 convert::{TryFrom, TryInto}, 14 convert::{TryFrom, TryInto},
@@ -22,7 +23,7 @@ use text_size::{TextRange, TextSize};
22pub use dissimilar::diff as __diff; 23pub use dissimilar::diff as __diff;
23pub use rustc_hash::FxHashMap; 24pub use rustc_hash::FxHashMap;
24 25
25pub use crate::fixture::Fixture; 26pub use crate::{assert_linear::AssertLinear, fixture::Fixture};
26 27
27pub const CURSOR_MARKER: &str = "$0"; 28pub const CURSOR_MARKER: &str = "$0";
28pub const ESCAPED_CURSOR_MARKER: &str = "\\$0"; 29pub const ESCAPED_CURSOR_MARKER: &str = "\\$0";
diff --git a/docs/dev/style.md b/docs/dev/style.md
index 468dedff2..7c47c26b2 100644
--- a/docs/dev/style.md
+++ b/docs/dev/style.md
@@ -152,6 +152,16 @@ Do not reuse marks between several tests.
152 152
153**Rationale:** marks provide an easy way to find the canonical test for each bit of code. 153**Rationale:** marks provide an easy way to find the canonical test for each bit of code.
154This makes it much easier to understand. 154This makes it much easier to understand.
155More than one mark per test / code branch doesn't add significantly to understanding.
156
157## `#[should_panic]`
158
159Do not use `#[should_panic]` tests.
160Instead, explicitly check for `None`, `Err`, etc.
161
162**Rationale:**a `#[should_panic]` is a tool for library authors, to makes sure that API does not fail silently, when misused.
163`rust-analyzer` is not a library, we don't need to test for API misuse, and we have to handle any user input without panics.
164Panic messages in the logs from the `#[should_panic]` tests are confusing.
155 165
156## Function Preconditions 166## Function Preconditions
157 167