0.10.0: Archify Sync, optional live collaboration on a model
Several people edit one model at once through a server you run yourself (archify-vscode-ext-sync); the files stay ordinary files on each disk. Sign-in through the browser, offline-first with later merge, teammates' cursors in the Source view and text editors, and a Shared section on the model page with people, versions, Compare and Restore. Without a server nothing changes: the sync code is a separate bundle that loads only when a model is shared, after a sign-in, or when a sync command or invite link is used.
This commit is contained in:
1 parent
a9021312b0
commit
c99f3e5fe8
27 files changed
+2899
-14
No files matched your search
@@ -0,0 +1,100 @@
|
||||
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:/<space>/<version>/<path>`. */
|
||||
export class VersionContent implements vscode.TextDocumentContentProvider {
|
||||
private readonly cache = new Map<string, Promise<VersionFiles>>();
|
||||
|
||||
constructor(private readonly api: (space: string) => SyncApi | undefined) {}
|
||||
|
||||
files(space: string, version: number): Promise<VersionFiles> {
|
||||
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<string> {
|
||||
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<void> {
|
||||
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<boolean> {
|
||||
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<void> {
|
||||
const files = await content.files(space.link.space, v.id);
|
||||
const here = new Map<string, string>();
|
||||
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)`);
|
||||
}
|
||||
Reference in new issue
Block a user