Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
246 changes: 102 additions & 144 deletions crates/activity_indicator/src/activity_indicator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ use std::{
sync::Arc,
time::{Duration, Instant},
};
use ui::{CommonAnimationExt, ContextMenu, PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*};
use ui::{ContextMenu, PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*};
use util::truncate_and_trailoff;
use workspace::{StatusItemView, Workspace, item::ItemHandle};

Expand Down Expand Up @@ -62,8 +62,13 @@ struct PendingWork<'a> {
progress: &'a LanguageServerProgress,
}

enum ActivityIcon {
LoadingSpinner,
Icon(IconName),
}

struct Content {
icon: Option<gpui::AnyElement>,
icon: ActivityIcon,
message: String,
on_click:
Option<Arc<dyn Fn(&mut ActivityIndicator, &mut Window, &mut Context<ActivityIndicator>)>>,
Expand Down Expand Up @@ -310,24 +315,19 @@ impl ActivityIndicator {
.read(cx)
.language_server_statuses(cx)
.rev()
.filter_map(|(server_id, status)| {
if status.pending_work.is_empty() {
None
} else {
let mut pending_work = status
.pending_work
.iter()
.map(|(progress_token, progress)| PendingWork {
language_server_id: server_id,
progress_token,
progress,
})
.collect::<SmallVec<[_; 4]>>();
pending_work.sort_by_key(|work| Reverse(work.progress.last_update_at));
Some(pending_work)
}
.flat_map(|(server_id, status)| {
let mut pending_work = status
.pending_work
.iter()
.map(|(progress_token, progress)| PendingWork {
language_server_id: server_id,
progress_token,
progress,
})
.collect::<SmallVec<[_; 4]>>();
pending_work.sort_by_key(|work| Reverse(work.progress.last_update_at));
pending_work
})
.flatten()
}

fn pending_environment_error<'a>(&'a self, cx: &'a App) -> Option<&'a String> {
Expand All @@ -338,11 +338,7 @@ impl ActivityIndicator {
// Show if any direnv calls failed
if let Some(message) = self.pending_environment_error(cx) {
return Some(Content {
icon: Some(
Icon::new(IconName::Warning)
.size(IconSize::Small)
.into_any_element(),
),
icon: ActivityIcon::Icon(IconName::Warning),
message: message.clone(),
on_click: Some(Arc::new(move |this, window, cx| {
this.project.update(cx, |project, cx| {
Expand Down Expand Up @@ -379,12 +375,7 @@ impl ActivityIndicator {
}

return Some(Content {
icon: Some(
Icon::new(IconName::ArrowCircle)
.size(IconSize::Small)
.with_rotate_animation(2)
.into_any_element(),
),
icon: ActivityIcon::LoadingSpinner,
message,
on_click: Some(Arc::new(Self::toggle_language_server_work_context_menu)),
tooltip_message: None,
Expand All @@ -401,12 +392,7 @@ impl ActivityIndicator {
.find(|s| !s.read(cx).is_started())
{
return Some(Content {
icon: Some(
Icon::new(IconName::ArrowCircle)
.size(IconSize::Small)
.with_rotate_animation(2)
.into_any_element(),
),
icon: ActivityIcon::LoadingSpinner,
message: format!("Debug: {}", session.read(cx).adapter()),
tooltip_message: session.read(cx).label().map(|label| label.to_string()),
on_click: None,
Expand All @@ -424,12 +410,7 @@ impl ActivityIndicator {
&& Instant::now() - job_info.start >= GIT_OPERATION_DELAY
{
return Some(Content {
icon: Some(
Icon::new(IconName::ArrowCircle)
.size(IconSize::Small)
.with_rotate_animation(2)
.into_any_element(),
),
icon: ActivityIcon::LoadingSpinner,
message: job_info.message.into(),
on_click: None,
tooltip_message: None,
Expand All @@ -440,12 +421,7 @@ impl ActivityIndicator {
for fs_job in &self.fs_jobs {
if Instant::now().duration_since(fs_job.start) >= GIT_OPERATION_DELAY {
return Some(Content {
icon: Some(
Icon::new(IconName::ArrowCircle)
.size(IconSize::Small)
.with_rotate_animation(2)
.into_any_element(),
),
icon: ActivityIcon::LoadingSpinner,
message: fs_job.message.clone().into(),
on_click: None,
tooltip_message: None,
Expand Down Expand Up @@ -498,11 +474,7 @@ impl ActivityIndicator {

if !downloading.is_empty() {
return Some(Content {
icon: Some(
Icon::new(IconName::Download)
.size(IconSize::Small)
.into_any_element(),
),
icon: ActivityIcon::Icon(IconName::Download),
message: format!(
"Downloading {}...",
downloading.iter().map(|name| name.as_ref()).fold(
Expand All @@ -527,11 +499,7 @@ impl ActivityIndicator {

if !checking_for_update.is_empty() {
return Some(Content {
icon: Some(
Icon::new(IconName::Download)
.size(IconSize::Small)
.into_any_element(),
),
icon: ActivityIcon::Icon(IconName::Download),
message: format!(
"Checking for updates to {}...",
checking_for_update.iter().map(|name| name.as_ref()).fold(
Expand All @@ -556,11 +524,7 @@ impl ActivityIndicator {

if !failed.is_empty() {
return Some(Content {
icon: Some(
Icon::new(IconName::Warning)
.size(IconSize::Small)
.into_any_element(),
),
icon: ActivityIcon::Icon(IconName::Warning),
message: format!(
"Failed to run {}. Click to show error.",
failed
Expand All @@ -584,11 +548,7 @@ impl ActivityIndicator {
// Show any formatting failure
if let Some(failure) = self.project.read(cx).last_formatting_failure(cx) {
return Some(Content {
icon: Some(
Icon::new(IconName::Warning)
.size(IconSize::Small)
.into_any_element(),
),
icon: ActivityIcon::Icon(IconName::Warning),
message: format!("Formatting failed: {failure}. Click to see logs."),
on_click: Some(Arc::new(|indicator, window, cx| {
indicator.project.update(cx, |project, cx| {
Expand Down Expand Up @@ -630,11 +590,7 @@ impl ActivityIndicator {
};

return Some(Content {
icon: Some(
Icon::new(IconName::Warning)
.size(IconSize::Small)
.into_any_element(),
),
icon: ActivityIcon::Icon(IconName::Warning),
message: final_message,
tooltip_message,
on_click: Some(Arc::new(move |activity_indicator, window, cx| {
Expand All @@ -656,32 +612,23 @@ impl ActivityIndicator {
&& let Some((extension_id, operation)) =
extension_store.outstanding_operations().iter().next()
{
let (message, icon, rotate) = match operation {
let (message, icon) = match operation {
ExtensionOperation::Install => (
format!("Installing {extension_id} extension…"),
IconName::LoadCircle,
true,
ActivityIcon::LoadingSpinner,
),
ExtensionOperation::Upgrade => (
format!("Updating {extension_id} extension…"),
IconName::Download,
false,
ActivityIcon::Icon(IconName::Download),
),
ExtensionOperation::Remove => (
format!("Removing {extension_id} extension…"),
IconName::LoadCircle,
true,
ActivityIcon::LoadingSpinner,
),
};

return Some(Content {
icon: Some(Icon::new(icon).size(IconSize::Small).map(|this| {
if rotate {
this.with_rotate_animation(3).into_any_element()
} else {
this.into_any_element()
}
})),
icon,
message,
on_click: Some(Arc::new(|this, window, cx| {
this.dismiss_message(&Default::default(), window, cx)
Expand Down Expand Up @@ -718,6 +665,8 @@ impl Render for ActivityIndicator {
let activity_indicator = cx.entity().downgrade();
let truncate_content = content.message.len() > MAX_MESSAGE_LEN;

let has_click_handler = content.on_click.is_some();

result.gap_2().child(
PopoverMenu::new("activity-indicator-popover")
.trigger(
Expand All @@ -729,7 +678,14 @@ impl Render for ActivityIndicator {
}
})
.label_size(LabelSize::Small)
.loading(content.icon.is_some())
.map(|this| match content.icon {
ActivityIcon::LoadingSpinner => this.loading(true),
ActivityIcon::Icon(icon_name) => this.start_icon(
Icon::new(icon_name)
.size(IconSize::Small)
.color(Color::Muted),
),
})
.map(|button| {
if truncate_content {
button.tooltip(Tooltip::text(content.message))
Expand All @@ -746,64 +702,66 @@ impl Render for ActivityIndicator {
}),
)
.anchor(gpui::Anchor::BottomLeft)
.menu(move |window, cx| {
let strong_this = activity_indicator.upgrade()?;
let mut has_work = false;
let menu = ContextMenu::build(window, cx, |mut menu, _, cx| {
for work in strong_this.read(cx).pending_language_server_work(cx) {
has_work = true;
let activity_indicator = activity_indicator.clone();
let mut title = work
.progress
.title
.clone()
.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.clone();
let title = SharedString::from(title);
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| {
let token = token.clone();
activity_indicator
.update(cx, |activity_indicator, cx| {
activity_indicator.project.update(
cx,
|project, cx| {
project.cancel_language_server_work(
language_server_id,
Some(token),
cx,
);
},
);
activity_indicator.context_menu_handle.hide(cx);
cx.notify();
})
.ok();
},
);
} else {
if let Some(progress_message) = work.progress.message.as_ref() {
title.push_str(": ");
title.push_str(progress_message);
}
.when(!has_click_handler, |this| {
this.menu(move |window, cx| {
let strong_this = activity_indicator.upgrade()?;
let mut has_work = false;
let menu = ContextMenu::build(window, cx, |mut menu, _, cx| {
for work in strong_this.read(cx).pending_language_server_work(cx) {
has_work = true;
let activity_indicator = activity_indicator.clone();
let mut title = work
.progress
.title
.clone()
.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.clone();
let title = SharedString::from(title);
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| {
let token = token.clone();
activity_indicator
.update(cx, |activity_indicator, cx| {
activity_indicator.project.update(
cx,
|project, cx| {
project.cancel_language_server_work(
language_server_id,
Some(token),
cx,
);
},
);
activity_indicator.context_menu_handle.hide(cx);
cx.notify();
})
.ok();
},
);
} else {
if let Some(progress_message) = work.progress.message.as_ref() {
title.push_str(": ");
title.push_str(progress_message);
}

menu = menu.label(title);
menu = menu.label(title);
}
}
}
menu
});
has_work.then_some(menu)
menu
});
has_work.then_some(menu)
})
}),
)
}
Expand Down
Loading
Loading