feat: enable LSP semantic token highlighting by default

This commit is contained in:
2026-10-09 22:17:30 +02:00
parent 548711980c
commit d418d670ee
13 changed files with 1714 additions and 84 deletions
+166 -40
View File
@@ -390,50 +390,73 @@ impl EditorSession {
return Ok(false);
}
// Validate the whole applicable annotation batch before changing any buffer.
for effect in &update.effects {
match effect {
UpdateEffect::Diagnostics {
buffer_id,
revision,
items,
} => {
self.editor
.validate_diagnostics(*buffer_id, *revision, items)?;
}
UpdateEffect::Navigate {
buffer_id,
revision,
path,
..
} => {
let source = InvocationSource {
buffer_id: *buffer_id,
revision: *revision,
};
if self.request_is_current(owner, update.request_id, source)
&& (path.as_os_str().is_empty()
|| path.to_string_lossy().chars().any(char::is_control))
{
return Err(
"Navigation path must be nonempty and contain no controls".into()
);
let validation = (|| -> Result<(), String> {
for effect in &update.effects {
match effect {
UpdateEffect::Diagnostics {
buffer_id,
revision,
items,
} => {
self.editor
.validate_diagnostics(*buffer_id, *revision, items)?;
}
}
UpdateEffect::Hover {
buffer_id,
revision,
text,
} => {
let source = InvocationSource {
buffer_id: *buffer_id,
revision: *revision,
};
if self.hover_is_current(owner, update.request_id, source) {
crate::editor::validate_hover_text(text)?;
UpdateEffect::SemanticTokens {
buffer_id,
revision,
tokens,
} => {
if owner != "core" {
return Err("Semantic tokens are native LSP updates only".into());
}
self.editor.validate_semantic_tokens(
*buffer_id,
*revision,
tokens.as_deref(),
)?;
}
UpdateEffect::Navigate {
buffer_id,
revision,
path,
..
} => {
let source = InvocationSource {
buffer_id: *buffer_id,
revision: *revision,
};
if self.request_is_current(owner, update.request_id, source)
&& (path.as_os_str().is_empty()
|| path.to_string_lossy().chars().any(char::is_control))
{
return Err(
"Navigation path must be nonempty and contain no controls".into()
);
}
}
UpdateEffect::Hover {
buffer_id,
revision,
text,
} => {
let source = InvocationSource {
buffer_id: *buffer_id,
revision: *revision,
};
if self.hover_is_current(owner, update.request_id, source) {
crate::editor::validate_hover_text(text)?;
}
}
UpdateEffect::Message { .. } => {}
}
UpdateEffect::Message { .. } => {}
}
Ok(())
})();
if let Err(error) = validation {
if owner == "core" {
self.editor.clear_semantic_tokens();
}
return Err(error);
}
let mut changed = false;
for effect in update.effects {
@@ -447,6 +470,15 @@ impl EditorSession {
.editor
.replace_validated_diagnostics(owner, buffer_id, revision, items);
}
UpdateEffect::SemanticTokens {
buffer_id,
revision,
tokens,
} => {
changed |= self
.editor
.replace_validated_semantic_tokens(buffer_id, revision, tokens);
}
UpdateEffect::Navigate {
buffer_id,
revision,
@@ -595,6 +627,100 @@ mod tests {
static NEXT_FIXTURE: AtomicU64 = AtomicU64::new(0);
#[test]
fn native_semantic_batches_are_atomic_and_malformed_updates_clear_hidden_overlays() {
use crate::plugin::{SemanticToken, SemanticTokenKind};
use std::sync::Arc;
let fixture = Fixture::new();
fixture.file("buffer.txt", "source");
let target = fixture.file("target.txt", "target");
let mut session = fixture.open("");
let token = SemanticToken {
start: TextPosition { row: 0, column: 0 },
end: TextPosition { row: 0, column: 1 },
kind: SemanticTokenKind::Variable,
modifiers: 0,
};
let tokens: Arc<[SemanticToken]> = Arc::from([token]);
let semantic = |buffer_id, revision, tokens| UpdateEffect::SemanticTokens {
buffer_id,
revision,
tokens,
};
let update = |effects| PluginUpdate {
request_id: None,
effects,
};
session
.apply_update("core", update(vec![semantic(1, 0, Some(tokens.clone()))]))
.unwrap();
session.execute(&format!("edit {}", target.display()));
session
.apply_update("core", update(vec![semantic(2, 0, Some(tokens.clone()))]))
.unwrap();
let diagnostic = crate::plugin::Diagnostic {
start: token.start,
end: token.end,
severity: crate::plugin::DiagnosticSeverity::Warning,
message: "unchanged".into(),
};
session
.editor
.replace_diagnostics("plugin", 2, 0, vec![diagnostic.clone()])
.unwrap();
let malformed: Arc<[SemanticToken]> = Arc::from([SemanticToken {
end: TextPosition { row: 99, column: 0 },
..token
}]);
let before_message = session.editor.message().to_owned();
let result = session.apply_update(
"core",
update(vec![
UpdateEffect::Message {
text: "must not apply".into(),
},
UpdateEffect::Diagnostics {
buffer_id: 2,
revision: 0,
items: vec![diagnostic.clone()],
},
semantic(1, 0, Some(tokens.clone())),
semantic(2, 0, Some(malformed.clone())),
]),
);
assert!(result.is_err());
assert_eq!(session.editor.message(), before_message);
assert!(session.editor.semantic_tokens().is_empty());
assert_eq!(session.editor.diagnostics().count(), 1);
session.execute("buffer 1");
assert!(session.editor.semantic_tokens().is_empty());
assert!(
session
.apply_update(
"core",
update(vec![
semantic(999, 0, Some(malformed.clone())),
semantic(1, 99, Some(malformed)),
semantic(1, 0, Some(tokens.clone())),
])
)
.unwrap()
);
assert!(std::ptr::eq(
session.editor.semantic_tokens(),
tokens.as_ref()
));
assert_eq!(session.editor.lines(), ["source"]);
assert_eq!(session.editor.text_revision(), 0);
assert!(!session.editor.dirty());
assert!(
session
.apply_update("core", update(vec![semantic(1, 0, None)]))
.unwrap()
);
assert!(session.editor.semantic_tokens().is_empty());
}
struct Fixture(PathBuf);
impl Fixture {