diff --git a/extensions/typescript-language-features/src/languageFeatures/refactor.ts b/extensions/typescript-language-features/src/languageFeatures/refactor.ts index 8fa071892f782..1c0e9d006293b 100644 --- a/extensions/typescript-language-features/src/languageFeatures/refactor.ts +++ b/extensions/typescript-language-features/src/languageFeatures/refactor.ts @@ -21,6 +21,7 @@ import { nulToken } from '../utils/cancellation'; import FormattingOptionsManager from './fileConfigurationManager'; import { conditionalRegistration, requireSomeCapability } from './util/dependentRegistration'; import { EditorChatFollowUp, EditorChatFollowUp_Args, CompositeCommand } from './util/copilot'; +import * as PConst from '../tsServer/protocol/protocol.const'; function toWorkspaceEdit(client: ITypeScriptServiceClient, edits: readonly Proto.FileCodeEdits[]): vscode.WorkspaceEdit { const workspaceEdit = new vscode.WorkspaceEdit(); @@ -435,6 +436,30 @@ class MoveToFileCodeAction extends vscode.CodeAction { arguments: [{ action, document, range }] }; } + + private static readonly _declarationKinds = new Set([ + PConst.Kind.module, + PConst.Kind.class, + PConst.Kind.interface + ]); + + static isOnDeclarationName(node: Proto.NavigationTree | undefined, range: vscode.Range) { + if (!node) { + return false; + } + const isRangeInSpan = (span: Proto.TextSpan) => typeConverters.Range.fromTextSpan(span).contains(range); + if (MoveToFileCodeAction._declarationKinds.has(node.kind) && node.nameSpan && isRangeInSpan(node.nameSpan)) { + return true; + } + if (node.childItems && node.spans.some(isRangeInSpan)) { + for (const child of node.childItems) { + if (MoveToFileCodeAction.isOnDeclarationName(child, range)) { + return true; + } + } + } + return false; + } } class SelectCodeAction extends vscode.CodeAction { @@ -516,7 +541,8 @@ class TypeScriptRefactorProvider implements vscode.CodeActionProvider { + const applicableRefactors = await this.convertApplicableRefactors(document, response.body, rangeOrSelection); + const actions = await Promise.all(applicableRefactors.map(async (action) => { if (this.client.apiVersion.lt(API.v430)) { // Don't show 'infer return type' refactoring unless it has been explicitly requested // https://github.com/microsoft/TypeScript/issues/42993 @@ -524,8 +550,15 @@ class TypeScriptRefactorProvider implements vscode.CodeActionProvider applicableRefactors.filter((_, index) => mappedRefactors[index])); if (!context.only) { return actions; @@ -547,31 +580,44 @@ class TypeScriptRefactorProvider implements vscode.CodeActionProvider { + ): Promise> { + const actions: Array = []; for (const refactor of refactors) { if (refactor.inlineable === false) { - yield new SelectCodeAction(refactor, document, rangeOrSelection); + actions.push(new SelectCodeAction(refactor, document, rangeOrSelection)); } else { for (const action of refactor.actions) { - yield this.refactorActionToCodeAction(document, refactor, action, rangeOrSelection, refactor.actions); + const refactorAction = await this.refactorActionToCodeAction(document, refactor, action, rangeOrSelection, refactor.actions); + if (refactorAction) { + actions.push(refactorAction); + } } } } + return actions; } - private refactorActionToCodeAction( + private async refactorActionToCodeAction( document: vscode.TextDocument, refactor: Proto.ApplicableRefactorInfo, action: Proto.RefactorActionInfo, rangeOrSelection: vscode.Range | vscode.Selection, allActions: readonly Proto.RefactorActionInfo[], - ): TsCodeAction { + ): Promise { let codeAction: TsCodeAction; if (action.name === 'Move to file') { + const navigationTree = await this.client.execute('navtree', { file: document.uri.path }, nulToken); + if (navigationTree.type !== 'response') { + return; + } + const shouldIncludeMoveToAction = MoveToFileCodeAction.isOnDeclarationName(navigationTree.body, rangeOrSelection); + if (!shouldIncludeMoveToAction) { + return; + } codeAction = new MoveToFileCodeAction(document, action, rangeOrSelection); } else { let copilotRename: ((info: Proto.RefactorEditInfo) => vscode.Command) | undefined;