Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ export const getCodemirrorGraphqlExtensions = (opts: ExtensionsOptions) => {
autocomplete: (context: CompletionContext) => {
const nodeBefore = syntaxTree(context.state).resolveInner(context.pos, -1);

// Only show if inside a field SelectionSet
// Show if inside a field SelectionSet
if (
nodeBefore.name === 'SelectionSet' &&
nodeBefore.parent?.name === 'Field'
Expand All @@ -145,6 +145,27 @@ export const getCodemirrorGraphqlExtensions = (opts: ExtensionsOptions) => {
};
}

// Show if inside an argument ObjectValue (input object type)
if (
nodeBefore.name === 'ObjectValue' ||
(nodeBefore.name === '{' && nodeBefore.parent?.name === 'ObjectValue')
) {
return {
from: context.pos,
options: [
{
label: 'Fill all fields',
apply(view: EditorView) {
fillAllFieldsCommands(view);
},
boost: 99,
type: 'function',
info: 'Automatically fill in all the fields for this input object argument (controlled by addQueryDepthLimit in settings)',
},
],
};
}

return null;
},
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ const testQuery = `{
GOTBooks {
}
}`;

const testQueryWithArgument = `{
withGOTCharacter(character: { })
}`;

describe('fillAllFields', () => {
it('generates expected query', () => {
const schema = getTestSchema();
Expand All @@ -19,4 +24,16 @@ describe('fillAllFields', () => {
});
expect(res).toMatchSnapshot();
});

it('generates fields for input object arguments', () => {
const schema = getTestSchema();
// Position cursor inside the character argument object braces
const pos = new Position(1, 32);
const token = getTokenAtPosition(testQueryWithArgument, pos, 1);
const res = fillAllFields(schema, testQueryWithArgument, pos, token, {
maxDepth: 2,
});
expect(res.result).toContain('id:');
expect(res.result).toContain('book:');
});
});
105 changes: 103 additions & 2 deletions packages/altair-app/src/app/modules/altair/services/gql/fillFields.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
import { visit, print, TypeInfo, parse, GraphQLSchema, Kind } from 'graphql';
import {
visit,
print,
TypeInfo,
parse,
GraphQLSchema,
Kind,
isInputObjectType,
GraphQLInputObjectType,
} from 'graphql';
import { debug } from '../../utils/logger';
import getTypeInfo from 'codemirror-graphql/utils/getTypeInfo';
import { ContextToken } from 'graphql-language-service';
Expand Down Expand Up @@ -57,6 +66,45 @@ export interface FillAllFieldsOptions {
maxDepth?: number;
}

const buildInputObjectFields = (
inputType: GraphQLInputObjectType,
{ maxDepth = 1, currentDepth = 0 } = {}
): string => {
if (currentDepth >= maxDepth) {
return '';
}

const fields = inputType.getFields();
const fieldEntries = Object.entries(fields).map(([fieldName, field]) => {
// Unwrap the type to get to the base type (remove NonNull and List wrappers)
let unwrappedType = field.type;
while (
unwrappedType &&
('ofType' in unwrappedType) &&
unwrappedType.ofType
) {
unwrappedType = unwrappedType.ofType as any;
}

// For nested input objects, recursively build fields
if (isInputObjectType(unwrappedType)) {
if (currentDepth + 1 < maxDepth) {
const nestedFields = buildInputObjectFields(unwrappedType, {
maxDepth,
currentDepth: currentDepth + 1,
});
return `${fieldName}: {${nestedFields ? `\n ${nestedFields}\n` : ''}}`;
}
return `${fieldName}: `;
}

// For scalar types, just add the field name
return `${fieldName}: `;
});

return fieldEntries.join('\n');
};

// Improved version based on:
// https://github.com/graphql/graphiql/blob/272e2371fc7715217739efd7817ce6343cb4fbec/src/utility/fillLeafs.js
export const fillAllFields = (
Expand All @@ -72,11 +120,64 @@ export const fillAllFields = (
}

let tokenState = token.state as any;
let isSelectionSetMode = false;
let isObjectValueMode = false;

if (tokenState.kind === Kind.SELECTION_SET) {
tokenState.wasSelectionSet = true;
tokenState = { ...tokenState, ...tokenState.prevState };
isSelectionSetMode = true;
}
// Check if we're in an object value (argument)
// The token state kind for object values is typically 'ObjectValue' or the token itself is '{'
if (tokenState.kind === 'ObjectValue' || tokenState.kind === '{') {
tokenState.wasObjectValue = true;
tokenState = { ...tokenState, ...tokenState.prevState };
isObjectValueMode = true;
}

const typeInfoResult = getTypeInfo(schema, token.state);
const fieldType = typeInfoResult.type;
const inputType = typeInfoResult.inputType;

// For object value mode (arguments), handle specially without stripping
if (isObjectValueMode && inputType && isInputObjectType(inputType)) {
// Don't strip, parse as-is since `{ }` is valid for arguments
const ast = parseQuery(query);
if (!ast) {
return { insertions, result: query };
}

const typeInfo = new TypeInfo(schema);
visit(ast, {
enter(node) {
typeInfo.enter(node);
// Find the OBJECT node at the cursor position
if (node.kind === Kind.OBJECT && node.loc &&
node.loc.startToken.line - 1 === cursor.line) {
const currentInputType = typeInfo.getInputType();
if (currentInputType && isInputObjectType(currentInputType)) {
const fieldsString = buildInputObjectFields(currentInputType, { maxDepth });
const indent = getIndentation(query, node.loc.start);
if (fieldsString && node.fields.length === 0) {
// Only fill if the object is empty
insertions.push({
index: node.loc.start + 1,
string: '\n' + indent + ' ' + fieldsString.replace(/\n/g, '\n' + indent + ' ') + '\n' + indent,
});
}
}
}
},
});

return {
insertions,
result: withInsertions(query, insertions),
};
}
const fieldType = getTypeInfo(schema, token.state).type;

// Original logic for selection sets
// Strip out empty selection sets since those throw errors while parsing query
query = query.replace(/{\s*}/g, '');
const ast = parseQuery(query);
Expand Down
29 changes: 27 additions & 2 deletions packages/altair-docs/docs/features/autofill-fields-at-cursor.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,31 @@ Note: You can change the autocompletion depth limit using a [`addQueryDepthLimit

![Autofill fields](/assets/img/docs/autofill-fields.gif)

::: warning
Note: This only works for the query fields, and not for the arguments. You can still [generate whole queriea and fragments](/docs/features/add-queries-and-fragments) directly from the docs along with their arguments filled in.
## Works with arguments too!

This feature now also works with query arguments that accept input object types. When you place your cursor inside an empty argument object (e.g., `character: { }`), you can use the same keyboard shortcut (`Ctrl+Shift+Enter`) or select "Fill all fields" from the autocomplete menu to automatically fill in all the fields for that input type.

For example, given this query:
```graphql
{
withGOTCharacter(character: { })
}
```

Placing the cursor inside the empty braces and pressing `Ctrl+Shift+Enter` will automatically fill in the required fields:
```graphql
{
withGOTCharacter(character: {
id:
book: {
id:
url:
name:
}
})
}
```

::: tip
You can still [generate whole queries and fragments](/docs/features/add-queries-and-fragments) directly from the docs along with their arguments filled in.
:::
Loading