graphql-language-service
Version:
The official, runtime independent Language Service for GraphQL
1,068 lines (986 loc) • 31 kB
text/typescript
/**
* Copyright (c) 2021 GraphQL Contributors
* All rights reserved.
*
* This source code is licensed under the license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import {
FragmentDefinitionNode,
GraphQLDirective,
GraphQLSchema,
GraphQLType,
GraphQLCompositeType,
GraphQLEnumValue,
GraphQLField,
GraphQLFieldMap,
GraphQLNamedType,
isInterfaceType,
GraphQLInterfaceType,
GraphQLObjectType,
Kind,
DirectiveLocation,
GraphQLArgument,
// isNonNullType,
isScalarType,
isObjectType,
isUnionType,
isEnumType,
isInputObjectType,
isOutputType,
GraphQLBoolean,
GraphQLEnumType,
GraphQLInputObjectType,
SchemaMetaFieldDef,
TypeMetaFieldDef,
TypeNameMetaFieldDef,
assertAbstractType,
doTypesOverlap,
getNamedType,
isAbstractType,
isCompositeType,
isInputType,
visit,
parse,
} from 'graphql';
import {
CompletionItem,
AllTypeInfo,
IPosition,
CompletionItemKind,
InsertTextFormat,
} from '../types';
import type {
ContextToken,
State,
RuleKind,
ContextTokenForCodeMirror,
} from '../parser';
import {
getTypeInfo,
runOnlineParser,
RuleKinds,
getContextAtPosition,
getDefinitionState,
GraphQLDocumentMode,
} from '../parser';
import {
hintList,
objectValues,
getInputInsertText,
getFieldInsertText,
getInsertText,
} from './autocompleteUtils';
import { InsertTextMode } from 'vscode-languageserver-types';
export { runOnlineParser, getTypeInfo };
export const SuggestionCommand = {
command: 'editor.action.triggerSuggest',
title: 'Suggestions',
};
const collectFragmentDefs = (op: string | undefined) => {
const externalFragments: FragmentDefinitionNode[] = [];
if (op) {
try {
visit(parse(op), {
FragmentDefinition(def) {
externalFragments.push(def);
},
});
} catch {
return [];
}
}
return externalFragments;
};
export type AutocompleteSuggestionOptions = {
/**
* EXPERIMENTAL: Automatically fill required leaf nodes recursively
* upon triggering code completion events.
*
*
* - [x] fills required nodes
* - [x] automatically expands relay-style node/edge fields
* - [ ] automatically jumps to first required argument field
* - then, continues to prompt for required argument fields
* - (fixing this will make it non-experimental)
* - when it runs out of arguments, or you choose `{` as a completion option
* that appears when all required arguments are supplied, the argument
* selection closes `)` and the leaf field expands again `{ \n| }`
*/
fillLeafsOnComplete?: boolean;
uri?: string;
mode?: GraphQLDocumentMode;
};
type InternalAutocompleteOptions = AutocompleteSuggestionOptions & {
schema?: GraphQLSchema;
};
/**
* Given GraphQLSchema, queryText, and context of the current position within
* the source text, provide a list of typeahead entries.
*/
export function getAutocompleteSuggestions(
schema: GraphQLSchema,
queryText: string,
cursor: IPosition,
contextToken?: ContextTokenForCodeMirror,
fragmentDefs?: FragmentDefinitionNode[] | string,
options?: AutocompleteSuggestionOptions,
): Array<CompletionItem> {
const opts = {
...options,
schema,
} as InternalAutocompleteOptions;
const context = getContextAtPosition(
queryText,
cursor,
schema,
contextToken,
options,
);
if (!context) {
return [];
}
const { state, typeInfo, mode, token } = context;
const { kind, step, prevState } = state;
// Definition kinds
if (kind === RuleKinds.DOCUMENT) {
if (mode === GraphQLDocumentMode.TYPE_SYSTEM) {
return getSuggestionsForTypeSystemDefinitions(token);
}
if (mode === GraphQLDocumentMode.EXECUTABLE) {
return getSuggestionsForExecutableDefinitions(token);
}
return getSuggestionsForUnknownDocumentMode(token);
}
if (kind === RuleKinds.EXTEND_DEF) {
return getSuggestionsForExtensionDefinitions(token);
}
if (
prevState?.prevState?.kind === RuleKinds.EXTENSION_DEFINITION &&
state.name
) {
return hintList(token, []);
}
// extend scalar
if (prevState?.kind === Kind.SCALAR_TYPE_EXTENSION) {
return hintList(
token,
Object.values(schema.getTypeMap())
.filter(isScalarType)
.map(type => ({
label: type.name,
kind: CompletionItemKind.Function,
})),
);
}
// extend object type
if (prevState?.kind === Kind.OBJECT_TYPE_EXTENSION) {
return hintList(
token,
Object.values(schema.getTypeMap())
.filter(type => isObjectType(type) && !type.name.startsWith('__'))
.map(type => ({
label: type.name,
kind: CompletionItemKind.Function,
})),
);
}
// extend interface type
if (prevState?.kind === Kind.INTERFACE_TYPE_EXTENSION) {
return hintList(
token,
Object.values(schema.getTypeMap())
.filter(isInterfaceType)
.map(type => ({
label: type.name,
kind: CompletionItemKind.Function,
})),
);
}
// extend union type
if (prevState?.kind === Kind.UNION_TYPE_EXTENSION) {
return hintList(
token,
Object.values(schema.getTypeMap())
.filter(isUnionType)
.map(type => ({
label: type.name,
kind: CompletionItemKind.Function,
})),
);
}
// extend enum type
if (prevState?.kind === Kind.ENUM_TYPE_EXTENSION) {
return hintList(
token,
Object.values(schema.getTypeMap())
.filter(type => isEnumType(type) && !type.name.startsWith('__'))
.map(type => ({
label: type.name,
kind: CompletionItemKind.Function,
})),
);
}
// extend input object type
if (prevState?.kind === Kind.INPUT_OBJECT_TYPE_EXTENSION) {
return hintList(
token,
Object.values(schema.getTypeMap())
.filter(isInputObjectType)
.map(type => ({
label: type.name,
kind: CompletionItemKind.Function,
})),
);
}
if (
kind === RuleKinds.IMPLEMENTS ||
(kind === RuleKinds.NAMED_TYPE && prevState?.kind === RuleKinds.IMPLEMENTS)
) {
return getSuggestionsForImplements(
token,
state,
schema,
queryText,
typeInfo,
);
}
// Field names
if (
kind === RuleKinds.SELECTION_SET ||
kind === RuleKinds.FIELD ||
kind === RuleKinds.ALIASED_FIELD
) {
return getSuggestionsForFieldNames(token, typeInfo, opts);
}
// Argument names
if (
kind === RuleKinds.ARGUMENTS ||
(kind === RuleKinds.ARGUMENT && step === 0)
) {
const { argDefs } = typeInfo;
if (argDefs) {
return hintList(
token,
argDefs.map(
(argDef: GraphQLArgument): CompletionItem => ({
label: argDef.name,
insertText: getInputInsertText(argDef.name + ': ', argDef.type),
insertTextMode: InsertTextMode.adjustIndentation,
insertTextFormat: InsertTextFormat.Snippet,
command: SuggestionCommand,
labelDetails: {
detail: ' ' + String(argDef.type),
},
documentation: argDef.description ?? undefined,
kind: CompletionItemKind.Variable,
type: argDef.type,
}),
),
);
}
}
// Input Object fields
if (
(kind === RuleKinds.OBJECT_VALUE ||
(kind === RuleKinds.OBJECT_FIELD && step === 0)) &&
typeInfo.objectFieldDefs
) {
const objectFields = objectValues(typeInfo.objectFieldDefs);
const completionKind =
kind === RuleKinds.OBJECT_VALUE
? CompletionItemKind.Value
: CompletionItemKind.Field;
return hintList(
token,
objectFields.map(field => ({
label: field.name,
detail: String(field.type),
documentation: field?.description ?? undefined,
kind: completionKind,
type: field.type,
insertText: getInputInsertText(field.name + ': ', field.type),
insertTextMode: InsertTextMode.adjustIndentation,
insertTextFormat: InsertTextFormat.Snippet,
command: SuggestionCommand,
})),
);
}
// Input values: Enum and Boolean
if (
kind === RuleKinds.ENUM_VALUE ||
(kind === RuleKinds.LIST_VALUE && step === 1) ||
(kind === RuleKinds.OBJECT_FIELD && step === 2) ||
(kind === RuleKinds.ARGUMENT && step === 2)
) {
return getSuggestionsForInputValues(token, typeInfo, queryText, schema);
}
// complete for all variables available in the query scoped to this
if (kind === RuleKinds.VARIABLE && step === 1) {
const namedInputType = getNamedType(typeInfo.inputType!);
const variableDefinitions = getVariableCompletions(
queryText,
schema,
token,
);
return hintList(
token,
variableDefinitions.filter(v => v.detail === namedInputType?.name),
);
}
// Fragment type conditions
if (
(kind === RuleKinds.TYPE_CONDITION && step === 1) ||
(kind === RuleKinds.NAMED_TYPE &&
prevState != null &&
prevState.kind === RuleKinds.TYPE_CONDITION)
) {
return getSuggestionsForFragmentTypeConditions(
token,
typeInfo,
schema,
kind,
);
}
// Fragment spread names
if (kind === RuleKinds.FRAGMENT_SPREAD && step === 1) {
return getSuggestionsForFragmentSpread(
token,
typeInfo,
schema,
queryText,
Array.isArray(fragmentDefs)
? fragmentDefs
: collectFragmentDefs(fragmentDefs),
);
}
const unwrappedState = unwrapType(state);
if (unwrappedState.kind === RuleKinds.FIELD_DEF) {
return hintList(
token,
Object.values(schema.getTypeMap())
.filter(type => isOutputType(type) && !type.name.startsWith('__'))
.map(type => ({
label: type.name,
kind: CompletionItemKind.Function,
insertText: options?.fillLeafsOnComplete
? type.name + '\n'
: type.name,
insertTextMode: InsertTextMode.adjustIndentation,
})),
);
}
if (unwrappedState.kind === RuleKinds.INPUT_VALUE_DEF && step === 2) {
return hintList(
token,
Object.values(schema.getTypeMap())
.filter(type => isInputType(type) && !type.name.startsWith('__'))
.map(type => ({
label: type.name,
kind: CompletionItemKind.Function,
insertText: options?.fillLeafsOnComplete
? type.name + '\n$1'
: type.name,
insertTextMode: InsertTextMode.adjustIndentation,
insertTextFormat: InsertTextFormat.Snippet,
})),
);
}
// Variable definition types
if (
(kind === RuleKinds.VARIABLE_DEFINITION && step === 2) ||
(kind === RuleKinds.LIST_TYPE && step === 1) ||
(kind === RuleKinds.NAMED_TYPE &&
prevState &&
(prevState.kind === RuleKinds.VARIABLE_DEFINITION ||
prevState.kind === RuleKinds.LIST_TYPE ||
prevState.kind === RuleKinds.NON_NULL_TYPE))
) {
return getSuggestionsForVariableDefinition(token, schema, kind);
}
// Directive names
if (kind === RuleKinds.DIRECTIVE) {
return getSuggestionsForDirective(token, state, schema, kind);
}
if (kind === RuleKinds.DIRECTIVE_DEF) {
return getSuggestionsForDirectiveArguments(token, state, schema, kind);
}
return [];
}
const typeSystemCompletionItems: CompletionItem[] = [
{ label: 'type', kind: CompletionItemKind.Function },
{ label: 'interface', kind: CompletionItemKind.Function },
{ label: 'union', kind: CompletionItemKind.Function },
{ label: 'input', kind: CompletionItemKind.Function },
{ label: 'scalar', kind: CompletionItemKind.Function },
{ label: 'schema', kind: CompletionItemKind.Function },
];
const executableCompletionItems: CompletionItem[] = [
{ label: 'query', kind: CompletionItemKind.Function },
{ label: 'mutation', kind: CompletionItemKind.Function },
{ label: 'subscription', kind: CompletionItemKind.Function },
{ label: 'fragment', kind: CompletionItemKind.Function },
{ label: '{', kind: CompletionItemKind.Constructor },
];
// Helper functions to get suggestions for each kinds
function getSuggestionsForTypeSystemDefinitions(
token: ContextToken,
): CompletionItem[] {
return hintList(token, [
{ label: 'extend', kind: CompletionItemKind.Function },
...typeSystemCompletionItems,
]);
}
function getSuggestionsForExecutableDefinitions(
token: ContextToken,
): CompletionItem[] {
return hintList(token, executableCompletionItems);
}
function getSuggestionsForUnknownDocumentMode(
token: ContextToken,
): CompletionItem[] {
return hintList(token, [
{ label: 'extend', kind: CompletionItemKind.Function },
...executableCompletionItems,
...typeSystemCompletionItems,
]);
}
function getSuggestionsForExtensionDefinitions(
token: ContextToken,
): CompletionItem[] {
return hintList(token, typeSystemCompletionItems);
}
function getSuggestionsForFieldNames(
token: ContextToken,
typeInfo: AllTypeInfo,
options?: InternalAutocompleteOptions,
): CompletionItem[] {
if (typeInfo.parentType) {
const { parentType } = typeInfo;
// const { parentType, fieldDef, argDefs } = typeInfo;
let fields: GraphQLField<null, null>[] = [];
if ('getFields' in parentType) {
fields = objectValues<GraphQLField<null, null>>(
// TODO: getFields returns `GraphQLFieldMap<any, any> | GraphQLInputFieldMap`
parentType.getFields() as GraphQLFieldMap<any, any>,
);
}
if (isCompositeType(parentType)) {
fields.push(TypeNameMetaFieldDef);
}
if (parentType === options?.schema?.getQueryType()) {
fields.push(SchemaMetaFieldDef, TypeMetaFieldDef);
}
return hintList(
token,
fields.map<CompletionItem>((field, index) => {
const suggestion: CompletionItem = {
// This will sort the fields in the same order they are listed in the schema
sortText: String(index) + field.name,
label: field.name,
detail: String(field.type),
documentation: field.description ?? undefined,
deprecated: Boolean(field.deprecationReason),
isDeprecated: Boolean(field.deprecationReason),
deprecationReason: field.deprecationReason,
kind: CompletionItemKind.Field,
labelDetails: {
detail: ' ' + field.type.toString(),
},
type: field.type,
};
if (options?.fillLeafsOnComplete) {
// const hasArgs =
// // token.state.needsAdvance &&
// // @ts-expect-error
// parentType?._fields[field?.name];
suggestion.insertText = getFieldInsertText(field);
// eslint-disable-next-line logical-assignment-operators
if (!suggestion.insertText) {
suggestion.insertText = getInsertText(
field.name,
field.type,
// if we are replacing a field with arguments, we don't want the extra line
field.name + (token.state.needsAdvance ? '' : '\n'),
);
}
if (suggestion.insertText) {
suggestion.insertTextFormat = InsertTextFormat.Snippet;
suggestion.insertTextMode = InsertTextMode.adjustIndentation;
suggestion.command = SuggestionCommand;
}
}
return suggestion;
}),
);
}
return [];
}
function getSuggestionsForInputValues(
token: ContextToken,
typeInfo: AllTypeInfo,
queryText: string,
schema: GraphQLSchema,
): Array<CompletionItem> {
const namedInputType = getNamedType(typeInfo.inputType!);
const queryVariables: CompletionItem[] = getVariableCompletions(
queryText,
schema,
token,
).filter(v => v.detail === namedInputType?.name);
if (namedInputType instanceof GraphQLEnumType) {
const values = namedInputType.getValues();
return hintList(
token,
values
.map<CompletionItem>((value: GraphQLEnumValue) => ({
label: value.name,
detail: String(namedInputType),
documentation: value.description ?? undefined,
deprecated: Boolean(value.deprecationReason),
isDeprecated: Boolean(value.deprecationReason),
deprecationReason: value.deprecationReason,
kind: CompletionItemKind.EnumMember,
type: namedInputType,
}))
.concat(queryVariables),
);
}
if (namedInputType === GraphQLBoolean) {
return hintList(
token,
queryVariables.concat([
{
label: 'true',
detail: String(GraphQLBoolean),
documentation: 'Not false.',
kind: CompletionItemKind.Variable,
type: GraphQLBoolean,
},
{
label: 'false',
detail: String(GraphQLBoolean),
documentation: 'Not true.',
kind: CompletionItemKind.Variable,
type: GraphQLBoolean,
},
]),
);
}
return queryVariables;
}
function getSuggestionsForImplements(
token: ContextToken,
tokenState: State,
schema: GraphQLSchema,
documentText: string,
typeInfo: AllTypeInfo,
): Array<CompletionItem> {
// exit empty if we need an &
if (tokenState.needsSeparator) {
return [];
}
const typeMap = schema.getTypeMap();
const schemaInterfaces = objectValues(typeMap).filter(isInterfaceType);
const schemaInterfaceNames = schemaInterfaces.map(({ name }) => name);
const inlineInterfaces: Set<string> = new Set();
runOnlineParser(documentText, (_, state: State) => {
if (state.name) {
// gather inline interface definitions
if (
state.kind === RuleKinds.INTERFACE_DEF &&
!schemaInterfaceNames.includes(state.name)
) {
inlineInterfaces.add(state.name);
}
// gather the other interfaces the current type/interface definition implements
// so we can filter them out below
if (
state.kind === RuleKinds.NAMED_TYPE &&
state.prevState?.kind === RuleKinds.IMPLEMENTS
) {
if (typeInfo.interfaceDef) {
const existingType = typeInfo.interfaceDef
?.getInterfaces()
.find(({ name }) => name === state.name);
if (existingType) {
return;
}
const type = schema.getType(state.name);
const interfaceConfig = typeInfo.interfaceDef?.toConfig();
typeInfo.interfaceDef = new GraphQLInterfaceType({
...interfaceConfig,
interfaces: [
...interfaceConfig.interfaces,
(type as GraphQLInterfaceType) ||
new GraphQLInterfaceType({ name: state.name, fields: {} }),
],
});
} else if (typeInfo.objectTypeDef) {
const existingType = typeInfo.objectTypeDef
?.getInterfaces()
.find(({ name }) => name === state.name);
if (existingType) {
return;
}
const type = schema.getType(state.name);
const objectTypeConfig = typeInfo.objectTypeDef?.toConfig();
typeInfo.objectTypeDef = new GraphQLObjectType({
...objectTypeConfig,
interfaces: [
...objectTypeConfig.interfaces,
(type as GraphQLInterfaceType) ||
new GraphQLInterfaceType({ name: state.name, fields: {} }),
],
});
}
}
}
});
const currentTypeToExtend = typeInfo.interfaceDef || typeInfo.objectTypeDef;
const siblingInterfaces = currentTypeToExtend?.getInterfaces() || [];
const siblingInterfaceNames = siblingInterfaces.map(({ name }) => name);
// TODO: we should be using schema.getPossibleTypes() here, but
const possibleInterfaces = schemaInterfaces
.concat(
[...inlineInterfaces].map(name => ({ name }) as GraphQLInterfaceType),
)
.filter(
({ name }) =>
name !== currentTypeToExtend?.name &&
!siblingInterfaceNames.includes(name),
);
return hintList(
token,
possibleInterfaces.map(type => {
const result = {
label: type.name,
kind: CompletionItemKind.Interface,
type,
} as CompletionItem;
if (type?.description) {
result.documentation = type.description;
}
// TODO: should we report what an interface implements in CompletionItem.detail?
// result.detail = 'Interface'
// const interfaces = type.astNode?.interfaces;
// if (interfaces && interfaces.length > 0) {
// result.detail += ` (implements ${interfaces
// .map(i => i.name.value)
// .join(' & ')})`;
// }
return result;
}),
);
}
function getSuggestionsForFragmentTypeConditions(
token: ContextToken,
typeInfo: AllTypeInfo,
schema: GraphQLSchema,
_kind: 'NamedType' | 'TypeCondition',
): Array<CompletionItem> {
let possibleTypes: GraphQLType[];
if (typeInfo.parentType) {
if (isAbstractType(typeInfo.parentType)) {
const abstractType = assertAbstractType(typeInfo.parentType);
// Collect both the possible Object types as well as the interfaces
// they implement.
const possibleObjTypes = schema.getPossibleTypes(abstractType);
const possibleIfaceMap = Object.create(null);
for (const type of possibleObjTypes) {
for (const iface of type.getInterfaces()) {
possibleIfaceMap[iface.name] = iface;
}
}
possibleTypes = possibleObjTypes.concat(objectValues(possibleIfaceMap));
} else {
// The parent type is a non-abstract Object type, so the only possible
// type that can be used is that same type.
possibleTypes = [typeInfo.parentType];
}
} else {
const typeMap = schema.getTypeMap();
possibleTypes = objectValues(typeMap).filter(
type => isCompositeType(type) && !type.name.startsWith('__'),
);
}
return hintList(
token,
possibleTypes.map(type => {
const namedType = getNamedType(type);
return {
label: String(type),
documentation: (namedType?.description as string | undefined) || '',
kind: CompletionItemKind.Field,
};
}),
);
}
function getSuggestionsForFragmentSpread(
token: ContextToken,
typeInfo: AllTypeInfo,
schema: GraphQLSchema,
queryText: string,
fragmentDefs?: FragmentDefinitionNode[],
): Array<CompletionItem> {
if (!queryText) {
return [];
}
const typeMap = schema.getTypeMap();
const defState = getDefinitionState(token.state);
const fragments = getFragmentDefinitions(queryText);
if (fragmentDefs && fragmentDefs.length > 0) {
fragments.push(...fragmentDefs);
}
// Filter down to only the fragments which may exist here.
const relevantFrags = fragments.filter(
frag =>
// Only include fragments with known types.
typeMap[frag.typeCondition.name.value] &&
// Only include fragments which are not cyclic.
!(
defState &&
defState.kind === RuleKinds.FRAGMENT_DEFINITION &&
defState.name === frag.name.value
) &&
// Only include fragments which could possibly be spread here.
isCompositeType(typeInfo.parentType) &&
isCompositeType(typeMap[frag.typeCondition.name.value]) &&
doTypesOverlap(
schema,
typeInfo.parentType,
typeMap[frag.typeCondition.name.value] as GraphQLCompositeType,
),
);
return hintList(
token,
relevantFrags.map(frag => ({
label: frag.name.value,
detail: String(typeMap[frag.typeCondition.name.value]),
documentation: `fragment ${frag.name.value} on ${frag.typeCondition.name.value}`,
labelDetails: {
detail: `fragment ${frag.name.value} on ${frag.typeCondition.name.value}`,
},
kind: CompletionItemKind.Field,
type: typeMap[frag.typeCondition.name.value],
})),
);
}
// TODO: should be using getTypeInfo() for this if we can
const getParentDefinition = (state: State, kind: RuleKind) => {
if (state.prevState?.kind === kind) {
return state.prevState;
}
if (state.prevState?.prevState?.kind === kind) {
return state.prevState.prevState;
}
if (state.prevState?.prevState?.prevState?.kind === kind) {
return state.prevState.prevState.prevState;
}
if (state.prevState?.prevState?.prevState?.prevState?.kind === kind) {
return state.prevState.prevState.prevState.prevState;
}
};
export function getVariableCompletions(
queryText: string,
schema: GraphQLSchema,
token: ContextToken,
): CompletionItem[] {
let variableName: null | string = null;
let variableType: GraphQLInputObjectType | undefined | null;
const definitions: Record<string, any> = Object.create({});
runOnlineParser(queryText, (_, state: State) => {
// TODO: gather this as part of `AllTypeInfo`, as I don't think it's optimal to re-run the parser like this
if (state?.kind === RuleKinds.VARIABLE && state.name) {
variableName = state.name;
}
if (state?.kind === RuleKinds.NAMED_TYPE && variableName) {
const parentDefinition = getParentDefinition(state, RuleKinds.TYPE);
if (parentDefinition?.type) {
variableType = schema.getType(
parentDefinition?.type,
) as GraphQLInputObjectType;
}
}
if (variableName && variableType && !definitions[variableName]) {
// append `$` if the `token.string` is not already `$`, or describing a variable
// this appears to take care of it everywhere
const replaceString =
token.string === '$' || token?.state?.kind === 'Variable'
? variableName
: '$' + variableName;
definitions[variableName] = {
detail: variableType.toString(),
insertText: replaceString,
label: '$' + variableName,
rawInsert: replaceString,
type: variableType,
kind: CompletionItemKind.Variable,
} as CompletionItem;
variableName = null;
variableType = null;
}
});
return objectValues(definitions);
}
export function getFragmentDefinitions(
queryText: string,
): Array<FragmentDefinitionNode> {
const fragmentDefs: FragmentDefinitionNode[] = [];
runOnlineParser(queryText, (_, state: State) => {
if (
state.kind === RuleKinds.FRAGMENT_DEFINITION &&
state.name &&
state.type
) {
fragmentDefs.push({
kind: RuleKinds.FRAGMENT_DEFINITION,
name: {
kind: Kind.NAME,
value: state.name,
},
selectionSet: {
kind: RuleKinds.SELECTION_SET,
selections: [],
},
typeCondition: {
kind: RuleKinds.NAMED_TYPE,
name: {
kind: Kind.NAME,
value: state.type,
},
},
});
}
});
return fragmentDefs;
}
function getSuggestionsForVariableDefinition(
token: ContextToken,
schema: GraphQLSchema,
_kind: string,
): Array<CompletionItem> {
const inputTypeMap = schema.getTypeMap();
const inputTypes = objectValues(inputTypeMap).filter(isInputType);
return hintList(
token,
// TODO: couldn't get Exclude<> working here
inputTypes.map((type: GraphQLNamedType) => ({
label: type.name,
documentation: type?.description || '',
kind: CompletionItemKind.Variable,
})),
);
}
function getSuggestionsForDirective(
token: ContextToken,
state: State,
schema: GraphQLSchema,
_kind: string,
): Array<CompletionItem> {
if (state.prevState?.kind) {
const directives = schema
.getDirectives()
.filter(directive => canUseDirective(state.prevState, directive));
return hintList(
token,
directives.map(directive => ({
label: directive.name,
documentation: directive?.description || '',
kind: CompletionItemKind.Function,
})),
);
}
return [];
}
// I thought this added functionality somewhere, but I couldn't write any tests
// to execute it. I think it's handled as Arguments
function getSuggestionsForDirectiveArguments(
token: ContextToken,
state: State,
schema: GraphQLSchema,
_kind: string,
): Array<CompletionItem> {
const directive = schema.getDirectives().find(d => d.name === state.name);
return hintList(
token,
directive?.args.map(arg => ({
label: arg.name,
documentation: arg.description || '',
kind: CompletionItemKind.Field,
})) || [],
);
}
export function canUseDirective(
state: State['prevState'],
directive: GraphQLDirective,
): boolean {
if (!state?.kind) {
return false;
}
const { kind, prevState } = state;
const { locations } = directive;
switch (kind) {
case RuleKinds.QUERY:
return locations.includes(DirectiveLocation.QUERY);
case RuleKinds.MUTATION:
return locations.includes(DirectiveLocation.MUTATION);
case RuleKinds.SUBSCRIPTION:
return locations.includes(DirectiveLocation.SUBSCRIPTION);
case RuleKinds.FIELD:
case RuleKinds.ALIASED_FIELD:
return locations.includes(DirectiveLocation.FIELD);
case RuleKinds.FRAGMENT_DEFINITION:
return locations.includes(DirectiveLocation.FRAGMENT_DEFINITION);
case RuleKinds.FRAGMENT_SPREAD:
return locations.includes(DirectiveLocation.FRAGMENT_SPREAD);
case RuleKinds.INLINE_FRAGMENT:
return locations.includes(DirectiveLocation.INLINE_FRAGMENT);
// Schema Definitions
case RuleKinds.SCHEMA_DEF:
return locations.includes(DirectiveLocation.SCHEMA);
case RuleKinds.SCALAR_DEF:
return locations.includes(DirectiveLocation.SCALAR);
case RuleKinds.OBJECT_TYPE_DEF:
return locations.includes(DirectiveLocation.OBJECT);
case RuleKinds.FIELD_DEF:
return locations.includes(DirectiveLocation.FIELD_DEFINITION);
case RuleKinds.INTERFACE_DEF:
return locations.includes(DirectiveLocation.INTERFACE);
case RuleKinds.UNION_DEF:
return locations.includes(DirectiveLocation.UNION);
case RuleKinds.ENUM_DEF:
return locations.includes(DirectiveLocation.ENUM);
case RuleKinds.ENUM_VALUE:
return locations.includes(DirectiveLocation.ENUM_VALUE);
case RuleKinds.INPUT_DEF:
return locations.includes(DirectiveLocation.INPUT_OBJECT);
case RuleKinds.INPUT_VALUE_DEF:
const prevStateKind = prevState?.kind;
switch (prevStateKind) {
case RuleKinds.ARGUMENTS_DEF:
return locations.includes(DirectiveLocation.ARGUMENT_DEFINITION);
case RuleKinds.INPUT_DEF:
return locations.includes(DirectiveLocation.INPUT_FIELD_DEFINITION);
}
}
return false;
}
function unwrapType(state: State): State {
if (
state.prevState &&
state.kind &&
(
[
RuleKinds.NAMED_TYPE,
RuleKinds.LIST_TYPE,
RuleKinds.TYPE,
RuleKinds.NON_NULL_TYPE,
] as RuleKind[]
).includes(state.kind)
) {
return unwrapType(state.prevState);
}
return state;
}