Skip to content

Commit 24e7224

Browse files
author
Oleksandr Dzhychko
authored
Merge pull request #1297 from modelix/MODELIX-920
fix(mps-model-adapters): fix MPSNode.replaceNode
2 parents 0858d83 + 954860b commit 24e7224

File tree

3 files changed

+131
-33
lines changed

3 files changed

+131
-33
lines changed
Lines changed: 101 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,119 @@
11
package org.modelix.model.mpsadapters
22

3-
import org.junit.Ignore
3+
import jetbrains.mps.smodel.SNode
4+
import jetbrains.mps.smodel.adapter.MetaAdapterByDeclaration
45
import org.modelix.model.api.BuiltinLanguages
56
import org.modelix.model.api.ConceptReference
67
import org.modelix.model.api.INode
78
import org.modelix.model.api.IReplaceableNode
89

9-
@Ignore("Replacing a node through MPS-model-adapters is broken. See MODELIX-920")
1010
class ReplaceNodeTest : MpsAdaptersTestBase("SimpleProject") {
1111

12-
fun testReplaceNode() {
13-
readAction {
14-
assertEquals(1, mpsProject.projectModules.size)
15-
}
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 {
17+
val rootNode = getRootUnderTest()
18+
val nodeToReplace = rootNode.allChildren.first() as IReplaceableNode
19+
val oldContainmentLink = nodeToReplace.getContainmentLink()
20+
val nodesToKeep = rootNode.allChildren.drop(1)
21+
val oldProperties = nodeToReplace.getAllProperties().toSet()
22+
check(oldProperties.isNotEmpty()) { "Test should replace node with properties." }
23+
val oldReferences = nodeToReplace.getAllReferenceTargetRefs().toSet()
24+
check(oldReferences.isNotEmpty()) { "Test should replace node with references." }
25+
val oldChildren = nodeToReplace.allChildren.toList()
26+
check(oldChildren.isNotEmpty()) { "Test should replace node with children." }
27+
val newConcept = ConceptReference("mps:f3061a53-9226-4cc5-a443-f952ceaf5816/1083245097125")
28+
29+
val newNode = nodeToReplace.replaceNode(newConcept)
30+
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())
35+
assertEquals(oldProperties, newNode.getAllProperties().toSet())
36+
assertEquals(oldReferences, newNode.getAllReferenceTargetRefs().toSet())
37+
assertEquals(oldChildren, newNode.allChildren.toList())
38+
}
39+
40+
fun `test replace node without parent but with module (aka root node)`() = runCommandOnEDT {
41+
val rootNode = getRootUnderTest()
42+
val oldContainmentLink = rootNode.getContainmentLink()
43+
val model = getModelUnderTest()
44+
val newConcept = ConceptReference("mps:f3061a53-9226-4cc5-a443-f952ceaf5816/1083245097125")
45+
46+
val newNode = rootNode.replaceNode(newConcept)
47+
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")
1661

17-
val repositoryNode: INode = MPSRepositoryAsNode(mpsProject.repository)
62+
val newNode = freeFloatingNode.replaceNode(newConcept)
1863

19-
runCommandOnEDT {
20-
val module = repositoryNode.getChildren(BuiltinLanguages.MPSRepositoryConcepts.Repository.modules)
21-
.single { it.getPropertyValue(BuiltinLanguages.jetbrains_mps_lang_core.INamedConcept.name) == "Solution1" }
22-
val model = module.getChildren(BuiltinLanguages.MPSRepositoryConcepts.Module.models)
23-
.single { it.getPropertyValue(BuiltinLanguages.jetbrains_mps_lang_core.INamedConcept.name) == "Solution1.model1" }
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())
87+
}
2488

25-
val rootNode = model.getChildren(BuiltinLanguages.MPSRepositoryConcepts.Model.rootNodes).single() as IReplaceableNode
89+
fun `test fail to replace node with null concept`() = runCommandOnEDT {
90+
val rootNode = getRootUnderTest()
91+
val nodeToReplace = rootNode.allChildren.first() as IReplaceableNode
2692

27-
val oldProperties = rootNode.getAllProperties().toSet()
28-
val oldReferences = rootNode.getAllReferenceTargetRefs().toSet()
29-
val oldChildren = rootNode.allChildren.toList()
93+
val expectedMessage = "Cannot replace node `method1` with a null concept. Explicitly specify a concept (e.g., `BaseConcept`)."
94+
assertThrows(IllegalArgumentException::class.java, expectedMessage) {
95+
nodeToReplace.replaceNode(null)
96+
}
97+
}
3098

31-
val newConcept = ConceptReference("mps:f3061a53-9226-4cc5-a443-f952ceaf5816/1083245097125")
32-
val newNode = rootNode.replaceNode(newConcept)
99+
fun `test fail to replace node with non mps concept`() = runCommandOnEDT {
100+
val rootNode = getRootUnderTest()
101+
val nodeToReplace = rootNode.allChildren.first() as IReplaceableNode
102+
val newConcept = ConceptReference("notMpsConcept")
33103

34-
assertEquals(oldProperties, newNode.getAllProperties().toSet())
35-
assertEquals(oldReferences, newNode.getAllReferenceTargetRefs().toSet())
36-
assertEquals(oldChildren, newNode.allChildren.toList())
37-
assertEquals(newConcept, newNode.getConceptReference())
104+
val expectedMessage = "Concept UID `notMpsConcept` cannot be parsed as MPS concept."
105+
assertThrows(IllegalArgumentException::class.java, expectedMessage) {
106+
nodeToReplace.replaceNode(newConcept)
38107
}
39108
}
109+
110+
private fun getModelUnderTest(): INode {
111+
val repositoryNode = MPSRepositoryAsNode(mpsProject.repository)
112+
val module = repositoryNode.getChildren(BuiltinLanguages.MPSRepositoryConcepts.Repository.modules)
113+
.single { it.getPropertyValue(BuiltinLanguages.jetbrains_mps_lang_core.INamedConcept.name) == "Solution1" }
114+
return module.getChildren(BuiltinLanguages.MPSRepositoryConcepts.Module.models).single()
115+
}
116+
117+
private fun getRootUnderTest(): IReplaceableNode = getModelUnderTest()
118+
.getChildren(BuiltinLanguages.MPSRepositoryConcepts.Model.rootNodes).single() as IReplaceableNode
40119
}

mps-model-adapters-plugin/testdata/SimpleProject/solutions/Solution1/models/Solution1.model1.mps

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@
1414
</concept>
1515
<concept id="1068580123165" name="jetbrains.mps.baseLanguage.structure.InstanceMethodDeclaration" flags="ig" index="3clFb_" />
1616
<concept id="1068580123136" name="jetbrains.mps.baseLanguage.structure.StatementList" flags="sn" stub="5293379017992965193" index="3clFbS" />
17-
<concept id="1068581517677" name="jetbrains.mps.baseLanguage.structure.VoidType" flags="in" index="3cqZAl" />
1817
<concept id="1107461130800" name="jetbrains.mps.baseLanguage.structure.Classifier" flags="ng" index="3pOWGL">
1918
<child id="5375687026011219971" name="member" index="jymVt" unordered="true" />
2019
</concept>
20+
<concept id="1107535904670" name="jetbrains.mps.baseLanguage.structure.ClassifierType" flags="in" index="3uibUv">
21+
<reference id="1107535924139" name="classifier" index="3uigEE" />
22+
</concept>
2123
<concept id="1178549954367" name="jetbrains.mps.baseLanguage.structure.IVisible" flags="ng" index="1B3ioH">
2224
<child id="1178549979242" name="visibility" index="1B3o_S" />
2325
</concept>
@@ -33,7 +35,7 @@
3335
<property role="TrG5h" value="Class1" />
3436
<node concept="3clFb_" id="3cIAtmcX1Te" role="jymVt">
3537
<property role="TrG5h" value="method1" />
36-
<node concept="3cqZAl" id="3cIAtmcX1Tg" role="3clF45" />
38+
<ref role="3uigEE" node="3cIAtmcX1Sw" resolve="Class1" />
3739
<node concept="3Tm1VV" id="3cIAtmcX1Th" role="1B3o_S" />
3840
<node concept="3clFbS" id="3cIAtmcX1Ti" role="3clF47" />
3941
</node>

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

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,9 @@ package org.modelix.model.mpsadapters
33
import jetbrains.mps.lang.smodel.generator.smodelAdapter.SNodeOperations
44
import jetbrains.mps.smodel.MPSModuleRepository
55
import jetbrains.mps.smodel.adapter.MetaAdapterByDeclaration
6-
import jetbrains.mps.smodel.adapter.ids.SConceptId
76
import jetbrains.mps.smodel.adapter.ids.SContainmentLinkId
87
import jetbrains.mps.smodel.adapter.ids.SPropertyId
98
import jetbrains.mps.smodel.adapter.ids.SReferenceLinkId
10-
import jetbrains.mps.smodel.adapter.structure.concept.SConceptAdapterById
119
import jetbrains.mps.smodel.adapter.structure.link.SContainmentLinkAdapterById
1210
import jetbrains.mps.smodel.adapter.structure.property.SPropertyAdapterById
1311
import jetbrains.mps.smodel.adapter.structure.ref.SReferenceLinkAdapterById
@@ -62,21 +60,40 @@ data class MPSNode(val node: SNode) : IDefaultNodeAdapter, IReplaceableNode {
6260
}
6361

6462
override fun replaceNode(concept: ConceptReference?): INode {
65-
requireNotNull(concept) { "Can't replace $node with null concept. Use BaseConcept explicitly." }
63+
requireNotNull(concept) { "Cannot replace node `$node` with a null concept. Explicitly specify a concept (e.g., `BaseConcept`)." }
64+
val mpsConcept = MPSConcept.tryParseUID(concept.uid)
65+
requireNotNull(mpsConcept) { "Concept UID `${concept.uid}` cannot be parsed as MPS concept." }
66+
val sConcept = MetaAdapterByDeclaration.asInstanceConcept(mpsConcept.concept)
67+
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+
}
6688

67-
val id = node.nodeId
68-
val model = checkNotNull(node.model) { "Node is not part of a model" }
69-
val newNode = model.createNode(SConceptAdapterById(SConceptId.deserialize(concept.uid), ""), id)
7089
node.properties.forEach { newNode.setProperty(it, node.getProperty(it)) }
7190
node.references.forEach { newNode.setReference(it.link, it.targetNodeReference) }
7291
node.children.forEach { child ->
7392
val link = checkNotNull(child.containmentLink) { "Containment link of child node not found" }
93+
node.removeChild(child)
7494
newNode.addChild(link, child)
7595
}
7696

77-
val parent = checkNotNull(node.parent) { "Cannot replace node without a parent" }
78-
parent.insertChildBefore(getMPSContainmentLink(getContainmentLink()), node, newNode)
79-
node.delete()
8097
return MPSNode(newNode)
8198
}
8299

0 commit comments

Comments
 (0)