Skip to content

Commit 0ad3777

Browse files
authored
Allow other VS Code extensions to install LSP middleware on language feature responses (#64583)
1 parent dca8e23 commit 0ad3777

25 files changed

Lines changed: 3966 additions & 56 deletions

‎.dprint.jsonc‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@
4848
"**/testdata",
4949
"packages/vscode-typescript/l10n/**",
5050
"tsc/internal/bundled/libs/**",
51-
"tsc/internal/lsp/lsproto/_generate/*.json",
52-
"tsc/internal/lsp/lsproto/_generate/metaModelSchema.mts",
51+
"tools/scripts/lsp/*.json",
52+
"tools/scripts/lsp/metaModelSchema.mts",
5353
// Needs to be LF to have a working shebang.
5454
"packages/typescript/bin/tsc",
5555
"tsc/internal/bundled/source/**",

‎Herebyfile.mjs‎

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -613,32 +613,47 @@ export const generateExtensionTest = task({
613613

614614
async function runGenerateLSP() {
615615
const { GeneratedFile } = await import("./tools/scripts/gen/generatedFile.mts");
616-
const directory = path.join(__dirname, "tsc/internal/lsp/lsproto/_generate");
616+
const directory = path.join(__dirname, "tools/scripts/lsp");
617617
const modelFiles = ["metaModel.json", "metaModelSchema.mts"].map(file => new GeneratedFile(path.join(directory, file), [path.join(directory, "fetchModel.mts"), path.join(__dirname, "package-lock.json")]));
618618
if (!modelFiles.every(file => file.isCurrent(!!options.force))) {
619619
for (const file of modelFiles) file.invalidate();
620-
const { default: fetchModel } = await import("./tsc/internal/lsp/lsproto/_generate/fetchModel.mts");
620+
const { default: fetchModel } = await import("./tools/scripts/lsp/fetchModel.mts");
621621
await fetchModel();
622622
for (const file of modelFiles) file.markCurrent();
623623
}
624-
const output = new GeneratedFile(path.join(directory, "../lsp_generated.go"), [
624+
const output = new GeneratedFile(path.join(__dirname, "tsc/internal/lsp/lsproto/lsp_generated.go"), [
625625
__filename,
626626
path.join(directory, "generate.mts"),
627627
...modelFiles.map(file => file.fileName),
628628
]);
629-
if (output.isCurrent(!!options.force)) {
629+
if (!output.isCurrent(!!options.force)) {
630+
output.invalidate();
631+
const { default: generate } = await import("./tools/scripts/lsp/generate.mts");
632+
await generate();
633+
output.markCurrent();
634+
}
635+
else {
630636
console.log("LSP bindings are up to date.");
631-
return;
632637
}
633-
output.invalidate();
634-
const { default: generate } = await import("./tsc/internal/lsp/lsproto/_generate/generate.mts");
635-
await generate();
636-
output.markCurrent();
638+
const typeScriptOutput = new GeneratedFile(path.join(__dirname, "packages/typescript/src/vscode/protocol.generated.ts"), [
639+
__filename,
640+
path.join(directory, "generate.mts"),
641+
path.join(directory, "generateTypeScript.mts"),
642+
path.join(directory, "typeScript.mts"),
643+
path.join(__dirname, "packages/vscode-typescript/src/lspMiddleware.ts"),
644+
...modelFiles.map(file => file.fileName),
645+
]);
646+
if (!typeScriptOutput.isCurrent(!!options.force)) {
647+
typeScriptOutput.invalidate();
648+
const { default: generate } = await import("./tools/scripts/lsp/generateTypeScript.mts");
649+
await generate();
650+
typeScriptOutput.markCurrent();
651+
}
637652
}
638653

639654
export const generateLSP = task({
640655
name: "generate:lsp",
641-
description: "Generates LSP bindings from the pinned protocol model. Pass --force to regenerate unchanged files.",
656+
description: "Generates Go LSP bindings and extension API types from the pinned protocol model. Pass --force to regenerate unchanged files.",
642657
run: runGenerateLSP,
643658
});
644659

@@ -1315,7 +1330,7 @@ export const checkVsceVersion = task({
13151330
const scriptTsconfigs = [
13161331
"./tools/scripts/gen/tsconfig.json",
13171332
"./tools/scripts/tsc/tsconfig.json",
1318-
"./tsc/internal/lsp/lsproto/_generate/tsconfig.json",
1333+
"./tools/scripts/lsp/tsconfig.json",
13191334
];
13201335

13211336
export const checkScripts = task({

‎package-lock.json‎

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎packages/typescript/package.json‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@
4141
"./schemas/tsconfig.schema.json": "./schemas/tsconfig.schema.json",
4242
"./schemas/jsconfig.schema.json": "./schemas/jsconfig.schema.json",
4343
".": "./lib/version.cjs",
44+
"./unstable/vscode": {
45+
"types": "./dist/vscode/extensionApi.d.ts"
46+
},
4447
"./unstable/sync": {
4548
"@typescript/source": "./src/api/sync/api.ts",
4649
"default": "./dist/api/sync/api.js"
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import type { LspMiddlewareRequests } from "./protocol.generated.ts";
2+
export type * from "./protocol.generated.ts";
3+
4+
export interface Disposable {
5+
dispose(): unknown;
6+
}
7+
8+
export interface Event<T> {
9+
(listener: (event: T) => unknown, thisArgs?: unknown, disposables?: Disposable[] | undefined): Disposable;
10+
}
11+
12+
/** The URI properties used when launching a content mapper. */
13+
export interface ContentMapperUri {
14+
readonly scheme: string;
15+
readonly fsPath: string;
16+
}
17+
18+
export interface ContentMapperManifest {
19+
readonly name: string;
20+
readonly version?: string | undefined;
21+
readonly exec: readonly string[];
22+
readonly cwd?: ContentMapperUri | undefined;
23+
readonly compilerOptions?: readonly string[] | undefined;
24+
readonly dynamicConfig?: boolean | undefined;
25+
}
26+
27+
export interface ContentMapperContribution {
28+
readonly extensions: readonly string[];
29+
readonly inferredProjectContribution?: {
30+
readonly options?: Readonly<Record<string, unknown>> | undefined;
31+
readonly manifest: ContentMapperManifest;
32+
} | undefined;
33+
}
34+
35+
export type LspMiddlewareMethod = keyof LspMiddlewareRequests;
36+
37+
export type DeepReadonly<T> = unknown extends T ? unknown : T extends object ? { readonly [K in keyof T]: DeepReadonly<T[K]>; } : T;
38+
39+
export type LspMiddlewareResult<M extends LspMiddlewareMethod> = LspMiddlewareRequests[M]["result"];
40+
41+
export type LspMiddlewareContext<M extends LspMiddlewareMethod> = { readonly params: DeepReadonly<LspMiddlewareRequests[M]["params"]>; };
42+
43+
export type LspMiddlewareTransformer<M extends LspMiddlewareMethod> = (
44+
result: LspMiddlewareResult<M>,
45+
context: LspMiddlewareContext<M>,
46+
) => LspMiddlewareResult<M> | PromiseLike<LspMiddlewareResult<M>>;
47+
48+
export interface ExtensionAPI {
49+
onLanguageServerInitialized: Event<void>;
50+
initializeAPIConnection(pipe?: string): Promise<string>;
51+
registerContentMappers(contributorId: string, contributions: readonly ContentMapperContribution[]): Disposable;
52+
53+
/**
54+
* Changes an LSP language-feature response before conversion to VS Code objects.
55+
* Each callback gets its own copy of the request parameters.
56+
*
57+
* Callbacks run in registration order. Order between extensions may vary.
58+
* If a callback fails, we log the error and use the original server response.
59+
*
60+
* Registrations survive server restarts. Disposing removes the callback from
61+
* future responses but does not interrupt a response already being processed.
62+
*
63+
* Requests, notifications, lifecycle messages, and partial results are excluded.
64+
*/
65+
registerLspMiddleware<M extends LspMiddlewareMethod>(
66+
method: M,
67+
transformer: LspMiddlewareTransformer<NoInfer<M>>,
68+
): Disposable;
69+
}

0 commit comments

Comments
 (0)