mirror of https://github.com/helix-editor/helix
rework positioning/rendering and enable softwrap/virtual text (#5420)
* rework positioning/rendering, enables softwrap/virtual text This commit is a large rework of the core text positioning and rendering code in helix to remove the assumption that on-screen columns/lines correspond to text columns/lines. A generic `DocFormatter` is introduced that positions graphemes on and is used both for rendering and for movements/scrolling. Both virtual text support (inline, grapheme overlay and multi-line) and a capable softwrap implementation is included. fix picker highlight cleanup doc formatter, use word bondaries for wrapping make visual vertical movement a seperate commnad estimate line gutter width to improve performance cache cursor position cleanup and optimize doc formatter cleanup documentation fix typos Co-authored-by: Daniel Hines <d4hines@gmail.com> update documentation fix panic in last_visual_line funciton improve soft-wrap documentation add extend_visual_line_up/down commands fix non-visual vertical movement streamline virtual text highlighting, add softwrap indicator fix cursor position if softwrap is disabled improve documentation of text_annotations module avoid crashes if view anchor is out of bounds fix: consider horizontal offset when traslation char_idx -> vpos improve default configuration fix: mixed up horizontal and vertical offset reset view position after config reload apply suggestions from review disabled softwrap for very small screens to avoid endless spin fix wrap_indicator setting fix bar cursor disappearring on the EOF character add keybinding for linewise vertical movement fix: inconsistent gutter highlights improve virtual text API make scope idx lookup more ergonomic allow overlapping overlays correctly track char_pos for virtual text adjust configuration deprecate old position fucntions fix infinite loop in highlight lookup fix gutter style fix formatting document max-line-width interaction with softwrap change wrap-indicator example to use empty string fix: rare panic when view is in invalid state (bis) * Apply suggestions from code review Co-authored-by: Michael Davis <mcarsondavis@gmail.com> * improve documentation for positoning functions * simplify tests * fix documentation of Grapheme::width * Apply suggestions from code review Co-authored-by: Michael Davis <mcarsondavis@gmail.com> * add explicit drop invocation * Add explicit MoveFn type alias * add docuntation to Editor::cursor_cache * fix a few typos * explain use of allow(deprecated) * make gj and gk extend in select mode * remove unneded debug and TODO * mark tab_width_at #[inline] * add fast-path to move_vertically_visual in case softwrap is disabled * rename first_line to first_visual_line * simplify duplicate if/else --------- Co-authored-by: Michael Davis <mcarsondavis@gmail.com>pull/5755/head
parent
4eca4b3079
commit
4dcf1fe66b
@ -0,0 +1,384 @@
|
||||
//! The `DocumentFormatter` forms the bridge between the raw document text
|
||||
//! and onscreen positioning. It yields the text graphemes as an iterator
|
||||
//! and traverses (part) of the document text. During that traversal it
|
||||
//! handles grapheme detection, softwrapping and annotations.
|
||||
//! It yields `FormattedGrapheme`s and their corresponding visual coordinates.
|
||||
//!
|
||||
//! As both virtual text and softwrapping can insert additional lines into the document
|
||||
//! it is generally not possible to find the start of the previous visual line.
|
||||
//! Instead the `DocumentFormatter` starts at the last "checkpoint" (usually a linebreak)
|
||||
//! called a "block" and the caller must advance it as needed.
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::fmt::Debug;
|
||||
use std::mem::{replace, take};
|
||||
|
||||
#[cfg(test)]
|
||||
mod test;
|
||||
|
||||
use unicode_segmentation::{Graphemes, UnicodeSegmentation};
|
||||
|
||||
use crate::graphemes::{Grapheme, GraphemeStr};
|
||||
use crate::syntax::Highlight;
|
||||
use crate::text_annotations::TextAnnotations;
|
||||
use crate::{Position, RopeGraphemes, RopeSlice};
|
||||
|
||||
/// TODO make Highlight a u32 to reduce the size of this enum to a single word.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum GraphemeSource {
|
||||
Document {
|
||||
codepoints: u32,
|
||||
},
|
||||
/// Inline virtual text can not be highlighted with a `Highlight` iterator
|
||||
/// because it's not part of the document. Instead the `Highlight`
|
||||
/// is emitted right by the document formatter
|
||||
VirtualText {
|
||||
highlight: Option<Highlight>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FormattedGrapheme<'a> {
|
||||
pub grapheme: Grapheme<'a>,
|
||||
pub source: GraphemeSource,
|
||||
}
|
||||
|
||||
impl<'a> FormattedGrapheme<'a> {
|
||||
pub fn new(
|
||||
g: GraphemeStr<'a>,
|
||||
visual_x: usize,
|
||||
tab_width: u16,
|
||||
source: GraphemeSource,
|
||||
) -> FormattedGrapheme<'a> {
|
||||
FormattedGrapheme {
|
||||
grapheme: Grapheme::new(g, visual_x, tab_width),
|
||||
source,
|
||||
}
|
||||
}
|
||||
/// Returns whether this grapheme is virtual inline text
|
||||
pub fn is_virtual(&self) -> bool {
|
||||
matches!(self.source, GraphemeSource::VirtualText { .. })
|
||||
}
|
||||
|
||||
pub fn placeholder() -> Self {
|
||||
FormattedGrapheme {
|
||||
grapheme: Grapheme::Other { g: " ".into() },
|
||||
source: GraphemeSource::Document { codepoints: 0 },
|
||||
}
|
||||
}
|
||||
|
||||
pub fn doc_chars(&self) -> usize {
|
||||
match self.source {
|
||||
GraphemeSource::Document { codepoints } => codepoints as usize,
|
||||
GraphemeSource::VirtualText { .. } => 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_whitespace(&self) -> bool {
|
||||
self.grapheme.is_whitespace()
|
||||
}
|
||||
|
||||
pub fn width(&self) -> usize {
|
||||
self.grapheme.width()
|
||||
}
|
||||
|
||||
pub fn is_word_boundary(&self) -> bool {
|
||||
self.grapheme.is_word_boundary()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TextFormat {
|
||||
pub soft_wrap: bool,
|
||||
pub tab_width: u16,
|
||||
pub max_wrap: u16,
|
||||
pub max_indent_retain: u16,
|
||||
pub wrap_indicator: Box<str>,
|
||||
pub wrap_indicator_highlight: Option<Highlight>,
|
||||
pub viewport_width: u16,
|
||||
}
|
||||
|
||||
// test implementation is basically only used for testing or when softwrap is always disabled
|
||||
impl Default for TextFormat {
|
||||
fn default() -> Self {
|
||||
TextFormat {
|
||||
soft_wrap: false,
|
||||
tab_width: 4,
|
||||
max_wrap: 3,
|
||||
max_indent_retain: 4,
|
||||
wrap_indicator: Box::from(" "),
|
||||
viewport_width: 17,
|
||||
wrap_indicator_highlight: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DocumentFormatter<'t> {
|
||||
text_fmt: &'t TextFormat,
|
||||
annotations: &'t TextAnnotations,
|
||||
|
||||
/// The visual position at the end of the last yielded word boundary
|
||||
visual_pos: Position,
|
||||
graphemes: RopeGraphemes<'t>,
|
||||
/// The character pos of the `graphemes` iter used for inserting annotations
|
||||
char_pos: usize,
|
||||
/// The line pos of the `graphemes` iter used for inserting annotations
|
||||
line_pos: usize,
|
||||
exhausted: bool,
|
||||
|
||||
/// Line breaks to be reserved for virtual text
|
||||
/// at the next line break
|
||||
virtual_lines: usize,
|
||||
inline_anntoation_graphemes: Option<(Graphemes<'t>, Option<Highlight>)>,
|
||||
|
||||
// softwrap specific
|
||||
/// The indentation of the current line
|
||||
/// Is set to `None` if the indentation level is not yet known
|
||||
/// because no non-whitespace graphemes have been encountered yet
|
||||
indent_level: Option<usize>,
|
||||
/// In case a long word needs to be split a single grapheme might need to be wrapped
|
||||
/// while the rest of the word stays on the same line
|
||||
peeked_grapheme: Option<(FormattedGrapheme<'t>, usize)>,
|
||||
/// A first-in first-out (fifo) buffer for the Graphemes of any given word
|
||||
word_buf: Vec<FormattedGrapheme<'t>>,
|
||||
/// The index of the next grapheme that will be yielded from the `word_buf`
|
||||
word_i: usize,
|
||||
}
|
||||
|
||||
impl<'t> DocumentFormatter<'t> {
|
||||
/// Creates a new formatter at the last block before `char_idx`.
|
||||
/// A block is a chunk which always ends with a linebreak.
|
||||
/// This is usually just a normal line break.
|
||||
/// However very long lines are always wrapped at constant intervals that can be cheaply calculated
|
||||
/// to avoid pathological behaviour.
|
||||
pub fn new_at_prev_checkpoint(
|
||||
text: RopeSlice<'t>,
|
||||
text_fmt: &'t TextFormat,
|
||||
annotations: &'t TextAnnotations,
|
||||
char_idx: usize,
|
||||
) -> (Self, usize) {
|
||||
// TODO divide long lines into blocks to avoid bad performance for long lines
|
||||
let block_line_idx = text.char_to_line(char_idx.min(text.len_chars()));
|
||||
let block_char_idx = text.line_to_char(block_line_idx);
|
||||
annotations.reset_pos(block_char_idx);
|
||||
(
|
||||
DocumentFormatter {
|
||||
text_fmt,
|
||||
annotations,
|
||||
visual_pos: Position { row: 0, col: 0 },
|
||||
graphemes: RopeGraphemes::new(text.slice(block_char_idx..)),
|
||||
char_pos: block_char_idx,
|
||||
exhausted: false,
|
||||
virtual_lines: 0,
|
||||
indent_level: None,
|
||||
peeked_grapheme: None,
|
||||
word_buf: Vec::with_capacity(64),
|
||||
word_i: 0,
|
||||
line_pos: block_line_idx,
|
||||
inline_anntoation_graphemes: None,
|
||||
},
|
||||
block_char_idx,
|
||||
)
|
||||
}
|
||||
|
||||
fn next_inline_annotation_grapheme(&mut self) -> Option<(&'t str, Option<Highlight>)> {
|
||||
loop {
|
||||
if let Some(&mut (ref mut annotation, highlight)) =
|
||||
self.inline_anntoation_graphemes.as_mut()
|
||||
{
|
||||
if let Some(grapheme) = annotation.next() {
|
||||
return Some((grapheme, highlight));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((annotation, highlight)) =
|
||||
self.annotations.next_inline_annotation_at(self.char_pos)
|
||||
{
|
||||
self.inline_anntoation_graphemes = Some((
|
||||
UnicodeSegmentation::graphemes(&*annotation.text, true),
|
||||
highlight,
|
||||
))
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn advance_grapheme(&mut self, col: usize) -> Option<FormattedGrapheme<'t>> {
|
||||
let (grapheme, source) =
|
||||
if let Some((grapheme, highlight)) = self.next_inline_annotation_grapheme() {
|
||||
(grapheme.into(), GraphemeSource::VirtualText { highlight })
|
||||
} else if let Some(grapheme) = self.graphemes.next() {
|
||||
self.virtual_lines += self.annotations.annotation_lines_at(self.char_pos);
|
||||
let codepoints = grapheme.len_chars() as u32;
|
||||
|
||||
let overlay = self.annotations.overlay_at(self.char_pos);
|
||||
let grapheme = match overlay {
|
||||
Some((overlay, _)) => overlay.grapheme.as_str().into(),
|
||||
None => Cow::from(grapheme).into(),
|
||||
};
|
||||
|
||||
self.char_pos += codepoints as usize;
|
||||
(grapheme, GraphemeSource::Document { codepoints })
|
||||
} else {
|
||||
if self.exhausted {
|
||||
return None;
|
||||
}
|
||||
self.exhausted = true;
|
||||
// EOF grapheme is required for rendering
|
||||
// and correct position computations
|
||||
return Some(FormattedGrapheme {
|
||||
grapheme: Grapheme::Other { g: " ".into() },
|
||||
source: GraphemeSource::Document { codepoints: 0 },
|
||||
});
|
||||
};
|
||||
|
||||
let grapheme = FormattedGrapheme::new(grapheme, col, self.text_fmt.tab_width, source);
|
||||
|
||||
Some(grapheme)
|
||||
}
|
||||
|
||||
/// Move a word to the next visual line
|
||||
fn wrap_word(&mut self, virtual_lines_before_word: usize) -> usize {
|
||||
// softwrap this word to the next line
|
||||
let indent_carry_over = if let Some(indent) = self.indent_level {
|
||||
if indent as u16 <= self.text_fmt.max_indent_retain {
|
||||
indent as u16
|
||||
} else {
|
||||
0
|
||||
}
|
||||
} else {
|
||||
// ensure the indent stays 0
|
||||
self.indent_level = Some(0);
|
||||
0
|
||||
};
|
||||
|
||||
self.visual_pos.col = indent_carry_over as usize;
|
||||
self.virtual_lines -= virtual_lines_before_word;
|
||||
self.visual_pos.row += 1 + virtual_lines_before_word;
|
||||
let mut i = 0;
|
||||
let mut word_width = 0;
|
||||
let wrap_indicator = UnicodeSegmentation::graphemes(&*self.text_fmt.wrap_indicator, true)
|
||||
.map(|g| {
|
||||
i += 1;
|
||||
let grapheme = FormattedGrapheme::new(
|
||||
g.into(),
|
||||
self.visual_pos.col + word_width,
|
||||
self.text_fmt.tab_width,
|
||||
GraphemeSource::VirtualText {
|
||||
highlight: self.text_fmt.wrap_indicator_highlight,
|
||||
},
|
||||
);
|
||||
word_width += grapheme.width();
|
||||
grapheme
|
||||
});
|
||||
self.word_buf.splice(0..0, wrap_indicator);
|
||||
|
||||
for grapheme in &mut self.word_buf[i..] {
|
||||
let visual_x = self.visual_pos.col + word_width;
|
||||
grapheme
|
||||
.grapheme
|
||||
.change_position(visual_x, self.text_fmt.tab_width);
|
||||
word_width += grapheme.width();
|
||||
}
|
||||
word_width
|
||||
}
|
||||
|
||||
fn advance_to_next_word(&mut self) {
|
||||
self.word_buf.clear();
|
||||
let mut word_width = 0;
|
||||
let virtual_lines_before_word = self.virtual_lines;
|
||||
let mut virtual_lines_before_grapheme = self.virtual_lines;
|
||||
|
||||
loop {
|
||||
// softwrap word if necessary
|
||||
if word_width + self.visual_pos.col >= self.text_fmt.viewport_width as usize {
|
||||
// wrapping this word would move too much text to the next line
|
||||
// split the word at the line end instead
|
||||
if word_width > self.text_fmt.max_wrap as usize {
|
||||
// Usually we stop accomulating graphemes as soon as softwrapping becomes necessary.
|
||||
// However if the last grapheme is multiple columns wide it might extend beyond the EOL.
|
||||
// The condition below ensures that this grapheme is not cutoff and instead wrapped to the next line
|
||||
if word_width + self.visual_pos.col > self.text_fmt.viewport_width as usize {
|
||||
self.peeked_grapheme = self.word_buf.pop().map(|grapheme| {
|
||||
(grapheme, self.virtual_lines - virtual_lines_before_grapheme)
|
||||
});
|
||||
self.virtual_lines = virtual_lines_before_grapheme;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
word_width = self.wrap_word(virtual_lines_before_word);
|
||||
}
|
||||
|
||||
virtual_lines_before_grapheme = self.virtual_lines;
|
||||
|
||||
let grapheme = if let Some((grapheme, virtual_lines)) = self.peeked_grapheme.take() {
|
||||
self.virtual_lines += virtual_lines;
|
||||
grapheme
|
||||
} else if let Some(grapheme) = self.advance_grapheme(self.visual_pos.col + word_width) {
|
||||
grapheme
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Track indentation
|
||||
if !grapheme.is_whitespace() && self.indent_level.is_none() {
|
||||
self.indent_level = Some(self.visual_pos.col);
|
||||
} else if grapheme.grapheme == Grapheme::Newline {
|
||||
self.indent_level = None;
|
||||
}
|
||||
|
||||
let is_word_boundary = grapheme.is_word_boundary();
|
||||
word_width += grapheme.width();
|
||||
self.word_buf.push(grapheme);
|
||||
|
||||
if is_word_boundary {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// returns the document line pos of the **next** grapheme that will be yielded
|
||||
pub fn line_pos(&self) -> usize {
|
||||
self.line_pos
|
||||
}
|
||||
|
||||
/// returns the visual pos of the **next** grapheme that will be yielded
|
||||
pub fn visual_pos(&self) -> Position {
|
||||
self.visual_pos
|
||||
}
|
||||
}
|
||||
|
||||
impl<'t> Iterator for DocumentFormatter<'t> {
|
||||
type Item = (FormattedGrapheme<'t>, Position);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let grapheme = if self.text_fmt.soft_wrap {
|
||||
if self.word_i >= self.word_buf.len() {
|
||||
self.advance_to_next_word();
|
||||
self.word_i = 0;
|
||||
}
|
||||
let grapheme = replace(
|
||||
self.word_buf.get_mut(self.word_i)?,
|
||||
FormattedGrapheme::placeholder(),
|
||||
);
|
||||
self.word_i += 1;
|
||||
grapheme
|
||||
} else {
|
||||
self.advance_grapheme(self.visual_pos.col)?
|
||||
};
|
||||
|
||||
let pos = self.visual_pos;
|
||||
if grapheme.grapheme == Grapheme::Newline {
|
||||
self.visual_pos.row += 1;
|
||||
self.visual_pos.row += take(&mut self.virtual_lines);
|
||||
self.visual_pos.col = 0;
|
||||
self.line_pos += 1;
|
||||
} else {
|
||||
self.visual_pos.col += grapheme.width();
|
||||
}
|
||||
Some((grapheme, pos))
|
||||
}
|
||||
}
|
@ -0,0 +1,222 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::doc_formatter::{DocumentFormatter, TextFormat};
|
||||
use crate::text_annotations::{InlineAnnotation, Overlay, TextAnnotations};
|
||||
|
||||
impl TextFormat {
|
||||
fn new_test(softwrap: bool) -> Self {
|
||||
TextFormat {
|
||||
soft_wrap: softwrap,
|
||||
tab_width: 2,
|
||||
max_wrap: 3,
|
||||
max_indent_retain: 4,
|
||||
wrap_indicator: ".".into(),
|
||||
wrap_indicator_highlight: None,
|
||||
// use a prime number to allow lining up too often with repeat
|
||||
viewport_width: 17,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'t> DocumentFormatter<'t> {
|
||||
fn collect_to_str(&mut self) -> String {
|
||||
use std::fmt::Write;
|
||||
let mut res = String::new();
|
||||
let viewport_width = self.text_fmt.viewport_width;
|
||||
let mut line = 0;
|
||||
|
||||
for (grapheme, pos) in self {
|
||||
if pos.row != line {
|
||||
line += 1;
|
||||
assert_eq!(pos.row, line);
|
||||
write!(res, "\n{}", ".".repeat(pos.col)).unwrap();
|
||||
assert!(
|
||||
pos.col <= viewport_width as usize,
|
||||
"softwrapped failed {}<={viewport_width}",
|
||||
pos.col
|
||||
);
|
||||
}
|
||||
write!(res, "{}", grapheme.grapheme).unwrap();
|
||||
}
|
||||
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
fn softwrap_text(text: &str) -> String {
|
||||
DocumentFormatter::new_at_prev_checkpoint(
|
||||
text.into(),
|
||||
&TextFormat::new_test(true),
|
||||
&TextAnnotations::default(),
|
||||
0,
|
||||
)
|
||||
.0
|
||||
.collect_to_str()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn basic_softwrap() {
|
||||
assert_eq!(
|
||||
softwrap_text(&"foo ".repeat(10)),
|
||||
"foo foo foo foo \n.foo foo foo foo \n.foo foo "
|
||||
);
|
||||
assert_eq!(
|
||||
softwrap_text(&"fooo ".repeat(10)),
|
||||
"fooo fooo fooo \n.fooo fooo fooo \n.fooo fooo fooo \n.fooo "
|
||||
);
|
||||
|
||||
// check that we don't wrap unnecessarily
|
||||
assert_eq!(softwrap_text("\t\txxxx1xxxx2xx\n"), " xxxx1xxxx2xx \n ");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn softwrap_indentation() {
|
||||
assert_eq!(
|
||||
softwrap_text("\t\tfoo1 foo2 foo3 foo4 foo5 foo6\n"),
|
||||
" foo1 foo2 \n.....foo3 foo4 \n.....foo5 foo6 \n "
|
||||
);
|
||||
assert_eq!(
|
||||
softwrap_text("\t\t\tfoo1 foo2 foo3 foo4 foo5 foo6\n"),
|
||||
" foo1 foo2 \n.foo3 foo4 foo5 \n.foo6 \n "
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn long_word_softwrap() {
|
||||
assert_eq!(
|
||||
softwrap_text("\t\txxxx1xxxx2xxxx3xxxx4xxxx5xxxx6xxxx7xxxx8xxxx9xxx\n"),
|
||||
" xxxx1xxxx2xxx\n.....x3xxxx4xxxx5\n.....xxxx6xxxx7xx\n.....xx8xxxx9xxx \n "
|
||||
);
|
||||
assert_eq!(
|
||||
softwrap_text("xxxxxxxx1xxxx2xxx\n"),
|
||||
"xxxxxxxx1xxxx2xxx\n. \n "
|
||||
);
|
||||
assert_eq!(
|
||||
softwrap_text("\t\txxxx1xxxx 2xxxx3xxxx4xxxx5xxxx6xxxx7xxxx8xxxx9xxx\n"),
|
||||
" xxxx1xxxx \n.....2xxxx3xxxx4x\n.....xxx5xxxx6xxx\n.....x7xxxx8xxxx9\n.....xxx \n "
|
||||
);
|
||||
assert_eq!(
|
||||
softwrap_text("\t\txxxx1xxx 2xxxx3xxxx4xxxx5xxxx6xxxx7xxxx8xxxx9xxx\n"),
|
||||
" xxxx1xxx 2xxx\n.....x3xxxx4xxxx5\n.....xxxx6xxxx7xx\n.....xx8xxxx9xxx \n "
|
||||
);
|
||||
}
|
||||
|
||||
fn overlay_text(text: &str, char_pos: usize, softwrap: bool, overlays: &[Overlay]) -> String {
|
||||
DocumentFormatter::new_at_prev_checkpoint(
|
||||
text.into(),
|
||||
&TextFormat::new_test(softwrap),
|
||||
TextAnnotations::default().add_overlay(overlays.into(), None),
|
||||
char_pos,
|
||||
)
|
||||
.0
|
||||
.collect_to_str()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overlay() {
|
||||
assert_eq!(
|
||||
overlay_text(
|
||||
"foobar",
|
||||
0,
|
||||
false,
|
||||
&[
|
||||
Overlay {
|
||||
char_idx: 0,
|
||||
grapheme: "X".into(),
|
||||
},
|
||||
Overlay {
|
||||
char_idx: 2,
|
||||
grapheme: "\t".into(),
|
||||
},
|
||||
]
|
||||
),
|
||||
"Xo bar "
|
||||
);
|
||||
assert_eq!(
|
||||
overlay_text(
|
||||
&"foo ".repeat(10),
|
||||
0,
|
||||
true,
|
||||
&[
|
||||
Overlay {
|
||||
char_idx: 2,
|
||||
grapheme: "\t".into(),
|
||||
},
|
||||
Overlay {
|
||||
char_idx: 5,
|
||||
grapheme: "\t".into(),
|
||||
},
|
||||
Overlay {
|
||||
char_idx: 16,
|
||||
grapheme: "X".into(),
|
||||
},
|
||||
]
|
||||
),
|
||||
"fo f o foo \n.foo Xoo foo foo \n.foo foo foo "
|
||||
);
|
||||
}
|
||||
|
||||
fn annotate_text(text: &str, softwrap: bool, annotations: &[InlineAnnotation]) -> String {
|
||||
DocumentFormatter::new_at_prev_checkpoint(
|
||||
text.into(),
|
||||
&TextFormat::new_test(softwrap),
|
||||
TextAnnotations::default().add_inline_annotations(annotations.into(), None),
|
||||
0,
|
||||
)
|
||||
.0
|
||||
.collect_to_str()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn annotation() {
|
||||
assert_eq!(
|
||||
annotate_text(
|
||||
"bar",
|
||||
false,
|
||||
&[InlineAnnotation {
|
||||
char_idx: 0,
|
||||
text: "foo".into(),
|
||||
}]
|
||||
),
|
||||
"foobar "
|
||||
);
|
||||
assert_eq!(
|
||||
annotate_text(
|
||||
&"foo ".repeat(10),
|
||||
true,
|
||||
&[InlineAnnotation {
|
||||
char_idx: 0,
|
||||
text: "foo ".into(),
|
||||
}]
|
||||
),
|
||||
"foo foo foo foo \n.foo foo foo foo \n.foo foo foo "
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn annotation_and_overlay() {
|
||||
assert_eq!(
|
||||
DocumentFormatter::new_at_prev_checkpoint(
|
||||
"bbar".into(),
|
||||
&TextFormat::new_test(false),
|
||||
TextAnnotations::default()
|
||||
.add_inline_annotations(
|
||||
Rc::new([InlineAnnotation {
|
||||
char_idx: 0,
|
||||
text: "fooo".into(),
|
||||
}]),
|
||||
None
|
||||
)
|
||||
.add_overlay(
|
||||
Rc::new([Overlay {
|
||||
char_idx: 0,
|
||||
grapheme: "\t".into(),
|
||||
}]),
|
||||
None
|
||||
),
|
||||
0,
|
||||
)
|
||||
.0
|
||||
.collect_to_str(),
|
||||
"fooo bar "
|
||||
);
|
||||
}
|
@ -0,0 +1,271 @@
|
||||
use std::cell::Cell;
|
||||
use std::convert::identity;
|
||||
use std::ops::Range;
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::syntax::Highlight;
|
||||
use crate::Tendril;
|
||||
|
||||
/// An inline annotation is continuous text shown
|
||||
/// on the screen before the grapheme that starts at
|
||||
/// `char_idx`
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct InlineAnnotation {
|
||||
pub text: Tendril,
|
||||
pub char_idx: usize,
|
||||
}
|
||||
|
||||
/// Represents a **single Grapheme** that is part of the document
|
||||
/// that start at `char_idx` that will be replaced with
|
||||
/// a different `grapheme`.
|
||||
/// If `grapheme` contains multiple graphemes the text
|
||||
/// will render incorrectly.
|
||||
/// If you want to overlay multiple graphemes simply
|
||||
/// use multiple `Overlays`.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// The following examples are valid overlays for the following text:
|
||||
///
|
||||
/// `aX͎̊͢͜͝͡bc`
|
||||
///
|
||||
/// ```
|
||||
/// use helix_core::text_annotations::Overlay;
|
||||
///
|
||||
/// // replaces a
|
||||
/// Overlay {
|
||||
/// char_idx: 0,
|
||||
/// grapheme: "X".into(),
|
||||
/// };
|
||||
///
|
||||
/// // replaces X͎̊͢͜͝͡
|
||||
/// Overlay{
|
||||
/// char_idx: 1,
|
||||
/// grapheme: "\t".into(),
|
||||
/// };
|
||||
///
|
||||
/// // replaces b
|
||||
/// Overlay{
|
||||
/// char_idx: 6,
|
||||
/// grapheme: "X̢̢̟͖̲͌̋̇͑͝".into(),
|
||||
/// };
|
||||
/// ```
|
||||
///
|
||||
/// The following examples are invalid uses
|
||||
///
|
||||
/// ```
|
||||
/// use helix_core::text_annotations::Overlay;
|
||||
///
|
||||
/// // overlay is not aligned at grapheme boundary
|
||||
/// Overlay{
|
||||
/// char_idx: 3,
|
||||
/// grapheme: "x".into(),
|
||||
/// };
|
||||
///
|
||||
/// // overlay contains multiple graphemes
|
||||
/// Overlay{
|
||||
/// char_idx: 0,
|
||||
/// grapheme: "xy".into(),
|
||||
/// };
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Overlay {
|
||||
pub char_idx: usize,
|
||||
pub grapheme: Tendril,
|
||||
}
|
||||
|
||||
/// Line annotations allow for virtual text between normal
|
||||
/// text lines. They cause `height` empty lines to be inserted
|
||||
/// below the document line that contains `anchor_char_idx`.
|
||||
///
|
||||
/// These lines can be filled with text in the rendering code
|
||||
/// as their contents have no effect beyond visual appearance.
|
||||
///
|
||||
/// To insert a line after a document line simply set
|
||||
/// `anchor_char_idx` to `doc.line_to_char(line_idx)`
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LineAnnotation {
|
||||
pub anchor_char_idx: usize,
|
||||
pub height: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Layer<A, M> {
|
||||
annotations: Rc<[A]>,
|
||||
current_index: Cell<usize>,
|
||||
metadata: M,
|
||||
}
|
||||
|
||||
impl<A, M: Clone> Clone for Layer<A, M> {
|
||||
fn clone(&self) -> Self {
|
||||
Layer {
|
||||
annotations: self.annotations.clone(),
|
||||
current_index: self.current_index.clone(),
|
||||
metadata: self.metadata.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<A, M> Layer<A, M> {
|
||||
pub fn reset_pos(&self, char_idx: usize, get_char_idx: impl Fn(&A) -> usize) {
|
||||
let new_index = self
|
||||
.annotations
|
||||
.binary_search_by_key(&char_idx, get_char_idx)
|
||||
.unwrap_or_else(identity);
|
||||
|
||||
self.current_index.set(new_index);
|
||||
}
|
||||
|
||||
pub fn consume(&self, char_idx: usize, get_char_idx: impl Fn(&A) -> usize) -> Option<&A> {
|
||||
let annot = self.annotations.get(self.current_index.get())?;
|
||||
debug_assert!(get_char_idx(annot) >= char_idx);
|
||||
if get_char_idx(annot) == char_idx {
|
||||
self.current_index.set(self.current_index.get() + 1);
|
||||
Some(annot)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<A, M> From<(Rc<[A]>, M)> for Layer<A, M> {
|
||||
fn from((annotations, metadata): (Rc<[A]>, M)) -> Layer<A, M> {
|
||||
Layer {
|
||||
annotations,
|
||||
current_index: Cell::new(0),
|
||||
metadata,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn reset_pos<A, M>(layers: &[Layer<A, M>], pos: usize, get_pos: impl Fn(&A) -> usize) {
|
||||
for layer in layers {
|
||||
layer.reset_pos(pos, &get_pos)
|
||||
}
|
||||
}
|
||||
|
||||
/// Annotations that change that is displayed when the document is render.
|
||||
/// Also commonly called virtual text.
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct TextAnnotations {
|
||||
inline_annotations: Vec<Layer<InlineAnnotation, Option<Highlight>>>,
|
||||
overlays: Vec<Layer<Overlay, Option<Highlight>>>,
|
||||
line_annotations: Vec<Layer<LineAnnotation, ()>>,
|
||||
}
|
||||
|
||||
impl TextAnnotations {
|
||||
/// Prepare the TextAnnotations for iteration starting at char_idx
|
||||
pub fn reset_pos(&self, char_idx: usize) {
|
||||
reset_pos(&self.inline_annotations, char_idx, |annot| annot.char_idx);
|
||||
reset_pos(&self.overlays, char_idx, |annot| annot.char_idx);
|
||||
reset_pos(&self.line_annotations, char_idx, |annot| {
|
||||
annot.anchor_char_idx
|
||||
});
|
||||
}
|
||||
|
||||
pub fn collect_overlay_highlights(
|
||||
&self,
|
||||
char_range: Range<usize>,
|
||||
) -> Vec<(usize, Range<usize>)> {
|
||||
let mut highlights = Vec::new();
|
||||
self.reset_pos(char_range.start);
|
||||
for char_idx in char_range {
|
||||
if let Some((_, Some(highlight))) = self.overlay_at(char_idx) {
|
||||
// we don't know the number of chars the original grapheme takes
|
||||
// however it doesn't matter as highlight bounderies are automatically
|
||||
// aligned to grapheme boundaries in the rendering code
|
||||
highlights.push((highlight.0, char_idx..char_idx + 1))
|
||||
}
|
||||
}
|
||||
|
||||
highlights
|
||||
}
|
||||
|
||||
/// Add new inline annotations.
|
||||
///
|
||||
/// The annotations grapheme will be rendered with `highlight`
|
||||
/// patched on top of `ui.text`.
|
||||
///
|
||||
/// The annotations **must be sorted** by their `char_idx`.
|
||||
/// Multiple annotations with the same `char_idx` are allowed,
|
||||
/// they will be display in the order that they are present in the layer.
|
||||
///
|
||||
/// If multiple layers contain annotations at the same position
|
||||
/// the annotations that belong to the layers added first will be shown first.
|
||||
pub fn add_inline_annotations(
|
||||
&mut self,
|
||||
layer: Rc<[InlineAnnotation]>,
|
||||
highlight: Option<Highlight>,
|
||||
) -> &mut Self {
|
||||
self.inline_annotations.push((layer, highlight).into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Add new grapheme overlays.
|
||||
///
|
||||
/// The overlayed grapheme will be rendered with `highlight`
|
||||
/// patched on top of `ui.text`.
|
||||
///
|
||||
/// The overlays **must be sorted** by their `char_idx`.
|
||||
/// Multiple overlays with the same `char_idx` **are allowed**.
|
||||
///
|
||||
/// If multiple layers contain overlay at the same position
|
||||
/// the overlay from the layer added last will be show.
|
||||
pub fn add_overlay(&mut self, layer: Rc<[Overlay]>, highlight: Option<Highlight>) -> &mut Self {
|
||||
self.overlays.push((layer, highlight).into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Add new annotation lines.
|
||||
///
|
||||
/// The line annotations **must be sorted** by their `char_idx`.
|
||||
/// Multiple line annotations with the same `char_idx` **are not allowed**.
|
||||
pub fn add_line_annotation(&mut self, layer: Rc<[LineAnnotation]>) -> &mut Self {
|
||||
self.line_annotations.push((layer, ()).into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Removes all line annotations, useful for vertical motions
|
||||
/// so that virtual text lines are automatically skipped.
|
||||
pub fn clear_line_annotations(&mut self) {
|
||||
self.line_annotations.clear();
|
||||
}
|
||||
|
||||
pub(crate) fn next_inline_annotation_at(
|
||||
&self,
|
||||
char_idx: usize,
|
||||
) -> Option<(&InlineAnnotation, Option<Highlight>)> {
|
||||
self.inline_annotations.iter().find_map(|layer| {
|
||||
let annotation = layer.consume(char_idx, |annot| annot.char_idx)?;
|
||||
Some((annotation, layer.metadata))
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn overlay_at(&self, char_idx: usize) -> Option<(&Overlay, Option<Highlight>)> {
|
||||
let mut overlay = None;
|
||||
for layer in &self.overlays {
|
||||
while let Some(new_overlay) = layer.consume(char_idx, |annot| annot.char_idx) {
|
||||
overlay = Some((new_overlay, layer.metadata));
|
||||
}
|
||||
}
|
||||
overlay
|
||||
}
|
||||
|
||||
pub(crate) fn annotation_lines_at(&self, char_idx: usize) -> usize {
|
||||
self.line_annotations
|
||||
.iter()
|
||||
.map(|layer| {
|
||||
let mut lines = 0;
|
||||
while let Some(annot) = layer.annotations.get(layer.current_index.get()) {
|
||||
if annot.anchor_char_idx == char_idx {
|
||||
layer.current_index.set(layer.current_index.get() + 1);
|
||||
lines += annot.height
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
lines
|
||||
})
|
||||
.sum()
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue