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,    // `$$` -> `$ $`
  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 */ },
  },
});

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, drop diagnostics that belong to package dependencies or other files, and briefly suppress diagnostics over a region the user just edited (re-emitted on the next clean compile) to avoid flicker. Tune the window with suppressEditedWithinMs.

Composing extensions individually

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

import {
  forwardEdits,
  highlightExtension,
  autocompleteExtension,
  tooltipExtension,
  definitionExtension,
  dollarExtension,
} 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