diff --git a/src/graphql-language-service-adapter/get-definition-at-position.test.ts b/src/graphql-language-service-adapter/get-definition-at-position.test.ts new file mode 100644 index 00000000..d1a6ac96 --- /dev/null +++ b/src/graphql-language-service-adapter/get-definition-at-position.test.ts @@ -0,0 +1,44 @@ +import ts from 'typescript'; +import { mark, type Frets } from 'fretted-strings'; +import { AdapterFixture } from './testing/adapter-fixture'; +import { getDefinitionAtPosition } from './get-definition-at-position'; + +function createFixture(name: string) { + return new AdapterFixture(name); +} + +describe(getDefinitionAtPosition, () => { + const delegateFn = jest.fn(() => []); + it('should works', () => { + const fixture = createFixture('input.ts'); + const frets: Frets = {}; + fixture.source = mark( + ` + const query = \` + query MyQuery { + ...MyFragment + %%% ^ %%% + %%% cur %%% + } + + fragment MyFragment on Query { + %%% ^ ^ %%% + %%% d1 d2 %%% + __typename + } + \` + `, + frets, + ); + const actual = fixture.adapter.getDefinitionAtPosition(delegateFn, 'input.ts', frets.cur.pos); + expect(actual).toMatchObject([ + { + fileName: 'input.ts', + textSpan: { + start: frets.d1.pos, + length: frets.d2.pos - frets.d1.pos, + }, + }, + ] as Partial[]); + }); +}); diff --git a/src/graphql-language-service-adapter/get-definition-at-position.ts b/src/graphql-language-service-adapter/get-definition-at-position.ts new file mode 100644 index 00000000..364faa54 --- /dev/null +++ b/src/graphql-language-service-adapter/get-definition-at-position.ts @@ -0,0 +1,54 @@ +import ts from 'typescript'; +import { visit, type FragmentSpreadNode } from 'graphql'; +import { getFragmentsInDocument } from '../gql-ast-util'; +import type { AnalysisContext, GetDefinitionAtPosition } from './types'; + +export function getDefinitionAtPosition( + ctx: AnalysisContext, + delegate: GetDefinitionAtPosition, + fileName: string, + position: number, +) { + if (ctx.getScriptSourceHelper().isExcluded(fileName)) return delegate(fileName, position); + const node = ctx.findAscendantTemplateNode(fileName, position); + if (!node) return delegate(fileName, position); + const { resolvedInfo } = ctx.resolveTemplateInfo(fileName, node); + if (!resolvedInfo) return delegate(fileName, position); + const { combinedText, getInnerPosition, getSourcePosition } = resolvedInfo; + const documentNode = ctx.getGraphQLDocumentNode(combinedText); + if (!documentNode) return delegate(fileName, position); + + try { + const innerPosition = getInnerPosition(position); + if (innerPosition.isInOtherExpression) return delegate(fileName, position); + let fragmentSpreadNodeUnderCursor: FragmentSpreadNode | undefined = undefined; + visit(documentNode, { + FragmentSpread: node => { + if (node.loc!.start <= innerPosition.pos && innerPosition.pos < node.loc!.end) { + fragmentSpreadNodeUnderCursor = node; + } + }, + }); + if (!fragmentSpreadNodeUnderCursor) return delegate(fileName, position); + const foundFragmentDefinitionNode = getFragmentsInDocument(documentNode).find( + fragmentDef => fragmentDef.name.value === fragmentSpreadNodeUnderCursor?.name.value, + ); + if (!foundFragmentDefinitionNode) return delegate(fileName, position); + const definitionSourcePosition = getSourcePosition(foundFragmentDefinitionNode.name.loc!.start); + return [ + { + fileName: definitionSourcePosition.fileName, + name: foundFragmentDefinitionNode.name.value, + textSpan: { + start: definitionSourcePosition.pos, + length: foundFragmentDefinitionNode.name.loc!.end - foundFragmentDefinitionNode.name.loc!.start, + }, + kind: ts.ScriptElementKind.unknown, + containerKind: ts.ScriptElementKind.unknown, + containerName: '', + }, + ] satisfies ts.DefinitionInfo[]; + } catch { + return delegate(fileName, position); + } +} diff --git a/src/graphql-language-service-adapter/graphql-language-service-adapter.ts b/src/graphql-language-service-adapter/graphql-language-service-adapter.ts index c2218aed..4cbb7dd6 100644 --- a/src/graphql-language-service-adapter/graphql-language-service-adapter.ts +++ b/src/graphql-language-service-adapter/graphql-language-service-adapter.ts @@ -9,10 +9,17 @@ import { } from '../ts-ast-util'; import type { SchemaBuildErrorInfo } from '../schema-manager/schema-manager'; import { getFragmentNamesInDocument, detectDuplicatedFragments, type FragmentRegistry } from '../gql-ast-util'; -import type { AnalysisContext, GetCompletionAtPosition, GetSemanticDiagnostics, GetQuickInfoAtPosition } from './types'; +import type { + AnalysisContext, + GetCompletionAtPosition, + GetSemanticDiagnostics, + GetQuickInfoAtPosition, + GetDefinitionAtPosition, +} from './types'; import { getCompletionAtPosition } from './get-completion-at-position'; import { getSemanticDiagnostics } from './get-semantic-diagnostics'; import { getQuickInfoAtPosition } from './get-quick-info-at-position'; +import { getDefinitionAtPosition } from './get-definition-at-position'; import { LRUCache } from '../cache'; export interface GraphQLLanguageServiceAdapterCreateOptions { @@ -58,6 +65,10 @@ export class GraphQLLanguageServiceAdapter { return getQuickInfoAtPosition(this._analysisContext, delegate, ...args); } + getDefinitionAtPosition(delegate: GetDefinitionAtPosition, ...args: Parameters) { + return getDefinitionAtPosition(this._analysisContext, delegate, ...args); + } + updateSchema(errors: SchemaBuildErrorInfo[] | null, schema: GraphQLSchema | null) { if (errors) { this._schemaErrors = errors; diff --git a/src/graphql-language-service-adapter/types.ts b/src/graphql-language-service-adapter/types.ts index 9c3225d4..94f9426f 100644 --- a/src/graphql-language-service-adapter/types.ts +++ b/src/graphql-language-service-adapter/types.ts @@ -6,6 +6,7 @@ import type { SchemaBuildErrorInfo } from '../schema-manager/schema-manager'; export type GetCompletionAtPosition = ts.LanguageService['getCompletionsAtPosition']; export type GetSemanticDiagnostics = ts.LanguageService['getSemanticDiagnostics']; export type GetQuickInfoAtPosition = ts.LanguageService['getQuickInfoAtPosition']; +export type GetDefinitionAtPosition = ts.LanguageService['getDefinitionAtPosition']; export interface AnalysisContext { debug(msg: string): void; diff --git a/src/language-service-plugin/plugin-module-factory.ts b/src/language-service-plugin/plugin-module-factory.ts index 2e2b4cce..6e3f0864 100644 --- a/src/language-service-plugin/plugin-module-factory.ts +++ b/src/language-service-plugin/plugin-module-factory.ts @@ -74,6 +74,7 @@ function create(info: ts.server.PluginCreateInfo): ts.LanguageService { .wrap('getCompletionsAtPosition', delegate => adapter.getCompletionAtPosition.bind(adapter, delegate)) .wrap('getSemanticDiagnostics', delegate => adapter.getSemanticDiagnostics.bind(adapter, delegate)) .wrap('getQuickInfoAtPosition', delegate => adapter.getQuickInfoAtPosition.bind(adapter, delegate)) + .wrap('getDefinitionAtPosition', delegate => adapter.getDefinitionAtPosition.bind(adapter, delegate)) .build(); schemaManager.registerOnChange(adapter.updateSchema.bind(adapter));