aboutsummaryrefslogtreecommitdiff
path: root/crates/ra_lsp_server
diff options
context:
space:
mode:
authorEmil Lauridsen <[email protected]>2019-12-25 11:21:38 +0000
committerEmil Lauridsen <[email protected]>2019-12-25 16:37:40 +0000
commit66e8ef53a0ed018d03340577a0443030a193f773 (patch)
treece35fbd25ac7bb3b7374dccbb79d89545d9904a7 /crates/ra_lsp_server
parent52b44ba7edbdb64a30b781292eaaea59e8c2490d (diff)
Initial implementation of cargo check watching
Diffstat (limited to 'crates/ra_lsp_server')
-rw-r--r--crates/ra_lsp_server/Cargo.toml1
-rw-r--r--crates/ra_lsp_server/src/caps.rs4
-rw-r--r--crates/ra_lsp_server/src/cargo_check.rs533
-rw-r--r--crates/ra_lsp_server/src/lib.rs1
-rw-r--r--crates/ra_lsp_server/src/main_loop.rs27
-rw-r--r--crates/ra_lsp_server/src/main_loop/handlers.rs28
-rw-r--r--crates/ra_lsp_server/src/world.rs8
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" }
27ra_prof = { path = "../ra_prof" } 27ra_prof = { path = "../ra_prof" }
28ra_vfs_glob = { path = "../ra_vfs_glob" } 28ra_vfs_glob = { path = "../ra_vfs_glob" }
29env_logger = { version = "0.7.1", default-features = false, features = ["humantime"] } 29env_logger = { version = "0.7.1", default-features = false, features = ["humantime"] }
30cargo_metadata = "0.9.1"
30 31
31[dev-dependencies] 32[dev-dependencies]
32tempfile = "3" 33tempfile = "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
12pub fn server_capabilities() -> ServerCapabilities { 12pub 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 @@
1use cargo_metadata::{
2 diagnostic::{
3 Applicability, Diagnostic as RustDiagnostic, DiagnosticLevel, DiagnosticSpan,
4 DiagnosticSpanMacroExpansion,
5 },
6 Message,
7};
8use crossbeam_channel::{select, unbounded, Receiver, RecvError, Sender, TryRecvError};
9use lsp_types::{
10 Diagnostic, DiagnosticRelatedInformation, DiagnosticSeverity, DiagnosticTag, Location,
11 NumberOrString, Position, Range, Url,
12};
13use parking_lot::RwLock;
14use 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)]
25pub 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
32impl 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
52pub 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)]
61pub struct CheckWatcherSharedState {
62 diagnostic_collection: HashMap<Url, Vec<Diagnostic>>,
63 suggested_fix_collection: HashMap<Url, Vec<SuggestedFix>>,
64}
65
66impl 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)]
127pub enum CheckTask {
128 Update(Url),
129}
130
131pub enum CheckCommand {
132 Update,
133}
134
135impl 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.
226struct WatchThread {
227 message_recv: Receiver<cargo_metadata::Message>,
228 cancel_send: Sender<()>,
229}
230
231impl 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
264fn 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
276fn 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
281fn 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
297fn 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`.
320fn 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
334fn 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
347fn 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)]
359pub 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
367impl 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
387enum MappedRustChildDiagnostic {
388 Related(DiagnosticRelatedInformation),
389 SuggestedFix(SuggestedFix),
390 MessageLine(String),
391}
392
393fn 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
432struct 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`
448fn 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
528fn 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
24mod caps; 24mod caps;
25mod cargo_check;
25mod cargo_target_spec; 26mod cargo_target_spec;
26mod conv; 27mod conv;
27mod main_loop; 28mod 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};
19use threadpool::ThreadPool; 19use threadpool::ThreadPool;
20 20
21use crate::{ 21use 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
227impl fmt::Debug for Event { 230impl 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(&params.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
25use crate::{ 25use 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};
29use std::str::FromStr; 30use 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
66impl WorldState { 69impl 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