vscode-apollo 2.1.0 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/.github/workflows/build-prs.yml +55 -0
  2. package/.github/workflows/release.yml +1 -1
  3. package/.gitleaks.toml +10 -3
  4. package/.vscode/launch.json +1 -0
  5. package/.vscodeignore +0 -1
  6. package/CHANGELOG.md +24 -0
  7. package/package.json +2 -2
  8. package/sampleWorkspace/httpSchema/apollo.config.ts +2 -0
  9. package/sampleWorkspace/httpSchema/self-signed.crt +22 -0
  10. package/sampleWorkspace/httpSchema/self-signed.key +28 -0
  11. package/src/__e2e__/mockServer.js +37 -11
  12. package/src/__e2e__/mocks.js +11 -7
  13. package/src/__e2e__/runTests.js +8 -6
  14. package/src/language-server/__e2e__/studioGraph.e2e.ts +4 -3
  15. package/src/language-server/config/__tests__/loadConfig.ts +9 -6
  16. package/src/language-server/config/config.ts +24 -11
  17. package/src/language-server/config/loadConfig.ts +2 -1
  18. package/src/language-server/fileSet.ts +8 -13
  19. package/src/language-server/project/base.ts +24 -17
  20. package/src/language-server/project/client.ts +1 -14
  21. package/src/language-server/project/internal.ts +18 -13
  22. package/src/language-server/project/rover/DocumentSynchronization.ts +120 -21
  23. package/src/language-server/project/rover/project.ts +84 -19
  24. package/src/language-server/providers/schema/endpoint.ts +15 -8
  25. package/src/language-server/server.ts +70 -48
  26. package/src/language-server/utilities/languageIdForExtension.ts +39 -0
  27. package/src/language-server/workspace.ts +27 -3
  28. package/src/languageServerClient.ts +13 -15
  29. package/src/tools/utilities/getLanguageInformation.ts +41 -0
  30. package/src/tools/utilities/languageInformation.ts +41 -0
  31. package/syntaxes/graphql.json +2 -2
@@ -12,6 +12,7 @@ import {
12
12
  TextDocumentChangeEvent,
13
13
  StarRequestHandler,
14
14
  StarNotificationHandler,
15
+ ServerCapabilities,
15
16
  } from "vscode-languageserver/node";
16
17
  import { TextDocument } from "vscode-languageserver-textdocument";
17
18
 
@@ -46,12 +47,13 @@ export abstract class GraphQLProject {
46
47
  private readyPromise: Promise<void>;
47
48
  public config: ApolloConfig;
48
49
  protected schema?: GraphQLSchema;
49
- protected fileSet: FileSet;
50
50
  protected rootURI: URI;
51
51
  protected loadingHandler: LoadingHandler;
52
52
 
53
53
  protected lastLoadDate?: number;
54
54
 
55
+ private configFileSet: FileSet;
56
+
55
57
  constructor({
56
58
  config,
57
59
  configFolderURI,
@@ -63,7 +65,7 @@ export abstract class GraphQLProject {
63
65
  // if a config doesn't have a uri associated, we can assume the `rootURI` is the project's root.
64
66
  this.rootURI = config.configDirURI || configFolderURI;
65
67
 
66
- this.fileSet = new FileSet({
68
+ this.configFileSet = new FileSet({
67
69
  rootURI: this.rootURI,
68
70
  includes: [
69
71
  ".env",
@@ -73,7 +75,6 @@ export abstract class GraphQLProject {
73
75
  "apollo.config.ts",
74
76
  ],
75
77
  excludes: [],
76
- configURI: config.configURI,
77
78
  });
78
79
 
79
80
  this._isReady = false;
@@ -117,32 +118,38 @@ export abstract class GraphQLProject {
117
118
  this._onDiagnostics = handler;
118
119
  }
119
120
 
120
- abstract includesFile(uri: DocumentUri): boolean;
121
+ abstract includesFile(uri: DocumentUri, languageId?: string): boolean;
122
+ isConfiguredBy(uri: DocumentUri): boolean {
123
+ return this.configFileSet.includesFile(uri);
124
+ }
121
125
 
122
126
  abstract onDidChangeWatchedFiles: ConnectionHandler["onDidChangeWatchedFiles"];
123
- abstract onDidOpen?: (event: TextDocumentChangeEvent<TextDocument>) => void;
124
- abstract onDidClose?: (event: TextDocumentChangeEvent<TextDocument>) => void;
127
+ onDidOpen?: (event: TextDocumentChangeEvent<TextDocument>) => void;
128
+ onDidClose?: (event: TextDocumentChangeEvent<TextDocument>) => void;
125
129
  abstract documentDidChange(document: TextDocument): void;
126
130
  abstract clearAllDiagnostics(): void;
127
131
 
128
- abstract onCompletion?: ConnectionHandler["onCompletion"];
129
- abstract onHover?: ConnectionHandler["onHover"];
130
- abstract onDefinition?: ConnectionHandler["onDefinition"];
131
- abstract onReferences?: ConnectionHandler["onReferences"];
132
- abstract onDocumentSymbol?: ConnectionHandler["onDocumentSymbol"];
133
- abstract onCodeLens?: ConnectionHandler["onCodeLens"];
134
- abstract onCodeAction?: ConnectionHandler["onCodeAction"];
132
+ onCompletion?: ConnectionHandler["onCompletion"];
133
+ onHover?: ConnectionHandler["onHover"];
134
+ onDefinition?: ConnectionHandler["onDefinition"];
135
+ onReferences?: ConnectionHandler["onReferences"];
136
+ onDocumentSymbol?: ConnectionHandler["onDocumentSymbol"];
137
+ onCodeLens?: ConnectionHandler["onCodeLens"];
138
+ onCodeAction?: ConnectionHandler["onCodeAction"];
135
139
 
136
- abstract onUnhandledRequest?: StarRequestHandler;
137
- abstract onUnhandledNotification?: (
140
+ onUnhandledRequest?: StarRequestHandler;
141
+ onUnhandledNotification?: (
138
142
  connection: Connection,
139
143
  ...rest: Parameters<StarNotificationHandler>
140
144
  ) => ReturnType<StarNotificationHandler>;
141
145
 
142
- abstract dispose?(): void;
146
+ dispose?(): void;
143
147
 
144
- abstract provideSymbol?(
148
+ provideSymbol?(
145
149
  query: string,
146
150
  token: CancellationToken,
147
151
  ): Promise<SymbolInformation[]>;
152
+
153
+ onVSCodeConnectionInitialized?(connection: Connection): void;
154
+ validate?(): void;
148
155
  }
@@ -201,20 +201,7 @@ export class GraphQLClientProject extends GraphQLInternalProject {
201
201
  super({ config, configFolderURI, loadingHandler, clientIdentity });
202
202
  this.serviceID = config.graph;
203
203
 
204
- /**
205
- * This function is used in the Array.filter function below it to remove any .env files and config files.
206
- * If there are 0 files remaining after removing those files, we should warn the user that their config
207
- * may be wrong. We shouldn't throw an error here, since they could just be initially setting up a project
208
- * and there's no way to know for sure that there _should_ be files.
209
- */
210
- const filterConfigAndEnvFiles = (path: string) =>
211
- !(
212
- path.includes("apollo.config") ||
213
- path.includes(".env") ||
214
- (config.configURI && path === config.configURI.fsPath)
215
- );
216
-
217
- if (this.allIncludedFiles().filter(filterConfigAndEnvFiles).length === 0) {
204
+ if (this.allIncludedFiles().length === 0) {
218
205
  console.warn(
219
206
  "⚠️ It looks like there are 0 files associated with this Apollo Project. " +
220
207
  "This may be because you don't have any files yet, or your includes/excludes " +
@@ -30,6 +30,8 @@ import {
30
30
  import { ApolloEngineClient, ClientIdentity } from "../engine";
31
31
  import { GraphQLProject, DocumentUri, GraphQLProjectConfig } from "./base";
32
32
  import throttle from "lodash.throttle";
33
+ import { FileSet } from "../fileSet";
34
+ import { getSupportedExtensions } from "../utilities/languageIdForExtension";
33
35
 
34
36
  const fileAssociations: { [extension: string]: string } = {
35
37
  ".graphql": "graphql",
@@ -58,6 +60,7 @@ export abstract class GraphQLInternalProject
58
60
  {
59
61
  public schemaProvider: GraphQLSchemaProvider;
60
62
  protected engineClient?: ApolloEngineClient;
63
+ private fileSet: FileSet;
61
64
 
62
65
  private needsValidation = false;
63
66
 
@@ -70,16 +73,23 @@ export abstract class GraphQLInternalProject
70
73
  clientIdentity,
71
74
  }: GraphQLInternalProjectConfig) {
72
75
  super({ config, configFolderURI, loadingHandler });
73
- const { includes = [], excludes = [] } = config.client;
76
+ const {
77
+ // something like
78
+ // 'src/**/*{.gql,.graphql,.graphqls,.js,.mjs,.cjs,.es6,.pac,.ts,.mts,.cts,.jsx,.tsx,.vue,.svelte,.py,.rpy,.pyw,.cpy,.gyp,.gypi,.pyi,.ipy,.pyt,.rb,.rbx,.rjs,.gemspec,.rake,.ru,.erb,.podspec,.rbi,.dart,.re,.ex,.exs}'
79
+ includes = [`src/**/*{${getSupportedExtensions().join(",")}}`],
80
+ excludes = [],
81
+ } = config.client;
74
82
 
75
83
  this.documentsByFile = new Map();
76
-
77
- this.fileSet.pushIncludes(includes);
78
- // We do not want to include the local schema file in our list of documents
79
- this.fileSet.pushExcludes([
80
- ...excludes,
81
- ...this.getRelativeLocalSchemaFilePaths(),
82
- ]);
84
+ this.fileSet = new FileSet({
85
+ rootURI: this.rootURI,
86
+ includes,
87
+ excludes: [
88
+ ...excludes,
89
+ // We do not want to include the local schema file in our list of documents
90
+ ...this.getRelativeLocalSchemaFilePaths(),
91
+ ],
92
+ });
83
93
 
84
94
  this.schemaProvider = schemaProviderFromConfig(config, clientIdentity);
85
95
  const { engine } = config;
@@ -327,8 +337,6 @@ export abstract class GraphQLInternalProject
327
337
  }
328
338
  return definitionsAndExtensions;
329
339
  }
330
- onDidOpen: undefined;
331
- onDidClose: undefined;
332
340
  onDidChangeWatchedFiles: GraphQLProject["onDidChangeWatchedFiles"] = (
333
341
  params,
334
342
  ) => {
@@ -343,7 +351,4 @@ export abstract class GraphQLInternalProject
343
351
  }
344
352
  }
345
353
  };
346
- onUnhandledRequest: undefined;
347
- onUnhandledNotification: undefined;
348
- dispose: undefined;
349
354
  }
@@ -8,6 +8,11 @@ import {
8
8
  Diagnostic,
9
9
  NotificationHandler,
10
10
  PublishDiagnosticsParams,
11
+ SemanticTokensRequest,
12
+ ProtocolRequestType,
13
+ SemanticTokensParams,
14
+ SemanticTokens,
15
+ CancellationToken,
11
16
  } from "vscode-languageserver-protocol";
12
17
  import { TextDocument } from "vscode-languageserver-textdocument";
13
18
  import { DocumentUri, GraphQLProject } from "../base";
@@ -94,10 +99,17 @@ export class DocumentSynchronization {
94
99
  private pendingDocumentChanges = new Map<DocumentUri, TextDocument>();
95
100
  private knownFiles = new Map<
96
101
  DocumentUri,
97
- {
98
- full: TextDocument;
99
- parts: ReadonlyArray<FilePart>;
100
- }
102
+ | {
103
+ source: "editor";
104
+ full: TextDocument;
105
+ parts: ReadonlyArray<FilePart>;
106
+ }
107
+ | {
108
+ source: "lsp";
109
+ full: Pick<TextDocument, "uri">;
110
+ parts?: undefined;
111
+ diagnostics?: Diagnostic[];
112
+ }
101
113
  >();
102
114
 
103
115
  constructor(
@@ -105,6 +117,11 @@ export class DocumentSynchronization {
105
117
  type: ProtocolNotificationType<P, RO>,
106
118
  params?: P,
107
119
  ) => Promise<void>,
120
+ private sendRequest: <P, R, PR, E, RO>(
121
+ type: ProtocolRequestType<P, R, PR, E, RO>,
122
+ params: P,
123
+ token?: CancellationToken,
124
+ ) => Promise<R>,
108
125
  private sendDiagnostics: NotificationHandler<PublishDiagnosticsParams>,
109
126
  ) {}
110
127
 
@@ -148,7 +165,11 @@ export class DocumentSynchronization {
148
165
  const newObj = Object.fromEntries(
149
166
  newParts.map((p) => [p.fractionalIndex, p]),
150
167
  );
151
- this.knownFiles.set(document.uri, { full: document, parts: newParts });
168
+ this.knownFiles.set(document.uri, {
169
+ source: "editor",
170
+ full: document,
171
+ parts: newParts,
172
+ });
152
173
 
153
174
  for (const newPart of newParts) {
154
175
  const previousPart = previousObj[newPart.fractionalIndex];
@@ -188,7 +209,9 @@ export class DocumentSynchronization {
188
209
 
189
210
  async resendAllDocuments() {
190
211
  for (const file of this.knownFiles.values()) {
191
- await this.sendDocumentChanges(file.full, []);
212
+ if (file.source === "editor") {
213
+ await this.sendDocumentChanges(file.full, []);
214
+ }
192
215
  }
193
216
  }
194
217
 
@@ -198,7 +221,7 @@ export class DocumentSynchronization {
198
221
  this.documentDidChange(params.document);
199
222
  };
200
223
 
201
- onDidCloseTextDocument: NonNullable<GraphQLProject["onDidClose"]> = (
224
+ onDidCloseTextDocument: NonNullable<GraphQLProject["onDidClose"]> = async (
202
225
  params,
203
226
  ) => {
204
227
  const known = this.knownFiles.get(params.document.uri);
@@ -206,15 +229,15 @@ export class DocumentSynchronization {
206
229
  return;
207
230
  }
208
231
  this.knownFiles.delete(params.document.uri);
209
- return Promise.all(
210
- known.parts.map((part) =>
211
- this.sendNotification(DidCloseTextDocumentNotification.type, {
232
+ if (known.source === "editor") {
233
+ for (const part of known.parts) {
234
+ await this.sendNotification(DidCloseTextDocumentNotification.type, {
212
235
  textDocument: {
213
236
  uri: getUri(known.full, part),
214
237
  },
215
- }),
216
- ),
217
- );
238
+ });
239
+ }
240
+ }
218
241
  };
219
242
 
220
243
  async documentDidChange(document: TextDocument) {
@@ -244,7 +267,7 @@ export class DocumentSynchronization {
244
267
  ): Promise<T | undefined> {
245
268
  await this.synchronizedWithDocument(positionParams.textDocument.uri);
246
269
  const found = this.knownFiles.get(positionParams.textDocument.uri);
247
- if (!found) {
270
+ if (!found || found.source !== "editor") {
248
271
  return;
249
272
  }
250
273
  const match = findContainedSourceAndPosition(
@@ -264,11 +287,14 @@ export class DocumentSynchronization {
264
287
  handlePartDiagnostics(params: PublishDiagnosticsParams) {
265
288
  DEBUG && console.log("Received diagnostics", params);
266
289
  const uriDetails = splitUri(params.uri);
267
- if (!uriDetails) {
268
- return;
269
- }
270
290
  const found = this.knownFiles.get(uriDetails.uri);
271
- if (!found) {
291
+ if (!found || found.source === "lsp") {
292
+ this.knownFiles.set(uriDetails.uri, {
293
+ source: "lsp",
294
+ full: { uri: uriDetails.uri },
295
+ diagnostics: params.diagnostics,
296
+ });
297
+ this.sendDiagnostics(params);
272
298
  return;
273
299
  }
274
300
  const part = found.parts.find(
@@ -294,15 +320,88 @@ export class DocumentSynchronization {
294
320
  }
295
321
 
296
322
  get openDocuments() {
297
- return [...this.knownFiles.values()].map((f) => f.full);
323
+ return [...this.knownFiles.values()]
324
+ .filter((f) => f.source === "editor")
325
+ .map((f) => f.full);
298
326
  }
299
327
 
300
328
  clearAllDiagnostics() {
301
329
  for (const file of this.knownFiles.values()) {
302
- for (const part of file.parts) {
303
- part.diagnostics = [];
330
+ if (file.source === "editor") {
331
+ for (const part of file.parts) {
332
+ part.diagnostics = [];
333
+ }
334
+ } else {
335
+ file.diagnostics = [];
304
336
  }
305
337
  this.sendDiagnostics({ uri: file.full.uri, diagnostics: [] });
306
338
  }
307
339
  }
340
+
341
+ /**
342
+ * Receives semantic tokens for all sub-documents and glues them together.
343
+ * See https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_semanticTokens
344
+ * TLDR: The tokens are a flat array of numbers, where each token is represented by 5 numbers.
345
+ * The first two numbers represent the token's delta line and delta start character and might need adjusing
346
+ * relative to the start of a sub-document in relation to the position of the last token of the previous sub-document.
347
+ *
348
+ * There is also an "incremental" version of this request, but we don't support it yet.
349
+ * This is complicated enough as it is.
350
+ */
351
+ async getFullSemanticTokens(
352
+ params: SemanticTokensParams,
353
+ cancellationToken: CancellationToken,
354
+ ): Promise<SemanticTokens | null> {
355
+ await this.synchronizedWithDocument(params.textDocument.uri);
356
+ const found = this.knownFiles.get(params.textDocument.uri);
357
+ if (!found || found.source !== "editor") {
358
+ return null;
359
+ }
360
+ const allParts = await Promise.all(
361
+ found.parts.map(async (part) => {
362
+ return {
363
+ part,
364
+ tokens: await this.sendRequest(
365
+ SemanticTokensRequest.type,
366
+ {
367
+ textDocument: { uri: getUri(found.full, part) },
368
+ },
369
+ cancellationToken,
370
+ ),
371
+ };
372
+ }),
373
+ );
374
+ let line = 0,
375
+ char = 0,
376
+ lastLine = 0,
377
+ lastChar = 0;
378
+ const combinedTokens = [];
379
+ for (const { part, tokens } of allParts) {
380
+ if (!tokens) {
381
+ continue;
382
+ }
383
+ line = part.source.locationOffset.line - 1;
384
+ char = part.source.locationOffset.column - 1;
385
+ for (let i = 0; i < tokens.data.length; i += 5) {
386
+ const deltaLine = tokens.data[i],
387
+ deltaStartChar = tokens.data[i + 1];
388
+
389
+ // We need to run this loop fully to correctly calculate the `lastLine` and `lastChar`
390
+ // so for the next incoming tokens, we can adjust the delta correctly.
391
+ line = line + deltaLine;
392
+ char = deltaLine === 0 ? char + deltaStartChar : deltaStartChar;
393
+ // we just need to adjust the deltas only for the first token
394
+ if (i === 0) {
395
+ tokens.data[0] = line - lastLine;
396
+ tokens.data[1] = line === lastLine ? lastChar - char : char;
397
+ }
398
+ }
399
+ combinedTokens.push(...tokens.data);
400
+ lastLine = line;
401
+ lastChar = char;
402
+ }
403
+ return {
404
+ data: combinedTokens,
405
+ };
406
+ }
308
407
  }
@@ -3,7 +3,6 @@ import { DocumentUri, GraphQLProject } from "../base";
3
3
  import { TextDocument } from "vscode-languageserver-textdocument";
4
4
  import {
5
5
  CancellationToken,
6
- SymbolInformation,
7
6
  InitializeRequest,
8
7
  StreamMessageReader,
9
8
  StreamMessageWriter,
@@ -19,6 +18,12 @@ import {
19
18
  PublishDiagnosticsNotification,
20
19
  ConnectionError,
21
20
  ConnectionErrors,
21
+ SemanticTokensRequest,
22
+ ProtocolRequestType0,
23
+ ServerCapabilities,
24
+ SemanticTokensRegistrationType,
25
+ SemanticTokensOptions,
26
+ SemanticTokensRegistrationOptions,
22
27
  } from "vscode-languageserver/node";
23
28
  import cp from "node:child_process";
24
29
  import { GraphQLProjectConfig } from "../base";
@@ -26,6 +31,10 @@ import { ApolloConfig, RoverConfig } from "../../config";
26
31
  import { DocumentSynchronization } from "./DocumentSynchronization";
27
32
  import { AsyncLocalStorage } from "node:async_hooks";
28
33
  import internal from "node:stream";
34
+ import { VSCodeConnection } from "../../server";
35
+ import { getLanguageIdForExtension } from "../../utilities/languageIdForExtension";
36
+ import { extname } from "node:path";
37
+ import type { FileExtension } from "../../../tools/utilities/languageInformation";
29
38
 
30
39
  export const DEBUG = true;
31
40
 
@@ -38,6 +47,10 @@ export interface RoverProjectConfig extends GraphQLProjectConfig {
38
47
  capabilities: ClientCapabilities;
39
48
  }
40
49
 
50
+ const supportedLanguageIds = (
51
+ process.env.APOLLO_ROVER_LANGUAGE_IDS || "graphql"
52
+ ).split(",");
53
+
41
54
  export class RoverProject extends GraphQLProject {
42
55
  config: RoverConfig;
43
56
  /**
@@ -52,11 +65,13 @@ export class RoverProject extends GraphQLProject {
52
65
  | undefined;
53
66
  private disposed = false;
54
67
  readonly capabilities: ClientCapabilities;
68
+ roverCapabilities?: ServerCapabilities;
55
69
  get displayName(): string {
56
70
  return "Rover Project";
57
71
  }
58
72
  private documents = new DocumentSynchronization(
59
73
  this.sendNotification.bind(this),
74
+ this.sendRequest.bind(this),
60
75
  (diagnostics) => this._onDiagnostics?.(diagnostics),
61
76
  );
62
77
 
@@ -143,7 +158,18 @@ export class RoverProject extends GraphQLProject {
143
158
  "Connection is closed.",
144
159
  );
145
160
  }
146
- const child = cp.spawn(this.config.rover.bin, ["lsp"], {
161
+ const args = ["lsp", "--elv2-license", "accept"];
162
+ if (this.config.rover.profile) {
163
+ args.push("--profile", this.config.rover.profile);
164
+ }
165
+ if (this.config.rover.supergraphConfig) {
166
+ args.push("--supergraph-config", this.config.rover.supergraphConfig);
167
+ }
168
+ args.push(...this.config.rover.extraArgs);
169
+
170
+ DEBUG &&
171
+ console.log(`starting ${this.config.rover.bin} '${args.join("' '")}'`);
172
+ const child = cp.spawn(this.config.rover.bin, args, {
147
173
  env: DEBUG ? { RUST_BACKTRACE: "1" } : {},
148
174
  stdio: ["pipe", "pipe", DEBUG ? "inherit" : "ignore"],
149
175
  });
@@ -185,6 +211,7 @@ export class RoverProject extends GraphQLProject {
185
211
  },
186
212
  source.token,
187
213
  );
214
+ this.roverCapabilities = status.capabilities;
188
215
  DEBUG && console.log("Connection initialized", status);
189
216
 
190
217
  await this.connectionStorage.run(
@@ -203,12 +230,16 @@ export class RoverProject extends GraphQLProject {
203
230
  return { type: "Rover", loaded: true };
204
231
  }
205
232
 
206
- includesFile(uri: DocumentUri) {
207
- return uri.startsWith(this.rootURI.toString());
233
+ includesFile(
234
+ uri: DocumentUri,
235
+ languageId = getLanguageIdForExtension(extname(uri) as FileExtension),
236
+ ) {
237
+ return (
238
+ uri.startsWith(this.rootURI.toString()) &&
239
+ supportedLanguageIds.includes(languageId)
240
+ );
208
241
  }
209
242
 
210
- validate?: () => void;
211
-
212
243
  onDidChangeWatchedFiles: GraphQLProject["onDidChangeWatchedFiles"] = (
213
244
  params,
214
245
  ) => {
@@ -250,8 +281,17 @@ export class RoverProject extends GraphQLProject {
250
281
  this.sendRequest(HoverRequest.type, virtualParams, token),
251
282
  );
252
283
 
253
- onUnhandledRequest: GraphQLProject["onUnhandledRequest"] = (type, params) => {
254
- DEBUG && console.info("unhandled request from VSCode", { type, params });
284
+ onUnhandledRequest: GraphQLProject["onUnhandledRequest"] = async (
285
+ type,
286
+ params,
287
+ token,
288
+ ) => {
289
+ if (isRequestType(SemanticTokensRequest.type, type, params)) {
290
+ return this.documents.getFullSemanticTokens(params, token);
291
+ } else {
292
+ DEBUG && console.info("unhandled request from VSCode", { type, params });
293
+ return undefined;
294
+ }
255
295
  };
256
296
  onUnhandledNotification: GraphQLProject["onUnhandledNotification"] = (
257
297
  _connection,
@@ -262,15 +302,40 @@ export class RoverProject extends GraphQLProject {
262
302
  console.info("unhandled notification from VSCode", { type, params });
263
303
  };
264
304
 
265
- // these are not supported yet
266
- onDefinition: GraphQLProject["onDefinition"];
267
- onReferences: GraphQLProject["onReferences"];
268
- onDocumentSymbol: GraphQLProject["onDocumentSymbol"];
269
- onCodeLens: GraphQLProject["onCodeLens"];
270
- onCodeAction: GraphQLProject["onCodeAction"];
271
-
272
- provideSymbol?(
273
- query: string,
274
- token: CancellationToken,
275
- ): Promise<SymbolInformation[]>;
305
+ async onVSCodeConnectionInitialized(connection: VSCodeConnection) {
306
+ // Report the actual capabilities of the upstream LSP to VSCode.
307
+ // It is important to actually "ask" the LSP for this, because the capabilities
308
+ // also define the semantic token legend, which is needed to interpret the tokens.
309
+ await this.getConnection();
310
+ const capabilities = this.roverCapabilities;
311
+ if (capabilities?.semanticTokensProvider) {
312
+ connection.client.register(SemanticTokensRegistrationType.type, {
313
+ documentSelector: null,
314
+ ...capabilities.semanticTokensProvider,
315
+ full: {
316
+ // the upstream LSP supports "true" here, but we don't yet
317
+ delta: false,
318
+ },
319
+ });
320
+ }
321
+ }
322
+ }
323
+
324
+ function isRequestType<R, PR, E, RO>(
325
+ type: ProtocolRequestType0<R, PR, E, RO>,
326
+ method: string,
327
+ params: any,
328
+ ): params is PR;
329
+ function isRequestType<P, R, PR, E, RO>(
330
+ type: ProtocolRequestType<P, R, PR, E, RO>,
331
+ method: string,
332
+ params: any,
333
+ ): params is P;
334
+ function isRequestType(
335
+ type:
336
+ | ProtocolRequestType0<any, any, any, any>
337
+ | ProtocolRequestType<any, any, any, any, any>,
338
+ method: string,
339
+ ) {
340
+ return type.method === method;
276
341
  }
@@ -17,7 +17,16 @@ import { RemoteServiceConfig } from "../../config";
17
17
  import { GraphQLSchemaProvider, SchemaChangeUnsubscribeHandler } from "./base";
18
18
  import { Debug } from "../../utilities";
19
19
  import { isString } from "util";
20
-
20
+ import { fetch as undiciFetch, Agent } from "undici";
21
+
22
+ const skipSSLValidationFetchOptions = {
23
+ // see https://github.com/nodejs/undici/issues/1489#issuecomment-1543856261
24
+ dispatcher: new Agent({
25
+ connect: {
26
+ rejectUnauthorized: false,
27
+ },
28
+ }),
29
+ } satisfies import("undici").RequestInit;
21
30
  export class EndpointSchemaProvider implements GraphQLSchemaProvider {
22
31
  private schema?: GraphQLSchema;
23
32
  private federatedServiceSDL?: string;
@@ -28,11 +37,11 @@ export class EndpointSchemaProvider implements GraphQLSchemaProvider {
28
37
  const { skipSSLValidation, url, headers } = this.config;
29
38
  const options: HttpOptions = {
30
39
  uri: url,
40
+ fetch: undiciFetch as typeof fetch,
31
41
  };
42
+
32
43
  if (url.startsWith("https:") && skipSSLValidation) {
33
- options.fetchOptions = {
34
- agent: new HTTPSAgent({ rejectUnauthorized: false }),
35
- };
44
+ options.fetchOptions = skipSSLValidationFetchOptions;
36
45
  }
37
46
 
38
47
  const { data, errors } = (await toPromise(
@@ -92,12 +101,10 @@ export class EndpointSchemaProvider implements GraphQLSchemaProvider {
92
101
  const { skipSSLValidation, url, headers } = this.config;
93
102
  const options: HttpOptions = {
94
103
  uri: url,
95
- fetch,
104
+ fetch: undiciFetch as typeof fetch,
96
105
  };
97
106
  if (url.startsWith("https:") && skipSSLValidation) {
98
- options.fetchOptions = {
99
- agent: new HTTPSAgent({ rejectUnauthorized: false }),
100
- };
107
+ options.fetchOptions = skipSSLValidationFetchOptions;
101
108
  }
102
109
 
103
110
  const getFederationInfoQuery = `