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/**", diff --git a/Herebyfile.mjs b/Herebyfile.mjs index 4ab41dc8cc21b..d84265355b9a2 100644 --- a/Herebyfile.mjs +++ b/Herebyfile.mjs @@ -613,32 +613,47 @@ 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)) { + if (!output.isCurrent(!!options.force)) { + output.invalidate(); + const { default: generate } = await import("./tools/scripts/lsp/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("./tools/scripts/lsp/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, }); @@ -1315,7 +1330,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/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 dac74ea78b24d..db037a91932b6 100644 --- a/packages/typescript/package.json +++ b/packages/typescript/package.json @@ -41,6 +41,9 @@ "./schemas/tsconfig.schema.json": "./schemas/tsconfig.schema.json", "./schemas/jsconfig.schema.json": "./schemas/jsconfig.schema.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..cde6b32f85c17 --- /dev/null +++ b/packages/typescript/src/vscode/protocol.generated.ts @@ -0,0 +1,2913 @@ +// Code generated by tools/scripts/lsp/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..81aea93963b0a 100644 --- a/packages/vscode-typescript/package.json +++ b/packages/vscode-typescript/package.json @@ -368,9 +368,9 @@ "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", + "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", @@ -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/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..c8529d11ee8b2 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 "@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 7bff6514e4397..b2f679ee2ddca 100644 --- a/packages/vscode-typescript/src/extension.ts +++ b/packages/vscode-typescript/src/extension.ts @@ -1,10 +1,11 @@ +import type { ExtensionAPI } from "@typescript/typescript/unstable/vscode"; import * as vscode from "vscode"; +export type { ExtensionAPI } from "@typescript/typescript/unstable/vscode"; 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..999b38534f4fa --- /dev/null +++ b/packages/vscode-typescript/src/lspMiddleware.ts @@ -0,0 +1,149 @@ +import type { + LspMiddlewareContext, + LspMiddlewareMethod, + LspMiddlewareResult, + LspMiddlewareTransformer, +} from "@typescript/typescript/unstable/vscode"; +import type { + CancellationToken, + Disposable, + MessageSignature, +} from "vscode-languageserver-protocol"; + +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; +} + +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); + for (const registration of registrations) { + if (token?.isCancellationRequested) return original; + result = await registration.invoke(result, structuredClone(context)); + } + 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..e9e8f78541bf9 100644 --- a/packages/vscode-typescript/src/session.ts +++ b/packages/vscode-typescript/src/session.ts @@ -1,3 +1,7 @@ +import type { + LspMiddlewareMethod, + LspMiddlewareTransformer, +} from "@typescript/typescript/unstable/vscode"; import * as path from "path"; import * as vscode from "vscode"; import { ActiveJsTsEditorTracker } from "./activeJsTsEditorTracker"; @@ -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..40d7ccc4e685d --- /dev/null +++ b/packages/vscode-typescript/test/lspMiddleware.test.ts @@ -0,0 +1,413 @@ +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"; +import { + CancellationToken, + CancellationTokenSource, + createProtocolConnection, + type Diagnostic, + HoverRequest, + PublishDiagnosticsNotification, + type PublishDiagnosticsParams, +} from "vscode-languageserver-protocol/node"; +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.equal(Object.isFrozen(context.params.textDocument), false); + 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("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.deepEqual(await registry.sendRequest("textDocument/hover", params, undefined, async () => original), { contents: "first" }); + assert.equal(params.textDocument.uri, "file:///test.ts"); + assert.equal(params.position.line, 0); + assert.deepEqual(original, { contents: "server" }); + }); + + 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 independent 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.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) => { + 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..8ffd842b3e7f8 100644 --- a/packages/vscode-typescript/test/tsconfig.json +++ b/packages/vscode-typescript/test/tsconfig.json @@ -2,5 +2,9 @@ "extends": "../tsconfig.json", "compilerOptions": { "rootDir": ".." - } + }, + "include": ["."], + "references": [ + { "path": "../../typescript" } + ] } diff --git a/packages/vscode-typescript/tsconfig.json b/packages/vscode-typescript/tsconfig.json index 1dbb37fcc5554..af28654b3b2b6 100644 --- a/packages/vscode-typescript/tsconfig.json +++ b/packages/vscode-typescript/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "rootDir": "./src", + "rootDir": "src", "outDir": "./dist", "noEmit": true, @@ -12,5 +12,8 @@ "strict": true, "noImplicitOverride": true }, - "include": ["./src/**/*"] + "include": ["./src/**/*"], + "references": [ + { "path": "../typescript" } + ] } diff --git a/tools/scripts/gen/generatedFile.test.mts b/tools/scripts/gen/generatedFile.test.mts index d7ef8ce6cb88f..ce1291442d2cd 100644 --- a/tools/scripts/gen/generatedFile.test.mts +++ b/tools/scripts/gen/generatedFile.test.mts @@ -436,12 +436,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..888120494c75a --- /dev/null +++ b/tools/scripts/gen/lspTypeScript.test.mts @@ -0,0 +1,153 @@ +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 "../lsp/generate.mts"; +import { getMiddlewareMethods } from "../lsp/generateTypeScript.mts"; +import type { + MetaModel, + Type, +} from "../lsp/metaModelSchema.mts"; +import { generateTypeScript } from "../lsp/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 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/); + 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/.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 96% rename from tsc/internal/lsp/lsproto/_generate/generate.mts rename to tools/scripts/lsp/generate.mts index a08413c59965f..1f160c3403f6d --- a/tsc/internal/lsp/lsproto/_generate/generate.mts +++ b/tools/scripts/lsp/generate.mts @@ -21,17 +21,23 @@ 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); } -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/tools/scripts/lsp/generateTypeScript.mts b/tools/scripts/lsp/generateTypeScript.mts new file mode 100644 index 0000000000000..9edae682035ea --- /dev/null +++ b/tools/scripts/lsp/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/metaModelSchema.mts b/tools/scripts/lsp/metaModelSchema.mts similarity index 100% rename from tsc/internal/lsp/lsproto/_generate/metaModelSchema.mts rename to tools/scripts/lsp/metaModelSchema.mts 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/tools/scripts/lsp/typeScript.mts b/tools/scripts/lsp/typeScript.mts new file mode 100644 index 0000000000000..a67cd9c2da9a3 --- /dev/null +++ b/tools/scripts/lsp/typeScript.mts @@ -0,0 +1,128 @@ +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, 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") { + 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, 3)}[]`; + case "map": + return `{ [key: ${type(value.key)}]: ${type(value.value)}; }`; + case "and": + return value.items.map(item => type(item, 2)).join(" & "); + case "or": + return [...new Set(value.items.map(item => type(item, 1)))].join(" | "); + case "tuple": + return `[${value.items.map(item => type(item)).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(item => type(item)).join(", ")}]` : type(request.params); + return `${JSON.stringify(method)}: { params: ${params}; result: ${type(request.result)}; };`; + }); + return [ + "// 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"), + "/** Language-feature requests supported by the extension middleware API. */", + `export interface LspMiddlewareRequests {\n${entries.join("\n")}\n}`, + "", + ].join("\n"); +}