All files / src/packlets/command-registry index.ts

93.02% Statements 40/43
90% Branches 9/10
90% Functions 18/20
94.73% Lines 36/38

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123                                                    18x 18x     21x 1x   20x 20x 20x 5x 5x         4x       3x 3x 2x   1x       16x       2x       7x         2x     3x       3x 2x 3x                     6x 6x 6x       7x 7x   7x 8x 8x   8x 5x 5x       7x       7x                 1x      
/**
 * @packageDocumentation
 *
 * Typed command registry for editor actions. Commands are fire-and-forget
 * units of work with metadata (title, shortcut) suitable for toolbars,
 * keyboard shortcuts, and command palettes.
 *
 * The registry is intentionally decoupled from React and the editor core.
 * It only knows how to store and dispatch commands; what a command does
 * is defined at registration time via closures.
 */
 
import { createNanoEvents } from "nanoevents";
import type { Emitter } from "nanoevents";
import { createKeybindingsHandler } from "tinykeys";
import type { KeyBindingMap } from "tinykeys";
 
export interface Command {
  id: string;
  title: string;
  shortcut?: string;
  shortcutMac?: string;
  execute: () => void;
}
 
export class CommandRegistry {
  private commands = new Map<string, Command>();
  private emitter: Emitter<{ change: () => void }> = createNanoEvents();
 
  register(command: Command): () => void {
    if (this.commands.has(command.id)) {
      throw new Error(`Command "${command.id}" is already registered`);
    }
    this.commands.set(command.id, command);
    this.emitter.emit("change");
    return () => {
      this.commands.delete(command.id);
      this.emitter.emit("change");
    };
  }
 
  get(id: string): Command | undefined {
    return this.commands.get(id);
  }
 
  execute(id: string): void {
    const command = this.commands.get(id);
    if (!command) {
      throw new Error(`Command "${id}" not found`);
    }
    command.execute();
  }
 
  getAll(): Command[] {
    return Array.from(this.commands.values());
  }
 
  findByShortcut(shortcut: string): Command | undefined {
    return this.getAll().find((c) => c.shortcut === shortcut);
  }
 
  subscribe(cb: () => void): () => void {
    return this.emitter.on("change", cb);
  }
}
 
export class CommandSet {
  private commands: Command[] = [];
 
  add(command: Command): void {
    this.commands.push(command);
  }
 
  registerTo(registry: CommandRegistry): () => void {
    const unregisters = this.commands.map((c) => registry.register(c));
    return () => {
      unregisters.forEach((fn) => fn());
    };
  }
}
 
export class KeyboardShortcutHandler {
  private registry: CommandRegistry;
  private handler: (event: Event) => void = () => {};
  private unsubRegistry?: () => void;
 
  constructor(options: { registry: CommandRegistry }) {
    this.registry = options.registry;
    this.unsubRegistry = this.registry.subscribe(() => this.refresh());
    this.refresh();
  }
 
  private refresh() {
    const bindings: KeyBindingMap = {};
    const isMac = navigator.platform.includes("Mac");
 
    for (const command of this.registry.getAll()) {
      const shortcut = isMac && command.shortcutMac ? command.shortcutMac : command.shortcut;
      Iif (!shortcut) continue;
 
      bindings[shortcut] = (event) => {
        event.preventDefault();
        command.execute();
      };
    }
 
    this.handler = createKeybindingsHandler(bindings);
  }
 
  onKeyDown(event: KeyboardEvent): void {
    this.handler(event);
  }
 
  dispose() {
    this.unsubRegistry?.();
  }
}
 
/** Global singleton for the active editor instance. */
export const globalCommandRegistry = new CommandRegistry();
 
export { CommandPalette } from "./palette";