Customizable "Wrap with widget" code actions for Flutter/Dart.
The Dart/Flutter extension's "Wrap with Column / Container / Center" quick fixes are baked
into the analysis server and cannot be extended. Real projects collect their own widgets in a
shared/ folder (your design system: AppButton, AppCard, AppScaffold …). Wrappy adds your
own wrappers to the cmd+. (Quick Fix) menu:
// Cursor on Text → cmd+. → "Wrappy: AppButton" (under the Quick Fix group)
Text('Hello')
// Result:
AppButton(
child: Text('Hello'),
onPressed: ▮, // ← cursor here (tabstop)
)- 🧩 Wrap with your own widgets — every widget you define in config shows up in the cmd+. menu.
- 🎯 Widget field or builder field —
AppCard(child: …)orResponsiveBuilder(builder: (context, constraints) => …). - ⌨️ Tabstops — fill fields like
onTap: $1right after wrapping (Tab to move between them). - 🧠 Smart detection — finds the bounds of the widget under the cursor with a fast, AST-free heuristic (aware of strings / comments / generics / named constructors); picks the innermost widget.
- 🎨 Auto-format — triggers "Format Document" after wrapping so the code is properly indented.
- 🛡️ Resilient — malformed config entries are skipped silently; the extension never crashes.
- VS Code
^1.85.0(also works in OpenVSX-based editors like Cursor / Windsurf). - The Dart-Code extension is recommended — Wrappy works without it, but
formatAfterWraprelies on a registered formatter, which the Dart extension provides.
From the VS Code Marketplace — search for Wrappy in the Extensions view, or run:
ext install mysCod3r.wrappy
Or build and install a local .vsix:
npm install
npm run vsix # produces wrappy-0.0.1.vsix
code --install-extension wrappy-0.0.1.vsixThree ways:
- Side panel (Activity Bar → Wrappy) — recommended: manage all wrappers through a form.
Every field is its own text input (labelled required/optional), a
widget/builderselector, a live preview, and a scope (Workspace/User) selector. Edit/Delete from the list, Save from the form. Changes are written straight tosettings.json. - Command wizard: Command Palette (
Cmd/Ctrl+Shift+P) →Wrappy: Add Wrapper— asks for the widget name, field type (widget/builder), field name, (for builder) the signature, and optional extra arguments (onTap: $1) step by step; writes to the Workspace or User settings.Wrappy: Manage Wrappers— lists the defined wrappers and deletes the selected one.
- Hand-edited JSON — write to
settings.jsonwith the schema below (for advanced / bulk edits).
titlePrefix,formatAfterWrapandignoreWidgetscan be edited directly in the Settings UI (Cmd+, → "wrappy").
Add to settings.json (global) or .vscode/settings.json (workspace — overrides global):
Named constructor: put a dot in the
widgetfield:"widget": "AppButton.icon"→AppButton.icon(child: <widget>). TheAdd Wrapperwizard accepts this form too.
To make a value (e.g. a spacing token AppSpacing.md, or a named-constructor variant) a
pre-selected, editable tabstop, use ${1:default} in a raw template:
{ "widget": "AppPadding", "template": "AppPadding(\n size: ${1:AppSpacing.md},\n child: $WIDGET$,\n)" }After wrapping, AppSpacing.md comes up selected: press Tab/Esc to keep it, or type to change
it. The same works for any field — $1, ${1:default}, $0 (final cursor).
Multi-line output: structured wrappers are produced multi-line automatically. If you use a raw
templatethat contains a tabstop, format is skipped, so add\nyourself for a multi-line look (as above).insertSnippetaligns the lines to the cursor's indentation.
| Field | Required | Default | Description |
|---|---|---|---|
widget |
✓ unless template |
— | Wrapper widget name, e.g. AppButton. |
field |
— | child |
Field the existing widget is placed in, e.g. child, body, title. |
fieldType |
— | widget |
widget or builder. |
builderSignature |
— | (context) |
Signature for fieldType: "builder", e.g. (context, constraints), (context, index). |
snippetSuffix |
— | — | Arguments appended after the target field; may contain a tabstop, e.g. onTap: $1. |
template |
— | — | Raw snippet template; overrides the other fields when present. Must contain $WIDGET$. |
label |
— | widget name |
Label shown in the menu. |
| Setting | Default | Description |
|---|---|---|
wrappy.titlePrefix |
"Wrappy: " |
Prefix added before menu labels (e.g. Wrappy: AppButton). |
wrappy.formatAfterWrap |
true |
Trigger "Format Document" after a wrap? (Skipped for tabstop wraps.) |
wrappy.ignoreWidgets |
[] |
Wrap is not offered when the cursor is on one of these. Common non-widget types (EdgeInsets, TextStyle, Duration, Color, BoxDecoration …) are ignored by default; add your own types here. |
- Put the cursor on the name of the widget you want to wrap (e.g.
Text,Column). Wrappy only offers actions when the cursor is on a widget's name (head) — not between arguments, in whitespace, or after a comma. - cmd+. (Quick Fix) / Ctrl+. on Windows/Linux.
- Each wrapper in
wrappy.wrappersappears with a<titlePrefix><widget>label. - Pick one → the existing widget is placed in the wrapper's field; the cursor lands on a tabstop if any.
Common non-widget constructors (
EdgeInsets,TextStyle,Duration…) are ignored; add your own types withwrappy.ignoreWidgets.
npm install
npm run watch # esbuild watch
# F5 → Extension Development Host (opens the examples/ folder automatically)
npm run test:unit # pure logic (span detection, templates) — fast
npm test # integration (@vscode/test-electron)See ROADMAP.md for the roadmap and decisions.
- Widget detection is heuristic (not an AST); it covers ~95% of cases. You need to put the cursor
on the name of the widget you want to wrap. Without type information, non-widget constructors are
filtered out with a built-in ignore list (+
wrappy.ignoreWidgets). - v1 only wraps. Unwrap, selection ranges and multi-cursor are on the roadmap (v2).
Contributions are welcome! See CONTRIBUTING.md for setup, the dev workflow,
tests and coding conventions. Good first issues: new builder signatures, more default
ignoreWidgets, and the v2 items in ROADMAP.md (unwrap, multi-cursor).

{ "wrappy.titlePrefix": "Wrappy: ", "wrappy.wrappers": [ // --- widget field: "child" (the default) --- { "widget": "AppCard", "field": "child" }, { "widget": "AppContainer", "field": "child" }, // child field + an interactive tabstop you fill after wrapping { "widget": "AppButton", "field": "child", "snippetSuffix": "onPressed: $1" }, // --- a different field name: "body" --- { "widget": "AppScaffold", "field": "body" }, // --- builder field: standalone (context) --- { "widget": "AppBuilder", "field": "builder", "fieldType": "builder", "builderSignature": "(context)" }, // --- builder field: (context, constraints) --- { "widget": "ResponsiveBuilder", "field": "builder", "fieldType": "builder", "builderSignature": "(context, constraints)" }, // --- builder field: (context, index) --- { "widget": "AppListView", "field": "itemBuilder", "fieldType": "builder", "builderSignature": "(context, index)" }, // --- named constructor — use a dot in the widget name --- { "widget": "AppButton.icon", "field": "child", "snippetSuffix": "onPressed: $1" }, // --- raw template (full control) — must contain the $WIDGET$ placeholder --- { "widget": "AppPadding", "template": "AppPadding(\n size: ${1:AppSpacing.md},\n child: $WIDGET$,\n)" } ] }