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) .tab_index(0isize) .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 }; let diagnostics_label = match (self.summary.error_count, self.summary.warning_count) { (0, 0) => "Project diagnostics: no problems".to_string(), (errors, warnings) => { let mut parts = Vec::new(); if errors > 0 { parts.push(format!( "{errors} error{}", if errors == 1 { "" } else { "s" } )); } if warnings > 0 { parts.push(format!( "{warnings} warning{}", if warnings == 1 { "" } else { "s" } )); } format!("Project diagnostics: {}", parts.join(", ")) } }; indicator .child( ButtonLike::new("diagnostic-indicator") .child(diagnostic_indicator) .tab_index(0isize) .aria_label(diagnostics_label) .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); })) }}
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)
.tab_index(0isize)
.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
};
let diagnostics_label = match (self.summary.error_count, self.summary.warning_count) {
(0, 0) => "Project diagnostics: no problems".to_string(),
(errors, warnings) => {
let mut parts = Vec::new();
if errors > 0 {
parts.push(format!(
"{errors} error{}",
if errors == 1 { "" } else { "s" }
));
}
if warnings > 0 {
parts.push(format!(
"{warnings} warning{}",
if warnings == 1 { "" } else { "s" }
));
}
format!("Project diagnostics: {}", parts.join(", "))
}
};
indicator
.child(
ButtonLike::new("diagnostic-indicator")
.child(diagnostic_indicator)
.tab_index(0isize)
.aria_label(diagnostics_label)
.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);
}))
}
}