Post hero imagePost hero image

I released Aardink 0.6.0 on 3 October 2026. Underneath, the editor no longer wraps Compose’s text field. It draws only the lines on screen and takes its own text input, on Android, the desktop and the web. Part 5 covers why and how. This post is what got built on top of it, and what it costs you to upgrade.

Two patch releases followed. 0.6.1 on 4 October fixed the XML formatter and theme colours for squiggles, and 0.6.2 on 12 October fixed a gap above the keyboard toolbar on Android. Use 0.6.2.

Upgrading

This release breaks the API on purpose. There is no deprecation bridge, because everything that assumed one text field holding the whole document has nothing left to stand on. Most apps need three changes:

  • Pass EditorOptions instead of the boolean flags.
  • Stop reading CodeEditorState.textFieldState. Read text and selections, and edit with applyEdit.
  • Drop annotatedText. Colours come from the tokenizer and EditorTheme.
// 0.5
CodeEditorLayout(state = state, readOnly = true, softWrap = true)

// 0.6
CodeEditorLayout(state = state, options = EditorOptions(readOnly = true, softWrap = true))

The undo manager is gone too. undoManager.canUndo becomes canUndo, and it is snapshot state now, so the derivedStateOf you wrapped around it can go.

One change compiles fine and still bites. diagnostics now defaults to null, which means “ask the language service”. An app that passed nothing and has a language service will suddenly show squiggles. Pass emptyList() to keep the old behaviour.

On the web, the package is on npm and publishes from CI with provenance:

pnpm add @aardarch/aardink-web

docs/MIGRATION_0.6.md lists every removed and changed symbol with a before and after.

Editing

Multiple cursors use VS Code’s keys. Alt+click adds a caret, Ctrl/Cmd+D adds the next occurrence, Ctrl+Shift+L selects every occurrence, and Ctrl+Alt+Up/Down adds a caret above or below. Shift+Alt+drag makes a column selection. Every edit lands at every caret as one undo step.

An input method composes at the main caret. When the composition ends, the result is repeated at the other carets, in the same undo step.

Line commands are on VS Code’s keys too: toggle comment (Ctrl/Cmd+/), move lines (Alt+Up/Down), copy lines (Shift+Alt+Up/Down), and delete lines (Ctrl/Cmd+Shift+K).

Undo groups the way Monaco does: a word at a time, backspaces together, and every other edit on its own. For dirty tracking, store alternativeVersionId when you save and compare it later. Undo back to the saved text and it matches again.

Seeing more

The minimap draws the whole text as blocks in token colours, in tiles of 256 lines that are kept until a line in them changes. Scrolling a 5,000-line file on the web, it adds 0.1 to 0.5 ms a frame and scrolling stays at 60 frames a second.

Sticky scroll pins the first line of up to five enclosing blocks at the top of the view. Click one to jump to it.

The caret’s line is highlighted, the bracket pair at the caret is boxed, and brackets are coloured by depth. The colours are VS Code’s by default, and the theme parser reads editorBracketHighlight.foreground1 to 6, so a VS Code theme brings its own.

Popups sit next to the caret now. With a hardware keyboard, completions open as a list at the caret, signature help sits above it, and quick fixes sit at their diagnostic. Rest the mouse on a symbol for half a second and its documentation shows. The language services had hover docs all along. The editor never showed them, which is a bit of a bugger to find out this late.

Language services the editor finally uses

Same story for navigation. Language services have offered definition, references and format since 0.4, and nothing called them.

  • F12 or Ctrl/Cmd+click goes to the definition. A definition in another file goes to onNavigateToLocation for the host to open.
  • Shift+F12 lists the references at the caret.
  • Shift+Alt+F formats the selection or the whole document. It changes only the lines that differ, as one undo step, so carets and folds elsewhere stay put.

Diagnostics run without the host. The editor asks the language service after a pause in typing, drops any answer for text that has changed since it asked, and moves squiggles with the text while you type.

Grammars can be data. DeclarativeGrammar.parse(json) reads a subset of Monaco’s Monarch format, and the aardflex web app’s Monaco grammar reads unchanged. Lookbehind is refused when the grammar is read, with where it is, because Kotlin/wasm’s regex engine runs it at every position. On the web, registerLanguage takes the same grammar plus JavaScript functions for completions, hover and diagnostics.

Screenshots

The scenes from the screenshot tests, one per feature. Click one to open the viewer.

Brackets coloured by depth
Bracket-pair colours
A folded for loop
Folding
The minimap beside the text
Minimap
A column selection over four lines
Several carets and a column selection
Find matches and a multi-line selection
Find and selection
An error and a warning squiggle with gutter dots
Diagnostics
Class, function and loop lines pinned at the top
Sticky scroll
Spaces drawn as dots and tabs as arrows
Whitespace
Long lines wrapped to the view
Soft wrap

Try it here

This is the npm package, 0.6.2, with the minimap and sticky scroll on. Its theme follows the site’s light or dark mode. Have a crack at Ctrl/Cmd+D on shape, Alt+click for more carets, or Shift+Alt+F after making a mess of the indents.

Downloads about 4.7 MB. Needs Chrome or Edge 119, Firefox 120, or Safari 18.2.

The editor is about 4.7 MB gzipped, so the page loads none of it until you click. Hovering or focusing the button calls preloadAardink(), which usually hides most of the wait.

This blog uses Astro’s ClientRouter, so pages swap without a reload. On Compose Multiplatform 1.12, every disposed editor leaks about 320 KB, and one left mounted when the page swaps leaks the lot. The component calls dispose() on astro:before-swap. Don’t transition:persist the container either: moving a mounted editor breaks it from Compose Multiplatform 1.13.

The rest is config. Vite’s dependency pre-bundler moves the package into .vite/deps, which breaks the relative URLs it loads its WebAssembly from:

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

Azure Static Web Apps needs to serve the .wasm files with the right type, or the browser can’t compile them while they download:

// staticwebapp.config.json
"mimeTypes": {
  ".wasm": "application/wasm"
}

Known limitations

  • A disposed editor is not fully released. It leaks about 320 KB on Compose Multiplatform 1.12.1, which 0.6 ships on. Upstream fixed it for 1.13. Until Aardink moves to 1.13, reuse one editor and call setValue instead of mounting a new one per view.
  • A theme rule’s background is ignored. Its foreground and fontStyle apply.
  • The console logs “Accessing memory via wasmExports is deprecated” once, when the editor loads its font. It is harmless and comes from Compose’s resources library.
  • Some checks still need real devices: input methods (CJK on Windows and macOS, Gboard in Android Chrome), TalkBack and NVDA, and the clipboard in Firefox and Safari.
  • The browser needs WebAssembly GC: Chrome or Edge 119, Firefox 120, or Safari 18.2.

Part 7 swaps Monaco out of Aardflex for this editor.