feat: enable LSP semantic token highlighting by default
This commit is contained in:
+522
-9
@@ -19,7 +19,8 @@ use crate::{
|
||||
editor::Context,
|
||||
plugin::{
|
||||
Diagnostic, DiagnosticSeverity, DocumentEvent, DocumentEventKind, DocumentSnapshot,
|
||||
PluginUpdate, TextPosition, UpdateEffect,
|
||||
PluginUpdate, SemanticToken, SemanticTokenKind, SemanticTokenModifier, TextPosition,
|
||||
UpdateEffect,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -49,6 +50,7 @@ struct CachedDocument {
|
||||
snapshot: Arc<DocumentSnapshot>,
|
||||
servers: Vec<ServerKey>,
|
||||
diagnostics: HashMap<usize, Contribution>,
|
||||
semantic_tokens: Option<Arc<[SemanticToken]>>,
|
||||
}
|
||||
|
||||
struct Contribution {
|
||||
@@ -62,6 +64,10 @@ enum Output {
|
||||
snapshot: Arc<DocumentSnapshot>,
|
||||
items: Vec<Diagnostic>,
|
||||
},
|
||||
SemanticTokens {
|
||||
snapshot: Arc<DocumentSnapshot>,
|
||||
tokens: Option<Arc<[SemanticToken]>>,
|
||||
},
|
||||
}
|
||||
|
||||
struct Worker {
|
||||
@@ -78,6 +84,7 @@ enum Input {
|
||||
snapshot: Arc<DocumentSnapshot>,
|
||||
event: DocumentEventKind,
|
||||
served: bool,
|
||||
primary: bool,
|
||||
},
|
||||
Position(PositionKind, Source),
|
||||
Diagnostics(usize),
|
||||
@@ -115,6 +122,7 @@ pub struct Lsp {
|
||||
servers: BTreeMap<ServerKey, Worker>,
|
||||
updates: Vec<PluginUpdate>,
|
||||
changed_diagnostics: HashSet<usize>,
|
||||
changed_semantics: HashSet<usize>,
|
||||
}
|
||||
|
||||
impl Lsp {
|
||||
@@ -126,6 +134,7 @@ impl Lsp {
|
||||
servers: BTreeMap::new(),
|
||||
updates: Vec::new(),
|
||||
changed_diagnostics: HashSet::new(),
|
||||
changed_semantics: HashSet::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,6 +161,7 @@ impl Lsp {
|
||||
if matches!(event.event, DocumentEventKind::Close) {
|
||||
self.documents.remove(&snapshot.id);
|
||||
self.changed_diagnostics.remove(&snapshot.id);
|
||||
self.changed_semantics.remove(&snapshot.id);
|
||||
} else if self.documents.get(&snapshot.id).is_none_or(|cached| {
|
||||
cached.snapshot.path != snapshot.path || cached.snapshot.root != snapshot.root
|
||||
}) {
|
||||
@@ -193,9 +203,11 @@ impl Lsp {
|
||||
snapshot: Arc::clone(&snapshot),
|
||||
servers: keys,
|
||||
diagnostics: HashMap::new(),
|
||||
semantic_tokens: None,
|
||||
},
|
||||
);
|
||||
self.changed_diagnostics.insert(snapshot.id);
|
||||
self.changed_semantics.insert(snapshot.id);
|
||||
for index in 0..self.documents[&snapshot.id].servers.len() {
|
||||
let key = self.documents[&snapshot.id].servers[index].clone();
|
||||
if let Err(error) = self.start(&key) {
|
||||
@@ -207,6 +219,8 @@ impl Lsp {
|
||||
if !Arc::ptr_eq(&cached.snapshot, &snapshot) {
|
||||
cached.snapshot = Arc::clone(&snapshot);
|
||||
cached.diagnostics.clear();
|
||||
cached.semantic_tokens = None;
|
||||
self.changed_semantics.insert(snapshot.id);
|
||||
self.changed_diagnostics.insert(snapshot.id);
|
||||
}
|
||||
}
|
||||
@@ -224,6 +238,10 @@ impl Lsp {
|
||||
.documents
|
||||
.get(&snapshot.id)
|
||||
.is_some_and(|doc| doc.servers.contains(server_key)),
|
||||
primary: self
|
||||
.documents
|
||||
.get(&snapshot.id)
|
||||
.is_some_and(|doc| doc.servers.first() == Some(server_key)),
|
||||
})
|
||||
.is_err()
|
||||
{
|
||||
@@ -330,6 +348,7 @@ impl Lsp {
|
||||
worker,
|
||||
&mut self.documents,
|
||||
&mut self.changed_diagnostics,
|
||||
&mut self.changed_semantics,
|
||||
&mut self.updates,
|
||||
"LSP server stopped: combined diagnostic messages exceed 4 MiB"
|
||||
.into(),
|
||||
@@ -344,10 +363,20 @@ impl Lsp {
|
||||
}
|
||||
self.changed_diagnostics.insert(snapshot.id);
|
||||
}
|
||||
Output::SemanticTokens { snapshot, tokens } => {
|
||||
let Some(doc) = self.documents.get_mut(&snapshot.id).filter(|doc| {
|
||||
doc.servers.first() == Some(key)
|
||||
&& Arc::ptr_eq(&doc.snapshot, &snapshot)
|
||||
}) else {
|
||||
continue;
|
||||
};
|
||||
doc.semantic_tokens = tokens;
|
||||
self.changed_semantics.insert(snapshot.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let effects: Vec<_> = self
|
||||
let mut effects: Vec<_> = self
|
||||
.changed_diagnostics
|
||||
.drain()
|
||||
.filter_map(|id| {
|
||||
@@ -370,6 +399,14 @@ impl Lsp {
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
effects.extend(self.changed_semantics.drain().filter_map(|id| {
|
||||
let doc = self.documents.get(&id)?;
|
||||
Some(UpdateEffect::SemanticTokens {
|
||||
buffer_id: id,
|
||||
revision: doc.snapshot.revision,
|
||||
tokens: doc.semantic_tokens.clone(),
|
||||
})
|
||||
}));
|
||||
if !effects.is_empty() {
|
||||
self.updates.push(PluginUpdate {
|
||||
request_id: None,
|
||||
@@ -396,7 +433,13 @@ impl Lsp {
|
||||
let initial: Vec<_> = self
|
||||
.documents
|
||||
.values()
|
||||
.map(|doc| (Arc::clone(&doc.snapshot), doc.servers.contains(key)))
|
||||
.map(|doc| {
|
||||
(
|
||||
Arc::clone(&doc.snapshot),
|
||||
doc.servers.contains(key),
|
||||
doc.servers.first() == Some(key),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let started = thread::Builder::new()
|
||||
.name(format!("lsp-{}", config.language_id))
|
||||
@@ -415,8 +458,8 @@ impl Lsp {
|
||||
};
|
||||
let result = initial
|
||||
.into_iter()
|
||||
.try_for_each(|(snapshot, served)| {
|
||||
runtime.document(snapshot, DocumentEventKind::Open, served)
|
||||
.try_for_each(|(snapshot, served, primary)| {
|
||||
runtime.document(snapshot, DocumentEventKind::Open, served, primary)
|
||||
})
|
||||
.and_then(|()| runtime.run());
|
||||
if let Err(reason) = result {
|
||||
@@ -483,6 +526,7 @@ impl Lsp {
|
||||
worker,
|
||||
&mut self.documents,
|
||||
&mut self.changed_diagnostics,
|
||||
&mut self.changed_semantics,
|
||||
&mut self.updates,
|
||||
reason,
|
||||
);
|
||||
@@ -493,6 +537,7 @@ impl Lsp {
|
||||
worker: &mut Worker,
|
||||
documents: &mut HashMap<usize, CachedDocument>,
|
||||
changed: &mut HashSet<usize>,
|
||||
changed_semantics: &mut HashSet<usize>,
|
||||
updates: &mut Vec<PluginUpdate>,
|
||||
reason: String,
|
||||
) {
|
||||
@@ -502,6 +547,10 @@ impl Lsp {
|
||||
worker.retired = true;
|
||||
worker.cancelled.store(true, Ordering::Release);
|
||||
for (id, doc) in documents {
|
||||
if doc.servers.first() == Some(key) {
|
||||
doc.semantic_tokens = None;
|
||||
changed_semantics.insert(*id);
|
||||
}
|
||||
if doc.servers.contains(key) {
|
||||
doc.diagnostics.remove(&key.config);
|
||||
changed.insert(*id);
|
||||
@@ -535,6 +584,8 @@ struct Document {
|
||||
version: u64,
|
||||
opened: bool,
|
||||
saved: bool,
|
||||
primary: bool,
|
||||
semantic_dirty: bool,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -549,6 +600,7 @@ enum Request {
|
||||
Initialize,
|
||||
Definition(Source),
|
||||
Hover(Source),
|
||||
SemanticTokens(Arc<DocumentSnapshot>),
|
||||
Shutdown,
|
||||
}
|
||||
|
||||
@@ -563,10 +615,12 @@ struct Runtime {
|
||||
documents: HashMap<usize, Document>,
|
||||
requests: HashMap<u64, Request>,
|
||||
pending: VecDeque<(PositionKind, Source)>,
|
||||
semantic_inflight: HashSet<usize>,
|
||||
next_id: u64,
|
||||
ready: bool,
|
||||
definition: bool,
|
||||
hover: bool,
|
||||
semantic: Option<SemanticOptions>,
|
||||
sync: SyncOptions,
|
||||
}
|
||||
|
||||
@@ -590,10 +644,12 @@ impl Runtime {
|
||||
documents: HashMap::new(),
|
||||
requests: HashMap::new(),
|
||||
pending: VecDeque::new(),
|
||||
semantic_inflight: HashSet::new(),
|
||||
next_id: 1,
|
||||
ready: false,
|
||||
definition: false,
|
||||
hover: false,
|
||||
semantic: None,
|
||||
sync: SyncOptions::default(),
|
||||
};
|
||||
runtime.request(
|
||||
@@ -606,14 +662,26 @@ impl Runtime {
|
||||
"initializationOptions": runtime.config.initialization_options,
|
||||
"capabilities": {
|
||||
"general": {"positionEncodings": ["utf-16"]},
|
||||
"workspace": {"configuration": true, "workspaceFolders": true},
|
||||
"workspace": {"configuration": true, "workspaceFolders": true,
|
||||
"semanticTokens": {"refreshSupport": true}},
|
||||
"textDocument": {
|
||||
"synchronization": {"dynamicRegistration": false, "willSave": false,
|
||||
"willSaveWaitUntil": false, "didSave": true},
|
||||
"definition": {"dynamicRegistration": false, "linkSupport": true},
|
||||
"hover": {"dynamicRegistration": false,
|
||||
"contentFormat": ["plaintext", "markdown"]},
|
||||
"publishDiagnostics": {"relatedInformation": false, "versionSupport": true}
|
||||
"publishDiagnostics": {"relatedInformation": false, "versionSupport": true},
|
||||
"semanticTokens": {
|
||||
"dynamicRegistration": false,
|
||||
"requests": {"range": true, "full": true},
|
||||
"tokenTypes": SEMANTIC_TYPES.iter().map(|(name, _)| *name).collect::<Vec<_>>(),
|
||||
"tokenModifiers": SEMANTIC_MODIFIERS.iter().map(|(name, _)| *name).collect::<Vec<_>>(),
|
||||
"formats": ["relative"],
|
||||
"overlappingTokenSupport": false,
|
||||
"multilineTokenSupport": false,
|
||||
"serverCancelSupport": false,
|
||||
"augmentsSyntaxTokens": true
|
||||
}
|
||||
}
|
||||
}
|
||||
})),
|
||||
@@ -630,7 +698,8 @@ impl Runtime {
|
||||
snapshot,
|
||||
event,
|
||||
served,
|
||||
}) => self.document(snapshot, event, served)?,
|
||||
primary,
|
||||
}) => self.document(snapshot, event, served, primary)?,
|
||||
Ok(Input::Position(kind, source)) => self.position(kind, source)?,
|
||||
Ok(Input::Diagnostics(id)) => {
|
||||
let text = if !self.documents.contains_key(&id) {
|
||||
@@ -661,6 +730,19 @@ impl Runtime {
|
||||
};
|
||||
self.incoming(frame)?;
|
||||
}
|
||||
if self.ready && self.semantic.is_some() && self.requests.len() < QUEUE_SIZE {
|
||||
let pending: Vec<_> = self
|
||||
.documents
|
||||
.iter()
|
||||
.filter(|(id, doc)| {
|
||||
doc.primary && doc.semantic_dirty && !self.semantic_inflight.contains(*id)
|
||||
})
|
||||
.map(|(id, _)| *id)
|
||||
.collect();
|
||||
for id in pending {
|
||||
self.semantic_request(id)?;
|
||||
}
|
||||
}
|
||||
self.transport.check_alive()?;
|
||||
thread::sleep(TICK);
|
||||
}
|
||||
@@ -683,6 +765,17 @@ impl Runtime {
|
||||
})
|
||||
}
|
||||
|
||||
fn emit_semantics(
|
||||
&self,
|
||||
snapshot: &Arc<DocumentSnapshot>,
|
||||
tokens: Option<Arc<[SemanticToken]>>,
|
||||
) -> Result<(), String> {
|
||||
self.send(Output::SemanticTokens {
|
||||
snapshot: Arc::clone(snapshot),
|
||||
tokens,
|
||||
})
|
||||
}
|
||||
|
||||
fn send(&self, output: Output) -> Result<(), String> {
|
||||
self.output.try_send(output).map_err(|error| match error {
|
||||
TrySendError::Full(_) => "LSP incoming update queue is full".into(),
|
||||
@@ -695,6 +788,7 @@ impl Runtime {
|
||||
snapshot: Arc<DocumentSnapshot>,
|
||||
event: DocumentEventKind,
|
||||
served: bool,
|
||||
primary: bool,
|
||||
) -> Result<(), String> {
|
||||
let id = snapshot.id;
|
||||
let old = self.documents.remove(&id);
|
||||
@@ -733,11 +827,16 @@ impl Runtime {
|
||||
return Err("LSP document version exhausted".into());
|
||||
}
|
||||
let mut doc = Document {
|
||||
snapshot,
|
||||
snapshot: Arc::clone(&snapshot),
|
||||
uri,
|
||||
version,
|
||||
opened: same_uri && old.as_ref().is_some_and(|old| old.opened),
|
||||
saved: matches!(event, DocumentEventKind::Save),
|
||||
primary,
|
||||
semantic_dirty: primary
|
||||
&& old.as_ref().is_none_or(|old| {
|
||||
old.semantic_dirty || !old.primary || !Arc::ptr_eq(&old.snapshot, &snapshot)
|
||||
}),
|
||||
};
|
||||
if changed {
|
||||
self.emit_diagnostics(&doc.snapshot, Vec::new())?;
|
||||
@@ -753,6 +852,7 @@ impl Runtime {
|
||||
}
|
||||
}
|
||||
self.documents.insert(id, doc);
|
||||
self.semantic_request(id)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -857,6 +957,60 @@ impl Runtime {
|
||||
self.request(method, Some(params), request)
|
||||
}
|
||||
|
||||
fn semantic_request(&mut self, id: usize) -> Result<(), String> {
|
||||
let Some(options) = &self.semantic else {
|
||||
return Ok(());
|
||||
};
|
||||
let Some(doc) = self.documents.get(&id).filter(|doc| {
|
||||
self.ready && doc.primary && doc.semantic_dirty && !self.semantic_inflight.contains(&id)
|
||||
}) else {
|
||||
return Ok(());
|
||||
};
|
||||
if self.requests.len() == QUEUE_SIZE {
|
||||
return Ok(());
|
||||
}
|
||||
let snapshot = Arc::clone(&doc.snapshot);
|
||||
let mut params = json!({"textDocument": {"uri": doc.uri}});
|
||||
let method = if options.full {
|
||||
"textDocument/semanticTokens/full"
|
||||
} else {
|
||||
let text = normalized(&snapshot.text);
|
||||
let (row, line) = text.split('\n').enumerate().last().unwrap();
|
||||
params["range"] = json!({"start": {"line": 0, "character": 0},
|
||||
"end": {"line": row, "character": line.encode_utf16().count()}});
|
||||
"textDocument/semanticTokens/range"
|
||||
};
|
||||
self.request(method, Some(params), Request::SemanticTokens(snapshot))?;
|
||||
self.semantic_inflight.insert(id);
|
||||
self.documents.get_mut(&id).unwrap().semantic_dirty = false;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn semantic_result(
|
||||
&mut self,
|
||||
snapshot: Arc<DocumentSnapshot>,
|
||||
result: Result<&Value, String>,
|
||||
) -> Result<(), String> {
|
||||
self.semantic_inflight.remove(&snapshot.id);
|
||||
let fresh = self.documents.get(&snapshot.id).is_some_and(|doc| {
|
||||
doc.primary && !doc.semantic_dirty && Arc::ptr_eq(&doc.snapshot, &snapshot)
|
||||
});
|
||||
// Stale responses are discarded before inspecting their token payload.
|
||||
if fresh {
|
||||
let decoded = result.and_then(|result| {
|
||||
decode_semantic_tokens(&snapshot.text, result, self.semantic.as_ref().unwrap())
|
||||
});
|
||||
match decoded {
|
||||
Ok(tokens) => self.emit_semantics(&snapshot, tokens)?,
|
||||
Err(reason) => {
|
||||
self.emit_semantics(&snapshot, None)?;
|
||||
self.emit(message(&format!("semantic tokens failed: {reason}"), None))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.semantic_request(snapshot.id)
|
||||
}
|
||||
|
||||
fn request(
|
||||
&mut self,
|
||||
method: &str,
|
||||
@@ -920,6 +1074,9 @@ impl Runtime {
|
||||
Request::Hover(source) => {
|
||||
self.hover_result(source, Err("unexpected LSP response".into()))
|
||||
}
|
||||
Request::SemanticTokens(snapshot) => {
|
||||
self.semantic_result(snapshot, Err("unexpected LSP response".into()))
|
||||
}
|
||||
_ => Err("unexpected LSP response".into()),
|
||||
};
|
||||
}
|
||||
@@ -932,6 +1089,9 @@ impl Runtime {
|
||||
Request::Hover(source) => {
|
||||
self.hover_result(source, Err("invalid LSP error response".into()))
|
||||
}
|
||||
Request::SemanticTokens(snapshot) => {
|
||||
self.semantic_result(snapshot, Err("invalid LSP error response".into()))
|
||||
}
|
||||
_ => Err("invalid LSP error response".into()),
|
||||
};
|
||||
};
|
||||
@@ -942,6 +1102,9 @@ impl Runtime {
|
||||
Some(source.request_id),
|
||||
)),
|
||||
Request::Hover(source) => self.hover_result(source, Err(text.to_owned())),
|
||||
Request::SemanticTokens(snapshot) => {
|
||||
self.semantic_result(snapshot, Err(text.to_owned()))
|
||||
}
|
||||
Request::Shutdown => Ok(()),
|
||||
}
|
||||
} else {
|
||||
@@ -949,6 +1112,9 @@ impl Runtime {
|
||||
Request::Initialize => self.initialized(&frame["result"]),
|
||||
Request::Definition(source) => self.definition_result(source, &frame["result"]),
|
||||
Request::Hover(source) => self.hover_result(source, Ok(&frame["result"])),
|
||||
Request::SemanticTokens(snapshot) => {
|
||||
self.semantic_result(snapshot, Ok(&frame["result"]))
|
||||
}
|
||||
Request::Shutdown => self.notify("exit", None),
|
||||
}
|
||||
}
|
||||
@@ -972,6 +1138,12 @@ impl Runtime {
|
||||
self.hover = capabilities
|
||||
.get("hoverProvider")
|
||||
.is_some_and(|value| value == true || value.is_object());
|
||||
match semantic_options(capabilities.get("semanticTokensProvider")) {
|
||||
Ok(options) => self.semantic = options,
|
||||
Err(reason) => {
|
||||
self.emit(message(&format!("semantic tokens failed: {reason}"), None))?
|
||||
}
|
||||
}
|
||||
self.notify("initialized", Some(json!({})))?;
|
||||
self.notify(
|
||||
"workspace/didChangeConfiguration",
|
||||
@@ -985,6 +1157,7 @@ impl Runtime {
|
||||
self.save(&mut doc)?;
|
||||
}
|
||||
self.documents.insert(id, doc);
|
||||
self.semantic_request(id)?;
|
||||
}
|
||||
while let Some((kind, source)) = self.pending.pop_front() {
|
||||
self.position(kind, source)?;
|
||||
@@ -1032,6 +1205,24 @@ impl Runtime {
|
||||
}
|
||||
"workspace/workspaceFolders" => self.workspace_folders()?,
|
||||
"window/showMessageRequest" => Value::Null,
|
||||
"workspace/semanticTokens/refresh" => {
|
||||
self.transport
|
||||
.send(json!({"jsonrpc": "2.0", "id": id, "result": null}))?;
|
||||
if self.semantic.is_some() {
|
||||
let ids: Vec<_> = self
|
||||
.documents
|
||||
.iter()
|
||||
.filter(|(_, doc)| doc.primary)
|
||||
.map(|(id, _)| *id)
|
||||
.collect();
|
||||
for id in ids {
|
||||
let doc = self.documents.get_mut(&id).unwrap();
|
||||
doc.semantic_dirty = true;
|
||||
self.semantic_request(id)?;
|
||||
}
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
_ => {
|
||||
response.as_object_mut().unwrap().remove("result");
|
||||
response["error"] =
|
||||
@@ -1252,6 +1443,267 @@ impl Runtime {
|
||||
}
|
||||
}
|
||||
|
||||
const SEMANTIC_TYPES: &[(&str, SemanticTokenKind)] = &[
|
||||
("namespace", SemanticTokenKind::Namespace),
|
||||
("type", SemanticTokenKind::Type),
|
||||
("class", SemanticTokenKind::Class),
|
||||
("enum", SemanticTokenKind::Enum),
|
||||
("interface", SemanticTokenKind::Interface),
|
||||
("struct", SemanticTokenKind::Struct),
|
||||
("typeParameter", SemanticTokenKind::TypeParameter),
|
||||
("parameter", SemanticTokenKind::Parameter),
|
||||
("variable", SemanticTokenKind::Variable),
|
||||
("property", SemanticTokenKind::Property),
|
||||
("enumMember", SemanticTokenKind::EnumMember),
|
||||
("event", SemanticTokenKind::Event),
|
||||
("function", SemanticTokenKind::Function),
|
||||
("method", SemanticTokenKind::Method),
|
||||
("macro", SemanticTokenKind::Macro),
|
||||
("keyword", SemanticTokenKind::Keyword),
|
||||
("modifier", SemanticTokenKind::Modifier),
|
||||
("comment", SemanticTokenKind::Comment),
|
||||
("string", SemanticTokenKind::String),
|
||||
("number", SemanticTokenKind::Number),
|
||||
("regexp", SemanticTokenKind::Regexp),
|
||||
("operator", SemanticTokenKind::Operator),
|
||||
("decorator", SemanticTokenKind::Decorator),
|
||||
];
|
||||
|
||||
const SEMANTIC_MODIFIERS: &[(&str, SemanticTokenModifier)] = &[
|
||||
("declaration", SemanticTokenModifier::Declaration),
|
||||
("definition", SemanticTokenModifier::Definition),
|
||||
("readonly", SemanticTokenModifier::Readonly),
|
||||
("static", SemanticTokenModifier::Static),
|
||||
("deprecated", SemanticTokenModifier::Deprecated),
|
||||
("abstract", SemanticTokenModifier::Abstract),
|
||||
("async", SemanticTokenModifier::Async),
|
||||
("modification", SemanticTokenModifier::Modification),
|
||||
("documentation", SemanticTokenModifier::Documentation),
|
||||
("defaultLibrary", SemanticTokenModifier::DefaultLibrary),
|
||||
];
|
||||
|
||||
struct SemanticOptions {
|
||||
full: bool,
|
||||
types: Vec<Option<SemanticTokenKind>>,
|
||||
modifiers: [u16; 31],
|
||||
modifier_mask: u32,
|
||||
}
|
||||
|
||||
fn semantic_options(value: Option<&Value>) -> Result<Option<SemanticOptions>, String> {
|
||||
let Some(value) = value.filter(|value| !value.is_null() && *value != false) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let object = value
|
||||
.as_object()
|
||||
.ok_or("invalid semantic tokens capability")?;
|
||||
let supported = |name| -> Result<bool, String> {
|
||||
match object.get(name) {
|
||||
None | Some(Value::Bool(false)) => Ok(false),
|
||||
Some(Value::Bool(true) | Value::Object(_)) => Ok(true),
|
||||
_ => Err("invalid semantic tokens request capability".into()),
|
||||
}
|
||||
};
|
||||
let full = supported("full")?;
|
||||
if !full && !supported("range")? {
|
||||
return Ok(None);
|
||||
}
|
||||
let types = value["legend"]["tokenTypes"]
|
||||
.as_array()
|
||||
.filter(|items| items.len() <= 65536)
|
||||
.ok_or("invalid semantic token type legend")?;
|
||||
let names = value["legend"]["tokenModifiers"]
|
||||
.as_array()
|
||||
.ok_or("invalid semantic token modifier legend")?;
|
||||
let types = types
|
||||
.iter()
|
||||
.map(|value| {
|
||||
let name = value.as_str().ok_or("invalid semantic token type legend")?;
|
||||
Ok(SEMANTIC_TYPES
|
||||
.iter()
|
||||
.find(|(candidate, _)| *candidate == name)
|
||||
.map(|(_, kind)| *kind))
|
||||
})
|
||||
.collect::<Result<Vec<_>, String>>()?;
|
||||
let mut modifiers = [0; 31];
|
||||
for (index, value) in names.iter().enumerate() {
|
||||
let name = value
|
||||
.as_str()
|
||||
.ok_or("invalid semantic token modifier legend")?;
|
||||
if let Some(slot) = modifiers.get_mut(index) {
|
||||
*slot = SEMANTIC_MODIFIERS
|
||||
.iter()
|
||||
.find(|(candidate, _)| *candidate == name)
|
||||
.map_or(0, |(_, modifier)| *modifier as u16);
|
||||
}
|
||||
}
|
||||
Ok(Some(SemanticOptions {
|
||||
full,
|
||||
types,
|
||||
modifiers,
|
||||
modifier_mask: (1u32 << names.len().min(31)) - 1,
|
||||
}))
|
||||
}
|
||||
|
||||
// Both cursors only advance: strict codepoint boundaries and outward grapheme
|
||||
// rounding cost O(document + tokens), including many tokens in one grapheme.
|
||||
struct SemanticLine<'a> {
|
||||
chars: std::str::Chars<'a>,
|
||||
graphemes: unicode_segmentation::Graphemes<'a>,
|
||||
units: usize,
|
||||
bytes: usize,
|
||||
column: usize,
|
||||
grapheme_start: usize,
|
||||
grapheme_end: usize,
|
||||
}
|
||||
|
||||
impl<'a> SemanticLine<'a> {
|
||||
fn new(line: &'a str) -> Self {
|
||||
let mut graphemes = line.graphemes(true);
|
||||
let grapheme_end = graphemes.next().map_or(0, str::len);
|
||||
Self {
|
||||
chars: line.chars(),
|
||||
graphemes,
|
||||
units: 0,
|
||||
bytes: 0,
|
||||
column: 0,
|
||||
grapheme_start: 0,
|
||||
grapheme_end,
|
||||
}
|
||||
}
|
||||
|
||||
fn column(&mut self, target: usize, end: bool) -> Result<usize, String> {
|
||||
while self.units < target {
|
||||
let Some(character) = self.chars.next() else {
|
||||
if end {
|
||||
break; // LSP: an overlong end truncates at EOL, never wraps.
|
||||
}
|
||||
return Err("semantic token start is beyond its line".into());
|
||||
};
|
||||
let next = self.units + character.len_utf16();
|
||||
if next > target {
|
||||
return Err("semantic token splits a UTF-16 surrogate pair".into());
|
||||
}
|
||||
self.units = next;
|
||||
self.bytes += character.len_utf8();
|
||||
}
|
||||
while self.grapheme_end > self.grapheme_start && self.bytes >= self.grapheme_end {
|
||||
self.grapheme_start = self.grapheme_end;
|
||||
self.column += 1;
|
||||
self.grapheme_end += self.graphemes.next().map_or(0, str::len);
|
||||
}
|
||||
Ok(self.column + usize::from(end && self.bytes > self.grapheme_start))
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_semantic_tokens(
|
||||
text: &str,
|
||||
result: &Value,
|
||||
options: &SemanticOptions,
|
||||
) -> Result<Option<Arc<[SemanticToken]>>, String> {
|
||||
if result.is_null() {
|
||||
return Ok(None);
|
||||
}
|
||||
if result
|
||||
.get("resultId")
|
||||
.is_some_and(|value| !value.is_string())
|
||||
{
|
||||
return Err("invalid semantic token result ID".into());
|
||||
}
|
||||
let data = result["data"]
|
||||
.as_array()
|
||||
.ok_or("invalid semantic token data")?;
|
||||
if !data.len().is_multiple_of(5) {
|
||||
return Err("incomplete semantic token data".into());
|
||||
}
|
||||
let count = data.len() / 5;
|
||||
if count > MAX_BODY / std::mem::size_of::<SemanticToken>() {
|
||||
return Err("decoded semantic tokens exceed 4 MiB".into());
|
||||
}
|
||||
if count == 0 {
|
||||
return Ok(None);
|
||||
}
|
||||
let text = normalized(text);
|
||||
let mut lines = text.split('\n');
|
||||
let mut line = SemanticLine::new(lines.next().unwrap());
|
||||
let mut row = 0usize;
|
||||
let mut start = 0usize;
|
||||
let mut previous_end = 0usize;
|
||||
let mut tokens = Vec::with_capacity(count);
|
||||
for chunk in data.chunks_exact(5) {
|
||||
let integer = |index: usize| -> Result<usize, String> {
|
||||
chunk[index]
|
||||
.as_u64()
|
||||
.filter(|value| *value <= MAX_VERSION)
|
||||
.map(|value| value as usize)
|
||||
.ok_or_else(|| "invalid semantic token integer".into())
|
||||
};
|
||||
let delta_row = integer(0)?;
|
||||
let delta_start = integer(1)?;
|
||||
let length = integer(2)?;
|
||||
let kind = integer(3)?;
|
||||
let mask = integer(4)? as u32;
|
||||
let next_row = row
|
||||
.checked_add(delta_row)
|
||||
.filter(|value| *value <= MAX_VERSION as usize)
|
||||
.ok_or("semantic token row overflow")?;
|
||||
if delta_row != 0 {
|
||||
for _ in row..next_row {
|
||||
line =
|
||||
SemanticLine::new(lines.next().ok_or("semantic token row is out of bounds")?);
|
||||
}
|
||||
row = next_row;
|
||||
start = delta_start;
|
||||
previous_end = 0;
|
||||
} else {
|
||||
start = start
|
||||
.checked_add(delta_start)
|
||||
.filter(|value| *value <= MAX_VERSION as usize)
|
||||
.ok_or("semantic token column overflow")?;
|
||||
}
|
||||
let end = start
|
||||
.checked_add(length)
|
||||
.ok_or("semantic token length overflow")?;
|
||||
let token_kind = options
|
||||
.types
|
||||
.get(kind)
|
||||
.filter(|_| kind < 65536)
|
||||
.ok_or("semantic token type is outside its legend")?;
|
||||
if mask & !options.modifier_mask != 0 {
|
||||
return Err("semantic token modifier is outside its legend".into());
|
||||
}
|
||||
if start < previous_end {
|
||||
return Err("overlapping semantic tokens are unsupported".into());
|
||||
}
|
||||
let start_column = line.column(start, false)?;
|
||||
let end_column = line.column(end, true)?;
|
||||
previous_end = end.min(line.units);
|
||||
if length == 0 || start_column >= end_column {
|
||||
continue;
|
||||
}
|
||||
if let Some(kind) = token_kind {
|
||||
let mut modifiers = 0;
|
||||
let mut remaining = mask;
|
||||
while remaining != 0 {
|
||||
modifiers |= options.modifiers[remaining.trailing_zeros() as usize];
|
||||
remaining &= remaining - 1;
|
||||
}
|
||||
tokens.push(SemanticToken {
|
||||
start: TextPosition {
|
||||
row,
|
||||
column: start_column,
|
||||
},
|
||||
end: TextPosition {
|
||||
row,
|
||||
column: end_column,
|
||||
},
|
||||
kind: *kind,
|
||||
modifiers,
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok((!tokens.is_empty()).then(|| tokens.into()))
|
||||
}
|
||||
|
||||
fn sync_options(value: &Value) -> Result<SyncOptions, String> {
|
||||
if let Some(kind) = value.as_u64().filter(|kind| *kind <= 2) {
|
||||
return Ok(SyncOptions {
|
||||
@@ -2064,6 +2516,67 @@ fn uri_path(uri: &str) -> Result<PathBuf, String> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn semantic_decoder_validates_raw_coordinates_before_grapheme_rounding() {
|
||||
let options = semantic_options(Some(&json!({
|
||||
"full": true,
|
||||
"legend": {"tokenTypes": ["custom", "function"],
|
||||
"tokenModifiers": ["custom", "readonly", "declaration", "async"]}
|
||||
})))
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let tokens = decode_semantic_tokens(
|
||||
"A😀e\u{301}Z\r\n\r\nx",
|
||||
&json!({"data": [
|
||||
0, 1, 2, 1, 2,
|
||||
0, 2, 1, 1, 12,
|
||||
0, 1, 1, 1, 0,
|
||||
0, 1, 999, 0, 0,
|
||||
1, 0, 1, 1, 0,
|
||||
1, 0, 0, 1, 0,
|
||||
0, 0, 100, 1, 0
|
||||
]}),
|
||||
&options,
|
||||
)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(tokens.len(), 4);
|
||||
assert_eq!(tokens[0].start, TextPosition { row: 0, column: 1 });
|
||||
assert_eq!(tokens[0].end, TextPosition { row: 0, column: 2 });
|
||||
assert_eq!(tokens[0].modifiers, SemanticTokenModifier::Readonly as u16);
|
||||
assert_eq!(tokens[1].start, TextPosition { row: 0, column: 2 });
|
||||
assert_eq!(tokens[1].end, TextPosition { row: 0, column: 3 });
|
||||
assert_eq!(
|
||||
tokens[1].modifiers,
|
||||
SemanticTokenModifier::Declaration as u16 | SemanticTokenModifier::Async as u16
|
||||
);
|
||||
assert_eq!(tokens[2].start, tokens[1].start);
|
||||
assert_eq!(tokens[3].end, TextPosition { row: 2, column: 1 });
|
||||
for data in [
|
||||
json!([0, 0, 1, 1]),
|
||||
json!([-1, 0, 1, 1, 0]),
|
||||
json!([0, 0.5, 1, 1, 0]),
|
||||
json!([0, 0, 1, 1, 2147483648u64]),
|
||||
json!([0, 0, 1, 65536, 0]),
|
||||
json!([0, 0, 1, 2, 0]),
|
||||
json!([0, 0, 1, 1, 16]),
|
||||
json!([0, 2, 1, 0, 0]),
|
||||
json!([0, 1, 1, 1, 0]),
|
||||
json!([0, 7, 1, 1, 0]),
|
||||
json!([2, 0, 1, 1, 0]),
|
||||
json!([0, 1, 2, 1, 0, 0, 1, 1, 1, 0]),
|
||||
] {
|
||||
assert!(
|
||||
decode_semantic_tokens("A😀e\u{301}Z\n", &json!({"data": data}), &options).is_err(),
|
||||
"{data}"
|
||||
);
|
||||
}
|
||||
let count = MAX_BODY / std::mem::size_of::<SemanticToken>() + 1;
|
||||
assert!(
|
||||
decode_semantic_tokens("x", &json!({"data": vec![0; count * 5]}), &options).is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unicode_positions_and_local_uri_boundaries() {
|
||||
let text = "A😀e\u{301}Z\n";
|
||||
|
||||
Reference in New Issue
Block a user