on master help sign in
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>

commit4dcf1fe66ba30a78edc054780d9b65c2f826530f authorPascal Kuthe <pascal.kuthe@semimod.de> dateTue 31 Jan 2023, 18:03 (4 years ago) parent4eca4b3
25 files changed+3481−750
book/src/configuration.md+22−1 book/src/languages.md+1−1 helix-core/src/doc_formatter.rs+384 helix-core/src/doc_formatter/test.rs+222 helix-core/src/graphemes.rs+182−1 helix-core/src/lib.rs+7−1 helix-core/src/movement.rs+180−22 helix-core/src/position.rs+427−1 helix-core/src/selection.rs+17−15 helix-core/src/text_annotations.rs+271 helix-term/src/application.rs+11−2 helix-term/src/commands.rs+162−66 helix-term/src/keymap/default.rs+12−6 helix-term/src/ui/completion.rs+4−2 helix-term/src/ui/document.rs+475 helix-term/src/ui/editor.rs+192−320 helix-term/src/ui/lsp.rs+4−1 helix-term/src/ui/mod.rs+1 helix-term/src/ui/picker.rs+52−29 helix-view/src/document.rs+61−13 helix-view/src/editor.rs+76−9 helix-view/src/gutter.rs+170−123 helix-view/src/lib.rs+15−8 helix-view/src/theme.rs+14−1 helix-view/src/view.rs+519−128
book/src/configuration.md+22−1
@@ -307,4 +307,25 @@ Currently unused
307307
308308#### `[editor.gutters.spacer]` Section
309309
310−Currently unused
310+Currently unused
311+
312+### `[editor.soft-wrap]` Section
313+
314+Options for soft wrapping lines that exceed the view width
315+
316+| Key | Description | Default |
317+| --- | --- | --- |
318+| `enable` | Whether soft wrapping is enabled. | `false` |
319+| `max-wrap` | Maximum free space left at the end of the line. | `20` |
320+| `max-indent-retain` | Maximum indentation to carry over when soft wrapping a line. | `40` |
321+| `wrap-indicator` | Text inserted before soft wrapped lines, highlighted with `ui.virtual.wrap` | `↪ ` |
322+
323+Example:
324+
325+```toml
326+[editor.soft-wrap]
327+enable = true
328+max-wrap = 25 # increase value to reduce forced mid-word wrapping
329+max-indent-retain = 0
330+wrap-indicator = "" # set wrap-indicator to "" to hide it
331+```
book/src/languages.md+1−1
@@ -61,7 +61,7 @@ These configuration keys are available:
6161| `config` | Language Server configuration |
6262| `grammar` | The tree-sitter grammar to use (defaults to the value of `name`) |
6363| `formatter` | The formatter for the language, it will take precedence over the lsp when defined. The formatter must be able to take the original file as input from stdin and write the formatted file to stdout |
64−| `max-line-length` | Maximum line length. Used for the `:reflow` command |
64+| `max-line-length` | Maximum line length. Used for the `:reflow` command and soft-wrapping |
6565
6666### File-type detection and the `file-types` key
6767
helix-core/src/doc_formatter.rsnew file+384
@@ -0,0 +1,384 @@
1+//! The `DocumentFormatter` forms the bridge between the raw document text
2+//! and onscreen positioning. It yields the text graphemes as an iterator
3+//! and traverses (part) of the document text. During that traversal it
4+//! handles grapheme detection, softwrapping and annotations.
5+//! It yields `FormattedGrapheme`s and their corresponding visual coordinates.
6+//!
7+//! As both virtual text and softwrapping can insert additional lines into the document
8+//! it is generally not possible to find the start of the previous visual line.
9+//! Instead the `DocumentFormatter` starts at the last "checkpoint" (usually a linebreak)
10+//! called a "block" and the caller must advance it as needed.
11+
12+use std::borrow::Cow;
13+use std::fmt::Debug;
14+use std::mem::{replace, take};
15+
16+#[cfg(test)]
17+mod test;
18+
19+use unicode_segmentation::{Graphemes, UnicodeSegmentation};
20+
21+use crate::graphemes::{Grapheme, GraphemeStr};
22+use crate::syntax::Highlight;
23+use crate::text_annotations::TextAnnotations;
24+use crate::{Position, RopeGraphemes, RopeSlice};
25+
26+/// TODO make Highlight a u32 to reduce the size of this enum to a single word.
27+#[derive(Debug, Clone, Copy)]
28+pub enum GraphemeSource {
29+ Document {
30+ codepoints: u32,
31+ },
32+ /// Inline virtual text can not be highlighted with a `Highlight` iterator
33+ /// because it's not part of the document. Instead the `Highlight`
34+ /// is emitted right by the document formatter
35+ VirtualText {
36+ highlight: Option<Highlight>,
37+ },
38+}
39+
40+#[derive(Debug, Clone)]
41+pub struct FormattedGrapheme<'a> {
42+ pub grapheme: Grapheme<'a>,
43+ pub source: GraphemeSource,
44+}
45+
46+impl<'a> FormattedGrapheme<'a> {
47+ pub fn new(
48+ g: GraphemeStr<'a>,
49+ visual_x: usize,
50+ tab_width: u16,
51+ source: GraphemeSource,
52+ ) -> FormattedGrapheme<'a> {
53+ FormattedGrapheme {
54+ grapheme: Grapheme::new(g, visual_x, tab_width),
55+ source,
56+ }
57+ }
58+ /// Returns whether this grapheme is virtual inline text
59+ pub fn is_virtual(&self) -> bool {
60+ matches!(self.source, GraphemeSource::VirtualText { .. })
61+ }
62+
63+ pub fn placeholder() -> Self {
64+ FormattedGrapheme {
65+ grapheme: Grapheme::Other { g: " ".into() },
66+ source: GraphemeSource::Document { codepoints: 0 },
67+ }
68+ }
69+
70+ pub fn doc_chars(&self) -> usize {
71+ match self.source {
72+ GraphemeSource::Document { codepoints } => codepoints as usize,
73+ GraphemeSource::VirtualText { .. } => 0,
74+ }
75+ }
76+
77+ pub fn is_whitespace(&self) -> bool {
78+ self.grapheme.is_whitespace()
79+ }
80+
81+ pub fn width(&self) -> usize {
82+ self.grapheme.width()
83+ }
84+
85+ pub fn is_word_boundary(&self) -> bool {
86+ self.grapheme.is_word_boundary()
87+ }
88+}
89+
90+#[derive(Debug, Clone)]
91+pub struct TextFormat {
92+ pub soft_wrap: bool,
93+ pub tab_width: u16,
94+ pub max_wrap: u16,
95+ pub max_indent_retain: u16,
96+ pub wrap_indicator: Box<str>,
97+ pub wrap_indicator_highlight: Option<Highlight>,
98+ pub viewport_width: u16,
99+}
100+
101+// test implementation is basically only used for testing or when softwrap is always disabled
102+impl Default for TextFormat {
103+ fn default() -> Self {
104+ TextFormat {
105+ soft_wrap: false,
106+ tab_width: 4,
107+ max_wrap: 3,
108+ max_indent_retain: 4,
109+ wrap_indicator: Box::from(" "),
110+ viewport_width: 17,
111+ wrap_indicator_highlight: None,
112+ }
113+ }
114+}
115+
116+#[derive(Debug)]
117+pub struct DocumentFormatter<'t> {
118+ text_fmt: &'t TextFormat,
119+ annotations: &'t TextAnnotations,
120+
121+ /// The visual position at the end of the last yielded word boundary
122+ visual_pos: Position,
123+ graphemes: RopeGraphemes<'t>,
124+ /// The character pos of the `graphemes` iter used for inserting annotations
125+ char_pos: usize,
126+ /// The line pos of the `graphemes` iter used for inserting annotations
127+ line_pos: usize,
128+ exhausted: bool,
129+
130+ /// Line breaks to be reserved for virtual text
131+ /// at the next line break
132+ virtual_lines: usize,
133+ inline_anntoation_graphemes: Option<(Graphemes<'t>, Option<Highlight>)>,
134+
135+ // softwrap specific
136+ /// The indentation of the current line
137+ /// Is set to `None` if the indentation level is not yet known
138+ /// because no non-whitespace graphemes have been encountered yet
139+ indent_level: Option<usize>,
140+ /// In case a long word needs to be split a single grapheme might need to be wrapped
141+ /// while the rest of the word stays on the same line
142+ peeked_grapheme: Option<(FormattedGrapheme<'t>, usize)>,
143+ /// A first-in first-out (fifo) buffer for the Graphemes of any given word
144+ word_buf: Vec<FormattedGrapheme<'t>>,
145+ /// The index of the next grapheme that will be yielded from the `word_buf`
146+ word_i: usize,
147+}
148+
149+impl<'t> DocumentFormatter<'t> {
150+ /// Creates a new formatter at the last block before `char_idx`.
151+ /// A block is a chunk which always ends with a linebreak.
152+ /// This is usually just a normal line break.
153+ /// However very long lines are always wrapped at constant intervals that can be cheaply calculated
154+ /// to avoid pathological behaviour.
155+ pub fn new_at_prev_checkpoint(
156+ text: RopeSlice<'t>,
157+ text_fmt: &'t TextFormat,
158+ annotations: &'t TextAnnotations,
159+ char_idx: usize,
160+ ) -> (Self, usize) {
161+ // TODO divide long lines into blocks to avoid bad performance for long lines
162+ let block_line_idx = text.char_to_line(char_idx.min(text.len_chars()));
163+ let block_char_idx = text.line_to_char(block_line_idx);
164+ annotations.reset_pos(block_char_idx);
165+ (
166+ DocumentFormatter {
167+ text_fmt,
168+ annotations,
169+ visual_pos: Position { row: 0, col: 0 },
170+ graphemes: RopeGraphemes::new(text.slice(block_char_idx..)),
171+ char_pos: block_char_idx,
172+ exhausted: false,
173+ virtual_lines: 0,
174+ indent_level: None,
175+ peeked_grapheme: None,
176+ word_buf: Vec::with_capacity(64),
177+ word_i: 0,
178+ line_pos: block_line_idx,
179+ inline_anntoation_graphemes: None,
180+ },
181+ block_char_idx,
182+ )
183+ }
184+
185+ fn next_inline_annotation_grapheme(&mut self) -> Option<(&'t str, Option<Highlight>)> {
186+ loop {
187+ if let Some(&mut (ref mut annotation, highlight)) =
188+ self.inline_anntoation_graphemes.as_mut()
189+ {
190+ if let Some(grapheme) = annotation.next() {
191+ return Some((grapheme, highlight));
192+ }
193+ }
194+
195+ if let Some((annotation, highlight)) =
196+ self.annotations.next_inline_annotation_at(self.char_pos)
197+ {
198+ self.inline_anntoation_graphemes = Some((
199+ UnicodeSegmentation::graphemes(&*annotation.text, true),
200+ highlight,
201+ ))
202+ } else {
203+ return None;
204+ }
205+ }
206+ }
207+
208+ fn advance_grapheme(&mut self, col: usize) -> Option<FormattedGrapheme<'t>> {
209+ let (grapheme, source) =
210+ if let Some((grapheme, highlight)) = self.next_inline_annotation_grapheme() {
211+ (grapheme.into(), GraphemeSource::VirtualText { highlight })
212+ } else if let Some(grapheme) = self.graphemes.next() {
213+ self.virtual_lines += self.annotations.annotation_lines_at(self.char_pos);
214+ let codepoints = grapheme.len_chars() as u32;
215+
216+ let overlay = self.annotations.overlay_at(self.char_pos);
217+ let grapheme = match overlay {
218+ Some((overlay, _)) => overlay.grapheme.as_str().into(),
219+ None => Cow::from(grapheme).into(),
220+ };
221+
222+ self.char_pos += codepoints as usize;
223+ (grapheme, GraphemeSource::Document { codepoints })
224+ } else {
225+ if self.exhausted {
226+ return None;
227+ }
228+ self.exhausted = true;
229+ // EOF grapheme is required for rendering
230+ // and correct position computations
231+ return Some(FormattedGrapheme {
232+ grapheme: Grapheme::Other { g: " ".into() },
233+ source: GraphemeSource::Document { codepoints: 0 },
234+ });
235+ };
236+
237+ let grapheme = FormattedGrapheme::new(grapheme, col, self.text_fmt.tab_width, source);
238+
239+ Some(grapheme)
240+ }
241+
242+ /// Move a word to the next visual line
243+ fn wrap_word(&mut self, virtual_lines_before_word: usize) -> usize {
244+ // softwrap this word to the next line
245+ let indent_carry_over = if let Some(indent) = self.indent_level {
246+ if indent as u16 <= self.text_fmt.max_indent_retain {
247+ indent as u16
248+ } else {
249+ 0
250+ }
251+ } else {
252+ // ensure the indent stays 0
253+ self.indent_level = Some(0);
254+ 0
255+ };
256+
257+ self.visual_pos.col = indent_carry_over as usize;
258+ self.virtual_lines -= virtual_lines_before_word;
259+ self.visual_pos.row += 1 + virtual_lines_before_word;
260+ let mut i = 0;
261+ let mut word_width = 0;
262+ let wrap_indicator = UnicodeSegmentation::graphemes(&*self.text_fmt.wrap_indicator, true)
263+ .map(|g| {
264+ i += 1;
265+ let grapheme = FormattedGrapheme::new(
266+ g.into(),
267+ self.visual_pos.col + word_width,
268+ self.text_fmt.tab_width,
269+ GraphemeSource::VirtualText {
270+ highlight: self.text_fmt.wrap_indicator_highlight,
271+ },
272+ );
273+ word_width += grapheme.width();
274+ grapheme
275+ });
276+ self.word_buf.splice(0..0, wrap_indicator);
277+
278+ for grapheme in &mut self.word_buf[i..] {
279+ let visual_x = self.visual_pos.col + word_width;
280+ grapheme
281+ .grapheme
282+ .change_position(visual_x, self.text_fmt.tab_width);
283+ word_width += grapheme.width();
284+ }
285+ word_width
286+ }
287+
288+ fn advance_to_next_word(&mut self) {
289+ self.word_buf.clear();
290+ let mut word_width = 0;
291+ let virtual_lines_before_word = self.virtual_lines;
292+ let mut virtual_lines_before_grapheme = self.virtual_lines;
293+
294+ loop {
295+ // softwrap word if necessary
296+ if word_width + self.visual_pos.col >= self.text_fmt.viewport_width as usize {
297+ // wrapping this word would move too much text to the next line
298+ // split the word at the line end instead
299+ if word_width > self.text_fmt.max_wrap as usize {
300+ // Usually we stop accomulating graphemes as soon as softwrapping becomes necessary.
301+ // However if the last grapheme is multiple columns wide it might extend beyond the EOL.
302+ // The condition below ensures that this grapheme is not cutoff and instead wrapped to the next line
303+ if word_width + self.visual_pos.col > self.text_fmt.viewport_width as usize {
304+ self.peeked_grapheme = self.word_buf.pop().map(|grapheme| {
305+ (grapheme, self.virtual_lines - virtual_lines_before_grapheme)
306+ });
307+ self.virtual_lines = virtual_lines_before_grapheme;
308+ }
309+ return;
310+ }
311+
312+ word_width = self.wrap_word(virtual_lines_before_word);
313+ }
314+
315+ virtual_lines_before_grapheme = self.virtual_lines;
316+
317+ let grapheme = if let Some((grapheme, virtual_lines)) = self.peeked_grapheme.take() {
318+ self.virtual_lines += virtual_lines;
319+ grapheme
320+ } else if let Some(grapheme) = self.advance_grapheme(self.visual_pos.col + word_width) {
321+ grapheme
322+ } else {
323+ return;
324+ };
325+
326+ // Track indentation
327+ if !grapheme.is_whitespace() && self.indent_level.is_none() {
328+ self.indent_level = Some(self.visual_pos.col);
329+ } else if grapheme.grapheme == Grapheme::Newline {
330+ self.indent_level = None;
331+ }
332+
333+ let is_word_boundary = grapheme.is_word_boundary();
334+ word_width += grapheme.width();
335+ self.word_buf.push(grapheme);
336+
337+ if is_word_boundary {
338+ return;
339+ }
340+ }
341+ }
342+
343+ /// returns the document line pos of the **next** grapheme that will be yielded
344+ pub fn line_pos(&self) -> usize {
345+ self.line_pos
346+ }
347+
348+ /// returns the visual pos of the **next** grapheme that will be yielded
349+ pub fn visual_pos(&self) -> Position {
350+ self.visual_pos
351+ }
352+}
353+
354+impl<'t> Iterator for DocumentFormatter<'t> {
355+ type Item = (FormattedGrapheme<'t>, Position);
356+
357+ fn next(&mut self) -> Option<Self::Item> {
358+ let grapheme = if self.text_fmt.soft_wrap {
359+ if self.word_i >= self.word_buf.len() {
360+ self.advance_to_next_word();
361+ self.word_i = 0;
362+ }
363+ let grapheme = replace(
364+ self.word_buf.get_mut(self.word_i)?,
365+ FormattedGrapheme::placeholder(),
366+ );
367+ self.word_i += 1;
368+ grapheme
369+ } else {
370+ self.advance_grapheme(self.visual_pos.col)?
371+ };
372+
373+ let pos = self.visual_pos;
374+ if grapheme.grapheme == Grapheme::Newline {
375+ self.visual_pos.row += 1;
376+ self.visual_pos.row += take(&mut self.virtual_lines);
377+ self.visual_pos.col = 0;
378+ self.line_pos += 1;
379+ } else {
380+ self.visual_pos.col += grapheme.width();
381+ }
382+ Some((grapheme, pos))
383+ }
384+}
helix-core/src/doc_formatter/test.rsnew file+222
@@ -0,0 +1,222 @@
1+use std::rc::Rc;
2+
3+use crate::doc_formatter::{DocumentFormatter, TextFormat};
4+use crate::text_annotations::{InlineAnnotation, Overlay, TextAnnotations};
5+
6+impl TextFormat {
7+ fn new_test(softwrap: bool) -> Self {
8+ TextFormat {
9+ soft_wrap: softwrap,
10+ tab_width: 2,
11+ max_wrap: 3,
12+ max_indent_retain: 4,
13+ wrap_indicator: ".".into(),
14+ wrap_indicator_highlight: None,
15+ // use a prime number to allow lining up too often with repeat
16+ viewport_width: 17,
17+ }
18+ }
19+}
20+
21+impl<'t> DocumentFormatter<'t> {
22+ fn collect_to_str(&mut self) -> String {
23+ use std::fmt::Write;
24+ let mut res = String::new();
25+ let viewport_width = self.text_fmt.viewport_width;
26+ let mut line = 0;
27+
28+ for (grapheme, pos) in self {
29+ if pos.row != line {
30+ line += 1;
31+ assert_eq!(pos.row, line);
32+ write!(res, "\n{}", ".".repeat(pos.col)).unwrap();
33+ assert!(
34+ pos.col <= viewport_width as usize,
35+ "softwrapped failed {}<={viewport_width}",
36+ pos.col
37+ );
38+ }
39+ write!(res, "{}", grapheme.grapheme).unwrap();
40+ }
41+
42+ res
43+ }
44+}
45+
46+fn softwrap_text(text: &str) -> String {
47+ DocumentFormatter::new_at_prev_checkpoint(
48+ text.into(),
49+ &TextFormat::new_test(true),
50+ &TextAnnotations::default(),
51+ 0,
52+ )
53+ .0
54+ .collect_to_str()
55+}
56+
57+#[test]
58+fn basic_softwrap() {
59+ assert_eq!(
60+ softwrap_text(&"foo ".repeat(10)),
61+ "foo foo foo foo \n.foo foo foo foo \n.foo foo "
62+ );
63+ assert_eq!(
64+ softwrap_text(&"fooo ".repeat(10)),
65+ "fooo fooo fooo \n.fooo fooo fooo \n.fooo fooo fooo \n.fooo "
66+ );
67+
68+ // check that we don't wrap unnecessarily
69+ assert_eq!(softwrap_text("\t\txxxx1xxxx2xx\n"), " xxxx1xxxx2xx \n ");
70+}
71+
72+#[test]
73+fn softwrap_indentation() {
74+ assert_eq!(
75+ softwrap_text("\t\tfoo1 foo2 foo3 foo4 foo5 foo6\n"),
76+ " foo1 foo2 \n.....foo3 foo4 \n.....foo5 foo6 \n "
77+ );
78+ assert_eq!(
79+ softwrap_text("\t\t\tfoo1 foo2 foo3 foo4 foo5 foo6\n"),
80+ " foo1 foo2 \n.foo3 foo4 foo5 \n.foo6 \n "
81+ );
82+}
83+
84+#[test]
85+fn long_word_softwrap() {
86+ assert_eq!(
87+ softwrap_text("\t\txxxx1xxxx2xxxx3xxxx4xxxx5xxxx6xxxx7xxxx8xxxx9xxx\n"),
88+ " xxxx1xxxx2xxx\n.....x3xxxx4xxxx5\n.....xxxx6xxxx7xx\n.....xx8xxxx9xxx \n "
89+ );
90+ assert_eq!(
91+ softwrap_text("xxxxxxxx1xxxx2xxx\n"),
92+ "xxxxxxxx1xxxx2xxx\n. \n "
93+ );
94+ assert_eq!(
95+ softwrap_text("\t\txxxx1xxxx 2xxxx3xxxx4xxxx5xxxx6xxxx7xxxx8xxxx9xxx\n"),
96+ " xxxx1xxxx \n.....2xxxx3xxxx4x\n.....xxx5xxxx6xxx\n.....x7xxxx8xxxx9\n.....xxx \n "
97+ );
98+ assert_eq!(
99+ softwrap_text("\t\txxxx1xxx 2xxxx3xxxx4xxxx5xxxx6xxxx7xxxx8xxxx9xxx\n"),
100+ " xxxx1xxx 2xxx\n.....x3xxxx4xxxx5\n.....xxxx6xxxx7xx\n.....xx8xxxx9xxx \n "
101+ );
102+}
103+
104+fn overlay_text(text: &str, char_pos: usize, softwrap: bool, overlays: &[Overlay]) -> String {
105+ DocumentFormatter::new_at_prev_checkpoint(
106+ text.into(),
107+ &TextFormat::new_test(softwrap),
108+ TextAnnotations::default().add_overlay(overlays.into(), None),
109+ char_pos,
110+ )
111+ .0
112+ .collect_to_str()
113+}
114+
115+#[test]
116+fn overlay() {
117+ assert_eq!(
118+ overlay_text(
119+ "foobar",
120+ 0,
121+ false,
122+ &[
123+ Overlay {
124+ char_idx: 0,
125+ grapheme: "X".into(),
126+ },
127+ Overlay {
128+ char_idx: 2,
129+ grapheme: "\t".into(),
130+ },
131+ ]
132+ ),
133+ "Xo bar "
134+ );
135+ assert_eq!(
136+ overlay_text(
137+ &"foo ".repeat(10),
138+ 0,
139+ true,
140+ &[
141+ Overlay {
142+ char_idx: 2,
143+ grapheme: "\t".into(),
144+ },
145+ Overlay {
146+ char_idx: 5,
147+ grapheme: "\t".into(),
148+ },
149+ Overlay {
150+ char_idx: 16,
151+ grapheme: "X".into(),
152+ },
153+ ]
154+ ),
155+ "fo f o foo \n.foo Xoo foo foo \n.foo foo foo "
156+ );
157+}
158+
159+fn annotate_text(text: &str, softwrap: bool, annotations: &[InlineAnnotation]) -> String {
160+ DocumentFormatter::new_at_prev_checkpoint(
161+ text.into(),
162+ &TextFormat::new_test(softwrap),
163+ TextAnnotations::default().add_inline_annotations(annotations.into(), None),
164+ 0,
165+ )
166+ .0
167+ .collect_to_str()
168+}
169+
170+#[test]
171+fn annotation() {
172+ assert_eq!(
173+ annotate_text(
174+ "bar",
175+ false,
176+ &[InlineAnnotation {
177+ char_idx: 0,
178+ text: "foo".into(),
179+ }]
180+ ),
181+ "foobar "
182+ );
183+ assert_eq!(
184+ annotate_text(
185+ &"foo ".repeat(10),
186+ true,
187+ &[InlineAnnotation {
188+ char_idx: 0,
189+ text: "foo ".into(),
190+ }]
191+ ),
192+ "foo foo foo foo \n.foo foo foo foo \n.foo foo foo "
193+ );
194+}
195+#[test]
196+fn annotation_and_overlay() {
197+ assert_eq!(
198+ DocumentFormatter::new_at_prev_checkpoint(
199+ "bbar".into(),
200+ &TextFormat::new_test(false),
201+ TextAnnotations::default()
202+ .add_inline_annotations(
203+ Rc::new([InlineAnnotation {
204+ char_idx: 0,
205+ text: "fooo".into(),
206+ }]),
207+ None
208+ )
209+ .add_overlay(
210+ Rc::new([Overlay {
211+ char_idx: 0,
212+ grapheme: "\t".into(),
213+ }]),
214+ None
215+ ),
216+ 0,
217+ )
218+ .0
219+ .collect_to_str(),
220+ "fooo bar "
221+ );
222+}
helix-core/src/graphemes.rs+182−1
@@ -5,7 +5,88 @@ use ropey::{iter::Chunks, str_utils::byte_to_char_idx, RopeSlice};
55use unicode_segmentation::{GraphemeCursor, GraphemeIncomplete};
66use unicode_width::UnicodeWidthStr;
77
8−use std::fmt;
8+use std::borrow::Cow;
9+use std::fmt::{self, Debug, Display};
10+use std::marker::PhantomData;
11+use std::ops::Deref;
12+use std::ptr::NonNull;
13+use std::{slice, str};
14+
15+use crate::chars::{char_is_whitespace, char_is_word};
16+use crate::LineEnding;
17+
18+#[inline]
19+pub fn tab_width_at(visual_x: usize, tab_width: u16) -> usize {
20+ tab_width as usize - (visual_x % tab_width as usize)
21+}
22+
23+#[derive(Debug, Clone, PartialEq, Eq)]
24+pub enum Grapheme<'a> {
25+ Newline,
26+ Tab { width: usize },
27+ Other { g: GraphemeStr<'a> },
28+}
29+
30+impl<'a> Grapheme<'a> {
31+ pub fn new(g: GraphemeStr<'a>, visual_x: usize, tab_width: u16) -> Grapheme<'a> {
32+ match g {
33+ g if g == "\t" => Grapheme::Tab {
34+ width: tab_width_at(visual_x, tab_width),
35+ },
36+ _ if LineEnding::from_str(&g).is_some() => Grapheme::Newline,
37+ _ => Grapheme::Other { g },
38+ }
39+ }
40+
41+ pub fn change_position(&mut self, visual_x: usize, tab_width: u16) {
42+ if let Grapheme::Tab { width } = self {
43+ *width = tab_width_at(visual_x, tab_width)
44+ }
45+ }
46+
47+ /// Returns the a visual width of this grapheme,
48+ #[inline]
49+ pub fn width(&self) -> usize {
50+ match *self {
51+ // width is not cached because we are dealing with
52+ // ASCII almost all the time which already has a fastpath
53+ // it's okay to convert to u16 here because no codepoint has a width larger
54+ // than 2 and graphemes are usually atmost two visible codepoints wide
55+ Grapheme::Other { ref g } => grapheme_width(g),
56+ Grapheme::Tab { width } => width,
57+ Grapheme::Newline => 1,
58+ }
59+ }
60+
61+ pub fn is_whitespace(&self) -> bool {
62+ !matches!(&self, Grapheme::Other { g } if !g.chars().all(char_is_whitespace))
63+ }
64+
65+ // TODO currently word boundaries are used for softwrapping.
66+ // This works best for programming languages and well for prose.
67+ // This could however be improved in the future by considering unicode
68+ // character classes but
69+ pub fn is_word_boundary(&self) -> bool {
70+ !matches!(&self, Grapheme::Other { g,.. } if g.chars().all(char_is_word))
71+ }
72+}
73+
74+impl Display for Grapheme<'_> {
75+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76+ match *self {
77+ Grapheme::Newline => write!(f, " "),
78+ Grapheme::Tab { width } => {
79+ for _ in 0..width {
80+ write!(f, " ")?;
81+ }
82+ Ok(())
83+ }
84+ Grapheme::Other { ref g } => {
85+ write!(f, "{g}")
86+ }
87+ }
88+ }
89+}
990
1091#[must_use]
1192pub fn grapheme_width(g: &str) -> usize {
@@ -27,6 +108,8 @@ pub fn grapheme_width(g: &str) -> usize {
27108 // We use max(1) here because all grapeheme clusters--even illformed
28109 // ones--should have at least some width so they can be edited
29110 // properly.
111+ // TODO properly handle unicode width for all codepoints
112+ // example of where unicode width is currently wrong: 🤦🏼‍♂️ (taken from https://hsivonen.fi/string-length/)
30113 UnicodeWidthStr::width(g).max(1)
31114 }
32115}
@@ -341,3 +424,101 @@ impl<'a> Iterator for RopeGraphemes<'a> {
341424 }
342425 }
343426}
427+
428+/// A highly compressed Cow<'a, str> that holds
429+/// atmost u31::MAX bytes and is readonly
430+pub struct GraphemeStr<'a> {
431+ ptr: NonNull<u8>,
432+ len: u32,
433+ phantom: PhantomData<&'a str>,
434+}
435+
436+impl GraphemeStr<'_> {
437+ const MASK_OWNED: u32 = 1 << 31;
438+
439+ fn compute_len(&self) -> usize {
440+ (self.len & !Self::MASK_OWNED) as usize
441+ }
442+}
443+
444+impl Deref for GraphemeStr<'_> {
445+ type Target = str;
446+ fn deref(&self) -> &Self::Target {
447+ unsafe {
448+ let bytes = slice::from_raw_parts(self.ptr.as_ptr(), self.compute_len());
449+ str::from_utf8_unchecked(bytes)
450+ }
451+ }
452+}
453+
454+impl Drop for GraphemeStr<'_> {
455+ fn drop(&mut self) {
456+ if self.len & Self::MASK_OWNED != 0 {
457+ // free allocation
458+ unsafe {
459+ drop(Box::from_raw(slice::from_raw_parts_mut(
460+ self.ptr.as_ptr(),
461+ self.compute_len(),
462+ )));
463+ }
464+ }
465+ }
466+}
467+
468+impl<'a> From<&'a str> for GraphemeStr<'a> {
469+ fn from(g: &'a str) -> Self {
470+ GraphemeStr {
471+ ptr: unsafe { NonNull::new_unchecked(g.as_bytes().as_ptr() as *mut u8) },
472+ len: i32::try_from(g.len()).unwrap() as u32,
473+ phantom: PhantomData,
474+ }
475+ }
476+}
477+
478+impl<'a> From<String> for GraphemeStr<'a> {
479+ fn from(g: String) -> Self {
480+ let len = g.len();
481+ let ptr = Box::into_raw(g.into_bytes().into_boxed_slice()) as *mut u8;
482+ GraphemeStr {
483+ ptr: unsafe { NonNull::new_unchecked(ptr) },
484+ len: i32::try_from(len).unwrap() as u32,
485+ phantom: PhantomData,
486+ }
487+ }
488+}
489+
490+impl<'a> From<Cow<'a, str>> for GraphemeStr<'a> {
491+ fn from(g: Cow<'a, str>) -> Self {
492+ match g {
493+ Cow::Borrowed(g) => g.into(),
494+ Cow::Owned(g) => g.into(),
495+ }
496+ }
497+}
498+
499+impl<T: Deref<Target = str>> PartialEq<T> for GraphemeStr<'_> {
500+ fn eq(&self, other: &T) -> bool {
501+ self.deref() == other.deref()
502+ }
503+}
504+impl PartialEq<str> for GraphemeStr<'_> {
505+ fn eq(&self, other: &str) -> bool {
506+ self.deref() == other
507+ }
508+}
509+impl Eq for GraphemeStr<'_> {}
510+impl Debug for GraphemeStr<'_> {
511+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
512+ Debug::fmt(self.deref(), f)
513+ }
514+}
515+impl Display for GraphemeStr<'_> {
516+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
517+ Display::fmt(self.deref(), f)
518+ }
519+}
520+impl Clone for GraphemeStr<'_> {
521+ fn clone(&self) -> Self {
522+ self.deref().to_owned().into()
523+ }
524+}
helix-core/src/lib.rs+7−1
@@ -6,6 +6,7 @@ pub mod comment;
66pub mod config;
77pub mod diagnostic;
88pub mod diff;
9+pub mod doc_formatter;
910pub mod graphemes;
1011pub mod history;
1112pub mod increment;
@@ -24,6 +25,7 @@ pub mod shellwords;
2425pub mod surround;
2526pub mod syntax;
2627pub mod test;
28+pub mod text_annotations;
2729pub mod textobject;
2830mod transaction;
2931pub mod wrap;
@@ -95,8 +97,12 @@ pub use {regex, tree_sitter};
9597
9698pub use graphemes::RopeGraphemes;
9799pub use position::{
98− coords_at_pos, pos_at_coords, pos_at_visual_coords, visual_coords_at_pos, Position,
100+ char_idx_at_visual_offset, coords_at_pos, pos_at_coords, visual_offset_from_anchor,
101+ visual_offset_from_block, Position,
99102};
103+#[allow(deprecated)]
104+pub use position::{pos_at_visual_coords, visual_coords_at_pos};
105+
100106pub use selection::{Range, Selection};
101107pub use smallvec::{smallvec, SmallVec};
102108pub use syntax::Syntax;
helix-core/src/movement.rs+180−22
@@ -4,16 +4,19 @@ use ropey::iter::Chars;
44use tree_sitter::{Node, QueryCursor};
55
66use crate::{
7+ char_idx_at_visual_offset,
78 chars::{categorize_char, char_is_line_ending, CharCategory},
9+ doc_formatter::TextFormat,
810 graphemes::{
911 next_grapheme_boundary, nth_next_grapheme_boundary, nth_prev_grapheme_boundary,
1012 prev_grapheme_boundary,
1113 },
1214 line_ending::rope_is_line_ending,
13− pos_at_visual_coords,
15+ position::char_idx_at_visual_block_offset,
1416 syntax::LanguageConfiguration,
17+ text_annotations::TextAnnotations,
1518 textobject::TextObject,
16− visual_coords_at_pos, Position, Range, RopeSlice,
19+ visual_offset_from_block, Range, RopeSlice,
1720};
1821
1922#[derive(Debug, Copy, Clone, PartialEq, Eq)]
@@ -34,7 +37,8 @@ pub fn move_horizontally(
3437 dir: Direction,
3538 count: usize,
3639 behaviour: Movement,
37− _: usize,
40+ _: &TextFormat,
41+ _: &mut TextAnnotations,
3842) -> Range {
3943 let pos = range.cursor(slice);
4044
@@ -48,35 +52,116 @@ pub fn move_horizontally(
4852 range.put_cursor(slice, new_pos, behaviour == Movement::Extend)
4953}
5054
55+pub fn move_vertically_visual(
56+ slice: RopeSlice,
57+ range: Range,
58+ dir: Direction,
59+ count: usize,
60+ behaviour: Movement,
61+ text_fmt: &TextFormat,
62+ annotations: &mut TextAnnotations,
63+) -> Range {
64+ if !text_fmt.soft_wrap {
65+ move_vertically(slice, range, dir, count, behaviour, text_fmt, annotations);
66+ }
67+ annotations.clear_line_annotations();
68+ let pos = range.cursor(slice);
69+
70+ // Compute the current position's 2d coordinates.
71+ let (visual_pos, block_off) = visual_offset_from_block(slice, pos, pos, text_fmt, annotations);
72+ let new_col = range
73+ .old_visual_position
74+ .map_or(visual_pos.col as u32, |(_, col)| col);
75+
76+ // Compute the new position.
77+ let mut row_off = match dir {
78+ Direction::Forward => count as isize,
79+ Direction::Backward => -(count as isize),
80+ };
81+
82+ // TODO how to handle inline annotations that span an entire visual line (very unlikely).
83+
84+ // Compute visual offset relative to block start to avoid trasversing the block twice
85+ row_off += visual_pos.row as isize;
86+ let new_pos = char_idx_at_visual_offset(
87+ slice,
88+ block_off,
89+ row_off,
90+ new_col as usize,
91+ text_fmt,
92+ annotations,
93+ )
94+ .0;
95+
96+ // Special-case to avoid moving to the end of the last non-empty line.
97+ if behaviour == Movement::Extend && slice.line(slice.char_to_line(new_pos)).len_chars() == 0 {
98+ return range;
99+ }
100+
101+ let mut new_range = range.put_cursor(slice, new_pos, behaviour == Movement::Extend);
102+ new_range.old_visual_position = Some((0, new_col));
103+ new_range
104+}
105+
51106pub fn move_vertically(
52107 slice: RopeSlice,
53108 range: Range,
54109 dir: Direction,
55110 count: usize,
56111 behaviour: Movement,
57− tab_width: usize,
112+ text_fmt: &TextFormat,
113+ annotations: &mut TextAnnotations,
58114) -> Range {
115+ annotations.clear_line_annotations();
59116 let pos = range.cursor(slice);
117+ let line_idx = slice.char_to_line(pos);
118+ let line_start = slice.line_to_char(line_idx);
60119
61120 // Compute the current position's 2d coordinates.
62− let Position { row, col } = visual_coords_at_pos(slice, pos, tab_width);
63− let horiz = range.horiz.unwrap_or(col as u32);
121+ let visual_pos = visual_offset_from_block(slice, line_start, pos, text_fmt, annotations).0;
122+ let (mut new_row, new_col) = range
123+ .old_visual_position
124+ .map_or((visual_pos.row as u32, visual_pos.col as u32), |pos| pos);
125+ new_row = new_row.max(visual_pos.row as u32);
126+ let line_idx = slice.char_to_line(pos);
64127
65128 // Compute the new position.
66− let new_row = match dir {
67− Direction::Forward => (row + count).min(slice.len_lines().saturating_sub(1)),
68− Direction::Backward => row.saturating_sub(count),
129+ let mut new_line_idx = match dir {
130+ Direction::Forward => line_idx.saturating_add(count),
131+ Direction::Backward => line_idx.saturating_sub(count),
69132 };
70− let new_col = col.max(horiz as usize);
71− let new_pos = pos_at_visual_coords(slice, Position::new(new_row, new_col), tab_width);
133+
134+ let line = if new_line_idx >= slice.len_lines() - 1 {
135+ // there is no line terminator for the last line
136+ // so the logic below is not necessary here
137+ new_line_idx = slice.len_lines() - 1;
138+ slice
139+ } else {
140+ // char_idx_at_visual_block_offset returns a one-past-the-end index
141+ // in case it reaches the end of the slice
142+ // to avoid moving to the nextline in that case the line terminator is removed from the line
143+ let new_line_end = prev_grapheme_boundary(slice, slice.line_to_char(new_line_idx + 1));
144+ slice.slice(..new_line_end)
145+ };
146+
147+ let new_line_start = line.line_to_char(new_line_idx);
148+
149+ let (new_pos, _) = char_idx_at_visual_block_offset(
150+ line,
151+ new_line_start,
152+ new_row as usize,
153+ new_col as usize,
154+ text_fmt,
155+ annotations,
156+ );
72157
73158 // Special-case to avoid moving to the end of the last non-empty line.
74− if behaviour == Movement::Extend && slice.line(new_row).len_chars() == 0 {
159+ if behaviour == Movement::Extend && slice.line(new_line_idx).len_chars() == 0 {
75160 return range;
76161 }
77162
78163 let mut new_range = range.put_cursor(slice, new_pos, behaviour == Movement::Extend);
79− new_range.horiz = Some(horiz);
164+ new_range.old_visual_position = Some((new_row, new_col));
80165 new_range
81166}
82167
@@ -473,7 +558,16 @@ mod test {
473558 assert_eq!(
474559 coords_at_pos(
475560 slice,
476− move_vertically(slice, range, Direction::Forward, 1, Movement::Move, 4).head
561+ move_vertically_visual(
562+ slice,
563+ range,
564+ Direction::Forward,
565+ 1,
566+ Movement::Move,
567+ &TextFormat::default(),
568+ &mut TextAnnotations::default(),
569+ )
570+ .head
477571 ),
478572 (1, 3).into()
479573 );
@@ -497,7 +591,15 @@ mod test {
497591 ];
498592
499593 for ((direction, amount), coordinates) in moves_and_expected_coordinates {
500− range = move_horizontally(slice, range, direction, amount, Movement::Move, 0);
594+ range = move_horizontally(
595+ slice,
596+ range,
597+ direction,
598+ amount,
599+ Movement::Move,
600+ &TextFormat::default(),
601+ &mut TextAnnotations::default(),
602+ );
501603 assert_eq!(coords_at_pos(slice, range.head), coordinates.into())
502604 }
503605 }
@@ -523,7 +625,15 @@ mod test {
523625 ];
524626
525627 for ((direction, amount), coordinates) in moves_and_expected_coordinates {
526− range = move_horizontally(slice, range, direction, amount, Movement::Move, 0);
628+ range = move_horizontally(
629+ slice,
630+ range,
631+ direction,
632+ amount,
633+ Movement::Move,
634+ &TextFormat::default(),
635+ &mut TextAnnotations::default(),
636+ );
527637 assert_eq!(coords_at_pos(slice, range.head), coordinates.into());
528638 assert_eq!(range.head, range.anchor);
529639 }
@@ -545,7 +655,15 @@ mod test {
545655 ];
546656
547657 for (direction, amount) in moves {
548− range = move_horizontally(slice, range, direction, amount, Movement::Extend, 0);
658+ range = move_horizontally(
659+ slice,
660+ range,
661+ direction,
662+ amount,
663+ Movement::Extend,
664+ &TextFormat::default(),
665+ &mut TextAnnotations::default(),
666+ );
549667 assert_eq!(range.anchor, original_anchor);
550668 }
551669 }
@@ -569,7 +687,15 @@ mod test {
569687 ];
570688
571689 for ((direction, amount), coordinates) in moves_and_expected_coordinates {
572− range = move_vertically(slice, range, direction, amount, Movement::Move, 4);
690+ range = move_vertically_visual(
691+ slice,
692+ range,
693+ direction,
694+ amount,
695+ Movement::Move,
696+ &TextFormat::default(),
697+ &mut TextAnnotations::default(),
698+ );
573699 assert_eq!(coords_at_pos(slice, range.head), coordinates.into());
574700 assert_eq!(range.head, range.anchor);
575701 }
@@ -603,8 +729,24 @@ mod test {
603729
604730 for ((axis, direction, amount), coordinates) in moves_and_expected_coordinates {
605731 range = match axis {
606− Axis::H => move_horizontally(slice, range, direction, amount, Movement::Move, 0),
607− Axis::V => move_vertically(slice, range, direction, amount, Movement::Move, 4),
732+ Axis::H => move_horizontally(
733+ slice,
734+ range,
735+ direction,
736+ amount,
737+ Movement::Move,
738+ &TextFormat::default(),
739+ &mut TextAnnotations::default(),
740+ ),
741+ Axis::V => move_vertically_visual(
742+ slice,
743+ range,
744+ direction,
745+ amount,
746+ Movement::Move,
747+ &TextFormat::default(),
748+ &mut TextAnnotations::default(),
749+ ),
608750 };
609751 assert_eq!(coords_at_pos(slice, range.head), coordinates.into());
610752 assert_eq!(range.head, range.anchor);
@@ -638,8 +780,24 @@ mod test {
638780
639781 for ((axis, direction, amount), coordinates) in moves_and_expected_coordinates {
640782 range = match axis {
641− Axis::H => move_horizontally(slice, range, direction, amount, Movement::Move, 0),
642− Axis::V => move_vertically(slice, range, direction, amount, Movement::Move, 4),
783+ Axis::H => move_horizontally(
784+ slice,
785+ range,
786+ direction,
787+ amount,
788+ Movement::Move,
789+ &TextFormat::default(),
790+ &mut TextAnnotations::default(),
791+ ),
792+ Axis::V => move_vertically_visual(
793+ slice,
794+ range,
795+ direction,
796+ amount,
797+ Movement::Move,
798+ &TextFormat::default(),
799+ &mut TextAnnotations::default(),
800+ ),
643801 };
644802 assert_eq!(coords_at_pos(slice, range.head), coordinates.into());
645803 assert_eq!(range.head, range.anchor);
helix-core/src/position.rs+427−1
@@ -1,9 +1,11 @@
1−use std::borrow::Cow;
1+use std::{borrow::Cow, cmp::Ordering};
22
33use crate::{
44 chars::char_is_line_ending,
5+ doc_formatter::{DocumentFormatter, TextFormat},
56 graphemes::{ensure_grapheme_boundary_prev, grapheme_width, RopeGraphemes},
67 line_ending::line_end_char_index,
8+ text_annotations::TextAnnotations,
79 RopeSlice,
810};
911
@@ -73,6 +75,13 @@ pub fn coords_at_pos(text: RopeSlice, pos: usize) -> Position {
7375/// Takes \t, double-width characters (CJK) into account as well as text
7476/// not in the document in the future.
7577/// See [`coords_at_pos`] for an "objective" one.
78+///
79+/// This function should be used very rarely. Usually `visual_offset_from_anchor`
80+/// or `visual_offset_from_block` is preferable. However when you want to compute the
81+/// actual visual row/column in the text (not what is actually shown on screen)
82+/// then you should use this function. For example aligning text should ignore virtual
83+/// text and softwrap.
84+#[deprecated = "Doesn't account for softwrap or decorations, use visual_offset_from_anchor instead"]
7685pub fn visual_coords_at_pos(text: RopeSlice, pos: usize, tab_width: usize) -> Position {
7786 let line = text.char_to_line(pos);
7887
@@ -93,6 +102,82 @@ pub fn visual_coords_at_pos(text: RopeSlice, pos: usize, tab_width: usize) -> Po
93102 Position::new(line, col)
94103}
95104
105+/// Returns the visual offset from the start of the first visual line
106+/// in the block that contains anchor.
107+/// Text is always wrapped at blocks, they usually correspond to
108+/// actual line breaks but for very long lines
109+/// softwrapping positions are estimated with an O(1) algorithm
110+/// to ensure consistent performance for large lines (currently unimplemented)
111+///
112+/// Usualy you want to use `visual_offset_from_anchor` instead but this function
113+/// can be useful (and faster) if
114+/// * You already know the visual position of the block
115+/// * You only care about the horizontal offset (column) and not the vertical offset (row)
116+pub fn visual_offset_from_block(
117+ text: RopeSlice,
118+ anchor: usize,
119+ pos: usize,
120+ text_fmt: &TextFormat,
121+ annotations: &TextAnnotations,
122+) -> (Position, usize) {
123+ let mut last_pos = Position::default();
124+ let (formatter, block_start) =
125+ DocumentFormatter::new_at_prev_checkpoint(text, text_fmt, annotations, anchor);
126+ let mut char_pos = block_start;
127+
128+ for (grapheme, vpos) in formatter {
129+ last_pos = vpos;
130+ char_pos += grapheme.doc_chars();
131+
132+ if char_pos > pos {
133+ return (last_pos, block_start);
134+ }
135+ }
136+
137+ (last_pos, block_start)
138+}
139+
140+/// Returns the visual offset from the start of the visual line
141+/// that contains anchor.
142+pub fn visual_offset_from_anchor(
143+ text: RopeSlice,
144+ anchor: usize,
145+ pos: usize,
146+ text_fmt: &TextFormat,
147+ annotations: &TextAnnotations,
148+ max_rows: usize,
149+) -> Option<(Position, usize)> {
150+ let (formatter, block_start) =
151+ DocumentFormatter::new_at_prev_checkpoint(text, text_fmt, annotations, anchor);
152+ let mut char_pos = block_start;
153+ let mut anchor_line = None;
154+ let mut last_pos = Position::default();
155+
156+ for (grapheme, vpos) in formatter {
157+ last_pos = vpos;
158+ char_pos += grapheme.doc_chars();
159+
160+ if char_pos > anchor && anchor_line.is_none() {
161+ anchor_line = Some(last_pos.row);
162+ }
163+ if char_pos > pos {
164+ last_pos.row -= anchor_line.unwrap();
165+ return Some((last_pos, block_start));
166+ }
167+
168+ if let Some(anchor_line) = anchor_line {
169+ if vpos.row >= anchor_line + max_rows {
170+ return None;
171+ }
172+ }
173+ }
174+
175+ let anchor_line = anchor_line.unwrap_or(last_pos.row);
176+ last_pos.row -= anchor_line;
177+
178+ Some((last_pos, block_start))
179+}
180+
96181/// Convert (line, column) coordinates to a character index.
97182///
98183/// If the `line` coordinate is beyond the end of the file, the EOF
@@ -140,6 +225,11 @@ pub fn pos_at_coords(text: RopeSlice, coords: Position, limit_before_line_ending
140225/// If the `column` coordinate is past the end of the given line, the
141226/// line-end position (in this case, just before the line ending
142227/// character) will be returned.
228+/// This function should be used very rarely. Usually `char_idx_at_visual_offset` is preferable.
229+/// However when you want to compute a char position from the visual row/column in the text
230+/// (not what is actually shown on screen) then you should use this function.
231+/// For example aligning text should ignore virtual text and softwrap.
232+#[deprecated = "Doesn't account for softwrap or decorations, use char_idx_at_visual_offset instead"]
143233pub fn pos_at_visual_coords(text: RopeSlice, coords: Position, tab_width: usize) -> usize {
144234 let Position { mut row, col } = coords;
145235 row = row.min(text.len_lines() - 1);
@@ -169,6 +259,120 @@ pub fn pos_at_visual_coords(text: RopeSlice, coords: Position, tab_width: usize)
169259 line_start + col_char_offset
170260}
171261
262+/// Returns the char index on the visual line `row_offset` below the visual line of
263+/// the provided char index `anchor` that is closest to the supplied visual `column`.
264+///
265+/// If the targeted visual line is entirely covered by virtual text the last
266+/// char position before the virtual text and a virtual offset is returned instead.
267+///
268+/// If no (text) grapheme starts at exactly at the specified column the
269+/// start of the grapheme to the left is returned. If there is no grapheme
270+/// to the left (for example if the line starts with virtual text) then the positiong
271+/// of the next grapheme to the right is returned.
272+///
273+/// If the `line` coordinate is beyond the end of the file, the EOF
274+/// position will be returned.
275+///
276+/// If the `column` coordinate is past the end of the given line, the
277+/// line-end position (in this case, just before the line ending
278+/// character) will be returned.
279+///
280+/// # Returns
281+///
282+/// `(real_char_idx, virtual_lines)`
283+///
284+/// The nearest character idx "closest" (see above) to the specified visual offset
285+/// on the visual line is returned if the visual line contains any text:
286+/// If the visual line at the specified offset is a virtual line generated by a `LineAnnotation`
287+/// the previous char_index is returned, together with the remaining vertical offset (`virtual_lines`)
288+pub fn char_idx_at_visual_offset<'a>(
289+ text: RopeSlice<'a>,
290+ mut anchor: usize,
291+ mut row_offset: isize,
292+ column: usize,
293+ text_fmt: &TextFormat,
294+ annotations: &TextAnnotations,
295+) -> (usize, usize) {
296+ // convert row relative to visual line containing anchor to row relative to a block containing anchor (anchor may change)
297+ loop {
298+ let (visual_pos_in_block, block_char_offset) =
299+ visual_offset_from_block(text, anchor, anchor, text_fmt, annotations);
300+ row_offset += visual_pos_in_block.row as isize;
301+ anchor = block_char_offset;
302+ if row_offset >= 0 {
303+ break;
304+ }
305+
306+ if block_char_offset == 0 {
307+ row_offset = 0;
308+ break;
309+ }
310+ // the row_offset is negative so we need to look at the previous block
311+ // set the anchor to the last char before the current block
312+ // this char index is also always a line earlier so increase the row_offset by 1
313+ anchor -= 1;
314+ row_offset += 1;
315+ }
316+
317+ char_idx_at_visual_block_offset(
318+ text,
319+ anchor,
320+ row_offset as usize,
321+ column,
322+ text_fmt,
323+ annotations,
324+ )
325+}
326+
327+/// This function behaves the same as `char_idx_at_visual_offset`, except that
328+/// the vertical offset `row` is always computed relative to the block that contains `anchor`
329+/// instead of the visual line that contains `anchor`.
330+/// Usually `char_idx_at_visual_offset` is more useful but this function can be
331+/// used in some situations as an optimization when `visual_offset_from_block` was used
332+///
333+/// # Returns
334+///
335+/// `(real_char_idx, virtual_lines)`
336+///
337+/// See `char_idx_at_visual_offset` for details
338+pub fn char_idx_at_visual_block_offset(
339+ text: RopeSlice,
340+ anchor: usize,
341+ row: usize,
342+ column: usize,
343+ text_fmt: &TextFormat,
344+ annotations: &TextAnnotations,
345+) -> (usize, usize) {
346+ let (formatter, mut char_idx) =
347+ DocumentFormatter::new_at_prev_checkpoint(text, text_fmt, annotations, anchor);
348+ let mut last_char_idx = char_idx;
349+ let mut last_char_idx_on_line = None;
350+ let mut last_row = 0;
351+ for (grapheme, grapheme_pos) in formatter {
352+ match grapheme_pos.row.cmp(&row) {
353+ Ordering::Equal => {
354+ if grapheme_pos.col + grapheme.width() > column {
355+ if !grapheme.is_virtual() {
356+ return (char_idx, 0);
357+ } else if let Some(char_idx) = last_char_idx_on_line {
358+ return (char_idx, 0);
359+ }
360+ } else if !grapheme.is_virtual() {
361+ last_char_idx_on_line = Some(char_idx)
362+ }
363+ }
364+ Ordering::Greater => return (last_char_idx, row - last_row),
365+ _ => (),
366+ }
367+
368+ last_char_idx = char_idx;
369+ last_row = grapheme_pos.row;
370+ char_idx += grapheme.doc_chars();
371+ }
372+
373+ (char_idx, 0)
374+}
375+
172376#[cfg(test)]
173377mod test {
174378 use super::*;
@@ -228,6 +432,7 @@ mod test {
228432 }
229433
230434 #[test]
435+ #[allow(deprecated)]
231436 fn test_visual_coords_at_pos() {
232437 let text = Rope::from("ḧëḷḷö\nẅöṛḷḋ");
233438 let slice = text.slice(..);
@@ -275,6 +480,130 @@ mod test {
275480 assert_eq!(visual_coords_at_pos(slice, 2, 8), (0, 9).into());
276481 }
277482
483+ #[test]
484+ fn test_visual_off_from_block() {
485+ let text = Rope::from("ḧëḷḷö\nẅöṛḷḋ");
486+ let slice = text.slice(..);
487+ let annot = TextAnnotations::default();
488+ let text_fmt = TextFormat::default();
489+ assert_eq!(
490+ visual_offset_from_block(slice, 0, 0, &text_fmt, &annot).0,
491+ (0, 0).into()
492+ );
493+ assert_eq!(
494+ visual_offset_from_block(slice, 0, 5, &text_fmt, &annot).0,
495+ (0, 5).into()
496+ ); // position on \n
497+ assert_eq!(
498+ visual_offset_from_block(slice, 0, 6, &text_fmt, &annot).0,
499+ (1, 0).into()
500+ ); // position on w
501+ assert_eq!(
502+ visual_offset_from_block(slice, 0, 7, &text_fmt, &annot).0,
503+ (1, 1).into()
504+ ); // position on o
505+ assert_eq!(
506+ visual_offset_from_block(slice, 0, 10, &text_fmt, &annot).0,
507+ (1, 4).into()
508+ ); // position on d
509+
510+ // Test with wide characters.
511+ let text = Rope::from("今日はいい\n");
512+ let slice = text.slice(..);
513+ assert_eq!(
514+ visual_offset_from_block(slice, 0, 0, &text_fmt, &annot).0,
515+ (0, 0).into()
516+ );
517+ assert_eq!(
518+ visual_offset_from_block(slice, 0, 1, &text_fmt, &annot).0,
519+ (0, 2).into()
520+ );
521+ assert_eq!(
522+ visual_offset_from_block(slice, 0, 2, &text_fmt, &annot).0,
523+ (0, 4).into()
524+ );
525+ assert_eq!(
526+ visual_offset_from_block(slice, 0, 3, &text_fmt, &annot).0,
527+ (0, 6).into()
528+ );
529+ assert_eq!(
530+ visual_offset_from_block(slice, 0, 4, &text_fmt, &annot).0,
531+ (0, 8).into()
532+ );
533+ assert_eq!(
534+ visual_offset_from_block(slice, 0, 5, &text_fmt, &annot).0,
535+ (0, 10).into()
536+ );
537+ assert_eq!(
538+ visual_offset_from_block(slice, 0, 6, &text_fmt, &annot).0,
539+ (1, 0).into()
540+ );
541+
542+ // Test with grapheme clusters.
543+ let text = Rope::from("a̐éö̲\r\n");
544+ let slice = text.slice(..);
545+ assert_eq!(
546+ visual_offset_from_block(slice, 0, 0, &text_fmt, &annot).0,
547+ (0, 0).into()
548+ );
549+ assert_eq!(
550+ visual_offset_from_block(slice, 0, 2, &text_fmt, &annot).0,
551+ (0, 1).into()
552+ );
553+ assert_eq!(
554+ visual_offset_from_block(slice, 0, 4, &text_fmt, &annot).0,
555+ (0, 2).into()
556+ );
557+ assert_eq!(
558+ visual_offset_from_block(slice, 0, 7, &text_fmt, &annot).0,
559+ (0, 3).into()
560+ );
561+ assert_eq!(
562+ visual_offset_from_block(slice, 0, 9, &text_fmt, &annot).0,
563+ (1, 0).into()
564+ );
565+
566+ // Test with wide-character grapheme clusters.
567+ // TODO: account for cluster.
568+ let text = Rope::from("किमपि\n");
569+ let slice = text.slice(..);
570+ assert_eq!(
571+ visual_offset_from_block(slice, 0, 0, &text_fmt, &annot).0,
572+ (0, 0).into()
573+ );
574+ assert_eq!(
575+ visual_offset_from_block(slice, 0, 2, &text_fmt, &annot).0,
576+ (0, 2).into()
577+ );
578+ assert_eq!(
579+ visual_offset_from_block(slice, 0, 3, &text_fmt, &annot).0,
580+ (0, 3).into()
581+ );
582+ assert_eq!(
583+ visual_offset_from_block(slice, 0, 5, &text_fmt, &annot).0,
584+ (0, 5).into()
585+ );
586+ assert_eq!(
587+ visual_offset_from_block(slice, 0, 6, &text_fmt, &annot).0,
588+ (1, 0).into()
589+ );
590+
591+ // Test with tabs.
592+ let text = Rope::from("\tHello\n");
593+ let slice = text.slice(..);
594+ assert_eq!(
595+ visual_offset_from_block(slice, 0, 0, &text_fmt, &annot).0,
596+ (0, 0).into()
597+ );
598+ assert_eq!(
599+ visual_offset_from_block(slice, 0, 1, &text_fmt, &annot).0,
600+ (0, 4).into()
601+ );
602+ assert_eq!(
603+ visual_offset_from_block(slice, 0, 2, &text_fmt, &annot).0,
604+ (0, 5).into()
605+ );
606+ }
278607 #[test]
279608 fn test_pos_at_coords() {
280609 let text = Rope::from("ḧëḷḷö\nẅöṛḷḋ");
@@ -341,6 +670,7 @@ mod test {
341670 }
342671
343672 #[test]
673+ #[allow(deprecated)]
344674 fn test_pos_at_visual_coords() {
345675 let text = Rope::from("ḧëḷḷö\nẅöṛḷḋ");
346676 let slice = text.slice(..);
@@ -405,4 +735,100 @@ mod test {
405735 assert_eq!(pos_at_visual_coords(slice, (0, 10).into(), 4), 0);
406736 assert_eq!(pos_at_visual_coords(slice, (10, 10).into(), 4), 0);
407737 }
738+
739+ #[test]
740+ fn test_char_idx_at_visual_row_offset() {
741+ let text = Rope::from("ḧëḷḷö\nẅöṛḷḋ\nfoo");
742+ let slice = text.slice(..);
743+ let mut text_fmt = TextFormat::default();
744+ for i in 0isize..3isize {
745+ for j in -2isize..=2isize {
746+ if !(0..3).contains(&(i + j)) {
747+ continue;
748+ }
749+ println!("{i} {j}");
750+ assert_eq!(
751+ char_idx_at_visual_offset(
752+ slice,
753+ slice.line_to_char(i as usize),
754+ j,
755+ 3,
756+ &text_fmt,
757+ &TextAnnotations::default(),
758+ )
759+ .0,
760+ slice.line_to_char((i + j) as usize) + 3
761+ );
762+ }
763+ }
764+
765+ text_fmt.soft_wrap = true;
766+ let mut softwrapped_text = "foo ".repeat(10);
767+ softwrapped_text.push('\n');
768+ let last_char = softwrapped_text.len() - 1;
769+
770+ let text = Rope::from(softwrapped_text.repeat(3));
771+ let slice = text.slice(..);
772+ assert_eq!(
773+ char_idx_at_visual_offset(
774+ slice,
775+ last_char,
776+ 0,
777+ 0,
778+ &text_fmt,
779+ &TextAnnotations::default(),
780+ )
781+ .0,
782+ 32
783+ );
784+ assert_eq!(
785+ char_idx_at_visual_offset(
786+ slice,
787+ last_char,
788+ -1,
789+ 0,
790+ &text_fmt,
791+ &TextAnnotations::default(),
792+ )
793+ .0,
794+ 16
795+ );
796+ assert_eq!(
797+ char_idx_at_visual_offset(
798+ slice,
799+ last_char,
800+ -2,
801+ 0,
802+ &text_fmt,
803+ &TextAnnotations::default(),
804+ )
805+ .0,
806+ 0
807+ );
808+ assert_eq!(
809+ char_idx_at_visual_offset(
810+ slice,
811+ softwrapped_text.len() + last_char,
812+ -2,
813+ 0,
814+ &text_fmt,
815+ &TextAnnotations::default(),
816+ )
817+ .0,
818+ softwrapped_text.len()
819+ );
820+
821+ assert_eq!(
822+ char_idx_at_visual_offset(
823+ slice,
824+ softwrapped_text.len() + last_char,
825+ -5,
826+ 0,
827+ &text_fmt,
828+ &TextAnnotations::default(),
829+ )
830+ .0,
831+ 0
832+ );
833+ }
408834}
helix-core/src/selection.rs+17−15
@@ -53,7 +53,9 @@ pub struct Range {
5353 pub anchor: usize,
5454 /// The head of the range, moved when extending.
5555 pub head: usize,
56− pub horiz: Option<u32>,
56+ /// The previous visual offset (softwrapped lines and columns) from
57+ /// the start of the line
58+ pub old_visual_position: Option<(u32, u32)>,
5759}
5860
5961impl Range {
@@ -61,7 +63,7 @@ impl Range {
6163 Self {
6264 anchor,
6365 head,
64− horiz: None,
66+ old_visual_position: None,
6567 }
6668 }
6769
@@ -127,7 +129,7 @@ impl Range {
127129 Self {
128130 anchor: self.head,
129131 head: self.anchor,
130− horiz: self.horiz,
132+ old_visual_position: self.old_visual_position,
131133 }
132134 }
133135
@@ -185,7 +187,7 @@ impl Range {
185187 Self {
186188 anchor,
187189 head,
188− horiz: None,
190+ old_visual_position: None,
189191 }
190192 }
191193
@@ -198,13 +200,13 @@ impl Range {
198200 Self {
199201 anchor: self.anchor.min(from),
200202 head: self.head.max(to),
201− horiz: None,
203+ old_visual_position: None,
202204 }
203205 } else {
204206 Self {
205207 anchor: self.anchor.max(to),
206208 head: self.head.min(from),
207− horiz: None,
209+ old_visual_position: None,
208210 }
209211 }
210212 }
@@ -219,13 +221,13 @@ impl Range {
219221 Range {
220222 anchor: self.anchor.max(other.anchor),
221223 head: self.head.min(other.head),
222− horiz: None,
224+ old_visual_position: None,
223225 }
224226 } else {
225227 Range {
226228 anchor: self.from().min(other.from()),
227229 head: self.to().max(other.to()),
228− horiz: None,
230+ old_visual_position: None,
229231 }
230232 }
231233 }
@@ -279,8 +281,8 @@ impl Range {
279281 Range {
280282 anchor: new_anchor,
281283 head: new_head,
282− horiz: if new_anchor == self.anchor {
283− self.horiz
284+ old_visual_position: if new_anchor == self.anchor {
285+ self.old_visual_position
284286 } else {
285287 None
286288 },
@@ -306,7 +308,7 @@ impl Range {
306308 Range {
307309 anchor: self.anchor,
308310 head: next_grapheme_boundary(slice, self.head),
309− horiz: self.horiz,
311+ old_visual_position: self.old_visual_position,
310312 }
311313 } else {
312314 *self
@@ -378,7 +380,7 @@ impl From<(usize, usize)> for Range {
378380 Self {
379381 anchor,
380382 head,
381− horiz: None,
383+ old_visual_position: None,
382384 }
383385 }
384386}
@@ -482,7 +484,7 @@ impl Selection {
482484 ranges: smallvec![Range {
483485 anchor,
484486 head,
485− horiz: None
487+ old_visual_position: None
486488 }],
487489 primary_index: 0,
488490 }
@@ -566,9 +568,9 @@ impl Selection {
566568 }
567569
568570 /// Takes a closure and maps each `Range` over the closure.
569− pub fn transform<F>(mut self, f: F) -> Self
571+ pub fn transform<F>(mut self, mut f: F) -> Self
570572 where
571− F: Fn(Range) -> Range,
573+ F: FnMut(Range) -> Range,
572574 {
573575 for range in self.ranges.iter_mut() {
574576 *range = f(*range)
helix-core/src/text_annotations.rsnew file+271
@@ -0,0 +1,271 @@
1+use std::cell::Cell;
2+use std::convert::identity;
3+use std::ops::Range;
4+use std::rc::Rc;
5+
6+use crate::syntax::Highlight;
7+use crate::Tendril;
8+
9+/// An inline annotation is continuous text shown
10+/// on the screen before the grapheme that starts at
11+/// `char_idx`
12+#[derive(Debug, Clone)]
13+pub struct InlineAnnotation {
14+ pub text: Tendril,
15+ pub char_idx: usize,
16+}
17+
18+/// Represents a **single Grapheme** that is part of the document
19+/// that start at `char_idx` that will be replaced with
20+/// a different `grapheme`.
21+/// If `grapheme` contains multiple graphemes the text
22+/// will render incorrectly.
23+/// If you want to overlay multiple graphemes simply
24+/// use multiple `Overlays`.
25+///
26+/// # Examples
27+///
28+/// The following examples are valid overlays for the following text:
29+///
30+/// `aX͎̊͢͜͝͡bc`
31+///
32+/// ```
33+/// use helix_core::text_annotations::Overlay;
34+///
35+/// // replaces a
36+/// Overlay {
37+/// char_idx: 0,
38+/// grapheme: "X".into(),
39+/// };
40+///
41+/// // replaces X͎̊͢͜͝͡
42+/// Overlay{
43+/// char_idx: 1,
44+/// grapheme: "\t".into(),
45+/// };
46+///
47+/// // replaces b
48+/// Overlay{
49+/// char_idx: 6,
50+/// grapheme: "X̢̢̟͖̲͌̋̇͑͝".into(),
51+/// };
52+/// ```
53+///
54+/// The following examples are invalid uses
55+///
56+/// ```
57+/// use helix_core::text_annotations::Overlay;
58+///
59+/// // overlay is not aligned at grapheme boundary
60+/// Overlay{
61+/// char_idx: 3,
62+/// grapheme: "x".into(),
63+/// };
64+///
65+/// // overlay contains multiple graphemes
66+/// Overlay{
67+/// char_idx: 0,
68+/// grapheme: "xy".into(),
69+/// };
70+/// ```
71+#[derive(Debug, Clone)]
72+pub struct Overlay {
73+ pub char_idx: usize,
74+ pub grapheme: Tendril,
75+}
76+
77+/// Line annotations allow for virtual text between normal
78+/// text lines. They cause `height` empty lines to be inserted
79+/// below the document line that contains `anchor_char_idx`.
80+///
81+/// These lines can be filled with text in the rendering code
82+/// as their contents have no effect beyond visual appearance.
83+///
84+/// To insert a line after a document line simply set
85+/// `anchor_char_idx` to `doc.line_to_char(line_idx)`
86+#[derive(Debug, Clone)]
87+pub struct LineAnnotation {
88+ pub anchor_char_idx: usize,
89+ pub height: usize,
90+}
91+
92+#[derive(Debug)]
93+struct Layer<A, M> {
94+ annotations: Rc<[A]>,
95+ current_index: Cell<usize>,
96+ metadata: M,
97+}
98+
99+impl<A, M: Clone> Clone for Layer<A, M> {
100+ fn clone(&self) -> Self {
101+ Layer {
102+ annotations: self.annotations.clone(),
103+ current_index: self.current_index.clone(),
104+ metadata: self.metadata.clone(),
105+ }
106+ }
107+}
108+
109+impl<A, M> Layer<A, M> {
110+ pub fn reset_pos(&self, char_idx: usize, get_char_idx: impl Fn(&A) -> usize) {
111+ let new_index = self
112+ .annotations
113+ .binary_search_by_key(&char_idx, get_char_idx)
114+ .unwrap_or_else(identity);
115+
116+ self.current_index.set(new_index);
117+ }
118+
119+ pub fn consume(&self, char_idx: usize, get_char_idx: impl Fn(&A) -> usize) -> Option<&A> {
120+ let annot = self.annotations.get(self.current_index.get())?;
121+ debug_assert!(get_char_idx(annot) >= char_idx);
122+ if get_char_idx(annot) == char_idx {
123+ self.current_index.set(self.current_index.get() + 1);
124+ Some(annot)
125+ } else {
126+ None
127+ }
128+ }
129+}
130+
131+impl<A, M> From<(Rc<[A]>, M)> for Layer<A, M> {
132+ fn from((annotations, metadata): (Rc<[A]>, M)) -> Layer<A, M> {
133+ Layer {
134+ annotations,
135+ current_index: Cell::new(0),
136+ metadata,
137+ }
138+ }
139+}
140+
141+fn reset_pos<A, M>(layers: &[Layer<A, M>], pos: usize, get_pos: impl Fn(&A) -> usize) {
142+ for layer in layers {
143+ layer.reset_pos(pos, &get_pos)
144+ }
145+}
146+
147+/// Annotations that change that is displayed when the document is render.
148+/// Also commonly called virtual text.
149+#[derive(Default, Debug, Clone)]
150+pub struct TextAnnotations {
151+ inline_annotations: Vec<Layer<InlineAnnotation, Option<Highlight>>>,
152+ overlays: Vec<Layer<Overlay, Option<Highlight>>>,
153+ line_annotations: Vec<Layer<LineAnnotation, ()>>,
154+}
155+
156+impl TextAnnotations {
157+ /// Prepare the TextAnnotations for iteration starting at char_idx
158+ pub fn reset_pos(&self, char_idx: usize) {
159+ reset_pos(&self.inline_annotations, char_idx, |annot| annot.char_idx);
160+ reset_pos(&self.overlays, char_idx, |annot| annot.char_idx);
161+ reset_pos(&self.line_annotations, char_idx, |annot| {
162+ annot.anchor_char_idx
163+ });
164+ }
165+
166+ pub fn collect_overlay_highlights(
167+ &self,
168+ char_range: Range<usize>,
169+ ) -> Vec<(usize, Range<usize>)> {
170+ let mut highlights = Vec::new();
171+ self.reset_pos(char_range.start);
172+ for char_idx in char_range {
173+ if let Some((_, Some(highlight))) = self.overlay_at(char_idx) {
174+ // we don't know the number of chars the original grapheme takes
175+ // however it doesn't matter as highlight bounderies are automatically
176+ // aligned to grapheme boundaries in the rendering code
177+ highlights.push((highlight.0, char_idx..char_idx + 1))
178+ }
179+ }
180+
181+ highlights
182+ }
183+
184+ /// Add new inline annotations.
185+ ///
186+ /// The annotations grapheme will be rendered with `highlight`
187+ /// patched on top of `ui.text`.
188+ ///
189+ /// The annotations **must be sorted** by their `char_idx`.
190+ /// Multiple annotations with the same `char_idx` are allowed,
191+ /// they will be display in the order that they are present in the layer.
192+ ///
193+ /// If multiple layers contain annotations at the same position
194+ /// the annotations that belong to the layers added first will be shown first.
195+ pub fn add_inline_annotations(
196+ &mut self,
197+ layer: Rc<[InlineAnnotation]>,
198+ highlight: Option<Highlight>,
199+ ) -> &mut Self {
200+ self.inline_annotations.push((layer, highlight).into());
201+ self
202+ }
203+
204+ /// Add new grapheme overlays.
205+ ///
206+ /// The overlayed grapheme will be rendered with `highlight`
207+ /// patched on top of `ui.text`.
208+ ///
209+ /// The overlays **must be sorted** by their `char_idx`.
210+ /// Multiple overlays with the same `char_idx` **are allowed**.
211+ ///
212+ /// If multiple layers contain overlay at the same position
213+ /// the overlay from the layer added last will be show.
214+ pub fn add_overlay(&mut self, layer: Rc<[Overlay]>, highlight: Option<Highlight>) -> &mut Self {
215+ self.overlays.push((layer, highlight).into());
216+ self
217+ }
218+
219+ /// Add new annotation lines.
220+ ///
221+ /// The line annotations **must be sorted** by their `char_idx`.
222+ /// Multiple line annotations with the same `char_idx` **are not allowed**.
223+ pub fn add_line_annotation(&mut self, layer: Rc<[LineAnnotation]>) -> &mut Self {
224+ self.line_annotations.push((layer, ()).into());
225+ self
226+ }
227+
228+ /// Removes all line annotations, useful for vertical motions
229+ /// so that virtual text lines are automatically skipped.
230+ pub fn clear_line_annotations(&mut self) {
231+ self.line_annotations.clear();
232+ }
233+
234+ pub(crate) fn next_inline_annotation_at(
235+ &self,
236+ char_idx: usize,
237+ ) -> Option<(&InlineAnnotation, Option<Highlight>)> {
238+ self.inline_annotations.iter().find_map(|layer| {
239+ let annotation = layer.consume(char_idx, |annot| annot.char_idx)?;
240+ Some((annotation, layer.metadata))
241+ })
242+ }
243+
244+ pub(crate) fn overlay_at(&self, char_idx: usize) -> Option<(&Overlay, Option<Highlight>)> {
245+ let mut overlay = None;
246+ for layer in &self.overlays {
247+ while let Some(new_overlay) = layer.consume(char_idx, |annot| annot.char_idx) {
248+ overlay = Some((new_overlay, layer.metadata));
249+ }
250+ }
251+ overlay
252+ }
253+
254+ pub(crate) fn annotation_lines_at(&self, char_idx: usize) -> usize {
255+ self.line_annotations
256+ .iter()
257+ .map(|layer| {
258+ let mut lines = 0;
259+ while let Some(annot) = layer.annotations.get(layer.current_index.get()) {
260+ if annot.anchor_char_idx == char_idx {
261+ layer.current_index.set(layer.current_index.get() + 1);
262+ lines += annot.height
263+ } else {
264+ break;
265+ }
266+ }
267+ lines
268+ })
269+ .sum()
270+ }
271+}
helix-term/src/application.rs+11−2
@@ -172,7 +172,7 @@ impl Application {
172172 area,
173173 theme_loader.clone(),
174174 syn_loader.clone(),
175− Box::new(Map::new(Arc::clone(&config), |config: &Config| {
175+ Arc::new(Map::new(Arc::clone(&config), |config: &Config| {
176176 &config.editor
177177 })),
178178 );
@@ -309,8 +309,10 @@ impl Application {
309309 let surface = self.terminal.current_buffer_mut();
310310
311311 self.compositor.render(area, surface, &mut cx);
312−
313312 let (pos, kind) = self.compositor.cursor(area, &self.editor);
313+ // reset cursor cache
314+ self.editor.cursor_cache.set(None);
315+
314316 let pos = pos.map(|pos| (pos.col as u16, pos.row as u16));
315317 self.terminal.draw(pos, kind).unwrap();
316318 }
@@ -395,6 +397,13 @@ impl Application {
395397 // Update all the relevant members in the editor after updating
396398 // the configuration.
397399 self.editor.refresh_config();
400+
401+ // reset view position in case softwrap was enabled/disabled
402+ let scrolloff = self.editor.config().scrolloff;
403+ for (view, _) in self.editor.tree.views_mut() {
404+ let doc = &self.editor.documents[&view.doc];
405+ view.ensure_cursor_in_view(doc, scrolloff)
406+ }
398407 }
399408
400409 /// refresh language config after config change
helix-term/src/commands.rs+162−66
@@ -9,21 +9,25 @@ use tui::widgets::Row;
99pub use typed::*;
1010
1111use helix_core::{
12− comment, coords_at_pos, encoding, find_first_non_whitespace_char, find_root, graphemes,
12+ char_idx_at_visual_offset, comment,
13+ doc_formatter::TextFormat,
14+ encoding, find_first_non_whitespace_char, find_root, graphemes,
1315 history::UndoKind,
1416 increment, indent,
1517 indent::IndentStyle,
1618 line_ending::{get_line_ending_of_str, line_end_char_index, str_is_line_ending},
1719 match_brackets,
18− movement::{self, Direction},
19− object, pos_at_coords, pos_at_visual_coords,
20+ movement::{self, move_vertically_visual, Direction},
21+ object, pos_at_coords,
2022 regex::{self, Regex, RegexBuilder},
2123 search::{self, CharMatcher},
22− selection, shellwords, surround, textobject,
24+ selection, shellwords, surround,
25+ text_annotations::TextAnnotations,
26+ textobject,
2327 tree_sitter::Node,
2428 unicode::width::UnicodeWidthChar,
25− visual_coords_at_pos, LineEnding, Position, Range, Rope, RopeGraphemes, RopeSlice, Selection,
26− SmallVec, Tendril, Transaction,
29+ visual_offset_from_block, LineEnding, Position, Range, Rope, RopeGraphemes, RopeSlice,
30+ Selection, SmallVec, Tendril, Transaction,
2731};
2832use helix_view::{
2933 clipboard::ClipboardType,
@@ -200,10 +204,14 @@ impl MappableCommand {
200204 move_char_right, "Move right",
201205 move_line_up, "Move up",
202206 move_line_down, "Move down",
207+ move_visual_line_up, "Move up",
208+ move_visual_line_down, "Move down",
203209 extend_char_left, "Extend left",
204210 extend_char_right, "Extend right",
205211 extend_line_up, "Extend up",
206212 extend_line_down, "Extend down",
213+ extend_visual_line_up, "Extend up",
214+ extend_visual_line_down, "Extend down",
207215 copy_selection_on_next_line, "Copy selection on next line",
208216 copy_selection_on_prev_line, "Copy selection on previous line",
209217 move_next_word_start, "Move to start of next word",
@@ -538,18 +546,27 @@ impl PartialEq for MappableCommand {
538546
539547fn no_op(_cx: &mut Context) {}
540548
541−fn move_impl<F>(cx: &mut Context, move_fn: F, dir: Direction, behaviour: Movement)
542−where
543− F: Fn(RopeSlice, Range, Direction, usize, Movement, usize) -> Range,
544−{
549+type MoveFn =
550+ fn(RopeSlice, Range, Direction, usize, Movement, &TextFormat, &mut TextAnnotations) -> Range;
551+
552+fn move_impl(cx: &mut Context, move_fn: MoveFn, dir: Direction, behaviour: Movement) {
545553 let count = cx.count();
546554 let (view, doc) = current!(cx.editor);
547555 let text = doc.text().slice(..);
556+ let text_fmt = doc.text_format(view.inner_area(doc).width, None);
557+ let mut annotations = view.text_annotations(doc, None);
548558
549− let selection = doc
550− .selection(view.id)
551− .clone()
552− .transform(|range| move_fn(text, range, dir, count, behaviour, doc.tab_width()));
559+ let selection = doc.selection(view.id).clone().transform(|range| {
560+ move_fn(
561+ text,
562+ range,
563+ dir,
564+ count,
565+ behaviour,
566+ &text_fmt,
567+ &mut annotations,
568+ )
569+ });
553570 doc.set_selection(view.id, selection);
554571}
555572
@@ -571,6 +588,24 @@ fn move_line_down(cx: &mut Context) {
571588 move_impl(cx, move_vertically, Direction::Forward, Movement::Move)
572589}
573590
591+fn move_visual_line_up(cx: &mut Context) {
592+ move_impl(
593+ cx,
594+ move_vertically_visual,
595+ Direction::Backward,
596+ Movement::Move,
597+ )
598+}
599+
600+fn move_visual_line_down(cx: &mut Context) {
601+ move_impl(
602+ cx,
603+ move_vertically_visual,
604+ Direction::Forward,
605+ Movement::Move,
606+ )
607+}
608+
574609fn extend_char_left(cx: &mut Context) {
575610 move_impl(cx, move_horizontally, Direction::Backward, Movement::Extend)
576611}
@@ -587,6 +622,24 @@ fn extend_line_down(cx: &mut Context) {
587622 move_impl(cx, move_vertically, Direction::Forward, Movement::Extend)
588623}
589624
625+fn extend_visual_line_up(cx: &mut Context) {
626+ move_impl(
627+ cx,
628+ move_vertically_visual,
629+ Direction::Backward,
630+ Movement::Extend,
631+ )
632+}
633+
634+fn extend_visual_line_down(cx: &mut Context) {
635+ move_impl(
636+ cx,
637+ move_vertically_visual,
638+ Direction::Forward,
639+ Movement::Extend,
640+ )
641+}
642+
590643fn goto_line_end_impl(view: &mut View, doc: &mut Document, movement: Movement) {
591644 let text = doc.text().slice(..);
592645
@@ -814,7 +867,10 @@ fn trim_selections(cx: &mut Context) {
814867}
815868
816869// align text in selection
870+#[allow(deprecated)]
817871fn align_selections(cx: &mut Context) {
872+ use helix_core::visual_coords_at_pos;
873+
818874 let (view, doc) = current!(cx.editor);
819875 let text = doc.text().slice(..);
820876 let selection = doc.selection(view.id);
@@ -891,17 +947,22 @@ fn goto_window(cx: &mut Context, align: Align) {
891947 // as we type
892948 let scrolloff = config.scrolloff.min(height.saturating_sub(1) / 2);
893949
894− let last_line = view.last_line(doc);
950+ let last_visual_line = view.last_visual_line(doc);
895951
896− let line = match align {
897− Align::Top => view.offset.row + scrolloff + count,
898− Align::Center => view.offset.row + ((last_line - view.offset.row) / 2),
899− Align::Bottom => last_line.saturating_sub(scrolloff + count),
952+ let visual_line = match align {
953+ Align::Top => view.offset.vertical_offset + scrolloff + count,
954+ Align::Center => view.offset.vertical_offset + (last_visual_line / 2),
955+ Align::Bottom => {
956+ view.offset.vertical_offset + last_visual_line.saturating_sub(scrolloff + count)
957+ }
900958 }
901− .max(view.offset.row + scrolloff)
902− .min(last_line.saturating_sub(scrolloff));
959+ .max(view.offset.vertical_offset + scrolloff)
960+ .min(view.offset.vertical_offset + last_visual_line.saturating_sub(scrolloff));
961+
962+ let pos = view
963+ .pos_at_visual_coords(doc, visual_line as u16, 0, false)
964+ .expect("visual_line was constrained to the view area");
903965
904− let pos = doc.text().line_to_char(line);
905966 let text = doc.text().slice(..);
906967 let selection = doc
907968 .selection(view.id)
@@ -1385,53 +1446,72 @@ pub fn scroll(cx: &mut Context, offset: usize, direction: Direction) {
13851446 let range = doc.selection(view.id).primary();
13861447 let text = doc.text().slice(..);
13871448
1388− let cursor = visual_coords_at_pos(text, range.cursor(text), doc.tab_width());
1389− let doc_last_line = doc.text().len_lines().saturating_sub(1);
1390−
1391− let last_line = view.last_line(doc);
1392−
1393− if direction == Backward && view.offset.row == 0
1394− || direction == Forward && last_line == doc_last_line
1395− {
1396− return;
1397− }
1398−
1449+ let cursor = range.cursor(text);
13991450 let height = view.inner_height();
14001451
14011452 let scrolloff = config.scrolloff.min(height / 2);
1453+ let offset = match direction {
1454+ Forward => offset as isize,
1455+ Backward => -(offset as isize),
1456+ };
14021457
1403− view.offset.row = match direction {
1404− Forward => view.offset.row + offset,
1405− Backward => view.offset.row.saturating_sub(offset),
1406− }
1407− .min(doc_last_line);
1408−
1409− // recalculate last line
1410− let last_line = view.last_line(doc);
1411−
1412− // clamp into viewport
1413− let line = cursor
1414− .row
1415− .max(view.offset.row + scrolloff)
1416− .min(last_line.saturating_sub(scrolloff));
1458+ let doc_text = doc.text().slice(..);
1459+ let viewport = view.inner_area(doc);
1460+ let text_fmt = doc.text_format(viewport.width, None);
1461+ let annotations = view.text_annotations(doc, None);
1462+ (view.offset.anchor, view.offset.vertical_offset) = char_idx_at_visual_offset(
1463+ doc_text,
1464+ view.offset.anchor,
1465+ view.offset.vertical_offset as isize + offset,
1466+ 0,
1467+ &text_fmt,
1468+ &annotations,
1469+ );
14171470
1418− // If cursor needs moving, replace primary selection
1419− if line != cursor.row {
1420− let head = pos_at_visual_coords(text, Position::new(line, cursor.col), doc.tab_width()); // this func will properly truncate to line end
1471+ let head;
1472+ match direction {
1473+ Forward => {
1474+ head = char_idx_at_visual_offset(
1475+ doc_text,
1476+ view.offset.anchor,
1477+ (view.offset.vertical_offset + scrolloff) as isize,
1478+ 0,
1479+ &text_fmt,
1480+ &annotations,
1481+ )
1482+ .0;
1483+ if head <= cursor {
1484+ return;
1485+ }
1486+ }
1487+ Backward => {
1488+ head = char_idx_at_visual_offset(
1489+ doc_text,
1490+ view.offset.anchor,
1491+ (view.offset.vertical_offset + height - scrolloff) as isize,
1492+ 0,
1493+ &text_fmt,
1494+ &annotations,
1495+ )
1496+ .0;
1497+ if head >= cursor {
1498+ return;
1499+ }
1500+ }
1501+ }
14211502
1422− let anchor = if cx.editor.mode == Mode::Select {
1423− range.anchor
1424− } else {
1425− head
1426− };
1503+ let anchor = if cx.editor.mode == Mode::Select {
1504+ range.anchor
1505+ } else {
1506+ head
1507+ };
14271508
1428− // replace primary selection with an empty selection at cursor pos
1429− let prim_sel = Range::new(anchor, head);
1430− let mut sel = doc.selection(view.id).clone();
1431− let idx = sel.primary_index();
1432− sel = sel.replace(idx, prim_sel);
1433− doc.set_selection(view.id, sel);
1434− }
1509+ // replace primary selection with an empty selection at cursor pos
1510+ let prim_sel = Range::new(anchor, head);
1511+ let mut sel = doc.selection(view.id).clone();
1512+ let idx = sel.primary_index();
1513+ sel = sel.replace(idx, prim_sel);
1514+ doc.set_selection(view.id, sel);
14351515}
14361516
14371517fn page_up(cx: &mut Context) {
@@ -1458,7 +1538,15 @@ fn half_page_down(cx: &mut Context) {
14581538 scroll(cx, offset, Direction::Forward);
14591539}
14601540
1541+#[allow(deprecated)]
1542+// currently uses the deprected `visual_coords_at_pos`/`pos_at_visual_coords` functions
1543+// as this function ignores softwrapping (and virtual text) and instead only cares
1544+// about "text visual position"
1545+//
1546+// TODO: implement a variant of that uses visual lines and respects virtual text
14611547fn copy_selection_on_line(cx: &mut Context, direction: Direction) {
1548+ use helix_core::{pos_at_visual_coords, visual_coords_at_pos};
1549+
14621550 let count = cx.count();
14631551 let (view, doc) = current!(cx.editor);
14641552 let text = doc.text().slice(..);
@@ -4475,11 +4563,19 @@ fn align_view_bottom(cx: &mut Context) {
44754563
44764564fn align_view_middle(cx: &mut Context) {
44774565 let (view, doc) = current!(cx.editor);
4478− let text = doc.text().slice(..);
4479− let pos = doc.selection(view.id).primary().cursor(text);
4480− let pos = coords_at_pos(text, pos);
4566+ let inner_width = view.inner_width(doc);
4567+ let text_fmt = doc.text_format(inner_width, None);
4568+ // there is no horizontal position when softwrap is enabled
4569+ if text_fmt.soft_wrap {
4570+ return;
4571+ }
4572+ let doc_text = doc.text().slice(..);
4573+ let annotations = view.text_annotations(doc, None);
4574+ let pos = doc.selection(view.id).primary().cursor(doc_text);
4575+ let pos =
4576+ visual_offset_from_block(doc_text, view.offset.anchor, pos, &text_fmt, &annotations).0;
44814577
4482− view.offset.col = pos
4578+ view.offset.horizontal_offset = pos
44834579 .col
44844580 .saturating_sub((view.inner_area(doc).width as usize) / 2);
44854581}
helix-term/src/keymap/default.rs+12−6
@@ -7,8 +7,8 @@ use helix_core::hashmap;
77pub fn default() -> HashMap<Mode, Keymap> {
88 let normal = keymap!({ "Normal mode"
99 "h" | "left" => move_char_left,
10− "j" | "down" => move_line_down,
11− "k" | "up" => move_line_up,
10+ "j" | "down" => move_visual_line_down,
11+ "k" | "up" => move_visual_line_up,
1212 "l" | "right" => move_char_right,
1313
1414 "t" => find_till_char,
@@ -55,6 +55,8 @@ pub fn default() -> HashMap<Mode, Keymap> {
5555 "m" => goto_last_modified_file,
5656 "n" => goto_next_buffer,
5757 "p" => goto_previous_buffer,
58+ "k" => move_line_up,
59+ "j" => move_line_down,
5860 "." => goto_last_modification,
5961 },
6062 ":" => command_mode,
@@ -321,8 +323,8 @@ pub fn default() -> HashMap<Mode, Keymap> {
321323 let mut select = normal.clone();
322324 select.merge_nodes(keymap!({ "Select mode"
323325 "h" | "left" => extend_char_left,
324− "j" | "down" => extend_line_down,
325− "k" | "up" => extend_line_up,
326+ "j" | "down" => extend_visual_line_down,
327+ "k" | "up" => extend_visual_line_up,
326328 "l" | "right" => extend_char_right,
327329
328330 "w" => extend_next_word_start,
@@ -345,6 +347,10 @@ pub fn default() -> HashMap<Mode, Keymap> {
345347 "esc" => exit_select_mode,
346348
347349 "v" => normal_mode,
350+ "g" => { "Goto"
351+ "k" => extend_line_up,
352+ "j" => extend_line_down,
353+ },
348354 }));
349355 let insert = keymap!({ "Insert mode"
350356 "esc" => normal_mode,
@@ -362,8 +368,8 @@ pub fn default() -> HashMap<Mode, Keymap> {
362368 "C-j" | "ret" => insert_newline,
363369 "tab" => insert_tab,
364370
365− "up" => move_line_up,
366− "down" => move_line_down,
371+ "up" => move_visual_line_up,
372+ "down" => move_visual_line_down,
367373 "left" => move_char_left,
368374 "right" => move_char_right,
369375 "pageup" => page_up,
helix-term/src/ui/completion.rs+4−2
@@ -391,8 +391,10 @@ impl Component for Completion {
391391 let language = doc.language_name().unwrap_or("");
392392 let text = doc.text().slice(..);
393393 let cursor_pos = doc.selection(view.id).primary().cursor(text);
394− let coords = helix_core::visual_coords_at_pos(text, cursor_pos, doc.tab_width());
395− let cursor_pos = (coords.row - view.offset.row) as u16;
394+ let coords = view
395+ .screen_coords_at_pos(doc, text, cursor_pos)
396+ .expect("cursor must be in view");
397+ let cursor_pos = coords.row as u16;
396398
397399 let mut markdown_doc = match &option.documentation {
398400 Some(lsp::Documentation::String(contents))
helix-term/src/ui/document.rsnew file+475
@@ -0,0 +1,475 @@
1+use std::cmp::min;
2+
3+use helix_core::doc_formatter::{DocumentFormatter, GraphemeSource, TextFormat};
4+use helix_core::graphemes::Grapheme;
5+use helix_core::str_utils::char_to_byte_idx;
6+use helix_core::syntax::Highlight;
7+use helix_core::syntax::HighlightEvent;
8+use helix_core::text_annotations::TextAnnotations;
9+use helix_core::{visual_offset_from_block, Position, RopeSlice};
10+use helix_view::editor::{WhitespaceConfig, WhitespaceRenderValue};
11+use helix_view::graphics::Rect;
12+use helix_view::theme::Style;
13+use helix_view::view::ViewPosition;
14+use helix_view::Document;
15+use helix_view::Theme;
16+use tui::buffer::Buffer as Surface;
17+
18+pub trait LineDecoration {
19+ fn render_background(&mut self, _renderer: &mut TextRenderer, _pos: LinePos) {}
20+ fn render_foreground(
21+ &mut self,
22+ _renderer: &mut TextRenderer,
23+ _pos: LinePos,
24+ _end_char_idx: usize,
25+ ) {
26+ }
27+}
28+
29+impl<F: FnMut(&mut TextRenderer, LinePos)> LineDecoration for F {
30+ fn render_background(&mut self, renderer: &mut TextRenderer, pos: LinePos) {
31+ self(renderer, pos)
32+ }
33+}
34+
35+/// A wrapper around a HighlightIterator
36+/// that merges the layered highlights to create the final text style
37+/// and yields the active text style and the char_idx where the active
38+/// style will have to be recomputed.
39+struct StyleIter<'a, H: Iterator<Item = HighlightEvent>> {
40+ text_style: Style,
41+ active_highlights: Vec<Highlight>,
42+ highlight_iter: H,
43+ theme: &'a Theme,
44+}
45+
46+impl<H: Iterator<Item = HighlightEvent>> Iterator for StyleIter<'_, H> {
47+ type Item = (Style, usize);
48+ fn next(&mut self) -> Option<(Style, usize)> {
49+ while let Some(event) = self.highlight_iter.next() {
50+ match event {
51+ HighlightEvent::HighlightStart(highlights) => {
52+ self.active_highlights.push(highlights)
53+ }
54+ HighlightEvent::HighlightEnd => {
55+ self.active_highlights.pop();
56+ }
57+ HighlightEvent::Source { start, end } => {
58+ if start == end {
59+ continue;
60+ }
61+ let style = self
62+ .active_highlights
63+ .iter()
64+ .fold(self.text_style, |acc, span| {
65+ acc.patch(self.theme.highlight(span.0))
66+ });
67+ return Some((style, end));
68+ }
69+ }
70+ }
71+ None
72+ }
73+}
74+
75+#[derive(Debug, PartialEq, Eq, Copy, Clone)]
76+pub struct LinePos {
77+ /// Indicates whether the given visual line
78+ /// is the first visual line of the given document line
79+ pub first_visual_line: bool,
80+ /// The line index of the document line that contains the given visual line
81+ pub doc_line: usize,
82+ /// Vertical offset from the top of the inner view area
83+ pub visual_line: u16,
84+ /// The first char index of this visual line.
85+ /// Note that if the visual line is entirely filled by
86+ /// a very long inline virtual text then this index will point
87+ /// at the next (non-virtual) char after this visual line
88+ pub start_char_idx: usize,
89+}
90+
91+pub type TranslatedPosition<'a> = (usize, Box<dyn FnMut(&mut TextRenderer, Position) + 'a>);
92+
93+#[allow(clippy::too_many_arguments)]
94+pub fn render_document(
95+ surface: &mut Surface,
96+ viewport: Rect,
97+ doc: &Document,
98+ offset: ViewPosition,
99+ doc_annotations: &TextAnnotations,
100+ highlight_iter: impl Iterator<Item = HighlightEvent>,
101+ theme: &Theme,
102+ line_decoration: &mut [Box<dyn LineDecoration + '_>],
103+ translated_positions: &mut [TranslatedPosition],
104+) {
105+ let mut renderer = TextRenderer::new(surface, doc, theme, offset.horizontal_offset, viewport);
106+ render_text(
107+ &mut renderer,
108+ doc.text().slice(..),
109+ offset,
110+ &doc.text_format(viewport.width, Some(theme)),
111+ doc_annotations,
112+ highlight_iter,
113+ theme,
114+ line_decoration,
115+ translated_positions,
116+ )
117+}
118+
119+fn translate_positions(
120+ char_pos: usize,
121+ first_visisble_char_idx: usize,
122+ translated_positions: &mut [TranslatedPosition],
123+ text_fmt: &TextFormat,
124+ renderer: &mut TextRenderer,
125+ pos: Position,
126+) {
127+ // check if any positions translated on the fly (like cursor) has been reached
128+ for (char_idx, callback) in &mut *translated_positions {
129+ if *char_idx < char_pos && *char_idx >= first_visisble_char_idx {
130+ // by replacing the char_index with usize::MAX large number we ensure
131+ // that the same position is only translated once
132+ // text will never reach usize::MAX as rust memory allocations are limited
133+ // to isize::MAX
134+ *char_idx = usize::MAX;
135+
136+ if text_fmt.soft_wrap {
137+ callback(renderer, pos)
138+ } else if pos.col >= renderer.col_offset
139+ && pos.col - renderer.col_offset < renderer.viewport.width as usize
140+ {
141+ callback(
142+ renderer,
143+ Position {
144+ row: pos.row,
145+ col: pos.col - renderer.col_offset,
146+ },
147+ )
148+ }
149+ }
150+ }
151+}
152+
153+#[allow(clippy::too_many_arguments)]
154+pub fn render_text<'t>(
155+ renderer: &mut TextRenderer,
156+ text: RopeSlice<'t>,
157+ offset: ViewPosition,
158+ text_fmt: &TextFormat,
159+ text_annotations: &TextAnnotations,
160+ highlight_iter: impl Iterator<Item = HighlightEvent>,
161+ theme: &Theme,
162+ line_decorations: &mut [Box<dyn LineDecoration + '_>],
163+ translated_positions: &mut [TranslatedPosition],
164+) {
165+ let (
166+ Position {
167+ row: mut row_off, ..
168+ },
169+ mut char_pos,
170+ ) = visual_offset_from_block(
171+ text,
172+ offset.anchor,
173+ offset.anchor,
174+ text_fmt,
175+ text_annotations,
176+ );
177+ row_off += offset.vertical_offset;
178+ assert_eq!(0, offset.vertical_offset);
179+
180+ let (mut formatter, mut first_visible_char_idx) =
181+ DocumentFormatter::new_at_prev_checkpoint(text, text_fmt, text_annotations, offset.anchor);
182+ let mut styles = StyleIter {
183+ text_style: renderer.text_style,
184+ active_highlights: Vec::with_capacity(64),
185+ highlight_iter,
186+ theme,
187+ };
188+
189+ let mut last_line_pos = LinePos {
190+ first_visual_line: false,
191+ doc_line: usize::MAX,
192+ visual_line: u16::MAX,
193+ start_char_idx: usize::MAX,
194+ };
195+ let mut is_in_indent_area = true;
196+ let mut last_line_indent_level = 0;
197+ let mut style_span = styles
198+ .next()
199+ .unwrap_or_else(|| (Style::default(), usize::MAX));
200+
201+ loop {
202+ // formattter.line_pos returns to line index of the next grapheme
203+ // so it must be called before formatter.next
204+ let doc_line = formatter.line_pos();
205+ // TODO refactor with let .. else once MSRV reaches 1.65
206+ let (grapheme, mut pos) = if let Some(it) = formatter.next() {
207+ it
208+ } else {
209+ let mut last_pos = formatter.visual_pos();
210+ last_pos.col -= 1;
211+ // check if any positions translated on the fly (like cursor) are at the EOF
212+ translate_positions(
213+ char_pos + 1,
214+ first_visible_char_idx,
215+ translated_positions,
216+ text_fmt,
217+ renderer,
218+ last_pos,
219+ );
220+ break;
221+ };
222+
223+ // skip any graphemes on visual lines before the block start
224+ if pos.row < row_off {
225+ if char_pos >= style_span.1 {
226+ // TODO refactor using let..else once MSRV reaches 1.65
227+ style_span = if let Some(style_span) = styles.next() {
228+ style_span
229+ } else {
230+ break;
231+ }
232+ }
233+ char_pos += grapheme.doc_chars();
234+ first_visible_char_idx = char_pos + 1;
235+ continue;
236+ }
237+ pos.row -= row_off;
238+
239+ // if the end of the viewport is reached stop rendering
240+ if pos.row as u16 >= renderer.viewport.height {
241+ break;
242+ }
243+
244+ // apply decorations before rendering a new line
245+ if pos.row as u16 != last_line_pos.visual_line {
246+ if pos.row > 0 {
247+ renderer.draw_indent_guides(last_line_indent_level, last_line_pos.visual_line);
248+ is_in_indent_area = true;
249+ for line_decoration in &mut *line_decorations {
250+ line_decoration.render_foreground(renderer, last_line_pos, char_pos);
251+ }
252+ }
253+ last_line_pos = LinePos {
254+ first_visual_line: doc_line != last_line_pos.doc_line,
255+ doc_line,
256+ visual_line: pos.row as u16,
257+ start_char_idx: char_pos,
258+ };
259+ for line_decoration in &mut *line_decorations {
260+ line_decoration.render_background(renderer, last_line_pos);
261+ }
262+ }
263+
264+ // aquire the correct grapheme style
265+ if char_pos >= style_span.1 {
266+ // TODO refactor using let..else once MSRV reaches 1.65
267+ style_span = if let Some(style_span) = styles.next() {
268+ style_span
269+ } else {
270+ (Style::default(), usize::MAX)
271+ }
272+ }
273+ char_pos += grapheme.doc_chars();
274+
275+ // check if any positions translated on the fly (like cursor) has been reached
276+ translate_positions(
277+ char_pos,
278+ first_visible_char_idx,
279+ translated_positions,
280+ text_fmt,
281+ renderer,
282+ pos,
283+ );
284+
285+ let grapheme_style = if let GraphemeSource::VirtualText { highlight } = grapheme.source {
286+ let style = renderer.text_style;
287+ if let Some(highlight) = highlight {
288+ style.patch(theme.highlight(highlight.0))
289+ } else {
290+ style
291+ }
292+ } else {
293+ style_span.0
294+ };
295+
296+ renderer.draw_grapheme(
297+ grapheme.grapheme,
298+ grapheme_style,
299+ &mut last_line_indent_level,
300+ &mut is_in_indent_area,
301+ pos,
302+ );
303+ }
304+
305+ renderer.draw_indent_guides(last_line_indent_level, last_line_pos.visual_line);
306+ for line_decoration in &mut *line_decorations {
307+ line_decoration.render_foreground(renderer, last_line_pos, char_pos);
308+ }
309+}
310+
311+#[derive(Debug)]
312+pub struct TextRenderer<'a> {
313+ pub surface: &'a mut Surface,
314+ pub text_style: Style,
315+ pub whitespace_style: Style,
316+ pub indent_guide_char: String,
317+ pub indent_guide_style: Style,
318+ pub newline: String,
319+ pub nbsp: String,
320+ pub space: String,
321+ pub tab: String,
322+ pub tab_width: u16,
323+ pub starting_indent: usize,
324+ pub draw_indent_guides: bool,
325+ pub col_offset: usize,
326+ pub viewport: Rect,
327+}
328+
329+impl<'a> TextRenderer<'a> {
330+ pub fn new(
331+ surface: &'a mut Surface,
332+ doc: &Document,
333+ theme: &Theme,
334+ col_offset: usize,
335+ viewport: Rect,
336+ ) -> TextRenderer<'a> {
337+ let editor_config = doc.config.load();
338+ let WhitespaceConfig {
339+ render: ws_render,
340+ characters: ws_chars,
341+ } = &editor_config.whitespace;
342+
343+ let tab_width = doc.tab_width();
344+ let tab = if ws_render.tab() == WhitespaceRenderValue::All {
345+ std::iter::once(ws_chars.tab)
346+ .chain(std::iter::repeat(ws_chars.tabpad).take(tab_width - 1))
347+ .collect()
348+ } else {
349+ " ".repeat(tab_width)
350+ };
351+ let newline = if ws_render.newline() == WhitespaceRenderValue::All {
352+ ws_chars.newline.into()
353+ } else {
354+ " ".to_owned()
355+ };
356+
357+ let space = if ws_render.space() == WhitespaceRenderValue::All {
358+ ws_chars.space.into()
359+ } else {
360+ " ".to_owned()
361+ };
362+ let nbsp = if ws_render.nbsp() == WhitespaceRenderValue::All {
363+ ws_chars.nbsp.into()
364+ } else {
365+ " ".to_owned()
366+ };
367+
368+ let text_style = theme.get("ui.text");
369+
370+ TextRenderer {
371+ surface,
372+ indent_guide_char: editor_config.indent_guides.character.into(),
373+ newline,
374+ nbsp,
375+ space,
376+ tab_width: tab_width as u16,
377+ tab,
378+ whitespace_style: theme.get("ui.virtual.whitespace"),
379+ starting_indent: (col_offset / tab_width)
380+ + editor_config.indent_guides.skip_levels as usize,
381+ indent_guide_style: text_style.patch(
382+ theme
383+ .try_get("ui.virtual.indent-guide")
384+ .unwrap_or_else(|| theme.get("ui.virtual.whitespace")),
385+ ),
386+ text_style,
387+ draw_indent_guides: editor_config.indent_guides.render,
388+ viewport,
389+ col_offset,
390+ }
391+ }
392+
393+ /// Draws a single `grapheme` at the current render position with a specified `style`.
394+ pub fn draw_grapheme(
395+ &mut self,
396+ grapheme: Grapheme,
397+ mut style: Style,
398+ last_indent_level: &mut usize,
399+ is_in_indent_area: &mut bool,
400+ position: Position,
401+ ) {
402+ let cut_off_start = self.col_offset.saturating_sub(position.col as usize);
403+ let is_whitespace = grapheme.is_whitespace();
404+
405+ // TODO is it correct to apply the whitspace style to all unicode white spaces?
406+ if is_whitespace {
407+ style = style.patch(self.whitespace_style);
408+ }
409+
410+ let width = grapheme.width();
411+ let grapheme = match grapheme {
412+ Grapheme::Tab { width } => {
413+ let grapheme_tab_width = char_to_byte_idx(&self.tab, width as usize);
414+ &self.tab[..grapheme_tab_width]
415+ }
416+ // TODO special rendering for other whitespaces?
417+ Grapheme::Other { ref g } if g == " " => &self.space,
418+ Grapheme::Other { ref g } if g == "\u{00A0}" => &self.nbsp,
419+ Grapheme::Other { ref g } => &*g,
420+ Grapheme::Newline => &self.newline,
421+ };
422+
423+ let in_bounds = self.col_offset <= (position.col as usize)
424+ && (position.col as usize) < self.viewport.width as usize + self.col_offset;
425+
426+ if in_bounds {
427+ self.surface.set_string(
428+ self.viewport.x + (position.col - self.col_offset) as u16,
429+ self.viewport.y + position.row as u16,
430+ grapheme,
431+ style,
432+ );
433+ } else if cut_off_start != 0 && cut_off_start < width as usize {
434+ // partially on screen
435+ let rect = Rect::new(
436+ self.viewport.x as u16,
437+ self.viewport.y + position.row as u16,
438+ (width - cut_off_start) as u16,
439+ 1,
440+ );
441+ self.surface.set_style(rect, style);
442+ }
443+
444+ if *is_in_indent_area && !is_whitespace {
445+ *last_indent_level = position.col;
446+ *is_in_indent_area = false;
447+ }
448+ }
449+
450+ /// Overlay indentation guides ontop of a rendered line
451+ /// The indentation level is computed in `draw_lines`.
452+ /// Therefore this function must always be called afterwards.
453+ pub fn draw_indent_guides(&mut self, indent_level: usize, row: u16) {
454+ if !self.draw_indent_guides {
455+ return;
456+ }
457+
458+ // Don't draw indent guides outside of view
459+ let end_indent = min(
460+ indent_level,
461+ // Add tab_width - 1 to round up, since the first visible
462+ // indent might be a bit after offset.col
463+ self.col_offset + self.viewport.width as usize + (self.tab_width - 1) as usize,
464+ ) / self.tab_width as usize;
465+
466+ for i in self.starting_indent..end_indent {
467+ let x =
468+ (self.viewport.x as usize + (i * self.tab_width as usize) - self.col_offset) as u16;
469+ let y = self.viewport.y + row;
470+ debug_assert!(self.surface.in_bounds(x, y));
471+ self.surface
472+ .set_string(x, y, &self.indent_guide_char, self.indent_guide_style);
473+ }
474+ }
475+}
helix-term/src/ui/editor.rs+192−320
@@ -4,7 +4,10 @@ use crate::{
44 job::{self, Callback},
55 key,
66 keymap::{KeymapResult, Keymaps},
7− ui::{Completion, ProgressSpinners},
7+ ui::{
8+ document::{render_document, LinePos, TextRenderer, TranslatedPosition},
9+ Completion, ProgressSpinners,
10+ },
811};
912
1013use helix_core::{
@@ -13,8 +16,9 @@ use helix_core::{
1316 },
1417 movement::Direction,
1518 syntax::{self, HighlightEvent},
19+ text_annotations::TextAnnotations,
1620 unicode::width::UnicodeWidthStr,
17− visual_coords_at_pos, LineEnding, Position, Range, Selection, Transaction,
21+ visual_offset_from_block, Position, Range, Selection, Transaction,
1822};
1923use helix_view::{
2024 document::{Mode, SCRATCH_BUFFER_NAME},
@@ -24,12 +28,12 @@ use helix_view::{
2428 keyboard::{KeyCode, KeyModifiers},
2529 Document, Editor, Theme, View,
2630};
27−use std::{borrow::Cow, cmp::min, num::NonZeroUsize, path::PathBuf};
31+use std::{num::NonZeroUsize, path::PathBuf, rc::Rc};
2832
2933use tui::buffer::Buffer as Surface;
3034
31−use super::lsp::SignatureHelp;
3235use super::statusline;
36+use super::{document::LineDecoration, lsp::SignatureHelp};
3337
3438pub struct EditorView {
3539 pub keymaps: Keymaps,
@@ -83,6 +87,10 @@ impl EditorView {
8387 let theme = &editor.theme;
8488 let config = editor.config();
8589
90+ let text_annotations = view.text_annotations(doc, Some(theme));
91+ let mut line_decorations: Vec<Box<dyn LineDecoration>> = Vec::new();
92+ let mut translated_positions: Vec<TranslatedPosition> = Vec::new();
93+
8694 // DAP: Highlight current stack frame position
8795 let stack_frame = editor.debugger.as_ref().and_then(|debugger| {
8896 if let (Some(frame), Some(thread_id)) = (debugger.active_frame, debugger.thread_id) {
@@ -103,28 +111,40 @@ impl EditorView {
103111 == doc.path()
104112 {
105113 let line = frame.line - 1; // convert to 0-indexing
106− if line >= view.offset.row && line < view.offset.row + area.height as usize {
107− surface.set_style(
108− Rect::new(
109− area.x,
110− area.y + (line - view.offset.row) as u16,
111− area.width,
112− 1,
113− ),
114− theme.get("ui.highlight"),
115− );
116− }
114+ let style = theme.get("ui.highlight");
115+ let line_decoration = move |renderer: &mut TextRenderer, pos: LinePos| {
116+ if pos.doc_line != line {
117+ return;
118+ }
119+ renderer
120+ .surface
121+ .set_style(Rect::new(area.x, pos.visual_line, area.width, 1), style);
122+ };
123+
124+ line_decorations.push(Box::new(line_decoration));
117125 }
118126 }
119127
120128 if is_focused && config.cursorline {
121− Self::highlight_cursorline(doc, view, surface, theme);
129+ line_decorations.push(Self::cursorline_decorator(doc, view, theme))
122130 }
131+
123132 if is_focused && config.cursorcolumn {
124− Self::highlight_cursorcolumn(doc, view, surface, theme);
133+ Self::highlight_cursorcolumn(doc, view, surface, theme, inner, &text_annotations);
134+ }
135+
136+ let mut highlights =
137+ Self::doc_syntax_highlights(doc, view.offset.anchor, inner.height, theme);
138+ let overlay_highlights = Self::overlay_syntax_highlights(
139+ doc,
140+ view.offset.anchor,
141+ inner.height,
142+ &text_annotations,
143+ );
144+ if !overlay_highlights.is_empty() {
145+ highlights = Box::new(syntax::merge(highlights, overlay_highlights));
125146 }
126147
127− let mut highlights = Self::doc_syntax_highlights(doc, view.offset, inner.height, theme);
128148 for diagnostic in Self::doc_diagnostics_highlights(doc, theme) {
129149 // Most of the `diagnostic` Vecs are empty most of the time. Skipping
130150 // a merge for any empty Vec saves a significant amount of work.
@@ -133,8 +153,9 @@ impl EditorView {
133153 }
134154 highlights = Box::new(syntax::merge(highlights, diagnostic));
135155 }
156+
136157 let highlights: Box<dyn Iterator<Item = HighlightEvent>> = if is_focused {
137− Box::new(syntax::merge(
158+ let highlights = syntax::merge(
138159 highlights,
139160 Self::doc_selection_highlights(
140161 editor.mode(),
@@ -143,19 +164,52 @@ impl EditorView {
143164 theme,
144165 &config.cursor_shape,
145166 ),
146− ))
167+ );
168+ let focused_view_elements = Self::highlight_focused_view_elements(view, doc, theme);
169+ if focused_view_elements.is_empty() {
170+ Box::new(highlights)
171+ } else {
172+ Box::new(syntax::merge(highlights, focused_view_elements))
173+ }
147174 } else {
148175 Box::new(highlights)
149176 };
150177
151− Self::render_text_highlights(doc, view.offset, inner, surface, theme, highlights, &config);
152− Self::render_gutter(editor, doc, view, view.area, surface, theme, is_focused);
153− Self::render_rulers(editor, doc, view, inner, surface, theme);
178+ Self::render_gutter(
179+ editor,
180+ doc,
181+ view,
182+ view.area,
183+ theme,
184+ is_focused,
185+ &mut line_decorations,
186+ );
154187
155188 if is_focused {
156− Self::render_focused_view_elements(view, doc, inner, theme, surface);
189+ let cursor = doc
190+ .selection(view.id)
191+ .primary()
192+ .cursor(doc.text().slice(..));
193+ // set the cursor_cache to out of view in case the position is not found
194+ editor.cursor_cache.set(Some(None));
195+ let update_cursor_cache =
196+ |_: &mut TextRenderer, pos| editor.cursor_cache.set(Some(Some(pos)));
197+ translated_positions.push((cursor, Box::new(update_cursor_cache)));
157198 }
158199
200+ render_document(
201+ surface,
202+ inner,
203+ doc,
204+ view.offset,
205+ &text_annotations,
206+ highlights,
207+ theme,
208+ &mut line_decorations,
209+ &mut *translated_positions,
210+ );
211+ Self::render_rulers(editor, doc, view, inner, surface, theme);
212+
159213 // if we're not at the edge of the screen, draw a right border
160214 if viewport.right() != view.area.right() {
161215 let x = area.right();
@@ -203,31 +257,53 @@ impl EditorView {
203257 .iter()
204258 // View might be horizontally scrolled, convert from absolute distance
205259 // from the 1st column to relative distance from left of viewport
206− .filter_map(|ruler| ruler.checked_sub(1 + view.offset.col as u16))
260+ .filter_map(|ruler| ruler.checked_sub(1 + view.offset.horizontal_offset as u16))
207261 .filter(|ruler| ruler < &viewport.width)
208262 .map(|ruler| viewport.clip_left(ruler).with_width(1))
209263 .for_each(|area| surface.set_style(area, ruler_theme))
210264 }
211265
266+ pub fn overlay_syntax_highlights(
267+ doc: &Document,
268+ anchor: usize,
269+ height: u16,
270+ text_annotations: &TextAnnotations,
271+ ) -> Vec<(usize, std::ops::Range<usize>)> {
272+ let text = doc.text().slice(..);
273+ let row = text.char_to_line(anchor.min(text.len_chars()));
274+
275+ let range = {
276+ // Calculate viewport byte ranges:
277+ // Saturating subs to make it inclusive zero indexing.
278+ let last_line = text.len_lines().saturating_sub(1);
279+ let last_visible_line = (row + height as usize).saturating_sub(1).min(last_line);
280+ let start = text.line_to_byte(row.min(last_line));
281+ let end = text.line_to_byte(last_visible_line + 1);
282+
283+ start..end
284+ };
285+
286+ text_annotations.collect_overlay_highlights(range)
287+ }
288+
212289 /// Get syntax highlights for a document in a view represented by the first line
213290 /// and column (`offset`) and the last line. This is done instead of using a view
214291 /// directly to enable rendering syntax highlighted docs anywhere (eg. picker preview)
215292 pub fn doc_syntax_highlights<'doc>(
216293 doc: &'doc Document,
217− offset: Position,
294+ anchor: usize,
218295 height: u16,
219296 _theme: &Theme,
220297 ) -> Box<dyn Iterator<Item = HighlightEvent> + 'doc> {
221298 let text = doc.text().slice(..);
299+ let row = text.char_to_line(anchor.min(text.len_chars()));
222300
223301 let range = {
224302 // Calculate viewport byte ranges:
225303 // Saturating subs to make it inclusive zero indexing.
226− let last_line = doc.text().len_lines().saturating_sub(1);
227− let last_visible_line = (offset.row + height as usize)
228− .saturating_sub(1)
229− .min(last_line);
230− let start = text.line_to_byte(offset.row.min(last_line));
304+ let last_line = text.len_lines().saturating_sub(1);
305+ let last_visible_line = (row + height as usize).saturating_sub(1).min(last_line);
306+ let start = text.line_to_byte(row.min(last_line));
231307 let end = text.line_to_byte(last_visible_line + 1);
232308
233309 start..end
@@ -272,11 +348,11 @@ impl EditorView {
272348 use helix_core::diagnostic::Severity;
273349 let get_scope_of = |scope| {
274350 theme
275− .find_scope_index(scope)
351+ .find_scope_index_exact(scope)
276352 // get one of the themes below as fallback values
277− .or_else(|| theme.find_scope_index("diagnostic"))
278− .or_else(|| theme.find_scope_index("ui.cursor"))
279− .or_else(|| theme.find_scope_index("ui.selection"))
353+ .or_else(|| theme.find_scope_index_exact("diagnostic"))
354+ .or_else(|| theme.find_scope_index_exact("ui.cursor"))
355+ .or_else(|| theme.find_scope_index_exact("ui.selection"))
280356 .expect(
281357 "at least one of the following scopes must be defined in the theme: `diagnostic`, `ui.cursor`, or `ui.selection`",
282358 )
@@ -339,29 +415,29 @@ impl EditorView {
339415 let cursor_is_block = cursorkind == CursorKind::Block;
340416
341417 let selection_scope = theme
342− .find_scope_index("ui.selection")
418+ .find_scope_index_exact("ui.selection")
343419 .expect("could not find `ui.selection` scope in the theme!");
344420 let primary_selection_scope = theme
345− .find_scope_index("ui.selection.primary")
421+ .find_scope_index_exact("ui.selection.primary")
346422 .unwrap_or(selection_scope);
347423 let base_cursor_scope = theme
348− .find_scope_index("ui.cursor")
424+ .find_scope_index_exact("ui.cursor")
349425 .unwrap_or(selection_scope);
350426 let base_primary_cursor_scope = theme
351427 .find_scope_index("ui.cursor.primary")
352428 .unwrap_or(base_cursor_scope);
353429
354430 let cursor_scope = match mode {
355− Mode::Insert => theme.find_scope_index("ui.cursor.insert"),
356− Mode::Select => theme.find_scope_index("ui.cursor.select"),
357− Mode::Normal => theme.find_scope_index("ui.cursor.normal"),
431+ Mode::Insert => theme.find_scope_index_exact("ui.cursor.insert"),
432+ Mode::Select => theme.find_scope_index_exact("ui.cursor.select"),
433+ Mode::Normal => theme.find_scope_index_exact("ui.cursor.normal"),
358434 }
359435 .unwrap_or(base_cursor_scope);
360436
361437 let primary_cursor_scope = match mode {
362− Mode::Insert => theme.find_scope_index("ui.cursor.primary.insert"),
363− Mode::Select => theme.find_scope_index("ui.cursor.primary.select"),
364− Mode::Normal => theme.find_scope_index("ui.cursor.primary.normal"),
438+ Mode::Insert => theme.find_scope_index_exact("ui.cursor.primary.insert"),
439+ Mode::Select => theme.find_scope_index_exact("ui.cursor.primary.select"),
440+ Mode::Normal => theme.find_scope_index_exact("ui.cursor.primary.normal"),
365441 }
366442 .unwrap_or(base_primary_cursor_scope);
367443
@@ -424,248 +500,26 @@ impl EditorView {
424500 spans
425501 }
426502
427− pub fn render_text_highlights<H: Iterator<Item = HighlightEvent>>(
428− doc: &Document,
429− offset: Position,
430− viewport: Rect,
431− surface: &mut Surface,
432− theme: &Theme,
433− highlights: H,
434− config: &helix_view::editor::Config,
435− ) {
436− let whitespace = &config.whitespace;
437− use helix_view::editor::WhitespaceRenderValue;
438−
439− // It's slightly more efficient to produce a full RopeSlice from the Rope, then slice that a bunch
440− // of times than it is to always call Rope::slice/get_slice (it will internally always hit RSEnum::Light).
441− let text = doc.text().slice(..);
442−
443− let characters = &whitespace.characters;
444−
445− let mut spans = Vec::new();
446− let mut visual_x = 0usize;
447− let mut line = 0u16;
448− let tab_width = doc.tab_width();
449− let tab = if whitespace.render.tab() == WhitespaceRenderValue::All {
450− std::iter::once(characters.tab)
451− .chain(std::iter::repeat(characters.tabpad).take(tab_width - 1))
452− .collect()
453− } else {
454− " ".repeat(tab_width)
455− };
456− let space = characters.space.to_string();
457− let nbsp = characters.nbsp.to_string();
458− let newline = if whitespace.render.newline() == WhitespaceRenderValue::All {
459− characters.newline.to_string()
460− } else {
461− " ".to_string()
462− };
463− let indent_guide_char = config.indent_guides.character.to_string();
464−
465− let text_style = theme.get("ui.text");
466− let whitespace_style = theme.get("ui.virtual.whitespace");
467−
468− let mut is_in_indent_area = true;
469− let mut last_line_indent_level = 0;
470−
471− // use whitespace style as fallback for indent-guide
472− let indent_guide_style = text_style.patch(
473− theme
474− .try_get("ui.virtual.indent-guide")
475− .unwrap_or_else(|| theme.get("ui.virtual.whitespace")),
476− );
477−
478− let draw_indent_guides = |indent_level, line, surface: &mut Surface| {
479− if !config.indent_guides.render {
480− return;
481− }
482−
483− let starting_indent =
484− (offset.col / tab_width) + config.indent_guides.skip_levels as usize;
485−
486− // Don't draw indent guides outside of view
487− let end_indent = min(
488− indent_level,
489− // Add tab_width - 1 to round up, since the first visible
490− // indent might be a bit after offset.col
491− offset.col + viewport.width as usize + (tab_width - 1),
492− ) / tab_width;
493−
494− for i in starting_indent..end_indent {
495− let x = (viewport.x as usize + (i * tab_width) - offset.col) as u16;
496− let y = viewport.y + line;
497− debug_assert!(surface.in_bounds(x, y));
498− surface.set_string(x, y, &indent_guide_char, indent_guide_style);
499− }
500− };
501−
502− 'outer: for event in highlights {
503− match event {
504− HighlightEvent::HighlightStart(span) => {
505− spans.push(span);
506− }
507− HighlightEvent::HighlightEnd => {
508− spans.pop();
509− }
510− HighlightEvent::Source { start, end } => {
511− let is_trailing_cursor = text.len_chars() < end;
512−
513− // `unwrap_or_else` part is for off-the-end indices of
514− // the rope, to allow cursor highlighting at the end
515− // of the rope.
516− let text = text.get_slice(start..end).unwrap_or_else(|| " ".into());
517− let style = spans
518− .iter()
519− .fold(text_style, |acc, span| acc.patch(theme.highlight(span.0)));
520−
521− let space = if whitespace.render.space() == WhitespaceRenderValue::All
522− && !is_trailing_cursor
523− {
524− &space
525− } else {
526− " "
527− };
528−
529− let nbsp = if whitespace.render.nbsp() == WhitespaceRenderValue::All
530− && text.len_chars() < end
531− {
532− &nbsp
533− } else {
534− " "
535− };
536−
537− use helix_core::graphemes::{grapheme_width, RopeGraphemes};
538−
539− for grapheme in RopeGraphemes::new(text) {
540− let out_of_bounds = offset.col > visual_x
541− || visual_x >= viewport.width as usize + offset.col;
542−
543− if LineEnding::from_rope_slice(&grapheme).is_some() {
544− if !out_of_bounds {
545− // we still want to render an empty cell with the style
546− surface.set_string(
547− (viewport.x as usize + visual_x - offset.col) as u16,
548− viewport.y + line,
549− &newline,
550− style.patch(whitespace_style),
551− );
552− }
553−
554− draw_indent_guides(last_line_indent_level, line, surface);
555−
556− visual_x = 0;
557− line += 1;
558− is_in_indent_area = true;
559−
560− // TODO: with proper iter this shouldn't be necessary
561− if line >= viewport.height {
562− break 'outer;
563− }
564− } else {
565− let grapheme = Cow::from(grapheme);
566− let is_whitespace;
567−
568− let (display_grapheme, width) = if grapheme == "\t" {
569− is_whitespace = true;
570− // make sure we display tab as appropriate amount of spaces
571− let visual_tab_width = tab_width - (visual_x % tab_width);
572− let grapheme_tab_width =
573− helix_core::str_utils::char_to_byte_idx(&tab, visual_tab_width);
574−
575− (&tab[..grapheme_tab_width], visual_tab_width)
576− } else if grapheme == " " {
577− is_whitespace = true;
578− (space, 1)
579− } else if grapheme == "\u{00A0}" {
580− is_whitespace = true;
581− (nbsp, 1)
582− } else {
583− is_whitespace = false;
584− // Cow will prevent allocations if span contained in a single slice
585− // which should really be the majority case
586− let width = grapheme_width(&grapheme);
587− (grapheme.as_ref(), width)
588− };
589−
590− let cut_off_start = offset.col.saturating_sub(visual_x);
591−
592− if !out_of_bounds {
593− // if we're offscreen just keep going until we hit a new line
594− surface.set_string(
595− (viewport.x as usize + visual_x - offset.col) as u16,
596− viewport.y + line,
597− display_grapheme,
598− if is_whitespace {
599− style.patch(whitespace_style)
600− } else {
601− style
602− },
603− );
604− } else if cut_off_start != 0 && cut_off_start < width {
605− // partially on screen
606− let rect = Rect::new(
607− viewport.x,
608− viewport.y + line,
609− (width - cut_off_start) as u16,
610− 1,
611− );
612− surface.set_style(
613− rect,
614− if is_whitespace {
615− style.patch(whitespace_style)
616− } else {
617− style
618− },
619− );
620− }
621−
622− if is_in_indent_area && !(grapheme == " " || grapheme == "\t") {
623− draw_indent_guides(visual_x, line, surface);
624− is_in_indent_area = false;
625− last_line_indent_level = visual_x;
626− }
627−
628− visual_x = visual_x.saturating_add(width);
629− }
630− }
631− }
632− }
633− }
634− }
635−
636503 /// Render brace match, etc (meant for the focused view only)
637− pub fn render_focused_view_elements(
504+ pub fn highlight_focused_view_elements(
638505 view: &View,
639506 doc: &Document,
640− viewport: Rect,
641507 theme: &Theme,
642− surface: &mut Surface,
643− ) {
508+ ) -> Vec<(usize, std::ops::Range<usize>)> {
644509 // Highlight matching braces
645510 if let Some(syntax) = doc.syntax() {
646511 let text = doc.text().slice(..);
647512 use helix_core::match_brackets;
648513 let pos = doc.selection(view.id).primary().cursor(text);
649514
650− let pos = match_brackets::find_matching_bracket(syntax, doc.text(), pos)
651− .and_then(|pos| view.screen_coords_at_pos(doc, text, pos));
652−
653− if let Some(pos) = pos {
515+ if let Some(pos) = match_brackets::find_matching_bracket(syntax, doc.text(), pos) {
654516 // ensure col is on screen
655− if (pos.col as u16) < viewport.width + view.offset.col as u16
656− && pos.col >= view.offset.col
657− {
658− let style = theme.try_get("ui.cursor.match").unwrap_or_else(|| {
659− Style::default()
660− .add_modifier(Modifier::REVERSED)
661− .add_modifier(Modifier::DIM)
662− });
663−
664− surface[(viewport.x + pos.col as u16, viewport.y + pos.row as u16)]
665− .set_style(style);
517+ if let Some(highlight) = theme.find_scope_index_exact("ui.cursor.match") {
518+ return vec![(highlight, pos..pos + 1)];
666519 }
667520 }
668521 }
522+ Vec::new()
669523 }
670524
671525 /// Render bufferline at the top
@@ -721,22 +575,17 @@ impl EditorView {
721575 }
722576 }
723577
724− pub fn render_gutter(
725− editor: &Editor,
726− doc: &Document,
578+ pub fn render_gutter<'d>(
579+ editor: &'d Editor,
580+ doc: &'d Document,
727581 view: &View,
728582 viewport: Rect,
729− surface: &mut Surface,
730583 theme: &Theme,
731584 is_focused: bool,
585+ line_decorations: &mut Vec<Box<(dyn LineDecoration + 'd)>>,
732586 ) {
733587 let text = doc.text().slice(..);
734− let last_line = view.last_line(doc);
735−
736− // it's used inside an iterator so the collect isn't needless:
737− // https://github.com/rust-lang/rust-clippy/issues/6164
738− #[allow(clippy::needless_collect)]
739− let cursors: Vec<_> = doc
588+ let cursors: Rc<[_]> = doc
740589 .selection(view.id)
741590 .iter()
742591 .map(|range| range.cursor_line(text))
@@ -746,29 +595,36 @@ impl EditorView {
746595
747596 let gutter_style = theme.get("ui.gutter");
748597 let gutter_selected_style = theme.get("ui.gutter.selected");
749−
750− // avoid lots of small allocations by reusing a text buffer for each line
751− let mut text = String::with_capacity(8);
598+ let gutter_style_virtual = theme.get("ui.gutter.virtual");
599+ let gutter_selected_style_virtual = theme.get("ui.gutter.selected.virtual");
752600
753601 for gutter_type in view.gutters() {
754602 let mut gutter = gutter_type.style(editor, doc, view, theme, is_focused);
755603 let width = gutter_type.width(view, doc);
756− text.reserve(width); // ensure there's enough space for the gutter
757− for (i, line) in (view.offset.row..(last_line + 1)).enumerate() {
758− let selected = cursors.contains(&line);
604+ // avoid lots of small allocations by reusing a text buffer for each line
605+ let mut text = String::with_capacity(width);
606+ let cursors = cursors.clone();
607+ let gutter_decoration = move |renderer: &mut TextRenderer, pos: LinePos| {
608+ // TODO handle softwrap in gutters
609+ let selected = cursors.contains(&pos.doc_line);
759610 let x = viewport.x + offset;
760− let y = viewport.y + i as u16;
611+ let y = viewport.y + pos.visual_line;
761612
762− let gutter_style = if selected {
763− gutter_selected_style
764− } else {
765− gutter_style
613+ let gutter_style = match (selected, pos.first_visual_line) {
614+ (false, true) => gutter_style,
615+ (true, true) => gutter_selected_style,
616+ (false, false) => gutter_style_virtual,
617+ (true, false) => gutter_selected_style_virtual,
766618 };
767619
768− if let Some(style) = gutter(line, selected, &mut text) {
769− surface.set_stringn(x, y, &text, width, gutter_style.patch(style));
620+ if let Some(style) =
621+ gutter(pos.doc_line, selected, pos.first_visual_line, &mut text)
622+ {
623+ renderer
624+ .surface
625+ .set_stringn(x, y, &text, width, gutter_style.patch(style));
770626 } else {
771− surface.set_style(
627+ renderer.surface.set_style(
772628 Rect {
773629 x,
774630 y,
@@ -779,7 +635,8 @@ impl EditorView {
779635 );
780636 }
781637 text.clear();
782− }
638+ };
639+ line_decorations.push(Box::new(gutter_decoration));
783640
784641 offset += width as u16;
785642 }
@@ -840,10 +697,13 @@ impl EditorView {
840697 }
841698
842699 /// Apply the highlighting on the lines where a cursor is active
843− pub fn highlight_cursorline(doc: &Document, view: &View, surface: &mut Surface, theme: &Theme) {
700+ pub fn cursorline_decorator(
701+ doc: &Document,
702+ view: &View,
703+ theme: &Theme,
704+ ) -> Box<dyn LineDecoration> {
844705 let text = doc.text().slice(..);
845− let last_line = view.last_line(doc);
846−
706+ // TODO only highlight the visual line that contains the cursor instead of the full visual line
847707 let primary_line = doc.selection(view.id).primary().cursor_line(text);
848708
849709 // The secondary_lines do contain the primary_line, it doesn't matter
@@ -860,20 +720,23 @@ impl EditorView {
860720
861721 let primary_style = theme.get("ui.cursorline.primary");
862722 let secondary_style = theme.get("ui.cursorline.secondary");
723+ let viewport = view.area;
863724
864− for line in view.offset.row..(last_line + 1) {
725+ let line_decoration = move |renderer: &mut TextRenderer, pos: LinePos| {
865726 let area = Rect::new(
866− view.area.x,
867− view.area.y + (line - view.offset.row) as u16,
868− view.area.width,
727+ viewport.x,
728+ viewport.y + pos.visual_line as u16,
729+ viewport.width,
869730 1,
870731 );
871− if primary_line == line {
872− surface.set_style(area, primary_style);
873− } else if secondary_lines.binary_search(&line).is_ok() {
874− surface.set_style(area, secondary_style);
732+ if primary_line == pos.doc_line {
733+ renderer.surface.set_style(area, primary_style);
734+ } else if secondary_lines.binary_search(&pos.doc_line).is_ok() {
735+ renderer.surface.set_style(area, secondary_style);
875736 }
876− }
737+ };
738+
739+ Box::new(line_decoration)
877740 }
878741
879742 /// Apply the highlighting on the columns where a cursor is active
@@ -882,6 +745,8 @@ impl EditorView {
882745 view: &View,
883746 surface: &mut Surface,
884747 theme: &Theme,
748+ viewport: Rect,
749+ text_annotations: &TextAnnotations,
885750 ) {
886751 let text = doc.text().slice(..);
887752
@@ -897,19 +762,23 @@ impl EditorView {
897762 .unwrap_or_else(|| theme.get("ui.cursorline.secondary"));
898763
899764 let inner_area = view.inner_area(doc);
900− let offset = view.offset.col;
901765
902766 let selection = doc.selection(view.id);
903767 let primary = selection.primary();
768+ let text_format = doc.text_format(viewport.width, None);
904769 for range in selection.iter() {
905770 let is_primary = primary == *range;
771+ let cursor = range.cursor(text);
772+
773+ let Position { col, .. } =
774+ visual_offset_from_block(text, cursor, cursor, &text_format, text_annotations).0;
906775
907− let Position { row: _, col } =
908− visual_coords_at_pos(text, range.cursor(text), doc.tab_width());
909776 // if the cursor is horizontally in the view
910− if col >= offset && inner_area.width > (col - offset) as u16 {
777+ if col >= view.offset.horizontal_offset
778+ && inner_area.width > (col - view.offset.horizontal_offset) as u16
779+ {
911780 let area = Rect::new(
912− inner_area.x + (col - offset) as u16,
781+ inner_area.x + (col - view.offset.horizontal_offset) as u16,
913782 view.area.y,
914783 1,
915784 view.area.height,
@@ -1149,7 +1018,7 @@ impl EditorView {
11491018
11501019 let pos_and_view = |editor: &Editor, row, column| {
11511020 editor.tree.views().find_map(|(view, _focus)| {
1152− view.pos_at_screen_coords(&editor.documents[&view.doc], row, column)
1021+ view.pos_at_screen_coords(&editor.documents[&view.doc], row, column, true)
11531022 .map(|pos| (pos, view.id))
11541023 })
11551024 };
@@ -1191,8 +1060,10 @@ impl EditorView {
11911060 None => return EventResult::Ignored(None),
11921061 };
11931062
1194− let line = coords.row + view.offset.row;
1195− if line < doc.text().len_lines() {
1063+ if let Some(char_idx) =
1064+ view.pos_at_visual_coords(doc, coords.row as u16, coords.col as u16, true)
1065+ {
1066+ let line = doc.text().char_to_line(char_idx);
11961067 commands::dap_toggle_breakpoint_impl(cxt, path, line);
11971068 return EventResult::Consumed(None);
11981069 }
@@ -1204,7 +1075,7 @@ impl EditorView {
12041075 MouseEventKind::Drag(MouseButton::Left) => {
12051076 let (view, doc) = current!(cxt.editor);
12061077
1207− let pos = match view.pos_at_screen_coords(doc, row, column) {
1078+ let pos = match view.pos_at_screen_coords(doc, row, column, true) {
12081079 Some(pos) => pos,
12091080 None => return EventResult::Ignored(None),
12101081 };
@@ -1268,8 +1139,9 @@ impl EditorView {
12681139 cxt.editor.focus(view_id);
12691140
12701141 let (view, doc) = current!(cxt.editor);
1271− let line = coords.row + view.offset.row;
1272− if let Ok(pos) = doc.text().try_line_to_char(line) {
1142+ if let Some(pos) =
1143+ view.pos_at_visual_coords(doc, coords.row as u16, coords.col as u16, true)
1144+ {
12731145 doc.set_selection(view_id, Selection::point(pos));
12741146 if modifiers == KeyModifiers::ALT {
12751147 commands::MappableCommand::dap_edit_log.execute(cxt);
helix-term/src/ui/lsp.rs+4−1
@@ -53,7 +53,10 @@ impl Component for SignatureHelp {
5353
5454 let active_param_span = self.active_param_range.map(|(start, end)| {
5555 vec![(
56− cx.editor.theme.find_scope_index("ui.selection").unwrap(),
56+ cx.editor
57+ .theme
58+ .find_scope_index_exact("ui.selection")
59+ .unwrap(),
5760 start..end,
5861 )]
5962 });
helix-term/src/ui/mod.rs+1
@@ -1,4 +1,5 @@
11mod completion;
2+mod document;
23pub(crate) mod editor;
34mod fuzzy_match;
45mod info;
helix-term/src/ui/picker.rs+52−29
@@ -2,7 +2,12 @@ use crate::{
22 alt,
33 compositor::{Component, Compositor, Context, Event, EventResult},
44 ctrl, key, shift,
5− ui::{self, fuzzy_match::FuzzyQuery, EditorView},
5+ ui::{
6+ self,
7+ document::{render_document, LineDecoration, LinePos, TextRenderer},
8+ fuzzy_match::FuzzyQuery,
9+ EditorView,
10+ },
611};
712use futures_util::future::BoxFuture;
813use tui::{
@@ -19,11 +24,15 @@ use std::cmp::{self, Ordering};
1924use std::{collections::HashMap, io::Read, path::PathBuf};
2025
2126use crate::ui::{Prompt, PromptEvent};
22−use helix_core::{movement::Direction, unicode::segmentation::UnicodeSegmentation, Position};
27+use helix_core::{
28+ movement::Direction, text_annotations::TextAnnotations,
29+ unicode::segmentation::UnicodeSegmentation, Position,
30+};
2331use helix_view::{
2432 editor::Action,
2533 graphics::{CursorKind, Margin, Modifier, Rect},
2634 theme::Style,
35+ view::ViewPosition,
2736 Document, DocumentId, Editor,
2837};
2938
@@ -179,7 +188,7 @@ impl<T: Item> FilePicker<T> {
179188 }
180189 _ => {
181190 // TODO: enable syntax highlighting; blocked by async rendering
182− Document::open(path, None, None)
191+ Document::open(path, None, None, editor.config.clone())
183192 .map(|doc| CachedPreview::Document(Box::new(doc)))
184193 .unwrap_or(CachedPreview::NotFound)
185194 }
@@ -283,43 +292,57 @@ impl<T: Item + 'static> Component for FilePicker<T> {
283292 })
284293 .unwrap_or(0);
285294
286− let offset = Position::new(first_line, 0);
295+ let offset = ViewPosition {
296+ anchor: doc.text().line_to_char(first_line),
297+ horizontal_offset: 0,
298+ vertical_offset: 0,
299+ };
287300
288− let mut highlights =
289− EditorView::doc_syntax_highlights(doc, offset, area.height, &cx.editor.theme);
301+ let mut highlights = EditorView::doc_syntax_highlights(
302+ doc,
303+ offset.anchor,
304+ area.height,
305+ &cx.editor.theme,
306+ );
290307 for spans in EditorView::doc_diagnostics_highlights(doc, &cx.editor.theme) {
291308 if spans.is_empty() {
292309 continue;
293310 }
294311 highlights = Box::new(helix_core::syntax::merge(highlights, spans));
295312 }
296− EditorView::render_text_highlights(
313+ let mut decorations: Vec<Box<dyn LineDecoration>> = Vec::new();
314+
315+ if let Some((start, end)) = range {
316+ let style = cx
317+ .editor
318+ .theme
319+ .try_get("ui.highlight")
320+ .unwrap_or_else(|| cx.editor.theme.get("ui.selection"));
321+ let draw_highlight = move |renderer: &mut TextRenderer, pos: LinePos| {
322+ if (start..=end).contains(&pos.doc_line) {
323+ let area = Rect::new(
324+ renderer.viewport.x,
325+ renderer.viewport.y + pos.visual_line,
326+ renderer.viewport.width,
327+ 1,
328+ );
329+ renderer.surface.set_style(area, style)
330+ }
331+ };
332+ decorations.push(Box::new(draw_highlight))
333+ }
334+
335+ render_document(
336+ surface,
337+ inner,
297338 doc,
298339 offset,
299− inner,
300− surface,
301− &cx.editor.theme,
340+ &TextAnnotations::default(),
302341 highlights,
303− &cx.editor.config(),
342+ &cx.editor.theme,
343+ &mut decorations,
344+ &mut [],
304345 );
305−
306− // highlight the line
307− if let Some((start, end)) = range {
308− let offset = start.saturating_sub(first_line) as u16;
309− surface.set_style(
310− Rect::new(
311− inner.x,
312− inner.y + offset,
313− inner.width,
314− (end.saturating_sub(start) as u16 + 1)
315− .min(inner.height.saturating_sub(offset)),
316− ),
317− cx.editor
318− .theme
319− .try_get("ui.highlight")
320− .unwrap_or_else(|| cx.editor.theme.get("ui.selection")),
321− );
322− }
323346 }
324347 }
325348
helix-view/src/document.rs+61−13
@@ -1,7 +1,11 @@
11use anyhow::{anyhow, bail, Context, Error};
2+use arc_swap::access::DynAccess;
23use futures_util::future::BoxFuture;
34use futures_util::FutureExt;
45use helix_core::auto_pairs::AutoPairs;
6+use helix_core::doc_formatter::TextFormat;
7+use helix_core::syntax::Highlight;
8+use helix_core::text_annotations::TextAnnotations;
59use helix_core::Range;
610use helix_vcs::{DiffHandle, DiffProviderRegistry};
711
@@ -26,8 +30,8 @@ use helix_core::{
2630 DEFAULT_LINE_ENDING,
2731};
2832
29−use crate::editor::RedrawHandle;
30−use crate::{DocumentId, Editor, View, ViewId};
33+use crate::editor::{Config, RedrawHandle};
34+use crate::{DocumentId, Editor, Theme, View, ViewId};
3135
3236/// 8kB of buffer space for encoding and decoding `Rope`s.
3337const BUF_SIZE: usize = 8192;
@@ -127,6 +131,7 @@ pub struct Document {
127131 // it back as it separated from the edits. We could split out the parts manually but that will
128132 // be more troublesome.
129133 pub history: Cell<History>,
134+ pub config: Arc<dyn DynAccess<Config>>,
130135
131136 pub savepoint: Option<Transaction>,
132137
@@ -351,7 +356,11 @@ use helix_lsp::lsp;
351356use url::Url;
352357
353358impl Document {
354− pub fn from(text: Rope, encoding: Option<&'static encoding::Encoding>) -> Self {
359+ pub fn from(
360+ text: Rope,
361+ encoding: Option<&'static encoding::Encoding>,
362+ config: Arc<dyn DynAccess<Config>>,
363+ ) -> Self {
355364 let encoding = encoding.unwrap_or(encoding::UTF_8);
356365 let changes = ChangeSet::new(&text);
357366 let old_state = None;
@@ -377,9 +386,13 @@ impl Document {
377386 modified_since_accessed: false,
378387 language_server: None,
379388 diff_handle: None,
389+ config,
380390 }
381391 }
382−
392+ pub fn default(config: Arc<dyn DynAccess<Config>>) -> Self {
393+ let text = Rope::from(DEFAULT_LINE_ENDING.as_str());
394+ Self::from(text, None, config)
395+ }
383396 // TODO: async fn?
384397 /// Create a new document from `path`. Encoding is auto-detected, but it can be manually
385398 /// overwritten with the `encoding` parameter.
@@ -387,6 +400,7 @@ impl Document {
387400 path: &Path,
388401 encoding: Option<&'static encoding::Encoding>,
389402 config_loader: Option<Arc<syntax::Loader>>,
403+ config: Arc<dyn DynAccess<Config>>,
390404 ) -> Result<Self, Error> {
391405 // Open the file if it exists, otherwise assume it is a new file (and thus empty).
392406 let (rope, encoding) = if path.exists() {
@@ -398,7 +412,7 @@ impl Document {
398412 (Rope::from(DEFAULT_LINE_ENDING.as_str()), encoding)
399413 };
400414
401− let mut doc = Self::from(rope, Some(encoding));
415+ let mut doc = Self::from(rope, Some(encoding), config);
402416
403417 // set the path and try detecting the language
404418 doc.set_path(Some(path))?;
@@ -1192,12 +1206,34 @@ impl Document {
11921206 None => global_config,
11931207 }
11941208 }
1195−}
11961209
1197−impl Default for Document {
1198− fn default() -> Self {
1199− let text = Rope::from(DEFAULT_LINE_ENDING.as_str());
1200− Self::from(text, None)
1210+ pub fn text_format(&self, mut viewport_width: u16, theme: Option<&Theme>) -> TextFormat {
1211+ if let Some(max_line_len) = self
1212+ .language_config()
1213+ .and_then(|config| config.max_line_length)
1214+ {
1215+ viewport_width = viewport_width.min(max_line_len as u16)
1216+ }
1217+ let config = self.config.load();
1218+ let soft_wrap = &config.soft_wrap;
1219+ let tab_width = self.tab_width() as u16;
1220+ TextFormat {
1221+ soft_wrap: soft_wrap.enable && viewport_width > 10,
1222+ tab_width,
1223+ max_wrap: soft_wrap.max_wrap.min(viewport_width / 4),
1224+ max_indent_retain: soft_wrap.max_indent_retain.min(viewport_width * 2 / 5),
1225+ // avoid spinning forever when the window manager
1226+ // sets the size to something tiny
1227+ viewport_width,
1228+ wrap_indicator: soft_wrap.wrap_indicator.clone().into_boxed_str(),
1229+ wrap_indicator_highlight: theme
1230+ .and_then(|theme| theme.find_scope_index("ui.virtual.wrap"))
1231+ .map(Highlight),
1232+ }
1233+ }
1234+
1235+ pub fn text_annotations(&self, _theme: Option<&Theme>) -> TextAnnotations {
1236+ TextAnnotations::default()
12011237 }
12021238}
12031239
@@ -1236,13 +1272,19 @@ impl Display for FormatterError {
12361272
12371273#[cfg(test)]
12381274mod test {
1275+ use arc_swap::ArcSwap;
1276+
12391277 use super::*;
12401278
12411279 #[test]
12421280 fn changeset_to_changes_ignore_line_endings() {
12431281 use helix_lsp::{lsp, Client, OffsetEncoding};
12441282 let text = Rope::from("hello\r\nworld");
1245− let mut doc = Document::from(text, None);
1283+ let mut doc = Document::from(
1284+ text,
1285+ None,
1286+ Arc::new(ArcSwap::new(Arc::new(Config::default()))),
1287+ );
12461288 let view = ViewId::default();
12471289 doc.set_selection(view, Selection::single(0, 0));
12481290
@@ -1276,7 +1318,11 @@ mod test {
12761318 fn changeset_to_changes() {
12771319 use helix_lsp::{lsp, Client, OffsetEncoding};
12781320 let text = Rope::from("hello");
1279− let mut doc = Document::from(text, None);
1321+ let mut doc = Document::from(
1322+ text,
1323+ None,
1324+ Arc::new(ArcSwap::new(Arc::new(Config::default()))),
1325+ );
12801326 let view = ViewId::default();
12811327 doc.set_selection(view, Selection::single(5, 5));
12821328
@@ -1389,7 +1435,9 @@ mod test {
13891435 #[test]
13901436 fn test_line_ending() {
13911437 assert_eq!(
1392− Document::default().text().to_string(),
1438+ Document::default(Arc::new(ArcSwap::new(Arc::new(Config::default()))))
1439+ .text()
1440+ .to_string(),
13931441 DEFAULT_LINE_ENDING.as_str()
13941442 );
13951443 }
helix-view/src/editor.rs+76−9
@@ -7,6 +7,7 @@ use crate::{
77 input::KeyEvent,
88 theme::{self, Theme},
99 tree::{self, Tree},
10+ view::ViewPosition,
1011 Align, Document, DocumentId, View, ViewId,
1112};
1213use helix_vcs::DiffProviderRegistry;
@@ -18,6 +19,7 @@ use tokio_stream::wrappers::UnboundedReceiverStream;
1819
1920use std::{
2021 borrow::Cow,
22+ cell::Cell,
2123 collections::{BTreeMap, HashMap},
2224 io::stdin,
2325 num::NonZeroUsize,
@@ -268,6 +270,44 @@ pub struct Config {
268270 pub indent_guides: IndentGuidesConfig,
269271 /// Whether to color modes with different colors. Defaults to `false`.
270272 pub color_modes: bool,
273+ pub soft_wrap: SoftWrap,
274+}
275+
276+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
277+#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
278+pub struct SoftWrap {
279+ /// Soft wrap lines that exceed viewport width. Default to off
280+ pub enable: bool,
281+ /// Maximum space left free at the end of the line.
282+ /// This space is used to wrap text at word boundaries. If that is not possible within this limit
283+ /// the word is simply split at the end of the line.
284+ ///
285+ /// This is automatically hard-limited to a quarter of the viewport to ensure correct display on small views.
286+ ///
287+ /// Default to 20
288+ pub max_wrap: u16,
289+ /// Maximum number of indentation that can be carried over from the previous line when softwrapping.
290+ /// If a line is indented further then this limit it is rendered at the start of the viewport instead.
291+ ///
292+ /// This is automatically hard-limited to a quarter of the viewport to ensure correct display on small views.
293+ ///
294+ /// Default to 40
295+ pub max_indent_retain: u16,
296+ /// Indicator placed at the beginning of softwrapped lines
297+ ///
298+ /// Defaults to ↪
299+ pub wrap_indicator: String,
300+}
301+
302+impl Default for SoftWrap {
303+ fn default() -> Self {
304+ SoftWrap {
305+ enable: false,
306+ max_wrap: 20,
307+ max_indent_retain: 40,
308+ wrap_indicator: "↪ ".into(),
309+ }
310+ }
271311}
272312
273313#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
@@ -717,6 +757,7 @@ impl Default for Config {
717757 bufferline: BufferLine::default(),
718758 indent_guides: IndentGuidesConfig::default(),
719759 color_modes: false,
760+ soft_wrap: SoftWrap::default(),
720761 }
721762 }
722763}
@@ -797,7 +838,7 @@ pub struct Editor {
797838 pub status_msg: Option<(Cow<'static, str>, Severity)>,
798839 pub autoinfo: Option<Info>,
799840
800− pub config: Box<dyn DynAccess<Config>>,
841+ pub config: Arc<dyn DynAccess<Config>>,
801842 pub auto_pairs: Option<AutoPairs>,
802843
803844 pub idle_timer: Pin<Box<Sleep>>,
@@ -813,6 +854,19 @@ pub struct Editor {
813854 /// The `RwLock` blocks the editor from performing the render until an exclusive lock can be aquired
814855 pub redraw_handle: RedrawHandle,
815856 pub needs_redraw: bool,
857+ /// Cached position of the cursor calculated during rendering.
858+ /// The content of `cursor_cache` is returned by `Editor::cursor` if
859+ /// set to `Some(_)`. The value will be cleared after it's used.
860+ /// If `cursor_cache` is `None` then the `Editor::cursor` function will
861+ /// calculate the cursor position.
862+ ///
863+ /// `Some(None)` represents a cursor position outside of the visible area.
864+ /// This will just cause `Editor::cursor` to return `None`.
865+ ///
866+ /// This cache is only a performance optimization to
867+ /// avoid calculating the cursor position multiple
868+ /// times during rendering and should not be set by other functions.
869+ pub cursor_cache: Cell<Option<Option<Position>>>,
816870}
817871
818872pub type RedrawHandle = (Arc<Notify>, Arc<RwLock<()>>);
@@ -866,7 +920,7 @@ impl Editor {
866920 mut area: Rect,
867921 theme_loader: Arc<theme::Loader>,
868922 syn_loader: Arc<syntax::Loader>,
869− config: Box<dyn DynAccess<Config>>,
923+ config: Arc<dyn DynAccess<Config>>,
870924 ) -> Self {
871925 let conf = config.load();
872926 let auto_pairs = (&conf.auto_pairs).into();
@@ -910,6 +964,7 @@ impl Editor {
910964 config_events: unbounded_channel(),
911965 redraw_handle: Default::default(),
912966 needs_redraw: false,
967+ cursor_cache: Cell::new(None),
913968 }
914969 }
915970
@@ -994,7 +1049,7 @@ impl Editor {
9941049
9951050 fn set_theme_impl(&mut self, theme: Theme, preview: ThemeAction) {
9961051 // `ui.selection` is the only scope required to be able to render a theme.
997− if theme.find_scope_index("ui.selection").is_none() {
1052+ if theme.find_scope_index_exact("ui.selection").is_none() {
9981053 self.set_error("Invalid theme: `ui.selection` required");
9991054 return;
10001055 }
@@ -1077,7 +1132,7 @@ impl Editor {
10771132 fn replace_document_in_view(&mut self, current_view: ViewId, doc_id: DocumentId) {
10781133 let view = self.tree.get_mut(current_view);
10791134 view.doc = doc_id;
1080− view.offset = Position::default();
1135+ view.offset = ViewPosition::default();
10811136
10821137 let doc = doc_mut!(self, &doc_id);
10831138 doc.ensure_view_init(view.id);
@@ -1204,12 +1259,15 @@ impl Editor {
12041259 }
12051260
12061261 pub fn new_file(&mut self, action: Action) -> DocumentId {
1207− self.new_file_from_document(action, Document::default())
1262+ self.new_file_from_document(action, Document::default(self.config.clone()))
12081263 }
12091264
12101265 pub fn new_file_from_stdin(&mut self, action: Action) -> Result<DocumentId, Error> {
12111266 let (rope, encoding) = crate::document::from_reader(&mut stdin(), None)?;
1212− Ok(self.new_file_from_document(action, Document::from(rope, Some(encoding))))
1267+ Ok(self.new_file_from_document(
1268+ action,
1269+ Document::from(rope, Some(encoding), self.config.clone()),
1270+ ))
12131271 }
12141272
12151273 // ??? possible use for integration tests
@@ -1220,7 +1278,12 @@ impl Editor {
12201278 let id = if let Some(id) = id {
12211279 id
12221280 } else {
1223− let mut doc = Document::open(&path, None, Some(self.syn_loader.clone()))?;
1281+ let mut doc = Document::open(
1282+ &path,
1283+ None,
1284+ Some(self.syn_loader.clone()),
1285+ self.config.clone(),
1286+ )?;
12241287
12251288 let _ = Self::launch_language_server(&mut self.language_servers, &mut doc);
12261289 if let Some(diff_base) = self.diff_providers.get_diff_base(&path) {
@@ -1306,7 +1369,7 @@ impl Editor {
13061369 .iter()
13071370 .map(|(&doc_id, _)| doc_id)
13081371 .next()
1309− .unwrap_or_else(|| self.new_document(Document::default()));
1372+ .unwrap_or_else(|| self.new_document(Document::default(self.config.clone())));
13101373 let view = View::new(doc_id, self.config().gutters.clone());
13111374 let view_id = self.tree.insert(view);
13121375 let doc = doc_mut!(self, &doc_id);
@@ -1440,7 +1503,11 @@ impl Editor {
14401503 .selection(view.id)
14411504 .primary()
14421505 .cursor(doc.text().slice(..));
1443− if let Some(mut pos) = view.screen_coords_at_pos(doc, doc.text().slice(..), cursor) {
1506+ let pos = self
1507+ .cursor_cache
1508+ .get()
1509+ .unwrap_or_else(|| view.screen_coords_at_pos(doc, doc.text().slice(..), cursor));
1510+ if let Some(mut pos) = pos {
14441511 let inner = view.inner_area(doc);
14451512 pos.col += inner.x as usize;
14461513 pos.row += inner.y as usize;
helix-view/src/gutter.rs+170−123
@@ -12,7 +12,7 @@ fn count_digits(n: usize) -> usize {
1212 std::iter::successors(Some(n), |&n| (n >= 10).then(|| n / 10)).count()
1313}
1414
15−pub type GutterFn<'doc> = Box<dyn FnMut(usize, bool, &mut String) -> Option<Style> + 'doc>;
15+pub type GutterFn<'doc> = Box<dyn FnMut(usize, bool, bool, &mut String) -> Option<Style> + 'doc>;
1616pub type Gutter =
1717 for<'doc> fn(&'doc Editor, &'doc Document, &View, &Theme, bool, usize) -> GutterFn<'doc>;
1818
@@ -58,31 +58,36 @@ pub fn diagnostic<'doc>(
5858 let hint = theme.get("hint");
5959 let diagnostics = doc.diagnostics();
6060
61− Box::new(move |line: usize, _selected: bool, out: &mut String| {
62− use helix_core::diagnostic::Severity;
63− if let Ok(index) = diagnostics.binary_search_by_key(&line, |d| d.line) {
64− let after = diagnostics[index..].iter().take_while(|d| d.line == line);
65−
66− let before = diagnostics[..index]
67− .iter()
68− .rev()
69− .take_while(|d| d.line == line);
70−
71− let diagnostics_on_line = after.chain(before);
72−
73− // This unwrap is safe because the iterator cannot be empty as it contains at least the item found by the binary search.
74− let diagnostic = diagnostics_on_line.max_by_key(|d| d.severity).unwrap();
75−
76− write!(out, "●").unwrap();
77− return Some(match diagnostic.severity {
78− Some(Severity::Error) => error,
79− Some(Severity::Warning) | None => warning,
80− Some(Severity::Info) => info,
81− Some(Severity::Hint) => hint,
82− });
83− }
84− None
85− })
61+ Box::new(
62+ move |line: usize, _selected: bool, first_visual_line: bool, out: &mut String| {
63+ if !first_visual_line {
64+ return None;
65+ }
66+ use helix_core::diagnostic::Severity;
67+ if let Ok(index) = diagnostics.binary_search_by_key(&line, |d| d.line) {
68+ let after = diagnostics[index..].iter().take_while(|d| d.line == line);
69+
70+ let before = diagnostics[..index]
71+ .iter()
72+ .rev()
73+ .take_while(|d| d.line == line);
74+
75+ let diagnostics_on_line = after.chain(before);
76+
77+ // This unwrap is safe because the iterator cannot be empty as it contains at least the item found by the binary search.
78+ let diagnostic = diagnostics_on_line.max_by_key(|d| d.severity).unwrap();
79+
80+ write!(out, "●").unwrap();
81+ return Some(match diagnostic.severity {
82+ Some(Severity::Error) => error,
83+ Some(Severity::Warning) | None => warning,
84+ Some(Severity::Info) => info,
85+ Some(Severity::Hint) => hint,
86+ });
87+ }
88+ None
89+ },
90+ )
8691}
8792
8893pub fn diff<'doc>(
@@ -99,36 +104,41 @@ pub fn diff<'doc>(
99104 let hunks = diff_handle.hunks();
100105 let mut hunk_i = 0;
101106 let mut hunk = hunks.nth_hunk(hunk_i);
102− Box::new(move |line: usize, _selected: bool, out: &mut String| {
103− // truncating the line is fine here because we don't compute diffs
104− // for files with more lines than i32::MAX anyways
105− // we need to special case removals here
106− // these technically do not have a range of lines to highlight (`hunk.after.start == hunk.after.end`).
107− // However we still want to display these hunks correctly we must not yet skip to the next hunk here
108− while hunk.after.end < line as u32
109− || !hunk.is_pure_removal() && line as u32 == hunk.after.end
110− {
111− hunk_i += 1;
112− hunk = hunks.nth_hunk(hunk_i);
113− }
114−
115− if hunk.after.start > line as u32 {
116− return None;
117− }
118−
119− let (icon, style) = if hunk.is_pure_insertion() {
120− ("▍", added)
121− } else if hunk.is_pure_removal() {
122− ("▔", deleted)
123− } else {
124− ("▍", modified)
125− };
126−
127− write!(out, "{}", icon).unwrap();
128− Some(style)
129− })
107+ Box::new(
108+ move |line: usize, _selected: bool, first_visual_line: bool, out: &mut String| {
109+ // truncating the line is fine here because we don't compute diffs
110+ // for files with more lines than i32::MAX anyways
111+ // we need to special case removals here
112+ // these technically do not have a range of lines to highlight (`hunk.after.start == hunk.after.end`).
113+ // However we still want to display these hunks correctly we must not yet skip to the next hunk here
114+ while hunk.after.end < line as u32
115+ || !hunk.is_pure_removal() && line as u32 == hunk.after.end
116+ {
117+ hunk_i += 1;
118+ hunk = hunks.nth_hunk(hunk_i);
119+ }
120+
121+ if hunk.after.start > line as u32 {
122+ return None;
123+ }
124+
125+ let (icon, style) = if hunk.is_pure_insertion() {
126+ ("▍", added)
127+ } else if hunk.is_pure_removal() {
128+ if !first_visual_line {
129+ return None;
130+ }
131+ ("▔", deleted)
132+ } else {
133+ ("▍", modified)
134+ };
135+
136+ write!(out, "{}", icon).unwrap();
137+ Some(style)
138+ },
139+ )
130140 } else {
131− Box::new(move |_, _, _| None)
141+ Box::new(move |_, _, _, _| None)
132142 }
133143}
134144
@@ -142,7 +152,7 @@ pub fn line_numbers<'doc>(
142152 let text = doc.text().slice(..);
143153 let width = line_numbers_width(view, doc);
144154
145− let last_line_in_view = view.last_line(doc);
155+ let last_line_in_view = view.estimate_last_doc_line(doc);
146156
147157 // Whether to draw the line number for the last line of the
148158 // document or not. We only draw it if it's not an empty line.
@@ -158,34 +168,42 @@ pub fn line_numbers<'doc>(
158168 let line_number = editor.config().line_number;
159169 let mode = editor.mode;
160170
161− Box::new(move |line: usize, selected: bool, out: &mut String| {
162− if line == last_line_in_view && !draw_last {
163− write!(out, "{:>1$}", '~', width).unwrap();
164− Some(linenr)
165− } else {
166− use crate::{document::Mode, editor::LineNumber};
167−
168− let relative = line_number == LineNumber::Relative
169− && mode != Mode::Insert
170− && is_focused
171− && current_line != line;
172−
173− let display_num = if relative {
174− abs_diff(current_line, line)
171+ Box::new(
172+ move |line: usize, selected: bool, first_visual_line: bool, out: &mut String| {
173+ if line == last_line_in_view && !draw_last {
174+ write!(out, "{:>1$}", '~', width).unwrap();
175+ Some(linenr)
175176 } else {
176− line + 1
177− };
178−
179− let style = if selected && is_focused {
180− linenr_select
181− } else {
182− linenr
183− };
184−
185− write!(out, "{:>1$}", display_num, width).unwrap();
186− Some(style)
187− }
188− })
177+ use crate::{document::Mode, editor::LineNumber};
178+
179+ let relative = line_number == LineNumber::Relative
180+ && mode != Mode::Insert
181+ && is_focused
182+ && current_line != line;
183+
184+ let display_num = if relative {
185+ abs_diff(current_line, line)
186+ } else {
187+ line + 1
188+ };
189+
190+ let style = if selected && is_focused {
191+ linenr_select
192+ } else {
193+ linenr
194+ };
195+
196+ if first_visual_line {
197+ write!(out, "{:>1$}", display_num, width).unwrap();
198+ } else {
199+ write!(out, "{:>1$}", " ", width).unwrap();
200+ }
201+
202+ // TODO: Use then_some when MSRV reaches 1.62
203+ first_visual_line.then(|| style)
204+ }
205+ },
206+ )
189207}
190208
191209/// The width of a "line-numbers" gutter
@@ -210,7 +228,7 @@ pub fn padding<'doc>(
210228 _theme: &Theme,
211229 _is_focused: bool,
212230) -> GutterFn<'doc> {
213− Box::new(|_line: usize, _selected: bool, _out: &mut String| None)
231+ Box::new(|_line: usize, _selected: bool, _first_visual_line: bool, _out: &mut String| None)
214232}
215233
216234#[inline(always)]
@@ -237,41 +255,46 @@ pub fn breakpoints<'doc>(
237255
238256 let breakpoints = match breakpoints {
239257 Some(breakpoints) => breakpoints,
240− None => return Box::new(move |_, _, _| None),
258+ None => return Box::new(move |_, _, _, _| None),
241259 };
242260
243− Box::new(move |line: usize, _selected: bool, out: &mut String| {
244− let breakpoint = breakpoints
245− .iter()
246− .find(|breakpoint| breakpoint.line == line)?;
247−
248− let mut style = if breakpoint.condition.is_some() && breakpoint.log_message.is_some() {
249− error.underline_style(UnderlineStyle::Line)
250− } else if breakpoint.condition.is_some() {
251− error
252− } else if breakpoint.log_message.is_some() {
253− info
254− } else {
255− warning
256− };
257−
258− if !breakpoint.verified {
259− // Faded colors
260− style = if let Some(Color::Rgb(r, g, b)) = style.fg {
261− style.fg(Color::Rgb(
262− ((r as f32) * 0.4).floor() as u8,
263− ((g as f32) * 0.4).floor() as u8,
264− ((b as f32) * 0.4).floor() as u8,
265− ))
266− } else {
267− style.fg(Color::Gray)
261+ Box::new(
262+ move |line: usize, _selected: bool, first_visual_line: bool, out: &mut String| {
263+ if !first_visual_line {
264+ return None;
268265 }
269− };
266+ let breakpoint = breakpoints
267+ .iter()
268+ .find(|breakpoint| breakpoint.line == line)?;
269+
270+ let mut style = if breakpoint.condition.is_some() && breakpoint.log_message.is_some() {
271+ error.underline_style(UnderlineStyle::Line)
272+ } else if breakpoint.condition.is_some() {
273+ error
274+ } else if breakpoint.log_message.is_some() {
275+ info
276+ } else {
277+ warning
278+ };
270279
271− let sym = if breakpoint.verified { "▲" } else { "⊚" };
272− write!(out, "{}", sym).unwrap();
273− Some(style)
274− })
280+ if !breakpoint.verified {
281+ // Faded colors
282+ style = if let Some(Color::Rgb(r, g, b)) = style.fg {
283+ style.fg(Color::Rgb(
284+ ((r as f32) * 0.4).floor() as u8,
285+ ((g as f32) * 0.4).floor() as u8,
286+ ((b as f32) * 0.4).floor() as u8,
287+ ))
288+ } else {
289+ style.fg(Color::Gray)
290+ }
291+ };
292+
293+ let sym = if breakpoint.verified { "▲" } else { "⊚" };
294+ write!(out, "{}", sym).unwrap();
295+ Some(style)
296+ },
297+ )
275298}
276299
277300pub fn diagnostics_or_breakpoints<'doc>(
@@ -284,18 +307,22 @@ pub fn diagnostics_or_breakpoints<'doc>(
284307 let mut diagnostics = diagnostic(editor, doc, view, theme, is_focused);
285308 let mut breakpoints = breakpoints(editor, doc, view, theme, is_focused);
286309
287− Box::new(move |line, selected, out| {
288− breakpoints(line, selected, out).or_else(|| diagnostics(line, selected, out))
310+ Box::new(move |line, selected, first_visual_line: bool, out| {
311+ breakpoints(line, selected, first_visual_line, out)
312+ .or_else(|| diagnostics(line, selected, first_visual_line, out))
289313 })
290314}
291315
292316#[cfg(test)]
293317mod tests {
318+ use std::sync::Arc;
319+
294320 use super::*;
295321 use crate::document::Document;
296− use crate::editor::{GutterConfig, GutterLineNumbersConfig};
322+ use crate::editor::{Config, GutterConfig, GutterLineNumbersConfig};
297323 use crate::graphics::Rect;
298324 use crate::DocumentId;
325+ use arc_swap::ArcSwap;
299326 use helix_core::Rope;
300327
301328 #[test]
@@ -304,7 +331,11 @@ mod tests {
304331 view.area = Rect::new(40, 40, 40, 40);
305332
306333 let rope = Rope::from_str("abc\n\tdef");
307− let doc = Document::from(rope, None);
334+ let doc = Document::from(
335+ rope,
336+ None,
337+ Arc::new(ArcSwap::new(Arc::new(Config::default()))),
338+ );
308339
309340 assert_eq!(view.gutters.layout.len(), 5);
310341 assert_eq!(view.gutters.layout[0].width(&view, &doc), 1);
@@ -325,7 +356,11 @@ mod tests {
325356 view.area = Rect::new(40, 40, 40, 40);
326357
327358 let rope = Rope::from_str("abc\n\tdef");
328− let doc = Document::from(rope, None);
359+ let doc = Document::from(
360+ rope,
361+ None,
362+ Arc::new(ArcSwap::new(Arc::new(Config::default()))),
363+ );
329364
330365 assert_eq!(view.gutters.layout.len(), 1);
331366 assert_eq!(view.gutters.layout[0].width(&view, &doc), 1);
@@ -339,7 +374,11 @@ mod tests {
339374 view.area = Rect::new(40, 40, 40, 40);
340375
341376 let rope = Rope::from_str("abc\n\tdef");
342− let doc = Document::from(rope, None);
377+ let doc = Document::from(
378+ rope,
379+ None,
380+ Arc::new(ArcSwap::new(Arc::new(Config::default()))),
381+ );
343382
344383 assert_eq!(view.gutters.layout.len(), 2);
345384 assert_eq!(view.gutters.layout[0].width(&view, &doc), 1);
@@ -357,10 +396,18 @@ mod tests {
357396 view.area = Rect::new(40, 40, 40, 40);
358397
359398 let rope = Rope::from_str("a\nb");
360− let doc_short = Document::from(rope, None);
399+ let doc_short = Document::from(
400+ rope,
401+ None,
402+ Arc::new(ArcSwap::new(Arc::new(Config::default()))),
403+ );
361404
362405 let rope = Rope::from_str("a\nb\nc\nd\ne\nf\ng\nh\ni\nj\nk\nl\nm\nn\no\np");
363− let doc_long = Document::from(rope, None);
406+ let doc_long = Document::from(
407+ rope,
408+ None,
409+ Arc::new(ArcSwap::new(Arc::new(Config::default()))),
410+ );
364411
365412 assert_eq!(view.gutters.layout.len(), 2);
366413 assert_eq!(view.gutters.layout[1].width(&view, &doc_short), 1);
helix-view/src/lib.rs+15−8
@@ -49,13 +49,10 @@ pub enum Align {
4949}
5050
5151pub fn align_view(doc: &Document, view: &mut View, align: Align) {
52− let pos = doc
53− .selection(view.id)
54− .primary()
55− .cursor(doc.text().slice(..));
56− let line = doc.text().char_to_line(pos);
57−
58− let last_line_height = view.inner_height().saturating_sub(1);
52+ let doc_text = doc.text().slice(..);
53+ let cursor = doc.selection(view.id).primary().cursor(doc_text);
54+ let viewport = view.inner_area(doc);
55+ let last_line_height = viewport.height.saturating_sub(1);
5956
6057 let relative = match align {
6158 Align::Center => last_line_height / 2,
@@ -63,10 +60,20 @@ pub fn align_view(doc: &Document, view: &mut View, align: Align) {
6360 Align::Bottom => last_line_height,
6461 };
6562
66− view.offset.row = line.saturating_sub(relative);
63+ let text_fmt = doc.text_format(viewport.width, None);
64+ let annotations = view.text_annotations(doc, None);
65+ (view.offset.anchor, view.offset.vertical_offset) = char_idx_at_visual_offset(
66+ doc_text,
67+ cursor,
68+ -(relative as isize),
69+ 0,
70+ &text_fmt,
71+ &annotations,
72+ );
6773}
6874
6975pub use document::Document;
7076pub use editor::Editor;
77+use helix_core::char_idx_at_visual_offset;
7178pub use theme::Theme;
7279pub use view::View;
helix-view/src/theme.rs+14−1
@@ -314,10 +314,23 @@ impl Theme {
314314 &self.scopes
315315 }
316316
317− pub fn find_scope_index(&self, scope: &str) -> Option<usize> {
317+ pub fn find_scope_index_exact(&self, scope: &str) -> Option<usize> {
318318 self.scopes().iter().position(|s| s == scope)
319319 }
320320
321+ pub fn find_scope_index(&self, mut scope: &str) -> Option<usize> {
322+ loop {
323+ if let Some(highlight) = self.find_scope_index_exact(scope) {
324+ return Some(highlight);
325+ }
326+ if let Some(new_end) = scope.rfind('.') {
327+ scope = &scope[..new_end];
328+ } else {
329+ return None;
330+ }
331+ }
332+ }
333+
321334 pub fn is_16_color(&self) -> bool {
322335 self.styles.iter().all(|(_, style)| {
323336 [style.fg, style.bg]
helix-view/src/view.rs+519−128
@@ -2,11 +2,13 @@ use crate::{
22 align_view,
33 editor::{GutterConfig, GutterType},
44 graphics::Rect,
5− Align, Document, DocumentId, ViewId,
5+ Align, Document, DocumentId, Theme, ViewId,
66};
77
88use helix_core::{
9− pos_at_visual_coords, visual_coords_at_pos, Position, RopeSlice, Selection, Transaction,
9+ char_idx_at_visual_offset, doc_formatter::TextFormat, text_annotations::TextAnnotations,
10+ visual_offset_from_anchor, visual_offset_from_block, Position, RopeSlice, Selection,
11+ Transaction,
1012};
1113
1214use std::{
@@ -93,10 +95,17 @@ impl JumpList {
9395 }
9496}
9597
98+#[derive(Clone, Debug, PartialEq, Eq, Copy, Default)]
99+pub struct ViewPosition {
100+ pub anchor: usize,
101+ pub horizontal_offset: usize,
102+ pub vertical_offset: usize,
103+}
104+
96105#[derive(Clone)]
97106pub struct View {
98107 pub id: ViewId,
99− pub offset: Position,
108+ pub offset: ViewPosition,
100109 pub area: Rect,
101110 pub doc: DocumentId,
102111 pub jumps: JumpList,
@@ -133,7 +142,11 @@ impl View {
133142 Self {
134143 id: ViewId::default(),
135144 doc,
136− offset: Position::new(0, 0),
145+ offset: ViewPosition {
146+ anchor: 0,
147+ horizontal_offset: 0,
148+ vertical_offset: 0,
149+ },
137150 area: Rect::default(), // will get calculated upon inserting into tree
138151 jumps: JumpList::new((doc, Selection::point(0))), // TODO: use actual sel
139152 docs_access_history: Vec::new(),
@@ -159,6 +172,10 @@ impl View {
159172 self.area.clip_bottom(1).height.into() // -1 for statusline
160173 }
161174
175+ pub fn inner_width(&self, doc: &Document) -> u16 {
176+ self.area.clip_left(self.gutter_offset(doc)).width
177+ }
178+
162179 pub fn gutters(&self) -> &[GutterType] {
163180 &self.gutters.layout
164181 }
@@ -176,84 +193,120 @@ impl View {
176193 &self,
177194 doc: &Document,
178195 scrolloff: usize,
179− ) -> Option<(usize, usize)> {
180− self.offset_coords_to_in_view_center(doc, scrolloff, false)
196+ ) -> Option<ViewPosition> {
197+ self.offset_coords_to_in_view_center::<false>(doc, scrolloff)
181198 }
182199
183− pub fn offset_coords_to_in_view_center(
200+ pub fn offset_coords_to_in_view_center<const CENTERING: bool>(
184201 &self,
185202 doc: &Document,
186203 scrolloff: usize,
187− centering: bool,
188− ) -> Option<(usize, usize)> {
189− let cursor = doc
190− .selection(self.id)
191− .primary()
192− .cursor(doc.text().slice(..));
193−
194− let Position { col, row: line } =
195− visual_coords_at_pos(doc.text().slice(..), cursor, doc.tab_width());
196−
197− let inner_area = self.inner_area(doc);
198− let last_line = (self.offset.row + inner_area.height as usize).saturating_sub(1);
199− let last_col = self.offset.col + inner_area.width.saturating_sub(1) as usize;
200−
201− let new_offset = |scrolloff: usize| {
202− // - 1 so we have at least one gap in the middle.
203− // a height of 6 with padding of 3 on each side will keep shifting the view back and forth
204− // as we type
205− let scrolloff = scrolloff.min(inner_area.height.saturating_sub(1) as usize / 2);
206−
207− let row = if line > last_line.saturating_sub(scrolloff) {
208− // scroll down
209− self.offset.row + line - (last_line.saturating_sub(scrolloff))
210− } else if line < self.offset.row + scrolloff {
211− // scroll up
212− line.saturating_sub(scrolloff)
204+ ) -> Option<ViewPosition> {
205+ let doc_text = doc.text().slice(..);
206+ let viewport = self.inner_area(doc);
207+ let vertical_viewport_end = self.offset.vertical_offset + viewport.height as usize;
208+ let text_fmt = doc.text_format(viewport.width, None);
209+ let annotations = self.text_annotations(doc, None);
210+
211+ // - 1 so we have at least one gap in the middle.
212+ // a height of 6 with padding of 3 on each side will keep shifting the view back and forth
213+ // as we type
214+ let scrolloff = scrolloff.min(viewport.height.saturating_sub(1) as usize / 2);
215+
216+ let cursor = doc.selection(self.id).primary().cursor(doc_text);
217+ let mut offset = self.offset;
218+
219+ let (visual_off, mut at_top) = if cursor >= offset.anchor {
220+ let off = visual_offset_from_anchor(
221+ doc_text,
222+ offset.anchor,
223+ cursor,
224+ &text_fmt,
225+ &annotations,
226+ vertical_viewport_end,
227+ );
228+ (off, false)
229+ } else if CENTERING {
230+ // cursor out of view
231+ return None;
232+ } else {
233+ (None, true)
234+ };
235+
236+ let new_anchor = match visual_off {
237+ Some((visual_pos, _)) if visual_pos.row < scrolloff + offset.vertical_offset => {
238+ if CENTERING && visual_pos.row < offset.vertical_offset {
239+ // cursor out of view
240+ return None;
241+ }
242+ at_top = true;
243+ true
244+ }
245+ Some((visual_pos, _)) if visual_pos.row >= vertical_viewport_end - scrolloff => {
246+ if CENTERING && visual_pos.row >= vertical_viewport_end as usize {
247+ // cursor out of view
248+ return None;
249+ }
250+ true
251+ }
252+ Some(_) => false,
253+ None => true,
254+ };
255+
256+ if new_anchor {
257+ let v_off = if at_top {
258+ scrolloff as isize
213259 } else {
214− self.offset.row
260+ viewport.height as isize - scrolloff as isize
215261 };
262+ (offset.anchor, offset.vertical_offset) =
263+ char_idx_at_visual_offset(doc_text, cursor, -v_off, 0, &text_fmt, &annotations);
264+ }
216265
217− let col = if col > last_col.saturating_sub(scrolloff) {
266+ if text_fmt.soft_wrap {
267+ offset.horizontal_offset = 0;
268+ } else {
269+ // determine the current visual column of the text
270+ let col = visual_off
271+ .unwrap_or_else(|| {
272+ visual_offset_from_block(
273+ doc_text,
274+ offset.anchor,
275+ cursor,
276+ &text_fmt,
277+ &annotations,
278+ )
279+ })
280+ .0
281+ .col;
282+
283+ let last_col = offset.horizontal_offset + viewport.width.saturating_sub(1) as usize;
284+ if col > last_col.saturating_sub(scrolloff) {
218285 // scroll right
219− self.offset.col + col - (last_col.saturating_sub(scrolloff))
220− } else if col < self.offset.col + scrolloff {
286+ offset.horizontal_offset += col - (last_col.saturating_sub(scrolloff))
287+ } else if col < offset.horizontal_offset + scrolloff {
221288 // scroll left
222− col.saturating_sub(scrolloff)
223− } else {
224− self.offset.col
289+ offset.horizontal_offset = col.saturating_sub(scrolloff)
225290 };
226− (row, col)
227− };
228− let current_offset = (self.offset.row, self.offset.col);
229− if centering {
230− // return None if cursor is out of view
231− let offset = new_offset(0);
232− (offset == current_offset).then(|| {
233− if scrolloff == 0 {
234− offset
235− } else {
236− new_offset(scrolloff)
237− }
238− })
239− } else {
240− // return None if cursor is in (view - scrolloff)
241− let offset = new_offset(scrolloff);
242− (offset != current_offset).then(|| offset) // TODO: use 'then_some' when 1.62 <= MSRV
243291 }
292+
293+ // if we are not centering return None if view position is unchanged
294+ if !CENTERING && offset == self.offset {
295+ return None;
296+ }
297+
298+ Some(offset)
244299 }
245300
246301 pub fn ensure_cursor_in_view(&mut self, doc: &Document, scrolloff: usize) {
247− if let Some((row, col)) = self.offset_coords_to_in_view_center(doc, scrolloff, false) {
248− self.offset.row = row;
249− self.offset.col = col;
302+ if let Some(offset) = self.offset_coords_to_in_view_center::<false>(doc, scrolloff) {
303+ self.offset = offset;
250304 }
251305 }
252306
253307 pub fn ensure_cursor_in_view_center(&mut self, doc: &Document, scrolloff: usize) {
254− if let Some((row, col)) = self.offset_coords_to_in_view_center(doc, scrolloff, true) {
255− self.offset.row = row;
256− self.offset.col = col;
308+ if let Some(offset) = self.offset_coords_to_in_view_center::<true>(doc, scrolloff) {
309+ self.offset = offset;
257310 } else {
258311 align_view(doc, self, Align::Center);
259312 }
@@ -263,14 +316,51 @@ impl View {
263316 self.offset_coords_to_in_view(doc, scrolloff).is_none()
264317 }
265318
266− /// Calculates the last visible line on screen
319+ /// Estimates the last visible document line on screen.
320+ /// This estimate is an upper bound obtained by calculating the first
321+ /// visible line and adding the viewport height.
322+ /// The actual last visible line may be smaller if softwrapping occurs
323+ /// or virtual text lines are visible
267324 #[inline]
268− pub fn last_line(&self, doc: &Document) -> usize {
269− std::cmp::min(
270− // Saturating subs to make it inclusive zero indexing.
271− (self.offset.row + self.inner_height()).saturating_sub(1),
272− doc.text().len_lines().saturating_sub(1),
273− )
325+ pub fn estimate_last_doc_line(&self, doc: &Document) -> usize {
326+ let doc_text = doc.text().slice(..);
327+ let line = doc_text.char_to_line(self.offset.anchor.min(doc_text.len_chars()));
328+ // Saturating subs to make it inclusive zero indexing.
329+ (line + self.inner_height())
330+ .min(doc_text.len_lines())
331+ .saturating_sub(1)
332+ }
333+
334+ /// Calculates the last non-empty visual line on screen
335+ #[inline]
336+ pub fn last_visual_line(&self, doc: &Document) -> usize {
337+ let doc_text = doc.text().slice(..);
338+ let viewport = self.inner_area(doc);
339+ let text_fmt = doc.text_format(viewport.width, None);
340+ let annotations = self.text_annotations(doc, None);
341+
342+ // last visual line in view is trivial to compute
343+ let visual_height = self.offset.vertical_offset + viewport.height as usize;
344+
345+ // fast path when the EOF is not visible on the screen,
346+ if self.estimate_last_doc_line(doc) < doc_text.len_lines() - 1 {
347+ return visual_height.saturating_sub(1);
348+ }
349+
350+ // translate to document line
351+ let pos = visual_offset_from_anchor(
352+ doc_text,
353+ self.offset.anchor,
354+ usize::MAX,
355+ &text_fmt,
356+ &annotations,
357+ visual_height,
358+ );
359+
360+ match pos {
361+ Some((Position { row, .. }, _)) => row.saturating_sub(self.offset.vertical_offset),
362+ None => visual_height.saturating_sub(1),
363+ }
274364 }
275365
276366 /// Translates a document position to an absolute position in the terminal.
@@ -282,22 +372,39 @@ impl View {
282372 text: RopeSlice,
283373 pos: usize,
284374 ) -> Option<Position> {
285− let line = text.char_to_line(pos);
286−
287− if line < self.offset.row || line > self.last_line(doc) {
375+ if pos < self.offset.anchor {
288376 // Line is not visible on screen
289377 return None;
290378 }
291379
292− let tab_width = doc.tab_width();
293− // TODO: visual_coords_at_pos also does char_to_line which we ignore, can we reuse the call?
294− let Position { col, .. } = visual_coords_at_pos(text, pos, tab_width);
380+ let viewport = self.inner_area(doc);
381+ let text_fmt = doc.text_format(viewport.width, None);
382+ let annotations = self.text_annotations(doc, None);
383+
384+ let mut pos = visual_offset_from_anchor(
385+ text,
386+ self.offset.anchor,
387+ pos,
388+ &text_fmt,
389+ &annotations,
390+ viewport.height as usize,
391+ )?
392+ .0;
393+ if pos.row < self.offset.vertical_offset {
394+ return None;
395+ }
396+ pos.row -= self.offset.vertical_offset;
397+ if pos.row >= viewport.height as usize {
398+ return None;
399+ }
400+ pos.col = pos.col.saturating_sub(self.offset.horizontal_offset);
295401
296− // It is possible for underflow to occur if the buffer length is larger than the terminal width.
297− let row = line.saturating_sub(self.offset.row);
298− let col = col.saturating_sub(self.offset.col);
402+ Some(pos)
403+ }
299404
300− Some(Position::new(row, col))
405+ pub fn text_annotations(&self, doc: &Document, theme: Option<&Theme>) -> TextAnnotations {
406+ // TODO custom annotations for custom views like side by side diffs
407+ doc.text_annotations(theme)
301408 }
302409
303410 pub fn text_pos_at_screen_coords(
@@ -305,9 +412,10 @@ impl View {
305412 doc: &Document,
306413 row: u16,
307414 column: u16,
308− tab_width: usize,
415+ fmt: TextFormat,
416+ annotations: &TextAnnotations,
417+ ignore_virtual_text: bool,
309418 ) -> Option<usize> {
310− let text = doc.text().slice(..);
311419 let inner = self.inner_area(doc);
312420 // 1 for status
313421 if row < inner.top() || row >= inner.bottom() {
@@ -318,27 +426,80 @@ impl View {
318426 return None;
319427 }
320428
321− let text_row = (row - inner.y) as usize + self.offset.row;
322− if text_row > text.len_lines() - 1 {
323− return Some(text.len_chars());
324− }
429+ self.text_pos_at_visual_coords(
430+ doc,
431+ row - inner.y,
432+ column - inner.x,
433+ fmt,
434+ annotations,
435+ ignore_virtual_text,
436+ )
437+ }
438+
439+ pub fn text_pos_at_visual_coords(
440+ &self,
441+ doc: &Document,
442+ row: u16,
443+ column: u16,
444+ text_fmt: TextFormat,
445+ annotations: &TextAnnotations,
446+ ignore_virtual_text: bool,
447+ ) -> Option<usize> {
448+ let text = doc.text().slice(..);
325449
326− let text_col = (column - inner.x) as usize + self.offset.col;
450+ let text_row = row as usize + self.offset.vertical_offset;
451+ let text_col = column as usize + self.offset.horizontal_offset;
327452
328− Some(pos_at_visual_coords(
453+ let (char_idx, virt_lines) = char_idx_at_visual_offset(
329454 text,
330− Position {
331− row: text_row,
332− col: text_col,
333− },
334− tab_width,
335− ))
455+ self.offset.anchor,
456+ text_row as isize,
457+ text_col,
458+ &text_fmt,
459+ annotations,
460+ );
461+
462+ // if the cursor is on a line with only virtual text return None
463+ if virt_lines != 0 && ignore_virtual_text {
464+ return None;
465+ }
466+ Some(char_idx)
336467 }
337468
338469 /// Translates a screen position to position in the text document.
339470 /// Returns a usize typed position in bounds of the text if found in this view, None if out of view.
340− pub fn pos_at_screen_coords(&self, doc: &Document, row: u16, column: u16) -> Option<usize> {
341− self.text_pos_at_screen_coords(doc, row, column, doc.tab_width())
471+ pub fn pos_at_screen_coords(
472+ &self,
473+ doc: &Document,
474+ row: u16,
475+ column: u16,
476+ ignore_virtual_text: bool,
477+ ) -> Option<usize> {
478+ self.text_pos_at_screen_coords(
479+ doc,
480+ row,
481+ column,
482+ doc.text_format(self.inner_width(doc), None),
483+ &self.text_annotations(doc, None),
484+ ignore_virtual_text,
485+ )
486+ }
487+
488+ pub fn pos_at_visual_coords(
489+ &self,
490+ doc: &Document,
491+ row: u16,
492+ column: u16,
493+ ignore_virtual_text: bool,
494+ ) -> Option<usize> {
495+ self.text_pos_at_visual_coords(
496+ doc,
497+ row,
498+ column,
499+ doc.text_format(self.inner_width(doc), None),
500+ &self.text_annotations(doc, None),
501+ ignore_virtual_text,
502+ )
342503 }
343504
344505 /// Translates screen coordinates into coordinates on the gutter of the view.
@@ -419,7 +580,10 @@ impl View {
419580
420581#[cfg(test)]
421582mod tests {
583+ use std::sync::Arc;
584+
422585 use super::*;
586+ use arc_swap::ArcSwap;
423587 use helix_core::Rope;
424588
425589 // 1 diagnostic + 1 spacer + 3 linenr (< 1000 lines) + 1 spacer + 1 diff
@@ -429,52 +593,174 @@ mod tests {
429593 const DEFAULT_GUTTER_OFFSET_ONLY_DIAGNOSTICS: u16 = 3;
430594
431595 use crate::document::Document;
432− use crate::editor::{GutterConfig, GutterLineNumbersConfig, GutterType};
596+ use crate::editor::{Config, GutterConfig, GutterLineNumbersConfig, GutterType};
433597
434598 #[test]
435599 fn test_text_pos_at_screen_coords() {
436600 let mut view = View::new(DocumentId::default(), GutterConfig::default());
437601 view.area = Rect::new(40, 40, 40, 40);
438602 let rope = Rope::from_str("abc\n\tdef");
439− let doc = Document::from(rope, None);
603+ let doc = Document::from(
604+ rope,
605+ None,
606+ Arc::new(ArcSwap::new(Arc::new(Config::default()))),
607+ );
440608
441− assert_eq!(view.text_pos_at_screen_coords(&doc, 40, 2, 4), None);
609+ assert_eq!(
610+ view.text_pos_at_screen_coords(
611+ &doc,
612+ 40,
613+ 2,
614+ TextFormat::default(),
615+ &TextAnnotations::default(),
616+ true
617+ ),
618+ None
619+ );
442620
443− assert_eq!(view.text_pos_at_screen_coords(&doc, 40, 41, 4), None);
621+ assert_eq!(
622+ view.text_pos_at_screen_coords(
623+ &doc,
624+ 40,
625+ 41,
626+ TextFormat::default(),
627+ &TextAnnotations::default(),
628+ true
629+ ),
630+ None
631+ );
444632
445− assert_eq!(view.text_pos_at_screen_coords(&doc, 0, 2, 4), None);
633+ assert_eq!(
634+ view.text_pos_at_screen_coords(
635+ &doc,
636+ 0,
637+ 2,
638+ TextFormat::default(),
639+ &TextAnnotations::default(),
640+ true
641+ ),
642+ None
643+ );
446644
447− assert_eq!(view.text_pos_at_screen_coords(&doc, 0, 49, 4), None);
645+ assert_eq!(
646+ view.text_pos_at_screen_coords(
647+ &doc,
648+ 0,
649+ 49,
650+ TextFormat::default(),
651+ &TextAnnotations::default(),
652+ true
653+ ),
654+ None
655+ );
448656
449− assert_eq!(view.text_pos_at_screen_coords(&doc, 0, 41, 4), None);
657+ assert_eq!(
658+ view.text_pos_at_screen_coords(
659+ &doc,
660+ 0,
661+ 41,
662+ TextFormat::default(),
663+ &TextAnnotations::default(),
664+ true
665+ ),
666+ None
667+ );
450668
451− assert_eq!(view.text_pos_at_screen_coords(&doc, 40, 81, 4), None);
669+ assert_eq!(
670+ view.text_pos_at_screen_coords(
671+ &doc,
672+ 40,
673+ 81,
674+ TextFormat::default(),
675+ &TextAnnotations::default(),
676+ true
677+ ),
678+ None
679+ );
452680
453− assert_eq!(view.text_pos_at_screen_coords(&doc, 78, 41, 4), None);
681+ assert_eq!(
682+ view.text_pos_at_screen_coords(
683+ &doc,
684+ 78,
685+ 41,
686+ TextFormat::default(),
687+ &TextAnnotations::default(),
688+ true
689+ ),
690+ None
691+ );
454692
455693 assert_eq!(
456− view.text_pos_at_screen_coords(&doc, 40, 40 + DEFAULT_GUTTER_OFFSET + 3, 4),
694+ view.text_pos_at_screen_coords(
695+ &doc,
696+ 40,
697+ 40 + DEFAULT_GUTTER_OFFSET + 3,
698+ TextFormat::default(),
699+ &TextAnnotations::default(),
700+ true
701+ ),
457702 Some(3)
458703 );
459704
460− assert_eq!(view.text_pos_at_screen_coords(&doc, 40, 80, 4), Some(3));
705+ assert_eq!(
706+ view.text_pos_at_screen_coords(
707+ &doc,
708+ 40,
709+ 80,
710+ TextFormat::default(),
711+ &TextAnnotations::default(),
712+ true
713+ ),
714+ Some(3)
715+ );
461716
462717 assert_eq!(
463− view.text_pos_at_screen_coords(&doc, 41, 40 + DEFAULT_GUTTER_OFFSET + 1, 4),
718+ view.text_pos_at_screen_coords(
719+ &doc,
720+ 41,
721+ 40 + DEFAULT_GUTTER_OFFSET + 1,
722+ TextFormat::default(),
723+ &TextAnnotations::default(),
724+ true
725+ ),
464726 Some(4)
465727 );
466728
467729 assert_eq!(
468− view.text_pos_at_screen_coords(&doc, 41, 40 + DEFAULT_GUTTER_OFFSET + 4, 4),
730+ view.text_pos_at_screen_coords(
731+ &doc,
732+ 41,
733+ 40 + DEFAULT_GUTTER_OFFSET + 4,
734+ TextFormat::default(),
735+ &TextAnnotations::default(),
736+ true
737+ ),
469738 Some(5)
470739 );
471740
472741 assert_eq!(
473− view.text_pos_at_screen_coords(&doc, 41, 40 + DEFAULT_GUTTER_OFFSET + 7, 4),
742+ view.text_pos_at_screen_coords(
743+ &doc,
744+ 41,
745+ 40 + DEFAULT_GUTTER_OFFSET + 7,
746+ TextFormat::default(),
747+ &TextAnnotations::default(),
748+ true
749+ ),
474750 Some(8)
475751 );
476752
477− assert_eq!(view.text_pos_at_screen_coords(&doc, 41, 80, 4), Some(8));
753+ assert_eq!(
754+ view.text_pos_at_screen_coords(
755+ &doc,
756+ 41,
757+ 80,
758+ TextFormat::default(),
759+ &TextAnnotations::default(),
760+ true
761+ ),
762+ Some(8)
763+ );
478764 }
479765
480766 #[test]
@@ -488,13 +774,19 @@ mod tests {
488774 );
489775 view.area = Rect::new(40, 40, 40, 40);
490776 let rope = Rope::from_str("abc\n\tdef");
491− let doc = Document::from(rope, None);
777+ let doc = Document::from(
778+ rope,
779+ None,
780+ Arc::new(ArcSwap::new(Arc::new(Config::default()))),
781+ );
492782 assert_eq!(
493783 view.text_pos_at_screen_coords(
494784 &doc,
495785 41,
496786 40 + DEFAULT_GUTTER_OFFSET_ONLY_DIAGNOSTICS + 1,
497− 4
787+ TextFormat::default(),
788+ &TextAnnotations::default(),
789+ true
498790 ),
499791 Some(4)
500792 );
@@ -511,8 +803,22 @@ mod tests {
511803 );
512804 view.area = Rect::new(40, 40, 40, 40);
513805 let rope = Rope::from_str("abc\n\tdef");
514− let doc = Document::from(rope, None);
515− assert_eq!(view.text_pos_at_screen_coords(&doc, 41, 40 + 1, 4), Some(4));
806+ let doc = Document::from(
807+ rope,
808+ None,
809+ Arc::new(ArcSwap::new(Arc::new(Config::default()))),
810+ );
811+ assert_eq!(
812+ view.text_pos_at_screen_coords(
813+ &doc,
814+ 41,
815+ 40 + 1,
816+ TextFormat::default(),
817+ &TextAnnotations::default(),
818+ true
819+ ),
820+ Some(4)
821+ );
516822 }
517823
518824 #[test]
@@ -520,34 +826,80 @@ mod tests {
520826 let mut view = View::new(DocumentId::default(), GutterConfig::default());
521827 view.area = Rect::new(40, 40, 40, 40);
522828 let rope = Rope::from_str("Hi! こんにちは皆さん");
523− let doc = Document::from(rope, None);
829+ let doc = Document::from(
830+ rope,
831+ None,
832+ Arc::new(ArcSwap::new(Arc::new(Config::default()))),
833+ );
524834
525835 assert_eq!(
526− view.text_pos_at_screen_coords(&doc, 40, 40 + DEFAULT_GUTTER_OFFSET, 4),
836+ view.text_pos_at_screen_coords(
837+ &doc,
838+ 40,
839+ 40 + DEFAULT_GUTTER_OFFSET,
840+ TextFormat::default(),
841+ &TextAnnotations::default(),
842+ true
843+ ),
527844 Some(0)
528845 );
529846
530847 assert_eq!(
531− view.text_pos_at_screen_coords(&doc, 40, 40 + DEFAULT_GUTTER_OFFSET + 4, 4),
848+ view.text_pos_at_screen_coords(
849+ &doc,
850+ 40,
851+ 40 + DEFAULT_GUTTER_OFFSET + 4,
852+ TextFormat::default(),
853+ &TextAnnotations::default(),
854+ true
855+ ),
532856 Some(4)
533857 );
534858 assert_eq!(
535− view.text_pos_at_screen_coords(&doc, 40, 40 + DEFAULT_GUTTER_OFFSET + 5, 4),
859+ view.text_pos_at_screen_coords(
860+ &doc,
861+ 40,
862+ 40 + DEFAULT_GUTTER_OFFSET + 5,
863+ TextFormat::default(),
864+ &TextAnnotations::default(),
865+ true
866+ ),
536867 Some(4)
537868 );
538869
539870 assert_eq!(
540− view.text_pos_at_screen_coords(&doc, 40, 40 + DEFAULT_GUTTER_OFFSET + 6, 4),
871+ view.text_pos_at_screen_coords(
872+ &doc,
873+ 40,
874+ 40 + DEFAULT_GUTTER_OFFSET + 6,
875+ TextFormat::default(),
876+ &TextAnnotations::default(),
877+ true
878+ ),
541879 Some(5)
542880 );
543881
544882 assert_eq!(
545− view.text_pos_at_screen_coords(&doc, 40, 40 + DEFAULT_GUTTER_OFFSET + 7, 4),
883+ view.text_pos_at_screen_coords(
884+ &doc,
885+ 40,
886+ 40 + DEFAULT_GUTTER_OFFSET + 7,
887+ TextFormat::default(),
888+ &TextAnnotations::default(),
889+ true
890+ ),
546891 Some(5)
547892 );
548893
549894 assert_eq!(
550− view.text_pos_at_screen_coords(&doc, 40, 40 + DEFAULT_GUTTER_OFFSET + 8, 4),
895+ view.text_pos_at_screen_coords(
896+ &doc,
897+ 40,
898+ 40 + DEFAULT_GUTTER_OFFSET + 8,
899+ TextFormat::default(),
900+ &TextAnnotations::default(),
901+ true
902+ ),
551903 Some(6)
552904 );
553905 }
@@ -557,30 +909,69 @@ mod tests {
557909 let mut view = View::new(DocumentId::default(), GutterConfig::default());
558910 view.area = Rect::new(40, 40, 40, 40);
559911 let rope = Rope::from_str("Hèl̀l̀ò world!");
560− let doc = Document::from(rope, None);
912+ let doc = Document::from(
913+ rope,
914+ None,
915+ Arc::new(ArcSwap::new(Arc::new(Config::default()))),
916+ );
561917
562918 assert_eq!(
563− view.text_pos_at_screen_coords(&doc, 40, 40 + DEFAULT_GUTTER_OFFSET, 4),
919+ view.text_pos_at_screen_coords(
920+ &doc,
921+ 40,
922+ 40 + DEFAULT_GUTTER_OFFSET,
923+ TextFormat::default(),
924+ &TextAnnotations::default(),
925+ true
926+ ),
564927 Some(0)
565928 );
566929
567930 assert_eq!(
568− view.text_pos_at_screen_coords(&doc, 40, 40 + DEFAULT_GUTTER_OFFSET + 1, 4),
931+ view.text_pos_at_screen_coords(
932+ &doc,
933+ 40,
934+ 40 + DEFAULT_GUTTER_OFFSET + 1,
935+ TextFormat::default(),
936+ &TextAnnotations::default(),
937+ true
938+ ),
569939 Some(1)
570940 );
571941
572942 assert_eq!(
573− view.text_pos_at_screen_coords(&doc, 40, 40 + DEFAULT_GUTTER_OFFSET + 2, 4),
943+ view.text_pos_at_screen_coords(
944+ &doc,
945+ 40,
946+ 40 + DEFAULT_GUTTER_OFFSET + 2,
947+ TextFormat::default(),
948+ &TextAnnotations::default(),
949+ true
950+ ),
574951 Some(3)
575952 );
576953
577954 assert_eq!(
578− view.text_pos_at_screen_coords(&doc, 40, 40 + DEFAULT_GUTTER_OFFSET + 3, 4),
955+ view.text_pos_at_screen_coords(
956+ &doc,
957+ 40,
958+ 40 + DEFAULT_GUTTER_OFFSET + 3,
959+ TextFormat::default(),
960+ &TextAnnotations::default(),
961+ true
962+ ),
579963 Some(5)
580964 );
581965
582966 assert_eq!(
583− view.text_pos_at_screen_coords(&doc, 40, 40 + DEFAULT_GUTTER_OFFSET + 4, 4),
967+ view.text_pos_at_screen_coords(
968+ &doc,
969+ 40,
970+ 40 + DEFAULT_GUTTER_OFFSET + 4,
971+ TextFormat::default(),
972+ &TextAnnotations::default(),
973+ true
974+ ),
584975 Some(7)
585976 );
586977 }