fix: preserve semantic roles in terminal palette highlighting
This commit is contained in:
@@ -203,6 +203,6 @@ Hover accepts plaintext/Markdown MarkupContent and string/code MarkedString form
|
|||||||
|
|
||||||
Semantic highlighting is **enabled automatically** when the first server advertises [LSP semantic tokens](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_semanticTokens). No additional configuration or binding is needed. Full-document requests are preferred; range-only servers receive a whole-document range request. Token colors override Tree-sitter where recognized; unsupported servers, unknown custom token types, missing tokens, and failed replies retain syntax highlighting. Text/path changes clear old semantic colors immediately, and stale replies cannot restore them. Same-text refreshes keep the current overlay until the replacement arrives.
|
Semantic highlighting is **enabled automatically** when the first server advertises [LSP semantic tokens](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_semanticTokens). No additional configuration or binding is needed. Full-document requests are preferred; range-only servers receive a whole-document range request. Token colors override Tree-sitter where recognized; unsupported servers, unknown custom token types, missing tokens, and failed replies retain syntax highlighting. Text/path changes clear old semantic colors immediately, and stale replies cannot restore them. Same-text refreshes keep the current overlay until the replacement arrives.
|
||||||
|
|
||||||
rust-analyzer and ty support semantic tokens. Solargraph 0.61.0 and Ruff do not; Ruby therefore retains Tree-sitter colors with the Solargraph preset, while Python uses ty's tokens and Ruff's diagnostics. Declaration/definition modifiers use bold, deprecated symbols use strikethrough, and readonly variables/properties use yellow. Diagnostics, search, selections, and carets retain their existing styling precedence.
|
rust-analyzer and ty support semantic tokens. Solargraph 0.61.0 and Ruff do not; Ruby therefore retains Tree-sitter colors with the Solargraph preset, while Python uses ty's tokens and Ruff's diagnostics. Declaration/definition modifiers use bold, deprecated symbols use strikethrough, and readonly variables/parameters/properties use italics without changing their role's color. Diagnostics, search, selections, and carets retain their existing styling precedence.
|
||||||
|
|
||||||
Native LSP currently provides synchronization, diagnostics, go-to-definition, hover, and semantic highlighting—not completion, rename, formatting, code actions, or workspace edits. Semantic token deltas, dynamic registration, and partial-result streaming are not implemented.
|
Native LSP currently provides synchronization, diagnostics, go-to-definition, hover, and semantic highlighting—not completion, rename, formatting, code actions, or workspace edits. Semantic token deltas, dynamic registration, and partial-result streaming are not implemented.
|
||||||
|
|||||||
+1
-1
@@ -206,4 +206,4 @@ Colors retain the terminal palette: comments dark gray, strings green, keywords
|
|||||||
|
|
||||||
Markdown heading markers (`#` through `######`, or Setext underlines) share the heading text's markup color. Plain-text hashes and other punctuation keep their existing styling.
|
Markdown heading markers (`#` through `######`, or Setext underlines) share the heading text's markup color. Plain-text hashes and other punctuation keep their existing styling.
|
||||||
|
|
||||||
An enabled supporting LSP server supplies semantic tokens automatically, overriding syntax colors only on recognized spans. Variables/parameters use light cyan, properties blue, and readonly variables/properties yellow; declarations/definitions are bold, documentation comments italic, and deprecated symbols struck through. Tree-sitter fills gaps and remains available while new tokens are pending after edits. Search, Visual selections, diagnostics, and carets keep their existing precedence. See [native LSP configuration](configuration.md#native-lsp) for provider support and server-stack routing.
|
An enabled supporting LSP server supplies semantic tokens automatically, overriding syntax colors only on recognized spans. Semantic colors also use the terminal palette: namespaces neutral gray, types cyan, variables/parameters light cyan, and properties blue. Readonly variables/parameters/properties keep those role colors and add italics; declarations/definitions are bold, documentation comments italic, and deprecated symbols struck through. Italic emphasis depends on terminal support. Tree-sitter fills gaps and remains available while new tokens are pending after edits. Search, Visual selections, diagnostics, and carets keep their existing precedence. See [native LSP configuration](configuration.md#native-lsp) for provider support and server-stack routing.
|
||||||
|
|||||||
+67
-23
@@ -1080,9 +1080,9 @@ fn semantic_style(token: &SemanticToken) -> Style {
|
|||||||
use SemanticTokenKind::*;
|
use SemanticTokenKind::*;
|
||||||
let readonly = token.modifiers & SemanticTokenModifier::Readonly as u16 != 0;
|
let readonly = token.modifiers & SemanticTokenModifier::Readonly as u16 != 0;
|
||||||
let color = match token.kind {
|
let color = match token.kind {
|
||||||
Namespace | Type | Class | Enum | Interface | Struct | TypeParameter => Color::Cyan,
|
Namespace => Color::Gray,
|
||||||
|
Type | Class | Enum | Interface | Struct | TypeParameter => Color::Cyan,
|
||||||
Function | Method | Event | Macro => Color::LightBlue,
|
Function | Method | Event | Macro => Color::LightBlue,
|
||||||
Parameter | Variable | Property if readonly => Color::Yellow,
|
|
||||||
Parameter | Variable => Color::LightCyan,
|
Parameter | Variable => Color::LightCyan,
|
||||||
Property => Color::Blue,
|
Property => Color::Blue,
|
||||||
EnumMember | Number => Color::Yellow,
|
EnumMember | Number => Color::Yellow,
|
||||||
@@ -1091,6 +1091,9 @@ fn semantic_style(token: &SemanticToken) -> Style {
|
|||||||
Comment => Color::DarkGray,
|
Comment => Color::DarkGray,
|
||||||
};
|
};
|
||||||
let mut style = Style::default().fg(color);
|
let mut style = Style::default().fg(color);
|
||||||
|
if readonly && matches!(token.kind, Parameter | Variable | Property) {
|
||||||
|
style = style.add_modifier(ratatui::style::Modifier::ITALIC);
|
||||||
|
}
|
||||||
if token.modifiers
|
if token.modifiers
|
||||||
& (SemanticTokenModifier::Declaration as u16 | SemanticTokenModifier::Definition as u16)
|
& (SemanticTokenModifier::Declaration as u16 | SemanticTokenModifier::Definition as u16)
|
||||||
!= 0
|
!= 0
|
||||||
@@ -1655,6 +1658,60 @@ end
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn semantic_roles_remain_distinct_when_values_and_fields_are_readonly() {
|
||||||
|
let mut editor = Editor::open(None, vec![]).unwrap();
|
||||||
|
editor
|
||||||
|
.apply(vec![crate::editor::Effect::Replace(
|
||||||
|
"ns Type local frozen field fixed param frozenparam".into(),
|
||||||
|
)])
|
||||||
|
.unwrap();
|
||||||
|
let readonly = SemanticTokenModifier::Readonly as u16;
|
||||||
|
editor
|
||||||
|
.replace_semantic_tokens(
|
||||||
|
editor.buffer_id(),
|
||||||
|
editor.text_revision(),
|
||||||
|
Some(Arc::from([
|
||||||
|
semantic_token(0, 0, 2, SemanticTokenKind::Namespace, 0),
|
||||||
|
semantic_token(0, 3, 7, SemanticTokenKind::Type, 0),
|
||||||
|
semantic_token(0, 8, 13, SemanticTokenKind::Variable, 0),
|
||||||
|
semantic_token(0, 14, 20, SemanticTokenKind::Variable, readonly),
|
||||||
|
semantic_token(0, 21, 26, SemanticTokenKind::Property, 0),
|
||||||
|
semantic_token(0, 27, 32, SemanticTokenKind::Property, readonly),
|
||||||
|
semantic_token(0, 33, 38, SemanticTokenKind::Parameter, 0),
|
||||||
|
semantic_token(0, 39, 50, SemanticTokenKind::Parameter, readonly),
|
||||||
|
])),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(60, 4)).unwrap();
|
||||||
|
terminal
|
||||||
|
.draw(|frame| BufferViewports::default().draw(frame, &editor))
|
||||||
|
.unwrap();
|
||||||
|
let buffer = terminal.backend().buffer();
|
||||||
|
let foregrounds = [0, 3, 8, 14, 21, 27, 33, 39].map(|column| buffer[(column + 2, 0)].fg);
|
||||||
|
assert_ne!(foregrounds[0], foregrounds[1]);
|
||||||
|
assert_ne!(foregrounds[1], foregrounds[2]);
|
||||||
|
assert_ne!(foregrounds[2], foregrounds[4]);
|
||||||
|
assert_eq!(foregrounds[2], foregrounds[3]);
|
||||||
|
assert_eq!(foregrounds[4], foregrounds[5]);
|
||||||
|
assert_eq!(foregrounds[6], foregrounds[7]);
|
||||||
|
for (column, is_readonly) in [
|
||||||
|
(0, false),
|
||||||
|
(3, false),
|
||||||
|
(8, false),
|
||||||
|
(14, true),
|
||||||
|
(21, false),
|
||||||
|
(27, true),
|
||||||
|
(33, false),
|
||||||
|
(39, true),
|
||||||
|
] {
|
||||||
|
assert_eq!(
|
||||||
|
buffer[(column + 2, 0)].modifier.contains(Modifier::ITALIC),
|
||||||
|
is_readonly,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn semantic_colors_modifiers_and_syntax_fallback_cover_whole_graphemes_and_clipped_tabs() {
|
fn semantic_colors_modifiers_and_syntax_fallback_cover_whole_graphemes_and_clipped_tabs() {
|
||||||
let directory = Directory::new();
|
let directory = Directory::new();
|
||||||
@@ -1700,23 +1757,12 @@ end
|
|||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let mut syntax = SyntaxCache::new();
|
let mut syntax = SyntaxCache::new();
|
||||||
for (offset, width, caret, colors) in [
|
for (offset, width, caret, coverage) in [
|
||||||
(2, 5, 4, vec![Color::Yellow; 5]),
|
(2, 5, 4, vec![true; 5]),
|
||||||
(5, 2, 4, vec![Color::Reset, Color::Yellow]),
|
(5, 2, 4, vec![false, true]),
|
||||||
(7, 2, 5, vec![Color::Yellow; 2]),
|
(7, 2, 5, vec![true; 2]),
|
||||||
(8, 2, 6, vec![Color::Reset, Color::DarkGray]),
|
(8, 2, 6, vec![false; 2]),
|
||||||
(
|
(0, 5, 2, vec![false, false, true, true, false]),
|
||||||
0,
|
|
||||||
5,
|
|
||||||
2,
|
|
||||||
vec![
|
|
||||||
Color::DarkGray,
|
|
||||||
Color::DarkGray,
|
|
||||||
Color::Yellow,
|
|
||||||
Color::Yellow,
|
|
||||||
Color::Reset,
|
|
||||||
],
|
|
||||||
),
|
|
||||||
] {
|
] {
|
||||||
editor.handle(Input::Home);
|
editor.handle(Input::Home);
|
||||||
for _ in 0..caret {
|
for _ in 0..caret {
|
||||||
@@ -1743,12 +1789,10 @@ end
|
|||||||
Paragraph::new(visible_line(source, offset, usize::from(width)))
|
Paragraph::new(visible_line(source, offset, usize::from(width)))
|
||||||
.render(text_area, &mut expected);
|
.render(text_area, &mut expected);
|
||||||
let buffer = terminal.backend().buffer();
|
let buffer = terminal.backend().buffer();
|
||||||
for (cell, color) in colors.into_iter().enumerate() {
|
for (cell, semantic_cell) in coverage.into_iter().enumerate() {
|
||||||
let position = (text_area.x + cell as u16, text_area.y);
|
let position = (text_area.x + cell as u16, text_area.y);
|
||||||
assert_eq!(buffer[position].symbol(), expected[position].symbol());
|
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);
|
assert_eq!(rendered[position].bg, Color::Reset);
|
||||||
let semantic_cell = color == Color::Yellow;
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
rendered[position].modifier.contains(Modifier::BOLD),
|
rendered[position].modifier.contains(Modifier::BOLD),
|
||||||
semantic_cell
|
semantic_cell
|
||||||
@@ -1766,7 +1810,7 @@ end
|
|||||||
}
|
}
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
rendered[position].modifier.contains(Modifier::ITALIC),
|
rendered[position].modifier.contains(Modifier::ITALIC),
|
||||||
offset == 0 && cell == 0
|
semantic_cell || (offset == 0 && cell == 0)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
assert_eq!(buffer[(area.x, area.y)].fg, Color::Yellow);
|
assert_eq!(buffer[(area.x, area.y)].fg, Color::Yellow);
|
||||||
|
|||||||
Reference in New Issue
Block a user