Skip to content

Commit 62f705f

Browse files
the-mikedavisShekhinah Memmel
authored and
Shekhinah Memmel
committed
Overlay all diagnostics with highest severity on top (helix-editor#4113)
Here we separate the diagnostics by severity and then overlay the Vec of spans for each severity on top of the highlights. The error diagnostics end up overlaid on the warning diagnostics, which are overlaid on the hints, overlaid on info, overlaid on any other severity (default), then overlaid on the syntax highlights. This fixes two things: * Error diagnostics are now always visible when overlapped with other diagnostics. * Ghost text is eliminated. * Ghost text was caused by duplicate diagnostics at the EOF: overlaps within the merged `Vec<(usize, Range<usize>)>` violate assumptions in `helix_core::syntax::Merge`. * When we push a new range, we check it against the last range and merge the two if they overlap. This is safe because they both have the same severity and therefore highlight. The actual merge is skipped for any of these when they are empty, so this is very fast in practice. For some data, I threw together an FPS counter which renders as fast as possible and logs the renders per second. With no diagnostics, I see an FPS gain from this change from 868 FPS to 878 (+1.1%) on a release build on a Rust file. On an Erlang file with 12 error diagnostics and 6 warnings in view (233 errors and 66 warnings total), I see a decrease in average FPS from 795 to 790 (-0.6%) on a release build.
1 parent 9c2030c commit 62f705f

File tree

1 file changed

+46
-19
lines changed

1 file changed

+46
-19
lines changed

helix-term/src/ui/editor.rs

+46-19
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,15 @@ impl EditorView {
125125
Self::highlight_cursorcolumn(doc, view, surface, theme);
126126
}
127127

128-
let highlights = Self::doc_syntax_highlights(doc, view.offset, inner.height, theme);
129-
let highlights = syntax::merge(highlights, Self::doc_diagnostics_highlights(doc, theme));
128+
let mut highlights = Self::doc_syntax_highlights(doc, view.offset, inner.height, theme);
129+
for diagnostic in Self::doc_diagnostics_highlights(doc, theme) {
130+
// Most of the `diagnostic` Vecs are empty most of the time. Skipping
131+
// a merge for any empty Vec saves a significant amount of work.
132+
if diagnostic.is_empty() {
133+
continue;
134+
}
135+
highlights = Box::new(syntax::merge(highlights, diagnostic));
136+
}
130137
let highlights: Box<dyn Iterator<Item = HighlightEvent>> = if is_focused {
131138
Box::new(syntax::merge(
132139
highlights,
@@ -270,7 +277,7 @@ impl EditorView {
270277
pub fn doc_diagnostics_highlights(
271278
doc: &Document,
272279
theme: &Theme,
273-
) -> Vec<(usize, std::ops::Range<usize>)> {
280+
) -> [Vec<(usize, std::ops::Range<usize>)>; 5] {
274281
use helix_core::diagnostic::Severity;
275282
let get_scope_of = |scope| {
276283
theme
@@ -291,22 +298,42 @@ impl EditorView {
291298
let error = get_scope_of("diagnostic.error");
292299
let r#default = get_scope_of("diagnostic"); // this is a bit redundant but should be fine
293300

294-
doc.diagnostics()
295-
.iter()
296-
.map(|diagnostic| {
297-
let diagnostic_scope = match diagnostic.severity {
298-
Some(Severity::Info) => info,
299-
Some(Severity::Hint) => hint,
300-
Some(Severity::Warning) => warning,
301-
Some(Severity::Error) => error,
302-
_ => r#default,
303-
};
304-
(
305-
diagnostic_scope,
306-
diagnostic.range.start..diagnostic.range.end,
307-
)
308-
})
309-
.collect()
301+
let mut default_vec: Vec<(usize, std::ops::Range<usize>)> = Vec::new();
302+
let mut info_vec = Vec::new();
303+
let mut hint_vec = Vec::new();
304+
let mut warning_vec = Vec::new();
305+
let mut error_vec = Vec::new();
306+
307+
let diagnostics = doc.diagnostics();
308+
309+
// Diagnostics must be sorted by range. Otherwise, the merge strategy
310+
// below would not be accurate.
311+
debug_assert!(diagnostics
312+
.windows(2)
313+
.all(|window| window[0].range.start <= window[1].range.start
314+
&& window[0].range.end <= window[1].range.end));
315+
316+
for diagnostic in diagnostics {
317+
// Separate diagnostics into different Vecs by severity.
318+
let (vec, scope) = match diagnostic.severity {
319+
Some(Severity::Info) => (&mut info_vec, info),
320+
Some(Severity::Hint) => (&mut hint_vec, hint),
321+
Some(Severity::Warning) => (&mut warning_vec, warning),
322+
Some(Severity::Error) => (&mut error_vec, error),
323+
_ => (&mut default_vec, r#default),
324+
};
325+
326+
// If any diagnostic overlaps ranges with the prior diagnostic,
327+
// merge the two together. Otherwise push a new span.
328+
match vec.last_mut() {
329+
Some((_, range)) if diagnostic.range.start <= range.end => {
330+
range.end = diagnostic.range.end.max(range.end)
331+
}
332+
_ => vec.push((scope, diagnostic.range.start..diagnostic.range.end)),
333+
}
334+
}
335+
336+
[default_vec, info_vec, hint_vec, warning_vec, error_vec]
310337
}
311338

312339
/// Get highlight spans for selections in a document view.

0 commit comments

Comments
 (0)