From 44364062a82cedddac60a063bd6feded39f5dac8 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Thu, 1 Oct 2026 14:22:30 -0700 Subject: [PATCH 01/10] Allow other VS Code extensions to install LSP middleware on language feature responses --- packages/vscode-typescript/src/api.ts | 147 +++++++ packages/vscode-typescript/src/client.ts | 5 +- .../src/contentMapperContributions.ts | 20 +- packages/vscode-typescript/src/extension.ts | 12 +- .../vscode-typescript/src/lspMiddleware.ts | 159 +++++++ packages/vscode-typescript/src/session.ts | 21 +- packages/vscode-typescript/test/index.test.ts | 1 + .../test/lspMiddleware.test.ts | 395 ++++++++++++++++++ packages/vscode-typescript/test/tsconfig.json | 3 +- 9 files changed, 733 insertions(+), 30 deletions(-) create mode 100644 packages/vscode-typescript/src/api.ts create mode 100644 packages/vscode-typescript/src/lspMiddleware.ts create mode 100644 packages/vscode-typescript/test/lspMiddleware.test.ts diff --git a/packages/vscode-typescript/src/api.ts b/packages/vscode-typescript/src/api.ts new file mode 100644 index 0000000000000..47cfe987fd81e --- /dev/null +++ b/packages/vscode-typescript/src/api.ts @@ -0,0 +1,147 @@ +import type * as vscode from "vscode"; +import type * as lsp from "vscode-languageserver-protocol"; + +export interface ContentMapperManifest { + readonly name: string; + readonly version?: string; + readonly exec: readonly string[]; + readonly cwd?: vscode.Uri; + readonly compilerOptions?: readonly string[]; + readonly dynamicConfig?: boolean; +} + +export interface ContentMapperContribution { + readonly extensions: readonly string[]; + readonly inferredProjectContribution?: { + readonly options?: Readonly>; + readonly manifest: ContentMapperManifest; + }; +} + +type Request = T extends lsp.ProtocolRequestType ? { params: P; result: R; } + : never; + +export interface MultiDocumentHighlightParams extends lsp.TextDocumentPositionParams { + filesToSearch: string[]; +} + +export interface MultiDocumentHighlight { + uri: lsp.DocumentUri; + highlights: lsp.DocumentHighlight[]; +} + +export interface AutoInsertParams { + _vs_textDocument: lsp.TextDocumentIdentifier; + _vs_position: lsp.Position; + _vs_ch: string; +} + +export interface AutoInsertResult { + _vs_textEditFormat: lsp.InsertTextFormat; + _vs_textEdit: lsp.TextEdit; +} + +export interface ClassifiedTextElement { + Runs: { + ClassificationTypeName: string; + Text: string; + MarkerTagType?: string; + Style?: number | null; + _vs_type: "ClassifiedTextRun"; + }[]; + _vs_type: "ClassifiedTextElement"; +} + +export interface VSReferenceItem { + _vs_id: number; + _vs_definitionId?: number; + _vs_kind?: number[]; + _vs_location: lsp.Location; + _vs_definitionText?: ClassifiedTextElement; + _vs_projectName?: string; + _vs_containingType?: string; +} + +/** Language-feature requests supported by this extension's middleware API. */ +export interface LspMiddlewareRequests { + "textDocument/hover": { + params: lsp.HoverParams & { verbosityLevel?: number; }; + result: (lsp.Hover & { canIncreaseVerbosity?: boolean; }) | null; + }; + "textDocument/completion": Request; + "completionItem/resolve": Request; + "textDocument/signatureHelp": Request; + "textDocument/definition": Request; + "textDocument/typeDefinition": Request; + "textDocument/implementation": Request; + "textDocument/references": Request; + "textDocument/documentHighlight": Request; + "textDocument/documentSymbol": Request; + "workspace/symbol": Request & { + params: { textDocument?: lsp.TextDocumentIdentifier; }; + }; + "textDocument/rename": Request; + "textDocument/prepareRename": Request; + "textDocument/formatting": Request; + "textDocument/rangeFormatting": Request; + "textDocument/onTypeFormatting": Request; + "textDocument/selectionRange": Request; + "textDocument/foldingRange": Request; + "textDocument/inlayHint": Request; + "textDocument/codeAction": Request; + "textDocument/codeLens": Request; + "codeLens/resolve": Request; + "textDocument/prepareCallHierarchy": Request; + "callHierarchy/incomingCalls": Request; + "callHierarchy/outgoingCalls": Request; + "textDocument/linkedEditingRange": Request; + "textDocument/semanticTokens/full": Request; + "textDocument/semanticTokens/range": Request; + "textDocument/diagnostic": Request; + "custom/textDocument/sourceDefinition": { + params: lsp.TextDocumentPositionParams; + result: lsp.Location | lsp.Location[] | lsp.LocationLink[] | null; + }; + "custom/textDocument/multiDocumentHighlight": { + params: MultiDocumentHighlightParams; + result: MultiDocumentHighlight[] | null; + }; + "textDocument/_vs_onAutoInsert": { params: AutoInsertParams; result: AutoInsertResult | null; }; + "textDocument/_vs_references": { params: lsp.ReferenceParams; result: VSReferenceItem[] | null; }; +} + +export type LspMiddlewareMethod = keyof LspMiddlewareRequests; + +export type DeepReadonly = unknown extends T ? unknown : T extends object ? { readonly [K in keyof T]: DeepReadonly; } : T; + +export type LspMiddlewareResult = LspMiddlewareRequests[M]["result"]; + +export type LspMiddlewareContext = { readonly params: DeepReadonly; }; + +export type LspMiddlewareTransformer = ( + result: LspMiddlewareResult, + context: LspMiddlewareContext, +) => LspMiddlewareResult | PromiseLike>; + +export interface ExtensionAPI { + onLanguageServerInitialized: vscode.Event; + initializeAPIConnection(pipe?: string): Promise; + registerContentMappers(contributorId: string, contributions: readonly ContentMapperContribution[]): vscode.Disposable; + + /** + * Changes an LSP language-feature response before conversion to VS Code objects. + * Request parameters are provided as a frozen copy. + * + * Callbacks run in registration order. Order between extensions may vary. + * If a callback fails, we log the error and use the original server response. + * + * Registrations survive server restarts. Disposing removes the callback from + * future responses but does not interrupt a response already being processed. + * + * Requests, notifications, lifecycle messages, and partial results are excluded. + */ + registerLspMiddleware( + method: M, + transformer: LspMiddlewareTransformer>, + ): vscode.Disposable; +} diff --git a/packages/vscode-typescript/src/client.ts b/packages/vscode-typescript/src/client.ts index b8bd17df8540a..80ff3d6169259 100644 --- a/packages/vscode-typescript/src/client.ts +++ b/packages/vscode-typescript/src/client.ts @@ -29,6 +29,7 @@ import { registerMultiDocumentHighlightFeature } from "./languageFeatures/docume import { registerHoverFeature } from "./languageFeatures/hover"; import { registerOnAutoInsertFeature } from "./languageFeatures/onAutoInsert"; import { registerSourceDefinitionFeature } from "./languageFeatures/sourceDefinition"; +import type { LspMiddlewareRegistry } from "./lspMiddleware"; import * as tr from "./telemetryReporting"; import { contentMappersEnabled, @@ -39,7 +40,6 @@ import { readNativePreviewConfig, } from "./util"; import { getLanguageForUri } from "./util"; -import { workspaceSymbolSendRequestMiddleware } from "./workspaceSymbolMiddleware"; // Registration IDs the server uses for content mapper capabilities all share this prefix (see // RegisterContentMapperExtensions in internal/lsp/server.go). The extension watches for these dynamic @@ -88,6 +88,7 @@ export class Client implements vscode.Disposable { outputChannel: vscode.LogOutputChannel, initializedEventEmitter: vscode.EventEmitter, telemetryReporter: tr.TelemetryReporter, + private readonly lspMiddleware: LspMiddlewareRegistry, ) { this.outputChannel = outputChannel; this.initializedEventEmitter = initializedEventEmitter; @@ -128,7 +129,7 @@ export class Client implements vscode.Disposable { }, }, sendNotification: sendNotificationMiddleware, - sendRequest: workspaceSymbolSendRequestMiddleware, + sendRequest: (type, params, token, next) => this.lspMiddleware.sendRequest(type, params, token, next), provideHover: () => undefined, handleRegisterCapability: async (params, next) => { await next(params, CancellationToken.None); diff --git a/packages/vscode-typescript/src/contentMapperContributions.ts b/packages/vscode-typescript/src/contentMapperContributions.ts index a4570d7bd96bb..c4c0e6e4ddbc5 100644 --- a/packages/vscode-typescript/src/contentMapperContributions.ts +++ b/packages/vscode-typescript/src/contentMapperContributions.ts @@ -1,21 +1,5 @@ -import * as vscode from "vscode"; - -export interface ContentMapperManifest { - readonly name: string; - readonly version?: string; - readonly exec: readonly string[]; - readonly cwd?: vscode.Uri; - readonly compilerOptions?: readonly string[]; - readonly dynamicConfig?: boolean; -} - -export interface ContentMapperContribution { - readonly extensions: readonly string[]; - readonly inferredProjectContribution?: { - readonly options?: Readonly>; - readonly manifest: ContentMapperManifest; - }; -} +import type { ContentMapperContribution } from "./api"; +export type { ContentMapperContribution, ContentMapperManifest } from "./api"; export interface SerializedContentMapperContribution { readonly contributorId: string; diff --git a/packages/vscode-typescript/src/extension.ts b/packages/vscode-typescript/src/extension.ts index 7bff6514e4397..8936dee0d0e71 100644 --- a/packages/vscode-typescript/src/extension.ts +++ b/packages/vscode-typescript/src/extension.ts @@ -1,10 +1,11 @@ import * as vscode from "vscode"; +import type { ExtensionAPI } from "./api"; +export type { ExtensionAPI } from "./api"; import { registerEnablementCommands, updateUseTsgoSetting, } from "./commands"; -import type { ContentMapperContribution } from "./contentMapperContributions"; import { aiConnectionString, getExplicitConfigTarget, @@ -24,12 +25,6 @@ import { createTelemetryReporter } from "./telemetryReporting"; import assert from "node:assert"; -export interface ExtensionAPI { - onLanguageServerInitialized: vscode.Event; - initializeAPIConnection(pipe?: string): Promise; - registerContentMappers(contributorId: string, contributions: readonly ContentMapperContribution[]): vscode.Disposable; -} - export async function activate(context: vscode.ExtensionContext): Promise { await vscode.commands.executeCommand("setContext", "typescript.native-preview.serverRunning", false); @@ -85,6 +80,9 @@ export async function activate(context: vscode.ExtensionContext): Promise | undefined; diff --git a/packages/vscode-typescript/src/lspMiddleware.ts b/packages/vscode-typescript/src/lspMiddleware.ts new file mode 100644 index 0000000000000..319d8f42253f2 --- /dev/null +++ b/packages/vscode-typescript/src/lspMiddleware.ts @@ -0,0 +1,159 @@ +import type { + CancellationToken, + Disposable, + MessageSignature, +} from "vscode-languageserver-protocol"; +import type { + LspMiddlewareContext, + LspMiddlewareMethod, + LspMiddlewareResult, + LspMiddlewareTransformer, +} from "./api"; + +const supportedMethods: { readonly [M in LspMiddlewareMethod]: true; } = { + "textDocument/hover": true, + "textDocument/completion": true, + "completionItem/resolve": true, + "textDocument/signatureHelp": true, + "textDocument/definition": true, + "textDocument/typeDefinition": true, + "textDocument/implementation": true, + "textDocument/references": true, + "textDocument/documentHighlight": true, + "textDocument/documentSymbol": true, + "workspace/symbol": true, + "textDocument/rename": true, + "textDocument/prepareRename": true, + "textDocument/formatting": true, + "textDocument/rangeFormatting": true, + "textDocument/onTypeFormatting": true, + "textDocument/selectionRange": true, + "textDocument/foldingRange": true, + "textDocument/inlayHint": true, + "textDocument/codeAction": true, + "textDocument/codeLens": true, + "codeLens/resolve": true, + "textDocument/prepareCallHierarchy": true, + "callHierarchy/incomingCalls": true, + "callHierarchy/outgoingCalls": true, + "textDocument/linkedEditingRange": true, + "textDocument/semanticTokens/full": true, + "textDocument/semanticTokens/range": true, + "textDocument/diagnostic": true, + "custom/textDocument/sourceDefinition": true, + "custom/textDocument/multiDocumentHighlight": true, + "textDocument/_vs_onAutoInsert": true, + "textDocument/_vs_references": true, +}; + +function isSupportedMethod(method: string): method is LspMiddlewareMethod { + return Object.hasOwn(supportedMethods, method); +} + +/** Internal middleware can change requests and receives the original parameters. */ +export interface FirstPartyLspMiddleware { + ( + type: string | MessageSignature, + params: P | undefined, + token: CancellationToken | undefined, + next: (type: string | MessageSignature, params?: P, token?: CancellationToken) => Promise, + ): Promise; +} + +interface Registration { + invoke(result: unknown, context: unknown): unknown; +} + +function freezeDeep(value: unknown): void { + if (typeof value !== "object" || value === null) return; + Object.freeze(value); + for (const child of Object.values(value)) { + freezeDeep(child); + } +} + +export class LspMiddlewareRegistry implements Disposable { + private readonly registrations = new Map(); + private disposed = false; + + constructor( + private readonly reportError: (method: LspMiddlewareMethod, error: unknown) => void, + private readonly firstPartyMiddleware: readonly FirstPartyLspMiddleware[] = [], + ) {} + + register(method: M, transformer: LspMiddlewareTransformer>): Disposable { + if (this.disposed) { + throw new Error("LSP middleware registry is disposed."); + } + if (!isSupportedMethod(method)) { + throw new TypeError(`LSP middleware method '${method}' is not supported.`); + } + if (typeof transformer !== "function") { + throw new TypeError("LSP middleware transformer must be a function."); + } + const registration: Registration = { + // The transport erases protocol types; registration keeps each callback paired with its method. + invoke: (result, context) => transformer(result as LspMiddlewareResult, context as LspMiddlewareContext), + }; + const registrations = this.registrations.get(method) ?? []; + registrations.push(registration); + this.registrations.set(method, registrations); + let disposed = false; + return { + dispose: () => { + if (disposed) return; + disposed = true; + const index = registrations.indexOf(registration); + if (index !== -1) registrations.splice(index, 1); + if (registrations.length === 0 && this.registrations.get(method) === registrations) { + this.registrations.delete(method); + } + }, + }; + } + + sendRequest( + type: string | MessageSignature, + params: P | undefined, + token: CancellationToken | undefined, + next: (type: string | MessageSignature, params?: P, token?: CancellationToken) => Promise, + ): Promise { + const dispatch = async (type: string | MessageSignature, params?: P, token?: CancellationToken): Promise => { + const result = await next(type, params, token); + const method = typeof type === "string" ? type : type.method; + if (!isSupportedMethod(method)) return result; + return this.transform(method, result, { params }, token); + }; + const invoke = (index: number, type: string | MessageSignature, params?: P, token?: CancellationToken): Promise => { + const middleware = this.firstPartyMiddleware[index]; + return middleware + ? middleware(type, params, token, (type, params, token) => invoke(index + 1, type, params, token)) + : dispatch(type, params, token); + }; + return invoke(0, type, params, token); + } + + private async transform(method: LspMiddlewareMethod, original: R, context: unknown, token?: CancellationToken): Promise { + const registrations = this.registrations.get(method)?.slice(); + if (!registrations?.length || token?.isCancellationRequested) return original; + try { + let result: unknown = structuredClone(original); + const readonlyContext: unknown = structuredClone(context); + freezeDeep(readonlyContext); + for (const registration of registrations) { + if (token?.isCancellationRequested) return original; + result = await registration.invoke(result, readonlyContext); + } + return token?.isCancellationRequested ? original : result as R; + } + catch (error) { + this.reportError(method, error); + return original; + } + } + + dispose(): void { + this.disposed = true; + this.registrations.clear(); + } +} diff --git a/packages/vscode-typescript/src/session.ts b/packages/vscode-typescript/src/session.ts index 46303606b836f..b3323dd07680f 100644 --- a/packages/vscode-typescript/src/session.ts +++ b/packages/vscode-typescript/src/session.ts @@ -1,6 +1,10 @@ import * as path from "path"; import * as vscode from "vscode"; import { ActiveJsTsEditorTracker } from "./activeJsTsEditorTracker"; +import type { + LspMiddlewareMethod, + LspMiddlewareTransformer, +} from "./api"; import { Client } from "./client"; import { registerCodeLensShowLocationsCommand, @@ -12,6 +16,7 @@ import { serializeContentMapperContributions, validateContentMapperRegistration, } from "./contentMapperContributions"; +import { LspMiddlewareRegistry } from "./lspMiddleware"; import { ProjectStatus } from "./projectStatus"; import { setupStatusBar } from "./statusBar"; import { TelemetryReporter } from "./telemetryReporting"; @@ -27,6 +32,7 @@ import { useWorkspaceTsdkStorageKey, workspaceConfigBase, } from "./util"; +import { workspaceSymbolSendRequestMiddleware } from "./workspaceSymbolMiddleware"; /** * SessionManager's lifetime is equal to that of the extension. It is responsible @@ -39,6 +45,7 @@ export class SessionManager implements vscode.Disposable { private initializedEventEmitter: vscode.EventEmitter; private telemetryReporter: TelemetryReporter; private readonly contentMapperRegistrations = new Map(); + private readonly lspMiddleware: LspMiddlewareRegistry; private lifecycleOperation = Promise.resolve(); private contentMapperSyncOperation = Promise.resolve(); @@ -51,6 +58,10 @@ export class SessionManager implements vscode.Disposable { this.outputChannel = outputChannel; this.telemetryReporter = telemetryReporter; this.initializedEventEmitter = initializedEventEmitter; + this.lspMiddleware = new LspMiddlewareRegistry((method, error) => { + const detail = error instanceof Error ? error.stack ?? error.message : String(error); + this.outputChannel.error(`LSP middleware for '${method}' failed; using original server data: ${detail}`); + }, [workspaceSymbolSendRequestMiddleware]); this.disposables.push(vscode.workspace.onDidChangeConfiguration(event => { if (this.currentSession && event.affectsConfiguration("js/ts.contentMappers.enabled")) { @@ -82,7 +93,7 @@ export class SessionManager implements vscode.Disposable { this.outputChannel.appendLine("Restarting TypeScript language server..."); await this.currentSession.stop(); } - const session = new Session(context, this.outputChannel, this.initializedEventEmitter, this.telemetryReporter, () => this.stop(), () => this.restart(context)); + const session = new Session(context, this.outputChannel, this.initializedEventEmitter, this.telemetryReporter, () => this.stop(), () => this.restart(context), this.lspMiddleware); this.currentSession = session; try { await session.start(context); @@ -130,6 +141,10 @@ export class SessionManager implements vscode.Disposable { }); } + registerLspMiddleware(method: M, transformer: LspMiddlewareTransformer>): vscode.Disposable { + return this.lspMiddleware.register(method, transformer); + } + private syncContentMapperContributions(): Promise { const operation = this.contentMapperSyncOperation.then(() => this.syncContentMapperContributionsNow()); this.contentMapperSyncOperation = operation.catch(() => {}); @@ -160,6 +175,7 @@ export class SessionManager implements vscode.Disposable { dispose(): Promise { return this.enqueueLifecycleOperation(async () => { + this.lspMiddleware.dispose(); await this.currentSession?.dispose(); this.currentSession = undefined; await Promise.all(this.disposables.splice(0).map(d => d.dispose())); @@ -192,8 +208,9 @@ class Session implements vscode.Disposable { telemetryReporter: TelemetryReporter, stopSession: () => Promise, restartSession: () => Promise, + lspMiddleware: LspMiddlewareRegistry, ) { - this.client = new Client(outputChannel, initializedEventEmitter, telemetryReporter); + this.client = new Client(outputChannel, initializedEventEmitter, telemetryReporter, lspMiddleware); this.context = context; this.outputChannel = outputChannel; this.telemetryReporter = telemetryReporter; diff --git a/packages/vscode-typescript/test/index.test.ts b/packages/vscode-typescript/test/index.test.ts index 1688ac53bc213..76a3a8d1e641f 100644 --- a/packages/vscode-typescript/test/index.test.ts +++ b/packages/vscode-typescript/test/index.test.ts @@ -1,2 +1,3 @@ import "./contentMapperContributions.test"; +import "./lspMiddleware.test"; import "./tsdkPackage.test"; diff --git a/packages/vscode-typescript/test/lspMiddleware.test.ts b/packages/vscode-typescript/test/lspMiddleware.test.ts new file mode 100644 index 0000000000000..8f9c87f4cae90 --- /dev/null +++ b/packages/vscode-typescript/test/lspMiddleware.test.ts @@ -0,0 +1,395 @@ +import assert from "node:assert/strict"; +import { PassThrough } from "node:stream"; +import test, { describe } from "node:test"; +import { + CancellationToken, + CancellationTokenSource, + createProtocolConnection, + type Diagnostic, + HoverRequest, + PublishDiagnosticsNotification, + type PublishDiagnosticsParams, +} from "vscode-languageserver-protocol/node"; +import type { ExtensionAPI } from "../src/api"; +import { + type FirstPartyLspMiddleware, + LspMiddlewareRegistry, +} from "../src/lspMiddleware"; + +const params = { textDocument: { uri: "file:///test.ts" }, position: { line: 0, character: 0 } }; + +function deferred() { + let resolve!: () => void; + const promise = new Promise(resolver => { + resolve = resolver; + }); + return { promise, resolve }; +} + +describe("LSP middleware", () => { + test("transforms raw results in registration order without changing requests", async () => { + const registry = new LspMiddlewareRegistry(() => assert.fail("Unexpected error")); + registry.register("textDocument/hover", (result, context) => { + assert.deepEqual(context.params, params); + assert.notEqual(context.params, params); + assert.ok(Object.isFrozen(context.params.textDocument)); + return result && { ...result, contents: "first" }; + }); + registry.register("textDocument/hover", async result => { + assert.equal(result?.contents, "first"); + return result && { ...result, contents: "second" }; + }); + const original = { contents: "server", canIncreaseVerbosity: true }; + let requests = 0; + const result = await registry.sendRequest("textDocument/hover", params, undefined, async (type, sentParams, token) => { + requests++; + assert.equal(type, "textDocument/hover"); + assert.equal(sentParams, params); + assert.equal(token, undefined); + return original; + }); + assert.equal(requests, 1); + assert.deepEqual(result, { contents: "second", canIncreaseVerbosity: true }); + assert.equal(original.contents, "server"); + }); + + test("rejects excluded methods at registration", () => { + const registry = new LspMiddlewareRegistry(() => assert.fail("Unexpected error")); + for ( + const method of [ + "initialize", + "shutdown", + "initialized", + "exit", + "textDocument/didOpen", + "textDocument/didChange", + "textDocument/publishDiagnostics", + "window/logMessage", + "window/showMessage", + "workspace/didChangeConfiguration", + "workspace/willRenameFiles", + "workspace/executeCommand", + "workspace/diagnostic/refresh", + "client/registerCapability", + "custom/initializeAPISession", + "custom/setContentMapperContributions", + "custom/runGC", + "custom/projectInfo", + "$/progress", + "unknown/feature", + "__proto__", + ] + ) { + // @ts-expect-error Non-feature methods are also rejected at runtime. + assert.throws(() => registry.register(method, result => result), /not supported/); + } + }); + + test("rejects invalid callbacks and registration after extension disposal", () => { + const registry = new LspMiddlewareRegistry(() => assert.fail("Unexpected error")); + // @ts-expect-error Invalid JavaScript consumers are checked at runtime too. + assert.throws(() => registry.register("textDocument/hover", {}), /must be a function/); + registry.dispose(); + assert.throws(() => registry.register("textDocument/hover", result => result), /disposed/); + }); + + test("passes excluded requests through unchanged", async () => { + const registry = new LspMiddlewareRegistry(() => assert.fail("Unexpected error")); + registry.register("textDocument/hover", () => assert.fail("Excluded request intercepted")); + const original = { result: "unchanged" }; + const token = CancellationToken.None; + for (const method of ["initialize", "shutdown", "custom/projectInfo", "textDocument/didOpen", "textDocument/publishDiagnostics"]) { + assert.equal( + await registry.sendRequest(method, params, token, async (type, sentParams, sentToken) => { + assert.equal(type, method); + assert.equal(sentParams, params); + assert.equal(sentToken, token); + return original; + }), + original, + ); + } + }); + + test("handles protocol request signatures and preserves token identity", async () => { + const registry = new LspMiddlewareRegistry(() => assert.fail("Unexpected error")); + const token = CancellationToken.None; + registry.register("textDocument/hover", result => result && { ...result, contents: "transformed" }); + const result = await registry.sendRequest(HoverRequest.type, params, token, async (type, sentParams, sentToken) => { + assert.equal(type, HoverRequest.type); + assert.equal(sentParams, params); + assert.equal(sentToken, token); + return { contents: "server" }; + }); + assert.deepEqual(result, { contents: "transformed" }); + }); + + test("passes server failures through without invoking middleware or logging a transformer error", async () => { + const registry = new LspMiddlewareRegistry(() => assert.fail("Server errors are not transformer errors")); + registry.register("textDocument/hover", () => assert.fail("No successful result")); + const failure = new Error("Server failure"); + await assert.rejects( + registry.sendRequest("textDocument/hover", params, undefined, async () => { + throw failure; + }), + error => error === failure, + ); + }); + + test("preserves null and empty responses and permits intentional replacements", async () => { + const registry = new LspMiddlewareRegistry(() => assert.fail("Unexpected error")); + assert.equal(await registry.sendRequest("textDocument/hover", params, undefined, async () => null), null); + const empty: Diagnostic[] = []; + const original = { kind: "full", items: empty }; + assert.equal(await registry.sendRequest("textDocument/diagnostic", params, undefined, async () => original), original); + registry.register("textDocument/hover", result => result ?? { contents: "replacement" }); + assert.deepEqual(await registry.sendRequest("textDocument/hover", params, undefined, async () => null), { contents: "replacement" }); + registry.register("textDocument/diagnostic", result => { + if (result.kind === "full") assert.equal(result.items.length, 0); + return result; + }); + assert.deepEqual(await registry.sendRequest("textDocument/diagnostic", params, undefined, async () => original), original); + }); + + test("exposes deeply frozen detached context without modifying outgoing parameters", async () => { + const errors: unknown[] = []; + const registry = new LspMiddlewareRegistry((_method, error) => errors.push(error)); + registry.register("textDocument/hover", (result, context) => { + // @ts-expect-error Deliberate runtime mutation of readonly request context. + context.params.textDocument.uri = "file:///modified.ts"; + return result; + }); + const original = { contents: "server" }; + assert.equal(await registry.sendRequest("textDocument/hover", params, undefined, async () => original), original); + assert.equal(params.textDocument.uri, "file:///test.ts"); + assert.equal(errors.length, 1); + assert.ok(errors[0] instanceof TypeError); + }); + + test("captures registrations on response arrival, not request dispatch", async () => { + const registry = new LspMiddlewareRegistry(() => assert.fail("Unexpected error")); + const response = deferred(); + const running = registry.sendRequest("textDocument/hover", params, undefined, async () => { + await response.promise; + return { contents: "server" }; + }); + registry.register("textDocument/hover", () => ({ contents: "registered while waiting" })); + response.resolve(); + assert.deepEqual(await running, { contents: "registered while waiting" }); + }); + + test("skips remaining transformers when cancellation is requested", async () => { + const registry = new LspMiddlewareRegistry(() => assert.fail("Unexpected error")); + const source = new CancellationTokenSource(); + const original = { contents: "server" }; + registry.register("textDocument/hover", result => { + source.cancel(); + return result && { ...result, contents: "cancelled" }; + }); + registry.register("textDocument/hover", () => assert.fail("Cancelled chain must stop")); + assert.equal(await registry.sendRequest("textDocument/hover", params, source.token, async () => original), original); + assert.equal(await registry.sendRequest("textDocument/hover", params, source.token, async () => original), original); + source.dispose(); + }); + + test("logs failures, stops the chain, and restores pristine original data", async () => { + const errors: unknown[] = []; + const registry = new LspMiddlewareRegistry((method, error) => { + assert.equal(method, "textDocument/hover"); + errors.push(error); + }); + registry.register("textDocument/hover", result => { + if (result) result.contents = "mutated"; + return result; + }); + const failure = new Error("Transformer failed"); + registry.register("textDocument/hover", async () => { + throw failure; + }); + registry.register("textDocument/hover", () => assert.fail("Chain must stop")); + const original = { contents: "server" }; + assert.equal(await registry.sendRequest("textDocument/hover", params, undefined, async () => original), original); + assert.deepEqual(original, { contents: "server" }); + assert.deepEqual(errors, [failure]); + }); + + test("disposal is idempotent and affects future chains, not a running chain", async () => { + const registry = new LspMiddlewareRegistry(() => assert.fail("Unexpected error")); + const gate = deferred(); + const started = deferred(); + registry.register("textDocument/hover", async result => { + started.resolve(); + await gate.promise; + return result; + }); + let calls = 0; + const disposable = registry.register("textDocument/hover", result => { + calls++; + return result; + }); + const running = registry.sendRequest("textDocument/hover", params, undefined, async () => null); + await started.promise; + disposable.dispose(); + disposable.dispose(); + gate.resolve(); + await running; + await registry.sendRequest("textDocument/hover", params, undefined, async () => null); + assert.equal(calls, 1); + }); +}); + +describe("first-party middleware", () => { + test("uses original parameters and results without cloning or freezing", async () => { + const request = { query: "symbol", local: () => "not cloneable" }; + const original = { local: () => "not cloneable" }; + const middleware: FirstPartyLspMiddleware = async (type, sentParams, token, next) => { + assert.equal(sentParams, request); + assert.equal(Object.isFrozen(sentParams), false); + return next(type, sentParams, token); + }; + const registry = new LspMiddlewareRegistry(() => assert.fail("Unexpected error"), [middleware]); + const result = await registry.sendRequest("workspace/symbol", request, CancellationToken.None, async (_type, sentParams, token) => { + assert.equal(sentParams, request); + assert.equal(token, CancellationToken.None); + return original; + }); + assert.equal(result, original); + assert.equal(Object.isFrozen(request), false); + }); + + test("supplies third-party callbacks with frozen copies of first-party request changes", async () => { + const middleware: FirstPartyLspMiddleware = (type, sentParams, token, next) => + next( + type, + Object.assign({}, sentParams, { + textDocument: { uri: "file:///workspace.ts" }, + }), + token, + ); + const registry = new LspMiddlewareRegistry(() => assert.fail("Unexpected error"), [middleware]); + const request = { query: "symbol" }; + let outgoing: unknown; + registry.register("workspace/symbol", (result, context) => { + assert.deepEqual(context.params, { query: "symbol", textDocument: { uri: "file:///workspace.ts" } }); + assert.notEqual(context.params, outgoing); + assert.ok(Object.isFrozen(context.params)); + assert.ok(Object.isFrozen(context.params.textDocument)); + return result; + }); + await registry.sendRequest("workspace/symbol", request, undefined, async (_type, sentParams) => { + outgoing = sentParams; + assert.equal(Object.isFrozen(sentParams), false); + return []; + }); + assert.deepEqual(request, { query: "symbol" }); + assert.equal(Object.isFrozen(outgoing), false); + }); + + test("runs first-party middleware around dispatch and third-party response transforms", async () => { + const order: string[] = []; + const first: FirstPartyLspMiddleware = async (type, sentParams, token, next) => { + order.push("first request"); + const result = await next(type, sentParams, token); + order.push("first response"); + return result; + }; + const second: FirstPartyLspMiddleware = async (type, sentParams, token, next) => { + order.push("second request"); + const result = await next(type, sentParams, token); + order.push("second response"); + return result; + }; + const registry = new LspMiddlewareRegistry(() => assert.fail("Unexpected error"), [first, second]); + registry.register("textDocument/hover", result => { + order.push("third-party response"); + return result; + }); + await registry.sendRequest("textDocument/hover", params, undefined, async () => { + order.push("server"); + return null; + }); + assert.deepEqual(order, ["first request", "second request", "server", "third-party response", "second response", "first response"]); + }); + + test("propagates first-party failures without invoking third-party fallback", async () => { + const failure = new Error("First-party failure"); + const middleware: FirstPartyLspMiddleware = () => Promise.reject(failure); + const registry = new LspMiddlewareRegistry(() => assert.fail("Not a third-party failure"), [middleware]); + registry.register("textDocument/hover", () => assert.fail("No response")); + await assert.rejects(registry.sendRequest("textDocument/hover", params, undefined, async () => assert.fail("No dispatch")), error => error === failure); + }); +}); + +test("transforms real protocol responses and leaves push diagnostics untouched", async t => { + const registry = new LspMiddlewareRegistry(() => assert.fail("Unexpected error")); + registry.register("textDocument/hover", result => result && { ...result, contents: "transformed hover" }); + const serverToClient = new PassThrough(); + const clientToServer = new PassThrough(); + const client = createProtocolConnection(serverToClient, clientToServer); + const server = createProtocolConnection(clientToServer, serverToClient); + t.after(() => { + client.dispose(); + server.dispose(); + serverToClient.destroy(); + clientToServer.destroy(); + }); + server.onRequest(HoverRequest.type, sentParams => { + assert.deepEqual(sentParams, params); + return { contents: "server hover" }; + }); + const original: PublishDiagnosticsParams = { + uri: params.textDocument.uri, + version: 1, + diagnostics: [{ + range: { start: { line: 0, character: 0 }, end: { line: 0, character: 1 } }, + message: "server diagnostic", + data: { preserved: true }, + }], + }; + const received = deferred(); + client.onNotification(PublishDiagnosticsNotification.type, diagnostics => { + assert.deepEqual(diagnostics, original); + received.resolve(); + }); + client.listen(); + server.listen(); + const result = await registry.sendRequest(HoverRequest.type, params, CancellationToken.None, (type, params, token) => client.sendRequest(typeof type === "string" ? type : type.method, params, token)); + assert.deepEqual(result, { contents: "transformed hover" }); + await server.sendNotification(PublishDiagnosticsNotification.type, original); + await received.promise; +}); + +function checkPublicTypes(api: ExtensionAPI): void { + api.registerLspMiddleware("textDocument/hover", (result, context) => { + // @ts-expect-error Request context cannot be mutated. + context.params.textDocument.uri = "file:///changed.ts"; + return result; + }); + // @ts-expect-error Push-diagnostic notifications cannot be intercepted. + api.registerLspMiddleware("textDocument/publishDiagnostics", result => result); + // @ts-expect-error Transformers must return the method's result type. + api.registerLspMiddleware("textDocument/hover", () => []); + // @ts-expect-error Lifecycle methods cannot be registered. + api.registerLspMiddleware("initialize", result => result); + api.registerLspMiddleware("textDocument/completion", (result, context) => { + const trigger: string | undefined = context.params.context?.triggerCharacter; + void trigger; + return result; + }); + api.registerLspMiddleware("custom/textDocument/multiDocumentHighlight", (result, context) => { + const files: readonly string[] = context.params.filesToSearch; + void files; + return result; + }); + api.registerLspMiddleware("workspace/symbol", (result, context) => { + const uri: string | undefined = context.params.textDocument?.uri; + void uri; + return result; + }); + api.registerLspMiddleware("completionItem/resolve", (result, context) => { + // @ts-expect-error Opaque protocol data must not bypass the readonly context contract. + context.params.data.property = "modified"; + return result; + }); +} +void checkPublicTypes; diff --git a/packages/vscode-typescript/test/tsconfig.json b/packages/vscode-typescript/test/tsconfig.json index e4aa2a217c5bd..baecbadc1c78c 100644 --- a/packages/vscode-typescript/test/tsconfig.json +++ b/packages/vscode-typescript/test/tsconfig.json @@ -2,5 +2,6 @@ "extends": "../tsconfig.json", "compilerOptions": { "rootDir": ".." - } + }, + "include": ["."] } From cfd035e6f7853c0c6cc57743ff9033162c1f585d Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Thu, 1 Oct 2026 14:29:27 -0700 Subject: [PATCH 02/10] Give each middleware its own copy of request context rather than freezing one copy --- packages/vscode-typescript/src/api.ts | 2 +- .../vscode-typescript/src/lspMiddleware.ts | 12 +----- .../test/lspMiddleware.test.ts | 38 ++++++++++++++----- 3 files changed, 30 insertions(+), 22 deletions(-) diff --git a/packages/vscode-typescript/src/api.ts b/packages/vscode-typescript/src/api.ts index 47cfe987fd81e..2e55df19aa254 100644 --- a/packages/vscode-typescript/src/api.ts +++ b/packages/vscode-typescript/src/api.ts @@ -130,7 +130,7 @@ export interface ExtensionAPI { /** * Changes an LSP language-feature response before conversion to VS Code objects. - * Request parameters are provided as a frozen copy. + * Each callback gets its own copy of the request parameters. * * Callbacks run in registration order. Order between extensions may vary. * If a callback fails, we log the error and use the original server response. diff --git a/packages/vscode-typescript/src/lspMiddleware.ts b/packages/vscode-typescript/src/lspMiddleware.ts index 319d8f42253f2..bf002353d3083 100644 --- a/packages/vscode-typescript/src/lspMiddleware.ts +++ b/packages/vscode-typescript/src/lspMiddleware.ts @@ -64,14 +64,6 @@ interface Registration { invoke(result: unknown, context: unknown): unknown; } -function freezeDeep(value: unknown): void { - if (typeof value !== "object" || value === null) return; - Object.freeze(value); - for (const child of Object.values(value)) { - freezeDeep(child); - } -} - export class LspMiddlewareRegistry implements Disposable { private readonly registrations = new Map(); private disposed = false; @@ -138,11 +130,9 @@ export class LspMiddlewareRegistry implements Disposable { if (!registrations?.length || token?.isCancellationRequested) return original; try { let result: unknown = structuredClone(original); - const readonlyContext: unknown = structuredClone(context); - freezeDeep(readonlyContext); for (const registration of registrations) { if (token?.isCancellationRequested) return original; - result = await registration.invoke(result, readonlyContext); + result = await registration.invoke(result, structuredClone(context)); } return token?.isCancellationRequested ? original : result as R; } diff --git a/packages/vscode-typescript/test/lspMiddleware.test.ts b/packages/vscode-typescript/test/lspMiddleware.test.ts index 8f9c87f4cae90..555752488ba87 100644 --- a/packages/vscode-typescript/test/lspMiddleware.test.ts +++ b/packages/vscode-typescript/test/lspMiddleware.test.ts @@ -32,7 +32,7 @@ describe("LSP middleware", () => { registry.register("textDocument/hover", (result, context) => { assert.deepEqual(context.params, params); assert.notEqual(context.params, params); - assert.ok(Object.isFrozen(context.params.textDocument)); + assert.equal(Object.isFrozen(context.params.textDocument), false); return result && { ...result, contents: "first" }; }); registry.register("textDocument/hover", async result => { @@ -151,19 +151,37 @@ describe("LSP middleware", () => { assert.deepEqual(await registry.sendRequest("textDocument/diagnostic", params, undefined, async () => original), original); }); - test("exposes deeply frozen detached context without modifying outgoing parameters", async () => { - const errors: unknown[] = []; - const registry = new LspMiddlewareRegistry((_method, error) => errors.push(error)); + test("isolates mutable context copies between callbacks and from outgoing parameters", async () => { + const registry = new LspMiddlewareRegistry(() => assert.fail("Unexpected error")); + let firstContext: unknown; registry.register("textDocument/hover", (result, context) => { + firstContext = context; + assert.equal(Object.isFrozen(context), false); + assert.equal(Object.isFrozen(context.params.textDocument), false); // @ts-expect-error Deliberate runtime mutation of readonly request context. context.params.textDocument.uri = "file:///modified.ts"; + // @ts-expect-error Nested context mutations must also remain isolated. + context.params.position.line = 42; + return result && { ...result, contents: "first" }; + }); + registry.register("textDocument/hover", async (result, context) => { + assert.notEqual(context, firstContext); + assert.deepEqual(context.params, params); + assert.equal(result?.contents, "first"); + // @ts-expect-error Deliberate mutation of this callback's independent copy. + context.params.textDocument.uri = "file:///second.ts"; + await Promise.resolve(); + return result; + }); + registry.register("textDocument/hover", (result, context) => { + assert.deepEqual(context.params, params); return result; }); const original = { contents: "server" }; - assert.equal(await registry.sendRequest("textDocument/hover", params, undefined, async () => original), original); + assert.deepEqual(await registry.sendRequest("textDocument/hover", params, undefined, async () => original), { contents: "first" }); assert.equal(params.textDocument.uri, "file:///test.ts"); - assert.equal(errors.length, 1); - assert.ok(errors[0] instanceof TypeError); + assert.equal(params.position.line, 0); + assert.deepEqual(original, { contents: "server" }); }); test("captures registrations on response arrival, not request dispatch", async () => { @@ -257,7 +275,7 @@ describe("first-party middleware", () => { assert.equal(Object.isFrozen(request), false); }); - test("supplies third-party callbacks with frozen copies of first-party request changes", async () => { + test("supplies third-party callbacks with independent copies of first-party request changes", async () => { const middleware: FirstPartyLspMiddleware = (type, sentParams, token, next) => next( type, @@ -272,8 +290,8 @@ describe("first-party middleware", () => { registry.register("workspace/symbol", (result, context) => { assert.deepEqual(context.params, { query: "symbol", textDocument: { uri: "file:///workspace.ts" } }); assert.notEqual(context.params, outgoing); - assert.ok(Object.isFrozen(context.params)); - assert.ok(Object.isFrozen(context.params.textDocument)); + assert.equal(Object.isFrozen(context.params), false); + assert.equal(Object.isFrozen(context.params.textDocument), false); return result; }); await registry.sendRequest("workspace/symbol", request, undefined, async (_type, sentParams) => { From 3aade68a75e9d4d21e50b3737d8825d6ce89fa97 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Thu, 1 Oct 2026 16:20:17 -0700 Subject: [PATCH 03/10] Include VS Code extension API in typescript package --- Herebyfile.mjs | 29 +- package-lock.json | 1 + packages/typescript/package.json | 3 + .../typescript/src/vscode/extensionApi.ts | 69 + .../src/vscode/protocol.generated.ts | 2913 +++++++++++++++++ packages/vscode-typescript/package.json | 1 + packages/vscode-typescript/src/api.ts | 147 - .../src/contentMapperContributions.ts | 4 +- packages/vscode-typescript/src/extension.ts | 4 +- .../vscode-typescript/src/lspMiddleware.ts | 12 +- packages/vscode-typescript/src/session.ts | 8 +- .../test/lspMiddleware.test.ts | 2 +- packages/vscode-typescript/test/tsconfig.json | 3 - packages/vscode-typescript/tsconfig.json | 5 +- tools/scripts/gen/generatedFile.test.mts | 6 +- tools/scripts/gen/lspTypeScript.test.mts | 125 + .../lsp/lsproto/_generate/generate.mts | 28 +- .../lsproto/_generate/generateTypeScript.mts | 31 + .../lsp/lsproto/_generate/typeScript.mts | 122 + 19 files changed, 3336 insertions(+), 177 deletions(-) create mode 100644 packages/typescript/src/vscode/extensionApi.ts create mode 100644 packages/typescript/src/vscode/protocol.generated.ts delete mode 100644 packages/vscode-typescript/src/api.ts create mode 100644 tools/scripts/gen/lspTypeScript.test.mts create mode 100644 tsc/internal/lsp/lsproto/_generate/generateTypeScript.mts create mode 100644 tsc/internal/lsp/lsproto/_generate/typeScript.mts diff --git a/Herebyfile.mjs b/Herebyfile.mjs index 09529b70db9c3..82ae71b370051 100644 --- a/Herebyfile.mjs +++ b/Herebyfile.mjs @@ -605,19 +605,34 @@ async function runGenerateLSP() { path.join(directory, "generate.mts"), ...modelFiles.map(file => file.fileName), ]); - if (output.isCurrent(!!options.force)) { + if (!output.isCurrent(!!options.force)) { + output.invalidate(); + const { default: generate } = await import("./tsc/internal/lsp/lsproto/_generate/generate.mts"); + await generate(); + output.markCurrent(); + } + else { console.log("LSP bindings are up to date."); - return; } - output.invalidate(); - const { default: generate } = await import("./tsc/internal/lsp/lsproto/_generate/generate.mts"); - await generate(); - output.markCurrent(); + const typeScriptOutput = new GeneratedFile(path.join(__dirname, "packages/typescript/src/vscode/protocol.generated.ts"), [ + __filename, + path.join(directory, "generate.mts"), + path.join(directory, "generateTypeScript.mts"), + path.join(directory, "typeScript.mts"), + path.join(__dirname, "packages/vscode-typescript/src/lspMiddleware.ts"), + ...modelFiles.map(file => file.fileName), + ]); + if (!typeScriptOutput.isCurrent(!!options.force)) { + typeScriptOutput.invalidate(); + const { default: generate } = await import("./tsc/internal/lsp/lsproto/_generate/generateTypeScript.mts"); + await generate(); + typeScriptOutput.markCurrent(); + } } export const generateLSP = task({ name: "generate:lsp", - description: "Generates LSP bindings from the pinned protocol model. Pass --force to regenerate unchanged files.", + description: "Generates Go LSP bindings and extension API types from the pinned protocol model. Pass --force to regenerate unchanged files.", run: runGenerateLSP, }); diff --git a/package-lock.json b/package-lock.json index 7fbd7d1c05241..b23894304b407 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3546,6 +3546,7 @@ "devDependencies": { "@types/vscode": "~1.125.0", "@typescript/bundled-typescript": "npm:typescript@7.0.2", + "@typescript/typescript": "0.0.0", "@vscode/vsce": "4.0.0", "esbuild": "^0.28.2" }, diff --git a/packages/typescript/package.json b/packages/typescript/package.json index 5e26fd27a4524..820a988b25e0c 100644 --- a/packages/typescript/package.json +++ b/packages/typescript/package.json @@ -38,6 +38,9 @@ "exports": { "./package.json": "./package.json", ".": "./lib/version.cjs", + "./unstable/vscode": { + "types": "./dist/vscode/extensionApi.d.ts" + }, "./unstable/sync": { "@typescript/source": "./src/api/sync/api.ts", "default": "./dist/api/sync/api.js" diff --git a/packages/typescript/src/vscode/extensionApi.ts b/packages/typescript/src/vscode/extensionApi.ts new file mode 100644 index 0000000000000..2e2db90b84478 --- /dev/null +++ b/packages/typescript/src/vscode/extensionApi.ts @@ -0,0 +1,69 @@ +import type { LspMiddlewareRequests } from "./protocol.generated.ts"; +export type * from "./protocol.generated.ts"; + +export interface Disposable { + dispose(): unknown; +} + +export interface Event { + (listener: (event: T) => unknown, thisArgs?: unknown, disposables?: Disposable[] | undefined): Disposable; +} + +/** The URI properties used when launching a content mapper. */ +export interface ContentMapperUri { + readonly scheme: string; + readonly fsPath: string; +} + +export interface ContentMapperManifest { + readonly name: string; + readonly version?: string | undefined; + readonly exec: readonly string[]; + readonly cwd?: ContentMapperUri | undefined; + readonly compilerOptions?: readonly string[] | undefined; + readonly dynamicConfig?: boolean | undefined; +} + +export interface ContentMapperContribution { + readonly extensions: readonly string[]; + readonly inferredProjectContribution?: { + readonly options?: Readonly> | undefined; + readonly manifest: ContentMapperManifest; + } | undefined; +} + +export type LspMiddlewareMethod = keyof LspMiddlewareRequests; + +export type DeepReadonly = unknown extends T ? unknown : T extends object ? { readonly [K in keyof T]: DeepReadonly; } : T; + +export type LspMiddlewareResult = LspMiddlewareRequests[M]["result"]; + +export type LspMiddlewareContext = { readonly params: DeepReadonly; }; + +export type LspMiddlewareTransformer = ( + result: LspMiddlewareResult, + context: LspMiddlewareContext, +) => LspMiddlewareResult | PromiseLike>; + +export interface ExtensionAPI { + onLanguageServerInitialized: Event; + initializeAPIConnection(pipe?: string): Promise; + registerContentMappers(contributorId: string, contributions: readonly ContentMapperContribution[]): Disposable; + + /** + * Changes an LSP language-feature response before conversion to VS Code objects. + * Each callback gets its own copy of the request parameters. + * + * Callbacks run in registration order. Order between extensions may vary. + * If a callback fails, we log the error and use the original server response. + * + * Registrations survive server restarts. Disposing removes the callback from + * future responses but does not interrupt a response already being processed. + * + * Requests, notifications, lifecycle messages, and partial results are excluded. + */ + registerLspMiddleware( + method: M, + transformer: LspMiddlewareTransformer>, + ): Disposable; +} diff --git a/packages/typescript/src/vscode/protocol.generated.ts b/packages/typescript/src/vscode/protocol.generated.ts new file mode 100644 index 0000000000000..e972203028a7e --- /dev/null +++ b/packages/typescript/src/vscode/protocol.generated.ts @@ -0,0 +1,2913 @@ +// Code generated by tsc/internal/lsp/lsproto/_generate/generateTypeScript.mts. DO NOT EDIT. +// LSP metamodel copyright (c) Microsoft Corporation. Licensed under the MIT License. + +/** + * A special text edit with an additional change annotation. + * + * @since 3.16.0. + */ +export interface AnnotatedTextEdit { + /** + * The range of the text document to be manipulated. To insert + * text into a document create a range where start === end. + */ + range: Range; + /** + * The string to be inserted. For delete operations use an + * empty string. + */ + newText: string; + /** + * The actual identifier of the change annotation + */ + annotationId: ChangeAnnotationIdentifier; +} + +/** + * Defines how values from a set of defaults and an individual item will be + * merged. + * + * @since 3.18.0 + */ +export type ApplyKind = 1 | 2; + +/** + * Represents an incoming call, e.g. a caller of a method or constructor. + * + * @since 3.16.0 + */ +export interface CallHierarchyIncomingCall { + /** + * The item that makes the call. + */ + from: CallHierarchyItem; + /** + * The ranges at which the calls appear. This is relative to the caller + * denoted by {@link CallHierarchyIncomingCall.from `this.from`}. + */ + fromRanges: (Range)[]; +} + +/** + * The parameter of a `callHierarchy/incomingCalls` request. + * + * @since 3.16.0 + */ +export interface CallHierarchyIncomingCallsParams { + /** + * An optional token that a server can use to report work done progress. + */ + workDoneToken?: ProgressToken | undefined; + /** + * An optional token that a server can use to report partial results (e.g. streaming) to + * the client. + */ + partialResultToken?: ProgressToken | undefined; + item: CallHierarchyItem; +} + +/** + * Represents programming constructs like functions or constructors in the context + * of call hierarchy. + * + * @since 3.16.0 + */ +export interface CallHierarchyItem { + /** + * The name of this item. + */ + name: string; + /** + * The kind of this item. + */ + kind: SymbolKind; + /** + * Tags for this item. + */ + tags?: (SymbolTag)[] | undefined; + /** + * More detail for this item, e.g. the signature of a function. + */ + detail?: string | undefined; + /** + * The resource identifier of this item. + */ + uri: DocumentUri; + /** + * The range enclosing this symbol not including leading/trailing whitespace but everything else, e.g. comments and code. + */ + range: Range; + /** + * The range that should be selected and revealed when this symbol is being picked, e.g. the name of a function. + * Must be contained by the {@link CallHierarchyItem.range `range`}. + */ + selectionRange: Range; + /** + * A data entry field that is preserved between a call hierarchy prepare and + * incoming calls or outgoing calls requests. + */ + data?: LSPAny | undefined; +} + +/** + * Represents an outgoing call, e.g. calling a getter from a method or a method from a constructor etc. + * + * @since 3.16.0 + */ +export interface CallHierarchyOutgoingCall { + /** + * The item that is called. + */ + to: CallHierarchyItem; + /** + * The range at which this item is called. This is the range relative to the caller, e.g the item + * passed to {@link CallHierarchyItemProvider.provideCallHierarchyOutgoingCalls `provideCallHierarchyOutgoingCalls`} + * and not {@link CallHierarchyOutgoingCall.to `this.to`}. + */ + fromRanges: (Range)[]; +} + +/** + * The parameter of a `callHierarchy/outgoingCalls` request. + * + * @since 3.16.0 + */ +export interface CallHierarchyOutgoingCallsParams { + /** + * An optional token that a server can use to report work done progress. + */ + workDoneToken?: ProgressToken | undefined; + /** + * An optional token that a server can use to report partial results (e.g. streaming) to + * the client. + */ + partialResultToken?: ProgressToken | undefined; + item: CallHierarchyItem; +} + +/** + * The parameter of a `textDocument/prepareCallHierarchy` request. + * + * @since 3.16.0 + */ +export interface CallHierarchyPrepareParams { + /** + * The text document. + */ + textDocument: TextDocumentIdentifier; + /** + * The position inside the text document. + */ + position: Position; + /** + * An optional token that a server can use to report work done progress. + */ + workDoneToken?: ProgressToken | undefined; +} + +/** + * Additional information that describes document changes. + * + * @since 3.16.0 + */ +export interface ChangeAnnotation { + /** + * A human-readable string describing the actual change. The string + * is rendered prominent in the user interface. + */ + label: string; + /** + * A flag which indicates that user confirmation is needed + * before applying the change. + */ + needsConfirmation?: boolean | undefined; + /** + * A human-readable string which is rendered less prominent in + * the user interface. + */ + description?: string | undefined; +} + +/** + * An identifier to refer to a change annotation stored with a workspace edit. + */ +export type ChangeAnnotationIdentifier = string; + +/** + * A code action represents a change that can be performed in code, e.g. to fix a problem or + * to refactor code. + * + * A CodeAction must set either `edit` and/or a `command`. If both are supplied, the `edit` is applied first, then the `command` is executed. + */ +export interface CodeAction { + /** + * A short, human-readable, title for this code action. + */ + title: string; + /** + * The kind of the code action. + * + * Used to filter code actions. + */ + kind?: CodeActionKind | undefined; + /** + * The diagnostics that this code action resolves. + */ + diagnostics?: (Diagnostic)[] | undefined; + /** + * Marks this as a preferred action. Preferred actions are used by the `auto fix` command and can be targeted + * by keybindings. + * + * A quick fix should be marked preferred if it properly addresses the underlying error. + * A refactoring should be marked preferred if it is the most reasonable choice of actions to take. + * + * @since 3.15.0 + */ + isPreferred?: boolean | undefined; + /** + * Marks that the code action cannot currently be applied. + * + * Clients should follow the following guidelines regarding disabled code actions: + * + * - Disabled code actions are not shown in automatic [lightbulbs](https://code.visualstudio.com/docs/editor/editingevolved#_code-action) + * code action menus. + * + * - Disabled actions are shown as faded out in the code action menu when the user requests a more specific type + * of code action, such as refactorings. + * + * - If the user has a [keybinding](https://code.visualstudio.com/docs/editor/refactoring#_keybindings-for-code-actions) + * that auto applies a code action and only disabled code actions are returned, the client should show the user an + * error message with `reason` in the editor. + * + * @since 3.16.0 + */ + disabled?: CodeActionDisabled | undefined; + /** + * The workspace edit this code action performs. + */ + edit?: WorkspaceEdit | undefined; + /** + * A command this code action executes. If a code action + * provides an edit and a command, first the edit is + * executed and then the command. + */ + command?: Command | undefined; + /** + * A data entry field that is preserved on a code action between + * a `textDocument/codeAction` and a `codeAction/resolve` request. + * + * @since 3.16.0 + */ + data?: LSPAny | undefined; + /** + * Tags for this code action. + * + * @since 3.18.0 + */ + tags?: (CodeActionTag)[] | undefined; +} + +/** + * Contains additional diagnostic information about the context in which + * a {@link CodeActionProvider.provideCodeActions code action} is run. + */ +export interface CodeActionContext { + /** + * An array of diagnostics known on the client side overlapping the range provided to the + * `textDocument/codeAction` request. They are provided so that the server knows which + * errors are currently presented to the user for the given range. There is no guarantee + * that these accurately reflect the error state of the resource. The primary parameter + * to compute code actions is the provided range. + */ + diagnostics: (Diagnostic)[]; + /** + * Requested kind of actions to return. + * + * Actions not of this kind are filtered out by the client before being shown. So servers + * can omit computing them. + */ + only?: (CodeActionKind)[] | undefined; + /** + * The reason why code actions were requested. + * + * @since 3.17.0 + */ + triggerKind?: CodeActionTriggerKind | undefined; +} + +/** + * Captures why the code action is currently disabled. + * + * @since 3.18.0 + */ +export interface CodeActionDisabled { + /** + * Human readable description of why the code action is currently disabled. + * + * This is displayed in the code actions UI. + */ + reason: string; +} + +/** + * A set of predefined code action kinds + */ +export type CodeActionKind = "" | "quickfix" | "refactor" | "refactor.extract" | "refactor.inline" | "refactor.move" | "refactor.rewrite" | "source" | "source.organizeImports" | "source.fixAll" | "notebook" | string; + +/** + * The parameters of a {@link CodeActionRequest}. + */ +export interface CodeActionParams { + /** + * An optional token that a server can use to report work done progress. + */ + workDoneToken?: ProgressToken | undefined; + /** + * An optional token that a server can use to report partial results (e.g. streaming) to + * the client. + */ + partialResultToken?: ProgressToken | undefined; + /** + * The document in which the command was invoked. + */ + textDocument: TextDocumentIdentifier; + /** + * The range for which the command was invoked. + */ + range: Range; + /** + * Context carrying additional information. + */ + context: CodeActionContext; +} + +/** + * Code action tags are extra annotations that tweak the behavior of a code action. + * + * @since 3.18.0 + */ +export type CodeActionTag = 1; + +/** + * The reason why code actions were requested. + * + * @since 3.17.0 + */ +export type CodeActionTriggerKind = 1 | 2; + +/** + * Structure to capture a description for an error code. + * + * @since 3.16.0 + */ +export interface CodeDescription { + /** + * An URI to open with more information about the diagnostic error. + */ + href: URI; +} + +/** + * A code lens represents a {@link Command command} that should be shown along with + * source text, like the number of references, a way to run tests, etc. + * + * A code lens is _unresolved_ when no command is associated to it. For performance + * reasons the creation of a code lens and resolving should be done in two stages. + */ +export interface CodeLens { + /** + * The range in which this code lens is valid. Should only span a single line. + */ + range: Range; + /** + * The command this code lens represents. + */ + command?: Command | undefined; + /** + * A data entry field that is preserved on a code lens item between + * a {@link CodeLensRequest} and a {@link CodeLensResolveRequest} + */ + data?: LSPAny | undefined; +} + +/** + * The parameters of a {@link CodeLensRequest}. + */ +export interface CodeLensParams { + /** + * An optional token that a server can use to report work done progress. + */ + workDoneToken?: ProgressToken | undefined; + /** + * An optional token that a server can use to report partial results (e.g. streaming) to + * the client. + */ + partialResultToken?: ProgressToken | undefined; + /** + * The document to request code lens for. + */ + textDocument: TextDocumentIdentifier; +} + +/** + * Represents a reference to a command. Provides a title which + * will be used to represent a command in the UI and, optionally, + * an array of arguments which will be passed to the command handler + * function when invoked. + */ +export interface Command { + /** + * Title of the command, like `save`. + */ + title: string; + /** + * An optional tooltip. + * + * @since 3.18.0 + */ + tooltip?: string | undefined; + /** + * The identifier of the actual command handler. + */ + command: string; + /** + * Arguments that the command handler should be + * invoked with. + */ + arguments?: (LSPAny)[] | undefined; +} + +/** + * Contains additional information about the context in which a completion request is triggered. + */ +export interface CompletionContext { + /** + * How the completion was triggered. + */ + triggerKind: CompletionTriggerKind; + /** + * The trigger character (a single character) that has trigger code complete. + * Is undefined if `triggerKind !== CompletionTriggerKind.TriggerCharacter` + */ + triggerCharacter?: string | undefined; +} + +/** + * A completion item represents a text snippet that is + * proposed to complete text that is being typed. + */ +export interface CompletionItem { + /** + * The label of this completion item. + * + * The label property is also by default the text that + * is inserted when selecting this completion. + * + * If label details are provided the label itself should + * be an unqualified name of the completion item. + */ + label: string; + /** + * Additional details for the label + * + * @since 3.17.0 + */ + labelDetails?: CompletionItemLabelDetails | undefined; + /** + * The kind of this completion item. Based of the kind + * an icon is chosen by the editor. + */ + kind?: CompletionItemKind | undefined; + /** + * Tags for this completion item. + * + * @since 3.15.0 + */ + tags?: (CompletionItemTag)[] | undefined; + /** + * A human-readable string with additional information + * about this item, like type or symbol information. + */ + detail?: string | undefined; + /** + * A human-readable string that represents a doc-comment. + */ + documentation?: (string | MarkupContent) | undefined; + /** + * Indicates if this item is deprecated. + * @deprecated Use `tags` instead. + */ + deprecated?: boolean | undefined; + /** + * Select this item when showing. + * + * *Note* that only one completion item can be selected and that the + * tool / client decides which item that is. The rule is that the *first* + * item of those that match best is selected. + */ + preselect?: boolean | undefined; + /** + * A string that should be used when comparing this item + * with other items. When `falsy` the {@link CompletionItem.label label} + * is used. + */ + sortText?: string | undefined; + /** + * A string that should be used when filtering a set of + * completion items. When `falsy` the {@link CompletionItem.label label} + * is used. + */ + filterText?: string | undefined; + /** + * A string that should be inserted into a document when selecting + * this completion. When `falsy` the {@link CompletionItem.label label} + * is used. + * + * The `insertText` is subject to interpretation by the client side. + * Some tools might not take the string literally. For example + * VS Code when code complete is requested in this example + * `con` and a completion item with an `insertText` of + * `console` is provided it will only insert `sole`. Therefore it is + * recommended to use `textEdit` instead since it avoids additional client + * side interpretation. + */ + insertText?: string | undefined; + /** + * The format of the insert text. The format applies to both the + * `insertText` property and the `newText` property of a provided + * `textEdit`. If omitted defaults to `InsertTextFormat.PlainText`. + * + * Please note that the insertTextFormat doesn't apply to + * `additionalTextEdits`. + */ + insertTextFormat?: InsertTextFormat | undefined; + /** + * How whitespace and indentation is handled during completion + * item insertion. If not provided the clients default value depends on + * the `textDocument.completion.insertTextMode` client capability. + * + * @since 3.16.0 + */ + insertTextMode?: InsertTextMode | undefined; + /** + * An {@link TextEdit edit} which is applied to a document when selecting + * this completion. When an edit is provided the value of + * {@link CompletionItem.insertText insertText} is ignored. + * + * Most editors support two different operations when accepting a completion + * item. One is to insert a completion text and the other is to replace an + * existing text with a completion text. Since this can usually not be + * predetermined by a server it can report both ranges. Clients need to + * signal support for `InsertReplaceEdits` via the + * `textDocument.completion.insertReplaceSupport` client capability + * property. + * + * *Note 1:* The text edit's range as well as both ranges from an insert + * replace edit must be a [single line] and they must contain the position + * at which completion has been requested. + * *Note 2:* If an `InsertReplaceEdit` is returned the edit's insert range + * must be a prefix of the edit's replace range, that means it must be + * contained and starting at the same position. + * + * @since 3.16.0 additional type `InsertReplaceEdit` + */ + textEdit?: (TextEdit | InsertReplaceEdit) | undefined; + /** + * The edit text used if the completion item is part of a CompletionList and + * CompletionList defines an item default for the text edit range. + * + * Clients will only honor this property if they opt into completion list + * item defaults using the capability `completionList.itemDefaults`. + * + * If not provided and a list's default range is provided the label + * property is used as a text. + * + * @since 3.17.0 + */ + textEditText?: string | undefined; + /** + * An optional array of additional {@link TextEdit text edits} that are applied when + * selecting this completion. Edits must not overlap (including the same insert position) + * with the main {@link CompletionItem.textEdit edit} nor with themselves. + * + * Additional text edits should be used to change text unrelated to the current cursor position + * (for example adding an import statement at the top of the file if the completion item will + * insert an unqualified type). + */ + additionalTextEdits?: (TextEdit)[] | undefined; + /** + * An optional set of characters that when pressed while this completion is active will accept it first and + * then type that character. *Note* that all commit characters should have `length=1` and that superfluous + * characters will be ignored. + */ + commitCharacters?: (string)[] | undefined; + /** + * An optional {@link Command command} that is executed *after* inserting this completion. *Note* that + * additional modifications to the current document should be described with the + * {@link CompletionItem.additionalTextEdits additionalTextEdits}-property. + */ + command?: Command | undefined; + /** + * A data entry field that is preserved on a completion item between a + * {@link CompletionRequest} and a {@link CompletionResolveRequest}. + */ + data?: LSPAny | undefined; +} + +/** + * Specifies how fields from a completion item should be combined with those + * from `completionList.itemDefaults`. + * + * If unspecified, all fields will be treated as ApplyKind.Replace. + * + * If a field's value is ApplyKind.Replace, the value from a completion item (if + * provided and not `null`) will always be used instead of the value from + * `completionItem.itemDefaults`. + * + * If a field's value is ApplyKind.Merge, the values will be merged using the rules + * defined against each field below. + * + * Servers are only allowed to return `applyKind` if the client + * signals support for this via the `completionList.applyKindSupport` + * capability. + * + * @since 3.18.0 + */ +export interface CompletionItemApplyKinds { + /** + * Specifies whether commitCharacters on a completion will replace or be + * merged with those in `completionList.itemDefaults.commitCharacters`. + * + * If ApplyKind.Replace, the commit characters from the completion item will + * always be used unless not provided, in which case those from + * `completionList.itemDefaults.commitCharacters` will be used. An + * empty list can be used if a completion item does not have any commit + * characters and also should not use those from + * `completionList.itemDefaults.commitCharacters`. + * + * If ApplyKind.Merge the commitCharacters for the completion will be the + * union of all values in both `completionList.itemDefaults.commitCharacters` + * and the completion's own `commitCharacters`. + * + * @since 3.18.0 + */ + commitCharacters?: ApplyKind | undefined; + /** + * Specifies whether the `data` field on a completion will replace or + * be merged with data from `completionList.itemDefaults.data`. + * + * If ApplyKind.Replace, the data from the completion item will be used if + * provided (and not `null`), otherwise + * `completionList.itemDefaults.data` will be used. An empty object can + * be used if a completion item does not have any data but also should + * not use the value from `completionList.itemDefaults.data`. + * + * If ApplyKind.Merge, a shallow merge will be performed between + * `completionList.itemDefaults.data` and the completion's own data + * using the following rules: + * + * - If a completion's `data` field is not provided (or `null`), the + * entire `data` field from `completionList.itemDefaults.data` will be + * used as-is. + * - If a completion's `data` field is provided, each field will + * overwrite the field of the same name in + * `completionList.itemDefaults.data` but no merging of nested fields + * within that value will occur. + * + * @since 3.18.0 + */ + data?: ApplyKind | undefined; +} + +/** + * In many cases the items of an actual completion result share the same + * value for properties like `commitCharacters` or the range of a text + * edit. A completion list can therefore define item defaults which will + * be used if a completion item itself doesn't specify the value. + * + * If a completion list specifies a default value and a completion item + * also specifies a corresponding value, the rules for combining these are + * defined by `applyKinds` (if the client supports it), defaulting to + * ApplyKind.Replace. + * + * Servers are only allowed to return default values if the client + * signals support for this via the `completionList.itemDefaults` + * capability. + * + * @since 3.17.0 + */ +export interface CompletionItemDefaults { + /** + * A default commit character set. + * + * @since 3.17.0 + */ + commitCharacters?: (string)[] | undefined; + /** + * A default edit range. + * + * @since 3.17.0 + */ + editRange?: (Range | EditRangeWithInsertReplace) | undefined; + /** + * A default insert text format. + * + * @since 3.17.0 + */ + insertTextFormat?: InsertTextFormat | undefined; + /** + * A default insert text mode. + * + * @since 3.17.0 + */ + insertTextMode?: InsertTextMode | undefined; + /** + * A default data value. + * + * @since 3.17.0 + */ + data?: LSPAny | undefined; +} + +/** + * The kind of a completion entry. + */ +export type CompletionItemKind = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25; + +/** + * Additional details for a completion item label. + * + * @since 3.17.0 + */ +export interface CompletionItemLabelDetails { + /** + * An optional string which is rendered less prominently directly after {@link CompletionItem.label label}, + * without any spacing. Should be used for function signatures and type annotations. + */ + detail?: string | undefined; + /** + * An optional string which is rendered less prominently after {@link CompletionItem.detail}. Should be used + * for fully qualified names and file paths. + */ + description?: string | undefined; +} + +/** + * Completion item tags are extra annotations that tweak the rendering of a completion + * item. + * + * @since 3.15.0 + */ +export type CompletionItemTag = 1; + +/** + * Represents a collection of {@link CompletionItem completion items} to be presented + * in the editor. + */ +export interface CompletionList { + /** + * This list it not complete. Further typing results in recomputing this list. + * + * Recomputed lists have all their items replaced (not appended) in the + * incomplete completion sessions. + */ + isIncomplete: boolean; + /** + * In many cases the items of an actual completion result share the same + * value for properties like `commitCharacters` or the range of a text + * edit. A completion list can therefore define item defaults which will + * be used if a completion item itself doesn't specify the value. + * + * If a completion list specifies a default value and a completion item + * also specifies a corresponding value, the rules for combining these are + * defined by `applyKinds` (if the client supports it), defaulting to + * ApplyKind.Replace. + * + * Servers are only allowed to return default values if the client + * signals support for this via the `completionList.itemDefaults` + * capability. + * + * @since 3.17.0 + */ + itemDefaults?: CompletionItemDefaults | undefined; + /** + * Specifies how fields from a completion item should be combined with those + * from `completionList.itemDefaults`. + * + * If unspecified, all fields will be treated as ApplyKind.Replace. + * + * If a field's value is ApplyKind.Replace, the value from a completion item + * (if provided and not `null`) will always be used instead of the value + * from `completionItem.itemDefaults`. + * + * If a field's value is ApplyKind.Merge, the values will be merged using + * the rules defined against each field below. + * + * Servers are only allowed to return `applyKind` if the client + * signals support for this via the `completionList.applyKindSupport` + * capability. + * + * @since 3.18.0 + */ + applyKind?: CompletionItemApplyKinds | undefined; + /** + * The completion items. + */ + items: (CompletionItem)[]; +} + +/** + * Completion parameters + */ +export interface CompletionParams { + /** + * The text document. + */ + textDocument: TextDocumentIdentifier; + /** + * The position inside the text document. + */ + position: Position; + /** + * An optional token that a server can use to report work done progress. + */ + workDoneToken?: ProgressToken | undefined; + /** + * An optional token that a server can use to report partial results (e.g. streaming) to + * the client. + */ + partialResultToken?: ProgressToken | undefined; + /** + * The completion context. This is only available it the client specifies + * to send this using the client capability `textDocument.completion.contextSupport === true` + */ + context?: CompletionContext | undefined; +} + +/** + * How a completion was triggered + */ +export type CompletionTriggerKind = 1 | 2 | 3; + +/** + * Create file operation. + */ +export interface CreateFile { + /** + * A create + */ + kind: "create"; + /** + * An optional annotation identifier describing the operation. + * + * @since 3.16.0 + */ + annotationId?: ChangeAnnotationIdentifier | undefined; + /** + * The resource to create. + */ + uri: DocumentUri; + /** + * Additional options + */ + options?: CreateFileOptions | undefined; +} + +/** + * Options to create a file. + */ +export interface CreateFileOptions { + /** + * Overwrite existing file. Overwrite wins over `ignoreIfExists` + */ + overwrite?: boolean | undefined; + /** + * Ignore if exists. + */ + ignoreIfExists?: boolean | undefined; +} + +/** + * The definition of a symbol represented as one or many {@link Location locations}. + * For most programming languages there is only one location at which a symbol is + * defined. + * + * Servers should prefer returning `DefinitionLink` over `Definition` if supported + * by the client. + */ +export type Definition = Location | (Location)[]; + +/** + * Information about where a symbol is defined. + * + * Provides additional metadata over normal {@link Location location} definitions, including the range of + * the defining symbol + */ +export type DefinitionLink = LocationLink; + +/** + * Parameters for a {@link DefinitionRequest}. + */ +export interface DefinitionParams { + /** + * The text document. + */ + textDocument: TextDocumentIdentifier; + /** + * The position inside the text document. + */ + position: Position; + /** + * An optional token that a server can use to report work done progress. + */ + workDoneToken?: ProgressToken | undefined; + /** + * An optional token that a server can use to report partial results (e.g. streaming) to + * the client. + */ + partialResultToken?: ProgressToken | undefined; +} + +/** + * Delete file operation + */ +export interface DeleteFile { + /** + * A delete + */ + kind: "delete"; + /** + * An optional annotation identifier describing the operation. + * + * @since 3.16.0 + */ + annotationId?: ChangeAnnotationIdentifier | undefined; + /** + * The file to delete. + */ + uri: DocumentUri; + /** + * Delete options. + */ + options?: DeleteFileOptions | undefined; +} + +/** + * Delete file options + */ +export interface DeleteFileOptions { + /** + * Delete the content recursively if a folder is denoted. + */ + recursive?: boolean | undefined; + /** + * Ignore the operation if the file doesn't exist. + */ + ignoreIfNotExists?: boolean | undefined; +} + +/** + * Represents a diagnostic, such as a compiler error or warning. Diagnostic objects + * are only valid in the scope of a resource. + */ +export interface Diagnostic { + /** + * The range at which the message applies + */ + range: Range; + /** + * The diagnostic's severity. To avoid interpretation mismatches when a + * server is used with different clients it is highly recommended that servers + * always provide a severity value. + */ + severity?: DiagnosticSeverity | undefined; + /** + * The diagnostic's code, which usually appear in the user interface. + */ + code?: (number | string) | undefined; + /** + * An optional property to describe the error code. + * Requires the code field (above) to be present/not null. + * + * @since 3.16.0 + */ + codeDescription?: CodeDescription | undefined; + /** + * A human-readable string describing the source of this + * diagnostic, e.g. 'typescript' or 'super lint'. It usually + * appears in the user interface. + */ + source?: string | undefined; + /** + * The diagnostic's message. It usually appears in the user interface. + * + * @since 3.18.0 - support for MarkupContent. This is guarded by the client + * capability `textDocument.diagnostic.markupMessageSupport`. + */ + message: string | MarkupContent; + /** + * Additional metadata about the diagnostic. + * + * @since 3.15.0 + */ + tags?: (DiagnosticTag)[] | undefined; + /** + * An array of related diagnostic information, e.g. when symbol-names within + * a scope collide all definitions can be marked via this property. + */ + relatedInformation?: (DiagnosticRelatedInformation)[] | undefined; + /** + * A data entry field that is preserved between a `textDocument/publishDiagnostics` + * notification and `textDocument/codeAction` request. + * + * @since 3.16.0 + */ + data?: LSPAny | undefined; +} + +/** + * Represents a related message and source code location for a diagnostic. This should be + * used to point to code locations that cause or related to a diagnostics, e.g when duplicating + * a symbol in a scope. + */ +export interface DiagnosticRelatedInformation { + /** + * The location of this related diagnostic information. + */ + location: Location; + /** + * The message of this related diagnostic information. + */ + message: string; +} + +/** + * The diagnostic's severity. + */ +export type DiagnosticSeverity = 1 | 2 | 3 | 4; + +/** + * The diagnostic tags. + * + * @since 3.15.0 + */ +export type DiagnosticTag = 1 | 2; + +/** + * Parameters of the document diagnostic request. + * + * @since 3.17.0 + */ +export interface DocumentDiagnosticParams { + /** + * An optional token that a server can use to report work done progress. + */ + workDoneToken?: ProgressToken | undefined; + /** + * An optional token that a server can use to report partial results (e.g. streaming) to + * the client. + */ + partialResultToken?: ProgressToken | undefined; + /** + * The text document. + */ + textDocument: TextDocumentIdentifier; + /** + * The additional identifier provided during registration. + */ + identifier?: string | undefined; + /** + * The result id of a previous response if provided. + */ + previousResultId?: string | undefined; +} + +/** + * The result of a document diagnostic pull request. A report can + * either be a full report containing all diagnostics for the + * requested document or an unchanged report indicating that nothing + * has changed in terms of diagnostics in comparison to the last + * pull request. + * + * @since 3.17.0 + */ +export type DocumentDiagnosticReport = RelatedFullDocumentDiagnosticReport | RelatedUnchangedDocumentDiagnosticReport; + +/** + * The parameters of a {@link DocumentFormattingRequest}. + */ +export interface DocumentFormattingParams { + /** + * An optional token that a server can use to report work done progress. + */ + workDoneToken?: ProgressToken | undefined; + /** + * The document to format. + */ + textDocument: TextDocumentIdentifier; + /** + * The format options. + */ + options: FormattingOptions; +} + +/** + * A document highlight is a range inside a text document which deserves + * special attention. Usually a document highlight is visualized by changing + * the background color of its range. + */ +export interface DocumentHighlight { + /** + * The range this highlight applies to. + */ + range: Range; + /** + * The highlight kind, default is {@link DocumentHighlightKind.Text text}. + */ + kind?: DocumentHighlightKind | undefined; +} + +/** + * A document highlight kind. + */ +export type DocumentHighlightKind = 1 | 2 | 3; + +/** + * Parameters for a {@link DocumentHighlightRequest}. + */ +export interface DocumentHighlightParams { + /** + * The text document. + */ + textDocument: TextDocumentIdentifier; + /** + * The position inside the text document. + */ + position: Position; + /** + * An optional token that a server can use to report work done progress. + */ + workDoneToken?: ProgressToken | undefined; + /** + * An optional token that a server can use to report partial results (e.g. streaming) to + * the client. + */ + partialResultToken?: ProgressToken | undefined; +} + +/** + * The parameters of a {@link DocumentOnTypeFormattingRequest}. + */ +export interface DocumentOnTypeFormattingParams { + /** + * The document to format. + */ + textDocument: TextDocumentIdentifier; + /** + * The position around which the on type formatting should happen. + * This is not necessarily the exact position where the character denoted + * by the property `ch` got typed. + */ + position: Position; + /** + * The character that has been typed that triggered the formatting + * on type request. That is not necessarily the last character that + * got inserted into the document since the client could auto insert + * characters as well (e.g. like automatic brace completion). + */ + ch: string; + /** + * The formatting options. + */ + options: FormattingOptions; +} + +/** + * The parameters of a {@link DocumentRangeFormattingRequest}. + */ +export interface DocumentRangeFormattingParams { + /** + * An optional token that a server can use to report work done progress. + */ + workDoneToken?: ProgressToken | undefined; + /** + * The document to format. + */ + textDocument: TextDocumentIdentifier; + /** + * The range to format + */ + range: Range; + /** + * The format options + */ + options: FormattingOptions; +} + +/** + * Represents programming constructs like variables, classes, interfaces etc. + * that appear in a document. Document symbols can be hierarchical and they + * have two ranges: one that encloses its definition and one that points to + * its most interesting range, e.g. the range of an identifier. + */ +export interface DocumentSymbol { + /** + * The name of this symbol. Will be displayed in the user interface and therefore must not be + * an empty string or a string only consisting of white spaces. + */ + name: string; + /** + * More detail for this symbol, e.g the signature of a function. + */ + detail?: string | undefined; + /** + * The kind of this symbol. + */ + kind: SymbolKind; + /** + * Tags for this document symbol. + * + * @since 3.16.0 + */ + tags?: (SymbolTag)[] | undefined; + /** + * Indicates if this symbol is deprecated. + * + * @deprecated Use tags instead + */ + deprecated?: boolean | undefined; + /** + * The range enclosing this symbol not including leading/trailing whitespace but everything else + * like comments. This information is typically used to determine if the clients cursor is + * inside the symbol to reveal in the symbol in the UI. + */ + range: Range; + /** + * The range that should be selected and revealed when this symbol is being picked, e.g the name of a function. + * Must be contained by the `range`. + */ + selectionRange: Range; + /** + * Children of this symbol, e.g. properties of a class. + */ + children?: (DocumentSymbol)[] | undefined; +} + +/** + * Parameters for a {@link DocumentSymbolRequest}. + */ +export interface DocumentSymbolParams { + /** + * An optional token that a server can use to report work done progress. + */ + workDoneToken?: ProgressToken | undefined; + /** + * An optional token that a server can use to report partial results (e.g. streaming) to + * the client. + */ + partialResultToken?: ProgressToken | undefined; + /** + * The text document. + */ + textDocument: TextDocumentIdentifier; +} + +export type DocumentUri = string; + +/** + * Edit range variant that includes ranges for insert and replace operations. + * + * @since 3.18.0 + */ +export interface EditRangeWithInsertReplace { + insert: Range; + replace: Range; +} + +/** + * Represents a folding range. To be valid, start and end line must be bigger than zero and smaller + * than the number of lines in the document. Clients are free to ignore invalid ranges. + */ +export interface FoldingRange { + /** + * The zero-based start line of the range to fold. The folded area starts after the line's last character. + * To be valid, the end must be zero or larger and smaller than the number of lines in the document. + */ + startLine: number; + /** + * The zero-based character offset from where the folded range starts. If not defined, defaults to the length of the start line. + */ + startCharacter?: number | undefined; + /** + * The zero-based end line of the range to fold. The folded area ends with the line's last character. + * To be valid, the end must be zero or larger and smaller than the number of lines in the document. + */ + endLine: number; + /** + * The zero-based character offset before the folded range ends. If not defined, defaults to the length of the end line. + */ + endCharacter?: number | undefined; + /** + * Describes the kind of the folding range such as 'comment' or 'region'. The kind + * is used to categorize folding ranges and used by commands like 'Fold all comments'. + * See {@link FoldingRangeKind} for an enumeration of standardized kinds. + */ + kind?: FoldingRangeKind | undefined; + /** + * The text that the client should show when the specified range is + * collapsed. If not defined or not supported by the client, a default + * will be chosen by the client. + * + * @since 3.17.0 + */ + collapsedText?: string | undefined; +} + +/** + * A set of predefined range kinds. + */ +export type FoldingRangeKind = "comment" | "imports" | "region" | string; + +/** + * Parameters for a {@link FoldingRangeRequest}. + */ +export interface FoldingRangeParams { + /** + * An optional token that a server can use to report work done progress. + */ + workDoneToken?: ProgressToken | undefined; + /** + * An optional token that a server can use to report partial results (e.g. streaming) to + * the client. + */ + partialResultToken?: ProgressToken | undefined; + /** + * The text document. + */ + textDocument: TextDocumentIdentifier; +} + +/** + * Value-object describing what options formatting should use. + */ +export interface FormattingOptions { + /** + * Size of a tab in spaces. + */ + tabSize: number; + /** + * Prefer spaces over tabs. + */ + insertSpaces: boolean; + /** + * Trim trailing whitespace on a line. + * + * @since 3.15.0 + */ + trimTrailingWhitespace?: boolean | undefined; + /** + * Insert a newline character at the end of the file if one does not exist. + * + * @since 3.15.0 + */ + insertFinalNewline?: boolean | undefined; + /** + * Trim all newlines after the final newline at the end of the file. + * + * @since 3.15.0 + */ + trimFinalNewlines?: boolean | undefined; + [key: string]: boolean | number | string | undefined; +} + +/** + * A diagnostic report with a full set of problems. + * + * @since 3.17.0 + */ +export interface FullDocumentDiagnosticReport { + /** + * A full document diagnostic report. + */ + kind: "full"; + /** + * An optional result id. If provided it will + * be sent on the next diagnostic request for the + * same document. + */ + resultId?: string | undefined; + /** + * The actual items. + */ + items: (Diagnostic)[]; +} + +/** + * The result of a hover request. + */ +export interface Hover { + /** + * The hover's content + */ + contents: MarkupContent | MarkedString | (MarkedString)[]; + /** + * An optional range inside the text document that is used to + * visualize the hover, e.g. by changing the background color. + */ + range?: Range | undefined; + /** + * Whether the verbosity level can be increased for this hover. + */ + canIncreaseVerbosity?: boolean | undefined; + /** + * VS-specific rich content (symbol icon + colorized/classified text) rendered by clients that support Visual Studio extensions, in place of `contents`. + */ + _vs_rawContent?: VSContainerElement | undefined; +} + +/** + * Parameters for a {@link HoverRequest}. + */ +export interface HoverParams { + /** + * The text document. + */ + textDocument: TextDocumentIdentifier; + /** + * The position inside the text document. + */ + position: Position; + /** + * An optional token that a server can use to report work done progress. + */ + workDoneToken?: ProgressToken | undefined; + /** + * Controls how many levels of type definitions will be expanded. Default is 0. + */ + verbosityLevel?: number | undefined; +} + +export interface ImplementationParams { + /** + * The text document. + */ + textDocument: TextDocumentIdentifier; + /** + * The position inside the text document. + */ + position: Position; + /** + * An optional token that a server can use to report work done progress. + */ + workDoneToken?: ProgressToken | undefined; + /** + * An optional token that a server can use to report partial results (e.g. streaming) to + * the client. + */ + partialResultToken?: ProgressToken | undefined; +} + +/** + * Inlay hint information. + * + * @since 3.17.0 + */ +export interface InlayHint { + /** + * The position of this hint. + * + * If multiple hints have the same position, they will be shown in the order + * they appear in the response. + */ + position: Position; + /** + * The label of this hint. A human readable string or an array of + * InlayHintLabelPart label parts. + * + * *Note* that neither the string nor the label part can be empty. + */ + label: string | (InlayHintLabelPart)[]; + /** + * The kind of this hint. Can be omitted in which case the client + * should fall back to a reasonable default. + */ + kind?: InlayHintKind | undefined; + /** + * Optional text edits that are performed when accepting this inlay hint. + * + * *Note* that edits are expected to change the document so that the inlay + * hint (or its nearest variant) is now part of the document and the inlay + * hint itself is now obsolete. + */ + textEdits?: (TextEdit)[] | undefined; + /** + * The tooltip text when you hover over this item. + */ + tooltip?: (string | MarkupContent) | undefined; + /** + * Render padding before the hint. + * + * Note: Padding should use the editor's background color, not the + * background color of the hint itself. That means padding can be used + * to visually align/separate an inlay hint. + */ + paddingLeft?: boolean | undefined; + /** + * Render padding after the hint. + * + * Note: Padding should use the editor's background color, not the + * background color of the hint itself. That means padding can be used + * to visually align/separate an inlay hint. + */ + paddingRight?: boolean | undefined; + /** + * A data entry field that is preserved on an inlay hint between + * a `textDocument/inlayHint` and a `inlayHint/resolve` request. + */ + data?: LSPAny | undefined; +} + +/** + * Inlay hint kinds. + * + * @since 3.17.0 + */ +export type InlayHintKind = 1 | 2; + +/** + * An inlay hint label part allows for interactive and composite labels + * of inlay hints. + * + * @since 3.17.0 + */ +export interface InlayHintLabelPart { + /** + * The value of this label part. + */ + value: string; + /** + * The tooltip text when you hover over this label part. Depending on + * the client capability `inlayHint.resolveSupport` clients might resolve + * this property late using the resolve request. + */ + tooltip?: (string | MarkupContent) | undefined; + /** + * An optional source code location that represents this + * label part. + * + * The editor will use this location for the hover and for code navigation + * features: This part will become a clickable link that resolves to the + * definition of the symbol at the given location (not necessarily the + * location itself), it shows the hover that shows at the given location, + * and it shows a context menu with further code navigation commands. + * + * Depending on the client capability `inlayHint.resolveSupport` clients + * might resolve this property late using the resolve request. + */ + location?: Location | undefined; + /** + * An optional command for this label part. + * + * Depending on the client capability `inlayHint.resolveSupport` clients + * might resolve this property late using the resolve request. + */ + command?: Command | undefined; +} + +/** + * A parameter literal used in inlay hint requests. + * + * @since 3.17.0 + */ +export interface InlayHintParams { + /** + * An optional token that a server can use to report work done progress. + */ + workDoneToken?: ProgressToken | undefined; + /** + * The text document. + */ + textDocument: TextDocumentIdentifier; + /** + * The document range for which inlay hints should be computed. + */ + range: Range; +} + +/** + * A special text edit to provide an insert and a replace operation. + * + * @since 3.16.0 + */ +export interface InsertReplaceEdit { + /** + * The string to be inserted. + */ + newText: string; + /** + * The range if the insert is requested + */ + insert: Range; + /** + * The range if the replace is requested. + */ + replace: Range; +} + +/** + * Defines whether the insert text in a completion item should be interpreted as + * plain text or a snippet. + */ +export type InsertTextFormat = 1 | 2; + +/** + * How whitespace and indentation is handled during completion + * item insertion. + * + * @since 3.16.0 + */ +export type InsertTextMode = 1 | 2; + +/** + * The LSP any type. + * Please note that strictly speaking a property with the value `undefined` + * can't be converted into JSON preserving the property name. However for + * convenience it is allowed and assumed that all these properties are + * optional as well. + * @since 3.17.0 + */ +export type LSPAny = LSPObject | LSPArray | string | number | boolean | null; + +/** + * LSP arrays. + * @since 3.17.0 + */ +export type LSPArray = (LSPAny)[]; + +/** + * LSP object definition. + * @since 3.17.0 + */ +export type LSPObject = { [key: string]: LSPAny; }; + +export interface LinkedEditingRangeParams { + /** + * The text document. + */ + textDocument: TextDocumentIdentifier; + /** + * The position inside the text document. + */ + position: Position; + /** + * An optional token that a server can use to report work done progress. + */ + workDoneToken?: ProgressToken | undefined; +} + +/** + * The result of a linked editing range request. + * + * @since 3.16.0 + */ +export interface LinkedEditingRanges { + /** + * A list of ranges that can be edited together. The ranges must have + * identical length and contain identical text content. The ranges cannot overlap. + */ + ranges: (Range)[]; + /** + * An optional word pattern (regular expression) that describes valid contents for + * the given ranges. If no pattern is provided, the client configuration's word + * pattern will be used. + */ + wordPattern?: string | undefined; +} + +/** + * Represents a location inside a resource, such as a line + * inside a text file. + */ +export interface Location { + uri: DocumentUri; + range: Range; +} + +/** + * Represents the connection of two locations. Provides additional metadata over normal {@link Location locations}, + * including an origin range. + */ +export interface LocationLink { + /** + * Span of the origin of this link. + * + * Used as the underlined span for mouse interaction. Defaults to the word range at + * the definition position. + */ + originSelectionRange?: Range | undefined; + /** + * The target resource identifier of this link. + */ + targetUri: DocumentUri; + /** + * The full target range of this link. If the target for example is a symbol then target range is the + * range enclosing this symbol not including leading/trailing whitespace but everything else + * like comments. This information is typically used to highlight the range in the editor. + */ + targetRange: Range; + /** + * The range that should be selected and revealed when this link is being followed, e.g the name of a function. + * Must be contained by the `targetRange`. See also `DocumentSymbol#range` + */ + targetSelectionRange: Range; +} + +/** + * Location with only uri and does not include range. + * + * @since 3.18.0 + */ +export interface LocationUriOnly { + uri: DocumentUri; +} + +/** + * MarkedString can be used to render human readable text. It is either a markdown string + * or a code-block that provides a language and a code snippet. The language identifier + * is semantically equal to the optional language identifier in fenced code blocks in GitHub + * issues. See https://help.github.com/articles/creating-and-highlighting-code-blocks/#syntax-highlighting + * + * The pair of a language and a value is an equivalent to markdown: + * ```${language} + * ${value} + * ``` + * + * Note that markdown strings will be sanitized - that means html will be escaped. + * @deprecated use MarkupContent instead. + */ +export type MarkedString = string | MarkedStringWithLanguage; + +/** + * @since 3.18.0 + * @deprecated use MarkupContent instead. + */ +export interface MarkedStringWithLanguage { + language: string; + value: string; +} + +/** + * A `MarkupContent` literal represents a string value which content is interpreted base on its + * kind flag. Currently the protocol supports `plaintext` and `markdown` as markup kinds. + * + * If the kind is `markdown` then the value can contain fenced code blocks like in GitHub issues. + * See https://help.github.com/articles/creating-and-highlighting-code-blocks/#syntax-highlighting + * + * Here is an example how such a string can be constructed using JavaScript / TypeScript: + * ```ts + * let markdown: MarkdownContent = { + * kind: MarkupKind.Markdown, + * value: [ + * '# Header', + * 'Some text', + * '```typescript', + * 'someCode();', + * '```' + * ].join('\n') + * }; + * ``` + * + * *Please Note* that clients might sanitize the return markdown. A client could decide to + * remove HTML from the markdown to avoid script execution. + */ +export interface MarkupContent { + /** + * The type of the Markup + */ + kind: MarkupKind; + /** + * The content itself + */ + value: string; +} + +/** + * Describes the content type that a client supports in various + * result literals like `Hover`, `ParameterInfo` or `CompletionItem`. + * + * Please note that `MarkupKinds` must not start with a `$`. This kinds + * are reserved for internal usage. + */ +export type MarkupKind = "plaintext" | "markdown"; + +/** + * Represents a collection of document highlights from a single document, used in multi-document highlight responses. + */ +export interface MultiDocumentHighlight { + /** + * The URI of the document containing the highlights. + */ + uri: DocumentUri; + /** + * The highlights for the document. + */ + highlights: (DocumentHighlight)[]; +} + +/** + * Parameters for the custom/textDocument/multiDocumentHighlight request. + */ +export interface MultiDocumentHighlightParams { + /** + * The text document. + */ + textDocument: TextDocumentIdentifier; + /** + * The position inside the text document. + */ + position: Position; + /** + * The list of file URIs to search for highlights across. + */ + filesToSearch: (DocumentUri)[]; +} + +/** + * A text document identifier to optionally denote a specific version of a text document. + */ +export interface OptionalVersionedTextDocumentIdentifier { + /** + * The text document's uri. + */ + uri: DocumentUri; + /** + * The version number of this document. If a versioned text document identifier + * is sent from the server to the client and the file is not open in the editor + * (the server has not received an open notification before) the server can send + * `null` to indicate that the version is unknown and the content on disk is the + * truth (as specified with document content ownership). + */ + version: number | null; +} + +/** + * Represents a parameter of a callable-signature. A parameter can + * have a label and a doc-comment. + */ +export interface ParameterInformation { + /** + * The label of this parameter information. + * + * Either a string or an inclusive start and exclusive end offsets within its containing + * signature label. (see SignatureInformation.label). The offsets are based on a UTF-16 + * string representation as `Position` and `Range` does. + * + * To avoid ambiguities a server should use the [start, end] offset value instead of using + * a substring. Whether a client support this is controlled via `labelOffsetSupport` client + * capability. + * + * *Note*: a label of type string should be a substring of its containing signature label. + * Its intended use case is to highlight the parameter label part in the `SignatureInformation.label`. + */ + label: string | [number, number]; + /** + * The human-readable doc-comment of this parameter. Will be shown + * in the UI but can be omitted. + */ + documentation?: (string | MarkupContent) | undefined; +} + +/** + * Position in a text document expressed as zero-based line and character + * offset. Prior to 3.17 the offsets were always based on a UTF-16 string + * representation. So a string of the form `a𐐀b` the character offset of the + * character `a` is 0, the character offset of `𐐀` is 1 and the character + * offset of b is 3 since `𐐀` is represented using two code units in UTF-16. + * Since 3.17 clients and servers can agree on a different string encoding + * representation (e.g. UTF-8). The client announces it's supported encoding + * via the client capability [`general.positionEncodings`](https://microsoft.github.io/language-server-protocol/specifications/specification-current/#clientCapabilities). + * The value is an array of position encodings the client supports, with + * decreasing preference (e.g. the encoding at index `0` is the most preferred + * one). To stay backwards compatible the only mandatory encoding is UTF-16 + * represented via the string `utf-16`. The server can pick one of the + * encodings offered by the client and signals that encoding back to the + * client via the initialize result's property + * [`capabilities.positionEncoding`](https://microsoft.github.io/language-server-protocol/specifications/specification-current/#serverCapabilities). If the string value + * `utf-16` is missing from the client's capability `general.positionEncodings` + * servers can safely assume that the client supports UTF-16. If the server + * omits the position encoding in its initialize result the encoding defaults + * to the string value `utf-16`. Implementation considerations: since the + * conversion from one encoding into another requires the content of the + * file / line the conversion is best done where the file is read which is + * usually on the server side. + * + * Positions are line end character agnostic. So you can not specify a position + * that denotes `\r|\n` or `\n|` where `|` represents the character offset. + * + * @since 3.17.0 - support for negotiated position encoding. + */ +export interface Position { + /** + * Line position in a document (zero-based). + */ + line: number; + /** + * Character offset on a line in a document (zero-based). + * + * The meaning of this offset is determined by the negotiated + * `PositionEncodingKind`. + */ + character: number; +} + +/** + * @since 3.18.0 + */ +export interface PrepareRenameDefaultBehavior { + defaultBehavior: boolean; +} + +export interface PrepareRenameParams { + /** + * The text document. + */ + textDocument: TextDocumentIdentifier; + /** + * The position inside the text document. + */ + position: Position; + /** + * An optional token that a server can use to report work done progress. + */ + workDoneToken?: ProgressToken | undefined; +} + +/** + * @since 3.18.0 + */ +export interface PrepareRenamePlaceholder { + range: Range; + placeholder: string; +} + +export type PrepareRenameResult = Range | PrepareRenamePlaceholder | PrepareRenameDefaultBehavior; + +export type ProgressToken = number | string; + +/** + * A range in a text document expressed as (zero-based) start and end positions. + * + * If you want to specify a range that contains a line including the line ending + * character(s) then use an end position denoting the start of the next line. + * For example: + * ```ts + * { + * start: { line: 5, character: 23 } + * end : { line 6, character : 0 } + * } + * ``` + */ +export interface Range { + /** + * The range's start position. + */ + start: Position; + /** + * The range's end position. + */ + end: Position; +} + +/** + * Value-object that contains additional information when + * requesting references. + */ +export interface ReferenceContext { + /** + * Include the declaration of the current symbol. + */ + includeDeclaration: boolean; +} + +/** + * Parameters for a {@link ReferencesRequest}. + */ +export interface ReferenceParams { + /** + * The text document. + */ + textDocument: TextDocumentIdentifier; + /** + * The position inside the text document. + */ + position: Position; + /** + * An optional token that a server can use to report work done progress. + */ + workDoneToken?: ProgressToken | undefined; + /** + * An optional token that a server can use to report partial results (e.g. streaming) to + * the client. + */ + partialResultToken?: ProgressToken | undefined; + context: ReferenceContext; +} + +/** + * A full diagnostic report with a set of related documents. + * + * @since 3.17.0 + */ +export interface RelatedFullDocumentDiagnosticReport { + /** + * A full document diagnostic report. + */ + kind: "full"; + /** + * An optional result id. If provided it will + * be sent on the next diagnostic request for the + * same document. + */ + resultId?: string | undefined; + /** + * The actual items. + */ + items: (Diagnostic)[]; + /** + * Diagnostics of related documents. This information is useful + * in programming languages where code in a file A can generate + * diagnostics in a file B which A depends on. An example of + * such a language is C/C++ where marco definitions in a file + * a.cpp and result in errors in a header file b.hpp. + * + * @since 3.17.0 + */ + relatedDocuments?: { [key: DocumentUri]: FullDocumentDiagnosticReport | UnchangedDocumentDiagnosticReport; } | undefined; +} + +/** + * An unchanged diagnostic report with a set of related documents. + * + * @since 3.17.0 + */ +export interface RelatedUnchangedDocumentDiagnosticReport { + /** + * A document diagnostic report indicating + * no changes to the last result. A server can + * only return `unchanged` if result ids are + * provided. + */ + kind: "unchanged"; + /** + * A result id which will be sent on the next + * diagnostic request for the same document. + */ + resultId: string; + /** + * Diagnostics of related documents. This information is useful + * in programming languages where code in a file A can generate + * diagnostics in a file B which A depends on. An example of + * such a language is C/C++ where marco definitions in a file + * a.cpp and result in errors in a header file b.hpp. + * + * @since 3.17.0 + */ + relatedDocuments?: { [key: DocumentUri]: FullDocumentDiagnosticReport | UnchangedDocumentDiagnosticReport; } | undefined; +} + +/** + * Rename file operation + */ +export interface RenameFile { + /** + * A rename + */ + kind: "rename"; + /** + * An optional annotation identifier describing the operation. + * + * @since 3.16.0 + */ + annotationId?: ChangeAnnotationIdentifier | undefined; + /** + * The old (existing) location. + */ + oldUri: DocumentUri; + /** + * The new location. + */ + newUri: DocumentUri; + /** + * Rename options. + */ + options?: RenameFileOptions | undefined; +} + +/** + * Rename file options + */ +export interface RenameFileOptions { + /** + * Overwrite target if existing. Overwrite wins over `ignoreIfExists` + */ + overwrite?: boolean | undefined; + /** + * Ignores if target exists. + */ + ignoreIfExists?: boolean | undefined; +} + +/** + * The parameters of a {@link RenameRequest}. + */ +export interface RenameParams { + /** + * The text document. + */ + textDocument: TextDocumentIdentifier; + /** + * The position inside the text document. + */ + position: Position; + /** + * An optional token that a server can use to report work done progress. + */ + workDoneToken?: ProgressToken | undefined; + /** + * The new name of the symbol. If the given name is not valid the + * request must return a {@link ResponseError} with an + * appropriate message set. + */ + newName: string; +} + +/** + * A selection range represents a part of a selection hierarchy. A selection range + * may have a parent selection range that contains it. + */ +export interface SelectionRange { + /** + * The {@link Range range} of this selection range. + */ + range: Range; + /** + * The parent selection range containing this range. Therefore `parent.range` must contain `this.range`. + */ + parent?: SelectionRange | undefined; +} + +/** + * A parameter literal used in selection range requests. + */ +export interface SelectionRangeParams { + /** + * An optional token that a server can use to report work done progress. + */ + workDoneToken?: ProgressToken | undefined; + /** + * An optional token that a server can use to report partial results (e.g. streaming) to + * the client. + */ + partialResultToken?: ProgressToken | undefined; + /** + * The text document. + */ + textDocument: TextDocumentIdentifier; + /** + * The positions inside the text document. + */ + positions: (Position)[]; +} + +/** + * @since 3.16.0 + */ +export interface SemanticTokens { + /** + * An optional result id. If provided and clients support delta updating + * the client will include the result id in the next semantic token request. + * A server can then instead of computing all semantic tokens again simply + * send a delta. + */ + resultId?: string | undefined; + /** + * The actual tokens. + */ + data: (number)[]; +} + +/** + * @since 3.16.0 + */ +export interface SemanticTokensParams { + /** + * An optional token that a server can use to report work done progress. + */ + workDoneToken?: ProgressToken | undefined; + /** + * An optional token that a server can use to report partial results (e.g. streaming) to + * the client. + */ + partialResultToken?: ProgressToken | undefined; + /** + * The text document. + */ + textDocument: TextDocumentIdentifier; +} + +/** + * @since 3.16.0 + */ +export interface SemanticTokensRangeParams { + /** + * An optional token that a server can use to report work done progress. + */ + workDoneToken?: ProgressToken | undefined; + /** + * An optional token that a server can use to report partial results (e.g. streaming) to + * the client. + */ + partialResultToken?: ProgressToken | undefined; + /** + * The text document. + */ + textDocument: TextDocumentIdentifier; + /** + * The range the semantic tokens are requested for. + */ + range: Range; +} + +/** + * Signature help represents the signature of something + * callable. There can be multiple signature but only one + * active and only one active parameter. + */ +export interface SignatureHelp { + /** + * One or more signatures. + */ + signatures: (SignatureInformation)[]; + /** + * The active signature. If omitted or the value lies outside the + * range of `signatures` the value defaults to zero or is ignored if + * the `SignatureHelp` has no signatures. + * + * Whenever possible implementors should make an active decision about + * the active signature and shouldn't rely on a default value. + * + * In future version of the protocol this property might become + * mandatory to better express this. + */ + activeSignature?: number | undefined; + /** + * The active parameter of the active signature. + * + * If `null`, no parameter of the signature is active (for example a named + * argument that does not match any declared parameters). This is only valid + * if the client specifies the client capability + * `textDocument.signatureHelp.noActiveParameterSupport === true` + * + * If omitted or the value lies outside the range of + * `signatures[activeSignature].parameters` defaults to 0 if the active + * signature has parameters. + * + * If the active signature has no parameters it is ignored. + * + * In future version of the protocol this property might become + * mandatory (but still nullable) to better express the active parameter if + * the active signature does have any. + * + * Since version 3.16.0 the `SignatureInformation` itself provides a + * `activeParameter` property and it should be used instead of this one. + */ + activeParameter?: (number | null) | undefined; +} + +/** + * Additional information about the context in which a signature help request was triggered. + * + * @since 3.15.0 + */ +export interface SignatureHelpContext { + /** + * Action that caused signature help to be triggered. + */ + triggerKind: SignatureHelpTriggerKind; + /** + * Character that caused signature help to be triggered. + * + * This is undefined when `triggerKind !== SignatureHelpTriggerKind.TriggerCharacter` + */ + triggerCharacter?: string | undefined; + /** + * `true` if signature help was already showing when it was triggered. + * + * Retriggers occurs when the signature help is already active and can be caused by actions such as + * typing a trigger character, a cursor move, or document content changes. + */ + isRetrigger: boolean; + /** + * The currently active `SignatureHelp`. + * + * The `activeSignatureHelp` has its `SignatureHelp.activeSignature` field updated based on + * the user navigating through available signatures. + */ + activeSignatureHelp?: SignatureHelp | undefined; +} + +/** + * Parameters for a {@link SignatureHelpRequest}. + */ +export interface SignatureHelpParams { + /** + * The text document. + */ + textDocument: TextDocumentIdentifier; + /** + * The position inside the text document. + */ + position: Position; + /** + * An optional token that a server can use to report work done progress. + */ + workDoneToken?: ProgressToken | undefined; + /** + * The signature help context. This is only available if the client specifies + * to send this using the client capability `textDocument.signatureHelp.contextSupport === true` + * + * @since 3.15.0 + */ + context?: SignatureHelpContext | undefined; +} + +/** + * How a signature help was triggered. + * + * @since 3.15.0 + */ +export type SignatureHelpTriggerKind = 1 | 2 | 3; + +/** + * Represents the signature of something callable. A signature + * can have a label, like a function-name, a doc-comment, and + * a set of parameters. + */ +export interface SignatureInformation { + /** + * The label of this signature. Will be shown in + * the UI. + */ + label: string; + /** + * The human-readable doc-comment of this signature. Will be shown + * in the UI but can be omitted. + */ + documentation?: (string | MarkupContent) | undefined; + /** + * The parameters of this signature. + */ + parameters?: (ParameterInformation)[] | undefined; + /** + * The index of the active parameter. + * + * If `null`, no parameter of the signature is active (for example a named + * argument that does not match any declared parameters). This is only valid + * if the client specifies the client capability + * `textDocument.signatureHelp.noActiveParameterSupport === true` + * + * If provided (or `null`), this is used in place of + * `SignatureHelp.activeParameter`. + * + * @since 3.16.0 + */ + activeParameter?: (number | null) | undefined; + /** + * A colorized label for the signature, providing classified text runs for VS syntax coloring. + */ + _vs_colorizedLabel?: VSClassifiedTextElement | undefined; +} + +/** + * An interactive text edit. + * + * @since 3.18.0 + */ +export interface SnippetTextEdit { + /** + * The range of the text document to be manipulated. + */ + range: Range; + /** + * The snippet to be inserted. + */ + snippet: StringValue; + /** + * The actual identifier of the snippet edit. + */ + annotationId?: ChangeAnnotationIdentifier | undefined; +} + +/** + * A string value used as a snippet is a template which allows to insert text + * and to control the editor cursor when insertion happens. + * + * A snippet can define tab stops and placeholders with `$1`, `$2` + * and `${3:foo}`. `$0` defines the final tab stop, it defaults to + * the end of the snippet. Variables are defined with `$name` and + * `${name:default value}`. + * + * @since 3.18.0 + */ +export interface StringValue { + /** + * The kind of string value. + */ + kind: "snippet"; + /** + * The snippet string. + */ + value: string; +} + +/** + * Represents information about programming constructs like variables, classes, + * interfaces etc. + */ +export interface SymbolInformation { + /** + * The name of this symbol. + */ + name: string; + /** + * The kind of this symbol. + */ + kind: SymbolKind; + /** + * Tags for this symbol. + * + * @since 3.16.0 + */ + tags?: (SymbolTag)[] | undefined; + /** + * The name of the symbol containing this symbol. This information is for + * user interface purposes (e.g. to render a qualifier in the user interface + * if necessary). It can't be used to re-infer a hierarchy for the document + * symbols. + */ + containerName?: string | undefined; + /** + * Indicates if this symbol is deprecated. + * + * @deprecated Use tags instead + */ + deprecated?: boolean | undefined; + /** + * The location of this symbol. The location's range is used by a tool + * to reveal the location in the editor. If the symbol is selected in the + * tool the range's start information is used to position the cursor. So + * the range usually spans more than the actual symbol's name and does + * normally include things like visibility modifiers. + * + * The range doesn't have to denote a node range in the sense of an abstract + * syntax tree. It can therefore not be used to re-construct a hierarchy of + * the symbols. + */ + location: Location; +} + +/** + * A symbol kind. + */ +export type SymbolKind = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26; + +/** + * Symbol tags are extra annotations that tweak the rendering of a symbol. + * + * @since 3.16 + */ +export type SymbolTag = 1; + +/** + * Describes textual changes on a text document. A TextDocumentEdit describes all changes + * on a document version Si and after they are applied move the document to version Si+1. + * So the creator of a TextDocumentEdit doesn't need to sort the array of edits or do any + * kind of ordering. However the edits must be non overlapping. + */ +export interface TextDocumentEdit { + /** + * The text document to change. + */ + textDocument: OptionalVersionedTextDocumentIdentifier; + /** + * The edits to be applied. + * + * @since 3.16.0 - support for AnnotatedTextEdit. This is guarded using a + * client capability. + * + * @since 3.18.0 - support for SnippetTextEdit. This is guarded using a + * client capability. + */ + edits: ((TextEdit | AnnotatedTextEdit | SnippetTextEdit))[]; +} + +/** + * A literal to identify a text document in the client. + */ +export interface TextDocumentIdentifier { + /** + * The text document's uri. + */ + uri: DocumentUri; +} + +/** + * A parameter literal used in requests to pass a text document and a position inside that + * document. + */ +export interface TextDocumentPositionParams { + /** + * The text document. + */ + textDocument: TextDocumentIdentifier; + /** + * The position inside the text document. + */ + position: Position; +} + +/** + * A text edit applicable to a text document. + */ +export interface TextEdit { + /** + * The range of the text document to be manipulated. To insert + * text into a document create a range where start === end. + */ + range: Range; + /** + * The string to be inserted. For delete operations use an + * empty string. + */ + newText: string; +} + +export interface TypeDefinitionParams { + /** + * The text document. + */ + textDocument: TextDocumentIdentifier; + /** + * The position inside the text document. + */ + position: Position; + /** + * An optional token that a server can use to report work done progress. + */ + workDoneToken?: ProgressToken | undefined; + /** + * An optional token that a server can use to report partial results (e.g. streaming) to + * the client. + */ + partialResultToken?: ProgressToken | undefined; +} + +export type URI = string; + +/** + * A diagnostic report indicating that the last returned + * report is still accurate. + * + * @since 3.17.0 + */ +export interface UnchangedDocumentDiagnosticReport { + /** + * A document diagnostic report indicating + * no changes to the last result. A server can + * only return `unchanged` if result ids are + * provided. + */ + kind: "unchanged"; + /** + * A result id which will be sent on the next + * diagnostic request for the same document. + */ + resultId: string; +} + +/** + * A classified text element containing an array of classified text runs, used for colorized labels in VS. + */ +export interface VSClassifiedTextElement { + /** + * The classified text runs that make up this element. + */ + Runs: (VSClassifiedTextRun)[]; + /** + * VS type discriminator required by ObjectContentConverter for deserialization. + */ + _vs_type: "ClassifiedTextElement"; +} + +/** + * A classified text run with text and classification type, used for colorized display in VS. + */ +export interface VSClassifiedTextRun { + /** + * The classification type name (e.g. 'keyword', 'class name', 'parameter name'). + */ + ClassificationTypeName: string; + /** + * The text content of this run. + */ + Text: string; + /** + * Optional marker tag type. + */ + MarkerTagType?: string | undefined; + /** + * The style of this text run. + */ + Style?: number | undefined; + /** + * VS type discriminator required by ObjectContentConverter for deserialization. + */ + _vs_type: "ClassifiedTextRun"; +} + +/** + * A container element that groups other VS rich-content elements (images, classified text, or nested containers). Used to build the VS hover raw content that combines a symbol icon with colorized text. + */ +export interface VSContainerElement { + /** + * Layout style for the child elements. + */ + Style: VSContainerElementStyle; + /** + * The child elements contained within this container. + */ + Elements: ((VSImageElement | VSClassifiedTextElement | VSContainerElement))[]; + /** + * VS type discriminator required by ObjectContentConverter for deserialization. + */ + _vs_type: "ContainerElement"; +} + +/** + * Layout style for a VSContainerElement's children, mirroring VS's Microsoft.VisualStudio.Text.Adornments.ContainerElementStyle. + */ +export type VSContainerElementStyle = 0 | 1; + +/** + * An image element (e.g. a symbol-kind icon) for use in VS rich content such as hover tooltips. + */ +export interface VSImageElement { + /** + * The image to display. + */ + ImageId: VSImageId; + /** + * VS type discriminator required by ObjectContentConverter for deserialization. + */ + _vs_type: "ImageElement"; +} + +/** + * Identifies an image in a VS image catalog. Used to render symbol-kind icons (e.g. in hover tooltips). + */ +export interface VSImageId { + /** + * The GUID of the image catalog containing this image. + */ + Guid: string; + /** + * The numeric identifier of the image within its catalog. + */ + Id: number; + /** + * VS type discriminator required by ObjectContentConverter for deserialization. + */ + _vs_type: "ImageId"; +} + +/** + * Parameters for the textDocument/_vs_onAutoInsert request. + */ +export interface VSOnAutoInsertParams { + /** + * The text document. + */ + _vs_textDocument: TextDocumentIdentifier; + /** + * The position inside the text document. + */ + _vs_position: Position; + /** + * The character that triggered the auto-insert. + */ + _vs_ch: string; +} + +/** + * Response item for the textDocument/_vs_onAutoInsert request. + */ +export interface VSOnAutoInsertResponseItem { + /** + * The format of the text edit (plaintext or snippet). + */ + _vs_textEditFormat: InsertTextFormat; + /** + * The text edit to apply for the auto-insertion. + */ + _vs_textEdit: TextEdit; +} + +/** + * A VS-specific reference item with grouping support for Find All References. + */ +export interface VSReferenceItem { + /** + * Unique identifier for this reference item. + */ + _vs_id: number; + /** + * The ID of the definition item this reference belongs to. Absent for definition items themselves. + */ + _vs_definitionId?: number | undefined; + /** + * The kind(s) of this reference (read, write, etc.). + */ + _vs_kind?: (VSReferenceKind)[] | undefined; + /** + * The location of this reference. + */ + _vs_location: Location; + /** + * Classified display text for the definition (used for grouping headers in the UI). + */ + _vs_definitionText?: VSClassifiedTextElement | undefined; + /** + * The project name for this reference. + */ + _vs_projectName?: string | undefined; + /** + * The containing type for this reference. + */ + _vs_containingType?: string | undefined; +} + +export type VSReferenceKind = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17; + +/** + * A workspace edit represents changes to many resources managed in the workspace. The edit + * should either provide `changes` or `documentChanges`. If documentChanges are present + * they are preferred over `changes` if the client can handle versioned document edits. + * + * Since version 3.13.0 a workspace edit can contain resource operations as well. If resource + * operations are present clients need to execute the operations in the order in which they + * are provided. So a workspace edit for example can consist of the following two changes: + * (1) a create file a.txt and (2) a text document edit which insert text into file a.txt. + * + * An invalid sequence (e.g. (1) delete file a.txt and (2) insert text into file a.txt) will + * cause failure of the operation. How the client recovers from the failure is described by + * the client capability: `workspace.workspaceEdit.failureHandling` + */ +export interface WorkspaceEdit { + /** + * Holds changes to existing resources. + */ + changes?: { [key: DocumentUri]: (TextEdit)[]; } | undefined; + /** + * Depending on the client capability `workspace.workspaceEdit.resourceOperations` document changes + * are either an array of `TextDocumentEdit`s to express changes to n different text documents + * where each text document edit addresses a specific version of a text document. Or it can contain + * above `TextDocumentEdit`s mixed with create, rename and delete file / folder operations. + * + * Whether a client supports versioned document edits is expressed via + * `workspace.workspaceEdit.documentChanges` client capability. + * + * If a client neither supports `documentChanges` nor `workspace.workspaceEdit.resourceOperations` then + * only plain `TextEdit`s using the `changes` property are supported. + */ + documentChanges?: ((TextDocumentEdit | CreateFile | RenameFile | DeleteFile))[] | undefined; + /** + * A map of change annotations that can be referenced in `AnnotatedTextEdit`s or create, rename and + * delete file / folder operations. + * + * Whether clients honor this property depends on the client capability `workspace.changeAnnotationSupport`. + * + * @since 3.16.0 + */ + changeAnnotations?: { [key: ChangeAnnotationIdentifier]: ChangeAnnotation; } | undefined; +} + +/** + * A special workspace symbol that supports locations without a range. + * + * See also SymbolInformation. + * + * @since 3.17.0 + */ +export interface WorkspaceSymbol { + /** + * The name of this symbol. + */ + name: string; + /** + * The kind of this symbol. + */ + kind: SymbolKind; + /** + * Tags for this symbol. + * + * @since 3.16.0 + */ + tags?: (SymbolTag)[] | undefined; + /** + * The name of the symbol containing this symbol. This information is for + * user interface purposes (e.g. to render a qualifier in the user interface + * if necessary). It can't be used to re-infer a hierarchy for the document + * symbols. + */ + containerName?: string | undefined; + /** + * The location of the symbol. Whether a server is allowed to + * return a location without a range depends on the client + * capability `workspace.symbol.resolveSupport`. + * + * See SymbolInformation#location for more details. + */ + location: Location | LocationUriOnly; + /** + * A data entry field that is preserved on a workspace symbol between a + * workspace symbol request and a workspace symbol resolve request. + */ + data?: LSPAny | undefined; +} + +/** + * The parameters of a {@link WorkspaceSymbolRequest}. + */ +export interface WorkspaceSymbolParams { + /** + * An optional token that a server can use to report work done progress. + */ + workDoneToken?: ProgressToken | undefined; + /** + * An optional token that a server can use to report partial results (e.g. streaming) to + * the client. + */ + partialResultToken?: ProgressToken | undefined; + /** + * A query string to filter symbols by. Clients may send an empty + * string here to request all symbols. + * + * The `query`-parameter should be interpreted in a *relaxed way* as editors + * will apply their own highlighting and scoring on the results. A good rule + * of thumb is to match case-insensitive and to simply check that the + * characters of *query* appear in their order in a candidate symbol. + * Servers shouldn't use prefix, substring, or similar strict matching. + */ + query: string; + /** + * Scopes the workspace symbol search to projects containing this document. + */ + textDocument?: TextDocumentIdentifier | undefined; +} + +/** Language-feature requests supported by the extension middleware API. */ +export interface LspMiddlewareRequests { + "textDocument/hover": { params: HoverParams; result: Hover | null; }; + "textDocument/completion": { params: CompletionParams; result: (CompletionItem)[] | CompletionList | null; }; + "completionItem/resolve": { params: CompletionItem; result: CompletionItem; }; + "textDocument/signatureHelp": { params: SignatureHelpParams; result: SignatureHelp | null; }; + "textDocument/definition": { params: DefinitionParams; result: Definition | (DefinitionLink)[] | null; }; + "textDocument/typeDefinition": { params: TypeDefinitionParams; result: Definition | (DefinitionLink)[] | null; }; + "textDocument/implementation": { params: ImplementationParams; result: Definition | (DefinitionLink)[] | null; }; + "textDocument/references": { params: ReferenceParams; result: (Location)[] | null; }; + "textDocument/documentHighlight": { params: DocumentHighlightParams; result: (DocumentHighlight)[] | null; }; + "textDocument/documentSymbol": { params: DocumentSymbolParams; result: (SymbolInformation)[] | (DocumentSymbol)[] | null; }; + "workspace/symbol": { params: WorkspaceSymbolParams; result: (SymbolInformation)[] | (WorkspaceSymbol)[] | null; }; + "textDocument/rename": { params: RenameParams; result: WorkspaceEdit | null; }; + "textDocument/prepareRename": { params: PrepareRenameParams; result: PrepareRenameResult | null; }; + "textDocument/formatting": { params: DocumentFormattingParams; result: (TextEdit)[] | null; }; + "textDocument/rangeFormatting": { params: DocumentRangeFormattingParams; result: (TextEdit)[] | null; }; + "textDocument/onTypeFormatting": { params: DocumentOnTypeFormattingParams; result: (TextEdit)[] | null; }; + "textDocument/selectionRange": { params: SelectionRangeParams; result: (SelectionRange)[] | null; }; + "textDocument/foldingRange": { params: FoldingRangeParams; result: (FoldingRange)[] | null; }; + "textDocument/inlayHint": { params: InlayHintParams; result: (InlayHint)[] | null; }; + "textDocument/codeAction": { params: CodeActionParams; result: ((Command | CodeAction))[] | null; }; + "textDocument/codeLens": { params: CodeLensParams; result: (CodeLens)[] | null; }; + "codeLens/resolve": { params: CodeLens; result: CodeLens; }; + "textDocument/prepareCallHierarchy": { params: CallHierarchyPrepareParams; result: (CallHierarchyItem)[] | null; }; + "callHierarchy/incomingCalls": { params: CallHierarchyIncomingCallsParams; result: (CallHierarchyIncomingCall)[] | null; }; + "callHierarchy/outgoingCalls": { params: CallHierarchyOutgoingCallsParams; result: (CallHierarchyOutgoingCall)[] | null; }; + "textDocument/linkedEditingRange": { params: LinkedEditingRangeParams; result: LinkedEditingRanges | null; }; + "textDocument/semanticTokens/full": { params: SemanticTokensParams; result: SemanticTokens | null; }; + "textDocument/semanticTokens/range": { params: SemanticTokensRangeParams; result: SemanticTokens | null; }; + "textDocument/diagnostic": { params: DocumentDiagnosticParams; result: DocumentDiagnosticReport; }; + "custom/textDocument/sourceDefinition": { params: TextDocumentPositionParams; result: Definition | (DefinitionLink)[] | null; }; + "custom/textDocument/multiDocumentHighlight": { params: MultiDocumentHighlightParams; result: (MultiDocumentHighlight)[] | null; }; + "textDocument/_vs_onAutoInsert": { params: VSOnAutoInsertParams; result: VSOnAutoInsertResponseItem | null; }; + "textDocument/_vs_references": { params: ReferenceParams; result: (VSReferenceItem)[] | null; }; +} diff --git a/packages/vscode-typescript/package.json b/packages/vscode-typescript/package.json index 3b2579cd89b26..d00bfe862addb 100644 --- a/packages/vscode-typescript/package.json +++ b/packages/vscode-typescript/package.json @@ -383,6 +383,7 @@ "vscode-tas-client": "^0.3.3" }, "devDependencies": { + "@typescript/typescript": "0.0.0", "@types/vscode": "~1.125.0", "@typescript/bundled-typescript": "npm:typescript@7.0.2", "@vscode/vsce": "4.0.0", diff --git a/packages/vscode-typescript/src/api.ts b/packages/vscode-typescript/src/api.ts deleted file mode 100644 index 2e55df19aa254..0000000000000 --- a/packages/vscode-typescript/src/api.ts +++ /dev/null @@ -1,147 +0,0 @@ -import type * as vscode from "vscode"; -import type * as lsp from "vscode-languageserver-protocol"; - -export interface ContentMapperManifest { - readonly name: string; - readonly version?: string; - readonly exec: readonly string[]; - readonly cwd?: vscode.Uri; - readonly compilerOptions?: readonly string[]; - readonly dynamicConfig?: boolean; -} - -export interface ContentMapperContribution { - readonly extensions: readonly string[]; - readonly inferredProjectContribution?: { - readonly options?: Readonly>; - readonly manifest: ContentMapperManifest; - }; -} - -type Request = T extends lsp.ProtocolRequestType ? { params: P; result: R; } - : never; - -export interface MultiDocumentHighlightParams extends lsp.TextDocumentPositionParams { - filesToSearch: string[]; -} - -export interface MultiDocumentHighlight { - uri: lsp.DocumentUri; - highlights: lsp.DocumentHighlight[]; -} - -export interface AutoInsertParams { - _vs_textDocument: lsp.TextDocumentIdentifier; - _vs_position: lsp.Position; - _vs_ch: string; -} - -export interface AutoInsertResult { - _vs_textEditFormat: lsp.InsertTextFormat; - _vs_textEdit: lsp.TextEdit; -} - -export interface ClassifiedTextElement { - Runs: { - ClassificationTypeName: string; - Text: string; - MarkerTagType?: string; - Style?: number | null; - _vs_type: "ClassifiedTextRun"; - }[]; - _vs_type: "ClassifiedTextElement"; -} - -export interface VSReferenceItem { - _vs_id: number; - _vs_definitionId?: number; - _vs_kind?: number[]; - _vs_location: lsp.Location; - _vs_definitionText?: ClassifiedTextElement; - _vs_projectName?: string; - _vs_containingType?: string; -} - -/** Language-feature requests supported by this extension's middleware API. */ -export interface LspMiddlewareRequests { - "textDocument/hover": { - params: lsp.HoverParams & { verbosityLevel?: number; }; - result: (lsp.Hover & { canIncreaseVerbosity?: boolean; }) | null; - }; - "textDocument/completion": Request; - "completionItem/resolve": Request; - "textDocument/signatureHelp": Request; - "textDocument/definition": Request; - "textDocument/typeDefinition": Request; - "textDocument/implementation": Request; - "textDocument/references": Request; - "textDocument/documentHighlight": Request; - "textDocument/documentSymbol": Request; - "workspace/symbol": Request & { - params: { textDocument?: lsp.TextDocumentIdentifier; }; - }; - "textDocument/rename": Request; - "textDocument/prepareRename": Request; - "textDocument/formatting": Request; - "textDocument/rangeFormatting": Request; - "textDocument/onTypeFormatting": Request; - "textDocument/selectionRange": Request; - "textDocument/foldingRange": Request; - "textDocument/inlayHint": Request; - "textDocument/codeAction": Request; - "textDocument/codeLens": Request; - "codeLens/resolve": Request; - "textDocument/prepareCallHierarchy": Request; - "callHierarchy/incomingCalls": Request; - "callHierarchy/outgoingCalls": Request; - "textDocument/linkedEditingRange": Request; - "textDocument/semanticTokens/full": Request; - "textDocument/semanticTokens/range": Request; - "textDocument/diagnostic": Request; - "custom/textDocument/sourceDefinition": { - params: lsp.TextDocumentPositionParams; - result: lsp.Location | lsp.Location[] | lsp.LocationLink[] | null; - }; - "custom/textDocument/multiDocumentHighlight": { - params: MultiDocumentHighlightParams; - result: MultiDocumentHighlight[] | null; - }; - "textDocument/_vs_onAutoInsert": { params: AutoInsertParams; result: AutoInsertResult | null; }; - "textDocument/_vs_references": { params: lsp.ReferenceParams; result: VSReferenceItem[] | null; }; -} - -export type LspMiddlewareMethod = keyof LspMiddlewareRequests; - -export type DeepReadonly = unknown extends T ? unknown : T extends object ? { readonly [K in keyof T]: DeepReadonly; } : T; - -export type LspMiddlewareResult = LspMiddlewareRequests[M]["result"]; - -export type LspMiddlewareContext = { readonly params: DeepReadonly; }; - -export type LspMiddlewareTransformer = ( - result: LspMiddlewareResult, - context: LspMiddlewareContext, -) => LspMiddlewareResult | PromiseLike>; - -export interface ExtensionAPI { - onLanguageServerInitialized: vscode.Event; - initializeAPIConnection(pipe?: string): Promise; - registerContentMappers(contributorId: string, contributions: readonly ContentMapperContribution[]): vscode.Disposable; - - /** - * Changes an LSP language-feature response before conversion to VS Code objects. - * Each callback gets its own copy of the request parameters. - * - * Callbacks run in registration order. Order between extensions may vary. - * If a callback fails, we log the error and use the original server response. - * - * Registrations survive server restarts. Disposing removes the callback from - * future responses but does not interrupt a response already being processed. - * - * Requests, notifications, lifecycle messages, and partial results are excluded. - */ - registerLspMiddleware( - method: M, - transformer: LspMiddlewareTransformer>, - ): vscode.Disposable; -} diff --git a/packages/vscode-typescript/src/contentMapperContributions.ts b/packages/vscode-typescript/src/contentMapperContributions.ts index c4c0e6e4ddbc5..c8529d11ee8b2 100644 --- a/packages/vscode-typescript/src/contentMapperContributions.ts +++ b/packages/vscode-typescript/src/contentMapperContributions.ts @@ -1,5 +1,5 @@ -import type { ContentMapperContribution } from "./api"; -export type { ContentMapperContribution, ContentMapperManifest } from "./api"; +import type { ContentMapperContribution } from "@typescript/typescript/unstable/vscode"; +export type { ContentMapperContribution, ContentMapperManifest } from "@typescript/typescript/unstable/vscode"; export interface SerializedContentMapperContribution { readonly contributorId: string; diff --git a/packages/vscode-typescript/src/extension.ts b/packages/vscode-typescript/src/extension.ts index 8936dee0d0e71..b2f679ee2ddca 100644 --- a/packages/vscode-typescript/src/extension.ts +++ b/packages/vscode-typescript/src/extension.ts @@ -1,6 +1,6 @@ +import type { ExtensionAPI } from "@typescript/typescript/unstable/vscode"; import * as vscode from "vscode"; -import type { ExtensionAPI } from "./api"; -export type { ExtensionAPI } from "./api"; +export type { ExtensionAPI } from "@typescript/typescript/unstable/vscode"; import { registerEnablementCommands, diff --git a/packages/vscode-typescript/src/lspMiddleware.ts b/packages/vscode-typescript/src/lspMiddleware.ts index bf002353d3083..999b38534f4fa 100644 --- a/packages/vscode-typescript/src/lspMiddleware.ts +++ b/packages/vscode-typescript/src/lspMiddleware.ts @@ -1,14 +1,14 @@ -import type { - CancellationToken, - Disposable, - MessageSignature, -} from "vscode-languageserver-protocol"; import type { LspMiddlewareContext, LspMiddlewareMethod, LspMiddlewareResult, LspMiddlewareTransformer, -} from "./api"; +} from "@typescript/typescript/unstable/vscode"; +import type { + CancellationToken, + Disposable, + MessageSignature, +} from "vscode-languageserver-protocol"; const supportedMethods: { readonly [M in LspMiddlewareMethod]: true; } = { "textDocument/hover": true, diff --git a/packages/vscode-typescript/src/session.ts b/packages/vscode-typescript/src/session.ts index b3323dd07680f..e9e8f78541bf9 100644 --- a/packages/vscode-typescript/src/session.ts +++ b/packages/vscode-typescript/src/session.ts @@ -1,10 +1,10 @@ -import * as path from "path"; -import * as vscode from "vscode"; -import { ActiveJsTsEditorTracker } from "./activeJsTsEditorTracker"; import type { LspMiddlewareMethod, LspMiddlewareTransformer, -} from "./api"; +} from "@typescript/typescript/unstable/vscode"; +import * as path from "path"; +import * as vscode from "vscode"; +import { ActiveJsTsEditorTracker } from "./activeJsTsEditorTracker"; import { Client } from "./client"; import { registerCodeLensShowLocationsCommand, diff --git a/packages/vscode-typescript/test/lspMiddleware.test.ts b/packages/vscode-typescript/test/lspMiddleware.test.ts index 555752488ba87..40d7ccc4e685d 100644 --- a/packages/vscode-typescript/test/lspMiddleware.test.ts +++ b/packages/vscode-typescript/test/lspMiddleware.test.ts @@ -1,3 +1,4 @@ +import type { ExtensionAPI } from "@typescript/typescript/unstable/vscode"; import assert from "node:assert/strict"; import { PassThrough } from "node:stream"; import test, { describe } from "node:test"; @@ -10,7 +11,6 @@ import { PublishDiagnosticsNotification, type PublishDiagnosticsParams, } from "vscode-languageserver-protocol/node"; -import type { ExtensionAPI } from "../src/api"; import { type FirstPartyLspMiddleware, LspMiddlewareRegistry, diff --git a/packages/vscode-typescript/test/tsconfig.json b/packages/vscode-typescript/test/tsconfig.json index baecbadc1c78c..3ae2a24af8b9e 100644 --- a/packages/vscode-typescript/test/tsconfig.json +++ b/packages/vscode-typescript/test/tsconfig.json @@ -1,7 +1,4 @@ { "extends": "../tsconfig.json", - "compilerOptions": { - "rootDir": ".." - }, "include": ["."] } diff --git a/packages/vscode-typescript/tsconfig.json b/packages/vscode-typescript/tsconfig.json index 1dbb37fcc5554..e5492794a84c4 100644 --- a/packages/vscode-typescript/tsconfig.json +++ b/packages/vscode-typescript/tsconfig.json @@ -1,12 +1,15 @@ { "compilerOptions": { - "rootDir": "./src", + "rootDir": "..", "outDir": "./dist", "noEmit": true, "target": "es2023", "module": "preserve", "moduleResolution": "bundler", + "paths": { + "@typescript/typescript/unstable/vscode": ["../typescript/src/vscode/extensionApi.ts"] + }, "types": ["node"], "strict": true, diff --git a/tools/scripts/gen/generatedFile.test.mts b/tools/scripts/gen/generatedFile.test.mts index e8c256a58b451..dedf52ae37c68 100644 --- a/tools/scripts/gen/generatedFile.test.mts +++ b/tools/scripts/gen/generatedFile.test.mts @@ -387,12 +387,12 @@ test("generate includes standalone generators without Go traversal", async () => fs.readFileSync(path.join(root, "packages/typescript/vendor/vscode-jsonrpc/package.json")), fs.readFileSync(path.join(root, "node_modules/vscode-jsonrpc/package.json")), ); - const lspOutput = path.join(root, "tsc/internal/lsp/lsproto/lsp_generated.go"); - const timestamp = fs.statSync(lspOutput).mtimeMs; + const lspOutputs = ["tsc/internal/lsp/lsproto/lsp_generated.go", "packages/typescript/src/vscode/protocol.generated.ts"].map(file => path.join(root, file)); + const timestamps = lspOutputs.map(file => fs.statSync(file).mtimeMs); const current = await x("npx", ["hereby", "generate:lsp"], { throwOnError: true, nodeOptions: { cwd: root } }); assert.match(current.stdout, /LSP bindings are up to date/); assert.doesNotMatch(current.stdout, /Using vscode-languageclient/); - assert.equal(fs.statSync(lspOutput).mtimeMs, timestamp); + assert.deepEqual(lspOutputs.map(file => fs.statSync(file).mtimeMs), timestamps); }); test("localization and vendoring preserve current outputs", async context => { diff --git a/tools/scripts/gen/lspTypeScript.test.mts b/tools/scripts/gen/lspTypeScript.test.mts new file mode 100644 index 0000000000000..9844c7a0712f4 --- /dev/null +++ b/tools/scripts/gen/lspTypeScript.test.mts @@ -0,0 +1,125 @@ +import assert from "node:assert/strict"; +import { execFileSync } from "node:child_process"; +import fs from "node:fs"; +import path from "node:path"; +import { test } from "node:test"; +import ts from "typescript"; +import { getTypeScriptModel } from "../../../tsc/internal/lsp/lsproto/_generate/generate.mts"; +import { getMiddlewareMethods } from "../../../tsc/internal/lsp/lsproto/_generate/generateTypeScript.mts"; +import type { MetaModel } from "../../../tsc/internal/lsp/lsproto/_generate/metaModelSchema.mts"; +import { generateTypeScript } from "../../../tsc/internal/lsp/lsproto/_generate/typeScript.mts"; + +function fixture(): MetaModel { + return { + metaData: { version: "3.18.0" }, + requests: [{ + method: "test/feature", + messageDirection: "clientToServer", + params: { kind: "reference", name: "Child" }, + result: { kind: "reference", name: "Result" }, + }], + notifications: [], + enumerations: [ + { name: "Kind", type: { kind: "base", name: "string" }, values: [{ name: "First", value: "first" }, { name: "Second", value: "second" }] }, + { name: "OpenKind", type: { kind: "base", name: "integer" }, values: [{ name: "First", value: 1 }], supportsCustomValues: true }, + ], + structures: [ + { name: "Parent", properties: [{ name: "inherited", type: { kind: "base", name: "string" } }, { name: "override", type: { kind: "base", name: "integer" } }] }, + { name: "Mixin", properties: [{ name: "mixed", type: { kind: "reference", name: "Kind" } }] }, + { + name: "Child", + extends: [{ kind: "reference", name: "Parent" }], + mixins: [{ kind: "reference", name: "Mixin" }], + properties: [ + { name: "override", type: { kind: "base", name: "boolean" } }, + { name: "optional", type: { kind: "reference", name: "OpenKind" }, optional: true }, + { name: "zero", type: { kind: "base", name: "boolean" }, omitzeroValue: true }, + { name: "recursive", type: { kind: "reference", name: "Child" }, optional: true }, + { name: "uri", type: { kind: "base", name: "DocumentUri" } }, + { name: "map", type: { kind: "map", key: { kind: "base", name: "string" }, value: { kind: "reference", name: "Kind" } } }, + { name: "tuple", type: { kind: "tuple", items: [{ kind: "base", name: "integer" }, { kind: "stringLiteral", value: "tuple" }] } }, + { name: "intersection", type: { kind: "and", items: [{ kind: "literal", value: { properties: [{ name: "flag", type: { kind: "booleanLiteral", value: true } }] } }, { kind: "literal", value: { properties: [{ name: "number", type: { kind: "integerLiteral", value: 2 } }] } }] } }, + ], + }, + { name: "UnusedLifecycle", properties: [] }, + ], + typeAliases: [{ + name: "Result", + type: { kind: "or", items: [{ kind: "array", element: { kind: "reference", name: "Child" } }, { kind: "base", name: "null" }] }, + }], + }; +} + +test("LSP TypeScript generation traverses reachable types and preserves wire shapes", () => { + const model = fixture(); + const before = structuredClone(model); + const output = generateTypeScript(model, ["test/feature"]); + assert.deepEqual(model, before); + assert.equal(output, generateTypeScript(model, ["test/feature"])); + assert.match(output, /"inherited": string/); + assert.match(output, /"mixed": Kind/); + assert.match(output, /"override": boolean/); + assert.doesNotMatch(output, /"override": number/); + assert.match(output, /"optional"\?: OpenKind \| undefined/); + assert.match(output, /"zero"\?: boolean \| undefined/); + assert.match(output, /"recursive"\?: Child \| undefined/); + assert.match(output, /export type DocumentUri = string/); + assert.match(output, /export type Kind = "first" \| "second"/); + assert.match(output, /export type OpenKind = 1 \| number/); + assert.match(output, /export type Result = \(\(Child\)\[\] \| null\)/); + assert.doesNotMatch(output, /UnusedLifecycle|export interface Parent|export interface Mixin/); + const file = ts.createSourceFile("generated.ts", output, ts.ScriptTarget.Latest, true); + assert.ok(file.statements.every(statement => ts.isInterfaceDeclaration(statement) || ts.isTypeAliasDeclaration(statement))); +}); + +test("LSP TypeScript generation reports invalid schema and method inputs", () => { + assert.throws(() => generateTypeScript(fixture(), ["missing/feature"]), /Not a client-to-server request/); + assert.throws(() => generateTypeScript(fixture(), ["test/feature", "test/feature"]), /Duplicate/); + const serverRequest = fixture(); + serverRequest.requests[0].messageDirection = "serverToClient"; + assert.throws(() => generateTypeScript(serverRequest, ["test/feature"]), /Not a client-to-server request/); + const unknown = fixture(); + unknown.requests[0].result = { kind: "reference", name: "Missing" }; + assert.throws(() => generateTypeScript(unknown, ["test/feature"]), /Unknown LSP type Missing/); + const cycle = fixture(); + cycle.structures[0].extends = [{ kind: "reference", name: "Child" }]; + assert.throws(() => generateTypeScript(cycle, ["test/feature"]), /Cyclic inheritance/); +}); + +test("middleware allowlist parsing rejects nonliteral or disabled entries", () => { + assert.deepEqual(getMiddlewareMethods('const supportedMethods = { "textDocument/hover": true };'), ["textDocument/hover"]); + for (const source of ["const supportedMethods = {}; const other = {};", "const supportedMethods = methods;", 'const supportedMethods = { "textDocument/hover": false };', "const supportedMethods = { ...methods };"]) { + if (source.includes("supportedMethods = {}")) { + assert.deepEqual(getMiddlewareMethods(source), []); + } + else { + assert.throws(() => getMiddlewareMethods(source), /Middleware/); + } + } +}); + +test("committed TypeScript protocol types are reproducible from the shared model", () => { + const root = path.resolve(import.meta.dirname, "../../.."); + const outputPath = path.join(root, "packages/typescript/src/vscode/protocol.generated.ts"); + const methods = getMiddlewareMethods(fs.readFileSync(path.join(root, "packages/vscode-typescript/src/lspMiddleware.ts"), "utf8")); + const output = generateTypeScript(getTypeScriptModel(), methods); + const formatted = execFileSync(path.join(root, "node_modules/.bin/dprint"), ["fmt", "--stdin", outputPath], { input: output, encoding: "utf8" }); + assert.equal(formatted, fs.readFileSync(outputPath, "utf8")); + const file = ts.createSourceFile("generated.ts", output, ts.ScriptTarget.Latest, true); + assert.ok(file.statements.every(statement => ts.isInterfaceDeclaration(statement) || ts.isTypeAliasDeclaration(statement))); + assert.doesNotMatch(output, /CompletionItemData|InitializeParams|Shutdown/); + assert.match(output, /"verbosityLevel"\?: number \| undefined/); + assert.match(output, /"canIncreaseVerbosity"\?: boolean \| undefined/); + assert.match(output, /"data"\?: LSPAny \| undefined/); + assert.match(output, /"custom\/textDocument\/sourceDefinition"/); + assert.match(output, /"textDocument\/_vs_onAutoInsert"/); + assert.match(output, /"textDocument\/_vs_references"/); +}); + +test("wire model snapshots do not expose Go-specific opaque data structures", () => { + const model = getTypeScriptModel(); + const completion = model.structures.find(structure => structure.name === "CompletionItem"); + assert.deepEqual(completion?.properties.find(property => property.name === "data")?.type, { kind: "reference", name: "LSPAny" }); + model.requests[0].method = "modified/by/test"; + assert.notEqual(getTypeScriptModel().requests[0].method, "modified/by/test"); +}); diff --git a/tsc/internal/lsp/lsproto/_generate/generate.mts b/tsc/internal/lsp/lsproto/_generate/generate.mts index a08413c59965f..6f983c63ab8fe 100755 --- a/tsc/internal/lsp/lsproto/_generate/generate.mts +++ b/tsc/internal/lsp/lsproto/_generate/generate.mts @@ -31,7 +31,13 @@ if (!fs.existsSync(metaModelPath)) { process.exit(1); } -const model: MetaModel = JSON.parse(fs.readFileSync(metaModelPath, "utf-8")); +const originalModel: MetaModel = JSON.parse(fs.readFileSync(metaModelPath, "utf-8")); +const model = structuredClone(originalModel); +let typeScriptModel: MetaModel; + +export function getTypeScriptModel(): MetaModel { + return structuredClone(typeScriptModel); +} // Custom structures to add to the model const customStructures: Structure[] = [ @@ -1270,6 +1276,26 @@ function patchAndPreprocessModel() { model.requests.push(...customRequests); model.notifications.push(...customNotifications); + typeScriptModel = structuredClone({ + ...model, + typeAliases: [...model.typeAliases, ...customTypeAliases], + }); + const sourceDefinition = typeScriptModel.requests.find(request => request.method === "custom/textDocument/sourceDefinition"); + const definition = typeScriptModel.requests.find(request => request.method === "textDocument/definition"); + if (!sourceDefinition || !definition) throw new Error("Missing definition request schemas."); + // The Go request names a synthesized union; clients use the standard definition result. + sourceDefinition.result = structuredClone(definition.result); + // Go's typed data structures are implementation details, not client wire contracts. + for (const structure of originalModel.structures) { + for (const property of structure.properties) { + if (property.name === "data" && property.type.kind === "reference" && property.type.name === "LSPAny") { + const target = typeScriptModel.structures.find(candidate => candidate.name === structure.name)?.properties.find(candidate => candidate.name === property.name); + if (!target) throw new Error(`Missing wire property ${structure.name}.${property.name}`); + target.type = structuredClone(property.type); + } + } + } + // Build structure map for preprocessing const structureMap = new Map(); for (const structure of model.structures) { diff --git a/tsc/internal/lsp/lsproto/_generate/generateTypeScript.mts b/tsc/internal/lsp/lsproto/_generate/generateTypeScript.mts new file mode 100644 index 0000000000000..cfe69e42acb61 --- /dev/null +++ b/tsc/internal/lsp/lsproto/_generate/generateTypeScript.mts @@ -0,0 +1,31 @@ +import fs from "node:fs"; +import path from "node:path"; +import { x } from "tinyexec"; +import ts from "typescript"; +import { getTypeScriptModel } from "./generate.mts"; +import { generateTypeScript } from "./typeScript.mts"; + +const root = path.resolve(import.meta.dirname, "../../../../.."); +const output = path.join(root, "packages/typescript/src/vscode/protocol.generated.ts"); +const registry = path.join(root, "packages/vscode-typescript/src/lspMiddleware.ts"); + +export function getMiddlewareMethods(source: string): string[] { + const file = ts.createSourceFile(registry, source, ts.ScriptTarget.Latest, true); + const declarations = file.statements.filter(ts.isVariableStatement).flatMap(statement => statement.declarationList.declarations); + const declaration = declarations.find(declaration => ts.isIdentifier(declaration.name) && declaration.name.text === "supportedMethods"); + if (!declaration?.initializer || !ts.isObjectLiteralExpression(declaration.initializer)) throw new Error("Middleware supportedMethods must be an object literal."); + return declaration.initializer.properties.map(property => { + if (!ts.isPropertyAssignment(property) || !ts.isStringLiteral(property.name) || property.initializer.kind !== ts.SyntaxKind.TrueKeyword) { + throw new Error("Middleware methods must be string literal properties with value true."); + } + return property.name.text; + }); +} + +export default async function generate() { + const content = generateTypeScript(getTypeScriptModel(), getMiddlewareMethods(fs.readFileSync(registry, "utf8"))); + fs.writeFileSync(output, content); + await x("dprint", ["fmt", output], { throwOnError: true, nodeOptions: { cwd: root, stdio: "inherit" } }); +} + +if (process.argv[1] === import.meta.filename) await generate(); diff --git a/tsc/internal/lsp/lsproto/_generate/typeScript.mts b/tsc/internal/lsp/lsproto/_generate/typeScript.mts new file mode 100644 index 0000000000000..acd97f6f54779 --- /dev/null +++ b/tsc/internal/lsp/lsproto/_generate/typeScript.mts @@ -0,0 +1,122 @@ +import type { + Enumeration, + MetaModel, + Property, + Structure, + Type, + TypeAlias, +} from "./metaModelSchema.mts"; + +/** Generates only the types reachable from the selected client-to-server requests. */ +export function generateTypeScript(model: MetaModel, methods: readonly string[]): string { + const definitions = new Map([ + ...model.structures.map(value => [value.name, value] as const), + ...model.enumerations.map(value => [value.name, value] as const), + ...model.typeAliases.map(value => [value.name, value] as const), + ]); + const declarations = new Map(); + const visited = new Set(); + const baseTypes = { URI: "string", DocumentUri: "string", integer: "number", uinteger: "number", decimal: "number", RegExp: "string", string: "string", boolean: "boolean", null: "null" }; + + function documentation(text: string | undefined): string { + if (!text) return ""; + return `/**\n${text.replaceAll("*/", "* /").split("\n").map(line => ` * ${line}`).join("\n")}\n */\n`; + } + + function properties(values: readonly Property[]): string { + return values.map(property => { + const optional = property.optional || property.omitzeroValue; + return `${documentation(property.documentation)}${JSON.stringify(property.name)}${optional ? "?" : ""}: ${type(property.type)}${optional ? " | undefined" : ""};`; + }).join("\n"); + } + + function structureProperties(structure: Structure, ancestors = new Set()): Property[] { + if (ancestors.has(structure.name)) throw new Error(`Cyclic inheritance in ${structure.name}`); + const inherited = new Map(); + const nextAncestors = new Set([...ancestors, structure.name]); + for (const parent of [...structure.extends ?? [], ...structure.mixins ?? []]) { + if (parent.kind !== "reference") throw new Error(`Unsupported inheritance type in ${structure.name}: ${parent.kind}`); + const definition = definitions.get(parent.name); + if (!definition || !("properties" in definition)) throw new Error(`Unknown parent structure ${parent.name}`); + for (const property of structureProperties(definition, nextAncestors)) inherited.set(property.name, property); + } + for (const property of structure.properties) inherited.set(property.name, property); + return [...inherited.values()]; + } + + function reference(name: string): string { + if (visited.has(name)) return name; + visited.add(name); + const definition = definitions.get(name); + if (!definition) throw new Error(`Unknown LSP type ${name}`); + let declaration: string; + if ("properties" in definition) { + // The metamodel omits FormattingOptions' open extension properties. + const index = name === "FormattingOptions" ? "\n[key: string]: boolean | number | string | undefined;" : ""; + declaration = `export interface ${name} {\n${properties(structureProperties(definition))}${index}\n}`; + } + else if ("values" in definition) { + const values = definition.values.map(value => JSON.stringify(value.value)); + if (definition.supportsCustomValues) values.push(baseTypes[definition.type.name]); + declaration = `export type ${name} = ${values.join(" | ") || "never"};`; + } + else { + declaration = `export type ${name} = ${type(definition.type)};`; + } + declarations.set(name, documentation(definition.documentation) + declaration); + return name; + } + + function type(value: Type): string { + switch (value.kind) { + case "base": + if (value.name === "DocumentUri" || value.name === "URI") { + declarations.set(value.name, `export type ${value.name} = string;`); + return value.name; + } + return baseTypes[value.name]; + case "reference": + return reference(value.name); + case "array": + return `(${type(value.element)})[]`; + case "map": + return `{ [key: ${type(value.key)}]: ${type(value.value)}; }`; + case "and": + return `(${value.items.map(type).join(" & ")})`; + case "or": + return `(${[...new Set(value.items.map(type))].join(" | ")})`; + case "tuple": + return `[${value.items.map(type).join(", ")}]`; + case "literal": + return `{\n${properties(value.value.properties)}\n}`; + case "stringLiteral": + case "integerLiteral": + case "booleanLiteral": + return JSON.stringify(value.value); + default: { + const exhaustive: never = value; + throw new Error(`Unsupported LSP type ${JSON.stringify(exhaustive)}`); + } + } + } + + const requests = new Map(model.requests.map(request => [request.method, request])); + const selected = new Set(); + const entries = methods.map(method => { + if (selected.has(method)) throw new Error(`Duplicate middleware method ${method}`); + selected.add(method); + const request = requests.get(method); + if (!request || request.messageDirection !== "clientToServer") throw new Error(`Not a client-to-server request: ${method}`); + const params = !request.params ? "undefined" : Array.isArray(request.params) ? `[${request.params.map(type).join(", ")}]` : type(request.params); + return `${JSON.stringify(method)}: { params: ${params}; result: ${type(request.result)}; };`; + }); + return [ + "// Code generated by tsc/internal/lsp/lsproto/_generate/generateTypeScript.mts. DO NOT EDIT.", + "// LSP metamodel copyright (c) Microsoft Corporation. Licensed under the MIT License.", + "", + ...[...declarations].sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([, declaration]) => declaration + "\n"), + "/** Language-feature requests supported by the extension middleware API. */", + `export interface LspMiddlewareRequests {\n${entries.join("\n")}\n}`, + "", + ].join("\n"); +} From c1607f67a089761b343e7d63fc11f22b51c22617 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Thu, 1 Oct 2026 16:55:58 -0700 Subject: [PATCH 04/10] Move lsproto generation to top-level scripts --- Herebyfile.mjs | 12 +- .../src/vscode/protocol.generated.ts | 2 +- tools/scripts/gen/lspTypeScript.test.mts | 8 +- .../scripts/lsp}/.gitignore | 0 .../scripts/lsp}/fetchModel.mts | 2 +- .../scripts/lsp}/generate.mts | 6 +- .../scripts/lsp}/generateTypeScript.mts | 2 +- tools/scripts/lsp/metaModelSchema.mts | 634 +++++++++++++++++ .../scripts/lsp}/tsconfig.json | 2 +- .../scripts/lsp}/typeScript.mts | 2 +- .../lsp/lsproto/_generate/metaModelSchema.mts | 635 ------------------ 11 files changed, 652 insertions(+), 653 deletions(-) rename {tsc/internal/lsp/lsproto/_generate => tools/scripts/lsp}/.gitignore (100%) rename {tsc/internal/lsp/lsproto/_generate => tools/scripts/lsp}/fetchModel.mts (94%) mode change 100755 => 100644 rename {tsc/internal/lsp/lsproto/_generate => tools/scripts/lsp}/generate.mts (97%) mode change 100755 => 100644 rename {tsc/internal/lsp/lsproto/_generate => tools/scripts/lsp}/generateTypeScript.mts (94%) create mode 100644 tools/scripts/lsp/metaModelSchema.mts rename {tsc/internal/lsp/lsproto/_generate => tools/scripts/lsp}/tsconfig.json (92%) rename {tsc/internal/lsp/lsproto/_generate => tools/scripts/lsp}/typeScript.mts (96%) delete mode 100644 tsc/internal/lsp/lsproto/_generate/metaModelSchema.mts diff --git a/Herebyfile.mjs b/Herebyfile.mjs index 82ae71b370051..adb584f80a87d 100644 --- a/Herebyfile.mjs +++ b/Herebyfile.mjs @@ -592,22 +592,22 @@ export const generateExtensionTest = task({ async function runGenerateLSP() { const { GeneratedFile } = await import("./tools/scripts/gen/generatedFile.mts"); - const directory = path.join(__dirname, "tsc/internal/lsp/lsproto/_generate"); + const directory = path.join(__dirname, "tools/scripts/lsp"); 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")])); if (!modelFiles.every(file => file.isCurrent(!!options.force))) { for (const file of modelFiles) file.invalidate(); - const { default: fetchModel } = await import("./tsc/internal/lsp/lsproto/_generate/fetchModel.mts"); + const { default: fetchModel } = await import("./tools/scripts/lsp/fetchModel.mts"); await fetchModel(); for (const file of modelFiles) file.markCurrent(); } - const output = new GeneratedFile(path.join(directory, "../lsp_generated.go"), [ + const output = new GeneratedFile(path.join(__dirname, "tsc/internal/lsp/lsproto/lsp_generated.go"), [ __filename, path.join(directory, "generate.mts"), ...modelFiles.map(file => file.fileName), ]); if (!output.isCurrent(!!options.force)) { output.invalidate(); - const { default: generate } = await import("./tsc/internal/lsp/lsproto/_generate/generate.mts"); + const { default: generate } = await import("./tools/scripts/lsp/generate.mts"); await generate(); output.markCurrent(); } @@ -624,7 +624,7 @@ async function runGenerateLSP() { ]); if (!typeScriptOutput.isCurrent(!!options.force)) { typeScriptOutput.invalidate(); - const { default: generate } = await import("./tsc/internal/lsp/lsproto/_generate/generateTypeScript.mts"); + const { default: generate } = await import("./tools/scripts/lsp/generateTypeScript.mts"); await generate(); typeScriptOutput.markCurrent(); } @@ -1305,7 +1305,7 @@ export const checkVsceVersion = task({ const scriptTsconfigs = [ "./tools/scripts/gen/tsconfig.json", "./tools/scripts/tsc/tsconfig.json", - "./tsc/internal/lsp/lsproto/_generate/tsconfig.json", + "./tools/scripts/lsp/tsconfig.json", ]; export const checkScripts = task({ diff --git a/packages/typescript/src/vscode/protocol.generated.ts b/packages/typescript/src/vscode/protocol.generated.ts index e972203028a7e..db81fca3ac632 100644 --- a/packages/typescript/src/vscode/protocol.generated.ts +++ b/packages/typescript/src/vscode/protocol.generated.ts @@ -1,4 +1,4 @@ -// Code generated by tsc/internal/lsp/lsproto/_generate/generateTypeScript.mts. DO NOT EDIT. +// Code generated by tools/scripts/lsp/generateTypeScript.mts. DO NOT EDIT. // LSP metamodel copyright (c) Microsoft Corporation. Licensed under the MIT License. /** diff --git a/tools/scripts/gen/lspTypeScript.test.mts b/tools/scripts/gen/lspTypeScript.test.mts index 9844c7a0712f4..979dfa480f584 100644 --- a/tools/scripts/gen/lspTypeScript.test.mts +++ b/tools/scripts/gen/lspTypeScript.test.mts @@ -4,10 +4,10 @@ import fs from "node:fs"; import path from "node:path"; import { test } from "node:test"; import ts from "typescript"; -import { getTypeScriptModel } from "../../../tsc/internal/lsp/lsproto/_generate/generate.mts"; -import { getMiddlewareMethods } from "../../../tsc/internal/lsp/lsproto/_generate/generateTypeScript.mts"; -import type { MetaModel } from "../../../tsc/internal/lsp/lsproto/_generate/metaModelSchema.mts"; -import { generateTypeScript } from "../../../tsc/internal/lsp/lsproto/_generate/typeScript.mts"; +import { getTypeScriptModel } from "../lsp/generate.mts"; +import { getMiddlewareMethods } from "../lsp/generateTypeScript.mts"; +import type { MetaModel } from "../lsp/metaModelSchema.mts"; +import { generateTypeScript } from "../lsp/typeScript.mts"; function fixture(): MetaModel { return { diff --git a/tsc/internal/lsp/lsproto/_generate/.gitignore b/tools/scripts/lsp/.gitignore similarity index 100% rename from tsc/internal/lsp/lsproto/_generate/.gitignore rename to tools/scripts/lsp/.gitignore diff --git a/tsc/internal/lsp/lsproto/_generate/fetchModel.mts b/tools/scripts/lsp/fetchModel.mts old mode 100755 new mode 100644 similarity index 94% rename from tsc/internal/lsp/lsproto/_generate/fetchModel.mts rename to tools/scripts/lsp/fetchModel.mts index 02db3453f5de6..e24a717d8ba9d --- a/tsc/internal/lsp/lsproto/_generate/fetchModel.mts +++ b/tools/scripts/lsp/fetchModel.mts @@ -13,7 +13,7 @@ const metaModelPath = path.join(__dirname, "metaModel.json"); const metaModelSchemaPath = path.join(__dirname, "metaModelSchema.mts"); export default async function fetchModel() { - const lockfilePath = path.resolve(__dirname, "../../../../../package-lock.json"); + const lockfilePath = path.resolve(__dirname, "../../../package-lock.json"); const lockfile = JSON.parse(fs.readFileSync(lockfilePath, "utf-8")); const clientVersion: string = lockfile.packages["node_modules/vscode-languageclient"].version; diff --git a/tsc/internal/lsp/lsproto/_generate/generate.mts b/tools/scripts/lsp/generate.mts old mode 100755 new mode 100644 similarity index 97% rename from tsc/internal/lsp/lsproto/_generate/generate.mts rename to tools/scripts/lsp/generate.mts index 6f983c63ab8fe..1f160c3403f6d --- a/tsc/internal/lsp/lsproto/_generate/generate.mts +++ b/tools/scripts/lsp/generate.mts @@ -21,13 +21,13 @@ import type { const __filename = url.fileURLToPath(new URL(import.meta.url)); const __dirname = path.dirname(__filename); -const repoRoot = path.resolve(__dirname, "../../../.."); +const repoRoot = path.resolve(__dirname, "../../.."); -const out = path.resolve(__dirname, "../lsp_generated.go"); +const out = path.join(repoRoot, "tsc/internal/lsp/lsproto/lsp_generated.go"); const metaModelPath = path.resolve(__dirname, "metaModel.json"); if (!fs.existsSync(metaModelPath)) { - console.error("Meta model file not found; did you forget to run fetchModel.mjs?"); + console.error("Meta model file not found; did you forget to run fetchModel.mts?"); process.exit(1); } diff --git a/tsc/internal/lsp/lsproto/_generate/generateTypeScript.mts b/tools/scripts/lsp/generateTypeScript.mts similarity index 94% rename from tsc/internal/lsp/lsproto/_generate/generateTypeScript.mts rename to tools/scripts/lsp/generateTypeScript.mts index cfe69e42acb61..9edae682035ea 100644 --- a/tsc/internal/lsp/lsproto/_generate/generateTypeScript.mts +++ b/tools/scripts/lsp/generateTypeScript.mts @@ -5,7 +5,7 @@ import ts from "typescript"; import { getTypeScriptModel } from "./generate.mts"; import { generateTypeScript } from "./typeScript.mts"; -const root = path.resolve(import.meta.dirname, "../../../../.."); +const root = path.resolve(import.meta.dirname, "../../.."); const output = path.join(root, "packages/typescript/src/vscode/protocol.generated.ts"); const registry = path.join(root, "packages/vscode-typescript/src/lspMiddleware.ts"); diff --git a/tools/scripts/lsp/metaModelSchema.mts b/tools/scripts/lsp/metaModelSchema.mts new file mode 100644 index 0000000000000..5afa9d1ebf0c1 --- /dev/null +++ b/tools/scripts/lsp/metaModelSchema.mts @@ -0,0 +1,634 @@ +/* -------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * ------------------------------------------------------------------------------------------ */ + +export type BaseTypes = "URI" | "DocumentUri" | "integer" | "uinteger" | "decimal" | "RegExp" | "string" | "boolean" | "null"; + +export type TypeKind = "base" | "reference" | "array" | "map" | "and" | "or" | "tuple" | "literal" | "stringLiteral" | "integerLiteral" | "booleanLiteral"; + +/** + * Indicates in which direction a message is sent in the protocol. + */ +export type MessageDirection = "clientToServer" | "serverToClient" | "both"; + +/** + * Represents a base type like `string` or `DocumentUri`. + */ +export type BaseType = { + kind: "base"; + name: BaseTypes; +}; + +/** + * Represents a reference to another type (e.g. `TextDocument`). + * This is either a `Structure`, a `Enumeration` or a `TypeAlias` + * in the same meta model. + */ +export type ReferenceType = { + kind: "reference"; + name: string; +}; + +/** + * Represents an array type (e.g. `TextDocument[]`). + */ +export type ArrayType = { + kind: "array"; + element: Type; +}; + +/** + * Represents a type that can be used as a key in a + * map type. If a reference type is used then the + * type must either resolve to a `string` or `integer` + * type. (e.g. `type ChangeAnnotationIdentifier === string`). + */ +export type MapKeyType = { kind: "base"; name: "URI" | "DocumentUri" | "string" | "integer"; } | ReferenceType; + +/** + * Represents a JSON object map + * (e.g. `interface Map { [key: K] => V; }`). + */ +export type MapType = { + kind: "map"; + key: MapKeyType; + value: Type; +}; + +/** + * Represents an `and` type + * (e.g. TextDocumentParams & WorkDoneProgressParams`). + */ +export type AndType = { + kind: "and"; + items: Type[]; +}; + +/** + * Represents an `or` type + * (e.g. `Location | LocationLink`). + */ +export type OrType = { + kind: "or"; + items: Type[]; +}; + +/** + * Represents a `tuple` type + * (e.g. `[integer, integer]`). + */ +export type TupleType = { + kind: "tuple"; + items: Type[]; +}; + +/** + * Represents a literal structure + * (e.g. `property: { start: uinteger; end: uinteger; }`). + */ +export type StructureLiteralType = { + kind: "literal"; + value: StructureLiteral; +}; + +/** + * Represents a string literal type + * (e.g. `kind: 'rename'`). + */ +export type StringLiteralType = { + kind: "stringLiteral"; + value: string; +}; + +export type IntegerLiteralType = { + /** + * Represents an integer literal type + * (e.g. `kind: 1`). + */ + kind: "integerLiteral"; + value: number; +}; + +/** + * Represents a boolean literal type + * (e.g. `kind: true`). + */ +export type BooleanLiteralType = { + kind: "booleanLiteral"; + value: boolean; +}; + +export type Type = BaseType | ReferenceType | ArrayType | MapType | AndType | OrType | TupleType | StructureLiteralType | StringLiteralType | IntegerLiteralType | BooleanLiteralType; + +/** + * Represents a LSP request + */ +export type Request = { + /** + * The request's method name. + */ + method: string; + + /** + * The type name of the request if any. + */ + typeName?: string; + + /** + * The parameter type(s) if any. + */ + params?: Type | Type[]; + + /** + * The result type. + */ + result: Type; + + /** + * Optional partial result type if the request + * supports partial result reporting. + */ + partialResult?: Type; + + /** + * An optional error data type. + */ + errorData?: Type; + + /** + * Optional a dynamic registration method if it + * different from the request's method. + */ + registrationMethod?: string; + + /** + * Optional registration options if the request + * supports dynamic registration. + */ + registrationOptions?: Type; + + /** + * The direction in which this request is sent + * in the protocol. + */ + messageDirection: MessageDirection; + + /** + * An optional documentation; + */ + documentation?: string; + + /** + * Since when (release number) this request is + * available. Is undefined if not known. + */ + since?: string; + + /** + * All since tags in case there was more than one tag. + * Is undefined if not known. + */ + sinceTags?: string[]; + + /** + * Whether this is a proposed feature. If omitted + * the feature is final. + */ + proposed?: boolean; + + /** + * Whether the request is deprecated or not. If deprecated + * the property contains the deprecation message. + */ + deprecated?: string; + + /** + * The client capability property path if any. + */ + clientCapability?: string; + + /** + * The server capability property path if any. + */ + serverCapability?: string; +}; + +/** + * Represents a LSP notification + */ +export type Notification = { + /** + * The notifications's method name. + */ + method: string; + + /** + * The type name of the notifications if any. + */ + typeName?: string; + + /** + * The parameter type(s) if any. + */ + params?: Type | Type[]; + + /** + * Optional a dynamic registration method if it + * different from the notifications's method. + */ + registrationMethod?: string; + + /** + * Optional registration options if the notification + * supports dynamic registration. + */ + registrationOptions?: Type; + + /** + * The direction in which this notification is sent + * in the protocol. + */ + messageDirection: MessageDirection; + + /** + * An optional documentation; + */ + documentation?: string; + + /** + * Since when (release number) this notification is + * available. Is undefined if not known. + */ + since?: string; + + /** + * All since tags in case there was more than one tag. + * Is undefined if not known. + */ + sinceTags?: string[]; + + /** + * Whether this is a proposed notification. If omitted + * the notification is final. + */ + proposed?: boolean; + + /** + * Whether the notification is deprecated or not. If deprecated + * the property contains the deprecation message. + */ + deprecated?: string; + + /** + * The client capability property path if any. + */ + clientCapability?: string; + + /** + * The server capability property path if any. + */ + serverCapability?: string; +}; + +/** + * Represents an object property. + */ +export type Property = { + /** + * The property name; + */ + name: string; + + /** + * The type of the property + */ + type: Type; + + /** + * Whether the property is optional. If + * omitted, the property is mandatory. + */ + optional?: boolean; + + /** + * An optional documentation. + */ + documentation?: string; + + /** + * Since when (release number) this property is + * available. Is undefined if not known. + */ + since?: string; + + /** + * All since tags in case there was more than one tag. + * Is undefined if not known. + */ + sinceTags?: string[]; + + /** + * Whether this is a proposed property. If omitted, + * the structure is final. + */ + proposed?: boolean; + + /** + * Whether the property is deprecated or not. If deprecated + * the property contains the deprecation message. + */ + deprecated?: string; + + /** + * Whether this property uses omitzero without being a pointer. + * Custom extension for special value types. + */ + omitzeroValue?: boolean; +}; + +/** + * Defines the structure of an object literal. + */ +export type Structure = { + /** + * The name of the structure. + */ + name: string; + + /** + * Structures extended from. This structures form + * a polymorphic type hierarchy. + */ + extends?: Type[]; + + /** + * Structures to mix in. The properties of these + * structures are `copied` into this structure. + * Mixins don't form a polymorphic type hierarchy in + * LSP. + */ + mixins?: Type[]; + + /** + * The properties. + */ + properties: Property[]; + + /** + * An optional documentation; + */ + documentation?: string; + + /** + * Since when (release number) this structure is + * available. Is undefined if not known. + */ + since?: string; + + /** + * All since tags in case there was more than one tag. + * Is undefined if not known. + */ + sinceTags?: string[]; + + /** + * Whether this is a proposed structure. If omitted, + * the structure is final. + */ + proposed?: boolean; + + /** + * Whether the structure is deprecated or not. If deprecated + * the property contains the deprecation message. + */ + deprecated?: string; +}; + +/** + * Defines an unnamed structure of an object literal. + */ +export type StructureLiteral = { + /** + * The properties. + */ + properties: Property[]; + + /** + * An optional documentation. + */ + documentation?: string; + + /** + * Since when (release number) this structure is + * available. Is undefined if not known. + */ + since?: string; + + /** + * All since tags in case there was more than one tag. + * Is undefined if not known. + */ + sinceTags?: string[]; + + /** + * Whether this is a proposed structure. If omitted, + * the structure is final. + */ + proposed?: boolean; + + /** + * Whether the literal is deprecated or not. If deprecated + * the property contains the deprecation message. + */ + deprecated?: string; +}; + +/** + * Defines a type alias. + * (e.g. `type Definition = Location | LocationLink`) + */ +export type TypeAlias = { + /** + * The name of the type alias. + */ + name: string; + + /** + * The aliased type. + */ + type: Type; + + /** + * An optional documentation. + */ + documentation?: string; + + /** + * Since when (release number) this structure is + * available. Is undefined if not known. + */ + since?: string; + + /** + * All since tags in case there was more than one tag. + * Is undefined if not known. + */ + sinceTags?: string[]; + + /** + * Whether this is a proposed type alias. If omitted, + * the type alias is final. + */ + proposed?: boolean; + + /** + * Whether the type alias is deprecated or not. If deprecated + * the property contains the deprecation message. + */ + deprecated?: string; +}; + +/** + * Defines an enumeration entry. + */ +export type EnumerationEntry = { + /** + * The name of the enum item. + */ + name: string; + + /** + * The value. + */ + value: string | number; + + /** + * An optional documentation. + */ + documentation?: string; + + /** + * Since when (release number) this enumeration entry is + * available. Is undefined if not known. + */ + since?: string; + + /** + * All since tags in case there was more than one tag. + * Is undefined if not known. + */ + sinceTags?: string[]; + + /** + * Whether this is a proposed enumeration entry. If omitted, + * the enumeration entry is final. + */ + proposed?: boolean; + + /** + * Whether the enum entry is deprecated or not. If deprecated + * the property contains the deprecation message. + */ + deprecated?: string; +}; + +export type EnumerationType = { kind: "base"; name: "string" | "integer" | "uinteger"; }; + +/** + * Defines an enumeration. + */ +export type Enumeration = { + /** + * The name of the enumeration. + */ + name: string; + + /** + * The type of the elements. + */ + type: EnumerationType; + + /** + * The enum values. + */ + values: EnumerationEntry[]; + + /** + * Whether the enumeration supports custom values (e.g. values which are not + * part of the set defined in `values`). If omitted no custom values are + * supported. + */ + supportsCustomValues?: boolean; + + /** + * An optional documentation. + */ + documentation?: string; + + /** + * Since when (release number) this enumeration is + * available. Is undefined if not known. + */ + since?: string; + + /** + * All since tags in case there was more than one tag. + * Is undefined if not known. + */ + sinceTags?: string[]; + + /** + * Whether this is a proposed enumeration. If omitted, + * the enumeration is final. + */ + proposed?: boolean; + + /** + * Whether the enumeration is deprecated or not. If deprecated + * the property contains the deprecation message. + */ + deprecated?: string; +}; + +export type MetaData = { + /** + * The protocol version. + */ + version: string; +}; + +/** + * The actual meta model. + */ +export type MetaModel = { + /** + * Additional meta data. + */ + metaData: MetaData; + + /** + * The requests. + */ + requests: Request[]; + + /** + * The notifications. + */ + notifications: Notification[]; + + /** + * The structures. + */ + structures: Structure[]; + + /** + * The enumerations. + */ + enumerations: Enumeration[]; + + /** + * The type aliases. + */ + typeAliases: TypeAlias[]; +}; diff --git a/tsc/internal/lsp/lsproto/_generate/tsconfig.json b/tools/scripts/lsp/tsconfig.json similarity index 92% rename from tsc/internal/lsp/lsproto/_generate/tsconfig.json rename to tools/scripts/lsp/tsconfig.json index 1ed48f5ac3291..3f620e5c4f01b 100644 --- a/tsc/internal/lsp/lsproto/_generate/tsconfig.json +++ b/tools/scripts/lsp/tsconfig.json @@ -11,7 +11,7 @@ "verbatimModuleSyntax": true, "types": ["node"], "noUnusedLocals": true, - "noUnusedParameters": true, + "noUnusedParameters": true }, "include": [ "*.mts", diff --git a/tsc/internal/lsp/lsproto/_generate/typeScript.mts b/tools/scripts/lsp/typeScript.mts similarity index 96% rename from tsc/internal/lsp/lsproto/_generate/typeScript.mts rename to tools/scripts/lsp/typeScript.mts index acd97f6f54779..2ba53e86a2548 100644 --- a/tsc/internal/lsp/lsproto/_generate/typeScript.mts +++ b/tools/scripts/lsp/typeScript.mts @@ -111,7 +111,7 @@ export function generateTypeScript(model: MetaModel, methods: readonly string[]) return `${JSON.stringify(method)}: { params: ${params}; result: ${type(request.result)}; };`; }); return [ - "// Code generated by tsc/internal/lsp/lsproto/_generate/generateTypeScript.mts. DO NOT EDIT.", + "// Code generated by tools/scripts/lsp/generateTypeScript.mts. DO NOT EDIT.", "// LSP metamodel copyright (c) Microsoft Corporation. Licensed under the MIT License.", "", ...[...declarations].sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([, declaration]) => declaration + "\n"), diff --git a/tsc/internal/lsp/lsproto/_generate/metaModelSchema.mts b/tsc/internal/lsp/lsproto/_generate/metaModelSchema.mts deleted file mode 100644 index 30b2efc27bd2d..0000000000000 --- a/tsc/internal/lsp/lsproto/_generate/metaModelSchema.mts +++ /dev/null @@ -1,635 +0,0 @@ -/* -------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - * ------------------------------------------------------------------------------------------ */ - -export type BaseTypes = 'URI' | 'DocumentUri' | 'integer' | 'uinteger' | 'decimal' | 'RegExp' | 'string' | 'boolean' | 'null'; - -export type TypeKind = 'base' | 'reference' | 'array' | 'map' | 'and' | 'or' | 'tuple' | 'literal' | 'stringLiteral' | 'integerLiteral' | 'booleanLiteral'; - -/** - * Indicates in which direction a message is sent in the protocol. - */ -export type MessageDirection = 'clientToServer' | 'serverToClient' | 'both'; - -/** - * Represents a base type like `string` or `DocumentUri`. - */ -export type BaseType = { - kind: 'base'; - name: BaseTypes; -}; - -/** - * Represents a reference to another type (e.g. `TextDocument`). - * This is either a `Structure`, a `Enumeration` or a `TypeAlias` - * in the same meta model. - */ -export type ReferenceType = { - kind: 'reference'; - name: string; -}; - -/** - * Represents an array type (e.g. `TextDocument[]`). - */ -export type ArrayType = { - kind: 'array'; - element: Type; -}; - -/** - * Represents a type that can be used as a key in a - * map type. If a reference type is used then the - * type must either resolve to a `string` or `integer` - * type. (e.g. `type ChangeAnnotationIdentifier === string`). - */ -export type MapKeyType = { kind: 'base'; name: 'URI' | 'DocumentUri' | 'string' | 'integer' } | ReferenceType; - -/** - * Represents a JSON object map - * (e.g. `interface Map { [key: K] => V; }`). - */ -export type MapType = { - kind: 'map'; - key: MapKeyType; - value: Type; -}; - -/** - * Represents an `and` type - * (e.g. TextDocumentParams & WorkDoneProgressParams`). - */ -export type AndType = { - kind: 'and'; - items: Type[]; -}; - -/** - * Represents an `or` type - * (e.g. `Location | LocationLink`). - */ -export type OrType = { - kind: 'or'; - items: Type[]; -}; - -/** - * Represents a `tuple` type - * (e.g. `[integer, integer]`). - */ -export type TupleType = { - kind: 'tuple'; - items: Type[]; -}; - -/** - * Represents a literal structure - * (e.g. `property: { start: uinteger; end: uinteger; }`). - */ -export type StructureLiteralType = { - kind: 'literal'; - value: StructureLiteral; -}; - -/** - * Represents a string literal type - * (e.g. `kind: 'rename'`). - */ -export type StringLiteralType = { - kind: 'stringLiteral'; - value: string; -}; - -export type IntegerLiteralType = { - /** - * Represents an integer literal type - * (e.g. `kind: 1`). - */ - kind: 'integerLiteral'; - value: number; -}; - -/** - * Represents a boolean literal type - * (e.g. `kind: true`). - */ -export type BooleanLiteralType = { - kind: 'booleanLiteral'; - value: boolean; -}; - -export type Type = BaseType | ReferenceType | ArrayType | MapType | AndType | OrType | TupleType | StructureLiteralType | StringLiteralType | IntegerLiteralType | BooleanLiteralType; - -/** - * Represents a LSP request - */ -export type Request = { - /** - * The request's method name. - */ - method: string; - - /** - * The type name of the request if any. - */ - typeName?: string; - - /** - * The parameter type(s) if any. - */ - params?: Type | Type[]; - - /** - * The result type. - */ - result: Type; - - /** - * Optional partial result type if the request - * supports partial result reporting. - */ - partialResult?: Type; - - /** - * An optional error data type. - */ - errorData?: Type; - - /** - * Optional a dynamic registration method if it - * different from the request's method. - */ - registrationMethod?: string; - - /** - * Optional registration options if the request - * supports dynamic registration. - */ - registrationOptions?: Type; - - /** - * The direction in which this request is sent - * in the protocol. - */ - messageDirection: MessageDirection; - - /** - * An optional documentation; - */ - documentation?: string; - - /** - * Since when (release number) this request is - * available. Is undefined if not known. - */ - since?: string; - - /** - * All since tags in case there was more than one tag. - * Is undefined if not known. - */ - sinceTags?: string[]; - - /** - * Whether this is a proposed feature. If omitted - * the feature is final. - */ - proposed?: boolean; - - /** - * Whether the request is deprecated or not. If deprecated - * the property contains the deprecation message. - */ - deprecated?: string; - - /** - * The client capability property path if any. - */ - clientCapability?: string; - - /** - * The server capability property path if any. - */ - serverCapability?: string; -}; - -/** - * Represents a LSP notification - */ -export type Notification = { - /** - * The notifications's method name. - */ - method: string; - - /** - * The type name of the notifications if any. - */ - typeName?: string; - - /** - * The parameter type(s) if any. - */ - params?: Type | Type[]; - - /** - * Optional a dynamic registration method if it - * different from the notifications's method. - */ - registrationMethod?: string; - - /** - * Optional registration options if the notification - * supports dynamic registration. - */ - registrationOptions?: Type; - - /** - * The direction in which this notification is sent - * in the protocol. - */ - messageDirection: MessageDirection; - - /** - * An optional documentation; - */ - documentation?: string; - - /** - * Since when (release number) this notification is - * available. Is undefined if not known. - */ - since?: string; - - /** - * All since tags in case there was more than one tag. - * Is undefined if not known. - */ - sinceTags?: string[]; - - /** - * Whether this is a proposed notification. If omitted - * the notification is final. - */ - proposed?: boolean; - - /** - * Whether the notification is deprecated or not. If deprecated - * the property contains the deprecation message. - */ - deprecated?: string; - - /** - * The client capability property path if any. - */ - clientCapability?: string; - - /** - * The server capability property path if any. - */ - serverCapability?: string; -}; - -/** - * Represents an object property. - */ -export type Property = { - /** - * The property name; - */ - name: string; - - /** - * The type of the property - */ - type: Type; - - /** - * Whether the property is optional. If - * omitted, the property is mandatory. - */ - optional?: boolean; - - /** - * An optional documentation. - */ - documentation?: string; - - /** - * Since when (release number) this property is - * available. Is undefined if not known. - */ - since?: string; - - /** - * All since tags in case there was more than one tag. - * Is undefined if not known. - */ - sinceTags?: string[]; - - /** - * Whether this is a proposed property. If omitted, - * the structure is final. - */ - proposed?: boolean; - - /** - * Whether the property is deprecated or not. If deprecated - * the property contains the deprecation message. - */ - deprecated?: string; - - /** - * Whether this property uses omitzero without being a pointer. - * Custom extension for special value types. - */ - omitzeroValue?: boolean; -}; - -/** - * Defines the structure of an object literal. - */ -export type Structure = { - /** - * The name of the structure. - */ - name: string; - - /** - * Structures extended from. This structures form - * a polymorphic type hierarchy. - */ - extends?: Type[]; - - /** - * Structures to mix in. The properties of these - * structures are `copied` into this structure. - * Mixins don't form a polymorphic type hierarchy in - * LSP. - */ - mixins?: Type[]; - - /** - * The properties. - */ - properties: Property[]; - - /** - * An optional documentation; - */ - documentation?: string; - - /** - * Since when (release number) this structure is - * available. Is undefined if not known. - */ - since?: string; - - /** - * All since tags in case there was more than one tag. - * Is undefined if not known. - */ - sinceTags?: string[]; - - /** - * Whether this is a proposed structure. If omitted, - * the structure is final. - */ - proposed?: boolean; - - /** - * Whether the structure is deprecated or not. If deprecated - * the property contains the deprecation message. - */ - deprecated?: string; -}; - -/** - * Defines an unnamed structure of an object literal. - */ -export type StructureLiteral = { - - /** - * The properties. - */ - properties: Property[]; - - /** - * An optional documentation. - */ - documentation?: string; - - /** - * Since when (release number) this structure is - * available. Is undefined if not known. - */ - since?: string; - - /** - * All since tags in case there was more than one tag. - * Is undefined if not known. - */ - sinceTags?: string[]; - - /** - * Whether this is a proposed structure. If omitted, - * the structure is final. - */ - proposed?: boolean; - - /** - * Whether the literal is deprecated or not. If deprecated - * the property contains the deprecation message. - */ - deprecated?: string; -}; - -/** - * Defines a type alias. - * (e.g. `type Definition = Location | LocationLink`) - */ -export type TypeAlias = { - /** - * The name of the type alias. - */ - name: string; - - /** - * The aliased type. - */ - type: Type; - - /** - * An optional documentation. - */ - documentation?: string; - - /** - * Since when (release number) this structure is - * available. Is undefined if not known. - */ - since?: string; - - /** - * All since tags in case there was more than one tag. - * Is undefined if not known. - */ - sinceTags?: string[]; - - /** - * Whether this is a proposed type alias. If omitted, - * the type alias is final. - */ - proposed?: boolean; - - /** - * Whether the type alias is deprecated or not. If deprecated - * the property contains the deprecation message. - */ - deprecated?: string; -}; - -/** - * Defines an enumeration entry. - */ -export type EnumerationEntry = { - /** - * The name of the enum item. - */ - name: string; - - /** - * The value. - */ - value: string | number; - - /** - * An optional documentation. - */ - documentation?: string; - - /** - * Since when (release number) this enumeration entry is - * available. Is undefined if not known. - */ - since?: string; - - /** - * All since tags in case there was more than one tag. - * Is undefined if not known. - */ - sinceTags?: string[]; - - /** - * Whether this is a proposed enumeration entry. If omitted, - * the enumeration entry is final. - */ - proposed?: boolean; - - /** - * Whether the enum entry is deprecated or not. If deprecated - * the property contains the deprecation message. - */ - deprecated?: string; -}; - -export type EnumerationType = { kind: 'base'; name: 'string' | 'integer' | 'uinteger' }; - -/** - * Defines an enumeration. - */ -export type Enumeration = { - /** - * The name of the enumeration. - */ - name: string; - - /** - * The type of the elements. - */ - type: EnumerationType; - - /** - * The enum values. - */ - values: EnumerationEntry[]; - - /** - * Whether the enumeration supports custom values (e.g. values which are not - * part of the set defined in `values`). If omitted no custom values are - * supported. - */ - supportsCustomValues?: boolean; - - /** - * An optional documentation. - */ - documentation?: string; - - /** - * Since when (release number) this enumeration is - * available. Is undefined if not known. - */ - since?: string; - - /** - * All since tags in case there was more than one tag. - * Is undefined if not known. - */ - sinceTags?: string[]; - - /** - * Whether this is a proposed enumeration. If omitted, - * the enumeration is final. - */ - proposed?: boolean; - - /** - * Whether the enumeration is deprecated or not. If deprecated - * the property contains the deprecation message. - */ - deprecated?: string; -}; - -export type MetaData = { - /** - * The protocol version. - */ - version: string; -}; - -/** - * The actual meta model. - */ -export type MetaModel = { - /** - * Additional meta data. - */ - metaData: MetaData; - - /** - * The requests. - */ - requests: Request[]; - - /** - * The notifications. - */ - notifications: Notification[]; - - /** - * The structures. - */ - structures: Structure[]; - - /** - * The enumerations. - */ - enumerations: Enumeration[]; - - /** - * The type aliases. - */ - typeAliases: TypeAlias[]; -}; From aa1f2f49fb06818e32858f07be4caa692aa4389d Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Thu, 1 Oct 2026 17:40:07 -0700 Subject: [PATCH 05/10] Generate --- tools/scripts/lsp/metaModelSchema.mts | 1269 +++++++++++++------------ 1 file changed, 635 insertions(+), 634 deletions(-) diff --git a/tools/scripts/lsp/metaModelSchema.mts b/tools/scripts/lsp/metaModelSchema.mts index 5afa9d1ebf0c1..30b2efc27bd2d 100644 --- a/tools/scripts/lsp/metaModelSchema.mts +++ b/tools/scripts/lsp/metaModelSchema.mts @@ -1,634 +1,635 @@ -/* -------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - * ------------------------------------------------------------------------------------------ */ - -export type BaseTypes = "URI" | "DocumentUri" | "integer" | "uinteger" | "decimal" | "RegExp" | "string" | "boolean" | "null"; - -export type TypeKind = "base" | "reference" | "array" | "map" | "and" | "or" | "tuple" | "literal" | "stringLiteral" | "integerLiteral" | "booleanLiteral"; - -/** - * Indicates in which direction a message is sent in the protocol. - */ -export type MessageDirection = "clientToServer" | "serverToClient" | "both"; - -/** - * Represents a base type like `string` or `DocumentUri`. - */ -export type BaseType = { - kind: "base"; - name: BaseTypes; -}; - -/** - * Represents a reference to another type (e.g. `TextDocument`). - * This is either a `Structure`, a `Enumeration` or a `TypeAlias` - * in the same meta model. - */ -export type ReferenceType = { - kind: "reference"; - name: string; -}; - -/** - * Represents an array type (e.g. `TextDocument[]`). - */ -export type ArrayType = { - kind: "array"; - element: Type; -}; - -/** - * Represents a type that can be used as a key in a - * map type. If a reference type is used then the - * type must either resolve to a `string` or `integer` - * type. (e.g. `type ChangeAnnotationIdentifier === string`). - */ -export type MapKeyType = { kind: "base"; name: "URI" | "DocumentUri" | "string" | "integer"; } | ReferenceType; - -/** - * Represents a JSON object map - * (e.g. `interface Map { [key: K] => V; }`). - */ -export type MapType = { - kind: "map"; - key: MapKeyType; - value: Type; -}; - -/** - * Represents an `and` type - * (e.g. TextDocumentParams & WorkDoneProgressParams`). - */ -export type AndType = { - kind: "and"; - items: Type[]; -}; - -/** - * Represents an `or` type - * (e.g. `Location | LocationLink`). - */ -export type OrType = { - kind: "or"; - items: Type[]; -}; - -/** - * Represents a `tuple` type - * (e.g. `[integer, integer]`). - */ -export type TupleType = { - kind: "tuple"; - items: Type[]; -}; - -/** - * Represents a literal structure - * (e.g. `property: { start: uinteger; end: uinteger; }`). - */ -export type StructureLiteralType = { - kind: "literal"; - value: StructureLiteral; -}; - -/** - * Represents a string literal type - * (e.g. `kind: 'rename'`). - */ -export type StringLiteralType = { - kind: "stringLiteral"; - value: string; -}; - -export type IntegerLiteralType = { - /** - * Represents an integer literal type - * (e.g. `kind: 1`). - */ - kind: "integerLiteral"; - value: number; -}; - -/** - * Represents a boolean literal type - * (e.g. `kind: true`). - */ -export type BooleanLiteralType = { - kind: "booleanLiteral"; - value: boolean; -}; - -export type Type = BaseType | ReferenceType | ArrayType | MapType | AndType | OrType | TupleType | StructureLiteralType | StringLiteralType | IntegerLiteralType | BooleanLiteralType; - -/** - * Represents a LSP request - */ -export type Request = { - /** - * The request's method name. - */ - method: string; - - /** - * The type name of the request if any. - */ - typeName?: string; - - /** - * The parameter type(s) if any. - */ - params?: Type | Type[]; - - /** - * The result type. - */ - result: Type; - - /** - * Optional partial result type if the request - * supports partial result reporting. - */ - partialResult?: Type; - - /** - * An optional error data type. - */ - errorData?: Type; - - /** - * Optional a dynamic registration method if it - * different from the request's method. - */ - registrationMethod?: string; - - /** - * Optional registration options if the request - * supports dynamic registration. - */ - registrationOptions?: Type; - - /** - * The direction in which this request is sent - * in the protocol. - */ - messageDirection: MessageDirection; - - /** - * An optional documentation; - */ - documentation?: string; - - /** - * Since when (release number) this request is - * available. Is undefined if not known. - */ - since?: string; - - /** - * All since tags in case there was more than one tag. - * Is undefined if not known. - */ - sinceTags?: string[]; - - /** - * Whether this is a proposed feature. If omitted - * the feature is final. - */ - proposed?: boolean; - - /** - * Whether the request is deprecated or not. If deprecated - * the property contains the deprecation message. - */ - deprecated?: string; - - /** - * The client capability property path if any. - */ - clientCapability?: string; - - /** - * The server capability property path if any. - */ - serverCapability?: string; -}; - -/** - * Represents a LSP notification - */ -export type Notification = { - /** - * The notifications's method name. - */ - method: string; - - /** - * The type name of the notifications if any. - */ - typeName?: string; - - /** - * The parameter type(s) if any. - */ - params?: Type | Type[]; - - /** - * Optional a dynamic registration method if it - * different from the notifications's method. - */ - registrationMethod?: string; - - /** - * Optional registration options if the notification - * supports dynamic registration. - */ - registrationOptions?: Type; - - /** - * The direction in which this notification is sent - * in the protocol. - */ - messageDirection: MessageDirection; - - /** - * An optional documentation; - */ - documentation?: string; - - /** - * Since when (release number) this notification is - * available. Is undefined if not known. - */ - since?: string; - - /** - * All since tags in case there was more than one tag. - * Is undefined if not known. - */ - sinceTags?: string[]; - - /** - * Whether this is a proposed notification. If omitted - * the notification is final. - */ - proposed?: boolean; - - /** - * Whether the notification is deprecated or not. If deprecated - * the property contains the deprecation message. - */ - deprecated?: string; - - /** - * The client capability property path if any. - */ - clientCapability?: string; - - /** - * The server capability property path if any. - */ - serverCapability?: string; -}; - -/** - * Represents an object property. - */ -export type Property = { - /** - * The property name; - */ - name: string; - - /** - * The type of the property - */ - type: Type; - - /** - * Whether the property is optional. If - * omitted, the property is mandatory. - */ - optional?: boolean; - - /** - * An optional documentation. - */ - documentation?: string; - - /** - * Since when (release number) this property is - * available. Is undefined if not known. - */ - since?: string; - - /** - * All since tags in case there was more than one tag. - * Is undefined if not known. - */ - sinceTags?: string[]; - - /** - * Whether this is a proposed property. If omitted, - * the structure is final. - */ - proposed?: boolean; - - /** - * Whether the property is deprecated or not. If deprecated - * the property contains the deprecation message. - */ - deprecated?: string; - - /** - * Whether this property uses omitzero without being a pointer. - * Custom extension for special value types. - */ - omitzeroValue?: boolean; -}; - -/** - * Defines the structure of an object literal. - */ -export type Structure = { - /** - * The name of the structure. - */ - name: string; - - /** - * Structures extended from. This structures form - * a polymorphic type hierarchy. - */ - extends?: Type[]; - - /** - * Structures to mix in. The properties of these - * structures are `copied` into this structure. - * Mixins don't form a polymorphic type hierarchy in - * LSP. - */ - mixins?: Type[]; - - /** - * The properties. - */ - properties: Property[]; - - /** - * An optional documentation; - */ - documentation?: string; - - /** - * Since when (release number) this structure is - * available. Is undefined if not known. - */ - since?: string; - - /** - * All since tags in case there was more than one tag. - * Is undefined if not known. - */ - sinceTags?: string[]; - - /** - * Whether this is a proposed structure. If omitted, - * the structure is final. - */ - proposed?: boolean; - - /** - * Whether the structure is deprecated or not. If deprecated - * the property contains the deprecation message. - */ - deprecated?: string; -}; - -/** - * Defines an unnamed structure of an object literal. - */ -export type StructureLiteral = { - /** - * The properties. - */ - properties: Property[]; - - /** - * An optional documentation. - */ - documentation?: string; - - /** - * Since when (release number) this structure is - * available. Is undefined if not known. - */ - since?: string; - - /** - * All since tags in case there was more than one tag. - * Is undefined if not known. - */ - sinceTags?: string[]; - - /** - * Whether this is a proposed structure. If omitted, - * the structure is final. - */ - proposed?: boolean; - - /** - * Whether the literal is deprecated or not. If deprecated - * the property contains the deprecation message. - */ - deprecated?: string; -}; - -/** - * Defines a type alias. - * (e.g. `type Definition = Location | LocationLink`) - */ -export type TypeAlias = { - /** - * The name of the type alias. - */ - name: string; - - /** - * The aliased type. - */ - type: Type; - - /** - * An optional documentation. - */ - documentation?: string; - - /** - * Since when (release number) this structure is - * available. Is undefined if not known. - */ - since?: string; - - /** - * All since tags in case there was more than one tag. - * Is undefined if not known. - */ - sinceTags?: string[]; - - /** - * Whether this is a proposed type alias. If omitted, - * the type alias is final. - */ - proposed?: boolean; - - /** - * Whether the type alias is deprecated or not. If deprecated - * the property contains the deprecation message. - */ - deprecated?: string; -}; - -/** - * Defines an enumeration entry. - */ -export type EnumerationEntry = { - /** - * The name of the enum item. - */ - name: string; - - /** - * The value. - */ - value: string | number; - - /** - * An optional documentation. - */ - documentation?: string; - - /** - * Since when (release number) this enumeration entry is - * available. Is undefined if not known. - */ - since?: string; - - /** - * All since tags in case there was more than one tag. - * Is undefined if not known. - */ - sinceTags?: string[]; - - /** - * Whether this is a proposed enumeration entry. If omitted, - * the enumeration entry is final. - */ - proposed?: boolean; - - /** - * Whether the enum entry is deprecated or not. If deprecated - * the property contains the deprecation message. - */ - deprecated?: string; -}; - -export type EnumerationType = { kind: "base"; name: "string" | "integer" | "uinteger"; }; - -/** - * Defines an enumeration. - */ -export type Enumeration = { - /** - * The name of the enumeration. - */ - name: string; - - /** - * The type of the elements. - */ - type: EnumerationType; - - /** - * The enum values. - */ - values: EnumerationEntry[]; - - /** - * Whether the enumeration supports custom values (e.g. values which are not - * part of the set defined in `values`). If omitted no custom values are - * supported. - */ - supportsCustomValues?: boolean; - - /** - * An optional documentation. - */ - documentation?: string; - - /** - * Since when (release number) this enumeration is - * available. Is undefined if not known. - */ - since?: string; - - /** - * All since tags in case there was more than one tag. - * Is undefined if not known. - */ - sinceTags?: string[]; - - /** - * Whether this is a proposed enumeration. If omitted, - * the enumeration is final. - */ - proposed?: boolean; - - /** - * Whether the enumeration is deprecated or not. If deprecated - * the property contains the deprecation message. - */ - deprecated?: string; -}; - -export type MetaData = { - /** - * The protocol version. - */ - version: string; -}; - -/** - * The actual meta model. - */ -export type MetaModel = { - /** - * Additional meta data. - */ - metaData: MetaData; - - /** - * The requests. - */ - requests: Request[]; - - /** - * The notifications. - */ - notifications: Notification[]; - - /** - * The structures. - */ - structures: Structure[]; - - /** - * The enumerations. - */ - enumerations: Enumeration[]; - - /** - * The type aliases. - */ - typeAliases: TypeAlias[]; -}; +/* -------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * ------------------------------------------------------------------------------------------ */ + +export type BaseTypes = 'URI' | 'DocumentUri' | 'integer' | 'uinteger' | 'decimal' | 'RegExp' | 'string' | 'boolean' | 'null'; + +export type TypeKind = 'base' | 'reference' | 'array' | 'map' | 'and' | 'or' | 'tuple' | 'literal' | 'stringLiteral' | 'integerLiteral' | 'booleanLiteral'; + +/** + * Indicates in which direction a message is sent in the protocol. + */ +export type MessageDirection = 'clientToServer' | 'serverToClient' | 'both'; + +/** + * Represents a base type like `string` or `DocumentUri`. + */ +export type BaseType = { + kind: 'base'; + name: BaseTypes; +}; + +/** + * Represents a reference to another type (e.g. `TextDocument`). + * This is either a `Structure`, a `Enumeration` or a `TypeAlias` + * in the same meta model. + */ +export type ReferenceType = { + kind: 'reference'; + name: string; +}; + +/** + * Represents an array type (e.g. `TextDocument[]`). + */ +export type ArrayType = { + kind: 'array'; + element: Type; +}; + +/** + * Represents a type that can be used as a key in a + * map type. If a reference type is used then the + * type must either resolve to a `string` or `integer` + * type. (e.g. `type ChangeAnnotationIdentifier === string`). + */ +export type MapKeyType = { kind: 'base'; name: 'URI' | 'DocumentUri' | 'string' | 'integer' } | ReferenceType; + +/** + * Represents a JSON object map + * (e.g. `interface Map { [key: K] => V; }`). + */ +export type MapType = { + kind: 'map'; + key: MapKeyType; + value: Type; +}; + +/** + * Represents an `and` type + * (e.g. TextDocumentParams & WorkDoneProgressParams`). + */ +export type AndType = { + kind: 'and'; + items: Type[]; +}; + +/** + * Represents an `or` type + * (e.g. `Location | LocationLink`). + */ +export type OrType = { + kind: 'or'; + items: Type[]; +}; + +/** + * Represents a `tuple` type + * (e.g. `[integer, integer]`). + */ +export type TupleType = { + kind: 'tuple'; + items: Type[]; +}; + +/** + * Represents a literal structure + * (e.g. `property: { start: uinteger; end: uinteger; }`). + */ +export type StructureLiteralType = { + kind: 'literal'; + value: StructureLiteral; +}; + +/** + * Represents a string literal type + * (e.g. `kind: 'rename'`). + */ +export type StringLiteralType = { + kind: 'stringLiteral'; + value: string; +}; + +export type IntegerLiteralType = { + /** + * Represents an integer literal type + * (e.g. `kind: 1`). + */ + kind: 'integerLiteral'; + value: number; +}; + +/** + * Represents a boolean literal type + * (e.g. `kind: true`). + */ +export type BooleanLiteralType = { + kind: 'booleanLiteral'; + value: boolean; +}; + +export type Type = BaseType | ReferenceType | ArrayType | MapType | AndType | OrType | TupleType | StructureLiteralType | StringLiteralType | IntegerLiteralType | BooleanLiteralType; + +/** + * Represents a LSP request + */ +export type Request = { + /** + * The request's method name. + */ + method: string; + + /** + * The type name of the request if any. + */ + typeName?: string; + + /** + * The parameter type(s) if any. + */ + params?: Type | Type[]; + + /** + * The result type. + */ + result: Type; + + /** + * Optional partial result type if the request + * supports partial result reporting. + */ + partialResult?: Type; + + /** + * An optional error data type. + */ + errorData?: Type; + + /** + * Optional a dynamic registration method if it + * different from the request's method. + */ + registrationMethod?: string; + + /** + * Optional registration options if the request + * supports dynamic registration. + */ + registrationOptions?: Type; + + /** + * The direction in which this request is sent + * in the protocol. + */ + messageDirection: MessageDirection; + + /** + * An optional documentation; + */ + documentation?: string; + + /** + * Since when (release number) this request is + * available. Is undefined if not known. + */ + since?: string; + + /** + * All since tags in case there was more than one tag. + * Is undefined if not known. + */ + sinceTags?: string[]; + + /** + * Whether this is a proposed feature. If omitted + * the feature is final. + */ + proposed?: boolean; + + /** + * Whether the request is deprecated or not. If deprecated + * the property contains the deprecation message. + */ + deprecated?: string; + + /** + * The client capability property path if any. + */ + clientCapability?: string; + + /** + * The server capability property path if any. + */ + serverCapability?: string; +}; + +/** + * Represents a LSP notification + */ +export type Notification = { + /** + * The notifications's method name. + */ + method: string; + + /** + * The type name of the notifications if any. + */ + typeName?: string; + + /** + * The parameter type(s) if any. + */ + params?: Type | Type[]; + + /** + * Optional a dynamic registration method if it + * different from the notifications's method. + */ + registrationMethod?: string; + + /** + * Optional registration options if the notification + * supports dynamic registration. + */ + registrationOptions?: Type; + + /** + * The direction in which this notification is sent + * in the protocol. + */ + messageDirection: MessageDirection; + + /** + * An optional documentation; + */ + documentation?: string; + + /** + * Since when (release number) this notification is + * available. Is undefined if not known. + */ + since?: string; + + /** + * All since tags in case there was more than one tag. + * Is undefined if not known. + */ + sinceTags?: string[]; + + /** + * Whether this is a proposed notification. If omitted + * the notification is final. + */ + proposed?: boolean; + + /** + * Whether the notification is deprecated or not. If deprecated + * the property contains the deprecation message. + */ + deprecated?: string; + + /** + * The client capability property path if any. + */ + clientCapability?: string; + + /** + * The server capability property path if any. + */ + serverCapability?: string; +}; + +/** + * Represents an object property. + */ +export type Property = { + /** + * The property name; + */ + name: string; + + /** + * The type of the property + */ + type: Type; + + /** + * Whether the property is optional. If + * omitted, the property is mandatory. + */ + optional?: boolean; + + /** + * An optional documentation. + */ + documentation?: string; + + /** + * Since when (release number) this property is + * available. Is undefined if not known. + */ + since?: string; + + /** + * All since tags in case there was more than one tag. + * Is undefined if not known. + */ + sinceTags?: string[]; + + /** + * Whether this is a proposed property. If omitted, + * the structure is final. + */ + proposed?: boolean; + + /** + * Whether the property is deprecated or not. If deprecated + * the property contains the deprecation message. + */ + deprecated?: string; + + /** + * Whether this property uses omitzero without being a pointer. + * Custom extension for special value types. + */ + omitzeroValue?: boolean; +}; + +/** + * Defines the structure of an object literal. + */ +export type Structure = { + /** + * The name of the structure. + */ + name: string; + + /** + * Structures extended from. This structures form + * a polymorphic type hierarchy. + */ + extends?: Type[]; + + /** + * Structures to mix in. The properties of these + * structures are `copied` into this structure. + * Mixins don't form a polymorphic type hierarchy in + * LSP. + */ + mixins?: Type[]; + + /** + * The properties. + */ + properties: Property[]; + + /** + * An optional documentation; + */ + documentation?: string; + + /** + * Since when (release number) this structure is + * available. Is undefined if not known. + */ + since?: string; + + /** + * All since tags in case there was more than one tag. + * Is undefined if not known. + */ + sinceTags?: string[]; + + /** + * Whether this is a proposed structure. If omitted, + * the structure is final. + */ + proposed?: boolean; + + /** + * Whether the structure is deprecated or not. If deprecated + * the property contains the deprecation message. + */ + deprecated?: string; +}; + +/** + * Defines an unnamed structure of an object literal. + */ +export type StructureLiteral = { + + /** + * The properties. + */ + properties: Property[]; + + /** + * An optional documentation. + */ + documentation?: string; + + /** + * Since when (release number) this structure is + * available. Is undefined if not known. + */ + since?: string; + + /** + * All since tags in case there was more than one tag. + * Is undefined if not known. + */ + sinceTags?: string[]; + + /** + * Whether this is a proposed structure. If omitted, + * the structure is final. + */ + proposed?: boolean; + + /** + * Whether the literal is deprecated or not. If deprecated + * the property contains the deprecation message. + */ + deprecated?: string; +}; + +/** + * Defines a type alias. + * (e.g. `type Definition = Location | LocationLink`) + */ +export type TypeAlias = { + /** + * The name of the type alias. + */ + name: string; + + /** + * The aliased type. + */ + type: Type; + + /** + * An optional documentation. + */ + documentation?: string; + + /** + * Since when (release number) this structure is + * available. Is undefined if not known. + */ + since?: string; + + /** + * All since tags in case there was more than one tag. + * Is undefined if not known. + */ + sinceTags?: string[]; + + /** + * Whether this is a proposed type alias. If omitted, + * the type alias is final. + */ + proposed?: boolean; + + /** + * Whether the type alias is deprecated or not. If deprecated + * the property contains the deprecation message. + */ + deprecated?: string; +}; + +/** + * Defines an enumeration entry. + */ +export type EnumerationEntry = { + /** + * The name of the enum item. + */ + name: string; + + /** + * The value. + */ + value: string | number; + + /** + * An optional documentation. + */ + documentation?: string; + + /** + * Since when (release number) this enumeration entry is + * available. Is undefined if not known. + */ + since?: string; + + /** + * All since tags in case there was more than one tag. + * Is undefined if not known. + */ + sinceTags?: string[]; + + /** + * Whether this is a proposed enumeration entry. If omitted, + * the enumeration entry is final. + */ + proposed?: boolean; + + /** + * Whether the enum entry is deprecated or not. If deprecated + * the property contains the deprecation message. + */ + deprecated?: string; +}; + +export type EnumerationType = { kind: 'base'; name: 'string' | 'integer' | 'uinteger' }; + +/** + * Defines an enumeration. + */ +export type Enumeration = { + /** + * The name of the enumeration. + */ + name: string; + + /** + * The type of the elements. + */ + type: EnumerationType; + + /** + * The enum values. + */ + values: EnumerationEntry[]; + + /** + * Whether the enumeration supports custom values (e.g. values which are not + * part of the set defined in `values`). If omitted no custom values are + * supported. + */ + supportsCustomValues?: boolean; + + /** + * An optional documentation. + */ + documentation?: string; + + /** + * Since when (release number) this enumeration is + * available. Is undefined if not known. + */ + since?: string; + + /** + * All since tags in case there was more than one tag. + * Is undefined if not known. + */ + sinceTags?: string[]; + + /** + * Whether this is a proposed enumeration. If omitted, + * the enumeration is final. + */ + proposed?: boolean; + + /** + * Whether the enumeration is deprecated or not. If deprecated + * the property contains the deprecation message. + */ + deprecated?: string; +}; + +export type MetaData = { + /** + * The protocol version. + */ + version: string; +}; + +/** + * The actual meta model. + */ +export type MetaModel = { + /** + * Additional meta data. + */ + metaData: MetaData; + + /** + * The requests. + */ + requests: Request[]; + + /** + * The notifications. + */ + notifications: Notification[]; + + /** + * The structures. + */ + structures: Structure[]; + + /** + * The enumerations. + */ + enumerations: Enumeration[]; + + /** + * The type aliases. + */ + typeAliases: TypeAlias[]; +}; From ea095150fab30190466fd4460513d0618dc5956c Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Thu, 1 Oct 2026 18:01:30 -0700 Subject: [PATCH 06/10] Update formatter exception locations --- .dprint.jsonc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.dprint.jsonc b/.dprint.jsonc index f115b3daa0099..2280c3ecdd702 100644 --- a/.dprint.jsonc +++ b/.dprint.jsonc @@ -48,8 +48,8 @@ "**/testdata", "packages/vscode-typescript/l10n/**", "tsc/internal/bundled/libs/**", - "tsc/internal/lsp/lsproto/_generate/*.json", - "tsc/internal/lsp/lsproto/_generate/metaModelSchema.mts", + "tools/scripts/lsp/*.json", + "tools/scripts/lsp/metaModelSchema.mts", // Needs to be LF to have a working shebang. "packages/typescript/bin/tsc", "tsc/internal/bundled/source/**", From 2b9d5a5d8591112aa5842a59b503eca37125ae35 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Fri, 2 Oct 2026 11:01:53 -0700 Subject: [PATCH 07/10] Fix shameful tsconfig --- packages/vscode-typescript/package.json | 2 +- packages/vscode-typescript/tsconfig.json | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/vscode-typescript/package.json b/packages/vscode-typescript/package.json index d00bfe862addb..62e1e8a7e8678 100644 --- a/packages/vscode-typescript/package.json +++ b/packages/vscode-typescript/package.json @@ -368,7 +368,7 @@ "package.nls.json" ], "scripts": { - "build": "tsc && npm run bundle", + "build": "tsc -b && npm run bundle", "bundle": "esbuild src/extension.ts --bundle --external:vscode --platform=node --format=cjs --outfile=dist/extension.bundle.js --sourcemap", "test": "tsc -p test && esbuild test/index.test.ts --bundle --platform=node --format=cjs --outfile=dist/test/index.test.cjs && node --test dist/test/index.test.cjs", "watch": "npm run bundle -- --watch", diff --git a/packages/vscode-typescript/tsconfig.json b/packages/vscode-typescript/tsconfig.json index e5492794a84c4..af28654b3b2b6 100644 --- a/packages/vscode-typescript/tsconfig.json +++ b/packages/vscode-typescript/tsconfig.json @@ -1,19 +1,19 @@ { "compilerOptions": { - "rootDir": "..", + "rootDir": "src", "outDir": "./dist", "noEmit": true, "target": "es2023", "module": "preserve", "moduleResolution": "bundler", - "paths": { - "@typescript/typescript/unstable/vscode": ["../typescript/src/vscode/extensionApi.ts"] - }, "types": ["node"], "strict": true, "noImplicitOverride": true }, - "include": ["./src/**/*"] + "include": ["./src/**/*"], + "references": [ + { "path": "../typescript" } + ] } From 5afb58d562fa447dd42e1dd92ed6fad53ffcf650 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Fri, 2 Oct 2026 11:11:37 -0700 Subject: [PATCH 08/10] Fix test tsconfig too --- packages/vscode-typescript/package.json | 2 +- packages/vscode-typescript/test/tsconfig.json | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/vscode-typescript/package.json b/packages/vscode-typescript/package.json index 62e1e8a7e8678..81aea93963b0a 100644 --- a/packages/vscode-typescript/package.json +++ b/packages/vscode-typescript/package.json @@ -370,7 +370,7 @@ "scripts": { "build": "tsc -b && npm run bundle", "bundle": "esbuild src/extension.ts --bundle --external:vscode --platform=node --format=cjs --outfile=dist/extension.bundle.js --sourcemap", - "test": "tsc -p test && esbuild test/index.test.ts --bundle --platform=node --format=cjs --outfile=dist/test/index.test.cjs && node --test dist/test/index.test.cjs", + "test": "tsc -b test && esbuild test/index.test.ts --bundle --platform=node --format=cjs --outfile=dist/test/index.test.cjs && node --test dist/test/index.test.cjs", "watch": "npm run bundle -- --watch", "generateLocTest": "node ../../tools/scripts/gen/extensionLocalization.mts pseudo", "generateLocBundle": "node ../../tools/scripts/gen/extensionLocalization.mts bundle", diff --git a/packages/vscode-typescript/test/tsconfig.json b/packages/vscode-typescript/test/tsconfig.json index 3ae2a24af8b9e..7fa447657ecec 100644 --- a/packages/vscode-typescript/test/tsconfig.json +++ b/packages/vscode-typescript/test/tsconfig.json @@ -1,4 +1,10 @@ { "extends": "../tsconfig.json", - "include": ["."] + "compilerOptions": { + "rootDir": "..", + }, + "include": ["."], + "references": [ + { "path": "../../typescript" } + ] } From 057446ba0516794cc32c486ba38b6f5bedcc5848 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Fri, 2 Oct 2026 11:19:49 -0700 Subject: [PATCH 09/10] Fix superfluous parens --- .../src/vscode/protocol.generated.ts | 142 +++++++++--------- tools/scripts/gen/lspTypeScript.test.mts | 29 +++- tools/scripts/lsp/typeScript.mts | 18 ++- 3 files changed, 110 insertions(+), 79 deletions(-) diff --git a/packages/typescript/src/vscode/protocol.generated.ts b/packages/typescript/src/vscode/protocol.generated.ts index db81fca3ac632..cde6b32f85c17 100644 --- a/packages/typescript/src/vscode/protocol.generated.ts +++ b/packages/typescript/src/vscode/protocol.generated.ts @@ -45,7 +45,7 @@ export interface CallHierarchyIncomingCall { * The ranges at which the calls appear. This is relative to the caller * denoted by {@link CallHierarchyIncomingCall.from `this.from`}. */ - fromRanges: (Range)[]; + fromRanges: Range[]; } /** @@ -84,7 +84,7 @@ export interface CallHierarchyItem { /** * Tags for this item. */ - tags?: (SymbolTag)[] | undefined; + tags?: SymbolTag[] | undefined; /** * More detail for this item, e.g. the signature of a function. */ @@ -124,7 +124,7 @@ export interface CallHierarchyOutgoingCall { * passed to {@link CallHierarchyItemProvider.provideCallHierarchyOutgoingCalls `provideCallHierarchyOutgoingCalls`} * and not {@link CallHierarchyOutgoingCall.to `this.to`}. */ - fromRanges: (Range)[]; + fromRanges: Range[]; } /** @@ -213,7 +213,7 @@ export interface CodeAction { /** * The diagnostics that this code action resolves. */ - diagnostics?: (Diagnostic)[] | undefined; + diagnostics?: Diagnostic[] | undefined; /** * Marks this as a preferred action. Preferred actions are used by the `auto fix` command and can be targeted * by keybindings. @@ -264,7 +264,7 @@ export interface CodeAction { * * @since 3.18.0 */ - tags?: (CodeActionTag)[] | undefined; + tags?: CodeActionTag[] | undefined; } /** @@ -279,14 +279,14 @@ export interface CodeActionContext { * that these accurately reflect the error state of the resource. The primary parameter * to compute code actions is the provided range. */ - diagnostics: (Diagnostic)[]; + diagnostics: Diagnostic[]; /** * Requested kind of actions to return. * * Actions not of this kind are filtered out by the client before being shown. So servers * can omit computing them. */ - only?: (CodeActionKind)[] | undefined; + only?: CodeActionKind[] | undefined; /** * The reason why code actions were requested. * @@ -434,7 +434,7 @@ export interface Command { * Arguments that the command handler should be * invoked with. */ - arguments?: (LSPAny)[] | undefined; + arguments?: LSPAny[] | undefined; } /** @@ -483,7 +483,7 @@ export interface CompletionItem { * * @since 3.15.0 */ - tags?: (CompletionItemTag)[] | undefined; + tags?: CompletionItemTag[] | undefined; /** * A human-readable string with additional information * about this item, like type or symbol information. @@ -492,7 +492,7 @@ export interface CompletionItem { /** * A human-readable string that represents a doc-comment. */ - documentation?: (string | MarkupContent) | undefined; + documentation?: string | MarkupContent | undefined; /** * Indicates if this item is deprecated. * @deprecated Use `tags` instead. @@ -571,7 +571,7 @@ export interface CompletionItem { * * @since 3.16.0 additional type `InsertReplaceEdit` */ - textEdit?: (TextEdit | InsertReplaceEdit) | undefined; + textEdit?: TextEdit | InsertReplaceEdit | undefined; /** * The edit text used if the completion item is part of a CompletionList and * CompletionList defines an item default for the text edit range. @@ -594,13 +594,13 @@ export interface CompletionItem { * (for example adding an import statement at the top of the file if the completion item will * insert an unqualified type). */ - additionalTextEdits?: (TextEdit)[] | undefined; + additionalTextEdits?: TextEdit[] | undefined; /** * An optional set of characters that when pressed while this completion is active will accept it first and * then type that character. *Note* that all commit characters should have `length=1` and that superfluous * characters will be ignored. */ - commitCharacters?: (string)[] | undefined; + commitCharacters?: string[] | undefined; /** * An optional {@link Command command} that is executed *after* inserting this completion. *Note* that * additional modifications to the current document should be described with the @@ -702,13 +702,13 @@ export interface CompletionItemDefaults { * * @since 3.17.0 */ - commitCharacters?: (string)[] | undefined; + commitCharacters?: string[] | undefined; /** * A default edit range. * * @since 3.17.0 */ - editRange?: (Range | EditRangeWithInsertReplace) | undefined; + editRange?: Range | EditRangeWithInsertReplace | undefined; /** * A default insert text format. * @@ -813,7 +813,7 @@ export interface CompletionList { /** * The completion items. */ - items: (CompletionItem)[]; + items: CompletionItem[]; } /** @@ -895,7 +895,7 @@ export interface CreateFileOptions { * Servers should prefer returning `DefinitionLink` over `Definition` if supported * by the client. */ -export type Definition = Location | (Location)[]; +export type Definition = Location | Location[]; /** * Information about where a symbol is defined. @@ -984,7 +984,7 @@ export interface Diagnostic { /** * The diagnostic's code, which usually appear in the user interface. */ - code?: (number | string) | undefined; + code?: number | string | undefined; /** * An optional property to describe the error code. * Requires the code field (above) to be present/not null. @@ -1010,12 +1010,12 @@ export interface Diagnostic { * * @since 3.15.0 */ - tags?: (DiagnosticTag)[] | undefined; + tags?: DiagnosticTag[] | undefined; /** * An array of related diagnostic information, e.g. when symbol-names within * a scope collide all definitions can be marked via this property. */ - relatedInformation?: (DiagnosticRelatedInformation)[] | undefined; + relatedInformation?: DiagnosticRelatedInformation[] | undefined; /** * A data entry field that is preserved between a `textDocument/publishDiagnostics` * notification and `textDocument/codeAction` request. @@ -1229,7 +1229,7 @@ export interface DocumentSymbol { * * @since 3.16.0 */ - tags?: (SymbolTag)[] | undefined; + tags?: SymbolTag[] | undefined; /** * Indicates if this symbol is deprecated. * @@ -1250,7 +1250,7 @@ export interface DocumentSymbol { /** * Children of this symbol, e.g. properties of a class. */ - children?: (DocumentSymbol)[] | undefined; + children?: DocumentSymbol[] | undefined; } /** @@ -1399,7 +1399,7 @@ export interface FullDocumentDiagnosticReport { /** * The actual items. */ - items: (Diagnostic)[]; + items: Diagnostic[]; } /** @@ -1409,7 +1409,7 @@ export interface Hover { /** * The hover's content */ - contents: MarkupContent | MarkedString | (MarkedString)[]; + contents: MarkupContent | MarkedString | MarkedString[]; /** * An optional range inside the text document that is used to * visualize the hover, e.g. by changing the background color. @@ -1486,7 +1486,7 @@ export interface InlayHint { * * *Note* that neither the string nor the label part can be empty. */ - label: string | (InlayHintLabelPart)[]; + label: string | InlayHintLabelPart[]; /** * The kind of this hint. Can be omitted in which case the client * should fall back to a reasonable default. @@ -1499,11 +1499,11 @@ export interface InlayHint { * hint (or its nearest variant) is now part of the document and the inlay * hint itself is now obsolete. */ - textEdits?: (TextEdit)[] | undefined; + textEdits?: TextEdit[] | undefined; /** * The tooltip text when you hover over this item. */ - tooltip?: (string | MarkupContent) | undefined; + tooltip?: string | MarkupContent | undefined; /** * Render padding before the hint. * @@ -1550,7 +1550,7 @@ export interface InlayHintLabelPart { * the client capability `inlayHint.resolveSupport` clients might resolve * this property late using the resolve request. */ - tooltip?: (string | MarkupContent) | undefined; + tooltip?: string | MarkupContent | undefined; /** * An optional source code location that represents this * label part. @@ -1642,7 +1642,7 @@ export type LSPAny = LSPObject | LSPArray | string | number | boolean | null; * LSP arrays. * @since 3.17.0 */ -export type LSPArray = (LSPAny)[]; +export type LSPArray = LSPAny[]; /** * LSP object definition. @@ -1675,7 +1675,7 @@ export interface LinkedEditingRanges { * A list of ranges that can be edited together. The ranges must have * identical length and contain identical text content. The ranges cannot overlap. */ - ranges: (Range)[]; + ranges: Range[]; /** * An optional word pattern (regular expression) that describes valid contents for * the given ranges. If no pattern is provided, the client configuration's word @@ -1811,7 +1811,7 @@ export interface MultiDocumentHighlight { /** * The highlights for the document. */ - highlights: (DocumentHighlight)[]; + highlights: DocumentHighlight[]; } /** @@ -1829,7 +1829,7 @@ export interface MultiDocumentHighlightParams { /** * The list of file URIs to search for highlights across. */ - filesToSearch: (DocumentUri)[]; + filesToSearch: DocumentUri[]; } /** @@ -1874,7 +1874,7 @@ export interface ParameterInformation { * The human-readable doc-comment of this parameter. Will be shown * in the UI but can be omitted. */ - documentation?: (string | MarkupContent) | undefined; + documentation?: string | MarkupContent | undefined; } /** @@ -2032,7 +2032,7 @@ export interface RelatedFullDocumentDiagnosticReport { /** * The actual items. */ - items: (Diagnostic)[]; + items: Diagnostic[]; /** * Diagnostics of related documents. This information is useful * in programming languages where code in a file A can generate @@ -2176,7 +2176,7 @@ export interface SelectionRangeParams { /** * The positions inside the text document. */ - positions: (Position)[]; + positions: Position[]; } /** @@ -2193,7 +2193,7 @@ export interface SemanticTokens { /** * The actual tokens. */ - data: (number)[]; + data: number[]; } /** @@ -2247,7 +2247,7 @@ export interface SignatureHelp { /** * One or more signatures. */ - signatures: (SignatureInformation)[]; + signatures: SignatureInformation[]; /** * The active signature. If omitted or the value lies outside the * range of `signatures` the value defaults to zero or is ignored if @@ -2281,7 +2281,7 @@ export interface SignatureHelp { * Since version 3.16.0 the `SignatureInformation` itself provides a * `activeParameter` property and it should be used instead of this one. */ - activeParameter?: (number | null) | undefined; + activeParameter?: number | null | undefined; } /** @@ -2363,11 +2363,11 @@ export interface SignatureInformation { * The human-readable doc-comment of this signature. Will be shown * in the UI but can be omitted. */ - documentation?: (string | MarkupContent) | undefined; + documentation?: string | MarkupContent | undefined; /** * The parameters of this signature. */ - parameters?: (ParameterInformation)[] | undefined; + parameters?: ParameterInformation[] | undefined; /** * The index of the active parameter. * @@ -2381,7 +2381,7 @@ export interface SignatureInformation { * * @since 3.16.0 */ - activeParameter?: (number | null) | undefined; + activeParameter?: number | null | undefined; /** * A colorized label for the signature, providing classified text runs for VS syntax coloring. */ @@ -2448,7 +2448,7 @@ export interface SymbolInformation { * * @since 3.16.0 */ - tags?: (SymbolTag)[] | undefined; + tags?: SymbolTag[] | undefined; /** * The name of the symbol containing this symbol. This information is for * user interface purposes (e.g. to render a qualifier in the user interface @@ -2508,7 +2508,7 @@ export interface TextDocumentEdit { * @since 3.18.0 - support for SnippetTextEdit. This is guarded using a * client capability. */ - edits: ((TextEdit | AnnotatedTextEdit | SnippetTextEdit))[]; + edits: (TextEdit | AnnotatedTextEdit | SnippetTextEdit)[]; } /** @@ -2602,7 +2602,7 @@ export interface VSClassifiedTextElement { /** * The classified text runs that make up this element. */ - Runs: (VSClassifiedTextRun)[]; + Runs: VSClassifiedTextRun[]; /** * VS type discriminator required by ObjectContentConverter for deserialization. */ @@ -2646,7 +2646,7 @@ export interface VSContainerElement { /** * The child elements contained within this container. */ - Elements: ((VSImageElement | VSClassifiedTextElement | VSContainerElement))[]; + Elements: (VSImageElement | VSClassifiedTextElement | VSContainerElement)[]; /** * VS type discriminator required by ObjectContentConverter for deserialization. */ @@ -2737,7 +2737,7 @@ export interface VSReferenceItem { /** * The kind(s) of this reference (read, write, etc.). */ - _vs_kind?: (VSReferenceKind)[] | undefined; + _vs_kind?: VSReferenceKind[] | undefined; /** * The location of this reference. */ @@ -2776,7 +2776,7 @@ export interface WorkspaceEdit { /** * Holds changes to existing resources. */ - changes?: { [key: DocumentUri]: (TextEdit)[]; } | undefined; + changes?: { [key: DocumentUri]: TextEdit[]; } | undefined; /** * Depending on the client capability `workspace.workspaceEdit.resourceOperations` document changes * are either an array of `TextDocumentEdit`s to express changes to n different text documents @@ -2789,7 +2789,7 @@ export interface WorkspaceEdit { * If a client neither supports `documentChanges` nor `workspace.workspaceEdit.resourceOperations` then * only plain `TextEdit`s using the `changes` property are supported. */ - documentChanges?: ((TextDocumentEdit | CreateFile | RenameFile | DeleteFile))[] | undefined; + documentChanges?: (TextDocumentEdit | CreateFile | RenameFile | DeleteFile)[] | undefined; /** * A map of change annotations that can be referenced in `AnnotatedTextEdit`s or create, rename and * delete file / folder operations. @@ -2822,7 +2822,7 @@ export interface WorkspaceSymbol { * * @since 3.16.0 */ - tags?: (SymbolTag)[] | undefined; + tags?: SymbolTag[] | undefined; /** * The name of the symbol containing this symbol. This information is for * user interface purposes (e.g. to render a qualifier in the user interface @@ -2878,36 +2878,36 @@ export interface WorkspaceSymbolParams { /** Language-feature requests supported by the extension middleware API. */ export interface LspMiddlewareRequests { "textDocument/hover": { params: HoverParams; result: Hover | null; }; - "textDocument/completion": { params: CompletionParams; result: (CompletionItem)[] | CompletionList | null; }; + "textDocument/completion": { params: CompletionParams; result: CompletionItem[] | CompletionList | null; }; "completionItem/resolve": { params: CompletionItem; result: CompletionItem; }; "textDocument/signatureHelp": { params: SignatureHelpParams; result: SignatureHelp | null; }; - "textDocument/definition": { params: DefinitionParams; result: Definition | (DefinitionLink)[] | null; }; - "textDocument/typeDefinition": { params: TypeDefinitionParams; result: Definition | (DefinitionLink)[] | null; }; - "textDocument/implementation": { params: ImplementationParams; result: Definition | (DefinitionLink)[] | null; }; - "textDocument/references": { params: ReferenceParams; result: (Location)[] | null; }; - "textDocument/documentHighlight": { params: DocumentHighlightParams; result: (DocumentHighlight)[] | null; }; - "textDocument/documentSymbol": { params: DocumentSymbolParams; result: (SymbolInformation)[] | (DocumentSymbol)[] | null; }; - "workspace/symbol": { params: WorkspaceSymbolParams; result: (SymbolInformation)[] | (WorkspaceSymbol)[] | null; }; + "textDocument/definition": { params: DefinitionParams; result: Definition | DefinitionLink[] | null; }; + "textDocument/typeDefinition": { params: TypeDefinitionParams; result: Definition | DefinitionLink[] | null; }; + "textDocument/implementation": { params: ImplementationParams; result: Definition | DefinitionLink[] | null; }; + "textDocument/references": { params: ReferenceParams; result: Location[] | null; }; + "textDocument/documentHighlight": { params: DocumentHighlightParams; result: DocumentHighlight[] | null; }; + "textDocument/documentSymbol": { params: DocumentSymbolParams; result: SymbolInformation[] | DocumentSymbol[] | null; }; + "workspace/symbol": { params: WorkspaceSymbolParams; result: SymbolInformation[] | WorkspaceSymbol[] | null; }; "textDocument/rename": { params: RenameParams; result: WorkspaceEdit | null; }; "textDocument/prepareRename": { params: PrepareRenameParams; result: PrepareRenameResult | null; }; - "textDocument/formatting": { params: DocumentFormattingParams; result: (TextEdit)[] | null; }; - "textDocument/rangeFormatting": { params: DocumentRangeFormattingParams; result: (TextEdit)[] | null; }; - "textDocument/onTypeFormatting": { params: DocumentOnTypeFormattingParams; result: (TextEdit)[] | null; }; - "textDocument/selectionRange": { params: SelectionRangeParams; result: (SelectionRange)[] | null; }; - "textDocument/foldingRange": { params: FoldingRangeParams; result: (FoldingRange)[] | null; }; - "textDocument/inlayHint": { params: InlayHintParams; result: (InlayHint)[] | null; }; - "textDocument/codeAction": { params: CodeActionParams; result: ((Command | CodeAction))[] | null; }; - "textDocument/codeLens": { params: CodeLensParams; result: (CodeLens)[] | null; }; + "textDocument/formatting": { params: DocumentFormattingParams; result: TextEdit[] | null; }; + "textDocument/rangeFormatting": { params: DocumentRangeFormattingParams; result: TextEdit[] | null; }; + "textDocument/onTypeFormatting": { params: DocumentOnTypeFormattingParams; result: TextEdit[] | null; }; + "textDocument/selectionRange": { params: SelectionRangeParams; result: SelectionRange[] | null; }; + "textDocument/foldingRange": { params: FoldingRangeParams; result: FoldingRange[] | null; }; + "textDocument/inlayHint": { params: InlayHintParams; result: InlayHint[] | null; }; + "textDocument/codeAction": { params: CodeActionParams; result: (Command | CodeAction)[] | null; }; + "textDocument/codeLens": { params: CodeLensParams; result: CodeLens[] | null; }; "codeLens/resolve": { params: CodeLens; result: CodeLens; }; - "textDocument/prepareCallHierarchy": { params: CallHierarchyPrepareParams; result: (CallHierarchyItem)[] | null; }; - "callHierarchy/incomingCalls": { params: CallHierarchyIncomingCallsParams; result: (CallHierarchyIncomingCall)[] | null; }; - "callHierarchy/outgoingCalls": { params: CallHierarchyOutgoingCallsParams; result: (CallHierarchyOutgoingCall)[] | null; }; + "textDocument/prepareCallHierarchy": { params: CallHierarchyPrepareParams; result: CallHierarchyItem[] | null; }; + "callHierarchy/incomingCalls": { params: CallHierarchyIncomingCallsParams; result: CallHierarchyIncomingCall[] | null; }; + "callHierarchy/outgoingCalls": { params: CallHierarchyOutgoingCallsParams; result: CallHierarchyOutgoingCall[] | null; }; "textDocument/linkedEditingRange": { params: LinkedEditingRangeParams; result: LinkedEditingRanges | null; }; "textDocument/semanticTokens/full": { params: SemanticTokensParams; result: SemanticTokens | null; }; "textDocument/semanticTokens/range": { params: SemanticTokensRangeParams; result: SemanticTokens | null; }; "textDocument/diagnostic": { params: DocumentDiagnosticParams; result: DocumentDiagnosticReport; }; - "custom/textDocument/sourceDefinition": { params: TextDocumentPositionParams; result: Definition | (DefinitionLink)[] | null; }; - "custom/textDocument/multiDocumentHighlight": { params: MultiDocumentHighlightParams; result: (MultiDocumentHighlight)[] | null; }; + "custom/textDocument/sourceDefinition": { params: TextDocumentPositionParams; result: Definition | DefinitionLink[] | null; }; + "custom/textDocument/multiDocumentHighlight": { params: MultiDocumentHighlightParams; result: MultiDocumentHighlight[] | null; }; "textDocument/_vs_onAutoInsert": { params: VSOnAutoInsertParams; result: VSOnAutoInsertResponseItem | null; }; - "textDocument/_vs_references": { params: ReferenceParams; result: (VSReferenceItem)[] | null; }; + "textDocument/_vs_references": { params: ReferenceParams; result: VSReferenceItem[] | null; }; } diff --git a/tools/scripts/gen/lspTypeScript.test.mts b/tools/scripts/gen/lspTypeScript.test.mts index 979dfa480f584..a97034b3d4c72 100644 --- a/tools/scripts/gen/lspTypeScript.test.mts +++ b/tools/scripts/gen/lspTypeScript.test.mts @@ -6,7 +6,7 @@ import { test } from "node:test"; import ts from "typescript"; import { getTypeScriptModel } from "../lsp/generate.mts"; import { getMiddlewareMethods } from "../lsp/generateTypeScript.mts"; -import type { MetaModel } from "../lsp/metaModelSchema.mts"; +import type { MetaModel, Type } from "../lsp/metaModelSchema.mts"; import { generateTypeScript } from "../lsp/typeScript.mts"; function fixture(): MetaModel { @@ -66,12 +66,37 @@ test("LSP TypeScript generation traverses reachable types and preserves wire sha assert.match(output, /export type DocumentUri = string/); assert.match(output, /export type Kind = "first" \| "second"/); assert.match(output, /export type OpenKind = 1 \| number/); - assert.match(output, /export type Result = \(\(Child\)\[\] \| null\)/); + assert.match(output, /export type Result = Child\[\] \| null;/); assert.doesNotMatch(output, /UnusedLifecycle|export interface Parent|export interface Mixin/); const file = ts.createSourceFile("generated.ts", output, ts.ScriptTarget.Latest, true); assert.ok(file.statements.every(statement => ts.isInterfaceDeclaration(statement) || ts.isTypeAliasDeclaration(statement))); }); +test("LSP TypeScript generation parenthesizes only lower-precedence types", () => { + const model = fixture(); + const a: Type = { kind: "reference", name: "Child" }; + const b: Type = { kind: "reference", name: "Parent" }; + const c: Type = { kind: "reference", name: "Mixin" }; + const union: Type = { kind: "or", items: [a, b] }; + const intersection: Type = { kind: "and", items: [a, b] }; + const cases: [Type, string][] = [ + [{ kind: "array", element: a }, "Child[]"], + [{ kind: "array", element: { kind: "array", element: a } }, "Child[][]"], + [union, "Child | Parent"], + [intersection, "Child & Parent"], + [{ kind: "array", element: union }, "(Child | Parent)[]"], + [{ kind: "array", element: intersection }, "(Child & Parent)[]"], + [{ kind: "and", items: [union, c] }, "(Child | Parent) & Mixin"], + [{ kind: "or", items: [intersection, c] }, "Child & Parent | Mixin"], + [{ kind: "tuple", items: [union, intersection] }, "[Child | Parent, Child & Parent]"], + ]; + for (const [result, expected] of cases) { + model.requests[0].result = result; + const output = generateTypeScript(model, ["test/feature"]); + assert.ok(output.includes(`result: ${expected};`), expected); + } +}); + test("LSP TypeScript generation reports invalid schema and method inputs", () => { assert.throws(() => generateTypeScript(fixture(), ["missing/feature"]), /Not a client-to-server request/); assert.throws(() => generateTypeScript(fixture(), ["test/feature", "test/feature"]), /Duplicate/); diff --git a/tools/scripts/lsp/typeScript.mts b/tools/scripts/lsp/typeScript.mts index 2ba53e86a2548..a67cd9c2da9a3 100644 --- a/tools/scripts/lsp/typeScript.mts +++ b/tools/scripts/lsp/typeScript.mts @@ -67,7 +67,13 @@ export function generateTypeScript(model: MetaModel, methods: readonly string[]) return name; } - function type(value: Type): string { + function type(value: Type, minimumPrecedence = 0): string { + const precedence = value.kind === "or" ? 1 : value.kind === "and" ? 2 : value.kind === "array" ? 3 : 4; + const text = typeText(value); + return precedence < minimumPrecedence ? `(${text})` : text; + } + + function typeText(value: Type): string { switch (value.kind) { case "base": if (value.name === "DocumentUri" || value.name === "URI") { @@ -78,15 +84,15 @@ export function generateTypeScript(model: MetaModel, methods: readonly string[]) case "reference": return reference(value.name); case "array": - return `(${type(value.element)})[]`; + return `${type(value.element, 3)}[]`; case "map": return `{ [key: ${type(value.key)}]: ${type(value.value)}; }`; case "and": - return `(${value.items.map(type).join(" & ")})`; + return value.items.map(item => type(item, 2)).join(" & "); case "or": - return `(${[...new Set(value.items.map(type))].join(" | ")})`; + return [...new Set(value.items.map(item => type(item, 1)))].join(" | "); case "tuple": - return `[${value.items.map(type).join(", ")}]`; + return `[${value.items.map(item => type(item)).join(", ")}]`; case "literal": return `{\n${properties(value.value.properties)}\n}`; case "stringLiteral": @@ -107,7 +113,7 @@ export function generateTypeScript(model: MetaModel, methods: readonly string[]) selected.add(method); const request = requests.get(method); if (!request || request.messageDirection !== "clientToServer") throw new Error(`Not a client-to-server request: ${method}`); - const params = !request.params ? "undefined" : Array.isArray(request.params) ? `[${request.params.map(type).join(", ")}]` : type(request.params); + const params = !request.params ? "undefined" : Array.isArray(request.params) ? `[${request.params.map(item => type(item)).join(", ")}]` : type(request.params); return `${JSON.stringify(method)}: { params: ${params}; result: ${type(request.result)}; };`; }); return [ From d7c0d83ca286b2f10f94d72d8910411015b9ee41 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Fri, 2 Oct 2026 11:29:21 -0700 Subject: [PATCH 10/10] Format --- packages/vscode-typescript/test/tsconfig.json | 2 +- tools/scripts/gen/lspTypeScript.test.mts | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/vscode-typescript/test/tsconfig.json b/packages/vscode-typescript/test/tsconfig.json index 7fa447657ecec..8ffd842b3e7f8 100644 --- a/packages/vscode-typescript/test/tsconfig.json +++ b/packages/vscode-typescript/test/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "../tsconfig.json", "compilerOptions": { - "rootDir": "..", + "rootDir": ".." }, "include": ["."], "references": [ diff --git a/tools/scripts/gen/lspTypeScript.test.mts b/tools/scripts/gen/lspTypeScript.test.mts index a97034b3d4c72..888120494c75a 100644 --- a/tools/scripts/gen/lspTypeScript.test.mts +++ b/tools/scripts/gen/lspTypeScript.test.mts @@ -6,7 +6,10 @@ import { test } from "node:test"; import ts from "typescript"; import { getTypeScriptModel } from "../lsp/generate.mts"; import { getMiddlewareMethods } from "../lsp/generateTypeScript.mts"; -import type { MetaModel, Type } from "../lsp/metaModelSchema.mts"; +import type { + MetaModel, + Type, +} from "../lsp/metaModelSchema.mts"; import { generateTypeScript } from "../lsp/typeScript.mts"; function fixture(): MetaModel {