UNPKG

@prisma/language-server

Version:
344 lines (305 loc) • 10.3 kB
import { DocumentFormattingParams, TextEdit, DeclarationParams, CompletionParams, CompletionList, CompletionItem, HoverParams, Hover, CodeActionParams, CodeAction, Diagnostic, DiagnosticSeverity, RenameParams, WorkspaceEdit, DocumentSymbolParams, DocumentSymbol, SymbolKind, LocationLink, ReferenceParams, Location, } from 'vscode-languageserver' import type { TextDocument } from 'vscode-languageserver-textdocument' import format from './prisma-schema-wasm/format' import lint from './prisma-schema-wasm/lint' import { quickFix } from './code-actions' import { insertBasicRename, renameReferencesForModelName, isEnumValue, renameReferencesForEnumValue, isValidFieldName, extractCurrentName, mapExistsAlready, insertMapAttribute, renameReferencesForFieldName, printLogMessage, isRelationField, isDatamodelBlockName, EditsMap, mergeEditMaps, } from './code-actions/rename' import { validateIgnoredBlocks } from './validations' import { fullDocumentRange, getWordAtPosition, getBlockAtPosition, Block, getBlocks } from './ast' import { prismaSchemaWasmCompletions, localCompletions } from './completions' import { PrismaSchema, SchemaDocument } from './Schema' import { DiagnosticMap } from './DiagnosticMap' import references from './prisma-schema-wasm/references' import hover from './prisma-schema-wasm/hover' export function handleDiagnosticsRequest( schema: PrismaSchema, onError?: (errorMessage: string) => void, ): DiagnosticMap { const res = lint(schema, (errorMessage: string) => { if (onError) { onError(errorMessage) } }) const diagnostics = new DiagnosticMap(schema.documents.map((doc) => doc.uri)) if ( res.some( (diagnostic) => diagnostic.text === "Field declarations don't require a `:`." || diagnostic.text === 'Model declarations have to be indicated with the `model` keyword.', ) ) { if (onError) { onError( "You might currently be viewing a Prisma 1 datamodel which is based on the GraphQL syntax. The current Prisma Language Server doesn't support this syntax. If you are handling a Prisma 1 datamodel, please change the file extension to `.graphql` so the new Prisma Language Server does not get triggered anymore.", ) } } 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), end: document.positionAt(diag.end), }, message: previewNotKnownRegex.test(diag.text) ? `${diag.text}.\nIf this is unexpected, it might be due to your editor's Prisma Extension being out of date.` : diag.text, source: 'Prisma', } if (diag.is_warning) { diagnostic.severity = DiagnosticSeverity.Warning } else { diagnostic.severity = DiagnosticSeverity.Error } diagnostics.add(uri, diagnostic) } validateIgnoredBlocks(schema, diagnostics) return diagnostics } /** * @todo Use official schema.prisma parser. This is a workaround! */ export function handleDefinitionRequest( schema: PrismaSchema, initiatingDocument: TextDocument, params: DeclarationParams, ): LocationLink[] | undefined { const position = params.position const word = getWordAtPosition(initiatingDocument, position) if (word === '') { return } // get start position of block const results = schema .linesAsArray() .map(({ document, lineIndex, text }) => { if ( (text.includes('model') && text.includes(word)) || (text.includes('type') && text.includes(word)) || (text.includes('enum') && text.includes(word)) ) { return [document, lineIndex] } }) .filter((result) => result !== undefined) as [SchemaDocument, number][] if (results.length === 0) { return } const foundBlocks: Block[] = results .map(([document, lineNo]) => { const block = getBlockAtPosition(document.uri, lineNo, schema) if (block && block.name === word && block.range.start.line === lineNo) { return block } }) .filter((block) => block !== undefined) if (foundBlocks.length !== 1) { return } if (!foundBlocks[0]) { return } return [ { targetUri: foundBlocks[0].definingDocument.uri, targetRange: foundBlocks[0].range, targetSelectionRange: foundBlocks[0].nameRange, }, ] } /** * This handler provides the modification to the document to be formatted. */ export function handleDocumentFormatting( schema: PrismaSchema, initiatingDocument: TextDocument, params: DocumentFormattingParams, onError?: (errorMessage: string) => void, ): TextEdit[] { const formatted = format(schema, initiatingDocument, params, onError) return [TextEdit.replace(fullDocumentRange(initiatingDocument), formatted)] } export function handleHoverRequest( schema: PrismaSchema, initiatingDocument: TextDocument, params: HoverParams, onError?: (errorMessage: string) => void, ): Hover | undefined { return hover(schema, initiatingDocument, params, onError) } /** * * This handler provides the initial list of the completion items. */ export function handleCompletionRequest( schema: PrismaSchema, document: TextDocument, params: CompletionParams, onError?: (errorMessage: string) => void, ): CompletionList | undefined { return prismaSchemaWasmCompletions(schema, params, onError) || localCompletions(schema, document, params, onError) } export function handleReferencesRequest( schema: PrismaSchema, params: ReferenceParams, onError?: (errorMessage: string) => void, ): Location[] | undefined { return references(schema, params, onError) } export function handleRenameRequest( schema: PrismaSchema, initiatingDocument: TextDocument, params: RenameParams, ): WorkspaceEdit | undefined { const schemaLines = schema.linesAsArray() const position = params.position 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, initiatingDocument) const isMappable = ['model', 'enum', 'view'].includes(block.type) const needsMap = !isDatamodelBlockRename ? true : isMappable 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) { const edits: EditsMap[] = [] const currentName = extractCurrentName( currentLine, isDatamodelBlockRename, isEnumValueRename, isValidFieldRename, initiatingDocument, params.position, ) let lineNumberOfDefinition = position.line let blockOfDefinition = block let lineOfDefinitionContent = currentLine if (isDatamodelBlockRename) { // get definition of model or enum const matchBlockBeginning = new RegExp(`\\s*(${block.type})\\s+(${currentName})\\s*({)`, 'g') const lineOfDefinition = schemaLines.find((line) => matchBlockBeginning.test(line.text)) if (!lineOfDefinition) { return } const { document: definitionDoc, lineIndex, text } = lineOfDefinition lineNumberOfDefinition = lineIndex lineOfDefinitionContent = text const definitionBlockAtPosition = getBlockAtPosition(definitionDoc.uri, lineNumberOfDefinition, schema) if (!definitionBlockAtPosition) { return } blockOfDefinition = definitionBlockAtPosition } // rename marked string edits.push(insertBasicRename(params.newName, currentName, initiatingDocument, lineNumberOfDefinition)) // check if map exists already if ( !isRelationFieldRename && !mapExistsAlready(lineOfDefinitionContent, schema, blockOfDefinition, isDatamodelBlockRename) && needsMap ) { // add map attribute edits.push(insertMapAttribute(currentName, position, blockOfDefinition, isDatamodelBlockRename)) } // rename references if (isDatamodelBlockRename) { edits.push(renameReferencesForModelName(currentName, params.newName, schema)) } else if (isEnumValueRename) { edits.push(renameReferencesForEnumValue(currentName, params.newName, schema, blockOfDefinition.name)) } else if (isValidFieldRename) { edits.push( renameReferencesForFieldName(currentName, params.newName, schema, blockOfDefinition, isRelationFieldRename), ) } printLogMessage( currentName, params.newName, isDatamodelBlockRename, isValidFieldRename, isEnumValueRename, block.type, ) return { changes: mergeEditMaps(edits), } } return } /** * * @param item This handler resolves additional information for the item selected in the completion list. */ export function handleCompletionResolveRequest(item: CompletionItem): CompletionItem { return item } export function handleCodeActions( schema: PrismaSchema, initiatingDocument: TextDocument, params: CodeActionParams, onError?: (errorMessage: string) => void, ): CodeAction[] { if (!params.context.diagnostics.length) { return [] } return quickFix(schema, initiatingDocument, params, onError) } export function handleDocumentSymbol(params: DocumentSymbolParams, document: TextDocument): DocumentSymbol[] { const schema = PrismaSchema.singleFile(document) return Array.from(getBlocks(schema), (block) => ({ kind: { model: SymbolKind.Class, enum: SymbolKind.Enum, type: SymbolKind.Interface, view: SymbolKind.Class, datasource: SymbolKind.Struct, generator: SymbolKind.Function, }[block.type], name: block.name, range: block.range, selectionRange: block.nameRange, })) }