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:
parent
a7eb3a9b9f
commit
0a9c78a58d
12 changed files with 262 additions and 30 deletions
|
@ -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"] }
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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>,
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue