diff --git a/packages/apollo-language-server/src/languageProvider.ts b/packages/apollo-language-server/src/languageProvider.ts index 48bd7e9ca9..0deca345fa 100644 --- a/packages/apollo-language-server/src/languageProvider.ts +++ b/packages/apollo-language-server/src/languageProvider.ts @@ -6,11 +6,12 @@ import { Hover, Definition, CodeLens, - Command, ReferenceContext, - InsertTextFormat + InsertTextFormat, + DocumentSymbol, + SymbolKind, + SymbolInformation } from "vscode-languageserver"; -import Uri from "vscode-uri"; // should eventually be moved into this package, since we're overriding a lot of the existing behavior here import { getAutocompleteSuggestions } from "@apollographql/graphql-language-service-interface"; @@ -46,16 +47,15 @@ import { isNonNullType, ASTNode, FieldDefinitionNode, - FieldNode, visit, - BREAK, - ObjectTypeDefinitionNode + isExecutableDefinitionNode, + isTypeSystemDefinitionNode, + isTypeSystemExtensionNode } from "graphql"; import { highlightNodeForNode } from "./utilities/graphql"; import { GraphQLClientProject, isClientProject } from "./project/client"; import { isNotNullOrUndefined } from "@apollographql/apollo-tools"; -import { notDeepEqual } from "assert"; function hasFields(type: GraphQLType): boolean { return ( @@ -79,6 +79,17 @@ function locationForASTNode(node: ASTNode): Location | null { return Location.create(uri, rangeForASTNode(node)); } +function symbolForFieldDefinition( + definition: FieldDefinitionNode +): DocumentSymbol { + return { + name: definition.name.value, + kind: SymbolKind.Field, + range: rangeForASTNode(definition), + selectionRange: rangeForASTNode(definition) + }; +} + export class GraphQLLanguageProvider { constructor(public workspace: GraphQLWorkspace) {} @@ -409,6 +420,77 @@ ${argumentNode.description ? argumentNode.description : ""} return null; } + async provideDocumentSymbol( + uri: DocumentUri, + _token: CancellationToken + ): Promise { + const project = this.workspace.projectForFile(uri); + if (!project) return []; + + const definitions = project.definitionsAt(uri); + + const symbols: DocumentSymbol[] = []; + + for (const definition of definitions) { + if (isExecutableDefinitionNode(definition)) { + if (!definition.name) continue; + const location = locationForASTNode(definition); + if (!location) continue; + symbols.push({ + name: definition.name.value, + kind: SymbolKind.Function, + range: rangeForASTNode(definition), + selectionRange: rangeForASTNode(highlightNodeForNode(definition)) + }); + } else if ( + isTypeSystemDefinitionNode(definition) || + isTypeSystemExtensionNode(definition) + ) { + if ( + definition.kind === Kind.SCHEMA_DEFINITION || + definition.kind === Kind.SCHEMA_EXTENSION + ) { + continue; + } + symbols.push({ + name: definition.name.value, + kind: SymbolKind.Class, + range: rangeForASTNode(definition), + selectionRange: rangeForASTNode(highlightNodeForNode(definition)), + children: + definition.kind === Kind.OBJECT_TYPE_DEFINITION || + definition.kind === Kind.OBJECT_TYPE_EXTENSION + ? (definition.fields || []).map(symbolForFieldDefinition) + : undefined + }); + } + } + + return symbols; + } + + async provideWorkspaceSymbol( + query: string, + _token: CancellationToken + ): Promise { + const symbols: SymbolInformation[] = []; + for (const project of this.workspace.projects) { + for (const definition of project.definitions) { + if (isExecutableDefinitionNode(definition)) { + if (!definition.name) continue; + const location = locationForASTNode(definition); + if (!location) continue; + symbols.push({ + name: definition.name.value, + kind: SymbolKind.Function, + location + }); + } + } + } + return symbols; + } + async provideCodeLenses( uri: DocumentUri, _token: CancellationToken diff --git a/packages/apollo-language-server/src/server.ts b/packages/apollo-language-server/src/server.ts index 1e953357df..a00a2a94cc 100644 --- a/packages/apollo-language-server/src/server.ts +++ b/packages/apollo-language-server/src/server.ts @@ -4,7 +4,8 @@ import { ProposedFeatures, TextDocuments, FileChangeType, - NotificationType + NotificationType, + ServerCapabilities } from "vscode-languageserver"; import { QuickPickItem } from "vscode"; import { GraphQLWorkspace } from "./workspace"; @@ -72,12 +73,14 @@ connection.onInitialize(async params => { return { capabilities: { hoverProvider: true, - definitionProvider: true, - referencesProvider: true, completionProvider: { resolveProvider: false, triggerCharacters: ["..."] }, + definitionProvider: true, + referencesProvider: true, + documentSymbolProvider: true, + workspaceSymbolProvider: true, codeLensProvider: { resolveProvider: false }, @@ -88,7 +91,7 @@ connection.onInitialize(async params => { ] }, textDocumentSync: documents.syncKind - } + } as ServerCapabilities }; }); @@ -194,6 +197,14 @@ connection.onReferences((params, token) => ) ); +connection.onDocumentSymbol((params, token) => + languageProvider.provideDocumentSymbol(params.textDocument.uri, token) +); + +connection.onWorkspaceSymbol((params, token) => + languageProvider.provideWorkspaceSymbol(params.query, token) +); + connection.onCompletion((params, token) => languageProvider.provideCompletionItems( params.textDocument.uri, diff --git a/packages/apollo-language-server/src/typings/graphql.d.ts b/packages/apollo-language-server/src/typings/graphql.d.ts index 5837001eb3..19a145b2ea 100644 --- a/packages/apollo-language-server/src/typings/graphql.d.ts +++ b/packages/apollo-language-server/src/typings/graphql.d.ts @@ -2,12 +2,16 @@ import { ASTNode, TypeSystemDefinitionNode, TypeSystemExtensionNode, - FragmentDefinitionNode + FragmentDefinitionNode, + OperationDefinitionNode } from "graphql"; // FIXME: We should add proper type guards for these predicate functions // to `@types/graphql`. declare module "graphql/language/predicates" { + function isExecutableDefinitionNode( + node: ASTNode + ): node is OperationDefinitionNode | FragmentDefinitionNode; function isTypeSystemDefinitionNode( node: ASTNode ): node is TypeSystemDefinitionNode;