Step 1: SemanticTokenStore/InlayHintStore::for_uri (+ repair main)

Adds the URI-only store views the semantic-frontend producer arc
needs. The stores key by (server, uri); the producer only knows the
document, so `for_uri` scans by uri and picks the lowest *numeric*
server id deterministically (HashMap order is otherwise
nondeterministic; lowest id == oldest/primary attachment). Blending
multiple servers' styling on one buffer is a deferred open question.

Inlay-hint `for_uri` returns no server: Step 0 established inlay
positions are already pmacs byte offsets by the time they hit the
store (the absorb path's inbound_converted rewrites the
Position-shaped InlayHint.position), so the producer needs no
per-server encoding for them. Semantic tokens differ — start/length
stay UTF-16, hence the (server, response) tuple so the matching
LspManager::semantic_style_context can resolve encoding + legend.

Also repairs main: PR #28 (file-watch) inadvertently swept in the
uncommitted lsp.rs side of this work — SemanticStyleContext and
semantic_style_context, which call store.for_uri — without these
defining accessors, leaving main referencing an undefined method
(no method `for_uri`). This commit supplies the missing definitions,
so main compiles again.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Levi Neuwirth 2026-05-19 15:04:19 -04:00
parent 8445b6191f
commit 822aa946f4
2 changed files with 111 additions and 0 deletions

View File

@ -197,6 +197,28 @@ impl InlayHintStore {
pub fn get(&self, key: &InlayHintKey) -> Option<&InlayHintResponse> {
self.by_key.get(key)
}
/// Look up the entry for `uri` regardless of which server keyed
/// it. Mirrors [`crate::semantic_tokens::SemanticTokenStore::for_uri`]:
/// the lowest (numeric) server id wins for determinism across
/// `HashMap` order. No server is returned — unlike semantic
/// tokens, inlay-hint positions are already pmacs byte offsets by
/// the time they reach the store (the absorb path's
/// `inbound_converted` rewrites the `Position`-shaped
/// `InlayHint.position`), so the producer needs no per-server
/// encoding to place them.
#[must_use]
pub fn for_uri(&self, uri: &str) -> Option<&InlayHintResponse> {
self.by_key
.iter()
.filter(|(k, _)| k.uri == uri)
.min_by_key(|(k, _)| {
k.server
.parse::<u64>()
.map_or((u64::MAX, k.server.as_str()), |n| (n, ""))
})
.map(|(_, v)| v)
}
}
/// Cheaply-cloneable shared handle.
@ -291,4 +313,29 @@ mod tests {
s.clear(&key);
assert!(s.get(&key).is_none());
}
#[test]
fn for_uri_filters_by_uri_and_picks_lowest_server() {
let mk = |label: &str| InlayHintResponse {
hints: vec![InlayHint {
line: 0,
col: 0,
label: label.into(),
kind: None,
padding_left: false,
padding_right: false,
tooltip: None,
}],
};
let mut s = InlayHintStore::new();
s.set(InlayHintKey::new("10", "file:///a"), mk("s10"));
s.set(InlayHintKey::new("9", "file:///a"), mk("s9"));
s.set(InlayHintKey::new("2", "file:///b"), mk("s2"));
// Lowest *numeric* server id wins ("9" < "10" numerically,
// not lexicographically).
assert_eq!(s.for_uri("file:///a").unwrap().hints[0].label, "s9");
assert_eq!(s.for_uri("file:///b").unwrap().hints[0].label, "s2");
assert!(s.for_uri("file:///nope").is_none());
}
}

View File

@ -263,6 +263,39 @@ impl SemanticTokenStore {
pub fn get(&self, key: &SemanticTokenKey) -> Option<&SemanticTokensResponse> {
self.by_key.get(key)
}
/// Look up the entry for `uri` regardless of which server keyed
/// it, returning `(server, response)`. The store keys by
/// `(server, uri)` but the semantic-render producer only knows the
/// document; this is the URI-only view the diagnostics store
/// (`DiagnosticStore`) offers natively.
///
/// When more than one server has tokens for the same URI the
/// **lowest server id** wins, chosen by numeric value so the
/// result is deterministic across `HashMap` iteration order
/// (server ids are assigned monotonically, so the lowest is the
/// oldest / primary attachment). Blending styling from multiple
/// servers on one buffer is a deliberately deferred open question
/// — see `docs/semantic-frontend-protocol.md`.
#[must_use]
pub fn for_uri(&self, uri: &str) -> Option<(&str, &SemanticTokensResponse)> {
self.by_key
.iter()
.filter(|(k, _)| k.uri == uri)
.min_by_key(|(k, _)| server_sort_key(&k.server))
.map(|(k, v)| (k.server.as_str(), v))
}
}
/// Order key for picking a representative server: numeric if the id
/// parses (the normal case — ids are decimal `LspServerId::raw`),
/// else a max sentinel so unparsable ids sort last but the lookup
/// still yields *something* rather than nothing.
fn server_sort_key(server: &str) -> (u64, &str) {
match server.parse::<u64>() {
Ok(n) => (n, ""),
Err(_) => (u64::MAX, server),
}
}
/// Cheaply-cloneable shared handle.
@ -392,6 +425,37 @@ mod tests {
assert!(s.get(&key).is_none());
}
#[test]
fn for_uri_filters_by_uri_and_picks_lowest_server() {
let mk = |tok_type: u32| SemanticTokensResponse {
tokens: vec![SemanticToken {
line: 0,
start: 0,
length: 1,
token_type: tok_type,
token_modifiers: 0,
}],
result_id: None,
raw: vec![0, 0, 1, tok_type, 0],
};
let mut s = SemanticTokenStore::new();
// Same URI under two servers; ids deliberately inserted so
// numeric (not lexicographic: "10" < "9" as strings) ordering
// is what makes the test meaningful.
s.set(SemanticTokenKey::new("10", "file:///a"), mk(10));
s.set(SemanticTokenKey::new("9", "file:///a"), mk(9));
s.set(SemanticTokenKey::new("2", "file:///b"), mk(2));
let (server, resp) = s.for_uri("file:///a").expect("entry for /a");
assert_eq!(server, "9", "lowest *numeric* server id wins");
assert_eq!(resp.tokens[0].token_type, 9);
let (server_b, _) = s.for_uri("file:///b").expect("entry for /b");
assert_eq!(server_b, "2");
assert!(s.for_uri("file:///nope").is_none());
}
#[test]
fn from_lsp_value_retains_raw() {
let v = json!({ "resultId": "r1", "data": [0, 0, 4, 1, 1, 0, 5, 3, 2, 0] });