From 94d66ff137e4b417dd81d12d003872d92e5abfa2 Mon Sep 17 00:00:00 2001 From: Davide Scaccia Date: Wed, 25 Feb 2026 16:09:30 +0100 Subject: [PATCH] project_panel: Add diagnostic count badges (#49802) Discussed in #6668 specifically this comment from @zackangelo: > The biggest thing keeping me from using Zed as a daily driver is error indication in the project panel. When I'm making big project-wide changes I can't clearly see which files have errors (in editors like VSCode the filenames turn red). > VSCode seems to use a letter on the right gutter to indicate git status and a number next to it to indicate diagnostic status. The color indicates either. This PR implements that, I added an opt-in `diagnostic_badges` setting (default is false) that shows error and warning counts as colored labels on the right side of each project panel entry. Counts bubble up to parent directories. When `diagnostic_badges` is enabled, diagnostic severity takes priority over git status for entry text color. Since warnings and git-modified share the same yellow, git status with this option on is readable through the file icon decoration and the absence of a number badge on the right. Example: image image Release Notes: - Added diagnostic count badges to the project panel, displaying error and warning counts next to file names. You can modify this setting using the `diagnostic_badges` option, which is enabled by default. --------- Co-authored-by: Smit Barmase --- assets/settings/default.json | 2 + crates/project_panel/src/project_panel.rs | 119 +++++++++++++++--- .../src/project_panel_settings.rs | 6 +- crates/settings/src/vscode_import.rs | 1 + crates/settings_content/src/workspace.rs | 4 + crates/settings_ui/src/page_data.rs | 24 +++- 6 files changed, 136 insertions(+), 20 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 0a57472a5f21657cab89bd3e6f64e259a4a220e6..9dc077fb29458089e68061d5bd121ed9770108d7 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -799,6 +799,8 @@ // 3. Show files first, then directories: // "files_first" "sort_mode": "directories_first", + // Whether to show error and warning count badges next to file names in the project panel. + "diagnostic_badges": true, // Whether to enable drag-and-drop operations in the project panel. "drag_and_drop": true, // Whether to hide the root entry when only one folder is open in the window; diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 6c0c10c0715a35de25efaa7f6fddbcb5c0257934..e11c04755e59b7d62ea16340d6ed23bdb36daf6d 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -146,6 +146,7 @@ pub struct ProjectPanel { width: Option, pending_serialization: Task>, diagnostics: HashMap<(WorktreeId, Arc), DiagnosticSeverity>, + diagnostic_counts: HashMap<(WorktreeId, Arc), DiagnosticCount>, diagnostic_summary_update: Task<()>, // 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. @@ -232,6 +233,30 @@ enum ClipboardEntry { Cut(BTreeSet), } +#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)] +struct DiagnosticCount { + error_count: usize, + warning_count: usize, +} + +impl DiagnosticCount { + fn capped_error_count(&self) -> String { + Self::capped_count(self.error_count) + } + + fn capped_warning_count(&self) -> String { + Self::capped_count(self.warning_count) + } + + fn capped_count(count: usize) -> String { + if count > 99 { + "99+".to_string() + } else { + count.to_string() + } + } +} + #[derive(Debug, PartialEq, Eq, Clone)] struct EntryDetails { filename: String, @@ -249,6 +274,7 @@ struct EntryDetails { sticky: Option, filename_text_color: Color, diagnostic_severity: Option, + diagnostic_count: Option, git_status: GitSummary, is_private: bool, worktree_id: WorktreeId, @@ -847,6 +873,7 @@ impl ProjectPanel { width: None, pending_serialization: Task::ready(None), diagnostics: Default::default(), + diagnostic_counts: Default::default(), diagnostic_summary_update: Task::ready(()), scroll_handle, mouse_down: false, @@ -1029,6 +1056,26 @@ impl ProjectPanel { }); } self.diagnostics = diagnostics; + + let diagnostic_badges = ProjectPanelSettings::get_global(cx).diagnostic_badges; + self.diagnostic_counts = + if diagnostic_badges && show_diagnostics_setting != ShowDiagnostics::Off { + self.project.read(cx).diagnostic_summaries(false, cx).fold( + HashMap::default(), + |mut counts, (project_path, _, summary)| { + let entry = counts + .entry((project_path.worktree_id, project_path.path)) + .or_default(); + entry.error_count += summary.error_count; + if show_diagnostics_setting == ShowDiagnostics::All { + entry.warning_count += summary.warning_count; + } + counts + }, + ) + } else { + Default::default() + }; } fn update_strongest_diagnostic_severity( @@ -5044,6 +5091,7 @@ impl ProjectPanel { let filename_text_color = details.filename_text_color; let diagnostic_severity = details.diagnostic_severity; + let diagnostic_count = details.diagnostic_count; let item_colors = get_item_color(is_sticky, cx); let canonical_path = details @@ -5482,22 +5530,55 @@ impl ProjectPanel { ProjectPanelEntrySpacing::Standard => ListItemSpacing::ExtraDense, }) .selectable(false) - .when_some(canonical_path, |this, path| { - this.end_slot::( - div() - .id("symlink_icon") - .pr_3() - .tooltip(move |_window, cx| { - Tooltip::with_meta(path.to_string(), None, "Symbolic Link", cx) - }) - .child( - Icon::new(IconName::ArrowUpRight) - .size(IconSize::Indicator) - .color(filename_text_color), - ) - .into_any_element(), - ) - }) + .when( + canonical_path.is_some() || diagnostic_count.is_some(), + |this| { + let symlink_element = canonical_path.map(|path| { + div() + .id("symlink_icon") + .tooltip(move |_window, cx| { + Tooltip::with_meta( + path.to_string(), + None, + "Symbolic Link", + cx, + ) + }) + .child( + Icon::new(IconName::ArrowUpRight) + .size(IconSize::Indicator) + .color(filename_text_color), + ) + }); + this.end_slot::( + h_flex() + .gap_1() + .flex_none() + .pr_3() + .when_some(diagnostic_count, |this, count| { + this.when(count.error_count > 0, |this| { + this.child( + Label::new(count.capped_error_count()) + .size(LabelSize::Small) + .color(Color::Error), + ) + }) + .when( + count.warning_count > 0, + |this| { + this.child( + Label::new(count.capped_warning_count()) + .size(LabelSize::Small) + .color(Color::Warning), + ) + }, + ) + }) + .when_some(symlink_element, |this, el| this.child(el)) + .into_any_element(), + ) + }, + ) .child(if let Some(icon) = &icon { if let Some((_, decoration_color)) = entry_diagnostic_aware_icon_decoration_and_color(diagnostic_severity) @@ -5907,6 +5988,11 @@ impl ProjectPanel { .get(&(worktree_id, entry.path.clone())) .cloned(); + let diagnostic_count = self + .diagnostic_counts + .get(&(worktree_id, entry.path.clone())) + .copied(); + let filename_text_color = entry_git_aware_label_color(git_status, entry.is_ignored, is_marked); @@ -5931,6 +6017,7 @@ impl ProjectPanel { sticky, filename_text_color, diagnostic_severity, + diagnostic_count, git_status, is_private: entry.is_private, worktree_id, diff --git a/crates/project_panel/src/project_panel_settings.rs b/crates/project_panel/src/project_panel_settings.rs index 6b6b7a377276a9fb8b812e495a07a6c4c7aac15e..0d703c55c06dfff2976fe59f6e030ad9eb1d758b 100644 --- a/crates/project_panel/src/project_panel_settings.rs +++ b/crates/project_panel/src/project_panel_settings.rs @@ -35,6 +35,7 @@ pub struct ProjectPanelSettings { pub drag_and_drop: bool, pub auto_open: AutoOpenSettings, pub sort_mode: ProjectPanelSortMode, + pub diagnostic_badges: bool, } #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] @@ -125,9 +126,8 @@ impl Settings for ProjectPanelSettings { on_drop: auto_open.on_drop.unwrap(), } }, - sort_mode: project_panel - .sort_mode - .unwrap_or(ProjectPanelSortMode::DirectoriesFirst), + sort_mode: project_panel.sort_mode.unwrap(), + diagnostic_badges: project_panel.diagnostic_badges.unwrap(), } } } diff --git a/crates/settings/src/vscode_import.rs b/crates/settings/src/vscode_import.rs index d0643be3bbee82be02c9c461a5f18ba62893a3cd..8a5a497d265c02787d6944915c0dba56e2381a79 100644 --- a/crates/settings/src/vscode_import.rs +++ b/crates/settings/src/vscode_import.rs @@ -801,6 +801,7 @@ impl VsCodeSettings { starts_open: None, sticky_scroll: None, auto_open: None, + diagnostic_badges: None, }; if let (Some(false), Some(false)) = ( diff --git a/crates/settings_content/src/workspace.rs b/crates/settings_content/src/workspace.rs index 3778ccc0373f4b937a08e3a435de40ad6a6d2cff..7262a83b384665b0bcd868bf14dbfaa2928a35c1 100644 --- a/crates/settings_content/src/workspace.rs +++ b/crates/settings_content/src/workspace.rs @@ -739,6 +739,10 @@ pub struct ProjectPanelSettingsContent { /// /// Default: directories_first pub sort_mode: Option, + /// Whether to show error and warning count badges next to file names in the project panel. + /// + /// Default: true + pub diagnostic_badges: Option, } #[derive( diff --git a/crates/settings_ui/src/page_data.rs b/crates/settings_ui/src/page_data.rs index 738eff917bc57a7a2543f9c31494af02883299d1..40bc8705920e5d30d69a22cf8967a8931181db9b 100644 --- a/crates/settings_ui/src/page_data.rs +++ b/crates/settings_ui/src/page_data.rs @@ -4256,7 +4256,7 @@ fn window_and_layout_page() -> SettingsPage { } fn panels_page() -> SettingsPage { - fn project_panel_section() -> [SettingsPageItem; 21] { + fn project_panel_section() -> [SettingsPageItem; 22] { [ SettingsPageItem::SectionHeader("Project Panel"), SettingsPageItem::SettingItem(SettingItem { @@ -4556,6 +4556,28 @@ fn panels_page() -> SettingsPage { metadata: None, files: USER, }), + SettingsPageItem::SettingItem(SettingItem { + title: "Diagnostic Badges", + description: "Show error and warning count badges next to file names in the project panel.", + field: Box::new(SettingField { + json_path: Some("project_panel.diagnostic_badges"), + pick: |settings_content| { + settings_content + .project_panel + .as_ref()? + .diagnostic_badges + .as_ref() + }, + write: |settings_content, value| { + settings_content + .project_panel + .get_or_insert_default() + .diagnostic_badges = value; + }, + }), + metadata: None, + files: USER, + }), SettingsPageItem::SettingItem(SettingItem { title: "Sticky Scroll", description: "Whether to stick parent directories at top of the project panel.",