Skip to content

Commit f729f9a

Browse files
authored
feat: Symbol support for single document (#1244)
* document symbols * more outline/symbol support for schema definition types * support interface definitions, full enum values for symbols
1 parent 0f99f17 commit f729f9a

File tree

12 files changed

+2666
-2902
lines changed

12 files changed

+2666
-2902
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@ name: Node.JS CI
22
on: [push]
33
jobs:
44
test:
5-
runs-on: ubuntu-latest
65
name: lint & test
6+
runs-on: ubuntu-16.04
77
steps:
88
- uses: actions/checkout@v1
99
- uses: bahmutov/npm-install@v1
1010
- run: yarn ci
1111
e2e:
12+
name: cypress
1213
runs-on: ubuntu-16.04
1314
steps:
1415
- uses: actions/checkout@v1

package.json

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -73,11 +73,11 @@
7373
"@commitlint/config-lerna-scopes": "^8.1.0",
7474
"@strictsoftware/typedoc-plugin-monorepo": "^0.2.1",
7575
"@testing-library/jest-dom": "^5.1.1",
76-
"@types/codemirror": "^0.0.84",
76+
"@types/codemirror": "^0.0.85",
7777
"@types/fetch-mock": "^7.3.2",
7878
"@types/jest": "^25.1.1",
7979
"@types/node": "^13.7.1",
80-
"@typescript-eslint/eslint-plugin": "^2.16.0",
80+
"@typescript-eslint/eslint-plugin": "^2.19.2",
8181
"@typescript-eslint/parser": "^2.18.0",
8282
"babel-eslint": "^10.0.1",
8383
"babel-jest": "^25.1.0",
@@ -86,17 +86,17 @@
8686
"conventional-changelog-conventionalcommits": "^4.1.0",
8787
"copy": "^0.3.2",
8888
"cross-env": "^7.0.0",
89-
"cypress": "^4.0.1",
89+
"cypress": "^4.0.2",
9090
"eslint": "^6.8.0",
9191
"eslint-config-prettier": "6.10.0",
9292
"eslint-plugin-babel": "5.3.0",
93-
"eslint-plugin-cypress": "^2.7.0",
93+
"eslint-plugin-cypress": "^2.10.1",
9494
"eslint-plugin-flowtype": "4.6.0",
9595
"eslint-plugin-import": "^2.20.0",
9696
"eslint-plugin-jest": "^23.1.1",
9797
"eslint-plugin-prefer-object-spread": "1.2.1",
98-
"eslint-plugin-react": "7.18.1",
99-
"fetch-mock": "^6.0.0",
98+
"eslint-plugin-react": "7.18.3",
99+
"fetch-mock": "6.5.2",
100100
"flow-bin": "^0.118.0",
101101
"graphql": "^14.6.0",
102102
"husky": "^4.0.7",
@@ -108,9 +108,9 @@
108108
"mkdirp": "^1.0.3",
109109
"mocha": "7.0.1",
110110
"prettier": "^1.18.2",
111-
"rimraf": "^3.0.1",
111+
"rimraf": "^3.0.2",
112112
"ts-jest": "^25.2.0",
113-
"typedoc": "^0.15.6",
113+
"typedoc": "^0.15.1",
114114
"typescript": "^3.6.3"
115115
}
116116
}

packages/graphql-language-service-interface/src/GraphQLLanguageService.ts

Lines changed: 96 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import {
1111
DocumentNode,
1212
FragmentSpreadNode,
1313
FragmentDefinitionNode,
14-
OperationDefinitionNode,
1514
TypeDefinitionNode,
1615
NamedTypeNode,
1716
ValidationRule,
@@ -26,10 +25,16 @@ import {
2625
GraphQLProjectConfig,
2726
Uri,
2827
Position,
28+
Outline,
29+
OutlineTree,
2930
} from 'graphql-language-service-types';
3031

3132
// import { Position } from 'graphql-language-service-utils';
32-
import { Hover, DiagnosticSeverity } from 'vscode-languageserver-types';
33+
import {
34+
Hover,
35+
SymbolInformation,
36+
SymbolKind,
37+
} from 'vscode-languageserver-types';
3338

3439
import { Kind, parse, print } from 'graphql';
3540
import { getAutocompleteSuggestions } from './getAutocompleteSuggestions';
@@ -41,6 +46,8 @@ import {
4146
getDefinitionQueryResultForNamedType,
4247
} from './getDefinition';
4348

49+
import { getOutline } from './getOutline';
50+
4451
import {
4552
getASTNodeAtPosition,
4653
requireFile,
@@ -67,6 +74,33 @@ const {
6774
NAMED_TYPE,
6875
} = Kind;
6976

77+
const KIND_TO_SYMBOL_KIND: { [key: string]: SymbolKind } = {
78+
Field: SymbolKind.Field,
79+
OperationDefinition: SymbolKind.Class,
80+
FragmentDefinition: SymbolKind.Class,
81+
FragmentSpread: SymbolKind.Struct,
82+
ObjectTypeDefinition: SymbolKind.Class,
83+
EnumTypeDefinition: SymbolKind.Enum,
84+
EnumValueDefinition: SymbolKind.EnumMember,
85+
InputObjectTypeDefinition: SymbolKind.Class,
86+
InputValueDefinition: SymbolKind.Field,
87+
FieldDefinition: SymbolKind.Field,
88+
InterfaceTypeDefinition: SymbolKind.Interface,
89+
Document: SymbolKind.File,
90+
FieldWithArguments: SymbolKind.Method,
91+
};
92+
93+
function getKind(tree: OutlineTree) {
94+
if (
95+
tree.kind === 'FieldDefinition' &&
96+
tree.children &&
97+
tree.children.length > 0
98+
) {
99+
return KIND_TO_SYMBOL_KIND.FieldWithArguments;
100+
}
101+
return KIND_TO_SYMBOL_KIND[tree.kind];
102+
}
103+
70104
export class GraphQLLanguageService {
71105
_graphQLCache: GraphQLCache;
72106
_graphQLConfig: GraphQLConfig;
@@ -84,7 +118,7 @@ export class GraphQLLanguageService {
84118
throw Error(`No config found for uri: ${uri}`);
85119
}
86120

87-
async getDiagnostics(
121+
public async getDiagnostics(
88122
query: string,
89123
uri: Uri,
90124
isRelayCompatMode?: boolean,
@@ -123,7 +157,7 @@ export class GraphQLLanguageService {
123157
const range = getRange(error.locations[0], query);
124158
return [
125159
{
126-
severity: SEVERITY.ERROR as DiagnosticSeverity,
160+
severity: SEVERITY.ERROR,
127161
message: error.message,
128162
source: 'GraphQL: Syntax',
129163
range,
@@ -187,7 +221,7 @@ export class GraphQLLanguageService {
187221
return validateQuery(validationAst, schema, customRules, isRelayCompatMode);
188222
}
189223

190-
async getAutocompleteSuggestions(
224+
public async getAutocompleteSuggestions(
191225
query: string,
192226
position: Position,
193227
filePath: Uri,
@@ -203,7 +237,7 @@ export class GraphQLLanguageService {
203237
return [];
204238
}
205239

206-
async getHoverInformation(
240+
public async getHoverInformation(
207241
query: string,
208242
position: Position,
209243
filePath: Uri,
@@ -219,7 +253,7 @@ export class GraphQLLanguageService {
219253
return '';
220254
}
221255

222-
async getDefinition(
256+
public async getDefinition(
223257
query: string,
224258
position: Position,
225259
filePath: Uri,
@@ -250,7 +284,7 @@ export class GraphQLLanguageService {
250284
return getDefinitionQueryResultForDefinitionNode(
251285
filePath,
252286
query,
253-
node as FragmentDefinitionNode | OperationDefinitionNode,
287+
node,
254288
);
255289

256290
case NAMED_TYPE:
@@ -266,6 +300,55 @@ export class GraphQLLanguageService {
266300
return null;
267301
}
268302

303+
public async getDocumentSymbols(
304+
document: string,
305+
filePath: Uri,
306+
): Promise<SymbolInformation[]> {
307+
const outline = await this.getOutline(document);
308+
if (!outline) {
309+
return [];
310+
}
311+
312+
const output: Array<SymbolInformation> = [];
313+
const input = outline.outlineTrees.map((tree: OutlineTree) => [null, tree]);
314+
315+
while (input.length > 0) {
316+
const res = input.pop();
317+
if (!res) {
318+
return [];
319+
}
320+
const [parent, tree] = res;
321+
if (!tree) {
322+
return [];
323+
}
324+
325+
output.push({
326+
// @ts-ignore
327+
name: tree.representativeName,
328+
kind: getKind(tree),
329+
location: {
330+
uri: filePath,
331+
range: {
332+
start: tree.startPosition,
333+
// @ts-ignore
334+
end: tree.endPosition,
335+
},
336+
},
337+
containerName: parent ? parent.representativeName : undefined,
338+
});
339+
input.push(...tree.children.map(child => [tree, child]));
340+
}
341+
return output;
342+
}
343+
//
344+
// public async getReferences(
345+
// document: string,
346+
// position: Position,
347+
// filePath: Uri,
348+
// ): Promise<Location[]> {
349+
//
350+
// }
351+
269352
async _getDefinitionForNamedType(
270353
query: string,
271354
ast: DocumentNode,
@@ -287,7 +370,8 @@ export class GraphQLLanguageService {
287370
definition.kind === OBJECT_TYPE_DEFINITION ||
288371
definition.kind === INPUT_OBJECT_TYPE_DEFINITION ||
289372
definition.kind === ENUM_TYPE_DEFINITION ||
290-
definition.kind === SCALAR_TYPE_DEFINITION,
373+
definition.kind === SCALAR_TYPE_DEFINITION ||
374+
definition.kind === INTERFACE_TYPE_DEFINITION,
291375
);
292376

293377
const typeCastedDefs = (localObjectTypeDefinitions as any) as Array<
@@ -351,4 +435,7 @@ export class GraphQLLanguageService {
351435

352436
return result;
353437
}
438+
async getOutline(query: string): Promise<Outline | null | undefined> {
439+
return getOutline(query);
440+
}
354441
}

packages/graphql-language-service-interface/src/__tests__/GraphQLLanguageService-test.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,14 @@
77
*
88
*/
99

10-
import { Position } from 'graphql-language-service-types';
1110
import { join } from 'path';
1211
import * as fs from 'fs';
1312
import { buildSchema } from 'graphql';
1413

1514
import { GraphQLConfig } from 'graphql-config';
1615
import { GraphQLLanguageService } from '../GraphQLLanguageService';
16+
import { SymbolKind } from 'vscode-languageserver-protocol';
17+
import { Position } from 'graphql-language-service-utils';
1718

1819
const MOCK_CONFIG = {
1920
schemaPath: './__schema__/StarWarsSchema.graphql',
@@ -110,4 +111,29 @@ describe('GraphQLLanguageService', () => {
110111
'String\n\nThe `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.',
111112
);
112113
});
114+
115+
it('runs document symbol requests as expected', async () => {
116+
const validQuery = `
117+
query OperationExample {
118+
item(episode: EMPIRE){
119+
...testFragment
120+
}
121+
}
122+
`;
123+
124+
const result = await languageService.getDocumentSymbols(
125+
validQuery,
126+
'file://file.graphql',
127+
);
128+
129+
expect(result).not.toBeUndefined();
130+
expect(result.length).toEqual(3);
131+
// expect(result[0].name).toEqual('item');
132+
expect(result[1].name).toEqual('item');
133+
expect(result[1].kind).toEqual(SymbolKind.Field);
134+
expect(result[1].location.range.start.line).toEqual(2);
135+
expect(result[1].location.range.start.character).toEqual(4);
136+
expect(result[1].location.range.end.line).toEqual(4);
137+
expect(result[1].location.range.end.character).toEqual(5);
138+
});
113139
});

0 commit comments

Comments
 (0)