@prisma/language-server
Version:
Prisma Language Server
411 lines (371 loc) • 10.5 kB
text/typescript
import {
CompletionItem,
CompletionItemKind,
CompletionList,
Position,
TextEdit,
InsertTextMode,
InsertTextFormat,
} from 'vscode-languageserver'
import * as completions from './completions.json'
import { Block, getValuesInsideSquareBrackets, isInsideAttribute } from '../ast'
import { convertToCompletionItems } from './internals'
import { klona } from 'klona'
import listAllAvailablePreviewFeatures from '../prisma-schema-wasm/listAllAvailablePreviewFeatures'
import { PrismaSchema } from '../Schema'
/**
* ```prisma
* generator client {
* |
* }
* ```
*/
const supportedGeneratorFields: CompletionItem[] = convertToCompletionItems(
completions.generatorFields,
CompletionItemKind.Field,
)
// generator.provider
const generatorProviders: CompletionItem[] = convertToCompletionItems(
completions.generatorProviders,
CompletionItemKind.Constant,
)
/**
* ```prisma
* generator client {
* provider = "|"
* }
* ```
*/
const generatorProviderArguments: CompletionItem[] = convertToCompletionItems(
completions.generatorProviderArguments,
CompletionItemKind.Property,
)
// generator.previewFeatures
const previewFeaturesArguments: CompletionItem[] = convertToCompletionItems(
completions.previewFeaturesArguments,
CompletionItemKind.Property,
)
/**
* ```prisma
* generator client {
* provider = "prisma-client"
* runtime = |
* }
* ```
*/
const runtimeArguments: CompletionItem[] = convertToCompletionItems(
completions.runtimeArguments,
CompletionItemKind.Property,
)
/**
* ```prisma
* generator client {
* provider = "prisma-client"
* runtime = "|"
* }
* ```
*/
const runtimes: CompletionItem[] = convertToCompletionItems(completions.runtimes, CompletionItemKind.Constant)
/**
* ```prisma
* generator client {
* provider = "prisma-client"
* moduleFormats = |
* }
* ```
*/
const moduleFormatsArguments: CompletionItem[] = convertToCompletionItems(
completions.moduleFormatArguments,
CompletionItemKind.Property,
)
/**
* ```prisma
* generator client {
* provider = "prisma-client"
* moduleFormats = "|""
* }
* ```
*/
const moduleFormats: CompletionItem[] = convertToCompletionItems(completions.moduleFormats, CompletionItemKind.Constant)
/**
* ```prisma
* generator client {
* provider = "prisma-client"
* generatedFileExtension = |
* }
* ```
*/
const generatedFileExtensionArguments: CompletionItem[] = convertToCompletionItems(
completions.generatedFileExtensionArguments,
CompletionItemKind.Property,
)
/**
* ```prisma
* generator client {
* provider = "prisma-client"
* generatedFileExtension = "|""
* }
* ```
*/
const generatedFileExtensions: CompletionItem[] = convertToCompletionItems(
completions.generatedFileExtensions,
CompletionItemKind.Constant,
)
/**
* ```prisma
* generator client {
* provider = "prisma-client"
* importFileExtension = |
* }
* ```
*/
const importFileExtensionArguments: CompletionItem[] = convertToCompletionItems(
completions.importFileExtensionArguments,
CompletionItemKind.Property,
)
/**
* ```prisma
* generator client {
* provider = "prisma-client"
* importFileExtension = "|""
* }
* ```
*/
const importFileExtensions: CompletionItem[] = convertToCompletionItems(
completions.importFileExtension,
CompletionItemKind.Constant,
)
const compilerBuildArguments: CompletionItem[] = convertToCompletionItems(
completions.compilerBuildArguments,
CompletionItemKind.Property,
)
const compilerBuilds: CompletionItem[] = convertToCompletionItems(
completions.compilerBuild,
CompletionItemKind.Constant,
)
// Fields for prisma-client generator
const prismaClientFields: CompletionItem[] = convertToCompletionItems(
completions.generatorPrismaClientFields,
CompletionItemKind.Field,
)
// Fields for prisma-client-js generator
const prismaClientJSFields: CompletionItem[] = convertToCompletionItems(
completions.generatorPrismaClientJSFields,
CompletionItemKind.Field,
)
/**
* Helper function to get the provider value from a generator block
*/
function getProviderFromBlock(block: Block): string | undefined {
const lines = block.definingDocument.lines
for (let lineIndex = block.range.start.line + 1; lineIndex < block.range.end.line; lineIndex++) {
const line = lines[lineIndex].text.trim()
if (line.startsWith('provider')) {
// given a line like `provider = "prisma-client"`, extract the value of the provider (e.g., 'prisma-client').
const match = line.match(/\s*provider\s*=\s*"([^"]+)"/)
return match ? match[1] : undefined
}
}
return undefined
}
function handlePreviewFeatures(
previewFeaturesArray: string[],
position: Position,
currentLineUntrimmed: string,
isInsideQuotation: boolean,
): CompletionList {
const previewFeatures: CompletionItem[] = previewFeaturesArray.map((pf) => CompletionItem.create(pf))
if (isInsideAttribute(currentLineUntrimmed, position, '[]')) {
if (isInsideQuotation) {
const usedValues = getValuesInsideSquareBrackets(currentLineUntrimmed)
return {
items: previewFeatures.filter((t) => !usedValues.includes(t.label)),
isIncomplete: true,
}
}
return {
items: previewFeaturesArguments.filter((arg) => !arg.label.includes('[')),
isIncomplete: true,
}
} else {
if (!isInsideQuotation) {
return {
items: previewFeaturesArguments.filter((arg) => !arg.label.includes('"')),
isIncomplete: true,
}
}
const firstQuoteIndex = currentLineUntrimmed.indexOf('"')
const lastQuoteIndex = currentLineUntrimmed.lastIndexOf('"')
const convertToListAndSuggestMore: CompletionItem = {
additionalTextEdits: [TextEdit.insert(Position.create(position.line, lastQuoteIndex + 1), ']')],
command: {
command: 'editor.action.triggerSuggest',
title: 'Trigger Suggest',
},
kind: CompletionItemKind.Value,
label: '[""] (convert to list)',
insertTextFormat: InsertTextFormat.Snippet,
insertTextMode: InsertTextMode.asIs,
preselect: true,
textEdit: TextEdit.insert(Position.create(position.line, firstQuoteIndex - 1), ' ["'),
}
return {
items: [convertToListAndSuggestMore],
isIncomplete: true,
}
}
}
/**
* Removes all field suggestion that are invalid in this context. E.g. fields that are used already in a block will not be suggested again.
* This function removes all field suggestion that are invalid in a certain context. E.g. in a generator block `provider, output, platforms, pinnedPlatForm`
* are possible fields. But those fields are only valid suggestions if they haven't been used in this block yet. So in case `provider` has already been used, only
* `output, platforms, pinnedPlatform` will be suggested.
*/
function removeInvalidFieldSuggestions(
supportedFields: string[],
block: Block,
schema: PrismaSchema,
position: Position,
): string[] {
let reachedStartLine = false
for (const { lineIndex, text } of schema.iterLines()) {
if (lineIndex === block.range.start.line + 1) {
reachedStartLine = true
}
if (!reachedStartLine || lineIndex === position.line) {
continue
}
if (lineIndex === block.range.end.line) {
break
}
const fieldName = text.replace(/ .*/, '')
if (supportedFields.includes(fieldName)) {
supportedFields = supportedFields.filter((field) => field !== fieldName)
}
}
return supportedFields
}
export function getSuggestionForGeneratorField(
block: Block,
schema: PrismaSchema,
position: Position,
): CompletionItem[] {
const provider = getProviderFromBlock(block)
let suggestions: CompletionItem[]
if (provider === 'prisma-client-js') {
suggestions = klona(prismaClientJSFields)
} else if (provider === 'prisma-client') {
suggestions = klona(prismaClientFields)
} else {
// Default to showing all fields if no provider is set yet
suggestions = klona(supportedGeneratorFields)
}
const labels = removeInvalidFieldSuggestions(
suggestions.map((item) => item.label),
block,
schema,
position,
)
return suggestions.filter((item) => labels.includes(item.label))
}
export const generatorSuggestions = (
line: string,
untrimmedLine: string,
position: Position,
isInsideQuotation: boolean,
onError?: (errorMessage: string) => void,
) => {
/**
* Common cases for generator blocks:
*/
// provider
if (line.startsWith('provider')) {
const providers: CompletionItem[] = generatorProviders
if (isInsideQuotation) {
return {
items: providers,
isIncomplete: true,
}
}
return {
items: generatorProviderArguments,
isIncomplete: true,
}
}
// previewFeatures
if (line.startsWith('previewFeatures')) {
const generatorPreviewFeatures: string[] = listAllAvailablePreviewFeatures(onError)
if (generatorPreviewFeatures.length > 0) {
return handlePreviewFeatures(generatorPreviewFeatures, position, untrimmedLine, isInsideQuotation)
}
return undefined
}
/**
* Cases for `provider = "prisma-client"`
*/
// runtime
if (line.startsWith('runtime')) {
if (isInsideQuotation) {
return {
items: runtimes,
isIncomplete: true,
}
}
return {
items: runtimeArguments,
isIncomplete: true,
}
}
// runtime
if (line.startsWith('moduleFormat')) {
if (isInsideQuotation) {
return {
items: moduleFormats,
isIncomplete: true,
}
}
return {
items: moduleFormatsArguments,
isIncomplete: true,
}
}
// generatedFileExtension
if (line.startsWith('generatedFileExtension')) {
if (isInsideQuotation) {
return {
items: generatedFileExtensions,
isIncomplete: true,
}
}
return {
items: generatedFileExtensionArguments,
isIncomplete: true,
}
}
// importFileExtension
if (line.startsWith('importFileExtension')) {
if (isInsideQuotation) {
return {
items: importFileExtensions,
isIncomplete: true,
}
}
return {
items: importFileExtensionArguments,
isIncomplete: true,
}
}
// compilerBuild
if (line.startsWith('compilerBuild')) {
if (isInsideQuotation) {
return {
items: compilerBuilds,
isIncomplete: true,
}
}
return {
items: compilerBuildArguments,
isIncomplete: true,
}
}
}