mirror of https://github.com/helix-editor/helix
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
552 lines
18 KiB
Rust
552 lines
18 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::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;
|
|
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 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<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 { 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,
|
|
offset: ViewPosition,
|
|
doc_annotations: &TextAnnotations,
|
|
syntax_highlight_iter: impl Iterator<Item = HighlightEvent>,
|
|
overlay_highlight_iter: impl Iterator<Item = HighlightEvent>,
|
|
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_highlight_iter,
|
|
overlay_highlight_iter,
|
|
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_highlight_iter: impl Iterator<Item = HighlightEvent>,
|
|
overlay_highlight_iter: impl Iterator<Item = HighlightEvent>,
|
|
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 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 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 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 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 virtual_tab = " ".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 nnbsp = if ws_render.nnbsp() == WhitespaceRenderValue::All {
|
|
ws_chars.nnbsp.into()
|
|
} else {
|
|
" ".to_owned()
|
|
};
|
|
|
|
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(),
|
|
newline,
|
|
nbsp,
|
|
nnbsp,
|
|
space,
|
|
tab,
|
|
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
|
|
+ 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 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 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 => &self.newline,
|
|
};
|
|
|
|
let in_bounds = self.column_in_bounds(position.col, width);
|
|
|
|
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, 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);
|
|
}
|
|
|
|
/// 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,
|
|
)
|
|
}
|
|
}
|