@@ -73,6 +73,7 @@ import type {
73
73
UnsafeUnknownSchema ,
74
74
ViewableTree ,
75
75
TreeBranch ,
76
+ TreeChangeEvents ,
76
77
} from "../simple-tree/index.js" ;
77
78
import { getCheckout , SchematizingSimpleTreeView } from "./schematizingTreeView.js" ;
78
79
@@ -342,6 +343,8 @@ export interface RevertMetrics {
342
343
export class TreeCheckout implements ITreeCheckoutFork {
343
344
public disposed = false ;
344
345
346
+ private readonly editLock : EditLock ;
347
+
345
348
private readonly views = new Set < TreeView < ImplicitFieldSchema > > ( ) ;
346
349
347
350
/**
@@ -432,6 +435,8 @@ export class TreeCheckout implements ITreeCheckoutFork {
432
435
} ,
433
436
) ;
434
437
438
+ this . editLock = new EditLock ( this . #transaction. activeBranchEditor ) ;
439
+
435
440
branch . events . on ( "afterChange" , ( event ) => {
436
441
// The following logic allows revertibles to be generated for the change.
437
442
// Currently only appends (including merges and transaction commits) are supported.
@@ -484,6 +489,7 @@ export class TreeCheckout implements ITreeCheckoutFork {
484
489
}
485
490
486
491
private readonly onAfterChange = ( event : SharedTreeBranchChange < SharedTreeChange > ) : void => {
492
+ this . editLock . lock ( ) ;
487
493
this . #events. emit ( "beforeBatch" , event ) ;
488
494
if ( event . change !== undefined ) {
489
495
const revision =
@@ -521,6 +527,7 @@ export class TreeCheckout implements ITreeCheckoutFork {
521
527
}
522
528
}
523
529
this . #events. emit ( "afterBatch" ) ;
530
+ this . editLock . unlock ( ) ;
524
531
if ( event . type === "append" ) {
525
532
event . newCommits . forEach ( ( commit ) => this . validateCommit ( commit ) ) ;
526
533
}
@@ -665,7 +672,7 @@ export class TreeCheckout implements ITreeCheckoutFork {
665
672
666
673
public get editor ( ) : ISharedTreeEditor {
667
674
this . checkNotDisposed ( ) ;
668
- return this . #transaction . activeBranchEditor ;
675
+ return this . editLock . editor ;
669
676
}
670
677
671
678
public locate ( anchor : Anchor ) : AnchorNode | undefined {
@@ -692,6 +699,7 @@ export class TreeCheckout implements ITreeCheckoutFork {
692
699
this . checkNotDisposed (
693
700
"The parent branch has already been disposed and can no longer create new branches." ,
694
701
) ;
702
+ this . editLock . checkUnlocked ( "Branching" ) ;
695
703
const anchors = new AnchorSet ( ) ;
696
704
const branch = this . #transaction. activeBranch . fork ( ) ;
697
705
const storedSchema = this . storedSchema . clone ( ) ;
@@ -720,6 +728,7 @@ export class TreeCheckout implements ITreeCheckoutFork {
720
728
checkout . checkNotDisposed (
721
729
"The source of the branch rebase has been disposed and cannot be rebased." ,
722
730
) ;
731
+ this . editLock . checkUnlocked ( "Rebasing" ) ;
723
732
assert (
724
733
! checkout . transaction . isInProgress ( ) ,
725
734
0x9af /* A view cannot be rebased while it has a pending transaction */ ,
@@ -748,6 +757,7 @@ export class TreeCheckout implements ITreeCheckoutFork {
748
757
checkout . checkNotDisposed (
749
758
"The source of the branch merge has been disposed and cannot be merged." ,
750
759
) ;
760
+ this . editLock . checkUnlocked ( "Merging" ) ;
751
761
assert (
752
762
! this . transaction . isInProgress ( ) ,
753
763
0x9b0 /* Views cannot be merged into a view while it has a pending transaction */ ,
@@ -768,6 +778,7 @@ export class TreeCheckout implements ITreeCheckoutFork {
768
778
}
769
779
770
780
public dispose ( ) : void {
781
+ this . editLock . checkUnlocked ( "Disposing a view" ) ;
771
782
this [ disposeSymbol ] ( ) ;
772
783
}
773
784
@@ -955,3 +966,103 @@ export class TreeCheckout implements ITreeCheckoutFork {
955
966
956
967
// #endregion Commit Validation
957
968
}
969
+
970
+ /**
971
+ * A helper class that assists {@link TreeCheckout} in preventing functionality from being used while the tree is in the middle of being edited.
972
+ */
973
+ class EditLock {
974
+ /**
975
+ * Edits the tree by calling the methods of the editor passed into the {@link EditLock} constructor.
976
+ * @remarks Edits will throw an error if the lock is currently locked.
977
+ */
978
+ public editor : ISharedTreeEditor ;
979
+ private locked = false ;
980
+
981
+ /**
982
+ * @param editor - an editor which will be used to create a new editor that is monitored to determine if any changes are happening to the tree.
983
+ * Use {@link EditLock.editor} in place of the original editor to ensure that changes are monitored.
984
+ */
985
+ public constructor ( editor : ISharedTreeEditor ) {
986
+ const checkLock = ( ) : void => this . checkUnlocked ( "Editing the tree" ) ;
987
+ this . editor = {
988
+ get schema ( ) {
989
+ return editor . schema ;
990
+ } ,
991
+ valueField ( ...fieldArgs ) {
992
+ const valueField = editor . valueField ( ...fieldArgs ) ;
993
+ return {
994
+ set ( ...editArgs ) {
995
+ checkLock ( ) ;
996
+ valueField . set ( ...editArgs ) ;
997
+ } ,
998
+ } ;
999
+ } ,
1000
+ optionalField ( ...fieldArgs ) {
1001
+ const optionalField = editor . optionalField ( ...fieldArgs ) ;
1002
+ return {
1003
+ set ( ...editArgs ) {
1004
+ checkLock ( ) ;
1005
+ optionalField . set ( ...editArgs ) ;
1006
+ } ,
1007
+ } ;
1008
+ } ,
1009
+ sequenceField ( ...fieldArgs ) {
1010
+ const sequenceField = editor . sequenceField ( ...fieldArgs ) ;
1011
+ return {
1012
+ insert ( ...editArgs ) {
1013
+ checkLock ( ) ;
1014
+ sequenceField . insert ( ...editArgs ) ;
1015
+ } ,
1016
+ remove ( ...editArgs ) {
1017
+ checkLock ( ) ;
1018
+ sequenceField . remove ( ...editArgs ) ;
1019
+ } ,
1020
+ } ;
1021
+ } ,
1022
+ move ( ...moveArgs ) {
1023
+ checkLock ( ) ;
1024
+ editor . move ( ...moveArgs ) ;
1025
+ } ,
1026
+ addNodeExistsConstraint ( path ) {
1027
+ editor . addNodeExistsConstraint ( path ) ;
1028
+ } ,
1029
+ } ;
1030
+ }
1031
+
1032
+ /**
1033
+ * Prevent further changes from being made to {@link EditLock.editor} until {@link EditLock.unlock} is called.
1034
+ * @remarks May only be called when the lock is not already locked.
1035
+ */
1036
+ public lock ( ) : void {
1037
+ if ( this . locked ) {
1038
+ debugger ;
1039
+ }
1040
+ assert ( ! this . locked , "Checkout has already been locked" ) ;
1041
+ this . locked = true ;
1042
+ }
1043
+
1044
+ /**
1045
+ * Throws an error if the lock is currently locked.
1046
+ * @param action - The current action being performed by the user.
1047
+ * This must start with a capital letter, as it shows up as the first part of the error message and we want it to look nice.
1048
+ */
1049
+ public checkUnlocked < T extends string > ( action : T extends Capitalize < T > ? T : never ) : void {
1050
+ if ( this . locked ) {
1051
+ // These type assertions ensure that the event name strings used here match the actual event names
1052
+ const nodeChanged : keyof TreeChangeEvents = "nodeChanged" ;
1053
+ const treeChanged : keyof TreeChangeEvents = "treeChanged" ;
1054
+ throw new UsageError (
1055
+ `${ action } is forbidden during a ${ nodeChanged } or ${ treeChanged } event` ,
1056
+ ) ;
1057
+ }
1058
+ }
1059
+
1060
+ /**
1061
+ * Allow changes to be made to {@link EditLock.editor} again.
1062
+ * @remarks May only be called when the lock is currently locked.
1063
+ */
1064
+ public unlock ( ) : void {
1065
+ assert ( this . locked , "Checkout has not been locked" ) ;
1066
+ this . locked = false ;
1067
+ }
1068
+ }
0 commit comments