diff --git a/bulk-model-sync-mps/src/main/kotlin/org/modelix/mps/model/sync/bulk/MPSProjectSyncMask.kt b/bulk-model-sync-mps/src/main/kotlin/org/modelix/mps/model/sync/bulk/MPSProjectSyncMask.kt index 3dcf538876..0be9bfa0ac 100644 --- a/bulk-model-sync-mps/src/main/kotlin/org/modelix/mps/model/sync/bulk/MPSProjectSyncMask.kt +++ b/bulk-model-sync-mps/src/main/kotlin/org/modelix/mps/model/sync/bulk/MPSProjectSyncMask.kt @@ -1,6 +1,5 @@ package org.modelix.mps.model.sync.bulk -import jetbrains.mps.project.MPSProject import org.modelix.model.api.BuiltinLanguages import org.modelix.model.api.IChildLinkReference import org.modelix.model.api.IReadableNode @@ -9,7 +8,7 @@ import org.modelix.model.mpsadapters.MPSModuleAsNode import org.modelix.model.mpsadapters.MPSProjectAsNode import org.modelix.model.sync.bulk.IModelMask -class MPSProjectSyncMask(val projects: List, val isMPSSide: Boolean) : IModelMask { +class MPSProjectSyncMask(val projects: List, val isMPSSide: Boolean) : IModelMask { override fun filterChildren( parent: IReadableNode, @@ -21,7 +20,9 @@ class MPSProjectSyncMask(val projects: List, val isMPSSide: Boolean) role.matches(BuiltinLanguages.MPSRepositoryConcepts.Repository.tempModules.toReference()) -> emptyList() role.matches(BuiltinLanguages.MPSRepositoryConcepts.Repository.modules.toReference()) -> { if (isMPSSide) { - val included = projects.flatMap { it.projectModules }.map { MPSModuleAsNode(it).getNodeReference().serialize() }.toSet() + val included = projects + .flatMap { it.project.getModules() } + .map { MPSModuleAsNode(it).getNodeReference().serialize() }.toSet() children.filter { included.contains(it.getNodeReference().serialize()) } } else { children @@ -29,7 +30,7 @@ class MPSProjectSyncMask(val projects: List, val isMPSSide: Boolean) } role.matches(BuiltinLanguages.MPSRepositoryConcepts.Repository.projects.toReference()) -> { if (isMPSSide) { - val included = projects.map { MPSProjectAsNode(it).getNodeReference().serialize() }.toSet() + val included = projects.map { it.getNodeReference().serialize() }.toSet() children.filter { included.contains(it.getNodeReference().serialize()) } } else { children diff --git a/model-api/src/commonMain/kotlin/org/modelix/model/api/BuiltinLanguages.kt b/model-api/src/commonMain/kotlin/org/modelix/model/api/BuiltinLanguages.kt index c3aa01e6ad..9e845a160e 100644 --- a/model-api/src/commonMain/kotlin/org/modelix/model/api/BuiltinLanguages.kt +++ b/model-api/src/commonMain/kotlin/org/modelix/model/api/BuiltinLanguages.kt @@ -146,6 +146,11 @@ object BuiltinLanguages { uid = "0a7577d1-d4e5-431d-98b1-fae38f9aee80/474657388638618895/2206727074858242373", ).also(this::addProperty) + val isReadOnly = SimpleProperty( + "isReadOnly", + uid = "0a7577d1-d4e5-431d-98b1-fae38f9aee80/474657388638618895/4225291355523310000", + ).also(this::addProperty) + val models = SimpleChildLink( simpleName = "models", isMultiple = true, diff --git a/mps-model-adapters/src/main/kotlin/org/modelix/model/mpsadapters/MPSConcept.kt b/mps-model-adapters/src/main/kotlin/org/modelix/model/mpsadapters/MPSConcept.kt index 9183d64f9d..342c2191b4 100644 --- a/mps-model-adapters/src/main/kotlin/org/modelix/model/mpsadapters/MPSConcept.kt +++ b/mps-model-adapters/src/main/kotlin/org/modelix/model/mpsadapters/MPSConcept.kt @@ -135,3 +135,5 @@ data class MPSConcept(val concept: SAbstractConceptAdapter) : IConcept { } } } + +fun SAbstractConcept.toModelix() = MPSConcept(this) diff --git a/mps-model-adapters/src/main/kotlin/org/modelix/model/mpsadapters/MPSModuleAsNode.kt b/mps-model-adapters/src/main/kotlin/org/modelix/model/mpsadapters/MPSModuleAsNode.kt index 49af9d8b2c..e75f11b671 100644 --- a/mps-model-adapters/src/main/kotlin/org/modelix/model/mpsadapters/MPSModuleAsNode.kt +++ b/mps-model-adapters/src/main/kotlin/org/modelix/model/mpsadapters/MPSModuleAsNode.kt @@ -31,6 +31,7 @@ import org.modelix.model.api.IReferenceLinkReference import org.modelix.model.api.IWritableNode import org.modelix.model.data.asData import org.modelix.mps.api.ModelixMpsApi +import org.modelix.mps.multiplatform.model.MPSModelReference fun MPSModuleAsNode(module: SModule) = MPSModuleAsNode.create(module) @@ -107,6 +108,22 @@ abstract class MPSModuleAsNode : MPSGenericNodeAdapter() { (element as Solution).moduleDescriptor.compileInMPS = value.toBoolean() } }, + BuiltinLanguages.MPSRepositoryConcepts.Module.isReadOnly.toReference() to object : IPropertyAccessor { + override fun read(element: SModule): String? { + return element.isReadOnly.takeIf { it }?.toString() + } + + override fun write(element: SModule, value: String?) { + if (read(element).toBoolean() == value.toBoolean()) return + check(element is Solution) { + "Property 'isReadOnly' can only be changed on Solutions, but ${element.moduleName} is a ${element.javaClass.simpleName}" + } + check(!element.isPackaged) { + "Property 'isReadOnly' can't be changed on packaged modules. [module=${element.moduleName}]" + } + element.moduleDescriptor.readOnlyStubModule(value.toBoolean()) + } + }, ) private val referenceAccessors = listOf>>() @@ -123,8 +140,10 @@ abstract class MPSModuleAsNode : MPSGenericNodeAdapter() { return element.createModel( name = sourceNode.getNode().getPropertyValue(BuiltinLanguages.jetbrains_mps_lang_core.INamedConcept.name.toReference()) ?: "${element.moduleName}.unnamed", - id = sourceNode.getNode().getPropertyValue(BuiltinLanguages.MPSRepositoryConcepts.Model.id.toReference()) - ?.let { PersistenceFacade.getInstance().createModelId(it) } ?: SModelId.generate(), + id = MPSModelReference.tryConvert(sourceNode.getNode().getNodeReference())?.toMPS()?.modelId + ?: sourceNode.getNode().getPropertyValue(BuiltinLanguages.MPSRepositoryConcepts.Model.id.toReference()) + ?.let { PersistenceFacade.getInstance().createModelId(it) } + ?: SModelId.generate(), ).let { MPSModelAsNode(it) } } diff --git a/mps-model-adapters/src/main/kotlin/org/modelix/model/mpsadapters/MPSProjectAsNode.kt b/mps-model-adapters/src/main/kotlin/org/modelix/model/mpsadapters/MPSProjectAsNode.kt index ef0d0bd309..03dbe61808 100644 --- a/mps-model-adapters/src/main/kotlin/org/modelix/model/mpsadapters/MPSProjectAsNode.kt +++ b/mps-model-adapters/src/main/kotlin/org/modelix/model/mpsadapters/MPSProjectAsNode.kt @@ -13,26 +13,25 @@ import org.modelix.mps.api.ModelixMpsApi import org.modelix.mps.multiplatform.model.MPSModuleReference import org.modelix.mps.multiplatform.model.MPSProjectReference -data class MPSProjectAsNode(val project: IMPSProject) : MPSGenericNodeAdapter() { +data class MPSProjectAsNode(private val id: MPSProjectReference, val project: IMPSProject) : MPSGenericNodeAdapter() { + + constructor(project: IMPSProject) : this(MPSProjectReference(project), project) companion object { - val CONTEXT_PROJECTS = ContextValue>(emptyList()) + private val CONTEXT_PROJECTS = ContextValue>(emptyList()) - fun getContextProject(): IMPSProject { - return CONTEXT_PROJECTS.getValueOrNull()?.lastOrNull() ?: MPSProjectAdapter(ModelixMpsApi.getMPSProject()) + fun getContextProjectNode(): MPSProjectAsNode { + return CONTEXT_PROJECTS.getValueOrNull()?.lastOrNull() ?: MPSProjectAsNode(ModelixMpsApi.getMPSProject()) } - fun getContextProjects(): List { + fun getContextProject(): IMPSProject = getContextProjectNode().project + + fun getContextProjectNodes(): List { return CONTEXT_PROJECTS.getValueOrNull()?.takeIf { it.isNotEmpty() } - ?: ModelixMpsApi.getMPSProjects().map { MPSProjectAdapter(it) } + ?: ModelixMpsApi.getMPSProjects().map { MPSProjectAsNode(MPSProjectAdapter(it)) } } - fun getAllProjects(): List { - return ( - ModelixMpsApi.getMPSProjects().map { MPSProjectAdapter(it) } + - CONTEXT_PROJECTS.getValueOrNull().orEmpty() - ).distinct() - } + fun getContextProjects(): List = getContextProjectNodes().map { it.project } fun runWithProject(project: org.jetbrains.mps.openapi.project.Project, body: () -> R): R { return runWithProjects(listOf(project), body) @@ -46,35 +45,45 @@ data class MPSProjectAsNode(val project: IMPSProject) : MPSGenericNodeAdapter runWithProjectNode(project: MPSProjectAsNode, body: () -> R): R { + return runWithProjectNodes(listOf(project), body) + } + + fun runWithProjectNodes(projects: List, body: () -> R): R { + if (projects.isEmpty()) return body() + val newProjects = CONTEXT_PROJECTS.getValueOrNull().orEmpty() + projects + return CONTEXT_PROJECTS.computeWith(newProjects) { + ModelixMpsApi.runWithRepository(projects.last().project.getRepository(), body) + } + } + fun runWithProject(project: IMPSProject, body: () -> R): R { return runWithProjects(listOf(project), body) } fun runWithProjects(projects: List, body: () -> R): R { if (projects.isEmpty()) return body() - return CONTEXT_PROJECTS.computeWith(CONTEXT_PROJECTS.getValueOrNull().orEmpty() + projects) { - ModelixMpsApi.runWithRepository(projects.last().getRepository(), body) - } + return runWithProjectNodes(projects.map { MPSProjectAsNode(it) }, body) } - private val propertyAccessors: List>> = listOf( - BuiltinLanguages.jetbrains_mps_lang_core.INamedConcept.name.toReference() to object : IPropertyAccessor { - override fun read(element: IMPSProject): String? { - return element.getName() + private val propertyAccessors: List>> = listOf( + BuiltinLanguages.jetbrains_mps_lang_core.INamedConcept.name.toReference() to object : IPropertyAccessor { + override fun read(element: MPSProjectAsNode): String? { + return element.project.getName() } - override fun write(element: IMPSProject, value: String?) { - element.setName(value ?: "") + override fun write(element: MPSProjectAsNode, value: String?) { + element.project.setName(value ?: "") } }, ) - private val childAccessors: List>> = listOf( - BuiltinLanguages.MPSRepositoryConcepts.Project.projectModules.toReference() to object : IChildAccessor { - override fun read(element: IMPSProject): List { - return element.getModules().map { MPSProjectModuleAsNode(element, it) } + private val childAccessors: List>> = listOf( + BuiltinLanguages.MPSRepositoryConcepts.Project.projectModules.toReference() to object : IChildAccessor { + override fun read(element: MPSProjectAsNode): List { + return element.project.getModules().map { MPSProjectModuleAsNode(element, it) } } - override fun addNew(element: IMPSProject, index: Int, sourceNode: SpecWithResolvedConcept): IWritableNode { + override fun addNew(element: MPSProjectAsNode, index: Int, sourceNode: SpecWithResolvedConcept): IWritableNode { val targetModuleRef = requireNotNull(sourceNode.getNode().getReferenceTargetRef(BuiltinLanguages.MPSRepositoryConcepts.ModuleReference.module.toReference())) { "Reference to module isn't set" } @@ -88,25 +97,25 @@ data class MPSProjectAsNode(val project: IMPSProject) : MPSGenericNodeAdapter { - override fun read(element: IMPSProject): List { + BuiltinLanguages.MPSRepositoryConcepts.Project.modules.toReference() to object : IChildAccessor { + override fun read(element: MPSProjectAsNode): List { return return emptyList() // modules child link is deprecated } - override fun addNew(element: IMPSProject, index: Int, sourceNode: SpecWithResolvedConcept): IWritableNode { + override fun addNew(element: MPSProjectAsNode, index: Int, sourceNode: SpecWithResolvedConcept): IWritableNode { throw UnsupportedOperationException("read only") } - override fun remove(element: IMPSProject, child: IWritableNode) { + override fun remove(element: MPSProjectAsNode, child: IWritableNode) { throw UnsupportedOperationException("read only") } }, @@ -115,19 +124,19 @@ data class MPSProjectAsNode(val project: IMPSProject) : MPSGenericNodeAdapter>> { + override fun getPropertyAccessors(): List>> { return propertyAccessors } - override fun getReferenceAccessors(): List>> { + override fun getReferenceAccessors(): List>> { return emptyList() } - override fun getChildAccessors(): List>> { + override fun getChildAccessors(): List>> { return childAccessors } @@ -136,7 +145,7 @@ data class MPSProjectAsNode(val project: IMPSProject) : MPSGenericNodeAdapter() { +data class MPSProjectModuleAsNode(val projectNode: MPSProjectAsNode, val module: SModule) : MPSGenericNodeAdapter() { companion object { private val propertyAccessors: List>> = listOf( @@ -43,6 +43,8 @@ data class MPSProjectModuleAsNode(val project: IMPSProject, val module: SModule) ) } + val project: IMPSProject get() = projectNode.project + override fun getElement(): MPSProjectModuleAsNode { return this } @@ -60,7 +62,7 @@ data class MPSProjectModuleAsNode(val project: IMPSProject, val module: SModule) } override fun getParent(): MPSProjectAsNode { - return MPSProjectAsNode(project) + return projectNode } override fun getNodeReference(): INodeReference { diff --git a/mps-model-adapters/src/main/kotlin/org/modelix/model/mpsadapters/MPSRepositoryAsNode.kt b/mps-model-adapters/src/main/kotlin/org/modelix/model/mpsadapters/MPSRepositoryAsNode.kt index 7f3b141133..f2e86dc82f 100644 --- a/mps-model-adapters/src/main/kotlin/org/modelix/model/mpsadapters/MPSRepositoryAsNode.kt +++ b/mps-model-adapters/src/main/kotlin/org/modelix/model/mpsadapters/MPSRepositoryAsNode.kt @@ -66,6 +66,7 @@ data class MPSRepositoryAsNode(@get:JvmName("getRepository_") val repository: SR requireNotNull(sourceNode.getNode().getPropertyValue(BuiltinLanguages.MPSRepositoryConcepts.Module.id.toReference())) { "Solution has no ID: ${sourceNode.getNode()}" }.let { ModuleId.fromString(it) }, + sourceNode.getNode().getPropertyValue(BuiltinLanguages.MPSRepositoryConcepts.Module.isReadOnly.toReference()).toBoolean(), ).let { MPSModuleAsNode(it) } } BuiltinLanguages.MPSRepositoryConcepts.Language.getReference() -> { @@ -111,7 +112,7 @@ data class MPSRepositoryAsNode(@get:JvmName("getRepository_") val repository: SR }, BuiltinLanguages.MPSRepositoryConcepts.Repository.projects.toReference() to object : IChildAccessor { override fun read(element: SRepository): List { - return MPSProjectAsNode.getContextProjects().map { MPSProjectAsNode(it) } + return MPSProjectAsNode.getContextProjectNodes() } override fun addNew(element: SRepository, index: Int, sourceNode: SpecWithResolvedConcept): IWritableNode { diff --git a/mps-model-adapters/src/main/kotlin/org/modelix/model/mpsadapters/SolutionProducer.kt b/mps-model-adapters/src/main/kotlin/org/modelix/model/mpsadapters/SolutionProducer.kt index 7424778419..db99fa5c8e 100644 --- a/mps-model-adapters/src/main/kotlin/org/modelix/model/mpsadapters/SolutionProducer.kt +++ b/mps-model-adapters/src/main/kotlin/org/modelix/model/mpsadapters/SolutionProducer.kt @@ -20,23 +20,23 @@ import jetbrains.mps.vfs.IFile class SolutionProducer(private val myProject: IMPSProject) { - fun create(name: String, id: ModuleId): Solution { + fun create(name: String, id: ModuleId, readOnly: Boolean = false): Solution { val basePath = checkNotNull(myProject.getBasePath()) { "Project has no base path: $myProject" } val projectBaseDir = myProject.getFileSystem().getFile(basePath) val solutionBaseDir = projectBaseDir.findChild("solutions").findChild(name) - return create(name, id, solutionBaseDir) + return create(name, id, solutionBaseDir, readOnly) } - fun create(namespace: String, id: ModuleId, moduleDir: IFile): Solution { + fun create(namespace: String, id: ModuleId, moduleDir: IFile, readOnly: Boolean): Solution { val descriptorFile = moduleDir.findChild(namespace + MPSExtentions.DOT_SOLUTION) - val descriptor: SolutionDescriptor = createSolutionDescriptor(namespace, id, descriptorFile) + val descriptor: SolutionDescriptor = createSolutionDescriptor(namespace, id, descriptorFile, readOnly) val module = GeneralModuleFactory().instantiate(descriptor, descriptorFile) as Solution myProject.addModule(module) module.save() return module } - private fun createSolutionDescriptor(namespace: String, id: ModuleId, descriptorFile: IFile): SolutionDescriptor { + private fun createSolutionDescriptor(namespace: String, id: ModuleId, descriptorFile: IFile, readOnly: Boolean): SolutionDescriptor { val descriptor = SolutionDescriptor() // using outputPath instead of outputRoot for backwards compatibility // descriptor.outputRoot = "\${module}/source_gen" @@ -53,6 +53,7 @@ class SolutionProducer(private val myProject: IMPSProject) { descriptor.modelRootDescriptors.add(DefaultModelRoot.createDescriptor(modelsDir.parent!!, modelsDir)) descriptor.outputPath = descriptorFile.parent!!.findChild("source_gen").path + descriptor.readOnlyStubModule(readOnly) return descriptor } } diff --git a/mps-multiplatform-lib/src/commonMain/kotlin/org/modelix/mps/multiplatform/model/MPSModelReference.kt b/mps-multiplatform-lib/src/commonMain/kotlin/org/modelix/mps/multiplatform/model/MPSModelReference.kt index b36f650b93..8728d659a4 100644 --- a/mps-multiplatform-lib/src/commonMain/kotlin/org/modelix/mps/multiplatform/model/MPSModelReference.kt +++ b/mps-multiplatform-lib/src/commonMain/kotlin/org/modelix/mps/multiplatform/model/MPSModelReference.kt @@ -73,7 +73,7 @@ data class MPSModelReference(val moduleReference: MPSModuleReference?, val model return arrayOf(moduleId, modelID, moduleName, modelName) } - fun random() = MPSModelReference(null, randomUUID()) + fun random(): MPSModelReference = MPSModelReference(null, "r:" + randomUUID()) } override fun serialize(): String { diff --git a/mps-sync-plugin3/src/main/kotlin/org/modelix/mps/sync3/BindingWorker.kt b/mps-sync-plugin3/src/main/kotlin/org/modelix/mps/sync3/BindingWorker.kt index 9965c134e8..94b7e5db10 100644 --- a/mps-sync-plugin3/src/main/kotlin/org/modelix/mps/sync3/BindingWorker.kt +++ b/mps-sync-plugin3/src/main/kotlin/org/modelix/mps/sync3/BindingWorker.kt @@ -5,7 +5,6 @@ import com.intellij.openapi.application.ModalityState import io.ktor.client.plugins.ResponseException import io.ktor.http.HttpStatusCode import io.ktor.utils.io.CancellationException -import jetbrains.mps.project.MPSProject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.async @@ -36,7 +35,6 @@ import org.modelix.model.api.getOriginalReference import org.modelix.model.area.PArea import org.modelix.model.lazy.BranchReference import org.modelix.model.lazy.CLVersion -import org.modelix.model.mpsadapters.MPSProjectAdapter import org.modelix.model.mpsadapters.MPSProjectAsNode import org.modelix.model.mpsadapters.MPSRepositoryAsNode import org.modelix.model.mpsadapters.computeRead @@ -53,11 +51,14 @@ import org.modelix.mps.model.sync.bulk.MPSProjectSyncMask import org.modelix.mps.multiplatform.model.MPSProjectReference import org.modelix.streams.iterateSuspending import java.util.concurrent.atomic.AtomicBoolean +import kotlin.collections.map class SyncTarget( val serverConnection: ModelSyncService.Connection, val bindingId: BindingId, val initialVersionHash: String?, + val readonly: Boolean, + val projectId: MPSProjectReference?, ) { suspend fun client() = serverConnection.getClient() val branchRef: BranchReference get() = bindingId.branchRef @@ -65,7 +66,7 @@ class SyncTarget( class BindingWorker( val coroutinesScope: CoroutineScope, - val mpsProject: MPSProject, + val mpsProject: MPSProjectAsNode, val syncTargets: List, val continueOnError: () -> Boolean, ) { @@ -82,7 +83,7 @@ class BindingWorker( private var previousSyncStack: List = emptyList() private var status: List = syncTargets.map { IBinding.Status.Disabled } - private val repository: SRepository get() = mpsProject.repository + private val repository: SRepository get() = mpsProject.project.getRepository() init { require(syncTargets.isNotEmpty()) @@ -173,10 +174,12 @@ class BindingWorker( check(syncJob?.isActive == true) { "Synchronization is not active" } var reason = checkInSync() var i = 0 + var delay = 10.0 while (reason != null) { i++ if (i % 10 == 0) LOG.debug { "Still waiting for the synchronization to finish: $reason" } - delay(100) + delay(delay.toLong()) + delay = (delay * 1.5).coerceAtMost(3000.0) reason = checkInSync() } return lastSyncedVersion.getValue()!! @@ -225,8 +228,19 @@ class BindingWorker( // continuous sync to MPS val latestHashes: List> = forEachTargetIndexed> { index -> channelFlow { + // The `combine` operator returns the latest values of all flows whenever one of them changes, but it + // only starts streaming values when each stream returned at least one value. + // If we don't send a first value here, changes are not detected until the first pollHash call for each + // repository timed out. The synchronization is effectively blocked for the first ~30 seconds. + channel.send(lastSyncedVersion.getValue()?.get(index)?.getContentHash() ?: "") + launchLoop { - channel.send(client().pollHash(branchRef, lastSyncedVersion.getValue()?.get(index))) + val lastKnownVersion = lastSyncedVersion.getValue()?.get(index) + channel.send( + client().pollHash(branchRef, lastKnownVersion).also { hash -> + LOG.trace { "pollHash($branchRef, $lastKnownVersion) -> $hash" } + }, + ) } } } @@ -338,7 +352,22 @@ class BindingWorker( LOG.debug { "Updating MPS project from $oldVersions to $newVersions" } val newTrees = newVersions.map { it.getModelTree() } - val sourceModel = SyncTargetModel(newTrees.map { it.asModelSingleThreaded() }) + val sourceModel = SyncTargetModel( + mpsProject, + newVersions.zip(syncTargets).map { (newVersion, syncTarget) -> + val model = newVersion.getModelTree().asModelSingleThreaded() + val projectNode: () -> IWritableNode? = { + findMatchingProjectNode(model.getRootNode()) as IWritableNode? + } + SyncTargetConfig( + model = model, + readonly = syncTarget.readonly, + projectId = syncTarget.projectId + ?: projectNode()?.getNodeReference()?.let { MPSProjectReference.tryConvert(it) } + ?: mpsProject.getNodeReference(), + ) + }, + ) val baseVersions = oldVersions val filter = if (baseVersions.all { it != null } && incremental) { val invalidationTree = DefaultInvalidationTree(sourceModel.getRootNode().getNodeReference(), 100_000) @@ -380,8 +409,8 @@ class BindingWorker( val projectNode: IReadableNode? = findMatchingProjectNode(sourceRoot) if (projectNode != null) { val projectId = getProjectId(projectNode) - if (projectId != getProjectId(MPSProjectAsNode(mpsProject))) { - MPSProjectAdapter(mpsProject).setName(projectId) + if (projectId != mpsProject.getNodeReference().projectName) { + mpsProject.project.setName(projectId) } } @@ -412,7 +441,7 @@ class BindingWorker( ApplicationManager.getApplication().invokeAndWait({ ApplicationManager.getApplication().runWriteAction { repository.modelAccess.executeUndoTransparentCommand { - MPSProjectAsNode.runWithProject(mpsProject) { + MPSProjectAsNode.runWithProjectNode(mpsProject) { result += body() } } @@ -446,27 +475,35 @@ class BindingWorker( fun sync(invalidationTree: ModelSynchronizer.IIncrementalUpdateInformation): List? { val idGenerator = DummyIdGenerator() val versionedTrees = oldVersions.map { VersionedModelTree(it, idGenerator) } - val model = SyncTargetModel(versionedTrees.map { it.asModel() }) + val model = SyncTargetModel( + mpsProject, + versionedTrees.zip(syncTargets).map { (versionedTree, syncTarget) -> + val model = versionedTree.asModel() + val projectNode: () -> IWritableNode? = { + model.executeRead { + findMatchingProjectNode(model.getRootNode()) as IWritableNode? + } + } + SyncTargetConfig( + model = model, + readonly = syncTarget.readonly, + projectId = syncTarget.projectId + ?: projectNode()?.getNodeReference()?.let { MPSProjectReference.tryConvert(it) } + ?: mpsProject.getNodeReference(), + ) + }, + ) model.executeWrite { val targetRoot = model.getRootNode() - MPSProjectAsNode.runWithProject(mpsProject) { - val sourceRoot = MPSRepositoryAsNode(mpsProject.repository) + MPSProjectAsNode.runWithProjectNode(mpsProject) { + val sourceRoot = MPSRepositoryAsNode(mpsProject.project.getRepository()) val legacyMutableTree = (targetRoot.getModel().asArea() as? PArea)?.branch - val projectNode: IWritableNode? = findMatchingProjectNode(targetRoot) as IWritableNode? val nodeAssociation = if (legacyMutableTree != null) { - NodeAssociationToModelServer(legacyMutableTree).also { nodeAssociation -> - // handled renamed projects - if (projectNode != null && !nodeAssociation.matches(MPSProjectAsNode(mpsProject), projectNode)) { - nodeAssociation.associate(MPSProjectAsNode(mpsProject), projectNode) - } - } + NodeAssociationToModelServer(legacyMutableTree) } else { - var overrides = mapOf(sourceRoot.getNodeReference() to targetRoot.getNodeReference()) - if (projectNode != null) { - overrides += MPSProjectAsNode(mpsProject).getNodeReference() to projectNode.getNodeReference() - } + val overrides = mapOf(sourceRoot.getNodeReference() to targetRoot.getNodeReference()) IdentityPreservingNodeAssociation(targetRoot.getModel(), overrides) } @@ -535,8 +572,8 @@ class BindingWorker( 0 -> null 1 -> projectNodes.single() else -> projectNodes.find { - getProjectId(it) == getProjectId(MPSProjectAsNode(mpsProject)) - } + getProjectId(it) == getProjectId(mpsProject) + } ?: projectNodes.first() } } diff --git a/mps-sync-plugin3/src/main/kotlin/org/modelix/mps/sync3/IModuleMappings.kt b/mps-sync-plugin3/src/main/kotlin/org/modelix/mps/sync3/IModuleMappings.kt new file mode 100644 index 0000000000..d84a805f47 --- /dev/null +++ b/mps-sync-plugin3/src/main/kotlin/org/modelix/mps/sync3/IModuleMappings.kt @@ -0,0 +1,8 @@ +package org.modelix.mps.sync3 + +import org.jetbrains.mps.openapi.module.SModuleId + +interface IModuleMappings { + fun getAllModuleOwners(): Map + fun getModuleOwner(moduleId: SModuleId): IModuleOwnerId? +} diff --git a/mps-sync-plugin3/src/main/kotlin/org/modelix/mps/sync3/ModelSyncService.kt b/mps-sync-plugin3/src/main/kotlin/org/modelix/mps/sync3/ModelSyncService.kt index 15953a3766..3ecfd63a0d 100644 --- a/mps-sync-plugin3/src/main/kotlin/org/modelix/mps/sync3/ModelSyncService.kt +++ b/mps-sync-plugin3/src/main/kotlin/org/modelix/mps/sync3/ModelSyncService.kt @@ -20,7 +20,7 @@ import org.jdom.Element import org.modelix.model.IVersion import org.modelix.model.client2.IModelClientV2 import org.modelix.model.lazy.BranchReference -import org.modelix.model.lazy.RepositoryId +import org.modelix.model.mpsadapters.MPSProjectAsNode import org.modelix.model.oauth.IAuthConfig import org.modelix.model.oauth.OAuthConfigBuilder import org.modelix.model.oauth.TokenProvider @@ -200,12 +200,14 @@ class ModelSyncService(val project: Project) : return worker.getOrPut { it ?: BindingWorker( coroutinesScope, - mpsProject, + MPSProjectAsNode(mpsProject), syncTargets = bindings.map { (id, state) -> SyncTarget( serverConnection = addServer(id.connectionProperties.copy(repositoryId = id.branchRef.repositoryId)), bindingId = id, initialVersionHash = state?.versionHash, + readonly = state?.readonly ?: false, + projectId = state?.projectId, ) }, continueOnError = { IModelSyncService.continueOnError ?: true }, @@ -213,64 +215,6 @@ class ModelSyncService(val project: Project) : } } - data class SyncServiceState( - val bindings: Map = emptyMap(), - ) { - fun toXml() = Element("model-sync").also { - it.children.addAll( - bindings.map { bindingEntry -> - Element("binding").also { - it.children.add(Element("enabled").also { it.text = bindingEntry.value.enabled.toString() }) - it.children.add( - Element("url").also { - it.text = bindingEntry.key.connectionProperties.url - it.setAttribute("repositoryScoped", "${bindingEntry.key.connectionProperties.repositoryId != null}") - }, - ) - bindingEntry.key.connectionProperties.oauthClientId?.let { oauthClientId -> - it.children.add(Element("oauthClientId").also { it.text = oauthClientId }) - } - bindingEntry.key.connectionProperties.oauthClientSecret?.let { oauthClientSecret -> - it.children.add(Element("oauthClientSecret").also { it.text = oauthClientSecret }) - } - it.children.add(Element("repository").also { it.text = bindingEntry.key.branchRef.repositoryId.id }) - it.children.add(Element("branch").also { it.text = bindingEntry.key.branchRef.branchName }) - it.children.add(Element("versionHash").also { it.text = bindingEntry.value.versionHash }) - } - }, - ) - } - companion object { - fun fromXml(element: Element): SyncServiceState { - return SyncServiceState( - element.getChildren("binding").mapNotNull> { element -> - val repositoryId = RepositoryId(element.getChild("repository")?.text ?: return@mapNotNull null) - BindingId( - connectionProperties = ModelServerConnectionProperties( - url = element.getChild("url")?.text ?: return@mapNotNull null, - repositoryId = repositoryId.takeIf { element.getChild("url")?.getAttribute("repositoryScoped")?.value != "false" }, - oauthClientId = element.getChild("oauthClientId")?.text, - oauthClientSecret = element.getChild("oauthClientSecret")?.text, - ), - branchRef = BranchReference( - repositoryId, - element.getChild("branch")?.text ?: return@mapNotNull null, - ), - ) to BindingState( - versionHash = element.getChild("versionHash")?.text, - enabled = element.getChild("enabled")?.text.toBoolean(), - ) - }.toMap(), - ) - } - } - } - - data class BindingState( - val versionHash: String? = null, - val enabled: Boolean = false, - ) - inner class Connection(val connection: AppLevelModelSyncService.ServerConnection) : IServerConnection { override fun setTokenProvider(tokenProvider: TokenProvider) { connection.setAuthorizationConfig(IAuthConfig.fromTokenProvider(tokenProvider)) @@ -462,10 +406,4 @@ suspend fun jobLoop( } } -data class BindingId(val connectionProperties: ModelServerConnectionProperties, val branchRef: BranchReference) { - override fun toString(): String { - return "BindingId($connectionProperties, ${branchRef.repositoryId}, ${branchRef.branchName})" - } -} - private fun AtomicReference.getOrPut(initializer: (T?) -> T) = updateAndGet { initializer(it) } as T diff --git a/mps-sync-plugin3/src/main/kotlin/org/modelix/mps/sync3/SyncServiceState.kt b/mps-sync-plugin3/src/main/kotlin/org/modelix/mps/sync3/SyncServiceState.kt new file mode 100644 index 0000000000..1045ba8307 --- /dev/null +++ b/mps-sync-plugin3/src/main/kotlin/org/modelix/mps/sync3/SyncServiceState.kt @@ -0,0 +1,130 @@ +package org.modelix.mps.sync3 + +import org.jdom.Element +import org.jetbrains.mps.openapi.module.SModuleId +import org.jetbrains.mps.openapi.persistence.PersistenceFacade +import org.modelix.model.lazy.BranchReference +import org.modelix.model.lazy.RepositoryId +import org.modelix.mps.multiplatform.model.MPSProjectReference + +data class SyncServiceState( + val bindings: Map = emptyMap(), + /** + * Modules that exist locally, but aren't synchronized to any repository. + */ + val localModules: Set = emptySet(), +) : IModuleMappings { + override fun getAllModuleOwners(): Map { + return ( + bindings.flatMap { binding -> + binding.value.ownedModules.map { it to binding.key } + } + localModules.map { it to LocalOnlyModuleOwner } + ).toMap() + } + + override fun getModuleOwner(moduleId: SModuleId): IModuleOwnerId? { + return getAllModuleOwners()[moduleId] + } + + fun assignModuleOwner(moduleId: SModuleId, owner: IModuleOwnerId?): SyncServiceState { + return copy( + bindings = bindings.mapValues { + if (it.key == owner) { + it.value.copy(ownedModules = it.value.ownedModules + moduleId) + } else { + it.value.copy(ownedModules = it.value.ownedModules - moduleId) + } + }, + localModules = if (owner == LocalOnlyModuleOwner) localModules + moduleId else localModules - moduleId, + ) + } + + fun toXml() = Element("model-sync").also { + it.children.addAll( + bindings.map { bindingEntry -> + Element("binding").also { bindingElement -> + bindingElement.children.add(Element("enabled").also { it.text = bindingEntry.value.enabled.toString() }) + bindingElement.children.add( + Element("url").also { + it.text = bindingEntry.key.connectionProperties.url + it.setAttribute("repositoryScoped", "${bindingEntry.key.connectionProperties.repositoryId != null}") + }, + ) + bindingEntry.key.connectionProperties.oauthClientId?.let { oauthClientId -> + bindingElement.children.add(Element("oauthClientId").also { it.text = oauthClientId }) + } + bindingEntry.key.connectionProperties.oauthClientSecret?.let { oauthClientSecret -> + bindingElement.children.add(Element("oauthClientSecret").also { it.text = oauthClientSecret }) + } + bindingElement.children.add(Element("repository").also { it.text = bindingEntry.key.branchRef.repositoryId.id }) + bindingElement.children.add(Element("branch").also { it.text = bindingEntry.key.branchRef.branchName }) + bindingElement.children.add(Element("versionHash").also { it.text = bindingEntry.value.versionHash }) + bindingElement.children.add(Element("readonly").also { it.text = bindingEntry.value.readonly.toString() }) + bindingEntry.value.projectId?.projectName?.takeIf { it.isNotEmpty() }?.let { projectId -> + bindingElement.children.add(Element("projectId").also { it.text = projectId }) + } + bindingEntry.value.ownedModules.forEach { moduleId -> + bindingElement.children.add(Element("module").also { it.text = moduleId.toString() }) + } + } + } + localModules.map { moduleId -> + Element("module").also { it.text = moduleId.toString() } + }, + ) + } + companion object { + fun fromXml(element: Element): SyncServiceState { + return SyncServiceState( + bindings = element.getChildren("binding").mapNotNull> { element -> + val repositoryId = RepositoryId(element.getChild("repository")?.text ?: return@mapNotNull null) + BindingId( + connectionProperties = ModelServerConnectionProperties( + url = element.getChild("url")?.text ?: return@mapNotNull null, + repositoryId = repositoryId.takeIf { element.getChild("url")?.getAttribute("repositoryScoped")?.value != "false" }, + oauthClientId = element.getChild("oauthClientId")?.text, + oauthClientSecret = element.getChild("oauthClientSecret")?.text, + ), + branchRef = BranchReference( + repositoryId, + element.getChild("branch")?.text ?: return@mapNotNull null, + ), + ) to BindingState( + versionHash = element.getChild("versionHash")?.text, + enabled = element.getChild("enabled")?.text.toBoolean(), + readonly = element.getChild("readonly")?.text.toBoolean(), + projectId = element.getChild("projectId")?.text?.takeIf { it.isNotEmpty() }?.let { MPSProjectReference(it) }, + ownedModules = element.getChildren("module").mapNotNull { + runCatching { PersistenceFacade.getInstance().createModuleId(it.text) }.getOrNull() + }.toSet(), + ) + }.toMap(), + localModules = element.getChildren("module").mapNotNull { element -> + runCatching { PersistenceFacade.getInstance().createModuleId(element.text) }.getOrNull() + }.toSet(), + ) + } + } +} + +data class BindingState( + val versionHash: String? = null, + val enabled: Boolean = false, + val readonly: Boolean = false, + + /** + * If null, the first found project is used. + */ + val projectId: MPSProjectReference? = null, + val ownedModules: Set = emptySet(), + val ignoredModules: Set = emptySet(), +) + +data class BindingId(val connectionProperties: ModelServerConnectionProperties, val branchRef: BranchReference) : IModuleOwnerId { + override fun toString(): String { + return "BindingId($connectionProperties, ${branchRef.repositoryId}, ${branchRef.branchName})" + } +} + +sealed interface IModuleOwnerId + +object LocalOnlyModuleOwner : IModuleOwnerId diff --git a/mps-sync-plugin3/src/main/kotlin/org/modelix/mps/sync3/SyncTargetModel.kt b/mps-sync-plugin3/src/main/kotlin/org/modelix/mps/sync3/SyncTargetModel.kt index eb9713f398..8bc214a73f 100644 --- a/mps-sync-plugin3/src/main/kotlin/org/modelix/mps/sync3/SyncTargetModel.kt +++ b/mps-sync-plugin3/src/main/kotlin/org/modelix/mps/sync3/SyncTargetModel.kt @@ -16,11 +16,26 @@ import org.modelix.model.api.NullChildLinkReference import org.modelix.model.api.remove import org.modelix.model.api.syncNewChild import org.modelix.model.api.syncNewChildren +import org.modelix.model.mpsadapters.MPSProjectAsNode import org.modelix.mps.multiplatform.model.MPSModuleReference import org.modelix.mps.multiplatform.model.MPSProjectModuleReference import org.modelix.mps.multiplatform.model.MPSProjectReference -class SyncTargetModel(val models: List) : IMutableModel { +data class SyncTargetConfig( + val model: IMutableModel, + val readonly: Boolean, + + /** + * @see BindingState.projectId + */ + val projectId: MPSProjectReference, +) + +class SyncTargetModel( + val project: MPSProjectAsNode, + val targetConfigs: List, +) : IMutableModel { + private val models: List get() = targetConfigs.map { it.model } private val repositoryNode: RepositoryWrapper = RepositoryWrapper() override fun getRootNode(): IWritableNode = repositoryNode @@ -74,17 +89,15 @@ class SyncTargetModel(val models: List) : IMutableModel { } fun getMPSModules(): List { - return getRepositories() - .flatMap { it.getChildren(modulesRole) } - .distinctBy { it.getNodeReference() } + return targetConfigs.flatMap { config -> + config.model.getRootNodes().flatMap { repositoryNode -> + repositoryNode.getChildren(modulesRole).map { ModuleWrapper(it, config) } + } + }.distinctBy { it.getNodeReference() } } fun getMPSProjects(): List { - return getRepositories() - .flatMap { it.getChildren(projectsRole) } - .map { it.getNodeReference() } - .distinct() - .map { ProjectWrapper(MPSProjectReference.convert(it)) } + return listOf(ProjectWrapper(project.getNodeReference())) } override fun getAllChildren(): List { @@ -219,38 +232,77 @@ class SyncTargetModel(val models: List) : IMutableModel { } } - inner class ProjectWrapper(val id: MPSProjectReference) : WrapperBase() { - override fun delegates(): Sequence = models.asSequence().mapNotNull { it.tryResolveNode(id) } + /** + * One local MPS project can have bindings to multiple server side projects in different repositories. + * This class provides a merged representation of all server side projects as a single project so that it can be + * more easily synchronized with the single local MPS project. + */ + inner class ProjectWrapper(val localId: MPSProjectReference) : WrapperBase() { + override fun delegates(): Sequence = targetConfigs.asSequence().mapNotNull { + it.model.tryResolveNode(it.projectId) + } + + override fun getOrCreateDelegate(): IWritableNode = delegates().firstOrNull() + ?: targetConfigs.filterNot { it.readonly }.first().let { config -> + config.model.getRootNode().syncNewChild( + role = BuiltinLanguages.MPSRepositoryConcepts.Repository.projects.toReference(), + index = 0, + sourceNode = NewNodeSpec( + conceptRef = BuiltinLanguages.MPSRepositoryConcepts.Project.getReference(), + preferredNodeReference = config.projectId, + ), + ) + } override fun getModel() = this@SyncTargetModel - private fun getProjectModules(): List { - return models.flatMapIndexed { index, model -> - val projectNode = model.tryResolveNode(id) - if (projectNode == null) { - if (index == 0) { + private fun getProjectModules(): List> { + return targetConfigs.map { targetConfig -> + val serverSideProjectId = targetConfig.projectId + if (serverSideProjectId == null) { + // If the binding doesn't specify a project ID, project nodes are ignored for that repository and + // all modules are considered being part of the binding. + targetConfig.model.getRootNode() + .getChildren(BuiltinLanguages.MPSRepositoryConcepts.Repository.modules.toReference()) + .map { it.getNodeReference() } + } else { + val projectNode = targetConfig.model.tryResolveNode(serverSideProjectId) + if (projectNode == null) { emptyList() } else { - model.getRootNode() - .getChildren(BuiltinLanguages.MPSRepositoryConcepts.Repository.modules.toReference()) - .map { it.getNodeReference() } + projectNode + .getChildren(BuiltinLanguages.MPSRepositoryConcepts.Project.projectModules.toReference()) + .mapNotNull { + it.getReferenceTargetRef(BuiltinLanguages.MPSRepositoryConcepts.ModuleReference.module.toReference()) + } } - } else { - projectNode.getChildren(BuiltinLanguages.MPSRepositoryConcepts.Project.projectModules.toReference()) - .mapNotNull { it.getReferenceTargetRef(BuiltinLanguages.MPSRepositoryConcepts.ModuleReference.module.toReference()) } }.map { - ProjectModuleWrapper(this, MPSProjectModuleReference(MPSModuleReference.convert(it), id)) + ProjectModuleWrapper(this, MPSProjectModuleReference(MPSModuleReference.convert(it), localId)) } } } + private fun getAllModuleIds(): List> { + return targetConfigs.map { targetConfig -> + ( + targetConfig.model.getRootNodes() + .flatMap { it.getChildren(BuiltinLanguages.MPSRepositoryConcepts.Repository.modules.toReference()) } + .map { it.getNodeReference() } + + targetConfig.model.getRootNodes() + .flatMap { it.getChildren(BuiltinLanguages.MPSRepositoryConcepts.Repository.projects.toReference()) } + .flatMap { it.getChildren(BuiltinLanguages.MPSRepositoryConcepts.Project.projectModules.toReference()) } + .mapNotNull { it.getReferenceTargetRef(BuiltinLanguages.MPSRepositoryConcepts.ModuleReference.module.toReference()) } + ).toSet() + } + } + override fun getAllChildren(): List { - return getProjectModules() + return getProjectModules().flatten() } override fun getChildren(role: IChildLinkReference): List { if (role.matches(BuiltinLanguages.MPSRepositoryConcepts.Project.projectModules.toReference())) { - return getProjectModules() + return getProjectModules().flatten() } return emptyList() } @@ -267,15 +319,11 @@ class SyncTargetModel(val models: List) : IMutableModel { override fun changeConcept(newConcept: ConceptReference): IWritableNode { require(newConcept == BuiltinLanguages.MPSRepositoryConcepts.Project.getReference()) { - "Unexpected concept change:$newConcept" + "Unexpected concept change: $newConcept" } return this } - override fun setPropertyValue(property: IPropertyReference, value: String?) { - throw UnsupportedOperationException("$property = $value") - } - override fun moveChild( role: IChildLinkReference, index: Int, @@ -314,7 +362,7 @@ class SyncTargetModel(val models: List) : IMutableModel { } override fun getNodeReference(): INodeReference { - return id + return localId } override fun getConcept(): IConcept { @@ -336,10 +384,22 @@ class SyncTargetModel(val models: List) : IMutableModel { ): List { when { role.matches(BuiltinLanguages.MPSRepositoryConcepts.Project.projectModules.toReference()) -> { - return delegates().first().syncNewChildren(role, index, specs).map { - val id = MPSProjectModuleReference.convert(it.getNodeReference()) - it.setReferenceTargetRef(BuiltinLanguages.MPSRepositoryConcepts.ModuleReference.module.toReference(), id.moduleRef) - ProjectModuleWrapper(this, id) + val moduleOwners = getAllModuleIds().withIndex().flatMap { (configIndex, modules) -> + modules.map { it to configIndex } + }.toMap() + + return specs.groupBy { moduleOwners[it.preferredOrCurrentRef] ?: 0 }.flatMap { (ownerIndex, specs) -> + val targetConfig = targetConfigs[ownerIndex] + val projectNode = targetConfig.model.tryResolveNode(targetConfig.projectId) + ?: targetConfig.model.getRootNode().addNewChild( + BuiltinLanguages.MPSRepositoryConcepts.Repository.projects.toReference(), -1, + BuiltinLanguages.MPSRepositoryConcepts.Project.getReference(), + ) + projectNode.syncNewChildren(role, -1, specs).map { + val id = MPSProjectModuleReference.convert(it.getNodeReference()) + it.setReferenceTargetRef(BuiltinLanguages.MPSRepositoryConcepts.ModuleReference.module.toReference(), id.moduleRef) + ProjectModuleWrapper(this, MPSProjectModuleReference(id.moduleRef, localId)) + } } } else -> throw UnsupportedOperationException("role = $role") @@ -352,7 +412,7 @@ class SyncTargetModel(val models: List) : IMutableModel { .flatMap { it.getChildren(BuiltinLanguages.MPSRepositoryConcepts.Project.projectModules.toReference()) } .filter { it.getReferenceTargetRef(BuiltinLanguages.MPSRepositoryConcepts.ModuleReference.module.toReference()) == id.moduleRef } - fun getOrCreateDelegate(): IWritableNode { + override fun getOrCreateDelegate(): IWritableNode { return delegates().firstOrNull() ?: project.delegates().first().syncNewChild(getContainmentLink(), -1, NewNodeSpec(this)) } @@ -467,7 +527,11 @@ class SyncTargetModel(val models: List) : IMutableModel { private fun IWritableNode.unwrap() = if (this is NodeWrapper) this.node else this - inner class NodeWrapper(private val model: IMutableModel, val node: IWritableNode) : IWritableNode by node, ISyncTargetNode { + open inner class NodeWrapper(private val model: IMutableModel, val node: IWritableNode) : IWritableNode by node, ISyncTargetNode { + init { + require(node !is NodeWrapper) + } + private fun IWritableNode.wrap() = NodeWrapper(model, this) private fun Iterable.wrap() = map { it.wrap() } @@ -559,8 +623,29 @@ class SyncTargetModel(val models: List) : IMutableModel { } } + inner class ModuleWrapper(node: IWritableNode, val owner: SyncTargetConfig) : NodeWrapper(owner.model, node) { + override fun isReadOnly(): Boolean { + return owner.readonly || node.isReadOnly() + } + + override fun getPropertyValue(property: IPropertyReference): String? { + if (property.matches(BuiltinLanguages.MPSRepositoryConcepts.Module.isReadOnly.toReference()) && owner.readonly) { + return true.toString() + } + return super.getPropertyValue(property) + } + + override fun setPropertyValue(property: IPropertyReference, value: String?) { + if (property.matches(BuiltinLanguages.MPSRepositoryConcepts.Module.isReadOnly.toReference())) { + return // changes not supported + } + return super.setPropertyValue(property, value) + } + } + abstract inner class WrapperBase : IWritableNode, ISyncTargetNode { abstract fun delegates(): Sequence + open fun getOrCreateDelegate(): IWritableNode = delegates().first() override fun getPropertyValue(property: IPropertyReference): String? { return delegates().firstNotNullOfOrNull { it.getPropertyValue(property) } @@ -587,21 +672,21 @@ class SyncTargetModel(val models: List) : IMutableModel { } override fun setPropertyValue(property: IPropertyReference, value: String?) { - delegates().first().setPropertyValue(property, value) + getOrCreateDelegate().setPropertyValue(property, value) } override fun setReferenceTarget( role: IReferenceLinkReference, target: IWritableNode?, ) { - delegates().first().setReferenceTarget(role, target?.unwrap()) + getOrCreateDelegate().setReferenceTarget(role, target?.unwrap()) } override fun setReferenceTargetRef( role: IReferenceLinkReference, target: INodeReference?, ) { - delegates().first().setReferenceTargetRef(role, target) + getOrCreateDelegate().setReferenceTargetRef(role, target) } } } diff --git a/mps-sync-plugin3/src/test/kotlin/org/modelix/mps/sync3/LibraryRepositoryTest.kt b/mps-sync-plugin3/src/test/kotlin/org/modelix/mps/sync3/LibraryRepositoryTest.kt new file mode 100644 index 0000000000..cdbd3892bd --- /dev/null +++ b/mps-sync-plugin3/src/test/kotlin/org/modelix/mps/sync3/LibraryRepositoryTest.kt @@ -0,0 +1,274 @@ +package org.modelix.mps.sync3 + +import jetbrains.mps.smodel.SNodeUtil +import jetbrains.mps.smodel.adapter.structure.MetaAdapterFactory +import org.modelix.datastructures.model.MutationParameters +import org.modelix.model.api.BuiltinLanguages +import org.modelix.model.api.IWritableNode +import org.modelix.model.client2.ModelClientV2 +import org.modelix.model.client2.runWriteOnTree +import org.modelix.model.lazy.RepositoryId +import org.modelix.model.mpsadapters.MPSProjectReference +import org.modelix.model.mpsadapters.toModelix +import org.modelix.model.mutable.IMutableModelTree +import org.modelix.model.mutable.asModelSingleThreaded +import org.modelix.model.mutable.getRootNode +import org.modelix.model.mutable.setProperty +import org.modelix.mps.multiplatform.model.MPSIdGenerator +import org.modelix.mps.multiplatform.model.MPSModelReference +import org.modelix.mps.multiplatform.model.MPSModuleReference +import org.modelix.mps.multiplatform.model.MPSProjectModuleReference +import kotlin.io.path.writeText + +/** + * This covers the use case where, in addition to the primary repository, a read-only library is loaded from a + * second repository. + * The library repository is expected to also contain a project, but its ID and name isn't relevant and doesn't have + * to match the local project name. + */ +class LibraryRepositoryTest : ProjectSyncTestBase() { + + private val branchRefMain = RepositoryId("main-repository").getBranchReference() + private val branchRefLib = RepositoryId("lib-repository").getBranchReference() + + fun `test checkout`() = runTest { port, client -> + val expectedLibHash = client.pullHash(branchRefLib) + openProjectWithBindings(port) + + val service = IModelSyncService.getInstance(mpsProject) + assertEquals(2, service.getBindings().size) + service.getBindings().forEach { it.flush() } + + assertContainsElements( + readAction { mpsProject.repository.modules.map { it.moduleName }.toSet() }, + "main.module1", + "main.module2", + "lib.module3", + "lib.module4", + ) + assertEquals( + setOf( + "main.module1" to false, + "main.module2" to false, + "lib.module3" to true, + "lib.module4" to true, + ), + readAction { mpsProject.projectModules.map { it.moduleName to it.isReadOnly }.toSet() }, + ) + + // lib repository is read only and should remain unchanged + assertEquals(expectedLibHash, client.pullHash(branchRefLib)) + } + + fun `test add module in main repository`() = runTest { port, client -> + openProjectWithBindings(port) + + val service = IModelSyncService.getInstance(mpsProject) + assertEquals(2, service.getBindings().size) + service.getBindings().forEach { it.flush() } + + assertContainsElements( + readAction { mpsProject.repository.modules.map { it.moduleName }.toSet() }, + "main.module1", + "main.module2", + "lib.module3", + "lib.module4", + ) + assertEquals( + setOf( + "main.module1" to false, + "main.module2" to false, + "lib.module3" to true, + "lib.module4" to true, + ), + readAction { mpsProject.projectModules.map { it.moduleName to it.isReadOnly }.toSet() }, + ) + + // add new module + client.runWriteOnTree(branchRefMain, nodeIdGenerator = { MPSIdGenerator(client.getIdGenerator(), it) }) { tree -> + val repo = tree.getRootNode() + val t = tree.getWriteTransaction() + val module = repo.addNewModule("main.newModule") + t.mutate( + MutationParameters.AddNew( + MPSProjectReference("main-project"), + BuiltinLanguages.MPSRepositoryConcepts.Project.projectModules.toReference(), + -1, + listOf(MPSProjectModuleReference(MPSModuleReference.convert(module.getNodeReference()), MPSProjectReference("main-project")) to BuiltinLanguages.MPSRepositoryConcepts.ProjectModule.getReference()), + ), + ) + t.mutate( + MutationParameters.Reference( + MPSProjectModuleReference(MPSModuleReference.convert(module.getNodeReference()), MPSProjectReference("main-project")), + BuiltinLanguages.MPSRepositoryConcepts.ModuleReference.module.toReference(), + module.getNodeReference(), + ), + ) + } + + service.getBindings().forEach { it.flush() } + + assertContainsElements( + readAction { mpsProject.repository.modules.map { it.moduleName }.toSet() }, + "main.module1", + "main.module2", + "main.newModule", + "lib.module3", + "lib.module4", + ) + assertEquals( + setOf( + "main.module1" to false, + "main.module2" to false, + "main.newModule" to false, + "lib.module3" to true, + "lib.module4" to true, + ), + readAction { mpsProject.projectModules.map { it.moduleName to it.isReadOnly }.toSet() }, + ) + } + + fun `test add root node in MPS`() = runTest { port, client -> + openProjectWithBindings(port) + + val service = IModelSyncService.getInstance(mpsProject) + assertEquals(2, service.getBindings().size) + service.getBindings().forEach { it.flush() } + + // add new root node + val classConcept = MetaAdapterFactory.getConcept(-0xcf9e5ac6dd9b33bL, -0x5bbc06ad3150a7eaL, 0xf8c108ca66L, "jetbrains.mps.baseLanguage.structure.ClassConcept") + writeAction { + val model = mpsProject.projectModules.first { it.moduleName == "main.module1" } + .models.first { it.name.simpleName == "modelA" } + model.addRootNode( + model.createNode(classConcept).also { + it.setProperty(SNodeUtil.property_INamedConcept_name, "MyClass") + }, + ) + } + + service.getBindings().forEach { it.flush() } + + val version = client.pull(branchRefMain, null) + val repositoryNode = version.getModelTree().asModelSingleThreaded().getRootNode() + val module1 = repositoryNode.getChildren(BuiltinLanguages.MPSRepositoryConcepts.Repository.modules.toReference()) + .first { it.getPropertyValue(BuiltinLanguages.jetbrains_mps_lang_core.INamedConcept.name.toReference()) == "main.module1" } + val modelA = module1.getChildren(BuiltinLanguages.MPSRepositoryConcepts.Module.models.toReference()) + .first { it.getPropertyValue(BuiltinLanguages.jetbrains_mps_lang_core.INamedConcept.name.toReference()) == "main.module1.modelA" } + val classNode = modelA.getChildren(BuiltinLanguages.MPSRepositoryConcepts.Model.rootNodes.toReference()).first() + assertEquals(classConcept.toModelix().getReference(), classNode.getConceptReference()) + assertEquals("MyClass", classNode.getPropertyValue(BuiltinLanguages.jetbrains_mps_lang_core.INamedConcept.name.toReference())) + } + + private fun IWritableNode.addNewModule(name: String, modelNames: List = emptyList()): IWritableNode { + return addNewChild( + BuiltinLanguages.MPSRepositoryConcepts.Repository.modules.toReference(), + -1, + BuiltinLanguages.MPSRepositoryConcepts.Solution.getReference(), + ).also { module -> + module.setPropertyValue( + BuiltinLanguages.MPSRepositoryConcepts.Module.id.toReference(), + MPSModuleReference.tryConvert(module.getNodeReference())!!.moduleId, + ) + module.setPropertyValue(BuiltinLanguages.jetbrains_mps_lang_core.INamedConcept.name.toReference(), name) + for (modelName in modelNames) { + val model = module.addNewChild( + BuiltinLanguages.MPSRepositoryConcepts.Module.models.toReference(), + -1, + BuiltinLanguages.MPSRepositoryConcepts.Model.getReference(), + ) + model.setPropertyValue( + BuiltinLanguages.jetbrains_mps_lang_core.INamedConcept.name.toReference(), + modelName, + ) + model.setPropertyValue( + BuiltinLanguages.MPSRepositoryConcepts.Model.id.toReference(), + MPSModelReference.convert(model.getNodeReference()).modelId, + ) + } + } + } + + private fun runTest(body: suspend (port: Int, client: ModelClientV2) -> Unit) = runWithModelServer { port -> + val client = ModelClientV2.builder().url("http://localhost:$port").lazyAndBlockingQueries().build() + client.initRepository(branchRefMain.repositoryId) + client.initRepository(branchRefLib.repositoryId) + client.runWriteOnTree(branchRefMain, nodeIdGenerator = { MPSIdGenerator(client.getIdGenerator(), it) }) { tree -> + val repo = tree.getRootNode() + repo.changeConcept(BuiltinLanguages.MPSRepositoryConcepts.Repository.getReference()) + createProjectAndModules(tree, "main-project", listOf("main.module1", "main.module2")) + } + client.runWriteOnTree(branchRefLib, nodeIdGenerator = { MPSIdGenerator(client.getIdGenerator(), it) }) { tree -> + val repo = tree.getRootNode() + repo.changeConcept(BuiltinLanguages.MPSRepositoryConcepts.Repository.getReference()) + createProjectAndModules(tree, "lib-project", listOf("lib.module3", "lib.module4")) + } + + body(port, client) + } + + fun createProjectAndModules(tree: IMutableModelTree, projectName: String, moduleNames: List) { + val repo = tree.getRootNode() + repo.changeConcept(BuiltinLanguages.MPSRepositoryConcepts.Repository.getReference()) + + val modules = moduleNames.map { repo.addNewModule(it, listOf("$it.modelA")) } + + val t = tree.getWriteTransaction() + t.mutate( + MutationParameters.AddNew( + t.tree.getRootNodeId(), + BuiltinLanguages.MPSRepositoryConcepts.Repository.projects.toReference(), + -1, + listOf(MPSProjectReference(projectName) to BuiltinLanguages.MPSRepositoryConcepts.Project.getReference()), + ), + ) + t.setProperty(MPSProjectReference(projectName), BuiltinLanguages.jetbrains_mps_lang_core.INamedConcept.name.toReference(), projectName) + + t.mutate( + MutationParameters.AddNew( + MPSProjectReference(projectName), + BuiltinLanguages.MPSRepositoryConcepts.Project.projectModules.toReference(), + -1, + modules.map { + MPSProjectModuleReference(MPSModuleReference.convert(it.getNodeReference()), MPSProjectReference(projectName)) to BuiltinLanguages.MPSRepositoryConcepts.ProjectModule.getReference() + }, + ), + ) + for (module in modules) { + t.mutate( + MutationParameters.Reference( + MPSProjectModuleReference(MPSModuleReference.convert(module.getNodeReference()), MPSProjectReference(projectName)), + BuiltinLanguages.MPSRepositoryConcepts.ModuleReference.module.toReference(), + module.getNodeReference(), + ), + ) + } + } + + private fun openProjectWithBindings(port: Int) { + openTestProject(null, projectName = "test-project") { projectDir -> + projectDir.resolve(".mps").resolve("modelix.xml").writeText( + """ + + + + + true + http://localhost:$port + ${branchRefMain.repositoryId.id} + ${branchRefMain.branchName} + + + true + http://localhost:$port + ${branchRefLib.repositoryId.id} + ${branchRefLib.branchName} + true + + + + """.trimIndent(), + ) + } + } +} diff --git a/mps-sync-plugin3/src/test/kotlin/org/modelix/mps/sync3/MultipleBindingsTest.kt b/mps-sync-plugin3/src/test/kotlin/org/modelix/mps/sync3/MultipleBindingsTest.kt deleted file mode 100644 index ef46e41baf..0000000000 --- a/mps-sync-plugin3/src/test/kotlin/org/modelix/mps/sync3/MultipleBindingsTest.kt +++ /dev/null @@ -1,121 +0,0 @@ -package org.modelix.mps.sync3 - -import org.modelix.datastructures.model.MutationParameters -import org.modelix.model.api.BuiltinLanguages -import org.modelix.model.api.IWritableNode -import org.modelix.model.client2.ModelClientV2 -import org.modelix.model.client2.runWriteOnModel -import org.modelix.model.client2.runWriteOnTree -import org.modelix.model.lazy.RepositoryId -import org.modelix.model.mpsadapters.MPSProjectReference -import org.modelix.model.mutable.getRootNode -import org.modelix.model.mutable.setProperty -import org.modelix.mps.multiplatform.model.MPSIdGenerator -import org.modelix.mps.multiplatform.model.MPSModuleReference -import org.modelix.mps.multiplatform.model.MPSProjectModuleReference -import kotlin.io.path.writeText - -class MultipleBindingsTest : ProjectSyncTestBase() { - - private fun IWritableNode.addNewModule(name: String) = addNewChild(BuiltinLanguages.MPSRepositoryConcepts.Repository.modules.toReference(), -1, BuiltinLanguages.MPSRepositoryConcepts.Solution.getReference()).also { - it.setPropertyValue(BuiltinLanguages.MPSRepositoryConcepts.Module.id.toReference(), MPSModuleReference.tryConvert(it.getNodeReference())!!.moduleId) - it.setPropertyValue(BuiltinLanguages.jetbrains_mps_lang_core.INamedConcept.name.toReference(), name) - } - - fun `test no overlap`() = runWithModelServer { port -> - val client = ModelClientV2.builder().url("http://localhost:$port").lazyAndBlockingQueries().build() - val branchRefMain = RepositoryId("main-repository").getBranchReference() - val branchRefLib = RepositoryId("lib-repository").getBranchReference() - client.initRepository(branchRefMain.repositoryId) - client.initRepository(branchRefLib.repositoryId) - client.runWriteOnTree(branchRefMain, nodeIdGenerator = { MPSIdGenerator(client.getIdGenerator(), it) }) { tree -> - val repo = tree.getRootNode() - repo.changeConcept(BuiltinLanguages.MPSRepositoryConcepts.Repository.getReference()) - - val module1 = repo.addNewModule("module1") - val module2 = repo.addNewModule("module2") - - val t = tree.getWriteTransaction() - t.mutate( - MutationParameters.AddNew( - t.tree.getRootNodeId(), - BuiltinLanguages.MPSRepositoryConcepts.Repository.projects.toReference(), - -1, - listOf(MPSProjectReference("test-project") to BuiltinLanguages.MPSRepositoryConcepts.Project.getReference()), - ), - ) - t.setProperty(MPSProjectReference("test-project"), BuiltinLanguages.jetbrains_mps_lang_core.INamedConcept.name.toReference(), "test-project") - - t.mutate( - MutationParameters.AddNew( - MPSProjectReference("test-project"), - BuiltinLanguages.MPSRepositoryConcepts.Project.projectModules.toReference(), - -1, - listOf( - MPSProjectModuleReference(MPSModuleReference.convert(module1.getNodeReference()), MPSProjectReference("test-project")) to BuiltinLanguages.MPSRepositoryConcepts.ProjectModule.getReference(), - MPSProjectModuleReference(MPSModuleReference.convert(module2.getNodeReference()), MPSProjectReference("test-project")) to BuiltinLanguages.MPSRepositoryConcepts.ProjectModule.getReference(), - ), - ), - ) - t.mutate( - MutationParameters.Reference( - MPSProjectModuleReference(MPSModuleReference.convert(module1.getNodeReference()), MPSProjectReference("test-project")), - BuiltinLanguages.MPSRepositoryConcepts.ModuleReference.module.toReference(), - module1.getNodeReference(), - ), - ) - t.mutate( - MutationParameters.Reference( - MPSProjectModuleReference(MPSModuleReference.convert(module2.getNodeReference()), MPSProjectReference("test-project")), - BuiltinLanguages.MPSRepositoryConcepts.ModuleReference.module.toReference(), - module2.getNodeReference(), - ), - ) - } - client.runWriteOnModel(branchRefLib, nodeIdGenerator = { MPSIdGenerator(client.getIdGenerator(), it) }) { repo -> - repo.changeConcept(BuiltinLanguages.MPSRepositoryConcepts.Repository.getReference()) - val module3 = repo.addNewModule("module3") - val module4 = repo.addNewModule("module4") - } - - openTestProject(null, projectName = "test-project") { projectDir -> - projectDir.resolve(".mps").resolve("modelix.xml").writeText( - """ - - - - - true - http://localhost:$port - ${branchRefMain.repositoryId.id} - ${branchRefMain.branchName} - - - true - http://localhost:$port - ${branchRefLib.repositoryId.id} - ${branchRefLib.branchName} - - - - """.trimIndent(), - ) - } - - val service = IModelSyncService.getInstance(mpsProject) - assertEquals(2, service.getBindings().size) - service.getBindings().forEach { it.flush() } - - assertContainsElements( - readAction { mpsProject.repository.modules.map { it.moduleName }.toSet() }, - "module1", - "module2", - "module3", - "module4", - ) - assertEquals( - setOf("module1", "module2", "module3", "module4"), - readAction { mpsProject.projectModules.map { it.moduleName }.toSet() }, - ) - } -} diff --git a/mps-sync-plugin3/src/test/kotlin/org/modelix/mps/sync3/ProjectSyncTest.kt b/mps-sync-plugin3/src/test/kotlin/org/modelix/mps/sync3/ProjectSyncTest.kt index 6bd41ee92d..cba03fd342 100644 --- a/mps-sync-plugin3/src/test/kotlin/org/modelix/mps/sync3/ProjectSyncTest.kt +++ b/mps-sync-plugin3/src/test/kotlin/org/modelix/mps/sync3/ProjectSyncTest.kt @@ -881,6 +881,7 @@ class ProjectSyncTest : ProjectSyncTestBase() { ${branchRef.repositoryId.id} ${branchRef.branchName} ${version1.getContentHash()} + false diff --git a/mps-sync-plugin3/src/test/resources/logback-test.xml b/mps-sync-plugin3/src/test/resources/logback-test.xml index 7c480bf3f2..5271c3b2af 100644 --- a/mps-sync-plugin3/src/test/resources/logback-test.xml +++ b/mps-sync-plugin3/src/test/resources/logback-test.xml @@ -19,4 +19,5 @@ +