diff options
author | Emil Lauridsen <[email protected]> | 2020-01-31 18:23:25 +0000 |
---|---|---|
committer | Emil Lauridsen <[email protected]> | 2020-02-03 10:34:24 +0000 |
commit | 790788d5f4013d8d92f110bc12a581d18cf4b6ae (patch) | |
tree | 311e11529c7546b7a09486d5c161039d8bd8f975 /crates/ra_cargo_watch | |
parent | 52456c44901c8c38c8bcb742ebe305484af8f36f (diff) |
Rework how we send diagnostics to client.
The previous way of sending from the thread pool suffered from stale
diagnostics due to being canceled before we could clear the old ones.
The key change is moving to sending diagnostics from the main loop
thread, but doing all the hard work in the thread pool. This should
provide the best of both worlds, with little to no of the downsides.
This should hopefully fix a lot of issues, but we'll need testing in
each individual issue to be sure.
Diffstat (limited to 'crates/ra_cargo_watch')
-rw-r--r-- | crates/ra_cargo_watch/src/conv.rs | 66 | ||||
-rw-r--r-- | crates/ra_cargo_watch/src/lib.rs | 110 |
2 files changed, 38 insertions, 138 deletions
diff --git a/crates/ra_cargo_watch/src/conv.rs b/crates/ra_cargo_watch/src/conv.rs index 8fba400ae..506370535 100644 --- a/crates/ra_cargo_watch/src/conv.rs +++ b/crates/ra_cargo_watch/src/conv.rs | |||
@@ -1,12 +1,11 @@ | |||
1 | //! This module provides the functionality needed to convert diagnostics from | 1 | //! This module provides the functionality needed to convert diagnostics from |
2 | //! `cargo check` json format to the LSP diagnostic format. | 2 | //! `cargo check` json format to the LSP diagnostic format. |
3 | use cargo_metadata::diagnostic::{ | 3 | use cargo_metadata::diagnostic::{ |
4 | Applicability, Diagnostic as RustDiagnostic, DiagnosticLevel, DiagnosticSpan, | 4 | Diagnostic as RustDiagnostic, DiagnosticLevel, DiagnosticSpan, DiagnosticSpanMacroExpansion, |
5 | DiagnosticSpanMacroExpansion, | ||
6 | }; | 5 | }; |
7 | use lsp_types::{ | 6 | use lsp_types::{ |
8 | Diagnostic, DiagnosticRelatedInformation, DiagnosticSeverity, DiagnosticTag, Location, | 7 | CodeAction, Diagnostic, DiagnosticRelatedInformation, DiagnosticSeverity, DiagnosticTag, |
9 | NumberOrString, Position, Range, Url, | 8 | Location, NumberOrString, Position, Range, TextEdit, Url, WorkspaceEdit, |
10 | }; | 9 | }; |
11 | use std::{ | 10 | use std::{ |
12 | fmt::Write, | 11 | fmt::Write, |
@@ -117,38 +116,9 @@ fn is_deprecated(rd: &RustDiagnostic) -> bool { | |||
117 | } | 116 | } |
118 | } | 117 | } |
119 | 118 | ||
120 | #[derive(Clone, Debug)] | ||
121 | pub struct SuggestedFix { | ||
122 | pub title: String, | ||
123 | pub location: Location, | ||
124 | pub replacement: String, | ||
125 | pub applicability: Applicability, | ||
126 | pub diagnostics: Vec<Diagnostic>, | ||
127 | } | ||
128 | |||
129 | impl std::cmp::PartialEq<SuggestedFix> for SuggestedFix { | ||
130 | fn eq(&self, other: &SuggestedFix) -> bool { | ||
131 | if self.title == other.title | ||
132 | && self.location == other.location | ||
133 | && self.replacement == other.replacement | ||
134 | { | ||
135 | // Applicability doesn't impl PartialEq... | ||
136 | match (&self.applicability, &other.applicability) { | ||
137 | (Applicability::MachineApplicable, Applicability::MachineApplicable) => true, | ||
138 | (Applicability::HasPlaceholders, Applicability::HasPlaceholders) => true, | ||
139 | (Applicability::MaybeIncorrect, Applicability::MaybeIncorrect) => true, | ||
140 | (Applicability::Unspecified, Applicability::Unspecified) => true, | ||
141 | _ => false, | ||
142 | } | ||
143 | } else { | ||
144 | false | ||
145 | } | ||
146 | } | ||
147 | } | ||
148 | |||
149 | enum MappedRustChildDiagnostic { | 119 | enum MappedRustChildDiagnostic { |
150 | Related(DiagnosticRelatedInformation), | 120 | Related(DiagnosticRelatedInformation), |
151 | SuggestedFix(SuggestedFix), | 121 | SuggestedFix(CodeAction), |
152 | MessageLine(String), | 122 | MessageLine(String), |
153 | } | 123 | } |
154 | 124 | ||
@@ -176,12 +146,20 @@ fn map_rust_child_diagnostic( | |||
176 | rd.message.clone() | 146 | rd.message.clone() |
177 | }; | 147 | }; |
178 | 148 | ||
179 | MappedRustChildDiagnostic::SuggestedFix(SuggestedFix { | 149 | let edit = { |
150 | let edits = vec![TextEdit::new(location.range, suggested_replacement.clone())]; | ||
151 | let mut edit_map = std::collections::HashMap::new(); | ||
152 | edit_map.insert(location.uri, edits); | ||
153 | WorkspaceEdit::new(edit_map) | ||
154 | }; | ||
155 | |||
156 | MappedRustChildDiagnostic::SuggestedFix(CodeAction { | ||
180 | title, | 157 | title, |
181 | location, | 158 | kind: Some("quickfix".to_string()), |
182 | replacement: suggested_replacement.clone(), | 159 | diagnostics: None, |
183 | applicability: span.suggestion_applicability.clone().unwrap_or(Applicability::Unknown), | 160 | edit: Some(edit), |
184 | diagnostics: vec![], | 161 | command: None, |
162 | is_preferred: None, | ||
185 | }) | 163 | }) |
186 | } else { | 164 | } else { |
187 | MappedRustChildDiagnostic::Related(DiagnosticRelatedInformation { | 165 | MappedRustChildDiagnostic::Related(DiagnosticRelatedInformation { |
@@ -195,7 +173,7 @@ fn map_rust_child_diagnostic( | |||
195 | pub(crate) struct MappedRustDiagnostic { | 173 | pub(crate) struct MappedRustDiagnostic { |
196 | pub location: Location, | 174 | pub location: Location, |
197 | pub diagnostic: Diagnostic, | 175 | pub diagnostic: Diagnostic, |
198 | pub suggested_fixes: Vec<SuggestedFix>, | 176 | pub fixes: Vec<CodeAction>, |
199 | } | 177 | } |
200 | 178 | ||
201 | /// Converts a Rust root diagnostic to LSP form | 179 | /// Converts a Rust root diagnostic to LSP form |
@@ -250,15 +228,13 @@ pub(crate) fn map_rust_diagnostic_to_lsp( | |||
250 | } | 228 | } |
251 | } | 229 | } |
252 | 230 | ||
253 | let mut suggested_fixes = vec![]; | 231 | let mut fixes = vec![]; |
254 | let mut message = rd.message.clone(); | 232 | let mut message = rd.message.clone(); |
255 | for child in &rd.children { | 233 | for child in &rd.children { |
256 | let child = map_rust_child_diagnostic(&child, workspace_root); | 234 | let child = map_rust_child_diagnostic(&child, workspace_root); |
257 | match child { | 235 | match child { |
258 | MappedRustChildDiagnostic::Related(related) => related_information.push(related), | 236 | MappedRustChildDiagnostic::Related(related) => related_information.push(related), |
259 | MappedRustChildDiagnostic::SuggestedFix(suggested_fix) => { | 237 | MappedRustChildDiagnostic::SuggestedFix(code_action) => fixes.push(code_action.into()), |
260 | suggested_fixes.push(suggested_fix) | ||
261 | } | ||
262 | MappedRustChildDiagnostic::MessageLine(message_line) => { | 238 | MappedRustChildDiagnostic::MessageLine(message_line) => { |
263 | write!(&mut message, "\n{}", message_line).unwrap(); | 239 | write!(&mut message, "\n{}", message_line).unwrap(); |
264 | 240 | ||
@@ -295,7 +271,7 @@ pub(crate) fn map_rust_diagnostic_to_lsp( | |||
295 | tags: if !tags.is_empty() { Some(tags) } else { None }, | 271 | tags: if !tags.is_empty() { Some(tags) } else { None }, |
296 | }; | 272 | }; |
297 | 273 | ||
298 | Some(MappedRustDiagnostic { location, diagnostic, suggested_fixes }) | 274 | Some(MappedRustDiagnostic { location, diagnostic, fixes }) |
299 | } | 275 | } |
300 | 276 | ||
301 | /// Returns a `Url` object from a given path, will lowercase drive letters if present. | 277 | /// Returns a `Url` object from a given path, will lowercase drive letters if present. |
diff --git a/crates/ra_cargo_watch/src/lib.rs b/crates/ra_cargo_watch/src/lib.rs index a718a5e52..f07c34549 100644 --- a/crates/ra_cargo_watch/src/lib.rs +++ b/crates/ra_cargo_watch/src/lib.rs | |||
@@ -4,22 +4,20 @@ | |||
4 | use cargo_metadata::Message; | 4 | use cargo_metadata::Message; |
5 | use crossbeam_channel::{never, select, unbounded, Receiver, RecvError, Sender}; | 5 | use crossbeam_channel::{never, select, unbounded, Receiver, RecvError, Sender}; |
6 | use lsp_types::{ | 6 | use lsp_types::{ |
7 | Diagnostic, Url, WorkDoneProgress, WorkDoneProgressBegin, WorkDoneProgressEnd, | 7 | CodeAction, CodeActionOrCommand, Diagnostic, Url, WorkDoneProgress, WorkDoneProgressBegin, |
8 | WorkDoneProgressReport, | 8 | WorkDoneProgressEnd, WorkDoneProgressReport, |
9 | }; | 9 | }; |
10 | use std::{ | 10 | use std::{ |
11 | collections::HashMap, | ||
12 | io::{BufRead, BufReader}, | 11 | io::{BufRead, BufReader}, |
13 | path::PathBuf, | 12 | path::PathBuf, |
14 | process::{Command, Stdio}, | 13 | process::{Command, Stdio}, |
15 | sync::Arc, | ||
16 | thread::JoinHandle, | 14 | thread::JoinHandle, |
17 | time::Instant, | 15 | time::Instant, |
18 | }; | 16 | }; |
19 | 17 | ||
20 | mod conv; | 18 | mod conv; |
21 | 19 | ||
22 | use crate::conv::{map_rust_diagnostic_to_lsp, MappedRustDiagnostic, SuggestedFix}; | 20 | use crate::conv::{map_rust_diagnostic_to_lsp, MappedRustDiagnostic}; |
23 | 21 | ||
24 | pub use crate::conv::url_from_path_with_drive_lowercasing; | 22 | pub use crate::conv::url_from_path_with_drive_lowercasing; |
25 | 23 | ||
@@ -38,7 +36,6 @@ pub struct CheckOptions { | |||
38 | #[derive(Debug)] | 36 | #[derive(Debug)] |
39 | pub struct CheckWatcher { | 37 | pub struct CheckWatcher { |
40 | pub task_recv: Receiver<CheckTask>, | 38 | pub task_recv: Receiver<CheckTask>, |
41 | pub state: Arc<CheckState>, | ||
42 | cmd_send: Option<Sender<CheckCommand>>, | 39 | cmd_send: Option<Sender<CheckCommand>>, |
43 | handle: Option<JoinHandle<()>>, | 40 | handle: Option<JoinHandle<()>>, |
44 | } | 41 | } |
@@ -46,7 +43,6 @@ pub struct CheckWatcher { | |||
46 | impl CheckWatcher { | 43 | impl CheckWatcher { |
47 | pub fn new(options: &CheckOptions, workspace_root: PathBuf) -> CheckWatcher { | 44 | pub fn new(options: &CheckOptions, workspace_root: PathBuf) -> CheckWatcher { |
48 | let options = options.clone(); | 45 | let options = options.clone(); |
49 | let state = Arc::new(CheckState::new()); | ||
50 | 46 | ||
51 | let (task_send, task_recv) = unbounded::<CheckTask>(); | 47 | let (task_send, task_recv) = unbounded::<CheckTask>(); |
52 | let (cmd_send, cmd_recv) = unbounded::<CheckCommand>(); | 48 | let (cmd_send, cmd_recv) = unbounded::<CheckCommand>(); |
@@ -54,13 +50,12 @@ impl CheckWatcher { | |||
54 | let mut check = CheckWatcherThread::new(options, workspace_root); | 50 | let mut check = CheckWatcherThread::new(options, workspace_root); |
55 | check.run(&task_send, &cmd_recv); | 51 | check.run(&task_send, &cmd_recv); |
56 | }); | 52 | }); |
57 | CheckWatcher { task_recv, cmd_send: Some(cmd_send), handle: Some(handle), state } | 53 | CheckWatcher { task_recv, cmd_send: Some(cmd_send), handle: Some(handle) } |
58 | } | 54 | } |
59 | 55 | ||
60 | /// Returns a CheckWatcher that doesn't actually do anything | 56 | /// Returns a CheckWatcher that doesn't actually do anything |
61 | pub fn dummy() -> CheckWatcher { | 57 | pub fn dummy() -> CheckWatcher { |
62 | let state = Arc::new(CheckState::new()); | 58 | CheckWatcher { task_recv: never(), cmd_send: None, handle: None } |
63 | CheckWatcher { task_recv: never(), cmd_send: None, handle: None, state } | ||
64 | } | 59 | } |
65 | 60 | ||
66 | /// Schedule a re-start of the cargo check worker. | 61 | /// Schedule a re-start of the cargo check worker. |
@@ -87,84 +82,13 @@ impl std::ops::Drop for CheckWatcher { | |||
87 | } | 82 | } |
88 | } | 83 | } |
89 | 84 | ||
90 | #[derive(Clone, Debug)] | ||
91 | pub struct CheckState { | ||
92 | diagnostic_collection: HashMap<Url, Vec<Diagnostic>>, | ||
93 | suggested_fix_collection: HashMap<Url, Vec<SuggestedFix>>, | ||
94 | } | ||
95 | |||
96 | impl CheckState { | ||
97 | fn new() -> CheckState { | ||
98 | CheckState { | ||
99 | diagnostic_collection: HashMap::new(), | ||
100 | suggested_fix_collection: HashMap::new(), | ||
101 | } | ||
102 | } | ||
103 | |||
104 | /// Clear the cached diagnostics, and schedule updating diagnostics by the | ||
105 | /// server, to clear stale results. | ||
106 | pub fn clear(&mut self) -> Vec<Url> { | ||
107 | let cleared_files: Vec<Url> = self.diagnostic_collection.keys().cloned().collect(); | ||
108 | self.diagnostic_collection.clear(); | ||
109 | self.suggested_fix_collection.clear(); | ||
110 | cleared_files | ||
111 | } | ||
112 | |||
113 | pub fn diagnostics_for(&self, uri: &Url) -> Option<&[Diagnostic]> { | ||
114 | self.diagnostic_collection.get(uri).map(|d| d.as_slice()) | ||
115 | } | ||
116 | |||
117 | pub fn fixes_for(&self, uri: &Url) -> Option<&[SuggestedFix]> { | ||
118 | self.suggested_fix_collection.get(uri).map(|d| d.as_slice()) | ||
119 | } | ||
120 | |||
121 | pub fn add_diagnostic_with_fixes(&mut self, file_uri: Url, diagnostic: DiagnosticWithFixes) { | ||
122 | for fix in diagnostic.suggested_fixes { | ||
123 | self.add_suggested_fix_for_diagnostic(fix, &diagnostic.diagnostic); | ||
124 | } | ||
125 | self.add_diagnostic(file_uri, diagnostic.diagnostic); | ||
126 | } | ||
127 | |||
128 | fn add_diagnostic(&mut self, file_uri: Url, diagnostic: Diagnostic) { | ||
129 | let diagnostics = self.diagnostic_collection.entry(file_uri).or_default(); | ||
130 | |||
131 | // If we're building multiple targets it's possible we've already seen this diagnostic | ||
132 | let is_duplicate = diagnostics.iter().any(|d| are_diagnostics_equal(d, &diagnostic)); | ||
133 | if is_duplicate { | ||
134 | return; | ||
135 | } | ||
136 | |||
137 | diagnostics.push(diagnostic); | ||
138 | } | ||
139 | |||
140 | fn add_suggested_fix_for_diagnostic( | ||
141 | &mut self, | ||
142 | mut suggested_fix: SuggestedFix, | ||
143 | diagnostic: &Diagnostic, | ||
144 | ) { | ||
145 | let file_uri = suggested_fix.location.uri.clone(); | ||
146 | let file_suggestions = self.suggested_fix_collection.entry(file_uri).or_default(); | ||
147 | |||
148 | let existing_suggestion: Option<&mut SuggestedFix> = | ||
149 | file_suggestions.iter_mut().find(|s| s == &&suggested_fix); | ||
150 | if let Some(existing_suggestion) = existing_suggestion { | ||
151 | // The existing suggestion also applies to this new diagnostic | ||
152 | existing_suggestion.diagnostics.push(diagnostic.clone()); | ||
153 | } else { | ||
154 | // We haven't seen this suggestion before | ||
155 | suggested_fix.diagnostics.push(diagnostic.clone()); | ||
156 | file_suggestions.push(suggested_fix); | ||
157 | } | ||
158 | } | ||
159 | } | ||
160 | |||
161 | #[derive(Debug)] | 85 | #[derive(Debug)] |
162 | pub enum CheckTask { | 86 | pub enum CheckTask { |
163 | /// Request a clearing of all cached diagnostics from the check watcher | 87 | /// Request a clearing of all cached diagnostics from the check watcher |
164 | ClearDiagnostics, | 88 | ClearDiagnostics, |
165 | 89 | ||
166 | /// Request adding a diagnostic with fixes included to a file | 90 | /// Request adding a diagnostic with fixes included to a file |
167 | AddDiagnostic(Url, DiagnosticWithFixes), | 91 | AddDiagnostic { url: Url, diagnostic: Diagnostic, fixes: Vec<CodeActionOrCommand> }, |
168 | 92 | ||
169 | /// Request check progress notification to client | 93 | /// Request check progress notification to client |
170 | Status(WorkDoneProgress), | 94 | Status(WorkDoneProgress), |
@@ -279,10 +203,17 @@ impl CheckWatcherThread { | |||
279 | None => return, | 203 | None => return, |
280 | }; | 204 | }; |
281 | 205 | ||
282 | let MappedRustDiagnostic { location, diagnostic, suggested_fixes } = map_result; | 206 | let MappedRustDiagnostic { location, diagnostic, fixes } = map_result; |
207 | let fixes = fixes | ||
208 | .into_iter() | ||
209 | .map(|fix| { | ||
210 | CodeAction { diagnostics: Some(vec![diagnostic.clone()]), ..fix }.into() | ||
211 | }) | ||
212 | .collect(); | ||
283 | 213 | ||
284 | let diagnostic = DiagnosticWithFixes { diagnostic, suggested_fixes }; | 214 | task_send |
285 | task_send.send(CheckTask::AddDiagnostic(location.uri, diagnostic)).unwrap(); | 215 | .send(CheckTask::AddDiagnostic { url: location.uri, diagnostic, fixes }) |
216 | .unwrap(); | ||
286 | } | 217 | } |
287 | 218 | ||
288 | CheckEvent::Msg(Message::BuildScriptExecuted(_msg)) => {} | 219 | CheckEvent::Msg(Message::BuildScriptExecuted(_msg)) => {} |
@@ -294,7 +225,7 @@ impl CheckWatcherThread { | |||
294 | #[derive(Debug)] | 225 | #[derive(Debug)] |
295 | pub struct DiagnosticWithFixes { | 226 | pub struct DiagnosticWithFixes { |
296 | diagnostic: Diagnostic, | 227 | diagnostic: Diagnostic, |
297 | suggested_fixes: Vec<SuggestedFix>, | 228 | fixes: Vec<CodeAction>, |
298 | } | 229 | } |
299 | 230 | ||
300 | /// WatchThread exists to wrap around the communication needed to be able to | 231 | /// WatchThread exists to wrap around the communication needed to be able to |
@@ -429,10 +360,3 @@ impl std::ops::Drop for WatchThread { | |||
429 | } | 360 | } |
430 | } | 361 | } |
431 | } | 362 | } |
432 | |||
433 | fn are_diagnostics_equal(left: &Diagnostic, right: &Diagnostic) -> bool { | ||
434 | left.source == right.source | ||
435 | && left.severity == right.severity | ||
436 | && left.range == right.range | ||
437 | && left.message == right.message | ||
438 | } | ||