VS Code Extensions
Refactory

Refactory

by mark-ku

Convention-aware refactorings for C#, React/TypeScript, Python, Go, Java, Rust and more — one keystroke, your project's conventions.

Downloads

1

Rating

(0)

Version

0.0.48

Last updated

Aug 03, 2026

Convention-aware refactorings for C#, React/TypeScript and fifteen more languages in VS Code.

Status: early but real. 34 refactorings plus a 38-rule code-smell inspection layer across 18 languages, complexity metrics, scan history and churn×complexity hotspots, 532 tests (94 xunit for the C# engine, 406 for the two TypeScript-hosted engines, 32 running inside an actual VS Code). On the Marketplace as mark-ku.refactory, or build a VSIX yourself with .\pack.ps1. Recent changes: CHANGELOG.md.

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 four things nobody serves:

  1. 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.
  2. Solution-shaped multi-file operations — DI registration, interface ↔ implementation sync, controller version clones.
  3. React/Next semantics'use client' propagation, extract component and extract hook into real files, alias-aware imports.
  4. The everyday structural edits, in the languages that have none. Invert this if, merge these two, pull this expression into a variable, turn this concatenation into interpolation. Every JetBrains IDE has had these for fifteen years; VS Code has them for almost no language. Refactory has them for Python, Go, Java, Kotlin, Rust, PHP, C, C++, Objective-C, Dart, Swift and Scala — with no language server, no toolchain, and no project load.

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 — and now shows the unavailable ones too, with the reason on the line.


One keystroke, everything applicable

Ctrl+Alt+Shift+T — unbound on every platform, so nothing of yours is stolen.

Refactor This menu

Ours are grouped under Refactory; VS Code's and the language servers' own actions stay right there under Built-in refactorings. The ones you used most recently float to the top of our group — refactoring is repetitive work, and a rename sweep is the same action forty times.

A third group holds what is unavailable here, with each provider's own reason as the detail line. "Why is Extract Method not offered on this selection" is the question a refactoring menu exists to answer, and an action that silently vanishes answers it with nothing.

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:

Native refactor menu


React: extract a component, keep the conventions

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:

Extract component

Real recording, not a mockup — scripts/gif.ps1 films the editor doing it.

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

C# refactor menu

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:

Add injected dependency

Six edits, one undo stop, and every one of them visible before it lands.

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.

Code smells and the Code Health dashboard

The inspection layer the JetBrains tools ship and VS Code does not: squiggles that say "this works, but it will hurt you", each carrying a quick fix that opens the refactoring menu on the offending range.

  • React / Next.js, live while you type: a component defined inside another component (the state-destroying one), useEffect with no dependency array, the map index used as a key, too many hooks / props, oversized effects and components, nested ternaries in JSX, a raw <img> in a Next.js app — plus general TypeScript smells (explicit any, empty catch, console.log, long files).
  • C#, on open and save via the Roslyn sidecar: constructor over-injection, empty catch and catch-all-without-rethrow, async void outside event handlers, long methods, large classes, deep nesting, magic numbers in comparisons, public mutable fields, several top-level types in one file.

The severity budget is deliberate: runtime traps are warnings, structure worth a look is information, taste is a faded hint — so the noise never drowns the signal. refactory.smells.react / refactory.smells.csharp turn each half off, and the smells section of .refactory.json disables individual rules or moves thresholds per repository. The full catalogue — every rule id, severity and threshold — is in docs/SMELLS.md.

Refactory: Code Health Dashboard aggregates everything into one themed panel — grouped by file or by rule, sorted by severity or by score, click to jump — with a status bar counter for the active file (✓ when clean). Scan workspace sweeps every React/TS and C# file off disk without opening anything. The dashboard has eight themes: Auto follows your VS Code theme; Nocturne, Abyss, Forest, Orchid, Amber, Ember and Daylight are its own.

Code Health dashboard


The three numbers that say what is worth fixing

A list of smells says what is wrong. It does not say whether you are winning, or which mess actually costs you anything. Three measurements answer that.

Complexity and a health score. Cyclomatic complexity per function, method, constructor, local function and accessor — counted identically in both engines, so a mixed repo's numbers are comparable. Anonymous callbacks fold into the function containing them (a component's complexity includes its inline handlers); a named nested function is its own unit. Each file gets a score, 100 − (10·warnings + 3·infos + 1·hints + complexity overflow), graded A–F and shown beside its name, with a Most complex functions list across the workspace. ts.complexFunction / cs.complexFunction squiggle above the threshold (default 15, tunable per repo).

Trends. Every completed Scan workspace records a snapshot — totals, per-rule counts, average and worst score, worst ten files. The dashboard draws one bar per scan and the per-rule delta since the last one (+3 cs.magicNumber, −2 react.indexAsKey), so "is this getting better" has an answer. Last 100 scans are kept; Refactory: Code Health: Clear Scan History starts over.

Hotspots. One git log for the whole repository, joined against the health score: 100 · ln(1+commits)/ln(1+max) · (100−score)/100. A complex file nobody touches is not the emergency a complex file touched weekly is, and this is the section that tells them apart. Not a git repository? The section is simply absent, never an error. Tune the window with a hotspots block in .refactory.json — see docs/HOTSPOTS.md.

✦ Review on any file or hotspot writes a briefing — metrics, churn, hotspot rank, every finding with its rule id and line — into .refactory/reviews/ and opens your own claude session pointed at it, asking for a prioritised refactoring assessment that respects .refactory.json. Review top 3 briefs the three riskiest files in one session, because "which of these first?" cannot be answered by a session that sees only one. The briefing travels as a file rather than a command-line argument: one backtick in a diagnostic message is enough for PowerShell to execute it. The reviews directory self-ignores, so nothing lands in your commits.

Mechanical smells carry one-key fixes (throw ex;throw;, remove a console.log), and Refactory: Clean Up This File applies all of them at once — one undo reverts the lot.

If the Claude Code CLI is installed, every smell also offers Fix with Claude Code… — it opens a terminal running your own claude session with the smell's exact location, rule and message as the prompt, told to keep the change minimal and obey .refactory.json. You watch the diff land; nothing is applied behind your back.


Draw it: Mermaid diagrams from your actual code

Every section above answers what is wrong or what is worth fixing. This one answers the question people ask first about code they did not write: what does this do, and what does it talk to.

Select some code — or nothing, for the whole file — and pick Diagram This with Claude (AI) from the context menu. Refactory writes a briefing, opens your own claude session pointed at it, and when the diagram comes back it opens rendered, in the same themes as the dashboard, with zoom, pan, Copy, Save SVG and Save PNG.

Want just one method, not the whole file? Three ways in: put the cursor anywhere inside it and pick Diagram This Method (no selection needed — Refactory resolves the enclosing method itself, for a plain function or a const foo = () => {} alike); click the "Diagram this method" CodeLens that sits above every function a language server can see; or select the method by hand, same as ever. Every diagram — method, selection or whole file — now arrives with a plain-language explanation alongside it, spelling out the business logic specifically: what the code guarantees, what it forbids, which numbers are policy rather than accident. A worked example lives in demo/diagram-method/.

The Mermaid diagram panel beside the file it describes

Every node in that flowchart is a statement in the file open beside it — including the dashed edge at the bottom, where the button assigns the literal 'next', which is not any post's id. That is the kind of thing a diagram surfaces and a top-to-bottom read does not.

Kind Answers
Auto let Claude read the code and choose
Flowchart branches, guard clauses, early returns, the error path
Sequence who calls whom, in order, with the awaits and the failures
Class types, inheritance, composition, who owns what
State the machine this code implements — including the state with no way out
ER entities, fields, and real cardinality rather than a guess
Dependency the module graph around this file, and any cycle in it

The briefing carries what Claude cannot cheaply rediscover: the exact selection (quoted, up to 300 lines), the complexity Refactory already measured — so the flowchart does not quietly smooth over the branchiest function in the file — the direct imports it already parsed, and your .refactory.json conventions so the diagram uses your vocabulary instead of inventing new names.

It also carries the Mermaid rules a model writing from memory gets wrong: unquoted () in a label, end as a node id, forty nodes nobody will read. Each of those renders as a red parse error rather than a slightly-wrong picture, which is why they earn more space in the prompt than the diagram's content does.

The handoff is Deep Review's, plus a return path. The prompt travels as a file; the command line stays a fixed sentence whose only variable is a filename this extension sanitised itself; and the one output path both sides agreed on in advance is watched for the answer. Nothing scrapes the terminal, so nothing breaks the first time a session wraps a line. .refactory/diagrams/ self-ignores, exactly like the reviews directory.

A ◇ Diagram button sits beside ✦ Review on every file in the Code Health dashboard — the worst file in the workspace is one click from a picture of what it does.

Refactory: Preview Mermaid Diagram renders what you are already looking at — a .mmd file, a selection, or a fenced ```mermaid block — with no AI round trip at all.

Mermaid ships inside the VSIX. The panel runs under default-src 'none' and loads the renderer from media/vendor/, so diagrams work offline, behind a corporate proxy and in an air-gapped checkout; an integration test asserts the bundled build contains no eval and no dynamic import, because either would fail silently inside that CSP. Labels are drawn as real SVG text rather than <foreignObject> HTML — which is what makes Save PNG work at all, and what makes a saved SVG look the same wherever it is opened.

The whole contract — what the briefing promises, what Claude must write back, what the panel will render, and how to verify the renderer headlessly — is in docs/DIAGRAMS.md.


Thirteen more languages, one lexer

Open a .py, .go, .rs, .java, .kt, .php, .c, .cpp, .m, .dart, .swift or .scala file and Ctrl+Alt+Shift+T has answers there too:

Invert if condition negate the condition, swap the branches
Merge nested if · Split if condition collapse a pyramid of guards, or open one up to hold an else
Add braces · Remove braces the C-family ones only
Introduce variable · Inline variable the name edited in place, both occurrences linked
Convert to an interpolated string "a " + x$"a {x}" · "a $x" · "a \(x)", whichever your language writes
Apply De Morgan's law !(a && b)!a || !b, both directions
Convert to an f-string Python's "…".format(x) and "…" % x
Replace if with ?: two returns or two assignments become one expression — Python's a if c else b, Rust's and Kotlin's if-expression

Every one of these is one lexical pass over the buffer. No compiler, no language server, no project load — which is the only reason thirteen languages are affordable at all, and also the hard limit: nothing here answers a question that needs a symbol resolved.

Why a lexer is enough — and where it is not

The engine builds a mask that classifies every character as code, string or comment, then searches only the code. That single decision is the whole safety story:

# if x: pass          <- a comment
s = "if y: pass"      # <- a string
if z is None:         # <- the only `if` here

A refactoring that matched raw text would eventually rewrite the inside of a string literal. That is not a worse suggestion; it is a corrupted file that still compiles and passes review. The mask handles Python's triple quotes and f/r/b prefixes, Go's raw strings, C#'s verbatim @"…""…", Rust's 'lifetime versus 'c', PHP's # comments, Objective-C's @"…", and JavaScript's template literals — each of which is a case where the naive answer is silently wrong. Rust and Scala nest block comments, so an inner opener does not let the first closer end the outer one; treating them like C's leaves a dangling closer that reads as code and switches the whole file off.

Where the lexer cannot be sure, the refactoring refuses and says so:

  • An unterminated string or an unbalanced brace stops everything. Past that point the mask is a guess, and every offset this engine produces comes from the mask.
  • Ruby is absent on purpose. Heredocs, %w[] literals and parenthesis-less calls make its lexical structure genuinely ambiguous without a parser. A mask that is wrong one time in a hundred is worse than no support.
  • if (a < b) inverted to if (a >= b) is reported when the operands look floating-point. Both comparisons are false for NaN, so flipping the operator and swapping the branches sends a NaN the other way. Every market tool flips regardless; this one flips too, and shows you a preview when it matters.
  • Braces are not removed from a declaration, or where an else would re-bind. if (a) int x = 1; does not compile, and if (a) if (b) x(); else y(); compiles as something else entirely.
  • "%d" % value is not converted to an f-string. "%d" % 3.7 is "3"; f"{3.7}" is "3.7". Only %s and %r survive the trip unchanged.

What it does NOT offer, and why

Introduce variable and Convert to an interpolated string are withheld for TypeScript and JavaScript, because VS Code's own Extract to constant and Convert to template string are already there and are type-aware in ways a lexer cannot be. Same rule as everywhere else in this extension: a second, worse copy of a refactoring the editor already has is a downgrade, not a feature.


What ships today

React / TypeScript — and JavaScript

Every one of these now serves .js, .jsx, .mjs and .cjs as well as .ts/.tsx. It is the same parser either way; what changes is that generated code carries no type annotation, so a .jsx file gets function Badge({ label }) { and no interface BadgeProps. Emitting one would not be a degraded result — it would be a file that does not parse.

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

Python · Go · Java · Kotlin · Rust · PHP · C · C++ · Objective-C · Dart · Swift · Scala

Refactoring Where
Invert if condition every language
Merge nested if · Split if condition every language
Add braces · Remove braces the brace languages
Introduce variable · Inline variable every language except TS/JS, where the editor has one
Convert to an interpolated string Python, C#, Kotlin, PHP, Dart, Swift
Apply De Morgan's law every language
Convert .format() / % to an f-string Python
Replace if with a conditional expression everywhere except Go, which has none

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

Inspection layer (since 0.0.42)

Feature Scope
36 code-smell rules — 18 for React/Next.js/TypeScript, 18 for C# (the catalogue) live for TS, open/save for C#
Code Health dashboard — 8 themes, by-file/by-rule grouping, severity filters, Copy-report whole workspace via Scan workspace
One-key mechanical fixes, Clean Up This File, Fix with Claude Code… quick-fix menu

Diagrams (since 0.0.45)

Feature Scope
Diagram This with Claude (AI) — 7 diagram kinds, briefed with the metrics and imports already parsed selection, or whole file
Diagram This Method with Claude (AI) — right-click, or the "Diagram this method" CodeLens above every function one method, resolved automatically
Every diagram ships with a plain-language explanation, business logic called out specifically any diagram
Themed Mermaid panel — zoom, pan, Copy, Save SVG, Save PNG; renderer vendored, no network any diagram
Preview Mermaid Diagram.mmd, a selection, or a fenced block no AI needed

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.

Within a file the same discipline applies. provideCodeActions fires on every cursor move and runs every provider we have, so each engine analyses the buffer once per keystroke, not once per refactoring — a cache keyed by the file's full text, which means editing simply misses rather than serving an analysis of text that is no longer there. A nine-provider pass over a 1,400-line .tsx used to parse it nine times before deciding most of them had nothing to offer, and the polyglot engine masked a file once per provider for the same reason. Both are now one, and a test says so for each.

The second of those was found by a performance test rather than by reading the code, which is the argument for having one: the budget it asserts is two orders of magnitude loose, and eight scans of a 100,000-character file still showed up against it.

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.


Using it

Install it from the Marketplace (ext install mark-ku.refactory), or build the VSIX yourself (.\pack.ps1, or code --install-extension refactory-*.vsix). Reload the window and open a file in any of the eighteen languages. Three ways in:

Ctrl+Alt+Shift+T Refactor This — ours grouped under Refactory, the editor's own under Built-in refactorings
Ctrl+. the native menu; our actions nest into the groups they belong to
Ctrl+Shift+PRefactory commands that operate on a whole file rather than a caret

Where to put the caret

Nothing is offered "just in case", so position is what makes an action appear. When one is genuinely unavailable it still says why — see When nothing is offered below.

React / TypeScript

To get Select / place the caret on
Convert an import to alias or relative form the module specifier string itself ('../../data/posts') — needs paths in the governing tsconfig
Convert every import in the file Command Palette → Refactory: TypeScript: Convert Imports to…. Deliberately absent from the lightbulb: a file-wide rewrite offered on every cursor move is noise
Extract JSX into a component, same file or new file a complete JSX region, selected
Extract a custom hook contiguous statements including at least one hook call
const X: FC<Props> → function declaration the component's declaration
Wrap JSX in a fragment / conditional / .map a complete JSX region, selected
Inline variable a const declaration, or one of its references
Move to its own file · Safe delete · Rename prop the name being declared (for a prop: the property in the props type)
'use client' fixes nothing — the diagnostics appear in Problems, apply with Ctrl+.

C#

All seven appear together on an explicit invoke, and never on the automatic lightbulb. Whether the caret is inside a class is a question only Roslyn can answer, and spawning a .NET process on every cursor move to ask it would be indefensible — so the offer list is cheap and the real answer, including a readable refusal, comes when you pick one.

Needs a .NET 8 runtime. If none is found, only the C# half switches off; the TypeScript half is unaffected, and a TypeScript-only session never starts the sidecar at all.

Python · Go · Java · Kotlin · Rust · PHP · C · C++ · Objective-C · Dart · Swift · Scala

To get Select / place the caret on
Invert if · Merge nested if · Split if · Add/Remove braces · De Morgan · Replace if with ?: the if keyword or its condition — never merely somewhere in the body, or four nested ifs would all offer at once
Introduce variable a selected expression
Inline variable the declaration line of a local
Convert to an interpolated string inside a string literal that is joined to something with + (or . in PHP)
Convert to an f-string inside the string literal of a "…".format(…) or "…" % …

These need nothing installed: no language server, no SDK, no toolchain. Opening a .go file in a bare VS Code with no Go extension still gets all of them.

Kotlin is the one exception, and not for a reason of ours: VS Code registers the language ids for the other fourteen itself, but not kotlin. Until some Kotlin extension is installed a .kt file opens as plain text, and every language-scoped feature in every extension — including this one — correctly declines. Any of them will do; nothing else is needed.

Settings

Setting Default
refactory.preview multiFile when to route edits through the Refactor Preview panel. A plan carrying warnings forces a preview regardless
refactory.keymap none riderStyle adds Ctrl+Alt+M / Ctrl+Alt+V pointing at VS Code's built-in extract refactorings. Off by default because Ctrl+Alt+<letter> is indistinguishable from AltGr+<letter> on non-US layouts, where it types a real character
refactory.smells.react true React/Next.js code-smell hints, live while you type
refactory.smells.csharp true C# code-smell hints via the Roslyn sidecar, on open and save
refactory.dotnetPath "" explicit dotnet for the C# engine
refactory.disabledLanguages [] language ids to stay out of entirely, when another extension already covers one the way you want. resource-scoped, so a multi-root workspace can decide per folder
refactory.developerMode false exposes the Dev: commands

Project conventions — .refactory.json

Conventions are properties of the repository, so they live in a committed file at the workspace root; VS Code settings carry personal preferences only. Everything in it is optional — what is absent is detected.

{
  "version": 1,
  "typescript": {
    "components": { "declaration": "exportDefaultFunction", "propsStyle": "interfaceSuffixProps" },
    "hooks": {
      // First match wins, so put the specific paths above the catch-all.
      "location": [
        { "when": "src/components/admin/**", "dir": "src/components/admin/_hooks" },
        { "when": "**", "dir": "src/hooks" }
      ],
      // The one value worth settling by hand: a repo with genuinely mixed hook
      // filenames gives the tool nothing to infer from, so it asks every time
      // rather than quietly flipping a coin.
      "fileNaming": "camelCase"
    }
  },
  "exclude": ["**/node_modules/**", "**/.next/**"],
  "smells": {
    // Rule ids from docs/SMELLS.md. Thresholds move where the repo has made a
    // different honest choice; disable is for rules the team has ruled out.
    "disable": ["cs.todoComment"],
    "thresholds": { "componentLines": 200, "ctorDependencies": 6 }
  }
}

This file is read once, at the workspace root — not walked up per-file the way tsconfig aliases are, because thresholds are a property of the repository, not of any one folder inside it.

Every threshold smells.thresholds can move, and which rule each one drives:

Key Default Language Drives
componentLines 150 React react.longComponent
propsCount 8 React react.tooManyProps
hookCount 8 React react.tooManyHooks
jsxDepth 6 React react.deepJsx
effectLines 30 React react.largeEffect
fileLines 400 TypeScript ts.longFile
methodLines 60 C# cs.longMethod
parameterCount 5 C# cs.tooManyParameters
ctorDependencies 5 C# cs.ctorOverInjection
nestingDepth 4 C# cs.deepNesting
classMembers 25 C# cs.largeClass
classLines 500 C# cs.largeClass
complexity 15 both ts.complexFunction, cs.complexFunction, and the health score

complexity is the one key with no language behind it, on purpose: the number also feeds the health score, so a mixed repo needs one shared scale rather than a C# ruler and a React ruler that happen to use the same word.

hotspots, alongside smells, tunes the churn×complexity ranking (docs/HOTSPOTS.md):

{
  "hotspots": { "windowDays": 90, "maxCommits": 5000, "top": 10 }
}
Key Default Means
windowDays 90 how far back git log looks
maxCommits 5000 upper bound on commits examined, for repositories with very long histories
top 10 how many rows the Hotspots list shows

Import aliases are not configured here — they are read from the tsconfig that governs each file, which in a monorepo is not the same one for every file.

Copy a full example: Next.js · layered C#. A .refactorforge.json left over from before the rename is still honoured when no .refactory.json is present.

When nothing is offered

  1. Set "refactory.developerMode": true.

  2. Ctrl+Shift+PRefactory: Dev: Why Is Nothing Offered Here? — reports which tsconfig governs the file, which aliases were derived, and what the engine saw at the caret. This is the most common question a refactoring tool has to answer, so it answers it structurally rather than by guesswork.

    For the polyglot languages it answers the three boring causes first, because they are the real ones far more often than a missing feature is: is the language known (supported), does the file lex (lexable — one unterminated string earlier in the file disables everything after it), and is the caret in code rather than inside a string or a comment (regionAtCaret). It also lists what was deliberately left to the editor's own refactorings (deferredToEditor).

  3. Still puzzling: the Refactory output channel has the trace.

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 concurrencyWorkspaceEdit 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.

The sweep

Example tests encode what someone thought of. For the polyglot engine there is also a property test that encodes what nobody thought of: every caret position and every short selection in a realistic file, in every language, through every offer — and each offer that fires through its execute. Over a hundred thousand offer calls per pass, run three times — LF, CRLF, and a sample written in Traditional Chinese with emoji — asserting that

  • nothing throws (an error toast while someone is typing is worse than silence),
  • no edit ever begins partway through a string or a comment — a whole literal is a fine thing to hoist into a variable, half of one is a rewritten sentence,
  • no two edits in a plan overlap and every span is inside the file,
  • and an available plan is never empty.

It earned its place on the first run, by finding a real bug: Introduce variable accepted a selection lying inside a string literal, because its "is this an expression" check skipped non-code characters instead of refusing on them. The regression test for that case is written out longhand next to it, because a sweep tells you that something broke and an example tells you what.

See docs/M0-spike-report.md for the editor capabilities this rests on and how each was verified.


Layout

docs/ARCHITECTURE.md has the whole picture: the three engines, the tier system, the offer/execute split, and what each package is forbidden from doing.

Path Role
packages/extension VS Code host. The only package allowed to import vscode.
packages/core-ts TS/React engine, on TypeScript's own parser. Must not import vscode or touch the filesystem — enforced by a test, not a convention.
packages/core-lang Polyglot engine: one masked lexer, one profile per language. Must not import vscode, the filesystem, or a parser — all three enforced by tests, because the moment it imports one the next nine languages stop being free.
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.
scripts/gif.ps1 Films the GIFs above from a real editor.

Adding a language

A language is a profile, not a package: comment delimiters, string rules, brace-or-indent blocks, how a local is declared, how a condition is negated. See packages/core-lang/src/profiles.ts — the Go entry is 20 lines. Two tests then hold you to it: one lexes a hello-world in every declared language, and one checks that every profile fills in the fields the refactorings actually read. Nothing appears in the editor until SUPPORTED_LANGUAGES and the manifest's activationEvents agree.

Development

npm install
npm run build          # tsc --noEmit + esbuild bundle
npm run test:unit      # vitest, both engines — no editor, ~2 seconds
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.

If test:int suddenly reports that no code action appears anywhere, check the cache before the code: a half-extracted .vscode-test/vscode-*-archive-* leaves a runnable Code.exe beside an almost-empty resources/app/extensions, so VS Code starts with five languages instead of seventy-nine, every fixture opens as plaintext, and every editorLangId guard in this extension correctly declines. Delete the directory and let it download again.

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 and GIFs

.\scripts\screenshots.ps1            # media\screenshots\*.png
.\scripts\screenshots.ps1 -Only 08   # just one scene — the other images stay put
.\scripts\gif.ps1                    # media\demo-*.gif  (needs ffmpeg)

-Only exists because one new feature does not justify re-capturing seven images already in the README: a re-shoot of a scene nobody changed is a diff of PNG noise at best, and a worse composition at worst.

Both drive a real Extension Development Host through marker files rather than synthetic keystrokes, which never fight for focus and never desynchronise. The GIF handshake has one extra beat, because the camera has to be rolling before the refactoring runs: ready-N (posed) → go-N (rolling) → after-N (edit landed). Scenes invoke the refactoring the way the code action does, so what is filmed is the extension's own work.

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. The recorder launches the host with --profile-temp for the same reason in reverse — a throwaway profile keeps the user's sidebars, chat panels and extensions out of the frame without editing their real layout to do it.

License

MIT

Related extensions