Update block diagnostics (#28006)
Release Notes: - "Block" diagnostics (that show up in the diagnostics view, or when using `f8`/`shift-f8`) are rendered more clearly - `f8`/`shift-f8` now always go to the "next" or "prev" diagnostic, regardless of the state of the editor  --------- Co-authored-by: Kirill Bulatov <mail4score@gmail.com> Co-authored-by: Julia Ryan <juliaryan3.14@gmail.com>
This commit is contained in:
parent
ccf9aef767
commit
afabcd1547
17 changed files with 1794 additions and 1987 deletions
|
@ -15,17 +15,22 @@ doctest = false
|
|||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
collections.workspace = true
|
||||
component.workspace = true
|
||||
ctor.workspace = true
|
||||
editor.workspace = true
|
||||
env_logger.workspace = true
|
||||
gpui.workspace = true
|
||||
indoc.workspace = true
|
||||
language.workspace = true
|
||||
linkme.workspace = true
|
||||
log.workspace = true
|
||||
lsp.workspace = true
|
||||
markdown.workspace = true
|
||||
project.workspace = true
|
||||
rand.workspace = true
|
||||
serde.workspace = true
|
||||
settings.workspace = true
|
||||
text.workspace = true
|
||||
theme.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
|
@ -37,6 +42,7 @@ client = { workspace = true, features = ["test-support"] }
|
|||
editor = { workspace = true, features = ["test-support"] }
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
language = { workspace = true, features = ["test-support"] }
|
||||
markdown = { workspace = true, features = ["test-support"] }
|
||||
lsp = { workspace = true, features = ["test-support"] }
|
||||
serde_json.workspace = true
|
||||
theme = { workspace = true, features = ["test-support"] }
|
||||
|
|
302
crates/diagnostics/src/diagnostic_renderer.rs
Normal file
302
crates/diagnostics/src/diagnostic_renderer.rs
Normal file
|
@ -0,0 +1,302 @@
|
|||
use std::{ops::Range, sync::Arc};
|
||||
|
||||
use editor::{
|
||||
Anchor, Editor, EditorSnapshot, ToOffset,
|
||||
display_map::{BlockContext, BlockPlacement, BlockProperties, BlockStyle},
|
||||
hover_markdown_style,
|
||||
scroll::Autoscroll,
|
||||
};
|
||||
use gpui::{AppContext, Entity, Focusable, WeakEntity};
|
||||
use language::{BufferId, DiagnosticEntry};
|
||||
use lsp::DiagnosticSeverity;
|
||||
use markdown::{Markdown, MarkdownElement};
|
||||
use settings::Settings;
|
||||
use text::{AnchorRangeExt, Point};
|
||||
use theme::ThemeSettings;
|
||||
use ui::{
|
||||
ActiveTheme, AnyElement, App, Context, IntoElement, ParentElement, SharedString, Styled,
|
||||
Window, div, px,
|
||||
};
|
||||
use util::maybe;
|
||||
|
||||
use crate::ProjectDiagnosticsEditor;
|
||||
|
||||
pub struct DiagnosticRenderer;
|
||||
|
||||
impl DiagnosticRenderer {
|
||||
pub fn diagnostic_blocks_for_group(
|
||||
diagnostic_group: Vec<DiagnosticEntry<Point>>,
|
||||
buffer_id: BufferId,
|
||||
diagnostics_editor: Option<WeakEntity<ProjectDiagnosticsEditor>>,
|
||||
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].clone();
|
||||
let mut same_row = Vec::new();
|
||||
let mut close = Vec::new();
|
||||
let mut distant = Vec::new();
|
||||
let group_id = primary.diagnostic.group_id;
|
||||
for (ix, entry) in diagnostic_group.into_iter().enumerate() {
|
||||
if entry.diagnostic.is_primary {
|
||||
continue;
|
||||
}
|
||||
if entry.range.start.row == primary.range.start.row {
|
||||
same_row.push(entry)
|
||||
} else if entry.range.start.row.abs_diff(primary.range.start.row) < 5 {
|
||||
close.push(entry)
|
||||
} else {
|
||||
distant.push((ix, entry))
|
||||
}
|
||||
}
|
||||
|
||||
let mut markdown =
|
||||
Markdown::escape(&if let Some(source) = primary.diagnostic.source.as_ref() {
|
||||
format!("{}: {}", source, primary.diagnostic.message)
|
||||
} else {
|
||||
primary.diagnostic.message
|
||||
})
|
||||
.to_string();
|
||||
for entry in same_row {
|
||||
markdown.push_str("\n- hint: ");
|
||||
markdown.push_str(&Markdown::escape(&entry.diagnostic.message))
|
||||
}
|
||||
|
||||
for (ix, entry) in &distant {
|
||||
markdown.push_str("\n- hint: [");
|
||||
markdown.push_str(&Markdown::escape(&entry.diagnostic.message));
|
||||
markdown.push_str(&format!("](file://#diagnostic-{group_id}-{ix})\n",))
|
||||
}
|
||||
|
||||
let mut results = vec![DiagnosticBlock {
|
||||
initial_range: primary.range,
|
||||
severity: primary.diagnostic.severity,
|
||||
buffer_id,
|
||||
diagnostics_editor: diagnostics_editor.clone(),
|
||||
markdown: cx.new(|cx| Markdown::new(markdown.into(), None, None, cx)),
|
||||
}];
|
||||
|
||||
for entry in close {
|
||||
let markdown = if let Some(source) = entry.diagnostic.source.as_ref() {
|
||||
format!("{}: {}", source, entry.diagnostic.message)
|
||||
} else {
|
||||
entry.diagnostic.message
|
||||
};
|
||||
let markdown = Markdown::escape(&markdown).to_string();
|
||||
|
||||
results.push(DiagnosticBlock {
|
||||
initial_range: entry.range,
|
||||
severity: entry.diagnostic.severity,
|
||||
buffer_id,
|
||||
diagnostics_editor: diagnostics_editor.clone(),
|
||||
markdown: cx.new(|cx| Markdown::new(markdown.into(), None, None, cx)),
|
||||
});
|
||||
}
|
||||
|
||||
for (_, entry) in distant {
|
||||
let markdown = if let Some(source) = entry.diagnostic.source.as_ref() {
|
||||
format!("{}: {}", source, entry.diagnostic.message)
|
||||
} else {
|
||||
entry.diagnostic.message
|
||||
};
|
||||
let mut markdown = Markdown::escape(&markdown).to_string();
|
||||
markdown.push_str(&format!(
|
||||
" ([back](file://#diagnostic-{group_id}-{primary_ix}))"
|
||||
));
|
||||
// problem: group-id changes...
|
||||
// - only an issue in diagnostics because caching
|
||||
|
||||
results.push(DiagnosticBlock {
|
||||
initial_range: entry.range,
|
||||
severity: entry.diagnostic.severity,
|
||||
buffer_id,
|
||||
diagnostics_editor: diagnostics_editor.clone(),
|
||||
markdown: cx.new(|cx| Markdown::new(markdown.into(), None, None, cx)),
|
||||
});
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
}
|
||||
|
||||
impl editor::DiagnosticRenderer for DiagnosticRenderer {
|
||||
fn render_group(
|
||||
&self,
|
||||
diagnostic_group: Vec<DiagnosticEntry<Point>>,
|
||||
buffer_id: BufferId,
|
||||
snapshot: EditorSnapshot,
|
||||
editor: WeakEntity<Editor>,
|
||||
cx: &mut App,
|
||||
) -> Vec<BlockProperties<Anchor>> {
|
||||
let blocks = Self::diagnostic_blocks_for_group(diagnostic_group, buffer_id, None, 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()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct DiagnosticBlock {
|
||||
pub(crate) initial_range: Range<Point>,
|
||||
pub(crate) severity: DiagnosticSeverity,
|
||||
pub(crate) buffer_id: BufferId,
|
||||
pub(crate) markdown: Entity<Markdown>,
|
||||
pub(crate) diagnostics_editor: Option<WeakEntity<ProjectDiagnosticsEditor>>,
|
||||
}
|
||||
|
||||
impl DiagnosticBlock {
|
||||
pub fn render_block(&self, editor: WeakEntity<Editor>, bcx: &BlockContext) -> AnyElement {
|
||||
let cx = &bcx.app;
|
||||
let status_colors = bcx.app.theme().status();
|
||||
let max_width = px(600.);
|
||||
|
||||
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.info),
|
||||
_ => (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 buffer_id = self.buffer_id;
|
||||
let diagnostics_editor = self.diagnostics_editor.clone();
|
||||
|
||||
div()
|
||||
.border_l_2()
|
||||
.px_2()
|
||||
.line_height(line_height)
|
||||
.bg(background_color)
|
||||
.border_color(border_color)
|
||||
.max_w(max_width)
|
||||
.child(
|
||||
MarkdownElement::new(self.markdown.clone(), hover_markdown_style(bcx.window, cx))
|
||||
.on_url_click({
|
||||
move |link, window, cx| {
|
||||
Self::open_link(
|
||||
editor.clone(),
|
||||
&diagnostics_editor,
|
||||
link,
|
||||
window,
|
||||
buffer_id,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
}),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
pub fn open_link(
|
||||
editor: WeakEntity<Editor>,
|
||||
diagnostics_editor: &Option<WeakEntity<ProjectDiagnosticsEditor>>,
|
||||
link: SharedString,
|
||||
window: &mut Window,
|
||||
buffer_id: BufferId,
|
||||
cx: &mut App,
|
||||
) {
|
||||
editor
|
||||
.update(cx, |editor, cx| {
|
||||
let Some(diagnostic_link) = link.strip_prefix("file://#diagnostic-") else {
|
||||
editor::hover_popover::open_markdown_url(link, window, cx);
|
||||
return;
|
||||
};
|
||||
let Some((group_id, ix)) = maybe!({
|
||||
let (group_id, ix) = diagnostic_link.split_once('-')?;
|
||||
let group_id: usize = group_id.parse().ok()?;
|
||||
let ix: usize = ix.parse().ok()?;
|
||||
Some((group_id, ix))
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(diagnostics_editor) = diagnostics_editor {
|
||||
if let Some(diagnostic) = diagnostics_editor
|
||||
.update(cx, |diagnostics, _| {
|
||||
diagnostics
|
||||
.diagnostics
|
||||
.get(&buffer_id)
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.filter(|d| d.diagnostic.group_id == group_id)
|
||||
.nth(ix)
|
||||
})
|
||||
.ok()
|
||||
.flatten()
|
||||
{
|
||||
let multibuffer = editor.buffer().read(cx);
|
||||
let Some(snapshot) = multibuffer
|
||||
.buffer(buffer_id)
|
||||
.map(|entity| entity.read(cx).snapshot())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
for (excerpt_id, range) in multibuffer.excerpts_for_buffer(buffer_id, cx) {
|
||||
if range.context.overlaps(&diagnostic.range, &snapshot) {
|
||||
Self::jump_to(
|
||||
editor,
|
||||
Anchor::range_in_buffer(
|
||||
excerpt_id,
|
||||
buffer_id,
|
||||
diagnostic.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)
|
||||
}
|
||||
};
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
fn jump_to<T: ToOffset>(
|
||||
editor: &mut Editor,
|
||||
range: Range<T>,
|
||||
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(Some(Autoscroll::fit()), window, cx, |s| {
|
||||
s.select_ranges([range.start..range.start]);
|
||||
});
|
||||
window.focus(&editor.focus_handle(cx));
|
||||
}
|
||||
}
|
|
@ -1,38 +1,39 @@
|
|||
pub mod items;
|
||||
mod toolbar_controls;
|
||||
|
||||
mod diagnostic_renderer;
|
||||
|
||||
#[cfg(test)]
|
||||
mod diagnostics_tests;
|
||||
|
||||
use anyhow::Result;
|
||||
use collections::{BTreeSet, HashSet};
|
||||
use collections::{BTreeSet, HashMap};
|
||||
use diagnostic_renderer::DiagnosticBlock;
|
||||
use editor::{
|
||||
Editor, EditorEvent, ExcerptId, ExcerptRange, MultiBuffer, ToOffset, diagnostic_block_renderer,
|
||||
display_map::{BlockPlacement, BlockProperties, BlockStyle, CustomBlockId, RenderBlock},
|
||||
highlight_diagnostic_message,
|
||||
DEFAULT_MULTIBUFFER_CONTEXT, Editor, EditorEvent, ExcerptRange, MultiBuffer, PathKey,
|
||||
display_map::{BlockPlacement, BlockProperties, BlockStyle, CustomBlockId},
|
||||
scroll::Autoscroll,
|
||||
};
|
||||
use gpui::{
|
||||
AnyElement, AnyView, App, AsyncApp, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
Global, HighlightStyle, InteractiveElement, IntoElement, ParentElement, Render, SharedString,
|
||||
Styled, StyledText, Subscription, Task, WeakEntity, Window, actions, div, svg,
|
||||
Global, InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled,
|
||||
Subscription, Task, WeakEntity, Window, actions, div,
|
||||
};
|
||||
use language::{
|
||||
Bias, Buffer, BufferRow, BufferSnapshot, Diagnostic, DiagnosticEntry, DiagnosticSeverity,
|
||||
Point, Selection, SelectionGoal, ToTreeSitterPoint,
|
||||
Bias, Buffer, BufferRow, BufferSnapshot, DiagnosticEntry, Point, ToTreeSitterPoint,
|
||||
};
|
||||
use lsp::LanguageServerId;
|
||||
use lsp::DiagnosticSeverity;
|
||||
use project::{DiagnosticSummary, Project, ProjectPath, project_settings::ProjectSettings};
|
||||
use settings::Settings;
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
cmp,
|
||||
cmp::Ordering,
|
||||
mem,
|
||||
ops::{Range, RangeInclusive},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use text::{BufferId, OffsetRangeExt};
|
||||
use theme::ActiveTheme;
|
||||
pub use toolbar_controls::ToolbarControls;
|
||||
use ui::{Icon, IconName, Label, h_flex, prelude::*};
|
||||
|
@ -49,41 +50,28 @@ struct IncludeWarnings(bool);
|
|||
impl Global for IncludeWarnings {}
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
editor::set_diagnostic_renderer(diagnostic_renderer::DiagnosticRenderer {}, cx);
|
||||
cx.observe_new(ProjectDiagnosticsEditor::register).detach();
|
||||
}
|
||||
|
||||
struct ProjectDiagnosticsEditor {
|
||||
pub(crate) struct ProjectDiagnosticsEditor {
|
||||
project: Entity<Project>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
focus_handle: FocusHandle,
|
||||
editor: Entity<Editor>,
|
||||
diagnostics: HashMap<BufferId, Vec<DiagnosticEntry<text::Anchor>>>,
|
||||
blocks: HashMap<BufferId, Vec<CustomBlockId>>,
|
||||
summary: DiagnosticSummary,
|
||||
excerpts: Entity<MultiBuffer>,
|
||||
path_states: Vec<PathState>,
|
||||
paths_to_update: BTreeSet<(ProjectPath, Option<LanguageServerId>)>,
|
||||
multibuffer: Entity<MultiBuffer>,
|
||||
paths_to_update: BTreeSet<ProjectPath>,
|
||||
include_warnings: bool,
|
||||
context: u32,
|
||||
update_excerpts_task: Option<Task<Result<()>>>,
|
||||
_subscription: Subscription,
|
||||
}
|
||||
|
||||
struct PathState {
|
||||
path: ProjectPath,
|
||||
diagnostic_groups: Vec<DiagnosticGroupState>,
|
||||
}
|
||||
|
||||
struct DiagnosticGroupState {
|
||||
language_server_id: LanguageServerId,
|
||||
primary_diagnostic: DiagnosticEntry<language::Anchor>,
|
||||
primary_excerpt_ix: usize,
|
||||
excerpts: Vec<ExcerptId>,
|
||||
blocks: HashSet<CustomBlockId>,
|
||||
block_count: usize,
|
||||
}
|
||||
|
||||
impl EventEmitter<EditorEvent> for ProjectDiagnosticsEditor {}
|
||||
|
||||
const DIAGNOSTICS_UPDATE_DEBOUNCE: Duration = Duration::from_millis(50);
|
||||
const DIAGNOSTICS_UPDATE_DELAY: Duration = Duration::from_millis(50);
|
||||
|
||||
impl Render for ProjectDiagnosticsEditor {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
|
@ -149,8 +137,7 @@ impl ProjectDiagnosticsEditor {
|
|||
workspace.register_action(Self::deploy);
|
||||
}
|
||||
|
||||
fn new_with_context(
|
||||
context: u32,
|
||||
fn new(
|
||||
include_warnings: bool,
|
||||
project_handle: Entity<Project>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
|
@ -170,8 +157,7 @@ impl ProjectDiagnosticsEditor {
|
|||
language_server_id,
|
||||
path,
|
||||
} => {
|
||||
this.paths_to_update
|
||||
.insert((path.clone(), Some(*language_server_id)));
|
||||
this.paths_to_update.insert(path.clone());
|
||||
this.summary = project.read(cx).diagnostic_summary(false, cx);
|
||||
cx.emit(EditorEvent::TitleChanged);
|
||||
|
||||
|
@ -201,6 +187,7 @@ impl ProjectDiagnosticsEditor {
|
|||
Editor::for_multibuffer(excerpts.clone(), Some(project_handle.clone()), window, cx);
|
||||
editor.set_vertical_scroll_margin(5, cx);
|
||||
editor.disable_inline_diagnostics();
|
||||
editor.set_all_diagnostics_active(cx);
|
||||
editor
|
||||
});
|
||||
cx.subscribe_in(
|
||||
|
@ -210,7 +197,7 @@ impl ProjectDiagnosticsEditor {
|
|||
cx.emit(event.clone());
|
||||
match event {
|
||||
EditorEvent::Focused => {
|
||||
if this.path_states.is_empty() {
|
||||
if this.multibuffer.read(cx).is_empty() {
|
||||
window.focus(&this.focus_handle);
|
||||
}
|
||||
}
|
||||
|
@ -229,14 +216,14 @@ impl ProjectDiagnosticsEditor {
|
|||
let project = project_handle.read(cx);
|
||||
let mut this = Self {
|
||||
project: project_handle.clone(),
|
||||
context,
|
||||
summary: project.diagnostic_summary(false, cx),
|
||||
diagnostics: Default::default(),
|
||||
blocks: Default::default(),
|
||||
include_warnings,
|
||||
workspace,
|
||||
excerpts,
|
||||
multibuffer: excerpts,
|
||||
focus_handle,
|
||||
editor,
|
||||
path_states: Default::default(),
|
||||
paths_to_update: Default::default(),
|
||||
update_excerpts_task: None,
|
||||
_subscription: project_event_subscription,
|
||||
|
@ -252,15 +239,15 @@ impl ProjectDiagnosticsEditor {
|
|||
let project_handle = self.project.clone();
|
||||
self.update_excerpts_task = Some(cx.spawn_in(window, async move |this, cx| {
|
||||
cx.background_executor()
|
||||
.timer(DIAGNOSTICS_UPDATE_DEBOUNCE)
|
||||
.timer(DIAGNOSTICS_UPDATE_DELAY)
|
||||
.await;
|
||||
loop {
|
||||
let Some((path, language_server_id)) = this.update(cx, |this, _| {
|
||||
let Some((path, language_server_id)) = this.paths_to_update.pop_first() else {
|
||||
let Some(path) = this.update(cx, |this, _| {
|
||||
let Some(path) = this.paths_to_update.pop_first() else {
|
||||
this.update_excerpts_task.take();
|
||||
return None;
|
||||
};
|
||||
Some((path, language_server_id))
|
||||
Some(path)
|
||||
})?
|
||||
else {
|
||||
break;
|
||||
|
@ -272,7 +259,7 @@ impl ProjectDiagnosticsEditor {
|
|||
.log_err()
|
||||
{
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.update_excerpts(path, language_server_id, buffer, window, cx)
|
||||
this.update_excerpts(buffer, window, cx)
|
||||
})?
|
||||
.await?;
|
||||
}
|
||||
|
@ -281,23 +268,6 @@ impl ProjectDiagnosticsEditor {
|
|||
}));
|
||||
}
|
||||
|
||||
fn new(
|
||||
project_handle: Entity<Project>,
|
||||
include_warnings: bool,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
Self::new_with_context(
|
||||
editor::DEFAULT_MULTIBUFFER_CONTEXT,
|
||||
include_warnings,
|
||||
project_handle,
|
||||
workspace,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
fn deploy(
|
||||
workspace: &mut Workspace,
|
||||
_: &Deploy,
|
||||
|
@ -319,8 +289,8 @@ impl ProjectDiagnosticsEditor {
|
|||
|
||||
let diagnostics = cx.new(|cx| {
|
||||
ProjectDiagnosticsEditor::new(
|
||||
workspace.project().clone(),
|
||||
include_warnings,
|
||||
workspace.project().clone(),
|
||||
workspace_handle,
|
||||
window,
|
||||
cx,
|
||||
|
@ -338,7 +308,7 @@ impl ProjectDiagnosticsEditor {
|
|||
}
|
||||
|
||||
fn focus_in(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.focus_handle.is_focused(window) && !self.path_states.is_empty() {
|
||||
if self.focus_handle.is_focused(window) && !self.multibuffer.read(cx).is_empty() {
|
||||
self.editor.focus_handle(cx).focus(window)
|
||||
}
|
||||
}
|
||||
|
@ -356,396 +326,212 @@ impl ProjectDiagnosticsEditor {
|
|||
self.project.update(cx, |project, cx| {
|
||||
let mut paths = project
|
||||
.diagnostic_summaries(false, cx)
|
||||
.map(|(path, _, _)| (path, None))
|
||||
.map(|(path, _, _)| path)
|
||||
.collect::<BTreeSet<_>>();
|
||||
paths.extend(
|
||||
self.path_states
|
||||
.iter()
|
||||
.map(|state| (state.path.clone(), None)),
|
||||
);
|
||||
let paths_to_update = std::mem::take(&mut self.paths_to_update);
|
||||
paths.extend(paths_to_update.into_iter().map(|(path, _)| (path, None)));
|
||||
self.multibuffer.update(cx, |multibuffer, cx| {
|
||||
for buffer in multibuffer.all_buffers() {
|
||||
if let Some(file) = buffer.read(cx).file() {
|
||||
paths.insert(ProjectPath {
|
||||
path: file.path().clone(),
|
||||
worktree_id: file.worktree_id(cx),
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
self.paths_to_update = paths;
|
||||
});
|
||||
self.update_stale_excerpts(window, cx);
|
||||
}
|
||||
|
||||
fn diagnostics_are_unchanged(
|
||||
&self,
|
||||
existing: &Vec<DiagnosticEntry<text::Anchor>>,
|
||||
new: &Vec<DiagnosticEntry<text::Anchor>>,
|
||||
snapshot: &BufferSnapshot,
|
||||
) -> bool {
|
||||
if existing.len() != new.len() {
|
||||
return false;
|
||||
}
|
||||
existing.iter().zip(new.iter()).all(|(existing, new)| {
|
||||
existing.diagnostic.message == new.diagnostic.message
|
||||
&& existing.diagnostic.severity == new.diagnostic.severity
|
||||
&& existing.diagnostic.is_primary == new.diagnostic.is_primary
|
||||
&& existing.range.to_offset(snapshot) == new.range.to_offset(snapshot)
|
||||
})
|
||||
}
|
||||
|
||||
fn update_excerpts(
|
||||
&mut self,
|
||||
path_to_update: ProjectPath,
|
||||
server_to_update: Option<LanguageServerId>,
|
||||
buffer: Entity<Buffer>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let was_empty = self.path_states.is_empty();
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
let path_ix = match self
|
||||
.path_states
|
||||
.binary_search_by_key(&&path_to_update, |e| &e.path)
|
||||
{
|
||||
Ok(ix) => ix,
|
||||
Err(ix) => {
|
||||
self.path_states.insert(
|
||||
ix,
|
||||
PathState {
|
||||
path: path_to_update.clone(),
|
||||
diagnostic_groups: Default::default(),
|
||||
},
|
||||
);
|
||||
ix
|
||||
}
|
||||
};
|
||||
let mut prev_excerpt_id = if path_ix > 0 {
|
||||
let prev_path_last_group = &self.path_states[path_ix - 1]
|
||||
.diagnostic_groups
|
||||
.last()
|
||||
.unwrap();
|
||||
*prev_path_last_group.excerpts.last().unwrap()
|
||||
} else {
|
||||
ExcerptId::min()
|
||||
};
|
||||
|
||||
let mut new_group_ixs = Vec::new();
|
||||
let mut blocks_to_add = Vec::new();
|
||||
let mut blocks_to_remove = HashSet::default();
|
||||
let mut first_excerpt_id = None;
|
||||
let was_empty = self.multibuffer.read(cx).is_empty();
|
||||
let buffer_snapshot = buffer.read(cx).snapshot();
|
||||
let buffer_id = buffer_snapshot.remote_id();
|
||||
let max_severity = if self.include_warnings {
|
||||
DiagnosticSeverity::WARNING
|
||||
} else {
|
||||
DiagnosticSeverity::ERROR
|
||||
};
|
||||
let excerpts = self.excerpts.clone().downgrade();
|
||||
let context = self.context;
|
||||
let editor = self.editor.clone().downgrade();
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let mut old_groups = this
|
||||
.update(cx, |this, _| {
|
||||
mem::take(&mut this.path_states[path_ix].diagnostic_groups)
|
||||
})?
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.peekable();
|
||||
let mut new_groups = snapshot
|
||||
.diagnostic_groups(server_to_update)
|
||||
.into_iter()
|
||||
.filter(|(_, group)| {
|
||||
group.entries[group.primary_ix].diagnostic.severity <= max_severity
|
||||
})
|
||||
.peekable();
|
||||
loop {
|
||||
let mut to_insert = None;
|
||||
let mut to_remove = None;
|
||||
let mut to_keep = None;
|
||||
match (old_groups.peek(), new_groups.peek()) {
|
||||
(None, None) => break,
|
||||
(None, Some(_)) => to_insert = new_groups.next(),
|
||||
(Some((_, old_group)), None) => {
|
||||
if server_to_update.map_or(true, |id| id == old_group.language_server_id) {
|
||||
to_remove = old_groups.next();
|
||||
} else {
|
||||
to_keep = old_groups.next();
|
||||
}
|
||||
}
|
||||
(Some((_, old_group)), Some((new_language_server_id, new_group))) => {
|
||||
let old_primary = &old_group.primary_diagnostic;
|
||||
let new_primary = &new_group.entries[new_group.primary_ix];
|
||||
match compare_diagnostics(old_primary, new_primary, &snapshot)
|
||||
.then_with(|| old_group.language_server_id.cmp(new_language_server_id))
|
||||
{
|
||||
Ordering::Less => {
|
||||
if server_to_update
|
||||
.map_or(true, |id| id == old_group.language_server_id)
|
||||
{
|
||||
to_remove = old_groups.next();
|
||||
} else {
|
||||
to_keep = old_groups.next();
|
||||
}
|
||||
}
|
||||
Ordering::Equal => {
|
||||
to_keep = old_groups.next();
|
||||
new_groups.next();
|
||||
}
|
||||
Ordering::Greater => to_insert = new_groups.next(),
|
||||
}
|
||||
}
|
||||
|
||||
cx.spawn_in(window, async move |this, mut cx| {
|
||||
let diagnostics = buffer_snapshot
|
||||
.diagnostics_in_range::<_, text::Anchor>(
|
||||
Point::zero()..buffer_snapshot.max_point(),
|
||||
false,
|
||||
)
|
||||
.filter(|d| !(d.diagnostic.is_primary && d.diagnostic.is_unnecessary))
|
||||
.collect::<Vec<_>>();
|
||||
let unchanged = this.update(cx, |this, _| {
|
||||
if this.diagnostics.get(&buffer_id).is_some_and(|existing| {
|
||||
this.diagnostics_are_unchanged(existing, &diagnostics, &buffer_snapshot)
|
||||
}) {
|
||||
return true;
|
||||
}
|
||||
this.diagnostics.insert(buffer_id, diagnostics.clone());
|
||||
return false;
|
||||
})?;
|
||||
if unchanged {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some((language_server_id, group)) = to_insert {
|
||||
let mut group_state = DiagnosticGroupState {
|
||||
language_server_id,
|
||||
primary_diagnostic: group.entries[group.primary_ix].clone(),
|
||||
primary_excerpt_ix: 0,
|
||||
excerpts: Default::default(),
|
||||
blocks: Default::default(),
|
||||
block_count: 0,
|
||||
};
|
||||
let mut pending_range: Option<(Range<Point>, Range<Point>, usize)> = None;
|
||||
let mut is_first_excerpt_for_group = true;
|
||||
for (ix, entry) in group.entries.iter().map(Some).chain([None]).enumerate() {
|
||||
let resolved_entry = entry.map(|e| e.resolve::<Point>(&snapshot));
|
||||
let expanded_range = if let Some(entry) = &resolved_entry {
|
||||
Some(
|
||||
context_range_for_entry(
|
||||
entry.range.clone(),
|
||||
context,
|
||||
snapshot.clone(),
|
||||
(**cx).clone(),
|
||||
)
|
||||
.await,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some((range, context_range, start_ix)) = &mut pending_range {
|
||||
if let Some(expanded_range) = expanded_range.clone() {
|
||||
// If the entries are overlapping or next to each-other, merge them into one excerpt.
|
||||
if context_range.end.row + 1 >= expanded_range.start.row {
|
||||
context_range.end = context_range.end.max(expanded_range.end);
|
||||
continue;
|
||||
}
|
||||
let mut grouped: HashMap<usize, Vec<_>> = HashMap::default();
|
||||
for entry in diagnostics {
|
||||
grouped
|
||||
.entry(entry.diagnostic.group_id)
|
||||
.or_default()
|
||||
.push(DiagnosticEntry {
|
||||
range: entry.range.to_point(&buffer_snapshot),
|
||||
diagnostic: entry.diagnostic,
|
||||
})
|
||||
}
|
||||
let mut blocks: Vec<DiagnosticBlock> = Vec::new();
|
||||
|
||||
for (_, group) in grouped {
|
||||
let group_severity = group.iter().map(|d| d.diagnostic.severity).min();
|
||||
if group_severity.is_none_or(|s| s > max_severity) {
|
||||
continue;
|
||||
}
|
||||
let more = cx.update(|_, cx| {
|
||||
crate::diagnostic_renderer::DiagnosticRenderer::diagnostic_blocks_for_group(
|
||||
group,
|
||||
buffer_snapshot.remote_id(),
|
||||
Some(this.clone()),
|
||||
cx,
|
||||
)
|
||||
})?;
|
||||
|
||||
for item in more {
|
||||
let insert_pos = blocks
|
||||
.binary_search_by(|existing| {
|
||||
match existing.initial_range.start.cmp(&item.initial_range.start) {
|
||||
Ordering::Equal => item
|
||||
.initial_range
|
||||
.end
|
||||
.cmp(&existing.initial_range.end)
|
||||
.reverse(),
|
||||
other => other,
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|pos| pos);
|
||||
|
||||
let excerpt_id = excerpts.update(cx, |excerpts, cx| {
|
||||
excerpts
|
||||
.insert_excerpts_after(
|
||||
prev_excerpt_id,
|
||||
buffer.clone(),
|
||||
[ExcerptRange {
|
||||
context: context_range.clone(),
|
||||
primary: range.clone(),
|
||||
}],
|
||||
cx,
|
||||
)
|
||||
.pop()
|
||||
.unwrap()
|
||||
})?;
|
||||
|
||||
prev_excerpt_id = excerpt_id;
|
||||
first_excerpt_id.get_or_insert(prev_excerpt_id);
|
||||
group_state.excerpts.push(excerpt_id);
|
||||
let header_position = (excerpt_id, language::Anchor::MIN);
|
||||
|
||||
if is_first_excerpt_for_group {
|
||||
is_first_excerpt_for_group = false;
|
||||
let mut primary =
|
||||
group.entries[group.primary_ix].diagnostic.clone();
|
||||
primary.message =
|
||||
primary.message.split('\n').next().unwrap().to_string();
|
||||
group_state.block_count += 1;
|
||||
blocks_to_add.push(BlockProperties {
|
||||
placement: BlockPlacement::Above(header_position),
|
||||
height: Some(2),
|
||||
style: BlockStyle::Sticky,
|
||||
render: diagnostic_header_renderer(primary),
|
||||
priority: 0,
|
||||
});
|
||||
}
|
||||
|
||||
for entry in &group.entries[*start_ix..ix] {
|
||||
let mut diagnostic = entry.diagnostic.clone();
|
||||
if diagnostic.is_primary {
|
||||
group_state.primary_excerpt_ix = group_state.excerpts.len() - 1;
|
||||
diagnostic.message =
|
||||
entry.diagnostic.message.split('\n').skip(1).collect();
|
||||
}
|
||||
|
||||
if !diagnostic.message.is_empty() {
|
||||
group_state.block_count += 1;
|
||||
blocks_to_add.push(BlockProperties {
|
||||
placement: BlockPlacement::Below((
|
||||
excerpt_id,
|
||||
entry.range.start,
|
||||
)),
|
||||
height: Some(
|
||||
diagnostic.message.matches('\n').count() as u32 + 1,
|
||||
),
|
||||
style: BlockStyle::Fixed,
|
||||
render: diagnostic_block_renderer(diagnostic, None, true),
|
||||
priority: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pending_range.take();
|
||||
}
|
||||
|
||||
if let Some(entry) = resolved_entry.as_ref() {
|
||||
let range = entry.range.clone();
|
||||
pending_range = Some((range, expanded_range.unwrap(), ix));
|
||||
}
|
||||
}
|
||||
|
||||
this.update(cx, |this, _| {
|
||||
new_group_ixs.push(this.path_states[path_ix].diagnostic_groups.len());
|
||||
this.path_states[path_ix]
|
||||
.diagnostic_groups
|
||||
.push(group_state);
|
||||
})?;
|
||||
} else if let Some((_, group_state)) = to_remove {
|
||||
excerpts.update(cx, |excerpts, cx| {
|
||||
excerpts.remove_excerpts(group_state.excerpts.iter().copied(), cx)
|
||||
})?;
|
||||
blocks_to_remove.extend(group_state.blocks.iter().copied());
|
||||
} else if let Some((_, group_state)) = to_keep {
|
||||
prev_excerpt_id = *group_state.excerpts.last().unwrap();
|
||||
first_excerpt_id.get_or_insert(prev_excerpt_id);
|
||||
|
||||
this.update(cx, |this, _| {
|
||||
this.path_states[path_ix]
|
||||
.diagnostic_groups
|
||||
.push(group_state)
|
||||
})?;
|
||||
blocks.insert(insert_pos, item);
|
||||
}
|
||||
}
|
||||
|
||||
let excerpts_snapshot = excerpts.update(cx, |excerpts, cx| excerpts.snapshot(cx))?;
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.remove_blocks(blocks_to_remove, None, cx);
|
||||
let block_ids = editor.insert_blocks(
|
||||
blocks_to_add.into_iter().flat_map(|block| {
|
||||
let placement = match block.placement {
|
||||
BlockPlacement::Above((excerpt_id, text_anchor)) => {
|
||||
BlockPlacement::Above(
|
||||
excerpts_snapshot.anchor_in_excerpt(excerpt_id, text_anchor)?,
|
||||
)
|
||||
}
|
||||
BlockPlacement::Below((excerpt_id, text_anchor)) => {
|
||||
BlockPlacement::Below(
|
||||
excerpts_snapshot.anchor_in_excerpt(excerpt_id, text_anchor)?,
|
||||
)
|
||||
}
|
||||
BlockPlacement::Replace(_) | BlockPlacement::Near(_) => {
|
||||
unreachable!(
|
||||
"no Near/Replace block should have been pushed to blocks_to_add"
|
||||
)
|
||||
}
|
||||
};
|
||||
Some(BlockProperties {
|
||||
placement,
|
||||
height: block.height,
|
||||
style: block.style,
|
||||
render: block.render,
|
||||
priority: 0,
|
||||
})
|
||||
}),
|
||||
Some(Autoscroll::fit()),
|
||||
cx,
|
||||
);
|
||||
|
||||
let mut block_ids = block_ids.into_iter();
|
||||
this.update(cx, |this, _| {
|
||||
for ix in new_group_ixs {
|
||||
let group_state = &mut this.path_states[path_ix].diagnostic_groups[ix];
|
||||
group_state.blocks =
|
||||
block_ids.by_ref().take(group_state.block_count).collect();
|
||||
}
|
||||
})?;
|
||||
Result::<(), anyhow::Error>::Ok(())
|
||||
})??;
|
||||
let mut excerpt_ranges: Vec<ExcerptRange<Point>> = Vec::new();
|
||||
for b in blocks.iter() {
|
||||
let excerpt_range = context_range_for_entry(
|
||||
b.initial_range.clone(),
|
||||
DEFAULT_MULTIBUFFER_CONTEXT,
|
||||
buffer_snapshot.clone(),
|
||||
&mut cx,
|
||||
)
|
||||
.await;
|
||||
excerpt_ranges.push(ExcerptRange {
|
||||
context: excerpt_range,
|
||||
primary: b.initial_range.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
if this.path_states[path_ix].diagnostic_groups.is_empty() {
|
||||
this.path_states.remove(path_ix);
|
||||
if let Some(block_ids) = this.blocks.remove(&buffer_id) {
|
||||
this.editor.update(cx, |editor, cx| {
|
||||
editor.display_map.update(cx, |display_map, cx| {
|
||||
display_map.remove_blocks(block_ids.into_iter().collect(), cx)
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
this.editor.update(cx, |editor, cx| {
|
||||
let groups;
|
||||
let mut selections;
|
||||
let new_excerpt_ids_by_selection_id;
|
||||
if was_empty {
|
||||
groups = this.path_states.first()?.diagnostic_groups.as_slice();
|
||||
new_excerpt_ids_by_selection_id =
|
||||
[(0, ExcerptId::min())].into_iter().collect();
|
||||
selections = vec![Selection {
|
||||
id: 0,
|
||||
start: 0,
|
||||
end: 0,
|
||||
reversed: false,
|
||||
goal: SelectionGoal::None,
|
||||
}];
|
||||
} else {
|
||||
groups = this.path_states.get(path_ix)?.diagnostic_groups.as_slice();
|
||||
new_excerpt_ids_by_selection_id =
|
||||
editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
|
||||
s.refresh()
|
||||
});
|
||||
selections = editor.selections.all::<usize>(cx);
|
||||
}
|
||||
|
||||
// If any selection has lost its position, move it to start of the next primary diagnostic.
|
||||
let snapshot = editor.snapshot(window, cx);
|
||||
for selection in &mut selections {
|
||||
if let Some(new_excerpt_id) =
|
||||
new_excerpt_ids_by_selection_id.get(&selection.id)
|
||||
{
|
||||
let group_ix = match groups.binary_search_by(|probe| {
|
||||
probe
|
||||
.excerpts
|
||||
.last()
|
||||
.unwrap()
|
||||
.cmp(new_excerpt_id, &snapshot.buffer_snapshot)
|
||||
}) {
|
||||
Ok(ix) | Err(ix) => ix,
|
||||
};
|
||||
if let Some(group) = groups.get(group_ix) {
|
||||
if let Some(offset) = excerpts_snapshot
|
||||
.anchor_in_excerpt(
|
||||
group.excerpts[group.primary_excerpt_ix],
|
||||
group.primary_diagnostic.range.start,
|
||||
)
|
||||
.map(|anchor| anchor.to_offset(&excerpts_snapshot))
|
||||
{
|
||||
selection.start = offset;
|
||||
selection.end = offset;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
editor.change_selections(None, window, cx, |s| {
|
||||
s.select(selections);
|
||||
});
|
||||
Some(())
|
||||
let (anchor_ranges, _) = this.multibuffer.update(cx, |multi_buffer, cx| {
|
||||
multi_buffer.set_excerpt_ranges_for_path(
|
||||
PathKey::for_buffer(&buffer, cx),
|
||||
buffer.clone(),
|
||||
&buffer_snapshot,
|
||||
excerpt_ranges,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
})?;
|
||||
#[cfg(test)]
|
||||
let cloned_blocks = blocks.clone();
|
||||
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
if this.path_states.is_empty() {
|
||||
if this.editor.focus_handle(cx).is_focused(window) {
|
||||
window.focus(&this.focus_handle);
|
||||
if was_empty {
|
||||
if let Some(anchor_range) = anchor_ranges.first() {
|
||||
let range_to_select = anchor_range.start..anchor_range.start;
|
||||
this.editor.update(cx, |editor, cx| {
|
||||
editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
|
||||
s.select_anchor_ranges([range_to_select]);
|
||||
})
|
||||
})
|
||||
}
|
||||
} else if this.focus_handle.is_focused(window) {
|
||||
let focus_handle = this.editor.focus_handle(cx);
|
||||
window.focus(&focus_handle);
|
||||
}
|
||||
|
||||
let editor_blocks =
|
||||
anchor_ranges
|
||||
.into_iter()
|
||||
.zip(blocks.into_iter())
|
||||
.map(|(anchor, block)| {
|
||||
let editor = this.editor.downgrade();
|
||||
BlockProperties {
|
||||
placement: BlockPlacement::Near(anchor.start),
|
||||
height: Some(1),
|
||||
style: BlockStyle::Flex,
|
||||
render: Arc::new(move |bcx| {
|
||||
block.render_block(editor.clone(), bcx)
|
||||
}),
|
||||
priority: 1,
|
||||
}
|
||||
});
|
||||
let block_ids = this.editor.update(cx, |editor, cx| {
|
||||
editor.display_map.update(cx, |display_map, cx| {
|
||||
display_map.insert_blocks(editor_blocks, cx)
|
||||
})
|
||||
});
|
||||
|
||||
#[cfg(test)]
|
||||
this.check_invariants(cx);
|
||||
{
|
||||
for (block_id, block) in block_ids.iter().zip(cloned_blocks.iter()) {
|
||||
let markdown = block.markdown.clone();
|
||||
editor::test::set_block_content_for_tests(
|
||||
&this.editor,
|
||||
*block_id,
|
||||
cx,
|
||||
move |cx| {
|
||||
markdown::MarkdownElement::rendered_text(
|
||||
markdown.clone(),
|
||||
cx,
|
||||
editor::hover_markdown_style,
|
||||
)
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
this.blocks.insert(buffer_id, block_ids);
|
||||
cx.notify()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn check_invariants(&self, cx: &mut Context<Self>) {
|
||||
let mut excerpts = Vec::new();
|
||||
for (id, buffer, _) in self.excerpts.read(cx).snapshot(cx).excerpts() {
|
||||
if let Some(file) = buffer.file() {
|
||||
excerpts.push((id, file.path().clone()));
|
||||
}
|
||||
}
|
||||
|
||||
let mut prev_path = None;
|
||||
for (_, path) in &excerpts {
|
||||
if let Some(prev_path) = prev_path {
|
||||
if path < prev_path {
|
||||
panic!("excerpts are not sorted by path {:?}", excerpts);
|
||||
}
|
||||
}
|
||||
prev_path = Some(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Focusable for ProjectDiagnosticsEditor {
|
||||
|
@ -857,8 +643,8 @@ impl Item for ProjectDiagnosticsEditor {
|
|||
{
|
||||
Some(cx.new(|cx| {
|
||||
ProjectDiagnosticsEditor::new(
|
||||
self.project.clone(),
|
||||
self.include_warnings,
|
||||
self.project.clone(),
|
||||
self.workspace.clone(),
|
||||
window,
|
||||
cx,
|
||||
|
@ -867,15 +653,15 @@ impl Item for ProjectDiagnosticsEditor {
|
|||
}
|
||||
|
||||
fn is_dirty(&self, cx: &App) -> bool {
|
||||
self.excerpts.read(cx).is_dirty(cx)
|
||||
self.multibuffer.read(cx).is_dirty(cx)
|
||||
}
|
||||
|
||||
fn has_deleted_file(&self, cx: &App) -> bool {
|
||||
self.excerpts.read(cx).has_deleted_file(cx)
|
||||
self.multibuffer.read(cx).has_deleted_file(cx)
|
||||
}
|
||||
|
||||
fn has_conflict(&self, cx: &App) -> bool {
|
||||
self.excerpts.read(cx).has_conflict(cx)
|
||||
self.multibuffer.read(cx).has_conflict(cx)
|
||||
}
|
||||
|
||||
fn can_save(&self, _: &App) -> bool {
|
||||
|
@ -950,128 +736,31 @@ impl Item for ProjectDiagnosticsEditor {
|
|||
}
|
||||
}
|
||||
|
||||
const DIAGNOSTIC_HEADER: &str = "diagnostic header";
|
||||
|
||||
fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
|
||||
let (message, code_ranges) = highlight_diagnostic_message(&diagnostic, None);
|
||||
let message: SharedString = message;
|
||||
Arc::new(move |cx| {
|
||||
let color = cx.theme().colors();
|
||||
let highlight_style: HighlightStyle = color.text_accent.into();
|
||||
|
||||
h_flex()
|
||||
.id(DIAGNOSTIC_HEADER)
|
||||
.block_mouse_down()
|
||||
.h(2. * cx.window.line_height())
|
||||
.w_full()
|
||||
.px_9()
|
||||
.justify_between()
|
||||
.gap_2()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.px_1()
|
||||
.rounded_sm()
|
||||
.bg(color.surface_background.opacity(0.5))
|
||||
.map(|stack| {
|
||||
stack.child(
|
||||
svg()
|
||||
.size(cx.window.text_style().font_size)
|
||||
.flex_none()
|
||||
.map(|icon| {
|
||||
if diagnostic.severity == DiagnosticSeverity::ERROR {
|
||||
icon.path(IconName::XCircle.path())
|
||||
.text_color(Color::Error.color(cx))
|
||||
} else {
|
||||
icon.path(IconName::Warning.path())
|
||||
.text_color(Color::Warning.color(cx))
|
||||
}
|
||||
}),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
StyledText::new(message.clone()).with_default_highlights(
|
||||
&cx.window.text_style(),
|
||||
code_ranges
|
||||
.iter()
|
||||
.map(|range| (range.clone(), highlight_style)),
|
||||
),
|
||||
)
|
||||
.when_some(diagnostic.code.as_ref(), |stack, code| {
|
||||
stack.child(
|
||||
div()
|
||||
.child(SharedString::from(format!("({code:?})")))
|
||||
.text_color(color.text_muted),
|
||||
)
|
||||
}),
|
||||
),
|
||||
)
|
||||
.when_some(diagnostic.source.as_ref(), |stack, source| {
|
||||
stack.child(
|
||||
div()
|
||||
.child(SharedString::from(source.clone()))
|
||||
.text_color(color.text_muted),
|
||||
)
|
||||
})
|
||||
.into_any_element()
|
||||
})
|
||||
}
|
||||
|
||||
fn compare_diagnostics(
|
||||
old: &DiagnosticEntry<language::Anchor>,
|
||||
new: &DiagnosticEntry<language::Anchor>,
|
||||
snapshot: &language::BufferSnapshot,
|
||||
) -> Ordering {
|
||||
use language::ToOffset;
|
||||
|
||||
// The diagnostics may point to a previously open Buffer for this file.
|
||||
if !old.range.start.is_valid(snapshot) || !new.range.start.is_valid(snapshot) {
|
||||
return Ordering::Greater;
|
||||
}
|
||||
|
||||
old.range
|
||||
.start
|
||||
.to_offset(snapshot)
|
||||
.cmp(&new.range.start.to_offset(snapshot))
|
||||
.then_with(|| {
|
||||
old.range
|
||||
.end
|
||||
.to_offset(snapshot)
|
||||
.cmp(&new.range.end.to_offset(snapshot))
|
||||
})
|
||||
.then_with(|| old.diagnostic.message.cmp(&new.diagnostic.message))
|
||||
}
|
||||
|
||||
const DIAGNOSTIC_EXPANSION_ROW_LIMIT: u32 = 32;
|
||||
|
||||
fn context_range_for_entry(
|
||||
async fn context_range_for_entry(
|
||||
range: Range<Point>,
|
||||
context: u32,
|
||||
snapshot: BufferSnapshot,
|
||||
cx: AsyncApp,
|
||||
) -> Task<Range<Point>> {
|
||||
cx.spawn(async move |cx| {
|
||||
if let Some(rows) = heuristic_syntactic_expand(
|
||||
range.clone(),
|
||||
DIAGNOSTIC_EXPANSION_ROW_LIMIT,
|
||||
snapshot.clone(),
|
||||
cx,
|
||||
)
|
||||
.await
|
||||
{
|
||||
return Range {
|
||||
start: Point::new(*rows.start(), 0),
|
||||
end: snapshot.clip_point(Point::new(*rows.end(), u32::MAX), Bias::Left),
|
||||
};
|
||||
}
|
||||
Range {
|
||||
start: Point::new(range.start.row.saturating_sub(context), 0),
|
||||
end: snapshot.clip_point(Point::new(range.end.row + context, u32::MAX), Bias::Left),
|
||||
}
|
||||
})
|
||||
cx: &mut AsyncApp,
|
||||
) -> Range<Point> {
|
||||
if let Some(rows) = heuristic_syntactic_expand(
|
||||
range.clone(),
|
||||
DIAGNOSTIC_EXPANSION_ROW_LIMIT,
|
||||
snapshot.clone(),
|
||||
cx,
|
||||
)
|
||||
.await
|
||||
{
|
||||
return Range {
|
||||
start: Point::new(*rows.start(), 0),
|
||||
end: snapshot.clip_point(Point::new(*rows.end(), u32::MAX), Bias::Left),
|
||||
};
|
||||
}
|
||||
Range {
|
||||
start: Point::new(range.start.row.saturating_sub(context), 0),
|
||||
end: snapshot.clip_point(Point::new(range.end.row + context, u32::MAX), Bias::Left),
|
||||
}
|
||||
}
|
||||
|
||||
/// Expands the input range using syntax information from TreeSitter. This expansion will be limited
|
||||
|
|
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue