This PR introduces a new `MultiBufferOffset` new type wrapping size. The goal of this is to make it clear at the type level when we are interacting with offsets of a multi buffer versus offsets of a language / text buffer. This improves readability of things quite a bit by making it clear what kind of offsets one is working with while also reducing accidental bugs by using the wrong kin of offset for the wrong API. This PR also uncovered two minor bugs due to that. Does not yet introduce the MultiBufferPoint equivalent, that is for a follow up PR. Release Notes: - N/A *or* Added/Fixed/Improved ...
227 lines
8.6 KiB
Rust
227 lines
8.6 KiB
Rust
use std::time::Duration;
|
|
|
|
use editor::{Editor, MultiBufferOffset};
|
|
use gpui::{
|
|
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::{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();
|
|
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);
|
|
Some(
|
|
Button::new("diagnostic_message", SharedString::new(message))
|
|
.label_size(LabelSize::Small)
|
|
.tooltip(|_window, cx| {
|
|
Tooltip::for_action(
|
|
"Next Diagnostic",
|
|
&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 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_impl(
|
|
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();
|
|
}
|
|
}
|