From bd42e956d7ed64654f44df19a72d8e9f169fcb15 Mon Sep 17 00:00:00 2001 From: Eric Bryan Date: Thu, 1 Jan 2026 10:47:45 -0800 Subject: [PATCH] Add `schemaPath` support --- docs/language-server.md | 36 ++++++ packages/language-server/package.json | 2 +- .../src/__test__/schema-path.test.ts | 122 ++++++++++++++++++ packages/language-server/src/lib/Schema.ts | 34 ++++- packages/language-server/src/lib/types.ts | 12 ++ packages/language-server/src/server.ts | 47 +++++-- packages/vscode/package.json | 7 +- 7 files changed, 246 insertions(+), 14 deletions(-) create mode 100644 packages/language-server/src/__test__/schema-path.test.ts diff --git a/docs/language-server.md b/docs/language-server.md index 657ae1cd43..e6ea16324e 100644 --- a/docs/language-server.md +++ b/docs/language-server.md @@ -24,12 +24,48 @@ const schema = await PrismaSchema.load({ allDocuments: documents.all(), }) +// Loading with explicit schema path (from VS Code settings) +const schema = await PrismaSchema.load( + { + currentDocument: textDocument, + allDocuments: documents.all(), + }, + { + schemaPath: '/path/to/schema/directory', + } +) + // Iterating over all lines across files for (const line of schema.iterLines()) { // line.document, line.lineIndex, line.text } ``` +### Schema Path Resolution + +The language server determines the schema location in the following priority order: + +1. **`schemaPath` option** (from VS Code `prisma.schemaPath` setting) +2. **`prisma.config.ts`** (discovered by searching upward from `schemaPath` or current document) +3. **Current document path** (fallback) + +This matches the behavior of the Prisma CLI, ensuring consistency between IDE and command-line tooling. + +### VS Code Configuration + +Users can configure the schema path in `.vscode/settings.json`: + +```json +{ + "prisma.schemaPath": "packages/backend/prisma" +} +``` + +This setting can point to: +- A directory containing multiple `.prisma` files +- A single `.prisma` file +- A relative path (from workspace root) or absolute path + 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 diff --git a/packages/language-server/package.json b/packages/language-server/package.json index f152e3db3a..925437533c 100644 --- a/packages/language-server/package.json +++ b/packages/language-server/package.json @@ -62,4 +62,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/packages/language-server/src/__test__/schema-path.test.ts b/packages/language-server/src/__test__/schema-path.test.ts new file mode 100644 index 0000000000..b840e9b91e --- /dev/null +++ b/packages/language-server/src/__test__/schema-path.test.ts @@ -0,0 +1,122 @@ +import { test, expect, describe } from 'vitest' +import { PrismaSchema, SchemaDocument } from '../lib/Schema' +import { TextDocument } from 'vscode-languageserver-textdocument' +import { URI } from 'vscode-uri' +import path from 'path' +import { loadSchemaFiles } from '@prisma/schema-files-loader' + +const multifileFixturesDir = path.join(__dirname, '__fixtures__/multi-file') + +describe('PrismaSchema.load with schemaPath option', () => { + test('loads multi-file schema using schemaPath option', async () => { + const schemaPath = path.join(multifileFixturesDir, 'user-posts') + + // Create a single document (simulating opening one file in the editor) + const singleFilePath = path.join(schemaPath, 'Post.prisma') + const files = await loadSchemaFiles(schemaPath) + const postFile = files.find(([filePath]) => filePath.endsWith('Post.prisma')) + + if (!postFile) { + throw new Error('Post.prisma not found in fixtures') + } + + const currentDocument = TextDocument.create(URI.file(singleFilePath).toString(), 'prisma', 1, postFile[1]) + + // Load schema with schemaPath option (simulating VS Code setting) + const schema = await PrismaSchema.load({ currentDocument, allDocuments: [currentDocument] }, { schemaPath }) + + // Should have loaded all files from the directory, not just the current document + expect(schema.documents.length).toBeGreaterThan(1) + + // Should include User.prisma, Post.prisma, config.prisma, etc. + const fileNames = schema.documents.map((doc) => { + const uri = URI.parse(doc.uri) + return path.basename(uri.fsPath) + }) + + expect(fileNames).toContain('User.prisma') + expect(fileNames).toContain('Post.prisma') + expect(fileNames).toContain('config.prisma') + }) + + test('falls back to current document path when schemaPath is not provided', async () => { + const schemaPath = path.join(multifileFixturesDir, 'user-posts') + const files = await loadSchemaFiles(schemaPath) + const postFile = files.find(([filePath]) => filePath.endsWith('Post.prisma')) + + if (!postFile) { + throw new Error('Post.prisma not found in fixtures') + } + + const singleFilePath = path.join(schemaPath, 'Post.prisma') + const currentDocument = TextDocument.create(URI.file(singleFilePath).toString(), 'prisma', 1, postFile[1]) + + // Load schema WITHOUT schemaPath option + const schema = await PrismaSchema.load( + { currentDocument, allDocuments: [currentDocument] }, + {}, // No schemaPath provided + ) + + // Should still load all related files (via loadRelatedSchemaFiles) + expect(schema.documents.length).toBeGreaterThan(1) + }) + + test('supports backward compatibility with string configRoot parameter', async () => { + const schemaPath = path.join(multifileFixturesDir, 'user-posts') + const files = await loadSchemaFiles(schemaPath) + const postFile = files.find(([filePath]) => filePath.endsWith('Post.prisma')) + + if (!postFile) { + throw new Error('Post.prisma not found in fixtures') + } + + const singleFilePath = path.join(schemaPath, 'Post.prisma') + const currentDocument = TextDocument.create(URI.file(singleFilePath).toString(), 'prisma', 1, postFile[1]) + + // Load schema with old signature (string configRoot) + const schema = await PrismaSchema.load( + { currentDocument, allDocuments: [currentDocument] }, + schemaPath, // Old signature: string instead of options object + ) + + expect(schema.documents.length).toBeGreaterThan(1) + }) + + test('schemaPath takes priority over prisma.config.ts', async () => { + const externalConfigDir = path.join(multifileFixturesDir, 'external-config') + const schemaPath = path.join(externalConfigDir, 'schema.prisma') + + const files = await loadSchemaFiles(externalConfigDir) + const schemaFile = files.find(([filePath]) => filePath.endsWith('schema.prisma')) + + if (!schemaFile) { + throw new Error('schema.prisma not found in fixtures') + } + + const currentDocument = TextDocument.create(URI.file(schemaPath).toString(), 'prisma', 1, schemaFile[1]) + + // Load with explicit schemaPath + const schema = await PrismaSchema.load({ currentDocument, allDocuments: [currentDocument] }, { schemaPath }) + + // Should load based on schemaPath, not config + expect(schema.documents.length).toBeGreaterThan(0) + const fileNames = schema.documents.map((doc) => path.basename(URI.parse(doc.uri).fsPath)) + expect(fileNames).toContain('schema.prisma') + }) + + test('loads schema from SchemaDocument array directly', async () => { + const schemaPath = path.join(multifileFixturesDir, 'user-posts') + const files = await loadSchemaFiles(schemaPath) + + const schemaDocs = files.map(([filePath, content]) => { + const uri = URI.file(filePath).toString() + const doc = TextDocument.create(uri, 'prisma', 1, content) + return new SchemaDocument(doc) + }) + + // Load with array input (used in tests) + const schema = await PrismaSchema.load(schemaDocs, { configRoot: schemaPath }) + + expect(schema.documents.length).toBe(schemaDocs.length) + }) +}) diff --git a/packages/language-server/src/lib/Schema.ts b/packages/language-server/src/lib/Schema.ts index 31d3539244..567481973b 100644 --- a/packages/language-server/src/lib/Schema.ts +++ b/packages/language-server/src/lib/Schema.ts @@ -92,13 +92,39 @@ async function loadSchemaDocumentsFromPath(fsPath: string, allDocuments: TextDoc type PrismaSchemaInput = { currentDocument: TextDocument; allDocuments: TextDocument[] } | SchemaDocument[] +export interface PrismaSchemaLoadOptions { + /** + * Optional path to the schema file or directory. + * This corresponds to the VS Code `prisma.schemaPath` setting. + */ + schemaPath?: string + /** + * Optional path to the directory containing prisma.config.ts + * Used for testing purposes. + */ + configRoot?: string +} + export class PrismaSchema { // TODO: remove, use `PrismaSchema.load` directly static singleFile(textDocument: TextDocument) { return new PrismaSchema([new SchemaDocument(textDocument)]) } - static async load(input: PrismaSchemaInput, configRoot?: string): Promise { + static async load(input: PrismaSchemaInput, options?: PrismaSchemaLoadOptions | string): Promise { + // Support both old signature (configRoot as string) and new signature (options object) + // for backward compatibility with tests + const opts: PrismaSchemaLoadOptions = typeof options === 'string' ? { configRoot: options } : (options ?? {}) + + // Determine the configRoot for finding prisma.config.ts + // Priority: explicit configRoot > schemaPath directory > current document directory + let configRoot = opts.configRoot + if (!configRoot && opts.schemaPath) { + // If schemaPath is provided, use its directory as configRoot + const schemaUri = URI.file(opts.schemaPath) + configRoot = schemaUri.fsPath + } + let config: PrismaConfigInternal | undefined try { config = await loadConfig(configRoot) @@ -111,7 +137,11 @@ export class PrismaSchema { if (Array.isArray(input)) { schemaDocs = input } else { - const fsPath = config?.schema ?? URI.parse(input.currentDocument.uri).fsPath + // Priority for determining schema path: + // 1. schemaPath from VS Code settings + // 2. schema path from prisma.config.ts + // 3. Current document's path (fallback) + const fsPath = opts.schemaPath ?? config?.schema ?? URI.parse(input.currentDocument.uri).fsPath schemaDocs = await loadSchemaDocumentsFromPath(fsPath, input.allDocuments) } return new PrismaSchema(schemaDocs, config) diff --git a/packages/language-server/src/lib/types.ts b/packages/language-server/src/lib/types.ts index 9dcfff47c7..be67c0185b 100644 --- a/packages/language-server/src/lib/types.ts +++ b/packages/language-server/src/lib/types.ts @@ -23,4 +23,16 @@ export interface LSSettings { * Whether to show diagnostics */ enableDiagnostics?: boolean + /** + * Path to the Prisma schema file or directory containing schema files. + * Can be: + * - A path to a single .prisma file + * - A path to a directory containing multiple .prisma files + * - Relative to the workspace root or absolute + * + * If not provided, the language server will: + * 1. Try to find prisma.config.ts and use its schema path + * 2. Fall back to the currently opened document's directory + */ + schemaPath?: string } diff --git a/packages/language-server/src/server.ts b/packages/language-server/src/server.ts index ed0586ceca..691d325926 100644 --- a/packages/language-server/src/server.ts +++ b/packages/language-server/src/server.ts @@ -159,7 +159,10 @@ export function startServer(options?: LSOptions): void { return } - const schema = await PrismaSchema.load({ currentDocument: textDocument, allDocuments: documents.all() }) + const schema = await PrismaSchema.load( + { currentDocument: textDocument, allDocuments: documents.all() }, + { schemaPath: settings.schemaPath }, + ) const diagnostics = MessageHandler.handleDiagnosticsRequest(schema, showErrorToast) for (const [uri, fileDiagnostics] of diagnostics.entries()) { await connection.sendDiagnostics({ uri, diagnostics: fileDiagnostics }) @@ -177,7 +180,11 @@ export function startServer(options?: LSOptions): void { connection.onDefinition(async (params: DeclarationParams) => { const doc = getDocument(params.textDocument.uri) if (doc) { - const schema = await PrismaSchema.load({ currentDocument: doc, allDocuments: documents.all() }) + const settings = await getDocumentSettings(doc.uri) + const schema = await PrismaSchema.load( + { currentDocument: doc, allDocuments: documents.all() }, + { schemaPath: settings.schemaPath }, + ) return MessageHandler.handleDefinitionRequest(schema, doc, params) } }) @@ -185,7 +192,11 @@ export function startServer(options?: LSOptions): void { connection.onCompletion(async (params: CompletionParams) => { const doc = getDocument(params.textDocument.uri) if (doc) { - const schema = await PrismaSchema.load({ currentDocument: doc, allDocuments: documents.all() }) + const settings = await getDocumentSettings(doc.uri) + const schema = await PrismaSchema.load( + { currentDocument: doc, allDocuments: documents.all() }, + { schemaPath: settings.schemaPath }, + ) return MessageHandler.handleCompletionRequest(schema, doc, params, showErrorToast) } }) @@ -194,7 +205,11 @@ export function startServer(options?: LSOptions): void { const doc = getDocument(params.textDocument.uri) if (doc) { - const schema = await PrismaSchema.load({ currentDocument: doc, allDocuments: documents.all() }) + const settings = await getDocumentSettings(doc.uri) + const schema = await PrismaSchema.load( + { currentDocument: doc, allDocuments: documents.all() }, + { schemaPath: settings.schemaPath }, + ) return MessageHandler.handleReferencesRequest(schema, params, showErrorToast) } @@ -217,7 +232,11 @@ export function startServer(options?: LSOptions): void { connection.onHover(async (params: HoverParams) => { const doc = getDocument(params.textDocument.uri) if (doc) { - const schema = await PrismaSchema.load({ currentDocument: doc, allDocuments: documents.all() }) + const settings = await getDocumentSettings(doc.uri) + const schema = await PrismaSchema.load( + { currentDocument: doc, allDocuments: documents.all() }, + { schemaPath: settings.schemaPath }, + ) return MessageHandler.handleHoverRequest(schema, doc, params, showErrorToast) } }) @@ -225,7 +244,11 @@ export function startServer(options?: LSOptions): void { connection.onDocumentFormatting(async (params: DocumentFormattingParams) => { const doc = getDocument(params.textDocument.uri) if (doc) { - const schema = await PrismaSchema.load({ currentDocument: doc, allDocuments: documents.all() }) + const settings = await getDocumentSettings(doc.uri) + const schema = await PrismaSchema.load( + { currentDocument: doc, allDocuments: documents.all() }, + { schemaPath: settings.schemaPath }, + ) return MessageHandler.handleDocumentFormatting(schema, doc, params, showErrorToast) } }) @@ -233,7 +256,11 @@ export function startServer(options?: LSOptions): void { connection.onCodeAction(async (params: CodeActionParams) => { const doc = getDocument(params.textDocument.uri) if (doc) { - const schema = await PrismaSchema.load({ currentDocument: doc, allDocuments: documents.all() }) + const settings = await getDocumentSettings(doc.uri) + const schema = await PrismaSchema.load( + { currentDocument: doc, allDocuments: documents.all() }, + { schemaPath: settings.schemaPath }, + ) return MessageHandler.handleCodeActions(schema, doc, params, showErrorToast) } }) @@ -241,7 +268,11 @@ export function startServer(options?: LSOptions): void { connection.onRenameRequest(async (params: RenameParams) => { const doc = getDocument(params.textDocument.uri) if (doc) { - const schema = await PrismaSchema.load({ currentDocument: doc, allDocuments: documents.all() }) + const settings = await getDocumentSettings(doc.uri) + const schema = await PrismaSchema.load( + { currentDocument: doc, allDocuments: documents.all() }, + { schemaPath: settings.schemaPath }, + ) return MessageHandler.handleRenameRequest(schema, doc, params) } }) diff --git a/packages/vscode/package.json b/packages/vscode/package.json index d9f848828d..7ee24b80aa 100644 --- a/packages/vscode/package.json +++ b/packages/vscode/package.json @@ -163,9 +163,10 @@ "prisma.schemaPath": { "type": "string", "examples": [ - "/path/to/your/schema.prisma" + "/path/to/your/schema.prisma", + "/path/to/your/prisma/directory" ], - "description": "If you have a Prisma schema file in a custom path, you will need to provide said path `/path/to/your/schema.prisma` to run generate" + "description": "If you have a Prisma schema file in a custom path, you will need to provide said path `/path/to/your/schema.prisma` to run generate and enable language server features." }, "prisma.pinToPrisma6": { "type": "boolean", @@ -703,4 +704,4 @@ "access": "public" }, "preview": true -} \ No newline at end of file +}