@lionweb/class-core 0.7.0-beta.2 → 0.7.0-beta.20

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.
Files changed (115) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +6 -6
  3. package/dist/LionCore_builtins.g.d.ts +2 -2
  4. package/dist/LionCore_builtins.g.d.ts.map +1 -1
  5. package/dist/LionCore_builtins.g.js +1 -1
  6. package/dist/LionCore_builtins.g.js.map +1 -1
  7. package/dist/base-types.d.ts +7 -11
  8. package/dist/base-types.d.ts.map +1 -1
  9. package/dist/base-types.js +3 -28
  10. package/dist/base-types.js.map +1 -1
  11. package/dist/convenience.d.ts +7 -7
  12. package/dist/convenience.d.ts.map +1 -1
  13. package/dist/convenience.js +7 -7
  14. package/dist/convenience.js.map +1 -1
  15. package/dist/deltas/appliers.d.ts +5 -4
  16. package/dist/deltas/appliers.d.ts.map +1 -1
  17. package/dist/deltas/appliers.js +369 -163
  18. package/dist/deltas/appliers.js.map +1 -1
  19. package/dist/deltas/compositor.d.ts +51 -0
  20. package/dist/deltas/compositor.d.ts.map +1 -0
  21. package/dist/deltas/compositor.js +95 -0
  22. package/dist/deltas/compositor.js.map +1 -0
  23. package/dist/deltas/index.d.ts +2 -1
  24. package/dist/deltas/index.d.ts.map +1 -1
  25. package/dist/deltas/index.js +2 -1
  26. package/dist/deltas/index.js.map +1 -1
  27. package/dist/deltas/inverters.d.ts.map +1 -1
  28. package/dist/deltas/inverters.js +91 -28
  29. package/dist/deltas/inverters.js.map +1 -1
  30. package/dist/deltas/receivers.d.ts +39 -0
  31. package/dist/deltas/receivers.d.ts.map +1 -0
  32. package/dist/deltas/receivers.js +60 -0
  33. package/dist/deltas/receivers.js.map +1 -0
  34. package/dist/deltas/serialization/deserializer.g.d.ts.map +1 -1
  35. package/dist/deltas/serialization/deserializer.g.js +167 -59
  36. package/dist/deltas/serialization/deserializer.g.js.map +1 -1
  37. package/dist/deltas/serialization/serializer-helpers.d.ts.map +1 -1
  38. package/dist/deltas/serialization/serializer.g.d.ts +2 -2
  39. package/dist/deltas/serialization/serializer.g.d.ts.map +1 -1
  40. package/dist/deltas/serialization/serializer.g.js +186 -47
  41. package/dist/deltas/serialization/serializer.g.js.map +1 -1
  42. package/dist/deltas/serialization/types.g.d.ts +156 -43
  43. package/dist/deltas/serialization/types.g.d.ts.map +1 -1
  44. package/dist/deltas/types.g.d.ts +155 -55
  45. package/dist/deltas/types.g.d.ts.map +1 -1
  46. package/dist/deltas/types.g.js +170 -57
  47. package/dist/deltas/types.g.js.map +1 -1
  48. package/dist/deserializer.d.ts +6 -6
  49. package/dist/deserializer.d.ts.map +1 -1
  50. package/dist/deserializer.js +15 -26
  51. package/dist/deserializer.js.map +1 -1
  52. package/dist/duplicator.d.ts.map +1 -1
  53. package/dist/duplicator.js.map +1 -1
  54. package/dist/factory.d.ts +15 -0
  55. package/dist/factory.d.ts.map +1 -0
  56. package/dist/factory.js +53 -0
  57. package/dist/factory.js.map +1 -0
  58. package/dist/id-mapping.d.ts +27 -3
  59. package/dist/id-mapping.d.ts.map +1 -1
  60. package/dist/id-mapping.js +38 -14
  61. package/dist/id-mapping.js.map +1 -1
  62. package/dist/index.d.ts +1 -0
  63. package/dist/index.d.ts.map +1 -1
  64. package/dist/index.js +1 -0
  65. package/dist/index.js.map +1 -1
  66. package/dist/serializer.d.ts.map +1 -1
  67. package/dist/textualizer.d.ts +1 -1
  68. package/dist/textualizer.d.ts.map +1 -1
  69. package/dist/textualizer.js +1 -1
  70. package/dist/textualizer.js.map +1 -1
  71. package/dist/value-managers/annotations.d.ts +5 -0
  72. package/dist/value-managers/annotations.d.ts.map +1 -1
  73. package/dist/value-managers/annotations.js +31 -3
  74. package/dist/value-managers/annotations.js.map +1 -1
  75. package/dist/value-managers/base.d.ts +2 -2
  76. package/dist/value-managers/base.d.ts.map +1 -1
  77. package/dist/value-managers/base.js +4 -4
  78. package/dist/value-managers/base.js.map +1 -1
  79. package/dist/value-managers/containments.d.ts +4 -1
  80. package/dist/value-managers/containments.d.ts.map +1 -1
  81. package/dist/value-managers/containments.js +152 -20
  82. package/dist/value-managers/containments.js.map +1 -1
  83. package/dist/value-managers/properties.js.map +1 -1
  84. package/dist/value-managers/references.d.ts +3 -0
  85. package/dist/value-managers/references.d.ts.map +1 -1
  86. package/dist/value-managers/references.js +26 -8
  87. package/dist/value-managers/references.js.map +1 -1
  88. package/package.json +34 -37
  89. package/src/LionCore_builtins.g.ts +2 -2
  90. package/src/base-types.ts +6 -32
  91. package/src/convenience.ts +10 -10
  92. package/src/deltas/appliers.ts +387 -187
  93. package/src/deltas/compositor.ts +107 -0
  94. package/src/deltas/index.ts +3 -1
  95. package/src/deltas/inverters.ts +109 -37
  96. package/src/deltas/receivers.ts +92 -0
  97. package/src/deltas/serialization/deserializer.g.ts +189 -68
  98. package/src/deltas/serialization/serializer.g.ts +246 -67
  99. package/src/deltas/serialization/types.g.ts +192 -53
  100. package/src/deltas/types.g.ts +192 -53
  101. package/src/deserializer.ts +31 -42
  102. package/src/duplicator.ts +1 -1
  103. package/src/factory.ts +63 -0
  104. package/src/id-mapping.ts +34 -5
  105. package/src/index.ts +1 -0
  106. package/src/textualizer.ts +1 -1
  107. package/src/value-managers/annotations.ts +29 -3
  108. package/src/value-managers/base.ts +4 -4
  109. package/src/value-managers/containments.ts +145 -20
  110. package/src/value-managers/references.ts +28 -8
  111. package/dist/deltas/handlers.d.ts +0 -17
  112. package/dist/deltas/handlers.d.ts.map +0 -1
  113. package/dist/deltas/handlers.js +0 -43
  114. package/dist/deltas/handlers.js.map +0 -1
  115. package/src/deltas/handlers.ts +0 -64
@@ -21,7 +21,6 @@ import {
21
21
  Containment,
22
22
  defaultSimplisticHandler,
23
23
  Enumeration,
24
- Language,
25
24
  MemoisingSymbolTable,
26
25
  PrimitiveType,
27
26
  Property,
@@ -31,9 +30,10 @@ import {
31
30
  unresolved
32
31
  } from "@lionweb/core"
33
32
  import { LionWebId, LionWebJsonChunk, LionWebJsonNode } from "@lionweb/json"
34
- import { byIdMap } from "@lionweb/ts-utils"
33
+ import { byIdMap, keepDefineds } from "@lionweb/ts-utils"
35
34
 
36
- import { DeltaHandler, IdMapping, ILanguageBase, INodeBase } from "./index.js"
35
+ import { DeltaReceiver, IdMapping, ILanguageBase, INodeBase } from "./index.js"
36
+ import { combinedLanguageBaseLookupFor } from "./factory.js"
37
37
  import { NodesToInstall } from "./linking.js"
38
38
 
39
39
 
@@ -43,26 +43,12 @@ import { NodesToInstall } from "./linking.js"
43
43
  export type Deserializer<T> = (
44
44
  serializationChunk: LionWebJsonChunk,
45
45
  dependentNodes?: INodeBase[],
46
+ idMapping?: IdMapping,
46
47
  propertyValueDeserializer?: PropertyValueDeserializer,
47
48
  problemHandler?: SimplisticHandler
48
49
  ) => T;
49
50
 
50
51
 
51
- const languageBaseLookupFor = (languageBases: ILanguageBase[]) =>
52
- (language: Language) => {
53
- const languageBase = languageBases.find((languageBase) => languageBase.language === language);
54
- if (languageBase === undefined) {
55
- throw new Error(`language ${language.name} (with key=${language.key} and version=${language.version}) not known`);
56
- }
57
- return languageBase;
58
- };
59
-
60
- const factoryLookupFor = (languageBases: ILanguageBase[], handleDelta?: DeltaHandler) => {
61
- const lookup = languageBaseLookupFor(languageBases);
62
- return (language: Language) => lookup(language).factory(handleDelta);
63
- }
64
-
65
-
66
52
  /**
67
53
  * A quasi-tuple of the roots (of type {@link INodeBase}) of a model,
68
54
  * and its {@link IdMapping} instance.
@@ -73,24 +59,24 @@ export type RootsWithIdMapping = { roots: INodeBase[], idMapping: IdMapping };
73
59
  /**
74
60
  * @return a {@link Deserializer} function for the given languages (given as {@link ILanguageBase}s) that returns a {@link RootsWithIdMapping}.
75
61
  * @param languageBases the {@link ILanguageBase}s for (at least) all the languages used in the {@link LionWebJsonChunk} to deserialize, minus LionCore M3 and built-ins.
76
- * @param handleDelta an optional {@link DeltaHandler} that will be injected in all {@link INodeBase nodes} created.
62
+ * @param receiveDelta an optional {@link DeltaReceiver} that will be injected in all {@link INodeBase nodes} created.
77
63
  */
78
- export const nodeBaseDeserializerWithIdMapping = (languageBases: ILanguageBase[], handleDelta?: DeltaHandler): Deserializer<RootsWithIdMapping> => {
64
+ export const nodeBaseDeserializerWithIdMapping = (languageBases: ILanguageBase[], receiveDelta?: DeltaReceiver): Deserializer<RootsWithIdMapping> => {
79
65
 
80
66
  const symbolTable = new MemoisingSymbolTable(languageBases.map(({language}) => language));
81
- const languageBaseFor = languageBaseLookupFor(languageBases);
82
- const factoryFor = factoryLookupFor(languageBases, handleDelta);
67
+ const languageBaseFor = combinedLanguageBaseLookupFor(languageBases);
83
68
 
84
69
  return (
85
- serializationChunk: LionWebJsonChunk,
86
- dependentNodes: INodeBase[] = [],
87
- propertyValueDeserializer: PropertyValueDeserializer = new BuiltinPropertyValueDeserializer(),
88
- problemsHandler: SimplisticHandler = defaultSimplisticHandler
70
+ serializationChunk,
71
+ dependentNodes = [],
72
+ idMapping,
73
+ propertyValueDeserializer = new BuiltinPropertyValueDeserializer(),
74
+ problemsHandler = defaultSimplisticHandler
89
75
  ): RootsWithIdMapping => {
90
76
 
91
77
  const nodesToInstall: NodesToInstall[] = [];
92
78
 
93
- const createNode = ({id, classifier: classifierMetaPointer, properties, containments, references, annotations}: LionWebJsonNode) => {
79
+ const createNode = ({id, classifier: classifierMetaPointer, properties, containments, references, annotations}: LionWebJsonNode): (INodeBase | undefined) => {
94
80
  const languageMessage = `language ${classifierMetaPointer.language} (${classifierMetaPointer.version})`;
95
81
  const classifier = symbolTable.entityMatching(classifierMetaPointer);
96
82
  if (classifier === undefined || !(classifier instanceof Classifier)) {
@@ -98,7 +84,7 @@ export const nodeBaseDeserializerWithIdMapping = (languageBases: ILanguageBase[]
98
84
  return undefined;
99
85
  }
100
86
 
101
- const node = factoryFor(classifier.language)(classifier, id);
87
+ const node = languageBaseFor(classifier.language).factory(receiveDelta)(classifier, id);
102
88
 
103
89
  properties.forEach(({property: propertyMetaPointer, value}) => {
104
90
  const feature = symbolTable.featureMatching(classifierMetaPointer, propertyMetaPointer);
@@ -152,15 +138,17 @@ export const nodeBaseDeserializerWithIdMapping = (languageBases: ILanguageBase[]
152
138
  };
153
139
 
154
140
  const nodesById = byIdMap(
155
- serializationChunk.nodes
156
- .map(createNode)
157
- .filter((nodeOrUndef) => nodeOrUndef !== undefined) as INodeBase[]
141
+ keepDefineds(
142
+ serializationChunk
143
+ .nodes
144
+ .map(createNode)
145
+ )
158
146
  );
159
147
 
160
- const dependentNodesById = byIdMap(dependentNodes)
148
+ const dependentNodesById = byIdMap(dependentNodes);
161
149
 
162
- const lookupNodeById = (id: LionWebId) =>
163
- nodesById[id] ?? dependentNodesById[id];
150
+ const lookupNodeById = (id: LionWebId): (INodeBase | undefined) =>
151
+ nodesById[id] ?? dependentNodesById[id] ?? idMapping?.tryFromId(id);
164
152
 
165
153
  nodesToInstall.forEach(([node, feature, ids]) => {
166
154
  if (feature instanceof Containment) {
@@ -217,16 +205,17 @@ export const nodeBaseDeserializerWithIdMapping = (languageBases: ILanguageBase[]
217
205
  /**
218
206
  * @return a {@link Deserializer} function for the languages (given as {@link ILanguageBase}s) that returns the roots (of type {@link INodeBase}) of the deserialized model.
219
207
  * @param languageBases the {@link ILanguageBase}s for (at least) all the languages used in the {@link LionWebJsonChunk} to deserialize, minus LionCore M3 and built-ins.
220
- * @param handleDelta an optional {@link DeltaHandler} that will be injected in all {@link INodeBase nodes} created.
208
+ * @param receiveDelta an optional {@link DeltaReceiver} that will be injected in all {@link INodeBase nodes} created.
221
209
  */
222
- export const nodeBaseDeserializer = (languageBases: ILanguageBase[], handleDelta?: DeltaHandler): Deserializer<INodeBase[]> => {
223
- const deserializerWithIdMapping = nodeBaseDeserializerWithIdMapping(languageBases, handleDelta);
210
+ export const nodeBaseDeserializer = (languageBases: ILanguageBase[], receiveDelta?: DeltaReceiver): Deserializer<INodeBase[]> => {
211
+ const deserializerWithIdMapping = nodeBaseDeserializerWithIdMapping(languageBases, receiveDelta);
224
212
  return (
225
- serializationChunk: LionWebJsonChunk,
226
- dependentNodes: INodeBase[] = [],
227
- propertyValueDeserializer: PropertyValueDeserializer = new BuiltinPropertyValueDeserializer(),
228
- problemsHandler: SimplisticHandler = defaultSimplisticHandler
213
+ serializationChunk,
214
+ dependentNodes,
215
+ idMapping,
216
+ propertyValueDeserializer = new BuiltinPropertyValueDeserializer(),
217
+ problemsHandler = defaultSimplisticHandler
229
218
  ): INodeBase[] =>
230
- deserializerWithIdMapping(serializationChunk, dependentNodes, propertyValueDeserializer, problemsHandler).roots
219
+ deserializerWithIdMapping(serializationChunk, dependentNodes, idMapping, propertyValueDeserializer, problemsHandler).roots
231
220
  }
232
221
 
package/src/duplicator.ts CHANGED
@@ -15,7 +15,7 @@
15
15
  // SPDX-FileCopyrightText: 2025 TRUMPF Laser SE and other contributors
16
16
  // SPDX-License-Identifier: Apache-2.0
17
17
 
18
- import { Containment, Feature, idOf, Link, Property, Reference, SingleRef, unresolved } from "@lionweb/core"
18
+ import { Containment, Feature, idOf, Link, Property, Reference, SingleRef, unresolved } from "@lionweb/core"
19
19
  import { LionWebId } from "@lionweb/json"
20
20
  import { asArray } from "@lionweb/ts-utils"
21
21
 
package/src/factory.ts ADDED
@@ -0,0 +1,63 @@
1
+ // Copyright 2025 TRUMPF Laser SE and other contributors
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License")
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+ //
15
+ // SPDX-FileCopyrightText: 2025 TRUMPF Laser SE and other contributors
16
+ // SPDX-License-Identifier: Apache-2.0
17
+
18
+ import { Language } from "@lionweb/core"
19
+ import { lazyMapGet } from "@lionweb/ts-utils"
20
+ import { ILanguageBase, NodeBaseFactory } from "./base-types.js"
21
+ import { DeltaReceiver } from "./deltas/index.js"
22
+
23
+
24
+ /**
25
+ * @return a function that looks up the {@link ILanguageBase language base} for the {@link Language language} passed to it,
26
+ * from among the given language bases.
27
+ * The returned function throws when the language wasn't among the languages the given bases were for.
28
+ * The lookup is hashmap-backed, so efficient.
29
+ */
30
+ export const combinedLanguageBaseLookupFor = (languageBases: ILanguageBase[]): ((language: Language) => ILanguageBase) => {
31
+ // create lookup map:
32
+ const languageKey2version2base: { [key: string]: { [version: string]: ILanguageBase } } = {}
33
+ languageBases.forEach((languageBase) => {
34
+ const {key, version} = languageBase.language
35
+ const version2base = lazyMapGet(languageKey2version2base, key, () => ({}))
36
+ lazyMapGet(version2base, version, () => languageBase)
37
+ })
38
+
39
+ return (language) => {
40
+ const {key, version, name} = language
41
+ const version2base = languageKey2version2base[key]
42
+ if (version2base === undefined) {
43
+ throw new Error(`language ${name} with key=${key} not registered`)
44
+ }
45
+ const base = version2base[version]
46
+ if (base === undefined) {
47
+ const candidateVersions = Object.keys(version2base)
48
+ throw new Error(`language ${name} with key=${key} and version=${version} not registered${candidateVersions.length > 0 ? `- candidate version${candidateVersions.length > 1 ? `s` : ``}: ${candidateVersions.join(", ")}` : ``}`)
49
+ }
50
+ return base
51
+ }
52
+ }
53
+
54
+
55
+ /**
56
+ * @return a {@link NodeBaseFactory factory function} that works for all given {@link ILanguageBase language bases}.
57
+ */
58
+ export const combinedFactoryFor = (languageBases: ILanguageBase[], receiveDelta?: DeltaReceiver): NodeBaseFactory => {
59
+ const baseOf = combinedLanguageBaseLookupFor(languageBases)
60
+ return (classifier, id) =>
61
+ baseOf(classifier.language).factory(receiveDelta)(classifier, id)
62
+ }
63
+
package/src/id-mapping.ts CHANGED
@@ -20,6 +20,10 @@ import { LionWebId } from "@lionweb/json"
20
20
 
21
21
  import { INodeBase } from "./index.js"
22
22
 
23
+
24
+ /**
25
+ * Type def. for a (hash-)map {@link LionWebId ID} &rarr; {@link INodeBase}.
26
+ */
23
27
  type NodesById = { [id: LionWebId]: INodeBase }
24
28
 
25
29
 
@@ -30,31 +34,56 @@ type NodesById = { [id: LionWebId]: INodeBase }
30
34
  */
31
35
  export class IdMapping {
32
36
 
33
- nodesById: NodesById;
37
+ private nodesById: NodesById;
34
38
  constructor(nodesById: NodesById) {
35
39
  this.nodesById = {...nodesById};
36
40
  }
37
41
  // TODO consider using an instance of Map<Id, INodeBase> instead
38
42
 
39
- fromId(id: LionWebId): INodeBase {
43
+ /**
44
+ * @return the {@link INodeBase node} with the given {@link LionWebId `id`}, or
45
+ * @throws an Error if no node with the given ID was registered.
46
+ */
47
+ fromId = (id: LionWebId): INodeBase => {
40
48
  if (!(id in this.nodesById)) {
41
49
  throw new Error(`node with id=${id} not in ID mapping`);
42
50
  }
43
51
  return this.nodesById[id];
44
52
  }
45
53
 
54
+ /**
55
+ * @return the {@link INodeBase node} with the given {@link LionWebId `id`},
56
+ * or `undefined` if no node with the given ID was registered.
57
+ */
46
58
  tryFromId = (id: LionWebId): (INodeBase | undefined) =>
47
59
  this.nodesById[id];
48
60
 
61
+ /**
62
+ * @return the {@link INodeBase node} referenced from the given {@link LionWebId ID},
63
+ * or `unresolved` if `unresolved` was passed in or no node with the given ID was registered.
64
+ */
49
65
  fromRefId = (idOrUnresolved: IdOrUnresolved): SingleRef<INodeBase> =>
50
- idOrUnresolved === null
51
- ? null
66
+ idOrUnresolved === unresolved
67
+ ? unresolved
52
68
  : (this.nodesById[idOrUnresolved] ?? unresolved);
53
69
 
54
- updateWith(node: INodeBase) {
70
+ /**
71
+ * Updates this {@link IdMapping} with the given `node` *and all its descendants* (recursively).
72
+ */
73
+ updateWith= (node: INodeBase) => {
55
74
  this.nodesById[node.id] = node;
56
75
  node.children // recurse into all children
57
76
  .forEach((child) => this.updateWith(child));
77
+ // TODO figure out when it's really necessary to call this, as it's potentially *very* expensive
78
+ }
79
+
80
+ /**
81
+ * Re-initializes this {@link IdMapping ID mapping} with the given nodes-by-ID.
82
+ * This completely removes all registrations of nodes,
83
+ * and should only be used by components which are in complete control of the nodes being passed to this method.
84
+ */
85
+ reinitializeWith = (nodesById: NodesById) => {
86
+ this.nodesById = nodesById;
58
87
  }
59
88
 
60
89
  }
package/src/index.ts CHANGED
@@ -21,6 +21,7 @@ export * from "./deltas/index.js";
21
21
  export * from "./deserializer.js";
22
22
  export { deepDuplicateWith } from "./duplicator.js";
23
23
  export * from "./id-mapping.js";
24
+ export { combinedFactoryFor } from "./factory.js";
24
25
  // skip linking.js: see comment there
25
26
  export * from "./LionCore_builtins.g.js";
26
27
  export {serializeNodeBases} from "./serializer.js";
@@ -54,7 +54,7 @@ export const asTreeTextWith = (identificationFor: (node: INodeBase) => string):
54
54
  const valueManager = node.getPropertyValueManager(feature)
55
55
  const displayValue = (() => {
56
56
  if (!valueManager.isSet()) {
57
- return `$<not set>`
57
+ return `<not set>`
58
58
  }
59
59
  const value = valueManager.getDirectly()
60
60
  if (feature.type === LionCore_builtinsBase.INSTANCE.String) {
@@ -17,15 +17,16 @@
17
17
 
18
18
  import { action, observable } from "mobx"
19
19
 
20
- import { INodeBase, removeFromParent } from "../base-types.js"
20
+ import { INodeBase } from "../base-types.js"
21
+ import { checkIndex, ValueManager } from "./base.js"
21
22
  import {
22
23
  AnnotationAddedDelta,
23
24
  AnnotationDeletedDelta,
25
+ AnnotationMovedAndReplacedInSameParentDelta,
24
26
  AnnotationMovedFromOtherParentDelta,
25
27
  AnnotationMovedInSameParentDelta,
26
28
  AnnotationReplacedDelta
27
29
  } from "../deltas/index.js"
28
- import { checkIndex, ValueManager } from "./base.js"
29
30
 
30
31
 
31
32
  /**
@@ -66,7 +67,7 @@ export class AnnotationsValueManager extends ValueManager {
66
67
  newAnnotation.attachTo(this.container, null);
67
68
  return false;
68
69
  } else {
69
- const oldIndex = removeFromParent(oldParent, newAnnotation);
70
+ const oldIndex = oldParent.annotationsValueManager.removeDirectly(newAnnotation);
70
71
  newAnnotation.attachTo(this.container, null);
71
72
  return [oldParent, oldIndex];
72
73
  }
@@ -119,6 +120,31 @@ export class AnnotationsValueManager extends ValueManager {
119
120
  this.emitDelta(() => new AnnotationReplacedDelta(this.container, index, replacedAnnotation, newAnnotation));
120
121
  }
121
122
 
123
+ /**
124
+ * @return the moved and replaced annotations, as an array tuple.
125
+ */
126
+ @action moveAndReplaceAtIndexDirectly(oldIndex: number, newIndex: number): [INodeBase, INodeBase] | undefined {
127
+ checkIndex(oldIndex, this.annotations.length, false);
128
+ checkIndex(newIndex, this.annotations.length, false);
129
+ if (oldIndex !== newIndex) {
130
+ const movedAnnotation = this.annotations[oldIndex];
131
+ const replacedAnnotation = this.annotations[newIndex];
132
+ this.annotations[newIndex] = movedAnnotation;
133
+ this.annotations.splice(oldIndex, 1);
134
+ replacedAnnotation.detach();
135
+ return [movedAnnotation, replacedAnnotation];
136
+ }
137
+ return undefined;
138
+ }
139
+
140
+ @action moveAndReplaceAtIndex(oldIndex: number, newIndex: number) {
141
+ const participants = this.moveAndReplaceAtIndexDirectly(oldIndex, newIndex);
142
+ if (participants !== undefined) {
143
+ const [movedAnnotation, replacedAnnotation] = participants;
144
+ this.emitDelta(() => new AnnotationMovedAndReplacedInSameParentDelta(this.container, oldIndex, newIndex, replacedAnnotation, movedAnnotation));
145
+ }
146
+ }
147
+
122
148
  @action removeDirectly(annotationToRemove: INodeBase): number {
123
149
  const index = this.annotations.indexOf(annotationToRemove);
124
150
  if (index > -1) {
@@ -29,12 +29,12 @@ export abstract class ValueManager {
29
29
  }
30
30
 
31
31
  /**
32
- * Emits a delta if a {@link DeltaHandler} is registered with the container.
33
- * @param deltaThunk a thunk that generates the delta, and is only called when a delta handler is registered with the container.
32
+ * Emits a delta if a {@link DeltaReceiver} is registered with the container.
33
+ * @param deltaThunk a thunk that generates the delta, and is only called when a delta receiver is registered with the container.
34
34
  */
35
35
  emitDelta(deltaThunk: () => IDelta) {
36
- if (this.container.handleDelta) {
37
- this.container.handleDelta(deltaThunk());
36
+ if (this.container.receiveDelta) {
37
+ this.container.receiveDelta(deltaThunk());
38
38
  }
39
39
  }
40
40
 
@@ -18,8 +18,18 @@
18
18
  import { Containment } from "@lionweb/core"
19
19
  import { action, observable } from "mobx"
20
20
 
21
- import { INodeBase, removeFromParent } from "../base-types.js"
22
- import { ChildAddedDelta, ChildDeletedDelta, ChildMovedDelta, ChildMovedInSameContainmentDelta, ChildReplacedDelta } from "../deltas/index.js"
21
+ import { INodeBase } from "../base-types.js"
22
+ import {
23
+ ChildAddedDelta,
24
+ ChildDeletedDelta,
25
+ ChildMovedAndReplacedFromOtherContainmentDelta,
26
+ ChildMovedAndReplacedFromOtherContainmentInSameParentDelta,
27
+ ChildMovedAndReplacedInSameContainmentDelta,
28
+ ChildMovedFromOtherContainmentDelta,
29
+ ChildMovedFromOtherContainmentInSameParentDelta,
30
+ ChildMovedInSameContainmentDelta,
31
+ ChildReplacedDelta
32
+ } from "../deltas/index.js"
23
33
  import { checkIndex, FeatureValueManager } from "./base.js"
24
34
 
25
35
 
@@ -68,11 +78,47 @@ export abstract class SingleContainmentValueManager<T extends INodeBase> extends
68
78
  @action addDirectly(newChild: T) {
69
79
  const oldChild = this.getDirectly();
70
80
  if (oldChild !== undefined) {
71
- throw new Error(`replacing a child using addDirectly on a value manager for a single-valued containment isn't allowed`); // FIXME unit test this
81
+ throw new Error(`replacing a child using addDirectly on a value manager for a single-valued containment isn't allowed`); // TODO unit test this
72
82
  }
73
83
  this.child.set(newChild);
74
84
  }
75
85
 
86
+ @action replaceWith(movedChild: T) {
87
+ const replacedChild = this.getDirectly();
88
+ if (replacedChild === undefined) {
89
+ // not a proper replace, but an add-set => delegate to regular setter (— unfortunately, through necessarily “unfolding the hierarchy”):
90
+ if (this instanceof OptionalSingleContainmentValueManager) {
91
+ this.set(movedChild);
92
+ }
93
+ if (this instanceof RequiredSingleContainmentValueManager) {
94
+ this.set(movedChild);
95
+ }
96
+ } else {
97
+ if (replacedChild === movedChild) {
98
+ // do nothing: nothing's changed
99
+ } else {
100
+ if (movedChild.parent === undefined) {
101
+ this.emitDelta(() => new ChildReplacedDelta(this.container, this.feature, 0, replacedChild, movedChild));
102
+ } else {
103
+ if (movedChild.parent === this.container) {
104
+ if (movedChild.containment === this.containment) {
105
+ this.emitDelta(() => new ChildMovedAndReplacedInSameContainmentDelta(this.container, this.containment, 0, 0, movedChild, replacedChild));
106
+ } else {
107
+ const oldIndex = removeFromContainment(replacedChild);
108
+ this.emitDelta(() => new ChildMovedAndReplacedFromOtherContainmentInSameParentDelta(this.container, movedChild.containment!, oldIndex, this.containment, 0, movedChild, replacedChild));
109
+ }
110
+ } else {
111
+ const oldIndex = removeFromContainment(replacedChild);
112
+ this.emitDelta(() => new ChildMovedAndReplacedFromOtherContainmentDelta(this.container, this.containment, 0, movedChild, movedChild.parent!, movedChild.containment!, oldIndex, replacedChild));
113
+ }
114
+ }
115
+ this.setDirectly(movedChild);
116
+ movedChild.attachTo(this.container, this.containment);
117
+ replacedChild.detach();
118
+ }
119
+ }
120
+ }
121
+
76
122
  }
77
123
 
78
124
 
@@ -95,8 +141,8 @@ export class OptionalSingleContainmentValueManager<T extends INodeBase> extends
95
141
  } else {
96
142
  if (newChild.parent && newChild.containment) {
97
143
  const oldParent = newChild.parent;
98
- removeFromParent(oldParent, newChild);
99
- this.emitDelta(() => new ChildMovedDelta(oldParent, newChild.containment!, 0, this.container, this.feature, 0, newChild));
144
+ removeFromContainment(newChild);
145
+ this.emitDelta(() => new ChildMovedFromOtherContainmentDelta(oldParent, newChild.containment!, 0, this.container, this.feature, 0, newChild));
100
146
  } else {
101
147
  this.emitDelta(() => new ChildAddedDelta(this.container, this.feature, 0, newChild));
102
148
  }
@@ -112,13 +158,14 @@ export class OptionalSingleContainmentValueManager<T extends INodeBase> extends
112
158
  if (oldChild === newChild) {
113
159
  // do nothing: nothing's changed
114
160
  } else {
161
+ // FIXME this could emit 2 deltas where it should be a single ChildReplaced-delta
115
162
  if (oldChild.parent && oldChild.containment && oldChild.parent === this.container && oldChild.containment === this.feature) {
116
163
  // FIXME oldChild.parent COULD be this.container
117
164
  this.emitDelta(() => new ChildDeletedDelta(this.container, this.feature, 0, oldChild));
118
165
  }
119
166
  oldChild.detach();
120
167
  if (newChild.parent && newChild.containment) {
121
- removeFromParent(newChild.parent, newChild);
168
+ removeFromContainment(newChild);
122
169
  }
123
170
  this.setDirectly(newChild);
124
171
  newChild.attachTo(this.container, this.feature);
@@ -154,8 +201,8 @@ export class RequiredSingleContainmentValueManager<T extends INodeBase> extends
154
201
  } else {
155
202
  if (newChild.parent && newChild.containment) {
156
203
  const oldParent = newChild.parent;
157
- removeFromParent(oldParent, newChild);
158
- this.emitDelta(() => new ChildMovedDelta(oldParent, newChild.containment!, 0, this.container, this.feature, 0, newChild));
204
+ removeFromContainment(newChild);
205
+ this.emitDelta(() => new ChildMovedFromOtherContainmentDelta(oldParent, newChild.containment!, 0, this.container, this.feature, 0, newChild));
159
206
  } else {
160
207
  this.emitDelta(() => new ChildAddedDelta(this.container, this.feature, 0, newChild));
161
208
  }
@@ -175,7 +222,7 @@ export class RequiredSingleContainmentValueManager<T extends INodeBase> extends
175
222
  }
176
223
  oldChild.detach();
177
224
  if (newChild.parent && newChild.containment) {
178
- removeFromParent(newChild.parent, newChild);
225
+ removeFromContainment(newChild);
179
226
  }
180
227
  this.setDirectly(newChild);
181
228
  newChild.attachTo(this.container, this.feature);
@@ -222,21 +269,36 @@ export abstract class MultiContainmentValueManager<T extends INodeBase> extends
222
269
  this.children.splice(index, 0, newChild);
223
270
  }
224
271
 
225
- @action insertAtIndex(newChild: T, index: number) {
226
- this.insertAtIndexDirectly(newChild, index);
227
- if (newChild.parent === undefined && newChild.containment === undefined) {
228
- this.emitDelta(() => new ChildAddedDelta(this.container, this.containment, index, newChild));
272
+ @action insertAtIndex(newChild: T, newIndex: number) {
273
+ if (newChild.parent === undefined) {
274
+ this.insertAtIndexDirectly(newChild, newIndex);
275
+ newChild.attachTo(this.container, this.containment);
276
+ this.emitDelta(() => new ChildAddedDelta(this.container, this.containment, newIndex, newChild));
229
277
  } else {
230
- const oldIndex = removeFromParent(newChild.parent, newChild);
231
- this.emitDelta(() => new ChildMovedDelta(newChild.parent!, newChild.containment!, oldIndex, this.container, this.containment, index, newChild));
232
- newChild.detach();
278
+ if (newChild.parent === this.container) {
279
+ if (newChild.containment === this.containment) {
280
+ const oldIndex = this.children.indexOf(newChild);
281
+ this.moveDirectly(oldIndex, newIndex);
282
+ this.emitDelta(() => new ChildMovedInSameContainmentDelta(this.container, this.containment, oldIndex, newIndex, newChild));
283
+ } else {
284
+ const oldIndex = removeFromContainment(newChild);
285
+ checkIndex(newIndex, this.children.length, true);
286
+ this.insertAtIndexDirectly(newChild, newIndex);
287
+ this.emitDelta(() => new ChildMovedFromOtherContainmentInSameParentDelta(this.container, newChild.containment!, oldIndex, newChild, this.containment, newIndex));
288
+ newChild.attachTo(this.container, this.containment);
289
+ }
290
+ } else {
291
+ const oldIndex = removeFromContainment(newChild);
292
+ this.insertAtIndexDirectly(newChild, newIndex);
293
+ this.emitDelta(() => new ChildMovedFromOtherContainmentDelta(newChild.parent!, newChild.containment!, oldIndex, this.container, this.containment, newIndex, newChild));
294
+ newChild.attachTo(this.container, this.containment);
295
+ }
233
296
  }
234
- newChild.attachTo(this.container, this.containment);
235
297
  }
236
298
 
237
299
  @action removeDirectly(childToRemove: T): number {
238
300
  const children = this.getDirectly();
239
- const index = children.findIndex((child) => child === childToRemove);
301
+ const index = children.indexOf(childToRemove);
240
302
  if (index > -1) {
241
303
  children.splice(index, 1);
242
304
  return index;
@@ -244,6 +306,11 @@ export abstract class MultiContainmentValueManager<T extends INodeBase> extends
244
306
  return -1;
245
307
  }
246
308
 
309
+ @action removeAtIndexDirectly(index: number) {
310
+ checkIndex(index, this.children.length, false);
311
+ this.getDirectly().splice(index, 1);
312
+ }
313
+
247
314
  @action moveDirectly(oldIndex: number, newIndex: number): T | undefined {
248
315
  checkIndex(oldIndex, this.children.length, false);
249
316
  checkIndex(newIndex, this.children.length, false);
@@ -262,6 +329,40 @@ export abstract class MultiContainmentValueManager<T extends INodeBase> extends
262
329
  }
263
330
  }
264
331
 
332
+ @action replaceAtIndex(movedChild: T, newIndex: number) {
333
+ checkIndex(newIndex, this.children.length, false);
334
+ if (movedChild.parent === undefined) {
335
+ throw new Error(`a child-to-move (id=${movedChild.id}) must already be contained`);
336
+ }
337
+ const replacedChild = this.children[newIndex];
338
+ if (replacedChild === movedChild) {
339
+ // do nothing: nothing's changed
340
+ } else {
341
+ this.children.splice(newIndex, 1, movedChild);
342
+ if (replacedChild.parent !== undefined) {
343
+ const oldValueManager = replacedChild.parent.getContainmentValueManager(replacedChild.containment!);
344
+ const oldIndex = oldValueManager instanceof SingleContainmentValueManager
345
+ ? 0
346
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
347
+ : (oldValueManager as MultiContainmentValueManager<any>).children.indexOf(replacedChild);
348
+ if (replacedChild.parent === movedChild.parent) {
349
+ if (replacedChild.containment === movedChild.containment) {
350
+ this.emitDelta(() => new ChildMovedAndReplacedInSameContainmentDelta(this.container, this.containment, oldIndex, newIndex, movedChild, replacedChild));
351
+ } else {
352
+ this.emitDelta(() => new ChildMovedAndReplacedFromOtherContainmentInSameParentDelta(this.container, replacedChild.containment!, oldIndex, this.containment, newIndex, movedChild, replacedChild));
353
+ }
354
+ } else {
355
+ this.emitDelta(() => new ChildMovedAndReplacedFromOtherContainmentDelta(this.container, this.containment, newIndex, movedChild, movedChild.parent!, movedChild.containment!, oldIndex, replacedChild));
356
+ }
357
+ replacedChild.detach();
358
+ } else {
359
+ // not a move+replace, but a regular replace instead:
360
+ this.emitDelta(() => new ChildReplacedDelta(this.container, this.containment, newIndex, replacedChild, movedChild));
361
+ }
362
+ movedChild.attachTo(this.container, this.containment);
363
+ }
364
+ }
365
+
265
366
  }
266
367
 
267
368
 
@@ -274,7 +375,7 @@ export class OptionalMultiContainmentValueManager<T extends INodeBase> extends M
274
375
 
275
376
  @action remove(childToRemove: T) {
276
377
  const children = this.getDirectly();
277
- const index = children.findIndex((child) => child === childToRemove);
378
+ const index = children.indexOf(childToRemove);
278
379
  if (index > -1) {
279
380
  children.splice(index, 1);
280
381
  childToRemove.detach();
@@ -302,7 +403,7 @@ export class RequiredMultiContainmentValueManager<T extends INodeBase> extends M
302
403
 
303
404
  @action remove(childToRemove: T) {
304
405
  const children = this.getDirectly();
305
- const index = children.findIndex((child) => child === childToRemove);
406
+ const index = children.indexOf(childToRemove);
306
407
  if (index > -1) {
307
408
  if (children.length === 1) {
308
409
  this.throwOnUnset();
@@ -315,3 +416,27 @@ export class RequiredMultiContainmentValueManager<T extends INodeBase> extends M
315
416
 
316
417
  }
317
418
 
419
+ /**
420
+ * Removes the given child node from its containment, and returns the containment index it had before removal.
421
+ */
422
+ const removeFromContainment = (child: INodeBase): number => {
423
+ if (child.parent === undefined) {
424
+ throw new Error(`can't remove an orphan from its parent`)
425
+ }
426
+ if (child.containment === undefined) {
427
+ throw new Error(`can't remove an orphan from its containing feature`)
428
+ }
429
+ if (child.containment === null) {
430
+ throw new Error(`can't remove an annotation`)
431
+ }
432
+ const valueManager = child.parent.getContainmentValueManager(child.containment)
433
+ if (valueManager instanceof SingleContainmentValueManager) {
434
+ valueManager.setDirectly(undefined)
435
+ return 0
436
+ } else if (valueManager instanceof MultiContainmentValueManager) {
437
+ return valueManager.removeDirectly(child)
438
+ } else {
439
+ throw new Error(`don't know how to remove a child that's contained through a value manager of type ${valueManager.constructor.name}`)
440
+ }
441
+ }
442
+