import * as vscode from "vscode"; import { DocumentBinding } from "./binding"; import { Cursors } from "./cursors"; import { ForgeApi } from "./forge"; import type { EditorSessionInfo } from "./forge"; import { GitforFileSystem } from "./fs"; import { RoomClient } from "./room-client"; import { GitforScm } from "./scm"; import { registerSearch } from "./search"; import { registerContainer } from "./terminal"; const SESSION_COMMAND = "gitfor.session"; const REFRESH_COMMAND = "gitfor.refreshSession"; /** * gitfor-fs: the built-in web extension that turns the VS Code for the Web workbench into a gitfor * editor. It gets its ref-scoped token from the embedding page (never cookies), mounts the branch as * `gitfor:`, joins the branch's co-editing room and commits through the forge. */ export async function activate(context: vscode.ExtensionContext): Promise { const session = await vscode.commands.executeCommand(SESSION_COMMAND); if (!session) throw new Error("No gitfor editor session"); const api = new ForgeApi(session); const root = `/${session.owner}/${session.repo}`; const status = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 100); status.text = "$(sync~spin) Connecting"; status.show(); let scm: GitforScm | null = null; let cursors: Cursors | null = null; const room = new RoomClient(api.roomUrl(), session.token, { onControl(message) { if (message.type === "status") scm?.onStatus(message); if (message.type === "committed") { fs.refresh(); void vscode.window.showInformationMessage( `${message.by} committed ${message.sha.slice(0, 7)} to ${message.branch}${message.pullUrl ? " (pull request opened)" : ""}.`, ); } if (message.type === "error" && !message.path) void vscode.window.showWarningMessage(message.message); }, onState(state) { if (state === "online") status.text = `$(live-share) ${session.ref}`; if (state === "offline" || state === "connecting") status.text = "$(cloud-offline) Reconnecting"; if (state === "revoked") { status.text = "$(lock) Access revoked"; void vscode.window.showErrorMessage( "Your access to this editor session was revoked. Edits are no longer shared.", { modal: true, }, ); } updatePeers(); }, async refreshToken() { const next = await vscode.commands.executeCommand(REFRESH_COMMAND); if (!next) throw new Error("Couldn't refresh the session"); api.update(next); scm?.update(next); return next.token; }, }); const fs = new GitforFileSystem(api, room, root); context.subscriptions.push( vscode.workspace.registerFileSystemProvider("gitfor", fs, { isCaseSensitive: true }), { dispose: () => room.dispose() }, status, ); if (session.refKind !== "branch") { void vscode.window.showInformationMessage(`${session.ref} isn't a branch, so this session is read-only.`); } // A workbench without the search proposals enabled must not take the rest of the extension down with it. try { context.subscriptions.push(registerSearch(api, fs, room)); } catch (error) { console.error("gitfor search unavailable", error); } scm = new GitforScm(api, fs, session); cursors = new Cursors(room, (uri) => fs.pathOf(uri), session.user); context.subscriptions.push(scm, cursors); const bindings = new Map(); // Saving a bound editor writes exactly its own buffer, which is already streaming into the room. fs.isBound = (path, content) => [...bindings.values()].some((binding) => binding.path === path && binding.document.getText() === content); const bind = (document: vscode.TextDocument) => { const path = fs.pathOf(document.uri); if (!path || bindings.has(document.uri.toString())) return; void room.open(path).then( (meta) => { if (meta.kind !== "text" || bindings.has(document.uri.toString())) return; bindings.set(document.uri.toString(), new DocumentBinding(room, document, path)); }, () => undefined, ); }; vscode.workspace.textDocuments.forEach(bind); context.subscriptions.push( vscode.workspace.onDidOpenTextDocument(bind), vscode.workspace.onDidCloseTextDocument((document) => { bindings.get(document.uri.toString())?.dispose(); bindings.delete(document.uri.toString()); }), vscode.workspace.onDidChangeTextDocument((event) => bindings.get(event.document.uri.toString())?.onLocalChange(event), ), vscode.commands.registerCommand("gitfor.resolve", (path: string) => room.control({ type: "resolve", path })), ); function updatePeers(): void { const peers = cursors?.peers() ?? []; status.tooltip = peers.length ? `Editing with ${peers.join(", ")}` : "Nobody else is here yet"; const base = status.text.replace(/ \u00b7 \d+ others?$/, ""); status.text = peers.length ? `${base} \u00b7 ${peers.length} other${peers.length === 1 ? "" : "s"}` : base; } room.awareness.on("change", updatePeers); if (session.containers) registerContainer(context, api, room, session.startContainer === true); const initial = session.openPath?.replace(/^\/+/, "") || "README.md"; if (vscode.window.visibleTextEditors.length === 0 || session.openPath) { void vscode.workspace.fs.stat(fs.uri(initial)).then( () => vscode.commands.executeCommand("vscode.open", fs.uri(initial)), () => undefined, ); } } export function deactivate(): void { return; }