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

fix preceding fragments offsetting quick-info (#78)

+5
.changeset/stupid-cobras-boil.md
···
···
+
---
+
'@0no-co/graphqlsp': patch
+
---
+
+
Fix quick-info getting offset by preceding fragments
+27 -3
packages/example/src/Pokemon.generated.ts
···
import * as Types from '../__generated__/baseGraphQLSP';
import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
export type PokemonFieldsFragment = {
-
__typename?: 'Pokemon';
id: string;
name: string;
attacks?: {
-
__typename?: 'AttacksConnection';
fast?: Array<{
-
__typename?: 'Attack';
damage?: number | null;
name?: string | null;
} | null> | null;
} | null;
};
export const PokemonFieldsFragmentDoc = {
···
},
],
} as unknown as DocumentNode<PokemonFieldsFragment, unknown>;
···
import * as Types from '../__generated__/baseGraphQLSP';
import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
export type PokemonFieldsFragment = {
+
__typename: 'Pokemon';
id: string;
name: string;
attacks?: {
+
__typename: 'AttacksConnection';
fast?: Array<{
+
__typename: 'Attack';
damage?: number | null;
name?: string | null;
} | null> | null;
} | null;
+
};
+
+
export type WeaknessFieldsFragment = {
+
__typename: 'Pokemon';
+
weaknesses?: Array<Types.PokemonType | null> | null;
};
export const PokemonFieldsFragmentDoc = {
···
},
],
} as unknown as DocumentNode<PokemonFieldsFragment, unknown>;
+
export const WeaknessFieldsFragmentDoc = {
+
kind: 'Document',
+
definitions: [
+
{
+
kind: 'FragmentDefinition',
+
name: { kind: 'Name', value: 'weaknessFields' },
+
typeCondition: {
+
kind: 'NamedType',
+
name: { kind: 'Name', value: 'Pokemon' },
+
},
+
selectionSet: {
+
kind: 'SelectionSet',
+
selections: [
+
{ kind: 'Field', name: { kind: 'Name', value: 'weaknesses' } },
+
],
+
},
+
},
+
],
+
} as unknown as DocumentNode<WeaknessFieldsFragment, unknown>;
+9
packages/example/src/Pokemon.ts
···
fragment pokemonFields on Pokemon {
id
name
attacks {
fast {
damage
···
}
}
` as typeof import('./Pokemon.generated').PokemonFieldsFragmentDoc;
export const Pokemon = (data: any) => {
const pokemon = useFragment(PokemonFields, data);
return `hi ${pokemon.name}`;
···
fragment pokemonFields on Pokemon {
id
name
+
...someUnknownFragment
attacks {
fast {
damage
···
}
}
` as typeof import('./Pokemon.generated').PokemonFieldsFragmentDoc;
+
+
export const WeakFields = gql`
+
fragment weaknessFields on Pokemon {
+
weaknesses
+
someUnknownField
+
}
+
` as typeof import('./Pokemon.generated').WeaknessFieldsFragmentDoc;
+
export const Pokemon = (data: any) => {
const pokemon = useFragment(PokemonFields, data);
return `hi ${pokemon.name}`;
+100
packages/example/src/index.generated.ts
···
} | null> | null;
};
export type PoQueryVariables = Types.Exact<{
id: Types.Scalars['ID'];
}>;
···
} | null;
};
export const PokDocument = {
kind: 'Document',
definitions: [
···
} | null> | null;
};
+
export type PokemonFieldsFragment = {
+
__typename: 'Pokemon';
+
id: string;
+
name: string;
+
resistant?: Array<Types.PokemonType | null> | null;
+
attacks?: {
+
__typename: 'AttacksConnection';
+
fast?: Array<{
+
__typename: 'Attack';
+
damage?: number | null;
+
name?: string | null;
+
} | null> | null;
+
} | null;
+
};
+
+
export type MoreFieldsFragment = {
+
__typename: 'Pokemon';
+
resistant?: Array<Types.PokemonType | null> | null;
+
};
+
export type PoQueryVariables = Types.Exact<{
id: Types.Scalars['ID'];
}>;
···
} | null;
};
+
export const MoreFieldsFragmentDoc = {
+
kind: 'Document',
+
definitions: [
+
{
+
kind: 'FragmentDefinition',
+
name: { kind: 'Name', value: 'moreFields' },
+
typeCondition: {
+
kind: 'NamedType',
+
name: { kind: 'Name', value: 'Pokemon' },
+
},
+
selectionSet: {
+
kind: 'SelectionSet',
+
selections: [
+
{ kind: 'Field', name: { kind: 'Name', value: 'resistant' } },
+
],
+
},
+
},
+
],
+
} as unknown as DocumentNode<MoreFieldsFragment, unknown>;
+
export const PokemonFieldsFragmentDoc = {
+
kind: 'Document',
+
definitions: [
+
{
+
kind: 'FragmentDefinition',
+
name: { kind: 'Name', value: 'pokemonFields' },
+
typeCondition: {
+
kind: 'NamedType',
+
name: { kind: 'Name', value: 'Pokemon' },
+
},
+
selectionSet: {
+
kind: 'SelectionSet',
+
selections: [
+
{ kind: 'Field', name: { kind: 'Name', value: 'id' } },
+
{ kind: 'Field', name: { kind: 'Name', value: 'name' } },
+
{
+
kind: 'FragmentSpread',
+
name: { kind: 'Name', value: 'moreFields' },
+
},
+
{
+
kind: 'Field',
+
name: { kind: 'Name', value: 'attacks' },
+
selectionSet: {
+
kind: 'SelectionSet',
+
selections: [
+
{
+
kind: 'Field',
+
name: { kind: 'Name', value: 'fast' },
+
selectionSet: {
+
kind: 'SelectionSet',
+
selections: [
+
{
+
kind: 'Field',
+
name: { kind: 'Name', value: 'damage' },
+
},
+
{ kind: 'Field', name: { kind: 'Name', value: 'name' } },
+
],
+
},
+
},
+
],
+
},
+
},
+
],
+
},
+
},
+
{
+
kind: 'FragmentDefinition',
+
name: { kind: 'Name', value: 'moreFields' },
+
typeCondition: {
+
kind: 'NamedType',
+
name: { kind: 'Name', value: 'Pokemon' },
+
},
+
selectionSet: {
+
kind: 'SelectionSet',
+
selections: [
+
{ kind: 'Field', name: { kind: 'Name', value: 'resistant' } },
+
],
+
},
+
},
+
],
+
} as unknown as DocumentNode<PokemonFieldsFragment, unknown>;
export const PokDocument = {
kind: 'Document',
definitions: [
+5 -2
packages/example/src/index.ts
···
import { gql, createClient } from '@urql/core';
-
import { Pokemon, PokemonFields } from './Pokemon';
const PokemonsQuery = gql`
query Pok {
pokemons {
id
name
-
__typename
fleeRate
}
}
` as typeof import('./index.generated').PokDocument;
const client = createClient({
···
import { gql, createClient } from '@urql/core';
+
import { Pokemon, PokemonFields, WeakFields } from './Pokemon';
const PokemonsQuery = gql`
query Pok {
pokemons {
id
name
fleeRate
+
__typenam
}
}
+
+
${PokemonFields}
+
${WeakFields}
` as typeof import('./index.generated').PokDocument;
const client = createClient({
+3 -1
packages/graphqlsp/src/ast/index.ts
···
isNoSubstitutionTemplateLiteral,
isTaggedTemplateExpression,
isTemplateExpression,
isToken,
} from 'typescript';
import fs from 'fs';
···
while (
isNoSubstitutionTemplateLiteral(node) ||
isToken(node) ||
-
isTemplateExpression(node)
) {
node = node.parent;
}
···
isNoSubstitutionTemplateLiteral,
isTaggedTemplateExpression,
isTemplateExpression,
+
isTemplateSpan,
isToken,
} from 'typescript';
import fs from 'fs';
···
while (
isNoSubstitutionTemplateLiteral(node) ||
isToken(node) ||
+
isTemplateExpression(node) ||
+
isTemplateSpan(node)
) {
node = node.parent;
}
+3
packages/graphqlsp/src/ast/resolve.ts
···
type TemplateResult = {
combinedText: string;
resolvedSpans: Array<{
identifier: string;
original: { start: number; length: number };
new: { start: number; length: number };
···
);
const alteredSpan = {
identifier: identifierName,
original: originalRange,
new: {
···
text.combinedText
);
const alteredSpan = {
identifier: identifierName,
original: originalRange,
new: {
···
type TemplateResult = {
combinedText: string;
resolvedSpans: Array<{
+
lines: number;
identifier: string;
original: { start: number; length: number };
new: { start: number; length: number };
···
);
const alteredSpan = {
+
lines: text.combinedText.split('\n').length,
identifier: identifierName,
original: originalRange,
new: {
···
text.combinedText
);
const alteredSpan = {
+
lines: text.combinedText.split('\n').length,
identifier: identifierName,
original: originalRange,
new: {
+5 -2
packages/graphqlsp/src/autoComplete.ts
···
const foundToken = getToken(template, cursorPosition);
if (!foundToken || !schema.current) return undefined;
-
const text = resolveTemplate(node, filename, info).combinedText;
-
const cursor = new Cursor(foundToken.line, foundToken.start);
const [suggestions, spreadSuggestions] = getSuggestionsInternal(
···
const foundToken = getToken(template, cursorPosition);
if (!foundToken || !schema.current) return undefined;
+
const { combinedText: text, resolvedSpans } = resolveTemplate(
+
node,
+
filename,
+
info
+
);
const cursor = new Cursor(foundToken.line, foundToken.start);
const [suggestions, spreadSuggestions] = getSuggestionsInternal(
+20 -1
packages/graphqlsp/src/quickInfo.ts
···
schema: { current: GraphQLSchema | null },
info: ts.server.PluginCreateInfo
): ts.QuickInfo | undefined {
const tagTemplate = info.config.template || 'gql';
const source = getSource(info, filename);
···
if (!foundToken || !schema.current) return undefined;
const hoverInfo = getHoverInformation(
schema.current,
-
resolveTemplate(node, filename, info).combinedText,
new Cursor(foundToken.line, foundToken.start)
);
···
schema: { current: GraphQLSchema | null },
info: ts.server.PluginCreateInfo
): ts.QuickInfo | undefined {
+
const logger = (msg: string) =>
+
info.project.projectService.logger.info(`[GraphQLSP] ${msg}`);
+
const tagTemplate = info.config.template || 'gql';
const source = getSource(info, filename);
···
if (!foundToken || !schema.current) return undefined;
+
const { combinedText: text, resolvedSpans } = resolveTemplate(
+
node,
+
filename,
+
info
+
);
+
+
const amountOfLines = resolvedSpans
+
.filter(
+
x =>
+
x.original.start < cursorPosition &&
+
x.original.start + x.original.length < cursorPosition
+
)
+
.reduce((acc, span) => acc + (span.lines - 1), 0);
+
+
foundToken.line = foundToken.line + amountOfLines;
+
const hoverInfo = getHoverInformation(
schema.current,
+
text,
new Cursor(foundToken.line, foundToken.start)
);
+2 -2
pnpm-lock.yaml
···
-
lockfileVersion: '6.1'
settings:
autoInstallPeers: true
···
resolution: {directory: packages/graphqlsp, type: directory}
id: file:packages/graphqlsp
name: '@0no-co/graphqlsp'
-
version: 0.7.1
dependencies:
'@graphql-codegen/add': 4.0.1(graphql@16.6.0)
'@graphql-codegen/core': 3.1.0(graphql@16.6.0)
···
+
lockfileVersion: '6.0'
settings:
autoInstallPeers: true
···
resolution: {directory: packages/graphqlsp, type: directory}
id: file:packages/graphqlsp
name: '@0no-co/graphqlsp'
+
version: 0.7.2
dependencies:
'@graphql-codegen/add': 4.0.1(graphql@16.6.0)
'@graphql-codegen/core': 3.1.0(graphql@16.6.0)