Look at commit a8e8ff17f07a5000689a2b3e6cc7994227695926 for a complete example of adding a new refactoring.
Follow Test-Driven Development:
- Create tests first to illustrate desired behavior
- Implement to make tests pass
- Refactor to match coding style
Each refactoring lives in its own directory:
src/refactorings/{refactoring-name}/
├── index.ts # Configuration export
├── {refactoring-name}.ts # Main implementation
└── {refactoring-name}.test.ts # Tests
- Create
src/refactorings/{name}/{name}.ts- Main implementation - Create
src/refactorings/{name}/{name}.test.ts- Tests - Create
src/refactorings/{name}/index.ts- Config export - Modify
src/extension.ts- Import and register the refactoring - Modify
package.json- Add command, configuration, and menu entry
Always use AST transformations, not RegExp. Follow this structure:
import * as t from "../../ast";
import { Selection } from "../../editor/selection";
import { COMMANDS, EditorCommand, RefactoringState } from "../../refactorings";
export function myRefactoring(state: RefactoringState): EditorCommand {
const updatedCode = updateCode(t.parse(state.code), state.selection);
if (!updatedCode.hasCodeChanged) {
return COMMANDS.showErrorDidNotFind("element to refactor");
}
return COMMANDS.write(updatedCode.code);
}
function updateCode(ast: t.AST, selection: Selection): t.Transformed {
return t.transformAST(
ast,
createVisitor(selection, (path) => {
// Transform the AST node
})
);
}
export function createVisitor(
selection: Selection,
onMatch: (path: t.NodePath) => void
): t.Visitor {
return {
// Visitor for specific AST node types
SomeNodeType(path) {
if (!selection.isInsidePath(path)) return;
onMatch(path);
}
};
}For refactorings with Quick Fix action provider:
import { RefactoringWithActionProviderConfig } from "../../refactorings";
import { myRefactoring, createVisitor } from "./my-refactoring";
const config: RefactoringWithActionProviderConfig = {
command: {
key: "myRefactoring", // camelCase
operation: myRefactoring,
title: "My Refactoring"
},
actionProvider: {
message: "Refactor this",
createVisitor
}
};
export default config;import { InMemoryEditor } from "../../editor/adapters/in-memory-editor";
import { Code } from "../../editor/editor";
import { myRefactoring } from "./my-refactoring";
describe("My Refactoring", () => {
it("does the thing", () => {
shouldRefactor({
code: `input code with [cursor] marker`,
expected: `expected output`
});
});
it("shows error when not applicable", () => {
shouldShowError({
code: `code that should not match`
});
});
});
function shouldRefactor({ code, expected }: { code: Code; expected: Code }) {
const editor = new InMemoryEditor(code);
const result = myRefactoring({
state: "new",
code: editor.code,
selection: editor.selection,
highlightSources: []
});
expect(result).toMatchObject({ action: "write", code: expected });
}
function shouldShowError({ code }: { code: Code }) {
const editor = new InMemoryEditor(code);
const result = myRefactoring({
state: "new",
code: editor.code,
selection: editor.selection,
highlightSources: []
});
expect(result.action).toBe("show error");
}Test cursor markers:
[cursor]- Cursor position[start]and[end]- Selection range
// 1. Import
import myRefactoring from "./refactorings/my-refactoring";
// 2. Add to appropriate category in refactorings object
const refactorings = {
allLanguages: {
withActionProvider: [
// ... existing refactorings
myRefactoring
]
}
};Add in three places:
- Command in
contributes.commands:
{
"command": "abracadabra.myRefactoring",
"title": "My Refactoring",
"category": "Abracadabra"
}- Configuration in
contributes.configuration.properties:
"abracadabra.myRefactoring.showInQuickFix": {
"type": "boolean",
"default": true,
"description": "Check if it should appear in the Quick Fix suggestions when it can be executed"
}- Menu in
contributes.menus.commandPalette:
{
"command": "abracadabra.myRefactoring",
"when": "editorLangId == javascript || editorLangId == javascriptreact || editorLangId == typescript || editorLangId == typescriptreact || editorLangId == vue || editorLangId == svelte"
}- Use AST, not RegExp - Use Babel types (
t.isFunctionDeclaration(), etc.) instead of regex patterns - Use
t.transformAST- Standard way to transform code - Export
createVisitor- Required for action provider to detect when refactoring is applicable - Handle Recast comments - Use
@ts-expect-errorfor Recast's customcommentsattribute when manipulating comments - Check
hasCodeChanged- Always verify transformation happened before returning
t.parse(code)- Parse code to ASTt.transformAST(ast, visitor)- Transform AST with visitort.traverseAST(ast, visitor)- Traverse without transformingselection.isInsidePath(path)- Check if cursor is in a nodepath.node.leadingComments- Access comments attached to a node
yarn test --testPathPatterns="my-refactoring" --no-coverage
yarn typecheck