pull/14045/merge
iBrahizy 2025-07-24 14:36:53 -03:00 committed by GitHub
commit 085638be3a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 166 additions and 33 deletions

View File

@ -456,7 +456,7 @@ fn main() {
| `prefix-len` | How many horizontal bars `─` are rendered before the diagnostic text. | `1` | | `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-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` | | `max-diagnostics` | Maximum number of diagnostics to render inline for a given line | `10` |
| `timeout` | Time in milliseconds after moving the primary cursor before inline diagnostics are shown (if `cursor-line` is not set to `disable`). | `350` |
The allowed values for `cursor-line` and `other-lines` are: `error`, `warning`, `info`, `hint`. The allowed values for `cursor-line` and `other-lines` are: `error`, `warning`, `info`, `hint`.
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): 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):

View File

@ -1,3 +1,5 @@
use std::time::Duration;
use helix_core::diagnostic::Severity; use helix_core::diagnostic::Severity;
use helix_core::doc_formatter::{FormattedGrapheme, TextFormat}; use helix_core::doc_formatter::{FormattedGrapheme, TextFormat};
use helix_core::text_annotations::LineAnnotation; use helix_core::text_annotations::LineAnnotation;
@ -32,6 +34,26 @@ impl<'de> Deserialize<'de> for DiagnosticFilter {
} }
} }
fn deserialize_duration_millis<'de, D>(deserializer: D) -> Result<Duration, D::Error>
where
D: serde::Deserializer<'de>,
{
let millis = u64::deserialize(deserializer)?;
Ok(Duration::from_millis(millis))
}
fn serialize_duration_millis<S>(duration: &Duration, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_u64(
duration
.as_millis()
.try_into()
.map_err(|_| serde::ser::Error::custom("duration value overflowed u64"))?,
)
}
impl Serialize for DiagnosticFilter { impl Serialize for DiagnosticFilter {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where where
@ -57,6 +79,11 @@ pub struct InlineDiagnosticsConfig {
pub prefix_len: u16, pub prefix_len: u16,
pub max_wrap: u16, pub max_wrap: u16,
pub max_diagnostics: usize, pub max_diagnostics: usize,
#[serde(
serialize_with = "serialize_duration_millis",
deserialize_with = "deserialize_duration_millis"
)]
pub timeout: Duration,
} }
impl InlineDiagnosticsConfig { impl InlineDiagnosticsConfig {
@ -115,6 +142,7 @@ impl Default for InlineDiagnosticsConfig {
prefix_len: 1, prefix_len: 1,
max_wrap: 20, max_wrap: 20,
max_diagnostics: 10, max_diagnostics: 10,
timeout: Duration::from_millis(350),
} }
} }
} }

View File

@ -1727,7 +1727,13 @@ impl Editor {
.try_get(self.tree.focus) .try_get(self.tree.focus)
.filter(|v| id == v.doc) // Different Document .filter(|v| id == v.doc) // Different Document
.cloned() .cloned()
.unwrap_or_else(|| View::new(id, self.config().gutters.clone())); .unwrap_or_else(|| {
View::new(
id,
self.config().gutters.clone(),
self.config().inline_diagnostics.clone(),
)
});
let view_id = self.tree.split( let view_id = self.tree.split(
view, view,
match action { match action {
@ -1923,7 +1929,11 @@ impl Editor {
self.syn_loader.clone(), self.syn_loader.clone(),
)) ))
}); });
let view = View::new(doc_id, self.config().gutters.clone()); let view = View::new(
doc_id,
self.config().gutters.clone(),
self.config().inline_diagnostics.clone(),
);
let view_id = self.tree.insert(view); let view_id = self.tree.insert(view);
let doc = doc_mut!(self, &doc_id); let doc = doc_mut!(self, &doc_id);
doc.ensure_view_init(view_id); doc.ensure_view_init(view_id);

View File

@ -329,6 +329,7 @@ mod tests {
use std::sync::Arc; use std::sync::Arc;
use super::*; use super::*;
use crate::annotations::diagnostics::InlineDiagnosticsConfig;
use crate::document::Document; use crate::document::Document;
use crate::editor::{Config, GutterConfig, GutterLineNumbersConfig}; use crate::editor::{Config, GutterConfig, GutterLineNumbersConfig};
use crate::graphics::Rect; use crate::graphics::Rect;
@ -338,7 +339,11 @@ mod tests {
#[test] #[test]
fn test_default_gutter_widths() { fn test_default_gutter_widths() {
let mut view = View::new(DocumentId::default(), GutterConfig::default()); let mut view = View::new(
DocumentId::default(),
GutterConfig::default(),
InlineDiagnosticsConfig::default(),
);
view.area = Rect::new(40, 40, 40, 40); view.area = Rect::new(40, 40, 40, 40);
let rope = Rope::from_str("abc\n\tdef"); let rope = Rope::from_str("abc\n\tdef");
@ -364,7 +369,11 @@ mod tests {
..Default::default() ..Default::default()
}; };
let mut view = View::new(DocumentId::default(), gutters); let mut view = View::new(
DocumentId::default(),
gutters,
InlineDiagnosticsConfig::default(),
);
view.area = Rect::new(40, 40, 40, 40); view.area = Rect::new(40, 40, 40, 40);
let rope = Rope::from_str("abc\n\tdef"); let rope = Rope::from_str("abc\n\tdef");
@ -383,7 +392,11 @@ mod tests {
line_numbers: GutterLineNumbersConfig { min_width: 10 }, line_numbers: GutterLineNumbersConfig { min_width: 10 },
}; };
let mut view = View::new(DocumentId::default(), gutters); let mut view = View::new(
DocumentId::default(),
gutters,
InlineDiagnosticsConfig::default(),
);
view.area = Rect::new(40, 40, 40, 40); view.area = Rect::new(40, 40, 40, 40);
let rope = Rope::from_str("abc\n\tdef"); let rope = Rope::from_str("abc\n\tdef");
@ -406,7 +419,11 @@ mod tests {
line_numbers: GutterLineNumbersConfig { min_width: 1 }, line_numbers: GutterLineNumbersConfig { min_width: 1 },
}; };
let mut view = View::new(DocumentId::default(), gutters); let mut view = View::new(
DocumentId::default(),
gutters,
InlineDiagnosticsConfig::default(),
);
view.area = Rect::new(40, 40, 40, 40); view.area = Rect::new(40, 40, 40, 40);
let rope = Rope::from_str("a\nb"); let rope = Rope::from_str("a\nb");

View File

@ -8,6 +8,7 @@ use helix_event::{request_redraw, send_blocking, AsyncHook};
use tokio::sync::mpsc::Sender; use tokio::sync::mpsc::Sender;
use tokio::time::Instant; use tokio::time::Instant;
use crate::annotations::diagnostics::InlineDiagnosticsConfig;
use crate::{Document, DocumentId, ViewId}; use crate::{Document, DocumentId, ViewId};
#[derive(Debug)] #[derive(Debug)]
@ -19,10 +20,9 @@ pub enum DiagnosticEvent {
struct DiagnosticTimeout { struct DiagnosticTimeout {
active_generation: Arc<AtomicUsize>, active_generation: Arc<AtomicUsize>,
generation: usize, generation: usize,
timeout: Duration,
} }
const TIMEOUT: Duration = Duration::from_millis(350);
impl AsyncHook for DiagnosticTimeout { impl AsyncHook for DiagnosticTimeout {
type Event = DiagnosticEvent; type Event = DiagnosticEvent;
@ -35,12 +35,12 @@ impl AsyncHook for DiagnosticTimeout {
DiagnosticEvent::CursorLineChanged { generation } => { DiagnosticEvent::CursorLineChanged { generation } => {
if generation > self.generation { if generation > self.generation {
self.generation = generation; self.generation = generation;
Some(Instant::now() + TIMEOUT) Some(Instant::now() + self.timeout)
} else { } else {
timeout timeout
} }
} }
DiagnosticEvent::Refresh if timeout.is_some() => Some(Instant::now() + TIMEOUT), DiagnosticEvent::Refresh if timeout.is_some() => Some(Instant::now() + self.timeout),
DiagnosticEvent::Refresh => None, DiagnosticEvent::Refresh => None,
} }
} }
@ -61,6 +61,7 @@ pub struct DiagnosticsHandler {
last_cursor_line: Cell<usize>, last_cursor_line: Cell<usize>,
pub active: bool, pub active: bool,
pub events: Sender<DiagnosticEvent>, pub events: Sender<DiagnosticEvent>,
config: InlineDiagnosticsConfig,
} }
// make sure we never share handlers across multiple views this is a stop // make sure we never share handlers across multiple views this is a stop
@ -69,17 +70,18 @@ pub struct DiagnosticsHandler {
// but to fix that larger architecutre changes are needed // but to fix that larger architecutre changes are needed
impl Clone for DiagnosticsHandler { impl Clone for DiagnosticsHandler {
fn clone(&self) -> Self { fn clone(&self) -> Self {
Self::new() Self::new(self.config.clone())
} }
} }
impl DiagnosticsHandler { impl DiagnosticsHandler {
#[allow(clippy::new_without_default)] #[allow(clippy::new_without_default)]
pub fn new() -> Self { pub fn new(inline_diagnostics_config: InlineDiagnosticsConfig) -> Self {
let active_generation = Arc::new(AtomicUsize::new(0)); let active_generation = Arc::new(AtomicUsize::new(0));
let events = DiagnosticTimeout { let events = DiagnosticTimeout {
active_generation: active_generation.clone(), active_generation: active_generation.clone(),
generation: 0, generation: 0,
timeout: inline_diagnostics_config.timeout,
} }
.spawn(); .spawn();
Self { Self {
@ -89,6 +91,7 @@ impl DiagnosticsHandler {
last_doc: Cell::new(DocumentId(NonZeroUsize::new(usize::MAX).unwrap())), last_doc: Cell::new(DocumentId(NonZeroUsize::new(usize::MAX).unwrap())),
last_cursor_line: Cell::new(usize::MAX), last_cursor_line: Cell::new(usize::MAX),
active: true, active: true,
config: inline_diagnostics_config,
} }
} }
} }

View File

@ -725,6 +725,7 @@ impl DoubleEndedIterator for Traverse<'_> {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;
use crate::annotations::diagnostics::InlineDiagnosticsConfig;
use crate::editor::GutterConfig; use crate::editor::GutterConfig;
use crate::DocumentId; use crate::DocumentId;
@ -736,22 +737,38 @@ mod test {
width: 180, width: 180,
height: 80, height: 80,
}); });
let mut view = View::new(DocumentId::default(), GutterConfig::default()); let mut view = View::new(
DocumentId::default(),
GutterConfig::default(),
InlineDiagnosticsConfig::default(),
);
view.area = Rect::new(0, 0, 180, 80); view.area = Rect::new(0, 0, 180, 80);
tree.insert(view); tree.insert(view);
let l0 = tree.focus; let l0 = tree.focus;
let view = View::new(DocumentId::default(), GutterConfig::default()); let view = View::new(
DocumentId::default(),
GutterConfig::default(),
InlineDiagnosticsConfig::default(),
);
tree.split(view, Layout::Vertical); tree.split(view, Layout::Vertical);
let r0 = tree.focus; let r0 = tree.focus;
tree.focus = l0; tree.focus = l0;
let view = View::new(DocumentId::default(), GutterConfig::default()); let view = View::new(
DocumentId::default(),
GutterConfig::default(),
InlineDiagnosticsConfig::default(),
);
tree.split(view, Layout::Horizontal); tree.split(view, Layout::Horizontal);
let l1 = tree.focus; let l1 = tree.focus;
tree.focus = l0; tree.focus = l0;
let view = View::new(DocumentId::default(), GutterConfig::default()); let view = View::new(
DocumentId::default(),
GutterConfig::default(),
InlineDiagnosticsConfig::default(),
);
tree.split(view, Layout::Vertical); tree.split(view, Layout::Vertical);
// Tree in test // Tree in test
@ -792,28 +809,44 @@ mod test {
}); });
let doc_l0 = DocumentId::default(); let doc_l0 = DocumentId::default();
let mut view = View::new(doc_l0, GutterConfig::default()); let mut view = View::new(
doc_l0,
GutterConfig::default(),
InlineDiagnosticsConfig::default(),
);
view.area = Rect::new(0, 0, 180, 80); view.area = Rect::new(0, 0, 180, 80);
tree.insert(view); tree.insert(view);
let l0 = tree.focus; let l0 = tree.focus;
let doc_r0 = DocumentId::default(); let doc_r0 = DocumentId::default();
let view = View::new(doc_r0, GutterConfig::default()); let view = View::new(
doc_r0,
GutterConfig::default(),
InlineDiagnosticsConfig::default(),
);
tree.split(view, Layout::Vertical); tree.split(view, Layout::Vertical);
let r0 = tree.focus; let r0 = tree.focus;
tree.focus = l0; tree.focus = l0;
let doc_l1 = DocumentId::default(); let doc_l1 = DocumentId::default();
let view = View::new(doc_l1, GutterConfig::default()); let view = View::new(
doc_l1,
GutterConfig::default(),
InlineDiagnosticsConfig::default(),
);
tree.split(view, Layout::Horizontal); tree.split(view, Layout::Horizontal);
let l1 = tree.focus; let l1 = tree.focus;
tree.focus = l0; tree.focus = l0;
let doc_l2 = DocumentId::default(); let doc_l2 = DocumentId::default();
let view = View::new(doc_l2, GutterConfig::default()); let view = View::new(
doc_l2,
GutterConfig::default(),
InlineDiagnosticsConfig::default(),
);
tree.split(view, Layout::Vertical); tree.split(view, Layout::Vertical);
let l2 = tree.focus; let l2 = tree.focus;
@ -908,19 +941,35 @@ mod test {
width: tree_area_width, width: tree_area_width,
height: 80, height: 80,
}); });
let mut view = View::new(DocumentId::default(), GutterConfig::default()); let mut view = View::new(
DocumentId::default(),
GutterConfig::default(),
InlineDiagnosticsConfig::default(),
);
view.area = Rect::new(0, 0, 180, 80); view.area = Rect::new(0, 0, 180, 80);
tree.insert(view); tree.insert(view);
let view = View::new(DocumentId::default(), GutterConfig::default()); let view = View::new(
DocumentId::default(),
GutterConfig::default(),
InlineDiagnosticsConfig::default(),
);
tree.split(view, Layout::Vertical); tree.split(view, Layout::Vertical);
let view = View::new(DocumentId::default(), GutterConfig::default()); let view = View::new(
DocumentId::default(),
GutterConfig::default(),
InlineDiagnosticsConfig::default(),
);
tree.split(view, Layout::Horizontal); tree.split(view, Layout::Horizontal);
tree.remove(tree.focus); tree.remove(tree.focus);
let view = View::new(DocumentId::default(), GutterConfig::default()); let view = View::new(
DocumentId::default(),
GutterConfig::default(),
InlineDiagnosticsConfig::default(),
);
tree.split(view, Layout::Vertical); tree.split(view, Layout::Vertical);
// Make sure that we only have one level in the tree. // Make sure that we only have one level in the tree.
@ -946,12 +995,20 @@ mod test {
width: tree_area_width, width: tree_area_width,
height: tree_area_height, height: tree_area_height,
}); });
let mut view = View::new(DocumentId::default(), GutterConfig::default()); let mut view = View::new(
DocumentId::default(),
GutterConfig::default(),
InlineDiagnosticsConfig::default(),
);
view.area = Rect::new(0, 0, tree_area_width, tree_area_height); view.area = Rect::new(0, 0, tree_area_width, tree_area_height);
tree.insert(view); tree.insert(view);
for _ in 0..9 { for _ in 0..9 {
let view = View::new(DocumentId::default(), GutterConfig::default()); let view = View::new(
DocumentId::default(),
GutterConfig::default(),
InlineDiagnosticsConfig::default(),
);
tree.split(view, Layout::Vertical); tree.split(view, Layout::Vertical);
} }

View File

@ -1,6 +1,6 @@
use crate::{ use crate::{
align_view, align_view,
annotations::diagnostics::InlineDiagnostics, annotations::diagnostics::{InlineDiagnostics, InlineDiagnosticsConfig},
document::{DocumentColorSwatches, DocumentInlayHints}, document::{DocumentColorSwatches, DocumentInlayHints},
editor::{GutterConfig, GutterType}, editor::{GutterConfig, GutterType},
graphics::Rect, graphics::Rect,
@ -167,7 +167,11 @@ impl fmt::Debug for View {
} }
impl View { impl View {
pub fn new(doc: DocumentId, gutters: GutterConfig) -> Self { pub fn new(
doc: DocumentId,
gutters: GutterConfig,
inline_diagnostics_config: InlineDiagnosticsConfig,
) -> Self {
Self { Self {
id: ViewId::default(), id: ViewId::default(),
doc, doc,
@ -178,7 +182,7 @@ impl View {
object_selections: Vec::new(), object_selections: Vec::new(),
gutters, gutters,
doc_revisions: HashMap::new(), doc_revisions: HashMap::new(),
diagnostics_handler: DiagnosticsHandler::new(), diagnostics_handler: DiagnosticsHandler::new(inline_diagnostics_config),
} }
} }
@ -704,7 +708,11 @@ mod tests {
#[test] #[test]
fn test_text_pos_at_screen_coords() { fn test_text_pos_at_screen_coords() {
let mut view = View::new(DocumentId::default(), GutterConfig::default()); let mut view = View::new(
DocumentId::default(),
GutterConfig::default(),
InlineDiagnosticsConfig::default(),
);
view.area = Rect::new(40, 40, 40, 40); view.area = Rect::new(40, 40, 40, 40);
let rope = Rope::from_str("abc\n\tdef"); let rope = Rope::from_str("abc\n\tdef");
let mut doc = Document::from( let mut doc = Document::from(
@ -880,6 +888,7 @@ mod tests {
layout: vec![GutterType::Diagnostics], layout: vec![GutterType::Diagnostics],
line_numbers: GutterLineNumbersConfig::default(), line_numbers: GutterLineNumbersConfig::default(),
}, },
InlineDiagnosticsConfig::default(),
); );
view.area = Rect::new(40, 40, 40, 40); view.area = Rect::new(40, 40, 40, 40);
let rope = Rope::from_str("abc\n\tdef"); let rope = Rope::from_str("abc\n\tdef");
@ -911,6 +920,7 @@ mod tests {
layout: vec![], layout: vec![],
line_numbers: GutterLineNumbersConfig::default(), line_numbers: GutterLineNumbersConfig::default(),
}, },
InlineDiagnosticsConfig::default(),
); );
view.area = Rect::new(40, 40, 40, 40); view.area = Rect::new(40, 40, 40, 40);
let rope = Rope::from_str("abc\n\tdef"); let rope = Rope::from_str("abc\n\tdef");
@ -936,7 +946,11 @@ mod tests {
#[test] #[test]
fn test_text_pos_at_screen_coords_cjk() { fn test_text_pos_at_screen_coords_cjk() {
let mut view = View::new(DocumentId::default(), GutterConfig::default()); let mut view = View::new(
DocumentId::default(),
GutterConfig::default(),
InlineDiagnosticsConfig::default(),
);
view.area = Rect::new(40, 40, 40, 40); view.area = Rect::new(40, 40, 40, 40);
let rope = Rope::from_str("Hi! こんにちは皆さん"); let rope = Rope::from_str("Hi! こんにちは皆さん");
let mut doc = Document::from( let mut doc = Document::from(
@ -1021,7 +1035,11 @@ mod tests {
#[test] #[test]
fn test_text_pos_at_screen_coords_graphemes() { fn test_text_pos_at_screen_coords_graphemes() {
let mut view = View::new(DocumentId::default(), GutterConfig::default()); let mut view = View::new(
DocumentId::default(),
GutterConfig::default(),
InlineDiagnosticsConfig::default(),
);
view.area = Rect::new(40, 40, 40, 40); view.area = Rect::new(40, 40, 40, 40);
let rope = Rope::from_str("Hèl̀l̀ò world!"); let rope = Rope::from_str("Hèl̀l̀ò world!");
let mut doc = Document::from( let mut doc = Document::from(