Allow canceling in-progress language server work (e.g. cargo check) (#13173)

Release Notes:

- Added a more detailed message in place of the generic `checking...`
messages when Rust-analyzer is running.
- Added a rate limit for language server status messages, to reduce
noisiness of those updates.
- Added a `cancel language server work` action which will cancel
long-running language server tasks.

---------

Co-authored-by: Richard <richard@zed.dev>
This commit is contained in:
Max Brunsfeld 2024-06-17 17:58:47 -07:00 committed by GitHub
parent f489c8b79f
commit 7003b0f211
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 308 additions and 164 deletions

View file

@ -3,22 +3,19 @@ use editor::Editor;
use extension::ExtensionStore; use extension::ExtensionStore;
use futures::StreamExt; use futures::StreamExt;
use gpui::{ use gpui::{
actions, svg, AppContext, CursorStyle, EventEmitter, InteractiveElement as _, Model, actions, percentage, Animation, AnimationExt as _, AppContext, CursorStyle, EventEmitter,
ParentElement as _, Render, SharedString, StatefulInteractiveElement, Styled, View, InteractiveElement as _, Model, ParentElement as _, Render, SharedString,
ViewContext, VisualContext as _, StatefulInteractiveElement, Styled, Transformation, View, ViewContext, VisualContext as _,
}; };
use language::{LanguageRegistry, LanguageServerBinaryStatus, LanguageServerName}; use language::{LanguageRegistry, LanguageServerBinaryStatus, LanguageServerName};
use project::{LanguageServerProgress, Project}; use project::{LanguageServerProgress, Project};
use smallvec::SmallVec; use smallvec::SmallVec;
use std::{cmp::Reverse, fmt::Write, sync::Arc}; use std::{cmp::Reverse, fmt::Write, sync::Arc, time::Duration};
use ui::prelude::*; use ui::prelude::*;
use workspace::{item::ItemHandle, StatusItemView, Workspace}; use workspace::{item::ItemHandle, StatusItemView, Workspace};
actions!(activity_indicator, [ShowErrorMessage]); actions!(activity_indicator, [ShowErrorMessage]);
const DOWNLOAD_ICON: &str = "icons/download.svg";
const WARNING_ICON: &str = "icons/warning.svg";
pub enum Event { pub enum Event {
ShowError { lsp_name: Arc<str>, error: String }, ShowError { lsp_name: Arc<str>, error: String },
} }
@ -35,14 +32,13 @@ struct LspStatus {
} }
struct PendingWork<'a> { struct PendingWork<'a> {
language_server_name: &'a str,
progress_token: &'a str, progress_token: &'a str,
progress: &'a LanguageServerProgress, progress: &'a LanguageServerProgress,
} }
#[derive(Default)] #[derive(Default)]
struct Content { struct Content {
icon: Option<&'static str>, icon: Option<gpui::AnyElement>,
message: String, message: String,
on_click: Option<Arc<dyn Fn(&mut ActivityIndicator, &mut ViewContext<ActivityIndicator>)>>, on_click: Option<Arc<dyn Fn(&mut ActivityIndicator, &mut ViewContext<ActivityIndicator>)>>,
} }
@ -159,7 +155,6 @@ impl ActivityIndicator {
.pending_work .pending_work
.iter() .iter()
.map(|(token, progress)| PendingWork { .map(|(token, progress)| PendingWork {
language_server_name: status.name.as_str(),
progress_token: token.as_str(), progress_token: token.as_str(),
progress, progress,
}) })
@ -175,31 +170,41 @@ impl ActivityIndicator {
// Show any language server has pending activity. // Show any language server has pending activity.
let mut pending_work = self.pending_language_server_work(cx); let mut pending_work = self.pending_language_server_work(cx);
if let Some(PendingWork { if let Some(PendingWork {
language_server_name,
progress_token, progress_token,
progress, progress,
}) = pending_work.next() }) = pending_work.next()
{ {
let mut message = language_server_name.to_string(); let mut message = progress
.title
message.push_str(": "); .as_deref()
if let Some(progress_message) = progress.message.as_ref() { .unwrap_or(progress_token)
message.push_str(progress_message); .to_string();
} else {
message.push_str(progress_token);
}
if let Some(percentage) = progress.percentage { if let Some(percentage) = progress.percentage {
write!(&mut message, " ({}%)", percentage).unwrap(); write!(&mut message, " ({}%)", percentage).unwrap();
} }
if let Some(progress_message) = progress.message.as_ref() {
message.push_str(": ");
message.push_str(progress_message);
}
let additional_work_count = pending_work.count(); let additional_work_count = pending_work.count();
if additional_work_count > 0 { if additional_work_count > 0 {
write!(&mut message, " + {} more", additional_work_count).unwrap(); write!(&mut message, " + {} more", additional_work_count).unwrap();
} }
return Content { return Content {
icon: None, icon: Some(
Icon::new(IconName::ArrowCircle)
.size(IconSize::Small)
.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
)
.into_any_element(),
),
message, message,
on_click: None, on_click: None,
}; };
@ -222,7 +227,11 @@ impl ActivityIndicator {
if !downloading.is_empty() { if !downloading.is_empty() {
return Content { return Content {
icon: Some(DOWNLOAD_ICON), icon: Some(
Icon::new(IconName::Download)
.size(IconSize::Small)
.into_any_element(),
),
message: format!("Downloading {}...", downloading.join(", "),), message: format!("Downloading {}...", downloading.join(", "),),
on_click: None, on_click: None,
}; };
@ -230,7 +239,11 @@ impl ActivityIndicator {
if !checking_for_update.is_empty() { if !checking_for_update.is_empty() {
return Content { return Content {
icon: Some(DOWNLOAD_ICON), icon: Some(
Icon::new(IconName::Download)
.size(IconSize::Small)
.into_any_element(),
),
message: format!( message: format!(
"Checking for updates to {}...", "Checking for updates to {}...",
checking_for_update.join(", "), checking_for_update.join(", "),
@ -241,7 +254,11 @@ impl ActivityIndicator {
if !failed.is_empty() { if !failed.is_empty() {
return Content { return Content {
icon: Some(WARNING_ICON), icon: Some(
Icon::new(IconName::ExclamationTriangle)
.size(IconSize::Small)
.into_any_element(),
),
message: format!( message: format!(
"Failed to download {}. Click to show error.", "Failed to download {}. Click to show error.",
failed.join(", "), failed.join(", "),
@ -255,7 +272,11 @@ impl ActivityIndicator {
// Show any formatting failure // Show any formatting failure
if let Some(failure) = self.project.read(cx).last_formatting_failure() { if let Some(failure) = self.project.read(cx).last_formatting_failure() {
return Content { return Content {
icon: Some(WARNING_ICON), icon: Some(
Icon::new(IconName::ExclamationTriangle)
.size(IconSize::Small)
.into_any_element(),
),
message: format!("Formatting failed: {}. Click to see logs.", failure), message: format!("Formatting failed: {}. Click to see logs.", failure),
on_click: Some(Arc::new(|_, cx| { on_click: Some(Arc::new(|_, cx| {
cx.dispatch_action(Box::new(workspace::OpenLog)); cx.dispatch_action(Box::new(workspace::OpenLog));
@ -267,17 +288,29 @@ impl ActivityIndicator {
if let Some(updater) = &self.auto_updater { if let Some(updater) = &self.auto_updater {
return match &updater.read(cx).status() { return match &updater.read(cx).status() {
AutoUpdateStatus::Checking => Content { AutoUpdateStatus::Checking => Content {
icon: Some(DOWNLOAD_ICON), icon: Some(
Icon::new(IconName::Download)
.size(IconSize::Small)
.into_any_element(),
),
message: "Checking for Zed updates…".to_string(), message: "Checking for Zed updates…".to_string(),
on_click: None, on_click: None,
}, },
AutoUpdateStatus::Downloading => Content { AutoUpdateStatus::Downloading => Content {
icon: Some(DOWNLOAD_ICON), icon: Some(
Icon::new(IconName::Download)
.size(IconSize::Small)
.into_any_element(),
),
message: "Downloading Zed update…".to_string(), message: "Downloading Zed update…".to_string(),
on_click: None, on_click: None,
}, },
AutoUpdateStatus::Installing => Content { AutoUpdateStatus::Installing => Content {
icon: Some(DOWNLOAD_ICON), icon: Some(
Icon::new(IconName::Download)
.size(IconSize::Small)
.into_any_element(),
),
message: "Installing Zed update…".to_string(), message: "Installing Zed update…".to_string(),
on_click: None, on_click: None,
}, },
@ -292,7 +325,11 @@ impl ActivityIndicator {
})), })),
}, },
AutoUpdateStatus::Errored => Content { AutoUpdateStatus::Errored => Content {
icon: Some(WARNING_ICON), icon: Some(
Icon::new(IconName::ExclamationTriangle)
.size(IconSize::Small)
.into_any_element(),
),
message: "Auto update failed".to_string(), message: "Auto update failed".to_string(),
on_click: Some(Arc::new(|this, cx| { on_click: Some(Arc::new(|this, cx| {
this.dismiss_error_message(&Default::default(), cx) this.dismiss_error_message(&Default::default(), cx)
@ -307,7 +344,11 @@ impl ActivityIndicator {
{ {
if let Some(extension_id) = extension_store.outstanding_operations().keys().next() { if let Some(extension_id) = extension_store.outstanding_operations().keys().next() {
return Content { return Content {
icon: Some(DOWNLOAD_ICON), icon: Some(
Icon::new(IconName::Download)
.size(IconSize::Small)
.into_any_element(),
),
message: format!("Updating {extension_id} extension…"), message: format!("Updating {extension_id} extension…"),
on_click: None, on_click: None,
}; };
@ -338,7 +379,8 @@ impl Render for ActivityIndicator {
} }
result result
.children(content.icon.map(|icon| svg().path(icon))) .gap_2()
.children(content.icon)
.child(Label::new(SharedString::from(content.message)).size(LabelSize::Small)) .child(Label::new(SharedString::from(content.message)).size(LabelSize::Small))
} }
} }

View file

@ -28,7 +28,7 @@ use language::{
use multi_buffer::MultiBufferRow; use multi_buffer::MultiBufferRow;
use project::{ use project::{
project_settings::{InlineBlameSettings, ProjectSettings}, project_settings::{InlineBlameSettings, ProjectSettings},
SERVER_PROGRESS_DEBOUNCE_TIMEOUT, SERVER_PROGRESS_THROTTLE_TIMEOUT,
}; };
use recent_projects::disconnected_overlay::DisconnectedOverlay; use recent_projects::disconnected_overlay::DisconnectedOverlay;
use rpc::RECEIVE_TIMEOUT; use rpc::RECEIVE_TIMEOUT;
@ -1006,6 +1006,8 @@ async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut Tes
let fake_language_server = fake_language_servers.next().await.unwrap(); let fake_language_server = fake_language_servers.next().await.unwrap();
fake_language_server.start_progress("the-token").await; fake_language_server.start_progress("the-token").await;
executor.advance_clock(SERVER_PROGRESS_THROTTLE_TIMEOUT);
fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams { fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
token: lsp::NumberOrString::String("the-token".to_string()), token: lsp::NumberOrString::String("the-token".to_string()),
value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report( value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
@ -1015,7 +1017,6 @@ async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut Tes
}, },
)), )),
}); });
executor.advance_clock(SERVER_PROGRESS_DEBOUNCE_TIMEOUT);
executor.run_until_parked(); executor.run_until_parked();
project_a.read_with(cx_a, |project, _| { project_a.read_with(cx_a, |project, _| {
@ -1040,6 +1041,7 @@ async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut Tes
assert_eq!(status.name, "the-language-server"); assert_eq!(status.name, "the-language-server");
}); });
executor.advance_clock(SERVER_PROGRESS_THROTTLE_TIMEOUT);
fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams { fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
token: lsp::NumberOrString::String("the-token".to_string()), token: lsp::NumberOrString::String("the-token".to_string()),
value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report( value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
@ -1049,7 +1051,6 @@ async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut Tes
}, },
)), )),
}); });
executor.advance_clock(SERVER_PROGRESS_DEBOUNCE_TIMEOUT);
executor.run_until_parked(); executor.run_until_parked();
project_a.read_with(cx_a, |project, _| { project_a.read_with(cx_a, |project, _| {

View file

@ -1,9 +1,7 @@
use std::time::Duration;
use editor::Editor; use editor::Editor;
use gpui::{ use gpui::{
percentage, rems, Animation, AnimationExt, EventEmitter, IntoElement, ParentElement, Render, rems, EventEmitter, IntoElement, ParentElement, Render, Styled, Subscription, View,
Styled, Subscription, Transformation, View, ViewContext, WeakView, ViewContext, WeakView,
}; };
use language::Diagnostic; use language::Diagnostic;
use ui::{h_flex, prelude::*, Button, ButtonLike, Color, Icon, IconName, Label, Tooltip}; use ui::{h_flex, prelude::*, Button, ButtonLike, Color, Icon, IconName, Label, Tooltip};
@ -61,42 +59,7 @@ impl Render for DiagnosticIndicator {
.child(Label::new(warning_count.to_string()).size(LabelSize::Small)), .child(Label::new(warning_count.to_string()).size(LabelSize::Small)),
}; };
let has_in_progress_checks = self let status = if let Some(diagnostic) = &self.current_diagnostic {
.workspace
.upgrade()
.and_then(|workspace| {
workspace
.read(cx)
.project()
.read(cx)
.language_servers_running_disk_based_diagnostics()
.next()
})
.is_some();
let status = if has_in_progress_checks {
Some(
h_flex()
.gap_2()
.child(
Icon::new(IconName::ArrowCircle)
.size(IconSize::Small)
.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| {
icon.transform(Transformation::rotate(percentage(delta)))
},
),
)
.child(
Label::new("Checking…")
.size(LabelSize::Small)
.into_any_element(),
)
.into_any_element(),
)
} else if let Some(diagnostic) = &self.current_diagnostic {
let message = diagnostic.message.split('\n').next().unwrap().to_string(); let message = diagnostic.message.split('\n').next().unwrap().to_string();
Some( Some(
Button::new("diagnostic_message", message) Button::new("diagnostic_message", message)

View file

@ -169,6 +169,7 @@ gpui::actions!(
AddSelectionBelow, AddSelectionBelow,
Backspace, Backspace,
Cancel, Cancel,
CancelLanguageServerWork,
ConfirmRename, ConfirmRename,
ContextMenuFirst, ContextMenuFirst,
ContextMenuLast, ContextMenuLast,

View file

@ -9494,6 +9494,20 @@ impl Editor {
} }
} }
fn cancel_language_server_work(
&mut self,
_: &CancelLanguageServerWork,
cx: &mut ViewContext<Self>,
) {
if let Some(project) = self.project.clone() {
self.buffer.update(cx, |multi_buffer, cx| {
project.update(cx, |project, cx| {
project.cancel_language_server_work_for_buffers(multi_buffer.all_buffers(), cx);
});
})
}
}
fn show_character_palette(&mut self, _: &ShowCharacterPalette, cx: &mut ViewContext<Self>) { fn show_character_palette(&mut self, _: &ShowCharacterPalette, cx: &mut ViewContext<Self>) {
cx.show_character_palette(); cx.show_character_palette();
} }

View file

@ -341,6 +341,7 @@ impl EditorElement {
} }
}); });
register_action(view, cx, Editor::restart_language_server); register_action(view, cx, Editor::restart_language_server);
register_action(view, cx, Editor::cancel_language_server_work);
register_action(view, cx, Editor::show_character_palette); register_action(view, cx, Editor::show_character_palette);
register_action(view, cx, |editor, action, cx| { register_action(view, cx, |editor, action, cx| {
if let Some(task) = editor.confirm_completion(action, cx) { if let Some(task) = editor.confirm_completion(action, cx) {

View file

@ -13,7 +13,7 @@ use std::{
Arc, Arc,
}, },
task::{Context, Poll}, task::{Context, Poll},
time::Duration, time::{Duration, Instant},
}; };
use util::TryFutureExt; use util::TryFutureExt;
use waker_fn::waker_fn; use waker_fn::waker_fn;
@ -316,6 +316,14 @@ impl BackgroundExecutor {
} }
} }
/// Get the current time.
///
/// Calling this instead of `std::time::Instant::now` allows the use
/// of fake timers in tests.
pub fn now(&self) -> Instant {
self.dispatcher.now()
}
/// Returns a task that will complete after the given duration. /// Returns a task that will complete after the given duration.
/// Depending on other concurrent tasks the elapsed duration may be longer /// Depending on other concurrent tasks the elapsed duration may be longer
/// than requested. /// than requested.

View file

@ -38,7 +38,7 @@ use seahash::SeaHasher;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::borrow::Cow; use std::borrow::Cow;
use std::hash::{Hash, Hasher}; use std::hash::{Hash, Hasher};
use std::time::Duration; use std::time::{Duration, Instant};
use std::{ use std::{
fmt::{self, Debug}, fmt::{self, Debug},
ops::Range, ops::Range,
@ -275,6 +275,9 @@ pub trait PlatformDispatcher: Send + Sync {
fn dispatch_after(&self, duration: Duration, runnable: Runnable); fn dispatch_after(&self, duration: Duration, runnable: Runnable);
fn park(&self, timeout: Option<Duration>) -> bool; fn park(&self, timeout: Option<Duration>) -> bool;
fn unparker(&self) -> Unparker; fn unparker(&self) -> Unparker;
fn now(&self) -> Instant {
Instant::now()
}
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
fn as_test(&self) -> Option<&TestDispatcher> { fn as_test(&self) -> Option<&TestDispatcher> {

View file

@ -11,7 +11,7 @@ use std::{
pin::Pin, pin::Pin,
sync::Arc, sync::Arc,
task::{Context, Poll}, task::{Context, Poll},
time::Duration, time::{Duration, Instant},
}; };
use util::post_inc; use util::post_inc;
@ -32,6 +32,7 @@ struct TestDispatcherState {
background: Vec<Runnable>, background: Vec<Runnable>,
deprioritized_background: Vec<Runnable>, deprioritized_background: Vec<Runnable>,
delayed: Vec<(Duration, Runnable)>, delayed: Vec<(Duration, Runnable)>,
start_time: Instant,
time: Duration, time: Duration,
is_main_thread: bool, is_main_thread: bool,
next_id: TestDispatcherId, next_id: TestDispatcherId,
@ -52,6 +53,7 @@ impl TestDispatcher {
deprioritized_background: Vec::new(), deprioritized_background: Vec::new(),
delayed: Vec::new(), delayed: Vec::new(),
time: Duration::ZERO, time: Duration::ZERO,
start_time: Instant::now(),
is_main_thread: true, is_main_thread: true,
next_id: TestDispatcherId(1), next_id: TestDispatcherId(1),
allow_parking: false, allow_parking: false,
@ -251,6 +253,11 @@ impl PlatformDispatcher for TestDispatcher {
self.state.lock().is_main_thread self.state.lock().is_main_thread
} }
fn now(&self) -> Instant {
let state = self.state.lock();
state.start_time + state.time
}
fn dispatch(&self, runnable: Runnable, label: Option<TaskLabel>) { fn dispatch(&self, runnable: Runnable, label: Option<TaskLabel>) {
{ {
let mut state = self.state.lock(); let mut state = self.state.lock();

View file

@ -1351,6 +1351,14 @@ impl FakeLanguageServer {
/// Simulate that the server has started work and notifies about its progress with the specified token. /// Simulate that the server has started work and notifies about its progress with the specified token.
pub async fn start_progress(&self, token: impl Into<String>) { pub async fn start_progress(&self, token: impl Into<String>) {
self.start_progress_with(token, Default::default()).await
}
pub async fn start_progress_with(
&self,
token: impl Into<String>,
progress: WorkDoneProgressBegin,
) {
let token = token.into(); let token = token.into();
self.request::<request::WorkDoneProgressCreate>(WorkDoneProgressCreateParams { self.request::<request::WorkDoneProgressCreate>(WorkDoneProgressCreateParams {
token: NumberOrString::String(token.clone()), token: NumberOrString::String(token.clone()),
@ -1359,7 +1367,7 @@ impl FakeLanguageServer {
.unwrap(); .unwrap();
self.notify::<notification::Progress>(ProgressParams { self.notify::<notification::Progress>(ProgressParams {
token: NumberOrString::String(token), token: NumberOrString::String(token),
value: ProgressParamsValue::WorkDone(WorkDoneProgress::Begin(Default::default())), value: ProgressParamsValue::WorkDone(WorkDoneProgress::Begin(progress)),
}); });
} }

View file

@ -19,7 +19,7 @@ use client::{
TypedEnvelope, UserStore, TypedEnvelope, UserStore,
}; };
use clock::ReplicaId; use clock::ReplicaId;
use collections::{hash_map, BTreeMap, HashMap, HashSet, VecDeque}; use collections::{btree_map, hash_map, BTreeMap, HashMap, HashSet, VecDeque};
use debounced_delay::DebouncedDelay; use debounced_delay::DebouncedDelay;
use futures::{ use futures::{
channel::{ channel::{
@ -62,6 +62,7 @@ use lsp::{
DocumentHighlightKind, Edit, FileSystemWatcher, InsertTextFormat, LanguageServer, DocumentHighlightKind, Edit, FileSystemWatcher, InsertTextFormat, LanguageServer,
LanguageServerBinary, LanguageServerId, LspRequestFuture, MessageActionItem, OneOf, LanguageServerBinary, LanguageServerId, LspRequestFuture, MessageActionItem, OneOf,
ServerCapabilities, ServerHealthStatus, ServerStatus, TextEdit, Uri, ServerCapabilities, ServerHealthStatus, ServerStatus, TextEdit, Uri,
WorkDoneProgressCancelParams,
}; };
use lsp_command::*; use lsp_command::*;
use node_runtime::NodeRuntime; use node_runtime::NodeRuntime;
@ -131,7 +132,7 @@ pub use worktree::{
const MAX_SERVER_REINSTALL_ATTEMPT_COUNT: u64 = 4; const MAX_SERVER_REINSTALL_ATTEMPT_COUNT: u64 = 4;
const SERVER_REINSTALL_DEBOUNCE_TIMEOUT: Duration = Duration::from_secs(1); const SERVER_REINSTALL_DEBOUNCE_TIMEOUT: Duration = Duration::from_secs(1);
const SERVER_LAUNCHING_BEFORE_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5); const SERVER_LAUNCHING_BEFORE_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5);
pub const SERVER_PROGRESS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(100); pub const SERVER_PROGRESS_THROTTLE_TIMEOUT: Duration = Duration::from_millis(100);
const MAX_PROJECT_SEARCH_HISTORY_SIZE: usize = 500; const MAX_PROJECT_SEARCH_HISTORY_SIZE: usize = 500;
@ -164,9 +165,6 @@ pub struct Project {
worktrees_reordered: bool, worktrees_reordered: bool,
active_entry: Option<ProjectEntryId>, active_entry: Option<ProjectEntryId>,
buffer_ordered_messages_tx: mpsc::UnboundedSender<BufferOrderedMessage>, buffer_ordered_messages_tx: mpsc::UnboundedSender<BufferOrderedMessage>,
pending_language_server_update: Option<BufferOrderedMessage>,
flush_language_server_update: Option<Task<()>>,
languages: Arc<LanguageRegistry>, languages: Arc<LanguageRegistry>,
supplementary_language_servers: supplementary_language_servers:
HashMap<LanguageServerId, (LanguageServerName, Arc<LanguageServer>)>, HashMap<LanguageServerId, (LanguageServerName, Arc<LanguageServer>)>,
@ -381,6 +379,9 @@ pub struct LanguageServerStatus {
#[derive(Clone, Debug, Serialize)] #[derive(Clone, Debug, Serialize)]
pub struct LanguageServerProgress { pub struct LanguageServerProgress {
pub is_disk_based_diagnostics_progress: bool,
pub is_cancellable: bool,
pub title: Option<String>,
pub message: Option<String>, pub message: Option<String>,
pub percentage: Option<usize>, pub percentage: Option<usize>,
#[serde(skip_serializing)] #[serde(skip_serializing)]
@ -723,8 +724,6 @@ impl Project {
worktrees: Vec::new(), worktrees: Vec::new(),
worktrees_reordered: false, worktrees_reordered: false,
buffer_ordered_messages_tx: tx, buffer_ordered_messages_tx: tx,
flush_language_server_update: None,
pending_language_server_update: None,
collaborators: Default::default(), collaborators: Default::default(),
opened_buffers: Default::default(), opened_buffers: Default::default(),
shared_buffers: Default::default(), shared_buffers: Default::default(),
@ -864,8 +863,6 @@ impl Project {
worktrees: Vec::new(), worktrees: Vec::new(),
worktrees_reordered: false, worktrees_reordered: false,
buffer_ordered_messages_tx: tx, buffer_ordered_messages_tx: tx,
pending_language_server_update: None,
flush_language_server_update: None,
loading_buffers_by_path: Default::default(), loading_buffers_by_path: Default::default(),
loading_buffers: Default::default(), loading_buffers: Default::default(),
shared_buffers: Default::default(), shared_buffers: Default::default(),
@ -4142,6 +4139,40 @@ impl Project {
.detach(); .detach();
} }
pub fn cancel_language_server_work_for_buffers(
&mut self,
buffers: impl IntoIterator<Item = Model<Buffer>>,
cx: &mut ModelContext<Self>,
) {
let servers = buffers
.into_iter()
.flat_map(|buffer| {
self.language_server_ids_for_buffer(buffer.read(cx), cx)
.into_iter()
})
.collect::<HashSet<_>>();
for server_id in servers {
let status = self.language_server_statuses.get(&server_id);
let server = self.language_servers.get(&server_id);
if let Some((server, status)) = server.zip(status) {
if let LanguageServerState::Running { server, .. } = server {
for (token, progress) in &status.pending_work {
if progress.is_cancellable {
server
.notify::<lsp::notification::WorkDoneProgressCancel>(
WorkDoneProgressCancelParams {
token: lsp::NumberOrString::String(token.clone()),
},
)
.ok();
}
}
}
}
}
}
fn check_errored_server( fn check_errored_server(
language: Arc<Language>, language: Arc<Language>,
adapter: Arc<CachedLspAdapter>, adapter: Arc<CachedLspAdapter>,
@ -4211,35 +4242,7 @@ impl Project {
.detach(); .detach();
} }
fn enqueue_language_server_progress(
&mut self,
message: BufferOrderedMessage,
cx: &mut ModelContext<Self>,
) {
self.pending_language_server_update.replace(message);
self.flush_language_server_update.get_or_insert_with(|| {
cx.spawn(|this, mut cx| async move {
cx.background_executor()
.timer(SERVER_PROGRESS_DEBOUNCE_TIMEOUT)
.await;
this.update(&mut cx, |this, _| {
this.flush_language_server_update.take();
if let Some(update) = this.pending_language_server_update.take() {
this.enqueue_buffer_ordered_message(update).ok();
}
})
.ok();
})
});
}
fn enqueue_buffer_ordered_message(&mut self, message: BufferOrderedMessage) -> Result<()> { fn enqueue_buffer_ordered_message(&mut self, message: BufferOrderedMessage) -> Result<()> {
if let Some(pending_message) = self.pending_language_server_update.take() {
self.flush_language_server_update.take();
self.buffer_ordered_messages_tx
.unbounded_send(pending_message)
.map_err(|e| anyhow!(e))?;
}
self.buffer_ordered_messages_tx self.buffer_ordered_messages_tx
.unbounded_send(message) .unbounded_send(message)
.map_err(|e| anyhow!(e)) .map_err(|e| anyhow!(e))
@ -4259,6 +4262,7 @@ impl Project {
return; return;
} }
}; };
let lsp::ProgressParamsValue::WorkDone(progress) = progress.value; let lsp::ProgressParamsValue::WorkDone(progress) = progress.value;
let language_server_status = let language_server_status =
if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) { if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) {
@ -4281,32 +4285,36 @@ impl Project {
lsp::WorkDoneProgress::Begin(report) => { lsp::WorkDoneProgress::Begin(report) => {
if is_disk_based_diagnostics_progress { if is_disk_based_diagnostics_progress {
self.disk_based_diagnostics_started(language_server_id, cx); self.disk_based_diagnostics_started(language_server_id, cx);
} else {
self.on_lsp_work_start(
language_server_id,
token.clone(),
LanguageServerProgress {
message: report.message.clone(),
percentage: report.percentage.map(|p| p as usize),
last_update_at: Instant::now(),
},
cx,
);
} }
self.on_lsp_work_start(
language_server_id,
token.clone(),
LanguageServerProgress {
title: Some(report.title),
is_disk_based_diagnostics_progress,
is_cancellable: report.cancellable.unwrap_or(false),
message: report.message.clone(),
percentage: report.percentage.map(|p| p as usize),
last_update_at: cx.background_executor().now(),
},
cx,
);
} }
lsp::WorkDoneProgress::Report(report) => { lsp::WorkDoneProgress::Report(report) => {
if !is_disk_based_diagnostics_progress { if self.on_lsp_work_progress(
self.on_lsp_work_progress( language_server_id,
language_server_id, token.clone(),
token.clone(), LanguageServerProgress {
LanguageServerProgress { title: None,
message: report.message.clone(), is_disk_based_diagnostics_progress,
percentage: report.percentage.map(|p| p as usize), is_cancellable: report.cancellable.unwrap_or(false),
last_update_at: Instant::now(), message: report.message.clone(),
}, percentage: report.percentage.map(|p| p as usize),
cx, last_update_at: cx.background_executor().now(),
); },
self.enqueue_language_server_progress( cx,
) {
self.enqueue_buffer_ordered_message(
BufferOrderedMessage::LanguageServerUpdate { BufferOrderedMessage::LanguageServerUpdate {
language_server_id, language_server_id,
message: proto::update_language_server::Variant::WorkProgress( message: proto::update_language_server::Variant::WorkProgress(
@ -4317,17 +4325,15 @@ impl Project {
}, },
), ),
}, },
cx, )
); .ok();
} }
} }
lsp::WorkDoneProgress::End(_) => { lsp::WorkDoneProgress::End(_) => {
language_server_status.progress_tokens.remove(&token); language_server_status.progress_tokens.remove(&token);
self.on_lsp_work_end(language_server_id, token.clone(), cx);
if is_disk_based_diagnostics_progress { if is_disk_based_diagnostics_progress {
self.disk_based_diagnostics_finished(language_server_id, cx); self.disk_based_diagnostics_finished(language_server_id, cx);
} else {
self.on_lsp_work_end(language_server_id, token.clone(), cx);
} }
} }
} }
@ -4350,6 +4356,7 @@ impl Project {
language_server_id, language_server_id,
message: proto::update_language_server::Variant::WorkStart(proto::LspWorkStart { message: proto::update_language_server::Variant::WorkStart(proto::LspWorkStart {
token, token,
title: progress.title,
message: progress.message, message: progress.message,
percentage: progress.percentage.map(|p| p as u32), percentage: progress.percentage.map(|p| p as u32),
}), }),
@ -4364,25 +4371,34 @@ impl Project {
token: String, token: String,
progress: LanguageServerProgress, progress: LanguageServerProgress,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) { ) -> bool {
if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) { if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) {
let entry = status match status.pending_work.entry(token) {
.pending_work btree_map::Entry::Vacant(entry) => {
.entry(token) entry.insert(progress);
.or_insert(LanguageServerProgress { cx.notify();
message: Default::default(), return true;
percentage: Default::default(), }
last_update_at: progress.last_update_at, btree_map::Entry::Occupied(mut entry) => {
}); let entry = entry.get_mut();
if progress.message.is_some() { if (progress.last_update_at - entry.last_update_at)
entry.message = progress.message; >= SERVER_PROGRESS_THROTTLE_TIMEOUT
{
entry.last_update_at = progress.last_update_at;
if progress.message.is_some() {
entry.message = progress.message;
}
if progress.percentage.is_some() {
entry.percentage = progress.percentage;
}
cx.notify();
return true;
}
}
} }
if progress.percentage.is_some() {
entry.percentage = progress.percentage;
}
entry.last_update_at = progress.last_update_at;
cx.notify();
} }
false
} }
fn on_lsp_work_end( fn on_lsp_work_end(
@ -4392,8 +4408,11 @@ impl Project {
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) { ) {
if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) { if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) {
cx.emit(Event::RefreshInlayHints); if let Some(work) = status.pending_work.remove(&token) {
status.pending_work.remove(&token); if !work.is_disk_based_diagnostics_progress {
cx.emit(Event::RefreshInlayHints);
}
}
cx.notify(); cx.notify();
} }
@ -7384,9 +7403,12 @@ impl Project {
language_server.server_id(), language_server.server_id(),
id.to_string(), id.to_string(),
LanguageServerProgress { LanguageServerProgress {
is_disk_based_diagnostics_progress: false,
is_cancellable: false,
title: None,
message: status.clone(), message: status.clone(),
percentage: None, percentage: None,
last_update_at: Instant::now(), last_update_at: cx.background_executor().now(),
}, },
cx, cx,
); );
@ -9005,9 +9027,12 @@ impl Project {
language_server_id, language_server_id,
payload.token, payload.token,
LanguageServerProgress { LanguageServerProgress {
title: payload.title,
is_disk_based_diagnostics_progress: false,
is_cancellable: false,
message: payload.message, message: payload.message,
percentage: payload.percentage.map(|p| p as usize), percentage: payload.percentage.map(|p| p as usize),
last_update_at: Instant::now(), last_update_at: cx.background_executor().now(),
}, },
cx, cx,
); );
@ -9018,9 +9043,12 @@ impl Project {
language_server_id, language_server_id,
payload.token, payload.token,
LanguageServerProgress { LanguageServerProgress {
title: None,
is_disk_based_diagnostics_progress: false,
is_cancellable: false,
message: payload.message, message: payload.message,
percentage: payload.percentage.map(|p| p as usize), percentage: payload.percentage.map(|p| p as usize),
last_update_at: Instant::now(), last_update_at: cx.background_executor().now(),
}, },
cx, cx,
); );

View file

@ -7,6 +7,7 @@ use language::{
tree_sitter_rust, tree_sitter_typescript, Diagnostic, FakeLspAdapter, LanguageConfig, tree_sitter_rust, tree_sitter_typescript, Diagnostic, FakeLspAdapter, LanguageConfig,
LanguageMatcher, LineEnding, OffsetRangeExt, Point, ToPoint, LanguageMatcher, LineEnding, OffsetRangeExt, Point, ToPoint,
}; };
use lsp::NumberOrString;
use parking_lot::Mutex; use parking_lot::Mutex;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use serde_json::json; use serde_json::json;
@ -1461,6 +1462,69 @@ async fn test_restarted_server_reporting_invalid_buffer_version(cx: &mut gpui::T
assert_eq!(notification.version, 0); assert_eq!(notification.version, 0);
} }
#[gpui::test]
async fn test_cancel_language_server_work(cx: &mut gpui::TestAppContext) {
init_test(cx);
let progress_token = "the-progress-token";
let fs = FakeFs::new(cx.executor());
fs.insert_tree("/dir", json!({ "a.rs": "" })).await;
let project = Project::test(fs, ["/dir".as_ref()], cx).await;
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
language_registry.add(rust_lang());
let mut fake_servers = language_registry.register_fake_lsp_adapter(
"Rust",
FakeLspAdapter {
name: "the-language-server",
disk_based_diagnostics_sources: vec!["disk".into()],
disk_based_diagnostics_progress_token: Some(progress_token.into()),
..Default::default()
},
);
let buffer = project
.update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
.await
.unwrap();
// Simulate diagnostics starting to update.
let mut fake_server = fake_servers.next().await.unwrap();
fake_server
.start_progress_with(
"another-token",
lsp::WorkDoneProgressBegin {
cancellable: Some(false),
..Default::default()
},
)
.await;
fake_server
.start_progress_with(
progress_token,
lsp::WorkDoneProgressBegin {
cancellable: Some(true),
..Default::default()
},
)
.await;
cx.executor().run_until_parked();
project.update(cx, |project, cx| {
project.cancel_language_server_work_for_buffers([buffer.clone()], cx)
});
let cancel_notification = fake_server
.receive_notification::<lsp::notification::WorkDoneProgressCancel>()
.await;
assert_eq!(
cancel_notification.token,
NumberOrString::String(progress_token.into())
);
}
#[gpui::test] #[gpui::test]
async fn test_toggling_enable_language_server(cx: &mut gpui::TestAppContext) { async fn test_toggling_enable_language_server(cx: &mut gpui::TestAppContext) {
init_test(cx); init_test(cx);
@ -3758,6 +3822,7 @@ async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) {
#[gpui::test] #[gpui::test]
async fn test_rename(cx: &mut gpui::TestAppContext) { async fn test_rename(cx: &mut gpui::TestAppContext) {
// hi
init_test(cx); init_test(cx);
let fs = FakeFs::new(cx.executor()); let fs = FakeFs::new(cx.executor());

View file

@ -1199,6 +1199,7 @@ message UpdateLanguageServer {
message LspWorkStart { message LspWorkStart {
string token = 1; string token = 1;
optional string title = 4;
optional string message = 2; optional string message = 2;
optional uint32 percentage = 3; optional uint32 percentage = 3;
} }

View file

@ -117,6 +117,7 @@ pub enum IconName {
Dash, Dash,
Delete, Delete,
Disconnected, Disconnected,
Download,
Ellipsis, Ellipsis,
Envelope, Envelope,
Escape, Escape,
@ -248,6 +249,7 @@ impl IconName {
IconName::Dash => "icons/dash.svg", IconName::Dash => "icons/dash.svg",
IconName::Delete => "icons/delete.svg", IconName::Delete => "icons/delete.svg",
IconName::Disconnected => "icons/disconnected.svg", IconName::Disconnected => "icons/disconnected.svg",
IconName::Download => "icons/download.svg",
IconName::Ellipsis => "icons/ellipsis.svg", IconName::Ellipsis => "icons/ellipsis.svg",
IconName::Envelope => "icons/feedback.svg", IconName::Envelope => "icons/feedback.svg",
IconName::Escape => "icons/escape.svg", IconName::Escape => "icons/escape.svg",

View file

@ -172,7 +172,7 @@ setTimeout(() => {
env: Object.assign({}, process.env, { env: Object.assign({}, process.env, {
ZED_IMPERSONATE: users[i], ZED_IMPERSONATE: users[i],
ZED_WINDOW_POSITION: position, ZED_WINDOW_POSITION: position,
ZED_STATELESS: isStateful && i == 0 ? "1" : "", ZED_STATELESS: isStateful && i == 0 ? "" : "1",
ZED_ALWAYS_ACTIVE: "1", ZED_ALWAYS_ACTIVE: "1",
ZED_SERVER_URL: "http://localhost:3000", ZED_SERVER_URL: "http://localhost:3000",
ZED_RPC_URL: "http://localhost:8080/rpc", ZED_RPC_URL: "http://localhost:8080/rpc",