Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions packages/1-framework/3-tooling/language-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Supported capabilities are intentionally narrow: parse diagnostics, whole-docume
## Responsibilities

- Resolve workspace/project configuration for open PSL documents and keep managed projects aligned with config-file changes.
- Publish parse diagnostics and serve whole-document formatting, folding ranges, full/range semantic tokens, and model field type completion for configured PSL inputs.
- Serve parse diagnostics (LSP 3.17 pull with push fallback) plus whole-document formatting, folding ranges, full/range semantic tokens, and model field type completion for configured PSL inputs.
- Preserve parser artifacts per project so editor features share the same AST, source-file, and symbol-table lifecycle instead of reparsing through feature-specific paths.
- Fail safely for unsupported documents, missing or closed buffers, config-load failures, malformed inputs, and oversized semantic-token requests.

Expand All @@ -25,14 +25,14 @@ Supported capabilities are intentionally narrow: parse diagnostics, whole-docume

## How it works

1. **`initialize`** — resolves the workspace root from the client's `rootUri` and registers config-file watching when the client supports it. The server advertises incremental text sync, whole-document formatting, folding ranges, and `semanticTokensProvider` with a stable standard-only legend, `full: true`, and `range: true`. Configs are loaded when matching documents open or when watched config files change. If a config cannot be loaded, the server does not manage that project.
2. **Document sync** — text-document sync is **incremental** (`TextDocumentSyncKind.Incremental`); the `TextDocuments` manager applies incremental edits, and the server re-parses the full current buffer on each change.
3. **Diagnostics** — on `didOpen` / `didChange` of a document whose URI is a configured PSL input, the server runs `@prisma-next/psl-parser`'s `parse()` (the CST path) and `buildSymbolTable()`, then publishes the merged, mapped diagnostics via `textDocument/publishDiagnostics`. A clean document publishes an empty array (clearing markers). Documents that are not configured inputs publish nothing.
1. **`initialize`** — resolves the workspace root from the client's `rootUri` and registers config-file watching when the client supports it. The server advertises incremental text sync, whole-document formatting, folding ranges, and `semanticTokensProvider` with a stable standard-only legend, `full: true`, and `range: true`. When the client advertises `textDocument.diagnostic`, the server also advertises `diagnosticProvider` with `{ interFileDependencies: false, workspaceDiagnostics: false }` — flags that describe the current single-input implementation scope, not PSL itself. Configs are loaded when matching documents open, on the first read that needs them, or when watched config files change. If a config cannot be loaded, the server does not manage that project.
2. **Document sync** — text-document sync is **incremental** (`TextDocumentSyncKind.Incremental`); the `TextDocuments` manager applies incremental edits and bumps the document version, which alone marks derived state stale. The server parses the full current buffer at most once per version, when a read (diagnostic pull, completion, semantic tokens, folding, or a push publish) triggers materialization.
3. **Diagnostics** — served over exactly one transport per client, decided at `initialize`. For clients that advertise `textDocument.diagnostic` (pull), `didOpen` / `didChange` only invalidate — the version bump in `TextDocuments` marks derived state stale and no eager work runs; a `textDocument/diagnostic` request materializes the current buffer through the shared `ensureCurrent` seam (running `@prisma-next/psl-parser`'s `parse()` and `buildSymbolTable()` once per document version) and returns a full report with the merged, mapped diagnostics. Documents that are not configured inputs, or whose project cannot load, return an empty full report. On a config-file change the server reloads the project and asks the client to re-pull via `workspace/diagnostic/refresh` when the client advertises `workspace.diagnostics.refreshSupport`, instead of republishing. For clients without pull support, the previous push behavior is preserved: the server computes on `didOpen` / `didChange` and publishes via `textDocument/publishDiagnostics` (a clean document publishes an empty array, clearing markers; unconfigured documents publish nothing), and config changes republish affected open documents. The report builder is project-scoped so a future multi-input symbol table can attach `relatedDocuments`; today reports carry only the requested document's items.
4. **Formatting** — on `textDocument/formatting`, the server formats the current in-memory document text with `@prisma-next/psl-parser/format` when the document is a configured PSL input. It returns one whole-document edit when the formatted text differs, and returns no edits for missing or closed documents, unconfigured documents, already canonical text, malformed PSL, or invalid formatter options.
5. **Folding ranges** — on `textDocument/foldingRange`, the server reads the preserved document AST for the configured input and returns foldable declaration/block ranges. Missing, unconfigured, or not-yet-parsed documents return an empty result.
6. **Semantic tokens** — on `textDocument/semanticTokens/full` and `textDocument/semanticTokens/range`, the server reads the current preserved `DocumentAst`, `SourceFile`, project `SymbolTable`, and control-stack scalar types from the same `ProjectArtifacts` lifecycle used by diagnostics. It classifies PSL keywords, declaration names, field/property names, type references, attributes, strings, numbers, booleans, and comments into standard token types/modifiers, then encodes them as LSP five-integer relative semantic-token data. The range request filters to intersecting tokens before encoding. Unconfigured, missing, closed, config-resolution-failed, oversized, or stale documents return `{ data: [] }` instead of throwing or reparsing through a semantic-token-specific path. Malformed PSL returns best-effort tokens from parser recovery when artifacts are available.
5. **Folding ranges** — on `textDocument/foldingRange`, the server materializes the current document artifacts for the configured input and returns foldable declaration/block ranges. Missing or unconfigured documents return an empty result.
6. **Semantic tokens** — on `textDocument/semanticTokens/full` and `textDocument/semanticTokens/range`, the server reads the current preserved `DocumentAst`, `SourceFile`, project `SymbolTable`, and control-stack scalar types from the same `ProjectArtifacts` lifecycle used by diagnostics. It classifies PSL keywords, declaration names, field/property names, type references, attributes, strings, numbers, booleans, and comments into standard token types/modifiers, then encodes them as LSP five-integer relative semantic-token data. The range request filters to intersecting tokens before encoding. Unconfigured, missing, closed, config-resolution-failed, or oversized documents return `{ data: [] }` instead of throwing; a stale cache is materialized to the current buffer through the same shared seam rather than served or emptied. Malformed PSL returns best-effort tokens from parser recovery when artifacts are available.
7. **Completion** — on `textDocument/completion`, the server serves configured PSL model field type positions, descriptor-backed generic block parameter-key positions, and declaration keyword positions from cached parse artifacts. It classifies the cursor using the cached AST/source file, reads the current project symbol table plus project control-stack block descriptors, threads the client's snippet capability into declaration keyword item construction, and returns `[]` for missing or closed documents, unconfigured documents, unavailable artifacts, unsupported contexts, ordinary attributes or attribute arguments, generic block parameter values, generic block value positions, relation-aware scenarios, and external contract-space discovery gaps.
8. **Preserved artifacts** — each project keeps the parse artifacts it produces: the AST per open document (keyed by URI) and one symbol table per project, rebuilt from the open configured input on each edit and dropped when the document closes. Diagnostics populate these artifacts, and folding/semantic-token/completion handlers read them instead of constructing independent parse/token caches. They are exposed through `getDocumentAst` / `getProjectSymbolTable` for future features. Filling the project table from several inputs — and reading unopened inputs from disk — is deferred cross-file work.
8. **Preserved artifacts** — each project keeps the parse artifacts it produces: the AST per open document (keyed by URI) and one symbol table per project, rebuilt lazily from the open configured input when a read observes a new document version and dropped when the document closes. Reads materialize these artifacts on demand through a single version-keyed seam, and diagnostics, folding, semantic-token, and completion handlers all consume it instead of constructing independent parse/token caches. They are exposed through `getDocumentAst` / `getProjectSymbolTable` for future features. Filling the project table from several inputs — and reading unopened inputs from disk — is deferred cross-file work.

## Module layout

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import type { SchemaInputSet } from './schema-inputs';
export interface CachedDocument {
readonly document: DocumentAst;
readonly sourceFile: SourceFile;
readonly text: string;
/** The `TextDocument.version` the artifacts were parsed from. */
readonly version: number;
readonly diagnostics: readonly LspDiagnostic[];
}

export interface ProjectArtifacts {
Expand All @@ -17,20 +19,34 @@ export interface ProjectArtifacts {
update(
uri: string,
text: string,
version: number,
inputs: SchemaInputSet,
controlStack: PipelineInputs,
): readonly LspDiagnostic[] | null;
/**
* Marks every cached entry stale so the next `update` recomputes even at an
* unchanged document version — required after a config reload, where the
* inputs or control stack may have changed underneath the cache.
*/
invalidate(): void;
remove(uri: string): void;
}

// LSP document versions only increase, so this never matches a live version.
const invalidatedVersion = -1;

export function createProjectArtifacts(): ProjectArtifacts {
const documents = new Map<string, CachedDocument>();
let symbolTable: SymbolTable | undefined;

return {
getDocument: (uri) => documents.get(uri),
getSymbolTable: () => symbolTable,
update: (uri, text, inputs, controlStack) => {
update: (uri, text, version, inputs, controlStack) => {
const cached = documents.get(uri);
if (cached !== undefined && cached.version === version) {
return cached.diagnostics;
}
const computed = computeDocumentDiagnostics(uri, text, inputs, controlStack);
if (computed === null) {
if (documents.delete(uri)) {
Expand All @@ -41,7 +57,8 @@ export function createProjectArtifacts(): ProjectArtifacts {
documents.set(uri, {
document: computed.document,
sourceFile: computed.sourceFile,
text,
version,
diagnostics: computed.diagnostics,
});
// One symbol table per project. Single-input reality: it is (re)built from
// the one open configured input on every edit. Merging several open inputs
Expand All @@ -51,6 +68,11 @@ export function createProjectArtifacts(): ProjectArtifacts {
symbolTable = computed.symbolTable;
return computed.diagnostics;
},
invalidate: () => {
for (const [uri, cached] of documents) {
documents.set(uri, { ...cached, version: invalidatedVersion });
}
},
remove: (uri) => {
if (documents.delete(uri)) {
symbolTable = undefined;
Expand Down
Loading
Loading