More robust status notifications

This commit is contained in:
Aleksey Kladov 2021-04-06 14:16:35 +03:00
parent 9143e3925c
commit 8fe20b19d4
11 changed files with 169 additions and 154 deletions

View file

@ -445,8 +445,8 @@ pub fn code_action_group(&self) -> bool {
pub fn hover_actions(&self) -> bool {
self.experimental("hoverActions")
}
pub fn status_notification(&self) -> bool {
self.experimental("statusNotification")
pub fn server_status_notification(&self) -> bool {
self.experimental("serverStatusNotification")
}
pub fn publish_diagnostics(&self) -> bool {

View file

@ -23,6 +23,7 @@
document::DocumentData,
from_proto,
line_index::{LineEndings, LineIndex},
lsp_ext,
main_loop::Task,
op_queue::OpQueue,
reload::SourceRootConfig,
@ -32,20 +33,6 @@
Result,
};
#[derive(Eq, PartialEq, Copy, Clone)]
pub(crate) enum Status {
Loading,
Ready { partial: bool },
Invalid,
NeedsReload,
}
impl Default for Status {
fn default() -> Self {
Status::Loading
}
}
// Enforces drop order
pub(crate) struct Handle<H, C> {
pub(crate) handle: H,
@ -73,7 +60,7 @@ pub(crate) struct GlobalState {
pub(crate) mem_docs: FxHashMap<VfsPath, DocumentData>,
pub(crate) semantic_tokens_cache: Arc<Mutex<FxHashMap<Url, SemanticTokens>>>,
pub(crate) shutdown_requested: bool,
pub(crate) status: Status,
pub(crate) last_reported_status: Option<lsp_ext::ServerStatusParams>,
pub(crate) source_root_config: SourceRootConfig,
pub(crate) proc_macro_client: Option<ProcMacroClient>,
@ -83,6 +70,7 @@ pub(crate) struct GlobalState {
pub(crate) vfs: Arc<RwLock<(vfs::Vfs, FxHashMap<FileId, LineEndings>)>>,
pub(crate) vfs_config_version: u32,
pub(crate) vfs_progress_config_version: u32,
pub(crate) vfs_progress_n_total: usize,
pub(crate) vfs_progress_n_done: usize,
@ -141,7 +129,7 @@ pub(crate) fn new(sender: Sender<lsp_server::Message>, config: Config) -> Global
mem_docs: FxHashMap::default(),
semantic_tokens_cache: Arc::new(Default::default()),
shutdown_requested: false,
status: Status::default(),
last_reported_status: None,
source_root_config: SourceRootConfig::default(),
proc_macro_client: None,
@ -151,14 +139,15 @@ pub(crate) fn new(sender: Sender<lsp_server::Message>, config: Config) -> Global
vfs: Arc::new(RwLock::new((vfs::Vfs::default(), FxHashMap::default()))),
vfs_config_version: 0,
vfs_progress_config_version: 0,
vfs_progress_n_total: 0,
vfs_progress_n_done: 0,
workspaces: Arc::new(Vec::new()),
fetch_workspaces_queue: OpQueue::default(),
workspace_build_data: None,
fetch_build_data_queue: OpQueue::default(),
fetch_build_data_queue: OpQueue::default(),
latest_requests: Default::default(),
}
}

View file

@ -241,26 +241,26 @@ pub struct SsrParams {
pub selections: Vec<lsp_types::Range>,
}
pub enum StatusNotification {}
pub enum ServerStatusNotification {}
#[derive(Serialize, Deserialize)]
impl Notification for ServerStatusNotification {
type Params = ServerStatusParams;
const METHOD: &'static str = "experimental/serverStatus";
}
#[derive(Deserialize, Serialize, PartialEq, Eq, Clone)]
pub struct ServerStatusParams {
pub health: Health,
pub quiescent: bool,
pub message: Option<String>,
}
#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum Status {
Loading,
ReadyPartial,
Ready,
NeedsReload,
Invalid,
}
#[derive(Deserialize, Serialize)]
pub struct StatusParams {
pub status: Status,
}
impl Notification for StatusNotification {
type Params = StatusParams;
const METHOD: &'static str = "rust-analyzer/status";
pub enum Health {
Ok,
Warning,
Error,
}
pub enum CodeActionRequest {}

View file

@ -21,7 +21,7 @@
dispatch::{NotificationDispatcher, RequestDispatcher},
document::DocumentData,
from_proto,
global_state::{file_id_to_url, url_to_file_id, GlobalState, Status},
global_state::{file_id_to_url, url_to_file_id, GlobalState},
handlers, lsp_ext,
lsp_utils::{apply_document_changes, is_canceled, notification_is, Progress},
reload::{BuildDataProgress, ProjectWorkspaceProgress},
@ -189,7 +189,7 @@ fn handle_event(&mut self, event: Event) -> Result<()> {
log::info!("task queue len: {}", task_queue_len);
}
let mut new_status = self.status;
let was_quiescent = self.is_quiescent();
match event {
Event::Lsp(msg) => match msg {
lsp_server::Message::Request(req) => self.on_request(loop_start, req)?,
@ -314,9 +314,12 @@ fn handle_event(&mut self, event: Event) -> Result<()> {
}
}
vfs::loader::Message::Progress { n_total, n_done, config_version } => {
always!(config_version <= self.vfs_config_version);
self.vfs_progress_config_version = config_version;
self.vfs_progress_n_total = n_total;
self.vfs_progress_n_done = n_done;
always!(config_version <= self.vfs_config_version);
let state = if n_done == 0 {
Progress::Begin
} else if n_done < n_total {
@ -406,18 +409,14 @@ fn handle_event(&mut self, event: Event) -> Result<()> {
}
let state_changed = self.process_changes();
let prev_status = self.status;
if prev_status != new_status {
self.transition(new_status);
}
let is_ready = matches!(self.status, Status::Ready { .. });
if prev_status == Status::Loading && is_ready {
if self.is_quiescent() && !was_quiescent {
for flycheck in &self.flycheck {
flycheck.update();
}
}
if is_ready && (state_changed || prev_status == Status::Loading) {
if self.is_quiescent() && (!was_quiescent || state_changed) {
self.update_file_notifications_on_threadpool();
// Refresh semantic tokens if the client supports it.
@ -451,6 +450,8 @@ fn handle_event(&mut self, event: Event) -> Result<()> {
}
self.fetch_build_data_if_needed();
self.report_new_status_if_needed();
let loop_duration = loop_start.elapsed();
if loop_duration > Duration::from_millis(100) {
log::warn!("overly long loop turn: {:?}", loop_duration);
@ -477,7 +478,8 @@ fn on_request(&mut self, request_received: Instant, req: Request) -> Result<()>
return Ok(());
}
if self.status == Status::Loading && req.method != "shutdown" {
// Avoid flashing a bunch of unresolved references during initial load.
if self.workspaces.is_empty() && !self.is_quiescent() {
self.respond(lsp_server::Response::new_err(
req.id,
// FIXME: i32 should impl From<ErrorCode> (from() guarantees lossless conversion)

View file

@ -2,27 +2,27 @@
//! at a time.
pub(crate) struct OpQueue<Args, Output> {
op_scheduled: Option<Args>,
op_requested: Option<Args>,
op_in_progress: bool,
last_op_result: Output,
}
impl<Args, Output: Default> Default for OpQueue<Args, Output> {
fn default() -> Self {
Self { op_scheduled: None, op_in_progress: false, last_op_result: Default::default() }
Self { op_requested: None, op_in_progress: false, last_op_result: Default::default() }
}
}
impl<Args, Output> OpQueue<Args, Output> {
pub(crate) fn request_op(&mut self, data: Args) {
self.op_scheduled = Some(data);
self.op_requested = Some(data);
}
pub(crate) fn should_start_op(&mut self) -> Option<Args> {
if self.op_in_progress {
return None;
}
self.op_in_progress = self.op_scheduled.is_some();
self.op_scheduled.take()
self.op_in_progress = self.op_requested.is_some();
self.op_requested.take()
}
pub(crate) fn op_completed(&mut self, result: Output) {
assert!(self.op_in_progress);
@ -34,4 +34,10 @@ pub(crate) fn op_completed(&mut self, result: Output) {
pub(crate) fn last_op_result(&self) -> &Output {
&self.last_op_result
}
pub(crate) fn op_in_progress(&self) -> bool {
self.op_in_progress
}
pub(crate) fn op_requested(&self) -> bool {
self.op_requested.is_some()
}
}

View file

@ -9,11 +9,10 @@
use crate::{
config::{Config, FilesWatcher, LinkedProject},
global_state::{GlobalState, Status},
global_state::GlobalState,
lsp_ext,
main_loop::Task,
};
use lsp_ext::StatusParams;
#[derive(Debug)]
pub(crate) enum ProjectWorkspaceProgress {
@ -30,6 +29,13 @@ pub(crate) enum BuildDataProgress {
}
impl GlobalState {
pub(crate) fn is_quiescent(&self) -> bool {
!(self.fetch_workspaces_queue.op_in_progress()
|| self.fetch_build_data_queue.op_in_progress()
|| self.vfs_progress_config_version < self.vfs_config_version
|| self.vfs_progress_n_done < self.vfs_progress_n_total)
}
pub(crate) fn update_configuration(&mut self, config: Config) {
let _p = profile::span("GlobalState::update_configuration");
let old_config = mem::replace(&mut self.config, Arc::new(config));
@ -46,17 +52,13 @@ pub(crate) fn maybe_refresh(&mut self, changes: &[(AbsPathBuf, ChangeKind)]) {
if !changes.iter().any(|(path, kind)| is_interesting(path, *kind)) {
return;
}
match self.status {
Status::Loading | Status::NeedsReload => return,
Status::Ready { .. } | Status::Invalid => (),
}
log::info!(
"Reloading workspace because of the following changes: {}",
"Requesting workspace reload because of the following changes: {}",
itertools::join(
changes
.iter()
.filter(|(path, kind)| is_interesting(path, *kind))
.map(|(path, kind)| format!("{}/{:?}", path.display(), kind)),
.map(|(path, kind)| format!("{}: {:?}", path.display(), kind)),
", "
)
);
@ -97,19 +99,31 @@ fn is_interesting(path: &AbsPath, change_kind: ChangeKind) -> bool {
false
}
}
pub(crate) fn transition(&mut self, new_status: Status) {
self.status = new_status;
if self.config.status_notification() {
let lsp_status = match new_status {
Status::Loading => lsp_ext::Status::Loading,
Status::Ready { partial: true } => lsp_ext::Status::ReadyPartial,
Status::Ready { partial: false } => lsp_ext::Status::Ready,
Status::Invalid => lsp_ext::Status::Invalid,
Status::NeedsReload => lsp_ext::Status::NeedsReload,
};
self.send_notification::<lsp_ext::StatusNotification>(StatusParams {
status: lsp_status,
});
pub(crate) fn report_new_status_if_needed(&mut self) {
if !self.config.server_status_notification() {
return;
}
let mut status = lsp_ext::ServerStatusParams {
health: lsp_ext::Health::Ok,
quiescent: self.is_quiescent(),
message: None,
};
if !self.config.cargo_autoreload()
&& self.is_quiescent()
&& self.fetch_workspaces_queue.op_requested()
{
status.health = lsp_ext::Health::Warning;
status.message = Some("Workspace reload required".to_string())
}
if let Some(error) = self.loading_error() {
status.health = lsp_ext::Health::Error;
status.message = Some(format!("Workspace reload failed: {}", error))
}
if self.last_reported_status.as_ref() != Some(&status) {
self.last_reported_status = Some(status.clone());
self.send_notification::<lsp_ext::ServerStatusNotification>(status);
}
}
@ -201,45 +215,28 @@ pub(crate) fn fetch_build_data_completed(
pub(crate) fn switch_workspaces(&mut self) {
let _p = profile::span("GlobalState::switch_workspaces");
let workspaces = self.fetch_workspaces_queue.last_op_result();
log::info!("will switch workspaces: {:?}", workspaces);
log::info!("will switch workspaces");
let mut error_message = None;
let workspaces = workspaces
.iter()
.filter_map(|res| match res {
Ok(it) => Some(it.clone()),
Err(err) => {
log::error!("failed to load workspace: {:#}", err);
let message = error_message.get_or_insert_with(String::new);
stdx::format_to!(
message,
"rust-analyzer failed to load workspace: {:#}\n",
err
);
None
}
})
.collect::<Vec<_>>();
let workspace_build_data = match self.fetch_build_data_queue.last_op_result() {
Some(Ok(it)) => Some(it.clone()),
None => None,
Some(Err(err)) => {
log::error!("failed to fetch build data: {:#}", err);
let message = error_message.get_or_insert_with(String::new);
stdx::format_to!(message, "rust-analyzer failed to fetch build data: {:#}\n", err);
None
}
};
if let Some(error_message) = error_message {
if let Some(error_message) = self.loading_error() {
log::error!("failed to switch workspaces: {}", error_message);
self.show_message(lsp_types::MessageType::Error, error_message);
if !self.workspaces.is_empty() {
return;
}
}
let workspaces = self
.fetch_workspaces_queue
.last_op_result()
.iter()
.filter_map(|res| res.as_ref().ok().cloned())
.collect::<Vec<_>>();
let workspace_build_data = match self.fetch_build_data_queue.last_op_result() {
Some(Ok(it)) => Some(it.clone()),
None | Some(Err(_)) => None,
};
if *self.workspaces == workspaces && self.workspace_build_data == workspace_build_data {
return;
}
@ -346,6 +343,24 @@ pub(crate) fn switch_workspaces(&mut self) {
log::info!("did switch workspaces");
}
fn loading_error(&self) -> Option<String> {
let mut message = None;
for ws in self.fetch_workspaces_queue.last_op_result() {
if let Err(err) = ws {
let message = message.get_or_insert_with(String::new);
stdx::format_to!(message, "rust-analyzer failed to load workspace: {:#}\n", err);
}
}
if let Some(Err(err)) = self.fetch_build_data_queue.last_op_result() {
let message = message.get_or_insert_with(String::new);
stdx::format_to!(message, "rust-analyzer failed to fetch build data: {:#}\n", err);
}
message
}
fn reload_flycheck(&mut self) {
let _p = profile::span("GlobalState::reload_flycheck");
let config = match self.config.flycheck() {

View file

@ -103,7 +103,7 @@ pub(crate) fn server(self) -> Server {
..Default::default()
}),
experimental: Some(json!({
"statusNotification": true,
"serverStatusNotification": true,
})),
..Default::default()
},
@ -213,13 +213,12 @@ fn send_request_(&self, r: Request) -> Value {
}
pub(crate) fn wait_until_workspace_is_loaded(self) -> Server {
self.wait_for_message_cond(1, &|msg: &Message| match msg {
Message::Notification(n) if n.method == "rust-analyzer/status" => {
Message::Notification(n) if n.method == "experimental/serverStatus" => {
let status = n
.clone()
.extract::<lsp_ext::StatusParams>("rust-analyzer/status")
.unwrap()
.status;
matches!(status, lsp_ext::Status::Ready)
.extract::<lsp_ext::ServerStatusParams>("experimental/serverStatus")
.unwrap();
status.quiescent
}
_ => false,
})

View file

@ -1,5 +1,5 @@
<!---
lsp_ext.rs hash: e8a7502bd2b2c2f5
lsp_ext.rs hash: faae991334a151d0
If you need to change the above hash to make the test pass, please check if you
need to adjust this doc as well and ping this issue:
@ -419,23 +419,37 @@ Returns internal status message, mostly for debugging purposes.
Reloads project information (that is, re-executes `cargo metadata`).
## Status Notification
## Server Status
**Experimental Client Capability:** `{ "statusNotification": boolean }`
**Experimental Client Capability:** `{ "serverStatus": boolean }`
**Method:** `rust-analyzer/status`
**Method:** `experimental/serverStatus`
**Notification:**
```typescript
interface StatusParams {
status: "loading" | "readyPartial" | "ready" | "invalid" | "needsReload",
interface ServerStatusParams {
/// `ok` means that the server is completely functional.
///
/// `warning` means that the server is partially functional.
/// It can server requests, but some results might be wrong due to,
/// for example, some missing dependencies.
///
/// `error` means that the server is not functional. For example,
/// there's a fatal build configuration problem.
health: "ok" | "warning" | "error",
/// Is there any pending background work which might change the status?
/// For example, are dependencies being downloaded?
quiescent: bool,
/// Explanatory message to show on hover.
message?: string,
}
```
This notification is sent from server to client.
The client can use it to display persistent status to the user (in modline).
For `needsReload` state, the client can provide a context-menu action to run `rust-analyzer/reloadWorkspace` request.
The client can use it to display *persistent* status to the user (in modline).
It is similar to the `showMessage`, but is intended for stares rather than point-in-time events.
## Syntax Tree

View file

@ -159,7 +159,7 @@ class ExperimentalFeatures implements lc.StaticFeature {
caps.snippetTextEdit = true;
caps.codeActionGroup = true;
caps.hoverActions = true;
caps.statusNotification = true;
caps.serverStatusNotification = true;
capabilities.experimental = caps;
}
initialize(_capabilities: lc.ServerCapabilities<any>, _documentSelector: lc.DocumentSelector | undefined): void {

View file

@ -5,7 +5,7 @@ import * as ra from './lsp_ext';
import { Config } from './config';
import { createClient } from './client';
import { isRustEditor, RustEditor } from './util';
import { Status } from './lsp_ext';
import { ServerStatusParams } from './lsp_ext';
export class Ctx {
private constructor(
@ -36,7 +36,7 @@ export class Ctx {
res.pushCleanup(client.start());
await client.onReady();
client.onNotification(ra.status, (params) => res.setStatus(params.status));
client.onNotification(ra.serverStatus, (params) => res.setServerStatus(params));
return res;
}
@ -66,39 +66,28 @@ export class Ctx {
return this.extCtx.subscriptions;
}
setStatus(status: Status) {
switch (status) {
case "loading":
this.statusBar.text = "$(sync~spin) rust-analyzer";
this.statusBar.tooltip = "Loading the project";
this.statusBar.command = undefined;
setServerStatus(status: ServerStatusParams) {
this.statusBar.tooltip = status.message ?? "Ready";
let icon = "";
switch (status.health) {
case "ok":
this.statusBar.color = undefined;
break;
case "readyPartial":
this.statusBar.text = "rust-analyzer";
this.statusBar.tooltip = "Ready (Partial)";
this.statusBar.command = undefined;
this.statusBar.color = undefined;
break;
case "ready":
this.statusBar.text = "rust-analyzer";
this.statusBar.tooltip = "Ready";
this.statusBar.command = undefined;
this.statusBar.color = undefined;
break;
case "invalid":
this.statusBar.text = "$(error) rust-analyzer";
this.statusBar.tooltip = "Failed to load the project";
this.statusBar.command = undefined;
this.statusBar.color = new vscode.ThemeColor("notificationsErrorIcon.foreground");
break;
case "needsReload":
this.statusBar.text = "$(warning) rust-analyzer";
this.statusBar.tooltip = "Click to reload";
case "warning":
this.statusBar.tooltip += "\nClick to reload."
this.statusBar.command = "rust-analyzer.reloadWorkspace";
this.statusBar.color = new vscode.ThemeColor("notificationsWarningIcon.foreground");
icon = "$(warning) ";
break;
case "error":
this.statusBar.tooltip += "\nClick to reload."
this.statusBar.command = "rust-analyzer.reloadWorkspace";
this.statusBar.color = new vscode.ThemeColor("notificationsErrorIcon.foreground");
icon = "$(error) ";
break;
}
if (!status.quiescent) icon = "$(sync~spin) ";
this.statusBar.text = `${icon} rust-analyzer`;
}
pushCleanup(d: Disposable) {

View file

@ -10,11 +10,12 @@ export interface AnalyzerStatusParams {
export const analyzerStatus = new lc.RequestType<AnalyzerStatusParams, string, void>("rust-analyzer/analyzerStatus");
export const memoryUsage = new lc.RequestType0<string, void>("rust-analyzer/memoryUsage");
export type Status = "loading" | "ready" | "readyPartial" | "invalid" | "needsReload";
export interface StatusParams {
status: Status;
export interface ServerStatusParams {
health: "ok" | "warning" | "error"
quiescent: boolean
message?: string
}
export const status = new lc.NotificationType<StatusParams>("rust-analyzer/status");
export const serverStatus = new lc.NotificationType<ServerStatusParams>("experimental/serverStatus");
export const reloadWorkspace = new lc.RequestType0<null, void>("rust-analyzer/reloadWorkspace");