diff --git a/mps-model-adapters-plugin/src/test/kotlin/org/modelix/model/mpsadapters/ReplaceNodeTest.kt b/mps-model-adapters-plugin/src/test/kotlin/org/modelix/model/mpsadapters/ReplaceNodeTest.kt index e44b050bf8..93d0cd767c 100644 --- a/mps-model-adapters-plugin/src/test/kotlin/org/modelix/model/mpsadapters/ReplaceNodeTest.kt +++ b/mps-model-adapters-plugin/src/test/kotlin/org/modelix/model/mpsadapters/ReplaceNodeTest.kt @@ -1,40 +1,119 @@ package org.modelix.model.mpsadapters -import org.junit.Ignore +import jetbrains.mps.smodel.SNode +import jetbrains.mps.smodel.adapter.MetaAdapterByDeclaration import org.modelix.model.api.BuiltinLanguages import org.modelix.model.api.ConceptReference import org.modelix.model.api.INode import org.modelix.model.api.IReplaceableNode -@Ignore("Replacing a node through MPS-model-adapters is broken. See MODELIX-920") class ReplaceNodeTest : MpsAdaptersTestBase("SimpleProject") { - fun testReplaceNode() { - readAction { - assertEquals(1, mpsProject.projectModules.size) - } + private val sampleConcept = MetaAdapterByDeclaration.asInstanceConcept( + MPSConcept.tryParseUID(BuiltinLanguages.jetbrains_mps_lang_core.BaseConcept.getUID())!!.concept, + ) + + fun `test replace node with parent and module (aka regular node)`() = runCommandOnEDT { + val rootNode = getRootUnderTest() + val nodeToReplace = rootNode.allChildren.first() as IReplaceableNode + val oldContainmentLink = nodeToReplace.getContainmentLink() + val nodesToKeep = rootNode.allChildren.drop(1) + val oldProperties = nodeToReplace.getAllProperties().toSet() + check(oldProperties.isNotEmpty()) { "Test should replace node with properties." } + val oldReferences = nodeToReplace.getAllReferenceTargetRefs().toSet() + check(oldReferences.isNotEmpty()) { "Test should replace node with references." } + val oldChildren = nodeToReplace.allChildren.toList() + check(oldChildren.isNotEmpty()) { "Test should replace node with children." } + val newConcept = ConceptReference("mps:f3061a53-9226-4cc5-a443-f952ceaf5816/1083245097125") + + val newNode = nodeToReplace.replaceNode(newConcept) + + assertEquals(listOf(newNode) + nodesToKeep, rootNode.allChildren.toList()) + assertEquals((nodeToReplace as MPSNode).node.nodeId, (newNode as MPSNode).node.nodeId) + assertEquals(oldContainmentLink, newNode.getContainmentLink()) + assertEquals(newConcept, newNode.getConceptReference()) + assertEquals(oldProperties, newNode.getAllProperties().toSet()) + assertEquals(oldReferences, newNode.getAllReferenceTargetRefs().toSet()) + assertEquals(oldChildren, newNode.allChildren.toList()) + } + + fun `test replace node without parent but with module (aka root node)`() = runCommandOnEDT { + val rootNode = getRootUnderTest() + val oldContainmentLink = rootNode.getContainmentLink() + val model = getModelUnderTest() + val newConcept = ConceptReference("mps:f3061a53-9226-4cc5-a443-f952ceaf5816/1083245097125") + + val newNode = rootNode.replaceNode(newConcept) + + assertEquals(listOf(newNode), model.getChildren(BuiltinLanguages.MPSRepositoryConcepts.Model.rootNodes)) + assertEquals((rootNode as MPSNode).node.nodeId, (newNode as MPSNode).node.nodeId) + assertEquals(oldContainmentLink, newNode.getContainmentLink()) + assertEquals(newConcept, newNode.getConceptReference()) + } + + fun `test replace node without parent and without module (aka free-floating node)`() = runCommandOnEDT { + val untouchedRootNode = getRootUnderTest() + val model = getModelUnderTest() + val freeFloatingSNode = SNode(sampleConcept) + val freeFloatingNode = MPSNode(freeFloatingSNode) + val oldContainmentLink = freeFloatingNode.getContainmentLink() + val newConcept = ConceptReference("mps:f3061a53-9226-4cc5-a443-f952ceaf5816/1083245097125") - val repositoryNode: INode = MPSRepositoryAsNode(mpsProject.repository) + val newNode = freeFloatingNode.replaceNode(newConcept) - runCommandOnEDT { - val module = repositoryNode.getChildren(BuiltinLanguages.MPSRepositoryConcepts.Repository.modules) - .single { it.getPropertyValue(BuiltinLanguages.jetbrains_mps_lang_core.INamedConcept.name) == "Solution1" } - val model = module.getChildren(BuiltinLanguages.MPSRepositoryConcepts.Module.models) - .single { it.getPropertyValue(BuiltinLanguages.jetbrains_mps_lang_core.INamedConcept.name) == "Solution1.model1" } + assertEquals(listOf(untouchedRootNode), model.getChildren(BuiltinLanguages.MPSRepositoryConcepts.Model.rootNodes)) + assertEquals(freeFloatingNode.node.nodeId, (newNode as MPSNode).node.nodeId) + assertEquals(oldContainmentLink, newNode.getContainmentLink()) + assertEquals(newConcept, newNode.getConceptReference()) + } + + fun `test replace node with parent but without module (aka descendant of free-floating node)`() = runCommandOnEDT { + val freeFloatingSNode = SNode(sampleConcept) + val freeFloatingNode = MPSNode(freeFloatingSNode) + val nodeToReplace = freeFloatingNode.addNewChild( + BuiltinLanguages.MPSRepositoryConcepts.Model.usedLanguages, + -1, + BuiltinLanguages.jetbrains_mps_lang_core.BaseConcept, + ) as IReplaceableNode + val oldContainmentLink = nodeToReplace.getContainmentLink() + val newConcept = ConceptReference("mps:f3061a53-9226-4cc5-a443-f952ceaf5816/1083245097125") + + val newNode = nodeToReplace.replaceNode(newConcept) + + assertEquals(listOf(newNode), freeFloatingNode.allChildren.toList()) + assertEquals((nodeToReplace as MPSNode).node.nodeId, (newNode as MPSNode).node.nodeId) + assertEquals(oldContainmentLink, newNode.getContainmentLink()) + assertEquals(newConcept, newNode.getConceptReference()) + } - val rootNode = model.getChildren(BuiltinLanguages.MPSRepositoryConcepts.Model.rootNodes).single() as IReplaceableNode + fun `test fail to replace node with null concept`() = runCommandOnEDT { + val rootNode = getRootUnderTest() + val nodeToReplace = rootNode.allChildren.first() as IReplaceableNode - val oldProperties = rootNode.getAllProperties().toSet() - val oldReferences = rootNode.getAllReferenceTargetRefs().toSet() - val oldChildren = rootNode.allChildren.toList() + val expectedMessage = "Cannot replace node `method1` with a null concept. Explicitly specify a concept (e.g., `BaseConcept`)." + assertThrows(IllegalArgumentException::class.java, expectedMessage) { + nodeToReplace.replaceNode(null) + } + } - val newConcept = ConceptReference("mps:f3061a53-9226-4cc5-a443-f952ceaf5816/1083245097125") - val newNode = rootNode.replaceNode(newConcept) + fun `test fail to replace node with non mps concept`() = runCommandOnEDT { + val rootNode = getRootUnderTest() + val nodeToReplace = rootNode.allChildren.first() as IReplaceableNode + val newConcept = ConceptReference("notMpsConcept") - assertEquals(oldProperties, newNode.getAllProperties().toSet()) - assertEquals(oldReferences, newNode.getAllReferenceTargetRefs().toSet()) - assertEquals(oldChildren, newNode.allChildren.toList()) - assertEquals(newConcept, newNode.getConceptReference()) + val expectedMessage = "Concept UID `notMpsConcept` cannot be parsed as MPS concept." + assertThrows(IllegalArgumentException::class.java, expectedMessage) { + nodeToReplace.replaceNode(newConcept) } } + + private fun getModelUnderTest(): INode { + val repositoryNode = MPSRepositoryAsNode(mpsProject.repository) + val module = repositoryNode.getChildren(BuiltinLanguages.MPSRepositoryConcepts.Repository.modules) + .single { it.getPropertyValue(BuiltinLanguages.jetbrains_mps_lang_core.INamedConcept.name) == "Solution1" } + return module.getChildren(BuiltinLanguages.MPSRepositoryConcepts.Module.models).single() + } + + private fun getRootUnderTest(): IReplaceableNode = getModelUnderTest() + .getChildren(BuiltinLanguages.MPSRepositoryConcepts.Model.rootNodes).single() as IReplaceableNode } diff --git a/mps-model-adapters-plugin/testdata/SimpleProject/solutions/Solution1/models/Solution1.model1.mps b/mps-model-adapters-plugin/testdata/SimpleProject/solutions/Solution1/models/Solution1.model1.mps index 64b0724b32..4e065fedb6 100644 --- a/mps-model-adapters-plugin/testdata/SimpleProject/solutions/Solution1/models/Solution1.model1.mps +++ b/mps-model-adapters-plugin/testdata/SimpleProject/solutions/Solution1/models/Solution1.model1.mps @@ -14,10 +14,12 @@ - + + + @@ -33,7 +35,7 @@ - + diff --git a/mps-model-adapters/src/main/kotlin/org/modelix/model/mpsadapters/MPSNode.kt b/mps-model-adapters/src/main/kotlin/org/modelix/model/mpsadapters/MPSNode.kt index 92c5628f9a..6567a17cea 100644 --- a/mps-model-adapters/src/main/kotlin/org/modelix/model/mpsadapters/MPSNode.kt +++ b/mps-model-adapters/src/main/kotlin/org/modelix/model/mpsadapters/MPSNode.kt @@ -3,11 +3,9 @@ package org.modelix.model.mpsadapters import jetbrains.mps.lang.smodel.generator.smodelAdapter.SNodeOperations import jetbrains.mps.smodel.MPSModuleRepository import jetbrains.mps.smodel.adapter.MetaAdapterByDeclaration -import jetbrains.mps.smodel.adapter.ids.SConceptId import jetbrains.mps.smodel.adapter.ids.SContainmentLinkId import jetbrains.mps.smodel.adapter.ids.SPropertyId import jetbrains.mps.smodel.adapter.ids.SReferenceLinkId -import jetbrains.mps.smodel.adapter.structure.concept.SConceptAdapterById import jetbrains.mps.smodel.adapter.structure.link.SContainmentLinkAdapterById import jetbrains.mps.smodel.adapter.structure.property.SPropertyAdapterById import jetbrains.mps.smodel.adapter.structure.ref.SReferenceLinkAdapterById @@ -62,21 +60,40 @@ data class MPSNode(val node: SNode) : IDefaultNodeAdapter, IReplaceableNode { } override fun replaceNode(concept: ConceptReference?): INode { - requireNotNull(concept) { "Can't replace $node with null concept. Use BaseConcept explicitly." } + requireNotNull(concept) { "Cannot replace node `$node` with a null concept. Explicitly specify a concept (e.g., `BaseConcept`)." } + val mpsConcept = MPSConcept.tryParseUID(concept.uid) + requireNotNull(mpsConcept) { "Concept UID `${concept.uid}` cannot be parsed as MPS concept." } + val sConcept = MetaAdapterByDeclaration.asInstanceConcept(mpsConcept.concept) + + val maybeModel = node.model + val maybeParent = node.parent + val containmentLink = getMPSContainmentLink(getContainmentLink()) + val maybeNextSibling = node.nextSibling + // The existing node needs to be deleted before the replacing node is created, + // because `SModel.createNode` will not use the provided ID if it already exists. + node.delete() + + val newNode = if (maybeModel != null) { + maybeModel.createNode(sConcept, node.nodeId) + } else { + jetbrains.mps.smodel.SNode(sConcept, node.nodeId) + } + + if (maybeParent != null) { + // When `maybeNextSibling` is `null`, `replacingNode` is inserted as a last child. + maybeParent.insertChildBefore(containmentLink, newNode, maybeNextSibling) + } else if (maybeModel != null) { + maybeModel.addRootNode(newNode) + } - val id = node.nodeId - val model = checkNotNull(node.model) { "Node is not part of a model" } - val newNode = model.createNode(SConceptAdapterById(SConceptId.deserialize(concept.uid), ""), id) node.properties.forEach { newNode.setProperty(it, node.getProperty(it)) } node.references.forEach { newNode.setReference(it.link, it.targetNodeReference) } node.children.forEach { child -> val link = checkNotNull(child.containmentLink) { "Containment link of child node not found" } + node.removeChild(child) newNode.addChild(link, child) } - val parent = checkNotNull(node.parent) { "Cannot replace node without a parent" } - parent.insertChildBefore(getMPSContainmentLink(getContainmentLink()), node, newNode) - node.delete() return MPSNode(newNode) }