Commit Graph

8 Commits

Author SHA1 Message Date
Levi Neuwirth f7267cd720 T M4.5 L1: cross-file nav foundations — per-buffer file_path, jump ring, x-file go-to-def
Lays the groundwork for WorkspaceEdit/rename (L2+) by making
navigation cross-file-correct.

- Relocate file_path/file_meta from the EditorCore global onto
  Buffer itself, so each buffer keeps its own filesystem identity
  across cross-file navigation. Accessors + registry/editor/lua/
  semantic_render call sites migrated; zero behavioural change for
  single-file flows.
- uri->path: project_index::uri_to_path made pub; pmacs.lsp.path_for_uri.
- find-or-open: BufferRegistry::find_by_path + pmacs.buffer.find_or_open
  dedups an already-open file instead of spawning a duplicate buffer
  (SP-4 Gap A).
- Bounded jump ring on EditorCore (cap 64, oldest-evict, stale-buffer
  skip): push_jump/jump_back + pmacs.editor.* bindings + lsp.jump-back
  command bound to M-,.
- pmacs.lsp.go_to_definition cross-file branch: decode URI ->
  push_jump -> find_or_open -> reposition, with a failure path that
  unwinds the pushed origin. ensure_server now passes cfg.env through.

Tests: 5 jump-ring unit tests; m4_12_cross_file_go_to_definition_and_
jump_back end-to-end via a new `defenv` fake-LSP mode. All gates green
(lib 1262/0, m4 67/0, m8_1/m8_9/m8_10, m9_1, m11_5 --features crdt 2/0).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 09:53:29 -04:00
Levi Neuwirth 80c0679a5b T M4.5: documentSymbol / workspace symbol / documentHighlight
Completes the read-only LSP feature set (everything except edits).
Same request→store→Lua pattern as the nav batch; these three need
new parsing (new response shapes), so they were deliberately split
from the Location-family PR (#16).

- src/symbol.rs: one flat Symbol type for both symbol requests.
  from_lsp_value handles BOTH LSP shapes — hierarchical
  DocumentSymbol[] (flattened with depth + parent chain) and flat
  SymbolInformation[]/WorkspaceSymbol[] (location.uri, range
  optional for WorkspaceSymbol). Scope-keyed (Document(uri) vs
  Workspace(query)) so an outline and a query don't collide.
- src/document_highlight.rs: range + DocumentHighlightKind (absent
  defaults to Text=1 per spec), (server,uri)-keyed.
- lsp.rs: three ResponseRoute variants + absorb arms + request
  methods. documentSymbol/documentHighlight ranges convert via the
  requested-doc codec; workspace/symbol results are cross-file →
  route uri "" → non-destructive passthrough (same rule as
  cross-file definition).
- Lua: raw bindings + pmacs.document_symbol / .workspace_symbol /
  .document_highlight read surfaces (the new LSP Symbol is aliased
  to avoid the pre-existing project_index::Symbol name clash);
  lsp.lua wrappers + an lsp.document-symbols command on C-c o
  (modeline summary; outline buffer is future UX).
- Tests: 6 parser unit tests (hierarchical depth/parent, flat
  SymbolInformation, range-less WorkspaceSymbol, highlight kind
  default, scope non-collision) + an e2e driving all three through
  the async bridge asserting shape correctness.

Also includes a pre-existing rustfmt normalization of the #15
semantic-frontend files (protocol.rs / semantic_client.rs /
semantic_render.rs) — main was not rustfmt-clean there after the #15
merge; bundled here per operator decision so the fmt gate is green.

Gate: fmt clean; clippy --all-targets -D warnings clean; lib
1257/0; m4_acceptance 66/0; m9_1 18/0; m8_1/m8_9/m8_10 green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 22:15:03 -04:00
Levi Neuwirth 96780b547d T M4.5: nav requests — references/declaration/typeDefinition/implementation
The "cheap batch" subset that is genuine template-fill: these four
return the exact `Location | Location[] | LocationLink[] | null`
shape `textDocument/definition` already parses, so no new parsing —
only a kind discriminator so they don't collide on (server, uri).

- src/locations.rs: a (server, uri, kind)-keyed store whose value
  type is the reused crate::definition::DefinitionResponse. The
  proven definition store + Lua API are untouched.
- lsp.rs: ResponseRoute::Locations { uri, kind }; one absorb arm; a
  DRY request_locations helper + request_references /
  request_declaration / request_type_definition /
  request_implementation. references sends
  context.includeDeclaration. Supersede keys derive from each
  kind's distinct method, so the four don't cancel each other.
- Lua: _request_*_raw bindings + install_locations exposing
  pmacs.references / .declaration / .type_definition /
  .implementation ({ locations, clear }, mirroring pmacs.definition,
  reusing definition_response_to_lua). lsp.lua Handle wrappers + a
  lsp.find-references command bound to M-? (modeline summary;
  references-list buffer is future UX, like the hover panel).
- Tests: locations.rs unit tests (kind labels distinct; keys don't
  collide); e2e driving all four through the async bridge and
  asserting each routes to its own kind slot (fake returns distinct
  lines 11/21/31/41).

Scoped: documentSymbol / workspaceSymbol / documentHighlight return
different shapes (new parsing) — a separate follow-up, not crammed
in here.

Gate: fmt clean; clippy --all-targets -D warnings clean; lib
1232/0; m4_acceptance 65/0; m9_1 18/0; m8_1/m8_9/m8_10 green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 21:39:02 -04:00
Levi Neuwirth fb4c18e490 T M4.5: default C/C++ (clangd) + Go (gopls) language entries
Two more real languages on the proven async + UTF-16 + config-pull
base. Pure pattern application of the #12 Python shape.

- pmacs.lsp.config.c / .cpp → clangd `--background-index`. One
  binary serves both; separate entries only so the didOpen
  languageId is accurate. No `settings`: clangd's project model is
  compile_commands.json / compile_flags.txt, not
  workspace/configuration (documented in-line).
- pmacs.lsp.config.go → gopls (no args = stdio). settings =
  { gopls = {} } so the #13 workspace/configuration pull is answered
  "use defaults" (present, not null — gopls prefers that).
- pmacs.lsp.filetypes extended: c/h → c (.h defaults to C,
  remappable); cpp cc cxx hpp hh hxx ipp inl cppm → cpp; go → go.
- Two PATH-gated acceptance tests via a shared DRY helper, mirroring
  m4_5_basedpyright: reach Initialized + assert the negotiated
  positionEncoding is one pmacs can encode. Skip cleanly when the
  binary is absent.

clangd is on the dev PATH, so its test ran for real: the full stack
(async bridge + Option B UTF-16 + registry + filetypes) is validated
end-to-end against a real strict-default C/C++ server, closing the
"validate UTF-16 against a real strict server" gap from the Option B
evaluation. gopls test skips here; runs wherever gopls is installed.

Gate: fmt clean; clippy --all-targets -D warnings clean; lib
1230/0; m4_acceptance 64/0 (clangd ran live); m9_1 18/0;
m8_1/m8_9/m8_10 green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 21:20:19 -04:00
Levi Neuwirth 195949f73c T M4.5: answer workspace/configuration pull from per-server settings
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>
2026-05-18 21:07:24 -04:00
Levi Neuwirth 9ab931d6dd T M4.5: default Python LSP (basedpyright) + LSP-independent filetypes
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>
2026-05-18 20:51:12 -04:00
Levi Neuwirth ad530a5beb T M4.5 async bridge: LSP requests settle async-runtime jobs
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>
2026-05-18 19:36:17 -04:00
Levi Neuwirth 4da4b09d5d Initial commit: v0.1.0 2026-05-03 19:51:06 -04:00