Skip to content
Merged
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
1 change: 1 addition & 0 deletions apps/lsp-playground/fixtures/broken.psl
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// use prisma-next
model User {
id Int @id
email String
Expand Down
52 changes: 2 additions & 50 deletions packages/1-framework/3-tooling/language-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,56 +4,8 @@

The Prisma Next language server speaks the Language Server Protocol over stdio for PSL schema inputs declared in a project's `prisma.config.ts`. It is launched by the `prisma lsp` subcommand, so editor features come from the project's own Prisma Next version and stay version-matched by construction.

## Scope

Supported capabilities are intentionally narrow: parse diagnostics, whole-document formatting, folding ranges, full/range semantic tokens, model field type completion, descriptor-backed generic block parameter completion, and declaration keyword completion for configured PSL inputs. Formatting is only available for documents listed in `contract.source.inputs`, uses `@internal/psl-parser/format`, and applies formatter options from the project's Prisma config `formatter` block. Semantic tokens use the standard LSP token taxonomy advertised by the server; they do not introduce Prisma-specific token names or a second parser. Completion is only available for open configured PSL inputs. At model field type positions, it suggests configured scalar types plus visible model, composite type, scalar, type-alias, and namespace qualifier candidates from the current project symbol table; bare positions offer namespace segments such as `auth.`, and after a namespace qualifier the provider suggests visible model and composite type members inside that namespace. The classifier also accepts contract-space-qualified type-position syntax such as `supabase:auth.User` when the namespace data is visible in the current cached artifacts. Inside descriptor-backed generic block bodies, it suggests declared parameter keys and excludes keys already present in sibling entries. At document top-level declaration positions, it suggests native PSL block keywords `model`, `type`, `types`, and `namespace`, plus descriptor-backed generic block keywords from `pslBlockDescriptors`. Inside namespace bodies, declaration keyword completion suggests only namespace-valid native keywords `model` and `type`, plus descriptor-backed generic block keywords; it does not suggest nested `namespace` or `types`. Declaration keyword items are snippets only when the client advertises `textDocument.completion.completionItem.snippetSupport === true`; otherwise the server returns plain-text edits for the same labels. Ordinary PSL `@` / `@@` attribute completions, attribute argument completions, generic block parameter value completions, generic block value completions, relation-aware completions, and new external contract-space candidate discovery are not part of this slice. Hover, navigation, range formatting, on-type formatting, semantic-token delta requests, and editor-extension work are out of scope. A server process can manage multiple projects under the workspace root, keyed by the config file each open document belongs to.
The server only handles documents whose first non-whitespace content is a `// use prisma-next` line comment; all other documents belong to the legacy (Prisma ≤7) language server and are ignored per request from current document content. The check must stay byte-for-byte in sync with the legacy server's copy in `prisma/language-tools`.

## Responsibilities

- Resolve workspace/project configuration for open PSL documents and keep managed projects aligned with config-file changes.
- 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.

## Dependencies

- `@internal/config-loader` — discovers nearest config files and loads project configuration.
- `@internal/psl-parser` — parses PSL, builds symbol tables, exposes syntax artifacts, and formats PSL text.
- `@internal/framework-components` — supplies control-stack input types used when resolving project configuration.
- `@internal/errors` and `@internal/utils` — shared framework utilities used by parsing/config plumbing.
- `vscode-languageserver` and `vscode-languageserver-textdocument` — LSP connection, request/notification types, semantic-token/folding/formatting types, and incremental document management.

## 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`. 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 each open or change is forwarded to the project's artifact store as a document-changed event. The server parses the full current buffer at most once per change, when a read (diagnostic pull, completion, semantic tokens, folding, or a push publish) next needs the document's artifacts.
3. **Diagnostics** — served over exactly one transport per client, decided at `initialize`. For clients that advertise `textDocument.diagnostic` (pull), `didOpen` / `didChange` only invalidate — the artifact store is only told the document changed and no eager work runs; a `textDocument/diagnostic` request reads the document artifacts from the store, which parses the current buffer internally when needed (running `@internal/psl-parser`'s `parse()` and `buildSymbolTable()` once per change) 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 `@internal/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 current document artifacts from the project store 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; the store parses the current buffer internally before returning artifacts, so tokens never derive from an out-of-date parse. 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. **Project artifact store** — each project load constructs one artifact store with the config's inputs, control stack, and a text provider over the open-document mirror. The store returns the AST, `SourceFile`, and diagnostics per open configured input plus one project symbol table, parsing internally when a read needs them; document-changed/closed events (edits, closes) and store replacement on config reload are the only things that change what reads return. The artifacts 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

- `diagnostic-mapping.ts` — pure `ParseDiagnostic[] → LspDiagnostic[]` mapping. Free of any `vscode-languageserver` import; it returns plain shape objects (ranges pass through unchanged) so it stays reusable. The connection layer adapts the numeric severity to the LSP enum.
- `schema-inputs.ts` — resolves the schema-input set (`SchemaInputSet`) from a config and answers URI membership.
- `config-resolution.ts` — wraps `loadConfig` and resolves schema inputs, formatter options, and control-stack inputs for a config. A standalone async function so it can be re-run on a config change without rewiring the server.
- `document-diagnostics.ts` — `computeDocumentDiagnostics(uri, text, inputs, controlStack)`, the pure seam that parses, builds the symbol table, and returns the diagnostics plus the parse artifacts.
- `project-artifacts.ts` — `createProjectArtifacts({ inputs, controlStack, getText })`, the per-project-load store that owns document artifacts and the project symbol table, parsing on demand and responding to document-changed/closed events.
- `folding-ranges.ts` — pure AST-to-LSP folding-range computation for declaration/block bodies.
- `semantic-tokens.ts` — pure PSL semantic-token collection, range filtering, multiline normalization, duplicate resolution, modifier bitset encoding, and LSP semantic-token data encoding.
- `completion-context.ts` — pure cursor classifier for PSL completion contexts, currently routing model field type positions, descriptor-backed generic block parameter-key positions, and declaration keyword positions while marking everything outside slice scope unsupported.
- `completion-provider.ts` — pure completion item provider for supported model field type, generic block parameter, and declaration keyword contexts.
- `server.ts` — `createServer(connection)` wires diagnostics, whole-document formatting, folding ranges, semantic-token handlers, completion, config watching, and project-artifact access onto an injected connection.
- `start-server.ts` — `startServer()` and `startServer(streams)`, the two ways in, kept apart by overloads so neither can be awaited wrongly. With no argument it builds its own stdio connection from the process arguments and returns nothing: `vscode-languageserver/node` ends the process itself on disconnect, so there is no exit code to hand back. With the host's streams it resolves with one.
- `stream-server.ts` — runs the server over an injected stream pair and resolves with the exit code the client's departure implies: 0 after a `shutdown`, 1 otherwise (and 1 for a frame the reader cannot make sense of, which leaves it desynchronised for good). It waits for the work in flight to finish first — end of input is not the end of the conversation — and redirects the global console to the host's stderr for the length of the run, so nothing but frames reaches the client. Nothing here ends the process; the host owns it.
- `stdio-transport.ts` — the byte adapter between a host's streams (`AsyncIterable<Uint8Array>` in, `write(text: string)` out) and the Node streams `vscode-languageserver`'s reader and writer want. Output is decoded through a `StringDecoder`, so a body split mid-character across two writes still declares the `Content-Length` it has.
- `guarded-connection.ts` — wraps the connection so a send the departed client cannot receive does nothing. Without it a send after the input ends throws, and a throw escaping a notification handler ends the process.

## Package Location

- **Domain**: framework (target-agnostic)
- **Layer**: tooling
- **Plane**: migration
- **Path**: `packages/1-framework/3-tooling/language-server`
- Serve diagnostics, whole-document formatting, folding ranges, semantic tokens, and completion for open configured PSL inputs carrying the directive.
Comment thread
SevInf marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { SymbolTable } from '@internal/psl-parser';
import type { DocumentAst, SourceFile } from '@internal/psl-parser/syntax';
import type { LspDiagnostic } from './diagnostic-mapping';
import { type PipelineInputs, runPipeline } from './pipeline';
import { isPrismaNextSchema } from './schema-directive';
import type { SchemaInputSet } from './schema-inputs';

export interface DocumentDiagnostics {
Expand All @@ -23,7 +24,7 @@ export function computeDocumentDiagnostics(
inputs: SchemaInputSet,
controlStack: PipelineInputs,
): DocumentDiagnostics | null {
if (!inputs.includes(uri)) {
if (!inputs.includes(uri) || !isPrismaNextSchema(text)) {
return null;
}
const { document, sourceFile, symbolTable, diagnostics } = runPipeline(text, controlStack);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ export function createProjectArtifacts(options: ProjectArtifactsOptions): Projec
// once its last open input closes. Throwing loudly beats serving a
// fabricated empty symbolTable that would mask the broken invariant.
throw new InternalError(
'invariant violated: project has no open configured input — the server must drop such projects',
'invariant violated: project has no readable configured input — callers must check document artifacts first',
);
},
documentChanged: drop,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const PRISMA_NEXT_DIRECTIVE = /^\s*\/\/ *use +prisma-next *(?!\S)/;
Comment thread
coderabbitai[bot] marked this conversation as resolved.

export function isPrismaNextSchema(text: string): boolean {
return PRISMA_NEXT_DIRECTIVE.test(text);
}
15 changes: 13 additions & 2 deletions packages/1-framework/3-tooling/language-server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import {
type DocumentArtifacts,
type ProjectArtifacts,
} from './project-artifacts';
import { isPrismaNextSchema } from './schema-directive';
import type { SchemaInputSet } from './schema-inputs';
import { buildSemanticTokens, semanticTokensLegend } from './semantic-tokens';

Expand Down Expand Up @@ -379,12 +380,16 @@ function createServerOn(connection: Connection): LanguageServer {
return [];
}

const source = document.getText();
if (!isPrismaNextSchema(source)) {
return [];
}

const project = await resolveProjectForDocument(uri);
if (project === undefined) {
return [];
}

const source = document.getText();
let formatted: string;
try {
formatted = format(source, project.formatter);
Expand Down Expand Up @@ -652,7 +657,13 @@ function createServerOn(connection: Connection): LanguageServer {
getDocumentAst: (uri) => artifactsForDocument(uri)?.document(uri),
// `| undefined` only because the uri may be unmanaged (closed, non-input,
// or projectless); a managed document's project always yields a symbolTable.
getProjectSymbolTable: (uri) => artifactsForDocument(uri)?.symbolTable(),
getProjectSymbolTable: (uri) => {
const artifacts = artifactsForDocument(uri);
if (artifacts?.document(uri) === undefined) {
return undefined;
}
return artifacts.symbolTable();
},
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ const controlStack: PipelineInputs = {
pslBlockDescriptors: {},
};

const directive = '// use prisma-next';

const duplicateModelSource = [
directive,
'model User {',
' id Int @id',
'}',
Expand All @@ -29,7 +32,7 @@ const duplicateModelSource = [

describe('computeDocumentDiagnostics', () => {
it('publishes parser diagnostics for a configured PSL input with a parse error', () => {
const source = 'model {';
const source = '// use prisma-next\nmodel {';
const result = computeDocumentDiagnostics(schemaUri, source, inputs, controlStack);
expect(result).not.toBeNull();
expect(result?.diagnostics).toEqual(mapParseDiagnostics(parse(source).diagnostics));
Expand All @@ -39,7 +42,7 @@ describe('computeDocumentDiagnostics', () => {
it('publishes an empty array for a clean configured PSL input', () => {
const result = computeDocumentDiagnostics(
schemaUri,
'model User {\n id Int @id\n}\n',
'// use prisma-next\nmodel User {\n id Int @id\n}\n',
inputs,
controlStack,
);
Expand All @@ -51,6 +54,12 @@ describe('computeDocumentDiagnostics', () => {
const result = computeDocumentDiagnostics(otherUri, 'model {', inputs, controlStack);
expect(result).toBeNull();
});

it('returns null for a configured input without the prisma-next directive', () => {
const result = computeDocumentDiagnostics(schemaUri, 'model {', inputs, controlStack);
expect(result).toBeNull();
});

it('runs the symbol-table tier and reports a duplicate top-level declaration', () => {
const result = computeDocumentDiagnostics(
schemaUri,
Expand All @@ -64,7 +73,7 @@ describe('computeDocumentDiagnostics', () => {
});

it('matches the merged parse + symbol-table diagnostics for the same inputs', () => {
const source = ['model Profile {', ' user a.b.c', '}'].join('\n');
const source = [directive, 'model Profile {', ' user a.b.c', '}'].join('\n');
const { document, sourceFile, diagnostics: parseDiagnostics } = parse(source);
const { diagnostics: symbolTableDiagnostics } = buildSymbolTable({
document,
Expand All @@ -82,7 +91,7 @@ describe('computeDocumentDiagnostics', () => {
it('exposes the parsed AST and the symbol table as artifacts', () => {
const result = computeDocumentDiagnostics(
schemaUri,
'model User {\n id Int @id\n}\n',
'// use prisma-next\nmodel User {\n id Int @id\n}\n',
inputs,
controlStack,
);
Expand All @@ -100,7 +109,12 @@ describe('computeDocumentDiagnostics', () => {

it('does not throw on a malformed, half-typed buffer', () => {
expect(() =>
computeDocumentDiagnostics(schemaUri, 'model User {\n id ', inputs, controlStack),
computeDocumentDiagnostics(
schemaUri,
'// use prisma-next\nmodel User {\n id ',
inputs,
controlStack,
),
).not.toThrow();
});
});
Loading
Loading