aboutsummaryrefslogtreecommitdiff
path: root/crates/ra_flycheck
diff options
context:
space:
mode:
Diffstat (limited to 'crates/ra_flycheck')
-rw-r--r--crates/ra_flycheck/src/lib.rs268
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.
4mod conv;
5
6use std::{
7 env,
8 io::{self, BufRead, BufReader},
9 path::PathBuf,
10 process::{Command, Stdio},
11 time::Instant,
12};
13
4use cargo_metadata::Message; 14use cargo_metadata::Message;
5use crossbeam_channel::{never, select, unbounded, Receiver, RecvError, Sender}; 15use crossbeam_channel::{never, select, unbounded, Receiver, RecvError, Sender};
6use lsp_types::{ 16use lsp_types::{
7 CodeAction, CodeActionOrCommand, Diagnostic, Url, WorkDoneProgress, WorkDoneProgressBegin, 17 CodeAction, CodeActionOrCommand, Diagnostic, Url, WorkDoneProgress, WorkDoneProgressBegin,
8 WorkDoneProgressEnd, WorkDoneProgressReport, 18 WorkDoneProgressEnd, WorkDoneProgressReport,
9}; 19};
10use std::{
11 error, fmt,
12 io::{BufRead, BufReader},
13 path::{Path, PathBuf},
14 process::{Command, Stdio},
15 time::Instant,
16};
17
18mod conv;
19 20
20use crate::conv::{map_rust_diagnostic_to_lsp, MappedRustDiagnostic}; 21use crate::conv::{map_rust_diagnostic_to_lsp, MappedRustDiagnostic};
21 22
22pub use crate::conv::url_from_path_with_drive_lowercasing; 23pub use crate::conv::url_from_path_with_drive_lowercasing;
23 24
24#[derive(Clone, Debug)] 25#[derive(Clone, Debug)]
25pub struct CheckConfig { 26pub 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)]
37pub struct CheckWatcher { 36pub 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
44impl CheckWatcher { 43impl 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
78struct CheckWatcherThread { 76struct 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
85impl CheckWatcherThread { 90impl 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.
224struct WatchThread {
225 // XXX: drop order is significant
226 message_recv: Receiver<CheckEvent>,
227 _handle: Option<jod_thread::JoinHandle<()>>,
228}
229
230enum CheckEvent { 276enum 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)]
237pub struct CargoError(String);
238
239impl fmt::Display for CargoError {
240 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
241 write!(f, "Cargo failed: {}", self.0)
242 }
243}
244impl error::Error for CargoError {}
245
246fn run_cargo( 282fn 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
320impl 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 338fn 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}