From 99312ecdf35c7978b8d1a5aa0874628a44786b5b Mon Sep 17 00:00:00 2001 From: Sergey Tatarintsev Date: Wed, 8 May 2024 15:11:44 +0200 Subject: [PATCH] Split schema support Use `@prisma/schema-files-loader` to resolve schema files from memory or disk. See also https://github.com/prisma/prisma/pull/24085 Message handler's methods now accept `PrismaSchema` instance from outside, rather than creating it from a single document. Singatures of the methods are also adjusted to consistently be in `(schema, intitianigDocument, params)` form. --- packages/language-server/package-lock.json | 114 ++++++++++++++++-- packages/language-server/package.json | 8 +- .../src/__test__/artificial-panic.test.ts | 13 +- .../src/__test__/completion.test.ts | 11 +- .../src/__test__/format.test.ts | 3 +- .../language-server/src/__test__/helper.ts | 19 ++- .../src/__test__/hover.test.ts | 3 +- .../src/__test__/jumpToDefinition.test.ts | 7 +- .../src/__test__/linting.test.ts | 3 +- .../src/__test__/quickFix.test.ts | 12 +- .../src/__test__/rename.test.ts | 7 +- .../language-server/src/lib/DiagnosticMap.ts | 25 ++++ .../language-server/src/lib/MessageHandler.ts | 87 ++++++------- packages/language-server/src/lib/Schema.ts | 97 ++++++++++++--- packages/language-server/src/lib/ast/block.ts | 13 +- .../src/lib/ast/configBlock.ts | 26 ---- .../src/lib/code-actions/index.ts | 14 +-- .../src/lib/completions/index.ts | 29 ++--- .../src/lib/prisma-schema-wasm/format.ts | 16 ++- .../src/lib/prisma-schema-wasm/lint.ts | 6 +- .../textDocumentCompletion.ts | 7 +- .../language-server/src/lib/validations.ts | 31 +---- packages/language-server/src/server.ts | 45 ++++--- scripts/update_package_json_files.js | 1 + 24 files changed, 399 insertions(+), 198 deletions(-) create mode 100644 packages/language-server/src/lib/DiagnosticMap.ts diff --git a/packages/language-server/package-lock.json b/packages/language-server/package-lock.json index c0fcac6ac5..f34fa66870 100644 --- a/packages/language-server/package-lock.json +++ b/packages/language-server/package-lock.json @@ -9,13 +9,15 @@ "version": "31.0.3641", "license": "Apache-2.0", "dependencies": { - "@prisma/prisma-schema-wasm": "5.15.0-3.97f638f5e0f371a1a553cf726f9d597bbe811bff", + "@prisma/prisma-schema-wasm": "5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48", + "@prisma/schema-files-loader": "5.14.0-dev.77", "@types/js-levenshtein": "1.1.3", "js-levenshtein": "1.1.6", "klona": "2.0.6", "nyc": "15.1.0", "vscode-languageserver": "8.1.0", - "vscode-languageserver-textdocument": "1.0.11" + "vscode-languageserver-textdocument": "1.0.11", + "vscode-uri": "^3.0.8" }, "bin": { "prisma-language-server": "dist/src/bin.js" @@ -520,9 +522,23 @@ } }, "node_modules/@prisma/prisma-schema-wasm": { - "version": "5.15.0-3.97f638f5e0f371a1a553cf726f9d597bbe811bff", - "resolved": "https://registry.npmjs.org/@prisma/prisma-schema-wasm/-/prisma-schema-wasm-5.15.0-3.97f638f5e0f371a1a553cf726f9d597bbe811bff.tgz", - "integrity": "sha512-FGyf9R/qipmCmZ8H7xb1EfH2KfYz5akfXTtON32k9PgrKA2k7h1PAlZXI7JWAYB06HTb5KNRJcpxaNih1W8ueQ==" + "version": "5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48", + "resolved": "https://registry.npmjs.org/@prisma/prisma-schema-wasm/-/prisma-schema-wasm-5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48.tgz", + "integrity": "sha512-WeTmJ0mK8ALoKJUQFO+465k9lm1JWS4ODUg7akJq1wjgyDU1RTAzDFli8ESmNJlMVgJgoAd6jXmzcnoA0HT9Lg==" + }, + "node_modules/@prisma/schema-files-loader": { + "version": "5.14.0-dev.77", + "resolved": "https://registry.npmjs.org/@prisma/schema-files-loader/-/schema-files-loader-5.14.0-dev.77.tgz", + "integrity": "sha512-XajLTyk1J76vXiRwgL9vbIggJDch7acuUwiIPrto/ymqxi5l/272s9dooBMBwuY4FXmtFwsrPnvJVyRDdEMNtg==", + "dependencies": { + "@prisma/prisma-schema-wasm": "5.14.0-17.56ca112d5a19c9925b53af75c3c6b7ada97f9f85", + "fs-extra": "11.1.1" + } + }, + "node_modules/@prisma/schema-files-loader/node_modules/@prisma/prisma-schema-wasm": { + "version": "5.14.0-17.56ca112d5a19c9925b53af75c3c6b7ada97f9f85", + "resolved": "https://registry.npmjs.org/@prisma/prisma-schema-wasm/-/prisma-schema-wasm-5.14.0-17.56ca112d5a19c9925b53af75c3c6b7ada97f9f85.tgz", + "integrity": "sha512-SX9vE9dGYBap6xsfJuDE5b2eoA6w1vKsx8QpLUHZR+kIV6GQVUYUboEfkvYYoBVen3s9LqxJ1+LjHL/1MqBZag==" }, "node_modules/@types/js-levenshtein": { "version": "1.1.3", @@ -1037,6 +1053,19 @@ } ] }, + "node_modules/fs-extra": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz", + "integrity": "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -1435,6 +1464,17 @@ "node": ">=6" } }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, "node_modules/klona": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", @@ -2249,6 +2289,14 @@ "node": ">=14.17" } }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/update-browserslist-db": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.5.tgz", @@ -2320,6 +2368,11 @@ "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.3.tgz", "integrity": "sha512-SYU4z1dL0PyIMd4Vj8YOqFvHu7Hz/enbWtpfnVbJHU4Nd1YNYx8u0ennumc6h48GQNeOLxmwySmnADouT/AuZA==" }, + "node_modules/vscode-uri": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", + "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==" + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -2811,9 +2864,25 @@ } }, "@prisma/prisma-schema-wasm": { - "version": "5.15.0-3.97f638f5e0f371a1a553cf726f9d597bbe811bff", - "resolved": "https://registry.npmjs.org/@prisma/prisma-schema-wasm/-/prisma-schema-wasm-5.15.0-3.97f638f5e0f371a1a553cf726f9d597bbe811bff.tgz", - "integrity": "sha512-FGyf9R/qipmCmZ8H7xb1EfH2KfYz5akfXTtON32k9PgrKA2k7h1PAlZXI7JWAYB06HTb5KNRJcpxaNih1W8ueQ==" + "version": "5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48", + "resolved": "https://registry.npmjs.org/@prisma/prisma-schema-wasm/-/prisma-schema-wasm-5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48.tgz", + "integrity": "sha512-WeTmJ0mK8ALoKJUQFO+465k9lm1JWS4ODUg7akJq1wjgyDU1RTAzDFli8ESmNJlMVgJgoAd6jXmzcnoA0HT9Lg==" + }, + "@prisma/schema-files-loader": { + "version": "5.14.0-dev.77", + "resolved": "https://registry.npmjs.org/@prisma/schema-files-loader/-/schema-files-loader-5.14.0-dev.77.tgz", + "integrity": "sha512-XajLTyk1J76vXiRwgL9vbIggJDch7acuUwiIPrto/ymqxi5l/272s9dooBMBwuY4FXmtFwsrPnvJVyRDdEMNtg==", + "requires": { + "@prisma/prisma-schema-wasm": "5.14.0-17.56ca112d5a19c9925b53af75c3c6b7ada97f9f85", + "fs-extra": "11.1.1" + }, + "dependencies": { + "@prisma/prisma-schema-wasm": { + "version": "5.14.0-17.56ca112d5a19c9925b53af75c3c6b7ada97f9f85", + "resolved": "https://registry.npmjs.org/@prisma/prisma-schema-wasm/-/prisma-schema-wasm-5.14.0-17.56ca112d5a19c9925b53af75c3c6b7ada97f9f85.tgz", + "integrity": "sha512-SX9vE9dGYBap6xsfJuDE5b2eoA6w1vKsx8QpLUHZR+kIV6GQVUYUboEfkvYYoBVen3s9LqxJ1+LjHL/1MqBZag==" + } + } }, "@types/js-levenshtein": { "version": "1.1.3", @@ -3171,6 +3240,16 @@ "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.3.2.tgz", "integrity": "sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg==" }, + "fs-extra": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz", + "integrity": "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==", + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -3449,6 +3528,15 @@ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==" }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, "klona": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", @@ -4040,6 +4128,11 @@ "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", "dev": true }, + "universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==" + }, "update-browserslist-db": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.5.tgz", @@ -4086,6 +4179,11 @@ "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.3.tgz", "integrity": "sha512-SYU4z1dL0PyIMd4Vj8YOqFvHu7Hz/enbWtpfnVbJHU4Nd1YNYx8u0ennumc6h48GQNeOLxmwySmnADouT/AuZA==" }, + "vscode-uri": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", + "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==" + }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/packages/language-server/package.json b/packages/language-server/package.json index e9eba6e675..094b58c007 100644 --- a/packages/language-server/package.json +++ b/packages/language-server/package.json @@ -30,13 +30,15 @@ "main": "./dist/index.js", "typings": "dist/src/index", "dependencies": { - "@prisma/prisma-schema-wasm": "5.15.0-3.97f638f5e0f371a1a553cf726f9d597bbe811bff", + "@prisma/prisma-schema-wasm": "5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48", + "@prisma/schema-files-loader": "5.14.0-dev.77", "@types/js-levenshtein": "1.1.3", "js-levenshtein": "1.1.6", "klona": "2.0.6", "nyc": "15.1.0", "vscode-languageserver": "8.1.0", - "vscode-languageserver-textdocument": "1.0.11" + "vscode-languageserver-textdocument": "1.0.11", + "vscode-uri": "^3.0.8" }, "devDependencies": { "@types/mocha": "10.0.6", @@ -60,4 +62,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/packages/language-server/src/__test__/artificial-panic.test.ts b/packages/language-server/src/__test__/artificial-panic.test.ts index ad3890bd3d..486c2588ad 100644 --- a/packages/language-server/src/__test__/artificial-panic.test.ts +++ b/packages/language-server/src/__test__/artificial-panic.test.ts @@ -9,6 +9,7 @@ import { import { CURSOR_CHARACTER, findCursorPosition, getTextDocument } from './helper' import * as assert from 'assert' +import { PrismaSchema } from '../lib/Schema' suite('Artificial Panics', () => { const OLD_ENV = { ...process.env } @@ -49,7 +50,7 @@ suite('Artificial Panics', () => { } try { - const _codeActions = handleCodeActions(params, document, onError) + const _codeActions = handleCodeActions(PrismaSchema.singleFile(document), document, params, onError) assert.fail("This shouldn't happen!") } catch (e) { @@ -83,7 +84,7 @@ suite('Artificial Panics', () => { } try { - const _formatResult = handleDocumentFormatting(params, document, onError) + const _formatResult = handleDocumentFormatting(PrismaSchema.singleFile(document), document, params, onError) assert.fail("This shouldn't happen!") } catch (e) { @@ -109,7 +110,7 @@ suite('Artificial Panics', () => { } try { - const _diagnostics = handleDiagnosticsRequest(document, onError) + const _diagnostics = handleDiagnosticsRequest(PrismaSchema.singleFile(document), onError) assert.fail("This shouldn't happen!") } catch (e) { @@ -151,7 +152,7 @@ suite('Artificial Panics', () => { } try { - const _completions = handleCompletionRequest(params, document, onError) + const _completions = handleCompletionRequest(PrismaSchema.singleFile(document), document, params, onError) assert.fail("This shouldn't happen!") } catch (e) { @@ -193,7 +194,7 @@ suite('Artificial Panics', () => { } try { - const _completions = handleCompletionRequest(params, document, onError) + const _completions = handleCompletionRequest(PrismaSchema.singleFile(document), document, params, onError) assert.fail("This shouldn't happen!") } catch (e) { @@ -224,7 +225,7 @@ suite('Artificial Panics', () => { } try { - const _completions = handleCompletionRequest(params, document, onError) + const _completions = handleCompletionRequest(PrismaSchema.singleFile(document), document, params, onError) assert.fail("This shouldn't happen!") } catch (e) { diff --git a/packages/language-server/src/__test__/completion.test.ts b/packages/language-server/src/__test__/completion.test.ts index 810c871cc8..2d33f5f2d5 100644 --- a/packages/language-server/src/__test__/completion.test.ts +++ b/packages/language-server/src/__test__/completion.test.ts @@ -4,8 +4,7 @@ import { CompletionList, CompletionParams, CompletionItemKind, CompletionTrigger import assert from 'assert' import dedent from 'ts-dedent' import { CURSOR_CHARACTER, findCursorPosition } from './helper' - -/* eslint-disable @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-misused-promises */ +import { PrismaSchema } from '../lib/Schema' type DatasourceProvider = 'sqlite' | 'postgresql' | 'mysql' | 'mongodb' | 'sqlserver' | 'cockroachdb' @@ -57,7 +56,7 @@ function assertCompletion({ const position = findCursorPosition(schema) const document: TextDocument = TextDocument.create( - 'completions/none.prisma', + 'file:///completions/none.prisma', 'prisma', 1, schema.replace(CURSOR_CHARACTER, ''), @@ -71,7 +70,11 @@ function assertCompletion({ }, } - const completionResult: CompletionList | undefined = handleCompletionRequest(completionParams, document) + const completionResult: CompletionList | undefined = handleCompletionRequest( + PrismaSchema.singleFile(document), + document, + completionParams, + ) assert.ok(completionResult !== undefined) diff --git a/packages/language-server/src/__test__/format.test.ts b/packages/language-server/src/__test__/format.test.ts index 27fad0da18..5dda150c5c 100644 --- a/packages/language-server/src/__test__/format.test.ts +++ b/packages/language-server/src/__test__/format.test.ts @@ -2,6 +2,7 @@ import { handleDocumentFormatting } from '../lib/MessageHandler' import { TextEdit, DocumentFormattingParams } from 'vscode-languageserver' import * as assert from 'assert' import { getTextDocument } from './helper' +import { PrismaSchema } from '../lib/Schema' function assertFormat(fixturePath: string): void { const textDocument = getTextDocument(fixturePath) @@ -13,7 +14,7 @@ function assertFormat(fixturePath: string): void { }, } - const formatResult: TextEdit[] = handleDocumentFormatting(params, textDocument) + const formatResult: TextEdit[] = handleDocumentFormatting(PrismaSchema.singleFile(textDocument), textDocument, params) assert.ok(formatResult.length !== 0) } diff --git a/packages/language-server/src/__test__/helper.ts b/packages/language-server/src/__test__/helper.ts index f0971c7255..720ac6fd92 100644 --- a/packages/language-server/src/__test__/helper.ts +++ b/packages/language-server/src/__test__/helper.ts @@ -1,12 +1,27 @@ import * as fs from 'fs' import { Position, TextDocument } from 'vscode-languageserver-textdocument' import path from 'path' +import { URI } from 'vscode-uri' export const CURSOR_CHARACTER = '|' +const fixturesDir = path.resolve(__dirname, '../../../test/fixtures') + export function getTextDocument(testFilePath: string): TextDocument { - const content: string = fs.readFileSync(path.join(__dirname, '../../../test/fixtures', testFilePath), 'utf8') - return TextDocument.create(testFilePath, 'prisma', 1, content) + const absPath = path.join(fixturesDir, testFilePath) + const content: string = fs.readFileSync(absPath, 'utf8') + + return TextDocument.create(fixturePathToUri(testFilePath), 'prisma', 1, content) +} + +export function fixturePathToUri(fixturePath: string) { + const absPath = path.join(fixturesDir, fixturePath) + + // that would normalize testFilePath and resolve all of + // the . and .. + const relPath = path.relative(fixturesDir, absPath) + // this will normalize slashes on win/linux + return URI.file(`/${relPath}`).toString() } export const findCursorPosition = (input: string): Position => { diff --git a/packages/language-server/src/__test__/hover.test.ts b/packages/language-server/src/__test__/hover.test.ts index df90c10aec..a77dbc4bb1 100644 --- a/packages/language-server/src/__test__/hover.test.ts +++ b/packages/language-server/src/__test__/hover.test.ts @@ -3,6 +3,7 @@ import { handleHoverRequest } from '../lib/MessageHandler' import { Hover } from 'vscode-languageserver' import * as assert from 'assert' import { getTextDocument } from './helper' +import { PrismaSchema } from '../lib/Schema' function assertHover(position: Position, expected: Hover, fixturePath: string): void { const textDocument = getTextDocument(fixturePath) @@ -11,7 +12,7 @@ function assertHover(position: Position, expected: Hover, fixturePath: string): textDocument, position: position, } - const hoverResult: Hover | undefined = handleHoverRequest(textDocument, params) + const hoverResult: Hover | undefined = handleHoverRequest(PrismaSchema.singleFile(textDocument), textDocument, params) assert.ok(hoverResult !== undefined) assert.deepStrictEqual(hoverResult.contents, expected.contents) diff --git a/packages/language-server/src/__test__/jumpToDefinition.test.ts b/packages/language-server/src/__test__/jumpToDefinition.test.ts index e9473c84c5..bf431a1338 100644 --- a/packages/language-server/src/__test__/jumpToDefinition.test.ts +++ b/packages/language-server/src/__test__/jumpToDefinition.test.ts @@ -3,12 +3,17 @@ import { handleDefinitionRequest } from '../lib/MessageHandler' import { LocationLink, Range } from 'vscode-languageserver' import * as assert from 'assert' import { getTextDocument } from './helper' +import { PrismaSchema } from '../lib/Schema' function assertJumpToDefinition(position: Position, expectedRange: Range, fixturePath: string): void { const textDocument = getTextDocument(fixturePath) const params = { textDocument, position } - const defResult: LocationLink[] | undefined = handleDefinitionRequest(textDocument, params) + const defResult: LocationLink[] | undefined = handleDefinitionRequest( + PrismaSchema.singleFile(textDocument), + textDocument, + params, + ) assert.ok(defResult !== undefined) assert.deepStrictEqual(defResult[0].targetRange, expectedRange) diff --git a/packages/language-server/src/__test__/linting.test.ts b/packages/language-server/src/__test__/linting.test.ts index 36afd4108b..5b24f8af36 100644 --- a/packages/language-server/src/__test__/linting.test.ts +++ b/packages/language-server/src/__test__/linting.test.ts @@ -4,11 +4,12 @@ import * as assert from 'assert' import { getTextDocument } from './helper' import { MAX_SAFE_VALUE_i32 } from '../lib/constants' import listAllAvailablePreviewFeatures from '../lib/prisma-schema-wasm/listAllAvailablePreviewFeatures' +import { PrismaSchema } from '../lib/Schema' function assertLinting(expected: Diagnostic[], fixturePath: string): void { const document = getTextDocument(fixturePath) - const diagnosticsResults: Diagnostic[] = handleDiagnosticsRequest(document) + const diagnosticsResults: Diagnostic[] = handleDiagnosticsRequest(PrismaSchema.singleFile(document)).get(document.uri) assert.ok(diagnosticsResults.length != 0) expected.forEach((expectedDiagnostic, i) => { diff --git a/packages/language-server/src/__test__/quickFix.test.ts b/packages/language-server/src/__test__/quickFix.test.ts index faf15151fd..ef4a243ad8 100644 --- a/packages/language-server/src/__test__/quickFix.test.ts +++ b/packages/language-server/src/__test__/quickFix.test.ts @@ -8,15 +8,15 @@ import { Diagnostic, } from 'vscode-languageserver' import * as assert from 'assert' -import { getTextDocument } from './helper' +import { fixturePathToUri, getTextDocument } from './helper' +import { PrismaSchema } from '../lib/Schema' function assertQuickFix(expected: CodeAction[], fixturePath: string, range: Range, diagnostics: Diagnostic[]): void { const textDocument = getTextDocument(fixturePath) const params: CodeActionParams = { textDocument: { - // prisma-schema-wasm expects a URI starting with file:///, if not it will return nothing ([]) - uri: `file:///${textDocument.uri.substring(2)}`, + uri: textDocument.uri, }, context: { diagnostics, @@ -24,7 +24,7 @@ function assertQuickFix(expected: CodeAction[], fixturePath: string, range: Rang range, } - const quickFixResult: CodeAction[] = quickFix(textDocument, params) + const quickFixResult: CodeAction[] = quickFix(PrismaSchema.singleFile(textDocument), textDocument, params) assert.ok(quickFixResult.length !== 0, "Expected a quick fix, but didn't get one") assert.deepStrictEqual(quickFixResult, expected) @@ -42,7 +42,7 @@ function createDiagnosticErrorUnknownType(unknownType: string, range: Range): Di suite('Quick Fixes', () => { suite('from TS', () => { const fixturePath = './codeActions/quickFixes.prisma' - const expectedPath = `file:///${fixturePath.substring(2)}` + const expectedPath = fixturePathToUri(fixturePath) const rangeNewModel: Range = { start: { line: 16, character: 9 }, @@ -200,7 +200,7 @@ suite('Quick Fixes', () => { suite('from prisma-schema-wasm', () => { const fixturePath = './codeActions/one_to_many_referenced_side_misses_unique_single_field.prisma' - const expectedPath = `file:///${fixturePath.substring(2)}` + const expectedPath = fixturePathToUri(fixturePath) test('@relation referenced side missing @unique', () => { const diagnostics: Diagnostic[] = [ diff --git a/packages/language-server/src/__test__/rename.test.ts b/packages/language-server/src/__test__/rename.test.ts index 36b1b175fb..cf0626b324 100644 --- a/packages/language-server/src/__test__/rename.test.ts +++ b/packages/language-server/src/__test__/rename.test.ts @@ -4,6 +4,7 @@ import { WorkspaceEdit, RenameParams, Position } from 'vscode-languageserver' import * as assert from 'assert' import { getTextDocument } from './helper' import { MAX_SAFE_VALUE_i32 } from '../lib/constants' +import { PrismaSchema } from '../lib/Schema' function assertRename(expected: WorkspaceEdit, document: TextDocument, newName: string, position: Position): void { const params: RenameParams = { @@ -12,7 +13,11 @@ function assertRename(expected: WorkspaceEdit, document: TextDocument, newName: position: position, } - const renameResult: WorkspaceEdit | undefined = handleRenameRequest(params, document) + const renameResult: WorkspaceEdit | undefined = handleRenameRequest( + PrismaSchema.singleFile(document), + document, + params, + ) assert.notStrictEqual(renameResult, undefined) assert.deepStrictEqual(renameResult, expected) diff --git a/packages/language-server/src/lib/DiagnosticMap.ts b/packages/language-server/src/lib/DiagnosticMap.ts new file mode 100644 index 0000000000..01defa2dbb --- /dev/null +++ b/packages/language-server/src/lib/DiagnosticMap.ts @@ -0,0 +1,25 @@ +import { Diagnostic } from 'vscode-languageserver' + +export class DiagnosticMap { + private _map = new Map() + + constructor(uris: string[]) { + for (const uri of uris) { + this._map.set(uri, []) + } + } + + add(fileUri: string, diagnostic: Diagnostic) { + const entry = this._map.get(fileUri) ?? [] + this._map.set(fileUri, entry) + entry.push(diagnostic) + } + + get(fileUri: string): Diagnostic[] { + return this._map.get(fileUri) ?? [] + } + + entries(): IterableIterator<[string, Diagnostic[]]> { + return this._map.entries() + } +} diff --git a/packages/language-server/src/lib/MessageHandler.ts b/packages/language-server/src/lib/MessageHandler.ts index 484bf8aa31..c751984f2d 100644 --- a/packages/language-server/src/lib/MessageHandler.ts +++ b/packages/language-server/src/lib/MessageHandler.ts @@ -40,7 +40,7 @@ import { EditsMap, mergeEditMaps, } from './code-actions/rename' -import { validateExperimentalFeatures, validateIgnoredBlocks } from './validations' +import { validateIgnoredBlocks } from './validations' import { fullDocumentRange, getWordAtPosition, @@ -52,19 +52,19 @@ import { } from './ast' import { prismaSchemaWasmCompletions, localCompletions } from './completions' import { PrismaSchema, SchemaDocument } from './Schema' +import { DiagnosticMap } from './DiagnosticMap' export function handleDiagnosticsRequest( - document: TextDocument, + schema: PrismaSchema, onError?: (errorMessage: string) => void, -): Diagnostic[] { - const text = document.getText(fullDocumentRange(document)) - const res = lint(text, (errorMessage: string) => { +): DiagnosticMap { + const res = lint(schema, (errorMessage: string) => { if (onError) { onError(errorMessage) } }) - const diagnostics: Diagnostic[] = [] + const diagnostics = new DiagnosticMap(schema.documents.map((doc) => doc.uri)) if ( res.some( (diagnostic) => @@ -81,6 +81,11 @@ export function handleDiagnosticsRequest( for (const diag of res) { const previewNotKnownRegex = /The preview feature \"[a-zA-Z]+\" is not known/ + const uri = diag.file_name + const document = schema.findDocByUri(uri) + if (!document) { + continue + } const diagnostic: Diagnostic = { range: { start: document.positionAt(diag.start), @@ -97,12 +102,9 @@ export function handleDiagnosticsRequest( } else { diagnostic.severity = DiagnosticSeverity.Error } - diagnostics.push(diagnostic) + diagnostics.add(uri, diagnostic) } - validateExperimentalFeatures(document, diagnostics) - - const schema = PrismaSchema.singleFile(document) validateIgnoredBlocks(schema, diagnostics) return diagnostics @@ -111,11 +113,14 @@ export function handleDiagnosticsRequest( /** * @todo Use official schema.prisma parser. This is a workaround! */ -export function handleDefinitionRequest(document: TextDocument, params: DeclarationParams): LocationLink[] | undefined { +export function handleDefinitionRequest( + schema: PrismaSchema, + initiatingDocument: TextDocument, + params: DeclarationParams, +): LocationLink[] | undefined { const position = params.position - const schema = PrismaSchema.singleFile(document) - const word = getWordAtPosition(document, position) + const word = getWordAtPosition(initiatingDocument, position) if (word === '') { return @@ -169,19 +174,23 @@ export function handleDefinitionRequest(document: TextDocument, params: Declarat * This handler provides the modification to the document to be formatted. */ export function handleDocumentFormatting( + schema: PrismaSchema, + initiatingDocument: TextDocument, params: DocumentFormattingParams, - document: TextDocument, onError?: (errorMessage: string) => void, ): TextEdit[] { - const formatted = format(document.getText(), params, onError) - return [TextEdit.replace(fullDocumentRange(document), formatted)] + const formatted = format(schema, initiatingDocument, params, onError) + return [TextEdit.replace(fullDocumentRange(initiatingDocument), formatted)] } -export function handleHoverRequest(document: TextDocument, params: HoverParams): Hover | undefined { +export function handleHoverRequest( + schema: PrismaSchema, + initiatingDocument: TextDocument, + params: HoverParams, +): Hover | undefined { const position = params.position - const schema = PrismaSchema.singleFile(document) - const word = getWordAtPosition(document, position) + const word = getWordAtPosition(initiatingDocument, position) if (word === '') { return @@ -192,22 +201,13 @@ export function handleHoverRequest(document: TextDocument, params: HoverParams): return } - const blockDocumentation = getDocumentationForBlock(document, block) + const blockDocumentation = getDocumentationForBlock(block) if (blockDocumentation.length !== 0) { return { contents: blockDocumentation.join('\n\n'), } } - - // TODO uncomment once https://github.com/prisma/prisma/issues/2546 is resolved! - /*if (docComments.startsWith('//')) { - return { - contents: docComments.slice(3).trim(), - } - } */ - - return } /** @@ -215,31 +215,35 @@ export function handleHoverRequest(document: TextDocument, params: HoverParams): * This handler provides the initial list of the completion items. */ export function handleCompletionRequest( - params: CompletionParams, + schema: PrismaSchema, document: TextDocument, + params: CompletionParams, onError?: (errorMessage: string) => void, ): CompletionList | undefined { - return prismaSchemaWasmCompletions(params, document, onError) || localCompletions(params, document, onError) + return prismaSchemaWasmCompletions(schema, params, onError) || localCompletions(schema, document, params, onError) } -export function handleRenameRequest(params: RenameParams, document: TextDocument): WorkspaceEdit | undefined { - const schema = PrismaSchema.singleFile(document) +export function handleRenameRequest( + schema: PrismaSchema, + initiatingDocument: TextDocument, + params: RenameParams, +): WorkspaceEdit | undefined { const schemaLines = schema.linesAsArray() const position = params.position - const block = getBlockAtPosition(document.uri, position.line, schema) + const block = getBlockAtPosition(initiatingDocument.uri, position.line, schema) if (!block) { return undefined } const currentLine = block.definingDocument.lines[params.position.line].text - const isDatamodelBlockRename = isDatamodelBlockName(position, block, schema, document) + const isDatamodelBlockRename = isDatamodelBlockName(position, block, schema, initiatingDocument) const isMappable = ['model', 'enum', 'view'].includes(block.type) const needsMap = !isDatamodelBlockRename ? true : isMappable - const isEnumValueRename: boolean = isEnumValue(currentLine, params.position, block, document) - const isValidFieldRename: boolean = isValidFieldName(currentLine, params.position, block, document) + const isEnumValueRename: boolean = isEnumValue(currentLine, params.position, block, initiatingDocument) + const isValidFieldRename: boolean = isValidFieldName(currentLine, params.position, block, initiatingDocument) const isRelationFieldRename: boolean = isValidFieldRename && isRelationField(currentLine, schema) if (isDatamodelBlockRename || isEnumValueRename || isValidFieldRename) { @@ -249,7 +253,7 @@ export function handleRenameRequest(params: RenameParams, document: TextDocument isDatamodelBlockRename, isEnumValueRename, isValidFieldRename, - document, + initiatingDocument, params.position, ) @@ -274,7 +278,7 @@ export function handleRenameRequest(params: RenameParams, document: TextDocument } // rename marked string - edits.push(insertBasicRename(params.newName, currentName, document, lineNumberOfDefinition)) + edits.push(insertBasicRename(params.newName, currentName, initiatingDocument, lineNumberOfDefinition)) // check if map exists already if ( @@ -323,15 +327,16 @@ export function handleCompletionResolveRequest(item: CompletionItem): Completion } export function handleCodeActions( + schema: PrismaSchema, + initiatingDocument: TextDocument, params: CodeActionParams, - document: TextDocument, onError?: (errorMessage: string) => void, ): CodeAction[] { if (!params.context.diagnostics.length) { return [] } - return quickFix(document, params, onError) + return quickFix(schema, initiatingDocument, params, onError) } export function handleDocumentSymbol(params: DocumentSymbolParams, document: TextDocument): DocumentSymbol[] { diff --git a/packages/language-server/src/lib/Schema.ts b/packages/language-server/src/lib/Schema.ts index 8f19e374a4..ffd633e221 100644 --- a/packages/language-server/src/lib/Schema.ts +++ b/packages/language-server/src/lib/Schema.ts @@ -1,4 +1,14 @@ +import { + loadRelatedSchemaFiles, + InMemoryFilesResolver, + realFsResolver, + CompositeFilesResolver, + FilesResolver, + CaseSensitivityOptions, +} from '@prisma/schema-files-loader' +import { Position, TextDocuments } from 'vscode-languageserver' import { TextDocument } from 'vscode-languageserver-textdocument' +import { URI } from 'vscode-uri' export type Line = { readonly document: SchemaDocument @@ -9,20 +19,28 @@ export type Line = { export class SchemaDocument { readonly lines: Line[] - static fromTextDocument(textDocument: TextDocument): SchemaDocument { - return new SchemaDocument(textDocument.uri, textDocument.getText()) + constructor(private textDocument: TextDocument) { + this.lines = textDocument + .getText() + .split(/\r?\n/) + .map((untrimmedText, lineIndex) => ({ + document: this, + lineIndex, + untrimmedText, + text: untrimmedText.trim(), + })) } - constructor( - readonly uri: string, - readonly content: string, - ) { - this.lines = content.split(/\r?\n/).map((untrimmedText, lineIndex) => ({ - document: this, - lineIndex, - untrimmedText, - text: untrimmedText.trim(), - })) + get uri(): string { + return this.textDocument.uri + } + + get content(): string { + return this.textDocument.getText() + } + + positionAt(offset: number): Position { + return this.textDocument.positionAt(offset) } getLineContent(lineIndex: number): string { @@ -37,10 +55,21 @@ type FindRegexpResult = { export class PrismaSchema { static singleFile(textDocument: TextDocument) { - return new PrismaSchema([SchemaDocument.fromTextDocument(textDocument)]) + return new PrismaSchema([new SchemaDocument(textDocument)]) } - constructor(private readonly documents: SchemaDocument[]) {} + static async load(currentDocument: TextDocument, allDocuments: TextDocuments): Promise { + const schemaFiles = await loadRelatedSchemaFiles( + URI.parse(currentDocument.uri).fsPath, + createFilesResolver(allDocuments), + ) + const documents = schemaFiles.map(([filePath, content]) => { + return new SchemaDocument(TextDocument.create(URI.file(filePath).toString(), 'prisma', 1, content)) + }) + return new PrismaSchema(documents) + } + + constructor(readonly documents: SchemaDocument[]) {} *iterLines(): Generator { for (const doc of this.documents) { @@ -68,4 +97,44 @@ export class PrismaSchema { } return undefined } + + /** + * + * @returns array of (uri, content) tuples. Expected input for prisma-schema-wasm + */ + toTuples(): Array<[string, string]> { + return this.documents.map((doc) => [doc.uri, doc.content]) + } + + toJSON() { + return this.toTuples() + } +} + +function createFilesResolver(allDocuments: TextDocuments): FilesResolver { + const options = { + // Technically, macos and Windows can use case-sensitive file systems + // too, however, VSCode does not support this at the moment, so there is + // no meaningful way for us to support them in extension + // See: + // - https://github.com/microsoft/vscode/issues/123660 + // - https://github.com/microsoft/vscode/issues/94307 + // - https://github.com/microsoft/vscode/blob/c06c555b481aaac4afd51d6fc7691d7658949651/src/vs/platform/files/node/diskFileSystemProvider.ts#L81 + caseSensitive: process.platform === 'linux', + } + return new CompositeFilesResolver(createInMemoryResolver(allDocuments, options), realFsResolver, options) +} + +function createInMemoryResolver( + allDocuments: TextDocuments, + options: CaseSensitivityOptions, +): InMemoryFilesResolver { + const resolver = new InMemoryFilesResolver(options) + for (const doc of allDocuments.all()) { + const filePath = URI.parse(doc.uri).fsPath + const content = doc.getText() + resolver.addFile(filePath, content) + } + + return resolver } diff --git a/packages/language-server/src/lib/ast/block.ts b/packages/language-server/src/lib/ast/block.ts index 1579454b73..fe5ab3fb6f 100644 --- a/packages/language-server/src/lib/ast/block.ts +++ b/packages/language-server/src/lib/ast/block.ts @@ -1,8 +1,6 @@ import { Position, Range } from 'vscode-languageserver' -import { TextDocument } from 'vscode-languageserver-textdocument' import { BlockType } from '../types' -import { MAX_SAFE_VALUE_i32 } from '../constants' import { getFieldType } from './fields' import { getBlockAtPosition } from './findAtPosition' @@ -215,15 +213,12 @@ function getFieldNameFromLine(line: string) { return firstPartOfLine } -export const getDocumentationForBlock = (document: TextDocument, block: Block): string[] => { - return getDocumentation(document, block.range.start.line, []) +export const getDocumentationForBlock = (block: Block): string[] => { + return getDocumentation(block.definingDocument, block.range.start.line, []) } -const getDocumentation = (document: TextDocument, line: number, comments: string[]): string[] => { - const comment = document.getText({ - start: { line: line - 1, character: 0 }, - end: { line: line - 1, character: MAX_SAFE_VALUE_i32 }, - }) +const getDocumentation = (document: SchemaDocument, line: number, comments: string[]): string[] => { + const comment = document.lines[line - 1]?.untrimmedText ?? '' if (comment.startsWith('///')) { comments.unshift(comment.slice(4).trim()) diff --git a/packages/language-server/src/lib/ast/configBlock.ts b/packages/language-server/src/lib/ast/configBlock.ts index ff76da30a4..a8adafcd56 100644 --- a/packages/language-server/src/lib/ast/configBlock.ts +++ b/packages/language-server/src/lib/ast/configBlock.ts @@ -57,29 +57,3 @@ export function getAllPreviewFeaturesFromGenerators(schema: PrismaSchema): Previ return undefined } - -// TODO (Joël) can be removed? Since it was renamed to `previewFeatures` a long time ago -export function getExperimentalFeaturesRange(document: TextDocument): Range | undefined { - const lines = convertDocumentTextToTrimmedLineArray(document) - const experimentalFeatures = 'experimentalFeatures' - let reachedStartLine = false - for (const [key, item] of lines.entries()) { - if (item.startsWith('generator') && item.includes('{')) { - reachedStartLine = true - } - if (!reachedStartLine) { - continue - } - if (reachedStartLine && item.startsWith('}')) { - return - } - - if (item.startsWith(experimentalFeatures)) { - const startIndex = getCurrentLine(document, key).indexOf(experimentalFeatures) - return { - start: { line: key, character: startIndex }, - end: { line: key, character: startIndex + experimentalFeatures.length }, - } - } - } -} diff --git a/packages/language-server/src/lib/code-actions/index.ts b/packages/language-server/src/lib/code-actions/index.ts index 9572a8f3c2..75721406df 100644 --- a/packages/language-server/src/lib/code-actions/index.ts +++ b/packages/language-server/src/lib/code-actions/index.ts @@ -85,18 +85,18 @@ function addTypeModifiers(hasTypeModifierArray: boolean, hasTypeModifierOptional } export function quickFix( - textDocument: TextDocument, + schema: PrismaSchema, + initiatingDocument: TextDocument, params: CodeActionParams, onError?: (errorMessage: string) => void, ): CodeAction[] { - const schema = PrismaSchema.singleFile(textDocument) const diagnostics: Diagnostic[] = params.context.diagnostics if (!diagnostics || diagnostics.length === 0) { return [] } - const codeActionList = codeActions(textDocument.getText(), JSON.stringify(params), (errorMessage: string) => { + const codeActionList = codeActions(JSON.stringify(schema), JSON.stringify(params), (errorMessage: string) => { if (onError) { onError(errorMessage) } @@ -112,7 +112,7 @@ export function quickFix( // See https://github.com/prisma/prisma-engines/pull/4813 diag.message.includes('is neither a built-in type, nor refers to another model,') ) { - let diagText = textDocument.getText(diag.range) + let diagText = initiatingDocument.getText(diag.range) const hasTypeModifierArray: boolean = diagText.endsWith('[]') const hasTypeModifierOptional: boolean = diagText.endsWith('?') diagText = removeTypeModifiers(hasTypeModifierArray, hasTypeModifierOptional, diagText) @@ -142,7 +142,7 @@ export function quickFix( changes: { [params.textDocument.uri]: [ { - range: getInsertRange(textDocument), + range: getInsertRange(initiatingDocument), newText: `\nmodel ${diagText} {\n\n}\n`, }, ], @@ -157,7 +157,7 @@ export function quickFix( changes: { [params.textDocument.uri]: [ { - range: getInsertRange(textDocument), + range: getInsertRange(initiatingDocument), newText: `\nenum ${diagText} {\n\n}\n`, }, ], @@ -184,7 +184,7 @@ export function quickFix( diag.severity === DiagnosticSeverity.Error && diag.message.includes('It does not start with any known Prisma schema keyword.') ) { - const diagText = textDocument.getText(diag.range).split(/\s/) + const diagText = initiatingDocument.getText(diag.range).split(/\s/) if (diagText.length !== 0) { const spellingSuggestion = getSpellingSuggestions(diagText[0], ['model', 'enum', 'datasource', 'generator']) if (spellingSuggestion) { diff --git a/packages/language-server/src/lib/completions/index.ts b/packages/language-server/src/lib/completions/index.ts index f15ecadbda..531660bdd8 100644 --- a/packages/language-server/src/lib/completions/index.ts +++ b/packages/language-server/src/lib/completions/index.ts @@ -11,7 +11,6 @@ import type { TextDocument } from 'vscode-languageserver-textdocument' import textDocumentCompletion from '../prisma-schema-wasm/textDocumentCompletion' import { - fullDocumentRange, getCurrentLine, getSymbolBeforePosition, getBlockAtPosition, @@ -588,13 +587,11 @@ function getSuggestionForFirstInsideBlock( } export function prismaSchemaWasmCompletions( + schema: PrismaSchema, params: CompletionParams, - document: TextDocument, onError?: (errorMessage: string) => void, ): CompletionList | undefined { - const text = document.getText(fullDocumentRange(document)) - - const completionList = textDocumentCompletion(text, params, (errorMessage: string) => { + const completionList = textDocumentCompletion(schema, params, (errorMessage: string) => { if (onError) { onError(errorMessage) } @@ -608,26 +605,26 @@ export function prismaSchemaWasmCompletions( } export function localCompletions( + schema: PrismaSchema, + initiatingDocument: TextDocument, params: CompletionParams, - document: TextDocument, onError?: (errorMessage: string) => void, ): CompletionList | undefined { const context = params.context const position = params.position - const schema = PrismaSchema.singleFile(document) - const currentLineUntrimmed = getCurrentLine(document, position.line) + const currentLineUntrimmed = getCurrentLine(initiatingDocument, position.line) const currentLineTillPosition = currentLineUntrimmed.slice(0, position.character - 1).trim() const wordsBeforePosition: string[] = currentLineTillPosition.split(/\s+/) - const symbolBeforePosition = getSymbolBeforePosition(document, position) + const symbolBeforePosition = getSymbolBeforePosition(initiatingDocument, position) const symbolBeforePositionIsWhiteSpace = symbolBeforePosition.search(/\s/) !== -1 const positionIsAfterArray: boolean = wordsBeforePosition.length >= 3 && !currentLineTillPosition.includes('[') && symbolBeforePositionIsWhiteSpace // datasource, generator, model, type or enum - const foundBlock = getBlockAtPosition(document.uri, position.line, schema) + const foundBlock = getBlockAtPosition(initiatingDocument.uri, position.line, schema) if (!foundBlock) { if (wordsBeforePosition.length > 1 || (wordsBeforePosition.length === 1 && symbolBeforePositionIsWhiteSpace)) { return @@ -644,15 +641,15 @@ export function localCompletions( if (context?.triggerKind === CompletionTriggerKind.TriggerCharacter) { switch (context.triggerCharacter as '@' | '"' | '.') { case '@': - if (!positionIsAfterFieldAndType(position, document, wordsBeforePosition)) { + if (!positionIsAfterFieldAndType(position, initiatingDocument, wordsBeforePosition)) { return } return getSuggestionForFieldAttribute( foundBlock, - getCurrentLine(document, position.line), + getCurrentLine(initiatingDocument, position.line), schema, wordsBeforePosition, - document, + initiatingDocument, onError, ) case '"': @@ -670,7 +667,7 @@ export function localCompletions( if (['model', 'view'].includes(foundBlock.type) && isInsideAttribute(currentLineUntrimmed, position, '()')) { return getSuggestionsForInsideRoundBrackets(currentLineUntrimmed, schema, position, foundBlock) } else { - return getSuggestionForNativeTypes(foundBlock, schema, wordsBeforePosition, document, onError) + return getSuggestionForNativeTypes(foundBlock, schema, wordsBeforePosition, initiatingDocument, onError) } } } @@ -684,7 +681,7 @@ export function localCompletions( return getSuggestionsForInsideRoundBrackets(currentLineUntrimmed, schema, position, foundBlock) } // check if field type - if (!positionIsAfterFieldAndType(position, document, wordsBeforePosition)) { + if (!positionIsAfterFieldAndType(position, initiatingDocument, wordsBeforePosition)) { return getSuggestionsForFieldTypes(schema, position, currentLineUntrimmed) } return getSuggestionForFieldAttribute( @@ -692,7 +689,7 @@ export function localCompletions( foundBlock.definingDocument.getLineContent(position.line), schema, wordsBeforePosition, - document, + initiatingDocument, onError, ) case 'datasource': diff --git a/packages/language-server/src/lib/prisma-schema-wasm/format.ts b/packages/language-server/src/lib/prisma-schema-wasm/format.ts index 25f3a7e14f..3566a1b7b5 100644 --- a/packages/language-server/src/lib/prisma-schema-wasm/format.ts +++ b/packages/language-server/src/lib/prisma-schema-wasm/format.ts @@ -1,9 +1,12 @@ import { DocumentFormattingParams } from 'vscode-languageserver' import { prismaSchemaWasm } from '.' import { handleFormatPanic, handleWasmError } from './internals' +import { PrismaSchema } from '../Schema' +import { TextDocument } from 'vscode-languageserver-textdocument' export default function format( - schema: string, + schema: PrismaSchema, + initiatingDocument: TextDocument, options: DocumentFormattingParams, onError?: (errorMessage: string) => void, ): string { @@ -17,7 +20,14 @@ export default function format( }) } - return prismaSchemaWasm.format(JSON.stringify(schema), JSON.stringify(options)) + const result = prismaSchemaWasm.format(JSON.stringify(schema), JSON.stringify(options)) + // tuples of [filePath, content] + const formattedFiles = JSON.parse(result) as Array<[string, string]> + const formatResult = formattedFiles.find(([uri]) => uri === initiatingDocument.uri) + if (!formatResult) { + return initiatingDocument.getText() + } + return formatResult[1] } catch (e) { const err = e as Error @@ -27,6 +37,6 @@ export default function format( handleWasmError(err, 'format', onError) - return schema + return initiatingDocument.getText() } } diff --git a/packages/language-server/src/lib/prisma-schema-wasm/lint.ts b/packages/language-server/src/lib/prisma-schema-wasm/lint.ts index d8555001c7..93377fd2ed 100644 --- a/packages/language-server/src/lib/prisma-schema-wasm/lint.ts +++ b/packages/language-server/src/lib/prisma-schema-wasm/lint.ts @@ -1,14 +1,16 @@ import { prismaSchemaWasm } from '.' +import { PrismaSchema } from '../Schema' import { handleFormatPanic, handleWasmError } from './internals' export interface LinterError { + file_name: string start: number end: number text: string is_warning: boolean } -export default function lint(text: string, onError?: (errorMessage: string) => void): LinterError[] { +export default function lint(schema: PrismaSchema, onError?: (errorMessage: string) => void): LinterError[] { console.log('running lint() from prisma-schema-wasm') try { if (process.env.FORCE_PANIC_PRISMA_SCHEMA) { @@ -18,7 +20,7 @@ export default function lint(text: string, onError?: (errorMessage: string) => v }) } - const result = prismaSchemaWasm.lint(JSON.stringify(text)) + const result = prismaSchemaWasm.lint(JSON.stringify(schema)) return JSON.parse(result) as LinterError[] } catch (e) { diff --git a/packages/language-server/src/lib/prisma-schema-wasm/textDocumentCompletion.ts b/packages/language-server/src/lib/prisma-schema-wasm/textDocumentCompletion.ts index 9bb1a8fd01..f408a8e642 100644 --- a/packages/language-server/src/lib/prisma-schema-wasm/textDocumentCompletion.ts +++ b/packages/language-server/src/lib/prisma-schema-wasm/textDocumentCompletion.ts @@ -1,13 +1,14 @@ import { CompletionParams, CompletionList } from 'vscode-languageserver' import { prismaSchemaWasm } from '.' import { handleFormatPanic, handleWasmError } from './internals' +import { PrismaSchema } from '../Schema' /* eslint-disable @typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-return */ // This can't panic / throw exceptions. Any panic here should be considered a // bug to be fixed. prisma-schema-wasm will return an empty CompletionList on error. export default function textDocumentCompletion( - schema: string, + schema: PrismaSchema, params: CompletionParams, onError?: (errorMessage: string) => void, ): CompletionList { @@ -18,7 +19,7 @@ export default function textDocumentCompletion( // // prisma-schema-wasm expects something spec-compliant, so we enforce this here. const correctParams: any = params - correctParams['textDocument'] = { uri: 'file:/dev/null' } + correctParams['textDocument'] = { uri: params.textDocument.uri } const stringifiedParams = JSON.stringify(correctParams) try { if (process.env.FORCE_PANIC_PRISMA_SCHEMA) { @@ -28,7 +29,7 @@ export default function textDocumentCompletion( }) } - const response = prismaSchemaWasm.text_document_completion(schema, stringifiedParams) + const response = prismaSchemaWasm.text_document_completion(JSON.stringify(schema), stringifiedParams) return JSON.parse(response) } catch (e) { const err = e as Error diff --git a/packages/language-server/src/lib/validations.ts b/packages/language-server/src/lib/validations.ts index 48b620cb2f..9a34d2252c 100644 --- a/packages/language-server/src/lib/validations.ts +++ b/packages/language-server/src/lib/validations.ts @@ -1,35 +1,16 @@ -import { Diagnostic, DiagnosticSeverity, Range, DiagnosticTag } from 'vscode-languageserver' -import { TextDocument } from 'vscode-languageserver-textdocument' +import { DiagnosticSeverity, DiagnosticTag } from 'vscode-languageserver' -import { getBlockAtPosition, getExperimentalFeaturesRange } from './ast' +import { getBlockAtPosition } from './ast' import { MAX_SAFE_VALUE_i32 } from './constants' import { PrismaSchema } from './Schema' +import { DiagnosticMap } from './DiagnosticMap' -// TODO (Joël) can be removed? Since it was renamed to `previewFeatures` -// check for experimentalFeatures inside generator block -// Related code in codeActionProvider.ts, around lines 185-204 -export const validateExperimentalFeatures = (document: TextDocument, diagnostics: Diagnostic[]) => { - if (document.getText().includes('experimentalFeatures')) { - const experimentalFeaturesRange: Range | undefined = getExperimentalFeaturesRange(document) - if (experimentalFeaturesRange) { - diagnostics.push({ - severity: DiagnosticSeverity.Error, - range: experimentalFeaturesRange, - message: - "The `experimentalFeatures` property is obsolete and has been renamed to 'previewFeatures' to better communicate what it is.", - code: 'Prisma 5', - tags: [2], - }) - } - } -} - -export const validateIgnoredBlocks = (schema: PrismaSchema, diagnostics: Diagnostic[]) => { +export const validateIgnoredBlocks = (schema: PrismaSchema, diagnostics: DiagnosticMap) => { schema.linesAsArray().map(({ document, lineIndex, text }) => { if (text.includes('@@ignore')) { const block = getBlockAtPosition(document.uri, lineIndex, schema) if (block) { - diagnostics.push({ + diagnostics.add(document.uri, { range: { start: block.range.start, end: block.range.end }, message: '@@ignore: When using Prisma Migrate, this model will be kept in sync with the database schema, however, it will not be exposed in Prisma Client.', @@ -42,7 +23,7 @@ export const validateIgnoredBlocks = (schema: PrismaSchema, diagnostics: Diagnos }) } } else if (text.includes('@ignore')) { - diagnostics.push({ + diagnostics.add(document.uri, { range: { start: { line: lineIndex, character: 0 }, end: { line: lineIndex, character: MAX_SAFE_VALUE_i32 }, diff --git a/packages/language-server/src/server.ts b/packages/language-server/src/server.ts index 7e2ceb32d4..341dca9053 100644 --- a/packages/language-server/src/server.ts +++ b/packages/language-server/src/server.ts @@ -1,6 +1,5 @@ import { TextDocuments, - Diagnostic, InitializeParams, InitializeResult, CodeActionKind, @@ -21,6 +20,7 @@ import { TextDocument } from 'vscode-languageserver-textdocument' import * as MessageHandler from './lib/MessageHandler' import type { LSOptions, LSSettings } from './lib/types' import { getVersion, getEnginesVersion, getCliVersion } from './lib/prisma-schema-wasm/internals' +import { PrismaSchema } from './lib/Schema' const packageJson = require('../../package.json') // eslint-disable-line @@ -151,30 +151,35 @@ export function startServer(options?: LSOptions): void { connection.window.showErrorMessage(errorMessage) } - function validateTextDocument(textDocument: TextDocument) { - const diagnostics: Diagnostic[] = MessageHandler.handleDiagnosticsRequest(textDocument, showErrorToast) - void connection.sendDiagnostics({ uri: textDocument.uri, diagnostics }) + async function validateTextDocument(textDocument: TextDocument) { + const schema = await PrismaSchema.load(textDocument, documents) + const diagnostics = MessageHandler.handleDiagnosticsRequest(schema, showErrorToast) + for (const [uri, fileDiagnostics] of diagnostics.entries()) { + await connection.sendDiagnostics({ uri, diagnostics: fileDiagnostics }) + } } - documents.onDidChangeContent((change: { document: TextDocument }) => { - validateTextDocument(change.document) + documents.onDidChangeContent(async (change: { document: TextDocument }) => { + await validateTextDocument(change.document) }) function getDocument(uri: string): TextDocument | undefined { return documents.get(uri) } - connection.onDefinition((params: DeclarationParams) => { + connection.onDefinition(async (params: DeclarationParams) => { const doc = getDocument(params.textDocument.uri) if (doc) { - return MessageHandler.handleDefinitionRequest(doc, params) + const schema = await PrismaSchema.load(doc, documents) + return MessageHandler.handleDefinitionRequest(schema, doc, params) } }) - connection.onCompletion((params: CompletionParams) => { + connection.onCompletion(async (params: CompletionParams) => { const doc = getDocument(params.textDocument.uri) if (doc) { - return MessageHandler.handleCompletionRequest(params, doc, showErrorToast) + const schema = await PrismaSchema.load(doc, documents) + return MessageHandler.handleCompletionRequest(schema, doc, params, showErrorToast) } }) @@ -192,31 +197,35 @@ export function startServer(options?: LSOptions): void { void connection.sendNotification('prisma/didChangeWatchedFiles', {}) }) - connection.onHover((params: HoverParams) => { + connection.onHover(async (params: HoverParams) => { const doc = getDocument(params.textDocument.uri) if (doc) { - return MessageHandler.handleHoverRequest(doc, params) + const schema = await PrismaSchema.load(doc, documents) + return MessageHandler.handleHoverRequest(schema, doc, params) } }) - connection.onDocumentFormatting((params: DocumentFormattingParams) => { + connection.onDocumentFormatting(async (params: DocumentFormattingParams) => { const doc = getDocument(params.textDocument.uri) if (doc) { - return MessageHandler.handleDocumentFormatting(params, doc, showErrorToast) + const schema = await PrismaSchema.load(doc, documents) + return MessageHandler.handleDocumentFormatting(schema, doc, params, showErrorToast) } }) - connection.onCodeAction((params: CodeActionParams) => { + connection.onCodeAction(async (params: CodeActionParams) => { const doc = getDocument(params.textDocument.uri) if (doc) { - return MessageHandler.handleCodeActions(params, doc, showErrorToast) + const schema = await PrismaSchema.load(doc, documents) + return MessageHandler.handleCodeActions(schema, doc, params, showErrorToast) } }) - connection.onRenameRequest((params: RenameParams) => { + connection.onRenameRequest(async (params: RenameParams) => { const doc = getDocument(params.textDocument.uri) if (doc) { - return MessageHandler.handleRenameRequest(params, doc) + const schema = await PrismaSchema.load(doc, documents) + return MessageHandler.handleRenameRequest(schema, doc, params) } }) diff --git a/scripts/update_package_json_files.js b/scripts/update_package_json_files.js index 47bf355793..87730bf757 100644 --- a/scripts/update_package_json_files.js +++ b/scripts/update_package_json_files.js @@ -67,6 +67,7 @@ function bumpVersionsInRepo({ channel, newExtensionVersion, newPrismaVersion = ' languageServerPackageJson['prisma']['enginesVersion'] = engineSha // update engines version languageServerPackageJson['dependencies']['@prisma/prisma-schema-wasm'] = engineVersion + languageServerPackageJson['dependencies']['@prisma/schema-files-loader'] = newPrismaVersion // update CLI version languageServerPackageJson['prisma']['cliVersion'] = newPrismaVersion writeJsonToPackageJson({