helix/helix-term/src/ui/document.rs

584 lines
19 KiB
Rust

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::text_annotations::TextAnnotations;
use helix_core::{visual_offset_from_block, Position, RopeSlice};
use helix_stdx::rope::RopeSliceExt;
use helix_view::editor::WhitespaceFeature;
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 super::trailing_whitespace::{TrailingWhitespaceTracker, WhitespaceKind};
use crate::ui::text_decorations::DecorationManager;
#[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,
offset: ViewPosition,
doc_annotations: &TextAnnotations,
syntax_highlighter: Option<Highlighter<'_>>,
overlay_highlights: Vec<syntax::OverlayHighlights>,
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(..),
offset.anchor,
&doc.text_format(viewport.width, Some(theme)),
doc_annotations,
syntax_highlighter,
overlay_highlights,
theme,
decorations,
)
}
#[allow(clippy::too_many_arguments)]
pub fn render_text(
renderer: &mut TextRenderer,
text: RopeSlice<'_>,
anchor: usize,
text_fmt: &TextFormat,
text_annotations: &TextAnnotations,
syntax_highlighter: Option<Highlighter<'_>>,
overlay_highlights: Vec<syntax::OverlayHighlights>,
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_highlighter =
SyntaxHighlighter::new(syntax_highlighter, text, theme, renderer.text_style);
let mut overlay_highlighter = OverlayHighlighter::new(overlay_highlights, theme);
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 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_highlighter.pos {
syntax_highlighter.advance();
}
while grapheme.char_idx >= overlay_highlighter.pos {
overlay_highlighter.advance();
}
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 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,
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,
pub text_style: Style,
pub whitespace_style: Style,
pub trailing_whitespace_style: Style,
pub indent_guide_char: String,
pub indent_guide_style: Style,
pub newline: String,
pub nbsp: String,
pub nnbsp: String,
pub space: String,
pub tab: String,
pub virtual_tab: String,
pub indent_width: u16,
pub starting_indent: usize,
pub draw_indent_guides: bool,
pub viewport: Rect,
pub offset: Position,
pub trailing_whitespace_tracker: TrailingWhitespaceTracker,
}
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 tab_width = doc.tab_width();
let text_style = theme.get("ui.text");
let indent_width = doc.indent_style.indent_width(tab_width) as u16;
let ws = &editor_config.whitespace;
let regular_ws = WhitespaceFeature::Regular.palette(ws, tab_width);
let trailing_ws = WhitespaceFeature::Trailing.palette(ws, tab_width);
let trailing_whitespace_tracker = TrailingWhitespaceTracker::new(ws.render, trailing_ws);
TextRenderer {
surface,
indent_guide_char: editor_config.indent_guides.character.into(),
newline: regular_ws.newline,
nbsp: regular_ws.nbsp,
nnbsp: regular_ws.nnbsp,
space: regular_ws.space,
tab: regular_ws.tab,
virtual_tab: regular_ws.virtual_tab,
whitespace_style: theme.get("ui.virtual.whitespace"),
trailing_whitespace_style: theme.get("ui.virtual.trailing_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,
trailing_whitespace_tracker,
}
}
/// 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`.
pub fn draw_grapheme(
&mut self,
grapheme: Grapheme,
grapheme_style: GraphemeStyle,
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);
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 space = if is_virtual { " " } else { &self.space };
let nbsp = if is_virtual { " " } else { &self.nbsp };
let nnbsp = if is_virtual { " " } else { &self.nnbsp };
let tab = if is_virtual {
&self.virtual_tab
} else {
&self.tab
};
let mut whitespace_kind = WhitespaceKind::None;
let grapheme = match grapheme {
Grapheme::Tab { width } => {
whitespace_kind = WhitespaceKind::Tab;
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 == " " => {
whitespace_kind = WhitespaceKind::Space;
space
}
Grapheme::Other { ref g } if g == "\u{00A0}" => {
whitespace_kind = WhitespaceKind::NonBreakingSpace;
nbsp
}
Grapheme::Other { ref g } if g == "\u{202F}" => {
whitespace_kind = WhitespaceKind::NarrowNonBreakingSpace;
nnbsp
}
Grapheme::Other { ref g } => g,
Grapheme::Newline => {
whitespace_kind = WhitespaceKind::Newline;
&self.newline
}
};
let viewport_right_edge = self.viewport.width as usize + self.offset.col - 1;
let in_bounds = self.column_in_bounds(position.col, width);
if in_bounds {
let in_bounds_col = position.col - self.offset.col;
self.surface.set_string(
self.viewport.x + in_bounds_col as u16,
self.viewport.y + position.row as u16,
grapheme,
style,
);
if self
.trailing_whitespace_tracker
.track(in_bounds_col, whitespace_kind)
|| position.col == viewport_right_edge
{
self.trailing_whitespace_tracker.render(
&mut |trailing_whitespace: &str, from: usize| {
self.surface.set_string(
self.viewport.x + from as u16,
self.viewport.y + position.row as u16,
trailing_whitespace,
style.patch(self.trailing_whitespace_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, 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 {
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<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();
}
}