From fc685aad9f0b895aa12118d15c729bbccfa606b3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Oct 2025 13:41:41 +0000 Subject: [PATCH 1/7] Initial plan From d642ec8b0c4128ffde34166306f2d7eb5d8a8617 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Oct 2025 13:55:36 +0000 Subject: [PATCH 2/7] Add support for Ctrl+Shift+Enter in argument objects Co-authored-by: imolorhe <4608143+imolorhe@users.noreply.github.com> --- .../altair/services/gql/fillFields.spec.ts | 17 ++++ .../modules/altair/services/gql/fillFields.ts | 87 ++++++++++++++++++- 2 files changed, 100 insertions(+), 4 deletions(-) diff --git a/packages/altair-app/src/app/modules/altair/services/gql/fillFields.spec.ts b/packages/altair-app/src/app/modules/altair/services/gql/fillFields.spec.ts index ab3c738997..b15e704949 100644 --- a/packages/altair-app/src/app/modules/altair/services/gql/fillFields.spec.ts +++ b/packages/altair-app/src/app/modules/altair/services/gql/fillFields.spec.ts @@ -8,6 +8,11 @@ const testQuery = `{ GOTBooks { } }`; + +const testQueryWithArgument = `{ + withGOTCharacter(character: { }) +}`; + describe('fillAllFields', () => { it('generates expected query', () => { const schema = getTestSchema(); @@ -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:'); + }); }); diff --git a/packages/altair-app/src/app/modules/altair/services/gql/fillFields.ts b/packages/altair-app/src/app/modules/altair/services/gql/fillFields.ts index b5fe8d550e..c947ccd6d4 100644 --- a/packages/altair-app/src/app/modules/altair/services/gql/fillFields.ts +++ b/packages/altair-app/src/app/modules/altair/services/gql/fillFields.ts @@ -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'; @@ -57,6 +66,42 @@ 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]) => { + const fieldType = field.type; + const namedType = fieldType.toString().replace(/[!\[\]]/g, ''); + + // For nested input objects, recursively build fields + if (isInputObjectType(field.type) || + (field.type.toString().includes(namedType) && + isInputObjectType((field.type as any).ofType || field.type))) { + const nestedType = isInputObjectType(field.type) + ? field.type + : (field.type as any).ofType; + if (nestedType && isInputObjectType(nestedType)) { + const nestedFields = buildInputObjectFields(nestedType, { + maxDepth, + currentDepth: currentDepth + 1, + }); + return `${fieldName}: {${nestedFields ? `\n ${nestedFields}\n` : ''}}`; + } + } + + // 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 = ( @@ -72,16 +117,25 @@ export const fillAllFields = ( } let tokenState = token.state as any; + let isObjectValue = false; if (tokenState.kind === Kind.SELECTION_SET) { tokenState.wasSelectionSet = true; tokenState = { ...tokenState, ...tokenState.prevState }; } - const fieldType = getTypeInfo(schema, token.state).type; - // Strip out empty selection sets since those throw errors while parsing query + // Check if we're in an object value (argument) + if (tokenState.kind === 'ObjectValue' || tokenState.kind === '{') { + isObjectValue = true; + tokenState.wasObjectValue = true; + // Get the type from token state + } + const typeInfoResult = getTypeInfo(schema, token.state); + const fieldType = typeInfoResult.type; + const inputType = typeInfoResult.inputType; + // Strip out empty selection sets and empty objects since those throw errors while parsing query query = query.replace(/{\s*}/g, ''); const ast = parseQuery(query); - if (!fieldType || !ast) { + if ((!fieldType && !inputType) || !ast) { return { insertions, result: query }; } @@ -121,6 +175,31 @@ export const fillAllFields = ( }; } } + + // Handle arguments with input object types + if (node.kind === Kind.OBJECT && node.loc) { + const currentInputType = typeInfo.getInputType(); + debug.log('OBJECT node:', node, 'inputType:', currentInputType, 'cursor:', cursor, 'tokenState:', tokenState); + + // Check if this is the object node at the cursor position + // and if it has an input object type + if ( + currentInputType && + isInputObjectType(currentInputType) && + (tokenState.wasObjectValue || node.loc.startToken.line - 1 === cursor.line) + ) { + debug.log('Found input object at cursor:', currentInputType, maxDepth); + const fieldsString = buildInputObjectFields(currentInputType, { maxDepth }); + const indent = getIndentation(query, node.loc.start); + + if (fieldsString) { + insertions.push({ + index: node.loc.start + 1, // Insert after the opening brace + string: '\n' + indent + ' ' + fieldsString.replace(/\n/g, '\n' + indent + ' ') + '\n' + indent, + }); + } + } + } }, }); From 3a98a7bc2a96005676ca54e952868ad5ed9b6212 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Oct 2025 13:57:50 +0000 Subject: [PATCH 3/7] Improve argument object field generation logic Co-authored-by: imolorhe <4608143+imolorhe@users.noreply.github.com> --- .../modules/altair/services/gql/fillFields.ts | 44 ++++++++++--------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/packages/altair-app/src/app/modules/altair/services/gql/fillFields.ts b/packages/altair-app/src/app/modules/altair/services/gql/fillFields.ts index c947ccd6d4..fd4fdd7132 100644 --- a/packages/altair-app/src/app/modules/altair/services/gql/fillFields.ts +++ b/packages/altair-app/src/app/modules/altair/services/gql/fillFields.ts @@ -76,23 +76,26 @@ const buildInputObjectFields = ( const fields = inputType.getFields(); const fieldEntries = Object.entries(fields).map(([fieldName, field]) => { - const fieldType = field.type; - const namedType = fieldType.toString().replace(/[!\[\]]/g, ''); + // 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(field.type) || - (field.type.toString().includes(namedType) && - isInputObjectType((field.type as any).ofType || field.type))) { - const nestedType = isInputObjectType(field.type) - ? field.type - : (field.type as any).ofType; - if (nestedType && isInputObjectType(nestedType)) { - const nestedFields = buildInputObjectFields(nestedType, { + 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 @@ -117,16 +120,15 @@ export const fillAllFields = ( } let tokenState = token.state as any; - let isObjectValue = false; if (tokenState.kind === Kind.SELECTION_SET) { tokenState.wasSelectionSet = true; tokenState = { ...tokenState, ...tokenState.prevState }; } // 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 === '{') { - isObjectValue = true; tokenState.wasObjectValue = true; - // Get the type from token state + tokenState = { ...tokenState, ...tokenState.prevState }; } const typeInfoResult = getTypeInfo(schema, token.state); const fieldType = typeInfoResult.type; @@ -177,24 +179,26 @@ export const fillAllFields = ( } // Handle arguments with input object types - if (node.kind === Kind.OBJECT && node.loc) { + if (node.kind === Kind.ARGUMENT && node.loc) { const currentInputType = typeInfo.getInputType(); - debug.log('OBJECT node:', node, 'inputType:', currentInputType, 'cursor:', cursor, 'tokenState:', tokenState); + debug.log('ARGUMENT node:', node, 'inputType:', currentInputType, 'cursor:', cursor, 'tokenState:', tokenState); - // Check if this is the object node at the cursor position - // and if it has an input object type + // Check if this argument's value is at the cursor position and has an input object type + // When tokenState.wasObjectValue is true, we're inside an empty object value if ( currentInputType && isInputObjectType(currentInputType) && - (tokenState.wasObjectValue || node.loc.startToken.line - 1 === cursor.line) + tokenState.wasObjectValue && + node.value.kind === Kind.OBJECT && + node.value.loc ) { debug.log('Found input object at cursor:', currentInputType, maxDepth); const fieldsString = buildInputObjectFields(currentInputType, { maxDepth }); - const indent = getIndentation(query, node.loc.start); + const indent = getIndentation(query, node.value.loc.start); if (fieldsString) { insertions.push({ - index: node.loc.start + 1, // Insert after the opening brace + index: node.value.loc.start + 1, // Insert after the opening brace string: '\n' + indent + ' ' + fieldsString.replace(/\n/g, '\n' + indent + ' ') + '\n' + indent, }); } From fe7058ca306b485b1a527b1a378c7f3dfec460e4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Oct 2025 14:00:22 +0000 Subject: [PATCH 4/7] Refactor to handle arguments separately from fields Co-authored-by: imolorhe <4608143+imolorhe@users.noreply.github.com> --- .../modules/altair/services/gql/fillFields.ts | 76 ++++++++++++------- 1 file changed, 48 insertions(+), 28 deletions(-) diff --git a/packages/altair-app/src/app/modules/altair/services/gql/fillFields.ts b/packages/altair-app/src/app/modules/altair/services/gql/fillFields.ts index fd4fdd7132..b38e2821d6 100644 --- a/packages/altair-app/src/app/modules/altair/services/gql/fillFields.ts +++ b/packages/altair-app/src/app/modules/altair/services/gql/fillFields.ts @@ -120,24 +120,69 @@ 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; - // Strip out empty selection sets and empty objects since those throw errors while parsing query + + // 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), + }; + } + + // 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); - if ((!fieldType && !inputType) || !ast) { + if (!fieldType || !ast) { return { insertions, result: query }; } @@ -178,32 +223,7 @@ export const fillAllFields = ( } } - // Handle arguments with input object types - if (node.kind === Kind.ARGUMENT && node.loc) { - const currentInputType = typeInfo.getInputType(); - debug.log('ARGUMENT node:', node, 'inputType:', currentInputType, 'cursor:', cursor, 'tokenState:', tokenState); - - // Check if this argument's value is at the cursor position and has an input object type - // When tokenState.wasObjectValue is true, we're inside an empty object value - if ( - currentInputType && - isInputObjectType(currentInputType) && - tokenState.wasObjectValue && - node.value.kind === Kind.OBJECT && - node.value.loc - ) { - debug.log('Found input object at cursor:', currentInputType, maxDepth); - const fieldsString = buildInputObjectFields(currentInputType, { maxDepth }); - const indent = getIndentation(query, node.value.loc.start); - - if (fieldsString) { - insertions.push({ - index: node.value.loc.start + 1, // Insert after the opening brace - string: '\n' + indent + ' ' + fieldsString.replace(/\n/g, '\n' + indent + ' ') + '\n' + indent, - }); - } - } - } + }, }); From 1eb457386e14070f5f2a8c2bed5801f01a78633a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Oct 2025 14:03:40 +0000 Subject: [PATCH 5/7] Add autocomplete suggestion for arguments Co-authored-by: imolorhe <4608143+imolorhe@users.noreply.github.com> --- .../components/query-editor/gql-extensions.ts | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/packages/altair-app/src/app/modules/altair/components/query-editor/gql-extensions.ts b/packages/altair-app/src/app/modules/altair/components/query-editor/gql-extensions.ts index f1f6e70628..bf9fd0fb9f 100644 --- a/packages/altair-app/src/app/modules/altair/components/query-editor/gql-extensions.ts +++ b/packages/altair-app/src/app/modules/altair/components/query-editor/gql-extensions.ts @@ -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' @@ -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; }, }), From 368bacbc2dff2888503e7ea23d7ad33d65db820d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Oct 2025 14:04:47 +0000 Subject: [PATCH 6/7] Update documentation for argument autofill feature Co-authored-by: imolorhe <4608143+imolorhe@users.noreply.github.com> --- .../features/autofill-fields-at-cursor.md | 29 +++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/packages/altair-docs/docs/features/autofill-fields-at-cursor.md b/packages/altair-docs/docs/features/autofill-fields-at-cursor.md index 09c61fd70d..1ef16e5fb2 100644 --- a/packages/altair-docs/docs/features/autofill-fields-at-cursor.md +++ b/packages/altair-docs/docs/features/autofill-fields-at-cursor.md @@ -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. ::: From c461d48c2a8ce775aef4cd48721d8d2a966e23d3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Oct 2025 14:08:20 +0000 Subject: [PATCH 7/7] Fix syntax: remove extra blank lines in visit callback Co-authored-by: imolorhe <4608143+imolorhe@users.noreply.github.com> --- .../src/app/modules/altair/services/gql/fillFields.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/altair-app/src/app/modules/altair/services/gql/fillFields.ts b/packages/altair-app/src/app/modules/altair/services/gql/fillFields.ts index b38e2821d6..f55a2b3107 100644 --- a/packages/altair-app/src/app/modules/altair/services/gql/fillFields.ts +++ b/packages/altair-app/src/app/modules/altair/services/gql/fillFields.ts @@ -222,8 +222,6 @@ export const fillAllFields = ( }; } } - - }, });