@helping-ai-workflow/md2doc 3.12.1 → 3.13.0
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/bin/md2doc.js +6 -0
- package/lib/editor/client.js +356 -180
- package/lib/editor/server.js +5 -0
- package/lib/editor/wave-canvas.js +301 -0
- package/lib/editor/wave-codec.js +417 -11
- package/lib/editor/wave-draw.js +209 -1202
- package/lib/editor/wave-geometry.js +145 -10
- package/lib/editor/wave-panels.js +907 -918
- package/lib/editor/wave-store.js +486 -91
- package/lib/editor/wave-ui.js +3588 -2232
- package/lib/md2doc.js +342 -460
- package/lib/theme/runtime.js +18 -3
- package/lib/wave-overflow.js +107 -0
- package/package.json +3 -3
- package/scripts/release-check.js +60 -0
package/lib/editor/client.js
CHANGED
|
@@ -650,6 +650,12 @@
|
|
|
650
650
|
// v3.4.0 batch3 Task 7 fix 2 (MUST-FIX 1) — may a wave session's own commits
|
|
651
651
|
// simply be popped off the document stack?
|
|
652
652
|
//
|
|
653
|
+
// Since wave redesign Task 11 its one caller is the net-zero unwind
|
|
654
|
+
// (unwindWaveOps(), when a session's drawing is back at its opening bytes):
|
|
655
|
+
// closing the editor never pops anything any more. The MEASURED sequences
|
|
656
|
+
// below that end in an Escape were taken on the retired Escape discard,
|
|
657
|
+
// which asked these same two questions.
|
|
658
|
+
//
|
|
653
659
|
// Two questions, asked separately, each answered by the stack itself rather
|
|
654
660
|
// than by arithmetic over a quantity that can be wrong:
|
|
655
661
|
//
|
|
@@ -707,8 +713,36 @@
|
|
|
707
713
|
return stack.savedDepth === null || stack.savedDepth <= seam.baseDepth;
|
|
708
714
|
}
|
|
709
715
|
|
|
716
|
+
// Wave redesign Task 11 — does a whole-session undo (or its Ctrl+Y) offer
|
|
717
|
+
// still describe the document? `now` is `{ lines, depth, burstOpen }`. The
|
|
718
|
+
// offer holds only while `lines` is the very array it was made over (every
|
|
719
|
+
// edit, undo, redo and reload replaces it — equal bytes in a new array still
|
|
720
|
+
// mean something happened), the stack is at the same depth, and no burst
|
|
721
|
+
// holds typing `lines` has not seen yet. Pure, so test/editor-client.test.js
|
|
722
|
+
// drives it; waveUndoOfferValid() / waveRedoOfferValid() feed it the live
|
|
723
|
+
// state.
|
|
724
|
+
function waveOfferHolds(offer, now) {
|
|
725
|
+
return offer !== null && offer !== undefined && now.lines === offer.lines &&
|
|
726
|
+
now.depth === offer.depth && now.burstOpen !== true;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// Task 11 fix round 1 (Ruling R24) — may Ctrl+Y re-apply a whole-session undo
|
|
730
|
+
// as one step? `ro` is `{ baseDepth, ops }`, `ops` the entries the undo
|
|
731
|
+
// popped, in pop order; `now` is `{ depth, tail, burstOpen }`, `tail` the
|
|
732
|
+
// stack's redo branch (redoTail(), top last). It holds while the stack is at
|
|
733
|
+
// the session's opening depth with exactly those op OBJECTS on top of the
|
|
734
|
+
// redo branch — what an ordinary undo and redo of something else leaves
|
|
735
|
+
// standing, and what any new edit (which clears the branch) does not.
|
|
736
|
+
function waveRedoHolds(ro, now) {
|
|
737
|
+
if (ro === null || ro === undefined || now.burstOpen === true || now.depth !== ro.baseDepth) return false;
|
|
738
|
+
const n = ro.ops.length;
|
|
739
|
+
if (n === 0 || now.tail.length < n) return false;
|
|
740
|
+
const top = now.tail.slice(now.tail.length - n);
|
|
741
|
+
return top.every((op, i) => op === ro.ops[i]);
|
|
742
|
+
}
|
|
743
|
+
|
|
710
744
|
if (typeof module === 'object' && module.exports) {
|
|
711
|
-
module.exports = { pasteIsAllListLines, LIST_LINE_RE, extractBlockSource, commitEdit, commitRangeEdit, commitRangeRemoval, commitListBlockRemoval, commitBlockInsertion, planBlockMove, commitBlockMove, reorderSpanRange, spanMoveRange, spanIndentsAreAnchored, blockMoveSeamRefusal, rollbackFailedRender, headingDepthOf, withHeadingDepth, resolveDrawioLanding, shiftBlocksAfterBodyEdit, waveDiscardIsSafe };
|
|
745
|
+
module.exports = { pasteIsAllListLines, LIST_LINE_RE, extractBlockSource, commitEdit, commitRangeEdit, commitRangeRemoval, commitListBlockRemoval, commitBlockInsertion, planBlockMove, commitBlockMove, reorderSpanRange, spanMoveRange, spanIndentsAreAnchored, blockMoveSeamRefusal, rollbackFailedRender, headingDepthOf, withHeadingDepth, resolveDrawioLanding, shiftBlocksAfterBodyEdit, waveDiscardIsSafe, waveOfferHolds, waveRedoHolds };
|
|
712
746
|
return; // node: pure core only
|
|
713
747
|
}
|
|
714
748
|
|
|
@@ -888,44 +922,51 @@
|
|
|
888
922
|
discardedRawEdit = null;
|
|
889
923
|
}
|
|
890
924
|
|
|
891
|
-
//
|
|
892
|
-
//
|
|
893
|
-
//
|
|
894
|
-
//
|
|
895
|
-
//
|
|
896
|
-
//
|
|
897
|
-
//
|
|
898
|
-
//
|
|
899
|
-
//
|
|
900
|
-
//
|
|
901
|
-
//
|
|
902
|
-
//
|
|
903
|
-
//
|
|
904
|
-
//
|
|
905
|
-
//
|
|
906
|
-
//
|
|
907
|
-
//
|
|
908
|
-
//
|
|
909
|
-
//
|
|
910
|
-
//
|
|
911
|
-
//
|
|
912
|
-
//
|
|
913
|
-
//
|
|
914
|
-
//
|
|
915
|
-
//
|
|
916
|
-
//
|
|
917
|
-
//
|
|
918
|
-
//
|
|
919
|
-
//
|
|
920
|
-
//
|
|
921
|
-
//
|
|
922
|
-
//
|
|
923
|
-
//
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
}
|
|
925
|
+
// Wave redesign Task 11 (spec section 4.8) — the first Ctrl+Z after the
|
|
926
|
+
// wave editor closes takes that whole session back as ONE user step.
|
|
927
|
+
//
|
|
928
|
+
// Closing always keeps (✕, 完成 and Esc are all only "close"): every gesture
|
|
929
|
+
// was written into `lines` as it was drawn, so there is nothing to throw
|
|
930
|
+
// away. What the user may want instead is to take the whole drawing back,
|
|
931
|
+
// and one Ctrl+Z per gesture is the wrong size for that. So the close leaves
|
|
932
|
+
// an offer, made by finishWaveSession() once its render is done:
|
|
933
|
+
//
|
|
934
|
+
// { blockId, startLine, endLine, baseSource, baseDepth, lines, depth, count }
|
|
935
|
+
//
|
|
936
|
+
// blockId the block, found again by id in the CURRENT `blocks` when the
|
|
937
|
+
// offer is used — never by the line numbers it had at the close
|
|
938
|
+
// startLine, endLine the body range at the close (a record, not used to
|
|
939
|
+
// address anything)
|
|
940
|
+
// baseSource the block body when the editor OPENED: the seam's own copy,
|
|
941
|
+
// never the store's source, which rebases on every write-back
|
|
942
|
+
// (Rulings R16/R17)
|
|
943
|
+
// lines the `lines` ARRAY at the close, by reference, and
|
|
944
|
+
// depth `stack.depth` at the close. Together they are the whole
|
|
945
|
+
// validity contract (waveUndoOfferValid()): every edit, undo,
|
|
946
|
+
// redo and reload replaces `lines`, so an offer whose array is
|
|
947
|
+
// no longer the live one belongs to a document that has moved
|
|
948
|
+
// on, even when the bytes happen to be equal again
|
|
949
|
+
// baseDepth the stack depth when the editor opened (seam.baseDepth); the
|
|
950
|
+
// offer is only made when the close finds the stack exactly
|
|
951
|
+
// `baseDepth + count` deep, i.e. the session's own entries on top
|
|
952
|
+
// count how many gestures the session wrote (seam.ops): the toast's N,
|
|
953
|
+
// and how many entries the revert pops (Ruling R24)
|
|
954
|
+
//
|
|
955
|
+
// It is spent by the Ctrl+Z that asks it, whether or not it still held. The
|
|
956
|
+
// step itself (undoWaveSession(), Ruling R24) pops the session's `count`
|
|
957
|
+
// entries off the stack and renders once. It is not the old Escape discard's
|
|
958
|
+
// unwind: that one ran before any render, never rendered, and threw the
|
|
959
|
+
// entries away (discardTop); this keeps them on the redo branch.
|
|
960
|
+
//
|
|
961
|
+
// `waveRedoOffer` is the other half (spec: 可再被 Ctrl+Y 取消): made when
|
|
962
|
+
// the revert lands, `{ baseDepth, ops, offer }` (`ops` the popped entries),
|
|
963
|
+
// and Ctrl+Y while the stack still stands there (waveRedoOfferValid())
|
|
964
|
+
// re-applies them as one step and re-makes the undo offer.
|
|
965
|
+
//
|
|
966
|
+
// Declared up here, beside the stashes, for the same temporal-dead-zone
|
|
967
|
+
// reason as `waveUnwrittenEdit` below.
|
|
968
|
+
let waveUndoOffer = null;
|
|
969
|
+
let waveRedoOffer = null;
|
|
929
970
|
|
|
930
971
|
// v3.4.0 batch3 Task 7 — a wave gesture whose patch was REFUSED, i.e. work
|
|
931
972
|
// the user can see in the overlay that `lines` does not hold and cannot be
|
|
@@ -942,6 +983,14 @@
|
|
|
942
983
|
// definition nothing reached `lines`.
|
|
943
984
|
let waveUnwrittenEdit = false;
|
|
944
985
|
|
|
986
|
+
// wave redesign Task 6b: the open wave editor's `setSaveStatus`, or null.
|
|
987
|
+
// The editor's title bar shows the FILE's save state (spec section 4.8),
|
|
988
|
+
// so setDirty() pushes every change to it. Kept up here rather than read
|
|
989
|
+
// off `waveEditor` for the same reason as `waveUnwrittenEdit` above:
|
|
990
|
+
// setDirty() is reachable before the wave wiring's own `let`s far below
|
|
991
|
+
// have left their temporal dead zone.
|
|
992
|
+
let waveSaveStatusSink = null;
|
|
993
|
+
|
|
945
994
|
// §10-gap fix (review): the block insertBlockBelow() most recently
|
|
946
995
|
// inserted, tracked from the moment its edit surface is first focused
|
|
947
996
|
// until its FIRST resolution (blur, Escape, explicit commit, or Ctrl+Z) —
|
|
@@ -1147,6 +1196,7 @@
|
|
|
1147
1196
|
function setDirty() {
|
|
1148
1197
|
document.title = (documentIsDirty() ? '● ' : '') + baseTitle;
|
|
1149
1198
|
paintSaveStatus(documentIsDirty() ? 'dirty' : 'clean');
|
|
1199
|
+
if (waveSaveStatusSink !== null) waveSaveStatusSink(documentIsDirty());
|
|
1150
1200
|
}
|
|
1151
1201
|
|
|
1152
1202
|
// setDirty() is push-driven: something has to call it. MEASURED by keeping
|
|
@@ -1323,6 +1373,33 @@
|
|
|
1323
1373
|
}
|
|
1324
1374
|
function showError(message, actionLabel, onAction) { return showBanner(message, actionLabel, onAction, 'error'); }
|
|
1325
1375
|
function showNotice(message) { return showBanner(message, null, null, 'notice'); }
|
|
1376
|
+
// Wave redesign Task 6b fix round 1 (spec E2): the wave editor's notices
|
|
1377
|
+
// must never take the place of a red card that is still up — a write-back
|
|
1378
|
+
// refusal, a failed save, the disk-conflict card with 重新載入. With one
|
|
1379
|
+
// banner on the page, showBanner() would replace it, and that card is the
|
|
1380
|
+
// only record of the problem. The notice is dropped instead (the editor
|
|
1381
|
+
// still keeps it in its own data-wave-status). Scoped to the wave editor's
|
|
1382
|
+
// messages rather than changed inside showNotice() for every caller: the
|
|
1383
|
+
// global change could only be verified by the long editor-journey suite,
|
|
1384
|
+
// which this phase of the redesign does not run.
|
|
1385
|
+
//
|
|
1386
|
+
// Task 11: while the editor is open the toast sits just above its hint bar
|
|
1387
|
+
// (spec section 4.8: 編輯器內底部置中) rather than at the viewport's
|
|
1388
|
+
// `bottom: 28px`. MEASURED at 1366x768 before this: the toast spanned
|
|
1389
|
+
// y 696-740 and the hint bar 697-729, so the toast covered the middle of
|
|
1390
|
+
// the bar the user reads for the next step. Measured off the bar itself, so
|
|
1391
|
+
// any panel size or bar height lands it right.
|
|
1392
|
+
function showWaveNotice(message, actionLabel, onAction) {
|
|
1393
|
+
if (activeBanner !== null && document.contains(activeBanner) &&
|
|
1394
|
+
activeBanner.getAttribute('data-level') === 'error') return null;
|
|
1395
|
+
const el = showBanner(message, actionLabel, onAction, 'notice');
|
|
1396
|
+
const hintBar = waveEditorIsModal() ? document.querySelector('.ed-wave-overlay .ed-wave-hint') : null;
|
|
1397
|
+
if (el !== null && hintBar !== null) {
|
|
1398
|
+
const top = hintBar.getBoundingClientRect().top;
|
|
1399
|
+
el.style.bottom = Math.max(8, Math.round(window.innerHeight - top + 8)) + 'px';
|
|
1400
|
+
}
|
|
1401
|
+
return el;
|
|
1402
|
+
}
|
|
1326
1403
|
|
|
1327
1404
|
// Task 9 review I3: MEASURED AT THE TIME — `.ed-conflict` was
|
|
1328
1405
|
// `position:fixed; top:0; z-index:999` against `.ed-toolbar`'s
|
|
@@ -1554,9 +1631,6 @@
|
|
|
1554
1631
|
// twin — this render is about to replace `lines`' rendering of whatever
|
|
1555
1632
|
// discardedRawEdit's stashed `source` names.
|
|
1556
1633
|
dropDiscardedRawEdit();
|
|
1557
|
-
// v3.4.0 batch3 Task 7: and the wave twin — its staleness guard compares
|
|
1558
|
-
// `lines` at a span, which this render is about to repaint from.
|
|
1559
|
-
dropDiscardedWaveEdit();
|
|
1560
1634
|
const scrollY = window.scrollY;
|
|
1561
1635
|
// S3 Task 5 (§4.4 step 2): the line range this render's operation declared
|
|
1562
1636
|
// for the rebuilt selection. Consumed HERE, before the first failure exit,
|
|
@@ -3021,8 +3095,6 @@
|
|
|
3021
3095
|
// own comment for why only the more recent of the two stashes may
|
|
3022
3096
|
// ever answer the next Ctrl+Z.
|
|
3023
3097
|
dropDiscardedBurst();
|
|
3024
|
-
// …and with the wave twin, for the same reason.
|
|
3025
|
-
dropDiscardedWaveEdit();
|
|
3026
3098
|
discardedRawEdit = {
|
|
3027
3099
|
blockId, range, startLine, endLine, source,
|
|
3028
3100
|
value: ta.value, caret: ta.selectionStart,
|
|
@@ -7271,8 +7343,6 @@
|
|
|
7271
7343
|
// discardedRawEdit's own comment for why the two stashes must stay
|
|
7272
7344
|
// mutually exclusive.
|
|
7273
7345
|
dropDiscardedRawEdit();
|
|
7274
|
-
// v3.4.0 batch3 Task 7: …and the wave twin, same rule, three stashes.
|
|
7275
|
-
dropDiscardedWaveEdit();
|
|
7276
7346
|
const blockEl = editEl.closest('.ed-block');
|
|
7277
7347
|
if (!blockEl) return;
|
|
7278
7348
|
const blockId = Number(blockEl.getAttribute('data-block-id'));
|
|
@@ -8548,7 +8618,6 @@
|
|
|
8548
8618
|
// v3.4.0 (Task 7 item 5): mutual exclusivity with the raw-editor twin —
|
|
8549
8619
|
// see discardedRawEdit's own comment.
|
|
8550
8620
|
dropDiscardedRawEdit();
|
|
8551
|
-
dropDiscardedWaveEdit();
|
|
8552
8621
|
discardedBurst = {
|
|
8553
8622
|
history: burst.history, editEl, blockType: burst.blockType, cellIdx: -1,
|
|
8554
8623
|
reverted: burstBaselineHtml(editEl),
|
|
@@ -8570,7 +8639,6 @@
|
|
|
8570
8639
|
// v3.4.0 (Task 7 item 5): mutual exclusivity with the raw-editor twin —
|
|
8571
8640
|
// see discardedRawEdit's own comment.
|
|
8572
8641
|
dropDiscardedRawEdit();
|
|
8573
|
-
dropDiscardedWaveEdit();
|
|
8574
8642
|
discardedBurst = {
|
|
8575
8643
|
history: burst.history, editEl: tableEl, blockType: 'table', cellIdx,
|
|
8576
8644
|
reverted: burstBaselineHtml(tableEl),
|
|
@@ -10002,7 +10070,6 @@
|
|
|
10002
10070
|
dropDiscardedBurst();
|
|
10003
10071
|
// v3.4.0 (Task 7 item 5): see startBurst()'s own comment just above.
|
|
10004
10072
|
dropDiscardedRawEdit();
|
|
10005
|
-
dropDiscardedWaveEdit();
|
|
10006
10073
|
const tableEl = cellEl.closest('table');
|
|
10007
10074
|
const blockEl = tableEl && tableEl.closest('.ed-block');
|
|
10008
10075
|
if (!blockEl) return;
|
|
@@ -15451,6 +15518,10 @@
|
|
|
15451
15518
|
mtimeMs = j.mtimeMs;
|
|
15452
15519
|
stack.markSaved(token);
|
|
15453
15520
|
setDirty();
|
|
15521
|
+
// Spec section 4.8: Ctrl+S inside the wave editor saves the whole file
|
|
15522
|
+
// and leaves the editor open; the toolbar's status slot is behind the
|
|
15523
|
+
// modal, so the result is said at the bottom as well.
|
|
15524
|
+
if (waveSaveStatusSink !== null) showWaveNotice('已儲存', null, null);
|
|
15454
15525
|
// v3.4.0 §3: markSaved() just moved documentIsDirty() from true to
|
|
15455
15526
|
// false, and the `save` button's own lit/grey state is read off
|
|
15456
15527
|
// exactly that value (toolbarContext()'s `dirty` field) — without this
|
|
@@ -15622,11 +15693,6 @@
|
|
|
15622
15693
|
// discardedRawEdit's own comment), so checking both in sequence never
|
|
15623
15694
|
// answers a stale one while the other substrate holds the fresher discard.
|
|
15624
15695
|
if (await restoreDiscardedRawEdit()) return;
|
|
15625
|
-
// v3.4.0 batch3 Task 7: the wave twin. Third in the sequence for no reason
|
|
15626
|
-
// other than that it is the newest; all three are mutually exclusive by
|
|
15627
|
-
// construction (one editing session at a time) and each nulls its own slot
|
|
15628
|
-
// before deciding, so order cannot make a stale one answer.
|
|
15629
|
-
if (await restoreDiscardedWaveEdit()) return;
|
|
15630
15696
|
// Resolve any open editor BEFORE the .content-replacing swap below —
|
|
15631
15697
|
// see the `activeEditor` / switchAwayFrom() comments near its
|
|
15632
15698
|
// declaration for why this is required (undo/redo silently detaching an
|
|
@@ -15635,6 +15701,24 @@
|
|
|
15635
15701
|
// first), so the undo that follows targets whatever is now the newest
|
|
15636
15702
|
// op — which, if an auto-commit just happened, IS that commit.
|
|
15637
15703
|
if (!(await switchAwayFrom())) return;
|
|
15704
|
+
// Wave redesign Task 11 (spec section 4.8): the first Ctrl+Z after the wave
|
|
15705
|
+
// editor closed takes that whole session back as one step, while nothing
|
|
15706
|
+
// else has changed the document since. After the two stashes above on
|
|
15707
|
+
// purpose: either of them can only exist because something was opened and
|
|
15708
|
+
// discarded AFTER the close, so it is the newer thing to undo. And after
|
|
15709
|
+
// switchAwayFrom() (Ruling R25): typing still sitting in an open editor is
|
|
15710
|
+
// newer than the close too, and its commit replaces `lines`, which expires
|
|
15711
|
+
// the offer. Spent here whether or not it held — an offer that did not
|
|
15712
|
+
// hold now never will.
|
|
15713
|
+
if (waveUndoOffer !== null) {
|
|
15714
|
+
const held = waveUndoOfferValid();
|
|
15715
|
+
const offer = waveUndoOffer;
|
|
15716
|
+
waveUndoOffer = null;
|
|
15717
|
+
if (held) {
|
|
15718
|
+
const done = await undoWaveSession(offer);
|
|
15719
|
+
if (done !== 'missing') return;
|
|
15720
|
+
}
|
|
15721
|
+
}
|
|
15638
15722
|
const prevLines = lines;
|
|
15639
15723
|
const r = stack.undo(lines);
|
|
15640
15724
|
if (!r) return;
|
|
@@ -15679,6 +15763,19 @@
|
|
|
15679
15763
|
|
|
15680
15764
|
async function redo() {
|
|
15681
15765
|
if (!(await switchAwayFrom())) return;
|
|
15766
|
+
// Wave redesign Task 11 (spec section 4.8: 可再被 Ctrl+Y 取消): while the
|
|
15767
|
+
// stack stands where the whole-session undo left it, Ctrl+Y re-applies the
|
|
15768
|
+
// session as one step (Ruling R24). A stack question, not an identity of
|
|
15769
|
+
// `lines`: an ordinary Ctrl+Z / Ctrl+Y over an earlier edit comes back to
|
|
15770
|
+
// the same stack, and the drawing still comes back whole after it. So the
|
|
15771
|
+
// offer is NOT spent when it does not hold; it simply stops holding for
|
|
15772
|
+
// good once a new edit clears the redo branch.
|
|
15773
|
+
if (waveRedoOffer !== null && waveRedoOfferValid()) {
|
|
15774
|
+
const ro = waveRedoOffer;
|
|
15775
|
+
waveRedoOffer = null;
|
|
15776
|
+
await redoWaveSession(ro);
|
|
15777
|
+
return;
|
|
15778
|
+
}
|
|
15682
15779
|
const prevLines = lines;
|
|
15683
15780
|
const r = stack.redo(lines);
|
|
15684
15781
|
if (!r) return;
|
|
@@ -18760,16 +18857,18 @@
|
|
|
18760
18857
|
//
|
|
18761
18858
|
// Task 7's own fields, and why each is here rather than derived:
|
|
18762
18859
|
// ops how many commits this session has pushed onto the document
|
|
18763
|
-
// stack
|
|
18764
|
-
//
|
|
18860
|
+
// stack since it last stood at its opening bytes. The net-zero
|
|
18861
|
+
// unwind (unwindWaveOps()) reverses exactly this many, and the
|
|
18862
|
+
// close's toast counts them (spec 4.8: N 處修改).
|
|
18765
18863
|
// baseLines `lines` as it stood when the editor opened, and
|
|
18766
|
-
// baseBlocks `blocks` likewise.
|
|
18767
|
-
// trusting the reversal's arithmetic to land
|
|
18768
|
-
// started; the two are the same answer, and one
|
|
18769
|
-
// be off by a line.
|
|
18770
|
-
// baseSource the block body the store was opened over
|
|
18771
|
-
//
|
|
18772
|
-
//
|
|
18864
|
+
// baseBlocks `blocks` likewise. The net-zero unwind restores both BY
|
|
18865
|
+
// VALUE instead of trusting the reversal's arithmetic to land
|
|
18866
|
+
// back where it started; the two are the same answer, and one
|
|
18867
|
+
// of them cannot be off by a line.
|
|
18868
|
+
// baseSource the block body the store was opened over: what the net-zero
|
|
18869
|
+
// unwind compares against, and what the whole-session undo
|
|
18870
|
+
// after closing writes back (waveUndoOffer). The seam's own
|
|
18871
|
+
// copy, never the store's rebased source (Rulings R16/R17).
|
|
18773
18872
|
// unwritten this session has a gesture the file could not express (the
|
|
18774
18873
|
// document-level flag `waveUnwrittenEdit` is the same fact; the
|
|
18775
18874
|
// flag is what documentIsDirty() reads, this is what the
|
|
@@ -18780,8 +18879,8 @@
|
|
|
18780
18879
|
// killed the arithmetic it replaces (fix 2 / MUST-FIX 1).
|
|
18781
18880
|
// baseRedoTail the redo branch that existed before the session (fix 1 /
|
|
18782
18881
|
// F3). The session's first commit clears it like any other edit;
|
|
18783
|
-
// a session that is
|
|
18784
|
-
// silently loses a redo to a document that ends up
|
|
18882
|
+
// a session that is unwound to nothing has to hand it back, or
|
|
18883
|
+
// the user silently loses a redo to a document that ends up
|
|
18785
18884
|
// byte-identical.
|
|
18786
18885
|
let waveSeam = null;
|
|
18787
18886
|
// Gestures that arrived with no editor left to own them. Zero is the
|
|
@@ -18966,9 +19065,16 @@
|
|
|
18966
19065
|
// button does nothing visible", never to a throw out of a click handler.
|
|
18967
19066
|
if (!ui || !mods || !mods['wave-store.js'] || !mods['wave-codec.js'] ||
|
|
18968
19067
|
!mods['wave-geometry.js'] || !mods['wave-draw.js'] ||
|
|
18969
|
-
!mods['wave-panels.js']) return;
|
|
19068
|
+
!mods['wave-panels.js'] || !mods['wave-canvas.js']) return;
|
|
18970
19069
|
if (waveEditor !== null) return;
|
|
18971
19070
|
hideWaveEditBtn();
|
|
19071
|
+
// Spec section 6: no engine, no editor — a red card, never a blank canvas
|
|
19072
|
+
// and never a fallback drawing of our own (the engine IS the canvas).
|
|
19073
|
+
if (typeof WaveDrom === 'undefined' || WaveDrom === null ||
|
|
19074
|
+
typeof WaveDrom.RenderWaveForm !== 'function') {
|
|
19075
|
+
showError('波形引擎沒有載入,無法編輯', null, null);
|
|
19076
|
+
return;
|
|
19077
|
+
}
|
|
18972
19078
|
|
|
18973
19079
|
// Settle the document BEFORE reading a single line out of it. An open
|
|
18974
19080
|
// burst or block editor is committed here, deliberately in front of the
|
|
@@ -19048,6 +19154,13 @@
|
|
|
19048
19154
|
geometry: mods['wave-geometry.js'],
|
|
19049
19155
|
draw: mods['wave-draw.js'],
|
|
19050
19156
|
panels: mods['wave-panels.js'],
|
|
19157
|
+
// The engine canvas (wave redesign Task 6a) and the theme API it
|
|
19158
|
+
// recolours with in dark (lib/theme/runtime.js).
|
|
19159
|
+
canvas: mods['wave-canvas.js'],
|
|
19160
|
+
theme: window.md2docTheme || null,
|
|
19161
|
+
// Task 10: the overflow rule's `suggestScale` (lib/wave-overflow.js),
|
|
19162
|
+
// for ⚙'s and the canvas chip's 「改每拍 K×」.
|
|
19163
|
+
waveOverflow: mods['wave-overflow.js'] || null,
|
|
19051
19164
|
createStore: mods['wave-store.js'].createStore,
|
|
19052
19165
|
// The store's own answer to「哪一條 lane 是哪一條」across one operation,
|
|
19053
19166
|
// handed over rather than re-derived: the editor's cell cursor and its
|
|
@@ -19057,12 +19170,24 @@
|
|
|
19057
19170
|
// another.
|
|
19058
19171
|
laneTrace: mods['wave-store.js'].laneTrace,
|
|
19059
19172
|
wavedrom: (typeof WaveDrom !== 'undefined') ? WaveDrom : null,
|
|
19173
|
+
// Spec section 4.8's two message levels, on this page's own banner:
|
|
19174
|
+
// 'error' is the red card that stays, 'notice' the 4-second toast.
|
|
19175
|
+
notify: (level, text, action) => {
|
|
19176
|
+
const label = action && action.label ? action.label : null;
|
|
19177
|
+
const run = action && typeof action.run === 'function' ? action.run : null;
|
|
19178
|
+
if (level === 'error') showError(text, label, run);
|
|
19179
|
+
else showWaveNotice(text, label !== null && run !== null ? label : null, run);
|
|
19180
|
+
},
|
|
19181
|
+
// The file's save state for the editor's title bar; later changes
|
|
19182
|
+
// arrive through `waveSaveStatusSink` (setDirty()).
|
|
19183
|
+
saveStatus: () => ({ dirty: documentIsDirty() }),
|
|
19184
|
+
saveKeyHint: SAVE_KEY_HINT,
|
|
19060
19185
|
onGesture: (info) => {
|
|
19061
19186
|
if (waveSeam === null) { waveStrayGestures++; return; }
|
|
19062
19187
|
waveSeam.lastGesture = info;
|
|
19063
19188
|
writeWaveGestureBack(info);
|
|
19064
19189
|
},
|
|
19065
|
-
onClose: (
|
|
19190
|
+
onClose: () => {
|
|
19066
19191
|
// Read the seam out and null BOTH before doing anything slow: the
|
|
19067
19192
|
// predicate every global listener asks (`waveEditorIsModal()`) has to
|
|
19068
19193
|
// answer "no" from this instant, and finishWaveSession() below awaits
|
|
@@ -19071,7 +19196,8 @@
|
|
|
19071
19196
|
const seam = waveSeam;
|
|
19072
19197
|
waveEditor = null;
|
|
19073
19198
|
waveSeam = null;
|
|
19074
|
-
|
|
19199
|
+
waveSaveStatusSink = null;
|
|
19200
|
+
finishWaveSession(seam)
|
|
19075
19201
|
// …and only then the keyboard, because the commit path RE-RENDERS
|
|
19076
19202
|
// `.content` and every block element that existed when the overlay
|
|
19077
19203
|
// was up is gone by the time that resolves. Focusing before the
|
|
@@ -19087,9 +19213,11 @@
|
|
|
19087
19213
|
} catch (err) {
|
|
19088
19214
|
waveEditor = null;
|
|
19089
19215
|
waveSeam = null;
|
|
19216
|
+
waveSaveStatusSink = null;
|
|
19090
19217
|
throw err;
|
|
19091
19218
|
}
|
|
19092
19219
|
waveEditor = editor;
|
|
19220
|
+
waveSaveStatusSink = typeof editor.setSaveStatus === 'function' ? editor.setSaveStatus : null;
|
|
19093
19221
|
waveSeam = { blockId: id, startLine: body.startLine, endLine: body.endLine,
|
|
19094
19222
|
lastGesture: null, ops: 0, unwritten: false,
|
|
19095
19223
|
blockStartLine: block.startLine, blockEndLine: block.endLine,
|
|
@@ -19099,6 +19227,10 @@
|
|
|
19099
19227
|
// A fresh session cannot inherit the previous one's unwritten work: that
|
|
19100
19228
|
// session is over, and whatever it could not express went with it.
|
|
19101
19229
|
waveUnwrittenEdit = false;
|
|
19230
|
+
// Nor the previous session's whole-session undo: a new session takes over
|
|
19231
|
+
// the block, and a Ctrl+Z after IT closes is about it alone.
|
|
19232
|
+
waveUndoOffer = null;
|
|
19233
|
+
waveRedoOffer = null;
|
|
19102
19234
|
}
|
|
19103
19235
|
|
|
19104
19236
|
// ── Task 7: one gesture, one write-back ────────────────────────────────
|
|
@@ -19119,16 +19251,15 @@
|
|
|
19119
19251
|
// written directly. Two things fall out of that for free and are load-
|
|
19120
19252
|
// bearing, not incidental: `stack.dirtyDepth` counts the waveform (so the
|
|
19121
19253
|
// title dot, the save button and the beforeunload guard all see it without
|
|
19122
|
-
// knowing what a waveform is), and
|
|
19123
|
-
//
|
|
19254
|
+
// knowing what a waveform is), and a session that comes back to its opening
|
|
19255
|
+
// bytes can take its own ops back off the stack (unwindWaveOps()).
|
|
19124
19256
|
//
|
|
19125
19257
|
// What is deliberately NOT done here: re-rendering `.content`. The overlay
|
|
19126
19258
|
// covers the document, a render replaces `.content` wholesale, and doing it
|
|
19127
19259
|
// per paint would tear down and re-run WaveDrom on every gesture for a
|
|
19128
19260
|
// picture nobody can see. `.content` therefore lags `lines` for exactly as
|
|
19129
|
-
// long as the modal is up, and the session's close settles it
|
|
19130
|
-
//
|
|
19131
|
-
// showing.
|
|
19261
|
+
// long as the modal is up, and the session's close settles it by rendering
|
|
19262
|
+
// (every route out of the editor keeps the drawing — spec section 4.8).
|
|
19132
19263
|
function writeWaveGestureBack(info) {
|
|
19133
19264
|
const seam = waveSeam;
|
|
19134
19265
|
if (seam === null || !info || !info.patch) return;
|
|
@@ -19222,7 +19353,8 @@
|
|
|
19222
19353
|
//
|
|
19223
19354
|
// That is a FAMILY, not one caller: `addLaneFromToolbar` and 複製 have
|
|
19224
19355
|
// written over the same refusal since before Task 16, and the rail's drag
|
|
19225
|
-
// drop
|
|
19356
|
+
// drop announced nothing at all (since v3.13 the ⠿ grip's drop goes
|
|
19357
|
+
// through `moveLaneTo`, which says where the lane landed). Re-ordering those says is behaviour, and
|
|
19226
19358
|
// it is parked as a next-version follow-up rather than done here. So the
|
|
19227
19359
|
// banner below is not a nicety layered on a status line that already said
|
|
19228
19360
|
// it — it is the only part of the refusal that cannot be clobbered.
|
|
@@ -19357,12 +19489,6 @@
|
|
|
19357
19489
|
});
|
|
19358
19490
|
}
|
|
19359
19491
|
|
|
19360
|
-
/**
|
|
19361
|
-
* Settle a wave session that has just gone off screen.
|
|
19362
|
-
*
|
|
19363
|
-
* `reason` is wave-ui's, not inferred here: 'escape' means the user asked for
|
|
19364
|
-
* the session to be thrown away, anything else means they kept it.
|
|
19365
|
-
*/
|
|
19366
19492
|
/**
|
|
19367
19493
|
* Give the keyboard back to the block the editor was opened over.
|
|
19368
19494
|
*
|
|
@@ -19397,60 +19523,16 @@
|
|
|
19397
19523
|
setBlockSelection({ anchorLine: b.startLine, focusLine: b.startLine });
|
|
19398
19524
|
}
|
|
19399
19525
|
|
|
19400
|
-
|
|
19526
|
+
/**
|
|
19527
|
+
* Settle a wave session that has just gone off screen.
|
|
19528
|
+
*
|
|
19529
|
+
* Every route out of the editor — ✕, 完成, Esc — keeps the session (spec
|
|
19530
|
+
* section 4.8): each gesture was committed as it was drawn, so all that is
|
|
19531
|
+
* left is the one render `.content` has been waiting for, and the offer
|
|
19532
|
+
* that lets the next Ctrl+Z take the whole session back at once.
|
|
19533
|
+
*/
|
|
19534
|
+
async function finishWaveSession(seam) {
|
|
19401
19535
|
if (seam === null) return;
|
|
19402
|
-
if (reason === 'escape') {
|
|
19403
|
-
// Escape DISCARDS — the same word it means everywhere else in v3.3.0 —
|
|
19404
|
-
// and here that has to reach the document stack, because the session has
|
|
19405
|
-
// been committing all along.
|
|
19406
|
-
const painted = bodyNow(seam);
|
|
19407
|
-
const hadOps = seam.ops > 0;
|
|
19408
|
-
if (!unwindWaveOps(seam)) {
|
|
19409
|
-
// A Ctrl+S landed inside this session, so its commits cannot simply be
|
|
19410
|
-
// popped — see unwindWaveOps() for the measured failure that produces.
|
|
19411
|
-
// A revert COMMIT is the honest answer instead: the document goes back
|
|
19412
|
-
// to where the session found it, and the stack only ever grows, so
|
|
19413
|
-
// `dirtyDepth` stays strictly positive and can never walk back through
|
|
19414
|
-
// zero over a document that differs from disk. The cost is a `●` the
|
|
19415
|
-
// user clears with one Ctrl+S — which is TRUE, because disk is still
|
|
19416
|
-
// holding the drawing they just discarded.
|
|
19417
|
-
const result = commitRangeEdit({ lines, blocks, stack },
|
|
19418
|
-
seam.startLine, seam.endLine, seam.baseSource);
|
|
19419
|
-
if (result.op !== null) lines = result.lines;
|
|
19420
|
-
blocks = seam.baseBlocks;
|
|
19421
|
-
seam.endLine = seam.startLine + seam.baseSource.split('\n').length - 1;
|
|
19422
|
-
// fix 2 (R1): the redo branch, on THIS path too. Both Escape branches
|
|
19423
|
-
// end with `lines` byte-identical to `seam.baseLines` — only the seam
|
|
19424
|
-
// range is ever written during a session — so the pre-session redo ops
|
|
19425
|
-
// still address the document they were recorded against, and a user who
|
|
19426
|
-
// lands here is otherwise in exactly F3's position: a byte-identical
|
|
19427
|
-
// document with their redo silently eaten. AFTER the commit, because
|
|
19428
|
-
// `push()` is what cleared it.
|
|
19429
|
-
stack.setRedoTail(seam.baseRedoTail);
|
|
19430
|
-
}
|
|
19431
|
-
if (hadOps && painted !== seam.baseSource) {
|
|
19432
|
-
// …and the discard is recoverable by exactly one Ctrl+Z. See
|
|
19433
|
-
// discardedWaveEdit's own comment for which of discardedBurst's rules
|
|
19434
|
-
// this reuses and which of them cannot apply here.
|
|
19435
|
-
dropDiscardedBurst();
|
|
19436
|
-
dropDiscardedRawEdit();
|
|
19437
|
-
discardedWaveEdit = {
|
|
19438
|
-
blockId: seam.blockId,
|
|
19439
|
-
blockStartLine: seam.blockStartLine, blockEndLine: seam.blockEndLine,
|
|
19440
|
-
startLine: seam.startLine, endLine: seam.endLine,
|
|
19441
|
-
source: seam.baseSource, painted: painted,
|
|
19442
|
-
};
|
|
19443
|
-
}
|
|
19444
|
-
// Work the file could not hold goes with the session it belonged to.
|
|
19445
|
-
// The banner that said so is deliberately NOT dismissed here: it is the
|
|
19446
|
-
// only record the user has that something did not survive.
|
|
19447
|
-
waveUnwrittenEdit = false;
|
|
19448
|
-
setDirty();
|
|
19449
|
-
// No render. `.content` was never repainted while the overlay was up, so
|
|
19450
|
-
// it is still showing exactly the base document just restored — rendering
|
|
19451
|
-
// would be a round trip to say nothing.
|
|
19452
|
-
return;
|
|
19453
|
-
}
|
|
19454
19536
|
if (seam.unwritten) {
|
|
19455
19537
|
// Kept, with a hole in it. The user is told once more on the way out,
|
|
19456
19538
|
// because the status line they were reading has just been removed from
|
|
@@ -19463,6 +19545,8 @@
|
|
|
19463
19545
|
waveUnwrittenEdit = false;
|
|
19464
19546
|
setDirty();
|
|
19465
19547
|
}
|
|
19548
|
+
// Nothing written (or the drawing came back to its opening bytes, which
|
|
19549
|
+
// unwound the ops): no render, no toast, no offer.
|
|
19466
19550
|
if (seam.ops === 0) return;
|
|
19467
19551
|
// fix 1 (F2): `blocks` is settled from the session's own arithmetic BEFORE
|
|
19468
19552
|
// the render, not instead of it. A successful render overwrites it with the
|
|
@@ -19497,61 +19581,150 @@
|
|
|
19497
19581
|
'畫面沒有重畫成功,但你畫的波形還在文件裡(按 Ctrl+S 可以存檔,' +
|
|
19498
19582
|
'Ctrl+Z 可以撤銷)。', null, null);
|
|
19499
19583
|
}
|
|
19584
|
+
// Wave redesign Task 11: the whole-session undo offer (see waveUndoOffer's
|
|
19585
|
+
// own comment). Made AFTER the render rather than before it: an offer used
|
|
19586
|
+
// while the close render is still in flight would start a second render
|
|
19587
|
+
// racing this one. `lines` does not move across a render, so this is the
|
|
19588
|
+
// array the close left either way. The keyboard's way in is Ctrl+Z (spec
|
|
19589
|
+
// section 7.3: the keyboard goes back to the block, not to the toast); the
|
|
19590
|
+
// toast's 整段復原 is the pointer's.
|
|
19591
|
+
//
|
|
19592
|
+
// Ruling R24: the offer pops the session's own entries, so it is made only
|
|
19593
|
+
// when they are exactly the top `ops` of the stack. When they are not
|
|
19594
|
+
// (nothing known reaches that today), the close still says what changed,
|
|
19595
|
+
// without promising a one-step undo it could not keep.
|
|
19596
|
+
waveRedoOffer = null;
|
|
19597
|
+
if (stack.depth === seam.baseDepth + seam.ops) {
|
|
19598
|
+
waveUndoOffer = { blockId: seam.blockId, startLine: seam.startLine, endLine: seam.endLine,
|
|
19599
|
+
baseSource: seam.baseSource, baseDepth: seam.baseDepth,
|
|
19600
|
+
lines: lines, depth: stack.depth, count: seam.ops };
|
|
19601
|
+
showWaveNotice('波形已更新(' + seam.ops + ' 處修改)· Ctrl+Z 整段復原', '整段復原',
|
|
19602
|
+
() => { revertWaveSessionFromToast(); });
|
|
19603
|
+
} else {
|
|
19604
|
+
showWaveNotice('波形已更新(' + seam.ops + ' 處修改)', null, null);
|
|
19605
|
+
}
|
|
19606
|
+
}
|
|
19607
|
+
|
|
19608
|
+
/** Whether the closed session's whole-session undo still describes the document. */
|
|
19609
|
+
function waveUndoOfferValid() {
|
|
19610
|
+
return waveOfferHolds(waveUndoOffer, { lines: lines, depth: stack.depth, burstOpen: burstHasUncommittedEdit() });
|
|
19611
|
+
}
|
|
19612
|
+
/** Whether the stack still stands where the whole-session undo left it, so
|
|
19613
|
+
* Ctrl+Y can re-apply the session as one step. */
|
|
19614
|
+
function waveRedoOfferValid() {
|
|
19615
|
+
return waveRedoHolds(waveRedoOffer, { depth: stack.depth, tail: stack.redoTail(), burstOpen: burstHasUncommittedEdit() });
|
|
19616
|
+
}
|
|
19617
|
+
|
|
19618
|
+
/**
|
|
19619
|
+
* The toast's 整段復原: the same step as Ctrl+Z, but only while the offer
|
|
19620
|
+
* still holds. A press on a toast that outlived its offer must not fall
|
|
19621
|
+
* through to an ordinary undo of somebody else's edit. An open editor is
|
|
19622
|
+
* committed first, exactly as undo() does (Ruling R25).
|
|
19623
|
+
*/
|
|
19624
|
+
async function revertWaveSessionFromToast() {
|
|
19625
|
+
if (!(await switchAwayFrom())) return;
|
|
19626
|
+
if (!waveUndoOfferValid()) {
|
|
19627
|
+
waveUndoOffer = null;
|
|
19628
|
+
showNotice('文件在關閉後又改過了,整段復原已經失效;請用 Ctrl+Z 一步一步退');
|
|
19629
|
+
return;
|
|
19630
|
+
}
|
|
19631
|
+
const offer = waveUndoOffer;
|
|
19632
|
+
waveUndoOffer = null;
|
|
19633
|
+
await undoWaveSession(offer);
|
|
19500
19634
|
}
|
|
19501
19635
|
|
|
19502
19636
|
/**
|
|
19503
|
-
*
|
|
19504
|
-
*
|
|
19637
|
+
* Spec section 4.8 / Ruling R24: take the closed session back as ONE user
|
|
19638
|
+
* step by popping its own `count` entries off the document stack, then
|
|
19639
|
+
* render once.
|
|
19505
19640
|
*
|
|
19506
|
-
*
|
|
19507
|
-
*
|
|
19508
|
-
*
|
|
19509
|
-
*
|
|
19510
|
-
*
|
|
19511
|
-
*
|
|
19512
|
-
* document.
|
|
19641
|
+
* A pop, not a commit. The first cut wrote the opening body back as an
|
|
19642
|
+
* ordinary commit, and the stack then ping-ponged: the next Ctrl+Z popped
|
|
19643
|
+
* that commit and the whole drawing came back, so an edit made before the
|
|
19644
|
+
* session was count + 3 presses away. Popping keeps the stack linear (the
|
|
19645
|
+
* next Ctrl+Z is whatever came before the session), the save marker honest,
|
|
19646
|
+
* and Ctrl+Y re-applies the same entries (redoWaveSession()).
|
|
19513
19647
|
*
|
|
19514
|
-
*
|
|
19515
|
-
*
|
|
19516
|
-
*
|
|
19517
|
-
*
|
|
19648
|
+
* Why popping exactly `count` entries is the session: the caller has checked
|
|
19649
|
+
* the offer holds (`lines` is still the array the close left and the stack
|
|
19650
|
+
* the depth it left), and the offer was only made when that depth was
|
|
19651
|
+
* `baseDepth + ops`. The opening body is still checked after the pops — a
|
|
19652
|
+
* gesture that ever wrote other than one entry would show up there — and a
|
|
19653
|
+
* mismatch puts the entries back and says so instead of writing anything.
|
|
19654
|
+
*
|
|
19655
|
+
* Returns 'done', 'refused' (nothing changed; already said) or 'missing'
|
|
19656
|
+
* (the block is not in the map; said, and the caller undoes one step as
|
|
19657
|
+
* usual).
|
|
19518
19658
|
*/
|
|
19519
|
-
async function
|
|
19520
|
-
const
|
|
19521
|
-
|
|
19522
|
-
|
|
19523
|
-
|
|
19524
|
-
|
|
19525
|
-
if (lines.slice(d.startLine - 1, d.endLine).join('\n') !== d.source) return false;
|
|
19659
|
+
async function undoWaveSession(offer) {
|
|
19660
|
+
const b = blocks.find((x) => x.id === offer.blockId);
|
|
19661
|
+
if (!b) {
|
|
19662
|
+
showNotice('找不到剛才編輯的那張波形,沒辦法整段退回;這一下 Ctrl+Z 照常退一步');
|
|
19663
|
+
return 'missing';
|
|
19664
|
+
}
|
|
19526
19665
|
const prevLines = lines;
|
|
19527
|
-
const
|
|
19528
|
-
|
|
19529
|
-
|
|
19530
|
-
|
|
19531
|
-
|
|
19532
|
-
|
|
19533
|
-
|
|
19534
|
-
//
|
|
19535
|
-
//
|
|
19536
|
-
|
|
19537
|
-
const
|
|
19538
|
-
startLine
|
|
19539
|
-
|
|
19540
|
-
|
|
19666
|
+
const popped = [];
|
|
19667
|
+
while (popped.length < offer.count) {
|
|
19668
|
+
const r = stack.undo(lines);
|
|
19669
|
+
if (!r) break;
|
|
19670
|
+
lines = r.lines;
|
|
19671
|
+
popped.push(r.op);
|
|
19672
|
+
}
|
|
19673
|
+
// `b` is from the map the close render made: the fence line has not moved
|
|
19674
|
+
// (every gesture rewrote the body only), so the body starts right after it.
|
|
19675
|
+
const n = offer.baseSource.split('\n').length;
|
|
19676
|
+
const back = popped.length === offer.count &&
|
|
19677
|
+
lines.slice(b.startLine, b.startLine + n).join('\n') === offer.baseSource;
|
|
19678
|
+
if (back) setDirty();
|
|
19679
|
+
const ok = back && await safeRerenderAll({
|
|
19680
|
+
startLine: b.startLine + 1, endLine: b.startLine + n, delta: lines.length - prevLines.length,
|
|
19541
19681
|
});
|
|
19542
19682
|
if (!ok) {
|
|
19543
|
-
|
|
19683
|
+
for (let i = 0; i < popped.length; i++) lines = stack.redo(lines).lines;
|
|
19684
|
+
setDirty();
|
|
19685
|
+
if (!back) {
|
|
19686
|
+
showNotice('這次的波形修改沒辦法整段退回(文件的復原紀錄跟關閉時對不上),所以沒有動;' +
|
|
19687
|
+
'請用 Ctrl+Z 一步一步退');
|
|
19688
|
+
}
|
|
19689
|
+
return 'refused';
|
|
19690
|
+
}
|
|
19691
|
+
waveRedoOffer = { baseDepth: offer.baseDepth, ops: popped, offer: offer };
|
|
19692
|
+
showWaveNotice('已退回這次的波形修改', null, null);
|
|
19693
|
+
return 'done';
|
|
19694
|
+
}
|
|
19695
|
+
|
|
19696
|
+
/**
|
|
19697
|
+
* Ctrl+Y while the stack stands where undoWaveSession() left it: re-apply
|
|
19698
|
+
* the session's entries with stack.redo(), as one step, and render once. The
|
|
19699
|
+
* document is then exactly where the close left it, so the whole-session
|
|
19700
|
+
* undo is on offer again. waveRedoHolds() has checked that the top of the
|
|
19701
|
+
* redo branch is those very entries, so the redos cannot come up short; if
|
|
19702
|
+
* the render fails they are undone again.
|
|
19703
|
+
*/
|
|
19704
|
+
async function redoWaveSession(ro) {
|
|
19705
|
+
const prevLines = lines;
|
|
19706
|
+
let done = 0;
|
|
19707
|
+
while (done < ro.ops.length) {
|
|
19708
|
+
const r = stack.redo(lines);
|
|
19709
|
+
if (!r) break;
|
|
19710
|
+
lines = r.lines;
|
|
19711
|
+
done++;
|
|
19712
|
+
}
|
|
19713
|
+
let ok = done === ro.ops.length;
|
|
19714
|
+
if (ok) {
|
|
19715
|
+
setDirty();
|
|
19716
|
+
const b = blocks.find((x) => x.id === ro.offer.blockId);
|
|
19717
|
+
ok = await safeRerenderAll(b ? { startLine: b.startLine + 1, endLine: b.startLine + 1,
|
|
19718
|
+
delta: lines.length - prevLines.length } : undefined);
|
|
19719
|
+
}
|
|
19720
|
+
if (!ok) {
|
|
19721
|
+
for (let i = 0; i < done; i++) lines = stack.undo(lines).lines;
|
|
19544
19722
|
setDirty();
|
|
19545
19723
|
return false;
|
|
19546
19724
|
}
|
|
19547
|
-
|
|
19548
|
-
|
|
19549
|
-
|
|
19550
|
-
// argument and does not depend on WaveDrom having finished painting a
|
|
19551
|
-
// `.wavedrom-diagram` into it yet.
|
|
19552
|
-
const blockEl = blockElById(d.blockId);
|
|
19553
|
-
if (!blockEl) return true; // the drawing is back in the document either way
|
|
19554
|
-
await openWaveEditor(blockEl);
|
|
19725
|
+
waveUndoOffer = Object.assign({}, ro.offer, { lines: lines, depth: stack.depth });
|
|
19726
|
+
showWaveNotice('波形修改已恢復 · Ctrl+Z 整段復原', '整段復原',
|
|
19727
|
+
() => { revertWaveSessionFromToast(); });
|
|
19555
19728
|
return true;
|
|
19556
19729
|
}
|
|
19557
19730
|
|
|
@@ -19564,13 +19737,16 @@
|
|
|
19564
19737
|
return {
|
|
19565
19738
|
open: waveEditor !== null,
|
|
19566
19739
|
stray: waveStrayGestures,
|
|
19567
|
-
// Task 7's
|
|
19740
|
+
// Task 7's two: whether the document currently believes it has
|
|
19568
19741
|
// anything unsaved (the thing the Reload button's beforeunload guard
|
|
19569
|
-
// asks)
|
|
19570
|
-
// whether a
|
|
19742
|
+
// asks) and whether this session has work the file could not hold.
|
|
19743
|
+
// Task 11's two: whether a closed session's whole-session undo is on
|
|
19744
|
+
// offer and would hold now, and the same for its Ctrl+Y.
|
|
19571
19745
|
dirty: documentIsDirty(),
|
|
19572
19746
|
unwritten: waveUnwrittenEdit,
|
|
19573
|
-
|
|
19747
|
+
undoOffer: waveUndoOffer === null ? null
|
|
19748
|
+
: { count: waveUndoOffer.count, blockId: waveUndoOffer.blockId, holds: waveUndoOfferValid() },
|
|
19749
|
+
redoOffer: waveRedoOffer === null ? null : { holds: waveRedoOfferValid() },
|
|
19574
19750
|
seam: waveSeam === null ? null : {
|
|
19575
19751
|
blockId: waveSeam.blockId,
|
|
19576
19752
|
startLine: waveSeam.startLine,
|