feat: enable LSP semantic token highlighting by default
This commit is contained in:
+187
-22
@@ -31,7 +31,9 @@ use unicode_width::UnicodeWidthStr;
|
||||
use crate::{
|
||||
editor::{Editor, Hover, Input, Mode, Palette, StartupDiagnostic},
|
||||
files::{DockSide, FilePicker, FilePresentation, FileView},
|
||||
plugin::{Diagnostic, DiagnosticSeverity},
|
||||
plugin::{
|
||||
Diagnostic, DiagnosticSeverity, SemanticToken, SemanticTokenKind, SemanticTokenModifier,
|
||||
},
|
||||
session::EditorSession,
|
||||
syntax::{SyntaxCache, SyntaxKind},
|
||||
};
|
||||
@@ -345,15 +347,25 @@ fn draw(
|
||||
.collect();
|
||||
frame.render_widget(Paragraph::new(lines), viewport);
|
||||
let buffer = frame.buffer_mut();
|
||||
let semantic = editor.semantic_tokens();
|
||||
let mut semantic_index = semantic.partition_point(|token| token.start.row < *top);
|
||||
for (row, line) in editor.lines().iter().enumerate().skip(*top).take(height) {
|
||||
let spans = syntax.line(row);
|
||||
if spans.is_empty() {
|
||||
while semantic_index < semantic.len() && semantic[semantic_index].start.row < row {
|
||||
semantic_index += 1;
|
||||
}
|
||||
if spans.is_empty()
|
||||
&& semantic
|
||||
.get(semantic_index)
|
||||
.is_none_or(|token| token.start.row != row)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let mut span_index = 0;
|
||||
let mut token_style = None;
|
||||
let mut column = 0;
|
||||
let right = left.saturating_add(width);
|
||||
for (byte, grapheme) in line.grapheme_indices(true) {
|
||||
for (index, (byte, grapheme)) in line.grapheme_indices(true).enumerate() {
|
||||
if column >= right {
|
||||
break;
|
||||
}
|
||||
@@ -366,22 +378,39 @@ fn draw(
|
||||
while span_index < spans.len() && spans[span_index].end_byte <= byte {
|
||||
span_index += 1;
|
||||
}
|
||||
if let Some(span) = spans.get(span_index)
|
||||
&& span.start_byte <= byte
|
||||
while semantic_index < semantic.len()
|
||||
&& semantic[semantic_index].start.row == row
|
||||
&& semantic[semantic_index].end.column <= index
|
||||
{
|
||||
semantic_index += 1;
|
||||
token_style = None;
|
||||
}
|
||||
let style = semantic
|
||||
.get(semantic_index)
|
||||
.filter(|token| token.start.row == row && token.start.column <= index)
|
||||
.map(|token| *token_style.get_or_insert_with(|| semantic_style(token)))
|
||||
.or_else(|| {
|
||||
spans
|
||||
.get(span_index)
|
||||
.filter(|span| span.start_byte <= byte)
|
||||
.map(|span| {
|
||||
Style::default().fg(match span.kind {
|
||||
SyntaxKind::Comment => Color::DarkGray,
|
||||
SyntaxKind::String => Color::Green,
|
||||
SyntaxKind::Keyword => Color::Magenta,
|
||||
SyntaxKind::Number => Color::Yellow,
|
||||
SyntaxKind::Type | SyntaxKind::Markup => Color::Cyan,
|
||||
SyntaxKind::Function => Color::LightBlue,
|
||||
})
|
||||
})
|
||||
});
|
||||
if let Some(style) = style
|
||||
&& end > *left
|
||||
&& (grapheme == "\t" || (column >= *left && end <= right))
|
||||
{
|
||||
let color = match span.kind {
|
||||
SyntaxKind::Comment => Color::DarkGray,
|
||||
SyntaxKind::String => Color::Green,
|
||||
SyntaxKind::Keyword => Color::Magenta,
|
||||
SyntaxKind::Number => Color::Yellow,
|
||||
SyntaxKind::Type | SyntaxKind::Markup => Color::Cyan,
|
||||
SyntaxKind::Function => Color::LightBlue,
|
||||
};
|
||||
let y = viewport.y + (row - *top) as u16;
|
||||
for cell in column.max(*left)..end.min(right) {
|
||||
buffer[(viewport.x + (cell - *left) as u16, y)].set_fg(color);
|
||||
buffer[(viewport.x + (cell - *left) as u16, y)].set_style(style);
|
||||
}
|
||||
}
|
||||
column = end;
|
||||
@@ -1044,6 +1073,36 @@ fn draw_palette(frame: &mut Frame, palette: &Palette, mode: Mode, navigation_hin
|
||||
);
|
||||
}
|
||||
|
||||
fn semantic_style(token: &SemanticToken) -> Style {
|
||||
use SemanticTokenKind::*;
|
||||
let readonly = token.modifiers & SemanticTokenModifier::Readonly as u16 != 0;
|
||||
let color = match token.kind {
|
||||
Namespace | Type | Class | Enum | Interface | Struct | TypeParameter => Color::Cyan,
|
||||
Function | Method | Event | Macro => Color::LightBlue,
|
||||
Parameter | Variable | Property if readonly => Color::Yellow,
|
||||
Parameter | Variable => Color::LightCyan,
|
||||
Property => Color::Blue,
|
||||
EnumMember | Number => Color::Yellow,
|
||||
Keyword | Modifier | Operator | Decorator => Color::Magenta,
|
||||
String | Regexp => Color::Green,
|
||||
Comment => Color::DarkGray,
|
||||
};
|
||||
let mut style = Style::default().fg(color);
|
||||
if token.modifiers
|
||||
& (SemanticTokenModifier::Declaration as u16 | SemanticTokenModifier::Definition as u16)
|
||||
!= 0
|
||||
{
|
||||
style = style.add_modifier(ratatui::style::Modifier::BOLD);
|
||||
}
|
||||
if token.kind == Comment && token.modifiers & SemanticTokenModifier::Documentation as u16 != 0 {
|
||||
style = style.add_modifier(ratatui::style::Modifier::ITALIC);
|
||||
}
|
||||
if token.modifiers & SemanticTokenModifier::Deprecated as u16 != 0 {
|
||||
style = style.add_modifier(ratatui::style::Modifier::CROSSED_OUT);
|
||||
}
|
||||
style
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -1150,6 +1209,21 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn semantic_token(
|
||||
row: usize,
|
||||
start: usize,
|
||||
end: usize,
|
||||
kind: SemanticTokenKind,
|
||||
modifiers: u16,
|
||||
) -> SemanticToken {
|
||||
SemanticToken {
|
||||
start: crate::plugin::TextPosition { row, column: start },
|
||||
end: crate::plugin::TextPosition { row, column: end },
|
||||
kind,
|
||||
modifiers,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diagnostic_underlines_preserve_syntax_and_whole_graphemes_at_viewport_edges() {
|
||||
let directory = Directory::new();
|
||||
@@ -1579,15 +1653,54 @@ end
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn syntax_colors_whole_unicode_graphemes_and_clipped_tabs_without_painting_wide_blanks() {
|
||||
fn semantic_colors_modifiers_and_syntax_fallback_cover_whole_graphemes_and_clipped_tabs() {
|
||||
let directory = Directory::new();
|
||||
let source = "#a\t界e\u{301}👩🚀z";
|
||||
let mut editor = Editor::open(Some(directory.file("unicode.py", source)), vec![]).unwrap();
|
||||
editor.set_clipboard_mode("separate").unwrap();
|
||||
editor
|
||||
.replace_semantic_tokens(
|
||||
editor.buffer_id(),
|
||||
editor.text_revision(),
|
||||
Some(Arc::from([
|
||||
semantic_token(
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
SemanticTokenKind::Comment,
|
||||
SemanticTokenModifier::Documentation as u16,
|
||||
),
|
||||
semantic_token(
|
||||
0,
|
||||
2,
|
||||
6,
|
||||
SemanticTokenKind::Property,
|
||||
SemanticTokenModifier::Readonly as u16
|
||||
| SemanticTokenModifier::Definition as u16
|
||||
| SemanticTokenModifier::Deprecated as u16,
|
||||
),
|
||||
semantic_token(0, 2, 3, SemanticTokenKind::Variable, 0),
|
||||
])),
|
||||
)
|
||||
.unwrap();
|
||||
editor
|
||||
.replace_diagnostics(
|
||||
"fixture",
|
||||
editor.buffer_id(),
|
||||
editor.text_revision(),
|
||||
vec![annotation(
|
||||
(0, 2),
|
||||
(0, 6),
|
||||
DiagnosticSeverity::Warning,
|
||||
"semantic",
|
||||
)],
|
||||
)
|
||||
.unwrap();
|
||||
let mut syntax = SyntaxCache::new();
|
||||
for (offset, width, caret, colors) in [
|
||||
(2, 5, 4, vec![Color::DarkGray; 5]),
|
||||
(5, 2, 4, vec![Color::Reset, Color::DarkGray]),
|
||||
(7, 2, 5, vec![Color::DarkGray; 2]),
|
||||
(2, 5, 4, vec![Color::Yellow; 5]),
|
||||
(5, 2, 4, vec![Color::Reset, Color::Yellow]),
|
||||
(7, 2, 5, vec![Color::Yellow; 2]),
|
||||
(8, 2, 6, vec![Color::Reset, Color::DarkGray]),
|
||||
(
|
||||
0,
|
||||
@@ -1596,8 +1709,8 @@ end
|
||||
vec![
|
||||
Color::DarkGray,
|
||||
Color::DarkGray,
|
||||
Color::DarkGray,
|
||||
Color::DarkGray,
|
||||
Color::Yellow,
|
||||
Color::Yellow,
|
||||
Color::Reset,
|
||||
],
|
||||
),
|
||||
@@ -1632,21 +1745,73 @@ end
|
||||
assert_eq!(buffer[position].symbol(), expected[position].symbol());
|
||||
assert_eq!(rendered[position].fg, color, "left={offset}, cell={cell}");
|
||||
assert_eq!(rendered[position].bg, Color::Reset);
|
||||
let semantic_cell = color == Color::Yellow;
|
||||
assert_eq!(
|
||||
rendered[position].modifier.contains(Modifier::BOLD),
|
||||
semantic_cell
|
||||
);
|
||||
assert_eq!(
|
||||
rendered[position].modifier.contains(Modifier::CROSSED_OUT),
|
||||
semantic_cell
|
||||
);
|
||||
assert_eq!(
|
||||
rendered[position].modifier.contains(Modifier::UNDERLINED),
|
||||
semantic_cell
|
||||
);
|
||||
if semantic_cell {
|
||||
assert_eq!(rendered[position].underline_color, Color::Yellow);
|
||||
}
|
||||
assert_eq!(
|
||||
rendered[position].modifier.contains(Modifier::ITALIC),
|
||||
offset == 0 && cell == 0
|
||||
);
|
||||
}
|
||||
assert_eq!(buffer[(area.x, area.y)].fg, Color::Cyan);
|
||||
assert_eq!(buffer[(area.x, area.y)].fg, Color::Yellow);
|
||||
assert_eq!(buffer[(0, 0)].fg, Color::Reset);
|
||||
assert_eq!(editor.lines(), &[source]);
|
||||
}
|
||||
editor.handle(Input::Char('x'));
|
||||
assert!(editor.semantic_tokens().is_empty());
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(20, 4)).unwrap();
|
||||
let (mut top, mut left) = (0, 0);
|
||||
terminal
|
||||
.draw(|frame| draw(frame, &editor, &mut top, &mut left, &mut syntax))
|
||||
.unwrap();
|
||||
assert_eq!(terminal.backend().buffer()[(2, 0)].fg, Color::DarkGray);
|
||||
assert!(
|
||||
!terminal.backend().buffer()[(2, 0)]
|
||||
.modifier
|
||||
.contains(Modifier::ITALIC)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_visual_selection_and_secondary_carets_override_syntax_foreground() {
|
||||
fn search_visual_selection_and_secondary_carets_override_semantic_foreground() {
|
||||
let directory = Directory::new();
|
||||
let mut editor = Editor::open(
|
||||
Some(directory.file("overlays.py", "# word\n# word\n# word")),
|
||||
vec![],
|
||||
)
|
||||
.unwrap();
|
||||
editor
|
||||
.replace_semantic_tokens(
|
||||
editor.buffer_id(),
|
||||
editor.text_revision(),
|
||||
Some(Arc::from(
|
||||
(0..3)
|
||||
.map(|row| {
|
||||
semantic_token(
|
||||
row,
|
||||
2,
|
||||
6,
|
||||
SemanticTokenKind::Function,
|
||||
SemanticTokenModifier::Declaration as u16,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
)),
|
||||
)
|
||||
.unwrap();
|
||||
editor
|
||||
.replace_diagnostics(
|
||||
"fixture",
|
||||
|
||||
Reference in New Issue
Block a user