UNPKG

apollo-language-server

Version:

A language server for Apollo GraphQL projects

488 lines 23.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.GraphQLLanguageProvider = void 0; const vscode_languageserver_1 = require("vscode-languageserver"); const graphql_language_service_interface_1 = require("@apollographql/graphql-language-service-interface"); const getAutocompleteSuggestions_1 = require("@apollographql/graphql-language-service-interface/dist/getAutocompleteSuggestions"); const source_1 = require("./utilities/source"); const graphql_1 = require("graphql"); const graphql_2 = require("./utilities/graphql"); const client_1 = require("./project/client"); const apollo_tools_1 = require("@apollographql/apollo-tools"); const diagnostics_1 = require("./diagnostics"); const DirectiveLocations = Object.keys(graphql_1.DirectiveLocation); function hasFields(type) { return ((0, graphql_1.isObjectType)(type) || ((0, graphql_1.isListType)(type) && hasFields(type.ofType)) || ((0, graphql_1.isNonNullType)(type) && hasFields(type.ofType))); } function uriForASTNode(node) { const uri = node.loc && node.loc.source && node.loc.source.name; if (!uri || uri === "GraphQL") { return null; } return uri; } function locationForASTNode(node) { const uri = uriForASTNode(node); if (!uri) return null; return vscode_languageserver_1.Location.create(uri, (0, source_1.rangeForASTNode)(node)); } function symbolForFieldDefinition(definition) { return { name: definition.name.value, kind: vscode_languageserver_1.SymbolKind.Field, range: (0, source_1.rangeForASTNode)(definition), selectionRange: (0, source_1.rangeForASTNode)(definition), }; } class GraphQLLanguageProvider { constructor(workspace) { this.workspace = workspace; } async provideStats(uri) { if (this.workspace.projects.length && uri) { const project = this.workspace.projectForFile(uri); return project ? project.getProjectStats() : { loaded: false }; } return { loaded: false }; } async provideCompletionItems(uri, position, _token) { const project = this.workspace.projectForFile(uri); if (!(project && project instanceof client_1.GraphQLClientProject)) return []; const document = project.documentAt(uri, position); if (!document) return []; if (!project.schema) return []; const positionInDocument = (0, source_1.positionFromPositionInContainingDocument)(document.source, position); const token = (0, getAutocompleteSuggestions_1.getTokenAtPosition)(document.source.body, positionInDocument); const state = token.state.kind === "Invalid" ? token.state.prevState : token.state; const typeInfo = (0, getAutocompleteSuggestions_1.getTypeInfo)(project.schema, token.state); if (state.kind === "DirectiveLocation") { return DirectiveLocations.map((location) => ({ label: location, kind: vscode_languageserver_1.CompletionItemKind.Constant, })); } const suggestions = (0, graphql_language_service_interface_1.getAutocompleteSuggestions)(project.schema, document.source.body, positionInDocument); if (state.kind === "SelectionSet" || state.kind === "Field" || state.kind === "AliasedField") { const parentType = typeInfo.parentType; const parentFields = Object.assign({}, parentType.getFields()); if ((0, graphql_1.isAbstractType)(parentType)) { parentFields[graphql_1.TypeNameMetaFieldDef.name] = graphql_1.TypeNameMetaFieldDef; } if (parentType === project.schema.getQueryType()) { parentFields[graphql_1.SchemaMetaFieldDef.name] = graphql_1.SchemaMetaFieldDef; parentFields[graphql_1.TypeMetaFieldDef.name] = graphql_1.TypeMetaFieldDef; } return suggestions.map((suggest) => { const suggestedField = parentFields[suggest.label]; if (!suggestedField) { return suggest; } else { const requiredArgs = suggestedField.args.filter((a) => (0, graphql_1.isNonNullType)(a.type)); const paramsSection = requiredArgs.length > 0 ? `(${requiredArgs .map((a, i) => `${a.name}: $${i + 1}`) .join(", ")})` : ``; const isClientType = parentType.clientSchema && parentType.clientSchema.localFields && parentType.clientSchema.localFields.includes(suggestedField.name); const directives = isClientType ? " @client" : ""; const snippet = hasFields(suggestedField.type) ? `${suggest.label}${paramsSection}${directives} {\n\t$0\n}` : `${suggest.label}${paramsSection}${directives}`; return Object.assign(Object.assign({}, suggest), { insertText: snippet, insertTextFormat: vscode_languageserver_1.InsertTextFormat.Snippet }); } }); } if (state.kind === "Directive") { return suggestions.map((suggest) => { const directive = project.schema.getDirective(suggest.label); if (!directive) { return suggest; } const requiredArgs = directive.args.filter(graphql_1.isNonNullType); const paramsSection = requiredArgs.length > 0 ? `(${requiredArgs .map((a, i) => `${a.name}: $${i + 1}`) .join(", ")})` : ``; const snippet = `${suggest.label}${paramsSection}`; const argsString = directive.args.length > 0 ? `(${directive.args .map((a) => `${a.name}: ${a.type}`) .join(", ")})` : ""; const content = [ [`\`\`\`graphql`, `@${suggest.label}${argsString}`, `\`\`\``].join("\n"), ]; if (suggest.documentation) { if (typeof suggest.documentation === "string") { content.push(suggest.documentation); } else { content.push(suggest.documentation.value); } } const doc = { kind: vscode_languageserver_1.MarkupKind.Markdown, value: content.join("\n\n"), }; return Object.assign(Object.assign({}, suggest), { documentation: doc, insertText: snippet, insertTextFormat: vscode_languageserver_1.InsertTextFormat.Snippet }); }); } return suggestions; } async provideHover(uri, position, _token) { const project = this.workspace.projectForFile(uri); if (!(project && project instanceof client_1.GraphQLClientProject)) return null; const document = project.documentAt(uri, position); if (!(document && document.ast)) return null; if (!project.schema) return null; const positionInDocument = (0, source_1.positionFromPositionInContainingDocument)(document.source, position); const nodeAndTypeInfo = (0, source_1.getASTNodeAndTypeInfoAtPosition)(document.source, positionInDocument, document.ast, project.schema); if (nodeAndTypeInfo) { const [node, typeInfo] = nodeAndTypeInfo; switch (node.kind) { case graphql_1.Kind.FRAGMENT_SPREAD: { const fragmentName = node.name.value; const fragment = project.fragments[fragmentName]; if (fragment) { return { contents: { language: "graphql", value: `fragment ${fragmentName} on ${fragment.typeCondition.name.value}`, }, }; } break; } case graphql_1.Kind.FIELD: { const parentType = typeInfo.getParentType(); const fieldDef = typeInfo.getFieldDef(); if (parentType && fieldDef) { const argsString = fieldDef.args.length > 0 ? `(${fieldDef.args .map((a) => `${a.name}: ${a.type}`) .join(", ")})` : ""; const isClientType = parentType.clientSchema && parentType.clientSchema.localFields && parentType.clientSchema.localFields.includes(fieldDef.name); const isResolvedLocally = node.directives && node.directives.some((directive) => directive.name.value === "client"); const content = [ [ `\`\`\`graphql`, `${parentType}.${fieldDef.name}${argsString}: ${fieldDef.type}`, `\`\`\``, ].join("\n"), ]; const info = []; if (isClientType) { info.push("`Client-Only Field`"); } if (isResolvedLocally) { info.push("`Resolved locally`"); } if (info.length !== 0) { content.push(info.join(" ")); } if (fieldDef.description) { content.push(fieldDef.description); } return { contents: content.join("\n\n---\n\n"), range: (0, source_1.rangeForASTNode)((0, graphql_2.highlightNodeForNode)(node)), }; } break; } case graphql_1.Kind.NAMED_TYPE: { const type = project.schema.getType(node.name.value); if (!type) break; const content = [[`\`\`\`graphql`, `${type}`, `\`\`\``].join("\n")]; if (type.description) { content.push(type.description); } return { contents: content.join("\n\n---\n\n"), range: (0, source_1.rangeForASTNode)((0, graphql_2.highlightNodeForNode)(node)), }; } case graphql_1.Kind.ARGUMENT: { const argumentNode = typeInfo.getArgument(); const content = [ [ `\`\`\`graphql`, `${argumentNode.name}: ${argumentNode.type}`, `\`\`\``, ].join("\n"), ]; if (argumentNode.description) { content.push(argumentNode.description); } return { contents: content.join("\n\n---\n\n"), range: (0, source_1.rangeForASTNode)((0, graphql_2.highlightNodeForNode)(node)), }; } case graphql_1.Kind.DIRECTIVE: { const directiveNode = typeInfo.getDirective(); if (!directiveNode) break; const argsString = directiveNode.args.length > 0 ? `(${directiveNode.args .map((a) => `${a.name}: ${a.type}`) .join(", ")})` : ""; const content = [ [ `\`\`\`graphql`, `@${directiveNode.name}${argsString}`, `\`\`\``, ].join("\n"), ]; if (directiveNode.description) { content.push(directiveNode.description); } return { contents: content.join("\n\n---\n\n"), range: (0, source_1.rangeForASTNode)((0, graphql_2.highlightNodeForNode)(node)), }; } } } return null; } async provideDefinition(uri, position, _token) { const project = this.workspace.projectForFile(uri); if (!(project && project instanceof client_1.GraphQLClientProject)) return null; const document = project.documentAt(uri, position); if (!(document && document.ast)) return null; if (!project.schema) return null; const positionInDocument = (0, source_1.positionFromPositionInContainingDocument)(document.source, position); const nodeAndTypeInfo = (0, source_1.getASTNodeAndTypeInfoAtPosition)(document.source, positionInDocument, document.ast, project.schema); if (nodeAndTypeInfo) { const [node, typeInfo] = nodeAndTypeInfo; switch (node.kind) { case graphql_1.Kind.FRAGMENT_SPREAD: { const fragmentName = node.name.value; const fragment = project.fragments[fragmentName]; if (fragment && fragment.loc) { return locationForASTNode(fragment); } break; } case graphql_1.Kind.FIELD: { const fieldDef = typeInfo.getFieldDef(); if (!(fieldDef && fieldDef.astNode && fieldDef.astNode.loc)) break; return locationForASTNode(fieldDef.astNode); } case graphql_1.Kind.NAMED_TYPE: { const type = (0, graphql_1.typeFromAST)(project.schema, node); if (!(type && type.astNode && type.astNode.loc)) break; return locationForASTNode(type.astNode); } case graphql_1.Kind.DIRECTIVE: { const directive = project.schema.getDirective(node.name.value); if (!(directive && directive.astNode && directive.astNode.loc)) break; return locationForASTNode(directive.astNode); } } } return null; } async provideReferences(uri, position, _context, _token) { const project = this.workspace.projectForFile(uri); if (!project) return null; const document = project.documentAt(uri, position); if (!(document && document.ast)) return null; if (!project.schema) return null; const positionInDocument = (0, source_1.positionFromPositionInContainingDocument)(document.source, position); const nodeAndTypeInfo = (0, source_1.getASTNodeAndTypeInfoAtPosition)(document.source, positionInDocument, document.ast, project.schema); if (nodeAndTypeInfo) { const [node, typeInfo] = nodeAndTypeInfo; switch (node.kind) { case graphql_1.Kind.FRAGMENT_DEFINITION: { if (!(0, client_1.isClientProject)(project)) return null; const fragmentName = node.name.value; return project .fragmentSpreadsForFragment(fragmentName) .map((fragmentSpread) => locationForASTNode(fragmentSpread)) .filter(apollo_tools_1.isNotNullOrUndefined); } case graphql_1.Kind.FIELD_DEFINITION: { if (!(0, client_1.isClientProject)(project)) return null; const offset = (0, source_1.positionToOffset)(document.source, positionInDocument); let parent = null; (0, graphql_1.visit)(document.ast, { enter(node) { if (node.loc && node.loc.start <= offset && offset <= node.loc.end && (node.kind === graphql_1.Kind.OBJECT_TYPE_DEFINITION || node.kind === graphql_1.Kind.OBJECT_TYPE_EXTENSION || node.kind === graphql_1.Kind.INTERFACE_TYPE_DEFINITION || node.kind === graphql_1.Kind.INTERFACE_TYPE_EXTENSION || node.kind === graphql_1.Kind.INPUT_OBJECT_TYPE_DEFINITION || node.kind === graphql_1.Kind.INPUT_OBJECT_TYPE_EXTENSION || node.kind === graphql_1.Kind.ENUM_TYPE_DEFINITION || node.kind === graphql_1.Kind.ENUM_TYPE_EXTENSION)) { parent = node; } return; }, }); return project .getOperationFieldsFromFieldDefinition(node.name.value, parent) .map((fieldNode) => locationForASTNode(fieldNode)) .filter(apollo_tools_1.isNotNullOrUndefined); } } } return null; } async provideDocumentSymbol(uri, _token) { const project = this.workspace.projectForFile(uri); if (!project) return []; const definitions = project.definitionsAt(uri); const symbols = []; for (const definition of definitions) { if ((0, graphql_1.isExecutableDefinitionNode)(definition)) { if (!definition.name) continue; const location = locationForASTNode(definition); if (!location) continue; symbols.push({ name: definition.name.value, kind: vscode_languageserver_1.SymbolKind.Function, range: (0, source_1.rangeForASTNode)(definition), selectionRange: (0, source_1.rangeForASTNode)((0, graphql_2.highlightNodeForNode)(definition)), }); } else if ((0, graphql_1.isTypeSystemDefinitionNode)(definition) || (0, graphql_1.isTypeSystemExtensionNode)(definition)) { if (definition.kind === graphql_1.Kind.SCHEMA_DEFINITION || definition.kind === graphql_1.Kind.SCHEMA_EXTENSION) { continue; } symbols.push({ name: definition.name.value, kind: vscode_languageserver_1.SymbolKind.Class, range: (0, source_1.rangeForASTNode)(definition), selectionRange: (0, source_1.rangeForASTNode)((0, graphql_2.highlightNodeForNode)(definition)), children: definition.kind === graphql_1.Kind.OBJECT_TYPE_DEFINITION || definition.kind === graphql_1.Kind.OBJECT_TYPE_EXTENSION ? (definition.fields || []).map(symbolForFieldDefinition) : undefined, }); } } return symbols; } async provideWorkspaceSymbol(query, _token) { const symbols = []; for (const project of this.workspace.projects) { for (const definition of project.definitions) { if ((0, graphql_1.isExecutableDefinitionNode)(definition)) { if (!definition.name) continue; const location = locationForASTNode(definition); if (!location) continue; symbols.push({ name: definition.name.value, kind: vscode_languageserver_1.SymbolKind.Function, location, }); } } } return symbols; } async provideCodeLenses(uri, _token) { const project = this.workspace.projectForFile(uri); if (!(project && project instanceof client_1.GraphQLClientProject)) return []; await project.whenReady; const documents = project.documentsAt(uri); if (!documents) return []; let codeLenses = []; for (const document of documents) { if (!document.ast) continue; for (const definition of document.ast.definitions) { if (definition.kind === graphql_1.Kind.OPERATION_DEFINITION) { } else if (definition.kind === graphql_1.Kind.FRAGMENT_DEFINITION) { } } } return codeLenses; } async provideCodeAction(uri, range, _token) { function isPositionLessThanOrEqual(a, b) { return a.line !== b.line ? a.line < b.line : a.character <= b.character; } const project = this.workspace.projectForFile(uri); if (!(project && project instanceof client_1.GraphQLClientProject && project.diagnosticSet)) return []; await project.whenReady; const documents = project.documentsAt(uri); if (!documents) return []; const errors = new Set(); for (const [diagnosticUri, diagnostics,] of project.diagnosticSet.entries()) { if (diagnosticUri !== uri) continue; for (const diagnostic of diagnostics) { if (diagnostics_1.GraphQLDiagnostic.is(diagnostic) && isPositionLessThanOrEqual(range.start, diagnostic.range.end) && isPositionLessThanOrEqual(diagnostic.range.start, range.end)) { errors.add(diagnostic.error); } } } const result = []; for (const error of errors) { const { extensions } = error; if (!extensions || !extensions.codeAction) continue; const { message, edits } = extensions.codeAction; const codeAction = vscode_languageserver_1.CodeAction.create(message, { changes: { [uri]: edits } }, vscode_languageserver_1.CodeActionKind.QuickFix); result.push(codeAction); } return result; } } exports.GraphQLLanguageProvider = GraphQLLanguageProvider; //# sourceMappingURL=languageProvider.js.map