diff --git a/docs/configuration.md b/docs/configuration.md index bae97804..9775f01c 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 [`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 @@ -30,6 +33,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", 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 11047e56..13d49a67 100644 --- a/src/lsp-server.ts +++ b/src/lsp-server.ts @@ -110,6 +110,9 @@ export class LspServer { // Setup supported features. this.features.completionDisableFilterText = userInitializationOptions.completionDisableFilterText ?? false; + this.features.moveToFileCodeActionSupport = + userInitializationOptions.supportsMoveToFileCodeAction && + typescriptVersion.version?.gte(API.v520); const { textDocument } = clientCapabilities; if (textDocument) { const { codeAction, completion, definition, publishDiagnostics } = textDocument; @@ -782,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), fileRangeArgs, this.features)); + actions.push(...provideRefactors(await this.getRefactors(fileRangeArgs, params.context, this.features, token), fileRangeArgs, this.features)); } for (const kind of kinds || []) { @@ -836,11 +839,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, features: SupportedFeatures, token?: lsp.CancellationToken): 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 +871,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..bf03e33b 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 { @@ -369,6 +370,7 @@ export interface TypeScriptInitializationOptions { 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 {