diff --git a/.github/workflows/mps-compatibility.yaml b/.github/workflows/mps-compatibility.yaml index 27b53b375b..de9384a7ee 100644 --- a/.github/workflows/mps-compatibility.yaml +++ b/.github/workflows/mps-compatibility.yaml @@ -15,6 +15,7 @@ jobs: timeout-minutes: 60 strategy: + fail-fast: false matrix: version: - "2020.3" @@ -47,4 +48,13 @@ jobs: :metamodel-export:build :mps-model-adapters:build :mps-model-adapters-plugin:build + :mps-sync-plugin3:build -Pmps.version.major=${{ matrix.version }} + - name: Archive test report + uses: actions/upload-artifact@v4 + if: always() + with: + name: test-report-${{ matrix.version }} + path: | + */build/test-results + */build/reports diff --git a/mps-model-adapters/src/main/kotlin/org/modelix/model/mpsadapters/MPSArea.kt b/mps-model-adapters/src/main/kotlin/org/modelix/model/mpsadapters/MPSArea.kt index 30769bf6fa..a5d5f81435 100644 --- a/mps-model-adapters/src/main/kotlin/org/modelix/model/mpsadapters/MPSArea.kt +++ b/mps-model-adapters/src/main/kotlin/org/modelix/model/mpsadapters/MPSArea.kt @@ -90,11 +90,7 @@ data class MPSArea(val repository: SRepository) : IArea, IAreaReference { } override fun executeRead(f: () -> T): T { - var result: T? = null - repository.modelAccess.runReadAction { - result = f() - } - return result!! + return repository.computeRead(f) } override fun executeWrite(f: () -> T): T { @@ -365,3 +361,11 @@ data class MPSArea(val repository: SRepository) : IArea, IAreaReference { return repository.asLegacyNode() } } + +fun SRepository.computeRead(body: () -> R): R { + var result: R? = null + modelAccess.runReadAction { + result = body() + } + return result as R +} diff --git a/mps-model-adapters/src/main/kotlin/org/modelix/model/mpsadapters/MPSJavaModuleFacetAsNode.kt b/mps-model-adapters/src/main/kotlin/org/modelix/model/mpsadapters/MPSJavaModuleFacetAsNode.kt index b5d3fa64cc..8495594d41 100644 --- a/mps-model-adapters/src/main/kotlin/org/modelix/model/mpsadapters/MPSJavaModuleFacetAsNode.kt +++ b/mps-model-adapters/src/main/kotlin/org/modelix/model/mpsadapters/MPSJavaModuleFacetAsNode.kt @@ -1,7 +1,8 @@ package org.modelix.model.mpsadapters -import jetbrains.mps.persistence.MementoImpl import jetbrains.mps.project.facets.JavaModuleFacet +import jetbrains.mps.smodel.Generator +import jetbrains.mps.util.MacroHelper import jetbrains.mps.util.MacrosFactory import org.jetbrains.mps.openapi.module.SRepository import org.jetbrains.mps.openapi.persistence.Memento @@ -30,15 +31,24 @@ data class MPSJavaModuleFacetAsNode(val facet: JavaModuleFacet) : MPSGenericNode }, BuiltinLanguages.MPSRepositoryConcepts.JavaModuleFacet.path.toReference() to object : IPropertyAccessor { override fun read(facet: JavaModuleFacet): String? { - return facet.classesGen?.let { MacrosFactory().module(facet.module).shrinkPath(it.path) } + // return facet.classesGen?.let { facet.macroHelper().shrinkPath(it.path) } + return null } override fun write(element: JavaModuleFacet, value: String?) { - element.classesGen - val memento = MementoImpl() - element.save(memento) - memento.getOrCreateChild("classes").put("path", value?.let { MacrosFactory().module(element.module).expandPath(it) }) - element.load(memento) +// val memento = MementoImpl() +// element.save(memento) +// memento.getOrCreateChild("classes").let { +// it.put("generated", "true") +// it.put("path", value.also { println("${element.module} / path = $it") }) +// } +// element.load(memento) + } + + private fun JavaModuleFacet.macroHelper(): MacroHelper { + return module + .let { if (it is Generator) it.sourceLanguage().sourceModule else it } + .let { MacrosFactory().module(it) } } }, ) 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 e7fe3b0300..4202a5bbd6 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 @@ -156,10 +156,10 @@ abstract class MPSModuleAsNode : MPSGenericNodeAdapter() { val moduleDescriptor = checkNotNull(module.moduleDescriptor) { "Has no moduleDescriptor: $module" } val newFacet = FacetsFacade.getInstance().getFacetFactory(JavaModuleFacet.FACET_TYPE)!!.create(element) as JavaModuleFacetImpl newFacet.load(MementoImpl()) - val moduleDir = if (element is Generator) element.getGeneratorLocation() else module.moduleSourceDir - if (moduleDir != null) { - newFacet.setGeneratedClassesLocation(moduleDir.findChild(AbstractModule.CLASSES_GEN)) - } +// val moduleDir = if (element is Generator) element.getGeneratorLocation() else module.moduleSourceDir +// if (moduleDir != null) { +// newFacet.setGeneratedClassesLocation(moduleDir.findChild(AbstractModule.CLASSES_GEN)) +// } moduleDescriptor.addFacetDescriptor(newFacet) module.setModuleDescriptor(moduleDescriptor) // notify listeners read(element).filterIsInstance().single() diff --git a/mps-model-adapters/src/main/kotlin/org/modelix/model/mpsadapters/MPSWritableNode.kt b/mps-model-adapters/src/main/kotlin/org/modelix/model/mpsadapters/MPSWritableNode.kt index 4672c9d7d0..72b0e2b563 100644 --- a/mps-model-adapters/src/main/kotlin/org/modelix/model/mpsadapters/MPSWritableNode.kt +++ b/mps-model-adapters/src/main/kotlin/org/modelix/model/mpsadapters/MPSWritableNode.kt @@ -331,7 +331,7 @@ fun org.jetbrains.mps.openapi.model.SNodeId.tryDecodeModelixReference(): NodeRef } fun INodeReference.encodeAsForeignId(): SNodeId { - return SNodeId.Foreign.fromIdNoPrefix("mx" + Hex.encodeHexString(serialize().toByteArray())) + return SNodeId.Foreign("~mx" + Hex.encodeHexString(serialize().toByteArray())) } /** 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 1a18722f73..35b7cb6976 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 @@ -1,8 +1,11 @@ +@file:Suppress("removal") + package org.modelix.model.mpsadapters import jetbrains.mps.extapi.persistence.FileBasedModelRoot import jetbrains.mps.ide.project.ProjectHelper import jetbrains.mps.persistence.DefaultModelRoot +import jetbrains.mps.project.AbstractModule import jetbrains.mps.project.DevKit import jetbrains.mps.project.MPSExtentions import jetbrains.mps.project.MPSProject @@ -37,7 +40,9 @@ class SolutionProducer(private val myProject: MPSProject) { private fun createSolutionDescriptor(namespace: String, id: ModuleId, descriptorFile: IFile): SolutionDescriptor { val descriptor = SolutionDescriptor() - descriptor.outputRoot = "\${module}/source_gen" + // using outputPath instead of outputRoot for backwards compatibility + // descriptor.outputRoot = "\${module}/source_gen" + descriptor.outputPath = descriptorFile.parent!!.findChild("source_gen").path descriptor.namespace = namespace descriptor.id = id val moduleLocation = descriptorFile.parent @@ -74,7 +79,9 @@ class LanguageProducer(private val myProject: MPSProject) { private fun createDescriptor(namespace: String, id: ModuleId, descriptorFile: IFile): LanguageDescriptor { val descriptor = LanguageDescriptor() - descriptor.outputRoot = "\${module}/source_gen" + // using genPath instead of outputRoot for backwards compatibility + // descriptor.outputRoot = "\${module}/source_gen" + descriptor.genPath = descriptorFile.parent!!.findChild("source_gen").path descriptor.namespace = namespace descriptor.id = id val moduleLocation = descriptorFile.parent @@ -127,7 +134,9 @@ class GeneratorProducer(private val myProject: MPSProject) { private fun createDescriptor(namespace: String, id: ModuleId, alias: String?, generatorModuleLocation: IFile, templateModelsLocation: IFile?): GeneratorDescriptor { val descriptor = GeneratorDescriptor() - descriptor.outputRoot = "\${module}/${generatorModuleLocation.name}/source_gen" + // using outputPath instead of outputRoot for backwards compatibility + // descriptor.outputRoot = "\${module}/${generatorModuleLocation.name}/source_gen" + descriptor.outputPath = generatorModuleLocation.findChild(AbstractModule.CLASSES_GEN).path descriptor.namespace = namespace descriptor.id = id descriptor.alias = alias ?: "main" @@ -142,7 +151,7 @@ class GeneratorProducer(private val myProject: MPSProject) { } fun Generator.getGeneratorLocation(): IFile? { - return modelRoots.filterIsInstance().firstNotNullOfOrNull { it.contentDirectory } + return modelRoots.filterIsInstance().mapNotNull { it.contentDirectory }.firstOrNull() } class DevkitProducer(private val myProject: MPSProject) { diff --git a/mps-sync-plugin3/build.gradle.kts b/mps-sync-plugin3/build.gradle.kts index 8443fe7d83..2c3842f6a0 100644 --- a/mps-sync-plugin3/build.gradle.kts +++ b/mps-sync-plugin3/build.gradle.kts @@ -1,6 +1,7 @@ import org.modelix.copyMps import org.modelix.mpsHomeDir import org.modelix.mpsMajorVersion +import org.modelix.mpsPlatformVersion plugins { `modelix-kotlin-jvm` @@ -52,9 +53,6 @@ tasks { onlyIf { !setOf( "2020.3", // incompatible with the intellij plugin - "2021.2", // hangs when executed on CI - "2021.3", // hangs when executed on CI - "2022.2", // hangs when executed on CI ).contains(mpsMajorVersion) } jvmArgs("-Dintellij.platform.load.app.info.from.resources=true") @@ -91,3 +89,17 @@ publishing { } } } + +// disable coroutines agent +if (mpsPlatformVersion < 241) { + afterEvaluate { + val testTask = tasks.test.get() + val originalProviders = testTask.jvmArgumentProviders.toList() + testTask.jvmArgumentProviders.clear() + testTask.jvmArgumentProviders.add(object : CommandLineArgumentProvider { + override fun asArguments(): Iterable { + return originalProviders.flatMap { it.asArguments() }.filterNot { it.contains("coroutines-javaagent.jar") } + } + }) + } +} diff --git a/mps-sync-plugin3/src/main/kotlin/org/modelix/mps/sync3/MPSInvalidatingListener.kt b/mps-sync-plugin3/src/main/kotlin/org/modelix/mps/sync3/MPSInvalidatingListener.kt index aae11a1611..1de4addcae 100644 --- a/mps-sync-plugin3/src/main/kotlin/org/modelix/mps/sync3/MPSInvalidatingListener.kt +++ b/mps-sync-plugin3/src/main/kotlin/org/modelix/mps/sync3/MPSInvalidatingListener.kt @@ -25,7 +25,7 @@ import org.jetbrains.mps.openapi.module.SModule import org.jetbrains.mps.openapi.module.SModuleListener import org.jetbrains.mps.openapi.module.SModuleReference import org.jetbrains.mps.openapi.module.SRepository -import org.jetbrains.mps.openapi.module.SRepositoryListener +import org.jetbrains.mps.openapi.module.SRepositoryListenerBase import org.modelix.model.api.IReadableNode import org.modelix.model.api.toSerialized import org.modelix.model.mpsadapters.GlobalModelListener @@ -42,7 +42,6 @@ abstract class MPSInvalidatingListener(val repository: SRepository) : GlobalModelListener(), SNodeChangeListener, SModuleListener, - SRepositoryListener, SModelListener, org.jetbrains.mps.openapi.model.SModelListener { @@ -117,11 +116,11 @@ abstract class MPSInvalidatingListener(val repository: SRepository) : } override fun addListener(repository: SRepository) { - repository.addRepositoryListener(this) + repository.addRepositoryListener(srepositoryListener) } override fun removeListener(repository: SRepository) { - repository.removeRepositoryListener(this) + repository.removeRepositoryListener(srepositoryListener) } override fun propertyChanged(e: SPropertyChangeEvent) { @@ -210,9 +209,16 @@ abstract class MPSInvalidatingListener(val repository: SRepository) : override fun languageAdded(module: SModule, language: SLanguage) { invalidate(module) } override fun languageRemoved(module: SModule, language: SLanguage) { invalidate(module) } override fun moduleChanged(module: SModule) { invalidate(module) } - override fun moduleAdded(module: SModule) { invalidate(repository) } - override fun beforeModuleRemoved(module: SModule) {} - override fun moduleRemoved(reference: SModuleReference) { invalidate(repository) } - override fun commandStarted(repository: SRepository) {} - override fun commandFinished(repository: SRepository) {} + + /** + * For compatibility with MPS 2020.3, SRepositoryListenerBase is used because SRepositoryListener had the additional + * methods updateStarted and updateFinished in that version. + */ + private val srepositoryListener = object : SRepositoryListenerBase() { + override fun moduleAdded(module: SModule) { invalidate(repository) } + override fun beforeModuleRemoved(module: SModule) {} + override fun moduleRemoved(reference: SModuleReference) { invalidate(repository) } + override fun commandStarted(repository: SRepository) {} + override fun commandFinished(repository: SRepository) {} + } } diff --git a/mps-sync-plugin3/src/main/kotlin/org/modelix/mps/sync3/ModelSyncForMPSProject.kt b/mps-sync-plugin3/src/main/kotlin/org/modelix/mps/sync3/ModelSyncForMPSProject.kt index e8d1f9b2c3..2534c062b9 100644 --- a/mps-sync-plugin3/src/main/kotlin/org/modelix/mps/sync3/ModelSyncForMPSProject.kt +++ b/mps-sync-plugin3/src/main/kotlin/org/modelix/mps/sync3/ModelSyncForMPSProject.kt @@ -1,10 +1,11 @@ +@file:OptIn(ExperimentalTime::class) + package org.modelix.mps.sync3 import com.intellij.openapi.Disposable import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.application.EDT +import com.intellij.openapi.application.ModalityState import com.intellij.openapi.components.Service -import com.intellij.openapi.components.service import com.intellij.openapi.project.Project import jetbrains.mps.ide.project.ProjectHelper import jetbrains.mps.project.MPSProject @@ -17,7 +18,6 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock -import kotlinx.coroutines.withContext import org.jetbrains.mps.openapi.module.SRepository import org.modelix.model.IVersion import org.modelix.model.api.TreePointer @@ -27,6 +27,7 @@ import org.modelix.model.client2.ModelClientV2 import org.modelix.model.client2.runWrite import org.modelix.model.lazy.BranchReference import org.modelix.model.mpsadapters.MPSRepositoryAsNode +import org.modelix.model.mpsadapters.computeRead import org.modelix.model.sync.bulk.FullSyncFilter import org.modelix.model.sync.bulk.InvalidatingVisitor import org.modelix.model.sync.bulk.InvalidationTree @@ -38,16 +39,15 @@ import org.modelix.mps.api.ModelixMpsApi import org.modelix.mps.model.sync.bulk.MPSProjectSyncMask import org.modelix.mps.sync3.Binding.Companion.LOG import java.util.concurrent.atomic.AtomicBoolean -import kotlin.time.Duration -import kotlin.time.Duration.Companion.milliseconds -import kotlin.time.Duration.Companion.seconds +import kotlin.math.roundToLong +import kotlin.time.ExperimentalTime @Service(Service.Level.APP) class AppLevelModelSyncService() : Disposable { companion object { fun getInstance(): AppLevelModelSyncService { - return ApplicationManager.getApplication().service() + return ApplicationManager.getApplication().getService(AppLevelModelSyncService::class.java) } } @@ -55,8 +55,8 @@ class AppLevelModelSyncService() : Disposable { private val coroutinesScope = CoroutineScope(Dispatchers.Default) private val connectionCheckingJob = coroutinesScope.launchLoop( BackoffStrategy( - initialDelay = 3.seconds, - maxDelay = 10.seconds, + initialDelay = 3_000, + maxDelay = 10_000, factor = 1.2, ), ) { @@ -97,7 +97,7 @@ class AppLevelModelSyncService() : Disposable { @Service(Service.Level.PROJECT) class ModelSyncService(val project: Project) : IModelSyncService, Disposable { - private val mpsProject: MPSProject get() = ProjectHelper.fromIdeaProjectOrFail(project) + private val mpsProject: MPSProject get() = ProjectHelper.fromIdeaProject(project)!! private val bindings = ArrayList() private val coroutinesScope = CoroutineScope(Dispatchers.IO) @@ -210,7 +210,7 @@ class Binding( while (reason != null) { i++ if (i % 10 == 0) LOG.debug { "Still waiting for the synchronization to finish: $reason" } - delay(100.milliseconds) + delay(100) reason = checkInSync() } return lastSyncedVersion.getValue()!! @@ -347,13 +347,17 @@ class Binding( private suspend fun writeToMPS(body: () -> R): R { val result = ArrayList() - withContext(Dispatchers.EDT) { - repository.modelAccess.executeUndoTransparentCommand { - ModelixMpsApi.runWithProject(mpsProject) { - result += body() + ApplicationManager.getApplication().invokeAndWait({ + ApplicationManager.getApplication().runWriteAction { + repository.modelAccess.executeUndoTransparentCommand { + ModelixMpsApi.runWithProject(mpsProject) { + result += body() + } } } - } + }, ModalityState.NON_MODAL) +// withContext(Dispatchers.Main) { +// } return result.single() } @@ -379,7 +383,7 @@ class Binding( val mpsProjects = listOf(mpsProject as MPSProject) val client = client() - val newVersion = repository.modelAccess.computeReadAction { + val newVersion = repository.computeRead { fun sync(invalidationTree: IIncrementalUpdateInformation): IVersion? { return oldVersion.runWrite(client) { branch -> ModelixMpsApi.runWithProject(mpsProject) { @@ -449,14 +453,14 @@ suspend fun jobLoop( } class BackoffStrategy( - val initialDelay: Duration = 500.milliseconds, - val maxDelay: Duration = 10.seconds, + val initialDelay: Long = 500, + val maxDelay: Long = 10_000, val factor: Double = 1.5, ) { - var currentDelay: Duration = initialDelay + var currentDelay: Long = initialDelay fun failed() { - currentDelay = (currentDelay * factor).coerceAtMost(maxDelay) + currentDelay = (currentDelay * factor).roundToLong().coerceAtMost(maxDelay) } fun success() { diff --git a/mps-sync-plugin3/src/main/kotlin/org/modelix/mps/sync3/ModelSyncStartupActivity.kt b/mps-sync-plugin3/src/main/kotlin/org/modelix/mps/sync3/ModelSyncStartupActivity.kt index c72929f4c1..892fb71d61 100644 --- a/mps-sync-plugin3/src/main/kotlin/org/modelix/mps/sync3/ModelSyncStartupActivity.kt +++ b/mps-sync-plugin3/src/main/kotlin/org/modelix/mps/sync3/ModelSyncStartupActivity.kt @@ -2,10 +2,10 @@ package org.modelix.mps.sync3 import com.intellij.openapi.components.service import com.intellij.openapi.project.Project -import com.intellij.openapi.startup.ProjectActivity +import com.intellij.openapi.startup.StartupActivity -class ModelSyncStartupActivity : ProjectActivity { - override suspend fun execute(project: Project) { +class ModelSyncStartupActivity : StartupActivity { + override fun runActivity(project: Project) { project.service() // just ensure it's initialized } } diff --git a/mps-sync-plugin3/src/main/kotlin/org/modelix/mps/sync3/ValidatingJob.kt b/mps-sync-plugin3/src/main/kotlin/org/modelix/mps/sync3/ValidatingJob.kt index 26e3ef30cd..c8b030f401 100644 --- a/mps-sync-plugin3/src/main/kotlin/org/modelix/mps/sync3/ValidatingJob.kt +++ b/mps-sync-plugin3/src/main/kotlin/org/modelix/mps/sync3/ValidatingJob.kt @@ -1,17 +1,18 @@ package org.modelix.mps.sync3 import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.launch private val LOG = mu.KotlinLogging.logger { } class ValidatingJob(private val validate: suspend () -> Unit) { - private val dirty = Channel(1, onBufferOverflow = BufferOverflow.DROP_LATEST) + private val dirty = Channel(1) fun invalidate() { - dirty.trySend(Unit) + // can't use trySend because it doesn't exist in MPS 2020.3 + @Suppress("DEPRECATION_ERROR") + dirty.offer(Unit) } suspend fun run() { diff --git a/mps-sync-plugin3/src/test/kotlin/org/modelix/mps/sync3/MPSTestBase.kt b/mps-sync-plugin3/src/test/kotlin/org/modelix/mps/sync3/MPSTestBase.kt index 4928cf567b..5539bc70c8 100644 --- a/mps-sync-plugin3/src/test/kotlin/org/modelix/mps/sync3/MPSTestBase.kt +++ b/mps-sync-plugin3/src/test/kotlin/org/modelix/mps/sync3/MPSTestBase.kt @@ -3,7 +3,7 @@ package org.modelix.mps.sync3 import com.intellij.ide.impl.OpenProjectTask import com.intellij.openapi.Disposable import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.application.EDT +import com.intellij.openapi.application.ModalityState import com.intellij.openapi.project.Project import com.intellij.openapi.project.ProjectManager import com.intellij.openapi.project.ex.ProjectManagerEx @@ -87,8 +87,10 @@ abstract class MPSTestBase : UsefulTestCase() { protected suspend fun command(body: () -> R): R { var result: R? = null - withContext(Dispatchers.EDT) { - mpsProject.modelAccess.executeCommand { result = body() } + withContext(Dispatchers.Main) { + ApplicationManager.getApplication().invokeAndWait({ + mpsProject.modelAccess.executeCommand { result = body() } + }, ModalityState.NON_MODAL) } return result as R } diff --git a/mps-sync-plugin3/src/test/kotlin/org/modelix/mps/sync3/ProjectSnapshot.kt b/mps-sync-plugin3/src/test/kotlin/org/modelix/mps/sync3/ProjectSnapshot.kt index 41f1b811f4..6614a6ac85 100644 --- a/mps-sync-plugin3/src/test/kotlin/org/modelix/mps/sync3/ProjectSnapshot.kt +++ b/mps-sync-plugin3/src/test/kotlin/org/modelix/mps/sync3/ProjectSnapshot.kt @@ -4,12 +4,15 @@ import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.project.Project import jetbrains.mps.ide.project.ProjectHelper import jetbrains.mps.project.AbstractModule +import jetbrains.mps.project.MPSExtentions import jetbrains.mps.smodel.Language import org.jetbrains.mps.openapi.model.EditableSModel import org.modelix.mps.api.ModelixMpsApi import org.w3c.dom.Element +import java.io.File import java.nio.file.Path import kotlin.io.path.absolute +import kotlin.io.path.extension import kotlin.io.path.isRegularFile import kotlin.io.path.pathString import kotlin.io.path.readText @@ -49,16 +52,36 @@ private fun Project.captureFileContents(): Map { ApplicationManager.getApplication().saveAll() save() } - return Path.of(this.basePath).walk().filter { it.isRegularFile() }.associate { file -> - val name = file.absolute().relativeTo(Path.of(basePath).absolute()).pathString - val content = file.readText().trim() - val xmlEndings = setOf("mps", "devkit", "mpl", "msd") - val normalizedContent = when { - xmlEndings.contains(name.substringAfterLast(".")) -> normalizeXmlFile(content) - else -> content + + // Files sometimes don't get deleted. Ignore them if they are not listed in the modules.xml + val visibleModules = HashSet() + File(basePath).resolve(".mps/modules.xml").takeIf { it.isFile }?.let { readXmlFile(it) }?.visitAll { + if (it is Element && it.tagName == "modulePath") { + visibleModules.add(Path.of(it.getAttribute("path").replace("\$PROJECT_DIR\$", basePath!!))) } - name to normalizedContent } + + val moduleEndings = setOf(MPSExtentions.DEVKIT, MPSExtentions.LANGUAGE, MPSExtentions.SOLUTION) + val xmlEndings = moduleEndings + setOf(MPSExtentions.MODEL) + + return Path.of(this.basePath).walk() + .filter { it.isRegularFile() } + .filter { + val isModuleFile = moduleEndings.contains(it.extension) + !isModuleFile || visibleModules.contains(it) + } + .associate { file -> + val name = file.absolute().relativeTo(Path.of(basePath).absolute()).pathString + val content = file.readText().trim() + + val normalizedContent = when { + xmlEndings.contains(name.substringAfterLast(".")) -> { + normalizeXmlFile(content) + } + else -> content + } + name to normalizedContent + } } private fun normalizeXmlFile(content: String): String { @@ -83,6 +106,21 @@ private fun normalizeXmlFile(content: String): String { node.setAttribute("path", "$contentPath/$location") } } + "classes" -> { + node.removeAttribute("path") + } + "language", "solution", "generator" -> { + node.removeAttribute("generatorOutputPath") + } + "registry" -> { + // metamodel may not be built yet and the names not available. + // Ignore them as they don't have any semantic meaning. + node.visitAll { (it as? Element)?.removeAttribute("name") } + } + "facets" -> { + // facets are not synchronized yet + node.parentNode.removeChild(node) + } } } return xmlToString(xml).lineSequence().filter { it.isNotBlank() }.joinToString("\n") diff --git a/mps-sync-plugin3/src/test/kotlin/org/modelix/mps/sync3/XMLUtils.kt b/mps-sync-plugin3/src/test/kotlin/org/modelix/mps/sync3/XMLUtils.kt index 5c94ece5dd..8dad081687 100644 --- a/mps-sync-plugin3/src/test/kotlin/org/modelix/mps/sync3/XMLUtils.kt +++ b/mps-sync-plugin3/src/test/kotlin/org/modelix/mps/sync3/XMLUtils.kt @@ -55,8 +55,7 @@ private fun disableDTD(dbf: DocumentBuilderFactory) { fun Node.visitAll(visitor: (Node) -> Unit) { visitor(this) - val childNodes = this.childNodes - for (i in 0 until childNodes.length) childNodes.item(i).visitAll(visitor) + children().forEach { it.visitAll(visitor) } } fun Node.childElements(): List = children().filterIsInstance()