Tooling
Editor support
The language server and the Zed extension.
Kiln has a language server that gives any editor that supports the Language Server Protocol errors as you type, formatting, completions and more. There are ready-made extensions for VS Code and Zed.
VS Code
Search for Kiln in the Extensions view. The extension is on the Visual Studio Marketplace and on Open VSX, so it also works in Cursor, Windsurf and VSCodium. It adds:
- Syntax highlighting for Kiln and the HTML around it, including Kiln inside attribute values, with JavaScript in
<script>and CSS in<style>. - A file icon for
.kilnfiles, which shows when your icon theme doesn't have one. - Comment toggling with
{# #}, bracket matching, auto-closing for{{and{%, folding for block tags, and indenting as you type. - Snippets for each block tag, such as
forandcomponent. - The language server, which comes with the extension and runs on VS Code's own Node.
VS Code formats with the language server. To format when you save, add this to your settings. The editor.defaultFormatter line matters if your settings pick a formatter for every language, such as Biome or Prettier, which can't format Kiln.
{
"[kiln]": {
"editor.defaultFormatter": "buildlucid.kiln",
"editor.formatOnSave": true
}
}
VS Code's indentation settings decide whether the formatter indents with tabs or spaces. The extension defaults to tabs.
To use Tailwind CSS class completion and Emmet abbreviations in templates, treat Kiln as HTML in both:
{
"tailwindCSS.includeLanguages": { "kiln": "html" },
"emmet.includeLanguages": { "kiln": "html" }
}
Zed
Install the Kiln extension from the extensions page (zed: extensions). It adds:
- Syntax highlighting for Kiln and the HTML around it, with JavaScript in
<script>and CSS in<style>. - Comment toggling with
{# #}, bracket matching, and auto-closing for{{and{%. - Indenting as you type, and an outline of blocks, macros, components and slots.
- Snippets for each block tag, such as
forandcomponent. - The language server, which Zed installs and starts for you. It needs Node, which Zed provides.
Zed formats with the language server. To format when you save, add this to your settings. The formatter line matters if your settings pick a formatter for every language, such as Biome or Prettier, which can't format Kiln.
{
"languages": {
"Kiln": {
"formatter": "language_server",
"format_on_save": "on"
}
}
}
Zed's tab settings decide whether the formatter indents with tabs or spaces. The extension defaults to tabs.
To use Tailwind CSS class completion in templates, add Kiln to the Tailwind language server's languages:
{
"languages": {
"Kiln": {
"language_servers": ["kiln", "tailwindcss-language-server", "..."]
}
},
"lsp": {
"tailwindcss-language-server": {
"settings": {
"includeLanguages": { "kiln": "html" }
}
}
}
}
Other editors
Install the language server from npm and point your editor at it. It talks over standard input and output.
npm install --save-dev @lucidkiln/language-server
npx kiln-language-server --stdio
Register it for files ending in .kiln. Kiln's syntax is close to Twig's, so a Twig highlighting grammar is a reasonable stand-in until your editor has a Kiln one.
What the language server does
- Errors as you type. The same problems
checkreports, from syntax errors to props a component doesn't declare. - Formatting. The whole file or a selection, using the formatter.
- Completions for tags, filters, functions, tests, variables in scope,
loop's properties, template names inextends,include,componentandimport, the props of the component or template you're passing data to, and properties after a dot when the language server knows the value's type. Tags and built-ins show the same docs as hover. - Hover for tags, filters, functions, tests, variables and templates. Tags and built-ins show their docs from this site, up to the first example, with a link to the section. End tags, such as
endfor, show the tag they close. A template shows its props. Your own functions, filters, tests, globals and typed values show their type and doc comment from TypeScript. - HTML completions and hover outside
{{ }}and{% %}: elements, attributes and their values, such as the values ofbutton'stype, with MDN's description and a link to its page. They come from the HTML service VS Code uses. - Go to definition for template names, macros, blocks and variables. A variable goes to the prop,
set,for, macro argument or import that brings it into scope. Your own functions, filters, tests, globals and the properties of typed values go to their TypeScript source. - Links on template names, and a list of the template's blocks, macros and slots for your editor's outline.
The language server uses the version of Kiln installed in your project when there is one, so its errors match how your templates render.
Telling the editor about your project
The language server reads its settings from a kiln.json file. It uses the nearest one in the template's folder or any folder above it.
Checks use unsaved text from every open template, including after kiln.json changes. Template discovery skips symlinks, hidden folders and node_modules without entering them.
{
"root": "src/templates",
"strict": true,
"functions": ["documents", "document"],
"filters": ["markdown"],
"tests": [],
"globals": ["site"],
"types": ["Post"]
}
| Field | Description |
|---|---|
root | The folder template names are relative to, from the folder holding kiln.json. Default ".". |
strict | Whether your environment uses strict mode. Default true. |
functions, filters, tests | The names of the functions, filters and tests your project registers, on top of Kiln's own. |
globals | The names of your globals. |
types | The names of the prop types you register in the types option, such as Post. |
htmlChecks | Your htmlChecks option. Default true. |
The editor never runs your code. It only needs the names. Without a kiln.json, the language server can't tell which names your project adds or where template names start, so it reports syntax errors and HTML checks only, and go to definition stays inside the current template.
Generating kiln.json
Rather than keep the file up to date by hand, write it from your environment with generateManifest, next to generateTypes. Pass the template folder relative to where you write the file.
import { writeFile } from "node:fs/promises";
import { createKiln, generateManifest } from "@lucidkiln/core";
import { fileLoader } from "@lucidkiln/core/node";
const kiln = createKiln({ loader: fileLoader({ root: "src/templates" }), functions, filters });
await writeFile("kiln.json", generateManifest(kiln, { root: "src/templates" }));
Names are sorted, so the file only changes when you add or remove one.
Types from TypeScript
If your project uses TypeScript, the language server can read the types of your functions, filters, tests, globals and prop types. With them, it:
- Completes properties after a dot, such as
post., orbrick.inside a loop overpage.bricks. - Shows the type and doc comment of your functions, filters, tests, globals and their properties when you hover over them.
- Goes to the TypeScript source of a function, filter, test, global or property.
The editor still never runs your code. It asks your project's own copy of TypeScript what the types are.
Describing your functions
Add your functions, filters, tests and globals to Kiln's interfaces through module augmentation, the same way as project-wide prop types:
import { createKiln } from "@lucidkiln/core";
import { fileLoader } from "@lucidkiln/core/node";
const functions = {
/** Finds the published documents in a collection, newest first. */
documents: async (collection: string, query: { limit?: number } = {}) =>
db.list(collection, query),
};
const filters = { markdown };
const globals = { site: { name: "Kiln", url: "https://kilnlang.com" } };
export const kiln = createKiln({
loader: fileLoader({ root: "src/templates" }),
functions,
filters,
globals,
types: { Post: (value) => value instanceof PostView },
});
type Functions = typeof functions;
type Filters = typeof filters;
type Globals = typeof globals;
declare module "@lucidkiln/core" {
interface KilnFunctions extends Functions {}
interface KilnFilters extends Filters {}
interface KilnGlobals extends Globals {}
interface KilnTypes {
Post: PostView;
}
}
- The interfaces are
KilnFunctions,KilnFilters,KilnTests,KilnGlobalsandKilnTypes. - They only describe your project to tools.
createKilndoesn't check them against what you register, so keep them next to the code that registers your functions. - More than one file can add to them. A plugin can describe the functions it registers, and you add yours.
- Keep each function's own type. A map typed as
Record<string, KilnCallable>tells the editor only that each entry is some function. - For a function wrapped in
withRender, the editor leaves out the first argument, which Kiln passes for you. - For a filter, the first argument is the value before the
|. - Prop types come from
KilnTypes. If you pass your type map to the generatedSchemainstead, add it toKilnTypestoo, withinterface KilnTypes extends HostTypes {}.
What the editor needs
- TypeScript 6 or later installed in your project.
- A
tsconfig.jsonin the folder holdingkiln.jsonor a folder above it, which includes the file with your declarations. The editor uses the nearest one.
Without them, everything else on this page still works. The editor sees changes to TypeScript files when you save them. The first hover or completion after you open a project can take a moment while TypeScript starts.
How the editor follows types
The editor works out a value's type from where it comes from:
| Value | Type |
|---|---|
| A prop | Its type in props, with your types from KilnTypes. |
| A global | Its type in KilnGlobals. |
| A call to your function or filter | What it returns. When that is a promise, the value Kiln waits for. |
| A built-in filter | The type the filter gives, such as an item of the list for first, the same list for sort, and a number for length. |
for item in list | An item of the list, or a value of a hash or map. In for key, value in …, the key is a number for a list and a string for a hash. |
An arrow function's parameter in map, filter or sort | An item of the list. |
set name = value | The value's type. A set block holds markup. |
a.b, a["b"], a[0] | The property's or item's type. |
When the editor can't tell a value's type, as for an untyped prop or a macro argument, it doesn't guess. You get no property completions for that value.