Make LSP task cancellation discoverable (#13226)

Release Notes:

- Added the ability to cancel a cargo check by clicking on the status
bar item.
This commit is contained in:
Max Brunsfeld 2024-06-18 12:44:35 -07:00 committed by GitHub
parent 84a44bef8a
commit 89d2ace713
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 144 additions and 30 deletions

View File

@ -3,15 +3,18 @@ use editor::Editor;
use extension::ExtensionStore;
use futures::StreamExt;
use gpui::{
actions, percentage, Animation, AnimationExt as _, AppContext, CursorStyle, EventEmitter,
InteractiveElement as _, Model, ParentElement as _, Render, SharedString,
StatefulInteractiveElement, Styled, Transformation, View, ViewContext, VisualContext as _,
actions, anchored, deferred, percentage, Animation, AnimationExt as _, AppContext, CursorStyle,
DismissEvent, EventEmitter, InteractiveElement as _, Model, ParentElement as _, Render,
SharedString, StatefulInteractiveElement, Styled, Transformation, View, ViewContext,
VisualContext as _,
};
use language::{
LanguageRegistry, LanguageServerBinaryStatus, LanguageServerId, LanguageServerName,
};
use language::{LanguageRegistry, LanguageServerBinaryStatus, LanguageServerName};
use project::{LanguageServerProgress, Project};
use smallvec::SmallVec;
use std::{cmp::Reverse, fmt::Write, sync::Arc, time::Duration};
use ui::prelude::*;
use ui::{prelude::*, ContextMenu};
use workspace::{item::ItemHandle, StatusItemView, Workspace};
actions!(activity_indicator, [ShowErrorMessage]);
@ -24,6 +27,7 @@ pub struct ActivityIndicator {
statuses: Vec<LspStatus>,
project: Model<Project>,
auto_updater: Option<Model<AutoUpdater>>,
context_menu: Option<View<ContextMenu>>,
}
struct LspStatus {
@ -32,6 +36,7 @@ struct LspStatus {
}
struct PendingWork<'a> {
language_server_id: LanguageServerId,
progress_token: &'a str,
progress: &'a LanguageServerProgress,
}
@ -74,6 +79,7 @@ impl ActivityIndicator {
statuses: Default::default(),
project: project.clone(),
auto_updater,
context_menu: None,
}
});
@ -147,7 +153,7 @@ impl ActivityIndicator {
.read(cx)
.language_server_statuses()
.rev()
.filter_map(|status| {
.filter_map(|(server_id, status)| {
if status.pending_work.is_empty() {
None
} else {
@ -155,6 +161,7 @@ impl ActivityIndicator {
.pending_work
.iter()
.map(|(token, progress)| PendingWork {
language_server_id: server_id,
progress_token: token.as_str(),
progress,
})
@ -172,6 +179,7 @@ impl ActivityIndicator {
if let Some(PendingWork {
progress_token,
progress,
..
}) = pending_work.next()
{
let mut message = progress
@ -206,7 +214,7 @@ impl ActivityIndicator {
.into_any_element(),
),
message,
on_click: None,
on_click: Some(Arc::new(Self::toggle_language_server_work_context_menu)),
};
}
@ -357,6 +365,75 @@ impl ActivityIndicator {
Default::default()
}
fn toggle_language_server_work_context_menu(&mut self, cx: &mut ViewContext<Self>) {
if self.context_menu.take().is_some() {
return;
}
self.build_lsp_work_context_menu(cx);
cx.notify();
}
fn build_lsp_work_context_menu(&mut self, cx: &mut ViewContext<Self>) {
let mut has_work = false;
let this = cx.view().downgrade();
let context_menu = ContextMenu::build(cx, |mut menu, cx| {
for work in self.pending_language_server_work(cx) {
has_work = true;
let this = this.clone();
let title = SharedString::from(
work.progress
.title
.as_deref()
.unwrap_or(work.progress_token)
.to_string(),
);
if work.progress.is_cancellable {
let language_server_id = work.language_server_id;
let token = work.progress_token.to_string();
menu = menu.custom_entry(
move |_| {
h_flex()
.w_full()
.justify_between()
.child(Label::new(title.clone()))
.child(Icon::new(IconName::XCircle))
.into_any_element()
},
move |cx| {
this.update(cx, |this, cx| {
this.project.update(cx, |project, cx| {
project.cancel_language_server_work(
language_server_id,
Some(token.clone()),
cx,
);
});
this.context_menu.take();
})
.ok();
},
);
} else {
menu = menu.label(title.clone());
}
}
menu
});
if has_work {
cx.subscribe(&context_menu, |this, _, _: &DismissEvent, cx| {
this.context_menu.take();
cx.notify();
})
.detach();
cx.focus_view(&context_menu);
self.context_menu = Some(context_menu);
cx.notify();
}
}
}
impl EventEmitter<Event> for ActivityIndicator {}
@ -382,6 +459,14 @@ impl Render for ActivityIndicator {
.gap_2()
.children(content.icon)
.child(Label::new(SharedString::from(content.message)).size(LabelSize::Small))
.children(self.context_menu.as_ref().map(|menu| {
deferred(
anchored()
.anchor(gpui::AnchorCorner::BottomLeft)
.child(menu.clone()),
)
.with_priority(1)
}))
}
}

View File

@ -1020,7 +1020,7 @@ async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut Tes
executor.run_until_parked();
project_a.read_with(cx_a, |project, _| {
let status = project.language_server_statuses().next().unwrap();
let status = project.language_server_statuses().next().unwrap().1;
assert_eq!(status.name, "the-language-server");
assert_eq!(status.pending_work.len(), 1);
assert_eq!(
@ -1037,7 +1037,7 @@ async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut Tes
let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
project_b.read_with(cx_b, |project, _| {
let status = project.language_server_statuses().next().unwrap();
let status = project.language_server_statuses().next().unwrap().1;
assert_eq!(status.name, "the-language-server");
});
@ -1054,7 +1054,7 @@ async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut Tes
executor.run_until_parked();
project_a.read_with(cx_a, |project, _| {
let status = project.language_server_statuses().next().unwrap();
let status = project.language_server_statuses().next().unwrap().1;
assert_eq!(status.name, "the-language-server");
assert_eq!(status.pending_work.len(), 1);
assert_eq!(
@ -1064,7 +1064,7 @@ async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut Tes
});
project_b.read_with(cx_b, |project, _| {
let status = project.language_server_statuses().next().unwrap();
let status = project.language_server_statuses().next().unwrap().1;
assert_eq!(status.name, "the-language-server");
assert_eq!(status.pending_work.len(), 1);
assert_eq!(

View File

@ -4772,7 +4772,7 @@ async fn test_references(
// User is informed that a request is pending.
executor.run_until_parked();
project_b.read_with(cx_b, |project, _| {
let status = project.language_server_statuses().next().cloned().unwrap();
let status = project.language_server_statuses().next().unwrap().1;
assert_eq!(status.name, "my-fake-lsp-adapter");
assert_eq!(
status.pending_work.values().next().unwrap().message,
@ -4802,7 +4802,7 @@ async fn test_references(
executor.run_until_parked();
project_b.read_with(cx_b, |project, cx| {
// User is informed that a request is no longer pending.
let status = project.language_server_statuses().next().unwrap();
let status = project.language_server_statuses().next().unwrap().1;
assert!(status.pending_work.is_empty());
assert_eq!(references.len(), 3);
@ -4830,7 +4830,7 @@ async fn test_references(
// User is informed that a request is pending.
executor.run_until_parked();
project_b.read_with(cx_b, |project, _| {
let status = project.language_server_statuses().next().cloned().unwrap();
let status = project.language_server_statuses().next().unwrap().1;
assert_eq!(status.name, "my-fake-lsp-adapter");
assert_eq!(
status.pending_work.values().next().unwrap().message,
@ -4847,7 +4847,7 @@ async fn test_references(
// User is informed that the request is no longer pending.
executor.run_until_parked();
project_b.read_with(cx_b, |project, _| {
let status = project.language_server_statuses().next().unwrap();
let status = project.language_server_statuses().next().unwrap().1;
assert!(status.pending_work.is_empty());
});
}

View File

@ -4149,21 +4149,35 @@ impl Project {
.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();
self.cancel_language_server_work(server_id, None, cx);
}
}
pub fn cancel_language_server_work(
&mut self,
server_id: LanguageServerId,
token_to_cancel: Option<String>,
_cx: &mut ModelContext<Self>,
) {
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 let Some(token_to_cancel) = token_to_cancel.as_ref() {
if token != token_to_cancel {
continue;
}
}
if progress.is_cancellable {
server
.notify::<lsp::notification::WorkDoneProgressCancel>(
WorkDoneProgressCancelParams {
token: lsp::NumberOrString::String(token.clone()),
},
)
.ok();
}
}
}
}
@ -4580,8 +4594,10 @@ impl Project {
pub fn language_server_statuses(
&self,
) -> impl DoubleEndedIterator<Item = &LanguageServerStatus> {
self.language_server_statuses.values()
) -> impl DoubleEndedIterator<Item = (LanguageServerId, &LanguageServerStatus)> {
self.language_server_statuses
.iter()
.map(|(key, value)| (*key, value))
}
pub fn last_formatting_failure(&self) -> Option<&str> {

View File

@ -14,6 +14,7 @@ use theme::ThemeSettings;
enum ContextMenuItem {
Separator,
Header(SharedString),
Label(SharedString),
Entry {
toggled: Option<bool>,
label: SharedString,
@ -147,6 +148,12 @@ impl ContextMenu {
self
}
pub fn label(mut self, label: impl Into<SharedString>) -> Self {
let label = label.into();
self.items.push(ContextMenuItem::Label(label));
self
}
pub fn action(mut self, label: impl Into<SharedString>, action: Box<dyn Action>) -> Self {
self.items.push(ContextMenuItem::Entry {
toggled: None,
@ -284,6 +291,7 @@ impl ContextMenuItem {
fn is_selectable(&self) -> bool {
match self {
ContextMenuItem::Separator => false,
ContextMenuItem::Label { .. } => false,
ContextMenuItem::Header(_) => false,
ContextMenuItem::Entry { .. } => true,
ContextMenuItem::CustomEntry { selectable, .. } => *selectable,
@ -333,6 +341,11 @@ impl Render for ContextMenu {
.inset(true)
.into_any_element()
}
ContextMenuItem::Label(label) => ListItem::new(ix)
.inset(true)
.disabled(true)
.child(Label::new(label.clone()))
.into_any_element(),
ContextMenuItem::Entry {
toggled,
label,