diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index e0a984d20d29..fe7dc0433edb 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -1,3 +1,5 @@ +pub mod span; + use crate::{ auto_pairs::AutoPairs, chars::char_is_line_ending, @@ -1142,7 +1144,7 @@ pub enum Error { } /// Represents a single step in rendering a syntax-highlighted document. -#[derive(Copy, Clone, Debug)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum HighlightEvent { Source { start: usize, end: usize }, HighlightStart(Highlight), @@ -1859,136 +1861,349 @@ fn injection_for_match<'a>( (language_name, content_node, included_children) } -pub struct Merge { - iter: I, - spans: Box)>>, +struct Merge, R: Iterator> { + left: iter::Peekable, + right: iter::Peekable, - next_event: Option, - next_span: Option<(usize, std::ops::Range)>, + left_queue: Vec, + right_queue: Vec, + + merge_queue: VecDeque, + + // These fields track the number of open HighlightStart events emitted by each + // side. `right` may be terminated early and this count is used to balance the + // number of HighlightStart and HighlightEnd events. `left` is also tracked in + // order to check invariants. + left_highlights: usize, + right_highlights: usize, - queue: Vec, + // These fields are only used to assert the invariants in debug builds. + prior_left: Option>, + prior_right: Option>, +} + +/// Merges two HighlightEvent iterators. +/// +/// Highlights from `right` overlay highlights from `left`. +/// Any `Source` events in `right` which preceed the first `Source` +/// in `left` are truncated or discarded. The final `Source` in `right` +/// which extends beyond the final `Source` in `left` is not truncated +/// or discarded: this is a special case made for the trailing character +/// when the cursor rests past the end-of-file. +/// +/// If all of the following invariants are satisfied for both event streams +/// `left` and `right` individually, then the invariants hold for the +/// resulting merged iterator as well. +/// +/// - `Source` events are sorted by `start` monotonically increasing, and are +/// non-overlapping. Ignoring `HighlightStart` and `HighlightEnd` events, for +/// all consecutive `a` and `b`, `a.start < b.start` (monotonically +/// increasing) and `a.end <= b.start` (non-overlapping). +/// - For all `Source` events, `start != end`. +/// - There are equal numbers of `HighlightStart` and `HighlightEnd` events. +/// - `Source` events may not be consecutive with any other `Source` event. +/// +/// # Panics +/// +/// The Iterator produced by this function may panic if any of the above +/// invariants or assumptions are not met. +pub fn merge, R: Iterator>( + left: L, + right: R, +) -> impl Iterator { + Merge { + left: left.peekable(), + right: right.peekable(), + left_queue: Vec::new(), + right_queue: Vec::new(), + merge_queue: VecDeque::new(), + left_highlights: 0, + right_highlights: 0, + prior_left: None, + prior_right: None, + } } -/// Merge a list of spans into the highlight event stream. -pub fn merge>( - iter: I, - spans: Vec<(usize, std::ops::Range)>, -) -> Merge { - let spans = Box::new(spans.into_iter()); - let mut merge = Merge { - iter, - spans, - next_event: None, - next_span: None, - queue: Vec::new(), - }; - merge.next_event = merge.iter.next(); - merge.next_span = merge.spans.next(); - merge +impl, R: Iterator> Merge { + /// Checks the invariants for the highlight event streams in `left` and `right`. + /// See the documentation for [merge] above. + fn check_invariants(&mut self) -> (bool, bool, bool, bool, bool, bool) { + let (left_min_width_1, left_monotonically_increasing, left_non_overlapping) = + match self.left.peek() { + Some(HighlightEvent::Source { start, end }) => match self.prior_left.take() { + Some(prior) => { + self.prior_left = Some(*start..*end); + (start != end, *start > prior.start, *start >= prior.end) + } + None => (true, true, true), + }, + _ => (true, true, true), + }; + + let (right_min_width_1, right_monotonically_increasing, right_non_overlapping) = + match self.right.peek() { + Some(HighlightEvent::Source { start, end }) => match self.prior_right.take() { + Some(prior) => { + self.prior_right = Some(*start..*end); + (start != end, *start > prior.start, *start >= prior.end) + } + None => (start != end, true, true), + }, + _ => (true, true, true), + }; + + ( + left_min_width_1, + left_monotonically_increasing, + left_non_overlapping, + right_min_width_1, + right_monotonically_increasing, + right_non_overlapping, + ) + } } -impl> Iterator for Merge { +impl, R: Iterator> Iterator + for Merge +{ type Item = HighlightEvent; + fn next(&mut self) -> Option { use HighlightEvent::*; - if let Some(event) = self.queue.pop() { + + // Emit any queued events. + if let Some(event) = self.merge_queue.pop_front() { return Some(event); } loop { - match (self.next_event, &self.next_span) { - // this happens when range is partially or fully offscreen - (Some(Source { start, .. }), Some((span, range))) if start > range.start => { - if start > range.end { - self.next_span = self.spans.next(); + debug_assert_eq!( + (true, true, true, true, true, true), + self.check_invariants() + ); + + match (self.left.peek_mut(), self.right.peek_mut()) { + // Left starts before right. + ( + Some(Source { + start: left_start, + end: left_end, + }), + Some(Source { + start: right_start, + end: _right_end, + }), + ) if *left_start < *right_start => { + let intersect = *(right_start.min(left_end)); + let event = Source { + start: *left_start, + end: intersect, + }; + + self.merge_queue.extend(self.left_queue.drain(..)); + self.merge_queue.push_back(event); + + // If left ends before right starts, complete left. + // Otherwise, subslice left and continue. + if *left_end == intersect { + self.left.next(); + while let Some(HighlightEnd) = self.left.peek() { + self.merge_queue.push_back(self.left.next().unwrap()); + } } else { - self.next_span = Some((*span, start..range.end)); + *left_start = intersect; }; + + return self.merge_queue.pop_front(); } - _ => break, - } - } - match (self.next_event, &self.next_span) { - (Some(HighlightStart(i)), _) => { - self.next_event = self.iter.next(); - Some(HighlightStart(i)) - } - (Some(HighlightEnd), _) => { - self.next_event = self.iter.next(); - Some(HighlightEnd) - } - (Some(Source { start, end }), Some((_, range))) if start < range.start => { - let intersect = range.start.min(end); - let event = Source { - start, - end: intersect, - }; + // Left and right start at the same point. + ( + Some(Source { + start: left_start, + end: left_end, + }), + Some(Source { + start: right_start, + end: right_end, + }), + ) if left_start == right_start => { + let intersect = *(right_end.min(left_end)); + let event = Source { + start: *left_start, + end: intersect, + }; - if end == intersect { - // the event is complete - self.next_event = self.iter.next(); - } else { - // subslice the event - self.next_event = Some(Source { - start: intersect, - end, - }); - }; + if intersect == *left_end && intersect == *right_end { + // If they end at the same point too, push both + // HighlightStart queues, a single Source for both, + // then the HighlightEnds for both. + self.left.next(); + self.right.next(); + + self.merge_queue.extend(self.left_queue.drain(..)); + self.merge_queue.extend(self.right_queue.drain(..)); + self.merge_queue.push_back(event); + + while let Some(HighlightEnd) = self.left.peek() { + debug_assert!(self.left_highlights != 0); + self.left_highlights = + self.left_highlights.checked_sub(1).unwrap_or_default(); + self.merge_queue.push_back(self.left.next().unwrap()); + } + while let Some(HighlightEnd) = self.right.peek() { + debug_assert!(self.right_highlights != 0); + self.right_highlights = + self.right_highlights.checked_sub(1).unwrap_or_default(); + self.merge_queue.push_back(self.right.next().unwrap()); + } + } else if *left_end == intersect { + // Right is longer than left. Complete left and subslice + // right. When completing left and subslicing right, we + // need to pop all of right's HighlightEnd events and then + // re-emit the HighlightStarts since right's highlights are + // layered on top of left's. + self.left.next(); + + self.merge_queue.extend(self.left_queue.drain(..)); + self.merge_queue.extend(self.right_queue.clone()); + self.merge_queue.push_back(event); + for _ in 0..self.right_queue.len() { + self.merge_queue.push_back(HighlightEnd); + } + while let Some(HighlightEnd) = self.left.peek() { + debug_assert!(self.left_highlights != 0); + self.left_highlights = + self.left_highlights.checked_sub(1).unwrap_or_default(); + self.merge_queue.push_back(self.left.next().unwrap()); + } + *right_start = intersect; + } else { + // Left is longer than right. Complete right and subslice left. + self.right.next(); + + self.merge_queue.extend(self.left_queue.drain(..)); + self.merge_queue.extend(self.right_queue.drain(..)); + self.merge_queue.push_back(event); + while let Some(HighlightEnd) = self.right.peek() { + debug_assert!(self.right_highlights != 0); + self.right_highlights = + self.right_highlights.checked_sub(1).unwrap_or_default(); + self.merge_queue.push_back(self.right.next().unwrap()); + } + *left_start = intersect; + } - Some(event) - } - (Some(Source { start, end }), Some((span, range))) if start == range.start => { - let intersect = range.end.min(end); - let event = HighlightStart(Highlight(*span)); - - // enqueue in reverse order - self.queue.push(HighlightEnd); - self.queue.push(Source { - start, - end: intersect, - }); + return self.merge_queue.pop_front(); + } - if end == intersect { - // the event is complete - self.next_event = self.iter.next(); - } else { - // subslice the event - self.next_event = Some(Source { - start: intersect, - end, - }); - }; + // Any `Source`s in `right` that come before `left` are truncated or discarded. + ( + Some(Source { + start: left_start, + end: _left_end, + }), + Some(Source { + start: right_start, + end: right_end, + }), + ) if *left_start > *right_start => { + if left_start >= right_end { + // Discard + self.right.next(); + while let Some(HighlightEnd) = self.right.peek() { + debug_assert!(self.right_highlights != 0); + self.right_highlights = + self.right_highlights.checked_sub(1).unwrap_or_default(); + let highlight_start = self.right_queue.pop(); + debug_assert!(highlight_start.is_some()); + self.right.next(); + } + } else { + // Truncate + *right_start = *left_start; + } + } - if intersect == range.end { - self.next_span = self.spans.next(); - } else { - self.next_span = Some((*span, intersect..range.end)); + // Queue any HighlightStart events. + (Some(left @ HighlightStart(_)), Some(_)) => { + self.left_highlights += 1; + self.left_queue.push(*left); + self.left.next(); + } + (Some(_), Some(right @ HighlightStart(_))) => { + self.right_highlights += 1; + self.right_queue.push(*right); + self.right.next(); + } + // These next two patterns are possible when a `HighlightEnd` + // immediately follows a `HighlightStart` with no `Source` in between. + // These branches are otherwise unreachable since the branches + // above that match on `Source`s consume `HighlightEnd`s eagerly. + (Some(HighlightEnd), Some(_)) => { + debug_assert!(self.left_highlights != 0); + self.left_highlights = self.left_highlights.checked_sub(1).unwrap_or_default(); + assert!(self.left_queue.pop().is_some()); + self.left.next(); + } + (Some(_), Some(HighlightEnd)) => { + debug_assert!(self.right_highlights != 0); + self.right_highlights = + self.right_highlights.checked_sub(1).unwrap_or_default(); + assert!(self.right_queue.pop().is_some()); + self.right.next(); } - Some(event) - } - (Some(event), None) => { - self.next_event = self.iter.next(); - Some(event) - } - // Can happen if cursor at EOF and/or diagnostic reaches past the end. - // We need to actually emit events for the cursor-at-EOF situation, - // even though the range is past the end of the text. This needs to be - // handled appropriately by the drawing code by not assuming that - // all `Source` events point to valid indices in the rope. - (None, Some((span, range))) => { - let event = HighlightStart(Highlight(*span)); - self.queue.push(HighlightEnd); - self.queue.push(Source { - start: range.start, - end: range.end, - }); - self.next_span = self.spans.next(); - Some(event) - } - (None, None) => None, - e => unreachable!("{:?}", e), + // If `right` finishes before `left` and `left` has been drained, + // let `left` run itself out. + (Some(_), None) if self.left_queue.is_empty() => return self.left.next(), + (Some(Source { start, end }), None) => { + // Right is finished. Any queued events must be drained before emitting + // more events from `left`. + self.merge_queue.extend(self.left_queue.drain(..)); + self.merge_queue.push_back(Source { + start: *start, + end: *end, + }); + self.left.next(); + return self.merge_queue.pop_front(); + } + // If `left` is finished and `right` has been drained, any remaining + // spans in `right` are past all `spans` in left and should be + // discarded. + (None, Some(_)) if self.right_highlights == 0 => return None, + (None, Some(event)) => { + // Left is finished. Drain any outstanding HighlightStart + // and HighlightEnd events in right, then finish right (the + // above clause). + self.merge_queue.extend(self.right_queue.drain(..)); + + if let Source { start, end } = event { + // This is a special case which allows a trailing cursor + // to be merged in by the selection highlights: the last + // `Source` in `right` is emitted and not truncated or + // discarded. + self.merge_queue.push_back(Source { + start: *start, + end: *end, + }); + } + + for _ in 0..self.right_highlights { + debug_assert!(self.right_highlights != 0); + self.right_highlights = + self.right_highlights.checked_sub(1).unwrap_or_default(); + self.merge_queue.push_back(HighlightEnd); + } + self.right.next(); + return self.merge_queue.pop_front(); + } + (None, None) => return None, + pattern => unreachable!( + "Impossible pattern in highlight event stream merge: {:?}", + pattern + ), + }; } } } @@ -2173,4 +2388,454 @@ mod test { let results = load_runtime_file("rust", "does-not-exist"); assert!(results.is_err()); } + + #[test] + fn test_sample_highlight_event_stream_merge() { + use HighlightEvent::*; + + /* + Left: + 2 3 + |-----------|-----------| + + 1 + |-----------------------------------------------| + + |---|---|---|---|---|---|---|---|---|---|---|---| + 0 1 2 3 4 5 6 7 8 9 10 11 12 + */ + let left = vec![ + HighlightStart(Highlight(1)), + Source { start: 0, end: 3 }, + HighlightStart(Highlight(2)), + Source { start: 3, end: 6 }, + HighlightEnd, // ends 2 + HighlightStart(Highlight(3)), + Source { start: 6, end: 9 }, + HighlightEnd, // ends 3 + Source { start: 9, end: 12 }, + HighlightEnd, // ends 1 + ] + .into_iter(); + + /* + Right: + 100 200 + |-------| |---------------| + + |---|---|---|---|---|---|---|---|---|---|---|---| + 0 1 2 3 4 5 6 7 8 9 10 11 12 + */ + let right = Box::new( + vec![ + HighlightStart(Highlight(100)), + Source { start: 2, end: 4 }, + HighlightEnd, // ends 100 + HighlightStart(Highlight(200)), + Source { start: 5, end: 9 }, + HighlightEnd, // ends 200 + ] + .into_iter(), + ); + + /* + Output: + 100 200 + |---| |---------------| + + 100 2 3 + |---|-----------|-----------| + + 1 + |-----------------------------------------------| + + |---|---|---|---|---|---|---|---|---|---|---|---| + 0 1 2 3 4 5 6 7 8 9 10 11 12 + */ + let output: Vec<_> = merge(left, right).collect(); + + assert_eq!( + output, + &[ + HighlightStart(Highlight(1)), + Source { start: 0, end: 2 }, + HighlightStart(Highlight(100)), + Source { start: 2, end: 3 }, + HighlightEnd, // ends 100 + HighlightStart(Highlight(2)), + HighlightStart(Highlight(100)), + Source { start: 3, end: 4 }, + HighlightEnd, // ends 100 + Source { start: 4, end: 5 }, + HighlightStart(Highlight(200)), + Source { start: 5, end: 6 }, + HighlightEnd, // ends 200 + HighlightEnd, // ends 2 + HighlightStart(Highlight(3)), + HighlightStart(Highlight(200)), + Source { start: 6, end: 9 }, + HighlightEnd, // ends 200 + HighlightEnd, // ends 3 + Source { start: 9, end: 12 }, + HighlightEnd // ends 1 + ], + ); + } + + #[test] + fn test_highlight_event_stream_merge_overlapping() { + use HighlightEvent::*; + + /* + Left: + 1, 2, 3 + |-------------------| + + |---|---|---|---|---| + 0 1 2 3 4 5 + */ + let left = vec![ + HighlightStart(Highlight(1)), + HighlightStart(Highlight(2)), + HighlightStart(Highlight(3)), + Source { start: 0, end: 5 }, + HighlightEnd, // ends 3 + HighlightEnd, // ends 2 + HighlightEnd, // ends 1 + ] + .into_iter(); + + /* + Right: + 4 + |-------------------| + + |---|---|---|---|---| + 0 1 2 3 4 5 + */ + let right = Box::new( + vec![ + HighlightStart(Highlight(4)), + Source { start: 0, end: 5 }, + HighlightEnd, // ends 4 + ] + .into_iter(), + ); + + /* + Output: + 1, 2, 3, 4 + |-------------------| + + |---|---|---|---|---| + 0 1 2 3 4 5 + */ + let output: Vec<_> = merge(left, right).collect(); + + assert_eq!( + output, + &[ + HighlightStart(Highlight(1)), + HighlightStart(Highlight(2)), + HighlightStart(Highlight(3)), + HighlightStart(Highlight(4)), + Source { start: 0, end: 5 }, + HighlightEnd, // ends 4 + HighlightEnd, // ends 3 + HighlightEnd, // ends 2 + HighlightEnd, // ends 1 + ], + ); + } + + #[test] + fn test_highlight_event_stream_merge_right_is_truncated() { + use HighlightEvent::*; + // This can happen when there are selections outside of the + // viewport. `left` is the syntax highlight event stream and + // `right` is the `span_iter` of selections/cursors. + + /* + Left: + 1 + |-------------------| + + |---|---|---|---|---|---|---|---|---|---| + 0 1 2 3 4 5 6 7 8 9 10 + */ + let left = vec![ + HighlightStart(Highlight(1)), + Source { start: 2, end: 7 }, + HighlightEnd, // ends 1 + ] + .into_iter(); + + /* + Right: + 2 3 4 + |---|-------------------------------|---| + + |---|---|---|---|---|---|---|---|---|---| + 0 1 2 3 4 5 6 7 8 9 10 + */ + let right = Box::new( + vec![ + HighlightStart(Highlight(2)), + Source { start: 0, end: 1 }, + HighlightEnd, // ends 2 + HighlightStart(Highlight(3)), + Source { start: 1, end: 9 }, + HighlightEnd, // ends 3 + HighlightStart(Highlight(4)), + Source { start: 9, end: 10 }, + HighlightEnd, // ends 4 + ] + .into_iter(), + ); + + // 2 and 4 are out of range and are discarded. 3 is truncated at the + // beginning but allowed to finish after the `left`. This is a special + // case for the trailing space from selection highlights. + /* + Output: + 1, 3 3 + |-------------------|-------| + + |---|---|---|---|---|---|---|---|---|---| + 0 1 2 3 4 5 6 7 8 9 10 + */ + let output: Vec<_> = merge(left, right).collect(); + + assert_eq!( + output, + &[ + HighlightStart(Highlight(1)), + HighlightStart(Highlight(3)), + Source { start: 2, end: 7 }, + HighlightEnd, // ends 3 + HighlightEnd, // ends 1 + HighlightStart(Highlight(3)), + Source { start: 7, end: 9 }, + HighlightEnd, // ends 3 + ], + ); + } + + #[test] + fn test_highlight_event_stream_right_ends_before_left_starts() { + use HighlightEvent::*; + + /* + Left: + 1 + |-------------------| + + |---|---|---|---|---|---|---|---|---|---| + 0 1 2 3 4 5 6 7 8 9 10 + */ + let left = vec![ + HighlightStart(Highlight(1)), + Source { start: 5, end: 10 }, + HighlightEnd, // ends 1 + ]; + + /* + Right: + 2 + |---------------| + + |---|---|---|---|---|---|---|---|---|---| + 0 1 2 3 4 5 6 7 8 9 10 + */ + let right = Box::new( + vec![ + HighlightStart(Highlight(2)), + Source { start: 0, end: 4 }, + HighlightEnd, // ends 2 + ] + .into_iter(), + ); + + // Left starts after right ends. Right is discarded. + let output: Vec<_> = merge(left.clone().into_iter(), right).collect(); + assert_eq!(output, left); + } + + #[test] + fn test_highlight_event_stream_right_ends_as_left_starts() { + use HighlightEvent::*; + + /* + Left: + 1 + |-------------------| + + |---|---|---|---|---|---|---|---|---|---| + 0 1 2 3 4 5 6 7 8 9 10 + */ + let left = vec![ + HighlightStart(Highlight(1)), + Source { start: 5, end: 10 }, + HighlightEnd, // ends 1 + ]; + + /* + Right: + 2 + |-------------------| + + |---|---|---|---|---|---|---|---|---|---| + 0 1 2 3 4 5 6 7 8 9 10 + */ + let right = Box::new( + vec![ + HighlightStart(Highlight(2)), + Source { start: 0, end: 5 }, + HighlightEnd, // ends 2 + ] + .into_iter(), + ); + + // Right is discarded if the range ends in the same place as left starts. + let output: Vec<_> = merge(left.clone().into_iter(), right).collect(); + assert_eq!(output, left); + } + + #[test] + fn test_highlight_event_stream_merge_layered_overlapping() { + use HighlightEvent::*; + + /* + Left: + 1 + |-----------| + + |---|---|---|---|---|---|---|---|---|---| + 0 1 2 3 4 5 6 7 8 9 10 + */ + let left = vec![ + HighlightStart(Highlight(1)), + Source { start: 3, end: 6 }, + HighlightEnd, // ends 1 + ] + .into_iter(); + + /* + Right: + 3 4 (0-width) + |-----------| + + 2 + |-------------------------------| + + |---|---|---|---|---|---|---|---|---|---| + 0 1 2 3 4 5 6 7 8 9 10 + */ + let right = Box::new( + vec![ + HighlightStart(Highlight(2)), + Source { start: 1, end: 3 }, + HighlightStart(Highlight(3)), + Source { start: 3, end: 6 }, + HighlightEnd, // ends 3 + HighlightStart(Highlight(4)), + // Trimmed zero-width Source. + HighlightEnd, // ends 4 + Source { start: 6, end: 9 }, + HighlightEnd, // ends 2 + ] + .into_iter(), + ); + + /* + Output: + 1,2,3 + |-----------| + + |---|---|---|---|---|---|---|---|---|---| + 0 1 2 3 4 5 6 7 8 9 10 + */ + let output: Vec<_> = merge(left, right).collect(); + + assert_eq!( + output, + &[ + HighlightStart(Highlight(1)), + HighlightStart(Highlight(2)), + HighlightStart(Highlight(3)), + Source { start: 3, end: 6 }, + HighlightEnd, // ends 3 + HighlightEnd, // ends 2 + HighlightEnd, // ends 1 + ], + ); + } + + #[test] + fn test_highlight_event_stream_merge_double_zero_width_span() { + use HighlightEvent::*; + // This is possible when merging two syntax highlight iterators. + // Syntax highlight iterators may produce a HighlightStart + // immediately followed by a HighlightEnd. + + /* + Left: + 1 2 (0-width) + |-----------| + + |---|---|---|---|---|---|---|---|---|---| + 0 1 2 3 4 5 6 7 8 9 10 + */ + let left = vec![ + HighlightStart(Highlight(1)), + Source { start: 3, end: 6 }, + HighlightEnd, // ends 1 + HighlightStart(Highlight(2)), + HighlightEnd, // ends 2 + ] + .into_iter(); + + /* + Right: + 3 4 (0-width) + |-----------| + + |---|---|---|---|---|---|---|---|---|---| + 0 1 2 3 4 5 6 7 8 9 10 + */ + let right = Box::new( + vec![ + HighlightStart(Highlight(3)), + Source { start: 3, end: 6 }, + HighlightEnd, // ends 3 + HighlightStart(Highlight(4)), + HighlightEnd, // ends 4 + ] + .into_iter(), + ); + + /* + Output: + 1,3 + |-----------| + + |---|---|---|---|---|---|---|---|---|---| + 0 1 2 3 4 5 6 7 8 9 10 + */ + let output: Vec<_> = merge(left, right).collect(); + + assert_eq!( + output, + &[ + HighlightStart(Highlight(1)), + HighlightStart(Highlight(3)), + Source { start: 3, end: 6 }, + HighlightEnd, // ends 3 + HighlightEnd, // ends 1 + HighlightStart(Highlight(4)), + // Zero-width Source span. + HighlightEnd, // ends 4 + ], + ); + } } diff --git a/helix-core/src/syntax/span.rs b/helix-core/src/syntax/span.rs new file mode 100644 index 000000000000..d94826021bfb --- /dev/null +++ b/helix-core/src/syntax/span.rs @@ -0,0 +1,511 @@ +use std::collections::VecDeque; + +use crate::syntax::Highlight; + +use super::HighlightEvent; + +/// A range highlighted with a given scope. +/// +/// Spans are a simplifer data structure for describing a highlight range +/// than [super::HighlightEvent]s. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct Span { + pub scope: usize, + pub start: usize, + pub end: usize, +} + +impl Ord for Span { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + // Sort by range: ascending by start and then ascending by end for ties. + if self.start == other.start { + self.end.cmp(&other.end) + } else { + self.start.cmp(&other.start) + } + } +} + +impl PartialOrd for Span { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +struct SpanIter { + spans: Vec, + index: usize, + event_queue: VecDeque, + range_ends: Vec, + cursor: usize, +} + +/// Creates an iterator of [HighlightEvent]s from a [Vec] of [Span]s. +/// +/// Spans may overlap. In the produced [HighlightEvent] iterator, all +/// `HighlightEvent::Source` events will be sorted by `start` and will not +/// overlap. The iterator produced by this function satisfies all invariants +/// and assumptions for [super::merge] +/// +/// `spans` is assumed to be sorted by `range.start` ascending and then by +/// `range.end` descending for any ties. +/// +/// # Panics +/// +/// Panics on debug builds when the input spans overlap or are not sorted. +pub fn span_iter(spans: Vec) -> impl Iterator { + // Assert that `spans` is sorted by `range.start` ascending and + // `range.end` descending. + debug_assert!(spans.windows(2).all(|window| window[0] <= window[1])); + + SpanIter { + spans, + index: 0, + event_queue: VecDeque::new(), + range_ends: Vec::new(), + cursor: 0, + } +} + +impl Iterator for SpanIter { + type Item = HighlightEvent; + + fn next(&mut self) -> Option { + use HighlightEvent::*; + + // Emit any queued highlight events + if let Some(event) = self.event_queue.pop_front() { + return Some(event); + } + + if self.index == self.spans.len() { + // There are no more spans. Emit Sources and HighlightEnds for + // any ranges which have not been terminated yet. + for end in self.range_ends.drain(..) { + if self.cursor != end { + debug_assert!(self.cursor < end); + self.event_queue.push_back(Source { + start: self.cursor, + end, + }); + } + self.event_queue.push_back(HighlightEnd); + self.cursor = end; + } + return self.event_queue.pop_front(); + } + + let span = self.spans[self.index]; + let mut subslice = None; + + self.range_ends.retain(|end| { + if span.start >= *end { + // The new range is past the end of this in-progress range. + // Complete the in-progress range by emitting a Source, + // if necessary, and a HighlightEnd and advance the cursor. + if self.cursor != *end { + debug_assert!(self.cursor < *end); + self.event_queue.push_back(Source { + start: self.cursor, + end: *end, + }); + } + self.event_queue.push_back(HighlightEnd); + self.cursor = *end; + false + } else if span.end > *end && subslice.is_none() { + // If the new range is longer than some in-progress range, + // we need to subslice this range and any ranges with the + // same start. `subslice` is set to the smallest `end` for + // which `range.start < end < range.end`. + subslice = Some(*end); + true + } else { + true + } + }); + + // Emit a Source event between consecutive HighlightStart events + if span.start != self.cursor && !self.range_ends.is_empty() { + debug_assert!(self.cursor < span.start); + self.event_queue.push_back(Source { + start: self.cursor, + end: span.start, + }); + } + + self.cursor = span.start; + + // Handle all spans that share this starting point. Either subslice + // or fully consume the span. + let mut i = self.index; + let mut subslices = 0; + loop { + match self.spans.get_mut(i) { + Some(span) if span.start == self.cursor => { + self.event_queue + .push_back(HighlightStart(Highlight(span.scope))); + i += 1; + + match subslice { + Some(intersect) => { + // If this span needs to be subsliced, consume the + // left part of the subslice and leave the right. + self.range_ends.push(intersect); + span.start = intersect; + subslices += 1; + } + None => { + // If there is no subslice, consume the span. + self.range_ends.push(span.end); + self.index = i; + } + } + } + _ => break, + } + } + + // Ensure range-ends are sorted ascending. Ranges which start at the + // same point may be in descending order because of the assumed + // sort-order of input ranges. + self.range_ends.sort_unstable(); + + // When spans are subsliced, the span Vec may need to be re-sorted + // because the `range.start` may now be greater than some `range.start` + // later in the Vec. This is not a classic "sort": we take several + // shortcuts to improve the runtime so that the sort may be done in + // time linear to the cardinality of the span Vec. Practically speaking + // the runtime is even better since we only scan from `self.index` to + // the first element of the Vec with a `range.start` after this range. + if let Some(intersect) = subslice { + let mut after = None; + + // Find the index of the largest span smaller than the intersect point. + // `i` starts on the index after the last subsliced span. + loop { + match self.spans.get(i) { + Some(span) if span.start < intersect => { + after = Some(i); + i += 1; + } + _ => break, + } + } + + // Rotate the subsliced spans so that they come after the spans that + // have smaller `range.start`s. + if let Some(after) = after { + self.spans[self.index..=after].rotate_left(subslices); + } + } + + self.event_queue.pop_front() + } +} + +struct FlatSpanIter { + iter: I, +} + +/// Converts a Vec of spans into an [Iterator] over [HighlightEvent]s +/// +/// This implementation does not resolve overlapping spans. Zero-width spans are +/// eliminated but otherwise the ranges are trusted to not overlap. +/// +/// This iterator has much less overhead than [span_iter] and is appropriate for +/// cases where the input spans are known to satisfy all of [super::merge]'s +/// assumptions and invariants, such as with selection highlights. +/// +/// # Panics +/// +/// Panics on debug builds when the input spans overlap or are not sorted. +pub fn flat_span_iter(spans: Vec) -> impl Iterator { + use HighlightEvent::*; + + // Consecutive items are sorted and non-overlapping + debug_assert!(spans + .windows(2) + .all(|window| window[1].start >= window[0].end)); + + FlatSpanIter { + iter: spans + .into_iter() + .filter(|span| span.start != span.end) + .flat_map(|span| { + [ + HighlightStart(Highlight(span.scope)), + Source { + start: span.start, + end: span.end, + }, + HighlightEnd, + ] + }), + } +} + +impl> Iterator for FlatSpanIter { + type Item = HighlightEvent; + fn next(&mut self) -> Option { + self.iter.next() + } +} + +#[cfg(test)] +mod test { + use super::*; + + macro_rules! span { + ($scope:literal, $range:expr) => { + Span { + scope: $scope, + start: $range.start, + end: $range.end, + } + }; + } + + #[test] + fn test_non_overlapping_span_iter_events() { + use HighlightEvent::*; + let input = vec![span!(1, 0..5), span!(2, 6..10)]; + let output: Vec<_> = span_iter(input).collect(); + assert_eq!( + output, + &[ + HighlightStart(Highlight(1)), + Source { start: 0, end: 5 }, + HighlightEnd, // ends 1 + HighlightStart(Highlight(2)), + Source { start: 6, end: 10 }, + HighlightEnd, // ends 2 + ], + ); + } + + #[test] + fn test_simple_overlapping_span_iter_events() { + use HighlightEvent::*; + + let input = vec![span!(1, 0..10), span!(2, 3..6)]; + let output: Vec<_> = span_iter(input).collect(); + assert_eq!( + output, + &[ + HighlightStart(Highlight(1)), + Source { start: 0, end: 3 }, + HighlightStart(Highlight(2)), + Source { start: 3, end: 6 }, + HighlightEnd, // ends 2 + Source { start: 6, end: 10 }, + HighlightEnd, // ends 1 + ], + ); + } + + #[test] + fn test_many_overlapping_span_iter_events() { + use HighlightEvent::*; + + /* + Input: + + 5 + |-------| + 4 + |----------| + 3 + |---------------------------| + 2 + |---------------| + 1 + |---------------------------------------| + + |---|---|---|---|---|---|---|---|---|---|---|---|---|---|---| + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 + */ + let input = vec![ + span!(1, 0..10), + span!(2, 1..5), + span!(3, 6..13), + span!(4, 12..15), + span!(5, 13..15), + ]; + + /* + Output: + + 2 3 4 5 + |---------------| |---------------| |---|-------| + + 1 3 4 + |---------------------------------------|-----------|-------| + + |---|---|---|---|---|---|---|---|---|---|---|---|---|---|---| + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 + */ + let output: Vec<_> = span_iter(input).collect(); + + assert_eq!( + output, + &[ + HighlightStart(Highlight(1)), + Source { start: 0, end: 1 }, + HighlightStart(Highlight(2)), + Source { start: 1, end: 5 }, + HighlightEnd, // ends 2 + Source { start: 5, end: 6 }, + HighlightStart(Highlight(3)), + Source { start: 6, end: 10 }, + HighlightEnd, // ends 3 + HighlightEnd, // ends 1 + HighlightStart(Highlight(3)), + Source { start: 10, end: 12 }, + HighlightStart(Highlight(4)), + Source { start: 12, end: 13 }, + HighlightEnd, // ends 4 + HighlightEnd, // ends 3 + HighlightStart(Highlight(4)), + HighlightStart(Highlight(5)), + Source { start: 13, end: 15 }, + HighlightEnd, // ends 5 + HighlightEnd, // ends 4 + ], + ); + } + + #[test] + fn test_multiple_duplicate_overlapping_span_iter_events() { + use HighlightEvent::*; + // This is based an a realistic case from rust-analyzer + // diagnostics. Spans may both overlap and duplicate one + // another at varying diagnostic levels. + + /* + Input: + + 4,5 + |-----------------------| + 3 + |---------------| + 1,2 + |-----------------------| + + |---|---|---|---|---|---|---|---|---|---| + 0 1 2 3 4 5 6 7 8 9 10 + */ + + let input = vec![ + span!(1, 0..6), + span!(2, 0..6), + span!(3, 4..8), + span!(4, 4..10), + span!(5, 4..10), + ]; + + /* + Output: + + 1,2 1..5 3..5 4,5 + |---------------|-------|-------|-------| + + |---|---|---|---|---|---|---|---|---|---| + 0 1 2 3 4 5 6 7 8 9 10 + */ + let output: Vec<_> = span_iter(input).collect(); + assert_eq!( + output, + &[ + HighlightStart(Highlight(1)), + HighlightStart(Highlight(2)), + Source { start: 0, end: 4 }, + HighlightStart(Highlight(3)), + HighlightStart(Highlight(4)), + HighlightStart(Highlight(5)), + Source { start: 4, end: 6 }, + HighlightEnd, // ends 5 + HighlightEnd, // ends 4 + HighlightEnd, // ends 3 + HighlightEnd, // ends 2 + HighlightEnd, // ends 1 + HighlightStart(Highlight(3)), + HighlightStart(Highlight(4)), + HighlightStart(Highlight(5)), + Source { start: 6, end: 8 }, + HighlightEnd, // ends 5 + Source { start: 8, end: 10 }, + HighlightEnd, // ends 4 + HighlightEnd, // ends 3 + ], + ); + } + + #[test] + fn test_span_iter_events_where_ranges_must_be_sorted() { + use HighlightEvent::*; + // This case needs the span Vec to be re-sorted because + // span 3 is subsliced to 9..10, putting it after span 4 and 5 + // in the ordering. + + /* + Input: + + 4 5 + |---|---| + 2 3 + |---------------| |---------------| + 1 + |-----------------------------------| + + |---|---|---|---|---|---|---|---|---|---| + 0 1 2 3 4 5 6 7 8 9 10 + */ + let input = vec![ + span!(1, 0..9), + span!(2, 1..5), + span!(3, 6..10), + span!(4, 7..8), + span!(5, 8..9), + ]; + + /* + Output: + + 4 5 + |---|---| + 2 3 + |---------------| |-----------| + 1 3 + |-----------------------------------|---| + + |---|---|---|---|---|---|---|---|---|---| + 0 1 2 3 4 5 6 7 8 9 10 + */ + let output: Vec<_> = span_iter(input).collect(); + assert_eq!( + output, + &[ + HighlightStart(Highlight(1)), + Source { start: 0, end: 1 }, + HighlightStart(Highlight(2)), + Source { start: 1, end: 5 }, + HighlightEnd, // ends 2 + Source { start: 5, end: 6 }, + HighlightStart(Highlight(3)), + Source { start: 6, end: 7 }, + HighlightStart(Highlight(4)), + Source { start: 7, end: 8 }, + HighlightEnd, // ends 4 + HighlightStart(Highlight(5)), + Source { start: 8, end: 9 }, + HighlightEnd, // ends 5 + HighlightEnd, // ends 3 + HighlightEnd, // ends 1 + HighlightStart(Highlight(3)), + Source { start: 9, end: 10 }, + HighlightEnd, // ends 3 + ], + ); + } +} diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 7cb29c3b1ecb..471c40b0f25f 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -11,7 +11,7 @@ use helix_core::{ ensure_grapheme_boundary_next_byte, next_grapheme_boundary, prev_grapheme_boundary, }, movement::Direction, - syntax::{self, HighlightEvent}, + syntax::{self, span::Span, HighlightEvent}, unicode::width::UnicodeWidthStr, LineEnding, Position, Range, Selection, Transaction, }; @@ -118,17 +118,24 @@ impl EditorView { } let highlights = Self::doc_syntax_highlights(doc, view.offset, inner.height, theme); - let highlights = syntax::merge(highlights, Self::doc_diagnostics_highlights(doc, theme)); + let highlights = syntax::merge( + highlights, + Box::new(syntax::span::span_iter(Self::doc_diagnostics_highlights( + doc, theme, + ))), + ); let highlights: Box> = if is_focused { Box::new(syntax::merge( highlights, - Self::doc_selection_highlights( - editor.mode(), - doc, - view, - theme, - &editor.config().cursor_shape, - ), + Box::new(syntax::span::flat_span_iter( + Self::doc_selection_highlights( + editor.mode(), + doc, + view, + theme, + &editor.config().cursor_shape, + ), + )), )) } else { Box::new(highlights) @@ -259,10 +266,7 @@ impl EditorView { } /// Get highlight spans for document diagnostics - pub fn doc_diagnostics_highlights( - doc: &Document, - theme: &Theme, - ) -> Vec<(usize, std::ops::Range)> { + pub fn doc_diagnostics_highlights(doc: &Document, theme: &Theme) -> Vec { use helix_core::diagnostic::Severity; let get_scope_of = |scope| { theme @@ -293,10 +297,11 @@ impl EditorView { Some(Severity::Error) => error, _ => r#default, }; - ( - diagnostic_scope, - diagnostic.range.start..diagnostic.range.end, - ) + Span { + scope: diagnostic_scope, + start: diagnostic.range.start, + end: diagnostic.range.end, + } }) .collect() } @@ -308,7 +313,7 @@ impl EditorView { view: &View, theme: &Theme, cursor_shape_config: &CursorShapeConfig, - ) -> Vec<(usize, std::ops::Range)> { + ) -> Vec { let text = doc.text().slice(..); let selection = doc.selection(view.id); let primary_idx = selection.primary_index(); @@ -337,7 +342,7 @@ impl EditorView { .find_scope_index("ui.selection.primary") .unwrap_or(selection_scope); - let mut spans: Vec<(usize, std::ops::Range)> = Vec::new(); + let mut spans: Vec = Vec::new(); for (i, range) in selection.iter().enumerate() { let selection_is_primary = i == primary_idx; let (cursor_scope, selection_scope) = if selection_is_primary { @@ -354,7 +359,11 @@ impl EditorView { // underline cursor (eg. when a regex prompt has focus) then // the primary cursor will be invisible. This doesn't happen // with block cursors since we manually draw *all* cursors. - spans.push((cursor_scope, range.head..range.head + 1)); + spans.push(Span { + scope: cursor_scope, + start: range.head, + end: range.head + 1, + }); } continue; } @@ -363,17 +372,33 @@ impl EditorView { if range.head > range.anchor { // Standard case. let cursor_start = prev_grapheme_boundary(text, range.head); - spans.push((selection_scope, range.anchor..cursor_start)); + spans.push(Span { + scope: selection_scope, + start: range.anchor, + end: cursor_start, + }); if !selection_is_primary || cursor_is_block { - spans.push((cursor_scope, cursor_start..range.head)); + spans.push(Span { + scope: cursor_scope, + start: cursor_start, + end: range.head, + }); } } else { // Reverse case. let cursor_end = next_grapheme_boundary(text, range.head); if !selection_is_primary || cursor_is_block { - spans.push((cursor_scope, range.head..cursor_end)); + spans.push(Span { + scope: cursor_scope, + start: range.head, + end: cursor_end, + }); } - spans.push((selection_scope, cursor_end..range.anchor)); + spans.push(Span { + scope: selection_scope, + start: cursor_end, + end: range.anchor, + }); } } diff --git a/helix-term/src/ui/lsp.rs b/helix-term/src/ui/lsp.rs index f2854551ded0..18af75b2cd5d 100644 --- a/helix-term/src/ui/lsp.rs +++ b/helix-term/src/ui/lsp.rs @@ -52,10 +52,11 @@ impl Component for SignatureHelp { let margin = Margin::horizontal(1); let active_param_span = self.active_param_range.map(|(start, end)| { - vec![( - cx.editor.theme.find_scope_index("ui.selection").unwrap(), - start..end, - )] + vec![syntax::span::Span { + scope: cx.editor.theme.find_scope_index("ui.selection").unwrap(), + start, + end, + }] }); let sig_text = crate::ui::markdown::highlighted_code_block( diff --git a/helix-term/src/ui/markdown.rs b/helix-term/src/ui/markdown.rs index 923dd73a16ab..afc6b0647a5d 100644 --- a/helix-term/src/ui/markdown.rs +++ b/helix-term/src/ui/markdown.rs @@ -31,7 +31,7 @@ pub fn highlighted_code_block<'a>( language: &str, theme: Option<&Theme>, config_loader: Arc, - additional_highlight_spans: Option)>>, + additional_highlight_spans: Option>, ) -> Text<'a> { let mut spans = Vec::new(); let mut lines = Vec::new(); @@ -61,7 +61,10 @@ pub fn highlighted_code_block<'a>( .map(|e| e.unwrap()); let highlight_iter: Box> = if let Some(spans) = additional_highlight_spans { - Box::new(helix_core::syntax::merge(highlight_iter, spans)) + Box::new(helix_core::syntax::merge( + highlight_iter, + Box::new(helix_core::syntax::span::span_iter(spans)), + )) } else { Box::new(highlight_iter) };