logiguard fork v3: full patch set on verified 8c74db0 tree
Includes prior-session patches (carry forward so the app compiles): - crates/gpui/build.rs: cross-compile manifest fix - crates/gpui/src/platform.rs: PlatformWindow::activate_with_token trait method - crates/gpui/src/window.rs: Window::activate_with_token public API - crates/gpui_linux/src/linux/wayland/window.rs: WaylandWindow::activate_with_token + activate() keyboard-serial fix Plus the focus-serial fix: - serial.rs: SerialKind::KeyboardEnter - client.rs: store wl_keyboard.enter serial; latest_serial_of() Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
55
crates/diagnostics/Cargo.toml
Normal file
55
crates/diagnostics/Cargo.toml
Normal file
@@ -0,0 +1,55 @@
|
||||
[package]
|
||||
name = "diagnostics"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/diagnostics.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
agent_settings.workspace = true
|
||||
anyhow.workspace = true
|
||||
collections.workspace = true
|
||||
component.workspace = true
|
||||
ctor.workspace = true
|
||||
editor.workspace = true
|
||||
futures-lite.workspace = true
|
||||
gpui.workspace = true
|
||||
indoc.workspace = true
|
||||
itertools.workspace = true
|
||||
language.workspace = true
|
||||
log.workspace = true
|
||||
lsp.workspace = true
|
||||
markdown.workspace = true
|
||||
project.workspace = true
|
||||
rand.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
text.workspace = true
|
||||
theme.workspace = true
|
||||
theme_settings.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
editor = { workspace = true, features = ["test-support"] }
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
language = { workspace = true, features = ["test-support"] }
|
||||
lsp = { workspace = true, features = ["test-support"] }
|
||||
markdown = { workspace = true, features = ["test-support"] }
|
||||
pretty_assertions.workspace = true
|
||||
serde_json.workspace = true
|
||||
theme = { workspace = true, features = ["test-support"] }
|
||||
unindent.workspace = true
|
||||
workspace = { workspace = true, features = ["test-support"] }
|
||||
zlog.workspace = true
|
||||
1
crates/diagnostics/LICENSE-GPL
Symbolic link
1
crates/diagnostics/LICENSE-GPL
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE-GPL
|
||||
1014
crates/diagnostics/src/buffer_diagnostics.rs
Normal file
1014
crates/diagnostics/src/buffer_diagnostics.rs
Normal file
File diff suppressed because it is too large
Load Diff
325
crates/diagnostics/src/diagnostic_renderer.rs
Normal file
325
crates/diagnostics/src/diagnostic_renderer.rs
Normal file
@@ -0,0 +1,325 @@
|
||||
use std::{ops::Range, sync::Arc};
|
||||
|
||||
use editor::{
|
||||
Anchor, Editor, EditorSnapshot, ToOffset,
|
||||
display_map::{BlockContext, BlockPlacement, BlockProperties, BlockStyle},
|
||||
hover_popover::diagnostics_markdown_style,
|
||||
};
|
||||
use gpui::{AppContext, Entity, Focusable, WeakEntity};
|
||||
use language::{BufferId, Diagnostic, DiagnosticEntryRef, LanguageRegistry};
|
||||
use lsp::DiagnosticSeverity;
|
||||
use markdown::{CopyButtonVisibility, Markdown, MarkdownElement};
|
||||
use settings::Settings;
|
||||
use text::Point;
|
||||
use theme_settings::ThemeSettings;
|
||||
use ui::{CopyButton, prelude::*};
|
||||
use util::maybe;
|
||||
|
||||
use crate::toolbar_controls::DiagnosticsToolbarEditor;
|
||||
|
||||
pub struct DiagnosticRenderer;
|
||||
|
||||
impl DiagnosticRenderer {
|
||||
pub fn diagnostic_blocks_for_group(
|
||||
diagnostic_group: Vec<DiagnosticEntryRef<'_, Point>>,
|
||||
buffer_id: BufferId,
|
||||
diagnostics_editor: Option<Arc<dyn DiagnosticsToolbarEditor>>,
|
||||
language_registry: Option<Arc<LanguageRegistry>>,
|
||||
cx: &mut App,
|
||||
) -> Vec<DiagnosticBlock> {
|
||||
let Some(primary_ix) = diagnostic_group
|
||||
.iter()
|
||||
.position(|d| d.diagnostic.is_primary)
|
||||
else {
|
||||
return Vec::new();
|
||||
};
|
||||
let primary = &diagnostic_group[primary_ix];
|
||||
let group_id = primary.diagnostic.group_id;
|
||||
let mut results = vec![];
|
||||
for entry in diagnostic_group.iter() {
|
||||
let mut markdown = Self::markdown(&entry.diagnostic);
|
||||
if entry.diagnostic.is_primary {
|
||||
let diagnostic = &primary.diagnostic;
|
||||
if diagnostic.source.is_some() || diagnostic.code.is_some() {
|
||||
markdown.push_str(" (");
|
||||
}
|
||||
if let Some(source) = diagnostic.source.as_ref() {
|
||||
markdown.push_str(&Markdown::escape(source));
|
||||
}
|
||||
if diagnostic.source.is_some() && diagnostic.code.is_some() {
|
||||
markdown.push(' ');
|
||||
}
|
||||
if let Some(code) = diagnostic.code.as_ref() {
|
||||
if let Some(description) = diagnostic.code_description.as_ref() {
|
||||
markdown.push('[');
|
||||
markdown.push_str(&Markdown::escape(&code.to_string()));
|
||||
markdown.push_str("](");
|
||||
markdown.push_str(&Markdown::escape(description.as_ref()));
|
||||
markdown.push(')');
|
||||
} else {
|
||||
markdown.push_str(&Markdown::escape(&code.to_string()));
|
||||
}
|
||||
}
|
||||
if diagnostic.source.is_some() || diagnostic.code.is_some() {
|
||||
markdown.push(')');
|
||||
}
|
||||
|
||||
for (ix, entry) in diagnostic_group.iter().enumerate() {
|
||||
if entry.range.start.row.abs_diff(primary.range.start.row) >= 5 {
|
||||
markdown.push_str("\n- hint: [");
|
||||
markdown.push_str(&Markdown::escape(&entry.diagnostic.message));
|
||||
markdown.push_str(&format!(
|
||||
"](file://#diagnostic-{buffer_id}-{group_id}-{ix})\n",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
results.push(DiagnosticBlock {
|
||||
initial_range: primary.range.clone(),
|
||||
severity: primary.diagnostic.severity,
|
||||
diagnostics_editor: diagnostics_editor.clone(),
|
||||
copy_message: primary.diagnostic.message.clone().into(),
|
||||
markdown: cx.new(|cx| {
|
||||
Markdown::new(markdown.into(), language_registry.clone(), None, cx)
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
if entry.range.start.row.abs_diff(primary.range.start.row) >= 5 {
|
||||
markdown.push_str(&format!(
|
||||
" ([back](file://#diagnostic-{buffer_id}-{group_id}-{primary_ix}))"
|
||||
));
|
||||
}
|
||||
results.push(DiagnosticBlock {
|
||||
initial_range: entry.range.clone(),
|
||||
severity: entry.diagnostic.severity,
|
||||
diagnostics_editor: diagnostics_editor.clone(),
|
||||
copy_message: entry.diagnostic.message.clone().into(),
|
||||
markdown: cx.new(|cx| {
|
||||
Markdown::new(markdown.into(), language_registry.clone(), None, cx)
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
|
||||
fn markdown(diagnostic: &Diagnostic) -> String {
|
||||
let mut markdown = String::new();
|
||||
|
||||
if let Some(md) = &diagnostic.markdown {
|
||||
markdown.push_str(md);
|
||||
} else {
|
||||
markdown.push_str(&Markdown::escape(&diagnostic.message));
|
||||
};
|
||||
markdown
|
||||
}
|
||||
}
|
||||
|
||||
impl editor::DiagnosticRenderer for DiagnosticRenderer {
|
||||
fn render_group(
|
||||
&self,
|
||||
diagnostic_group: Vec<DiagnosticEntryRef<'_, Point>>,
|
||||
buffer_id: BufferId,
|
||||
snapshot: EditorSnapshot,
|
||||
editor: WeakEntity<Editor>,
|
||||
language_registry: Option<Arc<LanguageRegistry>>,
|
||||
cx: &mut App,
|
||||
) -> Vec<BlockProperties<Anchor>> {
|
||||
let blocks = Self::diagnostic_blocks_for_group(
|
||||
diagnostic_group,
|
||||
buffer_id,
|
||||
None,
|
||||
language_registry,
|
||||
cx,
|
||||
);
|
||||
|
||||
blocks
|
||||
.into_iter()
|
||||
.map(|block| {
|
||||
let editor = editor.clone();
|
||||
BlockProperties {
|
||||
placement: BlockPlacement::Near(
|
||||
snapshot
|
||||
.buffer_snapshot()
|
||||
.anchor_after(block.initial_range.start),
|
||||
),
|
||||
height: Some(1),
|
||||
style: BlockStyle::Flex,
|
||||
render: Arc::new(move |bcx| block.render_block(editor.clone(), bcx)),
|
||||
priority: 1,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn render_hover(
|
||||
&self,
|
||||
diagnostic_group: Vec<DiagnosticEntryRef<'_, Point>>,
|
||||
range: Range<Point>,
|
||||
buffer_id: BufferId,
|
||||
language_registry: Option<Arc<LanguageRegistry>>,
|
||||
cx: &mut App,
|
||||
) -> Option<Entity<Markdown>> {
|
||||
let blocks = Self::diagnostic_blocks_for_group(
|
||||
diagnostic_group,
|
||||
buffer_id,
|
||||
None,
|
||||
language_registry,
|
||||
cx,
|
||||
);
|
||||
blocks
|
||||
.into_iter()
|
||||
.find_map(|block| (block.initial_range == range).then(|| block.markdown))
|
||||
}
|
||||
|
||||
fn open_link(
|
||||
&self,
|
||||
editor: &mut Editor,
|
||||
link: SharedString,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) {
|
||||
DiagnosticBlock::open_link(editor, &None, link, window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct DiagnosticBlock {
|
||||
pub(crate) initial_range: Range<Point>,
|
||||
pub(crate) severity: DiagnosticSeverity,
|
||||
pub(crate) markdown: Entity<Markdown>,
|
||||
pub(crate) diagnostics_editor: Option<Arc<dyn DiagnosticsToolbarEditor>>,
|
||||
pub(crate) copy_message: SharedString,
|
||||
}
|
||||
|
||||
impl DiagnosticBlock {
|
||||
pub fn render_block(&self, editor: WeakEntity<Editor>, bcx: &BlockContext) -> AnyElement {
|
||||
let cx = &bcx.app;
|
||||
let status_colors = cx.theme().status();
|
||||
|
||||
let max_width = bcx.em_width * 120.;
|
||||
|
||||
let (background_color, border_color) = match self.severity {
|
||||
DiagnosticSeverity::ERROR => (status_colors.error_background, status_colors.error),
|
||||
DiagnosticSeverity::WARNING => {
|
||||
(status_colors.warning_background, status_colors.warning)
|
||||
}
|
||||
DiagnosticSeverity::INFORMATION => (status_colors.info_background, status_colors.info),
|
||||
DiagnosticSeverity::HINT => (status_colors.hint_background, status_colors.hint),
|
||||
_ => (status_colors.ignored_background, status_colors.ignored),
|
||||
};
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let editor_line_height = (settings.line_height() * settings.buffer_font_size(cx)).round();
|
||||
let line_height = editor_line_height;
|
||||
let diagnostics_editor = self.diagnostics_editor.clone();
|
||||
|
||||
let copy_button_id = format!(
|
||||
"copy-diagnostic-{}-{}-{}-{}",
|
||||
self.initial_range.start.row,
|
||||
self.initial_range.start.column,
|
||||
self.initial_range.end.row,
|
||||
self.initial_range.end.column
|
||||
);
|
||||
|
||||
h_flex()
|
||||
.max_w(max_width)
|
||||
.pl_1p5()
|
||||
.pr_0p5()
|
||||
.items_start()
|
||||
.gap_1()
|
||||
.border_l_2()
|
||||
.line_height(line_height)
|
||||
.bg(background_color)
|
||||
.border_color(border_color)
|
||||
.child(
|
||||
div().flex_1().min_w_0().child(
|
||||
MarkdownElement::new(
|
||||
self.markdown.clone(),
|
||||
diagnostics_markdown_style(bcx.window, cx),
|
||||
)
|
||||
.code_block_renderer(markdown::CodeBlockRenderer::Default {
|
||||
copy_button_visibility: CopyButtonVisibility::Hidden,
|
||||
border: false,
|
||||
})
|
||||
.on_url_click({
|
||||
move |link, window, cx| {
|
||||
editor
|
||||
.update(cx, |editor, cx| {
|
||||
Self::open_link(editor, &diagnostics_editor, link, window, cx)
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
CopyButton::new(copy_button_id, self.copy_message.clone())
|
||||
.tooltip_label("Copy Diagnostic"),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
pub fn open_link(
|
||||
editor: &mut Editor,
|
||||
diagnostics_editor: &Option<Arc<dyn DiagnosticsToolbarEditor>>,
|
||||
link: SharedString,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) {
|
||||
let Some(diagnostic_link) = link.strip_prefix("file://#diagnostic-") else {
|
||||
editor::hover_popover::open_markdown_url(link, window, cx);
|
||||
return;
|
||||
};
|
||||
let Some((buffer_id, group_id, ix)) = maybe!({
|
||||
let mut parts = diagnostic_link.split('-');
|
||||
let buffer_id: u64 = parts.next()?.parse().ok()?;
|
||||
let group_id: usize = parts.next()?.parse().ok()?;
|
||||
let ix: usize = parts.next()?.parse().ok()?;
|
||||
Some((BufferId::new(buffer_id).ok()?, group_id, ix))
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(diagnostics_editor) = diagnostics_editor {
|
||||
if let Some(diagnostic) = diagnostics_editor
|
||||
.get_diagnostics_for_buffer(buffer_id, cx)
|
||||
.into_iter()
|
||||
.filter(|d| d.diagnostic.group_id == group_id)
|
||||
.nth(ix)
|
||||
{
|
||||
let multibuffer = editor.buffer().read(cx);
|
||||
if let Some(anchor_range) = multibuffer
|
||||
.snapshot(cx)
|
||||
.buffer_anchor_range_to_anchor_range(diagnostic.range)
|
||||
{
|
||||
Self::jump_to(editor, anchor_range, window, cx);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else if let Some(diagnostic) = editor
|
||||
.snapshot(window, cx)
|
||||
.buffer_snapshot()
|
||||
.diagnostic_group(buffer_id, group_id)
|
||||
.nth(ix)
|
||||
{
|
||||
Self::jump_to(editor, diagnostic.range, window, cx)
|
||||
};
|
||||
}
|
||||
|
||||
fn jump_to<I: ToOffset>(
|
||||
editor: &mut Editor,
|
||||
range: Range<I>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) {
|
||||
let snapshot = &editor.buffer().read(cx).snapshot(cx);
|
||||
let range = range.start.to_offset(snapshot)..range.end.to_offset(snapshot);
|
||||
|
||||
editor.unfold_ranges(&[range.start..range.end], true, false, cx);
|
||||
editor.change_selections(Default::default(), window, cx, |s| {
|
||||
s.select_ranges([range.start..range.start]);
|
||||
});
|
||||
window.focus(&editor.focus_handle(cx), cx);
|
||||
}
|
||||
}
|
||||
1150
crates/diagnostics/src/diagnostics.rs
Normal file
1150
crates/diagnostics/src/diagnostics.rs
Normal file
File diff suppressed because it is too large
Load Diff
2152
crates/diagnostics/src/diagnostics_tests.rs
Normal file
2152
crates/diagnostics/src/diagnostics_tests.rs
Normal file
File diff suppressed because it is too large
Load Diff
233
crates/diagnostics/src/items.rs
Normal file
233
crates/diagnostics/src/items.rs
Normal file
@@ -0,0 +1,233 @@
|
||||
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);
|
||||
Some(
|
||||
Button::new("diagnostic_message", SharedString::new(message))
|
||||
.label_size(LabelSize::Small)
|
||||
.truncate(true)
|
||||
.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();
|
||||
}
|
||||
|
||||
fn hide_setting(&self, _: &App) -> Option<HideStatusItem> {
|
||||
Some(HideStatusItem::new(|settings| {
|
||||
settings.diagnostics.get_or_insert_default().button = Some(false);
|
||||
}))
|
||||
}
|
||||
}
|
||||
173
crates/diagnostics/src/toolbar_controls.rs
Normal file
173
crates/diagnostics/src/toolbar_controls.rs
Normal file
@@ -0,0 +1,173 @@
|
||||
use crate::{BufferDiagnosticsEditor, ProjectDiagnosticsEditor, ToggleDiagnosticsRefresh};
|
||||
use agent_settings::AgentSettings;
|
||||
use gpui::{Context, EventEmitter, ParentElement, Render, Window};
|
||||
use language::DiagnosticEntry;
|
||||
use settings::Settings;
|
||||
use text::{Anchor, BufferId};
|
||||
use ui::{Tooltip, prelude::*};
|
||||
use workspace::{ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, item::ItemHandle};
|
||||
use zed_actions::assistant::InlineAssist;
|
||||
use zed_actions::buffer_search;
|
||||
|
||||
pub struct ToolbarControls {
|
||||
editor: Option<Box<dyn DiagnosticsToolbarEditor>>,
|
||||
}
|
||||
|
||||
pub(crate) trait DiagnosticsToolbarEditor: Send + Sync {
|
||||
/// Informs the toolbar whether warnings are included in the diagnostics.
|
||||
fn include_warnings(&self, cx: &App) -> bool;
|
||||
/// Toggles whether warning diagnostics should be displayed by the
|
||||
/// diagnostics editor.
|
||||
fn toggle_warnings(&self, window: &mut Window, cx: &mut App);
|
||||
/// Indicates whether the diagnostics editor is currently updating the
|
||||
/// diagnostics.
|
||||
fn is_updating(&self, cx: &App) -> bool;
|
||||
/// Requests that the diagnostics editor stop updating the diagnostics.
|
||||
fn stop_updating(&self, cx: &mut App);
|
||||
/// Requests that the diagnostics editor updates the displayed diagnostics
|
||||
/// with the latest information.
|
||||
fn refresh_diagnostics(&self, window: &mut Window, cx: &mut App);
|
||||
/// Returns a list of diagnostics for the provided buffer id.
|
||||
fn get_diagnostics_for_buffer(
|
||||
&self,
|
||||
buffer_id: BufferId,
|
||||
cx: &App,
|
||||
) -> Vec<DiagnosticEntry<Anchor>>;
|
||||
}
|
||||
|
||||
impl Render for ToolbarControls {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let mut include_warnings = false;
|
||||
let mut is_updating = false;
|
||||
|
||||
match &self.editor {
|
||||
Some(editor) => {
|
||||
include_warnings = editor.include_warnings(cx);
|
||||
is_updating = editor.is_updating(cx);
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
|
||||
let is_agent_enabled = AgentSettings::get_global(cx).enabled(cx);
|
||||
|
||||
let (warning_tooltip, warning_color) = if include_warnings {
|
||||
("Exclude Warnings", Color::Warning)
|
||||
} else {
|
||||
("Include Warnings", Color::Disabled)
|
||||
};
|
||||
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child({
|
||||
IconButton::new("toggle_search", IconName::MagnifyingGlass)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip(Tooltip::for_action_title(
|
||||
"Buffer Search",
|
||||
&buffer_search::Deploy::find(),
|
||||
))
|
||||
.on_click(|_, window, cx| {
|
||||
window.dispatch_action(Box::new(buffer_search::Deploy::find()), cx);
|
||||
})
|
||||
})
|
||||
.when(is_agent_enabled, |this| {
|
||||
this.child(
|
||||
IconButton::new("inline_assist", IconName::ZedAssistant)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip(Tooltip::for_action_title(
|
||||
"Inline Assist",
|
||||
&InlineAssist::default(),
|
||||
))
|
||||
.on_click(|_, window, cx| {
|
||||
window.dispatch_action(Box::new(InlineAssist::default()), cx);
|
||||
}),
|
||||
)
|
||||
})
|
||||
.map(|div| {
|
||||
if is_updating {
|
||||
div.child(
|
||||
IconButton::new("stop-updating", IconName::Stop)
|
||||
.icon_color(Color::Error)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip(Tooltip::for_action_title(
|
||||
"Stop Diagnostics Update",
|
||||
&ToggleDiagnosticsRefresh,
|
||||
))
|
||||
.on_click(cx.listener(move |toolbar_controls, _, _, cx| {
|
||||
if let Some(editor) = toolbar_controls.editor() {
|
||||
editor.stop_updating(cx);
|
||||
cx.notify();
|
||||
}
|
||||
})),
|
||||
)
|
||||
} else {
|
||||
div.child(
|
||||
IconButton::new("refresh-diagnostics", IconName::ArrowCircle)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip(Tooltip::for_action_title(
|
||||
"Refresh Diagnostics",
|
||||
&ToggleDiagnosticsRefresh,
|
||||
))
|
||||
.on_click(cx.listener({
|
||||
move |toolbar_controls, _, window, cx| {
|
||||
if let Some(editor) = toolbar_controls.editor() {
|
||||
editor.refresh_diagnostics(window, cx)
|
||||
}
|
||||
}
|
||||
})),
|
||||
)
|
||||
}
|
||||
})
|
||||
.child(
|
||||
IconButton::new("toggle-warnings", IconName::Warning)
|
||||
.icon_color(warning_color)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip(Tooltip::text(warning_tooltip))
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
if let Some(editor) = &this.editor {
|
||||
editor.toggle_warnings(window, cx)
|
||||
}
|
||||
})),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<ToolbarItemEvent> for ToolbarControls {}
|
||||
|
||||
impl ToolbarItemView for ToolbarControls {
|
||||
fn set_active_pane_item(
|
||||
&mut self,
|
||||
active_pane_item: Option<&dyn ItemHandle>,
|
||||
_window: &mut Window,
|
||||
_: &mut Context<Self>,
|
||||
) -> ToolbarItemLocation {
|
||||
if let Some(pane_item) = active_pane_item.as_ref() {
|
||||
if let Some(editor) = pane_item.downcast::<ProjectDiagnosticsEditor>() {
|
||||
self.editor = Some(Box::new(editor.downgrade()));
|
||||
ToolbarItemLocation::PrimaryRight
|
||||
} else if let Some(editor) = pane_item.downcast::<BufferDiagnosticsEditor>() {
|
||||
self.editor = Some(Box::new(editor.downgrade()));
|
||||
ToolbarItemLocation::PrimaryRight
|
||||
} else {
|
||||
ToolbarItemLocation::Hidden
|
||||
}
|
||||
} else {
|
||||
ToolbarItemLocation::Hidden
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ToolbarControls {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolbarControls {
|
||||
pub fn new() -> Self {
|
||||
ToolbarControls { editor: None }
|
||||
}
|
||||
|
||||
fn editor(&self) -> Option<&dyn DiagnosticsToolbarEditor> {
|
||||
self.editor.as_deref()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user