@jxsuite/studio 0.36.0 → 0.37.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/iframe-entry.js +18 -9
- package/dist/iframe-entry.js.map +4 -4
- package/dist/studio.js +11525 -2877
- package/dist/studio.js.map +64 -17
- package/package.json +4 -2
- package/src/canvas/canvas-render.ts +85 -12
- package/src/canvas/iframe-host.ts +85 -0
- package/src/canvas/iframe-overlay.ts +30 -1
- package/src/collab/collab-session.ts +903 -0
- package/src/collab/collab-state.ts +71 -0
- package/src/collab/monaco-cursors.ts +69 -0
- package/src/collab/presence-chips.ts +58 -0
- package/src/files/file-ops.ts +6 -0
- package/src/files/files.ts +6 -0
- package/src/panels/git-panel.ts +5 -0
- package/src/panels/toolbar.ts +13 -3
- package/src/platforms/cloud.ts +31 -0
- package/src/platforms/devserver.ts +37 -0
- package/src/studio.ts +17 -0
- package/src/tabs/doc-op-apply.ts +5 -81
- package/src/tabs/patch-ops.ts +7 -23
- package/src/tabs/transact.ts +138 -12
- package/src/types.ts +8 -0
- package/src/workspace/workspace.ts +4 -0
package/src/tabs/transact.ts
CHANGED
|
@@ -79,22 +79,64 @@ export type JxNodeValue =
|
|
|
79
79
|
|
|
80
80
|
// ─── Transactional layer ─────────────────────────────────────────────────────
|
|
81
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Where a transaction came from: a direct user/tool edit, the undo/redo machinery replaying
|
|
84
|
+
* recorded ops, or a remote collaborator's ops applied locally. Observers use this to avoid
|
|
85
|
+
* republishing what they themselves applied.
|
|
86
|
+
*/
|
|
87
|
+
export type TransactOrigin = "user" | "history" | "remote";
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Module-level hook invoked at the end of every transactDoc (after the canvas patch apply and the
|
|
91
|
+
* dirty mark). Registered by the collab layer to mirror local edits into a shared document; at most
|
|
92
|
+
* one observer exists. `record.docOps` may be empty for un-instrumented mutations — observers must
|
|
93
|
+
* handle that (e.g. by diffing).
|
|
94
|
+
*/
|
|
95
|
+
export type TransactObserver = (
|
|
96
|
+
tab: Tab,
|
|
97
|
+
record: TransactionRecord,
|
|
98
|
+
origin: TransactOrigin,
|
|
99
|
+
) => void;
|
|
100
|
+
|
|
101
|
+
let _transactObserver: TransactObserver | null = null;
|
|
102
|
+
|
|
103
|
+
export function setTransactObserver(fn: TransactObserver | null) {
|
|
104
|
+
_transactObserver = fn;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Module-level gate consulted at transactDoc entry: return a refusal reason to block the mutation
|
|
109
|
+
* (the collab layer soft-freezes structural editing while a peer holds source-canonical), or null
|
|
110
|
+
* to proceed. Remote-origin transactions always pass — they ARE the frozen representation's
|
|
111
|
+
* mirror.
|
|
112
|
+
*/
|
|
113
|
+
export type TransactGate = (tab: Tab, origin: TransactOrigin) => string | null;
|
|
114
|
+
|
|
115
|
+
let _transactGate: TransactGate | null = null;
|
|
116
|
+
|
|
117
|
+
export function setTransactGate(fn: TransactGate | null) {
|
|
118
|
+
_transactGate = fn;
|
|
119
|
+
}
|
|
120
|
+
|
|
82
121
|
/**
|
|
83
122
|
* Apply a document mutation transactionally: push to history and mark dirty. The mutationFn
|
|
84
123
|
* receives the tab and should mutate tab.doc.document in place.
|
|
85
124
|
*
|
|
86
125
|
* @param {Tab | null} tab
|
|
87
126
|
* @param {(tab: Tab) => void} mutationFn
|
|
88
|
-
* @param {{ skipHistory?: boolean }} [opts]
|
|
127
|
+
* @param {{ skipHistory?: boolean; origin?: TransactOrigin }} [opts]
|
|
89
128
|
*/
|
|
90
129
|
export function transactDoc(
|
|
91
130
|
tab: Tab | null,
|
|
92
131
|
mutationFn: (tab: Tab) => void,
|
|
93
|
-
{ skipHistory = false }: { skipHistory?: boolean } = {},
|
|
132
|
+
{ skipHistory = false, origin = "user" }: { skipHistory?: boolean; origin?: TransactOrigin } = {},
|
|
94
133
|
) {
|
|
95
134
|
if (!tab) {
|
|
96
135
|
return;
|
|
97
136
|
}
|
|
137
|
+
if (origin !== "remote" && _transactGate?.(tab, origin)) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
98
140
|
const selectionBefore = tab.session.selection ? [...tab.session.selection] : null;
|
|
99
141
|
beginRecording();
|
|
100
142
|
let record: TransactionRecord;
|
|
@@ -141,6 +183,8 @@ export function transactDoc(
|
|
|
141
183
|
}
|
|
142
184
|
|
|
143
185
|
tab.doc.dirty = true;
|
|
186
|
+
|
|
187
|
+
_transactObserver?.(tab, record, origin);
|
|
144
188
|
}
|
|
145
189
|
|
|
146
190
|
/**
|
|
@@ -215,16 +259,38 @@ function materializeState(
|
|
|
215
259
|
*
|
|
216
260
|
* @param {Tab | null} tab
|
|
217
261
|
* @param {(doc: JxMutableNode) => void} fn
|
|
218
|
-
* @param {{ skipHistory?: boolean }} [opts]
|
|
262
|
+
* @param {{ skipHistory?: boolean; origin?: TransactOrigin }} [opts]
|
|
219
263
|
*/
|
|
220
264
|
export function transact(
|
|
221
265
|
tab: Tab | null,
|
|
222
266
|
fn: (doc: JxMutableNode) => void,
|
|
223
|
-
opts?: { skipHistory?: boolean },
|
|
267
|
+
opts?: { skipHistory?: boolean; origin?: TransactOrigin },
|
|
224
268
|
) {
|
|
225
269
|
transactDoc(tab, (t) => fn(t.doc.document), opts);
|
|
226
270
|
}
|
|
227
271
|
|
|
272
|
+
/**
|
|
273
|
+
* Apply externally-produced doc ops (a remote collaborator's edits) through the normal transaction
|
|
274
|
+
* pipeline: the canvas patches surgically exactly as it does for undo/redo replay, panels' effects
|
|
275
|
+
* fire, and — because the origin is "remote" — the transact observer will not republish them.
|
|
276
|
+
* History is skipped; while a collab session is attached, undo is delegated (see
|
|
277
|
+
* setHistoryDelegate) and local-only.
|
|
278
|
+
*
|
|
279
|
+
* @param {Tab} tab
|
|
280
|
+
* @param {JxDocOp[]} ops
|
|
281
|
+
*/
|
|
282
|
+
export function applyExternalDocOps(tab: Tab, ops: JxDocOp[]) {
|
|
283
|
+
transactDoc(
|
|
284
|
+
tab,
|
|
285
|
+
(t) => {
|
|
286
|
+
for (const op of ops) {
|
|
287
|
+
applyDocOp(t, op);
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
{ origin: "remote", skipHistory: true },
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
|
|
228
294
|
// ─── Document-op application ─────────────────────────────────────────────────
|
|
229
295
|
|
|
230
296
|
/** Document-level keys whose changes require a full scope/panel rebuild on the canvas. */
|
|
@@ -289,26 +355,40 @@ function applyDocOp(tab: Tab, op: JxDocOp) {
|
|
|
289
355
|
|
|
290
356
|
let _batchTab: Tab | null = null;
|
|
291
357
|
|
|
358
|
+
/** Module-level hook invoked when a batch closes (collab flushes its buffered ops as one step). */
|
|
359
|
+
export type BatchEndNotifier = (tab: Tab) => void;
|
|
360
|
+
|
|
361
|
+
let _batchEndNotifier: BatchEndNotifier | null = null;
|
|
362
|
+
|
|
363
|
+
export function setBatchEndNotifier(fn: BatchEndNotifier | null) {
|
|
364
|
+
_batchEndNotifier = fn;
|
|
365
|
+
}
|
|
366
|
+
|
|
292
367
|
export function beginBatch(tab: Tab | null) {
|
|
293
368
|
_batchTab = tab;
|
|
294
369
|
}
|
|
295
370
|
|
|
296
371
|
export function endBatch() {
|
|
297
|
-
|
|
298
|
-
|
|
372
|
+
const tab = _batchTab;
|
|
373
|
+
// A history delegate owns undo grouping while registered; the snapshot push would be dead weight.
|
|
374
|
+
if (tab && !_historyDelegates.has(tab)) {
|
|
375
|
+
const raw = toRaw(tab.doc.document);
|
|
299
376
|
const snapshot = {
|
|
300
377
|
document: jsonClone(raw),
|
|
301
|
-
selection:
|
|
378
|
+
selection: tab.session.selection ? [...tab.session.selection] : null,
|
|
302
379
|
};
|
|
303
|
-
const truncated =
|
|
380
|
+
const truncated = tab.history.snapshots.slice(0, tab.history.index + 1);
|
|
304
381
|
truncated.push(snapshot);
|
|
305
382
|
if (truncated.length > HISTORY_LIMIT) {
|
|
306
383
|
truncated.shift();
|
|
307
384
|
}
|
|
308
|
-
|
|
309
|
-
|
|
385
|
+
tab.history.snapshots = truncated;
|
|
386
|
+
tab.history.index = truncated.length - 1;
|
|
310
387
|
}
|
|
311
388
|
_batchTab = null;
|
|
389
|
+
if (tab) {
|
|
390
|
+
_batchEndNotifier?.(tab);
|
|
391
|
+
}
|
|
312
392
|
}
|
|
313
393
|
|
|
314
394
|
export function isBatching(): boolean {
|
|
@@ -317,6 +397,42 @@ export function isBatching(): boolean {
|
|
|
317
397
|
|
|
318
398
|
// ─── Undo / Redo ─────────────────────────────────────────────────────────────
|
|
319
399
|
|
|
400
|
+
/**
|
|
401
|
+
* Per-tab replacement for the built-in op-log history. While a delegate is registered (a collab
|
|
402
|
+
* session's Y.UndoManager), undo/redo route to it and the snapshot history is bypassed — keeping
|
|
403
|
+
* this module free of any yjs knowledge.
|
|
404
|
+
*/
|
|
405
|
+
export interface HistoryDelegate {
|
|
406
|
+
undo: (tab: Tab) => void;
|
|
407
|
+
redo: (tab: Tab) => void;
|
|
408
|
+
canUndo: (tab: Tab) => boolean;
|
|
409
|
+
canRedo: (tab: Tab) => boolean;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const _historyDelegates = new WeakMap<Tab, HistoryDelegate>();
|
|
413
|
+
|
|
414
|
+
export function setHistoryDelegate(tab: Tab, delegate: HistoryDelegate | null) {
|
|
415
|
+
if (delegate) {
|
|
416
|
+
_historyDelegates.set(tab, delegate);
|
|
417
|
+
} else {
|
|
418
|
+
_historyDelegates.delete(tab);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
export function getHistoryDelegate(tab: Tab): HistoryDelegate | null {
|
|
423
|
+
return _historyDelegates.get(tab) ?? null;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
export function canUndo(tab: Tab): boolean {
|
|
427
|
+
const delegate = _historyDelegates.get(tab);
|
|
428
|
+
return delegate ? delegate.canUndo(tab) : tab.history.index > 0;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
export function canRedo(tab: Tab): boolean {
|
|
432
|
+
const delegate = _historyDelegates.get(tab);
|
|
433
|
+
return delegate ? delegate.canRedo(tab) : tab.history.index < tab.history.snapshots.length - 1;
|
|
434
|
+
}
|
|
435
|
+
|
|
320
436
|
/** Restore a materialized snapshot state (full-render path). */
|
|
321
437
|
function restoreState(tab: Tab, index: number) {
|
|
322
438
|
const { snapshots } = tab.history;
|
|
@@ -345,6 +461,11 @@ function assertHistoryConsistency(tab: Tab, index: number) {
|
|
|
345
461
|
|
|
346
462
|
/** @param {Tab} tab */
|
|
347
463
|
export function undo(tab: Tab) {
|
|
464
|
+
const delegate = _historyDelegates.get(tab);
|
|
465
|
+
if (delegate) {
|
|
466
|
+
delegate.undo(tab);
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
348
469
|
if (tab.history.index <= 0) {
|
|
349
470
|
return;
|
|
350
471
|
}
|
|
@@ -360,7 +481,7 @@ export function undo(tab: Tab) {
|
|
|
360
481
|
applyDocOp(t, toRaw(inverseOps[i]) as JxDocOp);
|
|
361
482
|
}
|
|
362
483
|
},
|
|
363
|
-
{ skipHistory: true },
|
|
484
|
+
{ origin: "history", skipHistory: true },
|
|
364
485
|
);
|
|
365
486
|
tab.session.selection = entry.selectionBefore ? [...toRaw(entry.selectionBefore)] : null;
|
|
366
487
|
tab.history.index -= 1;
|
|
@@ -374,6 +495,11 @@ export function undo(tab: Tab) {
|
|
|
374
495
|
|
|
375
496
|
/** @param {Tab} tab */
|
|
376
497
|
export function redo(tab: Tab) {
|
|
498
|
+
const delegate = _historyDelegates.get(tab);
|
|
499
|
+
if (delegate) {
|
|
500
|
+
delegate.redo(tab);
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
377
503
|
if (tab.history.index >= tab.history.snapshots.length - 1) {
|
|
378
504
|
return;
|
|
379
505
|
}
|
|
@@ -387,7 +513,7 @@ export function redo(tab: Tab) {
|
|
|
387
513
|
applyDocOp(t, toRaw(op) as JxDocOp);
|
|
388
514
|
}
|
|
389
515
|
},
|
|
390
|
-
{ skipHistory: true },
|
|
516
|
+
{ origin: "history", skipHistory: true },
|
|
391
517
|
);
|
|
392
518
|
tab.session.selection = entry.selection ? [...toRaw(entry.selection)] : null;
|
|
393
519
|
tab.history.index += 1;
|
package/src/types.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/// <reference lib="dom" />
|
|
2
|
+
import type { CollabHandle } from "@jxsuite/collab/provider";
|
|
2
3
|
import type { JxMutableNode, JxPath, ProjectConfig } from "@jxsuite/schema/types";
|
|
3
4
|
|
|
4
5
|
// ─── Wire types (the Studio Backend Protocol) ───────────────────────────────
|
|
@@ -224,6 +225,13 @@ export interface StudioPlatform {
|
|
|
224
225
|
* projects are found via the native picker instead.
|
|
225
226
|
*/
|
|
226
227
|
listProjects?: () => Promise<ProjectListEntry[]>;
|
|
228
|
+
/**
|
|
229
|
+
* Open a realtime co-editing session for a project-relative document path. Optional: platforms
|
|
230
|
+
* without a collab backend omit it and Studio edits solo with file-level saves. Resolves null
|
|
231
|
+
* when the backend refuses a room for this doc (binary, oversized). The returned handle's Y.Doc
|
|
232
|
+
* starts empty and fills from the provider — see `@jxsuite/collab/provider` for the contract.
|
|
233
|
+
*/
|
|
234
|
+
collab?: (docPath: string) => Promise<CollabHandle | null>;
|
|
227
235
|
// ─── Identity & hosting connections (publish surface) ───────────────────────
|
|
228
236
|
/** The signed-in user's identity, when the platform has one (cloud). */
|
|
229
237
|
getUser?: () => Promise<{ login: string; name?: string; avatarUrl?: string } | null>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { computed, reactive, toRaw } from "../reactivity";
|
|
2
|
+
import { ensureCollab, rekeyCollab } from "../collab/collab-session";
|
|
2
3
|
import { createTab, disposeTab } from "../tabs/tab";
|
|
3
4
|
import type { Tab } from "../tabs/tab";
|
|
4
5
|
|
|
@@ -96,6 +97,7 @@ export function openTab(opts: {
|
|
|
96
97
|
workspace.tabs.set(tab.id, tab);
|
|
97
98
|
workspace.tabOrder.push(tab.id);
|
|
98
99
|
workspace.activeTabId = tab.id;
|
|
100
|
+
ensureCollab(tab);
|
|
99
101
|
return tab;
|
|
100
102
|
}
|
|
101
103
|
|
|
@@ -155,6 +157,7 @@ export function replaceAllTabs(newTabOpts: {
|
|
|
155
157
|
workspace.tabs.set(newTab.id, newTab);
|
|
156
158
|
workspace.activeTabId = newTab.id;
|
|
157
159
|
workspace.tabOrder = [newTab.id];
|
|
160
|
+
ensureCollab(newTab);
|
|
158
161
|
|
|
159
162
|
for (const id of oldIds) {
|
|
160
163
|
if (id === newTab.id) {
|
|
@@ -201,4 +204,5 @@ export function renameTab(oldId: string, newId: string, newDocumentPath: string)
|
|
|
201
204
|
if (workspace.activeTabId === oldId) {
|
|
202
205
|
workspace.activeTabId = newId;
|
|
203
206
|
}
|
|
207
|
+
rekeyCollab(tab);
|
|
204
208
|
}
|