@prisma/language-server
Version:
Prisma Language Server
551 lines • 25.4 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.prismaSchemaWasmCompletions = prismaSchemaWasmCompletions;
exports.localCompletions = localCompletions;
const vscode_languageserver_1 = require("vscode-languageserver");
const textDocumentCompletion_1 = __importDefault(require("../prisma-schema-wasm/textDocumentCompletion"));
const ast_1 = require("../ast");
const attributes_1 = require("./attributes");
const blocks_1 = require("./blocks");
const internals_1 = require("./internals");
const types_1 = require("./types");
const datasource_1 = require("./datasource");
const generator_1 = require("./generator");
const arguments_1 = require("./arguments");
const functions_1 = require("./functions");
function getDefaultValueSuggestions({ currentLine, schema, wordsBeforePosition, }) {
const suggestions = [];
const datasourceProvider = (0, ast_1.getFirstDatasourceProvider)(schema);
// Completions for sequence(|)
if (datasourceProvider === 'cockroachdb') {
if (wordsBeforePosition.some((a) => a.includes('sequence('))) {
const sequenceProperties = ['virtual', 'minValue', 'maxValue', 'cache', 'increment', 'start'];
// No suggestions if virtual is present
if (currentLine.includes('virtual')) {
return suggestions;
}
if (!sequenceProperties.some((it) => currentLine.includes(it))) {
(0, arguments_1.virtualSequenceDefaultCompletion)(suggestions);
}
if (!currentLine.includes('minValue')) {
(0, arguments_1.minValueSequenceDefaultCompletion)(suggestions);
}
if (!currentLine.includes('maxValue')) {
(0, arguments_1.maxValueSequenceDefaultCompletion)(suggestions);
}
if (!currentLine.includes('cache')) {
(0, arguments_1.cacheSequenceDefaultCompletion)(suggestions);
}
if (!currentLine.includes('increment')) {
(0, arguments_1.incrementSequenceDefaultCompletion)(suggestions);
}
if (!currentLine.includes('start')) {
(0, arguments_1.startSequenceDefaultCompletion)(suggestions);
}
return suggestions;
}
}
if (datasourceProvider === 'mongodb') {
(0, functions_1.autoDefaultCompletion)(suggestions);
}
else {
(0, functions_1.dbgeneratedDefaultCompletion)(suggestions);
}
const fieldType = (0, ast_1.getFieldType)(currentLine);
if (!fieldType) {
return [];
}
const isScalarList = fieldType.endsWith('[]');
if (isScalarList) {
const isDefaultEmpty = wordsBeforePosition.at(-1)?.trim().endsWith('(');
const listItemFieldType = fieldType.slice(0, -2);
const listItemSuggestions = getDefaultValueSuggestions({
currentLine: currentLine.replace(fieldType, listItemFieldType),
schema,
wordsBeforePosition,
})
// functions are currently not supported for list defaults
.filter((item) => item.kind !== vscode_languageserver_1.CompletionItemKind.Function);
if (isDefaultEmpty) {
suggestions.unshift({
command: listItemSuggestions.length
? {
command: 'editor.action.triggerSuggest',
title: 'triggerSuggest',
}
: undefined,
documentation: 'Set a default value on the list field',
insertText: '[$0]',
insertTextFormat: vscode_languageserver_1.InsertTextFormat.Snippet,
kind: vscode_languageserver_1.CompletionItemKind.Value,
label: '[]',
});
return suggestions;
}
return listItemSuggestions;
}
switch (fieldType) {
case 'BigInt':
case 'Int':
if (datasourceProvider === 'cockroachdb') {
(0, functions_1.sequenceDefaultCompletion)(suggestions);
if (fieldType === 'Int') {
// @default(autoincrement()) is only supported on BigInt fields for cockroachdb.
break;
}
}
(0, functions_1.autoincrementDefaultCompletion)(suggestions);
break;
case 'DateTime':
(0, functions_1.nowDefaultCompletion)(suggestions);
break;
case 'String':
(0, functions_1.uuidDefaultCompletion)(suggestions);
(0, functions_1.cuidDefaultCompletion)(suggestions);
(0, functions_1.ulidDefaultCompletion)(suggestions);
(0, functions_1.nanoidDefaultCompletion)(suggestions);
break;
case 'Boolean':
(0, arguments_1.booleanDefaultCompletions)(suggestions);
break;
}
const dataBlock = (0, ast_1.getDatamodelBlock)(fieldType, schema);
if (dataBlock && dataBlock.type === 'enum') {
// get fields from enum block for suggestions
const values = (0, ast_1.getFieldsFromCurrentBlock)(schema, dataBlock);
values.forEach((v) => suggestions.push({ label: v, kind: vscode_languageserver_1.CompletionItemKind.Value }));
}
return suggestions;
}
function getSuggestionsForAttribute({ attribute, wordsBeforePosition, untrimmedCurrentLine, schema, block, position, }) {
const firstWordBeforePosition = wordsBeforePosition[wordsBeforePosition.length - 1];
const secondWordBeforePosition = wordsBeforePosition[wordsBeforePosition.length - 2];
const wordBeforePosition = firstWordBeforePosition === '' ? secondWordBeforePosition : firstWordBeforePosition;
let suggestions = [];
// We can filter on the datasource
const datasourceProvider = (0, ast_1.getFirstDatasourceProvider)(schema);
// We can filter on the previewFeatures enabled
const previewFeatures = (0, ast_1.getAllPreviewFeaturesFromGenerators)(schema);
if (attribute === '@relation') {
if (datasourceProvider === 'mongodb') {
suggestions = arguments_1.relationArguments.filter((arg) => arg.label !== 'map' && arg.label !== 'onDelete' && arg.label !== 'onUpdate');
}
else {
suggestions = arguments_1.relationArguments;
}
// If we are right after @relation(
if (wordBeforePosition.includes('@relation')) {
return {
items: suggestions,
isIncomplete: false,
};
}
// TODO check fields with [] shortcut
if ((0, ast_1.isInsideGivenProperty)(untrimmedCurrentLine, wordsBeforePosition, 'fields', position)) {
return {
items: (0, internals_1.toCompletionItems)((0, ast_1.getFieldsFromCurrentBlock)(schema, block, position), vscode_languageserver_1.CompletionItemKind.Field),
isIncomplete: false,
};
}
if ((0, ast_1.isInsideGivenProperty)(untrimmedCurrentLine, wordsBeforePosition, 'references', position)) {
// Get the name by potentially removing ? and [] from Foo? or Foo[]
const referencedModelName = wordsBeforePosition[1].replace('?', '').replace('[]', '');
const referencedBlock = (0, ast_1.getDatamodelBlock)(referencedModelName, schema);
// referenced model does not exist
// TODO type?
if (!referencedBlock || referencedBlock.type !== 'model') {
return;
}
return {
items: (0, internals_1.toCompletionItems)((0, ast_1.getFieldsFromCurrentBlock)(schema, referencedBlock), vscode_languageserver_1.CompletionItemKind.Field),
isIncomplete: false,
};
}
}
else {
// @id, @unique
// @@id, @@unique, @@index, @@fulltext
// The length argument is available on MySQL only on the
// @id, @@id, @unique, @@unique and @@index fields.
// The sort argument is available for all databases on the
// @unique, @@unique and @@index fields.
// Additionally, SQL Server also allows it on @id and @@id.
let attribute = undefined;
if (wordsBeforePosition.some((a) => a.includes('@@id'))) {
attribute = '@@id';
}
else if (wordsBeforePosition.some((a) => a.includes('@id'))) {
attribute = '@id';
}
else if (wordsBeforePosition.some((a) => a.includes('@@unique'))) {
attribute = '@@unique';
}
else if (wordsBeforePosition.some((a) => a.includes('@unique'))) {
attribute = '@unique';
}
else if (wordsBeforePosition.some((a) => a.includes('@@index'))) {
attribute = '@@index';
}
else if (wordsBeforePosition.some((a) => a.includes('@@fulltext'))) {
attribute = '@@fulltext';
}
/**
* inside []
* suggest composite types for MongoDB
* suggest fields and extendedIndexes arguments (sort / length)
*
* Examples
* field attribute: slug String @unique(sort: Desc, length: 42) @db.VarChar(3000)
* block attribute: @@id([title(length: 100, sort: Desc), abstract(length: 10)])
*/
if (attribute && attribute !== '@@fulltext' && (0, ast_1.isInsideAttribute)(untrimmedCurrentLine, position, '[]')) {
if ((0, ast_1.isInsideFieldArgument)(untrimmedCurrentLine, position)) {
// extendedIndexes
const items = [];
// https://www.notion.so/prismaio/Proposal-More-PostgreSQL-index-types-GiST-GIN-SP-GiST-and-BRIN-e27ef762ee4846a9a282eec1a5129270
if (datasourceProvider === 'postgresql' && attribute === '@@index') {
(0, arguments_1.opsIndexFulltextCompletion)(items);
}
items.push(...(0, arguments_1.filterSortLengthBasedOnInput)(attribute, previewFeatures, datasourceProvider, wordBeforePosition, arguments_1.sortLengthProperties));
return {
items,
isIncomplete: false,
};
}
const fieldsFromLine = (0, ast_1.getValuesInsideSquareBrackets)(untrimmedCurrentLine);
/*
* MongoDB composite type fields, see https://www.prisma.io/docs/concepts/components/prisma-schema/data-model#composite-type-unique-constraints
* Examples
* @@unique([address.|]) or @@unique(fields: [address.|])
* @@index([address.|]) or @@index(fields: [address.|])
*/
if (datasourceProvider === 'mongodb' && fieldsFromLine && firstWordBeforePosition.endsWith('.')) {
const getFieldName = (text) => {
const [_, __, value] = new RegExp(/(.*\[)?(.+)/).exec(text) || [];
let name = value;
// Example for `@@index([email,address.|])` when there is no space between fields
if (name?.includes(',')) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
name = name.split(',').pop();
}
// Remove . to only get the name
if (name?.endsWith('.')) {
name = name.slice(0, -1);
}
return name;
};
const currentFieldName = getFieldName(firstWordBeforePosition);
if (!currentFieldName) {
return {
isIncomplete: false,
items: [],
};
}
const currentCompositeAsArray = currentFieldName.split('.');
const fieldTypesFromCurrentBlock = (0, ast_1.getFieldTypesFromCurrentBlock)(schema, block);
const fields = (0, ast_1.getCompositeTypeFieldsRecursively)(schema, currentCompositeAsArray, fieldTypesFromCurrentBlock);
return {
items: (0, internals_1.toCompletionItems)(fields, vscode_languageserver_1.CompletionItemKind.Field),
isIncomplete: false,
};
}
let fieldsFromCurrentBlock = (0, ast_1.getFieldsFromCurrentBlock)(schema, block, position);
if (fieldsFromLine.length > 0) {
// If we are in a composite type, exit here, to not pollute results with first level fields
if (firstWordBeforePosition.includes('.')) {
return {
isIncomplete: false,
items: [],
};
}
// Remove items already used
fieldsFromCurrentBlock = fieldsFromCurrentBlock.filter((s) => !fieldsFromLine.includes(s));
// Return fields
// `onCompletionResolve` will take care of filtering the partial matches
if (firstWordBeforePosition !== '' &&
!firstWordBeforePosition.endsWith(',') &&
!firstWordBeforePosition.endsWith(', ')) {
return {
items: (0, internals_1.toCompletionItems)(fieldsFromCurrentBlock, vscode_languageserver_1.CompletionItemKind.Field),
isIncomplete: false,
};
}
}
return {
items: (0, internals_1.toCompletionItems)(fieldsFromCurrentBlock, vscode_languageserver_1.CompletionItemKind.Field),
isIncomplete: false,
};
}
// "@@" block attributes
let blockAtrributeArguments = [];
if (attribute === '@@unique') {
blockAtrributeArguments = (0, arguments_1.getCompletionsForBlockAttributeArgs)({
blockAttributeWithParams: '@@unique',
wordBeforePosition,
datasourceProvider,
previewFeatures,
});
}
else if (attribute === '@@id') {
blockAtrributeArguments = (0, arguments_1.getCompletionsForBlockAttributeArgs)({
blockAttributeWithParams: '@@id',
wordBeforePosition,
datasourceProvider,
previewFeatures,
});
}
else if (attribute === '@@index') {
blockAtrributeArguments = (0, arguments_1.getCompletionsForBlockAttributeArgs)({
blockAttributeWithParams: '@@index',
wordBeforePosition,
datasourceProvider,
previewFeatures,
});
}
else if (attribute === '@@fulltext') {
blockAtrributeArguments = (0, arguments_1.getCompletionsForBlockAttributeArgs)({
blockAttributeWithParams: '@@fulltext',
wordBeforePosition,
datasourceProvider,
previewFeatures,
});
}
if (blockAtrributeArguments.length) {
suggestions = blockAtrributeArguments;
}
else {
// "@" field attributes
let fieldAtrributeArguments = [];
if (attribute === '@unique') {
fieldAtrributeArguments = (0, arguments_1.getCompletionsForFieldAttributeArgs)('@unique', previewFeatures, datasourceProvider, wordBeforePosition);
}
else if (attribute === '@id') {
fieldAtrributeArguments = (0, arguments_1.getCompletionsForFieldAttributeArgs)('@id', previewFeatures, datasourceProvider, wordBeforePosition);
}
suggestions = fieldAtrributeArguments;
}
}
// Check which attributes are already present
// so we can filter them out from the suggestions
const attributesFound = new Set();
for (const word of wordsBeforePosition) {
if (word.includes('references')) {
attributesFound.add('references');
}
if (word.includes('fields')) {
attributesFound.add('fields');
}
if (word.includes('onUpdate')) {
attributesFound.add('onUpdate');
}
if (word.includes('onDelete')) {
attributesFound.add('onDelete');
}
if (word.includes('map')) {
attributesFound.add('map');
}
if (word.includes('name') || /".*"/.exec(word)) {
attributesFound.add('name');
attributesFound.add('""');
}
if (word.includes('type')) {
attributesFound.add('type');
}
}
// now filter them out of the suggestions as they are already present
const filteredSuggestions = suggestions.reduce((accumulator, sugg) => {
let suggestionMatch = false;
for (const attribute of attributesFound) {
if (sugg.label.includes(attribute)) {
suggestionMatch = true;
}
}
if (!suggestionMatch) {
accumulator.push(sugg);
}
return accumulator;
}, []);
// nothing to present any more, return
if (filteredSuggestions.length === 0) {
return;
}
return {
items: filteredSuggestions,
isIncomplete: false,
};
}
function getSuggestionsForInsideRoundBrackets(untrimmedCurrentLine, schema, position, block) {
const wordsBeforePosition = untrimmedCurrentLine.slice(0, position.character).trimStart().split(/\s+/);
if (wordsBeforePosition.some((a) => a.includes('@default'))) {
return {
items: getDefaultValueSuggestions({
currentLine: block.definingDocument.getLineContent(position.line),
schema,
wordsBeforePosition,
}),
isIncomplete: false,
};
}
if (wordsBeforePosition.some((a) => a.includes('@relation'))) {
return getSuggestionsForAttribute({
attribute: '@relation',
wordsBeforePosition,
untrimmedCurrentLine,
schema,
block,
position,
});
}
if (
// matches
// @id, @unique
// @@id, @@unique, @@index, @@fulltext
wordsBeforePosition.some((a) => a.includes('@unique') || a.includes('@id') || a.includes('@@index') || a.includes('@@fulltext'))) {
return getSuggestionsForAttribute({
wordsBeforePosition,
untrimmedCurrentLine,
schema,
block,
position,
});
}
return {
items: (0, internals_1.toCompletionItems)([], vscode_languageserver_1.CompletionItemKind.Field),
isIncomplete: false,
};
}
// Suggest fields for a BlockType
function getSuggestionForSupportedFields(blockType, currentLine, currentLineUntrimmed, position, schema, onError) {
const isInsideQuotation = (0, internals_1.isInsideQuotationMark)(currentLineUntrimmed, position);
// We can filter on the datasource
const datasourceProvider = (0, ast_1.getFirstDatasourceProvider)(schema);
switch (blockType) {
case 'generator':
return (0, generator_1.generatorSuggestions)(currentLine, currentLineUntrimmed, position, isInsideQuotation, onError);
case 'datasource':
return (0, datasource_1.dataSourceSuggestions)(currentLine, isInsideQuotation, datasourceProvider);
default:
return undefined;
}
}
/**
* gets suggestions for block type
*/
function getSuggestionForFirstInsideBlock(blockType, schema, position, block) {
let suggestions = [];
switch (blockType) {
case 'generator':
suggestions = (0, generator_1.getSuggestionForGeneratorField)(block, schema, position);
break;
case 'model':
case 'view':
suggestions = (0, attributes_1.getSuggestionForBlockAttribute)(block, schema);
break;
case 'type':
// No suggestions
break;
}
return {
items: suggestions,
isIncomplete: false,
};
}
function prismaSchemaWasmCompletions(schema, params, onError) {
const completionList = (0, textDocumentCompletion_1.default)(schema, params, (errorMessage) => {
if (onError) {
onError(errorMessage);
}
});
if (completionList.items.length === 0) {
return undefined;
}
else {
return completionList;
}
}
function localCompletions(schema, initiatingDocument, params, onError) {
const context = params.context;
const position = params.position;
const currentLineUntrimmed = (0, ast_1.getCurrentLine)(initiatingDocument, position.line);
const currentLineTillPosition = currentLineUntrimmed.slice(0, position.character - 1).trim();
const wordsBeforePosition = currentLineTillPosition.split(/\s+/);
const symbolBeforePosition = (0, ast_1.getSymbolBeforePosition)(initiatingDocument, position);
const symbolBeforePositionIsWhiteSpace = symbolBeforePosition.search(/\s/) !== -1;
const positionIsAfterArray = wordsBeforePosition.length >= 3 && !currentLineTillPosition.includes('[') && symbolBeforePositionIsWhiteSpace;
// datasource, generator, model, type or enum
const foundBlock = (0, ast_1.getBlockAtPosition)(initiatingDocument.uri, position.line, schema);
if (!foundBlock) {
if (wordsBeforePosition.length > 1 || (wordsBeforePosition.length === 1 && symbolBeforePositionIsWhiteSpace)) {
return;
}
return (0, blocks_1.getSuggestionForBlockTypes)(schema);
}
if ((0, ast_1.isFirstInsideBlock)(position, currentLineUntrimmed)) {
return getSuggestionForFirstInsideBlock(foundBlock.type, schema, position, foundBlock);
}
// Completion was triggered by a triggerCharacter
// triggerCharacters defined in src/server.ts
if (context?.triggerKind === vscode_languageserver_1.CompletionTriggerKind.TriggerCharacter) {
switch (context.triggerCharacter) {
case '@':
if (!(0, ast_1.positionIsAfterFieldAndType)(position, initiatingDocument, wordsBeforePosition)) {
return;
}
return (0, attributes_1.getSuggestionForFieldAttribute)(foundBlock, (0, ast_1.getCurrentLine)(initiatingDocument, position.line), schema, wordsBeforePosition, onError);
case '"':
return getSuggestionForSupportedFields(foundBlock.type, foundBlock.definingDocument.getLineContent(position.line), currentLineUntrimmed, position, schema, onError);
case '.':
// check if inside attribute
// Useful to complete composite types
if (['model', 'view'].includes(foundBlock.type) && (0, ast_1.isInsideAttribute)(currentLineUntrimmed, position, '()')) {
return getSuggestionsForInsideRoundBrackets(currentLineUntrimmed, schema, position, foundBlock);
}
else {
return (0, types_1.getSuggestionForNativeTypes)(foundBlock, schema, wordsBeforePosition, onError);
}
}
}
switch (foundBlock.type) {
case 'model':
case 'view':
case 'type':
// check if inside attribute
if ((0, ast_1.isInsideAttribute)(currentLineUntrimmed, position, '()')) {
return getSuggestionsForInsideRoundBrackets(currentLineUntrimmed, schema, position, foundBlock);
}
// check if field type
if (!(0, ast_1.positionIsAfterFieldAndType)(position, initiatingDocument, wordsBeforePosition)) {
return (0, types_1.getSuggestionsForFieldTypes)(schema, position, currentLineUntrimmed);
}
return (0, attributes_1.getSuggestionForFieldAttribute)(foundBlock, foundBlock.definingDocument.getLineContent(position.line), schema, wordsBeforePosition, onError);
case 'datasource':
case 'generator':
// If we're on a line with just whitespace, suggest field names
if (currentLineTillPosition.trim() === '') {
if (foundBlock.type === 'generator') {
const generatorFields = (0, generator_1.getSuggestionForGeneratorField)(foundBlock, schema, position);
return {
items: generatorFields,
isIncomplete: false,
};
}
// For datasource, we could add similar logic here if needed
}
if (wordsBeforePosition.length === 1 && symbolBeforePositionIsWhiteSpace) {
return (0, internals_1.suggestEqualSymbol)(foundBlock.type);
}
if (currentLineTillPosition.includes('=') &&
!currentLineTillPosition.includes(']') &&
!positionIsAfterArray &&
symbolBeforePosition !== ',') {
return getSuggestionForSupportedFields(foundBlock.type, foundBlock.definingDocument.getLineContent(position.line), currentLineUntrimmed, position, schema, onError);
}
break;
case 'enum':
break;
}
}
//# sourceMappingURL=index.js.map