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:
<img width="522" height="785" alt="image"
src="https://github.com/user-attachments/assets/2da62580-86fe-480b-9b57-ff137ea42285"
/>


<img width="884" height="580" alt="image"
src="https://github.com/user-attachments/assets/198e9a45-dacd-4f1e-a66c-f2b84fd4db63"
/>

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 <heysmitbarmase@gmail.com>
This commit is contained in:
Davide ScacciaandSmit Barmase authored and GitHub committed 2026-02-25 20:39:30 +05:30
1 parent f8ef6c8c20
commit f765a0efcb
6 files changed
+136 -20

No files matched your search

+2
View File
@@ -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;
+103 -16
View File
@@ -146,6 +146,7 @@ pub struct ProjectPanel {
width: Option<Pixels>,
pending_serialization: Task<Option<()>>,
diagnostics: HashMap<(WorktreeId, Arc<RelPath>), DiagnosticSeverity>,
diagnostic_counts: HashMap<(WorktreeId, Arc<RelPath>), 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<SelectedEntry>),
}
#[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<StickyDetails>,
filename_text_color: Color,
diagnostic_severity: Option<DiagnosticSeverity>,
diagnostic_count: Option<DiagnosticCount>,
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::<AnyElement>(
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::<AnyElement>(
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,
@@ -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(),
}
}
}
+1
View File
@@ -801,6 +801,7 @@ impl VsCodeSettings {
starts_open: None,
sticky_scroll: None,
auto_open: None,
diagnostic_badges: None,
};
if let (Some(false), Some(false)) = (
+4
View File
@@ -739,6 +739,10 @@ pub struct ProjectPanelSettingsContent {
///
/// Default: directories_first
pub sort_mode: Option<ProjectPanelSortMode>,
/// Whether to show error and warning count badges next to file names in the project panel.
///
/// Default: true
pub diagnostic_badges: Option<bool>,
}
#[derive(
+23 -1
View File
@@ -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.",