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

@ -16,6 +16,7 @@
- [`[editor.gutters.spacer]` Section](#editorguttersspacer-section) - [`[editor.gutters.spacer]` Section](#editorguttersspacer-section)
- [`[editor.soft-wrap]` Section](#editorsoft-wrap-section) - [`[editor.soft-wrap]` Section](#editorsoft-wrap-section)
- [`[editor.smart-tab]` Section](#editorsmart-tab-section) - [`[editor.smart-tab]` Section](#editorsmart-tab-section)
- [`[editor.inline-diagnostics]` Section](#editorinline-diagnostics-section)
### `[editor]` Section ### `[editor]` Section
@ -50,6 +51,7 @@
| `popup-border` | Draw border around `popup`, `menu`, `all`, or `none` | `none` | | `popup-border` | Draw border around `popup`, `menu`, `all`, or `none` | `none` |
| `indent-heuristic` | How the indentation for a newly inserted line is computed: `simple` just copies the indentation level from the previous line, `tree-sitter` computes the indentation based on the syntax tree and `hybrid` combines both approaches. If the chosen heuristic is not available, a different one will be used as a fallback (the fallback order being `hybrid` -> `tree-sitter` -> `simple`). | `hybrid` | `indent-heuristic` | How the indentation for a newly inserted line is computed: `simple` just copies the indentation level from the previous line, `tree-sitter` computes the indentation based on the syntax tree and `hybrid` combines both approaches. If the chosen heuristic is not available, a different one will be used as a fallback (the fallback order being `hybrid` -> `tree-sitter` -> `simple`). | `hybrid`
| `jump-label-alphabet` | The characters that are used to generate two character jump labels. Characters at the start of the alphabet are used first. | `"abcdefghijklmnopqrstuvwxyz"` | `jump-label-alphabet` | The characters that are used to generate two character jump labels. Characters at the start of the alphabet are used first. | `"abcdefghijklmnopqrstuvwxyz"`
| `end-of-line-diagnostics` | Minimum severity of diagnostics to render at the end of the line. Set to `disable` to disable entirely. Refer to the setting about `inline-diagnostics` for more details | "disable"
### `[editor.statusline]` Section ### `[editor.statusline]` Section
@ -393,3 +395,41 @@ S-tab = "move_parent_node_start"
tab = "extend_parent_node_end" tab = "extend_parent_node_end"
S-tab = "extend_parent_node_start" S-tab = "extend_parent_node_start"
``` ```
### `[editor.inline-diagnostics]` Section
Options for rendering diagnostics inside the text like shown below
```
fn main() {
let foo = bar;
└─ no such value in this scope
}
````
| Key | Description | Default |
|------------|-------------|---------|
| `cursor-line` | The minimum severity that a diagnostic must have to be shown inline on the line that contains the primary cursor. Set to `disable` to not show any diagnostics inline. This option does not have any effect when in insert-mode and will only take effect 350ms after moving the cursor to a different line. | `"disable"` |
| `other-lines` | The minimum severity that a diagnostic must have to be shown inline on a line that does not contain the cursor-line. Set to `disable` to not show any diagnostics inline. | `"disable"` |
| `prefix-len` | How many horizontal bars `─` are rendered before the diagnostic text. | `1` |
| `max-wrap` | Equivalent of the `editor.soft-wrap.max-wrap` option for diagnostics. | `20` |
| `max-diagnostics` | Maximum number of diagnostics to render inline for a given line | `10` |
The (first) diagnostic with the highest severity that is not shown inline is rendered at the end of the line (as long as its severity is higher than the `end-of-line-diagnostics` config option):
```
fn main() {
let baz = 1;
let foo = bar; a local variable with a similar name exists: baz
└─ no such value in this scope
}
```
The new diagnostic rendering is not yet enabled by default. As soon as end of line or inline diagnostics are enabled the old diagnostics rendering is automatically disabled. The recommended default setting are:
```
end-of-line-diagnostics = "hint"
[editor.inline-diagnostics]
cursor-line = "warning" # show warnings and errors on the cursorline inline
```

View file

@ -4,7 +4,8 @@ use std::fmt;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
/// Describes the severity level of a [`Diagnostic`]. /// Describes the severity level of a [`Diagnostic`].
#[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord, Deserialize, Serialize)] #[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Severity { pub enum Severity {
Hint, Hint,
Info, Info,
@ -25,6 +26,12 @@ pub struct Range {
pub end: usize, 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)] #[derive(Debug, Eq, Hash, PartialEq, Clone, Deserialize, Serialize)]
pub enum NumberOrString { pub enum NumberOrString {
Number(i32), Number(i32),
@ -71,3 +78,10 @@ impl fmt::Display for LanguageServerId {
write!(f, "{:?}", self.0) write!(f, "{:?}", self.0)
} }
} }
impl Diagnostic {
#[inline]
pub fn severity(&self) -> Severity {
self.severity.unwrap_or(Severity::Warning)
}
}

View file

@ -28,6 +28,11 @@ pub enum Grapheme<'a> {
} }
impl<'a> 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> { pub fn new(g: GraphemeStr<'a>, visual_x: usize, tab_width: u16) -> Grapheme<'a> {
match g { match g {
g if g == "\t" => Grapheme::Tab { g if g == "\t" => Grapheme::Tab {

View file

@ -53,8 +53,8 @@ pub use {regex, tree_sitter};
pub use graphemes::RopeGraphemes; pub use graphemes::RopeGraphemes;
pub use position::{ pub use position::{
char_idx_at_visual_offset, coords_at_pos, pos_at_coords, visual_offset_from_anchor, char_idx_at_visual_offset, coords_at_pos, pos_at_coords, softwrapped_dimensions,
visual_offset_from_block, Position, VisualOffsetError, visual_offset_from_anchor, visual_offset_from_block, Position, VisualOffsetError,
}; };
#[allow(deprecated)] #[allow(deprecated)]
pub use position::{pos_at_visual_coords, visual_coords_at_pos}; pub use position::{pos_at_visual_coords, visual_coords_at_pos};

View file

@ -171,6 +171,17 @@ pub fn visual_offset_from_block(
(last_pos, block_start) (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)] #[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum VisualOffsetError { pub enum VisualOffsetError {
PosBeforeAnchorRow, PosBeforeAnchorRow,

View file

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

View file

@ -114,7 +114,7 @@ pub fn render_document(
} }
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub fn render_text<'t>( pub fn render_text(
renderer: &mut TextRenderer, renderer: &mut TextRenderer,
text: RopeSlice<'_>, text: RopeSlice<'_>,
anchor: usize, anchor: usize,
@ -348,6 +348,44 @@ impl<'a> TextRenderer<'a> {
offset, 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`. /// Draws a single `grapheme` at the current render position with a specified `style`.
pub fn draw_grapheme( pub fn draw_grapheme(

View file

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

View file

@ -0,0 +1 @@
pub mod diagnostics;

View file

@ -0,0 +1,309 @@
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 {
// 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: DiagnosticFilter::Disable,
other_lines: DiagnosticFilter::Disable,
..
}
);
!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_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)
}
}

View file

@ -1,5 +1,6 @@
use crate::{ use crate::{
align_view, align_view,
annotations::diagnostics::{DiagnosticFilter, InlineDiagnosticsConfig},
document::{ document::{
DocumentOpenError, DocumentSavedEventFuture, DocumentSavedEventResult, Mode, SavePoint, DocumentOpenError, DocumentSavedEventFuture, DocumentSavedEventResult, Mode, SavePoint,
}, },
@ -343,6 +344,9 @@ pub struct Config {
deserialize_with = "deserialize_alphabet" deserialize_with = "deserialize_alphabet"
)] )]
pub jump_label_alphabet: Vec<char>, pub jump_label_alphabet: Vec<char>,
/// Display diagnostic below the line they occur.
pub inline_diagnostics: InlineDiagnosticsConfig,
pub end_of_line_diagnostics: DiagnosticFilter,
} }
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Eq, PartialOrd, Ord)] #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Eq, PartialOrd, Ord)]
@ -975,6 +979,8 @@ impl Default for Config {
popup_border: PopupBorderConfig::None, popup_border: PopupBorderConfig::None,
indent_heuristic: IndentationHeuristic::default(), indent_heuristic: IndentationHeuristic::default(),
jump_label_alphabet: ('a'..='z').collect(), jump_label_alphabet: ('a'..='z').collect(),
inline_diagnostics: InlineDiagnosticsConfig::default(),
end_of_line_diagnostics: DiagnosticFilter::Disable,
} }
} }
} }

View file

@ -1,6 +1,7 @@
#[macro_use] #[macro_use]
pub mod macros; pub mod macros;
pub mod annotations;
pub mod base64; pub mod base64;
pub mod clipboard; pub mod clipboard;
pub mod document; pub mod document;

View file

@ -1,5 +1,6 @@
use crate::{ use crate::{
align_view, align_view,
annotations::diagnostics::InlineDiagnostics,
document::DocumentInlayHints, document::DocumentInlayHints,
editor::{GutterConfig, GutterType}, editor::{GutterConfig, GutterType},
graphics::Rect, graphics::Rect,
@ -438,18 +439,15 @@ impl View {
text_annotations.add_overlay(labels, style); text_annotations.add_overlay(labels, style);
} }
let DocumentInlayHints { if let Some(DocumentInlayHints {
id: _, id: _,
type_inlay_hints, type_inlay_hints,
parameter_inlay_hints, parameter_inlay_hints,
other_inlay_hints, other_inlay_hints,
padding_before_inlay_hints, padding_before_inlay_hints,
padding_after_inlay_hints, padding_after_inlay_hints,
} = match doc.inlay_hints.get(&self.id) { }) = doc.inlay_hints.get(&self.id)
Some(doc_inlay_hints) => doc_inlay_hints, {
None => return text_annotations,
};
let type_style = theme let type_style = theme
.and_then(|t| t.find_scope_index("ui.virtual.inlay-hint.type")) .and_then(|t| t.find_scope_index("ui.virtual.inlay-hint.type"))
.map(Highlight); .map(Highlight);
@ -469,6 +467,23 @@ impl View {
.add_inline_annotations(parameter_inlay_hints, parameter_style) .add_inline_annotations(parameter_inlay_hints, parameter_style)
.add_inline_annotations(other_inlay_hints, other_style) .add_inline_annotations(other_inlay_hints, other_style)
.add_inline_annotations(padding_after_inlay_hints, None); .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,
cursor,
width,
self.offset.horizontal_offset,
config,
));
}
text_annotations text_annotations
} }