From 4de41cc0585a69d5abdfb643734d5370cab49244 Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Tue, 21 Mar 2023 00:36:46 +0100 Subject: [PATCH] Show LSP diagnostics inline --- book/src/configuration.md | 1 + book/src/themes.md | 1 + helix-core/src/diagnostic.rs | 12 + helix-core/src/graphemes.rs | 5 + helix-core/src/lib.rs | 4 +- helix-core/src/position.rs | 11 + helix-term/src/ui/document.rs | 36 +++ helix-term/src/ui/editor.rs | 21 +- helix-term/src/ui/text_decorations.rs | 13 +- .../src/ui/text_decorations/diagnostics.rs | 247 ++++++++++++++++ helix-view/src/annotations.rs | 1 + helix-view/src/annotations/diagnostics.rs | 267 ++++++++++++++++++ helix-view/src/document.rs | 5 +- helix-view/src/editor.rs | 7 + helix-view/src/lib.rs | 1 + helix-view/src/view.rs | 63 +++-- 16 files changed, 656 insertions(+), 39 deletions(-) create mode 100644 helix-term/src/ui/text_decorations/diagnostics.rs create mode 100644 helix-view/src/annotations.rs create mode 100644 helix-view/src/annotations/diagnostics.rs diff --git a/book/src/configuration.md b/book/src/configuration.md index bf31499374e82..87bfda2851bcd 100644 --- a/book/src/configuration.md +++ b/book/src/configuration.md @@ -123,6 +123,7 @@ The following statusline elements can be configured: | `auto-signature-help` | Enable automatic popup of signature help (parameter hints) | `true` | | `display-inlay-hints` | Display inlay hints[^2] | `false` | | `display-signature-help-docs` | Display docs under signature help popup | `true` | +| `display-inline-diagnostics` | Display diagnostics under their starting line | `true` | [^1]: By default, a progress spinner is shown in the statusline beside the file path. [^2]: You may also have to activate them in the LSP config for them to appear, not just in Helix. diff --git a/book/src/themes.md b/book/src/themes.md index 994542c5a7d26..bb974caa98253 100644 --- a/book/src/themes.md +++ b/book/src/themes.md @@ -295,6 +295,7 @@ These scopes are used for theming the editor interface: | `ui.text.inactive` | Same as `ui.text` but when the text is inactive (e.g. suggestions) | | `ui.text.info` | The key: command text in `ui.popup.info` boxes | | `ui.virtual.ruler` | Ruler columns (see the [`editor.rulers` config][editor-section]) | +| `ui.virtual.diagnostics` | Default style for inline diagnostics lines (notably control the background) | | `ui.virtual.whitespace` | Visible whitespace characters | | `ui.virtual.indent-guide` | Vertical indent width guides | | `ui.virtual.inlay-hint` | Default style for inlay hints of all kinds | diff --git a/helix-core/src/diagnostic.rs b/helix-core/src/diagnostic.rs index 58ddb0383a0ad..69c3d33774924 100644 --- a/helix-core/src/diagnostic.rs +++ b/helix-core/src/diagnostic.rs @@ -23,6 +23,12 @@ pub struct Range { pub end: usize, } +impl Range { + pub fn contains(self, pos: usize) -> bool { + (self.start..self.end).contains(&pos) + } +} + #[derive(Debug, Eq, Hash, PartialEq, Clone, Deserialize, Serialize)] pub enum NumberOrString { Number(i32), @@ -47,3 +53,9 @@ pub struct Diagnostic { pub source: Option, pub data: Option, } + +impl Diagnostic { + pub fn severity(&self) -> Severity { + self.severity.unwrap_or(Severity::Warning) + } +} diff --git a/helix-core/src/graphemes.rs b/helix-core/src/graphemes.rs index 15ef3eb043e81..fca19ca2cf1ab 100644 --- a/helix-core/src/graphemes.rs +++ b/helix-core/src/graphemes.rs @@ -28,6 +28,11 @@ pub enum Grapheme<'a> { } impl<'a> Grapheme<'a> { + pub fn new_decoration(g: &'static str) -> Grapheme<'a> { + assert_ne!(g, "\t"); + Grapheme::new(g.into(), 0, 0) + } + pub fn new(g: GraphemeStr<'a>, visual_x: usize, tab_width: u16) -> Grapheme<'a> { match g { g if g == "\t" => Grapheme::Tab { diff --git a/helix-core/src/lib.rs b/helix-core/src/lib.rs index 4d50e48bf14bf..00ccdb91f58bd 100644 --- a/helix-core/src/lib.rs +++ b/helix-core/src/lib.rs @@ -97,8 +97,8 @@ pub use {regex, tree_sitter}; pub use graphemes::RopeGraphemes; pub use position::{ - char_idx_at_visual_offset, coords_at_pos, pos_at_coords, visual_offset_from_anchor, - visual_offset_from_block, Position, VisualOffsetError, + char_idx_at_visual_offset, coords_at_pos, pos_at_coords, softwrapped_dimensions, + visual_offset_from_anchor, visual_offset_from_block, Position, VisualOffsetError, }; #[allow(deprecated)] pub use position::{pos_at_visual_coords, visual_coords_at_pos}; diff --git a/helix-core/src/position.rs b/helix-core/src/position.rs index 059c8e32d72dc..22fbecc5bf082 100644 --- a/helix-core/src/position.rs +++ b/helix-core/src/position.rs @@ -135,6 +135,17 @@ pub fn visual_offset_from_block( (last_pos, block_start) } +/// Returns the height of the given text when softwrapping +pub fn softwrapped_dimensions(text: RopeSlice, text_fmt: &TextFormat) -> (usize, u16) { + let last_pos = + visual_offset_from_block(text, 0, usize::MAX, text_fmt, &TextAnnotations::default()).0; + if last_pos.row == 0 { + (1, last_pos.col as u16) + } else { + (last_pos.row + 1, text_fmt.viewport_width) + } +} + #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum VisualOffsetError { PosBeforeAnchorRow, diff --git a/helix-term/src/ui/document.rs b/helix-term/src/ui/document.rs index ccf5e44783df2..095be2d55f048 100644 --- a/helix-term/src/ui/document.rs +++ b/helix-term/src/ui/document.rs @@ -290,6 +290,42 @@ impl<'a> TextRenderer<'a> { col_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, + row: u16, + col: u16, + ) -> bool { + if row >= self.viewport.height || col >= self.viewport.width { + return false; + } + let is_whitespace = grapheme.is_whitespace(); + + // TODO is it correct to apply the whitspace style to all unicode white spaces? + if 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 as u16, + grapheme, + style, + ); + true + } /// Draws a single `grapheme` at the current render position with a specified `style`. pub fn draw_grapheme( diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 5cbe05310d716..d1f316ce33cc4 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -6,7 +6,7 @@ use crate::{ keymap::{KeymapResult, Keymaps}, ui::{ document::{render_document, LinePos, TextRenderer}, - text_decorations::{self, Decoration, DecorationManager}, + text_decorations::{self, Decoration, DecorationManager, InlineDiagnostics}, Completion, ProgressSpinners, }, }; @@ -187,17 +187,24 @@ impl EditorView { is_focused, &mut decorations, ); - + let primary_cursor = doc + .selection(view.id) + .primary() + .cursor(doc.text().slice(..)); if is_focused { decorations.add_decoration(text_decorations::Cursor { cache: &editor.cursor_cache, - primary_cursor: doc - .selection(view.id) - .primary() - .cursor(doc.text().slice(..)), + primary_cursor, }); } - + if config.lsp.inline_diagnostics.enable(inner.width) { + decorations.add_decoration(InlineDiagnostics::new( + doc.diagnostics(), + theme, + primary_cursor, + config.lsp.inline_diagnostics.clone(), + )); + } render_document( surface, inner, diff --git a/helix-term/src/ui/text_decorations.rs b/helix-term/src/ui/text_decorations.rs index bf08da3c0fd8d..23b150533989f 100644 --- a/helix-term/src/ui/text_decorations.rs +++ b/helix-term/src/ui/text_decorations.rs @@ -5,6 +5,10 @@ use helix_view::editor::CursorCache; use crate::ui::document::{LinePos, TextRenderer}; +pub use diagnostics::InlineDiagnostics; + +mod diagnostics; + /// Decorations are the primary mechanisim for extending the text rendering. /// /// Any on-screen element which is anchored to the rendered text in some form should @@ -54,8 +58,8 @@ pub trait Decoration { &mut self, _renderer: &mut TextRenderer, _pos: LinePos, - _virt_off: usize, - ) -> usize { + _virt_off: u16, + ) -> u16 { 0 } @@ -126,8 +130,11 @@ impl<'a> DecorationManager<'a> { } pub fn render_virtual_lines(&mut self, renderer: &mut TextRenderer, pos: LinePos) { - let mut virt_off = 0; + let mut virt_off = 1; // start at 1 so the line is never overwritten for (decoration, _) in &mut self.decorations { + if pos.visual_line + virt_off >= renderer.viewport.height { + break; + } virt_off += decoration.render_virt_lines(renderer, pos, virt_off); } } 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 0000000000000..19e02ff3cfaac --- /dev/null +++ b/helix-term/src/ui/text_decorations/diagnostics.rs @@ -0,0 +1,247 @@ +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; +use helix_view::annotations::diagnostics::{InlineDiagnosticAccumulator, InlineDiagnosticsConfig}; + +use helix_view::theme::Style; +use helix_view::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>, + styles: Styles, +} + +impl<'a> InlineDiagnostics<'a> { + pub fn new( + diagnostics: &'a [Diagnostic], + theme: &Theme, + cursor: usize, + config: InlineDiagnosticsConfig, + ) -> Self { + InlineDiagnostics { + state: InlineDiagnosticAccumulator::new(cursor, diagnostics, config), + styles: Styles::new(theme), + } + } +} + +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_diagnostic(&mut self, diag: &Diagnostic, col: u16, next_severity: Option) { + 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().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: u16, + ) -> u16 { + self.state.compute_line_diagnostics(); + let mut renderer = Renderer { + renderer, + first_row: pos.visual_line + virt_off, + row: pos.visual_line + virt_off, + config: &self.state.config, + styles: &self.styles, + }; + renderer.draw_multi_diagnostics(&mut self.state.stack); + renderer.draw_diagnostics(&mut self.state.stack); + renderer.row - renderer.first_row + } + + 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.col_offset) + } +} diff --git a/helix-view/src/annotations.rs b/helix-view/src/annotations.rs new file mode 100644 index 0000000000000..4c630487f1cb5 --- /dev/null +++ b/helix-view/src/annotations.rs @@ -0,0 +1 @@ +pub mod diagnostics; diff --git a/helix-view/src/annotations/diagnostics.rs b/helix-view/src/annotations/diagnostics.rs new file mode 100644 index 0000000000000..eaee014c1fdb3 --- /dev/null +++ b/helix-view/src/annotations/diagnostics.rs @@ -0,0 +1,267 @@ +use helix_core::diagnostic::Severity; +use helix_core::doc_formatter::{FormattedGrapheme, TextFormat}; +use helix_core::text_annotations::LineAnnotation; +use helix_core::{softwrapped_dimensions, Diagnostic}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case", deny_unknown_fields, untagged)] +pub enum SeverityFilter { + AtLeast(Severity), + OneOf(Box<[Severity]>), +} + +impl SeverityFilter { + pub fn matches(&self, severity: Severity) -> bool { + match self { + SeverityFilter::AtLeast(min) => severity >= *min, + SeverityFilter::OneOf(list) => list.contains(&severity), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(default, rename_all = "kebab-case", deny_unknown_fields)] +pub struct InlineDiagnosticsConfig { + pub cursor_line: SeverityFilter, + pub other_lines: SeverityFilter, + pub min_diagnostic_width: u16, + pub prefix_len: u16, + pub max_warp: u16, + pub max_diagnostics: usize, +} + +impl InlineDiagnosticsConfig { + // last column where to start diagnostics + // every diagnostics that start afterwards will be displayed with a "backwards + // line" to ensure they are still rendered with `min_diagnostic_widht`. If `width` + // it too small to display diagnostics with atleast `min_diagnostic_width` space + // (or inline diagnostics are displed) `None` is returned. In that case inline + // diagnostics should not be shown + pub fn enable(&self, width: u16) -> bool { + let disabled = matches!( + self, + Self { + cursor_line: SeverityFilter::OneOf(cursor_line), + other_lines: SeverityFilter::OneOf(other_lines), + .. + } if cursor_line.is_empty() && other_lines.is_empty() + ); + !disabled && width >= self.min_diagnostic_width + self.prefix_len + } + + 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_warp.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: SeverityFilter::AtLeast(Severity::Hint), + other_lines: SeverityFilter::AtLeast(Severity::Warning), + min_diagnostic_width: 40, + prefix_len: 1, + max_warp: 20, + max_diagnostics: 20, + } + } +} + +#[derive(Default)] +pub struct InlineDiagnosticAccumulator<'a> { + idx: usize, + pub stack: Vec<(&'a Diagnostic, u16)>, + diagnostics: &'a [Diagnostic], + pub config: InlineDiagnosticsConfig, + cursor: usize, + cursor_line: bool, +} + +impl<'a> InlineDiagnosticAccumulator<'a> { + pub fn new( + cursor: usize, + diagnostics: &'a [Diagnostic], + config: InlineDiagnosticsConfig, + ) -> Self { + InlineDiagnosticAccumulator { + idx: 0, + stack: Vec::new(), + diagnostics, + config, + cursor, + cursor_line: false, + } + } + + pub fn reset_pos(&mut self, char_idx: usize) -> usize { + self.idx = 0; + self.skip_concealed(char_idx) + } + + pub fn skip_concealed(&mut self, conceal_end_char_idx: usize) -> usize { + let diagnostics = &self.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 + .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 + } + } + + 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 centrilize this somehwere? + // In the DocFormatter? + if grapheme.char_idx == self.cursor { + self.cursor_line = true; + if self + .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.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.diagnostics[self.idx..] + .iter() + .take_while(|diag| diag.range.start == grapheme.char_idx) + .count(); + } + self.next_anchor(grapheme.char_idx + 1) + } + + 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 + }; + self.stack + .retain(|(diag, _)| filter.matches(diag.severity())); + 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( + diagnostics: &'a [Diagnostic], + cursor: usize, + width: u16, + horizontal_off: usize, + config: InlineDiagnosticsConfig, + ) -> Box { + Box::new(InlineDiagnostics { + state: InlineDiagnosticAccumulator::new(cursor, diagnostics, 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, + _vertical_off: usize, + _doc_line: usize, + ) -> usize { + 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().into(), &text_fmt).0 + }) + .sum(); + multi as usize + diagostic_height + } +} diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index e4782c480a379..4afb79d4bdca2 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -95,7 +95,6 @@ impl Serialize for Mode { serializer.collect_str(self) } } - /// A snapshot of the text of a document that we want to write out to disk #[derive(Debug, Clone)] pub struct DocumentSavedEvent { @@ -982,7 +981,7 @@ impl Document { diagnostic.line = self.text.char_to_line(diagnostic.range.start); } self.diagnostics - .sort_unstable_by_key(|diagnostic| diagnostic.range); + .sort_by_key(|diagnostic| (diagnostic.range.start, diagnostic.severity)); // Update the inlay hint annotations' positions, helping ensure they are displayed in the proper place let apply_inlay_hint_changes = |annotations: &mut Rc<[InlineAnnotation]>| { @@ -1388,7 +1387,7 @@ impl Document { pub fn set_diagnostics(&mut self, diagnostics: Vec) { self.diagnostics = diagnostics; self.diagnostics - .sort_unstable_by_key(|diagnostic| diagnostic.range); + .sort_by_key(|diagnostic| (diagnostic.range.start, diagnostic.severity)); } /// Get the document's auto pairs. If the document has a recognized diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 0118bbbfcd749..9377fce0fae23 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -1,5 +1,6 @@ use crate::{ align_view, + annotations::diagnostics::InlineDiagnosticsConfig, clipboard::{get_clipboard_provider, ClipboardProvider}, document::{DocumentSavedEventFuture, DocumentSavedEventResult, Mode}, graphics::{CursorKind, Rect}, @@ -349,6 +350,10 @@ pub struct LspConfig { pub display_signature_help_docs: bool, /// Display inlay hints pub display_inlay_hints: bool, + /// Display diagnostic on the same line they occur automatically. + /// Also called "error lens"-style diagnostics, in reference to the popular VSCode extension. + pub inline_diagnostics: InlineDiagnosticsConfig, + pub display_diagnostic_message: bool, } impl Default for LspConfig { @@ -359,6 +364,8 @@ impl Default for LspConfig { auto_signature_help: true, display_signature_help_docs: true, display_inlay_hints: false, + inline_diagnostics: InlineDiagnosticsConfig::default(), + display_diagnostic_message: false, } } } diff --git a/helix-view/src/lib.rs b/helix-view/src/lib.rs index c3f67345b3618..33686ceec4cb3 100644 --- a/helix-view/src/lib.rs +++ b/helix-view/src/lib.rs @@ -1,6 +1,7 @@ #[macro_use] pub mod macros; +pub mod annotations; pub mod clipboard; pub mod document; pub mod editor; diff --git a/helix-view/src/view.rs b/helix-view/src/view.rs index 38dd90a02300f..763fd8d3944d0 100644 --- a/helix-view/src/view.rs +++ b/helix-view/src/view.rs @@ -1,5 +1,6 @@ use crate::{ align_view, + annotations::diagnostics::InlineDiagnostics, document::DocumentInlayHints, editor::{GutterConfig, GutterType}, graphics::Rect, @@ -409,37 +410,51 @@ impl View { ) -> TextAnnotations<'a> { let mut text_annotations = TextAnnotations::default(); - let DocumentInlayHints { + if let Some(DocumentInlayHints { id: _, type_inlay_hints, parameter_inlay_hints, other_inlay_hints, padding_before_inlay_hints, padding_after_inlay_hints, - } = match doc.inlay_hints.get(&self.id) { - Some(doc_inlay_hints) => doc_inlay_hints, - None => return text_annotations, + }) = doc.inlay_hints.get(&self.id) + { + let type_style = theme + .and_then(|t| t.find_scope_index("ui.virtual.inlay-hint.type")) + .map(Highlight); + let parameter_style = theme + .and_then(|t| t.find_scope_index("ui.virtual.inlay-hint.parameter")) + .map(Highlight); + let other_style = theme + .and_then(|t| t.find_scope_index("ui.virtual.inlay-hint")) + .map(Highlight); + + // Overlapping annotations are ignored apart from the first so the order here is not random: + // types -> parameters -> others should hopefully be the "correct" order for most use cases, + // with the padding coming before and after as expected. + text_annotations + .add_inline_annotations(padding_before_inlay_hints, None) + .add_inline_annotations(type_inlay_hints, type_style) + .add_inline_annotations(parameter_inlay_hints, parameter_style) + .add_inline_annotations(other_inlay_hints, other_style) + .add_inline_annotations(padding_after_inlay_hints, None); }; - - let type_style = theme - .and_then(|t| t.find_scope_index("ui.virtual.inlay-hint.type")) - .map(Highlight); - let parameter_style = theme - .and_then(|t| t.find_scope_index("ui.virtual.inlay-hint.parameter")) - .map(Highlight); - let other_style = theme - .and_then(|t| t.find_scope_index("ui.virtual.inlay-hint")) - .map(Highlight); - - // Overlapping annotations are ignored apart from the first so the order here is not random: - // types -> parameters -> others should hopefully be the "correct" order for most use cases, - // with the padding coming before and after as expected. - text_annotations - .add_inline_annotations(padding_before_inlay_hints, None) - .add_inline_annotations(type_inlay_hints, type_style) - .add_inline_annotations(parameter_inlay_hints, parameter_style) - .add_inline_annotations(other_inlay_hints, other_style) - .add_inline_annotations(padding_after_inlay_hints, None); + let width = self.inner_width(doc); + let config = doc.config.load(); + if config.lsp.inline_diagnostics.enable(width) { + let config = config.lsp.inline_diagnostics.clone(); + let cursor = doc + .selection(self.id) + .primary() + .cursor(doc.text().slice(..)); + text_annotations.add_line_annotation(InlineDiagnostics::new( + doc.diagnostics(), + cursor, + width, + self.offset.horizontal_offset, + config, + )); + } text_annotations }