122 lines
2.9 KiB
Plaintext
122 lines
2.9 KiB
Plaintext
/**
|
|
* Copyright (c) Facebook, Inc.
|
|
* All rights reserved.
|
|
*
|
|
* This source code is licensed under the license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @flow
|
|
*/
|
|
|
|
import type {
|
|
Outline,
|
|
TextToken,
|
|
TokenKind,
|
|
} from 'graphql-language-service-types';
|
|
|
|
import {Kind, parse, visit} from 'graphql';
|
|
import {offsetToPosition} from 'graphql-language-service-utils';
|
|
|
|
const {INLINE_FRAGMENT} = Kind;
|
|
|
|
const OUTLINEABLE_KINDS = {
|
|
Field: true,
|
|
OperationDefinition: true,
|
|
Document: true,
|
|
SelectionSet: true,
|
|
Name: true,
|
|
FragmentDefinition: true,
|
|
FragmentSpread: true,
|
|
InlineFragment: true,
|
|
};
|
|
|
|
type OutlineTreeConverterType = {[name: string]: Function};
|
|
|
|
export function getOutline(queryText: string): ?Outline {
|
|
let ast;
|
|
try {
|
|
ast = parse(queryText);
|
|
} catch (error) {
|
|
return null;
|
|
}
|
|
|
|
const visitorFns = outlineTreeConverter(queryText);
|
|
const outlineTrees = visit(ast, {
|
|
leave(node) {
|
|
if (
|
|
OUTLINEABLE_KINDS.hasOwnProperty(node.kind) &&
|
|
visitorFns[node.kind]
|
|
) {
|
|
return visitorFns[node.kind](node);
|
|
}
|
|
return null;
|
|
},
|
|
});
|
|
return {outlineTrees};
|
|
}
|
|
|
|
function outlineTreeConverter(docText: string): OutlineTreeConverterType {
|
|
const meta = node => ({
|
|
representativeName: node.name,
|
|
startPosition: offsetToPosition(docText, node.loc.start),
|
|
endPosition: offsetToPosition(docText, node.loc.end),
|
|
children: node.selectionSet || [],
|
|
});
|
|
return {
|
|
Field: node => {
|
|
const tokenizedText = node.alias
|
|
? [buildToken('plain', node.alias), buildToken('plain', ': ')]
|
|
: [];
|
|
tokenizedText.push(buildToken('plain', node.name));
|
|
return {tokenizedText, ...meta(node)};
|
|
},
|
|
OperationDefinition: node => ({
|
|
tokenizedText: [
|
|
buildToken('keyword', node.operation),
|
|
buildToken('whitespace', ' '),
|
|
buildToken('class-name', node.name),
|
|
],
|
|
...meta(node),
|
|
}),
|
|
Document: node => node.definitions,
|
|
SelectionSet: node =>
|
|
concatMap(node.selections, child => {
|
|
return child.kind === INLINE_FRAGMENT ? child.selectionSet : child;
|
|
}),
|
|
Name: node => node.value,
|
|
FragmentDefinition: node => ({
|
|
tokenizedText: [
|
|
buildToken('keyword', 'fragment'),
|
|
buildToken('whitespace', ' '),
|
|
buildToken('class-name', node.name),
|
|
],
|
|
...meta(node),
|
|
}),
|
|
FragmentSpread: node => ({
|
|
tokenizedText: [
|
|
buildToken('plain', '...'),
|
|
buildToken('class-name', node.name),
|
|
],
|
|
...meta(node),
|
|
}),
|
|
InlineFragment: node => node.selectionSet,
|
|
};
|
|
}
|
|
|
|
function buildToken(kind: TokenKind, value: string): TextToken {
|
|
return {kind, value};
|
|
}
|
|
|
|
function concatMap(arr: Array<any>, fn: Function): Array<any> {
|
|
const res = [];
|
|
for (let i = 0; i < arr.length; i++) {
|
|
const x = fn(arr[i], i);
|
|
if (Array.isArray(x)) {
|
|
res.push(...x);
|
|
} else {
|
|
res.push(x);
|
|
}
|
|
}
|
|
return res;
|
|
}
|