Skip to content

Commit 790788d

Browse files
committed
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.
1 parent 52456c4 commit 790788d

File tree

7 files changed

+178
-208
lines changed

7 files changed

+178
-208
lines changed

crates/ra_cargo_watch/src/conv.rs

Lines changed: 21 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
//! This module provides the functionality needed to convert diagnostics from
22
//! `cargo check` json format to the LSP diagnostic format.
33
use cargo_metadata::diagnostic::{
4-
Applicability, Diagnostic as RustDiagnostic, DiagnosticLevel, DiagnosticSpan,
5-
DiagnosticSpanMacroExpansion,
4+
Diagnostic as RustDiagnostic, DiagnosticLevel, DiagnosticSpan, DiagnosticSpanMacroExpansion,
65
};
76
use lsp_types::{
8-
Diagnostic, DiagnosticRelatedInformation, DiagnosticSeverity, DiagnosticTag, Location,
9-
NumberOrString, Position, Range, Url,
7+
CodeAction, Diagnostic, DiagnosticRelatedInformation, DiagnosticSeverity, DiagnosticTag,
8+
Location, NumberOrString, Position, Range, TextEdit, Url, WorkspaceEdit,
109
};
1110
use std::{
1211
fmt::Write,
@@ -117,38 +116,9 @@ fn is_deprecated(rd: &RustDiagnostic) -> bool {
117116
}
118117
}
119118

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-
149119
enum MappedRustChildDiagnostic {
150120
Related(DiagnosticRelatedInformation),
151-
SuggestedFix(SuggestedFix),
121+
SuggestedFix(CodeAction),
152122
MessageLine(String),
153123
}
154124

@@ -176,12 +146,20 @@ fn map_rust_child_diagnostic(
176146
rd.message.clone()
177147
};
178148

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 {
180157
title,
181-
location,
182-
replacement: suggested_replacement.clone(),
183-
applicability: span.suggestion_applicability.clone().unwrap_or(Applicability::Unknown),
184-
diagnostics: vec![],
158+
kind: Some("quickfix".to_string()),
159+
diagnostics: None,
160+
edit: Some(edit),
161+
command: None,
162+
is_preferred: None,
185163
})
186164
} else {
187165
MappedRustChildDiagnostic::Related(DiagnosticRelatedInformation {
@@ -195,7 +173,7 @@ fn map_rust_child_diagnostic(
195173
pub(crate) struct MappedRustDiagnostic {
196174
pub location: Location,
197175
pub diagnostic: Diagnostic,
198-
pub suggested_fixes: Vec<SuggestedFix>,
176+
pub fixes: Vec<CodeAction>,
199177
}
200178

201179
/// Converts a Rust root diagnostic to LSP form
@@ -250,15 +228,13 @@ pub(crate) fn map_rust_diagnostic_to_lsp(
250228
}
251229
}
252230

253-
let mut suggested_fixes = vec![];
231+
let mut fixes = vec![];
254232
let mut message = rd.message.clone();
255233
for child in &rd.children {
256234
let child = map_rust_child_diagnostic(&child, workspace_root);
257235
match child {
258236
MappedRustChildDiagnostic::Related(related) => related_information.push(related),
259-
MappedRustChildDiagnostic::SuggestedFix(suggested_fix) => {
260-
suggested_fixes.push(suggested_fix)
261-
}
237+
MappedRustChildDiagnostic::SuggestedFix(code_action) => fixes.push(code_action.into()),
262238
MappedRustChildDiagnostic::MessageLine(message_line) => {
263239
write!(&mut message, "\n{}", message_line).unwrap();
264240

@@ -295,7 +271,7 @@ pub(crate) fn map_rust_diagnostic_to_lsp(
295271
tags: if !tags.is_empty() { Some(tags) } else { None },
296272
};
297273

298-
Some(MappedRustDiagnostic { location, diagnostic, suggested_fixes })
274+
Some(MappedRustDiagnostic { location, diagnostic, fixes })
299275
}
300276

301277
/// Returns a `Url` object from a given path, will lowercase drive letters if present.

crates/ra_cargo_watch/src/lib.rs

Lines changed: 17 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,20 @@
44
use cargo_metadata::Message;
55
use crossbeam_channel::{never, select, unbounded, Receiver, RecvError, Sender};
66
use lsp_types::{
7-
Diagnostic, Url, WorkDoneProgress, WorkDoneProgressBegin, WorkDoneProgressEnd,
8-
WorkDoneProgressReport,
7+
CodeAction, CodeActionOrCommand, Diagnostic, Url, WorkDoneProgress, WorkDoneProgressBegin,
8+
WorkDoneProgressEnd, WorkDoneProgressReport,
99
};
1010
use std::{
11-
collections::HashMap,
1211
io::{BufRead, BufReader},
1312
path::PathBuf,
1413
process::{Command, Stdio},
15-
sync::Arc,
1614
thread::JoinHandle,
1715
time::Instant,
1816
};
1917

2018
mod conv;
2119

22-
use crate::conv::{map_rust_diagnostic_to_lsp, MappedRustDiagnostic, SuggestedFix};
20+
use crate::conv::{map_rust_diagnostic_to_lsp, MappedRustDiagnostic};
2321

2422
pub use crate::conv::url_from_path_with_drive_lowercasing;
2523

@@ -38,29 +36,26 @@ pub struct CheckOptions {
3836
#[derive(Debug)]
3937
pub struct CheckWatcher {
4038
pub task_recv: Receiver<CheckTask>,
41-
pub state: Arc<CheckState>,
4239
cmd_send: Option<Sender<CheckCommand>>,
4340
handle: Option<JoinHandle<()>>,
4441
}
4542

4643
impl CheckWatcher {
4744
pub fn new(options: &CheckOptions, workspace_root: PathBuf) -> CheckWatcher {
4845
let options = options.clone();
49-
let state = Arc::new(CheckState::new());
5046

5147
let (task_send, task_recv) = unbounded::<CheckTask>();
5248
let (cmd_send, cmd_recv) = unbounded::<CheckCommand>();
5349
let handle = std::thread::spawn(move || {
5450
let mut check = CheckWatcherThread::new(options, workspace_root);
5551
check.run(&task_send, &cmd_recv);
5652
});
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) }
5854
}
5955

6056
/// Returns a CheckWatcher that doesn't actually do anything
6157
pub fn dummy() -> CheckWatcher {
62-
let state = Arc::new(CheckState::new());
63-
CheckWatcher { task_recv: never(), cmd_send: None, handle: None, state }
58+
CheckWatcher { task_recv: never(), cmd_send: None, handle: None }
6459
}
6560

6661
/// Schedule a re-start of the cargo check worker.
@@ -87,84 +82,13 @@ impl std::ops::Drop for CheckWatcher {
8782
}
8883
}
8984

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-
16185
#[derive(Debug)]
16286
pub enum CheckTask {
16387
/// Request a clearing of all cached diagnostics from the check watcher
16488
ClearDiagnostics,
16589

16690
/// Request adding a diagnostic with fixes included to a file
167-
AddDiagnostic(Url, DiagnosticWithFixes),
91+
AddDiagnostic { url: Url, diagnostic: Diagnostic, fixes: Vec<CodeActionOrCommand> },
16892

16993
/// Request check progress notification to client
17094
Status(WorkDoneProgress),
@@ -279,10 +203,17 @@ impl CheckWatcherThread {
279203
None => return,
280204
};
281205

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();
283213

284-
let diagnostic = DiagnosticWithFixes { diagnostic, suggested_fixes };
285-
task_send.send(CheckTask::AddDiagnostic(location.uri, diagnostic)).unwrap();
214+
task_send
215+
.send(CheckTask::AddDiagnostic { url: location.uri, diagnostic, fixes })
216+
.unwrap();
286217
}
287218

288219
CheckEvent::Msg(Message::BuildScriptExecuted(_msg)) => {}
@@ -294,7 +225,7 @@ impl CheckWatcherThread {
294225
#[derive(Debug)]
295226
pub struct DiagnosticWithFixes {
296227
diagnostic: Diagnostic,
297-
suggested_fixes: Vec<SuggestedFix>,
228+
fixes: Vec<CodeAction>,
298229
}
299230

300231
/// WatchThread exists to wrap around the communication needed to be able to
@@ -429,10 +360,3 @@ impl std::ops::Drop for WatchThread {
429360
}
430361
}
431362
}
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-
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
//! Book keeping for keeping diagnostics easily in sync with the client.
2+
use lsp_types::{CodeActionOrCommand, Diagnostic, Range};
3+
use ra_ide::FileId;
4+
use std::{collections::HashMap, sync::Arc};
5+
6+
pub type CheckFixes = Arc<HashMap<FileId, Vec<Fix>>>;
7+
8+
#[derive(Debug, Default, Clone)]
9+
pub struct DiagnosticCollection {
10+
pub native: HashMap<FileId, Vec<Diagnostic>>,
11+
pub check: HashMap<FileId, Vec<Diagnostic>>,
12+
pub check_fixes: CheckFixes,
13+
}
14+
15+
#[derive(Debug, Clone)]
16+
pub struct Fix {
17+
pub range: Range,
18+
pub action: CodeActionOrCommand,
19+
}
20+
21+
#[derive(Debug)]
22+
pub enum DiagnosticTask {
23+
ClearCheck,
24+
AddCheck(FileId, Diagnostic, Vec<CodeActionOrCommand>),
25+
SetNative(FileId, Vec<Diagnostic>),
26+
}
27+
28+
impl DiagnosticCollection {
29+
pub fn clear_check(&mut self) -> Vec<FileId> {
30+
Arc::make_mut(&mut self.check_fixes).clear();
31+
self.check.drain().map(|(key, _value)| key).collect()
32+
}
33+
34+
pub fn add_check_diagnostic(
35+
&mut self,
36+
file_id: FileId,
37+
diagnostic: Diagnostic,
38+
fixes: Vec<CodeActionOrCommand>,
39+
) {
40+
let diagnostics = self.check.entry(file_id).or_default();
41+
for existing_diagnostic in diagnostics.iter() {
42+
if are_diagnostics_equal(&existing_diagnostic, &diagnostic) {
43+
return;
44+
}
45+
}
46+
47+
let check_fixes = Arc::make_mut(&mut self.check_fixes);
48+
check_fixes
49+
.entry(file_id)
50+
.or_default()
51+
.extend(fixes.into_iter().map(|action| Fix { range: diagnostic.range, action }));
52+
diagnostics.push(diagnostic);
53+
}
54+
55+
pub fn set_native_diagnostics(&mut self, file_id: FileId, diagnostics: Vec<Diagnostic>) {
56+
self.native.insert(file_id, diagnostics);
57+
}
58+
59+
pub fn diagnostics_for(&self, file_id: FileId) -> impl Iterator<Item = &Diagnostic> {
60+
let native = self.native.get(&file_id).into_iter().flatten();
61+
let check = self.check.get(&file_id).into_iter().flatten();
62+
native.chain(check)
63+
}
64+
65+
pub fn handle_task(&mut self, task: DiagnosticTask) -> Vec<FileId> {
66+
match task {
67+
DiagnosticTask::ClearCheck => self.clear_check(),
68+
DiagnosticTask::AddCheck(file_id, diagnostic, fixes) => {
69+
self.add_check_diagnostic(file_id, diagnostic, fixes);
70+
vec![file_id]
71+
}
72+
DiagnosticTask::SetNative(file_id, diagnostics) => {
73+
self.set_native_diagnostics(file_id, diagnostics);
74+
vec![file_id]
75+
}
76+
}
77+
}
78+
}
79+
80+
fn are_diagnostics_equal(left: &Diagnostic, right: &Diagnostic) -> bool {
81+
left.source == right.source
82+
&& left.severity == right.severity
83+
&& left.range == right.range
84+
&& left.message == right.message
85+
}

crates/ra_lsp_server/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ mod markdown;
2929
pub mod req;
3030
mod config;
3131
mod world;
32+
mod diagnostics;
3233

3334
pub type Result<T> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>;
3435
pub use crate::{

0 commit comments

Comments
 (0)