Skip to content

Commit 8934219

Browse files
committed
feat: add relay-lsp style locateCommand
1 parent db58db0 commit 8934219

File tree

5 files changed

+164
-72
lines changed

5 files changed

+164
-72
lines changed

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

Lines changed: 38 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
print,
2222
isTypeDefinitionNode,
2323
ArgumentNode,
24+
typeFromAST,
2425
} from 'graphql';
2526

2627
import {
@@ -41,7 +42,6 @@ import {
4142
getDefinitionQueryResultForDefinitionNode,
4243
getDefinitionQueryResultForNamedType,
4344
getDefinitionQueryResultForField,
44-
DefinitionQueryResult,
4545
getASTNodeAtPosition,
4646
getTokenAtPosition,
4747
getTypeInfo,
@@ -57,7 +57,10 @@ import {
5757
SymbolInformation,
5858
SymbolKind,
5959
} from 'vscode-languageserver-types';
60-
import { getDefinitionQueryResultForArgument } from 'graphql-language-service/src/interface';
60+
import {
61+
DefinitionQueryResponse,
62+
getDefinitionQueryResultForArgument,
63+
} from 'graphql-language-service/src/interface';
6164

6265
const KIND_TO_SYMBOL_KIND: { [key: string]: SymbolKind } = {
6366
[Kind.FIELD]: SymbolKind.Field,
@@ -275,12 +278,17 @@ export class GraphQLLanguageService {
275278
query: string,
276279
position: IPosition,
277280
filePath: Uri,
278-
): Promise<DefinitionQueryResult | null> {
281+
): Promise<DefinitionQueryResponse | null> {
279282
const projectConfig = this.getConfigForURI(filePath);
283+
280284
if (!projectConfig) {
281285
return null;
282286
}
283287

288+
const schema = await this._graphQLCache.getSchema(projectConfig.name);
289+
if (!schema) {
290+
return null;
291+
}
284292
let ast;
285293
try {
286294
ast = parse(query);
@@ -289,55 +297,67 @@ export class GraphQLLanguageService {
289297
}
290298

291299
const node = getASTNodeAtPosition(query, ast, position);
300+
// @ts-expect-error
301+
const type = node && typeFromAST(schema, node);
302+
303+
let queryResult: DefinitionQueryResponse | null = null;
292304
if (node) {
293305
switch (node.kind) {
294306
case Kind.FRAGMENT_SPREAD:
295-
return this._getDefinitionForFragmentSpread(
307+
queryResult = await this._getDefinitionForFragmentSpread(
296308
query,
297309
ast,
298310
node,
299311
filePath,
300312
projectConfig,
301313
);
302-
314+
break;
303315
case Kind.FRAGMENT_DEFINITION:
304316
case Kind.OPERATION_DEFINITION:
305-
return getDefinitionQueryResultForDefinitionNode(
317+
queryResult = getDefinitionQueryResultForDefinitionNode(
306318
filePath,
307319
query,
308320
node,
309321
);
310-
322+
break;
311323
case Kind.NAMED_TYPE:
312-
return this._getDefinitionForNamedType(
324+
queryResult = await this._getDefinitionForNamedType(
313325
query,
314326
ast,
315327
node,
316328
filePath,
317329
projectConfig,
318330
);
319-
331+
break;
320332
case Kind.FIELD:
321-
return this._getDefinitionForField(
333+
queryResult = await this._getDefinitionForField(
322334
query,
323335
ast,
324336
node,
325337
filePath,
326338
projectConfig,
327339
position,
328340
);
329-
341+
break;
330342
case Kind.ARGUMENT:
331-
return this._getDefinitionForArgument(
343+
queryResult = await this._getDefinitionForArgument(
332344
query,
333345
ast,
334346
node,
335347
filePath,
336348
projectConfig,
337349
position,
338350
);
351+
break;
339352
}
340353
}
354+
if (queryResult) {
355+
return {
356+
...queryResult,
357+
node,
358+
type,
359+
};
360+
}
341361
return null;
342362
}
343363

@@ -396,7 +416,7 @@ export class GraphQLLanguageService {
396416
node: NamedTypeNode,
397417
filePath: Uri,
398418
projectConfig: GraphQLProjectConfig,
399-
): Promise<DefinitionQueryResult | null> {
419+
): Promise<DefinitionQueryResponse | null> {
400420
const objectTypeDefinitions =
401421
await this._graphQLCache.getObjectTypeDefinitions(projectConfig);
402422

@@ -414,13 +434,11 @@ export class GraphQLLanguageService {
414434
definition,
415435
}));
416436

417-
const result = await getDefinitionQueryResultForNamedType(
437+
return getDefinitionQueryResultForNamedType(
418438
query,
419439
node,
420440
dependencies.concat(localOperationDefinitionInfos),
421441
);
422-
423-
return result;
424442
}
425443

426444
async _getDefinitionForField(
@@ -446,14 +464,11 @@ export class GraphQLLanguageService {
446464
// TODO: need something like getObjectTypeDependenciesForAST?
447465
const dependencies = [...objectTypeDefinitions.values()];
448466

449-
const result = await getDefinitionQueryResultForField(
467+
return getDefinitionQueryResultForField(
450468
fieldName,
451469
parentTypeName,
452470
dependencies,
453-
typeInfo,
454471
);
455-
456-
return result;
457472
}
458473

459474
return null;
@@ -483,15 +498,12 @@ export class GraphQLLanguageService {
483498
// TODO: need something like getObjectTypeDependenciesForAST?
484499
const dependencies = [...objectTypeDefinitions.values()];
485500

486-
const result = await getDefinitionQueryResultForArgument(
501+
return getDefinitionQueryResultForArgument(
487502
argumentName,
488503
fieldName,
489504
parentTypeName,
490505
dependencies,
491-
typeInfo,
492506
);
493-
494-
return result;
495507
}
496508

497509
return null;
@@ -503,7 +515,7 @@ export class GraphQLLanguageService {
503515
node: FragmentSpreadNode,
504516
filePath: Uri,
505517
projectConfig: GraphQLProjectConfig,
506-
): Promise<DefinitionQueryResult | null> {
518+
): Promise<DefinitionQueryResponse | null> {
507519
const fragmentDefinitions = await this._graphQLCache.getFragmentDefinitions(
508520
projectConfig,
509521
);
@@ -528,13 +540,11 @@ export class GraphQLLanguageService {
528540
}),
529541
);
530542

531-
const result = await getDefinitionQueryResultForFragmentSpread(
543+
return getDefinitionQueryResultForFragmentSpread(
532544
query,
533545
node,
534546
dependencies.concat(localFragInfos),
535547
);
536-
537-
return result;
538548
}
539549
async getOutline(documentText: string): Promise<Outline | null> {
540550
return getOutline(documentText);

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

Lines changed: 77 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,14 @@ import type { UnnormalizedTypeDefPointer } from '@graphql-tools/load';
5757
import { getGraphQLCache, GraphQLCache } from './GraphQLCache';
5858
import { parseDocument } from './parseDocument';
5959

60-
import { printSchema, visit, parse, FragmentDefinitionNode } from 'graphql';
60+
import {
61+
printSchema,
62+
visit,
63+
parse,
64+
FragmentDefinitionNode,
65+
GraphQLType,
66+
ASTNode,
67+
} from 'graphql';
6168
import { tmpdir } from 'node:os';
6269
import {
6370
ConfigEmptyError,
@@ -82,6 +89,31 @@ type CachedDocumentType = {
8289
version: number;
8390
contents: CachedContent[];
8491
};
92+
93+
type AdditionalLocateInfo = {
94+
node?: ASTNode | null;
95+
type?: GraphQLType | null;
96+
project: GraphQLProjectConfig;
97+
};
98+
99+
type RelayLSPLocateCommand = (
100+
// either Type, Type.field or Type.field(argument)
101+
projectName: string,
102+
typeName: string,
103+
info: AdditionalLocateInfo,
104+
) => string;
105+
106+
type GraphQLLocateCommand = (
107+
projectName: string,
108+
typeName: string,
109+
info: AdditionalLocateInfo,
110+
) => {
111+
range: RangeType;
112+
uri: string;
113+
};
114+
115+
type LocateCommand = RelayLSPLocateCommand | GraphQLLocateCommand;
116+
85117
function toPosition(position: VscodePosition): IPosition {
86118
return new Position(position.line, position.character);
87119
}
@@ -789,7 +821,7 @@ export class MessageProcessor {
789821
const { textDocument, position } = params;
790822
const project = this._graphQLCache.getProjectForFile(textDocument.uri);
791823
const cachedDocument = this._getCachedDocument(textDocument.uri);
792-
if (!cachedDocument) {
824+
if (!cachedDocument || !project) {
793825
return [];
794826
}
795827

@@ -830,6 +862,10 @@ export class MessageProcessor {
830862
},
831863
});
832864
} catch {}
865+
866+
const locateCommand = project?.extensions?.languageService
867+
?.locateCommand as LocateCommand | undefined;
868+
833869
const formatted = result
834870
? result.definitions.map(res => {
835871
const defRange = res.range as Range;
@@ -857,10 +893,48 @@ export class MessageProcessor {
857893
);
858894
}
859895
}
896+
if (locateCommand && result.printedName) {
897+
try {
898+
const locateResult = locateCommand(
899+
project.name,
900+
result.printedName,
901+
{
902+
node: result.node,
903+
type: result.type,
904+
project,
905+
},
906+
);
907+
if (typeof locateResult === 'string') {
908+
const [uri, startLine, endLine] = locateResult.split(':');
909+
return {
910+
uri,
911+
range: new Range(
912+
new Position(parseInt(startLine, 10), 0),
913+
new Position(parseInt(endLine, 10), 0),
914+
),
915+
};
916+
}
917+
return (
918+
locateResult || {
919+
uri: res.path,
920+
range: defRange,
921+
}
922+
);
923+
} catch (error) {
924+
this._logger.error(
925+
'There was an error executing user defined locateCommand\n\n' +
926+
(error as Error).toString(),
927+
);
928+
return {
929+
uri: res.path,
930+
range: defRange,
931+
};
932+
}
933+
}
860934
return {
861935
uri: res.path,
862936
range: defRange,
863-
} as Location;
937+
};
864938
})
865939
: [];
866940

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

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ describe('MessageProcessor with config', () => {
160160
textDocument: { uri: project.uri('query.graphql') },
161161
position: { character: 16, line: 0 },
162162
});
163+
163164
expect(firstQueryDefRequest[0].uri).toEqual(
164165
project.uri('fragments.graphql'),
165166
);
@@ -173,6 +174,7 @@ describe('MessageProcessor with config', () => {
173174
character: 1,
174175
},
175176
});
177+
176178
// change the file to make the fragment invalid
177179
project.changeFile(
178180
'schema.graphql',
@@ -256,7 +258,7 @@ describe('MessageProcessor with config', () => {
256258
// simulating codegen
257259
project.changeFile(
258260
'fragments.graphql',
259-
'fragment A on Foo { bar }\n\nfragment B on Test { test }',
261+
'fragment A on Foo { bad }\n\nfragment B on Test { test }',
260262
);
261263
await project.lsp.handleWatchedFilesChangedNotification({
262264
changes: [
@@ -271,6 +273,21 @@ describe('MessageProcessor with config', () => {
271273
);
272274
expect(fragCache?.get('A')?.definition.name.value).toEqual('A');
273275
expect(fragCache?.get('B')?.definition.name.value).toEqual('B');
276+
const queryFieldDefRequest = await project.lsp.handleDefinitionRequest({
277+
textDocument: { uri: project.uri('fragments.graphql') },
278+
position: { character: 22, line: 0 },
279+
});
280+
expect(queryFieldDefRequest[0].uri).toEqual(project.uri('schema.graphql'));
281+
expect(serializeRange(queryFieldDefRequest[0].range)).toEqual({
282+
start: {
283+
line: 8,
284+
character: 11,
285+
},
286+
end: {
287+
line: 8,
288+
character: 19,
289+
},
290+
});
274291

275292
// on the second request, the position has changed
276293
const secondQueryDefRequest = await project.lsp.handleDefinitionRequest({
@@ -348,7 +365,7 @@ describe('MessageProcessor with config', () => {
348365

349366
// ensure that fragment definitions work
350367
const definitions = await project.lsp.handleDefinitionRequest({
351-
textDocument: { uri: project.uri('query.graphql') },
368+
textDocument: { uri: project.uri('query.graphql') }, // console.log(project.uri('query.graphql'))
352369
position: { character: 26, line: 0 },
353370
});
354371
expect(definitions[0].uri).toEqual(project.uri('fragments.graphql'));

0 commit comments

Comments
 (0)