diff --git a/assets/icons/indicator_x.svg b/assets/icons/indicator_x.svg deleted file mode 100644 index d812c40fd2..0000000000 --- a/assets/icons/indicator_x.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/knockouts/triangle_bg.svg b/assets/icons/knockouts/triangle_bg.svg index b0c5ae6e77..990b439952 100644 --- a/assets/icons/knockouts/triangle_bg.svg +++ b/assets/icons/knockouts/triangle_bg.svg @@ -1,3 +1,10 @@ - + + + + + + + + diff --git a/assets/icons/knockouts/triangle_fg.svg b/assets/icons/knockouts/triangle_fg.svg index f8f8b8c2bc..e3b31446b3 100644 --- a/assets/icons/knockouts/triangle_fg.svg +++ b/assets/icons/knockouts/triangle_fg.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/triangle.svg b/assets/icons/triangle.svg new file mode 100644 index 0000000000..8c44b91b78 --- /dev/null +++ b/assets/icons/triangle.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/x.svg b/assets/icons/x.svg new file mode 100644 index 0000000000..d090cb55bf --- /dev/null +++ b/assets/icons/x.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/settings/default.json b/assets/settings/default.json index ee08ce562d..4dd9aa9dbc 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -383,6 +383,16 @@ /// "never" "show": null }, + /// Which files containing diagnostic errors/warnings to mark in the project panel. + /// This setting can take the following three values: + /// + /// 1. Do not mark any files: + /// "off" + /// 2. Only mark files with errors: + /// "errors" + /// 3. Mark files with errors and warnings: + /// "all" + "show_diagnostics": "all", // Settings related to indent guides in the project panel. "indent_guides": { // When to show indent guides in the project panel. diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 6ddf73cb00..1f027ba29e 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -18,6 +18,7 @@ use gpui::{ use language::{ proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, CharKind, Point, SelectionGoal, }; +use lsp::DiagnosticSeverity; use multi_buffer::AnchorRangeExt; use project::{ lsp_store::FormatTrigger, project_settings::ProjectSettings, search::SearchQuery, Item as _, @@ -39,7 +40,7 @@ use std::{ }; use text::{BufferId, Selection}; use theme::{Theme, ThemeSettings}; -use ui::{h_flex, prelude::*, Label}; +use ui::{h_flex, prelude::*, IconDecorationKind, Label}; use util::{paths::PathExt, ResultExt, TryFutureExt}; use workspace::item::{BreadcrumbText, FollowEvent}; use workspace::{ @@ -1515,6 +1516,26 @@ pub fn entry_label_color(selected: bool) -> Color { } } +pub fn entry_diagnostic_aware_icon_name_and_color( + diagnostic_severity: Option, +) -> Option<(IconName, Color)> { + match diagnostic_severity { + Some(DiagnosticSeverity::ERROR) => Some((IconName::X, Color::Error)), + Some(DiagnosticSeverity::WARNING) => Some((IconName::Triangle, Color::Warning)), + _ => None, + } +} + +pub fn entry_diagnostic_aware_icon_decoration_and_color( + diagnostic_severity: Option, +) -> Option<(IconDecorationKind, Color)> { + match diagnostic_severity { + Some(DiagnosticSeverity::ERROR) => Some((IconDecorationKind::X, Color::Error)), + Some(DiagnosticSeverity::WARNING) => Some((IconDecorationKind::Triangle, Color::Warning)), + _ => None, + } +} + pub fn entry_git_aware_label_color( git_status: Option, ignored: bool, diff --git a/crates/project_panel/Cargo.toml b/crates/project_panel/Cargo.toml index 23241a0f88..dbcabc9f83 100644 --- a/crates/project_panel/Cargo.toml +++ b/crates/project_panel/Cargo.toml @@ -37,6 +37,7 @@ util.workspace = true client.workspace = true worktree.workspace = true workspace.workspace = true +language.workspace = true [dev-dependencies] client = { workspace = true, features = ["test-support"] } diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index d8ad0c58a5..cfced9921d 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -1,12 +1,15 @@ mod project_panel_settings; use client::{ErrorCode, ErrorExt}; +use language::DiagnosticSeverity; use settings::{Settings, SettingsStore}; -use ui::{Scrollbar, ScrollbarState}; use db::kvp::KEY_VALUE_STORE; use editor::{ - items::entry_git_aware_label_color, + items::{ + entry_diagnostic_aware_icon_decoration_and_color, + entry_diagnostic_aware_icon_name_and_color, entry_git_aware_label_color, + }, scroll::{Autoscroll, ScrollbarAutoHide}, Editor, EditorEvent, EditorSettings, ShowScrollbar, }; @@ -18,7 +21,7 @@ use git::repository::GitFileStatus; use gpui::{ actions, anchored, deferred, div, impl_actions, point, px, size, uniform_list, Action, AnyElement, AppContext, AssetSource, AsyncWindowContext, Bounds, ClipboardItem, DismissEvent, - Div, DragMoveEvent, EventEmitter, ExternalPaths, FocusHandle, FocusableView, + Div, DragMoveEvent, EventEmitter, ExternalPaths, FocusHandle, FocusableView, Hsla, InteractiveElement, KeyContext, ListHorizontalSizingBehavior, ListSizingBehavior, Model, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Render, ScrollStrategy, Stateful, Styled, Subscription, Task, UniformListScrollHandle, View, ViewContext, @@ -30,7 +33,9 @@ use project::{ relativize_path, Entry, EntryKind, Fs, Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId, }; -use project_panel_settings::{ProjectPanelDockPosition, ProjectPanelSettings, ShowIndentGuides}; +use project_panel_settings::{ + ProjectPanelDockPosition, ProjectPanelSettings, ShowDiagnostics, ShowIndentGuides, +}; use serde::{Deserialize, Serialize}; use smallvec::SmallVec; use std::{ @@ -44,8 +49,9 @@ use std::{ }; use theme::ThemeSettings; use ui::{ - prelude::*, v_flex, ContextMenu, Icon, IndentGuideColors, IndentGuideLayout, KeyBinding, Label, - ListItem, Tooltip, + prelude::*, v_flex, ContextMenu, DecoratedIcon, Icon, IconDecoration, IconDecorationKind, + IndentGuideColors, IndentGuideLayout, KeyBinding, Label, ListItem, Scrollbar, ScrollbarState, + Tooltip, }; use util::{maybe, ResultExt, TryFutureExt}; use workspace::{ @@ -90,6 +96,7 @@ pub struct ProjectPanel { vertical_scrollbar_state: ScrollbarState, horizontal_scrollbar_state: ScrollbarState, hide_scrollbar_task: Option>, + diagnostics: HashMap<(WorktreeId, PathBuf), DiagnosticSeverity>, max_width_item_index: Option, // We keep track of the mouse down state on entries so we don't flash the UI // in case a user clicks to open a file. @@ -133,6 +140,8 @@ struct EntryDetails { is_editing: bool, is_processing: bool, is_cut: bool, + filename_text_color: Color, + diagnostic_severity: Option, git_status: Option, is_private: bool, worktree_id: WorktreeId, @@ -234,6 +243,26 @@ struct DraggedProjectEntryView { selections: Arc>, } +struct ItemColors { + default: Hsla, + hover: Hsla, + drag_over: Hsla, + selected: Hsla, + marked_active: Hsla, +} + +fn get_item_color(cx: &ViewContext) -> ItemColors { + let colors = cx.theme().colors(); + + ItemColors { + default: colors.surface_background, + hover: colors.ghost_element_hover, + drag_over: colors.drop_target_background, + selected: colors.surface_background, + marked_active: colors.ghost_element_selected, + } +} + impl ProjectPanel { fn new(workspace: &mut Workspace, cx: &mut ViewContext) -> View { let project = workspace.project().clone(); @@ -257,6 +286,14 @@ impl ProjectPanel { project::Event::ActivateProjectPanel => { cx.emit(PanelEvent::Activate); } + project::Event::DiskBasedDiagnosticsFinished { .. } + | project::Event::DiagnosticsUpdated { .. } => { + if ProjectPanelSettings::get_global(cx).show_diagnostics != ShowDiagnostics::Off + { + this.update_diagnostics(cx); + cx.notify(); + } + } project::Event::WorktreeRemoved(id) => { this.expanded_dir_ids.remove(id); this.update_visible_entries(None, cx); @@ -302,10 +339,11 @@ impl ProjectPanel { .detach(); let mut project_panel_settings = *ProjectPanelSettings::get_global(cx); - cx.observe_global::(move |_, cx| { + cx.observe_global::(move |this, cx| { let new_settings = *ProjectPanelSettings::get_global(cx); if project_panel_settings != new_settings { project_panel_settings = new_settings; + this.update_diagnostics(cx); cx.notify(); } }) @@ -340,6 +378,7 @@ impl ProjectPanel { horizontal_scrollbar_state: ScrollbarState::new(scroll_handle.clone()) .parent_view(cx.view()), max_width_item_index: None, + diagnostics: Default::default(), scroll_handle, mouse_down: false, }; @@ -456,6 +495,64 @@ impl ProjectPanel { }) } + fn update_diagnostics(&mut self, cx: &mut ViewContext) { + let mut diagnostics: HashMap<(WorktreeId, PathBuf), DiagnosticSeverity> = + Default::default(); + let show_diagnostics_setting = ProjectPanelSettings::get_global(cx).show_diagnostics; + + if show_diagnostics_setting != ShowDiagnostics::Off { + self.project + .read(cx) + .diagnostic_summaries(false, cx) + .filter_map(|(path, _, diagnostic_summary)| { + if diagnostic_summary.error_count > 0 { + Some((path, DiagnosticSeverity::ERROR)) + } else if show_diagnostics_setting == ShowDiagnostics::All + && diagnostic_summary.warning_count > 0 + { + Some((path, DiagnosticSeverity::WARNING)) + } else { + None + } + }) + .for_each(|(project_path, diagnostic_severity)| { + let mut path_buffer = PathBuf::new(); + Self::update_strongest_diagnostic_severity( + &mut diagnostics, + &project_path, + path_buffer.clone(), + diagnostic_severity, + ); + + for component in project_path.path.components() { + path_buffer.push(component); + Self::update_strongest_diagnostic_severity( + &mut diagnostics, + &project_path, + path_buffer.clone(), + diagnostic_severity, + ); + } + }); + } + self.diagnostics = diagnostics; + } + + fn update_strongest_diagnostic_severity( + diagnostics: &mut HashMap<(WorktreeId, PathBuf), DiagnosticSeverity>, + project_path: &ProjectPath, + path_buffer: PathBuf, + diagnostic_severity: DiagnosticSeverity, + ) { + diagnostics + .entry((project_path.worktree_id, path_buffer.clone())) + .and_modify(|strongest_diagnostic_severity| { + *strongest_diagnostic_severity = + std::cmp::min(*strongest_diagnostic_severity, diagnostic_severity); + }) + .or_insert(diagnostic_severity); + } + fn serialize(&mut self, cx: &mut ViewContext) { let width = self.width; self.pending_serialization = cx.background_executor().spawn( @@ -2353,6 +2450,23 @@ impl ProjectPanel { worktree_id: snapshot.id(), entry_id: entry.id, }; + + let is_marked = self.marked_entries.contains(&selection); + + let diagnostic_severity = self + .diagnostics + .get(&(*worktree_id, entry.path.to_path_buf())) + .cloned(); + + let filename_text_color = if entry.kind.is_file() + && diagnostic_severity + .map_or(false, |severity| severity == DiagnosticSeverity::ERROR) + { + Color::Error + } else { + entry_git_aware_label_color(status, entry.is_ignored, is_marked) + }; + let mut details = EntryDetails { filename, icon, @@ -2362,13 +2476,15 @@ impl ProjectPanel { is_ignored: entry.is_ignored, is_expanded, is_selected: self.selection == Some(selection), - is_marked: self.marked_entries.contains(&selection), + is_marked, is_editing: false, is_processing: false, is_cut: self .clipboard .as_ref() .map_or(false, |e| e.is_cut() && e.items().contains(&selection)), + filename_text_color, + diagnostic_severity, git_status: status, is_private: entry.is_private, worktree_id: *worktree_id, @@ -2480,18 +2596,20 @@ impl ProjectPanel { let kind = details.kind; let settings = ProjectPanelSettings::get_global(cx); let show_editor = details.is_editing && !details.is_processing; + let selection = SelectedEntry { worktree_id: details.worktree_id, entry_id, }; + let is_marked = self.marked_entries.contains(&selection); let is_active = self .selection .map_or(false, |selection| selection.entry_id == entry_id); + let width = self.size(cx); - let filename_text_color = - entry_git_aware_label_color(details.git_status, details.is_ignored, is_marked); let file_name = details.filename.clone(); + let mut icon = details.icon.clone(); if settings.file_icons && show_editor && details.kind.is_file() { let filename = self.filename_editor.read(cx).text(cx); @@ -2500,6 +2618,10 @@ impl ProjectPanel { } } + let filename_text_color = details.filename_text_color; + let diagnostic_severity = details.diagnostic_severity; + let item_colors = get_item_color(cx); + let canonical_path = details .canonical_path .as_ref() @@ -2579,9 +2701,7 @@ impl ProjectPanel { selections: selection.marked_selections.clone(), }) }) - .drag_over::(|style, _, cx| { - style.bg(cx.theme().colors().drop_target_background) - }) + .drag_over::(move |style, _, _| style.bg(item_colors.drag_over)) .on_drop(cx.listener(move |this, selections: &DraggedSelection, cx| { this.hover_scroll_task.take(); this.drag_onto(selections, entry_id, kind.is_file(), cx); @@ -2675,12 +2795,60 @@ impl ProjectPanel { ) }) .child(if let Some(icon) = &icon { - h_flex().child(Icon::from_path(icon.to_string()).color(filename_text_color)) + // Check if there's a diagnostic severity and get the decoration color + if let Some((_, decoration_color)) = + entry_diagnostic_aware_icon_decoration_and_color(diagnostic_severity) + { + // Determine if the diagnostic is a warning + let is_warning = diagnostic_severity + .map(|severity| matches!(severity, DiagnosticSeverity::WARNING)) + .unwrap_or(false); + div().child( + DecoratedIcon::new( + Icon::from_path(icon.clone()).color(Color::Muted), + Some( + IconDecoration::new( + if kind.is_file() { + if is_warning { + IconDecorationKind::Triangle + } else { + IconDecorationKind::X + } + } else { + IconDecorationKind::Dot + }, + if is_marked || is_active { + item_colors.selected + } else { + item_colors.default + }, + cx, + ) + .color(decoration_color.color(cx)) + .position(Point { + x: px(-2.), + y: px(-2.), + }), + ), + ) + .into_any_element(), + ) + } else { + h_flex().child(Icon::from_path(icon.to_string()).color(Color::Muted)) + } } else { - h_flex() - .size(IconSize::default().rems()) - .invisible() - .flex_none() + if let Some((icon_name, color)) = + entry_diagnostic_aware_icon_name_and_color(diagnostic_severity) + { + h_flex() + .size(IconSize::default().rems()) + .child(Icon::new(icon_name).color(color).size(IconSize::Small)) + } else { + h_flex() + .size(IconSize::default().rems()) + .invisible() + .flex_none() + } }) .child( if let (Some(editor), true) = (Some(&self.filename_editor), show_editor) { @@ -2770,14 +2938,14 @@ impl ProjectPanel { if is_active { style } else { - let hover_color = cx.theme().colors().ghost_element_hover; - style.bg(hover_color).border_color(hover_color) + style.bg(item_colors.hover).border_color(item_colors.hover) } }) .when(is_marked || is_active, |this| { - let colors = cx.theme().colors(); - this.when(is_marked, |this| this.bg(colors.ghost_element_selected)) - .border_color(colors.ghost_element_selected) + this.when(is_marked, |this| { + this.bg(item_colors.marked_active) + .border_color(item_colors.marked_active) + }) }) .when( !self.mouse_down && is_active && self.focus_handle.contains_focused(cx), diff --git a/crates/project_panel/src/project_panel_settings.rs b/crates/project_panel/src/project_panel_settings.rs index c2b84bd1e7..92ea7cea2d 100644 --- a/crates/project_panel/src/project_panel_settings.rs +++ b/crates/project_panel/src/project_panel_settings.rs @@ -31,6 +31,7 @@ pub struct ProjectPanelSettings { pub auto_reveal_entries: bool, pub auto_fold_dirs: bool, pub scrollbar: ScrollbarSettings, + pub show_diagnostics: ShowDiagnostics, } #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] @@ -60,6 +61,21 @@ pub struct ScrollbarSettingsContent { pub show: Option>, } +/// Whether to indicate diagnostic errors and/or warnings in project panel items. +/// +/// Default: all +#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum ShowDiagnostics { + /// Never mark the diagnostic errors/warnings in the project panel. + Off, + /// Mark files containing only diagnostic errors in the project panel. + Errors, + #[default] + /// Mark files containing diagnostic errors or warnings in the project panel. + All, +} + #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)] pub struct ProjectPanelSettingsContent { /// Whether to show the project panel button in the status bar. @@ -103,6 +119,10 @@ pub struct ProjectPanelSettingsContent { pub auto_fold_dirs: Option, /// Scrollbar-related settings pub scrollbar: Option, + /// Which files containing diagnostic errors/warnings to mark in the project panel. + /// + /// Default: all + pub show_diagnostics: Option, /// Settings related to indent guides in the project panel. pub indent_guides: Option, } diff --git a/crates/ui/src/components/icon.rs b/crates/ui/src/components/icon.rs index 4f9317203d..161f4c60b7 100644 --- a/crates/ui/src/components/icon.rs +++ b/crates/ui/src/components/icon.rs @@ -277,6 +277,7 @@ pub enum IconName { Terminal, Trash, TrashAlt, + Triangle, TriangleRight, Undo, Unpin, @@ -290,6 +291,7 @@ pub enum IconName { ZedAssistant, ZedAssistantFilled, ZedXCopilot, + X, } impl From for Icon { diff --git a/crates/ui/src/styles/color.rs b/crates/ui/src/styles/color.rs index fe8de2ff73..b9fa96f64c 100644 --- a/crates/ui/src/styles/color.rs +++ b/crates/ui/src/styles/color.rs @@ -2,7 +2,7 @@ use gpui::{Hsla, WindowContext}; use theme::ActiveTheme; /// Sets a color that has a consistent meaning across all themes. -#[derive(Debug, Default, PartialEq, Copy, Clone)] +#[derive(Debug, Default, Eq, PartialEq, Copy, Clone)] pub enum Color { #[default] /// The default text color. Might be known as "foreground" or "primary" in