Mirror: TypeScript LSP plugin that finds GraphQL documents in your code and provides diagnostics, auto-complete and hover-information.

fix: off-by-one error in token range (regression in #15). (#244)

Co-authored-by: Jovi De Croock <decroockjovi@gmail.com>

L 183e5f0c f218751f

Changed files
+287 -4
.changeset
packages
graphqlsp
src
ast
test
e2e
fixture-project
fixtures
fixture-project-tada
fixtures
+5
.changeset/new-chairs-wonder.md
···
···
+
---
+
"@0no-co/graphqlsp": patch
+
---
+
+
fix case where the hover-information would target the wrong TypeScript node by one character
+2 -2
packages/graphqlsp/src/ast/token.ts
···
let foundToken: Token | undefined = undefined;
for (let line = 0; line < input.length; line++) {
-
const lPos = cPos;
const stream = new CharacterStream(input[line] + '\n');
while (!stream.eol()) {
const token = parser.token(stream, state);
const string = stream.current();
if (
-
lPos + stream.getStartOfToken() <= cursorPosition &&
lPos + stream.getCurrentPosition() >= cursorPosition
) {
foundToken = {
···
let foundToken: Token | undefined = undefined;
for (let line = 0; line < input.length; line++) {
+
const lPos = cPos - 1;
const stream = new CharacterStream(input[line] + '\n');
while (!stream.eol()) {
const token = parser.token(stream, state);
const string = stream.current();
if (
+
lPos + stream.getStartOfToken() + 1 <= cursorPosition &&
lPos + stream.getCurrentPosition() >= cursorPosition
) {
foundToken = {
+10
test/e2e/fixture-project-tada/fixtures/fragment.ts
···
}
`);
export const Pokemon = () => {};
···
}
`);
+
// prettier-ignore
+
export const Regression190 = graphql(`
+
fragment pokemonFields on Pokemon {
+
id
+
name
+
fleeRate
+
+
}
+
`);
+
export const Pokemon = () => {};
+6
test/e2e/fixture-project/fixtures/simple.ts
···
}
`;
const sql = (x: string | TemplateStringsArray) => x;
const x = sql`'{}'`;
···
}
`;
+
const Regression190 = gql`
+
query AllPosts {
+
+
}
+
`;
+
const sql = (x: string | TemplateStringsArray) => x;
const x = sql`'{}'`;
+74 -2
test/e2e/graphqlsp.test.ts
···
]);
}, 7500);
-
it('Gives quick-info when hovering', async () => {
server.send({
seq: 9,
type: 'request',
···
arguments: {
file: testFile,
line: 5,
-
offset: 7,
},
});
···
expect(res?.body.documentation).toEqual(
`Query.posts: [Post]\n\nList out all posts`
);
}, 7500);
});
···
]);
}, 7500);
+
it('Gives quick-info when hovering start (#15)', async () => {
server.send({
seq: 9,
type: 'request',
···
arguments: {
file: testFile,
line: 5,
+
offset: 5,
},
});
···
expect(res?.body.documentation).toEqual(
`Query.posts: [Post]\n\nList out all posts`
);
+
}, 7500);
+
+
it('Handles empty line (#190)', async () => {
+
server.send({
+
seq: 10,
+
type: 'request',
+
command: 'completionInfo',
+
arguments: {
+
file: testFile,
+
line: 14,
+
offset: 3,
+
includeExternalModuleExports: true,
+
includeInsertTextCompletions: true,
+
triggerKind: 1,
+
},
+
});
+
+
await server.waitForResponse(
+
response =>
+
response.type === 'response' && response.command === 'completionInfo'
+
);
+
+
const res = server.responses
+
.reverse()
+
.find(
+
resp => resp.type === 'response' && resp.command === 'completionInfo'
+
);
+
+
expect(res).toBeDefined();
+
expect(typeof res?.body.entries).toEqual('object');
+
const defaultAttrs = { kind: 'var', kindModifiers: 'declare' };
+
expect(res?.body.entries).toEqual([
+
{
+
...defaultAttrs,
+
name: 'post',
+
sortText: '0post',
+
labelDetails: { detail: ' Post' },
+
},
+
{
+
...defaultAttrs,
+
name: 'posts',
+
sortText: '1posts',
+
labelDetails: { detail: ' [Post]', description: 'List out all posts' },
+
},
+
{
+
...defaultAttrs,
+
name: '__typename',
+
sortText: '2__typename',
+
labelDetails: {
+
detail: ' String!',
+
description: 'The name of the current Object type at runtime.',
+
},
+
},
+
{
+
...defaultAttrs,
+
name: '__schema',
+
sortText: '3__schema',
+
labelDetails: {
+
detail: ' __Schema!',
+
description: 'Access the current type schema of this server.',
+
},
+
},
+
{
+
...defaultAttrs,
+
name: '__type',
+
sortText: '4__type',
+
labelDetails: {
+
detail: ' __Type',
+
description: 'Request the type information of a single type.',
+
},
+
},
+
]);
}, 7500);
});
+190
test/e2e/tada.test.ts
···
]
`);
}, 30000);
});
···
]
`);
}, 30000);
+
+
it('gives quick-info at start of word (#15)', async () => {
+
server.send({
+
seq: 11,
+
type: 'request',
+
command: 'quickinfo',
+
arguments: {
+
file: outfileCombinations,
+
line: 7,
+
offset: 5,
+
},
+
});
+
+
await server.waitForResponse(
+
response =>
+
response.type === 'response' && response.command === 'quickinfo'
+
);
+
+
const res = server.responses
+
.reverse()
+
.find(resp => resp.type === 'response' && resp.command === 'quickinfo');
+
+
expect(res).toBeDefined();
+
expect(typeof res?.body).toEqual('object');
+
expect(res?.body.documentation).toEqual(`Pokemon.name: String!`);
+
}, 30000);
+
+
it('gives suggestions with empty line (#190)', async () => {
+
server.send({
+
seq: 12,
+
type: 'request',
+
command: 'completionInfo',
+
arguments: {
+
file: outfileCombinations,
+
line: 19,
+
offset: 3,
+
includeExternalModuleExports: true,
+
includeInsertTextCompletions: true,
+
triggerKind: 1,
+
},
+
});
+
+
await server.waitForResponse(
+
response =>
+
response.type === 'response' && response.command === 'completionInfo'
+
);
+
+
const res = server.responses
+
.reverse()
+
.find(
+
resp => resp.type === 'response' && resp.command === 'completionInfo'
+
);
+
+
expect(res).toBeDefined();
+
expect(typeof res?.body.entries).toEqual('object');
+
expect(res?.body.entries).toMatchInlineSnapshot(`
+
[
+
{
+
"kind": "var",
+
"kindModifiers": "declare",
+
"labelDetails": {
+
"detail": " AttacksConnection",
+
},
+
"name": "attacks",
+
"sortText": "0attacks",
+
},
+
{
+
"kind": "var",
+
"kindModifiers": "declare",
+
"labelDetails": {
+
"detail": " [EvolutionRequirement]",
+
},
+
"name": "evolutionRequirements",
+
"sortText": "2evolutionRequirements",
+
},
+
{
+
"kind": "var",
+
"kindModifiers": "declare",
+
"labelDetails": {
+
"detail": " [Pokemon]",
+
},
+
"name": "evolutions",
+
"sortText": "3evolutions",
+
},
+
{
+
"kind": "var",
+
"kindModifiers": "declare",
+
"labelDetails": {
+
"description": "Likelihood of an attempt to catch a Pokémon to fail.",
+
"detail": " Float",
+
},
+
"name": "fleeRate",
+
"sortText": "4fleeRate",
+
},
+
{
+
"kind": "var",
+
"kindModifiers": "declare",
+
"labelDetails": {
+
"detail": " PokemonDimension",
+
},
+
"name": "height",
+
"sortText": "5height",
+
},
+
{
+
"kind": "var",
+
"kindModifiers": "declare",
+
"labelDetails": {
+
"detail": " ID!",
+
},
+
"name": "id",
+
"sortText": "6id",
+
},
+
{
+
"kind": "var",
+
"kindModifiers": "declare",
+
"labelDetails": {
+
"description": "Maximum combat power a Pokémon may achieve at max level.",
+
"detail": " Int",
+
},
+
"name": "maxCP",
+
"sortText": "7maxCP",
+
},
+
{
+
"kind": "var",
+
"kindModifiers": "declare",
+
"labelDetails": {
+
"description": "Maximum health points a Pokémon may achieve at max level.",
+
"detail": " Int",
+
},
+
"name": "maxHP",
+
"sortText": "8maxHP",
+
},
+
{
+
"kind": "var",
+
"kindModifiers": "declare",
+
"labelDetails": {
+
"detail": " String!",
+
},
+
"name": "name",
+
"sortText": "9name",
+
},
+
{
+
"kind": "var",
+
"kindModifiers": "declare",
+
"labelDetails": {
+
"detail": " [PokemonType]",
+
},
+
"name": "resistant",
+
"sortText": "10resistant",
+
},
+
{
+
"kind": "var",
+
"kindModifiers": "declare",
+
"labelDetails": {
+
"detail": " [PokemonType]",
+
},
+
"name": "types",
+
"sortText": "11types",
+
},
+
{
+
"kind": "var",
+
"kindModifiers": "declare",
+
"labelDetails": {
+
"detail": " [PokemonType]",
+
},
+
"name": "weaknesses",
+
"sortText": "12weaknesses",
+
},
+
{
+
"kind": "var",
+
"kindModifiers": "declare",
+
"labelDetails": {
+
"detail": " PokemonDimension",
+
},
+
"name": "weight",
+
"sortText": "13weight",
+
},
+
{
+
"kind": "var",
+
"kindModifiers": "declare",
+
"labelDetails": {
+
"description": "The name of the current Object type at runtime.",
+
"detail": " String!",
+
},
+
"name": "__typename",
+
"sortText": "14__typename",
+
},
+
]
+
`);
+
}, 30000);
});