Skip to content

Commit

Permalink
feat: interfaces implementing interfaces for the LSP
Browse files Browse the repository at this point in the history
  • Loading branch information
acao committed Dec 23, 2020
1 parent 7117b7c commit f1a4751
Show file tree
Hide file tree
Showing 10 changed files with 303 additions and 65 deletions.
31 changes: 28 additions & 3 deletions examples/monaco-graphql-webpack/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,25 @@ import JSONWorker from 'worker-loader!monaco-editor/esm/vs/language/json/json.wo
// @ts-ignore
import GraphQLWorker from 'worker-loader!monaco-graphql/esm/graphql.worker';

const SCHEMA_URL = 'https://api.spacex.land/graphql/';
// const SCHEMA_URL = 'https://api.spacex.land/graphql/';

const SCHEMA_URL = 'https://api.github.com/graphql';

let API_TOKEN = localStorage.getItem('GH_TOKEN')

const promptForToken = () => {
if(!API_TOKEN) {
API_TOKEN = prompt('Please enter github PAT')
}
if (!API_TOKEN || API_TOKEN.length < 32) {
promptForToken()
}
}

if (!API_TOKEN) {
promptForToken()
localStorage.setItem("GH_TOKEN", API_TOKEN as string)
}
// @ts-ignore
window.MonacoEnvironment = {
getWorker(_workerId: string, label: string) {
Expand Down Expand Up @@ -149,7 +166,11 @@ async function executeCurrentOp() {
}
const result = await fetch(GraphQLAPI.schemaConfig.uri || SCHEMA_URL, {
method: 'POST',
headers: { 'content-type': 'application/json' },
headers: {
'content-type': 'application/json',
// @ts-ignore
authorization: `Bearer ${API_TOKEN}`,
},
body: JSON.stringify(body),
});

Expand Down Expand Up @@ -186,7 +207,11 @@ resultsEditor.addAction(opAction);
let initialSchema = false;

if (!initialSchema) {
GraphQLAPI.setSchemaConfig({ uri: SCHEMA_URL });
GraphQLAPI.setSchemaConfig({
uri: SCHEMA_URL,
// @ts-ignore
requestOpts: { headers: { Authorization: `Bearer ${API_TOKEN}` } },
});
initialSchema = true;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,16 @@ input InputType {
value: Int = 42
}

type TestType {
testField: String
interface TestInterface {
testField: String!
}

interface AnotherInterface implements TestInterface {
testField: String!
}

type TestType implements TestInterface & AnotherInterface {
testField: String!
}

type Query {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -217,10 +217,12 @@ query name {
it('provides correct typeCondition suggestions on fragment', () => {
const result = testSuggestions('fragment Foo on {}', new Position(0, 16));
expect(result.filter(({ label }) => !label.startsWith('__'))).toEqual([
{ label: 'AnotherInterface' },
{ label: 'Character' },
{ label: 'Droid' },
{ label: 'Human' },
{ label: 'Query' },
{ label: 'TestInterface' },
{ label: 'TestType' },
]);
});
Expand Down Expand Up @@ -331,4 +333,48 @@ query name {
{ label: 'onAllDefs' },
{ label: 'onArg' },
]));

it('provides correct interface suggestions when extending with an interface', () =>
expect(
testSuggestions('type Type implements ', new Position(0, 20)),
).toEqual([
{ label: 'AnotherInterface' },
{ label: 'Character' },
{ label: 'TestInterface' },
]));

it('provides correct interface suggestions when extending a type with multiple interfaces', () =>
expect(
testSuggestions(
'type Type implements TestInterface & ',
new Position(0, 37),
),
).toEqual([
{ label: 'AnotherInterface' },
{ label: 'Character' },
{ label: 'TestInterface' },
]));
it('provides correct interface suggestions when extending an interface with multiple interfaces', () =>
expect(
testSuggestions(
'interface Type implements TestInterface & ',
new Position(0, 44),
),
).toEqual([
{ label: 'AnotherInterface' },
{ label: 'Character' },
{ label: 'TestInterface' },
]));
it('provides correct interface suggestions when extending an interface with an inline interface', () =>
expect(
testSuggestions(
'interface A { id: String }\ninterface MyInterface implements ',
new Position(1, 33),
),
).toEqual([
{ label: 'A' },
{ label: 'AnotherInterface' },
{ label: 'Character' },
{ label: 'TestInterface' },
]));
});
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import {
visit,
VariableDefinitionNode,
GraphQLNamedType,
isInterfaceType,
GraphQLInterfaceType,
} from 'graphql';

import {
Expand Down Expand Up @@ -84,6 +86,7 @@ export function getAutocompleteSuggestions(

const kind = state.kind;
const step = state.step;

const typeInfo = getTypeInfo(schema, token.state);
// Definition kinds
if (kind === RuleKinds.DOCUMENT) {
Expand All @@ -96,13 +99,17 @@ export function getAutocompleteSuggestions(
]);
}

if (kind === RuleKinds.IMPLEMENTS) {
return getSuggestionsForImplements(token, state, schema, queryText);
}

// Field names
if (
kind === RuleKinds.SELECTION_SET ||
kind === RuleKinds.FIELD ||
kind === RuleKinds.ALIASED_FIELD
) {
return getSuggestionsForFieldNames(token, typeInfo, schema, kind);
return getSuggestionsForFieldNames(token, typeInfo, schema);
}

// Argument names
Expand Down Expand Up @@ -154,7 +161,7 @@ export function getAutocompleteSuggestions(
(kind === RuleKinds.OBJECT_FIELD && step === 2) ||
(kind === RuleKinds.ARGUMENT && step === 2)
) {
return getSuggestionsForInputValues(token, typeInfo, kind as string);
return getSuggestionsForInputValues(token, typeInfo);
}

// complete for all variables available in the query
Expand Down Expand Up @@ -240,8 +247,6 @@ function getSuggestionsForFieldNames(
token: ContextToken,
typeInfo: AllTypeInfo,
schema: GraphQLSchema,
// kind: RuleKind.SelectionSet | RuleKind.Field | RuleKind.AliasedField,
_kind: string,
): Array<CompletionItem> {
if (typeInfo.parentType) {
const parentType = typeInfo.parentType;
Expand Down Expand Up @@ -280,7 +285,6 @@ function getSuggestionsForFieldNames(
function getSuggestionsForInputValues(
token: ContextToken,
typeInfo: AllTypeInfo,
_kind: string,
): CompletionItem[] {
const namedInputType = getNamedType(typeInfo.inputType as GraphQLType);
if (namedInputType instanceof GraphQLEnumType) {
Expand Down Expand Up @@ -320,6 +324,56 @@ function getSuggestionsForInputValues(
return [];
}

function getSuggestionsForImplements(
token: ContextToken,
tokenState: State,
schema: GraphQLSchema,
documentText: string,
): Array<CompletionItem> {
// exit empty if we need an &

if (tokenState.needsSeperator) {
return [];
}
// gather inline interfaces so we can reference them.
// we don't know if they're part of the schema yet,
// but if they're defined in the same file that's a good sign
const inlineInterfaces: Set<string> = new Set();
runOnlineParser(documentText, (_, state: State) => {
const value = state.name || state.type;
if (
(state.kind === RuleKinds.INTERFACE_DEF ||
state.kind === RuleKinds.INTERFACE_TYPE_DEFINITION) &&
value
) {
inlineInterfaces.add(<string>value);
}
});
const typeMap = schema.getTypeMap();
const possibleTypes = objectValues(typeMap).filter(isInterfaceType);
const possibleInterfaces = possibleTypes.concat(
[...inlineInterfaces]
.filter(v => v !== tokenState.prevState?.name)
.map(name => ({ name } as GraphQLInterfaceType)),
);

return hintList(
token,
possibleInterfaces.map(type => {
const result = {
label: type.name,
kind: CompletionItemKind.Interface,
} as CompletionItem;
if (type?.description) {
result.detail = type.description;
}

return result;
}),
);
return [];
}

function getSuggestionsForFragmentTypeConditions(
token: ContextToken,
typeInfo: AllTypeInfo,
Expand Down Expand Up @@ -677,6 +731,20 @@ export function getTypeInfo(
case RuleKinds.DIRECTIVE:
directiveDef = state.name ? schema.getDirective(state.name) : null;
break;
case RuleKinds.IMPLEMENTS:
const interfaceType = getNamedType(type!);
// @ts-ignore
objectFieldDefs =
interfaceType instanceof GraphQLInterfaceType
? interfaceType.getFields()
: null;
if (state.type) {
type = schema.getType(state.type);
}
if (state.prevState?.name) {
parentType = schema.getType(state.prevState.name as string);
}
break;
case RuleKinds.ARGUMENTS:
if (!state.prevState) {
argDefs = null;
Expand Down
40 changes: 20 additions & 20 deletions packages/graphql-language-service-parser/src/CharacterStream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,21 @@
import { TokenPattern, CharacterStreamInterface } from './types';

export default class CharacterStream implements CharacterStreamInterface {
_start: number;
_pos: number;
_sourceText: string;
private _start: number;
private _pos: number;
private _sourceText: string;

constructor(sourceText: string) {
this._start = 0;
this._pos = 0;
this._sourceText = sourceText;
}

getStartOfToken = (): number => this._start;
public getStartOfToken = (): number => this._start;

getCurrentPosition = (): number => this._pos;
public getCurrentPosition = (): number => this._pos;

_testNextCharacter(pattern: TokenPattern): boolean {
private _testNextCharacter(pattern: TokenPattern): boolean {
const character = this._sourceText.charAt(this._pos);
let isMatched = false;
if (typeof pattern === 'string') {
Expand All @@ -48,23 +48,23 @@ export default class CharacterStream implements CharacterStreamInterface {
return isMatched;
}

eol = (): boolean => this._sourceText.length === this._pos;
public eol = (): boolean => this._sourceText.length === this._pos;

sol = (): boolean => this._pos === 0;
public sol = (): boolean => this._pos === 0;

peek = (): string | null => {
public peek = (): string | null => {
return this._sourceText.charAt(this._pos)
? this._sourceText.charAt(this._pos)
: null;
};

next = (): string => {
public next = (): string => {
const char = this._sourceText.charAt(this._pos);
this._pos++;
return char;
};

eat = (pattern: TokenPattern): string | undefined => {
public eat = (pattern: TokenPattern): string | undefined => {
const isMatched = this._testNextCharacter(pattern);
if (isMatched) {
this._start = this._pos;
Expand All @@ -74,7 +74,7 @@ export default class CharacterStream implements CharacterStreamInterface {
return undefined;
};

eatWhile = (match: TokenPattern): boolean => {
public eatWhile = (match: TokenPattern): boolean => {
let isMatched = this._testNextCharacter(match);
let didEat = false;

Expand All @@ -93,17 +93,17 @@ export default class CharacterStream implements CharacterStreamInterface {
return didEat;
};

eatSpace = (): boolean => this.eatWhile(/[\s\u00a0]/);
public eatSpace = (): boolean => this.eatWhile(/[\s\u00a0]/);

skipToEnd = (): void => {
public skipToEnd = (): void => {
this._pos = this._sourceText.length;
};

skipTo = (position: number): void => {
public skipTo = (position: number): void => {
this._pos = position;
};

match = (
public match = (
pattern: TokenPattern,
consume: boolean | null | undefined = true,
caseFold: boolean | null | undefined = false,
Expand Down Expand Up @@ -143,13 +143,13 @@ export default class CharacterStream implements CharacterStreamInterface {
return false;
};

backUp = (num: number): void => {
public backUp = (num: number): void => {
this._pos -= num;
};

column = (): number => this._pos;
public column = (): number => this._pos;

indentation = (): number => {
public indentation = (): number => {
const match = this._sourceText.match(/\s*/);
let indent = 0;
if (match && match.length !== 0) {
Expand All @@ -168,5 +168,5 @@ export default class CharacterStream implements CharacterStreamInterface {
return indent;
};

current = (): string => this._sourceText.slice(this._start, this._pos);
public current = (): string => this._sourceText.slice(this._start, this._pos);
}
Loading

0 comments on commit f1a4751

Please sign in to comment.