mirror of
https://github.com/helix-editor/helix.git
synced 2024-11-21 17:06:18 +04:00
render diagnostic inline
This commit is contained in:
parent
39b3d81abf
commit
6d051d7084
@ -16,6 +16,7 @@ ## Editor
|
||||
- [`[editor.gutters.spacer]` Section](#editorguttersspacer-section)
|
||||
- [`[editor.soft-wrap]` Section](#editorsoft-wrap-section)
|
||||
- [`[editor.smart-tab]` Section](#editorsmart-tab-section)
|
||||
- [`[editor.inline-diagnostics]` Section](#editorinline-diagnostics-section)
|
||||
|
||||
### `[editor]` Section
|
||||
|
||||
@ -50,6 +51,7 @@ ### `[editor]` Section
|
||||
| `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`
|
||||
| `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
|
||||
|
||||
@ -393,3 +395,41 @@ ### `[editor.smart-tab]` Section
|
||||
tab = "extend_parent_node_end"
|
||||
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
|
||||
```
|
||||
|
@ -4,7 +4,8 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// 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 {
|
||||
Hint,
|
||||
Info,
|
||||
@ -25,6 +26,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),
|
||||
@ -71,3 +78,10 @@ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:?}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Diagnostic {
|
||||
#[inline]
|
||||
pub fn severity(&self) -> Severity {
|
||||
self.severity.unwrap_or(Severity::Warning)
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -53,8 +53,8 @@ pub mod unicode {
|
||||
|
||||
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};
|
||||
|
@ -171,6 +171,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,
|
||||
|
@ -1711,6 +1711,7 @@ pub fn scroll(cx: &mut Context, offset: usize, direction: Direction, sync_cursor
|
||||
&mut annotations,
|
||||
)
|
||||
});
|
||||
drop(annotations);
|
||||
doc.set_selection(view.id, selection);
|
||||
return;
|
||||
}
|
||||
|
@ -114,7 +114,7 @@ pub fn render_document(
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn render_text<'t>(
|
||||
pub fn render_text(
|
||||
renderer: &mut TextRenderer,
|
||||
text: RopeSlice<'_>,
|
||||
anchor: usize,
|
||||
@ -348,6 +348,44 @@ pub fn new(
|
||||
offset,
|
||||
}
|
||||
}
|
||||
/// Draws a single `grapheme` at the current render position with a specified `style`.
|
||||
pub fn draw_decoration_grapheme(
|
||||
&mut self,
|
||||
grapheme: Grapheme,
|
||||
mut style: Style,
|
||||
mut row: u16,
|
||||
col: u16,
|
||||
) -> bool {
|
||||
if (row as usize) < self.offset.row
|
||||
|| row >= self.viewport.height
|
||||
|| col >= self.viewport.width
|
||||
{
|
||||
return false;
|
||||
}
|
||||
row -= self.offset.row as u16;
|
||||
// TODO is it correct to apply the whitspace style to all unicode white spaces?
|
||||
if grapheme.is_whitespace() {
|
||||
style = style.patch(self.whitespace_style);
|
||||
}
|
||||
|
||||
let grapheme = match grapheme {
|
||||
Grapheme::Tab { width } => {
|
||||
let grapheme_tab_width = char_to_byte_idx(&self.virtual_tab, width);
|
||||
&self.virtual_tab[..grapheme_tab_width]
|
||||
}
|
||||
Grapheme::Other { ref g } if g == "\u{00A0}" => " ",
|
||||
Grapheme::Other { ref g } => g,
|
||||
Grapheme::Newline => " ",
|
||||
};
|
||||
|
||||
self.surface.set_string(
|
||||
self.viewport.x + col,
|
||||
self.viewport.y + row,
|
||||
grapheme,
|
||||
style,
|
||||
);
|
||||
true
|
||||
}
|
||||
|
||||
/// Draws a single `grapheme` at the current render position with a specified `style`.
|
||||
pub fn draw_grapheme(
|
||||
|
@ -22,6 +22,7 @@
|
||||
visual_offset_from_block, Change, Position, Range, Selection, Transaction,
|
||||
};
|
||||
use helix_view::{
|
||||
annotations::diagnostics::DiagnosticFilter,
|
||||
document::{Mode, SavePoint, SCRATCH_BUFFER_NAME},
|
||||
editor::{CompleteAction, CursorShapeConfig},
|
||||
graphics::{Color, CursorKind, Modifier, Rect, Style},
|
||||
@ -185,6 +186,12 @@ pub fn render_view(
|
||||
primary_cursor,
|
||||
});
|
||||
}
|
||||
decorations.add_decoration(InlineDiagnostics::new(
|
||||
doc,
|
||||
theme,
|
||||
primary_cursor,
|
||||
config.lsp.inline_diagnostics.clone(),
|
||||
));
|
||||
render_document(
|
||||
surface,
|
||||
inner,
|
||||
@ -210,7 +217,11 @@ pub fn render_view(
|
||||
}
|
||||
}
|
||||
|
||||
Self::render_diagnostics(doc, view, inner, surface, theme);
|
||||
if config.inline_diagnostics.disabled()
|
||||
&& config.end_of_line_diagnostics == DiagnosticFilter::Disable
|
||||
{
|
||||
Self::render_diagnostics(doc, view, inner, surface, theme);
|
||||
}
|
||||
|
||||
let statusline_area = view
|
||||
.area
|
||||
|
305
helix-term/src/ui/text_decorations/diagnostics.rs
Normal file
305
helix-term/src/ui/text_decorations/diagnostics.rs
Normal 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)
|
||||
}
|
||||
}
|
1
helix-view/src/annotations.rs
Normal file
1
helix-view/src/annotations.rs
Normal file
@ -0,0 +1 @@
|
||||
pub mod diagnostics;
|
309
helix-view/src/annotations/diagnostics.rs
Normal file
309
helix-view/src/annotations/diagnostics.rs
Normal 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)
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
use crate::{
|
||||
align_view,
|
||||
annotations::diagnostics::{DiagnosticFilter, InlineDiagnosticsConfig},
|
||||
document::{
|
||||
DocumentOpenError, DocumentSavedEventFuture, DocumentSavedEventResult, Mode, SavePoint,
|
||||
},
|
||||
@ -343,6 +344,9 @@ pub struct Config {
|
||||
deserialize_with = "deserialize_alphabet"
|
||||
)]
|
||||
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)]
|
||||
@ -975,6 +979,8 @@ fn default() -> Self {
|
||||
popup_border: PopupBorderConfig::None,
|
||||
indent_heuristic: IndentationHeuristic::default(),
|
||||
jump_label_alphabet: ('a'..='z').collect(),
|
||||
inline_diagnostics: InlineDiagnosticsConfig::default(),
|
||||
end_of_line_diagnostics: DiagnosticFilter::Disable,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
#[macro_use]
|
||||
pub mod macros;
|
||||
|
||||
pub mod annotations;
|
||||
pub mod base64;
|
||||
pub mod clipboard;
|
||||
pub mod document;
|
||||
|
@ -1,5 +1,6 @@
|
||||
use crate::{
|
||||
align_view,
|
||||
annotations::diagnostics::InlineDiagnostics,
|
||||
document::DocumentInlayHints,
|
||||
editor::{GutterConfig, GutterType},
|
||||
graphics::Rect,
|
||||
@ -438,37 +439,51 @@ pub fn text_annotations<'a>(
|
||||
text_annotations.add_overlay(labels, style);
|
||||
}
|
||||
|
||||
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,
|
||||
cursor,
|
||||
width,
|
||||
self.offset.horizontal_offset,
|
||||
config,
|
||||
));
|
||||
}
|
||||
|
||||
text_annotations
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user