streamline text decoration API

This commit brings the text decoration API inline with the
LineAnnotation API (so they are consistent) resulting in a single
streamlined API instead of multiple ADHOK callbacks.
pull/6417/head
Pascal Kuthe 1 year ago
parent 9a93240d27
commit 2c0506aa96
No known key found for this signature in database
GPG Key ID: D715E8655AE166A6

@ -290,7 +290,7 @@ impl Application {
self.compositor.render(area, surface, &mut cx); self.compositor.render(area, surface, &mut cx);
let (pos, kind) = self.compositor.cursor(area, &self.editor); let (pos, kind) = self.compositor.cursor(area, &self.editor);
// reset cursor cache // reset cursor cache
self.editor.cursor_cache.set(None); self.editor.cursor_cache.reset();
let pos = pos.map(|pos| (pos.col as u16, pos.row as u16)); let pos = pos.map(|pos| (pos.col as u16, pos.row as u16));
self.terminal.draw(pos, kind).unwrap(); self.terminal.draw(pos, kind).unwrap();

@ -12,26 +12,10 @@ use helix_view::editor::{WhitespaceConfig, WhitespaceRenderValue};
use helix_view::graphics::Rect; use helix_view::graphics::Rect;
use helix_view::theme::Style; use helix_view::theme::Style;
use helix_view::view::ViewPosition; use helix_view::view::ViewPosition;
use helix_view::Document; use helix_view::{Document, Theme};
use helix_view::Theme;
use tui::buffer::Buffer as Surface; use tui::buffer::Buffer as Surface;
pub trait LineDecoration { use crate::ui::text_decorations::DecorationManager;
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)
}
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)] #[derive(Debug, PartialEq, Eq, Clone, Copy)]
enum StyleIterKind { enum StyleIterKind {
@ -95,15 +79,8 @@ pub struct LinePos {
pub doc_line: usize, pub doc_line: usize,
/// Vertical offset from the top of the inner view area /// Vertical offset from the top of the inner view area
pub visual_line: u16, 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)] #[allow(clippy::too_many_arguments)]
pub fn render_document( pub fn render_document(
surface: &mut Surface, surface: &mut Surface,
@ -114,84 +91,46 @@ pub fn render_document(
syntax_highlight_iter: impl Iterator<Item = HighlightEvent>, syntax_highlight_iter: impl Iterator<Item = HighlightEvent>,
overlay_highlight_iter: impl Iterator<Item = HighlightEvent>, overlay_highlight_iter: impl Iterator<Item = HighlightEvent>,
theme: &Theme, theme: &Theme,
line_decoration: &mut [Box<dyn LineDecoration + '_>], decorations: DecorationManager,
translated_positions: &mut [TranslatedPosition],
) { ) {
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( render_text(
&mut renderer, &mut renderer,
doc.text().slice(..), doc.text().slice(..),
offset, offset.anchor,
&doc.text_format(viewport.width, Some(theme)), &doc.text_format(viewport.width, Some(theme)),
doc_annotations, doc_annotations,
syntax_highlight_iter, syntax_highlight_iter,
overlay_highlight_iter, overlay_highlight_iter,
theme, theme,
line_decoration, decorations,
translated_positions,
) )
} }
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)] #[allow(clippy::too_many_arguments)]
pub fn render_text<'t>( pub fn render_text<'t>(
renderer: &mut TextRenderer, renderer: &mut TextRenderer,
text: RopeSlice<'t>, text: RopeSlice<'_>,
offset: ViewPosition, anchor: usize,
text_fmt: &TextFormat, text_fmt: &TextFormat,
text_annotations: &TextAnnotations, text_annotations: &TextAnnotations,
syntax_highlight_iter: impl Iterator<Item = HighlightEvent>, syntax_highlight_iter: impl Iterator<Item = HighlightEvent>,
overlay_highlight_iter: impl Iterator<Item = HighlightEvent>, overlay_highlight_iter: impl Iterator<Item = HighlightEvent>,
theme: &Theme, theme: &Theme,
line_decorations: &mut [Box<dyn LineDecoration + '_>], mut decorations: DecorationManager,
translated_positions: &mut [TranslatedPosition],
) { ) {
let mut row_off = visual_offset_from_block( let row_off = visual_offset_from_block(text, anchor, anchor, text_fmt, text_annotations)
text, .0
offset.anchor, .row;
offset.anchor,
text_fmt,
text_annotations,
)
.0
.row;
row_off += offset.vertical_offset;
let mut formatter = let mut formatter =
DocumentFormatter::new_at_prev_checkpoint(text, text_fmt, text_annotations, offset.anchor); DocumentFormatter::new_at_prev_checkpoint(text, text_fmt, text_annotations, anchor);
let mut syntax_styles = StyleIter { let mut syntax_styles = StyleIter {
text_style: renderer.text_style, text_style: renderer.text_style,
active_highlights: Vec::with_capacity(64), active_highlights: Vec::with_capacity(64),
@ -213,8 +152,8 @@ pub fn render_text<'t>(
first_visual_line: false, first_visual_line: false,
doc_line: usize::MAX, doc_line: usize::MAX,
visual_line: u16::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 is_in_indent_area = true;
let mut last_line_indent_level = 0; let mut last_line_indent_level = 0;
let mut syntax_style_span = syntax_styles let mut syntax_style_span = syntax_styles
@ -223,58 +162,22 @@ pub fn render_text<'t>(
let mut overlay_style_span = overlay_styles let mut overlay_style_span = overlay_styles
.next() .next()
.unwrap_or_else(|| (Style::default(), usize::MAX)); .unwrap_or_else(|| (Style::default(), usize::MAX));
let mut first_visible_char_idx = formatter.next_char_pos(); let mut reached_view_top = false;
loop { loop {
// formattter.line_pos returns to line index of the next grapheme
// so it must be called before formatter.next
let Some(mut grapheme) = formatter.next() else { let Some(mut grapheme) = formatter.next() else {
let mut last_pos = formatter.next_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(
text.len_chars() + 1,
first_visible_char_idx,
translated_positions,
text_fmt,
renderer,
last_pos,
);
}
break; break;
}; };
// skip any graphemes on visual lines before the block start // 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
if grapheme.visual_pos.row < row_off { if grapheme.visual_pos.row < row_off {
if grapheme.char_idx >= style_span.1 {
style_span = if let Some(style_span) = styles.next() {
style_span
} else {
break;
};
overlay_span = if let Some(overlay_span) = overlays.next() {
overlay_span
} else {
break;
};
}
first_visible_char_idx = formatter.next_char_pos();
continue; continue;
} }
grapheme.visual_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 the end of the viewport is reached stop rendering
if grapheme.visual_pos.row as u16 >= renderer.viewport.height + renderer.offset.row as u16 { if grapheme.visual_pos.row as u16 >= renderer.viewport.height + renderer.offset.row as u16 {
@ -283,87 +186,67 @@ pub fn render_text<'t>(
// apply decorations before rendering a new line // apply decorations before rendering a new line
if grapheme.visual_pos.row as u16 != last_line_pos.visual_line { if grapheme.visual_pos.row as u16 != last_line_pos.visual_line {
if grapheme.visual_pos.row > 0 { // 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 // draw indent guides for the last line
renderer renderer.draw_indent_guides(last_line_indent_level, last_line_pos.visual_line);
.draw_indent_guides(last_line_indent_level, last_line_pos.visual_line as u16);
is_in_indent_area = true; is_in_indent_area = true;
for line_decoration in &mut *line_decorations { decorations.render_virtual_lines(renderer, last_line_pos, last_line_end)
line_decoration.render_foreground(renderer, last_line_pos, grapheme.char_idx);
}
} }
last_line_pos = LinePos { last_line_pos = LinePos {
first_visual_line: grapheme.line_idx != last_line_pos.doc_line, first_visual_line: grapheme.line_idx != last_line_pos.doc_line,
doc_line: grapheme.line_idx, doc_line: grapheme.line_idx,
visual_line: grapheme.visual_pos.row as u16, visual_line: grapheme.visual_pos.row as u16,
start_char_idx: grapheme.char_idx,
}; };
for line_decoration in &mut *line_decorations { decorations.decorate_line(renderer, last_line_pos);
line_decoration.render_background(renderer, last_line_pos);
}
} }
// acquire the correct grapheme style // acquire the correct grapheme style
while grapheme.char_idx >= syntax_style_span.1 { while grapheme.char_idx >= syntax_style_span.1 {
syntax_style_span = syntax_styles syntax_style_span = syntax_styles
.next() .next()
.unwrap_or((Style::default(), usize::MAX)); .unwrap_or((Style::default(), usize::MAX));
} }
while grapheme.char_idx >= overlay_style_span.1 { while grapheme.char_idx >= overlay_style_span.1 {
overlay_style_span = overlay_styles overlay_style_span = overlay_styles
.next() .next()
.unwrap_or((Style::default(), usize::MAX)); .unwrap_or((Style::default(), usize::MAX));
} }
// check if any positions translated on the fly (like cursor) has been reached let grapheme_style = if let GraphemeSource::VirtualText { highlight } = grapheme.source {
translate_positions( let mut style = renderer.text_style;
formatter.next_char_pos(), if let Some(highlight) = highlight {
first_visible_char_idx, style = style.patch(theme.highlight(highlight.0));
translated_positions, }
text_fmt,
renderer,
grapheme.visual_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(
<<<<<<< HEAD
grapheme.grapheme,
GraphemeStyle { GraphemeStyle {
syntax_style, syntax_style: style,
overlay_style, overlay_style: Style::default(),
}, }
is_virtual, } else {
||||||| parent of 5e32edd8 (track char_idx in DocFormatter) GraphemeStyle {
grapheme.grapheme, syntax_style: syntax_style_span.0,
grapheme_style, overlay_style: overlay_style_span.0,
virt, }
======= };
decorations.decorate_grapheme(renderer, &grapheme);
let virt = grapheme.is_virtual();
let grapheme_width = renderer.draw_grapheme(
grapheme.raw, grapheme.raw,
grapheme_style, grapheme_style,
virt, virt,
>>>>>>> 5e32edd8 (track char_idx in DocFormatter)
&mut last_line_indent_level, &mut last_line_indent_level,
&mut is_in_indent_area, &mut is_in_indent_area,
grapheme.visual_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); renderer.draw_indent_guides(last_line_indent_level, last_line_pos.visual_line);
for line_decoration in &mut *line_decorations { decorations.render_virtual_lines(renderer, last_line_pos, last_line_end)
line_decoration.render_foreground(renderer, last_line_pos, formatter.next_char_pos());
}
} }
#[derive(Debug)] #[derive(Debug)]
@ -382,8 +265,8 @@ pub struct TextRenderer<'a> {
pub indent_width: u16, pub indent_width: u16,
pub starting_indent: usize, pub starting_indent: usize,
pub draw_indent_guides: bool, pub draw_indent_guides: bool,
pub col_offset: usize,
pub viewport: Rect, pub viewport: Rect,
pub offset: Position,
} }
pub struct GraphemeStyle { pub struct GraphemeStyle {
@ -396,7 +279,7 @@ impl<'a> TextRenderer<'a> {
surface: &'a mut Surface, surface: &'a mut Surface,
doc: &Document, doc: &Document,
theme: &Theme, theme: &Theme,
col_offset: usize, offset: Position,
viewport: Rect, viewport: Rect,
) -> TextRenderer<'a> { ) -> TextRenderer<'a> {
let editor_config = doc.config.load(); let editor_config = doc.config.load();
@ -451,8 +334,8 @@ impl<'a> TextRenderer<'a> {
virtual_tab, virtual_tab,
whitespace_style: theme.get("ui.virtual.whitespace"), whitespace_style: theme.get("ui.virtual.whitespace"),
indent_width, indent_width,
starting_indent: col_offset / indent_width as usize starting_indent: offset.col / indent_width as usize
+ (col_offset % indent_width as usize != 0) as usize + (offset.col % indent_width as usize != 0) as usize
+ editor_config.indent_guides.skip_levels as usize, + editor_config.indent_guides.skip_levels as usize,
indent_guide_style: text_style.patch( indent_guide_style: text_style.patch(
theme theme
@ -462,7 +345,7 @@ impl<'a> TextRenderer<'a> {
text_style, text_style,
draw_indent_guides: editor_config.indent_guides.render, draw_indent_guides: editor_config.indent_guides.render,
viewport, viewport,
col_offset, offset,
} }
} }
@ -474,9 +357,13 @@ impl<'a> TextRenderer<'a> {
is_virtual: bool, is_virtual: bool,
last_indent_level: &mut usize, last_indent_level: &mut usize,
is_in_indent_area: &mut bool, is_in_indent_area: &mut bool,
position: Position, mut position: Position,
) { ) -> usize {
let cut_off_start = self.col_offset.saturating_sub(position.col); 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(); let is_whitespace = grapheme.is_whitespace();
// TODO is it correct to apply the whitespace style to all unicode white spaces? // TODO is it correct to apply the whitespace style to all unicode white spaces?
@ -508,12 +395,11 @@ impl<'a> TextRenderer<'a> {
Grapheme::Newline => &self.newline, Grapheme::Newline => &self.newline,
}; };
let in_bounds = self.col_offset <= position.col let in_bounds = self.column_in_bounds(position.col + width - 1);
&& position.col < self.viewport.width as usize + self.col_offset;
if in_bounds { if in_bounds {
self.surface.set_string( 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, self.viewport.y + position.row as u16,
grapheme, grapheme,
style, style,
@ -533,26 +419,33 @@ impl<'a> TextRenderer<'a> {
*last_indent_level = position.col; *last_indent_level = position.col;
*is_in_indent_area = false; *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 /// Overlay indentation guides ontop of a rendered line
/// The indentation level is computed in `draw_lines`. /// The indentation level is computed in `draw_lines`.
/// Therefore this function must always be called afterwards. /// Therefore this function must always be called afterwards.
pub fn draw_indent_guides(&mut self, indent_level: usize, row: u16) { pub fn draw_indent_guides(&mut self, indent_level: usize, mut row: u16) {
if !self.draw_indent_guides { if !self.draw_indent_guides || self.offset.row > row as usize {
return; return;
} }
row -= self.offset.row as u16;
// Don't draw indent guides outside of view // Don't draw indent guides outside of view
let end_indent = min( let end_indent = min(
indent_level, indent_level,
// Add indent_width - 1 to round up, since the first visible // Add indent_width - 1 to round up, since the first visible
// indent might be a bit after offset.col // 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; ) / self.indent_width as usize;
for i in self.starting_indent..end_indent { 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; as u16;
let y = self.viewport.y + row; let y = self.viewport.y + row;
debug_assert!(self.surface.in_bounds(x, y)); debug_assert!(self.surface.in_bounds(x, y));
@ -560,4 +453,62 @@ impl<'a> TextRenderer<'a> {
.set_string(x, y, &self.indent_guide_char, self.indent_guide_style); .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,
)
}
} }

@ -5,8 +5,10 @@ use crate::{
key, key,
keymap::{KeymapResult, Keymaps}, keymap::{KeymapResult, Keymaps},
ui::{ ui::{
document::{render_document, LinePos, TextRenderer, TranslatedPosition}, document::{render_document, LinePos, TextRenderer},
Completion, ProgressSpinners, statusline,
text_decorations::{self, Decoration, DecorationManager, InlineDiagnostics},
Completion, CompletionItem, ProgressSpinners,
}, },
}; };
@ -31,9 +33,6 @@ use std::{mem::take, num::NonZeroUsize, path::PathBuf, rc::Rc, sync::Arc};
use tui::{buffer::Buffer as Surface, text::Span}; use tui::{buffer::Buffer as Surface, text::Span};
use super::document::LineDecoration;
use super::{completion::CompletionItem, statusline};
pub struct EditorView { pub struct EditorView {
pub keymaps: Keymaps, pub keymaps: Keymaps,
on_next_key: Option<OnKeyCallback>, on_next_key: Option<OnKeyCallback>,
@ -94,11 +93,10 @@ impl EditorView {
let config = editor.config(); let config = editor.config();
let text_annotations = view.text_annotations(doc, Some(theme)); let text_annotations = view.text_annotations(doc, Some(theme));
let mut line_decorations: Vec<Box<dyn LineDecoration>> = Vec::new(); let mut decorations = DecorationManager::default();
let mut translated_positions: Vec<TranslatedPosition> = Vec::new();
if is_focused && config.cursorline { if is_focused && config.cursorline {
line_decorations.push(Self::cursorline_decorator(doc, view, theme)) decorations.add_decoration(Self::cursorline(doc, view, theme));
} }
if is_focused && config.cursorcolumn { if is_focused && config.cursorcolumn {
@ -113,13 +111,10 @@ impl EditorView {
if pos.doc_line != dap_line { if pos.doc_line != dap_line {
return; return;
} }
renderer.surface.set_style( renderer.set_style(Rect::new(inner.x, pos.visual_line, inner.width, 1), style);
Rect::new(inner.x, inner.y + pos.visual_line, inner.width, 1),
style,
);
}; };
line_decorations.push(Box::new(line_decoration)); decorations.add_decoration(line_decoration);
} }
let syntax_highlights = let syntax_highlights =
@ -176,22 +171,20 @@ impl EditorView {
view.area, view.area,
theme, theme,
is_focused & self.terminal_focused, is_focused & self.terminal_focused,
&mut line_decorations, &mut decorations,
); );
} }
let primary_cursor = doc
.selection(view.id)
.primary()
.cursor(doc.text().slice(..));
if is_focused { if is_focused {
let cursor = doc decorations.add_decoration(text_decorations::Cursor {
.selection(view.id) cache: &editor.cursor_cache,
.primary() primary_cursor,
.cursor(doc.text().slice(..)); });
// set the cursor_cache to out of view in case the position is not found
editor.cursor_cache.set(Some(None));
let update_cursor_cache =
|_: &mut TextRenderer, pos| editor.cursor_cache.set(Some(Some(pos)));
translated_positions.push((cursor, Box::new(update_cursor_cache)));
} }
render_document( render_document(
surface, surface,
inner, inner,
@ -201,8 +194,7 @@ impl EditorView {
syntax_highlights, syntax_highlights,
overlay_highlights, overlay_highlights,
theme, theme,
&mut line_decorations, decorations,
&mut translated_positions,
); );
Self::render_rulers(editor, doc, view, inner, surface, theme); Self::render_rulers(editor, doc, view, inner, surface, theme);
@ -637,7 +629,7 @@ impl EditorView {
viewport: Rect, viewport: Rect,
theme: &Theme, theme: &Theme,
is_focused: bool, is_focused: bool,
line_decorations: &mut Vec<Box<(dyn LineDecoration + 'd)>>, decoration_manager: &mut DecorationManager<'d>,
) { ) {
let text = doc.text().slice(..); let text = doc.text().slice(..);
let cursors: Rc<[_]> = doc let cursors: Rc<[_]> = doc
@ -663,7 +655,7 @@ impl EditorView {
// TODO handle softwrap in gutters // TODO handle softwrap in gutters
let selected = cursors.contains(&pos.doc_line); let selected = cursors.contains(&pos.doc_line);
let x = viewport.x + offset; let x = viewport.x + offset;
let y = viewport.y + pos.visual_line; let y = pos.visual_line;
let gutter_style = match (selected, pos.first_visual_line) { let gutter_style = match (selected, pos.first_visual_line) {
(false, true) => gutter_style, (false, true) => gutter_style,
@ -675,11 +667,9 @@ impl EditorView {
if let Some(style) = if let Some(style) =
gutter(pos.doc_line, selected, pos.first_visual_line, &mut text) gutter(pos.doc_line, selected, pos.first_visual_line, &mut text)
{ {
renderer renderer.set_stringn(x, y, &text, width, gutter_style.patch(style));
.surface
.set_stringn(x, y, &text, width, gutter_style.patch(style));
} else { } else {
renderer.surface.set_style( renderer.set_style(
Rect { Rect {
x, x,
y, y,
@ -691,7 +681,7 @@ impl EditorView {
} }
text.clear(); text.clear();
}; };
line_decorations.push(Box::new(gutter_decoration)); decoration_manager.add_decoration(gutter_decoration);
offset += width as u16; offset += width as u16;
} }
@ -761,11 +751,7 @@ impl EditorView {
} }
/// Apply the highlighting on the lines where a cursor is active /// Apply the highlighting on the lines where a cursor is active
pub fn cursorline_decorator( pub fn cursorline(doc: &Document, view: &View, theme: &Theme) -> impl Decoration {
doc: &Document,
view: &View,
theme: &Theme,
) -> Box<dyn LineDecoration> {
let text = doc.text().slice(..); let text = doc.text().slice(..);
// TODO only highlight the visual line that contains the cursor instead of the full visual line // TODO only highlight the visual line that contains the cursor instead of the full visual line
let primary_line = doc.selection(view.id).primary().cursor_line(text); let primary_line = doc.selection(view.id).primary().cursor_line(text);
@ -786,16 +772,14 @@ impl EditorView {
let secondary_style = theme.get("ui.cursorline.secondary"); let secondary_style = theme.get("ui.cursorline.secondary");
let viewport = view.area; let viewport = view.area;
let line_decoration = move |renderer: &mut TextRenderer, pos: LinePos| { move |renderer: &mut TextRenderer, pos: LinePos| {
let area = Rect::new(viewport.x, viewport.y + pos.visual_line, viewport.width, 1); let area = Rect::new(viewport.x, pos.visual_line, viewport.width, 1);
if primary_line == pos.doc_line { if primary_line == pos.doc_line {
renderer.surface.set_style(area, primary_style); renderer.set_style(area, primary_style);
} else if secondary_lines.binary_search(&pos.doc_line).is_ok() { } else if secondary_lines.binary_search(&pos.doc_line).is_ok() {
renderer.surface.set_style(area, secondary_style); renderer.set_style(area, secondary_style);
} }
}; }
Box::new(line_decoration)
} }
/// Apply the highlighting on the columns where a cursor is active /// Apply the highlighting on the columns where a cursor is active

@ -12,6 +12,7 @@ mod prompt;
mod spinner; mod spinner;
mod statusline; mod statusline;
mod text; mod text;
mod text_decorations;
use crate::compositor::Compositor; use crate::compositor::Compositor;
use crate::filter_picker_entry; use crate::filter_picker_entry;

@ -7,8 +7,9 @@ use crate::{
ctrl, key, shift, ctrl, key, shift,
ui::{ ui::{
self, self,
document::{render_document, LineDecoration, LinePos, TextRenderer}, document::{render_document, LinePos, TextRenderer},
picker::query::PickerQuery, picker::query::PickerQuery,
text_decorations::DecorationManager,
EditorView, EditorView,
}, },
}; };
@ -895,7 +896,7 @@ impl<T: 'static + Send + Sync, D: 'static + Send + Sync> Picker<T, D> {
} }
overlay_highlights = Box::new(helix_core::syntax::merge(overlay_highlights, spans)); overlay_highlights = Box::new(helix_core::syntax::merge(overlay_highlights, spans));
} }
let mut decorations: Vec<Box<dyn LineDecoration>> = Vec::new(); let mut decorations = DecorationManager::default();
if let Some((start, end)) = range { if let Some((start, end)) = range {
let style = cx let style = cx
@ -907,14 +908,14 @@ impl<T: 'static + Send + Sync, D: 'static + Send + Sync> Picker<T, D> {
if (start..=end).contains(&pos.doc_line) { if (start..=end).contains(&pos.doc_line) {
let area = Rect::new( let area = Rect::new(
renderer.viewport.x, renderer.viewport.x,
renderer.viewport.y + pos.visual_line, pos.visual_line,
renderer.viewport.width, renderer.viewport.width,
1, 1,
); );
renderer.surface.set_style(area, style) renderer.set_style(area, style)
} }
}; };
decorations.push(Box::new(draw_highlight)) decorations.add_decoration(draw_highlight);
} }
render_document( render_document(
@ -927,8 +928,7 @@ impl<T: 'static + Send + Sync, D: 'static + Send + Sync> Picker<T, D> {
syntax_highlights, syntax_highlights,
overlay_highlights, overlay_highlights,
&cx.editor.theme, &cx.editor.theme,
&mut decorations, decorations,
&mut [],
); );
} }
} }

@ -0,0 +1,175 @@
use std::cmp::Ordering;
use helix_core::doc_formatter::FormattedGrapheme;
use helix_core::Position;
use helix_view::editor::CursorCache;
use crate::ui::document::{LinePos, TextRenderer};
pub use diagnostics::InlineDiagnostics;
mod diagnostics;
/// Decorations are the primary mechanism for extending the text rendering.
///
/// Any on-screen element which is anchored to the rendered text in some form
/// should be implemented using this trait. Translating char positions to
/// on-screen positions can be expensive and should not be done manually in the
/// ui loop. Instead such translations are automatically performed on the fly
/// while the text is being rendered. The results are provided to this trait by
/// the rendering infrastructure.
///
/// To reserve space for virtual text lines (which is then filled by this trait) emit appropriate
/// [`LineAnnotation`](helix_core::text_annotations::LineAnnotation)s in [`helix_view::View::text_annotations`]
pub trait Decoration {
/// Called **before** a **visual** line is rendered. A visual line does not
/// necessarily correspond to a single line in a document as soft wrapping can
/// spread a single document line across multiple visual lines.
///
/// This function is called before text is rendered as any decorations should
/// never overlap the document text. That means that setting the forground color
/// here is (essentially) useless as the text color is overwritten by the
/// rendered text. This _of course_ doesn't apply when rendering inside virtual lines
/// below the line reserved by `LineAnnotation`s as no text will be rendered here.
fn decorate_line(&mut self, _renderer: &mut TextRenderer, _pos: LinePos) {}
/// Called **after** a **visual** line is rendered. A visual line does not
/// necessarily correspond to a single line in a document as soft wrapping can
/// spread a single document line across multiple visual lines.
///
/// This function is called after text is rendered so that decorations can collect
/// horizontal positions on the line (see [`Decoration::decorate_grapheme`]) first and
/// use those positions` while rendering
/// virtual text.
/// That means that setting the forground color
/// here is (essentially) useless as the text color is overwritten by the
/// rendered text. This -ofcourse- doesn't apply when rendering inside virtual lines
/// below the line reserved by `LineAnnotation`s. e as no text will be rendered here.
/// **Note**: To avoid overlapping decorations in the virtual lines, each decoration
/// must return the number of virtual text lines it has taken up. Each `Decoration` recieves
/// an offset `virt_off` based on these return values where it can render virtual text:
///
/// That means that a `render_line` implementation that returns `X` can render virtual text
/// in the following area:
/// ``` no-compile
/// let start = inner.y + pos.virtual_line + virt_off;
/// start .. start + X
/// ````
fn render_virt_lines(
&mut self,
_renderer: &mut TextRenderer,
_pos: LinePos,
_virt_off: Position,
) -> Position {
Position::new(0, 0)
}
fn reset_pos(&mut self, _pos: usize) -> usize {
usize::MAX
}
fn skip_concealed_anchor(&mut self, conceal_end_char_idx: usize) -> usize {
self.reset_pos(conceal_end_char_idx)
}
/// This function is called **before** the grapheme at `char_idx` is rendered.
///
/// # Returns
///
/// The char idx of the next grapheme that this function should be called for
fn decorate_grapheme(
&mut self,
_renderer: &mut TextRenderer,
_grapheme: &FormattedGrapheme,
) -> usize {
usize::MAX
}
}
impl<F: FnMut(&mut TextRenderer, LinePos)> Decoration for F {
fn decorate_line(&mut self, renderer: &mut TextRenderer, pos: LinePos) {
self(renderer, pos);
}
}
#[derive(Default)]
pub struct DecorationManager<'a> {
decorations: Vec<(Box<dyn Decoration + 'a>, usize)>,
}
impl<'a> DecorationManager<'a> {
pub fn add_decoration(&mut self, decoration: impl Decoration + 'a) {
self.decorations.push((Box::new(decoration), 0));
}
pub fn prepare_for_rendering(&mut self, first_visible_char: usize) {
for (decoration, next_position) in &mut self.decorations {
*next_position = decoration.reset_pos(first_visible_char)
}
}
pub fn decorate_grapheme(&mut self, renderer: &mut TextRenderer, grapheme: &FormattedGrapheme) {
for (decoration, hook_char_idx) in &mut self.decorations {
loop {
match (*hook_char_idx).cmp(&grapheme.char_idx) {
// this grapheme has been concealed or we are at the first grapheme
Ordering::Less => {
*hook_char_idx = decoration.skip_concealed_anchor(grapheme.char_idx)
}
Ordering::Equal => {
*hook_char_idx = decoration.decorate_grapheme(renderer, grapheme)
}
Ordering::Greater => break,
}
}
}
}
pub fn decorate_line(&mut self, renderer: &mut TextRenderer, pos: LinePos) {
for (decoration, _) in &mut self.decorations {
decoration.decorate_line(renderer, pos);
}
}
pub fn render_virtual_lines(
&mut self,
renderer: &mut TextRenderer,
pos: LinePos,
line_width: usize,
) {
let mut virt_off = Position::new(1, line_width); // start at 1 so the line is never overwritten
for (decoration, _) in &mut self.decorations {
virt_off += decoration.render_virt_lines(renderer, pos, virt_off);
}
}
}
/// Cursor rendering is done externally so all the cursor decoration
/// does is save the position of primary cursor
pub struct Cursor<'a> {
pub cache: &'a CursorCache,
pub primary_cursor: usize,
}
impl Decoration for Cursor<'_> {
fn reset_pos(&mut self, pos: usize) -> usize {
if pos <= self.primary_cursor {
self.primary_cursor
} else {
usize::MAX
}
}
fn decorate_grapheme(
&mut self,
renderer: &mut TextRenderer,
grapheme: &FormattedGrapheme,
) -> usize {
if renderer.column_in_bounds(grapheme.visual_pos.col)
&& renderer.offset.row < grapheme.visual_pos.row
{
let position = grapheme.visual_pos - renderer.offset;
self.cache.set(Some(position));
}
usize::MAX
}
}

@ -1070,10 +1070,10 @@ pub struct Editor {
/// This cache is only a performance optimization to /// This cache is only a performance optimization to
/// avoid calculating the cursor position multiple /// avoid calculating the cursor position multiple
/// times during rendering and should not be set by other functions. /// times during rendering and should not be set by other functions.
pub cursor_cache: Cell<Option<Option<Position>>>,
pub handlers: Handlers, pub handlers: Handlers,
pub mouse_down_range: Option<Range>, pub mouse_down_range: Option<Range>,
pub cursor_cache: CursorCache,
} }
pub type Motion = Box<dyn Fn(&mut Editor)>; pub type Motion = Box<dyn Fn(&mut Editor)>;
@ -1188,9 +1188,9 @@ impl Editor {
exit_code: 0, exit_code: 0,
config_events: unbounded_channel(), config_events: unbounded_channel(),
needs_redraw: false, needs_redraw: false,
cursor_cache: Cell::new(None),
handlers, handlers,
mouse_down_range: None, mouse_down_range: None,
cursor_cache: CursorCache::default(),
} }
} }
@ -1985,15 +1985,7 @@ impl Editor {
pub fn cursor(&self) -> (Option<Position>, CursorKind) { pub fn cursor(&self) -> (Option<Position>, CursorKind) {
let config = self.config(); let config = self.config();
let (view, doc) = current_ref!(self); let (view, doc) = current_ref!(self);
let cursor = doc if let Some(mut pos) = self.cursor_cache.get(view, doc) {
.selection(view.id)
.primary()
.cursor(doc.text().slice(..));
let pos = self
.cursor_cache
.get()
.unwrap_or_else(|| view.screen_coords_at_pos(doc, doc.text().slice(..), cursor));
if let Some(mut pos) = pos {
let inner = view.inner_area(doc); let inner = view.inner_area(doc);
pos.col += inner.x as usize; pos.col += inner.x as usize;
pos.row += inner.y as usize; pos.row += inner.y as usize;
@ -2188,3 +2180,28 @@ fn try_restore_indent(doc: &mut Document, view: &mut View) {
doc.apply(&transaction, view.id); doc.apply(&transaction, view.id);
} }
} }
#[derive(Default)]
pub struct CursorCache(Cell<Option<Option<Position>>>);
impl CursorCache {
pub fn get(&self, view: &View, doc: &Document) -> Option<Position> {
if let Some(pos) = self.0.get() {
return pos;
}
let text = doc.text().slice(..);
let cursor = doc.selection(view.id).primary().cursor(text);
let res = view.screen_coords_at_pos(doc, text, cursor);
self.set(res);
res
}
pub fn set(&self, cursor_pos: Option<Position>) {
self.0.set(Some(cursor_pos))
}
pub fn reset(&self) {
self.0.set(None)
}
}

Loading…
Cancel
Save