Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1248 from Quramy/goto_def
feat: Add getDefinitionAtPosition
- Loading branch information
Showing
10 changed files
with
372 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
const assert = require('assert'); | ||
const path = require('path'); | ||
const { mark } = require('fretted-strings'); | ||
|
||
function findResponse(responses, commandName) { | ||
return responses.find(response => response.command === commandName); | ||
} | ||
|
||
async function run(server) { | ||
const fileFragments = path.resolve(__dirname, '../../project-fixtures/simple-prj/fragments.ts'); | ||
const fileMain = path.resolve(__dirname, '../../project-fixtures/simple-prj/main.ts'); | ||
const frets = {}; | ||
const fileFragmentsContent = ` | ||
const fragment = gql\` | ||
fragment MyFragment on Query { | ||
hello | ||
} | ||
\`; | ||
`; | ||
const fileMainContent = mark( | ||
` | ||
const query = gql\` | ||
query MyQuery { | ||
...MyFragment | ||
%%% ^ %%% | ||
%%% p %%% | ||
} | ||
\`; | ||
`, | ||
frets, | ||
); | ||
server.send({ | ||
command: 'open', | ||
arguments: { file: fileFragments, fileContent: fileFragmentsContent, scriptKindName: 'TS' }, | ||
}); | ||
server.send({ command: 'open', arguments: { file: fileMain, fileContent: fileMainContent, scriptKindName: 'TS' } }); | ||
|
||
await server.waitEvent('projectLoadingFinish'); | ||
|
||
server.send({ | ||
command: 'definition', | ||
arguments: { file: fileMain, offset: frets.p.character + 1, line: frets.p.line + 1, prefix: '' }, | ||
}); | ||
|
||
await server.waitResponse('definition'); | ||
|
||
server.send({ | ||
command: 'definitionAndBoundSpan', | ||
arguments: { file: fileMain, offset: frets.p.character + 1, line: frets.p.line + 1, prefix: '' }, | ||
}); | ||
|
||
await server.waitResponse('definitionAndBoundSpan'); | ||
|
||
await server.close(); | ||
|
||
const definitionResponse = findResponse(server.responses, 'definition'); | ||
assert(!!definitionResponse); | ||
assert(definitionResponse.body.length === 1); | ||
assert(definitionResponse.body[0].file === fileFragments); | ||
|
||
const definitionAndBoundSpanResponse = findResponse(server.responses, 'definitionAndBoundSpan'); | ||
assert(!!definitionAndBoundSpanResponse); | ||
assert(definitionAndBoundSpanResponse.body.definitions.length === 1); | ||
assert(definitionAndBoundSpanResponse.body.definitions[0].file === fileFragments); | ||
} | ||
|
||
module.exports = run; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
140 changes: 140 additions & 0 deletions
140
src/graphql-language-service-adapter/get-definition-and-bound-span.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
import ts from 'typescript'; | ||
import { mark, type Frets } from 'fretted-strings'; | ||
import { AdapterFixture } from './testing/adapter-fixture'; | ||
|
||
function createFixture(name: string) { | ||
return new AdapterFixture(name); | ||
} | ||
|
||
describe('getDefinitionAndBoundSpan', () => { | ||
const delegateFn = jest.fn(() => undefined); | ||
|
||
beforeEach(() => { | ||
delegateFn.mockClear(); | ||
}); | ||
|
||
it.each([ | ||
{ | ||
name: 'cursor on not template leteral', | ||
source: ` | ||
const query = 100; | ||
%%% ^ %%% | ||
%%% s1 %%% | ||
`, | ||
}, | ||
{ | ||
name: 'incomplete operation', | ||
source: ` | ||
const query = \` | ||
query MyQuery { | ||
...MyFragment | ||
%%% ^ %%% | ||
%%% s1 %%% | ||
\`; | ||
`, | ||
}, | ||
{ | ||
name: 'cursor on not fragment spread', | ||
source: ` | ||
const query = \` | ||
query MyQuery { | ||
...MyFragment | ||
%%% ^ %%% | ||
%%% s1 %%% | ||
} | ||
fragment MyFragment on Query { | ||
__typename | ||
} | ||
\`; | ||
`, | ||
}, | ||
{ | ||
name: 'not exsisting fragment definition', | ||
source: ` | ||
const query = \` | ||
query MyQuery { | ||
...MyFragment | ||
%%% ^ %%% | ||
%%% s1 %%% | ||
} | ||
fragment OtherFragment on Query { | ||
__typename | ||
} | ||
\`; | ||
`, | ||
}, | ||
])('should return no definition info for $name .', ({ source }) => { | ||
const fixture = createFixture('input.ts'); | ||
const frets: Frets = {}; | ||
fixture.source = mark(source, frets); | ||
fixture.adapter.getDefinitionAndBoundSpan(delegateFn, 'input.ts', frets.s1.pos); | ||
expect(delegateFn).toHaveBeenCalledTimes(1); | ||
}); | ||
|
||
it('should return definition info when cursor is on fragment spread', () => { | ||
const fixture = createFixture('input.ts'); | ||
const frets: Frets = {}; | ||
fixture.source = mark( | ||
` | ||
const query = \` | ||
query MyQuery { | ||
...MyFragment | ||
%%% ^ ^ %%% | ||
%%% s1 s2 %%% | ||
} | ||
fragment MyFragment on Query { | ||
%%% ^ ^ %%% | ||
%%% d1 d2 %%% | ||
__typename | ||
} | ||
\`; | ||
`, | ||
frets, | ||
); | ||
const actual = fixture.adapter.getDefinitionAndBoundSpan(delegateFn, 'input.ts', frets.s1.pos); | ||
expect(actual).toMatchObject({ | ||
textSpan: { | ||
start: frets.s1.pos, | ||
length: frets.s2.pos - frets.s1.pos, | ||
}, | ||
definitions: [ | ||
{ | ||
fileName: 'input.ts', | ||
textSpan: { | ||
start: frets.d1.pos, | ||
length: frets.d2.pos - frets.d1.pos, | ||
}, | ||
}, | ||
] as Partial<ts.DefinitionInfo>[], | ||
}); | ||
}); | ||
|
||
it('should return definition to other file', () => { | ||
const fixture = createFixture('input.ts'); | ||
const frets: Frets = {}; | ||
fixture.registerFragment( | ||
'fragments.ts', | ||
` | ||
fragment MyFragment on Query { | ||
__typename | ||
} | ||
`, | ||
).source = mark( | ||
` | ||
const query = \` | ||
query MyQuery { | ||
...MyFragment | ||
%%% ^ %%% | ||
%%% s1 %%% | ||
} | ||
\`; | ||
`, | ||
frets, | ||
); | ||
const actual = fixture.adapter.getDefinitionAndBoundSpan(delegateFn, 'input.ts', frets.s1.pos); | ||
expect(actual?.definitions?.[0].fileName).toBe('fragments.ts'); | ||
}); | ||
}); |
50 changes: 50 additions & 0 deletions
50
src/graphql-language-service-adapter/get-definition-and-bound-span.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
import ts from 'typescript'; | ||
import { visit, type FragmentSpreadNode } from 'graphql'; | ||
import { getSanitizedTemplateText } from '../ts-ast-util'; | ||
import type { AnalysisContext, GetDefinitionAndBoundSpan } from './types'; | ||
|
||
export function getDefinitionAndBoundSpan( | ||
ctx: AnalysisContext, | ||
delegate: GetDefinitionAndBoundSpan, | ||
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 { text, sourcePosition } = getSanitizedTemplateText(node); | ||
const documentNode = ctx.getGraphQLDocumentNode(text); | ||
if (!documentNode) return delegate(fileName, position); | ||
const innerPosition = position - sourcePosition; | ||
let fragmentSpreadNodeUnderCursor: FragmentSpreadNode | undefined; | ||
visit(documentNode, { | ||
FragmentSpread: node => { | ||
if (node.name.loc!.start <= innerPosition && innerPosition < node.name.loc!.end) { | ||
fragmentSpreadNodeUnderCursor = node; | ||
} | ||
}, | ||
}); | ||
if (!fragmentSpreadNodeUnderCursor) return delegate(fileName, position); | ||
const foundDefinitionDetail = ctx.getGlobalFragmentDefinitionEntry(fragmentSpreadNodeUnderCursor.name.value); | ||
if (!foundDefinitionDetail) return delegate(fileName, position); | ||
const definitionSourcePosition = foundDefinitionDetail.position + foundDefinitionDetail.node.name.loc!.start; | ||
return { | ||
textSpan: { | ||
start: sourcePosition + fragmentSpreadNodeUnderCursor.name.loc!.start, | ||
length: fragmentSpreadNodeUnderCursor.name.loc!.end - fragmentSpreadNodeUnderCursor.name.loc!.start, | ||
}, | ||
definitions: [ | ||
{ | ||
fileName: foundDefinitionDetail.fileName, | ||
name: foundDefinitionDetail.node.name.value, | ||
textSpan: { | ||
start: definitionSourcePosition, | ||
length: foundDefinitionDetail.node.name.loc!.end - foundDefinitionDetail.node.name.loc!.start, | ||
}, | ||
kind: ts.ScriptElementKind.unknown, | ||
containerKind: ts.ScriptElementKind.unknown, | ||
containerName: '', | ||
}, | ||
], | ||
} satisfies ts.DefinitionInfoAndBoundSpan; | ||
} |
56 changes: 56 additions & 0 deletions
56
src/graphql-language-service-adapter/get-definition-at-position.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
import { mark, type Frets } from 'fretted-strings'; | ||
import { AdapterFixture } from './testing/adapter-fixture'; | ||
|
||
function createFixture(name: string) { | ||
return new AdapterFixture(name); | ||
} | ||
|
||
describe('getDefinitionAtPosition', () => { | ||
const delegateFn = jest.fn(() => []); | ||
|
||
it('should not return definition info when the cursor does not point fragment spread', () => { | ||
const fixture = createFixture('input.ts'); | ||
const frets: Frets = {}; | ||
fixture.source = mark( | ||
` | ||
const query = \` | ||
query MyQuery { | ||
%%% ^ %%% | ||
%%% cur %%% | ||
...MyFragment | ||
} | ||
fragment MyFragment on Query { | ||
__typename | ||
} | ||
\`; | ||
`, | ||
frets, | ||
); | ||
const actual = fixture.adapter.getDefinitionAtPosition(delegateFn, 'input.ts', frets.cur.pos); | ||
expect(actual?.length).toBe(0); | ||
}); | ||
|
||
it('should return definition of fragment spread under cursor', () => { | ||
const fixture = createFixture('input.ts'); | ||
const frets: Frets = {}; | ||
fixture.source = mark( | ||
` | ||
const query = \` | ||
query MyQuery { | ||
...MyFragment | ||
%%% ^ %%% | ||
%%% cur %%% | ||
} | ||
fragment MyFragment on Query { | ||
__typename | ||
} | ||
\`; | ||
`, | ||
frets, | ||
); | ||
const actual = fixture.adapter.getDefinitionAtPosition(delegateFn, 'input.ts', frets.cur.pos); | ||
expect(actual?.length).toBe(1); | ||
}); | ||
}); |
13 changes: 13 additions & 0 deletions
13
src/graphql-language-service-adapter/get-definition-at-position.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import type { AnalysisContext, GetDefinitionAtPosition } from './types'; | ||
import { getDefinitionAndBoundSpan } from './get-definition-and-bound-span'; | ||
|
||
export function getDefinitionAtPosition( | ||
ctx: AnalysisContext, | ||
delegate: GetDefinitionAtPosition, | ||
fileName: string, | ||
position: number, | ||
) { | ||
const result = getDefinitionAndBoundSpan(ctx, () => undefined, fileName, position); | ||
if (!result) return delegate(fileName, position); | ||
return result.definitions; | ||
} |
Oops, something went wrong.