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