1022 lines · 36.8 KB · Rust
RawDownload
use crate::{SuppressNotification, Toast, Workspace};use anyhow::Context as _;use gpui::{    AnyView, App, AppContext as _, AsyncWindowContext, ClickEvent, ClipboardItem, Context,    DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, PromptLevel, Render, ScrollHandle,    Task, svg,};use parking_lot::Mutex;use std::ops::Deref;use std::sync::{Arc, LazyLock};use std::{any::TypeId, time::Duration};use ui::{Tooltip, prelude::*};use util::ResultExt; #[derive(Default)]pub struct Notifications {    notifications: Vec<(NotificationId, AnyView)>,} impl Deref for Notifications {    type Target = Vec<(NotificationId, AnyView)>;     fn deref(&self) -> &Self::Target {        &self.notifications    }} impl std::ops::DerefMut for Notifications {    fn deref_mut(&mut self) -> &mut Self::Target {        &mut self.notifications    }} #[derive(Debug, Eq, PartialEq, Clone, Hash)]pub enum NotificationId {    Unique(TypeId),    Composite(TypeId, ElementId),    Named(SharedString),} impl NotificationId {    /// Returns a unique [`NotificationId`] for the given type.    pub fn unique<T: 'static>() -> Self {        Self::Unique(TypeId::of::<T>())    }     /// Returns a [`NotificationId`] for the given type that is also identified    /// by the provided ID.    pub fn composite<T: 'static>(id: impl Into<ElementId>) -> Self {        Self::Composite(TypeId::of::<T>(), id.into())    }     /// Builds a `NotificationId` out of the given string.    pub fn named(id: SharedString) -> Self {        Self::Named(id)    }} pub trait Notification:    EventEmitter<DismissEvent> + EventEmitter<SuppressEvent> + Focusable + Render{} pub struct SuppressEvent; impl Workspace {    #[cfg(any(test, feature = "test-support"))]    pub fn notification_ids(&self) -> Vec<NotificationId> {        self.notifications            .iter()            .map(|(id, _)| id)            .cloned()            .collect()    }     pub fn show_notification<V: Notification>(        &mut self,        id: NotificationId,        cx: &mut Context<Self>,        build_notification: impl FnOnce(&mut Context<Self>) -> Entity<V>,    ) {        self.show_notification_without_handling_dismiss_events(&id, cx, |cx| {            let notification = build_notification(cx);            cx.subscribe(&notification, {                let id = id.clone();                move |this, _, _: &DismissEvent, cx| {                    this.dismiss_notification(&id, cx);                }            })            .detach();            cx.subscribe(&notification, {                let id = id.clone();                move |workspace: &mut Workspace, _, _: &SuppressEvent, cx| {                    workspace.suppress_notification(&id, cx);                }            })            .detach();            notification.into()        });    }     /// Shows a notification in this workspace's window. Caller must handle dismiss.    ///    /// This exists so that the `build_notification` closures stored for app notifications can    /// return `AnyView`. Subscribing to events from an `AnyView` is not supported, so instead that    /// responsibility is pushed to the caller where the `V` type is known.    pub(crate) fn show_notification_without_handling_dismiss_events(        &mut self,        id: &NotificationId,        cx: &mut Context<Self>,        build_notification: impl FnOnce(&mut Context<Self>) -> AnyView,    ) {        if self.suppressed_notifications.contains(id) {            return;        }        self.dismiss_notification(id, cx);        self.notifications            .push((id.clone(), build_notification(cx)));        cx.notify();    }     pub fn show_error<E>(&mut self, err: &E, cx: &mut Context<Self>)    where        E: std::fmt::Debug + std::fmt::Display,    {        self.show_notification(workspace_error_notification_id(), cx, |cx| {            cx.new(|cx| ErrorMessagePrompt::new(format!("Error: {err}"), cx))        });    }     pub fn show_portal_error(&mut self, err: String, cx: &mut Context<Self>) {        struct PortalError;         self.show_notification(NotificationId::unique::<PortalError>(), cx, |cx| {            cx.new(|cx| {                ErrorMessagePrompt::new(err.to_string(), cx).with_link_button(                    "See docs",                    "https://zed.dev/docs/linux#i-cant-open-any-files",                )            })        });    }     pub fn dismiss_notification(&mut self, id: &NotificationId, cx: &mut Context<Self>) {        self.notifications.retain(|(existing_id, _)| {            if existing_id == id {                cx.notify();                false            } else {                true            }        });    }     pub fn show_toast(&mut self, toast: Toast, cx: &mut Context<Self>) {        self.dismiss_notification(&toast.id, cx);        self.show_notification(toast.id.clone(), cx, |cx| {            cx.new(|cx| match toast.on_click.as_ref() {                Some((click_msg, on_click)) => {                    let on_click = on_click.clone();                    simple_message_notification::MessageNotification::new(toast.msg.clone(), cx)                        .primary_message(click_msg.clone())                        .primary_on_click(move |window, cx| on_click(window, cx))                }                None => {                    simple_message_notification::MessageNotification::new(toast.msg.clone(), cx)                }            })        });        if toast.autohide {            cx.spawn(async move |workspace, cx| {                cx.background_executor()                    .timer(Duration::from_millis(5000))                    .await;                workspace                    .update(cx, |workspace, cx| workspace.dismiss_toast(&toast.id, cx))                    .ok();            })            .detach();        }    }     pub fn dismiss_toast(&mut self, id: &NotificationId, cx: &mut Context<Self>) {        self.dismiss_notification(id, cx);    }     pub fn clear_all_notifications(&mut self, cx: &mut Context<Self>) {        self.notifications.clear();        cx.notify();    }     pub fn suppress_notification(&mut self, id: &NotificationId, cx: &mut Context<Self>) {        self.dismiss_notification(id, cx);        self.suppressed_notifications.insert(id.clone());    }     pub fn show_initial_notifications(&mut self, cx: &mut Context<Self>) {        // Allow absence of the global so that tests don't need to initialize it.        let app_notifications = GLOBAL_APP_NOTIFICATIONS            .lock()            .app_notifications            .iter()            .cloned()            .collect::<Vec<_>>();        for (id, build_notification) in app_notifications {            self.show_notification_without_handling_dismiss_events(&id, cx, |cx| {                build_notification(cx)            });        }    }} pub struct LanguageServerPrompt {    focus_handle: FocusHandle,    request: Option<project::LanguageServerPromptRequest>,    scroll_handle: ScrollHandle,} impl Focusable for LanguageServerPrompt {    fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {        self.focus_handle.clone()    }} impl Notification for LanguageServerPrompt {} impl LanguageServerPrompt {    pub fn new(request: project::LanguageServerPromptRequest, cx: &mut App) -> Self {        Self {            focus_handle: cx.focus_handle(),            request: Some(request),            scroll_handle: ScrollHandle::new(),        }    }     async fn select_option(this: Entity<Self>, ix: usize, cx: &mut AsyncWindowContext) {        util::maybe!(async move {            let potential_future = this.update(cx, |this, _| {                this.request.take().map(|request| request.respond(ix))            });             potential_future? // App Closed                .context("Response already sent")?                .await                .context("Stream already closed")?;             this.update(cx, |_, cx| cx.emit(DismissEvent))?;             anyhow::Ok(())        })        .await        .log_err();    }} impl Render for LanguageServerPrompt {    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {        let Some(request) = &self.request else {            return div().id("language_server_prompt_notification");        };         let (icon, color) = match request.level {            PromptLevel::Info => (IconName::Info, Color::Accent),            PromptLevel::Warning => (IconName::Warning, Color::Warning),            PromptLevel::Critical => (IconName::XCircle, Color::Error),        };         let suppress = window.modifiers().shift;        let (close_id, close_icon) = if suppress {            ("suppress", IconName::Minimize)        } else {            ("close", IconName::Close)        };         div()            .id("language_server_prompt_notification")            .group("language_server_prompt_notification")            .occlude()            .w_full()            .max_h(vh(0.8, window))            .elevation_3(cx)            .overflow_y_scroll()            .track_scroll(&self.scroll_handle)            .on_modifiers_changed(cx.listener(|_, _, _, cx| cx.notify()))            .child(                v_flex()                    .p_3()                    .overflow_hidden()                    .child(                        h_flex()                            .justify_between()                            .items_start()                            .child(                                h_flex()                                    .gap_2()                                    .child(Icon::new(icon).color(color))                                    .child(Label::new(request.lsp_name.clone())),                            )                            .child(                                h_flex()                                    .gap_2()                                    .child(                                        IconButton::new("copy", IconName::Copy)                                            .on_click({                                                let message = request.message.clone();                                                move |_, _, cx| {                                                    cx.write_to_clipboard(                                                        ClipboardItem::new_string(message.clone()),                                                    )                                                }                                            })                                            .tooltip(Tooltip::text("Copy Description")),                                    )                                    .child(                                        IconButton::new(close_id, close_icon)                                            .tooltip(move |window, cx| {                                                if suppress {                                                    Tooltip::for_action(                                                        "Suppress.\nClose with click.",                                                        &SuppressNotification,                                                        window,                                                        cx,                                                    )                                                } else {                                                    Tooltip::for_action(                                                        "Close.\nSuppress with shift-click.",                                                        &menu::Cancel,                                                        window,                                                        cx,                                                    )                                                }                                            })                                            .on_click(cx.listener(                                                move |_, _: &ClickEvent, _, cx| {                                                    if suppress {                                                        cx.emit(SuppressEvent);                                                    } else {                                                        cx.emit(DismissEvent);                                                    }                                                },                                            )),                                    ),                            ),                    )                    .child(Label::new(request.message.to_string()).size(LabelSize::Small))                    .children(request.actions.iter().enumerate().map(|(ix, action)| {                        let this_handle = cx.entity().clone();                        Button::new(ix, action.title.clone())                            .size(ButtonSize::Large)                            .on_click(move |_, window, cx| {                                let this_handle = this_handle.clone();                                window                                    .spawn(cx, async move |cx| {                                        LanguageServerPrompt::select_option(this_handle, ix, cx)                                            .await                                    })                                    .detach()                            })                    })),            )    }} impl EventEmitter<DismissEvent> for LanguageServerPrompt {}impl EventEmitter<SuppressEvent> for LanguageServerPrompt {} fn workspace_error_notification_id() -> NotificationId {    struct WorkspaceErrorNotification;    NotificationId::unique::<WorkspaceErrorNotification>()} #[derive(Debug, Clone)]pub struct ErrorMessagePrompt {    message: SharedString,    focus_handle: gpui::FocusHandle,    label_and_url_button: Option<(SharedString, SharedString)>,} impl ErrorMessagePrompt {    pub fn new<S>(message: S, cx: &mut App) -> Self    where        S: Into<SharedString>,    {        Self {            message: message.into(),            focus_handle: cx.focus_handle(),            label_and_url_button: None,        }    }     pub fn with_link_button<S>(mut self, label: S, url: S) -> Self    where        S: Into<SharedString>,    {        self.label_and_url_button = Some((label.into(), url.into()));        self    }} impl Render for ErrorMessagePrompt {    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {        h_flex()            .id("error_message_prompt_notification")            .occlude()            .elevation_3(cx)            .items_start()            .justify_between()            .p_2()            .gap_2()            .w_full()            .child(                v_flex()                    .w_full()                    .child(                        h_flex()                            .w_full()                            .justify_between()                            .child(                                svg()                                    .size(window.text_style().font_size)                                    .flex_none()                                    .mr_2()                                    .mt(px(-2.0))                                    .map(|icon| {                                        icon.path(IconName::Warning.path())                                            .text_color(Color::Error.color(cx))                                    }),                            )                            .child(                                ui::IconButton::new("close", ui::IconName::Close)                                    .on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))),                            ),                    )                    .child(                        div()                            .id("error_message")                            .max_w_96()                            .max_h_40()                            .overflow_y_scroll()                            .child(Label::new(self.message.clone()).size(LabelSize::Small)),                    )                    .when_some(self.label_and_url_button.clone(), |elm, (label, url)| {                        elm.child(                            div().mt_2().child(                                ui::Button::new("error_message_prompt_notification_button", label)                                    .on_click(move |_, _, cx| cx.open_url(&url)),                            ),                        )                    }),            )    }} impl Focusable for ErrorMessagePrompt {    fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {        self.focus_handle.clone()    }} impl EventEmitter<DismissEvent> for ErrorMessagePrompt {}impl EventEmitter<SuppressEvent> for ErrorMessagePrompt {} impl Notification for ErrorMessagePrompt {} pub mod simple_message_notification {    use std::sync::Arc;     use gpui::{        AnyElement, ClickEvent, DismissEvent, EventEmitter, FocusHandle, Focusable, ParentElement,        Render, SharedString, Styled, div,    };    use ui::{Tooltip, prelude::*};     use crate::SuppressNotification;     use super::{Notification, SuppressEvent};     pub struct MessageNotification {        focus_handle: FocusHandle,        build_content: Box<dyn Fn(&mut Window, &mut Context<Self>) -> AnyElement>,        primary_message: Option<SharedString>,        primary_icon: Option<IconName>,        primary_icon_color: Option<Color>,        primary_on_click: Option<Arc<dyn Fn(&mut Window, &mut Context<Self>)>>,        secondary_message: Option<SharedString>,        secondary_icon: Option<IconName>,        secondary_icon_color: Option<Color>,        secondary_on_click: Option<Arc<dyn Fn(&mut Window, &mut Context<Self>)>>,        more_info_message: Option<SharedString>,        more_info_url: Option<Arc<str>>,        show_close_button: bool,        show_suppress_button: bool,        title: Option<SharedString>,    }     impl Focusable for MessageNotification {        fn focus_handle(&self, _: &App) -> FocusHandle {            self.focus_handle.clone()        }    }

Lines 1–500 of 1022.