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