Refactory
Convention-aware refactorings for C# and React/TypeScript in VS Code.
Status: early but real. 23 actions, 187 tests (58 xunit for the C# engine, 108
for the TypeScript engine, 21 running inside an actual VS Code). Not on the
Marketplace yet — build a VSIX with .\pack.ps1.
What this is — and what it deliberately is not
VS Code already ships extract-function, move-to-new-file, organize-imports and
rename. JetBrains ships an official ReSharper extension for generic C#
refactoring. Re-implementing any of that would produce a worse copy.
This extension does the three things nobody serves:
- Convention-aware generation — put the file where your repo actually puts
it, with your copyright header, your XML docs, your
this. qualification and
your BOM, so the result passes your analyzers instead of flooding review.
- Solution-shaped multi-file operations — DI registration, interface ↔
implementation sync, controller version clones.
- React/Next semantics —
'use client' propagation, extract component and
extract hook into real files, alias-aware imports.
Plus one Refactor This menu that lists the built-in refactorings alongside
ours, so "we don't rebuild what exists" is visible rather than merely claimed.
One keystroke, everything applicable
Ctrl+Alt+Shift+T — unbound on every platform, so nothing of yours is stolen.

Ours are grouped under Refactory; VS Code's and the language servers'
own actions stay right there under Built-in refactorings. The menu opens
immediately and streams the slower ones in.
The same actions also nest correctly in the native refactor menu (Ctrl+.), in
the groups they belong to — not in a parallel menu of their own:

Select JSX, extract. Props are inferred from what the selection captures, key
moves to the call site, and the name is edited in place — type once and the
interface, the function and the props type all update together:

The new-file variant carries 'use client' across when the source had it, brings
only the imports the markup actually uses, and exports a module-level
constant rather than duplicating it — a copied object literal is a different
object, which quietly breaks reference equality downstream.
If a captured value's type cannot be derived from the file, the refactoring
refuses and names it. A plausible-but-wrong type compiles, which makes it
exactly the corruption no later gate would catch.
C#: edits that look like their neighbours

Adding an injected dependency means a using in sorted position, a field, its
XML doc, a constructor parameter, a <param> tag at the matching position, and
an assignment. Style is measured from the class being edited — underscore or
not, this. or not, documented fields or not:

Two refusals matter more than the generation itself:
- A manual
new Foo(...) call site means we would have to invent an argument.
Injecting null! to keep it compiling is precisely the corruption this tool
exists to prevent.
- Whether the type is registered in DI is never implied to have been checked.
An unregistered dependency compiles perfectly and throws at application start,
invisible to every type-based verifier.
What ships today
React / TypeScript
| Refactoring |
Needs |
| Convert import to alias path / to relative path |
one file |
| Extract JSX into a component (same file, with in-place naming) |
one file |
| Extract JSX into a new component file |
one file |
Extract a custom hook (same file, or into your _hooks/ folder) |
one file |
Convert const X: FC<Props> to a function declaration |
one file |
Wrap JSX in a fragment / a conditional / a .map |
one file |
| Inline variable |
one file |
'use client' boundary diagnostics + quick fixes (Next.js) |
one file |
| Move a declaration to its own file, repointing every importer |
import graph |
| Safe delete a declaration |
import graph |
| Rename a component prop, including every JSX call site |
import graph |
C#
| Refactoring |
Needs |
| Add injected dependency (six coordinated edits) |
one file |
Add missing ConfigureAwait(false) |
one file |
| Document a member (copying the interface's wording when there is one) |
declaration index |
| Implement an interface member in every implementation |
declaration index |
| Extract interface into the folder your repo actually uses |
declaration index |
| Register a service in the DI container |
declaration index |
| Clone a controller into the next API version |
declaration index |
Plus the Refactor This menu, which lists all of the above alongside
everything VS Code and the language servers already offer.
Nothing here loads a TypeScript program or a Roslyn solution. The editor
already holds one of each; doubling that is how an extension becomes the thing
users blame for a slow VS Code. Cross-file questions are answered by two cheap
indexes instead:
- a declaration index for C# — a parallel parse of top-level declarations, no
MetadataReference, no MSBuild — which answers "who implements this" in under
a second instead of after a 20–60 second solution load;
- a lex-only import graph for TypeScript, which answers "which files mention
this file" without a type checker.
The limits of that choice are surfaced, not hidden. An implementation that
inherits an interface indirectly is not found, and the refusal says so; an
import statement whose bindings could not be read cheaply is left alone, and the
plan reports how many.
Safety
Every refactoring is a pure function producing a serializable plan, which then
passes a fixed pipeline before anything touches disk:
- Preconditions — never "offer and hope". Three states only: available,
available-with-warnings (preview forced), or unavailable with a reason you
can read.
- Optimistic concurrency —
WorkspaceEdit has no version field, so we built
one. If a file changed while we were analysing, the edit aborts before touching
disk. Stale offsets are never rebased onto changed text.
- Post-apply verification — a
WorkspaceEdit containing file creations is
documented as not all-or-nothing, so creations are ordered first and content
hashes are re-checked afterwards.
- One undo stop — verified in the real editor, including the created file.
See docs/M0-spike-report.md for the editor capabilities
this rests on and how each was verified.
Layout
| Path |
Role |
packages/extension |
VS Code host. The only package allowed to import vscode. |
packages/core-ts |
TS/React engine. Must not import vscode or touch the filesystem — enforced by a test, not a convention. |
server-dotnet |
Roslyn sidecar, JSON-RPC over stdio. Lazy: never spawned for a TypeScript-only session. |
fixtures/ |
Golden fixtures, test workspaces and the screenshot demo project. |
scripts/screenshots.ps1 |
Captures the screenshots above from a real editor. |
Development
npm install
npm run build # tsc --noEmit + esbuild bundle
npm run test:unit # vitest — no editor, milliseconds
npm run test:cs # xunit — the C# engine
npm run test:int # @vscode/test-cli — downloads VS Code on first run
npm test # all three
.\pack.ps1 -Bump # version bump + dotnet publish + vsce package + install
F5 launches an Extension Development Host. Three configurations are provided:
an empty workspace, a React repo and a C# solution.
Developer commands
Set "refactory.developerMode": true to expose them.
- Why Is Nothing Offered Here? — structured diagnosis of the caret position:
which tsconfig governs the file, which aliases were detected, what the engine
saw. This is the most common question a refactoring tool has to answer.
- Dump Available Code Actions Here — everything every provider offers at the
cursor. Run it in a
.cs file with C# Dev Kit on and in a .tsx file to keep
the "don't rebuild what exists" table honest as the SDKs move.
- Run UX Capability Spike — probes the editor capabilities the design depends
on and writes a report.
Screenshots
.\scripts\screenshots.ps1
Captures with PrintWindow(PW_RENDERFULLCONTENT), which reads the window's own
content. That is a privacy decision, not a technical one: anything that grabs a
screen rectangle captures whatever is actually on screen if the target window is
not on top.
License
MIT