mirror of https://github.com/helix-editor/helix
314 lines
9.4 KiB
Rust
314 lines
9.4 KiB
Rust
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 somewhere?
|
|
// 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()
|
|
.is_some_and(|&(_, 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)
|
|
}
|
|
}
|