Skip to content

Commit facda8d

Browse files
pascalkutheAlexanderDickie
authored andcommitted
Improve line annotation API
The line annotation as implemented in helix-editor#5420 had two shortcomings: * It required the height of virtual text lines to be known ahead time * It checked for line anchors at every grapheme The first problem made the API impractical to use in practice because almost all virtual text needs to be softwrapped. For example inline diagnostics should be softwrapped to avoid cutting off the diagnostic message (as no scrolling is possible). While more complex virtual text like side by side diffs must dynamically calculate the number of empty lines two align two documents (which requires taking account both softwrap and virtual text). To address this, the API has been refactored to use a trait. The second issue caused some performance overhead and unnecessarily complicated the `DocumentFormatter`. It was addressed by only calling the trait mentioned above at line breaks (instead of always). This allows offers additional flexibility to annotations as it offers the flexibility to align lines (needed for side by side diffs).
1 parent 81ac3a5 commit facda8d

File tree

2 files changed

+213
-55
lines changed

2 files changed

+213
-55
lines changed

helix-core/src/doc_formatter.rs

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
1212
use std::borrow::Cow;
1313
use std::fmt::Debug;
14-
use std::mem::{replace, take};
14+
use std::mem::replace;
1515

1616
#[cfg(test)]
1717
mod test;
@@ -166,9 +166,6 @@ pub struct DocumentFormatter<'t> {
166166
line_pos: usize,
167167
exhausted: bool,
168168

169-
/// Line breaks to be reserved for virtual text
170-
/// at the next line break
171-
virtual_lines: usize,
172169
inline_anntoation_graphemes: Option<(Graphemes<'t>, Option<Highlight>)>,
173170

174171
// softwrap specific
@@ -209,7 +206,6 @@ impl<'t> DocumentFormatter<'t> {
209206
graphemes: RopeGraphemes::new(text.slice(block_char_idx..)),
210207
char_pos: block_char_idx,
211208
exhausted: false,
212-
virtual_lines: 0,
213209
indent_level: None,
214210
peeked_grapheme: None,
215211
word_buf: Vec::with_capacity(64),
@@ -250,7 +246,6 @@ impl<'t> DocumentFormatter<'t> {
250246
if let Some((grapheme, highlight)) = self.next_inline_annotation_grapheme(char_pos) {
251247
(grapheme.into(), GraphemeSource::VirtualText { highlight })
252248
} else if let Some(grapheme) = self.graphemes.next() {
253-
self.virtual_lines += self.annotations.annotation_lines_at(self.char_pos);
254249
let codepoints = grapheme.len_chars() as u32;
255250

256251
let overlay = self.annotations.overlay_at(char_pos);
@@ -293,8 +288,11 @@ impl<'t> DocumentFormatter<'t> {
293288
0
294289
};
295290

291+
let virtual_lines =
292+
self.annotations
293+
.virtual_lines_at(self.char_pos, self.visual_pos, self.line_pos);
296294
self.visual_pos.col = indent_carry_over as usize;
297-
self.visual_pos.row += 1 + take(&mut self.virtual_lines);
295+
self.visual_pos.row += 1 + virtual_lines;
298296
let mut i = 0;
299297
let mut word_width = 0;
300298
let wrap_indicator = UnicodeSegmentation::graphemes(&*self.text_fmt.wrap_indicator, true)
@@ -404,24 +402,32 @@ impl<'t> Iterator for DocumentFormatter<'t> {
404402
self.advance_grapheme(self.visual_pos.col, self.char_pos)?
405403
};
406404

407-
let visual_pos = self.visual_pos;
408-
let char_pos = self.char_pos;
405+
let grapheme = FormattedGrapheme {
406+
raw: grapheme.grapheme,
407+
source: grapheme.source,
408+
visual_pos: self.visual_pos,
409+
line_idx: self.line_pos,
410+
char_idx: self.char_pos,
411+
};
412+
409413
self.char_pos += grapheme.doc_chars();
410-
let line_idx = self.line_pos;
411-
if grapheme.grapheme == Grapheme::Newline {
412-
self.visual_pos.row += 1;
413-
self.visual_pos.row += take(&mut self.virtual_lines);
414+
if !grapheme.is_virtual() {
415+
self.annotations.process_virtual_text_anchors(&grapheme);
416+
}
417+
if grapheme.raw == Grapheme::Newline {
418+
// move to end of newline char
419+
self.visual_pos.col += 1;
420+
let virtual_lines =
421+
self.annotations
422+
.virtual_lines_at(self.char_pos, self.visual_pos, self.line_pos);
423+
self.visual_pos.row += 1 + virtual_lines;
414424
self.visual_pos.col = 0;
415-
self.line_pos += 1;
425+
if !grapheme.is_virtual() {
426+
self.line_pos += 1;
427+
}
416428
} else {
417429
self.visual_pos.col += grapheme.width();
418430
}
419-
Some(FormattedGrapheme {
420-
raw: grapheme.grapheme,
421-
source: grapheme.source,
422-
visual_pos,
423-
line_idx,
424-
char_idx: char_pos,
425-
})
431+
Some(grapheme)
426432
}
427433
}

helix-core/src/text_annotations.rs

Lines changed: 186 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
use std::cell::Cell;
2+
use std::cmp::Ordering;
3+
use std::fmt::Debug;
24
use std::ops::Range;
5+
use std::ptr::NonNull;
36

7+
use crate::doc_formatter::FormattedGrapheme;
48
use crate::syntax::Highlight;
5-
use crate::Tendril;
9+
use crate::{Position, Tendril};
610

711
/// An inline annotation is continuous text shown
812
/// on the screen before the grapheme that starts at
@@ -75,19 +79,98 @@ impl Overlay {
7579
}
7680
}
7781

78-
/// Line annotations allow for virtual text between normal
79-
/// text lines. They cause `height` empty lines to be inserted
80-
/// below the document line that contains `anchor_char_idx`.
82+
/// Line annotations allow inserting virtual text lines between normal text
83+
/// lines. These lines can be filled with text in the rendering code as their
84+
/// contents have no effect beyond visual appearance.
8185
///
82-
/// These lines can be filled with text in the rendering code
83-
/// as their contents have no effect beyond visual appearance.
86+
/// The height of virtual text is usually not known ahead of time as virtual
87+
/// text often requires softwrapping. Furthermore the height of some virtual
88+
/// text like side-by-side diffs depends on the height of the text (again
89+
/// influenced by softwrap) and other virtual text. Therefore line annotations
90+
/// are computed on the fly instead of ahead of time like other annotations.
8491
///
85-
/// To insert a line after a document line simply set
86-
/// `anchor_char_idx` to `doc.line_to_char(line_idx)`
87-
#[derive(Debug, Clone)]
88-
pub struct LineAnnotation {
89-
pub anchor_char_idx: usize,
90-
pub height: usize,
92+
/// The core of this trait `insert_virtual_lines` function. It is called at the
93+
/// end of every visual line and allows the `LineAnnotation` to insert empty
94+
/// virtual lines. Apart from that the `LineAnnotation` trait has multiple
95+
/// methods that allow it to track anchors in the document.
96+
///
97+
/// When a new traversal of a document starts `reset_pos` is called. Afterwards
98+
/// the other functions are called with indices that are larger then the
99+
/// one passed to `reset_pos`. This allows performing a binary search (use
100+
/// `partition_point`) in `reset_pos` once and then to only look at the next
101+
/// anchor during each method call.
102+
///
103+
/// The `reset_pos`, `skip_conceal` and `process_anchor` functions all return a
104+
/// `char_idx` anchor. This anchor is stored when transversing the document and
105+
/// when the grapheme at the anchor is traversed the `process_anchor` function
106+
/// is called.
107+
///
108+
/// # Note
109+
///
110+
/// All functions only receive immutable references to `self`.
111+
/// `LineAnnotation`s that want to store an internal position or
112+
/// state of some kind should use `Cell`. Using interior mutability for
113+
/// caches is preferable as otherwise a lot of lifetimes become invariant
114+
/// which complicates APIs a lot.
115+
pub trait LineAnnotation {
116+
/// Resets the internal position to `char_idx`. This function is called
117+
//// when a new transveral of a document starts.
118+
///
119+
/// All `char_idx` passed to `insert_virtual_lines` are strictly monotonically increasing
120+
/// with the first `char_idx` greater or equal to the `char_idx`
121+
/// passed to this function.
122+
///
123+
/// # Returns
124+
///
125+
/// The `char_idx` of the next anchor this `LineAnnotation` is interested in,
126+
/// replaces the currently registered anchor. Return `usize::MAX` to ignore
127+
fn reset_pos(&mut self, _char_idx: usize) -> usize {
128+
usize::MAX
129+
}
130+
131+
/// Called when a text is concealed that contains an anchor registered by this `LineAnnotation`
132+
///. In this case the line decorations **must** ensure that virtual text anchored within that
133+
/// char range is skipped.
134+
///
135+
/// # Returns
136+
///
137+
/// The `char_idx` of the next anchor this `LineAnnotation` is interested in,
138+
/// **after the end of conceal_end_char_idx**
139+
/// replaces the currently registered anchor. Return `usize::MAX` to ignore
140+
fn skip_concealed_anchors(&mut self, conceal_end_char_idx: usize) -> usize {
141+
self.reset_pos(conceal_end_char_idx)
142+
}
143+
144+
/// Process an anchor (horizontal position is provided) and returns the next anchor.
145+
///
146+
/// # Returns
147+
///
148+
/// The `char_idx` of the next anchor this `LineAnnotation` is interested in,
149+
/// replaces the currently registered anchor. Return `usize::MAX` to ignore
150+
fn process_anchor(&mut self, _grapheme: &FormattedGrapheme) -> usize {
151+
usize::MAX
152+
}
153+
154+
/// This function is called at the end of a visual line to insert virtual text
155+
///
156+
/// # Returns
157+
///
158+
/// The number of additional virtual lines to reserve
159+
///
160+
/// # Note
161+
///
162+
/// The `line_end_visual_pos` paramater indiciates the visual vertical distance
163+
/// from the start of block where the transversal start. This includes the offset
164+
/// from other `LineAnnotations`. This allows inline annotations to consider
165+
/// the height of the text and "align" two different documents (like for side
166+
/// by side diffs). These annotations that want to "align" two documents should
167+
/// therefore be added last so that other virtual text is also considered while aligning
168+
fn insert_virtual_lines(
169+
&mut self,
170+
line_end_char_idx: usize,
171+
line_end_visual_pos: Position,
172+
doc_line: usize,
173+
) -> Position;
91174
}
92175

93176
#[derive(Debug)]
@@ -143,23 +226,77 @@ fn reset_pos<A, M>(layers: &[Layer<A, M>], pos: usize, get_pos: impl Fn(&A) -> u
143226
}
144227
}
145228

229+
/// Safety: We store LineAnnotation in a NonNull pointer. This is necessary to work
230+
/// around an unfortunate inconsistency in rusts variance system that unnnecesarily
231+
/// makes the lifetime invariant if implemented with safe code. This makes the
232+
/// DocFormatter API very cumbersome/basically impossible to work with.
233+
///
234+
/// Normally object types dyn Foo + 'a so if we used Box<dyn LineAnnotation + 'a> below
235+
/// everything would be alright. However we want to use `Cell<Box<dyn LineAnnotation + 'a>>`
236+
/// to be able to call the mutable function on `LineAnnotation`. The problem is that
237+
/// some types like `Cell` make all their arguments invariant. This is important for soundness
238+
/// normally for the same reasons that &'a mut T is invariant over T
239+
/// (see <https://doc.rust-lang.org/nomicon/subtyping.html>). However for &'a mut (dyn Foo + 'b)
240+
/// there is a specical rule in the language to make 'b covariant (otherwise trait objects would be
241+
/// super annoying to use). See <https://users.rust-lang.org/t/solved-variance-of-dyn-trait-a> for
242+
/// why this is sound. Sadly that rule doesn't apply to Cell<Box<(dyn Foo + 'a)>
243+
/// (or other invariant types like `UnsafeCell` or `*mut (dyn Foo + 'a)`).
244+
///
245+
/// We sidestep the problem by using `NonNull` which is covariant. In the specical case
246+
/// of trait objects this is sound (easily checked by adding a `PhantomData<&'a mut Foo + 'a)>` field).
247+
/// We don't need an explicit Cell type here because we never hand out any refereces
248+
/// to the trait objects so even without guarding mutable access to the pointer data behind a ``
249+
/// are covariant over 'a (or in other words it's a raw pointer, as long as we don't hand out
250+
/// references we are free to do whatever we want).
251+
struct RawBox<T: ?Sized>(NonNull<T>);
252+
253+
impl<T: ?Sized> RawBox<T> {
254+
/// Safety: Only a single mutable reference
255+
/// created by this function may exist at a given time.
256+
#[allow(clippy::mut_from_ref)]
257+
unsafe fn get(&self) -> &mut T {
258+
&mut *self.0.as_ptr()
259+
}
260+
}
261+
impl<T: ?Sized> From<Box<T>> for RawBox<T> {
262+
fn from(box_: Box<T>) -> Self {
263+
// obviously safe because Box::into_raw never returns null
264+
unsafe { Self(NonNull::new_unchecked(Box::into_raw(box_))) }
265+
}
266+
}
267+
268+
impl<T: ?Sized> Drop for RawBox<T> {
269+
fn drop(&mut self) {
270+
unsafe { drop(Box::from_raw(self.0.as_ptr())) }
271+
}
272+
}
273+
146274
/// Annotations that change that is displayed when the document is render.
147275
/// Also commonly called virtual text.
148-
#[derive(Default, Debug, Clone)]
276+
#[derive(Default)]
149277
pub struct TextAnnotations<'a> {
150278
inline_annotations: Vec<Layer<'a, InlineAnnotation, Option<Highlight>>>,
151279
overlays: Vec<Layer<'a, Overlay, Option<Highlight>>>,
152-
line_annotations: Vec<Layer<'a, LineAnnotation, ()>>,
280+
line_annotations: Vec<(Cell<usize>, RawBox<dyn LineAnnotation + 'a>)>,
281+
}
282+
283+
impl Debug for TextAnnotations<'_> {
284+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
285+
f.debug_struct("TextAnnotations")
286+
.field("inline_annotations", &self.inline_annotations)
287+
.field("overlays", &self.overlays)
288+
.finish_non_exhaustive()
289+
}
153290
}
154291

155292
impl<'a> TextAnnotations<'a> {
156293
/// Prepare the TextAnnotations for iteration starting at char_idx
157294
pub fn reset_pos(&self, char_idx: usize) {
158295
reset_pos(&self.inline_annotations, char_idx, |annot| annot.char_idx);
159296
reset_pos(&self.overlays, char_idx, |annot| annot.char_idx);
160-
reset_pos(&self.line_annotations, char_idx, |annot| {
161-
annot.anchor_char_idx
162-
});
297+
for (next_anchor, layer) in &self.line_annotations {
298+
next_anchor.set(unsafe { layer.get().reset_pos(char_idx) });
299+
}
163300
}
164301

165302
pub fn collect_overlay_highlights(
@@ -219,8 +356,9 @@ impl<'a> TextAnnotations<'a> {
219356
///
220357
/// The line annotations **must be sorted** by their `char_idx`.
221358
/// Multiple line annotations with the same `char_idx` **are not allowed**.
222-
pub fn add_line_annotation(&mut self, layer: &'a [LineAnnotation]) -> &mut Self {
223-
self.line_annotations.push((layer, ()).into());
359+
pub fn add_line_annotation(&mut self, layer: Box<dyn LineAnnotation + 'a>) -> &mut Self {
360+
self.line_annotations
361+
.push((Cell::new(usize::MAX), layer.into()));
224362
self
225363
}
226364

@@ -250,21 +388,35 @@ impl<'a> TextAnnotations<'a> {
250388
overlay
251389
}
252390

253-
pub(crate) fn annotation_lines_at(&self, char_idx: usize) -> usize {
254-
self.line_annotations
255-
.iter()
256-
.map(|layer| {
257-
let mut lines = 0;
258-
while let Some(annot) = layer.annotations.get(layer.current_index.get()) {
259-
if annot.anchor_char_idx == char_idx {
260-
layer.current_index.set(layer.current_index.get() + 1);
261-
lines += annot.height
262-
} else {
263-
break;
391+
pub(crate) fn process_virtual_text_anchors(&self, grapheme: &FormattedGrapheme) {
392+
for (next_anchor, layer) in &self.line_annotations {
393+
loop {
394+
match next_anchor.get().cmp(&grapheme.char_idx) {
395+
Ordering::Less => next_anchor
396+
.set(unsafe { layer.get().skip_concealed_anchors(grapheme.char_idx) }),
397+
Ordering::Equal => {
398+
next_anchor.set(unsafe { layer.get().process_anchor(grapheme) })
264399
}
265-
}
266-
lines
267-
})
268-
.sum()
400+
Ordering::Greater => break,
401+
};
402+
}
403+
}
404+
}
405+
406+
pub(crate) fn virtual_lines_at(
407+
&self,
408+
char_idx: usize,
409+
line_end_visual_pos: Position,
410+
doc_line: usize,
411+
) -> usize {
412+
let mut virt_off = Position::new(0, 0);
413+
for (_, layer) in &self.line_annotations {
414+
virt_off += unsafe {
415+
layer
416+
.get()
417+
.insert_virtual_lines(char_idx, line_end_visual_pos + virt_off, doc_line)
418+
};
419+
}
420+
virt_off.row
269421
}
270422
}

0 commit comments

Comments
 (0)