Mirror: TypeScript LSP plugin that finds GraphQL documents in your code and provides diagnostics, auto-complete and hover-information.

fix: support suggestions in type-conditions (#262)

authored by

Jovi De Croock and committed by
GitHub
8affa100 894be553

+127 -7
+5
.changeset/chilly-coins-end.md
··· 1 + --- 2 + '@0no-co/graphqlsp': patch 3 + --- 4 + 5 + Fix type-condition suggestions
+17 -2
packages/graphqlsp/src/ast/token.ts
··· 21 21 let cPos = template.getStart() + 1; 22 22 23 23 let foundToken: Token | undefined = undefined; 24 + let prevToken: Token | undefined = undefined; 24 25 for (let line = 0; line < input.length; line++) { 26 + if (foundToken) continue; 25 27 const lPos = cPos - 1; 26 28 const stream = new CharacterStream(input[line] + '\n'); 27 29 while (!stream.eol()) { ··· 32 34 lPos + stream.getStartOfToken() + 1 <= cursorPosition && 33 35 lPos + stream.getCurrentPosition() >= cursorPosition 34 36 ) { 35 - foundToken = { 37 + foundToken = prevToken 38 + ? prevToken 39 + : { 40 + line, 41 + start: stream.getStartOfToken() + 1, 42 + end: stream.getCurrentPosition(), 43 + string, 44 + state, 45 + tokenKind: token, 46 + }; 47 + break; 48 + } else if (string === 'on') { 49 + prevToken = { 36 50 line, 37 51 start: stream.getStartOfToken() + 1, 38 52 end: stream.getCurrentPosition(), ··· 40 54 state, 41 55 tokenKind: token, 42 56 }; 43 - break; 57 + } else { 58 + prevToken = undefined; 44 59 } 45 60 } 46 61
+23 -5
packages/graphqlsp/src/autoComplete.ts
··· 146 146 ) as Array<FragmentDefinitionNode>; 147 147 } catch (e) {} 148 148 149 - let suggestions = getAutocompleteSuggestions(schema, queryText, cursor); 150 - let spreadSuggestions = getSuggestionsForFragmentSpread( 151 - token, 152 - getTypeInfo(schema, token.state), 149 + const isOnTypeCondition = 150 + token.string === 'on' && token.state.kind === 'TypeCondition'; 151 + let suggestions = getAutocompleteSuggestions( 153 152 schema, 154 153 queryText, 155 - fragments 154 + cursor, 155 + isOnTypeCondition 156 + ? { 157 + ...token, 158 + state: { 159 + ...token.state, 160 + step: 1, 161 + }, 162 + type: null, 163 + } 164 + : undefined 156 165 ); 166 + let spreadSuggestions = !isOnTypeCondition 167 + ? getSuggestionsForFragmentSpread( 168 + token, 169 + getTypeInfo(schema, token.state), 170 + schema, 171 + queryText, 172 + fragments 173 + ) 174 + : []; 157 175 158 176 const state = 159 177 token.state.kind === 'Invalid' ? token.state.prevState : token.state;
+17
test/e2e/fixture-project-tada/fixtures/type-condition.ts
··· 1 + import { graphql } from './graphql'; 2 + import { PokemonFields } from './fragment'; 3 + 4 + // prettier-ignore 5 + const x = graphql(` 6 + query Pok($limit: Int!) { 7 + pokemons(limit: $limit) { 8 + id 9 + name 10 + fleeRate 11 + classification 12 + ...pokemonFields 13 + __typename 14 + ... on 15 + } 16 + } 17 + `, [PokemonFields]);
+4
test/e2e/fixture-project-tada/introspection.d.ts
··· 422 422 { 423 423 "kind": "SCALAR", 424 424 "name": "Boolean" 425 + }, 426 + { 427 + "kind": "SCALAR", 428 + "name": "Any" 425 429 } 426 430 ], 427 431 "directives": []
+61
test/e2e/tada.test.ts
··· 10 10 const projectPath = path.resolve(__dirname, 'fixture-project-tada'); 11 11 describe('Fragment + operations', () => { 12 12 const outfileCombo = path.join(projectPath, 'simple.ts'); 13 + const outfileTypeCondition = path.join(projectPath, 'type-condition.ts'); 13 14 const outfileUnusedFragment = path.join(projectPath, 'unused-fragment.ts'); 14 15 const outfileCombinations = path.join(projectPath, 'fragment.ts'); 15 16 ··· 19 20 20 21 server.sendCommand('open', { 21 22 file: outfileCombo, 23 + fileContent: '// empty', 24 + scriptKindName: 'TS', 25 + } satisfies ts.server.protocol.OpenRequestArgs); 26 + server.sendCommand('open', { 27 + file: outfileTypeCondition, 22 28 fileContent: '// empty', 23 29 scriptKindName: 'TS', 24 30 } satisfies ts.server.protocol.OpenRequestArgs); ··· 43 49 ), 44 50 }, 45 51 { 52 + file: outfileTypeCondition, 53 + fileContent: fs.readFileSync( 54 + path.join(projectPath, 'fixtures/type-condition.ts'), 55 + 'utf-8' 56 + ), 57 + }, 58 + { 46 59 file: outfileCombo, 47 60 fileContent: fs.readFileSync( 48 61 path.join(projectPath, 'fixtures/simple.ts'), ··· 64 77 tmpfile: outfileCombo, 65 78 } satisfies ts.server.protocol.SavetoRequestArgs); 66 79 server.sendCommand('saveto', { 80 + file: outfileTypeCondition, 81 + tmpfile: outfileTypeCondition, 82 + } satisfies ts.server.protocol.SavetoRequestArgs); 83 + server.sendCommand('saveto', { 67 84 file: outfileCombinations, 68 85 tmpfile: outfileCombinations, 69 86 } satisfies ts.server.protocol.SavetoRequestArgs); ··· 78 95 fs.unlinkSync(outfileUnusedFragment); 79 96 fs.unlinkSync(outfileCombinations); 80 97 fs.unlinkSync(outfileCombo); 98 + fs.unlinkSync(outfileTypeCondition); 81 99 } catch {} 82 100 }); 83 101 ··· 553 571 }, 554 572 "name": "__typename", 555 573 "sortText": "14__typename", 574 + }, 575 + ] 576 + `); 577 + }, 30000); 578 + 579 + it('gives suggestions for type-conditions (#261)', async () => { 580 + server.send({ 581 + seq: 13, 582 + type: 'request', 583 + command: 'completionInfo', 584 + arguments: { 585 + file: outfileTypeCondition, 586 + line: 14, 587 + offset: 14, 588 + includeExternalModuleExports: true, 589 + includeInsertTextCompletions: true, 590 + triggerKind: 1, 591 + }, 592 + }); 593 + 594 + await server.waitForResponse( 595 + response => 596 + response.type === 'response' && response.command === 'completionInfo' 597 + ); 598 + 599 + const res = server.responses 600 + .reverse() 601 + .find( 602 + resp => resp.type === 'response' && resp.command === 'completionInfo' 603 + ); 604 + 605 + expect(res).toBeDefined(); 606 + expect(typeof res?.body.entries).toEqual('object'); 607 + expect(res?.body.entries).toMatchInlineSnapshot(` 608 + [ 609 + { 610 + "kind": "var", 611 + "kindModifiers": "declare", 612 + "labelDetails": { 613 + "description": "", 614 + }, 615 + "name": "Pokemon", 616 + "sortText": "0", 556 617 }, 557 618 ] 558 619 `);