Skip to content

Commit 6ebd6c2

Browse files
authored
Show error and warning indicators in tabs (#21383)
Closes #21179 Release Notes: - Add setting to display error and warning indicators in tabs. <img width="454" alt="demo_with_icons" src="https://github.com/user-attachments/assets/6002b4d4-dca8-4e2a-842d-1df3e281fcd2"> <img width="454" alt="demo_without_icons" src="https://github.com/user-attachments/assets/df4b67bd-1a6c-4354-847e-d7fea95c1b7e">
1 parent 92dea06 commit 6ebd6c2

File tree

5 files changed

+131
-38
lines changed

5 files changed

+131
-38
lines changed

Cargo.lock

Lines changed: 0 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

assets/settings/default.json

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -567,7 +567,17 @@
567567
// "History"
568568
// 2. Activate the neighbour tab (prefers the right one, if present)
569569
// "Neighbour"
570-
"activate_on_close": "history"
570+
"activate_on_close": "history",
571+
/// Which files containing diagnostic errors/warnings to mark in the tabs.
572+
/// This setting can take the following three values:
573+
///
574+
/// 1. Do not mark any files:
575+
/// "off"
576+
/// 2. Only mark files with errors:
577+
/// "errors"
578+
/// 3. Mark files with errors and warnings:
579+
/// "all"
580+
"show_diagnostics": "all"
571581
},
572582
// Settings related to preview tabs.
573583
"preview_tabs": {

crates/workspace/Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ db.workspace = true
3838
derive_more.workspace = true
3939
fs.workspace = true
4040
futures.workspace = true
41-
git.workspace = true
4241
gpui.workspace = true
4342
http_client.workspace = true
4443
itertools.workspace = true

crates/workspace/src/item.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ pub struct ItemSettings {
4242
pub close_position: ClosePosition,
4343
pub activate_on_close: ActivateOnClose,
4444
pub file_icons: bool,
45+
pub show_diagnostics: ShowDiagnostics,
4546
pub always_show_close_button: bool,
4647
}
4748

@@ -60,6 +61,15 @@ pub enum ClosePosition {
6061
Right,
6162
}
6263

64+
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
65+
#[serde(rename_all = "snake_case")]
66+
pub enum ShowDiagnostics {
67+
Off,
68+
Errors,
69+
#[default]
70+
All,
71+
}
72+
6373
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
6474
#[serde(rename_all = "lowercase")]
6575
pub enum ActivateOnClose {
@@ -86,6 +96,11 @@ pub struct ItemSettingsContent {
8696
///
8797
/// Default: history
8898
pub activate_on_close: Option<ActivateOnClose>,
99+
/// Which files containing diagnostic errors/warnings to mark in the tabs.
100+
/// This setting can take the following three values:
101+
///
102+
/// Default: all
103+
show_diagnostics: Option<ShowDiagnostics>,
89104
/// Whether to always show the close button on tabs.
90105
///
91106
/// Default: false

crates/workspace/src/pane.rs

Lines changed: 105 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use crate::{
22
item::{
33
ActivateOnClose, ClosePosition, Item, ItemHandle, ItemSettings, PreviewTabsSettings,
4-
TabContentParams, WeakItemHandle,
4+
ShowDiagnostics, TabContentParams, WeakItemHandle,
55
},
66
move_item,
77
notifications::NotifyResultExt,
@@ -13,7 +13,6 @@ use crate::{
1313
use anyhow::Result;
1414
use collections::{BTreeSet, HashMap, HashSet, VecDeque};
1515
use futures::{stream::FuturesUnordered, StreamExt};
16-
use git::repository::GitFileStatus;
1716
use gpui::{
1817
actions, anchored, deferred, impl_actions, prelude::*, Action, AnchorCorner, AnyElement,
1918
AppContext, AsyncWindowContext, ClickEvent, ClipboardItem, Div, DragMoveEvent, EntityId,
@@ -23,6 +22,7 @@ use gpui::{
2322
WindowContext,
2423
};
2524
use itertools::Itertools;
25+
use language::DiagnosticSeverity;
2626
use parking_lot::Mutex;
2727
use project::{Project, ProjectEntryId, ProjectPath, WorktreeId};
2828
use serde::Deserialize;
@@ -39,10 +39,10 @@ use std::{
3939
},
4040
};
4141
use theme::ThemeSettings;
42-
4342
use ui::{
44-
prelude::*, right_click_menu, ButtonSize, Color, IconButton, IconButtonShape, IconName,
45-
IconSize, Indicator, Label, PopoverMenu, PopoverMenuHandle, Tab, TabBar, TabPosition, Tooltip,
43+
prelude::*, right_click_menu, ButtonSize, Color, DecoratedIcon, IconButton, IconButtonShape,
44+
IconDecoration, IconDecorationKind, IconName, IconSize, Indicator, Label, PopoverMenu,
45+
PopoverMenuHandle, Tab, TabBar, TabPosition, Tooltip,
4646
};
4747
use ui::{v_flex, ContextMenu};
4848
use util::{debug_panic, maybe, truncate_and_remove_front, ResultExt};
@@ -305,6 +305,7 @@ pub struct Pane {
305305
pub new_item_context_menu_handle: PopoverMenuHandle<ContextMenu>,
306306
pub split_item_context_menu_handle: PopoverMenuHandle<ContextMenu>,
307307
pinned_tab_count: usize,
308+
diagnostics: HashMap<ProjectPath, DiagnosticSeverity>,
308309
}
309310

310311
pub struct ActivationHistoryEntry {
@@ -381,6 +382,7 @@ impl Pane {
381382
cx.on_focus_in(&focus_handle, Pane::focus_in),
382383
cx.on_focus_out(&focus_handle, Pane::focus_out),
383384
cx.observe_global::<SettingsStore>(Self::settings_changed),
385+
cx.subscribe(&project, Self::project_events),
384386
];
385387

386388
let handle = cx.view().downgrade();
@@ -504,6 +506,7 @@ impl Pane {
504506
split_item_context_menu_handle: Default::default(),
505507
new_item_context_menu_handle: Default::default(),
506508
pinned_tab_count: 0,
509+
diagnostics: Default::default(),
507510
}
508511
}
509512

@@ -598,13 +601,55 @@ impl Pane {
598601
cx.notify();
599602
}
600603

604+
fn project_events(
605+
this: &mut Pane,
606+
_project: Model<Project>,
607+
event: &project::Event,
608+
cx: &mut ViewContext<Self>,
609+
) {
610+
match event {
611+
project::Event::DiskBasedDiagnosticsFinished { .. }
612+
| project::Event::DiagnosticsUpdated { .. } => {
613+
if ItemSettings::get_global(cx).show_diagnostics != ShowDiagnostics::Off {
614+
this.update_diagnostics(cx);
615+
cx.notify();
616+
}
617+
}
618+
_ => {}
619+
}
620+
}
621+
622+
fn update_diagnostics(&mut self, cx: &mut ViewContext<Self>) {
623+
let show_diagnostics = ItemSettings::get_global(cx).show_diagnostics;
624+
self.diagnostics = if show_diagnostics != ShowDiagnostics::Off {
625+
self.project
626+
.read(cx)
627+
.diagnostic_summaries(false, cx)
628+
.filter_map(|(project_path, _, diagnostic_summary)| {
629+
if diagnostic_summary.error_count > 0 {
630+
Some((project_path, DiagnosticSeverity::ERROR))
631+
} else if diagnostic_summary.warning_count > 0
632+
&& show_diagnostics != ShowDiagnostics::Errors
633+
{
634+
Some((project_path, DiagnosticSeverity::WARNING))
635+
} else {
636+
None
637+
}
638+
})
639+
.collect::<HashMap<_, _>>()
640+
} else {
641+
Default::default()
642+
}
643+
}
644+
601645
fn settings_changed(&mut self, cx: &mut ViewContext<Self>) {
602646
if let Some(display_nav_history_buttons) = self.display_nav_history_buttons.as_mut() {
603647
*display_nav_history_buttons = TabBarSettings::get_global(cx).show_nav_history_buttons;
604648
}
605649
if !PreviewTabsSettings::get_global(cx).enabled {
606650
self.preview_item_id = None;
607651
}
652+
self.update_diagnostics(cx);
608653
cx.notify();
609654
}
610655

@@ -1839,23 +1884,6 @@ impl Pane {
18391884
}
18401885
}
18411886

1842-
pub fn git_aware_icon_color(
1843-
git_status: Option<GitFileStatus>,
1844-
ignored: bool,
1845-
selected: bool,
1846-
) -> Color {
1847-
if ignored {
1848-
Color::Ignored
1849-
} else {
1850-
match git_status {
1851-
Some(GitFileStatus::Added) => Color::Created,
1852-
Some(GitFileStatus::Modified) => Color::Modified,
1853-
Some(GitFileStatus::Conflict) => Color::Conflict,
1854-
None => Self::icon_color(selected),
1855-
}
1856-
}
1857-
}
1858-
18591887
fn toggle_pin_tab(&mut self, _: &TogglePinTab, cx: &mut ViewContext<'_, Self>) {
18601888
if self.items.is_empty() {
18611889
return;
@@ -1919,8 +1947,6 @@ impl Pane {
19191947
focus_handle: &FocusHandle,
19201948
cx: &mut ViewContext<'_, Pane>,
19211949
) -> impl IntoElement {
1922-
let project_path = item.project_path(cx);
1923-
19241950
let is_active = ix == self.active_item_index;
19251951
let is_preview = self
19261952
.preview_item_id
@@ -1936,19 +1962,57 @@ impl Pane {
19361962
cx,
19371963
);
19381964

1939-
let icon_color = if ItemSettings::get_global(cx).git_status {
1940-
project_path
1941-
.as_ref()
1942-
.and_then(|path| self.project.read(cx).entry_for_path(path, cx))
1943-
.map(|entry| {
1944-
Self::git_aware_icon_color(entry.git_status, entry.is_ignored, is_active)
1945-
})
1946-
.unwrap_or_else(|| Self::icon_color(is_active))
1965+
let item_diagnostic = item
1966+
.project_path(cx)
1967+
.map_or(None, |project_path| self.diagnostics.get(&project_path));
1968+
1969+
let decorated_icon = item_diagnostic.map_or(None, |diagnostic| {
1970+
let icon = match item.tab_icon(cx) {
1971+
Some(icon) => icon,
1972+
None => return None,
1973+
};
1974+
1975+
let knockout_item_color = if is_active {
1976+
cx.theme().colors().tab_active_background
1977+
} else {
1978+
cx.theme().colors().tab_bar_background
1979+
};
1980+
1981+
let (icon_decoration, icon_color) = if matches!(diagnostic, &DiagnosticSeverity::ERROR)
1982+
{
1983+
(IconDecorationKind::X, Color::Error)
1984+
} else {
1985+
(IconDecorationKind::Triangle, Color::Warning)
1986+
};
1987+
1988+
Some(DecoratedIcon::new(
1989+
icon.size(IconSize::Small).color(Color::Muted),
1990+
Some(
1991+
IconDecoration::new(icon_decoration, knockout_item_color, cx)
1992+
.color(icon_color.color(cx))
1993+
.position(Point {
1994+
x: px(-2.),
1995+
y: px(-2.),
1996+
}),
1997+
),
1998+
))
1999+
});
2000+
2001+
let icon = if decorated_icon.is_none() {
2002+
match item_diagnostic {
2003+
Some(&DiagnosticSeverity::ERROR) => {
2004+
Some(Icon::new(IconName::X).color(Color::Error))
2005+
}
2006+
Some(&DiagnosticSeverity::WARNING) => {
2007+
Some(Icon::new(IconName::Triangle).color(Color::Warning))
2008+
}
2009+
_ => item.tab_icon(cx).map(|icon| icon.color(Color::Muted)),
2010+
}
2011+
.map(|icon| icon.size(IconSize::Small))
19472012
} else {
1948-
Self::icon_color(is_active)
2013+
None
19492014
};
19502015

1951-
let icon = item.tab_icon(cx);
19522016
let settings = ItemSettings::get_global(cx);
19532017
let close_side = &settings.close_position;
19542018
let always_show_close_button = settings.always_show_close_button;
@@ -2078,7 +2142,13 @@ impl Pane {
20782142
.child(
20792143
h_flex()
20802144
.gap_1()
2081-
.children(icon.map(|icon| icon.size(IconSize::Small).color(icon_color)))
2145+
.child(if let Some(decorated_icon) = decorated_icon {
2146+
div().child(decorated_icon.into_any_element())
2147+
} else if let Some(icon) = icon {
2148+
div().child(icon.into_any_element())
2149+
} else {
2150+
div()
2151+
})
20822152
.child(label),
20832153
);
20842154

0 commit comments

Comments
 (0)