vscode-apollo 2.0.1 → 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 (37) hide show
  1. package/.circleci/config.yml +1 -1
  2. package/.vscode/launch.json +4 -1
  3. package/CHANGELOG.md +27 -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 +19 -10
  22. package/src/language-server/config/config.ts +26 -1
  23. package/src/language-server/config/which.d.ts +19 -0
  24. package/src/language-server/document.ts +86 -53
  25. package/src/language-server/fileSet.ts +7 -0
  26. package/src/language-server/project/base.ts +58 -316
  27. package/src/language-server/project/client.ts +730 -7
  28. package/src/language-server/project/internal.ts +349 -0
  29. package/src/language-server/project/rover/DocumentSynchronization.ts +308 -0
  30. package/src/language-server/project/rover/__tests__/DocumentSynchronization.test.ts +302 -0
  31. package/src/language-server/project/rover/project.ts +276 -0
  32. package/src/language-server/server.ts +129 -62
  33. package/src/language-server/utilities/__tests__/source.test.ts +162 -0
  34. package/src/language-server/utilities/source.ts +38 -3
  35. package/src/language-server/workspace.ts +34 -9
  36. package/syntaxes/graphql.js.json +18 -21
  37. package/src/language-server/languageProvider.ts +0 -795
@@ -0,0 +1,302 @@
1
+ import { Source } from "graphql";
2
+ import { extractGraphQLSources } from "../../../document";
3
+ import { handleFilePartUpdates } from "../DocumentSynchronization";
4
+ import { TextDocument } from "vscode-languageserver-textdocument";
5
+
6
+ const initialFile = `
7
+ Test
8
+ gql\`
9
+ query Test1 {
10
+ test
11
+ }
12
+ \`
13
+
14
+ More test
15
+
16
+ gql\`
17
+ query Test2 {
18
+ test
19
+ }
20
+ \`
21
+ `;
22
+
23
+ const editedFile = `
24
+ Test edited
25
+ foo
26
+ bar
27
+ gql\`
28
+ query Test1 {
29
+ test
30
+ }
31
+ \`
32
+
33
+ More test lalala
34
+
35
+ gql\`
36
+ query Test2 {
37
+ test
38
+ }
39
+ \`
40
+ More stuff here
41
+ `;
42
+
43
+ const insertedFile = `
44
+ Test edited
45
+ foo
46
+ bar
47
+ gql\`
48
+ query Test1 {
49
+ test
50
+ }
51
+ \`
52
+ More test lalala
53
+
54
+ gql\`
55
+ query Test3 {
56
+ test
57
+ }
58
+ \`
59
+ More test lalala
60
+
61
+ gql\`
62
+ query Test2 {
63
+ test
64
+ }
65
+ \`
66
+ More stuff here
67
+ `;
68
+
69
+ const pushedFile = `
70
+ Test edited
71
+ foo
72
+ bar
73
+ gql\`
74
+ query Test1 {
75
+ test
76
+ }
77
+ \`
78
+ More test lalala
79
+
80
+ gql\`
81
+ query Test2 {
82
+ test
83
+ }
84
+ \`
85
+ More test lalala
86
+
87
+ gql\`
88
+ query Test3 {
89
+ test
90
+ }
91
+ \`
92
+ More stuff here
93
+ `;
94
+
95
+ const shiftedFile = `
96
+ Test
97
+ More test
98
+
99
+ gql\`
100
+ query Test2 {
101
+ test
102
+ }
103
+ \`
104
+ `;
105
+
106
+ const poppedFile = `
107
+ Test
108
+ gql\`
109
+ query Test1 {
110
+ test
111
+ }
112
+ \`
113
+
114
+ More test
115
+
116
+ `;
117
+
118
+ const query1 = `
119
+ query Test1 {
120
+ test
121
+ }
122
+ `;
123
+ const query2 = `
124
+ query Test2 {
125
+ test
126
+ }
127
+ `;
128
+ const query3 = `
129
+ query Test3 {
130
+ test
131
+ }
132
+ `;
133
+
134
+ describe("handleFilePartUpdates", () => {
135
+ const initialUpdates = handleFilePartUpdates(
136
+ extractGraphQLSources(
137
+ TextDocument.create("uri", "javascript", 1, initialFile),
138
+ )!,
139
+ [],
140
+ );
141
+
142
+ test("newly parsed file", () => {
143
+ expect(initialUpdates).toEqual([
144
+ {
145
+ fractionalIndex: "a0",
146
+ diagnostics: [],
147
+ source: new Source(query1, "uri", {
148
+ column: 5,
149
+ line: 3,
150
+ }),
151
+ },
152
+ {
153
+ fractionalIndex: "a1",
154
+ diagnostics: [],
155
+ source: new Source(query2, "uri", {
156
+ column: 5,
157
+ line: 11,
158
+ }),
159
+ },
160
+ ]);
161
+ });
162
+
163
+ test("edited file", () => {
164
+ expect(
165
+ handleFilePartUpdates(
166
+ extractGraphQLSources(
167
+ TextDocument.create("uri", "javascript", 2, editedFile),
168
+ )!,
169
+ initialUpdates,
170
+ ),
171
+ ).toEqual([
172
+ {
173
+ fractionalIndex: "a0",
174
+ diagnostics: [],
175
+ source: new Source(query1, "uri", {
176
+ column: 5,
177
+ line: 5,
178
+ }),
179
+ },
180
+ {
181
+ fractionalIndex: "a1",
182
+ diagnostics: [],
183
+ source: new Source(query2, "uri", {
184
+ column: 5,
185
+ line: 13,
186
+ }),
187
+ },
188
+ ]);
189
+ });
190
+
191
+ test("inserted file", () => {
192
+ expect(
193
+ handleFilePartUpdates(
194
+ extractGraphQLSources(
195
+ TextDocument.create("uri", "javascript", 2, insertedFile),
196
+ )!,
197
+ initialUpdates,
198
+ ),
199
+ ).toEqual([
200
+ {
201
+ fractionalIndex: "a0",
202
+ diagnostics: [],
203
+ source: new Source(query1, "uri", {
204
+ column: 5,
205
+ line: 5,
206
+ }),
207
+ },
208
+ {
209
+ fractionalIndex: "a0V",
210
+ diagnostics: [],
211
+ source: new Source(query3, "uri", {
212
+ column: 5,
213
+ line: 12,
214
+ }),
215
+ },
216
+ {
217
+ fractionalIndex: "a1",
218
+ diagnostics: [],
219
+ source: new Source(query2, "uri", {
220
+ column: 5,
221
+ line: 19,
222
+ }),
223
+ },
224
+ ]);
225
+ });
226
+
227
+ test("pushed file", () => {
228
+ expect(
229
+ handleFilePartUpdates(
230
+ extractGraphQLSources(
231
+ TextDocument.create("uri", "javascript", 2, pushedFile),
232
+ )!,
233
+ initialUpdates,
234
+ ),
235
+ ).toEqual([
236
+ {
237
+ fractionalIndex: "a0",
238
+ diagnostics: [],
239
+ source: new Source(query1, "uri", {
240
+ column: 5,
241
+ line: 5,
242
+ }),
243
+ },
244
+ {
245
+ fractionalIndex: "a1",
246
+ diagnostics: [],
247
+ source: new Source(query2, "uri", {
248
+ column: 5,
249
+ line: 12,
250
+ }),
251
+ },
252
+ {
253
+ fractionalIndex: "a2",
254
+ diagnostics: [],
255
+ source: new Source(query3, "uri", {
256
+ column: 5,
257
+ line: 19,
258
+ }),
259
+ },
260
+ ]);
261
+ });
262
+
263
+ test("shifted file", () => {
264
+ expect(
265
+ handleFilePartUpdates(
266
+ extractGraphQLSources(
267
+ TextDocument.create("uri", "javascript", 2, shiftedFile),
268
+ )!,
269
+ initialUpdates,
270
+ ),
271
+ ).toEqual([
272
+ {
273
+ fractionalIndex: "a1",
274
+ diagnostics: [],
275
+ source: new Source(query2, "uri", {
276
+ column: 5,
277
+ line: 5,
278
+ }),
279
+ },
280
+ ]);
281
+ });
282
+
283
+ test("popped file", () => {
284
+ expect(
285
+ handleFilePartUpdates(
286
+ extractGraphQLSources(
287
+ TextDocument.create("uri", "javascript", 2, poppedFile),
288
+ )!,
289
+ initialUpdates,
290
+ ),
291
+ ).toEqual([
292
+ {
293
+ fractionalIndex: "a0",
294
+ diagnostics: [],
295
+ source: new Source(query1, "uri", {
296
+ column: 5,
297
+ line: 3,
298
+ }),
299
+ },
300
+ ]);
301
+ });
302
+ });
@@ -0,0 +1,276 @@
1
+ import { ProjectStats } from "src/messages";
2
+ import { DocumentUri, GraphQLProject } from "../base";
3
+ import { TextDocument } from "vscode-languageserver-textdocument";
4
+ import {
5
+ CancellationToken,
6
+ SymbolInformation,
7
+ InitializeRequest,
8
+ StreamMessageReader,
9
+ StreamMessageWriter,
10
+ createProtocolConnection,
11
+ ProtocolConnection,
12
+ ClientCapabilities,
13
+ ProtocolRequestType,
14
+ CompletionRequest,
15
+ ProtocolNotificationType,
16
+ DidChangeWatchedFilesNotification,
17
+ HoverRequest,
18
+ CancellationTokenSource,
19
+ PublishDiagnosticsNotification,
20
+ ConnectionError,
21
+ ConnectionErrors,
22
+ } from "vscode-languageserver/node";
23
+ import cp from "node:child_process";
24
+ import { GraphQLProjectConfig } from "../base";
25
+ import { ApolloConfig, RoverConfig } from "../../config";
26
+ import { DocumentSynchronization } from "./DocumentSynchronization";
27
+ import { AsyncLocalStorage } from "node:async_hooks";
28
+ import internal from "node:stream";
29
+
30
+ export const DEBUG = true;
31
+
32
+ export function isRoverConfig(config: ApolloConfig): config is RoverConfig {
33
+ return config instanceof RoverConfig;
34
+ }
35
+
36
+ export interface RoverProjectConfig extends GraphQLProjectConfig {
37
+ config: RoverConfig;
38
+ capabilities: ClientCapabilities;
39
+ }
40
+
41
+ export class RoverProject extends GraphQLProject {
42
+ config: RoverConfig;
43
+ /**
44
+ * Allows overriding the connection for a certain async code path, so inside of initialization,
45
+ * calls to `this.connection` can immediately resolve without having to wait for initialization
46
+ * to complete (like every other call to `this.connection` would).
47
+ */
48
+ private connectionStorage = new AsyncLocalStorage<ProtocolConnection>();
49
+ private _connection?: Promise<ProtocolConnection>;
50
+ private child:
51
+ | cp.ChildProcessByStdio<internal.Writable, internal.Readable, null>
52
+ | undefined;
53
+ private disposed = false;
54
+ readonly capabilities: ClientCapabilities;
55
+ get displayName(): string {
56
+ return "Rover Project";
57
+ }
58
+ private documents = new DocumentSynchronization(
59
+ this.sendNotification.bind(this),
60
+ (diagnostics) => this._onDiagnostics?.(diagnostics),
61
+ );
62
+
63
+ constructor(options: RoverProjectConfig) {
64
+ super(options);
65
+ this.config = options.config;
66
+ this.capabilities = options.capabilities;
67
+ }
68
+
69
+ initialize() {
70
+ return [this.getConnection().then(() => {})];
71
+ }
72
+
73
+ /**
74
+ * Since Rover projects do not scan all the folders in the workspace on start,
75
+ * we need to restore the information about open documents from the previous session
76
+ * in case or a project recreation (configuration reload).
77
+ */
78
+ restoreFromPreviousProject(previousProject: RoverProject) {
79
+ for (const document of previousProject.documents.openDocuments) {
80
+ this.documents.onDidOpenTextDocument({ document });
81
+ }
82
+ }
83
+
84
+ getConnection(): Promise<ProtocolConnection> {
85
+ const connectionFromStorage = this.connectionStorage.getStore();
86
+ if (connectionFromStorage) {
87
+ return Promise.resolve(connectionFromStorage);
88
+ }
89
+
90
+ if (!this._connection) {
91
+ this._connection = this.initializeConnection();
92
+ }
93
+
94
+ return this._connection;
95
+ }
96
+
97
+ private async sendNotification<P, RO>(
98
+ type: ProtocolNotificationType<P, RO>,
99
+ params?: P,
100
+ ): Promise<void> {
101
+ const connection = await this.getConnection();
102
+ DEBUG &&
103
+ console.log("sending notification %o", {
104
+ type: type.method,
105
+ params,
106
+ });
107
+ try {
108
+ return await connection.sendNotification(type, params);
109
+ } catch (error) {
110
+ if (error instanceof Error) {
111
+ (error as any).cause = { type: type.method, params };
112
+ }
113
+ throw error;
114
+ }
115
+ }
116
+
117
+ private async sendRequest<P, R, PR, E, RO>(
118
+ type: ProtocolRequestType<P, R, PR, E, RO>,
119
+ params: P,
120
+ token?: CancellationToken,
121
+ ): Promise<R> {
122
+ const connection = await this.getConnection();
123
+ DEBUG && console.log("sending request %o", { type: type.method, params });
124
+ try {
125
+ const result = await connection.sendRequest(type, params, token);
126
+ DEBUG && console.log({ result });
127
+ return result;
128
+ } catch (error) {
129
+ if (error instanceof Error) {
130
+ (error as any).cause = { type: type.method, params };
131
+ }
132
+ throw error;
133
+ }
134
+ }
135
+
136
+ async initializeConnection() {
137
+ if (this.child) {
138
+ this.child.kill();
139
+ }
140
+ if (this.disposed) {
141
+ throw new ConnectionError(
142
+ ConnectionErrors.Closed,
143
+ "Connection is closed.",
144
+ );
145
+ }
146
+ const child = cp.spawn(this.config.rover.bin, ["lsp"], {
147
+ env: DEBUG ? { RUST_BACKTRACE: "1" } : {},
148
+ stdio: ["pipe", "pipe", DEBUG ? "inherit" : "ignore"],
149
+ });
150
+ this.child = child;
151
+ const reader = new StreamMessageReader(child.stdout);
152
+ const writer = new StreamMessageWriter(child.stdin);
153
+ const connection = createProtocolConnection(reader, writer);
154
+ connection.onClose(() => {
155
+ DEBUG && console.log("Connection closed");
156
+ child.kill();
157
+ source.cancel();
158
+ this._connection = undefined;
159
+ });
160
+
161
+ connection.onError((err) => {
162
+ console.error({ err });
163
+ });
164
+
165
+ connection.onNotification(
166
+ PublishDiagnosticsNotification.type,
167
+ this.documents.handlePartDiagnostics.bind(this.documents),
168
+ );
169
+
170
+ connection.onUnhandledNotification((notification) => {
171
+ DEBUG && console.info("unhandled notification from LSP", notification);
172
+ });
173
+
174
+ connection.listen();
175
+ DEBUG && console.log("Initializing connection");
176
+
177
+ const source = new CancellationTokenSource();
178
+ try {
179
+ const status = await connection.sendRequest(
180
+ InitializeRequest.type,
181
+ {
182
+ capabilities: this.capabilities,
183
+ processId: process.pid,
184
+ rootUri: this.rootURI.toString(),
185
+ },
186
+ source.token,
187
+ );
188
+ DEBUG && console.log("Connection initialized", status);
189
+
190
+ await this.connectionStorage.run(
191
+ connection,
192
+ this.documents.resendAllDocuments.bind(this.documents),
193
+ );
194
+
195
+ return connection;
196
+ } catch (error) {
197
+ console.error("Connection failed to initialize", error);
198
+ throw error;
199
+ }
200
+ }
201
+
202
+ getProjectStats(): ProjectStats {
203
+ return { type: "Rover", loaded: true };
204
+ }
205
+
206
+ includesFile(uri: DocumentUri) {
207
+ return uri.startsWith(this.rootURI.toString());
208
+ }
209
+
210
+ validate?: () => void;
211
+
212
+ onDidChangeWatchedFiles: GraphQLProject["onDidChangeWatchedFiles"] = (
213
+ params,
214
+ ) => {
215
+ return this.sendNotification(
216
+ DidChangeWatchedFilesNotification.type,
217
+ params,
218
+ );
219
+ };
220
+
221
+ onDidOpen: GraphQLProject["onDidOpen"] = (params) =>
222
+ this.documents.onDidOpenTextDocument(params);
223
+ onDidClose: GraphQLProject["onDidClose"] = (params) =>
224
+ this.documents.onDidCloseTextDocument(params);
225
+
226
+ async documentDidChange(document: TextDocument) {
227
+ return this.documents.documentDidChange(document);
228
+ }
229
+
230
+ clearAllDiagnostics() {
231
+ this.documents.clearAllDiagnostics();
232
+ }
233
+
234
+ dispose() {
235
+ this.disposed = true;
236
+ if (this.child) {
237
+ // this will immediately close the connection without a direct reference
238
+ this.child.stdout.emit("close");
239
+ this.child.kill();
240
+ }
241
+ }
242
+
243
+ onCompletion: GraphQLProject["onCompletion"] = async (params, token) =>
244
+ this.documents.insideVirtualDocument(params, (virtualParams) =>
245
+ this.sendRequest(CompletionRequest.type, virtualParams, token),
246
+ );
247
+
248
+ onHover: GraphQLProject["onHover"] = async (params, token) =>
249
+ this.documents.insideVirtualDocument(params, (virtualParams) =>
250
+ this.sendRequest(HoverRequest.type, virtualParams, token),
251
+ );
252
+
253
+ onUnhandledRequest: GraphQLProject["onUnhandledRequest"] = (type, params) => {
254
+ DEBUG && console.info("unhandled request from VSCode", { type, params });
255
+ };
256
+ onUnhandledNotification: GraphQLProject["onUnhandledNotification"] = (
257
+ _connection,
258
+ type,
259
+ params,
260
+ ) => {
261
+ DEBUG &&
262
+ console.info("unhandled notification from VSCode", { type, params });
263
+ };
264
+
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[]>;
276
+ }