From 822aa946f47e4815566277bc2e55485ed59ff790 Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Tue, 19 May 2026 15:04:19 -0400 Subject: [PATCH] Step 1: SemanticTokenStore/InlayHintStore::for_uri (+ repair main) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- src/inlay_hint.rs | 47 +++++++++++++++++++++++++++++++ src/semantic_tokens.rs | 64 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+) diff --git a/src/inlay_hint.rs b/src/inlay_hint.rs index d336519..295bd7b 100644 --- a/src/inlay_hint.rs +++ b/src/inlay_hint.rs @@ -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::() + .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()); + } } diff --git a/src/semantic_tokens.rs b/src/semantic_tokens.rs index a753927..cd9dfb5 100644 --- a/src/semantic_tokens.rs +++ b/src/semantic_tokens.rs @@ -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::() { + 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] });