Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
1857e2e
TML-2946: add PSL model type completions
SevInf Jun 25, 2026
a8cbee3
TML-2946: add generic block completions
SevInf Jun 25, 2026
86926a9
TML-2946: complete namespaces before members
SevInf Jun 25, 2026
f9071dd
feat(language-server): complete declaration keywords
SevInf Jun 26, 2026
81b472c
chore(drive): record top-level keyword completion slice
SevInf Jun 26, 2026
4ffa748
fix(lsp-playground): serve runtime config over HTTP
SevInf Jun 26, 2026
0071da5
feat(psl-parser): add rust-analyzer-style red-tree cursor navigation
SevInf Jun 26, 2026
2c9369f
refactor(language-server): rewrite completion classifier on red-tree …
SevInf Jun 26, 2026
17583bd
fix(language-server): refresh stale completion artifacts and harden p…
SevInf Jun 26, 2026
3f1344f
chore(drive): record navigation + classifier-simplification slices an…
SevInf Jun 26, 2026
48dcdd3
docs(language-server,psl-parser): drop rust-analyzer references from …
SevInf Jun 26, 2026
65410f3
refactor(language-server): derive qualified type-name prefix from the…
SevInf Jun 26, 2026
4f5a36b
style(language-server): sort merged server imports after rebase
SevInf Jun 29, 2026
94920d9
refactor(language-server): drop trailing dot from namespace completion
SevInf Jun 29, 2026
d575ed7
chore(drive): drop transient lsp-autocomplete project artifacts
SevInf Jun 29, 2026
c58a12f
refactor(psl-parser): model TokenAtOffset with a private discriminate…
SevInf Jun 29, 2026
97099f8
refactor(language-server): dissolve TokenContext into on-demand token…
SevInf Jun 29, 2026
23cda1b
refactor(psl-parser): expose endOffset on SyntaxNode
SevInf Jun 29, 2026
49b2280
refactor(language-server): inline currentToken/touchingToken into cur…
SevInf Jun 29, 2026
936b5d4
feat(psl-parser): add isInside/isOutside containment helpers
SevInf Jun 29, 2026
085c52c
refactor(language-server): use isInside/isOutside in field-type class…
SevInf Jun 29, 2026
1c1c87e
fix(psl-parser): make QualifiedName roles separator-positional
SevInf Jun 29, 2026
d1c92e0
refactor(language-server): derive type-name prefix from QualifiedName…
SevInf Jun 29, 2026
2ffc4df
refactor(language-server): let the client filter completions by prefix
SevInf Jun 29, 2026
db372d9
refactor(language-server): drop dead completion prefix payload
SevInf Jun 29, 2026
67c53f0
refactor(language-server): split type and generic-block completion by…
SevInf Jun 29, 2026
b8923f2
feat(psl-parser): add endOffset to SyntaxToken
SevInf Jun 29, 2026
f98cd0f
refactor(language-server): address completion-context review batch
SevInf Jun 29, 2026
332fee5
refactor(language-server): collapse anchorNode/contextNode into one
SevInf Jun 29, 2026
73c9985
feat(psl-parser): add BracedBlock interface for braced declarations
SevInf Jun 29, 2026
0d135d8
refactor(language-server): address completion-context review batch
SevInf Jun 29, 2026
efac40e
refactor(language-server): compose multi-kind closestAst via an any()…
SevInf Jun 29, 2026
147369e
refactor(language-server): fold chained closestAst walks through any()
SevInf Jun 29, 2026
c8a4938
refactor(language-server): infer any() union via overload pair
SevInf Jun 29, 2026
dc43f25
refactor(psl-parser): move closestAst + any into the parser package
SevInf Jun 29, 2026
fa89074
fix(language-server): only suppress completion inside comments, not b…
SevInf Jun 29, 2026
49dfe4d
refactor(psl-parser): rename findClosestParent to findAncestor
SevInf Jun 29, 2026
45ef2a6
refactor(language-server): drop dead offset from unsupported context
SevInf Jun 29, 2026
4cca99d
refactor(psl-parser): navigate siblings via stored child index
SevInf Jun 30, 2026
0cd3ef0
refactor(language-server): read decorator @ prefix from the token, no…
SevInf Jun 30, 2026
1d2866c
refactor(psl-parser): compose previousNonTriviaToken from skipTriviaT…
SevInf Jun 30, 2026
0997d59
refactor(language-server): classify declaration-keyword position stru…
SevInf Jun 30, 2026
bbc60d8
refactor(language-server): detect keyword-only declarations via acces…
SevInf Jun 30, 2026
63e92d3
refactor(psl-parser): stop emitting the empty TypeAnnotation placeholder
SevInf Jun 30, 2026
00e29a3
refactor(psl-parser): drop empty AttributeArg placeholder and zero-wi…
SevInf Jun 30, 2026
7b8c073
refactor(language-server): unify gap recovery onto a single contextTo…
SevInf Jun 30, 2026
5e84a10
refactor(psl-parser): remove orphaned previousNonTriviaToken navigati…
SevInf Jun 30, 2026
6aa1fa0
refactor(language-server): compute cursorIdentifier and contextToken …
SevInf Jun 30, 2026
3972b02
refactor(language-server): rename canBeginDeclaration to canCompleteD…
SevInf Jun 30, 2026
5886883
refactor(language-server): detect the empty type slot via the context…
SevInf Jun 30, 2026
f9691a6
refactor(language-server): rename contextToken to precedingToken
SevInf Jun 30, 2026
82b8f4e
refactor(language-server): carry the block AST on the context, gather…
SevInf Jun 30, 2026
642719d
fix(language-server): do not serve local namespace members for a fore…
SevInf Jul 3, 2026
9bab940
fix(language-server): stop sending on a disposed connection
SevInf Jul 3, 2026
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
4 changes: 2 additions & 2 deletions apps/lsp-playground/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ Monaco editor + VS Code API shim --LSP/WebSocket--> ws bridge --spawn+stdio--
```

- `src/bridge.ts` — `ws` + `vscode-ws-jsonrpc/server` (`createServerProcess` + `forward`), adapted from the TypeFox example (MIT). Each browser WebSocket connection spawns `node <built-cli> lsp --stdio` and forwards JSON-RPC between the browser and the language server process.
- `src/cli.ts` — arg parsing, config resolution, runtime module generation, and startup for the shared HTTP server that hosts Vite plus the LSP WebSocket bridge.
- `src/client/main.ts` — Monaco editor setup via `EditorApp`, VS Code API service overrides, and `LanguageClientWrapper` startup for the `prisma` language id.
- `src/cli.ts` — arg parsing, config resolution, startup for the shared HTTP server that hosts Vite plus the LSP WebSocket bridge, and serving launch-time client config as same-origin JSON at `/__psl_playground_runtime.json` without rewriting tracked source files.
- `src/client/main.ts` — Monaco editor setup via `EditorApp`, VS Code API service overrides, runtime config fetch/validation, and `LanguageClientWrapper` startup for the `prisma` language id.

## Semantic tokens

Expand Down
83 changes: 66 additions & 17 deletions apps/lsp-playground/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,27 @@ import { findNearestConfig } from './find-config';
const PACKAGE_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
const PORT = 5295;
const LSP_PATH = '/psl';
const RUNTIME_CONFIG_PATH = '/__psl_playground_runtime.json';
const REQUEST_URL_BASE = 'http://localhost/';

interface RuntimeConfig {
readonly wsPath: string;
readonly documentUri: string;
readonly rootUri: string;
readonly schemaPath: string;
readonly schemaText: string;
}

function requestPathname(requestUrl: string | undefined): string | undefined {
if (requestUrl === undefined) {
return undefined;
}
try {
return new URL(requestUrl, REQUEST_URL_BASE).pathname;
} catch {
return undefined;
}
}

async function fileExists(path: string): Promise<boolean> {
try {
Expand Down Expand Up @@ -132,22 +153,13 @@ async function main(): Promise<void> {
const documentUri = pathToFileURL(schemaPath).toString();
const rootUri = pathToFileURL(dirname(configPath)).toString();

// Hand the browser client its runtime values via a generated module (rather
// than Vite `define`, whose bare-identifier substitution is unreliable in the
// programmatic dev-server path). The WS URL is relative so the editor and the
// LSP bridge share this single origin/port.
const runtimeModule = resolve(PACKAGE_ROOT, 'src/client/runtime.ts');
await writeFile(
runtimeModule,
`// Generated by psl-playground at launch. Do not edit.
export const wsPath = ${JSON.stringify(LSP_PATH)};
export const documentUri = ${JSON.stringify(documentUri)};
export const rootUri = ${JSON.stringify(rootUri)};
export const schemaPath = ${JSON.stringify(schemaPath)};
export const schemaText = ${JSON.stringify(schemaText)};
`,
'utf8',
);
const runtimeConfig: RuntimeConfig = {
wsPath: LSP_PATH,
documentUri,
rootUri,
schemaPath,
schemaText,
};

// One HTTP server hosts both the editor (Vite, in middleware mode) and the
// LSP WebSocket bridge (on LSP_PATH). Vite's HMR WebSocket is bound to the
Expand Down Expand Up @@ -192,7 +204,44 @@ export const schemaText = ${JSON.stringify(schemaText)};
})(),
},
});
httpServer.on('request', viteServer.middlewares);
httpServer.on(
'request',
(request: nodeHttp.IncomingMessage, response: nodeHttp.ServerResponse) => {
const requestPath = requestPathname(request.url);
if (requestPath === undefined) {
response.statusCode = 400;
response.end('Bad Request');
return;
}
if (requestPath === RUNTIME_CONFIG_PATH) {
if (request.method !== 'GET' && request.method !== 'HEAD') {
response.statusCode = 405;
response.setHeader('allow', 'GET, HEAD');
response.end('Method Not Allowed');
return;
}
response.statusCode = 200;
response.setHeader('content-type', 'application/json; charset=utf-8');
response.setHeader('cache-control', 'no-store');
response.end(request.method === 'HEAD' ? undefined : JSON.stringify(runtimeConfig));
return;
}

viteServer.middlewares(request, response, (error?: unknown) => {
if (response.writableEnded) {
return;
}
if (error !== undefined) {
console.error(error);
response.statusCode = 500;
response.end('Internal Server Error');
return;
}
response.statusCode = 404;
response.end('Not Found');
});
},
);

const stopBridge = attachBridge(httpServer, { cliEntry, path: LSP_PATH });

Expand Down
59 changes: 49 additions & 10 deletions apps/lsp-playground/src/client/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ import {
} from 'monaco-languageclient/vscodeApiWrapper';
import { defineDefaultWorkerLoaders, useWorkerFactory } from 'monaco-languageclient/workerFactory';
import * as vscode from 'vscode';
import { documentUri, rootUri, schemaPath, schemaText, wsPath } from './runtime';

const LANGUAGE_ID = 'prisma';
const RUNTIME_CONFIG_PATH = '/__psl_playground_runtime.json';

const pslSemanticThemeExtension = {
name: 'prisma-psl-semantic-theme-bridge',
Expand Down Expand Up @@ -52,9 +52,41 @@ const pslSemanticThemeExtension = {

registerExtension(pslSemanticThemeExtension, undefined, { system: true });

const pathEl = document.getElementById('schema-path');
if (pathEl !== null) {
pathEl.textContent = schemaPath;
interface RuntimeConfig {
readonly wsPath: string;
readonly documentUri: string;
readonly rootUri: string;
readonly schemaPath: string;
readonly schemaText: string;
}

function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null;
}

function isRuntimeConfig(value: unknown): value is RuntimeConfig {
return (
isRecord(value) &&
typeof value['wsPath'] === 'string' &&
typeof value['documentUri'] === 'string' &&
typeof value['rootUri'] === 'string' &&
typeof value['schemaPath'] === 'string' &&
typeof value['schemaText'] === 'string'
);
}

async function loadRuntimeConfig(): Promise<RuntimeConfig> {
const response = await fetch(RUNTIME_CONFIG_PATH, { cache: 'no-store' });
if (!response.ok) {
throw new Error(
`Failed to load playground runtime config: ${response.status} ${response.statusText}`,
);
}
const value: unknown = await response.json();
if (!isRuntimeConfig(value)) {
throw new Error('Invalid playground runtime config');
}
return value;
}

function configureWorkerFactory(logger?: ILogger): void {
Expand All @@ -64,13 +96,20 @@ function configureWorkerFactory(logger?: ILogger): void {
useWorkerFactory(config);
}

function buildWebSocketUrl(): string {
function buildWebSocketUrl(wsPath: string): string {
const host = `${window.location.host}${wsPath}`;
// nosemgrep: javascript.lang.security.detect-insecure-websocket.detect-insecure-websocket
return window.location.protocol === 'https:' ? `wss://${host}` : `ws://${host}`;
}

async function main(): Promise<void> {
const runtimeConfig = await loadRuntimeConfig();

const pathEl = document.getElementById('schema-path');
if (pathEl !== null) {
pathEl.textContent = runtimeConfig.schemaPath;
}

const htmlContainer = document.getElementById('editor');
if (htmlContainer === null) {
throw new Error('#editor mount point not found');
Expand All @@ -81,9 +120,9 @@ async function main(): Promise<void> {
throw new Error('#format-document button not found');
}

const fileUri = vscode.Uri.parse(documentUri);
const fileUri = vscode.Uri.parse(runtimeConfig.documentUri);
const fileSystemProvider = new RegisteredFileSystemProvider(false);
fileSystemProvider.registerFile(new RegisteredMemoryFile(fileUri, schemaText));
fileSystemProvider.registerFile(new RegisteredMemoryFile(fileUri, runtimeConfig.schemaText));
registerFileSystemOverlay(1, fileSystemProvider);

const vscodeApiConfig: MonacoVscodeApiConfig = {
Expand Down Expand Up @@ -111,7 +150,7 @@ async function main(): Promise<void> {
},
};

const wsUrl = buildWebSocketUrl();
const wsUrl = buildWebSocketUrl(runtimeConfig.wsPath);
const languageClientConfig: LanguageClientConfig = {
languageId: LANGUAGE_ID,
connection: {
Expand All @@ -133,15 +172,15 @@ async function main(): Promise<void> {
workspaceFolder: {
index: 0,
name: 'workspace',
uri: vscode.Uri.parse(rootUri),
uri: vscode.Uri.parse(runtimeConfig.rootUri),
},
},
};

const editorAppConfig: EditorAppConfig = {
codeResources: {
modified: {
text: schemaText,
text: runtimeConfig.schemaText,
uri: fileUri.path,
},
},
Expand Down
8 changes: 0 additions & 8 deletions apps/lsp-playground/src/client/runtime.ts

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,26 @@ export {
export { IdentifierAst } from '../syntax/ast/identifier';
export { QualifiedNameAst } from '../syntax/ast/qualified-name';
export { TypeAnnotationAst } from '../syntax/ast/type-annotation';
export type { AstNode } from '../syntax/ast-helpers';
export { filterChildren, findChildToken, findFirstChild, printSyntax } from '../syntax/ast-helpers';
export type { AstNode, BracedBlock } from '../syntax/ast-helpers';
export {
any,
filterChildren,
findChildToken,
findFirstChild,
printSyntax,
} from '../syntax/ast-helpers';
export type { GreenElement, GreenNode, GreenToken } from '../syntax/green';
export { greenNode, greenToken } from '../syntax/green';
export { GreenNodeBuilder } from '../syntax/green-builder';
// Navigation helpers
export type { Direction } from '../syntax/navigation';
export {
isTrivia,
isTriviaKind,
nonTriviaSibling,
skipTriviaToken,
} from '../syntax/navigation';
// Red layer
export type { SyntaxElement, SyntaxToken } from '../syntax/red';
export { createSyntaxTree, SyntaxNode } from '../syntax/red';
export type { SyntaxElement } from '../syntax/red';
export { createSyntaxTree, SyntaxNode, SyntaxToken, TokenAtOffset } from '../syntax/red';
export type { SyntaxKind } from '../syntax/syntax-kind';
28 changes: 23 additions & 5 deletions packages/1-framework/2-authoring/psl-parser/src/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -399,14 +399,24 @@ function parseParenArgs(cursor: Cursor): void {
}
}

export function parseAttributeArg(cursor: Cursor): GreenNode {
export function parseAttributeArg(cursor: Cursor): void {
const kind = cursor.peekKind();
if (
kind !== 'Ident' &&
kind !== 'StringLiteral' &&
kind !== 'NumberLiteral' &&
kind !== 'LBracket' &&
kind !== 'LBrace'
) {
return;
}
cursor.startNode('AttributeArg');
if (cursor.peekKind() === 'Ident' && cursor.peekKind(1) === 'Colon') {
parseIdentifier(cursor);
cursor.bump();
}
parseArgValue(cursor);
return cursor.finishNode();
cursor.finishNode();
}

function parseArgValue(cursor: Cursor): void {
Expand Down Expand Up @@ -435,8 +445,16 @@ export function parseAttribute(cursor: Cursor): GreenNode {
return cursor.finishNode();
}

/** A type annotation: `QualifiedName (argList)? ([])? (?)?`, e.g. `pgvector.Vector(1536)[]?`. */
export function parseTypeAnnotation(cursor: Cursor): GreenNode {
/**
* A type annotation: `QualifiedName (argList)? ([])? (?)?`, e.g.
* `pgvector.Vector(1536)[]?`. When the field has no type, no node is emitted —
* a missing type is the absence of a `TypeAnnotation`, not a zero-width one.
*/
export function parseTypeAnnotation(cursor: Cursor): void {
const kind = cursor.peekKind();
if (kind !== 'Ident' && kind !== 'LBracket' && kind !== 'Question') {
return;
}
cursor.startNode('TypeAnnotation');
if (cursor.peekKind() === 'Ident') {
parseQualifiedName(cursor);
Expand All @@ -453,7 +471,7 @@ export function parseTypeAnnotation(cursor: Cursor): GreenNode {
if (cursor.peekKind() === 'Question') {
cursor.bump();
}
return cursor.finishNode();
cursor.finishNode();
}

type MemberParser = (cursor: Cursor) => void;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ export interface AstNode {
readonly syntax: SyntaxNode;
}

export interface BracedBlock extends AstNode {
lbrace(): SyntaxToken | undefined;
rbrace(): SyntaxToken | undefined;
}

export function findChildToken(node: SyntaxNode, kind: TokenKind): SyntaxToken | undefined {
for (const child of node.children()) {
if (!(child instanceof SyntaxNode) && child.kind === kind) {
Expand Down Expand Up @@ -35,6 +40,25 @@ export function* filterChildren<T>(
}
}

type CastTarget<C> = C extends (node: SyntaxNode) => infer R ? Exclude<R, undefined> : never;

export function any<Casts extends readonly ((node: SyntaxNode) => unknown)[]>(
...casts: Casts
): (node: SyntaxNode) => CastTarget<Casts[number]> | undefined;
export function any(
...casts: ReadonlyArray<(node: SyntaxNode) => unknown>
): (node: SyntaxNode) => unknown {
return (node) => {
for (const cast of casts) {
const result = cast(node);
if (result !== undefined) {
return result;
}
}
return undefined;
};
}

/**
* Raw source text of a CST node, verbatim (quotes and brackets preserved). For
* the decoded value of a string literal, decode it instead.
Expand Down
Loading
Loading