diff --git a/Cargo.lock b/Cargo.lock index 054f48e2afd581919fb9151f15fb09c0ae736de1..609313363ec80bef1d686120eae4dfea975e0b11 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9503,9 +9503,12 @@ dependencies = [ "language", "picker", "project", + "schemars", "serde", "serde_json", + "settings", "task", + "terminal", "tree-sitter-rust", "tree-sitter-typescript", "ui", diff --git a/crates/tasks_ui/Cargo.toml b/crates/tasks_ui/Cargo.toml index d71ac4e6202107ba843f26351a76037b07cf3053..223e9be61cc2675eaa896c1e5a48ab92c195fa3c 100644 --- a/crates/tasks_ui/Cargo.toml +++ b/crates/tasks_ui/Cargo.toml @@ -17,9 +17,12 @@ gpui.workspace = true picker.workspace = true project.workspace = true task.workspace = true +schemars.workspace = true serde.workspace = true +settings.workspace = true ui.workspace = true util.workspace = true +terminal.workspace = true workspace.workspace = true language.workspace = true itertools.workspace = true diff --git a/crates/tasks_ui/src/lib.rs b/crates/tasks_ui/src/lib.rs index 2c0bc5e55fd462a76f54dd419d89de3d8628ea6b..bfc439e62bd122ca4efc8753bae7bb6d5851961c 100644 --- a/crates/tasks_ui/src/lib.rs +++ b/crates/tasks_ui/src/lib.rs @@ -1,5 +1,6 @@ use std::{path::PathBuf, sync::Arc}; +use ::settings::Settings; use editor::Editor; use gpui::{AppContext, ViewContext, WeakView, WindowContext}; use language::{Language, Point}; @@ -10,8 +11,13 @@ use util::ResultExt; use workspace::Workspace; mod modal; +mod settings; +mod status_indicator; + +pub use status_indicator::TaskStatusIndicator; pub fn init(cx: &mut AppContext) { + settings::TaskSettings::register(cx); cx.observe_new_views( |workspace: &mut Workspace, _: &mut ViewContext| { workspace diff --git a/crates/tasks_ui/src/modal.rs b/crates/tasks_ui/src/modal.rs index 0e5443d81c75fae42d068686e69fe9c913ab3af5..19571a9b6d0d45bc0c10b8a09d8566fd79c79970 100644 --- a/crates/tasks_ui/src/modal.rs +++ b/crates/tasks_ui/src/modal.rs @@ -30,6 +30,11 @@ pub struct Spawn { pub task_name: Option, } +impl Spawn { + pub(crate) fn modal() -> Self { + Self { task_name: None } + } +} /// Rerun last task #[derive(PartialEq, Clone, Deserialize, Default)] pub struct Rerun { @@ -144,16 +149,11 @@ impl TasksModal { } impl Render for TasksModal { - fn render(&mut self, cx: &mut ViewContext) -> impl gpui::prelude::IntoElement { + fn render(&mut self, _: &mut ViewContext) -> impl gpui::prelude::IntoElement { v_flex() .key_context("TasksModal") .w(rems(34.)) .child(self.picker.clone()) - .on_mouse_down_out(cx.listener(|modal, _, cx| { - modal.picker.update(cx, |picker, cx| { - picker.cancel(&Default::default(), cx); - }) - })) } } diff --git a/crates/tasks_ui/src/settings.rs b/crates/tasks_ui/src/settings.rs new file mode 100644 index 0000000000000000000000000000000000000000..8bff20a98428da1a452650597c8eb97bca264a54 --- /dev/null +++ b/crates/tasks_ui/src/settings.rs @@ -0,0 +1,34 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, PartialEq, Default)] +pub(crate) struct TaskSettings { + pub(crate) show_status_indicator: bool, +} + +/// Task-related settings. +#[derive(Serialize, Deserialize, PartialEq, Default, Clone, JsonSchema)] +pub(crate) struct TaskSettingsContent { + /// Whether to show task status indicator in the status bar. Default: true + show_status_indicator: Option, +} + +impl settings::Settings for TaskSettings { + const KEY: Option<&'static str> = Some("task"); + + type FileContent = TaskSettingsContent; + + fn load( + default_value: &Self::FileContent, + user_values: &[&Self::FileContent], + _: &mut gpui::AppContext, + ) -> gpui::Result + where + Self: Sized, + { + let this = Self::json_merge(default_value, user_values)?; + Ok(Self { + show_status_indicator: this.show_status_indicator.unwrap_or(true), + }) + } +} diff --git a/crates/tasks_ui/src/status_indicator.rs b/crates/tasks_ui/src/status_indicator.rs new file mode 100644 index 0000000000000000000000000000000000000000..8ed837cf481c0b1013695bbe2f4023c77a80e5dd --- /dev/null +++ b/crates/tasks_ui/src/status_indicator.rs @@ -0,0 +1,98 @@ +use gpui::{IntoElement, Render, View, WeakView}; +use settings::Settings; +use ui::{ + div, ButtonCommon, Clickable, Color, FluentBuilder, IconButton, IconName, Tooltip, + VisualContext, WindowContext, +}; +use workspace::{item::ItemHandle, StatusItemView, Workspace}; + +use crate::{modal::Spawn, settings::TaskSettings}; + +enum TaskStatus { + Failed, + Running, + Succeeded, +} + +/// A status bar icon that surfaces the status of running tasks. +/// It has a different color depending on the state of running tasks: +/// - red if any open task tab failed +/// - else, yellow if any open task tab is still running +/// - else, green if there tasks tabs open, and they have all succeeded +/// - else, no indicator if there are no open task tabs +pub struct TaskStatusIndicator { + workspace: WeakView, +} + +impl TaskStatusIndicator { + pub fn new(workspace: WeakView, cx: &mut WindowContext) -> View { + cx.new_view(|_| Self { workspace }) + } + fn current_status(&self, cx: &mut WindowContext) -> Option { + self.workspace + .update(cx, |this, cx| { + let mut status = None; + let project = this.project().read(cx); + + for handle in project.local_terminal_handles() { + let Some(handle) = handle.upgrade() else { + continue; + }; + let handle = handle.read(cx); + let task_state = handle.task(); + if let Some(state) = task_state { + match state.status { + terminal::TaskStatus::Running => { + let _ = status.insert(TaskStatus::Running); + } + terminal::TaskStatus::Completed { success } => { + if !success { + let _ = status.insert(TaskStatus::Failed); + return status; + } + status.get_or_insert(TaskStatus::Succeeded); + } + _ => {} + }; + } + } + status + }) + .ok() + .flatten() + } +} + +impl Render for TaskStatusIndicator { + fn render(&mut self, cx: &mut gpui::ViewContext) -> impl IntoElement { + if !TaskSettings::get_global(cx).show_status_indicator { + return div().into_any_element(); + } + let current_status = self.current_status(cx); + let color = current_status.map(|status| match status { + TaskStatus::Failed => Color::Error, + TaskStatus::Running => Color::Warning, + TaskStatus::Succeeded => Color::Success, + }); + IconButton::new("tasks-activity-indicator", IconName::Play) + .when_some(color, |this, color| this.icon_color(color)) + .on_click(cx.listener(|this, _, cx| { + this.workspace + .update(cx, |this, cx| { + crate::spawn_task_or_modal(this, &Spawn::modal(), cx) + }) + .ok(); + })) + .tooltip(|cx| Tooltip::for_action("Spawn tasks", &Spawn { task_name: None }, cx)) + .into_any_element() + } +} + +impl StatusItemView for TaskStatusIndicator { + fn set_active_pane_item( + &mut self, + _: Option<&dyn ItemHandle>, + _: &mut ui::prelude::ViewContext, + ) { + } +} diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 95d2c45aee0f1065444e35a14c7adf2752f74a3e..a5607519a869be0ac58b3c4765aa9fbc0351cf79 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -127,6 +127,7 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { cx.new_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace, cx)); let activity_indicator = activity_indicator::ActivityIndicator::new(workspace, app_state.languages.clone(), cx); + let tasks_indicator = tasks_ui::TaskStatusIndicator::new(workspace.weak_handle(), cx); let active_buffer_language = cx.new_view(|_| language_selector::ActiveBufferLanguage::new(workspace)); let vim_mode_indicator = cx.new_view(|cx| vim::ModeIndicator::new(cx)); @@ -136,6 +137,7 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { status_bar.add_left_item(diagnostic_summary, cx); status_bar.add_left_item(activity_indicator, cx); status_bar.add_right_item(copilot, cx); + status_bar.add_right_item(tasks_indicator, cx); status_bar.add_right_item(active_buffer_language, cx); status_bar.add_right_item(vim_mode_indicator, cx); status_bar.add_right_item(cursor_position, cx); @@ -3077,6 +3079,7 @@ mod tests { project_panel::init((), cx); terminal_view::init(cx); assistant::init(app_state.client.clone(), cx); + tasks_ui::init(cx); initialize_workspace(app_state.clone(), cx); app_state })