aboutsummaryrefslogtreecommitdiff
path: root/helix-term/src
diff options
context:
space:
mode:
authorPascal Kuthe2023-01-31 17:03:19 +0000
committerGitHub2023-01-31 17:03:19 +0000
commit4dcf1fe66ba30a78edc054780d9b65c2f826530f (patch)
treeffb84ea94f07ceb52494a955b1bd78f115395dc0 /helix-term/src
parent4eca4b3079bf53de874959270d0b3471d320debc (diff)
rework positioning/rendering and enable softwrap/virtual text (#5420)
* rework positioning/rendering, enables softwrap/virtual text This commit is a large rework of the core text positioning and rendering code in helix to remove the assumption that on-screen columns/lines correspond to text columns/lines. A generic `DocFormatter` is introduced that positions graphemes on and is used both for rendering and for movements/scrolling. Both virtual text support (inline, grapheme overlay and multi-line) and a capable softwrap implementation is included. fix picker highlight cleanup doc formatter, use word bondaries for wrapping make visual vertical movement a seperate commnad estimate line gutter width to improve performance cache cursor position cleanup and optimize doc formatter cleanup documentation fix typos Co-authored-by: Daniel Hines <d4hines@gmail.com> update documentation fix panic in last_visual_line funciton improve soft-wrap documentation add extend_visual_line_up/down commands fix non-visual vertical movement streamline virtual text highlighting, add softwrap indicator fix cursor position if softwrap is disabled improve documentation of text_annotations module avoid crashes if view anchor is out of bounds fix: consider horizontal offset when traslation char_idx -> vpos improve default configuration fix: mixed up horizontal and vertical offset reset view position after config reload apply suggestions from review disabled softwrap for very small screens to avoid endless spin fix wrap_indicator setting fix bar cursor disappearring on the EOF character add keybinding for linewise vertical movement fix: inconsistent gutter highlights improve virtual text API make scope idx lookup more ergonomic allow overlapping overlays correctly track char_pos for virtual text adjust configuration deprecate old position fucntions fix infinite loop in highlight lookup fix gutter style fix formatting document max-line-width interaction with softwrap change wrap-indicator example to use empty string fix: rare panic when view is in invalid state (bis) * Apply suggestions from code review Co-authored-by: Michael Davis <mcarsondavis@gmail.com> * improve documentation for positoning functions * simplify tests * fix documentation of Grapheme::width * Apply suggestions from code review Co-authored-by: Michael Davis <mcarsondavis@gmail.com> * add explicit drop invocation * Add explicit MoveFn type alias * add docuntation to Editor::cursor_cache * fix a few typos * explain use of allow(deprecated) * make gj and gk extend in select mode * remove unneded debug and TODO * mark tab_width_at #[inline] * add fast-path to move_vertically_visual in case softwrap is disabled * rename first_line to first_visual_line * simplify duplicate if/else --------- Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
Diffstat (limited to 'helix-term/src')
-rw-r--r--helix-term/src/application.rs13
-rw-r--r--helix-term/src/commands.rs228
-rw-r--r--helix-term/src/keymap/default.rs18
-rw-r--r--helix-term/src/ui/completion.rs6
-rw-r--r--helix-term/src/ui/document.rs475
-rw-r--r--helix-term/src/ui/editor.rs512
-rw-r--r--helix-term/src/ui/lsp.rs5
-rw-r--r--helix-term/src/ui/mod.rs1
-rw-r--r--helix-term/src/ui/picker.rs81
9 files changed, 913 insertions, 426 deletions
diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs
index c0cbc245..05ceb874 100644
--- a/helix-term/src/application.rs
+++ b/helix-term/src/application.rs
@@ -172,7 +172,7 @@ impl Application {
area,
theme_loader.clone(),
syn_loader.clone(),
- Box::new(Map::new(Arc::clone(&config), |config: &Config| {
+ Arc::new(Map::new(Arc::clone(&config), |config: &Config| {
&config.editor
})),
);
@@ -309,8 +309,10 @@ impl Application {
let surface = self.terminal.current_buffer_mut();
self.compositor.render(area, surface, &mut cx);
-
let (pos, kind) = self.compositor.cursor(area, &self.editor);
+ // reset cursor cache
+ self.editor.cursor_cache.set(None);
+
let pos = pos.map(|pos| (pos.col as u16, pos.row as u16));
self.terminal.draw(pos, kind).unwrap();
}
@@ -395,6 +397,13 @@ impl Application {
// Update all the relevant members in the editor after updating
// the configuration.
self.editor.refresh_config();
+
+ // reset view position in case softwrap was enabled/disabled
+ let scrolloff = self.editor.config().scrolloff;
+ for (view, _) in self.editor.tree.views_mut() {
+ let doc = &self.editor.documents[&view.doc];
+ view.ensure_cursor_in_view(doc, scrolloff)
+ }
}
/// refresh language config after config change
diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs
index 47ef1ff1..1cbdd0fb 100644
--- a/helix-term/src/commands.rs
+++ b/helix-term/src/commands.rs
@@ -9,21 +9,25 @@ use tui::widgets::Row;
pub use typed::*;
use helix_core::{
- comment, coords_at_pos, encoding, find_first_non_whitespace_char, find_root, graphemes,
+ char_idx_at_visual_offset, comment,
+ doc_formatter::TextFormat,
+ encoding, find_first_non_whitespace_char, find_root, graphemes,
history::UndoKind,
increment, indent,
indent::IndentStyle,
line_ending::{get_line_ending_of_str, line_end_char_index, str_is_line_ending},
match_brackets,
- movement::{self, Direction},
- object, pos_at_coords, pos_at_visual_coords,
+ movement::{self, move_vertically_visual, Direction},
+ object, pos_at_coords,
regex::{self, Regex, RegexBuilder},
search::{self, CharMatcher},
- selection, shellwords, surround, textobject,
+ selection, shellwords, surround,
+ text_annotations::TextAnnotations,
+ textobject,
tree_sitter::Node,
unicode::width::UnicodeWidthChar,
- visual_coords_at_pos, LineEnding, Position, Range, Rope, RopeGraphemes, RopeSlice, Selection,
- SmallVec, Tendril, Transaction,
+ visual_offset_from_block, LineEnding, Position, Range, Rope, RopeGraphemes, RopeSlice,
+ Selection, SmallVec, Tendril, Transaction,
};
use helix_view::{
clipboard::ClipboardType,
@@ -200,10 +204,14 @@ impl MappableCommand {
move_char_right, "Move right",
move_line_up, "Move up",
move_line_down, "Move down",
+ move_visual_line_up, "Move up",
+ move_visual_line_down, "Move down",
extend_char_left, "Extend left",
extend_char_right, "Extend right",
extend_line_up, "Extend up",
extend_line_down, "Extend down",
+ extend_visual_line_up, "Extend up",
+ extend_visual_line_down, "Extend down",
copy_selection_on_next_line, "Copy selection on next line",
copy_selection_on_prev_line, "Copy selection on previous line",
move_next_word_start, "Move to start of next word",
@@ -538,18 +546,27 @@ impl PartialEq for MappableCommand {
fn no_op(_cx: &mut Context) {}
-fn move_impl<F>(cx: &mut Context, move_fn: F, dir: Direction, behaviour: Movement)
-where
- F: Fn(RopeSlice, Range, Direction, usize, Movement, usize) -> Range,
-{
+type MoveFn =
+ fn(RopeSlice, Range, Direction, usize, Movement, &TextFormat, &mut TextAnnotations) -> Range;
+
+fn move_impl(cx: &mut Context, move_fn: MoveFn, dir: Direction, behaviour: Movement) {
let count = cx.count();
let (view, doc) = current!(cx.editor);
let text = doc.text().slice(..);
+ let text_fmt = doc.text_format(view.inner_area(doc).width, None);
+ let mut annotations = view.text_annotations(doc, None);
- let selection = doc
- .selection(view.id)
- .clone()
- .transform(|range| move_fn(text, range, dir, count, behaviour, doc.tab_width()));
+ let selection = doc.selection(view.id).clone().transform(|range| {
+ move_fn(
+ text,
+ range,
+ dir,
+ count,
+ behaviour,
+ &text_fmt,
+ &mut annotations,
+ )
+ });
doc.set_selection(view.id, selection);
}
@@ -571,6 +588,24 @@ fn move_line_down(cx: &mut Context) {
move_impl(cx, move_vertically, Direction::Forward, Movement::Move)
}
+fn move_visual_line_up(cx: &mut Context) {
+ move_impl(
+ cx,
+ move_vertically_visual,
+ Direction::Backward,
+ Movement::Move,
+ )
+}
+
+fn move_visual_line_down(cx: &mut Context) {
+ move_impl(
+ cx,
+ move_vertically_visual,
+ Direction::Forward,
+ Movement::Move,
+ )
+}
+
fn extend_char_left(cx: &mut Context) {
move_impl(cx, move_horizontally, Direction::Backward, Movement::Extend)
}
@@ -587,6 +622,24 @@ fn extend_line_down(cx: &mut Context) {
move_impl(cx, move_vertically, Direction::Forward, Movement::Extend)
}
+fn extend_visual_line_up(cx: &mut Context) {
+ move_impl(
+ cx,
+ move_vertically_visual,
+ Direction::Backward,
+ Movement::Extend,
+ )
+}
+
+fn extend_visual_line_down(cx: &mut Context) {
+ move_impl(
+ cx,
+ move_vertically_visual,
+ Direction::Forward,
+ Movement::Extend,
+ )
+}
+
fn goto_line_end_impl(view: &mut View, doc: &mut Document, movement: Movement) {
let text = doc.text().slice(..);
@@ -814,7 +867,10 @@ fn trim_selections(cx: &mut Context) {
}
// align text in selection
+#[allow(deprecated)]
fn align_selections(cx: &mut Context) {
+ use helix_core::visual_coords_at_pos;
+
let (view, doc) = current!(cx.editor);
let text = doc.text().slice(..);
let selection = doc.selection(view.id);
@@ -891,17 +947,22 @@ fn goto_window(cx: &mut Context, align: Align) {
// as we type
let scrolloff = config.scrolloff.min(height.saturating_sub(1) / 2);
- let last_line = view.last_line(doc);
+ let last_visual_line = view.last_visual_line(doc);
- let line = match align {
- Align::Top => view.offset.row + scrolloff + count,
- Align::Center => view.offset.row + ((last_line - view.offset.row) / 2),
- Align::Bottom => last_line.saturating_sub(scrolloff + count),
+ let visual_line = match align {
+ Align::Top => view.offset.vertical_offset + scrolloff + count,
+ Align::Center => view.offset.vertical_offset + (last_visual_line / 2),
+ Align::Bottom => {
+ view.offset.vertical_offset + last_visual_line.saturating_sub(scrolloff + count)
+ }
}
- .max(view.offset.row + scrolloff)
- .min(last_line.saturating_sub(scrolloff));
+ .max(view.offset.vertical_offset + scrolloff)
+ .min(view.offset.vertical_offset + last_visual_line.saturating_sub(scrolloff));
+
+ let pos = view
+ .pos_at_visual_coords(doc, visual_line as u16, 0, false)
+ .expect("visual_line was constrained to the view area");
- let pos = doc.text().line_to_char(line);
let text = doc.text().slice(..);
let selection = doc
.selection(view.id)
@@ -1385,53 +1446,72 @@ pub fn scroll(cx: &mut Context, offset: usize, direction: Direction) {
let range = doc.selection(view.id).primary();
let text = doc.text().slice(..);
- let cursor = visual_coords_at_pos(text, range.cursor(text), doc.tab_width());
- let doc_last_line = doc.text().len_lines().saturating_sub(1);
-
- let last_line = view.last_line(doc);
-
- if direction == Backward && view.offset.row == 0
- || direction == Forward && last_line == doc_last_line
- {
- return;
- }
-
+ let cursor = range.cursor(text);
let height = view.inner_height();
let scrolloff = config.scrolloff.min(height / 2);
+ let offset = match direction {
+ Forward => offset as isize,
+ Backward => -(offset as isize),
+ };
- view.offset.row = match direction {
- Forward => view.offset.row + offset,
- Backward => view.offset.row.saturating_sub(offset),
- }
- .min(doc_last_line);
-
- // recalculate last line
- let last_line = view.last_line(doc);
-
- // clamp into viewport
- let line = cursor
- .row
- .max(view.offset.row + scrolloff)
- .min(last_line.saturating_sub(scrolloff));
+ let doc_text = doc.text().slice(..);
+ let viewport = view.inner_area(doc);
+ let text_fmt = doc.text_format(viewport.width, None);
+ let annotations = view.text_annotations(doc, None);
+ (view.offset.anchor, view.offset.vertical_offset) = char_idx_at_visual_offset(
+ doc_text,
+ view.offset.anchor,
+ view.offset.vertical_offset as isize + offset,
+ 0,
+ &text_fmt,
+ &annotations,
+ );
- // If cursor needs moving, replace primary selection
- if line != cursor.row {
- let head = pos_at_visual_coords(text, Position::new(line, cursor.col), doc.tab_width()); // this func will properly truncate to line end
+ let head;
+ match direction {
+ Forward => {
+ head = char_idx_at_visual_offset(
+ doc_text,
+ view.offset.anchor,
+ (view.offset.vertical_offset + scrolloff) as isize,
+ 0,
+ &text_fmt,
+ &annotations,
+ )
+ .0;
+ if head <= cursor {
+ return;
+ }
+ }
+ Backward => {
+ head = char_idx_at_visual_offset(
+ doc_text,
+ view.offset.anchor,
+ (view.offset.vertical_offset + height - scrolloff) as isize,
+ 0,
+ &text_fmt,
+ &annotations,
+ )
+ .0;
+ if head >= cursor {
+ return;
+ }
+ }
+ }
- let anchor = if cx.editor.mode == Mode::Select {
- range.anchor
- } else {
- head
- };
+ let anchor = if cx.editor.mode == Mode::Select {
+ range.anchor
+ } else {
+ head
+ };
- // replace primary selection with an empty selection at cursor pos
- let prim_sel = Range::new(anchor, head);
- let mut sel = doc.selection(view.id).clone();
- let idx = sel.primary_index();
- sel = sel.replace(idx, prim_sel);
- doc.set_selection(view.id, sel);
- }
+ // replace primary selection with an empty selection at cursor pos
+ let prim_sel = Range::new(anchor, head);
+ let mut sel = doc.selection(view.id).clone();
+ let idx = sel.primary_index();
+ sel = sel.replace(idx, prim_sel);
+ doc.set_selection(view.id, sel);
}
fn page_up(cx: &mut Context) {
@@ -1458,7 +1538,15 @@ fn half_page_down(cx: &mut Context) {
scroll(cx, offset, Direction::Forward);
}
+#[allow(deprecated)]
+// currently uses the deprected `visual_coords_at_pos`/`pos_at_visual_coords` functions
+// as this function ignores softwrapping (and virtual text) and instead only cares
+// about "text visual position"
+//
+// TODO: implement a variant of that uses visual lines and respects virtual text
fn copy_selection_on_line(cx: &mut Context, direction: Direction) {
+ use helix_core::{pos_at_visual_coords, visual_coords_at_pos};
+
let count = cx.count();
let (view, doc) = current!(cx.editor);
let text = doc.text().slice(..);
@@ -4475,11 +4563,19 @@ fn align_view_bottom(cx: &mut Context) {
fn align_view_middle(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
- let text = doc.text().slice(..);
- let pos = doc.selection(view.id).primary().cursor(text);
- let pos = coords_at_pos(text, pos);
+ let inner_width = view.inner_width(doc);
+ let text_fmt = doc.text_format(inner_width, None);
+ // there is no horizontal position when softwrap is enabled
+ if text_fmt.soft_wrap {
+ return;
+ }
+ let doc_text = doc.text().slice(..);
+ let annotations = view.text_annotations(doc, None);
+ let pos = doc.selection(view.id).primary().cursor(doc_text);
+ let pos =
+ visual_offset_from_block(doc_text, view.offset.anchor, pos, &text_fmt, &annotations).0;
- view.offset.col = pos
+ view.offset.horizontal_offset = pos
.col
.saturating_sub((view.inner_area(doc).width as usize) / 2);
}
diff --git a/helix-term/src/keymap/default.rs b/helix-term/src/keymap/default.rs
index d48e6935..01184f80 100644
--- a/helix-term/src/keymap/default.rs
+++ b/helix-term/src/keymap/default.rs
@@ -7,8 +7,8 @@ use helix_core::hashmap;
pub fn default() -> HashMap<Mode, Keymap> {
let normal = keymap!({ "Normal mode"
"h" | "left" => move_char_left,
- "j" | "down" => move_line_down,
- "k" | "up" => move_line_up,
+ "j" | "down" => move_visual_line_down,
+ "k" | "up" => move_visual_line_up,
"l" | "right" => move_char_right,
"t" => find_till_char,
@@ -55,6 +55,8 @@ pub fn default() -> HashMap<Mode, Keymap> {
"m" => goto_last_modified_file,
"n" => goto_next_buffer,
"p" => goto_previous_buffer,
+ "k" => move_line_up,
+ "j" => move_line_down,
"." => goto_last_modification,
},
":" => command_mode,
@@ -321,8 +323,8 @@ pub fn default() -> HashMap<Mode, Keymap> {
let mut select = normal.clone();
select.merge_nodes(keymap!({ "Select mode"
"h" | "left" => extend_char_left,
- "j" | "down" => extend_line_down,
- "k" | "up" => extend_line_up,
+ "j" | "down" => extend_visual_line_down,
+ "k" | "up" => extend_visual_line_up,
"l" | "right" => extend_char_right,
"w" => extend_next_word_start,
@@ -345,6 +347,10 @@ pub fn default() -> HashMap<Mode, Keymap> {
"esc" => exit_select_mode,
"v" => normal_mode,
+ "g" => { "Goto"
+ "k" => extend_line_up,
+ "j" => extend_line_down,
+ },
}));
let insert = keymap!({ "Insert mode"
"esc" => normal_mode,
@@ -362,8 +368,8 @@ pub fn default() -> HashMap<Mode, Keymap> {
"C-j" | "ret" => insert_newline,
"tab" => insert_tab,
- "up" => move_line_up,
- "down" => move_line_down,
+ "up" => move_visual_line_up,
+ "down" => move_visual_line_down,
"left" => move_char_left,
"right" => move_char_right,
"pageup" => page_up,
diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs
index 2eca709d..90e2fed0 100644
--- a/helix-term/src/ui/completion.rs
+++ b/helix-term/src/ui/completion.rs
@@ -391,8 +391,10 @@ impl Component for Completion {
let language = doc.language_name().unwrap_or("");
let text = doc.text().slice(..);
let cursor_pos = doc.selection(view.id).primary().cursor(text);
- let coords = helix_core::visual_coords_at_pos(text, cursor_pos, doc.tab_width());
- let cursor_pos = (coords.row - view.offset.row) as u16;
+ let coords = view
+ .screen_coords_at_pos(doc, text, cursor_pos)
+ .expect("cursor must be in view");
+ let cursor_pos = coords.row as u16;
let mut markdown_doc = match &option.documentation {
Some(lsp::Documentation::String(contents))
diff --git a/helix-term/src/ui/document.rs b/helix-term/src/ui/document.rs
new file mode 100644
index 00000000..66332410
--- /dev/null
+++ b/helix-term/src/ui/document.rs
@@ -0,0 +1,475 @@
+use std::cmp::min;
+
+use helix_core::doc_formatter::{DocumentFormatter, GraphemeSource, TextFormat};
+use helix_core::graphemes::Grapheme;
+use helix_core::str_utils::char_to_byte_idx;
+use helix_core::syntax::Highlight;
+use helix_core::syntax::HighlightEvent;
+use helix_core::text_annotations::TextAnnotations;
+use helix_core::{visual_offset_from_block, Position, RopeSlice};
+use helix_view::editor::{WhitespaceConfig, WhitespaceRenderValue};
+use helix_view::graphics::Rect;
+use helix_view::theme::Style;
+use helix_view::view::ViewPosition;
+use helix_view::Document;
+use helix_view::Theme;
+use tui::buffer::Buffer as Surface;
+
+pub trait LineDecoration {
+ fn render_background(&mut self, _renderer: &mut TextRenderer, _pos: LinePos) {}
+ fn render_foreground(
+ &mut self,
+ _renderer: &mut TextRenderer,
+ _pos: LinePos,
+ _end_char_idx: usize,
+ ) {
+ }
+}
+
+impl<F: FnMut(&mut TextRenderer, LinePos)> LineDecoration for F {
+ fn render_background(&mut self, renderer: &mut TextRenderer, pos: LinePos) {
+ self(renderer, pos)
+ }
+}
+
+/// A wrapper around a HighlightIterator
+/// that merges the layered highlights to create the final text style
+/// and yields the active text style and the char_idx where the active
+/// style will have to be recomputed.
+struct StyleIter<'a, H: Iterator<Item = HighlightEvent>> {
+ text_style: Style,
+ active_highlights: Vec<Highlight>,
+ highlight_iter: H,
+ theme: &'a Theme,
+}
+
+impl<H: Iterator<Item = HighlightEvent>> Iterator for StyleIter<'_, H> {
+ type Item = (Style, usize);
+ fn next(&mut self) -> Option<(Style, usize)> {
+ while let Some(event) = self.highlight_iter.next() {
+ match event {
+ HighlightEvent::HighlightStart(highlights) => {
+ self.active_highlights.push(highlights)
+ }
+ HighlightEvent::HighlightEnd => {
+ self.active_highlights.pop();
+ }
+ HighlightEvent::Source { start, end } => {
+ if start == end {
+ continue;
+ }
+ let style = self
+ .active_highlights
+ .iter()
+ .fold(self.text_style, |acc, span| {
+ acc.patch(self.theme.highlight(span.0))
+ });
+ return Some((style, end));
+ }
+ }
+ }
+ None
+ }
+}
+
+#[derive(Debug, PartialEq, Eq, Copy, Clone)]
+pub struct LinePos {
+ /// Indicates whether the given visual line
+ /// is the first visual line of the given document line
+ pub first_visual_line: bool,
+ /// The line index of the document line that contains the given visual line
+ pub doc_line: usize,
+ /// Vertical offset from the top of the inner view area
+ pub visual_line: u16,
+ /// The first char index of this visual line.
+ /// Note that if the visual line is entirely filled by
+ /// a very long inline virtual text then this index will point
+ /// at the next (non-virtual) char after this visual line
+ pub start_char_idx: usize,
+}
+
+pub type TranslatedPosition<'a> = (usize, Box<dyn FnMut(&mut TextRenderer, Position) + 'a>);
+
+#[allow(clippy::too_many_arguments)]
+pub fn render_document(
+ surface: &mut Surface,
+ viewport: Rect,
+ doc: &Document,
+ offset: ViewPosition,
+ doc_annotations: &TextAnnotations,
+ highlight_iter: impl Iterator<Item = HighlightEvent>,
+ theme: &Theme,
+ line_decoration: &mut [Box<dyn LineDecoration + '_>],
+ translated_positions: &mut [TranslatedPosition],
+) {
+ let mut renderer = TextRenderer::new(surface, doc, theme, offset.horizontal_offset, viewport);
+ render_text(
+ &mut renderer,
+ doc.text().slice(..),
+ offset,
+ &doc.text_format(viewport.width, Some(theme)),
+ doc_annotations,
+ highlight_iter,
+ theme,
+ line_decoration,
+ translated_positions,
+ )
+}
+
+fn translate_positions(
+ char_pos: usize,
+ first_visisble_char_idx: usize,
+ translated_positions: &mut [TranslatedPosition],
+ text_fmt: &TextFormat,
+ renderer: &mut TextRenderer,
+ pos: Position,
+) {
+ // check if any positions translated on the fly (like cursor) has been reached
+ for (char_idx, callback) in &mut *translated_positions {
+ if *char_idx < char_pos && *char_idx >= first_visisble_char_idx {
+ // by replacing the char_index with usize::MAX large number we ensure
+ // that the same position is only translated once
+ // text will never reach usize::MAX as rust memory allocations are limited
+ // to isize::MAX
+ *char_idx = usize::MAX;
+
+ if text_fmt.soft_wrap {
+ callback(renderer, pos)
+ } else if pos.col >= renderer.col_offset
+ && pos.col - renderer.col_offset < renderer.viewport.width as usize
+ {
+ callback(
+ renderer,
+ Position {
+ row: pos.row,
+ col: pos.col - renderer.col_offset,
+ },
+ )
+ }
+ }
+ }
+}
+
+#[allow(clippy::too_many_arguments)]
+pub fn render_text<'t>(
+ renderer: &mut TextRenderer,
+ text: RopeSlice<'t>,
+ offset: ViewPosition,
+ text_fmt: &TextFormat,
+ text_annotations: &TextAnnotations,
+ highlight_iter: impl Iterator<Item = HighlightEvent>,
+ theme: &Theme,
+ line_decorations: &mut [Box<dyn LineDecoration + '_>],
+ translated_positions: &mut [TranslatedPosition],
+) {
+ let (
+ Position {
+ row: mut row_off, ..
+ },
+ mut char_pos,
+ ) = visual_offset_from_block(
+ text,
+ offset.anchor,
+ offset.anchor,
+ text_fmt,
+ text_annotations,
+ );
+ row_off += offset.vertical_offset;
+ assert_eq!(0, offset.vertical_offset);
+
+ let (mut formatter, mut first_visible_char_idx) =
+ DocumentFormatter::new_at_prev_checkpoint(text, text_fmt, text_annotations, offset.anchor);
+ let mut styles = StyleIter {
+ text_style: renderer.text_style,
+ active_highlights: Vec::with_capacity(64),
+ highlight_iter,
+ theme,
+ };
+
+ let mut last_line_pos = LinePos {
+ first_visual_line: false,
+ doc_line: usize::MAX,
+ visual_line: u16::MAX,
+ start_char_idx: usize::MAX,
+ };
+ let mut is_in_indent_area = true;
+ let mut last_line_indent_level = 0;
+ let mut style_span = styles
+ .next()
+ .unwrap_or_else(|| (Style::default(), usize::MAX));
+
+ loop {
+ // formattter.line_pos returns to line index of the next grapheme
+ // so it must be called before formatter.next
+ let doc_line = formatter.line_pos();
+ // TODO refactor with let .. else once MSRV reaches 1.65
+ let (grapheme, mut pos) = if let Some(it) = formatter.next() {
+ it
+ } else {
+ let mut last_pos = formatter.visual_pos();
+ last_pos.col -= 1;
+ // check if any positions translated on the fly (like cursor) are at the EOF
+ translate_positions(
+ char_pos + 1,
+ first_visible_char_idx,
+ translated_positions,
+ text_fmt,
+ renderer,
+ last_pos,
+ );
+ break;
+ };
+
+ // skip any graphemes on visual lines before the block start
+ if pos.row < row_off {
+ if char_pos >= style_span.1 {
+ // TODO refactor using let..else once MSRV reaches 1.65
+ style_span = if let Some(style_span) = styles.next() {
+ style_span
+ } else {
+ break;
+ }
+ }
+ char_pos += grapheme.doc_chars();
+ first_visible_char_idx = char_pos + 1;
+ continue;
+ }
+ pos.row -= row_off;
+
+ // if the end of the viewport is reached stop rendering
+ if pos.row as u16 >= renderer.viewport.height {
+ break;
+ }
+
+ // apply decorations before rendering a new line
+ if pos.row as u16 != last_line_pos.visual_line {
+ if pos.row > 0 {
+ renderer.draw_indent_guides(last_line_indent_level, last_line_pos.visual_line);
+ is_in_indent_area = true;
+ for line_decoration in &mut *line_decorations {
+ line_decoration.render_foreground(renderer, last_line_pos, char_pos);
+ }
+ }
+ last_line_pos = LinePos {
+ first_visual_line: doc_line != last_line_pos.doc_line,
+ doc_line,
+ visual_line: pos.row as u16,
+ start_char_idx: char_pos,
+ };
+ for line_decoration in &mut *line_decorations {
+ line_decoration.render_background(renderer, last_line_pos);
+ }
+ }
+
+ // aquire the correct grapheme style
+ if char_pos >= style_span.1 {
+ // TODO refactor using let..else once MSRV reaches 1.65
+ style_span = if let Some(style_span) = styles.next() {
+ style_span
+ } else {
+ (Style::default(), usize::MAX)
+ }
+ }
+ char_pos += grapheme.doc_chars();
+
+ // check if any positions translated on the fly (like cursor) has been reached
+ translate_positions(
+ char_pos,
+ first_visible_char_idx,
+ translated_positions,
+ text_fmt,
+ renderer,
+ pos,
+ );
+
+ let grapheme_style = if let GraphemeSource::VirtualText { highlight } = grapheme.source {
+ let style = renderer.text_style;
+ if let Some(highlight) = highlight {
+ style.patch(theme.highlight(highlight.0))
+ } else {
+ style
+ }
+ } else {
+ style_span.0
+ };
+
+ renderer.draw_grapheme(
+ grapheme.grapheme,
+ grapheme_style,
+ &mut last_line_indent_level,
+ &mut is_in_indent_area,
+ pos,
+ );
+ }
+
+ renderer.draw_indent_guides(last_line_indent_level, last_line_pos.visual_line);
+ for line_decoration in &mut *line_decorations {
+ line_decoration.render_foreground(renderer, last_line_pos, char_pos);
+ }
+}
+
+#[derive(Debug)]
+pub struct TextRenderer<'a> {
+ pub surface: &'a mut Surface,
+ pub text_style: Style,
+ pub whitespace_style: Style,
+ pub indent_guide_char: String,
+ pub indent_guide_style: Style,
+ pub newline: String,
+ pub nbsp: String,
+ pub space: String,
+ pub tab: String,
+ pub tab_width: u16,
+ pub starting_indent: usize,
+ pub draw_indent_guides: bool,
+ pub col_offset: usize,
+ pub viewport: Rect,
+}
+
+impl<'a> TextRenderer<'a> {
+ pub fn new(
+ surface: &'a mut Surface,
+ doc: &Document,
+ theme: &Theme,
+ col_offset: usize,
+ viewport: Rect,
+ ) -> TextRenderer<'a> {
+ let editor_config = doc.config.load();
+ let WhitespaceConfig {
+ render: ws_render,
+ characters: ws_chars,
+ } = &editor_config.whitespace;
+
+ let tab_width = doc.tab_width();
+ let tab = if ws_render.tab() == WhitespaceRenderValue::All {
+ std::iter::once(ws_chars.tab)
+ .chain(std::iter::repeat(ws_chars.tabpad).take(tab_width - 1))
+ .collect()
+ } else {
+ " ".repeat(tab_width)
+ };
+ let newline = if ws_render.newline() == WhitespaceRenderValue::All {
+ ws_chars.newline.into()
+ } else {
+ " ".to_owned()
+ };
+
+ let space = if ws_render.space() == WhitespaceRenderValue::All {
+ ws_chars.space.into()
+ } else {
+ " ".to_owned()
+ };
+ let nbsp = if ws_render.nbsp() == WhitespaceRenderValue::All {
+ ws_chars.nbsp.into()
+ } else {
+ " ".to_owned()
+ };
+
+ let text_style = theme.get("ui.text");
+
+ TextRenderer {
+ surface,
+ indent_guide_char: editor_config.indent_guides.character.into(),
+ newline,
+ nbsp,
+ space,
+ tab_width: tab_width as u16,
+ tab,
+ whitespace_style: theme.get("ui.virtual.whitespace"),
+ starting_indent: (col_offset / tab_width)
+ + editor_config.indent_guides.skip_levels as usize,
+ indent_guide_style: text_style.patch(
+ theme
+ .try_get("ui.virtual.indent-guide")
+ .unwrap_or_else(|| theme.get("ui.virtual.whitespace")),
+ ),
+ text_style,
+ draw_indent_guides: editor_config.indent_guides.render,
+ viewport,
+ col_offset,
+ }
+ }
+
+ /// Draws a single `grapheme` at the current render position with a specified `style`.
+ pub fn draw_grapheme(
+ &mut self,
+ grapheme: Grapheme,
+ mut style: Style,
+ last_indent_level: &mut usize,
+ is_in_indent_area: &mut bool,
+ position: Position,
+ ) {
+ let cut_off_start = self.col_offset.saturating_sub(position.col as usize);
+ let is_whitespace = grapheme.is_whitespace();
+
+ // TODO is it correct to apply the whitspace style to all unicode white spaces?
+ if is_whitespace {
+ style = style.patch(self.whitespace_style);
+ }
+
+ let width = grapheme.width();
+ let grapheme = match grapheme {
+ Grapheme::Tab { width } => {
+ let grapheme_tab_width = char_to_byte_idx(&self.tab, width as usize);
+ &self.tab[..grapheme_tab_width]
+ }
+ // TODO special rendering for other whitespaces?
+ Grapheme::Other { ref g } if g == " " => &self.space,
+ Grapheme::Other { ref g } if g == "\u{00A0}" => &self.nbsp,
+ Grapheme::Other { ref g } => &*g,
+ Grapheme::Newline => &self.newline,
+ };
+
+ let in_bounds = self.col_offset <= (position.col as usize)
+ && (position.col as usize) < self.viewport.width as usize + self.col_offset;
+
+ if in_bounds {
+ self.surface.set_string(
+ self.viewport.x + (position.col - self.col_offset) as u16,
+ self.viewport.y + position.row as u16,
+ grapheme,
+ style,
+ );
+ } else if cut_off_start != 0 && cut_off_start < width as usize {
+ // partially on screen
+ let rect = Rect::new(
+ self.viewport.x as u16,
+ self.viewport.y + position.row as u16,
+ (width - cut_off_start) as u16,
+ 1,
+ );
+ self.surface.set_style(rect, style);
+ }
+
+ if *is_in_indent_area && !is_whitespace {
+ *last_indent_level = position.col;
+ *is_in_indent_area = false;
+ }
+ }
+
+ /// Overlay indentation guides ontop of a rendered line
+ /// The indentation level is computed in `draw_lines`.
+ /// Therefore this function must always be called afterwards.
+ pub fn draw_indent_guides(&mut self, indent_level: usize, row: u16) {
+ if !self.draw_indent_guides {
+ return;
+ }
+
+ // Don't draw indent guides outside of view
+ let end_indent = min(
+ indent_level,
+ // Add tab_width - 1 to round up, since the first visible
+ // indent might be a bit after offset.col
+ self.col_offset + self.viewport.width as usize + (self.tab_width - 1) as usize,
+ ) / self.tab_width as usize;
+
+ for i in self.starting_indent..end_indent {
+ let x =
+ (self.viewport.x as usize + (i * self.tab_width as usize) - self.col_offset) as u16;
+ let y = self.viewport.y + row;
+ debug_assert!(self.surface.in_bounds(x, y));
+ self.surface
+ .set_string(x, y, &self.indent_guide_char, self.indent_guide_style);
+ }
+ }
+}
diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs
index a0518964..f297b44e 100644
--- a/helix-term/src/ui/editor.rs
+++ b/helix-term/src/ui/editor.rs
@@ -4,7 +4,10 @@ use crate::{
job::{self, Callback},
key,
keymap::{KeymapResult, Keymaps},
- ui::{Completion, ProgressSpinners},
+ ui::{
+ document::{render_document, LinePos, TextRenderer, TranslatedPosition},
+ Completion, ProgressSpinners,
+ },
};
use helix_core::{
@@ -13,8 +16,9 @@ use helix_core::{
},
movement::Direction,
syntax::{self, HighlightEvent},
+ text_annotations::TextAnnotations,
unicode::width::UnicodeWidthStr,
- visual_coords_at_pos, LineEnding, Position, Range, Selection, Transaction,
+ visual_offset_from_block, Position, Range, Selection, Transaction,
};
use helix_view::{
document::{Mode, SCRATCH_BUFFER_NAME},
@@ -24,12 +28,12 @@ use helix_view::{
keyboard::{KeyCode, KeyModifiers},
Document, Editor, Theme, View,
};
-use std::{borrow::Cow, cmp::min, num::NonZeroUsize, path::PathBuf};
+use std::{num::NonZeroUsize, path::PathBuf, rc::Rc};
use tui::buffer::Buffer as Surface;
-use super::lsp::SignatureHelp;
use super::statusline;
+use super::{document::LineDecoration, lsp::SignatureHelp};
pub struct EditorView {
pub keymaps: Keymaps,
@@ -83,6 +87,10 @@ impl EditorView {
let theme = &editor.theme;
let config = editor.config();
+ let text_annotations = view.text_annotations(doc, Some(theme));
+ let mut line_decorations: Vec<Box<dyn LineDecoration>> = Vec::new();
+ let mut translated_positions: Vec<TranslatedPosition> = Vec::new();
+
// DAP: Highlight current stack frame position
let stack_frame = editor.debugger.as_ref().and_then(|debugger| {
if let (Some(frame), Some(thread_id)) = (debugger.active_frame, debugger.thread_id) {
@@ -103,28 +111,40 @@ impl EditorView {
== doc.path()
{
let line = frame.line - 1; // convert to 0-indexing
- if line >= view.offset.row && line < view.offset.row + area.height as usize {
- surface.set_style(
- Rect::new(
- area.x,
- area.y + (line - view.offset.row) as u16,
- area.width,
- 1,
- ),
- theme.get("ui.highlight"),
- );
- }
+ let style = theme.get("ui.highlight");
+ let line_decoration = move |renderer: &mut TextRenderer, pos: LinePos| {
+ if pos.doc_line != line {
+ return;
+ }
+ renderer
+ .surface
+ .set_style(Rect::new(area.x, pos.visual_line, area.width, 1), style);
+ };
+
+ line_decorations.push(Box::new(line_decoration));
}
}
if is_focused && config.cursorline {
- Self::highlight_cursorline(doc, view, surface, theme);
+ line_decorations.push(Self::cursorline_decorator(doc, view, theme))
}
+
if is_focused && config.cursorcolumn {
- Self::highlight_cursorcolumn(doc, view, surface, theme);
+ Self::highlight_cursorcolumn(doc, view, surface, theme, inner, &text_annotations);
+ }
+
+ let mut highlights =
+ Self::doc_syntax_highlights(doc, view.offset.anchor, inner.height, theme);
+ let overlay_highlights = Self::overlay_syntax_highlights(
+ doc,
+ view.offset.anchor,
+ inner.height,
+ &text_annotations,
+ );
+ if !overlay_highlights.is_empty() {
+ highlights = Box::new(syntax::merge(highlights, overlay_highlights));
}
- let mut highlights = Self::doc_syntax_highlights(doc, view.offset, inner.height, theme);
for diagnostic in Self::doc_diagnostics_highlights(doc, theme) {
// Most of the `diagnostic` Vecs are empty most of the time. Skipping
// a merge for any empty Vec saves a significant amount of work.
@@ -133,8 +153,9 @@ impl EditorView {
}
highlights = Box::new(syntax::merge(highlights, diagnostic));
}
+
let highlights: Box<dyn Iterator<Item = HighlightEvent>> = if is_focused {
- Box::new(syntax::merge(
+ let highlights = syntax::merge(
highlights,
Self::doc_selection_highlights(
editor.mode(),
@@ -143,19 +164,52 @@ impl EditorView {
theme,
&config.cursor_shape,
),
- ))
+ );
+ let focused_view_elements = Self::highlight_focused_view_elements(view, doc, theme);
+ if focused_view_elements.is_empty() {
+ Box::new(highlights)
+ } else {
+ Box::new(syntax::merge(highlights, focused_view_elements))
+ }
} else {
Box::new(highlights)
};
- Self::render_text_highlights(doc, view.offset, inner, surface, theme, highlights, &config);
- Self::render_gutter(editor, doc, view, view.area, surface, theme, is_focused);
- Self::render_rulers(editor, doc, view, inner, surface, theme);
+ Self::render_gutter(
+ editor,
+ doc,
+ view,
+ view.area,
+ theme,
+ is_focused,
+ &mut line_decorations,
+ );
if is_focused {
- Self::render_focused_view_elements(view, doc, inner, theme, surface);
+ let cursor = doc
+ .selection(view.id)
+ .primary()
+ .cursor(doc.text().slice(..));
+ // set the cursor_cache to out of view in case the position is not found
+ editor.cursor_cache.set(Some(None));
+ let update_cursor_cache =
+ |_: &mut TextRenderer, pos| editor.cursor_cache.set(Some(Some(pos)));
+ translated_positions.push((cursor, Box::new(update_cursor_cache)));
}
+ render_document(
+ surface,
+ inner,
+ doc,
+ view.offset,
+ &text_annotations,
+ highlights,
+ theme,
+ &mut line_decorations,
+ &mut *translated_positions,
+ );
+ Self::render_rulers(editor, doc, view, inner, surface, theme);
+
// if we're not at the edge of the screen, draw a right border
if viewport.right() != view.area.right() {
let x = area.right();
@@ -203,31 +257,53 @@ impl EditorView {
.iter()
// View might be horizontally scrolled, convert from absolute distance
// from the 1st column to relative distance from left of viewport
- .filter_map(|ruler| ruler.checked_sub(1 + view.offset.col as u16))
+ .filter_map(|ruler| ruler.checked_sub(1 + view.offset.horizontal_offset as u16))
.filter(|ruler| ruler < &viewport.width)
.map(|ruler| viewport.clip_left(ruler).with_width(1))
.for_each(|area| surface.set_style(area, ruler_theme))
}
+ pub fn overlay_syntax_highlights(
+ doc: &Document,
+ anchor: usize,
+ height: u16,
+ text_annotations: &TextAnnotations,
+ ) -> Vec<(usize, std::ops::Range<usize>)> {
+ let text = doc.text().slice(..);
+ let row = text.char_to_line(anchor.min(text.len_chars()));
+
+ let range = {
+ // Calculate viewport byte ranges:
+ // Saturating subs to make it inclusive zero indexing.
+ let last_line = text.len_lines().saturating_sub(1);
+ let last_visible_line = (row + height as usize).saturating_sub(1).min(last_line);
+ let start = text.line_to_byte(row.min(last_line));
+ let end = text.line_to_byte(last_visible_line + 1);
+
+ start..end
+ };
+
+ text_annotations.collect_overlay_highlights(range)
+ }
+
/// Get syntax highlights for a document in a view represented by the first line
/// and column (`offset`) and the last line. This is done instead of using a view
/// directly to enable rendering syntax highlighted docs anywhere (eg. picker preview)
pub fn doc_syntax_highlights<'doc>(
doc: &'doc Document,
- offset: Position,
+ anchor: usize,
height: u16,
_theme: &Theme,
) -> Box<dyn Iterator<Item = HighlightEvent> + 'doc> {
let text = doc.text().slice(..);
+ let row = text.char_to_line(anchor.min(text.len_chars()));
let range = {
// Calculate viewport byte ranges:
// Saturating subs to make it inclusive zero indexing.
- let last_line = doc.text().len_lines().saturating_sub(1);
- let last_visible_line = (offset.row + height as usize)
- .saturating_sub(1)
- .min(last_line);
- let start = text.line_to_byte(offset.row.min(last_line));
+ let last_line = text.len_lines().saturating_sub(1);
+ let last_visible_line = (row + height as usize).saturating_sub(1).min(last_line);
+ let start = text.line_to_byte(row.min(last_line));
let end = text.line_to_byte(last_visible_line + 1);
start..end
@@ -272,11 +348,11 @@ impl EditorView {
use helix_core::diagnostic::Severity;
let get_scope_of = |scope| {
theme
- .find_scope_index(scope)
+ .find_scope_index_exact(scope)
// get one of the themes below as fallback values
- .or_else(|| theme.find_scope_index("diagnostic"))
- .or_else(|| theme.find_scope_index("ui.cursor"))
- .or_else(|| theme.find_scope_index("ui.selection"))
+ .or_else(|| theme.find_scope_index_exact("diagnostic"))
+ .or_else(|| theme.find_scope_index_exact("ui.cursor"))
+ .or_else(|| theme.find_scope_index_exact("ui.selection"))
.expect(
"at least one of the following scopes must be defined in the theme: `diagnostic`, `ui.cursor`, or `ui.selection`",
)
@@ -339,29 +415,29 @@ impl EditorView {
let cursor_is_block = cursorkind == CursorKind::Block;
let selection_scope = theme
- .find_scope_index("ui.selection")
+ .find_scope_index_exact("ui.selection")
.expect("could not find `ui.selection` scope in the theme!");
let primary_selection_scope = theme
- .find_scope_index("ui.selection.primary")
+ .find_scope_index_exact("ui.selection.primary")
.unwrap_or(selection_scope);
let base_cursor_scope = theme
- .find_scope_index("ui.cursor")
+ .find_scope_index_exact("ui.cursor")
.unwrap_or(selection_scope);
let base_primary_cursor_scope = theme
.find_scope_index("ui.cursor.primary")
.unwrap_or(base_cursor_scope);
let cursor_scope = match mode {
- Mode::Insert => theme.find_scope_index("ui.cursor.insert"),
- Mode::Select => theme.find_scope_index("ui.cursor.select"),
- Mode::Normal => theme.find_scope_index("ui.cursor.normal"),
+ Mode::Insert => theme.find_scope_index_exact("ui.cursor.insert"),
+ Mode::Select => theme.find_scope_index_exact("ui.cursor.select"),
+ Mode::Normal => theme.find_scope_index_exact("ui.cursor.normal"),
}
.unwrap_or(base_cursor_scope);
let primary_cursor_scope = match mode {
- Mode::Insert => theme.find_scope_index("ui.cursor.primary.insert"),
- Mode::Select => theme.find_scope_index("ui.cursor.primary.select"),
- Mode::Normal => theme.find_scope_index("ui.cursor.primary.normal"),
+ Mode::Insert => theme.find_scope_index_exact("ui.cursor.primary.insert"),
+ Mode::Select => theme.find_scope_index_exact("ui.cursor.primary.select"),
+ Mode::Normal => theme.find_scope_index_exact("ui.cursor.primary.normal"),
}
.unwrap_or(base_primary_cursor_scope);
@@ -424,248 +500,26 @@ impl EditorView {
spans
}
- pub fn render_text_highlights<H: Iterator<Item = HighlightEvent>>(
- doc: &Document,
- offset: Position,
- viewport: Rect,
- surface: &mut Surface,
- theme: &Theme,
- highlights: H,
- config: &helix_view::editor::Config,
- ) {
- let whitespace = &config.whitespace;
- use helix_view::editor::WhitespaceRenderValue;
-
- // It's slightly more efficient to produce a full RopeSlice from the Rope, then slice that a bunch
- // of times than it is to always call Rope::slice/get_slice (it will internally always hit RSEnum::Light).
- let text = doc.text().slice(..);
-
- let characters = &whitespace.characters;
-
- let mut spans = Vec::new();
- let mut visual_x = 0usize;
- let mut line = 0u16;
- let tab_width = doc.tab_width();
- let tab = if whitespace.render.tab() == WhitespaceRenderValue::All {
- std::iter::once(characters.tab)
- .chain(std::iter::repeat(characters.tabpad).take(tab_width - 1))
- .collect()
- } else {
- " ".repeat(tab_width)
- };
- let space = characters.space.to_string();
- let nbsp = characters.nbsp.to_string();
- let newline = if whitespace.render.newline() == WhitespaceRenderValue::All {
- characters.newline.to_string()
- } else {
- " ".to_string()
- };
- let indent_guide_char = config.indent_guides.character.to_string();
-
- let text_style = theme.get("ui.text");
- let whitespace_style = theme.get("ui.virtual.whitespace");
-
- let mut is_in_indent_area = true;
- let mut last_line_indent_level = 0;
-
- // use whitespace style as fallback for indent-guide
- let indent_guide_style = text_style.patch(
- theme
- .try_get("ui.virtual.indent-guide")
- .unwrap_or_else(|| theme.get("ui.virtual.whitespace")),
- );
-
- let draw_indent_guides = |indent_level, line, surface: &mut Surface| {
- if !config.indent_guides.render {
- return;
- }
-
- let starting_indent =
- (offset.col / tab_width) + config.indent_guides.skip_levels as usize;
-
- // Don't draw indent guides outside of view
- let end_indent = min(
- indent_level,
- // Add tab_width - 1 to round up, since the first visible
- // indent might be a bit after offset.col
- offset.col + viewport.width as usize + (tab_width - 1),
- ) / tab_width;
-
- for i in starting_indent..end_indent {
- let x = (viewport.x as usize + (i * tab_width) - offset.col) as u16;
- let y = viewport.y + line;
- debug_assert!(surface.in_bounds(x, y));
- surface.set_string(x, y, &indent_guide_char, indent_guide_style);
- }
- };
-
- 'outer: for event in highlights {
- match event {
- HighlightEvent::HighlightStart(span) => {
- spans.push(span);
- }
- HighlightEvent::HighlightEnd => {
- spans.pop();
- }
- HighlightEvent::Source { start, end } => {
- let is_trailing_cursor = text.len_chars() < end;
-
- // `unwrap_or_else` part is for off-the-end indices of
- // the rope, to allow cursor highlighting at the end
- // of the rope.
- let text = text.get_slice(start..end).unwrap_or_else(|| " ".into());
- let style = spans
- .iter()
- .fold(text_style, |acc, span| acc.patch(theme.highlight(span.0)));
-
- let space = if whitespace.render.space() == WhitespaceRenderValue::All
- && !is_trailing_cursor
- {
- &space
- } else {
- " "
- };
-
- let nbsp = if whitespace.render.nbsp() == WhitespaceRenderValue::All
- && text.len_chars() < end
- {
- &nbsp
- } else {
- " "
- };
-
- use helix_core::graphemes::{grapheme_width, RopeGraphemes};
-
- for grapheme in RopeGraphemes::new(text) {
- let out_of_bounds = offset.col > visual_x
- || visual_x >= viewport.width as usize + offset.col;
-
- 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 as usize + visual_x - offset.col) as u16,
- viewport.y + line,
- &newline,
- style.patch(whitespace_style),
- );
- }
-
- draw_indent_guides(last_line_indent_level, line, surface);
-
- visual_x = 0;
- line += 1;
- is_in_indent_area = true;
-
- // TODO: with proper iter this shouldn't be necessary
- if line >= viewport.height {
- break 'outer;
- }
- } else {
- let grapheme = Cow::from(grapheme);
- let is_whitespace;
-
- let (display_grapheme, width) = if grapheme == "\t" {
- is_whitespace = true;
- // make sure we display tab as appropriate amount of spaces
- let visual_tab_width = tab_width - (visual_x % tab_width);
- let grapheme_tab_width =
- helix_core::str_utils::char_to_byte_idx(&tab, visual_tab_width);
-
- (&tab[..grapheme_tab_width], visual_tab_width)
- } else if grapheme == " " {
- is_whitespace = true;
- (space, 1)
- } else if grapheme == "\u{00A0}" {
- is_whitespace = true;
- (nbsp, 1)
- } else {
- is_whitespace = false;
- // Cow will prevent allocations if span contained in a single slice
- // which should really be the majority case
- let width = grapheme_width(&grapheme);
- (grapheme.as_ref(), width)
- };
-
- let cut_off_start = offset.col.saturating_sub(visual_x);
-
- if !out_of_bounds {
- // if we're offscreen just keep going until we hit a new line
- surface.set_string(
- (viewport.x as usize + visual_x - offset.col) as u16,
- viewport.y + line,
- display_grapheme,
- if is_whitespace {
- style.patch(whitespace_style)
- } else {
- style
- },
- );
- } else if cut_off_start != 0 && cut_off_start < width {
- // partially on screen
- let rect = Rect::new(
- viewport.x,
- viewport.y + line,
- (width - cut_off_start) as u16,
- 1,
- );
- surface.set_style(
- rect,
- if is_whitespace {
- style.patch(whitespace_style)
- } else {
- style
- },
- );
- }
-
- if is_in_indent_area && !(grapheme == " " || grapheme == "\t") {
- draw_indent_guides(visual_x, line, surface);
- is_in_indent_area = false;
- last_line_indent_level = visual_x;
- }
-
- visual_x = visual_x.saturating_add(width);
- }
- }
- }
- }
- }
- }
-
/// Render brace match, etc (meant for the focused view only)
- pub fn render_focused_view_elements(
+ pub fn highlight_focused_view_elements(
view: &View,
doc: &Document,
- viewport: Rect,
theme: &Theme,
- surface: &mut Surface,
- ) {
+ ) -> Vec<(usize, std::ops::Range<usize>)> {
// Highlight matching braces
if let Some(syntax) = doc.syntax() {
let text = doc.text().slice(..);
use helix_core::match_brackets;
let pos = doc.selection(view.id).primary().cursor(text);
- let pos = match_brackets::find_matching_bracket(syntax, doc.text(), pos)
- .and_then(|pos| view.screen_coords_at_pos(doc, text, pos));
-
- if let Some(pos) = pos {
+ if let Some(pos) = match_brackets::find_matching_bracket(syntax, doc.text(), pos) {
// ensure col is on screen
- if (pos.col as u16) < viewport.width + view.offset.col as u16
- && pos.col >= view.offset.col
- {
- let style = theme.try_get("ui.cursor.match").unwrap_or_else(|| {
- Style::default()
- .add_modifier(Modifier::REVERSED)
- .add_modifier(Modifier::DIM)
- });
-
- surface[(viewport.x + pos.col as u16, viewport.y + pos.row as u16)]
- .set_style(style);
+ if let Some(highlight) = theme.find_scope_index_exact("ui.cursor.match") {
+ return vec![(highlight, pos..pos + 1)];
}
}
}
+ Vec::new()
}
/// Render bufferline at the top
@@ -721,22 +575,17 @@ impl EditorView {
}
}
- pub fn render_gutter(
- editor: &Editor,
- doc: &Document,
+ pub fn render_gutter<'d>(
+ editor: &'d Editor,
+ doc: &'d Document,
view: &View,
viewport: Rect,
- surface: &mut Surface,
theme: &Theme,
is_focused: bool,
+ line_decorations: &mut Vec<Box<(dyn LineDecoration + 'd)>>,
) {
let text = doc.text().slice(..);
- let last_line = view.last_line(doc);
-
- // it's used inside an iterator so the collect isn't needless:
- // https://github.com/rust-lang/rust-clippy/issues/6164
- #[allow(clippy::needless_collect)]
- let cursors: Vec<_> = doc
+ let cursors: Rc<[_]> = doc
.selection(view.id)
.iter()
.map(|range| range.cursor_line(text))
@@ -746,29 +595,36 @@ impl EditorView {
let gutter_style = theme.get("ui.gutter");
let gutter_selected_style = theme.get("ui.gutter.selected");
-
- // avoid lots of small allocations by reusing a text buffer for each line
- let mut text = String::with_capacity(8);
+ let gutter_style_virtual = theme.get("ui.gutter.virtual");
+ let gutter_selected_style_virtual = theme.get("ui.gutter.selected.virtual");
for gutter_type in view.gutters() {
let mut gutter = gutter_type.style(editor, doc, view, theme, is_focused);
let width = gutter_type.width(view, doc);
- text.reserve(width); // ensure there's enough space for the gutter
- for (i, line) in (view.offset.row..(last_line + 1)).enumerate() {
- let selected = cursors.contains(&line);
+ // avoid lots of small allocations by reusing a text buffer for each line
+ let mut text = String::with_capacity(width);
+ let cursors = cursors.clone();
+ let gutter_decoration = move |renderer: &mut TextRenderer, pos: LinePos| {
+ // TODO handle softwrap in gutters
+ let selected = cursors.contains(&pos.doc_line);
let x = viewport.x + offset;
- let y = viewport.y + i as u16;
+ let y = viewport.y + pos.visual_line;
- let gutter_style = if selected {
- gutter_selected_style
- } else {
- gutter_style
+ let gutter_style = match (selected, pos.first_visual_line) {
+ (false, true) => gutter_style,
+ (true, true) => gutter_selected_style,
+ (false, false) => gutter_style_virtual,
+ (true, false) => gutter_selected_style_virtual,
};
- if let Some(style) = gutter(line, selected, &mut text) {
- surface.set_stringn(x, y, &text, width, gutter_style.patch(style));
+ if let Some(style) =
+ gutter(pos.doc_line, selected, pos.first_visual_line, &mut text)
+ {
+ renderer
+ .surface
+ .set_stringn(x, y, &text, width, gutter_style.patch(style));
} else {
- surface.set_style(
+ renderer.surface.set_style(
Rect {
x,
y,
@@ -779,7 +635,8 @@ impl EditorView {
);
}
text.clear();
- }
+ };
+ line_decorations.push(Box::new(gutter_decoration));
offset += width as u16;
}
@@ -840,10 +697,13 @@ impl EditorView {
}
/// Apply the highlighting on the lines where a cursor is active
- pub fn highlight_cursorline(doc: &Document, view: &View, surface: &mut Surface, theme: &Theme) {
+ pub fn cursorline_decorator(
+ doc: &Document,
+ view: &View,
+ theme: &Theme,
+ ) -> Box<dyn LineDecoration> {
let text = doc.text().slice(..);
- let last_line = view.last_line(doc);
-
+ // TODO only highlight the visual line that contains the cursor instead of the full visual line
let primary_line = doc.selection(view.id).primary().cursor_line(text);
// The secondary_lines do contain the primary_line, it doesn't matter
@@ -860,20 +720,23 @@ impl EditorView {
let primary_style = theme.get("ui.cursorline.primary");
let secondary_style = theme.get("ui.cursorline.secondary");
+ let viewport = view.area;
- for line in view.offset.row..(last_line + 1) {
+ let line_decoration = move |renderer: &mut TextRenderer, pos: LinePos| {
let area = Rect::new(
- view.area.x,
- view.area.y + (line - view.offset.row) as u16,
- view.area.width,
+ viewport.x,
+ viewport.y + pos.visual_line as u16,
+ viewport.width,
1,
);
- if primary_line == line {
- surface.set_style(area, primary_style);
- } else if secondary_lines.binary_search(&line).is_ok() {
- surface.set_style(area, secondary_style);
+ if primary_line == pos.doc_line {
+ renderer.surface.set_style(area, primary_style);
+ } else if secondary_lines.binary_search(&pos.doc_line).is_ok() {
+ renderer.surface.set_style(area, secondary_style);
}
- }
+ };
+
+ Box::new(line_decoration)
}
/// Apply the highlighting on the columns where a cursor is active
@@ -882,6 +745,8 @@ impl EditorView {
view: &View,
surface: &mut Surface,
theme: &Theme,
+ viewport: Rect,
+ text_annotations: &TextAnnotations,
) {
let text = doc.text().slice(..);
@@ -897,19 +762,23 @@ impl EditorView {
.unwrap_or_else(|| theme.get("ui.cursorline.secondary"));
let inner_area = view.inner_area(doc);
- let offset = view.offset.col;
let selection = doc.selection(view.id);
let primary = selection.primary();
+ let text_format = doc.text_format(viewport.width, None);
for range in selection.iter() {
let is_primary = primary == *range;
+ let cursor = range.cursor(text);
+
+ let Position { col, .. } =
+ visual_offset_from_block(text, cursor, cursor, &text_format, text_annotations).0;
- let Position { row: _, col } =
- visual_coords_at_pos(text, range.cursor(text), doc.tab_width());
// if the cursor is horizontally in the view
- if col >= offset && inner_area.width > (col - offset) as u16 {
+ if col >= view.offset.horizontal_offset
+ && inner_area.width > (col - view.offset.horizontal_offset) as u16
+ {
let area = Rect::new(
- inner_area.x + (col - offset) as u16,
+ inner_area.x + (col - view.offset.horizontal_offset) as u16,
view.area.y,
1,
view.area.height,
@@ -1149,7 +1018,7 @@ impl EditorView {
let pos_and_view = |editor: &Editor, row, column| {
editor.tree.views().find_map(|(view, _focus)| {
- view.pos_at_screen_coords(&editor.documents[&view.doc], row, column)
+ view.pos_at_screen_coords(&editor.documents[&view.doc], row, column, true)
.map(|pos| (pos, view.id))
})
};
@@ -1191,8 +1060,10 @@ impl EditorView {
None => return EventResult::Ignored(None),
};
- let line = coords.row + view.offset.row;
- if line < doc.text().len_lines() {
+ if let Some(char_idx) =
+ view.pos_at_visual_coords(doc, coords.row as u16, coords.col as u16, true)
+ {
+ let line = doc.text().char_to_line(char_idx);
commands::dap_toggle_breakpoint_impl(cxt, path, line);
return EventResult::Consumed(None);
}
@@ -1204,7 +1075,7 @@ impl EditorView {
MouseEventKind::Drag(MouseButton::Left) => {
let (view, doc) = current!(cxt.editor);
- let pos = match view.pos_at_screen_coords(doc, row, column) {
+ let pos = match view.pos_at_screen_coords(doc, row, column, true) {
Some(pos) => pos,
None => return EventResult::Ignored(None),
};
@@ -1268,8 +1139,9 @@ impl EditorView {
cxt.editor.focus(view_id);
let (view, doc) = current!(cxt.editor);
- let line = coords.row + view.offset.row;
- if let Ok(pos) = doc.text().try_line_to_char(line) {
+ if let Some(pos) =
+ view.pos_at_visual_coords(doc, coords.row as u16, coords.col as u16, true)
+ {
doc.set_selection(view_id, Selection::point(pos));
if modifiers == KeyModifiers::ALT {
commands::MappableCommand::dap_edit_log.execute(cxt);
diff --git a/helix-term/src/ui/lsp.rs b/helix-term/src/ui/lsp.rs
index 393d24c4..44050aa1 100644
--- a/helix-term/src/ui/lsp.rs
+++ b/helix-term/src/ui/lsp.rs
@@ -53,7 +53,10 @@ impl Component for SignatureHelp {
let active_param_span = self.active_param_range.map(|(start, end)| {
vec![(
- cx.editor.theme.find_scope_index("ui.selection").unwrap(),
+ cx.editor
+ .theme
+ .find_scope_index_exact("ui.selection")
+ .unwrap(),
start..end,
)]
});
diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs
index eb480758..5e7f8c36 100644
--- a/helix-term/src/ui/mod.rs
+++ b/helix-term/src/ui/mod.rs
@@ -1,4 +1,5 @@
mod completion;
+mod document;
pub(crate) mod editor;
mod fuzzy_match;
mod info;
diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs
index 6bd64251..5fa75136 100644
--- a/helix-term/src/ui/picker.rs
+++ b/helix-term/src/ui/picker.rs
@@ -2,7 +2,12 @@ use crate::{
alt,
compositor::{Component, Compositor, Context, Event, EventResult},
ctrl, key, shift,
- ui::{self, fuzzy_match::FuzzyQuery, EditorView},
+ ui::{
+ self,
+ document::{render_document, LineDecoration, LinePos, TextRenderer},
+ fuzzy_match::FuzzyQuery,
+ EditorView,
+ },
};
use futures_util::future::BoxFuture;
use tui::{
@@ -19,11 +24,15 @@ use std::cmp::{self, Ordering};
use std::{collections::HashMap, io::Read, path::PathBuf};
use crate::ui::{Prompt, PromptEvent};
-use helix_core::{movement::Direction, unicode::segmentation::UnicodeSegmentation, Position};
+use helix_core::{
+ movement::Direction, text_annotations::TextAnnotations,
+ unicode::segmentation::UnicodeSegmentation, Position,
+};
use helix_view::{
editor::Action,
graphics::{CursorKind, Margin, Modifier, Rect},
theme::Style,
+ view::ViewPosition,
Document, DocumentId, Editor,
};
@@ -179,7 +188,7 @@ impl<T: Item> FilePicker<T> {
}
_ => {
// TODO: enable syntax highlighting; blocked by async rendering
- Document::open(path, None, None)
+ Document::open(path, None, None, editor.config.clone())
.map(|doc| CachedPreview::Document(Box::new(doc)))
.unwrap_or(CachedPreview::NotFound)
}
@@ -283,43 +292,57 @@ impl<T: Item + 'static> Component for FilePicker<T> {
})
.unwrap_or(0);
- let offset = Position::new(first_line, 0);
+ let offset = ViewPosition {
+ anchor: doc.text().line_to_char(first_line),
+ horizontal_offset: 0,
+ vertical_offset: 0,
+ };
- let mut highlights =
- EditorView::doc_syntax_highlights(doc, offset, area.height, &cx.editor.theme);
+ let mut highlights = EditorView::doc_syntax_highlights(
+ doc,
+ offset.anchor,
+ area.height,
+ &cx.editor.theme,
+ );
for spans in EditorView::doc_diagnostics_highlights(doc, &cx.editor.theme) {
if spans.is_empty() {
continue;
}
highlights = Box::new(helix_core::syntax::merge(highlights, spans));
}
- EditorView::render_text_highlights(
+ let mut decorations: Vec<Box<dyn LineDecoration>> = Vec::new();
+
+ if let Some((start, end)) = range {
+ let style = cx
+ .editor
+ .theme
+ .try_get("ui.highlight")
+ .unwrap_or_else(|| cx.editor.theme.get("ui.selection"));
+ let draw_highlight = move |renderer: &mut TextRenderer, pos: LinePos| {
+ if (start..=end).contains(&pos.doc_line) {
+ let area = Rect::new(
+ renderer.viewport.x,
+ renderer.viewport.y + pos.visual_line,
+ renderer.viewport.width,
+ 1,
+ );
+ renderer.surface.set_style(area, style)
+ }
+ };
+ decorations.push(Box::new(draw_highlight))
+ }
+
+ render_document(
+ surface,
+ inner,
doc,
offset,
- inner,
- surface,
- &cx.editor.theme,
+ &TextAnnotations::default(),
highlights,
- &cx.editor.config(),
+ &cx.editor.theme,
+ &mut decorations,
+ &mut [],
);
-
- // highlight the line
- if let Some((start, end)) = range {
- let offset = start.saturating_sub(first_line) as u16;
- surface.set_style(
- Rect::new(
- inner.x,
- inner.y + offset,
- inner.width,
- (end.saturating_sub(start) as u16 + 1)
- .min(inner.height.saturating_sub(offset)),
- ),
- cx.editor
- .theme
- .try_get("ui.highlight")
- .unwrap_or_else(|| cx.editor.theme.get("ui.selection")),
- );
- }
}
}