Changelog
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[Unreleased]
[0.8.0] - 2026-11-02
0.8 lets the editor understand the document’s structure and lets hosts draw on it. Every new
option is off by default. It breaks source in two places: FoldRange gains a kind, and
EditorCommand has new members, so an exhaustive when over it needs a branch for them.
docs/MIGRATION_0.8.md has the details.
Added
- Document symbols.
LanguageService.documentSymbolsreturns aDocumentSymboltree; the built-in XML, HTML, JSON, TOML, Kotlin and Markdown services provide one.CodeEditorState.symbolsholds it, moved with each edit, andsymbolPathAt(offset)maps the caret to the symbols around it. Ctrl+Shift+O opens Go to Symbol,EditorOptions.breadcrumbsshows the path at the caret, and sticky scroll prefers symbols over fold ranges. - Decorations.
CodeEditorState.setDecorations(owner, list): backgrounds, whole lines, underlines, borders, gutter bars, minimap and overview-ruler marks, and hover messages, kept on their text as it is edited. On the web,deltaDecorationsandcreateDecorationsCollection. - Overview ruler (
EditorOptions.overviewRuler): diagnostics, find matches, carets, changed lines and decorations along the right edge; a click scrolls there. The minimap marks errors, warnings and find matches too. - Occurrences (
occurrencesHighlight), expand and shrink selection (Shift+Alt+Right and Left), linked editing of tag pairs (linkedEditing), and links opened with Ctrl+click throughCodeEditorLayout(onOpenLink). - Colour decorators (
colorDecorators): a swatch under each colour, and a picker with hue, saturation, value, alpha and hex. Built in for hex colours in XML (as Android writes them), HTML, JSON, TOML and CSS. - Folding:
FoldKind, region markers (FoldingProvider.withRegionMarkers), anIndentationFoldingProvider, Ctrl+Shift+[ and ] at the caret, and the Ctrl+K chords: Ctrl+K Ctrl+0, Ctrl+J and Ctrl+1 to 5. - Keybindings as data:
KeyChord,EditorKeyBinding,EditorKeyBindings.DefaultandEditorOptions.keyBindings, and VS Code’s remaining keys: join lines, jump to bracket, cursor undo, skip occurrence, change all occurrences, carets at line ends, line and block comments, insert line above and below, select line, scroll by line, trim trailing whitespace. A click on a line number selects the line. - Zoom and typography:
EditorOptions.fontSize,lineHeight,pinchToZoom(also Ctrl+wheel and Ctrl+= / Ctrl+- / Ctrl+0) andfontLigatures;CodeEditorLayout(onFontSizeChange).EditorTheme.fromColorSchemefor Material 3 colours, andCodeBlockfor read-only code. - Web:
getDocumentSymbols,getSymbolPathAt,provideDocumentSymbolsandprovideDocumentColors;getVisibleRanges,getScrollTop,setScrollTop,getScrollHeight,onDidScrollChangeandlayout; diagnostics per owner (setDiagnostics(list, owner),setModelMarkers); two-key chords inaddCommand; the new options under Monaco’s names.
Changed
- A bound key whose command cannot run now goes on to whatever else takes it, rather than being swallowed: a browser’s page zoom keeps working in an editor that does not zoom.
- Markdown and CSS have a built-in language service, so the gutter keeps a diagnostics lane for them.
- A language registered on the web without
extendsfolds by indentation.
[0.7.1] - 2026-11-01
Fixed
- Desktop: letters, digits and every other typed character were dropped; only keys with a
binding, such as Enter and Backspace, worked. Compose wraps the AWT
KEY_TYPEDevent in its own native event, which the editor did not unwrap; it now reads it throughawtEventOrNull(#37). - Android, soft wrap: a line wrapped onto several rows drew those rows further apart than the editor’s row height on some phones, so its last row overlapped the next line. The gutter, scrolling and hit-testing step rows by the editor’s line height, but the text was drawn as one paragraph at whatever pitch Android’s text engine chose. Every row of a wrapped line, with its selection, caret, squiggles and whitespace marks, now sits at the editor’s pitch.
- Android, soft wrap: an indented line whose first word did not fit after its indentation was broken before that word, leaving a first row of nothing but indentation. Android’s line breaker starts a word after any white space, so under soft wrap the indentation is now laid out as invisible glyphs of the same width (a tab as one with letter spacing to its stop); the first word stays on the first row and the break falls inside what follows. Only for a monospace font, where the glyph is as wide as a space; carets, taps, selections and whitespace marks are unchanged.
[0.7.0] - 2026-10-24
0.7 makes the editor controllable and observable by its host, on a phone as well as with a
keyboard, and turns an XML dialect’s vocabulary into a language service. It breaks source in three
places: LanguageService.smartIndent, format and formatRange take FormattingOptions
(smartIndent returning the indentation as a string), Diagnostic.lineNumber is gone, and
CompletionKind and DiagnosticSeverity have new entries. docs/MIGRATION_0.7.md
has every change with a before and after.
Added
- Content and selection events.
CodeEditorState.addContentListener(andcontentChanges(), aFlow) reports every edit asContentChanges: the range replaced, in lines and columns too, the text inserted, the document’s version and whether it was an edit, an undo, a redo or a new text, enough for a language server’s incremental sync.addSelectionListenerreports the selections with aSelectionChangeReason(keyboard, pointer, edit, undo, redo, flush, API).isFocusedis snapshot state, andCodeDocument.snapshot()is public. - Commands.
CodeEditorState.execute(EditorCommand)runs what a key or a menu would: undo, redo, toggle comment, select all occurrences, insert a snippet, format the document or the selection, open completions, show hover, quick fixes, rename, go to definition, find references, next and previous problem, find, replace, go to line, fold all, unfold all, fold a level, and reveal an offset without moving the caret.canExecuteandavailableCommands(snapshot state) enable a host’s buttons. - Host items in the touch chrome.
CodeEditorLayout(keyboardToolbarItems = …)adds characters and actions (KeyboardToolbarItem.Character,KeyboardToolbarItem.Actionwith an icon or a label, running anEditorCommandor a click) after the language’s own characters;touchMenuItemsandcontextMenuItems(EditorMenuItem) add to the touch menu and the right-click menu. The touch menu offers Format and Quick Fix when a language service can, and scrolls sideways when it does not fit. - Diagnostics carry a
codeand itscodeDescription,tags(unnecessary code is drawn faded, deprecated code struck through),relatedInformation, and aHintseverity, drawn as three dots under its start with no gutter dot. A range may run over several lines. Resting the mouse on a squiggle shows its message, source and code above the hover documentation, with a Quick Fix button; tapping one on a touch screen opens its banner. F8 and Shift+F8 select the next and previous problem and show it in the banner with its place in the list; Escape closes it. - The completion list is arranged by the editor. Items are ordered by
sortPriority, keeping the service’s order among equals. A service that answerscompletionsAreIncomplete = falseis asked once per word and the editor narrows its list as the word grows, byfilterText, as VS Code filters.EditorOptions.quickSuggestionsopens the list on letters typed in code.CompletionItem.detailis shown beside the label;commitCharactersaccept the selected item when typed.CompletionKindhas Monaco’s kinds too. - Language composition in Kotlin.
CompositeLanguageService(primary, fallback)andLanguageDefinition.extending(base, overlay)put a dialect’s service over a base language’s: completions merged, diagnostics both, hover, definition and auto-close from the dialect else the base, indentation and formatting from the base when the dialect has none, as the web’sextendsdoes. - XML from a schema.
XmlSchemadescribes a dialect’s elements, attributes, values, nesting, required attributes and documentation, read from JSON withXmlSchema.parse;XmlSchemaLanguageServiceturns it into completions (elements by what the element at the caret may hold, inserted with their required attributes and a choice of their values; attributes; values), hover documentation, and checks for unknown elements and attributes, elements out of place, values not among an attribute’s and required attributes left out, over the built-in XML language’s well-formedness checks, closing tags, indentation and formatting. - Monaco theme data in Kotlin.
EditorThemeParser.fromMonacoJsonreadsdefineTheme’s{ base, inherit, rules, colors }, so one theme file serves the web editor and a Kotlin one. - Formatting options.
FormattingOptions(tabSize, insertSpaces)is what the editor passes tosmartIndent,formatandformatRange, with helpers (indentOf,indentWidth,indentAfter,indentUnit) that measure indentation in columns and write it as spaces or tabs. - Line endings.
CodeDocument.eol(LineEnding.LForCRLF, from the text’s first line break) is what Enter, typing and pasting insert, orEditorOptions.eolwhen set, so editing a CR LF file no longer mixes in LF. - Keys: F2 renames (also in the right-click menu), Ctrl/Cmd+. shows the quick fixes at the caret (also in the menus), F3 and Shift+F3 go to the next and previous find match, Alt+Enter selects every match, and Ctrl+F and Ctrl+H take a selection on one line as the query, selected in the find field so typing replaces it.
- Web:
onDidChangeContent’s event haschanges, in Monaco’sIModelContentChangeshape;onDidChangeCursorSelection,onDidFocusEditorTextandonDidBlurEditorText;getPositionandsetPosition;revealLine,revealLineInCenter,revealRangeandrevealRangeInCenter;triggerwith Monaco’s action ids;addCommand('CtrlCmd+S'or Monaco’sKeyMod | KeyCode),addAction(with keybindings and a right-click menu entry) andgetAction; thequickSuggestionsoption; completion answers withincomplete: false, items withdetailandcommitCharacters, Monaco’s kind names and numbers; diagnostics withendLineNumber,source,code,tags,relatedInformationandhint, and Monaco’s marker shape accepted as it is;registerLanguage({ schema })for an XML dialect. - Language servers: formatting sends the editor’s tab size; diagnostics keep their code, its
description, tags and related information, and severity 4 is a hint; completions keep their
kind,
detailandcommitCharacters, are ordered bysortText, and a complete list is asked for once per word.
Changed
- Breaking:
LanguageService.smartIndent(document, lineIndex, options)returns aString;formatandformatRangetakeFormattingOptions. The built-in formatters and Enter indentation followEditorOptions.tabSizeandinsertSpaces, tabs included, instead of four spaces. - Breaking:
Diagnostichas nolineNumber;lineIn(document)works it out. The 0.6 constructor still compiles, deprecated. - Breaking:
CompletionKindandDiagnosticSeverityhave new entries. - Web, export template:
AardinkWeb.onContentChange’s callback takes the changes as a fourth argument. - The built-in XML language’s completions come from a default
XmlSchema(the same Android elements, attributes and values as before).
Fixed
- A gutter dot showed the least severe of its line’s diagnostics; it shows the most severe.
- On the web, a reported diagnostic running over several lines ended at the end of its first.
[0.6.3] - 2026-10-22
Fixed
- Web: F8 typed a
wwhile the editor’s canvas had the focus, and the other function keys, Insert, a numpad key without NumLock and a dead key typed a character too. Compose gives such a key the code of the physical key as its character, which the editor took for typing; a character equal to its key’s code is now typed only for a letter, a digit or a space on its own key. - Find’s regex replacement was inserted as written, so
$1stayed$1. In regex mode it now reads VS Code’s syntax:$0or$&for the match,$1to$99for groups,$$for a dollar sign, and\n,\tand\\. Replace and Replace All leave out a match the text no longer has instead of replacing the text now at its place. - An accessibility service’s “set text” replaced the whole document with the 101 lines around the caret it is shown, so in a longer document every other line was lost. It now replaces only those lines, and of them only what changed.
- The KDoc of
AardinkWeb.updateOptionsanddocs/WEB_INTEGRATION.mdsaid changing the language starts a new undo history; the text, the selection and the undo history are all kept.
Changed
- Compose Multiplatform 1.12.2.
[0.6.2] - 2026-10-12
Fixed
- On Android, when a host put its own bars below
CodeEditorLayout, the keyboard toolbar floated above the soft keyboard with a gap the height of those bars, because the editor padded itself by the whole keyboard height. It now pads only by the part of the keyboard that covers it, so the toolbar sits right on the keyboard.
[0.6.1] - 2026-10-04
Fixed
- The XML formatter left a tag spread over several lines out of the depth, but its closing tag still closed a level, so every line after it sat one level too far left. Depth now counts every line’s tags, and a multi-line tag is re-indented as a block, keeping its continuation lines’ alignment and leaving lines inside an attribute value alone.
EditorThemeParserand the web’sregisterThemeignored a theme’seditorError.foreground,editorWarning.foregroundandeditorInfo.foreground, so squiggles always took the default colours. They now take the theme’s, and fall back to the base theme’s when unset.- The gutter’s diagnostic dots were drawn in
GutterAnnotationKind’s fixed palette; they now use the theme’serrorColor,warningColorandinfoColor, as the squiggles do. - The text shifted sideways as the gutter’s diff and diagnostic lanes appeared and disappeared (on the first edit, after a save cleared the last diff bar, or as diagnostics came and went). The lanes now stay while there is a saved baseline or a source of diagnostics.
[0.6.0] - 2026-10-03
0.6.0 replaces the editor’s text field with a renderer and text input of its own on every
platform, so typing costs the same in any size of document, and builds a wave of features on it:
multiple cursors, column selection, a minimap, sticky scroll, bracket-pair colours, go to
definition, references and format. The web editor is on npm, and takes languages and themes
registered from JavaScript. The public API breaks where it assumed one whole-document text field;
docs/MIGRATION_0.6.md lists every change with a before and after.
Added
- The editor’s own renderer, on every platform.
CodeEditorLayoutno longer wraps aBasicTextFieldholding the whole document: it lays out and draws only the lines on screen, from a cache of recently drawn ones, and takes its own text input. Keys follow VS Code’s bindings (Cmd on macOS); input methods compose and auto-correct as in a text field, the web’s seeing a window of text around the caret instead of the whole document; the clipboard goes through Compose’sClipboard, or on the web through the browser’s clipboard events, which need no permission. Mouse: click, Shift+click, Alt+click for another caret, double and triple click, drag, and a right-click menu. Touch: tap, long press, selection handles with the platform’s text toolbar, and Android’s magnifier. On the web’s 5,000-line reference document the editor settles about 70 ms after mounting (3.2 s in 0.5) and 8 to 10 ms after a keystroke (3.7 s), and loses no keys at any typing speed (15 to 29 of 30 at 10 keys a second). Seedocs/MIGRATION_0.6.md. EditorOptions, passed asCodeEditorLayout(options = …): read-only, soft wrap and the gutter switches, plushighlightCurrentLine,matchBrackets,bracketPairColorization,stickyScroll,showMinimap,tabSize,insertSpacesandrenderWhitespace.- Whitespace drawn as VS Code draws it:
EditorOptions.renderWhitespace(RenderWhitespace:None,Boundary,Selection,Trailing,All;Selectionby default, as in VS Code) shows spaces as dots and tabs as arrows inEditorTheme.whitespaceColor(read from a VS Code theme’seditorWhitespace.foreground; the text colour, faint, when unset). Tabs are now as wide as the columns to the next tab stop oftabSize, instead of about one character. - Snippet completions, as Monaco’s
InsertAsSnippet: aCompletionItemwithisSnippetinsertsinsertTextin VS Code’s snippet syntax ($1,${1:placeholder},${1|one,two|},$0,$TM_SELECTED_TEXTand the otherTM_variables) and selects its first tab stop. Tab and Shift+Tab step through the stops, a stop written twice is edited at both places, a choice offers its options in the completion list, and Escape, the final stop or a caret moved elsewhere ends it. Its lines follow the indentation of the line they land on unlesskeepWhitespaceis set. The built-in Kotlin, XML, TOML and JSON completions use them (an XML attribute leaves the caret between its quotes), and a language server’s snippets (insertTextFormat2) are filled in instead of stripped. On the web,insertTextRules(CompletionItemInsertTextRule). - Token colours by Monaco’s token names:
TokenType.scopegives every type a dotted name (keyword,comment,delimiter, …), and a type without its own entry inEditorTheme.tokenColorstakes the entry of the longest prefix of its name that has one, socomment.doctakescomment’s colour unless the theme gives it its own.NamedTokenTypemoved to:editor(com.aardarch.aardink.core) for this. The built-in languages name more: KDoc and JSDoc arecomment.doc,if/return/throwand the other control keywordskeyword.flow, escapes in stringsstring.escape, and numbersnumber.hex,number.binaryandnumber.float; the VS Code themes colourkeyword.flow,string.escapeandregexpas Dark+ and Light+ do. - Bold, italic, underline and strikethrough in themes:
EditorTheme.tokenFontStyles(TokenFontStyle), found by name as colours are, and read from a VS Code theme’sfontStyleor a Monaco rule’s. - Multiple selections:
CodeEditorState.selectionsandsetSelections(the first is the primary one, as in Monaco), and a settableselection. Ctrl/Cmd+D adds the next occurrence, Ctrl+Shift+L selects them all, Alt+click and Ctrl+Alt+Up/Down add carets (Alt+click on a caret removes it, Alt+drag adds a selection), and typing, deleting, pasting, commenting and moving lines work at every one, each as one undo step that brings every selection back. - Column (box) selection, as in VS Code: Shift+Alt+click or Shift+Alt+drag from the caret, a middle-button drag from where it starts, or Ctrl+Shift+Alt+arrows and PageUp/PageDown (Cmd+Shift+Option on macOS). Rows are the ones on screen, so wrapped rows count and folded lines do not; a row whose text ends before the box is left out.
- An input method composing with several carets composes at the primary one and, when the composition ends, puts the result at every other caret in the same undo step; an input method’s own backspace and corrections of the word before the caret happen at every caret too.
- Line commands with VS Code’s keys: toggle comment (Ctrl/Cmd+/), move lines (Alt+Up/Down), copy
lines (Shift+Alt+Up/Down), delete lines (Ctrl/Cmd+Shift+K). Tab and Shift+Tab work on every
selection.
CommentSyntaxandIncrementalTokenizer.commentSyntaxsay how a language comments; the built-in languages fill it in. - A Monaco-style undo surface:
canUndo,canRedo,pushUndoStop(),alternativeVersionId(compare it with the value stored at save time to know whether the document is dirty) andlastChangeKind(EditChangeKind.Edit,Undo,Redo,Flush). - Find and replace:
FindReplaceState.show(replace)andreplaceMode, Ctrl+H opens the panel in replace mode, and the panel has a toggle for the replace row. Searches stop atEditorLimits.maxFindMatches(10,000, shown as “10000+”), run in chunks on the web, and a new query cancels one still running. - Editor chrome, each switchable in
EditorOptions: the caret’s line is highlighted inEditorTheme.lineHighlight; the bracket pair the caret touches is boxed; and brackets are coloured by nesting depth from the newEditorTheme.bracketPairColors(VS Code’s colours by default;EditorThemeParserreadseditorBracketHighlight.foreground1to6). Brackets in strings and comments are left alone. Tab and Shift+Tab indent bytabSizespaces, or by a tab withoutinsertSpaces. - Sticky scroll (
EditorOptions.stickyScroll): the first lines of the blocks the top of the view is inside (up to five, from the folding provider’s ranges) stay pinned at the top, each pushed up as its block ends; a click scrolls to it. - A minimap (
EditorOptions.showMinimap): the whole text in small at the side, as blocks in its token colours, with a slider over what is shown; a press jumps there and dragging scrolls. It adds under half a millisecond per frame while scrolling 5,000 lines on the web. - Popups where the work is. With a hardware keyboard, completions open as a list at the caret
(Ctrl+Space asks for them; the arrows move through them, Enter or Tab takes one, Escape closes
it); touch devices keep the strip above the keyboard. Signature help sits above the caret,
quick fixes at their diagnostic, and all of them flip to the other side of the line or move
sideways to stay inside the window. Hover documentation (
LanguageService.hoverDoc, which the editor never showed before) appears when the mouse rests on a symbol for half a second, and from the touch menu’s “Info”. - Go to definition, references and format, which language services offered but the editor never
used. F12 or Ctrl/Cmd+click goes to the definition of the symbol there (holding Ctrl/Cmd
underlines a symbol that has one); a definition in another file goes to the new
CodeEditorLayout(onNavigateToLocation)for the host to open. Shift+F12 lists the references at the caret, to step through with the arrows and open with Enter or a click. Shift+Alt+F formats the selection (formatRange) or the whole document (format), changing only the lines that differ, as one undo step, so carets and folds elsewhere stay put. The right-click menu has all three. The mouse pointer is a text cursor over the text. - Diagnostics without a host.
CodeEditorLayout(diagnostics = null), the default, asks the language service 500 ms after the editor appears and after each pause in typing, drops any answer for text that has changed since it asked, and reports each list through the newonDiagnosticsChange. On the web the built-in languages’ checks pause for frames as they go. While the text is edited, every diagnostic (the host’s too) moves along with the text it marks instead of staying at its old offset until the next list. - Grammars as data:
DeclarativeGrammar.parse(json)reads a subset of Monaco’s Monarch (states, rules of regex, action and next state,cases, capture groups each with an action of its own,include,@rematch,defaultToken,ignoreCase,tokenPostfix,@nameregex attributes), andDeclarativeTokenizer(grammar)highlights with it, a line at a time and incrementally. Lookbehind is refused when the grammar is read, with where it is. Tokens get aNamedTokenType(name), dotted as in Monaco (tag.aardflex,comment.doc); the bare namescommentandstringget the editor’s own types, and any name with acommentorstringpart is a comment or a string to the editor (no bracket colours inside it). aardflex-web-app’s Monaco grammar reads unchanged and passes the cases its Monaco tests check. CodeEditorState.tokensForLine(line): the syntax tokens of one line, replacing the removedtokenCache.CodeEditorState.tokenizeris settable: switching the language of an open document keeps its text, selection and undo history. On the web,updateOptions({ language })now does this too.- The keyboard toolbar types through the typing rules, as a key does: its
(gets a). - Web:
@aardarch/aardink-webis published to npm with every release, through npm trusted publishing with provenance:pnpm add @aardarch/aardink-web. Pre-release versions go to thenextdist-tag. - Web: languages and themes of your own without building a wasm executable.
registerLanguagetakes a Monarch grammar (withextends: "xml"to keep a built-in language’s service) and JavaScript functions for completions, hover and diagnostics, answered at once or with a Promise;defineTheme/registerThemetake Monaco’s theme data or VS Code theme JSON, whose colours and font styles reach token names by dotted prefix (tag.aardflex, thentag). In Kotlin,AardinkWeb.registerLanguagewithWebLanguageProviders, andregisterTheme. - Web: more of Monaco’s editor API:
getSelections/setSelections,canUndo,canRedo,pushUndoStop,getAlternativeVersionId,format(),focus(),setBaseline(text)for the diff lane, a secondonDidChangeContentargument ({ versionId, isUndoing, isRedoing, isFlush }), the optionstabSize,insertSpaces,renderLineHighlight,renderWhitespace,bracketPairColorization,minimapandstickyScroll(in Monaco’s{ enabled }shape or as booleans), andtokenize(text, languageId)for debugging a grammar. - Web: the language’s own diagnostics show until the host calls
setDiagnostics;setDiagnostics(null)goes back to them, andAardinkWeb.onDiagnosticsChange(in the npm package,onDidChangeDiagnostics) reports them in the same Monaco-marker shape. - Web: a new editor waits up to half a second for its bundled font instead of flashing another
font first, and
preloadAardink()fetches the font early (AardinkWeb.preloadFont()).dispose()takes the editor’s element out of the page (the editor now mounts into an element of its own inside the container), which from Compose Multiplatform 1.13 releases it entirely. CodeEditorState.revalidate()collects the language service’s diagnostics again at once, without waiting for an edit, for when what they depend on outside the text has changed.- Web: more control for a registered language’s completions.
triggerCharacterson the providers (as on Monaco’s completion provider) open the list when typed, besides those of the language itextends.inheritCompletions: falsein the definition leaves out that language’s completions while keeping its diagnostics and folding, and an answer of Monaco’s{ suggestions }shape withexclusive: trueleaves them out for one request. Items take Monaco’sfilterText,sortTextandrange(the exact text an item replaces, so a provider no longer has to work around the characters the editor’s guess stops at), the kindstransformandcolorRef(or Monaco’sfunctionandcolor), anddocumentationas Monaco’s{ value }. A hover’sexampleis shown as code below it. - Web:
revalidate()(AardinkWeb.revalidate) collects the language’s own diagnostics again at once, for when what a registered language’sprovideDiagnosticsdepends on outside the text has changed. CodeEditorState.executeEdits(edits, selectionsAfter)andreplaceText(newText)apply edits, or a whole new text, as one undo step that keeps the history (unlikeloadText), each edit reduced to the characters it really changes so carets, folds and diagnostics in unchanged text stay put. Overlapping edits are refused.- Web: undoable programmatic edits.
replaceValue(text)replaces the whole text as one undo step that keeps the history, unlikesetValue, so an assistant’s rewrite can be undone without losing the user’s history; Monaco’sexecuteEdits(source, edits, endCursorState)applies ranges the same way, and returnsfalsein a read-only editor. Both reachonDidChangeContentwithisFlush: falseand a newversionId, andundo()returnsgetAlternativeVersionId()to its earlier value. In Kotlin,AardinkWeb.replaceValueandexecuteEditswithWebEditandWebRange.
Changed
- Breaking:
CodeEditorLayout’sreadOnly,softWrapandshow*parameters are now fields ofoptions: EditorOptions, anddiagnosticsis nullable:null, the default, collects them from the language service. - Breaking:
CodeDocumentimplementsCharSequenceand keeps its text in a gap buffer with a line index updated edit by edit, so a keystroke costs the size of the edit instead of a copy and a rescan of the whole document (10,000 keystrokes into 2 MB take ~20 ms on the JVM).textis built once per edit and shared. - Incremental tokenization. The built-in tokenizers remember their last scan and, after an
edit, rescan only from just before the change until the scan is back in step with the old one,
so a keystroke in a large file costs a few lines instead of the whole document. On the web a
large document now gets its visible lines highlighted first.
tokenizeLinesof a built-in tokenizer returns only the lines it rescanned, and recognises its own previous result aspreviousTokens; seeIncrementalTokenizer.tokenizeLines. A wrapper that post-processes the list keeps working, with full scans. - The first highlighting pass over a new document or language starts at once instead of after the typing debounce, and on the web a large document’s pass runs in slices of about 8 ms, so frames keep coming while it works.
- Language services and folding providers receive a read-only snapshot of the document, taken when the request was made, instead of the live document that a service running on the compute dispatcher could see change mid-request.
- Undo groups edits as Monaco does: a run of typing until the caret moves, the kind of edit changes or a space follows a word (so undo removes a word at a time), backspaces together, and every other edit on its own. Undo and redo restore the selections from before and after the step.
- Replace All is one undo step instead of one per match.
- The find panel shows its replace row only in replace mode.
- A new
CodeEditorStatestarts with the caret at the start of the text, as afterloadText, instead of at its end. - A touch selection gets the editor’s own menu (cut, copy, paste, select all, and “Info” with a language service) instead of the platform’s text toolbar, which cannot take items of an app’s own.
- Selections are drawn in
EditorTheme.selectionColorand find matches inEditorTheme.findMatchColor(the current one stronger), neither of which was used before; a find match inside a selection stays visible. - The diff lane is recomputed 300 ms after the last edit rather than on every keystroke.
- Web:
AardinkWeb.mountuses one shared language registry and theme map by default, whichregisterLanguageandregisterThemeadd to.
Removed
- Breaking:
CodeEditorState.textFieldState, theannotatedTextparameter ofCodeEditorLayout,EditorGutter,DrawScope.drawSquiggles,annotateTokens,applyFolding, andEditorTheme’s deprecatedfontFamily,fontSizeandlineHeight. Seedocs/MIGRATION_0.6.md. - Breaking:
TokenCacheandCodeEditorState.tokenCache. UseCodeEditorState.tokensForLine. - Breaking:
EditorUndoManager, itsEditOperationclasses, andCodeEditorState.undoManager. UseCodeEditorState.undo(),redo(),canUndoandcanRedo.
Fixed
- Fast typing lost keys in large documents on the web (about half of them at 10 keys a second past 1,000 lines). None are lost now.
- After a line was inserted or removed, lines below it could show the previous line’s syntax colours until the next tokenization pass, because cached tokens stayed keyed to their old line numbers. Colours now move with their text.
- The diff lane marked every line below an inserted line as modified; it now marks just the inserted one.
- Squiggles were placed by document offset against the folded layout, so they drifted once anything above them was folded; each is now drawn on its own line.
- Closed folds now follow edits above them instead of hiding the wrong lines until the next folding pass.
EditorThemeParsergave a built-in type the colour of whichever of its theme’s rules came last, so a theme with rules for bothkeywordandkeyword.controlcould colour every keyword as a control keyword. Each type now takes its most general rule, and the more specific ones colour the sub-names.EditorThemeParserread an 8-digit colour as#AARRGGBB; VS Code themes write#RRGGBBAA, so translucent colours such as a line highlight came out wrong. It also reads#RGBand#RGBAnow.- A touch tap in the web editor did nothing once a mouse had been over the page (a hovering mouse pointer never lifts, and the tap waited for it).
- The npm package kept a failed load of its WebAssembly module, so after one network error every
later
createEditor,registerLanguageandregisterThemeon the page failed too. The next call now loads again. Chrome still keeps a module whose download failed until the page is reloaded. - The web editor kept its size when only its container changed (a splitter dragged, a container
mounted into while
display: noneand shown later), so it was drawn at the old size and clicks landed in the wrong place. It now watches its container and follows it. - The find panel did not take the keyboard focus when it opened, from Ctrl+F, Ctrl+H or the web’s
showFind(), so what was typed next went into the document. It now focuses its find field (the replace field for Ctrl+H with something already to find). In its fields Enter goes to the next match, Shift+Enter to the previous one, and Escape closes the panel and gives the focus back to the text. - On touch screens the completion strip failed when two items had the same kind and label, as a
web language and the built-in one it
extendscan both offer. Such items now each get their own key, and a built-in language no longer repeats a completion the extending language offers. - XML and HTML offered element and attribute completions inside comments, CDATA sections,
<!DOCTYPE …>and<?xml …?>, so Enter in a comment could writeandroid:name=""into it. They offer none there now, and a<!--inside a closed CDATA section (or a<![CDATA[inside a closed comment) no longer turns them off for the rest of the document. - The touch chrome the editor draws itself (the keyboard toolbar, the completion strip above
it, the menu over a touch selection) and the right-click menu took the host’s Material colours,
so on the web, which has no Material theme, they were light over a dark editor. They now follow
the active
EditorTheme, built-in or registered: surfaces a shade off its background, text in its foreground, accents in its keyword colour.KeyboardToolbarDefaults.style()derives its defaults fromLocalEditorThemeaccordingly. The rest of the editor’s popups and panels do too: the completion list at the caret, the hover, signature help, references and code action cards, the find/replace panel and its fields, the diagnostic banner (now tinted with the theme’s error, warning or info colour), the go-to-line and rename dialogs and the attribution badge. Text in all of them is kept at 4.5:1 or more on its surface, lightened or darkened where a theme’s own colour is too dim (Solarized Dark’s text, VS Code Dark’s keyword blue).
Known issues
- Web: a disposed editor is not fully released (about 320 KB each) on Compose Multiplatform
1.12. Reuse one editor with
setValueandupdateOptionsrather than mounting one per view. Aardink adopts Compose Multiplatform 1.13, which fixes it, once it is stable. - Web: the console shows “Accessing
memoryviawasmExportsis deprecated” once, on load. Compose Multiplatform’s resources library (1.12.1) reads memory the way Kotlin 2.4.20 deprecates when it loads the bundled font; it is harmless. - Some checks need real devices and a person: input methods (CJK, dead keys, Gboard and Samsung
keyboards), touch selection, the mobile soft keyboard, browser zoom, TalkBack and NVDA. See the
checklist in
docs/WEB_INTEGRATION.md.
[0.6.0-rc3] - 2026-10-01
The third release candidate for 0.6.0, on Maven Central and on npm’s next tag. For the web: a
failed module load can be retried, the editor follows its container’s size, find takes the
keyboard focus, registered languages set their own completion trigger characters and can drop
their base language’s completions, completion items take Monaco’s range, filterText and
sortText, revalidate(), and replaceValue/executeEdits keep the undo history. Popups,
panels and touch chrome follow the editor theme. Its changes are listed under [0.6.0].
[0.6.0-rc2] - 2026-10-01
The second release candidate for 0.6.0, on Maven Central and on npm’s next tag, with
whitespace rendering, snippet completions, and theme colours and font styles by token sub-name.
Its changes are listed under [0.6.0].
[0.6.0-rc1] - 2026-09-27
A release candidate for 0.6.0, on Maven Central and on npm’s next tag. Its changes are
listed under [0.6.0].
[0.5.0] - 2026-09-27
Added
- Kotlin Multiplatform:
:editor,:languagesand:languages-lspnow build for JVM (desktop) and wasmJs (browser) in addition to Android. CodeEditorState.textFieldState, theBasicTextFieldinterop point for the newTextFieldStateinput model.com.aardarch.aardink.platform.EditorDispatchers— platform-appropriatecomputeandiodispatchers.Dispatchers.IOdoes not exist on wasmJs.com.aardarch.aardink.platform.PlatformInfoandKeyboardToolbarPlacement.platformDefault, so a host can take the toolbar placement and modifier-key convention that suit the platform.EditorTypographyandLocalEditorTypography— the editor’s font family and metrics are now provided rather than hardcoded.:editorbundles no font; the defaults are unchanged.CodeEditorLayout(onRequestGoToLine = ...), hardware-keyboard shortcuts (undo/redo, find, replace, go-to-line, Tab/Shift+Tab indent, Escape) and desktop/web scrollbars.CodeEditorState.indentSelection()/outdentSelection().WebSocketLspTransport(wasmJs only) — a browser transport forLspClient, alongside the existingStreamLspTransport(JVM/Android) andChannelLspTransport(common).- Public ABI validation for every library, with committed dumps under
*/api/. com.aardarch:aardink-editor-web(wasmJs), a browser bridge that mounts the editor into a DOM element behind a flatAardinkWebAPI (mount,getValue,setValue,patchOptions,onChange,setDiagnostics,dispose, …). It bundles JetBrains Mono (SIL OFL 1.1) for the font-less browser canvas, and comes with a compiled@JsExporttemplate for the app module that builds the executable. Seedocs/WEB_INTEGRATION.md.:sample-web, a browser harness, and the@aardarch/aardink-webnpm package it builds: the editor behind a Monaco-shapedcreateEditor(...)API, checked end to end in a real Vite app (tools/vite-smoke/).AardinkWeb.setResourceUrllets a bundler serve the bundled font from wherever it emitted it.IncrementalTokenizer.tokenizeFullCooperativeandEditorLimits— on a single-threaded host (wasmJs) documents over 64 KB are tokenized in chunks that yield to the event loop, and documents over 2 MB skip highlighting and folding. Both thresholds are configurable; Android and JVM tokenize off the UI thread and never consult them. The built-in tokenizers implement the chunked pass. The default delegates totokenizeFull, so custom tokenizers need no change.
Changed
- Packaging. Each library now publishes a Gradle Module Metadata root plus one artifact
per target instead of a single
.aar:com.aardarch:aardinkresolves toaardink-android,aardink-jvmoraardink-wasm-jsautomatically. Existing Android consumers keep writingimplementation("com.aardarch:aardink:<version>")unchanged — the mechanism is the onekotlinx.coroutinesalready relies on. A consumer that pinned the.aarclassifier explicitly would need to stop doing so. Compose itself now arrives through Compose Multiplatform 1.12.1, which resolves to the matching AndroidX Compose artifacts on Android. - The editor’s text input moved from the deprecated
BasicTextField(TextFieldValue)+VisualTransformationtoBasicTextField(TextFieldState)+InputTransformation/OutputTransformation.CodeEditorState’s public surface is unchanged. EditorThemeParserparses withkotlinx.serializationinstead of the Android-onlyorg.json, which makes:editorcompile as common Kotlin. This addskotlinx-serialization-jsonas:editor’s only non-Compose runtime dependency.LspClientsynchronises with aMutexinstead of@Synchronized/ConcurrentHashMap/AtomicLong, none of which exist in common Kotlin.:languagesno longer applies the Compose compiler plugin — it contains no Compose code.- The gutter sizes its line-number column by measuring the digits rather than assuming a fixed 0.7 em advance. The gutter is slightly narrower at the default font size.
Known issues
- Web: typing latency grows with document size. Comfortable for a few hundred lines,
noticeably laggy beyond ~1,000 lines of highlighted code (~1 s per keystroke at 5,000
lines). Compose lays out the whole text field as one paragraph on every change. Measurements
are in
docs/WEB_INTEGRATION.md#performance. - Web: a disposed editor is not fully released (~275 KB each), because Compose
Multiplatform 1.12.1 offers no way to tear down a
ComposeViewport. Reuse one editor withsetValue/updateOptionsrather than mounting one per view. - Several browser checks (IME composition, touch selection, mobile soft keyboard, some
browser shortcuts) have not yet been run on real devices; see the checklist in
docs/WEB_INTEGRATION.md.
Binary compatibility
CodeEditorLayoutgained a trailing defaultedonRequestGoToLineparameter. This is source-compatible but not binary-compatible: the Compose compiler encodes every parameter in a composable’s JVM signature, so any added parameter changes it and a consumer compiled against an earlier build must be recompiled. That is inherent to adding a parameter to a@Composableand applies to any future one too. Recompiling is all that is required; no source change.
Deprecated
applyFolding— superseded byEditorOutputTransformation; kept for hosts that built their own field around it.EditorTheme.fontFamily,EditorTheme.fontSizeandEditorTheme.lineHeight— these were never read by the editor. ProvideLocalEditorTypographyinstead.
Fixed
- Pasted or typed text now has CR LF (and a lone CR) normalised to LF. A paste from a Windows
or browser clipboard used to leave invisible carriage returns in the document, which the caret
could stop on, and mixed line endings. Text a host passes to
loadTextis still kept verbatim. - An input pass that changed no text (on the web, every programmatic update of the field is
echoed back this way) no longer advances
textVersion. It used to schedule a second tokenization and report the change to listeners twice. LspClientno longer skips connection teardown when the host cancels its scope while the receive loop is running, which could leave pending requests waiting forever and the transport open.LspClient.addDiagnosticsListener/removeDiagnosticsListenernow take effect before they return. Under lock contention they could previously be reordered or deferred past the notification they were registered for.LspClient.stop()no longer silently does nothing when its scope has already been cancelled.CodeEditorState.loadTextnow clears the text field’s own undo history after syncing the field, so a freshly loaded document can no longer be undone back to the previous one.- Tokenization no longer reads the token cache from a background thread while the main thread may be mutating it.
- The built-in regex tokenizers (Kotlin, TypeScript, JSON, TOML, CSS, Markdown) no longer take time quadratic in document size. Every rule was searched again from the cursor at every token, so a rule with no nearby match rescanned the rest of the document each time; one ~105 KB Kotlin file took over a minute on the JVM. Rule matches are now reused until the cursor passes them. Tokens are unchanged.
- Kotlin highlighting no longer freezes the browser. Its
class/object/interface/enumdeclaration-name rule used a lookbehind, which Kotlin/wasm’s regex engine evaluates at every position: a 3 KB file took about a second, and cost grew quadratically. Declaration names are now typed after scanning; 64 KB of Kotlin tokenizes in under 200 ms in the browser. Tokens are unchanged except in two contrived cases, a name after an@funannotation or after a line comment ending in “fun”, which were highlighted as functions and no longer are.
[0.4.1] - 2026-09-12
Changed
The Aardink icon in the sample app now styles better
[0.4.0] - 2026-09-07
Added
- TOML Language Support: Built-in TOML tokenizer (
TomlTokenizer), folding provider (TomlFoldingProvider), and language service (TomlLanguageService) supporting Android Version Catalog completions, duplicate key diagnostics, auto-closing, and formatting. Diagnostics, formatting and folding all read the document through one string-and-bracket scanner, so multiline values, inline comments after a table header, quoted keys containing=, and brackets or#inside a quoted value are treated as the data they are. Table headers are recognised only at the start of a line, so an array value such asdeps = ["a", "b"]is highlighted as strings rather than as a header. - Enhanced XML & HTML Services: Smart tag auto-closing (
<tag>-></tag>,</tag completion), Android XML element/attribute/value completions, duplicate attribute diagnostics, unescaped&warnings, and tag-depth formatting. Formatting re-indents whole-line markup only, leaving text nodes and mixed content verbatim; attribute names are scanned with quote state sofoo=inside a value is not an attribute; element and attribute names fold case only in HTML mode; a bare&inside an XML attribute value is flagged the same way as one in a text node, while HTML tolerates it and treats<script>and<style>contents as raw text; and a>typed in text content no longer auto-closes a tag that was already closed. - Enhanced JSON Service: Auto-closing (
{,[,"), value/property completions, duplicate key diagnostics on the decoded key so two escapes naming one member are one member, smart indentation, and 4-space JSON formatting. Completions after a comma follow the innermost container, skipping strings, so an array gets value completions and an object gets property completions, and an opening[offers values. - Kotlin Language Service: Built-in
KotlinLanguageServicewith syntax error diagnostics, dot completions for stdlib methods (map,filter,let,apply),@annotation completions (@Composable,@OptIn,@Preview),@Composablesnippets, auto-closing, and formatting. Character literals, raw strings and nested block comments are recognised as such, so a'}', a brace in a comment, or whitespace inside a"""string is never mistaken for structure, and a lone apostrophe in a backtick-quoted name opens no character literal when formatting. - Extended LSP Protocol Models & Contract:
TextEdit,Location,CodeAction,SignatureHelpdata models in:editor, and extendedLanguageServicedefault methods for code actions, definition lookup, references, signature help, range formatting, and rename refactorings.ParameterInformation.labelRangelets a provider say where a parameter sits in the signature, sofoo(Int, Int)highlights the parameter the server meant rather than the first text match.CompletionItem.additionalEditscarries edits that belong with a completion but sit elsewhere in the document, so accepting an auto-import completion inserts the symbol and its import in one undo batch.LanguageService.supportsRenamelets a host tell in advance whether a rename affordance is worth offering, sinceprepareRenameandrenameboth default to declining. - Atomic Multi-Edit Batch Application:
CodeEditorState.applyTextEdits()to apply multi-edit batches in reverse offset order and record them as single undoable operations inEditorUndoManager. Edits sharing an offset are applied last-to-first so that, as LSP requires, several inserts at one position appear in the order the batch lists them, and the selection is carried through the batch, so a rename or an import inserted above the caret moves it along instead of leaving it at an offset that now points into unrelated text. - LSP Editor UI Components: Floating
SignatureHelpPopupfor parameter hints,CodeActionMenupopup for quick fixes/refactorings, ”💡 Quick Fix” button inAnnotationTooltip, andRenameDialogprompt for symbol renaming. A rename whose edits arrive after the document has changed is discarded rather than applied to offsets that no longer mean what the server measured. The tooltip’s dismiss and quick-fix controls are real buttons with accessibility labels and minimum touch targets (as are the find panel’s arrow and close buttons), the rename dialog’s text field is labelled, and the signature-help popup is taken down when the host detaches the language service. A diagnostic banner and its quick-fix menu are dismissed by any edit, including the fix itself, since the diagnostic’s range no longer describes the text; a rename is dropped when the document was replaced while its dialog was open; and a completion list that outlives a keystroke has its provider-supplied ranges re-addressed, so an item accepted before the fresh list arrives still replaces the right span. - Rename & Completion Editor Hooks:
CodeEditorState.requestRename()opens the rename dialog for the symbol at an offset (resolved through the language service’sprepareRename, which also decides whether rename is offered at all - a null answer means the symbol cannot be renamed and no dialog opens), so a host app can drive the rename refactoring from its own menu;CompletionItem.replaceRangelets a provider state the exact range a completion replaces - used for a language server’stextEdit/InsertReplaceEdit- instead of the editor guessing a token boundary. - CSS LSP Sample: The sample app now demonstrates the external language-server bridge end to end with an in-process CSS language server, including initialization, document synchronization, CSS completions, hover text, and live
!importantdiagnostics. - External Language Server Bridge Module (
:languages-lsp): New published modulecom.aardarch:aardink-languages-lspshippingLspClient(coroutine JSON-RPC 2.0 client built on kotlinx-serialization, with multi-listenerpublishDiagnosticsfan-out, aninitialize/initializedhandshake helper, and generic replies toclient/registerCapability,window/showMessageRequestandworkspace/configurationserver requests),LspTransport(stdio/socket stream framing, with whole-frame writes serialised across concurrent senders), andLspLanguageService— a fullLanguageServiceadapter that maps completions, diagnostics, hover, formatting, code actions, definition/references, signature help, prepare-rename and rename onto LSP requests with offset ↔ line/column conversion. Completion trigger characters come from the server’s owncompletionProvider.triggerCharactersviaLspLanguageService.triggerCharactersFrom(), so member completion after.works; code actions that pair an edit with a command are omitted rather than applied by halves; and a closed connection stays closed, so a request sent afterwards fails instead of waiting for an answer that cannot arrive. A rename or code action whose workspace edit reaches another file, or creates, renames or deletes one, is declined outright rather than half-applied, and an action the server markeddisabledis never offered. The transport bounds the frame size it will allocate for, and a stream failure ends the connection instead of escaping the receive loop into the host’s uncaught-exception handler.core.Locationgained optionalline/columnfields so a cross-file definition/reference still carries a usable position.LspRenameSupportandLspLanguageService.renameSupportFrom()carry the server’srenameProvidercapability, so a server without rename support shows no rename command and one withoutprepareRenameis not asked for a range it cannot give; any failed write or read, not only anIOException, ends the connection, and the transport is closed exactly once; a client whose coroutine scope was cancelled fails requests instead of hanging them; an edit ending on a line past the last one reaches the end of the document, as a whole-file formatting edit intends; and a code-action request forwards only the diagnostics that actually overlap its range, not a neighbour that merely abuts it. The module’s only runtime dependencies beyond:editorare kotlinx-serialization-json and kotlinx-coroutines-core - it pulls in no Compose of its own.
[0.3.1] - 2026-08-29
Fixed
- Use compileDebugKotlin classpath for Dokka fix: keep commit lists as arrays in release scripts fix: update note on binary compatibility validator and enhance LanguageService description
[0.3.0] - 2026-06-22
Added
- Add adaptive launcher icons and update themes for Android 12+
- Refactor Dokka configuration and add version management
- Add spotless to sample app, floss files
- Add proper mascot to the sample app
Fixed
- Rename variable for user input in version prompt for clarity
[0.2.0] - 2026-05-12
Added
- Show find and replace functionality in sample editor screen
- Add dokka markdown output
Changed
- Simplify the sample ui logo/mascot placeholder
- Fix aardink namespace typo
[0.1.0] - 2026-05-12
Added
- Initial version 0.1.0 of Aardink