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">
This commit is contained in:
Nils Koch 2024-12-05 15:43:04 +01:00 committed by GitHub
parent 92dea066dd
commit 6ebd6c2893
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 131 additions and 38 deletions

1
Cargo.lock generated
View file

@ -15195,7 +15195,6 @@ dependencies = [
"env_logger 0.11.5", "env_logger 0.11.5",
"fs", "fs",
"futures 0.3.31", "futures 0.3.31",
"git",
"gpui", "gpui",
"http_client", "http_client",
"itertools 0.13.0", "itertools 0.13.0",

View file

@ -567,7 +567,17 @@
// "History" // "History"
// 2. Activate the neighbour tab (prefers the right one, if present) // 2. Activate the neighbour tab (prefers the right one, if present)
// "Neighbour" // "Neighbour"
"activate_on_close": "history" "activate_on_close": "history",
/// Which files containing diagnostic errors/warnings to mark in the tabs.
/// 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 preview tabs. // Settings related to preview tabs.
"preview_tabs": { "preview_tabs": {

View file

@ -38,7 +38,6 @@ db.workspace = true
derive_more.workspace = true derive_more.workspace = true
fs.workspace = true fs.workspace = true
futures.workspace = true futures.workspace = true
git.workspace = true
gpui.workspace = true gpui.workspace = true
http_client.workspace = true http_client.workspace = true
itertools.workspace = true itertools.workspace = true

View file

@ -42,6 +42,7 @@ pub struct ItemSettings {
pub close_position: ClosePosition, pub close_position: ClosePosition,
pub activate_on_close: ActivateOnClose, pub activate_on_close: ActivateOnClose,
pub file_icons: bool, pub file_icons: bool,
pub show_diagnostics: ShowDiagnostics,
pub always_show_close_button: bool, pub always_show_close_button: bool,
} }
@ -60,6 +61,15 @@ pub enum ClosePosition {
Right, Right,
} }
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ShowDiagnostics {
Off,
Errors,
#[default]
All,
}
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)] #[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
pub enum ActivateOnClose { pub enum ActivateOnClose {
@ -86,6 +96,11 @@ pub struct ItemSettingsContent {
/// ///
/// Default: history /// Default: history
pub activate_on_close: Option<ActivateOnClose>, pub activate_on_close: Option<ActivateOnClose>,
/// Which files containing diagnostic errors/warnings to mark in the tabs.
/// This setting can take the following three values:
///
/// Default: all
show_diagnostics: Option<ShowDiagnostics>,
/// Whether to always show the close button on tabs. /// Whether to always show the close button on tabs.
/// ///
/// Default: false /// Default: false

View file

@ -1,7 +1,7 @@
use crate::{ use crate::{
item::{ item::{
ActivateOnClose, ClosePosition, Item, ItemHandle, ItemSettings, PreviewTabsSettings, ActivateOnClose, ClosePosition, Item, ItemHandle, ItemSettings, PreviewTabsSettings,
TabContentParams, WeakItemHandle, ShowDiagnostics, TabContentParams, WeakItemHandle,
}, },
move_item, move_item,
notifications::NotifyResultExt, notifications::NotifyResultExt,
@ -13,7 +13,6 @@ use crate::{
use anyhow::Result; use anyhow::Result;
use collections::{BTreeSet, HashMap, HashSet, VecDeque}; use collections::{BTreeSet, HashMap, HashSet, VecDeque};
use futures::{stream::FuturesUnordered, StreamExt}; use futures::{stream::FuturesUnordered, StreamExt};
use git::repository::GitFileStatus;
use gpui::{ use gpui::{
actions, anchored, deferred, impl_actions, prelude::*, Action, AnchorCorner, AnyElement, actions, anchored, deferred, impl_actions, prelude::*, Action, AnchorCorner, AnyElement,
AppContext, AsyncWindowContext, ClickEvent, ClipboardItem, Div, DragMoveEvent, EntityId, AppContext, AsyncWindowContext, ClickEvent, ClipboardItem, Div, DragMoveEvent, EntityId,
@ -23,6 +22,7 @@ use gpui::{
WindowContext, WindowContext,
}; };
use itertools::Itertools; use itertools::Itertools;
use language::DiagnosticSeverity;
use parking_lot::Mutex; use parking_lot::Mutex;
use project::{Project, ProjectEntryId, ProjectPath, WorktreeId}; use project::{Project, ProjectEntryId, ProjectPath, WorktreeId};
use serde::Deserialize; use serde::Deserialize;
@ -39,10 +39,10 @@ use std::{
}, },
}; };
use theme::ThemeSettings; use theme::ThemeSettings;
use ui::{ use ui::{
prelude::*, right_click_menu, ButtonSize, Color, IconButton, IconButtonShape, IconName, prelude::*, right_click_menu, ButtonSize, Color, DecoratedIcon, IconButton, IconButtonShape,
IconSize, Indicator, Label, PopoverMenu, PopoverMenuHandle, Tab, TabBar, TabPosition, Tooltip, IconDecoration, IconDecorationKind, IconName, IconSize, Indicator, Label, PopoverMenu,
PopoverMenuHandle, Tab, TabBar, TabPosition, Tooltip,
}; };
use ui::{v_flex, ContextMenu}; use ui::{v_flex, ContextMenu};
use util::{debug_panic, maybe, truncate_and_remove_front, ResultExt}; use util::{debug_panic, maybe, truncate_and_remove_front, ResultExt};
@ -305,6 +305,7 @@ pub struct Pane {
pub new_item_context_menu_handle: PopoverMenuHandle<ContextMenu>, pub new_item_context_menu_handle: PopoverMenuHandle<ContextMenu>,
pub split_item_context_menu_handle: PopoverMenuHandle<ContextMenu>, pub split_item_context_menu_handle: PopoverMenuHandle<ContextMenu>,
pinned_tab_count: usize, pinned_tab_count: usize,
diagnostics: HashMap<ProjectPath, DiagnosticSeverity>,
} }
pub struct ActivationHistoryEntry { pub struct ActivationHistoryEntry {
@ -381,6 +382,7 @@ impl Pane {
cx.on_focus_in(&focus_handle, Pane::focus_in), cx.on_focus_in(&focus_handle, Pane::focus_in),
cx.on_focus_out(&focus_handle, Pane::focus_out), cx.on_focus_out(&focus_handle, Pane::focus_out),
cx.observe_global::<SettingsStore>(Self::settings_changed), cx.observe_global::<SettingsStore>(Self::settings_changed),
cx.subscribe(&project, Self::project_events),
]; ];
let handle = cx.view().downgrade(); let handle = cx.view().downgrade();
@ -504,6 +506,7 @@ impl Pane {
split_item_context_menu_handle: Default::default(), split_item_context_menu_handle: Default::default(),
new_item_context_menu_handle: Default::default(), new_item_context_menu_handle: Default::default(),
pinned_tab_count: 0, pinned_tab_count: 0,
diagnostics: Default::default(),
} }
} }
@ -598,6 +601,47 @@ impl Pane {
cx.notify(); cx.notify();
} }
fn project_events(
this: &mut Pane,
_project: Model<Project>,
event: &project::Event,
cx: &mut ViewContext<Self>,
) {
match event {
project::Event::DiskBasedDiagnosticsFinished { .. }
| project::Event::DiagnosticsUpdated { .. } => {
if ItemSettings::get_global(cx).show_diagnostics != ShowDiagnostics::Off {
this.update_diagnostics(cx);
cx.notify();
}
}
_ => {}
}
}
fn update_diagnostics(&mut self, cx: &mut ViewContext<Self>) {
let show_diagnostics = ItemSettings::get_global(cx).show_diagnostics;
self.diagnostics = if show_diagnostics != ShowDiagnostics::Off {
self.project
.read(cx)
.diagnostic_summaries(false, cx)
.filter_map(|(project_path, _, diagnostic_summary)| {
if diagnostic_summary.error_count > 0 {
Some((project_path, DiagnosticSeverity::ERROR))
} else if diagnostic_summary.warning_count > 0
&& show_diagnostics != ShowDiagnostics::Errors
{
Some((project_path, DiagnosticSeverity::WARNING))
} else {
None
}
})
.collect::<HashMap<_, _>>()
} else {
Default::default()
}
}
fn settings_changed(&mut self, cx: &mut ViewContext<Self>) { fn settings_changed(&mut self, cx: &mut ViewContext<Self>) {
if let Some(display_nav_history_buttons) = self.display_nav_history_buttons.as_mut() { if let Some(display_nav_history_buttons) = self.display_nav_history_buttons.as_mut() {
*display_nav_history_buttons = TabBarSettings::get_global(cx).show_nav_history_buttons; *display_nav_history_buttons = TabBarSettings::get_global(cx).show_nav_history_buttons;
@ -605,6 +649,7 @@ impl Pane {
if !PreviewTabsSettings::get_global(cx).enabled { if !PreviewTabsSettings::get_global(cx).enabled {
self.preview_item_id = None; self.preview_item_id = None;
} }
self.update_diagnostics(cx);
cx.notify(); cx.notify();
} }
@ -1839,23 +1884,6 @@ impl Pane {
} }
} }
pub fn git_aware_icon_color(
git_status: Option<GitFileStatus>,
ignored: bool,
selected: bool,
) -> Color {
if ignored {
Color::Ignored
} else {
match git_status {
Some(GitFileStatus::Added) => Color::Created,
Some(GitFileStatus::Modified) => Color::Modified,
Some(GitFileStatus::Conflict) => Color::Conflict,
None => Self::icon_color(selected),
}
}
}
fn toggle_pin_tab(&mut self, _: &TogglePinTab, cx: &mut ViewContext<'_, Self>) { fn toggle_pin_tab(&mut self, _: &TogglePinTab, cx: &mut ViewContext<'_, Self>) {
if self.items.is_empty() { if self.items.is_empty() {
return; return;
@ -1919,8 +1947,6 @@ impl Pane {
focus_handle: &FocusHandle, focus_handle: &FocusHandle,
cx: &mut ViewContext<'_, Pane>, cx: &mut ViewContext<'_, Pane>,
) -> impl IntoElement { ) -> impl IntoElement {
let project_path = item.project_path(cx);
let is_active = ix == self.active_item_index; let is_active = ix == self.active_item_index;
let is_preview = self let is_preview = self
.preview_item_id .preview_item_id
@ -1936,19 +1962,57 @@ impl Pane {
cx, cx,
); );
let icon_color = if ItemSettings::get_global(cx).git_status { let item_diagnostic = item
project_path .project_path(cx)
.as_ref() .map_or(None, |project_path| self.diagnostics.get(&project_path));
.and_then(|path| self.project.read(cx).entry_for_path(path, cx))
.map(|entry| { let decorated_icon = item_diagnostic.map_or(None, |diagnostic| {
Self::git_aware_icon_color(entry.git_status, entry.is_ignored, is_active) let icon = match item.tab_icon(cx) {
}) Some(icon) => icon,
.unwrap_or_else(|| Self::icon_color(is_active)) None => return None,
};
let knockout_item_color = if is_active {
cx.theme().colors().tab_active_background
} else {
cx.theme().colors().tab_bar_background
};
let (icon_decoration, icon_color) = if matches!(diagnostic, &DiagnosticSeverity::ERROR)
{
(IconDecorationKind::X, Color::Error)
} else {
(IconDecorationKind::Triangle, Color::Warning)
};
Some(DecoratedIcon::new(
icon.size(IconSize::Small).color(Color::Muted),
Some(
IconDecoration::new(icon_decoration, knockout_item_color, cx)
.color(icon_color.color(cx))
.position(Point {
x: px(-2.),
y: px(-2.),
}),
),
))
});
let icon = if decorated_icon.is_none() {
match item_diagnostic {
Some(&DiagnosticSeverity::ERROR) => {
Some(Icon::new(IconName::X).color(Color::Error))
}
Some(&DiagnosticSeverity::WARNING) => {
Some(Icon::new(IconName::Triangle).color(Color::Warning))
}
_ => item.tab_icon(cx).map(|icon| icon.color(Color::Muted)),
}
.map(|icon| icon.size(IconSize::Small))
} else { } else {
Self::icon_color(is_active) None
}; };
let icon = item.tab_icon(cx);
let settings = ItemSettings::get_global(cx); let settings = ItemSettings::get_global(cx);
let close_side = &settings.close_position; let close_side = &settings.close_position;
let always_show_close_button = settings.always_show_close_button; let always_show_close_button = settings.always_show_close_button;
@ -2078,7 +2142,13 @@ impl Pane {
.child( .child(
h_flex() h_flex()
.gap_1() .gap_1()
.children(icon.map(|icon| icon.size(IconSize::Small).color(icon_color))) .child(if let Some(decorated_icon) = decorated_icon {
div().child(decorated_icon.into_any_element())
} else if let Some(icon) = icon {
div().child(icon.into_any_element())
} else {
div()
})
.child(label), .child(label),
); );