wsb-daemon 0.275.7622

wsb's daemon: the workbench, the GitHub mirror's schedule and peer continuity, for every workspace on the machine
<script lang="ts">
  import { resolvePalette } from "@wsb/uikit/primitives";
  import { onMount } from "svelte";
  import editorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker";
  import jsonWorker from "monaco-editor/esm/vs/language/json/json.worker?worker";
  import cssWorker from "monaco-editor/esm/vs/language/css/css.worker?worker";
  import htmlWorker from "monaco-editor/esm/vs/language/html/html.worker?worker";
  import tsWorker from "monaco-editor/esm/vs/language/typescript/ts.worker?worker";
  import { ui } from "../../stores/ui.svelte.js";
  import { explorer as explorerStore } from "../../stores/explorer.svelte.js";
  import { commands } from "../../stores/commands.svelte.js";
  import type { WorkbenchTheme, TokenColorRule } from "../../api/types/workbench.js";
  import type { WorktreeId } from "../../api/scope.svelte.js";
  import { codeIntel } from "../../stores/code-intel.svelte.js";
  import { inWorktree } from "../../stores/per-worktree.svelte.js";
  import { SEMANTIC_FALLBACK_SCOPES, ruleForScope, toLspChanges } from "../../code/lsp-convert.js";
  import { acquire, builtinLanguages, install, mark, release, setLegend } from "../../code/monaco-code.js";
  import { scoped } from "../../stores/worktree-scope.svelte.js";
  const explorer = scoped(explorerStore);
  type Monaco = typeof import("monaco-editor");
  type MonacoEditorInstance =
    import("monaco-editor").editor.IStandaloneCodeEditor;

  function resolvedLength(
    element: HTMLElement,
    property: string,
    fallback: number,
  ): number {
    const value = Number.parseFloat(
      getComputedStyle(element).getPropertyValue(property),
    );
    return Number.isFinite(value) ? value : fallback;
  }

  function resolvedEditorDimensions(element: HTMLElement) {
    return {
      fontSize: resolvedLength(element, "--wb-monaco-font-size", 12),
      padding: {
        top: resolvedLength(element, "--wb-monaco-padding-block", 10),
        bottom: resolvedLength(element, "--wb-monaco-padding-block", 10),
      },
    };
  }

  function applyMonacoTheme(monacoInstance: Monaco, theme: WorkbenchTheme) {
    const base = theme.ui_theme.startsWith("hc") ? "hc-black" as const :
                 theme.ui_theme === "vs-dark" ? "vs-dark" as const : "vs" as const;

    const rules: import("monaco-editor").editor.ITokenThemeRule[] = [];
    const flat: { scope: string; foreground?: string; background?: string; fontStyle?: string }[] = [];
    for (const rule of (theme.token_colors as TokenColorRule[])) {
      const scopes = Array.isArray(rule.scope) ? rule.scope : [rule.scope];
      for (const scope of scopes) {
        flat.push({ scope, ...rule.settings });
        rules.push({
          token: scope,
          foreground: rule.settings.foreground?.replace("#", ""),
          background: rule.settings.background?.replace("#", ""),
          fontStyle: rule.settings.fontStyle,
        });
      }
    }
    // Semantic tokens are coloured by their type's rule; a theme of TextMate scopes names
    // none, so each type takes the colour of the scope it falls back to.
    for (const [type, scope] of Object.entries(SEMANTIC_FALLBACK_SCOPES)) {
      const rule = ruleForScope(flat, scope);
      if (rule) {
        rules.push({
          token: type,
          foreground: rule.foreground?.replace("#", ""),
          fontStyle: rule.fontStyle,
        });
      }
    }

    // The colours the page paints: the UI kit's palette under whatever theme
    // is in force — an extension theme's own, and the kit's for what it does
    // not name. Read as painted, so Monaco and the window around it agree.
    const colors = resolvePalette(document.documentElement);

    monacoInstance.editor.defineTheme("wsb-active", {
      base,
      inherit: true,
      rules,
      colors,
    });
    monacoInstance.editor.setTheme("wsb-active");
  }

  interface Props {
    value: string;
    language?: string;
    readOnly?: boolean;
    onchange: (value: string) => void;
    height?: string;
    /**
     * A line to put in the middle and select, 1-based.
     *
     * What a compiler error is FOR: `…/caps.rs:61:18` names a line, and a link
     * that opened the file at the top would leave the reader scrolling for the
     * thing they clicked on.
     */
    revealLine?: number | null;
    /** The column on that line, when the message named one. */
    revealColumn?: number | null;
    /**
     * The worktree file this editor shows, for omnicode's answers about it: its model is the
     * page's one model of that file, and its edits reach the language server.
     */
    file?: { worktree: WorktreeId; path: string } | null;
  }

  let {
    value,
    language = "plaintext",
    readOnly = false,
    onchange,
    height = "240px",
    revealLine = null,
    revealColumn = null,
    file = null,
  }: Props = $props();

  /**
   * Put the named line in the middle, whenever one is named.
   *
   * An effect rather than a one-off at creation: the same tab is reused when
   * somebody clicks a second error in the same file, and the editor is already
   * there by then.
   */
  $effect(() => {
    const line = revealLine;
    const column = revealColumn;
    if (!editor || line === null || line < 1) return;
    const model = editor.getModel();
    if (!model) return;
    const clamped = Math.min(line, model.getLineCount());
    editor.revealLineInCenter(clamped);
    editor.setPosition({ lineNumber: clamped, column: Math.max(1, column ?? 1) });
    editor.focus();
  });

  let container: HTMLDivElement | undefined = $state();
  // State, so what waits for the editor — the line to reveal, the value to show — runs once
  // Monaco has loaded and made it, not only before.
  let editor: MonacoEditorInstance | null = $state.raw(null);
  let monaco: Monaco | null = $state.raw(null);

  function registerExtensionLanguages(monaco: Monaco) {
    const registered = new Set(
      monaco.languages.getLanguages().map((entry) => entry.id),
    );

    for (const extensionLanguage of explorer.languages) {
      if (!registered.has(extensionLanguage.id)) {
        monaco.languages.register({
          id: extensionLanguage.id,
          aliases: extensionLanguage.aliases,
          extensions: extensionLanguage.extensions,
        });
        registered.add(extensionLanguage.id);
      }

      const configuration = toMonacoLanguageConfiguration(
        extensionLanguage.configuration,
      );
      if (configuration) {
        monaco.languages.setLanguageConfiguration(
          extensionLanguage.id,
          configuration,
        );
      }
    }
  }

  function toMonacoLanguageConfiguration(
    configuration: Record<string, unknown> | null,
  ): import("monaco-editor").languages.LanguageConfiguration | null {
    if (!configuration) {
      return null;
    }

    const next: import("monaco-editor").languages.LanguageConfiguration = {};

    if (Array.isArray(configuration.comments)) {
      next.comments = configuration.comments as import("monaco-editor").languages.CommentRule;
    } else if (isRecord(configuration.comments)) {
      next.comments = configuration.comments as import("monaco-editor").languages.CommentRule;
    }
    if (Array.isArray(configuration.brackets)) {
      next.brackets = configuration.brackets as [string, string][];
    }
    if (Array.isArray(configuration.autoClosingPairs)) {
      next.autoClosingPairs =
        configuration.autoClosingPairs as import("monaco-editor").languages.IAutoClosingPair[];
    }
    if (Array.isArray(configuration.surroundingPairs)) {
      next.surroundingPairs =
        configuration.surroundingPairs as import("monaco-editor").languages.IAutoClosingPair[];
    }
    if (Array.isArray(configuration.wordPattern)) {
      return next;
    }
    if (typeof configuration.wordPattern === "string") {
      next.wordPattern = new RegExp(configuration.wordPattern);
    }
    if (typeof configuration.autoCloseBefore === "string") {
      next.autoCloseBefore = configuration.autoCloseBefore;
    }
    if (Array.isArray(configuration.folding)) {
      return next;
    }
    if (isRecord(configuration.folding)) {
      next.folding = configuration.folding as import("monaco-editor").languages.FoldingRules;
    }

    return next;
  }

  function isRecord(value: unknown): value is Record<string, unknown> {
    return !!value && typeof value === "object" && !Array.isArray(value);
  }

  (self as typeof self & {
    MonacoEnvironment?: {
      getWorker(_: string, label: string): Worker;
    };
  }).MonacoEnvironment = {
    getWorker(_, label) {
      if (label === "json") return new jsonWorker();
      if (label === "css" || label === "scss" || label === "less") {
        return new cssWorker();
      }
      if (label === "html" || label === "handlebars" || label === "razor") {
        return new htmlWorker();
      }
      if (label === "typescript" || label === "javascript") {
        return new tsWorker();
      }
      return new editorWorker();
    },
  };

  onMount(() => {
    let disposable: { dispose(): void } | null = null;
    let tokenObserver: MutationObserver | null = null;
    let cancelled = false;
    let model: import("monaco-editor").editor.ITextModel | null = null;
    let unlisten: (() => void) | null = null;
    const code = file ? inWorktree(codeIntel, file.worktree) : null;

    (async () => {
      if (!container) return;
      monaco = await import("monaco-editor");
      if (!container || cancelled || !monaco) return;
      registerExtensionLanguages(monaco);

      // Define and apply the active workbench theme before creating the editor
      if (ui.appliedTheme) applyMonacoTheme(monaco, ui.appliedTheme);

      const dimensions = resolvedEditorDimensions(container);
      if (file) {
        model = acquire(monaco, file.worktree, file.path, value, language);
      }
      editor = monaco.editor.create(container, {
        ...(model ? { model } : { value, language }),
        "semanticHighlighting.enabled": true,
        inlayHints: { enabled: "on" },
        readOnly,
        automaticLayout: true,
        minimap: { enabled: false },
        scrollBeyondLastLine: false,
        wordWrap: "on",
        theme: "wsb-active",
        fontSize: dimensions.fontSize,
        fontFamily: "var(--wsb-font-mono, monospace)",
        padding: dimensions.padding,
      });

      tokenObserver = new MutationObserver(() => {
        if (!editor || !container) return;
        editor.updateOptions(resolvedEditorDimensions(container));
      });
      tokenObserver.observe(document.documentElement, { attributes: true });

      // Disable Monaco's native command palette entirely.
      // All commands go through the app-level command palette + registry.
      // F1 is Monaco's default keybinding for "Quick Command" — steal it.
      editor.addCommand(monaco.KeyCode.F1, () => {
        void commands.execute("workbench.action.showCommands");
      });
      // Override the "Command Palette" context menu action with a no-op ID
      // that has no contextMenuGroupId, effectively removing it from the menu.
      editor.addAction({
        id: "editor.action.quickCommand",
        label: "Command Palette",
        run: () => {
          void commands.execute("workbench.action.showCommands");
        },
      });

      disposable = editor.onDidChangeModelContent((event) => {
        if (file && code) code.edit(file.path, toLspChanges(event.changes));
        onchange(editor?.getValue() ?? "");
      });

      installCode();
      if (file && code && model) {
        const m = monaco;
        const shown = model;
        unlisten = code.onDiagnostics((path) => {
          if (path === file.path) void mark(m, file.worktree, path);
        });
        void code.open(file.path, () => shown.getValue()).then(() => mark(m, file.worktree, file.path));
      }
    })();

    return () => {
      cancelled = true;
      disposable?.dispose();
      tokenObserver?.disconnect();
      unlisten?.();
      editor?.dispose();
      if (file && code && model) {
        void code.close(file.path);
        release(model);
      }
      editor = null;
      monaco = null;
    };
  });

  /**
   * omnicode's providers, once `omni` says what it answers for: its token legend, its
   * trigger characters, and Monaco's own TypeScript features off where omnicode serves it.
   * Whichever comes last — Monaco loaded, or the `omni` described — installs them.
   */
  function installCode() {
    if (!file || !monaco) return;
    const omni = inWorktree(codeIntel, file.worktree).omni;
    if (!omni) return;
    setLegend(omni.legend);
    install(monaco, omni.completion_triggers, omni.signature_triggers);
    builtinLanguages(monaco, new Set(Object.keys(omni.languages)));
  }

  $effect(() => {
    if (file) void inWorktree(codeIntel, file.worktree).omni;
    installCode();
  });

  $effect(() => {
    if (!editor) return;
    const model = editor.getModel();
    if (!model) return;
    if (editor.getValue() !== value) {
      editor.setValue(value);
    }
  });

  $effect(() => {
    if (!editor || !monaco) return;
    registerExtensionLanguages(monaco);
    editor.updateOptions({ readOnly });
    monaco.editor.setModelLanguage(editor.getModel()!, language);
    // Re-applied once each theme is in force: the palette is read from the
    // page, so it is read after the theme has been put on it.
    if (ui.appliedTheme) applyMonacoTheme(monaco, ui.appliedTheme);
  });
</script>

<div class="monaco-shell" style:height bind:this={container}></div>

<style>
  .monaco-shell {
    width: 100%;
    border: 1px solid var(--vscode-widget-border);
    border-radius: var(--wsb-radius-sm);
    overflow: hidden;
    background: var(--vscode-input-background);
  }
</style>