Last PL — VS Code Extension
Last PL (last-pl) — Web PL/SQL embedded languages.
Native-feeling editing for HTML, CSS, and JavaScript embedded inside Oracle web
PL/SQL — HTP.P, HTF.*, OWA_UTIL.*, APEX_JAVASCRIPT.*, APEX_CSS.*, and
/*html*/ / /*css*/ / /*js*/ tagged string literals. Highlighting,
IntelliSense, diagnostics (HTMLHint + vscode-css-languageservice + ESLint), and
formatting (structure-aware PL/SQL re-indentation + Prettier/html/css language
services for the embedded code) — all delivered through one extension.
Quick start
- Install Web PL/SQL Embedded Languages from the marketplace, plus a
PL/SQL grammar provider (this extension rides on whichever you use):
- Oracle SQL Developer Extension for VSCode — language
oracle-sql, or
xyz.plsql-language — language plsql.
If you run both, pick one to own your PL/SQL files (they both claim .pks /
.sql / …): disable the other, or set files.associations. This extension
layers onto whichever owns the file.
- Open any
.pks / .pkb / .sql / .pls etc. file containing HTP.P('<div>…</div>')
or similar — the embedded HTML/CSS/JS is highlighted, validated, and
formattable immediately.
- Status bar (bottom-right) shows
$(globe) PL/SQL: N regions when an
embedded-content file is active. Click it to list every detected region.
- Cmd-Shift-P → "PL/SQL: Diagnose Setup" is the troubleshooting entry
point — confirms the host grammar is installed, lists regions, dumps
resolved diagnostics config.
What works today
Grammar injection into source.plsql, source.plsql.oracle, and
source.oracle-sql. No host PL/SQL extension is replaced — this one rides
alongside yours. Works with xyz.plsql-language (language plsql) and the
Oracle SQL Developer Extension (language oracle-sql); diagnostics are
grammar-agnostic and run on either.
Automatic HTML highlighting inside any call of the shape
HTP.<anything>(...), HTF.<anything>(...), or OWA_UTIL.<anything>(...).
JS is extracted from APEX_JAVASCRIPT.* calls and CSS from
APEX_CSS.* calls.
String arguments of any form — regular '...' or q-quoted
q'[...]', q'{...}', q'(...)', q'<...>', q'!...!', q'|...|' — are
tokenized as HTML and get nested CSS (<style>) and JS (<script>)
highlighting for free via the built-in HTML grammar.
Tagged-comment escape hatch for strings outside call context:
v_fragment := /*html*/ '<div class="card">...</div>';
v_style := /*css*/ q'{ .card { padding: 1rem; } }';
v_script := /*js*/ q'< const x = 1; console.log(x); >';
The tag must immediately precede the string. javascript is accepted as an
alias for js.
Native-feeling editing inside embedded regions (v0.2 → v0.15).
Requests are forwarded to VS Code's built-in HTML/CSS/JS language services
via virtual documents. The forwarded provider list as of v0.15 includes:
- Completion, hover, definition, type-definition, declaration,
implementation, references, document highlights, signature help.
- Document symbols (outline view), folding ranges, document links,
selection range (smart-expand), linked editing range
(auto-rename matching tag).
- Color picker / gutter swatches via document-color +
color-presentation providers.
- Auto-close matching HTML tag: type
<section> and </section> is
inserted automatically. Void elements (br, img, etc.) skipped.
- Emmet abbreviation expansion via
@vscode/emmet-helper —
type div.card>p#hello in an HTML region and Ctrl-Space (or Tab on
accept) expands it to the full HTML.
Diagnostics (v0.3). The extension runs three validators per file, debounced
at 300ms, and publishes real red/yellow squiggles on the PL/SQL source:
- CSS via
vscode-css-languageservice — unknown properties, parse errors,
vendor-prefix checks, duplicate properties, etc.
- HTML via
HTMLHint — tag-case, attribute-case, attribute quotes,
duplicate id, empty src, etc. Fragment-hostile rules like tag-pair
and doctype-first are off by default (enable them in settings if your
strings contain whole documents).
- JS via either
acorn (fast syntax-only, default) or eslint — set
plsqlWebEmbedded.diagnostics.jsLinter to eslint to get semantic rules
(no-const-assign, no-dupe-keys, no-unreachable, use-isnan, etc.)
on top of syntax errors. Set to off to disable JS validation entirely.
Customise the rule set with plsqlWebEmbedded.diagnostics.eslintRules
(e.g. { "no-undef": "error", "no-console": "warn" }) and declare
workspace globals with plsqlWebEmbedded.diagnostics.eslintGlobals
(e.g. { "apex": "readonly", "jQuery": "readonly" }) so no-undef
doesn't false-positive on framework names. Trusted workspaces have
their ESLint config auto-loaded. Flat config
(eslint.config.{js,mjs,cjs}) is fully supported; legacy formats
(.eslintrc.json, .eslintrc.yml/.yaml, package.json#eslintConfig)
have their inline rules, globals, and env (via the globals
package) extracted — extends, plugins, and overrides are logged
as unsupported. VS Code settings still win over the workspace config.
Run PL/SQL: Show Loaded Workspace ESLint Config to inspect what was
loaded. Toggle off via
plsqlWebEmbedded.diagnostics.loadWorkspaceEslintConfig.
Each validator is individually toggleable under
plsqlWebEmbedded.diagnostics.{html,css,js} in settings.
"Show Embedded Regions" command (PL/SQL: Show Embedded Regions in the
command palette) that lists every region the scanner detects in the active
file — useful for debugging the heuristics on your own code.
Format Document (Shift-Alt-F) on the whole .pks/.pkb file (v0.16-v0.17,
rebuilt for safety in v0.23):
- Outer PL/SQL is re-indented by the built-in basic-indent engine
(default): structure-aware (BEGIN/END, IF, LOOP, CASE, EXCEPTION,
program-unit IS/AS, multi-line call args) and corruption-proof by design —
it changes leading whitespace only, never tokens, and lines inside
multi-line literals/comments are kept verbatim. The bundled MIT-licensed
sql-formatter is available as an opt-in alternative
(plsqlWebEmbedded.formatter.plsql: "sql-formatter"); it re-flows whole
statements, runs with all string literals masked (so its multi-line
q-quote and 1..10 range bugs can't touch your code), and falls back to
basic-indent when it can't parse the file.
- Embedded HTML / CSS regions are formatted via
vscode-html-languageservice
and vscode-css-languageservice respectively; multi-line results are
indented relative to the literal's own line, and a second format pass is
a byte-level no-op.
- Embedded JS regions are formatted via Prettier's standalone build with
the babel parser. A workspace
.prettierrc{,.json,.yml,.yaml} or
package.json#prettier is auto-discovered (walking up from the document's
directory). Per-workspace overrides are layered on top via
plsqlWebEmbedded.formatter.prettierConfigOverrides. Each layer is
individually toggleable under plsqlWebEmbedded.formatter.{plsql,embedded,js}.
- Safety rails: in regular
'…' literals the '' escape is unescaped
before and re-escaped after formatting (so even singleQuote: true
Prettier output can't terminate the literal); fragments of
'…' || var || '…' concatenations are skipped (reformatting one fragment
would change the emitted page, e.g. trim the significant trailing space
in '<input type="text" '); and a q-quote region is reverted if
formatting would introduce its close sequence.
APEX_JAVASCRIPT / APEX_CSS grammar injection (v0.18.2). String args of
APEX_JAVASCRIPT.* calls are colorized as JS and APEX_CSS.* as CSS by the
TextMate grammar — same path as HTP.* / HTF.* / OWA_UTIL.* already had.
Status bar item (v0.18.0). Bottom-right shows $(globe) PL/SQL: N regions
when an embedded-content file is active; click it to invoke
PL/SQL: Show Embedded Regions.
"PL/SQL: Diagnose Setup" command (v0.18.0). Single-shot troubleshooter:
prints active editor, language ID, detect options, resolved diagnostics
config, host-grammar (xyz.plsql-language) install status, and region count
for the active doc. First thing to run if the extension seems silent.
First-error toast (v0.18.0). The first time any subsystem (formatter,
diagnostics, virtual-doc, eslint-config) logs something matching
failed | threw | rejected | error, a one-shot warning toast appears with
an "Open Output" button — so silent failures can't hide in the channel.
Auto-close < and > (v0.18.0, fixed v0.18.3). Typing < inside an
HTML region inserts the matching > for an empty pair; typing > after
<tagname … inserts the matching </tagname>. Void elements (br, img,
input, …) are skipped, as are self-closing /> tags.
Known lexer limitations
These are deliberately not fixed — each has a one-line reason:
- PL/SQL
'' escapes inside single-quoted strings aren't decoded.
HTP.P('<input type=''search''>') puts a literal ' in Oracle's runtime
string but the extension extracts the embedded HTML region by raw byte
offset and feeds HTMLHint type=''search'' (empty value followed by
garbage), which desyncs its parser and silently drops attribute-level
diagnostics on that tag. Workaround: use a q-quoted literal —
HTP.P(q'[<input type='search'>]') — when the HTML contains apostrophes.
- CASE / NVL / DECODE arguments aren't treated as chain members.
HTP.P(CASE WHEN x THEN '<b>a</b>' ELSE '<i>b</i>' END) — both branches get
recognized as HTML regions but share a chainId; validators may flag them as
one stream. Fixing needs semantic awareness of conditional PL/SQL constructs.
- Literals inside nested non-HTML-emitting calls aren't extracted.
HTP.P(DECODE(x, 1, '<b>a</b>')) — the literal is at parenDepth > 0 and
skipped. Correct for most nested calls (URLENCODE(s) shouldn't treat s
as HTML) but wrong for HTML-returning ones. Distinguishing them requires
hardcoded package knowledge.
- Multibyte (emoji/surrogate-pair) q-quote delimiters aren't recognized.
Oracle accepts
q'😀...😀'; we read only the high surrogate. Rare in
practice.
- Quoted identifiers
"HTP"."P"(...) aren't matched. Legal Oracle,
vanishingly rare in web PL/SQL.
- Apostrophe / whitespace q-quote delimiters are accepted by our scanner
but rejected by Oracle (
q'''...''', q' ... '). Minor spec-drift.
- Q-quote symmetric-delimiter early-close —
q'#foo'#' bar#' closes at
the first #'. Oracle documents this as a user footgun.
npm install triggers the ESLint config file-watcher (it matches any
package.json), causing churn. Cosmetic; config loads idempotent.
- Grammar over-colorizes APEX_JAVASCRIPT / APEX_CSS args after the first.
The region scanner correctly restricts these calls to the first arg only
(subsequent args like
p_key => 'my-snippet-key' are plain strings, not
code) — so diagnostics fire only on the first arg. The TextMate grammar,
added in v0.18.2, doesn't model arg position and colorizes ALL string args
as the embedded language. Cosmetic only — diagnostics are correct.
What does not work yet
- Legacy
.eslintrc.js / .cjs aren't executed. JS config files with
module.exports or export default need dynamic module resolution and
often reference require('@eslint-*/...') presets we can't resolve
without the workspace's ESLint. Stick to JSON or YAML for legacy.
extends / plugins / overrides in legacy configs are ignored.
Preset rule sets (eslint:recommended, airbnb-base, etc.) won't
apply. Migrate inline or to flat config.
- Workspace-loaded configs with
files patterns are ignored. Entries
that target specific globs aren't applied (we don't know the "real path"
of PL/SQL-embedded JS fragments). Use a globally-applicable entry with
no files to reach our validator.
- Plugin-based flat configs may fail to load if the plugin packages
aren't installed in the workspace's
node_modules. Check the PL/SQL
Embedded output channel for import errors.
- No type-level JS checking. The ESLint integration uses the built-in
rule set — no TypeScript typecheck, no user-provided
.eslintrc is loaded.
Rule set is curated for fragment-friendliness.
|| chaining only follows tagged comments or HTP/HTF/OWA call context.
A bare v := '<a>' || var || '</a>'; with no tag still validates each
string in isolation. Prefix with /*html*/ to opt into chain stitching. '<div>' || var || '</div>' is tokenized as two separate HTML strings plus a PL/SQL
concatenation. That is correct for highlighting but insufficient for parsing
the concatenated result as a single HTML document. The tagged-comment tag
applies to the string it precedes, not to the whole concatenation chain.
- Nested function calls inside
HTP.P(...) can close the region early. The
grammar's end: \) is not bracket-balanced. Real code that does
HTP.P(build_fragment(x, y)) will stop HTML treatment at the first ). Use
q-quoted literals for anything with inner parens, or use the tagged-comment
form outside the call.
- No real PL/SQL parser. Region detection is heuristic: a tokenizer that
knows about strings, line comments, block comments, and
HTP./HTF./
OWA_UTIL. call syntax.
Roadmap
The research that preceded this scaffolding identified four rough tiers beyond
v0.1:
| Tier |
Capability |
Effort |
| v0.1 |
Highlighting via grammar injection (shipped) |
— |
| v0.2 |
Virtual-document pull-forwarding: completions, hover, go-to-definition inside embedded regions, backed by the built-in HTML/CSS/JS language services (shipped) |
— |
| v0.3 |
Diagnostics via vscode-css-languageservice, HTMLHint, and acorn, with offset mapping back to the PL/SQL document (shipped) |
— |
| v0.4 |
Chain-aware stitching across '…' \|\| expr \|\| '…' inside HTP/HTF/OWA_UTIL calls (shipped) |
— |
| v0.4.1 |
Tagged-comment chain propagation: v := /*html*/ '<a>' \|\| x \|\| '</a>' stitches across the \|\| chain starting from the tagged string (shipped) |
— |
| v0.4.2 |
Diagnostic suppression widened: any diagnostic touching a stitched placeholder char is dropped, killing false positives from placeholder content (shipped) |
— |
| v0.5 |
ESLint via the Node Linter API: semantic rule violations, not just syntax errors (shipped) |
— |
| v0.6 |
Context-aware placeholder characters: 0 digits in CSS value, hex-color, and function-arg positions; x letters elsewhere (shipped) |
— |
| v0.7 |
Proper PL/SQL lexer: top-level string literals are skipped (no false calls from string content), nested HTP/HTF/OWA_UTIL calls get their own chainIds, identifier-skipping replaces regex word-boundary detection (shipped) |
— |
| v0.8 |
User-configurable ESLint rules and globals via VS Code settings (shipped) |
— |
| v0.9 |
Auto-load the workspace's eslint.config.js / .mjs / .cjs flat config; respects workspace trust; watched + reloaded on file change (shipped) |
— |
| v0.10 |
Source-mapped substitution for single-line \|\| gaps (shipped) |
— |
| v0.11 |
Legacy .eslintrc.json / .yml / package.json#eslintConfig with env/globals/rules extraction (shipped) |
— |
| v0.12 |
Arbitrary-length filler for multi-line gaps too (shipped) |
— |
| v0.13 |
Oracle APEX preset (shipped) |
— |
| v0.14 |
Vitest suite (109 cases) + GitHub Actions CI (shipped) |
— |
| v0.15 |
Native-parity sweep — folding, symbols, color picker, links, highlights, signature help, type-def/declaration/impl/refs, linked editing, auto-close tag, Emmet (shipped) |
— |
| v0.16 |
Format Document — sql-formatter for PL/SQL + vscode-html/css-languageservice for embedded regions (shipped) |
— |
| v0.17 |
Prettier (standalone build) for embedded JS regions, with workspace .prettierrc discovery (shipped) |
— |
| v0.18 |
UX shipping pass: extensionDependencies on host grammar, status bar, Diagnose Setup command, first-error toast, auto-close </>, APEX_JAVASCRIPT/CSS grammar injection (shipped v0.18.0–0.18.3) |
— |
| v0.19 |
Marketplace prep: LICENSE, icon, publisher verification, CHANGELOG |
~1 day |
| v0.20 |
Code actions / quick fixes; rename across virtual URIs |
~1 week |
The region scanner in src/regionScanner.ts is the kernel of all three
follow-on tiers: it already produces (language, kind, contentStart, contentEnd)
tuples that the virtual-document provider (v0.2) and diagnostic mapper (v0.3)
will consume directly.
Prior art
JMAResources.plsql-embedded-languages — closest existing extension; covers
highlighting only, does not handle || concatenation.
Tobermory.es6-string-html / bierner.comment-tagged-templates — source of
the tagged-comment convention used here.
microsoft/typescript-lit-html-plugin — canonical interpolation-substitution
pattern we plan to adopt for v0.4.
microsoft/vscode-html-languageservice, vscode-css-languageservice — the
services we plan to embed for v0.3 diagnostics.
Development
npm install
npm run typecheck # tsc --noEmit
npm test # vitest run (68 cases)
npm run build # esbuild bundle → out/extension.js
CI runs typecheck + test + build on Node 20 and 22 on every push and PR
(.github/workflows/ci.yml), and uploads the packaged .vsix as an artifact.
Open the repo in VS Code and press F5 to launch an Extension Development Host.
Open samples/dashboard.pks in the host window — you should see HTML, CSS, and
JS correctly coloured inside the PL/SQL.
The sample exercises every embedding case the grammar covers. If highlighting
looks wrong on real code, paste a fragment into samples/dashboard.pks so the
grammar can be regression-tested against it.
| |