Skip to content

Commit

Permalink
fix: autocompletion for argument values, more trigger characters (#2070)
Browse files Browse the repository at this point in the history
  • Loading branch information
acao authored Dec 1, 2021
1 parent c423619 commit 989fca6
Show file tree
Hide file tree
Showing 8 changed files with 128 additions and 46 deletions.
6 changes: 6 additions & 0 deletions .changeset/green-ghosts-agree.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'graphql-language-service-interface': patch
'monaco-graphql': patch
---

Fix a bug with variable completion with or without `$` across the ecosytem. Introduce more triggerCharacters for fun!
Original file line number Diff line number Diff line change
Expand Up @@ -279,15 +279,15 @@ query name {
]);
});

it('provides correct suggestions when autocompleting for declared variable while typing', () => {
it('provides correct suggestions for declared variables upon typing $', () => {
const result = testSuggestions(
'query($id: String, $ep: Episode!){ hero(episode: $ }',
new Position(0, 51),
);
expect(result).toEqual([{ label: '$ep', detail: 'Episode' }]);
});

it('provides correct suggestions when autocompleting for declared variable', () => {
it('provides correct suggestions for variables based on argument context', () => {
const result = testSuggestions(
'query($id: String!, $episode: Episode!){ hero(episode: ',
new Position(0, 55),
Expand All @@ -297,6 +297,7 @@ query name {
{ label: 'EMPIRE', detail: 'Episode' },
{ label: 'JEDI', detail: 'Episode' },
{ label: 'NEWHOPE', detail: 'Episode' },
// no $id here, it's not compatible :P
]);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,13 +156,20 @@ export function getAutocompleteSuggestions(
if (argDefs) {
return hintList(
token,
argDefs.map(argDef => ({
label: argDef.name,
detail: String(argDef.type),
documentation: argDef.description ?? undefined,
kind: CompletionItemKind.Variable,
type: argDef.type,
})),
argDefs.map(
(argDef: GraphQLArgument): CompletionItem => ({
label: argDef.name,
insertText: argDef.name + ': ',
command: {
command: 'editor.action.triggerSuggest',
title: 'Suggestions',
},
detail: String(argDef.type),
documentation: argDef.description ?? undefined,
kind: CompletionItemKind.Variable,
type: argDef.type,
}),
),
);
}
}
Expand Down Expand Up @@ -203,7 +210,11 @@ export function getAutocompleteSuggestions(
// complete for all variables available in the query
if (kind === RuleKinds.VARIABLE && step === 1) {
const namedInputType = getNamedType(typeInfo.inputType as GraphQLType);
const variableDefinitions = getVariableCompletions(queryText, schema);
const variableDefinitions = getVariableCompletions(
queryText,
schema,
token,
);
return hintList(
token,
variableDefinitions.filter(v => v.detail === namedInputType?.name),
Expand Down Expand Up @@ -311,7 +322,7 @@ function getSuggestionsForInputValues(
const queryVariables: CompletionItem[] = getVariableCompletions(
queryText,
schema,
true,
token,
).filter(v => v.detail === namedInputType.name);

if (namedInputType instanceof GraphQLEnumType) {
Expand Down Expand Up @@ -579,16 +590,17 @@ const getParentDefinition = (state: State, kind: RuleKind) => {
export function getVariableCompletions(
queryText: string,
schema: GraphQLSchema,
forcePrefix: boolean = false,
token: ContextToken,
): CompletionItem[] {
let variableName: null | string;
let variableName: null | string = null;
let variableType: GraphQLInputObjectType | undefined | null;
const definitions: Record<string, any> = Object.create({});
runOnlineParser(queryText, (_, state: State) => {
if (state.kind === RuleKinds.VARIABLE && state.name) {
// 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) {
if (state?.kind === RuleKinds.NAMED_TYPE && variableName) {
const parentDefinition = getParentDefinition(state, RuleKinds.TYPE);
if (parentDefinition?.type) {
variableType = schema.getType(
Expand All @@ -599,15 +611,15 @@ export function getVariableCompletions(

if (variableName && variableType) {
if (!definitions[variableName]) {
// append `$` if the `token.string` is not already `$`
const label = token.string === '$' ? variableName : '$' + variableName;
definitions[variableName] = {
detail: variableType.toString(),
label: `$${variableName}`,
label,
type: variableType,
kind: CompletionItemKind.Variable,
} as CompletionItem;
if (forcePrefix) {
definitions[variableName].insertText = `$${variableName}`;
}

variableName = null;
variableType = null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ export class MessageProcessor {
documentSymbolProvider: true,
completionProvider: {
resolveProvider: true,
triggerCharacters: ['@'],
triggerCharacters: [' ', ':', '$', '\n', ' ', '(', '@'],
},
definitionProvider: true,
textDocumentSync: 1,
Expand Down
63 changes: 63 additions & 0 deletions packages/graphql-language-service-types/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ export type CompletionItem = CompletionItemType & {
documentation?: string | null;
deprecationReason?: string | null;
type?: GraphQLType;
command?: CompletionItemType['command'];
};
// Below are basically a copy-paste from Nuclide rpc types for definitions.

Expand Down Expand Up @@ -246,6 +247,7 @@ export const FileChangeTypeKind = {
Deleted: 3,
};

// eslint-disable-next-line no-redeclare
export type FileChangeTypeKind = {
Created: 1;
Changed: 2;
Expand All @@ -255,3 +257,64 @@ export type FileChangeTypeKind = {
export type FileChangeTypeKeys = keyof FileChangeTypeKind;

export type FileChangeType = FileChangeTypeKind[FileChangeTypeKeys];

// copied from `microsoft/vscode-languageserver-types` to prevent import issues

/**
* The kind of a completion entry.
*/
export namespace CompletionItemKind {
export const Text: 1 = 1;
export const Method: 2 = 2;
export const Function: 3 = 3;
export const Constructor: 4 = 4;
export const Field: 5 = 5;
export const Variable: 6 = 6;
export const Class: 7 = 7;
export const Interface: 8 = 8;
export const Module: 9 = 9;
export const Property: 10 = 10;
export const Unit: 11 = 11;
export const Value: 12 = 12;
export const Enum: 13 = 13;
export const Keyword: 14 = 14;
export const Snippet: 15 = 15;
export const Color: 16 = 16;
export const File: 17 = 17;
export const Reference: 18 = 18;
export const Folder: 19 = 19;
export const EnumMember: 20 = 20;
export const Constant: 21 = 21;
export const Struct: 22 = 22;
export const Event: 23 = 23;
export const Operator: 24 = 24;
export const TypeParameter: 25 = 25;
}

// eslint-disable-next-line no-redeclare
export type CompletionItemKind =
| 1
| 2
| 3
| 4
| 5
| 6
| 7
| 8
| 9
| 10
| 11
| 12
| 13
| 14
| 15
| 16
| 17
| 18
| 19
| 20
| 21
| 22
| 23
| 24
| 25;
10 changes: 4 additions & 6 deletions packages/monaco-graphql/src/GraphQLWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,19 @@

import { FormattingOptions, ICreateData } from './typings';

import type { worker, editor, Position, IRange } from 'monaco-editor';
import type { worker, editor, Position } from 'monaco-editor';
import * as monaco from 'monaco-editor';

import { getRange, LanguageService } from 'graphql-language-service';

import type {
SchemaResponse,
CompletionItem as GraphQLCompletionItem,
} from 'graphql-language-service';
import type { SchemaResponse } from 'graphql-language-service';

import {
toGraphQLPosition,
toMonacoRange,
toMarkerData,
toCompletion,
GraphQLWorkerCompletionItem,
} from './utils';

import type { GraphQLSchema, DocumentNode } from 'graphql';
Expand Down Expand Up @@ -65,7 +63,7 @@ export class GraphQLWorker {
async doComplete(
uri: string,
position: Position,
): Promise<(GraphQLCompletionItem & { range: IRange })[]> {
): Promise<GraphQLWorkerCompletionItem[]> {
const document = this._getTextDocument(uri);
const graphQLPosition = toGraphQLPosition(position);

Expand Down
15 changes: 8 additions & 7 deletions packages/monaco-graphql/src/languageFeatures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,13 @@ import type {
Thenable,
CancellationToken,
IDisposable,
IRange,
} from 'monaco-editor';

import * as monaco from 'monaco-editor';

import { editor } from 'monaco-editor/esm/vs/editor/editor.api';
import { CompletionItemKind as lsCompletionItemKind } from 'vscode-languageserver-types';
import { CompletionItem as GraphQLCompletionItem } from 'graphql-language-service';
import { CompletionItemKind as lsCompletionItemKind } from 'graphql-language-service';
import { GraphQLWorkerCompletionItem } from './utils';
export interface WorkerAccessor {
(...more: Uri[]): Thenable<GraphQLWorker>;
}
Expand Down Expand Up @@ -174,17 +173,19 @@ export function toCompletionItemKind(kind: lsCompletionItemKind) {
}

export function toCompletion(
entry: GraphQLCompletionItem & { range: IRange },
entry: GraphQLWorkerCompletionItem,
): monaco.languages.CompletionItem {
return {
// @ts-expect-error
range: entry.range,
kind: toCompletionItemKind(entry.kind as lsCompletionItemKind),
label: entry.label,
insertText: entry.insertText || (entry.label as string),
sortText: entry.sortText,
filterText: entry.filterText,
documentation: entry.documentation,
detail: entry.detail,
range: entry.range,
kind: toCompletionItemKind(entry.kind as lsCompletionItemKind),
command: entry.command,
};
}

Expand All @@ -195,7 +196,7 @@ export class CompletionAdapter
}

public get triggerCharacters(): string[] {
return [' ', ':'];
return [':', '$', '\n', ' ', '(', '@'];
}

async provideCompletionItems(
Expand Down
27 changes: 14 additions & 13 deletions packages/monaco-graphql/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,10 @@ import type {

import { Position } from 'graphql-language-service';

// @ts-ignore
export type MonacoCompletionItem = monaco.languages.CompletionItem & {
isDeprecated?: boolean;
deprecationReason?: string | null;
};
// @ts-ignore
export function toMonacoRange(range: GraphQLRange): monaco.IRange {
return {
startLineNumber: range.start.line + 1,
Expand All @@ -29,31 +27,35 @@ export function toMonacoRange(range: GraphQLRange): monaco.IRange {
};
}

// @ts-ignore
export function toGraphQLPosition(position: monaco.Position): GraphQLPosition {
return new Position(position.lineNumber - 1, position.column - 1);
}

export type GraphQLWorkerCompletionItem = GraphQLCompletionItem & {
range?: monaco.IRange;
command?: monaco.languages.CompletionItem['command'];
};

export function toCompletion(
entry: GraphQLCompletionItem,
range?: GraphQLRange,
// @ts-ignore
): GraphQLCompletionItem & { range: monaco.IRange } {
return {
): GraphQLWorkerCompletionItem {
const results: GraphQLWorkerCompletionItem = {
label: entry.label,
// TODO: when adding variables to getAutocompleteSuggestions, we appended the $.
// this appears to cause an issue in monaco, but not vscode
insertText:
entry.insertText ||
(!entry.label.startsWith('$') ? entry.label : entry.label.substring(1)),
insertText: entry.insertText ?? entry.label,
sortText: entry.sortText,
filterText: entry.filterText,
documentation: entry.documentation,
detail: entry.detail,
// @ts-ignore
range: range ? toMonacoRange(range) : undefined,
kind: entry.kind,
};

if (entry.command) {
results.command = { ...entry.command, id: entry.command.command };
}

return results;
}

/**
Expand Down Expand Up @@ -84,7 +86,6 @@ export function toCompletion(

export function toMarkerData(
diagnostic: Diagnostic,
// @ts-ignore
): monaco.editor.IMarkerData {
return {
startLineNumber: diagnostic.range.start.line + 1,
Expand Down

2 comments on commit 989fca6

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.