122 lines · 4.4 KB
import * as vscode from "vscode";
import type * as Y from "yjs";
import type { RoomClient } from "./room-client";

const LOCAL = "vscode-local";

/**
 * Two-way binding between one VS Code TextDocument and its Y.Text. Local edits become Yjs ops; remote
 * ops become WorkspaceEdits. If an interleaving ever leaves the two out of step, the document is
 * re-synced from the shared text, which is the source of truth.
 */
export class DocumentBinding implements vscode.Disposable {
  private applying = 0;
  private readonly observer: (event: Y.YTextEvent, transaction: Y.Transaction) => void;
  private chain: Promise<void> = Promise.resolve();
  private pending = false;
  /** Remote deltas already in the Y.Text but not yet in the document; local offsets are mapped through them. */
  private unapplied: Array<Y.YTextEvent["delta"]> = [];
  private readonly text: Y.Text;

  constructor(
    private readonly room: RoomClient,
    readonly document: vscode.TextDocument,
    readonly path: string,
  ) {
    this.text = room.text(path);
    this.observer = (event, transaction) => {
      if (transaction.origin === LOCAL) return;
      // The room evicted this clean file to free capacity; keep the editor as is and reseed it from head.
      if (!room.files.has(path)) {
        room.control({ type: "open", path });
        return;
      }
      this.unapplied.push(event.delta);
      this.queueRemote();
    };
    this.text.observe(this.observer);
    if (document.getText() !== this.text.toString()) this.queueRemote();
  }