Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'helix-term/src/ui/text_decorations/diagnostics.rs')
-rw-r--r--helix-term/src/ui/text_decorations/diagnostics.rs305
1 files changed, 305 insertions, 0 deletions
diff --git a/helix-term/src/ui/text_decorations/diagnostics.rs b/helix-term/src/ui/text_decorations/diagnostics.rs
new file mode 100644
index 00000000..2d9e8370
--- /dev/null
+++ b/helix-term/src/ui/text_decorations/diagnostics.rs
@@ -0,0 +1,305 @@
+use std::cmp::Ordering;
+
+use helix_core::diagnostic::Severity;
+use helix_core::doc_formatter::{DocumentFormatter, FormattedGrapheme};
+use helix_core::graphemes::Grapheme;
+use helix_core::text_annotations::TextAnnotations;
+use helix_core::{Diagnostic, Position};
+use helix_view::annotations::diagnostics::{
+ DiagnosticFilter, InlineDiagnosticAccumulator, InlineDiagnosticsConfig,
+};
+
+use helix_view::theme::Style;
+use helix_view::{Document, Theme};
+
+use crate::ui::document::{LinePos, TextRenderer};
+use crate::ui::text_decorations::Decoration;
+
+#[derive(Debug)]
+struct Styles {
+ hint: Style,
+ info: Style,
+ warning: Style,
+ error: Style,
+}
+
+impl Styles {
+ fn new(theme: &Theme) -> Styles {
+ Styles {
+ hint: theme.get("hint"),
+ info: theme.get("info"),
+ warning: theme.get("warning"),
+ error: theme.get("error"),
+ }
+ }
+
+ fn severity_style(&self, severity: Severity) -> Style {
+ match severity {
+ Severity::Hint => self.hint,
+ Severity::Info => self.info,
+ Severity::Warning => self.warning,
+ Severity::Error => self.error,
+ }
+ }
+}
+
+pub struct InlineDiagnostics<'a> {
+ state: InlineDiagnosticAccumulator<'a>,
+ eol_diagnostics: DiagnosticFilter,
+ styles: Styles,
+}
+
+impl<'a> InlineDiagnostics<'a> {
+ pub fn new(
+ doc: &'a Document,
+ theme: &Theme,
+ cursor: usize,
+ config: InlineDiagnosticsConfig,
+ eol_diagnostics: DiagnosticFilter,
+ ) -> Self {
+ InlineDiagnostics {
+ state: InlineDiagnosticAccumulator::new(cursor, doc, config),
+ styles: Styles::new(theme),
+ eol_diagnostics,
+ }
+ }
+}
+
+const BL_CORNER: &str = "┘";
+const TR_CORNER: &str = "┌";
+const BR_CORNER: &str = "└";
+const STACK: &str = "├";
+const MULTI: &str = "┴";
+const HOR_BAR: &str = "─";
+const VER_BAR: &str = "│";
+
+struct Renderer<'a, 'b> {
+ renderer: &'a mut TextRenderer<'b>,
+ first_row: u16,
+ row: u16,
+ config: &'a InlineDiagnosticsConfig,
+ styles: &'a Styles,
+}
+
+impl Renderer<'_, '_> {
+ fn draw_decoration(&mut self, g: &'static str, severity: Severity, col: u16) {
+ self.draw_decoration_at(g, severity, col, self.row)
+ }
+
+ fn draw_decoration_at(&mut self, g: &'static str, severity: Severity, col: u16, row: u16) {
+ self.renderer.draw_decoration_grapheme(
+ Grapheme::new_decoration(g),
+ self.styles.severity_style(severity),
+ row,
+ col,
+ );
+ }
+
+ fn draw_eol_diagnostic(&mut self, diag: &Diagnostic, row: u16, col: usize) -> u16 {
+ let style = self.styles.severity_style(diag.severity());
+ let width = self.renderer.viewport.width;
+ if !self.renderer.column_in_bounds(col + 1) {
+ return 0;
+ }
+ let col = (col - self.renderer.offset.col) as u16;
+ let (new_col, _) = self.renderer.set_string_truncated(
+ self.renderer.viewport.x + col + 1,
+ row,
+ &diag.message,
+ width.saturating_sub(col + 1) as usize,
+ |_| style,
+ true,
+ false,
+ );
+ new_col - col
+ }
+
+ fn draw_diagnostic(&mut self, diag: &Diagnostic, col: u16, next_severity: Option<Severity>) {
+ let severity = diag.severity();
+ let (sym, sym_severity) = if let Some(next_severity) = next_severity {
+ (STACK, next_severity.max(severity))
+ } else {
+ (BR_CORNER, severity)
+ };
+ self.draw_decoration(sym, sym_severity, col);
+ for i in 0..self.config.prefix_len {
+ self.draw_decoration(HOR_BAR, severity, col + i + 1);
+ }
+
+ let text_col = col + self.config.prefix_len + 1;
+ let text_fmt = self.config.text_fmt(text_col, self.renderer.viewport.width);
+ let annotations = TextAnnotations::default();
+ let formatter = DocumentFormatter::new_at_prev_checkpoint(
+ diag.message.as_str().trim().into(),
+ &text_fmt,
+ &annotations,
+ 0,
+ );
+ let mut last_row = 0;
+ let style = self.styles.severity_style(severity);
+ for grapheme in formatter {
+ last_row = grapheme.visual_pos.row;
+ self.renderer.draw_decoration_grapheme(
+ grapheme.raw,
+ style,
+ self.row + grapheme.visual_pos.row as u16,
+ text_col + grapheme.visual_pos.col as u16,
+ );
+ }
+ self.row += 1;
+ // height is last_row + 1 and extra_rows is height - 1
+ let extra_lines = last_row;
+ if let Some(next_severity) = next_severity {
+ for _ in 0..extra_lines {
+ self.draw_decoration(VER_BAR, next_severity, col);
+ self.row += 1;
+ }
+ } else {
+ self.row += extra_lines as u16;
+ }
+ }
+
+ fn draw_multi_diagnostics(&mut self, stack: &mut Vec<(&Diagnostic, u16)>) {
+ let Some(&(last_diag, last_anchor)) = stack.last() else {
+ return;
+ };
+ let start = self
+ .config
+ .max_diagnostic_start(self.renderer.viewport.width);
+
+ if last_anchor <= start {
+ return;
+ }
+ let mut severity = last_diag.severity();
+ let mut last_anchor = last_anchor;
+ self.draw_decoration(BL_CORNER, severity, last_anchor);
+ let mut stacked_diagnostics = 1;
+ for &(diag, anchor) in stack.iter().rev().skip(1) {
+ let sym = match anchor.cmp(&start) {
+ Ordering::Less => break,
+ Ordering::Equal => STACK,
+ Ordering::Greater => MULTI,
+ };
+ stacked_diagnostics += 1;
+ severity = severity.max(diag.severity());
+ let old_severity = severity;
+ if anchor == last_anchor && severity == old_severity {
+ continue;
+ }
+ for col in (anchor + 1)..last_anchor {
+ self.draw_decoration(HOR_BAR, old_severity, col)
+ }
+ self.draw_decoration(sym, severity, anchor);
+ last_anchor = anchor;
+ }
+
+ // if no diagnostic anchor was found exactly at the start of the
+ // diagnostic text draw an upwards corner and ensure the last piece
+ // of the line is not missing
+ if last_anchor != start {
+ for col in (start + 1)..last_anchor {
+ self.draw_decoration(HOR_BAR, severity, col)
+ }
+ self.draw_decoration(TR_CORNER, severity, start)
+ }
+ self.row += 1;
+ let stacked_diagnostics = &stack[stack.len() - stacked_diagnostics..];
+
+ for (i, (diag, _)) in stacked_diagnostics.iter().rev().enumerate() {
+ let next_severity = stacked_diagnostics[..stacked_diagnostics.len() - i - 1]
+ .iter()
+ .map(|(diag, _)| diag.severity())
+ .max();
+ self.draw_diagnostic(diag, start, next_severity);
+ }
+
+ stack.truncate(stack.len() - stacked_diagnostics.len());
+ }
+
+ fn draw_diagnostics(&mut self, stack: &mut Vec<(&Diagnostic, u16)>) {
+ let mut stack = stack.drain(..).rev().peekable();
+ let mut last_anchor = self.renderer.viewport.width;
+ while let Some((diag, anchor)) = stack.next() {
+ if anchor != last_anchor {
+ for row in self.first_row..self.row {
+ self.draw_decoration_at(VER_BAR, diag.severity(), anchor, row);
+ }
+ }
+ let next_severity = stack.peek().and_then(|&(diag, next_anchor)| {
+ (next_anchor == anchor).then_some(diag.severity())
+ });
+ self.draw_diagnostic(diag, anchor, next_severity);
+ last_anchor = anchor;
+ }
+ }
+}
+
+impl Decoration for InlineDiagnostics<'_> {
+ fn render_virt_lines(
+ &mut self,
+ renderer: &mut TextRenderer,
+ pos: LinePos,
+ virt_off: Position,
+ ) -> Position {
+ let mut col_off = 0;
+ let filter = self.state.filter();
+ let eol_diagnostic = match self.eol_diagnostics {
+ DiagnosticFilter::Enable(eol_filter) => {
+ let eol_diganogistcs = self
+ .state
+ .stack
+ .iter()
+ .filter(|(diag, _)| eol_filter <= diag.severity());
+ match filter {
+ DiagnosticFilter::Enable(filter) => eol_diganogistcs
+ .filter(|(diag, _)| filter > diag.severity())
+ .max_by_key(|(diagnostic, _)| diagnostic.severity),
+ DiagnosticFilter::Disable => {
+ eol_diganogistcs.max_by_key(|(diagnostic, _)| diagnostic.severity)
+ }
+ }
+ }
+ DiagnosticFilter::Disable => None,
+ };
+ if let Some((eol_diagnostic, _)) = eol_diagnostic {
+ let mut renderer = Renderer {
+ renderer,
+ first_row: pos.visual_line,
+ row: pos.visual_line,
+ config: &self.state.config,
+ styles: &self.styles,
+ };
+ col_off = renderer.draw_eol_diagnostic(eol_diagnostic, pos.visual_line, virt_off.col);
+ }
+
+ self.state.compute_line_diagnostics();
+ let mut renderer = Renderer {
+ renderer,
+ first_row: pos.visual_line + virt_off.row as u16,
+ row: pos.visual_line + virt_off.row as u16,
+ config: &self.state.config,
+ styles: &self.styles,
+ };
+ renderer.draw_multi_diagnostics(&mut self.state.stack);
+ renderer.draw_diagnostics(&mut self.state.stack);
+ let horizontal_off = renderer.row - renderer.first_row;
+ Position::new(horizontal_off as usize, col_off as usize)
+ }
+
+ fn reset_pos(&mut self, pos: usize) -> usize {
+ self.state.reset_pos(pos)
+ }
+
+ fn skip_concealed_anchor(&mut self, conceal_end_char_idx: usize) -> usize {
+ self.state.skip_concealed(conceal_end_char_idx)
+ }
+
+ fn decorate_grapheme(
+ &mut self,
+ renderer: &mut TextRenderer,
+ grapheme: &FormattedGrapheme,
+ ) -> usize {
+ self.state
+ .proccess_anchor(grapheme, renderer.viewport.width, renderer.offset.col)
+ }
+}