aboutsummaryrefslogtreecommitdiff
path: root/crates
diff options
context:
space:
mode:
Diffstat (limited to 'crates')
-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
17 files changed, 313 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";