diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..30300b16 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "javascript.validate.enable": false +} \ No newline at end of file diff --git a/README.md b/README.md index fb42de09..7b284bed 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,6 @@ For each transport, there is a slight difference in JSON message format, especia | -------------------:|------------------------------|-----------------------------------| | Diagnostics | `getDiagnostics` | `textDocument/publishDiagnostics` | | Autocompletion | `getAutocompleteSuggestions` | `textDocument/completion` | -| Outline | `getOutline` | Not supported yet | +| Outline | `getOutline` | `textDocument/documentSymbol` | | Go-to definition | `getDefinition` | Not supported yet | | File Events | Not supported yet | `didOpen/didClose/didSave/didChange` events | diff --git a/packages/interface/src/GraphQLLanguageService.js b/packages/interface/src/GraphQLLanguageService.js index 8f96f362..a515ed49 100644 --- a/packages/interface/src/GraphQLLanguageService.js +++ b/packages/interface/src/GraphQLLanguageService.js @@ -21,6 +21,7 @@ import type { GraphQLCache, GraphQLConfig, GraphQLProjectConfig, + Outline, Uri, } from 'graphql-language-service-types'; import type {Position} from 'graphql-language-service-utils'; @@ -46,6 +47,7 @@ import { getDefinitionQueryResultForFragmentSpread, getDefinitionQueryResultForDefinitionNode, } from './getDefinition'; +import {getOutline} from './getOutline'; import {getASTNodeAtPosition} from 'graphql-language-service-utils'; export class GraphQLLanguageService { @@ -244,4 +246,8 @@ export class GraphQLLanguageService { return result; } + + async getOutline(query: string): Promise { + return getOutline(query); + } } diff --git a/packages/interface/src/getOutline.js b/packages/interface/src/getOutline.js index 9dd894a5..79d05af0 100644 --- a/packages/interface/src/getOutline.js +++ b/packages/interface/src/getOutline.js @@ -56,6 +56,7 @@ function outlineTreeConverter(docText: string): OutlineTreeConverterType { representativeName: node.name, startPosition: offsetToPosition(docText, node.loc.start), endPosition: offsetToPosition(docText, node.loc.end), + kind: node.kind, children: node.selectionSet || [], }); return { diff --git a/packages/server/src/MessageProcessor.js b/packages/server/src/MessageProcessor.js index 4fd5eb03..0382fcc2 100644 --- a/packages/server/src/MessageProcessor.js +++ b/packages/server/src/MessageProcessor.js @@ -35,6 +35,9 @@ import { InitializeResult, Location, PublishDiagnosticsParams, + DocumentSymbolParams, + SymbolInformation, + SymbolKind, } from 'vscode-languageserver'; import {getGraphQLCache} from './GraphQLCache'; @@ -48,6 +51,13 @@ type CachedDocumentType = { contents: Array, }; +const KIND_TO_SYMBOL_KIND = { + Field: SymbolKind.Field, + OperationDefinition: SymbolKind.Class, + FragmentDefinition: SymbolKind.Class, + FragmentSpread: SymbolKind.Struct, +}; + export class MessageProcessor { _graphQLCache: GraphQLCache; _languageService: GraphQLLanguageService; @@ -79,6 +89,7 @@ export class MessageProcessor { const serverCapabilities: ServerCapabilities = { capabilities: { completionProvider: {resolveProvider: true}, + documentSymbolProvider: true, definitionProvider: true, textDocumentSync: 1, }, @@ -416,6 +427,52 @@ export class MessageProcessor { return formatted; } + async handleDocumentSymbolRequest( + params: DocumentSymbolParams.type, + ): Promise> { + if (!this._isInitialized) { + return []; + } + + if (!params || !params.textDocument) { + throw new Error('`textDocument` argument is required.'); + } + + const textDocument = params.textDocument; + const cachedDocument = this._getCachedDocument(textDocument.uri); + if (!cachedDocument) { + throw new Error('A cached document cannot be found.'); + } + + const outline = await this._languageService.getOutline( + cachedDocument.contents[0].query, + ); + if (!outline) { + return []; + } + + const output: Array = []; + const input = outline.outlineTrees.map(tree => [null, tree]); + while (input.length > 0) { + const [parent, tree] = input.pop(); + output.push({ + name: tree.representativeName, + kind: KIND_TO_SYMBOL_KIND[tree.kind], + location: { + uri: textDocument.uri, + range: { + start: tree.startPosition, + end: tree.endPosition, + }, + }, + containerName: parent ? parent.representativeName : undefined, + }); + input.push(...tree.children.map(child => [tree, child])); + } + + return output; + } + _isRelayCompatMode(query: string): boolean { return ( query.indexOf('RelayCompat') !== -1 || diff --git a/packages/server/src/__tests__/MessageProcessor-test.js b/packages/server/src/__tests__/MessageProcessor-test.js index 109503a8..ff777e7d 100644 --- a/packages/server/src/__tests__/MessageProcessor-test.js +++ b/packages/server/src/__tests__/MessageProcessor-test.js @@ -10,6 +10,7 @@ import {expect} from 'chai'; import {Position, Range} from 'graphql-language-service-utils'; +import {SymbolKind} from 'vscode-languageserver-types'; import {beforeEach, describe, it} from 'mocha'; import {MessageProcessor} from '../MessageProcessor'; @@ -51,6 +52,19 @@ describe('MessageProcessor', () => { getDiagnostics: (query, uri) => { return []; }, + getOutline: query => { + return { + outlineTrees: [ + { + representativeName: 'item', + kind: 'Field', + startPosition: {line: 1, character: 2}, + endPosition: {line: 1, character: 4}, + children: [], + }, + ], + }; + }, }; }); messageProcessor._isInitialized = true; @@ -164,4 +178,39 @@ describe('MessageProcessor', () => { const result = await messageProcessor.handleDefinitionRequest(test); expect(result[0].uri).to.equal(`file://${queryDir}/testFragment.graphql`); }); + + it('runs document symbol requests', async () => { + const validQuery = ` + { + hero(episode: EMPIRE){ + ...testFragment + } + } + `; + + const newDocument = { + textDocument: { + text: validQuery, + uri: `${queryDir}/test3.graphql`, + version: 0, + }, + }; + + await messageProcessor.handleDidOpenOrSaveNotification(newDocument); + + const test = { + textDocument: newDocument.textDocument, + }; + + const result = await messageProcessor.handleDocumentSymbolRequest(test); + + expect(result).to.not.be.undefined; + expect(result.length).to.equal(1); + expect(result[0].name).to.equal('item'); + expect(result[0].kind).to.equal(SymbolKind.Field); + expect(result[0].location.range).to.deep.equal({ + start: {line: 1, character: 2}, + end: {line: 1, character: 4}, + }); + }); }); diff --git a/packages/server/src/startServer.js b/packages/server/src/startServer.js index 3c614e7f..98c25959 100644 --- a/packages/server/src/startServer.js +++ b/packages/server/src/startServer.js @@ -34,6 +34,7 @@ import { ExitNotification, InitializeRequest, PublishDiagnosticsNotification, + DocumentSymbolRequest, ShutdownRequest, } from 'vscode-languageserver'; @@ -168,4 +169,7 @@ function addHandlers( connection.onRequest(DefinitionRequest.type, params => messageProcessor.handleDefinitionRequest(params), ); + connection.onRequest(DocumentSymbolRequest.type, (params, token) => + messageProcessor.handleDocumentSymbolRequest(params), + ); } diff --git a/packages/types/src/index.js b/packages/types/src/index.js index 193f9850..6db35d6d 100644 --- a/packages/types/src/index.js +++ b/packages/types/src/index.js @@ -259,6 +259,7 @@ export type OutlineTree = { tokenizedText?: TokenizedText, representativeName?: string, + kind: string, startPosition: Position, endPosition?: Position, children: Array,