Skip to content

Commit 9b6304c

Browse files
committed
fix object field completion, add tests for the missing cases
1 parent 867714c commit 9b6304c

File tree

6 files changed

+165
-104
lines changed

6 files changed

+165
-104
lines changed

packages/graphql-language-service-server/src/GraphQLCache.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import {
2020
} from 'graphql';
2121
import type {
2222
CachedContent,
23-
GraphQLCache as GraphQLCacheInterface,
2423
GraphQLFileMetadata,
2524
GraphQLFileInfo,
2625
FragmentInfo,
@@ -93,7 +92,7 @@ export async function getGraphQLCache({
9392
});
9493
}
9594

96-
export class GraphQLCache implements GraphQLCacheInterface {
95+
export class GraphQLCache {
9796
_configDir: Uri;
9897
_graphQLFileListCache: Map<Uri, Map<string, GraphQLFileInfo>>;
9998
_graphQLConfig: GraphQLConfig;
@@ -596,8 +595,13 @@ export class GraphQLCache implements GraphQLCacheInterface {
596595
if (schemaPath && schemaKey) {
597596
schemaCacheKey = schemaKey as string;
598597

599-
// Read from disk
600-
schema = await projectConfig.getSchema();
598+
try {
599+
// Read from disk
600+
schema = await projectConfig.getSchema();
601+
} catch {
602+
// // if there is an error reading the schema, just use the last valid schema
603+
// schema = this._schemaMap.get(schemaCacheKey);
604+
}
601605

602606
if (this._schemaMap.has(schemaCacheKey)) {
603607
schema = this._schemaMap.get(schemaCacheKey);

packages/graphql-language-service-server/src/GraphQLLanguageService.ts

Lines changed: 25 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ import {
2929
IPosition,
3030
Outline,
3131
OutlineTree,
32-
GraphQLCache,
3332
getAutocompleteSuggestions,
3433
getHoverInformation,
3534
HoverConfig,
@@ -47,6 +46,8 @@ import {
4746
getTypeInfo,
4847
} from 'graphql-language-service';
4948

49+
import type { GraphQLCache } from './GraphQLCache';
50+
5051
import { GraphQLConfig, GraphQLProjectConfig } from 'graphql-config';
5152

5253
import type { Logger } from 'vscode-languageserver';
@@ -223,30 +224,31 @@ export class GraphQLLanguageService {
223224
return [];
224225
}
225226
const schema = await this._graphQLCache.getSchema(projectConfig.name);
226-
const fragmentDefinitions = await this._graphQLCache.getFragmentDefinitions(
227-
projectConfig,
228-
);
227+
if (!schema) {
228+
return [];
229+
}
230+
let fragmentInfo = [] as Array<FragmentDefinitionNode>;
231+
try {
232+
const fragmentDefinitions =
233+
await this._graphQLCache.getFragmentDefinitions(projectConfig);
234+
fragmentInfo = Array.from(fragmentDefinitions).map(
235+
([, info]) => info.definition,
236+
);
237+
} catch {}
229238

230-
const fragmentInfo = Array.from(fragmentDefinitions).map(
231-
([, info]) => info.definition,
239+
return getAutocompleteSuggestions(
240+
schema,
241+
query,
242+
position,
243+
undefined,
244+
fragmentInfo,
245+
{
246+
uri: filePath,
247+
fillLeafsOnComplete:
248+
projectConfig?.extensions?.languageService?.fillLeafsOnComplete ??
249+
false,
250+
},
232251
);
233-
234-
if (schema) {
235-
return getAutocompleteSuggestions(
236-
schema,
237-
query,
238-
position,
239-
undefined,
240-
fragmentInfo,
241-
{
242-
uri: filePath,
243-
fillLeafsOnComplete:
244-
projectConfig?.extensions?.languageService?.fillLeafsOnComplete ??
245-
false,
246-
},
247-
);
248-
}
249-
return [];
250252
}
251253

252254
public async getHoverInformation(

packages/graphql-language-service-server/src/MessageProcessor.ts

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -474,20 +474,23 @@ export class MessageProcessor {
474474

475475
if (project?.extensions?.languageService?.enableValidation !== false) {
476476
// Send the diagnostics onChange as well
477-
await Promise.all(
478-
contents.map(async ({ query, range }) => {
479-
const results = await this._languageService.getDiagnostics(
480-
query,
481-
uri,
482-
this._isRelayCompatMode(query),
483-
);
484-
if (results && results.length > 0) {
485-
diagnostics.push(
486-
...processDiagnosticsMessage(results, query, range),
477+
try {
478+
await Promise.all(
479+
contents.map(async ({ query, range }) => {
480+
const results = await this._languageService.getDiagnostics(
481+
query,
482+
uri,
483+
this._isRelayCompatMode(query),
487484
);
488-
}
489-
}),
490-
);
485+
if (results && results.length > 0) {
486+
diagnostics.push(
487+
...processDiagnosticsMessage(results, query, range),
488+
);
489+
}
490+
// skip diagnostic errors, usually related to parsing incomplete fragments
491+
}),
492+
);
493+
} catch {}
491494
}
492495

493496
this._logger.log(
@@ -600,6 +603,7 @@ export class MessageProcessor {
600603
if (range) {
601604
position.line -= range.start.line;
602605
}
606+
603607
const result = await this._languageService.getAutocompleteSuggestions(
604608
query,
605609
toPosition(position),
@@ -729,9 +733,8 @@ export class MessageProcessor {
729733

730734
return { uri, diagnostics };
731735
}
732-
} catch (err) {
733-
this._handleConfigError({ err, uri });
734-
}
736+
// skip diagnostics errors usually from incomplete files
737+
} catch {}
735738
return { uri, diagnostics: [] };
736739
}
737740
if (change.type === FileChangeTypeKind.Deleted) {
@@ -1191,7 +1194,13 @@ export class MessageProcessor {
11911194
const schemaFilePath = path.resolve(project.dirpath, schema);
11921195
const uriFilePath = URI.parse(uri).fsPath;
11931196
if (uriFilePath === schemaFilePath) {
1194-
await this._graphQLCache.invalidateSchemaCacheForProject(project);
1197+
try {
1198+
const file = await readFile(schemaFilePath, 'utf-8');
1199+
// only invalidate the schema cache if we can actually parse the file
1200+
// otherwise, leave the last valid one in place
1201+
parse(file, { noLocation: true });
1202+
this._graphQLCache.invalidateSchemaCacheForProject(project);
1203+
} catch {}
11951204
}
11961205
}),
11971206
);

packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts

Lines changed: 33 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -483,38 +483,6 @@ describe('project with simple config and graphql files', () => {
483483
'__schema',
484484
'__type',
485485
]);
486-
487-
// project.changeFile(
488-
// 'b/schema.graphql',
489-
// schemaFile[1] + '\ntype Example1 { field: }',
490-
// );
491-
// TODO: this didn't work at all for multi project,
492-
// whereas a schema change works above in a single schema context as per updating the cache
493-
//
494-
// how to register incomplete changes to model autocomplete, etc?
495-
// await project.lsp.handleWatchedFilesChangedNotification({
496-
// changes: [
497-
// { uri: project.uri('b/schema.graphql'), type: FileChangeType.Changed },
498-
// ],
499-
// });
500-
// better - fails on a graphql parsing error! annoying
501-
// await project.lsp.handleDidChangeNotification({
502-
// textDocument: { uri: project.uri('b/schema.graphql'), version: 1 },
503-
// contentChanges: [
504-
// { text: schemaFile[1] + '\ntype Example1 { field: }' },
505-
// ],
506-
// });
507-
508-
// const schemaCompletion = await project.lsp.handleCompletionRequest({
509-
// textDocument: { uri: project.uri('b/schema.graphql') },
510-
// position: { character: 23, line: 3 },
511-
// });
512-
// expect(schemaCompletion.items.map(i => i.label)).toEqual([
513-
// 'foo',
514-
// '__typename',
515-
// '__schema',
516-
// '__type',
517-
// ]);
518486
// this confirms that autocomplete respects cross-project boundaries for types.
519487
// it performs a definition request for the foo field in Query
520488
const schemaCompletion1 = await project.lsp.handleCompletionRequest({
@@ -529,6 +497,39 @@ describe('project with simple config and graphql files', () => {
529497
});
530498
expect(serializeRange(schemaDefinition[0].range)).toEqual(fooTypePosition);
531499

500+
// simulate a watched schema file change (codegen, etc)
501+
project.changeFile(
502+
'b/schema.graphql',
503+
schemaFile[1] + '\ntype Example1 { field: }',
504+
);
505+
await project.lsp.handleWatchedFilesChangedNotification({
506+
changes: [
507+
{ uri: project.uri('b/schema.graphql'), type: FileChangeType.Changed },
508+
],
509+
});
510+
// TODO: repeat this with other changes to the schema file and use a
511+
// didChange event to see if the schema updates properly as well
512+
// await project.lsp.handleDidChangeNotification({
513+
// textDocument: { uri: project.uri('b/schema.graphql'), version: 1 },
514+
// contentChanges: [
515+
// { text: schemaFile[1] + '\ntype Example1 { field: }' },
516+
// ],
517+
// });
518+
// console.log(project.fileCache.get('b/schema.graphql'));
519+
const schemaCompletion = await project.lsp.handleCompletionRequest({
520+
textDocument: { uri: project.uri('b/schema.graphql') },
521+
position: { character: 25, line: 5 },
522+
});
523+
// TODO: SDL completion still feels incomplete here... where is Int?
524+
// where is self-referential Example1?
525+
expect(schemaCompletion.items.map(i => i.label)).toEqual([
526+
'Query',
527+
'Foo',
528+
'String',
529+
'Test',
530+
'Boolean',
531+
]);
532+
532533
expect(project.lsp._logger.error).not.toHaveBeenCalled();
533534
});
534535
});

packages/graphql-language-service/src/interface/__tests__/getAutocompleteSuggestions-test.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -607,6 +607,39 @@ describe('getAutocompleteSuggestions', () => {
607607
{ label: 'TestType' },
608608
{ label: 'TestUnion' },
609609
]));
610+
// TODO: shouldn't TestType and TestUnion be available here?
611+
it('provides correct filtered suggestions on object fields in regular SDL files', () =>
612+
expect(
613+
testSuggestions('type Type {\n aField: s', new Position(0, 23), [], {
614+
uri: 'schema.graphql',
615+
}),
616+
).toEqual([
617+
{ label: 'Episode' },
618+
{ label: 'String' },
619+
{ label: 'TestInterface' },
620+
{ label: 'TestType' },
621+
{ label: 'TestUnion' },
622+
]));
623+
it('provides correct unfiltered suggestions on object fields in regular SDL files', () =>
624+
expect(
625+
testSuggestions('type Type {\n aField: ', new Position(0, 22), [], {
626+
uri: 'schema.graphql',
627+
}),
628+
).toEqual([
629+
{ label: 'AnotherInterface' },
630+
{ label: 'Boolean' },
631+
{ label: 'Character' },
632+
{ label: 'Droid' },
633+
{ label: 'Episode' },
634+
{ label: 'Human' },
635+
{ label: 'Int' },
636+
// TODO: maybe filter out types attached to top level schema?
637+
{ label: 'Query' },
638+
{ label: 'String' },
639+
{ label: 'TestInterface' },
640+
{ label: 'TestType' },
641+
{ label: 'TestUnion' },
642+
]));
610643
it('provides correct suggestions on object fields that are arrays', () =>
611644
expect(
612645
testSuggestions('type Type {\n aField: []', new Position(0, 25), [], {
@@ -626,6 +659,25 @@ describe('getAutocompleteSuggestions', () => {
626659
{ label: 'TestType' },
627660
{ label: 'TestUnion' },
628661
]));
662+
it('provides correct suggestions on object fields that are arrays in SDL context', () =>
663+
expect(
664+
testSuggestions('type Type {\n aField: []', new Position(0, 25), [], {
665+
uri: 'schema.graphql',
666+
}),
667+
).toEqual([
668+
{ label: 'AnotherInterface' },
669+
{ label: 'Boolean' },
670+
{ label: 'Character' },
671+
{ label: 'Droid' },
672+
{ label: 'Episode' },
673+
{ label: 'Human' },
674+
{ label: 'Int' },
675+
{ label: 'Query' },
676+
{ label: 'String' },
677+
{ label: 'TestInterface' },
678+
{ label: 'TestType' },
679+
{ label: 'TestUnion' },
680+
]));
629681
it('provides correct suggestions on input object fields', () =>
630682
expect(
631683
testSuggestions('input Type {\n aField: s', new Position(0, 23), [], {

packages/graphql-language-service/src/interface/getAutocompleteSuggestions.ts

Lines changed: 21 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -399,34 +399,27 @@ export function getAutocompleteSuggestions(
399399

400400
const unwrappedState = unwrapType(state);
401401

402-
if (
403-
(mode === GraphQLDocumentMode.TYPE_SYSTEM &&
404-
!unwrappedState.needsAdvance &&
405-
kind === RuleKinds.NAMED_TYPE) ||
406-
kind === RuleKinds.LIST_TYPE
407-
) {
408-
if (unwrappedState.kind === RuleKinds.FIELD_DEF) {
409-
return hintList(
410-
token,
411-
Object.values(schema.getTypeMap())
412-
.filter(type => isOutputType(type) && !type.name.startsWith('__'))
413-
.map(type => ({
414-
label: type.name,
415-
kind: CompletionItemKind.Function,
416-
})),
417-
);
418-
}
419-
if (unwrappedState.kind === RuleKinds.INPUT_VALUE_DEF) {
420-
return hintList(
421-
token,
422-
Object.values(schema.getTypeMap())
423-
.filter(type => isInputType(type) && !type.name.startsWith('__'))
424-
.map(type => ({
425-
label: type.name,
426-
kind: CompletionItemKind.Function,
427-
})),
428-
);
429-
}
402+
if (unwrappedState.kind === RuleKinds.FIELD_DEF) {
403+
return hintList(
404+
token,
405+
Object.values(schema.getTypeMap())
406+
.filter(type => isOutputType(type) && !type.name.startsWith('__'))
407+
.map(type => ({
408+
label: type.name,
409+
kind: CompletionItemKind.Function,
410+
})),
411+
);
412+
}
413+
if (unwrappedState.kind === RuleKinds.INPUT_VALUE_DEF) {
414+
return hintList(
415+
token,
416+
Object.values(schema.getTypeMap())
417+
.filter(type => isInputType(type) && !type.name.startsWith('__'))
418+
.map(type => ({
419+
label: type.name,
420+
kind: CompletionItemKind.Function,
421+
})),
422+
);
430423
}
431424

432425
// Variable definition types

0 commit comments

Comments
 (0)