gopls / pyright / basedpyright / clangd issue server→client
`workspace/configuration` during startup and degrade (or fall back
to defaults) without a reply. pmacs advertised `configuration:false`,
so it never got the chance.
- Advertise `workspace.configuration: true`.
- New `settings` field on the spawn spec, threaded through
lua_to_lsp_spec → ensure_server (pmacs.lsp.config[lang].settings).
- handle_request intercepts `workspace/configuration` (mirrors the
publishDiagnostics interception in handle_notification): each
item's dotted `section` resolves against the server's settings via
resolve_config_section; one array element per item; unknown
sections answer `null` (the spec's "not configured" signal,
distinct from a configured null). All other server→client requests
still surface as a `Request` event for the consumer.
- The Python default now ships
`python.analysis.typeCheckingMode = "basic"` (+ basedpyright.*
alias), so the #12 basedpyright-noise concern is now actually
fixed rather than only documented; a project pyrightconfig.json /
[tool.pyright] still wins where present.
Scoped: `scopeUri` ignored (single-root; same settings regardless
of scope) until multi-root, a separate deferred item.
Tests: exhaustive resolve_config_section unit test (dotted paths,
configured-null vs unknown-null, whole-object for absent section);
new `wsconfig` fake mode pulls config at `initialized` and echoes
pmacs's answer back; end-to-end test asserts the configured section
round-trips.
Gate: fmt clean; clippy --all-targets -D warnings clean; lib
1230/0; m4_acceptance 62/0; m9_1 18/0; m8_1/m8_9/m8_10 green.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
First real language on the now-correct async + UTF-16 substrate.
- pmacs.lsp.config.python → `basedpyright-langserver --stdio`.
basedpyright (MIT fork of pyright) re-enables inlay hints /
semantic tokens in the OSS server that upstream pyright withholds
for Pylance — matches the deferred-feature roadmap. No init_options:
strictness is project config (pyrightconfig.json / [tool.pyright]);
pmacs does not yet advertise workspace/configuration, so an
editor-side typeCheckingMode would not be honoured regardless
(documented in-line, with the upstream-pyright one-field override).
- LSP language detection separated from tree-sitter. pmacs.parse's
extension registry is grammar-gated (rejects "python" — no bundled
grammar). New user-extensible pmacs.lsp.filetypes map (py/pyi →
python); active_buffer_language() tries grammar-backed parse first
(rust/.rs etc. unchanged) then falls back to the map, so a language
with a server but no grammar still auto-attaches.
- PATH-gated acceptance test mirroring m4_5_rust_analyzer_initializes;
unique assertion: a real basedpyright must negotiate a
positionEncoding pmacs can encode — validates Option B against a
real strict server, not just the fake. Skips when absent.
Gate: fmt clean; clippy --all-targets -D warnings clean; lib
1229/0; m4_acceptance 61/0; m9_1 18/0; m8_1/m8_9/m8_10 green.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replaces the editor-blocking `poll_until` tick-loop in the LSP UX
with the M9.1 external-settle pattern: each `textDocument/*` request
registers a pending entry via `AsyncRuntime::register_external` and
returns the job id; the JSON-RPC response (or a server-teardown /
cancel / timeout) settles it, resuming a `Handle:await()` coroutine.
No worker thread is occupied for the round-trip.
Hybrid result delivery (operator decision): the response is absorbed
into the typed stores *and* carried through the Handle. The
completion popup and diagnostics gutter keep reading the stores
untouched; request/response command code awaits the value directly.
Core (src/lsp.rs):
- `LspManager` gains `runtime: SharedAsyncRuntime` (threaded through
`make_lsp_manager` / editor.rs, mirroring `make_mcp_manager`) plus
a `(server, request_id)` -> PendingExternal awaiter table parallel
to `pending_routes`.
- `request_*` return the async `JobId` (`= u64`, signature
unchanged; no caller consumed the old JSON-RPC id).
- `handle_response` settles every non-cancelled awaiter ok/failed
alongside store absorption; null result still wakes await with nil.
- Awaiters drain-cancelled at all three `pending_routes` purge sites
(restart generation flip / terminal exit / forget) so a coroutine
cannot park on a server that went away.
- Per-tick sweep: per-awaiter cancellation (Handle:cancel() or
supersede via a stable `lsp:{method}:{sid}:{uri}` key), with
`$/cancelRequest` + `cancelled_rids` on abandonment to drop the
cancel/response race silently. Mirrors mcp.rs.
- Per-request timeout (default 10s, `pmacs.lsp.set_request_timeout_ms`):
an alive-but-silent server fails the await instead of hanging.
Lua surface:
- `_request_*_raw` job-id bindings (mirror `pmacs.mcp._send_request_raw`).
- builtin/runtime/lsp.lua: Handle wrappers + the four commands
rewritten to spawn `pmacs.async` coroutines that `:await()`;
`poll_until` removed. Server-gone / error surface as structured
await failures in the modeline.
Tests:
- pmacs_fake_lsp: `error` / `silent` modes for deterministic
failure-path coverage.
- 5 end-to-end await-path tests (success+store, server-error->failed,
server-stop->cancelled, timeout->failed, supersede->cancelled).
Gate: fmt clean; clippy --all-targets -D warnings clean; lib
1223/0; m4_acceptance 58/0; m9_1_acceptance 18/0 (MCP unaffected).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>