Show error and warning indicators in project panel items (#18182)

Closes #5016

Release Notes:

- Add setting to display error and warning indicators in project panel
items.


https://github.com/user-attachments/assets/8f8031e6-ca47-42bf-a7eb-718eb1067f36

---------

Co-authored-by: Thorsten Ball <mrnugget@gmail.com>
Co-authored-by: Danilo Leal <67129314+danilo-leal@users.noreply.github.com>
This commit is contained in:
Nils Koch 2024-11-12 22:58:59 +01:00 committed by GitHub
parent a7eb3a9b9f
commit 0a9c78a58d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 262 additions and 30 deletions

View File

@ -1,3 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.4662 14.9152C13.5801 15.0291 13.7648 15.0291 13.8787 14.9152L14.9145 13.8793C15.0284 13.7654 15.0284 13.5807 14.9145 13.4667L12.9483 11.5004L14.9145 9.53392C15.0285 9.42004 15.0285 9.23533 14.9145 9.12137L13.8787 8.08547C13.7648 7.97154 13.5801 7.97154 13.4662 8.08547L11.5 10.0519L9.53376 8.08545C9.41988 7.97152 9.23517 7.97152 9.12124 8.08545L8.08543 9.12136C7.97152 9.23533 7.97152 9.42004 8.08543 9.53392L10.0517 11.5004L8.08545 13.4667C7.97155 13.5807 7.97155 13.7654 8.08545 13.8793L9.12126 14.9152C9.23517 15.0292 9.41988 15.0292 9.53376 14.9152L11.5 12.9489L13.4662 14.9152Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 756 B

View File

@ -1,3 +1,10 @@
<svg width="11" height="11" viewBox="0 0 11 11" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.364 7.6025L1.64681 8.75H3H8H9.35319L8.636 7.6025L6.136 3.6025L5.5 2.5849L4.864 3.6025L2.364 7.6025Z" stroke="black" stroke-width="1.5"/>
<g clip-path="url(#clip0_2080_948)">
<path d="M1.52492 8.72287L1.04243 9.55H2H9H9.95757L9.47508 8.72287L5.97508 2.72287L5.5 1.90845L5.02492 2.72287L1.52492 8.72287Z" stroke="black" stroke-width="1.1"/>
</g>
<defs>
<clipPath id="clip0_2080_948">
<rect width="11" height="11" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 252 B

After

Width:  |  Height:  |  Size: 412 B

View File

@ -1,3 +1,3 @@
<svg width="11" height="11" viewBox="0 0 11 11" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 8H8L5.5 4L3 8Z" fill="black"/>
<path d="M2 9H9L5.5 3L2 9Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 146 B

After

Width:  |  Height:  |  Size: 146 B

View File

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 4L4 12H12L8 4Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 155 B

3
assets/icons/x.svg Normal file
View File

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 4L12 12M12 4L4 12" stroke="currentColor" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 177 B

View File

@ -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.

View File

@ -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<DiagnosticSeverity>,
) -> 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<DiagnosticSeverity>,
) -> 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<GitFileStatus>,
ignored: bool,

View File

@ -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"] }

View File

@ -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<Task<()>>,
diagnostics: HashMap<(WorktreeId, PathBuf), DiagnosticSeverity>,
max_width_item_index: Option<usize>,
// 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<DiagnosticSeverity>,
git_status: Option<GitFileStatus>,
is_private: bool,
worktree_id: WorktreeId,
@ -234,6 +243,26 @@ struct DraggedProjectEntryView {
selections: Arc<BTreeSet<SelectedEntry>>,
}
struct ItemColors {
default: Hsla,
hover: Hsla,
drag_over: Hsla,
selected: Hsla,
marked_active: Hsla,
}
fn get_item_color(cx: &ViewContext<ProjectPanel>) -> 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<Workspace>) -> View<Self> {
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::<SettingsStore>(move |_, cx| {
cx.observe_global::<SettingsStore>(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<Self>) {
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<Self>) {
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::<DraggedSelection>(|style, _, cx| {
style.bg(cx.theme().colors().drop_target_background)
})
.drag_over::<DraggedSelection>(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),

View File

@ -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<Option<ShowScrollbar>>,
}
/// 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<bool>,
/// Scrollbar-related settings
pub scrollbar: Option<ScrollbarSettingsContent>,
/// Which files containing diagnostic errors/warnings to mark in the project panel.
///
/// Default: all
pub show_diagnostics: Option<ShowDiagnostics>,
/// Settings related to indent guides in the project panel.
pub indent_guides: Option<IndentGuidesSettingsContent>,
}

View File

@ -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<IconName> for Icon {

View File

@ -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