aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBlaž Hrastnik2021-06-25 04:01:08 +0000
committerBlaž Hrastnik2021-06-27 14:28:22 +0000
commit057bd630d804c37b8094b5e9f35f93eb77c4b9e6 (patch)
tree8c8329703fa34638c6aa8306972f3ebc582f2fdf
parent44566ea812eed023c89ab49b59f62e76ef2bd6c7 (diff)
Simplify selection rendering by injecting highlight scopes
-rw-r--r--helix-term/src/ui/editor.rs374
-rw-r--r--helix-view/src/theme.rs4
-rw-r--r--theme.toml5
3 files changed, 240 insertions, 143 deletions
diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs
index 78cca5c7..c737b3ce 100644
--- a/helix-term/src/ui/editor.rs
+++ b/helix-term/src/ui/editor.rs
@@ -9,7 +9,7 @@ use crate::{
use helix_core::{
coords_at_pos,
graphemes::ensure_grapheme_boundary,
- syntax::{self, HighlightEvent},
+ syntax::{self, Highlight, HighlightEvent},
LineEnding, Position, Range,
};
use helix_lsp::LspProgressMap;
@@ -44,6 +44,124 @@ 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);
+ }
+
+ loop {
+ match (self.next_event, &self.next_span) {
+ // this happens when range is partially or fully offscreen
+ (Some(Source { start, end }), Some((span, range))) if start > range.start => {
+ if start > range.end {
+ self.next_span = self.spans.next();
+ } else {
+ self.next_span = Some((*span, start..range.end));
+ };
+ }
+ _ => break,
+ }
+ }
+
+ 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)
+ }
+ (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,
+ });
+
+ 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)
+ }
+ (Some(event), None) => {
+ self.next_event = self.iter.next();
+ return Some(event);
+ }
+ (None, None) => return None,
+ e => unreachable!("{:?}", e),
+ }
+ }
+}
+
impl EditorView {
pub fn new(keymaps: Keymaps) -> Self {
Self {
@@ -140,9 +258,91 @@ 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 = ensure_grapheme_boundary(text, text.byte_to_char(start));
+ let end = ensure_grapheme_boundary(text, text.byte_to_char(end));
+ HighlightEvent::Source { start, end }
+ }
+ event => event,
+ });
+
+ let selections = doc.selection(view.id);
+ let primary_idx = selections.primary_index();
+
+ let selection_scope = theme
+ .find_scope_index("ui.selection")
+ .expect("no selection scope found!");
+
+ let base_cursor_scope = theme
+ .find_scope_index("ui.cursor")
+ .unwrap_or(selection_scope);
+
+ let cursor_scope = match doc.mode() {
+ Mode::Insert => theme.find_scope_index("ui.cursor.insert"),
+ Mode::Select => theme.find_scope_index("ui.cursor.select"),
+ Mode::Normal => Some(base_cursor_scope),
+ }
+ .unwrap_or(base_cursor_scope);
+
+ let primary_selection_scope = theme
+ .find_scope_index("ui.selection.primary")
+ .unwrap_or(selection_scope);
+
+ // TODO: primary + insert mode patching
+ // let primary_cursor_style = theme
+ // .try_get("ui.cursor.primary")
+ // .map(|style| {
+ // if mode != Mode::Normal {
+ // // we want to make sure that the insert and select highlights
+ // // also affect the primary cursor if set
+ // style.patch(cursor_style)
+ // } else {
+ // style
+ // }
+ // })
+ // .unwrap_or(cursor_style);
+
+ let primary_cursor_scope = theme
+ .find_scope_index("ui.cursor.primary")
+ .unwrap_or(cursor_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 (i, range) in selections.iter().enumerate() {
+ let (cursor_scope, selection_scope) = if i == primary_idx {
+ (primary_cursor_scope, primary_selection_scope)
+ } else {
+ (cursor_scope, selection_scope)
+ };
+
+ if range.head == range.anchor {
+ spans_.push((cursor_scope, range.head..range.head + 1));
+ continue;
+ }
+
+ let reverse = range.head < range.anchor;
+
+ if reverse {
+ spans_.push((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((cursor_scope, range.head..range.head + 1));
+ }
+ }
+
+ Box::new(merge(highlights, spans_))
+ } else {
+ Box::new(highlights)
+ };
+
'outer: for event in highlights {
- match event.unwrap() {
- HighlightEvent::HighlightStart(mut span) => {
+ match event {
+ HighlightEvent::HighlightStart(span) => {
spans.push(span);
}
HighlightEvent::HighlightEnd => {
@@ -150,29 +350,14 @@ impl EditorView {
}
HighlightEvent::Source { start, end } => {
// TODO: filter out spans out of viewport for now..
-
- // TODO: do these before iterating
- let start = ensure_grapheme_boundary(text, text.byte_to_char(start));
- let end = ensure_grapheme_boundary(text, 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
@@ -183,7 +368,19 @@ 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 LineEnding::from_rope_slice(&grapheme).is_some() {
+ 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;
@@ -192,11 +389,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);
@@ -283,126 +487,11 @@ impl EditorView {
Range::new(start, end)
};
- let mode = doc.mode();
- let base_cursor_style = theme
- .try_get("ui.cursor")
- .unwrap_or_else(|| Style::default().add_modifier(Modifier::REVERSED));
- let cursor_style = match mode {
- Mode::Insert => theme.try_get("ui.cursor.insert"),
- Mode::Select => theme.try_get("ui.cursor.select"),
- Mode::Normal => Some(base_cursor_style),
- }
- .unwrap_or(base_cursor_style);
- let primary_cursor_style = theme
- .try_get("ui.cursor.primary")
- .map(|style| {
- if mode != Mode::Normal {
- // we want to make sure that the insert and select highlights
- // also affect the primary cursor if set
- style.patch(cursor_style)
- } else {
- style
- }
- })
- .unwrap_or(cursor_style);
-
- let selection_style = theme.get("ui.selection");
- let primary_selection_style = theme
- .try_get("ui.selection.primary")
- .unwrap_or(selection_style);
-
let selection = doc.selection(view.id);
- let primary_idx = selection.primary_index();
-
- for (i, selection) in selection
- .iter()
- .enumerate()
- .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 (cursor_style, selection_style) = if i == primary_idx {
- (primary_cursor_style, primary_selection_style)
- } else {
- (cursor_style, selection_style)
- };
-
- 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,
- );
- }
-
- // cursor
+ for selection in selection.iter().filter(|range| range.overlaps(&screen)) {
+ let head = view.screen_coords_at_pos(doc, text, selection.head);
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_stringn(
viewport.x + 1 - OFFSET,
viewport.y + head.row as u16,
@@ -410,6 +499,7 @@ impl EditorView {
5,
linenr_select,
);
+
// TODO: set cursor position for IME
if let Some(syntax) = doc.syntax() {
use helix_core::match_brackets;
diff --git a/helix-view/src/theme.rs b/helix-view/src/theme.rs
index 947c6ee0..ece4fe9a 100644
--- a/helix-view/src/theme.rs
+++ b/helix-view/src/theme.rs
@@ -215,6 +215,10 @@ impl Theme {
pub fn scopes(&self) -> &[String] {
&self.scopes
}
+
+ pub fn find_scope_index(&self, scope: &str) -> Option<usize> {
+ self.scopes().iter().position(|s| s == scope)
+ }
}
#[test]
diff --git a/theme.toml b/theme.toml
index e7e79313..bf99fb7a 100644
--- a/theme.toml
+++ b/theme.toml
@@ -52,9 +52,12 @@
"ui.selection" = { bg = "#540099" }
"ui.selection.primary" = { bg = "#540099" }
+# TODO: namespace ui.cursor as ui.selection.cursor?
"ui.cursor.select" = { bg = "#6F44F0" }
-"ui.cursor.insert" = { bg = "#802F00" }
+"ui.cursor.insert" = { bg = "#ffffff" }
"ui.cursor.match" = { fg = "#212121", bg = "#6C6999" }
+"ui.cursor" = { modifiers = ["reversed"] }
+
"ui.menu.selected" = { fg = "#281733", bg = "#ffffff" } # revolver
"warning" = "#ffcd1c"