Skip to content

Commit

Permalink
Do not suggest fields/fragments/input vars that are already present. (#…
Browse files Browse the repository at this point in the history
…48)

* Do not suggest fields/fragments/input vars that are already present.

* do not import runOnlineParser from nested module

* Fix tests not locating ts properly

* use pnpm --filter for dev script

* fix test for already present fields

* Update packages/graphqlsp/src/autoComplete.ts

---------

Co-authored-by: Jovi De Croock <[email protected]>
  • Loading branch information
TheMightyPenguin and JoviDeCroock authored Apr 25, 2023
1 parent 700dd54 commit f1840f8
Show file tree
Hide file tree
Showing 5 changed files with 194 additions and 25 deletions.
5 changes: 5 additions & 0 deletions .changeset/unlucky-geese-double.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@0no-co/graphqlsp': minor
---

Do not suggest fields/fragments/input vars that are already present.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"main": "./dist/index.js",
"module": "./dist/index.module.js",
"scripts": {
"build": "rollup -c ./scripts/build.mjs",
"build": "pnpm --filter @0no-co/graphqlsp build",
"prepare": "husky install",
"dev": "pnpm --filter @0no-co/graphqlsp dev",
"launch-debug": "./scripts/launch-debug.sh",
Expand Down
204 changes: 187 additions & 17 deletions packages/graphqlsp/src/autoComplete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ import {
getAutocompleteSuggestions,
getTokenAtPosition,
getTypeInfo,
RuleKinds,
State,
RuleKind,
CompletionItem,
onlineParser,
CharacterStream,
ContextToken,
} from 'graphql-language-service';
import { FragmentDefinitionNode, GraphQLSchema, Kind, parse } from 'graphql';

Expand Down Expand Up @@ -42,28 +49,13 @@ export function getGraphQLCompletions(
if (!foundToken || !schema.current) return undefined;

const text = resolveTemplate(node, filename, info);
let fragments: Array<FragmentDefinitionNode> = [];
try {
const parsed = parse(text, { noLocation: true });
fragments = parsed.definitions.filter(
x => x.kind === Kind.FRAGMENT_DEFINITION
) as Array<FragmentDefinitionNode>;
} catch (e) {}

const cursor = new Cursor(foundToken.line, foundToken.start);
const suggestions = getAutocompleteSuggestions(
schema.current,
text,
cursor
);

const token = getTokenAtPosition(text, cursor);
const spreadSuggestions = getSuggestionsForFragmentSpread(
token,
getTypeInfo(schema.current, token.state),
const [suggestions, spreadSuggestions] = getSuggestionsInternal(
schema.current,
text,
fragments
cursor
);

return {
Expand Down Expand Up @@ -101,3 +93,181 @@ export function getGraphQLCompletions(
return undefined;
}
}

export function getSuggestionsInternal(
schema: GraphQLSchema,
queryText: string,
cursor: Cursor
): [CompletionItem[], CompletionItem[]] {
const token = getTokenAtPosition(queryText, cursor);

let fragments: Array<FragmentDefinitionNode> = [];
try {
const parsed = parse(queryText, { noLocation: true });
fragments = parsed.definitions.filter(
x => x.kind === Kind.FRAGMENT_DEFINITION
) as Array<FragmentDefinitionNode>;
} catch (e) {}

let suggestions = getAutocompleteSuggestions(schema, queryText, cursor);
let spreadSuggestions = getSuggestionsForFragmentSpread(
token,
getTypeInfo(schema, token.state),
schema,
queryText,
fragments
);

const state =
token.state.kind === 'Invalid' ? token.state.prevState : token.state;
const parentName = getParentDefinition(token.state, RuleKinds.FIELD)?.name;

if (state && parentName) {
const { kind } = state;

// Argument names
if (kind === RuleKinds.ARGUMENTS || kind === RuleKinds.ARGUMENT) {
const usedArguments = new Set<String>();

runOnlineParser(queryText, (_, state) => {
if (state.kind === RuleKinds.ARGUMENT) {
const parentDefinition = getParentDefinition(state, RuleKinds.FIELD);
if (
parentName &&
state.name &&
parentDefinition?.name === parentName
) {
usedArguments.add(state.name);
}
}
});

suggestions = suggestions.filter(
suggestion => !usedArguments.has(suggestion.label)
);
}

// Field names
if (
kind === RuleKinds.SELECTION_SET ||
kind === RuleKinds.FIELD ||
kind === RuleKinds.ALIASED_FIELD
) {
const usedFields = new Set<string>();
const usedFragments = getUsedFragments(queryText, parentName);

runOnlineParser(queryText, (_, state) => {
if (
state.kind === RuleKinds.FIELD ||
state.kind === RuleKinds.ALIASED_FIELD
) {
const parentDefinition = getParentDefinition(state, RuleKinds.FIELD);
if (
parentDefinition &&
parentDefinition.name === parentName &&
state.name
) {
usedFields.add(state.name);
}
}
});

suggestions = suggestions.filter(
suggestion => !usedFields.has(suggestion.label)
);
spreadSuggestions = spreadSuggestions.filter(
suggestion => !usedFragments.has(suggestion.label)
);
}

// Fragment spread names
if (kind === RuleKinds.FRAGMENT_SPREAD) {
const usedFragments = getUsedFragments(queryText, parentName);
suggestions = suggestions.filter(
suggestion => !usedFragments.has(suggestion.label)
);
spreadSuggestions = spreadSuggestions.filter(
suggestion => !usedFragments.has(suggestion.label)
);
}
}

return [suggestions, spreadSuggestions];
}

function getUsedFragments(queryText: string, parentName: string | undefined) {
const usedFragments = new Set<string>();

runOnlineParser(queryText, (_, state) => {
if (state.kind === RuleKinds.FRAGMENT_SPREAD && state.name) {
const parentDefinition = getParentDefinition(state, RuleKinds.FIELD);
if (parentName && parentDefinition?.name === parentName) {
usedFragments.add(state.name);
}
}
});

return usedFragments;
}

/**
* This is vendored from https://github.com/graphql/graphiql/blob/aeedf7614e422c783f5cfb5e226c5effa46318fd/packages/graphql-language-service/src/interface/getAutocompleteSuggestions.ts#L831
*/
function 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;
}
}

function runOnlineParser(
queryText: string,
callback: (
stream: CharacterStream,
state: State,
style: string,
index: number
) => void | 'BREAK'
): ContextToken {
const lines = queryText.split('\n');
const parser = onlineParser();
let state = parser.startState();
let style = '';

let stream: CharacterStream = new CharacterStream('');

for (let i = 0; i < lines.length; i++) {
stream = new CharacterStream(lines[i]);
while (!stream.eol()) {
style = parser.token(stream, state);
const code = callback(stream, state, style, i);
if (code === 'BREAK') {
break;
}
}

// Above while loop won't run if there is an empty line.
// Run the callback one more time to catch this.
callback(stream, state, style, i);

if (!state.kind) {
state = parser.startState();
}
}

return {
start: stream.getStartOfToken(),
end: stream.getCurrentPosition(),
string: stream.current(),
state,
style,
};
}
6 changes: 0 additions & 6 deletions test/e2e/graphqlsp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,12 +91,6 @@ describe('simple', () => {
sortText: '0id',
labelDetails: { detail: ' ID!' },
},
{
...defaultAttrs,
name: 'title',
sortText: '1title',
labelDetails: { detail: ' String!' },
},
{
...defaultAttrs,
name: 'content',
Expand Down
2 changes: 1 addition & 1 deletion test/e2e/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export class TSServer {
) {
const tsserverPath = path.resolve(
projectPath,
'node_modules/typescript/lib/tsserver.js'
'../../../node_modules/typescript/lib/tsserver.js'
);

fs.lstatSync(tsserverPath);
Expand Down

0 comments on commit f1840f8

Please sign in to comment.