Skip to content

Commit

Permalink
fix: third completion context mode, changeset update
Browse files Browse the repository at this point in the history
  • Loading branch information
acao committed Apr 7, 2024
1 parent ced343f commit 089c226
Show file tree
Hide file tree
Showing 6 changed files with 109 additions and 46 deletions.
27 changes: 19 additions & 8 deletions .changeset/rotten-seahorses-fry.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,23 @@
'graphql-language-service-server-cli': minor
---

Fix many schema and fragment lifecycle issues, for all contexts except for schema updates for url schemas.
Fix many schema and fragment lifecycle issues, not all of them, but many related to cacheing.
Note: this makes `cacheSchemaForLookup` enabled by default again for schema first contexts.

this fixes multiple cacheing bugs, on writing some in-depth integration coverage for the LSP server.
it also solves several bugs regarding loading config types, and properly restarts the server when there are config changes
This fixes multiple cacheing bugs, upon addomg some in-depth integration test coverage for the LSP server.
It also solves several bugs regarding loading config types, and properly restarts the server and invalidates schema when there are config changes.

### Bugfix Summary

- jump to definition in embedded files offset bug
- cache invalidation for fragments
- schema cache invalidation for schema files
- schema definition lookups & autocomplete crossing into the wrong workspace
- configurable polling updates for network and other code first schema configuration, set to a 30s interval by default. powered by `schemaCacheTTL` which can be configured in the IDE settings (vscode, nvim) or in the graphql config file. (1)
- jump to definition in embedded files offset bug, for both fragments and code files with SDL strings
- cache invalidation for fragments (fragment lookup/autcoomplete data is more accurate, but incomplete/invalid fragments still do not autocomplete or validate, and remember fragment options always filter/validate by the `on` type!)
- schema cache invalidation for schema files - schema updates as you change the SDL files, and the generated file for code first by the `schemaCacheTTL` setting
- schema definition lookups & autocomplete crossing over into the wrong project

**Notes**

1. If possible, configuring for your locally running framework or a registry client to handle schema updates and output to a `schema.graphql` or `introspection.json` will always provide a better experience. many graphql frameworks have this built in! Otherwise, we must use a lazy polling approach.

### Known Bugs Fixed

Expand All @@ -24,7 +29,7 @@ it also solves several bugs regarding loading config types, and properly restart
- #3469
- #2422
- #2820
- many others to add here...
- many more!

### Test Improvements

Expand All @@ -33,3 +38,9 @@ it also solves several bugs regarding loading config types, and properly restart
- **total increased test coverage of about 25% in the LSP server codebase.**
- many "happy paths" covered for both schema and code first contexts
- many bugs revealed (and their source)

### What's next?

Another stage of the rewrite is already almost ready. This will fix even more bugs and improve memory usage, eliminate redundant parsing and ensure that graphql config's loaders do _all_ of the parsing and heavy lifting, thus honoring all the configs as well. It also significantly reduces the code complexity.

There is also a plan to match Relay LSP's lookup config for either IDE (vscode, nvm, etc) settings as they provide, or by loading modules into your `graphql-config`!
10 changes: 7 additions & 3 deletions .changeset/silly-yaks-bathe.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
---
'graphiql': patch
'graphql-language-service': patch
'graphql-language-service-server': patch
'graphql-language-service-server-cli': patch
'codemirror-graphql': patch
'@graphiql/react': patch
'cm6-graphql': patch
'monaco-graphql': patch
'vscode-graphql': patch
---

bugfix to completion for SDL type fields
Fixes several issues with Type System (SDL) completion across the ecosystem:

- restores completion for object and input type fields when the schema context is not parseable
- correct top-level completions for either of the unknown, type system or executable definitions. this leads to mixed top level completions when the document is unparseable, but now you are not seemingly restricted to only executable top level definitions
- `.graphqls` ad-hoc standard functionality remains, but is not required, as it is not part of the official spec, and the spec also allows mixed mode documents in theory, and this concept is required when the type is unknown
Original file line number Diff line number Diff line change
Expand Up @@ -147,10 +147,17 @@ describe('getAutocompleteSuggestions', () => {
it('provides correct initial keywords', () => {
expect(testSuggestions('', new Position(0, 0))).toEqual([
{ label: '{' },
{ label: 'extend' },
{ label: 'fragment' },
{ label: 'input' },
{ label: 'interface' },
{ label: 'mutation' },
{ label: 'query' },
{ label: 'scalar' },
{ label: 'schema' },
{ label: 'subscription' },
{ label: 'type' },
{ label: 'union' },
]);

expect(testSuggestions('q', new Position(0, 1))).toEqual([
Expand All @@ -159,9 +166,9 @@ describe('getAutocompleteSuggestions', () => {
]);
});

it('provides correct suggestions at where the cursor is', () => {
it('provides correct top level suggestions when a simple query is already present', () => {
// Below should provide initial keywords
expect(testSuggestions(' {}', new Position(0, 0))).toEqual([
expect(testSuggestions(' { id }', new Position(0, 0))).toEqual([
{ label: '{' },
{ label: 'fragment' },
{ label: 'mutation' },
Expand Down Expand Up @@ -501,7 +508,7 @@ describe('getAutocompleteSuggestions', () => {
});

describe('with SDL types', () => {
it('provides correct initial keywords', () => {
it('provides correct initial keywords w/ graphqls', () => {
expect(
testSuggestions('', new Position(0, 0), [], { uri: 'schema.graphqls' }),
).toEqual([
Expand All @@ -515,6 +522,25 @@ describe('getAutocompleteSuggestions', () => {
]);
});

it('provides correct initial keywords w/out graphqls', () => {
expect(
testSuggestions('', new Position(0, 0), [], { uri: 'schema.graphql' }),
).toEqual([
{ label: '{' },
{ label: 'extend' },
{ label: 'fragment' },
{ label: 'input' },
{ label: 'interface' },
{ label: 'mutation' },
{ label: 'query' },
{ label: 'scalar' },
{ label: 'schema' },
{ label: 'subscription' },
{ label: 'type' },
{ label: 'union' },
]);
});

it('provides correct initial definition keywords', () => {
expect(
testSuggestions('type Type { field: String }\n\n', new Position(0, 31)),
Expand Down Expand Up @@ -595,7 +621,7 @@ describe('getAutocompleteSuggestions', () => {
expect(testSuggestions('type Type @', new Position(0, 11))).toEqual([
{ label: 'onAllDefs' },
]));
it('provides correct suggestions on object fields', () =>
it('provides correct suggestions on object field w/ .graphqls', () =>
expect(
testSuggestions('type Type {\n aField: s', new Position(0, 23), [], {
uri: 'schema.graphqls',
Expand All @@ -607,6 +633,18 @@ describe('getAutocompleteSuggestions', () => {
{ label: 'TestType' },
{ label: 'TestUnion' },
]));
it('provides correct suggestions on object fields', () =>
expect(
testSuggestions('type Type {\n aField: s', new Position(0, 23), [], {
uri: 'schema.graphql',
}),
).toEqual([
{ label: 'Episode' },
{ label: 'String' },
{ label: 'TestInterface' },
{ label: 'TestType' },
{ label: 'TestUnion' },
]));
// TODO: shouldn't TestType and TestUnion be available here?
it('provides correct filtered suggestions on object fields in regular SDL files', () =>
expect(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,27 +118,28 @@ const typeSystemKinds: Kind[] = [
Kind.INPUT_OBJECT_TYPE_EXTENSION,
];

const hasTypeSystemDefinitions = (sdl: string | undefined) => {
let hasTypeSystemDef = false;
const getParsedMode = (sdl: string | undefined): GraphQLDocumentMode => {
let mode = GraphQLDocumentMode.UNKNOWN;
if (sdl) {
try {
visit(parse(sdl), {
enter(node) {
if (node.kind === 'Document') {
mode = GraphQLDocumentMode.EXECUTABLE;
return;
}
if (typeSystemKinds.includes(node.kind)) {
hasTypeSystemDef = true;
mode = GraphQLDocumentMode.TYPE_SYSTEM;
return BREAK;
}
return false;
},
});
} catch {
return hasTypeSystemDef;
return mode;
}
}
return hasTypeSystemDef;
return mode;
};

export type AutocompleteSuggestionOptions = {
Expand Down Expand Up @@ -170,22 +171,24 @@ export function getAutocompleteSuggestions(
const state =
token.state.kind === 'Invalid' ? token.state.prevState : token.state;

const mode = options?.mode || getDocumentMode(queryText, options?.uri);

// relieve flow errors by checking if `state` exists
if (!state) {
return [];
}

const { kind, step, prevState } = state;
const typeInfo = getTypeInfo(schema, token.state);
const mode = options?.mode || getDocumentMode(queryText, options?.uri);

// Definition kinds
if (kind === RuleKinds.DOCUMENT) {
if (mode === GraphQLDocumentMode.TYPE_SYSTEM) {
return getSuggestionsForTypeSystemDefinitions(token);
}
return getSuggestionsForExecutableDefinitions(token);
if (mode === GraphQLDocumentMode.EXECUTABLE) {
return getSuggestionsForExecutableDefinitions(token);
}
return getSuggestionsForUnknownDocumentMode(token);
}

if (kind === RuleKinds.EXTEND_DEF) {
Expand Down Expand Up @@ -469,38 +472,45 @@ const getInsertText = (field: GraphQLField<null, null>) => {
return null;
};

const typeSystemCompletionItems = [
{ 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 = [
{ 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) {
return hintList(token, [
{ label: 'extend', kind: CompletionItemKind.Function },
{ 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 },
...typeSystemCompletionItems,
]);
}

function getSuggestionsForExecutableDefinitions(token: ContextToken) {
return hintList(token, executableCompletionItems);
}

function getSuggestionsForUnknownDocumentMode(token: ContextToken) {
return hintList(token, [
{ label: 'query', kind: CompletionItemKind.Function },
{ label: 'mutation', kind: CompletionItemKind.Function },
{ label: 'subscription', kind: CompletionItemKind.Function },
{ label: 'fragment', kind: CompletionItemKind.Function },
{ label: '{', kind: CompletionItemKind.Constructor },
{ label: 'extend', kind: CompletionItemKind.Function },
...executableCompletionItems,
...typeSystemCompletionItems,
]);
}

function getSuggestionsForExtensionDefinitions(token: ContextToken) {
return hintList(token, [
{ 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 },
]);
return hintList(token, typeSystemCompletionItems);
}

function getSuggestionsForFieldNames(
Expand Down Expand Up @@ -1280,6 +1290,7 @@ export function getTypeInfo(
export enum GraphQLDocumentMode {
TYPE_SYSTEM = 'TYPE_SYSTEM',
EXECUTABLE = 'EXECUTABLE',
UNKNOWN = 'UNKNOWN',
}

function getDocumentMode(
Expand All @@ -1289,9 +1300,7 @@ function getDocumentMode(
if (uri?.endsWith('.graphqls')) {
return GraphQLDocumentMode.TYPE_SYSTEM;
}
return hasTypeSystemDefinitions(documentText)
? GraphQLDocumentMode.TYPE_SYSTEM
: GraphQLDocumentMode.EXECUTABLE;
return getParsedMode(documentText);
}

function unwrapType(state: State): State {
Expand Down
1 change: 1 addition & 0 deletions packages/graphql-language-service/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import type {
GraphQLProjectConfig,
GraphQLExtensionDeclaration,
} from 'graphql-config';
import { GraphQLDocumentMode } from './interface';

Check failure on line 41 in packages/graphql-language-service/src/types.ts

View workflow job for this annotation

GitHub Actions / Build

'GraphQLDocumentMode' is declared but its value is never read.

Check failure on line 41 in packages/graphql-language-service/src/types.ts

View workflow job for this annotation

GitHub Actions / Build

'GraphQLDocumentMode' is declared but its value is never read.

export type {
GraphQLConfig,
Expand Down
2 changes: 1 addition & 1 deletion packages/vscode-graphql/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@
"type": [
"number"
],
"description": "Schema cache ttl in milliseconds before requesting a fresh schema when caching the local schema file is enabled. Defaults to 30000 (30 seconds).",
"description": "Schema cache ttl in milliseconds - the interval before requesting a fresh schema when caching the local schema file is enabled. Defaults to 30000 (30 seconds).",
"default": 30000
},
"graphql-config.load.rootDir": {
Expand Down

0 comments on commit 089c226

Please sign in to comment.