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