Skip to content

Commit

Permalink
Simplify selection rendering by injecting highlight scopes
Browse files Browse the repository at this point in the history
  • Loading branch information
archseer committed Jun 21, 2021
1 parent aca9d73 commit d1ac1a5
Show file tree
Hide file tree
Showing 2 changed files with 202 additions and 103 deletions.
304 changes: 201 additions & 103 deletions helix-term/src/ui/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use crate::{

use helix_core::{
coords_at_pos,
syntax::{self, HighlightEvent},
syntax::{self, Highlight, HighlightEvent},
Position, Range,
};
use helix_lsp::LspProgressMap;
Expand Down Expand Up @@ -43,6 +43,116 @@ impl Default for EditorView {
}
}

struct Merge<I> {
iter: I,
spans: Box<dyn Iterator<Item = (usize, std::ops::Range<usize>)>>,

next_event: Option<HighlightEvent>,
next_span: Option<(usize, std::ops::Range<usize>)>,

queue: Vec<HighlightEvent>,
}

fn merge<I: Iterator<Item = HighlightEvent>>(
iter: I,
spans: Vec<(usize, std::ops::Range<usize>)>,
) -> impl Iterator<Item = HighlightEvent> {
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<I: Iterator<Item = HighlightEvent>> Iterator for Merge<I> {
type Item = HighlightEvent;
fn next(&mut self) -> Option<Self::Item> {
use HighlightEvent::*;
if let Some(event) = self.queue.pop() {
return Some(event);
}
match (self.next_event, &self.next_span) {
(Some(HighlightStart(i)), _) => {
self.next_event = self.iter.next();
return Some(HighlightStart(i));
}
(Some(HighlightEnd), _) => {
self.next_event = self.iter.next();
return Some(HighlightEnd);
}
(Some(Source { start, end }), Some((span, range))) if start < range.start => {
let intersect = range.start.min(end);
let event = Source {
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,
});
};

Some(event)
}
// not implemented: (Some(Source { start: 4, end: 14 }), Some((37, 3..5)))'
(Some(Source { start, end }), Some((span, range))) if start == range.start => {
let intersect = range.end.min(end);
// TODO: emit span start
let event = HighlightStart(Highlight(*span));

// enqueue in reverse order
self.queue.push(HighlightEnd);
self.queue.push(Source {
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 == range.end {
self.next_span = self.spans.next();
} else {
self.next_span = Some((*span, intersect..range.end));
}

Some(event)

// if Source start == range_start
// emit Source {start, min(end, range.end)}
// now if end == min(end, range.end), replace source with iter.next
// truncate range.start to min(end, range.end)
}
(Some(event), None) => {
self.next_event = self.iter.next();
return Some(event);
}
(None, None) => return None,
e => unimplemented!("{:?}", e),
}
}
}

impl EditorView {
pub fn new(keymaps: Keymaps) -> Self {
Self {
Expand Down Expand Up @@ -141,8 +251,58 @@ impl EditorView {
let mut line = 0u16;
let tab_width = doc.tab_width();

let highlights = highlights.into_iter().map(|event| match event.unwrap() {
// convert byte offsets to char offset
HighlightEvent::Source { start, end } => {
let start = text.byte_to_char(start);
let end = text.byte_to_char(end);
HighlightEvent::Source { start, end }
}
event => event,
});

let selections = doc.selection(view.id);

let selection_scope = theme
.scopes()
.iter()
.position(|scope| scope == "ui.selection")
.expect("no selection scope found!");

let selection_cursor_scope = theme
.scopes()
.iter()
.position(|scope| scope == "ui.selection.cursor")
.unwrap_or(selection_scope);

let highlights: Box<dyn Iterator<Item = HighlightEvent>> = if is_focused {
// inject selections as highlight scopes
let mut spans_: Vec<(usize, std::ops::Range<usize>)> = Vec::new();

for range in selections {
if range.head == range.anchor {
spans_.push((selection_cursor_scope, range.head..range.head + 1));
continue;
}

let reverse = range.head < range.anchor;

if reverse {
spans_.push((selection_cursor_scope, range.head..range.head + 1));
spans_.push((selection_scope, range.head + 1..range.anchor + 1));
} else {
spans_.push((selection_scope, range.anchor..range.head));
spans_.push((selection_cursor_scope, range.head..range.head + 1));
}
}

Box::new(merge(highlights, spans_))
} else {
Box::new(highlights)
};

'outer: for event in highlights {
match event.unwrap() {
match event {
HighlightEvent::HighlightStart(span) => {
spans.push(span);
}
Expand All @@ -151,29 +311,14 @@ impl EditorView {
}
HighlightEvent::Source { start, end } => {
// TODO: filter out spans out of viewport for now..

// TODO: do these before iterating
let start = text.byte_to_char(start);
let end = text.byte_to_char(end);

let text = text.slice(start..end);

use helix_core::graphemes::{grapheme_width, RopeGraphemes};

// TODO: scope matching: biggest union match? [string] & [html, string], [string, html] & [ string, html]
// can do this by sorting our theme matches based on array len (longest first) then stopping at the
// first rule that matches (rule.all(|scope| scopes.contains(scope)))
// log::info!(
// "scopes: {:?}",
// spans
// .iter()
// .map(|span| theme.scopes()[span.0].as_str())
// .collect::<Vec<_>>()
// );
let style = match spans.first() {
Some(span) => theme.get(theme.scopes()[span.0].as_str()),
None => theme.get("ui.text"),
};
let style = spans.iter().fold(theme.get("ui.text"), |acc, span| {
let style = theme.get(theme.scopes()[span.0].as_str());
acc.patch(style)
});

// TODO: we could render the text to a surface, then cache that, that
// way if only the selection/cursor changes we can copy from cache
Expand All @@ -184,7 +329,20 @@ impl EditorView {

// iterate over range char by char
for grapheme in RopeGraphemes::new(text) {
let out_of_bounds = visual_x < view.first_col as u16
|| visual_x >= viewport.width + view.first_col as u16;

if grapheme == "\n" {
if !out_of_bounds {
// we still want to render an empty cell with the style
surface.set_string(
viewport.x + visual_x - view.first_col as u16,
viewport.y + line,
" ",
style,
);
}

visual_x = 0;
line += 1;

Expand All @@ -193,11 +351,18 @@ impl EditorView {
break 'outer;
}
} else if grapheme == "\t" {
if !out_of_bounds {
// we still want to render an empty cell with the style
surface.set_string(
viewport.x + visual_x - view.first_col as u16,
viewport.y + line,
" ".repeat(tab_width),
style,
);
}

visual_x = visual_x.saturating_add(tab_width as u16);
} else {
let out_of_bounds = visual_x < view.first_col as u16
|| visual_x >= viewport.width + view.first_col as u16;

// Cow will prevent allocations if span contained in a single slice
// which should really be the majority case
let grapheme = Cow::from(grapheme);
Expand Down Expand Up @@ -283,95 +448,28 @@ impl EditorView {
let end = text.line_to_char(last_line + 1);
Range::new(start, end)
};
let cursor_style = Style::default()
// .bg(Color::Rgb(255, 255, 255))
.add_modifier(Modifier::REVERSED);
// let cursor_style = Style::default().add_modifier(Modifier::REVERSED);

let selection_style = theme.get("ui.selection");
// let selection_style = theme.get("ui.selection");

for selection in doc
.selection(view.id)
.iter()
.filter(|range| range.overlaps(&screen))
{
// TODO: render also if only one of the ranges is in viewport
let mut start = view.screen_coords_at_pos(doc, text, selection.anchor);
let mut end = view.screen_coords_at_pos(doc, text, selection.head);

let head = end;

if selection.head < selection.anchor {
std::mem::swap(&mut start, &mut end);
}
let start = start.unwrap_or_else(|| Position::new(0, 0));
let end = end.unwrap_or_else(|| {
Position::new(viewport.height as usize, viewport.width as usize)
});

if start.row == end.row {
surface.set_style(
Rect::new(
viewport.x + start.col as u16,
viewport.y + start.row as u16,
// .min is important, because set_style does a
// for i in area.left()..area.right() and
// area.right = x + width !!! which shouldn't be > then surface.area.right()
// This is checked by a debug_assert! in Buffer::index_of
((end.col - start.col) as u16 + 1).min(
surface
.area
.width
.saturating_sub(viewport.x + start.col as u16),
),
1,
),
selection_style,
);
} else {
surface.set_style(
Rect::new(
viewport.x + start.col as u16,
viewport.y + start.row as u16,
// text.line(view.first_line).len_chars() as u16 - start.col as u16,
viewport.width.saturating_sub(start.col as u16),
1,
),
selection_style,
);
for i in start.row + 1..end.row {
surface.set_style(
Rect::new(
viewport.x,
viewport.y + i as u16,
// text.line(view.first_line + i).len_chars() as u16,
viewport.width,
1,
),
selection_style,
);
}
surface.set_style(
Rect::new(
viewport.x,
viewport.y + end.row as u16,
(end.col as u16).min(viewport.width),
1,
),
selection_style,
);
}
let head = view.screen_coords_at_pos(doc, text, selection.head);

// cursor
if let Some(head) = head {
surface.set_style(
Rect::new(
viewport.x + head.col as u16,
viewport.y + head.row as u16,
1,
1,
),
cursor_style,
);
// surface.set_style(
// Rect::new(
// viewport.x + head.col as u16,
// viewport.y + head.row as u16,
// 1,
// 1,
// ),
// cursor_style,
// );
surface.set_stringn(
viewport.x + 1 - OFFSET,
viewport.y + head.row as u16,
Expand Down
1 change: 1 addition & 0 deletions theme.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"ui.text.focus" = { fg = "#dbbfef"} # lilac

"ui.selection" = { bg = "#540099" }
"ui.selection.cursor" = { modifiers = ["reversed"] }
"ui.menu.selected" = { fg = "#281733", bg = "#ffffff" } # revolver

"warning" = "#ffcd1c"
Expand Down

0 comments on commit d1ac1a5

Please sign in to comment.