From 99cee8778cc7c6ee9ddd405f5f00caa713299d68 Mon Sep 17 00:00:00 2001
From: Alvaro Parker <64918109+AlvaroParker@users.noreply.github.com>
Date: Mon, 25 Aug 2025 16:18:03 -0400
Subject: [PATCH] tab_switcher: Add support for diagnostics (#34547)
Support to show diagnostics on the tab switcher in the same way they are
displayed on the tab bar. This follows the setting
`tabs.show_diagnostics`.
This will improve user experience when disabling the tab bar and still
being able to see the diagnostics when switching tabs
Preview:
Release Notes:
- Added diagnostics indicators to the tab switcher
---------
Co-authored-by: Kirill Bulatov
---
crates/language/src/buffer.rs | 20 +++--
crates/project/src/lsp_store.rs | 23 +++--
crates/tab_switcher/src/tab_switcher.rs | 114 +++++++++++++++++-------
3 files changed, 108 insertions(+), 49 deletions(-)
diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs
index b106110c33..4ddc2b3018 100644
--- a/crates/language/src/buffer.rs
+++ b/crates/language/src/buffer.rs
@@ -1569,11 +1569,21 @@ impl Buffer {
self.send_operation(op, true, cx);
}
- pub fn get_diagnostics(&self, server_id: LanguageServerId) -> Option<&DiagnosticSet> {
- let Ok(idx) = self.diagnostics.binary_search_by_key(&server_id, |v| v.0) else {
- return None;
- };
- Some(&self.diagnostics[idx].1)
+ pub fn buffer_diagnostics(
+ &self,
+ for_server: Option,
+ ) -> Vec<&DiagnosticEntry> {
+ match for_server {
+ Some(server_id) => match self.diagnostics.binary_search_by_key(&server_id, |v| v.0) {
+ Ok(idx) => self.diagnostics[idx].1.iter().collect(),
+ Err(_) => Vec::new(),
+ },
+ None => self
+ .diagnostics
+ .iter()
+ .flat_map(|(_, diagnostic_set)| diagnostic_set.iter())
+ .collect(),
+ }
}
fn request_autoindent(&mut self, cx: &mut Context) {
diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs
index 853490ddac..deebaedd74 100644
--- a/crates/project/src/lsp_store.rs
+++ b/crates/project/src/lsp_store.rs
@@ -7588,19 +7588,16 @@ impl LspStore {
let snapshot = buffer_handle.read(cx).snapshot();
let buffer = buffer_handle.read(cx);
let reused_diagnostics = buffer
- .get_diagnostics(server_id)
- .into_iter()
- .flat_map(|diag| {
- diag.iter()
- .filter(|v| merge(buffer, &v.diagnostic, cx))
- .map(|v| {
- let start = Unclipped(v.range.start.to_point_utf16(&snapshot));
- let end = Unclipped(v.range.end.to_point_utf16(&snapshot));
- DiagnosticEntry {
- range: start..end,
- diagnostic: v.diagnostic.clone(),
- }
- })
+ .buffer_diagnostics(Some(server_id))
+ .iter()
+ .filter(|v| merge(buffer, &v.diagnostic, cx))
+ .map(|v| {
+ let start = Unclipped(v.range.start.to_point_utf16(&snapshot));
+ let end = Unclipped(v.range.end.to_point_utf16(&snapshot));
+ DiagnosticEntry {
+ range: start..end,
+ diagnostic: v.diagnostic.clone(),
+ }
})
.collect::>();
diff --git a/crates/tab_switcher/src/tab_switcher.rs b/crates/tab_switcher/src/tab_switcher.rs
index 7c70bcd5b5..bf3ce7b568 100644
--- a/crates/tab_switcher/src/tab_switcher.rs
+++ b/crates/tab_switcher/src/tab_switcher.rs
@@ -2,12 +2,14 @@
mod tab_switcher_tests;
use collections::HashMap;
-use editor::items::entry_git_aware_label_color;
+use editor::items::{
+ entry_diagnostic_aware_icon_decoration_and_color, entry_git_aware_label_color,
+};
use fuzzy::StringMatchCandidate;
use gpui::{
Action, AnyElement, App, Context, DismissEvent, Entity, EntityId, EventEmitter, FocusHandle,
- Focusable, Modifiers, ModifiersChangedEvent, MouseButton, MouseUpEvent, ParentElement, Render,
- Styled, Task, WeakEntity, Window, actions, rems,
+ Focusable, Modifiers, ModifiersChangedEvent, MouseButton, MouseUpEvent, ParentElement, Point,
+ Render, Styled, Task, WeakEntity, Window, actions, rems,
};
use picker::{Picker, PickerDelegate};
use project::Project;
@@ -15,11 +17,14 @@ use schemars::JsonSchema;
use serde::Deserialize;
use settings::Settings;
use std::{cmp::Reverse, sync::Arc};
-use ui::{ListItem, ListItemSpacing, Tooltip, prelude::*};
+use ui::{
+ DecoratedIcon, IconDecoration, IconDecorationKind, ListItem, ListItemSpacing, Tooltip,
+ prelude::*,
+};
use util::ResultExt;
use workspace::{
ModalView, Pane, SaveIntent, Workspace,
- item::{ItemHandle, ItemSettings, TabContentParams},
+ item::{ItemHandle, ItemSettings, ShowDiagnostics, TabContentParams},
pane::{Event as PaneEvent, render_item_indicator, tab_details},
};
@@ -233,6 +238,77 @@ pub struct TabSwitcherDelegate {
restored_items: bool,
}
+impl TabMatch {
+ fn icon(
+ &self,
+ project: &Entity,
+ selected: bool,
+ window: &Window,
+ cx: &App,
+ ) -> Option {
+ let icon = self.item.tab_icon(window, cx)?;
+ let item_settings = ItemSettings::get_global(cx);
+ let show_diagnostics = item_settings.show_diagnostics;
+ let git_status_color = item_settings
+ .git_status
+ .then(|| {
+ let path = self.item.project_path(cx)?;
+ let project = project.read(cx);
+ let entry = project.entry_for_path(&path, cx)?;
+ let git_status = project
+ .project_path_git_status(&path, cx)
+ .map(|status| status.summary())
+ .unwrap_or_default();
+ Some(entry_git_aware_label_color(
+ git_status,
+ entry.is_ignored,
+ selected,
+ ))
+ })
+ .flatten();
+ let colored_icon = icon.color(git_status_color.unwrap_or_default());
+
+ let most_sever_diagostic_level = if show_diagnostics == ShowDiagnostics::Off {
+ None
+ } else {
+ let buffer_store = project.read(cx).buffer_store().read(cx);
+ let buffer = self
+ .item
+ .project_path(cx)
+ .and_then(|path| buffer_store.get_by_path(&path))
+ .map(|buffer| buffer.read(cx));
+ buffer.and_then(|buffer| {
+ buffer
+ .buffer_diagnostics(None)
+ .iter()
+ .map(|diagnostic_entry| diagnostic_entry.diagnostic.severity)
+ .min()
+ })
+ };
+
+ let decorations =
+ entry_diagnostic_aware_icon_decoration_and_color(most_sever_diagostic_level)
+ .filter(|(d, _)| {
+ *d != IconDecorationKind::Triangle
+ || show_diagnostics != ShowDiagnostics::Errors
+ })
+ .map(|(icon, color)| {
+ let knockout_item_color = if selected {
+ cx.theme().colors().element_selected
+ } else {
+ cx.theme().colors().element_background
+ };
+ IconDecoration::new(icon, knockout_item_color, cx)
+ .color(color.color(cx))
+ .position(Point {
+ x: px(-2.),
+ y: px(-2.),
+ })
+ });
+ Some(DecoratedIcon::new(colored_icon, decorations))
+ }
+}
+
impl TabSwitcherDelegate {
#[allow(clippy::complexity)]
fn new(
@@ -574,31 +650,7 @@ impl PickerDelegate for TabSwitcherDelegate {
};
let label = tab_match.item.tab_content(params, window, cx);
- let icon = tab_match.item.tab_icon(window, cx).map(|icon| {
- let git_status_color = ItemSettings::get_global(cx)
- .git_status
- .then(|| {
- tab_match
- .item
- .project_path(cx)
- .as_ref()
- .and_then(|path| {
- let project = self.project.read(cx);
- let entry = project.entry_for_path(path, cx)?;
- let git_status = project
- .project_path_git_status(path, cx)
- .map(|status| status.summary())
- .unwrap_or_default();
- Some((entry, git_status))
- })
- .map(|(entry, git_status)| {
- entry_git_aware_label_color(git_status, entry.is_ignored, selected)
- })
- })
- .flatten();
-
- icon.color(git_status_color.unwrap_or_default())
- });
+ let icon = tab_match.icon(&self.project, selected, window, cx);
let indicator = render_item_indicator(tab_match.item.boxed_clone(), cx);
let indicator_color = if let Some(ref indicator) = indicator {
@@ -640,7 +692,7 @@ impl PickerDelegate for TabSwitcherDelegate {
.inset(true)
.toggle_state(selected)
.child(h_flex().w_full().child(label))
- .start_slot::(icon)
+ .start_slot::(icon)
.map(|el| {
if self.selected_index == ix {
el.end_slot::(close_button)