From b2ac43ee721b73e054bbf2818153697e25f9b305 Mon Sep 17 00:00:00 2001 From: "Alexis (Poliorcetics) Bourget" Date: Mon, 20 Feb 2023 21:43:44 +0100 Subject: [PATCH 01/16] book: introduce `display-inline-diagnostics` --- book/src/configuration.md | 1 + 1 file changed, 1 insertion(+) diff --git a/book/src/configuration.md b/book/src/configuration.md index ec692cab1225..65b6a2ff8355 100644 --- a/book/src/configuration.md +++ b/book/src/configuration.md @@ -122,6 +122,7 @@ The following statusline elements can be configured: | `auto-signature-help` | Enable automatic popup of signature help (parameter hints) | `true` | | `display-inlay-hints` | Display inlay hints[^2] | `false` | | `display-signature-help-docs` | Display docs under signature help popup | `true` | +| `display-inline-diagnostics` | Display diagnostics under their starting line | `true` | [^1]: By default, a progress spinner is shown in the statusline beside the file path. [^2]: You may also have to activate them in the LSP config for them to appear, not just in Helix. From e177f0f87255b32346fe9ad90529e6820f54c77d Mon Sep 17 00:00:00 2001 From: "Alexis (Poliorcetics) Bourget" Date: Mon, 20 Feb 2023 21:50:13 +0100 Subject: [PATCH 02/16] refacto: use `Rc` for messages in diagnostics --- helix-core/src/diagnostic.rs | 5 ++++- helix-lsp/src/lib.rs | 2 +- helix-term/src/application.rs | 3 ++- helix-term/src/ui/editor.rs | 2 +- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/helix-core/src/diagnostic.rs b/helix-core/src/diagnostic.rs index 58ddb0383a0a..07b5890947a7 100644 --- a/helix-core/src/diagnostic.rs +++ b/helix-core/src/diagnostic.rs @@ -1,4 +1,6 @@ //! LSP diagnostic utility types. +use std::rc::Rc; + use serde::{Deserialize, Serialize}; /// Describes the severity level of a [`Diagnostic`]. @@ -40,7 +42,8 @@ pub enum DiagnosticTag { pub struct Diagnostic { pub range: Range, pub line: usize, - pub message: String, + // Messages will also be copied in the inline diagnostics, let's avoid allocating twice + pub message: Rc, pub severity: Option, pub code: Option, pub tags: Vec, diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index e31df59f4e2d..7d1f6af04c87 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -110,7 +110,7 @@ pub mod util { severity, code, source: diag.source.clone(), - message: diag.message.to_owned(), + message: diag.message.as_str().into(), related_information: None, tags, data: diag.data.to_owned(), diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index c7e939959ca4..fa530b5cd11e 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -32,6 +32,7 @@ use log::{debug, error, warn}; use std::{ io::{stdin, stdout}, path::Path, + rc::Rc, sync::Arc, time::{Duration, Instant}, }; @@ -779,7 +780,7 @@ impl Application { Some(Diagnostic { range: Range { start, end }, line: diagnostic.range.start.line as usize, - message: diagnostic.message.clone(), + message: Rc::new(diagnostic.message.clone()), severity, code, tags, diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 7c22df747642..a5cdf60d688a 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -682,7 +682,7 @@ impl EditorView { Some(Severity::Info) => info, Some(Severity::Hint) => hint, }); - let text = Text::styled(&diagnostic.message, style); + let text = Text::styled(&*diagnostic.message, style); lines.extend(text.lines); } From 8a51918138126834ad3efffdd19f70709542139c Mon Sep 17 00:00:00 2001 From: "Alexis (Poliorcetics) Bourget" Date: Mon, 20 Feb 2023 21:52:36 +0100 Subject: [PATCH 03/16] refacto: move some imports to create less churn in next commits --- helix-term/src/ui/editor.rs | 4 +--- helix-view/src/document.rs | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index a5cdf60d688a..a3b1765ccb23 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -11,6 +11,7 @@ use crate::{ }; use helix_core::{ + diagnostic::Severity, graphemes::{ ensure_grapheme_boundary_next_byte, next_grapheme_boundary, prev_grapheme_boundary, }, @@ -346,7 +347,6 @@ impl EditorView { doc: &Document, theme: &Theme, ) -> [Vec<(usize, std::ops::Range)>; 5] { - use helix_core::diagnostic::Severity; let get_scope_of = |scope| { theme .find_scope_index_exact(scope) @@ -650,7 +650,6 @@ impl EditorView { surface: &mut Surface, theme: &Theme, ) { - use helix_core::diagnostic::Severity; use tui::{ layout::Alignment, text::Text, @@ -1392,7 +1391,6 @@ impl Component for EditorView { // render status msg if let Some((status_msg, severity)) = &cx.editor.status_msg { status_msg_width = status_msg.width(); - use helix_view::editor::Severity; let style = if *severity == Severity::Error { cx.editor.theme.get("error") } else { diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 19220f286a22..65ed6eabd074 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -7,7 +7,7 @@ use helix_core::auto_pairs::AutoPairs; use helix_core::doc_formatter::TextFormat; use helix_core::syntax::Highlight; use helix_core::text_annotations::{InlineAnnotation, TextAnnotations}; -use helix_core::Range; +use helix_core::{Assoc, Range}; use helix_vcs::{DiffHandle, DiffProviderRegistry}; use ::parking_lot::Mutex; From 8394f343ae1c1b3800cc9f84837c6af709c0435e Mon Sep 17 00:00:00 2001 From: "Alexis (Poliorcetics) Bourget" Date: Mon, 20 Feb 2023 21:54:59 +0100 Subject: [PATCH 04/16] refacto: call `transaction.changes()` only once --- helix-view/src/document.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 65ed6eabd074..95372be895c0 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -920,14 +920,16 @@ impl Document { let old_doc = self.text().clone(); - let success = transaction.changes().apply(&mut self.text); + let changes = transaction.changes(); + + let success = changes.apply(&mut self.text); if success { for selection in self.selections.values_mut() { *selection = selection .clone() // Map through changes - .map(transaction.changes()) + .map(changes) // Ensure all selections across all views still adhere to invariants. .ensure_invariants(self.text.slice(..)); } @@ -943,7 +945,7 @@ impl Document { self.modified_since_accessed = true; } - if !transaction.changes().is_empty() { + if !changes.is_empty() { self.version += 1; // start computing the diff in parallel if let Some(diff_handle) = &self.diff_handle { @@ -968,9 +970,7 @@ impl Document { // update tree-sitter syntax tree if let Some(syntax) = &mut self.syntax { // TODO: no unwrap - syntax - .update(&old_doc, &self.text, transaction.changes()) - .unwrap(); + syntax.update(&old_doc, &self.text, changes).unwrap(); } let changes = transaction.changes(); From 3cae4a04bf5313a650e674a7b53735cb9e862bdf Mon Sep 17 00:00:00 2001 From: "Alexis (Poliorcetics) Bourget" Date: Mon, 20 Feb 2023 21:57:22 +0100 Subject: [PATCH 05/16] feat: Introduce `lsp.display_inline_diagnostics` configuration in code --- helix-view/src/editor.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index bbed58d6e7e6..a656f1eafc8d 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -347,6 +347,9 @@ pub struct LspConfig { pub display_signature_help_docs: bool, /// Display inlay hints pub display_inlay_hints: bool, + /// Display diagnostic on the same line they occur automatically. + /// Also called "error lens"-style diagnostics, in reference to the popular VSCode extension. + pub display_inline_diagnostics: bool, } impl Default for LspConfig { @@ -357,6 +360,7 @@ impl Default for LspConfig { auto_signature_help: true, display_signature_help_docs: true, display_inlay_hints: false, + display_inline_diagnostics: true, } } } From d239d3582d4e17999e80f7ce1924f31875a5013b Mon Sep 17 00:00:00 2001 From: "Alexis (Poliorcetics) Bourget" Date: Mon, 20 Feb 2023 22:28:13 +0100 Subject: [PATCH 06/16] feat: Introduce the `annotations` module --- helix-view/src/document.rs | 2 + helix-view/src/document/annotations.rs | 98 ++++++++++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 helix-view/src/document/annotations.rs diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 95372be895c0..3c7bfb4d962d 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -37,6 +37,8 @@ use helix_core::{ use crate::editor::{Config, RedrawHandle}; use crate::{DocumentId, Editor, Theme, View, ViewId}; +pub mod annotations; + /// 8kB of buffer space for encoding and decoding `Rope`s. const BUF_SIZE: usize = 8192; diff --git a/helix-view/src/document/annotations.rs b/helix-view/src/document/annotations.rs new file mode 100644 index 000000000000..1757141b17c6 --- /dev/null +++ b/helix-view/src/document/annotations.rs @@ -0,0 +1,98 @@ +//! This module contains the various annotations that can be added to a [`super::Document`] when +//! displaying it. +//! +//! Examples: inline diagnostics, inlay hints, git blames. + +use std::rc::Rc; + +use helix_core::diagnostic::Severity; +use helix_core::text_annotations::LineAnnotation; + +/// Diagnostics annotations are [`LineAnnotation`]s embed below the first line of the diagnostic +/// they're about. +/// +/// Below is an example in plain text of the expect result: +/// +/// ```text +/// use std::alloc::{alloc, Layout}; +/// │ └─── unused import: `Layout` +/// │ `#[warn(unused_imports)]` on by default +/// └─── remove the unused import +/// +/// fn main() { +/// match std::cmp::Ordering::Less { +/// └─── any code following this `match` expression is unreachable, as all arms diverge +/// std::cmp::Ordering::Less => todo!(), +/// std::cmp::Ordering:Equal => todo!(), +/// │ └─── Syntax Error: expected `,` +/// ├─── maybe write a path separator here: `::` +/// ├─── expected one of `!`, `(`, `...`, `..=`, `..`, `::`, `{`, or `|`, found `:` +/// │ expected one of 8 possible tokens +/// ├─── Syntax Error: expected expression +/// └─── Syntax Error: expected FAT_ARROW +/// std::cmp::Ordering::Greater => todo!(), +/// } +/// +/// let layout: Layout = Layou::new::(); +/// │ ├─── a struct with a similar name exists: `Layout` +/// │ └─── failed to resolve: use of undeclared type `Layou` +/// │ use of undeclared type `Layou` +/// └─── unreachable statement +/// `#[warn(unreachable_code)]` on by default +/// } +/// ``` +pub struct DiagnosticAnnotations { + /// The `LineAnnotation` don't contain any text, they're simply used to reserve the space for display. + pub annotations: Rc<[LineAnnotation]>, + + /// The messages are the text linked to the `annotations`. + /// + /// To make the work of the renderer less costly, this must maintain a sort order following + /// [`DiagnosticAnnotationMessage.anchor_char_idx`]. + /// + /// The function [`diagnostic_inline_messages_from_diagnostics()`] can be used to do this. + pub messages: Rc<[DiagnosticAnnotationMessage]>, +} + +/// A `DiagnosticAnnotationMessage` is a single diagnostic to be displayed inline. +#[derive(Debug)] +pub struct DiagnosticAnnotationMessage { + /// `line` is used to quickly gather all the diagnostics for a line. + pub line: usize, + /// The anchor is where the diagnostic is positioned in the document. This is used to compute + /// the exact column for rendering after taking virtual text into account. + pub anchor_char_idx: usize, + /// The message to display. It can contain line breaks so be careful when displaying them. + pub message: Rc, + /// The diagnostic's severity, to get the relevant style at rendering time. + pub severity: Option, +} + +impl Default for DiagnosticAnnotations { + fn default() -> Self { + Self { + annotations: Vec::new().into(), + messages: Vec::new().into(), + } + } +} + +/// Compute the list of `DiagnosticAnnotationMessage`s from the diagnostics. +pub fn diagnostic_inline_messages_from_diagnostics( + diagnostics: &[helix_core::Diagnostic], +) -> Rc<[DiagnosticAnnotationMessage]> { + let mut res = Vec::with_capacity(diagnostics.len()); + + for diag in diagnostics { + res.push(DiagnosticAnnotationMessage { + line: diag.line, + anchor_char_idx: diag.range.start, + message: Rc::clone(&diag.message), + severity: diag.severity, + }); + } + + res.sort_unstable_by_key(|a| a.anchor_char_idx); + + res.into() +} From f45a45672e3d2d965d514c837aa24de6e5f892cf Mon Sep 17 00:00:00 2001 From: "Alexis (Poliorcetics) Bourget" Date: Mon, 20 Feb 2023 23:55:59 +0100 Subject: [PATCH 07/16] feat: compute inline diagnostic annotations when receiving diagnostics from the LSP server --- helix-term/src/application.rs | 38 +++++++++++++++++++++++++++++++++-- helix-view/src/document.rs | 31 +++++++++++++++++++++++++++- helix-view/src/editor.rs | 6 ++++++ 3 files changed, 72 insertions(+), 3 deletions(-) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index fa530b5cd11e..fc27728f08c2 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -3,11 +3,14 @@ use futures_util::Stream; use helix_core::{ diagnostic::{DiagnosticTag, NumberOrString}, path::get_relative_path, - pos_at_coords, syntax, Selection, + pos_at_coords, syntax, + text_annotations::LineAnnotation, + Selection, }; use helix_lsp::{lsp, util::lsp_pos_to_pos, LspProgressMap}; use helix_view::{ align_view, + document::annotations::{diagnostic_inline_messages_from_diagnostics, DiagnosticAnnotations}, document::DocumentSavedEventResult, editor::{ConfigEvent, EditorEvent}, graphics::Rect, @@ -30,6 +33,7 @@ use crate::{ use log::{debug, error, warn}; use std::{ + collections::BTreeMap, io::{stdin, stdout}, path::Path, rc::Rc, @@ -688,11 +692,17 @@ impl Application { return; } }; + + let enabled_inline_diagnostics = + self.editor.config().lsp.display_inline_diagnostics; let doc = self.editor.document_by_path_mut(&path); if let Some(doc) = doc { let lang_conf = doc.language_config(); let text = doc.text(); + let text_slice = text.slice(..); + + let mut diagnostic_annotations = BTreeMap::new(); let diagnostics = params .diagnostics @@ -777,6 +787,12 @@ impl Application { Vec::new() }; + if enabled_inline_diagnostics { + let char_idx = text_slice.line_to_char(diagnostic.range.start.line as usize); + + *diagnostic_annotations.entry(char_idx).or_default() += diagnostic.message.trim().lines().count(); + } + Some(Diagnostic { range: Range { start, end }, line: diagnostic.range.start.line as usize, @@ -788,7 +804,24 @@ impl Application { data: diagnostic.data.clone(), }) }) - .collect(); + .collect::>(); + + if enabled_inline_diagnostics { + let diagnostic_annotations = diagnostic_annotations + .into_iter() + .map(|(anchor_char_idx, height)| LineAnnotation { + anchor_char_idx, + height, + }) + .collect::>(); + + doc.set_diagnostics_annotations(DiagnosticAnnotations { + annotations: diagnostic_annotations.into(), + messages: diagnostic_inline_messages_from_diagnostics( + &diagnostics, + ), + }) + } doc.set_diagnostics(diagnostics); } @@ -910,6 +943,7 @@ impl Application { == Some(server_id) { doc.set_diagnostics(Vec::new()); + doc.set_diagnostics_annotations(Default::default()); doc.url() } else { None diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 3c7bfb4d962d..0bd05c321553 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -167,6 +167,7 @@ pub struct Document { pub(crate) modified_since_accessed: bool, diagnostics: Vec, + diagnostic_annotations: annotations::DiagnosticAnnotations, language_server: Option>, diff_handle: Option, @@ -262,6 +263,7 @@ impl fmt::Debug for Document { .field("version", &self.version) .field("modified_since_accessed", &self.modified_since_accessed) .field("diagnostics", &self.diagnostics) + // .field("diagnostics_annotations", &self.diagnostics_annotations) // .field("language_server", &self.language_server) .finish() } @@ -488,6 +490,7 @@ impl Document { changes, old_state, diagnostics: Vec::new(), + diagnostic_annotations: Default::default(), version: 0, history: Cell::new(History::default()), savepoints: Vec::new(), @@ -1393,6 +1396,21 @@ impl Document { .sort_unstable_by_key(|diagnostic| diagnostic.range); } + #[inline] + pub fn diagnostic_annotations_messages( + &self, + ) -> Rc<[annotations::DiagnosticAnnotationMessage]> { + Rc::clone(&self.diagnostic_annotations.messages) + } + + #[inline] + pub fn set_diagnostics_annotations( + &mut self, + diagnostic_annotations: annotations::DiagnosticAnnotations, + ) { + self.diagnostic_annotations = diagnostic_annotations; + } + /// Get the document's auto pairs. If the document has a recognized /// language config with auto pairs configured, returns that; /// otherwise, falls back to the global auto pairs config. If the global @@ -1480,7 +1498,18 @@ impl Document { /// Get the text annotations that apply to the whole document, those that do not apply to any /// specific view. pub fn text_annotations(&self, _theme: Option<&Theme>) -> TextAnnotations { - TextAnnotations::default() + let mut text_annotations = TextAnnotations::default(); + + if !self.diagnostic_annotations.annotations.is_empty() { + text_annotations + .add_line_annotation(Rc::clone(&self.diagnostic_annotations.annotations)); + } + + text_annotations + } + + pub fn reset_diagnostics_annotations(&mut self) { + self.diagnostic_annotations = Default::default(); } /// Set the inlay hints for this document and `view_id`. diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index a656f1eafc8d..c735bce86f0f 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -1153,6 +1153,12 @@ impl Editor { } } + if !config.lsp.display_inline_diagnostics { + for doc in self.documents_mut() { + doc.reset_diagnostics_annotations(); + } + } + for (view, _) in self.tree.views_mut() { let doc = doc_mut!(self, &view.doc); view.sync_changes(doc); From 99ac56d18dc8061051507cb8b1ce3205055244e5 Mon Sep 17 00:00:00 2001 From: "Alexis (Poliorcetics) Bourget" Date: Mon, 20 Feb 2023 23:56:32 +0100 Subject: [PATCH 08/16] feat: update inline diagnostics annotations when the document is updated --- helix-view/src/document.rs | 4 +- helix-view/src/document/annotations.rs | 97 ++++++++++++++++++++++++++ 2 files changed, 99 insertions(+), 2 deletions(-) diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 0bd05c321553..8fb915946286 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -978,8 +978,6 @@ impl Document { syntax.update(&old_doc, &self.text, changes).unwrap(); } - let changes = transaction.changes(); - // map state.diagnostics over changes::map_pos too for diagnostic in &mut self.diagnostics { diagnostic.range.start = changes.map_pos(diagnostic.range.start, Assoc::After); @@ -1016,6 +1014,8 @@ impl Document { apply_inlay_hint_changes(padding_after_inlay_hints); } + annotations::apply_changes_to_diagnostic_annotations(self, changes); + // emit lsp notification if let Some(language_server) = self.language_server() { let notify = language_server.text_document_did_change( diff --git a/helix-view/src/document/annotations.rs b/helix-view/src/document/annotations.rs index 1757141b17c6..71ece9bcf59d 100644 --- a/helix-view/src/document/annotations.rs +++ b/helix-view/src/document/annotations.rs @@ -7,6 +7,8 @@ use std::rc::Rc; use helix_core::diagnostic::Severity; use helix_core::text_annotations::LineAnnotation; +use helix_core::Assoc; +use helix_core::ChangeSet; /// Diagnostics annotations are [`LineAnnotation`]s embed below the first line of the diagnostic /// they're about. @@ -96,3 +98,98 @@ pub fn diagnostic_inline_messages_from_diagnostics( res.into() } + +/// Used in [`super::Document::apply_impl()`] to recompute the inline diagnostics after changes have +/// been made to the document. +/// +/// **Must be called with sorted diagnostics.** +pub(super) fn apply_changes_to_diagnostic_annotations( + doc: &mut super::Document, + changes: &ChangeSet, +) { + // Only recompute if they're not empty since being empty probably means the annotation are + // disabled, no need to build them in this case (building them would not display them since the + // line annotations list is empty too in this case). + + match Rc::get_mut(&mut doc.diagnostic_annotations.messages) { + // If for some reason we can't update the annotations, just delete them: the document is being saved and they + // will be updated soon anyway. + None | Some([]) => { + doc.diagnostic_annotations = Default::default(); + return; + } + Some(messages) => { + // The diagnostics have been sorted after being updated in `Document::apply_impl()` but nothing got deleted + // so simply use the same order for the annotation messages. + for (diag, message) in doc.diagnostics.iter().zip(messages.iter_mut()) { + let DiagnosticAnnotationMessage { + line, + anchor_char_idx, + message, + severity, + } = message; + + *line = diag.line; + *anchor_char_idx = diag.range.start; + *message = Rc::clone(&diag.message); + *severity = diag.severity; + } + } + } + + match Rc::get_mut(&mut doc.diagnostic_annotations.annotations) { + // See `None` case above + None | Some([]) => doc.diagnostic_annotations = Default::default(), + Some(line_annotations) => { + let doc_text = doc.text.slice(..); + + let get_new_anchor_char_idx = |annot: &LineAnnotation| { + let new_char_idx = changes.map_pos(annot.anchor_char_idx, Assoc::After); + let line = doc_text.char_to_line(new_char_idx); + doc_text.line_to_char(line) + }; + + // The algorithm here does its best to modify in place to avoid reallocations as much as possible + // + // 1) We know the line annotations are non-empty because we checked for it in the match above. + // 2) We update the first line annotation. + // 3) For each subsequent annotation + // 1) We compute its new anchor + // 2) Logically, it cannot move further back than the previous one else the previous one would + // also have moved back more + // 3) IF the new anchor is equal to the new anchor of the previous annotation, add the current one's + // height to the previous + // 4) ELSE update the write position and write the current annotation (with updated anchor) there + // 4) If the last write position was not the last member of the current lines annotations, it means we + // merged some of them together so we update the saved line annotations. + + let new_anchor_char_idx = get_new_anchor_char_idx(&line_annotations[0]); + line_annotations[0].anchor_char_idx = new_anchor_char_idx; + + let mut previous_anchor_char_idx = new_anchor_char_idx; + + let mut writing_index = 0; + + for reading_index in 1..line_annotations.len() { + let annot = &mut line_annotations[reading_index]; + let new_anchor_char_idx = get_new_anchor_char_idx(annot); + + if new_anchor_char_idx == previous_anchor_char_idx { + line_annotations[writing_index].height += annot.height; + } else { + previous_anchor_char_idx = new_anchor_char_idx; + + writing_index += 1; + line_annotations[writing_index].height = annot.height; + line_annotations[writing_index].anchor_char_idx = new_anchor_char_idx; + } + } + + // If we updated less annotations than there was previously, keep only those. + if writing_index < line_annotations.len() - 1 { + doc.diagnostic_annotations.annotations = + line_annotations[..=writing_index].to_vec().into(); + } + } + } +} From ecba4a0ad6aa2781c1d3df884ab5d23e8dfb6df0 Mon Sep 17 00:00:00 2001 From: "Alexis (Poliorcetics) Bourget" Date: Tue, 21 Feb 2023 01:02:00 +0100 Subject: [PATCH 09/16] fix: unwrap that could panic in visual_offset_from_anchor --- helix-core/src/position.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helix-core/src/position.rs b/helix-core/src/position.rs index 7b8dc326e82c..97a5cc5cc9f9 100644 --- a/helix-core/src/position.rs +++ b/helix-core/src/position.rs @@ -161,7 +161,7 @@ pub fn visual_offset_from_anchor( anchor_line = Some(last_pos.row); } if char_pos > pos { - last_pos.row -= anchor_line.unwrap(); + last_pos.row -= anchor_line?; return Some((last_pos, block_start)); } From dc48aeb074d43aea3a48c57afa2a6de550bed1a9 Mon Sep 17 00:00:00 2001 From: "Alexis (Poliorcetics) Bourget" Date: Tue, 21 Feb 2023 01:02:14 +0100 Subject: [PATCH 10/16] feat: render inline diagnostics --- helix-term/src/ui/editor.rs | 18 +- .../src/ui/editor/diagnostics_annotations.rs | 269 ++++++++++++++++++ 2 files changed, 286 insertions(+), 1 deletion(-) create mode 100644 helix-term/src/ui/editor/diagnostics_annotations.rs diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index a3b1765ccb23..c11db09c1003 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -36,6 +36,8 @@ use tui::buffer::Buffer as Surface; use super::statusline; use super::{document::LineDecoration, lsp::SignatureHelp}; +mod diagnostics_annotations; + pub struct EditorView { pub keymaps: Keymaps, on_next_key: Option, @@ -127,6 +129,16 @@ impl EditorView { } } + if config.lsp.display_inline_diagnostics { + line_decorations.push(diagnostics_annotations::inline_diagnostics_decorator( + doc, + view, + inner, + theme, + &text_annotations, + )); + } + if is_focused && config.cursorline { line_decorations.push(Self::cursorline_decorator(doc, view, theme)) } @@ -224,7 +236,11 @@ impl EditorView { } } - Self::render_diagnostics(doc, view, inner, surface, theme); + // If inline diagnostics are already displayed, we don't need to add the diagnostics in the + // top right corner, they would be redundant + if !config.lsp.display_inline_diagnostics { + Self::render_diagnostics(doc, view, inner, surface, theme); + } let statusline_area = view .area diff --git a/helix-term/src/ui/editor/diagnostics_annotations.rs b/helix-term/src/ui/editor/diagnostics_annotations.rs new file mode 100644 index 000000000000..84b949ba1724 --- /dev/null +++ b/helix-term/src/ui/editor/diagnostics_annotations.rs @@ -0,0 +1,269 @@ +use std::borrow::Cow; +use std::rc::Rc; + +use helix_core::diagnostic::Severity; +use helix_core::text_annotations::TextAnnotations; +use helix_core::visual_offset_from_anchor; +use helix_core::SmallVec; +use helix_view::graphics::Rect; +use helix_view::theme::Style; +use helix_view::{Document, Theme, View}; + +use crate::ui::document::{LineDecoration, LinePos, TextRenderer}; + +pub fn inline_diagnostics_decorator( + doc: &Document, + view: &View, + viewport: Rect, + theme: &Theme, + text_annotations: &TextAnnotations, +) -> Box { + let hint = theme.get("hint"); + let info = theme.get("info"); + let warning = theme.get("warning"); + let error = theme.get("error"); + + let messages = doc.diagnostic_annotations_messages(); + + let text = doc.text().slice(..); + let text_fmt = doc.text_format(viewport.width, None); + + let mut visual_offsets = Vec::with_capacity(messages.len()); + for message in messages.iter() { + visual_offsets.push( + visual_offset_from_anchor( + text, + view.offset.anchor, + message.anchor_char_idx, + &text_fmt, + text_annotations, + viewport.height as usize, + ) + .map(|x| x.0), + ); + } + + // Compute the Style for a given severity + let sev_style = move |sev| match sev { + Some(Severity::Error) => error, + // The same is done when highlighting gutters so we do it here too to be consistent. + Some(Severity::Warning) | None => warning, + Some(Severity::Info) => info, + Some(Severity::Hint) => hint, + }; + + // Vectors used when computing the items to display. We declare them here so that they're not deallocated when the + // closure is done, only when it is dropped, that way calls are don't have to allocate as much. + let mut stack = Vec::new(); + let mut left = Vec::new(); + let mut center = SmallVec::<[_; 2]>::new(); + + let line_decoration = move |renderer: &mut TextRenderer, pos: LinePos| { + let mut first_message_idx = usize::MAX; + let mut found_first = false; + let mut last_message_idx = usize::MAX; + + for (idx, message) in messages.iter().enumerate() { + if message.line == pos.doc_line { + if !found_first { + first_message_idx = idx; + found_first = true; + } + last_message_idx = idx; + } + } + + // If we found no diagnostic for this position, do nothing. + if !found_first { + return; + } + + // Extract the relevant diagnostics and visual offsets. + let messages = match messages.get(first_message_idx..=last_message_idx) { + Some(m) => m, + None => return, + }; + let visual_offsets = match visual_offsets.get(first_message_idx..=last_message_idx) { + Some(v) => v, + None => return, + }; + + // Used to build a stack of diagnostics and items to use when computing `DisplayItem` + #[derive(Debug)] + enum StackItem { + // Insert `n` spaces + Space(u16), + // Two diagnostics are overlapping in their rendering, we'll need to insert a vertical bar + Overlap, + // Leave a blank space that needs a style (used when a diagnostic message is empty) + Blank(Style), + // A diagnostic and its style (computed from its severity) + Diagnostic(Rc, Style), + } + + // Additional items to display to point the messages to the diagnostic's position in the text + #[derive(Debug)] + enum DisplayItem { + Space(u16), + Static(&'static str, Style, u16), + String(String, Style, u16), + } + + stack.clear(); + stack.reserve( + stack + .capacity() + .saturating_sub(messages.len().saturating_mul(2)), + ); + let mut prev_col = None; + let mut line_count = 0_u16; + + // Attribution: the algorithm to compute the layout of the symbols and columns here has been + // originally written by Hugo Osvaldo Barrera, for https://git.sr.ht/~whynothugo/lsp_lines.nvim. + // At the time of this comment's writing, the commit used is ec98b45c8280e5ef8c84028d4f38aa447276c002. + // + // We diverge from the original code in that we don't iterate in reverse since we display at the end of the + // loop instead of later, which means we don't have the stack problem that `lsp_lines.nvim` has. + + // First we build the stack, inserting `StackItem`s as needed + for (message, visual_offset) in messages.iter().zip(visual_offsets.iter()) { + let visual_offset = match visual_offset { + Some(o) => *o, + None => continue, + }; + + let style = sev_style(message.severity); + + // First the item to offset the diagnostic's text + stack.push(match prev_col { + Some(prev_col) if prev_col != visual_offset.col => StackItem::Space( + visual_offset + .col + .abs_diff(prev_col) + // Account for the vertical bars that are inserted to point diagnostics to + // their position in the text + .saturating_sub(1) + .min(u16::MAX as _) as _, + ), + Some(_) => StackItem::Overlap, + None => StackItem::Space(visual_offset.col.min(u16::MAX as _) as _), + }); + + let trimmed = message.message.trim(); + + // Then the diagnostic's text + if trimmed.is_empty() { + stack.push(StackItem::Blank(style)); + } else { + stack.push(StackItem::Diagnostic(Rc::clone(&message.message), style)); + } + + prev_col = Some(visual_offset.col); + line_count = line_count.saturating_add(trimmed.lines().count().min(u16::MAX as _) as _); + } + + // When several diagnostics are present in the same virtual block, we will start by + // displaying the last one and go up one at a time + let mut pos_y = viewport + .y + .saturating_add(pos.visual_line) + .saturating_add(line_count); + + // Then we iterate the stack we just built to find diagnostics + for (idx, item) in stack.iter().enumerate() { + let (text, style) = match item { + StackItem::Diagnostic(text, style) => (text.trim(), *style), + _ => continue, + }; + + left.clear(); + let mut overlap = false; + let mut multi = 0; + + // Iterate the stack for this line to find elements on the left. + let mut peekable = stack[..idx].iter().peekable(); + while let Some(item2) = peekable.next() { + match item2 { + &StackItem::Space(n) if multi == 0 => left.push(DisplayItem::Space(n)), + &StackItem::Space(n) => { + left.push(DisplayItem::String("─".repeat(n as usize), style, n)) + } + StackItem::Blank(_) => { + left.push(DisplayItem::Static( + if multi == 0 { "└" } else { "┴" }, + style, + 1, + )); + multi += 1; + } + StackItem::Diagnostic(_, style) => { + // If an overlap follows this, don't add an extra column. + if !(matches!(peekable.peek(), Some(StackItem::Overlap))) { + left.push(DisplayItem::Static("│", *style, 1)); + } + overlap = false; + } + StackItem::Overlap => overlap = true, + } + } + + let center_symbol = if overlap && multi > 0 { + "┼─── " + } else if overlap { + "├─── " + } else if multi > 0 { + "┴─── " + } else { + "└─── " + }; + + center.clear(); + center.push(DisplayItem::Static(center_symbol, style, 5)); + + // TODO: We can draw on the left side if and only if: + // a. Is the last one stacked this line. + // b. Has enough space on the left. + // c. Is just one line. + // d. Is not an overlap. + + let lines_offset = text.lines().count(); + pos_y -= lines_offset as u16; + + for (offset, line) in text.lines().enumerate() { + let mut pos_x = viewport.x; + let pos_y = pos_y + 1 + offset as u16; + + for item in left.iter().chain(center.iter()) { + let (text, style, width): (Cow, _, _) = match *item { + // No need to allocate a string here when we simply want the default + // background filled with empty space + DisplayItem::Space(n) => { + pos_x = pos_x.saturating_add(n); + continue; + } + DisplayItem::Static(s, style, n) => (s.into(), style, n), + DisplayItem::String(ref s, style, n) => (s.into(), style, n), + }; + + renderer.surface.set_string(pos_x, pos_y, text, style); + pos_x = pos_x.saturating_add(width); + } + + renderer + .surface + .set_string(pos_x, pos_y, line.trim(), style); + + center.clear(); + // Special-case for continuation lines + if overlap { + center.push(DisplayItem::Static("│", style, 1)); + center.push(DisplayItem::Space(4)); + } else { + center.push(DisplayItem::Space(5)); + } + } + } + }; + + Box::new(line_decoration) +} From 990e87f1050733ebaac1ad813ec8f53e9236b852 Mon Sep 17 00:00:00 2001 From: "Alexis (Poliorcetics) Bourget" Date: Wed, 22 Feb 2023 09:49:36 +0100 Subject: [PATCH 11/16] feat: don't compute line start for annotations positions, it's costly for pretty much no gain --- helix-term/src/application.rs | 5 +---- helix-view/src/document/annotations.rs | 13 ++++--------- 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index fc27728f08c2..f314ade71705 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -700,7 +700,6 @@ impl Application { if let Some(doc) = doc { let lang_conf = doc.language_config(); let text = doc.text(); - let text_slice = text.slice(..); let mut diagnostic_annotations = BTreeMap::new(); @@ -788,9 +787,7 @@ impl Application { }; if enabled_inline_diagnostics { - let char_idx = text_slice.line_to_char(diagnostic.range.start.line as usize); - - *diagnostic_annotations.entry(char_idx).or_default() += diagnostic.message.trim().lines().count(); + *diagnostic_annotations.entry(start).or_default() += diagnostic.message.trim().lines().count(); } Some(Diagnostic { diff --git a/helix-view/src/document/annotations.rs b/helix-view/src/document/annotations.rs index 71ece9bcf59d..12e3f95a3bef 100644 --- a/helix-view/src/document/annotations.rs +++ b/helix-view/src/document/annotations.rs @@ -141,13 +141,8 @@ pub(super) fn apply_changes_to_diagnostic_annotations( // See `None` case above None | Some([]) => doc.diagnostic_annotations = Default::default(), Some(line_annotations) => { - let doc_text = doc.text.slice(..); - - let get_new_anchor_char_idx = |annot: &LineAnnotation| { - let new_char_idx = changes.map_pos(annot.anchor_char_idx, Assoc::After); - let line = doc_text.char_to_line(new_char_idx); - doc_text.line_to_char(line) - }; + let map_pos = + |annot: &LineAnnotation| changes.map_pos(annot.anchor_char_idx, Assoc::After); // The algorithm here does its best to modify in place to avoid reallocations as much as possible // @@ -163,7 +158,7 @@ pub(super) fn apply_changes_to_diagnostic_annotations( // 4) If the last write position was not the last member of the current lines annotations, it means we // merged some of them together so we update the saved line annotations. - let new_anchor_char_idx = get_new_anchor_char_idx(&line_annotations[0]); + let new_anchor_char_idx = map_pos(&line_annotations[0]); line_annotations[0].anchor_char_idx = new_anchor_char_idx; let mut previous_anchor_char_idx = new_anchor_char_idx; @@ -172,7 +167,7 @@ pub(super) fn apply_changes_to_diagnostic_annotations( for reading_index in 1..line_annotations.len() { let annot = &mut line_annotations[reading_index]; - let new_anchor_char_idx = get_new_anchor_char_idx(annot); + let new_anchor_char_idx = map_pos(annot); if new_anchor_char_idx == previous_anchor_char_idx { line_annotations[writing_index].height += annot.height; From 34edf7bbba8dfe4ca7259301cf35b0a9f179004f Mon Sep 17 00:00:00 2001 From: "Alexis (Poliorcetics) Bourget" Date: Fri, 24 Feb 2023 18:37:18 +0100 Subject: [PATCH 12/16] feat: support setting a default style for the inline diagnostics --- book/src/themes.md | 1 + .../src/ui/editor/diagnostics_annotations.rs | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/book/src/themes.md b/book/src/themes.md index 929f821e64cf..cfa4bda60e7d 100644 --- a/book/src/themes.md +++ b/book/src/themes.md @@ -295,6 +295,7 @@ These scopes are used for theming the editor interface: | `ui.text.info` | The key: command text in `ui.popup.info` boxes | | `ui.virtual.ruler` | Ruler columns (see the [`editor.rulers` config][editor-section]) | | `ui.virtual.whitespace` | Visible whitespace characters | +| `ui.virtual.diagnostics` | Default style for inline diagnostics lines (notably control the background) | | `ui.virtual.indent-guide` | Vertical indent width guides | | `ui.virtual.inlay-hint` | Default style for inlay hints of all kinds | | `ui.virtual.inlay-hint.parameter` | Style for inlay hints of kind `parameter` (LSPs are not required to set a kind) | diff --git a/helix-term/src/ui/editor/diagnostics_annotations.rs b/helix-term/src/ui/editor/diagnostics_annotations.rs index 84b949ba1724..c1948653a6d0 100644 --- a/helix-term/src/ui/editor/diagnostics_annotations.rs +++ b/helix-term/src/ui/editor/diagnostics_annotations.rs @@ -18,6 +18,9 @@ pub fn inline_diagnostics_decorator( theme: &Theme, text_annotations: &TextAnnotations, ) -> Box { + let whole_view_aera = view.area; + let background = theme.get("ui.virtual.diagnostics"); + let hint = theme.get("hint"); let info = theme.get("info"); let warning = theme.get("warning"); @@ -226,8 +229,18 @@ pub fn inline_diagnostics_decorator( // c. Is just one line. // d. Is not an overlap. - let lines_offset = text.lines().count(); - pos_y -= lines_offset as u16; + let lines_offset = text.lines().count() as u16; + pos_y -= lines_offset; + + // Use `view` since it's the whole outer view instead of just the inner area so that the background + // is also applied to the gutters and other elements that are not in the editable part of the document + let diag_area = Rect::new( + whole_view_aera.x, + pos_y + 1, + whole_view_aera.width, + lines_offset, + ); + renderer.surface.set_style(diag_area, background); for (offset, line) in text.lines().enumerate() { let mut pos_x = viewport.x; From 84f4b40218a41e73f46495a44c0492c9e48fe4e6 Mon Sep 17 00:00:00 2001 From: "Alexis (Poliorcetics) Bourget" Date: Sun, 26 Feb 2023 01:24:20 +0100 Subject: [PATCH 13/16] nit: aera -> area --- helix-term/src/ui/editor/diagnostics_annotations.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/helix-term/src/ui/editor/diagnostics_annotations.rs b/helix-term/src/ui/editor/diagnostics_annotations.rs index c1948653a6d0..d2c6212a3071 100644 --- a/helix-term/src/ui/editor/diagnostics_annotations.rs +++ b/helix-term/src/ui/editor/diagnostics_annotations.rs @@ -18,7 +18,7 @@ pub fn inline_diagnostics_decorator( theme: &Theme, text_annotations: &TextAnnotations, ) -> Box { - let whole_view_aera = view.area; + let whole_view_area = view.area; let background = theme.get("ui.virtual.diagnostics"); let hint = theme.get("hint"); @@ -235,9 +235,9 @@ pub fn inline_diagnostics_decorator( // Use `view` since it's the whole outer view instead of just the inner area so that the background // is also applied to the gutters and other elements that are not in the editable part of the document let diag_area = Rect::new( - whole_view_aera.x, + whole_view_area.x, pos_y + 1, - whole_view_aera.width, + whole_view_area.width, lines_offset, ); renderer.surface.set_style(diag_area, background); From f4f449182ff9033085f9e685fcbf4ce397c888ac Mon Sep 17 00:00:00 2001 From: "Alexis (Poliorcetics) Bourget" Date: Sun, 26 Feb 2023 02:01:17 +0100 Subject: [PATCH 14/16] fix: Prevent diagnostics from displaying out of view, fixing a panic --- .../src/ui/editor/diagnostics_annotations.rs | 34 ++++++++++++++----- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/helix-term/src/ui/editor/diagnostics_annotations.rs b/helix-term/src/ui/editor/diagnostics_annotations.rs index d2c6212a3071..927b244198a3 100644 --- a/helix-term/src/ui/editor/diagnostics_annotations.rs +++ b/helix-term/src/ui/editor/diagnostics_annotations.rs @@ -21,6 +21,11 @@ pub fn inline_diagnostics_decorator( let whole_view_area = view.area; let background = theme.get("ui.virtual.diagnostics"); + // The maximum Y that diagnostics can be printed on. Necessary because we may want to print + // 5 lines of diagnostics while the view only has 3 left at the bottom and two more just out + // of bounds. + let max_y = viewport.height.saturating_sub(1).saturating_add(viewport.y); + let hint = theme.get("hint"); let info = theme.get("info"); let warning = theme.get("warning"); @@ -167,7 +172,7 @@ pub fn inline_diagnostics_decorator( // When several diagnostics are present in the same virtual block, we will start by // displaying the last one and go up one at a time - let mut pos_y = viewport + let mut code_pos_y = viewport .y .saturating_add(pos.visual_line) .saturating_add(line_count); @@ -179,6 +184,16 @@ pub fn inline_diagnostics_decorator( _ => continue, }; + // Do the line count and check of pos_y now, it avoids having to build the display items + // for nothing + let lines_offset = text.lines().count() as u16; + code_pos_y -= lines_offset; + + // If the first line to be printed is out of bound, don't display anything more of the current diagnostic + if code_pos_y + 1 > max_y { + continue; + } + left.clear(); let mut overlap = false; let mut multi = 0; @@ -229,14 +244,12 @@ pub fn inline_diagnostics_decorator( // c. Is just one line. // d. Is not an overlap. - let lines_offset = text.lines().count() as u16; - pos_y -= lines_offset; - // Use `view` since it's the whole outer view instead of just the inner area so that the background // is also applied to the gutters and other elements that are not in the editable part of the document let diag_area = Rect::new( whole_view_area.x, - pos_y + 1, + // We checked at the start of the loop that this is valid + code_pos_y + 1, whole_view_area.width, lines_offset, ); @@ -244,7 +257,12 @@ pub fn inline_diagnostics_decorator( for (offset, line) in text.lines().enumerate() { let mut pos_x = viewport.x; - let pos_y = pos_y + 1 + offset as u16; + let diag_pos_y = code_pos_y + 1 + offset as u16; + // If we're out of bounds, don't display this diagnostic line, nor the following + // ones since they'll be out of bounds too. + if diag_pos_y > max_y { + break; + } for item in left.iter().chain(center.iter()) { let (text, style, width): (Cow, _, _) = match *item { @@ -258,13 +276,13 @@ pub fn inline_diagnostics_decorator( DisplayItem::String(ref s, style, n) => (s.into(), style, n), }; - renderer.surface.set_string(pos_x, pos_y, text, style); + renderer.surface.set_string(pos_x, diag_pos_y, text, style); pos_x = pos_x.saturating_add(width); } renderer .surface - .set_string(pos_x, pos_y, line.trim(), style); + .set_string(pos_x, diag_pos_y, line.trim(), style); center.clear(); // Special-case for continuation lines From 2c5dcfa7c001d6c0ded2f8b4dcc07313ed0ff9ba Mon Sep 17 00:00:00 2001 From: "Alexis (Poliorcetics) Bourget" Date: Thu, 9 Mar 2023 10:17:03 +0100 Subject: [PATCH 15/16] fix: inline diagnostics now don't extend past their own view --- .../src/ui/editor/diagnostics_annotations.rs | 41 ++++++++++++------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/helix-term/src/ui/editor/diagnostics_annotations.rs b/helix-term/src/ui/editor/diagnostics_annotations.rs index 927b244198a3..8004594adb03 100644 --- a/helix-term/src/ui/editor/diagnostics_annotations.rs +++ b/helix-term/src/ui/editor/diagnostics_annotations.rs @@ -113,8 +113,8 @@ pub fn inline_diagnostics_decorator( #[derive(Debug)] enum DisplayItem { Space(u16), - Static(&'static str, Style, u16), - String(String, Style, u16), + Static(&'static str, Style), + String(String, Style), } stack.clear(); @@ -204,20 +204,19 @@ pub fn inline_diagnostics_decorator( match item2 { &StackItem::Space(n) if multi == 0 => left.push(DisplayItem::Space(n)), &StackItem::Space(n) => { - left.push(DisplayItem::String("─".repeat(n as usize), style, n)) + left.push(DisplayItem::String("─".repeat(n as usize), style)) } StackItem::Blank(_) => { left.push(DisplayItem::Static( if multi == 0 { "└" } else { "┴" }, style, - 1, )); multi += 1; } StackItem::Diagnostic(_, style) => { // If an overlap follows this, don't add an extra column. if !(matches!(peekable.peek(), Some(StackItem::Overlap))) { - left.push(DisplayItem::Static("│", *style, 1)); + left.push(DisplayItem::Static("│", *style)); } overlap = false; } @@ -236,7 +235,7 @@ pub fn inline_diagnostics_decorator( }; center.clear(); - center.push(DisplayItem::Static(center_symbol, style, 5)); + center.push(DisplayItem::Static(center_symbol, style)); // TODO: We can draw on the left side if and only if: // a. Is the last one stacked this line. @@ -255,6 +254,8 @@ pub fn inline_diagnostics_decorator( ); renderer.surface.set_style(diag_area, background); + let area_right = diag_area.right(); + for (offset, line) in text.lines().enumerate() { let mut pos_x = viewport.x; let diag_pos_y = code_pos_y + 1 + offset as u16; @@ -265,29 +266,39 @@ pub fn inline_diagnostics_decorator( } for item in left.iter().chain(center.iter()) { - let (text, style, width): (Cow, _, _) = match *item { + let (text, style): (Cow, _) = match *item { // No need to allocate a string here when we simply want the default // background filled with empty space DisplayItem::Space(n) => { pos_x = pos_x.saturating_add(n); continue; } - DisplayItem::Static(s, style, n) => (s.into(), style, n), - DisplayItem::String(ref s, style, n) => (s.into(), style, n), + DisplayItem::Static(s, style) => (s.into(), style), + DisplayItem::String(ref s, style) => (s.into(), style), }; - renderer.surface.set_string(pos_x, diag_pos_y, text, style); - pos_x = pos_x.saturating_add(width); + let (new_x_pos, _) = renderer.surface.set_stringn( + pos_x, + diag_pos_y, + text, + area_right.saturating_sub(pos_x).into(), + style, + ); + pos_x = new_x_pos; } - renderer - .surface - .set_string(pos_x, diag_pos_y, line.trim(), style); + renderer.surface.set_stringn( + pos_x, + diag_pos_y, + line.trim(), + area_right.saturating_sub(pos_x).into(), + style, + ); center.clear(); // Special-case for continuation lines if overlap { - center.push(DisplayItem::Static("│", style, 1)); + center.push(DisplayItem::Static("│", style)); center.push(DisplayItem::Space(4)); } else { center.push(DisplayItem::Space(5)); From 21db369d007ff1ebb400f73a562d99bff614bff1 Mon Sep 17 00:00:00 2001 From: "Alexis (Poliorcetics) Bourget" Date: Sat, 11 Mar 2023 10:44:37 +0100 Subject: [PATCH 16/16] fix: remove duplicated import after rebase --- helix-view/src/document.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 8fb915946286..b475d217e395 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -921,8 +921,6 @@ impl Document { /// Apply a [`Transaction`] to the [`Document`] to change its text. fn apply_impl(&mut self, transaction: &Transaction, view_id: ViewId) -> bool { - use helix_core::Assoc; - let old_doc = self.text().clone(); let changes = transaction.changes();