From cdbf54f4bd6a20c782fa9a3a7d30f97906ecd2bf Mon Sep 17 00:00:00 2001 From: Wilfred Hughes Date: Wed, 4 Oct 2023 10:34:34 -0400 Subject: [PATCH] flycheck: initial implementation of `$saved_file` If the custom command has a $saved_file placeholder, and we know the file being saved, replace the placeholder and then run a check command. If there's a placeholder and we don't know the saved file, do nothing. --- crates/flycheck/src/lib.rs | 59 +++++++++++++++---- crates/rust-analyzer/src/config.rs | 5 ++ .../src/handlers/notification.rs | 10 ++-- crates/rust-analyzer/src/main_loop.rs | 3 +- docs/user/generated_config.adoc | 5 ++ editors/code/package.json | 2 +- 6 files changed, 66 insertions(+), 18 deletions(-) diff --git a/crates/flycheck/src/lib.rs b/crates/flycheck/src/lib.rs index 71ff3e7b0e..71882b6f0d 100644 --- a/crates/flycheck/src/lib.rs +++ b/crates/flycheck/src/lib.rs @@ -14,7 +14,7 @@ use std::{ use command_group::{CommandGroup, GroupChild}; use crossbeam_channel::{never, select, unbounded, Receiver, Sender}; -use paths::AbsPathBuf; +use paths::{AbsPath, AbsPathBuf}; use rustc_hash::FxHashMap; use serde::Deserialize; use stdx::process::streaming_output; @@ -102,13 +102,15 @@ impl FlycheckHandle { } /// Schedule a re-start of the cargo check worker to do a workspace wide check. - pub fn restart_workspace(&self) { - self.sender.send(StateChange::Restart(None)).unwrap(); + pub fn restart_workspace(&self, saved_file: Option) { + self.sender.send(StateChange::Restart { package: None, saved_file }).unwrap(); } /// Schedule a re-start of the cargo check worker to do a package wide check. pub fn restart_for_package(&self, package: String) { - self.sender.send(StateChange::Restart(Some(package))).unwrap(); + self.sender + .send(StateChange::Restart { package: Some(package), saved_file: None }) + .unwrap(); } /// Stop this cargo check worker. @@ -159,7 +161,7 @@ pub enum Progress { } enum StateChange { - Restart(Option), + Restart { package: Option, saved_file: Option }, Cancel, } @@ -186,6 +188,8 @@ enum Event { CheckEvent(Option), } +const SAVED_FILE_PLACEHOLDER: &str = "$saved_file"; + impl FlycheckActor { fn new( id: usize, @@ -221,7 +225,7 @@ impl FlycheckActor { tracing::debug!(flycheck_id = self.id, "flycheck cancelled"); self.cancel_check_process(); } - Event::RequestStateChange(StateChange::Restart(package)) => { + Event::RequestStateChange(StateChange::Restart { package, saved_file }) => { // Cancel the previously spawned process self.cancel_check_process(); while let Ok(restart) = inbox.recv_timeout(Duration::from_millis(50)) { @@ -231,7 +235,11 @@ impl FlycheckActor { } } - let command = self.check_command(package.as_deref()); + let command = + match self.check_command(package.as_deref(), saved_file.as_deref()) { + Some(c) => c, + None => continue, + }; let formatted_command = format!("{:?}", command); tracing::debug!(?command, "will restart flycheck"); @@ -305,7 +313,14 @@ impl FlycheckActor { } } - fn check_command(&self, package: Option<&str>) -> Command { + /// Construct a `Command` object for checking the user's code. If the user + /// has specified a custom command with placeholders that we cannot fill, + /// return None. + fn check_command( + &self, + package: Option<&str>, + saved_file: Option<&AbsPath>, + ) -> Option { let (mut cmd, args) = match &self.config { FlycheckConfig::CargoCommand { command, @@ -358,7 +373,7 @@ impl FlycheckActor { cmd.arg("--target-dir").arg(target_dir); } cmd.envs(extra_env); - (cmd, extra_args) + (cmd, extra_args.clone()) } FlycheckConfig::CustomCommand { command, @@ -387,12 +402,34 @@ impl FlycheckActor { } } - (cmd, args) + if args.contains(&SAVED_FILE_PLACEHOLDER.to_owned()) { + // If the custom command has a $saved_file placeholder, and + // we're saving a file, replace the placeholder in the arguments. + if let Some(saved_file) = saved_file { + let args = args + .iter() + .map(|arg| { + if arg == SAVED_FILE_PLACEHOLDER { + saved_file.to_string() + } else { + arg.clone() + } + }) + .collect(); + (cmd, args) + } else { + // The custom command has a $saved_file placeholder, + // but we had an IDE event that wasn't a file save. Do nothing. + return None; + } + } else { + (cmd, args.clone()) + } } }; cmd.args(args); - cmd + Some(cmd) } fn send(&self, check_task: Message) { diff --git a/crates/rust-analyzer/src/config.rs b/crates/rust-analyzer/src/config.rs index bf3e71a6bd..eec50860ee 100644 --- a/crates/rust-analyzer/src/config.rs +++ b/crates/rust-analyzer/src/config.rs @@ -209,6 +209,11 @@ config_data! { /// by changing `#rust-analyzer.check.invocationStrategy#` and /// `#rust-analyzer.check.invocationLocation#`. /// + /// If `$saved_file` is part of the command, rust-analyzer will pass + /// the absolute path of the saved file to the provided command. This is + /// intended to be used with non-Cargo build systems. + /// Note that `$saved_file` is experimental and may be removed in the futureg. + /// /// An example command would be: /// /// ```bash diff --git a/crates/rust-analyzer/src/handlers/notification.rs b/crates/rust-analyzer/src/handlers/notification.rs index d3c2073f09..09ca4392f4 100644 --- a/crates/rust-analyzer/src/handlers/notification.rs +++ b/crates/rust-analyzer/src/handlers/notification.rs @@ -168,7 +168,7 @@ pub(crate) fn handle_did_save_text_document( } else if state.config.check_on_save() { // No specific flycheck was triggered, so let's trigger all of them. for flycheck in state.flycheck.iter() { - flycheck.restart_workspace(); + flycheck.restart_workspace(None); } } Ok(()) @@ -314,6 +314,8 @@ fn run_flycheck(state: &mut GlobalState, vfs_path: VfsPath) -> bool { Some((idx, package)) }); + let saved_file = vfs_path.as_path().map(|p| p.to_owned()); + // Find and trigger corresponding flychecks for flycheck in world.flycheck.iter() { for (id, package) in workspace_ids.clone() { @@ -321,7 +323,7 @@ fn run_flycheck(state: &mut GlobalState, vfs_path: VfsPath) -> bool { updated = true; match package.filter(|_| !world.config.flycheck_workspace()) { Some(package) => flycheck.restart_for_package(package), - None => flycheck.restart_workspace(), + None => flycheck.restart_workspace(saved_file.clone()), } continue; } @@ -330,7 +332,7 @@ fn run_flycheck(state: &mut GlobalState, vfs_path: VfsPath) -> bool { // No specific flycheck was triggered, so let's trigger all of them. if !updated { for flycheck in world.flycheck.iter() { - flycheck.restart_workspace(); + flycheck.restart_workspace(saved_file.clone()); } } Ok(()) @@ -372,7 +374,7 @@ pub(crate) fn handle_run_flycheck( } // No specific flycheck was triggered, so let's trigger all of them. for flycheck in state.flycheck.iter() { - flycheck.restart_workspace(); + flycheck.restart_workspace(None); } Ok(()) } diff --git a/crates/rust-analyzer/src/main_loop.rs b/crates/rust-analyzer/src/main_loop.rs index 88660db7e9..5c3d34cc54 100644 --- a/crates/rust-analyzer/src/main_loop.rs +++ b/crates/rust-analyzer/src/main_loop.rs @@ -8,7 +8,6 @@ use std::{ use always_assert::always; use crossbeam_channel::{select, Receiver}; -use flycheck::FlycheckHandle; use ide_db::base_db::{SourceDatabaseExt, VfsPath}; use lsp_server::{Connection, Notification, Request}; use lsp_types::notification::Notification as _; @@ -337,7 +336,7 @@ impl GlobalState { if became_quiescent { if self.config.check_on_save() { // Project has loaded properly, kick off initial flycheck - self.flycheck.iter().for_each(FlycheckHandle::restart_workspace); + self.flycheck.iter().for_each(|flycheck| flycheck.restart_workspace(None)); } if self.config.prefill_caches() { self.prime_caches_queue.request_op("became quiescent".to_owned(), ()); diff --git a/docs/user/generated_config.adoc b/docs/user/generated_config.adoc index a86ef70941..b9e82c5527 100644 --- a/docs/user/generated_config.adoc +++ b/docs/user/generated_config.adoc @@ -234,6 +234,11 @@ each of them, with the working directory being the workspace root by changing `#rust-analyzer.check.invocationStrategy#` and `#rust-analyzer.check.invocationLocation#`. +If `$saved_file` is part of the command, rust-analyzer will pass +the absolute path of the saved file to the provided command. This is +intended to be used with non-Cargo build systems. +Note that `$saved_file` is experimental and may be removed in the futureg. + An example command would be: ```bash diff --git a/editors/code/package.json b/editors/code/package.json index b474471e5a..4f4a106b1d 100644 --- a/editors/code/package.json +++ b/editors/code/package.json @@ -775,7 +775,7 @@ ] }, "rust-analyzer.check.overrideCommand": { - "markdownDescription": "Override the command rust-analyzer uses instead of `cargo check` for\ndiagnostics on save. The command is required to output json and\nshould therefore include `--message-format=json` or a similar option\n(if your client supports the `colorDiagnosticOutput` experimental\ncapability, you can use `--message-format=json-diagnostic-rendered-ansi`).\n\nIf you're changing this because you're using some tool wrapping\nCargo, you might also want to change\n`#rust-analyzer.cargo.buildScripts.overrideCommand#`.\n\nIf there are multiple linked projects/workspaces, this command is invoked for\neach of them, with the working directory being the workspace root\n(i.e., the folder containing the `Cargo.toml`). This can be overwritten\nby changing `#rust-analyzer.check.invocationStrategy#` and\n`#rust-analyzer.check.invocationLocation#`.\n\nAn example command would be:\n\n```bash\ncargo check --workspace --message-format=json --all-targets\n```\n.", + "markdownDescription": "Override the command rust-analyzer uses instead of `cargo check` for\ndiagnostics on save. The command is required to output json and\nshould therefore include `--message-format=json` or a similar option\n(if your client supports the `colorDiagnosticOutput` experimental\ncapability, you can use `--message-format=json-diagnostic-rendered-ansi`).\n\nIf you're changing this because you're using some tool wrapping\nCargo, you might also want to change\n`#rust-analyzer.cargo.buildScripts.overrideCommand#`.\n\nIf there are multiple linked projects/workspaces, this command is invoked for\neach of them, with the working directory being the workspace root\n(i.e., the folder containing the `Cargo.toml`). This can be overwritten\nby changing `#rust-analyzer.check.invocationStrategy#` and\n`#rust-analyzer.check.invocationLocation#`.\n\nIf `$saved_file` is part of the command, rust-analyzer will pass\nthe absolute path of the saved file to the provided command. This is\nintended to be used with non-Cargo build systems.\nNote that `$saved_file` is experimental and may be removed in the futureg.\n\nAn example command would be:\n\n```bash\ncargo check --workspace --message-format=json --all-targets\n```\n.", "default": null, "type": [ "null",