diff --git a/book/src/editor.md b/book/src/editor.md index 7f1de68bb..d8b4d4587 100644 --- a/book/src/editor.md +++ b/book/src/editor.md @@ -456,7 +456,7 @@ fn main() { | `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` | - +| `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 (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): diff --git a/helix-view/src/annotations/diagnostics.rs b/helix-view/src/annotations/diagnostics.rs index 7802ca637..eeb5c3ea8 100644 --- a/helix-view/src/annotations/diagnostics.rs +++ b/helix-view/src/annotations/diagnostics.rs @@ -1,3 +1,5 @@ +use std::time::Duration; + use helix_core::diagnostic::Severity; use helix_core::doc_formatter::{FormattedGrapheme, TextFormat}; 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 +where + D: serde::Deserializer<'de>, +{ + let millis = u64::deserialize(deserializer)?; + Ok(Duration::from_millis(millis)) +} + +fn serialize_duration_millis(duration: &Duration, serializer: S) -> Result +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 { fn serialize(&self, serializer: S) -> Result where @@ -57,6 +79,11 @@ pub struct InlineDiagnosticsConfig { pub prefix_len: u16, pub max_wrap: u16, pub max_diagnostics: usize, + #[serde( + serialize_with = "serialize_duration_millis", + deserialize_with = "deserialize_duration_millis" + )] + pub timeout: Duration, } impl InlineDiagnosticsConfig { @@ -115,6 +142,7 @@ impl Default for InlineDiagnosticsConfig { prefix_len: 1, max_wrap: 20, max_diagnostics: 10, + timeout: Duration::from_millis(350), } } } diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 57e130881..f8e18ad04 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -1727,7 +1727,13 @@ impl Editor { .try_get(self.tree.focus) .filter(|v| id == v.doc) // Different Document .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( view, match action { @@ -1923,7 +1929,11 @@ impl Editor { 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 doc = doc_mut!(self, &doc_id); doc.ensure_view_init(view_id); diff --git a/helix-view/src/gutter.rs b/helix-view/src/gutter.rs index c2cbc0da5..de015cd22 100644 --- a/helix-view/src/gutter.rs +++ b/helix-view/src/gutter.rs @@ -329,6 +329,7 @@ mod tests { use std::sync::Arc; use super::*; + use crate::annotations::diagnostics::InlineDiagnosticsConfig; use crate::document::Document; use crate::editor::{Config, GutterConfig, GutterLineNumbersConfig}; use crate::graphics::Rect; @@ -338,7 +339,11 @@ mod tests { #[test] 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); let rope = Rope::from_str("abc\n\tdef"); @@ -364,7 +369,11 @@ mod tests { ..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); let rope = Rope::from_str("abc\n\tdef"); @@ -383,7 +392,11 @@ mod tests { 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); let rope = Rope::from_str("abc\n\tdef"); @@ -406,7 +419,11 @@ mod tests { 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); let rope = Rope::from_str("a\nb"); diff --git a/helix-view/src/handlers/diagnostics.rs b/helix-view/src/handlers/diagnostics.rs index 2b8ff6325..eafaae5c8 100644 --- a/helix-view/src/handlers/diagnostics.rs +++ b/helix-view/src/handlers/diagnostics.rs @@ -8,6 +8,7 @@ use helix_event::{request_redraw, send_blocking, AsyncHook}; use tokio::sync::mpsc::Sender; use tokio::time::Instant; +use crate::annotations::diagnostics::InlineDiagnosticsConfig; use crate::{Document, DocumentId, ViewId}; #[derive(Debug)] @@ -19,10 +20,9 @@ pub enum DiagnosticEvent { struct DiagnosticTimeout { active_generation: Arc, generation: usize, + timeout: Duration, } -const TIMEOUT: Duration = Duration::from_millis(350); - impl AsyncHook for DiagnosticTimeout { type Event = DiagnosticEvent; @@ -35,12 +35,12 @@ impl AsyncHook for DiagnosticTimeout { DiagnosticEvent::CursorLineChanged { generation } => { if generation > self.generation { self.generation = generation; - Some(Instant::now() + TIMEOUT) + Some(Instant::now() + self.timeout) } else { 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, } } @@ -61,6 +61,7 @@ pub struct DiagnosticsHandler { last_cursor_line: Cell, pub active: bool, pub events: Sender, + config: InlineDiagnosticsConfig, } // 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 impl Clone for DiagnosticsHandler { fn clone(&self) -> Self { - Self::new() + Self::new(self.config.clone()) } } impl DiagnosticsHandler { #[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 events = DiagnosticTimeout { active_generation: active_generation.clone(), generation: 0, + timeout: inline_diagnostics_config.timeout, } .spawn(); Self { @@ -89,6 +91,7 @@ impl DiagnosticsHandler { last_doc: Cell::new(DocumentId(NonZeroUsize::new(usize::MAX).unwrap())), last_cursor_line: Cell::new(usize::MAX), active: true, + config: inline_diagnostics_config, } } } diff --git a/helix-view/src/tree.rs b/helix-view/src/tree.rs index aba947a21..9dec43026 100644 --- a/helix-view/src/tree.rs +++ b/helix-view/src/tree.rs @@ -725,6 +725,7 @@ impl DoubleEndedIterator for Traverse<'_> { #[cfg(test)] mod test { use super::*; + use crate::annotations::diagnostics::InlineDiagnosticsConfig; use crate::editor::GutterConfig; use crate::DocumentId; @@ -736,22 +737,38 @@ mod test { width: 180, 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); tree.insert(view); 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); let r0 = tree.focus; 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); let l1 = tree.focus; 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 in test @@ -792,28 +809,44 @@ mod test { }); 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); tree.insert(view); let l0 = tree.focus; 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); let r0 = tree.focus; tree.focus = l0; 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); let l1 = tree.focus; tree.focus = l0; 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); let l2 = tree.focus; @@ -908,19 +941,35 @@ mod test { width: tree_area_width, 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); 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); - let view = View::new(DocumentId::default(), GutterConfig::default()); + let view = View::new( + DocumentId::default(), + GutterConfig::default(), + InlineDiagnosticsConfig::default(), + ); tree.split(view, Layout::Horizontal); 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); // Make sure that we only have one level in the tree. @@ -946,12 +995,20 @@ mod test { width: tree_area_width, 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); tree.insert(view); 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); } diff --git a/helix-view/src/view.rs b/helix-view/src/view.rs index aecf09a61..5fa408c63 100644 --- a/helix-view/src/view.rs +++ b/helix-view/src/view.rs @@ -1,6 +1,6 @@ use crate::{ align_view, - annotations::diagnostics::InlineDiagnostics, + annotations::diagnostics::{InlineDiagnostics, InlineDiagnosticsConfig}, document::{DocumentColorSwatches, DocumentInlayHints}, editor::{GutterConfig, GutterType}, graphics::Rect, @@ -167,7 +167,11 @@ impl fmt::Debug for View { } impl View { - pub fn new(doc: DocumentId, gutters: GutterConfig) -> Self { + pub fn new( + doc: DocumentId, + gutters: GutterConfig, + inline_diagnostics_config: InlineDiagnosticsConfig, + ) -> Self { Self { id: ViewId::default(), doc, @@ -178,7 +182,7 @@ impl View { object_selections: Vec::new(), gutters, doc_revisions: HashMap::new(), - diagnostics_handler: DiagnosticsHandler::new(), + diagnostics_handler: DiagnosticsHandler::new(inline_diagnostics_config), } } @@ -704,7 +708,11 @@ mod tests { #[test] 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); let rope = Rope::from_str("abc\n\tdef"); let mut doc = Document::from( @@ -880,6 +888,7 @@ mod tests { layout: vec![GutterType::Diagnostics], line_numbers: GutterLineNumbersConfig::default(), }, + InlineDiagnosticsConfig::default(), ); view.area = Rect::new(40, 40, 40, 40); let rope = Rope::from_str("abc\n\tdef"); @@ -911,6 +920,7 @@ mod tests { layout: vec![], line_numbers: GutterLineNumbersConfig::default(), }, + InlineDiagnosticsConfig::default(), ); view.area = Rect::new(40, 40, 40, 40); let rope = Rope::from_str("abc\n\tdef"); @@ -936,7 +946,11 @@ mod tests { #[test] 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); let rope = Rope::from_str("Hi! こんにちは皆さん"); let mut doc = Document::from( @@ -1021,7 +1035,11 @@ mod tests { #[test] 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); let rope = Rope::from_str("Hèl̀l̀ò world!"); let mut doc = Document::from(