import * as vscode from 'vscode'; import { SyncApi } from './api'; import { LiveSpace } from './liveSpace'; import { Version, VersionFiles, versionLabel } from './protocol'; export const VERSION_SCHEME = 'archify-sync-version'; /** Read-only documents of a version's files: `archify-sync-version:///`. */ export class VersionContent implements vscode.TextDocumentContentProvider { private readonly cache = new Map>(); constructor(private readonly api: (space: string) => SyncApi | undefined) {} files(space: string, version: number): Promise { const key = `${space}/${version}`; let found = this.cache.get(key); if (!found) { const api = this.api(space); if (!api) return Promise.reject(new Error('This model is not shared from here.')); found = api.versionFiles(space, version); found.catch(() => this.cache.delete(key)); this.cache.set(key, found); } return found; } async provideTextDocumentContent(uri: vscode.Uri): Promise { const [, space, version, ...rest] = uri.path.split('/'); const files = await this.files(space, Number(version)); return files.files.find((f) => f.path === rest.join('/'))?.text ?? ''; } } function describe(v: Version): vscode.QuickPickItem & { version: Version } { const when = new Date(v.created_at).toLocaleString(); const kind = v.kind === 'publish' ? 'published' : v.kind === 'restore' ? `restored ${v.restored_from ?? ''}`.trim() : 'snapshot'; return { label: `${v.kind === 'publish' ? '$(tag)' : v.kind === 'restore' ? '$(history)' : '$(circle-small)'} ${versionLabel(v)}${v.name ? ` ${v.name}` : ''}`, description: kind, detail: when, version: v, }; } /** Versions of a space: pick one, then compare with the files here or restore it. */ export async function showVersions(space: LiveSpace, api: SyncApi, content: VersionContent): Promise { const versions = await api.versions(space.link.space); if (!versions.length) { vscode.window.showInformationMessage(`Archify: ${space.name} has no versions yet. One is taken on its own after a pause in editing.`); return; } const picked = await vscode.window.showQuickPick(versions.map(describe), { title: `Archify | Versions of ${space.name}`, placeHolder: 'Pick a version' }); if (!picked) return; const v = picked.version; const action = await vscode.window.showQuickPick( [ { label: '$(diff) Compare with the files here', id: 'compare' }, { label: '$(history) Restore this version', id: 'restore', detail: 'Brings these files back for everyone, as a new version. Nothing is lost.' }, ], { title: `Archify | ${versionLabel(v)}${v.name ? ` ${v.name}` : ''}` }, ); if (action?.id === 'compare') await compareVersion(space, v, content); if (action?.id === 'restore') await restoreVersion(space, api, v); } /** Asks, then brings back a version's files for everyone as a new version. */ export async function restoreVersion(space: LiveSpace, api: SyncApi, v: Version): Promise { const ok = await vscode.window.showWarningMessage( `Restore ${versionLabel(v)} of ${space.name} for everyone?`, { modal: true, detail: 'The files go back to this version for everyone in the space. It becomes a new version; the history stays.' }, 'Restore', ); if (ok !== 'Restore') return false; const restored = await api.restore(space.link.space, v.id); vscode.window.showInformationMessage(`Archify: restored ${versionLabel(v)} as ${versionLabel(restored)}.`); return true; } /** Lists the files that differ from a version and opens the diff of the one picked. */ export async function compareVersion(space: LiveSpace, v: Version, content: VersionContent): Promise { const files = await content.files(space.link.space, v.id); const here = new Map(); for (const [rel, text] of space.doc.getMap<{ toString(): string }>('files').entries()) here.set(rel, text.toString()); const paths = [...new Set([...files.files.map((f) => f.path), ...here.keys()])].sort(); const changed = paths .map((p) => ({ path: p, then: files.files.find((f) => f.path === p)?.text, now: here.get(p) })) .filter((f) => f.then !== f.now); if (!changed.length) { vscode.window.showInformationMessage(`Archify: the files here are the same as ${versionLabel(v)}.`); return; } const pick = await vscode.window.showQuickPick( changed.map((f) => ({ label: f.path, description: f.then === undefined ? 'added since' : f.now === undefined ? 'removed since' : 'changed', path: f.path })), { title: `Archify | Changes since ${versionLabel(v)}`, placeHolder: `${changed.length} file(s) differ` }, ); if (!pick) return; const left = vscode.Uri.from({ scheme: VERSION_SCHEME, path: `/${space.link.space}/${v.id}/${pick.path}` }); const right = vscode.Uri.joinPath(space.root, ...pick.path.split('/')); await vscode.commands.executeCommand('vscode.diff', left, right, `${pick.path} (${versionLabel(v)} ↔ here)`); }