Files
zed/crates/diagnostics/src/items.rs
T
nullstalgiaanddino b81ce2f276 diagnostics: Prefer activating diagnostic under cursor (#52957)
Update the behavior of both `editor: go to diagnostic` and `editor: go
to previous diagnostic` in order to ensure that, if there's a diagnostic
under the user's cursor that isn't active, it is first activated, with a
subsequent call jumping to the next or previous diagnostic,
respectively.

These changes also update how diagnostic activation handles the
situation when the global diagnostic renderer is not registered, as we
used to not update the active diagnostic group in that situation.
However, we now rely on it to determine whether the user's cursor is
already in the active diagnostic, with some tests now failing, so we now
default to an empty set of blocks for the active diagnostic group when
no global renderer is registered.

Release Notes:

- Update both `editor: go to diagnostic` and `editor: go to previous
diagnostic` to prefer activating the diagnostic under the cursor before
jumping to the next or previous diagnostic, respectively

---------

Co-authored-by: dino <dinojoaocosta@gmail.com>
2026-05-28 17:30:36 +00:00

248 lines
9.4 KiB
Rust

use std::time::Duration;
use editor::{Editor, MultiBufferOffset};
use gpui::{
App, Context, Entity, EventEmitter, IntoElement, ParentElement, Render, Styled, Subscription,
Task, WeakEntity, Window,
};
use language::Diagnostic;
use project::project_settings::{GoToDiagnosticSeverityFilter, ProjectSettings};
use settings::Settings;
use ui::{Button, ButtonLike, Color, Icon, IconName, Label, Tooltip, h_flex, prelude::*};
use util::ResultExt;
use workspace::{HideStatusItem, StatusItemView, ToolbarItemEvent, Workspace, item::ItemHandle};
use crate::{Deploy, IncludeWarnings, ProjectDiagnosticsEditor};
/// The status bar item that displays diagnostic counts.
pub struct DiagnosticIndicator {
summary: project::DiagnosticSummary,
workspace: WeakEntity<Workspace>,
current_diagnostic: Option<Diagnostic>,
active_editor: Option<WeakEntity<Editor>>,
_observe_active_editor: Option<Subscription>,
diagnostics_update: Task<()>,
diagnostic_summary_update: Task<()>,
}
impl Render for DiagnosticIndicator {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let indicator = h_flex().gap_2().min_w_0().overflow_x_hidden();
if !ProjectSettings::get_global(cx).diagnostics.button {
return indicator.hidden();
}
let diagnostic_indicator = match (self.summary.error_count, self.summary.warning_count) {
(0, 0) => h_flex().child(
Icon::new(IconName::Check)
.size(IconSize::Small)
.color(Color::Default),
),
(error_count, warning_count) => h_flex()
.gap_1()
.when(error_count > 0, |this| {
this.child(
Icon::new(IconName::XCircle)
.size(IconSize::Small)
.color(Color::Error),
)
.child(Label::new(error_count.to_string()).size(LabelSize::Small))
})
.when(warning_count > 0, |this| {
this.child(
Icon::new(IconName::Warning)
.size(IconSize::Small)
.color(Color::Warning),
)
.child(Label::new(warning_count.to_string()).size(LabelSize::Small))
}),
};
let status = if let Some(diagnostic) = &self.current_diagnostic {
let message = diagnostic
.message
.split_once('\n')
.map_or(&*diagnostic.message, |(first, _)| first);
let diagnostics_already_active = self.any_active_diagnostics(cx);
let tooltip = if !diagnostics_already_active {
"Expand Diagnostics"
} else {
"Next Diagnostic"
};
Some(
Button::new("diagnostic_message", SharedString::new(message))
.label_size(LabelSize::Small)
.truncate(true)
.tooltip(move |_window, cx| {
Tooltip::for_action(
tooltip,
&editor::actions::GoToDiagnostic::default(),
cx,
)
})
.on_click(
cx.listener(|this, _, window, cx| this.go_to_next_diagnostic(window, cx)),
),
)
} else {
None
};
indicator
.child(
ButtonLike::new("diagnostic-indicator")
.child(diagnostic_indicator)
.tooltip(move |_window, cx| {
Tooltip::for_action("Project Diagnostics", &Deploy, cx)
})
.on_click(cx.listener(|this, _, window, cx| {
if let Some(workspace) = this.workspace.upgrade() {
if this.summary.error_count == 0 && this.summary.warning_count > 0 {
cx.update_default_global(
|show_warnings: &mut IncludeWarnings, _| show_warnings.0 = true,
);
}
workspace.update(cx, |workspace, cx| {
ProjectDiagnosticsEditor::deploy(
workspace,
&Default::default(),
window,
cx,
)
})
}
})),
)
.children(status)
}
}
impl DiagnosticIndicator {
pub fn new(workspace: &Workspace, cx: &mut Context<Self>) -> Self {
let project = workspace.project();
cx.subscribe(project, |this, project, event, cx| match event {
project::Event::DiskBasedDiagnosticsStarted { .. } => {
cx.notify();
}
project::Event::DiskBasedDiagnosticsFinished { .. }
| project::Event::LanguageServerRemoved(_) => {
this.summary = project.read(cx).diagnostic_summary(false, cx);
cx.notify();
}
project::Event::DiagnosticsUpdated { .. } => {
this.diagnostic_summary_update = cx.spawn(async move |this, cx| {
cx.background_executor()
.timer(Duration::from_millis(30))
.await;
this.update(cx, |this, cx| {
this.summary = project.read(cx).diagnostic_summary(false, cx);
cx.notify();
})
.log_err();
});
}
_ => {}
})
.detach();
Self {
summary: project.read(cx).diagnostic_summary(false, cx),
active_editor: None,
workspace: workspace.weak_handle(),
current_diagnostic: None,
_observe_active_editor: None,
diagnostics_update: Task::ready(()),
diagnostic_summary_update: Task::ready(()),
}
}
fn any_active_diagnostics(&self, cx: &mut Context<Self>) -> bool {
if let Some(editor) = self.active_editor.as_ref().and_then(|e| e.upgrade()) {
editor.read(cx).any_active_diagnostics()
} else {
false
}
}
fn go_to_next_diagnostic(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if let Some(editor) = self.active_editor.as_ref().and_then(|e| e.upgrade()) {
editor.update(cx, |editor, cx| {
editor.go_to_diagnostic_at_cursor(
editor::Direction::Next,
GoToDiagnosticSeverityFilter::default(),
window,
cx,
);
})
}
}
fn update(&mut self, editor: Entity<Editor>, window: &mut Window, cx: &mut Context<Self>) {
let (buffer, cursor_position) = editor.update(cx, |editor, cx| {
let buffer = editor.buffer().read(cx).snapshot(cx);
let cursor_position = editor
.selections
.newest::<MultiBufferOffset>(&editor.display_snapshot(cx))
.head();
(buffer, cursor_position)
});
let new_diagnostic = buffer
.diagnostics_in_range::<MultiBufferOffset>(cursor_position..cursor_position)
.filter(|entry| !entry.range.is_empty())
.min_by_key(|entry| {
(
entry.diagnostic.severity,
entry.range.end - entry.range.start,
)
})
.map(|entry| entry.diagnostic);
if new_diagnostic != self.current_diagnostic.as_ref() {
let new_diagnostic = new_diagnostic.cloned();
self.diagnostics_update =
cx.spawn_in(window, async move |diagnostics_indicator, cx| {
cx.background_executor()
.timer(Duration::from_millis(50))
.await;
diagnostics_indicator
.update(cx, |diagnostics_indicator, cx| {
diagnostics_indicator.current_diagnostic = new_diagnostic;
cx.notify();
})
.ok();
});
}
}
}
impl EventEmitter<ToolbarItemEvent> for DiagnosticIndicator {}
impl StatusItemView for DiagnosticIndicator {
fn set_active_pane_item(
&mut self,
active_pane_item: Option<&dyn ItemHandle>,
window: &mut Window,
cx: &mut Context<Self>,
) {
if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) {
self.active_editor = Some(editor.downgrade());
self._observe_active_editor = Some(cx.observe_in(&editor, window, Self::update));
self.update(editor, window, cx);
} else {
self.active_editor = None;
self.current_diagnostic = None;
self._observe_active_editor = None;
}
cx.notify();
}
fn hide_setting(&self, _: &App) -> Option<HideStatusItem> {
Some(HideStatusItem::new(|settings| {
settings.diagnostics.get_or_insert_default().button = Some(false);
}))
}
}