//! another compatible command (f.x. clippy) in a background thread and provide
//! LSP diagnostics based on the output of the command.
+#![warn(rust_2018_idioms, unused_lifetimes, semicolon_in_expressions_from_macros)]
+
use std::{
fmt, io,
process::{ChildStderr, ChildStdout, Command, Stdio},
// XXX: drop order is significant
sender: Sender<Restart>,
_thread: jod_thread::JoinHandle,
+ id: usize,
}
impl FlycheckHandle {
.name("Flycheck".to_owned())
.spawn(move || actor.run(receiver))
.expect("failed to spawn thread");
- FlycheckHandle { sender, _thread: thread }
+ FlycheckHandle { id, sender, _thread: thread }
}
/// Schedule a re-start of the cargo check worker.
pub fn update(&self) {
self.sender.send(Restart).unwrap();
}
+
+ pub fn id(&self) -> usize {
+ self.id
+ }
}
pub enum Message {
/// Request adding a diagnostic with fixes included to a file
- AddDiagnostic { workspace_root: AbsPathBuf, diagnostic: Diagnostic },
+ AddDiagnostic { id: usize, workspace_root: AbsPathBuf, diagnostic: Diagnostic },
/// Request check progress notification to client
Progress {
impl fmt::Debug for Message {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
- Message::AddDiagnostic { workspace_root, diagnostic } => f
+ Message::AddDiagnostic { id, workspace_root, diagnostic } => f
.debug_struct("AddDiagnostic")
+ .field("id", id)
.field("workspace_root", workspace_root)
.field("diagnostic_code", &diagnostic.code.as_ref().map(|it| &it.code))
.finish(),
}
}
Event::CheckEvent(None) => {
- tracing::debug!("flycheck finished");
+ tracing::debug!(flycheck_id = self.id, "flycheck finished");
// Watcher finished
let cargo_handle = self.cargo_handle.take().unwrap();
CargoMessage::Diagnostic(msg) => {
self.send(Message::AddDiagnostic {
+ id: self.id,
workspace_root: self.workspace_root.clone(),
diagnostic: msg,
});
//! Thin wrappers around `std::path`, distinguishing between absolute and
//! relative paths.
+
+#![warn(rust_2018_idioms, unused_lifetimes, semicolon_in_expressions_from_macros)]
+
use std::{
borrow::Borrow,
ffi::OsStr,
}
}
+ impl ToOwned for AbsPath {
+ type Owned = AbsPathBuf;
+
+ fn to_owned(&self) -> Self::Owned {
+ AbsPathBuf(self.0.to_owned())
+ }
+ }
+
impl<'a> TryFrom<&'a Path> for &'a AbsPath {
type Error = &'a Path;
fn try_from(path: &'a Path) -> Result<&'a AbsPath, &'a Path> {
pub(crate) proc_macro_changed: bool,
pub(crate) last_reported_status: Option<lsp_ext::ServerStatusParams>,
pub(crate) source_root_config: SourceRootConfig,
- pub(crate) proc_macro_client: Option<ProcMacroServer>,
+ pub(crate) proc_macro_clients: Vec<Result<ProcMacroServer, String>>,
pub(crate) flycheck: Vec<FlycheckHandle>,
pub(crate) flycheck_sender: Sender<flycheck::Message>,
proc_macro_changed: false,
last_reported_status: None,
source_root_config: SourceRootConfig::default(),
- proc_macro_client: None,
+ proc_macro_clients: vec![],
flycheck: Vec::new(),
flycheck_sender,
if let Some(path) = vfs.file_path(file.file_id).as_path() {
let path = path.to_path_buf();
if reload::should_refresh_for_change(&path, file.change_kind) {
+ tracing::warn!("fetch-fiel_change");
self.fetch_workspaces_queue
.request_op(format!("vfs file change: {}", path.display()));
}
}
}
+ // Clear native diagnostics when their file gets deleted
if !file.exists() {
self.diagnostics.clear_native_for(file.file_id);
}
};
pub(crate) fn handle_workspace_reload(state: &mut GlobalState, _: ()) -> Result<()> {
- state.proc_macro_client = None;
+ state.proc_macro_clients.clear();
state.proc_macro_changed = false;
state.fetch_workspaces_queue.request_op("reload workspace request".to_string());
state.fetch_build_data_queue.request_op("reload workspace request".to_string());
}
// Fixes from `cargo check`.
- for fix in snap.check_fixes.get(&frange.file_id).into_iter().flatten() {
+ for fix in
+ snap.check_fixes.values().filter_map(|it| it.get(&frange.file_id)).into_iter().flatten()
+ {
// FIXME: this mapping is awkward and shouldn't exist. Refactor
// `snap.check_fixes` to not convert to LSP prematurely.
let intersect_fix_range = fix
.unwrap(),
}),
source: Some("rust-analyzer".to_string()),
- message: d.message,
+ // https://github.com/rust-lang/rust-analyzer/issues/11404
+ message: if !d.message.is_empty() { d.message } else { " ".to_string() },
related_information: None,
tags: if d.unused { Some(vec![DiagnosticTag::UNNECESSARY]) } else { None },
data: None,
//! requests/replies and notifications back to the client.
use std::{
fmt,
+ ops::Deref,
sync::Arc,
time::{Duration, Instant},
};
use always_assert::always;
use crossbeam_channel::{select, Receiver};
- use ide_db::base_db::{SourceDatabaseExt, VfsPath};
+ use ide_db::base_db::{SourceDatabase, SourceDatabaseExt, VfsPath};
+ use itertools::Itertools;
use lsp_server::{Connection, Notification, Request};
use lsp_types::notification::Notification as _;
use vfs::{ChangeKind, FileId};
}
impl fmt::Debug for Event {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- let debug_verbose_not = |not: &Notification, f: &mut fmt::Formatter| {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ let debug_verbose_not = |not: &Notification, f: &mut fmt::Formatter<'_>| {
f.debug_struct("Notification").field("method", ¬.method).finish()
};
let _p = profile::span("GlobalState::handle_event/flycheck");
loop {
match task {
- flycheck::Message::AddDiagnostic { workspace_root, diagnostic } => {
+ flycheck::Message::AddDiagnostic { id, workspace_root, diagnostic } => {
let snap = self.snapshot();
let diagnostics =
crate::diagnostics::to_proto::map_rust_diagnostic_to_lsp(
for diag in diagnostics {
match url_to_file_id(&self.vfs.read().0, &diag.url) {
Ok(file_id) => self.diagnostics.add_check_diagnostic(
+ id,
file_id,
diag.diagnostic,
diag.fix,
flycheck::Message::Progress { id, progress } => {
let (state, message) = match progress {
flycheck::Progress::DidStart => {
- self.diagnostics.clear_check();
+ self.diagnostics.clear_check(id);
(Progress::Begin, None)
}
flycheck::Progress::DidCheckCrate(target) => {
let memdocs_added_or_removed = self.mem_docs.take_changes();
if self.is_quiescent() {
- if !was_quiescent {
+ if !was_quiescent
+ && !self.fetch_workspaces_queue.op_requested()
+ && !self.fetch_build_data_queue.op_requested()
+ {
for flycheck in &self.flycheck {
flycheck.update();
}
}
let url = file_id_to_url(&self.vfs.read().0, file_id);
- let diagnostics = self.diagnostics.diagnostics_for(file_id).cloned().collect();
+ let mut diagnostics =
+ self.diagnostics.diagnostics_for(file_id).cloned().collect::<Vec<_>>();
+ // https://github.com/rust-lang/rust-analyzer/issues/11404
+ for d in &mut diagnostics {
+ if d.message.is_empty() {
+ d.message = " ".to_string();
+ }
+ if let Some(rds) = d.related_information.as_mut() {
+ for rd in rds {
+ if rd.message.is_empty() {
+ rd.message = " ".to_string();
+ }
+ }
+ }
+ }
let version = from_proto::vfs_path(&url)
.map(|path| self.mem_docs.get(&path).map(|it| it.version))
.unwrap_or_default();
Ok(())
})?
.on::<lsp_types::notification::DidSaveTextDocument>(|this, params| {
- for flycheck in &this.flycheck {
- flycheck.update();
+ let mut updated = false;
+ if let Ok(vfs_path) = from_proto::vfs_path(¶ms.text_document.uri) {
+ let (vfs, _) = &*this.vfs.read();
+ if let Some(file_id) = vfs.file_id(&vfs_path) {
+ let analysis = this.analysis_host.analysis();
+ // Crates containing or depending on the saved file
+ let crate_ids: Vec<_> = analysis
+ .crate_for(file_id)?
+ .into_iter()
+ .flat_map(|id| {
+ this.analysis_host
+ .raw_database()
+ .crate_graph()
+ .transitive_rev_deps(id)
+ })
+ .sorted()
+ .unique()
+ .collect();
+
+ let crate_root_paths: Vec<_> = crate_ids
+ .iter()
+ .filter_map(|&crate_id| {
+ analysis
+ .crate_root(crate_id)
+ .map(|file_id| {
+ vfs.file_path(file_id).as_path().map(ToOwned::to_owned)
+ })
+ .transpose()
+ })
+ .collect::<ide::Cancellable<_>>()?;
+ let crate_root_paths: Vec<_> =
+ crate_root_paths.iter().map(Deref::deref).collect();
+
+ // Find all workspaces that have at least one target containing the saved file
+ let workspace_ids =
+ this.workspaces.iter().enumerate().filter(|(_, ws)| match ws {
+ project_model::ProjectWorkspace::Cargo { cargo, .. } => {
+ cargo.packages().any(|pkg| {
+ cargo[pkg].targets.iter().any(|&it| {
+ crate_root_paths.contains(&cargo[it].root.as_path())
+ })
+ })
+ }
+ project_model::ProjectWorkspace::Json { project, .. } => project
+ .crates()
+ .any(|(c, _)| crate_ids.iter().any(|&crate_id| crate_id == c)),
+ project_model::ProjectWorkspace::DetachedFiles { .. } => false,
+ });
+
+ // Find and trigger corresponding flychecks
+ for flycheck in &this.flycheck {
+ for (id, _) in workspace_ids.clone() {
+ if id == flycheck.id() {
+ updated = true;
+ flycheck.update();
+ continue;
+ }
+ }
+ }
+ }
+ if let Some(abs_path) = vfs_path.as_path() {
+ if reload::should_refresh_for_change(&abs_path, ChangeKind::Modify) {
+ this.fetch_workspaces_queue
+ .request_op(format!("DidSaveTextDocument {}", abs_path.display()));
+ }
+ }
}
- if let Ok(abs_path) = from_proto::abs_path(¶ms.text_document.uri) {
- if reload::should_refresh_for_change(&abs_path, ChangeKind::Modify) {
- this.fetch_workspaces_queue
- .request_op(format!("DidSaveTextDocument {}", abs_path.display()));
+ if !updated {
+ for flycheck in &this.flycheck {
+ flycheck.update();
}
}
Ok(())
let files_config = self.config.files();
let project_folders = ProjectFolders::new(&self.workspaces, &files_config.exclude);
- if self.proc_macro_client.is_none() {
+ let standalone_server_name =
+ format!("rust-analyzer-proc-macro-srv{}", std::env::consts::EXE_SUFFIX);
+
+ if self.proc_macro_clients.is_empty() {
if let Some((path, args)) = self.config.proc_macro_srv() {
- match ProcMacroServer::spawn(path.clone(), args) {
- Ok(it) => self.proc_macro_client = Some(it),
- Err(err) => {
- tracing::error!(
- "Failed to run proc_macro_srv from path {}, error: {:?}",
+ self.proc_macro_clients = self
+ .workspaces
+ .iter()
+ .map(|ws| {
+ let mut args = args.clone();
+ let mut path = path.clone();
+
+ if let ProjectWorkspace::Cargo { sysroot, .. } = ws {
+ tracing::info!("Found a cargo workspace...");
+ if let Some(sysroot) = sysroot.as_ref() {
+ tracing::info!("Found a cargo workspace with a sysroot...");
+ let server_path =
+ sysroot.root().join("libexec").join(&standalone_server_name);
+ if std::fs::metadata(&server_path).is_ok() {
+ tracing::info!(
+ "And the server exists at {}",
+ server_path.display()
+ );
+ path = server_path;
+ args = vec![];
+ } else {
+ tracing::info!(
+ "And the server does not exist at {}",
+ server_path.display()
+ );
+ }
+ }
+ }
+
+ tracing::info!(
+ "Using proc-macro server at {} with args {:?}",
path.display(),
- err
+ args
);
- }
- }
+ ProcMacroServer::spawn(path.clone(), args.clone()).map_err(|err| {
+ let error = format!(
+ "Failed to run proc_macro_srv from path {}, error: {:?}",
+ path.display(),
+ err
+ );
+ tracing::error!(error);
+ error
+ })
+ })
+ .collect();
}
}
let watch = match files_config.watcher {
FilesWatcher::Client => vec![],
- FilesWatcher::Notify => project_folders.watch,
+ FilesWatcher::Server => project_folders.watch,
};
self.vfs_config_version += 1;
self.loader.handle.set_config(vfs::loader::Config {
// Create crate graph from all the workspaces
let crate_graph = {
- let proc_macro_client = self.proc_macro_client.as_ref();
let dummy_replacements = self.config.dummy_replacements();
- let mut load_proc_macro = move |crate_name: &str, path: &AbsPath| {
- load_proc_macro(
- proc_macro_client,
- path,
- dummy_replacements.get(crate_name).map(|v| &**v).unwrap_or_default(),
- )
- };
let vfs = &mut self.vfs.write().0;
let loader = &mut self.loader;
};
let mut crate_graph = CrateGraph::default();
- for ws in self.workspaces.iter() {
+ for (idx, ws) in self.workspaces.iter().enumerate() {
+ let proc_macro_client = match self.proc_macro_clients.get(idx) {
+ Some(res) => res.as_ref().map_err(|e| &**e),
+ None => Err("Proc macros are disabled"),
+ };
+ let mut load_proc_macro = move |crate_name: &str, path: &AbsPath| {
+ load_proc_macro(
+ proc_macro_client,
+ path,
+ dummy_replacements.get(crate_name).map(|v| &**v).unwrap_or_default(),
+ )
+ };
crate_graph.extend(ws.to_crate_graph(&mut load_proc_macro, &mut load));
}
crate_graph
Some(it) => it,
None => {
self.flycheck = Vec::new();
- self.diagnostics.clear_check();
+ self.diagnostics.clear_check_all();
return;
}
};
/// Load the proc-macros for the given lib path, replacing all expanders whose names are in `dummy_replace`
/// with an identity dummy expander.
pub(crate) fn load_proc_macro(
- server: Option<&ProcMacroServer>,
+ server: Result<&ProcMacroServer, &str>,
path: &AbsPath,
dummy_replace: &[Box<str>],
) -> ProcMacroLoadResult {
let res: Result<Vec<_>, String> = (|| {
let dylib = MacroDylib::new(path.to_path_buf())
.map_err(|io| format!("Proc-macro dylib loading failed: {io}"))?;
- let server = server.ok_or_else(|| format!("Proc-macro server not started"))?;
+ let server = server.map_err(ToOwned::to_owned)?;
let vec = server.load_dylib(dylib).map_err(|e| format!("{e}"))?;
if vec.is_empty() {
return Err("proc macro library returned no proc macros".to_string());
};
let expander: Arc<dyn ProcMacroExpander> =
if dummy_replace.iter().any(|replace| &**replace == name) {
- Arc::new(DummyExpander)
+ match kind {
+ ProcMacroKind::Attr => Arc::new(IdentityExpander),
+ _ => Arc::new(EmptyExpander),
+ }
} else {
Arc::new(Expander(expander))
};
}
}
- /// Dummy identity expander, used for proc-macros that are deliberately ignored by the user.
+ /// Dummy identity expander, used for attribute proc-macros that are deliberately ignored by the user.
#[derive(Debug)]
- struct DummyExpander;
+ struct IdentityExpander;
- impl ProcMacroExpander for DummyExpander {
+ impl ProcMacroExpander for IdentityExpander {
fn expand(
&self,
subtree: &tt::Subtree,
Ok(subtree.clone())
}
}
+
+ /// Empty expander, used for proc-macros that are deliberately ignored by the user.
+ #[derive(Debug)]
+ struct EmptyExpander;
+
+ impl ProcMacroExpander for EmptyExpander {
+ fn expand(
+ &self,
+ _: &tt::Subtree,
+ _: Option<&tt::Subtree>,
+ _: &Env,
+ ) -> Result<tt::Subtree, ProcMacroExpansionError> {
+ Ok(tt::Subtree::default())
+ }
+ }
}
pub(crate) fn should_refresh_for_change(path: &AbsPath, change_kind: ChangeKind) -> bool {