diff --git a/Cargo.lock b/Cargo.lock index c2d00adebc5..bed9acf8fc5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -967,6 +967,7 @@ dependencies = [ "crossbeam-channel", "jod-thread", "log", + "ra_progress", "ra_toolchain", "serde_json", ] @@ -1080,7 +1081,11 @@ dependencies = [ "ra_hir", "ra_ide_db", "ra_prof", +<<<<<<< HEAD "ra_ssr", +======= + "ra_progress", +>>>>>>> Veetaha-feat/sync-branch "ra_syntax", "ra_text_edit", "rand", @@ -1168,6 +1173,13 @@ dependencies = [ "ra_arena", ] +[[package]] +name = "ra_progress" +version = "0.1.0" +dependencies = [ + "crossbeam-channel", +] + [[package]] name = "ra_project_model" version = "0.1.0" @@ -1392,6 +1404,7 @@ dependencies = [ "ra_mbe", "ra_proc_macro_srv", "ra_prof", + "ra_progress", "ra_project_model", "ra_syntax", "ra_text_edit", diff --git a/crates/ra_flycheck/Cargo.toml b/crates/ra_flycheck/Cargo.toml index 1aa39badea9..838973963b9 100644 --- a/crates/ra_flycheck/Cargo.toml +++ b/crates/ra_flycheck/Cargo.toml @@ -14,3 +14,4 @@ cargo_metadata = "0.10.0" serde_json = "1.0.48" jod-thread = "0.1.1" ra_toolchain = { path = "../ra_toolchain" } +ra_progress = { path = "../ra_progress" } diff --git a/crates/ra_flycheck/src/lib.rs b/crates/ra_flycheck/src/lib.rs index 6c41705298b..7b9f48eb025 100644 --- a/crates/ra_flycheck/src/lib.rs +++ b/crates/ra_flycheck/src/lib.rs @@ -3,6 +3,7 @@ //! LSP diagnostics based on the output of the command. use std::{ + fmt, io::{self, BufReader}, path::PathBuf, process::{Command, Stdio}, @@ -16,6 +17,9 @@ pub use cargo_metadata::diagnostic::{ Applicability, Diagnostic, DiagnosticLevel, DiagnosticSpan, DiagnosticSpanMacroExpansion, }; +type Progress = ra_progress::Progress<(), String>; +type ProgressSource = ra_progress::ProgressSource<(), String>; + #[derive(Clone, Debug, PartialEq, Eq)] pub enum FlycheckConfig { CargoCommand { @@ -31,6 +35,17 @@ pub enum FlycheckConfig { }, } +impl fmt::Display for FlycheckConfig { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + FlycheckConfig::CargoCommand { command, .. } => write!(f, "cargo {}", command), + FlycheckConfig::CustomCommand { command, args } => { + write!(f, "{} {}", command, args.join(" ")) + } + } + } +} + /// Flycheck wraps the shared state and communication machinery used for /// running `cargo check` (or other compatible command) and providing /// diagnostics based on the output. @@ -44,11 +59,15 @@ pub struct Flycheck { } impl Flycheck { - pub fn new(config: FlycheckConfig, workspace_root: PathBuf) -> Flycheck { + pub fn new( + config: FlycheckConfig, + workspace_root: PathBuf, + progress_src: ProgressSource, + ) -> Flycheck { let (task_send, task_recv) = unbounded::(); let (cmd_send, cmd_recv) = unbounded::(); let handle = jod_thread::spawn(move || { - FlycheckThread::new(config, workspace_root).run(&task_send, &cmd_recv); + FlycheckThread::new(config, workspace_root, progress_src).run(&task_send, &cmd_recv); }); Flycheck { task_recv, cmd_send, handle } } @@ -66,16 +85,6 @@ pub enum CheckTask { /// Request adding a diagnostic with fixes included to a file AddDiagnostic { workspace_root: PathBuf, diagnostic: Diagnostic }, - - /// Request check progress notification to client - Status(Status), -} - -#[derive(Debug)] -pub enum Status { - Being, - Progress(String), - End, } pub enum CheckCommand { @@ -87,6 +96,8 @@ struct FlycheckThread { config: FlycheckConfig, workspace_root: PathBuf, last_update_req: Option, + progress_src: ProgressSource, + progress: Option, // XXX: drop order is significant message_recv: Receiver, /// WatchThread exists to wrap around the communication needed to be able to @@ -98,11 +109,17 @@ struct FlycheckThread { } impl FlycheckThread { - fn new(config: FlycheckConfig, workspace_root: PathBuf) -> FlycheckThread { + fn new( + config: FlycheckConfig, + workspace_root: PathBuf, + progress_src: ProgressSource, + ) -> FlycheckThread { FlycheckThread { config, workspace_root, + progress_src, last_update_req: None, + progress: None, message_recv: never(), check_process: None, } @@ -140,9 +157,9 @@ impl FlycheckThread { } } - fn clean_previous_results(&self, task_send: &Sender) { + fn clean_previous_results(&mut self, task_send: &Sender) { task_send.send(CheckTask::ClearDiagnostics).unwrap(); - task_send.send(CheckTask::Status(Status::End)).unwrap(); + self.progress = None; } fn should_recheck(&mut self) -> bool { @@ -161,18 +178,17 @@ impl FlycheckThread { } } - fn handle_message(&self, msg: CheckEvent, task_send: &Sender) { + fn handle_message(&mut self, msg: CheckEvent, task_send: &Sender) { match msg { CheckEvent::Begin => { - task_send.send(CheckTask::Status(Status::Being)).unwrap(); + self.progress = Some(self.progress_src.begin(())); } - - CheckEvent::End => { - task_send.send(CheckTask::Status(Status::End)).unwrap(); - } - + CheckEvent::End => self.progress = None, CheckEvent::Msg(Message::CompilerArtifact(msg)) => { - task_send.send(CheckTask::Status(Status::Progress(msg.target.name))).unwrap(); + self.progress + .as_mut() + .expect("check process reported progress without the 'Begin' notification") + .report(msg.target.name); } CheckEvent::Msg(Message::CompilerMessage(msg)) => { diff --git a/crates/ra_progress/Cargo.toml b/crates/ra_progress/Cargo.toml new file mode 100644 index 00000000000..c7f7c6dd342 --- /dev/null +++ b/crates/ra_progress/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "ra_progress" +version = "0.1.0" +authors = ["rust-analyzer developers"] +edition = "2018" + +[dependencies] +crossbeam-channel = { version = "0.4" } diff --git a/crates/ra_progress/src/lib.rs b/crates/ra_progress/src/lib.rs new file mode 100644 index 00000000000..0ff1f846ca1 --- /dev/null +++ b/crates/ra_progress/src/lib.rs @@ -0,0 +1,129 @@ +//! General-purpose instrumentation for progress reporting. +//! +//! Note: +//! Most of the methods accept `&mut self` just to be more restrictive (for forward compat) +//! even tho for some of them we can weaken this requirement to shared reference (`&self`). + +use crossbeam_channel::Receiver; +use std::fmt; + +#[derive(Debug)] +pub enum ProgressStatus { + Begin(B), + Progress(P), + End, +} + +pub struct Progress(Option>>); +impl Progress { + pub fn report(&mut self, payload: P) { + self.report_with(|| payload); + } + + pub fn report_with(&mut self, payload: impl FnOnce() -> P) { + self.send_status(|| ProgressStatus::Progress(payload())); + } + + fn send_status(&self, status: impl FnOnce() -> ProgressStatus) { + if let Some(sender) = &self.0 { + sender.try_send(status()).expect("progress report must not block"); + } + } +} + +impl Drop for Progress { + fn drop(&mut self) { + self.send_status(|| ProgressStatus::End); + } +} + +pub struct ProgressSource(Option>>); +impl ProgressSource { + pub fn real_if(real: bool) -> (Receiver>, Self) { + if real { + let (sender, receiver) = crossbeam_channel::unbounded(); + (receiver, Self(Some(sender))) + } else { + (crossbeam_channel::never(), Self(None)) + } + } + + pub fn begin(&mut self, payload: B) -> Progress { + self.begin_with(|| payload) + } + + pub fn begin_with(&mut self, payload: impl FnOnce() -> B) -> Progress { + let progress = Progress(self.0.clone()); + progress.send_status(|| ProgressStatus::Begin(payload())); + progress + } +} + +impl Clone for ProgressSource { + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} + +impl fmt::Debug for ProgressSource { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("ProgressSource").field(&self.0).finish() + } +} + +pub type U32ProgressStatus = ProgressStatus; + +#[derive(Debug)] +pub struct U32ProgressReport { + pub processed: u32, + pub total: u32, +} +impl U32ProgressReport { + pub fn percentage(&self) -> f64 { + f64::from(100 * self.processed) / f64::from(self.total) + } + pub fn to_message(&self, prefix: &str, unit: &str) -> String { + format!("{} ({}/{} {})", prefix, self.processed, self.total, unit) + } +} + +pub struct U32Progress { + inner: Progress, + processed: u32, + total: u32, +} + +#[derive(Debug, Eq, PartialEq)] +pub struct IsDone(pub bool); + +impl U32Progress { + pub fn report(&mut self, new_processed: u32) -> IsDone { + if self.processed < new_processed { + self.processed = new_processed; + self.inner.report(U32ProgressReport { processed: new_processed, total: self.total }); + } + IsDone(self.processed >= self.total) + } +} + +#[derive(Clone)] +pub struct U32ProgressSource { + inner: ProgressSource, +} + +impl U32ProgressSource { + pub fn real_if( + real: bool, + ) -> (Receiver>, Self) { + let (recv, inner) = ProgressSource::real_if(real); + (recv, Self { inner }) + } + + pub fn begin(&mut self, initial: u32, total: u32) -> U32Progress { + U32Progress { + inner: self.inner.begin(U32ProgressReport { processed: initial, total }), + processed: initial, + total, + } + } +} diff --git a/crates/rust-analyzer/Cargo.toml b/crates/rust-analyzer/Cargo.toml index 68d04f3e32c..2bbed395f71 100644 --- a/crates/rust-analyzer/Cargo.toml +++ b/crates/rust-analyzer/Cargo.toml @@ -48,6 +48,7 @@ hir = { path = "../ra_hir", package = "ra_hir" } hir_def = { path = "../ra_hir_def", package = "ra_hir_def" } hir_ty = { path = "../ra_hir_ty", package = "ra_hir_ty" } ra_proc_macro_srv = { path = "../ra_proc_macro_srv" } +ra_progress = { path = "../ra_progress" } [target.'cfg(windows)'.dependencies] winapi = "0.3.8" diff --git a/crates/rust-analyzer/src/global_state.rs b/crates/rust-analyzer/src/global_state.rs index 87f3fe4db8d..7759c0ae309 100644 --- a/crates/rust-analyzer/src/global_state.rs +++ b/crates/rust-analyzer/src/global_state.rs @@ -27,9 +27,13 @@ use crate::{ }; use rustc_hash::{FxHashMap, FxHashSet}; -fn create_flycheck(workspaces: &[ProjectWorkspace], config: &FlycheckConfig) -> Option { +fn create_flycheck( + workspaces: &[ProjectWorkspace], + config: &FlycheckConfig, + progress_src: &ProgressSource<(), String>, +) -> Option { // FIXME: Figure out the multi-workspace situation - workspaces.iter().find_map(|w| match w { + workspaces.iter().find_map(move |w| match w { ProjectWorkspace::Cargo { cargo, .. } => { let cargo_project_root = cargo.workspace_root().to_path_buf(); Some(Flycheck::new(config.clone(), cargo_project_root.into())) @@ -143,7 +147,12 @@ impl GlobalState { } change.set_crate_graph(crate_graph); - let flycheck = config.check.as_ref().and_then(|c| create_flycheck(&workspaces, c)); + let (flycheck_progress_receiver, flycheck_progress_src) = + ProgressSource::real_if(config.client_caps.work_done_progress); + let flycheck = config + .check + .as_ref() + .and_then(|c| create_flycheck(&workspaces, c, &flycheck_progress_src)); let mut analysis_host = AnalysisHost::new(lru_capacity); analysis_host.apply_change(change); @@ -153,6 +162,8 @@ impl GlobalState { loader, task_receiver, flycheck, + flycheck_progress_src, + flycheck_progress_receiver, diagnostics: Default::default(), mem_docs: FxHashSet::default(), vfs: Arc::new(RwLock::new((vfs, FxHashMap::default()))), @@ -170,8 +181,10 @@ impl GlobalState { pub(crate) fn update_configuration(&mut self, config: Config) { self.analysis_host.update_lru_capacity(config.lru_capacity); if config.check != self.config.check { - self.flycheck = - config.check.as_ref().and_then(|it| create_flycheck(&self.workspaces, it)); + self.flycheck = config + .check + .as_ref() + .and_then(|it| create_flycheck(&self.workspaces, it, &self.flycheck_progress_src)); } self.config = config; diff --git a/crates/rust-analyzer/src/lsp_utils.rs b/crates/rust-analyzer/src/lsp_utils.rs new file mode 100644 index 00000000000..c790227972b --- /dev/null +++ b/crates/rust-analyzer/src/lsp_utils.rs @@ -0,0 +1,52 @@ +//! Utilities for LSP-related boilerplate code. + +use crossbeam_channel::Sender; +use lsp_server::{Message, Notification, Request, RequestId}; +use ra_db::Canceled; +use serde::{de::DeserializeOwned, Serialize}; +use std::error::Error; + +pub fn show_message( + typ: lsp_types::MessageType, + message: impl Into, + sender: &Sender, +) { + let message = message.into(); + let params = lsp_types::ShowMessageParams { typ, message }; + let not = notification_new::(params); + sender.send(not.into()).unwrap(); +} + +pub(crate) fn is_canceled(e: &(dyn Error + 'static)) -> bool { + e.downcast_ref::().is_some() +} + +pub(crate) fn notification_is( + notification: &Notification, +) -> bool { + notification.method == N::METHOD +} + +pub(crate) fn notification_cast(notification: Notification) -> Result +where + N: lsp_types::notification::Notification, + N::Params: DeserializeOwned, +{ + notification.extract(N::METHOD) +} + +pub(crate) fn notification_new(params: N::Params) -> Notification +where + N: lsp_types::notification::Notification, + N::Params: Serialize, +{ + Notification::new(N::METHOD.to_string(), params) +} + +pub(crate) fn request_new(id: RequestId, params: R::Params) -> Request +where + R: lsp_types::request::Request, + R::Params: Serialize, +{ + Request::new(id, R::METHOD.to_string(), params) +} diff --git a/crates/rust-analyzer/src/main_loop.rs b/crates/rust-analyzer/src/main_loop.rs index eb9e7f9130d..7ccdbd29cf5 100644 --- a/crates/rust-analyzer/src/main_loop.rs +++ b/crates/rust-analyzer/src/main_loop.rs @@ -28,6 +28,14 @@ use crate::{ request_metrics::RequestMetrics, LspError, Result, }; +pub use lsp_utils::show_message; +use lsp_utils::{is_canceled, notification_cast, notification_is, notification_new, request_new}; +use ra_progress::{ + IsDone, ProgressStatus, U32Progress, U32ProgressReport, U32ProgressSource, U32ProgressStatus, +}; + +const FLYCHECK_PROGRESS_TOKEN: &str = "rustAnalyzer/flycheck"; +const ROOTS_SCANNED_PROGRESS_TOKEN: &str = "rustAnalyzer/rootsScanned"; pub fn main_loop(config: Config, connection: Connection) -> Result<()> { log::info!("initial config: {:#?}", config); @@ -138,6 +146,18 @@ pub fn main_loop(config: Config, connection: Connection) -> Result<()> { recv(global_state.flycheck.as_ref().map_or(&never(), |it| &it.task_recv)) -> task => match task { Ok(task) => Event::CheckWatcher(task), Err(RecvError) => return Err("check watcher died".into()), + }, + recv(global_state.flycheck_progress_receiver) -> status => match status { + Ok(status) => Event::ProgressReport(ProgressReport::Flycheck(status)), + Err(RecvError) => return Err("check watcher died".into()), + }, + recv(roots_scanned_progress_receiver) -> status => match status { + Ok(status) => Event::ProgressReport(ProgressReport::RootsScanned(status)), + Err(RecvError) => { + // Roots analysis has finished, we no longer need this receiver + roots_scanned_progress_receiver = never(); + continue; + } } }; if let Event::Msg(Message::Request(req)) = &event { @@ -169,6 +189,7 @@ pub fn main_loop(config: Config, connection: Connection) -> Result<()> { enum Task { Respond(Response), Notify(Notification), + SendRequest(Request), Diagnostic(DiagnosticTask), } @@ -177,6 +198,13 @@ enum Event { Task(Task), Vfs(vfs::loader::Message), CheckWatcher(CheckTask), + ProgressReport(ProgressReport), +} + +#[derive(Debug)] +enum ProgressReport { + Flycheck(ProgressStatus<(), String>), + RootsScanned(U32ProgressStatus), } impl fmt::Debug for Event { @@ -212,6 +240,7 @@ impl fmt::Debug for Event { Event::Task(it) => fmt::Debug::fmt(it, f), Event::Vfs(it) => fmt::Debug::fmt(it, f), Event::CheckWatcher(it) => fmt::Debug::fmt(it, f), + Event::ProgressReport(it) => fmt::Debug::fmt(it, f), } } } @@ -262,6 +291,9 @@ fn loop_turn( } }, Event::CheckWatcher(task) => on_check_task(task, global_state, task_sender)?, + Event::ProgressReport(report) => { + on_progress_report(report, task_sender, loop_state, global_state) + } Event::Msg(msg) => match msg { Message::Request(req) => { on_request(global_state, pool, task_sender, &connection.sender, loop_start, req)? @@ -826,7 +858,7 @@ where Err(e) => match e.downcast::() { Ok(lsp_error) => Response::new_err(id, lsp_error.code, lsp_error.message), Err(e) => { - if is_canceled(&e) { + if is_canceled(&*e) { Response::new_err( id, ErrorCode::ContentModified as i32, @@ -853,7 +885,7 @@ fn update_file_notifications_on_threadpool( for file_id in subscriptions { match handlers::publish_diagnostics(&world, file_id) { Err(e) => { - if !is_canceled(&e) { + if !is_canceled(&*e) { log::error!("failed to compute diagnostics: {:?}", e); } } diff --git a/crates/rust-analyzer/tests/heavy_tests/support.rs b/crates/rust-analyzer/tests/heavy_tests/support.rs index 49f194f7efb..15d2a05a419 100644 --- a/crates/rust-analyzer/tests/heavy_tests/support.rs +++ b/crates/rust-analyzer/tests/heavy_tests/support.rs @@ -202,7 +202,11 @@ impl Server { ProgressParams { token: lsp_types::ProgressToken::String(ref token), value: ProgressParamsValue::WorkDone(WorkDoneProgress::End(_)), +<<<<<<< HEAD } if token == "rustAnalyzer/roots scanned" => true, +======= + } if token == "rustAnalyzer/rootsScanned" => true, +>>>>>>> Veetaha-feat/sync-branch _ => false, } } diff --git a/editors/code/src/main.ts b/editors/code/src/main.ts index 12b4d051088..cdb63b46f92 100644 --- a/editors/code/src/main.ts +++ b/editors/code/src/main.ts @@ -5,7 +5,6 @@ import { promises as fs, PathLike } from "fs"; import * as commands from './commands'; import { activateInlayHints } from './inlay_hints'; -import { activateStatusDisplay } from './status_display'; import { Ctx } from './ctx'; import { Config, NIGHTLY_TAG } from './config'; import { log, assert, isValidExecutable } from './util'; @@ -117,8 +116,6 @@ export async function activate(context: vscode.ExtensionContext) { ctx.pushCleanup(activateTaskProvider(workspaceFolder)); - activateStatusDisplay(ctx); - activateInlayHints(ctx); vscode.workspace.onDidChangeConfiguration( diff --git a/editors/code/src/status_display.ts b/editors/code/src/status_display.ts deleted file mode 100644 index f9cadc8a225..00000000000 --- a/editors/code/src/status_display.ts +++ /dev/null @@ -1,100 +0,0 @@ -import * as vscode from 'vscode'; - -import { WorkDoneProgress, WorkDoneProgressBegin, WorkDoneProgressReport, WorkDoneProgressEnd, Disposable } from 'vscode-languageclient'; - -import { Ctx } from './ctx'; - -const spinnerFrames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']; - -export function activateStatusDisplay(ctx: Ctx) { - const statusDisplay = new StatusDisplay(ctx.config.checkOnSave.command); - ctx.pushCleanup(statusDisplay); - const client = ctx.client; - if (client != null) { - ctx.pushCleanup(client.onProgress( - WorkDoneProgress.type, - 'rustAnalyzer/cargoWatcher', - params => statusDisplay.handleProgressNotification(params) - )); - } -} - -class StatusDisplay implements Disposable { - packageName?: string; - - private i: number = 0; - private statusBarItem: vscode.StatusBarItem; - private command: string; - private timer?: NodeJS.Timeout; - - constructor(command: string) { - this.statusBarItem = vscode.window.createStatusBarItem( - vscode.StatusBarAlignment.Left, - 10, - ); - this.command = command; - this.statusBarItem.hide(); - } - - show() { - this.packageName = undefined; - - this.timer = - this.timer || - setInterval(() => { - this.tick(); - this.refreshLabel(); - }, 300); - - this.statusBarItem.show(); - } - - hide() { - if (this.timer) { - clearInterval(this.timer); - this.timer = undefined; - } - - this.statusBarItem.hide(); - } - - dispose() { - if (this.timer) { - clearInterval(this.timer); - this.timer = undefined; - } - - this.statusBarItem.dispose(); - } - - refreshLabel() { - if (this.packageName) { - this.statusBarItem.text = `${spinnerFrames[this.i]} cargo ${this.command} [${this.packageName}]`; - } else { - this.statusBarItem.text = `${spinnerFrames[this.i]} cargo ${this.command}`; - } - } - - handleProgressNotification(params: WorkDoneProgressBegin | WorkDoneProgressReport | WorkDoneProgressEnd) { - switch (params.kind) { - case 'begin': - this.show(); - break; - - case 'report': - if (params.message) { - this.packageName = params.message; - this.refreshLabel(); - } - break; - - case 'end': - this.hide(); - break; - } - } - - private tick() { - this.i = (this.i + 1) % spinnerFrames.length; - } -}