feat(language-server): lazy document state via encapsulated artifact store + pull diagnostics - #887
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yml Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (12)
📝 WalkthroughWalkthroughThis PR refactors the language server's diagnostics architecture to support LSP 3.17 pull diagnostics alongside push fallback. The project artifact store is rewritten from an edit-driven cache to a read-through model keyed by ChangesLanguage Server Diagnostics and Artifact Refactor
Estimated code review effort: 4 (Complex) | ~75 minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning Tools execution failed with the following error: Failed to run tools: 13 INTERNAL: Received RST_STREAM with code 2 (Internal server error) Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
bc000fb to
2c5a078
Compare
size-limit report 📦
|
@prisma-next/extension-author-tools
@prisma-next/mongo-runtime
@prisma-next/family-mongo
@prisma-next/sql-runtime
@prisma-next/family-sql
@prisma-next/extension-arktype-json
@prisma-next/middleware-cache
@prisma-next/mongo
@prisma-next/extension-paradedb
@prisma-next/extension-pgvector
@prisma-next/extension-postgis
@prisma-next/postgres
@prisma-next/sql-orm-client
@prisma-next/sqlite
@prisma-next/extension-supabase
@prisma-next/target-mongo
@prisma-next/adapter-mongo
@prisma-next/driver-mongo
@prisma-next/contract
@prisma-next/utils
@prisma-next/config
@prisma-next/errors
@prisma-next/framework-components
@prisma-next/operations
@prisma-next/ts-render
@prisma-next/contract-authoring
@prisma-next/ids
@prisma-next/psl-parser
@prisma-next/psl-printer
@prisma-next/cli
@prisma-next/cli-telemetry
@prisma-next/config-loader
@prisma-next/emitter
@prisma-next/language-server
@prisma-next/migration-tools
prisma-next
@prisma-next/vite-plugin-contract-emit
@prisma-next/mongo-codec
@prisma-next/mongo-contract
@prisma-next/mongo-value
@prisma-next/mongo-contract-psl
@prisma-next/mongo-contract-ts
@prisma-next/mongo-emitter
@prisma-next/mongo-schema-ir
@prisma-next/mongo-query-ast
@prisma-next/mongo-orm
@prisma-next/mongo-query-builder
@prisma-next/mongo-lowering
@prisma-next/mongo-wire
@prisma-next/sql-contract
@prisma-next/sql-errors
@prisma-next/sql-operations
@prisma-next/sql-schema-ir
@prisma-next/sql-contract-psl
@prisma-next/sql-contract-ts
@prisma-next/sql-contract-emitter
@prisma-next/sql-lane-query-builder
@prisma-next/sql-relational-core
@prisma-next/sql-builder
@prisma-next/target-postgres
@prisma-next/target-sqlite
@prisma-next/adapter-postgres
@prisma-next/adapter-sqlite
@prisma-next/driver-postgres
@prisma-next/driver-sqlite
commit: |
Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
…eCurrent seam Key the per-project document cache on TextDocument.version: update() skips recompute at an unchanged version, ensureCurrent(project, uri) is the single materialize-on-read seam for completion, semantic tokens, and folding, and the per-request whole-text compare (currentDocumentArtifact) is gone. Config reloads invalidate cached versions so the next read recomputes against the new stack. Push diagnostics on didChange are unchanged. Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
Advertise diagnosticProvider ({ interFileDependencies: false,
workspaceDiagnostics: false } — single-input implementation scope, to flip
with the future multi-input symbol table) when the client supports
textDocument/diagnostic, and serve full reports through the ensureCurrent
seam via a project-scoped report builder that can carry relatedDocuments
later. For pull clients didOpen/didChange become invalidate-only and config
changes request workspace/diagnostic/refresh (gated on refreshSupport)
instead of republishing; push clients keep the previous eager publish
behavior. Exactly one transport is ever active per client.
Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
Section 2 still claimed a reparse on every change, contradicting the pull-diagnostics description: parsing happens at most once per document version, when a read triggers materialization. Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
…n report, and trace Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
The mutation points are fully enumerable (didOpen, didChange, config reload), so staleness detection moves to write time: each mutation evicts the document cache entry (didOpen/didChange via remove, config reload via clear) and reads parse on miss. Presence in the cache is currency — LSP messages are dispatched in order and the notification handlers evict synchronously against the already-updated text mirror. CachedDocument loses its version field, update becomes materialize with an entry-present fast path, and the -1 sentinel disappears. Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
Operator decision: the mutation points (didOpen, didChange, config reload) are enumerable, so staleness moves to write-time eviction; version keying was redundant. Trace updated for the D3 dispatch. Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
The store is constructed per project load with the config inputs, control stack, and a text provider over the TextDocuments mirror; reads (document(uri), symbolTable()) parse internally when needed and the only externally visible mutations are the domain events documentChanged and documentClosed plus store replacement on config reload. server.ts speaks no caching vocabulary: ensureCurrent is gone, reads go straight to the store, and open/change/close handlers raise events. CachedDocument is renamed DocumentArtifacts as it crosses the package-internal boundary. Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
…tore Operator decision: caching is fully encapsulated in ProjectArtifacts — constructor-injected inputs/control stack/text provider, domain events (document changed/closed, store replacement on config reload), lazy internal parsing on read, and no caching vocabulary in server.ts. Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
Drop comments that restate signatures or narrate visible handler flow, compress the single-input and report-builder rationales, and correct the store contract note: in-order message dispatch is a vscode-languageserver runtime property, not an LSP protocol guarantee. The capability-flags scope comment stays verbatim as the project spec mandates it. Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
…ingle resolution boundary Document when ProjectArtifacts.document/symbolTable return undefined; fold the four clientSupports* booleans into one ResolvedClientCapabilities object resolved at initialize; drop the pull-handler capability guard (non-pull clients are never advertised diagnosticProvider, so only a protocol-violating client could reach it); and make resolveProjectForDocument swallow config-discovery failures at its own boundary so every call site reads as a plain undefined check instead of repeating try/catch. Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
symbolTable() no longer peeks: when unset it reads the first configured input open in the text mirror through the same internal path document() uses, so its undefined shrinks to exactly one case — no configured input open. SchemaInputSet gains uris() to enumerate configured inputs; picking the first open one preserves the single-input reality (multi-input merging stays deferred). Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
…tion Every parse already produces a table, so each document read carries its own: ProjectArtifacts loses symbolTable() (and the walk-the-inputs materialization plus the shared table slot it needed), and consumers take the table from the artifacts they already read. SemanticTokenSource and PslCompletionCandidateSource become non-optional, dropping their internal undefined guards. SchemaInputSet.uris(), added only for the deleted walk, goes with it. getProjectSymbolTable keeps | undefined at the accessor boundary because the document itself may be closed or a non-input. Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
resolveProjectForDocument now validates membership after the nearest config resolves: a document that is not one of the declared inputs keeps no documentConfigPaths association and resolves to no project, so reads and events never reach the artifact store for it and push clients receive no publishes for stray files. A previously managed document that a config reload drops from the inputs gets one clearing publish on the republish path before becoming unmanaged. The explicit inputs check in formatDocument goes as redundant. Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
…symbol table A live project now always has at least one open input: didClose drops the project once no association remains, loads that settle after the last association vanished do not register (and clear any stale entry), and the watched-files handler refreshes only live or loading projects while new or fixed configs are still discovered lazily per document. On that invariant the symbol table returns to ProjectArtifacts as symbolTable(): SymbolTable with materialize-on-demand over the configured inputs (SchemaInputSet regains uris()); a degenerate empty table doubles as the unset sentinel and the type-totality safety net for the unreachable no-open-input case. DocumentArtifacts loses its per-document table from the overruled design. Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
The membership-failure path in resolveProjectForDocument now performs the same no-managed-documents drop the close handler does (shared helper), so a project a stray document alone caused to load does not outlive the resolution. A sibling open input keeps the project alive, and the register-or-delete load-settle logic composes unchanged. Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
…e in slice spec Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
… resolution join from node:path yields backslash separators on Windows, which LSP glob syntax never matches — the config watcher was dead there; pathe is the repo convention and normalizes to forward slashes. Root resolution now prefers workspaceFolders (first folder; multi-root out of scope) over the deprecated rootUri/rootPath, so clients that send only workspaceFolders watch the right tree instead of process.cwd(). Test imports switched to pathe alongside per the path rule. Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
… exposed The degenerate empty symbol table is gone: serving fabricated emptiness would mask an invariant violation as missing completions, so the walk now throws naming the broken invariant (unreachable while the server drops projects whose last input closes). The throw immediately caught a real gap: after the contributing document closed, a cache-hit read of a sibling input never refilled the table slot — the walk now rebuilds from the cached artifacts via a reusable pipeline symbol-table stage, without reparsing. Framework-vocabulary comment lines rephrased to the symbolTable identifier and the ratchet lowered to lock in 905. Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
7e78ca4 to
a2765ce
Compare
projects and projectLoads become a single map whose entry is either loading (carrying the in-flight promise and whether a loaded project preceded it) or loaded. Illegal states are unrepresentable: an entry can no longer exist in both shapes, a settling load transitions only if it is still the current entry and an association remains, and a failed load is reaped by stopManagingProject in every awaiter with push clears decided by the pre-load state. Reads during a config reload now await the fresh resolution instead of the pre-reload project (operator-acknowledged behavior shift, test-pinned); refresh chaining behind an in-flight load is preserved. Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
runSymbolTableStage added nothing over buildSymbolTable; both callers now call it directly and pipeline.ts returns to its pre-refactor shape. The symbolTable() walk uses early returns instead of nested undefined checks. The framework-vocabulary ratchet stays at 905: the inlined destructure adds one counted line, balanced by rewording the accessor comment to name the symbolTable method it describes. Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
The chaining clause narrated code the previousLoad chain already shows, and the settle comment restated the isCurrentLoad guard by name; both now carry only their non-obvious rationale. Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
The slice is merge-ready; per the team close-out discipline the transient project artifacts (spec, plan, slice spec, QA script and report, trace) leave the repo with the project. The durable design rationale lives in the language-server package (README and code) and in the PR narrative. Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
Reworks the language server's document-state lifecycle from eager, defensive reparse to invalidate-on-change + lazily materialize-on-read, synchronously, and moves diagnostics from push to LSP 3.17 pull (
textDocument/diagnostic) with a capability-gated push fallback.Changes
src/project-artifacts.ts): caching lives entirely insideProjectArtifacts, constructed per project load with the config's inputs, control stack, and a text provider over theTextDocumentsmirror. Reads parse internally exactly when needed; the server raises only domain events —documentChanged(uri)on open/edit,documentClosed(uri)on close — and a config reload replaces the store outright. No caching vocabulary appears inserver.ts; nothing outside the store can trigger a reparse except a text edit or a config change. An untouched read is a map hit with zero parsing; a read after an edit parses exactly once.currentDocumentArtifact— the per-request whole-document text compare + defensive reparse — is deleted; completion, semantic tokens, folding, and the pull handler all read through the store.src/server.ts): the server manages only documents declared in the config'sinputs(strays are never associated; a project loaded solely for a stray is dropped at resolution), and a project is dropped when its last open input closes. Invariant: a live project always has ≥1 open input — which makes the whole-projectsymbolTable(): SymbolTabletotal (noundefined, no fabricated empty-table fallback; a violated invariant throws loudly). Project state is one map ofManagedProject, a discriminated union ofloading | loaded— the illegal states two parallel maps permitted are unrepresentable, and the union surfaced a previously implicit cross-map flag (hadLoadedProject, which decides whether a failed reload owes push clients a diagnostics clear).textDocument.diagnostic, the server advertisesdiagnosticProviderand serves full reports through the store;didOpen/didChangebecome invalidate-only, and config/watched-file changes sendworkspace/diagnostic/refresh(gated onrefreshSupport) instead of republishing. Clients without pull support keep the previous eager push behavior unchanged. Exactly one transport is ever active per client. Flags ship as{ interFileDependencies: false, workspaceDiagnostics: false }with a scope comment: current single-input implementation, not a property of PSL; flips with the future multi-input symbol table.pathe(LSP globs are/-separated;node:pathbroke it on Windows), and workspace-root resolution honorsworkspaceFoldersahead of the deprecatedrootUri/rootPath.Why
publish+currentDocumentArtifact's per-request reparse with an O(n) whole-text compare) to dodge a stale-buffer race the LSPdidChangesync contract already rules out. Because the vscode-languageserver runtime dispatches messages in order and the store handles mutation events synchronously against the already-updated mirror, stored artifacts can never outlive a mutation that affects them — no versions, no snapshots, no defensive compares.Validation
pnpm --filter @prisma-next/language-server test(198 tests: no-reparse-while-clean, one-parse edit-then-complete, reparse-after-config-reload, lifecycle drop/retain/no-resurrection, sibling-input symbol-table rebuild, pull/push capability gating, refresh semantics, workspaceFolders root resolution), typecheck, lint; playground typecheck/lint.pnpm build,pnpm typecheck,pnpm lint:deps,pnpm lint:framework-vocabulary(ratchet tightened 906→905),pnpm lint:casts(delta 0).publishDiagnosticsto the pull client, post-edit pull correct, tokens/folding healthy). Visual Monaco-marker check pending a browser run.Non-goals
Removing
TextDocuments; the multi-input project-wide symbol table (andinterFileDependencies: true/workspace/diagnostic, gated on it); multi-project membership; behavior changes to completion/semantic-tokens/folding beyond their data source; watcher architecture / config-freshness mechanisms beyond the registration fixes above.Summary by CodeRabbit
New Features
Bug Fixes
Tests