1 | import {
|
2 | CancellationToken,
|
3 | Position,
|
4 | Location,
|
5 | Range,
|
6 | CompletionItem,
|
7 | Hover,
|
8 | Definition,
|
9 | CodeLens,
|
10 | ReferenceContext,
|
11 | InsertTextFormat,
|
12 | DocumentSymbol,
|
13 | SymbolKind,
|
14 | SymbolInformation,
|
15 | CodeAction,
|
16 | CodeActionKind,
|
17 | MarkupKind,
|
18 | CompletionItemKind,
|
19 | } from "vscode-languageserver";
|
20 |
|
21 |
|
22 | import { getAutocompleteSuggestions } from "@apollographql/graphql-language-service-interface";
|
23 | import {
|
24 | getTokenAtPosition,
|
25 | getTypeInfo,
|
26 | } from "@apollographql/graphql-language-service-interface/dist/getAutocompleteSuggestions";
|
27 | import { GraphQLWorkspace } from "./workspace";
|
28 | import { DocumentUri } from "./project/base";
|
29 |
|
30 | import {
|
31 | positionFromPositionInContainingDocument,
|
32 | rangeForASTNode,
|
33 | getASTNodeAndTypeInfoAtPosition,
|
34 | positionToOffset,
|
35 | } from "./utilities/source";
|
36 |
|
37 | import {
|
38 | GraphQLNamedType,
|
39 | Kind,
|
40 | GraphQLField,
|
41 | GraphQLNonNull,
|
42 | isAbstractType,
|
43 | TypeNameMetaFieldDef,
|
44 | SchemaMetaFieldDef,
|
45 | TypeMetaFieldDef,
|
46 | typeFromAST,
|
47 | GraphQLType,
|
48 | isObjectType,
|
49 | isListType,
|
50 | GraphQLList,
|
51 | isNonNullType,
|
52 | ASTNode,
|
53 | FieldDefinitionNode,
|
54 | visit,
|
55 | isExecutableDefinitionNode,
|
56 | isTypeSystemDefinitionNode,
|
57 | isTypeSystemExtensionNode,
|
58 | GraphQLError,
|
59 | DirectiveLocation,
|
60 | } from "graphql";
|
61 | import { highlightNodeForNode } from "./utilities/graphql";
|
62 |
|
63 | import { GraphQLClientProject, isClientProject } from "./project/client";
|
64 | import { isNotNullOrUndefined } from "@apollographql/apollo-tools";
|
65 | import { CodeActionInfo } from "./errors/validation";
|
66 | import { GraphQLDiagnostic } from "./diagnostics";
|
67 |
|
68 | const DirectiveLocations = Object.keys(DirectiveLocation);
|
69 |
|
70 | function hasFields(type: GraphQLType): boolean {
|
71 | return (
|
72 | isObjectType(type) ||
|
73 | (isListType(type) && hasFields((type as GraphQLList<any>).ofType)) ||
|
74 | (isNonNullType(type) && hasFields((type as GraphQLNonNull<any>).ofType))
|
75 | );
|
76 | }
|
77 |
|
78 | function uriForASTNode(node: ASTNode): DocumentUri | null {
|
79 | const uri = node.loc && node.loc.source && node.loc.source.name;
|
80 | if (!uri || uri === "GraphQL") {
|
81 | return null;
|
82 | }
|
83 | return uri;
|
84 | }
|
85 |
|
86 | function locationForASTNode(node: ASTNode): Location | null {
|
87 | const uri = uriForASTNode(node);
|
88 | if (!uri) return null;
|
89 | return Location.create(uri, rangeForASTNode(node));
|
90 | }
|
91 |
|
92 | function symbolForFieldDefinition(
|
93 | definition: FieldDefinitionNode
|
94 | ): DocumentSymbol {
|
95 | return {
|
96 | name: definition.name.value,
|
97 | kind: SymbolKind.Field,
|
98 | range: rangeForASTNode(definition),
|
99 | selectionRange: rangeForASTNode(definition),
|
100 | };
|
101 | }
|
102 |
|
103 | export class GraphQLLanguageProvider {
|
104 | constructor(public workspace: GraphQLWorkspace) {}
|
105 |
|
106 | async provideStats(uri?: DocumentUri) {
|
107 | if (this.workspace.projects.length && uri) {
|
108 | const project = this.workspace.projectForFile(uri);
|
109 | return project ? project.getProjectStats() : { loaded: false };
|
110 | }
|
111 |
|
112 | return { loaded: false };
|
113 | }
|
114 |
|
115 | async provideCompletionItems(
|
116 | uri: DocumentUri,
|
117 | position: Position,
|
118 | _token: CancellationToken
|
119 | ): Promise<CompletionItem[]> {
|
120 | const project = this.workspace.projectForFile(uri);
|
121 | if (!(project && project instanceof GraphQLClientProject)) return [];
|
122 |
|
123 | const document = project.documentAt(uri, position);
|
124 | if (!document) return [];
|
125 |
|
126 | if (!project.schema) return [];
|
127 |
|
128 | const positionInDocument = positionFromPositionInContainingDocument(
|
129 | document.source,
|
130 | position
|
131 | );
|
132 | const token = getTokenAtPosition(document.source.body, positionInDocument);
|
133 | const state =
|
134 | token.state.kind === "Invalid" ? token.state.prevState : token.state;
|
135 | const typeInfo = getTypeInfo(project.schema, token.state);
|
136 |
|
137 | if (state.kind === "DirectiveLocation") {
|
138 | return DirectiveLocations.map((location) => ({
|
139 | label: location,
|
140 | kind: CompletionItemKind.Constant,
|
141 | }));
|
142 | }
|
143 |
|
144 | const suggestions = getAutocompleteSuggestions(
|
145 | project.schema,
|
146 | document.source.body,
|
147 | positionInDocument
|
148 | );
|
149 |
|
150 | if (
|
151 | state.kind === "SelectionSet" ||
|
152 | state.kind === "Field" ||
|
153 | state.kind === "AliasedField"
|
154 | ) {
|
155 | const parentType = typeInfo.parentType;
|
156 | const parentFields = {
|
157 | ...(parentType.getFields() as {
|
158 | [label: string]: GraphQLField<any, any>;
|
159 | }),
|
160 | };
|
161 |
|
162 | if (isAbstractType(parentType)) {
|
163 | parentFields[TypeNameMetaFieldDef.name] = TypeNameMetaFieldDef;
|
164 | }
|
165 |
|
166 | if (parentType === project.schema.getQueryType()) {
|
167 | parentFields[SchemaMetaFieldDef.name] = SchemaMetaFieldDef;
|
168 | parentFields[TypeMetaFieldDef.name] = TypeMetaFieldDef;
|
169 | }
|
170 |
|
171 | return suggestions.map((suggest) => {
|
172 |
|
173 | const suggestedField = parentFields[suggest.label] as GraphQLField<
|
174 | void,
|
175 | void
|
176 | >;
|
177 | if (!suggestedField) {
|
178 | return suggest;
|
179 | } else {
|
180 | const requiredArgs = suggestedField.args.filter((a) =>
|
181 | isNonNullType(a.type)
|
182 | );
|
183 | const paramsSection =
|
184 | requiredArgs.length > 0
|
185 | ? `(${requiredArgs
|
186 | .map((a, i) => `${a.name}: $${i + 1}`)
|
187 | .join(", ")})`
|
188 | : ``;
|
189 |
|
190 | const isClientType =
|
191 | parentType.clientSchema &&
|
192 | parentType.clientSchema.localFields &&
|
193 | parentType.clientSchema.localFields.includes(suggestedField.name);
|
194 | const directives = isClientType ? " @client" : "";
|
195 |
|
196 | const snippet = hasFields(suggestedField.type)
|
197 | ? `${suggest.label}${paramsSection}${directives} {\n\t$0\n}`
|
198 | : `${suggest.label}${paramsSection}${directives}`;
|
199 |
|
200 | return {
|
201 | ...suggest,
|
202 | insertText: snippet,
|
203 | insertTextFormat: InsertTextFormat.Snippet,
|
204 | };
|
205 | }
|
206 | });
|
207 | }
|
208 |
|
209 | if (state.kind === "Directive") {
|
210 | return suggestions.map((suggest) => {
|
211 | const directive = project.schema!.getDirective(suggest.label);
|
212 | if (!directive) {
|
213 | return suggest;
|
214 | }
|
215 |
|
216 | const requiredArgs = directive.args.filter(isNonNullType);
|
217 | const paramsSection =
|
218 | requiredArgs.length > 0
|
219 | ? `(${requiredArgs
|
220 | .map((a, i) => `${a.name}: $${i + 1}`)
|
221 | .join(", ")})`
|
222 | : ``;
|
223 |
|
224 | const snippet = `${suggest.label}${paramsSection}`;
|
225 |
|
226 | const argsString =
|
227 | directive.args.length > 0
|
228 | ? `(${directive.args
|
229 | .map((a) => `${a.name}: ${a.type}`)
|
230 | .join(", ")})`
|
231 | : "";
|
232 |
|
233 | const content = [
|
234 | [`\`\`\`graphql`, `@${suggest.label}${argsString}`, `\`\`\``].join(
|
235 | "\n"
|
236 | ),
|
237 | ];
|
238 |
|
239 | if (suggest.documentation) {
|
240 | if (typeof suggest.documentation === "string") {
|
241 | content.push(suggest.documentation);
|
242 | } else {
|
243 | content.push(suggest.documentation.value);
|
244 | }
|
245 | }
|
246 |
|
247 | const doc = {
|
248 | kind: MarkupKind.Markdown,
|
249 | value: content.join("\n\n"),
|
250 | };
|
251 |
|
252 | return {
|
253 | ...suggest,
|
254 | documentation: doc,
|
255 | insertText: snippet,
|
256 | insertTextFormat: InsertTextFormat.Snippet,
|
257 | };
|
258 | });
|
259 | }
|
260 |
|
261 | return suggestions;
|
262 | }
|
263 |
|
264 | async provideHover(
|
265 | uri: DocumentUri,
|
266 | position: Position,
|
267 | _token: CancellationToken
|
268 | ): Promise<Hover | null> {
|
269 | const project = this.workspace.projectForFile(uri);
|
270 | if (!(project && project instanceof GraphQLClientProject)) return null;
|
271 |
|
272 | const document = project.documentAt(uri, position);
|
273 | if (!(document && document.ast)) return null;
|
274 |
|
275 | if (!project.schema) return null;
|
276 |
|
277 | const positionInDocument = positionFromPositionInContainingDocument(
|
278 | document.source,
|
279 | position
|
280 | );
|
281 |
|
282 | const nodeAndTypeInfo = getASTNodeAndTypeInfoAtPosition(
|
283 | document.source,
|
284 | positionInDocument,
|
285 | document.ast,
|
286 | project.schema
|
287 | );
|
288 |
|
289 | if (nodeAndTypeInfo) {
|
290 | const [node, typeInfo] = nodeAndTypeInfo;
|
291 |
|
292 | switch (node.kind) {
|
293 | case Kind.FRAGMENT_SPREAD: {
|
294 | const fragmentName = node.name.value;
|
295 | const fragment = project.fragments[fragmentName];
|
296 | if (fragment) {
|
297 | return {
|
298 | contents: {
|
299 | language: "graphql",
|
300 | value: `fragment ${fragmentName} on ${fragment.typeCondition.name.value}`,
|
301 | },
|
302 | };
|
303 | }
|
304 | break;
|
305 | }
|
306 |
|
307 | case Kind.FIELD: {
|
308 | const parentType = typeInfo.getParentType();
|
309 | const fieldDef = typeInfo.getFieldDef();
|
310 |
|
311 | if (parentType && fieldDef) {
|
312 | const argsString =
|
313 | fieldDef.args.length > 0
|
314 | ? `(${fieldDef.args
|
315 | .map((a) => `${a.name}: ${a.type}`)
|
316 | .join(", ")})`
|
317 | : "";
|
318 | const isClientType =
|
319 | parentType.clientSchema &&
|
320 | parentType.clientSchema.localFields &&
|
321 | parentType.clientSchema.localFields.includes(fieldDef.name);
|
322 |
|
323 | const isResolvedLocally =
|
324 | node.directives &&
|
325 | node.directives.some(
|
326 | (directive) => directive.name.value === "client"
|
327 | );
|
328 |
|
329 | const content = [
|
330 | [
|
331 | `\`\`\`graphql`,
|
332 | `${parentType}.${fieldDef.name}${argsString}: ${fieldDef.type}`,
|
333 | `\`\`\``,
|
334 | ].join("\n"),
|
335 | ];
|
336 |
|
337 | const info: string[] = [];
|
338 | if (isClientType) {
|
339 | info.push("`Client-Only Field`");
|
340 | }
|
341 | if (isResolvedLocally) {
|
342 | info.push("`Resolved locally`");
|
343 | }
|
344 |
|
345 | if (info.length !== 0) {
|
346 | content.push(info.join(" "));
|
347 | }
|
348 |
|
349 | if (fieldDef.description) {
|
350 | content.push(fieldDef.description);
|
351 | }
|
352 |
|
353 | return {
|
354 | contents: content.join("\n\n---\n\n"),
|
355 | range: rangeForASTNode(highlightNodeForNode(node)),
|
356 | };
|
357 | }
|
358 |
|
359 | break;
|
360 | }
|
361 |
|
362 | case Kind.NAMED_TYPE: {
|
363 | const type = project.schema.getType(
|
364 | node.name.value
|
365 | ) as GraphQLNamedType | void;
|
366 | if (!type) break;
|
367 |
|
368 | const content = [[`\`\`\`graphql`, `${type}`, `\`\`\``].join("\n")];
|
369 |
|
370 | if (type.description) {
|
371 | content.push(type.description);
|
372 | }
|
373 |
|
374 | return {
|
375 | contents: content.join("\n\n---\n\n"),
|
376 | range: rangeForASTNode(highlightNodeForNode(node)),
|
377 | };
|
378 | }
|
379 |
|
380 | case Kind.ARGUMENT: {
|
381 | const argumentNode = typeInfo.getArgument()!;
|
382 | const content = [
|
383 | [
|
384 | `\`\`\`graphql`,
|
385 | `${argumentNode.name}: ${argumentNode.type}`,
|
386 | `\`\`\``,
|
387 | ].join("\n"),
|
388 | ];
|
389 | if (argumentNode.description) {
|
390 | content.push(argumentNode.description);
|
391 | }
|
392 | return {
|
393 | contents: content.join("\n\n---\n\n"),
|
394 | range: rangeForASTNode(highlightNodeForNode(node)),
|
395 | };
|
396 | }
|
397 |
|
398 | case Kind.DIRECTIVE: {
|
399 | const directiveNode = typeInfo.getDirective();
|
400 | if (!directiveNode) break;
|
401 | const argsString =
|
402 | directiveNode.args.length > 0
|
403 | ? `(${directiveNode.args
|
404 | .map((a) => `${a.name}: ${a.type}`)
|
405 | .join(", ")})`
|
406 | : "";
|
407 | const content = [
|
408 | [
|
409 | `\`\`\`graphql`,
|
410 | `@${directiveNode.name}${argsString}`,
|
411 | `\`\`\``,
|
412 | ].join("\n"),
|
413 | ];
|
414 | if (directiveNode.description) {
|
415 | content.push(directiveNode.description);
|
416 | }
|
417 | return {
|
418 | contents: content.join("\n\n---\n\n"),
|
419 | range: rangeForASTNode(highlightNodeForNode(node)),
|
420 | };
|
421 | }
|
422 | }
|
423 | }
|
424 | return null;
|
425 | }
|
426 |
|
427 | async provideDefinition(
|
428 | uri: DocumentUri,
|
429 | position: Position,
|
430 | _token: CancellationToken
|
431 | ): Promise<Definition | null> {
|
432 | const project = this.workspace.projectForFile(uri);
|
433 | if (!(project && project instanceof GraphQLClientProject)) return null;
|
434 |
|
435 | const document = project.documentAt(uri, position);
|
436 | if (!(document && document.ast)) return null;
|
437 |
|
438 | if (!project.schema) return null;
|
439 |
|
440 | const positionInDocument = positionFromPositionInContainingDocument(
|
441 | document.source,
|
442 | position
|
443 | );
|
444 |
|
445 | const nodeAndTypeInfo = getASTNodeAndTypeInfoAtPosition(
|
446 | document.source,
|
447 | positionInDocument,
|
448 | document.ast,
|
449 | project.schema
|
450 | );
|
451 |
|
452 | if (nodeAndTypeInfo) {
|
453 | const [node, typeInfo] = nodeAndTypeInfo;
|
454 |
|
455 | switch (node.kind) {
|
456 | case Kind.FRAGMENT_SPREAD: {
|
457 | const fragmentName = node.name.value;
|
458 | const fragment = project.fragments[fragmentName];
|
459 | if (fragment && fragment.loc) {
|
460 | return locationForASTNode(fragment);
|
461 | }
|
462 | break;
|
463 | }
|
464 | case Kind.FIELD: {
|
465 | const fieldDef = typeInfo.getFieldDef();
|
466 |
|
467 | if (!(fieldDef && fieldDef.astNode && fieldDef.astNode.loc)) break;
|
468 |
|
469 | return locationForASTNode(fieldDef.astNode);
|
470 | }
|
471 | case Kind.NAMED_TYPE: {
|
472 | const type = typeFromAST(project.schema, node);
|
473 |
|
474 | if (!(type && type.astNode && type.astNode.loc)) break;
|
475 |
|
476 | return locationForASTNode(type.astNode);
|
477 | }
|
478 | case Kind.DIRECTIVE: {
|
479 | const directive = project.schema.getDirective(node.name.value);
|
480 |
|
481 | if (!(directive && directive.astNode && directive.astNode.loc)) break;
|
482 |
|
483 | return locationForASTNode(directive.astNode);
|
484 | }
|
485 | }
|
486 | }
|
487 | return null;
|
488 | }
|
489 |
|
490 | async provideReferences(
|
491 | uri: DocumentUri,
|
492 | position: Position,
|
493 | _context: ReferenceContext,
|
494 | _token: CancellationToken
|
495 | ): Promise<Location[] | null> {
|
496 | const project = this.workspace.projectForFile(uri);
|
497 | if (!project) return null;
|
498 | const document = project.documentAt(uri, position);
|
499 | if (!(document && document.ast)) return null;
|
500 |
|
501 | if (!project.schema) return null;
|
502 |
|
503 | const positionInDocument = positionFromPositionInContainingDocument(
|
504 | document.source,
|
505 | position
|
506 | );
|
507 |
|
508 | const nodeAndTypeInfo = getASTNodeAndTypeInfoAtPosition(
|
509 | document.source,
|
510 | positionInDocument,
|
511 | document.ast,
|
512 | project.schema
|
513 | );
|
514 |
|
515 | if (nodeAndTypeInfo) {
|
516 | const [node, typeInfo] = nodeAndTypeInfo;
|
517 |
|
518 | switch (node.kind) {
|
519 | case Kind.FRAGMENT_DEFINITION: {
|
520 | if (!isClientProject(project)) return null;
|
521 | const fragmentName = node.name.value;
|
522 | return project
|
523 | .fragmentSpreadsForFragment(fragmentName)
|
524 | .map((fragmentSpread) => locationForASTNode(fragmentSpread))
|
525 | .filter(isNotNullOrUndefined);
|
526 | }
|
527 |
|
528 |
|
529 | case Kind.FIELD_DEFINITION: {
|
530 |
|
531 |
|
532 |
|
533 | if (!isClientProject(project)) return null;
|
534 | const offset = positionToOffset(document.source, positionInDocument);
|
535 |
|
536 |
|
537 |
|
538 | let parent: ASTNode | null = null;
|
539 | visit(document.ast, {
|
540 | enter(node: ASTNode) {
|
541 |
|
542 | if (
|
543 | node.loc &&
|
544 | node.loc.start <= offset &&
|
545 | offset <= node.loc.end &&
|
546 | (node.kind === Kind.OBJECT_TYPE_DEFINITION ||
|
547 | node.kind === Kind.OBJECT_TYPE_EXTENSION ||
|
548 | node.kind === Kind.INTERFACE_TYPE_DEFINITION ||
|
549 | node.kind === Kind.INTERFACE_TYPE_EXTENSION ||
|
550 | node.kind === Kind.INPUT_OBJECT_TYPE_DEFINITION ||
|
551 | node.kind === Kind.INPUT_OBJECT_TYPE_EXTENSION ||
|
552 | node.kind === Kind.ENUM_TYPE_DEFINITION ||
|
553 | node.kind === Kind.ENUM_TYPE_EXTENSION)
|
554 | ) {
|
555 | parent = node;
|
556 | }
|
557 | return;
|
558 | },
|
559 | });
|
560 | return project
|
561 | .getOperationFieldsFromFieldDefinition(node.name.value, parent)
|
562 | .map((fieldNode) => locationForASTNode(fieldNode))
|
563 | .filter(isNotNullOrUndefined);
|
564 | }
|
565 | }
|
566 | }
|
567 |
|
568 | return null;
|
569 | }
|
570 |
|
571 | async provideDocumentSymbol(
|
572 | uri: DocumentUri,
|
573 | _token: CancellationToken
|
574 | ): Promise<DocumentSymbol[]> {
|
575 | const project = this.workspace.projectForFile(uri);
|
576 | if (!project) return [];
|
577 |
|
578 | const definitions = project.definitionsAt(uri);
|
579 |
|
580 | const symbols: DocumentSymbol[] = [];
|
581 |
|
582 | for (const definition of definitions) {
|
583 | if (isExecutableDefinitionNode(definition)) {
|
584 | if (!definition.name) continue;
|
585 | const location = locationForASTNode(definition);
|
586 | if (!location) continue;
|
587 | symbols.push({
|
588 | name: definition.name.value,
|
589 | kind: SymbolKind.Function,
|
590 | range: rangeForASTNode(definition),
|
591 | selectionRange: rangeForASTNode(highlightNodeForNode(definition)),
|
592 | });
|
593 | } else if (
|
594 | isTypeSystemDefinitionNode(definition) ||
|
595 | isTypeSystemExtensionNode(definition)
|
596 | ) {
|
597 | if (
|
598 | definition.kind === Kind.SCHEMA_DEFINITION ||
|
599 | definition.kind === Kind.SCHEMA_EXTENSION
|
600 | ) {
|
601 | continue;
|
602 | }
|
603 | symbols.push({
|
604 | name: definition.name.value,
|
605 | kind: SymbolKind.Class,
|
606 | range: rangeForASTNode(definition),
|
607 | selectionRange: rangeForASTNode(highlightNodeForNode(definition)),
|
608 | children:
|
609 | definition.kind === Kind.OBJECT_TYPE_DEFINITION ||
|
610 | definition.kind === Kind.OBJECT_TYPE_EXTENSION
|
611 | ? (definition.fields || []).map(symbolForFieldDefinition)
|
612 | : undefined,
|
613 | });
|
614 | }
|
615 | }
|
616 |
|
617 | return symbols;
|
618 | }
|
619 |
|
620 | async provideWorkspaceSymbol(
|
621 | query: string,
|
622 | _token: CancellationToken
|
623 | ): Promise<SymbolInformation[]> {
|
624 | const symbols: SymbolInformation[] = [];
|
625 | for (const project of this.workspace.projects) {
|
626 | for (const definition of project.definitions) {
|
627 | if (isExecutableDefinitionNode(definition)) {
|
628 | if (!definition.name) continue;
|
629 | const location = locationForASTNode(definition);
|
630 | if (!location) continue;
|
631 | symbols.push({
|
632 | name: definition.name.value,
|
633 | kind: SymbolKind.Function,
|
634 | location,
|
635 | });
|
636 | }
|
637 | }
|
638 | }
|
639 | return symbols;
|
640 | }
|
641 |
|
642 | async provideCodeLenses(
|
643 | uri: DocumentUri,
|
644 | _token: CancellationToken
|
645 | ): Promise<CodeLens[]> {
|
646 | const project = this.workspace.projectForFile(uri);
|
647 | if (!(project && project instanceof GraphQLClientProject)) return [];
|
648 |
|
649 |
|
650 |
|
651 | await project.whenReady;
|
652 |
|
653 | const documents = project.documentsAt(uri);
|
654 | if (!documents) return [];
|
655 |
|
656 | let codeLenses: CodeLens[] = [];
|
657 |
|
658 | for (const document of documents) {
|
659 | if (!document.ast) continue;
|
660 |
|
661 | for (const definition of document.ast.definitions) {
|
662 | if (definition.kind === Kind.OPERATION_DEFINITION) {
|
663 | |
664 |
|
665 |
|
666 |
|
667 |
|
668 |
|
669 |
|
670 |
|
671 |
|
672 |
|
673 |
|
674 |
|
675 |
|
676 |
|
677 |
|
678 |
|
679 |
|
680 |
|
681 |
|
682 |
|
683 |
|
684 |
|
685 |
|
686 |
|
687 |
|
688 |
|
689 |
|
690 |
|
691 |
|
692 |
|
693 |
|
694 |
|
695 |
|
696 |
|
697 |
|
698 |
|
699 |
|
700 |
|
701 |
|
702 |
|
703 | } else if (definition.kind === Kind.FRAGMENT_DEFINITION) {
|
704 |
|
705 |
|
706 |
|
707 |
|
708 |
|
709 |
|
710 |
|
711 |
|
712 |
|
713 |
|
714 |
|
715 |
|
716 |
|
717 |
|
718 |
|
719 |
|
720 |
|
721 | }
|
722 | }
|
723 | }
|
724 | return codeLenses;
|
725 | }
|
726 |
|
727 | async provideCodeAction(
|
728 | uri: DocumentUri,
|
729 | range: Range,
|
730 | _token: CancellationToken
|
731 | ): Promise<CodeAction[]> {
|
732 | function isPositionLessThanOrEqual(a: Position, b: Position) {
|
733 | return a.line !== b.line ? a.line < b.line : a.character <= b.character;
|
734 | }
|
735 |
|
736 | const project = this.workspace.projectForFile(uri);
|
737 | if (
|
738 | !(
|
739 | project &&
|
740 | project instanceof GraphQLClientProject &&
|
741 | project.diagnosticSet
|
742 | )
|
743 | )
|
744 | return [];
|
745 |
|
746 | await project.whenReady;
|
747 |
|
748 | const documents = project.documentsAt(uri);
|
749 | if (!documents) return [];
|
750 |
|
751 | const errors: Set<GraphQLError> = new Set();
|
752 |
|
753 | for (const [
|
754 | diagnosticUri,
|
755 | diagnostics,
|
756 | ] of project.diagnosticSet.entries()) {
|
757 | if (diagnosticUri !== uri) continue;
|
758 |
|
759 | for (const diagnostic of diagnostics) {
|
760 | if (
|
761 | GraphQLDiagnostic.is(diagnostic) &&
|
762 | isPositionLessThanOrEqual(range.start, diagnostic.range.end) &&
|
763 | isPositionLessThanOrEqual(diagnostic.range.start, range.end)
|
764 | ) {
|
765 | errors.add(diagnostic.error);
|
766 | }
|
767 | }
|
768 | }
|
769 |
|
770 | const result: CodeAction[] = [];
|
771 |
|
772 | for (const error of errors) {
|
773 | const { extensions } = error;
|
774 | if (!extensions || !extensions.codeAction) continue;
|
775 |
|
776 | const { message, edits }: CodeActionInfo = extensions.codeAction;
|
777 |
|
778 | const codeAction = CodeAction.create(
|
779 | message,
|
780 | { changes: { [uri]: edits } },
|
781 | CodeActionKind.QuickFix
|
782 | );
|
783 |
|
784 | result.push(codeAction);
|
785 | }
|
786 |
|
787 | return result;
|
788 | }
|
789 | }
|