vscode-apollo 2.0.0 → 2.1.0

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 (39) hide show
  1. package/.circleci/config.yml +1 -1
  2. package/.vscode/launch.json +4 -1
  3. package/CHANGELOG.md +33 -0
  4. package/package.json +9 -3
  5. package/renovate.json +2 -1
  6. package/sampleWorkspace/localSchema/src/test.js +3 -0
  7. package/sampleWorkspace/rover/apollo.config.js +3 -0
  8. package/sampleWorkspace/rover/src/test.graphql +14 -0
  9. package/sampleWorkspace/rover/src/test.js +30 -0
  10. package/sampleWorkspace/sampleWorkspace.code-workspace +25 -19
  11. package/src/language-server/__tests__/document.test.ts +161 -3
  12. package/src/language-server/__tests__/fixtures/TypeScript.tmLanguage.json +5749 -0
  13. package/src/language-server/__tests__/fixtures/documents/commentWithTemplate.ts +41 -0
  14. package/src/language-server/__tests__/fixtures/documents/commentWithTemplate.ts.snap +185 -0
  15. package/src/language-server/__tests__/fixtures/documents/functionCall.ts +93 -0
  16. package/src/language-server/__tests__/fixtures/documents/functionCall.ts.snap +431 -0
  17. package/src/language-server/__tests__/fixtures/documents/taggedTemplate.ts +80 -0
  18. package/src/language-server/__tests__/fixtures/documents/taggedTemplate.ts.snap +353 -0
  19. package/src/language-server/__tests__/fixtures/documents/templateWithComment.ts +38 -0
  20. package/src/language-server/__tests__/fixtures/documents/templateWithComment.ts.snap +123 -0
  21. package/src/language-server/config/__tests__/loadConfig.ts +43 -10
  22. package/src/language-server/config/config.ts +26 -1
  23. package/src/language-server/config/loadConfig.ts +7 -1
  24. package/src/language-server/config/loadTsConfig.ts +70 -0
  25. package/src/language-server/config/which.d.ts +19 -0
  26. package/src/language-server/document.ts +86 -53
  27. package/src/language-server/fileSet.ts +7 -0
  28. package/src/language-server/project/base.ts +58 -316
  29. package/src/language-server/project/client.ts +730 -7
  30. package/src/language-server/project/internal.ts +349 -0
  31. package/src/language-server/project/rover/DocumentSynchronization.ts +308 -0
  32. package/src/language-server/project/rover/__tests__/DocumentSynchronization.test.ts +302 -0
  33. package/src/language-server/project/rover/project.ts +276 -0
  34. package/src/language-server/server.ts +129 -62
  35. package/src/language-server/utilities/__tests__/source.test.ts +162 -0
  36. package/src/language-server/utilities/source.ts +38 -3
  37. package/src/language-server/workspace.ts +34 -9
  38. package/syntaxes/graphql.js.json +18 -21
  39. package/src/language-server/languageProvider.ts +0 -795
@@ -0,0 +1,349 @@
1
+ import path, { extname } from "path";
2
+ import { lstatSync, readFileSync } from "fs";
3
+ import { URI } from "vscode-uri";
4
+
5
+ import {
6
+ TypeSystemDefinitionNode,
7
+ isTypeSystemDefinitionNode,
8
+ TypeSystemExtensionNode,
9
+ isTypeSystemExtensionNode,
10
+ DefinitionNode,
11
+ GraphQLSchema,
12
+ Kind,
13
+ } from "graphql";
14
+
15
+ import {
16
+ FileChangeType,
17
+ NotificationHandler,
18
+ Position,
19
+ } from "vscode-languageserver/node";
20
+ import { TextDocument } from "vscode-languageserver-textdocument";
21
+
22
+ import { GraphQLDocument, extractGraphQLDocuments } from "../document";
23
+
24
+ import { ClientConfig, isClientConfig, isLocalServiceConfig } from "../config";
25
+ import {
26
+ schemaProviderFromConfig,
27
+ GraphQLSchemaProvider,
28
+ SchemaResolveConfig,
29
+ } from "../providers/schema";
30
+ import { ApolloEngineClient, ClientIdentity } from "../engine";
31
+ import { GraphQLProject, DocumentUri, GraphQLProjectConfig } from "./base";
32
+ import throttle from "lodash.throttle";
33
+
34
+ const fileAssociations: { [extension: string]: string } = {
35
+ ".graphql": "graphql",
36
+ ".gql": "graphql",
37
+ ".js": "javascript",
38
+ ".ts": "typescript",
39
+ ".jsx": "javascriptreact",
40
+ ".tsx": "typescriptreact",
41
+ ".vue": "vue",
42
+ ".svelte": "svelte",
43
+ ".py": "python",
44
+ ".rb": "ruby",
45
+ ".dart": "dart",
46
+ ".re": "reason",
47
+ ".ex": "elixir",
48
+ ".exs": "elixir",
49
+ };
50
+
51
+ export interface GraphQLInternalProjectConfig extends GraphQLProjectConfig {
52
+ config: ClientConfig;
53
+ clientIdentity: ClientIdentity;
54
+ }
55
+ export abstract class GraphQLInternalProject
56
+ extends GraphQLProject
57
+ implements GraphQLSchemaProvider
58
+ {
59
+ public schemaProvider: GraphQLSchemaProvider;
60
+ protected engineClient?: ApolloEngineClient;
61
+
62
+ private needsValidation = false;
63
+
64
+ protected documentsByFile: Map<DocumentUri, GraphQLDocument[]>;
65
+
66
+ constructor({
67
+ config,
68
+ configFolderURI,
69
+ loadingHandler,
70
+ clientIdentity,
71
+ }: GraphQLInternalProjectConfig) {
72
+ super({ config, configFolderURI, loadingHandler });
73
+ const { includes = [], excludes = [] } = config.client;
74
+
75
+ 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
+ ]);
83
+
84
+ this.schemaProvider = schemaProviderFromConfig(config, clientIdentity);
85
+ const { engine } = config;
86
+ if (engine.apiKey) {
87
+ this.engineClient = new ApolloEngineClient(
88
+ engine.apiKey!,
89
+ engine.endpoint,
90
+ clientIdentity,
91
+ );
92
+ }
93
+ }
94
+
95
+ public resolveSchema(config: SchemaResolveConfig): Promise<GraphQLSchema> {
96
+ this.lastLoadDate = +new Date();
97
+ return this.schemaProvider.resolveSchema(config);
98
+ }
99
+
100
+ public resolveFederatedServiceSDL() {
101
+ return this.schemaProvider.resolveFederatedServiceSDL();
102
+ }
103
+
104
+ public onSchemaChange(handler: NotificationHandler<GraphQLSchema>) {
105
+ this.lastLoadDate = +new Date();
106
+ return this.schemaProvider.onSchemaChange(handler);
107
+ }
108
+
109
+ includesFile(uri: DocumentUri) {
110
+ return this.fileSet.includesFile(uri);
111
+ }
112
+
113
+ allIncludedFiles() {
114
+ return this.fileSet.allFiles();
115
+ }
116
+
117
+ async scanAllIncludedFiles() {
118
+ await this.loadingHandler.handle(
119
+ `Loading queries for ${this.displayName}`,
120
+ (async () => {
121
+ for (const filePath of this.allIncludedFiles()) {
122
+ const uri = URI.file(filePath).toString();
123
+
124
+ // If we already have query documents for this file, that means it was either
125
+ // opened or changed before we got a chance to read it.
126
+ if (this.documentsByFile.has(uri)) continue;
127
+
128
+ this.fileDidChange(uri);
129
+ }
130
+ })(),
131
+ );
132
+ }
133
+
134
+ fileDidChange(uri: DocumentUri) {
135
+ const filePath = URI.parse(uri).fsPath;
136
+ const extension = extname(filePath);
137
+ const languageId = fileAssociations[extension];
138
+
139
+ // Don't process files of an unsupported filetype
140
+ if (!languageId) return;
141
+
142
+ // Don't process directories. Directories might be named like files so
143
+ // we have to explicitly check.
144
+ if (!lstatSync(filePath).isFile()) return;
145
+
146
+ const contents = readFileSync(filePath, "utf8");
147
+ const document = TextDocument.create(uri, languageId, -1, contents);
148
+ this.documentDidChange(document);
149
+ }
150
+
151
+ fileWasDeleted(uri: DocumentUri) {
152
+ this.removeGraphQLDocumentsFor(uri);
153
+ this.checkForDuplicateOperations();
154
+ }
155
+
156
+ documentDidChange = (document: TextDocument) => {
157
+ const documents = extractGraphQLDocuments(
158
+ document,
159
+ this.config.client && this.config.client.tagName,
160
+ );
161
+ if (documents) {
162
+ this.documentsByFile.set(document.uri, documents);
163
+ this.invalidate();
164
+ } else {
165
+ this.removeGraphQLDocumentsFor(document.uri);
166
+ }
167
+ this.checkForDuplicateOperations();
168
+ };
169
+
170
+ checkForDuplicateOperations = throttle(
171
+ () => {
172
+ const filePathForOperationName: Record<string, string> = {};
173
+ for (const [
174
+ fileUri,
175
+ documentsForFile,
176
+ ] of this.documentsByFile.entries()) {
177
+ const filePath = URI.parse(fileUri).fsPath;
178
+ for (const document of documentsForFile) {
179
+ if (!document.ast) continue;
180
+ for (const definition of document.ast.definitions) {
181
+ if (
182
+ definition.kind === Kind.OPERATION_DEFINITION &&
183
+ definition.name
184
+ ) {
185
+ const operationName = definition.name.value;
186
+ if (operationName in filePathForOperationName) {
187
+ const conflictingFilePath =
188
+ filePathForOperationName[operationName];
189
+ throw new Error(
190
+ `️️There are multiple definitions for the \`${definition.name.value}\` operation. Please fix all naming conflicts before continuing.\nConflicting definitions found at ${filePath} and ${conflictingFilePath}.`,
191
+ );
192
+ }
193
+ filePathForOperationName[operationName] = filePath;
194
+ }
195
+ }
196
+ }
197
+ }
198
+ },
199
+ 250,
200
+ { leading: true, trailing: true },
201
+ );
202
+
203
+ private removeGraphQLDocumentsFor(uri: DocumentUri) {
204
+ if (this.documentsByFile.has(uri)) {
205
+ this.documentsByFile.delete(uri);
206
+
207
+ if (this._onDiagnostics) {
208
+ this._onDiagnostics({ uri: uri, diagnostics: [] });
209
+ }
210
+
211
+ this.invalidate();
212
+ }
213
+ }
214
+
215
+ protected invalidate() {
216
+ if (!this.needsValidation && this.isReady) {
217
+ setTimeout(() => {
218
+ this.validateIfNeeded();
219
+ }, 0);
220
+ this.needsValidation = true;
221
+ }
222
+ }
223
+
224
+ private validateIfNeeded() {
225
+ if (!this.needsValidation || !this.isReady) return;
226
+
227
+ this.validate();
228
+
229
+ this.needsValidation = false;
230
+ }
231
+
232
+ private getRelativeLocalSchemaFilePaths(): string[] {
233
+ const serviceConfig =
234
+ isClientConfig(this.config) &&
235
+ typeof this.config.client.service === "object" &&
236
+ isLocalServiceConfig(this.config.client.service)
237
+ ? this.config.client.service
238
+ : undefined;
239
+ const localSchemaFile = serviceConfig?.localSchemaFile;
240
+ return (
241
+ localSchemaFile === undefined
242
+ ? []
243
+ : Array.isArray(localSchemaFile)
244
+ ? localSchemaFile
245
+ : [localSchemaFile]
246
+ ).map((filePath) =>
247
+ path.relative(this.rootURI.fsPath, path.join(process.cwd(), filePath)),
248
+ );
249
+ }
250
+
251
+ abstract validate(): void;
252
+
253
+ clearAllDiagnostics() {
254
+ if (!this._onDiagnostics) return;
255
+
256
+ for (const uri of this.documentsByFile.keys()) {
257
+ this._onDiagnostics({ uri, diagnostics: [] });
258
+ }
259
+ }
260
+
261
+ documentsAt(uri: DocumentUri): GraphQLDocument[] | undefined {
262
+ return this.documentsByFile.get(uri);
263
+ }
264
+
265
+ documentAt(
266
+ uri: DocumentUri,
267
+ position: Position,
268
+ ): GraphQLDocument | undefined {
269
+ const queryDocuments = this.documentsByFile.get(uri);
270
+ if (!queryDocuments) return undefined;
271
+
272
+ return queryDocuments.find((document) =>
273
+ document.containsPosition(position),
274
+ );
275
+ }
276
+
277
+ get documents(): GraphQLDocument[] {
278
+ const documents: GraphQLDocument[] = [];
279
+ for (const documentsForFile of this.documentsByFile.values()) {
280
+ documents.push(...documentsForFile);
281
+ }
282
+ return documents;
283
+ }
284
+
285
+ get definitions(): DefinitionNode[] {
286
+ const definitions = [];
287
+
288
+ for (const document of this.documents) {
289
+ if (!document.ast) continue;
290
+
291
+ definitions.push(...document.ast.definitions);
292
+ }
293
+
294
+ return definitions;
295
+ }
296
+
297
+ definitionsAt(uri: DocumentUri): DefinitionNode[] {
298
+ const documents = this.documentsAt(uri);
299
+ if (!documents) return [];
300
+
301
+ const definitions = [];
302
+
303
+ for (const document of documents) {
304
+ if (!document.ast) continue;
305
+
306
+ definitions.push(...document.ast.definitions);
307
+ }
308
+
309
+ return definitions;
310
+ }
311
+
312
+ get typeSystemDefinitionsAndExtensions(): (
313
+ | TypeSystemDefinitionNode
314
+ | TypeSystemExtensionNode
315
+ )[] {
316
+ const definitionsAndExtensions = [];
317
+ for (const document of this.documents) {
318
+ if (!document.ast) continue;
319
+ for (const definition of document.ast.definitions) {
320
+ if (
321
+ isTypeSystemDefinitionNode(definition) ||
322
+ isTypeSystemExtensionNode(definition)
323
+ ) {
324
+ definitionsAndExtensions.push(definition);
325
+ }
326
+ }
327
+ }
328
+ return definitionsAndExtensions;
329
+ }
330
+ onDidOpen: undefined;
331
+ onDidClose: undefined;
332
+ onDidChangeWatchedFiles: GraphQLProject["onDidChangeWatchedFiles"] = (
333
+ params,
334
+ ) => {
335
+ for (const { uri, type } of params.changes) {
336
+ switch (type) {
337
+ case FileChangeType.Created:
338
+ this.fileDidChange(uri);
339
+ break;
340
+ case FileChangeType.Deleted:
341
+ this.fileWasDeleted(uri);
342
+ break;
343
+ }
344
+ }
345
+ };
346
+ onUnhandledRequest: undefined;
347
+ onUnhandledNotification: undefined;
348
+ dispose: undefined;
349
+ }
@@ -0,0 +1,308 @@
1
+ import { extractGraphQLSources } from "../../document";
2
+ import {
3
+ ProtocolNotificationType,
4
+ DidChangeTextDocumentNotification,
5
+ DidOpenTextDocumentNotification,
6
+ DidCloseTextDocumentNotification,
7
+ TextDocumentPositionParams,
8
+ Diagnostic,
9
+ NotificationHandler,
10
+ PublishDiagnosticsParams,
11
+ } from "vscode-languageserver-protocol";
12
+ import { TextDocument } from "vscode-languageserver-textdocument";
13
+ import { DocumentUri, GraphQLProject } from "../base";
14
+ import { generateKeyBetween } from "fractional-indexing";
15
+ import { Source } from "graphql";
16
+ import {
17
+ findContainedSourceAndPosition,
18
+ rangeInContainingDocument,
19
+ } from "../../utilities/source";
20
+ import { URI } from "vscode-uri";
21
+ import { DEBUG } from "./project";
22
+
23
+ export interface FilePart {
24
+ fractionalIndex: string;
25
+ source: Source;
26
+ diagnostics: Diagnostic[];
27
+ }
28
+
29
+ export function handleFilePartUpdates(
30
+ parsed: ReadonlyArray<Source>,
31
+ previousParts: ReadonlyArray<FilePart>,
32
+ ): ReadonlyArray<FilePart> {
33
+ const newParts: FilePart[] = [];
34
+ let newIdx = 0;
35
+ let oldIdx = 0;
36
+ let offsetCorrection = 0;
37
+ while (newIdx < parsed.length || oldIdx < previousParts.length) {
38
+ const source = parsed[newIdx] as Source | undefined;
39
+ const oldPart = previousParts[oldIdx] as FilePart | undefined;
40
+ if (!source) return newParts;
41
+ const newOffset = source.locationOffset.line;
42
+
43
+ if (
44
+ oldPart &&
45
+ (source.body === oldPart.source.body ||
46
+ newOffset === oldPart.source.locationOffset.line + offsetCorrection)
47
+ ) {
48
+ // replacement of chunk
49
+ newParts.push({ ...oldPart, source });
50
+ offsetCorrection =
51
+ source.locationOffset.line - oldPart.source.locationOffset.line;
52
+ newIdx++;
53
+ oldIdx++;
54
+ } else if (
55
+ !oldPart ||
56
+ newOffset < oldPart.source.locationOffset.line + offsetCorrection
57
+ ) {
58
+ // inserted chunk
59
+ const fractionalIndex = generateKeyBetween(
60
+ newParts.length == 0
61
+ ? null
62
+ : newParts[newParts.length - 1].fractionalIndex,
63
+ oldPart ? oldPart.fractionalIndex : null,
64
+ );
65
+ newParts.push({ source, fractionalIndex, diagnostics: [] });
66
+ newIdx++;
67
+ offsetCorrection += source.body.split("\n").length - 1;
68
+ } else {
69
+ // deleted chunk
70
+ oldIdx++;
71
+ }
72
+ }
73
+ return newParts;
74
+ }
75
+
76
+ function getUri(document: TextDocument, part: FilePart) {
77
+ let uri = URI.parse(part.source.name);
78
+ if (document.languageId !== "graphql") {
79
+ uri = uri.with({ fragment: part.fractionalIndex });
80
+ }
81
+
82
+ return uri.toString();
83
+ }
84
+
85
+ function splitUri(fullUri: DocumentUri) {
86
+ const uri = URI.parse(fullUri);
87
+ return {
88
+ uri: uri.with({ fragment: null }).toString(),
89
+ fractionalIndex: uri.fragment || "a0",
90
+ };
91
+ }
92
+
93
+ export class DocumentSynchronization {
94
+ private pendingDocumentChanges = new Map<DocumentUri, TextDocument>();
95
+ private knownFiles = new Map<
96
+ DocumentUri,
97
+ {
98
+ full: TextDocument;
99
+ parts: ReadonlyArray<FilePart>;
100
+ }
101
+ >();
102
+
103
+ constructor(
104
+ private sendNotification: <P, RO>(
105
+ type: ProtocolNotificationType<P, RO>,
106
+ params?: P,
107
+ ) => Promise<void>,
108
+ private sendDiagnostics: NotificationHandler<PublishDiagnosticsParams>,
109
+ ) {}
110
+
111
+ private documentSynchronizationScheduled = false;
112
+ /**
113
+ * Ensures that only one `syncNextDocumentChange` is queued with the connection at a time.
114
+ * As a result, other, more important, changes can be processed with higher priority.
115
+ */
116
+ private scheduleDocumentSync = async () => {
117
+ if (
118
+ this.pendingDocumentChanges.size === 0 ||
119
+ this.documentSynchronizationScheduled
120
+ ) {
121
+ return;
122
+ }
123
+
124
+ this.documentSynchronizationScheduled = true;
125
+ try {
126
+ const next = this.pendingDocumentChanges.values().next();
127
+ if (next.done) return;
128
+ await this.sendDocumentChanges(next.value);
129
+ } finally {
130
+ this.documentSynchronizationScheduled = false;
131
+ setImmediate(this.scheduleDocumentSync);
132
+ }
133
+ };
134
+
135
+ private async sendDocumentChanges(
136
+ document: TextDocument,
137
+ previousParts = this.knownFiles.get(document.uri)?.parts || [],
138
+ ) {
139
+ this.pendingDocumentChanges.delete(document.uri);
140
+
141
+ const previousObj = Object.fromEntries(
142
+ previousParts.map((p) => [p.fractionalIndex, p]),
143
+ );
144
+ const newParts = handleFilePartUpdates(
145
+ extractGraphQLSources(document) || [],
146
+ previousParts,
147
+ );
148
+ const newObj = Object.fromEntries(
149
+ newParts.map((p) => [p.fractionalIndex, p]),
150
+ );
151
+ this.knownFiles.set(document.uri, { full: document, parts: newParts });
152
+
153
+ for (const newPart of newParts) {
154
+ const previousPart = previousObj[newPart.fractionalIndex];
155
+ if (!previousPart) {
156
+ await this.sendNotification(DidOpenTextDocumentNotification.type, {
157
+ textDocument: {
158
+ uri: getUri(document, newPart),
159
+ languageId: "graphql",
160
+ version: document.version,
161
+ text: newPart.source.body,
162
+ },
163
+ });
164
+ } else if (newPart.source.body !== previousPart.source.body) {
165
+ await this.sendNotification(DidChangeTextDocumentNotification.type, {
166
+ textDocument: {
167
+ uri: getUri(document, newPart),
168
+ version: document.version,
169
+ },
170
+ contentChanges: [
171
+ {
172
+ text: newPart.source.body,
173
+ },
174
+ ],
175
+ });
176
+ }
177
+ }
178
+ for (const previousPart of previousParts) {
179
+ if (!newObj[previousPart.fractionalIndex]) {
180
+ await this.sendNotification(DidCloseTextDocumentNotification.type, {
181
+ textDocument: {
182
+ uri: getUri(document, previousPart),
183
+ },
184
+ });
185
+ }
186
+ }
187
+ }
188
+
189
+ async resendAllDocuments() {
190
+ for (const file of this.knownFiles.values()) {
191
+ await this.sendDocumentChanges(file.full, []);
192
+ }
193
+ }
194
+
195
+ onDidOpenTextDocument: NonNullable<GraphQLProject["onDidOpen"]> = async (
196
+ params,
197
+ ) => {
198
+ this.documentDidChange(params.document);
199
+ };
200
+
201
+ onDidCloseTextDocument: NonNullable<GraphQLProject["onDidClose"]> = (
202
+ params,
203
+ ) => {
204
+ const known = this.knownFiles.get(params.document.uri);
205
+ if (!known) {
206
+ return;
207
+ }
208
+ this.knownFiles.delete(params.document.uri);
209
+ return Promise.all(
210
+ known.parts.map((part) =>
211
+ this.sendNotification(DidCloseTextDocumentNotification.type, {
212
+ textDocument: {
213
+ uri: getUri(known.full, part),
214
+ },
215
+ }),
216
+ ),
217
+ );
218
+ };
219
+
220
+ async documentDidChange(document: TextDocument) {
221
+ if (this.pendingDocumentChanges.has(document.uri)) {
222
+ // this will put the document at the end of the queue again
223
+ // in hopes that we can skip a bit of unnecessary work sometimes
224
+ // when many files change around a lot
225
+ // we will always ensure that a document is synchronized via `synchronizedWithDocument`
226
+ // before we do other operations on the document, so this is safe
227
+ this.pendingDocumentChanges.delete(document.uri);
228
+ }
229
+
230
+ this.pendingDocumentChanges.set(document.uri, document);
231
+ this.scheduleDocumentSync();
232
+ }
233
+
234
+ async synchronizedWithDocument(documentUri: DocumentUri): Promise<void> {
235
+ const document = this.pendingDocumentChanges.get(documentUri);
236
+ if (document) {
237
+ await this.sendDocumentChanges(document);
238
+ }
239
+ }
240
+
241
+ async insideVirtualDocument<T>(
242
+ positionParams: TextDocumentPositionParams,
243
+ cb: (virtualPositionParams: TextDocumentPositionParams) => Promise<T>,
244
+ ): Promise<T | undefined> {
245
+ await this.synchronizedWithDocument(positionParams.textDocument.uri);
246
+ const found = this.knownFiles.get(positionParams.textDocument.uri);
247
+ if (!found) {
248
+ return;
249
+ }
250
+ const match = findContainedSourceAndPosition(
251
+ found.parts,
252
+ positionParams.position,
253
+ );
254
+
255
+ if (!match) return;
256
+ return cb({
257
+ textDocument: {
258
+ uri: getUri(found.full, match),
259
+ },
260
+ position: match.position,
261
+ });
262
+ }
263
+
264
+ handlePartDiagnostics(params: PublishDiagnosticsParams) {
265
+ DEBUG && console.log("Received diagnostics", params);
266
+ const uriDetails = splitUri(params.uri);
267
+ if (!uriDetails) {
268
+ return;
269
+ }
270
+ const found = this.knownFiles.get(uriDetails.uri);
271
+ if (!found) {
272
+ return;
273
+ }
274
+ const part = found.parts.find(
275
+ (p) => p.fractionalIndex === uriDetails.fractionalIndex,
276
+ );
277
+ if (!part) {
278
+ return;
279
+ }
280
+ part.diagnostics = params.diagnostics;
281
+
282
+ const fullDocumentParams: PublishDiagnosticsParams = {
283
+ uri: found.full.uri,
284
+ version: found.full.version,
285
+ diagnostics: found.parts.flatMap((p) =>
286
+ p.diagnostics.map((diagnostic) => ({
287
+ ...diagnostic,
288
+ range: rangeInContainingDocument(p.source, diagnostic.range),
289
+ })),
290
+ ),
291
+ };
292
+
293
+ this.sendDiagnostics(fullDocumentParams);
294
+ }
295
+
296
+ get openDocuments() {
297
+ return [...this.knownFiles.values()].map((f) => f.full);
298
+ }
299
+
300
+ clearAllDiagnostics() {
301
+ for (const file of this.knownFiles.values()) {
302
+ for (const part of file.parts) {
303
+ part.diagnostics = [];
304
+ }
305
+ this.sendDiagnostics({ uri: file.full.uri, diagnostics: [] });
306
+ }
307
+ }
308
+ }