From 6af6c6e06d7ffafdcabb8f9f71b277f81415c6bc Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Wed, 25 Apr 2018 08:02:59 -0400 Subject: [PATCH] Autocomplete for GraphQL variables (#888) * Autocomplete for GraphQL variables * Add refresh key to audioi response viewer (Fixes #890) --- .../ui/components/codemirror/base-imports.js | 2 + .../ui/components/codemirror/code-editor.js | 4 +- .../codemirror/extensions/autocomplete.js | 46 ++++++++----- .../editors/body/graph-ql-editor.js | 66 ++++++++++++++++--- 4 files changed, 91 insertions(+), 27 deletions(-) diff --git a/packages/insomnia-app/app/ui/components/codemirror/base-imports.js b/packages/insomnia-app/app/ui/components/codemirror/base-imports.js index f35fb34ea2..10f596b572 100644 --- a/packages/insomnia-app/app/ui/components/codemirror/base-imports.js +++ b/packages/insomnia-app/app/ui/components/codemirror/base-imports.js @@ -42,6 +42,8 @@ import 'codemirror-graphql/lint'; import 'codemirror-graphql/mode'; import 'codemirror-graphql/info'; import 'codemirror-graphql/jump'; +import 'codemirror-graphql/variables/lint'; +import 'codemirror-graphql/variables/mode'; import './modes/nunjucks'; import './modes/curl'; import './extensions/autocomplete'; diff --git a/packages/insomnia-app/app/ui/components/codemirror/code-editor.js b/packages/insomnia-app/app/ui/components/codemirror/code-editor.js index 7f139275cd..6094019d27 100644 --- a/packages/insomnia-app/app/ui/components/codemirror/code-editor.js +++ b/packages/insomnia-app/app/ui/components/codemirror/code-editor.js @@ -540,7 +540,9 @@ class CodeEditor extends React.Component { _normalizeMode (mode) { const mimeType = mode ? mode.split(';')[0] : 'text/plain'; - if (mimeType.includes('graphql')) { + if (mimeType.includes('graphql-variables')) { + return 'graphql-variables'; + } else if (mimeType.includes('graphql')) { // Because graphQL plugin doesn't recognize application/graphql content-type return 'graphql'; } else if (this._isJSON(mimeType)) { diff --git a/packages/insomnia-app/app/ui/components/codemirror/extensions/autocomplete.js b/packages/insomnia-app/app/ui/components/codemirror/extensions/autocomplete.js index 7fd7f2c9f8..13f5e3acbb 100644 --- a/packages/insomnia-app/app/ui/components/codemirror/extensions/autocomplete.js +++ b/packages/insomnia-app/app/ui/components/codemirror/extensions/autocomplete.js @@ -154,10 +154,14 @@ CodeMirror.defineOption('environmentAutocomplete', null, (cm, options) => { clearTimeout(keydownDebounce); }); - // Add hot key triggers + // Remove keymap if we're already added it + cm.removeKeyMap('autocomplete-keymap'); + + // Add keymap cm.addKeyMap({ + name: 'autocomplete-keymap', 'Ctrl-Space': completeForce, // Force autocomplete on hotkey - "' '": completeIfAfterTagOrVarOpen + '\' \'': completeIfAfterTagOrVarOpen }); // Close dropdown whenever something is clicked @@ -199,37 +203,45 @@ function hint (cm, options) { const nameSegmentFull = previousText; // Actually try to match the list of things - const allShortMatches = []; - const allLongMatches = []; + const lowPriorityMatches = []; + const highPriorityMatches = []; // Match variables if (allowMatchingVariables) { matchSegments(variablesToMatch, nameSegment, TYPE_VARIABLE, MAX_VARIABLES) - .map(m => allShortMatches.push(m)); + .map(m => lowPriorityMatches.push(m)); matchSegments(variablesToMatch, nameSegmentLong, TYPE_VARIABLE, MAX_VARIABLES) - .map(m => allLongMatches.push(m)); + .map(m => highPriorityMatches.push(m)); } - // Match constants (only use long segment for a more strict match) - // TODO: Make this more flexible. This is really only here as a hack to make - // constants only match full string prefixes. + // Match constants if (allowMatchingConstants) { - // Only match full segments with constants - matchSegments(constantsToMatch, nameSegmentFull, TYPE_CONSTANT, MAX_CONSTANTS) - .map(m => allLongMatches.push(m)); + const cur = cm.getCursor(); + const token = cm.getTokenAt(cur); + + if (token.type === 'variable') { + // For GraphQL to autocomplete constants (variables) in JSON keys + matchSegments(constantsToMatch, nameSegment, TYPE_CONSTANT, MAX_CONSTANTS) + .map(m => highPriorityMatches.push(m)); + } else { + // Otherwise match full segments + matchSegments(constantsToMatch, nameSegmentFull, TYPE_CONSTANT, MAX_CONSTANTS) + .map(m => highPriorityMatches.push(m)); + } } // Match tags if (allowMatchingTags) { matchSegments(tagsToMatch, nameSegment, TYPE_TAG, MAX_TAGS) - .map(m => allShortMatches.push(m)); + .map(m => lowPriorityMatches.push(m)); matchSegments(tagsToMatch, nameSegmentLong, TYPE_TAG, MAX_TAGS) - .map(m => allLongMatches.push(m)); + .map(m => highPriorityMatches.push(m)); } - // NOTE: This puts the longer (more precise) matches in front of the short ones - const matches = [...allLongMatches, ...allShortMatches]; - const segment = allLongMatches.length ? nameSegmentLong : nameSegment; + const matches = [...highPriorityMatches, ...lowPriorityMatches]; + + // Autocomplete from longest matched segment + const segment = highPriorityMatches.length ? nameSegmentLong : nameSegment; const uniqueMatches = matches.reduce( (arr, v) => arr.find(a => a.text === v.text) ? arr : [...arr, v], diff --git a/packages/insomnia-app/app/ui/components/editors/body/graph-ql-editor.js b/packages/insomnia-app/app/ui/components/editors/body/graph-ql-editor.js index 150c8da142..729e4f2d41 100644 --- a/packages/insomnia-app/app/ui/components/editors/body/graph-ql-editor.js +++ b/packages/insomnia-app/app/ui/components/editors/body/graph-ql-editor.js @@ -4,7 +4,7 @@ import {newBodyRaw} from '../../../../models/request'; import classnames from 'classnames'; import * as React from 'react'; import autobind from 'autobind-decorator'; -import {parse, print} from 'graphql'; +import {parse, print, typeFromAST} from 'graphql'; import {introspectionQuery} from 'graphql/utilities/introspectionQuery'; import {buildClientSchema} from 'graphql/utilities/buildClientSchema'; import CodeEditor from '../../codemirror/code-editor'; @@ -124,6 +124,40 @@ class GraphQLEditor extends React.PureComponent { } } + _buildVariableTypes (schema: Object | null, query: string): {[string]: Object} { + if (!schema) { + return {}; + } + + let documentAST; + try { + documentAST = parse(query); + } catch (e) { + documentAST = null; + } + + const definitions = documentAST ? documentAST.definitions : []; + const variableToType = {}; + for (const {kind, variableDefinitions} of definitions) { + if (kind !== 'OperationDefinition') { + continue; + } + + if (!variableDefinitions) { + continue; + } + + for (const {variable, type} of variableDefinitions) { + const inputType = typeFromAST(schema, type); + if (!inputType) { + continue; + } + variableToType[variable.name.value] = inputType; + } + } + return variableToType; + } + async _handleRefreshSchema (): Promise { await this._fetchAndSetSchema(this.props.request); } @@ -139,11 +173,8 @@ class GraphQLEditor extends React.PureComponent { }, 200); } - _getOperationNames (query: string): Array { - let documentAST; - try { - documentAST = parse(query); - } catch (e) { + _getOperationNames (query: string, documentAST: Object | null): Array { + if (!documentAST) { return []; } @@ -154,7 +185,14 @@ class GraphQLEditor extends React.PureComponent { } _handleBodyChange (query: string, variables?: Object): void { - const operationNames = this._getOperationNames(query); + let documentAST; + try { + documentAST = parse(query); + } catch (e) { + documentAST = null; + } + + const operationNames = this._getOperationNames(query, documentAST); const body: GraphQLBody = {query}; @@ -166,7 +204,11 @@ class GraphQLEditor extends React.PureComponent { body.operationName = operationNames[0]; } - this.setState({variablesSyntaxError: '', body}); + this.setState({ + variablesSyntaxError: '', + body + }); + this.props.onChange(GraphQLEditor._graphQLToString(body)); } @@ -274,6 +316,8 @@ class GraphQLEditor extends React.PureComponent { const variables = prettify.json(JSON.stringify(variablesObject)); + const variableTypes = this._buildVariableTypes(schema, query); + return (
@@ -336,9 +380,13 @@ class GraphQLEditor extends React.PureComponent { className={className} render={render} getRenderContext={getRenderContext} + getAutocompleteConstants={() => Object.keys(variableTypes || {})} + lintOptions={{ + variableToType: variableTypes + }} nunjucksPowerUserMode={settings.nunjucksPowerUserMode} onChange={this._handleVariablesChange} - mode="application/json" + mode="graphql-variables" lineWrapping={settings.editorLineWrapping} placeholder="" />