Files
zed/crates/activity_indicator/src/activity_indicator.rs
T
Kirill Bulatov 851bf3d499 Make file scanner less eager in non-git-tracked directory trees (#62583)
Fixes https://github.com/zed-industries/zed/issues/35780
Collab schema migration PR:
https://github.com/zed-industries/cloud/pull/3422
The corresponding database schema migration has been created in the
Cloud repo and applied to the production database.

Before, Zed scanned each and every entry in the tree down from the
directory it was opened in, except gitignored files and scan exclusions.
The approach is unchanged, if Zed detects it was open inside a git
repository: e.g. the directory open in Zed contains `.git` directory.

For the rest of the projects, 2 optimizations are made:

* Limit the depth of file scan traversal.
Now, `file_scan_depth` (default `5`) restricts Zed from traversing any
directory that has same number or more segments in its file path.


Such directories behave similar to gitignored directories: their
contents is not available in file finder, project search and project
panel, but can be lazily traversed when the directory is expanded (e.g.
project panel expands it or a nested file is open by path via terminal,
etc.)

To indicate that to the users, a status entry is shown firs time the
limitation is hit in the project:

<img width="858" height="133" alt="image"
src="https://github.com/user-attachments/assets/7da6cfbb-98b4-4cc3-bf2a-8902a9597a15"
/>

* During the scan, any git repositories that are not direct children of
the directory open in Zed (depth >= 2), are traversed and indexed
normally, but their git metadata is never fetched eagerly.

Only when Zed opens a buffer from that repo the git metadata is fetched
and applied.

All that combined now uses a way more moderate amount of CPU and RAM
when opening `~`:

<img width="1717" height="368" alt="Screenshot 2026-08-13 at 17 43 53"
src="https://github.com/user-attachments/assets/ec83e2a9-f7cc-452b-8eb7-af158284ca4e"
/>

File scan inclusions and exclusions are considered still for such
projects.
Set `file_scan_depth` to `0` to enable old behavior.
The setting is supported in the project settings, so custom values can
be set based on the project's structure.

---

Release Notes:

- Fixed Zed using a lot of memory and CPU in large, non-git-tracked,
directory trees
2026-08-13 17:07:06 +00:00

881 lines
35 KiB
Rust

use auto_update::DismissMessage;
use editor::Editor;
use extension_host::{ExtensionOperation, ExtensionStore};
use futures::StreamExt;
use gpui::{
App, Context, Entity, EventEmitter, InteractiveElement as _, ParentElement as _, Render,
SharedString, Styled, Task, Window, actions,
};
use language::{
BinaryStatus, LanguageRegistry, LanguageServerId, LanguageServerName,
LanguageServerStatusUpdate, ServerHealth,
};
use project::{
LanguageServerProgress, LspStoreEvent, ProgressToken, Project, ProjectEnvironmentEvent,
git_store::{GitStoreEvent, Repository},
};
use smallvec::SmallVec;
use std::{
cmp::Reverse,
collections::HashSet,
fmt::Write,
sync::Arc,
time::{Duration, Instant},
};
use ui::{ContextMenu, PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*};
use util::truncate_and_trailoff;
use workspace::{StatusItemView, Workspace, item::ItemHandle};
const GIT_OPERATION_DELAY: Duration = Duration::from_millis(0);
pub const DEFERRED_SCAN_MESSAGE_TIMEOUT: Duration = Duration::from_secs(10);
actions!(
activity_indicator,
[
/// Displays error messages from language servers in the status bar.
ShowErrorMessage
]
);
pub enum Event {
ShowStatus {
server_name: LanguageServerName,
status: SharedString,
},
}
pub struct ActivityIndicator {
statuses: Vec<ServerStatus>,
project: Entity<Project>,
context_menu_handle: PopoverMenuHandle<ContextMenu>,
fs_jobs: Vec<fs::JobInfo>,
deferred_scan_message: DeferredScanMessage,
}
#[derive(Default)]
enum DeferredScanMessage {
#[default]
Undetected,
Pending,
Shown {
_dismiss_timer: Task<()>,
},
Dismissed,
}
#[derive(Debug)]
struct ServerStatus {
name: LanguageServerName,
status: LanguageServerStatusUpdate,
}
struct PendingWork<'a> {
language_server_id: LanguageServerId,
progress_token: &'a ProgressToken,
progress: &'a LanguageServerProgress,
}
enum ActivityIcon {
LoadingSpinner,
Icon(IconName),
}
struct Content {
icon: ActivityIcon,
message: String,
on_click:
Option<Arc<dyn Fn(&mut ActivityIndicator, &mut Window, &mut Context<ActivityIndicator>)>>,
tooltip_message: Option<String>,
}
impl ActivityIndicator {
pub fn new(
workspace: &mut Workspace,
languages: Arc<LanguageRegistry>,
window: &mut Window,
cx: &mut Context<Workspace>,
) -> Entity<ActivityIndicator> {
let project = workspace.project().clone();
let this = cx.new(|cx| {
let mut status_events = languages.language_server_binary_statuses();
cx.spawn(async move |this, cx| {
while let Some((name, binary_status)) = status_events.next().await {
this.update(cx, |this: &mut ActivityIndicator, cx| {
this.statuses.retain(|s| s.name != name);
this.statuses.push(ServerStatus {
name,
status: LanguageServerStatusUpdate::Binary(binary_status),
});
cx.notify();
})?;
}
anyhow::Ok(())
})
.detach();
let fs = project.read(cx).fs().clone();
let mut job_events = fs.subscribe_to_jobs();
cx.spawn(async move |this, cx| {
while let Some(job_event) = job_events.next().await {
this.update(cx, |this: &mut ActivityIndicator, cx| {
match job_event {
fs::JobEvent::Started { info } => {
this.fs_jobs.retain(|j| j.id != info.id);
this.fs_jobs.push(info);
}
fs::JobEvent::Completed { id } => {
this.fs_jobs.retain(|j| j.id != id);
}
}
cx.notify();
})?;
}
anyhow::Ok(())
})
.detach();
cx.subscribe(
&project.read(cx).lsp_store(),
|activity_indicator, _, event, cx| {
if let LspStoreEvent::LanguageServerUpdate { name, message, .. } = event {
if let proto::update_language_server::Variant::StatusUpdate(status_update) =
message
{
let Some(name) = name.clone() else {
return;
};
let status = match &status_update.status {
Some(proto::status_update::Status::Binary(binary_status)) => {
if let Some(binary_status) =
proto::ServerBinaryStatus::from_i32(*binary_status)
{
let binary_status = match binary_status {
proto::ServerBinaryStatus::None => BinaryStatus::None,
proto::ServerBinaryStatus::CheckingForUpdate => {
BinaryStatus::CheckingForUpdate
}
proto::ServerBinaryStatus::Downloading => {
BinaryStatus::Downloading
}
proto::ServerBinaryStatus::Starting => {
BinaryStatus::Starting
}
proto::ServerBinaryStatus::Stopping => {
BinaryStatus::Stopping
}
proto::ServerBinaryStatus::Stopped => {
BinaryStatus::Stopped
}
proto::ServerBinaryStatus::Failed => {
let Some(error) = status_update.message.clone()
else {
return;
};
BinaryStatus::Failed { error }
}
};
LanguageServerStatusUpdate::Binary(binary_status)
} else {
return;
}
}
Some(proto::status_update::Status::Health(health_status)) => {
if let Some(health) =
proto::ServerHealth::from_i32(*health_status)
{
let health = match health {
proto::ServerHealth::Ok => ServerHealth::Ok,
proto::ServerHealth::Warning => ServerHealth::Warning,
proto::ServerHealth::Error => ServerHealth::Error,
};
LanguageServerStatusUpdate::Health(
health,
status_update.message.clone().map(SharedString::from),
)
} else {
return;
}
}
None => return,
};
activity_indicator.statuses.retain(|s| s.name != name);
activity_indicator
.statuses
.push(ServerStatus { name, status });
}
cx.notify()
}
},
)
.detach();
cx.subscribe(
&project.read(cx).environment().clone(),
|_, _, event, cx| match event {
ProjectEnvironmentEvent::ErrorsUpdated => cx.notify(),
},
)
.detach();
cx.subscribe(
&project.read(cx).git_store().clone(),
|_, _, event: &GitStoreEvent, cx| {
if let project::git_store::GitStoreEvent::JobsUpdated = event {
cx.notify()
}
},
)
.detach();
cx.subscribe(
&project,
|this, _, event: &project::Event, cx| match event {
project::Event::WorktreeAdded(_)
| project::Event::WorktreeRemoved(_)
| project::Event::WorktreeUpdatedEntries(..) => {
this.update_deferred_scan_status(cx);
}
_ => {}
},
)
.detach();
let mut this = Self {
statuses: Vec::new(),
project: project.clone(),
context_menu_handle: PopoverMenuHandle::default(),
fs_jobs: Vec::new(),
deferred_scan_message: DeferredScanMessage::default(),
};
this.update_deferred_scan_status(cx);
this
});
cx.subscribe_in(&this, window, move |_, _, event, window, cx| match event {
Event::ShowStatus {
server_name,
status,
} => {
let create_buffer =
project.update(cx, |project, cx| project.create_buffer(None, false, cx));
let status = status.clone();
let server_name = server_name.clone();
cx.spawn_in(window, async move |workspace, cx| {
let buffer = create_buffer.await?;
buffer.update(cx, |buffer, cx| {
buffer.edit(
[(0..0, format!("Language server {server_name}:\n\n{status}"))],
None,
cx,
);
buffer.set_capability(language::Capability::ReadOnly, cx);
});
workspace.update_in(cx, |workspace, window, cx| {
workspace.add_item_to_active_pane(
Box::new(cx.new(|cx| {
let mut editor = Editor::for_buffer(buffer, None, window, cx);
editor.set_read_only(true);
editor
})),
None,
true,
window,
cx,
);
})?;
anyhow::Ok(())
})
.detach();
}
})
.detach();
this
}
fn show_error_message(&mut self, _: &ShowErrorMessage, _: &mut Window, cx: &mut Context<Self>) {
let mut status_message_shown = false;
self.statuses.retain(|status| match &status.status {
LanguageServerStatusUpdate::Binary(BinaryStatus::Failed { error })
if !status_message_shown =>
{
cx.emit(Event::ShowStatus {
server_name: status.name.clone(),
status: SharedString::from(error),
});
status_message_shown = true;
false
}
LanguageServerStatusUpdate::Health(
ServerHealth::Error | ServerHealth::Warning,
status_string,
) if !status_message_shown => match status_string {
Some(error) => {
cx.emit(Event::ShowStatus {
server_name: status.name.clone(),
status: error.clone(),
});
status_message_shown = true;
false
}
None => false,
},
_ => true,
});
}
fn dismiss_message(&mut self, _: &DismissMessage, _: &mut Window, cx: &mut Context<Self>) {
self.project.update(cx, |project, cx| {
if project.last_formatting_failure(cx).is_some() {
project.reset_last_formatting_failure(cx);
true
} else {
false
}
});
}
fn pending_language_server_work<'a>(
&self,
cx: &'a App,
) -> impl Iterator<Item = PendingWork<'a>> {
self.project
.read(cx)
.language_server_statuses(cx)
.rev()
.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
})
}
fn pending_environment_error<'a>(&'a self, cx: &'a App) -> Option<&'a String> {
self.project.read(cx).peek_environment_error(cx)
}
fn update_deferred_scan_status(&mut self, cx: &mut Context<Self>) {
let has_deferred_scan_dirs = self.project.read(cx).visible_worktrees(cx).any(|worktree| {
let worktree = worktree.read(cx);
worktree.deferred_scan_dir_count() > 0
&& worktree.as_local().is_none_or(|local_worktree| {
local_worktree.settings().file_scan_depth.is_some()
})
});
match &self.deferred_scan_message {
DeferredScanMessage::Undetected if has_deferred_scan_dirs => {
self.deferred_scan_message = DeferredScanMessage::Pending;
cx.notify();
}
DeferredScanMessage::Pending | DeferredScanMessage::Shown { .. }
if !has_deferred_scan_dirs =>
{
self.deferred_scan_message = DeferredScanMessage::Undetected;
cx.notify();
}
DeferredScanMessage::Dismissed if !has_deferred_scan_dirs => {
self.deferred_scan_message = DeferredScanMessage::Undetected;
}
_ => {}
}
}
#[cfg(feature = "test-support")]
pub fn message_to_render(&mut self, cx: &mut Context<Self>) -> Option<String> {
self.content_to_render(cx).map(|content| content.message)
}
fn content_to_render(&mut self, cx: &mut Context<Self>) -> Option<Content> {
if let Some(content) = self.primary_content(cx) {
return Some(content);
}
self.deferred_scan_content(cx)
}
fn primary_content(&mut self, cx: &mut Context<Self>) -> Option<Content> {
// Show if any direnv calls failed
if let Some(message) = self.pending_environment_error(cx) {
return Some(Content {
icon: ActivityIcon::Icon(IconName::Warning),
message: message.clone(),
on_click: Some(Arc::new(move |this, window, cx| {
this.project.update(cx, |project, cx| {
project.pop_environment_error(cx);
});
window.dispatch_action(Box::new(workspace::OpenLog), cx);
})),
tooltip_message: None,
});
}
// Show any language server has pending activity.
{
let mut pending_work = self.pending_language_server_work(cx);
if let Some(PendingWork {
progress_token,
progress,
..
}) = pending_work.next()
{
let mut message = progress.title.clone().unwrap_or(progress_token.to_string());
if let Some(percentage) = progress.percentage {
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();
if additional_work_count > 0 {
write!(&mut message, " + {} more", additional_work_count).unwrap();
}
return Some(Content {
icon: ActivityIcon::LoadingSpinner,
message,
on_click: None,
tooltip_message: None,
});
}
}
if let Some(session) = self
.project
.read(cx)
.dap_store()
.read(cx)
.sessions()
.find(|s| !s.read(cx).is_started())
{
return Some(Content {
icon: ActivityIcon::LoadingSpinner,
message: format!("Debug: {}", session.read(cx).adapter()),
tooltip_message: session.read(cx).label().map(|label| label.to_string()),
on_click: None,
});
}
let current_job = self
.project
.read(cx)
.active_repository(cx)
.map(|r| r.read(cx))
.and_then(Repository::current_job);
// Show any long-running git command
if let Some(job_info) = current_job
&& Instant::now() - job_info.start >= GIT_OPERATION_DELAY
{
return Some(Content {
icon: ActivityIcon::LoadingSpinner,
message: job_info.message.into(),
on_click: None,
tooltip_message: None,
});
}
// Show any long-running fs command
for fs_job in &self.fs_jobs {
if Instant::now().duration_since(fs_job.start) >= GIT_OPERATION_DELAY {
return Some(Content {
icon: ActivityIcon::LoadingSpinner,
message: fs_job.message.clone().into(),
on_click: None,
tooltip_message: None,
});
}
}
// Show any language server installation info.
let mut downloading = SmallVec::<[_; 3]>::new();
let mut checking_for_update = SmallVec::<[_; 3]>::new();
let mut failed = SmallVec::<[_; 3]>::new();
let mut health_messages = SmallVec::<[_; 3]>::new();
let mut servers_to_clear_statuses = HashSet::<LanguageServerName>::default();
for status in &self.statuses {
match &status.status {
LanguageServerStatusUpdate::Binary(
BinaryStatus::Starting | BinaryStatus::Stopping,
) => {}
LanguageServerStatusUpdate::Binary(BinaryStatus::Stopped) => {
servers_to_clear_statuses.insert(status.name.clone());
}
LanguageServerStatusUpdate::Binary(BinaryStatus::CheckingForUpdate) => {
checking_for_update.push(status.name.clone());
}
LanguageServerStatusUpdate::Binary(BinaryStatus::Downloading) => {
downloading.push(status.name.clone());
}
LanguageServerStatusUpdate::Binary(BinaryStatus::Failed { .. }) => {
failed.push(status.name.clone());
}
LanguageServerStatusUpdate::Binary(BinaryStatus::None) => {}
LanguageServerStatusUpdate::Health(health, server_status) => match server_status {
Some(server_status) => {
health_messages.push((status.name.clone(), *health, server_status.clone()));
}
None => {
servers_to_clear_statuses.insert(status.name.clone());
}
},
}
}
self.statuses
.retain(|status| !servers_to_clear_statuses.contains(&status.name));
health_messages.sort_by_key(|(_, health, _)| match health {
ServerHealth::Error => 2,
ServerHealth::Warning => 1,
ServerHealth::Ok => 0,
});
if !downloading.is_empty() {
return Some(Content {
icon: ActivityIcon::Icon(IconName::Download),
message: format!(
"Downloading {}...",
downloading.iter().map(|name| name.as_ref()).fold(
String::new(),
|mut acc, s| {
if !acc.is_empty() {
acc.push_str(", ");
}
acc.push_str(s);
acc
}
)
),
on_click: Some(Arc::new(move |this, window, cx| {
this.statuses
.retain(|status| !downloading.contains(&status.name));
this.dismiss_message(&DismissMessage, window, cx)
})),
tooltip_message: None,
});
}
if !checking_for_update.is_empty() {
return Some(Content {
icon: ActivityIcon::Icon(IconName::Download),
message: format!(
"Checking for updates to {}...",
checking_for_update.iter().map(|name| name.as_ref()).fold(
String::new(),
|mut acc, s| {
if !acc.is_empty() {
acc.push_str(", ");
}
acc.push_str(s);
acc
}
),
),
on_click: Some(Arc::new(move |this, window, cx| {
this.statuses
.retain(|status| !checking_for_update.contains(&status.name));
this.dismiss_message(&DismissMessage, window, cx)
})),
tooltip_message: None,
});
}
if !failed.is_empty() {
return Some(Content {
icon: ActivityIcon::Icon(IconName::Warning),
message: format!(
"Failed to run {}. Click to show error.",
failed
.iter()
.map(|name| name.as_ref())
.fold(String::new(), |mut acc, s| {
if !acc.is_empty() {
acc.push_str(", ");
}
acc.push_str(s);
acc
}),
),
on_click: Some(Arc::new(|this, window, cx| {
this.show_error_message(&ShowErrorMessage, window, cx)
})),
tooltip_message: None,
});
}
// Show any formatting failure
if let Some(failure) = self.project.read(cx).last_formatting_failure(cx) {
return Some(Content {
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| {
project.reset_last_formatting_failure(cx);
});
window.dispatch_action(Box::new(workspace::OpenLog), cx);
})),
tooltip_message: None,
});
}
// Show any health messages for the language servers
if let Some((server_name, health, message)) = health_messages.pop() {
let health_str = match health {
ServerHealth::Ok => format!("({server_name}) "),
ServerHealth::Warning => format!("({server_name}) Warning: "),
ServerHealth::Error => format!("({server_name}) Error: "),
};
let single_line_message = message
.lines()
.filter_map(|line| {
let line = line.trim();
if line.is_empty() { None } else { Some(line) }
})
.collect::<Vec<_>>()
.join(" ");
let mut altered_message = single_line_message != message;
let truncated_message = truncate_and_trailoff(
&single_line_message,
MAX_MESSAGE_LEN.saturating_sub(health_str.len()),
);
altered_message |= truncated_message != single_line_message;
let final_message = format!("{health_str}{truncated_message}");
let tooltip_message = if altered_message {
Some(format!("{health_str}{message}"))
} else {
None
};
return Some(Content {
icon: ActivityIcon::Icon(IconName::Warning),
message: final_message,
tooltip_message,
on_click: Some(Arc::new(move |activity_indicator, window, cx| {
if altered_message {
activity_indicator.show_error_message(&ShowErrorMessage, window, cx)
} else {
activity_indicator
.statuses
.retain(|status| status.name != server_name);
cx.notify();
}
})),
});
}
// Show any extension installation info.
if let Some(extension_store) =
ExtensionStore::try_global(cx).map(|extension_store| extension_store.read(cx))
&& let Some((extension_id, operation)) =
extension_store.outstanding_operations().iter().next()
{
let (message, icon) = match operation {
ExtensionOperation::Install => (
format!("Installing {extension_id} extension…"),
ActivityIcon::LoadingSpinner,
),
ExtensionOperation::Upgrade => (
format!("Updating {extension_id} extension…"),
ActivityIcon::Icon(IconName::Download),
),
ExtensionOperation::Remove => (
format!("Removing {extension_id} extension…"),
ActivityIcon::LoadingSpinner,
),
};
return Some(Content {
icon,
message,
on_click: Some(Arc::new(|this, window, cx| {
this.dismiss_message(&Default::default(), window, cx)
})),
tooltip_message: None,
});
}
None
}
fn deferred_scan_content(&mut self, cx: &mut Context<Self>) -> Option<Content> {
if !matches!(
self.deferred_scan_message,
DeferredScanMessage::Pending | DeferredScanMessage::Shown { .. }
) {
return None;
}
if matches!(self.deferred_scan_message, DeferredScanMessage::Pending) {
self.deferred_scan_message = DeferredScanMessage::Shown {
_dismiss_timer: cx.spawn(async move |this, cx| {
cx.background_executor()
.timer(DEFERRED_SCAN_MESSAGE_TIMEOUT)
.await;
this.update(cx, |this, cx| {
this.deferred_scan_message = DeferredScanMessage::Dismissed;
cx.notify();
})
.ok();
}),
};
}
Some(Content {
icon: ActivityIcon::Icon(IconName::Info),
message: "Partial file index".to_string(),
tooltip_message: Some("Directories outside of git repositories and deeper than the `file_scan_depth` setting will be indexed on demand.".to_string()),
on_click: Some(Arc::new(|this, _, cx| {
this.deferred_scan_message = DeferredScanMessage::Dismissed;
cx.notify();
})),
})
}
}
impl EventEmitter<Event> for ActivityIndicator {}
const MAX_MESSAGE_LEN: usize = 50;
impl Render for ActivityIndicator {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let result = h_flex()
.id("activity-indicator")
.on_action(cx.listener(Self::show_error_message))
.on_action(cx.listener(Self::dismiss_message));
let Some(content) = self.content_to_render(cx) else {
return result;
};
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.child(
PopoverMenu::new("activity-indicator-popover")
.trigger(
Button::new("activity-indicator-trigger", {
if truncate_content {
truncate_and_trailoff(&content.message, MAX_MESSAGE_LEN)
} else {
content.message.clone()
}
})
.label_size(LabelSize::Small)
.tab_index(0isize)
.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))
} else {
button.when_some(content.tooltip_message, |this, tooltip_message| {
this.tooltip(Tooltip::text(tooltip_message))
})
}
})
.when_some(content.on_click, |this, handler| {
this.on_click(cx.listener(move |this, _, window, cx| {
handler(this, window, cx);
}))
}),
)
.anchor(gpui::Anchor::BottomLeft)
.when(!has_click_handler, |this| {
this.menu(move |window, cx| {
let strong_this = activity_indicator.upgrade()?;
let mut has_cancellable_work = false;
let menu = ContextMenu::build(window, cx, |mut menu, _, cx| {
for work in strong_this.read(cx).pending_language_server_work(cx) {
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 {
has_cancellable_work = true;
let language_server_id = work.language_server_id;
let token = work.progress_token.clone();
let title = SharedString::from(format!("Cancel {title}"));
menu = menu.custom_entry(
move |_, _| {
h_flex()
.w_full()
.gap_1()
.child(
Icon::new(IconName::Close)
.color(Color::Muted)
.size(IconSize::Small),
)
.child(Label::new(title.clone()))
.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
});
has_cancellable_work.then_some(menu)
})
}),
)
}
}
impl StatusItemView for ActivityIndicator {
fn set_active_pane_item(
&mut self,
_: Option<&dyn ItemHandle>,
_window: &mut Window,
_: &mut Context<Self>,
) {
}
fn hide_setting(&self, _: &App) -> Option<workspace::HideStatusItem> {
// Activity indicator auto-hides when there's no work to display.
None
}
}