Skip to content
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
d3fae8d
test(vscode): add real multi-root integration workspace
StevenMcClankerton Aug 22, 2026
997db88
feat(vscode): add document ownership coordinator
StevenMcClankerton Aug 24, 2026
e21d76c
fix(vscode): guard ownership commit effects
StevenMcClankerton Aug 24, 2026
a459ee2
fix(vscode): align ownership after async commits
StevenMcClankerton Aug 24, 2026
bb24c24
feat(vscode): gate bundled client by document owner
StevenMcClankerton Aug 24, 2026
d10f54b
fix(vscode): balance bundled document synchronization
StevenMcClankerton Aug 24, 2026
a75f6bb
fix(vscode): reset bundled state on restart
StevenMcClankerton Aug 24, 2026
bcefc7e
feat(vscode): start root-local Prisma Next clients
StevenMcClankerton Aug 24, 2026
15e27dd
fix(vscode): launch local Prisma CLI with exact args
StevenMcClankerton Aug 24, 2026
5a26332
feat(vscode): transfer Prisma document ownership
StevenMcClankerton Aug 24, 2026
a2a44d2
fix(vscode): cancel routing for closed documents
StevenMcClankerton Aug 24, 2026
038dc7e
test(vscode): prove local Prisma Next routing
StevenMcClankerton Aug 24, 2026
426e6a1
test(vscode): restore integration fixtures
StevenMcClankerton Aug 24, 2026
eee2aa6
test(vscode): make routing e2e setup deterministic
StevenMcClankerton Aug 24, 2026
4d2f4ff
test(vscode): opt into bundled routing readiness
StevenMcClankerton Aug 24, 2026
a1d73fe
fix(vscode): await bundled client before routing
StevenMcClankerton Aug 24, 2026
deba8b4
fix(vscode): stabilize bundled client startup
StevenMcClankerton Aug 24, 2026
379d007
fix(vscode): contain bundled client stop failures
StevenMcClankerton Aug 24, 2026
9c89155
test(vscode): align routing diagnostics with local CLI
StevenMcClankerton Aug 24, 2026
bfb0210
test(vscode): preserve document EOL in routing E2E
StevenMcClankerton Aug 24, 2026
6dee7f7
test(vscode): support renamed macOS executable
StevenMcClankerton Aug 24, 2026
fb1701c
ci: run extension e2e harness on Node 22
StevenMcClankerton Aug 24, 2026
ff84827
fix(vscode): make studio asset copy repeatable
StevenMcClankerton Aug 24, 2026
89f6f7b
test(vscode): isolate Playwright Electron attempts
StevenMcClankerton Aug 24, 2026
cea546b
fix(vscode): shorten Playwright IPC paths on macOS
StevenMcClankerton Aug 24, 2026
91fb3dc
test(vscode): verify side-by-side Prisma completions
StevenMcClankerton Aug 25, 2026
a81ac7e
docs(vscode): document completion routing oracle
StevenMcClankerton Aug 25, 2026
a0a2fb8
test(vscode): load Prisma 8 ORM config
StevenMcClankerton Aug 25, 2026
4af47be
chore(vscode): type-check integration fixture lint
StevenMcClankerton Aug 25, 2026
f3b9117
fix(vscode): register local Prisma providers cross-platform
StevenMcClankerton Aug 25, 2026
d2544a0
fix(vscode): gate local semantic providers by owner
StevenMcClankerton Aug 25, 2026
2603c15
fix(vscode): fork local Prisma language server
StevenMcClankerton Aug 25, 2026
1a9070a
fix(vscode): use fixed Prisma 8 ORM stack
StevenMcClankerton Aug 25, 2026
d0285aa
test(vscode): use coherent Prisma 8 LSP build
StevenMcClankerton Aug 25, 2026
0d18a36
refactor(vscode): clarify language server ownership
StevenMcClankerton Aug 25, 2026
0d21c20
fix(vscode): preserve ownership through restart failures
StevenMcClankerton Aug 25, 2026
04c1c98
fix(vscode): serialize legacy server restarts
StevenMcClankerton Aug 25, 2026
bcaa22a
fix(vscode): serialize language server disposal
StevenMcClankerton Aug 25, 2026
8a7a933
fix(vscode): await plugin deactivation
StevenMcClankerton Aug 25, 2026
907fb2a
docs: simplify Prisma Next routing guidance
StevenMcClankerton Aug 25, 2026
6cde503
refactor(vscode): share document owner equality
StevenMcClankerton Aug 25, 2026
8571037
fix(vscode): clean up failed Prisma Next clients
StevenMcClankerton Aug 25, 2026
5280f73
fix(vscode): escape workspace glob roots
StevenMcClankerton Aug 25, 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
61 changes: 61 additions & 0 deletions docs/language-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,64 @@ for (const line of schema.iterLines()) {
See [Prisma Multi-File Schema Documentation][multi-file-docs] for details.

[multi-file-docs]: https://www.prisma.io/docs/orm/prisma-schema/overview/location#multi-file-prisma-schema

## VS Code document routing
Comment thread
SevInf marked this conversation as resolved.

When `prisma.pinToPrisma6` is disabled, the VS Code extension routes each open Prisma document independently:

| Document | Owner |
| --------------------------------------------------------------------------------- | ------------------------------------------ |
| No `// use prisma-next` directive | Bundled language server |
| Directive present, trusted file workspace, matching root, and local CLI available | Prisma Next client for that workspace root |
| Directive present but local execution is ineligible or unavailable | No active language-server synchronization |

The directive is content based and applies per file. A marked file does not opt sibling files or the rest of a multi-file schema into Prisma Next tooling.
Comment thread
SevInf marked this conversation as resolved.
Outdated

### Coordinator and synchronization boundary

`DocumentOwnershipCoordinator` is the authoritative per-URI state machine. Open and change events are serialized per document. A transfer performs these operations in order:

1. Close the prior synchronized owner.
2. Clear that owner's diagnostics for only the transferred URI.
3. Reclassify current unsaved text.
4. Lazily ensure the candidate root-local client when needed.
5. Reclassify after asynchronous startup.
6. Open the complete current document on the surviving owner.

A close event invalidates pending revisions immediately, queues final cleanup, and leaves the URI internally unowned. Candidate opens also check that the exact `TextDocument` remains in `workspace.textDocuments`. These checks prevent delayed startup or close operations from reopening an editor document that has already closed.

Bundled and local middleware maintain ledgers of documents actually synchronized to their client. Raw editor notifications are forwarded only when committed ownership, current content classification, and (for local clients) the exact workspace root agree. Completion and completion resolve, hover, definition, references, document symbols, formatting, rename, code actions, and diagnostics use the same ownership gate. Automatic local-client initial synchronization is suppressed until the coordinator explicitly opens an owned document, so unmarked contents are never sent to a local client over LSP.

### Root-local Prisma Next launch contract

The local-client registry is keyed by `WorkspaceFolder.uri.toString()` and coalesces concurrent startup for one root. Discovery checks only:

```text
<workspace-root>/node_modules/prisma/dist/prisma.js
```

The registry does not invoke a package manager, search parent directories, inspect package boundaries, or fall back to a global executable. Local execution requires `workspace.isTrusted` and a file-backed document in a file-backed workspace folder.

The extension launches the module with the extension-host runtime using the exact process shape:

```text
executable: process.execPath
argv: [<workspace-root>/node_modules/prisma/dist/prisma.js, "lsp"]
cwd: <workspace-root>
stdio: piped
shell: false
```

Electron extension hosts receive `ELECTRON_RUN_AS_NODE=1` and `ELECTRON_NO_ASAR=1`. The custom server-options launcher avoids transport arguments that `vscode-languageclient` would otherwise append.

### Registry lifecycle contract

The registry exposes a narrow lifecycle API used by routing and later workspace lifecycle handling:

- `ensureClientForDocument(document)` — trust/root checks, exact discovery, and coalesced lazy startup.
- `openDocument(rootUri, document)` — verifies the document is still open before inserting it into the local middleware ledger.
- `closeDocument(rootUri, document)` — idempotently balances an actually synchronized local document.
- `clearDiagnostics(rootUri, uri)` — clears only the requested URI.
- `getTestState()` — reports successful root starts without exposing process handles; it is reachable through a command only in debug/test sessions.

A started local client currently remains alive after its final marked document closes. Workspace-wide restart and rediscovery, runtime-failure recovery, workspace-folder removal, comprehensive deactivation, and live Prisma 6 pin transitions are separate lifecycle responsibilities that should build on this API rather than bypass the coordinator or middleware ledgers.
19 changes: 19 additions & 0 deletions docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,22 @@ pnpm test:e2e:vsix <extension_type> <os> <version>

Both scripts use the same tests in `packages/vscode/src/__test__` with fixtures
located in `packages/vscode/fixtures`.

## VS Code Electron integration tests

The Electron runner opens `packages/vscode/tests/fixtures/integration-workspace.code-workspace`. Its roots include:

- Two pnpm importers with the lockfile-resolved real Prisma Next CLI at `node_modules/prisma/dist/prisma.js`.
- An additional marked-document fixture without that exact entrypoint, used to verify silent no-fallback behavior.

Run the focused minimum-runtime workspace suite with:

```bash
pnpm --filter prisma test:integration:workspace
```

This command rebuilds the extension, compiles the integration tests, launches the minimum supported VS Code version, and runs `workspace.test.js`. The test uses the real Prisma CLI process; no mock language-server executable is part of the fixture. It covers lazy activation, successful real-client initialization per root, root reuse and independence, exclusive bundled/local ownership, complete-text unsaved directive transfers, bundled diagnostic production and transfer-time clearing, and missing-entrypoint behavior. The current Prisma Next CLI does not publish schema diagnostics.

The runner's installed `@vscode/test-electron` version always adds `--disable-workspace-trust`, so the Electron workspace is deterministically trusted. It cannot represent Restricted Mode without replacing or bypassing the runner's launch contract. Trust rejection is therefore covered at the production classifier and registry boundaries by focused unit tests; a manual Restricted Mode check remains necessary when validating trust behavior end to end.

Routing observations are available only when `isDebugOrTestSession()` is true. The test command reports ownership/routing events and successful start counts. Complete document text and version are captured only by the optional test observer; production activation installs neither the collector nor the command, and no process handles are exposed.
11 changes: 7 additions & 4 deletions packages/vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"license": "Apache-2.0",
"icon": "logo_white.png",
"engines": {
"vscode": "^1.96.0"
"vscode": "^1.104.0"
},
"publisher": "Prisma",
"categories": [
Expand Down Expand Up @@ -46,6 +46,8 @@
"build:types": "tsc -p ./ --emitDeclarationOnly",
"watch": "node esbuild.mjs --watch",
"test:integration": "rm -rf ./dist-tests && node esbuild.mjs && tsc -p tsconfig.test.json && node dist-tests/__test__/runTest true",
"test:integration:workspace": "rm -rf ./dist-tests && node esbuild.mjs && tsc -p tsconfig.test.json && node dist-tests/__test__/runTest --minimum-only --test-pattern workspace.test.js",
"test:ownership": "vitest run src/plugins/prisma-language-server/documentOwnership.test.ts src/plugins/prisma-language-server/documentRouting.test.ts src/plugins/prisma-language-server/bundledClientMiddleware.test.ts src/plugins/prisma-language-server/localClientMiddleware.test.ts src/plugins/prisma-language-server/localPrismaNextClientRegistry.test.ts",
"test:playwright": "playwright test",
"test:playwright:headless": "CI=true xvfb-run -a npm run test:playwright",
"vscode:prepublish": "pnpm run build",
Expand Down Expand Up @@ -686,7 +688,7 @@
"@types/mocha": "10.0.10",
"@types/node": "20.14.8",
"@types/sinon": "^20.0.0",
"@types/vscode": "1.96.0",
"@types/vscode": "1.104.0",
"@vscode/test-electron": "2.4.1",
"@vscode/vsce": "2.29.0",
"esbuild": "^0.27.1",
Expand All @@ -696,11 +698,12 @@
"ovsx": "0.10.1",
"pkg-types": "2.2.0",
"sinon": "^21.0.0",
"typescript": "5.7.3"
"typescript": "5.7.3",
"vitest": "^2.1.0"
},
"gitHead": "7d51b157647fe1705813a30d1a77b8ccf136b8d4",
"publishConfig": {
"access": "public"
},
"preview": false
}
}
49 changes: 47 additions & 2 deletions packages/vscode/src/__test__/helper.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import path from 'path'
import vscode from 'vscode'
import {
languageServerTestStateCommand,
type LanguageServerTestState,
} from '../plugins/prisma-language-server/languageServerTestState'

// Path from dist-tests/__test__/helper.js to package.json
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
Expand All @@ -18,7 +22,11 @@ export async function sleep(ms: number): Promise<void> {
* Activates the vscode.prisma extension
* @todo check readiness of the server instead of timeout
*/
export async function activate(docUri: vscode.Uri): Promise<void> {
export interface ActivateOptions {
readonly waitForBundledRouting?: boolean
}

export async function activate(docUri: vscode.Uri, options: ActivateOptions = {}): Promise<void> {
// The extensionId is `publisher.name` from package.json
const ext = vscode.extensions.getExtension(`${packageJson.publisher}.${packageJson.name}`)
if (!ext) {
Expand All @@ -29,10 +37,31 @@ export async function activate(docUri: vscode.Uri): Promise<void> {
try {
doc = await vscode.workspace.openTextDocument(docUri)
editor = await vscode.window.showTextDocument(doc)
await sleep(2500) // Wait for server activation
} catch (e) {
console.error(e)
return
}

if (options.waitForBundledRouting) {
await waitForBundledRouting(doc)
}
await sleep(2500) // Wait for server activation
}

async function waitForBundledRouting(document: vscode.TextDocument): Promise<void> {
const documentUri = document.uri.toString()
const deadline = Date.now() + 10_000

while (Date.now() < deadline) {
const state = await vscode.commands.executeCommand<LanguageServerTestState>(languageServerTestStateCommand)
const latestOpen = [...state.routingEvents]
.reverse()
.find((event) => event.type === 'opened' && event.documentUri === documentUri)
if (latestOpen?.owner.kind === 'bundled') return
await sleep(100)
}

throw new Error(`Timed out waiting for bundled language-server routing for ${documentUri}`)
}

export function toRange(sLine: number, sChar: number, eLine: number, eChar: number): vscode.Range {
Expand All @@ -49,6 +78,22 @@ export const getDocUri = (p: string): vscode.Uri => {
return vscode.Uri.file(getDocPath(p))
}

export function getWorkspaceFolder(name: string): vscode.WorkspaceFolder {
const workspaceFolder = vscode.workspace.workspaceFolders?.find((folder) => folder.name === name)
if (!workspaceFolder) {
throw new Error(`Workspace folder not found: ${name}`)
}
return workspaceFolder
}

export function getWorkspaceDocUri(workspaceFolder: vscode.WorkspaceFolder, relativePath: string): vscode.Uri {
return vscode.Uri.joinPath(workspaceFolder.uri, relativePath)
}

export function getPrismaCliEntrypoint(workspaceFolder: vscode.WorkspaceFolder): vscode.Uri {
return vscode.Uri.joinPath(workspaceFolder.uri, 'node_modules', 'prisma', 'dist', 'prisma.js')
}

export async function setTestContent(content: string): Promise<boolean> {
const all = new vscode.Range(doc.positionAt(0), doc.positionAt(doc.getText().length))
return editor.edit((eb) => eb.replace(all, content))
Expand Down
3 changes: 2 additions & 1 deletion packages/vscode/src/__test__/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ export function run(): Promise<void> {
const testsRoot = __dirname

return new Promise((resolve, reject) => {
glob('**/**.test.js', { cwd: testsRoot }, (err, files) => {
const testPattern = process.env.VSCODE_TEST_PATTERN ?? '**/**.test.js'
glob(testPattern, { cwd: testsRoot }, (err, files) => {
if (err) {
return reject(err)
}
Expand Down
14 changes: 14 additions & 0 deletions packages/vscode/src/__test__/language-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,17 @@

Only one test per feature is done here.
The goal is to check that the integration is working between the VS Code extension and the Language Server.

The integration runner opens `tests/fixtures/integration-workspace.code-workspace`. Two roots are pnpm workspace importers with the same lockfile-resolved real Prisma Next CLI at `node_modules/prisma/dist/prisma.js`; a third root intentionally has no local CLI entrypoint.

Run the full minimum-and-latest integration suite with `pnpm test:integration`. Run the focused real-CLI routing suite on the minimum supported VS Code runtime with:

```bash
pnpm --filter prisma test:integration:workspace
```

The focused suite verifies that activation and unmarked documents start no local process, each eligible marked root completes exactly one real client initialization handshake, additional documents reuse their root client, roots remain independent, and the missing-entrypoint root has no fallback process. It also observes exclusive bundled/local synchronization, both unsaved directive transfer directions, complete current text/version, and URI-scoped diagnostics clearing. The current Prisma Next CLI does not publish schema diagnostics, so diagnostic production is asserted only while the document is bundled; routing-state observations prove that those diagnostics are cleared during ownership transfers.

Test-only routing state is exposed through `prisma.test.languageServerRoutingState`. The command is registered only when `isDebugOrTestSession()` is true; production sessions do not install the observer or retain observed document contents. The state contains no process handles.

`@vscode/test-electron` adds `--disable-workspace-trust` unconditionally, so this harness always runs trusted. Restricted Mode execution remains a manual check; focused classifier and registry tests cover the untrusted production boundaries.
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ async function testCompletion(
triggerCharacter?: string,
): Promise<void> {
if (!isActivated) {
await activate(docUri)
await activate(docUri, { waitForBundledRouting: true })
}

const actualCompletions: vscode.CompletionList = await vscode.commands.executeCommand(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { getDocUri, activate } from '../helper'
import fs from 'fs'

async function testAutoFormat(docUri: vscode.Uri, expectedFormatted: string): Promise<void> {
await activate(docUri)
await activate(docUri, { waitForBundledRouting: true })

const actualFormatted = (await vscode.commands.executeCommand('vscode.executeFormatDocumentProvider', docUri, {
insertSpaces: true,
Expand Down
2 changes: 1 addition & 1 deletion packages/vscode/src/__test__/language-server/hover.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ suite('Should show /// documentation comments for', () => {
const expectedHover = `\`\`\`prisma\nmodel Post {\n\t...\n\tauthor User? @relation(name: "PostToUser", fields: [authorId], references: [id])\n}\n\`\`\`\n___\none-to-many\n___\nPost including an author and content.`

test('model', async () => {
await activate(docUri)
await activate(docUri, { waitForBundledRouting: true })
await testHover(docUri, new vscode.Position(22, 10), expectedHover)
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ suite('Jump-to-definition', () => {
const fixturePathSqlite = getDocUri('jump-to-definition/schema.prisma')

test('SQLite: from attribute to model', async function () {
await activate(fixturePathSqlite)
await activate(fixturePathSqlite, { waitForBundledRouting: true })

await testJumpToDefinition(
fixturePathSqlite,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as assert from 'assert'
import { getDocUri, activate, toRange } from '../helper'

async function testDiagnostics(docUri: vscode.Uri, expectedDiagnostics: vscode.Diagnostic[]): Promise<void> {
await activate(docUri)
await activate(docUri, { waitForBundledRouting: true })

const actualDiagnostics = vscode.languages.getDiagnostics(docUri)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ suite('Prisma-next directive', () => {

test('Sibling file without directive still gets diagnostics', async () => {
const docUri = getDocUri('linting/missingArgument.prisma')
await activate(docUri)
await activate(docUri, { waitForBundledRouting: true })
const diagnostics = await waitForDiagnostics(docUri, (d) => d.length > 0)
assert.ok(diagnostics.length > 0, 'expected diagnostics on regular file with errors')
})
Expand Down
25 changes: 21 additions & 4 deletions packages/vscode/src/__test__/runTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { runTests } from '@vscode/test-electron'
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
const packageJson = require('../../package.json') as { engines: { vscode: string } }

function test(version?: string) {
function test(version?: string, testPattern?: string) {
// The folder containing the Extension Manifest package.json
// Passed to `--extensionDevelopmentPath`
const extensionDevelopmentPath = path.resolve(__dirname, '../../')
Expand All @@ -14,12 +14,20 @@ function test(version?: string) {
// Passed to --extensionTestsPath
const extensionTestsPath = path.resolve(__dirname, './index')

// The explicit multi-root workspace opened by every integration test run.
const workspacePath = path.resolve(__dirname, '../../tests/fixtures/integration-workspace.code-workspace')

// Downloads VS Code, unzip it and run the integration test
return runTests({
version, // optional, default = latest
extensionDevelopmentPath,
extensionTestsPath,
extensionTestsEnv: {
PRISMA_VSCODE_TEST: '1',
...(testPattern ? { VSCODE_TEST_PATTERN: testPattern } : {}),
},
launchArgs: [
workspacePath,
// This disables all extensions except the one being testing
'--disable-extensions',
// ? This may or may not be necessary?
Expand All @@ -41,15 +49,24 @@ function test(version?: string) {

async function main(): Promise<void> {
try {
const minimumOnly = process.argv.includes('--minimum-only')
const testPatternFlag = process.argv.indexOf('--test-pattern')
const testPattern = testPatternFlag === -1 ? undefined : process.argv[testPatternFlag + 1]
if (testPatternFlag !== -1 && !testPattern) {
throw new Error('--test-pattern requires a glob pattern')
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

// 1 - Run on our minimum supported version from package.json
// eslint-disable-next-line
const minimumSupportedVersion: string = packageJson.engines.vscode.replace('~', '').replace('^', '') // remove semver chars
console.log(`*** Testing on minimum supported version of VS Code: ${minimumSupportedVersion} ***`)
await test(minimumSupportedVersion)
await test(minimumSupportedVersion, testPattern)

// 2 - Run again on latest version
console.log(`*** Testing on latest version of VS Code ***`)
await test()
if (!minimumOnly) {
console.log(`*** Testing on latest version of VS Code ***`)
await test(undefined, testPattern)
}
} catch (err) {
const errMsg = err instanceof Error ? ` ${err.message}` : ''

Expand Down
Loading
Loading