wsb-daemon 0.275.7622

wsb's daemon: the workbench, the GitHub mirror's schedule and peer continuity, for every workspace on the machine
<!--
  A unified diff, rendered.

  Only the rendering: what is being compared, and where the text came from, is
  the caller's business. Two things ask for this — the fleet's diff of a file
  against HEAD, and the diff between this machine's version of a conflicted
  file and a peer's — and a reader should not have to learn two layouts to
  read the same shape of answer.
-->
<script lang="ts">
  interface Props {
    /** The unified diff. Null while it is being fetched. */
    diff: string | null;
    /** What to say when the diff is empty rather than absent. */
    emptyMessage?: string;
    loading?: boolean;
    error?: string | null;
  }

  let {
    diff,
    emptyMessage = "No differences.",
    loading = false,
    error = null,
  }: Props = $props();

  type DiffLine =
    | { kind: "header"; text: string }
    | { kind: "hunk"; text: string }
    | { kind: "added"; text: string; lineNo: number }
    | { kind: "removed"; text: string; lineNo: number }
    | { kind: "context"; text: string; lineNo: number };

  function parseDiff(raw: string): DiffLine[] {
    const lines: DiffLine[] = [];
    let addedLine = 0;
    let removedLine = 0;
    // A `---` line is a header only BEFORE the first hunk. Inside one it is a
    // removed line whose content begins with `--`, and rendering that as a
    // header would show the reader a line their file does not contain.
    let inHunk = false;

    for (const text of raw.split("\n")) {
      if (!inHunk && (text.startsWith("diff ") || text.startsWith("index ") ||
          text.startsWith("--- ") || text.startsWith("+++ "))) {
        lines.push({ kind: "header", text });
      } else if (text.startsWith("@@")) {
        // Parse hunk header: @@ -a,b +c,d @@
        inHunk = true;
        const m = text.match(/@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
        if (m?.[1] && m[2]) {
          removedLine = parseInt(m[1], 10);
          addedLine = parseInt(m[2], 10);
        }
        lines.push({ kind: "hunk", text });
      } else if (text.startsWith("+")) {
        lines.push({ kind: "added", text: text.slice(1), lineNo: addedLine++ });
      } else if (text.startsWith("-")) {
        lines.push({ kind: "removed", text: text.slice(1), lineNo: removedLine++ });
      } else if (text.startsWith(" ") || text === "") {
        lines.push({ kind: "context", text: text.slice(1), lineNo: addedLine });
        addedLine++;
        removedLine++;
      }
    }

    return lines;
  }

  let parsed = $derived(diff !== null ? parseDiff(diff) : []);
  let isEmpty = $derived(!loading && !error && diff !== null && diff.trim() === "");
</script>

<div class="diff-viewer">
  {#if loading}
    <div class="diff-state">Loading…</div>
  {:else if error}
    <div class="diff-state diff-error">{error}</div>
  {:else if isEmpty}
    <div class="diff-state diff-empty">{emptyMessage}</div>
  {:else}
    <div class="diff-scroller">
      <table class="diff-table">
        <tbody>
          {#each parsed as line}
            {#if line.kind === "header"}
              <tr class="diff-header-row">
                <td class="diff-gutter"></td>
                <td class="diff-gutter"></td>
                <td class="diff-line diff-line--header">{line.text}</td>
              </tr>
            {:else if line.kind === "hunk"}
              <tr class="diff-hunk-row">
                <td class="diff-gutter"></td>
                <td class="diff-gutter"></td>
                <td class="diff-line diff-line--hunk">{line.text}</td>
              </tr>
            {:else if line.kind === "added"}
              <tr class="diff-added-row">
                <td class="diff-gutter"></td>
                <td class="diff-gutter diff-gutter--no">{line.lineNo}</td>
                <td class="diff-line diff-line--added">+{line.text}</td>
              </tr>
            {:else if line.kind === "removed"}
              <tr class="diff-removed-row">
                <td class="diff-gutter diff-gutter--no">{line.lineNo}</td>
                <td class="diff-gutter"></td>
                <td class="diff-line diff-line--removed">-{line.text}</td>
              </tr>
            {:else}
              <tr class="diff-context-row">
                <td class="diff-gutter diff-gutter--no">{line.lineNo}</td>
                <td class="diff-gutter diff-gutter--no">{line.lineNo}</td>
                <td class="diff-line diff-line--context"> {line.text}</td>
              </tr>
            {/if}
          {/each}
        </tbody>
      </table>
    </div>
  {/if}
</div>

<style>
  .diff-viewer {
    height: 100%;
    display: flex;
    flex-direction: column;
    background: var(--vscode-editor-background);
    color: var(--vscode-editor-foreground);
    font-family: var(--vscode-editor-font-family, monospace);
    font-size: var(--vscode-editor-font-size, var(--wb-font-size-12));
    line-height: 1.5;
  }

  .diff-state {
    padding: var(--wb-spacing-24);
    color: var(--vscode-disabledForeground);
    font-size: var(--wb-font-size-12);
  }

  .diff-error {
    color: var(--vscode-errorForeground);
  }

  .diff-empty {
    color: var(--vscode-disabledForeground);
  }

  .diff-scroller {
    flex: 1;
    overflow: auto;
  }

  .diff-table {
    width: 100%;
    border-collapse: collapse;
    table-layout: fixed;
  }

  .diff-gutter {
    width: var(--wb-control-size-44);
    min-width: var(--wb-control-size-44);
    padding: 0 var(--wb-spacing-8);
    text-align: right;
    color: var(--vscode-editorLineNumber-foreground);
    font-size: var(--wb-font-size-11);
    user-select: none;
    white-space: nowrap;
    vertical-align: top;
  }

  .diff-gutter--no {
    opacity: 0.6;
  }

  .diff-line {
    padding: 0 var(--wb-spacing-12);
    white-space: pre;
    word-break: keep-all;
    overflow: hidden;
  }

  .diff-header-row td {
    background: var(--vscode-editor-background);
    color: var(--vscode-disabledForeground);
    padding-top: var(--wb-spacing-8);
  }

  .diff-hunk-row td {
    background: color-mix(in srgb, #569cd6 10%, transparent);
    color: #569cd6;
    padding-top: var(--wb-spacing-4);
    padding-bottom: var(--wb-spacing-4);
  }

  .diff-added-row td {
    background: color-mix(in srgb, #73c991 12%, transparent);
  }

  .diff-line--added {
    color: #73c991;
  }

  .diff-removed-row td {
    background: color-mix(in srgb, #f14c4c 12%, transparent);
  }

  .diff-line--removed {
    color: #f14c4c;
  }

  .diff-context-row td {
    background: transparent;
  }

  .diff-line--context {
    color: var(--vscode-editor-foreground);
    opacity: 0.8;
  }

  .diff-line--hunk {
    color: #569cd6;
  }

  .diff-line--header {
    color: var(--vscode-disabledForeground);
    font-style: italic;
  }
</style>