Skip to content

Commit 954860b

Browse files
author
Oleksandr Dzhychko
committed
feat(mps-model-adapters): handle root nodes and free-floating nodes in MPSNode.replaceNode
1 parent 940c553 commit 954860b

File tree

2 files changed

+84
-22
lines changed
  • mps-model-adapters-plugin/src/test/kotlin/org/modelix/model/mpsadapters
  • mps-model-adapters/src/main/kotlin/org/modelix/model/mpsadapters

2 files changed

+84
-22
lines changed

mps-model-adapters-plugin/src/test/kotlin/org/modelix/model/mpsadapters/ReplaceNodeTest.kt

Lines changed: 63 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,23 @@
11
package org.modelix.model.mpsadapters
22

3+
import jetbrains.mps.smodel.SNode
4+
import jetbrains.mps.smodel.adapter.MetaAdapterByDeclaration
35
import org.modelix.model.api.BuiltinLanguages
46
import org.modelix.model.api.ConceptReference
7+
import org.modelix.model.api.INode
58
import org.modelix.model.api.IReplaceableNode
69

710
class ReplaceNodeTest : MpsAdaptersTestBase("SimpleProject") {
811

9-
fun `test replace node`() = runCommandOnEDT {
12+
private val sampleConcept = MetaAdapterByDeclaration.asInstanceConcept(
13+
MPSConcept.tryParseUID(BuiltinLanguages.jetbrains_mps_lang_core.BaseConcept.getUID())!!.concept,
14+
)
15+
16+
fun `test replace node with parent and module (aka regular node)`() = runCommandOnEDT {
1017
val rootNode = getRootUnderTest()
1118
val nodeToReplace = rootNode.allChildren.first() as IReplaceableNode
19+
val oldContainmentLink = nodeToReplace.getContainmentLink()
20+
val nodesToKeep = rootNode.allChildren.drop(1)
1221
val oldProperties = nodeToReplace.getAllProperties().toSet()
1322
check(oldProperties.isNotEmpty()) { "Test should replace node with properties." }
1423
val oldReferences = nodeToReplace.getAllReferenceTargetRefs().toSet()
@@ -19,25 +28,62 @@ class ReplaceNodeTest : MpsAdaptersTestBase("SimpleProject") {
1928

2029
val newNode = nodeToReplace.replaceNode(newConcept)
2130

31+
assertEquals(listOf(newNode) + nodesToKeep, rootNode.allChildren.toList())
32+
assertEquals((nodeToReplace as MPSNode).node.nodeId, (newNode as MPSNode).node.nodeId)
33+
assertEquals(oldContainmentLink, newNode.getContainmentLink())
34+
assertEquals(newConcept, newNode.getConceptReference())
2235
assertEquals(oldProperties, newNode.getAllProperties().toSet())
2336
assertEquals(oldReferences, newNode.getAllReferenceTargetRefs().toSet())
2437
assertEquals(oldChildren, newNode.allChildren.toList())
25-
assertEquals(newConcept, newNode.getConceptReference())
2638
}
2739

28-
fun `test fail to replace node without parent`() = runCommandOnEDT {
40+
fun `test replace node without parent but with module (aka root node)`() = runCommandOnEDT {
2941
val rootNode = getRootUnderTest()
30-
val oldChildren = rootNode.allChildren.toList()
31-
check(oldChildren.isNotEmpty()) { "Test should replace node with children." }
42+
val oldContainmentLink = rootNode.getContainmentLink()
43+
val model = getModelUnderTest()
44+
val newConcept = ConceptReference("mps:f3061a53-9226-4cc5-a443-f952ceaf5816/1083245097125")
3245

33-
val newConcept = ConceptReference(BuiltinLanguages.jetbrains_mps_lang_core.BaseConcept.getUID())
46+
val newNode = rootNode.replaceNode(newConcept)
3447

35-
val expectedMessage = "Cannot replace node `Class1` because it has no parent."
36-
assertThrows(IllegalArgumentException::class.java, expectedMessage) {
37-
rootNode.replaceNode(newConcept)
38-
}
39-
// Assert that precondition is check before children are deleted.
40-
assertEquals(oldChildren, rootNode.allChildren.toList())
48+
assertEquals(listOf(newNode), model.getChildren(BuiltinLanguages.MPSRepositoryConcepts.Model.rootNodes))
49+
assertEquals((rootNode as MPSNode).node.nodeId, (newNode as MPSNode).node.nodeId)
50+
assertEquals(oldContainmentLink, newNode.getContainmentLink())
51+
assertEquals(newConcept, newNode.getConceptReference())
52+
}
53+
54+
fun `test replace node without parent and without module (aka free-floating node)`() = runCommandOnEDT {
55+
val untouchedRootNode = getRootUnderTest()
56+
val model = getModelUnderTest()
57+
val freeFloatingSNode = SNode(sampleConcept)
58+
val freeFloatingNode = MPSNode(freeFloatingSNode)
59+
val oldContainmentLink = freeFloatingNode.getContainmentLink()
60+
val newConcept = ConceptReference("mps:f3061a53-9226-4cc5-a443-f952ceaf5816/1083245097125")
61+
62+
val newNode = freeFloatingNode.replaceNode(newConcept)
63+
64+
assertEquals(listOf(untouchedRootNode), model.getChildren(BuiltinLanguages.MPSRepositoryConcepts.Model.rootNodes))
65+
assertEquals(freeFloatingNode.node.nodeId, (newNode as MPSNode).node.nodeId)
66+
assertEquals(oldContainmentLink, newNode.getContainmentLink())
67+
assertEquals(newConcept, newNode.getConceptReference())
68+
}
69+
70+
fun `test replace node with parent but without module (aka descendant of free-floating node)`() = runCommandOnEDT {
71+
val freeFloatingSNode = SNode(sampleConcept)
72+
val freeFloatingNode = MPSNode(freeFloatingSNode)
73+
val nodeToReplace = freeFloatingNode.addNewChild(
74+
BuiltinLanguages.MPSRepositoryConcepts.Model.usedLanguages,
75+
-1,
76+
BuiltinLanguages.jetbrains_mps_lang_core.BaseConcept,
77+
) as IReplaceableNode
78+
val oldContainmentLink = nodeToReplace.getContainmentLink()
79+
val newConcept = ConceptReference("mps:f3061a53-9226-4cc5-a443-f952ceaf5816/1083245097125")
80+
81+
val newNode = nodeToReplace.replaceNode(newConcept)
82+
83+
assertEquals(listOf(newNode), freeFloatingNode.allChildren.toList())
84+
assertEquals((nodeToReplace as MPSNode).node.nodeId, (newNode as MPSNode).node.nodeId)
85+
assertEquals(oldContainmentLink, newNode.getContainmentLink())
86+
assertEquals(newConcept, newNode.getConceptReference())
4187
}
4288

4389
fun `test fail to replace node with null concept`() = runCommandOnEDT {
@@ -61,11 +107,13 @@ class ReplaceNodeTest : MpsAdaptersTestBase("SimpleProject") {
61107
}
62108
}
63109

64-
private fun getRootUnderTest(): IReplaceableNode {
110+
private fun getModelUnderTest(): INode {
65111
val repositoryNode = MPSRepositoryAsNode(mpsProject.repository)
66112
val module = repositoryNode.getChildren(BuiltinLanguages.MPSRepositoryConcepts.Repository.modules)
67113
.single { it.getPropertyValue(BuiltinLanguages.jetbrains_mps_lang_core.INamedConcept.name) == "Solution1" }
68-
val model = module.getChildren(BuiltinLanguages.MPSRepositoryConcepts.Module.models).single()
69-
return model.getChildren(BuiltinLanguages.MPSRepositoryConcepts.Model.rootNodes).single() as IReplaceableNode
114+
return module.getChildren(BuiltinLanguages.MPSRepositoryConcepts.Module.models).single()
70115
}
116+
117+
private fun getRootUnderTest(): IReplaceableNode = getModelUnderTest()
118+
.getChildren(BuiltinLanguages.MPSRepositoryConcepts.Model.rootNodes).single() as IReplaceableNode
71119
}

mps-model-adapters/src/main/kotlin/org/modelix/model/mpsadapters/MPSNode.kt

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -61,15 +61,31 @@ data class MPSNode(val node: SNode) : IDefaultNodeAdapter, IReplaceableNode {
6161

6262
override fun replaceNode(concept: ConceptReference?): INode {
6363
requireNotNull(concept) { "Cannot replace node `$node` with a null concept. Explicitly specify a concept (e.g., `BaseConcept`)." }
64-
val parent = requireNotNull(node.parent) { "Cannot replace node `$node` because it has no parent." }
65-
6664
val mpsConcept = MPSConcept.tryParseUID(concept.uid)
6765
requireNotNull(mpsConcept) { "Concept UID `${concept.uid}` cannot be parsed as MPS concept." }
6866
val sConcept = MetaAdapterByDeclaration.asInstanceConcept(mpsConcept.concept)
6967

70-
val id = node.nodeId
71-
val model = checkNotNull(node.model) { "Node is not part of a model" }
72-
val newNode = model.createNode(sConcept, id)
68+
val maybeModel = node.model
69+
val maybeParent = node.parent
70+
val containmentLink = getMPSContainmentLink(getContainmentLink())
71+
val maybeNextSibling = node.nextSibling
72+
// The existing node needs to be deleted before the replacing node is created,
73+
// because `SModel.createNode` will not use the provided ID if it already exists.
74+
node.delete()
75+
76+
val newNode = if (maybeModel != null) {
77+
maybeModel.createNode(sConcept, node.nodeId)
78+
} else {
79+
jetbrains.mps.smodel.SNode(sConcept, node.nodeId)
80+
}
81+
82+
if (maybeParent != null) {
83+
// When `maybeNextSibling` is `null`, `replacingNode` is inserted as a last child.
84+
maybeParent.insertChildBefore(containmentLink, newNode, maybeNextSibling)
85+
} else if (maybeModel != null) {
86+
maybeModel.addRootNode(newNode)
87+
}
88+
7389
node.properties.forEach { newNode.setProperty(it, node.getProperty(it)) }
7490
node.references.forEach { newNode.setReference(it.link, it.targetNodeReference) }
7591
node.children.forEach { child ->
@@ -78,8 +94,6 @@ data class MPSNode(val node: SNode) : IDefaultNodeAdapter, IReplaceableNode {
7894
newNode.addChild(link, child)
7995
}
8096

81-
parent.insertChildBefore(getMPSContainmentLink(getContainmentLink()), newNode, node)
82-
node.delete()
8397
return MPSNode(newNode)
8498
}
8599

0 commit comments

Comments
 (0)