on master help sign in
rework positioning/rendering and enable softwrap/virtual text (#5420)
4dcf1fePascal Kuthe
book/src/configuration.md+22−1
@@ -307,4 +307,25 @@ Currently unused
307
307
308#### `[editor.gutters.spacer]` Section
308#### `[editor.gutters.spacer]` Section
309
309
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:
61| `config` | Language Server configuration |
61| `config` | Language Server configuration |
62| `grammar` | The tree-sitter grammar to use (defaults to the value of `name`) |
62| `grammar` | The tree-sitter grammar to use (defaults to the value of `name`) |
63| `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 |
63| `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 |
65
65
66### File-type detection and the `file-types` key
66### File-type detection and the `file-types` key
67
67
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};
5use unicode_segmentation::{GraphemeCursor, GraphemeIncomplete};
5use unicode_segmentation::{GraphemeCursor, GraphemeIncomplete};
6use unicode_width::UnicodeWidthStr;
6use unicode_width::UnicodeWidthStr;
7
7
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+}
9
90
10#[must_use]
91#[must_use]
11pub fn grapheme_width(g: &str) -> usize {
92pub fn grapheme_width(g: &str) -> usize {
@@ -27,6 +108,8 @@ pub fn grapheme_width(g: &str) -> usize {
27 // We use max(1) here because all grapeheme clusters--even illformed
108 // We use max(1) here because all grapeheme clusters--even illformed
28 // ones--should have at least some width so they can be edited
109 // ones--should have at least some width so they can be edited
29 // properly.
110 // 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/)
30 UnicodeWidthStr::width(g).max(1)
113 UnicodeWidthStr::width(g).max(1)
31 }
114 }
32}
115}
@@ -341,3 +424,101 @@ impl<'a> Iterator for RopeGraphemes<'a> {
341 }
424 }
342 }
425 }
343}
426}
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;
6pub mod config;
6pub mod config;
7pub mod diagnostic;
7pub mod diagnostic;
8pub mod diff;
8pub mod diff;
9+pub mod doc_formatter;
9pub mod graphemes;
10pub mod graphemes;
10pub mod history;
11pub mod history;
11pub mod increment;
12pub mod increment;
@@ -24,6 +25,7 @@ pub mod shellwords;
24pub mod surround;
25pub mod surround;
25pub mod syntax;
26pub mod syntax;
26pub mod test;
27pub mod test;
28+pub mod text_annotations;
27pub mod textobject;
29pub mod textobject;
28mod transaction;
30mod transaction;
29pub mod wrap;
31pub mod wrap;
@@ -95,8 +97,12 @@ pub use {regex, tree_sitter};
95
97
96pub use graphemes::RopeGraphemes;
98pub use graphemes::RopeGraphemes;
97pub use position::{
99pub 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,
99};
102};
103+#[allow(deprecated)]
104+pub use position::{pos_at_visual_coords, visual_coords_at_pos};
105+
100pub use selection::{Range, Selection};
106pub use selection::{Range, Selection};
101pub use smallvec::{smallvec, SmallVec};
107pub use smallvec::{smallvec, SmallVec};
102pub use syntax::Syntax;
108pub use syntax::Syntax;
helix-core/src/movement.rs+180−22
@@ -4,16 +4,19 @@ use ropey::iter::Chars;
4use tree_sitter::{Node, QueryCursor};
4use tree_sitter::{Node, QueryCursor};
5
5
6use crate::{
6use crate::{
7+ char_idx_at_visual_offset,
7 chars::{categorize_char, char_is_line_ending, CharCategory},
8 chars::{categorize_char, char_is_line_ending, CharCategory},
9+ doc_formatter::TextFormat,
8 graphemes::{
10 graphemes::{
9 next_grapheme_boundary, nth_next_grapheme_boundary, nth_prev_grapheme_boundary,
11 next_grapheme_boundary, nth_next_grapheme_boundary, nth_prev_grapheme_boundary,
10 prev_grapheme_boundary,
12 prev_grapheme_boundary,
11 },
13 },
12 line_ending::rope_is_line_ending,
14 line_ending::rope_is_line_ending,
13− pos_at_visual_coords,
15+ position::char_idx_at_visual_block_offset,
14 syntax::LanguageConfiguration,
16 syntax::LanguageConfiguration,
17+ text_annotations::TextAnnotations,
15 textobject::TextObject,
18 textobject::TextObject,
16− visual_coords_at_pos, Position, Range, RopeSlice,
19+ visual_offset_from_block, Range, RopeSlice,
17};
20};
18
21
19#[derive(Debug, Copy, Clone, PartialEq, Eq)]
22#[derive(Debug, Copy, Clone, PartialEq, Eq)]
@@ -34,7 +37,8 @@ pub fn move_horizontally(
34 dir: Direction,
37 dir: Direction,
35 count: usize,
38 count: usize,
36 behaviour: Movement,
39 behaviour: Movement,
37− _: usize,
40+ _: &TextFormat,
41+ _: &mut TextAnnotations,
38) -> Range {
42) -> Range {
39 let pos = range.cursor(slice);
43 let pos = range.cursor(slice);
40
44
@@ -48,35 +52,116 @@ pub fn move_horizontally(
48 range.put_cursor(slice, new_pos, behaviour == Movement::Extend)
52 range.put_cursor(slice, new_pos, behaviour == Movement::Extend)
49}
53}
50
54
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+
51pub fn move_vertically(
106pub fn move_vertically(
52 slice: RopeSlice,
107 slice: RopeSlice,
53 range: Range,
108 range: Range,
54 dir: Direction,
109 dir: Direction,
55 count: usize,
110 count: usize,
56 behaviour: Movement,
111 behaviour: Movement,
57− tab_width: usize,
112+ text_fmt: &TextFormat,
113+ annotations: &mut TextAnnotations,
58) -> Range {
114) -> Range {
115+ annotations.clear_line_annotations();
59 let pos = range.cursor(slice);
116 let pos = range.cursor(slice);
117+ let line_idx = slice.char_to_line(pos);
118+ let line_start = slice.line_to_char(line_idx);
60
119
61 // Compute the current position's 2d coordinates.
120 // Compute the current position's 2d coordinates.
62− let Position { row, col } = visual_coords_at_pos(slice, pos, tab_width);
121+ let visual_pos = visual_offset_from_block(slice, line_start, pos, text_fmt, annotations).0;
63− let horiz = range.horiz.unwrap_or(col as u32);
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);
64
127
65 // Compute the new position.
128 // Compute the new position.
66− let new_row = match dir {
129+ let mut new_line_idx = match dir {
67− Direction::Forward => (row + count).min(slice.len_lines().saturating_sub(1)),
130+ Direction::Forward => line_idx.saturating_add(count),
68− Direction::Backward => row.saturating_sub(count),
131+ Direction::Backward => line_idx.saturating_sub(count),
69 };
132 };
70− let new_col = col.max(horiz as usize);
133+
71− let new_pos = pos_at_visual_coords(slice, Position::new(new_row, new_col), tab_width);
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+ );
72
157
73 // Special-case to avoid moving to the end of the last non-empty line.
158 // 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 {
75 return range;
160 return range;
76 }
161 }
77
162
78 let mut new_range = range.put_cursor(slice, new_pos, behaviour == Movement::Extend);
163 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));
80 new_range
165 new_range
81}
166}
82
167
@@ -473,7 +558,16 @@ mod test {
473 assert_eq!(
558 assert_eq!(
474 coords_at_pos(
559 coords_at_pos(
475 slice,
560 slice,
561+ move_vertically_visual(
562+ slice,
563+ range,
476− move_vertically(slice, range, Direction::Forward, 1, Movement::Move, 4).head
564+ Direction::Forward,
565+ 1,
566+ Movement::Move,
567+ &TextFormat::default(),
568+ &mut TextAnnotations::default(),
569+ )
570+ .head
477 ),
571 ),
478 (1, 3).into()
572 (1, 3).into()
479 );
573 );
@@ -497,7 +591,15 @@ mod test {
497 ];
591 ];
498
592
499 for ((direction, amount), coordinates) in moves_and_expected_coordinates {
593 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+ );
501 assert_eq!(coords_at_pos(slice, range.head), coordinates.into())
603 assert_eq!(coords_at_pos(slice, range.head), coordinates.into())
502 }
604 }
503 }
605 }
@@ -523,7 +625,15 @@ mod test {
523 ];
625 ];
524
626
525 for ((direction, amount), coordinates) in moves_and_expected_coordinates {
627 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+ );
527 assert_eq!(coords_at_pos(slice, range.head), coordinates.into());
637 assert_eq!(coords_at_pos(slice, range.head), coordinates.into());
528 assert_eq!(range.head, range.anchor);
638 assert_eq!(range.head, range.anchor);
529 }
639 }
@@ -545,7 +655,15 @@ mod test {
545 ];
655 ];
546
656
547 for (direction, amount) in moves {
657 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+ );
549 assert_eq!(range.anchor, original_anchor);
667 assert_eq!(range.anchor, original_anchor);
550 }
668 }
551 }
669 }
@@ -569,7 +687,15 @@ mod test {
569 ];
687 ];
570
688
571 for ((direction, amount), coordinates) in moves_and_expected_coordinates {
689 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+ );
573 assert_eq!(coords_at_pos(slice, range.head), coordinates.into());
699 assert_eq!(coords_at_pos(slice, range.head), coordinates.into());
574 assert_eq!(range.head, range.anchor);
700 assert_eq!(range.head, range.anchor);
575 }
701 }
@@ -603,8 +729,24 @@ mod test {
603
729
604 for ((axis, direction, amount), coordinates) in moves_and_expected_coordinates {
730 for ((axis, direction, amount), coordinates) in moves_and_expected_coordinates {
605 range = match axis {
731 range = match axis {
606− Axis::H => move_horizontally(slice, range, direction, amount, Movement::Move, 0),
732+ Axis::H => move_horizontally(
607− Axis::V => move_vertically(slice, range, direction, amount, Movement::Move, 4),
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+ ),
608 };
750 };
609 assert_eq!(coords_at_pos(slice, range.head), coordinates.into());
751 assert_eq!(coords_at_pos(slice, range.head), coordinates.into());
610 assert_eq!(range.head, range.anchor);
752 assert_eq!(range.head, range.anchor);
@@ -638,8 +780,24 @@ mod test {
638
780
639 for ((axis, direction, amount), coordinates) in moves_and_expected_coordinates {
781 for ((axis, direction, amount), coordinates) in moves_and_expected_coordinates {
640 range = match axis {
782 range = match axis {
641− Axis::H => move_horizontally(slice, range, direction, amount, Movement::Move, 0),
783+ Axis::H => move_horizontally(
642− Axis::V => move_vertically(slice, range, direction, amount, Movement::Move, 4),
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+ ),
643 };
801 };
644 assert_eq!(coords_at_pos(slice, range.head), coordinates.into());
802 assert_eq!(coords_at_pos(slice, range.head), coordinates.into());
645 assert_eq!(range.head, range.anchor);
803 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};
2
2
3use crate::{
3use crate::{
4 chars::char_is_line_ending,
4 chars::char_is_line_ending,
5+ doc_formatter::{DocumentFormatter, TextFormat},
5 graphemes::{ensure_grapheme_boundary_prev, grapheme_width, RopeGraphemes},
6 graphemes::{ensure_grapheme_boundary_prev, grapheme_width, RopeGraphemes},
6 line_ending::line_end_char_index,
7 line_ending::line_end_char_index,
8+ text_annotations::TextAnnotations,
7 RopeSlice,
9 RopeSlice,
8};
10};
9
11
@@ -73,6 +75,13 @@ pub fn coords_at_pos(text: RopeSlice, pos: usize) -> Position {
73/// Takes \t, double-width characters (CJK) into account as well as text
75/// Takes \t, double-width characters (CJK) into account as well as text
74/// not in the document in the future.
76/// not in the document in the future.
75/// See [`coords_at_pos`] for an "objective" one.
77/// 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"]
76pub fn visual_coords_at_pos(text: RopeSlice, pos: usize, tab_width: usize) -> Position {
85pub fn visual_coords_at_pos(text: RopeSlice, pos: usize, tab_width: usize) -> Position {
77 let line = text.char_to_line(pos);
86 let line = text.char_to_line(pos);
78
87
@@ -93,6 +102,82 @@ pub fn visual_coords_at_pos(text: RopeSlice, pos: usize, tab_width: usize) -> Po
93 Position::new(line, col)
102 Position::new(line, col)
94}
103}
95
104
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+
96/// Convert (line, column) coordinates to a character index.
181/// Convert (line, column) coordinates to a character index.
97///
182///
98/// If the `line` coordinate is beyond the end of the file, the EOF
183/// 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
140/// If the `column` coordinate is past the end of the given line, the
225/// If the `column` coordinate is past the end of the given line, the
141/// line-end position (in this case, just before the line ending
226/// line-end position (in this case, just before the line ending
142/// character) will be returned.
227/// 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"]
143pub fn pos_at_visual_coords(text: RopeSlice, coords: Position, tab_width: usize) -> usize {
233pub fn pos_at_visual_coords(text: RopeSlice, coords: Position, tab_width: usize) -> usize {
144 let Position { mut row, col } = coords;
234 let Position { mut row, col } = coords;
145 row = row.min(text.len_lines() - 1);
235 row = row.min(text.len_lines() - 1);
@@ -169,6 +259,120 @@ pub fn pos_at_visual_coords(text: RopeSlice, coords: Position, tab_width: usize)
169 line_start + col_char_offset
259 line_start + col_char_offset
170}
260}
171
261
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+
172#[cfg(test)]
376#[cfg(test)]
173mod test {
377mod test {
174 use super::*;
378 use super::*;
@@ -228,6 +432,7 @@ mod test {
228 }
432 }
229
433
230 #[test]
434 #[test]
435+ #[allow(deprecated)]
231 fn test_visual_coords_at_pos() {
436 fn test_visual_coords_at_pos() {
232 let text = Rope::from("ḧëḷḷö\nẅöṛḷḋ");
437 let text = Rope::from("ḧëḷḷö\nẅöṛḷḋ");
233 let slice = text.slice(..);
438 let slice = text.slice(..);
@@ -275,6 +480,130 @@ mod test {
275 assert_eq!(visual_coords_at_pos(slice, 2, 8), (0, 9).into());
480 assert_eq!(visual_coords_at_pos(slice, 2, 8), (0, 9).into());
276 }
481 }
277
482
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+ }
278 #[test]
607 #[test]
279 fn test_pos_at_coords() {
608 fn test_pos_at_coords() {
280 let text = Rope::from("ḧëḷḷö\nẅöṛḷḋ");
609 let text = Rope::from("ḧëḷḷö\nẅöṛḷḋ");
@@ -341,6 +670,7 @@ mod test {
341 }
670 }
342
671
343 #[test]
672 #[test]
673+ #[allow(deprecated)]
344 fn test_pos_at_visual_coords() {
674 fn test_pos_at_visual_coords() {
345 let text = Rope::from("ḧëḷḷö\nẅöṛḷḋ");
675 let text = Rope::from("ḧëḷḷö\nẅöṛḷḋ");
346 let slice = text.slice(..);
676 let slice = text.slice(..);
@@ -405,4 +735,100 @@ mod test {
405 assert_eq!(pos_at_visual_coords(slice, (0, 10).into(), 4), 0);
735 assert_eq!(pos_at_visual_coords(slice, (0, 10).into(), 4), 0);
406 assert_eq!(pos_at_visual_coords(slice, (10, 10).into(), 4), 0);
736 assert_eq!(pos_at_visual_coords(slice, (10, 10).into(), 4), 0);
407 }
737 }
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+ }
408}
834}
helix-core/src/selection.rs+17−15
@@ -53,7 +53,9 @@ pub struct Range {
53 pub anchor: usize,
53 pub anchor: usize,
54 /// The head of the range, moved when extending.
54 /// The head of the range, moved when extending.
55 pub head: usize,
55 pub head: usize,
56+ /// The previous visual offset (softwrapped lines and columns) from
57+ /// the start of the line
56− pub horiz: Option<u32>,
58+ pub old_visual_position: Option<(u32, u32)>,
57}
59}
58
60
59impl Range {
61impl Range {
@@ -61,7 +63,7 @@ impl Range {
61 Self {
63 Self {
62 anchor,
64 anchor,
63 head,
65 head,
64− horiz: None,
66+ old_visual_position: None,
65 }
67 }
66 }
68 }
67
69
@@ -127,7 +129,7 @@ impl Range {
127 Self {
129 Self {
128 anchor: self.head,
130 anchor: self.head,
129 head: self.anchor,
131 head: self.anchor,
130− horiz: self.horiz,
132+ old_visual_position: self.old_visual_position,
131 }
133 }
132 }
134 }
133
135
@@ -185,7 +187,7 @@ impl Range {
185 Self {
187 Self {
186 anchor,
188 anchor,
187 head,
189 head,
188− horiz: None,
190+ old_visual_position: None,
189 }
191 }
190 }
192 }
191
193
@@ -198,13 +200,13 @@ impl Range {
198 Self {
200 Self {
199 anchor: self.anchor.min(from),
201 anchor: self.anchor.min(from),
200 head: self.head.max(to),
202 head: self.head.max(to),
201− horiz: None,
203+ old_visual_position: None,
202 }
204 }
203 } else {
205 } else {
204 Self {
206 Self {
205 anchor: self.anchor.max(to),
207 anchor: self.anchor.max(to),
206 head: self.head.min(from),
208 head: self.head.min(from),
207− horiz: None,
209+ old_visual_position: None,
208 }
210 }
209 }
211 }
210 }
212 }
@@ -219,13 +221,13 @@ impl Range {
219 Range {
221 Range {
220 anchor: self.anchor.max(other.anchor),
222 anchor: self.anchor.max(other.anchor),
221 head: self.head.min(other.head),
223 head: self.head.min(other.head),
222− horiz: None,
224+ old_visual_position: None,
223 }
225 }
224 } else {
226 } else {
225 Range {
227 Range {
226 anchor: self.from().min(other.from()),
228 anchor: self.from().min(other.from()),
227 head: self.to().max(other.to()),
229 head: self.to().max(other.to()),
228− horiz: None,
230+ old_visual_position: None,
229 }
231 }
230 }
232 }
231 }
233 }
@@ -279,8 +281,8 @@ impl Range {
279 Range {
281 Range {
280 anchor: new_anchor,
282 anchor: new_anchor,
281 head: new_head,
283 head: new_head,
282− horiz: if new_anchor == self.anchor {
284+ old_visual_position: if new_anchor == self.anchor {
283− self.horiz
285+ self.old_visual_position
284 } else {
286 } else {
285 None
287 None
286 },
288 },
@@ -306,7 +308,7 @@ impl Range {
306 Range {
308 Range {
307 anchor: self.anchor,
309 anchor: self.anchor,
308 head: next_grapheme_boundary(slice, self.head),
310 head: next_grapheme_boundary(slice, self.head),
309− horiz: self.horiz,
311+ old_visual_position: self.old_visual_position,
310 }
312 }
311 } else {
313 } else {
312 *self
314 *self
@@ -378,7 +380,7 @@ impl From<(usize, usize)> for Range {
378 Self {
380 Self {
379 anchor,
381 anchor,
380 head,
382 head,
381− horiz: None,
383+ old_visual_position: None,
382 }
384 }
383 }
385 }
384}
386}
@@ -482,7 +484,7 @@ impl Selection {
482 ranges: smallvec![Range {
484 ranges: smallvec![Range {
483 anchor,
485 anchor,
484 head,
486 head,
485− horiz: None
487+ old_visual_position: None
486 }],
488 }],
487 primary_index: 0,
489 primary_index: 0,
488 }
490 }
@@ -566,9 +568,9 @@ impl Selection {
566 }
568 }
567
569
568 /// Takes a closure and maps each `Range` over the closure.
570 /// 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
570 where
572 where
571− F: Fn(Range) -> Range,
573+ F: FnMut(Range) -> Range,
572 {
574 {
573 for range in self.ranges.iter_mut() {
575 for range in self.ranges.iter_mut() {
574 *range = f(*range)
576 *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 {
172 area,
172 area,
173 theme_loader.clone(),
173 theme_loader.clone(),
174 syn_loader.clone(),
174 syn_loader.clone(),
175− Box::new(Map::new(Arc::clone(&config), |config: &Config| {
175+ Arc::new(Map::new(Arc::clone(&config), |config: &Config| {
176 &config.editor
176 &config.editor
177 })),
177 })),
178 );
178 );
@@ -309,8 +309,10 @@ impl Application {
309 let surface = self.terminal.current_buffer_mut();
309 let surface = self.terminal.current_buffer_mut();
310
310
311 self.compositor.render(area, surface, &mut cx);
311 self.compositor.render(area, surface, &mut cx);
312−
313 let (pos, kind) = self.compositor.cursor(area, &self.editor);
312 let (pos, kind) = self.compositor.cursor(area, &self.editor);
313+ // reset cursor cache
314+ self.editor.cursor_cache.set(None);
315+
314 let pos = pos.map(|pos| (pos.col as u16, pos.row as u16));
316 let pos = pos.map(|pos| (pos.col as u16, pos.row as u16));
315 self.terminal.draw(pos, kind).unwrap();
317 self.terminal.draw(pos, kind).unwrap();
316 }
318 }
@@ -395,6 +397,13 @@ impl Application {
395 // Update all the relevant members in the editor after updating
397 // Update all the relevant members in the editor after updating
396 // the configuration.
398 // the configuration.
397 self.editor.refresh_config();
399 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+ }
398 }
407 }
399
408
400 /// refresh language config after config change
409 /// refresh language config after config change
helix-term/src/commands.rs+162−66
@@ -9,21 +9,25 @@ use tui::widgets::Row;
9pub use typed::*;
9pub use typed::*;
10
10
11use helix_core::{
11use helix_core::{
12+ char_idx_at_visual_offset, comment,
13+ doc_formatter::TextFormat,
12− comment, coords_at_pos, encoding, find_first_non_whitespace_char, find_root, graphemes,
14+ encoding, find_first_non_whitespace_char, find_root, graphemes,
13 history::UndoKind,
15 history::UndoKind,
14 increment, indent,
16 increment, indent,
15 indent::IndentStyle,
17 indent::IndentStyle,
16 line_ending::{get_line_ending_of_str, line_end_char_index, str_is_line_ending},
18 line_ending::{get_line_ending_of_str, line_end_char_index, str_is_line_ending},
17 match_brackets,
19 match_brackets,
18− movement::{self, Direction},
20+ movement::{self, move_vertically_visual, Direction},
19− object, pos_at_coords, pos_at_visual_coords,
21+ object, pos_at_coords,
20 regex::{self, Regex, RegexBuilder},
22 regex::{self, Regex, RegexBuilder},
21 search::{self, CharMatcher},
23 search::{self, CharMatcher},
22− selection, shellwords, surround, textobject,
24+ selection, shellwords, surround,
25+ text_annotations::TextAnnotations,
26+ textobject,
23 tree_sitter::Node,
27 tree_sitter::Node,
24 unicode::width::UnicodeWidthChar,
28 unicode::width::UnicodeWidthChar,
25− visual_coords_at_pos, LineEnding, Position, Range, Rope, RopeGraphemes, RopeSlice, Selection,
29+ visual_offset_from_block, LineEnding, Position, Range, Rope, RopeGraphemes, RopeSlice,
26− SmallVec, Tendril, Transaction,
30+ Selection, SmallVec, Tendril, Transaction,
27};
31};
28use helix_view::{
32use helix_view::{
29 clipboard::ClipboardType,
33 clipboard::ClipboardType,
@@ -200,10 +204,14 @@ impl MappableCommand {
200 move_char_right, "Move right",
204 move_char_right, "Move right",
201 move_line_up, "Move up",
205 move_line_up, "Move up",
202 move_line_down, "Move down",
206 move_line_down, "Move down",
207+ move_visual_line_up, "Move up",
208+ move_visual_line_down, "Move down",
203 extend_char_left, "Extend left",
209 extend_char_left, "Extend left",
204 extend_char_right, "Extend right",
210 extend_char_right, "Extend right",
205 extend_line_up, "Extend up",
211 extend_line_up, "Extend up",
206 extend_line_down, "Extend down",
212 extend_line_down, "Extend down",
213+ extend_visual_line_up, "Extend up",
214+ extend_visual_line_down, "Extend down",
207 copy_selection_on_next_line, "Copy selection on next line",
215 copy_selection_on_next_line, "Copy selection on next line",
208 copy_selection_on_prev_line, "Copy selection on previous line",
216 copy_selection_on_prev_line, "Copy selection on previous line",
209 move_next_word_start, "Move to start of next word",
217 move_next_word_start, "Move to start of next word",
@@ -538,18 +546,27 @@ impl PartialEq for MappableCommand {
538
546
539fn no_op(_cx: &mut Context) {}
547fn no_op(_cx: &mut Context) {}
540
548
549+type MoveFn =
550+ fn(RopeSlice, Range, Direction, usize, Movement, &TextFormat, &mut TextAnnotations) -> Range;
551+
541−fn move_impl<F>(cx: &mut Context, move_fn: F, dir: Direction, behaviour: Movement)
552+fn move_impl(cx: &mut Context, move_fn: MoveFn, dir: Direction, behaviour: Movement) {
542−where
543− F: Fn(RopeSlice, Range, Direction, usize, Movement, usize) -> Range,
544−{
545 let count = cx.count();
553 let count = cx.count();
546 let (view, doc) = current!(cx.editor);
554 let (view, doc) = current!(cx.editor);
547 let text = doc.text().slice(..);
555 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);
548
558
549− let selection = doc
559+ let selection = doc.selection(view.id).clone().transform(|range| {
550− .selection(view.id)
560+ move_fn(
551− .clone()
561+ text,
552− .transform(|range| move_fn(text, range, dir, count, behaviour, doc.tab_width()));
562+ range,
563+ dir,
564+ count,
565+ behaviour,
566+ &text_fmt,
567+ &mut annotations,
568+ )
569+ });
553 doc.set_selection(view.id, selection);
570 doc.set_selection(view.id, selection);
554}
571}
555
572
@@ -571,6 +588,24 @@ fn move_line_down(cx: &mut Context) {
571 move_impl(cx, move_vertically, Direction::Forward, Movement::Move)
588 move_impl(cx, move_vertically, Direction::Forward, Movement::Move)
572}
589}
573
590
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+
574fn extend_char_left(cx: &mut Context) {
609fn extend_char_left(cx: &mut Context) {
575 move_impl(cx, move_horizontally, Direction::Backward, Movement::Extend)
610 move_impl(cx, move_horizontally, Direction::Backward, Movement::Extend)
576}
611}
@@ -587,6 +622,24 @@ fn extend_line_down(cx: &mut Context) {
587 move_impl(cx, move_vertically, Direction::Forward, Movement::Extend)
622 move_impl(cx, move_vertically, Direction::Forward, Movement::Extend)
588}
623}
589
624
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+
590fn goto_line_end_impl(view: &mut View, doc: &mut Document, movement: Movement) {
643fn goto_line_end_impl(view: &mut View, doc: &mut Document, movement: Movement) {
591 let text = doc.text().slice(..);
644 let text = doc.text().slice(..);
592
645
@@ -814,7 +867,10 @@ fn trim_selections(cx: &mut Context) {
814}
867}
815
868
816// align text in selection
869// align text in selection
870+#[allow(deprecated)]
817fn align_selections(cx: &mut Context) {
871fn align_selections(cx: &mut Context) {
872+ use helix_core::visual_coords_at_pos;
873+
818 let (view, doc) = current!(cx.editor);
874 let (view, doc) = current!(cx.editor);
819 let text = doc.text().slice(..);
875 let text = doc.text().slice(..);
820 let selection = doc.selection(view.id);
876 let selection = doc.selection(view.id);
@@ -891,17 +947,22 @@ fn goto_window(cx: &mut Context, align: Align) {
891 // as we type
947 // as we type
892 let scrolloff = config.scrolloff.min(height.saturating_sub(1) / 2);
948 let scrolloff = config.scrolloff.min(height.saturating_sub(1) / 2);
893
949
894− let last_line = view.last_line(doc);
950+ let last_visual_line = view.last_visual_line(doc);
895
951
896− let line = match align {
952+ let visual_line = match align {
897− Align::Top => view.offset.row + scrolloff + count,
953+ Align::Top => view.offset.vertical_offset + scrolloff + count,
898− Align::Center => view.offset.row + ((last_line - view.offset.row) / 2),
954+ Align::Center => view.offset.vertical_offset + (last_visual_line / 2),
955+ Align::Bottom => {
899− Align::Bottom => last_line.saturating_sub(scrolloff + count),
956+ view.offset.vertical_offset + last_visual_line.saturating_sub(scrolloff + count)
957+ }
900 }
958 }
901− .max(view.offset.row + scrolloff)
959+ .max(view.offset.vertical_offset + scrolloff)
902− .min(last_line.saturating_sub(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");
903
965
904− let pos = doc.text().line_to_char(line);
905 let text = doc.text().slice(..);
966 let text = doc.text().slice(..);
906 let selection = doc
967 let selection = doc
907 .selection(view.id)
968 .selection(view.id)
@@ -1385,53 +1446,72 @@ pub fn scroll(cx: &mut Context, offset: usize, direction: Direction) {
1385 let range = doc.selection(view.id).primary();
1446 let range = doc.selection(view.id).primary();
1386 let text = doc.text().slice(..);
1447 let text = doc.text().slice(..);
1387
1448
1388− let cursor = visual_coords_at_pos(text, range.cursor(text), doc.tab_width());
1449+ let cursor = range.cursor(text);
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−
1399 let height = view.inner_height();
1450 let height = view.inner_height();
1400
1451
1401 let scrolloff = config.scrolloff.min(height / 2);
1452 let scrolloff = config.scrolloff.min(height / 2);
1453+ let offset = match direction {
1454+ Forward => offset as isize,
1455+ Backward => -(offset as isize),
1456+ };
1402
1457
1403− view.offset.row = match direction {
1458+ let doc_text = doc.text().slice(..);
1404− Forward => view.offset.row + offset,
1459+ let viewport = view.inner_area(doc);
1405− Backward => view.offset.row.saturating_sub(offset),
1460+ let text_fmt = doc.text_format(viewport.width, None);
1406− }
1461+ let annotations = view.text_annotations(doc, None);
1407− .min(doc_last_line);
1462+ (view.offset.anchor, view.offset.vertical_offset) = char_idx_at_visual_offset(
1408−
1463+ doc_text,
1409− // recalculate last line
1464+ view.offset.anchor,
1410− let last_line = view.last_line(doc);
1465+ view.offset.vertical_offset as isize + offset,
1411−
1466+ 0,
1412− // clamp into viewport
1467+ &text_fmt,
1413− let line = cursor
1468+ &annotations,
1414− .row
1469+ );
1415− .max(view.offset.row + scrolloff)
1416− .min(last_line.saturating_sub(scrolloff));
1417
1470
1418− // If cursor needs moving, replace primary selection
1471+ let head;
1419− if line != cursor.row {
1472+ match direction {
1420− let head = pos_at_visual_coords(text, Position::new(line, cursor.col), doc.tab_width()); // this func will properly truncate to line end
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+ }
1421
1502
1422− let anchor = if cx.editor.mode == Mode::Select {
1503+ let anchor = if cx.editor.mode == Mode::Select {
1423− range.anchor
1504+ range.anchor
1424− } else {
1505+ } else {
1425− head
1506+ head
1426− };
1507+ };
1427
1508
1428− // replace primary selection with an empty selection at cursor pos
1509+ // replace primary selection with an empty selection at cursor pos
1429− let prim_sel = Range::new(anchor, head);
1510+ let prim_sel = Range::new(anchor, head);
1430− let mut sel = doc.selection(view.id).clone();
1511+ let mut sel = doc.selection(view.id).clone();
1431− let idx = sel.primary_index();
1512+ let idx = sel.primary_index();
1432− sel = sel.replace(idx, prim_sel);
1513+ sel = sel.replace(idx, prim_sel);
1433− doc.set_selection(view.id, sel);
1514+ doc.set_selection(view.id, sel);
1434− }
1435}
1515}
1436
1516
1437fn page_up(cx: &mut Context) {
1517fn page_up(cx: &mut Context) {
@@ -1458,7 +1538,15 @@ fn half_page_down(cx: &mut Context) {
1458 scroll(cx, offset, Direction::Forward);
1538 scroll(cx, offset, Direction::Forward);
1459}
1539}
1460
1540
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
1461fn copy_selection_on_line(cx: &mut Context, direction: Direction) {
1547fn copy_selection_on_line(cx: &mut Context, direction: Direction) {
1548+ use helix_core::{pos_at_visual_coords, visual_coords_at_pos};
1549+
1462 let count = cx.count();
1550 let count = cx.count();
1463 let (view, doc) = current!(cx.editor);
1551 let (view, doc) = current!(cx.editor);
1464 let text = doc.text().slice(..);
1552 let text = doc.text().slice(..);
@@ -4475,11 +4563,19 @@ fn align_view_bottom(cx: &mut Context) {
4475
4563
4476fn align_view_middle(cx: &mut Context) {
4564fn align_view_middle(cx: &mut Context) {
4477 let (view, doc) = current!(cx.editor);
4565 let (view, doc) = current!(cx.editor);
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+ }
4478− let text = doc.text().slice(..);
4572+ let doc_text = doc.text().slice(..);
4573+ let annotations = view.text_annotations(doc, None);
4479− let pos = doc.selection(view.id).primary().cursor(text);
4574+ let pos = doc.selection(view.id).primary().cursor(doc_text);
4480− let pos = coords_at_pos(text, pos);
4575+ let pos =
4576+ visual_offset_from_block(doc_text, view.offset.anchor, pos, &text_fmt, &annotations).0;
4481
4577
4482− view.offset.col = pos
4578+ view.offset.horizontal_offset = pos
4483 .col
4579 .col
4484 .saturating_sub((view.inner_area(doc).width as usize) / 2);
4580 .saturating_sub((view.inner_area(doc).width as usize) / 2);
4485}
4581}
helix-term/src/keymap/default.rs+12−6
@@ -7,8 +7,8 @@ use helix_core::hashmap;
7pub fn default() -> HashMap<Mode, Keymap> {
7pub fn default() -> HashMap<Mode, Keymap> {
8 let normal = keymap!({ "Normal mode"
8 let normal = keymap!({ "Normal mode"
9 "h" | "left" => move_char_left,
9 "h" | "left" => move_char_left,
10− "j" | "down" => move_line_down,
10+ "j" | "down" => move_visual_line_down,
11− "k" | "up" => move_line_up,
11+ "k" | "up" => move_visual_line_up,
12 "l" | "right" => move_char_right,
12 "l" | "right" => move_char_right,
13
13
14 "t" => find_till_char,
14 "t" => find_till_char,
@@ -55,6 +55,8 @@ pub fn default() -> HashMap<Mode, Keymap> {
55 "m" => goto_last_modified_file,
55 "m" => goto_last_modified_file,
56 "n" => goto_next_buffer,
56 "n" => goto_next_buffer,
57 "p" => goto_previous_buffer,
57 "p" => goto_previous_buffer,
58+ "k" => move_line_up,
59+ "j" => move_line_down,
58 "." => goto_last_modification,
60 "." => goto_last_modification,
59 },
61 },
60 ":" => command_mode,
62 ":" => command_mode,
@@ -321,8 +323,8 @@ pub fn default() -> HashMap<Mode, Keymap> {
321 let mut select = normal.clone();
323 let mut select = normal.clone();
322 select.merge_nodes(keymap!({ "Select mode"
324 select.merge_nodes(keymap!({ "Select mode"
323 "h" | "left" => extend_char_left,
325 "h" | "left" => extend_char_left,
324− "j" | "down" => extend_line_down,
326+ "j" | "down" => extend_visual_line_down,
325− "k" | "up" => extend_line_up,
327+ "k" | "up" => extend_visual_line_up,
326 "l" | "right" => extend_char_right,
328 "l" | "right" => extend_char_right,
327
329
328 "w" => extend_next_word_start,
330 "w" => extend_next_word_start,
@@ -345,6 +347,10 @@ pub fn default() -> HashMap<Mode, Keymap> {
345 "esc" => exit_select_mode,
347 "esc" => exit_select_mode,
346
348
347 "v" => normal_mode,
349 "v" => normal_mode,
350+ "g" => { "Goto"
351+ "k" => extend_line_up,
352+ "j" => extend_line_down,
353+ },
348 }));
354 }));
349 let insert = keymap!({ "Insert mode"
355 let insert = keymap!({ "Insert mode"
350 "esc" => normal_mode,
356 "esc" => normal_mode,
@@ -362,8 +368,8 @@ pub fn default() -> HashMap<Mode, Keymap> {
362 "C-j" | "ret" => insert_newline,
368 "C-j" | "ret" => insert_newline,
363 "tab" => insert_tab,
369 "tab" => insert_tab,
364
370
365− "up" => move_line_up,
371+ "up" => move_visual_line_up,
366− "down" => move_line_down,
372+ "down" => move_visual_line_down,
367 "left" => move_char_left,
373 "left" => move_char_left,
368 "right" => move_char_right,
374 "right" => move_char_right,
369 "pageup" => page_up,
375 "pageup" => page_up,
helix-term/src/ui/completion.rs+4−2
@@ -391,8 +391,10 @@ impl Component for Completion {
391 let language = doc.language_name().unwrap_or("");
391 let language = doc.language_name().unwrap_or("");
392 let text = doc.text().slice(..);
392 let text = doc.text().slice(..);
393 let cursor_pos = doc.selection(view.id).primary().cursor(text);
393 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());
394+ let coords = view
395+ .screen_coords_at_pos(doc, text, cursor_pos)
396+ .expect("cursor must be in view");
395− let cursor_pos = (coords.row - view.offset.row) as u16;
397+ let cursor_pos = coords.row as u16;
396
398
397 let mut markdown_doc = match &option.documentation {
399 let mut markdown_doc = match &option.documentation {
398 Some(lsp::Documentation::String(contents))
400 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::{
4 job::{self, Callback},
4 job::{self, Callback},
5 key,
5 key,
6 keymap::{KeymapResult, Keymaps},
6 keymap::{KeymapResult, Keymaps},
7+ ui::{
8+ document::{render_document, LinePos, TextRenderer, TranslatedPosition},
7− ui::{Completion, ProgressSpinners},
9+ Completion, ProgressSpinners,
10+ },
8};
11};
9
12
10use helix_core::{
13use helix_core::{
@@ -13,8 +16,9 @@ use helix_core::{
13 },
16 },
14 movement::Direction,
17 movement::Direction,
15 syntax::{self, HighlightEvent},
18 syntax::{self, HighlightEvent},
19+ text_annotations::TextAnnotations,
16 unicode::width::UnicodeWidthStr,
20 unicode::width::UnicodeWidthStr,
17− visual_coords_at_pos, LineEnding, Position, Range, Selection, Transaction,
21+ visual_offset_from_block, Position, Range, Selection, Transaction,
18};
22};
19use helix_view::{
23use helix_view::{
20 document::{Mode, SCRATCH_BUFFER_NAME},
24 document::{Mode, SCRATCH_BUFFER_NAME},
@@ -24,12 +28,12 @@ use helix_view::{
24 keyboard::{KeyCode, KeyModifiers},
28 keyboard::{KeyCode, KeyModifiers},
25 Document, Editor, Theme, View,
29 Document, Editor, Theme, View,
26};
30};
27−use std::{borrow::Cow, cmp::min, num::NonZeroUsize, path::PathBuf};
31+use std::{num::NonZeroUsize, path::PathBuf, rc::Rc};
28
32
29use tui::buffer::Buffer as Surface;
33use tui::buffer::Buffer as Surface;
30
34
31−use super::lsp::SignatureHelp;
32use super::statusline;
35use super::statusline;
36+use super::{document::LineDecoration, lsp::SignatureHelp};
33
37
34pub struct EditorView {
38pub struct EditorView {
35 pub keymaps: Keymaps,
39 pub keymaps: Keymaps,
@@ -83,6 +87,10 @@ impl EditorView {
83 let theme = &editor.theme;
87 let theme = &editor.theme;
84 let config = editor.config();
88 let config = editor.config();
85
89
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+
86 // DAP: Highlight current stack frame position
94 // DAP: Highlight current stack frame position
87 let stack_frame = editor.debugger.as_ref().and_then(|debugger| {
95 let stack_frame = editor.debugger.as_ref().and_then(|debugger| {
88 if let (Some(frame), Some(thread_id)) = (debugger.active_frame, debugger.thread_id) {
96 if let (Some(frame), Some(thread_id)) = (debugger.active_frame, debugger.thread_id) {
@@ -103,28 +111,40 @@ impl EditorView {
103 == doc.path()
111 == doc.path()
104 {
112 {
105 let line = frame.line - 1; // convert to 0-indexing
113 let line = frame.line - 1; // convert to 0-indexing
106− if line >= view.offset.row && line < view.offset.row + area.height as usize {
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
107− surface.set_style(
120+ .surface
108− Rect::new(
121+ .set_style(Rect::new(area.x, pos.visual_line, area.width, 1), style);
109− area.x,
122+ };
110− area.y + (line - view.offset.row) as u16,
123+
111− area.width,
124+ line_decorations.push(Box::new(line_decoration));
112− 1,
113− ),
114− theme.get("ui.highlight"),
115− );
116− }
117 }
125 }
118 }
126 }
119
127
120 if is_focused && config.cursorline {
128 if is_focused && config.cursorline {
121− Self::highlight_cursorline(doc, view, surface, theme);
129+ line_decorations.push(Self::cursorline_decorator(doc, view, theme))
122 }
130 }
131+
123 if is_focused && config.cursorcolumn {
132 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));
125 }
146 }
126
147
127− let mut highlights = Self::doc_syntax_highlights(doc, view.offset, inner.height, theme);
128 for diagnostic in Self::doc_diagnostics_highlights(doc, theme) {
148 for diagnostic in Self::doc_diagnostics_highlights(doc, theme) {
129 // Most of the `diagnostic` Vecs are empty most of the time. Skipping
149 // Most of the `diagnostic` Vecs are empty most of the time. Skipping
130 // a merge for any empty Vec saves a significant amount of work.
150 // a merge for any empty Vec saves a significant amount of work.
@@ -133,8 +153,9 @@ impl EditorView {
133 }
153 }
134 highlights = Box::new(syntax::merge(highlights, diagnostic));
154 highlights = Box::new(syntax::merge(highlights, diagnostic));
135 }
155 }
156+
136 let highlights: Box<dyn Iterator<Item = HighlightEvent>> = if is_focused {
157 let highlights: Box<dyn Iterator<Item = HighlightEvent>> = if is_focused {
137− Box::new(syntax::merge(
158+ let highlights = syntax::merge(
138 highlights,
159 highlights,
139 Self::doc_selection_highlights(
160 Self::doc_selection_highlights(
140 editor.mode(),
161 editor.mode(),
@@ -143,19 +164,52 @@ impl EditorView {
143 theme,
164 theme,
144 &config.cursor_shape,
165 &config.cursor_shape,
145 ),
166 ),
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+ }
147 } else {
174 } else {
148 Box::new(highlights)
175 Box::new(highlights)
149 };
176 };
150
177
151− Self::render_text_highlights(doc, view.offset, inner, surface, theme, highlights, &config);
178+ Self::render_gutter(
152− Self::render_gutter(editor, doc, view, view.area, surface, theme, is_focused);
179+ editor,
153− Self::render_rulers(editor, doc, view, inner, surface, theme);
180+ doc,
181+ view,
182+ view.area,
183+ theme,
184+ is_focused,
185+ &mut line_decorations,
186+ );
154
187
155 if is_focused {
188 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)));
157 }
198 }
158
199
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+
159 // if we're not at the edge of the screen, draw a right border
213 // if we're not at the edge of the screen, draw a right border
160 if viewport.right() != view.area.right() {
214 if viewport.right() != view.area.right() {
161 let x = area.right();
215 let x = area.right();
@@ -203,31 +257,53 @@ impl EditorView {
203 .iter()
257 .iter()
204 // View might be horizontally scrolled, convert from absolute distance
258 // View might be horizontally scrolled, convert from absolute distance
205 // from the 1st column to relative distance from left of viewport
259 // 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))
207 .filter(|ruler| ruler < &viewport.width)
261 .filter(|ruler| ruler < &viewport.width)
208 .map(|ruler| viewport.clip_left(ruler).with_width(1))
262 .map(|ruler| viewport.clip_left(ruler).with_width(1))
209 .for_each(|area| surface.set_style(area, ruler_theme))
263 .for_each(|area| surface.set_style(area, ruler_theme))
210 }
264 }
211
265
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+
212 /// Get syntax highlights for a document in a view represented by the first line
289 /// Get syntax highlights for a document in a view represented by the first line
213 /// and column (`offset`) and the last line. This is done instead of using a view
290 /// and column (`offset`) and the last line. This is done instead of using a view
214 /// directly to enable rendering syntax highlighted docs anywhere (eg. picker preview)
291 /// directly to enable rendering syntax highlighted docs anywhere (eg. picker preview)
215 pub fn doc_syntax_highlights<'doc>(
292 pub fn doc_syntax_highlights<'doc>(
216 doc: &'doc Document,
293 doc: &'doc Document,
217− offset: Position,
294+ anchor: usize,
218 height: u16,
295 height: u16,
219 _theme: &Theme,
296 _theme: &Theme,
220 ) -> Box<dyn Iterator<Item = HighlightEvent> + 'doc> {
297 ) -> Box<dyn Iterator<Item = HighlightEvent> + 'doc> {
221 let text = doc.text().slice(..);
298 let text = doc.text().slice(..);
299+ let row = text.char_to_line(anchor.min(text.len_chars()));
222
300
223 let range = {
301 let range = {
224 // Calculate viewport byte ranges:
302 // Calculate viewport byte ranges:
225 // Saturating subs to make it inclusive zero indexing.
303 // Saturating subs to make it inclusive zero indexing.
226− let last_line = doc.text().len_lines().saturating_sub(1);
304+ let last_line = text.len_lines().saturating_sub(1);
227− let last_visible_line = (offset.row + height as usize)
305+ let last_visible_line = (row + height as usize).saturating_sub(1).min(last_line);
228− .saturating_sub(1)
229− .min(last_line);
230− let start = text.line_to_byte(offset.row.min(last_line));
306+ let start = text.line_to_byte(row.min(last_line));
231 let end = text.line_to_byte(last_visible_line + 1);
307 let end = text.line_to_byte(last_visible_line + 1);
232
308
233 start..end
309 start..end
@@ -272,11 +348,11 @@ impl EditorView {
272 use helix_core::diagnostic::Severity;
348 use helix_core::diagnostic::Severity;
273 let get_scope_of = |scope| {
349 let get_scope_of = |scope| {
274 theme
350 theme
275− .find_scope_index(scope)
351+ .find_scope_index_exact(scope)
276 // get one of the themes below as fallback values
352 // get one of the themes below as fallback values
277− .or_else(|| theme.find_scope_index("diagnostic"))
353+ .or_else(|| theme.find_scope_index_exact("diagnostic"))
278− .or_else(|| theme.find_scope_index("ui.cursor"))
354+ .or_else(|| theme.find_scope_index_exact("ui.cursor"))
279− .or_else(|| theme.find_scope_index("ui.selection"))
355+ .or_else(|| theme.find_scope_index_exact("ui.selection"))
280 .expect(
356 .expect(
281 "at least one of the following scopes must be defined in the theme: `diagnostic`, `ui.cursor`, or `ui.selection`",
357 "at least one of the following scopes must be defined in the theme: `diagnostic`, `ui.cursor`, or `ui.selection`",
282 )
358 )
@@ -339,29 +415,29 @@ impl EditorView {
339 let cursor_is_block = cursorkind == CursorKind::Block;
415 let cursor_is_block = cursorkind == CursorKind::Block;
340
416
341 let selection_scope = theme
417 let selection_scope = theme
342− .find_scope_index("ui.selection")
418+ .find_scope_index_exact("ui.selection")
343 .expect("could not find `ui.selection` scope in the theme!");
419 .expect("could not find `ui.selection` scope in the theme!");
344 let primary_selection_scope = theme
420 let primary_selection_scope = theme
345− .find_scope_index("ui.selection.primary")
421+ .find_scope_index_exact("ui.selection.primary")
346 .unwrap_or(selection_scope);
422 .unwrap_or(selection_scope);
347 let base_cursor_scope = theme
423 let base_cursor_scope = theme
348− .find_scope_index("ui.cursor")
424+ .find_scope_index_exact("ui.cursor")
349 .unwrap_or(selection_scope);
425 .unwrap_or(selection_scope);
350 let base_primary_cursor_scope = theme
426 let base_primary_cursor_scope = theme
351 .find_scope_index("ui.cursor.primary")
427 .find_scope_index("ui.cursor.primary")
352 .unwrap_or(base_cursor_scope);
428 .unwrap_or(base_cursor_scope);
353
429
354 let cursor_scope = match mode {
430 let cursor_scope = match mode {
355− Mode::Insert => theme.find_scope_index("ui.cursor.insert"),
431+ Mode::Insert => theme.find_scope_index_exact("ui.cursor.insert"),
356− Mode::Select => theme.find_scope_index("ui.cursor.select"),
432+ Mode::Select => theme.find_scope_index_exact("ui.cursor.select"),
357− Mode::Normal => theme.find_scope_index("ui.cursor.normal"),
433+ Mode::Normal => theme.find_scope_index_exact("ui.cursor.normal"),
358 }
434 }
359 .unwrap_or(base_cursor_scope);
435 .unwrap_or(base_cursor_scope);
360
436
361 let primary_cursor_scope = match mode {
437 let primary_cursor_scope = match mode {
362− Mode::Insert => theme.find_scope_index("ui.cursor.primary.insert"),
438+ Mode::Insert => theme.find_scope_index_exact("ui.cursor.primary.insert"),
363− Mode::Select => theme.find_scope_index("ui.cursor.primary.select"),
439+ Mode::Select => theme.find_scope_index_exact("ui.cursor.primary.select"),
364− Mode::Normal => theme.find_scope_index("ui.cursor.primary.normal"),
440+ Mode::Normal => theme.find_scope_index_exact("ui.cursor.primary.normal"),
365 }
441 }
366 .unwrap_or(base_primary_cursor_scope);
442 .unwrap_or(base_primary_cursor_scope);
367
443
@@ -424,248 +500,26 @@ impl EditorView {
424 spans
500 spans
425 }
501 }
426
502
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−
636 /// Render brace match, etc (meant for the focused view only)
503 /// Render brace match, etc (meant for the focused view only)
637− pub fn render_focused_view_elements(
504+ pub fn highlight_focused_view_elements(
638 view: &View,
505 view: &View,
639 doc: &Document,
506 doc: &Document,
640− viewport: Rect,
641 theme: &Theme,
507 theme: &Theme,
642− surface: &mut Surface,
508+ ) -> Vec<(usize, std::ops::Range<usize>)> {
643− ) {
644 // Highlight matching braces
509 // Highlight matching braces
645 if let Some(syntax) = doc.syntax() {
510 if let Some(syntax) = doc.syntax() {
646 let text = doc.text().slice(..);
511 let text = doc.text().slice(..);
647 use helix_core::match_brackets;
512 use helix_core::match_brackets;
648 let pos = doc.selection(view.id).primary().cursor(text);
513 let pos = doc.selection(view.id).primary().cursor(text);
649
514
650− let pos = match_brackets::find_matching_bracket(syntax, doc.text(), pos)
515+ if let Some(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 {
654 // ensure col is on screen
516 // 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(|| {
517+ if let Some(highlight) = theme.find_scope_index_exact("ui.cursor.match") {
659− Style::default()
518+ return vec![(highlight, pos..pos + 1)];
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);
666 }
519 }
667 }
520 }
668 }
521 }
522+ Vec::new()
669 }
523 }
670
524
671 /// Render bufferline at the top
525 /// Render bufferline at the top
@@ -721,22 +575,17 @@ impl EditorView {
721 }
575 }
722 }
576 }
723
577
724− pub fn render_gutter(
578+ pub fn render_gutter<'d>(
725− editor: &Editor,
579+ editor: &'d Editor,
726− doc: &Document,
580+ doc: &'d Document,
727 view: &View,
581 view: &View,
728 viewport: Rect,
582 viewport: Rect,
729− surface: &mut Surface,
730 theme: &Theme,
583 theme: &Theme,
731 is_focused: bool,
584 is_focused: bool,
585+ line_decorations: &mut Vec<Box<(dyn LineDecoration + 'd)>>,
732 ) {
586 ) {
733 let text = doc.text().slice(..);
587 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
740 .selection(view.id)
589 .selection(view.id)
741 .iter()
590 .iter()
742 .map(|range| range.cursor_line(text))
591 .map(|range| range.cursor_line(text))
@@ -746,29 +595,36 @@ impl EditorView {
746
595
747 let gutter_style = theme.get("ui.gutter");
596 let gutter_style = theme.get("ui.gutter");
748 let gutter_selected_style = theme.get("ui.gutter.selected");
597 let gutter_selected_style = theme.get("ui.gutter.selected");
749−
598+ let gutter_style_virtual = theme.get("ui.gutter.virtual");
750− // avoid lots of small allocations by reusing a text buffer for each line
599+ let gutter_selected_style_virtual = theme.get("ui.gutter.selected.virtual");
751− let mut text = String::with_capacity(8);
752
600
753 for gutter_type in view.gutters() {
601 for gutter_type in view.gutters() {
754 let mut gutter = gutter_type.style(editor, doc, view, theme, is_focused);
602 let mut gutter = gutter_type.style(editor, doc, view, theme, is_focused);
755 let width = gutter_type.width(view, doc);
603 let width = gutter_type.width(view, doc);
756− text.reserve(width); // ensure there's enough space for the gutter
604+ // avoid lots of small allocations by reusing a text buffer for each line
757− for (i, line) in (view.offset.row..(last_line + 1)).enumerate() {
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
758− let selected = cursors.contains(&line);
609+ let selected = cursors.contains(&pos.doc_line);
759 let x = viewport.x + offset;
610 let x = viewport.x + offset;
760− let y = viewport.y + i as u16;
611+ let y = viewport.y + pos.visual_line;
761
612
762− let gutter_style = if selected {
613+ let gutter_style = match (selected, pos.first_visual_line) {
614+ (false, true) => gutter_style,
763− gutter_selected_style
615+ (true, true) => gutter_selected_style,
764− } else {
616+ (false, false) => gutter_style_virtual,
765− gutter_style
617+ (true, false) => gutter_selected_style_virtual,
766 };
618 };
767
619
768− if let Some(style) = gutter(line, selected, &mut text) {
620+ if let Some(style) =
621+ gutter(pos.doc_line, selected, pos.first_visual_line, &mut text)
622+ {
623+ renderer
624+ .surface
769− surface.set_stringn(x, y, &text, width, gutter_style.patch(style));
625+ .set_stringn(x, y, &text, width, gutter_style.patch(style));
770 } else {
626 } else {
771− surface.set_style(
627+ renderer.surface.set_style(
772 Rect {
628 Rect {
773 x,
629 x,
774 y,
630 y,
@@ -779,7 +635,8 @@ impl EditorView {
779 );
635 );
780 }
636 }
781 text.clear();
637 text.clear();
782− }
638+ };
639+ line_decorations.push(Box::new(gutter_decoration));
783
640
784 offset += width as u16;
641 offset += width as u16;
785 }
642 }
@@ -840,10 +697,13 @@ impl EditorView {
840 }
697 }
841
698
842 /// Apply the highlighting on the lines where a cursor is active
699 /// 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> {
844 let text = doc.text().slice(..);
705 let text = doc.text().slice(..);
845− let last_line = view.last_line(doc);
706+ // TODO only highlight the visual line that contains the cursor instead of the full visual line
846−
847 let primary_line = doc.selection(view.id).primary().cursor_line(text);
707 let primary_line = doc.selection(view.id).primary().cursor_line(text);
848
708
849 // The secondary_lines do contain the primary_line, it doesn't matter
709 // The secondary_lines do contain the primary_line, it doesn't matter
@@ -860,20 +720,23 @@ impl EditorView {
860
720
861 let primary_style = theme.get("ui.cursorline.primary");
721 let primary_style = theme.get("ui.cursorline.primary");
862 let secondary_style = theme.get("ui.cursorline.secondary");
722 let secondary_style = theme.get("ui.cursorline.secondary");
723+ let viewport = view.area;
863
724
864− for line in view.offset.row..(last_line + 1) {
725+ let line_decoration = move |renderer: &mut TextRenderer, pos: LinePos| {
865 let area = Rect::new(
726 let area = Rect::new(
866− view.area.x,
727+ viewport.x,
867− view.area.y + (line - view.offset.row) as u16,
728+ viewport.y + pos.visual_line as u16,
868− view.area.width,
729+ viewport.width,
869 1,
730 1,
870 );
731 );
871− if primary_line == line {
732+ if primary_line == pos.doc_line {
872− surface.set_style(area, primary_style);
733+ renderer.surface.set_style(area, primary_style);
873− } else if secondary_lines.binary_search(&line).is_ok() {
734+ } else if secondary_lines.binary_search(&pos.doc_line).is_ok() {
874− surface.set_style(area, secondary_style);
735+ renderer.surface.set_style(area, secondary_style);
875 }
736 }
876− }
737+ };
738+
739+ Box::new(line_decoration)
877 }
740 }
878
741
879 /// Apply the highlighting on the columns where a cursor is active
742 /// Apply the highlighting on the columns where a cursor is active
@@ -882,6 +745,8 @@ impl EditorView {
882 view: &View,
745 view: &View,
883 surface: &mut Surface,
746 surface: &mut Surface,
884 theme: &Theme,
747 theme: &Theme,
748+ viewport: Rect,
749+ text_annotations: &TextAnnotations,
885 ) {
750 ) {
886 let text = doc.text().slice(..);
751 let text = doc.text().slice(..);
887
752
@@ -897,19 +762,23 @@ impl EditorView {
897 .unwrap_or_else(|| theme.get("ui.cursorline.secondary"));
762 .unwrap_or_else(|| theme.get("ui.cursorline.secondary"));
898
763
899 let inner_area = view.inner_area(doc);
764 let inner_area = view.inner_area(doc);
900− let offset = view.offset.col;
901
765
902 let selection = doc.selection(view.id);
766 let selection = doc.selection(view.id);
903 let primary = selection.primary();
767 let primary = selection.primary();
768+ let text_format = doc.text_format(viewport.width, None);
904 for range in selection.iter() {
769 for range in selection.iter() {
905 let is_primary = primary == *range;
770 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;
906
775
907− let Position { row: _, col } =
908− visual_coords_at_pos(text, range.cursor(text), doc.tab_width());
909 // if the cursor is horizontally in the view
776 // if the cursor is horizontally in the view
777+ if col >= view.offset.horizontal_offset
910− if col >= offset && inner_area.width > (col - offset) as u16 {
778+ && inner_area.width > (col - view.offset.horizontal_offset) as u16
779+ {
911 let area = Rect::new(
780 let area = Rect::new(
912− inner_area.x + (col - offset) as u16,
781+ inner_area.x + (col - view.offset.horizontal_offset) as u16,
913 view.area.y,
782 view.area.y,
914 1,
783 1,
915 view.area.height,
784 view.area.height,
@@ -1149,7 +1018,7 @@ impl EditorView {
1149
1018
1150 let pos_and_view = |editor: &Editor, row, column| {
1019 let pos_and_view = |editor: &Editor, row, column| {
1151 editor.tree.views().find_map(|(view, _focus)| {
1020 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)
1153 .map(|pos| (pos, view.id))
1022 .map(|pos| (pos, view.id))
1154 })
1023 })
1155 };
1024 };
@@ -1191,8 +1060,10 @@ impl EditorView {
1191 None => return EventResult::Ignored(None),
1060 None => return EventResult::Ignored(None),
1192 };
1061 };
1193
1062
1194− let line = coords.row + view.offset.row;
1063+ if let Some(char_idx) =
1195− if line < doc.text().len_lines() {
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);
1196 commands::dap_toggle_breakpoint_impl(cxt, path, line);
1067 commands::dap_toggle_breakpoint_impl(cxt, path, line);
1197 return EventResult::Consumed(None);
1068 return EventResult::Consumed(None);
1198 }
1069 }
@@ -1204,7 +1075,7 @@ impl EditorView {
1204 MouseEventKind::Drag(MouseButton::Left) => {
1075 MouseEventKind::Drag(MouseButton::Left) => {
1205 let (view, doc) = current!(cxt.editor);
1076 let (view, doc) = current!(cxt.editor);
1206
1077
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) {
1208 Some(pos) => pos,
1079 Some(pos) => pos,
1209 None => return EventResult::Ignored(None),
1080 None => return EventResult::Ignored(None),
1210 };
1081 };
@@ -1268,8 +1139,9 @@ impl EditorView {
1268 cxt.editor.focus(view_id);
1139 cxt.editor.focus(view_id);
1269
1140
1270 let (view, doc) = current!(cxt.editor);
1141 let (view, doc) = current!(cxt.editor);
1271− let line = coords.row + view.offset.row;
1142+ if let Some(pos) =
1272− if let Ok(pos) = doc.text().try_line_to_char(line) {
1143+ view.pos_at_visual_coords(doc, coords.row as u16, coords.col as u16, true)
1144+ {
1273 doc.set_selection(view_id, Selection::point(pos));
1145 doc.set_selection(view_id, Selection::point(pos));
1274 if modifiers == KeyModifiers::ALT {
1146 if modifiers == KeyModifiers::ALT {
1275 commands::MappableCommand::dap_edit_log.execute(cxt);
1147 commands::MappableCommand::dap_edit_log.execute(cxt);
helix-term/src/ui/lsp.rs+4−1
@@ -53,7 +53,10 @@ impl Component for SignatureHelp {
53
53
54 let active_param_span = self.active_param_range.map(|(start, end)| {
54 let active_param_span = self.active_param_range.map(|(start, end)| {
55 vec![(
55 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(),
57 start..end,
60 start..end,
58 )]
61 )]
59 });
62 });
helix-term/src/ui/mod.rs+1
@@ -1,4 +1,5 @@
1mod completion;
1mod completion;
2+mod document;
2pub(crate) mod editor;
3pub(crate) mod editor;
3mod fuzzy_match;
4mod fuzzy_match;
4mod info;
5mod info;
helix-term/src/ui/picker.rs+52−29
@@ -2,7 +2,12 @@ use crate::{
2 alt,
2 alt,
3 compositor::{Component, Compositor, Context, Event, EventResult},
3 compositor::{Component, Compositor, Context, Event, EventResult},
4 ctrl, key, shift,
4 ctrl, key, shift,
5+ ui::{
6+ self,
7+ document::{render_document, LineDecoration, LinePos, TextRenderer},
5− ui::{self, fuzzy_match::FuzzyQuery, EditorView},
8+ fuzzy_match::FuzzyQuery,
9+ EditorView,
10+ },
6};
11};
7use futures_util::future::BoxFuture;
12use futures_util::future::BoxFuture;
8use tui::{
13use tui::{
@@ -19,11 +24,15 @@ use std::cmp::{self, Ordering};
19use std::{collections::HashMap, io::Read, path::PathBuf};
24use std::{collections::HashMap, io::Read, path::PathBuf};
20
25
21use crate::ui::{Prompt, PromptEvent};
26use crate::ui::{Prompt, PromptEvent};
27+use helix_core::{
28+ movement::Direction, text_annotations::TextAnnotations,
22−use helix_core::{movement::Direction, unicode::segmentation::UnicodeSegmentation, Position};
29+ unicode::segmentation::UnicodeSegmentation, Position,
30+};
23use helix_view::{
31use helix_view::{
24 editor::Action,
32 editor::Action,
25 graphics::{CursorKind, Margin, Modifier, Rect},
33 graphics::{CursorKind, Margin, Modifier, Rect},
26 theme::Style,
34 theme::Style,
35+ view::ViewPosition,
27 Document, DocumentId, Editor,
36 Document, DocumentId, Editor,
28};
37};
29
38
@@ -179,7 +188,7 @@ impl<T: Item> FilePicker<T> {
179 }
188 }
180 _ => {
189 _ => {
181 // TODO: enable syntax highlighting; blocked by async rendering
190 // TODO: enable syntax highlighting; blocked by async rendering
182− Document::open(path, None, None)
191+ Document::open(path, None, None, editor.config.clone())
183 .map(|doc| CachedPreview::Document(Box::new(doc)))
192 .map(|doc| CachedPreview::Document(Box::new(doc)))
184 .unwrap_or(CachedPreview::NotFound)
193 .unwrap_or(CachedPreview::NotFound)
185 }
194 }
@@ -283,43 +292,57 @@ impl<T: Item + 'static> Component for FilePicker<T> {
283 })
292 })
284 .unwrap_or(0);
293 .unwrap_or(0);
285
294
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+ };
287
300
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+ );
290 for spans in EditorView::doc_diagnostics_highlights(doc, &cx.editor.theme) {
307 for spans in EditorView::doc_diagnostics_highlights(doc, &cx.editor.theme) {
291 if spans.is_empty() {
308 if spans.is_empty() {
292 continue;
309 continue;
293 }
310 }
294 highlights = Box::new(helix_core::syntax::merge(highlights, spans));
311 highlights = Box::new(helix_core::syntax::merge(highlights, spans));
295 }
312 }
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,
297 doc,
338 doc,
298 offset,
339 offset,
299− inner,
340+ &TextAnnotations::default(),
300− surface,
301− &cx.editor.theme,
302 highlights,
341 highlights,
303− &cx.editor.config(),
342+ &cx.editor.theme,
343+ &mut decorations,
344+ &mut [],
304 );
345 );
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− }
323 }
346 }
324 }
347 }
325
348
helix-view/src/document.rs+61−13
@@ -1,7 +1,11 @@
1use anyhow::{anyhow, bail, Context, Error};
1use anyhow::{anyhow, bail, Context, Error};
2+use arc_swap::access::DynAccess;
2use futures_util::future::BoxFuture;
3use futures_util::future::BoxFuture;
3use futures_util::FutureExt;
4use futures_util::FutureExt;
4use helix_core::auto_pairs::AutoPairs;
5use 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;
5use helix_core::Range;
9use helix_core::Range;
6use helix_vcs::{DiffHandle, DiffProviderRegistry};
10use helix_vcs::{DiffHandle, DiffProviderRegistry};
7
11
@@ -26,8 +30,8 @@ use helix_core::{
26 DEFAULT_LINE_ENDING,
30 DEFAULT_LINE_ENDING,
27};
31};
28
32
29−use crate::editor::RedrawHandle;
33+use crate::editor::{Config, RedrawHandle};
30−use crate::{DocumentId, Editor, View, ViewId};
34+use crate::{DocumentId, Editor, Theme, View, ViewId};
31
35
32/// 8kB of buffer space for encoding and decoding `Rope`s.
36/// 8kB of buffer space for encoding and decoding `Rope`s.
33const BUF_SIZE: usize = 8192;
37const BUF_SIZE: usize = 8192;
@@ -127,6 +131,7 @@ pub struct Document {
127 // it back as it separated from the edits. We could split out the parts manually but that will
131 // it back as it separated from the edits. We could split out the parts manually but that will
128 // be more troublesome.
132 // be more troublesome.
129 pub history: Cell<History>,
133 pub history: Cell<History>,
134+ pub config: Arc<dyn DynAccess<Config>>,
130
135
131 pub savepoint: Option<Transaction>,
136 pub savepoint: Option<Transaction>,
132
137
@@ -351,7 +356,11 @@ use helix_lsp::lsp;
351use url::Url;
356use url::Url;
352
357
353impl Document {
358impl Document {
359+ pub fn from(
360+ text: Rope,
354− pub fn from(text: Rope, encoding: Option<&'static encoding::Encoding>) -> Self {
361+ encoding: Option<&'static encoding::Encoding>,
362+ config: Arc<dyn DynAccess<Config>>,
363+ ) -> Self {
355 let encoding = encoding.unwrap_or(encoding::UTF_8);
364 let encoding = encoding.unwrap_or(encoding::UTF_8);
356 let changes = ChangeSet::new(&text);
365 let changes = ChangeSet::new(&text);
357 let old_state = None;
366 let old_state = None;
@@ -377,9 +386,13 @@ impl Document {
377 modified_since_accessed: false,
386 modified_since_accessed: false,
378 language_server: None,
387 language_server: None,
379 diff_handle: None,
388 diff_handle: None,
389+ config,
380 }
390 }
381 }
391 }
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+ }
383 // TODO: async fn?
396 // TODO: async fn?
384 /// Create a new document from `path`. Encoding is auto-detected, but it can be manually
397 /// Create a new document from `path`. Encoding is auto-detected, but it can be manually
385 /// overwritten with the `encoding` parameter.
398 /// overwritten with the `encoding` parameter.
@@ -387,6 +400,7 @@ impl Document {
387 path: &Path,
400 path: &Path,
388 encoding: Option<&'static encoding::Encoding>,
401 encoding: Option<&'static encoding::Encoding>,
389 config_loader: Option<Arc<syntax::Loader>>,
402 config_loader: Option<Arc<syntax::Loader>>,
403+ config: Arc<dyn DynAccess<Config>>,
390 ) -> Result<Self, Error> {
404 ) -> Result<Self, Error> {
391 // Open the file if it exists, otherwise assume it is a new file (and thus empty).
405 // Open the file if it exists, otherwise assume it is a new file (and thus empty).
392 let (rope, encoding) = if path.exists() {
406 let (rope, encoding) = if path.exists() {
@@ -398,7 +412,7 @@ impl Document {
398 (Rope::from(DEFAULT_LINE_ENDING.as_str()), encoding)
412 (Rope::from(DEFAULT_LINE_ENDING.as_str()), encoding)
399 };
413 };
400
414
401− let mut doc = Self::from(rope, Some(encoding));
415+ let mut doc = Self::from(rope, Some(encoding), config);
402
416
403 // set the path and try detecting the language
417 // set the path and try detecting the language
404 doc.set_path(Some(path))?;
418 doc.set_path(Some(path))?;
@@ -1192,12 +1206,34 @@ impl Document {
1192 None => global_config,
1206 None => global_config,
1193 }
1207 }
1194 }
1208 }
1195−}
1196
1209
1197−impl Default for Document {
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 {
1198− fn default() -> Self {
1236+ TextAnnotations::default()
1199− let text = Rope::from(DEFAULT_LINE_ENDING.as_str());
1200− Self::from(text, None)
1201 }
1237 }
1202}
1238}
1203
1239
@@ -1236,13 +1272,19 @@ impl Display for FormatterError {
1236
1272
1237#[cfg(test)]
1273#[cfg(test)]
1238mod test {
1274mod test {
1275+ use arc_swap::ArcSwap;
1276+
1239 use super::*;
1277 use super::*;
1240
1278
1241 #[test]
1279 #[test]
1242 fn changeset_to_changes_ignore_line_endings() {
1280 fn changeset_to_changes_ignore_line_endings() {
1243 use helix_lsp::{lsp, Client, OffsetEncoding};
1281 use helix_lsp::{lsp, Client, OffsetEncoding};
1244 let text = Rope::from("hello\r\nworld");
1282 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+ );
1246 let view = ViewId::default();
1288 let view = ViewId::default();
1247 doc.set_selection(view, Selection::single(0, 0));
1289 doc.set_selection(view, Selection::single(0, 0));
1248
1290
@@ -1276,7 +1318,11 @@ mod test {
1276 fn changeset_to_changes() {
1318 fn changeset_to_changes() {
1277 use helix_lsp::{lsp, Client, OffsetEncoding};
1319 use helix_lsp::{lsp, Client, OffsetEncoding};
1278 let text = Rope::from("hello");
1320 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+ );
1280 let view = ViewId::default();
1326 let view = ViewId::default();
1281 doc.set_selection(view, Selection::single(5, 5));
1327 doc.set_selection(view, Selection::single(5, 5));
1282
1328
@@ -1389,7 +1435,9 @@ mod test {
1389 #[test]
1435 #[test]
1390 fn test_line_ending() {
1436 fn test_line_ending() {
1391 assert_eq!(
1437 assert_eq!(
1438+ Document::default(Arc::new(ArcSwap::new(Arc::new(Config::default()))))
1439+ .text()
1392− Document::default().text().to_string(),
1440+ .to_string(),
1393 DEFAULT_LINE_ENDING.as_str()
1441 DEFAULT_LINE_ENDING.as_str()
1394 );
1442 );
1395 }
1443 }
helix-view/src/editor.rs+76−9
@@ -7,6 +7,7 @@ use crate::{
7 input::KeyEvent,
7 input::KeyEvent,
8 theme::{self, Theme},
8 theme::{self, Theme},
9 tree::{self, Tree},
9 tree::{self, Tree},
10+ view::ViewPosition,
10 Align, Document, DocumentId, View, ViewId,
11 Align, Document, DocumentId, View, ViewId,
11};
12};
12use helix_vcs::DiffProviderRegistry;
13use helix_vcs::DiffProviderRegistry;
@@ -18,6 +19,7 @@ use tokio_stream::wrappers::UnboundedReceiverStream;
18
19
19use std::{
20use std::{
20 borrow::Cow,
21 borrow::Cow,
22+ cell::Cell,
21 collections::{BTreeMap, HashMap},
23 collections::{BTreeMap, HashMap},
22 io::stdin,
24 io::stdin,
23 num::NonZeroUsize,
25 num::NonZeroUsize,
@@ -268,6 +270,44 @@ pub struct Config {
268 pub indent_guides: IndentGuidesConfig,
270 pub indent_guides: IndentGuidesConfig,
269 /// Whether to color modes with different colors. Defaults to `false`.
271 /// Whether to color modes with different colors. Defaults to `false`.
270 pub color_modes: bool,
272 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+ }
271}
311}
272
312
273#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
313#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
@@ -717,6 +757,7 @@ impl Default for Config {
717 bufferline: BufferLine::default(),
757 bufferline: BufferLine::default(),
718 indent_guides: IndentGuidesConfig::default(),
758 indent_guides: IndentGuidesConfig::default(),
719 color_modes: false,
759 color_modes: false,
760+ soft_wrap: SoftWrap::default(),
720 }
761 }
721 }
762 }
722}
763}
@@ -797,7 +838,7 @@ pub struct Editor {
797 pub status_msg: Option<(Cow<'static, str>, Severity)>,
838 pub status_msg: Option<(Cow<'static, str>, Severity)>,
798 pub autoinfo: Option<Info>,
839 pub autoinfo: Option<Info>,
799
840
800− pub config: Box<dyn DynAccess<Config>>,
841+ pub config: Arc<dyn DynAccess<Config>>,
801 pub auto_pairs: Option<AutoPairs>,
842 pub auto_pairs: Option<AutoPairs>,
802
843
803 pub idle_timer: Pin<Box<Sleep>>,
844 pub idle_timer: Pin<Box<Sleep>>,
@@ -813,6 +854,19 @@ pub struct Editor {
813 /// The `RwLock` blocks the editor from performing the render until an exclusive lock can be aquired
854 /// The `RwLock` blocks the editor from performing the render until an exclusive lock can be aquired
814 pub redraw_handle: RedrawHandle,
855 pub redraw_handle: RedrawHandle,
815 pub needs_redraw: bool,
856 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>>>,
816}
870}
817
871
818pub type RedrawHandle = (Arc<Notify>, Arc<RwLock<()>>);
872pub type RedrawHandle = (Arc<Notify>, Arc<RwLock<()>>);
@@ -866,7 +920,7 @@ impl Editor {
866 mut area: Rect,
920 mut area: Rect,
867 theme_loader: Arc<theme::Loader>,
921 theme_loader: Arc<theme::Loader>,
868 syn_loader: Arc<syntax::Loader>,
922 syn_loader: Arc<syntax::Loader>,
869− config: Box<dyn DynAccess<Config>>,
923+ config: Arc<dyn DynAccess<Config>>,
870 ) -> Self {
924 ) -> Self {
871 let conf = config.load();
925 let conf = config.load();
872 let auto_pairs = (&conf.auto_pairs).into();
926 let auto_pairs = (&conf.auto_pairs).into();
@@ -910,6 +964,7 @@ impl Editor {
910 config_events: unbounded_channel(),
964 config_events: unbounded_channel(),
911 redraw_handle: Default::default(),
965 redraw_handle: Default::default(),
912 needs_redraw: false,
966 needs_redraw: false,
967+ cursor_cache: Cell::new(None),
913 }
968 }
914 }
969 }
915
970
@@ -994,7 +1049,7 @@ impl Editor {
994
1049
995 fn set_theme_impl(&mut self, theme: Theme, preview: ThemeAction) {
1050 fn set_theme_impl(&mut self, theme: Theme, preview: ThemeAction) {
996 // `ui.selection` is the only scope required to be able to render a theme.
1051 // `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() {
998 self.set_error("Invalid theme: `ui.selection` required");
1053 self.set_error("Invalid theme: `ui.selection` required");
999 return;
1054 return;
1000 }
1055 }
@@ -1077,7 +1132,7 @@ impl Editor {
1077 fn replace_document_in_view(&mut self, current_view: ViewId, doc_id: DocumentId) {
1132 fn replace_document_in_view(&mut self, current_view: ViewId, doc_id: DocumentId) {
1078 let view = self.tree.get_mut(current_view);
1133 let view = self.tree.get_mut(current_view);
1079 view.doc = doc_id;
1134 view.doc = doc_id;
1080− view.offset = Position::default();
1135+ view.offset = ViewPosition::default();
1081
1136
1082 let doc = doc_mut!(self, &doc_id);
1137 let doc = doc_mut!(self, &doc_id);
1083 doc.ensure_view_init(view.id);
1138 doc.ensure_view_init(view.id);
@@ -1204,12 +1259,15 @@ impl Editor {
1204 }
1259 }
1205
1260
1206 pub fn new_file(&mut self, action: Action) -> DocumentId {
1261 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()))
1208 }
1263 }
1209
1264
1210 pub fn new_file_from_stdin(&mut self, action: Action) -> Result<DocumentId, Error> {
1265 pub fn new_file_from_stdin(&mut self, action: Action) -> Result<DocumentId, Error> {
1211 let (rope, encoding) = crate::document::from_reader(&mut stdin(), None)?;
1266 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+ ))
1213 }
1271 }
1214
1272
1215 // ??? possible use for integration tests
1273 // ??? possible use for integration tests
@@ -1220,7 +1278,12 @@ impl Editor {
1220 let id = if let Some(id) = id {
1278 let id = if let Some(id) = id {
1221 id
1279 id
1222 } else {
1280 } else {
1281+ let mut doc = Document::open(
1282+ &path,
1283+ None,
1223− let mut doc = Document::open(&path, None, Some(self.syn_loader.clone()))?;
1284+ Some(self.syn_loader.clone()),
1285+ self.config.clone(),
1286+ )?;
1224
1287
1225 let _ = Self::launch_language_server(&mut self.language_servers, &mut doc);
1288 let _ = Self::launch_language_server(&mut self.language_servers, &mut doc);
1226 if let Some(diff_base) = self.diff_providers.get_diff_base(&path) {
1289 if let Some(diff_base) = self.diff_providers.get_diff_base(&path) {
@@ -1306,7 +1369,7 @@ impl Editor {
1306 .iter()
1369 .iter()
1307 .map(|(&doc_id, _)| doc_id)
1370 .map(|(&doc_id, _)| doc_id)
1308 .next()
1371 .next()
1309− .unwrap_or_else(|| self.new_document(Document::default()));
1372+ .unwrap_or_else(|| self.new_document(Document::default(self.config.clone())));
1310 let view = View::new(doc_id, self.config().gutters.clone());
1373 let view = View::new(doc_id, self.config().gutters.clone());
1311 let view_id = self.tree.insert(view);
1374 let view_id = self.tree.insert(view);
1312 let doc = doc_mut!(self, &doc_id);
1375 let doc = doc_mut!(self, &doc_id);
@@ -1440,7 +1503,11 @@ impl Editor {
1440 .selection(view.id)
1503 .selection(view.id)
1441 .primary()
1504 .primary()
1442 .cursor(doc.text().slice(..));
1505 .cursor(doc.text().slice(..));
1506+ let pos = self
1507+ .cursor_cache
1508+ .get()
1443− if let Some(mut pos) = view.screen_coords_at_pos(doc, doc.text().slice(..), cursor) {
1509+ .unwrap_or_else(|| view.screen_coords_at_pos(doc, doc.text().slice(..), cursor));
1510+ if let Some(mut pos) = pos {
1444 let inner = view.inner_area(doc);
1511 let inner = view.inner_area(doc);
1445 pos.col += inner.x as usize;
1512 pos.col += inner.x as usize;
1446 pos.row += inner.y as usize;
1513 pos.row += inner.y as usize;
helix-view/src/gutter.rs+170−123
@@ -12,7 +12,7 @@ fn count_digits(n: usize) -> usize {
12 std::iter::successors(Some(n), |&n| (n >= 10).then(|| n / 10)).count()
12 std::iter::successors(Some(n), |&n| (n >= 10).then(|| n / 10)).count()
13}
13}
14
14
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>;
16pub type Gutter =
16pub type Gutter =
17 for<'doc> fn(&'doc Editor, &'doc Document, &View, &Theme, bool, usize) -> GutterFn<'doc>;
17 for<'doc> fn(&'doc Editor, &'doc Document, &View, &Theme, bool, usize) -> GutterFn<'doc>;
18
18
@@ -58,31 +58,36 @@ pub fn diagnostic<'doc>(
58 let hint = theme.get("hint");
58 let hint = theme.get("hint");
59 let diagnostics = doc.diagnostics();
59 let diagnostics = doc.diagnostics();
60
60
61+ Box::new(
61− Box::new(move |line: usize, _selected: bool, out: &mut String| {
62+ move |line: usize, _selected: bool, first_visual_line: bool, out: &mut String| {
63+ if !first_visual_line {
64+ return None;
65+ }
62− use helix_core::diagnostic::Severity;
66+ use helix_core::diagnostic::Severity;
63− if let Ok(index) = diagnostics.binary_search_by_key(&line, |d| d.line) {
67+ 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);
68+ let after = diagnostics[index..].iter().take_while(|d| d.line == line);
65−
69+
66− let before = diagnostics[..index]
70+ let before = diagnostics[..index]
67− .iter()
71+ .iter()
68− .rev()
72+ .rev()
69− .take_while(|d| d.line == line);
73+ .take_while(|d| d.line == line);
70−
74+
71− let diagnostics_on_line = after.chain(before);
75+ let diagnostics_on_line = after.chain(before);
72−
76+
73− // This unwrap is safe because the iterator cannot be empty as it contains at least the item found by the binary search.
77+ // 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();
78+ let diagnostic = diagnostics_on_line.max_by_key(|d| d.severity).unwrap();
75−
79+
76− write!(out, "●").unwrap();
80+ write!(out, "●").unwrap();
77− return Some(match diagnostic.severity {
81+ return Some(match diagnostic.severity {
78− Some(Severity::Error) => error,
82+ Some(Severity::Error) => error,
79− Some(Severity::Warning) | None => warning,
83+ Some(Severity::Warning) | None => warning,
80− Some(Severity::Info) => info,
84+ Some(Severity::Info) => info,
81− Some(Severity::Hint) => hint,
85+ Some(Severity::Hint) => hint,
82− });
86+ });
83− }
87+ }
84− None
88+ None
89+ },
85− })
90+ )
86}
91}
87
92
88pub fn diff<'doc>(
93pub fn diff<'doc>(
@@ -99,36 +104,41 @@ pub fn diff<'doc>(
99 let hunks = diff_handle.hunks();
104 let hunks = diff_handle.hunks();
100 let mut hunk_i = 0;
105 let mut hunk_i = 0;
101 let mut hunk = hunks.nth_hunk(hunk_i);
106 let mut hunk = hunks.nth_hunk(hunk_i);
107+ Box::new(
102− Box::new(move |line: usize, _selected: bool, out: &mut String| {
108+ move |line: usize, _selected: bool, first_visual_line: bool, out: &mut String| {
103− // truncating the line is fine here because we don't compute diffs
109+ // truncating the line is fine here because we don't compute diffs
104− // for files with more lines than i32::MAX anyways
110+ // for files with more lines than i32::MAX anyways
105− // we need to special case removals here
111+ // 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`).
112+ // 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
113+ // 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
114+ while hunk.after.end < line as u32
109− || !hunk.is_pure_removal() && line as u32 == hunk.after.end
115+ || !hunk.is_pure_removal() && line as u32 == hunk.after.end
110− {
116+ {
111− hunk_i += 1;
117+ hunk_i += 1;
112− hunk = hunks.nth_hunk(hunk_i);
118+ hunk = hunks.nth_hunk(hunk_i);
113− }
119+ }
114−
120+
115− if hunk.after.start > line as u32 {
121+ if hunk.after.start > line as u32 {
116− return None;
122+ return None;
117− }
123+ }
118−
124+
119− let (icon, style) = if hunk.is_pure_insertion() {
125+ let (icon, style) = if hunk.is_pure_insertion() {
120− ("▍", added)
126+ ("▍", added)
121− } else if hunk.is_pure_removal() {
127+ } else if hunk.is_pure_removal() {
128+ if !first_visual_line {
129+ return None;
130+ }
122− ("▔", deleted)
131+ ("▔", deleted)
123− } else {
132+ } else {
124− ("▍", modified)
133+ ("▍", modified)
125− };
134+ };
126−
135+
127− write!(out, "{}", icon).unwrap();
136+ write!(out, "{}", icon).unwrap();
128− Some(style)
137+ Some(style)
138+ },
129− })
139+ )
130 } else {
140 } else {
131− Box::new(move |_, _, _| None)
141+ Box::new(move |_, _, _, _| None)
132 }
142 }
133}
143}
134
144
@@ -142,7 +152,7 @@ pub fn line_numbers<'doc>(
142 let text = doc.text().slice(..);
152 let text = doc.text().slice(..);
143 let width = line_numbers_width(view, doc);
153 let width = line_numbers_width(view, doc);
144
154
145− let last_line_in_view = view.last_line(doc);
155+ let last_line_in_view = view.estimate_last_doc_line(doc);
146
156
147 // Whether to draw the line number for the last line of the
157 // Whether to draw the line number for the last line of the
148 // document or not. We only draw it if it's not an empty line.
158 // document or not. We only draw it if it's not an empty line.
@@ -158,34 +168,42 @@ pub fn line_numbers<'doc>(
158 let line_number = editor.config().line_number;
168 let line_number = editor.config().line_number;
159 let mode = editor.mode;
169 let mode = editor.mode;
160
170
171+ Box::new(
161− Box::new(move |line: usize, selected: bool, out: &mut String| {
172+ move |line: usize, selected: bool, first_visual_line: bool, out: &mut String| {
162− if line == last_line_in_view && !draw_last {
173+ if line == last_line_in_view && !draw_last {
163− write!(out, "{:>1$}", '~', width).unwrap();
174+ write!(out, "{:>1$}", '~', width).unwrap();
164− Some(linenr)
175+ 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)
175 } else {
176 } else {
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 {
176− line + 1
187+ line + 1
177− };
188+ };
178−
189+
179− let style = if selected && is_focused {
190+ let style = if selected && is_focused {
180− linenr_select
191+ linenr_select
181− } else {
192+ } else {
182− linenr
193+ linenr
183− };
194+ };
184−
195+
196+ if first_visual_line {
185− write!(out, "{:>1$}", display_num, width).unwrap();
197+ write!(out, "{:>1$}", display_num, width).unwrap();
186− Some(style)
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+ }
187− }
205+ },
188− })
206+ )
189}
207}
190
208
191/// The width of a "line-numbers" gutter
209/// The width of a "line-numbers" gutter
@@ -210,7 +228,7 @@ pub fn padding<'doc>(
210 _theme: &Theme,
228 _theme: &Theme,
211 _is_focused: bool,
229 _is_focused: bool,
212) -> GutterFn<'doc> {
230) -> 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)
214}
232}
215
233
216#[inline(always)]
234#[inline(always)]
@@ -237,41 +255,46 @@ pub fn breakpoints<'doc>(
237
255
238 let breakpoints = match breakpoints {
256 let breakpoints = match breakpoints {
239 Some(breakpoints) => breakpoints,
257 Some(breakpoints) => breakpoints,
240− None => return Box::new(move |_, _, _| None),
258+ None => return Box::new(move |_, _, _, _| None),
241 };
259 };
242
260
261+ Box::new(
243− Box::new(move |line: usize, _selected: bool, out: &mut String| {
262+ move |line: usize, _selected: bool, first_visual_line: bool, out: &mut String| {
244− let breakpoint = breakpoints
263+ if !first_visual_line {
245− .iter()
264+ return None;
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)
268 }
265 }
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
269− };
278+ };
270
279
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+
271− let sym = if breakpoint.verified { "▲" } else { "⊚" };
293+ let sym = if breakpoint.verified { "▲" } else { "⊚" };
272− write!(out, "{}", sym).unwrap();
294+ write!(out, "{}", sym).unwrap();
273− Some(style)
295+ Some(style)
296+ },
274− })
297+ )
275}
298}
276
299
277pub fn diagnostics_or_breakpoints<'doc>(
300pub fn diagnostics_or_breakpoints<'doc>(
@@ -284,18 +307,22 @@ pub fn diagnostics_or_breakpoints<'doc>(
284 let mut diagnostics = diagnostic(editor, doc, view, theme, is_focused);
307 let mut diagnostics = diagnostic(editor, doc, view, theme, is_focused);
285 let mut breakpoints = breakpoints(editor, doc, view, theme, is_focused);
308 let mut breakpoints = breakpoints(editor, doc, view, theme, is_focused);
286
309
287− Box::new(move |line, selected, out| {
310+ Box::new(move |line, selected, first_visual_line: bool, out| {
311+ breakpoints(line, selected, first_visual_line, out)
288− breakpoints(line, selected, out).or_else(|| diagnostics(line, selected, out))
312+ .or_else(|| diagnostics(line, selected, first_visual_line, out))
289 })
313 })
290}
314}
291
315
292#[cfg(test)]
316#[cfg(test)]
293mod tests {
317mod tests {
318+ use std::sync::Arc;
319+
294 use super::*;
320 use super::*;
295 use crate::document::Document;
321 use crate::document::Document;
296− use crate::editor::{GutterConfig, GutterLineNumbersConfig};
322+ use crate::editor::{Config, GutterConfig, GutterLineNumbersConfig};
297 use crate::graphics::Rect;
323 use crate::graphics::Rect;
298 use crate::DocumentId;
324 use crate::DocumentId;
325+ use arc_swap::ArcSwap;
299 use helix_core::Rope;
326 use helix_core::Rope;
300
327
301 #[test]
328 #[test]
@@ -304,7 +331,11 @@ mod tests {
304 view.area = Rect::new(40, 40, 40, 40);
331 view.area = Rect::new(40, 40, 40, 40);
305
332
306 let rope = Rope::from_str("abc\n\tdef");
333 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+ );
308
339
309 assert_eq!(view.gutters.layout.len(), 5);
340 assert_eq!(view.gutters.layout.len(), 5);
310 assert_eq!(view.gutters.layout[0].width(&view, &doc), 1);
341 assert_eq!(view.gutters.layout[0].width(&view, &doc), 1);
@@ -325,7 +356,11 @@ mod tests {
325 view.area = Rect::new(40, 40, 40, 40);
356 view.area = Rect::new(40, 40, 40, 40);
326
357
327 let rope = Rope::from_str("abc\n\tdef");
358 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+ );
329
364
330 assert_eq!(view.gutters.layout.len(), 1);
365 assert_eq!(view.gutters.layout.len(), 1);
331 assert_eq!(view.gutters.layout[0].width(&view, &doc), 1);
366 assert_eq!(view.gutters.layout[0].width(&view, &doc), 1);
@@ -339,7 +374,11 @@ mod tests {
339 view.area = Rect::new(40, 40, 40, 40);
374 view.area = Rect::new(40, 40, 40, 40);
340
375
341 let rope = Rope::from_str("abc\n\tdef");
376 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+ );
343
382
344 assert_eq!(view.gutters.layout.len(), 2);
383 assert_eq!(view.gutters.layout.len(), 2);
345 assert_eq!(view.gutters.layout[0].width(&view, &doc), 1);
384 assert_eq!(view.gutters.layout[0].width(&view, &doc), 1);
@@ -357,10 +396,18 @@ mod tests {
357 view.area = Rect::new(40, 40, 40, 40);
396 view.area = Rect::new(40, 40, 40, 40);
358
397
359 let rope = Rope::from_str("a\nb");
398 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+ );
361
404
362 let rope = Rope::from_str("a\nb\nc\nd\ne\nf\ng\nh\ni\nj\nk\nl\nm\nn\no\np");
405 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+ );
364
411
365 assert_eq!(view.gutters.layout.len(), 2);
412 assert_eq!(view.gutters.layout.len(), 2);
366 assert_eq!(view.gutters.layout[1].width(&view, &doc_short), 1);
413 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 {
49}
49}
50
50
51pub fn align_view(doc: &Document, view: &mut View, align: Align) {
51pub fn align_view(doc: &Document, view: &mut View, align: Align) {
52− let pos = doc
52+ let doc_text = doc.text().slice(..);
53− .selection(view.id)
53+ let cursor = doc.selection(view.id).primary().cursor(doc_text);
54− .primary()
54+ let viewport = view.inner_area(doc);
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);
55+ let last_line_height = viewport.height.saturating_sub(1);
59
56
60 let relative = match align {
57 let relative = match align {
61 Align::Center => last_line_height / 2,
58 Align::Center => last_line_height / 2,
@@ -63,10 +60,20 @@ pub fn align_view(doc: &Document, view: &mut View, align: Align) {
63 Align::Bottom => last_line_height,
60 Align::Bottom => last_line_height,
64 };
61 };
65
62
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+ );
67}
73}
68
74
69pub use document::Document;
75pub use document::Document;
70pub use editor::Editor;
76pub use editor::Editor;
77+use helix_core::char_idx_at_visual_offset;
71pub use theme::Theme;
78pub use theme::Theme;
72pub use view::View;
79pub use view::View;
helix-view/src/theme.rs+14−1
@@ -314,10 +314,23 @@ impl Theme {
314 &self.scopes
314 &self.scopes
315 }
315 }
316
316
317− pub fn find_scope_index(&self, scope: &str) -> Option<usize> {
317+ pub fn find_scope_index_exact(&self, scope: &str) -> Option<usize> {
318 self.scopes().iter().position(|s| s == scope)
318 self.scopes().iter().position(|s| s == scope)
319 }
319 }
320
320
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+
321 pub fn is_16_color(&self) -> bool {
334 pub fn is_16_color(&self) -> bool {
322 self.styles.iter().all(|(_, style)| {
335 self.styles.iter().all(|(_, style)| {
323 [style.fg, style.bg]
336 [style.fg, style.bg]
helix-view/src/view.rs+519−128
@@ -2,11 +2,13 @@ use crate::{
2 align_view,
2 align_view,
3 editor::{GutterConfig, GutterType},
3 editor::{GutterConfig, GutterType},
4 graphics::Rect,
4 graphics::Rect,
5− Align, Document, DocumentId, ViewId,
5+ Align, Document, DocumentId, Theme, ViewId,
6};
6};
7
7
8use helix_core::{
8use 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,
10};
12};
11
13
12use std::{
14use std::{
@@ -93,10 +95,17 @@ impl JumpList {
93 }
95 }
94}
96}
95
97
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+
96#[derive(Clone)]
105#[derive(Clone)]
97pub struct View {
106pub struct View {
98 pub id: ViewId,
107 pub id: ViewId,
99− pub offset: Position,
108+ pub offset: ViewPosition,
100 pub area: Rect,
109 pub area: Rect,
101 pub doc: DocumentId,
110 pub doc: DocumentId,
102 pub jumps: JumpList,
111 pub jumps: JumpList,
@@ -133,7 +142,11 @@ impl View {
133 Self {
142 Self {
134 id: ViewId::default(),
143 id: ViewId::default(),
135 doc,
144 doc,
136− offset: Position::new(0, 0),
145+ offset: ViewPosition {
146+ anchor: 0,
147+ horizontal_offset: 0,
148+ vertical_offset: 0,
149+ },
137 area: Rect::default(), // will get calculated upon inserting into tree
150 area: Rect::default(), // will get calculated upon inserting into tree
138 jumps: JumpList::new((doc, Selection::point(0))), // TODO: use actual sel
151 jumps: JumpList::new((doc, Selection::point(0))), // TODO: use actual sel
139 docs_access_history: Vec::new(),
152 docs_access_history: Vec::new(),
@@ -159,6 +172,10 @@ impl View {
159 self.area.clip_bottom(1).height.into() // -1 for statusline
172 self.area.clip_bottom(1).height.into() // -1 for statusline
160 }
173 }
161
174
175+ pub fn inner_width(&self, doc: &Document) -> u16 {
176+ self.area.clip_left(self.gutter_offset(doc)).width
177+ }
178+
162 pub fn gutters(&self) -> &[GutterType] {
179 pub fn gutters(&self) -> &[GutterType] {
163 &self.gutters.layout
180 &self.gutters.layout
164 }
181 }
@@ -176,84 +193,120 @@ impl View {
176 &self,
193 &self,
177 doc: &Document,
194 doc: &Document,
178 scrolloff: usize,
195 scrolloff: usize,
179− ) -> Option<(usize, usize)> {
196+ ) -> Option<ViewPosition> {
180− self.offset_coords_to_in_view_center(doc, scrolloff, false)
197+ self.offset_coords_to_in_view_center::<false>(doc, scrolloff)
181 }
198 }
182
199
183− pub fn offset_coords_to_in_view_center(
200+ pub fn offset_coords_to_in_view_center<const CENTERING: bool>(
184 &self,
201 &self,
185 doc: &Document,
202 doc: &Document,
186 scrolloff: usize,
203 scrolloff: usize,
187− centering: bool,
204+ ) -> Option<ViewPosition> {
188− ) -> Option<(usize, usize)> {
189− let cursor = doc
190− .selection(self.id)
191− .primary()
192− .cursor(doc.text().slice(..));
205+ let doc_text = 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);
206+ let viewport = self.inner_area(doc);
198− let last_line = (self.offset.row + inner_area.height as usize).saturating_sub(1);
207+ let vertical_viewport_end = self.offset.vertical_offset + viewport.height as usize;
199− let last_col = self.offset.col + inner_area.width.saturating_sub(1) as usize;
208+ let text_fmt = doc.text_format(viewport.width, None);
200−
209+ let annotations = self.text_annotations(doc, None);
201− let new_offset = |scrolloff: usize| {
210+
202− // - 1 so we have at least one gap in the middle.
211+ // - 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
212+ // a height of 6 with padding of 3 on each side will keep shifting the view back and forth
204− // as we type
213+ // as we type
205− let scrolloff = scrolloff.min(inner_area.height.saturating_sub(1) as usize / 2);
214+ let scrolloff = scrolloff.min(viewport.height.saturating_sub(1) as usize / 2);
206−
215+
207− let row = if line > last_line.saturating_sub(scrolloff) {
216+ let cursor = doc.selection(self.id).primary().cursor(doc_text);
208− // scroll down
217+ let mut offset = self.offset;
209− self.offset.row + line - (last_line.saturating_sub(scrolloff))
218+
210− } else if line < self.offset.row + scrolloff {
219+ let (visual_off, mut at_top) = if cursor >= offset.anchor {
211− // scroll up
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 {
212− line.saturating_sub(scrolloff)
258+ scrolloff as isize
213 } else {
259 } else {
214− self.offset.row
260+ viewport.height as isize - scrolloff as isize
215 };
261 };
262+ (offset.anchor, offset.vertical_offset) =
263+ char_idx_at_visual_offset(doc_text, cursor, -v_off, 0, &text_fmt, &annotations);
264+ }
216
265
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;
217− let col = if col > last_col.saturating_sub(scrolloff) {
284+ if col > last_col.saturating_sub(scrolloff) {
218 // scroll right
285 // scroll right
219− self.offset.col + col - (last_col.saturating_sub(scrolloff))
286+ offset.horizontal_offset += col - (last_col.saturating_sub(scrolloff))
220− } else if col < self.offset.col + scrolloff {
287+ } else if col < offset.horizontal_offset + scrolloff {
221 // scroll left
288 // scroll left
222− col.saturating_sub(scrolloff)
289+ offset.horizontal_offset = col.saturating_sub(scrolloff)
223− } else {
224− self.offset.col
225 };
290 };
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
243 }
291 }
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)
244 }
299 }
245
300
246 pub fn ensure_cursor_in_view(&mut self, doc: &Document, scrolloff: usize) {
301 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) {
302+ if let Some(offset) = self.offset_coords_to_in_view_center::<false>(doc, scrolloff) {
248− self.offset.row = row;
303+ self.offset = offset;
249− self.offset.col = col;
250 }
304 }
251 }
305 }
252
306
253 pub fn ensure_cursor_in_view_center(&mut self, doc: &Document, scrolloff: usize) {
307 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) {
308+ if let Some(offset) = self.offset_coords_to_in_view_center::<true>(doc, scrolloff) {
255− self.offset.row = row;
309+ self.offset = offset;
256− self.offset.col = col;
257 } else {
310 } else {
258 align_view(doc, self, Align::Center);
311 align_view(doc, self, Align::Center);
259 }
312 }
@@ -263,14 +316,51 @@ impl View {
263 self.offset_coords_to_in_view(doc, scrolloff).is_none()
316 self.offset_coords_to_in_view(doc, scrolloff).is_none()
264 }
317 }
265
318
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
267 #[inline]
324 #[inline]
268− pub fn last_line(&self, doc: &Document) -> usize {
325+ pub fn estimate_last_doc_line(&self, doc: &Document) -> usize {
269− std::cmp::min(
326+ let doc_text = doc.text().slice(..);
327+ let line = doc_text.char_to_line(self.offset.anchor.min(doc_text.len_chars()));
270− // Saturating subs to make it inclusive zero indexing.
328+ // Saturating subs to make it inclusive zero indexing.
271− (self.offset.row + self.inner_height()).saturating_sub(1),
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 {
272− doc.text().len_lines().saturating_sub(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,
273− )
358+ );
359+
360+ match pos {
361+ Some((Position { row, .. }, _)) => row.saturating_sub(self.offset.vertical_offset),
362+ None => visual_height.saturating_sub(1),
363+ }
274 }
364 }
275
365
276 /// Translates a document position to an absolute position in the terminal.
366 /// Translates a document position to an absolute position in the terminal.
@@ -282,22 +372,39 @@ impl View {
282 text: RopeSlice,
372 text: RopeSlice,
283 pos: usize,
373 pos: usize,
284 ) -> Option<Position> {
374 ) -> Option<Position> {
285− let line = text.char_to_line(pos);
375+ if pos < self.offset.anchor {
286−
287− if line < self.offset.row || line > self.last_line(doc) {
288 // Line is not visible on screen
376 // Line is not visible on screen
289 return None;
377 return None;
290 }
378 }
291
379
292− let tab_width = doc.tab_width();
380+ let viewport = self.inner_area(doc);
293− // TODO: visual_coords_at_pos also does char_to_line which we ignore, can we reuse the call?
381+ let text_fmt = doc.text_format(viewport.width, None);
294− let Position { col, .. } = visual_coords_at_pos(text, pos, tab_width);
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);
295
401
296− // It is possible for underflow to occur if the buffer length is larger than the terminal width.
402+ Some(pos)
297− let row = line.saturating_sub(self.offset.row);
403+ }
298− let col = col.saturating_sub(self.offset.col);
299
404
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)
301 }
408 }
302
409
303 pub fn text_pos_at_screen_coords(
410 pub fn text_pos_at_screen_coords(
@@ -305,9 +412,10 @@ impl View {
305 doc: &Document,
412 doc: &Document,
306 row: u16,
413 row: u16,
307 column: u16,
414 column: u16,
308− tab_width: usize,
415+ fmt: TextFormat,
416+ annotations: &TextAnnotations,
417+ ignore_virtual_text: bool,
309 ) -> Option<usize> {
418 ) -> Option<usize> {
310− let text = doc.text().slice(..);
311 let inner = self.inner_area(doc);
419 let inner = self.inner_area(doc);
312 // 1 for status
420 // 1 for status
313 if row < inner.top() || row >= inner.bottom() {
421 if row < inner.top() || row >= inner.bottom() {
@@ -318,27 +426,80 @@ impl View {
318 return None;
426 return None;
319 }
427 }
320
428
321− let text_row = (row - inner.y) as usize + self.offset.row;
429+ self.text_pos_at_visual_coords(
322− if text_row > text.len_lines() - 1 {
430+ doc,
323− return Some(text.len_chars());
431+ row - inner.y,
324− }
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(..);
325
449
450+ let text_row = row as usize + self.offset.vertical_offset;
326− let text_col = (column - inner.x) as usize + self.offset.col;
451+ let text_col = column as usize + self.offset.horizontal_offset;
327
452
328− Some(pos_at_visual_coords(
453+ let (char_idx, virt_lines) = char_idx_at_visual_offset(
329 text,
454 text,
330− Position {
455+ self.offset.anchor,
331− row: text_row,
456+ text_row as isize,
332− col: text_col,
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;
333− },
465+ }
334− tab_width,
466+ Some(char_idx)
335− ))
336 }
467 }
337
468
338 /// Translates a screen position to position in the text document.
469 /// Translates a screen position to position in the text document.
339 /// Returns a usize typed position in bounds of the text if found in this view, None if out of view.
470 /// 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> {
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> {
341− self.text_pos_at_screen_coords(doc, row, column, doc.tab_width())
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+ )
342 }
503 }
343
504
344 /// Translates screen coordinates into coordinates on the gutter of the view.
505 /// Translates screen coordinates into coordinates on the gutter of the view.
@@ -419,7 +580,10 @@ impl View {
419
580
420#[cfg(test)]
581#[cfg(test)]
421mod tests {
582mod tests {
583+ use std::sync::Arc;
584+
422 use super::*;
585 use super::*;
586+ use arc_swap::ArcSwap;
423 use helix_core::Rope;
587 use helix_core::Rope;
424
588
425 // 1 diagnostic + 1 spacer + 3 linenr (< 1000 lines) + 1 spacer + 1 diff
589 // 1 diagnostic + 1 spacer + 3 linenr (< 1000 lines) + 1 spacer + 1 diff
@@ -429,52 +593,174 @@ mod tests {
429 const DEFAULT_GUTTER_OFFSET_ONLY_DIAGNOSTICS: u16 = 3;
593 const DEFAULT_GUTTER_OFFSET_ONLY_DIAGNOSTICS: u16 = 3;
430
594
431 use crate::document::Document;
595 use crate::document::Document;
432− use crate::editor::{GutterConfig, GutterLineNumbersConfig, GutterType};
596+ use crate::editor::{Config, GutterConfig, GutterLineNumbersConfig, GutterType};
433
597
434 #[test]
598 #[test]
435 fn test_text_pos_at_screen_coords() {
599 fn test_text_pos_at_screen_coords() {
436 let mut view = View::new(DocumentId::default(), GutterConfig::default());
600 let mut view = View::new(DocumentId::default(), GutterConfig::default());
437 view.area = Rect::new(40, 40, 40, 40);
601 view.area = Rect::new(40, 40, 40, 40);
438 let rope = Rope::from_str("abc\n\tdef");
602 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+ );
440
608
609+ assert_eq!(
441− assert_eq!(view.text_pos_at_screen_coords(&doc, 40, 2, 4), None);
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+ );
442
620
621+ assert_eq!(
443− assert_eq!(view.text_pos_at_screen_coords(&doc, 40, 41, 4), None);
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+ );
444
632
633+ assert_eq!(
445− assert_eq!(view.text_pos_at_screen_coords(&doc, 0, 2, 4), None);
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+ );
446
644
645+ assert_eq!(
447− assert_eq!(view.text_pos_at_screen_coords(&doc, 0, 49, 4), None);
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+ );
448
656
657+ assert_eq!(
449− assert_eq!(view.text_pos_at_screen_coords(&doc, 0, 41, 4), None);
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+ );
450
668
669+ assert_eq!(
451− assert_eq!(view.text_pos_at_screen_coords(&doc, 40, 81, 4), None);
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+ );
452
680
681+ assert_eq!(
453− assert_eq!(view.text_pos_at_screen_coords(&doc, 78, 41, 4), None);
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+ );
454
692
455 assert_eq!(
693 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+ ),
457 Some(3)
702 Some(3)
458 );
703 );
459
704
705+ assert_eq!(
460− assert_eq!(view.text_pos_at_screen_coords(&doc, 40, 80, 4), Some(3));
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+ );
461
716
462 assert_eq!(
717 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+ ),
464 Some(4)
726 Some(4)
465 );
727 );
466
728
467 assert_eq!(
729 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+ ),
469 Some(5)
738 Some(5)
470 );
739 );
471
740
472 assert_eq!(
741 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+ ),
474 Some(8)
750 Some(8)
475 );
751 );
476
752
753+ assert_eq!(
477− assert_eq!(view.text_pos_at_screen_coords(&doc, 41, 80, 4), Some(8));
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+ );
478 }
764 }
479
765
480 #[test]
766 #[test]
@@ -488,13 +774,19 @@ mod tests {
488 );
774 );
489 view.area = Rect::new(40, 40, 40, 40);
775 view.area = Rect::new(40, 40, 40, 40);
490 let rope = Rope::from_str("abc\n\tdef");
776 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+ );
492 assert_eq!(
782 assert_eq!(
493 view.text_pos_at_screen_coords(
783 view.text_pos_at_screen_coords(
494 &doc,
784 &doc,
495 41,
785 41,
496 40 + DEFAULT_GUTTER_OFFSET_ONLY_DIAGNOSTICS + 1,
786 40 + DEFAULT_GUTTER_OFFSET_ONLY_DIAGNOSTICS + 1,
497− 4
787+ TextFormat::default(),
788+ &TextAnnotations::default(),
789+ true
498 ),
790 ),
499 Some(4)
791 Some(4)
500 );
792 );
@@ -511,8 +803,22 @@ mod tests {
511 );
803 );
512 view.area = Rect::new(40, 40, 40, 40);
804 view.area = Rect::new(40, 40, 40, 40);
513 let rope = Rope::from_str("abc\n\tdef");
805 let rope = Rope::from_str("abc\n\tdef");
514− let doc = Document::from(rope, None);
806+ let doc = Document::from(
807+ rope,
808+ None,
809+ Arc::new(ArcSwap::new(Arc::new(Config::default()))),
810+ );
811+ assert_eq!(
515− assert_eq!(view.text_pos_at_screen_coords(&doc, 41, 40 + 1, 4), Some(4));
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+ );
516 }
822 }
517
823
518 #[test]
824 #[test]
@@ -520,34 +826,80 @@ mod tests {
520 let mut view = View::new(DocumentId::default(), GutterConfig::default());
826 let mut view = View::new(DocumentId::default(), GutterConfig::default());
521 view.area = Rect::new(40, 40, 40, 40);
827 view.area = Rect::new(40, 40, 40, 40);
522 let rope = Rope::from_str("Hi! こんにちは皆さん");
828 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+ );
524
834
525 assert_eq!(
835 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+ ),
527 Some(0)
844 Some(0)
528 );
845 );
529
846
530 assert_eq!(
847 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+ ),
532 Some(4)
856 Some(4)
533 );
857 );
534 assert_eq!(
858 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+ ),
536 Some(4)
867 Some(4)
537 );
868 );
538
869
539 assert_eq!(
870 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+ ),
541 Some(5)
879 Some(5)
542 );
880 );
543
881
544 assert_eq!(
882 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+ ),
546 Some(5)
891 Some(5)
547 );
892 );
548
893
549 assert_eq!(
894 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+ ),
551 Some(6)
903 Some(6)
552 );
904 );
553 }
905 }
@@ -557,30 +909,69 @@ mod tests {
557 let mut view = View::new(DocumentId::default(), GutterConfig::default());
909 let mut view = View::new(DocumentId::default(), GutterConfig::default());
558 view.area = Rect::new(40, 40, 40, 40);
910 view.area = Rect::new(40, 40, 40, 40);
559 let rope = Rope::from_str("Hèl̀l̀ò world!");
911 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+ );
561
917
562 assert_eq!(
918 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+ ),
564 Some(0)
927 Some(0)
565 );
928 );
566
929
567 assert_eq!(
930 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+ ),
569 Some(1)
939 Some(1)
570 );
940 );
571
941
572 assert_eq!(
942 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+ ),
574 Some(3)
951 Some(3)
575 );
952 );
576
953
577 assert_eq!(
954 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+ ),
579 Some(5)
963 Some(5)
580 );
964 );
581
965
582 assert_eq!(
966 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+ ),
584 Some(7)
975 Some(7)
585 );
976 );
586 }
977 }