@@ -18,11 +18,14 @@ package model
1818
1919import (
2020 "fmt"
21+ "regexp"
2122 "sync"
2223 "sync/atomic"
2324 "time"
2425
2526 "github.com/88250/gulu"
27+ "github.com/88250/lute/ast"
28+ "github.com/siyuan-note/siyuan/kernel/treenode"
2629)
2730
2831// UndoEntry 是撤销栈中的一条记录。跨文档操作(MutatedRootIDs 含多个 rootID)的 entry
@@ -363,3 +366,106 @@ func cloneOperations(ops []*Operation) []*Operation {
363366 }
364367 return ret
365368}
369+
370+ var dataNodeIDPattern = regexp .MustCompile (`data-node-id="([^"]+)"` )
371+ var refcountAttrPattern = regexp .MustCompile (`\s*refcount="[^"]*"` )
372+ var refcountDivPattern = regexp .MustCompile (`<div class="protyle-attr--refcount[^"]*"[^>]*>.*?</div>` )
373+
374+ // ResolveReplayDuplicateIds 在 undo/redo 重放事务前解决块 ID 冲突。
375+ // 场景:剪切块 X 后粘贴到别处(保留原 ID),再撤销剪切会 insert X,而 X 已存在于粘贴处,产生重复 ID。
376+ // 这里对即将重放的 insert 操作做检查——若其引入的 ID 在块树中已存在,则在正反向操作及关联字段
377+ // (ID/ParentID/PreviousID/NextID 与 Data 内联 ID)上统一替换为新 ID。
378+ // 替换同时作用于 do/undo 两套操作:重放只执行 doOperations,但若不同步改 undoOperations,
379+ // 随后对同一 entry 的 redo 会沿用旧 ID 再次撞库。
380+ func ResolveReplayDuplicateIds (tx * Transaction ) {
381+ if nil == tx || ! tx .isReplay {
382+ return
383+ }
384+
385+ // 收集所有 insert 操作引入的块 ID(op.ID + Data 内联的 data-node-id)
386+ ids := map [string ]struct {}{}
387+ collect := func (ops []* Operation ) {
388+ for _ , op := range ops {
389+ if "insert" != op .Action {
390+ continue
391+ }
392+ if "" != op .ID && ast .IsNodeIDPattern (op .ID ) {
393+ ids [op .ID ] = struct {}{}
394+ }
395+ data , ok := op .Data .(string )
396+ if ! ok {
397+ continue
398+ }
399+ for _ , m := range dataNodeIDPattern .FindAllStringSubmatch (data , - 1 ) {
400+ if ast .IsNodeIDPattern (m [1 ]) {
401+ ids [m [1 ]] = struct {}{}
402+ }
403+ }
404+ }
405+ }
406+ collect (tx .DoOperations )
407+ // 注意:只检测 DoOperations(实际执行的操作),不检测 UndoOperations。
408+ // UndoOperations 在 redo 时会作为新的 DoOperations 再次过 ResolveReplayDuplicateIds。
409+ // 若 undo 时也检测 UndoOperations 的 insert,会把 redo 用的 ID 换新,
410+ // 污染 DoOperations 的对应 delete(do/undo 共享 replacements),导致撤销删除错误 ID。
411+ if 0 == len (ids ) {
412+ return
413+ }
414+
415+ idList := make ([]string , 0 , len (ids ))
416+ for id := range ids {
417+ idList = append (idList , id )
418+ }
419+ exist := treenode .ExistBlockTrees (idList )
420+
421+ // 已存在的 ID 生成替换
422+ replacements := map [string ]string {}
423+ for _ , id := range idList {
424+ if exist [id ] {
425+ replacements [id ] = ast .NewNodeID ()
426+ }
427+ }
428+ if 0 == len (replacements ) {
429+ return
430+ }
431+
432+ // 对 do/undo 两套操作统一替换 ID 及关联字段
433+ apply := func (ops []* Operation ) {
434+ for _ , op := range ops {
435+ // 记录本操作的 ID 是否被换新(在改 op.ID 之前判断,否则 replacements 的 key 是 oldID 查不到)
436+ _ , idReplaced := replacements [op .ID ]
437+ if newID , ok := replacements [op .ID ]; ok {
438+ op .ID = newID
439+ }
440+ if newID , ok := replacements [op .ParentID ]; ok {
441+ op .ParentID = newID
442+ }
443+ if newID , ok := replacements [op .PreviousID ]; ok {
444+ op .PreviousID = newID
445+ }
446+ if newID , ok := replacements [op .NextID ]; ok {
447+ op .NextID = newID
448+ }
449+ data , ok := op .Data .(string )
450+ if ! ok {
451+ continue
452+ }
453+ for oldID , newID := range replacements {
454+ data = dataNodeIDPattern .ReplaceAllStringFunc (data , func (match string ) string {
455+ if sub := dataNodeIDPattern .FindStringSubmatch (match ); len (sub ) > 1 && sub [1 ] == oldID {
456+ return `data-node-id="` + newID + `"`
457+ }
458+ return match
459+ })
460+ }
461+ // ID 被换新的块(剪切粘贴后撤销恢复的副本)清除引用角标,避免显示旧的 refcount。
462+ // 角标由 kernel 异步刷新(refreshRefCount)重建为正确值。
463+ if idReplaced {
464+ data = refcountDivPattern .ReplaceAllString (data , "" )
465+ data = refcountAttrPattern .ReplaceAllString (data , "" )
466+ }
467+ op .Data = data
468+ }
469+ }
470+ apply (tx .DoOperations )
471+ }
0 commit comments