From 057bd630d804c37b8094b5e9f35f93eb77c4b9e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Fri, 25 Jun 2021 13:01:08 +0900 Subject: [PATCH] Simplify selection rendering by injecting highlight scopes --- helix-term/src/ui/editor.rs | 374 ++++++++++++++++++++++-------------- helix-view/src/theme.rs | 4 + theme.toml | 5 +- 3 files changed, 240 insertions(+), 143 deletions(-) diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 78cca5c78..c737b3cee 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 { + iter: I, + spans: Box)>>, + + next_event: Option, + next_span: Option<(usize, std::ops::Range)>, + + queue: Vec, +} + +fn merge>( + iter: I, + spans: Vec<(usize, std::ops::Range)>, +) -> impl Iterator { + 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> Iterator for Merge { + type Item = HighlightEvent; + fn next(&mut self) -> Option { + 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> = if is_focused { + // inject selections as highlight scopes + let mut spans_: Vec<(usize, std::ops::Range)> = 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::>() - // ); - 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 947c6ee04..ece4fe9ae 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 { + self.scopes().iter().position(|s| s == scope) + } } #[test] diff --git a/theme.toml b/theme.toml index e7e793137..bf99fb7a5 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"