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: <img width="768" height="523" alt="Screenshot From 2025-07-16 11-02-42" src="https://github.com/user-attachments/assets/308873ba-0458-485d-ae05-0de7c1cdfb28" /> Release Notes: - Added diagnostics indicators to the tab switcher --------- Co-authored-by: Kirill Bulatov <kirill@zed.dev>
This commit is contained in:
parent
823a0018e5
commit
99cee8778c
3 changed files with 108 additions and 49 deletions
|
@ -1569,11 +1569,21 @@ impl Buffer {
|
||||||
self.send_operation(op, true, cx);
|
self.send_operation(op, true, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_diagnostics(&self, server_id: LanguageServerId) -> Option<&DiagnosticSet> {
|
pub fn buffer_diagnostics(
|
||||||
let Ok(idx) = self.diagnostics.binary_search_by_key(&server_id, |v| v.0) else {
|
&self,
|
||||||
return None;
|
for_server: Option<LanguageServerId>,
|
||||||
};
|
) -> Vec<&DiagnosticEntry<Anchor>> {
|
||||||
Some(&self.diagnostics[idx].1)
|
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<Self>) {
|
fn request_autoindent(&mut self, cx: &mut Context<Self>) {
|
||||||
|
|
|
@ -7588,19 +7588,16 @@ impl LspStore {
|
||||||
let snapshot = buffer_handle.read(cx).snapshot();
|
let snapshot = buffer_handle.read(cx).snapshot();
|
||||||
let buffer = buffer_handle.read(cx);
|
let buffer = buffer_handle.read(cx);
|
||||||
let reused_diagnostics = buffer
|
let reused_diagnostics = buffer
|
||||||
.get_diagnostics(server_id)
|
.buffer_diagnostics(Some(server_id))
|
||||||
.into_iter()
|
.iter()
|
||||||
.flat_map(|diag| {
|
.filter(|v| merge(buffer, &v.diagnostic, cx))
|
||||||
diag.iter()
|
.map(|v| {
|
||||||
.filter(|v| merge(buffer, &v.diagnostic, cx))
|
let start = Unclipped(v.range.start.to_point_utf16(&snapshot));
|
||||||
.map(|v| {
|
let end = Unclipped(v.range.end.to_point_utf16(&snapshot));
|
||||||
let start = Unclipped(v.range.start.to_point_utf16(&snapshot));
|
DiagnosticEntry {
|
||||||
let end = Unclipped(v.range.end.to_point_utf16(&snapshot));
|
range: start..end,
|
||||||
DiagnosticEntry {
|
diagnostic: v.diagnostic.clone(),
|
||||||
range: start..end,
|
}
|
||||||
diagnostic: v.diagnostic.clone(),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
|
|
@ -2,12 +2,14 @@
|
||||||
mod tab_switcher_tests;
|
mod tab_switcher_tests;
|
||||||
|
|
||||||
use collections::HashMap;
|
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 fuzzy::StringMatchCandidate;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
Action, AnyElement, App, Context, DismissEvent, Entity, EntityId, EventEmitter, FocusHandle,
|
Action, AnyElement, App, Context, DismissEvent, Entity, EntityId, EventEmitter, FocusHandle,
|
||||||
Focusable, Modifiers, ModifiersChangedEvent, MouseButton, MouseUpEvent, ParentElement, Render,
|
Focusable, Modifiers, ModifiersChangedEvent, MouseButton, MouseUpEvent, ParentElement, Point,
|
||||||
Styled, Task, WeakEntity, Window, actions, rems,
|
Render, Styled, Task, WeakEntity, Window, actions, rems,
|
||||||
};
|
};
|
||||||
use picker::{Picker, PickerDelegate};
|
use picker::{Picker, PickerDelegate};
|
||||||
use project::Project;
|
use project::Project;
|
||||||
|
@ -15,11 +17,14 @@ use schemars::JsonSchema;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use std::{cmp::Reverse, sync::Arc};
|
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 util::ResultExt;
|
||||||
use workspace::{
|
use workspace::{
|
||||||
ModalView, Pane, SaveIntent, Workspace,
|
ModalView, Pane, SaveIntent, Workspace,
|
||||||
item::{ItemHandle, ItemSettings, TabContentParams},
|
item::{ItemHandle, ItemSettings, ShowDiagnostics, TabContentParams},
|
||||||
pane::{Event as PaneEvent, render_item_indicator, tab_details},
|
pane::{Event as PaneEvent, render_item_indicator, tab_details},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -233,6 +238,77 @@ pub struct TabSwitcherDelegate {
|
||||||
restored_items: bool,
|
restored_items: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl TabMatch {
|
||||||
|
fn icon(
|
||||||
|
&self,
|
||||||
|
project: &Entity<Project>,
|
||||||
|
selected: bool,
|
||||||
|
window: &Window,
|
||||||
|
cx: &App,
|
||||||
|
) -> Option<DecoratedIcon> {
|
||||||
|
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 {
|
impl TabSwitcherDelegate {
|
||||||
#[allow(clippy::complexity)]
|
#[allow(clippy::complexity)]
|
||||||
fn new(
|
fn new(
|
||||||
|
@ -574,31 +650,7 @@ impl PickerDelegate for TabSwitcherDelegate {
|
||||||
};
|
};
|
||||||
let label = tab_match.item.tab_content(params, window, cx);
|
let label = tab_match.item.tab_content(params, window, cx);
|
||||||
|
|
||||||
let icon = tab_match.item.tab_icon(window, cx).map(|icon| {
|
let icon = tab_match.icon(&self.project, selected, window, cx);
|
||||||
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 indicator = render_item_indicator(tab_match.item.boxed_clone(), cx);
|
let indicator = render_item_indicator(tab_match.item.boxed_clone(), cx);
|
||||||
let indicator_color = if let Some(ref indicator) = indicator {
|
let indicator_color = if let Some(ref indicator) = indicator {
|
||||||
|
@ -640,7 +692,7 @@ impl PickerDelegate for TabSwitcherDelegate {
|
||||||
.inset(true)
|
.inset(true)
|
||||||
.toggle_state(selected)
|
.toggle_state(selected)
|
||||||
.child(h_flex().w_full().child(label))
|
.child(h_flex().w_full().child(label))
|
||||||
.start_slot::<Icon>(icon)
|
.start_slot::<DecoratedIcon>(icon)
|
||||||
.map(|el| {
|
.map(|el| {
|
||||||
if self.selected_index == ix {
|
if self.selected_index == ix {
|
||||||
el.end_slot::<AnyElement>(close_button)
|
el.end_slot::<AnyElement>(close_button)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue