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::Highlight; use helix_core::syntax::HighlightEvent; use helix_core::text_annotations::TextAnnotations; use helix_core::{visual_offset_from_block, Position, RopeSlice, Selection}; use helix_stdx::rope::RopeSliceExt; use helix_view::editor::{ WhitespaceCharacters, WhitespaceConfig, WhitespaceRender, WhitespaceRenderValue, }; use helix_view::graphics::Rect; use helix_view::theme::Style; use helix_view::view::ViewPosition; use helix_view::{Document, Theme}; use tui::buffer::Buffer as Surface; use crate::ui::text_decorations::DecorationManager; #[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> { text_style: Style, active_highlights: Vec, highlight_iter: H, kind: StyleIterKind, text: RopeSlice<'a>, theme: &'a Theme, } impl> 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 { mut end, .. } => { 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 { /// Indicates whether the given visual line /// is the first visual line of the given document line pub first_visual_line: bool, /// The line index of the document line that contains the given visual line pub doc_line: usize, /// Vertical offset from the top of the inner view area pub visual_line: u16, } #[allow(clippy::too_many_arguments)] pub fn render_document( surface: &mut Surface, viewport: Rect, doc: &Document, selection: Option<&Selection>, offset: ViewPosition, doc_annotations: &TextAnnotations, syntax_highlight_iter: impl Iterator, overlay_highlight_iter: impl Iterator, theme: &Theme, decorations: DecorationManager, ) { let mut renderer = TextRenderer::new( surface, doc, theme, Position::new(offset.vertical_offset, offset.horizontal_offset), viewport, ); render_text( &mut renderer, doc.text().slice(..), selection, offset.anchor, &doc.text_format(viewport.width, Some(theme)), doc_annotations, syntax_highlight_iter, overlay_highlight_iter, theme, decorations, ) } #[allow(clippy::too_many_arguments)] pub fn render_text( renderer: &mut TextRenderer, text: RopeSlice<'_>, selection: Option<&Selection>, anchor: usize, text_fmt: &TextFormat, text_annotations: &TextAnnotations, syntax_highlight_iter: impl Iterator, overlay_highlight_iter: impl Iterator, theme: &Theme, mut decorations: DecorationManager, ) { 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_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, }; let mut last_line_end = 0; let mut is_in_indent_area = true; let mut last_line_indent_level = 0; 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)); let mut reached_view_top = false; loop { let Some(mut grapheme) = formatter.next() else { break; }; // skip any graphemes on visual lines before the block start if grapheme.visual_pos.row < row_off { continue; } grapheme.visual_pos.row -= row_off; if !reached_view_top { decorations.prepare_for_rendering(grapheme.char_idx); reached_view_top = true; } // 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 { 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 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) } 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, }; decorations.decorate_line(renderer, last_line_pos); } // acquire the correct grapheme style while grapheme.char_idx >= syntax_style_span.1 { syntax_style_span = syntax_styles .next() .unwrap_or((Style::default(), usize::MAX)); } while grapheme.char_idx >= overlay_style_span.1 { overlay_style_span = overlay_styles .next() .unwrap_or((Style::default(), usize::MAX)); } 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.0)); } GraphemeStyle { syntax_style: style, overlay_style: Style::default(), } } else { GraphemeStyle { syntax_style: syntax_style_span.0, overlay_style: overlay_style_span.0, } }; decorations.decorate_grapheme(renderer, &grapheme); let virt = grapheme.is_virtual(); let is_selected = selection.is_some_and(|selection| { selection .iter() .any(|range| range.contains(grapheme.char_idx)) }); let grapheme_width = renderer.draw_grapheme( grapheme.raw, grapheme_style, virt, is_selected, &mut last_line_indent_level, &mut is_in_indent_area, grapheme.visual_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) } #[derive(Debug)] pub struct TextRenderer<'a> { surface: &'a mut Surface, whitespace_entries: WhitespaceEntries, pub text_style: Style, pub whitespace_style: Style, pub indent_guide_char: String, pub indent_guide_style: Style, pub indent_width: u16, pub starting_indent: usize, pub draw_indent_guides: bool, pub viewport: Rect, pub offset: Position, } pub struct GraphemeStyle { syntax_style: Style, overlay_style: Style, } impl<'a> TextRenderer<'a> { pub fn new( surface: &'a mut Surface, doc: &Document, theme: &Theme, offset: Position, viewport: Rect, ) -> TextRenderer<'a> { let editor_config = doc.config.load(); let WhitespaceConfig { render: ws_render, characters: ws_chars, } = &editor_config.whitespace; let tab_width = doc.tab_width(); let whitespace_entries = WhitespaceEntries::new(ws_render, ws_chars, tab_width); let text_style = theme.get("ui.text"); let indent_width = doc.indent_style.indent_width(tab_width) as u16; TextRenderer { surface, indent_guide_char: editor_config.indent_guides.character.into(), whitespace_entries, 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 + editor_config.indent_guides.skip_levels as usize, indent_guide_style: text_style.patch( theme .try_get("ui.virtual.indent-guide") .unwrap_or_else(|| theme.get("ui.virtual.whitespace")), ), 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); } let virtual_tab = &self.whitespace_entries.tab.render(true, false); let grapheme = match grapheme { Grapheme::Tab { width } => { let grapheme_tab_width = char_to_byte_idx(virtual_tab, width); &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`. pub fn draw_grapheme( &mut self, grapheme: Grapheme, grapheme_style: GraphemeStyle, is_virtual: bool, is_selected: 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); let is_whitespace = grapheme.is_whitespace(); // TODO is it correct to apply the whitespace style to all unicode white spaces? let mut style = grapheme_style.syntax_style; if is_whitespace { style = style.patch(self.whitespace_style); } style = style.patch(grapheme_style.overlay_style); let width = grapheme.width(); let ws = &self.whitespace_entries; let tab = &ws.tab.render(is_virtual, is_selected); let space = &ws.space.render(is_virtual, is_selected); let nbsp = &ws.nbsp.render(is_virtual, is_selected); let nnbsp = &ws.nnbsp.render(is_virtual, is_selected); let newline = &ws.newline.render(false, is_selected); let grapheme = match grapheme { Grapheme::Tab { width } => { let grapheme_tab_width = char_to_byte_idx(tab, width); &tab[..grapheme_tab_width] } // TODO special rendering for other whitespaces? Grapheme::Other { ref g } if g == " " => space, Grapheme::Other { ref g } if g == "\u{00A0}" => nbsp, Grapheme::Other { ref g } if g == "\u{202F}" => nnbsp, Grapheme::Other { ref g } => g, Grapheme::Newline => newline, }; let in_bounds = self.column_in_bounds(position.col + width - 1); if in_bounds { self.surface.set_string( self.viewport.x + (position.col - self.offset.col) as u16, self.viewport.y + position.row as u16, grapheme, style, ); } else if cut_off_start != 0 && cut_off_start < width { // partially on screen let rect = Rect::new( self.viewport.x, self.viewport.y + position.row as u16, (width - cut_off_start) as u16, 1, ); 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) -> bool { self.offset.col <= colum && colum < self.viewport.width as usize + self.offset.col } /// 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 { 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.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) as u16; let y = self.viewport.y + row; debug_assert!(self.surface.in_bounds(x, y)); self.surface .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, 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, 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); } /// Sets the style of an area **within the text viewport* this accounts /// both for the renderers vertical offset and its viewport #[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, ) } } #[derive(Debug)] struct WhitespacePadding { grapheme_width: usize, padding_character: char, } #[derive(Debug)] struct Whitespace { render_value: WhitespaceRenderValue, character: char, padding: Option, } impl Whitespace { fn render_hidden(&self) -> String { let target_width = self.padding.as_ref().map(|p| p.grapheme_width).unwrap_or(1); " ".repeat(target_width) } fn render_visible(&self) -> String { match self.padding { Some(WhitespacePadding { grapheme_width, padding_character, }) => std::iter::once(self.character) .chain(std::iter::repeat(padding_character).take(grapheme_width - 1)) .collect(), None => self.character.to_string(), } } fn is_visible(&self, is_virtual: bool, is_selected: bool) -> bool { if is_virtual { return false; } match self.render_value { WhitespaceRenderValue::All => true, WhitespaceRenderValue::Selection => is_selected, WhitespaceRenderValue::None => false, } } fn render(&self, is_virtual: bool, is_selected: bool) -> String { if self.is_visible(is_virtual, is_selected) { self.render_visible() } else { self.render_hidden() } } } #[derive(Debug)] struct WhitespaceEntries { space: Whitespace, nbsp: Whitespace, nnbsp: Whitespace, tab: Whitespace, newline: Whitespace, } impl WhitespaceEntries { fn new( whitespace_render: &WhitespaceRender, whitespace_characters: &WhitespaceCharacters, tab_width: usize, ) -> Self { WhitespaceEntries { space: Whitespace { render_value: whitespace_render.space(), character: whitespace_characters.space, padding: None, }, nbsp: Whitespace { render_value: whitespace_render.nbsp(), character: whitespace_characters.nbsp, padding: None, }, nnbsp: Whitespace { render_value: whitespace_render.nnbsp(), character: whitespace_characters.nnbsp, padding: None, }, tab: Whitespace { render_value: whitespace_render.tab(), character: whitespace_characters.tab, padding: Some(WhitespacePadding { grapheme_width: tab_width, padding_character: whitespace_characters.tabpad, }), }, newline: Whitespace { render_value: whitespace_render.newline(), character: whitespace_characters.newline, padding: None, }, } } }