Mr. Henry's Frontend IDE
Cross-language IDE tooling for server-rendered Twig, CSS and
JavaScript/TypeScript projects. The extension builds an index of each
website in the workspace and connects the languages to each other: a CSS class
used in a .twig template resolves to its rule, a custom element to its
customElements.define registration, an include to the template it names.
- Publisher / id:
mrhenry / mrh-vscode-frontend
- Display name: Mr. Henry's Frontend IDE
- Requires VS Code
^1.100.0
- Works in untrusted workspaces
Quick start
Declare where the websites live, then let the extension discover the rest from
convention:
{
"frontend.websiteGlobs": ["wp-content/themes/*", "sites/*"],
"frontend.activeTheme": { "my-theme": "default" }
}
Each glob is relative to a workspace folder and matches a website's root
directory (a directory that contains a package.json or composer.json).
The website name is the matched directory name.
How a website is resolved
For every discovered website the extension resolves:
- Twig roots —
views, templates and blocks when present, otherwise
views. Every .twig file beneath them is indexed. .twig files outside the
roots are not served by the Twig features.
- Active theme —
frontend.activeTheme[<website>], falling back to
default. Only that theme's bundle is built and indexed.
- CSS / JS entrypoints — from
frontend.overrides[<website>].themes[<theme>]
when configured, otherwise from convention paths:
- CSS:
private/assets/css/application.css, …/application.pcss,
templates/_partials/style.css
- JS:
private/assets/js/app.js, …/app.ts,
templates/_partials/script.js, …/script.ts
Configuration
All settings live under frontend. and are read per resource (a website can
configure its own behavior through folder settings).
Workspace & websites
| Setting |
Type |
Default |
Description |
frontend.enabled |
boolean |
true |
Master switch for all cross-language tooling. |
frontend.websiteGlobs |
string[] |
[] |
Globs, relative to a workspace folder, matching each website root. Example: wp-content/themes/*, projects/*. |
frontend.activeTheme |
object |
{} |
Active theme per website, keyed by website name. A website without an entry uses default. |
frontend.overrides |
object |
{} |
Per-website overrides keyed by website name. Each value may set twigRoots and a themes map of theme name to { css, js } entrypoint globs, resolved relative to the website root. |
Example override:
{
"frontend.overrides": {
"my-theme": {
"twigRoots": ["views", "components"],
"themes": {
"default": {
"css": ["private/assets/css/application.css"],
"js": ["private/assets/js/app.js"]
}
}
}
}
}
Diagnostics
| Setting |
Type |
Default |
Description |
frontend.diagnostics.unknownClasses |
boolean |
false |
Report classes used in Twig but not defined in the active CSS bundle. Off by default because dynamic and externally-provided classes are common. |
frontend.diagnostics.unknownCustomElements |
boolean |
true |
Report custom elements used in Twig but not registered in the active JS bundle. |
frontend.diagnostics.allowedUnresolvedTemplates |
string[] |
[] |
Requested template paths (e.g. partials/missing.twig) for which the unresolved-template warning is suppressed. |
frontend.diagnostics.allowedUnregisteredCustomElements |
string[] |
[] |
Custom element names (e.g. my-widget) for which the unregistered-custom-element warning is suppressed. |
The unresolved-template and unregistered-custom-element warnings are also
suppressible in place through a quick fix that appends the entry to the matching
setting at the workspace or folder scope.
The in-memory index is bounded; cold website snapshots are evicted
least-recently-used and re-hydrated on demand. Parsed results are additionally
cached on disk between sessions.
| Setting |
Type |
Default |
Description |
frontend.index.maxSnapshots |
number |
64 |
Maximum number of website snapshots kept in memory. |
frontend.index.maxCharacters |
number |
8388608 |
Approximate memory budget for website snapshots, in characters. |
frontend.index.idleTimeout |
number |
900000 |
Grace period (ms) before a website index is released after its last visible file is hidden. Set to 0 to keep indexes until the memory bounds evict them. |
frontend.index.rebuildDebounce |
number |
400 |
Debounce (ms) before an edit triggers a re-index. Only changed files are re-parsed. |
| Setting |
Type |
Default |
Description |
frontend.format.enabled |
boolean |
false |
Format .twig documents through the workspace's prettier and @mrhenry/prettier-twig. The modules are resolved from the workspace root; formatting is a no-op when they are missing. |
Formatting is deliberately isolated from the workspace's regular Prettier
setup: no .prettierrc/.editorconfig is consulted, and the plugin's own
defaults (tabs, print width, trailing commas) apply.
Formatter conflicts. .twig files carry the html language id, so VS
Code resolves the default formatter per language rather than by file pattern.
A workspace/user "[html]".editor.defaultFormatter therefore overrides this
provider, and when another HTML formatter is installed VS Code may show a
formatter-conflict prompt for .twig files. The provider works first-class
when it is the only HTML formatter, or when picked explicitly via Format
Document With…. See ROADMAP.md.
Other
| Setting |
Type |
Default |
Description |
frontend.logLevel |
off | error | warn | info | debug |
info |
Verbosity of the Frontend output channel. |
Commands
| Command |
Title |
Notes |
frontend.refresh |
Frontend: Refresh Index |
Rebuild the website indexes. |
frontend.selectTheme |
Frontend: Select Active Theme |
Sets frontend.activeTheme for the active file's website, preserving the other websites' entries. |
frontend.openSettings |
Frontend: Open Settings |
Opens the settings UI filtered to frontend. |
frontend.convertPixelsToRem |
Frontend: Convert Pixels to Rem |
Rewrites every px dimension intersecting the selection as rem at 1rem = 16px. Bound to F10 in CSS editors and listed in the editor context menu. |
Twig language support
- Syntax highlighting from a bundled, spec-conformant Twig TextMate grammar
(
text.html.twig) covering core tags, filters, functions, tests and
operators, whitespace control (-/~), documentation/inline comments, string
interpolation, numeric separators, spread and verbatim. Embedded
<script>/<style> content reuses the built-in HTML highlighting with
embedded CSS and JavaScript.
- Language configuration for
.twig / .html.twig: Twig {# #} comments,
HTML auto-closing pairs, brackets, word pattern and indentation rules.
- Snippets for tags, filters and functions (e.g.
if, for, set,
include, block, json_encode), offered as completions. A srcset-…
snippet generates responsive srcset steps.
- Hover documentation with examples for Twig tags, filters and functions.
- Quick suggestions inside strings and other contexts are enabled by
default for the
html language, so completions appear while typing (including
inside quoted template paths).
Cross-language features
| Feature |
Languages |
What it does |
| Definition |
Twig, CSS |
A class → its CSS rule; a custom element → its JS class; an include/extends/embed/import/from/use path → its template; a Twig block → its definition; a CSS custom property → its declaration. |
| Hover |
Twig, CSS, JS, TS |
Classes (showing the defining CSS rule), custom elements, templates, CSS custom properties, and Twig blocks/macros/variables. |
| Completion |
Twig, CSS |
CSS classes inside class attributes (multi-line values supported, replacing only the typed prefix), custom elements after <, template paths inside path attributes and include()/source() calls, and CSS custom properties after --. |
| References |
Twig, CSS, JS, TS |
All usages of a class or custom element across languages. |
| Rename |
Twig, CSS, JS, TS |
CSS classes and custom-element tags across files, validating the new name. Renaming a class cascades to its BEM elements/modifiers (block__element, block--modifier). |
| Document symbols |
Twig, CSS, JS, TS |
Outline of the current file. |
| Workspace symbols |
all indexed |
Search symbols across every website. |
| Document highlights |
Twig, CSS |
Highlight occurrences of the symbol under the cursor in the current file. |
| Folding ranges |
Twig |
Collapse Twig blocks and macros. |
| Document links |
Twig |
Ctrl/Cmd-clickable include paths. |
Twig variables
Variables defined with {% set %} (including capture and multi-assignment, and
reads inside block tags) support definition, hover, references,
document highlights and rename, scoped to the declaring template. Object
property access (a.b.c) links back to the root variable without renaming the
property names.
- Twig
{## … ##} documentation attached to blocks and macros (with #} /
symmetric ##} closers and whitespace control).
- JSDoc
/** … */ attached to JavaScript classes and their registered custom
elements.
The captured documentation appears in hover and completion.
Diagnostics
Diagnostics are published by the frontend source and refreshed incrementally
as files change.
| Code |
Severity |
Reported when |
Disabled by default |
unresolved-template |
warning |
A requested template path cannot be resolved. |
no (suppressible per path) |
unknown-class |
information |
A Twig class is not defined in the active CSS bundle. |
yes |
unknown-custom-element |
information |
A custom element is not registered in the active JS bundle. |
no (suppressible per element) |
CSS & the 1rem = 16px ratio
Media query completion — typing @mq in a stylesheet offers an
@mq-<trigger> snippet for each @media rule annotated by a preceding
trigger comment in base/media-queries.css (or .pcss), inserting the
annotated block with a tab stop:
/* from-small,min-width-768px */
@media (width >= 48rem) {
}
Typing -- inside an @media line offers the bundle's @custom-media names.
rem hover — hovering a rem dimension previews its pixel value at
1rem = 16px (e.g. 1.5rem = 24px). Computed from the buffer, so it also
works outside a website.
px → rem — a code action on any px dimension converts it to rem
at full precision (24px → 1.5rem, 10px → 0.625rem). The same
conversion is available through Frontend: Convert Pixels to Rem / F10.
Indexing model
- Indexing runs in a background Node worker thread; the UI thread keeps
in-memory snapshots and talks to the worker over a message protocol.
- Re-indexing is debounced and driven by file watchers, and can be forced with
Frontend: Refresh Index.
- A website is indexed only while one of its files is visible in an editor;
indexes are released after the idle grace period.
- Per-file parse results are cached on disk and rebuilt into snapshots on
startup. Incremental re-indexing follows parse-time dependencies (the JS
module graph and CSS bundle members), so a change only invalidates what
depends on it.
- Watchers cover
.twig, .css (plus .pcss/.scss/.sass/.less/.styl/
.postcss) and the JavaScript/TypeScript extensions.
- An in-editor buffer overrides the on-disk content while it is open. Saving a
document makes the on-disk content authoritative and evicts the cached parse
for that file, even when the buffer did not change; closing the document
drops the override so later disk changes are seen again.
- The CSS bundler resolves
@import members from disk, so an unsaved edit to an
imported stylesheet is reflected once it is saved (the entry itself always
uses its in-editor buffer).
- The language features are shared by the VS Code providers and a
transport-agnostic LSP implementation, so the same tooling can serve other
editors.
Not yet available
- Semantic tokens for Twig class names and custom element tags. The provider
and token types are implemented but currently disabled.
- Inlay hints, call hierarchy for macros/blocks/custom elements, and
code actions that create missing CSS classes, custom elements or templates.
See
ROADMAP.md for the full list.
Development
npm run build # bundle the extension with esbuild
npm run watch # rebuild on change
npm run check-types # tsc --noEmit
npm run lint # eslint
npm run test # unit tests (node --test)
npm run test:integration # VS Code integration tests
| |