mirror of
https://github.com/zed-industries/zed.git
synced 2024-09-16 17:07:14 +03:00
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:
parent
84a44bef8a
commit
89d2ace713
@ -3,15 +3,18 @@ use editor::Editor;
|
|||||||
use extension::ExtensionStore;
|
use extension::ExtensionStore;
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, percentage, Animation, AnimationExt as _, AppContext, CursorStyle, EventEmitter,
|
actions, anchored, deferred, percentage, Animation, AnimationExt as _, AppContext, CursorStyle,
|
||||||
InteractiveElement as _, Model, ParentElement as _, Render, SharedString,
|
DismissEvent, EventEmitter, InteractiveElement as _, Model, ParentElement as _, Render,
|
||||||
StatefulInteractiveElement, Styled, Transformation, View, ViewContext, VisualContext as _,
|
SharedString, StatefulInteractiveElement, Styled, Transformation, View, ViewContext,
|
||||||
|
VisualContext as _,
|
||||||
|
};
|
||||||
|
use language::{
|
||||||
|
LanguageRegistry, LanguageServerBinaryStatus, LanguageServerId, 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, time::Duration};
|
use std::{cmp::Reverse, fmt::Write, sync::Arc, time::Duration};
|
||||||
use ui::prelude::*;
|
use ui::{prelude::*, ContextMenu};
|
||||||
use workspace::{item::ItemHandle, StatusItemView, Workspace};
|
use workspace::{item::ItemHandle, StatusItemView, Workspace};
|
||||||
|
|
||||||
actions!(activity_indicator, [ShowErrorMessage]);
|
actions!(activity_indicator, [ShowErrorMessage]);
|
||||||
@ -24,6 +27,7 @@ pub struct ActivityIndicator {
|
|||||||
statuses: Vec<LspStatus>,
|
statuses: Vec<LspStatus>,
|
||||||
project: Model<Project>,
|
project: Model<Project>,
|
||||||
auto_updater: Option<Model<AutoUpdater>>,
|
auto_updater: Option<Model<AutoUpdater>>,
|
||||||
|
context_menu: Option<View<ContextMenu>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct LspStatus {
|
struct LspStatus {
|
||||||
@ -32,6 +36,7 @@ struct LspStatus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct PendingWork<'a> {
|
struct PendingWork<'a> {
|
||||||
|
language_server_id: LanguageServerId,
|
||||||
progress_token: &'a str,
|
progress_token: &'a str,
|
||||||
progress: &'a LanguageServerProgress,
|
progress: &'a LanguageServerProgress,
|
||||||
}
|
}
|
||||||
@ -74,6 +79,7 @@ impl ActivityIndicator {
|
|||||||
statuses: Default::default(),
|
statuses: Default::default(),
|
||||||
project: project.clone(),
|
project: project.clone(),
|
||||||
auto_updater,
|
auto_updater,
|
||||||
|
context_menu: None,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -147,7 +153,7 @@ impl ActivityIndicator {
|
|||||||
.read(cx)
|
.read(cx)
|
||||||
.language_server_statuses()
|
.language_server_statuses()
|
||||||
.rev()
|
.rev()
|
||||||
.filter_map(|status| {
|
.filter_map(|(server_id, status)| {
|
||||||
if status.pending_work.is_empty() {
|
if status.pending_work.is_empty() {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
@ -155,6 +161,7 @@ impl ActivityIndicator {
|
|||||||
.pending_work
|
.pending_work
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(token, progress)| PendingWork {
|
.map(|(token, progress)| PendingWork {
|
||||||
|
language_server_id: server_id,
|
||||||
progress_token: token.as_str(),
|
progress_token: token.as_str(),
|
||||||
progress,
|
progress,
|
||||||
})
|
})
|
||||||
@ -172,6 +179,7 @@ impl ActivityIndicator {
|
|||||||
if let Some(PendingWork {
|
if let Some(PendingWork {
|
||||||
progress_token,
|
progress_token,
|
||||||
progress,
|
progress,
|
||||||
|
..
|
||||||
}) = pending_work.next()
|
}) = pending_work.next()
|
||||||
{
|
{
|
||||||
let mut message = progress
|
let mut message = progress
|
||||||
@ -206,7 +214,7 @@ impl ActivityIndicator {
|
|||||||
.into_any_element(),
|
.into_any_element(),
|
||||||
),
|
),
|
||||||
message,
|
message,
|
||||||
on_click: None,
|
on_click: Some(Arc::new(Self::toggle_language_server_work_context_menu)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -357,6 +365,75 @@ impl ActivityIndicator {
|
|||||||
|
|
||||||
Default::default()
|
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 {}
|
impl EventEmitter<Event> for ActivityIndicator {}
|
||||||
@ -382,6 +459,14 @@ impl Render for ActivityIndicator {
|
|||||||
.gap_2()
|
.gap_2()
|
||||||
.children(content.icon)
|
.children(content.icon)
|
||||||
.child(Label::new(SharedString::from(content.message)).size(LabelSize::Small))
|
.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)
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1020,7 +1020,7 @@ async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut Tes
|
|||||||
executor.run_until_parked();
|
executor.run_until_parked();
|
||||||
|
|
||||||
project_a.read_with(cx_a, |project, _| {
|
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.name, "the-language-server");
|
||||||
assert_eq!(status.pending_work.len(), 1);
|
assert_eq!(status.pending_work.len(), 1);
|
||||||
assert_eq!(
|
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;
|
let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
|
||||||
|
|
||||||
project_b.read_with(cx_b, |project, _| {
|
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.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();
|
executor.run_until_parked();
|
||||||
|
|
||||||
project_a.read_with(cx_a, |project, _| {
|
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.name, "the-language-server");
|
||||||
assert_eq!(status.pending_work.len(), 1);
|
assert_eq!(status.pending_work.len(), 1);
|
||||||
assert_eq!(
|
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, _| {
|
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.name, "the-language-server");
|
||||||
assert_eq!(status.pending_work.len(), 1);
|
assert_eq!(status.pending_work.len(), 1);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
@ -4772,7 +4772,7 @@ async fn test_references(
|
|||||||
// User is informed that a request is pending.
|
// User is informed that a request is pending.
|
||||||
executor.run_until_parked();
|
executor.run_until_parked();
|
||||||
project_b.read_with(cx_b, |project, _| {
|
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.name, "my-fake-lsp-adapter");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
status.pending_work.values().next().unwrap().message,
|
status.pending_work.values().next().unwrap().message,
|
||||||
@ -4802,7 +4802,7 @@ async fn test_references(
|
|||||||
executor.run_until_parked();
|
executor.run_until_parked();
|
||||||
project_b.read_with(cx_b, |project, cx| {
|
project_b.read_with(cx_b, |project, cx| {
|
||||||
// User is informed that a request is no longer pending.
|
// 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!(status.pending_work.is_empty());
|
||||||
|
|
||||||
assert_eq!(references.len(), 3);
|
assert_eq!(references.len(), 3);
|
||||||
@ -4830,7 +4830,7 @@ async fn test_references(
|
|||||||
// User is informed that a request is pending.
|
// User is informed that a request is pending.
|
||||||
executor.run_until_parked();
|
executor.run_until_parked();
|
||||||
project_b.read_with(cx_b, |project, _| {
|
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.name, "my-fake-lsp-adapter");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
status.pending_work.values().next().unwrap().message,
|
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.
|
// User is informed that the request is no longer pending.
|
||||||
executor.run_until_parked();
|
executor.run_until_parked();
|
||||||
project_b.read_with(cx_b, |project, _| {
|
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());
|
assert!(status.pending_work.is_empty());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -4149,11 +4149,26 @@ impl Project {
|
|||||||
.collect::<HashSet<_>>();
|
.collect::<HashSet<_>>();
|
||||||
|
|
||||||
for server_id in servers {
|
for server_id in servers {
|
||||||
|
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 status = self.language_server_statuses.get(&server_id);
|
||||||
let server = self.language_servers.get(&server_id);
|
let server = self.language_servers.get(&server_id);
|
||||||
if let Some((server, status)) = server.zip(status) {
|
if let Some((server, status)) = server.zip(status) {
|
||||||
if let LanguageServerState::Running { server, .. } = server {
|
if let LanguageServerState::Running { server, .. } = server {
|
||||||
for (token, progress) in &status.pending_work {
|
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 {
|
if progress.is_cancellable {
|
||||||
server
|
server
|
||||||
.notify::<lsp::notification::WorkDoneProgressCancel>(
|
.notify::<lsp::notification::WorkDoneProgressCancel>(
|
||||||
@ -4167,7 +4182,6 @@ impl Project {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fn check_errored_server(
|
fn check_errored_server(
|
||||||
language: Arc<Language>,
|
language: Arc<Language>,
|
||||||
@ -4580,8 +4594,10 @@ impl Project {
|
|||||||
|
|
||||||
pub fn language_server_statuses(
|
pub fn language_server_statuses(
|
||||||
&self,
|
&self,
|
||||||
) -> impl DoubleEndedIterator<Item = &LanguageServerStatus> {
|
) -> impl DoubleEndedIterator<Item = (LanguageServerId, &LanguageServerStatus)> {
|
||||||
self.language_server_statuses.values()
|
self.language_server_statuses
|
||||||
|
.iter()
|
||||||
|
.map(|(key, value)| (*key, value))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn last_formatting_failure(&self) -> Option<&str> {
|
pub fn last_formatting_failure(&self) -> Option<&str> {
|
||||||
|
@ -14,6 +14,7 @@ use theme::ThemeSettings;
|
|||||||
enum ContextMenuItem {
|
enum ContextMenuItem {
|
||||||
Separator,
|
Separator,
|
||||||
Header(SharedString),
|
Header(SharedString),
|
||||||
|
Label(SharedString),
|
||||||
Entry {
|
Entry {
|
||||||
toggled: Option<bool>,
|
toggled: Option<bool>,
|
||||||
label: SharedString,
|
label: SharedString,
|
||||||
@ -147,6 +148,12 @@ impl ContextMenu {
|
|||||||
self
|
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 {
|
pub fn action(mut self, label: impl Into<SharedString>, action: Box<dyn Action>) -> Self {
|
||||||
self.items.push(ContextMenuItem::Entry {
|
self.items.push(ContextMenuItem::Entry {
|
||||||
toggled: None,
|
toggled: None,
|
||||||
@ -284,6 +291,7 @@ impl ContextMenuItem {
|
|||||||
fn is_selectable(&self) -> bool {
|
fn is_selectable(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
ContextMenuItem::Separator => false,
|
ContextMenuItem::Separator => false,
|
||||||
|
ContextMenuItem::Label { .. } => false,
|
||||||
ContextMenuItem::Header(_) => false,
|
ContextMenuItem::Header(_) => false,
|
||||||
ContextMenuItem::Entry { .. } => true,
|
ContextMenuItem::Entry { .. } => true,
|
||||||
ContextMenuItem::CustomEntry { selectable, .. } => *selectable,
|
ContextMenuItem::CustomEntry { selectable, .. } => *selectable,
|
||||||
@ -333,6 +341,11 @@ impl Render for ContextMenu {
|
|||||||
.inset(true)
|
.inset(true)
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
ContextMenuItem::Label(label) => ListItem::new(ix)
|
||||||
|
.inset(true)
|
||||||
|
.disabled(true)
|
||||||
|
.child(Label::new(label.clone()))
|
||||||
|
.into_any_element(),
|
||||||
ContextMenuItem::Entry {
|
ContextMenuItem::Entry {
|
||||||
toggled,
|
toggled,
|
||||||
label,
|
label,
|
||||||
|
Loading…
Reference in New Issue
Block a user