Compelem VSCode Extension
A VS Code extension that provides rich editing support for the
compelem component framework,
especially for the HTML-style markup written inside the `h`...` tag
templates of a component's render() function.
Features
1. Syntax Highlighting
- The HTML text inside
`h`...` functions is highlighted.
- The CSS text inside
`css`...` template functions is highlighted.
- All compelem directive functions (
show / ifTrue / ifElse / forEach / when / slot / html / classes / styles / model / bind ...) have individually
configurable colors.
- When the cursor is on a tag, the matching opening/closing tag name is
highlighted — including tags that live inside
${ifElse(...)} /
${forEach(...)} / ${when(...)} branches, where the pair is resolved inside
that branch instead of against the outer <template>.
- In a
.ce file, the <script> section is tokenized as source.ts, so it
matches the equivalent .ts file token for token — decorators (@state,
@tag, …) are colored the same way, and Ctrl+/ inserts a // line comment
instead of an HTML comment.
- In a
.ce file, markup produced by a directive branch — the
() => h`...` inside ${ifElse(...)} / ${forEach(...)} / ${when(...)} —
is highlighted as HTML exactly like the markup that sits directly in
<template>, at any nesting depth. The ${...} parts around it stay
TypeScript.
- Code folding:
- paired HTML tags inside
`h`...` templates (.ts / .tsx) and inside
the <template> body of a .ce SFC;
- in
.ce files, the h`...` bodies nested inside ${...} directive
branches fold too, not just the tags that sit directly in <template>;
- in
.ce files only: every top-level section tag (<template> / <script> /
<style> / <cssvar>) and every multi-line { ... } block inside
<script> and <style>. Braces inside strings, comments, template-literal
interpolations and regular expressions are ignored.
- Toggle with
compelem.folding.enabled (on by default).
- Right-click formatting of the HTML markup template inside
`h`...`
functions is supported.
- Long tags are broken into one attribute per line; short elements
(e.g.
<span>Hello</span>) stay on a single line.
- When you want to keep the built-in TypeScript formatter as the default
document formatter, you can use the
Compelem: Format h... templates`
command.
- The number of indentation spaces can be set via
compelem.format.indentSize
(the default 0 means following the editor's tabSize — the very number the
status bar shows as Spaces: N). A positive value overrides that for
.ce files, so changing the status bar's Spaces no longer affects them;
the extension says so once in the status bar when the two disagree.
- In
.ce files the <script> / <style> / <cssvar> bodies are formatted
with VS Code's own TypeScript / CSS formatters and indented one level
below their section tag, matching <template>. Non-CSS <style> bodies
(e.g. lang="scss") are only re-indented — never handed to the CSS
formatter, which would rewrite their nesting.
- Set
compelem.format.embeddedIndent to flush to keep those bodies at
column 0 instead (the Vue convention). Prefer it when a <script> contains
multi-line template literals, whose leading whitespace is part of the
string. compelem.format.embeddedSections: false disables the pass entirely.
3. IntelliSense & Completion
- Typing
<l- (or a PascalCase prefix) inside `h`...` functions lists
the components registered in the workspace.
- Typing
. on a component tag lists the component's props — including the
properties inherited via extends (e.g. color / size / disabled from a
base class) — and shows their type, default value, and @attrs
documentation.
- In any tag's attribute area, global HTML attributes are suggested:
class / style / name / id / title / hidden / tabindex / role / aria-label / aria-hidden / lang / dir / slot and data-* (accepting inserts class=""
with the caret between the quotes).
- Typing
@ on a tag suggests event names: native tags get their
tag-specific events (<slot> → slotchange, <input> → input / change,
<form> → submit, ...), component tags additionally get their @emits
events. Only the event name is inserted, so modifiers can follow.
- Typing
. after an event suggests event modifiers (self / stop / prevent / once / capture / passive / left / right / middle / ctrl / alt / shift / meta / esc / debounce / throttle), chainable (@click.stop.self)
with used modifiers de-duplicated.
- Typing
= after an event inserts the handler reference
(@click= → @click="${this.onClick}", modifiers never leak into the name).
- Typing an opening tag like
<div> automatically inserts </div> and moves
the cursor inside the element.
- Typing
<xx propname= on a component tag automatically inserts
"${this.value}".
- Typing
show / bind / classes / styles / model / html on any tag suggests a
tag-directive snippet such as ${show(this.visible)}; inside ${ ... },
text directives (forEach / when / ifTrue / ifElse / slot) are suggested
instead.
- Typing
@ outside templates suggests decorators: @computed / @watch / @query / @tag / @prop / @csscope, with auto-completion on acceptance.
- In
.ce files, completion inside <script>, inside a <template>
expression and inside a <cssvar> value is answered by the real TypeScript
language service (request forwarding), so types, methods and inherited members
are offered there — the regex-based this. list is only a fallback for the
cases the bridge cannot serve.
.ce section tags (<style ...>) complete both attribute names and
values. lang offers exactly css / scss / sass (dart-sass compiles the
non-CSS ones at build time; any other value is a build error), and scope
offers inner / host / global as a whitespace-separated list
(scope="host inner" — only the token under the caret is replaced and
already-written values are skipped). <template> / <script> / <cssvar>
take no attributes and get no suggestions. A section tag must start at column
0, matching the parser's own ^<(template|script|style|cssvar) rule, so an
indented <style> is treated as a plain element inside the template and gets
no section attributes.
Values are reachable two ways: (1) accepting lang / scope inserts a
choice snippet, so the accepted values pop up right at the caret; (2)
inside the quotes, Ctrl+Space. Attribute values live inside a string,
and VS Code's default editor.quickSuggestions.strings is "off" — typing
inside a string never requests completion. This extension registers " and
' as trigger characters, so the list also appears the moment the opening
quote is typed.
4. Snippets
render → generates a render() that returns an `h`...` function
template containing a root <div>.
@watch → generates an empty @watch(...) method.
@computed → generates an empty @computed getter.
@csscope → generates an @csscope(Csscope.INNER) static getter template.
5. Diagnostics
- The
ref attribute must be an interpolation of a createRef member, e.g.
ref="${this.xxRef}".
- The
.xx directive attribute is allowed on any component tag — ce-xx
elements, dashed custom-element names (e.g. ce-xx, a-b-c), or
PascalCase components (e.g. <l-input .value="${this.xx}">); .xx on
<slot> is valid.
- The same directive (
show / bind / classes ...) may only appear once per
element.
- Decorator placement:
@csscope only on static getters, @watch only on
methods, @computed only on non-static getters, @emits / @tag only on
classes.
- Event names passed to
this.emit(...) must be declared in the class'
@emits decorator (ancestors are merged along the extends chain; classes
without any @emits on the chain are not checked).
@event="${this.onXxx}" values must be function references (method,
arrow-function field, or function field) — missing members and non-function
members are flagged.
- Shape rules for the
<script> section (a .ce with a <template> must
declare exactly one component class): E-CE-CLASS-COUNT (none, or more than
one), E-CE-CLASS-EXPORT (not exported), E-CE-CLASS-BASE (no extends, or
a base that cannot be traced to CompElem — extends HTMLElement, for
instance). Reported with source compelem-sfc, underlined on class Name.
The rules are the build's own implementation (vite/sfc/src/script-rules.ts
compiled into out/vendor/), so the editor and vite build cannot disagree.
Section detection follows the build too: a section tag must start at column
0, so an indented or single-line <script> does not exist as far as the
build is concerned and is reported as "no component class". A .ce without a
<template> is a plain module and is not checked.
- TypeScript errors in the
<script> section are produced by a private TS
language service and surface with source ts (e.g. TS2322 type mismatch,
TS2300 duplicate identifier). The canonical case: writing class X twice in
one <script> reports TS2300 Duplicate identifier on both declarations.
That is editor-side only; the build reports
The symbol "X" has already been declared from esbuild's TS→JS lowering step,
with the .ce line number.
6. Navigation & hover
- Hold Ctrl and click a component tag (
l-xx / PascalCase) to jump to its
@tag class definition.
- Hold Ctrl and click a
this.xxx reference to jump to that member's
declaration.
- Hold Ctrl and click a CSS class to find its
.class selector: first in the
css / styleSheet / insertStyleSheet template literals of the same file,
then in its <style> section, and finally across
**/*.{scss,css,less,ce} (other .ce components included).
- In
.ce files, three sections are forwarded to VS Code's built-in
TypeScript language service: the <script> section, the expressions inside
<template> (${...} / {...}), and the value expressions of every
<cssvar> entry (accentColor: this.count > 2 ? … : …). Imported classes,
class members and inherited members therefore resolve exactly — a target in
another file jumps there as-is, and a target inside the component class is
mapped back into this file's <script> section. Markup in <template>
(e.g. class="...") is not JS and uses the CSS class lookup above, and so is
a <cssvar> key (only its value is an expression).
- Hovering reuses the same three sections: hovering a symbol shows the same
signature and documentation TypeScript would show in a
.ts file. Markup
gets no tooltip on purpose — there is no regex fallback, a guessed tooltip
being worse than none. (Hovering the @ of a decorator shows nothing in
.ce and in .ts alike: TypeScript answers for identifiers, not punctuation.)
- There is no workspace symbol search fallback — only the navigations above.
Inside an h\`` template, Ctrl+/ normally inserts a JS line comment (//).
This extension already binds Ctrl+/ (Cmd+/ on macOS) to
compelem.toggleComment and Ctrl+Shift+/ (Cmd+Shift+/ on macOS) to
compelem.toggleBlockComment (TypeScript / TypeScript React files only),
toggling HTML comments instead — works out of the box. To override:
{
"key": "ctrl+/",
"command": "compelem.toggleComment",
"when": "editorTextFocus && (editorLangId == typescript || editorLangId == typescriptreact)"
}
- Line comment (
Ctrl+/): with no selection, the caret's line is wrapped as
<!-- ... --> (press again to unwrap). With a selection, each covered
line is wrapped individually (indentation kept); already-commented lines are
unwrapped.
- Block comment (
Ctrl+Shift+/): the whole selection is wrapped as
<!-- ... --> (multi-line selections wrap across lines; press again to
unwrap). With no selection, an empty <!-- --> is inserted.
- Code outside templates keeps VS Code's native
// / /* */ behavior.
License
Copyright (c) 2026 holyhigh2
| |