From 8afe935c115cc0a209762ada7cfb6c3add7e0a02 Mon Sep 17 00:00:00 2001 From: Guillaume Brunerie Date: Sun, 31 Aug 2025 00:51:00 +0200 Subject: [PATCH 1/7] feat: add support for "Move to file" code action --- docs/configuration.md | 1 + src/lsp-server.ts | 9 +++++++-- src/refactor.ts | 12 +++++++++--- src/ts-protocol.ts | 2 ++ 4 files changed, 19 insertions(+), 5 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index bae97804..474e0700 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -17,6 +17,7 @@ The language server accepts various settings through the `initializationOptions` | hostInfo | string | Information about the host, for example `"Emacs 24.4"` or `"Sublime Text v3075"`. **Default**: `undefined` | | completionDisableFilterText | boolean | Don't set `filterText` property on completion items. **Default**: `false` | | disableAutomaticTypingAcquisition | boolean | Disables tsserver from automatically fetching missing type definitions (`@types` packages) for external modules. | +| supportsMoveToFileCodeAction | boolean | Whether the client supports the "Move to file" interactive code action. | | maxTsServerMemory | number | The maximum size of the V8's old memory section in megabytes (for example `4096` means 4GB). The default value is dynamically configured by Node so can differ per system. Increase for very big projects that exceed allowed memory usage. **Default**: `undefined` | | npmLocation | string | Specifies the path to the NPM executable used for Automatic Type Acquisition. | | locale | string | The locale to use to show error messages. | diff --git a/src/lsp-server.ts b/src/lsp-server.ts index 11047e56..43888066 100644 --- a/src/lsp-server.ts +++ b/src/lsp-server.ts @@ -110,6 +110,7 @@ export class LspServer { // Setup supported features. this.features.completionDisableFilterText = userInitializationOptions.completionDisableFilterText ?? false; + this.features.moveToFileCodeActionSupport = userInitializationOptions.supportsMoveToFileCodeAction ?? false; const { textDocument } = clientCapabilities; if (textDocument) { const { codeAction, completion, definition, publishDiagnostics } = textDocument; @@ -782,7 +783,7 @@ export class LspServer { actions.push(...provideQuickFix(await this.getCodeFixes(fileRangeArgs, params.context, token), this.tsClient)); } if (!kinds || kinds.some(kind => kind.contains(CodeActionKind.Refactor))) { - actions.push(...provideRefactors(await this.getRefactors(fileRangeArgs, params.context, token), fileRangeArgs, this.features)); + actions.push(...provideRefactors(await this.getRefactors(fileRangeArgs, params.context, token, this.features), fileRangeArgs, this.features)); } for (const kind of kinds || []) { @@ -836,11 +837,12 @@ export class LspServer { const response = await this.tsClient.execute(CommandTypes.GetCodeFixes, args, token); return response.type === 'response' ? response : undefined; } - protected async getRefactors(fileRangeArgs: ts.server.protocol.FileRangeRequestArgs, context: lsp.CodeActionContext, token?: lsp.CancellationToken): Promise { + protected async getRefactors(fileRangeArgs: ts.server.protocol.FileRangeRequestArgs, context: lsp.CodeActionContext, token?: lsp.CancellationToken, features?: SupportedFeatures): Promise { const args: ts.server.protocol.GetApplicableRefactorsRequestArgs = { ...fileRangeArgs, triggerReason: context.triggerKind === lsp.CodeActionTriggerKind.Invoked ? 'invoked' : undefined, kind: context.only?.length === 1 ? context.only[0] : undefined, + includeInteractiveActions: features?.moveToFileCodeActionSupport, }; const response = await this.tsClient.execute(CommandTypes.GetApplicableRefactors, args, token); return response.type === 'response' ? response : undefined; @@ -867,6 +869,9 @@ export class LspServer { return; } const { body } = response; + if (body?.notApplicableReason) { + throw new Error(body.notApplicableReason); + } if (!body?.edits.length) { return; } diff --git a/src/refactor.ts b/src/refactor.ts index fdbedfa4..9c0aaaff 100644 --- a/src/refactor.ts +++ b/src/refactor.ts @@ -18,9 +18,15 @@ export function provideRefactors(response: ts.server.protocol.GetApplicableRefac if (info.inlineable === false) { actions.push(asSelectRefactoring(info, args)); } else { - const relevantActions = features.codeActionDisabledSupport - ? info.actions - : info.actions.filter(action => !action.notApplicableReason); + const relevantActions = info.actions.filter(action => { + if (action.notApplicableReason && !features.codeActionDisabledSupport) { + return false; + } + if (action.isInteractive && (!features.moveToFileCodeActionSupport || action.name !== 'Move to file')) { + return false; + } + return true; + }); for (const action of relevantActions) { actions.push(asApplyRefactoring(action, info, args)); } diff --git a/src/ts-protocol.ts b/src/ts-protocol.ts index 696b03b3..6e411958 100644 --- a/src/ts-protocol.ts +++ b/src/ts-protocol.ts @@ -352,6 +352,7 @@ export interface SupportedFeatures { definitionLinkSupport?: boolean; diagnosticsSupport?: boolean; diagnosticsTagSupport?: boolean; + moveToFileCodeActionSupport?: boolean; } export interface TypeScriptPlugin { @@ -363,6 +364,7 @@ export interface TypeScriptPlugin { export interface TypeScriptInitializationOptions { completionDisableFilterText?: boolean; disableAutomaticTypingAcquisition?: boolean; + supportsMoveToFileCodeAction?: boolean; hostInfo?: string; locale?: string; maxTsServerMemory?: number; From fcbd7bce27c2a000b44ad6b554c57c2f499a2e15 Mon Sep 17 00:00:00 2001 From: Guillaume Brunerie Date: Sat, 13 Sep 2025 18:59:05 +0200 Subject: [PATCH 2/7] add more documentation + sort initialization options + check TS version + check kind instead of name --- docs/configuration.md | 29 ++++++++++++++++++++++++++++- src/lsp-server.ts | 4 +++- src/refactor.ts | 2 +- src/ts-protocol.ts | 2 +- src/utils/api.ts | 1 + 5 files changed, 34 insertions(+), 4 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 474e0700..37ba0a54 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -17,12 +17,12 @@ The language server accepts various settings through the `initializationOptions` | hostInfo | string | Information about the host, for example `"Emacs 24.4"` or `"Sublime Text v3075"`. **Default**: `undefined` | | completionDisableFilterText | boolean | Don't set `filterText` property on completion items. **Default**: `false` | | disableAutomaticTypingAcquisition | boolean | Disables tsserver from automatically fetching missing type definitions (`@types` packages) for external modules. | -| supportsMoveToFileCodeAction | boolean | Whether the client supports the "Move to file" interactive code action. | | maxTsServerMemory | number | The maximum size of the V8's old memory section in megabytes (for example `4096` means 4GB). The default value is dynamically configured by Node so can differ per system. Increase for very big projects that exceed allowed memory usage. **Default**: `undefined` | | npmLocation | string | Specifies the path to the NPM executable used for Automatic Type Acquisition. | | locale | string | The locale to use to show error messages. | | plugins | object[] | An array of `{ name: string, location: string, languages?: string[] }` objects for registering a Typescript plugins. **Default**: [] | | preferences | object | Preferences passed to the Typescript (`tsserver`) process. See below for more | +| supportsMoveToFileCodeAction | boolean | Whether the client supports the "Move to file" interactive code action. See below for more | | tsserver | object | Options related to the `tsserver` process. See below for more | ### `plugins` option @@ -31,6 +31,33 @@ Accepts a list of `tsserver` (typescript) plugins. The `name` and the `location` are required. The `location` is a path to the package or a directory in which `tsserver` will try to import the plugin `name` using Node's `require` API. The `languages` property specifies which extra language IDs the language server should accept. This is required when plugin enables support for language IDs that this server does not support by default (so other than `typescript`, `typescriptreact`, `javascript`, `javascriptreact`). It's an optional property and only affects which file types the language server allows to be opened and do not concern the `tsserver` itself. +### `supportsMoveToFileCodeAction` option + +The "Move to file" code action is different from other code actions as it is interactive (it needs to ask the user for a file path) and therefore requires custom implementation in the client. + +In order to support it, when the user chooses the code action named "Move to file" (of kind `refactor.move.file`), the client should first ask the user for a file path (via a file picker or equivalent), and then send a `workspace/executeCommand` with parameters as follows: +```json +{ + "command": "_typescript.applyRefactoring", + "arguments": [ + { + "file": "/path/to/project/currentFile.ts", + "startLine": 1, + "startOffset": 1, + "endLine": 2, + "endOffset": 1, + "refactor": "Move to file", + "action": "Move to file", + "interactiveRefactorArguments": { + "targetFile": "/path/to/project/chosenDestinationFile.ts" + } + } + ] +} +``` + +That is, it should use the arguments provided by the code action (like for other code actions), but also insert an additional `interactiveRefactorArguments` argument containing a `targetFile` field containing the absolute path of the file chosen by the user. + ### `tsserver` options Specifies additional options related to the internal `tsserver` process, like tracing and logging: diff --git a/src/lsp-server.ts b/src/lsp-server.ts index 43888066..fa59b07c 100644 --- a/src/lsp-server.ts +++ b/src/lsp-server.ts @@ -110,7 +110,9 @@ export class LspServer { // Setup supported features. this.features.completionDisableFilterText = userInitializationOptions.completionDisableFilterText ?? false; - this.features.moveToFileCodeActionSupport = userInitializationOptions.supportsMoveToFileCodeAction ?? false; + this.features.moveToFileCodeActionSupport = + userInitializationOptions.supportsMoveToFileCodeAction && + typescriptVersion.version?.gte(API.v520); const { textDocument } = clientCapabilities; if (textDocument) { const { codeAction, completion, definition, publishDiagnostics } = textDocument; diff --git a/src/refactor.ts b/src/refactor.ts index 9c0aaaff..4803111d 100644 --- a/src/refactor.ts +++ b/src/refactor.ts @@ -22,7 +22,7 @@ export function provideRefactors(response: ts.server.protocol.GetApplicableRefac if (action.notApplicableReason && !features.codeActionDisabledSupport) { return false; } - if (action.isInteractive && (!features.moveToFileCodeActionSupport || action.name !== 'Move to file')) { + if (action.isInteractive && (!features.moveToFileCodeActionSupport || action.kind !== 'refactor.move.file')) { return false; } return true; diff --git a/src/ts-protocol.ts b/src/ts-protocol.ts index 6e411958..bf03e33b 100644 --- a/src/ts-protocol.ts +++ b/src/ts-protocol.ts @@ -364,13 +364,13 @@ export interface TypeScriptPlugin { export interface TypeScriptInitializationOptions { completionDisableFilterText?: boolean; disableAutomaticTypingAcquisition?: boolean; - supportsMoveToFileCodeAction?: boolean; hostInfo?: string; locale?: string; maxTsServerMemory?: number; npmLocation?: string; plugins?: TypeScriptPlugin[]; preferences?: ts.server.protocol.UserPreferences; + supportsMoveToFileCodeAction?: boolean; tsserver?: TsserverOptions; } diff --git a/src/utils/api.ts b/src/utils/api.ts index 871f07ed..f790dfaa 100644 --- a/src/utils/api.ts +++ b/src/utils/api.ts @@ -33,6 +33,7 @@ export default class API { public static readonly v490 = API.fromSimpleString('4.9.0'); public static readonly v500 = API.fromSimpleString('5.0.0'); public static readonly v510 = API.fromSimpleString('5.1.0'); + public static readonly v520 = API.fromSimpleString('5.2.0'); public static readonly v540 = API.fromSimpleString('5.4.0'); public static fromVersionString(versionString: string): API { From dd23879400d679ecc6b95248182a45dbed8937b1 Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Sat, 13 Sep 2025 19:31:28 +0200 Subject: [PATCH 3/7] tweak docs --- docs/configuration.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index 37ba0a54..7e6b6e0e 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -3,6 +3,8 @@ - [Configuration](#configuration) - [initializationOptions](#initializationoptions) + - [`plugins` option](#plugins-option) + - [`supportsMoveToFileCodeAction` option](#supportsmovetofilecodeaction-option) - [`tsserver` options](#tsserver-options) - [`preferences` options](#preferences-options) - [workspace/didChangeConfiguration](#workspacedidchangeconfiguration) @@ -21,7 +23,8 @@ The language server accepts various settings through the `initializationOptions` | npmLocation | string | Specifies the path to the NPM executable used for Automatic Type Acquisition. | | locale | string | The locale to use to show error messages. | | plugins | object[] | An array of `{ name: string, location: string, languages?: string[] }` objects for registering a Typescript plugins. **Default**: [] | -| preferences | object | Preferences passed to the Typescript (`tsserver`) process. See below for more | +| preferences | object | Preferences passed to the Typescript (`tsserver`) process. See [`supportsMoveToFileCodeAction` option](#supportsmovetofilecodeaction-option) + for more details. | | supportsMoveToFileCodeAction | boolean | Whether the client supports the "Move to file" interactive code action. See below for more | | tsserver | object | Options related to the `tsserver` process. See below for more | From 110bc8a056b978dd4e2ec097a8d2b19b944e28af Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Sat, 13 Sep 2025 19:32:07 +0200 Subject: [PATCH 4/7] fix docs --- docs/configuration.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 7e6b6e0e..d70dcc4e 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -23,8 +23,7 @@ The language server accepts various settings through the `initializationOptions` | npmLocation | string | Specifies the path to the NPM executable used for Automatic Type Acquisition. | | locale | string | The locale to use to show error messages. | | plugins | object[] | An array of `{ name: string, location: string, languages?: string[] }` objects for registering a Typescript plugins. **Default**: [] | -| preferences | object | Preferences passed to the Typescript (`tsserver`) process. See [`supportsMoveToFileCodeAction` option](#supportsmovetofilecodeaction-option) - for more details. | +| preferences | object | Preferences passed to the Typescript (`tsserver`) process. See [`supportsMoveToFileCodeAction` option](#supportsmovetofilecodeaction-option) for more details. | | supportsMoveToFileCodeAction | boolean | Whether the client supports the "Move to file" interactive code action. See below for more | | tsserver | object | Options related to the `tsserver` process. See below for more | From b9d4798b9e7dfd06ede8765193d1b60c7e8e94e9 Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Sat, 13 Sep 2025 19:33:07 +0200 Subject: [PATCH 5/7] fix --- docs/configuration.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index d70dcc4e..ebb3d018 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -23,8 +23,8 @@ The language server accepts various settings through the `initializationOptions` | npmLocation | string | Specifies the path to the NPM executable used for Automatic Type Acquisition. | | locale | string | The locale to use to show error messages. | | plugins | object[] | An array of `{ name: string, location: string, languages?: string[] }` objects for registering a Typescript plugins. **Default**: [] | -| preferences | object | Preferences passed to the Typescript (`tsserver`) process. See [`supportsMoveToFileCodeAction` option](#supportsmovetofilecodeaction-option) for more details. | -| supportsMoveToFileCodeAction | boolean | Whether the client supports the "Move to file" interactive code action. See below for more | +| preferences | object | Preferences passed to the Typescript (`tsserver`) process. See [`preferences` options](#preferences-options) for more details. | +| supportsMoveToFileCodeAction | boolean | Whether the client supports the "Move to file" interactive code action. See [`supportsMoveToFileCodeAction` option](#supportsmovetofilecodeaction-option) for more details. | | tsserver | object | Options related to the `tsserver` process. See below for more | ### `plugins` option From 82e3ce33e31727519c8a6c02d427471dc5dfab7b Mon Sep 17 00:00:00 2001 From: Guillaume Brunerie Date: Sat, 13 Sep 2025 20:35:42 +0200 Subject: [PATCH 6/7] make argument mandatory --- src/lsp-server.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lsp-server.ts b/src/lsp-server.ts index fa59b07c..9d3394cf 100644 --- a/src/lsp-server.ts +++ b/src/lsp-server.ts @@ -785,7 +785,7 @@ export class LspServer { actions.push(...provideQuickFix(await this.getCodeFixes(fileRangeArgs, params.context, token), this.tsClient)); } if (!kinds || kinds.some(kind => kind.contains(CodeActionKind.Refactor))) { - actions.push(...provideRefactors(await this.getRefactors(fileRangeArgs, params.context, token, this.features), fileRangeArgs, this.features)); + actions.push(...provideRefactors(await this.getRefactors(fileRangeArgs, params.context, this.features, token), fileRangeArgs, this.features)); } for (const kind of kinds || []) { @@ -839,7 +839,7 @@ export class LspServer { const response = await this.tsClient.execute(CommandTypes.GetCodeFixes, args, token); return response.type === 'response' ? response : undefined; } - protected async getRefactors(fileRangeArgs: ts.server.protocol.FileRangeRequestArgs, context: lsp.CodeActionContext, token?: lsp.CancellationToken, features?: SupportedFeatures): Promise { + protected async getRefactors(fileRangeArgs: ts.server.protocol.FileRangeRequestArgs, context: lsp.CodeActionContext, features: SupportedFeatures, token?: lsp.CancellationToken): Promise { const args: ts.server.protocol.GetApplicableRefactorsRequestArgs = { ...fileRangeArgs, triggerReason: context.triggerKind === lsp.CodeActionTriggerKind.Invoked ? 'invoked' : undefined, From c961eb0f83ae32e015cbea95e6e4ab461beb53f2 Mon Sep 17 00:00:00 2001 From: Guillaume Brunerie Date: Sun, 14 Sep 2025 23:46:38 +0200 Subject: [PATCH 7/7] check name instead of kind and remove question mark --- docs/configuration.md | 2 +- src/lsp-server.ts | 2 +- src/refactor.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index ebb3d018..9775f01c 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -37,7 +37,7 @@ The `languages` property specifies which extra language IDs the language server The "Move to file" code action is different from other code actions as it is interactive (it needs to ask the user for a file path) and therefore requires custom implementation in the client. -In order to support it, when the user chooses the code action named "Move to file" (of kind `refactor.move.file`), the client should first ask the user for a file path (via a file picker or equivalent), and then send a `workspace/executeCommand` with parameters as follows: +In order to support it, when the user chooses the code action named "Move to file", the client should first ask the user for a file path (via a file picker or equivalent), and then send a `workspace/executeCommand` with parameters as follows: ```json { "command": "_typescript.applyRefactoring", diff --git a/src/lsp-server.ts b/src/lsp-server.ts index 9d3394cf..13d49a67 100644 --- a/src/lsp-server.ts +++ b/src/lsp-server.ts @@ -844,7 +844,7 @@ export class LspServer { ...fileRangeArgs, triggerReason: context.triggerKind === lsp.CodeActionTriggerKind.Invoked ? 'invoked' : undefined, kind: context.only?.length === 1 ? context.only[0] : undefined, - includeInteractiveActions: features?.moveToFileCodeActionSupport, + includeInteractiveActions: features.moveToFileCodeActionSupport, }; const response = await this.tsClient.execute(CommandTypes.GetApplicableRefactors, args, token); return response.type === 'response' ? response : undefined; diff --git a/src/refactor.ts b/src/refactor.ts index 4803111d..9c0aaaff 100644 --- a/src/refactor.ts +++ b/src/refactor.ts @@ -22,7 +22,7 @@ export function provideRefactors(response: ts.server.protocol.GetApplicableRefac if (action.notApplicableReason && !features.codeActionDisabledSupport) { return false; } - if (action.isInteractive && (!features.moveToFileCodeActionSupport || action.kind !== 'refactor.move.file')) { + if (action.isInteractive && (!features.moveToFileCodeActionSupport || action.name !== 'Move to file')) { return false; } return true;