Onykia Docs

CodeMirror

@mudomi/onykia-codemirror are the CodeMirror 6 bindings for the engine.

npm install @mudomi/onykia-codemirror

CodeMirror 6 is a peer dependency - install the modules you use (@codemirror/view, @codemirror/state, @codemirror/commands, @codemirror/lint, @codemirror/language, @codemirror/autocomplete, @lezer/highlight).

Setup

typstExtensions(core, path) assembles the full feature set for one file and returns { extensions }. It is async because syntax highlighting fetches the compiler's tag table once.

import { EditorView, lineNumbers, keymap } from '@codemirror/view';
import { EditorState } from '@codemirror/state';
import { defaultKeymap, history, historyKeymap } from '@codemirror/commands';
import { lintGutter } from '@codemirror/lint';
import { typstExtensions, diagnosticsSubscription } from '@mudomi/onykia-codemirror';
 
const PATH = '/main.typ';
const INITIAL = '= Hello, Onykia x CodeMirror\n';
 
// `core` is an already-booted engine (see ./Quickstart). Create the file
// first so highlighting and autocomplete have something to read.
await core.create(PATH, 'text/x-typst', INITIAL);
 
const { extensions } = await typstExtensions(core, PATH);
 
const view = new EditorView({
  parent: document.getElementById('editor')!,
  state: EditorState.create({
    doc: INITIAL,
    extensions: [
      lineNumbers(),
      history(),
      lintGutter(),
      keymap.of([...defaultKeymap, ...historyKeymap]),
      ...extensions,
    ],
  }),
});
 
// Diagnostics are NOT wired by typstExtensions - own that side effect
// explicitly. This subscribes to the engine and pushes errors into the gutter.
diagnosticsSubscription(core, view, PATH);
 
await core.setMain(PATH);
await core.setTarget('svg');

typstExtensions includes a forwardEdits extension, so every keystroke is turned into a core.edit() delta automatically - you do not diff the document yourself. The engine recompiles and emits fresh pages/diagnostics.

What typstExtensions enables

Each feature can be toggled via the options argument; all default to on.

const { extensions } = await typstExtensions(core, PATH, {
  forwardEdits: true,      // push document changes to core.edit()
  autocomplete: true,      // package, symbol, label, font completions
  tooltip: true,           // hover docs
  definition: true,        // Ctrl/Cmd+click go-to-definition
  dollarAutoPair: true,    // `$$` -> `$ $`
  folding: true,           // fold ranges for blocks and heading sections
  highlight: {},           // styling overrides, or `false` to disable
});

For definition you can pass handlers instead of true to control how cross-file or URL jumps are resolved:

await typstExtensions(core, PATH, {
  definition: {
    // called when a definition lands in another VFS file
    openPath: (path, pos) => { /* switch editors, focus pos */ },
  },
});

Folding

folding supplies fold ranges for code blocks and heading sections. To show the controls, add CodeMirror's own gutter:

import { foldGutter } from '@codemirror/language';
// ...extensions: [foldGutter(), ...extensions]

Diagnostics

Diagnostics are deliberately left out of typstExtensions so the editor owns them explicitly. Two entry points:

  • diagnosticsSubscription(core, view, path) - subscribes to onDiagnostics and applies them; returns an unsubscribe function.
  • applyDiagnostics(view, diagnostics, path) - apply one batch yourself if you already manage the subscription.

Both convert the engine's byte offsets to CodeMirror positions and drop diagnostics that belong to package dependencies or other files. To stop diagnostics flickering over a region the user just edited, add modificationTracker(path) to your extensions; tune the window with { suppressEditedWithinMs } (default 250).

Collaborative editing (Yjs / Automerge)

Keep forwardEdits on when CodeMirror is the source of truth. With a CRDT, it depends on how changes are applied:

  • If the CRDT binding applies local and remote changes as CodeMirror transactions (the usual Yjs setup), forwardEdits still sees every mutation - leave it on.
  • Otherwise, disable forwardEdits and drive pushEditToCore from the CRDT, so engine state follows the CRDT rather than the editor.
import { pushEditToCore, primeFile } from '@mudomi/onykia-codemirror';
 
const { extensions } = await typstExtensions(core, PATH, { forwardEdits: false });
await primeFile(core, PATH, view); // seed the VFS with the current buffer
 
// on each CRDT change, with the text *before* the change (char offsets):
await pushEditToCore(core, PATH, oldText, [{ range: { start, end }, replacement }]);

pushEditToCore and forwardEdits share one per-path queue, so pendingEdit(path) resolves once in-flight edits have applied.

Composing extensions individually

If you don't want the bundle, import the pieces:

import {
  forwardEdits,
  highlightExtension,
  autocompleteExtension,
  tooltipExtension,
  definitionExtension,
  dollarExtension,
  foldingExtension,
} from '@mudomi/onykia-codemirror';

highlightExtension(core, path, options) is async (it loads the tag table); the rest are synchronous and take (core, path).

Extras

  • Spellcheck - spellcheckExtension(...) plus wireCoreSpellcheck(core, ...) delegate spell checking to a JS SpellChecker you provide (the engine ships no built-in dictionary).
  • Collaborative cursors - awarenessCursorExtension({ ... }) renders remote cursors from an awareness transport (e.g. a CRDT presence channel).
  • Drag & drop - dropFileExtension({ ... }) resolves dropped files and writes them into the VFS via your DropResolver.

On this page