Skip to content

Command System Overview

SciFlow's editor core exposes a command layer via the @sciflow/editor-core/commands entry point. The system centralises command registration and projects three complementary APIs:

  • Immediate commands (runner.commands) execute right away and dispatch their transaction automatically.
  • Flows (runner.flow()) create a lazy pipeline; each call queues a command and run() commits the accumulated changes.
  • Inspection helpers (runner.available()) perform dry-runs. The returned object mirrors the command names and exposes flow() so you can compose non-dispatching pipelines.

Registering Commands

Commands are registered through registerCommands. Each entry is a factory that returns a ProseMirror-compatible command once invoked with its arguments.

Important: registerCommands now scopes commands to the editor instance that is currently being configured. Call it from inside a feature's initialize() hook (or anywhere that is run while Editor.create() is wiring features) so each editor only receives the commands it explicitly enables.

import type { Feature } from '@sciflow/editor-core';
import { Editor } from '@sciflow/editor-core';
import { registerCommands } from '@sciflow/editor-core/commands';

export const annotationCommandsFeature: Feature = {
  name: 'annotation-commands',
  initialize() {
    registerCommands({
      annotate: (color: string) => ({ state, dispatch, tr }) => {
        if (!dispatch) {
          // Just report whether the mark exists when called through `available()`.
          return Boolean(state.schema.marks.highlight);
        }

        const { from, to } = tr.selection;
        dispatch(
          tr.addMark(from, to, state.schema.marks.highlight.create({ color })),
        );
        return true;
      },
    });
  },
};

// Later, opt-in to the commands on any editor instance:
await Editor.create({
  docId: 'doc-123',
  sync,
  features: [annotationCommandsFeature],
});

Commands become available immediately after registration on all three command surfaces, but only for the editor (and command runner) that executed the feature initialization above.

Using Commands

const runner = editor.getCommands();

// Immediate execution
runner.commands.focus();
runner.commands.insertText('Hello world');

// Flow-based pipelines
runner.flow().focus().toggleMark('strong').run();

// Capability checks with optional flow composition
if (runner.available().toggleMark('em')) {
  runner.flow().toggleMark('em').run();
}

Inside a command you receive CommandProps, which include the editor, view, state, the mutable transaction, dispatch, and live accessors to commands, flow() and available().

Built-in Commands

Out of the box the core package registers:

Command Description
focus Focus the current editor view.
blur Remove focus from the editor view.
insertText Insert text at the current selection.
toggleMark Toggle a mark by name or MarkType, with extendEmptyMarkRange support.

Feature packages can register additional commands by invoking registerCommands inside their initialize() hook; the editor automatically opens and closes the registration window while it instantiates each feature.