Add initial inline diagnostics support (#25297)
https://github.com/user-attachments/assets/eb881707-e575-47ef-9ae0-67d8085d8065 Closes https://github.com/zed-industries/zed/pull/22668 Closes https://github.com/zed-industries/zed/issues/4901 Takes https://github.com/zed-industries/zed/pull/22668 and fixes all review items on top. Inline diagnostics are disabled by default, but can be enabled via settings permanently, or temporarily toggled with the `editor: ToggleInlineDiagnostics` action and the corresponding editor menu item <img width="242" alt="image" src="https://github.com/user-attachments/assets/8e177511-4626-4434-902b-d6aa4d3fafd0" /> Inline diagnostics does not show currently active diagnostics group, as it gets inline into the editor too, inside the text. Inline git blame takes precedence and is shown instead of the diagnostics, edit predictions dim the diagnostics if located on the same line. One notable drawback of the implementation is the inability to wrap, making inline diagnostics cut off the right side:  (same as inline git blame and other elements to the right of the text) Given that it's disabled by default and go to next/prev diagnostics will show them better, seems fine to leave in the first iteration. Release Notes: - Added initial inline diagnostics support --------- Co-authored-by: Paul J. Davis <paul.davis@tiledb.com> Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
This commit is contained in:
parent
74c581b9f4
commit
5ae93ce68d
11 changed files with 579 additions and 81 deletions
|
@ -52,7 +52,7 @@ use multi_buffer::{
|
|||
Anchor, ExcerptId, ExcerptInfo, ExpandExcerptDirection, MultiBufferPoint, MultiBufferRow,
|
||||
RowInfo, ToOffset,
|
||||
};
|
||||
use project::project_settings::{GitGutterSetting, ProjectSettings};
|
||||
use project::project_settings::{self, GitGutterSetting, ProjectSettings};
|
||||
use settings::Settings;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use std::{
|
||||
|
@ -403,6 +403,7 @@ impl EditorElement {
|
|||
register_action(editor, window, Editor::toggle_indent_guides);
|
||||
register_action(editor, window, Editor::toggle_inlay_hints);
|
||||
register_action(editor, window, Editor::toggle_inline_completions);
|
||||
register_action(editor, window, Editor::toggle_inline_diagnostics);
|
||||
register_action(editor, window, hover_popover::hover);
|
||||
register_action(editor, window, Editor::reveal_in_finder);
|
||||
register_action(editor, window, Editor::copy_path);
|
||||
|
@ -1610,6 +1611,157 @@ impl EditorElement {
|
|||
display_hunks
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn layout_inline_diagnostics(
|
||||
&self,
|
||||
line_layouts: &[LineWithInvisibles],
|
||||
crease_trailers: &[Option<CreaseTrailerLayout>],
|
||||
content_origin: gpui::Point<Pixels>,
|
||||
scroll_pixel_position: gpui::Point<Pixels>,
|
||||
inline_completion_popover_origin: Option<gpui::Point<Pixels>>,
|
||||
start_row: DisplayRow,
|
||||
end_row: DisplayRow,
|
||||
line_height: Pixels,
|
||||
em_width: Pixels,
|
||||
style: &EditorStyle,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> HashMap<DisplayRow, AnyElement> {
|
||||
let max_severity = ProjectSettings::get_global(cx)
|
||||
.diagnostics
|
||||
.inline
|
||||
.max_severity
|
||||
.map_or(DiagnosticSeverity::HINT, |severity| match severity {
|
||||
project_settings::DiagnosticSeverity::Error => DiagnosticSeverity::ERROR,
|
||||
project_settings::DiagnosticSeverity::Warning => DiagnosticSeverity::WARNING,
|
||||
project_settings::DiagnosticSeverity::Info => DiagnosticSeverity::INFORMATION,
|
||||
project_settings::DiagnosticSeverity::Hint => DiagnosticSeverity::HINT,
|
||||
});
|
||||
|
||||
let active_diagnostics_group = self
|
||||
.editor
|
||||
.read(cx)
|
||||
.active_diagnostics
|
||||
.as_ref()
|
||||
.map(|active_diagnostics| active_diagnostics.group_id);
|
||||
|
||||
let diagnostics_by_rows = self.editor.update(cx, |editor, cx| {
|
||||
let snapshot = editor.snapshot(window, cx);
|
||||
editor
|
||||
.inline_diagnostics
|
||||
.iter()
|
||||
.filter(|(_, diagnostic)| diagnostic.severity <= max_severity)
|
||||
.filter(|(_, diagnostic)| match active_diagnostics_group {
|
||||
Some(active_diagnostics_group) => {
|
||||
// Active diagnostics are all shown in the editor already, no need to display them inline
|
||||
diagnostic.group_id != active_diagnostics_group
|
||||
}
|
||||
None => true,
|
||||
})
|
||||
.map(|(point, diag)| (point.to_display_point(&snapshot), diag.clone()))
|
||||
.skip_while(|(point, _)| point.row() < start_row)
|
||||
.take_while(|(point, _)| point.row() < end_row)
|
||||
.fold(HashMap::default(), |mut acc, (point, diagnostic)| {
|
||||
acc.entry(point.row())
|
||||
.or_insert_with(Vec::new)
|
||||
.push(diagnostic);
|
||||
acc
|
||||
})
|
||||
});
|
||||
|
||||
if diagnostics_by_rows.is_empty() {
|
||||
return HashMap::default();
|
||||
}
|
||||
|
||||
let severity_to_color = |sev: &DiagnosticSeverity| match sev {
|
||||
&DiagnosticSeverity::ERROR => Color::Error,
|
||||
&DiagnosticSeverity::WARNING => Color::Warning,
|
||||
&DiagnosticSeverity::INFORMATION => Color::Info,
|
||||
&DiagnosticSeverity::HINT => Color::Hint,
|
||||
_ => Color::Error,
|
||||
};
|
||||
|
||||
let padding = ProjectSettings::get_global(cx).diagnostics.inline.padding as f32 * em_width;
|
||||
let min_x = ProjectSettings::get_global(cx)
|
||||
.diagnostics
|
||||
.inline
|
||||
.min_column as f32
|
||||
* em_width;
|
||||
|
||||
let mut elements = HashMap::default();
|
||||
for (row, mut diagnostics) in diagnostics_by_rows {
|
||||
diagnostics.sort_by_key(|diagnostic| {
|
||||
(
|
||||
diagnostic.severity,
|
||||
std::cmp::Reverse(diagnostic.is_primary),
|
||||
diagnostic.start.row,
|
||||
diagnostic.start.column,
|
||||
)
|
||||
});
|
||||
|
||||
let Some(diagnostic_to_render) = diagnostics
|
||||
.iter()
|
||||
.find(|diagnostic| diagnostic.is_primary)
|
||||
.or_else(|| diagnostics.first())
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let pos_y = content_origin.y
|
||||
+ line_height * (row.0 as f32 - scroll_pixel_position.y / line_height);
|
||||
|
||||
let window_ix = row.minus(start_row) as usize;
|
||||
let pos_x = {
|
||||
let crease_trailer_layout = &crease_trailers[window_ix];
|
||||
let line_layout = &line_layouts[window_ix];
|
||||
|
||||
let line_end = if let Some(crease_trailer) = crease_trailer_layout {
|
||||
crease_trailer.bounds.right()
|
||||
} else {
|
||||
content_origin.x - scroll_pixel_position.x + line_layout.width
|
||||
};
|
||||
|
||||
let padded_line = line_end + padding;
|
||||
let min_start = content_origin.x - scroll_pixel_position.x + min_x;
|
||||
|
||||
cmp::max(padded_line, min_start)
|
||||
};
|
||||
|
||||
let behind_inline_completion_popover = inline_completion_popover_origin
|
||||
.as_ref()
|
||||
.map_or(false, |inline_completion_popover_origin| {
|
||||
(pos_y..pos_y + line_height).contains(&inline_completion_popover_origin.y)
|
||||
});
|
||||
let opacity = if behind_inline_completion_popover {
|
||||
0.5
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
|
||||
let mut element = h_flex()
|
||||
.id(("diagnostic", row.0))
|
||||
.h(line_height)
|
||||
.w_full()
|
||||
.px_1()
|
||||
.rounded_sm()
|
||||
.opacity(opacity)
|
||||
.bg(severity_to_color(&diagnostic_to_render.severity)
|
||||
.color(cx)
|
||||
.opacity(0.05))
|
||||
.text_color(severity_to_color(&diagnostic_to_render.severity).color(cx))
|
||||
.text_sm()
|
||||
.font_family(style.text.font().family)
|
||||
.child(diagnostic_to_render.message.clone())
|
||||
.into_any();
|
||||
|
||||
element.prepaint_as_root(point(pos_x, pos_y), AvailableSpace::min_size(), window, cx);
|
||||
|
||||
elements.insert(row, element);
|
||||
}
|
||||
|
||||
elements
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn layout_inline_blame(
|
||||
&self,
|
||||
|
@ -3573,7 +3725,7 @@ impl EditorElement {
|
|||
style: &EditorStyle,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Option<AnyElement> {
|
||||
) -> Option<(AnyElement, gpui::Point<Pixels>)> {
|
||||
const PADDING_X: Pixels = Pixels(24.);
|
||||
const PADDING_Y: Pixels = Pixels(2.);
|
||||
|
||||
|
@ -3626,7 +3778,7 @@ impl EditorElement {
|
|||
let origin = start_point + point(cursor_character_x, PADDING_Y);
|
||||
|
||||
element.prepaint_at(origin, window, cx);
|
||||
return Some(element);
|
||||
return Some((element, origin));
|
||||
} else if target_display_point.row() >= visible_row_range.end {
|
||||
let mut element = editor
|
||||
.render_edit_prediction_line_popover(
|
||||
|
@ -3654,7 +3806,7 @@ impl EditorElement {
|
|||
);
|
||||
|
||||
element.prepaint_at(origin, window, cx);
|
||||
return Some(element);
|
||||
return Some((element, origin));
|
||||
} else {
|
||||
const POLE_WIDTH: Pixels = px(2.);
|
||||
|
||||
|
@ -3694,7 +3846,7 @@ impl EditorElement {
|
|||
|
||||
element.prepaint_at(origin, window, cx);
|
||||
|
||||
return Some(element);
|
||||
return Some((element, origin));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3711,8 +3863,9 @@ impl EditorElement {
|
|||
let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
|
||||
let offset = point((text_bounds.size.width - size.width) / 2., PADDING_Y);
|
||||
|
||||
element.prepaint_at(text_bounds.origin + offset, window, cx);
|
||||
Some(element)
|
||||
let origin = text_bounds.origin + offset;
|
||||
element.prepaint_at(origin, window, cx);
|
||||
Some((element, origin))
|
||||
} else if (target_display_point.row().as_f32() + 1.) > scroll_bottom {
|
||||
let mut element = editor
|
||||
.render_edit_prediction_line_popover(
|
||||
|
@ -3729,8 +3882,9 @@ impl EditorElement {
|
|||
text_bounds.size.height - size.height - PADDING_Y,
|
||||
);
|
||||
|
||||
element.prepaint_at(text_bounds.origin + offset, window, cx);
|
||||
Some(element)
|
||||
let origin = text_bounds.origin + offset;
|
||||
element.prepaint_at(origin, window, cx);
|
||||
Some((element, origin))
|
||||
} else {
|
||||
let mut element = editor
|
||||
.render_edit_prediction_line_popover("Jump to Edit", None, window, cx)?
|
||||
|
@ -3743,13 +3897,9 @@ impl EditorElement {
|
|||
editor.display_to_pixel_point(target_line_end, editor_snapshot, window)
|
||||
})?;
|
||||
|
||||
element.prepaint_as_root(
|
||||
clamp_start(start_point + origin + point(PADDING_X, px(0.))),
|
||||
AvailableSpace::min_size(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
Some(element)
|
||||
let origin = clamp_start(start_point + origin + point(PADDING_X, px(0.)));
|
||||
element.prepaint_as_root(origin, AvailableSpace::min_size(), window, cx);
|
||||
Some((element, origin))
|
||||
}
|
||||
}
|
||||
InlineCompletion::Edit {
|
||||
|
@ -3805,13 +3955,9 @@ impl EditorElement {
|
|||
))
|
||||
})?;
|
||||
|
||||
element.prepaint_as_root(
|
||||
clamp_start(start_point + origin + point(PADDING_X, px(0.))),
|
||||
AvailableSpace::min_size(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
return Some(element);
|
||||
let origin = clamp_start(start_point + origin + point(PADDING_X, px(0.)));
|
||||
element.prepaint_as_root(origin, AvailableSpace::min_size(), window, cx);
|
||||
return Some((element, origin));
|
||||
}
|
||||
EditDisplayMode::Inline => return None,
|
||||
EditDisplayMode::DiffPopover => {}
|
||||
|
@ -4832,6 +4978,7 @@ impl EditorElement {
|
|||
self.paint_lines(&invisible_display_ranges, layout, window, cx);
|
||||
self.paint_redactions(layout, window);
|
||||
self.paint_cursors(layout, window, cx);
|
||||
self.paint_inline_diagnostics(layout, window, cx);
|
||||
self.paint_inline_blame(layout, window, cx);
|
||||
self.paint_diff_hunk_controls(layout, window, cx);
|
||||
window.with_element_namespace("crease_trailers", |window| {
|
||||
|
@ -5507,6 +5654,17 @@ impl EditorElement {
|
|||
}
|
||||
}
|
||||
|
||||
fn paint_inline_diagnostics(
|
||||
&mut self,
|
||||
layout: &mut EditorLayout,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
for mut inline_diagnostic in layout.inline_diagnostics.drain() {
|
||||
inline_diagnostic.1.paint(window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn paint_inline_blame(&mut self, layout: &mut EditorLayout, window: &mut Window, cx: &mut App) {
|
||||
if let Some(mut inline_blame) = layout.inline_blame.take() {
|
||||
window.paint_layer(layout.position_map.text_hitbox.bounds, |window| {
|
||||
|
@ -7221,6 +7379,40 @@ impl Element for EditorElement {
|
|||
)
|
||||
});
|
||||
|
||||
let (inline_completion_popover, inline_completion_popover_origin) = self
|
||||
.layout_edit_prediction_popover(
|
||||
&text_hitbox.bounds,
|
||||
content_origin,
|
||||
&snapshot,
|
||||
start_row..end_row,
|
||||
scroll_position.y,
|
||||
scroll_position.y + height_in_lines,
|
||||
&line_layouts,
|
||||
line_height,
|
||||
scroll_pixel_position,
|
||||
newest_selection_head,
|
||||
editor_width,
|
||||
&style,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.unzip();
|
||||
|
||||
let mut inline_diagnostics = self.layout_inline_diagnostics(
|
||||
&line_layouts,
|
||||
&crease_trailers,
|
||||
content_origin,
|
||||
scroll_pixel_position,
|
||||
inline_completion_popover_origin,
|
||||
start_row,
|
||||
end_row,
|
||||
line_height,
|
||||
em_width,
|
||||
&style,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
let mut inline_blame = None;
|
||||
if let Some(newest_selection_head) = newest_selection_head {
|
||||
let display_row = newest_selection_head.row();
|
||||
|
@ -7241,6 +7433,10 @@ impl Element for EditorElement {
|
|||
window,
|
||||
cx,
|
||||
);
|
||||
if inline_blame.is_some() {
|
||||
// Blame overrides inline diagnostics
|
||||
inline_diagnostics.remove(&display_row);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -7477,23 +7673,6 @@ impl Element for EditorElement {
|
|||
);
|
||||
}
|
||||
|
||||
let inline_completion_popover = self.layout_edit_prediction_popover(
|
||||
&text_hitbox.bounds,
|
||||
content_origin,
|
||||
&snapshot,
|
||||
start_row..end_row,
|
||||
scroll_position.y,
|
||||
scroll_position.y + height_in_lines,
|
||||
&line_layouts,
|
||||
line_height,
|
||||
scroll_pixel_position,
|
||||
newest_selection_head,
|
||||
editor_width,
|
||||
&style,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
let mouse_context_menu = self.layout_mouse_context_menu(
|
||||
&snapshot,
|
||||
start_row..end_row,
|
||||
|
@ -7600,6 +7779,7 @@ impl Element for EditorElement {
|
|||
line_elements,
|
||||
line_numbers,
|
||||
blamed_display_rows,
|
||||
inline_diagnostics,
|
||||
inline_blame,
|
||||
blocks,
|
||||
cursors,
|
||||
|
@ -7777,6 +7957,7 @@ pub struct EditorLayout {
|
|||
line_numbers: Arc<HashMap<MultiBufferRow, LineNumberLayout>>,
|
||||
display_hunks: Vec<(DisplayDiffHunk, Option<Hitbox>)>,
|
||||
blamed_display_rows: Option<Vec<AnyElement>>,
|
||||
inline_diagnostics: HashMap<DisplayRow, AnyElement>,
|
||||
inline_blame: Option<AnyElement>,
|
||||
blocks: Vec<BlockLayout>,
|
||||
highlighted_ranges: Vec<(Range<DisplayPoint>, Hsla)>,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue