diff options
Diffstat (limited to 'crates/ra_flycheck')
-rw-r--r-- | crates/ra_flycheck/src/lib.rs | 268 |
1 files changed, 117 insertions, 151 deletions
diff --git a/crates/ra_flycheck/src/lib.rs b/crates/ra_flycheck/src/lib.rs index 38940a77b..13494a731 100644 --- a/crates/ra_flycheck/src/lib.rs +++ b/crates/ra_flycheck/src/lib.rs | |||
@@ -1,55 +1,53 @@ | |||
1 | //! cargo_check provides the functionality needed to run `cargo check` or | 1 | //! cargo_check provides the functionality needed to run `cargo check` or |
2 | //! another compatible command (f.x. clippy) in a background thread and provide | 2 | //! another compatible command (f.x. clippy) in a background thread and provide |
3 | //! LSP diagnostics based on the output of the command. | 3 | //! LSP diagnostics based on the output of the command. |
4 | mod conv; | ||
5 | |||
6 | use std::{ | ||
7 | env, | ||
8 | io::{self, BufRead, BufReader}, | ||
9 | path::PathBuf, | ||
10 | process::{Command, Stdio}, | ||
11 | time::Instant, | ||
12 | }; | ||
13 | |||
4 | use cargo_metadata::Message; | 14 | use cargo_metadata::Message; |
5 | use crossbeam_channel::{never, select, unbounded, Receiver, RecvError, Sender}; | 15 | use crossbeam_channel::{never, select, unbounded, Receiver, RecvError, Sender}; |
6 | use lsp_types::{ | 16 | use lsp_types::{ |
7 | CodeAction, CodeActionOrCommand, Diagnostic, Url, WorkDoneProgress, WorkDoneProgressBegin, | 17 | CodeAction, CodeActionOrCommand, Diagnostic, Url, WorkDoneProgress, WorkDoneProgressBegin, |
8 | WorkDoneProgressEnd, WorkDoneProgressReport, | 18 | WorkDoneProgressEnd, WorkDoneProgressReport, |
9 | }; | 19 | }; |
10 | use std::{ | ||
11 | error, fmt, | ||
12 | io::{BufRead, BufReader}, | ||
13 | path::{Path, PathBuf}, | ||
14 | process::{Command, Stdio}, | ||
15 | time::Instant, | ||
16 | }; | ||
17 | |||
18 | mod conv; | ||
19 | 20 | ||
20 | use crate::conv::{map_rust_diagnostic_to_lsp, MappedRustDiagnostic}; | 21 | use crate::conv::{map_rust_diagnostic_to_lsp, MappedRustDiagnostic}; |
21 | 22 | ||
22 | pub use crate::conv::url_from_path_with_drive_lowercasing; | 23 | pub use crate::conv::url_from_path_with_drive_lowercasing; |
23 | 24 | ||
24 | #[derive(Clone, Debug)] | 25 | #[derive(Clone, Debug)] |
25 | pub struct CheckConfig { | 26 | pub enum FlycheckConfig { |
26 | pub enable: bool, | 27 | CargoCommand { command: String, all_targets: bool, extra_args: Vec<String> }, |
27 | pub args: Vec<String>, | 28 | CustomCommand { command: String, args: Vec<String> }, |
28 | pub command: String, | ||
29 | pub all_targets: bool, | ||
30 | } | 29 | } |
31 | 30 | ||
32 | /// CheckWatcher wraps the shared state and communication machinery used for | 31 | /// Flycheck wraps the shared state and communication machinery used for |
33 | /// running `cargo check` (or other compatible command) and providing | 32 | /// running `cargo check` (or other compatible command) and providing |
34 | /// diagnostics based on the output. | 33 | /// diagnostics based on the output. |
35 | /// The spawned thread is shut down when this struct is dropped. | 34 | /// The spawned thread is shut down when this struct is dropped. |
36 | #[derive(Debug)] | 35 | #[derive(Debug)] |
37 | pub struct CheckWatcher { | 36 | pub struct Flycheck { |
38 | // XXX: drop order is significant | 37 | // XXX: drop order is significant |
39 | cmd_send: Sender<CheckCommand>, | 38 | cmd_send: Sender<CheckCommand>, |
40 | handle: Option<jod_thread::JoinHandle<()>>, | 39 | handle: jod_thread::JoinHandle<()>, |
41 | pub task_recv: Receiver<CheckTask>, | 40 | pub task_recv: Receiver<CheckTask>, |
42 | } | 41 | } |
43 | 42 | ||
44 | impl CheckWatcher { | 43 | impl Flycheck { |
45 | pub fn new(config: CheckConfig, workspace_root: PathBuf) -> CheckWatcher { | 44 | pub fn new(config: FlycheckConfig, workspace_root: PathBuf) -> Flycheck { |
46 | let (task_send, task_recv) = unbounded::<CheckTask>(); | 45 | let (task_send, task_recv) = unbounded::<CheckTask>(); |
47 | let (cmd_send, cmd_recv) = unbounded::<CheckCommand>(); | 46 | let (cmd_send, cmd_recv) = unbounded::<CheckCommand>(); |
48 | let handle = jod_thread::spawn(move || { | 47 | let handle = jod_thread::spawn(move || { |
49 | let mut check = CheckWatcherThread::new(config, workspace_root); | 48 | FlycheckThread::new(config, workspace_root).run(&task_send, &cmd_recv); |
50 | check.run(&task_send, &cmd_recv); | ||
51 | }); | 49 | }); |
52 | CheckWatcher { task_recv, cmd_send, handle: Some(handle) } | 50 | Flycheck { task_recv, cmd_send, handle } |
53 | } | 51 | } |
54 | 52 | ||
55 | /// Schedule a re-start of the cargo check worker. | 53 | /// Schedule a re-start of the cargo check worker. |
@@ -75,20 +73,28 @@ pub enum CheckCommand { | |||
75 | Update, | 73 | Update, |
76 | } | 74 | } |
77 | 75 | ||
78 | struct CheckWatcherThread { | 76 | struct FlycheckThread { |
79 | options: CheckConfig, | 77 | config: FlycheckConfig, |
80 | workspace_root: PathBuf, | 78 | workspace_root: PathBuf, |
81 | watcher: WatchThread, | ||
82 | last_update_req: Option<Instant>, | 79 | last_update_req: Option<Instant>, |
80 | // XXX: drop order is significant | ||
81 | message_recv: Receiver<CheckEvent>, | ||
82 | /// WatchThread exists to wrap around the communication needed to be able to | ||
83 | /// run `cargo check` without blocking. Currently the Rust standard library | ||
84 | /// doesn't provide a way to read sub-process output without blocking, so we | ||
85 | /// have to wrap sub-processes output handling in a thread and pass messages | ||
86 | /// back over a channel. | ||
87 | check_process: Option<jod_thread::JoinHandle<()>>, | ||
83 | } | 88 | } |
84 | 89 | ||
85 | impl CheckWatcherThread { | 90 | impl FlycheckThread { |
86 | fn new(options: CheckConfig, workspace_root: PathBuf) -> CheckWatcherThread { | 91 | fn new(config: FlycheckConfig, workspace_root: PathBuf) -> FlycheckThread { |
87 | CheckWatcherThread { | 92 | FlycheckThread { |
88 | options, | 93 | config, |
89 | workspace_root, | 94 | workspace_root, |
90 | watcher: WatchThread::dummy(), | ||
91 | last_update_req: None, | 95 | last_update_req: None, |
96 | message_recv: never(), | ||
97 | check_process: None, | ||
92 | } | 98 | } |
93 | } | 99 | } |
94 | 100 | ||
@@ -105,25 +111,21 @@ impl CheckWatcherThread { | |||
105 | break; | 111 | break; |
106 | }, | 112 | }, |
107 | }, | 113 | }, |
108 | recv(self.watcher.message_recv) -> msg => match msg { | 114 | recv(self.message_recv) -> msg => match msg { |
109 | Ok(msg) => self.handle_message(msg, task_send), | 115 | Ok(msg) => self.handle_message(msg, task_send), |
110 | Err(RecvError) => { | 116 | Err(RecvError) => { |
111 | // Watcher finished, replace it with a never channel to | 117 | // Watcher finished, replace it with a never channel to |
112 | // avoid busy-waiting. | 118 | // avoid busy-waiting. |
113 | std::mem::replace(&mut self.watcher.message_recv, never()); | 119 | self.message_recv = never(); |
120 | self.check_process = None; | ||
114 | }, | 121 | }, |
115 | } | 122 | } |
116 | }; | 123 | }; |
117 | 124 | ||
118 | if self.should_recheck() { | 125 | if self.should_recheck() { |
119 | self.last_update_req.take(); | 126 | self.last_update_req = None; |
120 | task_send.send(CheckTask::ClearDiagnostics).unwrap(); | 127 | task_send.send(CheckTask::ClearDiagnostics).unwrap(); |
121 | 128 | self.restart_check_process(); | |
122 | // Replace with a dummy watcher first so we drop the original and wait for completion | ||
123 | std::mem::replace(&mut self.watcher, WatchThread::dummy()); | ||
124 | |||
125 | // Then create the actual new watcher | ||
126 | self.watcher = WatchThread::new(&self.options, &self.workspace_root); | ||
127 | } | 129 | } |
128 | } | 130 | } |
129 | } | 131 | } |
@@ -206,6 +208,63 @@ impl CheckWatcherThread { | |||
206 | CheckEvent::Msg(Message::Unknown) => {} | 208 | CheckEvent::Msg(Message::Unknown) => {} |
207 | } | 209 | } |
208 | } | 210 | } |
211 | |||
212 | fn restart_check_process(&mut self) { | ||
213 | // First, clear and cancel the old thread | ||
214 | self.message_recv = never(); | ||
215 | self.check_process = None; | ||
216 | |||
217 | let mut cmd = match &self.config { | ||
218 | FlycheckConfig::CargoCommand { command, all_targets, extra_args } => { | ||
219 | let mut cmd = Command::new(cargo_binary()); | ||
220 | cmd.arg(command); | ||
221 | cmd.args(&["--workspace", "--message-format=json", "--manifest-path"]); | ||
222 | cmd.arg(self.workspace_root.join("Cargo.toml")); | ||
223 | if *all_targets { | ||
224 | cmd.arg("--all-targets"); | ||
225 | } | ||
226 | cmd.args(extra_args); | ||
227 | cmd | ||
228 | } | ||
229 | FlycheckConfig::CustomCommand { command, args } => { | ||
230 | let mut cmd = Command::new(command); | ||
231 | cmd.args(args); | ||
232 | cmd | ||
233 | } | ||
234 | }; | ||
235 | cmd.current_dir(&self.workspace_root); | ||
236 | |||
237 | let (message_send, message_recv) = unbounded(); | ||
238 | self.message_recv = message_recv; | ||
239 | self.check_process = Some(jod_thread::spawn(move || { | ||
240 | // If we trigger an error here, we will do so in the loop instead, | ||
241 | // which will break out of the loop, and continue the shutdown | ||
242 | let _ = message_send.send(CheckEvent::Begin); | ||
243 | |||
244 | let res = run_cargo(cmd, &mut |message| { | ||
245 | // Skip certain kinds of messages to only spend time on what's useful | ||
246 | match &message { | ||
247 | Message::CompilerArtifact(artifact) if artifact.fresh => return true, | ||
248 | Message::BuildScriptExecuted(_) => return true, | ||
249 | Message::Unknown => return true, | ||
250 | _ => {} | ||
251 | } | ||
252 | |||
253 | // if the send channel was closed, we want to shutdown | ||
254 | message_send.send(CheckEvent::Msg(message)).is_ok() | ||
255 | }); | ||
256 | |||
257 | if let Err(err) = res { | ||
258 | // FIXME: make the `message_send` to be `Sender<Result<CheckEvent, CargoError>>` | ||
259 | // to display user-caused misconfiguration errors instead of just logging them here | ||
260 | log::error!("Cargo watcher failed {:?}", err); | ||
261 | } | ||
262 | |||
263 | // We can ignore any error here, as we are already in the progress | ||
264 | // of shutting down. | ||
265 | let _ = message_send.send(CheckEvent::End); | ||
266 | })) | ||
267 | } | ||
209 | } | 268 | } |
210 | 269 | ||
211 | #[derive(Debug)] | 270 | #[derive(Debug)] |
@@ -214,52 +273,18 @@ pub struct DiagnosticWithFixes { | |||
214 | fixes: Vec<CodeAction>, | 273 | fixes: Vec<CodeAction>, |
215 | } | 274 | } |
216 | 275 | ||
217 | /// WatchThread exists to wrap around the communication needed to be able to | ||
218 | /// run `cargo check` without blocking. Currently the Rust standard library | ||
219 | /// doesn't provide a way to read sub-process output without blocking, so we | ||
220 | /// have to wrap sub-processes output handling in a thread and pass messages | ||
221 | /// back over a channel. | ||
222 | /// The correct way to dispose of the thread is to drop it, on which the | ||
223 | /// sub-process will be killed, and the thread will be joined. | ||
224 | struct WatchThread { | ||
225 | // XXX: drop order is significant | ||
226 | message_recv: Receiver<CheckEvent>, | ||
227 | _handle: Option<jod_thread::JoinHandle<()>>, | ||
228 | } | ||
229 | |||
230 | enum CheckEvent { | 276 | enum CheckEvent { |
231 | Begin, | 277 | Begin, |
232 | Msg(cargo_metadata::Message), | 278 | Msg(cargo_metadata::Message), |
233 | End, | 279 | End, |
234 | } | 280 | } |
235 | 281 | ||
236 | #[derive(Debug)] | ||
237 | pub struct CargoError(String); | ||
238 | |||
239 | impl fmt::Display for CargoError { | ||
240 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | ||
241 | write!(f, "Cargo failed: {}", self.0) | ||
242 | } | ||
243 | } | ||
244 | impl error::Error for CargoError {} | ||
245 | |||
246 | fn run_cargo( | 282 | fn run_cargo( |
247 | args: &[String], | 283 | mut command: Command, |
248 | current_dir: Option<&Path>, | ||
249 | on_message: &mut dyn FnMut(cargo_metadata::Message) -> bool, | 284 | on_message: &mut dyn FnMut(cargo_metadata::Message) -> bool, |
250 | ) -> Result<(), CargoError> { | 285 | ) -> io::Result<()> { |
251 | let mut command = Command::new("cargo"); | 286 | let mut child = |
252 | if let Some(current_dir) = current_dir { | 287 | command.stdout(Stdio::piped()).stderr(Stdio::null()).stdin(Stdio::null()).spawn()?; |
253 | command.current_dir(current_dir); | ||
254 | } | ||
255 | |||
256 | let mut child = command | ||
257 | .args(args) | ||
258 | .stdout(Stdio::piped()) | ||
259 | .stderr(Stdio::null()) | ||
260 | .stdin(Stdio::null()) | ||
261 | .spawn() | ||
262 | .expect("couldn't launch cargo"); | ||
263 | 288 | ||
264 | // We manually read a line at a time, instead of using serde's | 289 | // We manually read a line at a time, instead of using serde's |
265 | // stream deserializers, because the deserializer cannot recover | 290 | // stream deserializers, because the deserializer cannot recover |
@@ -273,13 +298,7 @@ fn run_cargo( | |||
273 | let mut read_at_least_one_message = false; | 298 | let mut read_at_least_one_message = false; |
274 | 299 | ||
275 | for line in stdout.lines() { | 300 | for line in stdout.lines() { |
276 | let line = match line { | 301 | let line = line?; |
277 | Ok(line) => line, | ||
278 | Err(err) => { | ||
279 | log::error!("Couldn't read line from cargo: {}", err); | ||
280 | continue; | ||
281 | } | ||
282 | }; | ||
283 | 302 | ||
284 | let message = serde_json::from_str::<cargo_metadata::Message>(&line); | 303 | let message = serde_json::from_str::<cargo_metadata::Message>(&line); |
285 | let message = match message { | 304 | let message = match message { |
@@ -300,75 +319,22 @@ fn run_cargo( | |||
300 | // It is okay to ignore the result, as it only errors if the process is already dead | 319 | // It is okay to ignore the result, as it only errors if the process is already dead |
301 | let _ = child.kill(); | 320 | let _ = child.kill(); |
302 | 321 | ||
303 | let err_msg = match child.wait() { | 322 | let exit_status = child.wait()?; |
304 | Ok(exit_code) if !exit_code.success() && !read_at_least_one_message => { | 323 | if !exit_status.success() && !read_at_least_one_message { |
305 | // FIXME: Read the stderr to display the reason, see `read2()` reference in PR comment: | 324 | // FIXME: Read the stderr to display the reason, see `read2()` reference in PR comment: |
306 | // https://github.com/rust-analyzer/rust-analyzer/pull/3632#discussion_r395605298 | 325 | // https://github.com/rust-analyzer/rust-analyzer/pull/3632#discussion_r395605298 |
326 | return Err(io::Error::new( | ||
327 | io::ErrorKind::Other, | ||
307 | format!( | 328 | format!( |
308 | "the command produced no valid metadata (exit code: {:?}): cargo {}", | 329 | "the command produced no valid metadata (exit code: {:?}): {:?}", |
309 | exit_code, | 330 | exit_status, command |
310 | args.join(" ") | 331 | ), |
311 | ) | 332 | )); |
312 | } | ||
313 | Err(err) => format!("io error: {:?}", err), | ||
314 | Ok(_) => return Ok(()), | ||
315 | }; | ||
316 | |||
317 | Err(CargoError(err_msg)) | ||
318 | } | ||
319 | |||
320 | impl WatchThread { | ||
321 | fn dummy() -> WatchThread { | ||
322 | WatchThread { message_recv: never(), _handle: None } | ||
323 | } | 333 | } |
324 | 334 | ||
325 | fn new(options: &CheckConfig, workspace_root: &Path) -> WatchThread { | 335 | Ok(()) |
326 | let mut args: Vec<String> = vec![ | 336 | } |
327 | options.command.clone(), | ||
328 | "--workspace".to_string(), | ||
329 | "--message-format=json".to_string(), | ||
330 | "--manifest-path".to_string(), | ||
331 | format!("{}/Cargo.toml", workspace_root.display()), | ||
332 | ]; | ||
333 | if options.all_targets { | ||
334 | args.push("--all-targets".to_string()); | ||
335 | } | ||
336 | args.extend(options.args.iter().cloned()); | ||
337 | |||
338 | let (message_send, message_recv) = unbounded(); | ||
339 | let workspace_root = workspace_root.to_owned(); | ||
340 | let handle = if options.enable { | ||
341 | Some(jod_thread::spawn(move || { | ||
342 | // If we trigger an error here, we will do so in the loop instead, | ||
343 | // which will break out of the loop, and continue the shutdown | ||
344 | let _ = message_send.send(CheckEvent::Begin); | ||
345 | |||
346 | let res = run_cargo(&args, Some(&workspace_root), &mut |message| { | ||
347 | // Skip certain kinds of messages to only spend time on what's useful | ||
348 | match &message { | ||
349 | Message::CompilerArtifact(artifact) if artifact.fresh => return true, | ||
350 | Message::BuildScriptExecuted(_) => return true, | ||
351 | Message::Unknown => return true, | ||
352 | _ => {} | ||
353 | } | ||
354 | |||
355 | // if the send channel was closed, we want to shutdown | ||
356 | message_send.send(CheckEvent::Msg(message)).is_ok() | ||
357 | }); | ||
358 | |||
359 | if let Err(err) = res { | ||
360 | // FIXME: make the `message_send` to be `Sender<Result<CheckEvent, CargoError>>` | ||
361 | // to display user-caused misconfiguration errors instead of just logging them here | ||
362 | log::error!("Cargo watcher failed {:?}", err); | ||
363 | } | ||
364 | 337 | ||
365 | // We can ignore any error here, as we are already in the progress | 338 | fn cargo_binary() -> String { |
366 | // of shutting down. | 339 | env::var("CARGO").unwrap_or_else(|_| "cargo".to_string()) |
367 | let _ = message_send.send(CheckEvent::End); | ||
368 | })) | ||
369 | } else { | ||
370 | None | ||
371 | }; | ||
372 | WatchThread { message_recv, _handle: handle } | ||
373 | } | ||
374 | } | 340 | } |