-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(mps-sync-plugin): MPS sync plugin inital commit
- Loading branch information
Showing
63 changed files
with
6,309 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
plugins { | ||
kotlin("jvm") | ||
kotlin("plugin.serialization") | ||
} | ||
|
||
repositories { | ||
maven { url = uri("https://www.jetbrains.com/intellij-repository/releases") } | ||
} | ||
|
||
kotlin { | ||
compilerOptions { | ||
apiVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_6) | ||
} | ||
} | ||
|
||
// 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" | ||
|
||
val mpsZip by configurations.creating | ||
|
||
dependencies { | ||
implementation(kotlin("stdlib-jdk8")) | ||
implementation(libs.kotlin.logging.microutils) | ||
|
||
implementation(libs.modelix.model.api) | ||
implementation(libs.modelix.model.client) | ||
implementation(libs.modelix.mps.model.adapters) | ||
|
||
// extracting jars from zipped products | ||
mpsZip("com.jetbrains:mps:$mpsVersion") | ||
compileOnly( | ||
zipTree({ mpsZip.singleFile }).matching { | ||
include("lib/**/*.jar") | ||
}, | ||
) | ||
} | ||
|
||
group = "org.modelix.mps" | ||
description = "Generic helper library to sync model-server content with MPS" | ||
|
||
publishing { | ||
publications { | ||
create<MavenPublication>("maven") { | ||
artifactId = "sync-plugin-lib" | ||
from(components["kotlin"]) | ||
} | ||
} | ||
} |
38 changes: 38 additions & 0 deletions
38
mps-sync-plugin-lib/src/main/kotlin/org/modelix/mps/sync/SyncService.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
package org.modelix.mps.sync | ||
|
||
import com.intellij.openapi.project.Project | ||
import org.modelix.kotlin.utils.UnstableModelixFeature | ||
import org.modelix.model.api.INode | ||
import org.modelix.model.client2.ModelClientV2 | ||
import org.modelix.model.lazy.BranchReference | ||
import java.net.URL | ||
import java.util.concurrent.CompletableFuture | ||
|
||
@UnstableModelixFeature(reason = "The new modelix MPS plugin is under construction", intendedFinalization = "2024.1") | ||
interface SyncService { | ||
|
||
suspend fun bindModule( | ||
client: ModelClientV2, | ||
branchReference: BranchReference, | ||
module: INode, | ||
callback: (() -> Unit)? = null, | ||
): Iterable<IBinding> | ||
|
||
suspend fun connectModelServer(serverURL: URL, jwt: String, callback: (() -> Unit)? = null): ModelClientV2 | ||
|
||
fun disconnectModelServer(client: ModelClientV2, callback: (() -> Unit)? = null) | ||
|
||
fun setActiveProject(project: Project) | ||
|
||
fun dispose() | ||
} | ||
|
||
@UnstableModelixFeature(reason = "The new modelix MPS plugin is under construction", intendedFinalization = "2024.1") | ||
interface IBinding { | ||
|
||
fun activate(callback: Runnable? = null) | ||
|
||
fun deactivate(removeFromServer: Boolean, callback: Runnable? = null): CompletableFuture<Any?> | ||
|
||
fun name(): String | ||
} |
181 changes: 181 additions & 0 deletions
181
mps-sync-plugin-lib/src/main/kotlin/org/modelix/mps/sync/SyncServiceImpl.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,181 @@ | ||
package org.modelix.mps.sync | ||
|
||
import com.intellij.openapi.project.Project | ||
import jetbrains.mps.project.MPSProject | ||
import kotlinx.coroutines.CoroutineScope | ||
import kotlinx.coroutines.Dispatchers | ||
import kotlinx.coroutines.cancel | ||
import mu.KotlinLogging | ||
import org.modelix.kotlin.utils.UnstableModelixFeature | ||
import org.modelix.model.api.IBranchListener | ||
import org.modelix.model.api.ILanguageRepository | ||
import org.modelix.model.api.INode | ||
import org.modelix.model.client2.ModelClientV2 | ||
import org.modelix.model.client2.ReplicatedModel | ||
import org.modelix.model.client2.getReplicatedModel | ||
import org.modelix.model.lazy.BranchReference | ||
import org.modelix.model.mpsadapters.MPSLanguageRepository | ||
import org.modelix.mps.sync.bindings.BindingsRegistry | ||
import org.modelix.mps.sync.modelix.ModelixBranchListener | ||
import org.modelix.mps.sync.modelix.ReplicatedModelRegistry | ||
import org.modelix.mps.sync.mps.ActiveMpsProjectInjector | ||
import org.modelix.mps.sync.mps.RepositoryChangeListener | ||
import org.modelix.mps.sync.tasks.FuturesWaitQueue | ||
import org.modelix.mps.sync.tasks.SyncQueue | ||
import org.modelix.mps.sync.transformation.modelixToMps.initial.ITreeToSTreeTransformer | ||
import java.net.ConnectException | ||
import java.net.URL | ||
|
||
@UnstableModelixFeature(reason = "The new modelix MPS plugin is under construction", intendedFinalization = "2024.1") | ||
class SyncServiceImpl : SyncService { | ||
|
||
private val logger = KotlinLogging.logger {} | ||
private val mpsProjectInjector = ActiveMpsProjectInjector | ||
|
||
private val coroutineScope = CoroutineScope(Dispatchers.Default) | ||
val activeClients = mutableSetOf<ModelClientV2>() | ||
private val replicatedModelByBranchReference = mutableMapOf<BranchReference, ReplicatedModel>() | ||
private val changeListenerByReplicatedModel = mutableMapOf<ReplicatedModel, IBranchListener>() | ||
|
||
private var projectWithChangeListener: Pair<MPSProject, RepositoryChangeListener>? = null | ||
|
||
init { | ||
logger.info { "============================================ Registering builtin languages" } | ||
// just a dummy call, the initializer of ILanguageRegistry takes care of the rest... | ||
ILanguageRepository.default.javaClass | ||
} | ||
|
||
// todo add afterActivate to allow async refresh | ||
override suspend fun connectModelServer( | ||
serverURL: URL, | ||
jwt: String, | ||
callback: (() -> Unit)?, | ||
): ModelClientV2 { | ||
// avoid reconnect to existing server | ||
val client = activeClients.find { it.baseUrl == serverURL.toString() } | ||
client?.let { | ||
logger.info { "Using already existing connection to $serverURL" } | ||
return it | ||
} | ||
|
||
// TODO: use JWT here | ||
val modelClientV2: ModelClientV2 = ModelClientV2.builder().url(serverURL.toString()).build() | ||
try { | ||
logger.info { "Connecting to $serverURL" } | ||
modelClientV2.init() | ||
} catch (e: ConnectException) { | ||
logger.warn { "Unable to connect: ${e.message} / ${e.cause}" } | ||
throw e | ||
} | ||
|
||
logger.info { "Connection to $serverURL successful" } | ||
activeClients.add(modelClientV2) | ||
|
||
callback?.invoke() | ||
|
||
return modelClientV2 | ||
} | ||
|
||
override fun disconnectModelServer( | ||
client: ModelClientV2, | ||
callback: (() -> Unit)?, | ||
) { | ||
// TODO what shall happen with the bindings if we disconnect from model server? | ||
activeClients.remove(client) | ||
client.close() | ||
callback?.invoke() | ||
} | ||
|
||
override suspend fun bindModule( | ||
client: ModelClientV2, | ||
branchReference: BranchReference, | ||
module: INode, | ||
callback: (() -> Unit)?, | ||
): Iterable<IBinding> { | ||
// fetch replicated model and branch content | ||
// TODO how to handle multiple replicated models at the same time? | ||
val replicatedModel = | ||
replicatedModelByBranchReference.getOrDefault(branchReference, client.getReplicatedModel(branchReference)) | ||
val replicateModelIsAlreadySynched = replicatedModelByBranchReference.containsKey(branchReference) | ||
|
||
/* | ||
* TODO fixme: | ||
* (1) How to propagate replicated model to other places of code? | ||
* (2) How to know to which replicated model we want to upload? (E.g. when connecting to multiple model servers?) | ||
* (3) How to replace the outdated replicated models that are already used from the registry? | ||
* | ||
* Possible answers: | ||
* (1) via the registry | ||
* (2) Base the selection on the parent project and the active model server connections we have. E.g. let the user select to which model server they want to upload the changes and so they get the corresponding replicated model. | ||
* (3) We don't. We have to make sure that the places always have the latest replicated models from the registry. E.g. if we disconnect from the model server then we remove the replicated model (and thus break the registered event handlers), otherwise the event handlers as for the replicated model from the registry (based on some identifying metainfo for example, so to know which replicated model they need). | ||
*/ | ||
ReplicatedModelRegistry.model = replicatedModel | ||
replicatedModelByBranchReference[branchReference] = replicatedModel | ||
|
||
// TODO when and how to dispose the replicated model and everything that depends on it? | ||
val branch = if (replicateModelIsAlreadySynched) { | ||
replicatedModel.getBranch() | ||
} else { | ||
replicatedModel.start() | ||
} | ||
|
||
// transform the model | ||
val targetProject = mpsProjectInjector.activeMpsProject!! | ||
val languageRepository = registerLanguages(targetProject) | ||
val bindings = ITreeToSTreeTransformer(branch, languageRepository).transform(module) | ||
|
||
// register replicated model change listener | ||
if (!replicateModelIsAlreadySynched) { | ||
val listener = ModelixBranchListener(replicatedModel, languageRepository, branch) | ||
branch.addListener(listener) | ||
changeListenerByReplicatedModel[replicatedModel] = listener | ||
} | ||
|
||
// register MPS project change listener | ||
if (projectWithChangeListener == null) { | ||
val repositoryChangeListener = RepositoryChangeListener(branch) | ||
targetProject.repository.addRepositoryListener(repositoryChangeListener) | ||
projectWithChangeListener = Pair(targetProject, repositoryChangeListener) | ||
} | ||
|
||
// trigger callback after activation | ||
callback?.invoke() | ||
|
||
return bindings | ||
} | ||
|
||
override fun setActiveProject(project: Project) { | ||
mpsProjectInjector.setActiveProject(project) | ||
} | ||
|
||
override fun dispose() { | ||
// cancel all running coroutines | ||
coroutineScope.cancel() | ||
SyncQueue.close() | ||
FuturesWaitQueue.close() | ||
// unregister change listeners | ||
resetProjectWithChangeListener() | ||
changeListenerByReplicatedModel.forEach { it.key.getBranch().removeListener(it.value) } | ||
// dispose the clients | ||
activeClients.forEach { it.close() } | ||
// dispose all bindings | ||
BindingsRegistry.getModuleBindings().forEach { it.deactivate(removeFromServer = false) } | ||
BindingsRegistry.getModelBindings().forEach { it.deactivate(removeFromServer = false) } | ||
} | ||
|
||
private fun registerLanguages(project: MPSProject): MPSLanguageRepository { | ||
val repository = project.repository | ||
val mpsLanguageRepo = MPSLanguageRepository(repository) | ||
ILanguageRepository.register(mpsLanguageRepo) | ||
return mpsLanguageRepo | ||
} | ||
|
||
private fun resetProjectWithChangeListener() { | ||
projectWithChangeListener?.let { | ||
val project = it.first | ||
val listener = it.second | ||
project.repository.removeRepositoryListener(listener) | ||
projectWithChangeListener = null | ||
} | ||
} | ||
} |
46 changes: 46 additions & 0 deletions
46
mps-sync-plugin-lib/src/main/kotlin/org/modelix/mps/sync/bindings/BindingSortComparator.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
/* | ||
* Copyright (c) 2024. | ||
* | ||
* 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.mps.sync.bindings | ||
|
||
import org.modelix.kotlin.utils.UnstableModelixFeature | ||
import org.modelix.mps.sync.IBinding | ||
|
||
@UnstableModelixFeature(reason = "The new modelix MPS plugin is under construction", intendedFinalization = "2024.1") | ||
class BindingSortComparator : Comparator<IBinding> { | ||
/** | ||
* ModelBindings should come first, then ModuleBindings. If both bindings have the same type, then they are sorted lexicographically. | ||
*/ | ||
override fun compare(left: IBinding, right: IBinding): Int { | ||
val leftName = left.name() | ||
val rightName = right.name() | ||
|
||
if (left is ModelBinding) { | ||
if (right is ModelBinding) { | ||
return leftName.compareTo(rightName) | ||
} else if (right is ModuleBinding) { | ||
return -1 | ||
} | ||
} else if (left is ModuleBinding) { | ||
if (right is ModelBinding) { | ||
return 1 | ||
} else if (right is ModuleBinding) { | ||
return leftName.compareTo(rightName) | ||
} | ||
} | ||
return 0 | ||
} | ||
} |
Oops, something went wrong.