Wrappy — Wrap with Widget
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)
)
Features
- 🧩 Wrap with your own widgets — every widget you define in config shows up in the cmd+. menu.
- 🎯 Widget field or builder field —
AppCard(child: …) or ResponsiveBuilder(builder: (context, constraints) => …).
- ⌨️ Tabstops — fill fields like
onTap: $1 right 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.
Requirements
- 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
formatAfterWrap
relies on a registered formatter, which the Dart extension provides.
Install
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.vsix
Adding / managing wrappers
Three 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/builder selector,
a live preview, and a scope (Workspace/User) selector. Edit/Delete from the list,
Save from the form. Changes are written straight to settings.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.json with the schema below (for advanced / bulk edits).
titlePrefix, formatAfterWrap and ignoreWidgets can be edited directly in the Settings UI
(Cmd+, → "wrappy").
Configuration
Add to settings.json (global) or .vscode/settings.json (workspace — overrides global):
{
"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)" }
]
}
Named constructor: put a dot in the widget field: "widget": "AppButton.icon" →
AppButton.icon(child: <widget>). The Add Wrapper wizard accepts this form too.
Editable variant (placeholder)
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 template that contains a tabstop, format is skipped, so add \n yourself for a
multi-line look (as above). insertSnippet aligns the lines to the cursor's indentation.
Wrapper fields
| 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. |
Extension settings
| 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. |
How it works
- 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.wrappers appears 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 with wrappy.ignoreWidgets.
Development
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.
Limitations (v1)
- 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).
Contributing
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).
License
MIT