Skip to content

feat: add 'Extract to fragment' refactor - #412

Open
JoviDeCroock wants to merge 1 commit into
mainfrom
feat/extract-fragment-refactor
Open

feat: add 'Extract to fragment' refactor#412
JoviDeCroock wants to merge 1 commit into
mainfrom
feat/extract-fragment-refactor

Conversation

@JoviDeCroock

Copy link
Copy Markdown
Member

What/Why

Adds an "Extract to fragment" refactor to the language service. When a selection inside a graphql() document's selection set covers one or more complete sibling field selections, the plugin offers an "Extract to fragment" action under the existing "GraphQL" refactor group. Applying it:

  1. inserts a new const <parentType>Fields = graphql() fragment document above the statement containing the current call (above its leading comments), named <ParentType>Fields after the selected fields' parent type, with a numeric suffix if that fragment name is already used by a document in the file (or the derived variable name already occurs in the file),
  2. replaces the selected fields with a ...<FragmentName> spread, and
  3. appends the new fragment variable to the call's fragment array second argument, or adds , [<var>] when the call only has the document argument.

This makes it much easier to progressively split large gql.tada documents into co-located fragments.

Implementation notes

  • packages/graphqlsp/src/extractFragment.ts implements both applicability (canExtractFragment) and the edits (getExtractFragmentEdits). The TS selection range is mapped into the document text (getStart() + 1 offset mapping, as in definition.ts), trimmed of GraphQL ignored tokens (whitespace/commas), and validated against the parsed document AST: the trimmed range must start at a field selection and consecutive sibling fields must tile it exactly (a selection ending inside a field, crossing a selection-set boundary, or covering non-field selections is rejected).
  • The parent type is resolved by walking the document with graphql's TypeInfo; the schema is resolved per document via getSchemaForName using the document's schema name, the same way diagnostics.ts does, so multi-schema setups pick the right schema.
  • The generated statement mirrors the current call's callee text and quote/backtick style, detects the file's indentation unit from the document's own lines, and re-indents the extracted fields (preserving nested selection-set indentation). Single/double-quoted documents are extracted as a single-line fragment since those literals can't span lines.
  • index.ts now dispatches getEditsForRefactor by refactor/action name (instead of the previous early-return for the persisted-operations code fix) and getApplicableRefactors merges all applicable actions into one "GraphQL" refactor group. Everything stays behind the guard() fallback so exceptions never propagate into tsserver.
  • If the call's second argument exists but isn't an array literal (e.g. an identifier), the refactor isn't offered, since the fragment reference couldn't be added safely.

Test plan

  • New e2e suite test/e2e/extract-fragment.test.ts (new fixture test/e2e/fixture-project-tada/fixtures/extract-fragment.ts), driving a real tsserver via the getApplicableRefactors/getEditsForRefactor protocol commands:
    • action offered for a selection covering complete sibling fields (including leading/trailing whitespace trimming),
    • action NOT offered for a selection ending mid-field, one crossing a nested selection-set boundary, and a collapsed selection,
    • edits applied to the fixture content and asserted via full-file inline snapshots for: extraction appending to an existing fragment array (with <ParentType>Fields name collision resolving to PokemonFields2), extraction of a field with a nested selection set at the end of a run, and extraction from a call without a fragment array (adds , [pokemonFields2]).
  • pnpm --filter @0no-co/graphqlsp run build passes.
  • Full pnpm run test:e2e: 12 test files, 59 tests, all passing.

Limitations

  • The refactor is only offered in call-expression mode (templateIsCallExpression: true, the default, i.e. gql.tada-style graphql() calls). Tagged-template mode is not supported, since fragments there aren't composed through a fragment-array argument.
  • Only plain field selections can be extracted; runs containing fragment spreads or inline fragments are not offered the refactor.
  • The refactor requires the schema to be loaded to resolve the selection's parent type, so it may not be offered immediately after project start.

🤖 Generated with Claude Code

When a selection inside a graphql() document's selection set covers one
or more complete sibling field selections, offer an 'Extract to
fragment' action under the existing 'GraphQL' refactor group. Applying
it creates a new graphql() fragment document (named after the fields'
parent type) above the current statement, replaces the selected fields
with a fragment spread, and appends the new fragment variable to the
call's fragment array (creating the array when the call has none).

The refactor dispatch in getEditsForRefactor now switches on the
refactor/action name, and getApplicableRefactors merges all applicable
GraphQL actions into a single refactor group.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Jul 4, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: cc50966

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@0no-co/graphqlsp Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant