Post hero imagePost hero image

The 0.4.0 post was about what the editor could do: proper language services for the built-in languages and a bridge to a real language server. All of it on Android. Version 0.5.0 is about where it runs. I released it on 27 September 2026, and the same Compose code now runs on Android, on the desktop, and in a browser.

Part 2 and part 3 of this series covered how the code got there. This post is about what you get, and where it falls short.

Upgrading on Android

If you only use Aardink on Android, change the version number and you’re done:

implementation("com.aardarch:aardink:0.5.0")

Each library now publishes one artifact per target, and Gradle picks aardink-android for you. kotlinx.coroutines has worked this way for years. The public API only grew.

The one catch is binary, not source. CodeEditorLayout gained a parameter, and the Compose compiler puts every parameter in the JVM signature. Code compiled against 0.4 has to be recompiled. No source changes.

The new pieces work on every platform:

  • EditorTypography and LocalEditorTypography. The editor’s font is provided now, not hardcoded.
  • Hardware keyboard shortcuts: undo and redo, find, replace, go to line, and Tab and Shift+Tab to indent.
  • Scrollbars on desktop and web.
  • KeyboardToolbarPlacement.platformDefault, so the keyboard toolbar sits where the platform expects it.
  • WebSocketLspTransport, for talking to a language server from a browser.

EditorThemeParser also changed. It parsed themes with org.json, which only exists on Android. It uses kotlinx.serialization now, so :editor picks up kotlinx-serialization-json as its one runtime dependency outside Compose.

The same editor in a browser

Put the 0.4.0 sample next to the browser one and there is nothing new to learn. It is the same CodeEditorLayout, the same built-in languages, and the same LspClient. Only the transport changes: stdio on Android, a WebSocket in the browser.

AardinkWeb is a flat Kotlin API in com.aardarch:aardink-editor-web. It mounts the editor into a DOM element and hands back a handle. Your exports sit on top of that, and your page calls them. On Android you skip all of it and compose CodeEditorLayout directly.

To see it, run :sample-web from the repo. It has a language picker, a theme picker, and a stats bar along the bottom.

Using it from JavaScript

You don’t need Gradle or a JDK. The npm package has the built-in languages and themes:

pnpm add @aardarch/aardink-web

I pushed 0.5.0 to npm by hand on 27 September. From 0.6 it publishes from CI, which a post on 16 November covers.

The API is shaped like Monaco on purpose. If your app already has a Monaco editor, swapping one for the other is a small diff:

import { createEditor } from "@aardarch/aardink-web";

const editor = await createEditor(container, (text) => save(text), {
  value: source,
  language: "xml",
  theme: "vscode-dark",
});
editor.updateOptions({ readOnly: true });
editor.setDiagnostics([{ line: 3, startColumn: 5, endColumn: 9, message: "Unknown tag", severity: "error" }]);
const stop = editor.onDidChangeCursor((line, column) => status(line, column));
editor.dispose();

Vite needs one line of config. Its dev-server pre-bundler rewrites the package into .vite/deps, and that breaks the new URL('./x.wasm', import.meta.url) references the loader relies on. Leave the package out of it:

// vite.config.js
export default defineConfig({
  optimizeDeps: { exclude: ["@aardarch/aardink-web"] },
});

Under the wrapper, editors cross the JS boundary as integer handles, and every export is prefixed aardink so it can’t collide with yours. The exports come from a template that lives in Aardink’s own test sources. Every Aardink build compiles it, so it can’t quietly rot.

That template is also the way in for a grammar of your own. In 0.5.0 a language written in Kotlin means building your own small wasm module from the template, with your languages registered in it. 0.6 lets you register one from JavaScript instead.

Where 0.5.0 falls short

Typing gets slower as the file grows. About 50 ms a keystroke at 6 KB, 190 ms at 26 KB, and a full second at 139 KB. A profile put the time in Compose, laying out the whole text field as one paragraph, twice a frame: once to measure and once to paint.

Fast typing loses keys in big files. I found this just after release, while building the 0.6 performance gate. At 10 keys a second, about 120 words a minute, a 5,000-line file dropped 15 to 29 of 30 keys. At 250 lines it dropped none.

A disposed editor leaks about 275 KB. Compose Multiplatform 1.12.1 has no way to tear down a viewport, and removing its window listeners by hand didn’t free the memory either. Upstream fixed it for 1.13. Until then, reuse one editor and call setValue instead of mounting a new one per view.

I shipped it anyway. The first real user is Aardflex, and its XML files run to a few hundred lines. At that size she’ll be right: none of the three problems show. Bigger files need a different design, not a tweak, and part 5 is that design.