Streamlord for VS Code
The forge is one thing. The eye that sees the flaw before the blade is quenched is another.
I have both, and I lend them to you for nothing. Ask yourself why.
Sarn the Faceless
Datastar 1.0.4 as a language in your editor. Half of the
Streamlord toolchain, and the half that needs no
part of the other: it reads your HTML and template files whatever answers them, and your Kotlin
strings alongside the Streamlord SDK.
What it does
Diagnostics in Kotlin strings, whether passed to a Streamlord call or free-standing (a
function that returns HTML, a val holding a fragment), and in .html and template files:
- Kotlin interpolation of signals:
"$count++" becomes "++" at runtime. Flagged as an error
with the fix spelled out. In HTML strings, $name inside a data-* expression is a warning,
a hint when name is a number or a boolean, which is safe written in.
- A Kotlin value in an HTML string, judged by where it lands as
interpolate judges it at run time:
in text or a quoted attribute, where escaping saves it, a hint (a warning for a String) with a
fix that rewrites the literal as interpolate(...); at the start of a URL, in style, a data-*
expression, a script or an event handler, a warning, since escaping cannot help there. Constants,
including a const val anywhere in the workspace, literals and markup built in the template say
nothing. A call to interpolate is judged too: a miscounted %s and a value it will refuse are
errors, a Kotlin template in its format a warning with a fix that makes it a %s placeholder.
- Datastar expression syntax, checked with a real JavaScript parser, with the error at the
right column.
- Unknown actions (
@Post), Pro actions and Pro attributes (hint: they need the Pro bundle).
- An empty
data-nonce on <html>, which makes Datastar throw NonceRequired and not start,
is an error; one that is no base64 value, which a browser ignores in the policy, a warning; one
a template fills in says nothing. dataNonce of such a literal, which throws as the page
renders, is an error either way.
- Markup in
patchElements(...): unclosed or stray tags, text outside elements, missing id
on top-level elements when no selector is given, a mode that needs a selector.
data-* attributes: unknown names with "did you mean", unknown modifiers, wrong modifier
arguments (__debounce without a duration, __threshold.abc), missing or unexpected keys,
and a data-indicator on an element that sends no request.
- Casing: a capital letter in a key (
data-signals:fooBar), which the browser lowercases so
the signal becomes $foobar, and a hyphen after a signal in an expression ($foo-bar,
$count-1), which Datastar swallows into one signal name. Both come with the fix: the
kebab-case key (plus __case.camel for data-on and data-class), and the camelCase signal
or the spaced subtraction. In the DSL the SDK writes a camelCase key correctly
on its own, and a hint on the call shows what goes on the wire. A hyphen in a signal name
written in a DSL call, dataSignals("foo-bar", …) or signal("form.first-name"), is an error,
since the SDK refuses it at runtime, with the camelCase fix.
- What Datastar's own rewrite of an expression does that JavaScript would not: a
$name or
@name( in the text of a template literal that also holds a ${...}, or inside a regular
expression, is rewritten there too; a ' in a comment, a regular expression or a template
pairs with a later quote, and the $name between them stays a JavaScript variable; $år is no
signal, since only ASCII follows a $; and @get without its parenthesis is a syntax error.
- Signal names Datastar's client cannot hold or no
$name reaches: __proto__ anywhere in the
name, constructor or another property of Object.prototype with more of the name or an object
under it (data-signals:constructor.x), also once __case has turned the key into it, a $ in
data-bind="$user", and a letter beyond ASCII, where the message names the bracket form,
$['år'], that does reach it. data-signals:constructor="1" is held, and says nothing.
- Values the client evaluates or splits: a filter of
data-json-signals or
data-on-signal-patch-filter that does not compile or reads a signal, a pattern written as a
string with flags after its last slash, '/^user/i', there or in the filterSignals of an
action, where the client cuts one / from each end and matches the /i as text ('/^user/'
works as it is), data-computed without a key
whose value is sure to be no function, and a ; inside brackets in a value that is read, where
the client splits it and loses the value.
A string counts as HTML when it opens with a tag, a comment or a doctype, or when it carries
IntelliJ's injection marker: @Language("HTML") on the function or property, or a
// language=HTML comment just before it. Multi-dollar literals ($$"""...""", Kotlin 2.2+)
are read as Kotlin reads them: a single $ is text, so $count there is a signal and draws
no interpolation warning. Free-standing strings get the attribute checks of a
template file; the id and completeness rules apply only where the string is handed to
patchElements and its kin, because only there is the patch mode known.
Quick fixes on the lightbulb: $count becomes increment("count"), signal("count"),
toggle("open") or an escaped ${'$'}count; __debunce becomes __debounce, __debounce_150ms
becomes __debounce.150ms and __prevent.stop becomes __prevent__stop; @Post becomes
@post; data-signal becomes data-signals; a missing duration, id or selector is added.
Every diagnostic links to the matching page of the Datastar reference.
Snippets for Ktor and Spring routes and the DSL calls in Kotlin, for a function that
returns HTML as a $$"""...""" string (htmlFunction), and for every data-* attribute in
HTML. HTML custom data gives VS Code's own HTML service the Datastar attributes with
documentation, so they complete and hover in .html files like any other attribute.
Template languages: the HTML side (diagnostics, completion, hover) also runs in JTE and
kte, FreeMarker, Velocity, Mustache, Pebble, Twig, Jinja, Django, Razor, PHP, Blade, ERB,
Handlebars, EJS, Liquid, Nunjucks, Edge, Astro, Svelte, Vue and Go templates; Thymeleaf files
are .html and need no entry. The list is the setting streamlord.languages. The engine's own
syntax (${...}, !{...}, {{...}}, @if, #if, <#if>, <@macro>, [#if]) is left
alone: an attribute value that contains it cannot be judged before rendering, and directives
are not elements.
Completions: # and . in any selector argument offer the ids and classes declared
anywhere in the workspace, this file first; $ offers every signal name declared anywhere in the workspace (Kotlin DSL
calls, @Serializable classes, HTML strings and templates); @ offers actions with snippets; data-
offers attributes; __ offers modifiers and their values; in the options object of a backend
action, @get('/x', {|}), the options not yet written; after data-on:, Datastar's own events
(datastar-fetch, datastar-signal-patch) and the DOM's, in any file. Inside an HTML string in
Kotlin the HTML completions apply. A value is read as the browser reads it, so all of this works
over several lines and in single quotes, and in a Kotlin string a \$ or ${'$'} before a signal
stays as written.
Signal navigation: Go to Definition (F12, or ⌘+click on macOS and Ctrl+click elsewhere) on
$count goes to every place the workspace defines it: a data-signals on the page, a
patchSignals in a handler, a property of a signals class. Find All References (⇧F12) lists those
places and every expression that reads the signal, in markup and Kotlin strings alike. Rename
Symbol (F2) rewrites them all, each as its place needs: renaming itemCount to basketCount
writes the key data-signals:basket-count, since the browser lowercases a key. A place that
cannot hold the new name stops the rename with a message before anything is written: a key whose
__case gives no spelling of it back, a key of an object asked to move under another object, a
property of a signals class, which Kotlin's rename renames. In $user.name.length each part is
its own signal, so the caret on user renames user and on name renames user.name.
Signature help (⇧⌘Space on macOS, Ctrl+Shift+Space elsewhere, and on typing ( or ,) inside
an action call: @get(uri, options?), with the argument the caret is on. In Kotlin the help starts
once the DSL call's parenthesis is closed, as the IDE closes it for you.
Hover: documentation for every attribute, modifier, action and option of a backend action,
and for the DSL functions, in HTML files, templates and HTML strings in Kotlin alike.
Syntax highlighting of Datastar expressions and HTML inside the DSL strings and in
free-standing HTML strings, and of Datastar's attributes in HTML, the catalog's and no other
data-* (plugin, key, modifiers and modifier arguments each get their own scope, and the value
of an attribute that takes an expression is highlighted as a Datastar expression). Most themes color only
a few of these scopes, so run Streamlord: Apply recommended colors once to write a palette
into your user settings; Streamlord: Remove recommended colors takes it out again.
Stream Inspector (Streamlord: Open Stream Inspector): connect to a running endpoint with
a method, signals and headers; every SSE event arrives decoded, and the signal store is kept
exactly as the client would keep it.
It asks as Datastar's client does, with the same Accept, and reads what the client reads: a
200 that is not HTML, JSON or JavaScript is decoded as a stream, whatever its Content-Type. Any
other response is shown with its datastar-* headers and up to 1 MiB of its body; a
text/event-stream response with any status but 200 is not read, as the client reads none.
Saved requests live in .streamlord/inspector.json in the workspace (commit it to share
with the team, or ignore it). Save one with Save, and pick it again from the Request
list, which also holds the recent ones and says how many of each it has. Delete removes
the selected one.
Recent requests are remembered automatically, and the last one is prefilled on open.
Variables live in .streamlord/env.json in the workspace: three with a field of their own,
and the params of your routes:
{
"baseUrl": "http://127.0.0.1:8081",
"signals": { "search": "ash" },
"headers": { "Authorization": "Bearer dev-token" },
"params": { "partsnummer": "3000507723", "instans": "m1" }
}
| Variable |
Use it in |
Becomes |
{{baseUrl}} |
the URL |
the text, without a trailing slash |
{{signals}} |
the signals field |
the object, as written in the file |
{{headers}} |
the headers field |
one Name: value line per entry |
{{partsnummer}}, any name in params |
the URL or the headers |
the text, encoded in the URL |
Type {{ in a field to pick from the ones that are set. Without baseUrl in the file,
{{baseUrl}} is where a server you started in a debug session said it listens, read from its
output (Tomcat started on port 9102 (http) with context path '/', or Ktor's
Responding at …). With nothing running, it is the setting streamlord.inspector.defaultUrl,
which is
http://localhost:8080 unless you change it.
The Variables box in the panel lists each value, and says when baseUrl is the default or
which debug session it came from.
Edit variables opens the file, creating it with baseUrl filled in, and the panel picks
up the file as you save it. Any other key, or a value of the wrong kind, is reported by name
and stops the request until it is fixed; the file also has a JSON schema, so the editor marks
it as you type. The file is the place for local hosts, tokens and test values, so keep it out
of version control. A server that cannot be reached is reported together with the {{baseUrl}} it was
reached through; when the file's baseUrl is not where a running server said it started, a
link sets the file's value to that one, changing nothing else. An error response is shown with
the warnings and errors the server logged while answering it, such as Spring's
Required request parameter 'partsnummer' … is not present. Only a debug session's output
can be read by an extension, so a server started in a terminal is not seen.
Code lens above every Ktor route (route("/api") { get("/feed") }) and Spring mapping
(@GetMapping("/feed") under a class @RequestMapping): “Open in Stream Inspector” prefills
the method and the whole URL the handler needs: each path parameter and each required
@RequestParam as a {{name}}, and each required @RequestHeader("X-Id") as a line in the
headers. A value that is not in params yet is reported with a link that adds it, empty, for
you to fill in; the lens names the optional ones, such as required = false, which it leaves out.
Copy as curl puts an equivalent curl -N ... command on the clipboard.
Settings
| Setting |
Default |
Meaning |
streamlord.diagnostics.enabled |
true |
Validate Kotlin strings and HTML. |
streamlord.diagnostics.html |
true |
Also validate markup files. |
streamlord.diagnostics.unknownSignals |
true |
Warn on a $name no file in the workspace defines. |
streamlord.languages |
html, jte, kte, ftl, velocity, mustache, pebble, twig, … |
Language ids that get the HTML side. Reload after changing. |
streamlord.attributePrefix |
data- |
data-star- when you load the aliased bundle. |
streamlord.inspector.defaultUrl |
http://localhost:8080/ |
{{baseUrl}} unless .streamlord/env.json sets baseUrl. |
streamlord.inspector.requestsFile |
.streamlord/inspector.json |
Workspace-relative file holding the saved inspector requests. |
streamlord.inspector.codeLens |
true |
Show “Open in Stream Inspector” above Ktor and Spring routes. |
Datastar Pro
The extension knows the publicly documented names of Pro attributes and actions so it can
complete and validate them. It contains no Pro code and cannot make them work: that takes the
Pro bundle you license and load yourself.
Development
npm ci
npm test # unit tests: analysis, providers (with a vscode mock), grammars (real TextMate engine)
npm run test:live # the Stream Inspector client against the demo service (STREAMLORD_LIVE_URL, default http://localhost:8080)
npm run build # generates html-customdata.json and snippets/html.json from the catalog, then dist/extension.js
npm run package # streamlord-<version>.vsix
test:live needs the repository's demo running: ./gradlew :demo:run from the repository root,
then npm run test:live here.
Press F5 in the streamlord.code-workspace to launch an Extension Development Host.
Publishing
The extension is published to the VS Code Marketplace from CI only, never from a developer
machine. Bump version in package.json, commit, then push a tag vscode-v<version>. The
publish-vscode job checks that the tag matches, builds, tests, and runs vsce publish with
the VSCE_PAT repository secret (an Azure DevOps personal access token with the Marketplace
Manage scope for the MarkusAugust publisher). Which branch the bump goes on, and when the
tag may be pushed, is in the repository README.
No .vsix is ever committed; npm run package only builds one locally for installing by hand. The
attribute and action catalog lives in ../../catalog/ and is verified against the Kotlin DSL
by the SDK's own tests.
| |