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.rs382
1 files changed, 192 insertions, 190 deletions
diff --git a/helix-term/src/ui/document.rs b/helix-term/src/ui/document.rs
index bcbaa351..79145ba0 100644
--- a/helix-term/src/ui/document.rs
+++ b/helix-term/src/ui/document.rs
@@ -12,26 +12,10 @@ 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 helix_view::{Document, 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)
- }
-}
+use crate::ui::text_decorations::DecorationManager;
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
enum StyleIterKind {
@@ -68,10 +52,7 @@ impl<H: Iterator<Item = HighlightEvent>> Iterator for StyleIter<'_, H> {
HighlightEvent::HighlightEnd => {
self.active_highlights.pop();
}
- HighlightEvent::Source { start, mut end } => {
- if start == end {
- continue;
- }
+ HighlightEvent::Source { mut end, .. } => {
let style = self
.active_highlights
.iter()
@@ -98,15 +79,8 @@ 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,
@@ -117,87 +91,46 @@ pub fn render_document(
syntax_highlight_iter: impl Iterator<Item = HighlightEvent>,
overlay_highlight_iter: impl Iterator<Item = HighlightEvent>,
theme: &Theme,
- line_decoration: &mut [Box<dyn LineDecoration + '_>],
- translated_positions: &mut [TranslatedPosition],
+ decorations: DecorationManager,
) {
- let mut renderer = TextRenderer::new(surface, doc, theme, offset.horizontal_offset, viewport);
+ let mut renderer = TextRenderer::new(
+ surface,
+ doc,
+ theme,
+ Position::new(offset.vertical_offset, offset.horizontal_offset),
+ viewport,
+ );
render_text(
&mut renderer,
doc.text().slice(..),
- offset,
+ offset.anchor,
&doc.text_format(viewport.width, Some(theme)),
doc_annotations,
syntax_highlight_iter,
overlay_highlight_iter,
theme,
- line_decoration,
- translated_positions,
+ decorations,
)
}
-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<'t>(
+pub fn render_text(
renderer: &mut TextRenderer,
- text: RopeSlice<'t>,
- offset: ViewPosition,
+ text: RopeSlice<'_>,
+ anchor: usize,
text_fmt: &TextFormat,
text_annotations: &TextAnnotations,
syntax_highlight_iter: impl Iterator<Item = HighlightEvent>,
overlay_highlight_iter: impl Iterator<Item = HighlightEvent>,
theme: &Theme,
- line_decorations: &mut [Box<dyn LineDecoration + '_>],
- translated_positions: &mut [TranslatedPosition],
+ mut decorations: DecorationManager,
) {
- 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 row_off = visual_offset_from_block(text, anchor, anchor, text_fmt, text_annotations)
+ .0
+ .row;
- let (mut formatter, mut first_visible_char_idx) =
- DocumentFormatter::new_at_prev_checkpoint(text, text_fmt, text_annotations, offset.anchor);
+ 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),
@@ -219,8 +152,8 @@ pub fn render_text<'t>(
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 syntax_style_span = syntax_styles
@@ -229,133 +162,96 @@ pub fn render_text<'t>(
let mut overlay_style_span = overlay_styles
.next()
.unwrap_or_else(|| (Style::default(), usize::MAX));
+ let mut reached_view_top = false;
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();
- 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,
- );
- }
+ let Some(mut grapheme) = formatter.next() else {
break;
};
// skip any graphemes on visual lines before the block start
- 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;
+ if grapheme.visual_pos.row < row_off {
continue;
}
- pos.row -= row_off;
+ 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 pos.row as u16 >= renderer.viewport.height {
+ if grapheme.visual_pos.row as u16 >= renderer.viewport.height + renderer.offset.row as u16 {
break;
}
// apply decorations before rendering a new line
- if pos.row as u16 != last_line_pos.visual_line {
- if pos.row > 0 {
+ 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;
- for line_decoration in &mut *line_decorations {
- line_decoration.render_foreground(renderer, last_line_pos, char_pos);
- }
+ decorations.render_virtual_lines(renderer, last_line_pos, last_line_end)
}
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,
+ first_visual_line: grapheme.line_idx != last_line_pos.doc_line,
+ doc_line: grapheme.line_idx,
+ visual_line: grapheme.visual_pos.row as u16,
};
- for line_decoration in &mut *line_decorations {
- line_decoration.render_background(renderer, last_line_pos);
- }
+ decorations.decorate_line(renderer, last_line_pos);
}
// acquire the correct grapheme style
- if char_pos >= syntax_style_span.1 {
+ while grapheme.char_idx >= syntax_style_span.1 {
syntax_style_span = syntax_styles
.next()
.unwrap_or((Style::default(), usize::MAX));
}
- if char_pos >= overlay_style_span.1 {
+ while grapheme.char_idx >= 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 (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 is_virtual = grapheme.is_virtual();
- renderer.draw_grapheme(
- grapheme.grapheme,
+ 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,
- overlay_style,
- },
- is_virtual,
+ syntax_style: syntax_style_span.0,
+ overlay_style: overlay_style_span.0,
+ }
+ };
+ decorations.decorate_grapheme(renderer, &grapheme);
+
+ let virt = grapheme.is_virtual();
+ let grapheme_width = renderer.draw_grapheme(
+ grapheme.raw,
+ grapheme_style,
+ virt,
&mut last_line_indent_level,
&mut is_in_indent_area,
- pos,
+ 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);
- for line_decoration in &mut *line_decorations {
- line_decoration.render_foreground(renderer, last_line_pos, char_pos);
- }
+ decorations.render_virtual_lines(renderer, last_line_pos, last_line_end)
}
#[derive(Debug)]
pub struct TextRenderer<'a> {
- pub surface: &'a mut Surface,
+ surface: &'a mut Surface,
pub text_style: Style,
pub whitespace_style: Style,
pub indent_guide_char: String,
@@ -369,8 +265,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 {
@@ -383,7 +279,7 @@ impl<'a> TextRenderer<'a> {
surface: &'a mut Surface,
doc: &Document,
theme: &Theme,
- col_offset: usize,
+ offset: Position,
viewport: Rect,
) -> TextRenderer<'a> {
let editor_config = doc.config.load();
@@ -438,8 +334,8 @@ impl<'a> TextRenderer<'a> {
virtual_tab,
whitespace_style: theme.get("ui.virtual.whitespace"),
indent_width,
- starting_indent: col_offset / indent_width as usize
- + (col_offset % indent_width as usize != 0) as usize
+ 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
@@ -449,8 +345,46 @@ impl<'a> TextRenderer<'a> {
text_style,
draw_indent_guides: editor_config.indent_guides.render,
viewport,
- col_offset,
+ 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 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`.
@@ -461,9 +395,13 @@ impl<'a> TextRenderer<'a> {
is_virtual: bool,
last_indent_level: &mut usize,
is_in_indent_area: &mut bool,
- position: Position,
- ) {
- let cut_off_start = self.col_offset.saturating_sub(position.col);
+ 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?
@@ -495,12 +433,11 @@ impl<'a> TextRenderer<'a> {
Grapheme::Newline => &self.newline,
};
- let in_bounds = self.col_offset <= position.col
- && position.col < self.viewport.width as usize + self.col_offset;
+ let in_bounds = self.column_in_bounds(position.col + width - 1);
if in_bounds {
self.surface.set_string(
- self.viewport.x + (position.col - self.col_offset) as u16,
+ self.viewport.x + (position.col - self.offset.col) as u16,
self.viewport.y + position.row as u16,
grapheme,
style,
@@ -520,26 +457,33 @@ impl<'a> TextRenderer<'a> {
*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, row: u16) {
- if !self.draw_indent_guides {
+ 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.col_offset + self.viewport.width as usize + (self.indent_width as usize - 1),
+ 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.col_offset)
+ 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));
@@ -547,4 +491,62 @@ 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);
+ }
+
+ /// 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,
+ )
+ }
}