diff --git a/.github/workflows/mps-compatibility.yaml b/.github/workflows/mps-compatibility.yaml index 9a22a6bb..731aa3ed 100644 --- a/.github/workflows/mps-compatibility.yaml +++ b/.github/workflows/mps-compatibility.yaml @@ -32,7 +32,13 @@ jobs: - name: Build env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: ./gradlew :projectional-editor-ssr-mps:build -PciBuild=true -Pgpr.token=${{ secrets.GITHUB_TOKEN }} -Pmps.version.major=${{ matrix.version }} + run: >- + ./gradlew + :projectional-editor-ssr-mps:build + :projectional-editor-ssr-mps-languages:build + -PciBuild=true + -Pgpr.token=${{ secrets.GITHUB_TOKEN }} + -Pmps.version.major=${{ matrix.version }} - name: Archive test report uses: actions/upload-artifact@v4 if: always() diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0bd03c9a..bbc970c4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,8 +6,7 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.4.0 hooks: - - id: trailing-whitespace - - id: end-of-file-fixer +# - id: end-of-file-fixer # disabled, because it modifies MPS XML files - id: check-toml - id: check-yaml - id: check-added-large-files @@ -15,7 +14,7 @@ repos: - id: check-symlinks - id: fix-byte-order-marker - id: mixed-line-ending - - id: trailing-whitespace +# - id: trailing-whitespace # disabled, because it modifies multiline string literals to invalid values - repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook rev: v9.4.0 hooks: diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 876c922b..80bf5350 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -2,6 +2,10 @@ plugins { `kotlin-dsl` } +dependencies { + implementation(kotlin("stdlib")) +} + repositories { mavenCentral() } diff --git a/buildSrc/src/main/kotlin/org/modelix/CopyMps.kt b/buildSrc/src/main/kotlin/org/modelix/CopyMps.kt index 9610fbaf..b31286df 100644 --- a/buildSrc/src/main/kotlin/org/modelix/CopyMps.kt +++ b/buildSrc/src/main/kotlin/org/modelix/CopyMps.kt @@ -27,7 +27,7 @@ val Project.mpsMajorVersion: String get() { if (project != rootProject) return rootProject.mpsVersion return project.findProperty("mps.version.major")?.toString()?.takeIf { it.isNotEmpty() } ?: project.findProperty("mps.version")?.toString()?.takeIf { it.isNotEmpty() }?.replace(Regex("""(20\d\d\.\d+).*"""), "$1") - ?: "2023.3" + ?: "2023.2" } val Project.mpsVersion: String get() { @@ -61,6 +61,14 @@ val Project.mpsHomeDir: Provider get() { return project.layout.buildDirectory.dir("mps-$mpsVersion") } +val Project.mpsPluginsDir: File? get() { + val candidates = listOfNotNull( + project.findProperty("mps$mpsPlatformVersion.plugins.dir")?.toString()?.let { file(it) }, + System.getProperty("user.home")?.let { file(it).resolve("/Library/Application Support/JetBrains/MPS2023.2/plugins/") }, + ) + return candidates.firstOrNull { it.isDirectory } +} + fun Project.copyMps(): File { if (project != rootProject) return rootProject.copyMps() diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8a4ed8f7..7946e82f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,8 +6,8 @@ npm-publish = { id = "dev.petuska.npm.publish", version = "3.4.2" } modelix-mps-buildtools = { id = "org.modelix.mps.build-tools", version.ref = "modelixBuildtools" } [versions] -modelixCore = "7.2.0-pr707.7a5dda1" -modelixBuildtools="1.4.1" +modelixCore = "7.3.0" +modelixBuildtools="1.4.2-kotlin.support" [libraries] modelix-model-api = { group = "org.modelix", name = "model-api", version.ref = "modelixCore" } @@ -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.9" } kotlin-html = "org.jetbrains.kotlinx:kotlinx-html:0.11.0" +modelix-build-tools-lib = { group = "org.modelix.mps", name = "build-tools-lib", version.ref = "modelixBuildtools" } diff --git a/kernelf-angular-demo/build.gradle.kts b/kernelf-angular-demo/build.gradle.kts index 8c8f8c01..62325219 100644 --- a/kernelf-angular-demo/build.gradle.kts +++ b/kernelf-angular-demo/build.gradle.kts @@ -32,12 +32,12 @@ val updateTsModelApiVersion = tasks.create("updateTsModelApiVersion") { var text = packageJsonFile.readText() println("ts-model-api path: $localPath") val replacement = if (localPath.exists()) { - """"@modelix/ts-model-api": "file:${localPath.relativeTo(projectDir)}"""" + """"@modelix/ts-model-api": "file:${localPath.relativeTo(projectDir).toString().replace("\\", "\\\\")}"""" } else { """"@modelix/ts-model-api": "${rootProject.property("ts-model-api.version")}"""" } println("ts-model-api version: $replacement") - text = text.replace(Regex(""""@modelix/ts-model-api": ".*""""), replacement) + text = text.replace(Regex(""""@modelix/ts-model-api": ".*""""), { replacement }) packageJsonFile.writeText(text) } } diff --git a/mps/.mps/modules.xml b/mps/.mps/modules.xml index 09474367..882ff03c 100644 --- a/mps/.mps/modules.xml +++ b/mps/.mps/modules.xml @@ -2,13 +2,12 @@ - - - - - - - + + + + + + diff --git a/mps/build.gradle.kts b/mps/build.gradle.kts index 2de564de..64dedc2c 100644 --- a/mps/build.gradle.kts +++ b/mps/build.gradle.kts @@ -8,15 +8,20 @@ plugins { } mpsBuild { + dependsOn(":projectional-editor-ssr-mps:buildPlugin") mpsHome = mpsHomeDir.get().asFile.absolutePath javaHome = Jvm.current().javaHome - search(".") + search("../projectional-editor-ssr-mps/build/idea-sandbox/plugins/projectional-editor-ssr-mps") + search("modules") publication("editor-languages") { module("org.modelix.mps.webaspect.devkit") + module("org.modelix.mps.webaspect.genplan") + module("org.modelix.mps.baseLanguage2kotlin") + module("org.modelix.mps.notation") } - publication("already-packaged-modules") { - module("org.modelix.mps.editor.ssr.stubs") + publication("baseLanguage-notation") { + module("org.modelix.mps.notation.impl.baseLanguage") } } diff --git a/mps/org.modelix.mps.baseLanguage2kotlin/generator/templates/org.modelix.mps.baseLanguage2kotlin.generator.templates@generator.mps b/mps/modules/org.modelix.mps.baseLanguage2kotlin/generator/templates/org.modelix.mps.baseLanguage2kotlin.generator.templates@generator.mps similarity index 100% rename from mps/org.modelix.mps.baseLanguage2kotlin/generator/templates/org.modelix.mps.baseLanguage2kotlin.generator.templates@generator.mps rename to mps/modules/org.modelix.mps.baseLanguage2kotlin/generator/templates/org.modelix.mps.baseLanguage2kotlin.generator.templates@generator.mps diff --git a/mps/org.modelix.mps.baseLanguage2kotlin/models/org.modelix.mps.baseLanguage2kotlin.behavior.mps b/mps/modules/org.modelix.mps.baseLanguage2kotlin/models/org.modelix.mps.baseLanguage2kotlin.behavior.mps similarity index 100% rename from mps/org.modelix.mps.baseLanguage2kotlin/models/org.modelix.mps.baseLanguage2kotlin.behavior.mps rename to mps/modules/org.modelix.mps.baseLanguage2kotlin/models/org.modelix.mps.baseLanguage2kotlin.behavior.mps diff --git a/mps/org.modelix.mps.baseLanguage2kotlin/models/org.modelix.mps.baseLanguage2kotlin.constraints.mps b/mps/modules/org.modelix.mps.baseLanguage2kotlin/models/org.modelix.mps.baseLanguage2kotlin.constraints.mps similarity index 100% rename from mps/org.modelix.mps.baseLanguage2kotlin/models/org.modelix.mps.baseLanguage2kotlin.constraints.mps rename to mps/modules/org.modelix.mps.baseLanguage2kotlin/models/org.modelix.mps.baseLanguage2kotlin.constraints.mps diff --git a/mps/org.modelix.mps.baseLanguage2kotlin/models/org.modelix.mps.baseLanguage2kotlin.editor.mps b/mps/modules/org.modelix.mps.baseLanguage2kotlin/models/org.modelix.mps.baseLanguage2kotlin.editor.mps similarity index 100% rename from mps/org.modelix.mps.baseLanguage2kotlin/models/org.modelix.mps.baseLanguage2kotlin.editor.mps rename to mps/modules/org.modelix.mps.baseLanguage2kotlin/models/org.modelix.mps.baseLanguage2kotlin.editor.mps diff --git a/mps/org.modelix.mps.baseLanguage2kotlin/models/org.modelix.mps.baseLanguage2kotlin.structure.mps b/mps/modules/org.modelix.mps.baseLanguage2kotlin/models/org.modelix.mps.baseLanguage2kotlin.structure.mps similarity index 100% rename from mps/org.modelix.mps.baseLanguage2kotlin/models/org.modelix.mps.baseLanguage2kotlin.structure.mps rename to mps/modules/org.modelix.mps.baseLanguage2kotlin/models/org.modelix.mps.baseLanguage2kotlin.structure.mps diff --git a/mps/org.modelix.mps.baseLanguage2kotlin/models/org.modelix.mps.baseLanguage2kotlin.textGen.mps b/mps/modules/org.modelix.mps.baseLanguage2kotlin/models/org.modelix.mps.baseLanguage2kotlin.textGen.mps similarity index 100% rename from mps/org.modelix.mps.baseLanguage2kotlin/models/org.modelix.mps.baseLanguage2kotlin.textGen.mps rename to mps/modules/org.modelix.mps.baseLanguage2kotlin/models/org.modelix.mps.baseLanguage2kotlin.textGen.mps diff --git a/mps/org.modelix.mps.baseLanguage2kotlin/models/org.modelix.mps.baseLanguage2kotlin.typesystem.mps b/mps/modules/org.modelix.mps.baseLanguage2kotlin/models/org.modelix.mps.baseLanguage2kotlin.typesystem.mps similarity index 100% rename from mps/org.modelix.mps.baseLanguage2kotlin/models/org.modelix.mps.baseLanguage2kotlin.typesystem.mps rename to mps/modules/org.modelix.mps.baseLanguage2kotlin/models/org.modelix.mps.baseLanguage2kotlin.typesystem.mps diff --git a/mps/org.modelix.mps.baseLanguage2kotlin/org.modelix.mps.baseLanguage2kotlin.mpl b/mps/modules/org.modelix.mps.baseLanguage2kotlin/org.modelix.mps.baseLanguage2kotlin.mpl similarity index 100% rename from mps/org.modelix.mps.baseLanguage2kotlin/org.modelix.mps.baseLanguage2kotlin.mpl rename to mps/modules/org.modelix.mps.baseLanguage2kotlin/org.modelix.mps.baseLanguage2kotlin.mpl diff --git a/mps/solutions/org.modelix.mps.notation.impl.baseLanguage/models/org.modelix.mps.notation.impl.baseLanguage.modelix.mps b/mps/modules/org.modelix.mps.notation.impl.baseLanguage/models/org.modelix.mps.notation.impl.baseLanguage.modelix.mps similarity index 99% rename from mps/solutions/org.modelix.mps.notation.impl.baseLanguage/models/org.modelix.mps.notation.impl.baseLanguage.modelix.mps rename to mps/modules/org.modelix.mps.notation.impl.baseLanguage/models/org.modelix.mps.notation.impl.baseLanguage.modelix.mps index 2c83def9..362bf057 100644 --- a/mps/solutions/org.modelix.mps.notation.impl.baseLanguage/models/org.modelix.mps.notation.impl.baseLanguage.modelix.mps +++ b/mps/modules/org.modelix.mps.notation.impl.baseLanguage/models/org.modelix.mps.notation.impl.baseLanguage.modelix.mps @@ -34,6 +34,7 @@ + @@ -121,6 +122,7 @@ + @@ -238,6 +240,7 @@ + @@ -1437,7 +1440,10 @@ - + + + + diff --git a/mps/solutions/org.modelix.mps.notation.impl.baseLanguage/org.modelix.mps.notation.impl.baseLanguage.msd b/mps/modules/org.modelix.mps.notation.impl.baseLanguage/org.modelix.mps.notation.impl.baseLanguage.msd similarity index 100% rename from mps/solutions/org.modelix.mps.notation.impl.baseLanguage/org.modelix.mps.notation.impl.baseLanguage.msd rename to mps/modules/org.modelix.mps.notation.impl.baseLanguage/org.modelix.mps.notation.impl.baseLanguage.msd diff --git a/mps/languages/org.modelix.mps.notation/generator/templates/org.modelix.mps.notation.generator.templates@generator.mps b/mps/modules/org.modelix.mps.notation/generator/templates/org.modelix.mps.notation.generator.templates@generator.mps similarity index 88% rename from mps/languages/org.modelix.mps.notation/generator/templates/org.modelix.mps.notation.generator.templates@generator.mps rename to mps/modules/org.modelix.mps.notation/generator/templates/org.modelix.mps.notation.generator.templates@generator.mps index de539a75..1232833f 100644 --- a/mps/languages/org.modelix.mps.notation/generator/templates/org.modelix.mps.notation.generator.templates@generator.mps +++ b/mps/modules/org.modelix.mps.notation/generator/templates/org.modelix.mps.notation.generator.templates@generator.mps @@ -35,8 +35,8 @@ - + @@ -76,6 +76,9 @@ + + + @@ -90,6 +93,10 @@ + + + + @@ -99,6 +106,10 @@ + + + + @@ -636,7 +647,45 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -722,7 +771,49 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1253,94 +1344,20 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + - - - - - @@ -1463,4 +1480,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mps/languages/org.modelix.mps.notation/models/org.modelix.mps.notation.behavior.mps b/mps/modules/org.modelix.mps.notation/models/org.modelix.mps.notation.behavior.mps similarity index 100% rename from mps/languages/org.modelix.mps.notation/models/org.modelix.mps.notation.behavior.mps rename to mps/modules/org.modelix.mps.notation/models/org.modelix.mps.notation.behavior.mps diff --git a/mps/languages/org.modelix.mps.notation/models/org.modelix.mps.notation.constraints.mps b/mps/modules/org.modelix.mps.notation/models/org.modelix.mps.notation.constraints.mps similarity index 100% rename from mps/languages/org.modelix.mps.notation/models/org.modelix.mps.notation.constraints.mps rename to mps/modules/org.modelix.mps.notation/models/org.modelix.mps.notation.constraints.mps diff --git a/mps/languages/org.modelix.mps.notation/models/org.modelix.mps.notation.editor.mps b/mps/modules/org.modelix.mps.notation/models/org.modelix.mps.notation.editor.mps similarity index 100% rename from mps/languages/org.modelix.mps.notation/models/org.modelix.mps.notation.editor.mps rename to mps/modules/org.modelix.mps.notation/models/org.modelix.mps.notation.editor.mps diff --git a/mps/languages/org.modelix.mps.notation/models/org.modelix.mps.notation.structure.mps b/mps/modules/org.modelix.mps.notation/models/org.modelix.mps.notation.structure.mps similarity index 100% rename from mps/languages/org.modelix.mps.notation/models/org.modelix.mps.notation.structure.mps rename to mps/modules/org.modelix.mps.notation/models/org.modelix.mps.notation.structure.mps diff --git a/mps/languages/org.modelix.mps.notation/models/org.modelix.mps.notation.typesystem.mps b/mps/modules/org.modelix.mps.notation/models/org.modelix.mps.notation.typesystem.mps similarity index 100% rename from mps/languages/org.modelix.mps.notation/models/org.modelix.mps.notation.typesystem.mps rename to mps/modules/org.modelix.mps.notation/models/org.modelix.mps.notation.typesystem.mps diff --git a/mps/languages/org.modelix.mps.notation/org.modelix.mps.notation.mpl b/mps/modules/org.modelix.mps.notation/org.modelix.mps.notation.mpl similarity index 100% rename from mps/languages/org.modelix.mps.notation/org.modelix.mps.notation.mpl rename to mps/modules/org.modelix.mps.notation/org.modelix.mps.notation.mpl diff --git a/mps/devkits/org.modelix.mps.webaspect.devkit/org.modelix.mps.webaspect.devkit.devkit b/mps/modules/org.modelix.mps.webaspect.devkit/org.modelix.mps.webaspect.devkit.devkit similarity index 100% rename from mps/devkits/org.modelix.mps.webaspect.devkit/org.modelix.mps.webaspect.devkit.devkit rename to mps/modules/org.modelix.mps.webaspect.devkit/org.modelix.mps.webaspect.devkit.devkit diff --git a/mps/solutions/org.modelix.mps.webaspect.genplan/models/org.modelix.mps.webaspect.genplan.mps b/mps/modules/org.modelix.mps.webaspect.genplan/models/org.modelix.mps.webaspect.genplan.mps similarity index 100% rename from mps/solutions/org.modelix.mps.webaspect.genplan/models/org.modelix.mps.webaspect.genplan.mps rename to mps/modules/org.modelix.mps.webaspect.genplan/models/org.modelix.mps.webaspect.genplan.mps diff --git a/mps/solutions/org.modelix.mps.webaspect.genplan/org.modelix.mps.webaspect.genplan.msd b/mps/modules/org.modelix.mps.webaspect.genplan/org.modelix.mps.webaspect.genplan.msd similarity index 100% rename from mps/solutions/org.modelix.mps.webaspect.genplan/org.modelix.mps.webaspect.genplan.msd rename to mps/modules/org.modelix.mps.webaspect.genplan/org.modelix.mps.webaspect.genplan.msd diff --git a/mps/org.modelix.mps.editor.ssr.stubs/org.modelix.mps.editor.ssr.stubs.msd b/mps/org.modelix.mps.editor.ssr.stubs/org.modelix.mps.editor.ssr.stubs.msd deleted file mode 100644 index 0f197c95..00000000 --- a/mps/org.modelix.mps.editor.ssr.stubs/org.modelix.mps.editor.ssr.stubs.msd +++ /dev/null @@ -1,101 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 6354ebe7-c22a-4a0f-ac54-50b52ab9b065(JDK) - b50d89c0-0fb9-4105-b652-222148c26a9b(jetbrains.mps.kotlin.stdlib) - d2c5f31a-2aac-440d-a15c-2d8cba42bf3d(jetbrains.mps.kotlin.stdlib.jvm) - 6944825c-ddc2-4099-8cc7-5e6dbbf7f0be(jetbrains.mps.kotlin.stubs) - 8865b7a8-5271-43d3-884c-6fd1d9cfdd34(MPS.OpenAPI) - - - - - - - - - - - - - - - - - - - - - diff --git a/mps/org.modelix.mps.notation.runtime/org.modelix.mps.notation.runtime.msd b/mps/org.modelix.mps.notation.runtime/org.modelix.mps.notation.runtime.msd deleted file mode 100644 index 79b6144d..00000000 --- a/mps/org.modelix.mps.notation.runtime/org.modelix.mps.notation.runtime.msd +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/mps/stubs-template/org.modelix.mps.editor.ssr.stubs/org.modelix.mps.editor.ssr.stubs.msd b/mps/stubs-template/org.modelix.mps.editor.ssr.stubs/org.modelix.mps.editor.ssr.stubs.msd new file mode 100644 index 00000000..3f83bd49 --- /dev/null +++ b/mps/stubs-template/org.modelix.mps.editor.ssr.stubs/org.modelix.mps.editor.ssr.stubs.msd @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 6354ebe7-c22a-4a0f-ac54-50b52ab9b065(JDK) + b50d89c0-0fb9-4105-b652-222148c26a9b(jetbrains.mps.kotlin.stdlib) + d2c5f31a-2aac-440d-a15c-2d8cba42bf3d(jetbrains.mps.kotlin.stdlib.jvm) + 6944825c-ddc2-4099-8cc7-5e6dbbf7f0be(jetbrains.mps.kotlin.stubs) + 8865b7a8-5271-43d3-884c-6fd1d9cfdd34(MPS.OpenAPI) + + + + + + + + + + + + + + + + + + + + + diff --git a/projectional-editor-ssr-mps-languages/build.gradle.kts b/projectional-editor-ssr-mps-languages/build.gradle.kts new file mode 100644 index 00000000..2399ebd7 --- /dev/null +++ b/projectional-editor-ssr-mps-languages/build.gradle.kts @@ -0,0 +1,149 @@ +import org.modelix.mpsHomeDir +import org.modelix.mpsPluginsDir + +buildscript { + dependencies { + classpath(coreLibs.semver) + classpath(libs.modelix.build.tools.lib) + } +} + +plugins { + kotlin("jvm") + id("org.jetbrains.intellij") version "1.17.3" +} + +kotlin { + jvmToolchain(11) +} + +dependencies { + testImplementation(project(":projectional-editor-ssr-mps")) + testImplementation(project(":projectional-editor")) + testImplementation(libs.modelix.mps.model.adapters) +} + +intellij { + localPath = mpsHomeDir.map { it.asFile.absolutePath } + instrumentCode = false + plugins = listOf(project(":projectional-editor-ssr-mps")) + listOf( +// "Git4Idea", +// "Subversion", +// "com.intellij.copyright", +// "com.intellij.laf.macos", +// "com.intellij.platform.images", +// "com.intellij.properties", +// "com.intellij.properties.bundle.editor", +// "com.intellij.tasks", +// "com.jetbrains.changeReminder", +// "jetbrains.jetpad", +// "jetbrains.mps.build", +// "jetbrains.mps.build.ui", +// "jetbrains.mps.console", + "jetbrains.mps.core", +// "jetbrains.mps.debugger.api", +// "jetbrains.mps.debugger.java", +// "jetbrains.mps.editor.contextActions", +// "jetbrains.mps.editor.diagram", +// "jetbrains.mps.editor.spellcheck", +// "jetbrains.mps.editor.tooltips", +// "jetbrains.mps.execution.api", +// "jetbrains.mps.execution.configurations", +// "jetbrains.mps.execution.languages", +// "jetbrains.mps.git4idea.stubs", +// "jetbrains.mps.ide", +// "jetbrains.mps.ide.devkit", +// "jetbrains.mps.ide.httpsupport", +// "jetbrains.mps.ide.java", +// "jetbrains.mps.ide.make", +// "jetbrains.mps.ide.memtool", +// "jetbrains.mps.ide.migration.workbench", +// "jetbrains.mps.ide.modelchecker", +// "jetbrains.mps.ide.mpsmigration", + "jetbrains.mps.kotlin", +// "jetbrains.mps.navbar", +// "jetbrains.mps.rcp", +// "jetbrains.mps.samples", +// "jetbrains.mps.testing", +// "jetbrains.mps.tool.make", +// "jetbrains.mps.trove", +// "jetbrains.mps.vcs", +// "org.intellij.plugins.markdown", +// "org.jetbrains.plugins.github", +// "org.jetbrains.settingsRepository", + +// "com.intellij", +// "com.jetbrains.sh", +// "org.jetbrains.plugins.terminal", + ) +} + +tasks { + compileKotlin { + kotlinOptions { + apiVersion = "1.6" + } + } + patchPluginXml { + sinceBuild.set("232") + untilBuild.set("233.*") + } + + buildSearchableOptions { + enabled = false + } + +// signPlugin { +// certificateChain.set(System.getenv("CERTIFICATE_CHAIN")) +// privateKey.set(System.getenv("PRIVATE_KEY")) +// password.set(System.getenv("PRIVATE_KEY_PASSWORD")) +// } +// +// publishPlugin { +// token.set(System.getenv("PUBLISH_TOKEN")) +// } + + runIde { + autoReloadPlugins.set(true) + } + + val pluginDir = mpsPluginsDir + if (pluginDir != null) { + create("installMpsPlugin") { + dependsOn(prepareSandbox) + from(project.layout.buildDirectory.dir("idea-sandbox/plugins/${project.name}")) + into(pluginDir.resolve(project.name)) + } + } + + withType().configureEach { + dependsOn(project(":mps").tasks.named("packageMpsPublications")) + + intoChild(pluginName.map { "$it/META-INF" }) + .from(project.layout.projectDirectory.file("src/main/resources/META-INF")) + .exclude("plugin.xml") + intoChild(pluginName.map { "$it/META-INF" }) + .from(patchPluginXml.flatMap { it.outputFiles }) + + listOf("editor-languages", "baseLanguage-notation").forEach { publicationName -> + intoChild(pluginName.map { "$it/languages" }) + .from(zipTree({ project(":mps").layout.buildDirectory.file("mpsbuild/publications/$publicationName.zip") })) + .eachFile { + path = path.replaceFirst("packaged-modules/", "") + } + } + } +} + +group = "org.modelix.mps" + +publishing { + publications { + create("maven") { + artifactId = "projectional-editor-languages-plugin" + artifact(tasks.buildPlugin) { + extension = "zip" + } + } + } +} diff --git a/projectional-editor-ssr-mps-languages/src/main/resources/META-INF/plugin.xml b/projectional-editor-ssr-mps-languages/src/main/resources/META-INF/plugin.xml new file mode 100644 index 00000000..a82cc8b8 --- /dev/null +++ b/projectional-editor-ssr-mps-languages/src/main/resources/META-INF/plugin.xml @@ -0,0 +1,28 @@ + + + + org.modelix.mps.editor.languages + + + Notation Language for the Modelix Web Editor + + + itemis AG + + + + + + com.intellij.modules.mps + jetbrains.mps.core + org.modelix.mps.editor + + + + diff --git a/projectional-editor-ssr-mps-languages/src/main/resources/META-INF/pluginIcon.svg b/projectional-editor-ssr-mps-languages/src/main/resources/META-INF/pluginIcon.svg new file mode 100644 index 00000000..ddaf89cb --- /dev/null +++ b/projectional-editor-ssr-mps-languages/src/main/resources/META-INF/pluginIcon.svg @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/projectional-editor-ssr-mps-languages/src/test/kotlin/org/modelix/editor/ssr/mps/BaseLanguageTests.kt b/projectional-editor-ssr-mps-languages/src/test/kotlin/org/modelix/editor/ssr/mps/BaseLanguageTests.kt new file mode 100644 index 00000000..df211fd3 --- /dev/null +++ b/projectional-editor-ssr-mps-languages/src/test/kotlin/org/modelix/editor/ssr/mps/BaseLanguageTests.kt @@ -0,0 +1,73 @@ +package org.modelix.editor.ssr.mps + +import org.modelix.editor.CaretSelection +import org.modelix.editor.EditorEngine +import org.modelix.editor.JSKeyboardEvent +import org.modelix.editor.JSKeyboardEventType +import org.modelix.editor.KnownKeys +import org.modelix.editor.NodeCellReference +import org.modelix.editor.getVisibleText +import org.modelix.editor.lastLeaf +import org.modelix.editor.layoutable +import org.modelix.incremental.IncrementalEngine +import org.modelix.model.api.INode +import org.modelix.model.mpsadapters.MPSNode + +/** + * Test editor for MPS baseLanguage ClassConcept + */ +class BaseLanguageTests : TestBase("SimpleProject") { + fun `test inserting new line into class`() { + lateinit var rootNode: MPSNode + lateinit var firstMethod: INode + readAction { + val solution = mpsProject.projectModules.first { it.moduleName == "Solution1" } + val model = solution.models.first() + rootNode = model.rootNodes.first().let { MPSNode(it) } + firstMethod = rootNode.allChildren.first { it.concept?.getShortName() == "InstanceMethodDeclaration" } + } + + fun memberConcepts() = readAction { + rootNode.allChildren.filter { it.getContainmentLink()?.getSimpleName() == "member" }.map { it.concept?.getShortName() } + } + assertEquals(listOf("InstanceMethodDeclaration"), memberConcepts()) + + val incrementalEngine = IncrementalEngine() + val editorEngine = EditorEngine(incrementalEngine) + val mpsIntegration = EditorIntegrationForMPS(editorEngine) + mpsIntegration.init(mpsProject.repository) + + val editor = editorEngine.editNode(rootNode) + fun assertEditorText(expected: String) { + assertEquals(expected, editor.getRootCell().layout.toString()) + } + + assertEditorText( + """ + class Class1 { + public void method1() { + + } + } + """.trimIndent(), + ) + + val firstMethodCell = editor.resolveCell(NodeCellReference(firstMethod.reference)).first() + val lastLeafCell = firstMethodCell.lastLeaf() + assertEquals("}", lastLeafCell.getVisibleText()) + + editor.changeSelection(CaretSelection(lastLeafCell.layoutable()!!, -1)) + editor.processKeyEvent(JSKeyboardEvent(JSKeyboardEventType.KEYDOWN, KnownKeys.Enter)) + + assertEditorText( + """ + class Class1 { + public void method1() { + + } + + } + """.trimIndent(), + ) + } +} diff --git a/projectional-editor-ssr-mps-languages/src/test/kotlin/org/modelix/editor/ssr/mps/TestBase.kt b/projectional-editor-ssr-mps-languages/src/test/kotlin/org/modelix/editor/ssr/mps/TestBase.kt new file mode 100644 index 00000000..7d6e9e6f --- /dev/null +++ b/projectional-editor-ssr-mps-languages/src/test/kotlin/org/modelix/editor/ssr/mps/TestBase.kt @@ -0,0 +1,123 @@ +/* + * 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.editor.ssr.mps + +import com.intellij.ide.impl.OpenProjectTask +import com.intellij.openapi.Disposable +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.project.Project +import com.intellij.openapi.project.ProjectManager +import com.intellij.openapi.project.ex.ProjectManagerEx +import com.intellij.openapi.util.Disposer +import com.intellij.testFramework.TestApplicationManager +import com.intellij.testFramework.UsefulTestCase +import jetbrains.mps.ide.ThreadUtils +import jetbrains.mps.ide.project.ProjectHelper +import jetbrains.mps.project.MPSProject +import java.io.File +import java.nio.file.Files +import java.nio.file.Path +import kotlin.io.path.ExperimentalPathApi +import kotlin.io.path.absolute +import kotlin.io.path.deleteRecursively + +/** + * Based on org.jetbrains.uast.test.env.AbstractLargeProjectTest + */ +@Suppress("removal") +abstract class TestBase(val testDataName: String?) : UsefulTestCase() { + init { + // workaround for MPS 2023.3 failing to start in test mode + System.setProperty("intellij.platform.load.app.info.from.resources", "true") + } + + protected lateinit var project: Project + + override fun runInDispatchThread() = false + + override fun setUp() { + super.setUp() + TestApplicationManager.getInstance() + project = openTestProject() + } + + override fun tearDown() { + super.tearDown() + } + + @OptIn(ExperimentalPathApi::class) + private fun openTestProject(): Project { + val projectDirParent = Path.of("build", "test-projects").absolute() + projectDirParent.toFile().mkdirs() + val projectDir = Files.createTempDirectory(projectDirParent, "mps-project") + projectDir.deleteRecursively() + projectDir.toFile().mkdirs() + projectDir.toFile().deleteOnExit() + val project = if (testDataName != null) { + val sourceDir = File("testdata/$testDataName") + sourceDir.copyRecursively(projectDir.toFile(), overwrite = true) + ProjectManagerEx.getInstanceEx().openProject(projectDir, OpenProjectTask())!! + } else { + ProjectManagerEx.getInstanceEx().newProject(projectDir, OpenProjectTask())!! + } + + disposeOnTearDownInEdt { ProjectManager.getInstance().closeAndDispose(project) } + + ApplicationManager.getApplication().invokeAndWait { + // empty - openTestProject executed not in EDT, so, invokeAndWait just forces + // processing of all events that were queued during project opening + } + + return project + } + + private fun disposeOnTearDownInEdt(runnable: Runnable) { + Disposer.register( + testRootDisposable, + Disposable { + ApplicationManager.getApplication().invokeAndWait(runnable) + }, + ) + } + + protected val mpsProject: MPSProject get() { + return checkNotNull(ProjectHelper.fromIdeaProject(project)) { "MPS project not loaded" } + } + + protected fun writeAction(body: () -> R): R { + return mpsProject.modelAccess.computeWriteAction(body) + } + + protected fun writeActionOnEdt(body: () -> R): R { + return onEdt { writeAction { body() } } + } + + protected fun onEdt(body: () -> R): R { + var result: R? = null + ThreadUtils.runInUIThreadAndWait { + result = body() + } + return result as R + } + + protected fun readAction(body: () -> R): R { + var result: R? = null + mpsProject.modelAccess.runReadAction { + result = body() + } + return result as R + } +} diff --git a/projectional-editor-ssr-mps-languages/src/test/kotlin/org/modelix/editor/ssr/mps/TestFrameworkSetupTest.kt b/projectional-editor-ssr-mps-languages/src/test/kotlin/org/modelix/editor/ssr/mps/TestFrameworkSetupTest.kt new file mode 100644 index 00000000..05251c41 --- /dev/null +++ b/projectional-editor-ssr-mps-languages/src/test/kotlin/org/modelix/editor/ssr/mps/TestFrameworkSetupTest.kt @@ -0,0 +1,35 @@ +package org.modelix.editor.ssr.mps + +import com.intellij.ide.plugins.PluginManager +import jetbrains.mps.classloading.ModuleClassLoader +import jetbrains.mps.module.ReloadableModuleBase + +/** + * Check that the environment is initialized properly and all plugins and modules are loaded properly. + */ +class TestFrameworkSetupTest : TestBase("SimpleProject") { + fun `test plugins loaded`() { + // IDEA plugins + assertContainsElements( + PluginManager.getPlugins().map { it.pluginId.idString }.sorted(), + "org.modelix.mps.editor.languages", + "org.modelix.mps.editor", + ) + + // MPS modules inside those IDEA plugins + readAction { + assertContainsElements( + mpsProject.repository.modules.map { it.moduleName ?: "" }.sorted(), + "org.modelix.mps.editor.ssr.stubs", + "org.modelix.mps.notation.impl.baseLanguage", + ) + } + } + + fun `test module is valid for classloading`() { + readAction { + val module = mpsProject.repository.modules.filterIsInstance().first { it.moduleName == "org.modelix.mps.notation.impl.baseLanguage" } + assertInstanceOf(module.classLoader, ModuleClassLoader::class.java) + } + } +} diff --git a/projectional-editor-ssr-mps-languages/testdata/SimpleProject/.mps/migration.xml b/projectional-editor-ssr-mps-languages/testdata/SimpleProject/.mps/migration.xml new file mode 100644 index 00000000..d512ce45 --- /dev/null +++ b/projectional-editor-ssr-mps-languages/testdata/SimpleProject/.mps/migration.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/projectional-editor-ssr-mps-languages/testdata/SimpleProject/.mps/modules.xml b/projectional-editor-ssr-mps-languages/testdata/SimpleProject/.mps/modules.xml new file mode 100644 index 00000000..d24dfe06 --- /dev/null +++ b/projectional-editor-ssr-mps-languages/testdata/SimpleProject/.mps/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/projectional-editor-ssr-mps-languages/testdata/SimpleProject/.mps/vcs.xml b/projectional-editor-ssr-mps-languages/testdata/SimpleProject/.mps/vcs.xml new file mode 100644 index 00000000..fbbc5665 --- /dev/null +++ b/projectional-editor-ssr-mps-languages/testdata/SimpleProject/.mps/vcs.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/projectional-editor-ssr-mps-languages/testdata/SimpleProject/.mps/workspace.xml b/projectional-editor-ssr-mps-languages/testdata/SimpleProject/.mps/workspace.xml new file mode 100644 index 00000000..3a6ee076 --- /dev/null +++ b/projectional-editor-ssr-mps-languages/testdata/SimpleProject/.mps/workspace.xml @@ -0,0 +1,181 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + { + "associatedIndex": 1 +} + + + + + + + + + + + + + + + + + + + + + + + + + { + "keyToString": { + "RunOnceActivity.OpenProjectViewOnStart": "true", + "RunOnceActivity.ShowReadmeOnStart": "true", + "settings.editor.selected.configurable": "preferences.projectLibraryManager" + } +} + + + + + 1714286490472 + + + + + + + + + + + diff --git a/projectional-editor-ssr-mps-languages/testdata/SimpleProject/solutions/Solution1/Solution1.msd b/projectional-editor-ssr-mps-languages/testdata/SimpleProject/solutions/Solution1/Solution1.msd new file mode 100644 index 00000000..f015948f --- /dev/null +++ b/projectional-editor-ssr-mps-languages/testdata/SimpleProject/solutions/Solution1/Solution1.msd @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/projectional-editor-ssr-mps-languages/testdata/SimpleProject/solutions/Solution1/models/Solution1.model1.mps b/projectional-editor-ssr-mps-languages/testdata/SimpleProject/solutions/Solution1/models/Solution1.model1.mps new file mode 100644 index 00000000..64b0724b --- /dev/null +++ b/projectional-editor-ssr-mps-languages/testdata/SimpleProject/solutions/Solution1/models/Solution1.model1.mps @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/projectional-editor-ssr-mps/build.gradle.kts b/projectional-editor-ssr-mps/build.gradle.kts index 28b776f3..cb5a30e1 100644 --- a/projectional-editor-ssr-mps/build.gradle.kts +++ b/projectional-editor-ssr-mps/build.gradle.kts @@ -2,10 +2,12 @@ import com.jetbrains.plugin.structure.intellij.utils.JDOMUtil import org.jdom2.Element import org.jetbrains.intellij.transformXml import org.modelix.mpsHomeDir +import org.modelix.mpsPluginsDir buildscript { dependencies { classpath(coreLibs.semver) + classpath(libs.modelix.build.tools.lib) } } @@ -87,7 +89,7 @@ tasks { doLast { val ownJar: File = prepareSandbox.get().pluginJar.get().asFile - val originalFile = project(":mps").layout.projectDirectory.file("$stubsSolutionName/$stubsSolutionName.msd").asFile + val originalFile = project(":mps").layout.projectDirectory.file("stubs-template/$stubsSolutionName/$stubsSolutionName.msd").asFile val xml = originalFile.inputStream().use { JDOMUtil.loadDocument(it) } val modelRoot = xml.descendants.filterIsInstance().first { it.name == "modelRoot" } @@ -117,12 +119,12 @@ tasks { archiveFileName = "$stubsSolutionName.jar" } - val mpsPluginDir = project.findProperty("mps232.plugins.dir")?.toString()?.let { file(it) } - if (mpsPluginDir != null && mpsPluginDir.isDirectory) { + val pluginDir = mpsPluginsDir + if (pluginDir != null) { create("installMpsPlugin") { dependsOn(prepareSandbox) from(project.layout.buildDirectory.dir("idea-sandbox/plugins/${project.name}")) - into(mpsPluginDir.resolve(project.name)) + into(pluginDir.resolve(project.name)) } } @@ -130,16 +132,11 @@ tasks { dependsOn(packageStubsSolution) intoChild(pluginName.map { "$it/languages" }) .from(packageStubsSolution.map { it.archiveFile }) - - if ("true" == project.findProperty("ciBuild")) { - // packaging the MPS modules during development would make them all read-only - dependsOn(project(":mps").tasks.named("packageMpsPublications")) - intoChild(pluginName.map { "$it/languages" }) - .from(zipTree({ project(":mps").layout.buildDirectory.file("mpsbuild/publications/editor-languages.zip") })) - .eachFile { - path = path.replaceFirst("packaged-modules/", "") - } - } + intoChild(pluginName.map { "$it/META-INF" }) + .from(project.layout.projectDirectory.file("src/main/resources/META-INF")) + .exclude("plugin.xml") + intoChild(pluginName.map { "$it/META-INF" }) + .from(patchPluginXml.flatMap { it.outputFiles }) } } diff --git a/projectional-editor-ssr-mps/src/main/kotlin/org/modelix/editor/ssr/mps/EditorIntegrationForMPS.kt b/projectional-editor-ssr-mps/src/main/kotlin/org/modelix/editor/ssr/mps/EditorIntegrationForMPS.kt new file mode 100644 index 00000000..9f79cf30 --- /dev/null +++ b/projectional-editor-ssr-mps/src/main/kotlin/org/modelix/editor/ssr/mps/EditorIntegrationForMPS.kt @@ -0,0 +1,34 @@ +package org.modelix.editor.ssr.mps + +import org.jetbrains.mps.openapi.module.SRepository +import org.modelix.constraints.ConstraintsAspect +import org.modelix.editor.EditorEngine +import org.modelix.model.api.ILanguageRepository +import org.modelix.model.mpsadapters.MPSChangeTranslator +import org.modelix.model.mpsadapters.MPSLanguageRepository +import org.modelix.scopes.ScopeAspect + +class EditorIntegrationForMPS(val editorEngine: EditorEngine) { + private var aspectsFromMPS: LanguageAspectsFromMPSModules? = null + private var mpsChangeTranslator: MPSChangeTranslator? = null + private var mpsLanguageRepository: MPSLanguageRepository? = null + + fun init(repository: SRepository) { + mpsLanguageRepository = MPSLanguageRepository(repository) + ILanguageRepository.register(mpsLanguageRepository!!) + mpsChangeTranslator = MPSChangeTranslator() + mpsChangeTranslator!!.start(repository) + aspectsFromMPS = LanguageAspectsFromMPSModules(repository) + ScopeAspect.registerScopeProvider(MPSScopeProvider) + ConstraintsAspect.checkers.add(MPSConstraints) + editorEngine.addRegistry(aspectsFromMPS!!) + } + + fun dispose() { + mpsLanguageRepository?.let { ILanguageRepository.unregister(it) } + mpsLanguageRepository = null + + ScopeAspect.unregisterScopeProvider(MPSScopeProvider) + ConstraintsAspect.checkers.remove(MPSConstraints) + } +} diff --git a/projectional-editor-ssr-mps/src/main/kotlin/org/modelix/editor/ssr/mps/ModelixSSRServerForMPS.kt b/projectional-editor-ssr-mps/src/main/kotlin/org/modelix/editor/ssr/mps/ModelixSSRServerForMPS.kt index d0f9313d..c47d5d70 100644 --- a/projectional-editor-ssr-mps/src/main/kotlin/org/modelix/editor/ssr/mps/ModelixSSRServerForMPS.kt +++ b/projectional-editor-ssr-mps/src/main/kotlin/org/modelix/editor/ssr/mps/ModelixSSRServerForMPS.kt @@ -54,7 +54,6 @@ import org.jetbrains.mps.openapi.language.SContainmentLink import org.jetbrains.mps.openapi.language.SProperty import org.jetbrains.mps.openapi.language.SReferenceLink import org.jetbrains.mps.openapi.model.SNode -import org.modelix.constraints.ConstraintsAspect import org.modelix.constraints.IConstraintViolation import org.modelix.constraints.IConstraintsChecker import org.modelix.editor.ExistingNode @@ -64,23 +63,19 @@ import org.modelix.editor.ssr.server.ModelixSSRServer import org.modelix.model.api.BuiltinLanguages import org.modelix.model.api.IChildLink import org.modelix.model.api.IConcept -import org.modelix.model.api.ILanguageRepository import org.modelix.model.api.INode import org.modelix.model.api.IProperty import org.modelix.model.api.IReferenceLink import org.modelix.model.api.NodeReference import org.modelix.model.api.runSynchronized -import org.modelix.model.mpsadapters.MPSChangeTranslator import org.modelix.model.mpsadapters.MPSChildLink import org.modelix.model.mpsadapters.MPSConcept -import org.modelix.model.mpsadapters.MPSLanguageRepository import org.modelix.model.mpsadapters.MPSNode import org.modelix.model.mpsadapters.MPSProperty import org.modelix.model.mpsadapters.MPSReferenceLink import org.modelix.model.mpsadapters.MPSRepositoryAsNode import org.modelix.scopes.IScope import org.modelix.scopes.IScopeProvider -import org.modelix.scopes.ScopeAspect import java.net.URLEncoder import java.nio.charset.StandardCharsets import java.time.Duration @@ -103,10 +98,8 @@ class ModelixSSRServerForMPS : Disposable { private var ssrServer: ModelixSSRServer? = null private var ktorServer: ApplicationEngine? = null - private var aspectsFromMPS: LanguageAspectsFromMPSModules? = null - private var mpsChangeTranslator: MPSChangeTranslator? = null - private var mpsLanguageRepository: MPSLanguageRepository? = null private val projects: MutableSet = Collections.synchronizedSet(HashSet()) + private var mpsIntegration: EditorIntegrationForMPS? = null fun registerProject(project: Project) { projects.add(project) @@ -135,17 +128,10 @@ class ModelixSSRServerForMPS : Disposable { println("starting modelix SSR server") - val repository = getMPSProjects().first().repository - mpsLanguageRepository = MPSLanguageRepository(repository) - ILanguageRepository.register(mpsLanguageRepository!!) - mpsChangeTranslator = MPSChangeTranslator() - mpsChangeTranslator!!.start(repository) val ssrServer = ModelixSSRServer((getRootNode() ?: return).getArea()) this.ssrServer = ssrServer - aspectsFromMPS = LanguageAspectsFromMPSModules(repository) - ScopeAspect.registerScopeProvider(MPSScopeProvider) - ConstraintsAspect.checkers.add(MPSConstraints) - ssrServer.editorEngine.addRegistry(aspectsFromMPS!!) + mpsIntegration = EditorIntegrationForMPS(ssrServer.editorEngine) + mpsIntegration!!.init(getMPSProjects().first().repository) ktorServer = embeddedServer(Netty, port = 43593) { initKtorServer(ssrServer) } @@ -167,11 +153,11 @@ class ModelixSSRServerForMPS : Disposable { repository.getArea().executeRead { body { ul { - repository.getChildren(BuiltinLanguages.MPSRepositoryConcepts.Repository.modules).forEach { + repository.getChildren(BuiltinLanguages.MPSRepositoryConcepts.Repository.modules).sortedBy { it.name }.forEach { li { a { href = "module/${URLEncoder.encode(it.reference.serialize(), StandardCharsets.UTF_8)}/" - +(it.getPropertyValue(BuiltinLanguages.jetbrains_mps_lang_core.INamedConcept.name) ?: "") + +(it.name ?: "") } } } @@ -188,11 +174,11 @@ class ModelixSSRServerForMPS : Disposable { val module = repository.getArea().resolveNode(NodeReference(moduleRef))!! body { ul { - module.getChildren(BuiltinLanguages.MPSRepositoryConcepts.Module.models).forEach { + module.getChildren(BuiltinLanguages.MPSRepositoryConcepts.Module.models).sortedBy { it.name }.forEach { li { a { href = "../../model/${URLEncoder.encode(it.reference.serialize(), StandardCharsets.UTF_8)}/" - +(it.getPropertyValue(BuiltinLanguages.jetbrains_mps_lang_core.INamedConcept.name) ?: "") + +(it.name ?: "") } } } @@ -209,11 +195,11 @@ class ModelixSSRServerForMPS : Disposable { val model = repository.getArea().resolveNode(NodeReference(modelRef))!! body { ul { - model.getChildren(BuiltinLanguages.MPSRepositoryConcepts.Model.rootNodes).forEach { + model.getChildren(BuiltinLanguages.MPSRepositoryConcepts.Model.rootNodes).sortedBy { it.name }.forEach { li { a { href = "../../editor/${URLEncoder.encode(it.reference.serialize(), StandardCharsets.UTF_8)}/" - +(it.getPropertyValue(BuiltinLanguages.jetbrains_mps_lang_core.INamedConcept.name) ?: "") + +(it.name ?: "") } } } @@ -255,12 +241,8 @@ class ModelixSSRServerForMPS : Disposable { ktorServer = null ssrServer?.dispose() ssrServer = null - - mpsLanguageRepository?.let { ILanguageRepository.unregister(it) } - mpsLanguageRepository = null - - ScopeAspect.unregisterScopeProvider(MPSScopeProvider) - ConstraintsAspect.checkers.remove(MPSConstraints) + mpsIntegration?.dispose() + mpsIntegration = null } } @@ -283,7 +265,7 @@ object MPSScopeProvider : IScopeProvider { val containmentLink: SContainmentLink = sourceNode.getContainmentLink().toMPS()!! val index = sourceNode.index() val association: SReferenceLink = link.toMPS()!! - val concept: SAbstractConcept = (sourceNode.getNode()?.concept ?: sourceNode.expectedConcept()).toMPS()!! + val concept: SAbstractConcept = sourceNode.expectedConcept().toMPS()!! ModelConstraints.getReferenceDescriptor( contextNode, containmentLink, @@ -309,10 +291,14 @@ object MPSConstraints : IConstraintsChecker { // Constraints only prevent creating a node. If it already exists, it's handled by the model checker. if (node.getNode() != null) return emptyList() + val parentNode = node.getParent()?.getNode().toMPS() + // MPS doesn't support constraint checking without a parent node + if (parentNode == null) return emptyList() + // ConstraintsCanBeFacade.checkCanBeRoot() val containmentContext = ContainmentContext.Builder() - .parentNode(node.getParent()?.getNode().toMPS()) + .parentNode(parentNode) .link(node.getContainmentLink().toMPS()) .childConcept(node.expectedConcept().toMPS()!!) .build() @@ -323,7 +309,7 @@ object MPSConstraints : IConstraintsChecker { ConstraintsCanBeFacade.checkCanBeAncestor( CanBeAncestorContext.Builder() .ancestorNode(ancestorNode) - .parentNode(node.getParent()?.getNode().toMPS()) + .parentNode(parentNode) .childConcept(node.expectedConcept().toMPS()!!) .descendantNode(node.getNode().toMPS()) .link(node.getContainmentLink().toMPS()) @@ -343,4 +329,6 @@ fun IReferenceLink?.toMPS(): SReferenceLink? = if (this is MPSReferenceLink) thi fun IProperty?.toMPS(): SProperty? = if (this is MPSProperty) this.property else null fun IConcept?.toMPS(): SAbstractConcept? = if (this is MPSConcept) this.concept else null +val INode.name get() = getPropertyValue(BuiltinLanguages.jetbrains_mps_lang_core.INamedConcept.name) + class MPSConstraintViolation(val rule: Rule<*>) : IConstraintViolation diff --git a/projectional-editor-ssr-mps/src/main/resources/META-INF/pluginIcon.svg b/projectional-editor-ssr-mps/src/main/resources/META-INF/pluginIcon.svg index 1555925e..ddaf89cb 100644 --- a/projectional-editor-ssr-mps/src/main/resources/META-INF/pluginIcon.svg +++ b/projectional-editor-ssr-mps/src/main/resources/META-INF/pluginIcon.svg @@ -1,27 +1,66 @@ - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/projectional-editor-ssr-mps/testdata/SimpleProject/.mps/modules.xml b/projectional-editor-ssr-mps/testdata/SimpleProject/.mps/modules.xml new file mode 100644 index 00000000..47f90476 --- /dev/null +++ b/projectional-editor-ssr-mps/testdata/SimpleProject/.mps/modules.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/projectional-editor-ssr-mps/testdata/SimpleProject/.mps/vcs.xml b/projectional-editor-ssr-mps/testdata/SimpleProject/.mps/vcs.xml new file mode 100644 index 00000000..fbbc5665 --- /dev/null +++ b/projectional-editor-ssr-mps/testdata/SimpleProject/.mps/vcs.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/projectional-editor-ssr-server/src/main/kotlin/org/modelix/editor/ssr/server/ModelixSSRServer.kt b/projectional-editor-ssr-server/src/main/kotlin/org/modelix/editor/ssr/server/ModelixSSRServer.kt index 1e53c267..94c894e2 100644 --- a/projectional-editor-ssr-server/src/main/kotlin/org/modelix/editor/ssr/server/ModelixSSRServer.kt +++ b/projectional-editor-ssr-server/src/main/kotlin/org/modelix/editor/ssr/server/ModelixSSRServer.kt @@ -44,11 +44,8 @@ import java.util.Collections import kotlin.collections.HashMap import kotlin.collections.LinkedHashSet import kotlin.collections.List -import kotlin.collections.Map import kotlin.collections.MutableMap import kotlin.collections.MutableSet -import kotlin.collections.asSequence -import kotlin.collections.emptyMap import kotlin.collections.filter import kotlin.collections.forEach import kotlin.collections.get @@ -172,7 +169,6 @@ class ModelixSSRServer(private val nodeResolutionScope: INodeResolutionScope) { private inner class EditorSession(val editorId: String) { private var editorComponent: EditorComponent? = null private val commonElementPrefix = editorId + "-" - private var domStateOnClient: Map = emptyMap() private fun getEditor() = checkNotNull(editorComponent) { "Editor $editorId isn't initialized" } @@ -201,55 +197,57 @@ class ModelixSSRServer(private val nodeResolutionScope: INodeResolutionScope) { fun sendUpdate() { LOG.debug { "($editorId) sendUpdate" } editorComponent!!.update() - val dom = editorComponent!!.getHtmlElement()!! - LOG.debug { "($editorId) dom: $dom" } - val latestDomState = HashMap() - // TODO performance - var rootData = toUpdateData(dom, latestDomState) - if (rootData is ElementReference) rootData = latestDomState[rootData.id]!! - check(rootData is HTMLElementUpdateData) - if (rootData.id != editorId) { - latestDomState.remove(rootData.id) - rootData = rootData.copy(id = editorId) - latestDomState[editorId] = rootData + val dom = editorComponent!!.getHtmlElement()!! as VirtualDom.Node + val changedElements = HashMap() + var rootData: INodeUpdateData? = toUpdateData(dom, changedElements) + dom.resetModificationMarker() + if (rootData is ElementReference) rootData = changedElements[rootData.id] + if (rootData != null) { + check(rootData is HTMLElementUpdateData) + if (rootData.id != editorId) { + changedElements.remove(rootData.id) + rootData = rootData.copy(id = editorId) + changedElements[editorId] = rootData + } } - val changesOnly = latestDomState.entries.asSequence() - .filter { domStateOnClient[it.key] != it.value } - .map { it.value }.toList() - if (changesOnly.isEmpty()) return - - domStateOnClient = latestDomState + if (changedElements.isEmpty()) return ws.outgoing.trySend( Frame.Text( MessageFromServer( editorId = editorId, domUpdate = DomTreeUpdate( - elements = changesOnly, + elements = changedElements.values.toList(), ), ).toJson(), ), ) } - fun toUpdateData(node: IVirtualDom.Node, id2data: MutableMap): INodeUpdateData { + fun toUpdateData(node: VirtualDom.Node, id2data: MutableMap): INodeUpdateData { return when (node) { - is IVirtualDom.Text -> TextNodeUpdateData(node.textContent ?: "") - is IVirtualDom.Element -> { + is VirtualDom.Text -> TextNodeUpdateData(node.textContent ?: "") + is VirtualDom.Element -> { val id = node.id?.takeIf { it.isNotEmpty() }?.let { if (it.startsWith(commonElementPrefix)) it else commonElementPrefix + it } - val data = HTMLElementUpdateData( + fun createData() = HTMLElementUpdateData( id = id, tagName = node.tagName, attributes = node.getAttributes() - "id", children = node.childNodes.toList().map { toUpdateData(it, id2data) }, ) if (id == null) { - data + createData() } else { - id2data[id] = data + if (node.wasModified()) { + id2data[id] = createData() + } else { + if (node.wasAnyDescendantModified()) { + node.childNodes.forEach { toUpdateData(it, id2data) } + } + } ElementReference(id) } } diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CaretSelection.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CaretSelection.kt index 0e9a81f8..bdcb9655 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CaretSelection.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CaretSelection.kt @@ -164,10 +164,12 @@ class CaretSelection(val layoutable: LayoutableCell, val start: Int, val end: In } ).toList() val params = CodeCompletionParameters(editor, typedText) - val actions = providers.flatMap { it.flattenApplicableActions(params) } - val matchingActions = actions - .filter { it.getMatchingText().startsWith(typedText) } - .applyShadowing() + val matchingActions = editor.runRead { + val actions = providers.flatMap { it.flattenApplicableActions(params) } + actions + .filter { it.getMatchingText().startsWith(typedText) } + .applyShadowing() + } if (matchingActions.isNotEmpty()) { if (matchingActions.size == 1 && matchingActions.first().getMatchingText() == typedText) { matchingActions.first().execute(editor) @@ -209,7 +211,7 @@ class CaretSelection(val layoutable: LayoutableCell, val start: Int, val end: In // complete immediately if there is a single matching action val providers = layoutable.cell.getSubstituteActions() val params = CodeCompletionParameters(editor, newText) - val actions = providers.flatMap { it.flattenApplicableActions(params) }.toList() + val actions = editor.runRead { providers.flatMap { it.flattenApplicableActions(params) }.toList() } val matchingActions = actions .filter { it.getMatchingText() == newText } .applyShadowing() diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTemplate.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTemplate.kt index e5de5dd1..1e72da12 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTemplate.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTemplate.kt @@ -8,6 +8,7 @@ import org.modelix.model.api.INode import org.modelix.model.api.INodeReference import org.modelix.model.api.IProperty import org.modelix.model.api.IReferenceLink +import org.modelix.model.api.isSubConceptOf import org.modelix.scopes.ScopeAspect import kotlin.jvm.JvmName @@ -333,7 +334,7 @@ class ReferenceCellTemplate( return sourceNode.getReferenceTarget(link) } override fun getInstantiationActions(location: INonExistingNode, parameters: CodeCompletionParameters): List { - val sourceNode = NonExistingChild(location.getParent()!!, location.getContainmentLink()!!, location.index()).ofSubConcept(concept) + val sourceNode = location.replacement(concept) val scope = ScopeAspect.getScope(sourceNode, link) val targets = scope.getVisibleElements(sourceNode, link) return targets.map { target -> @@ -358,8 +359,7 @@ class ReferenceCellTemplate( val sourceNode = location.getOrCreateNode(concept) sourceNode.setReferenceTarget(link, target.getOrCreateNode()) editor.selectAfterUpdate { - CaretPositionPolicy(createCellReference(sourceNode)) - .getBestSelection(editor) + CaretPositionPolicy(createCellReference(sourceNode)).getBestSelection(editor) } } } @@ -384,6 +384,13 @@ class ChildCellTemplate( private var separatorCell: CellTemplate? = null + /** + * When pressing ENTER a new node of this concept is inserted. + * If this concept is null, then a special placeholder cell is added to the editor and a node can be created using + * the code completion menu. + */ + var newLineConcept: IConcept? = null + fun setSeparator(newSeparator: CellTemplate) { this.separatorCell = newSeparator reference?.let { newSeparator.setReference(SeparatorCellTemplateReference(it)) } @@ -398,7 +405,7 @@ class ChildCellTemplate( val childNodes = getChildNodes(node) val substitutionPlaceholder = context.editorState.substitutionPlaceholderPositions[createCellReference(node)] val placeholderIndex = substitutionPlaceholder?.index?.coerceIn(0..childNodes.size) ?: 0 - val addSubstitutionPlaceholder: (Int) -> Unit = { index -> + fun addSubstitutionPlaceholder(index: Int) { val isDefaultPlaceholder = childNodes.isEmpty() val placeholderText = if (isDefaultPlaceholder) "" else "" val placeholder = TextCellData("", placeholderText) @@ -413,10 +420,12 @@ class ChildCellTemplate( placeholder.properties[CommonCellProperties.tabTarget] = true cell.addChild(placeholder) } - val addInsertActionCell: (Int) -> Unit = { index -> + fun addInsertActionCell(index: Int) { if (link.isMultiple) { val actionCell = CellData() - val action = InsertSubstitutionPlaceholderAction(context.editorState, createCellReference(node), index) + val action = newLineConcept?.let { + InstantiateNodeAction(NonExistingChild(ExistingNode(node), link, index), it) + } ?: InsertSubstitutionPlaceholderAction(context.editorState, createCellReference(node), index) actionCell.properties[CellActionProperties.insert] = action cell.addChild(actionCell) } @@ -460,8 +469,20 @@ class ChildCellTemplate( fun getChildNodes(node: INode) = node.getChildren(link).toList() override fun getInstantiationActions(location: INonExistingNode, parameters: CodeCompletionParameters): List? { - // TODO - return listOf() + // This cell produces "wrappers". + // For example, in MPS baseLanguage you can type "int" (which is a Type) where a Statement is expected, + // and it is automatically wrapped with a LocalVariableDeclarationStatement. + // If the to-be-wrapped concept is already allowed without the wrapper, then it's not necessary to generate + // such an action. + if (link.targetConcept.isSubConceptOf(location.expectedConcept())) { + return emptyList() + } + + // prevent endless nesting + if (location.ancestors(true).takeWhile { it.getNode() == null }.count() > 10) return emptyList() + + val childNode = NonExistingChild(location.replacement(concept), link) + return listOf(ReplaceNodeActionProvider(childNode)) } } data class PlaceholderCellReference(val childCellRef: TemplateCellReference) : CellReference() @@ -483,6 +504,19 @@ class InsertSubstitutionPlaceholderAction( } } +class InstantiateNodeAction(val location: INonExistingNode, val concept: IConcept) : ICellAction { + override fun isApplicable(): Boolean = true + + override fun execute(editor: EditorComponent) { + val newNode = location.getExistingAncestor()!!.getArea().executeWrite { + location.replaceNode(concept) + } + editor.selectAfterUpdate { + CaretPositionPolicy(newNode).getBestSelection(editor) + } + } +} + fun CellTemplate.firstLeaf(): CellTemplate = if (getChildren().isEmpty()) this else getChildren().first().firstLeaf() fun CellTemplate.descendants(includeSelf: Boolean = false): Sequence { return if (includeSelf) { diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTemplateBuilder.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTemplateBuilder.kt index 036a896f..d9a4916d 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTemplateBuilder.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTemplateBuilder.kt @@ -265,19 +265,18 @@ open class CellTemplateBuilder(val template: CellTe ChildCellTemplate(template.concept, this).builder().also(body).template.also(template::addChild) } - fun ITypedChildListLink<*>.vertical(body: CellTemplateBuilder.() -> Unit = {}) { + fun ITypedChildListLink<*>.vertical(body: ChildCellTemplateBuilder.() -> Unit = {}) { this.untyped().vertical(body) } - fun IChildLink.vertical(body: CellTemplateBuilder.() -> Unit = {}) { - // TODO add layout information + fun IChildLink.vertical(body: ChildCellTemplateBuilder.() -> Unit = {}) { horizontal(separator = null) { template.properties[CommonCellProperties.layout] = ECellLayout.VERTICAL body() } } - fun ITypedChildListLink<*>.horizontal(separator: String? = ",", body: CellTemplateBuilder.() -> Unit = {}) { + fun ITypedChildListLink<*>.horizontal(separator: String? = ",", body: ChildCellTemplateBuilder.() -> Unit = {}) { this.untyped().horizontal(separator, body) } @@ -352,6 +351,10 @@ class ChildCellTemplateBuilder( CollectionCellTemplate(template.concept).also { body(it.builder()) }, ) } + + fun newLineConcept(newLineConcept: IConcept) { + (template as ChildCellTemplate).newLineConcept = newLineConcept + } } class ReferenceCellTemplateBuilder( diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/EditorEngine.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/EditorEngine.kt index c8c02263..a3b082c8 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/EditorEngine.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/EditorEngine.kt @@ -119,7 +119,7 @@ class EditorEngine(incrementalEngine: IncrementalEngine? = null) { } } - private fun resolveConceptEditor(concept: IConcept?): List { + fun resolveConceptEditor(concept: IConcept?): List { if (concept == null) return listOf(defaultConceptEditor) val editors = concept.getAllConcepts().firstNotNullOfOrNull { superConcept -> val conceptReference = superConcept.getReference() diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/INonExistingNode.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/INonExistingNode.kt index 72f7717b..c0ade64b 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/INonExistingNode.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/INonExistingNode.kt @@ -37,39 +37,33 @@ fun INonExistingNode.commonAncestor(otherNode: INonExistingNode): INonExistingNo return null } -data class SpecializedNonExistingNode(val node: INonExistingNode, val subConcept: IConcept) : INonExistingNode { - override fun getExistingAncestor(): INode? = node.getExistingAncestor() +data class NodeReplacement(val nodeToReplace: INonExistingNode, val replacementConcept: IConcept) : INonExistingNode { + override fun getExistingAncestor(): INode? = nodeToReplace.getExistingAncestor() - override fun getParent(): INonExistingNode? = node.getParent() + override fun getParent(): INonExistingNode? = nodeToReplace.getParent() - override fun getContainmentLink(): IChildLink? = node.getContainmentLink() + override fun getContainmentLink(): IChildLink? = nodeToReplace.getContainmentLink() - override fun index(): Int = node.index() + override fun index(): Int = nodeToReplace.index() override fun replaceNode(subConcept: IConcept?): INode { - return node.replaceNode(coerceOutputConcept(subConcept)) + return nodeToReplace.replaceNode(coerceOutputConcept(subConcept)) } override fun getOrCreateNode(subConcept: IConcept?): INode { - return node.getOrCreateNode(coerceOutputConcept(subConcept)) + return replaceNode(subConcept) } override fun getNode(): INode? { - return node.getNode() + return null } override fun expectedConcept(): IConcept { - return subConcept + return replacementConcept } } -fun INonExistingNode.ofSubConcept(subConcept: IConcept): INonExistingNode { - return if (expectedConcept().isSubConceptOf(subConcept)) { - this - } else { - SpecializedNonExistingNode(this, subConcept) - } -} +fun INonExistingNode.replacement(newConcept: IConcept): INonExistingNode = NodeReplacement(this, newConcept) fun INonExistingNode.coerceOutputConcept(subConcept: IConcept?): IConcept? { val expectedConcept = expectedConcept() @@ -100,8 +94,7 @@ data class ExistingNode(private val node: INode) : INonExistingNode { } override fun getOrCreateNode(subConcept: IConcept?): INode { - val outputConcept = coerceOutputConcept(subConcept) - return if (node.isInstanceOf(outputConcept)) { + return if (subConcept == null || node.isInstanceOf(coerceOutputConcept(subConcept))) { node } else { replaceNode(subConcept) @@ -130,9 +123,7 @@ data class NonExistingChild(private val parent: INonExistingNode, val link: IChi override fun replaceNode(subConcept: IConcept?): INode { val parentNode = parent.getOrCreateNode(null) - val existing = parentNode.getChildren(link).toList().getOrNull(index) val newNode = parentNode.addNewChild(link, index, coerceOutputConcept(subConcept)) - existing?.remove() return newNode } diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/IncrementalVirtualDOMBuilder.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/IncrementalVirtualDOMBuilder.kt index 6fd5b699..a0a7e161 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/IncrementalVirtualDOMBuilder.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/IncrementalVirtualDOMBuilder.kt @@ -44,9 +44,11 @@ class IncrementalVirtualDOMBuilder(val document: IVirtualDom, existingRootElemen if (index < parent.childNodes.size) { val actualChild = parent.childNodes.get(index) if (actualChild != expectedChild) { + expectedChild.parent?.removeChild(expectedChild) parent.insertBefore(expectedChild, actualChild) } } else { + expectedChild.parent?.removeChild(expectedChild) parent.appendChild(expectedChild) } } diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/ReplaceNodeActionProvider.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/ReplaceNodeActionProvider.kt index 81a26b29..88b081da 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/ReplaceNodeActionProvider.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/ReplaceNodeActionProvider.kt @@ -12,7 +12,7 @@ data class ReplaceNodeActionProvider(val location: INonExistingNode) : ICodeComp val allowedConcepts = expectedConcept.getAllSubConcepts(true) .filterNot { it.isAbstract() } .filter { concept -> - val newNode = SpecializedNonExistingNode(NonExistingChild(location.getParent()!!, location.getContainmentLink()!!, location.index()), concept) + val newNode = location.replacement(concept) ConstraintsAspect.canCreate(newNode) } val cellModels = allowedConcepts.map { concept -> diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/VirtualDom.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/VirtualDom.kt index e8da6cb4..24780620 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/VirtualDom.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/VirtualDom.kt @@ -160,9 +160,33 @@ class VirtualDom(override val ui: IVirtualDomUI, val idPrefix: String = "") : IV } open inner class Node : IVirtualDom.Node { + private var wasModified: Boolean = true + private var wasAnyDescendantModified: Boolean = true private val userObjects: MutableMap = HashMap() - override var parent: IVirtualDom.Node? = null - override val childNodes: MutableList = ArrayList() + override var parent: Node? = null + override val childNodes: MutableList = ArrayList() + + fun resetModificationMarker() { + if (!wasModified && wasAnyDescendantModified) return + childNodes.forEach { it.resetModificationMarker() } + wasAnyDescendantModified = false + wasModified = false + } + + fun markModified() { + wasModified = true + parent?.markDescendantModified() + } + + fun markDescendantModified() { + if (wasAnyDescendantModified) return + wasAnyDescendantModified = true + parent?.markDescendantModified() + } + + fun wasAnyDescendantModified() = wasAnyDescendantModified + + fun wasModified(): Boolean = wasModified override fun getVDom(): IVirtualDom = this@VirtualDom @@ -180,24 +204,46 @@ class VirtualDom(override val ui: IVirtualDomUI, val idPrefix: String = "") : IV if (referenceNode == null) return appendChild(newNode) val index = childNodes.indexOf(referenceNode) require(index >= 0) { "$referenceNode is not a child of $this" } - childNodes.add(index, newNode) + addChild(index, newNode) return newNode } + override fun appendChild(child: IVirtualDom.Node): IVirtualDom.Node { - childNodes += child + addChild(childNodes.size, child) return child } + override fun replaceChild(newChild: IVirtualDom.Node, oldChild: IVirtualDom.Node): IVirtualDom.Node { val index = childNodes.indexOf(oldChild) require(index >= 0) { "$oldChild is not a child of $this" } - childNodes[index] = newChild + + removeChildAt(index) + addChild(index, newChild) return oldChild } + override fun removeChild(child: IVirtualDom.Node): IVirtualDom.Node { - require(childNodes.remove(child)) { "$child is not a child of $this" } + val index = childNodes.indexOf(child) + require(index >= 0) { "$child is not a child of $this" } + removeChildAt(index) return child } + fun removeChildAt(index: Int) { + val child = childNodes.removeAt(index) + child.parent = null + markModified() + } + + fun addChild(index: Int, child: IVirtualDom.Node) { + require(child is Node) + check(child.parent == null) { "Node is already attached to a parent node" } + childNodes.add(index, child) + child.parent = this + markModified() + if (child.wasModified() || child.wasAnyDescendantModified()) markDescendantModified() + } + override fun remove() { parent?.removeChild(this) } @@ -208,21 +254,54 @@ class VirtualDom(override val ui: IVirtualDomUI, val idPrefix: String = "") : IV override fun getAttributeNames(): Array = attributes.keys.toTypedArray() override fun getAttribute(qualifiedName: String): String? = attributes[qualifiedName] override fun setAttribute(qualifiedName: String, value: String) { + if (attributes[qualifiedName] == value) return attributes[qualifiedName] = value if (qualifiedName == "id") { elementsMap[value] = this } + markModified() + } + override fun removeAttribute(qualifiedName: String) { + if (attributes.remove(qualifiedName) != null) { + markModified() + } } - override fun removeAttribute(qualifiedName: String) { attributes.remove(qualifiedName) } override fun getAttributes(): Map = attributes override fun getInnerBounds(): Bounds = ui.getInnerBounds(this) override fun getOuterBounds(): Bounds = ui.getOuterBounds(this) + + override fun toString(): String { + return buildString { + append("<$tagName>") + attributes.forEach { attribute -> + append(" ") + append(attribute.key) + append("=\"") + append(attribute.value) + append("\"") + } + append(">") + childNodes.forEach { child -> + append(child) + } + append("") + } + } } inner class HTMLElement(tagName: String) : Element(tagName), IVirtualDom.HTMLElement inner class Text : Node(), IVirtualDom.Text { override var textContent: String? = null + set(value) { + if (field == value) return + field = value + markModified() + } + + override fun toString(): String { + return textContent ?: "" + } } } diff --git a/settings.gradle.kts b/settings.gradle.kts index 0ebdc8c4..c7e58dd3 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -35,4 +35,5 @@ include("mps") include("projectional-editor-ssr-client") include("projectional-editor-ssr-common") include("projectional-editor-ssr-mps") +include("projectional-editor-ssr-mps-languages") include("projectional-editor-ssr-server")