Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'helix-term/src/ui/document.rs')
| -rw-r--r-- | helix-term/src/ui/document.rs | 586 |
1 files changed, 281 insertions, 305 deletions
diff --git a/helix-term/src/ui/document.rs b/helix-term/src/ui/document.rs index de85268a..bcbaa351 100644 --- a/helix-term/src/ui/document.rs +++ b/helix-term/src/ui/document.rs @@ -3,7 +3,8 @@ use std::cmp::min; use helix_core::doc_formatter::{DocumentFormatter, GraphemeSource, TextFormat}; use helix_core::graphemes::Grapheme; use helix_core::str_utils::char_to_byte_idx; -use helix_core::syntax::{self, HighlightEvent, Highlighter, OverlayHighlights}; +use helix_core::syntax::Highlight; +use helix_core::syntax::HighlightEvent; use helix_core::text_annotations::TextAnnotations; use helix_core::{visual_offset_from_block, Position, RopeSlice}; use helix_stdx::rope::RopeSliceExt; @@ -11,10 +12,82 @@ use helix_view::editor::{WhitespaceConfig, WhitespaceRenderValue}; use helix_view::graphics::Rect; use helix_view::theme::Style; use helix_view::view::ViewPosition; -use helix_view::{Document, Theme}; +use helix_view::Document; +use helix_view::Theme; use tui::buffer::Buffer as Surface; -use crate::ui::text_decorations::DecorationManager; +pub trait LineDecoration { + fn render_background(&mut self, _renderer: &mut TextRenderer, _pos: LinePos) {} + fn render_foreground( + &mut self, + _renderer: &mut TextRenderer, + _pos: LinePos, + _end_char_idx: usize, + ) { + } +} + +impl<F: FnMut(&mut TextRenderer, LinePos)> LineDecoration for F { + fn render_background(&mut self, renderer: &mut TextRenderer, pos: LinePos) { + self(renderer, pos) + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +enum StyleIterKind { + /// base highlights (usually emitted by TS), byte indices (potentially not codepoint aligned) + BaseHighlights, + /// overlay highlights (emitted by custom code from selections), char indices + Overlay, +} + +/// A wrapper around a HighlightIterator +/// that merges the layered highlights to create the final text style +/// and yields the active text style and the char_idx where the active +/// style will have to be recomputed. +/// +/// TODO(ropey2): hopefully one day helix and ropey will operate entirely +/// on byte ranges and we can remove this +struct StyleIter<'a, H: Iterator<Item = HighlightEvent>> { + text_style: Style, + active_highlights: Vec<Highlight>, + highlight_iter: H, + kind: StyleIterKind, + text: RopeSlice<'a>, + theme: &'a Theme, +} + +impl<H: Iterator<Item = HighlightEvent>> Iterator for StyleIter<'_, H> { + type Item = (Style, usize); + fn next(&mut self) -> Option<(Style, usize)> { + while let Some(event) = self.highlight_iter.next() { + match event { + HighlightEvent::HighlightStart(highlights) => { + self.active_highlights.push(highlights) + } + HighlightEvent::HighlightEnd => { + self.active_highlights.pop(); + } + HighlightEvent::Source { start, mut end } => { + if start == end { + continue; + } + let style = self + .active_highlights + .iter() + .fold(self.text_style, |acc, span| { + acc.patch(self.theme.highlight(span.0)) + }); + if self.kind == StyleIterKind::BaseHighlights { + end = self.text.byte_to_next_char(end); + } + return Some((style, end)); + } + } + } + None + } +} #[derive(Debug, PartialEq, Eq, Copy, Clone)] pub struct LinePos { @@ -25,8 +98,15 @@ pub struct LinePos { pub doc_line: usize, /// Vertical offset from the top of the inner view area pub visual_line: u16, + /// The first char index of this visual line. + /// Note that if the visual line is entirely filled by + /// a very long inline virtual text then this index will point + /// at the next (non-virtual) char after this visual line + pub start_char_idx: usize, } +pub type TranslatedPosition<'a> = (usize, Box<dyn FnMut(&mut TextRenderer, Position) + 'a>); + #[allow(clippy::too_many_arguments)] pub fn render_document( surface: &mut Surface, @@ -34,147 +114,248 @@ pub fn render_document( doc: &Document, offset: ViewPosition, doc_annotations: &TextAnnotations, - syntax_highlighter: Option<Highlighter<'_>>, - overlay_highlights: Vec<syntax::OverlayHighlights>, + syntax_highlight_iter: impl Iterator<Item = HighlightEvent>, + overlay_highlight_iter: impl Iterator<Item = HighlightEvent>, theme: &Theme, - decorations: DecorationManager, + line_decoration: &mut [Box<dyn LineDecoration + '_>], + translated_positions: &mut [TranslatedPosition], ) { - let mut renderer = TextRenderer::new( - surface, - doc, - theme, - Position::new(offset.vertical_offset, offset.horizontal_offset), - viewport, - ); + let mut renderer = TextRenderer::new(surface, doc, theme, offset.horizontal_offset, viewport); render_text( &mut renderer, doc.text().slice(..), - offset.anchor, + offset, &doc.text_format(viewport.width, Some(theme)), doc_annotations, - syntax_highlighter, - overlay_highlights, + syntax_highlight_iter, + overlay_highlight_iter, theme, - decorations, + line_decoration, + translated_positions, ) } +fn translate_positions( + char_pos: usize, + first_visible_char_idx: usize, + translated_positions: &mut [TranslatedPosition], + text_fmt: &TextFormat, + renderer: &mut TextRenderer, + pos: Position, +) { + // check if any positions translated on the fly (like cursor) has been reached + for (char_idx, callback) in &mut *translated_positions { + if *char_idx < char_pos && *char_idx >= first_visible_char_idx { + // by replacing the char_index with usize::MAX large number we ensure + // that the same position is only translated once + // text will never reach usize::MAX as rust memory allocations are limited + // to isize::MAX + *char_idx = usize::MAX; + + if text_fmt.soft_wrap { + callback(renderer, pos) + } else if pos.col >= renderer.col_offset + && pos.col - renderer.col_offset < renderer.viewport.width as usize + { + callback( + renderer, + Position { + row: pos.row, + col: pos.col - renderer.col_offset, + }, + ) + } + } + } +} + #[allow(clippy::too_many_arguments)] -pub fn render_text( +pub fn render_text<'t>( renderer: &mut TextRenderer, - text: RopeSlice<'_>, - anchor: usize, + text: RopeSlice<'t>, + offset: ViewPosition, text_fmt: &TextFormat, text_annotations: &TextAnnotations, - syntax_highlighter: Option<Highlighter<'_>>, - overlay_highlights: Vec<syntax::OverlayHighlights>, + syntax_highlight_iter: impl Iterator<Item = HighlightEvent>, + overlay_highlight_iter: impl Iterator<Item = HighlightEvent>, theme: &Theme, - mut decorations: DecorationManager, + line_decorations: &mut [Box<dyn LineDecoration + '_>], + translated_positions: &mut [TranslatedPosition], ) { - let row_off = visual_offset_from_block(text, anchor, anchor, text_fmt, text_annotations) - .0 - .row; - - let mut formatter = - DocumentFormatter::new_at_prev_checkpoint(text, text_fmt, text_annotations, anchor); - let mut syntax_highlighter = - SyntaxHighlighter::new(syntax_highlighter, text, theme, renderer.text_style); - let mut overlay_highlighter = OverlayHighlighter::new(overlay_highlights, theme); + let ( + Position { + row: mut row_off, .. + }, + mut char_pos, + ) = visual_offset_from_block( + text, + offset.anchor, + offset.anchor, + text_fmt, + text_annotations, + ); + row_off += offset.vertical_offset; + + let (mut formatter, mut first_visible_char_idx) = + DocumentFormatter::new_at_prev_checkpoint(text, text_fmt, text_annotations, offset.anchor); + let mut syntax_styles = StyleIter { + text_style: renderer.text_style, + active_highlights: Vec::with_capacity(64), + highlight_iter: syntax_highlight_iter, + kind: StyleIterKind::BaseHighlights, + theme, + text, + }; + let mut overlay_styles = StyleIter { + text_style: Style::default(), + active_highlights: Vec::with_capacity(64), + highlight_iter: overlay_highlight_iter, + kind: StyleIterKind::Overlay, + theme, + text, + }; let mut last_line_pos = LinePos { first_visual_line: false, doc_line: usize::MAX, visual_line: u16::MAX, + start_char_idx: usize::MAX, }; - let mut last_line_end = 0; let mut is_in_indent_area = true; let mut last_line_indent_level = 0; - let mut reached_view_top = false; + let mut syntax_style_span = syntax_styles + .next() + .unwrap_or_else(|| (Style::default(), usize::MAX)); + let mut overlay_style_span = overlay_styles + .next() + .unwrap_or_else(|| (Style::default(), usize::MAX)); loop { - let Some(mut grapheme) = formatter.next() else { + // formattter.line_pos returns to line index of the next grapheme + // so it must be called before formatter.next + let doc_line = formatter.line_pos(); + let Some((grapheme, mut pos)) = formatter.next() else { + let mut last_pos = formatter.visual_pos(); + if last_pos.row >= row_off { + last_pos.col -= 1; + last_pos.row -= row_off; + // check if any positions translated on the fly (like cursor) are at the EOF + translate_positions( + char_pos + 1, + first_visible_char_idx, + translated_positions, + text_fmt, + renderer, + last_pos, + ); + } break; }; // skip any graphemes on visual lines before the block start - if grapheme.visual_pos.row < row_off { + if pos.row < row_off { + if char_pos >= syntax_style_span.1 { + syntax_style_span = if let Some(syntax_style_span) = syntax_styles.next() { + syntax_style_span + } else { + break; + } + } + if char_pos >= overlay_style_span.1 { + overlay_style_span = if let Some(overlay_style_span) = overlay_styles.next() { + overlay_style_span + } else { + break; + } + } + char_pos += grapheme.doc_chars(); + first_visible_char_idx = char_pos + 1; continue; } - grapheme.visual_pos.row -= row_off; - if !reached_view_top { - decorations.prepare_for_rendering(grapheme.char_idx); - reached_view_top = true; - } + pos.row -= row_off; // if the end of the viewport is reached stop rendering - if grapheme.visual_pos.row as u16 >= renderer.viewport.height + renderer.offset.row as u16 { + if pos.row as u16 >= renderer.viewport.height { break; } // apply decorations before rendering a new line - if grapheme.visual_pos.row as u16 != last_line_pos.visual_line { - // we initiate doc_line with usize::MAX because no file - // can reach that size (memory allocations are limited to isize::MAX) - // initially there is no "previous" line (so doc_line is set to usize::MAX) - // in that case we don't need to draw indent guides/virtual text - if last_line_pos.doc_line != usize::MAX { - // draw indent guides for the last line + if pos.row as u16 != last_line_pos.visual_line { + if pos.row > 0 { renderer.draw_indent_guides(last_line_indent_level, last_line_pos.visual_line); is_in_indent_area = true; - decorations.render_virtual_lines(renderer, last_line_pos, last_line_end) + for line_decoration in &mut *line_decorations { + line_decoration.render_foreground(renderer, last_line_pos, char_pos); + } } last_line_pos = LinePos { - first_visual_line: grapheme.line_idx != last_line_pos.doc_line, - doc_line: grapheme.line_idx, - visual_line: grapheme.visual_pos.row as u16, + first_visual_line: doc_line != last_line_pos.doc_line, + doc_line, + visual_line: pos.row as u16, + start_char_idx: char_pos, }; - decorations.decorate_line(renderer, last_line_pos); + for line_decoration in &mut *line_decorations { + line_decoration.render_background(renderer, last_line_pos); + } } // acquire the correct grapheme style - while grapheme.char_idx >= syntax_highlighter.pos { - syntax_highlighter.advance(); + if char_pos >= syntax_style_span.1 { + syntax_style_span = syntax_styles + .next() + .unwrap_or((Style::default(), usize::MAX)); } - while grapheme.char_idx >= overlay_highlighter.pos { - overlay_highlighter.advance(); + if char_pos >= overlay_style_span.1 { + overlay_style_span = overlay_styles + .next() + .unwrap_or((Style::default(), usize::MAX)); } + char_pos += grapheme.doc_chars(); + + // check if any positions translated on the fly (like cursor) has been reached + translate_positions( + char_pos, + first_visible_char_idx, + translated_positions, + text_fmt, + renderer, + pos, + ); - let grapheme_style = if let GraphemeSource::VirtualText { highlight } = grapheme.source { - let mut style = renderer.text_style; - if let Some(highlight) = highlight { - style = style.patch(theme.highlight(highlight)); - } - GraphemeStyle { - syntax_style: style, - overlay_style: Style::default(), - } - } else { - GraphemeStyle { - syntax_style: syntax_highlighter.style, - overlay_style: overlay_highlighter.style, - } - }; - decorations.decorate_grapheme(renderer, &grapheme); + let (syntax_style, overlay_style) = + if let GraphemeSource::VirtualText { highlight } = grapheme.source { + let mut style = renderer.text_style; + if let Some(highlight) = highlight { + style = style.patch(theme.highlight(highlight.0)) + } + (style, Style::default()) + } else { + (syntax_style_span.0, overlay_style_span.0) + }; - let virt = grapheme.is_virtual(); - let grapheme_width = renderer.draw_grapheme( - grapheme.raw, - grapheme_style, - virt, + let is_virtual = grapheme.is_virtual(); + renderer.draw_grapheme( + grapheme.grapheme, + GraphemeStyle { + syntax_style, + overlay_style, + }, + is_virtual, &mut last_line_indent_level, &mut is_in_indent_area, - grapheme.visual_pos, + pos, ); - last_line_end = grapheme.visual_pos.col + grapheme_width; } renderer.draw_indent_guides(last_line_indent_level, last_line_pos.visual_line); - decorations.render_virtual_lines(renderer, last_line_pos, last_line_end) + for line_decoration in &mut *line_decorations { + line_decoration.render_foreground(renderer, last_line_pos, char_pos); + } } #[derive(Debug)] pub struct TextRenderer<'a> { - surface: &'a mut Surface, + pub surface: &'a mut Surface, pub text_style: Style, pub whitespace_style: Style, pub indent_guide_char: String, @@ -188,8 +369,8 @@ pub struct TextRenderer<'a> { pub indent_width: u16, pub starting_indent: usize, pub draw_indent_guides: bool, + pub col_offset: usize, pub viewport: Rect, - pub offset: Position, } pub struct GraphemeStyle { @@ -202,7 +383,7 @@ impl<'a> TextRenderer<'a> { surface: &'a mut Surface, doc: &Document, theme: &Theme, - offset: Position, + col_offset: usize, viewport: Rect, ) -> TextRenderer<'a> { let editor_config = doc.config.load(); @@ -214,7 +395,7 @@ impl<'a> TextRenderer<'a> { let tab_width = doc.tab_width(); let tab = if ws_render.tab() == WhitespaceRenderValue::All { std::iter::once(ws_chars.tab) - .chain(std::iter::repeat_n(ws_chars.tabpad, tab_width - 1)) + .chain(std::iter::repeat(ws_chars.tabpad).take(tab_width - 1)) .collect() } else { " ".repeat(tab_width) @@ -257,8 +438,8 @@ impl<'a> TextRenderer<'a> { virtual_tab, whitespace_style: theme.get("ui.virtual.whitespace"), indent_width, - starting_indent: offset.col / indent_width as usize - + (offset.col % indent_width as usize != 0) as usize + starting_indent: col_offset / indent_width as usize + + (col_offset % indent_width as usize != 0) as usize + editor_config.indent_guides.skip_levels as usize, indent_guide_style: text_style.patch( theme @@ -268,46 +449,8 @@ impl<'a> TextRenderer<'a> { text_style, draw_indent_guides: editor_config.indent_guides.render, viewport, - offset, - } - } - /// Draws a single `grapheme` at the current render position with a specified `style`. - pub fn draw_decoration_grapheme( - &mut self, - grapheme: Grapheme, - mut style: Style, - mut row: u16, - col: u16, - ) -> bool { - if (row as usize) < self.offset.row - || row >= self.viewport.height - || col >= self.viewport.width - { - return false; - } - row -= self.offset.row as u16; - // TODO is it correct to apply the whitspace style to all unicode white spaces? - if grapheme.is_whitespace() { - style = style.patch(self.whitespace_style); + col_offset, } - - let grapheme = match grapheme { - Grapheme::Tab { width } => { - let grapheme_tab_width = char_to_byte_idx(&self.virtual_tab, width); - &self.virtual_tab[..grapheme_tab_width] - } - Grapheme::Other { ref g } if g == "\u{00A0}" => " ", - Grapheme::Other { ref g } => g, - Grapheme::Newline => " ", - }; - - self.surface.set_string( - self.viewport.x + col, - self.viewport.y + row, - grapheme, - style, - ); - true } /// Draws a single `grapheme` at the current render position with a specified `style`. @@ -318,13 +461,9 @@ impl<'a> TextRenderer<'a> { is_virtual: bool, last_indent_level: &mut usize, is_in_indent_area: &mut bool, - mut position: Position, - ) -> usize { - if position.row < self.offset.row { - return 0; - } - position.row -= self.offset.row; - let cut_off_start = self.offset.col.saturating_sub(position.col); + position: Position, + ) { + let cut_off_start = self.col_offset.saturating_sub(position.col); let is_whitespace = grapheme.is_whitespace(); // TODO is it correct to apply the whitespace style to all unicode white spaces? @@ -356,11 +495,12 @@ impl<'a> TextRenderer<'a> { Grapheme::Newline => &self.newline, }; - let in_bounds = self.column_in_bounds(position.col, width); + let in_bounds = self.col_offset <= position.col + && position.col < self.viewport.width as usize + self.col_offset; if in_bounds { self.surface.set_string( - self.viewport.x + (position.col - self.offset.col) as u16, + self.viewport.x + (position.col - self.col_offset) as u16, self.viewport.y + position.row as u16, grapheme, style, @@ -375,37 +515,31 @@ impl<'a> TextRenderer<'a> { ); self.surface.set_style(rect, style); } + if *is_in_indent_area && !is_whitespace { *last_indent_level = position.col; *is_in_indent_area = false; } - - width - } - - pub fn column_in_bounds(&self, colum: usize, width: usize) -> bool { - self.offset.col <= colum && colum + width <= self.offset.col + self.viewport.width as usize } /// Overlay indentation guides ontop of a rendered line /// The indentation level is computed in `draw_lines`. /// Therefore this function must always be called afterwards. - pub fn draw_indent_guides(&mut self, indent_level: usize, mut row: u16) { - if !self.draw_indent_guides || self.offset.row > row as usize { + pub fn draw_indent_guides(&mut self, indent_level: usize, row: u16) { + if !self.draw_indent_guides { return; } - row -= self.offset.row as u16; // Don't draw indent guides outside of view let end_indent = min( indent_level, // Add indent_width - 1 to round up, since the first visible // indent might be a bit after offset.col - self.offset.col + self.viewport.width as usize + (self.indent_width as usize - 1), + self.col_offset + self.viewport.width as usize + (self.indent_width as usize - 1), ) / self.indent_width as usize; for i in self.starting_indent..end_indent { - let x = (self.viewport.x as usize + (i * self.indent_width as usize) - self.offset.col) + let x = (self.viewport.x as usize + (i * self.indent_width as usize) - self.col_offset) as u16; let y = self.viewport.y + row; debug_assert!(self.surface.in_bounds(x, y)); @@ -413,162 +547,4 @@ impl<'a> TextRenderer<'a> { .set_string(x, y, &self.indent_guide_char, self.indent_guide_style); } } - - pub fn set_string(&mut self, x: u16, y: u16, string: impl AsRef<str>, style: Style) { - if (y as usize) < self.offset.row { - return; - } - self.surface - .set_string(x, y + self.viewport.y, string, style) - } - - pub fn set_stringn( - &mut self, - x: u16, - y: u16, - string: impl AsRef<str>, - width: usize, - style: Style, - ) { - if (y as usize) < self.offset.row { - return; - } - self.surface - .set_stringn(x, y + self.viewport.y, string, width, style); - } - - /// Sets the style of an area **within the text viewport* this accounts - /// both for the renderers vertical offset and its viewport - pub fn set_style(&mut self, mut area: Rect, style: Style) { - area = area.clip_top(self.offset.row as u16); - area.y += self.viewport.y; - self.surface.set_style(area, style); - } - - #[allow(clippy::too_many_arguments)] - pub fn set_string_truncated( - &mut self, - x: u16, - y: u16, - string: &str, - width: usize, - style: impl Fn(usize) -> Style, // Map a grapheme's string offset to a style - ellipsis: bool, - truncate_start: bool, - ) -> (u16, u16) { - if (y as usize) < self.offset.row { - return (x, y); - } - self.surface.set_string_truncated( - x, - y + self.viewport.y, - string, - width, - style, - ellipsis, - truncate_start, - ) - } -} - -struct SyntaxHighlighter<'h, 'r, 't> { - inner: Option<Highlighter<'h>>, - text: RopeSlice<'r>, - /// The character index of the next highlight event, or `usize::MAX` if the highlighter is - /// finished. - pos: usize, - theme: &'t Theme, - text_style: Style, - style: Style, -} - -impl<'h, 'r, 't> SyntaxHighlighter<'h, 'r, 't> { - fn new( - inner: Option<Highlighter<'h>>, - text: RopeSlice<'r>, - theme: &'t Theme, - text_style: Style, - ) -> Self { - let mut highlighter = Self { - inner, - text, - pos: 0, - theme, - style: text_style, - text_style, - }; - highlighter.update_pos(); - highlighter - } - - fn update_pos(&mut self) { - self.pos = self - .inner - .as_ref() - .and_then(|highlighter| { - let next_byte_idx = highlighter.next_event_offset(); - (next_byte_idx != u32::MAX).then(|| { - // Move the byte index to the nearest character boundary (rounding up) and - // convert it to a character index. - self.text - .byte_to_char(self.text.ceil_char_boundary(next_byte_idx as usize)) - }) - }) - .unwrap_or(usize::MAX); - } - - fn advance(&mut self) { - let Some(highlighter) = self.inner.as_mut() else { - return; - }; - - let (event, highlights) = highlighter.advance(); - let base = match event { - HighlightEvent::Refresh => self.text_style, - HighlightEvent::Push => self.style, - }; - - self.style = highlights.fold(base, |acc, highlight| { - acc.patch(self.theme.highlight(highlight)) - }); - self.update_pos(); - } -} - -struct OverlayHighlighter<'t> { - inner: syntax::OverlayHighlighter, - pos: usize, - theme: &'t Theme, - style: Style, -} - -impl<'t> OverlayHighlighter<'t> { - fn new(overlays: Vec<OverlayHighlights>, theme: &'t Theme) -> Self { - let inner = syntax::OverlayHighlighter::new(overlays); - let mut highlighter = Self { - inner, - pos: 0, - theme, - style: Style::default(), - }; - highlighter.update_pos(); - highlighter - } - - fn update_pos(&mut self) { - self.pos = self.inner.next_event_offset(); - } - - fn advance(&mut self) { - let (event, highlights) = self.inner.advance(); - let base = match event { - HighlightEvent::Refresh => Style::default(), - HighlightEvent::Push => self.style, - }; - - self.style = highlights.fold(base, |acc, highlight| { - acc.patch(self.theme.highlight(highlight)) - }); - self.update_pos(); - } } |