From b9f3d7e6f7ded6979a469180a6f8c093531319c6 Mon Sep 17 00:00:00 2001 From: Rikki Date: Tue, 5 Jan 2021 16:29:32 -0500 Subject: [PATCH] feat: add external fragment execution to GraphiQL --- .../src/__tests__/hint-test.js | 1 + .../src/api/hooks/useQueryFacts.ts | 4 +- .../src/utility/getQueryFacts.ts | 7 +- packages/graphiql/package.json | 1 + packages/graphiql/src/components/GraphiQL.tsx | 63 ++++++++++++--- .../utility/__tests__/getQueryFacts.spec.ts | 2 +- .../graphiql/src/utility/getQueryFacts.ts | 26 +++++-- packages/graphiql/tsconfig.esm.json | 5 ++ packages/graphiql/tsconfig.json | 5 ++ .../src/GraphQLLanguageService.ts | 10 +-- .../src/getAutocompleteSuggestions.ts | 6 +- .../src/getDefinition.ts | 17 +++-- .../src/getDiagnostics.ts | 4 +- .../src/getHoverInformation.ts | 4 +- .../src/getOutline.ts | 13 +++- .../src/MessageProcessor.ts | 24 +++--- .../src/__tests__/MessageProcessor-test.ts | 2 +- .../src/index.js.flow | 4 +- .../src/index.ts | 34 ++++----- .../src/Range.ts | 19 ++--- .../src/__tests__/Range-test.ts | 6 +- .../__tests__/getASTNodeAtPosition-test.ts | 2 +- .../src/fragmentDependencies.ts | 76 +++++++++++++++++++ .../src/getASTNodeAtPosition.ts | 2 +- .../src/index.js.flow | 8 +- .../src/index.ts | 5 ++ .../src/LanguageService.ts | 6 +- .../graphql-language-service/src/index.ts | 1 + packages/monaco-graphql/src/utils.ts | 11 ++- yarn.lock | 11 +-- 30 files changed, 271 insertions(+), 108 deletions(-) create mode 100644 packages/graphql-language-service-utils/src/fragmentDependencies.ts diff --git a/packages/codemirror-graphql/src/__tests__/hint-test.js b/packages/codemirror-graphql/src/__tests__/hint-test.js index ed8595dc1eb..1c1bccf55e2 100644 --- a/packages/codemirror-graphql/src/__tests__/hint-test.js +++ b/packages/codemirror-graphql/src/__tests__/hint-test.js @@ -40,6 +40,7 @@ function createEditorWithHint() { schema: TestSchema, closeOnUnfocus: false, completeSingle: false, + externalFragments: [], }, }); } diff --git a/packages/graphiql-2-rfc-context/src/api/hooks/useQueryFacts.ts b/packages/graphiql-2-rfc-context/src/api/hooks/useQueryFacts.ts index 04583bbd3b3..a478f29ae82 100644 --- a/packages/graphiql-2-rfc-context/src/api/hooks/useQueryFacts.ts +++ b/packages/graphiql-2-rfc-context/src/api/hooks/useQueryFacts.ts @@ -8,14 +8,14 @@ import { useMemo } from 'react'; -import getQueryFacts from '../../utility/getQueryFacts'; +import getOperationFacts from '../../utility/getQueryFacts'; import useSchema from './useSchema'; import useOperation from './useOperation'; export default function useQueryFacts() { const schema = useSchema(); const { text } = useOperation(); - return useMemo(() => (schema ? getQueryFacts(schema, text) : null), [ + return useMemo(() => (schema ? getOperationFacts(schema, text) : null), [ schema, text, ]); diff --git a/packages/graphiql-2-rfc-context/src/utility/getQueryFacts.ts b/packages/graphiql-2-rfc-context/src/utility/getQueryFacts.ts index edf7bd74817..e49119c5554 100644 --- a/packages/graphiql-2-rfc-context/src/utility/getQueryFacts.ts +++ b/packages/graphiql-2-rfc-context/src/utility/getQueryFacts.ts @@ -13,6 +13,7 @@ import { OperationDefinitionNode, NamedTypeNode, GraphQLNamedType, + Kind, } from 'graphql'; export type VariableToType = { @@ -30,7 +31,7 @@ export type QueryFacts = { * * If the query cannot be parsed, returns undefined. */ -export default function getQueryFacts( +export default function getOperationFacts( schema?: GraphQLSchema, documentStr?: string | null, ): QueryFacts | undefined { @@ -52,7 +53,7 @@ export default function getQueryFacts( // Collect operations by their names. const operations: OperationDefinitionNode[] = []; documentAST.definitions.forEach(def => { - if (def.kind === 'OperationDefinition') { + if (def.kind === Kind.OPERATION_DEFINITION) { operations.push(def); } }); @@ -71,7 +72,7 @@ export function collectVariables( [variable: string]: GraphQLNamedType; } = Object.create(null); documentAST.definitions.forEach(definition => { - if (definition.kind === 'OperationDefinition') { + if (definition.kind === Kind.OPERATION_DEFINITION) { const variableDefinitions = definition.variableDefinitions; if (variableDefinitions) { variableDefinitions.forEach(({ variable, type }) => { diff --git a/packages/graphiql/package.json b/packages/graphiql/package.json index dd63a34bb11..dbfc5cfeacb 100644 --- a/packages/graphiql/package.json +++ b/packages/graphiql/package.json @@ -45,6 +45,7 @@ "codemirror": "^5.54.0", "codemirror-graphql": "^0.14.0", "copy-to-clipboard": "^3.2.0", + "graphql-language-service": "^3.0.2", "entities": "^2.0.0", "markdown-it": "^10.0.0" }, diff --git a/packages/graphiql/src/components/GraphiQL.tsx b/packages/graphiql/src/components/GraphiQL.tsx index 07c6a5920b6..55376bdc6f8 100644 --- a/packages/graphiql/src/components/GraphiQL.tsx +++ b/packages/graphiql/src/components/GraphiQL.tsx @@ -17,13 +17,16 @@ import { GraphQLSchema, parse, print, + visit, OperationDefinitionNode, IntrospectionQuery, GraphQLType, ValidationRule, FragmentDefinitionNode, + DocumentNode, } from 'graphql'; import copyToClipboard from 'copy-to-clipboard'; +import { getFragmentDependenciesForAST } from 'graphql-language-service'; import { ExecuteButton } from './ExecuteButton'; import { ImagePreview } from './ImagePreview'; @@ -38,7 +41,7 @@ import { DocExplorer } from './DocExplorer'; import { QueryHistory } from './QueryHistory'; import CodeMirrorSizer from '../utility/CodeMirrorSizer'; import StorageAPI, { Storage } from '../utility/StorageAPI'; -import getQueryFacts, { VariableToType } from '../utility/getQueryFacts'; +import getOperationFacts, { VariableToType } from '../utility/getQueryFacts'; import getSelectedOperationName from '../utility/getSelectedOperationName'; import debounce from '../utility/debounce'; import find from '../utility/find'; @@ -80,6 +83,7 @@ export type FetcherParams = { export type FetcherOpts = { headers?: { [key: string]: any }; shouldPersistHeaders: boolean; + documentAST?: DocumentNode; }; export type FetcherResult = @@ -125,7 +129,7 @@ export type GraphiQLProps = { shouldPersistHeaders?: boolean; externalFragments?: string | FragmentDefinitionNode[]; onCopyQuery?: (query?: string) => void; - onEditQuery?: (query?: string) => void; + onEditQuery?: (query?: string, documentAST?: DocumentNode) => void; onEditVariables?: (value: string) => void; onEditHeaders?: (value: string) => void; onEditOperationName?: (operationName: string) => void; @@ -160,6 +164,7 @@ export type GraphiQLState = { subscription?: Unsubscribable | null; variableToType?: VariableToType; operations?: OperationDefinitionNode[]; + documentAST?: DocumentNode; }; /** @@ -227,8 +232,7 @@ export class GraphiQL extends React.Component { : defaultQuery; // Get the initial query facts. - const queryFacts = getQueryFacts(props.schema, query); - + const queryFacts = getOperationFacts(props.schema, query); // Determine the initial variables to display. const variables = props.variables !== undefined @@ -814,6 +818,7 @@ export class GraphiQL extends React.Component { const fetcherOpts: FetcherOpts = { shouldPersistHeaders: Boolean(this.props.shouldPersistHeaders), + documentAST: this.state.documentAST, }; if (this.state.headers && this.state.headers.trim().length > 2) { fetcherOpts.headers = JSON.parse(this.state.headers); @@ -873,7 +878,7 @@ export class GraphiQL extends React.Component { if (typeof result !== 'string' && 'data' in result) { const schema = buildClientSchema(result.data); - const queryFacts = getQueryFacts(schema, this.state.query); + const queryFacts = getOperationFacts(schema, this.state.query); this.safeSetState({ schema, ...queryFacts }); } else { const responseString = @@ -926,6 +931,38 @@ export class GraphiQL extends React.Component { if (typeof jsonHeaders !== 'object') { throw new Error('Headers are not a JSON object.'); } + // TODO: memoize this + if (this.props.externalFragments) { + const externalFragments = new Map(); + + if (Array.isArray(this.props.externalFragments)) { + this.props.externalFragments.forEach(def => { + externalFragments.set(def.name.value, def); + }); + } else { + visit( + parse(this.props.externalFragments, { + experimentalFragmentVariables: true, + }), + { + FragmentDefinition(def) { + externalFragments.set(def.name.value, def); + }, + }, + ); + } + const fragmentDependencies = getFragmentDependenciesForAST( + this.state.documentAST!, + externalFragments, + ); + if (fragmentDependencies.length > 0) { + query += + '\n' + + fragmentDependencies + .map((node: FragmentDefinitionNode) => print(node)) + .join('\n'); + } + } const fetch = fetcher( { @@ -933,7 +970,11 @@ export class GraphiQL extends React.Component { variables: jsonVariables, operationName, }, - { headers: jsonHeaders, shouldPersistHeaders }, + { + headers: jsonHeaders, + shouldPersistHeaders, + documentAST: this.state.documentAST, + }, ); if (isPromise(fetch)) { @@ -1117,7 +1158,9 @@ export class GraphiQL extends React.Component { handlePrettifyQuery = () => { const editor = this.getQueryEditor(); const editorContent = editor?.getValue() ?? ''; - const prettifiedEditorContent = print(parse(editorContent)); + const prettifiedEditorContent = print( + parse(editorContent, { experimentalFragmentVariables: true }), + ); if (prettifiedEditorContent !== editorContent) { editor?.setValue(prettifiedEditorContent); @@ -1164,7 +1207,7 @@ export class GraphiQL extends React.Component { return; } - const ast = parse(query); + const ast = this.state.documentAST!; editor.setValue(print(mergeAST(ast, this.state.schema))); }; @@ -1181,7 +1224,7 @@ export class GraphiQL extends React.Component { }); this._storage.set('query', value); if (this.props.onEditQuery) { - return this.props.onEditQuery(value); + return this.props.onEditQuery(value, queryFacts?.documentAST); } }); @@ -1206,7 +1249,7 @@ export class GraphiQL extends React.Component { prevOperations?: OperationDefinitionNode[], schema?: GraphQLSchema, ) => { - const queryFacts = getQueryFacts(schema, query); + const queryFacts = getOperationFacts(schema, query); if (queryFacts) { // Update operation name should any query names change. const updatedOperationName = getSelectedOperationName( diff --git a/packages/graphiql/src/utility/__tests__/getQueryFacts.spec.ts b/packages/graphiql/src/utility/__tests__/getQueryFacts.spec.ts index 6cbd934aa55..bab933a2666 100644 --- a/packages/graphiql/src/utility/__tests__/getQueryFacts.spec.ts +++ b/packages/graphiql/src/utility/__tests__/getQueryFacts.spec.ts @@ -16,7 +16,7 @@ import { parse, } from 'graphql'; -import { collectVariables } from '../getQueryFacts'; +import { collectVariables } from '../getOperationFacts'; describe('collectVariables', () => { const TestType = new GraphQLObjectType({ diff --git a/packages/graphiql/src/utility/getQueryFacts.ts b/packages/graphiql/src/utility/getQueryFacts.ts index edf7bd74817..068db3c7768 100644 --- a/packages/graphiql/src/utility/getQueryFacts.ts +++ b/packages/graphiql/src/utility/getQueryFacts.ts @@ -13,6 +13,7 @@ import { OperationDefinitionNode, NamedTypeNode, GraphQLNamedType, + visit, } from 'graphql'; export type VariableToType = { @@ -22,15 +23,16 @@ export type VariableToType = { export type QueryFacts = { variableToType?: VariableToType; operations?: OperationDefinitionNode[]; + documentAST?: DocumentNode; }; /** - * Provided previous "queryFacts", a GraphQL schema, and a query document + * Provided previous "operationFacts", a GraphQL schema, and a query document * string, return a set of facts about that query useful for GraphiQL features. * * If the query cannot be parsed, returns undefined. */ -export default function getQueryFacts( +export default function getOperationFacts( schema?: GraphQLSchema, documentStr?: string | null, ): QueryFacts | undefined { @@ -40,7 +42,9 @@ export default function getQueryFacts( let documentAST: DocumentNode; try { - documentAST = parse(documentStr); + documentAST = parse(documentStr, { + experimentalFragmentVariables: true, + }); } catch { return; } @@ -51,15 +55,21 @@ export default function getQueryFacts( // Collect operations by their names. const operations: OperationDefinitionNode[] = []; - documentAST.definitions.forEach(def => { - if (def.kind === 'OperationDefinition') { - operations.push(def); - } + + visit(documentAST, { + OperationDefinition(node) { + operations.push(node); + }, }); - return { variableToType, operations }; + return { variableToType, operations, documentAST }; } +/** + * as a nod to folks who were clever enough to import this utility on their + */ +export const getQueryFacts = getOperationFacts; + /** * Provided a schema and a document, produces a `variableToType` Object. */ diff --git a/packages/graphiql/tsconfig.esm.json b/packages/graphiql/tsconfig.esm.json index aea6efbc174..678fccdd294 100644 --- a/packages/graphiql/tsconfig.esm.json +++ b/packages/graphiql/tsconfig.esm.json @@ -18,5 +18,10 @@ "**/*-test.js", "**/*.stories.js", "**/*.stories.ts" + ], + "references": [ + { + "path": "../graphql-language-service/tsconfig.esm.json" + } ] } diff --git a/packages/graphiql/tsconfig.json b/packages/graphiql/tsconfig.json index 458ba460e72..85ac2efcc27 100644 --- a/packages/graphiql/tsconfig.json +++ b/packages/graphiql/tsconfig.json @@ -19,5 +19,10 @@ "**/*.stories.js", "**/*.stories.ts", "**/*.stories.tsx" + ], + "references": [ + { + "path": "../graphql-language-service" + } ] } diff --git a/packages/graphql-language-service-interface/src/GraphQLLanguageService.ts b/packages/graphql-language-service-interface/src/GraphQLLanguageService.ts index e9d3c59c6a1..fe52782a3ee 100644 --- a/packages/graphql-language-service-interface/src/GraphQLLanguageService.ts +++ b/packages/graphql-language-service-interface/src/GraphQLLanguageService.ts @@ -18,10 +18,9 @@ import { import { CompletionItem, - DefinitionQueryResult, Diagnostic, Uri, - Position, + IPosition, Outline, OutlineTree, GraphQLCache, @@ -42,6 +41,7 @@ import { getDefinitionQueryResultForFragmentSpread, getDefinitionQueryResultForDefinitionNode, getDefinitionQueryResultForNamedType, + DefinitionQueryResult, } from './getDefinition'; import { getOutline } from './getOutline'; @@ -221,7 +221,7 @@ export class GraphQLLanguageService { public async getAutocompleteSuggestions( query: string, - position: Position, + position: IPosition, filePath: Uri, ): Promise> { const projectConfig = this.getConfigForURI(filePath); @@ -248,7 +248,7 @@ export class GraphQLLanguageService { public async getHoverInformation( query: string, - position: Position, + position: IPosition, filePath: Uri, ): Promise { const projectConfig = this.getConfigForURI(filePath); @@ -262,7 +262,7 @@ export class GraphQLLanguageService { public async getDefinition( query: string, - position: Position, + position: IPosition, filePath: Uri, ): Promise { const projectConfig = this.getConfigForURI(filePath); diff --git a/packages/graphql-language-service-interface/src/getAutocompleteSuggestions.ts b/packages/graphql-language-service-interface/src/getAutocompleteSuggestions.ts index 3481f27bdf0..7fc58f2f617 100644 --- a/packages/graphql-language-service-interface/src/getAutocompleteSuggestions.ts +++ b/packages/graphql-language-service-interface/src/getAutocompleteSuggestions.ts @@ -26,7 +26,7 @@ import { import { CompletionItem, AllTypeInfo, - Position, + IPosition, } from 'graphql-language-service-types'; import { @@ -70,7 +70,7 @@ import { export function getAutocompleteSuggestions( schema: GraphQLSchema, queryText: string, - cursor: Position, + cursor: IPosition, contextToken?: ContextToken, fragmentDefs?: FragmentDefinitionNode[], ): Array { @@ -665,7 +665,7 @@ function getSuggestionsForDirective( export function getTokenAtPosition( queryText: string, - cursor: Position, + cursor: IPosition, ): ContextToken { let styleAtCursor = null; let stateAtCursor = null; diff --git a/packages/graphql-language-service-interface/src/getDefinition.ts b/packages/graphql-language-service-interface/src/getDefinition.ts index 47955a685be..9298fd897de 100644 --- a/packages/graphql-language-service-interface/src/getDefinition.ts +++ b/packages/graphql-language-service-interface/src/getDefinition.ts @@ -19,15 +19,22 @@ import { import { Definition, - DefinitionQueryResult, FragmentInfo, - Position, - Range, Uri, ObjectTypeInfo, } from 'graphql-language-service-types'; -import { locToRange, offsetToPosition } from 'graphql-language-service-utils'; +import { + locToRange, + offsetToPosition, + Range, + Position, +} from 'graphql-language-service-utils'; + +export type DefinitionQueryResult = { + queryRange: Range[]; + definitions: Definition[]; +}; export const LANGUAGE = 'GraphQL'; @@ -40,7 +47,7 @@ function assert(value: any, message: string) { function getRange(text: string, node: ASTNode): Range { const location = node.loc as Location; assert(location, 'Expected ASTNode to have a location.'); - return locToRange(text, location); + return locToRange(text, location) as Range; } function getPosition(text: string, node: ASTNode): Position { diff --git a/packages/graphql-language-service-interface/src/getDiagnostics.ts b/packages/graphql-language-service-interface/src/getDiagnostics.ts index bd55bbf407e..d2474634b49 100644 --- a/packages/graphql-language-service-interface/src/getDiagnostics.ts +++ b/packages/graphql-language-service-interface/src/getDiagnostics.ts @@ -29,6 +29,8 @@ import { import { DiagnosticSeverity, Diagnostic } from 'vscode-languageserver-types'; +import { IRange } from 'graphql-language-service-types'; + // this doesn't work without the 'as', kinda goofy export const SEVERITY = { @@ -152,7 +154,7 @@ function annotations( return highlightedNodes; } -export function getRange(location: SourceLocation, queryText: string): Range { +export function getRange(location: SourceLocation, queryText: string): IRange { const parser = onlineParser(); const state = parser.startState(); const lines = queryText.split('\n'); diff --git a/packages/graphql-language-service-interface/src/getHoverInformation.ts b/packages/graphql-language-service-interface/src/getHoverInformation.ts index 0a2e3568b5d..516063d5eb8 100644 --- a/packages/graphql-language-service-interface/src/getHoverInformation.ts +++ b/packages/graphql-language-service-interface/src/getHoverInformation.ts @@ -21,7 +21,7 @@ import { GraphQLFieldConfig, } from 'graphql'; import { ContextToken } from 'graphql-language-service-parser'; -import { AllTypeInfo, Position } from 'graphql-language-service-types'; +import { AllTypeInfo, IPosition } from 'graphql-language-service-types'; import { Hover } from 'vscode-languageserver-types'; import { getTokenAtPosition, getTypeInfo } from './getAutocompleteSuggestions'; @@ -29,7 +29,7 @@ import { getTokenAtPosition, getTypeInfo } from './getAutocompleteSuggestions'; export function getHoverInformation( schema: GraphQLSchema, queryText: string, - cursor: Position, + cursor: IPosition, contextToken?: ContextToken, ): Hover['contents'] { const token = contextToken || getTokenAtPosition(queryText, cursor); diff --git a/packages/graphql-language-service-interface/src/getOutline.ts b/packages/graphql-language-service-interface/src/getOutline.ts index 6cb63738681..5bc35f3d63e 100644 --- a/packages/graphql-language-service-interface/src/getOutline.ts +++ b/packages/graphql-language-service-interface/src/getOutline.ts @@ -7,7 +7,12 @@ * */ -import { Outline, TextToken, TokenKind } from 'graphql-language-service-types'; +import { + Outline, + TextToken, + TokenKind, + IPosition, +} from 'graphql-language-service-types'; import { Kind, @@ -30,7 +35,7 @@ import { FieldDefinitionNode, EnumValueDefinitionNode, } from 'graphql'; -import { offsetToPosition, Position } from 'graphql-language-service-utils'; +import { offsetToPosition } from 'graphql-language-service-utils'; const { INLINE_FRAGMENT } = Kind; @@ -59,8 +64,8 @@ export type OutlineableKinds = keyof typeof OUTLINEABLE_KINDS; type OutlineTreeResult = | { representativeName: string; - startPosition: Position; - endPosition: Position; + startPosition: IPosition; + endPosition: IPosition; children: SelectionSetNode[] | []; tokenizedText: TextToken[]; } diff --git a/packages/graphql-language-service-server/src/MessageProcessor.ts b/packages/graphql-language-service-server/src/MessageProcessor.ts index 26a1d3e27fe..476177a331a 100644 --- a/packages/graphql-language-service-server/src/MessageProcessor.ts +++ b/packages/graphql-language-service-server/src/MessageProcessor.ts @@ -16,15 +16,13 @@ import { Uri, GraphQLConfig, GraphQLProjectConfig, -} from 'graphql-language-service'; - -import { GraphQLLanguageService, FileChangeTypeKind, + Range, + Position, + IPosition, } from 'graphql-language-service'; -import { Range, Position } from 'graphql-language-service-utils'; - import type { CompletionParams, FileEvent, @@ -48,6 +46,7 @@ import type { DidChangeWatchedFilesParams, InitializeParams, Range as RangeType, + Position as VscodePosition, TextDocumentPositionParams, DocumentSymbolParams, SymbolInformation, @@ -77,6 +76,9 @@ type CachedDocumentType = { version: number; contents: CachedContent[]; }; +function toPosition(position: VscodePosition): IPosition { + return new Position(position.line, position.character); +} export class MessageProcessor { _connection: IConnection; @@ -471,7 +473,7 @@ export class MessageProcessor { const found = cachedDocument.contents.find(content => { const currentRange = content.range; - if (currentRange && currentRange.containsPosition(position)) { + if (currentRange && currentRange.containsPosition(toPosition(position))) { return true; } }); @@ -488,7 +490,7 @@ export class MessageProcessor { } const result = await this._languageService.getAutocompleteSuggestions( query, - position, + toPosition(position), textDocument.uri, ); @@ -523,7 +525,7 @@ export class MessageProcessor { const found = cachedDocument.contents.find(content => { const currentRange = content.range; - if (currentRange && currentRange.containsPosition(position)) { + if (currentRange && currentRange.containsPosition(toPosition(position))) { return true; } }); @@ -540,7 +542,7 @@ export class MessageProcessor { } const result = await this._languageService.getHoverInformation( query, - position, + toPosition(position), textDocument.uri, ); @@ -651,7 +653,7 @@ export class MessageProcessor { const found = cachedDocument.contents.find(content => { const currentRange = content.range; - if (currentRange && currentRange.containsPosition(position)) { + if (currentRange && currentRange.containsPosition(toPosition(position))) { return true; } }); @@ -671,7 +673,7 @@ export class MessageProcessor { try { result = await this._languageService.getDefinition( query, - position, + toPosition(position), textDocument.uri, ); } catch (err) { diff --git a/packages/graphql-language-service-server/src/__tests__/MessageProcessor-test.ts b/packages/graphql-language-service-server/src/__tests__/MessageProcessor-test.ts index 7b918e5835a..9d2c190e062 100644 --- a/packages/graphql-language-service-server/src/__tests__/MessageProcessor-test.ts +++ b/packages/graphql-language-service-server/src/__tests__/MessageProcessor-test.ts @@ -8,7 +8,7 @@ */ import { tmpdir } from 'os'; import { SymbolKind } from 'vscode-languageserver'; -import { Position, Range } from 'graphql-language-service-utils'; +import { Position, IRange } from 'graphql-language-service-utils'; import { MessageProcessor } from '../MessageProcessor'; import { parseDocument } from '../parseDocument'; diff --git a/packages/graphql-language-service-types/src/index.js.flow b/packages/graphql-language-service-types/src/index.js.flow index 5cd597da323..f62d1d36f74 100644 --- a/packages/graphql-language-service-types/src/index.js.flow +++ b/packages/graphql-language-service-types/src/index.js.flow @@ -148,13 +148,13 @@ export interface GraphQLCache { } // online-parser related -export interface Position { +export interface IPosition { line: number; character: number; lessThanOrEqualTo: (position: Position) => boolean; } -export interface Range { +export interface IRange { start: Position; end: Position; containsPosition: (position: Position) => boolean; diff --git a/packages/graphql-language-service-types/src/index.ts b/packages/graphql-language-service-types/src/index.ts index d472322a5a3..0fb2f1e9455 100644 --- a/packages/graphql-language-service-types/src/index.ts +++ b/packages/graphql-language-service-types/src/index.ts @@ -107,21 +107,24 @@ export interface GraphQLCache { } // online-parser related -export type Position = { +export interface IPosition { line: number; character: number; - lessThanOrEqualTo?: (position: Position) => boolean; -}; - -export interface Range { - start: Position; - end: Position; - containsPosition: (position: Position) => boolean; + setLine(line: number): void; + setCharacter(character: number): void; + lessThanOrEqualTo(position: IPosition): boolean; } +export interface IRange { + start: IPosition; + end: IPosition; + setEnd(line: number, character: number): void; + setStart(line: number, character: number): void; + containsPosition(position: IPosition): boolean; +} export type CachedContent = { query: string; - range: Range | null; + range: IRange | null; }; // GraphQL Language Service related types @@ -192,19 +195,14 @@ export type CompletionItem = CompletionItemType & { // Definitions/hyperlink export type Definition = { path: Uri; - position: Position; - range?: Range; + position: IPosition; + range?: IRange; id?: string; name?: string; language?: string; projectRoot?: Uri; }; -export type DefinitionQueryResult = { - queryRange: Range[]; - definitions: Definition[]; -}; - // Outline view export type TokenKind = | 'keyword' @@ -228,8 +226,8 @@ export type OutlineTree = { tokenizedText?: TokenizedText; representativeName?: string; kind: string; - startPosition: Position; - endPosition?: Position; + startPosition: IPosition; + endPosition?: IPosition; children: OutlineTree[]; }; diff --git a/packages/graphql-language-service-utils/src/Range.ts b/packages/graphql-language-service-utils/src/Range.ts index 473a3e6a676..d36c36e2914 100644 --- a/packages/graphql-language-service-utils/src/Range.ts +++ b/packages/graphql-language-service-utils/src/Range.ts @@ -8,15 +8,12 @@ */ import { Location } from 'graphql/language'; -import { - Range as RangeInterface, - Position as PositionInterface, -} from 'graphql-language-service-types'; +import { IRange, IPosition } from 'graphql-language-service-types'; -export class Range implements RangeInterface { - start: PositionInterface; - end: PositionInterface; - constructor(start: PositionInterface, end: PositionInterface) { +export class Range implements IRange { + start: IPosition; + end: IPosition; + constructor(start: IPosition, end: IPosition) { this.start = start; this.end = end; } @@ -29,7 +26,7 @@ export class Range implements RangeInterface { this.end = new Position(line, character); } - containsPosition = (position: PositionInterface): boolean => { + containsPosition = (position: IPosition): boolean => { if (this.start.line === position.line) { return this.start.character <= position.character; } else if (this.end.line === position.line) { @@ -40,7 +37,7 @@ export class Range implements RangeInterface { }; } -export class Position implements PositionInterface { +export class Position implements IPosition { line: number; character: number; constructor(line: number, character: number) { @@ -56,7 +53,7 @@ export class Position implements PositionInterface { this.character = character; } - lessThanOrEqualTo = (position: PositionInterface): boolean => + lessThanOrEqualTo = (position: IPosition): boolean => this.line < position.line || (this.line === position.line && this.character <= position.character); } diff --git a/packages/graphql-language-service-utils/src/__tests__/Range-test.ts b/packages/graphql-language-service-utils/src/__tests__/Range-test.ts index 5d60b7e05ee..128d406f354 100644 --- a/packages/graphql-language-service-utils/src/__tests__/Range-test.ts +++ b/packages/graphql-language-service-utils/src/__tests__/Range-test.ts @@ -29,14 +29,14 @@ const offsetRangeStart = new Position(1, 2); const offsetRangeEnd = new Position(1, 5); describe('Position', () => { - it('constructs a Position object', () => { + it('constructs a IPosition object', () => { const pos = new Position(3, 5); expect(pos).not.toBeUndefined(); expect(pos.character).toEqual(5); expect(pos.line).toEqual(3); }); - it('compares Position objects', () => { + it('compares IPosition objects', () => { const posA = new Position(1, 2); const posB = new Position(2, 2); const posC = new Position(2, 3); @@ -57,7 +57,7 @@ describe('Range', () => { range = new Range(start, end); }); - it('constructs a Range object', () => { + it('constructs a IRange object', () => { expect(range).not.toBeUndefined(); expect(range.start).toEqual(start); expect(range.end).toEqual(end); diff --git a/packages/graphql-language-service-utils/src/__tests__/getASTNodeAtPosition-test.ts b/packages/graphql-language-service-utils/src/__tests__/getASTNodeAtPosition-test.ts index 26b7eb40387..8b47a8e544c 100644 --- a/packages/graphql-language-service-utils/src/__tests__/getASTNodeAtPosition-test.ts +++ b/packages/graphql-language-service-utils/src/__tests__/getASTNodeAtPosition-test.ts @@ -7,7 +7,7 @@ */ import { parse } from 'graphql'; -import { Position } from '../Range'; +import { IPosition } from '../Range'; import { getASTNodeAtPosition, pointToOffset } from '../getASTNodeAtPosition'; const doc = ` diff --git a/packages/graphql-language-service-utils/src/fragmentDependencies.ts b/packages/graphql-language-service-utils/src/fragmentDependencies.ts new file mode 100644 index 00000000000..a0a7ff0949f --- /dev/null +++ b/packages/graphql-language-service-utils/src/fragmentDependencies.ts @@ -0,0 +1,76 @@ +import { DocumentNode, FragmentDefinitionNode, parse, visit } from 'graphql'; +import nullthrows from 'nullthrows'; + +export const getFragmentDependencies = ( + operationString: string, + fragmentDefinitions?: Map | null, +): FragmentDefinitionNode[] => { + // If there isn't context for fragment references, + // return an empty array. + if (!fragmentDefinitions) { + return []; + } + // If the operation cannot be parsed, validations cannot happen yet. + // Return an empty array. + let parsedOperation; + try { + parsedOperation = parse(operationString, { + allowLegacySDLImplementsInterfaces: true, + allowLegacySDLEmptyFields: true, + }); + } catch (error) { + return []; + } + return getFragmentDependenciesForAST(parsedOperation, fragmentDefinitions); +}; + +export const getFragmentDependenciesForAST = ( + parsedOperation: DocumentNode, + fragmentDefinitions: Map, +): FragmentDefinitionNode[] => { + if (!fragmentDefinitions) { + return []; + } + + const existingFrags = new Map(); + const referencedFragNames = new Set(); + + visit(parsedOperation, { + FragmentDefinition(node) { + existingFrags.set(node.name.value, true); + }, + FragmentSpread(node) { + if (!referencedFragNames.has(node.name.value)) { + referencedFragNames.add(node.name.value); + } + }, + }); + + const asts = new Set(); + referencedFragNames.forEach(name => { + if (!existingFrags.has(name) && fragmentDefinitions.has(name)) { + asts.add(nullthrows(fragmentDefinitions.get(name))); + } + }); + + const referencedFragments: FragmentDefinitionNode[] = []; + + asts.forEach(ast => { + visit(ast, { + FragmentSpread(node) { + if ( + !referencedFragNames.has(node.name.value) && + fragmentDefinitions.get(node.name.value) + ) { + asts.add(nullthrows(fragmentDefinitions.get(node.name.value))); + referencedFragNames.add(node.name.value); + } + }, + }); + if (!existingFrags.has(ast.name.value)) { + referencedFragments.push(ast); + } + }); + + return referencedFragments; +}; diff --git a/packages/graphql-language-service-utils/src/getASTNodeAtPosition.ts b/packages/graphql-language-service-utils/src/getASTNodeAtPosition.ts index 4246b59fac1..d306436c867 100644 --- a/packages/graphql-language-service-utils/src/getASTNodeAtPosition.ts +++ b/packages/graphql-language-service-utils/src/getASTNodeAtPosition.ts @@ -9,7 +9,7 @@ import { ASTNode } from 'graphql/language'; -import { Position as TPosition } from 'graphql-language-service-types'; +import { IPosition as TPosition } from 'graphql-language-service-types'; import { visit } from 'graphql'; export function getASTNodeAtPosition( diff --git a/packages/graphql-language-service-utils/src/index.js.flow b/packages/graphql-language-service-utils/src/index.js.flow index f80080eade0..ad3abef4d4c 100644 --- a/packages/graphql-language-service-utils/src/index.js.flow +++ b/packages/graphql-language-service-utils/src/index.js.flow @@ -10,18 +10,18 @@ import type { Location } from 'graphql/language'; import type { - Range as RangeInterface, - Position as PositionInterface, + IRange as RangeInterface, + IPosition as PositionInterface, } from 'graphql-language-service-types'; declare export function getASTNodeAtPosition(): void; -declare export class Position implements PositionInterface { +declare export class IPosition implements PositionInterface { line: number; character: number; constructor(row: number, column: number): Position; lessThanOrEqualTo: (position: PositionInterface) => boolean; } -declare export class Range implements RangeInterface { +declare export class IRange implements RangeInterface { start: PositionInterface; end: PositionInterface; constructor(start: Position, end: Position): Range; diff --git a/packages/graphql-language-service-utils/src/index.ts b/packages/graphql-language-service-utils/src/index.ts index 11e7d6e676f..6667d4a367d 100644 --- a/packages/graphql-language-service-utils/src/index.ts +++ b/packages/graphql-language-service-utils/src/index.ts @@ -7,6 +7,11 @@ * */ +export { + getFragmentDependencies, + getFragmentDependenciesForAST, +} from './fragmentDependencies'; + export { getASTNodeAtPosition, pointToOffset } from './getASTNodeAtPosition'; export { Position, Range, locToRange, offsetToPosition } from './Range'; diff --git a/packages/graphql-language-service/src/LanguageService.ts b/packages/graphql-language-service/src/LanguageService.ts index 5f7951cc264..90e84c5fb02 100644 --- a/packages/graphql-language-service/src/LanguageService.ts +++ b/packages/graphql-language-service/src/LanguageService.ts @@ -12,7 +12,7 @@ import { FragmentDefinitionNode, visit, } from 'graphql'; -import type { Position } from 'graphql-language-service-types'; +import type { IPosition } from 'graphql-language-service-types'; import { getAutocompleteSuggestions, getDiagnostics, @@ -172,7 +172,7 @@ export class LanguageService { public getCompletion = async ( _uri: string, documentText: string, - position: Position, + position: IPosition, ) => { const schema = await this.getSchema(); if (!documentText || documentText.length < 1 || !schema) { @@ -202,7 +202,7 @@ export class LanguageService { public getHover = async ( _uri: string, documentText: string, - position: Position, + position: IPosition, ) => getHoverInformation( (await this.getSchema()) as GraphQLSchema, diff --git a/packages/graphql-language-service/src/index.ts b/packages/graphql-language-service/src/index.ts index 1506c5a0da4..1d2e0ca0180 100644 --- a/packages/graphql-language-service/src/index.ts +++ b/packages/graphql-language-service/src/index.ts @@ -12,3 +12,4 @@ export * from './LanguageService'; export * from 'graphql-language-service-interface'; export * from 'graphql-language-service-parser'; export * from 'graphql-language-service-types'; +export * from 'graphql-language-service-utils'; diff --git a/packages/monaco-graphql/src/utils.ts b/packages/monaco-graphql/src/utils.ts index 4095668bbb8..59f020d54c4 100644 --- a/packages/monaco-graphql/src/utils.ts +++ b/packages/monaco-graphql/src/utils.ts @@ -6,11 +6,13 @@ */ import type { - Range as GraphQLRange, - Position as GraphQLPosition, + IRange as GraphQLRange, + IPosition as GraphQLPosition, Diagnostic, CompletionItem as GraphQLCompletionItem, -} from 'graphql-language-service-types'; +} from 'graphql-language-service'; + +import { Position } from 'graphql-language-service'; // @ts-ignore export type MonacoCompletionItem = monaco.languages.CompletionItem & { @@ -26,9 +28,10 @@ export function toMonacoRange(range: GraphQLRange): monaco.IRange { endColumn: range.end.character + 1, }; } + // @ts-ignore export function toGraphQLPosition(position: monaco.Position): GraphQLPosition { - return { line: position.lineNumber - 1, character: position.column - 1 }; + return new Position(position.lineNumber - 1, position.column - 1); } export function toCompletion( diff --git a/yarn.lock b/yarn.lock index e62c018acf4..0f2f7c62793 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12632,12 +12632,13 @@ grapheme-breaker@^0.3.2: unicode-trie "^0.3.1" "graphiql@file:packages/graphiql": - version "1.2.1" + version "1.2.2" dependencies: codemirror "^5.54.0" - codemirror-graphql "^0.13.1" + codemirror-graphql "^0.14.0" copy-to-clipboard "^3.2.0" entities "^2.0.0" + graphql-language-service "^3.0.2" markdown-it "^10.0.0" graphql-config@^3.0.2, graphql-config@^3.0.3: @@ -16553,10 +16554,10 @@ monaco-editor@^0.20.0: integrity sha512-hkvf4EtPJRMQlPC3UbMoRs0vTAFAYdzFQ+gpMb8A+9znae1c43q8Mab9iVsgTcg/4PNiLGGn3SlDIa8uvK1FIQ== "monaco-graphql@file:packages/monaco-graphql": - version "0.3.4" + version "0.3.5" dependencies: - graphql-language-service "^3.0.5" - graphql-language-service-utils "^2.4.3" + graphql-language-service "^3.0.6" + graphql-language-service-utils "^2.4.4" monaco-editor "^0.20.0" move-concurrently@^1.0.1: