vscode-apollo 2.1.0 → 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/.vscode/launch.json +1 -0
- package/CHANGELOG.md +14 -0
- package/package.json +1 -2
- package/src/language-server/config/__tests__/loadConfig.ts +9 -6
- package/src/language-server/config/config.ts +24 -11
- package/src/language-server/config/loadConfig.ts +2 -1
- package/src/language-server/fileSet.ts +8 -13
- package/src/language-server/project/base.ts +24 -17
- package/src/language-server/project/client.ts +1 -14
- package/src/language-server/project/internal.ts +18 -13
- package/src/language-server/project/rover/DocumentSynchronization.ts +77 -0
- package/src/language-server/project/rover/project.ts +84 -19
- package/src/language-server/server.ts +70 -48
- package/src/language-server/utilities/languageIdForExtension.ts +39 -0
- package/src/language-server/workspace.ts +27 -3
- package/src/languageServerClient.ts +13 -15
- package/src/tools/utilities/getLanguageInformation.ts +41 -0
- package/src/tools/utilities/languageInformation.ts +41 -0
package/.vscode/launch.json
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
## 2.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#184](https://github.com/apollographql/vscode-graphql/pull/184) [`9c53a11e`](https://github.com/apollographql/vscode-graphql/commit/9c53a11e3006dd69675af976ef3857212d8f9f43) Thanks [@phryneas](https://github.com/phryneas)! - Derive extensions for supported languages and monitored files from other installed extensions.
|
|
8
|
+
Adjust default `includes` for client projects.
|
|
9
|
+
|
|
10
|
+
This changes the default `includes` similar to (depending on additional extensions you might have installed):
|
|
11
|
+
|
|
12
|
+
```diff
|
|
13
|
+
-'src/**/*.{ts,tsx,js,jsx,graphql,gql}',
|
|
14
|
+
+'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}'
|
|
15
|
+
```
|
|
16
|
+
|
|
3
17
|
## 2.1.0
|
|
4
18
|
|
|
5
19
|
### Minor Changes
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "vscode-apollo",
|
|
3
3
|
"displayName": "Apollo GraphQL",
|
|
4
4
|
"description": "Rich editor support for GraphQL client and server development that seamlessly integrates with the Apollo platform",
|
|
5
|
-
"version": "2.
|
|
5
|
+
"version": "2.2.0",
|
|
6
6
|
"referenceID": "87197759-7617-40d0-b32e-46d378e907c7",
|
|
7
7
|
"author": "Apollo GraphQL <opensource@apollographql.com>",
|
|
8
8
|
"license": "MIT",
|
|
@@ -40,7 +40,6 @@
|
|
|
40
40
|
"@apollo/client": "3.11.4",
|
|
41
41
|
"@apollo/subgraph": "2.8.4",
|
|
42
42
|
"@graphql-tools/schema": "10.0.5",
|
|
43
|
-
"@wry/context": "0.7.4",
|
|
44
43
|
"@wry/equality": "0.5.7",
|
|
45
44
|
"cosmiconfig": "9.0.0",
|
|
46
45
|
"dotenv": "16.4.5",
|
|
@@ -99,9 +99,6 @@ Object {
|
|
|
99
99
|
"**/node_modules",
|
|
100
100
|
"**/__tests__",
|
|
101
101
|
],
|
|
102
|
-
"includes": Array [
|
|
103
|
-
"src/**/*.{ts,tsx,js,jsx,graphql,gql}",
|
|
104
|
-
],
|
|
105
102
|
"service": "hello",
|
|
106
103
|
"tagName": "gql",
|
|
107
104
|
},
|
|
@@ -137,6 +134,7 @@ Object {
|
|
|
137
134
|
},
|
|
138
135
|
"rover": Object {
|
|
139
136
|
"bin": "${dir}/bin/rover",
|
|
137
|
+
"extraArgs": Array [],
|
|
140
138
|
},
|
|
141
139
|
}
|
|
142
140
|
`);
|
|
@@ -395,9 +393,14 @@ Object {
|
|
|
395
393
|
configPath: dirPath,
|
|
396
394
|
});
|
|
397
395
|
|
|
398
|
-
expect((config?.rawConfig as any).client.includes).toEqual(
|
|
399
|
-
|
|
400
|
-
|
|
396
|
+
expect((config?.rawConfig as any).client.includes).toEqual(
|
|
397
|
+
/**
|
|
398
|
+
* This will be calculated in the `GraphQLInternalProject` constructor by calling `getSupportedExtensions()`
|
|
399
|
+
* which will have information about all the extensions added by other VSCode extensions for the language ids
|
|
400
|
+
* that Apollo supports.
|
|
401
|
+
*/
|
|
402
|
+
undefined,
|
|
403
|
+
);
|
|
401
404
|
});
|
|
402
405
|
|
|
403
406
|
it("merges engine config defaults", async () => {
|
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import { dirname } from "path";
|
|
1
|
+
import { dirname, join } from "path";
|
|
2
2
|
import { URI } from "vscode-uri";
|
|
3
3
|
import { getGraphIdFromConfig, parseServiceSpecifier } from "./utils";
|
|
4
4
|
import { Debug } from "../utilities";
|
|
5
5
|
import z, { ZodError } from "zod";
|
|
6
6
|
import { ValidationRule } from "graphql/validation/ValidationContext";
|
|
7
|
-
import { Slot } from "@wry/context";
|
|
8
7
|
import { fromZodError } from "zod-validation-error";
|
|
9
8
|
import which from "which";
|
|
10
9
|
import { accessSync, constants as fsConstants, statSync } from "node:fs";
|
|
10
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
11
|
+
import { existsSync } from "fs";
|
|
11
12
|
|
|
12
13
|
const ROVER_AVAILABLE = (process.env.APOLLO_FEATURE_FLAGS || "")
|
|
13
14
|
.split(",")
|
|
@@ -29,8 +30,9 @@ function ignoredFieldWarning(
|
|
|
29
30
|
export interface Context {
|
|
30
31
|
apiKey?: string;
|
|
31
32
|
serviceName?: string;
|
|
33
|
+
configPath?: string;
|
|
32
34
|
}
|
|
33
|
-
const
|
|
35
|
+
const contextStore = new AsyncLocalStorage<Context>();
|
|
34
36
|
|
|
35
37
|
const studioServiceConfig = z.string();
|
|
36
38
|
|
|
@@ -49,7 +51,7 @@ const localServiceConfig = z.object({
|
|
|
49
51
|
export type LocalServiceConfig = z.infer<typeof localServiceConfig>;
|
|
50
52
|
|
|
51
53
|
const clientServiceConfig = z.preprocess(
|
|
52
|
-
(value) => value ||
|
|
54
|
+
(value) => value || contextStore.getStore()?.serviceName,
|
|
53
55
|
z.union([studioServiceConfig, remoteServiceConfig, localServiceConfig]),
|
|
54
56
|
);
|
|
55
57
|
export type ClientServiceConfig = z.infer<typeof clientServiceConfig>;
|
|
@@ -63,9 +65,7 @@ const clientConfig = z.object({
|
|
|
63
65
|
])
|
|
64
66
|
.optional(),
|
|
65
67
|
// maybe shared with rover?
|
|
66
|
-
includes: z
|
|
67
|
-
.array(z.string())
|
|
68
|
-
.default(["src/**/*.{ts,tsx,js,jsx,graphql,gql}"]),
|
|
68
|
+
includes: z.array(z.string()).optional(),
|
|
69
69
|
// maybe shared with rover?
|
|
70
70
|
excludes: z.array(z.string()).default(["**/node_modules", "**/__tests__"]),
|
|
71
71
|
// maybe shared with rover?
|
|
@@ -106,6 +106,21 @@ const roverConfig = z.object({
|
|
|
106
106
|
},
|
|
107
107
|
),
|
|
108
108
|
profile: z.string().optional(),
|
|
109
|
+
supergraphConfig: z
|
|
110
|
+
.preprocess((value) => {
|
|
111
|
+
if (value !== undefined) return value;
|
|
112
|
+
const configPath = contextStore.getStore()?.configPath!;
|
|
113
|
+
const supergraphConfig = join(configPath, "supergraph.yml");
|
|
114
|
+
return existsSync(supergraphConfig) ? supergraphConfig : undefined;
|
|
115
|
+
}, z.string().nullable().optional())
|
|
116
|
+
.describe(
|
|
117
|
+
"The path to your `supergraph.yml` file. \n" +
|
|
118
|
+
"Defaults to a `supergraph.yml` in the folder of your `apollo.config.js`, if there is one.",
|
|
119
|
+
),
|
|
120
|
+
extraArgs: z
|
|
121
|
+
.array(z.string())
|
|
122
|
+
.default([])
|
|
123
|
+
.describe("Extra arguments to pass to the Rover CLI."),
|
|
109
124
|
});
|
|
110
125
|
type RoverConfigFormat = z.infer<typeof roverConfig>;
|
|
111
126
|
|
|
@@ -117,7 +132,7 @@ const engineConfig = z.object({
|
|
|
117
132
|
"https://graphql.api.apollographql.com/api/graphql",
|
|
118
133
|
),
|
|
119
134
|
apiKey: z.preprocess(
|
|
120
|
-
(val) => val ||
|
|
135
|
+
(val) => val || contextStore.getStore()?.apiKey,
|
|
121
136
|
z.string().optional(),
|
|
122
137
|
),
|
|
123
138
|
});
|
|
@@ -202,9 +217,7 @@ export function parseApolloConfig(
|
|
|
202
217
|
configURI?: URI,
|
|
203
218
|
ctx: Context = {},
|
|
204
219
|
) {
|
|
205
|
-
const parsed =
|
|
206
|
-
configSchema.safeParse(rawConfig),
|
|
207
|
-
);
|
|
220
|
+
const parsed = contextStore.run(ctx, () => configSchema.safeParse(rawConfig));
|
|
208
221
|
if (!parsed.success) {
|
|
209
222
|
// Remove "or Required at rover" errors when a client config is provided
|
|
210
223
|
// Remove "or Required at client" errors when a rover config is provided
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { cosmiconfig, defaultLoaders } from "cosmiconfig";
|
|
2
|
-
import { resolve } from "path";
|
|
2
|
+
import { dirname, resolve } from "path";
|
|
3
3
|
import { readFileSync, existsSync, lstatSync } from "fs";
|
|
4
4
|
import {
|
|
5
5
|
ApolloConfig,
|
|
@@ -103,5 +103,6 @@ export async function loadConfig({
|
|
|
103
103
|
return parseApolloConfig(config, URI.file(resolve(filepath)), {
|
|
104
104
|
apiKey,
|
|
105
105
|
serviceName: nameFromKey,
|
|
106
|
+
configPath: dirname(filepath),
|
|
106
107
|
});
|
|
107
108
|
}
|
|
@@ -14,12 +14,10 @@ export class FileSet {
|
|
|
14
14
|
rootURI,
|
|
15
15
|
includes,
|
|
16
16
|
excludes,
|
|
17
|
-
configURI,
|
|
18
17
|
}: {
|
|
19
18
|
rootURI: URI;
|
|
20
19
|
includes: string[];
|
|
21
20
|
excludes: string[];
|
|
22
|
-
configURI?: URI;
|
|
23
21
|
}) {
|
|
24
22
|
invariant(rootURI, `Must provide "rootURI".`);
|
|
25
23
|
invariant(includes, `Must provide "includes".`);
|
|
@@ -30,13 +28,6 @@ export class FileSet {
|
|
|
30
28
|
this.excludes = excludes;
|
|
31
29
|
}
|
|
32
30
|
|
|
33
|
-
pushIncludes(files: string[]) {
|
|
34
|
-
this.includes.push(...files);
|
|
35
|
-
}
|
|
36
|
-
pushExcludes(files: string[]) {
|
|
37
|
-
this.excludes.push(...files);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
31
|
includesFile(filePath: string): boolean {
|
|
41
32
|
const normalizedFilePath = normalizeURI(filePath);
|
|
42
33
|
|
|
@@ -57,10 +48,14 @@ export class FileSet {
|
|
|
57
48
|
}
|
|
58
49
|
|
|
59
50
|
allFiles(): string[] {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
51
|
+
const joinedIncludes =
|
|
52
|
+
this.includes.length == 1
|
|
53
|
+
? this.includes[0]
|
|
54
|
+
: // since glob.sync takes a single pattern, but we allow an array of `includes`, we can join all the
|
|
55
|
+
// `includes` globs into a single pattern and pass to glob.sync. The `ignore` option does, however, allow
|
|
56
|
+
// an array of globs to ignore, so we can pass it in directly
|
|
57
|
+
`{${this.includes.join(",")}}`;
|
|
58
|
+
|
|
64
59
|
return globSync(joinedIncludes, {
|
|
65
60
|
cwd: this.rootURI.fsPath,
|
|
66
61
|
absolute: true,
|
|
@@ -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.
|
|
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
|
-
|
|
124
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
137
|
-
|
|
140
|
+
onUnhandledRequest?: StarRequestHandler;
|
|
141
|
+
onUnhandledNotification?: (
|
|
138
142
|
connection: Connection,
|
|
139
143
|
...rest: Parameters<StarNotificationHandler>
|
|
140
144
|
) => ReturnType<StarNotificationHandler>;
|
|
141
145
|
|
|
142
|
-
|
|
146
|
+
dispose?(): void;
|
|
143
147
|
|
|
144
|
-
|
|
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 {
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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";
|
|
@@ -105,6 +110,11 @@ export class DocumentSynchronization {
|
|
|
105
110
|
type: ProtocolNotificationType<P, RO>,
|
|
106
111
|
params?: P,
|
|
107
112
|
) => Promise<void>,
|
|
113
|
+
private sendRequest: <P, R, PR, E, RO>(
|
|
114
|
+
type: ProtocolRequestType<P, R, PR, E, RO>,
|
|
115
|
+
params: P,
|
|
116
|
+
token?: CancellationToken,
|
|
117
|
+
) => Promise<R>,
|
|
108
118
|
private sendDiagnostics: NotificationHandler<PublishDiagnosticsParams>,
|
|
109
119
|
) {}
|
|
110
120
|
|
|
@@ -305,4 +315,71 @@ export class DocumentSynchronization {
|
|
|
305
315
|
this.sendDiagnostics({ uri: file.full.uri, diagnostics: [] });
|
|
306
316
|
}
|
|
307
317
|
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Receives semantic tokens for all sub-documents and glues them together.
|
|
321
|
+
* See https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_semanticTokens
|
|
322
|
+
* TLDR: The tokens are a flat array of numbers, where each token is represented by 5 numbers.
|
|
323
|
+
* The first two numbers represent the token's delta line and delta start character and might need adjusing
|
|
324
|
+
* relative to the start of a sub-document in relation to the position of the last token of the previous sub-document.
|
|
325
|
+
*
|
|
326
|
+
* There is also an "incremental" version of this request, but we don't support it yet.
|
|
327
|
+
* This is complicated enough as it is.
|
|
328
|
+
*/
|
|
329
|
+
async getFullSemanticTokens(
|
|
330
|
+
params: SemanticTokensParams,
|
|
331
|
+
cancellationToken: CancellationToken,
|
|
332
|
+
): Promise<SemanticTokens | null> {
|
|
333
|
+
await this.synchronizedWithDocument(params.textDocument.uri);
|
|
334
|
+
const found = this.knownFiles.get(params.textDocument.uri);
|
|
335
|
+
if (!found) {
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
const allParts = await Promise.all(
|
|
339
|
+
found.parts.map(async (part) => {
|
|
340
|
+
return {
|
|
341
|
+
part,
|
|
342
|
+
tokens: await this.sendRequest(
|
|
343
|
+
SemanticTokensRequest.type,
|
|
344
|
+
{
|
|
345
|
+
textDocument: { uri: getUri(found.full, part) },
|
|
346
|
+
},
|
|
347
|
+
cancellationToken,
|
|
348
|
+
),
|
|
349
|
+
};
|
|
350
|
+
}),
|
|
351
|
+
);
|
|
352
|
+
let line = 0,
|
|
353
|
+
char = 0,
|
|
354
|
+
lastLine = 0,
|
|
355
|
+
lastChar = 0;
|
|
356
|
+
const combinedTokens = [];
|
|
357
|
+
for (const { part, tokens } of allParts) {
|
|
358
|
+
if (!tokens) {
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
line = part.source.locationOffset.line - 1;
|
|
362
|
+
char = part.source.locationOffset.column - 1;
|
|
363
|
+
for (let i = 0; i < tokens.data.length; i += 5) {
|
|
364
|
+
const deltaLine = tokens.data[i],
|
|
365
|
+
deltaStartChar = tokens.data[i + 1];
|
|
366
|
+
|
|
367
|
+
// We need to run this loop fully to correctly calculate the `lastLine` and `lastChar`
|
|
368
|
+
// so for the next incoming tokens, we can adjust the delta correctly.
|
|
369
|
+
line = line + deltaLine;
|
|
370
|
+
char = deltaLine === 0 ? char + deltaStartChar : deltaStartChar;
|
|
371
|
+
// we just need to adjust the deltas only for the first token
|
|
372
|
+
if (i === 0) {
|
|
373
|
+
tokens.data[0] = line - lastLine;
|
|
374
|
+
tokens.data[1] = line === lastLine ? lastChar - char : char;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
combinedTokens.push(...tokens.data);
|
|
378
|
+
lastLine = line;
|
|
379
|
+
lastChar = char;
|
|
380
|
+
}
|
|
381
|
+
return {
|
|
382
|
+
data: combinedTokens,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
308
385
|
}
|
|
@@ -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
|
|
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(
|
|
207
|
-
|
|
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"] = (
|
|
254
|
-
|
|
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
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
}
|
|
@@ -4,7 +4,6 @@ import {
|
|
|
4
4
|
ProposedFeatures,
|
|
5
5
|
TextDocuments,
|
|
6
6
|
FileChangeType,
|
|
7
|
-
ServerCapabilities,
|
|
8
7
|
TextDocumentSyncKind,
|
|
9
8
|
SymbolInformation,
|
|
10
9
|
FileEvent,
|
|
@@ -21,10 +20,16 @@ import {
|
|
|
21
20
|
LanguageServerRequests as Requests,
|
|
22
21
|
} from "../messages";
|
|
23
22
|
import { isValidationError } from "zod-validation-error";
|
|
24
|
-
import { Trie } from "@wry/trie";
|
|
25
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
|
+
};
|
|
26
30
|
|
|
27
31
|
const connection = createConnection(ProposedFeatures.all);
|
|
32
|
+
export type VSCodeConnection = typeof connection;
|
|
28
33
|
|
|
29
34
|
Debug.SetConnection(connection);
|
|
30
35
|
const { sendNotification: originalSendNotification } = connection;
|
|
@@ -37,8 +42,8 @@ connection.sendNotification = async (...args: [any, ...any[]]) => {
|
|
|
37
42
|
let hasWorkspaceFolderCapability = false;
|
|
38
43
|
|
|
39
44
|
// Awaitable promise for sending messages before the connection is initialized
|
|
40
|
-
let initializeConnection: () => void;
|
|
41
|
-
const whenConnectionInitialized: Promise<
|
|
45
|
+
let initializeConnection: (c: typeof connection) => void;
|
|
46
|
+
const whenConnectionInitialized: Promise<typeof connection> = new Promise(
|
|
42
47
|
(resolve) => (initializeConnection = resolve),
|
|
43
48
|
);
|
|
44
49
|
|
|
@@ -55,6 +60,7 @@ const workspace = new GraphQLWorkspace(
|
|
|
55
60
|
require("../../package.json").version,
|
|
56
61
|
},
|
|
57
62
|
},
|
|
63
|
+
whenConnectionInitialized,
|
|
58
64
|
);
|
|
59
65
|
|
|
60
66
|
workspace.onDiagnostics((params) => {
|
|
@@ -84,46 +90,52 @@ workspace.onConfigFilesFound(async (params) => {
|
|
|
84
90
|
);
|
|
85
91
|
});
|
|
86
92
|
|
|
87
|
-
connection.onInitialize(
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
// like `textDocument/codeLens`, and that way these can await `GraphQLProject#whenReady` to make sure
|
|
96
|
-
// we provide them eventually.
|
|
97
|
-
await Promise.all(
|
|
98
|
-
workspaceFolders.map((folder) => workspace.addProjectsInFolder(folder)),
|
|
93
|
+
connection.onInitialize(
|
|
94
|
+
async ({ capabilities, workspaceFolders, initializationOptions }) => {
|
|
95
|
+
const { languageIdExtensionMap } =
|
|
96
|
+
initializationOptions as InitializationOptions;
|
|
97
|
+
setLanguageIdExtensionMap(languageIdExtensionMap);
|
|
98
|
+
|
|
99
|
+
hasWorkspaceFolderCapability = !!(
|
|
100
|
+
capabilities.workspace && capabilities.workspace.workspaceFolders
|
|
99
101
|
);
|
|
100
|
-
|
|
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
|
+
}
|
|
101
112
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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,
|
|
115
132
|
},
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
},
|
|
120
|
-
textDocumentSync: TextDocumentSyncKind.Full,
|
|
121
|
-
} as ServerCapabilities,
|
|
122
|
-
};
|
|
123
|
-
});
|
|
133
|
+
};
|
|
134
|
+
},
|
|
135
|
+
);
|
|
124
136
|
|
|
125
137
|
connection.onInitialized(async () => {
|
|
126
|
-
initializeConnection();
|
|
138
|
+
initializeConnection(connection);
|
|
127
139
|
if (hasWorkspaceFolderCapability) {
|
|
128
140
|
connection.workspace.onDidChangeWorkspaceFolders(async (event) => {
|
|
129
141
|
await Promise.all([
|
|
@@ -147,7 +159,10 @@ function isFile(uri: string) {
|
|
|
147
159
|
}
|
|
148
160
|
|
|
149
161
|
documents.onDidChangeContent((params) => {
|
|
150
|
-
const project = workspace.projectForFile(
|
|
162
|
+
const project = workspace.projectForFile(
|
|
163
|
+
params.document.uri,
|
|
164
|
+
params.document.languageId,
|
|
165
|
+
);
|
|
151
166
|
if (!project) return;
|
|
152
167
|
|
|
153
168
|
// Only watch changes to files
|
|
@@ -160,12 +175,16 @@ documents.onDidChangeContent((params) => {
|
|
|
160
175
|
|
|
161
176
|
documents.onDidOpen(
|
|
162
177
|
(params) =>
|
|
163
|
-
workspace
|
|
178
|
+
workspace
|
|
179
|
+
.projectForFile(params.document.uri, params.document.languageId)
|
|
180
|
+
?.onDidOpen?.(params),
|
|
164
181
|
);
|
|
165
182
|
|
|
166
183
|
documents.onDidClose(
|
|
167
184
|
(params) =>
|
|
168
|
-
workspace
|
|
185
|
+
workspace
|
|
186
|
+
.projectForFile(params.document.uri, params.document.languageId)
|
|
187
|
+
?.onDidClose?.(params),
|
|
169
188
|
);
|
|
170
189
|
|
|
171
190
|
connection.onDidChangeWatchedFiles((params) => {
|
|
@@ -246,28 +265,31 @@ connection.onWorkspaceSymbol(async (params, token) => {
|
|
|
246
265
|
|
|
247
266
|
connection.onCompletion(
|
|
248
267
|
debounceHandler(
|
|
249
|
-
(params, token, workDoneProgress, resultProgress) =>
|
|
268
|
+
((params, token, workDoneProgress, resultProgress) =>
|
|
250
269
|
workspace
|
|
251
270
|
.projectForFile(params.textDocument.uri)
|
|
252
|
-
?.onCompletion?.(params, token, workDoneProgress, resultProgress) ??
|
|
271
|
+
?.onCompletion?.(params, token, workDoneProgress, resultProgress) ??
|
|
272
|
+
[]) satisfies Parameters<typeof connection.onCompletion>[0],
|
|
253
273
|
),
|
|
254
274
|
);
|
|
255
275
|
|
|
256
276
|
connection.onCodeLens(
|
|
257
277
|
debounceHandler(
|
|
258
|
-
(params, token, workDoneProgress, resultProgress) =>
|
|
278
|
+
((params, token, workDoneProgress, resultProgress) =>
|
|
259
279
|
workspace
|
|
260
280
|
.projectForFile(params.textDocument.uri)
|
|
261
|
-
?.onCodeLens?.(params, token, workDoneProgress, resultProgress) ??
|
|
281
|
+
?.onCodeLens?.(params, token, workDoneProgress, resultProgress) ??
|
|
282
|
+
[]) satisfies Parameters<typeof connection.onCodeLens>[0],
|
|
262
283
|
),
|
|
263
284
|
);
|
|
264
285
|
|
|
265
286
|
connection.onCodeAction(
|
|
266
287
|
debounceHandler(
|
|
267
|
-
(params, token, workDoneProgress, resultProgress) =>
|
|
288
|
+
((params, token, workDoneProgress, resultProgress) =>
|
|
268
289
|
workspace
|
|
269
290
|
.projectForFile(params.textDocument.uri)
|
|
270
|
-
?.onCodeAction?.(params, token, workDoneProgress, resultProgress) ??
|
|
291
|
+
?.onCodeAction?.(params, token, workDoneProgress, resultProgress) ??
|
|
292
|
+
[]) satisfies Parameters<typeof connection.onCodeAction>[0],
|
|
271
293
|
),
|
|
272
294
|
);
|
|
273
295
|
|
|
@@ -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
|
+
}
|
|
@@ -17,6 +17,7 @@ import { Debug } from "./utilities";
|
|
|
17
17
|
import type { EngineDecoration } from "../messages";
|
|
18
18
|
import { equal } from "@wry/equality";
|
|
19
19
|
import { isRoverConfig, RoverProject } from "./project/rover/project";
|
|
20
|
+
import { VSCodeConnection } from "./server";
|
|
20
21
|
|
|
21
22
|
export interface WorkspaceConfig {
|
|
22
23
|
clientIdentity: ClientIdentity;
|
|
@@ -35,6 +36,7 @@ export class GraphQLWorkspace {
|
|
|
35
36
|
constructor(
|
|
36
37
|
private LanguageServerLoadingHandler: LanguageServerLoadingHandler,
|
|
37
38
|
private config: WorkspaceConfig,
|
|
39
|
+
private whenConnectionInitialized: Promise<VSCodeConnection>,
|
|
38
40
|
) {}
|
|
39
41
|
|
|
40
42
|
onDiagnostics(handler: NotificationHandler<PublishDiagnosticsParams>) {
|
|
@@ -98,6 +100,11 @@ export class GraphQLWorkspace {
|
|
|
98
100
|
// base class which is used by codegen and other tools
|
|
99
101
|
project.whenReady.then(() => project.validate?.());
|
|
100
102
|
|
|
103
|
+
if (project.onVSCodeConnectionInitialized) {
|
|
104
|
+
this.whenConnectionInitialized.then(
|
|
105
|
+
project.onVSCodeConnectionInitialized.bind(project),
|
|
106
|
+
);
|
|
107
|
+
}
|
|
101
108
|
return project;
|
|
102
109
|
}
|
|
103
110
|
|
|
@@ -201,7 +208,7 @@ export class GraphQLWorkspace {
|
|
|
201
208
|
error = e;
|
|
202
209
|
}
|
|
203
210
|
|
|
204
|
-
const project = this.
|
|
211
|
+
const project = this.projectForConfigFile(configUri);
|
|
205
212
|
|
|
206
213
|
if (this._onConfigFilesFound) {
|
|
207
214
|
this._onConfigFilesFound([config || error]);
|
|
@@ -261,14 +268,31 @@ export class GraphQLWorkspace {
|
|
|
261
268
|
return Array.from(this.projectsByFolderUri.values()).flat();
|
|
262
269
|
}
|
|
263
270
|
|
|
264
|
-
|
|
271
|
+
projectForConfigFile(configUri: DocumentUri): GraphQLProject | undefined {
|
|
272
|
+
for (const projects of this.projectsByFolderUri.values()) {
|
|
273
|
+
const project = projects.find((project) =>
|
|
274
|
+
project.isConfiguredBy(configUri),
|
|
275
|
+
);
|
|
276
|
+
if (project) {
|
|
277
|
+
return project;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return undefined;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
projectForFile(
|
|
284
|
+
uri: DocumentUri,
|
|
285
|
+
languageId?: string,
|
|
286
|
+
): GraphQLProject | undefined {
|
|
265
287
|
const cachedResult = this._projectForFileCache.get(uri);
|
|
266
288
|
if (cachedResult) {
|
|
267
289
|
return cachedResult;
|
|
268
290
|
}
|
|
269
291
|
|
|
270
292
|
for (const projects of this.projectsByFolderUri.values()) {
|
|
271
|
-
const project = projects.find((project) =>
|
|
293
|
+
const project = projects.find((project) =>
|
|
294
|
+
project.includesFile(uri, languageId),
|
|
295
|
+
);
|
|
272
296
|
if (project) {
|
|
273
297
|
this._projectForFileCache.set(uri, project);
|
|
274
298
|
return project;
|
|
@@ -6,9 +6,17 @@ import {
|
|
|
6
6
|
RevealOutputChannelOn,
|
|
7
7
|
} from "vscode-languageclient/node";
|
|
8
8
|
import { workspace, OutputChannel } from "vscode";
|
|
9
|
+
import { supportedLanguageIds } from "./tools/utilities/languageInformation";
|
|
10
|
+
import type { InitializationOptions } from "./language-server/server";
|
|
11
|
+
import { getLangugageInformation } from "./tools/utilities/getLanguageInformation";
|
|
9
12
|
|
|
10
13
|
const { version, referenceID } = require("../package.json");
|
|
11
14
|
|
|
15
|
+
const languageIdExtensionMap = getLangugageInformation();
|
|
16
|
+
const supportedExtensions = supportedLanguageIds.flatMap(
|
|
17
|
+
(id) => languageIdExtensionMap[id],
|
|
18
|
+
);
|
|
19
|
+
|
|
12
20
|
export function getLanguageServerClient(
|
|
13
21
|
serverModule: string,
|
|
14
22
|
outputChannel: OutputChannel,
|
|
@@ -41,25 +49,12 @@ export function getLanguageServerClient(
|
|
|
41
49
|
};
|
|
42
50
|
|
|
43
51
|
const clientOptions: LanguageClientOptions = {
|
|
44
|
-
documentSelector:
|
|
45
|
-
"graphql",
|
|
46
|
-
"javascript",
|
|
47
|
-
"typescript",
|
|
48
|
-
"javascriptreact",
|
|
49
|
-
"typescriptreact",
|
|
50
|
-
"vue",
|
|
51
|
-
"svelte",
|
|
52
|
-
"python",
|
|
53
|
-
"ruby",
|
|
54
|
-
"dart",
|
|
55
|
-
"reason",
|
|
56
|
-
"elixir",
|
|
57
|
-
],
|
|
52
|
+
documentSelector: supportedLanguageIds,
|
|
58
53
|
synchronize: {
|
|
59
54
|
fileEvents: [
|
|
60
55
|
workspace.createFileSystemWatcher("**/.env?(.local)"),
|
|
61
56
|
workspace.createFileSystemWatcher(
|
|
62
|
-
"
|
|
57
|
+
"**/*{" + supportedExtensions.join(",") + "}",
|
|
63
58
|
),
|
|
64
59
|
],
|
|
65
60
|
},
|
|
@@ -69,6 +64,9 @@ export function getLanguageServerClient(
|
|
|
69
64
|
.get("debug.revealOutputOnLanguageServerError")
|
|
70
65
|
? RevealOutputChannelOn.Error
|
|
71
66
|
: RevealOutputChannelOn.Never,
|
|
67
|
+
initializationOptions: {
|
|
68
|
+
languageIdExtensionMap,
|
|
69
|
+
} satisfies InitializationOptions,
|
|
72
70
|
};
|
|
73
71
|
|
|
74
72
|
return new LanguageClient(
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import * as vscode from "vscode";
|
|
2
|
+
import {
|
|
3
|
+
LanguageIdExtensionMap,
|
|
4
|
+
minimumKnownExtensions,
|
|
5
|
+
} from "./languageInformation";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @returns An object with language identifiers as keys and file extensions as values.
|
|
9
|
+
* see https://github.com/microsoft/vscode/issues/109919
|
|
10
|
+
*/
|
|
11
|
+
export function getLangugageInformation(): LanguageIdExtensionMap {
|
|
12
|
+
const allKnownExtensions = vscode.extensions.all
|
|
13
|
+
.map(
|
|
14
|
+
(i) =>
|
|
15
|
+
i.packageJSON?.contributes?.languages as (
|
|
16
|
+
| undefined
|
|
17
|
+
| {
|
|
18
|
+
id?: string;
|
|
19
|
+
extensions?: string[];
|
|
20
|
+
}
|
|
21
|
+
)[],
|
|
22
|
+
)
|
|
23
|
+
.flat()
|
|
24
|
+
.filter(
|
|
25
|
+
(i): i is { id: string; extensions: `.${string}`[] } =>
|
|
26
|
+
!!(i && i.id && i.extensions?.length),
|
|
27
|
+
)
|
|
28
|
+
.reduce<Record<string, Set<`.${string}`>>>(
|
|
29
|
+
(acc, i) => {
|
|
30
|
+
if (!acc[i.id]) acc[i.id] = new Set();
|
|
31
|
+
for (const ext of i.extensions) acc[i.id].add(ext);
|
|
32
|
+
return acc;
|
|
33
|
+
},
|
|
34
|
+
Object.fromEntries(
|
|
35
|
+
Object.entries(minimumKnownExtensions).map(([k, v]) => [k, new Set(v)]),
|
|
36
|
+
),
|
|
37
|
+
);
|
|
38
|
+
return Object.fromEntries(
|
|
39
|
+
Object.entries(allKnownExtensions).map(([k, v]) => [k, [...v]] as const),
|
|
40
|
+
) as LanguageIdExtensionMap;
|
|
41
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const _supportedDocumentTypes = [
|
|
2
|
+
"graphql",
|
|
3
|
+
"javascript",
|
|
4
|
+
"typescript",
|
|
5
|
+
"javascriptreact",
|
|
6
|
+
"typescriptreact",
|
|
7
|
+
"vue",
|
|
8
|
+
"svelte",
|
|
9
|
+
"python",
|
|
10
|
+
"ruby",
|
|
11
|
+
"dart",
|
|
12
|
+
"reason",
|
|
13
|
+
"elixir",
|
|
14
|
+
] as const;
|
|
15
|
+
export type SupportedLanguageIds = (typeof _supportedDocumentTypes)[number];
|
|
16
|
+
export const supportedLanguageIds =
|
|
17
|
+
// remove the `readonly` we get from using `as const`
|
|
18
|
+
_supportedDocumentTypes as any as SupportedLanguageIds[];
|
|
19
|
+
|
|
20
|
+
export type FileExtension = `.${string}`;
|
|
21
|
+
|
|
22
|
+
export const minimumKnownExtensions: Record<
|
|
23
|
+
SupportedLanguageIds,
|
|
24
|
+
FileExtension[]
|
|
25
|
+
> = {
|
|
26
|
+
graphql: [".gql", ".graphql", ".graphqls"],
|
|
27
|
+
javascript: [".js", ".mjs", ".cjs"],
|
|
28
|
+
typescript: [".ts", ".mts", ".cts"],
|
|
29
|
+
javascriptreact: [".jsx"],
|
|
30
|
+
typescriptreact: [".tsx"],
|
|
31
|
+
vue: [".vue"],
|
|
32
|
+
svelte: [".svelte"],
|
|
33
|
+
python: [".py"],
|
|
34
|
+
ruby: [".rb"],
|
|
35
|
+
dart: [".dart"],
|
|
36
|
+
reason: [".re"],
|
|
37
|
+
elixir: [".ex", ".exs"],
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type LanguageIdExtensionMap = Record<string, `.${string}`[]> &
|
|
41
|
+
typeof minimumKnownExtensions;
|