Skip to content

Commit

Permalink
feat: new diff plugin
Browse files Browse the repository at this point in the history
It was originally implemented in MPS baseLanguage as part of the
modelix.mps repository. It is now implemented it in Kotlin and packaged
as a version independent IDEA plugin.
  • Loading branch information
slisson committed Feb 26, 2024
1 parent ab5e745 commit 32bf52e
Show file tree
Hide file tree
Showing 18 changed files with 1,297 additions and 4 deletions.
6 changes: 3 additions & 3 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ subprojects {
version = rootProject.version
group = rootProject.group

val kotlinApiVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_6
val kotlinApiVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_4
subproject.tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
kotlinOptions {
jvmTarget = "11"
Expand Down Expand Up @@ -123,9 +123,9 @@ allprojects {

val mpsVersion = project.findProperty("mps.version")?.toString()?.takeIf { it.isNotEmpty() }
?: "2021.1.4".also { ext["mps.version"] = it }
val mpsPlatformVersion = mpsVersion.replace(Regex("""20(\d\d)\.(\d+).*"""), "$1$2").toInt()
ext["mps.platform.version"] = mpsPlatformVersion
println("Building for MPS version $mpsVersion")
val mpsJavaVersion = if (mpsVersion >= "2022.2") 17 else 11
ext["mps.java.version"] = mpsJavaVersion

// Extract MPS during configuration phase, because using it in intellij.localPath requires it to already exist.
val mpsHome = project.layout.buildDirectory.dir("mps-$mpsVersion")
Expand Down
7 changes: 7 additions & 0 deletions check-binary-compatibility.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,10 @@ cp "mps-legacy-sync-plugin/build/libs/mps-legacy-sync-plugin-$VERSION.jar" "buil
./gradlew :mps-legacy-sync-plugin:clean :mps-legacy-sync-plugin:jar -Pmps.version=2023.2
cp "mps-legacy-sync-plugin/build/libs/mps-legacy-sync-plugin-$VERSION.jar" "build/binary-compatibility/mps-legacy-sync-plugin/b.jar"
./gradlew :mps-legacy-sync-plugin:checkBinaryCompatibility

#mkdir -p build/binary-compatibility/mps-diff-plugin
#./gradlew :mps-diff-plugin:clean :mps-diff-plugin:jar -Pmps.version=2021.1.4
#cp "mps-diff-plugin/build/libs/mps-diff-plugin-$VERSION.jar" "build/binary-compatibility/mps-diff-plugin/a.jar"
#./gradlew :mps-diff-plugin:clean :mps-diff-plugin:jar -Pmps.version=2023.2
#cp "mps-diff-plugin/build/libs/mps-diff-plugin-$VERSION.jar" "build/binary-compatibility/mps-diff-plugin/b.jar"
#./gradlew :mps-diff-plugin:checkBinaryCompatibility
1 change: 1 addition & 0 deletions commitlint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ module.exports = {
[
"deps",
"mps-legacy-sync-plugin",
"mps-diff-plugin",
],
],
"subject-case": [0, 'never'],
Expand Down
1 change: 1 addition & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
org.gradle.jvmargs=-Xmx1024m "-XX:MaxMetaspaceSize=2g"
systemProp.org.gradle.internal.http.socketTimeout=120000
kotlin.daemon.jvmargs=-Xmx2G
kotlin.stdlib.default.dependency = false
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ modelix-model-datastructure = { group = "org.modelix", name = "model-datastructu
modelix-mps-model-adapters = { group = "org.modelix.mps", name = "model-adapters", version.ref = "modelixCore" }
kotlin-logging = { group = "io.github.oshai", name = "kotlin-logging", version = "6.0.3" }
kotlin-html = "org.jetbrains.kotlinx:kotlinx-html:0.11.0"
zt-zip = { group = "org.zeroturnaround", name = "zt-zip", version = "1.14" }
189 changes: 189 additions & 0 deletions mps-diff-plugin/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import org.jetbrains.intellij.tasks.PrepareSandboxTask
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
import java.util.zip.ZipInputStream

buildscript {
dependencies {
classpath("org.modelix.mps:build-tools-lib:1.3.0")
}
}

plugins {
id("org.jetbrains.kotlin.jvm")
id("org.jetbrains.intellij") version "1.17.2"
}

group = "org.modelix.mps"
val mpsVersion = project.findProperty("mps.version").toString()
val mpsPlatformVersion = project.findProperty("mps.platform.version").toString().toInt()
val mpsHome = rootProject.layout.buildDirectory.dir("mps-$mpsVersion")

java {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
toolchain {
languageVersion.set(JavaLanguageVersion.of(11))
}
withSourcesJar()
}

tasks.compileJava {
options.release = 11
}

tasks.compileKotlin {
kotlinOptions {
jvmTarget = "11"
freeCompilerArgs += listOf("-Xjvm-default=all-compatibility")
apiVersion = "1.6"
}
}

kotlin {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_11)
}
sourceSets {
main {
languageSettings {
apiVersion = KotlinVersion.KOTLIN_1_4.version
}
}
}
}

dependencies {
fun ModuleDependency.excludedBundledLibraries() {
exclude(group = "org.jetbrains.kotlin")
exclude(group = "org.jetbrains.kotlinx", module = "kotlinx-coroutines-core")
exclude(group = "org.jetbrains.kotlinx", module = "kotlinx-coroutines-jdk8")
}
fun implementationWithoutBundled(dependencyNotation: String) {
implementation(dependencyNotation) {
excludedBundledLibraries()
}
}

// implementation(coreLibs.ktor.server.html.builder)
implementationWithoutBundled("io.ktor:ktor-server-html-builder:2.3.7")
implementationWithoutBundled("io.ktor:ktor-server-netty:2.3.7")
implementationWithoutBundled("io.ktor:ktor-server-cors:2.3.7")
implementationWithoutBundled("io.ktor:ktor-server-status-pages:2.3.7")
implementationWithoutBundled("io.github.microutils:kotlin-logging:3.0.5")
implementationWithoutBundled("org.jetbrains.kotlinx:kotlinx-coroutines-swing:1.7.3")

// contains RootDifferencePaneBase
// compileOnly(mpsHome.map { it.files("plugins/mps-vcs/lib/vcs-platform.jar") })

testImplementation(coreLibs.kotlin.coroutines.test)
testImplementation(coreLibs.ktor.server.test.host)
testImplementation(coreLibs.ktor.client.cio)
testImplementation(libs.zt.zip)
// testImplementation(libs.modelix.model.server)
// testImplementation(libs.modelix.authorization)
// testImplementation(coreLibs.kotlin.reflect)
// implementation(libs.ktor.server.core)
// implementation(libs.ktor.server.cors)
// implementation(libs.ktor.server.netty)
// implementation(libs.ktor.server.html.builder)
// implementation(libs.ktor.server.auth)
// implementation(libs.ktor.server.auth.jwt)
// implementation(libs.ktor.server.status.pages)
// implementation(libs.ktor.server.forwarded.header)
// testImplementation(coreLibs.ktor.server.websockets)
// testImplementation(coreLibs.ktor.server.content.negotiation)
// testImplementation(coreLibs.ktor.server.resources)
// implementation(libs.ktor.serialization.json)
}

// Configure Gradle IntelliJ Plugin
// Read more: https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html
intellij {
localPath = mpsHome.map { it.asFile.absolutePath }
instrumentCode = false
plugins = listOf(
"Git4Idea",
"jetbrains.mps.vcs",
)
}

tasks {
patchPluginXml {
sinceBuild.set("211") // 203 not supported, because VersionFixer was replaced by ModuleDependencyVersions in 211
untilBuild.set("232.10072.781")
}

test {
// tests currently fail for these versions
enabled = !setOf(
211, // jetbrains.mps.vcs plugin cannot be loaded
212, // timeout because of some deadlock
213, // timeout because of some deadlock
222, // timeout because of some deadlock
).contains(mpsPlatformVersion)
}

buildSearchableOptions {
enabled = false
}

runIde {
autoReloadPlugins.set(true)
}

val shortPlatformVersion = mpsVersion.replace(Regex("""20(\d\d)\.(\d+).*"""), "$1$2")
val mpsPluginDir = project.findProperty("mps$shortPlatformVersion.plugins.dir")?.toString()?.let { file(it) }
if (mpsPluginDir != null && mpsPluginDir.isDirectory) {
create<Sync>("installMpsPlugin") {
dependsOn(prepareSandbox)
from(buildDir.resolve("idea-sandbox/plugins/mps-diff-plugin"))
into(mpsPluginDir.resolve("mps-diff-plugin"))
}
}

withType<PrepareSandboxTask> {
intoChild(pluginName.map { "$it/languages" })
.from(project.layout.projectDirectory.dir("repositoryconcepts"))
}

val checkBinaryCompatibility by registering {
group = "verification"
doLast {
val ignoredFiles = setOf(
"META-INF/MANIFEST.MF",
)
fun loadEntries(fileName: String) = rootProject.layout.buildDirectory
.dir("binary-compatibility")
.dir(project.name)
.file(fileName)
.get().asFile.inputStream().use {
val zip = ZipInputStream(it)
val entries = generateSequence { zip.nextEntry }
entries.associate { it.name to "size:${it.size},crc:${it.crc}" }
} - ignoredFiles
val entriesA = loadEntries("a.jar")
val entriesB = loadEntries("b.jar")
val mismatches = (entriesA.keys + entriesB.keys).map { it to (entriesA[it] to entriesB[it]) }.filter { it.second.first != it.second.second }
check(mismatches.isEmpty()) {
"The following files have a different content:\n" + mismatches.joinToString("\n") { " ${it.first}: ${it.second.first} != ${it.second.second}" }
}
}
}
}

publishing {
publications {
create<MavenPublication>("maven") {
groupId = "org.modelix.mps"
artifactId = "diff-plugin"
artifact(tasks.buildPlugin) {
extension = "zip"
}
}
}
}

fun Provider<Directory>.dir(name: String): Provider<Directory> = map { it.dir(name) }
fun Provider<Directory>.file(name: String): Provider<RegularFile> = map { it.file(name) }
fun Provider<Directory>.dir(name: Provider<out CharSequence>): Provider<Directory> = flatMap { it.dir(name) }
Binary file added mps-diff-plugin/diff-test-project.zip
Binary file not shown.
145 changes: 145 additions & 0 deletions mps-diff-plugin/src/main/kotlin/org/modelix/ui/diff/DiffHandler.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package org.modelix.ui.diff

import com.intellij.openapi.project.ProjectManager
import io.ktor.http.ContentType
import io.ktor.http.HttpStatusCode
import io.ktor.server.application.ApplicationCall
import io.ktor.server.application.call
import io.ktor.server.application.createApplicationPlugin
import io.ktor.server.response.respondOutputStream
import io.ktor.server.response.respondText
import io.ktor.server.routing.Route
import io.ktor.server.routing.get
import io.ktor.server.routing.route
import io.ktor.server.routing.routing
import io.ktor.util.pipeline.PipelineContext
import jetbrains.mps.ide.project.ProjectHelper
import jetbrains.mps.project.MPSProject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.swing.Swing
import kotlinx.coroutines.withContext
import kotlinx.html.HTML
import kotlinx.html.body
import kotlinx.html.br
import kotlinx.html.div
import kotlinx.html.h1
import kotlinx.html.h2
import kotlinx.html.html
import kotlinx.html.img
import kotlinx.html.stream.createHTML
import kotlinx.html.style
import java.util.Collections
import javax.imageio.ImageIO

val DiffHandler = createApplicationPlugin("DiffHandler") {
val handler = DiffHandlerImpl()
application.routing {
handler.apply { installRoutes() }
}
}

data class DiffRequest(val leftRevision: String, val rightRevision: String)

class DiffHandlerImpl() {
private val diffRequests = Collections.synchronizedMap(HashMap<DiffRequest, Deferred<List<DiffImage>>>())
private val coroutineScope = CoroutineScope(Dispatchers.Default)

fun Route.installRoutes() {
get("/") {
call.respondText("Diff server")
}
get("/clear") {
diffRequests.clear()
call.respondText("Cache cleared")
}
route("/{leftRevision}/{rightRevision}") {
suspend fun PipelineContext<Unit, ApplicationCall>.getImages(): Deferred<List<DiffImage>>? {
val diffRequest = DiffRequest(call.parameters["leftRevision"]!!, call.parameters["rightRevision"]!!)

var diffResult = diffRequests[diffRequest]
if (diffResult == null) {
val project = (ProjectManager.getInstance().openProjects + ProjectManager.getInstance().defaultProject).firstOrNull()
if (project == null) {
call.respondText(text = "No project found", status = HttpStatusCode.ServiceUnavailable)
return null
}
val mpsProject: MPSProject? = ProjectHelper.fromIdeaProject(project)
if (mpsProject == null) {
call.respondText(text = "MPS project not initialized yet", status = HttpStatusCode.ServiceUnavailable)
return null
}

diffResult = synchronized(diffRequests) {
if (!diffRequests.containsKey(diffRequest)) {
diffRequests[diffRequest] = coroutineScope.async {
DiffImages(project)
.diffRevisions(diffRequest.leftRevision, diffRequest.rightRevision).flatMap {
// The computation of the diff is not allowed to happen on the EDT,
// but the rendering of the diff dialog has to happen on the EDT.
//
// Using Dispatchers.Swing instead of Dispatchers.Main, because it's not
// initialized in older MPS versions.
withContext(Dispatchers.Swing) {
it()
}
}
}
}
diffRequests[diffRequest]
}
}
return diffResult
}
get("/") {
val images0 = getImages() ?: return@get
val images = images0.await()
call.respondHtmlSafe {
body {
h1 { +"Diff" }
br { }
br { }
for (image in images) {
h2 {
text(image.affectedFile + " - " + image.rootNodePresentation)
}
div {
img(src = image.id + ".png") {
style = "height:auto;max-width:100%;width:${image.size.width}px"
}
br { }
br { }
br { }
}
}
}
}
}
get("{imageId}.png") {
val imageId = call.parameters["imageId"]
val images = getImages() ?: return@get
val image = images.await().find { it.id == imageId }
if (image == null) {
call.respondText("Image with ID $imageId not found", status = HttpStatusCode.NotFound)
} else {
call.respondOutputStream(contentType = ContentType.Image.PNG) {
ImageIO.write(image.image, "png", this)
}
}
}
}
}
}

/**
* respondHtml fails to respond anything if an exception is thrown in the body and an error handler is installed
* that tries to respond an error page.
*/
suspend fun ApplicationCall.respondHtmlSafe(status: HttpStatusCode = HttpStatusCode.OK, block: HTML.() -> Unit) {
val htmlText = createHTML().html {
block()
}
respondText(htmlText, ContentType.Text.Html, status)
}
Loading

0 comments on commit 32bf52e

Please sign in to comment.