Skip to content

Commit

Permalink
Overlay all diagnostics with highest severity on top (#4113)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
the-mikedavis authored Oct 11, 2022
1 parent 1a87cbd commit 65febe0
Showing 1 changed file with 46 additions and 19 deletions.
65 changes: 46 additions & 19 deletions helix-term/src/ui/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,15 @@ impl EditorView {
Self::highlight_cursorcolumn(doc, view, surface, theme);
}

let highlights = Self::doc_syntax_highlights(doc, view.offset, inner.height, theme);
let highlights = syntax::merge(highlights, Self::doc_diagnostics_highlights(doc, theme));
let mut highlights = Self::doc_syntax_highlights(doc, view.offset, inner.height, theme);
for diagnostic in Self::doc_diagnostics_highlights(doc, theme) {
// Most of the `diagnostic` Vecs are empty most of the time. Skipping
// a merge for any empty Vec saves a significant amount of work.
if diagnostic.is_empty() {
continue;
}
highlights = Box::new(syntax::merge(highlights, diagnostic));
}
let highlights: Box<dyn Iterator<Item = HighlightEvent>> = if is_focused {
Box::new(syntax::merge(
highlights,
Expand Down Expand Up @@ -268,7 +275,7 @@ impl EditorView {
pub fn doc_diagnostics_highlights(
doc: &Document,
theme: &Theme,
) -> Vec<(usize, std::ops::Range<usize>)> {
) -> [Vec<(usize, std::ops::Range<usize>)>; 5] {
use helix_core::diagnostic::Severity;
let get_scope_of = |scope| {
theme
Expand All @@ -289,22 +296,42 @@ impl EditorView {
let error = get_scope_of("diagnostic.error");
let r#default = get_scope_of("diagnostic"); // this is a bit redundant but should be fine

doc.diagnostics()
.iter()
.map(|diagnostic| {
let diagnostic_scope = match diagnostic.severity {
Some(Severity::Info) => info,
Some(Severity::Hint) => hint,
Some(Severity::Warning) => warning,
Some(Severity::Error) => error,
_ => r#default,
};
(
diagnostic_scope,
diagnostic.range.start..diagnostic.range.end,
)
})
.collect()
let mut default_vec: Vec<(usize, std::ops::Range<usize>)> = Vec::new();
let mut info_vec = Vec::new();
let mut hint_vec = Vec::new();
let mut warning_vec = Vec::new();
let mut error_vec = Vec::new();

let diagnostics = doc.diagnostics();

// Diagnostics must be sorted by range. Otherwise, the merge strategy
// below would not be accurate.
debug_assert!(diagnostics
.windows(2)
.all(|window| window[0].range.start <= window[1].range.start
&& window[0].range.end <= window[1].range.end));

for diagnostic in diagnostics {
// Separate diagnostics into different Vecs by severity.
let (vec, scope) = match diagnostic.severity {
Some(Severity::Info) => (&mut info_vec, info),
Some(Severity::Hint) => (&mut hint_vec, hint),
Some(Severity::Warning) => (&mut warning_vec, warning),
Some(Severity::Error) => (&mut error_vec, error),
_ => (&mut default_vec, r#default),
};

// If any diagnostic overlaps ranges with the prior diagnostic,
// merge the two together. Otherwise push a new span.
match vec.last_mut() {
Some((_, range)) if diagnostic.range.start <= range.end => {
range.end = diagnostic.range.end.max(range.end)
}
_ => vec.push((scope, diagnostic.range.start..diagnostic.range.end)),
}
}

[default_vec, info_vec, hint_vec, warning_vec, error_vec]
}

/// Get highlight spans for selections in a document view.
Expand Down

0 comments on commit 65febe0

Please sign in to comment.