diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index ad4f73bcb014d..f0828b4dd4c87 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -10,6 +10,8 @@ use helix_core::{ coords_at_pos, graphemes::ensure_grapheme_boundary, syntax::{self, HighlightEvent}, + unicode::segmentation::UnicodeSegmentation, + unicode::width::UnicodeWidthStr, LineEnding, Position, Range, }; use helix_view::{ @@ -111,6 +113,12 @@ impl EditorView { let last_line = view.last_line(doc); + let text_annotations = doc + .text_annotations() + .iter() + .filter(|v| (view.first_line..last_line).contains(&v.line)) + .collect::>(); + let range = { // calculate viewport byte ranges let start = text.line_to_byte(view.first_line); @@ -223,6 +231,27 @@ impl EditorView { .collect(), )); + let out_of_bounds = |visual_x: u16| { + visual_x < view.first_col as u16 || visual_x >= viewport.width + view.first_col as u16 + }; + + use crate::helix_view::decorations::TextAnnotation; + let render_annotation = |annot: &TextAnnotation, line: u16, pos: u16, surface: &mut Surface| { + let mut visual_x = pos; + for grapheme in annot.text.graphemes(true) { + if out_of_bounds(visual_x) { + break; + } + surface.set_string( + viewport.x + visual_x - view.first_col as u16, + viewport.y + line, + grapheme, + annot.style, + ); + visual_x = visual_x.saturating_add(grapheme.width() as u16); + } + }; + 'outer: for event in highlights { match event { HighlightEvent::HighlightStart(span) => { @@ -242,11 +271,8 @@ impl EditorView { }); for grapheme in RopeGraphemes::new(text) { - let out_of_bounds = visual_x < view.first_col as u16 - || visual_x >= viewport.width + view.first_col as u16; - if LineEnding::from_rope_slice(&grapheme).is_some() { - if !out_of_bounds { + if !out_of_bounds(visual_x) { // we still want to render an empty cell with the style surface.set_string( viewport.x + visual_x - view.first_col as u16, @@ -254,6 +280,13 @@ impl EditorView { " ", style, ); + visual_x += 1; + } + + if let Some(annot) = text_annotations.iter().find(|t| { + t.text_type.is_eol() && t.line == view.first_line + line as usize + }) { + render_annotation(annot, line, visual_x, surface); } visual_x = 0; @@ -276,7 +309,7 @@ impl EditorView { (grapheme.as_ref(), width) }; - if !out_of_bounds { + if !out_of_bounds(visual_x) { // if we're offscreen just keep going until we hit a new line surface.set_string( viewport.x + visual_x - view.first_col as u16, @@ -293,6 +326,12 @@ impl EditorView { } } + for &annot in text_annotations.iter().filter(|t| t.text_type.is_overlay()) { + let visual_x = annot.offset.unwrap() as u16; + let line = (annot.line - view.first_line) as u16; + render_annotation(annot, line, visual_x, surface); + } + // render gutters let linenr: Style = theme.get("ui.linenr"); diff --git a/helix-view/src/decorations.rs b/helix-view/src/decorations.rs new file mode 100644 index 0000000000000..c5c1ba1656775 --- /dev/null +++ b/helix-view/src/decorations.rs @@ -0,0 +1,30 @@ +use crate::graphics::Style; + +#[derive(Clone, Copy, PartialEq)] +pub enum TextAnnotationType { + /// Add to end of line + Eol, + /// Replace actual text or arbitary cells with annotations + Overlay, +} + +impl TextAnnotationType { + pub fn is_eol(&self) -> bool { + *self == Self::Eol + } + + pub fn is_overlay(&self) -> bool { + *self == Self::Overlay + } +} + +pub struct TextAnnotation { + /// String used to namespace and identify similar annotations + pub scope: String, + pub text: String, + pub style: Style, + pub line: usize, + /// Render at this column, None for Eol + pub offset: Option, + pub text_type: TextAnnotationType, +} diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index f85ded11699c8..2185d3051b2e1 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -18,7 +18,7 @@ use helix_core::{ }; use helix_lsp::util::LspFormatting; -use crate::{DocumentId, Theme, ViewId}; +use crate::{DocumentId, Theme, ViewId, decorations::TextAnnotation}; const BUF_SIZE: usize = 8192; @@ -105,6 +105,7 @@ pub struct Document { version: i32, // should be usize? diagnostics: Vec, + text_annotations: Vec, language_server: Option>, } @@ -438,6 +439,7 @@ impl Document { language: None, changes, old_state, + text_annotations: vec![], diagnostics: Vec::new(), version: 0, history: Cell::new(History::default()), @@ -1060,6 +1062,22 @@ impl Document { }) } + pub fn text_annotations(&self) -> &[TextAnnotation] { + &self.text_annotations + } + + pub fn push_text_annotations(&mut self, annots: Vec) { + self.text_annotations.extend(annots) + } + + /// Remove annotations that return true for the given predicate + pub fn remove_text_annotations(&mut self, f: F) + where + F: Fn(&TextAnnotation) -> bool, + { + self.text_annotations.retain(|t| !f(t)) + } + // pub fn slice(&self, range: R) -> RopeSlice where R: RangeBounds { // self.state.doc.slice // } diff --git a/helix-view/src/lib.rs b/helix-view/src/lib.rs index 9bcc0b7d51124..fd609ee99b48c 100644 --- a/helix-view/src/lib.rs +++ b/helix-view/src/lib.rs @@ -2,6 +2,7 @@ pub mod macros; pub mod clipboard; +pub mod decorations; pub mod document; pub mod editor; pub mod graphics;