render diagnostic inline

This commit is contained in:
Pascal Kuthe 2024-01-29 17:11:00 +01:00
parent 39b3d81abf
commit 6d051d7084
No known key found for this signature in database
GPG key ID: D715E8655AE166A6
14 changed files with 786 additions and 29 deletions

View file

@ -1711,6 +1711,7 @@ pub fn scroll(cx: &mut Context, offset: usize, direction: Direction, sync_cursor
&mut annotations,
)
});
drop(annotations);
doc.set_selection(view.id, selection);
return;
}

View file

@ -114,7 +114,7 @@ pub fn render_document(
}
#[allow(clippy::too_many_arguments)]
pub fn render_text<'t>(
pub fn render_text(
renderer: &mut TextRenderer,
text: RopeSlice<'_>,
anchor: usize,
@ -348,6 +348,44 @@ impl<'a> TextRenderer<'a> {
offset,
}
}
/// Draws a single `grapheme` at the current render position with a specified `style`.
pub fn draw_decoration_grapheme(
&mut self,
grapheme: Grapheme,
mut style: Style,
mut row: u16,
col: u16,
) -> bool {
if (row as usize) < self.offset.row
|| row >= self.viewport.height
|| col >= self.viewport.width
{
return false;
}
row -= self.offset.row as u16;
// TODO is it correct to apply the whitspace style to all unicode white spaces?
if grapheme.is_whitespace() {
style = style.patch(self.whitespace_style);
}
let grapheme = match grapheme {
Grapheme::Tab { width } => {
let grapheme_tab_width = char_to_byte_idx(&self.virtual_tab, width);
&self.virtual_tab[..grapheme_tab_width]
}
Grapheme::Other { ref g } if g == "\u{00A0}" => " ",
Grapheme::Other { ref g } => g,
Grapheme::Newline => " ",
};
self.surface.set_string(
self.viewport.x + col,
self.viewport.y + row,
grapheme,
style,
);
true
}
/// Draws a single `grapheme` at the current render position with a specified `style`.
pub fn draw_grapheme(

View file

@ -22,6 +22,7 @@ use helix_core::{
visual_offset_from_block, Change, Position, Range, Selection, Transaction,
};
use helix_view::{
annotations::diagnostics::DiagnosticFilter,
document::{Mode, SavePoint, SCRATCH_BUFFER_NAME},
editor::{CompleteAction, CursorShapeConfig},
graphics::{Color, CursorKind, Modifier, Rect, Style},
@ -185,6 +186,12 @@ impl EditorView {
primary_cursor,
});
}
decorations.add_decoration(InlineDiagnostics::new(
doc,
theme,
primary_cursor,
config.lsp.inline_diagnostics.clone(),
));
render_document(
surface,
inner,
@ -210,7 +217,11 @@ impl EditorView {
}
}
Self::render_diagnostics(doc, view, inner, surface, theme);
if config.inline_diagnostics.disabled()
&& config.end_of_line_diagnostics == DiagnosticFilter::Disable
{
Self::render_diagnostics(doc, view, inner, surface, theme);
}
let statusline_area = view
.area

View file

@ -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)
}
}