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.
This commit is contained in:
Wilfred Hughes 2023-10-04 10:34:34 -04:00 committed by Lukas Wirth
parent 47b4dd7273
commit cdbf54f4bd
6 changed files with 66 additions and 18 deletions

View file

@ -14,7 +14,7 @@ use std::{
use command_group::{CommandGroup, GroupChild}; use command_group::{CommandGroup, GroupChild};
use crossbeam_channel::{never, select, unbounded, Receiver, Sender}; use crossbeam_channel::{never, select, unbounded, Receiver, Sender};
use paths::AbsPathBuf; use paths::{AbsPath, AbsPathBuf};
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
use serde::Deserialize; use serde::Deserialize;
use stdx::process::streaming_output; 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. /// Schedule a re-start of the cargo check worker to do a workspace wide check.
pub fn restart_workspace(&self) { pub fn restart_workspace(&self, saved_file: Option<AbsPathBuf>) {
self.sender.send(StateChange::Restart(None)).unwrap(); 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. /// Schedule a re-start of the cargo check worker to do a package wide check.
pub fn restart_for_package(&self, package: String) { 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. /// Stop this cargo check worker.
@ -159,7 +161,7 @@ pub enum Progress {
} }
enum StateChange { enum StateChange {
Restart(Option<String>), Restart { package: Option<String>, saved_file: Option<AbsPathBuf> },
Cancel, Cancel,
} }
@ -186,6 +188,8 @@ enum Event {
CheckEvent(Option<CargoMessage>), CheckEvent(Option<CargoMessage>),
} }
const SAVED_FILE_PLACEHOLDER: &str = "$saved_file";
impl FlycheckActor { impl FlycheckActor {
fn new( fn new(
id: usize, id: usize,
@ -221,7 +225,7 @@ impl FlycheckActor {
tracing::debug!(flycheck_id = self.id, "flycheck cancelled"); tracing::debug!(flycheck_id = self.id, "flycheck cancelled");
self.cancel_check_process(); self.cancel_check_process();
} }
Event::RequestStateChange(StateChange::Restart(package)) => { Event::RequestStateChange(StateChange::Restart { package, saved_file }) => {
// Cancel the previously spawned process // Cancel the previously spawned process
self.cancel_check_process(); self.cancel_check_process();
while let Ok(restart) = inbox.recv_timeout(Duration::from_millis(50)) { 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); let formatted_command = format!("{:?}", command);
tracing::debug!(?command, "will restart flycheck"); 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<Command> {
let (mut cmd, args) = match &self.config { let (mut cmd, args) = match &self.config {
FlycheckConfig::CargoCommand { FlycheckConfig::CargoCommand {
command, command,
@ -358,7 +373,7 @@ impl FlycheckActor {
cmd.arg("--target-dir").arg(target_dir); cmd.arg("--target-dir").arg(target_dir);
} }
cmd.envs(extra_env); cmd.envs(extra_env);
(cmd, extra_args) (cmd, extra_args.clone())
} }
FlycheckConfig::CustomCommand { FlycheckConfig::CustomCommand {
command, 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.args(args);
cmd Some(cmd)
} }
fn send(&self, check_task: Message) { fn send(&self, check_task: Message) {

View file

@ -209,6 +209,11 @@ config_data! {
/// by changing `#rust-analyzer.check.invocationStrategy#` and /// by changing `#rust-analyzer.check.invocationStrategy#` and
/// `#rust-analyzer.check.invocationLocation#`. /// `#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: /// An example command would be:
/// ///
/// ```bash /// ```bash

View file

@ -168,7 +168,7 @@ pub(crate) fn handle_did_save_text_document(
} else if state.config.check_on_save() { } else if state.config.check_on_save() {
// No specific flycheck was triggered, so let's trigger all of them. // No specific flycheck was triggered, so let's trigger all of them.
for flycheck in state.flycheck.iter() { for flycheck in state.flycheck.iter() {
flycheck.restart_workspace(); flycheck.restart_workspace(None);
} }
} }
Ok(()) Ok(())
@ -314,6 +314,8 @@ fn run_flycheck(state: &mut GlobalState, vfs_path: VfsPath) -> bool {
Some((idx, package)) Some((idx, package))
}); });
let saved_file = vfs_path.as_path().map(|p| p.to_owned());
// Find and trigger corresponding flychecks // Find and trigger corresponding flychecks
for flycheck in world.flycheck.iter() { for flycheck in world.flycheck.iter() {
for (id, package) in workspace_ids.clone() { for (id, package) in workspace_ids.clone() {
@ -321,7 +323,7 @@ fn run_flycheck(state: &mut GlobalState, vfs_path: VfsPath) -> bool {
updated = true; updated = true;
match package.filter(|_| !world.config.flycheck_workspace()) { match package.filter(|_| !world.config.flycheck_workspace()) {
Some(package) => flycheck.restart_for_package(package), Some(package) => flycheck.restart_for_package(package),
None => flycheck.restart_workspace(), None => flycheck.restart_workspace(saved_file.clone()),
} }
continue; 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. // No specific flycheck was triggered, so let's trigger all of them.
if !updated { if !updated {
for flycheck in world.flycheck.iter() { for flycheck in world.flycheck.iter() {
flycheck.restart_workspace(); flycheck.restart_workspace(saved_file.clone());
} }
} }
Ok(()) Ok(())
@ -372,7 +374,7 @@ pub(crate) fn handle_run_flycheck(
} }
// No specific flycheck was triggered, so let's trigger all of them. // No specific flycheck was triggered, so let's trigger all of them.
for flycheck in state.flycheck.iter() { for flycheck in state.flycheck.iter() {
flycheck.restart_workspace(); flycheck.restart_workspace(None);
} }
Ok(()) Ok(())
} }

View file

@ -8,7 +8,6 @@ use std::{
use always_assert::always; use always_assert::always;
use crossbeam_channel::{select, Receiver}; use crossbeam_channel::{select, Receiver};
use flycheck::FlycheckHandle;
use ide_db::base_db::{SourceDatabaseExt, VfsPath}; use ide_db::base_db::{SourceDatabaseExt, VfsPath};
use lsp_server::{Connection, Notification, Request}; use lsp_server::{Connection, Notification, Request};
use lsp_types::notification::Notification as _; use lsp_types::notification::Notification as _;
@ -337,7 +336,7 @@ impl GlobalState {
if became_quiescent { if became_quiescent {
if self.config.check_on_save() { if self.config.check_on_save() {
// Project has loaded properly, kick off initial flycheck // 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() { if self.config.prefill_caches() {
self.prime_caches_queue.request_op("became quiescent".to_owned(), ()); self.prime_caches_queue.request_op("became quiescent".to_owned(), ());

View file

@ -234,6 +234,11 @@ each of them, with the working directory being the workspace root
by changing `#rust-analyzer.check.invocationStrategy#` and by changing `#rust-analyzer.check.invocationStrategy#` and
`#rust-analyzer.check.invocationLocation#`. `#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: An example command would be:
```bash ```bash

View file

@ -775,7 +775,7 @@
] ]
}, },
"rust-analyzer.check.overrideCommand": { "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, "default": null,
"type": [ "type": [
"null", "null",