diff options
author | Emil Lauridsen <[email protected]> | 2019-12-25 11:21:38 +0000 |
---|---|---|
committer | Emil Lauridsen <[email protected]> | 2019-12-25 16:37:40 +0000 |
commit | 66e8ef53a0ed018d03340577a0443030a193f773 (patch) | |
tree | ce35fbd25ac7bb3b7374dccbb79d89545d9904a7 /crates/ra_lsp_server | |
parent | 52b44ba7edbdb64a30b781292eaaea59e8c2490d (diff) |
Initial implementation of cargo check watching
Diffstat (limited to 'crates/ra_lsp_server')
-rw-r--r-- | crates/ra_lsp_server/Cargo.toml | 1 | ||||
-rw-r--r-- | crates/ra_lsp_server/src/caps.rs | 4 | ||||
-rw-r--r-- | crates/ra_lsp_server/src/cargo_check.rs | 533 | ||||
-rw-r--r-- | crates/ra_lsp_server/src/lib.rs | 1 | ||||
-rw-r--r-- | crates/ra_lsp_server/src/main_loop.rs | 27 | ||||
-rw-r--r-- | crates/ra_lsp_server/src/main_loop/handlers.rs | 28 | ||||
-rw-r--r-- | crates/ra_lsp_server/src/world.rs | 8 |
7 files changed, 598 insertions, 4 deletions
diff --git a/crates/ra_lsp_server/Cargo.toml b/crates/ra_lsp_server/Cargo.toml index 030e9033c..aa1acdc33 100644 --- a/crates/ra_lsp_server/Cargo.toml +++ b/crates/ra_lsp_server/Cargo.toml | |||
@@ -27,6 +27,7 @@ ra_project_model = { path = "../ra_project_model" } | |||
27 | ra_prof = { path = "../ra_prof" } | 27 | ra_prof = { path = "../ra_prof" } |
28 | ra_vfs_glob = { path = "../ra_vfs_glob" } | 28 | ra_vfs_glob = { path = "../ra_vfs_glob" } |
29 | env_logger = { version = "0.7.1", default-features = false, features = ["humantime"] } | 29 | env_logger = { version = "0.7.1", default-features = false, features = ["humantime"] } |
30 | cargo_metadata = "0.9.1" | ||
30 | 31 | ||
31 | [dev-dependencies] | 32 | [dev-dependencies] |
32 | tempfile = "3" | 33 | tempfile = "3" |
diff --git a/crates/ra_lsp_server/src/caps.rs b/crates/ra_lsp_server/src/caps.rs index ceb4c4259..0f84e7a34 100644 --- a/crates/ra_lsp_server/src/caps.rs +++ b/crates/ra_lsp_server/src/caps.rs | |||
@@ -6,7 +6,7 @@ use lsp_types::{ | |||
6 | ImplementationProviderCapability, RenameOptions, RenameProviderCapability, | 6 | ImplementationProviderCapability, RenameOptions, RenameProviderCapability, |
7 | SelectionRangeProviderCapability, ServerCapabilities, SignatureHelpOptions, | 7 | SelectionRangeProviderCapability, ServerCapabilities, SignatureHelpOptions, |
8 | TextDocumentSyncCapability, TextDocumentSyncKind, TextDocumentSyncOptions, | 8 | TextDocumentSyncCapability, TextDocumentSyncKind, TextDocumentSyncOptions, |
9 | TypeDefinitionProviderCapability, WorkDoneProgressOptions, | 9 | TypeDefinitionProviderCapability, WorkDoneProgressOptions, SaveOptions |
10 | }; | 10 | }; |
11 | 11 | ||
12 | pub fn server_capabilities() -> ServerCapabilities { | 12 | pub fn server_capabilities() -> ServerCapabilities { |
@@ -16,7 +16,7 @@ pub fn server_capabilities() -> ServerCapabilities { | |||
16 | change: Some(TextDocumentSyncKind::Full), | 16 | change: Some(TextDocumentSyncKind::Full), |
17 | will_save: None, | 17 | will_save: None, |
18 | will_save_wait_until: None, | 18 | will_save_wait_until: None, |
19 | save: None, | 19 | save: Some(SaveOptions::default()), |
20 | })), | 20 | })), |
21 | hover_provider: Some(true), | 21 | hover_provider: Some(true), |
22 | completion_provider: Some(CompletionOptions { | 22 | completion_provider: Some(CompletionOptions { |
diff --git a/crates/ra_lsp_server/src/cargo_check.rs b/crates/ra_lsp_server/src/cargo_check.rs new file mode 100644 index 000000000..d5ff02154 --- /dev/null +++ b/crates/ra_lsp_server/src/cargo_check.rs | |||
@@ -0,0 +1,533 @@ | |||
1 | use cargo_metadata::{ | ||
2 | diagnostic::{ | ||
3 | Applicability, Diagnostic as RustDiagnostic, DiagnosticLevel, DiagnosticSpan, | ||
4 | DiagnosticSpanMacroExpansion, | ||
5 | }, | ||
6 | Message, | ||
7 | }; | ||
8 | use crossbeam_channel::{select, unbounded, Receiver, RecvError, Sender, TryRecvError}; | ||
9 | use lsp_types::{ | ||
10 | Diagnostic, DiagnosticRelatedInformation, DiagnosticSeverity, DiagnosticTag, Location, | ||
11 | NumberOrString, Position, Range, Url, | ||
12 | }; | ||
13 | use parking_lot::RwLock; | ||
14 | use std::{ | ||
15 | collections::HashMap, | ||
16 | fmt::Write, | ||
17 | path::PathBuf, | ||
18 | process::{Command, Stdio}, | ||
19 | sync::Arc, | ||
20 | thread::JoinHandle, | ||
21 | time::Instant, | ||
22 | }; | ||
23 | |||
24 | #[derive(Debug)] | ||
25 | pub struct CheckWatcher { | ||
26 | pub task_recv: Receiver<CheckTask>, | ||
27 | pub cmd_send: Sender<CheckCommand>, | ||
28 | pub shared: Arc<RwLock<CheckWatcherSharedState>>, | ||
29 | handle: JoinHandle<()>, | ||
30 | } | ||
31 | |||
32 | impl CheckWatcher { | ||
33 | pub fn new(workspace_root: PathBuf) -> CheckWatcher { | ||
34 | let shared = Arc::new(RwLock::new(CheckWatcherSharedState::new())); | ||
35 | |||
36 | let (task_send, task_recv) = unbounded::<CheckTask>(); | ||
37 | let (cmd_send, cmd_recv) = unbounded::<CheckCommand>(); | ||
38 | let shared_ = shared.clone(); | ||
39 | let handle = std::thread::spawn(move || { | ||
40 | let mut check = CheckWatcherState::new(shared_, workspace_root); | ||
41 | check.run(&task_send, &cmd_recv); | ||
42 | }); | ||
43 | |||
44 | CheckWatcher { task_recv, cmd_send, handle, shared } | ||
45 | } | ||
46 | |||
47 | pub fn update(&self) { | ||
48 | self.cmd_send.send(CheckCommand::Update).unwrap(); | ||
49 | } | ||
50 | } | ||
51 | |||
52 | pub struct CheckWatcherState { | ||
53 | workspace_root: PathBuf, | ||
54 | running: bool, | ||
55 | watcher: WatchThread, | ||
56 | last_update_req: Option<Instant>, | ||
57 | shared: Arc<RwLock<CheckWatcherSharedState>>, | ||
58 | } | ||
59 | |||
60 | #[derive(Debug)] | ||
61 | pub struct CheckWatcherSharedState { | ||
62 | diagnostic_collection: HashMap<Url, Vec<Diagnostic>>, | ||
63 | suggested_fix_collection: HashMap<Url, Vec<SuggestedFix>>, | ||
64 | } | ||
65 | |||
66 | impl CheckWatcherSharedState { | ||
67 | fn new() -> CheckWatcherSharedState { | ||
68 | CheckWatcherSharedState { | ||
69 | diagnostic_collection: HashMap::new(), | ||
70 | suggested_fix_collection: HashMap::new(), | ||
71 | } | ||
72 | } | ||
73 | |||
74 | pub fn clear(&mut self, task_send: &Sender<CheckTask>) { | ||
75 | let cleared_files: Vec<Url> = self.diagnostic_collection.keys().cloned().collect(); | ||
76 | |||
77 | self.diagnostic_collection.clear(); | ||
78 | self.suggested_fix_collection.clear(); | ||
79 | |||
80 | for uri in cleared_files { | ||
81 | task_send.send(CheckTask::Update(uri.clone())).unwrap(); | ||
82 | } | ||
83 | } | ||
84 | |||
85 | pub fn diagnostics_for(&self, uri: &Url) -> Option<&[Diagnostic]> { | ||
86 | self.diagnostic_collection.get(uri).map(|d| d.as_slice()) | ||
87 | } | ||
88 | |||
89 | pub fn fixes_for(&self, uri: &Url) -> Option<&[SuggestedFix]> { | ||
90 | self.suggested_fix_collection.get(uri).map(|d| d.as_slice()) | ||
91 | } | ||
92 | |||
93 | fn add_diagnostic(&mut self, file_uri: Url, diagnostic: Diagnostic) { | ||
94 | let diagnostics = self.diagnostic_collection.entry(file_uri).or_default(); | ||
95 | |||
96 | // If we're building multiple targets it's possible we've already seen this diagnostic | ||
97 | let is_duplicate = diagnostics.iter().any(|d| are_diagnostics_equal(d, &diagnostic)); | ||
98 | if is_duplicate { | ||
99 | return; | ||
100 | } | ||
101 | |||
102 | diagnostics.push(diagnostic); | ||
103 | } | ||
104 | |||
105 | fn add_suggested_fix_for_diagnostic( | ||
106 | &mut self, | ||
107 | mut suggested_fix: SuggestedFix, | ||
108 | diagnostic: &Diagnostic, | ||
109 | ) { | ||
110 | let file_uri = suggested_fix.location.uri.clone(); | ||
111 | let file_suggestions = self.suggested_fix_collection.entry(file_uri).or_default(); | ||
112 | |||
113 | let existing_suggestion: Option<&mut SuggestedFix> = | ||
114 | file_suggestions.iter_mut().find(|s| s == &&suggested_fix); | ||
115 | if let Some(existing_suggestion) = existing_suggestion { | ||
116 | // The existing suggestion also applies to this new diagnostic | ||
117 | existing_suggestion.diagnostics.push(diagnostic.clone()); | ||
118 | } else { | ||
119 | // We haven't seen this suggestion before | ||
120 | suggested_fix.diagnostics.push(diagnostic.clone()); | ||
121 | file_suggestions.push(suggested_fix); | ||
122 | } | ||
123 | } | ||
124 | } | ||
125 | |||
126 | #[derive(Debug)] | ||
127 | pub enum CheckTask { | ||
128 | Update(Url), | ||
129 | } | ||
130 | |||
131 | pub enum CheckCommand { | ||
132 | Update, | ||
133 | } | ||
134 | |||
135 | impl CheckWatcherState { | ||
136 | pub fn new( | ||
137 | shared: Arc<RwLock<CheckWatcherSharedState>>, | ||
138 | workspace_root: PathBuf, | ||
139 | ) -> CheckWatcherState { | ||
140 | let watcher = WatchThread::new(&workspace_root); | ||
141 | CheckWatcherState { workspace_root, running: false, watcher, last_update_req: None, shared } | ||
142 | } | ||
143 | |||
144 | pub fn run(&mut self, task_send: &Sender<CheckTask>, cmd_recv: &Receiver<CheckCommand>) { | ||
145 | self.running = true; | ||
146 | while self.running { | ||
147 | select! { | ||
148 | recv(&cmd_recv) -> cmd => match cmd { | ||
149 | Ok(cmd) => self.handle_command(cmd), | ||
150 | Err(RecvError) => { | ||
151 | // Command channel has closed, so shut down | ||
152 | self.running = false; | ||
153 | }, | ||
154 | }, | ||
155 | recv(self.watcher.message_recv) -> msg => match msg { | ||
156 | Ok(msg) => self.handle_message(msg, task_send), | ||
157 | Err(RecvError) => {}, | ||
158 | } | ||
159 | }; | ||
160 | |||
161 | if self.should_recheck() { | ||
162 | self.last_update_req.take(); | ||
163 | self.shared.write().clear(task_send); | ||
164 | |||
165 | self.watcher.cancel(); | ||
166 | self.watcher = WatchThread::new(&self.workspace_root); | ||
167 | } | ||
168 | } | ||
169 | } | ||
170 | |||
171 | fn should_recheck(&mut self) -> bool { | ||
172 | if let Some(_last_update_req) = &self.last_update_req { | ||
173 | // We currently only request an update on save, as we need up to | ||
174 | // date source on disk for cargo check to do it's magic, so we | ||
175 | // don't really need to debounce the requests at this point. | ||
176 | return true; | ||
177 | } | ||
178 | false | ||
179 | } | ||
180 | |||
181 | fn handle_command(&mut self, cmd: CheckCommand) { | ||
182 | match cmd { | ||
183 | CheckCommand::Update => self.last_update_req = Some(Instant::now()), | ||
184 | } | ||
185 | } | ||
186 | |||
187 | fn handle_message(&mut self, msg: cargo_metadata::Message, task_send: &Sender<CheckTask>) { | ||
188 | match msg { | ||
189 | Message::CompilerArtifact(_msg) => { | ||
190 | // TODO: Status display | ||
191 | } | ||
192 | |||
193 | Message::CompilerMessage(msg) => { | ||
194 | let map_result = | ||
195 | match map_rust_diagnostic_to_lsp(&msg.message, &self.workspace_root) { | ||
196 | Some(map_result) => map_result, | ||
197 | None => return, | ||
198 | }; | ||
199 | |||
200 | let MappedRustDiagnostic { location, diagnostic, suggested_fixes } = map_result; | ||
201 | let file_uri = location.uri.clone(); | ||
202 | |||
203 | if !suggested_fixes.is_empty() { | ||
204 | for suggested_fix in suggested_fixes { | ||
205 | self.shared | ||
206 | .write() | ||
207 | .add_suggested_fix_for_diagnostic(suggested_fix, &diagnostic); | ||
208 | } | ||
209 | } | ||
210 | self.shared.write().add_diagnostic(file_uri, diagnostic); | ||
211 | |||
212 | task_send.send(CheckTask::Update(location.uri)).unwrap(); | ||
213 | } | ||
214 | |||
215 | Message::BuildScriptExecuted(_msg) => {} | ||
216 | Message::Unknown => {} | ||
217 | } | ||
218 | } | ||
219 | } | ||
220 | |||
221 | /// WatchThread exists to wrap around the communication needed to be able to | ||
222 | /// run `cargo check` without blocking. Currently the Rust standard library | ||
223 | /// doesn't provide a way to read sub-process output without blocking, so we | ||
224 | /// have to wrap sub-processes output handling in a thread and pass messages | ||
225 | /// back over a channel. | ||
226 | struct WatchThread { | ||
227 | message_recv: Receiver<cargo_metadata::Message>, | ||
228 | cancel_send: Sender<()>, | ||
229 | } | ||
230 | |||
231 | impl WatchThread { | ||
232 | fn new(workspace_root: &PathBuf) -> WatchThread { | ||
233 | let manifest_path = format!("{}/Cargo.toml", workspace_root.to_string_lossy()); | ||
234 | let (message_send, message_recv) = unbounded(); | ||
235 | let (cancel_send, cancel_recv) = unbounded(); | ||
236 | std::thread::spawn(move || { | ||
237 | let mut command = Command::new("cargo") | ||
238 | .args(&["check", "--message-format=json", "--manifest-path", &manifest_path]) | ||
239 | .stdout(Stdio::piped()) | ||
240 | .stderr(Stdio::null()) | ||
241 | .spawn() | ||
242 | .expect("couldn't launch cargo"); | ||
243 | |||
244 | for message in cargo_metadata::parse_messages(command.stdout.take().unwrap()) { | ||
245 | match cancel_recv.try_recv() { | ||
246 | Ok(()) | Err(TryRecvError::Disconnected) => { | ||
247 | command.kill().expect("couldn't kill command"); | ||
248 | } | ||
249 | Err(TryRecvError::Empty) => (), | ||
250 | } | ||
251 | |||
252 | message_send.send(message.unwrap()).unwrap(); | ||
253 | } | ||
254 | }); | ||
255 | WatchThread { message_recv, cancel_send } | ||
256 | } | ||
257 | |||
258 | fn cancel(&self) { | ||
259 | let _ = self.cancel_send.send(()); | ||
260 | } | ||
261 | } | ||
262 | |||
263 | /// Converts a Rust level string to a LSP severity | ||
264 | fn map_level_to_severity(val: DiagnosticLevel) -> Option<DiagnosticSeverity> { | ||
265 | match val { | ||
266 | DiagnosticLevel::Ice => Some(DiagnosticSeverity::Error), | ||
267 | DiagnosticLevel::Error => Some(DiagnosticSeverity::Error), | ||
268 | DiagnosticLevel::Warning => Some(DiagnosticSeverity::Warning), | ||
269 | DiagnosticLevel::Note => Some(DiagnosticSeverity::Information), | ||
270 | DiagnosticLevel::Help => Some(DiagnosticSeverity::Hint), | ||
271 | DiagnosticLevel::Unknown => None, | ||
272 | } | ||
273 | } | ||
274 | |||
275 | /// Check whether a file name is from macro invocation | ||
276 | fn is_from_macro(file_name: &str) -> bool { | ||
277 | file_name.starts_with('<') && file_name.ends_with('>') | ||
278 | } | ||
279 | |||
280 | /// Converts a Rust macro span to a LSP location recursively | ||
281 | fn map_macro_span_to_location( | ||
282 | span_macro: &DiagnosticSpanMacroExpansion, | ||
283 | workspace_root: &PathBuf, | ||
284 | ) -> Option<Location> { | ||
285 | if !is_from_macro(&span_macro.span.file_name) { | ||
286 | return Some(map_span_to_location(&span_macro.span, workspace_root)); | ||
287 | } | ||
288 | |||
289 | if let Some(expansion) = &span_macro.span.expansion { | ||
290 | return map_macro_span_to_location(&expansion, workspace_root); | ||
291 | } | ||
292 | |||
293 | None | ||
294 | } | ||
295 | |||
296 | /// Converts a Rust span to a LSP location | ||
297 | fn map_span_to_location(span: &DiagnosticSpan, workspace_root: &PathBuf) -> Location { | ||
298 | if is_from_macro(&span.file_name) && span.expansion.is_some() { | ||
299 | let expansion = span.expansion.as_ref().unwrap(); | ||
300 | if let Some(macro_range) = map_macro_span_to_location(&expansion, workspace_root) { | ||
301 | return macro_range; | ||
302 | } | ||
303 | } | ||
304 | |||
305 | let mut file_name = workspace_root.clone(); | ||
306 | file_name.push(&span.file_name); | ||
307 | let uri = Url::from_file_path(file_name).unwrap(); | ||
308 | |||
309 | let range = Range::new( | ||
310 | Position::new(span.line_start as u64 - 1, span.column_start as u64 - 1), | ||
311 | Position::new(span.line_end as u64 - 1, span.column_end as u64 - 1), | ||
312 | ); | ||
313 | |||
314 | Location { uri, range } | ||
315 | } | ||
316 | |||
317 | /// Converts a secondary Rust span to a LSP related information | ||
318 | /// | ||
319 | /// If the span is unlabelled this will return `None`. | ||
320 | fn map_secondary_span_to_related( | ||
321 | span: &DiagnosticSpan, | ||
322 | workspace_root: &PathBuf, | ||
323 | ) -> Option<DiagnosticRelatedInformation> { | ||
324 | if let Some(label) = &span.label { | ||
325 | let location = map_span_to_location(span, workspace_root); | ||
326 | Some(DiagnosticRelatedInformation { location, message: label.clone() }) | ||
327 | } else { | ||
328 | // Nothing to label this with | ||
329 | None | ||
330 | } | ||
331 | } | ||
332 | |||
333 | /// Determines if diagnostic is related to unused code | ||
334 | fn is_unused_or_unnecessary(rd: &RustDiagnostic) -> bool { | ||
335 | if let Some(code) = &rd.code { | ||
336 | match code.code.as_str() { | ||
337 | "dead_code" | "unknown_lints" | "unreachable_code" | "unused_attributes" | ||
338 | | "unused_imports" | "unused_macros" | "unused_variables" => true, | ||
339 | _ => false, | ||
340 | } | ||
341 | } else { | ||
342 | false | ||
343 | } | ||
344 | } | ||
345 | |||
346 | /// Determines if diagnostic is related to deprecated code | ||
347 | fn is_deprecated(rd: &RustDiagnostic) -> bool { | ||
348 | if let Some(code) = &rd.code { | ||
349 | match code.code.as_str() { | ||
350 | "deprecated" => true, | ||
351 | _ => false, | ||
352 | } | ||
353 | } else { | ||
354 | false | ||
355 | } | ||
356 | } | ||
357 | |||
358 | #[derive(Debug)] | ||
359 | pub struct SuggestedFix { | ||
360 | pub title: String, | ||
361 | pub location: Location, | ||
362 | pub replacement: String, | ||
363 | pub applicability: Applicability, | ||
364 | pub diagnostics: Vec<Diagnostic>, | ||
365 | } | ||
366 | |||
367 | impl std::cmp::PartialEq<SuggestedFix> for SuggestedFix { | ||
368 | fn eq(&self, other: &SuggestedFix) -> bool { | ||
369 | if self.title == other.title | ||
370 | && self.location == other.location | ||
371 | && self.replacement == other.replacement | ||
372 | { | ||
373 | // Applicability doesn't impl PartialEq... | ||
374 | match (&self.applicability, &other.applicability) { | ||
375 | (Applicability::MachineApplicable, Applicability::MachineApplicable) => true, | ||
376 | (Applicability::HasPlaceholders, Applicability::HasPlaceholders) => true, | ||
377 | (Applicability::MaybeIncorrect, Applicability::MaybeIncorrect) => true, | ||
378 | (Applicability::Unspecified, Applicability::Unspecified) => true, | ||
379 | _ => false, | ||
380 | } | ||
381 | } else { | ||
382 | false | ||
383 | } | ||
384 | } | ||
385 | } | ||
386 | |||
387 | enum MappedRustChildDiagnostic { | ||
388 | Related(DiagnosticRelatedInformation), | ||
389 | SuggestedFix(SuggestedFix), | ||
390 | MessageLine(String), | ||
391 | } | ||
392 | |||
393 | fn map_rust_child_diagnostic( | ||
394 | rd: &RustDiagnostic, | ||
395 | workspace_root: &PathBuf, | ||
396 | ) -> MappedRustChildDiagnostic { | ||
397 | let span: &DiagnosticSpan = match rd.spans.iter().find(|s| s.is_primary) { | ||
398 | Some(span) => span, | ||
399 | None => { | ||
400 | // `rustc` uses these spanless children as a way to print multi-line | ||
401 | // messages | ||
402 | return MappedRustChildDiagnostic::MessageLine(rd.message.clone()); | ||
403 | } | ||
404 | }; | ||
405 | |||
406 | // If we have a primary span use its location, otherwise use the parent | ||
407 | let location = map_span_to_location(&span, workspace_root); | ||
408 | |||
409 | if let Some(suggested_replacement) = &span.suggested_replacement { | ||
410 | // Include our replacement in the title unless it's empty | ||
411 | let title = if !suggested_replacement.is_empty() { | ||
412 | format!("{}: '{}'", rd.message, suggested_replacement) | ||
413 | } else { | ||
414 | rd.message.clone() | ||
415 | }; | ||
416 | |||
417 | MappedRustChildDiagnostic::SuggestedFix(SuggestedFix { | ||
418 | title, | ||
419 | location, | ||
420 | replacement: suggested_replacement.clone(), | ||
421 | applicability: span.suggestion_applicability.clone().unwrap_or(Applicability::Unknown), | ||
422 | diagnostics: vec![], | ||
423 | }) | ||
424 | } else { | ||
425 | MappedRustChildDiagnostic::Related(DiagnosticRelatedInformation { | ||
426 | location, | ||
427 | message: rd.message.clone(), | ||
428 | }) | ||
429 | } | ||
430 | } | ||
431 | |||
432 | struct MappedRustDiagnostic { | ||
433 | location: Location, | ||
434 | diagnostic: Diagnostic, | ||
435 | suggested_fixes: Vec<SuggestedFix>, | ||
436 | } | ||
437 | |||
438 | /// Converts a Rust root diagnostic to LSP form | ||
439 | /// | ||
440 | /// This flattens the Rust diagnostic by: | ||
441 | /// | ||
442 | /// 1. Creating a LSP diagnostic with the root message and primary span. | ||
443 | /// 2. Adding any labelled secondary spans to `relatedInformation` | ||
444 | /// 3. Categorising child diagnostics as either `SuggestedFix`es, | ||
445 | /// `relatedInformation` or additional message lines. | ||
446 | /// | ||
447 | /// If the diagnostic has no primary span this will return `None` | ||
448 | fn map_rust_diagnostic_to_lsp( | ||
449 | rd: &RustDiagnostic, | ||
450 | workspace_root: &PathBuf, | ||
451 | ) -> Option<MappedRustDiagnostic> { | ||
452 | let primary_span = rd.spans.iter().find(|s| s.is_primary)?; | ||
453 | |||
454 | let location = map_span_to_location(&primary_span, workspace_root); | ||
455 | |||
456 | let severity = map_level_to_severity(rd.level); | ||
457 | let mut primary_span_label = primary_span.label.as_ref(); | ||
458 | |||
459 | let mut source = String::from("rustc"); | ||
460 | let mut code = rd.code.as_ref().map(|c| c.code.clone()); | ||
461 | if let Some(code_val) = &code { | ||
462 | // See if this is an RFC #2103 scoped lint (e.g. from Clippy) | ||
463 | let scoped_code: Vec<&str> = code_val.split("::").collect(); | ||
464 | if scoped_code.len() == 2 { | ||
465 | source = String::from(scoped_code[0]); | ||
466 | code = Some(String::from(scoped_code[1])); | ||
467 | } | ||
468 | } | ||
469 | |||
470 | let mut related_information = vec![]; | ||
471 | let mut tags = vec![]; | ||
472 | |||
473 | for secondary_span in rd.spans.iter().filter(|s| !s.is_primary) { | ||
474 | let related = map_secondary_span_to_related(secondary_span, workspace_root); | ||
475 | if let Some(related) = related { | ||
476 | related_information.push(related); | ||
477 | } | ||
478 | } | ||
479 | |||
480 | let mut suggested_fixes = vec![]; | ||
481 | let mut message = rd.message.clone(); | ||
482 | for child in &rd.children { | ||
483 | let child = map_rust_child_diagnostic(&child, workspace_root); | ||
484 | match child { | ||
485 | MappedRustChildDiagnostic::Related(related) => related_information.push(related), | ||
486 | MappedRustChildDiagnostic::SuggestedFix(suggested_fix) => { | ||
487 | suggested_fixes.push(suggested_fix) | ||
488 | } | ||
489 | MappedRustChildDiagnostic::MessageLine(message_line) => { | ||
490 | write!(&mut message, "\n{}", message_line).unwrap(); | ||
491 | |||
492 | // These secondary messages usually duplicate the content of the | ||
493 | // primary span label. | ||
494 | primary_span_label = None; | ||
495 | } | ||
496 | } | ||
497 | } | ||
498 | |||
499 | if let Some(primary_span_label) = primary_span_label { | ||
500 | write!(&mut message, "\n{}", primary_span_label).unwrap(); | ||
501 | } | ||
502 | |||
503 | if is_unused_or_unnecessary(rd) { | ||
504 | tags.push(DiagnosticTag::Unnecessary); | ||
505 | } | ||
506 | |||
507 | if is_deprecated(rd) { | ||
508 | tags.push(DiagnosticTag::Deprecated); | ||
509 | } | ||
510 | |||
511 | let diagnostic = Diagnostic { | ||
512 | range: location.range, | ||
513 | severity, | ||
514 | code: code.map(NumberOrString::String), | ||
515 | source: Some(source), | ||
516 | message: rd.message.clone(), | ||
517 | related_information: if !related_information.is_empty() { | ||
518 | Some(related_information) | ||
519 | } else { | ||
520 | None | ||
521 | }, | ||
522 | tags: if !tags.is_empty() { Some(tags) } else { None }, | ||
523 | }; | ||
524 | |||
525 | Some(MappedRustDiagnostic { location, diagnostic, suggested_fixes }) | ||
526 | } | ||
527 | |||
528 | fn are_diagnostics_equal(left: &Diagnostic, right: &Diagnostic) -> bool { | ||
529 | left.source == right.source | ||
530 | && left.severity == right.severity | ||
531 | && left.range == right.range | ||
532 | && left.message == right.message | ||
533 | } | ||
diff --git a/crates/ra_lsp_server/src/lib.rs b/crates/ra_lsp_server/src/lib.rs index 2ca149fd5..2811231fa 100644 --- a/crates/ra_lsp_server/src/lib.rs +++ b/crates/ra_lsp_server/src/lib.rs | |||
@@ -22,6 +22,7 @@ macro_rules! print { | |||
22 | } | 22 | } |
23 | 23 | ||
24 | mod caps; | 24 | mod caps; |
25 | mod cargo_check; | ||
25 | mod cargo_target_spec; | 26 | mod cargo_target_spec; |
26 | mod conv; | 27 | mod conv; |
27 | mod main_loop; | 28 | mod main_loop; |
diff --git a/crates/ra_lsp_server/src/main_loop.rs b/crates/ra_lsp_server/src/main_loop.rs index dda318e43..943d38943 100644 --- a/crates/ra_lsp_server/src/main_loop.rs +++ b/crates/ra_lsp_server/src/main_loop.rs | |||
@@ -19,6 +19,7 @@ use serde::{de::DeserializeOwned, Serialize}; | |||
19 | use threadpool::ThreadPool; | 19 | use threadpool::ThreadPool; |
20 | 20 | ||
21 | use crate::{ | 21 | use crate::{ |
22 | cargo_check::CheckTask, | ||
22 | main_loop::{ | 23 | main_loop::{ |
23 | pending_requests::{PendingRequest, PendingRequests}, | 24 | pending_requests::{PendingRequest, PendingRequests}, |
24 | subscriptions::Subscriptions, | 25 | subscriptions::Subscriptions, |
@@ -176,7 +177,8 @@ pub fn main_loop( | |||
176 | Ok(task) => Event::Vfs(task), | 177 | Ok(task) => Event::Vfs(task), |
177 | Err(RecvError) => Err("vfs died")?, | 178 | Err(RecvError) => Err("vfs died")?, |
178 | }, | 179 | }, |
179 | recv(libdata_receiver) -> data => Event::Lib(data.unwrap()) | 180 | recv(libdata_receiver) -> data => Event::Lib(data.unwrap()), |
181 | recv(world_state.check_watcher.task_recv) -> task => Event::CheckWatcher(task.unwrap()) | ||
180 | }; | 182 | }; |
181 | if let Event::Msg(Message::Request(req)) = &event { | 183 | if let Event::Msg(Message::Request(req)) = &event { |
182 | if connection.handle_shutdown(&req)? { | 184 | if connection.handle_shutdown(&req)? { |
@@ -222,6 +224,7 @@ enum Event { | |||
222 | Task(Task), | 224 | Task(Task), |
223 | Vfs(VfsTask), | 225 | Vfs(VfsTask), |
224 | Lib(LibraryData), | 226 | Lib(LibraryData), |
227 | CheckWatcher(CheckTask), | ||
225 | } | 228 | } |
226 | 229 | ||
227 | impl fmt::Debug for Event { | 230 | impl fmt::Debug for Event { |
@@ -259,6 +262,7 @@ impl fmt::Debug for Event { | |||
259 | Event::Task(it) => fmt::Debug::fmt(it, f), | 262 | Event::Task(it) => fmt::Debug::fmt(it, f), |
260 | Event::Vfs(it) => fmt::Debug::fmt(it, f), | 263 | Event::Vfs(it) => fmt::Debug::fmt(it, f), |
261 | Event::Lib(it) => fmt::Debug::fmt(it, f), | 264 | Event::Lib(it) => fmt::Debug::fmt(it, f), |
265 | Event::CheckWatcher(it) => fmt::Debug::fmt(it, f), | ||
262 | } | 266 | } |
263 | } | 267 | } |
264 | } | 268 | } |
@@ -318,6 +322,20 @@ fn loop_turn( | |||
318 | world_state.maybe_collect_garbage(); | 322 | world_state.maybe_collect_garbage(); |
319 | loop_state.in_flight_libraries -= 1; | 323 | loop_state.in_flight_libraries -= 1; |
320 | } | 324 | } |
325 | Event::CheckWatcher(task) => match task { | ||
326 | CheckTask::Update(uri) => { | ||
327 | // We manually send a diagnostic update when the watcher asks | ||
328 | // us to, to avoid the issue of having to change the file to | ||
329 | // receive updated diagnostics. | ||
330 | let path = uri.to_file_path().map_err(|()| format!("invalid uri: {}", uri))?; | ||
331 | if let Some(file_id) = world_state.vfs.read().path2file(&path) { | ||
332 | let params = | ||
333 | handlers::publish_diagnostics(&world_state.snapshot(), FileId(file_id.0))?; | ||
334 | let not = notification_new::<req::PublishDiagnostics>(params); | ||
335 | task_sender.send(Task::Notify(not)).unwrap(); | ||
336 | } | ||
337 | } | ||
338 | }, | ||
321 | Event::Msg(msg) => match msg { | 339 | Event::Msg(msg) => match msg { |
322 | Message::Request(req) => on_request( | 340 | Message::Request(req) => on_request( |
323 | world_state, | 341 | world_state, |
@@ -517,6 +535,13 @@ fn on_notification( | |||
517 | } | 535 | } |
518 | Err(not) => not, | 536 | Err(not) => not, |
519 | }; | 537 | }; |
538 | let not = match notification_cast::<req::DidSaveTextDocument>(not) { | ||
539 | Ok(_params) => { | ||
540 | state.check_watcher.update(); | ||
541 | return Ok(()); | ||
542 | } | ||
543 | Err(not) => not, | ||
544 | }; | ||
520 | let not = match notification_cast::<req::DidCloseTextDocument>(not) { | 545 | let not = match notification_cast::<req::DidCloseTextDocument>(not) { |
521 | Ok(params) => { | 546 | Ok(params) => { |
522 | let uri = params.text_document.uri; | 547 | let uri = params.text_document.uri; |
diff --git a/crates/ra_lsp_server/src/main_loop/handlers.rs b/crates/ra_lsp_server/src/main_loop/handlers.rs index 39eb3df3e..331beab13 100644 --- a/crates/ra_lsp_server/src/main_loop/handlers.rs +++ b/crates/ra_lsp_server/src/main_loop/handlers.rs | |||
@@ -654,6 +654,29 @@ pub fn handle_code_action( | |||
654 | res.push(action.into()); | 654 | res.push(action.into()); |
655 | } | 655 | } |
656 | 656 | ||
657 | for fix in world.check_watcher.read().fixes_for(¶ms.text_document.uri).into_iter().flatten() | ||
658 | { | ||
659 | let fix_range = fix.location.range.conv_with(&line_index); | ||
660 | if fix_range.intersection(&range).is_none() { | ||
661 | continue; | ||
662 | } | ||
663 | |||
664 | let edits = vec![TextEdit::new(fix.location.range, fix.replacement.clone())]; | ||
665 | let mut edit_map = std::collections::HashMap::new(); | ||
666 | edit_map.insert(fix.location.uri.clone(), edits); | ||
667 | let edit = WorkspaceEdit::new(edit_map); | ||
668 | |||
669 | let action = CodeAction { | ||
670 | title: fix.title.clone(), | ||
671 | kind: Some("quickfix".to_string()), | ||
672 | diagnostics: Some(fix.diagnostics.clone()), | ||
673 | edit: Some(edit), | ||
674 | command: None, | ||
675 | is_preferred: None, | ||
676 | }; | ||
677 | res.push(action.into()); | ||
678 | } | ||
679 | |||
657 | for assist in assists { | 680 | for assist in assists { |
658 | let title = assist.change.label.clone(); | 681 | let title = assist.change.label.clone(); |
659 | let edit = assist.change.try_conv_with(&world)?; | 682 | let edit = assist.change.try_conv_with(&world)?; |
@@ -820,7 +843,7 @@ pub fn publish_diagnostics( | |||
820 | let _p = profile("publish_diagnostics"); | 843 | let _p = profile("publish_diagnostics"); |
821 | let uri = world.file_id_to_uri(file_id)?; | 844 | let uri = world.file_id_to_uri(file_id)?; |
822 | let line_index = world.analysis().file_line_index(file_id)?; | 845 | let line_index = world.analysis().file_line_index(file_id)?; |
823 | let diagnostics = world | 846 | let mut diagnostics: Vec<Diagnostic> = world |
824 | .analysis() | 847 | .analysis() |
825 | .diagnostics(file_id)? | 848 | .diagnostics(file_id)? |
826 | .into_iter() | 849 | .into_iter() |
@@ -834,6 +857,9 @@ pub fn publish_diagnostics( | |||
834 | tags: None, | 857 | tags: None, |
835 | }) | 858 | }) |
836 | .collect(); | 859 | .collect(); |
860 | if let Some(check_diags) = world.check_watcher.read().diagnostics_for(&uri) { | ||
861 | diagnostics.extend(check_diags.iter().cloned()); | ||
862 | } | ||
837 | Ok(req::PublishDiagnosticsParams { uri, diagnostics, version: None }) | 863 | Ok(req::PublishDiagnosticsParams { uri, diagnostics, version: None }) |
838 | } | 864 | } |
839 | 865 | ||
diff --git a/crates/ra_lsp_server/src/world.rs b/crates/ra_lsp_server/src/world.rs index 79431e7e6..8e9380ca0 100644 --- a/crates/ra_lsp_server/src/world.rs +++ b/crates/ra_lsp_server/src/world.rs | |||
@@ -24,6 +24,7 @@ use std::path::{Component, Prefix}; | |||
24 | 24 | ||
25 | use crate::{ | 25 | use crate::{ |
26 | main_loop::pending_requests::{CompletedRequest, LatestRequests}, | 26 | main_loop::pending_requests::{CompletedRequest, LatestRequests}, |
27 | cargo_check::{CheckWatcher, CheckWatcherSharedState}, | ||
27 | LspError, Result, | 28 | LspError, Result, |
28 | }; | 29 | }; |
29 | use std::str::FromStr; | 30 | use std::str::FromStr; |
@@ -52,6 +53,7 @@ pub struct WorldState { | |||
52 | pub vfs: Arc<RwLock<Vfs>>, | 53 | pub vfs: Arc<RwLock<Vfs>>, |
53 | pub task_receiver: Receiver<VfsTask>, | 54 | pub task_receiver: Receiver<VfsTask>, |
54 | pub latest_requests: Arc<RwLock<LatestRequests>>, | 55 | pub latest_requests: Arc<RwLock<LatestRequests>>, |
56 | pub check_watcher: CheckWatcher, | ||
55 | } | 57 | } |
56 | 58 | ||
57 | /// An immutable snapshot of the world's state at a point in time. | 59 | /// An immutable snapshot of the world's state at a point in time. |
@@ -61,6 +63,7 @@ pub struct WorldSnapshot { | |||
61 | pub analysis: Analysis, | 63 | pub analysis: Analysis, |
62 | pub vfs: Arc<RwLock<Vfs>>, | 64 | pub vfs: Arc<RwLock<Vfs>>, |
63 | pub latest_requests: Arc<RwLock<LatestRequests>>, | 65 | pub latest_requests: Arc<RwLock<LatestRequests>>, |
66 | pub check_watcher: Arc<RwLock<CheckWatcherSharedState>>, | ||
64 | } | 67 | } |
65 | 68 | ||
66 | impl WorldState { | 69 | impl WorldState { |
@@ -127,6 +130,9 @@ impl WorldState { | |||
127 | } | 130 | } |
128 | change.set_crate_graph(crate_graph); | 131 | change.set_crate_graph(crate_graph); |
129 | 132 | ||
133 | // FIXME: Figure out the multi-workspace situation | ||
134 | let check_watcher = CheckWatcher::new(folder_roots.first().cloned().unwrap()); | ||
135 | |||
130 | let mut analysis_host = AnalysisHost::new(lru_capacity, feature_flags); | 136 | let mut analysis_host = AnalysisHost::new(lru_capacity, feature_flags); |
131 | analysis_host.apply_change(change); | 137 | analysis_host.apply_change(change); |
132 | WorldState { | 138 | WorldState { |
@@ -138,6 +144,7 @@ impl WorldState { | |||
138 | vfs: Arc::new(RwLock::new(vfs)), | 144 | vfs: Arc::new(RwLock::new(vfs)), |
139 | task_receiver, | 145 | task_receiver, |
140 | latest_requests: Default::default(), | 146 | latest_requests: Default::default(), |
147 | check_watcher, | ||
141 | } | 148 | } |
142 | } | 149 | } |
143 | 150 | ||
@@ -199,6 +206,7 @@ impl WorldState { | |||
199 | analysis: self.analysis_host.analysis(), | 206 | analysis: self.analysis_host.analysis(), |
200 | vfs: Arc::clone(&self.vfs), | 207 | vfs: Arc::clone(&self.vfs), |
201 | latest_requests: Arc::clone(&self.latest_requests), | 208 | latest_requests: Arc::clone(&self.latest_requests), |
209 | check_watcher: self.check_watcher.shared.clone(), | ||
202 | } | 210 | } |
203 | } | 211 | } |
204 | 212 | ||