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.rs | 305 |
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) + } +} |