summaryrefslogtreecommitdiff
path: root/helix-term/src/ui/document.rs
diff options
context:
space:
mode:
Diffstat (limited to 'helix-term/src/ui/document.rs')
-rw-r--r--helix-term/src/ui/document.rs475
1 files changed, 475 insertions, 0 deletions
diff --git a/helix-term/src/ui/document.rs b/helix-term/src/ui/document.rs
new file mode 100644
index 00000000..66332410
--- /dev/null
+++ b/helix-term/src/ui/document.rs
@@ -0,0 +1,475 @@
+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};
+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;
+use helix_view::Theme;
+use tui::buffer::Buffer as Surface;
+
+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)
+ }
+}
+
+/// 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.
+struct StyleIter<'a, H: Iterator<Item = HighlightEvent>> {
+ text_style: Style,
+ active_highlights: Vec<Highlight>,
+ highlight_iter: H,
+ 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, end } => {
+ if start == end {
+ continue;
+ }
+ let style = self
+ .active_highlights
+ .iter()
+ .fold(self.text_style, |acc, span| {
+ acc.patch(self.theme.highlight(span.0))
+ });
+ 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,
+ /// 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,
+ viewport: Rect,
+ doc: &Document,
+ offset: ViewPosition,
+ doc_annotations: &TextAnnotations,
+ highlight_iter: impl Iterator<Item = HighlightEvent>,
+ theme: &Theme,
+ line_decoration: &mut [Box<dyn LineDecoration + '_>],
+ translated_positions: &mut [TranslatedPosition],
+) {
+ let mut renderer = TextRenderer::new(surface, doc, theme, offset.horizontal_offset, viewport);
+ render_text(
+ &mut renderer,
+ doc.text().slice(..),
+ offset,
+ &doc.text_format(viewport.width, Some(theme)),
+ doc_annotations,
+ highlight_iter,
+ theme,
+ line_decoration,
+ translated_positions,
+ )
+}
+
+fn translate_positions(
+ char_pos: usize,
+ first_visisble_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_visisble_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<'t>(
+ renderer: &mut TextRenderer,
+ text: RopeSlice<'t>,
+ offset: ViewPosition,
+ text_fmt: &TextFormat,
+ text_annotations: &TextAnnotations,
+ highlight_iter: impl Iterator<Item = HighlightEvent>,
+ theme: &Theme,
+ line_decorations: &mut [Box<dyn LineDecoration + '_>],
+ translated_positions: &mut [TranslatedPosition],
+) {
+ 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;
+ assert_eq!(0, 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 styles = StyleIter {
+ text_style: renderer.text_style,
+ active_highlights: Vec::with_capacity(64),
+ highlight_iter,
+ theme,
+ };
+
+ 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 is_in_indent_area = true;
+ let mut last_line_indent_level = 0;
+ let mut style_span = styles
+ .next()
+ .unwrap_or_else(|| (Style::default(), usize::MAX));
+
+ loop {
+ // 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();
+ // TODO refactor with let .. else once MSRV reaches 1.65
+ let (grapheme, mut pos) = if let Some(it) = formatter.next() {
+ it
+ } else {
+ let mut last_pos = formatter.visual_pos();
+ last_pos.col -= 1;
+ // 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 pos.row < row_off {
+ if char_pos >= style_span.1 {
+ // TODO refactor using let..else once MSRV reaches 1.65
+ style_span = if let Some(style_span) = styles.next() {
+ style_span
+ } else {
+ break;
+ }
+ }
+ char_pos += grapheme.doc_chars();
+ first_visible_char_idx = char_pos + 1;
+ continue;
+ }
+ pos.row -= row_off;
+
+ // if the end of the viewport is reached stop rendering
+ if pos.row as u16 >= renderer.viewport.height {
+ break;
+ }
+
+ // apply decorations before rendering a new 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;
+ for line_decoration in &mut *line_decorations {
+ line_decoration.render_foreground(renderer, last_line_pos, char_pos);
+ }
+ }
+ last_line_pos = LinePos {
+ first_visual_line: doc_line != last_line_pos.doc_line,
+ doc_line,
+ visual_line: pos.row as u16,
+ start_char_idx: char_pos,
+ };
+ for line_decoration in &mut *line_decorations {
+ line_decoration.render_background(renderer, last_line_pos);
+ }
+ }
+
+ // aquire the correct grapheme style
+ if char_pos >= style_span.1 {
+ // TODO refactor using let..else once MSRV reaches 1.65
+ style_span = if let Some(style_span) = styles.next() {
+ style_span
+ } else {
+ (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 style = renderer.text_style;
+ if let Some(highlight) = highlight {
+ style.patch(theme.highlight(highlight.0))
+ } else {
+ style
+ }
+ } else {
+ style_span.0
+ };
+
+ renderer.draw_grapheme(
+ grapheme.grapheme,
+ grapheme_style,
+ &mut last_line_indent_level,
+ &mut is_in_indent_area,
+ pos,
+ );
+ }
+
+ renderer.draw_indent_guides(last_line_indent_level, last_line_pos.visual_line);
+ for line_decoration in &mut *line_decorations {
+ line_decoration.render_foreground(renderer, last_line_pos, char_pos);
+ }
+}
+
+#[derive(Debug)]
+pub struct TextRenderer<'a> {
+ pub surface: &'a mut Surface,
+ pub text_style: Style,
+ pub whitespace_style: Style,
+ pub indent_guide_char: String,
+ pub indent_guide_style: Style,
+ pub newline: String,
+ pub nbsp: String,
+ pub space: String,
+ pub tab: String,
+ pub tab_width: u16,
+ pub starting_indent: usize,
+ pub draw_indent_guides: bool,
+ pub col_offset: usize,
+ pub viewport: Rect,
+}
+
+impl<'a> TextRenderer<'a> {
+ pub fn new(
+ surface: &'a mut Surface,
+ doc: &Document,
+ theme: &Theme,
+ col_offset: usize,
+ 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 tab = if ws_render.tab() == WhitespaceRenderValue::All {
+ std::iter::once(ws_chars.tab)
+ .chain(std::iter::repeat(ws_chars.tabpad).take(tab_width - 1))
+ .collect()
+ } else {
+ " ".repeat(tab_width)
+ };
+ let newline = if ws_render.newline() == WhitespaceRenderValue::All {
+ ws_chars.newline.into()
+ } else {
+ " ".to_owned()
+ };
+
+ let space = if ws_render.space() == WhitespaceRenderValue::All {
+ ws_chars.space.into()
+ } else {
+ " ".to_owned()
+ };
+ let nbsp = if ws_render.nbsp() == WhitespaceRenderValue::All {
+ ws_chars.nbsp.into()
+ } else {
+ " ".to_owned()
+ };
+
+ let text_style = theme.get("ui.text");
+
+ TextRenderer {
+ surface,
+ indent_guide_char: editor_config.indent_guides.character.into(),
+ newline,
+ nbsp,
+ space,
+ tab_width: tab_width as u16,
+ tab,
+ whitespace_style: theme.get("ui.virtual.whitespace"),
+ starting_indent: (col_offset / tab_width)
+ + 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,
+ col_offset,
+ }
+ }
+
+ /// Draws a single `grapheme` at the current render position with a specified `style`.
+ pub fn draw_grapheme(
+ &mut self,
+ grapheme: Grapheme,
+ mut style: Style,
+ last_indent_level: &mut usize,
+ is_in_indent_area: &mut bool,
+ position: Position,
+ ) {
+ let cut_off_start = self.col_offset.saturating_sub(position.col as usize);
+ let is_whitespace = grapheme.is_whitespace();
+
+ // TODO is it correct to apply the whitspace style to all unicode white spaces?
+ if is_whitespace {
+ style = style.patch(self.whitespace_style);
+ }
+
+ let width = grapheme.width();
+ let grapheme = match grapheme {
+ Grapheme::Tab { width } => {
+ let grapheme_tab_width = char_to_byte_idx(&self.tab, width as usize);
+ &self.tab[..grapheme_tab_width]
+ }
+ // TODO special rendering for other whitespaces?
+ Grapheme::Other { ref g } if g == " " => &self.space,
+ Grapheme::Other { ref g } if g == "\u{00A0}" => &self.nbsp,
+ Grapheme::Other { ref g } => &*g,
+ Grapheme::Newline => &self.newline,
+ };
+
+ let in_bounds = self.col_offset <= (position.col as usize)
+ && (position.col as usize) < self.viewport.width as usize + self.col_offset;
+
+ if in_bounds {
+ self.surface.set_string(
+ self.viewport.x + (position.col - self.col_offset) as u16,
+ self.viewport.y + position.row as u16,
+ grapheme,
+ style,
+ );
+ } else if cut_off_start != 0 && cut_off_start < width as usize {
+ // partially on screen
+ let rect = Rect::new(
+ self.viewport.x as u16,
+ 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;
+ }
+ }
+
+ /// 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, row: u16) {
+ if !self.draw_indent_guides {
+ return;
+ }
+
+ // Don't draw indent guides outside of view
+ let end_indent = min(
+ indent_level,
+ // Add tab_width - 1 to round up, since the first visible
+ // indent might be a bit after offset.col
+ self.col_offset + self.viewport.width as usize + (self.tab_width - 1) as usize,
+ ) / self.tab_width as usize;
+
+ for i in self.starting_indent..end_indent {
+ let x =
+ (self.viewport.x as usize + (i * self.tab_width as usize) - self.col_offset) 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);
+ }
+ }
+}