use crate::{ItemHandle, StatusItemView}; use futures::StreamExt; use gpui::{actions, AppContext, EventContext}; use gpui::{ elements::*, platform::CursorStyle, Entity, ModelHandle, MutableAppContext, RenderContext, View, ViewContext, }; use language::{LanguageRegistry, LanguageServerBinaryStatus}; use project::{LanguageServerProgress, Project}; use settings::Settings; use smallvec::SmallVec; use std::cmp::Reverse; use std::fmt::Write; use std::sync::Arc; actions!(lsp_status, [DismissErrorMessage]); pub struct LspStatus { checking_for_update: Vec, downloading: Vec, failed: Vec, project: ModelHandle, } pub fn init(cx: &mut MutableAppContext) { cx.add_action(LspStatus::dismiss_error_message); } impl LspStatus { pub fn new( project: &ModelHandle, languages: Arc, cx: &mut ViewContext, ) -> Self { let mut status_events = languages.language_server_binary_statuses(); cx.spawn_weak(|this, mut cx| async move { while let Some((language, event)) = status_events.next().await { if let Some(this) = this.upgrade(&cx) { this.update(&mut cx, |this, cx| { for vector in [ &mut this.checking_for_update, &mut this.downloading, &mut this.failed, ] { vector.retain(|name| name != language.name().as_ref()); } match event { LanguageServerBinaryStatus::CheckingForUpdate => { this.checking_for_update.push(language.name().to_string()); } LanguageServerBinaryStatus::Downloading => { this.downloading.push(language.name().to_string()); } LanguageServerBinaryStatus::Failed => { this.failed.push(language.name().to_string()); } LanguageServerBinaryStatus::Downloaded | LanguageServerBinaryStatus::Cached => {} } cx.notify(); }); } else { break; } } }) .detach(); cx.observe(project, |_, _, cx| cx.notify()).detach(); Self { checking_for_update: Default::default(), downloading: Default::default(), failed: Default::default(), project: project.clone(), } } fn dismiss_error_message(&mut self, _: &DismissErrorMessage, cx: &mut ViewContext) { self.failed.clear(); cx.notify(); } fn pending_language_server_work<'a>( &self, cx: &'a AppContext, ) -> impl Iterator { self.project .read(cx) .language_server_statuses() .rev() .filter_map(|status| { if status.pending_work.is_empty() { None } else { let mut pending_work = status .pending_work .iter() .map(|(token, progress)| (status.name.as_str(), token.as_str(), progress)) .collect::>(); pending_work.sort_by_key(|(_, _, progress)| Reverse(progress.last_update_at)); Some(pending_work) } }) .flatten() } } impl Entity for LspStatus { type Event = (); } impl View for LspStatus { fn ui_name() -> &'static str { "LspStatus" } fn render(&mut self, cx: &mut RenderContext) -> ElementBox { let mut message; let mut icon = None; let mut handler = None; let mut pending_work = self.pending_language_server_work(cx); if let Some((lang_server_name, progress_token, progress)) = pending_work.next() { message = lang_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); } if let Some(percentage) = progress.percentage { write!(&mut message, " ({}%)", percentage).unwrap(); } let additional_work_count = pending_work.count(); if additional_work_count > 0 { write!(&mut message, " + {} more", additional_work_count).unwrap(); } } else { drop(pending_work); if !self.downloading.is_empty() { icon = Some("icons/download-solid-14.svg"); message = format!( "Downloading {} language server{}...", self.downloading.join(", "), if self.downloading.len() > 1 { "s" } else { "" } ); } else if !self.checking_for_update.is_empty() { icon = Some("icons/download-solid-14.svg"); message = format!( "Checking for updates to {} language server{}...", self.checking_for_update.join(", "), if self.checking_for_update.len() > 1 { "s" } else { "" } ); } else if !self.failed.is_empty() { icon = Some("icons/warning-solid-14.svg"); message = format!( "Failed to download {} language server{}. Click to dismiss.", self.failed.join(", "), if self.failed.len() > 1 { "s" } else { "" } ); handler = Some(|_, _, cx: &mut EventContext| cx.dispatch_action(DismissErrorMessage)); } else { return Empty::new().boxed(); } } let mut element = MouseEventHandler::new::(0, cx, |state, cx| { let theme = &cx .global::() .theme .workspace .status_bar .lsp_status; let style = if state.hovered && handler.is_some() { theme.hover.as_ref().unwrap_or(&theme.default) } else { &theme.default }; Flex::row() .with_children(icon.map(|path| { Svg::new(path) .with_color(style.icon_color) .constrained() .with_width(style.icon_width) .contained() .with_margin_right(style.icon_spacing) .aligned() .named("warning-icon") })) .with_child(Label::new(message, style.message.clone()).aligned().boxed()) .constrained() .with_height(style.height) .contained() .with_style(style.container) .aligned() .boxed() }); if let Some(handler) = handler { element = element .with_cursor_style(CursorStyle::PointingHand) .on_click(handler); } element.boxed() } } impl StatusItemView for LspStatus { fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, _: &mut ViewContext) {} }