diff --git a/crates/activity_indicator/src/activity_indicator.rs b/crates/activity_indicator/src/activity_indicator.rs index 47f9d1c2e2..25509544d7 100644 --- a/crates/activity_indicator/src/activity_indicator.rs +++ b/crates/activity_indicator/src/activity_indicator.rs @@ -3,22 +3,19 @@ use editor::Editor; use extension::ExtensionStore; use futures::StreamExt; use gpui::{ - actions, svg, AppContext, CursorStyle, EventEmitter, InteractiveElement as _, Model, - ParentElement as _, Render, SharedString, StatefulInteractiveElement, Styled, View, - ViewContext, VisualContext as _, + actions, percentage, Animation, AnimationExt as _, AppContext, CursorStyle, EventEmitter, + InteractiveElement as _, Model, ParentElement as _, Render, SharedString, + StatefulInteractiveElement, Styled, Transformation, View, ViewContext, VisualContext as _, }; use language::{LanguageRegistry, LanguageServerBinaryStatus, LanguageServerName}; use project::{LanguageServerProgress, Project}; 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 workspace::{item::ItemHandle, StatusItemView, Workspace}; actions!(activity_indicator, [ShowErrorMessage]); -const DOWNLOAD_ICON: &str = "icons/download.svg"; -const WARNING_ICON: &str = "icons/warning.svg"; - pub enum Event { ShowError { lsp_name: Arc, error: String }, } @@ -35,14 +32,13 @@ struct LspStatus { } struct PendingWork<'a> { - language_server_name: &'a str, progress_token: &'a str, progress: &'a LanguageServerProgress, } #[derive(Default)] struct Content { - icon: Option<&'static str>, + icon: Option, message: String, on_click: Option)>>, } @@ -159,7 +155,6 @@ impl ActivityIndicator { .pending_work .iter() .map(|(token, progress)| PendingWork { - language_server_name: status.name.as_str(), progress_token: token.as_str(), progress, }) @@ -175,31 +170,41 @@ impl ActivityIndicator { // Show any language server has pending activity. let mut pending_work = self.pending_language_server_work(cx); if let Some(PendingWork { - language_server_name, progress_token, progress, }) = pending_work.next() { - let mut message = language_server_name.to_string(); - - message.push_str(": "); - if let Some(progress_message) = progress.message.as_ref() { - message.push_str(progress_message); - } else { - message.push_str(progress_token); - } + let mut message = progress + .title + .as_deref() + .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 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, on_click: None, }; @@ -222,7 +227,11 @@ impl ActivityIndicator { if !downloading.is_empty() { return Content { - icon: Some(DOWNLOAD_ICON), + icon: Some( + Icon::new(IconName::Download) + .size(IconSize::Small) + .into_any_element(), + ), message: format!("Downloading {}...", downloading.join(", "),), on_click: None, }; @@ -230,7 +239,11 @@ impl ActivityIndicator { if !checking_for_update.is_empty() { return Content { - icon: Some(DOWNLOAD_ICON), + icon: Some( + Icon::new(IconName::Download) + .size(IconSize::Small) + .into_any_element(), + ), message: format!( "Checking for updates to {}...", checking_for_update.join(", "), @@ -241,7 +254,11 @@ impl ActivityIndicator { if !failed.is_empty() { return Content { - icon: Some(WARNING_ICON), + icon: Some( + Icon::new(IconName::ExclamationTriangle) + .size(IconSize::Small) + .into_any_element(), + ), message: format!( "Failed to download {}. Click to show error.", failed.join(", "), @@ -255,7 +272,11 @@ impl ActivityIndicator { // Show any formatting failure if let Some(failure) = self.project.read(cx).last_formatting_failure() { 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), on_click: Some(Arc::new(|_, cx| { cx.dispatch_action(Box::new(workspace::OpenLog)); @@ -267,17 +288,29 @@ impl ActivityIndicator { if let Some(updater) = &self.auto_updater { return match &updater.read(cx).status() { 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(), on_click: None, }, 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(), on_click: None, }, 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(), on_click: None, }, @@ -292,7 +325,11 @@ impl ActivityIndicator { })), }, 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(), on_click: Some(Arc::new(|this, 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() { return Content { - icon: Some(DOWNLOAD_ICON), + icon: Some( + Icon::new(IconName::Download) + .size(IconSize::Small) + .into_any_element(), + ), message: format!("Updating {extension_id} extension…"), on_click: None, }; @@ -338,7 +379,8 @@ impl Render for ActivityIndicator { } result - .children(content.icon.map(|icon| svg().path(icon))) + .gap_2() + .children(content.icon) .child(Label::new(SharedString::from(content.message)).size(LabelSize::Small)) } } diff --git a/crates/collab/src/tests/editor_tests.rs b/crates/collab/src/tests/editor_tests.rs index fff8dc12d8..7b92f15cbe 100644 --- a/crates/collab/src/tests/editor_tests.rs +++ b/crates/collab/src/tests/editor_tests.rs @@ -28,7 +28,7 @@ use language::{ use multi_buffer::MultiBufferRow; use project::{ project_settings::{InlineBlameSettings, ProjectSettings}, - SERVER_PROGRESS_DEBOUNCE_TIMEOUT, + SERVER_PROGRESS_THROTTLE_TIMEOUT, }; use recent_projects::disconnected_overlay::DisconnectedOverlay; 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(); fake_language_server.start_progress("the-token").await; + + executor.advance_clock(SERVER_PROGRESS_THROTTLE_TIMEOUT); fake_language_server.notify::(lsp::ProgressParams { token: lsp::NumberOrString::String("the-token".to_string()), 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(); 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"); }); + executor.advance_clock(SERVER_PROGRESS_THROTTLE_TIMEOUT); fake_language_server.notify::(lsp::ProgressParams { token: lsp::NumberOrString::String("the-token".to_string()), 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(); project_a.read_with(cx_a, |project, _| { diff --git a/crates/diagnostics/src/items.rs b/crates/diagnostics/src/items.rs index 715da22ef1..80b31b999c 100644 --- a/crates/diagnostics/src/items.rs +++ b/crates/diagnostics/src/items.rs @@ -1,9 +1,7 @@ -use std::time::Duration; - use editor::Editor; use gpui::{ - percentage, rems, Animation, AnimationExt, EventEmitter, IntoElement, ParentElement, Render, - Styled, Subscription, Transformation, View, ViewContext, WeakView, + rems, EventEmitter, IntoElement, ParentElement, Render, Styled, Subscription, View, + ViewContext, WeakView, }; use language::Diagnostic; 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)), }; - let has_in_progress_checks = self - .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 status = if let Some(diagnostic) = &self.current_diagnostic { let message = diagnostic.message.split('\n').next().unwrap().to_string(); Some( Button::new("diagnostic_message", message) diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs index 4eb4a99528..39d8b3036f 100644 --- a/crates/editor/src/actions.rs +++ b/crates/editor/src/actions.rs @@ -169,6 +169,7 @@ gpui::actions!( AddSelectionBelow, Backspace, Cancel, + CancelLanguageServerWork, ConfirmRename, ContextMenuFirst, ContextMenuLast, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index fa21670ba7..f4e331d825 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -9494,6 +9494,20 @@ impl Editor { } } + fn cancel_language_server_work( + &mut self, + _: &CancelLanguageServerWork, + cx: &mut ViewContext, + ) { + 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) { cx.show_character_palette(); } diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index a078f64f11..09ed7a870e 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -341,6 +341,7 @@ impl EditorElement { } }); 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, action, cx| { if let Some(task) = editor.confirm_completion(action, cx) { diff --git a/crates/gpui/src/executor.rs b/crates/gpui/src/executor.rs index 2e1bd1bda3..a3a7e3646b 100644 --- a/crates/gpui/src/executor.rs +++ b/crates/gpui/src/executor.rs @@ -13,7 +13,7 @@ use std::{ Arc, }, task::{Context, Poll}, - time::Duration, + time::{Duration, Instant}, }; use util::TryFutureExt; 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. /// Depending on other concurrent tasks the elapsed duration may be longer /// than requested. diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index def79f526d..b9af658963 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -38,7 +38,7 @@ use seahash::SeaHasher; use serde::{Deserialize, Serialize}; use std::borrow::Cow; use std::hash::{Hash, Hasher}; -use std::time::Duration; +use std::time::{Duration, Instant}; use std::{ fmt::{self, Debug}, ops::Range, @@ -275,6 +275,9 @@ pub trait PlatformDispatcher: Send + Sync { fn dispatch_after(&self, duration: Duration, runnable: Runnable); fn park(&self, timeout: Option) -> bool; fn unparker(&self) -> Unparker; + fn now(&self) -> Instant { + Instant::now() + } #[cfg(any(test, feature = "test-support"))] fn as_test(&self) -> Option<&TestDispatcher> { diff --git a/crates/gpui/src/platform/test/dispatcher.rs b/crates/gpui/src/platform/test/dispatcher.rs index e9cab32e96..6319999293 100644 --- a/crates/gpui/src/platform/test/dispatcher.rs +++ b/crates/gpui/src/platform/test/dispatcher.rs @@ -11,7 +11,7 @@ use std::{ pin::Pin, sync::Arc, task::{Context, Poll}, - time::Duration, + time::{Duration, Instant}, }; use util::post_inc; @@ -32,6 +32,7 @@ struct TestDispatcherState { background: Vec, deprioritized_background: Vec, delayed: Vec<(Duration, Runnable)>, + start_time: Instant, time: Duration, is_main_thread: bool, next_id: TestDispatcherId, @@ -52,6 +53,7 @@ impl TestDispatcher { deprioritized_background: Vec::new(), delayed: Vec::new(), time: Duration::ZERO, + start_time: Instant::now(), is_main_thread: true, next_id: TestDispatcherId(1), allow_parking: false, @@ -251,6 +253,11 @@ impl PlatformDispatcher for TestDispatcher { 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) { { let mut state = self.state.lock(); diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index ce3b5d566b..bcfe3181cd 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -1351,6 +1351,14 @@ impl FakeLanguageServer { /// 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) { + self.start_progress_with(token, Default::default()).await + } + + pub async fn start_progress_with( + &self, + token: impl Into, + progress: WorkDoneProgressBegin, + ) { let token = token.into(); self.request::(WorkDoneProgressCreateParams { token: NumberOrString::String(token.clone()), @@ -1359,7 +1367,7 @@ impl FakeLanguageServer { .unwrap(); self.notify::(ProgressParams { token: NumberOrString::String(token), - value: ProgressParamsValue::WorkDone(WorkDoneProgress::Begin(Default::default())), + value: ProgressParamsValue::WorkDone(WorkDoneProgress::Begin(progress)), }); } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 748a489524..b478702a60 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -19,7 +19,7 @@ use client::{ TypedEnvelope, UserStore, }; 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 futures::{ channel::{ @@ -62,6 +62,7 @@ use lsp::{ DocumentHighlightKind, Edit, FileSystemWatcher, InsertTextFormat, LanguageServer, LanguageServerBinary, LanguageServerId, LspRequestFuture, MessageActionItem, OneOf, ServerCapabilities, ServerHealthStatus, ServerStatus, TextEdit, Uri, + WorkDoneProgressCancelParams, }; use lsp_command::*; use node_runtime::NodeRuntime; @@ -131,7 +132,7 @@ pub use worktree::{ const MAX_SERVER_REINSTALL_ATTEMPT_COUNT: u64 = 4; const SERVER_REINSTALL_DEBOUNCE_TIMEOUT: Duration = Duration::from_secs(1); 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; @@ -164,9 +165,6 @@ pub struct Project { worktrees_reordered: bool, active_entry: Option, buffer_ordered_messages_tx: mpsc::UnboundedSender, - pending_language_server_update: Option, - flush_language_server_update: Option>, - languages: Arc, supplementary_language_servers: HashMap)>, @@ -381,6 +379,9 @@ pub struct LanguageServerStatus { #[derive(Clone, Debug, Serialize)] pub struct LanguageServerProgress { + pub is_disk_based_diagnostics_progress: bool, + pub is_cancellable: bool, + pub title: Option, pub message: Option, pub percentage: Option, #[serde(skip_serializing)] @@ -723,8 +724,6 @@ impl Project { worktrees: Vec::new(), worktrees_reordered: false, buffer_ordered_messages_tx: tx, - flush_language_server_update: None, - pending_language_server_update: None, collaborators: Default::default(), opened_buffers: Default::default(), shared_buffers: Default::default(), @@ -864,8 +863,6 @@ impl Project { worktrees: Vec::new(), worktrees_reordered: false, buffer_ordered_messages_tx: tx, - pending_language_server_update: None, - flush_language_server_update: None, loading_buffers_by_path: Default::default(), loading_buffers: Default::default(), shared_buffers: Default::default(), @@ -4142,6 +4139,40 @@ impl Project { .detach(); } + pub fn cancel_language_server_work_for_buffers( + &mut self, + buffers: impl IntoIterator>, + cx: &mut ModelContext, + ) { + let servers = buffers + .into_iter() + .flat_map(|buffer| { + self.language_server_ids_for_buffer(buffer.read(cx), cx) + .into_iter() + }) + .collect::>(); + + 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::( + WorkDoneProgressCancelParams { + token: lsp::NumberOrString::String(token.clone()), + }, + ) + .ok(); + } + } + } + } + } + } + fn check_errored_server( language: Arc, adapter: Arc, @@ -4211,35 +4242,7 @@ impl Project { .detach(); } - fn enqueue_language_server_progress( - &mut self, - message: BufferOrderedMessage, - cx: &mut ModelContext, - ) { - 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<()> { - 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 .unbounded_send(message) .map_err(|e| anyhow!(e)) @@ -4259,6 +4262,7 @@ impl Project { return; } }; + let lsp::ProgressParamsValue::WorkDone(progress) = progress.value; let language_server_status = if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) { @@ -4281,32 +4285,36 @@ impl Project { lsp::WorkDoneProgress::Begin(report) => { if is_disk_based_diagnostics_progress { 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) => { - if !is_disk_based_diagnostics_progress { - self.on_lsp_work_progress( - 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.enqueue_language_server_progress( + if self.on_lsp_work_progress( + language_server_id, + token.clone(), + LanguageServerProgress { + title: None, + 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, + ) { + self.enqueue_buffer_ordered_message( BufferOrderedMessage::LanguageServerUpdate { language_server_id, message: proto::update_language_server::Variant::WorkProgress( @@ -4317,17 +4325,15 @@ impl Project { }, ), }, - cx, - ); + ) + .ok(); } } lsp::WorkDoneProgress::End(_) => { language_server_status.progress_tokens.remove(&token); - + self.on_lsp_work_end(language_server_id, token.clone(), cx); if is_disk_based_diagnostics_progress { 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, message: proto::update_language_server::Variant::WorkStart(proto::LspWorkStart { token, + title: progress.title, message: progress.message, percentage: progress.percentage.map(|p| p as u32), }), @@ -4364,25 +4371,34 @@ impl Project { token: String, progress: LanguageServerProgress, cx: &mut ModelContext, - ) { + ) -> bool { if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) { - let entry = status - .pending_work - .entry(token) - .or_insert(LanguageServerProgress { - message: Default::default(), - percentage: Default::default(), - last_update_at: progress.last_update_at, - }); - if progress.message.is_some() { - entry.message = progress.message; + match status.pending_work.entry(token) { + btree_map::Entry::Vacant(entry) => { + entry.insert(progress); + cx.notify(); + return true; + } + btree_map::Entry::Occupied(mut entry) => { + let entry = entry.get_mut(); + if (progress.last_update_at - entry.last_update_at) + >= 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( @@ -4392,8 +4408,11 @@ impl Project { cx: &mut ModelContext, ) { if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) { - cx.emit(Event::RefreshInlayHints); - status.pending_work.remove(&token); + if let Some(work) = status.pending_work.remove(&token) { + if !work.is_disk_based_diagnostics_progress { + cx.emit(Event::RefreshInlayHints); + } + } cx.notify(); } @@ -7384,9 +7403,12 @@ impl Project { language_server.server_id(), id.to_string(), LanguageServerProgress { + is_disk_based_diagnostics_progress: false, + is_cancellable: false, + title: None, message: status.clone(), percentage: None, - last_update_at: Instant::now(), + last_update_at: cx.background_executor().now(), }, cx, ); @@ -9005,9 +9027,12 @@ impl Project { language_server_id, payload.token, LanguageServerProgress { + title: payload.title, + is_disk_based_diagnostics_progress: false, + is_cancellable: false, message: payload.message, percentage: payload.percentage.map(|p| p as usize), - last_update_at: Instant::now(), + last_update_at: cx.background_executor().now(), }, cx, ); @@ -9018,9 +9043,12 @@ impl Project { language_server_id, payload.token, LanguageServerProgress { + title: None, + is_disk_based_diagnostics_progress: false, + is_cancellable: false, message: payload.message, percentage: payload.percentage.map(|p| p as usize), - last_update_at: Instant::now(), + last_update_at: cx.background_executor().now(), }, cx, ); diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index ed83d6d1ce..1aa3ad767a 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -7,6 +7,7 @@ use language::{ tree_sitter_rust, tree_sitter_typescript, Diagnostic, FakeLspAdapter, LanguageConfig, LanguageMatcher, LineEnding, OffsetRangeExt, Point, ToPoint, }; +use lsp::NumberOrString; use parking_lot::Mutex; use pretty_assertions::assert_eq; 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); } +#[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::() + .await; + assert_eq!( + cancel_notification.token, + NumberOrString::String(progress_token.into()) + ); +} + #[gpui::test] async fn test_toggling_enable_language_server(cx: &mut gpui::TestAppContext) { init_test(cx); @@ -3758,6 +3822,7 @@ async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_rename(cx: &mut gpui::TestAppContext) { + // hi init_test(cx); let fs = FakeFs::new(cx.executor()); diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index f78ae824d9..3b27c5b536 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -1199,6 +1199,7 @@ message UpdateLanguageServer { message LspWorkStart { string token = 1; + optional string title = 4; optional string message = 2; optional uint32 percentage = 3; } diff --git a/crates/ui/src/components/icon.rs b/crates/ui/src/components/icon.rs index 1433e6069a..bda16f6e13 100644 --- a/crates/ui/src/components/icon.rs +++ b/crates/ui/src/components/icon.rs @@ -117,6 +117,7 @@ pub enum IconName { Dash, Delete, Disconnected, + Download, Ellipsis, Envelope, Escape, @@ -248,6 +249,7 @@ impl IconName { IconName::Dash => "icons/dash.svg", IconName::Delete => "icons/delete.svg", IconName::Disconnected => "icons/disconnected.svg", + IconName::Download => "icons/download.svg", IconName::Ellipsis => "icons/ellipsis.svg", IconName::Envelope => "icons/feedback.svg", IconName::Escape => "icons/escape.svg", diff --git a/script/zed-local b/script/zed-local index 81d8abbc22..c3dfb2879d 100755 --- a/script/zed-local +++ b/script/zed-local @@ -172,7 +172,7 @@ setTimeout(() => { env: Object.assign({}, process.env, { ZED_IMPERSONATE: users[i], ZED_WINDOW_POSITION: position, - ZED_STATELESS: isStateful && i == 0 ? "1" : "", + ZED_STATELESS: isStateful && i == 0 ? "" : "1", ZED_ALWAYS_ACTIVE: "1", ZED_SERVER_URL: "http://localhost:3000", ZED_RPC_URL: "http://localhost:8080/rpc",