# The editor

> Completion, hover help and error messages for LO-TEK in VS Code, JetBrains, Neovim, Helix and other editors with LSP, also where no compiler looks at the markup.

If you write TypeScript or JSX, the types help you: `lt.` shows the whole system, and a variant that does not exist stops the build. See [Type-safe use](/type-safe-use.md). If you write markup without JavaScript, in a Go template, a Razor view, PHP or hand-written HTML, no compiler looks at the attributes. Then the editor has to tell you, and it does.

## VS Code

The extension [LO-TEK Interfaces](https://marketplace.visualstudio.com/items?itemName=lo-tek.interfaces) is on the Marketplace. Search for “LO-TEK” under Extensions, or run:

```bash
code --install-extension lo-tek.interfaces
```

What you get, in HTML files and in the template languages:

- **Completion** of `<lt-field>`, `<lt-popover>` and the other elements, with their attributes and allowed values. Inside `class="…"` the `lt-` classes are completed, and inside an attribute a class takes, such as `data-variant` on `lt-button`, the values the builder function knows.
- **Hover help** when you hold the mouse over an `lt-` class, and in HTML files also over an element, with a link to the component page, or over an attribute.
- **Error messages**, as red and yellow squiggles: an element that does not exist, an attribute the element does not have, a value outside the list, a class such as `lt-buton`, and `data-variant="secundary"` on a button. `<lt-field>` without a control or without a label gets the same message the component gives in the browser. And what the template must write itself for the components to work without JavaScript and survive an update from the server: a label that neither has `for` pointing to the field nor stands around it, a panel in `<lt-popover>` without an id, a tab that points to a panel that does not exist, a tab list without a name, `role="tab"` on something that is not a link, help text inside `<label>`, `data-value` without `.lt-suggestion__value`, a hidden error text next to `aria-invalid="true"`, the removed `invalid`, `required-marker` and `control-id` on `<lt-field>`, and an id the component made itself, copied in from DevTools.
- **Quick fixes** where there is one right answer: the light bulb changes `lt-buton` to `lt-button`, removes `invalid="false"` and the removed attributes, inserts a label, gives the tab list a name, and gives the field an id and the label `for` in one go, from the field’s `name`.
- **Snippets**: type `lt-popover` and pick the snippet, and the markup from the component page is there. The id is a tab stop: type it once, and `for` on the label and `href` on the tabs follow along.

The template languages it works in without settings: PHP, Razor, Astro, Svelte, Vue, Twig, Blade, Jinja, Django, Handlebars, ERB, Go templates, EJS, Liquid, Nunjucks and Edge. Where there is template syntax, it stays out of the way: a template is not whole, and the control can be in a partial. The list is the setting `lotek.languages`.

The error messages and quick fixes come from the language server, `lotek lsp`, the same one Neovim and Helix use. It checks against the LO-TEK version the project has installed, and against components you have [taken over](/command-line.md#takeover), see [The language server](#the-language-server). Completion is generated from the components in the repository. The extension has its own version number and does not follow the package, since it only changes when an element, a class or an attribute does.

## JetBrains

IntelliJ IDEA Ultimate and WebStorm need no extension. From `@lo-tek/interfaces` 0.15.0 the npm package includes `web-types.json`, JetBrains’ own format, and the IDE picks it up as soon as the package is in the project’s `package.json`. You get completion of the elements, attributes, values and `lt-` classes, and hover help with a link to the component page. Checked in IntelliJ IDEA Ultimate 2026.2.

`web-types.json` is read only for files. If the markup is in a string, such as an HTML template in Kotlin or TypeScript, you get no suggestions from it. The plugin [LO-TEK Interfaces](https://plugins.jetbrains.com/plugin/34764-lotek) covers that: search for “LO-TEK” under *Settings → Plugins → Marketplace*. It completes the `lt-` classes inside `class="…"` in a string marked as HTML:

```kotlin
// language=HTML
fun saveButton() = """
    <button class="lt-button" type="submit">Save</button>
"""
```

`// language=HTML` and `@Language("HTML")` are IntelliJ’s own markers, and need no dependency. Without such a marker the string is just text to the IDE, and then there is nothing for the plugin to attach to. Completion gives only the classes, not the elements or attributes. Checked in Kotlin and TypeScript in IntelliJ IDEA Ultimate 2026.2.3.

From version 0.4.0 the plugin has **live templates**: type `lt-field` and press Tab, and the markup from the component page is there, with the id as a variable and `for` following along. They are made from the same recipes as the snippets in VS Code.

From version 0.3.0 the plugin also gives the **error messages**, in HTML files and in marked strings: an element, a class, an attribute or a value that does not exist, with a quick fix where there is one right answer. The check is the same Rust core that `lotek check` and VS Code use, run in the IDE’s own JVM. It needs neither Node nor the IDE’s JavaScript support, and looks for the manifest the way the [language server](#the-language-server) does, from the file upwards to the project folder, so components you have taken over are known. Completion reads the classes from the same manifest, so it suggests the components you have taken over and never a class the check rejects. A finding you want to keep is suppressed with `<!-- lotek-ignore-next rule-name -->` above the tag.

## The language server

The error messages and quick fixes are available as a language server, `lotek lsp`, for every editor that speaks LSP. It is written in Rust, is the same check as `lotek check`, and is started in whichever way suits the project:

| If you have | The command |
| - | - |
| Node | `npx @lo-tek/interfaces lsp` |
| Java 17 or newer | `java -jar lotek.jar lsp` |
| wasmtime | `wasmtime run --dir=/::/ --env LOTEK_PACKAGE=/path/to/package lotek.wasm lsp` |
| `cargo install` | `lotek lsp` |

Where you get `lotek.jar`, `lotek.wasm` and the executable is described on [Command line](/command-line.md#without-node). wasmtime sees only the folders it is given, and `--dir=/::/` gives it the whole file system, with the same paths the editor uses. It is the weakest of the hosts. See [The drawbacks of wasmtime](/command-line.md#the-drawbacks-of-wasmtime).

The server checks against what the project actually has. It goes from the file’s folder upwards, the way Node finds a package, and looks in each folder for `build/lotek/manifest.json`, which the [Gradle plugin](/kotlin.md#the-gradle-plugin) writes, and then for `node_modules/@lo-tek/interfaces/manifest/manifest.json`, the installed package. The nearest folder with one of them wins, and it does not look above the root of the workspace. In a monorepo, a page in `apps/web` is therefore checked against the package `apps/web` has installed. If the file is outside the workspace, such as a scratch file, or the editor has not given a workspace, it does not search from the file at all: then the manifest at the root of the workspace applies, if there is one. If none of them exist, `npx`, `java -jar` and VS Code check against the package they bring with them. The executable and wasmtime check against the package `LOTEK_PACKAGE` points to, and without it they check nothing and say so. The file is read again when it changes, so a new build or an upgrade reaches the editor without a restart.

In **Neovim** 0.11 or newer:

```lua
vim.lsp.config("lotek", {
  cmd = { "npx", "@lo-tek/interfaces", "lsp" },
  filetypes = { "html", "htmldjango", "php", "gotmpl", "eruby", "jinja", "twig", "blade" },
  root_markers = { "package.json", "build.gradle.kts", ".git" },
})
vim.lsp.enable("lotek")
```

In **Helix**, in `languages.toml`, next to the HTML server:

```toml
[language-server.lotek]
command = "npx"
args = ["@lo-tek/interfaces", "lsp"]


[[language]]
name = "html"
language-servers = ["vscode-html-language-server", "lotek"]
```

**Zed** starts only language servers that a Zed extension has registered, and no such extension for LO-TEK exists yet.

The server gives findings and fixes. It does not do completion or hover help yet, so those still come from the extension in VS Code, and in JetBrains from `web-types.json` for files and from the plugin for HTML in strings. `initializationOptions` can have `manifest`, a path to a manifest that takes precedence over the others, and `rendered: true`, which checks every file as a whole page, with the wiring.

## The command line and the tests

The same check exists without an editor. `lotek check` reads files, standard input or an address, and writes each finding as `file:line:column: error: message`, or `warning:` where it is a warning. One finding gives an error code, so it can run in CI:

```bash
npx @lo-tek/interfaces check templates/*.html
npx @lo-tek/interfaces check http://localhost:8080/form
```

If the server makes the HTML in code, as strings in a Kotlin app, there is no template file to check. Then you check the page the server sends, with the address or with `--rendered` in the app’s own tests. Flags, output and examples for tests in JavaScript and Kotlin are on [Command line](/command-line.md).

## What none of them see

The editor sees one file at a time. A label in one partial and a field in another only fit together once the page is assembled. `check` sees that on a whole page, where it requires every id that is pointed to to exist and every field to be wired. See [Command line](/command-line.md#templates-and-whole-pages). In the browser, the web component reports it if the wiring is missing. See [Markup and behaviour](/markup-and-behaviour.md).
