T M4.5: server→client refresh requests (inlayHint + semanticTokens)
Backlog item 1, combined (1a+1b) now that semantic tokens (#23) is on main. Lets servers tell us cached inlay hints / semantic tokens are stale and have the client re-pull, instead of the on-demand- only v1 model. - src/lsp.rs: advertise workspace.inlayHint.refreshSupport=true and workspace.semanticTokens.refreshSupport=true. - builtin/runtime/lsp.lua: generalize the L3 workspace/applyEdit pump into handle_server_requests; add branches for workspace/inlayHint/refresh and workspace/semanticTokens/refresh — reply null per spec, then repull_for_attachments re-issues the matching request (request_inlay_hint / request_semantic_tokens) for every attached document on that server. Fire-and-forget; the response absorbs via its existing route like the command path. Only attachment servers are drained (directly-spawned test servers untouched). - pmacs_fake_lsp.rs: `inlayrefresh` / `semantictokensrefresh` modes send the respective server→client refresh request at `initialized` (mirrors the wsconfig pattern). - tests/m4_acceptance.rs: m4_18 / m4_19 attach via config and assert the store populates purely from the server-driven refresh chain — no explicit inlay_hints()/semantic_tokens() call. Gates: lib 1285/0, m4 74/0, m8_1 10/0, m8_9 26/0, m8_10 19/0, m9_1 18/0, m5_8 5/0, m11_5 (--features crdt) 2/0; fmt + clippy clean. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
cf1c4c6ddc
commit
7ee1db5028
|
|
@ -16,8 +16,10 @@
|
|||
-- `workspace/applyEdit` (L3), ordered resource-op edits
|
||||
-- (create/rename/delete file) with buffer-registry reconciliation
|
||||
-- (L4), inlay hints, and semantic tokens (each data + modeline;
|
||||
-- wiring them into rendering is a separate rendering milestone).
|
||||
-- File-watch capability registration is a later layer.
|
||||
-- wiring them into rendering is a separate rendering milestone),
|
||||
-- incl. the server→client `workspace/inlayHint/refresh` and
|
||||
-- `workspace/semanticTokens/refresh` requests. File-watch
|
||||
-- capability registration is a later layer.
|
||||
|
||||
pmacs.lsp = pmacs.lsp or {}
|
||||
pmacs.lsp.config = pmacs.lsp.config or {}
|
||||
|
|
@ -431,22 +433,42 @@ local function apply_workspace_edit(ops)
|
|||
return edit_total, files, res_ops
|
||||
end
|
||||
|
||||
-- T M4.5 L3 — server→client `workspace/applyEdit` pump.
|
||||
-- Re-pull a per-`(server, uri)` store for every buffer attached to
|
||||
-- `sid`. Fire-and-forget: the response absorbs into its store via the
|
||||
-- request's route, exactly like the explicit command path — no await
|
||||
-- needed. `request_fn(sid, uri)` issues the re-pull.
|
||||
local function repull_for_attachments(sid, request_fn)
|
||||
for _, rec in pairs(attachments) do
|
||||
if rec.server == sid and rec.uri then
|
||||
pcall(request_fn, sid, rec.uri)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- T M4.5 — server→client request pump.
|
||||
--
|
||||
-- After a code action's `executeCommand`, servers (rust-analyzer,
|
||||
-- gopls, …) deliver the actual change as a `workspace/applyEdit`
|
||||
-- *request* — surfaced by the manager as a `request` event on the
|
||||
-- server's event stream (the same "expose the request to the
|
||||
-- consumer" path as `workspace/configuration`, minus the built-in
|
||||
-- answer). We drain attachment servers' events each async tick, apply
|
||||
-- any applyEdit through the shared applier, and reply `{ applied }`.
|
||||
-- Some server→client *requests* are surfaced by the manager as a
|
||||
-- `request` event on the server's event stream (the same "expose the
|
||||
-- request to the consumer" path as `workspace/configuration`, minus a
|
||||
-- built-in answer). Each async tick we drain attachment servers'
|
||||
-- events and handle:
|
||||
--
|
||||
-- * `workspace/applyEdit` (L3) — apply the edit through the shared
|
||||
-- applier, reply `{ applied }`. After a code action's
|
||||
-- `executeCommand`, servers (rust-analyzer, gopls, …) deliver the
|
||||
-- actual change this way.
|
||||
-- * `workspace/inlayHint/refresh` /
|
||||
-- `workspace/semanticTokens/refresh` — the server signals its
|
||||
-- cached hints/tokens are stale; reply `null` and re-pull that
|
||||
-- family for every attached document so the matching store
|
||||
-- (`pmacs.inlay_hint` / `pmacs.semantic_tokens`) stays fresh.
|
||||
--
|
||||
-- Only servers in `attachments` are drained, so a test (or package)
|
||||
-- that owns its own directly-spawned server and reads its events
|
||||
-- itself is unaffected. Server ids are snapshotted before the loop
|
||||
-- because `apply_workspace_edit` → `find_or_open` can attach a new
|
||||
-- buffer mid-iteration (mutating `attachments`).
|
||||
local function handle_apply_edit_requests()
|
||||
local function handle_server_requests()
|
||||
local sids, seen = {}, {}
|
||||
for _, rec in pairs(attachments) do
|
||||
local sid = rec.server
|
||||
|
|
@ -475,6 +497,18 @@ local function handle_apply_edit_requests()
|
|||
local result = { applied = applied }
|
||||
if not applied then result.failureReason = tostring(reason) end
|
||||
pcall(pmacs.lsp.send_response, sid, ev.request_id, result)
|
||||
elseif ev.kind == "request"
|
||||
and ev.method == "workspace/inlayHint/refresh" then
|
||||
-- Result is `null` on success per the LSP spec; then
|
||||
-- re-pull so the store reflects the server's new state.
|
||||
pcall(pmacs.lsp.send_response, sid, ev.request_id, nil)
|
||||
repull_for_attachments(sid, function(s, uri)
|
||||
pmacs.lsp.request_inlay_hint(s, uri, 0, 0, 0xFFFFF, 0)
|
||||
end)
|
||||
elseif ev.kind == "request"
|
||||
and ev.method == "workspace/semanticTokens/refresh" then
|
||||
pcall(pmacs.lsp.send_response, sid, ev.request_id, nil)
|
||||
repull_for_attachments(sid, pmacs.lsp.request_semantic_tokens)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -485,7 +519,7 @@ if pmacs._async and pmacs._async.tick then
|
|||
local _prior_async_tick = pmacs._async.tick
|
||||
pmacs._async.tick = function(...)
|
||||
local ret = _prior_async_tick(...)
|
||||
pcall(handle_apply_edit_requests)
|
||||
pcall(handle_server_requests)
|
||||
return ret
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -159,6 +159,29 @@ fn main() {
|
|||
});
|
||||
write_frame(&mut stdout, &req);
|
||||
}
|
||||
// T M4.5 `inlayrefresh` / `semantictokensrefresh`: right
|
||||
// after initialize, signal that cached inlay hints /
|
||||
// semantic tokens are stale via the matching server→client
|
||||
// refresh request. The client must answer (null) and
|
||||
// re-pull the corresponding `textDocument/*`.
|
||||
("initialized", _) if mode == "inlayrefresh" => {
|
||||
let req = serde_json::json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": 9200,
|
||||
"method": "workspace/inlayHint/refresh",
|
||||
"params": serde_json::Value::Null
|
||||
});
|
||||
write_frame(&mut stdout, &req);
|
||||
}
|
||||
("initialized", _) if mode == "semantictokensrefresh" => {
|
||||
let req = serde_json::json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": 9201,
|
||||
"method": "workspace/semanticTokens/refresh",
|
||||
"params": serde_json::Value::Null
|
||||
});
|
||||
write_frame(&mut stdout, &req);
|
||||
}
|
||||
("initialized", _) => {}
|
||||
("shutdown", Some(idv)) => {
|
||||
let resp = serde_json::json!({
|
||||
|
|
|
|||
|
|
@ -2926,6 +2926,14 @@ fn default_capabilities() -> Value {
|
|||
"configuration": true,
|
||||
"workspaceFolders": true,
|
||||
"didChangeConfiguration": { "dynamicRegistration": false },
|
||||
// T M4.5 — let servers tell us cached inlay hints /
|
||||
// semantic tokens are stale via a server→client
|
||||
// `workspace/inlayHint/refresh` /
|
||||
// `workspace/semanticTokens/refresh` request. The Lua
|
||||
// server-request pump answers each and re-pulls the
|
||||
// affected documents into the matching store.
|
||||
"inlayHint": { "refreshSupport": true },
|
||||
"semanticTokens": { "refreshSupport": true },
|
||||
},
|
||||
"textDocument": {
|
||||
"synchronization": {
|
||||
|
|
|
|||
|
|
@ -3868,6 +3868,111 @@ fn m4_17_lua_surface_drives_semantic_tokens() {
|
|||
assert_eq!(type0_name, "namespace");
|
||||
}
|
||||
|
||||
/// T M4.5 — server-driven inlay-hint refresh. The `inlayrefresh`
|
||||
/// fake sends a `workspace/inlayHint/refresh` request right after
|
||||
/// `initialized`. The bundle's server-request pump must answer it
|
||||
/// and *re-pull* inlay hints for the attached document — so the
|
||||
/// `pmacs.inlay_hint` store populates without anyone ever calling
|
||||
/// `pmacs.lsp.inlay_hints()`.
|
||||
#[test]
|
||||
fn m4_18_inlay_hint_refresh_repulls_via_server_request() {
|
||||
use pmacs::editor::EditorState;
|
||||
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let a_path = dir.path().join("a.rs");
|
||||
std::fs::write(&a_path, b"fn a() {}\n\n").expect("write a");
|
||||
let a_disp = a_path.display().to_string();
|
||||
|
||||
let mut state = EditorState::new();
|
||||
let fake = fake_lsp_path();
|
||||
state
|
||||
.lua_host
|
||||
.lua()
|
||||
.load(format!(
|
||||
"pmacs.lsp.config.rust = {{
|
||||
command = '{fake}',
|
||||
env = {{ PMACS_FAKE_LSP_MODE = 'inlayrefresh' }},
|
||||
}}"
|
||||
))
|
||||
.exec()
|
||||
.expect("override rust config");
|
||||
state
|
||||
.lua_host
|
||||
.lua()
|
||||
.load(format!("pmacs.buffer.find_or_open('{a_disp}')"))
|
||||
.exec()
|
||||
.expect("open a.rs");
|
||||
|
||||
// No explicit `pmacs.lsp.inlay_hints()` call: the store filling
|
||||
// is driven purely by the server's refresh request.
|
||||
let flag = format!(
|
||||
"(function() \
|
||||
local sid \
|
||||
for _,r in ipairs(pmacs.lsp.list()) do \
|
||||
if r.state and r.state.kind=='initialized' then sid=r.id end \
|
||||
end \
|
||||
if not sid then return false end \
|
||||
local h = pmacs.inlay_hint.hints(sid, 'file://{a_disp}') \
|
||||
return h ~= nil and #h > 0 \
|
||||
end)()"
|
||||
);
|
||||
assert!(
|
||||
pump_lua_flag(&mut state, &flag, 5),
|
||||
"server-driven inlayHint/refresh never re-pulled hints into the store"
|
||||
);
|
||||
}
|
||||
|
||||
/// T M4.5 — server-driven semantic-tokens refresh. The
|
||||
/// `semantictokensrefresh` fake sends `workspace/semanticTokens/
|
||||
/// refresh` right after `initialized`; the pump must answer it and
|
||||
/// re-pull, so `pmacs.semantic_tokens` populates with no explicit
|
||||
/// `pmacs.lsp.semantic_tokens()` call.
|
||||
#[test]
|
||||
fn m4_19_semantic_tokens_refresh_repulls_via_server_request() {
|
||||
use pmacs::editor::EditorState;
|
||||
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let a_path = dir.path().join("a.rs");
|
||||
std::fs::write(&a_path, b"fn a() {}\n\n").expect("write a");
|
||||
let a_disp = a_path.display().to_string();
|
||||
|
||||
let mut state = EditorState::new();
|
||||
let fake = fake_lsp_path();
|
||||
state
|
||||
.lua_host
|
||||
.lua()
|
||||
.load(format!(
|
||||
"pmacs.lsp.config.rust = {{
|
||||
command = '{fake}',
|
||||
env = {{ PMACS_FAKE_LSP_MODE = 'semantictokensrefresh' }},
|
||||
}}"
|
||||
))
|
||||
.exec()
|
||||
.expect("override rust config");
|
||||
state
|
||||
.lua_host
|
||||
.lua()
|
||||
.load(format!("pmacs.buffer.find_or_open('{a_disp}')"))
|
||||
.exec()
|
||||
.expect("open a.rs");
|
||||
|
||||
let flag = format!(
|
||||
"(function() \
|
||||
local sid \
|
||||
for _,r in ipairs(pmacs.lsp.list()) do \
|
||||
if r.state and r.state.kind=='initialized' then sid=r.id end \
|
||||
end \
|
||||
if not sid then return false end \
|
||||
local t = pmacs.semantic_tokens.tokens(sid, 'file://{a_disp}') \
|
||||
return t ~= nil and #t > 0 \
|
||||
end)()"
|
||||
);
|
||||
assert!(
|
||||
pump_lua_flag(&mut state, &flag, 5),
|
||||
"server-driven semanticTokens/refresh never re-pulled tokens into the store"
|
||||
);
|
||||
}
|
||||
|
||||
/// Default LSP bundle (`builtin/runtime/lsp.lua`) is wired in: the
|
||||
/// hooks are defined, the namespace tables exist, the user-facing
|
||||
/// commands are registered with the command registry, and the default
|
||||
|
|
|
|||
Loading…
Reference in New Issue