Skip to content
Open
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
36 changes: 36 additions & 0 deletions docs/language-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion packages/language-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,4 @@
"publishConfig": {
"access": "public"
}
}
}
122 changes: 122 additions & 0 deletions packages/language-server/src/__test__/schema-path.test.ts
Original file line number Diff line number Diff line change
@@ -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)
})
})
34 changes: 32 additions & 2 deletions packages/language-server/src/lib/Schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<PrismaSchema> {
static async load(input: PrismaSchemaInput, options?: PrismaSchemaLoadOptions | string): Promise<PrismaSchema> {
// 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)
Expand All @@ -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)
Expand Down
12 changes: 12 additions & 0 deletions packages/language-server/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
47 changes: 39 additions & 8 deletions packages/language-server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
Expand All @@ -177,15 +180,23 @@ 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)
}
})

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)
}
})
Expand All @@ -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)
}
Expand All @@ -217,31 +232,47 @@ 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)
}
})

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)
}
})

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)
}
})

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)
}
})
Expand Down
7 changes: 4 additions & 3 deletions packages/vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -703,4 +704,4 @@
"access": "public"
},
"preview": true
}
}