diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 7cb29c3b1ecb..c6e5ed759cca 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -12,10 +12,11 @@ use helix_core::{ }, movement::Direction, syntax::{self, HighlightEvent}, - unicode::width::UnicodeWidthStr, + unicode::{segmentation::UnicodeSegmentation, width::UnicodeWidthStr}, LineEnding, Position, Range, Selection, Transaction, }; use helix_view::{ + decorations::{TextAnnotation, TextAnnotationKind}, document::{Mode, SCRATCH_BUFFER_NAME}, editor::{CompleteAction, CursorShapeConfig}, graphics::{Color, CursorKind, Modifier, Rect, Style}, @@ -395,6 +396,11 @@ impl EditorView { // It's slightly more efficient to produce a full RopeSlice from the Rope, then slice that a bunch // of times than it is to always call Rope::slice/get_slice (it will internally always hit RSEnum::Light). let text = doc.text().slice(..); + let last_line = std::cmp::min( + // Saturating subs to make it inclusive zero indexing. + (offset.row + viewport.height as usize).saturating_sub(1), + doc.text().len_lines().saturating_sub(1), + ); let characters = &whitespace.characters; @@ -450,6 +456,37 @@ impl EditorView { } }; + // It's slightly more efficient to produce a full RopeSlice from the Rope, then slice that a bunch + // of times than it is to always call Rope::slice/get_slice (it will internally always hit RSEnum::Light). + let text = text.slice(..); + let out_of_bounds = |visual_x: u16| { + visual_x < offset.col as u16 || visual_x >= viewport.width + offset.col as u16 + }; + + 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 - offset.col as u16, + viewport.y + line, + grapheme, + annot.style, + ); + visual_x = visual_x.saturating_add(grapheme.width() as u16); + } + }; + + let text_annotations = doc + .text_annotations() + .iter() + .flat_map(|(_, annots)| annots) + .filter(|annot| (offset.row..last_line).contains(&annot.line)) + .collect::>(); + 'outer: for event in highlights { match event { HighlightEvent::HighlightStart(span) => { @@ -488,11 +525,8 @@ impl EditorView { use helix_core::graphemes::{grapheme_width, RopeGraphemes}; for grapheme in RopeGraphemes::new(text) { - let out_of_bounds = visual_x < offset.col as u16 - || visual_x >= viewport.width + offset.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 - offset.col as u16, @@ -500,10 +534,18 @@ impl EditorView { &newline, style.patch(whitespace_style), ); + visual_x += 1; } draw_indent_guides(last_line_indent_level, line, surface); + if let Some(annot) = text_annotations + .iter() + .find(|t| t.kind.is_eol() && t.line == offset.row + line as usize) + { + render_annotation(annot, line, visual_x, surface); + } + visual_x = 0; line += 1; is_in_indent_area = true; @@ -540,7 +582,7 @@ impl EditorView { let cut_off_start = offset.col.saturating_sub(visual_x as usize); - 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 - offset.col as u16, @@ -582,6 +624,13 @@ impl EditorView { } } } + + for annot in &text_annotations { + if let TextAnnotationKind::Overlay(visual_x) = annot.kind { + let line = (annot.line - offset.row) as u16; + render_annotation(annot, line, visual_x as u16, surface); + } + } } /// Render brace match, etc (meant for the focused view only) diff --git a/helix-view/src/decorations.rs b/helix-view/src/decorations.rs new file mode 100644 index 000000000000..7ad0b261badd --- /dev/null +++ b/helix-view/src/decorations.rs @@ -0,0 +1,32 @@ +use std::borrow::Cow; + +use crate::graphics::Style; + +#[derive(Clone, Copy, PartialEq)] +pub enum TextAnnotationKind { + /// Add to end of line + Eol, + /// Replace actual text or arbitary cells with annotations. + /// Specifies an offset from the 0th column. + Overlay(usize), +} + +impl TextAnnotationKind { + pub fn is_eol(&self) -> bool { + *self == Self::Eol + } + + pub fn is_overlay(&self) -> bool { + matches!(*self, Self::Overlay(_)) + } +} + +/// Namespaces and identifes similar annotations +pub type TextAnnotationGroup = &'static str; + +pub struct TextAnnotation { + pub text: Cow<'static, str>, + pub style: Style, + pub line: usize, + pub kind: TextAnnotationKind, +} diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 2ef99c6ad136..948b6e6d4d72 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -23,7 +23,9 @@ use helix_core::{ DEFAULT_LINE_ENDING, }; -use crate::{DocumentId, Editor, ViewId}; +use crate::decorations::TextAnnotationGroup; +use crate::Editor; +use crate::{decorations::TextAnnotation, DocumentId, ViewId}; /// 8kB of buffer space for encoding and decoding `Rope`s. const BUF_SIZE: usize = 8192; @@ -119,6 +121,7 @@ pub struct Document { pub(crate) modified_since_accessed: bool, diagnostics: Vec, + text_annotations: HashMap>, language_server: Option>, } @@ -351,6 +354,7 @@ impl Document { language: None, changes, old_state, + text_annotations: HashMap::new(), diagnostics: Vec::new(), version: 0, history: Cell::new(History::default()), @@ -1038,6 +1042,31 @@ impl Document { .map(helix_core::path::get_relative_path) } + pub fn text_annotations(&self) -> &HashMap> { + &self.text_annotations + } + + pub fn push_text_annotations>( + &mut self, + group: TextAnnotationGroup, + annots: I, + ) { + self.text_annotations + .entry(group) + .or_default() + .extend(annots); + } + + pub fn clear_text_annotations(&mut self, group: TextAnnotationGroup) { + if let Some(annots) = self.text_annotations.get_mut(group) { + annots.clear() + } + } + + // pub fn slice(&self, range: R) -> RopeSlice where R: RangeBounds { + // self.state.doc.slice + // } + // transact(Fn) ? // -- LSP methods diff --git a/helix-view/src/lib.rs b/helix-view/src/lib.rs index 788304bce360..6bd2e099a55c 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;