vscode-apollo 2.0.1 → 2.2.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 (42) hide show
  1. package/.circleci/config.yml +1 -1
  2. package/.vscode/launch.json +5 -1
  3. package/CHANGELOG.md +41 -0
  4. package/package.json +9 -4
  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 +28 -16
  22. package/src/language-server/config/config.ts +50 -12
  23. package/src/language-server/config/loadConfig.ts +2 -1
  24. package/src/language-server/config/which.d.ts +19 -0
  25. package/src/language-server/document.ts +86 -53
  26. package/src/language-server/fileSet.ts +8 -6
  27. package/src/language-server/project/base.ts +64 -315
  28. package/src/language-server/project/client.ts +731 -21
  29. package/src/language-server/project/internal.ts +354 -0
  30. package/src/language-server/project/rover/DocumentSynchronization.ts +385 -0
  31. package/src/language-server/project/rover/__tests__/DocumentSynchronization.test.ts +302 -0
  32. package/src/language-server/project/rover/project.ts +341 -0
  33. package/src/language-server/server.ts +187 -98
  34. package/src/language-server/utilities/__tests__/source.test.ts +162 -0
  35. package/src/language-server/utilities/languageIdForExtension.ts +39 -0
  36. package/src/language-server/utilities/source.ts +38 -3
  37. package/src/language-server/workspace.ts +61 -12
  38. package/src/languageServerClient.ts +13 -15
  39. package/src/tools/utilities/getLanguageInformation.ts +41 -0
  40. package/src/tools/utilities/languageInformation.ts +41 -0
  41. package/syntaxes/graphql.js.json +18 -21
  42. package/src/language-server/languageProvider.ts +0 -795
@@ -4,13 +4,13 @@ import {
4
4
  ProposedFeatures,
5
5
  TextDocuments,
6
6
  FileChangeType,
7
- ServerCapabilities,
8
7
  TextDocumentSyncKind,
8
+ SymbolInformation,
9
+ FileEvent,
9
10
  } from "vscode-languageserver/node";
10
11
  import { TextDocument } from "vscode-languageserver-textdocument";
11
12
  import type { QuickPickItem } from "vscode";
12
13
  import { GraphQLWorkspace } from "./workspace";
13
- import { GraphQLLanguageProvider } from "./languageProvider";
14
14
  import { LanguageServerLoadingHandler } from "./loadingHandler";
15
15
  import { debounceHandler, Debug } from "./utilities";
16
16
  import { URI } from "vscode-uri";
@@ -20,8 +20,16 @@ import {
20
20
  LanguageServerRequests as Requests,
21
21
  } from "../messages";
22
22
  import { isValidationError } from "zod-validation-error";
23
+ import { GraphQLProject } from "./project/base";
24
+ import type { LanguageIdExtensionMap } from "../tools/utilities/languageInformation";
25
+ import { setLanguageIdExtensionMap } from "./utilities/languageIdForExtension";
26
+
27
+ export type InitializationOptions = {
28
+ languageIdExtensionMap: LanguageIdExtensionMap;
29
+ };
23
30
 
24
31
  const connection = createConnection(ProposedFeatures.all);
32
+ export type VSCodeConnection = typeof connection;
25
33
 
26
34
  Debug.SetConnection(connection);
27
35
  const { sendNotification: originalSendNotification } = connection;
@@ -34,8 +42,8 @@ connection.sendNotification = async (...args: [any, ...any[]]) => {
34
42
  let hasWorkspaceFolderCapability = false;
35
43
 
36
44
  // Awaitable promise for sending messages before the connection is initialized
37
- let initializeConnection: () => void;
38
- const whenConnectionInitialized: Promise<void> = new Promise(
45
+ let initializeConnection: (c: typeof connection) => void;
46
+ const whenConnectionInitialized: Promise<typeof connection> = new Promise(
39
47
  (resolve) => (initializeConnection = resolve),
40
48
  );
41
49
 
@@ -52,6 +60,7 @@ const workspace = new GraphQLWorkspace(
52
60
  require("../../package.json").version,
53
61
  },
54
62
  },
63
+ whenConnectionInitialized,
55
64
  );
56
65
 
57
66
  workspace.onDiagnostics((params) => {
@@ -81,45 +90,52 @@ workspace.onConfigFilesFound(async (params) => {
81
90
  );
82
91
  });
83
92
 
84
- connection.onInitialize(async ({ capabilities, workspaceFolders }) => {
85
- hasWorkspaceFolderCapability = !!(
86
- capabilities.workspace && capabilities.workspace.workspaceFolders
87
- );
93
+ connection.onInitialize(
94
+ async ({ capabilities, workspaceFolders, initializationOptions }) => {
95
+ const { languageIdExtensionMap } =
96
+ initializationOptions as InitializationOptions;
97
+ setLanguageIdExtensionMap(languageIdExtensionMap);
88
98
 
89
- if (workspaceFolders) {
90
- // We wait until all projects are added, because after `initialize` returns we can get additional requests
91
- // like `textDocument/codeLens`, and that way these can await `GraphQLProject#whenReady` to make sure
92
- // we provide them eventually.
93
- await Promise.all(
94
- workspaceFolders.map((folder) => workspace.addProjectsInFolder(folder)),
99
+ hasWorkspaceFolderCapability = !!(
100
+ capabilities.workspace && capabilities.workspace.workspaceFolders
95
101
  );
96
- }
102
+ workspace.capabilities = capabilities;
103
+
104
+ if (workspaceFolders) {
105
+ // We wait until all projects are added, because after `initialize` returns we can get additional requests
106
+ // like `textDocument/codeLens`, and that way these can await `GraphQLProject#whenReady` to make sure
107
+ // we provide them eventually.
108
+ await Promise.all(
109
+ workspaceFolders.map((folder) => workspace.addProjectsInFolder(folder)),
110
+ );
111
+ }
97
112
 
98
- return {
99
- capabilities: {
100
- hoverProvider: true,
101
- completionProvider: {
102
- resolveProvider: false,
103
- triggerCharacters: ["...", "@"],
104
- },
105
- definitionProvider: true,
106
- referencesProvider: true,
107
- documentSymbolProvider: true,
108
- workspaceSymbolProvider: true,
109
- codeLensProvider: {
110
- resolveProvider: false,
113
+ return {
114
+ capabilities: {
115
+ hoverProvider: true,
116
+ completionProvider: {
117
+ resolveProvider: false,
118
+ triggerCharacters: ["...", "@"],
119
+ },
120
+ definitionProvider: true,
121
+ referencesProvider: true,
122
+ documentSymbolProvider: true,
123
+ workspaceSymbolProvider: true,
124
+ codeLensProvider: {
125
+ resolveProvider: false,
126
+ },
127
+ codeActionProvider: true,
128
+ executeCommandProvider: {
129
+ commands: [],
130
+ },
131
+ textDocumentSync: TextDocumentSyncKind.Full,
111
132
  },
112
- codeActionProvider: true,
113
- executeCommandProvider: {
114
- commands: [],
115
- },
116
- textDocumentSync: TextDocumentSyncKind.Full,
117
- } as ServerCapabilities,
118
- };
119
- });
133
+ };
134
+ },
135
+ );
120
136
 
121
137
  connection.onInitialized(async () => {
122
- initializeConnection();
138
+ initializeConnection(connection);
123
139
  if (hasWorkspaceFolderCapability) {
124
140
  connection.workspace.onDidChangeWorkspaceFolders(async (event) => {
125
141
  await Promise.all([
@@ -142,21 +158,37 @@ function isFile(uri: string) {
142
158
  return URI.parse(uri).scheme === "file";
143
159
  }
144
160
 
145
- documents.onDidChangeContent(
146
- debounceHandler((params) => {
147
- const project = workspace.projectForFile(params.document.uri);
148
- if (!project) return;
161
+ documents.onDidChangeContent((params) => {
162
+ const project = workspace.projectForFile(
163
+ params.document.uri,
164
+ params.document.languageId,
165
+ );
166
+ if (!project) return;
149
167
 
150
- // Only watch changes to files
151
- if (!isFile(params.document.uri)) {
152
- return;
153
- }
168
+ // Only watch changes to files
169
+ if (!isFile(params.document.uri)) {
170
+ return;
171
+ }
172
+
173
+ project.documentDidChange(params.document);
174
+ });
175
+
176
+ documents.onDidOpen(
177
+ (params) =>
178
+ workspace
179
+ .projectForFile(params.document.uri, params.document.languageId)
180
+ ?.onDidOpen?.(params),
181
+ );
154
182
 
155
- project.documentDidChange(params.document);
156
- }),
183
+ documents.onDidClose(
184
+ (params) =>
185
+ workspace
186
+ .projectForFile(params.document.uri, params.document.languageId)
187
+ ?.onDidClose?.(params),
157
188
  );
158
189
 
159
190
  connection.onDidChangeWatchedFiles((params) => {
191
+ const handledByProject = new Map<GraphQLProject, FileEvent[]>();
160
192
  for (const { uri, type } of params.changes) {
161
193
  if (
162
194
  uri.endsWith("apollo.config.js") ||
@@ -182,75 +214,82 @@ connection.onDidChangeWatchedFiles((params) => {
182
214
  const project = workspace.projectForFile(uri);
183
215
  if (!project) continue;
184
216
 
185
- switch (type) {
186
- case FileChangeType.Created:
187
- project.fileDidChange(uri);
188
- break;
189
- case FileChangeType.Deleted:
190
- project.fileWasDeleted(uri);
191
- break;
192
- }
217
+ handledByProject.set(project, handledByProject.get(project) || []);
218
+ handledByProject.get(project)!.push({ uri, type });
219
+ }
220
+ for (const [project, changes] of handledByProject) {
221
+ project.onDidChangeWatchedFiles({ changes });
193
222
  }
194
223
  });
195
224
 
196
- const languageProvider = new GraphQLLanguageProvider(workspace);
197
-
198
- connection.onHover((params, token) =>
199
- languageProvider.provideHover(
200
- params.textDocument.uri,
201
- params.position,
202
- token,
203
- ),
225
+ connection.onHover(
226
+ (params, token, workDoneProgress, resultProgress) =>
227
+ workspace
228
+ .projectForFile(params.textDocument.uri)
229
+ ?.onHover?.(params, token, workDoneProgress, resultProgress) ?? null,
204
230
  );
205
231
 
206
- connection.onDefinition((params, token) =>
207
- languageProvider.provideDefinition(
208
- params.textDocument.uri,
209
- params.position,
210
- token,
211
- ),
232
+ connection.onDefinition(
233
+ (params, token, workDoneProgress, resultProgress) =>
234
+ workspace
235
+ .projectForFile(params.textDocument.uri)
236
+ ?.onDefinition?.(params, token, workDoneProgress, resultProgress) ?? null,
212
237
  );
213
238
 
214
- connection.onReferences((params, token) =>
215
- languageProvider.provideReferences(
216
- params.textDocument.uri,
217
- params.position,
218
- params.context,
219
- token,
220
- ),
239
+ connection.onReferences(
240
+ (params, token, workDoneProgress, resultProgress) =>
241
+ workspace
242
+ .projectForFile(params.textDocument.uri)
243
+ ?.onReferences?.(params, token, workDoneProgress, resultProgress) ?? null,
221
244
  );
222
245
 
223
- connection.onDocumentSymbol((params, token) =>
224
- languageProvider.provideDocumentSymbol(params.textDocument.uri, token),
246
+ connection.onDocumentSymbol(
247
+ (params, token, workDoneProgress, resultProgress) =>
248
+ workspace
249
+ .projectForFile(params.textDocument.uri)
250
+ ?.onDocumentSymbol?.(params, token, workDoneProgress, resultProgress) ??
251
+ [],
225
252
  );
226
253
 
227
- connection.onWorkspaceSymbol((params, token) =>
228
- languageProvider.provideWorkspaceSymbol(params.query, token),
229
- );
254
+ connection.onWorkspaceSymbol(async (params, token) => {
255
+ const symbols: SymbolInformation[] = [];
256
+ const symbolPromises = workspace.projects.map(
257
+ (project) =>
258
+ project.provideSymbol?.(params.query, token) || Promise.resolve([]),
259
+ );
260
+ for (const projectSymbols of await Promise.all(symbolPromises)) {
261
+ symbols.push(...projectSymbols);
262
+ }
263
+ return symbols;
264
+ });
230
265
 
231
266
  connection.onCompletion(
232
- debounceHandler((params, token) =>
233
- languageProvider.provideCompletionItems(
234
- params.textDocument.uri,
235
- params.position,
236
- token,
237
- ),
267
+ debounceHandler(
268
+ ((params, token, workDoneProgress, resultProgress) =>
269
+ workspace
270
+ .projectForFile(params.textDocument.uri)
271
+ ?.onCompletion?.(params, token, workDoneProgress, resultProgress) ??
272
+ []) satisfies Parameters<typeof connection.onCompletion>[0],
238
273
  ),
239
274
  );
240
275
 
241
276
  connection.onCodeLens(
242
- debounceHandler((params, token) =>
243
- languageProvider.provideCodeLenses(params.textDocument.uri, token),
277
+ debounceHandler(
278
+ ((params, token, workDoneProgress, resultProgress) =>
279
+ workspace
280
+ .projectForFile(params.textDocument.uri)
281
+ ?.onCodeLens?.(params, token, workDoneProgress, resultProgress) ??
282
+ []) satisfies Parameters<typeof connection.onCodeLens>[0],
244
283
  ),
245
284
  );
246
285
 
247
286
  connection.onCodeAction(
248
- debounceHandler((params, token) =>
249
- languageProvider.provideCodeAction(
250
- params.textDocument.uri,
251
- params.range,
252
- token,
253
- ),
287
+ debounceHandler(
288
+ ((params, token, workDoneProgress, resultProgress) =>
289
+ workspace
290
+ .projectForFile(params.textDocument.uri)
291
+ ?.onCodeAction?.(params, token, workDoneProgress, resultProgress) ??
292
+ []) satisfies Parameters<typeof connection.onCodeAction>[0],
254
293
  ),
255
294
  );
256
295
 
@@ -263,12 +302,62 @@ connection.onNotification(Commands.TagSelected, (selection: QuickPickItem) =>
263
302
  );
264
303
 
265
304
  connection.onNotification(Commands.GetStats, async ({ uri }) => {
266
- const status = await languageProvider.provideStats(uri);
267
- connection.sendNotification(Notifications.StatsLoaded, status);
305
+ const status = await workspace.projectForFile(uri)?.getProjectStats();
306
+ connection.sendNotification(
307
+ Notifications.StatsLoaded,
308
+ status ?? {
309
+ loaded: false,
310
+ },
311
+ );
268
312
  });
269
313
  connection.onRequest(Requests.FileStats, async ({ uri }) => {
270
- return languageProvider.provideStats(uri);
314
+ return workspace.projectForFile(uri)?.getProjectStats() ?? { loaded: false };
315
+ });
316
+
317
+ connection.onRequest((method, params, token) => {
318
+ return getProjectFromUnknownParams(params, workspace)?.onUnhandledRequest?.(
319
+ method,
320
+ params,
321
+ token,
322
+ );
323
+ });
324
+
325
+ connection.onNotification((method, params) => {
326
+ return getProjectFromUnknownParams(
327
+ params,
328
+ workspace,
329
+ )?.onUnhandledNotification?.(connection, method, params);
271
330
  });
272
331
 
273
332
  // Listen on the connection
274
333
  connection.listen();
334
+
335
+ // ------------------------------ utility functions ------------------------------
336
+
337
+ function getProjectFromUnknownParams(
338
+ params: object | any[] | undefined,
339
+ workspace: GraphQLWorkspace,
340
+ ) {
341
+ const param0 = Array.isArray(params) ? params[0] : params;
342
+ if (!param0) return;
343
+ let uri: string | undefined;
344
+ if (typeof param0 === "string" && param0.startsWith("file://")) {
345
+ uri = param0;
346
+ } else if (
347
+ typeof param0 === "object" &&
348
+ "uri" in param0 &&
349
+ param0.uri &&
350
+ typeof param0.uri === "string"
351
+ ) {
352
+ uri = param0.uri;
353
+ } else if (
354
+ typeof param0 === "object" &&
355
+ param0.textDocument &&
356
+ typeof param0.textDocument === "object" &&
357
+ param0.textDocument.uri &&
358
+ typeof param0.textDocument.uri === "string"
359
+ ) {
360
+ uri = param0.textDocument.uri;
361
+ }
362
+ return uri ? workspace.projectForFile(uri) : undefined;
363
+ }
@@ -0,0 +1,162 @@
1
+ import { extractGraphQLSources } from "../../document";
2
+ import { TextDocument } from "vscode-languageserver-textdocument";
3
+ import {
4
+ findContainedSourceAndPosition,
5
+ positionFromPositionInContainingDocument,
6
+ positionInContainingDocument,
7
+ } from "../source";
8
+ import { handleFilePartUpdates } from "../../project/rover/DocumentSynchronization";
9
+
10
+ const testText = `import gql from "graphql-tag";
11
+
12
+ const foo = 1
13
+
14
+ gql\`
15
+ query Test {
16
+ droid(id: "2000") {
17
+ name
18
+ }
19
+
20
+ }\`;
21
+
22
+ const verylonglala = gql\`type Foo { baaaaaar: String }\`
23
+ `;
24
+ describe("positionFromPositionInContainingDocument", () => {
25
+ const sources = extractGraphQLSources(
26
+ TextDocument.create("uri", "javascript", 1, testText),
27
+ "gql",
28
+ )!;
29
+
30
+ test("should return the correct position inside a document", () => {
31
+ expect(
32
+ positionFromPositionInContainingDocument(sources[0], {
33
+ line: 5,
34
+ character: 3,
35
+ }),
36
+ ).toEqual({ line: 1, character: 3 });
37
+ });
38
+
39
+ test("should return the correct position on the first line of a document", () => {
40
+ expect(
41
+ positionFromPositionInContainingDocument(sources[0], {
42
+ line: 4,
43
+ character: 4,
44
+ }),
45
+ ).toEqual({ line: 0, character: 0 });
46
+ });
47
+
48
+ test("should return the correct position on a single line document", () => {
49
+ expect(
50
+ positionFromPositionInContainingDocument(sources[1], {
51
+ line: 12,
52
+ character: 46,
53
+ }),
54
+ ).toEqual({ line: 0, character: 21 });
55
+ });
56
+ });
57
+
58
+ describe("findContainedSourceAndPosition", () => {
59
+ const parts = handleFilePartUpdates(
60
+ extractGraphQLSources(
61
+ TextDocument.create("uri", "javascript", 1, testText),
62
+ "gql",
63
+ )!,
64
+ [],
65
+ );
66
+
67
+ test("should return the correct position inside a document", () => {
68
+ expect(
69
+ findContainedSourceAndPosition(parts, {
70
+ line: 5,
71
+ character: 3,
72
+ }),
73
+ ).toEqual({ ...parts[0], position: { line: 1, character: 3 } });
74
+ });
75
+
76
+ test("should return the correct position on the first line of a document", () => {
77
+ expect(
78
+ findContainedSourceAndPosition(parts, {
79
+ line: 4,
80
+ character: 4,
81
+ }),
82
+ ).toEqual({ ...parts[0], position: { line: 0, character: 0 } });
83
+ });
84
+
85
+ test("should return the correct position on the last line of a document", () => {
86
+ expect(
87
+ findContainedSourceAndPosition(parts, {
88
+ line: 10,
89
+ character: 0,
90
+ }),
91
+ ).toEqual({ ...parts[0], position: { line: 6, character: 0 } });
92
+ });
93
+
94
+ test("should return null if the position is outside of the document", () => {
95
+ expect(
96
+ findContainedSourceAndPosition(parts, {
97
+ line: 4,
98
+ character: 3,
99
+ }),
100
+ ).toBeNull();
101
+ expect(
102
+ findContainedSourceAndPosition(parts, {
103
+ line: 10,
104
+ character: 1,
105
+ }),
106
+ ).toBeNull();
107
+ });
108
+
109
+ test("should return the correct position on a single line document", () => {
110
+ expect(
111
+ findContainedSourceAndPosition(parts, {
112
+ line: 12,
113
+ character: 46,
114
+ }),
115
+ ).toEqual({ ...parts[1], position: { line: 0, character: 21 } });
116
+ });
117
+ });
118
+ describe("positionInContainingDocument", () => {
119
+ const parts = handleFilePartUpdates(
120
+ extractGraphQLSources(
121
+ TextDocument.create("uri", "javascript", 1, testText),
122
+ "gql",
123
+ )!,
124
+ [],
125
+ );
126
+
127
+ test("should return the correct position inside a document", () => {
128
+ expect(
129
+ positionInContainingDocument(parts[0].source, {
130
+ line: 1,
131
+ character: 3,
132
+ }),
133
+ ).toEqual({ line: 5, character: 3 });
134
+ });
135
+
136
+ test("should return the correct position on the first line of a document", () => {
137
+ expect(
138
+ positionInContainingDocument(parts[0].source, {
139
+ line: 0,
140
+ character: 0,
141
+ }),
142
+ ).toEqual({ line: 4, character: 4 });
143
+ });
144
+
145
+ test("should return the correct position on the last line of a document", () => {
146
+ expect(
147
+ positionInContainingDocument(parts[0].source, {
148
+ line: 6,
149
+ character: 0,
150
+ }),
151
+ ).toEqual({ line: 10, character: 0 });
152
+ });
153
+
154
+ test("should return the correct position on a single line document", () => {
155
+ expect(
156
+ positionInContainingDocument(parts[1].source, {
157
+ line: 0,
158
+ character: 21,
159
+ }),
160
+ ).toEqual({ line: 12, character: 46 });
161
+ });
162
+ });
@@ -0,0 +1,39 @@
1
+ import {
2
+ FileExtension,
3
+ LanguageIdExtensionMap,
4
+ supportedLanguageIds,
5
+ } from "../../tools/utilities/languageInformation";
6
+
7
+ let languageIdPerExtension: Record<FileExtension, string> | undefined;
8
+ let supportedExtensions: FileExtension[] | undefined;
9
+
10
+ export function setLanguageIdExtensionMap(map: LanguageIdExtensionMap) {
11
+ languageIdPerExtension = Object.fromEntries(
12
+ Object.entries(map).flatMap(([languageId, extensions]) =>
13
+ extensions.map((extension) => [extension, languageId]),
14
+ ),
15
+ );
16
+ supportedExtensions = supportedLanguageIds.flatMap(
17
+ (languageId) => map[languageId],
18
+ );
19
+ }
20
+
21
+ /**
22
+ * @throws if called before the language server has received options via `onInitialize`.
23
+ */
24
+ export function getLanguageIdForExtension(ext: FileExtension) {
25
+ if (!languageIdPerExtension) {
26
+ throw new Error("LanguageIdExtensionMap not set");
27
+ }
28
+ return languageIdPerExtension[ext];
29
+ }
30
+
31
+ /**
32
+ * @throws if called before the language server has received options via `onInitialize`.
33
+ */
34
+ export function getSupportedExtensions() {
35
+ if (!supportedExtensions) {
36
+ throw new Error("LanguageIdExtensionMap not set");
37
+ }
38
+ return supportedExtensions;
39
+ }
@@ -65,14 +65,48 @@ export function visitWithTypeInfo(
65
65
  };
66
66
  }
67
67
 
68
+ export function findContainedSourceAndPosition<T extends { source: Source }>(
69
+ parts: ReadonlyArray<T>,
70
+ absolutePosition: Position,
71
+ ) {
72
+ for (const part of parts) {
73
+ const lines = part.source.body.split("\n");
74
+ const position = positionFromPositionInContainingDocument(
75
+ part.source,
76
+ absolutePosition,
77
+ );
78
+
79
+ // we are in a sub-document that's beyond the position we're looking for
80
+ // exit early to save on computing time
81
+ if (position.line < 0) return null;
82
+
83
+ if (
84
+ position.line >= 0 &&
85
+ position.line < lines.length &&
86
+ position.character >= 0 &&
87
+ (position.line < lines.length - 1 ||
88
+ position.character < lines[position.line].length)
89
+ ) {
90
+ return {
91
+ ...part,
92
+ position,
93
+ };
94
+ }
95
+ }
96
+ return null;
97
+ }
98
+
68
99
  export function positionFromPositionInContainingDocument(
69
100
  source: Source,
70
101
  position: Position,
71
102
  ) {
72
103
  if (!source.locationOffset) return position;
104
+ const line = position.line - (source.locationOffset.line - 1);
73
105
  return Position.create(
74
- position.line - (source.locationOffset.line - 1),
75
- position.character,
106
+ line,
107
+ line === 0
108
+ ? position.character - (source.locationOffset.column - 1)
109
+ : position.character,
76
110
  );
77
111
  }
78
112
 
@@ -83,7 +117,8 @@ export function positionInContainingDocument(
83
117
  if (!source.locationOffset) return position;
84
118
  return Position.create(
85
119
  source.locationOffset.line - 1 + position.line,
86
- position.character,
120
+ (position.line === 0 ? source.locationOffset.column - 1 : 0) +
121
+ position.character,
87
122
  );
88
123
  }
89
124