From bf205be2491d6c7d69903a06969891db628dfa19 Mon Sep 17 00:00:00 2001 From: Sascha Lisson Date: Mon, 20 Nov 2023 16:42:29 +0100 Subject: [PATCH 01/14] fix(model-api-gen): concept.typed().untyped() failed for unknown concepts Concepts that weren't registered or had no generated typed API weren't handled properly when converting between typed and untyped APIs. --- .../org/modelix/metamodel/GeneratedConcept.kt | 7 ++- .../org/modelix/metamodel/ITypedChildLink.kt | 2 +- .../modelix/metamodel/ITypedReferenceLink.kt | 2 +- .../org/modelix/metamodel/UnknownConcept.kt | 45 +++++++++++++++++-- 4 files changed, 49 insertions(+), 7 deletions(-) diff --git a/model-api-gen-runtime/src/commonMain/kotlin/org/modelix/metamodel/GeneratedConcept.kt b/model-api-gen-runtime/src/commonMain/kotlin/org/modelix/metamodel/GeneratedConcept.kt index b573eecc28..ea2b641c99 100644 --- a/model-api-gen-runtime/src/commonMain/kotlin/org/modelix/metamodel/GeneratedConcept.kt +++ b/model-api-gen-runtime/src/commonMain/kotlin/org/modelix/metamodel/GeneratedConcept.kt @@ -231,7 +231,10 @@ abstract class GeneratedChildLink +fun IChildLink.typed(): ITypedChildLink { + return this as? ITypedChildLink + ?: if (isMultiple) UnknownTypedChildLinkList(this) else UnknownTypedSingleChildLink(this) +} open class GeneratedSingleChildLink>( owner: IConcept, @@ -281,4 +284,4 @@ class GeneratedReferenceLink +fun IReferenceLink.typed() = this as? ITypedReferenceLink ?: UnknownTypedReferenceLink(this) diff --git a/model-api-gen-runtime/src/commonMain/kotlin/org/modelix/metamodel/ITypedChildLink.kt b/model-api-gen-runtime/src/commonMain/kotlin/org/modelix/metamodel/ITypedChildLink.kt index 8668fcd57b..859d62e1a3 100644 --- a/model-api-gen-runtime/src/commonMain/kotlin/org/modelix/metamodel/ITypedChildLink.kt +++ b/model-api-gen-runtime/src/commonMain/kotlin/org/modelix/metamodel/ITypedChildLink.kt @@ -17,7 +17,7 @@ import org.modelix.model.api.IChildLink import org.modelix.model.api.INode import org.modelix.model.api.remove -interface ITypedChildLink : ITypedConceptFeature { +interface ITypedChildLink : ITypedConceptFeature { fun untyped(): IChildLink fun castChild(childNode: INode): ChildT fun getTypedChildConcept(): IConceptOfTypedNode diff --git a/model-api-gen-runtime/src/commonMain/kotlin/org/modelix/metamodel/ITypedReferenceLink.kt b/model-api-gen-runtime/src/commonMain/kotlin/org/modelix/metamodel/ITypedReferenceLink.kt index 0184c07b1e..99d75e73e6 100644 --- a/model-api-gen-runtime/src/commonMain/kotlin/org/modelix/metamodel/ITypedReferenceLink.kt +++ b/model-api-gen-runtime/src/commonMain/kotlin/org/modelix/metamodel/ITypedReferenceLink.kt @@ -16,7 +16,7 @@ package org.modelix.metamodel import org.modelix.model.api.INode import org.modelix.model.api.IReferenceLink -interface ITypedReferenceLink : ITypedConceptFeature { +interface ITypedReferenceLink : ITypedConceptFeature { fun untyped(): IReferenceLink fun castTarget(target: INode): TargetT fun getTypedTargetConcept(): IConceptOfTypedNode diff --git a/model-api-gen-runtime/src/commonMain/kotlin/org/modelix/metamodel/UnknownConcept.kt b/model-api-gen-runtime/src/commonMain/kotlin/org/modelix/metamodel/UnknownConcept.kt index daed393675..5801ec6605 100644 --- a/model-api-gen-runtime/src/commonMain/kotlin/org/modelix/metamodel/UnknownConcept.kt +++ b/model-api-gen-runtime/src/commonMain/kotlin/org/modelix/metamodel/UnknownConcept.kt @@ -7,7 +7,9 @@ import org.modelix.model.api.ILanguage import org.modelix.model.api.INode import org.modelix.model.api.IProperty import org.modelix.model.api.IReferenceLink +import org.modelix.model.api.getConcept import org.modelix.model.area.IArea +import kotlin.reflect.KClass abstract class EmptyConcept : IConcept { override fun isAbstract(): Boolean = true @@ -77,15 +79,52 @@ data class UnknownConcept(private val ref: IConceptReference) : EmptyConcept() { override fun getLongName(): String = getShortName() } -data class UnknownTypedConcept(private val ref: IConceptReference?) : ITypedConcept { +data class UnknownTypedConcept(private val ref: IConcept?) : IConceptOfTypedNode { override fun untyped(): IConcept { - return ref?.let { UnknownConcept(it) } ?: NullConcept + return ref ?: NullConcept + } + + override fun getInstanceInterface(): KClass { + return UnknownConceptInstance::class } } data class UnknownConceptInstance(val node: INode) : ITypedNode { override val _concept: ITypedConcept - get() = UnknownTypedConcept(node.getConceptReference()) + get() = UnknownTypedConcept(node.getConcept()) override fun unwrap(): INode = node } + +abstract class UnknownTypedChildLink : ITypedChildLink { + + override fun castChild(childNode: INode): ITypedNode { + return childNode.typed() + } + + override fun getTypedChildConcept(): IConceptOfTypedNode { + return untyped().targetConcept.typed() as IConceptOfTypedNode + } +} + +data class UnknownTypedSingleChildLink(private val link: IChildLink) : UnknownTypedChildLink(), ITypedSingleChildLink { + override fun untyped(): IChildLink = link +} + +data class UnknownTypedChildLinkList(private val link: IChildLink) : UnknownTypedChildLink(), ITypedChildListLink { + override fun untyped(): IChildLink = link +} + +data class UnknownTypedReferenceLink(private val link: IReferenceLink) : ITypedReferenceLink { + override fun untyped(): IReferenceLink { + return link + } + + override fun castTarget(target: INode): ITypedNode { + return target.typed() + } + + override fun getTypedTargetConcept(): IConceptOfTypedNode { + return untyped().targetConcept.typed() as IConceptOfTypedNode + } +} From 8ed3a464ec41413cad553fc6145b63b744a9ee7e Mon Sep 17 00:00:00 2001 From: Sascha Lisson Date: Mon, 20 Nov 2023 16:44:09 +0100 Subject: [PATCH 02/14] fix(model-api): added @Serializable to NodeReference To allow direct use in other serializable classes. --- .../kotlin/org/modelix/model/api/SerializedNodeReference.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/model-api/src/commonMain/kotlin/org/modelix/model/api/SerializedNodeReference.kt b/model-api/src/commonMain/kotlin/org/modelix/model/api/SerializedNodeReference.kt index e372970ca0..3d968bcf50 100644 --- a/model-api/src/commonMain/kotlin/org/modelix/model/api/SerializedNodeReference.kt +++ b/model-api/src/commonMain/kotlin/org/modelix/model/api/SerializedNodeReference.kt @@ -1,8 +1,11 @@ package org.modelix.model.api +import kotlinx.serialization.Serializable + @Deprecated("renamed to NodeReference", ReplaceWith("NodeReference")) typealias SerializedNodeReference = NodeReference +@Serializable data class NodeReference(val serialized: String) : INodeReference { override fun serialize(): String = serialized } From 5882ac4b8ffec673d5217b184119ca29c4c18d86 Mon Sep 17 00:00:00 2001 From: Sascha Lisson Date: Mon, 20 Nov 2023 16:49:41 +0100 Subject: [PATCH 03/14] fix(model-api): deprecated IConcept.getConcept() It's an extension method that does an unnecessary resolution of the concept. The implementation INode.concept decides if the resolution is necessary or if the concept can be accessed in a more efficient way. --- .../commonMain/kotlin/org/modelix/metamodel/TypedNodeImpl.kt | 3 +-- .../commonMain/kotlin/org/modelix/metamodel/UnknownConcept.kt | 3 +-- model-api/src/commonMain/kotlin/org/modelix/model/api/INode.kt | 1 + 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/model-api-gen-runtime/src/commonMain/kotlin/org/modelix/metamodel/TypedNodeImpl.kt b/model-api-gen-runtime/src/commonMain/kotlin/org/modelix/metamodel/TypedNodeImpl.kt index 14cf5a1514..361fc5ca43 100644 --- a/model-api-gen-runtime/src/commonMain/kotlin/org/modelix/metamodel/TypedNodeImpl.kt +++ b/model-api-gen-runtime/src/commonMain/kotlin/org/modelix/metamodel/TypedNodeImpl.kt @@ -2,13 +2,12 @@ package org.modelix.metamodel import org.modelix.model.api.IConcept import org.modelix.model.api.INode -import org.modelix.model.api.getConcept abstract class TypedNodeImpl(val wrappedNode: INode) : ITypedNode { init { val expected: IConcept = _concept._concept - val actual: IConcept? = unwrap().getConcept() + val actual: IConcept? = unwrap().concept require(actual != null && actual.isSubConceptOf(expected)) { "Concept of node ${unwrap()} expected to be a sub-concept of $expected, but was $actual" } diff --git a/model-api-gen-runtime/src/commonMain/kotlin/org/modelix/metamodel/UnknownConcept.kt b/model-api-gen-runtime/src/commonMain/kotlin/org/modelix/metamodel/UnknownConcept.kt index 5801ec6605..527b6b5b3c 100644 --- a/model-api-gen-runtime/src/commonMain/kotlin/org/modelix/metamodel/UnknownConcept.kt +++ b/model-api-gen-runtime/src/commonMain/kotlin/org/modelix/metamodel/UnknownConcept.kt @@ -7,7 +7,6 @@ import org.modelix.model.api.ILanguage import org.modelix.model.api.INode import org.modelix.model.api.IProperty import org.modelix.model.api.IReferenceLink -import org.modelix.model.api.getConcept import org.modelix.model.area.IArea import kotlin.reflect.KClass @@ -91,7 +90,7 @@ data class UnknownTypedConcept(private val ref: IConcept?) : IConceptOfTypedNode data class UnknownConceptInstance(val node: INode) : ITypedNode { override val _concept: ITypedConcept - get() = UnknownTypedConcept(node.getConcept()) + get() = UnknownTypedConcept(node.concept) override fun unwrap(): INode = node } diff --git a/model-api/src/commonMain/kotlin/org/modelix/model/api/INode.kt b/model-api/src/commonMain/kotlin/org/modelix/model/api/INode.kt index 4233949423..670d77ef2e 100644 --- a/model-api/src/commonMain/kotlin/org/modelix/model/api/INode.kt +++ b/model-api/src/commonMain/kotlin/org/modelix/model/api/INode.kt @@ -307,6 +307,7 @@ fun INode.setReferenceTarget(link: IReferenceLink, target: INodeReference?): Uni fun INode.getPropertyValue(property: IProperty): String? = if (this is INodeEx) getPropertyValue(property) else getPropertyValue(property.key(this)) fun INode.setPropertyValue(property: IProperty, value: String?): Unit = if (this is INodeEx) setPropertyValue(property, value) else setPropertyValue(property.key(this), value) +@Deprecated("use INode.concept", ReplaceWith("concept")) fun INode.getConcept(): IConcept? = getConceptReference()?.resolve() fun INode.getResolvedReferenceTarget(role: String): INode? = getReferenceTargetRef(role)?.resolveIn(getArea()!!) fun INode.getResolvedConcept(): IConcept? = getConceptReference()?.resolve() From 449c9a870ec7be23d422aeb4950768d8447bcee0 Mon Sep 17 00:00:00 2001 From: Sascha Lisson Date: Fri, 24 Nov 2023 18:05:09 +0100 Subject: [PATCH 04/14] fix(model-api-gen): added out variance to INode.typed(nodeClass: KClass) Was INode.typed(nodeClass: KClass) before. Just to avoid some unnecessary casts. --- .../kotlin/org/modelix/metamodel/TypedLanguagesRegistry.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/model-api-gen-runtime/src/commonMain/kotlin/org/modelix/metamodel/TypedLanguagesRegistry.kt b/model-api-gen-runtime/src/commonMain/kotlin/org/modelix/metamodel/TypedLanguagesRegistry.kt index d4be88ea9e..bc3f6258cf 100644 --- a/model-api-gen-runtime/src/commonMain/kotlin/org/modelix/metamodel/TypedLanguagesRegistry.kt +++ b/model-api-gen-runtime/src/commonMain/kotlin/org/modelix/metamodel/TypedLanguagesRegistry.kt @@ -49,7 +49,7 @@ object TypedLanguagesRegistry : ILanguageRepository { override fun getPriority(): Int = 2000 } -fun INode.typed(nodeClass: KClass): NodeT = nodeClass.cast(TypedLanguagesRegistry.wrapNode(this)) +fun INode.typed(nodeClass: KClass): NodeT = nodeClass.cast(TypedLanguagesRegistry.wrapNode(this)) inline fun INode.typed(): NodeT = TypedLanguagesRegistry.wrapNode(this) as NodeT fun INode.typedUnsafe(): NodeT = TypedLanguagesRegistry.wrapNode(this) as NodeT From 5b37af5273aee33e94379e05855f70194cf0d30d Mon Sep 17 00:00:00 2001 From: Sascha Lisson Date: Fri, 24 Nov 2023 18:38:08 +0100 Subject: [PATCH 05/14] fix(model-api-gen-gradle): older MPS versions failed to run on Java 17 --- .../GenerateAntScriptForMpsMetaModelExport.kt | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/model-api-gen-gradle/src/main/kotlin/org/modelix/metamodel/gradle/GenerateAntScriptForMpsMetaModelExport.kt b/model-api-gen-gradle/src/main/kotlin/org/modelix/metamodel/gradle/GenerateAntScriptForMpsMetaModelExport.kt index a21fbf12a9..168d73a2cc 100644 --- a/model-api-gen-gradle/src/main/kotlin/org/modelix/metamodel/gradle/GenerateAntScriptForMpsMetaModelExport.kt +++ b/model-api-gen-gradle/src/main/kotlin/org/modelix/metamodel/gradle/GenerateAntScriptForMpsMetaModelExport.kt @@ -91,6 +91,41 @@ abstract class GenerateAntScriptForMpsMetaModelExport @Inject constructor(of: Ob + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ${ if (exportModulesFilter.isPresent) { """""" From c045d23f4905200a7126f894e850985b33008fc5 Mon Sep 17 00:00:00 2001 From: Sascha Lisson Date: Fri, 24 Nov 2023 21:52:41 +0100 Subject: [PATCH 06/14] feat(mps-model-adapters): support for incremental computing MPS adapters now notify about read/write access. --- mps-model-adapters/build.gradle.kts | 13 +- .../model/mpsadapters/GlobalModelListener.kt | 148 ++++++++++ .../model/mpsadapters/MPSChangeTranslator.kt | 253 ++++++++++++++++++ .../org/modelix/model/mpsadapters/MPSNode.kt | 39 ++- 4 files changed, 446 insertions(+), 7 deletions(-) create mode 100644 mps-model-adapters/src/main/kotlin/org/modelix/model/mpsadapters/GlobalModelListener.kt create mode 100644 mps-model-adapters/src/main/kotlin/org/modelix/model/mpsadapters/MPSChangeTranslator.kt diff --git a/mps-model-adapters/build.gradle.kts b/mps-model-adapters/build.gradle.kts index a765db517f..d0eef73e77 100644 --- a/mps-model-adapters/build.gradle.kts +++ b/mps-model-adapters/build.gradle.kts @@ -7,12 +7,17 @@ val mpsVersion = project.findProperty("mps.version")?.toString().takeIf { !it.is dependencies { api(project(":model-api")) + implementation(libs.modelix.incremental) - compileOnly("com.jetbrains:mps-openapi:$mpsVersion") - compileOnly("com.jetbrains:mps-core:$mpsVersion") - compileOnly("com.jetbrains:mps-environment:$mpsVersion") - implementation(libs.trove) + val mpsZip by configurations.creating + mpsZip("com.jetbrains:mps:$mpsVersion") + compileOnly( + zipTree({ mpsZip.singleFile }).matching { + include("lib/*.jar") + }, + ) + implementation(libs.trove) implementation(kotlin("stdlib")) implementation(libs.kotlin.logging) } diff --git a/mps-model-adapters/src/main/kotlin/org/modelix/model/mpsadapters/GlobalModelListener.kt b/mps-model-adapters/src/main/kotlin/org/modelix/model/mpsadapters/GlobalModelListener.kt new file mode 100644 index 0000000000..f8759b9951 --- /dev/null +++ b/mps-model-adapters/src/main/kotlin/org/modelix/model/mpsadapters/GlobalModelListener.kt @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2023. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.modelix.model.mpsadapters + +import org.jetbrains.mps.openapi.language.SLanguage +import org.jetbrains.mps.openapi.model.SModel +import org.jetbrains.mps.openapi.model.SModelReference +import org.jetbrains.mps.openapi.module.SDependency +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 + +abstract class GlobalModelListener { + @Suppress("removal") + protected var repositoryListener: SRepositoryListener = object : SRepositoryListener { + override fun moduleAdded(m: SModule) { + start(m) + } + + override fun beforeModuleRemoved(m: SModule) { + stop(m) + } + + override fun moduleRemoved(p0: SModuleReference) {} + override fun commandStarted(p0: SRepository?) {} + override fun commandFinished(p0: SRepository?) {} + override fun updateStarted(p0: SRepository?) {} + override fun updateFinished(p0: SRepository?) {} + override fun repositoryCommandStarted(p0: SRepository?) {} + override fun repositoryCommandFinished(p0: SRepository?) {} + } + + @Suppress("removal") + protected var moduleListener: SModuleListener = object : SModuleListener { + override fun modelAdded(module: SModule, model: SModel) { + start(model) + } + + override fun beforeModelRemoved(module: SModule, model: SModel) { + stop(model) + } + + override fun modelRemoved(p0: SModule?, p1: SModelReference?) {} + override fun beforeModelRenamed(p0: SModule?, p1: SModel?, p2: SModelReference?) {} + override fun modelRenamed(p0: SModule?, p1: SModel?, p2: SModelReference?) {} + override fun dependencyAdded(p0: SModule?, p1: SDependency?) {} + override fun dependencyRemoved(p0: SModule?, p1: SDependency?) {} + override fun languageAdded(p0: SModule?, p1: SLanguage?) {} + override fun languageRemoved(p0: SModule?, p1: SLanguage?) {} + override fun moduleChanged(p0: SModule?) {} + } + protected var myRepositories: MutableSet = HashSet() + protected var myModules: MutableSet = HashSet() + protected var myModels: MutableSet = HashSet() + fun start(repo: SRepository) { + if (myRepositories.contains(repo)) { + return + } + myRepositories.add(repo) + repo.addRepositoryListener(repositoryListener) + repo.modelAccess.runReadAction { + for (module in repo.modules) { + start(module) + } + } + addListener(repo) + } + + open fun start(module: SModule) { + if (myModules.contains(module)) { + return + } + myModules.add(module) + module.addModuleListener(moduleListener) + for (model in module.models) { + start(model) + } + addListener(module) + } + + fun start(model: SModel) { + if (myModels.contains(model)) { + return + } + myModels.add(model) + addListener(model) + } + + protected open fun addListener(repository: SRepository) {} + protected open fun addListener(module: SModule) {} + protected abstract fun addListener(model: SModel) + fun stop() { + for (repo in myRepositories) { + repo.modelAccess.runReadAction { stop(repo) } + } + } + + fun stop(repo: SRepository) { + if (!myRepositories.contains(repo)) { + return + } + myRepositories.remove(repo) + repo.removeRepositoryListener(repositoryListener) + for (module in repo.modules) { + stop(module) + } + removeListener(repo) + } + + open fun stop(module: SModule) { + if (!myModules.contains(module)) { + return + } + myModules.remove(module) + module.removeModuleListener(moduleListener) + for (model in module.models) { + stop(model) + } + removeListener(module) + } + + fun stop(model: SModel) { + if (!myModels.contains(model)) { + return + } + myModels.remove(model) + removeListener(model) + } + + protected open fun removeListener(repository: SRepository) {} + protected open fun removeListener(module: SModule) {} + protected abstract fun removeListener(model: SModel) +} diff --git a/mps-model-adapters/src/main/kotlin/org/modelix/model/mpsadapters/MPSChangeTranslator.kt b/mps-model-adapters/src/main/kotlin/org/modelix/model/mpsadapters/MPSChangeTranslator.kt new file mode 100644 index 0000000000..3cf644cf93 --- /dev/null +++ b/mps-model-adapters/src/main/kotlin/org/modelix/model/mpsadapters/MPSChangeTranslator.kt @@ -0,0 +1,253 @@ +package org.modelix.model.mpsadapters + +import jetbrains.mps.smodel.SModelInternal +import jetbrains.mps.smodel.event.SModelChildEvent +import jetbrains.mps.smodel.event.SModelDevKitEvent +import jetbrains.mps.smodel.event.SModelImportEvent +import jetbrains.mps.smodel.event.SModelLanguageEvent +import jetbrains.mps.smodel.event.SModelListener +import jetbrains.mps.smodel.event.SModelPropertyEvent +import jetbrains.mps.smodel.event.SModelReferenceEvent +import jetbrains.mps.smodel.event.SModelRenamedEvent +import jetbrains.mps.smodel.event.SModelRootEvent +import jetbrains.mps.smodel.loading.ModelLoadingState +import org.jetbrains.mps.openapi.event.SNodeAddEvent +import org.jetbrains.mps.openapi.event.SNodeRemoveEvent +import org.jetbrains.mps.openapi.event.SPropertyChangeEvent +import org.jetbrains.mps.openapi.event.SReferenceChangeEvent +import org.jetbrains.mps.openapi.language.SContainmentLink +import org.jetbrains.mps.openapi.language.SLanguage +import org.jetbrains.mps.openapi.language.SProperty +import org.jetbrains.mps.openapi.language.SReferenceLink +import org.jetbrains.mps.openapi.model.SModel +import org.jetbrains.mps.openapi.model.SModelReference +import org.jetbrains.mps.openapi.model.SNode +import org.jetbrains.mps.openapi.model.SNodeChangeListener +import org.jetbrains.mps.openapi.module.SDependency +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.modelix.incremental.DependencyTracking +import org.modelix.incremental.IStateVariableReference + +class MPSChangeTranslator : + GlobalModelListener(), + SNodeChangeListener, + SModuleListener, + SRepositoryListener, + SModelListener, + org.jetbrains.mps.openapi.model.SModelListener { + + @Suppress("removal") + override fun updateStarted(p0: SRepository?) {} + + @Suppress("removal") + override fun updateFinished(p0: SRepository?) {} + + @Suppress("removal") + override fun repositoryCommandStarted(p0: SRepository?) {} + + @Suppress("removal") + override fun repositoryCommandFinished(p0: SRepository?) {} + + private fun notifyChange(change: IStateVariableReference<*>) { + DependencyTracking.modified(change) + } + + override fun addListener(model: SModel) { + model.addChangeListener(this) + model.addModelListener(this) + (model as SModelInternal).addModelListener(this) + } + + override fun removeListener(model: SModel) { + model.removeChangeListener(this) + model.removeModelListener(this) + (model as SModelInternal).removeModelListener(this) + } + + override fun addListener(module: SModule) { + module.addModuleListener(this) + } + + override fun removeListener(module: SModule) { + module.removeModuleListener(this) + } + + override fun addListener(repository: SRepository) { + repository.addRepositoryListener(this) + } + + override fun removeListener(repository: SRepository) { + repository.removeRepositoryListener(this) + } + + override fun propertyChanged(e: SPropertyChangeEvent) { + notifyChange(MPSPropertyDependency(e.node, e.property)) + } + + override fun referenceChanged(e: SReferenceChangeEvent) { + notifyChange(MPSReferenceDependency(e.node, e.associationLink)) + } + + override fun nodeAdded(e: SNodeAddEvent) { + if (e.parent != null) { + notifyChange(MPSChildrenDependency(e.parent!!, e.aggregationLink!!)) + } else { + notifyChange(MPSRootNodesListDependency(e.model)) + } + notifyChange(MPSContainmentDependency(e.child)) + } + + override fun nodeRemoved(e: SNodeRemoveEvent) { + if (e.parent != null) { + notifyChange(MPSChildrenDependency(e.parent!!, e.aggregationLink!!)) + } else { + notifyChange(MPSRootNodesListDependency(e.model)) + } + notifyChange(MPSContainmentDependency(e.child)) + } + + override fun beforeChildRemoved(event: SModelChildEvent) {} + override fun beforeModelDisposed(model: SModel) {} + override fun beforeModelRenamed(event: SModelRenamedEvent) {} + override fun beforeRootRemoved(event: SModelRootEvent) {} + override fun childAdded(event: SModelChildEvent) {} + override fun childRemoved(event: SModelChildEvent) {} + override fun devkitAdded(event: SModelDevKitEvent) {} + override fun devkitRemoved(event: SModelDevKitEvent) {} + override fun getPriority(): SModelListener.SModelListenerPriority { + return SModelListener.SModelListenerPriority.CLIENT + } + + override fun importAdded(event: SModelImportEvent) { + notifyChange(MPSModelDependency(event.model)) + } + + override fun importRemoved(event: SModelImportEvent) { + notifyChange(MPSModelDependency(event.model)) + } + + override fun languageAdded(event: SModelLanguageEvent) {} + override fun languageRemoved(event: SModelLanguageEvent) {} + override fun modelLoadingStateChanged(model: SModel, state: ModelLoadingState) {} + override fun modelRenamed(event: SModelRenamedEvent) {} + override fun modelSaved(model: SModel) {} + override fun propertyChanged(event: SModelPropertyEvent) {} + override fun referenceAdded(event: SModelReferenceEvent) {} + override fun referenceRemoved(event: SModelReferenceEvent) {} + + @Deprecated("") + override fun rootAdded(event: SModelRootEvent) { + } + + @Deprecated("") + override fun rootRemoved(event: SModelRootEvent) { + } + + override fun modelLoaded(model: SModel, partially: Boolean) {} + override fun modelReplaced(model: SModel) { + notifyChange(MPSModelContentDependency(model)) + } + + override fun modelUnloaded(model: SModel) {} + override fun modelAttached(model: SModel, repository: SRepository) {} + override fun modelDetached(model: SModel, repository: SRepository) {} + override fun conflictDetected(model: SModel) {} + override fun problemsDetected(model: SModel, problems: Iterable) {} + override fun modelAdded(module: SModule, model: SModel) { + notifyChange(MPSModuleDependency(module)) + } + + override fun beforeModelRemoved(module: SModule, model: SModel) { + notifyChange(MPSModuleDependency(module)) + } + + override fun modelRemoved(module: SModule, reference: SModelReference) {} + override fun beforeModelRenamed(module: SModule, model: SModel, reference: SModelReference) {} + override fun modelRenamed(module: SModule, model: SModel, reference: SModelReference) { + notifyChange(MPSModelDependency(model)) + } + + override fun dependencyAdded(module: SModule, dependency: SDependency) {} + override fun dependencyRemoved(module: SModule, dependency: SDependency) {} + override fun languageAdded(module: SModule, language: SLanguage) {} + override fun languageRemoved(module: SModule, language: SLanguage) {} + override fun moduleChanged(module: SModule) {} + override fun moduleAdded(module: SModule) { + notifyChange(MPSRepositoryDependency) + } + + override fun beforeModuleRemoved(module: SModule) { + notifyChange(MPSRepositoryDependency) + } + + override fun moduleRemoved(reference: SModuleReference) {} + override fun commandStarted(repository: SRepository) {} + override fun commandFinished(repository: SRepository) {} +} + +abstract class MPSDependencyBase : IStateVariableReference { + override fun read(): Any? = null +} + +data class MPSNodeDependency(val node: SNode) : MPSDependencyBase() { + override fun getGroup() = node.parent?.let { MPSNodeDependency(it) } +} + +data class MPSPropertyDependency(val node: SNode, val property: SProperty) : MPSDependencyBase() { + override fun getGroup() = MPSAllPropertiesDependency(node) +} + +data class MPSReferenceDependency(val node: SNode, val link: SReferenceLink) : MPSDependencyBase() { + override fun getGroup() = MPSAllReferencesDependency(node) +} + +data class MPSChildrenDependency(val node: SNode, val link: SContainmentLink) : MPSDependencyBase() { + override fun getGroup() = MPSAllChildrenDependency(node) +} + +data class MPSAllChildrenDependency(val node: SNode) : MPSDependencyBase() { + override fun getGroup() = MPSNodeDependency(node) +} + +data class MPSAllPropertiesDependency(val node: SNode) : MPSDependencyBase() { + override fun getGroup() = MPSNodeDependency(node) +} + +data class MPSAllReferencesDependency(val node: SNode) : MPSDependencyBase() { + override fun getGroup() = MPSNodeDependency(node) +} + +data class MPSContainmentDependency(val node: SNode) : MPSDependencyBase() { + override fun getGroup() = MPSNodeDependency(node) +} + +/** + * No SRepository parameter, because there is only one repository in MPS. + * If one repository changes, all of them change. + */ +object MPSRepositoryDependency : MPSDependencyBase() { + override fun getGroup() = null +} + +data class MPSModuleDependency(val model: SModule) : MPSDependencyBase() { + override fun getGroup() = MPSRepositoryDependency +} + +data class MPSModelDependency(val model: SModel) : MPSDependencyBase() { + override fun getGroup() = MPSModuleDependency(model.module) +} + +data class MPSRootNodesListDependency(val model: SModel) : MPSDependencyBase() { + override fun getGroup() = MPSModelDependency(model) +} + +/** + * This is used to handle the case that MPS reloads a model from disk. + */ +data class MPSModelContentDependency(val model: SModel) : MPSDependencyBase() { + override fun getGroup() = MPSModelDependency(model) +} 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 7b35173284..0338b48ae0 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 @@ -24,6 +24,7 @@ import jetbrains.mps.smodel.adapter.structure.property.SPropertyAdapterById import jetbrains.mps.smodel.adapter.structure.ref.SReferenceLinkAdapterById import org.jetbrains.mps.openapi.language.SContainmentLink import org.jetbrains.mps.openapi.model.SNode +import org.modelix.incremental.DependencyTracking import org.modelix.model.api.BuiltinLanguages import org.modelix.model.api.ConceptReference import org.modelix.model.api.IChildLink @@ -46,9 +47,15 @@ data class MPSNode(val node: SNode) : IDefaultNodeAdapter { override val reference: INodeReference get() = MPSNodeReference(node.reference) override val concept: IConcept - get() = MPSConcept(node.concept) + get() { + DependencyTracking.accessed(MPSNodeDependency(node)) + return MPSConcept(node.concept) + } override val parent: INode? - get() = node.parent?.let { MPSNode(it) } ?: node.model?.let { MPSModelAsNode(it) } + get() { + DependencyTracking.accessed(MPSContainmentDependency(node)) + return node.parent?.let { MPSNode(it) } ?: node.model?.let { MPSModelAsNode(it) } + } override fun tryGetConcept(): IConcept { return MPSConcept(node.concept) @@ -59,7 +66,10 @@ data class MPSNode(val node: SNode) : IDefaultNodeAdapter { } override val allChildren: Iterable - get() = node.children.map { MPSNode(it) } + get() { + DependencyTracking.accessed(MPSAllChildrenDependency(node)) + return node.children.map { MPSNode(it) } + } override fun removeChild(child: INode) { require(child is MPSNode) { "child must be an MPSNode" } @@ -67,18 +77,26 @@ data class MPSNode(val node: SNode) : IDefaultNodeAdapter { } override fun getPropertyLinks(): List { + DependencyTracking.accessed(MPSNodeDependency(node)) return node.properties.map { MPSProperty(it) } } override fun getReferenceLinks(): List { + DependencyTracking.accessed(MPSNodeDependency(node)) return node.references.map { MPSReferenceLink(it.link) } } override fun getContainmentLink(): IChildLink { + DependencyTracking.accessed(MPSNodeDependency(node)) return node.containmentLink?.let { MPSChildLink(it) } ?: BuiltinLanguages.MPSRepositoryConcepts.Model.rootNodes } override fun getChildren(link: IChildLink): Iterable { + if (link is MPSChildLink) { + DependencyTracking.accessed(MPSChildrenDependency(node, link.link)) + } else { + DependencyTracking.accessed(MPSAllChildrenDependency(node)) + } return node.children.map { MPSNode(it) }.filter { it.getContainmentLink().conformsTo(link) } @@ -132,6 +150,11 @@ data class MPSNode(val node: SNode) : IDefaultNodeAdapter { } override fun getReferenceTarget(link: IReferenceLink): INode? { + if (link is MPSReferenceLink) { + DependencyTracking.accessed(MPSReferenceDependency(node, link.link)) + } else { + DependencyTracking.accessed(MPSAllReferencesDependency(node)) + } return node.references.filter { MPSReferenceLink(it.link).getUID() == link.getUID() } .firstOrNull()?.targetNode?.let { MPSNode(it) } } @@ -153,11 +176,21 @@ data class MPSNode(val node: SNode) : IDefaultNodeAdapter { } override fun getReferenceTargetRef(role: IReferenceLink): INodeReference? { + if (role is MPSReferenceLink) { + DependencyTracking.accessed(MPSReferenceDependency(node, role.link)) + } else { + DependencyTracking.accessed(MPSAllReferencesDependency(node)) + } return node.references.firstOrNull { MPSReferenceLink(it.link).getUID() == role.getUID() } ?.targetNodeReference?.let { MPSNodeReference(it) } } override fun getPropertyValue(property: IProperty): String? { + if (property is MPSProperty) { + DependencyTracking.accessed(MPSPropertyDependency(node, property.property)) + } else { + DependencyTracking.accessed(MPSAllPropertiesDependency(node)) + } val mpsProperty = node.properties.firstOrNull { MPSProperty(it).getUID() == property.getUID() } ?: return null return node.getProperty(mpsProperty) } From 40879a43cd9fa50353bf94011d07cc6f8255fab4 Mon Sep 17 00:00:00 2001 From: Sascha Lisson Date: Sat, 25 Nov 2023 13:35:12 +0100 Subject: [PATCH 07/14] fix(mps-model-adapters): execute command instead of writeAction if possible Changes done through Modelix weren't shown in the MPS editor. Since the change in https://github.com/JetBrains/MPS/commit/99b50c6e8cbf0c1de62074121170b859faefc2ee MPS doesn't ensure anymore that a command is used to modify models. --- .../org/modelix/model/mpsadapters/MPSArea.kt | 42 ++++++++++++++++--- 1 file changed, 37 insertions(+), 5 deletions(-) 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 acbc27c225..cace44d610 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 @@ -13,6 +13,8 @@ */ package org.modelix.model.mpsadapters +import jetbrains.mps.ide.ThreadUtils +import jetbrains.mps.project.Project import jetbrains.mps.project.ProjectBase import jetbrains.mps.project.ProjectManager import jetbrains.mps.project.facets.JavaModuleFacet @@ -108,12 +110,42 @@ data class MPSArea(val repository: SRepository) : IArea, IAreaReference { override fun executeWrite(f: () -> T): T { var result: T? = null - if (repository.modelAccess is GlobalModelAccess) { - repository.modelAccess.runWriteAction { result = f() } - } else { - repository.modelAccess.executeCommand { result = f() } + executeWrite({ result = f() }, enforceCommand = true) + return result as T + } + + fun executeWrite(f: () -> Unit, enforceCommand: Boolean) { + // Try to execute a command instead of a write action if possible, + // because write actions don't trigger an update of the MPS editor. + + // A command can only be executed on the EDT (Event Dispatch Thread/AWT Thread/UI Thread). + // We could dispatch it to the EDT and wait for the result, but that increases the risk for deadlocks. + // The caller is responsible for calling this method from the EDT if a command is desired. + val inEDT = ThreadUtils.isInEDT() + + if (inEDT || enforceCommand) { + val projects: Sequence = Sequence { ProjectManager.getInstance().openedProjects.iterator() } + val modelAccessCandidates = sequenceOf(repository.modelAccess) + projects.map { it.modelAccess } + // GlobalModelAccess throws an Exception when trying to execute a command. + // Only a ProjectModelAccess can execute a command. + val modelAccess = modelAccessCandidates.filter { it !is GlobalModelAccess }.firstOrNull() + + if (modelAccess != null) { + if (inEDT) { + modelAccess.executeCommand { f() } + } else { + ThreadUtils.runInUIThreadAndWait { + modelAccess.executeCommand { f() } + } + } + return + } } - return result!! + + // For a write access any ModelAccess works. + // If there is no ModelAccess that is not a GlobalModelAccess then there are probably no open projects and + // there can't be any open editors, so the issues doesn't exist. + repository.modelAccess.runWriteAction { f() } } override fun canRead(): Boolean { From 05f18f769e30bf6fb4ebc8a1a3e2927dcd000ae6 Mon Sep 17 00:00:00 2001 From: Sascha Lisson Date: Mon, 27 Nov 2023 16:57:20 +0100 Subject: [PATCH 08/14] ci: also build against MPS 2023.2 --- .github/workflows/mps-compatibility.yaml | 1 + .../model/mpsadapters/GlobalModelListener.kt | 32 ++++--------------- .../model/mpsadapters/MPSChangeTranslator.kt | 17 +++------- mps-model-server-plugin/build.gradle.kts | 13 +++++--- 4 files changed, 21 insertions(+), 42 deletions(-) diff --git a/.github/workflows/mps-compatibility.yaml b/.github/workflows/mps-compatibility.yaml index b1dad40cb3..25a4097f25 100644 --- a/.github/workflows/mps-compatibility.yaml +++ b/.github/workflows/mps-compatibility.yaml @@ -22,6 +22,7 @@ jobs: - "2021.3.3" - "2022.2" - "2022.3" + - "2023.2" steps: - uses: actions/checkout@v4 diff --git a/mps-model-adapters/src/main/kotlin/org/modelix/model/mpsadapters/GlobalModelListener.kt b/mps-model-adapters/src/main/kotlin/org/modelix/model/mpsadapters/GlobalModelListener.kt index f8759b9951..74e469ac83 100644 --- a/mps-model-adapters/src/main/kotlin/org/modelix/model/mpsadapters/GlobalModelListener.kt +++ b/mps-model-adapters/src/main/kotlin/org/modelix/model/mpsadapters/GlobalModelListener.kt @@ -1,3 +1,5 @@ +@file:Suppress("removal") + /* * Copyright (c) 2023. * @@ -13,21 +15,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.modelix.model.mpsadapters -import org.jetbrains.mps.openapi.language.SLanguage import org.jetbrains.mps.openapi.model.SModel -import org.jetbrains.mps.openapi.model.SModelReference -import org.jetbrains.mps.openapi.module.SDependency 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.SModuleListenerBase import org.jetbrains.mps.openapi.module.SRepository import org.jetbrains.mps.openapi.module.SRepositoryListener +import org.jetbrains.mps.openapi.module.SRepositoryListenerBase abstract class GlobalModelListener { - @Suppress("removal") - protected var repositoryListener: SRepositoryListener = object : SRepositoryListener { + protected var repositoryListener: SRepositoryListener = object : SRepositoryListenerBase() { override fun moduleAdded(m: SModule) { start(m) } @@ -35,18 +35,9 @@ abstract class GlobalModelListener { override fun beforeModuleRemoved(m: SModule) { stop(m) } - - override fun moduleRemoved(p0: SModuleReference) {} - override fun commandStarted(p0: SRepository?) {} - override fun commandFinished(p0: SRepository?) {} - override fun updateStarted(p0: SRepository?) {} - override fun updateFinished(p0: SRepository?) {} - override fun repositoryCommandStarted(p0: SRepository?) {} - override fun repositoryCommandFinished(p0: SRepository?) {} } - @Suppress("removal") - protected var moduleListener: SModuleListener = object : SModuleListener { + protected var moduleListener: SModuleListener = object : SModuleListenerBase() { override fun modelAdded(module: SModule, model: SModel) { start(model) } @@ -54,15 +45,6 @@ abstract class GlobalModelListener { override fun beforeModelRemoved(module: SModule, model: SModel) { stop(model) } - - override fun modelRemoved(p0: SModule?, p1: SModelReference?) {} - override fun beforeModelRenamed(p0: SModule?, p1: SModel?, p2: SModelReference?) {} - override fun modelRenamed(p0: SModule?, p1: SModel?, p2: SModelReference?) {} - override fun dependencyAdded(p0: SModule?, p1: SDependency?) {} - override fun dependencyRemoved(p0: SModule?, p1: SDependency?) {} - override fun languageAdded(p0: SModule?, p1: SLanguage?) {} - override fun languageRemoved(p0: SModule?, p1: SLanguage?) {} - override fun moduleChanged(p0: SModule?) {} } protected var myRepositories: MutableSet = HashSet() protected var myModules: MutableSet = HashSet() diff --git a/mps-model-adapters/src/main/kotlin/org/modelix/model/mpsadapters/MPSChangeTranslator.kt b/mps-model-adapters/src/main/kotlin/org/modelix/model/mpsadapters/MPSChangeTranslator.kt index 3cf644cf93..eaccd6f424 100644 --- a/mps-model-adapters/src/main/kotlin/org/modelix/model/mpsadapters/MPSChangeTranslator.kt +++ b/mps-model-adapters/src/main/kotlin/org/modelix/model/mpsadapters/MPSChangeTranslator.kt @@ -1,3 +1,5 @@ +@file:Suppress("removal") + package org.modelix.model.mpsadapters import jetbrains.mps.smodel.SModelInternal @@ -29,6 +31,7 @@ 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.incremental.DependencyTracking import org.modelix.incremental.IStateVariableReference @@ -36,22 +39,10 @@ class MPSChangeTranslator : GlobalModelListener(), SNodeChangeListener, SModuleListener, - SRepositoryListener, + SRepositoryListener by object : SRepositoryListenerBase() {}, SModelListener, org.jetbrains.mps.openapi.model.SModelListener { - @Suppress("removal") - override fun updateStarted(p0: SRepository?) {} - - @Suppress("removal") - override fun updateFinished(p0: SRepository?) {} - - @Suppress("removal") - override fun repositoryCommandStarted(p0: SRepository?) {} - - @Suppress("removal") - override fun repositoryCommandFinished(p0: SRepository?) {} - private fun notifyChange(change: IStateVariableReference<*>) { DependencyTracking.modified(change) } diff --git a/mps-model-server-plugin/build.gradle.kts b/mps-model-server-plugin/build.gradle.kts index 11b0b73f75..42fe3de97a 100644 --- a/mps-model-server-plugin/build.gradle.kts +++ b/mps-model-server-plugin/build.gradle.kts @@ -12,6 +12,7 @@ val mpsToIdeaMap = mapOf( "2021.3.3" to "213.7172.25", // https://github.com/JetBrains/MPS/blob/2021.3.3/build/version.properties "2022.2" to "222.4554.10", // https://github.com/JetBrains/MPS/blob/2021.2.1/build/version.properties "2022.3" to "223.8836.41", // https://github.com/JetBrains/MPS/blob/2022.3.0/build/version.properties (?) + "2023.2" to "232.10072.27", // https://github.com/JetBrains/MPS/blob/2023.2.0/build/version.properties ) // use the given MPS version, or 2022.2 (last version with JAVA 11) as default val mpsVersion = project.findProperty("mps.version")?.toString().takeIf { !it.isNullOrBlank() } ?: "2020.3.6" @@ -26,10 +27,14 @@ println("Building for MPS version $mpsVersion and IntelliJ version $ideaVersion dependencies { implementation(project(":model-server-lib")) implementation(project(":mps-model-adapters")) - compileOnly("com.jetbrains:mps-openapi:$mpsVersion") - compileOnly("com.jetbrains:mps-core:$mpsVersion") - compileOnly("com.jetbrains:mps-environment:$mpsVersion") - compileOnly("com.jetbrains:mps-platform:$mpsVersion") + + val mpsZip by configurations.creating + mpsZip("com.jetbrains:mps:$mpsVersion") + compileOnly( + zipTree({ mpsZip.singleFile }).matching { + include("lib/*.jar") + }, + ) } // Configure Gradle IntelliJ Plugin From 8a8c4c65fcb16f56292ad2b6a0cdd14e0f4fbf83 Mon Sep 17 00:00:00 2001 From: Sascha Lisson Date: Mon, 27 Nov 2023 17:54:39 +0100 Subject: [PATCH 09/14] build: removed ktlint from the Gradle build It's already executed by the pre-commit workflow. It's not useful if the other workflows also fail, and you don't know if there is an actual issue in the code or if it's just formatting. --- build.gradle.kts | 7 ------- model-api/build.gradle.kts | 11 ----------- model-server/build.gradle.kts | 16 ---------------- ts-model-api/build.gradle.kts | 1 - 4 files changed, 35 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 83fc7868bb..a0e301e324 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -40,7 +40,6 @@ plugins { alias(libs.plugins.kotlin.multiplatform) apply false alias(libs.plugins.kotlin.serialization) apply false alias(libs.plugins.gitVersion) - alias(libs.plugins.ktlint) apply false alias(libs.plugins.spotless) apply false alias(libs.plugins.tasktree) alias(libs.plugins.dokka) @@ -78,7 +77,6 @@ subprojects { val subproject = this apply(plugin = "maven-publish") apply(plugin = "org.jetbrains.dokka") - apply(plugin = "org.jlleitschuh.gradle.ktlint") apply(plugin = "io.gitlab.arturbosch.detekt") version = rootProject.version @@ -90,11 +88,6 @@ subprojects { } } - configure { - // IMPORTANT: keep in sync with the version in .pre-commit-config.yaml - version.set("0.50.0") - } - tasks.withType { parallel = true // For now, we only use the results here as hints diff --git a/model-api/build.gradle.kts b/model-api/build.gradle.kts index 29c0d3f988..ce1f380e6a 100644 --- a/model-api/build.gradle.kts +++ b/model-api/build.gradle.kts @@ -6,16 +6,6 @@ plugins { description = "API to access models stored in Modelix" -ktlint { - filter { - exclude { - val kotlinGeneratedFromTypeScript = - project(":ts-model-api").layout.buildDirectory.get().asFile.toPath().toAbsolutePath() - it.file.toPath().toAbsolutePath().startsWith(kotlinGeneratedFromTypeScript) - } - } -} - kotlin { jvm() js(IR) { @@ -70,7 +60,6 @@ kotlin { listOf( "sourcesJar", - "runKtlintCheckOverJsMainSourceSet", "jsSourcesJar", "jsPackageJson", "compileKotlinJs", diff --git a/model-server/build.gradle.kts b/model-server/build.gradle.kts index f614704c2c..7721677ee2 100644 --- a/model-server/build.gradle.kts +++ b/model-server/build.gradle.kts @@ -130,10 +130,6 @@ tasks.register("copyApis") { sourceSets["main"].resources.srcDir(project.layout.buildDirectory.dir("openapi/src/main/resources/")) } -tasks.named("runKtlintCheckOverMainSourceSet") { - dependsOn("copyApis") -} - tasks.named("compileKotlin") { dependsOn("copyApis") } @@ -282,18 +278,6 @@ openApiFiles.forEach { tasks.named("compileKotlin") { dependsOn(targetTaskName) } - tasks.named("runKtlintCheckOverMainSourceSet") { - dependsOn(targetTaskName) - } - - // do not apply ktlint on the generated files - ktlint { - filter { - exclude { - it.file.toPath().toAbsolutePath().startsWith(outputPath) - } - } - } // add openAPI generated artifacts to the sourceSets sourceSets["main"].kotlin.srcDir("$outputPath/src/main/kotlin") diff --git a/ts-model-api/build.gradle.kts b/ts-model-api/build.gradle.kts index a01fad2127..75347d1358 100644 --- a/ts-model-api/build.gradle.kts +++ b/ts-model-api/build.gradle.kts @@ -1,7 +1,6 @@ plugins { base alias(libs.plugins.node) - alias(libs.plugins.ktlint) apply false alias(libs.plugins.npm.publish) } From fa10531195dd9f9e80c2809ad8f65671a7e5b159 Mon Sep 17 00:00:00 2001 From: Sascha Lisson Date: Mon, 11 Dec 2023 07:45:36 +0100 Subject: [PATCH 10/14] feat(model-api): new method IConcept.getConceptProperty to read the alias The only way to get access to the MPS concept alias was the generated API, but when using the untyped API together with the mps-model-adapters this information was available, but not accessible. --- .../generator/internal/ConceptWrapperInterfaceGenerator.kt | 1 + .../commonMain/kotlin/org/modelix/model/api/IConcept.kt | 7 +++++++ .../kotlin/org/modelix/model/mpsadapters/MPSConcept.kt | 7 +++++++ 3 files changed, 15 insertions(+) diff --git a/model-api-gen/src/main/kotlin/org/modelix/metamodel/generator/internal/ConceptWrapperInterfaceGenerator.kt b/model-api-gen/src/main/kotlin/org/modelix/metamodel/generator/internal/ConceptWrapperInterfaceGenerator.kt index f9066016b3..a2529797bc 100644 --- a/model-api-gen/src/main/kotlin/org/modelix/metamodel/generator/internal/ConceptWrapperInterfaceGenerator.kt +++ b/model-api-gen/src/main/kotlin/org/modelix/metamodel/generator/internal/ConceptWrapperInterfaceGenerator.kt @@ -91,6 +91,7 @@ internal class ConceptWrapperInterfaceGenerator( private fun TypeSpec.Builder.addConceptMetaPropertiesIfNecessary() { if (conceptPropertiesInterfaceName == null) return + // TODO use IConcept.getConceptProperty(name: String) concept.metaProperties.forEach { (key, value) -> val propertySpec = PropertySpec.builder(key, String::class.asTypeName()).runBuild { addModifiers(KModifier.OVERRIDE) diff --git a/model-api/src/commonMain/kotlin/org/modelix/model/api/IConcept.kt b/model-api/src/commonMain/kotlin/org/modelix/model/api/IConcept.kt index 42cc51837d..2c3ff5d017 100644 --- a/model-api/src/commonMain/kotlin/org/modelix/model/api/IConcept.kt +++ b/model-api/src/commonMain/kotlin/org/modelix/model/api/IConcept.kt @@ -168,9 +168,16 @@ interface IConcept { * @return reference link */ fun getReferenceLink(name: String): IReferenceLink + + /** + * The alias of an MPS concept is one example of a concept property. + */ + fun getConceptProperty(name: String): String? = null } /** * @see IConcept.isSubConceptOf */ fun IConcept?.isSubConceptOf(superConcept: IConcept?) = this?.isSubConceptOf(superConcept) == true + +fun IConcept.conceptAlias() = getConceptProperty("alias") 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 78a4fea1e7..d51c374c4a 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 @@ -111,4 +111,11 @@ data class MPSConcept(val concept: SAbstractConceptAdapter) : IConcept { override fun getReferenceLink(name: String): IReferenceLink { return MPSReferenceLink(concept.referenceLinks.first { it.name == name }) } + + override fun getConceptProperty(name: String): String? { + return when (name) { + "alias" -> concept.conceptAlias + else -> null + } + } } From aab71500da156bc46899924511e6239cf72bfedf Mon Sep 17 00:00:00 2001 From: Sascha Lisson Date: Sun, 17 Dec 2023 12:26:22 +0100 Subject: [PATCH 11/14] fix(mps-model-adapters): publish sources jar --- mps-model-adapters/build.gradle.kts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/mps-model-adapters/build.gradle.kts b/mps-model-adapters/build.gradle.kts index d0eef73e77..0c8ad481e0 100644 --- a/mps-model-adapters/build.gradle.kts +++ b/mps-model-adapters/build.gradle.kts @@ -24,11 +24,15 @@ dependencies { group = "org.modelix.mps" +java { + withSourcesJar() +} + publishing { publications { create("maven") { artifactId = "model-adapters" - from(components["kotlin"]) + from(components["java"]) } } } From 043aaf8619b00de5d64049320604bb6766dfddfa Mon Sep 17 00:00:00 2001 From: Sascha Lisson Date: Sun, 17 Dec 2023 17:52:26 +0100 Subject: [PATCH 12/14] feat(model-api): introduced ITransactionManager --- .../modelix/model/api/ITransactionManager.kt | 24 +++++++++++++++++++ .../kotlin/org/modelix/model/area/IArea.kt | 11 +++++---- 2 files changed, 30 insertions(+), 5 deletions(-) create mode 100644 model-api/src/commonMain/kotlin/org/modelix/model/api/ITransactionManager.kt diff --git a/model-api/src/commonMain/kotlin/org/modelix/model/api/ITransactionManager.kt b/model-api/src/commonMain/kotlin/org/modelix/model/api/ITransactionManager.kt new file mode 100644 index 0000000000..480aaad2c8 --- /dev/null +++ b/model-api/src/commonMain/kotlin/org/modelix/model/api/ITransactionManager.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2023. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.modelix.model.api + +interface ITransactionManager { + fun executeRead(body: () -> R): R + fun executeWrite(body: () -> R): R + fun canRead(): Boolean + fun canWrite(): Boolean +} diff --git a/model-api/src/commonMain/kotlin/org/modelix/model/area/IArea.kt b/model-api/src/commonMain/kotlin/org/modelix/model/area/IArea.kt index f9097b52a9..63f188bdb8 100644 --- a/model-api/src/commonMain/kotlin/org/modelix/model/area/IArea.kt +++ b/model-api/src/commonMain/kotlin/org/modelix/model/area/IArea.kt @@ -19,6 +19,7 @@ import org.modelix.model.api.IConceptReference import org.modelix.model.api.INode import org.modelix.model.api.INodeReference import org.modelix.model.api.INodeResolutionScope +import org.modelix.model.api.ITransactionManager /** * An IArea is similar to an IBranch. They both provide transactional access to nodes, but the IBranch can only be used @@ -27,7 +28,7 @@ import org.modelix.model.api.INodeResolutionScope * It's like a unix filesystem with mount points. The model inside an area can also be an MPS model that is not a * persistent data structure. */ -interface IArea : INodeResolutionScope { +interface IArea : INodeResolutionScope, ITransactionManager { /** * The root of an area is not allowed to change */ @@ -53,10 +54,10 @@ interface IArea : INodeResolutionScope { fun getReference(): IAreaReference fun resolveArea(ref: IAreaReference): IArea? - fun executeRead(f: () -> T): T - fun executeWrite(f: () -> T): T - fun canRead(): Boolean - fun canWrite(): Boolean + override fun executeRead(body: () -> T): T + override fun executeWrite(body: () -> T): T + override fun canRead(): Boolean + override fun canWrite(): Boolean /** bigger numbers are locked first */ fun getLockOrderingPriority(): Long = 0 From e6e206847c796a048bdd5da0245c85401209b23d Mon Sep 17 00:00:00 2001 From: Sascha Lisson Date: Fri, 8 Mar 2024 10:33:25 +0100 Subject: [PATCH 13/14] fix(model-api-gen): generate ITypedChildLink types instead of GeneratedChildLink GeneratedChildLink implements ITypedChildLink and IChildLink which can result in conflicting overloads when there are extension methods for both interfaces. For example `C_ClassConcept.members` was of type GeneratedChildListLink and is now of type ITypedChildLinkList. The same change is done for reference links and properties. BREAKING CHANGE: The generated links/properties are not instances of their untyped interfaces (IProperty/IChildLink/IReferenceLink) anymore. New compile errors may appear in existing code which can be fixed by appending `.untyped()`. --- .../model/sync/bulk/gradle/test/PushTest.kt | 2 +- .../ConceptWrapperInterfaceGenerator.kt | 9 ++---- .../internal/NameConfigBasedGenerator.kt | 30 +++++++++++++++++++ 3 files changed, 34 insertions(+), 7 deletions(-) diff --git a/bulk-model-sync-gradle-test/src/test/kotlin/org/modelix/model/sync/bulk/gradle/test/PushTest.kt b/bulk-model-sync-gradle-test/src/test/kotlin/org/modelix/model/sync/bulk/gradle/test/PushTest.kt index ea9063ba64..1114dd1a6b 100644 --- a/bulk-model-sync-gradle-test/src/test/kotlin/org/modelix/model/sync/bulk/gradle/test/PushTest.kt +++ b/bulk-model-sync-gradle-test/src/test/kotlin/org/modelix/model/sync/bulk/gradle/test/PushTest.kt @@ -70,6 +70,6 @@ class PushTest { ?.getDescendants(false) ?.find { it.getConceptReference() == ConceptReference(_C_UntypedImpl_Graph.getUID()) }, ) - assertEquals(solution1Graph, solution2Graph.getReferenceTarget(C_Graph.relatedGraph)) + assertEquals(solution1Graph, solution2Graph.getReferenceTarget(C_Graph.relatedGraph.untyped())) } } diff --git a/model-api-gen/src/main/kotlin/org/modelix/metamodel/generator/internal/ConceptWrapperInterfaceGenerator.kt b/model-api-gen/src/main/kotlin/org/modelix/metamodel/generator/internal/ConceptWrapperInterfaceGenerator.kt index a2529797bc..5e4f0fc5e2 100644 --- a/model-api-gen/src/main/kotlin/org/modelix/metamodel/generator/internal/ConceptWrapperInterfaceGenerator.kt +++ b/model-api-gen/src/main/kotlin/org/modelix/metamodel/generator/internal/ConceptWrapperInterfaceGenerator.kt @@ -23,9 +23,7 @@ import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy import com.squareup.kotlinpoet.PropertySpec import com.squareup.kotlinpoet.TypeSpec import com.squareup.kotlinpoet.TypeVariableName -import com.squareup.kotlinpoet.asClassName import com.squareup.kotlinpoet.asTypeName -import org.modelix.metamodel.GeneratedProperty import org.modelix.metamodel.IConceptOfTypedNode import org.modelix.metamodel.INonAbstractConcept import org.modelix.metamodel.ITypedConcept @@ -103,7 +101,7 @@ internal class ConceptWrapperInterfaceGenerator( } private fun TypeSpec.Builder.addConceptWrapperInterfaceReferenceLink(referenceLink: ProcessedReferenceLink) { - val propertySpec = PropertySpec.builder(referenceLink.generatedName, referenceLink.generatedReferenceLinkType()).runBuild { + val propertySpec = PropertySpec.builder(referenceLink.generatedName, referenceLink.typedLinkType()).runBuild { getter(FunSpec.getterBuilder().addCode(referenceLink.returnKotlinRef()).build()) addDeprecationIfNecessary(referenceLink) } @@ -112,7 +110,7 @@ internal class ConceptWrapperInterfaceGenerator( } private fun TypeSpec.Builder.addConceptWrapperInterfaceChildLink(childLink: ProcessedChildLink) { - val propertySpec = PropertySpec.builder(childLink.generatedName, childLink.generatedChildLinkType()).runBuild { + val propertySpec = PropertySpec.builder(childLink.generatedName, childLink.typedLinkType()).runBuild { getter(FunSpec.getterBuilder().addCode(childLink.returnKotlinRef()).build()) addDeprecationIfNecessary(childLink) } @@ -123,8 +121,7 @@ internal class ConceptWrapperInterfaceGenerator( private fun TypeSpec.Builder.addConceptWrapperInterfaceProperty(property: ProcessedProperty) { val propertySpec = PropertySpec.builder( name = property.generatedName, - type = GeneratedProperty::class.asClassName() - .parameterizedBy(property.asKotlinType(alwaysUseNonNullableProperties)), + type = property.typedPropertyType(alwaysUseNonNullableProperties), ).runBuild { val getterSpec = FunSpec.getterBuilder().runBuild { addCode(property.returnKotlinRef()) diff --git a/model-api-gen/src/main/kotlin/org/modelix/metamodel/generator/internal/NameConfigBasedGenerator.kt b/model-api-gen/src/main/kotlin/org/modelix/metamodel/generator/internal/NameConfigBasedGenerator.kt index 1b556928d7..7d83f1a859 100644 --- a/model-api-gen/src/main/kotlin/org/modelix/metamodel/generator/internal/NameConfigBasedGenerator.kt +++ b/model-api-gen/src/main/kotlin/org/modelix/metamodel/generator/internal/NameConfigBasedGenerator.kt @@ -26,6 +26,11 @@ import org.modelix.metamodel.GeneratedChildListLink import org.modelix.metamodel.GeneratedMandatorySingleChildLink import org.modelix.metamodel.GeneratedReferenceLink import org.modelix.metamodel.GeneratedSingleChildLink +import org.modelix.metamodel.ITypedChildListLink +import org.modelix.metamodel.ITypedMandatorySingleChildLink +import org.modelix.metamodel.ITypedProperty +import org.modelix.metamodel.ITypedReferenceLink +import org.modelix.metamodel.ITypedSingleChildLink import org.modelix.metamodel.generator.NameConfig import org.modelix.metamodel.generator.ProcessedChildLink import org.modelix.metamodel.generator.ProcessedConcept @@ -77,6 +82,18 @@ internal abstract class NameConfigBasedGenerator(open val nameConfig: NameConfig ) } + protected fun ProcessedChildLink.typedLinkType(): TypeName { + val childConcept = type.resolved + val linkClass = if (multiple) { + ITypedChildListLink::class + } else { + if (optional) ITypedSingleChildLink::class else ITypedMandatorySingleChildLink::class + } + return linkClass.asClassName().parameterizedBy( + childConcept.nodeWrapperInterfaceType(), + ) + } + protected fun ProcessedReferenceLink.generatedReferenceLinkType(): TypeName { val targetConcept = type.resolved return GeneratedReferenceLink::class.asClassName().parameterizedBy( @@ -85,6 +102,19 @@ internal abstract class NameConfigBasedGenerator(open val nameConfig: NameConfig ) } + protected fun ProcessedReferenceLink.typedLinkType(): TypeName { + val targetConcept = type.resolved + return ITypedReferenceLink::class.asClassName().parameterizedBy( + targetConcept.nodeWrapperInterfaceType(), + ) + } + + protected fun ProcessedProperty.typedPropertyType(alwaysUseNonNullableProperties: Boolean): TypeName { + return ITypedProperty::class.asClassName().parameterizedBy( + asKotlinType(alwaysUseNonNullableProperties), + ) + } + protected fun ProcessedProperty.asKotlinType(alwaysUseNonNullableProperties: Boolean): TypeName { val nonNullableType = when (type) { is PrimitivePropertyType -> when ((type as PrimitivePropertyType).primitive) { From bb517af2b5ea0438c4863595b5513bebeeb60609 Mon Sep 17 00:00:00 2001 From: Sascha Lisson Date: Mon, 11 Mar 2024 20:48:52 +0100 Subject: [PATCH 14/14] test(bulk-model-sync-gradle): use correct JDK version for graph-lang-api bulk-model-sync-gradle-test specifies JDK11 so the graph-lang-api also has to be compatible to JDK11. --- bulk-model-sync-gradle-test/graph-lang-api/build.gradle.kts | 1 + 1 file changed, 1 insertion(+) diff --git a/bulk-model-sync-gradle-test/graph-lang-api/build.gradle.kts b/bulk-model-sync-gradle-test/graph-lang-api/build.gradle.kts index 120e9622a8..310d746e02 100644 --- a/bulk-model-sync-gradle-test/graph-lang-api/build.gradle.kts +++ b/bulk-model-sync-gradle-test/graph-lang-api/build.gradle.kts @@ -36,6 +36,7 @@ kotlin { sourceSets.named("main") { kotlin.srcDir(kotlinGenDir) } + jvmToolchain(11) } metamodel {