mirror of https://github.com/helix-editor/helix
Merge pull request #6417 from pascalkuthe/inline-diagnostics
commit
107cdf3e43
@ -0,0 +1,24 @@
|
|||||||
|
use helix_event::{register_hook, send_blocking};
|
||||||
|
use helix_view::document::Mode;
|
||||||
|
use helix_view::events::DiagnosticsDidChange;
|
||||||
|
use helix_view::handlers::diagnostics::DiagnosticEvent;
|
||||||
|
use helix_view::handlers::Handlers;
|
||||||
|
|
||||||
|
use crate::events::OnModeSwitch;
|
||||||
|
|
||||||
|
pub(super) fn register_hooks(_handlers: &Handlers) {
|
||||||
|
register_hook!(move |event: &mut DiagnosticsDidChange<'_>| {
|
||||||
|
if event.editor.mode != Mode::Insert {
|
||||||
|
for (view, _) in event.editor.tree.views_mut() {
|
||||||
|
send_blocking(&view.diagnostics_handler.events, DiagnosticEvent::Refresh)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
register_hook!(move |event: &mut OnModeSwitch<'_, '_>| {
|
||||||
|
for (view, _) in event.cx.editor.tree.views_mut() {
|
||||||
|
view.diagnostics_handler.active = event.new_mode != Mode::Insert;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,175 @@
|
|||||||
|
use std::cmp::Ordering;
|
||||||
|
|
||||||
|
use helix_core::doc_formatter::FormattedGrapheme;
|
||||||
|
use helix_core::Position;
|
||||||
|
use helix_view::editor::CursorCache;
|
||||||
|
|
||||||
|
use crate::ui::document::{LinePos, TextRenderer};
|
||||||
|
|
||||||
|
pub use diagnostics::InlineDiagnostics;
|
||||||
|
|
||||||
|
mod diagnostics;
|
||||||
|
|
||||||
|
/// Decorations are the primary mechanism for extending the text rendering.
|
||||||
|
///
|
||||||
|
/// Any on-screen element which is anchored to the rendered text in some form
|
||||||
|
/// should be implemented using this trait. Translating char positions to
|
||||||
|
/// on-screen positions can be expensive and should not be done manually in the
|
||||||
|
/// ui loop. Instead such translations are automatically performed on the fly
|
||||||
|
/// while the text is being rendered. The results are provided to this trait by
|
||||||
|
/// the rendering infrastructure.
|
||||||
|
///
|
||||||
|
/// To reserve space for virtual text lines (which is then filled by this trait) emit appropriate
|
||||||
|
/// [`LineAnnotation`](helix_core::text_annotations::LineAnnotation)s in [`helix_view::View::text_annotations`]
|
||||||
|
pub trait Decoration {
|
||||||
|
/// Called **before** a **visual** line is rendered. A visual line does not
|
||||||
|
/// necessarily correspond to a single line in a document as soft wrapping can
|
||||||
|
/// spread a single document line across multiple visual lines.
|
||||||
|
///
|
||||||
|
/// This function is called before text is rendered as any decorations should
|
||||||
|
/// never overlap the document text. That means that setting the forground color
|
||||||
|
/// here is (essentially) useless as the text color is overwritten by the
|
||||||
|
/// rendered text. This _of course_ doesn't apply when rendering inside virtual lines
|
||||||
|
/// below the line reserved by `LineAnnotation`s as no text will be rendered here.
|
||||||
|
fn decorate_line(&mut self, _renderer: &mut TextRenderer, _pos: LinePos) {}
|
||||||
|
|
||||||
|
/// Called **after** a **visual** line is rendered. A visual line does not
|
||||||
|
/// necessarily correspond to a single line in a document as soft wrapping can
|
||||||
|
/// spread a single document line across multiple visual lines.
|
||||||
|
///
|
||||||
|
/// This function is called after text is rendered so that decorations can collect
|
||||||
|
/// horizontal positions on the line (see [`Decoration::decorate_grapheme`]) first and
|
||||||
|
/// use those positions` while rendering
|
||||||
|
/// virtual text.
|
||||||
|
/// That means that setting the forground color
|
||||||
|
/// here is (essentially) useless as the text color is overwritten by the
|
||||||
|
/// rendered text. This -ofcourse- doesn't apply when rendering inside virtual lines
|
||||||
|
/// below the line reserved by `LineAnnotation`s. e as no text will be rendered here.
|
||||||
|
/// **Note**: To avoid overlapping decorations in the virtual lines, each decoration
|
||||||
|
/// must return the number of virtual text lines it has taken up. Each `Decoration` recieves
|
||||||
|
/// an offset `virt_off` based on these return values where it can render virtual text:
|
||||||
|
///
|
||||||
|
/// That means that a `render_line` implementation that returns `X` can render virtual text
|
||||||
|
/// in the following area:
|
||||||
|
/// ``` no-compile
|
||||||
|
/// let start = inner.y + pos.virtual_line + virt_off;
|
||||||
|
/// start .. start + X
|
||||||
|
/// ````
|
||||||
|
fn render_virt_lines(
|
||||||
|
&mut self,
|
||||||
|
_renderer: &mut TextRenderer,
|
||||||
|
_pos: LinePos,
|
||||||
|
_virt_off: Position,
|
||||||
|
) -> Position {
|
||||||
|
Position::new(0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset_pos(&mut self, _pos: usize) -> usize {
|
||||||
|
usize::MAX
|
||||||
|
}
|
||||||
|
|
||||||
|
fn skip_concealed_anchor(&mut self, conceal_end_char_idx: usize) -> usize {
|
||||||
|
self.reset_pos(conceal_end_char_idx)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This function is called **before** the grapheme at `char_idx` is rendered.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// The char idx of the next grapheme that this function should be called for
|
||||||
|
fn decorate_grapheme(
|
||||||
|
&mut self,
|
||||||
|
_renderer: &mut TextRenderer,
|
||||||
|
_grapheme: &FormattedGrapheme,
|
||||||
|
) -> usize {
|
||||||
|
usize::MAX
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<F: FnMut(&mut TextRenderer, LinePos)> Decoration for F {
|
||||||
|
fn decorate_line(&mut self, renderer: &mut TextRenderer, pos: LinePos) {
|
||||||
|
self(renderer, pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct DecorationManager<'a> {
|
||||||
|
decorations: Vec<(Box<dyn Decoration + 'a>, usize)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> DecorationManager<'a> {
|
||||||
|
pub fn add_decoration(&mut self, decoration: impl Decoration + 'a) {
|
||||||
|
self.decorations.push((Box::new(decoration), 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn prepare_for_rendering(&mut self, first_visible_char: usize) {
|
||||||
|
for (decoration, next_position) in &mut self.decorations {
|
||||||
|
*next_position = decoration.reset_pos(first_visible_char)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decorate_grapheme(&mut self, renderer: &mut TextRenderer, grapheme: &FormattedGrapheme) {
|
||||||
|
for (decoration, hook_char_idx) in &mut self.decorations {
|
||||||
|
loop {
|
||||||
|
match (*hook_char_idx).cmp(&grapheme.char_idx) {
|
||||||
|
// this grapheme has been concealed or we are at the first grapheme
|
||||||
|
Ordering::Less => {
|
||||||
|
*hook_char_idx = decoration.skip_concealed_anchor(grapheme.char_idx)
|
||||||
|
}
|
||||||
|
Ordering::Equal => {
|
||||||
|
*hook_char_idx = decoration.decorate_grapheme(renderer, grapheme)
|
||||||
|
}
|
||||||
|
Ordering::Greater => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decorate_line(&mut self, renderer: &mut TextRenderer, pos: LinePos) {
|
||||||
|
for (decoration, _) in &mut self.decorations {
|
||||||
|
decoration.decorate_line(renderer, pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_virtual_lines(
|
||||||
|
&mut self,
|
||||||
|
renderer: &mut TextRenderer,
|
||||||
|
pos: LinePos,
|
||||||
|
line_width: usize,
|
||||||
|
) {
|
||||||
|
let mut virt_off = Position::new(1, line_width); // start at 1 so the line is never overwritten
|
||||||
|
for (decoration, _) in &mut self.decorations {
|
||||||
|
virt_off += decoration.render_virt_lines(renderer, pos, virt_off);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cursor rendering is done externally so all the cursor decoration
|
||||||
|
/// does is save the position of primary cursor
|
||||||
|
pub struct Cursor<'a> {
|
||||||
|
pub cache: &'a CursorCache,
|
||||||
|
pub primary_cursor: usize,
|
||||||
|
}
|
||||||
|
impl Decoration for Cursor<'_> {
|
||||||
|
fn reset_pos(&mut self, pos: usize) -> usize {
|
||||||
|
if pos <= self.primary_cursor {
|
||||||
|
self.primary_cursor
|
||||||
|
} else {
|
||||||
|
usize::MAX
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decorate_grapheme(
|
||||||
|
&mut self,
|
||||||
|
renderer: &mut TextRenderer,
|
||||||
|
grapheme: &FormattedGrapheme,
|
||||||
|
) -> usize {
|
||||||
|
if renderer.column_in_bounds(grapheme.visual_pos.col)
|
||||||
|
&& renderer.offset.row < grapheme.visual_pos.row
|
||||||
|
{
|
||||||
|
let position = grapheme.visual_pos - renderer.offset;
|
||||||
|
self.cache.set(Some(position));
|
||||||
|
}
|
||||||
|
usize::MAX
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
pub mod diagnostics;
|
@ -0,0 +1,313 @@
|
|||||||
|
use helix_core::diagnostic::Severity;
|
||||||
|
use helix_core::doc_formatter::{FormattedGrapheme, TextFormat};
|
||||||
|
use helix_core::text_annotations::LineAnnotation;
|
||||||
|
use helix_core::{softwrapped_dimensions, Diagnostic, Position};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::Document;
|
||||||
|
|
||||||
|
/// Describes the severity level of a [`Diagnostic`].
|
||||||
|
#[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord)]
|
||||||
|
pub enum DiagnosticFilter {
|
||||||
|
Disable,
|
||||||
|
Enable(Severity),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for DiagnosticFilter {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
match &*String::deserialize(deserializer)? {
|
||||||
|
"disable" => Ok(DiagnosticFilter::Disable),
|
||||||
|
"hint" => Ok(DiagnosticFilter::Enable(Severity::Hint)),
|
||||||
|
"info" => Ok(DiagnosticFilter::Enable(Severity::Info)),
|
||||||
|
"warning" => Ok(DiagnosticFilter::Enable(Severity::Warning)),
|
||||||
|
"error" => Ok(DiagnosticFilter::Enable(Severity::Error)),
|
||||||
|
variant => Err(serde::de::Error::unknown_variant(
|
||||||
|
variant,
|
||||||
|
&["disable", "hint", "info", "warning", "error"],
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serialize for DiagnosticFilter {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
let filter = match self {
|
||||||
|
DiagnosticFilter::Disable => "disable",
|
||||||
|
DiagnosticFilter::Enable(Severity::Hint) => "hint",
|
||||||
|
DiagnosticFilter::Enable(Severity::Info) => "info",
|
||||||
|
DiagnosticFilter::Enable(Severity::Warning) => "warning",
|
||||||
|
DiagnosticFilter::Enable(Severity::Error) => "error",
|
||||||
|
};
|
||||||
|
filter.serialize(serializer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
|
||||||
|
pub struct InlineDiagnosticsConfig {
|
||||||
|
pub cursor_line: DiagnosticFilter,
|
||||||
|
pub other_lines: DiagnosticFilter,
|
||||||
|
pub min_diagnostic_width: u16,
|
||||||
|
pub prefix_len: u16,
|
||||||
|
pub max_wrap: u16,
|
||||||
|
pub max_diagnostics: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InlineDiagnosticsConfig {
|
||||||
|
pub fn disabled(&self) -> bool {
|
||||||
|
matches!(
|
||||||
|
self,
|
||||||
|
Self {
|
||||||
|
cursor_line: DiagnosticFilter::Disable,
|
||||||
|
other_lines: DiagnosticFilter::Disable,
|
||||||
|
..
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn prepare(&self, width: u16, enable_cursor_line: bool) -> Self {
|
||||||
|
let mut config = self.clone();
|
||||||
|
if width < self.min_diagnostic_width + self.prefix_len {
|
||||||
|
config.cursor_line = DiagnosticFilter::Disable;
|
||||||
|
config.other_lines = DiagnosticFilter::Disable;
|
||||||
|
} else if !enable_cursor_line {
|
||||||
|
config.cursor_line = self.cursor_line.min(self.other_lines);
|
||||||
|
}
|
||||||
|
config
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn max_diagnostic_start(&self, width: u16) -> u16 {
|
||||||
|
width - self.min_diagnostic_width - self.prefix_len
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn text_fmt(&self, anchor_col: u16, width: u16) -> TextFormat {
|
||||||
|
let width = if anchor_col > self.max_diagnostic_start(width) {
|
||||||
|
self.min_diagnostic_width
|
||||||
|
} else {
|
||||||
|
width - anchor_col - self.prefix_len
|
||||||
|
};
|
||||||
|
|
||||||
|
TextFormat {
|
||||||
|
soft_wrap: true,
|
||||||
|
tab_width: 4,
|
||||||
|
max_wrap: self.max_wrap.min(width / 4),
|
||||||
|
max_indent_retain: 0,
|
||||||
|
wrap_indicator: "".into(),
|
||||||
|
wrap_indicator_highlight: None,
|
||||||
|
viewport_width: width,
|
||||||
|
soft_wrap_at_text_width: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for InlineDiagnosticsConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
InlineDiagnosticsConfig {
|
||||||
|
cursor_line: DiagnosticFilter::Disable,
|
||||||
|
other_lines: DiagnosticFilter::Disable,
|
||||||
|
min_diagnostic_width: 40,
|
||||||
|
prefix_len: 1,
|
||||||
|
max_wrap: 20,
|
||||||
|
max_diagnostics: 10,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct InlineDiagnosticAccumulator<'a> {
|
||||||
|
idx: usize,
|
||||||
|
doc: &'a Document,
|
||||||
|
pub stack: Vec<(&'a Diagnostic, u16)>,
|
||||||
|
pub config: InlineDiagnosticsConfig,
|
||||||
|
cursor: usize,
|
||||||
|
cursor_line: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> InlineDiagnosticAccumulator<'a> {
|
||||||
|
pub fn new(cursor: usize, doc: &'a Document, config: InlineDiagnosticsConfig) -> Self {
|
||||||
|
InlineDiagnosticAccumulator {
|
||||||
|
idx: 0,
|
||||||
|
doc,
|
||||||
|
stack: Vec::new(),
|
||||||
|
config,
|
||||||
|
cursor,
|
||||||
|
cursor_line: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reset_pos(&mut self, char_idx: usize) -> usize {
|
||||||
|
self.idx = 0;
|
||||||
|
self.clear();
|
||||||
|
self.skip_concealed(char_idx)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn skip_concealed(&mut self, conceal_end_char_idx: usize) -> usize {
|
||||||
|
let diagnostics = &self.doc.diagnostics[self.idx..];
|
||||||
|
let idx = diagnostics.partition_point(|diag| diag.range.start < conceal_end_char_idx);
|
||||||
|
self.idx += idx;
|
||||||
|
self.next_anchor(conceal_end_char_idx)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next_anchor(&self, current_char_idx: usize) -> usize {
|
||||||
|
let next_diag_start = self
|
||||||
|
.doc
|
||||||
|
.diagnostics
|
||||||
|
.get(self.idx)
|
||||||
|
.map_or(usize::MAX, |diag| diag.range.start);
|
||||||
|
if (current_char_idx..next_diag_start).contains(&self.cursor) {
|
||||||
|
self.cursor
|
||||||
|
} else {
|
||||||
|
next_diag_start
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear(&mut self) {
|
||||||
|
self.cursor_line = false;
|
||||||
|
self.stack.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process_anchor_impl(
|
||||||
|
&mut self,
|
||||||
|
grapheme: &FormattedGrapheme,
|
||||||
|
width: u16,
|
||||||
|
horizontal_off: usize,
|
||||||
|
) -> bool {
|
||||||
|
// TODO: doing the cursor tracking here works well but is somewhat
|
||||||
|
// duplicate effort/tedious maybe centralize this somehwere?
|
||||||
|
// In the DocFormatter?
|
||||||
|
if grapheme.char_idx == self.cursor {
|
||||||
|
self.cursor_line = true;
|
||||||
|
if self
|
||||||
|
.doc
|
||||||
|
.diagnostics
|
||||||
|
.get(self.idx)
|
||||||
|
.map_or(true, |diag| diag.range.start != grapheme.char_idx)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(anchor_col) = grapheme.visual_pos.col.checked_sub(horizontal_off) else {
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
if anchor_col >= width as usize {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for diag in &self.doc.diagnostics[self.idx..] {
|
||||||
|
if diag.range.start != grapheme.char_idx {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
self.stack.push((diag, anchor_col as u16));
|
||||||
|
self.idx += 1;
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn proccess_anchor(
|
||||||
|
&mut self,
|
||||||
|
grapheme: &FormattedGrapheme,
|
||||||
|
width: u16,
|
||||||
|
horizontal_off: usize,
|
||||||
|
) -> usize {
|
||||||
|
if self.process_anchor_impl(grapheme, width, horizontal_off) {
|
||||||
|
self.idx += self.doc.diagnostics[self.idx..]
|
||||||
|
.iter()
|
||||||
|
.take_while(|diag| diag.range.start == grapheme.char_idx)
|
||||||
|
.count();
|
||||||
|
}
|
||||||
|
self.next_anchor(grapheme.char_idx + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn filter(&self) -> DiagnosticFilter {
|
||||||
|
if self.cursor_line {
|
||||||
|
self.config.cursor_line
|
||||||
|
} else {
|
||||||
|
self.config.other_lines
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn compute_line_diagnostics(&mut self) {
|
||||||
|
let filter = if self.cursor_line {
|
||||||
|
self.cursor_line = false;
|
||||||
|
self.config.cursor_line
|
||||||
|
} else {
|
||||||
|
self.config.other_lines
|
||||||
|
};
|
||||||
|
let DiagnosticFilter::Enable(filter) = filter else {
|
||||||
|
self.stack.clear();
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
self.stack.retain(|(diag, _)| diag.severity() >= filter);
|
||||||
|
self.stack.truncate(self.config.max_diagnostics)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn has_multi(&self, width: u16) -> bool {
|
||||||
|
self.stack.last().map_or(false, |&(_, anchor)| {
|
||||||
|
anchor > self.config.max_diagnostic_start(width)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct InlineDiagnostics<'a> {
|
||||||
|
state: InlineDiagnosticAccumulator<'a>,
|
||||||
|
width: u16,
|
||||||
|
horizontal_off: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> InlineDiagnostics<'a> {
|
||||||
|
#[allow(clippy::new_ret_no_self)]
|
||||||
|
pub(crate) fn new(
|
||||||
|
doc: &'a Document,
|
||||||
|
cursor: usize,
|
||||||
|
width: u16,
|
||||||
|
horizontal_off: usize,
|
||||||
|
config: InlineDiagnosticsConfig,
|
||||||
|
) -> Box<dyn LineAnnotation + 'a> {
|
||||||
|
Box::new(InlineDiagnostics {
|
||||||
|
state: InlineDiagnosticAccumulator::new(cursor, doc, config),
|
||||||
|
width,
|
||||||
|
horizontal_off,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LineAnnotation for InlineDiagnostics<'_> {
|
||||||
|
fn reset_pos(&mut self, char_idx: usize) -> usize {
|
||||||
|
self.state.reset_pos(char_idx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn skip_concealed_anchors(&mut self, conceal_end_char_idx: usize) -> usize {
|
||||||
|
self.state.skip_concealed(conceal_end_char_idx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process_anchor(&mut self, grapheme: &FormattedGrapheme) -> usize {
|
||||||
|
self.state
|
||||||
|
.proccess_anchor(grapheme, self.width, self.horizontal_off)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert_virtual_lines(
|
||||||
|
&mut self,
|
||||||
|
_line_end_char_idx: usize,
|
||||||
|
_line_end_visual_pos: Position,
|
||||||
|
_doc_line: usize,
|
||||||
|
) -> Position {
|
||||||
|
self.state.compute_line_diagnostics();
|
||||||
|
let multi = self.state.has_multi(self.width);
|
||||||
|
let diagostic_height: usize = self
|
||||||
|
.state
|
||||||
|
.stack
|
||||||
|
.drain(..)
|
||||||
|
.map(|(diag, anchor)| {
|
||||||
|
let text_fmt = self.state.config.text_fmt(anchor, self.width);
|
||||||
|
softwrapped_dimensions(diag.message.as_str().trim().into(), &text_fmt).0
|
||||||
|
})
|
||||||
|
.sum();
|
||||||
|
Position::new(multi as usize + diagostic_height, 0)
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,10 @@
|
|||||||
use helix_core::Rope;
|
use helix_core::Rope;
|
||||||
use helix_event::events;
|
use helix_event::events;
|
||||||
|
|
||||||
use crate::{Document, ViewId};
|
use crate::{Document, DocumentId, Editor, ViewId};
|
||||||
|
|
||||||
events! {
|
events! {
|
||||||
DocumentDidChange<'a> { doc: &'a mut Document, view: ViewId, old_text: &'a Rope }
|
DocumentDidChange<'a> { doc: &'a mut Document, view: ViewId, old_text: &'a Rope }
|
||||||
SelectionDidChange<'a> { doc: &'a mut Document, view: ViewId }
|
SelectionDidChange<'a> { doc: &'a mut Document, view: ViewId }
|
||||||
|
DiagnosticsDidChange<'a> { editor: &'a mut Editor, doc: DocumentId }
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,131 @@
|
|||||||
|
use std::cell::Cell;
|
||||||
|
use std::num::NonZeroUsize;
|
||||||
|
use std::sync::atomic::{self, AtomicUsize};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use helix_event::{request_redraw, send_blocking, AsyncHook};
|
||||||
|
use tokio::sync::mpsc::Sender;
|
||||||
|
use tokio::time::Instant;
|
||||||
|
|
||||||
|
use crate::{Document, DocumentId, ViewId};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum DiagnosticEvent {
|
||||||
|
CursorLineChanged { generation: usize },
|
||||||
|
Refresh,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DiagnosticTimeout {
|
||||||
|
active_generation: Arc<AtomicUsize>,
|
||||||
|
generation: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
const TIMEOUT: Duration = Duration::from_millis(350);
|
||||||
|
|
||||||
|
impl AsyncHook for DiagnosticTimeout {
|
||||||
|
type Event = DiagnosticEvent;
|
||||||
|
|
||||||
|
fn handle_event(
|
||||||
|
&mut self,
|
||||||
|
event: DiagnosticEvent,
|
||||||
|
timeout: Option<Instant>,
|
||||||
|
) -> Option<Instant> {
|
||||||
|
match event {
|
||||||
|
DiagnosticEvent::CursorLineChanged { generation } => {
|
||||||
|
if generation > self.generation {
|
||||||
|
self.generation = generation;
|
||||||
|
Some(Instant::now() + TIMEOUT)
|
||||||
|
} else {
|
||||||
|
timeout
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DiagnosticEvent::Refresh if timeout.is_some() => Some(Instant::now() + TIMEOUT),
|
||||||
|
DiagnosticEvent::Refresh => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finish_debounce(&mut self) {
|
||||||
|
if self.active_generation.load(atomic::Ordering::Relaxed) < self.generation {
|
||||||
|
self.active_generation
|
||||||
|
.store(self.generation, atomic::Ordering::Relaxed);
|
||||||
|
request_redraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DiagnosticsHandler {
|
||||||
|
active_generation: Arc<AtomicUsize>,
|
||||||
|
generation: Cell<usize>,
|
||||||
|
last_doc: Cell<DocumentId>,
|
||||||
|
last_cursor_line: Cell<usize>,
|
||||||
|
pub active: bool,
|
||||||
|
pub events: Sender<DiagnosticEvent>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure we never share handlers across multiple views this is a stop
|
||||||
|
// gap solution. We just shouldn't be cloneing a view to begin with (we do
|
||||||
|
// for :hsplit/vsplit) and really this should not be view specific to begin with
|
||||||
|
// but to fix that larger architecutre changes are needed
|
||||||
|
impl Clone for DiagnosticsHandler {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DiagnosticsHandler {
|
||||||
|
#[allow(clippy::new_without_default)]
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let active_generation = Arc::new(AtomicUsize::new(0));
|
||||||
|
let events = DiagnosticTimeout {
|
||||||
|
active_generation: active_generation.clone(),
|
||||||
|
generation: 0,
|
||||||
|
}
|
||||||
|
.spawn();
|
||||||
|
Self {
|
||||||
|
active_generation,
|
||||||
|
generation: Cell::new(0),
|
||||||
|
events,
|
||||||
|
last_doc: Cell::new(DocumentId(NonZeroUsize::new(usize::MAX).unwrap())),
|
||||||
|
last_cursor_line: Cell::new(usize::MAX),
|
||||||
|
active: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DiagnosticsHandler {
|
||||||
|
pub fn immediately_show_diagnostic(&self, doc: &Document, view: ViewId) {
|
||||||
|
self.last_doc.set(doc.id());
|
||||||
|
let cursor_line = doc
|
||||||
|
.selection(view)
|
||||||
|
.primary()
|
||||||
|
.cursor_line(doc.text().slice(..));
|
||||||
|
self.last_cursor_line.set(cursor_line);
|
||||||
|
self.active_generation
|
||||||
|
.store(self.generation.get(), atomic::Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
pub fn show_cursorline_diagnostics(&self, doc: &Document, view: ViewId) -> bool {
|
||||||
|
if !self.active {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let cursor_line = doc
|
||||||
|
.selection(view)
|
||||||
|
.primary()
|
||||||
|
.cursor_line(doc.text().slice(..));
|
||||||
|
if self.last_cursor_line.get() == cursor_line && self.last_doc.get() == doc.id() {
|
||||||
|
let active_generation = self.active_generation.load(atomic::Ordering::Relaxed);
|
||||||
|
self.generation.get() == active_generation
|
||||||
|
} else {
|
||||||
|
self.last_doc.set(doc.id());
|
||||||
|
self.last_cursor_line.set(cursor_line);
|
||||||
|
self.generation.set(self.generation.get() + 1);
|
||||||
|
send_blocking(
|
||||||
|
&self.events,
|
||||||
|
DiagnosticEvent::CursorLineChanged {
|
||||||
|
generation: self.generation.get(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue