code

A CodeMirror 6 wrapper.

Sometimes, it's nice to be able to just toss a code-editor in a web-page.

<tosi-code>'s value is the code it contains. Its mode attribute sets the language (javascript, typescript, tjs, css, html, markdown).

<tosi-code style="width: 100%; height: 100%" mode="css">
body {
  box-sizing: border-box;
}
</tosi-code>

Properties & events

member what it does
value the code in the editor (get/set)
mode javascript, typescript, tjs, ajs, css, html, markdown
disabled makes the editor read-only
original + showDiff(on) diff the current value against a baseline, as an overlay
diffResolvable make that overlay resolvable: each change gets keep/revert buttons, and the choices are applied to value when the diff closes
diffOriginalLabel / diffModifiedLabel what the two buttons say (default Original/Modified)
editor the underlying CodeMirror EditorView (undefined until loaded) — see Extending the editor below
undo() / redo() / canUndo() / canRedo() history control
tjsAutocomplete runtime-value autocomplete hooks (tjs mode) — see below
change event fires when the text changes; event.detail.value is the new text

In tjs/ajs mode the editor loads tjs-lang's CodeMirror language and completion source (if tjs-lang is installed — it's an optional peer). Set tjsAutocomplete to a TjsAutocompleteConfig and completion will suggest the real members of live runtime values — including proxy members no static analysis can see:

codeEl.tjsAutocomplete = { getLiveBindings: () => ({ app, elements }) }

Selective revert

showDiff(on) shows a read-only diff. Set diffResolvable first and the overlay becomes a review surface instead: every change carries keep/revert buttons, defaulting to keep, and whatever you leave alone survives. This is what <tosi-example>'s View changes uses — you tried four things and three worked, so revert the one you regret rather than all four.

The choices are applied when the diff closes, not as you click, and a change event fires if the text moved. That is deliberate: writing each choice straight back into value would feed the new text into the overlay's modified, and <tosi-diff> resets its decisions when either input changes — so live application would wipe the choices making it and renumber the hunks under the pointer.

codeEl.original = savedSource
codeEl.diffResolvable = true
codeEl.diffOriginalLabel = 'Source'
codeEl.diffModifiedLabel = 'Yours'
codeEl.showDiff(true)
// …user reverts a hunk or two…
codeEl.showDiff(false)   // codeEl.value now reflects their choices

Bundling

CodeMirror is a lazy chunk: with a bundler (ESM), a page that never uses <tosi-code> doesn't load it. This is not true of the IIFE (dist/iife.js) — bun's IIFE format cannot code-split, so CodeMirror is inlined there. That is a deliberate trade: the doc-system's editor (and its save-to-source flow) is the point of the IIFE, so it carries the editor. It costs ~376KB gzipped, up from ~118KB in 1.6.x.

Migrating from the ACE editor (pre-1.7)

1.7 replaced ACE with CodeMirror 6. value, original/showDiff(), mode and disabled are unchanged. Removed (each warns once, then no-ops):

removed replacement
theme style with --code-bg / --text-color
options (ACE-shaped) configure via editor (an EditorView)
ace there is no ACE global; use editor
editor.session.getUndoManager() undo() / redo() / canUndo() / canRedo()

editor changed type in place — it was an ACE Editor, it is now a CodeMirror EditorView. Code that reached into it needs revisiting; a grep for removed names won't catch this one.

Extending the editor

Import CodeMirror from tosijs-ui/codemirror, not from @codemirror/* directly:

import { gutter, GutterMarker, StateField, StateEffect } from 'tosijs-ui/codemirror'

const view = codeEl.editor // undefined until the editor has loaded
view?.dispatch({ effects: StateEffect.appendConfig.of([myField, myGutter]) })

This matters more than it looks. CodeMirror 6 keys facets, StateFields and gutters by object identity, not by module name. If your @codemirror/view resolves to a different copy than the one <tosi-code> uses — which a package manager will happily arrange when the versions don't dedupe — the view silently ignores your extension. No error, no warning; the gutter simply never renders, and every reading of your code says it should.

The re-export removes that failure by construction: there is only ever one copy, because it is the one this package resolves. Nothing to pin, no overrides to write.

Only @codemirror/state and @codemirror/view are re-exported — the two you need to extend a view. The language, lint and search packages are internal composition details; ask if you need one exposed.