diff --git a/src/services/refactorProvider.ts b/src/services/refactorProvider.ts index b341d18124404..911d167588cca 100644 --- a/src/services/refactorProvider.ts +++ b/src/services/refactorProvider.ts @@ -6,8 +6,10 @@ import { Refactor, RefactorContext, RefactorEditInfo, + FileTextChanges, } from "./_namespaces/ts.js"; import { refactorKindBeginsWith } from "./_namespaces/ts.refactor.js"; +import { getEditsForFileRename } from '../services/refactors/autoRenamePlugin.js'; // A map with the refactor code as key, the refactor itself as value // e.g. nonSuggestableRefactors[refactorCode] -> the refactor you want @@ -35,3 +37,34 @@ export function getEditsForRefactor(context: RefactorContext, refactorName: stri const refactor = refactors.get(refactorName); return refactor && refactor.getEditsForAction(context, actionName, interactiveRefactorArguments); } + +/** @internal */ +export function autoRenameRefactor( + fileName: string, + newFileName: string, + program: ts.Program +): RefactorEditInfo | undefined { + const edits: FileTextChanges[] = getEditsForFileRename(fileName, newFileName, program); + if (edits.length === 0) return; + return { edits }; +} + +registerRefactor("autoRename", { + kinds: ["refactor.autoRename"], + getAvailableActions(context: RefactorContext): ApplicableRefactorInfo[] { + const { fileName, newFileName } = context; + if (!fileName || !newFileName) return []; + + return [{ + name: "autoRenameFunction", + description: "Rename function to match the file name", + actions: [{ + name: "autoRenameFunctionAction", + description: "Automatically rename the function to match the file name", + }], + }]; + }, + getEditsForAction(context: RefactorContext): RefactorEditInfo | undefined { + return autoRenameRefactor(context.fileName, context.newFileName!, context.program); + }, +}); \ No newline at end of file diff --git a/src/services/refactors/autoRenamePlugin.ts b/src/services/refactors/autoRenamePlugin.ts new file mode 100644 index 0000000000000..0f8d29362ed97 --- /dev/null +++ b/src/services/refactors/autoRenamePlugin.ts @@ -0,0 +1,41 @@ +import * as ts from "../_namespaces/ts"; + +export function getEditsForFileRename( + oldFilePath: string, + newFilePath: string, + sourceFile: ts.SourceFile +): ts.FileTextChanges[] { + const edits: ts.FileTextChanges[] = []; + + // Extract function name from old and new file paths + const oldFileName = ts.getBaseFileName(oldFilePath).replace(/\.[jt]sx?$/, ""); + const newFileName = ts.getBaseFileName(newFilePath).replace(/\.[jt]sx?$/, ""); + + if (!oldFileName || !newFileName) return edits; + + // Traverse the source file to find a matching function + function visit(node: ts.Node) { + if ( + ts.isFunctionDeclaration(node) && + node.name?.text === oldFileName + ) { + edits.push({ + fileName: sourceFile.fileName, + textChanges: [ + { + newText: newFileName, + span: { + start: node.name.getStart(), + length: node.name.getWidth(), + }, + }, + ], + }); + } + + ts.forEachChild(node, visit); + } + + visit(sourceFile); + return edits; +} diff --git a/tests/cases/fourslash/autoRenamePlugin.ts b/tests/cases/fourslash/autoRenamePlugin.ts new file mode 100644 index 0000000000000..233b2af1ed193 --- /dev/null +++ b/tests/cases/fourslash/autoRenamePlugin.ts @@ -0,0 +1,83 @@ +/// + +// Test Case: Rename Function and Update Imports + +// Step 1: Create an initial file "myFunction.ts" with named, default exports and arrow function +// @filename: myFunction.ts +//// export function myFunction() {} +//// export const myFunc = () => {}; +//// export default function defaultFunction() {} + +// Step 2: Perform file renaming from "myFunction.ts" to "myNewFunction.ts" +goTo.file("myFunction.ts"); +renameFile("myFunction.ts", "myNewFunction.ts"); + +// Verify that the content of the current file reflects the correct function name change +verify.currentFileContentIs(`export function myNewFunction() {} +export const myFunc = () => {}; +export default function defaultFunction() {}`); + +// Step 3: Verify the imported path in other files + +// @filename: anotherFile.ts +//// import { myFunction } from './myFunction'; +//// import myDefaultFunction from './myFunction'; + +goTo.file("anotherFile.ts"); + +// After renaming, the import paths and bindings should be updated correctly +verify.currentFileContentIs(`import { myFunction } from './myNewFunction'; +import myDefaultFunction from './myNewFunction';`); + +verify.importBindingChange("myFunction", "myNewFunction"); // Check that the named function is renamed correctly +verify.importBindingChange("myDefaultFunction", "defaultFunction"); // Default function name should not change + +// Step 4: Edge Case - Renaming a class + +// Initial file: myClass.ts +//// export class MyClass {} +//// export default class MyClassDefault {} + +goTo.file("myClass.ts"); +renameFile("myClass.ts", "myNewClass.ts"); + +// Verify that the class name and file name are updated correctly +verify.currentFileContentIs(`export class MyNewClass {} +export default class MyClassDefault {}`); + +// Step 5: Verify imports in another file for the class + +// @filename: anotherFileWithClass.ts +//// import { MyClass } from './myClass'; +//// import MyClassDefault from './myClass'; + +goTo.file("anotherFileWithClass.ts"); + +// Verify that the import paths and bindings are updated correctly after renaming +verify.currentFileContentIs(`import { MyClass } from './myNewClass'; +import MyClassDefault from './myNewClass';`); + +verify.importBindingChange("MyClass", "MyNewClass"); // Check that the class name is renamed correctly +verify.importBindingChange("MyClassDefault", "MyClassDefault"); // Default class name should remain the same + +// Step 6: Edge Case - Renaming an arrow function + +// Initial file: myArrowFunction.ts +//// export const myArrowFunc = () => {}; + +goTo.file("myArrowFunction.ts"); +renameFile("myArrowFunction.ts", "myNewArrowFunction.ts"); + +// Verify the renamed arrow function is updated correctly +verify.currentFileContentIs(`export const myNewArrowFunc = () => {};`); + +// Step 7: Verify imports for the renamed arrow function + +// @filename: anotherFileWithArrow.ts +//// import { myArrowFunc } from './myArrowFunction'; + +goTo.file("anotherFileWithArrow.ts"); + +// Verify the import path and binding name after renaming the arrow function +verify.currentFileContentIs(`import { myArrowFunc } from './myNewArrowFunction';`); +verify.importBindingChange("myArrowFunc", "myNewArrowFunc"); // Verify the arrow function name change