Merge pull request #6417 from pascalkuthe/inline-diagnostics

pull/11182/head
Blaž Hrastnik 4 months ago committed by GitHub
commit 107cdf3e43
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -16,6 +16,7 @@
- [`[editor.gutters.spacer]` Section](#editorguttersspacer-section) - [`[editor.gutters.spacer]` Section](#editorguttersspacer-section)
- [`[editor.soft-wrap]` Section](#editorsoft-wrap-section) - [`[editor.soft-wrap]` Section](#editorsoft-wrap-section)
- [`[editor.smart-tab]` Section](#editorsmart-tab-section) - [`[editor.smart-tab]` Section](#editorsmart-tab-section)
- [`[editor.inline-diagnostics]` Section](#editorinline-diagnostics-section)
### `[editor]` Section ### `[editor]` Section
@ -50,6 +51,7 @@
| `popup-border` | Draw border around `popup`, `menu`, `all`, or `none` | `none` | | `popup-border` | Draw border around `popup`, `menu`, `all`, or `none` | `none` |
| `indent-heuristic` | How the indentation for a newly inserted line is computed: `simple` just copies the indentation level from the previous line, `tree-sitter` computes the indentation based on the syntax tree and `hybrid` combines both approaches. If the chosen heuristic is not available, a different one will be used as a fallback (the fallback order being `hybrid` -> `tree-sitter` -> `simple`). | `hybrid` | `indent-heuristic` | How the indentation for a newly inserted line is computed: `simple` just copies the indentation level from the previous line, `tree-sitter` computes the indentation based on the syntax tree and `hybrid` combines both approaches. If the chosen heuristic is not available, a different one will be used as a fallback (the fallback order being `hybrid` -> `tree-sitter` -> `simple`). | `hybrid`
| `jump-label-alphabet` | The characters that are used to generate two character jump labels. Characters at the start of the alphabet are used first. | `"abcdefghijklmnopqrstuvwxyz"` | `jump-label-alphabet` | The characters that are used to generate two character jump labels. Characters at the start of the alphabet are used first. | `"abcdefghijklmnopqrstuvwxyz"`
| `end-of-line-diagnostics` | Minimum severity of diagnostics to render at the end of the line. Set to `disable` to disable entirely. Refer to the setting about `inline-diagnostics` for more details | "disable"
### `[editor.statusline]` Section ### `[editor.statusline]` Section
@ -393,3 +395,41 @@ S-tab = "move_parent_node_start"
tab = "extend_parent_node_end" tab = "extend_parent_node_end"
S-tab = "extend_parent_node_start" S-tab = "extend_parent_node_start"
``` ```
### `[editor.inline-diagnostics]` Section
Options for rendering diagnostics inside the text like shown below
```
fn main() {
let foo = bar;
└─ no such value in this scope
}
````
| Key | Description | Default |
|------------|-------------|---------|
| `cursor-line` | The minimum severity that a diagnostic must have to be shown inline on the line that contains the primary cursor. Set to `disable` to not show any diagnostics inline. This option does not have any effect when in insert-mode and will only take effect 350ms after moving the cursor to a different line. | `"disable"` |
| `other-lines` | The minimum severity that a diagnostic must have to be shown inline on a line that does not contain the cursor-line. Set to `disable` to not show any diagnostics inline. | `"disable"` |
| `prefix-len` | How many horizontal bars `─` are rendered before the diagnostic text. | `1` |
| `max-wrap` | Equivalent of the `editor.soft-wrap.max-wrap` option for diagnostics. | `20` |
| `max-diagnostics` | Maximum number of diagnostics to render inline for a given line | `10` |
The (first) diagnostic with the highest severity that is not shown inline is rendered at the end of the line (as long as its severity is higher than the `end-of-line-diagnostics` config option):
```
fn main() {
let baz = 1;
let foo = bar; a local variable with a similar name exists: baz
└─ no such value in this scope
}
```
The new diagnostic rendering is not yet enabled by default. As soon as end of line or inline diagnostics are enabled the old diagnostics rendering is automatically disabled. The recommended default setting are:
```
end-of-line-diagnostics = "hint"
[editor.inline-diagnostics]
cursor-line = "warning" # show warnings and errors on the cursorline inline
```

@ -4,7 +4,8 @@ use std::fmt;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
/// Describes the severity level of a [`Diagnostic`]. /// Describes the severity level of a [`Diagnostic`].
#[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord, Deserialize, Serialize)] #[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Severity { pub enum Severity {
Hint, Hint,
Info, Info,
@ -25,6 +26,12 @@ pub struct Range {
pub end: usize, pub end: usize,
} }
impl Range {
pub fn contains(self, pos: usize) -> bool {
(self.start..self.end).contains(&pos)
}
}
#[derive(Debug, Eq, Hash, PartialEq, Clone, Deserialize, Serialize)] #[derive(Debug, Eq, Hash, PartialEq, Clone, Deserialize, Serialize)]
pub enum NumberOrString { pub enum NumberOrString {
Number(i32), Number(i32),
@ -71,3 +78,10 @@ impl fmt::Display for LanguageServerId {
write!(f, "{:?}", self.0) write!(f, "{:?}", self.0)
} }
} }
impl Diagnostic {
#[inline]
pub fn severity(&self) -> Severity {
self.severity.unwrap_or(Severity::Warning)
}
}

@ -10,8 +10,9 @@
//! called a "block" and the caller must advance it as needed. //! called a "block" and the caller must advance it as needed.
use std::borrow::Cow; use std::borrow::Cow;
use std::cmp::Ordering;
use std::fmt::Debug; use std::fmt::Debug;
use std::mem::{replace, take}; use std::mem::replace;
#[cfg(test)] #[cfg(test)]
mod test; mod test;
@ -37,52 +38,104 @@ pub enum GraphemeSource {
}, },
} }
impl GraphemeSource {
/// Returns whether this grapheme is virtual inline text
pub fn is_virtual(self) -> bool {
matches!(self, GraphemeSource::VirtualText { .. })
}
pub fn is_eof(self) -> bool {
// all doc chars except the EOF char have non-zero codepoints
matches!(self, GraphemeSource::Document { codepoints: 0 })
}
pub fn doc_chars(self) -> usize {
match self {
GraphemeSource::Document { codepoints } => codepoints as usize,
GraphemeSource::VirtualText { .. } => 0,
}
}
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct FormattedGrapheme<'a> { pub struct FormattedGrapheme<'a> {
pub grapheme: Grapheme<'a>, pub raw: Grapheme<'a>,
pub source: GraphemeSource, pub source: GraphemeSource,
pub visual_pos: Position,
/// Document line at the start of the grapheme
pub line_idx: usize,
/// Document char position at the start of the grapheme
pub char_idx: usize,
}
impl FormattedGrapheme<'_> {
pub fn is_virtual(&self) -> bool {
self.source.is_virtual()
}
pub fn doc_chars(&self) -> usize {
self.source.doc_chars()
}
pub fn is_whitespace(&self) -> bool {
self.raw.is_whitespace()
}
pub fn width(&self) -> usize {
self.raw.width()
}
pub fn is_word_boundary(&self) -> bool {
self.raw.is_word_boundary()
}
} }
impl<'a> FormattedGrapheme<'a> { #[derive(Debug, Clone)]
pub fn new( struct GraphemeWithSource<'a> {
grapheme: Grapheme<'a>,
source: GraphemeSource,
}
impl<'a> GraphemeWithSource<'a> {
fn new(
g: GraphemeStr<'a>, g: GraphemeStr<'a>,
visual_x: usize, visual_x: usize,
tab_width: u16, tab_width: u16,
source: GraphemeSource, source: GraphemeSource,
) -> FormattedGrapheme<'a> { ) -> GraphemeWithSource<'a> {
FormattedGrapheme { GraphemeWithSource {
grapheme: Grapheme::new(g, visual_x, tab_width), grapheme: Grapheme::new(g, visual_x, tab_width),
source, source,
} }
} }
/// Returns whether this grapheme is virtual inline text fn placeholder() -> Self {
pub fn is_virtual(&self) -> bool { GraphemeWithSource {
matches!(self.source, GraphemeSource::VirtualText { .. })
}
pub fn placeholder() -> Self {
FormattedGrapheme {
grapheme: Grapheme::Other { g: " ".into() }, grapheme: Grapheme::Other { g: " ".into() },
source: GraphemeSource::Document { codepoints: 0 }, source: GraphemeSource::Document { codepoints: 0 },
} }
} }
pub fn doc_chars(&self) -> usize { fn doc_chars(&self) -> usize {
match self.source { self.source.doc_chars()
GraphemeSource::Document { codepoints } => codepoints as usize,
GraphemeSource::VirtualText { .. } => 0,
}
} }
pub fn is_whitespace(&self) -> bool { fn is_whitespace(&self) -> bool {
self.grapheme.is_whitespace() self.grapheme.is_whitespace()
} }
pub fn width(&self) -> usize { fn is_newline(&self) -> bool {
matches!(self.grapheme, Grapheme::Newline)
}
fn is_eof(&self) -> bool {
self.source.is_eof()
}
fn width(&self) -> usize {
self.grapheme.width() self.grapheme.width()
} }
pub fn is_word_boundary(&self) -> bool { fn is_word_boundary(&self) -> bool {
self.grapheme.is_word_boundary() self.grapheme.is_word_boundary()
} }
} }
@ -96,6 +149,7 @@ pub struct TextFormat {
pub wrap_indicator: Box<str>, pub wrap_indicator: Box<str>,
pub wrap_indicator_highlight: Option<Highlight>, pub wrap_indicator_highlight: Option<Highlight>,
pub viewport_width: u16, pub viewport_width: u16,
pub soft_wrap_at_text_width: bool,
} }
// test implementation is basically only used for testing or when softwrap is always disabled // test implementation is basically only used for testing or when softwrap is always disabled
@ -109,6 +163,7 @@ impl Default for TextFormat {
wrap_indicator: Box::from(" "), wrap_indicator: Box::from(" "),
viewport_width: 17, viewport_width: 17,
wrap_indicator_highlight: None, wrap_indicator_highlight: None,
soft_wrap_at_text_width: false,
} }
} }
} }
@ -127,10 +182,7 @@ pub struct DocumentFormatter<'t> {
line_pos: usize, line_pos: usize,
exhausted: bool, exhausted: bool,
/// Line breaks to be reserved for virtual text inline_annotation_graphemes: Option<(Graphemes<'t>, Option<Highlight>)>,
/// at the next line break
virtual_lines: usize,
inline_anntoation_graphemes: Option<(Graphemes<'t>, Option<Highlight>)>,
// softwrap specific // softwrap specific
/// The indentation of the current line /// The indentation of the current line
@ -139,9 +191,9 @@ pub struct DocumentFormatter<'t> {
indent_level: Option<usize>, indent_level: Option<usize>,
/// In case a long word needs to be split a single grapheme might need to be wrapped /// In case a long word needs to be split a single grapheme might need to be wrapped
/// while the rest of the word stays on the same line /// while the rest of the word stays on the same line
peeked_grapheme: Option<(FormattedGrapheme<'t>, usize)>, peeked_grapheme: Option<GraphemeWithSource<'t>>,
/// A first-in first-out (fifo) buffer for the Graphemes of any given word /// A first-in first-out (fifo) buffer for the Graphemes of any given word
word_buf: Vec<FormattedGrapheme<'t>>, word_buf: Vec<GraphemeWithSource<'t>>,
/// The index of the next grapheme that will be yielded from the `word_buf` /// The index of the next grapheme that will be yielded from the `word_buf`
word_i: usize, word_i: usize,
} }
@ -157,35 +209,35 @@ impl<'t> DocumentFormatter<'t> {
text_fmt: &'t TextFormat, text_fmt: &'t TextFormat,
annotations: &'t TextAnnotations, annotations: &'t TextAnnotations,
char_idx: usize, char_idx: usize,
) -> (Self, usize) { ) -> Self {
// TODO divide long lines into blocks to avoid bad performance for long lines // TODO divide long lines into blocks to avoid bad performance for long lines
let block_line_idx = text.char_to_line(char_idx.min(text.len_chars())); let block_line_idx = text.char_to_line(char_idx.min(text.len_chars()));
let block_char_idx = text.line_to_char(block_line_idx); let block_char_idx = text.line_to_char(block_line_idx);
annotations.reset_pos(block_char_idx); annotations.reset_pos(block_char_idx);
(
DocumentFormatter { DocumentFormatter {
text_fmt, text_fmt,
annotations, annotations,
visual_pos: Position { row: 0, col: 0 }, visual_pos: Position { row: 0, col: 0 },
graphemes: RopeGraphemes::new(text.slice(block_char_idx..)), graphemes: RopeGraphemes::new(text.slice(block_char_idx..)),
char_pos: block_char_idx, char_pos: block_char_idx,
exhausted: false, exhausted: false,
virtual_lines: 0, indent_level: None,
indent_level: None, peeked_grapheme: None,
peeked_grapheme: None, word_buf: Vec::with_capacity(64),
word_buf: Vec::with_capacity(64), word_i: 0,
word_i: 0, line_pos: block_line_idx,
line_pos: block_line_idx, inline_annotation_graphemes: None,
inline_anntoation_graphemes: None, }
},
block_char_idx,
)
} }
fn next_inline_annotation_grapheme(&mut self) -> Option<(&'t str, Option<Highlight>)> { fn next_inline_annotation_grapheme(
&mut self,
char_pos: usize,
) -> Option<(&'t str, Option<Highlight>)> {
loop { loop {
if let Some(&mut (ref mut annotation, highlight)) = if let Some(&mut (ref mut annotation, highlight)) =
self.inline_anntoation_graphemes.as_mut() self.inline_annotation_graphemes.as_mut()
{ {
if let Some(grapheme) = annotation.next() { if let Some(grapheme) = annotation.next() {
return Some((grapheme, highlight)); return Some((grapheme, highlight));
@ -193,9 +245,9 @@ impl<'t> DocumentFormatter<'t> {
} }
if let Some((annotation, highlight)) = if let Some((annotation, highlight)) =
self.annotations.next_inline_annotation_at(self.char_pos) self.annotations.next_inline_annotation_at(char_pos)
{ {
self.inline_anntoation_graphemes = Some(( self.inline_annotation_graphemes = Some((
UnicodeSegmentation::graphemes(&*annotation.text, true), UnicodeSegmentation::graphemes(&*annotation.text, true),
highlight, highlight,
)) ))
@ -205,21 +257,19 @@ impl<'t> DocumentFormatter<'t> {
} }
} }
fn advance_grapheme(&mut self, col: usize) -> Option<FormattedGrapheme<'t>> { fn advance_grapheme(&mut self, col: usize, char_pos: usize) -> Option<GraphemeWithSource<'t>> {
let (grapheme, source) = let (grapheme, source) =
if let Some((grapheme, highlight)) = self.next_inline_annotation_grapheme() { if let Some((grapheme, highlight)) = self.next_inline_annotation_grapheme(char_pos) {
(grapheme.into(), GraphemeSource::VirtualText { highlight }) (grapheme.into(), GraphemeSource::VirtualText { highlight })
} else if let Some(grapheme) = self.graphemes.next() { } else if let Some(grapheme) = self.graphemes.next() {
self.virtual_lines += self.annotations.annotation_lines_at(self.char_pos);
let codepoints = grapheme.len_chars() as u32; let codepoints = grapheme.len_chars() as u32;
let overlay = self.annotations.overlay_at(self.char_pos); let overlay = self.annotations.overlay_at(char_pos);
let grapheme = match overlay { let grapheme = match overlay {
Some((overlay, _)) => overlay.grapheme.as_str().into(), Some((overlay, _)) => overlay.grapheme.as_str().into(),
None => Cow::from(grapheme).into(), None => Cow::from(grapheme).into(),
}; };
self.char_pos += codepoints as usize;
(grapheme, GraphemeSource::Document { codepoints }) (grapheme, GraphemeSource::Document { codepoints })
} else { } else {
if self.exhausted { if self.exhausted {
@ -228,19 +278,19 @@ impl<'t> DocumentFormatter<'t> {
self.exhausted = true; self.exhausted = true;
// EOF grapheme is required for rendering // EOF grapheme is required for rendering
// and correct position computations // and correct position computations
return Some(FormattedGrapheme { return Some(GraphemeWithSource {
grapheme: Grapheme::Other { g: " ".into() }, grapheme: Grapheme::Other { g: " ".into() },
source: GraphemeSource::Document { codepoints: 0 }, source: GraphemeSource::Document { codepoints: 0 },
}); });
}; };
let grapheme = FormattedGrapheme::new(grapheme, col, self.text_fmt.tab_width, source); let grapheme = GraphemeWithSource::new(grapheme, col, self.text_fmt.tab_width, source);
Some(grapheme) Some(grapheme)
} }
/// Move a word to the next visual line /// Move a word to the next visual line
fn wrap_word(&mut self, virtual_lines_before_word: usize) -> usize { fn wrap_word(&mut self) -> usize {
// softwrap this word to the next line // softwrap this word to the next line
let indent_carry_over = if let Some(indent) = self.indent_level { let indent_carry_over = if let Some(indent) = self.indent_level {
if indent as u16 <= self.text_fmt.max_indent_retain { if indent as u16 <= self.text_fmt.max_indent_retain {
@ -254,15 +304,17 @@ impl<'t> DocumentFormatter<'t> {
0 0
}; };
let virtual_lines =
self.annotations
.virtual_lines_at(self.char_pos, self.visual_pos, self.line_pos);
self.visual_pos.col = indent_carry_over as usize; self.visual_pos.col = indent_carry_over as usize;
self.virtual_lines -= virtual_lines_before_word; self.visual_pos.row += 1 + virtual_lines;
self.visual_pos.row += 1 + virtual_lines_before_word;
let mut i = 0; let mut i = 0;
let mut word_width = 0; let mut word_width = 0;
let wrap_indicator = UnicodeSegmentation::graphemes(&*self.text_fmt.wrap_indicator, true) let wrap_indicator = UnicodeSegmentation::graphemes(&*self.text_fmt.wrap_indicator, true)
.map(|g| { .map(|g| {
i += 1; i += 1;
let grapheme = FormattedGrapheme::new( let grapheme = GraphemeWithSource::new(
g.into(), g.into(),
self.visual_pos.col + word_width, self.visual_pos.col + word_width,
self.text_fmt.tab_width, self.text_fmt.tab_width,
@ -282,46 +334,71 @@ impl<'t> DocumentFormatter<'t> {
.change_position(visual_x, self.text_fmt.tab_width); .change_position(visual_x, self.text_fmt.tab_width);
word_width += grapheme.width(); word_width += grapheme.width();
} }
if let Some(grapheme) = &mut self.peeked_grapheme {
let visual_x = self.visual_pos.col + word_width;
grapheme
.grapheme
.change_position(visual_x, self.text_fmt.tab_width);
}
word_width word_width
} }
fn peek_grapheme(&mut self, col: usize, char_pos: usize) -> Option<&GraphemeWithSource<'t>> {
if self.peeked_grapheme.is_none() {
self.peeked_grapheme = self.advance_grapheme(col, char_pos);
}
self.peeked_grapheme.as_ref()
}
fn next_grapheme(&mut self, col: usize, char_pos: usize) -> Option<GraphemeWithSource<'t>> {
self.peek_grapheme(col, char_pos);
self.peeked_grapheme.take()
}
fn advance_to_next_word(&mut self) { fn advance_to_next_word(&mut self) {
self.word_buf.clear(); self.word_buf.clear();
let mut word_width = 0; let mut word_width = 0;
let virtual_lines_before_word = self.virtual_lines; let mut word_chars = 0;
let mut virtual_lines_before_grapheme = self.virtual_lines;
if self.exhausted {
return;
}
loop { loop {
// softwrap word if necessary let mut col = self.visual_pos.col + word_width;
if word_width + self.visual_pos.col >= self.text_fmt.viewport_width as usize { let char_pos = self.char_pos + word_chars;
// wrapping this word would move too much text to the next line match col.cmp(&(self.text_fmt.viewport_width as usize)) {
// split the word at the line end instead // The EOF char and newline chars are always selectable in helix. That means
if word_width > self.text_fmt.max_wrap as usize { // that wrapping happens "too-early" if a word fits a line perfectly. This
// Usually we stop accomulating graphemes as soon as softwrapping becomes necessary. // is intentional so that all selectable graphemes are always visisble (and
// However if the last grapheme is multiple columns wide it might extend beyond the EOL. // therefore the cursor never dissapears). However if the user manually set a
// The condition below ensures that this grapheme is not cutoff and instead wrapped to the next line // lower softwrap width then this is undesirable. Just increasing the viewport-
if word_width + self.visual_pos.col > self.text_fmt.viewport_width as usize { // width by one doesn't work because if a line is wrapped multiple times then
self.peeked_grapheme = self.word_buf.pop().map(|grapheme| { // some words may extend past the specified width.
(grapheme, self.virtual_lines - virtual_lines_before_grapheme) //
}); // So we special case a word that ends exactly at line bounds and is followed
self.virtual_lines = virtual_lines_before_grapheme; // by a newline/eof character here.
} Ordering::Equal
if self.text_fmt.soft_wrap_at_text_width
&& self.peek_grapheme(col, char_pos).map_or(false, |grapheme| {
grapheme.is_newline() || grapheme.is_eof()
}) => {}
Ordering::Equal if word_width > self.text_fmt.max_wrap as usize => return,
Ordering::Greater if word_width > self.text_fmt.max_wrap as usize => {
self.peeked_grapheme = self.word_buf.pop();
return; return;
} }
Ordering::Equal | Ordering::Greater => {
word_width = self.wrap_word(virtual_lines_before_word); word_width = self.wrap_word();
col = self.visual_pos.col + word_width;
}
Ordering::Less => (),
} }
virtual_lines_before_grapheme = self.virtual_lines; let Some(grapheme) = self.next_grapheme(col, char_pos) else {
let grapheme = if let Some((grapheme, virtual_lines)) = self.peeked_grapheme.take() {
self.virtual_lines += virtual_lines;
grapheme
} else if let Some(grapheme) = self.advance_grapheme(self.visual_pos.col + word_width) {
grapheme
} else {
return; return;
}; };
word_chars += grapheme.doc_chars();
// Track indentation // Track indentation
if !grapheme.is_whitespace() && self.indent_level.is_none() { if !grapheme.is_whitespace() && self.indent_level.is_none() {
@ -340,19 +417,18 @@ impl<'t> DocumentFormatter<'t> {
} }
} }
/// returns the document line pos of the **next** grapheme that will be yielded /// returns the char index at the end of the last yielded grapheme
pub fn line_pos(&self) -> usize { pub fn next_char_pos(&self) -> usize {
self.line_pos self.char_pos
} }
/// returns the visual position at the end of the last yielded grapheme
/// returns the visual pos of the **next** grapheme that will be yielded pub fn next_visual_pos(&self) -> Position {
pub fn visual_pos(&self) -> Position {
self.visual_pos self.visual_pos
} }
} }
impl<'t> Iterator for DocumentFormatter<'t> { impl<'t> Iterator for DocumentFormatter<'t> {
type Item = (FormattedGrapheme<'t>, Position); type Item = FormattedGrapheme<'t>;
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
let grapheme = if self.text_fmt.soft_wrap { let grapheme = if self.text_fmt.soft_wrap {
@ -362,23 +438,40 @@ impl<'t> Iterator for DocumentFormatter<'t> {
} }
let grapheme = replace( let grapheme = replace(
self.word_buf.get_mut(self.word_i)?, self.word_buf.get_mut(self.word_i)?,
FormattedGrapheme::placeholder(), GraphemeWithSource::placeholder(),
); );
self.word_i += 1; self.word_i += 1;
grapheme grapheme
} else { } else {
self.advance_grapheme(self.visual_pos.col)? self.advance_grapheme(self.visual_pos.col, self.char_pos)?
};
let grapheme = FormattedGrapheme {
raw: grapheme.grapheme,
source: grapheme.source,
visual_pos: self.visual_pos,
line_idx: self.line_pos,
char_idx: self.char_pos,
}; };
let pos = self.visual_pos; self.char_pos += grapheme.doc_chars();
if grapheme.grapheme == Grapheme::Newline { if !grapheme.is_virtual() {
self.visual_pos.row += 1; self.annotations.process_virtual_text_anchors(&grapheme);
self.visual_pos.row += take(&mut self.virtual_lines); }
if grapheme.raw == Grapheme::Newline {
// move to end of newline char
self.visual_pos.col += 1;
let virtual_lines =
self.annotations
.virtual_lines_at(self.char_pos, self.visual_pos, self.line_pos);
self.visual_pos.row += 1 + virtual_lines;
self.visual_pos.col = 0; self.visual_pos.col = 0;
self.line_pos += 1; if !grapheme.is_virtual() {
self.line_pos += 1;
}
} else { } else {
self.visual_pos.col += grapheme.width(); self.visual_pos.col += grapheme.width();
} }
Some((grapheme, pos)) Some(grapheme)
} }
} }

@ -12,6 +12,7 @@ impl TextFormat {
wrap_indicator_highlight: None, wrap_indicator_highlight: None,
// use a prime number to allow lining up too often with repeat // use a prime number to allow lining up too often with repeat
viewport_width: 17, viewport_width: 17,
soft_wrap_at_text_width: false,
} }
} }
} }
@ -21,20 +22,23 @@ impl<'t> DocumentFormatter<'t> {
use std::fmt::Write; use std::fmt::Write;
let mut res = String::new(); let mut res = String::new();
let viewport_width = self.text_fmt.viewport_width; let viewport_width = self.text_fmt.viewport_width;
let soft_wrap_at_text_width = self.text_fmt.soft_wrap_at_text_width;
let mut line = 0; let mut line = 0;
for (grapheme, pos) in self { for grapheme in self {
if pos.row != line { if grapheme.visual_pos.row != line {
line += 1; line += 1;
assert_eq!(pos.row, line); assert_eq!(grapheme.visual_pos.row, line);
write!(res, "\n{}", ".".repeat(pos.col)).unwrap(); write!(res, "\n{}", ".".repeat(grapheme.visual_pos.col)).unwrap();
}
if !soft_wrap_at_text_width {
assert!( assert!(
pos.col <= viewport_width as usize, grapheme.visual_pos.col <= viewport_width as usize,
"softwrapped failed {}<={viewport_width}", "softwrapped failed {}<={viewport_width}",
pos.col grapheme.visual_pos.col
); );
} }
write!(res, "{}", grapheme.grapheme).unwrap(); write!(res, "{}", grapheme.raw).unwrap();
} }
res res
@ -48,7 +52,6 @@ fn softwrap_text(text: &str) -> String {
&TextAnnotations::default(), &TextAnnotations::default(),
0, 0,
) )
.0
.collect_to_str() .collect_to_str()
} }
@ -99,6 +102,22 @@ fn long_word_softwrap() {
); );
} }
fn softwrap_text_at_text_width(text: &str) -> String {
let mut text_fmt = TextFormat::new_test(true);
text_fmt.soft_wrap_at_text_width = true;
let annotations = TextAnnotations::default();
let mut formatter =
DocumentFormatter::new_at_prev_checkpoint(text.into(), &text_fmt, &annotations, 0);
formatter.collect_to_str()
}
#[test]
fn long_word_softwrap_text_width() {
assert_eq!(
softwrap_text_at_text_width("xxxxxxxx1xxxx2xxx\nxxxxxxxx1xxxx2xxx"),
"xxxxxxxx1xxxx2xxx \nxxxxxxxx1xxxx2xxx "
);
}
fn overlay_text(text: &str, char_pos: usize, softwrap: bool, overlays: &[Overlay]) -> String { fn overlay_text(text: &str, char_pos: usize, softwrap: bool, overlays: &[Overlay]) -> String {
DocumentFormatter::new_at_prev_checkpoint( DocumentFormatter::new_at_prev_checkpoint(
text.into(), text.into(),
@ -106,7 +125,6 @@ fn overlay_text(text: &str, char_pos: usize, softwrap: bool, overlays: &[Overlay
TextAnnotations::default().add_overlay(overlays, None), TextAnnotations::default().add_overlay(overlays, None),
char_pos, char_pos,
) )
.0
.collect_to_str() .collect_to_str()
} }
@ -143,7 +161,6 @@ fn annotate_text(text: &str, softwrap: bool, annotations: &[InlineAnnotation]) -
TextAnnotations::default().add_inline_annotations(annotations, None), TextAnnotations::default().add_inline_annotations(annotations, None),
0, 0,
) )
.0
.collect_to_str() .collect_to_str()
} }
@ -182,7 +199,6 @@ fn annotation_and_overlay() {
.add_overlay(overlay.as_slice(), None), .add_overlay(overlay.as_slice(), None),
0, 0,
) )
.0
.collect_to_str(), .collect_to_str(),
"fooo bar " "fooo bar "
); );

@ -28,6 +28,11 @@ pub enum Grapheme<'a> {
} }
impl<'a> Grapheme<'a> { impl<'a> Grapheme<'a> {
pub fn new_decoration(g: &'static str) -> Grapheme<'a> {
assert_ne!(g, "\t");
Grapheme::new(g.into(), 0, 0)
}
pub fn new(g: GraphemeStr<'a>, visual_x: usize, tab_width: u16) -> Grapheme<'a> { pub fn new(g: GraphemeStr<'a>, visual_x: usize, tab_width: u16) -> Grapheme<'a> {
match g { match g {
g if g == "\t" => Grapheme::Tab { g if g == "\t" => Grapheme::Tab {

@ -53,8 +53,8 @@ pub use {regex, tree_sitter};
pub use graphemes::RopeGraphemes; pub use graphemes::RopeGraphemes;
pub use position::{ pub use position::{
char_idx_at_visual_offset, coords_at_pos, pos_at_coords, visual_offset_from_anchor, char_idx_at_visual_offset, coords_at_pos, pos_at_coords, softwrapped_dimensions,
visual_offset_from_block, Position, VisualOffsetError, visual_offset_from_anchor, visual_offset_from_block, Position, VisualOffsetError,
}; };
#[allow(deprecated)] #[allow(deprecated)]
pub use position::{pos_at_visual_coords, visual_coords_at_pos}; pub use position::{pos_at_visual_coords, visual_coords_at_pos};

@ -79,19 +79,19 @@ pub fn move_vertically_visual(
Direction::Backward => -(count as isize), Direction::Backward => -(count as isize),
}; };
// TODO how to handle inline annotations that span an entire visual line (very unlikely).
// Compute visual offset relative to block start to avoid trasversing the block twice // Compute visual offset relative to block start to avoid trasversing the block twice
row_off += visual_pos.row as isize; row_off += visual_pos.row as isize;
let new_pos = char_idx_at_visual_offset( let (mut new_pos, virtual_rows) = char_idx_at_visual_offset(
slice, slice,
block_off, block_off,
row_off, row_off,
new_col as usize, new_col as usize,
text_fmt, text_fmt,
annotations, annotations,
) );
.0; if dir == Direction::Forward {
new_pos += (virtual_rows != 0) as usize;
}
// Special-case to avoid moving to the end of the last non-empty line. // Special-case to avoid moving to the end of the last non-empty line.
if behaviour == Movement::Extend && slice.line(slice.char_to_line(new_pos)).len_chars() == 0 { if behaviour == Movement::Extend && slice.line(slice.char_to_line(new_pos)).len_chars() == 0 {

@ -1,4 +1,8 @@
use std::{borrow::Cow, cmp::Ordering}; use std::{
borrow::Cow,
cmp::Ordering,
ops::{Add, AddAssign, Sub, SubAssign},
};
use crate::{ use crate::{
chars::char_is_line_ending, chars::char_is_line_ending,
@ -16,6 +20,38 @@ pub struct Position {
pub col: usize, pub col: usize,
} }
impl AddAssign for Position {
fn add_assign(&mut self, rhs: Self) {
self.row += rhs.row;
self.col += rhs.col;
}
}
impl SubAssign for Position {
fn sub_assign(&mut self, rhs: Self) {
self.row -= rhs.row;
self.col -= rhs.col;
}
}
impl Sub for Position {
type Output = Position;
fn sub(mut self, rhs: Self) -> Self::Output {
self -= rhs;
self
}
}
impl Add for Position {
type Output = Position;
fn add(mut self, rhs: Self) -> Self::Output {
self += rhs;
self
}
}
impl Position { impl Position {
pub const fn new(row: usize, col: usize) -> Self { pub const fn new(row: usize, col: usize) -> Self {
Self { row, col } Self { row, col }
@ -121,22 +157,31 @@ pub fn visual_offset_from_block(
annotations: &TextAnnotations, annotations: &TextAnnotations,
) -> (Position, usize) { ) -> (Position, usize) {
let mut last_pos = Position::default(); let mut last_pos = Position::default();
let (formatter, block_start) = let mut formatter =
DocumentFormatter::new_at_prev_checkpoint(text, text_fmt, annotations, anchor); DocumentFormatter::new_at_prev_checkpoint(text, text_fmt, annotations, anchor);
let mut char_pos = block_start; let block_start = formatter.next_char_pos();
for (grapheme, vpos) in formatter { while let Some(grapheme) = formatter.next() {
last_pos = vpos; last_pos = grapheme.visual_pos;
char_pos += grapheme.doc_chars(); if formatter.next_char_pos() > pos {
return (grapheme.visual_pos, block_start);
if char_pos > pos {
return (last_pos, block_start);
} }
} }
(last_pos, block_start) (last_pos, block_start)
} }
/// Returns the height of the given text when softwrapping
pub fn softwrapped_dimensions(text: RopeSlice, text_fmt: &TextFormat) -> (usize, u16) {
let last_pos =
visual_offset_from_block(text, 0, usize::MAX, text_fmt, &TextAnnotations::default()).0;
if last_pos.row == 0 {
(1, last_pos.col as u16)
} else {
(last_pos.row + 1, text_fmt.viewport_width)
}
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)] #[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum VisualOffsetError { pub enum VisualOffsetError {
PosBeforeAnchorRow, PosBeforeAnchorRow,
@ -153,22 +198,21 @@ pub fn visual_offset_from_anchor(
annotations: &TextAnnotations, annotations: &TextAnnotations,
max_rows: usize, max_rows: usize,
) -> Result<(Position, usize), VisualOffsetError> { ) -> Result<(Position, usize), VisualOffsetError> {
let (formatter, block_start) = let mut formatter =
DocumentFormatter::new_at_prev_checkpoint(text, text_fmt, annotations, anchor); DocumentFormatter::new_at_prev_checkpoint(text, text_fmt, annotations, anchor);
let mut char_pos = block_start;
let mut anchor_line = None; let mut anchor_line = None;
let mut found_pos = None; let mut found_pos = None;
let mut last_pos = Position::default(); let mut last_pos = Position::default();
let block_start = formatter.next_char_pos();
if pos < block_start { if pos < block_start {
return Err(VisualOffsetError::PosBeforeAnchorRow); return Err(VisualOffsetError::PosBeforeAnchorRow);
} }
for (grapheme, vpos) in formatter { while let Some(grapheme) = formatter.next() {
last_pos = vpos; last_pos = grapheme.visual_pos;
char_pos += grapheme.doc_chars();
if char_pos > pos { if formatter.next_char_pos() > pos {
if let Some(anchor_line) = anchor_line { if let Some(anchor_line) = anchor_line {
last_pos.row -= anchor_line; last_pos.row -= anchor_line;
return Ok((last_pos, block_start)); return Ok((last_pos, block_start));
@ -176,7 +220,7 @@ pub fn visual_offset_from_anchor(
found_pos = Some(last_pos); found_pos = Some(last_pos);
} }
} }
if char_pos > anchor && anchor_line.is_none() { if formatter.next_char_pos() > anchor && anchor_line.is_none() {
if let Some(mut found_pos) = found_pos { if let Some(mut found_pos) = found_pos {
return if found_pos.row == last_pos.row { return if found_pos.row == last_pos.row {
found_pos.row = 0; found_pos.row = 0;
@ -190,7 +234,7 @@ pub fn visual_offset_from_anchor(
} }
if let Some(anchor_line) = anchor_line { if let Some(anchor_line) = anchor_line {
if vpos.row >= anchor_line + max_rows { if grapheme.visual_pos.row >= anchor_line + max_rows {
return Err(VisualOffsetError::PosAfterMaxRow); return Err(VisualOffsetError::PosAfterMaxRow);
} }
} }
@ -368,39 +412,43 @@ pub fn char_idx_at_visual_block_offset(
text_fmt: &TextFormat, text_fmt: &TextFormat,
annotations: &TextAnnotations, annotations: &TextAnnotations,
) -> (usize, usize) { ) -> (usize, usize) {
let (formatter, mut char_idx) = let mut formatter =
DocumentFormatter::new_at_prev_checkpoint(text, text_fmt, annotations, anchor); DocumentFormatter::new_at_prev_checkpoint(text, text_fmt, annotations, anchor);
let mut last_char_idx = char_idx; let mut last_char_idx = formatter.next_char_pos();
let mut last_char_idx_on_line = None; let mut found_non_virtual_on_row = false;
let mut last_row = 0; let mut last_row = 0;
for (grapheme, grapheme_pos) in formatter { for grapheme in &mut formatter {
match grapheme_pos.row.cmp(&row) { match grapheme.visual_pos.row.cmp(&row) {
Ordering::Equal => { Ordering::Equal => {
if grapheme_pos.col + grapheme.width() > column { if grapheme.visual_pos.col + grapheme.width() > column {
if !grapheme.is_virtual() { if !grapheme.is_virtual() {
return (char_idx, 0); return (grapheme.char_idx, 0);
} else if let Some(char_idx) = last_char_idx_on_line { } else if found_non_virtual_on_row {
return (char_idx, 0); return (last_char_idx, 0);
} }
} else if !grapheme.is_virtual() { } else if !grapheme.is_virtual() {
last_char_idx_on_line = Some(char_idx) found_non_virtual_on_row = true;
last_char_idx = grapheme.char_idx;
} }
} }
Ordering::Greater if found_non_virtual_on_row => return (last_char_idx, 0),
Ordering::Greater => return (last_char_idx, row - last_row), Ordering::Greater => return (last_char_idx, row - last_row),
_ => (), Ordering::Less => {
if !grapheme.is_virtual() {
last_row = grapheme.visual_pos.row;
last_char_idx = grapheme.char_idx;
}
}
} }
last_char_idx = char_idx;
last_row = grapheme_pos.row;
char_idx += grapheme.doc_chars();
} }
(char_idx, 0) (formatter.next_char_pos(), 0)
} }
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;
use crate::text_annotations::InlineAnnotation;
use crate::Rope; use crate::Rope;
#[test] #[test]
@ -761,6 +809,30 @@ mod test {
assert_eq!(pos_at_visual_coords(slice, (10, 10).into(), 4), 0); assert_eq!(pos_at_visual_coords(slice, (10, 10).into(), 4), 0);
} }
#[test]
fn test_char_idx_at_visual_row_offset_inline_annotation() {
let text = Rope::from("foo\nbar");
let slice = text.slice(..);
let mut text_fmt = TextFormat::default();
let annotations = [InlineAnnotation {
text: "x".repeat(100).into(),
char_idx: 3,
}];
text_fmt.soft_wrap = true;
assert_eq!(
char_idx_at_visual_offset(
slice,
0,
1,
0,
&text_fmt,
TextAnnotations::default().add_inline_annotations(&annotations, None)
),
(2, 1)
);
}
#[test] #[test]
fn test_char_idx_at_visual_row_offset() { fn test_char_idx_at_visual_row_offset() {
let text = Rope::from("ḧëḷḷö\nẅöṛḷḋ\nfoo"); let text = Rope::from("ḧëḷḷö\nẅöṛḷḋ\nfoo");

@ -1,8 +1,12 @@
use std::cell::Cell; use std::cell::Cell;
use std::cmp::Ordering;
use std::fmt::Debug;
use std::ops::Range; use std::ops::Range;
use std::ptr::NonNull;
use crate::doc_formatter::FormattedGrapheme;
use crate::syntax::Highlight; use crate::syntax::Highlight;
use crate::Tendril; use crate::{Position, Tendril};
/// An inline annotation is continuous text shown /// An inline annotation is continuous text shown
/// on the screen before the grapheme that starts at /// on the screen before the grapheme that starts at
@ -75,19 +79,98 @@ impl Overlay {
} }
} }
/// Line annotations allow for virtual text between normal /// Line annotations allow inserting virtual text lines between normal text
/// text lines. They cause `height` empty lines to be inserted /// lines. These lines can be filled with text in the rendering code as their
/// below the document line that contains `anchor_char_idx`. /// contents have no effect beyond visual appearance.
/// ///
/// These lines can be filled with text in the rendering code /// The height of virtual text is usually not known ahead of time as virtual
/// as their contents have no effect beyond visual appearance. /// text often requires softwrapping. Furthermore the height of some virtual
/// text like side-by-side diffs depends on the height of the text (again
/// influenced by softwrap) and other virtual text. Therefore line annotations
/// are computed on the fly instead of ahead of time like other annotations.
/// ///
/// To insert a line after a document line simply set /// The core of this trait `insert_virtual_lines` function. It is called at the
/// `anchor_char_idx` to `doc.line_to_char(line_idx)` /// end of every visual line and allows the `LineAnnotation` to insert empty
#[derive(Debug, Clone)] /// virtual lines. Apart from that the `LineAnnotation` trait has multiple
pub struct LineAnnotation { /// methods that allow it to track anchors in the document.
pub anchor_char_idx: usize, ///
pub height: usize, /// When a new traversal of a document starts `reset_pos` is called. Afterwards
/// the other functions are called with indices that are larger then the
/// one passed to `reset_pos`. This allows performing a binary search (use
/// `partition_point`) in `reset_pos` once and then to only look at the next
/// anchor during each method call.
///
/// The `reset_pos`, `skip_conceal` and `process_anchor` functions all return a
/// `char_idx` anchor. This anchor is stored when transversing the document and
/// when the grapheme at the anchor is traversed the `process_anchor` function
/// is called.
///
/// # Note
///
/// All functions only receive immutable references to `self`.
/// `LineAnnotation`s that want to store an internal position or
/// state of some kind should use `Cell`. Using interior mutability for
/// caches is preferable as otherwise a lot of lifetimes become invariant
/// which complicates APIs a lot.
pub trait LineAnnotation {
/// Resets the internal position to `char_idx`. This function is called
/// when a new traversal of a document starts.
///
/// All `char_idx` passed to `insert_virtual_lines` are strictly monotonically increasing
/// with the first `char_idx` greater or equal to the `char_idx`
/// passed to this function.
///
/// # Returns
///
/// The `char_idx` of the next anchor this `LineAnnotation` is interested in,
/// replaces the currently registered anchor. Return `usize::MAX` to ignore
fn reset_pos(&mut self, _char_idx: usize) -> usize {
usize::MAX
}
/// Called when a text is concealed that contains an anchor registered by this `LineAnnotation`.
/// In this case the line decorations **must** ensure that virtual text anchored within that
/// char range is skipped.
///
/// # Returns
///
/// The `char_idx` of the next anchor this `LineAnnotation` is interested in,
/// **after the end of conceal_end_char_idx**
/// replaces the currently registered anchor. Return `usize::MAX` to ignore
fn skip_concealed_anchors(&mut self, conceal_end_char_idx: usize) -> usize {
self.reset_pos(conceal_end_char_idx)
}
/// Process an anchor (horizontal position is provided) and returns the next anchor.
///
/// # Returns
///
/// The `char_idx` of the next anchor this `LineAnnotation` is interested in,
/// replaces the currently registered anchor. Return `usize::MAX` to ignore
fn process_anchor(&mut self, _grapheme: &FormattedGrapheme) -> usize {
usize::MAX
}
/// This function is called at the end of a visual line to insert virtual text
///
/// # Returns
///
/// The number of additional virtual lines to reserve
///
/// # Note
///
/// The `line_end_visual_pos` parameter indicates the visual vertical distance
/// from the start of block where the traversal starts. This includes the offset
/// from other `LineAnnotations`. This allows inline annotations to consider
/// the height of the text and "align" two different documents (like for side
/// by side diffs). These annotations that want to "align" two documents should
/// therefore be added last so that other virtual text is also considered while aligning
fn insert_virtual_lines(
&mut self,
line_end_char_idx: usize,
line_end_visual_pos: Position,
doc_line: usize,
) -> Position;
} }
#[derive(Debug)] #[derive(Debug)]
@ -143,13 +226,68 @@ fn reset_pos<A, M>(layers: &[Layer<A, M>], pos: usize, get_pos: impl Fn(&A) -> u
} }
} }
/// Safety: We store LineAnnotation in a NonNull pointer. This is necessary to work
/// around an unfortunate inconsistency in rusts variance system that unnnecesarily
/// makes the lifetime invariant if implemented with safe code. This makes the
/// DocFormatter API very cumbersome/basically impossible to work with.
///
/// Normally object types `dyn Foo + 'a` are covariant so if we used `Box<dyn LineAnnotation + 'a>` below
/// everything would be alright. However we want to use `Cell<Box<dyn LineAnnotation + 'a>>`
/// to be able to call the mutable function on `LineAnnotation`. The problem is that
/// some types like `Cell` make all their arguments invariant. This is important for soundness
/// normally for the same reasons that `&'a mut T` is invariant over `T`
/// (see <https://doc.rust-lang.org/nomicon/subtyping.html>). However for `&'a mut` (`dyn Foo + 'b`)
/// there is a specical rule in the language to make `'b` covariant (otherwise trait objects would be
/// super annoying to use). See <https://users.rust-lang.org/t/solved-variance-of-dyn-trait-a> for
/// why this is sound. Sadly that rule doesn't apply to `Cell<Box<(dyn Foo + 'a)>`
/// (or other invariant types like `UnsafeCell` or `*mut (dyn Foo + 'a)`).
///
/// We sidestep the problem by using `NonNull` which is covariant. In the
/// special case of trait objects this is sound (easily checked by adding a
/// `PhantomData<&'a mut Foo + 'a)>` field). We don't need an explicit `Cell`
/// type here because we never hand out any refereces to the trait objects. That
/// means any reference to the pointer can create a valid multable reference
/// that is covariant over `'a` (or in other words it's a raw pointer, as long as
/// we don't hand out references we are free to do whatever we want).
struct RawBox<T: ?Sized>(NonNull<T>);
impl<T: ?Sized> RawBox<T> {
/// Safety: Only a single mutable reference
/// created by this function may exist at a given time.
#[allow(clippy::mut_from_ref)]
unsafe fn get(&self) -> &mut T {
&mut *self.0.as_ptr()
}
}
impl<T: ?Sized> From<Box<T>> for RawBox<T> {
fn from(box_: Box<T>) -> Self {
// obviously safe because Box::into_raw never returns null
unsafe { Self(NonNull::new_unchecked(Box::into_raw(box_))) }
}
}
impl<T: ?Sized> Drop for RawBox<T> {
fn drop(&mut self) {
unsafe { drop(Box::from_raw(self.0.as_ptr())) }
}
}
/// Annotations that change that is displayed when the document is render. /// Annotations that change that is displayed when the document is render.
/// Also commonly called virtual text. /// Also commonly called virtual text.
#[derive(Default, Debug, Clone)] #[derive(Default)]
pub struct TextAnnotations<'a> { pub struct TextAnnotations<'a> {
inline_annotations: Vec<Layer<'a, InlineAnnotation, Option<Highlight>>>, inline_annotations: Vec<Layer<'a, InlineAnnotation, Option<Highlight>>>,
overlays: Vec<Layer<'a, Overlay, Option<Highlight>>>, overlays: Vec<Layer<'a, Overlay, Option<Highlight>>>,
line_annotations: Vec<Layer<'a, LineAnnotation, ()>>, line_annotations: Vec<(Cell<usize>, RawBox<dyn LineAnnotation + 'a>)>,
}
impl Debug for TextAnnotations<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TextAnnotations")
.field("inline_annotations", &self.inline_annotations)
.field("overlays", &self.overlays)
.finish_non_exhaustive()
}
} }
impl<'a> TextAnnotations<'a> { impl<'a> TextAnnotations<'a> {
@ -157,9 +295,9 @@ impl<'a> TextAnnotations<'a> {
pub fn reset_pos(&self, char_idx: usize) { pub fn reset_pos(&self, char_idx: usize) {
reset_pos(&self.inline_annotations, char_idx, |annot| annot.char_idx); reset_pos(&self.inline_annotations, char_idx, |annot| annot.char_idx);
reset_pos(&self.overlays, char_idx, |annot| annot.char_idx); reset_pos(&self.overlays, char_idx, |annot| annot.char_idx);
reset_pos(&self.line_annotations, char_idx, |annot| { for (next_anchor, layer) in &self.line_annotations {
annot.anchor_char_idx next_anchor.set(unsafe { layer.get().reset_pos(char_idx) });
}); }
} }
pub fn collect_overlay_highlights( pub fn collect_overlay_highlights(
@ -196,7 +334,9 @@ impl<'a> TextAnnotations<'a> {
layer: &'a [InlineAnnotation], layer: &'a [InlineAnnotation],
highlight: Option<Highlight>, highlight: Option<Highlight>,
) -> &mut Self { ) -> &mut Self {
self.inline_annotations.push((layer, highlight).into()); if !layer.is_empty() {
self.inline_annotations.push((layer, highlight).into());
}
self self
} }
@ -211,7 +351,9 @@ impl<'a> TextAnnotations<'a> {
/// If multiple layers contain overlay at the same position /// If multiple layers contain overlay at the same position
/// the overlay from the layer added last will be show. /// the overlay from the layer added last will be show.
pub fn add_overlay(&mut self, layer: &'a [Overlay], highlight: Option<Highlight>) -> &mut Self { pub fn add_overlay(&mut self, layer: &'a [Overlay], highlight: Option<Highlight>) -> &mut Self {
self.overlays.push((layer, highlight).into()); if !layer.is_empty() {
self.overlays.push((layer, highlight).into());
}
self self
} }
@ -219,8 +361,9 @@ impl<'a> TextAnnotations<'a> {
/// ///
/// The line annotations **must be sorted** by their `char_idx`. /// The line annotations **must be sorted** by their `char_idx`.
/// Multiple line annotations with the same `char_idx` **are not allowed**. /// Multiple line annotations with the same `char_idx` **are not allowed**.
pub fn add_line_annotation(&mut self, layer: &'a [LineAnnotation]) -> &mut Self { pub fn add_line_annotation(&mut self, layer: Box<dyn LineAnnotation + 'a>) -> &mut Self {
self.line_annotations.push((layer, ()).into()); self.line_annotations
.push((Cell::new(usize::MAX), layer.into()));
self self
} }
@ -250,21 +393,35 @@ impl<'a> TextAnnotations<'a> {
overlay overlay
} }
pub(crate) fn annotation_lines_at(&self, char_idx: usize) -> usize { pub(crate) fn process_virtual_text_anchors(&self, grapheme: &FormattedGrapheme) {
self.line_annotations for (next_anchor, layer) in &self.line_annotations {
.iter() loop {
.map(|layer| { match next_anchor.get().cmp(&grapheme.char_idx) {
let mut lines = 0; Ordering::Less => next_anchor
while let Some(annot) = layer.annotations.get(layer.current_index.get()) { .set(unsafe { layer.get().skip_concealed_anchors(grapheme.char_idx) }),
if annot.anchor_char_idx == char_idx { Ordering::Equal => {
layer.current_index.set(layer.current_index.get() + 1); next_anchor.set(unsafe { layer.get().process_anchor(grapheme) })
lines += annot.height
} else {
break;
} }
} Ordering::Greater => break,
lines };
}) }
.sum() }
}
pub(crate) fn virtual_lines_at(
&self,
char_idx: usize,
line_end_visual_pos: Position,
doc_line: usize,
) -> usize {
let mut virt_off = Position::new(0, 0);
for (_, layer) in &self.line_annotations {
virt_off += unsafe {
layer
.get()
.insert_virtual_lines(char_idx, line_end_visual_pos + virt_off, doc_line)
};
}
virt_off.row
} }
} }

@ -28,7 +28,10 @@ pub trait AsyncHook: Sync + Send + 'static + Sized {
// so it should only be reached in case of total CPU overload. // so it should only be reached in case of total CPU overload.
// However, a bounded channel is much more efficient so it's nice to use here // However, a bounded channel is much more efficient so it's nice to use here
let (tx, rx) = mpsc::channel(128); let (tx, rx) = mpsc::channel(128);
tokio::spawn(run(self, rx)); // only spawn worker if we are inside runtime to avoid having to spawn a runtime for unrelated unit tests
if tokio::runtime::Handle::try_current().is_ok() {
tokio::spawn(run(self, rx));
}
tx tx
} }
} }

@ -11,6 +11,7 @@ use helix_view::{
align_view, align_view,
document::{DocumentOpenError, DocumentSavedEventResult}, document::{DocumentOpenError, DocumentSavedEventResult},
editor::{ConfigEvent, EditorEvent}, editor::{ConfigEvent, EditorEvent},
events::DiagnosticsDidChange,
graphics::Rect, graphics::Rect,
theme, theme,
tree::Layout, tree::Layout,
@ -290,7 +291,7 @@ impl Application {
self.compositor.render(area, surface, &mut cx); self.compositor.render(area, surface, &mut cx);
let (pos, kind) = self.compositor.cursor(area, &self.editor); let (pos, kind) = self.compositor.cursor(area, &self.editor);
// reset cursor cache // reset cursor cache
self.editor.cursor_cache.set(None); self.editor.cursor_cache.reset();
let pos = pos.map(|pos| (pos.col as u16, pos.row as u16)); let pos = pos.map(|pos| (pos.col as u16, pos.row as u16));
self.terminal.draw(pos, kind).unwrap(); self.terminal.draw(pos, kind).unwrap();
@ -771,7 +772,7 @@ impl Application {
// Note: The `lsp::DiagnosticSeverity` enum is already defined in decreasing order // Note: The `lsp::DiagnosticSeverity` enum is already defined in decreasing order
params params
.diagnostics .diagnostics
.sort_unstable_by_key(|d| (d.severity, d.range.start)); .sort_by_key(|d| (d.severity, d.range.start));
} }
for source in &lang_conf.persistent_diagnostic_sources { for source in &lang_conf.persistent_diagnostic_sources {
let new_diagnostics = params let new_diagnostics = params
@ -812,9 +813,8 @@ impl Application {
// Sort diagnostics first by severity and then by line numbers. // Sort diagnostics first by severity and then by line numbers.
// Note: The `lsp::DiagnosticSeverity` enum is already defined in decreasing order // Note: The `lsp::DiagnosticSeverity` enum is already defined in decreasing order
diagnostics.sort_unstable_by_key(|(d, server_id)| { diagnostics
(d.severity, d.range.start, *server_id) .sort_by_key(|(d, server_id)| (d.severity, d.range.start, *server_id));
});
if let Some(doc) = doc { if let Some(doc) = doc {
let diagnostic_of_language_server_and_not_in_unchanged_sources = let diagnostic_of_language_server_and_not_in_unchanged_sources =
@ -835,6 +835,12 @@ impl Application {
&unchanged_diag_sources, &unchanged_diag_sources,
Some(server_id), Some(server_id),
); );
let doc = doc.id();
helix_event::dispatch(DiagnosticsDidChange {
editor: &mut self.editor,
doc,
});
} }
} }
Notification::ShowMessage(params) => { Notification::ShowMessage(params) => {

@ -1711,6 +1711,7 @@ pub fn scroll(cx: &mut Context, offset: usize, direction: Direction, sync_cursor
&mut annotations, &mut annotations,
) )
}); });
drop(annotations);
doc.set_selection(view.id, selection); doc.set_selection(view.id, selection);
return; return;
} }
@ -3549,6 +3550,8 @@ fn goto_first_diag(cx: &mut Context) {
None => return, None => return,
}; };
doc.set_selection(view.id, selection); doc.set_selection(view.id, selection);
view.diagnostics_handler
.immediately_show_diagnostic(doc, view.id);
} }
fn goto_last_diag(cx: &mut Context) { fn goto_last_diag(cx: &mut Context) {
@ -3558,6 +3561,8 @@ fn goto_last_diag(cx: &mut Context) {
None => return, None => return,
}; };
doc.set_selection(view.id, selection); doc.set_selection(view.id, selection);
view.diagnostics_handler
.immediately_show_diagnostic(doc, view.id);
} }
fn goto_next_diag(cx: &mut Context) { fn goto_next_diag(cx: &mut Context) {
@ -3580,6 +3585,8 @@ fn goto_next_diag(cx: &mut Context) {
None => return, None => return,
}; };
doc.set_selection(view.id, selection); doc.set_selection(view.id, selection);
view.diagnostics_handler
.immediately_show_diagnostic(doc, view.id);
}; };
cx.editor.apply_motion(motion); cx.editor.apply_motion(motion);
@ -3608,6 +3615,8 @@ fn goto_prev_diag(cx: &mut Context) {
None => return, None => return,
}; };
doc.set_selection(view.id, selection); doc.set_selection(view.id, selection);
view.diagnostics_handler
.immediately_show_diagnostic(doc, view.id);
}; };
cx.editor.apply_motion(motion) cx.editor.apply_motion(motion)
} }

@ -271,7 +271,10 @@ fn diag_picker(
let Some(path) = uri.as_path() else { let Some(path) = uri.as_path() else {
return; return;
}; };
jump_to_position(cx.editor, path, diag.range, *offset_encoding, action) jump_to_position(cx.editor, path, diag.range, *offset_encoding, action);
let (view, doc) = current!(cx.editor);
view.diagnostics_handler
.immediately_show_diagnostic(doc, view.id);
}, },
) )
.with_preview(move |_editor, PickerDiagnostic { uri, diag, .. }| { .with_preview(move |_editor, PickerDiagnostic { uri, diag, .. }| {

@ -1,6 +1,6 @@
use helix_event::{events, register_event}; use helix_event::{events, register_event};
use helix_view::document::Mode; use helix_view::document::Mode;
use helix_view::events::{DocumentDidChange, SelectionDidChange}; use helix_view::events::{DiagnosticsDidChange, DocumentDidChange, SelectionDidChange};
use crate::commands; use crate::commands;
use crate::keymap::MappableCommand; use crate::keymap::MappableCommand;
@ -17,4 +17,5 @@ pub fn register() {
register_event::<PostCommand>(); register_event::<PostCommand>();
register_event::<DocumentDidChange>(); register_event::<DocumentDidChange>();
register_event::<SelectionDidChange>(); register_event::<SelectionDidChange>();
register_event::<DiagnosticsDidChange>();
} }

@ -14,6 +14,7 @@ pub use helix_view::handlers::Handlers;
mod auto_save; mod auto_save;
pub mod completion; pub mod completion;
mod diagnostics;
mod signature_help; mod signature_help;
pub fn setup(config: Arc<ArcSwap<Config>>) -> Handlers { pub fn setup(config: Arc<ArcSwap<Config>>) -> Handlers {
@ -32,5 +33,6 @@ pub fn setup(config: Arc<ArcSwap<Config>>) -> Handlers {
completion::register_hooks(&handlers); completion::register_hooks(&handlers);
signature_help::register_hooks(&handlers); signature_help::register_hooks(&handlers);
auto_save::register_hooks(&handlers); auto_save::register_hooks(&handlers);
diagnostics::register_hooks(&handlers);
handlers handlers
} }

@ -0,0 +1,24 @@
use helix_event::{register_hook, send_blocking};
use helix_view::document::Mode;
use helix_view::events::DiagnosticsDidChange;
use helix_view::handlers::diagnostics::DiagnosticEvent;
use helix_view::handlers::Handlers;
use crate::events::OnModeSwitch;
pub(super) fn register_hooks(_handlers: &Handlers) {
register_hook!(move |event: &mut DiagnosticsDidChange<'_>| {
if event.editor.mode != Mode::Insert {
for (view, _) in event.editor.tree.views_mut() {
send_blocking(&view.diagnostics_handler.events, DiagnosticEvent::Refresh)
}
}
Ok(())
});
register_hook!(move |event: &mut OnModeSwitch<'_, '_>| {
for (view, _) in event.cx.editor.tree.views_mut() {
view.diagnostics_handler.active = event.new_mode != Mode::Insert;
}
Ok(())
});
}

@ -448,14 +448,12 @@ impl Component for Completion {
// --- // ---
// option.documentation // option.documentation
let (view, doc) = current!(cx.editor); let Some(coords) = cx.editor.cursor().0 else {
let language = doc.language_name().unwrap_or(""); return;
let text = doc.text().slice(..); };
let cursor_pos = doc.selection(view.id).primary().cursor(text);
let coords = view
.screen_coords_at_pos(doc, text, cursor_pos)
.expect("cursor must be in view");
let cursor_pos = coords.row as u16; let cursor_pos = coords.row as u16;
let doc = doc!(cx.editor);
let language = doc.language_name().unwrap_or("");
let markdowned = |lang: &str, detail: Option<&str>, doc: Option<&str>| { let markdowned = |lang: &str, detail: Option<&str>, doc: Option<&str>| {
let md = match (detail, doc) { let md = match (detail, doc) {

@ -12,26 +12,10 @@ use helix_view::editor::{WhitespaceConfig, WhitespaceRenderValue};
use helix_view::graphics::Rect; use helix_view::graphics::Rect;
use helix_view::theme::Style; use helix_view::theme::Style;
use helix_view::view::ViewPosition; use helix_view::view::ViewPosition;
use helix_view::Document; use helix_view::{Document, Theme};
use helix_view::Theme;
use tui::buffer::Buffer as Surface; use tui::buffer::Buffer as Surface;
pub trait LineDecoration { use crate::ui::text_decorations::DecorationManager;
fn render_background(&mut self, _renderer: &mut TextRenderer, _pos: LinePos) {}
fn render_foreground(
&mut self,
_renderer: &mut TextRenderer,
_pos: LinePos,
_end_char_idx: usize,
) {
}
}
impl<F: FnMut(&mut TextRenderer, LinePos)> LineDecoration for F {
fn render_background(&mut self, renderer: &mut TextRenderer, pos: LinePos) {
self(renderer, pos)
}
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)] #[derive(Debug, PartialEq, Eq, Clone, Copy)]
enum StyleIterKind { enum StyleIterKind {
@ -68,10 +52,7 @@ impl<H: Iterator<Item = HighlightEvent>> Iterator for StyleIter<'_, H> {
HighlightEvent::HighlightEnd => { HighlightEvent::HighlightEnd => {
self.active_highlights.pop(); self.active_highlights.pop();
} }
HighlightEvent::Source { start, mut end } => { HighlightEvent::Source { mut end, .. } => {
if start == end {
continue;
}
let style = self let style = self
.active_highlights .active_highlights
.iter() .iter()
@ -98,15 +79,8 @@ pub struct LinePos {
pub doc_line: usize, pub doc_line: usize,
/// Vertical offset from the top of the inner view area /// Vertical offset from the top of the inner view area
pub visual_line: u16, pub visual_line: u16,
/// The first char index of this visual line.
/// Note that if the visual line is entirely filled by
/// a very long inline virtual text then this index will point
/// at the next (non-virtual) char after this visual line
pub start_char_idx: usize,
} }
pub type TranslatedPosition<'a> = (usize, Box<dyn FnMut(&mut TextRenderer, Position) + 'a>);
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub fn render_document( pub fn render_document(
surface: &mut Surface, surface: &mut Surface,
@ -117,87 +91,46 @@ pub fn render_document(
syntax_highlight_iter: impl Iterator<Item = HighlightEvent>, syntax_highlight_iter: impl Iterator<Item = HighlightEvent>,
overlay_highlight_iter: impl Iterator<Item = HighlightEvent>, overlay_highlight_iter: impl Iterator<Item = HighlightEvent>,
theme: &Theme, theme: &Theme,
line_decoration: &mut [Box<dyn LineDecoration + '_>], decorations: DecorationManager,
translated_positions: &mut [TranslatedPosition],
) { ) {
let mut renderer = TextRenderer::new(surface, doc, theme, offset.horizontal_offset, viewport); let mut renderer = TextRenderer::new(
surface,
doc,
theme,
Position::new(offset.vertical_offset, offset.horizontal_offset),
viewport,
);
render_text( render_text(
&mut renderer, &mut renderer,
doc.text().slice(..), doc.text().slice(..),
offset, offset.anchor,
&doc.text_format(viewport.width, Some(theme)), &doc.text_format(viewport.width, Some(theme)),
doc_annotations, doc_annotations,
syntax_highlight_iter, syntax_highlight_iter,
overlay_highlight_iter, overlay_highlight_iter,
theme, theme,
line_decoration, decorations,
translated_positions,
) )
} }
fn translate_positions(
char_pos: usize,
first_visible_char_idx: usize,
translated_positions: &mut [TranslatedPosition],
text_fmt: &TextFormat,
renderer: &mut TextRenderer,
pos: Position,
) {
// check if any positions translated on the fly (like cursor) has been reached
for (char_idx, callback) in &mut *translated_positions {
if *char_idx < char_pos && *char_idx >= first_visible_char_idx {
// by replacing the char_index with usize::MAX large number we ensure
// that the same position is only translated once
// text will never reach usize::MAX as rust memory allocations are limited
// to isize::MAX
*char_idx = usize::MAX;
if text_fmt.soft_wrap {
callback(renderer, pos)
} else if pos.col >= renderer.col_offset
&& pos.col - renderer.col_offset < renderer.viewport.width as usize
{
callback(
renderer,
Position {
row: pos.row,
col: pos.col - renderer.col_offset,
},
)
}
}
}
}
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub fn render_text<'t>( pub fn render_text(
renderer: &mut TextRenderer, renderer: &mut TextRenderer,
text: RopeSlice<'t>, text: RopeSlice<'_>,
offset: ViewPosition, anchor: usize,
text_fmt: &TextFormat, text_fmt: &TextFormat,
text_annotations: &TextAnnotations, text_annotations: &TextAnnotations,
syntax_highlight_iter: impl Iterator<Item = HighlightEvent>, syntax_highlight_iter: impl Iterator<Item = HighlightEvent>,
overlay_highlight_iter: impl Iterator<Item = HighlightEvent>, overlay_highlight_iter: impl Iterator<Item = HighlightEvent>,
theme: &Theme, theme: &Theme,
line_decorations: &mut [Box<dyn LineDecoration + '_>], mut decorations: DecorationManager,
translated_positions: &mut [TranslatedPosition],
) { ) {
let ( let row_off = visual_offset_from_block(text, anchor, anchor, text_fmt, text_annotations)
Position { .0
row: mut row_off, .. .row;
},
mut char_pos,
) = visual_offset_from_block(
text,
offset.anchor,
offset.anchor,
text_fmt,
text_annotations,
);
row_off += offset.vertical_offset;
let (mut formatter, mut first_visible_char_idx) = let mut formatter =
DocumentFormatter::new_at_prev_checkpoint(text, text_fmt, text_annotations, offset.anchor); DocumentFormatter::new_at_prev_checkpoint(text, text_fmt, text_annotations, anchor);
let mut syntax_styles = StyleIter { let mut syntax_styles = StyleIter {
text_style: renderer.text_style, text_style: renderer.text_style,
active_highlights: Vec::with_capacity(64), active_highlights: Vec::with_capacity(64),
@ -219,8 +152,8 @@ pub fn render_text<'t>(
first_visual_line: false, first_visual_line: false,
doc_line: usize::MAX, doc_line: usize::MAX,
visual_line: u16::MAX, visual_line: u16::MAX,
start_char_idx: usize::MAX,
}; };
let mut last_line_end = 0;
let mut is_in_indent_area = true; let mut is_in_indent_area = true;
let mut last_line_indent_level = 0; let mut last_line_indent_level = 0;
let mut syntax_style_span = syntax_styles let mut syntax_style_span = syntax_styles
@ -229,133 +162,96 @@ pub fn render_text<'t>(
let mut overlay_style_span = overlay_styles let mut overlay_style_span = overlay_styles
.next() .next()
.unwrap_or_else(|| (Style::default(), usize::MAX)); .unwrap_or_else(|| (Style::default(), usize::MAX));
let mut reached_view_top = false;
loop { loop {
// formattter.line_pos returns to line index of the next grapheme let Some(mut grapheme) = formatter.next() else {
// so it must be called before formatter.next
let doc_line = formatter.line_pos();
let Some((grapheme, mut pos)) = formatter.next() else {
let mut last_pos = formatter.visual_pos();
if last_pos.row >= row_off {
last_pos.col -= 1;
last_pos.row -= row_off;
// check if any positions translated on the fly (like cursor) are at the EOF
translate_positions(
char_pos + 1,
first_visible_char_idx,
translated_positions,
text_fmt,
renderer,
last_pos,
);
}
break; break;
}; };
// skip any graphemes on visual lines before the block start // skip any graphemes on visual lines before the block start
if pos.row < row_off { if grapheme.visual_pos.row < row_off {
if char_pos >= syntax_style_span.1 {
syntax_style_span = if let Some(syntax_style_span) = syntax_styles.next() {
syntax_style_span
} else {
break;
}
}
if char_pos >= overlay_style_span.1 {
overlay_style_span = if let Some(overlay_style_span) = overlay_styles.next() {
overlay_style_span
} else {
break;
}
}
char_pos += grapheme.doc_chars();
first_visible_char_idx = char_pos + 1;
continue; continue;
} }
pos.row -= row_off; grapheme.visual_pos.row -= row_off;
if !reached_view_top {
decorations.prepare_for_rendering(grapheme.char_idx);
reached_view_top = true;
}
// if the end of the viewport is reached stop rendering // if the end of the viewport is reached stop rendering
if pos.row as u16 >= renderer.viewport.height { if grapheme.visual_pos.row as u16 >= renderer.viewport.height + renderer.offset.row as u16 {
break; break;
} }
// apply decorations before rendering a new line // apply decorations before rendering a new line
if pos.row as u16 != last_line_pos.visual_line { if grapheme.visual_pos.row as u16 != last_line_pos.visual_line {
if pos.row > 0 { // we initiate doc_line with usize::MAX because no file
// can reach that size (memory allocations are limited to isize::MAX)
// initially there is no "previous" line (so doc_line is set to usize::MAX)
// in that case we don't need to draw indent guides/virtual text
if last_line_pos.doc_line != usize::MAX {
// draw indent guides for the last line
renderer.draw_indent_guides(last_line_indent_level, last_line_pos.visual_line); renderer.draw_indent_guides(last_line_indent_level, last_line_pos.visual_line);
is_in_indent_area = true; is_in_indent_area = true;
for line_decoration in &mut *line_decorations { decorations.render_virtual_lines(renderer, last_line_pos, last_line_end)
line_decoration.render_foreground(renderer, last_line_pos, char_pos);
}
} }
last_line_pos = LinePos { last_line_pos = LinePos {
first_visual_line: doc_line != last_line_pos.doc_line, first_visual_line: grapheme.line_idx != last_line_pos.doc_line,
doc_line, doc_line: grapheme.line_idx,
visual_line: pos.row as u16, visual_line: grapheme.visual_pos.row as u16,
start_char_idx: char_pos,
}; };
for line_decoration in &mut *line_decorations { decorations.decorate_line(renderer, last_line_pos);
line_decoration.render_background(renderer, last_line_pos);
}
} }
// acquire the correct grapheme style // acquire the correct grapheme style
if char_pos >= syntax_style_span.1 { while grapheme.char_idx >= syntax_style_span.1 {
syntax_style_span = syntax_styles syntax_style_span = syntax_styles
.next() .next()
.unwrap_or((Style::default(), usize::MAX)); .unwrap_or((Style::default(), usize::MAX));
} }
if char_pos >= overlay_style_span.1 { while grapheme.char_idx >= overlay_style_span.1 {
overlay_style_span = overlay_styles overlay_style_span = overlay_styles
.next() .next()
.unwrap_or((Style::default(), usize::MAX)); .unwrap_or((Style::default(), usize::MAX));
} }
char_pos += grapheme.doc_chars();
// check if any positions translated on the fly (like cursor) has been reached
translate_positions(
char_pos,
first_visible_char_idx,
translated_positions,
text_fmt,
renderer,
pos,
);
let (syntax_style, overlay_style) =
if let GraphemeSource::VirtualText { highlight } = grapheme.source {
let mut style = renderer.text_style;
if let Some(highlight) = highlight {
style = style.patch(theme.highlight(highlight.0))
}
(style, Style::default())
} else {
(syntax_style_span.0, overlay_style_span.0)
};
let is_virtual = grapheme.is_virtual(); let grapheme_style = if let GraphemeSource::VirtualText { highlight } = grapheme.source {
renderer.draw_grapheme( let mut style = renderer.text_style;
grapheme.grapheme, if let Some(highlight) = highlight {
style = style.patch(theme.highlight(highlight.0));
}
GraphemeStyle {
syntax_style: style,
overlay_style: Style::default(),
}
} else {
GraphemeStyle { GraphemeStyle {
syntax_style, syntax_style: syntax_style_span.0,
overlay_style, overlay_style: overlay_style_span.0,
}, }
is_virtual, };
decorations.decorate_grapheme(renderer, &grapheme);
let virt = grapheme.is_virtual();
let grapheme_width = renderer.draw_grapheme(
grapheme.raw,
grapheme_style,
virt,
&mut last_line_indent_level, &mut last_line_indent_level,
&mut is_in_indent_area, &mut is_in_indent_area,
pos, grapheme.visual_pos,
); );
last_line_end = grapheme.visual_pos.col + grapheme_width;
} }
renderer.draw_indent_guides(last_line_indent_level, last_line_pos.visual_line); renderer.draw_indent_guides(last_line_indent_level, last_line_pos.visual_line);
for line_decoration in &mut *line_decorations { decorations.render_virtual_lines(renderer, last_line_pos, last_line_end)
line_decoration.render_foreground(renderer, last_line_pos, char_pos);
}
} }
#[derive(Debug)] #[derive(Debug)]
pub struct TextRenderer<'a> { pub struct TextRenderer<'a> {
pub surface: &'a mut Surface, surface: &'a mut Surface,
pub text_style: Style, pub text_style: Style,
pub whitespace_style: Style, pub whitespace_style: Style,
pub indent_guide_char: String, pub indent_guide_char: String,
@ -369,8 +265,8 @@ pub struct TextRenderer<'a> {
pub indent_width: u16, pub indent_width: u16,
pub starting_indent: usize, pub starting_indent: usize,
pub draw_indent_guides: bool, pub draw_indent_guides: bool,
pub col_offset: usize,
pub viewport: Rect, pub viewport: Rect,
pub offset: Position,
} }
pub struct GraphemeStyle { pub struct GraphemeStyle {
@ -383,7 +279,7 @@ impl<'a> TextRenderer<'a> {
surface: &'a mut Surface, surface: &'a mut Surface,
doc: &Document, doc: &Document,
theme: &Theme, theme: &Theme,
col_offset: usize, offset: Position,
viewport: Rect, viewport: Rect,
) -> TextRenderer<'a> { ) -> TextRenderer<'a> {
let editor_config = doc.config.load(); let editor_config = doc.config.load();
@ -438,8 +334,8 @@ impl<'a> TextRenderer<'a> {
virtual_tab, virtual_tab,
whitespace_style: theme.get("ui.virtual.whitespace"), whitespace_style: theme.get("ui.virtual.whitespace"),
indent_width, indent_width,
starting_indent: col_offset / indent_width as usize starting_indent: offset.col / indent_width as usize
+ (col_offset % indent_width as usize != 0) as usize + (offset.col % indent_width as usize != 0) as usize
+ editor_config.indent_guides.skip_levels as usize, + editor_config.indent_guides.skip_levels as usize,
indent_guide_style: text_style.patch( indent_guide_style: text_style.patch(
theme theme
@ -449,8 +345,46 @@ impl<'a> TextRenderer<'a> {
text_style, text_style,
draw_indent_guides: editor_config.indent_guides.render, draw_indent_guides: editor_config.indent_guides.render,
viewport, viewport,
col_offset, offset,
}
}
/// Draws a single `grapheme` at the current render position with a specified `style`.
pub fn draw_decoration_grapheme(
&mut self,
grapheme: Grapheme,
mut style: Style,
mut row: u16,
col: u16,
) -> bool {
if (row as usize) < self.offset.row
|| row >= self.viewport.height
|| col >= self.viewport.width
{
return false;
}
row -= self.offset.row as u16;
// TODO is it correct to apply the whitspace style to all unicode white spaces?
if grapheme.is_whitespace() {
style = style.patch(self.whitespace_style);
} }
let grapheme = match grapheme {
Grapheme::Tab { width } => {
let grapheme_tab_width = char_to_byte_idx(&self.virtual_tab, width);
&self.virtual_tab[..grapheme_tab_width]
}
Grapheme::Other { ref g } if g == "\u{00A0}" => " ",
Grapheme::Other { ref g } => g,
Grapheme::Newline => " ",
};
self.surface.set_string(
self.viewport.x + col,
self.viewport.y + row,
grapheme,
style,
);
true
} }
/// Draws a single `grapheme` at the current render position with a specified `style`. /// Draws a single `grapheme` at the current render position with a specified `style`.
@ -461,9 +395,13 @@ impl<'a> TextRenderer<'a> {
is_virtual: bool, is_virtual: bool,
last_indent_level: &mut usize, last_indent_level: &mut usize,
is_in_indent_area: &mut bool, is_in_indent_area: &mut bool,
position: Position, mut position: Position,
) { ) -> usize {
let cut_off_start = self.col_offset.saturating_sub(position.col); if position.row < self.offset.row {
return 0;
}
position.row -= self.offset.row;
let cut_off_start = self.offset.col.saturating_sub(position.col);
let is_whitespace = grapheme.is_whitespace(); let is_whitespace = grapheme.is_whitespace();
// TODO is it correct to apply the whitespace style to all unicode white spaces? // TODO is it correct to apply the whitespace style to all unicode white spaces?
@ -495,12 +433,11 @@ impl<'a> TextRenderer<'a> {
Grapheme::Newline => &self.newline, Grapheme::Newline => &self.newline,
}; };
let in_bounds = self.col_offset <= position.col let in_bounds = self.column_in_bounds(position.col + width - 1);
&& position.col < self.viewport.width as usize + self.col_offset;
if in_bounds { if in_bounds {
self.surface.set_string( self.surface.set_string(
self.viewport.x + (position.col - self.col_offset) as u16, self.viewport.x + (position.col - self.offset.col) as u16,
self.viewport.y + position.row as u16, self.viewport.y + position.row as u16,
grapheme, grapheme,
style, style,
@ -520,26 +457,33 @@ impl<'a> TextRenderer<'a> {
*last_indent_level = position.col; *last_indent_level = position.col;
*is_in_indent_area = false; *is_in_indent_area = false;
} }
width
}
pub fn column_in_bounds(&self, colum: usize) -> bool {
self.offset.col <= colum && colum < self.viewport.width as usize + self.offset.col
} }
/// Overlay indentation guides ontop of a rendered line /// Overlay indentation guides ontop of a rendered line
/// The indentation level is computed in `draw_lines`. /// The indentation level is computed in `draw_lines`.
/// Therefore this function must always be called afterwards. /// Therefore this function must always be called afterwards.
pub fn draw_indent_guides(&mut self, indent_level: usize, row: u16) { pub fn draw_indent_guides(&mut self, indent_level: usize, mut row: u16) {
if !self.draw_indent_guides { if !self.draw_indent_guides || self.offset.row > row as usize {
return; return;
} }
row -= self.offset.row as u16;
// Don't draw indent guides outside of view // Don't draw indent guides outside of view
let end_indent = min( let end_indent = min(
indent_level, indent_level,
// Add indent_width - 1 to round up, since the first visible // Add indent_width - 1 to round up, since the first visible
// indent might be a bit after offset.col // indent might be a bit after offset.col
self.col_offset + self.viewport.width as usize + (self.indent_width as usize - 1), self.offset.col + self.viewport.width as usize + (self.indent_width as usize - 1),
) / self.indent_width as usize; ) / self.indent_width as usize;
for i in self.starting_indent..end_indent { for i in self.starting_indent..end_indent {
let x = (self.viewport.x as usize + (i * self.indent_width as usize) - self.col_offset) let x = (self.viewport.x as usize + (i * self.indent_width as usize) - self.offset.col)
as u16; as u16;
let y = self.viewport.y + row; let y = self.viewport.y + row;
debug_assert!(self.surface.in_bounds(x, y)); debug_assert!(self.surface.in_bounds(x, y));
@ -547,4 +491,62 @@ impl<'a> TextRenderer<'a> {
.set_string(x, y, &self.indent_guide_char, self.indent_guide_style); .set_string(x, y, &self.indent_guide_char, self.indent_guide_style);
} }
} }
pub fn set_string(&mut self, x: u16, y: u16, string: impl AsRef<str>, style: Style) {
if (y as usize) < self.offset.row {
return;
}
self.surface
.set_string(x, y + self.viewport.y, string, style)
}
pub fn set_stringn(
&mut self,
x: u16,
y: u16,
string: impl AsRef<str>,
width: usize,
style: Style,
) {
if (y as usize) < self.offset.row {
return;
}
self.surface
.set_stringn(x, y + self.viewport.y, string, width, style);
}
/// Sets the style of an area **within the text viewport* this accounts
/// both for the renderers vertical offset and its viewport
pub fn set_style(&mut self, mut area: Rect, style: Style) {
area = area.clip_top(self.offset.row as u16);
area.y += self.viewport.y;
self.surface.set_style(area, style);
}
/// Sets the style of an area **within the text viewport* this accounts
/// both for the renderers vertical offset and its viewport
#[allow(clippy::too_many_arguments)]
pub fn set_string_truncated(
&mut self,
x: u16,
y: u16,
string: &str,
width: usize,
style: impl Fn(usize) -> Style, // Map a grapheme's string offset to a style
ellipsis: bool,
truncate_start: bool,
) -> (u16, u16) {
if (y as usize) < self.offset.row {
return (x, y);
}
self.surface.set_string_truncated(
x,
y + self.viewport.y,
string,
width,
style,
ellipsis,
truncate_start,
)
}
} }

@ -5,8 +5,10 @@ use crate::{
key, key,
keymap::{KeymapResult, Keymaps}, keymap::{KeymapResult, Keymaps},
ui::{ ui::{
document::{render_document, LinePos, TextRenderer, TranslatedPosition}, document::{render_document, LinePos, TextRenderer},
Completion, ProgressSpinners, statusline,
text_decorations::{self, Decoration, DecorationManager, InlineDiagnostics},
Completion, CompletionItem, ProgressSpinners,
}, },
}; };
@ -20,6 +22,7 @@ use helix_core::{
visual_offset_from_block, Change, Position, Range, Selection, Transaction, visual_offset_from_block, Change, Position, Range, Selection, Transaction,
}; };
use helix_view::{ use helix_view::{
annotations::diagnostics::DiagnosticFilter,
document::{Mode, SavePoint, SCRATCH_BUFFER_NAME}, document::{Mode, SavePoint, SCRATCH_BUFFER_NAME},
editor::{CompleteAction, CursorShapeConfig}, editor::{CompleteAction, CursorShapeConfig},
graphics::{Color, CursorKind, Modifier, Rect, Style}, graphics::{Color, CursorKind, Modifier, Rect, Style},
@ -31,9 +34,6 @@ use std::{mem::take, num::NonZeroUsize, path::PathBuf, rc::Rc, sync::Arc};
use tui::{buffer::Buffer as Surface, text::Span}; use tui::{buffer::Buffer as Surface, text::Span};
use super::document::LineDecoration;
use super::{completion::CompletionItem, statusline};
pub struct EditorView { pub struct EditorView {
pub keymaps: Keymaps, pub keymaps: Keymaps,
on_next_key: Option<OnKeyCallback>, on_next_key: Option<OnKeyCallback>,
@ -94,11 +94,10 @@ impl EditorView {
let config = editor.config(); let config = editor.config();
let text_annotations = view.text_annotations(doc, Some(theme)); let text_annotations = view.text_annotations(doc, Some(theme));
let mut line_decorations: Vec<Box<dyn LineDecoration>> = Vec::new(); let mut decorations = DecorationManager::default();
let mut translated_positions: Vec<TranslatedPosition> = Vec::new();
if is_focused && config.cursorline { if is_focused && config.cursorline {
line_decorations.push(Self::cursorline_decorator(doc, view, theme)) decorations.add_decoration(Self::cursorline(doc, view, theme));
} }
if is_focused && config.cursorcolumn { if is_focused && config.cursorcolumn {
@ -113,13 +112,10 @@ impl EditorView {
if pos.doc_line != dap_line { if pos.doc_line != dap_line {
return; return;
} }
renderer.surface.set_style( renderer.set_style(Rect::new(inner.x, pos.visual_line, inner.width, 1), style);
Rect::new(inner.x, inner.y + pos.visual_line, inner.width, 1),
style,
);
}; };
line_decorations.push(Box::new(line_decoration)); decorations.add_decoration(line_decoration);
} }
let syntax_highlights = let syntax_highlights =
@ -176,22 +172,33 @@ impl EditorView {
view.area, view.area,
theme, theme,
is_focused & self.terminal_focused, is_focused & self.terminal_focused,
&mut line_decorations, &mut decorations,
); );
} }
let primary_cursor = doc
.selection(view.id)
.primary()
.cursor(doc.text().slice(..));
if is_focused { if is_focused {
let cursor = doc decorations.add_decoration(text_decorations::Cursor {
.selection(view.id) cache: &editor.cursor_cache,
.primary() primary_cursor,
.cursor(doc.text().slice(..)); });
// set the cursor_cache to out of view in case the position is not found
editor.cursor_cache.set(Some(None));
let update_cursor_cache =
|_: &mut TextRenderer, pos| editor.cursor_cache.set(Some(Some(pos)));
translated_positions.push((cursor, Box::new(update_cursor_cache)));
} }
let width = view.inner_width(doc);
let config = doc.config.load();
let enable_cursor_line = view
.diagnostics_handler
.show_cursorline_diagnostics(doc, view.id);
let inline_diagnostic_config = config.inline_diagnostics.prepare(width, enable_cursor_line);
decorations.add_decoration(InlineDiagnostics::new(
doc,
theme,
primary_cursor,
inline_diagnostic_config,
config.end_of_line_diagnostics,
));
render_document( render_document(
surface, surface,
inner, inner,
@ -201,8 +208,7 @@ impl EditorView {
syntax_highlights, syntax_highlights,
overlay_highlights, overlay_highlights,
theme, theme,
&mut line_decorations, decorations,
&mut translated_positions,
); );
Self::render_rulers(editor, doc, view, inner, surface, theme); Self::render_rulers(editor, doc, view, inner, surface, theme);
@ -218,7 +224,11 @@ impl EditorView {
} }
} }
Self::render_diagnostics(doc, view, inner, surface, theme); if config.inline_diagnostics.disabled()
&& config.end_of_line_diagnostics == DiagnosticFilter::Disable
{
Self::render_diagnostics(doc, view, inner, surface, theme);
}
let statusline_area = view let statusline_area = view
.area .area
@ -637,7 +647,7 @@ impl EditorView {
viewport: Rect, viewport: Rect,
theme: &Theme, theme: &Theme,
is_focused: bool, is_focused: bool,
line_decorations: &mut Vec<Box<(dyn LineDecoration + 'd)>>, decoration_manager: &mut DecorationManager<'d>,
) { ) {
let text = doc.text().slice(..); let text = doc.text().slice(..);
let cursors: Rc<[_]> = doc let cursors: Rc<[_]> = doc
@ -663,7 +673,7 @@ impl EditorView {
// TODO handle softwrap in gutters // TODO handle softwrap in gutters
let selected = cursors.contains(&pos.doc_line); let selected = cursors.contains(&pos.doc_line);
let x = viewport.x + offset; let x = viewport.x + offset;
let y = viewport.y + pos.visual_line; let y = pos.visual_line;
let gutter_style = match (selected, pos.first_visual_line) { let gutter_style = match (selected, pos.first_visual_line) {
(false, true) => gutter_style, (false, true) => gutter_style,
@ -675,11 +685,9 @@ impl EditorView {
if let Some(style) = if let Some(style) =
gutter(pos.doc_line, selected, pos.first_visual_line, &mut text) gutter(pos.doc_line, selected, pos.first_visual_line, &mut text)
{ {
renderer renderer.set_stringn(x, y, &text, width, gutter_style.patch(style));
.surface
.set_stringn(x, y, &text, width, gutter_style.patch(style));
} else { } else {
renderer.surface.set_style( renderer.set_style(
Rect { Rect {
x, x,
y, y,
@ -691,7 +699,7 @@ impl EditorView {
} }
text.clear(); text.clear();
}; };
line_decorations.push(Box::new(gutter_decoration)); decoration_manager.add_decoration(gutter_decoration);
offset += width as u16; offset += width as u16;
} }
@ -761,11 +769,7 @@ impl EditorView {
} }
/// Apply the highlighting on the lines where a cursor is active /// Apply the highlighting on the lines where a cursor is active
pub fn cursorline_decorator( pub fn cursorline(doc: &Document, view: &View, theme: &Theme) -> impl Decoration {
doc: &Document,
view: &View,
theme: &Theme,
) -> Box<dyn LineDecoration> {
let text = doc.text().slice(..); let text = doc.text().slice(..);
// TODO only highlight the visual line that contains the cursor instead of the full visual line // TODO only highlight the visual line that contains the cursor instead of the full visual line
let primary_line = doc.selection(view.id).primary().cursor_line(text); let primary_line = doc.selection(view.id).primary().cursor_line(text);
@ -786,16 +790,14 @@ impl EditorView {
let secondary_style = theme.get("ui.cursorline.secondary"); let secondary_style = theme.get("ui.cursorline.secondary");
let viewport = view.area; let viewport = view.area;
let line_decoration = move |renderer: &mut TextRenderer, pos: LinePos| { move |renderer: &mut TextRenderer, pos: LinePos| {
let area = Rect::new(viewport.x, viewport.y + pos.visual_line, viewport.width, 1); let area = Rect::new(viewport.x, pos.visual_line, viewport.width, 1);
if primary_line == pos.doc_line { if primary_line == pos.doc_line {
renderer.surface.set_style(area, primary_style); renderer.set_style(area, primary_style);
} else if secondary_lines.binary_search(&pos.doc_line).is_ok() { } else if secondary_lines.binary_search(&pos.doc_line).is_ok() {
renderer.surface.set_style(area, secondary_style); renderer.set_style(area, secondary_style);
} }
}; }
Box::new(line_decoration)
} }
/// Apply the highlighting on the columns where a cursor is active /// Apply the highlighting on the columns where a cursor is active

@ -12,6 +12,7 @@ mod prompt;
mod spinner; mod spinner;
mod statusline; mod statusline;
mod text; mod text;
mod text_decorations;
use crate::compositor::Compositor; use crate::compositor::Compositor;
use crate::filter_picker_entry; use crate::filter_picker_entry;

@ -7,8 +7,9 @@ use crate::{
ctrl, key, shift, ctrl, key, shift,
ui::{ ui::{
self, self,
document::{render_document, LineDecoration, LinePos, TextRenderer}, document::{render_document, LinePos, TextRenderer},
picker::query::PickerQuery, picker::query::PickerQuery,
text_decorations::DecorationManager,
EditorView, EditorView,
}, },
}; };
@ -895,7 +896,7 @@ impl<T: 'static + Send + Sync, D: 'static + Send + Sync> Picker<T, D> {
} }
overlay_highlights = Box::new(helix_core::syntax::merge(overlay_highlights, spans)); overlay_highlights = Box::new(helix_core::syntax::merge(overlay_highlights, spans));
} }
let mut decorations: Vec<Box<dyn LineDecoration>> = Vec::new(); let mut decorations = DecorationManager::default();
if let Some((start, end)) = range { if let Some((start, end)) = range {
let style = cx let style = cx
@ -907,14 +908,14 @@ impl<T: 'static + Send + Sync, D: 'static + Send + Sync> Picker<T, D> {
if (start..=end).contains(&pos.doc_line) { if (start..=end).contains(&pos.doc_line) {
let area = Rect::new( let area = Rect::new(
renderer.viewport.x, renderer.viewport.x,
renderer.viewport.y + pos.visual_line, pos.visual_line,
renderer.viewport.width, renderer.viewport.width,
1, 1,
); );
renderer.surface.set_style(area, style) renderer.set_style(area, style)
} }
}; };
decorations.push(Box::new(draw_highlight)) decorations.add_decoration(draw_highlight);
} }
render_document( render_document(
@ -927,8 +928,7 @@ impl<T: 'static + Send + Sync, D: 'static + Send + Sync> Picker<T, D> {
syntax_highlights, syntax_highlights,
overlay_highlights, overlay_highlights,
&cx.editor.theme, &cx.editor.theme,
&mut decorations, decorations,
&mut [],
); );
} }
} }

@ -0,0 +1,175 @@
use std::cmp::Ordering;
use helix_core::doc_formatter::FormattedGrapheme;
use helix_core::Position;
use helix_view::editor::CursorCache;
use crate::ui::document::{LinePos, TextRenderer};
pub use diagnostics::InlineDiagnostics;
mod diagnostics;
/// Decorations are the primary mechanism for extending the text rendering.
///
/// Any on-screen element which is anchored to the rendered text in some form
/// should be implemented using this trait. Translating char positions to
/// on-screen positions can be expensive and should not be done manually in the
/// ui loop. Instead such translations are automatically performed on the fly
/// while the text is being rendered. The results are provided to this trait by
/// the rendering infrastructure.
///
/// To reserve space for virtual text lines (which is then filled by this trait) emit appropriate
/// [`LineAnnotation`](helix_core::text_annotations::LineAnnotation)s in [`helix_view::View::text_annotations`]
pub trait Decoration {
/// Called **before** a **visual** line is rendered. A visual line does not
/// necessarily correspond to a single line in a document as soft wrapping can
/// spread a single document line across multiple visual lines.
///
/// This function is called before text is rendered as any decorations should
/// never overlap the document text. That means that setting the forground color
/// here is (essentially) useless as the text color is overwritten by the
/// rendered text. This _of course_ doesn't apply when rendering inside virtual lines
/// below the line reserved by `LineAnnotation`s as no text will be rendered here.
fn decorate_line(&mut self, _renderer: &mut TextRenderer, _pos: LinePos) {}
/// Called **after** a **visual** line is rendered. A visual line does not
/// necessarily correspond to a single line in a document as soft wrapping can
/// spread a single document line across multiple visual lines.
///
/// This function is called after text is rendered so that decorations can collect
/// horizontal positions on the line (see [`Decoration::decorate_grapheme`]) first and
/// use those positions` while rendering
/// virtual text.
/// That means that setting the forground color
/// here is (essentially) useless as the text color is overwritten by the
/// rendered text. This -ofcourse- doesn't apply when rendering inside virtual lines
/// below the line reserved by `LineAnnotation`s. e as no text will be rendered here.
/// **Note**: To avoid overlapping decorations in the virtual lines, each decoration
/// must return the number of virtual text lines it has taken up. Each `Decoration` recieves
/// an offset `virt_off` based on these return values where it can render virtual text:
///
/// That means that a `render_line` implementation that returns `X` can render virtual text
/// in the following area:
/// ``` no-compile
/// let start = inner.y + pos.virtual_line + virt_off;
/// start .. start + X
/// ````
fn render_virt_lines(
&mut self,
_renderer: &mut TextRenderer,
_pos: LinePos,
_virt_off: Position,
) -> Position {
Position::new(0, 0)
}
fn reset_pos(&mut self, _pos: usize) -> usize {
usize::MAX
}
fn skip_concealed_anchor(&mut self, conceal_end_char_idx: usize) -> usize {
self.reset_pos(conceal_end_char_idx)
}
/// This function is called **before** the grapheme at `char_idx` is rendered.
///
/// # Returns
///
/// The char idx of the next grapheme that this function should be called for
fn decorate_grapheme(
&mut self,
_renderer: &mut TextRenderer,
_grapheme: &FormattedGrapheme,
) -> usize {
usize::MAX
}
}
impl<F: FnMut(&mut TextRenderer, LinePos)> Decoration for F {
fn decorate_line(&mut self, renderer: &mut TextRenderer, pos: LinePos) {
self(renderer, pos);
}
}
#[derive(Default)]
pub struct DecorationManager<'a> {
decorations: Vec<(Box<dyn Decoration + 'a>, usize)>,
}
impl<'a> DecorationManager<'a> {
pub fn add_decoration(&mut self, decoration: impl Decoration + 'a) {
self.decorations.push((Box::new(decoration), 0));
}
pub fn prepare_for_rendering(&mut self, first_visible_char: usize) {
for (decoration, next_position) in &mut self.decorations {
*next_position = decoration.reset_pos(first_visible_char)
}
}
pub fn decorate_grapheme(&mut self, renderer: &mut TextRenderer, grapheme: &FormattedGrapheme) {
for (decoration, hook_char_idx) in &mut self.decorations {
loop {
match (*hook_char_idx).cmp(&grapheme.char_idx) {
// this grapheme has been concealed or we are at the first grapheme
Ordering::Less => {
*hook_char_idx = decoration.skip_concealed_anchor(grapheme.char_idx)
}
Ordering::Equal => {
*hook_char_idx = decoration.decorate_grapheme(renderer, grapheme)
}
Ordering::Greater => break,
}
}
}
}
pub fn decorate_line(&mut self, renderer: &mut TextRenderer, pos: LinePos) {
for (decoration, _) in &mut self.decorations {
decoration.decorate_line(renderer, pos);
}
}
pub fn render_virtual_lines(
&mut self,
renderer: &mut TextRenderer,
pos: LinePos,
line_width: usize,
) {
let mut virt_off = Position::new(1, line_width); // start at 1 so the line is never overwritten
for (decoration, _) in &mut self.decorations {
virt_off += decoration.render_virt_lines(renderer, pos, virt_off);
}
}
}
/// Cursor rendering is done externally so all the cursor decoration
/// does is save the position of primary cursor
pub struct Cursor<'a> {
pub cache: &'a CursorCache,
pub primary_cursor: usize,
}
impl Decoration for Cursor<'_> {
fn reset_pos(&mut self, pos: usize) -> usize {
if pos <= self.primary_cursor {
self.primary_cursor
} else {
usize::MAX
}
}
fn decorate_grapheme(
&mut self,
renderer: &mut TextRenderer,
grapheme: &FormattedGrapheme,
) -> usize {
if renderer.column_in_bounds(grapheme.visual_pos.col)
&& renderer.offset.row < grapheme.visual_pos.row
{
let position = grapheme.visual_pos - renderer.offset;
self.cache.set(Some(position));
}
usize::MAX
}
}

@ -0,0 +1,305 @@
use std::cmp::Ordering;
use helix_core::diagnostic::Severity;
use helix_core::doc_formatter::{DocumentFormatter, FormattedGrapheme};
use helix_core::graphemes::Grapheme;
use helix_core::text_annotations::TextAnnotations;
use helix_core::{Diagnostic, Position};
use helix_view::annotations::diagnostics::{
DiagnosticFilter, InlineDiagnosticAccumulator, InlineDiagnosticsConfig,
};
use helix_view::theme::Style;
use helix_view::{Document, Theme};
use crate::ui::document::{LinePos, TextRenderer};
use crate::ui::text_decorations::Decoration;
#[derive(Debug)]
struct Styles {
hint: Style,
info: Style,
warning: Style,
error: Style,
}
impl Styles {
fn new(theme: &Theme) -> Styles {
Styles {
hint: theme.get("hint"),
info: theme.get("info"),
warning: theme.get("warning"),
error: theme.get("error"),
}
}
fn severity_style(&self, severity: Severity) -> Style {
match severity {
Severity::Hint => self.hint,
Severity::Info => self.info,
Severity::Warning => self.warning,
Severity::Error => self.error,
}
}
}
pub struct InlineDiagnostics<'a> {
state: InlineDiagnosticAccumulator<'a>,
eol_diagnostics: DiagnosticFilter,
styles: Styles,
}
impl<'a> InlineDiagnostics<'a> {
pub fn new(
doc: &'a Document,
theme: &Theme,
cursor: usize,
config: InlineDiagnosticsConfig,
eol_diagnostics: DiagnosticFilter,
) -> Self {
InlineDiagnostics {
state: InlineDiagnosticAccumulator::new(cursor, doc, config),
styles: Styles::new(theme),
eol_diagnostics,
}
}
}
const BL_CORNER: &str = "┘";
const TR_CORNER: &str = "┌";
const BR_CORNER: &str = "└";
const STACK: &str = "├";
const MULTI: &str = "┴";
const HOR_BAR: &str = "─";
const VER_BAR: &str = "│";
struct Renderer<'a, 'b> {
renderer: &'a mut TextRenderer<'b>,
first_row: u16,
row: u16,
config: &'a InlineDiagnosticsConfig,
styles: &'a Styles,
}
impl Renderer<'_, '_> {
fn draw_decoration(&mut self, g: &'static str, severity: Severity, col: u16) {
self.draw_decoration_at(g, severity, col, self.row)
}
fn draw_decoration_at(&mut self, g: &'static str, severity: Severity, col: u16, row: u16) {
self.renderer.draw_decoration_grapheme(
Grapheme::new_decoration(g),
self.styles.severity_style(severity),
row,
col,
);
}
fn draw_eol_diagnostic(&mut self, diag: &Diagnostic, row: u16, col: usize) -> u16 {
let style = self.styles.severity_style(diag.severity());
let width = self.renderer.viewport.width;
if !self.renderer.column_in_bounds(col + 1) {
return 0;
}
let col = (col - self.renderer.offset.col) as u16;
let (new_col, _) = self.renderer.set_string_truncated(
self.renderer.viewport.x + col + 1,
row,
&diag.message,
width.saturating_sub(col + 1) as usize,
|_| style,
true,
false,
);
new_col - col
}
fn draw_diagnostic(&mut self, diag: &Diagnostic, col: u16, next_severity: Option<Severity>) {
let severity = diag.severity();
let (sym, sym_severity) = if let Some(next_severity) = next_severity {
(STACK, next_severity.max(severity))
} else {
(BR_CORNER, severity)
};
self.draw_decoration(sym, sym_severity, col);
for i in 0..self.config.prefix_len {
self.draw_decoration(HOR_BAR, severity, col + i + 1);
}
let text_col = col + self.config.prefix_len + 1;
let text_fmt = self.config.text_fmt(text_col, self.renderer.viewport.width);
let annotations = TextAnnotations::default();
let formatter = DocumentFormatter::new_at_prev_checkpoint(
diag.message.as_str().trim().into(),
&text_fmt,
&annotations,
0,
);
let mut last_row = 0;
let style = self.styles.severity_style(severity);
for grapheme in formatter {
last_row = grapheme.visual_pos.row;
self.renderer.draw_decoration_grapheme(
grapheme.raw,
style,
self.row + grapheme.visual_pos.row as u16,
text_col + grapheme.visual_pos.col as u16,
);
}
self.row += 1;
// height is last_row + 1 and extra_rows is height - 1
let extra_lines = last_row;
if let Some(next_severity) = next_severity {
for _ in 0..extra_lines {
self.draw_decoration(VER_BAR, next_severity, col);
self.row += 1;
}
} else {
self.row += extra_lines as u16;
}
}
fn draw_multi_diagnostics(&mut self, stack: &mut Vec<(&Diagnostic, u16)>) {
let Some(&(last_diag, last_anchor)) = stack.last() else {
return;
};
let start = self
.config
.max_diagnostic_start(self.renderer.viewport.width);
if last_anchor <= start {
return;
}
let mut severity = last_diag.severity();
let mut last_anchor = last_anchor;
self.draw_decoration(BL_CORNER, severity, last_anchor);
let mut stacked_diagnostics = 1;
for &(diag, anchor) in stack.iter().rev().skip(1) {
let sym = match anchor.cmp(&start) {
Ordering::Less => break,
Ordering::Equal => STACK,
Ordering::Greater => MULTI,
};
stacked_diagnostics += 1;
severity = severity.max(diag.severity());
let old_severity = severity;
if anchor == last_anchor && severity == old_severity {
continue;
}
for col in (anchor + 1)..last_anchor {
self.draw_decoration(HOR_BAR, old_severity, col)
}
self.draw_decoration(sym, severity, anchor);
last_anchor = anchor;
}
// if no diagnostic anchor was found exactly at the start of the
// diagnostic text draw an upwards corner and ensure the last piece
// of the line is not missing
if last_anchor != start {
for col in (start + 1)..last_anchor {
self.draw_decoration(HOR_BAR, severity, col)
}
self.draw_decoration(TR_CORNER, severity, start)
}
self.row += 1;
let stacked_diagnostics = &stack[stack.len() - stacked_diagnostics..];
for (i, (diag, _)) in stacked_diagnostics.iter().rev().enumerate() {
let next_severity = stacked_diagnostics[..stacked_diagnostics.len() - i - 1]
.iter()
.map(|(diag, _)| diag.severity())
.max();
self.draw_diagnostic(diag, start, next_severity);
}
stack.truncate(stack.len() - stacked_diagnostics.len());
}
fn draw_diagnostics(&mut self, stack: &mut Vec<(&Diagnostic, u16)>) {
let mut stack = stack.drain(..).rev().peekable();
let mut last_anchor = self.renderer.viewport.width;
while let Some((diag, anchor)) = stack.next() {
if anchor != last_anchor {
for row in self.first_row..self.row {
self.draw_decoration_at(VER_BAR, diag.severity(), anchor, row);
}
}
let next_severity = stack.peek().and_then(|&(diag, next_anchor)| {
(next_anchor == anchor).then_some(diag.severity())
});
self.draw_diagnostic(diag, anchor, next_severity);
last_anchor = anchor;
}
}
}
impl Decoration for InlineDiagnostics<'_> {
fn render_virt_lines(
&mut self,
renderer: &mut TextRenderer,
pos: LinePos,
virt_off: Position,
) -> Position {
let mut col_off = 0;
let filter = self.state.filter();
let eol_diagnostic = match self.eol_diagnostics {
DiagnosticFilter::Enable(eol_filter) => {
let eol_diganogistcs = self
.state
.stack
.iter()
.filter(|(diag, _)| eol_filter <= diag.severity());
match filter {
DiagnosticFilter::Enable(filter) => eol_diganogistcs
.filter(|(diag, _)| filter > diag.severity())
.max_by_key(|(diagnostic, _)| diagnostic.severity),
DiagnosticFilter::Disable => {
eol_diganogistcs.max_by_key(|(diagnostic, _)| diagnostic.severity)
}
}
}
DiagnosticFilter::Disable => None,
};
if let Some((eol_diagnostic, _)) = eol_diagnostic {
let mut renderer = Renderer {
renderer,
first_row: pos.visual_line,
row: pos.visual_line,
config: &self.state.config,
styles: &self.styles,
};
col_off = renderer.draw_eol_diagnostic(eol_diagnostic, pos.visual_line, virt_off.col);
}
self.state.compute_line_diagnostics();
let mut renderer = Renderer {
renderer,
first_row: pos.visual_line + virt_off.row as u16,
row: pos.visual_line + virt_off.row as u16,
config: &self.state.config,
styles: &self.styles,
};
renderer.draw_multi_diagnostics(&mut self.state.stack);
renderer.draw_diagnostics(&mut self.state.stack);
let horizontal_off = renderer.row - renderer.first_row;
Position::new(horizontal_off as usize, col_off as usize)
}
fn reset_pos(&mut self, pos: usize) -> usize {
self.state.reset_pos(pos)
}
fn skip_concealed_anchor(&mut self, conceal_end_char_idx: usize) -> usize {
self.state.skip_concealed(conceal_end_char_idx)
}
fn decorate_grapheme(
&mut self,
renderer: &mut TextRenderer,
grapheme: &FormattedGrapheme,
) -> usize {
self.state
.proccess_anchor(grapheme, renderer.viewport.width, renderer.offset.col)
}
}

@ -0,0 +1 @@
pub mod diagnostics;

@ -0,0 +1,313 @@
use helix_core::diagnostic::Severity;
use helix_core::doc_formatter::{FormattedGrapheme, TextFormat};
use helix_core::text_annotations::LineAnnotation;
use helix_core::{softwrapped_dimensions, Diagnostic, Position};
use serde::{Deserialize, Serialize};
use crate::Document;
/// Describes the severity level of a [`Diagnostic`].
#[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord)]
pub enum DiagnosticFilter {
Disable,
Enable(Severity),
}
impl<'de> Deserialize<'de> for DiagnosticFilter {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
match &*String::deserialize(deserializer)? {
"disable" => Ok(DiagnosticFilter::Disable),
"hint" => Ok(DiagnosticFilter::Enable(Severity::Hint)),
"info" => Ok(DiagnosticFilter::Enable(Severity::Info)),
"warning" => Ok(DiagnosticFilter::Enable(Severity::Warning)),
"error" => Ok(DiagnosticFilter::Enable(Severity::Error)),
variant => Err(serde::de::Error::unknown_variant(
variant,
&["disable", "hint", "info", "warning", "error"],
)),
}
}
}
impl Serialize for DiagnosticFilter {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let filter = match self {
DiagnosticFilter::Disable => "disable",
DiagnosticFilter::Enable(Severity::Hint) => "hint",
DiagnosticFilter::Enable(Severity::Info) => "info",
DiagnosticFilter::Enable(Severity::Warning) => "warning",
DiagnosticFilter::Enable(Severity::Error) => "error",
};
filter.serialize(serializer)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
pub struct InlineDiagnosticsConfig {
pub cursor_line: DiagnosticFilter,
pub other_lines: DiagnosticFilter,
pub min_diagnostic_width: u16,
pub prefix_len: u16,
pub max_wrap: u16,
pub max_diagnostics: usize,
}
impl InlineDiagnosticsConfig {
pub fn disabled(&self) -> bool {
matches!(
self,
Self {
cursor_line: DiagnosticFilter::Disable,
other_lines: DiagnosticFilter::Disable,
..
}
)
}
pub fn prepare(&self, width: u16, enable_cursor_line: bool) -> Self {
let mut config = self.clone();
if width < self.min_diagnostic_width + self.prefix_len {
config.cursor_line = DiagnosticFilter::Disable;
config.other_lines = DiagnosticFilter::Disable;
} else if !enable_cursor_line {
config.cursor_line = self.cursor_line.min(self.other_lines);
}
config
}
pub fn max_diagnostic_start(&self, width: u16) -> u16 {
width - self.min_diagnostic_width - self.prefix_len
}
pub fn text_fmt(&self, anchor_col: u16, width: u16) -> TextFormat {
let width = if anchor_col > self.max_diagnostic_start(width) {
self.min_diagnostic_width
} else {
width - anchor_col - self.prefix_len
};
TextFormat {
soft_wrap: true,
tab_width: 4,
max_wrap: self.max_wrap.min(width / 4),
max_indent_retain: 0,
wrap_indicator: "".into(),
wrap_indicator_highlight: None,
viewport_width: width,
soft_wrap_at_text_width: true,
}
}
}
impl Default for InlineDiagnosticsConfig {
fn default() -> Self {
InlineDiagnosticsConfig {
cursor_line: DiagnosticFilter::Disable,
other_lines: DiagnosticFilter::Disable,
min_diagnostic_width: 40,
prefix_len: 1,
max_wrap: 20,
max_diagnostics: 10,
}
}
}
pub struct InlineDiagnosticAccumulator<'a> {
idx: usize,
doc: &'a Document,
pub stack: Vec<(&'a Diagnostic, u16)>,
pub config: InlineDiagnosticsConfig,
cursor: usize,
cursor_line: bool,
}
impl<'a> InlineDiagnosticAccumulator<'a> {
pub fn new(cursor: usize, doc: &'a Document, config: InlineDiagnosticsConfig) -> Self {
InlineDiagnosticAccumulator {
idx: 0,
doc,
stack: Vec::new(),
config,
cursor,
cursor_line: false,
}
}
pub fn reset_pos(&mut self, char_idx: usize) -> usize {
self.idx = 0;
self.clear();
self.skip_concealed(char_idx)
}
pub fn skip_concealed(&mut self, conceal_end_char_idx: usize) -> usize {
let diagnostics = &self.doc.diagnostics[self.idx..];
let idx = diagnostics.partition_point(|diag| diag.range.start < conceal_end_char_idx);
self.idx += idx;
self.next_anchor(conceal_end_char_idx)
}
pub fn next_anchor(&self, current_char_idx: usize) -> usize {
let next_diag_start = self
.doc
.diagnostics
.get(self.idx)
.map_or(usize::MAX, |diag| diag.range.start);
if (current_char_idx..next_diag_start).contains(&self.cursor) {
self.cursor
} else {
next_diag_start
}
}
pub fn clear(&mut self) {
self.cursor_line = false;
self.stack.clear();
}
fn process_anchor_impl(
&mut self,
grapheme: &FormattedGrapheme,
width: u16,
horizontal_off: usize,
) -> bool {
// TODO: doing the cursor tracking here works well but is somewhat
// duplicate effort/tedious maybe centralize this somehwere?
// In the DocFormatter?
if grapheme.char_idx == self.cursor {
self.cursor_line = true;
if self
.doc
.diagnostics
.get(self.idx)
.map_or(true, |diag| diag.range.start != grapheme.char_idx)
{
return false;
}
}
let Some(anchor_col) = grapheme.visual_pos.col.checked_sub(horizontal_off) else {
return true;
};
if anchor_col >= width as usize {
return true;
}
for diag in &self.doc.diagnostics[self.idx..] {
if diag.range.start != grapheme.char_idx {
break;
}
self.stack.push((diag, anchor_col as u16));
self.idx += 1;
}
false
}
pub fn proccess_anchor(
&mut self,
grapheme: &FormattedGrapheme,
width: u16,
horizontal_off: usize,
) -> usize {
if self.process_anchor_impl(grapheme, width, horizontal_off) {
self.idx += self.doc.diagnostics[self.idx..]
.iter()
.take_while(|diag| diag.range.start == grapheme.char_idx)
.count();
}
self.next_anchor(grapheme.char_idx + 1)
}
pub fn filter(&self) -> DiagnosticFilter {
if self.cursor_line {
self.config.cursor_line
} else {
self.config.other_lines
}
}
pub fn compute_line_diagnostics(&mut self) {
let filter = if self.cursor_line {
self.cursor_line = false;
self.config.cursor_line
} else {
self.config.other_lines
};
let DiagnosticFilter::Enable(filter) = filter else {
self.stack.clear();
return;
};
self.stack.retain(|(diag, _)| diag.severity() >= filter);
self.stack.truncate(self.config.max_diagnostics)
}
pub fn has_multi(&self, width: u16) -> bool {
self.stack.last().map_or(false, |&(_, anchor)| {
anchor > self.config.max_diagnostic_start(width)
})
}
}
pub(crate) struct InlineDiagnostics<'a> {
state: InlineDiagnosticAccumulator<'a>,
width: u16,
horizontal_off: usize,
}
impl<'a> InlineDiagnostics<'a> {
#[allow(clippy::new_ret_no_self)]
pub(crate) fn new(
doc: &'a Document,
cursor: usize,
width: u16,
horizontal_off: usize,
config: InlineDiagnosticsConfig,
) -> Box<dyn LineAnnotation + 'a> {
Box::new(InlineDiagnostics {
state: InlineDiagnosticAccumulator::new(cursor, doc, config),
width,
horizontal_off,
})
}
}
impl LineAnnotation for InlineDiagnostics<'_> {
fn reset_pos(&mut self, char_idx: usize) -> usize {
self.state.reset_pos(char_idx)
}
fn skip_concealed_anchors(&mut self, conceal_end_char_idx: usize) -> usize {
self.state.skip_concealed(conceal_end_char_idx)
}
fn process_anchor(&mut self, grapheme: &FormattedGrapheme) -> usize {
self.state
.proccess_anchor(grapheme, self.width, self.horizontal_off)
}
fn insert_virtual_lines(
&mut self,
_line_end_char_idx: usize,
_line_end_visual_pos: Position,
_doc_line: usize,
) -> Position {
self.state.compute_line_diagnostics();
let multi = self.state.has_multi(self.width);
let diagostic_height: usize = self
.state
.stack
.drain(..)
.map(|(diag, anchor)| {
let text_fmt = self.state.config.text_fmt(anchor, self.width);
softwrapped_dimensions(diag.message.as_str().trim().into(), &text_fmt).0
})
.sum();
Position::new(multi as usize + diagostic_height, 0)
}
}

@ -99,7 +99,6 @@ impl Serialize for Mode {
serializer.collect_str(self) serializer.collect_str(self)
} }
} }
/// A snapshot of the text of a document that we want to write out to disk /// A snapshot of the text of a document that we want to write out to disk
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct DocumentSavedEvent { pub struct DocumentSavedEvent {
@ -1321,7 +1320,7 @@ impl Document {
true true
}); });
self.diagnostics.sort_unstable_by_key(|diagnostic| { self.diagnostics.sort_by_key(|diagnostic| {
(diagnostic.range, diagnostic.severity, diagnostic.provider) (diagnostic.range, diagnostic.severity, diagnostic.provider)
}); });
@ -1911,9 +1910,8 @@ impl Document {
}); });
} }
self.diagnostics.extend(diagnostics); self.diagnostics.extend(diagnostics);
self.diagnostics.sort_unstable_by_key(|diagnostic| { self.diagnostics
(diagnostic.range, diagnostic.severity, diagnostic.provider) .sort_by_key(|diagnostic| (diagnostic.range, diagnostic.severity, diagnostic.provider));
});
} }
/// clears diagnostics for a given language server id if set, otherwise all diagnostics are cleared /// clears diagnostics for a given language server id if set, otherwise all diagnostics are cleared
@ -1953,7 +1951,7 @@ impl Document {
.language_config() .language_config()
.and_then(|config| config.text_width) .and_then(|config| config.text_width)
.unwrap_or(config.text_width); .unwrap_or(config.text_width);
let soft_wrap_at_text_width = self let mut soft_wrap_at_text_width = self
.language_config() .language_config()
.and_then(|config| { .and_then(|config| {
config config
@ -1964,12 +1962,13 @@ impl Document {
.or(config.soft_wrap.wrap_at_text_width) .or(config.soft_wrap.wrap_at_text_width)
.unwrap_or(false); .unwrap_or(false);
if soft_wrap_at_text_width { if soft_wrap_at_text_width {
// We increase max_line_len by 1 because softwrap considers the newline character // if the viewport is smaller than the specified
// as part of the line length while the "typical" expectation is that this is not the case. // width then this setting has no effcet
// In particular other commands like :reflow do not count the line terminator. if text_width >= viewport_width as usize {
// This is technically inconsistent for the last line as that line never has a line terminator soft_wrap_at_text_width = false;
// but having the last visual line exceed the width by 1 seems like a rare edge case. } else {
viewport_width = viewport_width.min(text_width as u16 + 1) viewport_width = text_width as u16;
}
} }
let config = self.config.load(); let config = self.config.load();
let editor_soft_wrap = &config.soft_wrap; let editor_soft_wrap = &config.soft_wrap;
@ -2006,6 +2005,7 @@ impl Document {
wrap_indicator_highlight: theme wrap_indicator_highlight: theme
.and_then(|theme| theme.find_scope_index("ui.virtual.wrap")) .and_then(|theme| theme.find_scope_index("ui.virtual.wrap"))
.map(Highlight), .map(Highlight),
soft_wrap_at_text_width,
} }
} }

@ -1,5 +1,6 @@
use crate::{ use crate::{
align_view, align_view,
annotations::diagnostics::{DiagnosticFilter, InlineDiagnosticsConfig},
document::{ document::{
DocumentOpenError, DocumentSavedEventFuture, DocumentSavedEventResult, Mode, SavePoint, DocumentOpenError, DocumentSavedEventFuture, DocumentSavedEventResult, Mode, SavePoint,
}, },
@ -343,6 +344,9 @@ pub struct Config {
deserialize_with = "deserialize_alphabet" deserialize_with = "deserialize_alphabet"
)] )]
pub jump_label_alphabet: Vec<char>, pub jump_label_alphabet: Vec<char>,
/// Display diagnostic below the line they occur.
pub inline_diagnostics: InlineDiagnosticsConfig,
pub end_of_line_diagnostics: DiagnosticFilter,
} }
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Eq, PartialOrd, Ord)] #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Eq, PartialOrd, Ord)]
@ -975,6 +979,8 @@ impl Default for Config {
popup_border: PopupBorderConfig::None, popup_border: PopupBorderConfig::None,
indent_heuristic: IndentationHeuristic::default(), indent_heuristic: IndentationHeuristic::default(),
jump_label_alphabet: ('a'..='z').collect(), jump_label_alphabet: ('a'..='z').collect(),
inline_diagnostics: InlineDiagnosticsConfig::default(),
end_of_line_diagnostics: DiagnosticFilter::Disable,
} }
} }
} }
@ -1070,10 +1076,10 @@ pub struct Editor {
/// This cache is only a performance optimization to /// This cache is only a performance optimization to
/// avoid calculating the cursor position multiple /// avoid calculating the cursor position multiple
/// times during rendering and should not be set by other functions. /// times during rendering and should not be set by other functions.
pub cursor_cache: Cell<Option<Option<Position>>>,
pub handlers: Handlers, pub handlers: Handlers,
pub mouse_down_range: Option<Range>, pub mouse_down_range: Option<Range>,
pub cursor_cache: CursorCache,
} }
pub type Motion = Box<dyn Fn(&mut Editor)>; pub type Motion = Box<dyn Fn(&mut Editor)>;
@ -1188,9 +1194,9 @@ impl Editor {
exit_code: 0, exit_code: 0,
config_events: unbounded_channel(), config_events: unbounded_channel(),
needs_redraw: false, needs_redraw: false,
cursor_cache: Cell::new(None),
handlers, handlers,
mouse_down_range: None, mouse_down_range: None,
cursor_cache: CursorCache::default(),
} }
} }
@ -1985,15 +1991,7 @@ impl Editor {
pub fn cursor(&self) -> (Option<Position>, CursorKind) { pub fn cursor(&self) -> (Option<Position>, CursorKind) {
let config = self.config(); let config = self.config();
let (view, doc) = current_ref!(self); let (view, doc) = current_ref!(self);
let cursor = doc if let Some(mut pos) = self.cursor_cache.get(view, doc) {
.selection(view.id)
.primary()
.cursor(doc.text().slice(..));
let pos = self
.cursor_cache
.get()
.unwrap_or_else(|| view.screen_coords_at_pos(doc, doc.text().slice(..), cursor));
if let Some(mut pos) = pos {
let inner = view.inner_area(doc); let inner = view.inner_area(doc);
pos.col += inner.x as usize; pos.col += inner.x as usize;
pos.row += inner.y as usize; pos.row += inner.y as usize;
@ -2188,3 +2186,28 @@ fn try_restore_indent(doc: &mut Document, view: &mut View) {
doc.apply(&transaction, view.id); doc.apply(&transaction, view.id);
} }
} }
#[derive(Default)]
pub struct CursorCache(Cell<Option<Option<Position>>>);
impl CursorCache {
pub fn get(&self, view: &View, doc: &Document) -> Option<Position> {
if let Some(pos) = self.0.get() {
return pos;
}
let text = doc.text().slice(..);
let cursor = doc.selection(view.id).primary().cursor(text);
let res = view.screen_coords_at_pos(doc, text, cursor);
self.set(res);
res
}
pub fn set(&self, cursor_pos: Option<Position>) {
self.0.set(Some(cursor_pos))
}
pub fn reset(&self) {
self.0.set(None)
}
}

@ -1,9 +1,10 @@
use helix_core::Rope; use helix_core::Rope;
use helix_event::events; use helix_event::events;
use crate::{Document, ViewId}; use crate::{Document, DocumentId, Editor, ViewId};
events! { events! {
DocumentDidChange<'a> { doc: &'a mut Document, view: ViewId, old_text: &'a Rope } DocumentDidChange<'a> { doc: &'a mut Document, view: ViewId, old_text: &'a Rope }
SelectionDidChange<'a> { doc: &'a mut Document, view: ViewId } SelectionDidChange<'a> { doc: &'a mut Document, view: ViewId }
DiagnosticsDidChange<'a> { editor: &'a mut Editor, doc: DocumentId }
} }

@ -5,6 +5,7 @@ use crate::handlers::lsp::SignatureHelpInvoked;
use crate::{DocumentId, Editor, ViewId}; use crate::{DocumentId, Editor, ViewId};
pub mod dap; pub mod dap;
pub mod diagnostics;
pub mod lsp; pub mod lsp;
#[derive(Debug)] #[derive(Debug)]

@ -0,0 +1,131 @@
use std::cell::Cell;
use std::num::NonZeroUsize;
use std::sync::atomic::{self, AtomicUsize};
use std::sync::Arc;
use std::time::Duration;
use helix_event::{request_redraw, send_blocking, AsyncHook};
use tokio::sync::mpsc::Sender;
use tokio::time::Instant;
use crate::{Document, DocumentId, ViewId};
#[derive(Debug)]
pub enum DiagnosticEvent {
CursorLineChanged { generation: usize },
Refresh,
}
struct DiagnosticTimeout {
active_generation: Arc<AtomicUsize>,
generation: usize,
}
const TIMEOUT: Duration = Duration::from_millis(350);
impl AsyncHook for DiagnosticTimeout {
type Event = DiagnosticEvent;
fn handle_event(
&mut self,
event: DiagnosticEvent,
timeout: Option<Instant>,
) -> Option<Instant> {
match event {
DiagnosticEvent::CursorLineChanged { generation } => {
if generation > self.generation {
self.generation = generation;
Some(Instant::now() + TIMEOUT)
} else {
timeout
}
}
DiagnosticEvent::Refresh if timeout.is_some() => Some(Instant::now() + TIMEOUT),
DiagnosticEvent::Refresh => None,
}
}
fn finish_debounce(&mut self) {
if self.active_generation.load(atomic::Ordering::Relaxed) < self.generation {
self.active_generation
.store(self.generation, atomic::Ordering::Relaxed);
request_redraw();
}
}
}
pub struct DiagnosticsHandler {
active_generation: Arc<AtomicUsize>,
generation: Cell<usize>,
last_doc: Cell<DocumentId>,
last_cursor_line: Cell<usize>,
pub active: bool,
pub events: Sender<DiagnosticEvent>,
}
// make sure we never share handlers across multiple views this is a stop
// gap solution. We just shouldn't be cloneing a view to begin with (we do
// for :hsplit/vsplit) and really this should not be view specific to begin with
// but to fix that larger architecutre changes are needed
impl Clone for DiagnosticsHandler {
fn clone(&self) -> Self {
Self::new()
}
}
impl DiagnosticsHandler {
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
let active_generation = Arc::new(AtomicUsize::new(0));
let events = DiagnosticTimeout {
active_generation: active_generation.clone(),
generation: 0,
}
.spawn();
Self {
active_generation,
generation: Cell::new(0),
events,
last_doc: Cell::new(DocumentId(NonZeroUsize::new(usize::MAX).unwrap())),
last_cursor_line: Cell::new(usize::MAX),
active: true,
}
}
}
impl DiagnosticsHandler {
pub fn immediately_show_diagnostic(&self, doc: &Document, view: ViewId) {
self.last_doc.set(doc.id());
let cursor_line = doc
.selection(view)
.primary()
.cursor_line(doc.text().slice(..));
self.last_cursor_line.set(cursor_line);
self.active_generation
.store(self.generation.get(), atomic::Ordering::Relaxed);
}
pub fn show_cursorline_diagnostics(&self, doc: &Document, view: ViewId) -> bool {
if !self.active {
return false;
}
let cursor_line = doc
.selection(view)
.primary()
.cursor_line(doc.text().slice(..));
if self.last_cursor_line.get() == cursor_line && self.last_doc.get() == doc.id() {
let active_generation = self.active_generation.load(atomic::Ordering::Relaxed);
self.generation.get() == active_generation
} else {
self.last_doc.set(doc.id());
self.last_cursor_line.set(cursor_line);
self.generation.set(self.generation.get() + 1);
send_blocking(
&self.events,
DiagnosticEvent::CursorLineChanged {
generation: self.generation.get(),
},
);
false
}
}
}

@ -1,6 +1,7 @@
#[macro_use] #[macro_use]
pub mod macros; pub mod macros;
pub mod annotations;
pub mod base64; pub mod base64;
pub mod clipboard; pub mod clipboard;
pub mod document; pub mod document;

@ -1,8 +1,10 @@
use crate::{ use crate::{
align_view, align_view,
annotations::diagnostics::InlineDiagnostics,
document::DocumentInlayHints, document::DocumentInlayHints,
editor::{GutterConfig, GutterType}, editor::{GutterConfig, GutterType},
graphics::Rect, graphics::Rect,
handlers::diagnostics::DiagnosticsHandler,
Align, Document, DocumentId, Theme, ViewId, Align, Document, DocumentId, Theme, ViewId,
}; };
@ -146,6 +148,14 @@ pub struct View {
/// mapping keeps track of the last applied history revision so that only new changes /// mapping keeps track of the last applied history revision so that only new changes
/// are applied. /// are applied.
doc_revisions: HashMap<DocumentId, usize>, doc_revisions: HashMap<DocumentId, usize>,
// HACKS: there should really only be a global diagnostics handler (the
// non-focused views should just not have different handling for the cursor
// line). For that we would need accces to editor everywhere (we want to use
// the positioning code) so this can only happen by refactoring View and
// Document into entity component like structure. That is a huge refactor
// left to future work. For now we treat all views as focused and give them
// each their own handler.
pub diagnostics_handler: DiagnosticsHandler,
} }
impl fmt::Debug for View { impl fmt::Debug for View {
@ -175,6 +185,7 @@ impl View {
object_selections: Vec::new(), object_selections: Vec::new(),
gutters, gutters,
doc_revisions: HashMap::new(), doc_revisions: HashMap::new(),
diagnostics_handler: DiagnosticsHandler::new(),
} }
} }
@ -392,11 +403,6 @@ impl View {
text: RopeSlice, text: RopeSlice,
pos: usize, pos: usize,
) -> Option<Position> { ) -> Option<Position> {
if pos < self.offset.anchor {
// Line is not visible on screen
return None;
}
let viewport = self.inner_area(doc); let viewport = self.inner_area(doc);
let text_fmt = doc.text_format(viewport.width, None); let text_fmt = doc.text_format(viewport.width, None);
let annotations = self.text_annotations(doc, None); let annotations = self.text_annotations(doc, None);
@ -438,37 +444,54 @@ impl View {
text_annotations.add_overlay(labels, style); text_annotations.add_overlay(labels, style);
} }
let DocumentInlayHints { if let Some(DocumentInlayHints {
id: _, id: _,
type_inlay_hints, type_inlay_hints,
parameter_inlay_hints, parameter_inlay_hints,
other_inlay_hints, other_inlay_hints,
padding_before_inlay_hints, padding_before_inlay_hints,
padding_after_inlay_hints, padding_after_inlay_hints,
} = match doc.inlay_hints.get(&self.id) { }) = doc.inlay_hints.get(&self.id)
Some(doc_inlay_hints) => doc_inlay_hints, {
None => return text_annotations, let type_style = theme
}; .and_then(|t| t.find_scope_index("ui.virtual.inlay-hint.type"))
.map(Highlight);
let parameter_style = theme
.and_then(|t| t.find_scope_index("ui.virtual.inlay-hint.parameter"))
.map(Highlight);
let other_style = theme
.and_then(|t| t.find_scope_index("ui.virtual.inlay-hint"))
.map(Highlight);
let type_style = theme // Overlapping annotations are ignored apart from the first so the order here is not random:
.and_then(|t| t.find_scope_index("ui.virtual.inlay-hint.type")) // types -> parameters -> others should hopefully be the "correct" order for most use cases,
.map(Highlight); // with the padding coming before and after as expected.
let parameter_style = theme text_annotations
.and_then(|t| t.find_scope_index("ui.virtual.inlay-hint.parameter")) .add_inline_annotations(padding_before_inlay_hints, None)
.map(Highlight); .add_inline_annotations(type_inlay_hints, type_style)
let other_style = theme .add_inline_annotations(parameter_inlay_hints, parameter_style)
.and_then(|t| t.find_scope_index("ui.virtual.inlay-hint")) .add_inline_annotations(other_inlay_hints, other_style)
.map(Highlight); .add_inline_annotations(padding_after_inlay_hints, None);
};
// Overlapping annotations are ignored apart from the first so the order here is not random: let config = doc.config.load();
// types -> parameters -> others should hopefully be the "correct" order for most use cases, let width = self.inner_width(doc);
// with the padding coming before and after as expected. let enable_cursor_line = self
text_annotations .diagnostics_handler
.add_inline_annotations(padding_before_inlay_hints, None) .show_cursorline_diagnostics(doc, self.id);
.add_inline_annotations(type_inlay_hints, type_style) let config = config.inline_diagnostics.prepare(width, enable_cursor_line);
.add_inline_annotations(parameter_inlay_hints, parameter_style) if !config.disabled() {
.add_inline_annotations(other_inlay_hints, other_style) let cursor = doc
.add_inline_annotations(padding_after_inlay_hints, None); .selection(self.id)
.primary()
.cursor(doc.text().slice(..));
text_annotations.add_line_annotation(InlineDiagnostics::new(
doc,
cursor,
width,
self.offset.horizontal_offset,
config,
));
}
text_annotations text_annotations
} }

Loading…
Cancel
Save