diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index 7f9264c9..571cc1ad 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -5,8 +5,17 @@ on: jobs: pre-commit: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v6 - - uses: actions/setup-python@v6 - - uses: pre-commit/action@v3.0.1 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 + - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6 + with: + node-version-file: '.nvmrc' + - uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5 + with: + path: node_modules + key: release-${{ hashFiles('package.json') }}-${{ hashFiles('package-lock.json') }} + - name: Install dependencies + run: npm ci + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 + - uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bbc970c4..bfc9723a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,8 +4,9 @@ default_install_hook_types: - commit-msg repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v6.0.0 hooks: +# - id: trailing-whitespace # disabled, because it breaks string literals # - id: end-of-file-fixer # disabled, because it modifies MPS XML files - id: check-toml - id: check-yaml @@ -16,16 +17,18 @@ repos: - id: mixed-line-ending # - 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 + rev: v9.24.0 hooks: - id: commitlint stages: [commit-msg] additional_dependencies: ["@commitlint/config-angular"] args: ["--config", "./commitlint.config.js"] - repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks - rev: v2.12.0 + rev: v2.16.0 hooks: - id: pretty-format-kotlin args: - - --ktlint-version=0.50.0 + - --ktlint-version=1.8.0 - --autofix + additional_dependencies: + - setuptools diff --git a/README.md b/README.md index 4a3fd0a6..7c25bb28 100644 --- a/README.md +++ b/README.md @@ -46,4 +46,4 @@ You can define a custom editor also in the special `modelix` model using the lan During development on this project you can run the `installMpsDevPlugins` task to build and install only those plugins that don't contain any MPS modules. -Then open the project in the `mps` folder with MPS to load and edit the MPS modules. +Then open the project in the `mps` folder with MPS to load and edit the MPS modules. diff --git a/build.gradle.kts b/build.gradle.kts index bc596f4a..d6f28707 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -21,6 +21,7 @@ plugins { id("com.dorongold.task-tree") version "4.0.1" alias(libs.plugins.kotlin.multiplatform) apply false alias(libs.plugins.kotlin.serialization) apply false + alias(libs.plugins.kotlin.rpc) apply false id("org.jetbrains.intellij") version "1.17.4" apply false alias(libs.plugins.npm.publish) apply false } @@ -61,7 +62,8 @@ fun computeVersion(): Any { } val tsModelApiPath = rootDir.parentFile.resolve("modelix.core").resolve("ts-model-api") -val tsModelApiVersion = libs.versions.modelixCore.get() // if (tsModelApiPath.exists()) "file:${tsModelApiPath.absolutePath}" else libs.versions.modelixCore.get() +val tsModelApiVersion = libs.versions.modelixCore.get() +// if (tsModelApiPath.exists()) "file:${tsModelApiPath.absolutePath}" else libs.versions.modelixCore.get() ext.set("ts-model-api.version", tsModelApiVersion) subprojects { @@ -83,11 +85,12 @@ allprojects { if (project.hasProperty("artifacts.itemis.cloud.user")) { maven { name = "itemis" - url = if (version.toString().contains("SNAPSHOT")) { - uri("https://artifacts.itemis.cloud/repository/maven-mps-snapshots/") - } else { - uri("https://artifacts.itemis.cloud/repository/maven-mps-releases/") - } + url = + if (version.toString().contains("SNAPSHOT")) { + uri("https://artifacts.itemis.cloud/repository/maven-mps-snapshots/") + } else { + uri("https://artifacts.itemis.cloud/repository/maven-mps-releases/") + } credentials { username = project.findProperty("artifacts.itemis.cloud.user").toString() password = project.findProperty("artifacts.itemis.cloud.pw").toString() @@ -151,7 +154,7 @@ rootProject.plugins.withType(org.jetbrains.kotlin.gradle.targets.js.yarn.YarnPlu copyMps() // make all 'packJsPackage' tasks depend on all 'kotlinNodeJsSetup' tasks, because gradle complained about this being missing -tasks.register("setupNodeEverywhere") { +tasks.register("setupNodeEverywhere") { dependsOn(":kernelf-apigen:kotlinNodeJsSetup") dependsOn(":kernelf-editor:kotlinNodeJsSetup") dependsOn(":parser:kotlinNodeJsSetup") diff --git a/buildSrc/src/main/kotlin/org/modelix/CopyMps.kt b/buildSrc/src/main/kotlin/org/modelix/CopyMps.kt index 4e404d4a..024f1846 100644 --- a/buildSrc/src/main/kotlin/org/modelix/CopyMps.kt +++ b/buildSrc/src/main/kotlin/org/modelix/CopyMps.kt @@ -28,7 +28,11 @@ import java.util.zip.ZipInputStream val Project.mpsMajorVersion: String get() { if (project != rootProject) return rootProject.mpsMajorVersion 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") + ?: project + .findProperty("mps.version") + ?.toString() + ?.takeIf { it.isNotEmpty() } + ?.replace(Regex("""(20\d\d\.\d+).*"""), "$1") ?: "2024.1" } @@ -65,10 +69,11 @@ val Project.mpsHomeDir: Provider get() { } 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/MPS$mpsMajorVersion/plugins/") }, - ) + 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/MPS$mpsMajorVersion/plugins/") }, + ) return candidates.firstOrNull { it.isDirectory } } @@ -105,7 +110,13 @@ fun Project.copyMps(): File { // The IntelliJ gradle plugin doesn't search in jar files when reading plugin descriptors, but the IDE does. // Copy the XML files from the jars to the META-INF folders to fix that. - for (pluginFolder in (mpsHomeDir.get().asFile.resolve("plugins").listFiles() ?: emptyArray())) { + for (pluginFolder in ( + mpsHomeDir + .get() + .asFile + .resolve("plugins") + .listFiles() ?: emptyArray() + )) { val jars = (pluginFolder.resolve("lib").listFiles() ?: emptyArray()).filter { it.extension == "jar" } for (jar in jars) { jar.inputStream().use { diff --git a/editor-common-mps/build.gradle.kts b/editor-common-mps/build.gradle.kts index 249790a4..a14701ce 100644 --- a/editor-common-mps/build.gradle.kts +++ b/editor-common-mps/build.gradle.kts @@ -64,11 +64,12 @@ tasks { val pluginDir = mpsPluginsDir if (pluginDir != null) { - val installMpsPlugin = register("installMpsPlugin") { - dependsOn(prepareSandbox) - from(project.layout.buildDirectory.dir("idea-sandbox/plugins/${project.name}")) - into(pluginDir.resolve(project.name)) - } + val installMpsPlugin = + register("installMpsPlugin") { + dependsOn(prepareSandbox) + from(project.layout.buildDirectory.dir("idea-sandbox/plugins/${project.name}")) + into(pluginDir.resolve(project.name)) + } register("installMpsDevPlugins") { dependsOn(installMpsPlugin) } diff --git a/editor-common-mps/src/main/kotlin/org/modelix/mps/editor/common/KtorUtils.kt b/editor-common-mps/src/main/kotlin/org/modelix/mps/editor/common/KtorUtils.kt index 01f95b2b..867a6573 100644 --- a/editor-common-mps/src/main/kotlin/org/modelix/mps/editor/common/KtorUtils.kt +++ b/editor-common-mps/src/main/kotlin/org/modelix/mps/editor/common/KtorUtils.kt @@ -10,19 +10,25 @@ import io.ktor.server.netty.NettyApplicationEngine import io.ktor.util.logging.KtorSimpleLogger import kotlinx.coroutines.GlobalScope -fun embeddedServer(port: Int, classLoader: ClassLoader? = null, module: Application.() -> Unit): EmbeddedServer { +fun embeddedServer( + port: Int, + classLoader: ClassLoader? = null, + module: Application.() -> Unit, +): EmbeddedServer { val portParam = port val classLoaderParam = classLoader val moduleParam = module - val environment = applicationEnvironment { - if (classLoaderParam != null) this.classLoader = classLoaderParam - this.log = KtorSimpleLogger("ktor.application") - } - val applicationProperties = serverConfig(environment) { - this.module(moduleParam) - this.parentCoroutineContext = GlobalScope.coroutineContext - this.watchPaths = emptyList() - } + val environment = + applicationEnvironment { + if (classLoaderParam != null) this.classLoader = classLoaderParam + this.log = KtorSimpleLogger("ktor.application") + } + val applicationProperties = + serverConfig(environment) { + this.module(moduleParam) + this.parentCoroutineContext = GlobalScope.coroutineContext + this.watchPaths = emptyList() + } return io.ktor.server.engine.embeddedServer(Netty, applicationProperties, configure = { this.connectors += EngineConnectorBuilder().also { it.port = portParam } }) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index def6a4a7..c7422079 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,11 +7,13 @@ modelix-mps-buildtools = { id = "org.modelix.mps.build-tools", version.ref = "mo kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } +kotlin-rpc = { id = "org.jetbrains.kotlinx.rpc.plugin", version.ref = "kotlinx-rpc" } [versions] modelixCore = "18.2.0" modelixBuildtools="2.0.1" kotlin = "2.2.21" +kotlinx-rpc = "0.10.1" [libraries] modelix-model-api = { group = "org.modelix", name = "model-api", version.ref = "modelixCore" } @@ -31,3 +33,11 @@ slf4j-api = { group = "org.slf4j", name = "slf4j-api", version = "2.0.17" } kotlin-collections-immutable = { group = "org.jetbrains.kotlinx", name = "kotlinx-collections-immutable", version = "0.4.0" } playwright = { group = "com.microsoft.playwright", name = "playwright", version = "1.58.0" } testcontainers = { group = "org.testcontainers", name = "testcontainers", version = "2.0.3" } + +# krpc +kotlinx-rpc-core = { module = "org.jetbrains.kotlinx:kotlinx-rpc-core", version.ref = "kotlinx-rpc" } +kotlinx-rpc-krpc-client = { module = "org.jetbrains.kotlinx:kotlinx-rpc-krpc-client", version.ref = "kotlinx-rpc" } +kotlinx-rpc-krpc-server = { module = "org.jetbrains.kotlinx:kotlinx-rpc-krpc-server", version.ref = "kotlinx-rpc" } +kotlinx-rpc-krpc-ktor-client = { module = "org.jetbrains.kotlinx:kotlinx-rpc-krpc-ktor-client", version.ref = "kotlinx-rpc" } +kotlinx-rpc-krpc-ktor-server = { module = "org.jetbrains.kotlinx:kotlinx-rpc-krpc-ktor-server", version.ref = "kotlinx-rpc" } +kotlinx-rpc-krpc-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-rpc-krpc-serialization-json", version.ref = "kotlinx-rpc" } diff --git a/interpreter-vm/src/commonMain/kotlin/org/modelix/interpreter/vm/core/Instructions.kt b/interpreter-vm/src/commonMain/kotlin/org/modelix/interpreter/vm/core/Instructions.kt index baf4e11b..3677fcd6 100644 --- a/interpreter-vm/src/commonMain/kotlin/org/modelix/interpreter/vm/core/Instructions.kt +++ b/interpreter-vm/src/commonMain/kotlin/org/modelix/interpreter/vm/core/Instructions.kt @@ -1,7 +1,9 @@ package org.modelix.interpreter.vm.core -class CallInstruction(val entryPoint: Instruction, val parameterCount: Int) : Instruction() { - +class CallInstruction( + val entryPoint: Instruction, + val parameterCount: Int, +) : Instruction() { override fun execute(state: VMState): VMState { var newFrame = StackFrame(returnTo = next) var newState = state @@ -16,7 +18,7 @@ class CallInstruction(val entryPoint: Instruction, val parameterCount: Int) : In } } -class ReturnInstruction() : Instruction() { +class ReturnInstruction : Instruction() { override fun execute(state: VMState): VMState { val (newCallStack, currentFrame) = state.callStack.popFrame() check(currentFrame.operandStack.size == 1) { "Operand stack should contain a single value, but was: " + currentFrame.operandStack } @@ -26,27 +28,29 @@ class ReturnInstruction() : Instruction() { } } -class PushConstantInstruction(val value: E) : Instruction() { - override fun execute(state: VMState): VMState { - return state.pushOperand(value) - } +class PushConstantInstruction( + val value: E, +) : Instruction() { + override fun execute(state: VMState): VMState = state.pushOperand(value) } -class StoreInstruction(val target: MemoryKey) : Instruction() { - override fun execute(state: VMState): VMState { - return state.popOperand().let { (value, newState) -> newState.writeMemory(target, value as E) } - } +class StoreInstruction( + val target: MemoryKey, +) : Instruction() { + override fun execute(state: VMState): VMState = state.popOperand().let { (value, newState) -> newState.writeMemory(target, value as E) } } -class LoadInstruction(val source: MemoryKey) : Instruction() { - override fun execute(state: VMState): VMState { - return state.pushOperand(state.readMemory(source)) - } +class LoadInstruction( + val source: MemoryKey, +) : Instruction() { + override fun execute(state: VMState): VMState = state.pushOperand(state.readMemory(source)) } -abstract class BinaryOperationInstruction() : Instruction() { - - abstract fun apply(arg1: Arg1, arg2: Arg2): Result +abstract class BinaryOperationInstruction : Instruction() { + abstract fun apply( + arg1: Arg1, + arg2: Arg2, + ): Result override fun execute(state: VMState): VMState { var newState: VMState = state @@ -65,43 +69,56 @@ abstract class BinaryOperationInstruction() : Instruction() } } -class AddIntegersInstruction() : BinaryOperationInstruction() { - override fun apply(arg1: Int, arg2: Int): Int { - return arg1 + arg2 - } +class AddIntegersInstruction : BinaryOperationInstruction() { + override fun apply( + arg1: Int, + arg2: Int, + ): Int = arg1 + arg2 } -class MultiplyIntegersInstruction() : BinaryOperationInstruction() { - override fun apply(arg1: Int, arg2: Int): Int { - return arg1 * arg2 - } +class MultiplyIntegersInstruction : BinaryOperationInstruction() { + override fun apply( + arg1: Int, + arg2: Int, + ): Int = arg1 * arg2 } -class JumpInstruction(val target: Instruction) : Instruction() { - override fun execute(state: VMState): VMState { - return state.copy(nextInstruction = target) - } +class JumpInstruction( + val target: Instruction, +) : Instruction() { + override fun execute(state: VMState): VMState = state.copy(nextInstruction = target) } -class ConditionalJumpInstruction(val condition: MemoryKey, val target: Instruction) : Instruction() { - override fun execute(state: VMState): VMState { - return if (state.readMemory(condition)) state.copy(nextInstruction = target) else state - } +class ConditionalJumpInstruction( + val condition: MemoryKey, + val target: Instruction, +) : Instruction() { + override fun execute(state: VMState): VMState = if (state.readMemory(condition)) state.copy(nextInstruction = target) else state } -class MoveInstruction(val source: MemoryKey, val target: MemoryKey) : Instruction() { - override fun execute(state: VMState): VMState { - return state.writeMemory(target, state.readMemory(source)) - } +class MoveInstruction( + val source: MemoryKey, + val target: MemoryKey, +) : Instruction() { + override fun execute(state: VMState): VMState = state.writeMemory(target, state.readMemory(source)) } class NoOpInstruction : Instruction() { - override fun execute(state: VMState): VMState { - return state - } + override fun execute(state: VMState): VMState = state } -data class NamedGlobalVarKey(val name: String) : MemoryKey(MemoryType.GLOBAL, name) -data class NamedLocalVarKey(val name: String) : MemoryKey(MemoryType.LOCAL, name) -data class ParameterKey(val index: Int) : MemoryKey(MemoryType.LOCAL, "parameter" + index) -data class ReturnValueKey(val index: Int) : MemoryKey(MemoryType.LOCAL, "returnValue" + index) +data class NamedGlobalVarKey( + val name: String, +) : MemoryKey(MemoryType.GLOBAL, name) + +data class NamedLocalVarKey( + val name: String, +) : MemoryKey(MemoryType.LOCAL, name) + +data class ParameterKey( + val index: Int, +) : MemoryKey(MemoryType.LOCAL, "parameter" + index) + +data class ReturnValueKey( + val index: Int, +) : MemoryKey(MemoryType.LOCAL, "returnValue" + index) diff --git a/interpreter-vm/src/commonMain/kotlin/org/modelix/interpreter/vm/core/InterpreterVM.kt b/interpreter-vm/src/commonMain/kotlin/org/modelix/interpreter/vm/core/InterpreterVM.kt index a76a504e..f61b7cd6 100644 --- a/interpreter-vm/src/commonMain/kotlin/org/modelix/interpreter/vm/core/InterpreterVM.kt +++ b/interpreter-vm/src/commonMain/kotlin/org/modelix/interpreter/vm/core/InterpreterVM.kt @@ -9,10 +9,13 @@ import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.plus import org.modelix.incremental.AtomicLong -class InterpreterVM(entryPoint: Instruction) { +class InterpreterVM( + entryPoint: Instruction, +) { private var state: VMState = VMState(nextInstruction = entryPoint) fun isTerminated() = state.nextInstruction == null + fun run(): VMState { while (!isTerminated()) { singleStep() @@ -25,7 +28,10 @@ class InterpreterVM(entryPoint: Instruction) { return instruction.execute(state.copy(nextInstruction = instruction.next)).also { state = it } } - fun writeMemory(key: MemoryKey, value: T) { + fun writeMemory( + key: MemoryKey, + value: T, + ) { state = state.writeMemory(key, value) } } @@ -36,68 +42,93 @@ data class VMState( val callStack: CallStack = CallStack().pushFrame(StackFrame(returnTo = null)), ) { fun readMemory(key: MemoryKey): T = key.memoryType.getMemory(this).read(key) - fun writeMemory(key: MemoryKey, value: T): VMState { - return key.memoryType.setMemory(this, key.memoryType.getMemory(this).write(key, value)) - } - fun updateCurrentFrame(body: (StackFrame) -> StackFrame): VMState { - return replaceCurrentFrame(body(callStack.currentFrame())) - } - fun replaceCurrentFrame(newFrame: StackFrame): VMState { - return copy(callStack = callStack.updateCurrentFrame(newFrame)) - } + + fun writeMemory( + key: MemoryKey, + value: T, + ): VMState = key.memoryType.setMemory(this, key.memoryType.getMemory(this).write(key, value)) + + fun updateCurrentFrame(body: (StackFrame) -> StackFrame): VMState = replaceCurrentFrame(body(callStack.currentFrame())) + + fun replaceCurrentFrame(newFrame: StackFrame): VMState = copy(callStack = callStack.updateCurrentFrame(newFrame)) + fun pushOperand(value: Any?): VMState = updateCurrentFrame { it.pushOperand(value) } - fun popOperand(): Pair { - return callStack.currentFrame().popOperand().let { (value, newFrame) -> value to replaceCurrentFrame(newFrame) } - } + + fun popOperand(): Pair = + callStack.currentFrame().popOperand().let { (value, newFrame) -> value to replaceCurrentFrame(newFrame) } } -data class Memory(private val data: PersistentMap, Any?> = persistentHashMapOf()) { +data class Memory( + private val data: PersistentMap, Any?> = persistentHashMapOf(), +) { fun hasKey(key: MemoryKey<*>): Boolean = data.containsKey(key) + fun read(key: MemoryKey): T { check(hasKey(key)) { "Uninitialized read: $key" } return data[key] as T } - fun write(key: MemoryKey, value: T): Memory = Memory(data.put(key, value)) + + fun write( + key: MemoryKey, + value: T, + ): Memory = Memory(data.put(key, value)) + fun getEntries(): ImmutableMap, Any?> = data } private val variableIdSequence = AtomicLong() -open class MemoryKey(val memoryType: MemoryType, val description: String = "var" + variableIdSequence.incrementAndGet()) { + +open class MemoryKey( + val memoryType: MemoryType, + val description: String = "var" + variableIdSequence.incrementAndGet(), +) { override fun toString() = description } abstract class MemoryType { abstract fun getMemory(state: VMState): Memory - abstract fun setMemory(state: VMState, memory: Memory): VMState + + abstract fun setMemory( + state: VMState, + memory: Memory, + ): VMState companion object { - val GLOBAL: MemoryType = object : MemoryType() { - override fun getMemory(state: VMState): Memory { - return state.globalMemory - } + val GLOBAL: MemoryType = + object : MemoryType() { + override fun getMemory(state: VMState): Memory = state.globalMemory - override fun setMemory(state: VMState, memory: Memory): VMState { - return state.copy(globalMemory = memory) - } - } - val LOCAL: MemoryType = object : MemoryType() { - override fun getMemory(state: VMState): Memory { - return state.callStack.currentFrame().localMemory + override fun setMemory( + state: VMState, + memory: Memory, + ): VMState = state.copy(globalMemory = memory) } + val LOCAL: MemoryType = + object : MemoryType() { + override fun getMemory(state: VMState): Memory = state.callStack.currentFrame().localMemory - override fun setMemory(state: VMState, memory: Memory): VMState { - return state.copy(callStack = state.callStack.updateCurrentFrame(state.callStack.currentFrame().copy(localMemory = memory))) + override fun setMemory( + state: VMState, + memory: Memory, + ): VMState = + state.copy(callStack = state.callStack.updateCurrentFrame(state.callStack.currentFrame().copy(localMemory = memory))) } - } } } -data class CallStack(val frames: PersistentList = persistentListOf()) { +data class CallStack( + val frames: PersistentList = persistentListOf(), +) { fun pushFrame(frame: StackFrame) = CallStack(frames + frame) + fun popFrame(): Pair = CallStack(frames.removeAt(frames.lastIndex)) to frames.last() + fun currentFrame() = frames.last() + fun updateCurrentFrame(newFrame: StackFrame) = CallStack(frames.set(frames.lastIndex, newFrame)) + fun getFrames(): ImmutableList = frames + fun size() = frames.size } @@ -106,12 +137,18 @@ data class StackFrame( val localMemory: Memory = Memory(), val operandStack: PersistentList = persistentListOf(), ) { - fun writeLocalMemory(key: MemoryKey, value: T): StackFrame = copy(localMemory = localMemory.write(key, value)) + fun writeLocalMemory( + key: MemoryKey, + value: T, + ): StackFrame = copy(localMemory = localMemory.write(key, value)) + fun pushOperand(value: Any?): StackFrame = copy(operandStack = operandStack.add(value)) + fun popOperand(): Pair = operandStack.last() to copy(operandStack = operandStack.removeAt(operandStack.lastIndex)) } abstract class Instruction { var next: Instruction? = null + abstract fun execute(state: VMState): VMState } diff --git a/interpreter-vm/src/commonMain/kotlin/org/modelix/interpreter/vm/core/ProgramBuilder.kt b/interpreter-vm/src/commonMain/kotlin/org/modelix/interpreter/vm/core/ProgramBuilder.kt index 971d0337..1baad4db 100644 --- a/interpreter-vm/src/commonMain/kotlin/org/modelix/interpreter/vm/core/ProgramBuilder.kt +++ b/interpreter-vm/src/commonMain/kotlin/org/modelix/interpreter/vm/core/ProgramBuilder.kt @@ -3,18 +3,19 @@ package org.modelix.interpreter.vm.core import kotlin.reflect.KProperty class ProgramBuilder { - private val functions: MutableMap = HashMap() - fun getFunction(key: Any): FunctionBuilder { - return checkNotNull(functions[key]) { "Function doesn't exist: $key" } - } + fun getFunction(key: Any): FunctionBuilder = checkNotNull(functions[key]) { "Function doesn't exist: $key" } - fun getOrBuildFunction(key: Any, body: FunctionBuilder.() -> Unit): FunctionBuilder { - return functions[key] ?: buildFunction(key, body) - } + fun getOrBuildFunction( + key: Any, + body: FunctionBuilder.() -> Unit, + ): FunctionBuilder = functions[key] ?: buildFunction(key, body) - fun buildFunction(key: Any, body: FunctionBuilder.() -> Unit): FunctionBuilder { + fun buildFunction( + key: Any, + body: FunctionBuilder.() -> Unit, + ): FunctionBuilder { check(functions[key] == null) { "Function already exists: $key" } val builder = FunctionBuilder() functions[key] = builder @@ -25,11 +26,18 @@ class ProgramBuilder { fun variable(type: MemoryType = MemoryType.LOCAL) = O(type) } -class O(private val type: MemoryType) { +class O( + private val type: MemoryType, +) { private var instance: MemoryKey? = null - operator fun getValue(thisRef: Nothing?, property: KProperty<*>): MemoryKey { - return instance ?: MemoryKey(type, property.name).also { instance = it } - } + + operator fun getValue( + thisRef: Nothing?, + property: KProperty<*>, + ): MemoryKey = + instance ?: MemoryKey(type, property.name).also { + instance = it + } } class FunctionBuilder { @@ -44,11 +52,12 @@ class FunctionBuilder { } } - fun getEntryPoint(): Instruction { - return firstInstruction ?: NoOpInstruction().also { firstInstruction = it } - } + fun getEntryPoint(): Instruction = firstInstruction ?: NoOpInstruction().also { firstInstruction = it } - fun load(value: T, variable: MemoryKey) { + fun load( + value: T, + variable: MemoryKey, + ) { addInstruction(PushConstantInstruction(value)) addInstruction(StoreInstruction(variable)) } diff --git a/interpreter-vm/src/commonTest/kotlin/org/modelix/interpreter.vm.core/InterpreterTest.kt b/interpreter-vm/src/commonTest/kotlin/org/modelix/interpreter.vm.core/InterpreterTest.kt index 78ac22d6..ee3eab87 100644 --- a/interpreter-vm/src/commonTest/kotlin/org/modelix/interpreter.vm.core/InterpreterTest.kt +++ b/interpreter-vm/src/commonTest/kotlin/org/modelix/interpreter.vm.core/InterpreterTest.kt @@ -4,17 +4,18 @@ import kotlin.test.Test import kotlin.test.assertEquals class InterpreterTest { - @Test fun addition() { val c = MemoryKey(MemoryType.GLOBAL, "c") - val entryPoint = ProgramBuilder().buildFunction("main") { - addInstruction(PushConstantInstruction(10)) - addInstruction(PushConstantInstruction(20)) - addInstruction(AddIntegersInstruction()) - addInstruction(StoreInstruction(c)) - }.getEntryPoint() + val entryPoint = + ProgramBuilder() + .buildFunction("main") { + addInstruction(PushConstantInstruction(10)) + addInstruction(PushConstantInstruction(20)) + addInstruction(AddIntegersInstruction()) + addInstruction(StoreInstruction(c)) + }.getEntryPoint() val finalState = InterpreterVM(entryPoint).run() val computationResult = finalState.readMemory(c) @@ -28,12 +29,14 @@ class InterpreterTest { val b = ParameterKey(1) val c = MemoryKey(MemoryType.GLOBAL, "c") - val entryPoint = ProgramBuilder().buildFunction("main") { - addInstruction(LoadInstruction(ParameterKey(1))) - addInstruction(LoadInstruction(ParameterKey(0))) - addInstruction(AddIntegersInstruction()) - addInstruction(StoreInstruction(c)) - }.getEntryPoint() + val entryPoint = + ProgramBuilder() + .buildFunction("main") { + addInstruction(LoadInstruction(ParameterKey(1))) + addInstruction(LoadInstruction(ParameterKey(0))) + addInstruction(AddIntegersInstruction()) + addInstruction(StoreInstruction(c)) + }.getEntryPoint() val vm = InterpreterVM(entryPoint) vm.writeMemory(a, 10) @@ -46,28 +49,31 @@ class InterpreterTest { @Test fun functionCall() { - val entryPoint = ProgramBuilder().run { - val plusFunction = buildFunction("plus") { - addInstruction(LoadInstruction(ParameterKey(0))) - addInstruction(LoadInstruction(ParameterKey(1))) - addInstruction(AddIntegersInstruction()) - addInstruction(ReturnInstruction()) - } - val mulFunction = buildFunction("mul") { - addInstruction(LoadInstruction(ParameterKey(0))) - addInstruction(LoadInstruction(ParameterKey(1))) - addInstruction(MultiplyIntegersInstruction()) - addInstruction(ReturnInstruction()) + val entryPoint = + ProgramBuilder().run { + val plusFunction = + buildFunction("plus") { + addInstruction(LoadInstruction(ParameterKey(0))) + addInstruction(LoadInstruction(ParameterKey(1))) + addInstruction(AddIntegersInstruction()) + addInstruction(ReturnInstruction()) + } + val mulFunction = + buildFunction("mul") { + addInstruction(LoadInstruction(ParameterKey(0))) + addInstruction(LoadInstruction(ParameterKey(1))) + addInstruction(MultiplyIntegersInstruction()) + addInstruction(ReturnInstruction()) + } + buildFunction("main") { + addInstruction(PushConstantInstruction(31)) + addInstruction(PushConstantInstruction(13)) + addInstruction(PushConstantInstruction(7)) + addInstruction(CallInstruction(plusFunction.getEntryPoint(), 2)) + addInstruction(CallInstruction(mulFunction.getEntryPoint(), 2)) + addInstruction(StoreInstruction(NamedGlobalVarKey("finalResult"))) + }.getEntryPoint() } - buildFunction("main") { - addInstruction(PushConstantInstruction(31)) - addInstruction(PushConstantInstruction(13)) - addInstruction(PushConstantInstruction(7)) - addInstruction(CallInstruction(plusFunction.getEntryPoint(), 2)) - addInstruction(CallInstruction(mulFunction.getEntryPoint(), 2)) - addInstruction(StoreInstruction(NamedGlobalVarKey("finalResult"))) - }.getEntryPoint() - } val vm = InterpreterVM(entryPoint) val finalState = vm.run() diff --git a/kernelf-angular-demo/build.gradle.kts b/kernelf-angular-demo/build.gradle.kts index 1b84a51a..1ff5a2c9 100644 --- a/kernelf-angular-demo/build.gradle.kts +++ b/kernelf-angular-demo/build.gradle.kts @@ -26,22 +26,24 @@ tasks.named("assemble") { dependsOn("npm_run_build") } -val updateTsModelApiVersion = tasks.create("updateTsModelApiVersion") { - doLast { - val localPath = rootDir.parentFile.resolve("modelix.core").resolve("ts-model-api") - val packageJsonFile = projectDir.resolve("package.json") - var text = packageJsonFile.readText() - println("ts-model-api path: $localPath") - val replacement = if (localPath.exists()) { - """"@modelix/ts-model-api": "file:${localPath.relativeTo(projectDir).toString().replace("\\", "\\\\")}"""" - } else { - """"@modelix/ts-model-api": "${rootProject.property("ts-model-api.version")}"""" +val updateTsModelApiVersion = + tasks.create("updateTsModelApiVersion") { + doLast { + val localPath = rootDir.parentFile.resolve("modelix.core").resolve("ts-model-api") + val packageJsonFile = projectDir.resolve("package.json") + var text = packageJsonFile.readText() + println("ts-model-api path: $localPath") + val replacement = + if (localPath.exists()) { + """"@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 }) + packageJsonFile.writeText(text) } - println("ts-model-api version: $replacement") - text = text.replace(Regex(""""@modelix/ts-model-api": ".*""""), { replacement }) - packageJsonFile.writeText(text) } -} tasks.withType { dependsOn(updateTsModelApiVersion) @@ -49,9 +51,10 @@ tasks.withType { dependsOn(":kernelf-editor:packJsPackage") } -val updateTask = tasks.register("updateOwnDependencies") { - args = listOf("update", "@modelix/kernelf-editor") -} +val updateTask = + tasks.register("updateOwnDependencies") { + args = listOf("update", "@modelix/kernelf-editor") + } tasks.withType { dependsOn(updateTask) diff --git a/kernelf-angular-demo/package.json b/kernelf-angular-demo/package.json index 595aca09..5a474d78 100644 --- a/kernelf-angular-demo/package.json +++ b/kernelf-angular-demo/package.json @@ -26,7 +26,7 @@ "@angular/platform-browser": "^14.0.0", "@angular/platform-browser-dynamic": "^14.0.0", "@angular/router": "^14.0.0", - "@modelix/ts-model-api": "16.2.1", + "@modelix/ts-model-api": "file:../../modelix.core/ts-model-api", "angular-split": "^14.0.0", "@modelix/kernelf-editor": "file:../kernelf-editor/build/packages/modelix-kernelf-editor.tgz", "rxjs": "~7.5.0", diff --git a/kernelf-editor/build.gradle.kts b/kernelf-editor/build.gradle.kts index 3bc3f597..4f0ec9e1 100644 --- a/kernelf-editor/build.gradle.kts +++ b/kernelf-editor/build.gradle.kts @@ -95,7 +95,10 @@ kotlin { } } -fun fixSourceMap(sourcesDir: File, sourceMapFile: File) { +fun fixSourceMap( + sourcesDir: File, + sourceMapFile: File, +) { if (!sourcesDir.exists()) return if (!sourceMapFile.exists()) return val json = JsonParser.parseString(sourceMapFile.readText()).asJsonObject @@ -153,33 +156,34 @@ tasks.named("packJsPackage") { val productionLibraryByKotlinOutputDirectory = layout.buildDirectory.dir("compileSync/js/main/productionLibrary/kotlin") val preparedProductionLibraryOutputDirectory = layout.buildDirectory.dir("npmPublication") -val patchTypesScriptInProductionLibrary = tasks.register("patchTypesScriptInProductionLibrary") { - dependsOn("compileProductionLibraryKotlinJs") - inputs.dir(productionLibraryByKotlinOutputDirectory) - outputs.dir(preparedProductionLibraryOutputDirectory) - outputs.cacheIf { true } - doLast { - // Delete old data - delete { - delete(preparedProductionLibraryOutputDirectory) - } +val patchTypesScriptInProductionLibrary = + tasks.register("patchTypesScriptInProductionLibrary") { + dependsOn("compileProductionLibraryKotlinJs") + inputs.dir(productionLibraryByKotlinOutputDirectory) + outputs.dir(preparedProductionLibraryOutputDirectory) + outputs.cacheIf { true } + doLast { + // Delete old data + delete { + delete(preparedProductionLibraryOutputDirectory) + } - // Copy over library create by Kotlin - copy { - from(productionLibraryByKotlinOutputDirectory) - into(preparedProductionLibraryOutputDirectory) - } + // Copy over library create by Kotlin + copy { + from(productionLibraryByKotlinOutputDirectory) + into(preparedProductionLibraryOutputDirectory) + } - // Add correct TypeScript imports. - val typescriptDeclaration = - preparedProductionLibraryOutputDirectory.get().file("modelix.editor-kernelf-editor.d.ts").asFile - val originalTypescriptDeclarationContent = typescriptDeclaration.readText() - typescriptDeclaration.writer().use { - it.appendLine("""import { INodeJS } from "@modelix/ts-model-api";""").appendLine() - it.append(originalTypescriptDeclarationContent) + // Add correct TypeScript imports. + val typescriptDeclaration = + preparedProductionLibraryOutputDirectory.get().file("modelix.editor-kernelf-editor.d.ts").asFile + val originalTypescriptDeclarationContent = typescriptDeclaration.readText() + typescriptDeclaration.writer().use { + it.appendLine("""import { INodeJS } from "@modelix/ts-model-api";""").appendLine() + it.append(originalTypescriptDeclarationContent) + } } } -} npmPublish { // registries { @@ -208,7 +212,8 @@ npmPublish { tasks.named("packJsPackage") { doLast { val packagesDir = buildDir.resolve("packages") - packagesDir.resolve("modelix-kernelf-editor-$version.tgz") + packagesDir + .resolve("modelix-kernelf-editor-$version.tgz") .copyTo(packagesDir.resolve("modelix-kernelf-editor.tgz"), overwrite = true) } } diff --git a/kernelf-editor/src/commonMain/kotlin/org/modelix/editor/kernelf/Behavior_org_iets3_core_expr.kt b/kernelf-editor/src/commonMain/kotlin/org/modelix/editor/kernelf/Behavior_org_iets3_core_expr.kt index a19e1465..207f4aa9 100644 --- a/kernelf-editor/src/commonMain/kotlin/org/modelix/editor/kernelf/Behavior_org_iets3_core_expr.kt +++ b/kernelf-editor/src/commonMain/kotlin/org/modelix/editor/kernelf/Behavior_org_iets3_core_expr.kt @@ -8,11 +8,16 @@ import org.iets3.core.expr.base.N_IRef import org.iets3.core.expr.base.N_ISingleSymbolRef import org.modelix.aspects.behavior.buildPolymorphicFunction -val binaryExpressionSymbols by buildPolymorphicFunction().returns().forConcept() - .defaultValue { it.alias ?: ":${it.untyped().getShortName()}:" }.delegate() +val binaryExpressionSymbols by buildPolymorphicFunction() + .returns() + .forConcept() + .defaultValue { it.alias ?: ":${it.untyped().getShortName()}:" } + .delegate() val ISingleSymbolRef_getSymbolName by buildPolymorphicFunction().returns().forNode(C_ISingleSymbolRef).delegate() + fun N_ISingleSymbolRef.getSymbolName() = ISingleSymbolRef_getSymbolName(this) val IRef_target by buildPolymorphicFunction().returns().forNode(C_IRef).delegate() + fun N_IRef.target() = IRef_target(this) diff --git a/kernelf-editor/src/commonMain/kotlin/org/modelix/editor/kernelf/Editor_de_slisson_mps_richtext.kt b/kernelf-editor/src/commonMain/kotlin/org/modelix/editor/kernelf/Editor_de_slisson_mps_richtext.kt index f2446ba8..e623a404 100644 --- a/kernelf-editor/src/commonMain/kotlin/org/modelix/editor/kernelf/Editor_de_slisson_mps_richtext.kt +++ b/kernelf-editor/src/commonMain/kotlin/org/modelix/editor/kernelf/Editor_de_slisson_mps_richtext.kt @@ -4,13 +4,14 @@ import de.slisson.mps.richtext.L_de_slisson_mps_richtext import org.modelix.aspects.languageAspects import org.modelix.editor.editor -val Editor_de_slisson_mps_richtext = languageAspects(L_de_slisson_mps_richtext) { - editor(language.Text) { - concept.words.horizontal() - } - editor(language.Word) { - concept.escapedValue.cell { - placeholderText("") +val Editor_de_slisson_mps_richtext = + languageAspects(L_de_slisson_mps_richtext) { + editor(language.Text) { + concept.words.horizontal() + } + editor(language.Word) { + concept.escapedValue.cell { + placeholderText("") + } } } -} diff --git a/kernelf-editor/src/commonMain/kotlin/org/modelix/editor/kernelf/Editor_jetbrains_mps_lang_test.kt b/kernelf-editor/src/commonMain/kotlin/org/modelix/editor/kernelf/Editor_jetbrains_mps_lang_test.kt index 3bf5fd0a..6c38d95a 100644 --- a/kernelf-editor/src/commonMain/kotlin/org/modelix/editor/kernelf/Editor_jetbrains_mps_lang_test.kt +++ b/kernelf-editor/src/commonMain/kotlin/org/modelix/editor/kernelf/Editor_jetbrains_mps_lang_test.kt @@ -4,19 +4,20 @@ import jetbrains.mps.lang.test.L_jetbrains_mps_lang_test import org.modelix.aspects.languageAspects import org.modelix.editor.editor -val Editor_jetbrains_mps_lang_test = languageAspects(L_jetbrains_mps_lang_test) { - editor(language.TestInfo) { - vertical { - horizontal { - "Project Path :".constant() - concept.projectPath.cell() - } - horizontal { - "ReOpen Project:".constant() - concept.reOpenProject.cell { - placeholderText("false") +val Editor_jetbrains_mps_lang_test = + languageAspects(L_jetbrains_mps_lang_test) { + editor(language.TestInfo) { + vertical { + horizontal { + "Project Path :".constant() + concept.projectPath.cell() + } + horizontal { + "ReOpen Project:".constant() + concept.reOpenProject.cell { + placeholderText("false") + } } } } } -} diff --git a/kernelf-editor/src/commonMain/kotlin/org/modelix/editor/kernelf/Editor_org_iets3_core_expr_base.kt b/kernelf-editor/src/commonMain/kotlin/org/modelix/editor/kernelf/Editor_org_iets3_core_expr_base.kt index 2d5725ea..71220088 100644 --- a/kernelf-editor/src/commonMain/kotlin/org/modelix/editor/kernelf/Editor_org_iets3_core_expr_base.kt +++ b/kernelf-editor/src/commonMain/kotlin/org/modelix/editor/kernelf/Editor_org_iets3_core_expr_base.kt @@ -8,547 +8,554 @@ import org.iets3.core.expr.lambda.L_org_iets3_core_expr_lambda import org.modelix.aspects.languageAspects import org.modelix.editor.editor -val Editor_org_iets3_core_expr_base = languageAspects(L_org_iets3_core_expr_base) { - val abstractMinMaxAliases = mapOf( - language.MinExpression to "min", - language.MaxExpression to "max", - ) - editor(language.AbstractMinMaxExpression) { - val alias = abstractMinMaxAliases[concept] - ?: "Unknown MinMaxExpression ${concept.untyped().getLongName()}" - alias.constant() - noSpace() - parentheses { - concept.values.horizontal(",") +val Editor_org_iets3_core_expr_base = + languageAspects(L_org_iets3_core_expr_base) { + val abstractMinMaxAliases = + mapOf( + language.MinExpression to "min", + language.MaxExpression to "max", + ) + editor(language.AbstractMinMaxExpression) { + val alias = + abstractMinMaxAliases[concept] + ?: "Unknown MinMaxExpression ${concept.untyped().getLongName()}" + alias.constant() + noSpace() + parentheses { + concept.values.horizontal(",") + } } - } - editor(language.AlternativesExpression) { - "alt".constant { - iets3keyword() + editor(language.AlternativesExpression) { + "alt".constant { + iets3keyword() + } + // TODO custom OpeningBracketCell + concept.alternatives.vertical() } - // TODO custom OpeningBracketCell - concept.alternatives.vertical() - } - editor(language.AltOption) { - concept.`when`.cell() - "=>".constant() - concept.then.cell() - } - editor(language.AlwaysValue) { - "always".constant() - } - editor(language.AttemptType) { - "attempt".constant { - iets3keyword() + editor(language.AltOption) { + concept.`when`.cell() + "=>".constant() + concept.then.cell() + } + editor(language.AlwaysValue) { + "always".constant() } - noSpace() - angleBrackets { - concept.successType.cell() + editor(language.AttemptType) { + "attempt".constant { + iets3keyword() + } noSpace() - optional { - "|".constant() - concept.errorLiterals.horizontal(",") + angleBrackets { + concept.successType.cell() + noSpace() + optional { + "|".constant() + concept.errorLiterals.horizontal(",") + } } } - } - editor(language.BangOp) { - concept.expr.cell() - noSpace() - "!".constant() - } + editor(language.BangOp) { + concept.expr.cell() + noSpace() + "!".constant() + } - binaryExpressionSymbols.implement(language.AssignmentExpr) { ":=" } + binaryExpressionSymbols.implement(language.AssignmentExpr) { ":=" } - binaryExpressionSymbols.implement(language.DivExpression) { "/" } - binaryExpressionSymbols.implement(language.MinusExpression) { "-" } - binaryExpressionSymbols.implement(language.ModExpression) { "%" } - binaryExpressionSymbols.implement(language.MulExpression) { "*" } - binaryExpressionSymbols.implement(language.PlusExpression) { "+" } + binaryExpressionSymbols.implement(language.DivExpression) { "/" } + binaryExpressionSymbols.implement(language.MinusExpression) { "-" } + binaryExpressionSymbols.implement(language.ModExpression) { "%" } + binaryExpressionSymbols.implement(language.MulExpression) { "*" } + binaryExpressionSymbols.implement(language.PlusExpression) { "+" } - binaryExpressionSymbols.implement(language.GreaterEqualsExpression) { ">=" } - binaryExpressionSymbols.implement(language.GreaterExpression) { ">" } - binaryExpressionSymbols.implement(language.LessEqualsExpression) { "<=>" } - binaryExpressionSymbols.implement(language.LessExpression) { "<" } + binaryExpressionSymbols.implement(language.GreaterEqualsExpression) { ">=" } + binaryExpressionSymbols.implement(language.GreaterExpression) { ">" } + binaryExpressionSymbols.implement(language.LessEqualsExpression) { "<=>" } + binaryExpressionSymbols.implement(language.LessExpression) { "<" } - binaryExpressionSymbols.implement(language.EqualsExpression) { "==" } - binaryExpressionSymbols.implement(language.NonStrictEqualsExpression) { "===" } - binaryExpressionSymbols.implement(language.NotEqualsExpression) { "!=" } + binaryExpressionSymbols.implement(language.EqualsExpression) { "==" } + binaryExpressionSymbols.implement(language.NonStrictEqualsExpression) { "===" } + binaryExpressionSymbols.implement(language.NotEqualsExpression) { "!=" } - binaryExpressionSymbols.implement(language.LogicalAndExpression) { "&&" } - binaryExpressionSymbols.implement(language.LogicalIffExpression) { "<=>" } - binaryExpressionSymbols.implement(language.LogicalImpliesExpression) { "=>" } - binaryExpressionSymbols.implement(language.LogicalOrExpression) { "||" } + binaryExpressionSymbols.implement(language.LogicalAndExpression) { "&&" } + binaryExpressionSymbols.implement(language.LogicalIffExpression) { "<=>" } + binaryExpressionSymbols.implement(language.LogicalImpliesExpression) { "=>" } + binaryExpressionSymbols.implement(language.LogicalOrExpression) { "||" } - binaryExpressionSymbols.implement(language.OptionOrExpression) { "?:" } - binaryExpressionSymbols.implement(L_org_iets3_core_expr_lambda.FunCompose) { ":o:" } + binaryExpressionSymbols.implement(language.OptionOrExpression) { "?:" } + binaryExpressionSymbols.implement(L_org_iets3_core_expr_lambda.FunCompose) { ":o:" } - editor(language.BinaryExpression, applicableToSubConcepts = true) { - val symbol = binaryExpressionSymbols(concept) - concept.left.cell() - symbol.constant() - concept.right.cell() - } - editor(language.CastExpression) { - "cast".constant { - iets3keyword() + editor(language.BinaryExpression, applicableToSubConcepts = true) { + val symbol = binaryExpressionSymbols(concept) + concept.left.cell() + symbol.constant() + concept.right.cell() + } + editor(language.CastExpression) { + "cast".constant { + iets3keyword() + } + noSpace() + angleBrackets { + concept.expectedType.cell() + } + noSpace() + parentheses { + concept.expr.cell() + } } - noSpace() - angleBrackets { - concept.expectedType.cell() + editor(language.CheckTypeConstraintsExpr) { + "check".constant { + iets3keyword() + } + concept.failIfInvalid.flagCell("failIfInvalid") + noSpace() + angleBrackets { + concept.tp.cell() + } + noSpace() + parentheses { + concept.expr.cell() + } } - noSpace() - parentheses { + editor(language.ColonCast) { concept.expr.cell() + noSpace() + ":".constant() + noSpace() + concept.type.cell() } - } - editor(language.CheckTypeConstraintsExpr) { - "check".constant { - iets3keyword() + editor(language.Contract) { + "where".constant { + iets3keyword() + } + largeBrackets { + concept.items.vertical() + } } - concept.failIfInvalid.flagCell("failIfInvalid") - noSpace() - angleBrackets { - concept.tp.cell() + editor(language.ContractItem) { + "".constant() } - noSpace() - parentheses { + editor(language.ConvenientBoolean) { + concept.value.cell() + } + editor(language.ConvenientValueCond) { + "if".constant() concept.expr.cell() } - } - editor(language.ColonCast) { - concept.expr.cell() - noSpace() - ":".constant() - noSpace() - concept.type.cell() - } - editor(language.Contract) { - "where".constant { - iets3keyword() + editor(language.DefaultValueExpression) { + "default".constant { + iets3keyword() + } + noSpace() + parentheses { + concept.type.cell() + } } - largeBrackets { - concept.items.vertical() + editor(language.DeRefTarget) { + "deref".constant() } - } - editor(language.ContractItem) { - "".constant() - } - editor(language.ConvenientBoolean) { - concept.value.cell() - } - editor(language.ConvenientValueCond) { - "if".constant() - concept.expr.cell() - } - editor(language.DefaultValueExpression) { - "default".constant { - iets3keyword() + editor(language.DotExpression) { + concept.expr.cell() + noSpace() + ".".constant() + noSpace() + concept.target.cell() } - noSpace() - parentheses { - concept.type.cell() + editor(language.EmptyExpression) { + "".constant() } - } - editor(language.DeRefTarget) { - "deref".constant() - } - editor(language.DotExpression) { - concept.expr.cell() - noSpace() - ".".constant() - noSpace() - concept.target.cell() - } - editor(language.EmptyExpression) { - "".constant() - } - editor(language.EmptyType) { - "emptytype".constant() - } - editor(language.EmptyValue) { - "empty".constant() - noSpace() - optional { - angleBrackets { - concept.type.cell() + editor(language.EmptyType) { + "emptytype".constant() + } + editor(language.EmptyValue) { + "empty".constant() + noSpace() + optional { + angleBrackets { + concept.type.cell() + } } } - } - editor(language.ErrorExpression) { - "error".constant { - iets3keyword() + editor(language.ErrorExpression) { + "error".constant { + iets3keyword() + } + noSpace() + parentheses { + concept.error.cell() + } } - noSpace() - parentheses { - concept.error.cell() + editor(language.ErrorLiteral) { + concept.name.cell() } - } - editor(language.ErrorLiteral) { - concept.name.cell() - } - editor(language.ErrorTarget) { - "err".constant() - } - editor(language.FailExpr) { - "fail".constant { - iets3keyword() + editor(language.ErrorTarget) { + "err".constant() } - noSpace() - optional { - angleBrackets { - concept.type.cell() + editor(language.FailExpr) { + "fail".constant { + iets3keyword() + } + noSpace() + optional { + angleBrackets { + concept.type.cell() + } + } + noSpace() + squareBrackets { + concept.message.cell() + } + noSpace() + optional { + ",".constant() + concept.contextExpression.cell() } } - noSpace() - squareBrackets { - concept.message.cell() - } - noSpace() - optional { - ",".constant() - concept.contextExpression.cell() + editor(language.GenericErrorType) { + "error".constant { + iets3keyword() + } } - } - editor(language.GenericErrorType) { - "error".constant { - iets3keyword() + editor(language.HasValueOp) { + "hasValue".constant() } - } - editor(language.HasValueOp) { - "hasValue".constant() - } - editor(language.IfElseSection) { - "else".constant { - iets3keyword() + editor(language.IfElseSection) { + "else".constant { + iets3keyword() + } + concept.expr.cell() } - concept.expr.cell() - } - editor(language.IfExpression) { - "if".constant { - iets3keyword() + editor(language.IfExpression) { + "if".constant { + iets3keyword() + } + concept.condition.cell() + "then".constant { + iets3keyword() + } + concept.thenPart.cell() + optional { + concept.elseSection.cell() + } } - concept.condition.cell() - "then".constant { - iets3keyword() + editor(language.ImplicitValidityValExpr) { + "it".constant() } - concept.thenPart.cell() - optional { - concept.elseSection.cell() + editor(language.InlineMessage) { + "message".constant() + noSpace() + squareBrackets { + concept.text.cell() + } } - } - editor(language.ImplicitValidityValExpr) { - "it".constant() - } - editor(language.InlineMessage) { - "message".constant() - noSpace() - squareBrackets { - concept.text.cell() + editor(language.Invariant) { + "inv".constant { + iets3keyword() + } + concept.warning.flagCell("warning") + concept.expr.cell() + optional { + indented { + ":".constant() + concept.err.cell() + } + } } - } - editor(language.Invariant) { - "inv".constant { - iets3keyword() + ISingleSymbolRef_getSymbolName.implement(C_ISingleSymbolRef) { node -> + ((node as? N_IRef)?.target() as? N_INamedConcept)?.name ?: "" } - concept.warning.flagCell("warning") - concept.expr.cell() - optional { - indented { - ":".constant() - concept.err.cell() + editor(language.ISingleSymbolRef) { + withNode { + node.getSymbolName().constant() } } - } - ISingleSymbolRef_getSymbolName.implement(C_ISingleSymbolRef) { node -> - ((node as? N_IRef)?.target() as? N_INamedConcept)?.name ?: "" - } - editor(language.ISingleSymbolRef) { - withNode { - node.getSymbolName().constant() + editor(language.IsSomeExpression) { + "isSome".constant { + iets3keyword() + } + noSpace() + parentheses { + concept.expr.cell() + } + optional { + "as".constant() + concept.optionalName.cell() + } } - } - editor(language.IsSomeExpression) { - "isSome".constant { - iets3keyword() + editor(language.JoinType) { + "join".constant() + noSpace() + angleBrackets { + concept.types.horizontal(",") + } } - noSpace() - parentheses { + editor(language.LogicalNotExpression) { + "!".constant() + noSpace() concept.expr.cell() } - optional { - "as".constant() - concept.optionalName.cell() + editor(language.MakeRefTarget) { + "ref".constant() } - } - editor(language.JoinType) { - "join".constant() - noSpace() - angleBrackets { - concept.types.horizontal(",") + editor(language.MessageValueType) { + "message".constant() } - } - editor(language.LogicalNotExpression) { - "!".constant() - noSpace() - concept.expr.cell() - } - editor(language.MakeRefTarget) { - "ref".constant() - } - editor(language.MessageValueType) { - "message".constant() - } - editor(language.NeverValue) { - "never".constant() - } - editor(language.NoneLiteral) { - "none".constant { - iets3keyword() + editor(language.NeverValue) { + "never".constant() } - optional { - noSpace() - angleBrackets { - concept.optionalBaseType.cell() + editor(language.NoneLiteral) { + "none".constant { + iets3keyword() + } + optional { + noSpace() + angleBrackets { + concept.optionalBaseType.cell() + } } } - } - editor(language.NoneType) { - "none".constant { - iets3keyword() + editor(language.NoneType) { + "none".constant { + iets3keyword() + } } - } - editor(language.OkTarget) { - "ok".constant() - } - editor(language.OneOfTarget) { - "oneOf".constant() - noSpace() - squareBrackets { - concept.values.horizontal(",") + editor(language.OkTarget) { + "ok".constant() } - } - editor(language.OperatorGroup) { - "join".constant { - iets3keyword() - } - noSpace() - angleBrackets { - concept.tag.cell() + editor(language.OneOfTarget) { + "oneOf".constant() + noSpace() + squareBrackets { + concept.values.horizontal(",") + } } - largeBrackets { - concept.expressions.vertical() + editor(language.OperatorGroup) { + "join".constant { + iets3keyword() + } + noSpace() + angleBrackets { + concept.tag.cell() + } + largeBrackets { + concept.expressions.vertical() + } } - } - val operatorTagSymbols = mapOf( - language.AndTag to "&&", - language.MulTag to "*", - language.OrTag to "||", - language.PlusTag to "+", - ) - editor(language.OperatorTag, applicableToSubConcepts = true) { - val symbol = operatorTagSymbols[concept] - ?: "Unknown operator tag ${concept.untyped().getLongName()}" - symbol.constant() - } - editor(language.OptionType) { - "opt".constant { - iets3keyword() + val operatorTagSymbols = + mapOf( + language.AndTag to "&&", + language.MulTag to "*", + language.OrTag to "||", + language.PlusTag to "+", + ) + editor(language.OperatorTag, applicableToSubConcepts = true) { + val symbol = + operatorTagSymbols[concept] + ?: "Unknown operator tag ${concept.untyped().getLongName()}" + symbol.constant() + } + editor(language.OptionType) { + "opt".constant { + iets3keyword() + } + noSpace() + angleBrackets { + concept.baseType.cell() + } } - noSpace() - angleBrackets { - concept.baseType.cell() + editor(language.ParensExpression) { + parentheses { + concept.expr.cell() + } } - } - editor(language.ParensExpression) { - parentheses { + editor(language.PlainConstraint) { + concept.warning.flagCell("warning") concept.expr.cell() + optional { + ":".constant() + concept.err.cell() + } } - } - editor(language.PlainConstraint) { - concept.warning.flagCell("warning") - concept.expr.cell() - optional { - ":".constant() - concept.err.cell() + editor(language.Postcondition) { + "post".constant { + iets3keyword() + } + concept.warning.flagCell("warning") + concept.expr.cell() + optional { + ":".constant() + concept.err.cell() + } } - } - editor(language.Postcondition) { - "post".constant { - iets3keyword() + editor(language.Precondition) { + "pre".constant { + iets3keyword() + } + concept.warning.flagCell("warning") + concept.expr.cell() + optional { + ":".constant() + concept.err.cell() + } } - concept.warning.flagCell("warning") - concept.expr.cell() - optional { - ":".constant() - concept.err.cell() + editor(language.PrimitiveType) { + (concept.alias ?: concept.untyped().getShortName()).constant { + iets3type() + } } - } - editor(language.Precondition) { - "pre".constant { - iets3keyword() + editor(language.ProgramLocationOp) { + "url".constant() } - concept.warning.flagCell("warning") - concept.expr.cell() - optional { - ":".constant() - concept.err.cell() + editor(language.ProgramLocationType) { + "loc".constant() } - } - editor(language.PrimitiveType) { - (concept.alias ?: concept.untyped().getShortName()).constant { - iets3type() + editor(language.RangeTarget) { + "inRange".constant() + noSpace() + concept.lowerExcluding.booleanCell("]", "[") + noSpace() + squareBrackets { + concept.min.cell() + "..".constant() + concept.max.cell() + } + noSpace() + concept.upperExcluding.booleanCell("[", "]") } - } - editor(language.ProgramLocationOp) { - "url".constant() - } - editor(language.ProgramLocationType) { - "loc".constant() - } - editor(language.RangeTarget) { - "inRange".constant() - noSpace() - concept.lowerExcluding.booleanCell("]", "[") - noSpace() - squareBrackets { - concept.min.cell() - "..".constant() - concept.max.cell() - } - noSpace() - concept.upperExcluding.booleanCell("[", "]") - } // editor(language.ReductionInspector) { // //TODO // } - editor(language.ReferenceType) { - "ref".constant { - iets3keyword() - } - noSpace() - angleBrackets { - concept.baseType.cell() + editor(language.ReferenceType) { + "ref".constant { + iets3keyword() + } + noSpace() + angleBrackets { + concept.baseType.cell() + } } - } // editor(language.Revealer) { // //TODO // } - editor(language.RevealerThis) { - "revealed".constant() - } + editor(language.RevealerThis) { + "revealed".constant() + } // editor(language.SimpleExpressionValueInspector) { // //TODO // } - editor(language.SomeValExpr) { - concept.someQuery.cell(presentation = { - expr.read { exprNode -> - if (exprNode == null) { - null - } else { - exprNode.unwrap().getReferenceRoles() - .map { exprNode.unwrap().getReferenceTarget(it) } - .filterIsInstance() - .map { it.name } - .firstOrNull() + editor(language.SomeValExpr) { + concept.someQuery.cell(presentation = { + expr.read { exprNode -> + if (exprNode == null) { + null + } else { + exprNode + .unwrap() + .getReferenceRoles() + .map { exprNode.unwrap().getReferenceTarget(it) } + .filterIsInstance() + .map { it.name } + .firstOrNull() + } } + }) + } + editor(language.SpecificErrorType) { + "error".constant() + noSpace() + angleBrackets { + concept.error.cell() } - }) - } - editor(language.SpecificErrorType) { - "error".constant() - noSpace() - angleBrackets { - concept.error.cell() } - } - editor(language.SuccessExpression) { - "success".constant { - iets3keyword() + editor(language.SuccessExpression) { + "success".constant { + iets3keyword() + } + noSpace() + parentheses { + concept.expr.cell() + } } - noSpace() - parentheses { - concept.expr.cell() + editor(language.SuccessType) { + "success".constant() + noSpace() + angleBrackets { + concept.baseType.cell() + } } - } - editor(language.SuccessType) { - "success".constant() - noSpace() - angleBrackets { - concept.baseType.cell() + editor(language.SuccessValueExpr) { + concept.`try`.cell({ name }) } - } - editor(language.SuccessValueExpr) { - concept.`try`.cell({ name }) - } - editor(language.ThisExpression) { - "this".constant { - iets3keyword() + editor(language.ThisExpression) { + "this".constant { + iets3keyword() + } } - } - editor(language.TracerExpression) { - squareBrackets { - concept.traced.cell() + editor(language.TracerExpression) { + squareBrackets { + concept.traced.cell() + } } - } - editor(language.TryErrorClause) { - "error".constant { - iets3keyword() + editor(language.TryErrorClause) { + "error".constant { + iets3keyword() + } + noSpace() + optional { + angleBrackets { + concept.errorLiteral.cell() + "=>".constant() + concept.expr.cell() + } + } } - noSpace() - optional { - angleBrackets { - concept.errorLiteral.cell() - "=>".constant() - concept.expr.cell() + editor(language.TryExpression) { + "try".constant { + iets3keyword() + } + concept.complete.flagCell("complete") + concept.expr.cell() + optional { + "as".constant() + concept.optionalName.cell() + } + concept.successClause.cell() + newLine() + indented { + concept.errorClauses.vertical() } } - } - editor(language.TryExpression) { - "try".constant { - iets3keyword() + editor(language.TrySuccessClause) { + "=>".constant() + concept.expr.cell() } - concept.complete.flagCell("complete") - concept.expr.cell() - optional { - "as".constant() - concept.optionalName.cell() + editor(language.TupleAccessExpr) { + concept.tuple.cell() + noSpace() + squareBrackets { + concept.index.cell() + } } - concept.successClause.cell() - newLine() - indented { - concept.errorClauses.vertical() + editor(language.TupleType) { + squareBrackets { + concept.elementTypes.horizontal(",") + } } - } - editor(language.TrySuccessClause) { - "=>".constant() - concept.expr.cell() - } - editor(language.TupleAccessExpr) { - concept.tuple.cell() - noSpace() - squareBrackets { - concept.index.cell() + editor(language.TupleValue) { + squareBrackets { + concept.values.horizontal(",") + } } - } - editor(language.TupleType) { - squareBrackets { - concept.elementTypes.horizontal(",") + editor(language.UnaryMinusExpression) { + "-".constant() + noSpace() + concept.expr.cell() } - } - editor(language.TupleValue) { - squareBrackets { - concept.values.horizontal(",") + editor(language.ValidityType) { + "validity".constant() + } + editor(language.VoidType) { + "void".constant() } } - editor(language.UnaryMinusExpression) { - "-".constant() - noSpace() - concept.expr.cell() - } - editor(language.ValidityType) { - "validity".constant() - } - editor(language.VoidType) { - "void".constant() - } -} diff --git a/kernelf-editor/src/commonMain/kotlin/org/modelix/editor/kernelf/Editor_org_iets3_core_expr_collections.kt b/kernelf-editor/src/commonMain/kotlin/org/modelix/editor/kernelf/Editor_org_iets3_core_expr_collections.kt index e4a8cce8..61112185 100644 --- a/kernelf-editor/src/commonMain/kotlin/org/modelix/editor/kernelf/Editor_org_iets3_core_expr_collections.kt +++ b/kernelf-editor/src/commonMain/kotlin/org/modelix/editor/kernelf/Editor_org_iets3_core_expr_collections.kt @@ -4,165 +4,166 @@ import org.iets3.core.expr.collections.L_org_iets3_core_expr_collections import org.modelix.aspects.languageAspects import org.modelix.editor.editor -val Editor_org_iets3_core_expr_collections = languageAspects(L_org_iets3_core_expr_collections) { - editor(language.AsSingletonList) { - "toList".constant() - } - editor(language.BracketOp) { - concept.expr.cell() - noSpace() - squareBrackets { - concept.index.cell() +val Editor_org_iets3_core_expr_collections = + languageAspects(L_org_iets3_core_expr_collections) { + editor(language.AsSingletonList) { + "toList".constant() } - } - editor(language.CollectionSizeSpec) { - angleBrackets { - concept.min.cell() - "|".constant() - concept.max.cell() + editor(language.BracketOp) { + concept.expr.cell() + noSpace() + squareBrackets { + concept.index.cell() + } } - } - editor(language.CollectionType) { - "collection".constant() - noSpace() - angleBrackets { - concept.baseType.cell() + editor(language.CollectionSizeSpec) { + angleBrackets { + concept.min.cell() + "|".constant() + concept.max.cell() + } } - optional { - concept.sizeConstraint.cell() + editor(language.CollectionType) { + "collection".constant() + noSpace() + angleBrackets { + concept.baseType.cell() + } + optional { + concept.sizeConstraint.cell() + } } - } - editor(language.ElementTypeConstraintMap) { - noSpace() - angleBrackets { - concept.typeConstraint1.cell() + editor(language.ElementTypeConstraintMap) { noSpace() - ",".constant() - concept.typeConstraint2.cell() + angleBrackets { + concept.typeConstraint1.cell() + noSpace() + ",".constant() + concept.typeConstraint2.cell() + } } - } - editor(language.ElementTypeConstraintSingle) { - noSpace() - angleBrackets { - concept.typeConstraint.cell() + editor(language.ElementTypeConstraintSingle) { + noSpace() + angleBrackets { + concept.typeConstraint.cell() + } } - } - editor(language.IndexExpr) { - "index".constant() - } - editor(language.KeyValuePair) { - concept.key.cell() - noSpace() - "->".constant() - noSpace() - concept.`val`.cell() - } - editor(language.ListInsertOp) { - "insert".constant() - noSpace() - parentheses { - concept.index.cell() + editor(language.IndexExpr) { + "index".constant() + } + editor(language.KeyValuePair) { + concept.key.cell() + noSpace() + "->".constant() noSpace() - ",".constant() - concept.arg.cell() + concept.`val`.cell() } - } - editor(language.ListLiteral) { - "list".constant() - optional { - concept.typeConstraint.cell() - } - noSpace() - parentheses { - concept.elements.horizontal { - separator { - noSpace() - ",".constant() + editor(language.ListInsertOp) { + "insert".constant() + noSpace() + parentheses { + concept.index.cell() + noSpace() + ",".constant() + concept.arg.cell() + } + } + editor(language.ListLiteral) { + "list".constant() + optional { + concept.typeConstraint.cell() + } + noSpace() + parentheses { + concept.elements.horizontal { + separator { + noSpace() + ",".constant() + } } } } - } - editor(language.ListPickOp) { - "pick".constant() - noSpace() - squareBrackets { - concept.selectorList.cell() + editor(language.ListPickOp) { + "pick".constant() + noSpace() + squareBrackets { + concept.selectorList.cell() + } } - } - editor(language.ListType) { - "list".constant() - noSpace() - angleBrackets { - concept.baseType.cell() + editor(language.ListType) { + "list".constant() + noSpace() + angleBrackets { + concept.baseType.cell() + } + optional { + concept.sizeConstraint.cell() + } } - optional { - concept.sizeConstraint.cell() + editor(language.MaxOp) { + "max".constant() } - } - editor(language.MaxOp) { - "max".constant() - } - editor(language.MapKeysOp) { - "keys".constant() - } - editor(language.MapLiteral) { - "map".constant() - optional { - concept.typeConstraint.cell() + editor(language.MapKeysOp) { + "keys".constant() } - noSpace() - parentheses { - concept.elements.horizontal(",") + editor(language.MapLiteral) { + "map".constant() + optional { + concept.typeConstraint.cell() + } + noSpace() + parentheses { + concept.elements.horizontal(",") + } } - } - editor(language.MapSizeOp) { - "size".constant() - } - editor(language.MapType) { - "map".constant() - noSpace() - angleBrackets { - concept.keyType.cell() + editor(language.MapSizeOp) { + "size".constant() + } + editor(language.MapType) { + "map".constant() noSpace() - ",".constant() - concept.valueType.cell() + angleBrackets { + concept.keyType.cell() + noSpace() + ",".constant() + concept.valueType.cell() + } } - } - editor(language.MapValuesOp) { - "values".constant() - } - editor(language.MinOp) { - "min".constant() - } - editor(language.SetLiteral) { - "set".constant() - optional { - concept.typeConstraint.cell() + editor(language.MapValuesOp) { + "values".constant() } - noSpace() - parentheses { - concept.elements.horizontal(",") + editor(language.MinOp) { + "min".constant() } - } - editor(language.SetType) { - "set".constant() - noSpace() - angleBrackets { - concept.baseType.cell() + editor(language.SetLiteral) { + "set".constant() + optional { + concept.typeConstraint.cell() + } + noSpace() + parentheses { + concept.elements.horizontal(",") + } } - } - editor(language.SimpleSortOp) { - "sort".constant() - noSpace() - parentheses { - concept.order.cell() + editor(language.SetType) { + "set".constant() + noSpace() + angleBrackets { + concept.baseType.cell() + } } - } - editor(language.UpToTarget) { - "upto".constant() - noSpace() - parentheses { - concept.max.cell() + editor(language.SimpleSortOp) { + "sort".constant() + noSpace() + parentheses { + concept.order.cell() + } + } + editor(language.UpToTarget) { + "upto".constant() + noSpace() + parentheses { + concept.max.cell() + } } } -} diff --git a/kernelf-editor/src/commonMain/kotlin/org/modelix/editor/kernelf/Editor_org_iets3_core_expr_datetime.kt b/kernelf-editor/src/commonMain/kotlin/org/modelix/editor/kernelf/Editor_org_iets3_core_expr_datetime.kt index 772aa508..66e76141 100644 --- a/kernelf-editor/src/commonMain/kotlin/org/modelix/editor/kernelf/Editor_org_iets3_core_expr_datetime.kt +++ b/kernelf-editor/src/commonMain/kotlin/org/modelix/editor/kernelf/Editor_org_iets3_core_expr_datetime.kt @@ -3,5 +3,6 @@ package org.modelix.editor.kernelf import org.iets3.core.expr.datetime.L_org_iets3_core_expr_datetime import org.modelix.aspects.languageAspects -val Editor_org_iets3_core_expr_datetime = languageAspects(L_org_iets3_core_expr_datetime) { -} +val Editor_org_iets3_core_expr_datetime = + languageAspects(L_org_iets3_core_expr_datetime) { + } diff --git a/kernelf-editor/src/commonMain/kotlin/org/modelix/editor/kernelf/Editor_org_iets3_core_expr_lambda.kt b/kernelf-editor/src/commonMain/kotlin/org/modelix/editor/kernelf/Editor_org_iets3_core_expr_lambda.kt index 6dd95bb4..a487de93 100644 --- a/kernelf-editor/src/commonMain/kotlin/org/modelix/editor/kernelf/Editor_org_iets3_core_expr_lambda.kt +++ b/kernelf-editor/src/commonMain/kotlin/org/modelix/editor/kernelf/Editor_org_iets3_core_expr_lambda.kt @@ -4,145 +4,146 @@ import org.iets3.core.expr.lambda.L_org_iets3_core_expr_lambda import org.modelix.aspects.languageAspects import org.modelix.editor.editor -val Editor_org_iets3_core_expr_lambda = languageAspects(L_org_iets3_core_expr_lambda) { - editor(language.ArgRef) { - concept.arg.cell({ name }) - } - editor(language.AssertExpr) { - "assert".constant { - iets3keyword() - } - noSpace() - parentheses { - concept.expr.cell() +val Editor_org_iets3_core_expr_lambda = + languageAspects(L_org_iets3_core_expr_lambda) { + editor(language.ArgRef) { + concept.arg.cell({ name }) + } + editor(language.AssertExpr) { + "assert".constant { + iets3keyword() + } + noSpace() + parentheses { + concept.expr.cell() + } } - } // editor(language.AttachedConstraint) { // //TODO // } - editor(language.BlockExpression) { - foldable("{...}") { - curlyBrackets { - newLine() - indented { - concept.expressions.vertical() + editor(language.BlockExpression) { + foldable("{...}") { + curlyBrackets { + newLine() + indented { + concept.expressions.vertical() + } + newLine() } - newLine() } } - } - editor(language.BindOp) { - "bind".constant() - noSpace() - parentheses { - concept.args.horizontal(",") - } - } - editor(language.CapturedValue) { - "!!!user objects are not supported!!!".constant() - } - editor(language.ExecOp) { - "exec".constant() - noSpace() - parentheses { - concept.args.horizontal(",") + editor(language.BindOp) { + "bind".constant() + noSpace() + parentheses { + concept.args.horizontal(",") + } } - } - editor(language.FunctionArgument) { - concept.name.cell() - noSpace() - optional { - ":".constant() - concept.type.cell() + editor(language.CapturedValue) { + "!!!user objects are not supported!!!".constant() } - } - editor(language.FunctionStyleExecOp) { - concept.`fun`.cell() - noSpace() - parentheses { - concept.args.horizontal(",") + editor(language.ExecOp) { + "exec".constant() + noSpace() + parentheses { + concept.args.horizontal(",") + } } - } - editor(language.FunctionType) { - parentheses { - concept.argumentTypes.horizontal(",") - "=>".constant() + editor(language.FunctionArgument) { + concept.name.cell() + noSpace() optional { - concept.effect.cell() + ":".constant() + concept.type.cell() } - concept.returnType.cell() - } - } - editor(language.FunResExpr) { - "res".constant { - iets3keyword() } - } - editor(language.LambdaArg) { - concept.name.cell() - noSpace() - optional { - ":".constant() - concept.type.cell() + editor(language.FunctionStyleExecOp) { + concept.`fun`.cell() + noSpace() + parentheses { + concept.args.horizontal(",") + } } - } - editor(language.LambdaArgRef) { - concept.arg.cell({ name }) - } - editor(language.LambdaExpression) { - brackets(true, "|", "|") { - concept.args.horizontal(",") - "=>".constant() - concept.expression.cell() + editor(language.FunctionType) { + parentheses { + concept.argumentTypes.horizontal(",") + "=>".constant() + optional { + concept.effect.cell() + } + concept.returnType.cell() + } } - } - editor(language.LocalVarDeclExpr) { - "var".constant { - iets3keyword() + editor(language.FunResExpr) { + "res".constant { + iets3keyword() + } } - optional { + editor(language.LambdaArg) { + concept.name.cell() noSpace() - ":".constant() - concept.type.cell() + optional { + ":".constant() + concept.type.cell() + } } - optional { - concept.contract.cell() + editor(language.LambdaArgRef) { + concept.arg.cell({ name }) } - "=".constant() - concept.expr.cell() - } - editor(language.LocalVarRef) { - concept.`var`.cell({ name }) - } - editor(language.ShortLambdaExpression) { - brackets(true, "|", "|") { - concept.expression.cell() + editor(language.LambdaExpression) { + brackets(true, "|", "|") { + concept.args.horizontal(",") + "=>".constant() + concept.expression.cell() + } } - } - editor(language.ShortLambdaItExpression) { - "it".constant() - } - editor(language.ValExpression) { - "val".constant { - iets3keyword() + editor(language.LocalVarDeclExpr) { + "var".constant { + iets3keyword() + } + optional { + noSpace() + ":".constant() + concept.type.cell() + } + optional { + concept.contract.cell() + } + "=".constant() + concept.expr.cell() } - concept.name.cell { - regex("[_a-zA-Z][_a-zA-Z0-9]*") + editor(language.LocalVarRef) { + concept.`var`.cell({ name }) } - optional { - ":".constant() - concept.type.cell() + editor(language.ShortLambdaExpression) { + brackets(true, "|", "|") { + concept.expression.cell() + } } - optional { - concept.contract.cell() + editor(language.ShortLambdaItExpression) { + "it".constant() + } + editor(language.ValExpression) { + "val".constant { + iets3keyword() + } + concept.name.cell { + regex("[_a-zA-Z][_a-zA-Z0-9]*") + } + optional { + ":".constant() + concept.type.cell() + } + optional { + concept.contract.cell() + } + "=".constant() + concept.expr.cell() + } + editor(language.ValRef) { + concept.`val`.cell({ name }) + } + editor(language.ValValueInContractExpr) { + "it".constant() } - "=".constant() - concept.expr.cell() - } - editor(language.ValRef) { - concept.`val`.cell({ name }) - } - editor(language.ValValueInContractExpr) { - "it".constant() } -} diff --git a/kernelf-editor/src/commonMain/kotlin/org/modelix/editor/kernelf/Editor_org_iets3_core_expr_path.kt b/kernelf-editor/src/commonMain/kotlin/org/modelix/editor/kernelf/Editor_org_iets3_core_expr_path.kt index 4f31adc0..dda6b680 100644 --- a/kernelf-editor/src/commonMain/kotlin/org/modelix/editor/kernelf/Editor_org_iets3_core_expr_path.kt +++ b/kernelf-editor/src/commonMain/kotlin/org/modelix/editor/kernelf/Editor_org_iets3_core_expr_path.kt @@ -10,18 +10,19 @@ import org.modelix.metamodel.typed import org.modelix.scopes.scope import org.modelix.typesystem.type -val Editor_org_iets3_core_expr_path = languageAspects(L_org_iets3_core_expr_path) { - editor(language.PathElement) { - concept.member.cell({ name }) - // TODO replace name with path label - } - scope(language.PathElement.member) { - val dot = it.getParent()?.getNode()?.typed() as? N_DotExpression ?: return@scope emptyList() - val left = dot.expr.get() ?: return@scope emptyList() - val leftType: ITypedNode = left.type() ?: return@scope emptyList() - when (leftType) { - is N_RecordType -> leftType.record.members.toList() - else -> emptyList() +val Editor_org_iets3_core_expr_path = + languageAspects(L_org_iets3_core_expr_path) { + editor(language.PathElement) { + concept.member.cell({ name }) + // TODO replace name with path label + } + scope(language.PathElement.member) { + val dot = it.getParent()?.getNode()?.typed() as? N_DotExpression ?: return@scope emptyList() + val left = dot.expr.get() ?: return@scope emptyList() + val leftType: ITypedNode = left.type() ?: return@scope emptyList() + when (leftType) { + is N_RecordType -> leftType.record.members.toList() + else -> emptyList() + } } } -} diff --git a/kernelf-editor/src/commonMain/kotlin/org/modelix/editor/kernelf/Editor_org_iets3_core_expr_repl.kt b/kernelf-editor/src/commonMain/kotlin/org/modelix/editor/kernelf/Editor_org_iets3_core_expr_repl.kt index 389b126c..d1be1cc1 100644 --- a/kernelf-editor/src/commonMain/kotlin/org/modelix/editor/kernelf/Editor_org_iets3_core_expr_repl.kt +++ b/kernelf-editor/src/commonMain/kotlin/org/modelix/editor/kernelf/Editor_org_iets3_core_expr_repl.kt @@ -4,176 +4,180 @@ import org.iets3.core.expr.repl.L_org_iets3_core_expr_repl import org.modelix.aspects.languageAspects import org.modelix.editor.editor -val Editor_org_iets3_core_expr_repl = languageAspects(L_org_iets3_core_expr_repl) { - val borderCellStyles = mapOf( - language.BottomBorderCellStyle to "bottom border", - language.LeftBorderCellStyle to "left border", - language.RightBorderCellStyle to "right border", - language.TopBorderCellStyle to "top border", - ) - editor(language.BorderCellStyle) { - val alias = borderCellStyles[concept] - ?: "Unknown BorderCellStyle: ${concept.untyped().getLongName()}" - alias.constant() - concept.width.cell() - } +val Editor_org_iets3_core_expr_repl = + languageAspects(L_org_iets3_core_expr_repl) { + val borderCellStyles = + mapOf( + language.BottomBorderCellStyle to "bottom border", + language.LeftBorderCellStyle to "left border", + language.RightBorderCellStyle to "right border", + language.TopBorderCellStyle to "top border", + ) + editor(language.BorderCellStyle) { + val alias = + borderCellStyles[concept] + ?: "Unknown BorderCellStyle: ${concept.untyped().getLongName()}" + alias.constant() + concept.width.cell() + } // editor(language.Cell) { // //TODO // } - editor(language.CellArg) { - concept.name.cell() - noSpace() - optional { + editor(language.CellArg) { + concept.name.cell() + noSpace() + optional { + concept.type.cell() + } + } + editor(language.CellArgRef) { + concept.arg.cell({ name }) + } + editor(language.CellConstraint) { + "type:".constant() concept.type.cell() + newLine() + "where".constant() + concept.constraint.cell() } - } - editor(language.CellArgRef) { - concept.arg.cell({ name }) - } - editor(language.CellConstraint) { - "type:".constant() - concept.type.cell() - newLine() - "where".constant() - concept.constraint.cell() - } - editor(language.CellConstraintIt) { - "it".constant() - } - editor(language.CellLabel) { - concept.name.cell() - noSpace() - ":".constant() - } - editor(language.CoordCellRef) { - "$".constant() - noSpace() - optional { - concept.finder.cell() + editor(language.CellConstraintIt) { + "it".constant() + } + editor(language.CellLabel) { + concept.name.cell() noSpace() - "/".constant() + ":".constant() + } + editor(language.CoordCellRef) { + "$".constant() + noSpace() + optional { + concept.finder.cell() + noSpace() + "/".constant() + } + concept.cell.cell() + // TODO argList if needActuals } - concept.cell.cell() - // TODO argList if needActuals - } // editor(language.DefaultEntry) { // //TODO // } - val fontStyles = mapOf( - language.FontBoldStyle to "font-bold", - ) - editor(language.FontStyle) { - val alias = fontStyles[concept] ?: "Unknown font style: ${concept.untyped().getLongName()}" - alias.constant() - } - editor(language.LabelExpression) { - "'".constant() - noSpace() - concept.text.cell() - } - editor(language.MakeListExpr) { - "makeList".constant() - noSpace() - squareBrackets { - concept.from.cell() + val fontStyles = + mapOf( + language.FontBoldStyle to "font-bold", + ) + editor(language.FontStyle) { + val alias = fontStyles[concept] ?: "Unknown font style: ${concept.untyped().getLongName()}" + alias.constant() + } + editor(language.LabelExpression) { + "'".constant() noSpace() - "..".constant() + concept.text.cell() + } + editor(language.MakeListExpr) { + "makeList".constant() noSpace() - concept.to.cell() + squareBrackets { + concept.from.cell() + noSpace() + "..".constant() + noSpace() + concept.to.cell() + } } - } - editor(language.MakeRecordExpr) { - "makeRecord".constant() - noSpace() - angleBrackets { - concept.record.cell() - } - noSpace() - squareBrackets { - concept.from.cell() + editor(language.MakeRecordExpr) { + "makeRecord".constant() noSpace() - "..".constant() + angleBrackets { + concept.record.cell() + } noSpace() - concept.to.cell() + squareBrackets { + concept.from.cell() + noSpace() + "..".constant() + noSpace() + concept.to.cell() + } } - } - editor(language.NamedCellRef) { - concept.label.cell({ name }) - // TODO argList if needActuals - } - editor(language.NamedSheetFinder) { - concept.sheet.cell({ name }) - } - editor(language.QuoteExpr) { - "quote".constant() - noSpace() - parentheses { - concept.cell.cell() + editor(language.NamedCellRef) { + concept.label.cell({ name }) + // TODO argList if needActuals + } + editor(language.NamedSheetFinder) { + concept.sheet.cell({ name }) + } + editor(language.QuoteExpr) { + "quote".constant() + noSpace() + parentheses { + concept.cell.cell() + } } - } // editor(language.REPL) { // //TODO // } // editor(language.ReplEntryRef) { // //TODO // } - editor(language.ReplEntryRefByName) { - concept.entry.cell({ optionalName }) { - textColor("blue") + editor(language.ReplEntryRefByName) { + concept.entry.cell({ optionalName }) { + textColor("blue") + } } - } // editor(language.Sheet) { // //TODO // } - editor(language.SheetEmbedExpr) { - ifEmpty(concept.sheet) { - "new sheet from".constant { - iets3keyword() - } - concept.template.cell({ name }) - "will be".constant { - iets3keyword() - } - concept.cols.cell() - "cols and".constant { - iets3keyword() + editor(language.SheetEmbedExpr) { + ifEmpty(concept.sheet) { + "new sheet from".constant { + iets3keyword() + } + concept.template.cell({ name }) + "will be".constant { + iets3keyword() + } + concept.cols.cell() + "cols and".constant { + iets3keyword() + } + concept.rows.cell() + "rows".constant { + iets3keyword() + } } - concept.rows.cell() - "rows".constant { - iets3keyword() + ifNotEmpty(concept.sheet) { + concept.sheet.cell() } } - ifNotEmpty(concept.sheet) { - concept.sheet.cell() - } - } - editor(language.SheetTestItem) { - ifEmpty(concept.sheet) { - "new sheet will be".constant() - concept.cols.cell() - "cols and".constant { - iets3keyword() + editor(language.SheetTestItem) { + ifEmpty(concept.sheet) { + "new sheet will be".constant() + concept.cols.cell() + "cols and".constant { + iets3keyword() + } + concept.rows.cell() + "rows".constant { + iets3keyword() + } } - concept.rows.cell() - "rows".constant { - iets3keyword() + ifNotEmpty(concept.sheet) { + concept.sheet.cell() } } - ifNotEmpty(concept.sheet) { - concept.sheet.cell() - } - } - editor(language.SheetType) { - "sheet".constant() - noSpace() - angleBrackets { - concept.template.cell({ name }) + editor(language.SheetType) { + "sheet".constant() + noSpace() + angleBrackets { + concept.template.cell({ name }) + } } - } // editor(language.TopLevelSheet) { // //TODO // } - editor(language.UpwardsSheetFinder) { - "..".constant() + editor(language.UpwardsSheetFinder) { + "..".constant() + } } -} diff --git a/kernelf-editor/src/commonMain/kotlin/org/modelix/editor/kernelf/Editor_org_iets3_core_expr_simpleTypes.kt b/kernelf-editor/src/commonMain/kotlin/org/modelix/editor/kernelf/Editor_org_iets3_core_expr_simpleTypes.kt index 231513c3..327aae7d 100644 --- a/kernelf-editor/src/commonMain/kotlin/org/modelix/editor/kernelf/Editor_org_iets3_core_expr_simpleTypes.kt +++ b/kernelf-editor/src/commonMain/kotlin/org/modelix/editor/kernelf/Editor_org_iets3_core_expr_simpleTypes.kt @@ -4,166 +4,169 @@ import org.iets3.core.expr.simpleTypes.L_org_iets3_core_expr_simpleTypes import org.modelix.aspects.languageAspects import org.modelix.editor.editor -val Editor_org_iets3_core_expr_simpleTypes = languageAspects(L_org_iets3_core_expr_simpleTypes) { - editor(language.StringLiteral) { - horizontal { - textColor("DarkGreen") - "\"".constant() - noSpace() +val Editor_org_iets3_core_expr_simpleTypes = + languageAspects(L_org_iets3_core_expr_simpleTypes) { + editor(language.StringLiteral) { + horizontal { + textColor("DarkGreen") + "\"".constant() + noSpace() + concept.value.cell { + placeholderText("") + regex(Regex("""([^"\\]|\\.)*""")) + validateValue { validateStringLiteral(it) } + } + noSpace() + "\"".constant() + } + } + editor(language.NumberLiteral) { concept.value.cell { - placeholderText("") - regex(Regex("""([^"\\]|\\.)*""")) - validateValue { validateStringLiteral(it) } + textColor("DarkMagenta") + regex("""[0-9]+([.][0-9]+)?""") + validateValue { it.toDoubleOrNull() != null } } - noSpace() - "\"".constant() } - } - editor(language.NumberLiteral) { - concept.value.cell { - textColor("DarkMagenta") - regex("""[0-9]+([.][0-9]+)?""") - validateValue { it.toDoubleOrNull() != null } + editor(language.TrueLiteral) { + "true".constant() } - } - editor(language.TrueLiteral) { - "true".constant() - } - editor(language.FalseLiteral) { - "false".constant() - } - editor(language.InterpolExprWord) { - brackets(singleLine = true, leftSymbol = "$(", rightSymbol = ")") { - concept.expr.cell() + editor(language.FalseLiteral) { + "false".constant() } - } - editor(language.NumberRangeSpec) { - "[".constant() - noSpace() - concept.min.cell { - validateValue { it.toDoubleOrNull() != null } - writeReplace { if (it.equals("-inf", ignoreCase = true)) "∞" else it.replace(",", ".") } - } - noSpace() - "|".constant() - noSpace() - concept.max.cell { - writeReplace { if (it.equals("inf", ignoreCase = true)) "∞" else it.replace(",", ".") } - } - noSpace() - "]".constant() - } - editor(language.NumberType) { - "number".constant() - optional { - noSpace() - concept.range.cell() + editor(language.InterpolExprWord) { + brackets(singleLine = true, leftSymbol = "$(", rightSymbol = ")") { + concept.expr.cell() + } } - optional { + editor(language.NumberRangeSpec) { + "[".constant() + noSpace() + concept.min.cell { + validateValue { it.toDoubleOrNull() != null } + writeReplace { if (it.equals("-inf", ignoreCase = true)) "∞" else it.replace(",", ".") } + } + noSpace() + "|".constant() noSpace() - concept.prec.cell() + concept.max.cell { + writeReplace { if (it.equals("inf", ignoreCase = true)) "∞" else it.replace(",", ".") } + } + noSpace() + "]".constant() } - } - editor(language.StringContainsTarget) { - "contains".constant() - noSpace() - parentheses { - concept.value.cell() + editor(language.NumberType) { + "number".constant() + optional { + noSpace() + concept.range.cell() + } + optional { + noSpace() + concept.prec.cell() + } } - } - editor(language.StringEndsWithTarget) { - "endsWith".constant() - noSpace() - parentheses { - concept.value.cell() + editor(language.StringContainsTarget) { + "contains".constant() + noSpace() + parentheses { + concept.value.cell() + } } - } - editor(language.StringInterpolationExpr) { - brackets(singleLine = true, leftSymbol = "'''", rightSymbol = "'''") { - concept.text.cell() + editor(language.StringEndsWithTarget) { + "endsWith".constant() + noSpace() + parentheses { + concept.value.cell() + } } - } - editor(language.StringLengthTarget) { - "length".constant() - } - editor(language.StringStartsWithTarget) { - "startsWith".constant() - noSpace() - parentheses { - concept.value.cell() + editor(language.StringInterpolationExpr) { + brackets(singleLine = true, leftSymbol = "'''", rightSymbol = "'''") { + concept.text.cell() + } } - } - editor(language.StringToIntTarget) { - "toInt".constant() - } - editor(language.StringType) { - "string".constant() - } - editor(language.BoundsExpression) { - "bounds".constant { - iets3keyword() + editor(language.StringLengthTarget) { + "length".constant() } - parentheses { - concept.expr.cell() - "⎵".constant() - concept.lower.cell() - "⎴".constant() - concept.upper.cell() + editor(language.StringStartsWithTarget) { + "startsWith".constant() + noSpace() + parentheses { + concept.value.cell() + } } - } - editor(language.LimitExpression) { - "limit".constant { - iets3keyword() + editor(language.StringToIntTarget) { + "toInt".constant() } - noSpace() - angleBrackets { - concept.type.cell() + editor(language.StringType) { + "string".constant() } - noSpace() - parentheses { - concept.expr.cell() + editor(language.BoundsExpression) { + "bounds".constant { + iets3keyword() + } + parentheses { + concept.expr.cell() + "⎵".constant() + concept.lower.cell() + "⎴".constant() + concept.upper.cell() + } } - } - editor(language.ConvertPrecisionNumberExpression) { - "precision".constant { - iets3keyword() + editor(language.LimitExpression) { + "limit".constant { + iets3keyword() + } + noSpace() + angleBrackets { + concept.type.cell() + } + noSpace() + parentheses { + concept.expr.cell() + } } - noSpace() - angleBrackets { - concept.rounding.cell() - "to".constant() - concept.targetPrecision.cell() + editor(language.ConvertPrecisionNumberExpression) { + "precision".constant { + iets3keyword() + } + noSpace() + angleBrackets { + concept.rounding.cell() + "to".constant() + concept.targetPrecision.cell() + } + noSpace() + parentheses { + concept.expr.cell() + } } - noSpace() - parentheses { - concept.expr.cell() + val roundingModes = + mapOf( + language.RoundDownRoundingMode to "round down", + language.RoundHalfUpRoundingMode to "round half up", + language.RoundUpRoundingMode to "round up", + language.TruncateRoundingMode to "truncate", + ) + editor(language.RoundingMode, applicableToSubConcepts = true) { + val mode = + roundingModes[concept] + ?: "Unknown rounding mode ${concept.untyped().getLongName()}" + mode.constant() + } + editor(language.NumberPrecSpec) { + noSpace() + curlyBrackets { + concept.prec.cell() + } } - } - val roundingModes = mapOf( - language.RoundDownRoundingMode to "round down", - language.RoundHalfUpRoundingMode to "round half up", - language.RoundUpRoundingMode to "round up", - language.TruncateRoundingMode to "truncate", - ) - editor(language.RoundingMode, applicableToSubConcepts = true) { - val mode = roundingModes[concept] - ?: "Unknown rounding mode ${concept.untyped().getLongName()}" - mode.constant() - } - editor(language.NumberPrecSpec) { - noSpace() - curlyBrackets { - concept.prec.cell() + editor(language.ToleranceExpr) { + concept.value.cell() + noSpace() + "±".constant() + noSpace() + concept.tolerance.cell() } } - editor(language.ToleranceExpr) { - concept.value.cell() - noSpace() - "±".constant() - noSpace() - concept.tolerance.cell() - } -} fun validateStringLiteral(value: String?): Boolean { if (value == null) return true diff --git a/kernelf-editor/src/commonMain/kotlin/org/modelix/editor/kernelf/Editor_org_iets3_core_expr_simpleTypes_test.kt b/kernelf-editor/src/commonMain/kotlin/org/modelix/editor/kernelf/Editor_org_iets3_core_expr_simpleTypes_test.kt index 9d8cf72f..fb2c5c1c 100644 --- a/kernelf-editor/src/commonMain/kotlin/org/modelix/editor/kernelf/Editor_org_iets3_core_expr_simpleTypes_test.kt +++ b/kernelf-editor/src/commonMain/kotlin/org/modelix/editor/kernelf/Editor_org_iets3_core_expr_simpleTypes_test.kt @@ -4,14 +4,15 @@ import org.iets3.core.expr.simpleTypes.tests.L_org_iets3_core_expr_simpleTypes_t import org.modelix.aspects.languageAspects import org.modelix.editor.editor -val Editor_org_iets3_core_expr_simpleTypes_test = languageAspects(L_org_iets3_core_expr_simpleTypes_tests) { - editor(language.EqClassProducer) { - "eqclass".constant() +val Editor_org_iets3_core_expr_simpleTypes_test = + languageAspects(L_org_iets3_core_expr_simpleTypes_tests) { + editor(language.EqClassProducer) { + "eqclass".constant() + } + editor(language.RandomVectorProducer) { + "random".constant() + concept.count.cell() + "only interesting".constant() + concept.onlyInteresing.cell() + } } - editor(language.RandomVectorProducer) { - "random".constant() - concept.count.cell() - "only interesting".constant() - concept.onlyInteresing.cell() - } -} diff --git a/kernelf-editor/src/commonMain/kotlin/org/modelix/editor/kernelf/Editor_org_iets3_core_expr_tests.kt b/kernelf-editor/src/commonMain/kotlin/org/modelix/editor/kernelf/Editor_org_iets3_core_expr_tests.kt index 1a9181df..c8a6158b 100644 --- a/kernelf-editor/src/commonMain/kotlin/org/modelix/editor/kernelf/Editor_org_iets3_core_expr_tests.kt +++ b/kernelf-editor/src/commonMain/kotlin/org/modelix/editor/kernelf/Editor_org_iets3_core_expr_tests.kt @@ -4,400 +4,401 @@ import org.iets3.core.expr.tests.L_org_iets3_core_expr_tests import org.modelix.aspects.languageAspects import org.modelix.editor.editor -val Editor_org_iets3_core_expr_tests = languageAspects(L_org_iets3_core_expr_tests) { - editor(language.AllExpressionsFilter) { - "expressions everywhere".constant() - } - editor(language.AllNodesFilter) { - "nodes everywhere".constant() - } - editor(language.AndMatcher) { - concept.left.cell() - "and".constant() { - iets3keyword() +val Editor_org_iets3_core_expr_tests = + languageAspects(L_org_iets3_core_expr_tests) { + editor(language.AllExpressionsFilter) { + "expressions everywhere".constant() } - concept.right.cell() - } - editor(language.AssertOptionTestItem) { - "assert-opt".constant { - iets3keyword() + editor(language.AllNodesFilter) { + "nodes everywhere".constant() } - concept.actual.cell() - "is".constant { - iets3keyword() + editor(language.AndMatcher) { + concept.left.cell() + "and".constant { + iets3keyword() + } + concept.right.cell() } - concept.what.cell() - // TODO editor component actualAndError - } - editor(language.AssertTestItem) { - optional { - concept.optionalName.cell() - "=".constant() + editor(language.AssertOptionTestItem) { + "assert-opt".constant { + iets3keyword() + } + concept.actual.cell() + "is".constant { + iets3keyword() + } + concept.what.cell() + // TODO editor component actualAndError } - "assert".constant { - iets3keyword() - } - concept.actual.cell() - concept.strict.flagCell() - concept.op.cell() - concept.expected.cell() - withNode { - if (node.isIgnored) { - "[ignored]".constant { - textColor("red") - backgroundColor("orange") + editor(language.AssertTestItem) { + optional { + concept.optionalName.cell() + "=".constant() + } + "assert".constant { + iets3keyword() + } + concept.actual.cell() + concept.strict.flagCell() + concept.op.cell() + concept.expected.cell() + withNode { + if (node.isIgnored) { + "[ignored]".constant { + textColor("red") + backgroundColor("orange") + } } } } - } - editor(language.AssertThatTestItem) { - optional { - concept.optionalName.cell() - "=".constant() + editor(language.AssertThatTestItem) { + optional { + concept.optionalName.cell() + "=".constant() + } + "assert-that".constant { + iets3keyword() + } + concept.value.cell() + "is".constant { + iets3keyword() + } + concept.matcher.cell() + // TODO editor component actualAndError } - "assert-that".constant { - iets3keyword() + editor(language.ConstraintFailedTestItem) { + "confail".constant { + iets3keyword() + } + concept.actual.cell() + optional { + "with error".constant() + concept.errmsg.cell() + } } - concept.value.cell() - "is".constant { - iets3keyword() + editor(language.ContainsString) { + "a string containing".constant() + concept.text.cell() } - concept.matcher.cell() - // TODO editor component actualAndError - } - editor(language.ConstraintFailedTestItem) { - "confail".constant { - iets3keyword() + editor(language.EmptyProducer) { + "empty".constant() } - concept.actual.cell() - optional { - "with error".constant() - concept.errmsg.cell() + editor(language.EmptyTestItem) { + "".constant() } - } - editor(language.ContainsString) { - "a string containing".constant() - concept.text.cell() - } - editor(language.EmptyProducer) { - "empty".constant() - } - editor(language.EmptyTestItem) { - "".constant() - } - editor(language.EqualsTestOp) { - "equals".constant { - iets3keyword() + editor(language.EqualsTestOp) { + "equals".constant { + iets3keyword() + } } - } - editor(language.EvalAnythingExpr) { - "evalanything".constant() - squareBrackets { - concept.anything.cell() + editor(language.EvalAnythingExpr) { + "evalanything".constant() + squareBrackets { + concept.anything.cell() + } } - } - editor(language.ForceCastExpr) { - "forceCast".constant { - iets3keyword() + editor(language.ForceCastExpr) { + "forceCast".constant { + iets3keyword() + } + noSpace() + angleBrackets { + concept.targetType.cell() + } + noSpace() + parentheses { + concept.expr.cell() + } } - noSpace() - angleBrackets { - concept.targetType.cell() + editor(language.FunctionSubjectAdapter) { + "function".constant() + concept.`fun`.cell({ name }) + newLine() + "results:".constant() + concept.checkResults.cell() + ifNotEmpty(concept.mutator) { + "mutator:".constant() + concept.mutator.cell() + } } - noSpace() - parentheses { - concept.expr.cell() + editor(language.IgnoredConcept) { + "concept".constant() + noSpace() + brackets(true, "/", "/") { + concept.concept.cell({ name }) + } } - } - editor(language.FunctionSubjectAdapter) { - "function".constant() - concept.`fun`.cell({ name }) - newLine() - "results:".constant() - concept.checkResults.cell() - ifNotEmpty(concept.mutator) { - "mutator:".constant() - concept.mutator.cell() + editor(language.InputValue) { + concept.value.cell() } - } - editor(language.IgnoredConcept) { - "concept".constant() - noSpace() - brackets(true, "/", "/") { + editor(language.InterpreterCoverageAssResult) { concept.concept.cell({ name }) + concept.comment.cell() } - } - editor(language.InputValue) { - concept.value.cell() - } - editor(language.InterpreterCoverageAssResult) { - concept.concept.cell({ name }) - concept.comment.cell() - } - editor(language.InterpreterCoverageAssSummary) { - "coverage".constant() - concept.coverageRatio.cell() - } - editor(language.InterpreterValueStat) { - concept.label.cell() - "=".constant() - concept.value.cell() - } - editor(language.InterpreterValueSummary) { - "value ranges".constant() - newLine() - indented { - concept.valueStats.vertical() + editor(language.InterpreterCoverageAssSummary) { + "coverage".constant() + concept.coverageRatio.cell() } - } - editor(language.InvalidInputOutcome) { - "invalid input".constant() - // TODO text color - } - editor(language.InvalidValueTestItem) { - "inval".constant { - iets3keyword() + editor(language.InterpreterValueStat) { + concept.label.cell() + "=".constant() + concept.value.cell() + } + editor(language.InterpreterValueSummary) { + "value ranges".constant() + newLine() + indented { + concept.valueStats.vertical() + } } - concept.actual.cell() - optional { - "with error".constant { + editor(language.InvalidInputOutcome) { + "invalid input".constant() + // TODO text color + } + editor(language.InvalidValueTestItem) { + "inval".constant { iets3keyword() } - concept.errmsg.cell() + concept.actual.cell() + optional { + "with error".constant { + iets3keyword() + } + concept.errmsg.cell() + } } - } - editor(language.IsInvalid) { - "invalid".constant { - iets3keyword() + editor(language.IsInvalid) { + "invalid".constant { + iets3keyword() + } + optional { + "with message".constant { + iets3keyword() + } + concept.messageMatcher.cell() + } } - optional { - "with message".constant { + editor(language.IsValidRecord) { + "a valid record".constant { iets3keyword() } - concept.messageMatcher.cell() } - } - editor(language.IsValidRecord) { - "a valid record".constant { - iets3keyword() + editor(language.LanguageRef) { + "language".constant() + brackets(true, "/", ",") { + concept.lang.cell() + } } - } - editor(language.LanguageRef) { - "language".constant() - brackets(true, "/", ",") { - concept.lang.cell() + editor(language.MatcherForAnyRecordType) { + "matcher-for-any-record-type".constant() } - } - editor(language.MatcherForAnyRecordType) { - "matcher-for-any-record-type".constant() - } - editor(language.MatcherForAnyType) { - "macher-for-any-type".constant() - } - editor(language.MatcherType) { - "matcher".constant() - noSpace() - angleBrackets { - concept.forType.cell() + editor(language.MatcherForAnyType) { + "macher-for-any-type".constant() } - } - editor(language.MeasureCoverageFor) { - "concept".constant() - brackets(true, "/", "/") { - concept.concept.cell({ name }) + editor(language.MatcherType) { + "matcher".constant() + noSpace() + angleBrackets { + concept.forType.cell() + } + } + editor(language.MeasureCoverageFor) { + "concept".constant() + brackets(true, "/", "/") { + concept.concept.cell({ name }) + } + "complete?".constant() + // TODO checkbox } - "complete?".constant() - // TODO checkbox - } // editor(language.ModelsCoverageAssResult) { // //TODO // } - editor(language.ModelsCoverageAssSummary) { - "coverage".constant() - concept.coverageRatio.cell() - } - editor(language.MutationEngine) { - "# of mutations".constant() - concept.numberOfMutations.cell() - newLine() - "keep all:".constant() - concept.keepAll.cell() - newLine() - ifNotEmpty(concept.logs) { - concept.logs.vertical() + editor(language.ModelsCoverageAssSummary) { + "coverage".constant() + concept.coverageRatio.cell() } - } - editor(language.MutationLog) { - "->".constant() - concept.newNode.cell({ concept.untyped().getShortName() }) - } - editor(language.MuteEffect) { - "mute".constant() - noSpace() - parentheses { - concept.expr.cell() + editor(language.MutationEngine) { + "# of mutations".constant() + concept.numberOfMutations.cell() + newLine() + "keep all:".constant() + concept.keepAll.cell() + newLine() + ifNotEmpty(concept.logs) { + concept.logs.vertical() + } + } + editor(language.MutationLog) { + "->".constant() + concept.newNode.cell({ concept.untyped().getShortName() }) + } + editor(language.MuteEffect) { + "mute".constant() + noSpace() + parentheses { + concept.expr.cell() + } } - } // editor(language.OldNodeAnnotation) { // //TODO // } - editor(language.NamedAssertRef) { - concept.item.cell({ name }) - } - editor(language.NoneExpr) { - "none".constant() - noSpace() - angleBrackets { - concept.expr.cell() + editor(language.NamedAssertRef) { + concept.item.cell({ name }) + } + editor(language.NoneExpr) { + "none".constant() + noSpace() + angleBrackets { + concept.expr.cell() + } } - } - editor(language.OptExpression) { - "some".constant() - noSpace() - angleBrackets { - concept.expr.cell() + editor(language.OptExpression) { + "some".constant() + noSpace() + angleBrackets { + concept.expr.cell() + } } - } - editor(language.OutputValue) { - concept.value.cell() - } - editor(language.RealEqualsTestOp) { - "real-equals".constant { - iets3keyword() + editor(language.OutputValue) { + concept.value.cell() } - noSpace() - squareBrackets { - concept.decimals.cell() + editor(language.RealEqualsTestOp) { + "real-equals".constant { + iets3keyword() + } + noSpace() + squareBrackets { + concept.decimals.cell() + } } - } - editor(language.ReportTestItem) { - "report".constant { - iets3keyword() + editor(language.ReportTestItem) { + "report".constant { + iets3keyword() + } + concept.actual.cell() + "=>".constant() + // TODO model access } - concept.actual.cell() - "=>".constant() - // TODO model access - } // editor(language.StackTraceElement) { // //TODO // } - editor(language.StructuralCoverageAssQuery) { - foldable("structural coverage {...}") { - "structural coverage".constant() - "in".constant() - concept.scope.cell() - newLine() - indented { - "limits:".constant() + editor(language.StructuralCoverageAssQuery) { + foldable("structural coverage {...}") { + "structural coverage".constant() + "in".constant() + concept.scope.cell() newLine() indented { - "min N =".constant() - concept.minTestCount.cell() + "limits:".constant() + newLine() + indented { + "min N =".constant() + concept.minTestCount.cell() + newLine() + "min V =".constant() + concept.minTestVolume.cell() + newLine() + "max MinH =".constant() + concept.maximalMinHetero.cell() + newLine() + "min MaxH =".constant() + concept.minimumMaxHetero.cell() + } + newLine() + "show limit errors:".constant() + // TODO checkbox + newLine() + "look outside suites:".constant() + // TODO checkbox + newLine() + "track properties:".constant() + // TODO checkbox newLine() - "min V =".constant() - concept.minTestVolume.cell() + "nodes filter:".constant() + concept.nodesFilter.cell() newLine() - "max MinH =".constant() - concept.maximalMinHetero.cell() + "languages:".constant() + concept.languages.vertical() newLine() - "min MaxH =".constant() - concept.minimumMaxHetero.cell() + "ignore".constant() + concept.ignoredConcepts.vertical() } - newLine() - "show limit errors:".constant() - // TODO checkbox - newLine() - "look outside suites:".constant() - // TODO checkbox - newLine() - "track properties:".constant() - // TODO checkbox - newLine() - "nodes filter:".constant() - concept.nodesFilter.cell() - newLine() - "languages:".constant() - concept.languages.vertical() - newLine() - "ignore".constant() - concept.ignoredConcepts.vertical() } } - } - editor(language.StructuralCoverageAssResult) { - concept.concept.cell({ name }) - concept.comment.cell() - } - editor(language.StructuralCoverageAssSummary) { - "coverage".constant() - concept.coverageRatio.cell() - } - editor(language.TestCase) { - "test case".constant { - iets3keyword() + editor(language.StructuralCoverageAssResult) { + concept.concept.cell({ name }) + concept.comment.cell() } - concept.name.cell() - foldable("{...}") { - // TODO test status - optional { - "setup".constant() - concept.setup.cell() - } - ifEmpty(concept.setup) { - newLine() + editor(language.StructuralCoverageAssSummary) { + "coverage".constant() + concept.coverageRatio.cell() + } + editor(language.TestCase) { + "test case".constant { + iets3keyword() } - curlyBrackets { - concept.items.vertical() + concept.name.cell() + foldable("{...}") { + // TODO test status + optional { + "setup".constant() + concept.setup.cell() + } + ifEmpty(concept.setup) { + newLine() + } + curlyBrackets { + concept.items.vertical() + } } } - } - editor(language.TestCoverageAssQuery) { - foldable("test coverage {...}") { - "test coverage".constant() - "in".constant() - concept.scope.cell() - newLine() - indented { - "effective models:".constant() - // TODO custom cell + editor(language.TestCoverageAssQuery) { + foldable("test coverage {...}") { + "test coverage".constant() + "in".constant() + concept.scope.cell() newLine() - "problems only:".constant() - // TODO checkbox - newLine() - "measure for:".constant() - // TODO calculate number of concepts - foldable("{X concepts}") { - concept.measureFor.vertical() - } - newLine() - "ignore:".constant() - foldable("{X ignored concepts") { - concept.ignoredConcepts.vertical() + indented { + "effective models:".constant() + // TODO custom cell + newLine() + "problems only:".constant() + // TODO checkbox + newLine() + "measure for:".constant() + // TODO calculate number of concepts + foldable("{X concepts}") { + concept.measureFor.vertical() + } + newLine() + "ignore:".constant() + foldable("{X ignored concepts") { + concept.ignoredConcepts.vertical() + } } } } - } // editor(language.TestItemVectorCollection) { // //TODO // } - editor(language.TestSuite) { - "test suite".constant() - concept.name.cell() - emptyLine() - "-----------------------------------".constant() - emptyLine() - concept.contents.vertical() - } - editor(language.ValidOutcome) { - "valid".constant() - // TODO text color - } - editor(language.VectorTestItem) { - "vectors".constant { - iets3keyword() + editor(language.TestSuite) { + "test suite".constant() + concept.name.cell() + emptyLine() + "-----------------------------------".constant() + emptyLine() + concept.contents.vertical() + } + editor(language.ValidOutcome) { + "valid".constant() + // TODO text color + } + editor(language.VectorTestItem) { + "vectors".constant { + iets3keyword() + } + concept.subject.cell() + "->".constant() + concept.vectors.cell() } - concept.subject.cell() - "->".constant() - concept.vectors.cell() } -} diff --git a/kernelf-editor/src/commonMain/kotlin/org/modelix/editor/kernelf/Editor_org_iets3_core_expr_toplevel.kt b/kernelf-editor/src/commonMain/kotlin/org/modelix/editor/kernelf/Editor_org_iets3_core_expr_toplevel.kt index 60f8b1f9..16f1a35f 100644 --- a/kernelf-editor/src/commonMain/kotlin/org/modelix/editor/kernelf/Editor_org_iets3_core_expr_toplevel.kt +++ b/kernelf-editor/src/commonMain/kotlin/org/modelix/editor/kernelf/Editor_org_iets3_core_expr_toplevel.kt @@ -11,321 +11,328 @@ import org.modelix.typesystem.asType import org.modelix.typesystem.asTypeVariable import org.modelix.typesystem.typesystem -val Editor_org_iets3_core_expr_toplevel = languageAspects(L_org_iets3_core_expr_toplevel) { - editor(language.AbstractFunctionAdapter) { - concept.`fun`.cell() - } - editor(language.AbstractFunctionLikeAdapter) { - concept.functionLike.cell() - } - editor(language.AbstractToplevelExprAdapter) { - concept.toplevelExprContent.cell() - } - editor(language.AllLitList) { - "literals".constant() - noSpace() - squareBrackets { - concept.enumType.cell() +val Editor_org_iets3_core_expr_toplevel = + languageAspects(L_org_iets3_core_expr_toplevel) { + editor(language.AbstractFunctionAdapter) { + concept.`fun`.cell() + } + editor(language.AbstractFunctionLikeAdapter) { + concept.functionLike.cell() + } + editor(language.AbstractToplevelExprAdapter) { + concept.toplevelExprContent.cell() + } + editor(language.AllLitList) { + "literals".constant() + noSpace() + squareBrackets { + concept.enumType.cell() + } } - } // editor(language.BuilderExpression) { // //TODO // } - editor(language.Constant) { - "val".constant { - iets3keyword() + editor(language.Constant) { + "val".constant { + iets3keyword() + } + concept.name.cell() + optional { + ":".constant() + concept.type.cell() + } + "=".constant() + concept.value.cell() } - concept.name.cell() - optional { - ":".constant() - concept.type.cell() + editor(language.ConstantRef) { + concept.constant.cell({ name }) + } + editor(language.EmptyMember) { + "".constant() + } + editor(language.EmptyToplevelContent) { + constant("") } - "=".constant() - concept.value.cell() - } - editor(language.ConstantRef) { - concept.constant.cell({ name }) - } - editor(language.EmptyMember) { - "".constant() - } - editor(language.EmptyToplevelContent) { - constant("") - } // editor(language.EnumDeclaration) { // //TODO // } - editor(language.EnumIndexOp) { - "index".constant() - } - editor(language.EnumIsInSelector) { - concept.literal.cell({ name }) - } - editor(language.EnumIsInTarget) { - "isIn".constant() - noSpace() - parentheses { - concept.selectors.horizontal(",") + editor(language.EnumIndexOp) { + "index".constant() } - } - editor(language.EnumIsTarget) { - "is".constant() - noSpace() - parentheses { + editor(language.EnumIsInSelector) { concept.literal.cell({ name }) } - } - editor(language.EnumLiteral) { - concept.name.cell() - withNode { - if (node.untyped().parent!!.typed().type.isSet()) { - "->".constant() - concept.value.cell() + editor(language.EnumIsInTarget) { + "isIn".constant() + noSpace() + parentheses { + concept.selectors.horizontal(",") } } - } - editor(language.EnumLiteralRef) { - concept.literal.cell({ - val enumDecl = untyped().parent!!.typed() - if (enumDecl.qualified) { - enumDecl.name + ":" + name - } else { - name - } - }) - } - editor(language.EnumType) { - concept.enum.cell({ name }) - } - editor(language.EnumValueAccessor) { - "value".constant() - } - editor(language.ExtensionFunctionCall) { - concept.extFun.cell({ name }) - // TODO effect descriptor - noSpace() - parentheses { - concept.args.horizontal(",") + editor(language.EnumIsTarget) { + "is".constant() + noSpace() + parentheses { + concept.literal.cell({ name }) + } } - } - editor(language.FieldSetter) { - concept.field.cell({ name }) - concept.value.cell() - } - editor(language.Function) { - concept.ext.flagCell("ext") { - iets3keyword() + editor(language.EnumLiteral) { + concept.name.cell() + withNode { + if (node + .untyped() + .parent!! + .typed() + .type + .isSet() + ) { + "->".constant() + concept.value.cell() + } + } } - "fun".constant { - iets3keyword() + editor(language.EnumLiteralRef) { + concept.literal.cell({ + val enumDecl = untyped().parent!!.typed() + if (enumDecl.qualified) { + enumDecl.name + ":" + name + } else { + name + } + }) + } + editor(language.EnumType) { + concept.enum.cell({ name }) + } + editor(language.EnumValueAccessor) { + "value".constant() + } + editor(language.ExtensionFunctionCall) { + concept.extFun.cell({ name }) + // TODO effect descriptor + noSpace() + parentheses { + concept.args.horizontal(",") + } } - concept.name.cell() - optional { - concept.effect.cell() + editor(language.FieldSetter) { + concept.field.cell({ name }) + concept.value.cell() } - noSpace() - parentheses { - concept.args.horizontal(",") + editor(language.Function) { + concept.ext.flagCell("ext") { + iets3keyword() + } + "fun".constant { + iets3keyword() + } + concept.name.cell() + optional { + concept.effect.cell() + } + noSpace() + parentheses { + concept.args.horizontal(",") + } + optional { + ":".constant() + concept.type.cell() + } + optional { + concept.contract.cell() + } + indented { + concept.body.cell() + } + // TODO ? = for single line body } - optional { - ":".constant() - concept.type.cell() + typesystem(language.Function) { + val body = node.body.get() + val returnType = node.type.get() + if (returnType != null) { + node.asTypeVariable() equalTo returnType.asType() + } + if (body != null) { + body.asTypeVariable() subtypeOf node.asTypeVariable() + } } - optional { - concept.contract.cell() + editor(language.FunctionCall) { + concept.function.cell({ name }) + // TODO effect descriptor + noSpace() + parentheses { + concept.args.horizontal() + } } - indented { - concept.body.cell() + typesystem(language.FunctionCall) { + node.asTypeVariable() equalTo node.function.asTypeVariable() } - // TODO ? = for single line body - } - typesystem(language.Function) { - val body = node.body.get() - val returnType = node.type.get() - if (returnType != null) { - node.asTypeVariable() equalTo returnType.asType() + editor(language.FunRef) { + ":".constant() + noSpace() + concept.`fun`.cell({ name }) } - if (body != null) { - body.asTypeVariable() subtypeOf node.asTypeVariable() + editor(language.GroupKeyTarget) { + "key".constant() } - } - editor(language.FunctionCall) { - concept.function.cell({ name }) - // TODO effect descriptor - noSpace() - parentheses { - concept.args.horizontal() + editor(language.GroupMembersTarget) { + "members".constant() } - } - typesystem(language.FunctionCall) { - node.asTypeVariable() equalTo node.function.asTypeVariable() - } - editor(language.FunRef) { - ":".constant() - noSpace() - concept.`fun`.cell({ name }) - } - editor(language.GroupKeyTarget) { - "key".constant() - } - editor(language.GroupMembersTarget) { - "members".constant() - } - editor(language.GroupType) { - "group".constant() - noSpace() - angleBrackets { - concept.keyType.cell() + editor(language.GroupType) { + "group".constant() noSpace() - ",".constant() - concept.memberType.cell() + angleBrackets { + concept.keyType.cell() + noSpace() + ",".constant() + concept.memberType.cell() + } } - } - editor(language.InlineRecordMemberAccess) { - concept.name.cell { - regex("[_a-zA-Z][_a-zA-Z0-9]*") + editor(language.InlineRecordMemberAccess) { + concept.name.cell { + regex("[_a-zA-Z][_a-zA-Z0-9]*") + } } - } - editor(language.InlineRecordType) { - "record".constant { - iets3keyword() + editor(language.InlineRecordType) { + "record".constant { + iets3keyword() + } + noSpace() + curlyBrackets { + concept.members.horizontal(",") + } + } + editor(language.Library) { + // TODO custom cells + "library".constant { + iets3keyword() + } + concept.name.cell() + indented { + "imports:".constant() + concept.imports.vertical() + } + emptyLine() + concept.contents.vertical() + } + editor(language.NewValueSetter) { + concept.member.cell({ name }) + optional { + "=".constant() + concept.newValue.cell() + } } - noSpace() - curlyBrackets { - concept.members.horizontal(",") + editor(language.OldMemberRef) { + concept.member.cell({ name }) } - } - editor(language.Library) { - // TODO custom cells - "library".constant { - iets3keyword() - } - concept.name.cell() - indented { - "imports:".constant() - concept.imports.vertical() - } - emptyLine() - concept.contents.vertical() - } - editor(language.NewValueSetter) { - concept.member.cell({ name }) - optional { + editor(language.OldValueExpr) { + "old".constant() + } + editor(language.ProjectIt) { + "it".constant() + } + editor(language.ProjectMember) { + concept.name.cell() "=".constant() - concept.newValue.cell() + concept.expr.cell() } - } - editor(language.OldMemberRef) { - concept.member.cell({ name }) - } - editor(language.OldValueExpr) { - "old".constant() - } - editor(language.ProjectIt) { - "it".constant() - } - editor(language.ProjectMember) { - concept.name.cell() - "=".constant() - concept.expr.cell() - } - editor(language.ProjectOp) { - "project".constant() - noSpace() - parentheses { - concept.members.horizontal(",") + editor(language.ProjectOp) { + "project".constant() + noSpace() + parentheses { + concept.members.horizontal(",") + } } - } - editor(language.QualifierRef) { - concept.enum.cell({ name }) - noSpace() - ":".constant() - noSpace() - concept.lit.cell({ name }) - } - editor(language.RecordChangeTarget) { - "with".constant() - noSpace() - parentheses { - concept.setters.horizontal(",") + editor(language.QualifierRef) { + concept.enum.cell({ name }) + noSpace() + ":".constant() + noSpace() + concept.lit.cell({ name }) } - } - editor(language.RecordDeclaration) { - optional { - concept.refFlag.cell() + editor(language.RecordChangeTarget) { + "with".constant() + noSpace() + parentheses { + concept.setters.horizontal(",") + } + } + editor(language.RecordDeclaration) { + optional { + concept.refFlag.cell() + } + "record".constant { + iets3keyword() + } + concept.name.cell() + curlyBrackets { + newLine() + indented { + concept.members.vertical() + } + newLine() + } + optional { + concept.contract.cell() + } } - "record".constant { - iets3keyword() + editor(language.RecordLiteral) { + "#".constant() + concept.type.cell() + curlyBrackets { + newLine() + concept.memberValues.horizontal(",") + } } - concept.name.cell() - curlyBrackets { - newLine() - indented { - concept.members.vertical() + typesystem(language.RecordLiteral) { + val recordType = node.type.get() as? N_RecordType + if (recordType != null) { + node.asTypeVariable() equalTo recordType.asType() } - newLine() } - optional { - concept.contract.cell() + editor(language.RecordMember) { + concept.name.cell() + ":".constant() + concept.type.cell() + optional { + concept.contract.cell() + } } - } - editor(language.RecordLiteral) { - "#".constant() - concept.type.cell() - curlyBrackets { - newLine() - concept.memberValues.horizontal(",") + editor(language.RecordMemberRefInConstraint) { + concept.member.cell({ name }) } - } - typesystem(language.RecordLiteral) { - val recordType = node.type.get() as? N_RecordType - if (recordType != null) { - node.asTypeVariable() equalTo recordType.asType() + editor(language.RecordType) { + concept.record.cell({ name }) } - } - editor(language.RecordMember) { - concept.name.cell() - ":".constant() - concept.type.cell() - optional { - concept.contract.cell() + editor(language.RecordTypeAdapter) { + concept.type.cell() } - } - editor(language.RecordMemberRefInConstraint) { - concept.member.cell({ name }) - } - editor(language.RecordType) { - concept.record.cell({ name }) - } - editor(language.RecordTypeAdapter) { - concept.type.cell() - } - editor(language.ReferenceableFlag) { - "referenceable".constant { - iets3keyword() + editor(language.ReferenceableFlag) { + "referenceable".constant { + iets3keyword() + } } - } - editor(language.SectionMarker) { - concept.label.cell() - newLine() - "-----------------------------------".constant() - } - editor(language.Typedef) { - "type".constant { - iets3keyword() + editor(language.SectionMarker) { + concept.label.cell() + newLine() + "-----------------------------------".constant() } - concept.name.cell() - noSpace() - ":".constant() - concept.originalType.cell() - optional { - concept.contract.cell() + editor(language.Typedef) { + "type".constant { + iets3keyword() + } + concept.name.cell() + noSpace() + ":".constant() + concept.originalType.cell() + optional { + concept.contract.cell() + } + } + editor(language.TypedefContractValExpr) { + "it".constant() + } + editor(language.TypedefType) { + concept.typedef.cell({ name }) } } - editor(language.TypedefContractValExpr) { - "it".constant() - } - editor(language.TypedefType) { - concept.typedef.cell({ name }) - } -} diff --git a/kernelf-editor/src/commonMain/kotlin/org/modelix/editor/kernelf/Editor_org_iets3_core_expr_tracing.kt b/kernelf-editor/src/commonMain/kotlin/org/modelix/editor/kernelf/Editor_org_iets3_core_expr_tracing.kt index 5d31dab6..1e6feb44 100644 --- a/kernelf-editor/src/commonMain/kotlin/org/modelix/editor/kernelf/Editor_org_iets3_core_expr_tracing.kt +++ b/kernelf-editor/src/commonMain/kotlin/org/modelix/editor/kernelf/Editor_org_iets3_core_expr_tracing.kt @@ -3,11 +3,12 @@ package org.modelix.editor.kernelf import org.iets3.core.expr.tracing.L_org_iets3_core_expr_tracing import org.modelix.aspects.languageAspects -val Editor_org_iets3_core_expr_tracing = languageAspects(L_org_iets3_core_expr_tracing) { +val Editor_org_iets3_core_expr_tracing = + languageAspects(L_org_iets3_core_expr_tracing) { // editor(language.GhostIconConcept) { // //TODO // } // editor(language.TracerIconConcept) { // //TODO // } -} + } diff --git a/kernelf-editor/src/commonMain/kotlin/org/modelix/editor/kernelf/KernelfAPI.kt b/kernelf-editor/src/commonMain/kotlin/org/modelix/editor/kernelf/KernelfAPI.kt index f8618008..baa7c8cc 100644 --- a/kernelf-editor/src/commonMain/kotlin/org/modelix/editor/kernelf/KernelfAPI.kt +++ b/kernelf-editor/src/commonMain/kotlin/org/modelix/editor/kernelf/KernelfAPI.kt @@ -7,8 +7,10 @@ import kotlinx.html.TagConsumer import kotlinx.html.consumers.DelayedConsumer import kotlinx.html.stream.HTMLStreamBuilder import org.iets3.core.expr.tests.N_TestSuite +import org.modelix.editor.CellTreeState import org.modelix.editor.EditorEngine -import org.modelix.editor.EditorState +import org.modelix.editor.text.frontend.layout +import org.modelix.editor.text.frontend.runLayoutOnCell import org.modelix.editor.toHtml import org.modelix.kernelf.KernelfLanguages import org.modelix.metamodel.ITypedNode @@ -27,7 +29,9 @@ import org.modelix.model.repositoryconcepts.N_Module import org.modelix.model.withIncrementalComputationSupport object KernelfAPI { - private val LOG = io.github.oshai.kotlinlogging.KotlinLogging.logger { } + private val LOG = + io.github.oshai.kotlinlogging.KotlinLogging + .logger { } val editorEngine = EditorEngine() init { @@ -35,9 +39,7 @@ object KernelfAPI { KernelfEditor.register(editorEngine) } - fun renderJsonAsHtmlText(json: String): String { - return renderModelAsHtmlText(ModelData.fromJson(json)) - } + fun renderJsonAsHtmlText(json: String): String = renderModelAsHtmlText(ModelData.fromJson(json)) fun loadModelFromJson(json: String): INode = loadModelsFromJson(arrayOf(json)) @@ -51,7 +53,13 @@ object KernelfAPI { return rootNode } - fun connectToModelServer(url: String? = null, initialJsonData: Array = emptyArray(), callback: (INode) -> Unit, errorCallback: (Exception) -> Unit = {}) { + fun connectToModelServer( + url: String? = null, + initialJsonData: Array = emptyArray(), + callback: (INode) -> Unit, + errorCallback: (Exception) -> Unit = { + }, + ) { GlobalScope.launch { try { if (url != null && (url.endsWith("/v2") || url.endsWith("/v2/"))) { @@ -63,7 +71,10 @@ object KernelfAPI { if (!repositoryExisted) { client.initRepository(repositoryId) } - val model: ReplicatedModel = client.getReplicatedModel(repositoryId.getBranchReference(), { ModelixIdGenerator(client.getIdGenerator(), it) }) + val model: ReplicatedModel = + client.getReplicatedModel(repositoryId.getBranchReference(), { + ModelixIdGenerator(client.getIdGenerator(), it) + }) model.start() TODO("Migration of IncrementalBranch to IMutableModelTree is needed") // val branch = model.getVersionedModelTree().withIncrementalComputationSupport() @@ -103,24 +114,31 @@ object KernelfAPI { } } - fun renderNodeAsHtmlText(rootNode: INode): String { - return renderTypedNodeAsHtmlText(rootNode.typed()) - } + fun renderNodeAsHtmlText(rootNode: INode): String = renderTypedNodeAsHtmlText(rootNode.typed()) fun renderTypedNodeAsHtmlText(rootNode: ITypedNode): String { val sb = StringBuilder() - renderTypedNode(EditorState(), rootNode, DelayedConsumer(HTMLStreamBuilder(out = sb, prettyPrint = true, xhtmlCompatible = true))) + renderTypedNode(CellTreeState(), rootNode, DelayedConsumer(HTMLStreamBuilder(out = sb, prettyPrint = true, xhtmlCompatible = true))) return sb.toString() } - fun renderNode(editorState: EditorState, rootNode: INode, tagConsumer: TagConsumer) { - renderTypedNode(editorState, rootNode.typed(), tagConsumer) + fun renderNode( + cellTreeState: CellTreeState, + rootNode: INode, + tagConsumer: TagConsumer, + ) { + renderTypedNode(cellTreeState, rootNode.typed(), tagConsumer) } - fun renderTypedNode(editorState: EditorState, rootNode: ITypedNode, tagConsumer: TagConsumer) { + fun renderTypedNode( + cellTreeState: CellTreeState, + rootNode: ITypedNode, + tagConsumer: TagConsumer, + ) { ModelFacade.readNode(rootNode.unwrap()) { - val cell = editorEngine.createCell(editorState, rootNode.unwrap()) - cell.layout.toHtml(tagConsumer) + val cell = editorEngine.createCell(cellTreeState, rootNode.unwrap()) + val layout = runLayoutOnCell(cell) + layout.toHtml(tagConsumer) } } @@ -134,34 +152,39 @@ object KernelfAPI { } fun nodeToString(node: Any): String { - val typedNode = when (node) { - is ITypedNode -> node - is INode -> node.typed() - else -> throw IllegalArgumentException("Unsupported node type: $node") - } + val typedNode = + when (node) { + is ITypedNode -> node + is INode -> node.typed() + else -> throw IllegalArgumentException("Unsupported node type: $node") + } return (if (typedNode is N_INamedConcept) typedNode.name else null) ?: typedNode.untypedConcept().getLongName() } - fun findTestSuites(rootNode: INode): Array { - return ModelFacade.readNode(rootNode) { - val modules = rootNode.getChildren("modules") - .map { TypedLanguagesRegistry.wrapNode(it) } - .filterIsInstance() - modules.flatMap { it.models }.flatMap { it.rootNodes } + fun findTestSuites(rootNode: INode): Array = + ModelFacade.readNode(rootNode) { + val modules = + rootNode + .getChildren("modules") + .map { TypedLanguagesRegistry.wrapNode(it) } + .filterIsInstance() + modules + .flatMap { it.models } + .flatMap { it.rootNodes } .filterIsInstance() .toTypedArray() } - } - fun getModules(rootNode: INode): Array { - return ModelFacade.readNode(rootNode) { - val modules = rootNode.getChildren("modules") - .map { TypedLanguagesRegistry.wrapNode(it) } - .filterIsInstance() - .map { it.unwrap() } + fun getModules(rootNode: INode): Array = + ModelFacade.readNode(rootNode) { + val modules = + rootNode + .getChildren("modules") + .map { TypedLanguagesRegistry.wrapNode(it) } + .filterIsInstance() + .map { it.unwrap() } modules.toTypedArray() } - } fun unwrapNode(node: Any): Any { if (node is INode) return node diff --git a/kernelf-editor/src/commonMain/kotlin/org/modelix/editor/kernelf/KernelfEditor.kt b/kernelf-editor/src/commonMain/kotlin/org/modelix/editor/kernelf/KernelfEditor.kt index ccbca7a1..1538e058 100644 --- a/kernelf-editor/src/commonMain/kotlin/org/modelix/editor/kernelf/KernelfEditor.kt +++ b/kernelf-editor/src/commonMain/kotlin/org/modelix/editor/kernelf/KernelfEditor.kt @@ -7,6 +7,7 @@ import org.modelix.editor.EditorEngine fun CellTemplateBuilder<*, *>.iets3keyword() { textColor("DarkBlue") } + fun CellTemplateBuilder<*, *>.iets3type() { textColor("rgb(0, 155, 191)") // TODO bold diff --git a/kernelf-editor/src/commonTest/kotlin/org/modelix/editor/kernelf/CodeCompletionTest.kt b/kernelf-editor/src/commonTest/kotlin/org/modelix/editor/kernelf/CodeCompletionTest.kt index e74a739e..334a6e73 100644 --- a/kernelf-editor/src/commonTest/kotlin/org/modelix/editor/kernelf/CodeCompletionTest.kt +++ b/kernelf-editor/src/commonTest/kotlin/org/modelix/editor/kernelf/CodeCompletionTest.kt @@ -1,134 +1,177 @@ package org.modelix.editor.kernelf +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.test.runTest import org.iets3.core.expr.simpleTypes.C_NumberLiteral import org.iets3.core.expr.simpleTypes.N_NumberLiteral import org.iets3.core.expr.tests.N_TestSuite import org.modelix.editor.CaretSelection import org.modelix.editor.Cell import org.modelix.editor.CodeCompletionParameters -import org.modelix.editor.EditorComponent import org.modelix.editor.EditorEngine +import org.modelix.editor.FrontendEditorComponent import org.modelix.editor.ICodeCompletionAction -import org.modelix.editor.celltemplate.descendants -import org.modelix.editor.celltemplate.firstLeaf import org.modelix.editor.commonAncestor import org.modelix.editor.descendants import org.modelix.editor.firstLeaf import org.modelix.editor.flattenApplicableActions import org.modelix.editor.getCompletionPattern import org.modelix.editor.getSubstituteActions -import org.modelix.editor.getVisibleText import org.modelix.editor.isVisible import org.modelix.editor.layoutable import org.modelix.editor.previousLeaf import org.modelix.editor.resolvePropertyCell +import org.modelix.editor.text.backend.TextEditorServiceImpl +import org.modelix.editor.text.frontend.getVisibleText +import org.modelix.editor.text.shared.celltree.cellReferences import org.modelix.incremental.IncrementalEngine import org.modelix.kernelf.KernelfLanguages import org.modelix.metamodel.ModelData import org.modelix.metamodel.descendants import org.modelix.metamodel.ofType +import org.modelix.metamodel.untypedReference import org.modelix.model.ModelFacade import org.modelix.model.api.IBranch import org.modelix.model.api.PBranch +import org.modelix.model.api.toSerialized import org.modelix.model.area.getArea import org.modelix.model.client.IdGenerator import org.modelix.model.repositoryconcepts.N_Module import org.modelix.model.repositoryconcepts.models import org.modelix.model.repositoryconcepts.rootNodes import org.modelix.model.withIncrementalComputationSupport -import kotlin.test.AfterTest -import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue class CodeCompletionTest { lateinit var numberLiteral: N_NumberLiteral - lateinit var editor: EditorComponent + lateinit var editor: FrontendEditorComponent lateinit var branch: IBranch lateinit var testSuite: N_TestSuite + lateinit var service: TextEditorServiceImpl - @BeforeTest - fun beforeTest() { + suspend fun CoroutineScope.beforeTest() { KernelfLanguages.registerAll() branch = PBranch(ModelFacade.newLocalTree(useRoleIds = false), IdGenerator.getInstance(56754)).withIncrementalComputationSupport() ModelData.fromJson(modelJson).load(branch) - val engine = EditorEngine(IncrementalEngine()) KernelfEditor.register(engine) - testSuite = branch.computeRead { branch.getArea().getRoot().allChildren.ofType().models.rootNodes.ofType().first() } - editor = engine.editNode(testSuite) + testSuite = + branch.computeRead { + branch + .getArea() + .getRoot() + .allChildren + .ofType() + .models.rootNodes + .ofType() + .first() + } + service = TextEditorServiceImpl(engine, branch.getArea().asModel(), this) + editor = FrontendEditorComponent(service) + editor.editNode(testSuite.untypedReference().toSerialized()) numberLiteral = branch.computeRead { testSuite.descendants().first() } - editor.selectAfterUpdate { - val cell = editor.resolvePropertyCell(C_NumberLiteral.value, numberLiteral) - CaretSelection(cell!!.layoutable()!!, 0) + editor.flushAndUpdateSelection { + val cell = + requireNotNull(editor.resolvePropertyCell(C_NumberLiteral.value, numberLiteral)) { + "Property cell not found. \n" + + editor + .getRootCell() + .descendants() + .flatMap { it.cellReferences } + .joinToString("\n") + } + val layoutable = + requireNotNull(cell.layoutable()) { + "Layoutable not found" + } + CaretSelection(editor, layoutable, 0) } - editor.update() } - @AfterTest - fun afterTest() { + suspend fun CoroutineScope.afterTest() { KernelfLanguages.languages.forEach { it.unregister() } + editor.dispose() + service.dispose() } @Test - fun printModel() { - println(editor.getRootCell().layout.toString()) - } + fun printModel() = + runCompletionTest { + println(editor.getRootCell().layout.toString()) + } @Test - fun printActions() { - val actions = getSubstituteActions(getNumberLiteralCell()) - val parameters = CodeCompletionParameters(editor, "") - actions.forEach { println(it.getCompletionPattern() + " | " + it.getDescription()) } - } + fun printActions() = + runCompletionTest { + val actions = getSubstituteActions(getNumberLiteralCell()) + actions.forEach { println(it.getCompletionPattern() + " | " + it.getDescription()) } + } @Test - fun notEmpty() { - val actions = getSubstituteActions(getNumberLiteralCell()) - assertTrue(actions.isNotEmpty()) - } + fun notEmpty() = + runCompletionTest { + val actions = getSubstituteActions(getNumberLiteralCell()) + assertTrue(actions.isNotEmpty()) + } @Test - fun actionsOnNameProperty() { - val namePropertyCell = editor.getRootCell().descendants().find { it.getVisibleText() == "stringTests" }!! - editor.changeSelection(CaretSelection(namePropertyCell.layoutable()!!, 0)) + fun actionsOnNameProperty() = + runCompletionTest { + val namePropertyCell = editor.getRootCell().descendants().find { it.getVisibleText() == "stringTests" }!! + editor.changeSelectionLater(CaretSelection(editor, namePropertyCell.layoutable()!!, 0)) - val firstLeaf = namePropertyCell.firstLeaf() - assertEquals("stringTests", firstLeaf.getVisibleText()) - val previousLeaf = namePropertyCell.previousLeaf { it.isVisible() }!! - assertEquals("test case", previousLeaf.getVisibleText()) - val commonAncestor = previousLeaf.commonAncestor(firstLeaf) - assertEquals(namePropertyCell.parent, commonAncestor) + val firstLeaf = namePropertyCell.firstLeaf() + assertEquals("stringTests", firstLeaf.getVisibleText()) + val previousLeaf = namePropertyCell.previousLeaf { it.isVisible() }!! + assertEquals("test case", previousLeaf.getVisibleText()) + val commonAncestor = previousLeaf.commonAncestor(firstLeaf) + assertEquals(namePropertyCell.getParent(), commonAncestor) - val actions = getSubstituteActions(namePropertyCell) - assertEquals(emptyList(), actions) - } + val actions = getSubstituteActions(namePropertyCell) + assertEquals(emptyList(), actions) + } @Test - fun noDuplicates() { - val parameters = CodeCompletionParameters(editor, "") - val actions = getSubstituteActions(getNumberLiteralCell()) - val knownDuplicates = setOf( - "it", - "ParamRef { }", - "StripUnitExpression { }", - "ValExpression { }" - ) - val actualDuplicates = actions.groupBy { it.getCompletionPattern() }.filter { it.value.size > 1 }.map { it.key } - val unexpectedDuplicates = actualDuplicates - knownDuplicates - val missingDuplicates = knownDuplicates - actualDuplicates - assertTrue(unexpectedDuplicates.isEmpty(), "Duplicate entries found: " + unexpectedDuplicates) - assertTrue(missingDuplicates.isEmpty(), "These entries aren't duplicates anymore: " + missingDuplicates) - } + fun noDuplicates() = + runCompletionTest { + val actions = getSubstituteActions(getNumberLiteralCell()) + val knownDuplicates = + setOf( + "it", + "ParamRef { }", + "StripUnitExpression { }", + "ValExpression { }" + ) + val actualDuplicates = actions.groupBy { it.getCompletionPattern() }.filter { it.value.size > 1 }.map { it.key } + val unexpectedDuplicates = actualDuplicates - knownDuplicates + val missingDuplicates = knownDuplicates - actualDuplicates + assertTrue(unexpectedDuplicates.isEmpty(), "Duplicate entries found: " + unexpectedDuplicates) + assertTrue(missingDuplicates.isEmpty(), "These entries aren't duplicates anymore: " + missingDuplicates) + } private fun getNumberLiteralCell() = editor.resolvePropertyCell(C_NumberLiteral.value, numberLiteral)!! private fun getSubstituteActions(cell: Cell): List { - val parameters = CodeCompletionParameters(editor, "") + val cell = service.getEditorBackend(editor.editorId).tree.getCell(cell.getId()) + val parameters = CodeCompletionParameters(service.getEditorBackend(editor.editorId), "") return branch.computeRead { - cell.getSubstituteActions().flatMap { it.flattenApplicableActions(parameters) } - .sortedBy { it.getCompletionPattern() }.toList() + cell + .getSubstituteActions() + .flatMap { it.flattenApplicableActions(parameters) } + .sortedBy { it.getCompletionPattern() } + .toList() } } + + private fun runCompletionTest(body: suspend () -> Unit) = + runTest { + try { + beforeTest() + body() + } finally { + afterTest() + } + } } diff --git a/kernelf-editor/src/commonTest/kotlin/org/modelix/editor/kernelf/IncrementalLayoutAfterInsert.kt b/kernelf-editor/src/commonTest/kotlin/org/modelix/editor/kernelf/IncrementalLayoutAfterInsert.kt index ea6af012..9801b80c 100644 --- a/kernelf-editor/src/commonTest/kotlin/org/modelix/editor/kernelf/IncrementalLayoutAfterInsert.kt +++ b/kernelf-editor/src/commonTest/kotlin/org/modelix/editor/kernelf/IncrementalLayoutAfterInsert.kt @@ -1,24 +1,27 @@ package org.modelix.editor.kernelf +import kotlinx.coroutines.test.runTest import org.iets3.core.expr.tests.N_AssertTestItem import org.iets3.core.expr.tests.N_TestSuite import org.modelix.editor.CaretSelection -import org.modelix.editor.EditorComponent import org.modelix.editor.EditorEngine +import org.modelix.editor.FrontendEditorComponent import org.modelix.editor.JSKeyboardEvent import org.modelix.editor.JSKeyboardEventType import org.modelix.editor.KnownKeys -import org.modelix.editor.celltemplate.firstLeaf import org.modelix.editor.firstLeaf import org.modelix.editor.isVisible import org.modelix.editor.layoutable import org.modelix.editor.nextLeafs import org.modelix.editor.resolveNodeCell +import org.modelix.editor.text.backend.TextEditorServiceImpl import org.modelix.incremental.IncrementalEngine import org.modelix.kernelf.KernelfLanguages import org.modelix.metamodel.ModelData import org.modelix.metamodel.descendants import org.modelix.metamodel.ofType +import org.modelix.metamodel.untyped +import org.modelix.metamodel.untypedReference import org.modelix.model.ModelFacade import org.modelix.model.api.IBranch import org.modelix.model.api.PBranch @@ -28,47 +31,60 @@ import org.modelix.model.repositoryconcepts.N_Module import org.modelix.model.repositoryconcepts.models import org.modelix.model.repositoryconcepts.rootNodes import org.modelix.model.withIncrementalComputationSupport -import kotlin.test.AfterTest -import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals open class IncrementalLayoutAfterInsert { lateinit var assertTestItem: N_AssertTestItem - lateinit var editor: EditorComponent + lateinit var editor: FrontendEditorComponent lateinit var branch: IBranch lateinit var testSuite: N_TestSuite - @BeforeTest - fun beforeTest() { - KernelfLanguages.registerAll() - branch = PBranch(ModelFacade.newLocalTree(useRoleIds = false), IdGenerator.getInstance(56754)).withIncrementalComputationSupport() - ModelData.fromJson(modelJson).load(branch) - - val engine = EditorEngine(IncrementalEngine()) - KernelfEditor.register(engine) - testSuite = branch.computeRead { branch.getArea().getRoot().allChildren.ofType().models.rootNodes.ofType().first() } - editor = engine.editNode(testSuite) - assertTestItem = branch.computeRead { testSuite.descendants().drop(1).first() } - editor.selectAfterUpdate { - val cell = editor.resolveNodeCell(assertTestItem)!!.firstLeaf().nextLeafs(true).first { it.isVisible() } - println(cell.toString()) - CaretSelection(cell.layoutable()!!, 0) + @Test + fun layoutAfterInsert() = + runLayoutTest { + editor.processKeyEvent(JSKeyboardEvent(JSKeyboardEventType.KEYDOWN, KnownKeys.Enter)) + val incrementalText = editor.getRootCell().layout.toString() + editor.clearLayoutCache() + val nonIncrementalText = editor.getRootCell().layout.toString() + assertEquals(nonIncrementalText, incrementalText) } - editor.update() - } - @AfterTest - fun afterTest() { - KernelfLanguages.languages.forEach { it.unregister() } - } + private fun runLayoutTest(body: suspend () -> Unit) = + runTest { + KernelfLanguages.registerAll() + branch = + PBranch(ModelFacade.newLocalTree(useRoleIds = false), IdGenerator.getInstance(56754)).withIncrementalComputationSupport() + ModelData.fromJson(modelJson).load(branch) - @Test - fun layoutAfterInsert() { - editor.processKeyEvent(JSKeyboardEvent(JSKeyboardEventType.KEYDOWN, KnownKeys.Enter)) - val incrementalText = editor.getRootCell().layout.toString() - editor.clearLayoutCache() - val nonIncrementalText = editor.getRootCell().layout.toString() - assertEquals(nonIncrementalText, incrementalText) - } + val engine = EditorEngine(IncrementalEngine()) + KernelfEditor.register(engine) + testSuite = + branch.computeRead { + branch + .getArea() + .getRoot() + .allChildren + .ofType() + .models.rootNodes + .ofType() + .first() + } + editor = + FrontendEditorComponent(TextEditorServiceImpl(engine, testSuite.untyped().asWritableNode().getModel(), backgroundScope)) + editor.editNode(testSuite.untypedReference()) + assertTestItem = branch.computeRead { testSuite.descendants().drop(1).first() } + editor.flushAndUpdateSelection { + val cell = + editor + .resolveNodeCell(assertTestItem)!! + .firstLeaf() + .nextLeafs(true) + .first { it.isVisible() } + println(cell.toString()) + CaretSelection(cell.layoutable()!!, 0) + } + body() + KernelfLanguages.languages.forEach { it.unregister() } + } } diff --git a/kernelf-editor/src/commonTest/kotlin/org/modelix/editor/kernelf/ParsingTest.kt b/kernelf-editor/src/commonTest/kotlin/org/modelix/editor/kernelf/ParsingTest.kt index 3c7864ae..4dfc87d4 100644 --- a/kernelf-editor/src/commonTest/kotlin/org/modelix/editor/kernelf/ParsingTest.kt +++ b/kernelf-editor/src/commonTest/kotlin/org/modelix/editor/kernelf/ParsingTest.kt @@ -12,7 +12,6 @@ import kotlin.test.assertTrue import kotlin.time.measureTime class ParsingTest { - @Test fun test() = runParsingTest("1+2") @Test fun test2() = runParsingTest("1 + 2") @@ -99,8 +98,13 @@ class ParsingTest { @Test fun completion12() = runCompletionTest("""(1 * ᚹ""") private fun runCompletionTest(inputString: String) = runTest(inputString, true) + private fun runParsingTest(inputString: String) = runTest(inputString, false) - private fun runTest(inputString: String, complete: Boolean = false) { + + private fun runTest( + inputString: String, + complete: Boolean = false, + ) { KernelfLanguages.registerAll() val engine = EditorEngine(IncrementalEngine()) @@ -112,9 +116,10 @@ class ParsingTest { KernelfLanguages.languages.forEach { it.unregister() } val parseTrees: List - val time = measureTime { - parseTrees = parser.parseForest(inputString, complete).toList() - } + val time = + measureTime { + parseTrees = parser.parseForest(inputString, complete).toList() + } // repeat(100) { parser.parseForest(inputString, complete).toList() } println(time) assertTrue(parseTrees.isNotEmpty()) diff --git a/kernelf-editor/src/commonTest/kotlin/org/modelix/editor/kernelf/PropertyChangeTest.kt b/kernelf-editor/src/commonTest/kotlin/org/modelix/editor/kernelf/PropertyChangeTest.kt index 31b56329..004a9764 100644 --- a/kernelf-editor/src/commonTest/kotlin/org/modelix/editor/kernelf/PropertyChangeTest.kt +++ b/kernelf-editor/src/commonTest/kotlin/org/modelix/editor/kernelf/PropertyChangeTest.kt @@ -1,5 +1,6 @@ package org.modelix.editor.kernelf +import kotlinx.coroutines.test.runTest import org.iets3.core.expr.base.C_MinusExpression import org.iets3.core.expr.base.C_ParensExpression import org.iets3.core.expr.base.C_PlusExpression @@ -8,8 +9,8 @@ import org.iets3.core.expr.simpleTypes.C_NumberLiteral import org.iets3.core.expr.simpleTypes.N_NumberLiteral import org.modelix.editor.CaretSelection import org.modelix.editor.CodeCompletionParameters -import org.modelix.editor.EditorComponent import org.modelix.editor.EditorEngine +import org.modelix.editor.FrontendEditorComponent import org.modelix.editor.ICodeCompletionAction import org.modelix.editor.JSKeyboardEvent import org.modelix.editor.JSKeyboardEventType @@ -20,75 +21,100 @@ import org.modelix.editor.getCompletionPattern import org.modelix.editor.getSubstituteActions import org.modelix.editor.layoutable import org.modelix.editor.resolvePropertyCell +import org.modelix.editor.text.backend.TextEditorServiceImpl +import org.modelix.editor.text.frontend.backend +import org.modelix.editor.text.shared.celltree.ICellTree import org.modelix.incremental.IncrementalEngine import org.modelix.kernelf.KernelfLanguages import org.modelix.metamodel.setNew import org.modelix.metamodel.typed +import org.modelix.metamodel.untyped +import org.modelix.metamodel.untypedReference import org.modelix.model.ModelFacade import org.modelix.model.area.PArea import org.modelix.model.withIncrementalComputationSupport -import kotlin.test.AfterTest -import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue class PropertyChangeTest { lateinit var numberLiteral: N_NumberLiteral - lateinit var editor: EditorComponent + lateinit var editor: FrontendEditorComponent + lateinit var service: TextEditorServiceImpl - @BeforeTest - fun beforeTest() { - KernelfLanguages.registerAll() - val branch = ModelFacade.toLocalBranch(ModelFacade.newLocalTree(useRoleIds = false)).withIncrementalComputationSupport() - val parensExpression = branch.computeWrite { - val parensExpression = PArea(branch).getRoot().addNewChild("root", -1, C_ParensExpression.untyped()).typed() - parensExpression.apply { - expr.setNew(C_PlusExpression) { - left.setNew(C_MinusExpression) { - left.setNew(C_NumberLiteral) { - numberLiteral = this - value = "200" - } - right.setNew(C_NumberLiteral) { - value = "56" - } - } - right.setNew(C_NumberLiteral) { - value = "100" - } - } - } - } + private fun ICellTree.Cell.backend() = backend(service, editor) - val engine = EditorEngine(IncrementalEngine()) - KernelfEditor.register(engine) - editor = engine.editNode(parensExpression) - editor.selectAfterUpdate { - val cell = editor.resolvePropertyCell(C_NumberLiteral.value, numberLiteral!!) - CaretSelection(cell!!.layoutable()!!, 0) + @Test + fun propertyChange() = + runPropertyTest { + assertEquals("200", numberLiteral.value) + editor.processKeyEvent( + JSKeyboardEvent(JSKeyboardEventType.KEYDOWN, "8", null, "8", Modifiers.NONE, KeyLocation.STANDARD, false, false) + ) + assertEquals("8200", numberLiteral.value) } - editor.update() - } - - @AfterTest - fun afterTest() { - KernelfLanguages.languages.forEach { it.unregister() } - } @Test - fun propertyChange() { - assertEquals("200", numberLiteral.value) - editor.processKeyEvent(JSKeyboardEvent(JSKeyboardEventType.KEYDOWN, "8", null, "8", Modifiers.NONE, KeyLocation.STANDARD, false, false)) - assertEquals("8200", numberLiteral.value) - } + fun substituteActions() = + runPropertyTest { + val parameters = CodeCompletionParameters(service.getEditorBackend(editor.editorId), "") + val cell = editor.resolvePropertyCell(C_NumberLiteral.value, numberLiteral)!! + val actions: List = + cell + .backend() + .getSubstituteActions() + .flatMap { + it.flattenApplicableActions(parameters) + }.toList() + actions.forEach { println(it.getCompletionPattern() + " | " + it.getDescription()) } + assertTrue(actions.isNotEmpty()) + } - @Test - fun substituteActions() { - val parameters = CodeCompletionParameters(editor, "") - val cell = editor.resolvePropertyCell(C_NumberLiteral.value, numberLiteral)!! - val actions: List = cell.getSubstituteActions().flatMap { it.flattenApplicableActions(parameters) }.toList() - actions.forEach { println(it.getCompletionPattern() + " | " + it.getDescription()) } - assertTrue(actions.isNotEmpty()) - } + private fun runPropertyTest(body: suspend () -> Unit) = + runTest { + KernelfLanguages.registerAll() + val branch = ModelFacade.toLocalBranch(ModelFacade.newLocalTree(useRoleIds = false)).withIncrementalComputationSupport() + val parensExpression = + branch.computeWrite { + val parensExpression = + PArea( + branch + ).getRoot().addNewChild("root", -1, C_ParensExpression.untyped()).typed() + parensExpression.apply { + expr.setNew(C_PlusExpression) { + left.setNew(C_MinusExpression) { + left.setNew(C_NumberLiteral) { + numberLiteral = this + value = "200" + } + right.setNew(C_NumberLiteral) { + value = "56" + } + } + right.setNew(C_NumberLiteral) { + value = "100" + } + } + } + } + + val engine = EditorEngine(IncrementalEngine()) + KernelfEditor.register(engine) + service = TextEditorServiceImpl(engine, parensExpression.untyped().asWritableNode().getModel(), backgroundScope) + editor = FrontendEditorComponent(service) + editor.editNode(parensExpression.untypedReference()) + editor.flushAndUpdateSelection { + val cell = + checkNotNull(editor.resolvePropertyCell(C_NumberLiteral.value, numberLiteral)) { + "Cell for property 'value' not found" + } + val layoutable = + checkNotNull(cell.layoutable()) { + "Layoutable not found" + } + CaretSelection(layoutable, 0) + } + body() + KernelfLanguages.languages.forEach { it.unregister() } + } } diff --git a/kernelf-editor/src/commonTest/kotlin/org/modelix/editor/kernelf/TestModel.kt b/kernelf-editor/src/commonTest/kotlin/org/modelix/editor/kernelf/TestModel.kt index bdb7b1ea..20ab5fd0 100644 --- a/kernelf-editor/src/commonTest/kotlin/org/modelix/editor/kernelf/TestModel.kt +++ b/kernelf-editor/src/commonTest/kotlin/org/modelix/editor/kernelf/TestModel.kt @@ -1,6 +1,7 @@ package org.modelix.editor.kernelf -val modelJson = """ +val modelJson = + """ { "root": { "id": "", @@ -3745,7 +3746,7 @@ val modelJson = """ ] } } -""".trimIndent() + """.trimIndent() val modelJson2 = """ { diff --git a/kernelf-editor/src/commonTest/kotlin/org/modelix/editor/kernelf/TypesystemTest.kt b/kernelf-editor/src/commonTest/kotlin/org/modelix/editor/kernelf/TypesystemTest.kt index 018e62d9..f100f172 100644 --- a/kernelf-editor/src/commonTest/kotlin/org/modelix/editor/kernelf/TypesystemTest.kt +++ b/kernelf-editor/src/commonTest/kotlin/org/modelix/editor/kernelf/TypesystemTest.kt @@ -45,9 +45,17 @@ class TypesystemTest { branch = PBranch(ModelFacade.newLocalTree(useRoleIds = false), IdGenerator.getInstance(56754)).withIncrementalComputationSupport() ModelData.fromJson(modelJson2).load(branch) - testSuite = branch.computeRead { - branch.getArea().getRoot().allChildren.ofType().models.rootNodes.ofType().first() - } + testSuite = + branch.computeRead { + branch + .getArea() + .getRoot() + .allChildren + .ofType() + .models.rootNodes + .ofType() + .first() + } recordMemberRef = branch.computeRead { testSuite.descendants().first() } } diff --git a/kernelf-editor/src/jsMain/kotlin/KernelfApiJS.kt b/kernelf-editor/src/jsMain/kotlin/KernelfApiJS.kt index 5b3aecb4..a289315b 100644 --- a/kernelf-editor/src/jsMain/kotlin/KernelfApiJS.kt +++ b/kernelf-editor/src/jsMain/kotlin/KernelfApiJS.kt @@ -2,15 +2,17 @@ import kotlinx.atomicfu.atomic import kotlinx.browser.document import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.cancel import kotlinx.coroutines.launch import kotlinx.html.dom.createTree -import org.modelix.editor.EditorState +import org.modelix.editor.CellTreeState import org.modelix.editor.GeneratedHtmlMap import org.modelix.editor.IncrementalVirtualDOMBuilder import org.modelix.editor.JSDom import org.modelix.editor.JsEditorComponent import org.modelix.editor.kernelf.KernelfAPI +import org.modelix.editor.text.backend.TextEditorServiceImpl import org.modelix.editor.unwrap import org.modelix.model.ModelFacade import org.modelix.model.api.IBranchListener @@ -18,6 +20,7 @@ import org.modelix.model.api.INode import org.modelix.model.api.ITree import org.modelix.model.api.JSNodeConverter import org.modelix.model.api.deepUnwrap +import org.modelix.model.api.toSerialized import org.modelix.model.area.IAreaChangeList import org.modelix.model.area.IAreaListener import org.w3c.dom.HTMLElement @@ -25,33 +28,48 @@ import org.w3c.dom.Node @JsExport object KernelfApiJS { - private val LOG = io.github.oshai.kotlinlogging.KotlinLogging.logger {} + private val LOG = + io.github.oshai.kotlinlogging.KotlinLogging + .logger {} private val generatedHtmlMap = GeneratedHtmlMap() - fun connectToModelServer(json: Array, callback: (INode) -> Unit) { + fun connectToModelServer( + json: Array, + callback: (INode) -> Unit, + ) { KernelfAPI.connectToModelServer( initialJsonData = json, callback = callback, errorCallback = { LOG.error(it) { "Failed to connect to model server" } }, ) } + fun loadModelsFromJson(json: Array): INode = KernelfAPI.loadModelsFromJson(json) + fun getModules(rootNode: INode): Array = KernelfAPI.getModules(rootNode) + fun nodeToString(node: Any): String = KernelfAPI.nodeToString(JSNodeConverter.toINode(node)) fun getNodeConverter() = JSNodeConverter - private fun renderNodeAsDom(editorState: EditorState, rootNode: INode): HTMLElement { + private fun renderNodeAsDom( + cellTreeState: CellTreeState, + rootNode: INode, + ): HTMLElement { val tagConsumer = document.createTree() - KernelfAPI.renderNode(editorState, rootNode, tagConsumer) + KernelfAPI.renderNode(cellTreeState, rootNode, tagConsumer) return tagConsumer.finalize() } - fun updateNodeAsDom(editorState: EditorState, rootNode: INode, parentElement: HTMLElement) { + fun updateNodeAsDom( + cellTreeState: CellTreeState, + rootNode: INode, + parentElement: HTMLElement, + ) { val existing = parentElement.firstElementChild as? HTMLElement val virtualDom = JSDom(parentElement.ownerDocument!!) val consumer = IncrementalVirtualDOMBuilder(virtualDom, existing?.let { virtualDom.wrap(it) }, generatedHtmlMap) - KernelfAPI.renderNode(editorState, rootNode, consumer) + KernelfAPI.renderNode(cellTreeState, rootNode, consumer) val newHtml = consumer.finalize() if (newHtml != existing) { if (existing != null) parentElement.removeChild(existing) @@ -60,49 +78,60 @@ object KernelfApiJS { } fun renderAndUpdateNodeAsDom(rootNode: INode): HTMLElement { - val editor = JsEditorComponent(KernelfAPI.editorEngine, rootNode.getArea()) { state -> - KernelfAPI.editorEngine.createCell(state, rootNode) + val service = TextEditorServiceImpl(KernelfAPI.editorEngine, rootNode.getArea().asModel(), GlobalScope) + val editor = JsEditorComponent(service) + GlobalScope.launch { + editor.editNode(rootNode.reference.toSerialized()) } val branch = ModelFacade.getBranch(rootNode)?.deepUnwrap() if (branch != null) { - branch.addListener(object : IBranchListener { - private var updateScheduled = atomic(false) - private val coroutinesScope = CoroutineScope(Dispatchers.Main) - override fun treeChanged(oldTree: ITree?, newTree: ITree) { - if (editor.containerElement.unwrap().isInDocument()) { - if (!updateScheduled.getAndSet(true)) { - coroutinesScope.launch { - updateScheduled.getAndSet(false) - editor.update() + branch.addListener( + object : IBranchListener { + private var updateScheduled = atomic(false) + private val coroutinesScope = CoroutineScope(Dispatchers.Main) + + override fun treeChanged( + oldTree: ITree?, + newTree: ITree, + ) { + if (editor.containerElement.unwrap().isInDocument()) { + if (!updateScheduled.getAndSet(true)) { + coroutinesScope.launch { + updateScheduled.getAndSet(false) + editor.updateNow() + } } + } else { + coroutinesScope.cancel("Editor removed from document") + branch.removeListener(this) + editor.dispose() } - } else { - coroutinesScope.cancel("Editor removed from document") - branch.removeListener(this) - editor.dispose() } } - }) + ) } else { val area = rootNode.getArea() - area.addListener(object : IAreaListener { - private var updateScheduled = atomic(false) - private val coroutinesScope = CoroutineScope(Dispatchers.Main) - override fun areaChanged(changes: IAreaChangeList) { - if (editor.containerElement.unwrap().isInDocument()) { - if (!updateScheduled.getAndSet(true)) { - coroutinesScope.launch { - updateScheduled.getAndSet(false) - editor.update() + area.addListener( + object : IAreaListener { + private var updateScheduled = atomic(false) + private val coroutinesScope = CoroutineScope(Dispatchers.Main) + + override fun areaChanged(changes: IAreaChangeList) { + if (editor.containerElement.unwrap().isInDocument()) { + if (!updateScheduled.getAndSet(true)) { + coroutinesScope.launch { + updateScheduled.getAndSet(false) + editor.updateNow() + } } + } else { + coroutinesScope.cancel("Editor removed from document") + area.removeListener(this) + editor.dispose() } - } else { - coroutinesScope.cancel("Editor removed from document") - area.removeListener(this) - editor.dispose() } } - }) + ) } editor.updateHtml() return editor.containerElement.unwrap() diff --git a/kernelf-editor/src/jsTest/kotlin/IncrementalDomTest.kt b/kernelf-editor/src/jsTest/kotlin/IncrementalDomTest.kt index 9daeda63..9f538a5e 100644 --- a/kernelf-editor/src/jsTest/kotlin/IncrementalDomTest.kt +++ b/kernelf-editor/src/jsTest/kotlin/IncrementalDomTest.kt @@ -1,7 +1,7 @@ import kotlinx.browser.document import kotlinx.html.dom.create import kotlinx.html.js.div -import org.modelix.editor.EditorState +import org.modelix.editor.CellTreeState import org.modelix.editor.kernelf.KernelfAPI import org.modelix.editor.kernelf.modelJson import org.w3c.dom.Node @@ -27,17 +27,18 @@ class IncrementalDomTest { val model = KernelfAPI.loadModelFromJson(modelJson) val testSuites = KernelfAPI.findTestSuites(model) val containerElement = document.create.div() - val editorState = EditorState() - KernelfApiJS.updateNodeAsDom(editorState, testSuites.first().unwrap(), containerElement) + val cellTreeState = CellTreeState() + KernelfApiJS.updateNodeAsDom(cellTreeState, testSuites.first().unwrap(), containerElement) val elements1 = containerElement.descendants().toList() testSuites.first().name = "changed" - KernelfApiJS.updateNodeAsDom(editorState, testSuites.first().unwrap(), containerElement) + KernelfApiJS.updateNodeAsDom(cellTreeState, testSuites.first().unwrap(), containerElement) val elements2 = containerElement.descendants().toList() assertEquals(elements1.size, elements2.size) - val expectedChanges = elements1.indices.joinToString { - val element2 = elements2[it] - if (element2 is Text && element2.textContent == "changed") "C" else "-" - } + val expectedChanges = + elements1.indices.joinToString { + val element2 = elements2[it] + if (element2 is Text && element2.textContent == "changed") "C" else "-" + } val actualChanges = elements1.indices.joinToString { if (elements1[it] === elements2[it]) "-" else "C" } println(actualChanges) assertEquals(expectedChanges, actualChanges) diff --git a/kernelf-editor/src/jsTest/kotlin/org/modelix/editor/kernelf/IncrementalLayouterAfterInsertJS.kt b/kernelf-editor/src/jsTest/kotlin/org/modelix/editor/kernelf/IncrementalLayouterAfterInsertJS.kt index 78ba4cc2..dad3cc27 100644 --- a/kernelf-editor/src/jsTest/kotlin/org/modelix/editor/kernelf/IncrementalLayouterAfterInsertJS.kt +++ b/kernelf-editor/src/jsTest/kotlin/org/modelix/editor/kernelf/IncrementalLayouterAfterInsertJS.kt @@ -1,6 +1,7 @@ package org.modelix.editor.kernelf import kotlinx.browser.document +import kotlinx.coroutines.test.runTest import kotlinx.html.dom.create import kotlinx.html.js.div import org.iets3.core.expr.tests.N_AssertTestItem @@ -14,12 +15,12 @@ import org.modelix.editor.JSKeyboardEvent import org.modelix.editor.JSKeyboardEventType import org.modelix.editor.JsEditorComponent import org.modelix.editor.KnownKeys -import org.modelix.editor.celltemplate.firstLeaf import org.modelix.editor.firstLeaf import org.modelix.editor.isVisible import org.modelix.editor.layoutable import org.modelix.editor.nextLeafs import org.modelix.editor.resolveNodeCell +import org.modelix.editor.text.backend.TextEditorServiceImpl import org.modelix.editor.toHtml import org.modelix.editor.unwrap import org.modelix.incremental.IncrementalEngine @@ -27,7 +28,7 @@ import org.modelix.kernelf.KernelfLanguages import org.modelix.metamodel.ModelData import org.modelix.metamodel.descendants import org.modelix.metamodel.ofType -import org.modelix.metamodel.untyped +import org.modelix.metamodel.untypedReference import org.modelix.model.ModelFacade import org.modelix.model.api.IBranch import org.modelix.model.api.PBranch @@ -37,8 +38,6 @@ import org.modelix.model.repositoryconcepts.N_Module import org.modelix.model.repositoryconcepts.models import org.modelix.model.repositoryconcepts.rootNodes import org.modelix.model.withIncrementalComputationSupport -import kotlin.test.AfterTest -import kotlin.test.BeforeTest import kotlin.test.Ignore import kotlin.test.Test import kotlin.test.assertEquals @@ -48,47 +47,89 @@ open class IncrementalLayoutAfterInsertJS { lateinit var editor: JsEditorComponent lateinit var branch: IBranch lateinit var testSuite: N_TestSuite - - @BeforeTest - fun beforeTest() { - KernelfLanguages.registerAll() - branch = PBranch(ModelFacade.newLocalTree(), IdGenerator.getInstance(56754)).withIncrementalComputationSupport() - ModelData.fromJson(modelJson).load(branch) - - val engine = EditorEngine(IncrementalEngine()) - KernelfEditor.register(engine) - testSuite = branch.computeRead { branch.getArea().getRoot().allChildren.ofType().models.rootNodes.ofType().first() } - editor = JsEditorComponent(engine, branch.getArea(), { editorState -> branch.computeRead { engine.createCell(editorState, testSuite.untyped()) } }) - assertTestItem = branch.computeRead { testSuite.descendants().drop(1).first() } - editor.selectAfterUpdate { - val cell = editor.resolveNodeCell(assertTestItem)!!.firstLeaf().nextLeafs(true).first { it.isVisible() } - println(cell.toString()) - CaretSelection(cell.layoutable()!!, 0) - } - editor.update() - } - - @AfterTest - fun afterTest() { - KernelfLanguages.languages.forEach { it.unregister() } - } + lateinit var service: TextEditorServiceImpl @Ignore @Test - fun domAfterInsert() { - val containerElement = document.create.div() - val generatedHtmlMap = GeneratedHtmlMap() - var consumer = JSDom(containerElement.ownerDocument!!).let { vdom -> IncrementalVirtualDOMBuilder(vdom, vdom.wrap(containerElement), generatedHtmlMap) } - val initialHtml = editor.getRootCell().layout.toHtml(consumer).unwrap().outerHTML + fun domAfterInsert() = + runLayoutTest { + val containerElement = document.create.div() + val generatedHtmlMap = GeneratedHtmlMap() + var consumer = + JSDom(containerElement.ownerDocument!!).let { vdom -> + IncrementalVirtualDOMBuilder(vdom, vdom.wrap(containerElement), generatedHtmlMap) + } + val initialHtml = + editor + .getRootCell() + .layout + .toHtml(consumer) + .unwrap() + .outerHTML - editor.processKeyEvent(JSKeyboardEvent(JSKeyboardEventType.KEYDOWN, KnownKeys.Enter)) - consumer = JSDom(containerElement.ownerDocument!!).let { vdom -> IncrementalVirtualDOMBuilder(vdom, vdom.wrap(containerElement), generatedHtmlMap) } - val incrementalHtml = editor.getRootCell().layout.toHtml(consumer).unwrap().outerHTML + editor.processKeyEvent(JSKeyboardEvent(JSKeyboardEventType.KEYDOWN, KnownKeys.Enter)) + consumer = + JSDom(containerElement.ownerDocument!!).let { vdom -> + IncrementalVirtualDOMBuilder(vdom, vdom.wrap(containerElement), generatedHtmlMap) + } + val incrementalHtml = + editor + .getRootCell() + .layout + .toHtml(consumer) + .unwrap() + .outerHTML - editor.clearLayoutCache() + editor.clearLayoutCache() - consumer = JSDom(containerElement.ownerDocument!!).let { vdom -> IncrementalVirtualDOMBuilder(vdom, vdom.wrap(containerElement), generatedHtmlMap) } - val nonIncrementalHtml = editor.getRootCell().layout.toHtml(consumer).unwrap().outerHTML - assertEquals(nonIncrementalHtml, incrementalHtml) - } + consumer = + JSDom(containerElement.ownerDocument!!).let { vdom -> + IncrementalVirtualDOMBuilder(vdom, vdom.wrap(containerElement), generatedHtmlMap) + } + val nonIncrementalHtml = + editor + .getRootCell() + .layout + .toHtml(consumer) + .unwrap() + .outerHTML + assertEquals(nonIncrementalHtml, incrementalHtml) + } + + private fun runLayoutTest(body: suspend () -> Unit) = + runTest { + KernelfLanguages.registerAll() + branch = PBranch(ModelFacade.newLocalTree(), IdGenerator.getInstance(56754)).withIncrementalComputationSupport() + ModelData.fromJson(modelJson).load(branch) + + val engine = EditorEngine(IncrementalEngine()) + KernelfEditor.register(engine) + testSuite = + branch.computeRead { + branch + .getArea() + .getRoot() + .allChildren + .ofType() + .models.rootNodes + .ofType() + .first() + } + service = TextEditorServiceImpl(engine, branch.getArea().asModel(), backgroundScope) + editor = JsEditorComponent(service) + editor.editNode(testSuite.untypedReference()) + assertTestItem = branch.computeRead { testSuite.descendants().drop(1).first() } + editor.flushAndUpdateSelection { + val cell = + editor + .resolveNodeCell(assertTestItem)!! + .firstLeaf() + .nextLeafs(true) + .first { it.isVisible() } + println(cell.toString()) + CaretSelection(cell.layoutable()!!, 0) + } + body() + KernelfLanguages.languages.forEach { it.unregister() } + } } diff --git a/kernelf-editor/src/jvmTest/kotlin/kernelf/EditorToText.kt b/kernelf-editor/src/jvmTest/kotlin/kernelf/EditorToText.kt index 45c25923..0ecd5caa 100644 --- a/kernelf-editor/src/jvmTest/kotlin/kernelf/EditorToText.kt +++ b/kernelf-editor/src/jvmTest/kotlin/kernelf/EditorToText.kt @@ -5,7 +5,6 @@ import java.io.File import kotlin.test.Test class EditorToText { - @Test fun toText() { val jsonFile = File("models/test.in.expr.os.strings@tests.json") diff --git a/kernelf-ssr-demo/src/main/kotlin/org/modelix/editor/ssr/demo/kernelf/Application.kt b/kernelf-ssr-demo/src/main/kotlin/org/modelix/editor/ssr/demo/kernelf/Application.kt index 61b199cf..f6f67f7a 100644 --- a/kernelf-ssr-demo/src/main/kotlin/org/modelix/editor/ssr/demo/kernelf/Application.kt +++ b/kernelf-ssr-demo/src/main/kotlin/org/modelix/editor/ssr/demo/kernelf/Application.kt @@ -24,26 +24,36 @@ import org.modelix.model.persistent.MapBasedStore import org.modelix.model.withIncrementalComputationSupport import kotlin.time.Duration.Companion.seconds -fun main(args: Array): Unit = io.ktor.server.netty.EngineMain.main(args) +fun main(args: Array): Unit = + io.ktor.server.netty.EngineMain + .main(args) fun Application.module() { val store = ObjectStoreCache(MapBasedStore()) - val tree = CLTree.builder(store).repositoryId("ssr-demo").useRoleIds(false).build() + val tree = + CLTree + .builder(store) + .repositoryId("ssr-demo") + .useRoleIds(false) + .build() val branch = PBranch(tree, IdGenerator.newInstance(0x8888)).withIncrementalComputationSupport() - val modelData = ModelData.fromJson( - javaClass.getResourceAsStream("/test.in.expr.os.strings@tests.json")!!.use { it.reader().readText() }, - ) + val modelData = + ModelData.fromJson( + javaClass.getResourceAsStream("/test.in.expr.os.strings@tests.json")!!.use { it.reader().readText() }, + ) modelData.load(branch) - val rootNodeRefs = branch.computeRead { - branch.getRootNode() - .getChildren(IChildLink.fromName("modules")) - .flatMap { it.getChildren(IChildLink.fromName("models")) } - .flatMap { it.getChildren(IChildLink.fromName("rootNodes")) } - .map { it.getPropertyValue(IProperty.fromName("name")) + ": " + it.reference.serialize() } - } + val rootNodeRefs = + branch.computeRead { + branch + .getRootNode() + .getChildren(IChildLink.fromName("modules")) + .flatMap { it.getChildren(IChildLink.fromName("models")) } + .flatMap { it.getChildren(IChildLink.fromName("rootNodes")) } + .map { it.getPropertyValue(IProperty.fromName("name")) + ": " + it.reference.serialize() } + } println("Root node references: \n" + rootNodeRefs.joinToString("\n")) - val ssrServer = ModelixSSRServer(branch.getArea()) + val ssrServer = ModelixSSRServer(branch.getArea().asModel()) KernelfEditor.register(ssrServer.editorEngine) KernelfLanguages.registerAll() diff --git a/mps-image-editor-server/build.gradle.kts b/mps-image-editor-server/build.gradle.kts index 1c0b6298..c4cb8705 100644 --- a/mps-image-editor-server/build.gradle.kts +++ b/mps-image-editor-server/build.gradle.kts @@ -56,11 +56,12 @@ tasks { val pluginDir = mpsPluginsDir if (pluginDir != null) { - val installMpsPlugin = register("installMpsPlugin") { - dependsOn(prepareSandbox) - from(project.layout.buildDirectory.dir("idea-sandbox/plugins/${project.name}")) - into(pluginDir.resolve(project.name)) - } + val installMpsPlugin = + register("installMpsPlugin") { + dependsOn(prepareSandbox) + from(project.layout.buildDirectory.dir("idea-sandbox/plugins/${project.name}")) + into(pluginDir.resolve(project.name)) + } register("installMpsDevPlugins") { dependsOn(installMpsPlugin) } @@ -74,7 +75,14 @@ tasks { .from(patchPluginXml.flatMap { it.outputFiles }) doLast { - val jarsInBasePlugin = defaultDestinationDir.get().resolve(project(":editor-common-mps").name).resolve("lib").list()?.toHashSet() ?: emptySet() + val jarsInBasePlugin = + defaultDestinationDir + .get() + .resolve(project(":editor-common-mps").name) + .resolve("lib") + .list() + ?.toHashSet() + ?: emptySet() defaultDestinationDir.get().resolve(project.name).resolve("lib").listFiles()?.forEach { if (jarsInBasePlugin.contains(it.name)) it.delete() } diff --git a/mps-image-editor-server/src/main/kotlin/org/modelix/mps/editor/image/ImageEditorForMPS.kt b/mps-image-editor-server/src/main/kotlin/org/modelix/mps/editor/image/ImageEditorForMPS.kt index a7737189..9c9de31e 100644 --- a/mps-image-editor-server/src/main/kotlin/org/modelix/mps/editor/image/ImageEditorForMPS.kt +++ b/mps-image-editor-server/src/main/kotlin/org/modelix/mps/editor/image/ImageEditorForMPS.kt @@ -45,11 +45,14 @@ import java.nio.charset.StandardCharsets import java.util.Collections import kotlin.time.Duration.Companion.seconds -private val LOG = io.github.oshai.kotlinlogging.KotlinLogging.logger { } +private val LOG = + io.github.oshai.kotlinlogging.KotlinLogging + .logger { } @Service(Service.Level.PROJECT) -class ImageEditorForMPSProject(private val project: Project) : Disposable { - +class ImageEditorForMPSProject( + private val project: Project, +) : Disposable { init { ApplicationManager.getApplication().service().registerProject(project) } @@ -61,18 +64,18 @@ class ImageEditorForMPSProject(private val project: Project) : Disposable { @Service(Service.Level.APP) class ImageEditorForMPS : Disposable { - companion object { fun getInstance() = ApplicationManager.getApplication().getService(ImageEditorForMPS::class.java) } private var ktorServer: EmbeddedServer<*, *>? = null private val projects: MutableSet = Collections.synchronizedSet(HashSet()) - private val commandLister = object : org.jetbrains.mps.openapi.repository.CommandListener { - override fun commandFinished() { - // ssrServer?.updateAll() + private val commandLister = + object : org.jetbrains.mps.openapi.repository.CommandListener { + override fun commandFinished() { + // ssrServer?.updateAll() + } } - } fun registerProject(project: Project) { projects.add(project) @@ -83,21 +86,19 @@ class ImageEditorForMPS : Disposable { projects.remove(project) } - private fun getMPSProjects(): List { - return runSynchronized(projects) { + private fun getMPSProjects(): List = + runSynchronized(projects) { projects.mapNotNull { it.getComponent(MPSProject::class.java) } } - } - private fun getRepository(): SRepository { - return getMPSProjects().asSequence().map { - it.repository - }.firstOrNull() ?: MPSModuleRepository.getInstance() - } + private fun getRepository(): SRepository = + getMPSProjects() + .asSequence() + .map { + it.repository + }.firstOrNull() ?: MPSModuleRepository.getInstance() - private fun getRootNode(): INode { - return MPSRepositoryAsNode(getRepository()).asLegacyNode() - } + private fun getRootNode(): INode = MPSRepositoryAsNode(getRepository()).asLegacyNode() fun ensureStarted() { runSynchronized(this) { @@ -106,9 +107,10 @@ class ImageEditorForMPS : Disposable { println("starting react SSR server") MPSModuleRepository.getInstance().modelAccess.addCommandListener(commandLister) - ktorServer = embeddedServer(Netty, port = 43596) { - initKtorServer() - } + ktorServer = + embeddedServer(Netty, port = 43596) { + initKtorServer() + } ktorServer!!.start() } } @@ -147,14 +149,19 @@ class ImageEditorForMPS : Disposable { ensureStopped() } - private suspend fun handleWebsocketSession(session: DefaultWebSocketServerSession, nodeRef: NodeReference) { + private suspend fun handleWebsocketSession( + session: DefaultWebSocketServerSession, + nodeRef: NodeReference, + ) { val repository = getMPSProjects().firstOrNull()?.repository - val rootNode = repository?.modelAccess?.computeRead { - (MPSArea(repository).let { nodeRef.resolveIn(it) }?.asWritableNode() as? MPSWritableNode)?.node - } + val rootNode = + repository?.modelAccess?.computeRead { + (MPSArea(repository).let { nodeRef.resolveIn(it) }?.asWritableNode() as? MPSWritableNode)?.node + } require(rootNode !is ModelixNodeAsMPSNode) { "MPS node without Modelix wrapper expected" } var inspectorEditorSession: RenderSession? = null - val mainEditorSession = RenderSession(getMPSProjects().first(), session, "unknown user", isInspector = false, { inspectorEditorSession }, rootNode) + val mainEditorSession = + RenderSession(getMPSProjects().first(), session, "unknown user", isInspector = false, { inspectorEditorSession }, rootNode) try { mainEditorSession.onOpen() @@ -166,7 +173,14 @@ class ImageEditorForMPS : Disposable { try { if (inspectorEditorSession == null) { inspectorEditorSession = - RenderSession(getMPSProjects().first(), session, "unknown user", isInspector = true, { null }, rootNode) + RenderSession( + getMPSProjects().first(), + session, + "unknown user", + isInspector = true, + { null }, + rootNode + ) inspectorEditorSession.onOpen() } inspectorEditorSession.processMessage(obj) @@ -181,6 +195,7 @@ class ImageEditorForMPS : Disposable { } } } + else -> {} } } diff --git a/mps-image-editor-server/src/main/kotlin/svg/plugin/EditorChangeDetector.kt b/mps-image-editor-server/src/main/kotlin/svg/plugin/EditorChangeDetector.kt index bf278e58..4fd25b41 100644 --- a/mps-image-editor-server/src/main/kotlin/svg/plugin/EditorChangeDetector.kt +++ b/mps-image-editor-server/src/main/kotlin/svg/plugin/EditorChangeDetector.kt @@ -18,7 +18,9 @@ import javax.swing.SwingUtilities import kotlin.math.max import kotlin.math.min -abstract class EditorChangeDetector(val coroutineScope: CoroutineScope) { +abstract class EditorChangeDetector( + val coroutineScope: CoroutineScope, +) { var lastImage: BufferedImage? = null private set var visibleYRange: Range? = null @@ -29,13 +31,24 @@ abstract class EditorChangeDetector(val coroutineScope: CoroutineScope) { protected abstract val editorComponent: EditorComponent? protected abstract suspend fun handleFullChange(newImage: BufferedImage?) - protected abstract suspend fun handlePartialChange(newImage: BufferedImage, offsetX: Int, offsetY: Int) - fun setVisibleYRange(minY: Int, maxY: Int) { + protected abstract suspend fun handlePartialChange( + newImage: BufferedImage, + offsetX: Int, + offsetY: Int, + ) + + fun setVisibleYRange( + minY: Int, + maxY: Int, + ) { visibleYRange = Range(minY, maxY) } - protected suspend fun handleChange(newImage: BufferedImage?, changedRect: Rectangle?) { + protected suspend fun handleChange( + newImage: BufferedImage?, + changedRect: Rectangle?, + ) { if (changedRect == null) { handleFullChange(newImage) } else { @@ -118,17 +131,19 @@ abstract class EditorChangeDetector(val coroutineScope: CoroutineScope) { height - 1 ) } - ) until ( + ) until ( ( if (rangeY == null) { height } else { limitValue( - rangeY.end + 1, 0, height + rangeY.end + 1, + 0, + height ) } - ) - )) { + ) + )) { oldImage.raster.getPixels(0, y, oldImage.width, 1, oldPixelData) newImage!!.raster.getPixels(0, y, newImage!!.width, 1, newPixelData) val lineChanged = !(oldPixelData.contentEquals(newPixelData)) @@ -138,7 +153,10 @@ abstract class EditorChangeDetector(val coroutineScope: CoroutineScope) { maxChangedY = max(maxChangedY.toDouble(), y.toDouble()).toInt() var x = 0 while (x < oldPixelData.size) { - if (oldPixelData[x] != newPixelData[x] || oldPixelData[1 + x] != newPixelData[1 + x] || oldPixelData[2 + x] != newPixelData[2 + x] || oldPixelData[3 + x] != newPixelData[3 + x]) { + if (oldPixelData[x] != newPixelData[x] || oldPixelData[1 + x] != newPixelData[1 + x] || + oldPixelData[2 + x] != newPixelData[2 + x] || + oldPixelData[3 + x] != newPixelData[3 + x] + ) { minChangedX = min(minChangedX.toDouble(), (x / 4).toDouble()).toInt() maxChangedX = max(maxChangedX.toDouble(), (x / 4).toDouble()).toInt() } @@ -163,7 +181,7 @@ abstract class EditorChangeDetector(val coroutineScope: CoroutineScope) { maxChangedY - minChangedY + 1 ) } - ) + ) ) } } @@ -227,7 +245,10 @@ abstract class EditorChangeDetector(val coroutineScope: CoroutineScope) { return img } - class Range(start: Int, end: Int) { + class Range( + start: Int, + end: Int, + ) { val start: Int val end: Int @@ -242,8 +263,10 @@ abstract class EditorChangeDetector(val coroutineScope: CoroutineScope) { } companion object { - private fun limitValue(value: Int, min: Int, max: Int): Int { - return max(min(value.toDouble(), max.toDouble()), min.toDouble()).toInt() - } + private fun limitValue( + value: Int, + min: Int, + max: Int, + ): Int = max(min(value.toDouble(), max.toDouble()), min.toDouble()).toInt() } } diff --git a/mps-image-editor-server/src/main/kotlin/svg/plugin/ReflectionUtil.kt b/mps-image-editor-server/src/main/kotlin/svg/plugin/ReflectionUtil.kt index a0bb25b1..95f99370 100644 --- a/mps-image-editor-server/src/main/kotlin/svg/plugin/ReflectionUtil.kt +++ b/mps-image-editor-server/src/main/kotlin/svg/plugin/ReflectionUtil.kt @@ -4,7 +4,11 @@ import java.lang.reflect.Field import java.lang.reflect.Modifier object ReflectionUtil { - fun readField(cls: Class<*>, obj: Any, fieldName: String): Any { + fun readField( + cls: Class<*>, + obj: Any, + fieldName: String, + ): Any { try { val field = cls.getDeclaredField(fieldName) field.isAccessible = true @@ -14,7 +18,12 @@ object ReflectionUtil { } } - fun writeField(cls: Class<*>, obj: Any, fieldName: String, value: Any?) { + fun writeField( + cls: Class<*>, + obj: Any, + fieldName: String, + value: Any?, + ) { try { val field = cls.getDeclaredField(fieldName) field.isAccessible = true @@ -61,9 +70,7 @@ object ReflectionUtil { methodName: String, argumentTypes: Array?>, arguments: Array, - ): Any { - return callMethod(cls, null, methodName, argumentTypes, arguments) - } + ): Any = callMethod(cls, null, methodName, argumentTypes, arguments) fun callStaticVoidMethod( cls: Class<*>, diff --git a/mps-image-editor-server/src/main/kotlin/svg/plugin/RemoteMouseCursor.kt b/mps-image-editor-server/src/main/kotlin/svg/plugin/RemoteMouseCursor.kt index c62341e4..cf1b3e46 100644 --- a/mps-image-editor-server/src/main/kotlin/svg/plugin/RemoteMouseCursor.kt +++ b/mps-image-editor-server/src/main/kotlin/svg/plugin/RemoteMouseCursor.kt @@ -7,11 +7,16 @@ import java.awt.Point import java.awt.event.MouseEvent import javax.swing.SwingUtilities -class RemoteMouseCursor(val targetComponent: Component) { +class RemoteMouseCursor( + val targetComponent: Component, +) { private var lastTarget: Component? = null private var lastPosition: Point? = null - fun mouseMoved(position: Point, modifiers: Int) { + fun mouseMoved( + position: Point, + modifiers: Int, + ) { ThreadUtils.assertEDT() val x = position.x @@ -47,21 +52,25 @@ class RemoteMouseCursor(val targetComponent: Component) { target: Component, button: Int, ) { - val event = MouseEvent( - targetComponent, - type, - System.currentTimeMillis(), - modifiers, - position.x, - position.y, - clickCount, - false, - button - ) + val event = + MouseEvent( + targetComponent, + type, + System.currentTimeMillis(), + modifiers, + position.x, + position.y, + clickCount, + false, + button + ) target.dispatchEvent(SwingUtilities.convertMouseEvent(targetComponent, event, target)) } - fun mouseClicked(position: Point, modifiers: Int) { + fun mouseClicked( + position: Point, + modifiers: Int, + ) { ThreadUtils.assertEDT() if (lastPosition != position) { @@ -123,7 +132,10 @@ class RemoteMouseCursor(val targetComponent: Component) { lastTarget = null } - fun getRedirectedTarget(x: Int, y: Int): Component { + fun getRedirectedTarget( + x: Int, + y: Int, + ): Component { ThreadUtils.assertEDT() var target = SwingUtilities.getDeepestComponentAt(targetComponent, x, y) diff --git a/mps-image-editor-server/src/main/kotlin/svg/plugin/RenderSession.kt b/mps-image-editor-server/src/main/kotlin/svg/plugin/RenderSession.kt index 442caf98..7f46360f 100644 --- a/mps-image-editor-server/src/main/kotlin/svg/plugin/RenderSession.kt +++ b/mps-image-editor-server/src/main/kotlin/svg/plugin/RenderSession.kt @@ -1,6 +1,6 @@ package svg.plugin -/*Generated by MPS */ +// Generated by MPS import com.intellij.ide.ProhibitAWTEvents import com.intellij.openapi.actionSystem.ActionPlaces @@ -68,262 +68,289 @@ import javax.swing.KeyStroke import kotlin.math.ceil import kotlin.math.roundToInt -private val LOG = io.github.oshai.kotlinlogging.KotlinLogging.logger { } - -class RenderSession @JvmOverloads constructor( - private val project: Project, - private val websocketSession: DefaultWebSocketServerSession, - private val user: String, - isInspector: Boolean = false, - val inspectorSession: () -> RenderSession?, - var rootNode: SNode? = null, -) { - private val MOUSE_EVENT_TYPE: Map = mapOf( - "mousemove" to MouseEvent.MOUSE_MOVED, - "mouseenter" to MouseEvent.MOUSE_ENTERED, - "mouseleave" to MouseEvent.MOUSE_EXITED - ) - - private var serverEditorComponent: ServerEditorComponent? = null - private var editorId: String? = null - - private var changeDetectionTimer: Job? = null - private var deltaUpdateCount = 0 - private var imageChangeDetector: EditorChangeDetector? = null - private var changeDetectionInterval = 1 - private var lastImageTime: Long = 0 - - private var lastSelectedIndex = -1 - private var lastIntentions: List>? = null - - var isInspector: Boolean = false - - private var remoteMouseCursor: RemoteMouseCursor? = null - - init { - this.isInspector = isInspector - init(project) - } +private val LOG = + io.github.oshai.kotlinlogging.KotlinLogging + .logger { } + +class RenderSession + @JvmOverloads + constructor( + private val project: Project, + private val websocketSession: DefaultWebSocketServerSession, + private val user: String, + isInspector: Boolean = false, + val inspectorSession: () -> RenderSession?, + var rootNode: SNode? = null, + ) { + @Suppress("ktlint:standard:property-naming") + private val MOUSE_EVENT_TYPE: Map = + mapOf( + "mousemove" to MouseEvent.MOUSE_MOVED, + "mouseenter" to MouseEvent.MOUSE_ENTERED, + "mouseleave" to MouseEvent.MOUSE_EXITED + ) - protected fun init(project: Project?) { - imageChangeDetector = object : EditorChangeDetector(websocketSession) { - override val editorComponent: EditorComponent - get() = this@RenderSession.editorComponent + private var serverEditorComponent: ServerEditorComponent? = null + private var editorId: String? = null - override suspend fun handleFullChange(newImage: BufferedImage?) { - changeDetectionInterval = 1 - if (!websocketSession.isActive) { - return - } - sendFullImage(newImage) - } + private var changeDetectionTimer: Job? = null + private var deltaUpdateCount = 0 + private var imageChangeDetector: EditorChangeDetector? = null + private var changeDetectionInterval = 1 + private var lastImageTime: Long = 0 - override suspend fun handlePartialChange(newImage: BufferedImage, offsetX: Int, offsetY: Int) { - changeDetectionInterval = 1 - if (!websocketSession.isActive) { - return - } - if (deltaUpdateCount < 20) { - sendPartialImage(newImage, offsetX, offsetY) - } else { - sendFullImage(lastImage) - } - } + private var lastSelectedIndex = -1 + private var lastIntentions: List>? = null + + var isInspector: Boolean = false + + private var remoteMouseCursor: RemoteMouseCursor? = null + + init { + this.isInspector = isInspector + init(project) } - changeDetectionTimer = websocketSession.launch { - var counter = 1 - while (isActive) { - if (counter >= changeDetectionInterval) { - counter = 1 - if (changeDetectionInterval < 500) { - changeDetectionInterval = Math.round(ceil(1.5 * changeDetectionInterval)).toInt() + protected fun init(project: Project?) { + imageChangeDetector = + object : EditorChangeDetector(websocketSession) { + override val editorComponent: EditorComponent + get() = this@RenderSession.editorComponent + + override suspend fun handleFullChange(newImage: BufferedImage?) { + changeDetectionInterval = 1 + if (!websocketSession.isActive) { + return + } + sendFullImage(newImage) } - if (isActive) { - imageChangeDetector!!.scheduleUpdate() + + override suspend fun handlePartialChange( + newImage: BufferedImage, + offsetX: Int, + offsetY: Int, + ) { + changeDetectionInterval = 1 + if (!websocketSession.isActive) { + return + } + if (deltaUpdateCount < 20) { + sendPartialImage(newImage, offsetX, offsetY) + } else { + sendFullImage(lastImage) + } } - } else { - counter++ } - if (deltaUpdateCount != 0 && System.currentTimeMillis() - lastImageTime > 3000) { - sendFullImage(imageChangeDetector!!.lastImage) + changeDetectionTimer = + websocketSession.launch { + var counter = 1 + while (isActive) { + if (counter >= changeDetectionInterval) { + counter = 1 + if (changeDetectionInterval < 500) { + changeDetectionInterval = Math.round(ceil(1.5 * changeDetectionInterval)).toInt() + } + if (isActive) { + imageChangeDetector!!.scheduleUpdate() + } + } else { + counter++ + } + + if (deltaUpdateCount != 0 && System.currentTimeMillis() - lastImageTime > 3000) { + sendFullImage(imageChangeDetector!!.lastImage) + } + delay(10) + } } - delay(10) - } } - } - suspend fun processCCMenu() { - if (serverEditorComponent == null) { - return - } - val chooser: NodeSubstituteChooser = serverEditorComponent!!.nodeSubstituteChooser - val chooserIsVisible: Boolean = chooser.isVisible - val selectedIndex = if (chooserIsVisible) { - ( - ReflectionUtil.callMethod( - NodeSubstituteChooser::class.java, - chooser, - "getSelectionIndex", - arrayOf?>(), - arrayOf() - ) as Int - ) - } else { - -1 - } + suspend fun processCCMenu() { + if (serverEditorComponent == null) { + return + } + val chooser: NodeSubstituteChooser = serverEditorComponent!!.nodeSubstituteChooser + val chooserIsVisible: Boolean = chooser.isVisible + val selectedIndex = + if (chooserIsVisible) { + ( + ReflectionUtil.callMethod( + NodeSubstituteChooser::class.java, + chooser, + "getSelectionIndex", + arrayOf?>(), + arrayOf() + ) as Int + ) + } else { + -1 + } - if (selectedIndex == lastSelectedIndex) return - val message = mutableMapOf() - if (selectedIndex != -1) { - project.repository.modelAccess.runReadAction { - message["type"] = JsonPrimitive("ccmenu") - val contextCell = ReflectionUtil.readField( - NodeSubstituteChooser::class.java, - chooser, - "myContextCell" - ) as EditorCell - message["x"] = JsonPrimitive(contextCell.x + contextCell.leftInset) - message["y"] = JsonPrimitive(contextCell.y + contextCell.height) - message["selectionIndex"] = JsonPrimitive( - ReflectionUtil.callMethod( - NodeSubstituteChooser::class.java, - chooser, - "getSelectionIndex", - arrayOf?>(), - arrayOf() - ) as Int - ) - val pattern: String = chooser.patternEditor.pattern - message["pattern"] = JsonPrimitive(pattern) - if (lastSelectedIndex == -1) { - val actions: List = ReflectionUtil.callMethod( - NodeSubstituteChooser::class.java, - chooser, - "getSubstituteActions", - arrayOf?>(), - arrayOf() - ) as List - message["actions"] = JsonArray( - actions.map { - JsonObject( - mapOf( - "pattern" to JsonPrimitive(it.getMatchingText(pattern)), - "description" to JsonPrimitive(it.getDescriptionText(pattern)) - ) + if (selectedIndex == lastSelectedIndex) return + val message = mutableMapOf() + if (selectedIndex != -1) { + project.repository.modelAccess.runReadAction { + message["type"] = JsonPrimitive("ccmenu") + val contextCell = + ReflectionUtil.readField( + NodeSubstituteChooser::class.java, + chooser, + "myContextCell" + ) as EditorCell + message["x"] = JsonPrimitive(contextCell.x + contextCell.leftInset) + message["y"] = JsonPrimitive(contextCell.y + contextCell.height) + message["selectionIndex"] = + JsonPrimitive( + ReflectionUtil.callMethod( + NodeSubstituteChooser::class.java, + chooser, + "getSelectionIndex", + arrayOf?>(), + arrayOf() + ) as Int + ) + val pattern: String = chooser.patternEditor.pattern + message["pattern"] = JsonPrimitive(pattern) + if (lastSelectedIndex == -1) { + val actions: List = + ReflectionUtil.callMethod( + NodeSubstituteChooser::class.java, + chooser, + "getSubstituteActions", + arrayOf?>(), + arrayOf() + ) as List + message["actions"] = + JsonArray( + actions.map { + JsonObject( + mapOf( + "pattern" to JsonPrimitive(it.getMatchingText(pattern)), + "description" to JsonPrimitive(it.getDescriptionText(pattern)) + ) + ) + } ) - } - ) + } } + } else { + message["type"] = JsonPrimitive("ccmenu.hide") } - } else { - message["type"] = JsonPrimitive("ccmenu.hide") - } - lastSelectedIndex = selectedIndex - - sendMessage(message) - } + lastSelectedIndex = selectedIndex - suspend fun sendMessage(message: MutableMap) { - message["inspector"] = JsonPrimitive(this.isInspector) - websocketSession.send(JsonObject(message).toString()) - } + sendMessage(message) + } - protected val editorComponent: ServerEditorComponent - get() { - if (serverEditorComponent == null) { - val repo = project.repository + suspend fun sendMessage(message: MutableMap) { + message["inspector"] = JsonPrimitive(this.isInspector) + websocketSession.send(JsonObject(message).toString()) + } - repo.modelAccess.runReadAction { - serverEditorComponent = if (this@RenderSession.isInspector) { - ServerInspectorEditorComponent(rootNode, project) - } else { - ServerEditorComponent(rootNode, project) - } - remoteMouseCursor = RemoteMouseCursor(serverEditorComponent!!.externalComponent) + protected val editorComponent: ServerEditorComponent + get() { + if (serverEditorComponent == null) { + val repo = project.repository + + repo.modelAccess.runReadAction { + serverEditorComponent = + if (this@RenderSession.isInspector) { + ServerInspectorEditorComponent(rootNode, project) + } else { + ServerEditorComponent(rootNode, project) + } + remoteMouseCursor = RemoteMouseCursor(serverEditorComponent!!.externalComponent) - EditorExtensionUtil.extendUsingProject(serverEditorComponent!!, project) - serverEditorComponent!!.selectionManager.addSelectionListener { p0, oldSelection, newSelection -> - imageChangeDetector!!.scheduleUpdate() - inspect(newSelection) - } - serverEditorComponent!!.updater.addListener(object : UpdaterListenerAdapter() { - override fun editorUpdated(p0: jetbrains.mps.openapi.editor.EditorComponent) { + EditorExtensionUtil.extendUsingProject(serverEditorComponent!!, project) + serverEditorComponent!!.selectionManager.addSelectionListener { p0, oldSelection, newSelection -> imageChangeDetector!!.scheduleUpdate() + inspect(newSelection) } - }) + serverEditorComponent!!.updater.addListener( + object : UpdaterListenerAdapter() { + override fun editorUpdated(p0: jetbrains.mps.openapi.editor.EditorComponent) { + imageChangeDetector!!.scheduleUpdate() + } + } + ) + } } + + return serverEditorComponent!! } - return serverEditorComponent!! + suspend fun sendFullImage(img: BufferedImage?) { + if (!(websocketSession.isActive)) { + return + } + val png: String = EditorToImage.toPngBase64(img) + val message = mutableMapOf() + message["type"] = JsonPrimitive("image.full") + val data = JsonObject(mapOf("rawData" to JsonPrimitive(png))) + message["data"] = data + sendMessage(message) + deltaUpdateCount = 0 + lastImageTime = System.currentTimeMillis() } - suspend fun sendFullImage(img: BufferedImage?) { - if (!(websocketSession.isActive)) { - return + suspend fun sendPartialImage( + img: BufferedImage, + offsetX: Int, + offsetY: Int, + ) { + val png: String = EditorToImage.toPngBase64(img) + val message = mutableMapOf() + message["type"] = JsonPrimitive("image.fragment") + val data = mutableMapOf() + data["x"] = JsonPrimitive(offsetX) + data["y"] = JsonPrimitive(offsetY) + data["width"] = JsonPrimitive(img.width) + data["height"] = JsonPrimitive(img.height) + data["rawData"] = JsonPrimitive(png) + message["data"] = JsonObject(data) + sendMessage(message) + deltaUpdateCount++ + lastImageTime = System.currentTimeMillis() } - val png: String = EditorToImage.toPngBase64(img) - val message = mutableMapOf() - message["type"] = JsonPrimitive("image.full") - val data = JsonObject(mapOf("rawData" to JsonPrimitive(png))) - message["data"] = data - sendMessage(message) - deltaUpdateCount = 0 - lastImageTime = System.currentTimeMillis() - } - - suspend fun sendPartialImage(img: BufferedImage, offsetX: Int, offsetY: Int) { - val png: String = EditorToImage.toPngBase64(img) - val message = mutableMapOf() - message["type"] = JsonPrimitive("image.fragment") - val data = mutableMapOf() - data["x"] = JsonPrimitive(offsetX) - data["y"] = JsonPrimitive(offsetY) - data["width"] = JsonPrimitive(img.width) - data["height"] = JsonPrimitive(img.height) - data["rawData"] = JsonPrimitive(png) - message["data"] = JsonObject(data) - sendMessage(message) - deltaUpdateCount++ - lastImageTime = System.currentTimeMillis() - } - suspend fun dispose() { - if (changeDetectionTimer != null) { - changeDetectionTimer!!.cancel("disposed") - } - if (this.serverEditorComponent != null) { - withContext(Dispatchers.EDT) { serverEditorComponent!!.dispose() } + suspend fun dispose() { + if (changeDetectionTimer != null) { + changeDetectionTimer!!.cancel("disposed") + } + if (this.serverEditorComponent != null) { + withContext(Dispatchers.EDT) { serverEditorComponent!!.dispose() } + } } - } - fun onOpen() { - imageChangeDetector!!.scheduleUpdate() - } - - fun onClose(code: Int, reason: String?) { - } + fun onOpen() { + imageChangeDetector!!.scheduleUpdate() + } - suspend fun processMessage(message: JsonObject) { - val data = message["data"] as JsonObject? - var type = message.optString("type") - if (type != null) { - type = type.lowercase(Locale.getDefault()) + fun onClose( + code: Int, + reason: String?, + ) { } - val key = data?.optString("key") - val keyChar = (if (key != null && key.length == 1) key[0] else '\u0000') - if (type == "click") { - // The first thing we do is simulating the click to update the selection in the editor. - withContext(Dispatchers.EDT) { - remoteMouseCursor!!.mouseClicked( - Point( - data!!.getInt("x"), - data.getInt("y") - ), - 0 - ) + + suspend fun processMessage(message: JsonObject) { + val data = message["data"] as JsonObject? + var type = message.optString("type") + if (type != null) { + type = type.lowercase(Locale.getDefault()) } + val key = data?.optString("key") + val keyChar = (if (key != null && key.length == 1) key[0] else '\u0000') + if (type == "click") { + // The first thing we do is simulating the click to update the selection in the editor. + withContext(Dispatchers.EDT) { + remoteMouseCursor!!.mouseClicked( + Point( + data!!.getInt("x"), + data.getInt("y") + ), + 0 + ) + } /* For following references MPS uses MPSEditorOpener that searches for an open @@ -331,269 +358,290 @@ class RenderSession @JvmOverloads constructor( This seems not to work with ServerEditorComponents so we have to implement our own logic for following references. */ - if (data?.optBoolean("ctrl") == true || data?.optBoolean("meta") == true) { - val cell = editorComponent.selectedCell - if (cell != null && cell.isReferenceCell) { - project.repository.modelAccess.runReadAction { - val node = APICellAdapter.getSNodeWRTReference(cell) - if (node != null) { - openNode(node) + if (data?.optBoolean("ctrl") == true || data?.optBoolean("meta") == true) { + val cell = editorComponent.selectedCell + if (cell != null && cell.isReferenceCell) { + project.repository.modelAccess.runReadAction { + val node = APICellAdapter.getSNodeWRTReference(cell) + if (node != null) { + openNode(node) + } } } } - } - } else if (MOUSE_EVENT_TYPE.containsKey(type)) { - val x = data?.getInt("x") - val y = data?.getInt("y") - val modifier = - (if ((data?.optBoolean("ctrl") == true || data?.optBoolean("meta") == true)) ctrlDownModifier() else 0) - val mouseEventType: Int = MOUSE_EVENT_TYPE[type]!! - when (mouseEventType) { - MouseEvent.MOUSE_MOVED -> withContext(Dispatchers.EDT) { - remoteMouseCursor!!.mouseMoved(Point(x!!, y!!), modifier) - } - MouseEvent.MOUSE_EXITED -> withContext(Dispatchers.EDT) { remoteMouseCursor!!.mouseExited() } - } - } else if (type == "keypress") { - simulateKeypress(data.getInt("keyCode"), keyChar) - } else if (type == "keydown") { - var modifier = 0 - if (data?.optBoolean("ctrl") == true || data?.optBoolean("meta") == true) { - modifier = modifier or ctrlDownModifier() - } - if (data?.optBoolean("alt") == true) { - modifier = modifier or KeyEvent.ALT_DOWN_MASK - } - if (data?.optBoolean("shift") == true) { - modifier = modifier or KeyEvent.SHIFT_DOWN_MASK - } - simulateKeyDown(data.getInt("keyCode"), keyChar, modifier) - } else if (type == "keyup") { - simulateKeyUp(data.getInt("keyCode"), keyChar) - } else if (type == "viewrange") { - imageChangeDetector!!.setVisibleYRange(message.getInt("top"), message.getInt("bottom")) - imageChangeDetector!!.scheduleUpdate() - } else if (type == "rootnode") { - websocketSession.launch(Dispatchers.EDT) { - val area: IArea = MPSArea(project.repository) - val nodeRefString = message.optString("nodeRef") - if (nodeRefString != null) { - area.executeRead { - val nodeRef: INodeReference = NodeReference(nodeRefString) - val node: SNode = (nodeRef.resolveIn(area)!!.asWritableNode() as MPSWritableNode).node - rootNode = node - updateEditorId() - this@RenderSession.editorComponent.editNode(node) + } else if (MOUSE_EVENT_TYPE.containsKey(type)) { + val x = data?.getInt("x") + val y = data?.getInt("y") + val modifier = + (if ((data?.optBoolean("ctrl") == true || data?.optBoolean("meta") == true)) ctrlDownModifier() else 0) + val mouseEventType: Int = MOUSE_EVENT_TYPE[type]!! + when (mouseEventType) { + MouseEvent.MOUSE_MOVED -> { + withContext(Dispatchers.EDT) { + remoteMouseCursor!!.mouseMoved(Point(x!!, y!!), modifier) + } } + + MouseEvent.MOUSE_EXITED -> { + withContext(Dispatchers.EDT) { remoteMouseCursor!!.mouseExited() } + } + } + } else if (type == "keypress") { + simulateKeypress(data.getInt("keyCode"), keyChar) + } else if (type == "keydown") { + var modifier = 0 + if (data?.optBoolean("ctrl") == true || data?.optBoolean("meta") == true) { + modifier = modifier or ctrlDownModifier() + } + if (data?.optBoolean("alt") == true) { + modifier = modifier or KeyEvent.ALT_DOWN_MASK + } + if (data?.optBoolean("shift") == true) { + modifier = modifier or KeyEvent.SHIFT_DOWN_MASK } + simulateKeyDown(data.getInt("keyCode"), keyChar, modifier) + } else if (type == "keyup") { + simulateKeyUp(data.getInt("keyCode"), keyChar) + } else if (type == "viewrange") { + imageChangeDetector!!.setVisibleYRange(message.getInt("top"), message.getInt("bottom")) imageChangeDetector!!.scheduleUpdate() - } - } else if (type == "intentions.execute") { - val index = message.getInt("index") - val expectedText = message.getString("text") - if (lastIntentions != null) { - val intention: Pair = lastIntentions!![index] - withContext(Dispatchers.EDT) { - project.repository.modelAccess.executeCommand { - val actualText: String = - intention.o1.getDescription(intention.o2, editorComponent.editorContext) - if (actualText == expectedText) { - intention.o1.execute(intention.o2, editorComponent.editorContext) - } else { - LOG.error("Intention $index is '$actualText' but '$expectedText' was expected") + } else if (type == "rootnode") { + websocketSession.launch(Dispatchers.EDT) { + val area: IArea = MPSArea(project.repository) + val nodeRefString = message.optString("nodeRef") + if (nodeRefString != null) { + area.executeRead { + val nodeRef: INodeReference = NodeReference(nodeRefString) + val node: SNode = (nodeRef.resolveIn(area)!!.asWritableNode() as MPSWritableNode).node + rootNode = node + updateEditorId() + this@RenderSession.editorComponent.editNode(node) + } + } + imageChangeDetector!!.scheduleUpdate() + } + } else if (type == "intentions.execute") { + val index = message.getInt("index") + val expectedText = message.getString("text") + if (lastIntentions != null) { + val intention: Pair = lastIntentions!![index] + withContext(Dispatchers.EDT) { + project.repository.modelAccess.executeCommand { + val actualText: String = + intention.o1.getDescription(intention.o2, editorComponent.editorContext) + if (actualText == expectedText) { + intention.o1.execute(intention.o2, editorComponent.editorContext) + } else { + LOG.error("Intention $index is '$actualText' but '$expectedText' was expected") + } } } } } - } - - imageChangeDetector!!.scheduleUpdate() - } - private fun updateEditorId() { - editorId = if (rootNode == null) { - null - } else { - (if (isInspector) "inspector" else "main") + ":rootNode:" + ModelixNodeAsMPSNode.toModelixNode( - rootNode!! - ).reference.serialize() + imageChangeDetector!!.scheduleUpdate() } - } - /** - * In MacOs Ctrl+click opens the context menu. The behavior we want is the same as in windows Ctrl+click so we need META_DOWN. - * - * @return META_DOWN_MASK for macOS, CTRL_DOWN_MASK otherwise - */ - private fun ctrlDownModifier(): Int { - return (if (SystemInfo.isMac) KeyEvent.META_DOWN_MASK else KeyEvent.CTRL_DOWN_MASK) - } + private fun updateEditorId() { + editorId = + if (rootNode == null) { + null + } else { + (if (isInspector) "inspector" else "main") + ":rootNode:" + + ModelixNodeAsMPSNode + .toModelixNode( + rootNode!! + ).reference + .serialize() + } + } - /** - * If the given node is in the current editor we select it otherwise we open a new tab. - * - * For checking if a node is in the current editor we check if it is a descendent of the root node. - * - * Because in Modelix any node can be the root of the editor, we use the node from the editor root cell - * as root instead of node.containingRoot. - */ - private fun openNode(node: SNode) { - val root = editorComponent.rootCell.sNode - if (SNodeOperations.getNodeAncestors(node, null, false).contains(root)) { - editorComponent.selectNode(node) - } else { - val obj = mutableMapOf() - obj["type"] = JsonPrimitive("opentab") - obj["url"] = JsonPrimitive("nodeAsHtml?nodeRef=" + ModelixNodeAsMPSNode.toModelixNode(SNodeOperations.getContainingRoot(node)).reference.serialize()) - websocketSession.launch { - sendMessage(obj) + /** + * In MacOs Ctrl+click opens the context menu. The behavior we want is the same as in windows Ctrl+click so we need META_DOWN. + * + * @return META_DOWN_MASK for macOS, CTRL_DOWN_MASK otherwise + */ + private fun ctrlDownModifier(): Int = (if (SystemInfo.isMac) KeyEvent.META_DOWN_MASK else KeyEvent.CTRL_DOWN_MASK) + + /** + * If the given node is in the current editor we select it otherwise we open a new tab. + * + * For checking if a node is in the current editor we check if it is a descendent of the root node. + * + * Because in Modelix any node can be the root of the editor, we use the node from the editor root cell + * as root instead of node.containingRoot. + */ + private fun openNode(node: SNode) { + val root = editorComponent.rootCell.sNode + if (SNodeOperations.getNodeAncestors(node, null, false).contains(root)) { + editorComponent.selectNode(node) + } else { + val obj = mutableMapOf() + obj["type"] = JsonPrimitive("opentab") + obj["url"] = + JsonPrimitive( + "nodeAsHtml?nodeRef=" + + ModelixNodeAsMPSNode.toModelixNode(SNodeOperations.getContainingRoot(node)).reference.serialize() + ) + websocketSession.launch { + sendMessage(obj) + } } } - } - val visibleComponent: JComponent - get() = editorComponent.externalComponent - - @Throws(InvocationTargetException::class, InterruptedException::class) - suspend fun simulateKeypress(keyCode: Int, key: Char) { - withContext(Dispatchers.EDT) { - focusOwner.dispatchEvent( - KeyEvent( - focusOwner, - KeyEvent.KEY_TYPED, - System.currentTimeMillis(), - 0, - KeyEvent.VK_UNDEFINED, - key + val visibleComponent: JComponent + get() = editorComponent.externalComponent + + @Throws(InvocationTargetException::class, InterruptedException::class) + suspend fun simulateKeypress( + keyCode: Int, + key: Char, + ) { + withContext(Dispatchers.EDT) { + focusOwner.dispatchEvent( + KeyEvent( + focusOwner, + KeyEvent.KEY_TYPED, + System.currentTimeMillis(), + 0, + KeyEvent.VK_UNDEFINED, + key + ) ) - ) + } } - } - val focusOwner: Component - get() { - val editorComponent = editorComponent - val window: Window = AWTExtensions.getWindow(editorComponent) - var focusOwner = window.mostRecentFocusOwner - for (popup in AWTExtensions.getVisibleOwnedWindows(window)) { - var popupFocusOwner: Component? = popup.mostRecentFocusOwner - if (popupFocusOwner == null) { - popupFocusOwner = popup + val focusOwner: Component + get() { + val editorComponent = editorComponent + val window: Window = AWTExtensions.getWindow(editorComponent) + var focusOwner = window.mostRecentFocusOwner + for (popup in AWTExtensions.getVisibleOwnedWindows(window)) { + var popupFocusOwner: Component? = popup.mostRecentFocusOwner + if (popupFocusOwner == null) { + popupFocusOwner = popup + } + if (popupFocusOwner != null) { + focusOwner = popupFocusOwner + } } - if (popupFocusOwner != null) { - focusOwner = popupFocusOwner + if (focusOwner == null) { + focusOwner = editorComponent } + return focusOwner } - if (focusOwner == null) { - focusOwner = editorComponent - } - return focusOwner - } - suspend fun simulateKeyDown(keyCode: Int, key: Char, modifiers: Int) { - LOG.debug { "down: $keyCode" } - withContext(Dispatchers.EDT) { - val focusOwner: Component = this@RenderSession.focusOwner - val keyEvent = KeyEvent( - focusOwner, - KeyEvent.KEY_PRESSED, - System.currentTimeMillis(), - modifiers, - keyCode, - KeyEvent.CHAR_UNDEFINED - ) + suspend fun simulateKeyDown( + keyCode: Int, + key: Char, + modifiers: Int, + ) { + LOG.debug { "down: $keyCode" } + withContext(Dispatchers.EDT) { + val focusOwner: Component = this@RenderSession.focusOwner + val keyEvent = + KeyEvent( + focusOwner, + KeyEvent.KEY_PRESSED, + System.currentTimeMillis(), + modifiers, + keyCode, + KeyEvent.CHAR_UNDEFINED + ) - if (focusOwner !== this@RenderSession.editorComponent || focusOwner === this@RenderSession.editorComponent) { - focusOwner.dispatchEvent(keyEvent) - if (keyEvent.isConsumed) { - return@withContext + if (focusOwner !== this@RenderSession.editorComponent || focusOwner === this@RenderSession.editorComponent) { + focusOwner.dispatchEvent(keyEvent) + if (keyEvent.isConsumed) { + return@withContext + } } - } - val dataContext: DataContext = this@RenderSession.editorComponent.dataContext - - if (!(keyEvent.isConsumed)) { - if (modifiers == KeyEvent.ALT_DOWN_MASK && keyCode == KeyEvent.VK_ENTER) { - val message = mutableMapOf() - project.repository.modelAccess.runReadAction { - message["type"] = JsonPrimitive("intentions") - val contextCell: EditorCell = this@RenderSession.editorComponent.selectedCell!! - message["x"] = JsonPrimitive(contextCell.x + contextCell.leftInset) - message["y"] = JsonPrimitive(contextCell.y + contextCell.height) - - val query: IntentionsManager.QueryDescriptor = IntentionsManager.QueryDescriptor() - query.setEnabledOnly(true) - - val intentions: Iterable> = - IntentionsManager.getInstance().getAvailableIntentions( - query, - this@RenderSession.editorComponent.selectedNode, - this@RenderSession.editorComponent.editorContext - ) - lastIntentions = intentions.toList() - message["intentions"] = JsonArray( - intentions.map { - JsonObject( - mapOf( - "text" to JsonPrimitive( - it.o1.getDescription( - it.o2, - this@RenderSession.editorComponent.editorContext + val dataContext: DataContext = this@RenderSession.editorComponent.dataContext + + if (!(keyEvent.isConsumed)) { + if (modifiers == KeyEvent.ALT_DOWN_MASK && keyCode == KeyEvent.VK_ENTER) { + val message = mutableMapOf() + project.repository.modelAccess.runReadAction { + message["type"] = JsonPrimitive("intentions") + val contextCell: EditorCell = this@RenderSession.editorComponent.selectedCell!! + message["x"] = JsonPrimitive(contextCell.x + contextCell.leftInset) + message["y"] = JsonPrimitive(contextCell.y + contextCell.height) + + val query: IntentionsManager.QueryDescriptor = IntentionsManager.QueryDescriptor() + query.setEnabledOnly(true) + + val intentions: Iterable> = + IntentionsManager.getInstance().getAvailableIntentions( + query, + this@RenderSession.editorComponent.selectedNode, + this@RenderSession.editorComponent.editorContext + ) + lastIntentions = intentions.toList() + message["intentions"] = + JsonArray( + intentions.map { + JsonObject( + mapOf( + "text" to + JsonPrimitive( + it.o1.getDescription( + it.o2, + this@RenderSession.editorComponent.editorContext + ) + ) ) ) - ) + } ) - } - ) - } - websocketSession.launch { - sendMessage(message) - } + } + websocketSession.launch { + sendMessage(message) + } - keyEvent.consume() + keyEvent.consume() + } } - } - if (!(keyEvent.isConsumed)) { - // TODO find component local keystroke (see IdeKeyEventDispatcher) - val keymap = KeymapManager.getInstance().activeKeymap - val actionIds: Array = - keymap.getActionIds(KeyStroke.getKeyStroke(keyCode, modifiers, false)) - if (actionIds.isNotEmpty()) { - val actionManager: ActionManagerEx = ActionManagerEx.getInstanceEx() - for (actionId in actionIds) { - val action = actionManager.getAction(actionId) ?: continue - val actionEvent = - AnActionEvent.createFromAnAction(action, keyEvent, ActionPlaces.MAIN_MENU, dataContext) - ProhibitAWTEvents.start("update").use { token -> - (TransactionGuard.getInstance() as TransactionGuardImpl).performUserActivity { - @Suppress("removal") - ActionUtil.performDumbAwareUpdate( - action, - actionEvent, - true - ) + if (!(keyEvent.isConsumed)) { + // TODO find component local keystroke (see IdeKeyEventDispatcher) + val keymap = KeymapManager.getInstance().activeKeymap + val actionIds: Array = + keymap.getActionIds(KeyStroke.getKeyStroke(keyCode, modifiers, false)) + if (actionIds.isNotEmpty()) { + val actionManager: ActionManagerEx = ActionManagerEx.getInstanceEx() + for (actionId in actionIds) { + val action = actionManager.getAction(actionId) ?: continue + val actionEvent = + AnActionEvent.createFromAnAction(action, keyEvent, ActionPlaces.MAIN_MENU, dataContext) + ProhibitAWTEvents.start("update").use { token -> + (TransactionGuard.getInstance() as TransactionGuardImpl).performUserActivity { + @Suppress("removal") + ActionUtil.performDumbAwareUpdate( + action, + actionEvent, + true + ) + } + } + if (!(actionEvent.presentation.isEnabled)) { + LOG.debug { "not applicable: $actionId" } + continue } - } - if (!(actionEvent.presentation.isEnabled)) { - LOG.debug { "not applicable: $actionId" } - continue - } - actionManager.fireBeforeActionPerformed(action, actionEvent) - (TransactionGuard.getInstance() as TransactionGuardImpl).performUserActivity { + actionManager.fireBeforeActionPerformed(action, actionEvent) + (TransactionGuard.getInstance() as TransactionGuardImpl).performUserActivity { // AuthorOverride.AUTHOR.runWith( // user, // Runnable { action.actionPerformed(actionEvent) }) - action.actionPerformed(actionEvent) + action.actionPerformed(actionEvent) + } + actionManager.fireAfterActionPerformed(action, actionEvent, AnActionResult.PERFORMED) + keyEvent.consume() + LOG.debug { "processed by " + actionEvent.presentation.text } + break } - actionManager.fireAfterActionPerformed(action, actionEvent, AnActionResult.PERFORMED) - keyEvent.consume() - LOG.debug { "processed by " + actionEvent.presentation.text } - break } } - } // if (!(keyEvent.isConsumed)) { // this@RenderSession.editorComponent.processKeyPressed(keyEvent) @@ -610,44 +658,49 @@ class RenderSession @JvmOverloads constructor( // } // } - imageChangeDetector!!.scheduleUpdate() + imageChangeDetector!!.scheduleUpdate() + } } - } - suspend fun simulateKeyUp(keyCode: Int, key: Char) { - withContext(Dispatchers.EDT) { - val focusOwner1: Component = focusOwner - val keyEvent = KeyEvent( - focusOwner1, - KeyEvent.KEY_RELEASED, - System.currentTimeMillis(), - 0, - keyCode, - KeyEvent.CHAR_UNDEFINED - ) - focusOwner1.dispatchEvent(keyEvent) - if (!(keyEvent.isConsumed)) { - LOG.debug { "unprocessed keyup: $keyCode" } + suspend fun simulateKeyUp( + keyCode: Int, + key: Char, + ) { + withContext(Dispatchers.EDT) { + val focusOwner1: Component = focusOwner + val keyEvent = + KeyEvent( + focusOwner1, + KeyEvent.KEY_RELEASED, + System.currentTimeMillis(), + 0, + keyCode, + KeyEvent.CHAR_UNDEFINED + ) + focusOwner1.dispatchEvent(keyEvent) + if (!(keyEvent.isConsumed)) { + LOG.debug { "unprocessed keyup: $keyCode" } + } } } - } - private fun inspect(newSelection: Selection) { - if (this.isInspector) { - return - } - val inspectorSession = this.inspectorSession() ?: return - val node = newSelection.selectedNodes[0] - if (node != null && inspectorSession.editorComponent.editedNode !== node) { - inspectorSession.rootNode = node - inspectorSession.editorComponent.editNode(node) - inspectorSession.imageChangeDetector!!.scheduleUpdate() - inspectorSession.updateEditorId() + private fun inspect(newSelection: Selection) { + if (this.isInspector) { + return + } + val inspectorSession = this.inspectorSession() ?: return + val node = newSelection.selectedNodes[0] + if (node != null && inspectorSession.editorComponent.editedNode !== node) { + inspectorSession.rootNode = node + inspectorSession.editorComponent.editNode(node) + inspectorSession.imageChangeDetector!!.scheduleUpdate() + inspectorSession.updateEditorId() + } } } -} fun JsonObject.optBoolean(name: String): Boolean? = get(name)?.jsonPrimitive?.boolean + fun JsonObject?.getInt(name: String): Int { val primitive = this!![name]!!.jsonPrimitive return try { @@ -658,4 +711,5 @@ fun JsonObject?.getInt(name: String): Int { } fun JsonObject?.getString(name: String): String = this!![name]!!.jsonPrimitive.content + fun JsonObject?.optString(name: String): String? = this?.get(name)?.jsonPrimitive?.contentOrNull diff --git a/mps-image-editor-server/src/main/kotlin/svg/plugin/ServerEditorComponent.kt b/mps-image-editor-server/src/main/kotlin/svg/plugin/ServerEditorComponent.kt index 5d9b53d5..4956623e 100644 --- a/mps-image-editor-server/src/main/kotlin/svg/plugin/ServerEditorComponent.kt +++ b/mps-image-editor-server/src/main/kotlin/svg/plugin/ServerEditorComponent.kt @@ -27,59 +27,42 @@ import java.awt.geom.AffineTransform import java.awt.image.ColorModel import javax.swing.JFrame -open class ServerEditorComponent(node: SNode?, project: Project) : - EditorComponent(project.repository, EditorConfigurationBuilder().showErrorsGutter(true).build()) { +open class ServerEditorComponent( + node: SNode?, + project: Project, +) : EditorComponent(project.repository, EditorConfigurationBuilder().showErrorsGutter(true).build()) { private val mpsProject: Project - val dataContext: DataContext = object : DataContext { - override fun getData(key: String): Any? { - return this@ServerEditorComponent.getData(key) + val dataContext: DataContext = + object : DataContext { + override fun getData(key: String): Any? = this@ServerEditorComponent.getData(key) } - } private val highlighter: Highlighter - private val gc: GraphicsConfiguration = object : GraphicsConfiguration() { - private val graphicsConfig: GraphicsConfiguration = this - private val device: GraphicsDevice = object : GraphicsDevice() { - override fun getType(): Int { - return TYPE_RASTER_SCREEN - } - - override fun getIDstring(): String { - return "Modelix EditorComponent" - } - - override fun getConfigurations(): Array { - return arrayOf(graphicsConfig) - } - - override fun getDefaultConfiguration(): GraphicsConfiguration { - return graphicsConfig - } - } + private val gc: GraphicsConfiguration = + object : GraphicsConfiguration() { + private val graphicsConfig: GraphicsConfiguration = this + private val device: GraphicsDevice = + object : GraphicsDevice() { + override fun getType(): Int = TYPE_RASTER_SCREEN - override fun getBounds(): Rectangle { - return Rectangle(0, 0, 1000, 1000) - } + override fun getIDstring(): String = "Modelix EditorComponent" - override fun getColorModel(): ColorModel { - return ColorModel.getRGBdefault() - } + override fun getConfigurations(): Array = arrayOf(graphicsConfig) - override fun getColorModel(transparency: Int): ColorModel { - return ColorModel.getRGBdefault() - } + override fun getDefaultConfiguration(): GraphicsConfiguration = graphicsConfig + } - override fun getDefaultTransform(): AffineTransform { - return AffineTransform() - } + override fun getBounds(): Rectangle = Rectangle(0, 0, 1000, 1000) - override fun getDevice(): GraphicsDevice { - return device - } + override fun getColorModel(): ColorModel = ColorModel.getRGBdefault() + + override fun getColorModel(transparency: Int): ColorModel = ColorModel.getRGBdefault() - override fun getNormalizingTransform(): AffineTransform { - return AffineTransform() + override fun getDefaultTransform(): AffineTransform = AffineTransform() + + override fun getDevice(): GraphicsDevice = device + + override fun getNormalizingTransform(): AffineTransform = AffineTransform() } - } private var frame: JFrame? = null init { @@ -108,62 +91,50 @@ open class ServerEditorComponent(node: SNode?, project: Project) : private val headlessPatternEditor: NodeSubstitutePatternEditor = object : NodeSubstitutePatternEditor() { private var active = false + override fun setText(text: String) { } - override fun getText(): String { - return "" - } + override fun getText(): String = "" override fun setCaretPosition(caretPosition: Int) { } - override fun getCaretPosition(): Int { - return 0 - } + override fun getCaretPosition(): Int = 0 - override fun isActivated(): Boolean { - return active - } + override fun isActivated(): Boolean = active - override fun processKeyPressed(keyEvent: KeyEvent): Boolean { - return false - } + override fun processKeyPressed(keyEvent: KeyEvent): Boolean = false override fun toggleReplaceMode() { } - override fun processKeyTyped(keyEvent: KeyEvent): Boolean { - return false - } + override fun processKeyTyped(keyEvent: KeyEvent): Boolean = false - override fun processTextChanged(textChangeEvent: TextChangeEvent): Boolean { - return false - } + override fun processTextChanged(textChangeEvent: TextChangeEvent): Boolean = false - override fun getPattern(): String { - return "" - } + override fun getPattern(): String = "" - override fun activate(owner: Window, location: Point, size: Dimension, show: Boolean) { + override fun activate( + owner: Window, + location: Point, + size: Dimension, + show: Boolean, + ) { active = true } override fun setLocation(point: Point) { } - override fun getLeftBottomPosition(): Point { - return Point(0, 0) - } + override fun getLeftBottomPosition(): Point = Point(0, 0) override fun done() { active = false } } - override fun getPatternEditor(): NodeSubstitutePatternEditor { - return headlessPatternEditor - } + override fun getPatternEditor(): NodeSubstitutePatternEditor = headlessPatternEditor } ) } @@ -180,9 +151,7 @@ open class ServerEditorComponent(node: SNode?, project: Project) : } @Suppress("removal") - override fun hasFocus(): Boolean { - return true - } + override fun hasFocus(): Boolean = true override fun getData(dataId: @NonNls String): Any? { if (CommonDataKeys.PROJECT.`is`(dataId)) { diff --git a/mps-image-editor-server/src/main/kotlin/svg/plugin/ServerInspectorEditorComponent.kt b/mps-image-editor-server/src/main/kotlin/svg/plugin/ServerInspectorEditorComponent.kt index 62fa8873..8157f86d 100644 --- a/mps-image-editor-server/src/main/kotlin/svg/plugin/ServerInspectorEditorComponent.kt +++ b/mps-image-editor-server/src/main/kotlin/svg/plugin/ServerInspectorEditorComponent.kt @@ -7,18 +7,20 @@ import org.jetbrains.mps.openapi.model.SModel import org.jetbrains.mps.openapi.model.SNode import org.jetbrains.mps.openapi.module.SRepository -class ServerInspectorEditorComponent(node: SNode?, project: Project) : ServerEditorComponent(node, project) { - override fun createEditorContext(model: SModel?, repository: SRepository): EditorContext { - return ServerInspectorEditorContext(this, model, repository) - } +class ServerInspectorEditorComponent( + node: SNode?, + project: Project, +) : ServerEditorComponent(node, project) { + override fun createEditorContext( + model: SModel?, + repository: SRepository, + ): EditorContext = ServerInspectorEditorContext(this, model, repository) private inner class ServerInspectorEditorContext( editorComponent: EditorComponent, model: SModel?, repository: SRepository, ) : EditorContext(editorComponent, model, repository) { - override fun isInspector(): Boolean { - return true - } + override fun isInspector(): Boolean = true } } diff --git a/mps-image-editor-server/src/main/kotlin/svg/svg/EditorToImage.kt b/mps-image-editor-server/src/main/kotlin/svg/svg/EditorToImage.kt index c2aed007..56676896 100644 --- a/mps-image-editor-server/src/main/kotlin/svg/svg/EditorToImage.kt +++ b/mps-image-editor-server/src/main/kotlin/svg/svg/EditorToImage.kt @@ -18,10 +18,13 @@ import java.io.UnsupportedEncodingException import java.util.Base64 import javax.imageio.ImageIO -/*Generated by MPS */ +// Generated by MPS object EditorToImage { - fun paintEditor(editor: EditorComponent, g: Graphics2D) { + fun paintEditor( + editor: EditorComponent, + g: Graphics2D, + ) { ThreadUtils.assertEDT() val paintedComponent = (editor as jetbrains.mps.nodeEditor.EditorComponent).externalComponent @@ -43,7 +46,10 @@ object EditorToImage { } } - fun toPngBase64(editor: EditorComponent, clip: Rectangle?): String { + fun toPngBase64( + editor: EditorComponent, + clip: Rectangle?, + ): String { val size = (editor as jetbrains.mps.nodeEditor.EditorComponent).size val img = BufferedImage(size.width, size.height, BufferedImage.TYPE_INT_ARGB) val g = img.createGraphics() @@ -96,7 +102,10 @@ object EditorToImage { } } - internal fun withGraphicsCopy(g: Graphics, r: _void_P1_E0) { + internal fun withGraphicsCopy( + g: Graphics, + r: _void_P1_E0, + ) { val g2 = g.create() try { r.invoke(g2) diff --git a/mps-image-editor-server/src/main/kotlin/svg/util/AWTExtensions.kt b/mps-image-editor-server/src/main/kotlin/svg/util/AWTExtensions.kt index 3dbdd051..9d0ef98d 100644 --- a/mps-image-editor-server/src/main/kotlin/svg/util/AWTExtensions.kt +++ b/mps-image-editor-server/src/main/kotlin/svg/util/AWTExtensions.kt @@ -10,12 +10,13 @@ object AWTExtensions { if (_this == null) { return emptySequence() } - return _this.ownedWindows.asSequence().filter { it.isVisible }.flatMap { sequenceOf(it) + getVisibleOwnedWindows(it) } + return _this.ownedWindows + .asSequence() + .filter { it.isVisible } + .flatMap { sequenceOf(it) + getVisibleOwnedWindows(it) } } - fun getWindow(_this: Component?): Window { - return SwingUtilities.getWindowAncestor(_this) - } + fun getWindow(_this: Component?): Window = SwingUtilities.getWindowAncestor(_this) fun descendants(_this: Component): Sequence { if (_this is Container) { @@ -25,7 +26,5 @@ object AWTExtensions { } } - fun descendantsAndSelf(_this: Component): Sequence { - return sequenceOf(_this) + descendants(_this) - } + fun descendantsAndSelf(_this: Component): Sequence = sequenceOf(_this) + descendants(_this) } diff --git a/mps/build.gradle.kts b/mps/build.gradle.kts index 5c91daab..9a1342ad 100644 --- a/mps/build.gradle.kts +++ b/mps/build.gradle.kts @@ -16,10 +16,11 @@ dependencies { } val repositoryConceptsFolder = layout.buildDirectory.dir("repositoryConcepts") -val extractRepositoryConcepts = tasks.register("extractRepositoryConcepts", Sync::class) { - from(zipTree({ repositoryConcepts.singleFile })) - into(repositoryConceptsFolder) -} +val extractRepositoryConcepts = + tasks.register("extractRepositoryConcepts", Sync::class) { + from(zipTree({ repositoryConcepts.singleFile })) + into(repositoryConceptsFolder) + } mpsBuild { dependsOn(extractRepositoryConcepts) diff --git a/parser/src/commonMain/kotlin/org/modelix/parser/Grammar.kt b/parser/src/commonMain/kotlin/org/modelix/parser/Grammar.kt index c0841d4f..020d9f0f 100644 --- a/parser/src/commonMain/kotlin/org/modelix/parser/Grammar.kt +++ b/parser/src/commonMain/kotlin/org/modelix/parser/Grammar.kt @@ -6,6 +6,7 @@ import org.modelix.model.api.getAllConcepts import kotlin.collections.plusAssign private val LOG = KotlinLogging.logger { } + class Grammar { private val rules = ArrayList() private val existingLists = HashSet() @@ -19,7 +20,13 @@ class Grammar { // if (forCodeCompletion) modifyForCodeCompletion() addGoal(startConcept) follows = computeFollows() - knownConstants = rules.asSequence().flatMap { it.symbols }.filterIsInstance().map { it.text }.toSet() + knownConstants = + rules + .asSequence() + .flatMap { it.symbols } + .filterIsInstance() + .map { it.text } + .toSet() } private fun modifyForCodeCompletion() { @@ -53,7 +60,12 @@ class Grammar { private fun addRule(rule: ProductionRule) { require(rule.head !is SubConceptsSymbol) { "${rule.head} is only allowed on the right hand side of a rule. Invalid rule: $rule" } - if (rule.symbols.asSequence().flatMap { it.leafSymbols() }.filterIsInstance().any { it.text.isBlank() }) { + if (rule.symbols + .asSequence() + .flatMap { it.leafSymbols() } + .filterIsInstance() + .any { it.text.isBlank() } + ) { LOG.warn { "Ignoring rule with empty constant: $rule" } return } @@ -95,21 +107,21 @@ class Grammar { fun getPossibleFollowingTerminals(nonTerminal: INonTerminalSymbol): Set = follows[nonTerminal] ?: emptySet() private val possibleFirstTokensCache = HashMap>() - fun getPossibleFirstTerminalSymbols(nonTerminal: INonTerminalSymbol): Set { - return possibleFirstTokensCache.getOrPut(nonTerminal) { + + fun getPossibleFirstTerminalSymbols(nonTerminal: INonTerminalSymbol): Set = + possibleFirstTokensCache.getOrPut(nonTerminal) { LinkedHashSet() .also { collectPossibleFirstSymbols(nonTerminal, HashSet(), it, HashSet()) } .filterIsInstance() .toSet() } - } private val possibleFirstRulesCache = HashMap>() - fun getPossibleFirstRules(nonTerminal: INonTerminalSymbol): Set { - return possibleFirstRulesCache.getOrPut(nonTerminal) { + + fun getPossibleFirstRules(nonTerminal: INonTerminalSymbol): Set = + possibleFirstRulesCache.getOrPut(nonTerminal) { LinkedHashSet().also { collectPossibleFirstSymbols(nonTerminal, HashSet(), HashSet(), it) } } - } private fun computeFollows(): Map> { val result = HashMap>() @@ -158,6 +170,7 @@ class Grammar { result.add(symbol) break } + is INonTerminalSymbol -> { val firsts = getPossibleFirstTerminalSymbols(symbol) epsilonInSymbolFirsts = epsilonInSymbolFirsts || firsts.contains(EmptySymbol) @@ -193,6 +206,7 @@ class Grammar { firstSymbols.add(firstSymbol) return } + is INonTerminalSymbol -> { val newSymbols = LinkedHashSet() collectPossibleFirstSymbols(firstSymbol, visited, newSymbols, firstRules) @@ -211,20 +225,20 @@ class Grammar { } private val getRulesForNonTerminalCache = HashMap>() - fun getRulesForNonTerminal(nonTerminal: INonTerminalSymbol): List { - return getRulesForNonTerminalCache.getOrPut(nonTerminal) { + + fun getRulesForNonTerminal(nonTerminal: INonTerminalSymbol): List = + getRulesForNonTerminalCache.getOrPut(nonTerminal) { rules.filter { it.head == nonTerminal } } - } private val getRulesContainingNonTerminalCache = HashMap>() - fun getRulesContainingNonTerminal(nonTerminal: INonTerminalSymbol): List { - return getRulesContainingNonTerminalCache.getOrPut(nonTerminal) { + + fun getRulesContainingNonTerminal(nonTerminal: INonTerminalSymbol): List = + getRulesContainingNonTerminalCache.getOrPut(nonTerminal) { rules.filter { it.symbols.any { it == nonTerminal } } } - } } diff --git a/parser/src/commonMain/kotlin/org/modelix/parser/GraphStructuredStack.kt b/parser/src/commonMain/kotlin/org/modelix/parser/GraphStructuredStack.kt index 96da50a0..4d79efef 100644 --- a/parser/src/commonMain/kotlin/org/modelix/parser/GraphStructuredStack.kt +++ b/parser/src/commonMain/kotlin/org/modelix/parser/GraphStructuredStack.kt @@ -4,41 +4,63 @@ import kotlin.math.min class EmptyGSS : IGSStack { override val containsMerges: Boolean get() = false + override fun peek(): E = throw NoSuchElementException("Empty stack") + override fun push(element: E): IGSStack = RegularGSSNode(element, this) + override fun pop(): Pair>> = throw NoSuchElementException("Empty stack") + override fun pop(n: Int): List, IGSStack>> = throw NoSuchElementException("Empty stack") + override fun elementAt(n: Int): E = throw NoSuchElementException("Empty stack") + override fun toString(): String = "" - override fun tryMerge(other: IGSStack): IGSStack? { - return if (other is EmptyGSS) this else null - } + + override fun tryMerge(other: IGSStack): IGSStack? = if (other is EmptyGSS) this else null + override fun withoutMerges(): List> = listOf(this) + override fun getSize(): IntRange = 0..0 } -class RegularGSSNode(private val element: E, private val previous: IGSStack) : IGSStack { +class RegularGSSNode( + private val element: E, + private val previous: IGSStack, +) : IGSStack { override val containsMerges: Boolean = previous.containsMerges + override fun peek(): E = element + override fun push(element: E): IGSStack = RegularGSSNode(element, this) + override fun pop(): Pair>> = element to listOf(previous) - override fun pop(n: Int): List, IGSStack>> { - return when (n) { - 0 -> listOf(emptyList() to this) - 1 -> listOf(listOf(element) to previous) - else -> previous.pop(n - 1).map { popped: Pair, IGSStack> -> - listOf(element) + popped.first to popped.second + + override fun pop(n: Int): List, IGSStack>> = + when (n) { + 0 -> { + listOf(emptyList() to this) + } + + 1 -> { + listOf(listOf(element) to previous) + } + + else -> { + previous.pop(n - 1).map { popped: Pair, IGSStack> -> + listOf(element) + popped.first to popped.second + } } } - } - override fun elementAt(n: Int): E { - return if (n == 0) element else previous.elementAt(n - 1) - } + override fun elementAt(n: Int): E = if (n == 0) element else previous.elementAt(n - 1) override fun tryMerge(other: IGSStack): IGSStack? { return when (other) { - this -> this + this -> { + this + } + is RegularGSSNode -> { if (element == other.element) { val mergedPrev = other.previous.tryMerge(previous) @@ -54,6 +76,7 @@ class RegularGSSNode(private val element: E, private val previo null } } + is MergeGSSNode -> { if (element == other.peek()) { MergeGSSNode(element, listOf(previous) + other.pop().second) @@ -61,40 +84,59 @@ class RegularGSSNode(private val element: E, private val previo null } } - else -> null + + else -> { + null + } } } - override fun withoutMerges(): List> { - return if (containsMerges) previous.withoutMerges().map { RegularGSSNode(element, it) } else listOf(this) - } + override fun withoutMerges(): List> = + if (containsMerges) { + previous.withoutMerges().map { + RegularGSSNode(element, it) + } + } else { + listOf(this) + } override fun getSize(): IntRange = previous.getSize().let { (it.first + 1)..(it.last + 1) } - override fun toString(): String { - return "$previous | $element" - } + override fun toString(): String = "$previous | $element" } -class MergeGSSNode(private val element: E, private val previous: List>) : IGSStack { +class MergeGSSNode( + private val element: E, + private val previous: List>, +) : IGSStack { override val containsMerges: Boolean get() = true + override fun peek(): E = element + override fun push(element: E): IGSStack = RegularGSSNode(element, this) + override fun pop(): Pair>> = element to previous - override fun pop(n: Int): List, IGSStack>> { - return when (n) { - 0 -> listOf(emptyList() to this) - 1 -> previous.map { listOf(element) to it } - else -> previous.flatMap { prev -> - prev.pop(n - 1).map { popped: Pair, IGSStack> -> - listOf(element) + popped.first to popped.second + + override fun pop(n: Int): List, IGSStack>> = + when (n) { + 0 -> { + listOf(emptyList() to this) + } + + 1 -> { + previous.map { listOf(element) to it } + } + + else -> { + previous.flatMap { prev -> + prev.pop(n - 1).map { popped: Pair, IGSStack> -> + listOf(element) + popped.first to popped.second + } } } } - } - override fun elementAt(n: Int): E { - return if (n == 0) element else error("Stack is merged and has multiple values") - } + + override fun elementAt(n: Int): E = if (n == 0) element else error("Stack is merged and has multiple values") override fun tryMerge(other: IGSStack): IGSStack? { if (other == this) return this @@ -103,35 +145,36 @@ class MergeGSSNode(private val element: E, private val previous return null } - override fun withoutMerges(): List> { - return previous.flatMap { it.withoutMerges() }.map { RegularGSSNode(element, it) } - } + override fun withoutMerges(): List> = previous.flatMap { it.withoutMerges() }.map { RegularGSSNode(element, it) } - override fun getSize(): IntRange { - return previous.map { it.getSize() } + override fun getSize(): IntRange = + previous + .map { it.getSize() } .reduce { acc, it -> min(acc.first, it.first)..min(acc.last, it.last) } .let { (it.first + 1)..(it.last + 1) } - } - override fun toString(): String { - return "merge/${previous.size} | $element" - } + override fun toString(): String = "merge/${previous.size} | $element" } -fun Iterable>.push(element: T): IGSStack { - return MergeGSSNode(element, toList()) -} +fun Iterable>.push(element: T): IGSStack = MergeGSSNode(element, toList()) interface IGSStack { fun push(element: E): IGSStack + fun pop(): Pair>> + fun pop(n: Int): List, IGSStack>> + fun peek(): E + fun elementAt(n: Int): E + fun getSize(): IntRange val containsMerges: Boolean + fun tryMerge(other: IGSStack): IGSStack? + fun withoutMerges(): List> } diff --git a/parser/src/commonMain/kotlin/org/modelix/parser/IDisambiguator.kt b/parser/src/commonMain/kotlin/org/modelix/parser/IDisambiguator.kt index 7d104bd6..683e8ed4 100644 --- a/parser/src/commonMain/kotlin/org/modelix/parser/IDisambiguator.kt +++ b/parser/src/commonMain/kotlin/org/modelix/parser/IDisambiguator.kt @@ -2,24 +2,24 @@ package org.modelix.parser interface IDisambiguator { fun chooseActionIndex(actions: List): Int + fun withLastDisambiguator(newLast: IDisambiguator): IDisambiguator + companion object { fun default() = ChooseFirstDisambiguator() // PreferReduceDisambiguator(ChooseFirstDisambiguator()) } } -fun IDisambiguator.chooseActionIndexIfNecessary(actions: List): Int { - return if (actions.size == 1) 0 else chooseActionIndex(actions) -} +fun IDisambiguator.chooseActionIndexIfNecessary(actions: List): Int = if (actions.size == 1) 0 else chooseActionIndex(actions) -fun IDisambiguator.chooseAction(actions: List): LRAction { - return if (actions.size == 1) actions[0] else actions[chooseActionIndex(actions)] -} +fun IDisambiguator.chooseAction(actions: List): LRAction = + if (actions.size == 1) actions[0] else actions[chooseActionIndex(actions)] class DepthFirstSearchDisambiguator : IDisambiguator { private val nextIndices = mutableListOf() private val isDone = mutableListOf() private var currentDepth = 0 + override fun chooseActionIndex(actions: List): Int { if (currentDepth > nextIndices.lastIndex) { nextIndices.add(0) @@ -30,6 +30,7 @@ class DepthFirstSearchDisambiguator : IDisambiguator { currentDepth++ return result } + fun next(): Boolean { if (isDone.isEmpty()) return false while (isDone.last()) { @@ -48,9 +49,9 @@ class DepthFirstSearchDisambiguator : IDisambiguator { class BreadthFirstSearchDisambiguator : IDisambiguator { private val root = SearchTree() private var currentPath = mutableListOf(root) - override fun chooseActionIndex(actions: List): Int { - return currentPath.last().chooseActionIndex(actions) - } + + override fun chooseActionIndex(actions: List): Int = currentPath.last().chooseActionIndex(actions) + fun next(): Boolean { currentPath.reversed().forEach { it.updateDoneState() } currentPath.clear() @@ -93,13 +94,14 @@ class BreadthFirstSearchDisambiguator : IDisambiguator { } class ChooseFirstDisambiguator : IDisambiguator { - override fun chooseActionIndex(actions: List): Int { - return 0 - } + override fun chooseActionIndex(actions: List): Int = 0 + override fun withLastDisambiguator(newLast: IDisambiguator): IDisambiguator = newLast } -class PreferShiftDisambiguator(val next: IDisambiguator) : IDisambiguator { +class PreferShiftDisambiguator( + val next: IDisambiguator, +) : IDisambiguator { override fun chooseActionIndex(actions: List): Int { if (actions.size == 2) { if (actions[0] is ShiftAction && actions[1] is ReduceAction) return 0 @@ -108,12 +110,13 @@ class PreferShiftDisambiguator(val next: IDisambiguator) : IDisambiguator { return next.chooseActionIndex(actions) } - override fun withLastDisambiguator(newLast: IDisambiguator): IDisambiguator { - return PreferShiftDisambiguator(next.withLastDisambiguator(newLast)) - } + override fun withLastDisambiguator(newLast: IDisambiguator): IDisambiguator = + PreferShiftDisambiguator(next.withLastDisambiguator(newLast)) } -class PreferReduceDisambiguator(val next: IDisambiguator) : IDisambiguator { +class PreferReduceDisambiguator( + val next: IDisambiguator, +) : IDisambiguator { override fun chooseActionIndex(actions: List): Int { if (actions.size == 2) { if (actions[0] is ShiftAction && actions[1] is ReduceAction) return 1 @@ -122,7 +125,6 @@ class PreferReduceDisambiguator(val next: IDisambiguator) : IDisambiguator { return next.chooseActionIndex(actions) } - override fun withLastDisambiguator(newLast: IDisambiguator): IDisambiguator { - return PreferReduceDisambiguator(next.withLastDisambiguator(newLast)) - } + override fun withLastDisambiguator(newLast: IDisambiguator): IDisambiguator = + PreferReduceDisambiguator(next.withLastDisambiguator(newLast)) } diff --git a/parser/src/commonMain/kotlin/org/modelix/parser/ISymbol.kt b/parser/src/commonMain/kotlin/org/modelix/parser/ISymbol.kt index 193e015e..3dc88441 100644 --- a/parser/src/commonMain/kotlin/org/modelix/parser/ISymbol.kt +++ b/parser/src/commonMain/kotlin/org/modelix/parser/ISymbol.kt @@ -7,65 +7,60 @@ import org.modelix.model.api.IRole interface ISymbol { fun leafSymbols(): Sequence = sequenceOf(this) + fun matches(token: IParseTreeNode): Boolean } + interface ITerminalSymbol : ISymbol + interface INonTerminalSymbol : ISymbol -data class OptionalSymbol(val children: List) : INonTerminalSymbol { +data class OptionalSymbol( + val children: List, +) : INonTerminalSymbol { constructor(vararg symbol: ISymbol) : this(symbol.toList()) - override fun leafSymbols(): Sequence { - return children.asSequence().flatMap { it.leafSymbols() } - } + override fun leafSymbols(): Sequence = children.asSequence().flatMap { it.leafSymbols() } - override fun matches(token: IParseTreeNode): Boolean { - return token is ParseTreeNode && token.rule.head == this - } + override fun matches(token: IParseTreeNode): Boolean = token is ParseTreeNode && token.rule.head == this - override fun toString(): String { - return "optional(${children.joinToString(" ")})" - } + override fun toString(): String = "optional(${children.joinToString(" ")})" } -data class ConstantSymbol(val text: String) : ITerminalSymbol { - override fun toString(): String { - return "constant[$text]" - } +data class ConstantSymbol( + val text: String, +) : ITerminalSymbol { + override fun toString(): String = "constant[$text]" - override fun matches(token: IParseTreeNode): Boolean { - return token is IToken && (token.symbol == null || token.symbol == this) && token.text == text - } + override fun matches(token: IParseTreeNode): Boolean = + token is IToken && (token.symbol == null || token.symbol == this) && token.text == text companion object { val CARET = ConstantSymbol("\u16B9") // ᚹ } } -data class ExactConceptSymbol(val concept: IConcept) : INonTerminalSymbol { - override fun toString(): String { - return concept.getShortName() - } +data class ExactConceptSymbol( + val concept: IConcept, +) : INonTerminalSymbol { + override fun toString(): String = concept.getShortName() - override fun matches(token: IParseTreeNode): Boolean { - return token is INonTerminalToken && token.getNonTerminalSymbol() == this - } + override fun matches(token: IParseTreeNode): Boolean = token is INonTerminalToken && token.getNonTerminalSymbol() == this } -data class SubConceptsSymbol(val concept: IConcept) : INonTerminalSymbol { - override fun toString(): String { - return concept.getShortName() + "+" - } +data class SubConceptsSymbol( + val concept: IConcept, +) : INonTerminalSymbol { + override fun toString(): String = concept.getShortName() + "+" - override fun matches(token: IParseTreeNode): Boolean { - return token is INonTerminalToken && token.getNonTerminalSymbol() == this - } + override fun matches(token: IParseTreeNode): Boolean = token is INonTerminalToken && token.getNonTerminalSymbol() == this } -open class RegexSymbol(val regex: Regex?) : ITerminalSymbol { - override fun matches(token: IParseTreeNode): Boolean { - return token is IToken && (token.symbol == null || token.symbol == this) && (regex == null || token.text.matches(regex)) - } +open class RegexSymbol( + val regex: Regex?, +) : ITerminalSymbol { + override fun matches(token: IParseTreeNode): Boolean = + token is IToken && (token.symbol == null || token.symbol == this) && (regex == null || token.text.matches(regex)) override fun equals(other: Any?): Boolean { if (this === other) return true @@ -76,13 +71,9 @@ open class RegexSymbol(val regex: Regex?) : ITerminalSymbol { return regex == other.regex } - override fun hashCode(): Int { - return regex?.pattern.hashCode() - } + override fun hashCode(): Int = regex?.pattern.hashCode() - override fun toString(): String { - return "regex/${regex?.pattern}/" - } + override fun toString(): String = "regex/${regex?.pattern}/" companion object { val defaultIdentifierPattern = Regex("""[_a-zA-Z][_a-zA-Z0-9]*""") @@ -91,7 +82,9 @@ open class RegexSymbol(val regex: Regex?) : ITerminalSymbol { } } -abstract class RoleSymbol(regex: Regex?) : RegexSymbol(regex) { +abstract class RoleSymbol( + regex: Regex?, +) : RegexSymbol(regex) { abstract val role: IRole override fun equals(other: Any?): Boolean { @@ -106,40 +99,39 @@ abstract class RoleSymbol(regex: Regex?) : RegexSymbol(regex) { return true } - override fun hashCode(): Int { - return 31 * role.hashCode() + regex?.pattern.hashCode() - } + override fun hashCode(): Int = 31 * role.hashCode() + regex?.pattern.hashCode() } -class ReferenceSymbol(override val role: IReferenceLink, regex: Regex? = defaultIdentifierPattern) : RoleSymbol(regex) { +class ReferenceSymbol( + override val role: IReferenceLink, + regex: Regex? = defaultIdentifierPattern, +) : RoleSymbol(regex) { override fun toString() = "reference[${role.getSimpleName()}, ${regex?.pattern}]" } -class PropertySymbol(override val role: IProperty, regex: Regex? = defaultPropertyPattern) : RoleSymbol(regex) { +class PropertySymbol( + override val role: IProperty, + regex: Regex? = defaultPropertyPattern, +) : RoleSymbol(regex) { override fun toString() = "property[${role.getSimpleName()}, ${regex?.pattern}]" } object EndOfInputSymbol : ITerminalSymbol { - override fun matches(token: IParseTreeNode): Boolean { - return token == EndOfInputToken - } + override fun matches(token: IParseTreeNode): Boolean = token == EndOfInputToken - override fun toString(): String { - return "$" - } + override fun toString(): String = "$" } object EmptySymbol : ITerminalSymbol { - override fun matches(token: IParseTreeNode): Boolean { - return token == EmptyToken - } + override fun matches(token: IParseTreeNode): Boolean = token == EmptyToken - override fun toString(): String { - return "ε" - } + override fun toString(): String = "ε" } -class ProductionRule(val head: INonTerminalSymbol, val symbols: List) { +class ProductionRule( + val head: INonTerminalSymbol, + val symbols: List, +) { constructor(head: INonTerminalSymbol, vararg symbols: ISymbol) : this(head, symbols.toList()) init { @@ -148,22 +140,24 @@ class ProductionRule(val head: INonTerminalSymbol, val symbols: List) { } } - override fun toString(): String { - return "$head -> ${symbols.ifEmpty { listOf(EmptySymbol) }.joinToString(" ")}" - } + override fun toString(): String = "$head -> ${symbols.ifEmpty { listOf(EmptySymbol) }.joinToString(" ")}" fun isGoal() = head == GoalSymbol + fun isEmpty() = symbols.isEmpty() } object GoalSymbol : INonTerminalSymbol { override fun toString(): String = "goal" + override fun matches(token: IParseTreeNode): Boolean = false } -data class ListSymbol(val item: ISymbol, val separator: ITerminalSymbol?) : INonTerminalSymbol { +data class ListSymbol( + val item: ISymbol, + val separator: ITerminalSymbol?, +) : INonTerminalSymbol { override fun toString(): String = "list<$item>" - override fun matches(token: IParseTreeNode): Boolean { - return token is INonTerminalToken && token.getNonTerminalSymbol() == this - } + + override fun matches(token: IParseTreeNode): Boolean = token is INonTerminalToken && token.getNonTerminalSymbol() == this } diff --git a/parser/src/commonMain/kotlin/org/modelix/parser/IToken.kt b/parser/src/commonMain/kotlin/org/modelix/parser/IToken.kt index 469934f5..be7a2c27 100644 --- a/parser/src/commonMain/kotlin/org/modelix/parser/IToken.kt +++ b/parser/src/commonMain/kotlin/org/modelix/parser/IToken.kt @@ -3,37 +3,54 @@ package org.modelix.parser sealed interface IToken : IParseTreeNode { val text: String val symbol: ISymbol? + fun textLength(): Int = text.length } -data class Token(override val text: String, val startPos: Int, override val symbol: ISymbol?) : IToken +data class Token( + override val text: String, + val startPos: Int, + override val symbol: ISymbol?, +) : IToken -data class WhitespaceToken(override val text: String, val startPos: Int) : IToken { +data class WhitespaceToken( + override val text: String, + val startPos: Int, +) : IToken { override val symbol: ISymbol? get() = null } data object EmptyToken : IToken { override val symbol: ISymbol? get() = null override val text: String = "" + override fun textLength(): Int = 0 } data object EndOfInputToken : IToken { override val symbol: ISymbol? get() = null override val text: String = "" + override fun textLength(): Int = 0 } sealed interface IParseTreeNode { fun childNodes(): Sequence = emptySequence() + fun descendants(): Sequence = childNodes().flatMap { it.descendantsAndSelf() } + fun descendantsAndSelf(): Sequence = sequenceOf(this) + descendants() } + sealed interface INonTerminalToken : IParseTreeNode { fun getNonTerminalSymbol(): INonTerminalSymbol } -class ParseTreeNode(val rule: ProductionRule, val children: List) : IParseTreeNode, INonTerminalToken { +class ParseTreeNode( + val rule: ProductionRule, + val children: List, +) : IParseTreeNode, + INonTerminalToken { override fun toString(): String { if (children.size > 1) { return "${rule.head} {\n${children.joinToString("\n").prependIndent()}\n}" @@ -42,36 +59,41 @@ class ParseTreeNode(val rule: ProductionRule, val children: List } } - override fun getNonTerminalSymbol(): INonTerminalSymbol { - return rule.head - } + override fun getNonTerminalSymbol(): INonTerminalSymbol = rule.head override fun childNodes(): Sequence = children.asSequence() } -class AmbiguousNode(val symbol: INonTerminalSymbol, val trees: List) : IParseTreeNode, INonTerminalToken { - override fun toString(): String { - return "ambiguous {\n${trees.joinToString("\n---\n").prependIndent()}\n}" - } +class AmbiguousNode( + val symbol: INonTerminalSymbol, + val trees: List, +) : IParseTreeNode, + INonTerminalToken { + override fun toString(): String = "ambiguous {\n${trees.joinToString("\n---\n").prependIndent()}\n}" - override fun getNonTerminalSymbol(): INonTerminalSymbol { - return symbol - } + override fun getNonTerminalSymbol(): INonTerminalSymbol = symbol fun flatten(): AmbiguousNode { - val newChildren = trees.flatMap { - if (it is AmbiguousNode && it.symbol == symbol) it.trees else listOf(it) - } + val newChildren = + trees.flatMap { + if (it is AmbiguousNode && it.symbol == symbol) it.trees else listOf(it) + } return if (newChildren.size == trees.size) this else AmbiguousNode(symbol, newChildren) } override fun childNodes(): Sequence = trees.asSequence() companion object { - fun create(trees: List): INonTerminalToken? { - return when (trees.size) { - 0 -> null - 1 -> trees.first() + fun create(trees: List): INonTerminalToken? = + when (trees.size) { + 0 -> { + null + } + + 1 -> { + trees.first() + } + else -> { val symbol = trees.first().getNonTerminalSymbol() check(trees.asSequence().drop(1).all { it.getNonTerminalSymbol() == symbol }) { @@ -80,12 +102,12 @@ class AmbiguousNode(val symbol: INonTerminalSymbol, val trees: List) { - +class SPPF( + val roots: List, +) { private var sequence = 0 private val nonSharedNodes = LinkedHashSet() private val sharedNodes = LinkedHashSet() @@ -107,27 +129,36 @@ class SPPF(val roots: List) { nonSharedNodes.add(node) } when (node) { - is AmbiguousNode -> node.trees.forEach { load(it) } - is ParseTreeNode -> node.children.forEach { load(it) } + is AmbiguousNode -> { + node.trees.forEach { load(it) } + } + + is ParseTreeNode -> { + node.children.forEach { load(it) } + } + else -> {} } } - override fun toString(): String { - return (sharedNodes + nonSharedNodes).joinToString("\n") { + override fun toString(): String = + (sharedNodes + nonSharedNodes).joinToString("\n") { toString(it) } - } - private fun toString(node: IParseTreeNode): String { - return when (node) { + private fun toString(node: IParseTreeNode): String = + when (node) { is AmbiguousNode -> { "n${id(node)} [label=\"${node.symbol}\", shape=diamond]\n" + node.trees.joinToString("\n") { "n${id(node)} -> n${id(it)}" } } + is ParseTreeNode -> { - "n${id(node)} [label=\"${node.rule.head}\", shape=box]\n" + node.children.joinToString("\n") { "n${id(node)} -> n${id(it)}" } + "n${id(node)} [label=\"${node.rule.head}\", shape=box]\n" + + node.children.joinToString("\n") { "n${id(node)} -> n${id(it)}" } + } + + else -> { + "n${id(node)} [label=\"${node}\", shape=oval]" } - else -> "n${id(node)} [label=\"${node}\", shape=oval]" } - } } diff --git a/parser/src/commonMain/kotlin/org/modelix/parser/LRClosureTable.kt b/parser/src/commonMain/kotlin/org/modelix/parser/LRClosureTable.kt index c0f57927..8b071dac 100644 --- a/parser/src/commonMain/kotlin/org/modelix/parser/LRClosureTable.kt +++ b/parser/src/commonMain/kotlin/org/modelix/parser/LRClosureTable.kt @@ -1,6 +1,8 @@ package org.modelix.parser -class LRClosureTable(val grammar: Grammar) { +class LRClosureTable( + val grammar: Grammar, +) { val kernels = KernelsList() fun load() { @@ -21,25 +23,26 @@ class LRClosureTable(val grammar: Grammar) { while (kernel.closure.size > oldSize) { oldSize = kernel.closure.size - kernel.closure.values.asSequence() + kernel.closure.values + .asSequence() .map { Pair( it.nextSymbol() as? INonTerminalSymbol, it.nextNextSymbol(), ) - } - .filter { it.first != null } + }.filter { it.first != null } .groupBy { it.first } .forEach { group -> val rules = grammar.getPossibleFirstRules(group.key!!) for (rule in rules) { val positionInRule = PositionInRule(0, rule) val existing = kernel.closure[positionInRule] - kernel.closure[positionInRule] = if (existing == null) { - RuleItem(positionInRule) - } else { - RuleItem(existing.positionInRule) - } + kernel.closure[positionInRule] = + if (existing == null) { + RuleItem(positionInRule) + } else { + RuleItem(existing.positionInRule) + } } } } @@ -87,7 +90,10 @@ class LRClosureTable(val grammar: Grammar) { fun getByItems(items: Set) = kernelsMap[items] } - class Kernel(val index: Int, var items: Set) { + class Kernel( + val index: Int, + var items: Set, + ) { var closure: MutableMap = items.associateBy { it.positionInRule }.toMutableMap() val gotos: MutableMap = HashMap() val keys: MutableSet = HashSet() @@ -96,13 +102,12 @@ class LRClosureTable(val grammar: Grammar) { fun List.iterateGrowingList(): Iterator = GrowingListIterator(this) -class GrowingListIterator(private val list: List) : Iterator { +class GrowingListIterator( + private val list: List, +) : Iterator { private var i = 0 - override fun hasNext(): Boolean { - return i < list.size - } - override fun next(): E { - return list[i++] - } + override fun hasNext(): Boolean = i < list.size + + override fun next(): E = list[i++] } diff --git a/parser/src/commonMain/kotlin/org/modelix/parser/LRParser.kt b/parser/src/commonMain/kotlin/org/modelix/parser/LRParser.kt index 37929993..0a79ebc0 100644 --- a/parser/src/commonMain/kotlin/org/modelix/parser/LRParser.kt +++ b/parser/src/commonMain/kotlin/org/modelix/parser/LRParser.kt @@ -1,39 +1,63 @@ package org.modelix.parser interface IParser { - fun parse(input: String, complete: Boolean): IParseTreeNode + fun parse( + input: String, + complete: Boolean, + ): IParseTreeNode + fun parse(input: String): IParseTreeNode = parse(input, complete = false) + fun parseCompleting(input: String): IParseTreeNode = parse(input, complete = true) - fun tryParse(input: String, complete: Boolean): IParseTreeNode? + + fun tryParse( + input: String, + complete: Boolean, + ): IParseTreeNode? + fun parseForest(input: String): Sequence = parseForest(input, false) - fun parseForest(input: String, complete: Boolean): Sequence + + fun parseForest( + input: String, + complete: Boolean, + ): Sequence } -class LRParser(val table: LRTable, private val defaultDisambiguator: IDisambiguator) : IParser { +class LRParser( + val table: LRTable, + private val defaultDisambiguator: IDisambiguator, +) : IParser { var stepLimit = 10_000 private var disambiguator = defaultDisambiguator - override fun parse(input: String, complete: Boolean): IParseTreeNode { - return tryParse(input, complete) ?: error("Invalid input: $input\nCurrent stack: ???") - } - - override fun tryParse(input: String, complete: Boolean): IParseTreeNode? { - return doParse(input, complete).firstOrNull() - } - - override fun parseForest(input: String, complete: Boolean): Sequence { - return doParse(input, complete).asSequence() - } - - private fun doParse(input: String, complete: Boolean): List { + override fun parse( + input: String, + complete: Boolean, + ): IParseTreeNode = tryParse(input, complete) ?: error("Invalid input: $input\nCurrent stack: ???") + + override fun tryParse( + input: String, + complete: Boolean, + ): IParseTreeNode? = doParse(input, complete).firstOrNull() + + override fun parseForest( + input: String, + complete: Boolean, + ): Sequence = doParse(input, complete).asSequence() + + private fun doParse( + input: String, + complete: Boolean, + ): List { val acceptedForks = ArrayList() - fun List.filterAccepted() = filter { - if (it.accepted) { - acceptedForks.add(it) + fun List.filterAccepted() = + filter { + if (it.accepted) { + acceptedForks.add(it) + } + !it.accepted } - !it.accepted - } val scanner = Scanner(input) scanner.addKnownConstants(table.knownConstants) @@ -79,22 +103,26 @@ class LRParser(val table: LRTable, private val defaultDisambiguator: IDisambigua private fun mergeForks(forks: List): List { if (forks.size < 2) return forks - val mergedForks = forks.filter { !it.stack.peek().isState() } + - forks.filter { it.stack.peek().isState() }.groupBy { it.stack.peek().getState() to it.actionToApply } - .map { group -> - if (group.value.size == 1) return@map group.value.first() - - val mergedStack = group.value - .map { it.stack } - .reduce { acc, it -> - checkNotNull(acc.tryMerge(it)) { "Merge failed" } - } - - Fork( - mergedStack, - group.key.second, - ) - } + val mergedForks = + forks.filter { !it.stack.peek().isState() } + + forks + .filter { it.stack.peek().isState() } + .groupBy { it.stack.peek().getState() to it.actionToApply } + .map { group -> + if (group.value.size == 1) return@map group.value.first() + + val mergedStack = + group.value + .map { it.stack } + .reduce { acc, it -> + checkNotNull(acc.tryMerge(it)) { "Merge failed" } + } + + Fork( + mergedStack, + group.key.second, + ) + } // if (forks.size != mergedForks.size) println("forks ${forks.size} -> ${mergedForks.size}") check(mergedForks.size <= 1000) { "Too many forks" } return mergedForks @@ -108,13 +136,12 @@ class LRParser(val table: LRTable, private val defaultDisambiguator: IDisambigua var output: List? = null fun stateIndex(): Int = (stack.peek().takeIf { it.isState() } ?: stack.elementAt(1)).getState() + fun state() = table.states[stateIndex()] fun readyToShift() = actionToApply is ShiftAction - override fun toString(): String { - return "$stack || $actionToApply" - } + override fun toString(): String = "$stack || $actionToApply" fun forksForNextActions(lookaheadTokens: List): List { check(!accepted) { "Already accepted" } @@ -128,10 +155,14 @@ class LRParser(val table: LRTable, private val defaultDisambiguator: IDisambigua state().getSymbolsAndActions().firstOrNull { it.first.matches(tokenOnStack) }?.second return actions?.map { Fork(stack, it) } ?: emptyList() } else { - val applicableActions = state().getSymbolsAndActions().filter { - val symbol = it.first - symbol == EmptySymbol || lookaheadTokens.any { symbol.matches(it) } - }.flatMap { it.second.asSequence() }.toSet() + val applicableActions = + state() + .getSymbolsAndActions() + .filter { + val symbol = it.first + symbol == EmptySymbol || lookaheadTokens.any { symbol.matches(it) } + }.flatMap { it.second.asSequence() } + .toSet() // TODO filter out reductions that don't match the actual content on the stack return applicableActions.map { Fork(stack, it) } } @@ -145,25 +176,40 @@ class LRParser(val table: LRTable, private val defaultDisambiguator: IDisambigua fun applyAction(tokensForShift: List): List { check(!accepted) { "Already accepted" } return when (val action = actionToApply) { - null -> error("No action applicable. Fork should have been discarded.") - is SkipAction -> listOf(Fork(stack, null)) + null -> { + error("No action applicable. Fork should have been discarded.") + } + + is SkipAction -> { + listOf(Fork(stack, null)) + } + is ShiftAction -> { var newStack = stack val matchingTokens = tokensForShift.filter { action.symbol.matches(it) } - val matchingToken = when (matchingTokens.size) { - 0 -> error("None of the tokens matches ${action.symbol}: $tokensForShift") - 1 -> matchingTokens.single() - else -> error("Multiple of the tokens matches ${action.symbol}: $matchingTokens") - } + val matchingToken = + when (matchingTokens.size) { + 0 -> error("None of the tokens matches ${action.symbol}: $tokensForShift") + 1 -> matchingTokens.single() + else -> error("Multiple of the tokens matches ${action.symbol}: $matchingTokens") + } newStack = newStack.pushNode(matchingToken) newStack = newStack.pushState(action.nextState) listOf(Fork(newStack, null)) } - is CompletionAction -> reduceItem(action.item) - is ReduceAction -> reduceItem(RuleItem(action.rule, action.rule.symbols.size)) + + is CompletionAction -> { + reduceItem(action.item) + } + + is ReduceAction -> { + reduceItem(RuleItem(action.rule, action.rule.symbols.size)) + } + is GotoAction -> { listOf(Fork(stack.pushState(action.nextState), null)) } + AcceptAction -> { output = stack.withoutMerges().map { it.elementAt(it.getSize().first - 2).getToken() } accepted = true @@ -188,9 +234,10 @@ class LRParser(val table: LRTable, private val defaultDisambiguator: IDisambigua if (removedTokens.size == 1) { val symbolToReduce: INonTerminalToken? = removedTokens.single() as? ParseTreeNode - val wrappers = generateSequence(symbolToReduce) { - (it as? ParseTreeNode)?.children?.singleOrNull() as? INonTerminalToken - }.map { it.getNonTerminalSymbol() } + val wrappers = + generateSequence(symbolToReduce) { + (it as? ParseTreeNode)?.children?.singleOrNull() as? INonTerminalToken + }.map { it.getNonTerminalSymbol() } val isUnnecessaryWrapper = wrappers.contains(item.rule.head) // if, after applying a series of wrappers, we end up with the same non-terminal that we // already had on the stack, it means we could have just taken that one without wrapping it @@ -213,16 +260,23 @@ class LRParser(val table: LRTable, private val defaultDisambiguator: IDisambigua return push(StackElement(state)) } - private data class StackElement private constructor(private val node: IParseTreeNode? = null, private val state: Int? = null) : IGSSElement { + private data class StackElement private constructor( + private val node: IParseTreeNode? = null, + private val state: Int? = null, + ) : IGSSElement { constructor(token: IParseTreeNode) : this(token, null) constructor(state: Int) : this(null, state) + fun isNode() = node != null + fun isState() = state != null + fun getToken() = checkNotNull(node) { "Not a token" } + fun getState() = checkNotNull(state) { "Not a state" } - override fun toString(): String { - return if (isNode()) getToken().toString() else "[" + getState().toString() + "]" - } + + override fun toString(): String = if (isNode()) getToken().toString() else "[" + getState().toString() + "]" + override fun merge(other: IGSSElement): IGSSElement? { check(other is StackElement) if (this == other) return this @@ -239,15 +293,14 @@ class LRParser(val table: LRTable, private val defaultDisambiguator: IDisambigua } } -fun Grammar.createParser(disambiguator: IDisambiguator = IDisambiguator.default()): LRParser { - return LRParser(createParseTable(), disambiguator) -} +fun Grammar.createParser(disambiguator: IDisambiguator = IDisambiguator.default()): LRParser = LRParser(createParseTable(), disambiguator) fun Grammar.createParseTable(): LRTable { val closureTable = LRClosureTable(grammar = this).also { it.load() } return LRTable().also { it.load(closureTable) } } -inline fun Sequence.associateWithNotNull(valueSelector: (K) -> V?): Map { - return associateWith { valueSelector(it) }.filterValues { it != null } as Map -} +inline fun Sequence.associateWithNotNull(valueSelector: (K) -> V?): Map = + associateWith { + valueSelector(it) + }.filterValues { it != null } as Map diff --git a/parser/src/commonMain/kotlin/org/modelix/parser/LRTable.kt b/parser/src/commonMain/kotlin/org/modelix/parser/LRTable.kt index 6c08131b..d4da3ae2 100644 --- a/parser/src/commonMain/kotlin/org/modelix/parser/LRTable.kt +++ b/parser/src/commonMain/kotlin/org/modelix/parser/LRTable.kt @@ -2,12 +2,15 @@ package org.modelix.parser import kotlin.math.min -class LRTable() { +class LRTable { val states: MutableList = ArrayList() var knownConstants: Set = emptySet() - fun getDistanceToAccept(action: LRAction, pathLength: Int = 0): Int { - return when (action) { + fun getDistanceToAccept( + action: LRAction, + pathLength: Int = 0, + ): Int = + when (action) { AcceptAction -> 0 is GotoAction -> 1 + getDistanceToAccept(states[action.nextState], pathLength + 1) is ReduceAction -> -action.rule.symbols.size @@ -15,9 +18,11 @@ class LRTable() { is ShiftAction -> 1 + getDistanceToAccept(states[action.nextState], pathLength + 1) is SkipAction -> error("Not expected to appear in the parse table") } - } - fun getDistanceToAccept(state: LRState, pathLength: Int): Int { + fun getDistanceToAccept( + state: LRState, + pathLength: Int, + ): Int { if (state.distanceToAccept == -1) { if (pathLength > 100) return Int.MAX_VALUE / 2 state.distanceToAccept = Int.MAX_VALUE / 2 // also avoid endless recursion @@ -38,11 +43,12 @@ class LRTable() { for (key in kernel.keys) { val nextStateIndex = kernel.gotos[key]!! - val action = if (key is INonTerminalSymbol) { - GotoAction(nextStateIndex) - } else { - ShiftAction(nextStateIndex, key as ITerminalSymbol) - } + val action = + if (key is INonTerminalSymbol) { + GotoAction(nextStateIndex) + } else { + ShiftAction(nextStateIndex, key as ITerminalSymbol) + } state.addAction(key, action) } @@ -69,13 +75,17 @@ class LRTable() { } private val emptyActionsArray: Array = emptyArray() + class LRState { var kernel: LRClosureTable.Kernel? = null var distanceToAccept: Int = -1 private var actions: Map>? = null - fun addAction(symbol: ISymbol, action: LRAction) { + fun addAction( + symbol: ISymbol, + action: LRAction, + ) { val oldMap = actions if (oldMap == null) { actions = SingleEntryMap(symbol, arrayOf(action)) @@ -93,9 +103,7 @@ class LRState { } } - fun getActions(symbol: ISymbol): Array { - return actions?.get(symbol) ?: emptyActionsArray - } + fun getActions(symbol: ISymbol): Array = actions?.get(symbol) ?: emptyActionsArray /** * Better performance than building a sequence. @@ -105,12 +113,17 @@ class LRState { if (actions is SingleEntryMap) visitActionsInArray((actions as SingleEntryMap).value, visitor) } - private fun visitActionsInArray(value: Any, visitor: (LRAction) -> Unit) { + private fun visitActionsInArray( + value: Any, + visitor: (LRAction) -> Unit, + ) { when (value) { null -> {} + is LRAction -> { visitor(value) } + else -> { for (action in (value as Array)) { visitor(action) @@ -121,20 +134,40 @@ class LRState { fun getSymbols(): Sequence = actions?.keys?.asSequence() ?: emptySequence() - fun getSymbolsAndActions(): Sequence>> { - return actions?.asSequence()?.map { it.key to it.value } ?: emptySequence() - } + fun getSymbolsAndActions(): Sequence>> = + actions?.asSequence()?.map { + it.key to it.value + } ?: emptySequence() } sealed class LRAction -data class ShiftAction(val nextState: Int, val symbol: ITerminalSymbol) : LRAction() -data class ReduceAction(val rule: ProductionRule) : LRAction() -data class CompletionAction(val item: RuleItem) : LRAction() -data class GotoAction(val nextState: Int) : LRAction() + +data class ShiftAction( + val nextState: Int, + val symbol: ITerminalSymbol, +) : LRAction() + +data class ReduceAction( + val rule: ProductionRule, +) : LRAction() + +data class CompletionAction( + val item: RuleItem, +) : LRAction() + +data class GotoAction( + val nextState: Int, +) : LRAction() + data object SkipAction : LRAction() + data object AcceptAction : LRAction() -private class SingleEntryMap(override val key: K, override val value: V) : Map, Map.Entry { +private class SingleEntryMap( + override val key: K, + override val value: V, +) : Map, + Map.Entry { override val entries: Set> get() = SingleEntrySet(this) override val keys: Set @@ -153,10 +186,10 @@ private class SingleEntryMap(override val key: K, override val value: V) : override fun isEmpty(): Boolean = false } -private class SingleEntrySet(val value: E) : Set { - override fun contains(element: E): Boolean { - return element == value - } +private class SingleEntrySet( + val value: E, +) : Set { + override fun contains(element: E): Boolean = element == value override val size: Int get() = 1 @@ -165,11 +198,7 @@ private class SingleEntrySet(val value: E) : Set { TODO("Not yet implemented") } - override fun isEmpty(): Boolean { - return false - } + override fun isEmpty(): Boolean = false - override fun iterator(): Iterator { - return listOf(value).iterator() - } + override fun iterator(): Iterator = listOf(value).iterator() } diff --git a/parser/src/commonMain/kotlin/org/modelix/parser/RuleItem.kt b/parser/src/commonMain/kotlin/org/modelix/parser/RuleItem.kt index 8f841508..dfd8177a 100644 --- a/parser/src/commonMain/kotlin/org/modelix/parser/RuleItem.kt +++ b/parser/src/commonMain/kotlin/org/modelix/parser/RuleItem.kt @@ -1,26 +1,32 @@ package org.modelix.parser -data class RuleItem(val positionInRule: PositionInRule) { +data class RuleItem( + val positionInRule: PositionInRule, +) { constructor(rule: ProductionRule, cursor: Int) : this(PositionInRule(cursor, rule)) + val rule: ProductionRule get() = positionInRule.rule val cursor: Int get() = positionInRule.position fun nextSymbol(): ISymbol? = rule.symbols.getOrNull(cursor) + fun nextNextSymbol(): ISymbol? = rule.symbols.getOrNull(cursor + 1) + fun forward(): RuleItem? { check(!isReadyForReduce()) return if (cursor < rule.symbols.size) RuleItem(rule, cursor + 1) else null } + fun isReadyForReduce() = nextSymbol() == null + fun size() = rule.symbols.size - override fun toString(): String { - return rule.head.toString() + + override fun toString(): String = + rule.head.toString() + " -> " + rule.symbols.take(cursor).joinToString(" ") + " # " + rule.symbols.drop(cursor).joinToString(" ") - } override fun equals(other: Any?): Boolean { if (this === other) return true @@ -33,8 +39,12 @@ data class RuleItem(val positionInRule: PositionInRule) { return true } - private val _hashCode = arrayOf(positionInRule).contentHashCode() - override fun hashCode(): Int = _hashCode + private val cachedHashCode = arrayOf(positionInRule).contentHashCode() + + override fun hashCode(): Int = cachedHashCode } -data class PositionInRule(val position: Int, val rule: ProductionRule) +data class PositionInRule( + val position: Int, + val rule: ProductionRule, +) diff --git a/parser/src/commonMain/kotlin/org/modelix/parser/Scanner.kt b/parser/src/commonMain/kotlin/org/modelix/parser/Scanner.kt index 9a424b37..a39926c7 100644 --- a/parser/src/commonMain/kotlin/org/modelix/parser/Scanner.kt +++ b/parser/src/commonMain/kotlin/org/modelix/parser/Scanner.kt @@ -5,7 +5,6 @@ class Scanner( private var position: Int = 0, private var knownConstants: Set = emptySet(), ) { - private val whitespaceRegex = Regex("\\s+") private val expectedNextTerminals: MutableSet = HashSet() @@ -31,7 +30,8 @@ class Scanner( private fun matchNextTokens(): List { if (isAtEnd()) return listOf(EndOfInputToken) check(expectedNextTerminals.isNotEmpty()) { "Possible terminal symbols unknown" } - return expectedNextTerminals.asSequence() + return expectedNextTerminals + .asSequence() .map { matchInput(it) } .plus(matchRegex(whitespaceRegex) { WhitespaceToken(it, position) }) .filterNotNull() @@ -56,8 +56,8 @@ class Scanner( expectedNextTerminals.add(terminal) } - private fun matchInput(symbol: ITerminalSymbol): IToken? { - return when (symbol) { + private fun matchInput(symbol: ITerminalSymbol): IToken? = + when (symbol) { is ConstantSymbol -> { if (input.startsWith(symbol.text, position)) { Token(symbol.text, position, symbol) @@ -65,19 +65,29 @@ class Scanner( null } } + is RegexSymbol -> { matchRegex(symbol.regex) { Token(it, position, symbol) } } + EndOfInputSymbol -> { if (isAtEnd()) EndOfInputToken else null } - EmptySymbol -> EmptyToken - else -> throw UnsupportedOperationException("Unknown symbol: $symbol") + + EmptySymbol -> { + EmptyToken + } + + else -> { + throw UnsupportedOperationException("Unknown symbol: $symbol") + } } - } - private fun matchRegex(regex: Regex?, createToken: (String) -> IToken): IToken? { - return if (regex != null) { + private fun matchRegex( + regex: Regex?, + createToken: (String) -> IToken, + ): IToken? = + if (regex != null) { val match = regex.matchAt(input, position) if (match != null) { check(match.range.first == position) @@ -94,13 +104,13 @@ class Scanner( null } } - } - private fun findNextConstants(): List> { - return knownConstants.asSequence().plus(" ") + private fun findNextConstants(): List> = + knownConstants + .asSequence() + .plus(" ") .map { it to input.indexOf(it, position) } .filter { it.second != -1 } .toList() .sortedBy { it.second } - } } diff --git a/parser/src/commonTest/kotlin/org/modelix/parser/ExpressionsTest.kt b/parser/src/commonTest/kotlin/org/modelix/parser/ExpressionsTest.kt index 0d8d4cc4..330ea636 100644 --- a/parser/src/commonTest/kotlin/org/modelix/parser/ExpressionsTest.kt +++ b/parser/src/commonTest/kotlin/org/modelix/parser/ExpressionsTest.kt @@ -5,126 +5,132 @@ import kotlin.test.assertTrue import kotlin.time.measureTime class ExpressionsTest { - - @Test fun integerLiteral() = runTest( - "1", - """ - Expression+ { IntegerLiteral { PropertyToken(text=1) } } - """.trimIndent() - ) - - @Test fun plus2() = runTest( - "1+2", - """ - Expression+ { PlusExpression { - Expression+ { IntegerLiteral { PropertyToken(text=1) } } - ConstantToken(text=+) - Expression+ { IntegerLiteral { PropertyToken(text=2) } } - } } - """.trimIndent() - ) - - @Test fun plus2withSpaces() = runTest( - "1 + 2", - """ - Expression+ { PlusExpression { + @Test fun integerLiteral() = + runTest( + "1", + """ Expression+ { IntegerLiteral { PropertyToken(text=1) } } - ConstantToken(text=+) - Expression+ { IntegerLiteral { PropertyToken(text=2) } } - } } - """.trimIndent() - ) + """.trimIndent() + ) - @Test fun plus3() = runTest( - "1+2+3", - """ - Expression+ { PlusExpression { + @Test fun plus2() = + runTest( + "1+2", + """ Expression+ { PlusExpression { Expression+ { IntegerLiteral { PropertyToken(text=1) } } ConstantToken(text=+) Expression+ { IntegerLiteral { PropertyToken(text=2) } } } } - ConstantToken(text=+) - Expression+ { IntegerLiteral { PropertyToken(text=3) } } - } } - --- - Expression+ { PlusExpression { - Expression+ { IntegerLiteral { PropertyToken(text=1) } } - ConstantToken(text=+) + """.trimIndent() + ) + + @Test fun plus2withSpaces() = + runTest( + "1 + 2", + """ Expression+ { PlusExpression { + Expression+ { IntegerLiteral { PropertyToken(text=1) } } + ConstantToken(text=+) Expression+ { IntegerLiteral { PropertyToken(text=2) } } + } } + """.trimIndent() + ) + + @Test fun plus3() = + runTest( + "1+2+3", + """ + Expression+ { PlusExpression { + Expression+ { PlusExpression { + Expression+ { IntegerLiteral { PropertyToken(text=1) } } + ConstantToken(text=+) + Expression+ { IntegerLiteral { PropertyToken(text=2) } } + } } ConstantToken(text=+) Expression+ { IntegerLiteral { PropertyToken(text=3) } } } } - } } - """.trimIndent() - ) - - @Test fun plus4() = runTest( - "1+2+3+4", - """ - Expression+ { PlusExpression { + --- Expression+ { PlusExpression { Expression+ { IntegerLiteral { PropertyToken(text=1) } } ConstantToken(text=+) - Expression+ { IntegerLiteral { PropertyToken(text=2) } } + Expression+ { PlusExpression { + Expression+ { IntegerLiteral { PropertyToken(text=2) } } + ConstantToken(text=+) + Expression+ { IntegerLiteral { PropertyToken(text=3) } } + } } } } - ConstantToken(text=+) - Expression+ { IntegerLiteral { PropertyToken(text=3) } } - } } - --- - Expression+ { PlusExpression { - Expression+ { IntegerLiteral { PropertyToken(text=1) } } - ConstantToken(text=+) + """.trimIndent() + ) + + @Test fun plus4() = + runTest( + "1+2+3+4", + """ Expression+ { PlusExpression { - Expression+ { IntegerLiteral { PropertyToken(text=2) } } + Expression+ { PlusExpression { + Expression+ { IntegerLiteral { PropertyToken(text=1) } } + ConstantToken(text=+) + Expression+ { IntegerLiteral { PropertyToken(text=2) } } + } } ConstantToken(text=+) Expression+ { IntegerLiteral { PropertyToken(text=3) } } } } - } } - """.trimIndent() - ) + --- + Expression+ { PlusExpression { + Expression+ { IntegerLiteral { PropertyToken(text=1) } } + ConstantToken(text=+) + Expression+ { PlusExpression { + Expression+ { IntegerLiteral { PropertyToken(text=2) } } + ConstantToken(text=+) + Expression+ { IntegerLiteral { PropertyToken(text=3) } } + } } + } } + """.trimIndent() + ) - @Test fun plus5() = runTest( - "1+2+3+4+5", - """ - PlusExpression { - IntegerLiteral { PropertyToken(text=1) } - ConstantToken(text=+) + @Test fun plus5() = + runTest( + "1+2+3+4+5", + """ PlusExpression { - IntegerLiteral { PropertyToken(text=2) } + IntegerLiteral { PropertyToken(text=1) } ConstantToken(text=+) PlusExpression { - IntegerLiteral { PropertyToken(text=3) } + IntegerLiteral { PropertyToken(text=2) } ConstantToken(text=+) PlusExpression { - IntegerLiteral { PropertyToken(text=4) } + IntegerLiteral { PropertyToken(text=3) } ConstantToken(text=+) PlusExpression { - IntegerLiteral { PropertyToken(text=5) } + IntegerLiteral { PropertyToken(text=4) } ConstantToken(text=+) PlusExpression { - IntegerLiteral { PropertyToken(text=6) } + IntegerLiteral { PropertyToken(text=5) } ConstantToken(text=+) PlusExpression { - IntegerLiteral { PropertyToken(text=7) } + IntegerLiteral { PropertyToken(text=6) } ConstantToken(text=+) PlusExpression { - IntegerLiteral { PropertyToken(text=8) } + IntegerLiteral { PropertyToken(text=7) } ConstantToken(text=+) PlusExpression { - IntegerLiteral { PropertyToken(text=9) } + IntegerLiteral { PropertyToken(text=8) } ConstantToken(text=+) PlusExpression { - IntegerLiteral { PropertyToken(text=10) } + IntegerLiteral { PropertyToken(text=9) } ConstantToken(text=+) PlusExpression { - IntegerLiteral { PropertyToken(text=11) } + IntegerLiteral { PropertyToken(text=10) } ConstantToken(text=+) PlusExpression { - IntegerLiteral { PropertyToken(text=12) } + IntegerLiteral { PropertyToken(text=11) } ConstantToken(text=+) - IntegerLiteral { PropertyToken(text=13) } + PlusExpression { + IntegerLiteral { PropertyToken(text=12) } + ConstantToken(text=+) + IntegerLiteral { PropertyToken(text=13) } + } } } } @@ -136,149 +142,137 @@ class ExpressionsTest { } } } - } - """.trimIndent() - ) + """.trimIndent() + ) - @Test fun test4() = runTest( - "1+2*3+4", - """ - PlusExpression { - IntegerLiteral { PropertyToken(text=1) } - ConstantToken(text=+) - MulExpression { - IntegerLiteral { PropertyToken(text=2) } - ConstantToken(text=*) + @Test fun test4() = + runTest( + "1+2*3+4", + """ + PlusExpression { + IntegerLiteral { PropertyToken(text=1) } + ConstantToken(text=+) + MulExpression { + IntegerLiteral { PropertyToken(text=2) } + ConstantToken(text=*) + PlusExpression { + IntegerLiteral { PropertyToken(text=3) } + ConstantToken(text=+) + IntegerLiteral { PropertyToken(text=4) } + } + } + } + """.trimIndent() + ) + + @Test fun test5() = + runTest( + "1+(2*3)+4", + """ + PlusExpression { + IntegerLiteral { PropertyToken(text=1) } + ConstantToken(text=+) PlusExpression { - IntegerLiteral { PropertyToken(text=3) } + ParensExpression { + ConstantToken(text=() + MulExpression { + IntegerLiteral { PropertyToken(text=2) } + ConstantToken(text=*) + IntegerLiteral { PropertyToken(text=3) } + } + ConstantToken(text=)) + } ConstantToken(text=+) IntegerLiteral { PropertyToken(text=4) } } } - } - """.trimIndent() - ) + """.trimIndent() + ) - @Test fun test5() = runTest( - "1+(2*3)+4", - """ - PlusExpression { - IntegerLiteral { PropertyToken(text=1) } - ConstantToken(text=+) + @Test fun test6() = + runTest( + "((1+2)*3)+(((4+5)+(6*7))*(8+9))", + """ PlusExpression { ParensExpression { ConstantToken(text=() MulExpression { - IntegerLiteral { PropertyToken(text=2) } + ParensExpression { + ConstantToken(text=() + PlusExpression { + IntegerLiteral { PropertyToken(text=1) } + ConstantToken(text=+) + IntegerLiteral { PropertyToken(text=2) } + } + ConstantToken(text=)) + } ConstantToken(text=*) IntegerLiteral { PropertyToken(text=3) } } ConstantToken(text=)) } ConstantToken(text=+) - IntegerLiteral { PropertyToken(text=4) } - } - } - """.trimIndent() - ) - - @Test fun test6() = runTest( - "((1+2)*3)+(((4+5)+(6*7))*(8+9))", - """ - PlusExpression { - ParensExpression { - ConstantToken(text=() - MulExpression { - ParensExpression { - ConstantToken(text=() - PlusExpression { - IntegerLiteral { PropertyToken(text=1) } - ConstantToken(text=+) - IntegerLiteral { PropertyToken(text=2) } - } - ConstantToken(text=)) - } - ConstantToken(text=*) - IntegerLiteral { PropertyToken(text=3) } - } - ConstantToken(text=)) - } - ConstantToken(text=+) - ParensExpression { - ConstantToken(text=() - MulExpression { - ParensExpression { - ConstantToken(text=() - PlusExpression { - ParensExpression { - ConstantToken(text=() - PlusExpression { - IntegerLiteral { PropertyToken(text=4) } - ConstantToken(text=+) - IntegerLiteral { PropertyToken(text=5) } + ParensExpression { + ConstantToken(text=() + MulExpression { + ParensExpression { + ConstantToken(text=() + PlusExpression { + ParensExpression { + ConstantToken(text=() + PlusExpression { + IntegerLiteral { PropertyToken(text=4) } + ConstantToken(text=+) + IntegerLiteral { PropertyToken(text=5) } + } + ConstantToken(text=)) } - ConstantToken(text=)) - } - ConstantToken(text=+) - ParensExpression { - ConstantToken(text=() - MulExpression { - IntegerLiteral { PropertyToken(text=6) } - ConstantToken(text=*) - IntegerLiteral { PropertyToken(text=7) } + ConstantToken(text=+) + ParensExpression { + ConstantToken(text=() + MulExpression { + IntegerLiteral { PropertyToken(text=6) } + ConstantToken(text=*) + IntegerLiteral { PropertyToken(text=7) } + } + ConstantToken(text=)) } - ConstantToken(text=)) } + ConstantToken(text=)) } - ConstantToken(text=)) - } - ConstantToken(text=*) - ParensExpression { - ConstantToken(text=() - PlusExpression { - IntegerLiteral { PropertyToken(text=8) } - ConstantToken(text=+) - IntegerLiteral { PropertyToken(text=9) } + ConstantToken(text=*) + ParensExpression { + ConstantToken(text=() + PlusExpression { + IntegerLiteral { PropertyToken(text=8) } + ConstantToken(text=+) + IntegerLiteral { PropertyToken(text=9) } + } + ConstantToken(text=)) } - ConstantToken(text=)) } + ConstantToken(text=)) } - ConstantToken(text=)) } - } - """.trimIndent() - ) - - @Test fun testParentheses1() = runTest( - "(1)", - """ - ParensExpression { - ConstantToken(text=() - IntegerLiteral { PropertyToken(text=1) } - ConstantToken(text=)) - } - """.trimIndent() - ) + """.trimIndent() + ) - @Test fun testParentheses2() = runTest( - "(1+2)", - """ - ParensExpression { - ConstantToken(text=() - PlusExpression { + @Test fun testParentheses1() = + runTest( + "(1)", + """ + ParensExpression { + ConstantToken(text=() IntegerLiteral { PropertyToken(text=1) } - ConstantToken(text=+) - IntegerLiteral { PropertyToken(text=2) } + ConstantToken(text=)) } - ConstantToken(text=)) - } - """.trimIndent() - ) + """.trimIndent() + ) - @Test fun testParentheses3() = runTest( - "(1+2)+3", - """ - PlusExpression { + @Test fun testParentheses2() = + runTest( + "(1+2)", + """ ParensExpression { ConstantToken(text=() PlusExpression { @@ -288,211 +282,242 @@ class ExpressionsTest { } ConstantToken(text=)) } - ConstantToken(text=+) - IntegerLiteral { PropertyToken(text=3) } - } - """.trimIndent() - ) + """.trimIndent() + ) - @Test fun testParentheses4() = runTest( - "(1+2)+(3+4)", - """ - PlusExpression { - ParensExpression { - ConstantToken(text=() - PlusExpression { - IntegerLiteral { PropertyToken(text=1) } - ConstantToken(text=+) - IntegerLiteral { PropertyToken(text=2) } + @Test fun testParentheses3() = + runTest( + "(1+2)+3", + """ + PlusExpression { + ParensExpression { + ConstantToken(text=() + PlusExpression { + IntegerLiteral { PropertyToken(text=1) } + ConstantToken(text=+) + IntegerLiteral { PropertyToken(text=2) } + } + ConstantToken(text=)) } - ConstantToken(text=)) + ConstantToken(text=+) + IntegerLiteral { PropertyToken(text=3) } } - ConstantToken(text=+) - ParensExpression { - ConstantToken(text=() - PlusExpression { - IntegerLiteral { PropertyToken(text=3) } - ConstantToken(text=+) - IntegerLiteral { PropertyToken(text=4) } + """.trimIndent() + ) + + @Test fun testParentheses4() = + runTest( + "(1+2)+(3+4)", + """ + PlusExpression { + ParensExpression { + ConstantToken(text=() + PlusExpression { + IntegerLiteral { PropertyToken(text=1) } + ConstantToken(text=+) + IntegerLiteral { PropertyToken(text=2) } + } + ConstantToken(text=)) + } + ConstantToken(text=+) + ParensExpression { + ConstantToken(text=() + PlusExpression { + IntegerLiteral { PropertyToken(text=3) } + ConstantToken(text=+) + IntegerLiteral { PropertyToken(text=4) } + } + ConstantToken(text=)) } - ConstantToken(text=)) } - } - """.trimIndent() - ) + """.trimIndent() + ) - @Test fun testParentheses5() = runTest( - "((1+2)+3)+4", - """ - PlusExpression { - ParensExpression { - ConstantToken(text=() - PlusExpression { - ParensExpression { - ConstantToken(text=() - PlusExpression { - IntegerLiteral { PropertyToken(text=1) } - ConstantToken(text=+) - IntegerLiteral { PropertyToken(text=2) } + @Test fun testParentheses5() = + runTest( + "((1+2)+3)+4", + """ + PlusExpression { + ParensExpression { + ConstantToken(text=() + PlusExpression { + ParensExpression { + ConstantToken(text=() + PlusExpression { + IntegerLiteral { PropertyToken(text=1) } + ConstantToken(text=+) + IntegerLiteral { PropertyToken(text=2) } + } + ConstantToken(text=)) } - ConstantToken(text=)) + ConstantToken(text=+) + IntegerLiteral { PropertyToken(text=3) } } - ConstantToken(text=+) - IntegerLiteral { PropertyToken(text=3) } + ConstantToken(text=)) } - ConstantToken(text=)) + ConstantToken(text=+) + IntegerLiteral { PropertyToken(text=4) } } - ConstantToken(text=+) - IntegerLiteral { PropertyToken(text=4) } - } - """.trimIndent() - ) + """.trimIndent() + ) - @Test fun testTernary() = runTest( - "1 * 2 ? 3 + 4 + 5 : 6 + 7", - """ - MulExpression { - IntegerLiteral { PropertyToken(text=1) } - ConstantToken(text=*) - TernaryExpression { - IntegerLiteral { PropertyToken(text=2) } - ConstantToken(text=?) - PlusExpression { - IntegerLiteral { PropertyToken(text=3) } - ConstantToken(text=+) - IntegerLiteral { PropertyToken(text=4) } - } - ConstantToken(text=:) - PlusExpression { - IntegerLiteral { PropertyToken(text=5) } - ConstantToken(text=+) - IntegerLiteral { PropertyToken(text=6) } + @Test fun testTernary() = + runTest( + "1 * 2 ? 3 + 4 + 5 : 6 + 7", + """ + MulExpression { + IntegerLiteral { PropertyToken(text=1) } + ConstantToken(text=*) + TernaryExpression { + IntegerLiteral { PropertyToken(text=2) } + ConstantToken(text=?) + PlusExpression { + IntegerLiteral { PropertyToken(text=3) } + ConstantToken(text=+) + IntegerLiteral { PropertyToken(text=4) } + } + ConstantToken(text=:) + PlusExpression { + IntegerLiteral { PropertyToken(text=5) } + ConstantToken(text=+) + IntegerLiteral { PropertyToken(text=6) } + } } } - } - """.trimIndent() - ) - - @Test fun testListLiteral1() = runTest( - "list[1]", - """ - ListLiteral { - ConstantToken(text=list) - ConstantToken(text=[) - list { IntegerLiteral { PropertyToken(text=1) } } - ConstantToken(text=]) - } - """.trimIndent() - ) + """.trimIndent() + ) - @Test fun testListLiteral2() = runTest( - "list[1, 2]", - """ - ListLiteral { - ConstantToken(text=list) - ConstantToken(text=[) - list { - IntegerLiteral { PropertyToken(text=1) } - ConstantToken(text=,) - list { IntegerLiteral { PropertyToken(text=2) } } + @Test fun testListLiteral1() = + runTest( + "list[1]", + """ + ListLiteral { + ConstantToken(text=list) + ConstantToken(text=[) + list { IntegerLiteral { PropertyToken(text=1) } } + ConstantToken(text=]) } - ConstantToken(text=]) - } - """.trimIndent() - ) + """.trimIndent() + ) - @Test fun testListLiteral3() = runTest( - "list[1, 2, 3]", - """ - ListLiteral { - ConstantToken(text=list) - ConstantToken(text=[) - list { - IntegerLiteral { PropertyToken(text=1) } - ConstantToken(text=,) + @Test fun testListLiteral2() = + runTest( + "list[1, 2]", + """ + ListLiteral { + ConstantToken(text=list) + ConstantToken(text=[) list { - IntegerLiteral { PropertyToken(text=2) } + IntegerLiteral { PropertyToken(text=1) } ConstantToken(text=,) - list { IntegerLiteral { PropertyToken(text=3) } } + list { IntegerLiteral { PropertyToken(text=2) } } } + ConstantToken(text=]) } - ConstantToken(text=]) - } - """.trimIndent() - ) - - @Test fun testStringLiteral() = runTest( - """"abc"""", - """ - StringLiteral { - ConstantToken(text=") - PropertyToken(text=abc) - ConstantToken(text=") - } - """.trimIndent() - ) + """.trimIndent() + ) - @Test fun testStringLiteral2() = runTest( - """ "abc" """, - """ - StringLiteral { - ConstantToken(text=") - PropertyToken(text=abc) - ConstantToken(text=") - } - """.trimIndent() - ) - - @Test fun testStringLiteral3() = runTest( - """ " abc " """, - """ - StringLiteral { - ConstantToken(text=") - PropertyToken(text= abc ) - ConstantToken(text=") - } - """.trimIndent() - ) + @Test fun testListLiteral3() = + runTest( + "list[1, 2, 3]", + """ + ListLiteral { + ConstantToken(text=list) + ConstantToken(text=[) + list { + IntegerLiteral { PropertyToken(text=1) } + ConstantToken(text=,) + list { + IntegerLiteral { PropertyToken(text=2) } + ConstantToken(text=,) + list { IntegerLiteral { PropertyToken(text=3) } } + } + } + ConstantToken(text=]) + } + """.trimIndent() + ) - @Test fun testStringLiteral4() = runTest( - """ " abc " + " def " """, - """ - PlusExpression { + @Test fun testStringLiteral() = + runTest( + """"abc"""", + """ StringLiteral { ConstantToken(text=") - PropertyToken(text= abc ) + PropertyToken(text=abc) ConstantToken(text=") } - ConstantToken(text=+) + """.trimIndent() + ) + + @Test fun testStringLiteral2() = + runTest( + """ "abc" """, + """ StringLiteral { ConstantToken(text=") - PropertyToken(text= def ) + PropertyToken(text=abc) ConstantToken(text=") } - } - """.trimIndent() - ) + """.trimIndent() + ) - @Test fun testStringLiteral5() = runTest( - """"Hello World!" + "Hello World!"""", - """ - PlusExpression { + @Test fun testStringLiteral3() = + runTest( + """ " abc " """, + """ StringLiteral { ConstantToken(text=") PropertyToken(text= abc ) ConstantToken(text=") } - ConstantToken(text=+) - StringLiteral { - ConstantToken(text=") - PropertyToken(text= def ) - ConstantToken(text=") + """.trimIndent() + ) + + @Test fun testStringLiteral4() = + runTest( + """ " abc " + " def " """, + """ + PlusExpression { + StringLiteral { + ConstantToken(text=") + PropertyToken(text= abc ) + ConstantToken(text=") + } + ConstantToken(text=+) + StringLiteral { + ConstantToken(text=") + PropertyToken(text= def ) + ConstantToken(text=") + } + } + """.trimIndent() + ) + + @Test fun testStringLiteral5() = + runTest( + """"Hello World!" + "Hello World!"""", + """ + PlusExpression { + StringLiteral { + ConstantToken(text=") + PropertyToken(text= abc ) + ConstantToken(text=") + } + ConstantToken(text=+) + StringLiteral { + ConstantToken(text=") + PropertyToken(text= def ) + ConstantToken(text=") + } } - } - """.trimIndent() - ) + """.trimIndent() + ) - fun runTest(input: String, expected: String) { + fun runTest( + input: String, + expected: String, + ) { val parser = TestGrammar.getParser(TestGrammar.expression) val parseTrees = parser.parseForest(input) println(measureTime { parser.parse(input) }) diff --git a/parser/src/commonTest/kotlin/org/modelix/parser/StatementsTest.kt b/parser/src/commonTest/kotlin/org/modelix/parser/StatementsTest.kt index 64f1ce68..3b28f9ee 100644 --- a/parser/src/commonTest/kotlin/org/modelix/parser/StatementsTest.kt +++ b/parser/src/commonTest/kotlin/org/modelix/parser/StatementsTest.kt @@ -5,43 +5,47 @@ import kotlin.test.assertTrue import kotlin.time.measureTime class StatementsTest { - - @Test fun localVarDeclWithoutInitializer() = runTest( - "int a;", - """ - Statement+ { LocalVariableDeclarationStatement { - LocalVariableDeclaration+ { LocalVariableDeclaration { - Type+ { IntegerType { ConstantToken(text=int) } } - PropertyToken(text=a) - optional(constant[=] Expression+) { EmptyToken } + @Test fun localVarDeclWithoutInitializer() = + runTest( + "int a;", + """ + Statement+ { LocalVariableDeclarationStatement { + LocalVariableDeclaration+ { LocalVariableDeclaration { + Type+ { IntegerType { ConstantToken(text=int) } } + PropertyToken(text=a) + optional(constant[=] Expression+) { EmptyToken } + } } + ConstantToken(text=;) } } - ConstantToken(text=;) - } } - """.trimIndent() - ) + """.trimIndent() + ) - @Test fun localVarDeclWithInitializer() = runTest( - "int a = 10 + 20;", - """ - Statement+ { LocalVariableDeclarationStatement { - LocalVariableDeclaration+ { LocalVariableDeclaration { - Type+ { IntegerType { ConstantToken(text=int) } } - PropertyToken(text=a) - optional(constant[=] Expression+) { - ConstantToken(text==) - Expression+ { PlusExpression { - Expression+ { IntegerLiteral { PropertyToken(text=10) } } - ConstantToken(text=+) - Expression+ { IntegerLiteral { PropertyToken(text=20) } } - } } - } + @Test fun localVarDeclWithInitializer() = + runTest( + "int a = 10 + 20;", + """ + Statement+ { LocalVariableDeclarationStatement { + LocalVariableDeclaration+ { LocalVariableDeclaration { + Type+ { IntegerType { ConstantToken(text=int) } } + PropertyToken(text=a) + optional(constant[=] Expression+) { + ConstantToken(text==) + Expression+ { PlusExpression { + Expression+ { IntegerLiteral { PropertyToken(text=10) } } + ConstantToken(text=+) + Expression+ { IntegerLiteral { PropertyToken(text=20) } } + } } + } + } } + ConstantToken(text=;) } } - ConstantToken(text=;) - } } - """.trimIndent() - ) + """.trimIndent() + ) - fun runTest(input: String, expected: String) { + fun runTest( + input: String, + expected: String, + ) { val parser = TestGrammar.getParser(TestGrammar.statement) val parseTrees = parser.parseForest(input) println(measureTime { parser.parse(input) }) diff --git a/parser/src/commonTest/kotlin/org/modelix/parser/TestGrammar.kt b/parser/src/commonTest/kotlin/org/modelix/parser/TestGrammar.kt index 531506ec..e2b9acf5 100644 --- a/parser/src/commonTest/kotlin/org/modelix/parser/TestGrammar.kt +++ b/parser/src/commonTest/kotlin/org/modelix/parser/TestGrammar.kt @@ -27,9 +27,22 @@ object TestGrammar { addRule(plusExpression, SubConceptsSymbol(expression), ConstantSymbol("+"), SubConceptsSymbol(expression)) addRule(mulExpression, SubConceptsSymbol(expression), ConstantSymbol("*"), SubConceptsSymbol(expression)) addRule(parensExpression, ConstantSymbol("("), SubConceptsSymbol(expression), ConstantSymbol(")")) - addRule(listLiteral, ConstantSymbol("list"), ConstantSymbol("["), ListSymbol(SubConceptsSymbol(expression), ConstantSymbol(",")), ConstantSymbol("]")) + addRule( + listLiteral, + ConstantSymbol("list"), + ConstantSymbol("["), + ListSymbol(SubConceptsSymbol(expression), ConstantSymbol(",")), + ConstantSymbol("]") + ) addRule(stringLiteral, ConstantSymbol("\""), RegexSymbol(RegexSymbol.defaultStringLiteralRegex), ConstantSymbol("\"")) - addRule(ternaryExpression, SubConceptsSymbol(expression), ConstantSymbol("?"), SubConceptsSymbol(expression), ConstantSymbol(":"), SubConceptsSymbol(expression)) + addRule( + ternaryExpression, + SubConceptsSymbol(expression), + ConstantSymbol("?"), + SubConceptsSymbol(expression), + ConstantSymbol(":"), + SubConceptsSymbol(expression) + ) addRule(localVariableDeclarationStatement, SubConceptsSymbol(localVariableDeclaration), ConstantSymbol(";")) addRule( @@ -45,7 +58,10 @@ object TestGrammar { addRule(integerType, ConstantSymbol("int")) } - fun getParser(startConcept: IConcept, disambiguator: IDisambiguator = IDisambiguator.default()): LRParser { + fun getParser( + startConcept: IConcept, + disambiguator: IDisambiguator = IDisambiguator.default(), + ): LRParser { val grammar = Grammar(startConcept, rules) val closureTable = LRClosureTable(grammar) closureTable.load() @@ -54,7 +70,10 @@ object TestGrammar { return LRParser(parsingTable, disambiguator) } - fun addRule(concept: IConcept, vararg symbols: ISymbol) { + fun addRule( + concept: IConcept, + vararg symbols: ISymbol, + ) { rules.add(ProductionRule(ExactConceptSymbol(concept), symbols.toList())) } } diff --git a/projectional-editor-ssr-client-lib/build.gradle.kts b/projectional-editor-ssr-client-lib/build.gradle.kts index 951341ab..64123aee 100644 --- a/projectional-editor-ssr-client-lib/build.gradle.kts +++ b/projectional-editor-ssr-client-lib/build.gradle.kts @@ -31,6 +31,8 @@ kotlin { implementation(libs.kotlin.html) implementation(libs.modelix.model.api) implementation(libs.kotlin.logging) + implementation(libs.kotlinx.rpc.krpc.ktor.client) + implementation(libs.kotlinx.rpc.krpc.serialization.json) } } val jsTest by getting { diff --git a/projectional-editor-ssr-client-lib/src/jsMain/kotlin/org/modelix/editor/ssr/client/ClientSideEditor.kt b/projectional-editor-ssr-client-lib/src/jsMain/kotlin/org/modelix/editor/ssr/client/ClientSideEditor.kt index f42b49e3..b9ef5faf 100644 --- a/projectional-editor-ssr-client-lib/src/jsMain/kotlin/org/modelix/editor/ssr/client/ClientSideEditor.kt +++ b/projectional-editor-ssr-client-lib/src/jsMain/kotlin/org/modelix/editor/ssr/client/ClientSideEditor.kt @@ -6,187 +6,29 @@ import kotlinx.html.dom.append import kotlinx.html.dom.create import kotlinx.html.id import kotlinx.html.js.div -import org.modelix.editor.JSKeyboardEventType -import org.modelix.editor.JSMouseEventType -import org.modelix.editor.convert -import org.modelix.editor.getAbsoluteBounds -import org.modelix.editor.getAbsoluteInnerBounds -import org.modelix.editor.relativeTo -import org.modelix.editor.ssr.common.DomTreeUpdate -import org.modelix.editor.ssr.common.ElementReference -import org.modelix.editor.ssr.common.HTMLElementBoundsUpdate -import org.modelix.editor.ssr.common.HTMLElementUpdateData -import org.modelix.editor.ssr.common.IElementUpdateData -import org.modelix.editor.ssr.common.INodeUpdateData -import org.modelix.editor.ssr.common.MessageFromClient -import org.modelix.editor.ssr.common.TextNodeUpdateData -import org.modelix.model.api.INodeReference -import org.w3c.dom.Element +import org.modelix.editor.JsEditorComponent import org.w3c.dom.HTMLDivElement -import org.w3c.dom.Node -import org.w3c.dom.asList -import org.w3c.dom.get private val LOG = KotlinLogging.logger {} class ClientSideEditor( - val editorId: String, - rootNodeReference: INodeReference, + val editorElementId: String, existingContainerElement: HTMLDivElement? = null, - val sendMessage: (MessageFromClient) -> Unit, + val editorComponent: JsEditorComponent, ) { - val containerElement: HTMLDivElement = (existingContainerElement ?: document.create.div("modelix-text-editor")).also { - it.tabIndex = -1 // allows setting the keyboard focus - } - val editorElement: HTMLDivElement = containerElement.append.div { - id = editorId - +"Loading ..." - } - private val elementMap: MutableMap = HashMap().also { it[editorId] = editorElement } - private val pendingUpdates: MutableMap = HashMap() - private val possiblyDetachedElements: MutableSet = HashSet() - private var boundsOnServer: Map = emptyMap() - - init { - containerElement.onclick = { event -> - MessageFromClient( - editorId = editorId, - mouseEvent = event.convert(JSMouseEventType.CLICK, containerElement), - ).withBounds().send() + val containerElement: HTMLDivElement = + (existingContainerElement ?: document.create.div("modelix-text-editor")).also { + it.tabIndex = -1 // allows setting the keyboard focus } - containerElement.onkeydown = { event -> - MessageFromClient( - editorId = editorId, - keyboardEvent = event.convert(JSKeyboardEventType.KEYDOWN), - ).withBounds().send() - event.preventDefault() + val editorElement: HTMLDivElement = + containerElement.append.div { + id = editorElementId + +"Loading ..." } - containerElement.onkeyup = { event -> - MessageFromClient( - editorId = editorId, - keyboardEvent = event.convert(JSKeyboardEventType.KEYUP), - ).withBounds().send() - event.preventDefault() - } - } - - private fun MessageFromClient.send() { - sendMessage(this) - } - - fun computeBoundsUpdate(): Map? { - // TODO performance - val origin = containerElement.getAbsoluteBounds() - val latest = elementMap.entries.associate { - val outer = it.value.getAbsoluteBounds().relativeTo(origin) - val inner = it.value.getAbsoluteInnerBounds().relativeTo(origin).takeIf { it != outer } - it.key to HTMLElementBoundsUpdate(outer = outer, inner = inner) - } - val changesOnly = latest.filter { boundsOnServer[it.key] != it.value } - boundsOnServer = latest - return changesOnly.takeIf { it.isNotEmpty() } - } - - fun sendBoundsUpdate() { - val update = computeBoundsUpdate() ?: return - MessageFromClient(editorId = editorId, boundUpdates = update).send() - } - - private fun MessageFromClient.withBounds(): MessageFromClient { - require(boundUpdates == null) { "Already contains bound update data" } - return copy(boundUpdates = computeBoundsUpdate()) - } fun dispose() { + // TODO call this method somewhere containerElement.remove() - MessageFromClient(editorId = editorId, dispose = true).send() - } - - fun applyUpdate(update: DomTreeUpdate) { - if (update.elements.isEmpty()) return - LOG.trace { "($editorId) Updating DOM" } - // this map allows updating nodes in a different order to resolve references during syncChildren - pendingUpdates.putAll( - update.elements.associateBy { - requireNotNull(it.id) { "Elements in DomTreeUpdate.elements are expected to have an ID" } - }, - ) - - for (elementUpdate in update.elements) { - if (!pendingUpdates.containsKey(elementUpdate.id)) continue - updateNode(elementUpdate) - } - - possiblyDetachedElements.forEach { id -> - val element = elementMap[id] ?: return@forEach - if (element.parentNode == null) { -// elementMap.remove(id) - } - } - possiblyDetachedElements.clear() - - sendBoundsUpdate() - } - - private fun updateNode(data: INodeUpdateData): Node { - return when (data) { - is TextNodeUpdateData -> document.createTextNode(data.text) - is HTMLElementUpdateData -> { - pendingUpdates.remove(data.id) - val element = elementMap[data.id]?.takeIf { it.tagName.lowercase() == data.tagName.lowercase() } - ?: document.createElement(data.tagName).also { element -> - data.id?.let { elementId -> - element.id = elementId - elementMap[elementId] = element - } - syncAttributes(element, data) - syncChildren(element, data) - } - syncAttributes(element, data) - syncChildren(element, data) - element - } - is ElementReference -> { - pendingUpdates[data.id]?.let { updateNode(it) } - ?: elementMap[data.id] - ?: throw NoSuchElementException("$editorId: element not found: ${data.id}") - } - } - } - - private fun syncAttributes(element: Element, updateData: HTMLElementUpdateData) { - val attributesToRemove = element.getAttributeNames().toMutableSet() - for (attributeData in updateData.attributes) { - if (element.getAttribute(attributeData.key) != attributeData.value) { - element.setAttribute(attributeData.key, attributeData.value) - } - attributesToRemove.remove(attributeData.key) - } - updateData.id?.let { id -> - element.setAttribute("id", id) - attributesToRemove.remove("id") - } - attributesToRemove.forEach(element::removeAttribute) - } - - private fun syncChildren(element: Element, updateData: HTMLElementUpdateData) { - val existingChildren: () -> List = { element.childNodes.asList() } - val expectedChildren: List = updateData.children.map { updateNode(it) } - if (existingChildren() == expectedChildren) return - - (existingChildren() - expectedChildren.toSet()).forEach { - element.removeChild(it) - (it as? Element)?.id?.let { possiblyDetachedElements.add(it) } - } - if (existingChildren() == expectedChildren) return - - for ((index, expected) in expectedChildren.withIndex()) { - val existing = if (index < element.childNodes.length) element.childNodes[index] else null - if (existing == null) { - element.appendChild(expected) - } else if (existing != expected) { - element.insertBefore(expected, existing) - } - } + editorComponent.dispose() } } diff --git a/projectional-editor-ssr-client-lib/src/jsMain/kotlin/org/modelix/editor/ssr/client/ClientSideEditors.kt b/projectional-editor-ssr-client-lib/src/jsMain/kotlin/org/modelix/editor/ssr/client/ClientSideEditors.kt index f64cc4cd..f80505a0 100644 --- a/projectional-editor-ssr-client-lib/src/jsMain/kotlin/org/modelix/editor/ssr/client/ClientSideEditors.kt +++ b/projectional-editor-ssr-client-lib/src/jsMain/kotlin/org/modelix/editor/ssr/client/ClientSideEditors.kt @@ -1,37 +1,40 @@ package org.modelix.editor.ssr.client import io.github.oshai.kotlinlogging.KotlinLogging -import org.modelix.editor.ssr.common.MessageFromClient -import org.modelix.editor.ssr.common.MessageFromServer +import kotlinx.coroutines.CoroutineScope +import org.modelix.editor.JsEditorComponent +import org.modelix.editor.text.shared.TextEditorService import org.modelix.model.api.INodeReference import org.w3c.dom.HTMLDivElement import org.w3c.dom.HTMLElement private val LOG = KotlinLogging.logger { } -class ClientSideEditors(val sendMessage: (MessageFromClient) -> Unit) { - +class ClientSideEditors( + val service: TextEditorService, + val coroutineScope: CoroutineScope, +) { private val editors: MutableMap = HashMap() private var nextEditorId: Long = 1000 - fun processMessage(msg: MessageFromServer) { - msg.error?.let { LOG.error { it } } - msg.editorId?.let { editorId -> - val editor = checkNotNull(editors[editorId]) { "Unknown editor ID: $editorId" } - msg.domUpdate?.let { editor.applyUpdate(it) } - } - } + fun createEditor( + rootNodeReference: INodeReference, + existingContainerElement: HTMLDivElement? = null, + ): HTMLElement { + val editorElementId = "modelix-editor-" + nextEditorId++.toString() + LOG.trace { "Trying to create new editor $editorElementId" } + + val editorComponent = JsEditorComponent(service) + editorComponent.openNode(rootNodeReference) - fun createEditor(rootNodeReference: INodeReference, existingContainerElement: HTMLDivElement? = null): HTMLElement { - val editorId = "modelix-editor-" + nextEditorId++.toString() - LOG.trace { "Trying to create new editor $editorId" } - val editorSession = ClientSideEditor(editorId, rootNodeReference, existingContainerElement, sendMessage) - LOG.info { "Creating editor ${editorSession.editorId}" } - editors[editorSession.editorId] = editorSession - MessageFromClient( - editorId = editorSession.editorId, - rootNodeReference = rootNodeReference.serialize(), - ).let(sendMessage) + val editorSession = + ClientSideEditor( + editorElementId = editorElementId, + existingContainerElement = existingContainerElement, + editorComponent = editorComponent + ) + LOG.info { "Creating editor ${editorSession.editorElementId}" } + editors[editorSession.editorElementId] = editorSession return editorSession.containerElement } } diff --git a/projectional-editor-ssr-client-lib/src/jsMain/kotlin/org/modelix/editor/ssr/client/ClientSideEditorsAPI.kt b/projectional-editor-ssr-client-lib/src/jsMain/kotlin/org/modelix/editor/ssr/client/ClientSideEditorsAPI.kt index fe679b15..5b0997e6 100644 --- a/projectional-editor-ssr-client-lib/src/jsMain/kotlin/org/modelix/editor/ssr/client/ClientSideEditorsAPI.kt +++ b/projectional-editor-ssr-client-lib/src/jsMain/kotlin/org/modelix/editor/ssr/client/ClientSideEditorsAPI.kt @@ -16,7 +16,6 @@ import org.w3c.dom.HTMLElement @OptIn(ExperimentalJsExport::class) @JsExport object ClientSideEditorsAPI { - private lateinit var client: ModelixSSRClient fun init() { @@ -25,25 +24,29 @@ object ClientSideEditorsAPI { println("ClientSideEditorsAPI.init()") KotlinLoggingConfiguration.logLevel = Level.TRACE val currentUrl = document.location!! - val wsUrl = URLBuilder().apply { - protocol = if (currentUrl.protocol.lowercase().trimEnd(':') == "http") URLProtocol.WS else URLProtocol.WSS - host = currentUrl.hostname - port = 43593 // currentUrl.port.toIntOrNull() ?: io.ktor.http.DEFAULT_PORT - pathSegments = listOf("ws") - }.buildString() + val wsUrl = + URLBuilder() + .apply { + protocol = if (currentUrl.protocol.lowercase().trimEnd(':') == "http") URLProtocol.WS else URLProtocol.WSS + host = currentUrl.hostname + port = 43593 // currentUrl.port.toIntOrNull() ?: io.ktor.http.DEFAULT_PORT + pathSegments = listOf("rpc") + }.buildString() console.log("Text editor URL: $wsUrl") initWithUrl(wsUrl) } fun initWithUrl(url: String) { println("ClientSideEditorsAPI.initWithUrl($url)") - val httpClient = HttpClient(Js) { - install(WebSockets) - } - client = ModelixSSRClient(httpClient, url).also { it.connect { } } + val httpClient = + HttpClient(Js) { + install(WebSockets) + } + client = ModelixSSRClient(httpClient, url) } - fun createEditor(rootNodeReference: String, existingContainerElement: HTMLDivElement? = null): HTMLElement { - return client.createEditor(NodeReference(rootNodeReference), existingContainerElement) - } + fun createEditor( + rootNodeReference: String, + existingContainerElement: HTMLDivElement? = null, + ): HTMLElement = client.createEditor(NodeReference(rootNodeReference), existingContainerElement) } diff --git a/projectional-editor-ssr-client-lib/src/jsMain/kotlin/org/modelix/editor/ssr/client/ModelixSSRClient.kt b/projectional-editor-ssr-client-lib/src/jsMain/kotlin/org/modelix/editor/ssr/client/ModelixSSRClient.kt index 0be5d041..b7d54145 100644 --- a/projectional-editor-ssr-client-lib/src/jsMain/kotlin/org/modelix/editor/ssr/client/ModelixSSRClient.kt +++ b/projectional-editor-ssr-client-lib/src/jsMain/kotlin/org/modelix/editor/ssr/client/ModelixSSRClient.kt @@ -3,60 +3,46 @@ package org.modelix.editor.ssr.client import io.github.oshai.kotlinlogging.KLogger import io.github.oshai.kotlinlogging.KotlinLogging import io.ktor.client.HttpClient -import io.ktor.client.plugins.websocket.DefaultClientWebSocketSession -import io.ktor.client.plugins.websocket.webSocket -import io.ktor.websocket.Frame -import io.ktor.websocket.readText import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.cancel import kotlinx.coroutines.launch -import org.modelix.editor.ssr.common.MessageFromClient -import org.modelix.editor.ssr.common.MessageFromServer +import kotlinx.rpc.krpc.ktor.client.rpc +import kotlinx.rpc.krpc.ktor.client.rpcConfig +import kotlinx.rpc.krpc.serialization.json.json +import kotlinx.rpc.withService +import org.modelix.editor.text.shared.TextEditorService import org.modelix.model.api.INodeReference import org.w3c.dom.HTMLDivElement import org.w3c.dom.HTMLElement private val LOG = KotlinLogging.logger { } -class ModelixSSRClient(private val httpClient: HttpClient, private val url: String) { - +class ModelixSSRClient( + private val httpClient: HttpClient, + private val url: String, +) { private val coroutineScope = CoroutineScope(Dispatchers.Default) - private var websocketSession: DefaultClientWebSocketSession? = null - private val editors = ClientSideEditors(::sendMessage) - - fun sendMessage(msg: MessageFromClient) { - websocketSession?.outgoing?.trySend(Frame.Text(msg.toJson())) - } + private val rpcClient = + httpClient.rpc(urlString = url) { + rpcConfig { + serialization { + json() + } + } + } + private val editors = ClientSideEditors(rpcClient.withService(), coroutineScope) fun dispose() { coroutineScope.cancel("Disposed") } - fun createEditor(rootNodeReference: INodeReference, existingContainerElement: HTMLDivElement? = null): HTMLElement { - return editors.createEditor(rootNodeReference, existingContainerElement) - } - - fun connect(callback: suspend () -> Unit = {}) { - coroutineScope.launchLogging { - httpClient.webSocket(urlString = url) { - websocketSession = this - callback() - for (wsMessage in incoming) { - try { - when (wsMessage) { - is Frame.Text -> editors.processMessage(MessageFromServer.fromJson(wsMessage.readText())) - else -> {} - } - } catch (ex: Throwable) { - LOG.error(ex) { "Failed to process message: $wsMessage" } - } - } - } - } - } + fun createEditor( + rootNodeReference: INodeReference, + existingContainerElement: HTMLDivElement? = null, + ): HTMLElement = editors.createEditor(rootNodeReference, existingContainerElement) } inline fun KLogger.logExceptions(body: () -> R): R { @@ -68,8 +54,7 @@ inline fun KLogger.logExceptions(body: () -> R): R { } } -fun CoroutineScope.launchLogging(body: suspend () -> Unit): Job { - return launch { +fun CoroutineScope.launchLogging(body: suspend CoroutineScope.() -> Unit): Job = + launch { LOG.logExceptions { body() } } -} diff --git a/projectional-editor-ssr-client/src/jsMain/kotlin/App.kt b/projectional-editor-ssr-client/src/jsMain/kotlin/App.kt index 066a5fc5..249d4b95 100644 --- a/projectional-editor-ssr-client/src/jsMain/kotlin/App.kt +++ b/projectional-editor-ssr-client/src/jsMain/kotlin/App.kt @@ -19,18 +19,21 @@ fun main() { KotlinLoggingConfiguration.logLevel = Level.TRACE LOG.info { "App started" } - val httpClient = HttpClient() { - install(WebSockets) - } + val httpClient = + HttpClient { + install(WebSockets) + } LOG.trace { "Coroutine in GlobalScope started" } val currentUrl = document.location!! - val wsUrl = URLBuilder().apply { - protocol = if (currentUrl.protocol.lowercase().trimEnd(':') == "http") URLProtocol.WS else URLProtocol.WSS - host = currentUrl.hostname - port = currentUrl.port.toIntOrNull() ?: DEFAULT_PORT - pathSegments = listOf("ws") - }.buildString() + val wsUrl = + URLBuilder() + .apply { + protocol = if (currentUrl.protocol.lowercase().trimEnd(':') == "http") URLProtocol.WS else URLProtocol.WSS + host = currentUrl.hostname + port = currentUrl.port.toIntOrNull() ?: DEFAULT_PORT + pathSegments = listOf("ws") + }.buildString() val client = ModelixSSRClient(httpClient, wsUrl) client.connect { LOG.trace { "Connected" } diff --git a/projectional-editor-ssr-client/src/jsMain/kotlin/org/modelix/editor/ssr/client/ModelixSSRClient.kt b/projectional-editor-ssr-client/src/jsMain/kotlin/org/modelix/editor/ssr/client/ModelixSSRClient.kt index 9f8f99cc..8e13e098 100644 --- a/projectional-editor-ssr-client/src/jsMain/kotlin/org/modelix/editor/ssr/client/ModelixSSRClient.kt +++ b/projectional-editor-ssr-client/src/jsMain/kotlin/org/modelix/editor/ssr/client/ModelixSSRClient.kt @@ -39,8 +39,10 @@ import org.w3c.dom.Node import org.w3c.dom.asList import org.w3c.dom.get -class ModelixSSRClient(private val httpClient: HttpClient, private val url: String) { - +class ModelixSSRClient( + private val httpClient: HttpClient, + private val url: String, +) { companion object { private val LOG = KotlinLogging.logger {} } @@ -68,7 +70,10 @@ class ModelixSSRClient(private val httpClient: HttpClient, private val url: Stri for (wsMessage in incoming) { try { when (wsMessage) { - is Frame.Text -> processMessage(MessageFromServer.fromJson(wsMessage.readText())) + is Frame.Text -> { + processMessage(MessageFromServer.fromJson(wsMessage.readText())) + } + else -> {} } } catch (ex: Throwable) { @@ -79,7 +84,10 @@ class ModelixSSRClient(private val httpClient: HttpClient, private val url: Stri } } - fun createEditor(rootNodeReference: INodeReference, existingContainerElement: HTMLDivElement? = null): HTMLElement { + fun createEditor( + rootNodeReference: INodeReference, + existingContainerElement: HTMLDivElement? = null, + ): HTMLElement { val editorId = "modelix-editor-" + nextEditorId++.toString() LOG.trace { "Trying to create new editor $editorId" } val ws = checkNotNull(websocketSession) { "Not connected" } @@ -109,14 +117,20 @@ class ModelixSSRClient(private val httpClient: HttpClient, private val url: Stri } } - private inner class EditorSession(val editorId: String, rootNodeReference: INodeReference, existingContainerElement: HTMLDivElement? = null) { - val containerElement: HTMLDivElement = (existingContainerElement ?: document.create.div("modelix-text-editor")).also { - it.tabIndex = -1 // allows setting the keyboard focus - } - val editorElement: HTMLDivElement = containerElement.append.div { - id = editorId - +"Loading ..." - } + private inner class EditorSession( + val editorId: String, + rootNodeReference: INodeReference, + existingContainerElement: HTMLDivElement? = null, + ) { + val containerElement: HTMLDivElement = + (existingContainerElement ?: document.create.div("modelix-text-editor")).also { + it.tabIndex = -1 // allows setting the keyboard focus + } + val editorElement: HTMLDivElement = + containerElement.append.div { + id = editorId + +"Loading ..." + } private val elementMap: MutableMap = HashMap().also { it[editorId] = editorElement } private val pendingUpdates: MutableMap = HashMap() private val possiblyDetachedElements: MutableSet = HashSet() @@ -148,11 +162,16 @@ class ModelixSSRClient(private val httpClient: HttpClient, private val url: Stri fun computeBoundsUpdate(): Map? { // TODO performance val origin = containerElement.getAbsoluteBounds() - val latest = elementMap.entries.associate { - val outer = it.value.getAbsoluteBounds().relativeTo(origin) - val inner = it.value.getAbsoluteInnerBounds().relativeTo(origin).takeIf { it != outer } - it.key to HTMLElementBoundsUpdate(outer = outer, inner = inner) - } + val latest = + elementMap.entries.associate { + val outer = it.value.getAbsoluteBounds().relativeTo(origin) + val inner = + it.value + .getAbsoluteInnerBounds() + .relativeTo(origin) + .takeIf { it != outer } + it.key to HTMLElementBoundsUpdate(outer = outer, inner = inner) + } val changesOnly = latest.filter { boundsOnServer[it.key] != it.value } boundsOnServer = latest return changesOnly.takeIf { it.isNotEmpty() } @@ -199,33 +218,40 @@ class ModelixSSRClient(private val httpClient: HttpClient, private val url: Stri sendBoundsUpdate() } - private fun updateNode(data: INodeUpdateData): Node { - return when (data) { - is TextNodeUpdateData -> document.createTextNode(data.text) + private fun updateNode(data: INodeUpdateData): Node = + when (data) { + is TextNodeUpdateData -> { + document.createTextNode(data.text) + } + is HTMLElementUpdateData -> { pendingUpdates.remove(data.id) - val element = elementMap[data.id]?.takeIf { it.tagName.lowercase() == data.tagName.lowercase() } - ?: document.createElement(data.tagName).also { element -> - data.id?.let { elementId -> - element.id = elementId - elementMap[elementId] = element + val element = + elementMap[data.id]?.takeIf { it.tagName.lowercase() == data.tagName.lowercase() } + ?: document.createElement(data.tagName).also { element -> + data.id?.let { elementId -> + element.id = elementId + elementMap[elementId] = element + } + syncAttributes(element, data) + syncChildren(element, data) } - syncAttributes(element, data) - syncChildren(element, data) - } syncAttributes(element, data) syncChildren(element, data) element } + is ElementReference -> { pendingUpdates[data.id]?.let { updateNode(it) } ?: elementMap[data.id] ?: throw NoSuchElementException("$editorId: element not found: ${data.id}") } } - } - private fun syncAttributes(element: Element, updateData: HTMLElementUpdateData) { + private fun syncAttributes( + element: Element, + updateData: HTMLElementUpdateData, + ) { val attributesToRemove = element.getAttributeNames().toMutableSet() for (attributeData in updateData.attributes) { if (element.getAttribute(attributeData.key) != attributeData.value) { @@ -240,7 +266,10 @@ class ModelixSSRClient(private val httpClient: HttpClient, private val url: Stri attributesToRemove.forEach(element::removeAttribute) } - private fun syncChildren(element: Element, updateData: HTMLElementUpdateData) { + private fun syncChildren( + element: Element, + updateData: HTMLElementUpdateData, + ) { val existingChildren: () -> List = { element.childNodes.asList() } val expectedChildren: List = updateData.children.map { updateNode(it) } if (existingChildren() == expectedChildren) return diff --git a/projectional-editor-ssr-common/build.gradle.kts b/projectional-editor-ssr-common/build.gradle.kts index 05b5d32d..6112ed05 100644 --- a/projectional-editor-ssr-common/build.gradle.kts +++ b/projectional-editor-ssr-common/build.gradle.kts @@ -1,6 +1,7 @@ plugins { kotlin("multiplatform") kotlin("plugin.serialization") + alias(libs.plugins.kotlin.rpc) `maven-publish` } @@ -13,33 +14,36 @@ kotlin { } sourceSets { - val commonMain by getting { + commonMain { dependencies { implementation(coreLibs.kotlin.serialization.json) api(project(":projectional-editor")) api(libs.modelix.model.api) + api(coreLibs.kotlin.serialization.json) + api(coreLibs.kotlin.coroutines.core) + api(libs.kotlinx.rpc.core) } } - val commonTest by getting { + commonTest { dependencies { implementation(kotlin("test")) } } - val jvmMain by getting { + jvmMain { dependencies { } } - val jvmTest by getting { + jvmTest { dependencies { implementation(kotlin("test")) implementation(kotlin("test-junit")) } } - val jsMain by getting { + jsMain { dependencies { } } - val jsTest by getting { + jsTest { dependencies { implementation(kotlin("test")) } diff --git a/projectional-editor-ssr-common/src/commonMain/kotlin/org/modelix/editor/ssr/common/MessageFromClient.kt b/projectional-editor-ssr-common/src/commonMain/kotlin/org/modelix/editor/ssr/common/MessageFromClient.kt index 788b03eb..beeae66d 100644 --- a/projectional-editor-ssr-common/src/commonMain/kotlin/org/modelix/editor/ssr/common/MessageFromClient.kt +++ b/projectional-editor-ssr-common/src/commonMain/kotlin/org/modelix/editor/ssr/common/MessageFromClient.kt @@ -14,26 +14,21 @@ data class MessageFromClient( * when creating a new editor component. */ val editorId: String? = null, - /** * The node to open in the editor. The first message containing a new `editorId` will instantiate * an editor component on the server side and should always specify the root node. */ val rootNodeReference: String? = null, - /** * When the editor component is not used anymore, the client can set this flag to free resources on the server and * stop getting updates for it. */ val dispose: Boolean = false, - /** * The user pressed a key on the client side that should be processed by the editor component. */ val keyboardEvent: JSKeyboardEvent? = null, - val mouseEvent: JSMouseEvent? = null, - val boundUpdates: Map? = null, ) { fun toJson() = Json.encodeToString(this) diff --git a/projectional-editor-ssr-common/src/commonMain/kotlin/org/modelix/editor/ssr/common/MessageFromServer.kt b/projectional-editor-ssr-common/src/commonMain/kotlin/org/modelix/editor/ssr/common/MessageFromServer.kt index 8123dc63..2b47ee20 100644 --- a/projectional-editor-ssr-common/src/commonMain/kotlin/org/modelix/editor/ssr/common/MessageFromServer.kt +++ b/projectional-editor-ssr-common/src/commonMain/kotlin/org/modelix/editor/ssr/common/MessageFromServer.kt @@ -13,18 +13,17 @@ class MessageFromServer( * @see MessageFromClient.editorId */ val editorId: String? = null, - /** * The server is responsible for computing the resulting DOM tree and sending incremental updates to the client. */ val domUpdate: DomTreeUpdate? = null, - /** * An exception was thrown on the server side. */ val error: String? = null, ) { fun toJson() = Json.encodeToString(this) + companion object { fun fromJson(msg: String) = Json.decodeFromString(msg) } @@ -45,7 +44,9 @@ sealed interface IElementUpdateData : INodeUpdateData { @Serializable @SerialName("Text") -data class TextNodeUpdateData(val text: String) : INodeUpdateData +data class TextNodeUpdateData( + val text: String, +) : INodeUpdateData @Serializable @SerialName("HTMLElement") @@ -58,4 +59,6 @@ data class HTMLElementUpdateData( @Serializable @SerialName("ref") -data class ElementReference(override val id: String) : IElementUpdateData +data class ElementReference( + override val id: String, +) : IElementUpdateData diff --git a/projectional-editor-ssr-mps-languages/build.gradle.kts b/projectional-editor-ssr-mps-languages/build.gradle.kts index eaee1545..24751b15 100644 --- a/projectional-editor-ssr-mps-languages/build.gradle.kts +++ b/projectional-editor-ssr-mps-languages/build.gradle.kts @@ -24,6 +24,7 @@ dependencies { testImplementation(project(":projectional-editor"), excludeMPSLibraries) testImplementation(libs.modelix.mps.model.adapters, excludeMPSLibraries) testImplementation(libs.playwright, excludeMPSLibraries) + testImplementation(coreLibs.kotlin.coroutines.test, excludeMPSLibraries) modelAdaptersPlugin(libs.modelix.mps.model.adapters.plugin) } @@ -34,7 +35,8 @@ intellij { project(":projectional-editor-ssr-mps"), project(":editor-common-mps"), project(":react-ssr-mps"), - ) + listOf( + ) + + listOf( // "Git4Idea", // "Subversion", // "com.intellij.copyright", @@ -48,7 +50,7 @@ intellij { // "jetbrains.mps.build", // "jetbrains.mps.build.ui", // "jetbrains.mps.console", - "jetbrains.mps.core", + "jetbrains.mps.core", // "jetbrains.mps.debugger.api", // "jetbrains.mps.debugger.java", // "jetbrains.mps.editor.contextActions", @@ -68,7 +70,7 @@ intellij { // "jetbrains.mps.ide.migration.workbench", // "jetbrains.mps.ide.modelchecker", // "jetbrains.mps.ide.mpsmigration", - "jetbrains.mps.kotlin", + "jetbrains.mps.kotlin", // "jetbrains.mps.navbar", // "jetbrains.mps.rcp", // "jetbrains.mps.samples", @@ -79,11 +81,10 @@ intellij { // "org.intellij.plugins.markdown", // "org.jetbrains.plugins.github", // "org.jetbrains.settingsRepository", - // "com.intellij", // "com.jetbrains.sh", // "org.jetbrains.plugins.terminal", - ) + ) } tasks { 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 index f2e5af01..eabb46e3 100644 --- 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 @@ -1,10 +1,13 @@ package org.modelix.editor.ssr.mps +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking import org.modelix.editor.CaretSelection import org.modelix.editor.CodeCompletionParameters import org.modelix.editor.CommonCellProperties -import org.modelix.editor.EditorComponent import org.modelix.editor.EditorEngine +import org.modelix.editor.FrontendEditorComponent import org.modelix.editor.ICodeCompletionAction import org.modelix.editor.ICodeCompletionActionProvider import org.modelix.editor.JSKeyboardEvent @@ -19,23 +22,30 @@ import org.modelix.editor.flattenApplicableActions import org.modelix.editor.getCompletionPattern import org.modelix.editor.getMaxCaretPos import org.modelix.editor.getSubstituteActions -import org.modelix.editor.getVisibleText import org.modelix.editor.lastLeaf import org.modelix.editor.layoutable +import org.modelix.editor.text.backend.TextEditorServiceImpl +import org.modelix.editor.text.frontend.getVisibleText +import org.modelix.editor.text.shared.celltree.ICellTree import org.modelix.incremental.IncrementalEngine import org.modelix.model.api.INode +import org.modelix.model.mpsadapters.MPSArea import org.modelix.model.mpsadapters.MPSWritableNode /** * Test editor for MPS baseLanguage ClassConcept */ -@Suppress("ktlint:standard:wrapping", "ktlint:standard:trailing-comma-on-call-site") +@Suppress("ktlint:standard:wrapping", "ktlint:standard:trailing-comma-on-call-site", "ktlint:standard:function-naming") class BaseLanguageTests : TestBase("SimpleProject") { - lateinit var editor: EditorComponent + lateinit var editor: FrontendEditorComponent + lateinit var service: TextEditorServiceImpl lateinit var mpsIntegration: EditorIntegrationForMPS lateinit var editorEngine: EditorEngine lateinit var incrementalEngine: IncrementalEngine lateinit var classNode: MPSWritableNode + lateinit var coroutineScope: CoroutineScope + + private fun getBackend() = service.getAllEditorBackends().single() override fun setUp() { super.setUp() @@ -50,7 +60,14 @@ class BaseLanguageTests : TestBase("SimpleProject") { editorEngine = EditorEngine(incrementalEngine) mpsIntegration = EditorIntegrationForMPS(editorEngine) mpsIntegration.init(mpsProject.repository) - editor = editorEngine.editNode(classNode.asLegacyNode()) + coroutineScope = CoroutineScope(Dispatchers.Default) + service = TextEditorServiceImpl(editorEngine, MPSArea(mpsProject.repository).asModel(), coroutineScope) + editor = FrontendEditorComponent(service) + runBlocking { + editor.openNode(classNode.getNodeReference()).await() + editor.flush() + println(editor.getRootCell().layout.toString()) + } } override fun tearDown() { @@ -61,12 +78,13 @@ class BaseLanguageTests : TestBase("SimpleProject") { super.tearDown() } + private fun ICellTree.Cell.backend() = service.getEditorBackend(editor.editorId).tree.getCell(getId()) + fun assertFinalEditorText(expected: String) { assertEditorText(expected) // Reset all editor state to ensure the typed text triggered a model transformation. - editor.state.reset() - editor.update() + runBlocking { editor.resetState() } assertEditorText(expected) } @@ -80,24 +98,27 @@ class BaseLanguageTests : TestBase("SimpleProject") { assertEquals(expected.trimIndent(), editor.getRootCell().layout.toString()) } - fun placeCaretAtEnd(node: INode) { + suspend fun placeCaretAtEnd(node: INode) { val cell = editor.resolveCell(NodeCellReference(node.reference)).first() val lastLeafCell = cell.lastLeaf() editor.changeSelection(CaretSelection(lastLeafCell.layoutable()!!, lastLeafCell.getMaxCaretPos())) } - fun placeCaretIntoCellWithText(text: String, position: Int = -1) { + fun placeCaretIntoCellWithText( + text: String, + position: Int = -1, + ) { val cell = editor.getRootCell().descendantsAndSelf().first { it.getVisibleText() == text } - editor.changeSelection(CaretSelection(cell.layoutable()!!, if (position == -1) cell.getMaxCaretPos() else position)) + editor.doChangeSelection(CaretSelection(cell.layoutable()!!, if (position == -1) cell.getMaxCaretPos() else position)) } - fun pressEnter() = pressKey(KnownKeys.Enter) + suspend fun pressEnter() = pressKey(KnownKeys.Enter) - fun pressKey(key: KnownKeys) { + suspend fun pressKey(key: KnownKeys) { editor.processKeyEvent(JSKeyboardEvent(JSKeyboardEventType.KEYDOWN, key)) } - fun typeText(text: CharSequence) { + suspend fun typeText(text: CharSequence) { for (c in text) { editor.processKeyEvent( JSKeyboardEvent( @@ -110,19 +131,23 @@ class BaseLanguageTests : TestBase("SimpleProject") { } } - fun getCodeCompletionEntries(pattern: String): List { - return readAction { - val actionProviders: Sequence = (editor.getSelection() as CaretSelection).layoutable.cell.getSubstituteActions() - val actions = actionProviders.flatMap { it.flattenApplicableActions(CodeCompletionParameters(editor, pattern)) }.toList() - val matchingActions = actions.filter { - val matchingText = it.getCompletionPattern() - matchingText.isNotEmpty() && matchingText.startsWith(pattern) - } + fun getCodeCompletionEntries(pattern: String): List = + readAction { + val selection = editor.getSelection() as CaretSelection + val actionProviders: Sequence = + selection.layoutable.cell + .backend() + .getSubstituteActions() + val actions = actionProviders.flatMap { it.flattenApplicableActions(CodeCompletionParameters(getBackend(), pattern)) }.toList() + val matchingActions = + actions.filter { + val matchingText = it.getCompletionPattern() + matchingText.isNotEmpty() && matchingText.startsWith(pattern) + } val shadowedActions = matchingActions.applyShadowing() val sortedActions = shadowedActions.sortedBy { it.getCompletionPattern().lowercase() } sortedActions } - } fun `test initial editor`() { assertFinalEditorText(""" @@ -134,11 +159,12 @@ class BaseLanguageTests : TestBase("SimpleProject") { """) } - fun `test inserting new line into class`() { - val lastMember = readAction { classNode.getAllChildren().last { it.getContainmentLink().getSimpleName() == "member" } } - placeCaretAtEnd(lastMember.asLegacyNode()) - pressEnter() - assertFinalEditorText(""" + fun `test inserting new line into class`() = + kotlinx.coroutines.test.runTest { + val lastMember = readAction { classNode.getAllChildren().last { it.getContainmentLink().getSimpleName() == "member" } } + placeCaretAtEnd(lastMember.asLegacyNode()) + pressEnter() + assertFinalEditorText(""" public class Class1 { public void method1() { @@ -146,87 +172,92 @@ class BaseLanguageTests : TestBase("SimpleProject") { } """) - } + } - fun `test creating LocalVariableDeclarationStatement by typing a type`() { - placeCaretIntoCellWithText("") - val actions = getCodeCompletionEntries("int") - assertEquals( - listOf( - "int ; | LocalVariableDeclarationStatement[LocalVariableDeclaration[IntegerType]]", - "int.class; | ExpressionStatement[PrimitiveClassExpression[IntegerType]]", - "int[].class; | ExpressionStatement[ArrayClassExpression[ArrayType[IntegerType]]]", - ), - actions.map { it.getCompletionPattern() + " | " + it.getDescription() }, - ) - typeText("int ") - assertFinalEditorText(""" + fun `test creating LocalVariableDeclarationStatement by typing a type`() = + kotlinx.coroutines.test.runTest { + placeCaretIntoCellWithText("") + val actions = getCodeCompletionEntries("int") + assertEquals( + listOf( + "int ; | LocalVariableDeclarationStatement[LocalVariableDeclaration[IntegerType]]", + "int.class; | ExpressionStatement[PrimitiveClassExpression[IntegerType]]", + "int[].class; | ExpressionStatement[ArrayClassExpression[ArrayType[IntegerType]]]", + ), + actions.map { it.getCompletionPattern() + " | " + it.getDescription() }, + ) + typeText("int ") + assertFinalEditorText(""" public class Class1 { public void method1() { int ; } } """) - } + } - fun `test naming LocalVariableDeclaration`() { - placeCaretIntoCellWithText("") - typeText("int ") - pressKey(KnownKeys.Tab) - typeText("abc") - assertFinalEditorText(""" + fun `test naming LocalVariableDeclaration`() = + kotlinx.coroutines.test.runTest { + placeCaretIntoCellWithText("") + typeText("int ") + pressKey(KnownKeys.Tab) + typeText("abc") + assertFinalEditorText(""" public class Class1 { public void method1() { int abc; } } """) - } + } - fun `test showing initializer of LocalVariableDeclaration using side transformation`() { - placeCaretIntoCellWithText("") - typeText("int ") - pressKey(KnownKeys.Tab) - typeText("abc") - typeText("=") - assertEditorText(""" + fun `test showing initializer of LocalVariableDeclaration using side transformation`() = + kotlinx.coroutines.test.runTest { + placeCaretIntoCellWithText("") + typeText("int ") + pressKey(KnownKeys.Tab) + typeText("abc") + typeText("=") + assertEditorText(""" public class Class1 { public void method1() { int abc = ; } } """) - assertCaretPosition("|") - } + assertCaretPosition("|") + } - fun `test showing initializer of LocalVariableDeclaration using TAB`() { - placeCaretIntoCellWithText("") - typeText("int ") - pressKey(KnownKeys.Tab) - typeText("abc") - pressKey(KnownKeys.Tab) - assertEditorText(""" + fun `test showing initializer of LocalVariableDeclaration using TAB`() = + kotlinx.coroutines.test.runTest { + placeCaretIntoCellWithText("") + typeText("int ") + pressKey(KnownKeys.Tab) + typeText("abc") + pressKey(KnownKeys.Tab) + assertEditorText(""" public class Class1 { public void method1() { int abc = ; } } """) - assertCaretPosition("|") - } - - fun `test previous optional is hidden when TABing to next`() { - placeCaretIntoCellWithText("") - typeText("int ") - pressKey(KnownKeys.Tab) - typeText("abc") - pressKey(KnownKeys.Enter) - typeText("int ") - pressKey(KnownKeys.Tab) - typeText("def") - placeCaretIntoCellWithText("abc") + assertCaretPosition("|") + } - assertEditorText(""" + fun `test previous optional is hidden when TABing to next`() = + kotlinx.coroutines.test.runTest { + placeCaretIntoCellWithText("") + typeText("int ") + pressKey(KnownKeys.Tab) + typeText("abc") + pressKey(KnownKeys.Enter) + typeText("int ") + pressKey(KnownKeys.Tab) + typeText("def") + placeCaretIntoCellWithText("abc") + + assertEditorText(""" public class Class1 { public void method1() { int abc; @@ -235,8 +266,8 @@ class BaseLanguageTests : TestBase("SimpleProject") { } """) - pressKey(KnownKeys.Tab) - assertEditorText(""" + pressKey(KnownKeys.Tab) + assertEditorText(""" public class Class1 { public void method1() { int abc = ; @@ -244,12 +275,12 @@ class BaseLanguageTests : TestBase("SimpleProject") { } } """) - assertCaretPosition("|") + assertCaretPosition("|") - pressKey(KnownKeys.Tab) - assertCaretPosition("|def") - pressKey(KnownKeys.Tab) - assertEditorText(""" + pressKey(KnownKeys.Tab) + assertCaretPosition("|def") + pressKey(KnownKeys.Tab) + assertEditorText(""" public class Class1 { public void method1() { int abc; @@ -257,92 +288,96 @@ class BaseLanguageTests : TestBase("SimpleProject") { } } """) - assertCaretPosition("|") - } + assertCaretPosition("|") + } - fun `test adding initializer to LocalVariableDeclaration`() { - placeCaretIntoCellWithText("") - typeText("int ") - pressKey(KnownKeys.Tab) - typeText("abc") - typeText("=") - typeText("10") - assertFinalEditorText(""" + fun `test adding initializer to LocalVariableDeclaration`() = + kotlinx.coroutines.test.runTest { + placeCaretIntoCellWithText("") + typeText("int ") + pressKey(KnownKeys.Tab) + typeText("abc") + typeText("=") + typeText("10") + assertFinalEditorText(""" public class Class1 { public void method1() { int abc = 10; } } """) - } + } - fun `test adding second parameter to InstanceMethodDeclaration by pressing ENTER`() { - placeCaretIntoCellWithText("") - typeText("int") - pressKey(KnownKeys.Tab) - typeText("p1") - pressKey(KnownKeys.Enter) - assertEditorText(""" + fun `test adding second parameter to InstanceMethodDeclaration by pressing ENTER`() = + kotlinx.coroutines.test.runTest { + placeCaretIntoCellWithText("") + typeText("int") + pressKey(KnownKeys.Tab) + typeText("p1") + pressKey(KnownKeys.Enter) + assertEditorText(""" public class Class1 { public void method1(int p1, ) { } } """) - typeText("int") - pressKey(KnownKeys.Tab) - typeText("p2") - assertFinalEditorText(""" + typeText("int") + pressKey(KnownKeys.Tab) + typeText("p2") + assertFinalEditorText(""" public class Class1 { public void method1(int p1, int p2) { } } """) - } + } - fun `test adding second parameter to InstanceMethodDeclaration by typing separator after last`() { - placeCaretIntoCellWithText("") - typeText("int") - pressKey(KnownKeys.Tab) - typeText("p1") - typeText(",") - typeText("int") - pressKey(KnownKeys.Tab) - typeText("p2") - assertFinalEditorText(""" + fun `test adding second parameter to InstanceMethodDeclaration by typing separator after last`() = + kotlinx.coroutines.test.runTest { + placeCaretIntoCellWithText("") + typeText("int") + pressKey(KnownKeys.Tab) + typeText("p1") + typeText(",") + typeText("int") + pressKey(KnownKeys.Tab) + typeText("p2") + assertFinalEditorText(""" public class Class1 { public void method1(int p1, int p2) { } } """) - assertCaretPosition("p2|") - } + assertCaretPosition("p2|") + } - fun `test adding second parameter to InstanceMethodDeclaration by typing separator after first`() { - placeCaretIntoCellWithText("") - typeText("int") - pressKey(KnownKeys.Tab) - typeText("p1") - typeText(",") - typeText("int") - pressKey(KnownKeys.Tab) - typeText("p2") - placeCaretIntoCellWithText("p1") - typeText(",") - typeText("int") - pressKey(KnownKeys.Tab) - typeText("p3") - assertFinalEditorText(""" + fun `test adding second parameter to InstanceMethodDeclaration by typing separator after first`() = + kotlinx.coroutines.test.runTest { + placeCaretIntoCellWithText("") + typeText("int") + pressKey(KnownKeys.Tab) + typeText("p1") + typeText(",") + typeText("int") + pressKey(KnownKeys.Tab) + typeText("p2") + placeCaretIntoCellWithText("p1") + typeText(",") + typeText("int") + pressKey(KnownKeys.Tab) + typeText("p3") + assertFinalEditorText(""" public class Class1 { public void method1(int p1, int p3, int p2) { } } """) - assertCaretPosition("p3|") - } + assertCaretPosition("p3|") + } /* fun `test adding second parameter to InstanceMethodDeclaration by typing separator before last`() { placeCaretIntoCellWithText("") @@ -367,123 +402,177 @@ class BaseLanguageTests : TestBase("SimpleProject") { """) }*/ - fun `test deleting parameter using BACKSPACE`() { - placeCaretIntoCellWithText("") - typeText("int") - pressKey(KnownKeys.Tab) - typeText("p1") - assertEditorText(""" + fun `test deleting parameter using BACKSPACE`() = + kotlinx.coroutines.test.runTest { + placeCaretIntoCellWithText("") + typeText("int") + pressKey(KnownKeys.Tab) + typeText("p1") + assertEditorText(""" public class Class1 { public void method1(int p1) { } } """) - assertCaretPosition("p1|") - pressKey(KnownKeys.ArrowLeft) - assertCaretPosition("p|1") - pressKey(KnownKeys.ArrowLeft) - assertCaretPosition("|p1") - pressKey(KnownKeys.Backspace) - assertFinalEditorText(""" + assertCaretPosition("p1|") + pressKey(KnownKeys.ArrowLeft) + assertCaretPosition("p|1") + pressKey(KnownKeys.ArrowLeft) + assertCaretPosition("|p1") + pressKey(KnownKeys.Backspace) + assertFinalEditorText(""" public class Class1 { public void method1() { } } """) - assertCaretPosition("|") - } + assertCaretPosition("|") + } - fun `test deleting parameter using DELETE`() { - placeCaretIntoCellWithText("") - typeText("int") - pressKey(KnownKeys.Tab) - typeText("p1") - assertEditorText(""" + fun `test deleting parameter using DELETE`() = + kotlinx.coroutines.test.runTest { + placeCaretIntoCellWithText("") + typeText("int") + pressKey(KnownKeys.Tab) + typeText("p1") + assertEditorText(""" public class Class1 { public void method1(int p1) { } } """) - pressKey(KnownKeys.Delete) - assertFinalEditorText(""" + pressKey(KnownKeys.Delete) + assertFinalEditorText(""" public class Class1 { public void method1() { } } """) - assertCaretPosition("|") - } + assertCaretPosition("|") + } - fun `test deleting placeholder`() { - placeCaretIntoCellWithText("") - typeText("int") - pressKey(KnownKeys.Tab) - typeText("p1") - pressKey(KnownKeys.Enter) - assertEditorText(""" + fun `test deleting placeholder`() = + kotlinx.coroutines.test.runTest { + placeCaretIntoCellWithText("") + typeText("int") + pressKey(KnownKeys.Tab) + typeText("p1") + pressKey(KnownKeys.Enter) + assertEditorText(""" public class Class1 { public void method1(int p1, ) { } } """) - pressKey(KnownKeys.Backspace) - assertFinalEditorText(""" + pressKey(KnownKeys.Backspace) + assertFinalEditorText(""" public class Class1 { public void method1(int p1) { } } """) - assertCaretPosition("p1|") - } + assertCaretPosition("p1|") + } - fun `test typing plus expression`() { - placeCaretIntoCellWithText("") - typeText("int ") - pressKey(KnownKeys.Tab) - typeText("abc") - typeText("=") - typeText("10") - typeText("+") - // pressKey(KnownKeys.Enter) - typeText("20") - assertFinalEditorText(""" + fun `test typing plus expression`() = + kotlinx.coroutines.test.runTest { + placeCaretIntoCellWithText("") + typeText("int ") + assertEditorText(""" + public class Class1 { + public void method1() { + int ; + } + } + """) + pressKey(KnownKeys.Tab) + typeText("abc") + assertEditorText(""" + public class Class1 { + public void method1() { + int abc; + } + } + """) + typeText("=") + assertEditorText(""" + public class Class1 { + public void method1() { + int abc = ; + } + } + """) + typeText("10") + assertEditorText(""" + public class Class1 { + public void method1() { + int abc = 10; + } + } + """) + typeText("+") + typeText("20") + assertEditorText(""" public class Class1 { public void method1() { int abc = 10 + 20; } } """) - } + assertFinalEditorText(""" + public class Class1 { + public void method1() { + int abc = 10 + 20; + } + } + """) + } private fun runParsingTest(input: String) = runParsingTest(input, false) + private fun runCompletionTest(input: String) = runParsingTest(input, true) - private fun runParsingTest(input: String, completion: Boolean) { + + private fun runParsingTest( + input: String, + completion: Boolean, + ) { readAction { println("Running test ...") placeCaretIntoCellWithText("") - val layoutable = (editor.getSelection() as CaretSelection).layoutable - val node = layoutable.cell.ancestors(true) - .mapNotNull { it.getProperty(CommonCellProperties.node) }.first() + val node = + layoutable.cell + .backend() + .ancestors(true) + .mapNotNull { it.getProperty(CommonCellProperties.node) } + .first() val parser = ParserForEditor(editorEngine).getParser(node.expectedConcept()!!, forCodeCompletion = completion) val parseTree = parser.parse(input) println(parseTree) } } - private fun runClassParsingTest(input: String, completion: Boolean) { + + private fun runClassParsingTest( + input: String, + completion: Boolean, + ) { println("Running test ...") placeCaretIntoCellWithText("class") val layoutable = (editor.getSelection() as CaretSelection).layoutable - val node = layoutable.cell.ancestors(true) - .mapNotNull { it.getProperty(CommonCellProperties.node) }.first() + val node = + layoutable.cell + .backend() + .ancestors(true) + .mapNotNull { it.getProperty(CommonCellProperties.node) } + .first() val concept = node.getNode()!!.concept!! val parser = ParserForEditor(editorEngine).getParser(concept, forCodeCompletion = completion) val parseTree = parser.parse(input) @@ -491,14 +580,19 @@ class BaseLanguageTests : TestBase("SimpleProject") { } fun `test statement parsing 1`() = runParsingTest("int a;") + fun `test statement parsing 2`() = runParsingTest("int a = 10 + 20;") + fun `test statement parsing 3`() = runParsingTest("return 10;") fun `test statement parsing 4`() = runParsingTest("""for ( int i = 0 ; i < 10 ; i++ ) { return i ; }""") + fun `test statement parsing 5`() = runParsingTest("""System.out.println("Hello");""") + fun `disabled test statement parsing 6`() = runParsingTest("""System.out.println("Hello World!");""") - fun `test class parsing 1`() = runClassParsingTest(""" + fun `test class parsing 1`() = + runClassParsingTest(""" class Math { public static int plus(int a, int b) { return a + b; @@ -507,22 +601,6 @@ class BaseLanguageTests : TestBase("SimpleProject") { """, false) fun `test completion 1`() = runParsingTest("""intᚹ""") - fun `test completion 2`() = runParsingTest("""int aᚹ""") - fun `disabled test parser completion`() { - placeCaretIntoCellWithText("") - (editor.getSelection() as CaretSelection).replaceText("int a") - // repeat(5) { pressKey(KnownKeys.ArrowLeft) } - (editor.getSelection() as CaretSelection).triggerParserCompletion() - val actions = editor.getCodeCompletionActions() - actions.forEach { println("Code Completion Entry: " + it.getCompletionPattern()) } - pressEnter() - assertFinalEditorText(""" - public class Class1 { - public void method1() { - int a; - } - } - """) - } + fun `test completion 2`() = runParsingTest("""int aᚹ""") } 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 index 7d6e9e6f..adf6a93b 100644 --- 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 @@ -38,7 +38,9 @@ import kotlin.io.path.deleteRecursively * Based on org.jetbrains.uast.test.env.AbstractLargeProjectTest */ @Suppress("removal") -abstract class TestBase(val testDataName: String?) : UsefulTestCase() { +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") @@ -66,13 +68,14 @@ abstract class TestBase(val testDataName: String?) : UsefulTestCase() { 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())!! - } + 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) } @@ -97,13 +100,9 @@ abstract class TestBase(val testDataName: String?) : UsefulTestCase() { return checkNotNull(ProjectHelper.fromIdeaProject(project)) { "MPS project not loaded" } } - protected fun writeAction(body: () -> R): R { - return mpsProject.modelAccess.computeWriteAction(body) - } + protected fun writeAction(body: () -> R): R = mpsProject.modelAccess.computeWriteAction(body) - protected fun writeActionOnEdt(body: () -> R): R { - return onEdt { writeAction { body() } } - } + protected fun writeActionOnEdt(body: () -> R): R = onEdt { writeAction { body() } } protected fun onEdt(body: () -> R): R { var result: R? = null 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 index 05251c41..3e51397d 100644 --- 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 @@ -7,6 +7,7 @@ import jetbrains.mps.module.ReloadableModuleBase /** * Check that the environment is initialized properly and all plugins and modules are loaded properly. */ +@Suppress("ktlint:standard:function-naming") class TestFrameworkSetupTest : TestBase("SimpleProject") { fun `test plugins loaded`() { // IDEA plugins @@ -19,7 +20,9 @@ class TestFrameworkSetupTest : TestBase("SimpleProject") { // MPS modules inside those IDEA plugins readAction { assertContainsElements( - mpsProject.repository.modules.map { it.moduleName ?: "" }.sorted(), + mpsProject.repository.modules + .map { it.moduleName ?: "" } + .sorted(), "org.modelix.mps.editor.ssr.stubs", "org.modelix.mps.notation.impl.baseLanguage", ) @@ -28,7 +31,11 @@ class TestFrameworkSetupTest : TestBase("SimpleProject") { fun `test module is valid for classloading`() { readAction { - val module = mpsProject.repository.modules.filterIsInstance().first { it.moduleName == "org.modelix.mps.notation.impl.baseLanguage" } + 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/build.gradle.kts b/projectional-editor-ssr-mps/build.gradle.kts index 82d22f34..c3bec803 100644 --- a/projectional-editor-ssr-mps/build.gradle.kts +++ b/projectional-editor-ssr-mps/build.gradle.kts @@ -64,11 +64,12 @@ tasks { val pluginDir = mpsPluginsDir if (pluginDir != null) { - val installMpsPlugin = register("installMpsPlugin") { - dependsOn(prepareSandbox) - from(project.layout.buildDirectory.dir("idea-sandbox/plugins/${project.name}")) - into(pluginDir.resolve(project.name)) - } + val installMpsPlugin = + register("installMpsPlugin") { + dependsOn(prepareSandbox) + from(project.layout.buildDirectory.dir("idea-sandbox/plugins/${project.name}")) + into(pluginDir.resolve(project.name)) + } register("installMpsDevPlugins") { dependsOn(installMpsPlugin) } @@ -82,7 +83,14 @@ tasks { .from(patchPluginXml.flatMap { it.outputFiles }) doLast { - val jarsInBasePlugin = defaultDestinationDir.get().resolve(project(":editor-common-mps").name).resolve("lib").list()?.toHashSet() ?: emptySet() + val jarsInBasePlugin = + defaultDestinationDir + .get() + .resolve(project(":editor-common-mps").name) + .resolve("lib") + .list() + ?.toHashSet() + ?: emptySet() defaultDestinationDir.get().resolve(project.name).resolve("lib").listFiles()?.forEach { if (jarsInBasePlugin.contains(it.name)) it.delete() } 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 index 9f79cf30..df595cdd 100644 --- 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 @@ -8,7 +8,9 @@ import org.modelix.model.mpsadapters.MPSChangeTranslator import org.modelix.model.mpsadapters.MPSLanguageRepository import org.modelix.scopes.ScopeAspect -class EditorIntegrationForMPS(val editorEngine: EditorEngine) { +class EditorIntegrationForMPS( + val editorEngine: EditorEngine, +) { private var aspectsFromMPS: LanguageAspectsFromMPSModules? = null private var mpsChangeTranslator: MPSChangeTranslator? = null private var mpsLanguageRepository: MPSLanguageRepository? = null diff --git a/projectional-editor-ssr-mps/src/main/kotlin/org/modelix/editor/ssr/mps/LanguageAspectsFromMPSModules.kt b/projectional-editor-ssr-mps/src/main/kotlin/org/modelix/editor/ssr/mps/LanguageAspectsFromMPSModules.kt index 7881f221..867ec394 100644 --- a/projectional-editor-ssr-mps/src/main/kotlin/org/modelix/editor/ssr/mps/LanguageAspectsFromMPSModules.kt +++ b/projectional-editor-ssr-mps/src/main/kotlin/org/modelix/editor/ssr/mps/LanguageAspectsFromMPSModules.kt @@ -15,25 +15,36 @@ import org.modelix.model.api.IConceptReference private val LOG = KotlinLogging.logger { } -class LanguageAspectsFromMPSModules(val repository: SRepository) : IConceptEditorRegistry { - +class LanguageAspectsFromMPSModules( + val repository: SRepository, +) : IConceptEditorRegistry { private var loadedAspects: Map> = emptyMap() private var conceptEditors: Map> = emptyMap() private var needsUpdate = true private val classLoaderManager = ApplicationManager.getApplication().getComponent(MPSCoreComponents::class.java).classLoaderManager - private val deployListener = object : DeployListener { - override fun onLoaded(loadedModules: MutableSet, monitor: ProgressMonitor) { - needsUpdate = true - } + private val deployListener = + object : DeployListener { + override fun onLoaded( + loadedModules: MutableSet, + monitor: ProgressMonitor, + ) { + needsUpdate = true + } - override fun onUnloaded(unloadedModules: MutableSet, monitor: ProgressMonitor) { - needsUpdate = true - } + override fun onUnloaded( + unloadedModules: MutableSet, + monitor: ProgressMonitor, + ) { + needsUpdate = true + } - override fun onUnloaded(callback: DeployListener.ResourceTrackerCallback, monitor: ProgressMonitor) { - needsUpdate = true + override fun onUnloaded( + callback: DeployListener.ResourceTrackerCallback, + monitor: ProgressMonitor, + ) { + needsUpdate = true + } } - } init { classLoaderManager.addListener(deployListener) @@ -46,34 +57,40 @@ class LanguageAspectsFromMPSModules(val repository: SRepository) : IConceptEdito private fun updateDescriptors() { needsUpdate = false val oldAspects = loadedAspects - val newDescriptors = repository.modules.filterIsInstance().mapNotNull { module -> - try { - val moduleName = module.moduleName ?: return@mapNotNull null - val descriptorClass = try { - module.getOwnClass(moduleName + ".modelix.AspectsDescriptor") - } catch (ex: ClassNotFoundException) { - return@mapNotNull null - } - descriptorClass.getField("INSTANCE").get(null) as ILanguageAspectsDescriptor - } catch (ex: Exception) { - LOG.error(ex) { "Failed to load descriptor from ${module.moduleName}" } - null - } - }.toSet() + val newDescriptors = + repository.modules + .filterIsInstance() + .mapNotNull { module -> + try { + val moduleName = module.moduleName ?: return@mapNotNull null + val descriptorClass = + try { + module.getOwnClass(moduleName + ".modelix.AspectsDescriptor") + } catch (ex: ClassNotFoundException) { + return@mapNotNull null + } + descriptorClass.getField("INSTANCE").get(null) as ILanguageAspectsDescriptor + } catch (ex: Exception) { + LOG.error(ex) { "Failed to load descriptor from ${module.moduleName}" } + null + } + }.toSet() if (oldAspects.keys == newDescriptors) return // nothing changed - val newAspects = newDescriptors.associateWith { descriptor -> - oldAspects[descriptor] ?: descriptor.createAspects() - } + val newAspects = + newDescriptors.associateWith { descriptor -> + oldAspects[descriptor] ?: descriptor.createAspects() + } loadedAspects = newAspects - conceptEditors = newAspects.values - .asSequence() - .flatten() - .filterIsInstance() - .flatMap { it.conceptEditors } - .filter { it.declaredConcept != null } - .groupBy { it.declaredConcept!!.getReference() } + conceptEditors = + newAspects.values + .asSequence() + .flatten() + .filterIsInstance() + .flatMap { it.conceptEditors } + .filter { it.declaredConcept != null } + .groupBy { it.declaredConcept!!.getReference() } } override fun getConceptEditors(concept: IConceptReference): List { 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 285d5fe6..03447539 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 @@ -90,8 +90,9 @@ import java.util.Collections import kotlin.time.Duration.Companion.seconds @Service(Service.Level.PROJECT) -class ModelixSSRServerForMPSProject(private val project: Project) : Disposable { - +class ModelixSSRServerForMPSProject( + private val project: Project, +) : Disposable { init { ApplicationManager.getApplication().service().registerProject(project) } @@ -103,7 +104,6 @@ class ModelixSSRServerForMPSProject(private val project: Project) : Disposable { @Service(Service.Level.APP) class ModelixSSRServerForMPS : Disposable { - private var ssrServer: ModelixSSRServer? = null private var ktorServer: EmbeddedServer<*, *>? = null private val projects: MutableSet = Collections.synchronizedSet(HashSet()) @@ -118,17 +118,17 @@ class ModelixSSRServerForMPS : Disposable { projects.remove(project) } - private fun getMPSProjects(): List { - return runSynchronized(projects) { + private fun getMPSProjects(): List = + runSynchronized(projects) { projects.mapNotNull { it.getComponent(MPSProject::class.java) } } - } - private fun getRootNode(): INode? { - return getMPSProjects().asSequence().map { - MPSRepositoryAsNode(it.repository).asLegacyNode() - }.firstOrNull() - } + private fun getRootNode(): INode? = + getMPSProjects() + .asSequence() + .map { + MPSRepositoryAsNode(it.repository).asLegacyNode() + }.firstOrNull() fun ensureStarted() { runSynchronized(this) { @@ -136,13 +136,14 @@ class ModelixSSRServerForMPS : Disposable { println("starting modelix SSR server") - val ssrServer = ModelixSSRServer((getRootNode() ?: return).getArea()) + val ssrServer = ModelixSSRServer((getRootNode() ?: return).getArea().asModel()) this.ssrServer = ssrServer mpsIntegration = EditorIntegrationForMPS(ssrServer.editorEngine) mpsIntegration!!.init(getMPSProjects().first().repository) - ktorServer = org.modelix.mps.editor.common.embeddedServer(port = 43593, classLoader = this.javaClass.classLoader) { - initKtorServer(ssrServer) - } + ktorServer = + org.modelix.mps.editor.common.embeddedServer(port = 43593, classLoader = this.javaClass.classLoader) { + initKtorServer(ssrServer) + } ktorServer!!.start() } } @@ -161,14 +162,18 @@ class ModelixSSRServerForMPS : Disposable { repository.getArea().executeRead { body { ul { - repository.getChildren(BuiltinLanguages.MPSRepositoryConcepts.Repository.modules).sortedBy { it.name }.forEach { - li { - a { - href = "module/${URLEncoder.encode(it.reference.serialize(), StandardCharsets.UTF_8)}/" - +(it.name ?: "") + repository + .getChildren( + BuiltinLanguages.MPSRepositoryConcepts.Repository.modules + ).sortedBy { it.name } + .forEach { + li { + a { + href = "module/${URLEncoder.encode(it.reference.serialize(), StandardCharsets.UTF_8)}/" + +(it.name ?: "") + } } } - } } } } @@ -266,32 +271,39 @@ class ModelixSSRServerForMPSStartupActivity : ProjectActivity { } object MPSScopeProvider : IScopeProvider { - override fun getScope(sourceNode: INonExistingNode, link: IReferenceLink): IScope { + override fun getScope( + sourceNode: INonExistingNode, + link: IReferenceLink, + ): IScope { val mpsSourceNode = sourceNode.getNode()?.asWritableNode() as? MPSWritableNode - val descriptor = if (mpsSourceNode == null) { - val contextNode: SNode = sourceNode.getExistingAncestor().toMPS()!! - val containmentLink: SContainmentLink = sourceNode.getContainmentLink().toMPS()!! - val index = sourceNode.index() - val association: SReferenceLink = link.toMPS()!! - val concept: SAbstractConcept = sourceNode.expectedConcept().toMPS()!! - ModelConstraints.getReferenceDescriptor( - contextNode, - containmentLink, - index, - association, - concept, - ) - } else { - ModelConstraints.getReferenceDescriptor(mpsSourceNode.node, link.toMPS()!!) - } + val descriptor = + if (mpsSourceNode == null) { + val contextNode: SNode = sourceNode.getExistingAncestor().toMPS()!! + val containmentLink: SContainmentLink = sourceNode.getContainmentLink().toMPS()!! + val index = sourceNode.index() + val association: SReferenceLink = link.toMPS()!! + val concept: SAbstractConcept = sourceNode.expectedConcept().toMPS()!! + ModelConstraints.getReferenceDescriptor( + contextNode, + containmentLink, + index, + association, + concept, + ) + } else { + ModelConstraints.getReferenceDescriptor(mpsSourceNode.node, link.toMPS()!!) + } return MPSScope(descriptor.getScope()) } } -class MPSScope(val scope: Scope) : IScope { - override fun getVisibleElements(node: INonExistingNode, link: IReferenceLink): List { - return scope.getAvailableElements("").map { ExistingNode(MPSNode(it)) } - } +class MPSScope( + val scope: Scope, +) : IScope { + override fun getVisibleElements( + node: INonExistingNode, + link: IReferenceLink, + ): List = scope.getAvailableElements("").map { ExistingNode(MPSNode(it)) } } object MPSConstraints : IConstraintsChecker { @@ -308,25 +320,29 @@ object MPSConstraints : IConstraintsChecker { // ConstraintsCanBeFacade.checkCanBeRoot() - val containmentContext = ContainmentContext.Builder() - .parentNode(parentNode) - .link(node.getContainmentLink().toMPS()) - .childConcept(node.expectedConcept().toMPS()!!) - .build() - - val ancestorViolations = node.ancestors().flatMap { ancestor -> - val ancestorNode = ancestor.getNode().toMPS() ?: return@flatMap emptyList() - - ConstraintsCanBeFacade.checkCanBeAncestor( - CanBeAncestorContext.Builder() - .ancestorNode(ancestorNode) - .parentNode(parentNode) - .childConcept(node.expectedConcept().toMPS()!!) - .descendantNode(node.getNode().toMPS()) - .link(node.getContainmentLink().toMPS()) - .build(), - ) - } + val containmentContext = + ContainmentContext + .Builder() + .parentNode(parentNode) + .link(node.getContainmentLink().toMPS()) + .childConcept(node.expectedConcept().toMPS()!!) + .build() + + val ancestorViolations = + node.ancestors().flatMap { ancestor -> + val ancestorNode = ancestor.getNode().toMPS() ?: return@flatMap emptyList() + + ConstraintsCanBeFacade.checkCanBeAncestor( + CanBeAncestorContext + .Builder() + .ancestorNode(ancestorNode) + .parentNode(parentNode) + .childConcept(node.expectedConcept().toMPS()!!) + .descendantNode(node.getNode().toMPS()) + .link(node.getContainmentLink().toMPS()) + .build(), + ) + } val parentViolations = ConstraintsCanBeFacade.checkCanBeParent(containmentContext).asSequence() val childViolations = ConstraintsCanBeFacade.checkCanBeChild(containmentContext).asSequence() return (ancestorViolations + parentViolations + childViolations).map { MPSConstraintViolation(it) }.toList() + @@ -336,9 +352,18 @@ object MPSConstraints : IConstraintsChecker { fun checkLanguageImported(node: INonExistingNode): List { val concept = node.expectedConcept() as? MPSConcept ?: return emptyList() val language = concept.concept.language - val model = node.ancestors().map { it.getNode() }.filterIsInstance() - .map { it.model }.firstOrNull() ?: return emptyList() - val usedLanguages = ModelDependencyResolver(LanguageRegistry.getInstance(model.repository), model.repository).usedLanguages(model).toSet() + val model = + node + .ancestors() + .map { it.getNode() } + .filterIsInstance() + .map { it.model } + .firstOrNull() ?: return emptyList() + val usedLanguages = + ModelDependencyResolver( + LanguageRegistry.getInstance(model.repository), + model.repository + ).usedLanguages(model).toSet() return if (!usedLanguages.contains(language)) { listOf(MPSLanguageNotImportedViolation(concept.concept)) } else { @@ -346,26 +371,47 @@ object MPSConstraints : IConstraintsChecker { } } - override fun checkPropertyValue(node: INonExistingNode, property: IProperty, value: String): List { + override fun checkPropertyValue( + node: INonExistingNode, + property: IProperty, + value: String, + ): List { val mpsProperty = property.toMPS() ?: return emptyList() val internalValue = IPropertyPresentationProvider.getPresentationProviderFor(mpsProperty).fromPresentation(value) - val mpsNode = node.getNode()?.toMPS() - ?: jetbrains.mps.smodel.SNode(node.expectedConcept().toMPS() as? SConcept ?: jetbrains.mps.smodel.SNodeUtil.concept_BaseConcept) + val mpsNode = + node.getNode()?.toMPS() + ?: jetbrains.mps.smodel.SNode( + node.expectedConcept().toMPS() as? SConcept ?: jetbrains.mps.smodel.SNodeUtil.concept_BaseConcept + ) val context = FailingPropertyConstraintContext(mpsNode, mpsProperty, internalValue) return ConstraintsChildAndPropFacade.checkPropertyValue(context).map { MPSProblem(it) } } } fun INode?.toMPS(): SNode? = this?.asWritableNode().toMPS() + fun IWritableNode?.toMPS(): SNode? = if (this is MPSWritableNode) this.node else null + fun IChildLink?.toMPS(): SContainmentLink? = if (this is MPSChildLink) this.link else null + fun IChildLinkDefinition?.toMPS(): SContainmentLink? = if (this is MPSChildLink) this.link else null + fun IReferenceLink?.toMPS(): SReferenceLink? = if (this is MPSReferenceLink) this.link else null + 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 -class MPSProblem(val problem: Problem) : IConstraintViolation -class MPSLanguageNotImportedViolation(val concept: SAbstractConcept) : IConstraintViolation +class MPSConstraintViolation( + val rule: Rule<*>, +) : IConstraintViolation + +class MPSProblem( + val problem: Problem, +) : IConstraintViolation + +class MPSLanguageNotImportedViolation( + val concept: SAbstractConcept, +) : IConstraintViolation diff --git a/projectional-editor-ssr-server/build.gradle.kts b/projectional-editor-ssr-server/build.gradle.kts index dfa09a73..1f37939a 100644 --- a/projectional-editor-ssr-server/build.gradle.kts +++ b/projectional-editor-ssr-server/build.gradle.kts @@ -9,6 +9,8 @@ dependencies { implementation(coreLibs.ktor.server.core) implementation(coreLibs.ktor.server.websockets) implementation(libs.kotlin.logging) + implementation(libs.kotlinx.rpc.krpc.ktor.server) + implementation(libs.kotlinx.rpc.krpc.serialization.json) } kotlin { 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 94c894e2..6776f6d8 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 @@ -2,76 +2,48 @@ package org.modelix.editor.ssr.server import io.github.oshai.kotlinlogging.KotlinLogging import io.ktor.server.routing.Route -import io.ktor.server.websocket.DefaultWebSocketServerSession -import io.ktor.server.websocket.webSocket -import io.ktor.utils.io.CancellationException -import io.ktor.websocket.Frame -import io.ktor.websocket.readText +import io.ktor.websocket.WebSocketSession import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job import kotlinx.coroutines.cancel -import kotlinx.coroutines.channels.BufferOverflow -import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import org.modelix.editor.Bounds -import org.modelix.editor.EditorComponent +import kotlinx.rpc.krpc.ktor.server.rpc +import kotlinx.rpc.krpc.serialization.json.json import org.modelix.editor.EditorEngine -import org.modelix.editor.IVirtualDom -import org.modelix.editor.IVirtualDomUI -import org.modelix.editor.VirtualDom -import org.modelix.editor.contains -import org.modelix.editor.id -import org.modelix.editor.ssr.common.DomTreeUpdate -import org.modelix.editor.ssr.common.ElementReference -import org.modelix.editor.ssr.common.HTMLElementBoundsUpdate -import org.modelix.editor.ssr.common.HTMLElementUpdateData -import org.modelix.editor.ssr.common.INodeUpdateData -import org.modelix.editor.ssr.common.MessageFromClient -import org.modelix.editor.ssr.common.MessageFromServer -import org.modelix.editor.ssr.common.TextNodeUpdateData +import org.modelix.editor.text.backend.TextEditorServiceImpl +import org.modelix.editor.text.shared.TextEditorService import org.modelix.incremental.DependencyTracking import org.modelix.incremental.IDependencyListener import org.modelix.incremental.IStateVariableGroup import org.modelix.incremental.IStateVariableReference import org.modelix.incremental.IncrementalEngine -import org.modelix.kotlin.utils.runSynchronized -import org.modelix.model.api.INodeResolutionScope -import org.modelix.model.api.NodeReference -import org.modelix.model.api.resolveIn -import org.modelix.model.area.IArea -import java.util.Collections -import kotlin.collections.HashMap -import kotlin.collections.LinkedHashSet -import kotlin.collections.List -import kotlin.collections.MutableMap -import kotlin.collections.MutableSet -import kotlin.collections.filter +import org.modelix.model.api.IMutableModel +import java.util.concurrent.atomic.AtomicReference import kotlin.collections.forEach -import kotlin.collections.get -import kotlin.collections.getOrPut -import kotlin.collections.map -import kotlin.collections.mapNotNull import kotlin.collections.minus -import kotlin.collections.remove -import kotlin.collections.set -import kotlin.collections.toList +import kotlin.time.Duration.Companion.days private val LOG = KotlinLogging.logger { } -class ModelixSSRServer(private val nodeResolutionScope: INodeResolutionScope) { - +class ModelixSSRServer( + private val model: IMutableModel, +) { private val incrementalEngine = IncrementalEngine() val editorEngine: EditorEngine = EditorEngine(incrementalEngine) - private val allSessions: MutableSet = Collections.synchronizedSet(LinkedHashSet()) private val lock = Any() private val coroutinesScope = CoroutineScope(Dispatchers.Default) - private val editorUpdater = Validator(coroutinesScope) { updateAll() } - private val dependencyListener: IDependencyListener = object : IDependencyListener { - override fun parentGroupChanged(childGroup: IStateVariableGroup) {} - override fun accessed(key: IStateVariableReference<*>) {} - override fun modified(key: IStateVariableReference<*>) { editorUpdater.invalidate() } - } + private val serviceInstances: AtomicReference> = AtomicReference(emptySet()) + private val dependencyListener: IDependencyListener = + object : IDependencyListener { + override fun parentGroupChanged(childGroup: IStateVariableGroup) {} + + override fun accessed(key: IStateVariableReference<*>) {} + + override fun modified(key: IStateVariableReference<*>) { + serviceInstances.get().forEach { it.triggerUpdates() } + } + } fun install(route: Route) { route.installRoutes() @@ -79,235 +51,44 @@ class ModelixSSRServer(private val nodeResolutionScope: INodeResolutionScope) { fun dispose() { DependencyTracking.removeListener(dependencyListener) - editorUpdater.stop() + serviceInstances.get().forEach { it.dispose() } coroutinesScope.cancel("disposed") } private fun Route.installRoutes() { DependencyTracking.registerListener(dependencyListener) - editorUpdater.start() - - webSocket("ws") { - val session = WebsocketSession(this) - try { - allSessions.add(session) - session.receiveMessages() - } finally { - allSessions.remove(session) - session.dispose() - } - } - } - - fun updateAll() { - val sessions = runSynchronized(allSessions) { allSessions.toList() } - runSynchronized(lock) { - sessions.forEach { - try { - it.updateAllEditors() - } catch (ex: Exception) { - LOG.error(ex) { "Failed to send editor update" } - } - } - } - } - private inner class WebsocketSession(val ws: DefaultWebSocketServerSession) { - private val editors = HashMap() - - suspend fun receiveMessages() { - for (wsMessage in ws.incoming) { - var clientMessage: MessageFromClient? = null - try { - when (wsMessage) { - is Frame.Text -> { - val serializedMessage = wsMessage.readText() - LOG.debug { "Received message: $serializedMessage" } - val deserializedMessage = MessageFromClient.fromJson(serializedMessage) - clientMessage = deserializedMessage - runSynchronized(lock) { - // TODO maybe use a single threaded coroutines dispatcher for all UI code - processMessage(deserializedMessage) - } - } - else -> {} - } - } catch (ex: Throwable) { - LOG.error(ex) { "Failed to process $wsMessage" } - ws.outgoing.send( - Frame.Text( - MessageFromServer( - editorId = clientMessage?.editorId, - error = ex.stackTraceToString(), - ).toJson(), - ), - ) + rpc("rpc") { + val websocketSession: WebSocketSession = this + rpcConfig { + serialization { + json() } } - } - fun processMessage(msg: MessageFromClient) { - msg.editorId?.let { editorId -> - if (msg.dispose) { - editors.remove(editorId)?.dispose() - } else { - val editor = editors.getOrPut(editorId) { EditorSession(editorId) } - editor.processMessage(msg) - } - } - } - - fun dispose() { - editors.values.forEach { it.dispose() } - editors.clear() - } - - fun updateAllEditors() { - editors.values.forEach { it.sendUpdate() } - } - - private inner class EditorSession(val editorId: String) { - private var editorComponent: EditorComponent? = null - private val commonElementPrefix = editorId + "-" - - private fun getEditor() = checkNotNull(editorComponent) { "Editor $editorId isn't initialized" } - - fun processMessage(msg: MessageFromClient) { - msg.rootNodeReference?.let { rootNodeReferenceString -> - (nodeResolutionScope as IArea).executeRead { - val rootNode = checkNotNull(NodeReference(rootNodeReferenceString).resolveIn(nodeResolutionScope)) { - "Root node not found: $rootNodeReferenceString" - } - LOG.debug { "Root node $rootNodeReferenceString found: $rootNode" } - editorComponent = editorEngine.editNode(rootNode, VirtualDom(VDomUI(), commonElementPrefix)) - } - } - msg.boundUpdates?.let { updates -> - (editorComponent!!.virtualDom.ui as VDomUI).bounds.putAll(updates) - } - msg.keyboardEvent?.let { event -> - getEditor().processKeyEvent(event) - } - msg.mouseEvent?.let { event -> - getEditor().processMouseEvent(event) - } - sendUpdate() - } - - fun sendUpdate() { - LOG.debug { "($editorId) sendUpdate" } - editorComponent!!.update() - 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 - } - } - - if (changedElements.isEmpty()) return - - ws.outgoing.trySend( - Frame.Text( - MessageFromServer( - editorId = editorId, - domUpdate = DomTreeUpdate( - elements = changedElements.values.toList(), - ), - ).toJson(), - ), - ) - } - - fun toUpdateData(node: VirtualDom.Node, id2data: MutableMap): INodeUpdateData { - return when (node) { - 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 - } - fun createData() = HTMLElementUpdateData( - id = id, - tagName = node.tagName, - attributes = node.getAttributes() - "id", - children = node.childNodes.toList().map { toUpdateData(it, id2data) }, - ) - if (id == null) { - createData() - } else { - if (node.wasModified()) { - id2data[id] = createData() - } else { - if (node.wasAnyDescendantModified()) { - node.childNodes.forEach { toUpdateData(it, id2data) } - } - } - ElementReference(id) - } - } - else -> throw UnsupportedOperationException("Unsupported element type: $node") - } - } - - fun dispose() { - editorComponent?.dispose() - editorComponent = null - } - - inner class VDomUI() : IVirtualDomUI { - val bounds: MutableMap = HashMap() - override fun getOuterBounds(element: IVirtualDom.Element): Bounds { - return bounds[element.id]?.outer ?: Bounds.ZERO - } - - override fun getInnerBounds(element: IVirtualDom.Element): Bounds { - return bounds[element.id]?.let { it.inner ?: it.outer } ?: Bounds.ZERO - } - - override fun getElementsAt(x: Double, y: Double): List { - // TODO performance - return bounds.filter { it.value.outer.contains(x, y) } - .mapNotNull { editorComponent!!.virtualDom.getElementById(it.key) } + registerService { + val instance = TextEditorServiceImpl(editorEngine, model, websocketSession) + serviceInstances.getAndUpdate { it + instance } + websocketSession.onCancellation { instance.dispose() } + websocketSession.onCancellation { + serviceInstances.getAndUpdate { it - instance } + instance.dispose() } + instance } } } } -/** - * When calling invalidate(), the `validator` function is executed, but avoid executing it too often when there are - * many invalidate() calls. - */ -class Validator(val coroutineScope: CoroutineScope, private val validator: suspend () -> Unit) { - private val channel = Channel(capacity = 1, onBufferOverflow = BufferOverflow.DROP_LATEST) - private var validationJob: Job? = null - fun invalidate() { channel.trySend(Unit) } - fun start() { - check(validationJob?.isActive != true) { "Already started" } - validationJob = coroutineScope.launch { - for (x in channel) { - try { - validator() - } catch (ex: CancellationException) { - throw ex - } catch (ex: Throwable) { - LOG.error(ex) { "Validation failed" } - } +private fun CoroutineScope.onCancellation(body: suspend () -> Unit) { + launch { + try { + while (true) { + delay(100000.days) } + } catch (ex: kotlinx.coroutines.CancellationException) { + body() + throw ex } } - fun stop() { - validationJob?.cancel("stopped") - validationJob = null - } - - companion object { - private val LOG = KotlinLogging.logger { } - } } diff --git a/projectional-editor/build.gradle.kts b/projectional-editor/build.gradle.kts index b98dd651..be0583a1 100644 --- a/projectional-editor/build.gradle.kts +++ b/projectional-editor/build.gradle.kts @@ -1,6 +1,7 @@ plugins { kotlin("multiplatform") kotlin("plugin.serialization") + alias(libs.plugins.kotlin.rpc) `maven-publish` } @@ -29,6 +30,7 @@ kotlin { implementation(coreLibs.kotlin.serialization.json) api(coreLibs.modelix.incremental) api(libs.kotlin.html) + api(libs.kotlinx.rpc.core) api(project(":parser")) } } diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/aspects/ILanguageAspect.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/aspects/ILanguageAspect.kt index 95532f33..b009dbf9 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/aspects/ILanguageAspect.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/aspects/ILanguageAspect.kt @@ -8,10 +8,8 @@ interface ILanguageAspectFactory { fun createInstance(language: ILanguage): AspectT } -fun ILanguageAspectFactory.getInstances(): List { - return LanguageAspects.getInstanceFromContext().getAllAspectInstances(this) -} +fun ILanguageAspectFactory.getInstances(): List = + LanguageAspects.getInstanceFromContext().getAllAspectInstances(this) -fun ILanguageAspectFactory.getInstance(language: ILanguage): AspectT { - return LanguageAspects.getInstanceFromContext().getAspect(language, this) -} +fun ILanguageAspectFactory.getInstance(language: ILanguage): AspectT = + LanguageAspects.getInstanceFromContext().getAspect(language, this) diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/aspects/LanguageAspects.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/aspects/LanguageAspects.kt index 16f2a63b..179f5d6a 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/aspects/LanguageAspects.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/aspects/LanguageAspects.kt @@ -6,22 +6,33 @@ import org.modelix.model.api.ILanguage class LanguageAspects { private val aspects: MutableMap, ILanguageAspect>> = HashMap() - fun getAspect(language: ILanguage, factory: ILanguageAspectFactory): T { - return aspects.getOrPut(language.getUID(), { HashMap() }).getOrPut(factory, { factory.createInstance(language) }) as T - } + fun getAspect( + language: ILanguage, + factory: ILanguageAspectFactory, + ): T = + aspects + .getOrPut(language.getUID(), { + HashMap() + }) + .getOrPut(factory, { factory.createInstance(language) }) as T - fun getAllAspectInstances(factory: ILanguageAspectFactory): List { - return aspects.values.mapNotNull { it[factory] as T? } - } + fun getAllAspectInstances(factory: ILanguageAspectFactory): List = + aspects.values.mapNotNull { + it[factory] as T? + } fun getAspects(language: ILanguage): List = aspects[language.getUID()]?.values?.toList() ?: emptyList() companion object { private val contextInstance: ContextValue = ContextValue(LanguageAspects()) - fun getInstanceFromContext(): LanguageAspects = contextInstance.getValue() - ?: throw IllegalStateException("No instance available") - fun runWithInstance(instance: LanguageAspects, body: () -> T): T { - return contextInstance.computeWith(instance, body) - } + + fun getInstanceFromContext(): LanguageAspects = + contextInstance.getValue() + ?: throw IllegalStateException("No instance available") + + fun runWithInstance( + instance: LanguageAspects, + body: () -> T, + ): T = contextInstance.computeWith(instance, body) } } diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/aspects/LanguageAspectsBuilder.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/aspects/LanguageAspectsBuilder.kt index 5cffa0b4..f1b0b86a 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/aspects/LanguageAspectsBuilder.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/aspects/LanguageAspectsBuilder.kt @@ -2,12 +2,14 @@ package org.modelix.aspects import org.modelix.model.api.ILanguage -fun languageAspects(language: LanguageT, body: LanguageAspectsBuilder.() -> Unit): LanguageAspectsBuilder { - return LanguageAspectsBuilder(LanguageAspects.getInstanceFromContext(), language).also(body) -} +fun languageAspects( + language: LanguageT, + body: LanguageAspectsBuilder.() -> Unit, +): LanguageAspectsBuilder = LanguageAspectsBuilder(LanguageAspects.getInstanceFromContext(), language).also(body) -class LanguageAspectsBuilder(val aspects: LanguageAspects, val language: LanguageT) { - fun getAspect(factory: ILanguageAspectFactory): T { - return aspects.getAspect(language, factory) - } +class LanguageAspectsBuilder( + val aspects: LanguageAspects, + val language: LanguageT, +) { + fun getAspect(factory: ILanguageAspectFactory): T = aspects.getAspect(language, factory) } diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/aspects/behavior/BehaviorAspect.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/aspects/behavior/BehaviorAspect.kt index 7bfdd67c..785ee8df 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/aspects/behavior/BehaviorAspect.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/aspects/behavior/BehaviorAspect.kt @@ -6,8 +6,6 @@ import org.modelix.model.api.ILanguage class BehaviorAspect : ILanguageAspect { companion object : ILanguageAspectFactory { - override fun createInstance(language: ILanguage): BehaviorAspect { - return BehaviorAspect() - } + override fun createInstance(language: ILanguage): BehaviorAspect = BehaviorAspect() } } diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/aspects/behavior/PolymorphicDispatch.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/aspects/behavior/PolymorphicDispatch.kt index 0d47d4bb..037f19dc 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/aspects/behavior/PolymorphicDispatch.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/aspects/behavior/PolymorphicDispatch.kt @@ -5,14 +5,20 @@ import org.modelix.model.api.IConceptReference import org.modelix.model.api.getAllConcepts import kotlin.jvm.Synchronized -class PolymorphicDispatch(val implementations: Map) { +class PolymorphicDispatch( + val implementations: Map, +) { private val cache: MutableMap?> = HashMap() @Synchronized - fun dispatch(receiverConcept: IConcept, default: () -> ValueT): ValueT { - val optionalResult = cache.getOrPut(receiverConcept.getReference()) { - findValue(receiverConcept) - } + fun dispatch( + receiverConcept: IConcept, + default: () -> ValueT, + ): ValueT { + val optionalResult = + cache.getOrPut(receiverConcept.getReference()) { + findValue(receiverConcept) + } return if (optionalResult == null) default() else optionalResult.value } @@ -25,5 +31,7 @@ class PolymorphicDispatch(val implementations: Map(val value: ValueT) + private class Value( + val value: ValueT, + ) } diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/aspects/behavior/PolymorphicFunctionBuilder.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/aspects/behavior/PolymorphicFunctionBuilder.kt index fb2a9e16..aa1b5c7d 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/aspects/behavior/PolymorphicFunctionBuilder.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/aspects/behavior/PolymorphicFunctionBuilder.kt @@ -7,19 +7,14 @@ import org.modelix.metamodel.untypedConcept import kotlin.reflect.KProperty fun buildPolymorphicFunction() = PolymorphicFunctionBuilder() -class PolymorphicFunctionBuilder { +class PolymorphicFunctionBuilder { fun returns(): WithReturnType = WithReturnType() - inner class WithReturnType() { - - fun forConcept(): ForConcept { - return ForConcept() - } + inner class WithReturnType { + fun forConcept(): ForConcept = ForConcept() - fun > forNode(concept: ConceptT): ForNode { - return ForNode() - } + fun > forNode(concept: ConceptT): ForNode = ForNode() abstract inner class ForNodeOrConcept { protected var defaultValue: ((ParameterT) -> ReturnT)? = null @@ -32,6 +27,7 @@ class PolymorphicFunctionBuilder { inner class ForConcept : ForNodeOrConcept() { fun build(name: String = "") = PolymorphicFunction(name) + fun delegate() = SingleInstanceDelegate { PolymorphicFunction(it) } override fun defaultValue(body: (ConceptT) -> ReturnT): ForConcept { @@ -39,8 +35,11 @@ class PolymorphicFunctionBuilder { return this } - inner class PolymorphicFunction(name: String) { + inner class PolymorphicFunction( + name: String, + ) { private var polymorphicValue: PolymorphicValue<(ConceptT) -> ReturnT> = PolymorphicValue(name) + operator fun invoke(concept: ConceptT): ReturnT { val d = defaultValue return if (d == null) { @@ -50,15 +49,18 @@ class PolymorphicFunctionBuilder { } } - fun implement(concept: SubConceptT, body: (SubConceptT) -> ReturnT) { + fun implement( + concept: SubConceptT, + body: (SubConceptT) -> ReturnT, + ) { polymorphicValue.addImplementation(concept.untyped()) { concept -> body(concept as SubConceptT) } } } } inner class ForNode> : ForNodeOrConcept() { - fun build(name: String = "") = PolymorphicFunction(name) + fun delegate() = SingleInstanceDelegate { PolymorphicFunction(it) } override fun defaultValue(body: (NodeT) -> ReturnT): ForNode { @@ -66,8 +68,11 @@ class PolymorphicFunctionBuilder { return this } - inner class PolymorphicFunction(name: String) { + inner class PolymorphicFunction( + name: String, + ) { private var polymorphicValue: PolymorphicValue<(NodeT) -> ReturnT> = PolymorphicValue(name) + operator fun invoke(node: NodeT): ReturnT { val d = defaultValue return if (d == null) { @@ -77,7 +82,10 @@ class PolymorphicFunctionBuilder { } } - fun > implement(concept: SubConceptT, body: (SubNodeT) -> ReturnT) { + fun > implement( + concept: SubConceptT, + body: (SubNodeT) -> ReturnT, + ) { polymorphicValue.addImplementation(concept.untyped()) { node -> body(node as SubNodeT) } } } @@ -85,9 +93,12 @@ class PolymorphicFunctionBuilder { } } -class SingleInstanceDelegate(val initializer: (String) -> E) { +class SingleInstanceDelegate( + val initializer: (String) -> E, +) { private lateinit var name: String private val instance by lazy { initializer(name) } + operator fun getValue( nothing: Nothing?, property: KProperty<*>, diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/aspects/behavior/PolymorphicValue.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/aspects/behavior/PolymorphicValue.kt index bbd1fa75..036de97f 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/aspects/behavior/PolymorphicValue.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/aspects/behavior/PolymorphicValue.kt @@ -2,21 +2,27 @@ package org.modelix.aspects.behavior import org.modelix.model.api.IConcept -class PolymorphicValue(val name: String) { +class PolymorphicValue( + val name: String, +) { private var implementations: PolymorphicDispatch = PolymorphicDispatch(emptyMap()) - fun getValue(concept: IConcept): ValueT { - return getValue(concept) { throw NoImplementationException(this, concept) } - } + fun getValue(concept: IConcept): ValueT = getValue(concept) { throw NoImplementationException(this, concept) } - fun getValue(concept: IConcept, default: () -> ValueT): ValueT { - return implementations.dispatch(concept, default) - } + fun getValue( + concept: IConcept, + default: () -> ValueT, + ): ValueT = implementations.dispatch(concept, default) - fun addImplementation(concept: IConcept, impl: ValueT) { + fun addImplementation( + concept: IConcept, + impl: ValueT, + ) { implementations = PolymorphicDispatch(implementations.implementations + (concept.getReference() to impl)) } } -class NoImplementationException(val value: PolymorphicValue<*>, val concept: IConcept) : - Exception("${value.name} has no implementation for concept ${concept.getLongName()}") +class NoImplementationException( + val value: PolymorphicValue<*>, + val concept: IConcept, +) : Exception("${value.name} has no implementation for concept ${concept.getLongName()}") diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/constraints/ConstraintsAspect.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/constraints/ConstraintsAspect.kt index 36b2d3f1..8bec54ff 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/constraints/ConstraintsAspect.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/constraints/ConstraintsAspect.kt @@ -8,14 +8,25 @@ object ConstraintsAspect { fun check(node: INonExistingNode) = checkers.flatMap { it.check(node) } - fun checkPropertyValue(node: INonExistingNode, property: IProperty, value: String) = checkers.flatMap { it.checkPropertyValue(node, property, value) } + fun checkPropertyValue( + node: INonExistingNode, + property: IProperty, + value: String, + ) = checkers.flatMap { + it.checkPropertyValue(node, property, value) + } fun canCreate(node: INonExistingNode) = check(node).isEmpty() } interface IConstraintsChecker { fun check(node: INonExistingNode): List - fun checkPropertyValue(node: INonExistingNode, property: IProperty, value: String): List + + fun checkPropertyValue( + node: INonExistingNode, + property: IProperty, + value: String, + ): List } interface IConstraintViolation diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/AstTransformation.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/AstTransformation.kt index a33f0b53..f062c5ce 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/AstTransformation.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/AstTransformation.kt @@ -18,79 +18,171 @@ import org.modelix.parser.SubConceptsSymbol interface IPendingNode : INode { fun commit(location: INonExistingNode): INode + fun flattenFirstAmbiguousNode(): List + fun replaceAllAmbiguousWithFirst(): INode } abstract class PendingNodeBase : IPendingNode { - override fun addNewChild(role: String?, index: Int, concept: IConcept?): INode = TODO("Not yet implemented") + override fun addNewChild( + role: String?, + index: Int, + concept: IConcept?, + ): INode = TODO("Not yet implemented") + override val allChildren: Iterable get() = TODO("Not yet implemented") override val concept: IConcept? get() = TODO("Not yet implemented") override val isValid: Boolean get() = TODO("Not yet implemented") override val parent: INode? get() = TODO("Not yet implemented") override val reference: INodeReference get() = TODO("Not yet implemented") override val roleInParent: String? get() = TODO("Not yet implemented") + override fun getArea(): IArea = TODO("Not yet implemented") + override fun getChildren(role: String?): Iterable = TODO("Not yet implemented") + override fun getConceptReference(): IConceptReference? = TODO("Not yet implemented") + override fun getPropertyRoles(): List = TODO("Not yet implemented") + override fun getPropertyValue(role: String): String? = TODO("Not yet implemented") + override fun getReferenceRoles(): List = TODO("Not yet implemented") + override fun getReferenceTarget(role: String): INode? = TODO("Not yet implemented") - override fun moveChild(role: String?, index: Int, child: INode): Unit = TODO("Not yet implemented") + + override fun moveChild( + role: String?, + index: Int, + child: INode, + ): Unit = TODO("Not yet implemented") + override fun removeChild(child: INode): Unit = TODO("Not yet implemented") - override fun setPropertyValue(role: String, value: String?): Unit = TODO("Not yet implemented") - override fun setReferenceTarget(role: String, target: INode?): Unit = TODO("Not yet implemented") - override fun addNewChild(role: IChildLink, index: Int, concept: IConcept?): INode = TODO("Not yet implemented") - override fun addNewChild(role: IChildLink, index: Int, concept: IConceptReference?): INode = TODO("Not yet implemented") - override fun addNewChild(role: String?, index: Int, concept: IConceptReference?): INode = TODO("Not yet implemented") - override fun addNewChildren(link: IChildLink, index: Int, concepts: List): List = TODO("Not yet implemented") - override fun addNewChildren(role: String?, index: Int, concepts: List): List = TODO("Not yet implemented") + + override fun setPropertyValue( + role: String, + value: String?, + ): Unit = TODO("Not yet implemented") + + override fun setReferenceTarget( + role: String, + target: INode?, + ): Unit = TODO("Not yet implemented") + + override fun addNewChild( + role: IChildLink, + index: Int, + concept: IConcept?, + ): INode = TODO("Not yet implemented") + + override fun addNewChild( + role: IChildLink, + index: Int, + concept: IConceptReference?, + ): INode = TODO("Not yet implemented") + + override fun addNewChild( + role: String?, + index: Int, + concept: IConceptReference?, + ): INode = TODO("Not yet implemented") + + override fun addNewChildren( + link: IChildLink, + index: Int, + concepts: List, + ): List = TODO("Not yet implemented") + + override fun addNewChildren( + role: String?, + index: Int, + concepts: List, + ): List = TODO("Not yet implemented") + override fun getAllChildrenAsFlow(): Flow = TODO("Not yet implemented") + override fun getAllProperties(): List> = TODO("Not yet implemented") + override fun getAllReferenceTargetRefs(): List> = TODO("Not yet implemented") + override fun getAllReferenceTargetRefsAsFlow(): Flow> = TODO("Not yet implemented") + override fun getAllReferenceTargets(): List> = TODO("Not yet implemented") + override fun getAllReferenceTargetsAsFlow(): Flow> = TODO("Not yet implemented") + override fun getChildren(link: IChildLink): Iterable = TODO("Not yet implemented") + override fun getChildrenAsFlow(role: IChildLink): Flow = TODO("Not yet implemented") + override fun getContainmentLink(): IChildLink? = TODO("Not yet implemented") + override fun getDescendantsAsFlow(includeSelf: Boolean): Flow = TODO("Not yet implemented") + override fun getOriginalReference(): String? = TODO("Not yet implemented") + override fun getParentAsFlow(): Flow = TODO("Not yet implemented") + override fun getPropertyLinks(): List = TODO("Not yet implemented") + override fun getPropertyValue(property: IProperty): String? = TODO("Not yet implemented") + override fun getPropertyValueAsFlow(role: IProperty): Flow = TODO("Not yet implemented") + override fun getReferenceLinks(): List = TODO("Not yet implemented") + override fun getReferenceTarget(link: IReferenceLink): INode? = TODO("Not yet implemented") + override fun getReferenceTargetAsFlow(role: IReferenceLink): Flow = TODO("Not yet implemented") + override fun getReferenceTargetRef(role: IReferenceLink): INodeReference? = TODO("Not yet implemented") + override fun getReferenceTargetRef(role: String): INodeReference? = TODO("Not yet implemented") + override fun getReferenceTargetRefAsFlow(role: IReferenceLink): Flow = TODO("Not yet implemented") - override fun moveChild(role: IChildLink, index: Int, child: INode): Unit = TODO("Not yet implemented") + + override fun moveChild( + role: IChildLink, + index: Int, + child: INode, + ): Unit = TODO("Not yet implemented") + override fun removeReference(role: IReferenceLink): Unit = TODO("Not yet implemented") - override fun setPropertyValue(property: IProperty, value: String?): Unit = TODO("Not yet implemented") - override fun setReferenceTarget(link: IReferenceLink, target: INode?): Unit = TODO("Not yet implemented") - override fun setReferenceTarget(role: IReferenceLink, target: INodeReference?): Unit = TODO("Not yet implemented") - override fun setReferenceTarget(role: String, target: INodeReference?): Unit = TODO("Not yet implemented") + + override fun setPropertyValue( + property: IProperty, + value: String?, + ): Unit = TODO("Not yet implemented") + + override fun setReferenceTarget( + link: IReferenceLink, + target: INode?, + ): Unit = TODO("Not yet implemented") + + override fun setReferenceTarget( + role: IReferenceLink, + target: INodeReference?, + ): Unit = TODO("Not yet implemented") + + override fun setReferenceTarget( + role: String, + target: INodeReference?, + ): Unit = TODO("Not yet implemented") + override fun tryGetConcept(): IConcept? = TODO("Not yet implemented") + override fun usesRoleIds(): Boolean = TODO("Not yet implemented") } class AmbiguousPendingNode( val alternatives: List, ) : PendingNodeBase() { - override fun commit(location: INonExistingNode): INode { - throw UnsupportedOperationException() - } + override fun commit(location: INonExistingNode): INode = throw UnsupportedOperationException() - override fun flattenFirstAmbiguousNode(): List { - return alternatives - } + override fun flattenFirstAmbiguousNode(): List = alternatives - override fun replaceAllAmbiguousWithFirst(): INode { - return (alternatives.first() as IPendingNode).replaceAllAmbiguousWithFirst() - } + override fun replaceAllAmbiguousWithFirst(): INode = (alternatives.first() as IPendingNode).replaceAllAmbiguousWithFirst() } data class PendingNode( @@ -99,21 +191,31 @@ data class PendingNode( val properties: MutableMap = LinkedHashMap(), val references: MutableMap = LinkedHashMap(), ) : PendingNodeBase() { - override fun flattenFirstAmbiguousNode(): List { - val allChildren: List> = children.flatMap { childrenInRole -> childrenInRole.value.map { childrenInRole.key to it } } + val allChildren: List> = + children.flatMap { childrenInRole -> + childrenInRole.value.map { + childrenInRole.key to + it + } + } for ((index, child) in allChildren.withIndex()) { val flattenedChild = child.second.flattenFirstAmbiguousNode() if (flattenedChild.size <= 1) continue - val allChildrenAlternatives = flattenedChild.map { alternative -> - allChildren.take(index) + (child.first to alternative) + allChildren.drop(index + 1) - } + val allChildrenAlternatives = + flattenedChild.map { alternative -> + allChildren.take(index) + (child.first to alternative) + allChildren.drop(index + 1) + } return allChildrenAlternatives.map { PendingNode( concept = concept, - children = it.groupBy { it.first }.mapValues { it.value.map { it.second as IPendingNode }.toMutableList() }.toMutableMap(), + children = + it + .groupBy { it.first } + .mapValues { it.value.map { it.second as IPendingNode }.toMutableList() } + .toMutableMap(), properties = properties.toMutableMap(), references = references.toMutableMap() ) @@ -124,7 +226,10 @@ data class PendingNode( } override fun replaceAllAmbiguousWithFirst(): INode { - val newChildren = children.mapValues { it.value.map { it.replaceAllAmbiguousWithFirst() as IPendingNode }.toMutableList() }.toMutableMap() + val newChildren = + children + .mapValues { it.value.map { it.replaceAllAmbiguousWithFirst() as IPendingNode }.toMutableList() } + .toMutableMap() return PendingNode( concept = concept, children = newChildren, @@ -154,13 +259,14 @@ data class PendingNode( return newNode } - override fun getChildren(link: IChildLink): Iterable { - return children[link] ?: emptyList() - } + override fun getChildren(link: IChildLink): Iterable = children[link] ?: emptyList() override val reference: INodeReference get() = TODO() - override fun setPropertyValue(property: IProperty, value: String?) { + override fun setPropertyValue( + property: IProperty, + value: String?, + ) { if (value == null) { properties.remove(property) } else { @@ -168,36 +274,50 @@ data class PendingNode( } } - override fun getPropertyValue(property: IProperty): String? { - return properties[property] - } + override fun getPropertyValue(property: IProperty): String? = properties[property] - override fun getReferenceTarget(link: IReferenceLink): INode? { - return references[link] - } + override fun getReferenceTarget(link: IReferenceLink): INode? = references[link] } interface IParseTreeToAstBuilder { fun currentNode(): INode + fun buildNode(parseTreeNode: IParseTreeNode): List + fun consumeNextToken(predicate: (IParseTreeNode) -> Boolean): IParseTreeNode? - fun buildChild(role: IChildLink, childParseTree: IParseTreeNode) + + fun buildChild( + role: IChildLink, + childParseTree: IParseTreeNode, + ) + fun consumeTokens(tokens: List) } -class ParseTreeToAstBuilder(val editorEngine: EditorEngine, var node: PendingNode, val unconsumedTokens: MutableList) : IParseTreeToAstBuilder { - override fun buildChild(role: IChildLink, childParseTree: IParseTreeNode) { +class ParseTreeToAstBuilder( + val editorEngine: EditorEngine, + var node: PendingNode, + val unconsumedTokens: MutableList, +) : IParseTreeToAstBuilder { + override fun buildChild( + role: IChildLink, + childParseTree: IParseTreeNode, + ) { val alternatives = buildNode(childParseTree) - node.children.getOrPut(role) { ArrayList() }.add(if (alternatives.size == 1) alternatives.single() else AmbiguousPendingNode(alternatives)) + node.children.getOrPut(role) { ArrayList() }.add( + if (alternatives.size == + 1 + ) { + alternatives.single() + } else { + AmbiguousPendingNode(alternatives) + } + ) } - override fun currentNode(): INode { - return node - } + override fun currentNode(): INode = node - override fun buildNode(parseTreeNode: IParseTreeNode): List { - return Companion.buildNodes(editorEngine, parseTreeNode) - } + override fun buildNode(parseTreeNode: IParseTreeNode): List = Companion.buildNodes(editorEngine, parseTreeNode) override fun consumeNextToken(predicate: (IParseTreeNode) -> Boolean): IParseTreeNode? { val index = unconsumedTokens.indexOfFirst(predicate) @@ -209,8 +329,11 @@ class ParseTreeToAstBuilder(val editorEngine: EditorEngine, var node: PendingNod } companion object { - fun buildNodes(editorEngine: EditorEngine, parseTreeNode: IParseTreeNode): List { - return when (parseTreeNode) { + fun buildNodes( + editorEngine: EditorEngine, + parseTreeNode: IParseTreeNode, + ): List = + when (parseTreeNode) { is ParseTreeNode -> { val nonTerminal = parseTreeNode.rule.head when (nonTerminal) { @@ -223,20 +346,28 @@ class ParseTreeToAstBuilder(val editorEngine: EditorEngine, var node: PendingNod } listOf(childNode) } + is SubConceptsSymbol -> { buildNodes(editorEngine, parseTreeNode.children.single()) } + is ListSymbol -> { parseTreeNode.children.flatMap { buildNodes(editorEngine, it) } } - else -> throw NotImplementedError("$nonTerminal") + + else -> { + throw NotImplementedError("$nonTerminal") + } } } + is AmbiguousNode -> { listOf(AmbiguousPendingNode(parseTreeNode.trees.map { buildNodes(editorEngine, it).single() })) } - else -> throw NotImplementedError("$parseTreeNode") + + else -> { + throw NotImplementedError("$parseTreeNode") + } } - } } } diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/Bounds.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/Bounds.kt index 0644b0b2..eaeb3b16 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/Bounds.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/Bounds.kt @@ -6,10 +6,18 @@ import kotlin.math.max import kotlin.math.min @Serializable -data class Bounds(val x: Double, val y: Double, val width: Double, val height: Double) { +data class Bounds( + val x: Double, + val y: Double, + val width: Double, + val height: Double, +) { fun maxX() = x + width + fun maxY() = y + height + fun minX() = x + fun minY() = y companion object { @@ -17,19 +25,16 @@ data class Bounds(val x: Double, val y: Double, val width: Double, val height: D } } -fun Bounds.relativeTo(origin: Bounds): Bounds { - return Bounds( +fun Bounds.relativeTo(origin: Bounds): Bounds = + Bounds( x - origin.x, y - origin.y, width, height, ) -} @JvmName("union_nullable") -fun Bounds?.union(other: Bounds?): Bounds? { - return if (this == null) other else union(other) -} +fun Bounds?.union(other: Bounds?): Bounds? = if (this == null) other else union(other) fun Bounds.union(other: Bounds?): Bounds { if (other == null) return this @@ -40,14 +45,20 @@ fun Bounds.union(other: Bounds?): Bounds { return Bounds(minX, minY, maxX - minX, maxY - minY) } -fun Bounds.translated(deltaX: Double, deltaY: Double) = copy(x = x + deltaX, y = y + deltaY) -fun Bounds.expanded(delta: Double) = copy( - x = x - delta, - y = y - delta, - width = width + delta * 2.0, - height = height + delta * 2.0, -) +fun Bounds.translated( + deltaX: Double, + deltaY: Double, +) = copy(x = x + deltaX, y = y + deltaY) -fun Bounds.contains(x: Double, y: Double): Boolean { - return (minX()..maxX()).contains(x) && (minY()..maxY()).contains(y) -} +fun Bounds.expanded(delta: Double) = + copy( + x = x - delta, + y = y - delta, + width = width + delta * 2.0, + height = height + delta * 2.0, + ) + +fun Bounds.contains( + x: Double, + y: Double, +): Boolean = (minX()..maxX()).contains(x) && (minY()..maxY()).contains(y) diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CaretPositionPolicy.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CaretPositionPolicy.kt index 53c33ed2..cc78da87 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CaretPositionPolicy.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CaretPositionPolicy.kt @@ -1,11 +1,38 @@ package org.modelix.editor +import kotlinx.serialization.Serializable +import org.modelix.editor.text.frontend.getSelectableText +import org.modelix.editor.text.shared.celltree.ICellTree +import org.modelix.editor.text.shared.celltree.cellReferences import org.modelix.model.api.INode -interface ICaretPositionPolicy { - fun getBestSelection(editor: EditorComponent): CaretSelection? +@Serializable +sealed interface ICaretPositionPolicy { + fun getBestSelection(editor: FrontendEditorComponent): CaretSelection? } +@Serializable +data class CaretPositionPolicyWithIndex( + val policy: ICaretPositionPolicy, + val index: Int, +) : ICaretPositionPolicy { + constructor(cellReference: CellReference, index: Int) : this(setOf(cellReference), index) + constructor(cellReferences: Set, index: Int) : this( + CaretPositionPolicy( + avoidedCellRefs = emptySet(), + preferredCellRefs = cellReferences + ), + index + ) + + override fun getBestSelection(editor: FrontendEditorComponent): CaretSelection? = + policy.getBestSelection(editor)?.let { + val expectedPos = if (index < 0) it.layoutable.getMaxCaretPos() + index + 1 else index + if (it.end != expectedPos) CaretSelection(editor, it.layoutable, expectedPos) else it + } +} + +@Serializable data class CaretPositionPolicy( private val avoidedCellRefs: Set, private val preferredCellRefs: Set, @@ -14,28 +41,36 @@ data class CaretPositionPolicy( constructor(preferredNode: INode) : this(NodeCellReference(preferredNode.reference)) fun prefer(cellReference: CellReference) = copy(preferredCellRefs = preferredCellRefs + cellReference) - fun avoid(cellReference: CellReference) = copy(avoidedCellRefs = avoidedCellRefs + cellReference) - - fun merge(other: CaretPositionPolicy) = CaretPositionPolicy( - avoidedCellRefs + other.avoidedCellRefs, - preferredCellRefs + other.preferredCellRefs, - ) - override fun getBestSelection(editor: EditorComponent): CaretSelection? { - val candidates = preferredCellRefs - .flatMap { editor.resolveCell(it) } - .flatMap { it.descendantsAndSelf() } - .mapNotNull { editor.resolveLayoutable(it) } - - val best = candidates - .sortedByDescending { it.cell.isTabTarget() } - .sortedBy { it.cell.ancestors(true).filter { isAvoided(it) }.count() } - .firstOrNull() ?: return null + fun avoid(cellReference: CellReference) = copy(avoidedCellRefs = avoidedCellRefs + cellReference) - return CaretSelection(best, (best.cell.getSelectableText() ?: "").length) + fun merge(other: CaretPositionPolicy) = + CaretPositionPolicy( + avoidedCellRefs + other.avoidedCellRefs, + preferredCellRefs + other.preferredCellRefs, + ) + + override fun getBestSelection(editor: FrontendEditorComponent): CaretSelection? { + val candidates = + preferredCellRefs + .flatMap { editor.cellTree.resolveCell(it) } + .flatMap { it.descendantsAndSelf() } + .mapNotNull { editor.resolveLayoutable(it) } + + val best = + candidates + .sortedByDescending { it.cell.isTabTarget() } + .sortedBy { + it.cell + .ancestors(true) + .filter { isAvoided(it) } + .count() + }.firstOrNull() ?: return null + + return CaretSelection(editor, best, (best.cell.getSelectableText() ?: "").length) } - private fun isAvoided(cell: Cell) = cell.data.cellReferences.intersect(avoidedCellRefs).isNotEmpty() + private fun isAvoided(cell: ICellTree.Cell) = cell.cellReferences.intersect(avoidedCellRefs).isNotEmpty() } enum class CaretPositionType { @@ -43,22 +78,23 @@ enum class CaretPositionType { END, } -class SavedCaretPosition( +@Serializable +data class SavedCaretPosition( val previousLeafs: Set, val nextLeafs: Set, val selectedCell: CellReference?, ) : ICaretPositionPolicy { - constructor(selectedCell: Cell) : this( - selectedCell.previousLeafs(false).mapNotNull { it.data.cellReferences.firstOrNull() }.toSet(), - selectedCell.nextLeafs(false).mapNotNull { it.data.cellReferences.firstOrNull() }.toSet(), - selectedCell.data.cellReferences.firstOrNull(), + constructor(selectedCell: ICellTree.Cell) : this( + selectedCell.previousLeafs(false).mapNotNull { it.cellReferences.firstOrNull() }.toSet(), + selectedCell.nextLeafs(false).mapNotNull { it.cellReferences.firstOrNull() }.toSet(), + selectedCell.cellReferences.firstOrNull(), ) - override fun getBestSelection(editor: EditorComponent): CaretSelection? { + override fun getBestSelection(editor: FrontendEditorComponent): CaretSelection? { if (selectedCell != null) { val resolvedCell = editor.resolveCell(selectedCell).firstOrNull()?.layoutable() if (resolvedCell != null) { - return CaretSelection(resolvedCell, resolvedCell.getMaxCaretPos()) + return CaretSelection(editor, resolvedCell, resolvedCell.getMaxCaretPos()) } } @@ -68,21 +104,21 @@ class SavedCaretPosition( val centerCells = leftCell.nextLeafs(false).takeWhile { it != rightCell }.mapNotNull { it.layoutable() } val lastCell = centerCells.lastOrNull() if (lastCell != null) { - return CaretSelection(lastCell, lastCell.getMaxCaretPos()) + return CaretSelection(editor, lastCell, lastCell.getMaxCaretPos()) } } if (leftCell != null) { val layoutable = leftCell.layoutable() if (layoutable != null) { - return CaretSelection(layoutable, layoutable.getMaxCaretPos()) + return CaretSelection(editor, layoutable, layoutable.getMaxCaretPos()) } } if (rightCell != null) { val layoutable = rightCell.layoutable() if (layoutable != null) { - return CaretSelection(layoutable, 0) + return CaretSelection(editor, layoutable, 0) } } @@ -90,8 +126,16 @@ class SavedCaretPosition( } companion object { - fun saveAndRun(editor: EditorComponent, body: () -> Unit): SavedCaretPosition? { - val savedCaretPosition = editor.getSelection()?.getSelectedCells()?.firstOrNull()?.let { SavedCaretPosition(it) } + fun saveAndRun( + editor: FrontendEditorComponent, + body: () -> Unit, + ): SavedCaretPosition? { + val savedCaretPosition = + editor + .getSelection() + ?.getSelectedCells() + ?.firstOrNull() + ?.let { SavedCaretPosition(it) } body() return savedCaretPosition } 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 16a7ecb9..b1e6ab62 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CaretSelection.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CaretSelection.kt @@ -1,177 +1,162 @@ package org.modelix.editor -import org.modelix.model.api.INode -import org.modelix.parser.ConstantSymbol -import org.modelix.parser.IParseTreeNode +import org.modelix.editor.text.frontend.editorComponent +import org.modelix.editor.text.frontend.getSelectableText +import org.modelix.editor.text.shared.celltree.ICellTree +import org.modelix.editor.text.shared.celltree.cellReferences import kotlin.math.max import kotlin.math.min -class CaretSelection(val layoutable: LayoutableCell, val start: Int, val end: Int, val desiredXPosition: Int? = null) : Selection() { - constructor(cell: LayoutableCell, pos: Int) : this(cell, pos, pos) - constructor(cell: LayoutableCell, pos: Int, desiredXPosition: Int?) : this(cell, pos, pos, desiredXPosition) +class CaretSelection( + val editor: FrontendEditorComponent, + val layoutable: LayoutableCell, + val start: Int, + val end: Int, + val desiredXPosition: Int? = null, +) : Selection() { + constructor(editor: FrontendEditorComponent, cell: LayoutableCell, pos: Int) : this(editor, cell, pos, pos) + constructor( + editor: FrontendEditorComponent, + cell: LayoutableCell, + pos: Int, + desiredXPosition: Int?, + ) : this(editor, cell, pos, pos, desiredXPosition) + constructor(cell: LayoutableCell, pos: Int) : this(cell.cell.editorComponent, cell, pos, pos) + constructor(cell: LayoutableCell, pos: Int, desiredXPosition: Int?) : this(cell.cell.editorComponent, cell, pos, pos, desiredXPosition) init { require(start >= 0) { "invalid start: $start" } require(end >= 0) { "invalid end: $start" } } - override fun getSelectedCells(): List { - return listOf(layoutable.cell) - } + fun getSelectedTextRange() = min(start, end) until max(start, end) + + override fun getSelectedCells(): List = listOf(layoutable.cell) override fun isValid(): Boolean { - val editor = getEditor() ?: return false val visibleText = editor.getRootCell().layout val ownText = layoutable.getLine()?.getText() return visibleText === ownText } - private fun reResolveLayoutable(editor: EditorComponent): LayoutableCell? { - return layoutable.cell.data.cellReferences.asSequence() + private fun reResolveLayoutable(editor: FrontendEditorComponent): LayoutableCell? = + layoutable.cell.cellReferences + .asSequence() .mapNotNull { editor.resolveCell(it).firstOrNull() } - .firstOrNull()?.layoutable() - } + .firstOrNull() + ?.layoutable() - override fun update(editor: EditorComponent): Selection? { + override fun update(editor: FrontendEditorComponent): Selection? { val newLayoutable = reResolveLayoutable(editor) ?: return null val textLength = newLayoutable.getLength() - return CaretSelection(newLayoutable, start.coerceAtMost(textLength), end.coerceAtMost(textLength)) + return CaretSelection(editor, newLayoutable, start.coerceAtMost(textLength), end.coerceAtMost(textLength)) } - override fun processKeyDown(event: JSKeyboardEvent): Boolean { - val editor = getEditor() ?: throw IllegalStateException("Not attached to any editor") + override suspend fun processKeyDown(event: JSKeyboardEvent): Boolean { val knownKey = event.knownKey when (knownKey) { KnownKeys.ArrowLeft -> { if (end > 0) { if (event.modifiers.shift) { - editor.changeSelection(CaretSelection(layoutable, start, end - 1)) + editor.doChangeSelection(CaretSelection(editor, layoutable, start, end - 1)) } else { - editor.changeSelection(CaretSelection(layoutable, end - 1)) + editor.doChangeSelection(CaretSelection(editor, layoutable, end - 1)) } } else { - val previous = layoutable.getSiblingsInText(next = false) - .filterIsInstance() - .find { it.cell.getSelectableText() != null } + val previous = + layoutable + .getSiblingsInText(next = false) + .filterIsInstance() + .find { it.cell.getSelectableText() != null } if (previous != null) { if (event.modifiers.shift) { val commonAncestor = layoutable.cell.commonAncestor(previous.cell) val selectableAncestor = commonAncestor.ancestors(true).filter { it.isSelectable() }.firstOrNull() - selectableAncestor?.let { editor.changeSelection(CellSelection(it, true, this)) } + selectableAncestor?.let { editor.doChangeSelection(CellSelection(editor, it, true, this)) } } else { - editor.changeSelection(CaretSelection(previous, previous.cell.getMaxCaretPos())) + editor.doChangeSelection(CaretSelection(editor, previous, previous.cell.getMaxCaretPos())) } } } } + KnownKeys.ArrowRight -> { if (end < (layoutable.cell.getSelectableText()?.length ?: 0)) { if (event.modifiers.shift) { - editor.changeSelection(CaretSelection(layoutable, start, end + 1)) + editor.doChangeSelection(CaretSelection(editor, layoutable, start, end + 1)) } else { - editor.changeSelection(CaretSelection(layoutable, end + 1)) + editor.doChangeSelection(CaretSelection(editor, layoutable, end + 1)) } } else { - val next = layoutable.getSiblingsInText(next = true) - .filterIsInstance() - .find { it.cell.getSelectableText() != null } + val next = + layoutable + .getSiblingsInText(next = true) + .filterIsInstance() + .find { it.cell.getSelectableText() != null } if (next != null) { if (event.modifiers.shift) { val commonAncestor = layoutable.cell.commonAncestor(next.cell) val selectableAncestor = commonAncestor.ancestors(true).filter { it.isSelectable() }.firstOrNull() - selectableAncestor?.let { editor.changeSelection(CellSelection(it, false, this)) } + selectableAncestor?.let { editor.doChangeSelection(CellSelection(editor, it, false, this)) } } else { - editor.changeSelection(CaretSelection(next, 0)) + editor.doChangeSelection(CaretSelection(editor, next, 0)) } } } } + KnownKeys.ArrowDown -> { selectNextPreviousLine(true) } + KnownKeys.ArrowUp -> { if (event.modifiers.meta) { - layoutable.cell.let { editor.changeSelection(CellSelection(it, true, this)) } + layoutable.cell.let { editor.doChangeSelection(CellSelection(editor, it, true, this)) } } else { selectNextPreviousLine(false) } } + KnownKeys.Tab -> { - for (c in if (event.modifiers.shift) layoutable.cell.previousCells() else layoutable.cell.nextCells()) { - if (c.isTabTarget()) { - val l = c.layoutable() - if (l != null) { - editor.changeSelection(CaretSelection(l, 0)) - break - } - } - val action = c.getProperty(CellActionProperties.show) - if (action != null) { - // cannot tab into nested optionals because the parent optional will disappear - if (!c.ancestors(true).any { it.getProperty(CommonCellProperties.isForceShown) }) { - editor.state.forceShowOptionals.clear() - action.executeAndUpdateSelection(editor) - break - } - } - } + editor.serviceCall { navigateTab(editor.editorId, layoutable.cell.getId(), forward = !event.modifiers.shift) } } + KnownKeys.Delete, KnownKeys.Backspace -> { if (start == end) { - val posToDelete = when (knownKey) { - KnownKeys.Delete -> end - KnownKeys.Backspace -> (end - 1) - else -> throw RuntimeException("Cannot happen") - } + val posToDelete = + when (knownKey) { + KnownKeys.Delete -> end + KnownKeys.Backspace -> (end - 1) + else -> throw RuntimeException("Cannot happen") + } val legalRange = 0 until (layoutable.cell.getSelectableText()?.length ?: 0) if (legalRange.contains(posToDelete)) { - replaceText(posToDelete..posToDelete, "", editor, true) + replaceText(posToDelete until posToDelete, "", editor, true) } else { - val deleteAction = layoutable.cell.ancestors(true) - .mapNotNull { it.data.properties[CellActionProperties.delete] } - .firstOrNull { it.isApplicable() } - if (deleteAction != null) { - deleteAction.executeAndUpdateSelection(editor) + val savedCaretPosition = SavedCaretPosition(layoutable.cell) + editor.serviceCall { + val updateData = executeDelete(editor.editorId, layoutable.cell.getId()) + updateData.copy( + selectionChange = updateData.selectionChange ?: savedCaretPosition + ) } } } else { replaceText(min(start, end) until max(start, end), "", editor, true) } } + KnownKeys.Enter -> { - val actionOnSelectedCell = layoutable.cell.getProperty(CellActionProperties.insert)?.takeIf { it.isApplicable() } - if (actionOnSelectedCell != null) { - actionOnSelectedCell.executeAndUpdateSelection(editor) - } else { - var previousLeaf: Cell? = layoutable.cell - while (previousLeaf != null) { - val nextLeaf = previousLeaf.nextLeaf { it.isVisible() } - val actions = getBordersBetween(previousLeaf.rightBorder(), nextLeaf?.leftBorder()) - .filter { it.isLeft } - .mapNotNull { it.cell.getProperty(CellActionProperties.insert) } - .distinct() - .filter { it.isApplicable() } - // TODO resolve conflicts if multiple actions are applicable - val action = actions.firstOrNull() - if (action != null) { - action.executeAndUpdateSelection(editor) - break - } - previousLeaf = nextLeaf - } - } + editor.serviceCall { executeInsert(editor.editorId, layoutable.cell.getId()) } } + else -> { val typedText = event.typedText if (!typedText.isNullOrEmpty()) { if (typedText == " " && event.modifiers.ctrl) { - if (event.modifiers.shift) { - triggerParserCompletion() - } else { - triggerCodeCompletion() - } + editor.serviceCall { triggerCodeCompletion(editor.editorId, layoutable.cell.getId(), min(start, end)) } } else { - processTypedText(typedText, editor) + editor.serviceCall { processTypedText(editor.editorId, layoutable.cell.getId(), getSelectedTextRange(), typedText) } } } } @@ -180,158 +165,33 @@ class CaretSelection(val layoutable: LayoutableCell, val start: Int, val end: In return true } - fun selectNextPreviousLine(next: Boolean) { - createNextPreviousLineSelection(next, desiredXPosition ?: getAbsoluteX()) - ?.let { getEditor()?.changeSelection(it) } + suspend fun processTypedText(typedText: String) { + editor.serviceCall { processTypedText(editor.editorId, layoutable.cell.getId(), getSelectedTextRange(), typedText) } } - fun processTypedText(typedText: String, editor: EditorComponent) { - val oldText = layoutable.cell.getSelectableText() ?: "" - val range = min(start, end) until max(start, end) - val textLength = oldText.length - val leftTransform = start == end && end == 0 - val rightTransform = start == end && end == textLength - if (leftTransform || rightTransform) { - // if (replaceText(range, typedText, editor, false)) return - - val completionPosition = if (leftTransform) CompletionPosition.LEFT else CompletionPosition.RIGHT - val providers = ( - if (completionPosition == CompletionPosition.LEFT) { - layoutable.cell.getActionsBefore() - } else { - layoutable.cell.getActionsAfter() - } - ).toList() - val params = CodeCompletionParameters(editor, typedText) - val matchingActions = editor.runRead { - val actions = providers.flatMap { it.flattenApplicableActions(params) } - actions - .filter { it.getCompletionPattern().startsWith(typedText) } - .applyShadowing() - } - if (matchingActions.isNotEmpty()) { - if (matchingActions.size == 1 && matchingActions.first().getCompletionPattern() == typedText) { - matchingActions.first().executeAndUpdateSelection(editor) - return - } - editor.showCodeCompletionMenu( - anchor = layoutable, - position = completionPosition, - entries = providers, - pattern = typedText, - caretPosition = typedText.length, - ) - return - } - } - replaceText(range, typedText, editor, true) + fun selectNextPreviousLine(next: Boolean) { + createNextPreviousLineSelection(next, desiredXPosition ?: getAbsoluteX()) + ?.let { editor?.doChangeSelection(it) } } fun getAbsoluteX() = layoutable.getX() + end fun getTextBeforeCaret() = (layoutable.cell.getSelectableText() ?: "").substring(0, end) - fun triggerCodeCompletion() { - val editor = getEditor() ?: throw IllegalStateException("Not attached to any editor") - val actionProviders = layoutable.cell.getSubstituteActions().toList() - editor.showCodeCompletionMenu( - anchor = layoutable, - position = CompletionPosition.CENTER, - entries = actionProviders, - pattern = layoutable.cell.getSelectableText() ?: "", - caretPosition = end, - ) - } - - fun triggerParserCompletion() { - val editor = checkNotNull(getEditor()) { "Not attached to any editor" } - val engine = checkNotNull(editor.engine) { "EditorEngine not available" } - val selectedNode = layoutable.cell.ancestors(true) - .mapNotNull { it.getProperty(CommonCellProperties.node) }.firstOrNull() ?: return - // TODO cell should have a provider for parser based completions - val text = layoutable.cell.getSelectableText() ?: "" // TODO include all cells of the node - val expectedConcept = selectedNode.expectedConcept() ?: return - var parseTrees: List = engine.parse(text, expectedConcept, false) - if (parseTrees.isEmpty()) parseTrees = engine.parse(text + ConstantSymbol.CARET.text, expectedConcept, true) - var asts: List = parseTrees - .flatMap { ParseTreeToAstBuilder.buildNodes(engine, it) } - - var previousSize: Int - do { - previousSize = asts.size - asts = asts.flatMap { (it as IPendingNode).flattenFirstAmbiguousNode() } - } while (asts.size != previousSize && asts.size < 1000) - - // .map { it.replaceAllAmbiguousWithFirst() as IPendingNode } - val actions = asts.map { ast -> - object : ICodeCompletionAction { - val rendered = engine.createCell(EditorState(), ast) - override fun execute(editor: EditorComponent): ICaretPositionPolicy? { - val newNode = editor.runWrite { - (ast as IPendingNode).commit(selectedNode) - } - return CaretPositionPolicy(newNode) - } - - override fun getMatchingText(): String { - return rendered.layout.toString() - } - - override fun getDescription(): String { - return "" - } - } - }.map { it.asProvider() }.toList() - editor.showCodeCompletionMenu( - anchor = layoutable, - position = CompletionPosition.CENTER, - entries = actions, - pattern = layoutable.cell.getSelectableText() ?: "", - caretPosition = end, - ) - } - fun getCurrentCellText() = layoutable.cell.getSelectableText() ?: "" - fun replaceText(newText: String): Boolean { - return replaceText(0 until getCurrentCellText().length, newText, layoutable.cell.editorComponent!!, false) - } - - private fun replaceText(range: IntRange, replacement: String, editor: EditorComponent, triggerCompletion: Boolean): Boolean { - val oldText = getCurrentCellText() - val newText = oldText.replaceRange(range, replacement) - - if (triggerCompletion) { - // complete immediately if there is a single matching action - val providers = layoutable.cell.getSubstituteActions() - val params = CodeCompletionParameters(editor, newText) - val actions = editor.runRead { providers.flatMap { it.flattenApplicableActions(params) }.toList() } - val matchingActions = actions - .filter { it.getTokens().consumeForAutoApply(newText)?.length == 0 } - .applyShadowing() - val singleAction = matchingActions.singleOrNull() - if (singleAction != null) { - editor.runWrite { - singleAction.executeAndUpdateSelection(editor) - editor.state.clearTextReplacement(layoutable) - } - return true - } - } + suspend fun replaceText(newText: String): Boolean = replaceText(0 until getCurrentCellText().length, newText, editor, false) - val replaceTextActions = layoutable.cell.centerAlignedHierarchy().mapNotNull { it.getProperty(CellActionProperties.replaceText) } - for (action in replaceTextActions) { - if (action.isValid(newText) && action.replaceText(editor, range, replacement, newText)) { - editor.selectAfterUpdate { - reResolveLayoutable(editor)?.let { CaretSelection(it, range.first + replacement.length) } - } - return true - } - } - return false - } - - fun getEditor(): EditorComponent? = layoutable.cell.editorComponent + private suspend fun replaceText( + range: IntRange, + replacement: String, + editor: FrontendEditorComponent, + triggerCompletion: Boolean, + ): Boolean = + editor + .serviceCall { + replaceText(editor.editorId, layoutable.cell.getId(), range, replacement, triggerCompletion) + }.result override fun equals(other: Any?): Boolean { if (this === other) return true @@ -339,6 +199,7 @@ class CaretSelection(val layoutable: LayoutableCell, val start: Int, val end: In other as CaretSelection + if (editor != other.editor) return false if (layoutable != other.layoutable) return false if (start != other.start) return false if (end != other.end) return false @@ -347,7 +208,8 @@ class CaretSelection(val layoutable: LayoutableCell, val start: Int, val end: In } override fun hashCode(): Int { - var result = layoutable.hashCode() + var result = editor.hashCode() + result = 31 * result + layoutable.hashCode() result = 31 * result + start result = 31 * result + end return result @@ -358,29 +220,49 @@ class CaretSelection(val layoutable: LayoutableCell, val start: Int, val end: In return text.substring(0 until end) + "|" + text.substring(end) } - private fun createNextPreviousLineSelection(next: Boolean, x: Int): CaretSelection? { + private fun createNextPreviousLineSelection( + next: Boolean, + x: Int, + ): CaretSelection? { val line: TextLine = layoutable.getLine() ?: return null val text: LayoutedText = line.getText() ?: return null val lines = text.lines.asSequence() - val nextPrevLines = if (next) { - lines.dropWhile { it != line }.drop(1) - } else { - lines.takeWhile { it != line }.toList().reversed().asSequence() - } + val nextPrevLines = + if (next) { + lines.dropWhile { it != line }.drop(1) + } else { + lines + .takeWhile { it != line } + .toList() + .reversed() + .asSequence() + } return nextPrevLines.mapNotNull { it.createBestMatchingCaretSelection(x) }.firstOrNull() } -} -fun TextLine.createBestMatchingCaretSelection(x: Int): CaretSelection? { - var currentOffset = 0 - for (layoutable in words) { - val length = layoutable.getLength() - val range = currentOffset..(currentOffset + length) - if (layoutable is LayoutableCell) { - if (x < range.first) return CaretSelection(layoutable, 0, desiredXPosition = x) - if (range.contains(x)) return CaretSelection(layoutable, (x - range.first).coerceAtMost(layoutable.cell.getMaxCaretPos()), desiredXPosition = x) + fun TextLine.createBestMatchingCaretSelection(x: Int): CaretSelection? { + var currentOffset = 0 + for (layoutable in words) { + val length = layoutable.getLength() + val range = currentOffset..(currentOffset + length) + if (layoutable is LayoutableCell) { + if (x < range.first) return CaretSelection(editor, layoutable, 0, desiredXPosition = x) + if (range.contains( + x + ) + ) { + return CaretSelection( + editor, + layoutable, + (x - range.first).coerceAtMost(layoutable.cell.getMaxCaretPos()), + desiredXPosition = x + ) + } + } + currentOffset += length + } + return words.filterIsInstance().lastOrNull()?.let { + CaretSelection(editor, it, it.cell.getMaxCaretPos(), desiredXPosition = x) } - currentOffset += length } - return words.filterIsInstance().lastOrNull()?.let { CaretSelection(it, it.cell.getMaxCaretPos(), desiredXPosition = x) } } diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CaretSelectionView.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CaretSelectionView.kt index 27f66ea2..9d7f69ce 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CaretSelectionView.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CaretSelectionView.kt @@ -4,11 +4,14 @@ import kotlinx.html.TagConsumer import kotlinx.html.classes import kotlinx.html.div import kotlinx.html.style +import org.modelix.editor.text.frontend.getVisibleText import kotlin.math.max import kotlin.math.min -class CaretSelectionView(selection: CaretSelection, val editor: EditorComponent) : SelectionView(selection) { - +class CaretSelectionView( + selection: CaretSelection, + val editor: FrontendEditorComponent, +) : SelectionView(selection) { private fun hasRange() = selection.start != selection.end override fun produceHtml(consumer: TagConsumer) { @@ -21,7 +24,10 @@ class CaretSelectionView(selection: CaretSelection, val editor: EditorComponent) } div("caret own") { style = "position: absolute" - val textLength = selection.layoutable.cell.getVisibleText()?.length ?: 0 + val textLength = + selection.layoutable.cell + .getVisibleText() + ?.length ?: 0 if (textLength == 0) { // A typical case is a StringLiteral editor for an empty string. // There is no space around the empty text cell. @@ -55,11 +61,21 @@ class CaretSelectionView(selection: CaretSelection, val editor: EditorComponent) } companion object { - fun updateCaretBounds(textElement: IVirtualDom.HTMLElement, caretPos: Int, coordinatesElement: IVirtualDom.HTMLElement?, caretDom: IVirtualDom.HTMLElement) { + fun updateCaretBounds( + textElement: IVirtualDom.HTMLElement, + caretPos: Int, + coordinatesElement: IVirtualDom.HTMLElement?, + caretDom: IVirtualDom.HTMLElement, + ) { updateCaretBounds(textElement, caretPos, coordinatesElement?.getOuterBounds() ?: Bounds.ZERO, caretDom) } - fun updateCaretBounds(textElement: IVirtualDom.HTMLElement, caretPos: Int, relativeTo: Bounds, caretDom: IVirtualDom.HTMLElement) { + fun updateCaretBounds( + textElement: IVirtualDom.HTMLElement, + caretPos: Int, + relativeTo: Bounds, + caretDom: IVirtualDom.HTMLElement, + ) { val textBoundsUtil = TextBoundsUtil(textElement, relativeTo) val textBounds = textBoundsUtil.getTextBounds() val text = textBoundsUtil.getText() @@ -74,21 +90,33 @@ class CaretSelectionView(selection: CaretSelection, val editor: EditorComponent) } } -private class TextBoundsUtil(val dom: IVirtualDom.HTMLElement, val relativeTo: Bounds = Bounds.ZERO) { +private class TextBoundsUtil( + val dom: IVirtualDom.HTMLElement, + val relativeTo: Bounds = Bounds.ZERO, +) { fun getText(): String = dom.innerText() + fun getTextLength() = getText().length + fun getTextBounds() = dom.getInnerBounds().relativeTo(relativeTo) + fun getTextWidth() = getTextBounds().width + fun getTextHeight() = getTextBounds().height + fun getCharWidth() = getTextWidth() / getTextLength() - fun getCaretX(pos: Int) = getTextBounds().let { - val charWidth = it.width / getTextLength() - it.x + pos * charWidth - } - fun getSubstringBounds(range: IntRange) = getTextBounds().let { - val charWidth = it.width / getTextLength() - val minX = it.x + range.first * charWidth - val maxX = it.x + (range.last + 1) * charWidth - it.copy(x = minX, width = maxX - minX) - } + + fun getCaretX(pos: Int) = + getTextBounds().let { + val charWidth = it.width / getTextLength() + it.x + pos * charWidth + } + + fun getSubstringBounds(range: IntRange) = + getTextBounds().let { + val charWidth = it.width / getTextLength() + val minX = it.x + range.first * charWidth + val maxX = it.x + (range.last + 1) * charWidth + it.copy(x = minX, width = maxX - minX) + } } diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellCreationContext.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellCreationContext.kt index 14f34377..8c5be58a 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellCreationContext.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellCreationContext.kt @@ -1,3 +1,6 @@ package org.modelix.editor -data class CellCreationContext(val editorEngine: EditorEngine, val editorState: EditorState) +data class CellCreationContext( + val editorEngine: EditorEngine, + val cellTreeState: CellTreeState, +) diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellNavigationUtils.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellNavigationUtils.kt index b6df07f3..3cf9477a 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellNavigationUtils.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellNavigationUtils.kt @@ -1,36 +1,32 @@ package org.modelix.editor -fun Cell.nextCells(): Sequence { - return nextSiblings().flatMap { it.descendantsAndSelf() } + (parent?.let { sequenceOf(it) + it.nextCells() } ?: emptySequence()) -} +fun Cell.nextCells(): Sequence = + nextSiblings().flatMap { + it.descendantsAndSelf() + } + (getParent()?.let { sequenceOf(it) + it.nextCells() } ?: emptySequence()) -fun Cell.previousCells(): Sequence { - return previousSiblings().flatMap { it.descendantsAndSelf(iterateBackwards = true) } + (parent?.let { sequenceOf(it) + it.previousCells() } ?: emptySequence()) -} +fun Cell.previousCells(): Sequence = + previousSiblings().flatMap { it.descendantsAndSelf(iterateBackwards = true) } + + (getParent()?.let { sequenceOf(it) + it.previousCells() } ?: emptySequence()) -fun Cell.previousLeafs(includeSelf: Boolean = false): Sequence { - return generateSequence(this) { it.previousLeaf() }.drop(if (includeSelf) 0 else 1) -} +fun Cell.previousLeafs(includeSelf: Boolean = false): Sequence = + generateSequence(this) { + it.previousLeaf() + }.drop(if (includeSelf) 0 else 1) -fun Cell.nextLeafs(includeSelf: Boolean = false): Sequence { - return generateSequence(this) { it.nextLeaf() }.drop(if (includeSelf) 0 else 1) -} +fun Cell.nextLeafs(includeSelf: Boolean = false): Sequence = generateSequence(this) { it.nextLeaf() }.drop(if (includeSelf) 0 else 1) -fun Cell.previousLeaf(condition: (Cell) -> Boolean): Cell? { - return previousLeafs(false).find(condition) -} +fun Cell.previousLeaf(condition: (Cell) -> Boolean): Cell? = previousLeafs(false).find(condition) -fun Cell.nextLeaf(condition: (Cell) -> Boolean): Cell? { - return nextLeafs(false).find(condition) -} +fun Cell.nextLeaf(condition: (Cell) -> Boolean): Cell? = nextLeafs(false).find(condition) fun Cell.previousLeaf(): Cell? { - val sibling = previousSibling() ?: return parent?.previousLeaf() + val sibling = previousSibling() ?: return getParent()?.previousLeaf() return sibling.lastLeaf() } fun Cell.nextLeaf(): Cell? { - val sibling = nextSibling() ?: return parent?.nextLeaf() + val sibling = nextSibling() ?: return getParent()?.nextLeaf() return sibling.firstLeaf() } @@ -44,42 +40,51 @@ fun Cell.lastLeaf(): Cell { return if (children.isEmpty()) this else children.last().lastLeaf() } -fun Cell.previousSibling(): Cell? { - return previousSiblings().firstOrNull() -} +fun Cell.previousSibling(): Cell? = previousSiblings().firstOrNull() -fun Cell.nextSibling(): Cell? { - return nextSiblings().firstOrNull() -} +fun Cell.nextSibling(): Cell? = nextSiblings().firstOrNull() fun Cell.previousSiblings(): Sequence { - val parent = this.parent ?: return emptySequence() - return parent.getChildren().asReversed().asSequence().dropWhile { it != this }.drop(1) + val parent = this.getParent() ?: return emptySequence() + return parent + .getChildren() + .asReversed() + .asSequence() + .dropWhile { it != this } + .drop(1) } fun Cell.nextSiblings(): Sequence { - val parent = this.parent ?: return emptySequence() - return parent.getChildren().asSequence().dropWhile { it != this }.drop(1) + val parent = this.getParent() ?: return emptySequence() + return parent + .getChildren() + .asSequence() + .dropWhile { it != this } + .drop(1) } -fun Cell.descendants(iterateBackwards: Boolean = false): Sequence { - return getChildren() +fun Cell.descendants(iterateBackwards: Boolean = false): Sequence = + getChildren() .let { if (iterateBackwards) it.asReversed() else it } .asSequence() .flatMap { it.descendantsAndSelf(iterateBackwards) } -} fun Cell.descendantsAndSelf(iterateBackwards: Boolean = false): Sequence = sequenceOf(this) + descendants(iterateBackwards) -fun Cell.ancestors(includeSelf: Boolean = false) = generateSequence(if (includeSelf) this else this.parent) { it.parent } -fun Cell.commonAncestor(other: Cell): Cell = (ancestors(true) - other.ancestors(true).toSet()).last().parent!! +fun Cell.ancestors(includeSelf: Boolean = false) = generateSequence(if (includeSelf) this else this.getParent()) { it.getParent() } + +fun Cell.commonAncestor(other: Cell): Cell = (ancestors(true) - other.ancestors(true).toSet()).last().getParent()!! fun Cell.isLeaf() = this.getChildren().isEmpty() + fun Cell.isFirstChild() = previousSibling() == null + fun Cell.isLastChild() = nextSibling() == null fun Cell.leftAlignedHierarchy() = firstLeaf().ancestors(true).takeWhilePrevious { it.isFirstChild() } + fun Cell.rightAlignedHierarchy() = lastLeaf().ancestors(true).takeWhilePrevious { it.isLastChild() } + fun Cell.centerAlignedHierarchy() = leftAlignedHierarchy().toList().intersect(rightAlignedHierarchy().toSet()) /** diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellProperties.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellProperties.kt index 177a10bf..dcdf1887 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellProperties.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellProperties.kt @@ -1,55 +1,178 @@ package org.modelix.editor +import org.modelix.editor.text.shared.celltree.BooleanCellPropertyValue +import org.modelix.editor.text.shared.celltree.CellPropertyValue +import org.modelix.editor.text.shared.celltree.CellReferenceListValue +import org.modelix.editor.text.shared.celltree.StringCellPropertyValue + class CellProperties : Freezable() { private val properties: MutableMap, Any?> = HashMap() - operator fun get(key: CellPropertyKey): T { - return if (properties.containsKey(key)) properties[key] as T else key.defaultValue - } + + operator fun get(key: CellPropertyKey): T = if (properties.containsKey(key)) properties[key] as T else key.defaultValue fun isSet(key: CellPropertyKey<*>): Boolean = properties.containsKey(key) - operator fun set(key: CellPropertyKey, value: T) { + operator fun set( + key: CellPropertyKey, + value: T, + ) { checkNotFrozen() // if (isSet(key)) throw IllegalStateException("property '$key' is already set") properties[key] = value } - fun copy(): CellProperties { - return CellProperties().also { it.addAll(this) } - } + fun copy(): CellProperties = CellProperties().also { it.addAll(this) } fun addAll(from: CellProperties) { checkNotFrozen() properties += from.properties } + + fun getKeys(): Set> = properties.keys } -class CellPropertyKey(val name: String, val defaultValue: E, val inherits: Boolean = false) { +sealed class CellPropertyKey( + val name: String, + val defaultValue: E, + val inherits: Boolean = false, + val frontend: Boolean = true, +) { override fun toString() = name + + abstract fun valueToString(value: E): String? + + abstract fun valueFromString(str: String?): E + + abstract fun toSerializableValue(value: E): CellPropertyValue<*>? + + abstract fun fromSerializableValue(value: Any?): E +} + +class BooleanCellPropertyKey( + name: String, + defaultValue: Boolean, + inherits: Boolean = false, + frontend: Boolean = true, +) : CellPropertyKey(name, defaultValue, inherits, frontend) { + override fun valueToString(value: Boolean): String = value.toString() + + override fun valueFromString(str: String?): Boolean = str.toBoolean() + + override fun toSerializableValue(value: Boolean) = BooleanCellPropertyValue(value) + + override fun fromSerializableValue(value: Any?): Boolean = value as Boolean +} + +class StringCellPropertyKey( + name: String, + defaultValue: String?, + inherits: Boolean = false, + frontend: Boolean = true, +) : CellPropertyKey(name, defaultValue, inherits, frontend) { + override fun valueToString(value: String?): String? = value + + override fun valueFromString(str: String?): String? = str + + override fun toSerializableValue(value: String?) = value?.let { StringCellPropertyValue(it) } + + override fun fromSerializableValue(value: Any?) = value as String? +} + +object CellReferenceListPropertyKey : + CellPropertyKey>("cell-references", emptyList(), inherits = false, frontend = true) { + override fun valueToString(value: List): String? { + TODO("Not yet implemented") + } + + override fun valueFromString(str: String?): List { + TODO("Not yet implemented") + } + + override fun toSerializableValue(value: List): CellPropertyValue<*>? = CellReferenceListValue(value) + + override fun fromSerializableValue(value: Any?): List = value as List } -fun CellPropertyKey.from(cell: Cell) = cell.data.properties[this] +class EnumCellPropertyKey>( + name: String, + defaultValue: E, + val deserializer: (Any?) -> E, + inherits: Boolean = false, + frontend: Boolean = true, +) : CellPropertyKey(name, defaultValue, inherits, frontend) { + override fun valueToString(value: E) = value.name + + override fun valueFromString(str: String?) = if (str == null) defaultValue else deserializer(str) + + override fun toSerializableValue(value: E) = StringCellPropertyValue(value.name) + + override fun fromSerializableValue(value: Any?): E = deserializer(value) +} + +inline fun > enumCellPropertyKey( + name: String, + defaultValue: E, + inherits: Boolean = false, + frontend: Boolean = true, +): EnumCellPropertyKey = + EnumCellPropertyKey( + name, + defaultValue, + { it as? E ?: enumValueOf(it.toString()) }, + inherits, + frontend + ) + +class BackendCellPropertyKey( + name: String, + defaultValue: E, + inherits: Boolean = false, +) : CellPropertyKey(name, defaultValue, inherits, frontend = false) { + override fun valueToString(value: E): String? = throw UnsupportedOperationException("backend only") + + override fun valueFromString(str: String?): E = throw UnsupportedOperationException("backend only") + + override fun toSerializableValue(value: E): CellPropertyValue<*>? = throw UnsupportedOperationException("backend only") + + override fun fromSerializableValue(value: Any?): E = throw UnsupportedOperationException("backend only") +} + +fun CellPropertyKey.from(cell: Cell) = cell.getProperty(this) enum class ECellLayout { VERTICAL, HORIZONTAL, } +enum class ECellType { + COLLECTION, + TEXT, +} + object CommonCellProperties { - val layout = CellPropertyKey("layout", ECellLayout.HORIZONTAL) - val indentChildren = CellPropertyKey("indent-children", false) - val onNewLine = CellPropertyKey("on-new-line", false) - val noSpace = CellPropertyKey("no-space", false) - val textColor = CellPropertyKey("text-color", null, inherits = true) - val placeholderTextColor = CellPropertyKey("placeholder-text-color", "lightGray", inherits = true) - val backgroundColor = CellPropertyKey("background-color", null) - val textReplacement = CellPropertyKey("text-replacement", null) - val tabTarget = CellPropertyKey("tab-target", false) // caret is placed into the cell when navigating via TAB - val selectable = CellPropertyKey("selectable", false) - val codeCompletionText = CellPropertyKey("code-completion-text", null) // empty string hides the entry - val isForceShown = CellPropertyKey("force-shown", false) - val node = CellPropertyKey("node", null) // set on the root cell of a node + val layout = enumCellPropertyKey("layout", ECellLayout.HORIZONTAL) + val type: CellPropertyKey = enumCellPropertyKey("type", ECellType.COLLECTION) + val indentChildren = BooleanCellPropertyKey("indent-children", false) + val onNewLine = BooleanCellPropertyKey("on-new-line", false) + val noSpace = BooleanCellPropertyKey("no-space", false) + val textColor = StringCellPropertyKey("text-color", null, inherits = true) + val placeholderTextColor = StringCellPropertyKey("placeholder-text-color", "lightGray", inherits = true) + val backgroundColor = StringCellPropertyKey("background-color", null) + val textReplacement = StringCellPropertyKey("text-replacement", null) + val tabTarget = BooleanCellPropertyKey("tab-target", false) // caret is placed into the cell when navigating via TAB + val selectable = BooleanCellPropertyKey("selectable", false) + val codeCompletionText = StringCellPropertyKey("code-completion-text", null) // empty string hides the entry + val isForceShown = BooleanCellPropertyKey("force-shown", false) + val node = BackendCellPropertyKey("node", null) // set on the root cell of a node + val cellCall = BackendCellPropertyKey("cell-call", null) // set on the root cell of a cell call + val cellReferences = CellReferenceListPropertyKey +} + +object TextCellProperties { + val text = StringCellPropertyKey(name = "text", defaultValue = null, inherits = false, frontend = true) + val placeholderText = StringCellPropertyKey(name = "placeholderText", defaultValue = null, inherits = false, frontend = true) } fun Cell.isTabTarget() = getProperty(CommonCellProperties.tabTarget) + fun Cell.isSelectable() = getProperty(CommonCellProperties.selectable) diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellReference.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellReference.kt index 6e6cfc8f..b521bd5d 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellReference.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellReference.kt @@ -1,45 +1,84 @@ package org.modelix.editor +import kotlinx.serialization.Serializable import org.modelix.metamodel.ITypedNode import org.modelix.metamodel.ITypedProperty import org.modelix.metamodel.untyped import org.modelix.metamodel.untypedReference -import org.modelix.model.api.IChildLink +import org.modelix.model.api.IChildLinkReference 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.IPropertyReference +import org.modelix.model.api.IReferenceLinkReference /** * A cell can have multiple CellReferences. Multiple CellReferences can resolve to the same cell. */ -abstract class CellReference +@Serializable +sealed class CellReference -data class PropertyCellReference(val property: IProperty, val nodeRef: INodeReference) : CellReference() +@Serializable +data class PropertyCellReference( + val property: IPropertyReference, + val nodeRef: INodeReference, +) : CellReference() -fun EditorComponent.resolvePropertyCell(property: IProperty, nodeRef: INodeReference): Cell? = - resolveCell(PropertyCellReference(property, nodeRef)).firstOrNull() +fun FrontendEditorComponent.resolvePropertyCell( + property: IProperty, + nodeRef: INodeReference, +): Cell? = resolveCell(PropertyCellReference(property.toReference(), nodeRef)).firstOrNull() -fun EditorComponent.resolvePropertyCell(property: IProperty, node: INode): Cell? = - resolvePropertyCell(property, node.reference) +fun FrontendEditorComponent.resolvePropertyCell( + property: IProperty, + node: INode, +): Cell? = resolvePropertyCell(property, node.reference) -fun EditorComponent.resolvePropertyCell(property: IProperty, node: ITypedNode): Cell? = - resolvePropertyCell(property, node.untyped()) +fun FrontendEditorComponent.resolvePropertyCell( + property: IProperty, + node: ITypedNode, +): Cell? = resolvePropertyCell(property, node.untyped()) -fun EditorComponent.resolvePropertyCell(property: ITypedProperty<*>, node: ITypedNode): Cell? = - resolvePropertyCell(property.untyped(), node.untyped()) +fun FrontendEditorComponent.resolvePropertyCell( + property: ITypedProperty<*>, + node: ITypedNode, +): Cell? = resolvePropertyCell(property.untyped(), node.untyped()) -data class NodeCellReference(val nodeRef: INodeReference) : CellReference() +data class NodeCellReference( + val nodeRef: INodeReference, +) : CellReference() -fun EditorComponent.resolveNodeCell(nodeRef: INodeReference): Cell? = - resolveCell(NodeCellReference(nodeRef)).firstOrNull() +fun FrontendEditorComponent.resolveNodeCell(nodeRef: INodeReference): Cell? = resolveCell(NodeCellReference(nodeRef)).firstOrNull() -fun EditorComponent.resolveNodeCell(node: INode): Cell? = - resolveNodeCell(node.reference) +fun FrontendEditorComponent.resolveNodeCell(node: INode): Cell? = resolveNodeCell(node.reference) -fun EditorComponent.resolveNodeCell(node: ITypedNode): Cell? = - resolveNodeCell(node.untypedReference()) +fun FrontendEditorComponent.resolveNodeCell(node: ITypedNode): Cell? = resolveNodeCell(node.untypedReference()) -data class ChildNodeCellReference(val parentNodeRef: INodeReference, val link: IChildLink, val index: Int = 0) : CellReference() -data class SeparatorCellReference(val before: CellReference) : CellReference() -data class ReferencedNodeCellReference(val sourceNodeRef: INodeReference, val link: IReferenceLink) : CellReference() +@Serializable +data class ChildNodeCellReference( + val parentNodeRef: INodeReference, + val link: IChildLinkReference, + val index: Int = 0, +) : CellReference() + +@Serializable +data class SeparatorCellReference( + val before: CellReference, +) : CellReference() + +@Serializable +data class ReferencedNodeCellReference( + val sourceNodeRef: INodeReference, + val link: IReferenceLinkReference, +) : CellReference() + +@Serializable +data class TemplateCellReference( + val template: ICellTemplateReference, + val node: INodeReference, +) : CellReference() + +@Serializable +data class PlaceholderCellReference( + val childCellRef: TemplateCellReference, +) : CellReference() diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellSelection.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellSelection.kt index 77321517..e7230d53 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellSelection.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellSelection.kt @@ -1,88 +1,88 @@ package org.modelix.editor -data class CellSelection(val cell: Cell, val directionLeft: Boolean, val previousSelection: Selection?) : Selection() { - fun getEditor(): EditorComponent? = cell.editorComponent +import org.modelix.editor.text.frontend.getSelectableText +import org.modelix.editor.text.frontend.layout +import org.modelix.editor.text.shared.celltree.cellReferences - override fun getSelectedCells(): List { - return listOf(cell) - } +data class CellSelection( + val editor: FrontendEditorComponent, + val cell: Cell, + val directionLeft: Boolean, + val previousSelection: Selection?, +) : Selection() { + override fun getSelectedCells(): List = listOf(cell) - override fun isValid(): Boolean { - return getEditor() != null - } + override fun isValid(): Boolean = cell.isAttached() - override fun update(editor: EditorComponent): Selection? { - return cell.data.cellReferences.asSequence() + override fun update(editor: FrontendEditorComponent): Selection? = + cell.cellReferences + .asSequence() .flatMap { editor.resolveCell(it) } - .map { CellSelection(it, directionLeft, previousSelection?.update(editor)) } + .map { CellSelection(editor, it, directionLeft, previousSelection?.update(editor)) } .firstOrNull() - } - override fun processKeyDown(event: JSKeyboardEvent): Boolean { - val editor = getEditor() ?: throw IllegalStateException("Not attached to any editor") + override suspend fun processKeyDown(event: JSKeyboardEvent): Boolean { when (event.knownKey) { KnownKeys.ArrowUp -> { if (event.modifiers.meta) { - cell.ancestors().firstOrNull { it.getProperty(CommonCellProperties.selectable) } - ?.let { editor.changeSelection(CellSelection(it, directionLeft, this)) } + cell + .ancestors() + .firstOrNull { it.getProperty(CommonCellProperties.selectable) } + ?.let { editor.doChangeSelection(CellSelection(editor, it, directionLeft, this)) } } else { unwrapCaretSelection()?.selectNextPreviousLine(false) } } + KnownKeys.ArrowDown -> { if (event.modifiers == Modifiers.META && previousSelection != null) { - editor.changeSelection(previousSelection) + editor.doChangeSelection(previousSelection) } else { unwrapCaretSelection()?.selectNextPreviousLine(true) } } + KnownKeys.ArrowLeft, KnownKeys.ArrowRight -> { if (event.modifiers == Modifiers.SHIFT) { val isLeft = event.knownKey == KnownKeys.ArrowLeft if (isLeft == directionLeft) { - cell.ancestors().firstOrNull { it.isSelectable() } - ?.let { editor.changeSelection(CellSelection(it, directionLeft, this)) } + cell + .ancestors() + .firstOrNull { it.isSelectable() } + ?.let { editor.doChangeSelection(CellSelection(editor, it, directionLeft, this)) } } else { - previousSelection?.let { editor.changeSelection(it) } + previousSelection?.let { editor.doChangeSelection(it) } } } else { val caretSelection = unwrapCaretSelection() if (caretSelection != null) { - editor.changeSelection(CaretSelection(caretSelection.layoutable, caretSelection.start)) + editor.doChangeSelection(CaretSelection(editor, caretSelection.layoutable, caretSelection.start)) } else { val tabTargets = cell.descendantsAndSelf().filter { it.isTabTarget() } if (event.knownKey == KnownKeys.ArrowLeft) { - tabTargets.firstOrNull()?.layoutable() - ?.let { editor.changeSelection(CaretSelection(it, 0)) } + tabTargets + .firstOrNull() + ?.layoutable() + ?.let { editor.doChangeSelection(CaretSelection(editor, it, 0)) } } else { - tabTargets.lastOrNull()?.layoutable() - ?.let { editor.changeSelection(CaretSelection(it, it.cell.getSelectableText()?.length ?: 0)) } + tabTargets + .lastOrNull() + ?.layoutable() + ?.let { editor.doChangeSelection(CaretSelection(editor, it, it.cell.getSelectableText()?.length ?: 0)) } } } } } + else -> { val typedText = event.typedText if (!typedText.isNullOrEmpty()) { val anchor = getLayoutables().filterIsInstance().firstOrNull() if (anchor != null) { - val actionProviders = cell.getSubstituteActions().toList() if (typedText == " " && event.modifiers == Modifiers.CTRL) { - editor.showCodeCompletionMenu( - anchor = anchor, - position = CompletionPosition.CENTER, - entries = actionProviders, - pattern = "", - caretPosition = 0, - ) + editor.serviceCall { triggerCodeCompletion(editor.editorId, anchor.cell.getId(), 0) } } else { - editor.showCodeCompletionMenu( - anchor = anchor, - position = CompletionPosition.CENTER, - entries = actionProviders, - pattern = typedText, - caretPosition = typedText.length, - ) + editor.serviceCall { triggerCodeCompletion(editor.editorId, anchor.cell.getId(), typedText.length) } } } } @@ -92,15 +92,16 @@ data class CellSelection(val cell: Cell, val directionLeft: Boolean, val previou return true } - private fun unwrapCaretSelection(): CaretSelection? { - return generateSequence(this) { (it as? CellSelection)?.previousSelection } + private fun unwrapCaretSelection(): CaretSelection? = + generateSequence(this) { (it as? CellSelection)?.previousSelection } .lastOrNull() as? CaretSelection - } fun getLayoutables(): List { - val editor = getEditor() ?: return emptyList() val rootText = editor.getRootCell().layout - return cell.layout.lines.asSequence().flatMap { it.words } - .filter { it.getLine()?.getText() === rootText }.toList() + return cell.layout.lines + .asSequence() + .flatMap { it.words } + .filter { it.getLine()?.getText() === rootText } + .toList() } } diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellSelectionView.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellSelectionView.kt index 813de6d9..85a444e7 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellSelectionView.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellSelectionView.kt @@ -5,8 +5,10 @@ import kotlinx.html.div import kotlinx.html.span import kotlinx.html.style -class CellSelectionView(selection: CellSelection, val editor: EditorComponent) : SelectionView(selection) { - +class CellSelectionView( + selection: CellSelection, + val editor: FrontendEditorComponent, +) : SelectionView(selection) { override fun update() { val mainLayerBounds = editor.getMainLayer()?.getOuterBounds() ?: Bounds.ZERO val selectionDom = editor.generatedHtmlMap.getOutput(this) ?: return diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellData.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellSpecBase.kt similarity index 54% rename from projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellData.kt rename to projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellSpecBase.kt index eb355784..9797b607 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellData.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellSpecBase.kt @@ -1,8 +1,13 @@ package org.modelix.editor +import org.modelix.editor.text.frontend.layout +import org.modelix.editor.text.shared.celltree.ICellTree +import org.modelix.editor.text.shared.celltree.IMutableCellTree import org.modelix.model.api.INode -open class CellData : Freezable(), ILocalOrChildNodeCell { +sealed class CellSpecBase : + Freezable(), + ILocalOrChildNodeCell { val cellReferences: MutableList = ArrayList() val children: MutableList = ArrayList() val properties = CellProperties() @@ -11,7 +16,10 @@ open class CellData : Freezable(), ILocalOrChildNodeCell { children.add(child) } - open fun layout(buffer: TextLayouter, cell: Cell) { + open fun layout( + buffer: TextLayouter, + cell: Cell, + ) { val body: () -> Unit = { if (properties[CommonCellProperties.onNewLine]) buffer.onNewLine() if (properties[CommonCellProperties.noSpace]) buffer.noSpace() @@ -30,22 +38,35 @@ open class CellData : Freezable(), ILocalOrChildNodeCell { open fun isVisible(): Boolean = false } -fun Cell.isVisible() = data.isVisible() +fun Cell.isVisible() = + when (getProperty(CommonCellProperties.type)) { + ECellType.COLLECTION -> false + ECellType.TEXT -> true + } + +sealed interface ILocalOrChildNodeCell -interface ILocalOrChildNodeCell +class ChildSpecReference( + val childNode: INode, +) : ILocalOrChildNodeCell -class ChildDataReference(val childNode: INode) : ILocalOrChildNodeCell +class CellSpec : CellSpecBase() -class TextCellData(val text: String, private val placeholderText: String = "") : CellData() { - fun getVisibleText(cell: Cell): String { - return if (cell.getChildren().isEmpty()) { +class TextCellSpec( + val text: String, + val placeholderText: String = "", +) : CellSpecBase() { + fun getVisibleText(cell: Cell): String = + if (cell.getChildren().isEmpty()) { text.ifEmpty { placeholderText } } else { """$text<${cell.getChildren()}>""" } - } - override fun layout(buffer: TextLayouter, cell: Cell) { + override fun layout( + buffer: TextLayouter, + cell: Cell, + ) { if (properties[CommonCellProperties.onNewLine]) buffer.onNewLine() if (properties[CommonCellProperties.noSpace]) buffer.noSpace() buffer.append(LayoutableCell(cell)) @@ -56,3 +77,9 @@ class TextCellData(val text: String, private val placeholderText: String = "") : override fun isVisible(): Boolean = true } + +val ICellTree.Cell.type: ECellType get() = getProperty(CommonCellProperties.type) + +var IMutableCellTree.MutableCell.type: ECellType? + get() = getProperty(CommonCellProperties.type) + set(value) = if (value == null) removeProperty(CommonCellProperties.type) else setProperty(CommonCellProperties.type, value) 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 9764b456..be2a9841 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTemplateBuilder.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTemplateBuilder.kt @@ -33,17 +33,30 @@ import kotlin.reflect.KClass private val LOG = KotlinLogging.logger { } -open class CellTemplateBuilder(val template: CellTemplate, val concept: ConceptT, protected val nodeConverter: INodeConverter) { +open class CellTemplateBuilder( + val template: CellTemplate, + val concept: ConceptT, + protected val nodeConverter: INodeConverter, +) { val properties = CellProperties() - protected fun CellTemplate.builder(): CellTemplateBuilder { - return CellTemplateBuilder(this, this@CellTemplateBuilder.concept, nodeConverter) - } + protected fun CellTemplate.builder(): CellTemplateBuilder = + CellTemplateBuilder(this, this@CellTemplateBuilder.concept, nodeConverter) + + fun ifEmpty( + link: ITypedChildLink<*>, + body: () -> Unit, + ) = ifEmpty(link.untyped(), body) - fun ifEmpty(link: ITypedChildLink<*>, body: () -> Unit) = ifEmpty(link.untyped(), body) - fun ifNotEmpty(link: ITypedChildLink<*>, body: () -> Unit) = ifNotEmpty(link.untyped(), body) + fun ifNotEmpty( + link: ITypedChildLink<*>, + body: () -> Unit, + ) = ifNotEmpty(link.untyped(), body) - fun ifEmpty(link: IChildLink, body: () -> Unit) { + fun ifEmpty( + link: IChildLink, + body: () -> Unit, + ) { withUntypedNode { node -> if (!node.getChildren(link).iterator().hasNext()) { body() @@ -51,7 +64,10 @@ open class CellTemplateBuilder(val template: CellTe } } - fun ifNotEmpty(link: IChildLink, body: () -> Unit) { + fun ifNotEmpty( + link: IChildLink, + body: () -> Unit, + ) { withUntypedNode { node -> if (node.getChildren(link).iterator().hasNext()) { body() @@ -84,17 +100,28 @@ open class CellTemplateBuilder(val template: CellTe constant(this, body) } - fun constant(text: String, body: CellTemplateBuilder.() -> Unit = {}) { - ConstantCellTemplate(template.concept, text).builder().also(body).template.also(template::addChild) + fun constant( + text: String, + body: CellTemplateBuilder.() -> Unit = {}, + ) { + ConstantCellTemplate(template.concept, text) + .builder() + .also(body) + .template + .also(template::addChild) } - fun untypedConcept() = when (concept) { - is IConcept -> concept - is ITypedConcept -> concept.untyped() - else -> throw RuntimeException("Unknown concept type: $concept") - } + fun untypedConcept() = + when (concept) { + is IConcept -> concept + is ITypedConcept -> concept.untyped() + else -> throw RuntimeException("Unknown concept type: $concept") + } - fun conceptProperty(name: String, body: CellTemplateBuilder.() -> Unit = {}) { + fun conceptProperty( + name: String, + body: CellTemplateBuilder.() -> Unit = {}, + ) { (untypedConcept().getConceptProperty(name) ?: untypedConcept().getShortName()).constant(body) } @@ -102,8 +129,15 @@ open class CellTemplateBuilder(val template: CellTe conceptProperty("alias", body) } - fun label(text: String, body: CellTemplateBuilder.() -> Unit = {}) { - LabelCellTemplate(template.concept, text).builder().also(body).template.also(template::addChild) + fun label( + text: String, + body: CellTemplateBuilder.() -> Unit = {}, + ) { + LabelCellTemplate(template.concept, text) + .builder() + .also(body) + .template + .also(template::addChild) } fun textColor(color: String) { @@ -116,22 +150,39 @@ open class CellTemplateBuilder(val template: CellTe fun vertical(body: CellTemplateBuilder.() -> Unit = {}) { // TODO add correct layout information - CollectionCellTemplate(template.concept).builder() - .also { it.template.properties[CommonCellProperties.layout] = ECellLayout.VERTICAL }.also(body).template.also(template::addChild) + CollectionCellTemplate(template.concept) + .builder() + .also { it.template.properties[CommonCellProperties.layout] = ECellLayout.VERTICAL } + .also( + body + ).template + .also(template::addChild) } fun horizontal(body: CellTemplateBuilder.() -> Unit = {}) { // TODO add layout information - CollectionCellTemplate(template.concept).builder() - .also(body).template.also(template::addChild) + CollectionCellTemplate(template.concept) + .builder() + .also(body) + .template + .also(template::addChild) } fun optional(body: CellTemplateBuilder.() -> Unit = {}) { - OptionalCellTemplate(template.concept).builder() - .also(body).template.also(template::addChild) - } - - fun brackets(singleLine: Boolean = true, leftSymbol: String, rightSymbol: String, body: CellTemplateBuilder.() -> Unit = {}) { + OptionalCellTemplate(template.concept) + .builder() + .also(body) + .template + .also(template::addChild) + } + + fun brackets( + singleLine: Boolean = true, + leftSymbol: String, + rightSymbol: String, + body: CellTemplateBuilder.() -> Unit = { + }, + ) { if (singleLine) { constant(leftSymbol) noSpace() @@ -151,15 +202,24 @@ open class CellTemplateBuilder(val template: CellTe } } - fun parentheses(singleLine: Boolean = true, body: CellTemplateBuilder.() -> Unit = {}) { + fun parentheses( + singleLine: Boolean = true, + body: CellTemplateBuilder.() -> Unit = {}, + ) { brackets(singleLine, "(", ")", body) } - fun curlyBrackets(singleLine: Boolean = false, body: CellTemplateBuilder.() -> Unit = {}) { + fun curlyBrackets( + singleLine: Boolean = false, + body: CellTemplateBuilder.() -> Unit = {}, + ) { brackets(singleLine, "{", "}", body) } - fun angleBrackets(singleLine: Boolean = true, body: CellTemplateBuilder.() -> Unit = {}) { + fun angleBrackets( + singleLine: Boolean = true, + body: CellTemplateBuilder.() -> Unit = {}, + ) { brackets(singleLine, "<", ">", body) } @@ -168,7 +228,10 @@ open class CellTemplateBuilder(val template: CellTe curlyBrackets(false, body) } - fun squareBrackets(singleLine: Boolean = true, body: CellTemplateBuilder.() -> Unit = {}) { + fun squareBrackets( + singleLine: Boolean = true, + body: CellTemplateBuilder.() -> Unit = {}, + ) { brackets(singleLine, "[", "]", body) } @@ -190,8 +253,10 @@ open class CellTemplateBuilder(val template: CellTe } fun noSpace() { - NoSpaceCellTemplate(template.concept).builder() - .template.also(template::addChild) + NoSpaceCellTemplate(template.concept) + .builder() + .template + .also(template::addChild) } fun indented(body: CellTemplateBuilder.() -> Unit = {}) { @@ -204,7 +269,10 @@ open class CellTemplateBuilder(val template: CellTe /** * The content is foldable */ - fun foldable(foldedText: String = "...", body: CellTemplateBuilder.() -> Unit = {}) { + fun foldable( + foldedText: String = "...", + body: CellTemplateBuilder.() -> Unit = {}, + ) { // TODO horizontal(body) } @@ -226,19 +294,34 @@ open class CellTemplateBuilder(val template: CellTe fun IProperty.propertyCell(body: PropertyCellTemplateBuilder.() -> Unit = {}) { PropertyCellTemplateBuilder(PropertyCellTemplate(template.concept, this), concept, nodeConverter) - .also(body).template.also(template::addChild) + .also(body) + .template + .also(template::addChild) } - fun ITypedProperty.flagCell(text: String? = null, body: CellTemplateBuilder.() -> Unit = {}) { + fun ITypedProperty.flagCell( + text: String? = null, + body: CellTemplateBuilder.() -> Unit = {}, + ) { untyped().flagCell(text, body) } - fun IProperty.flagCell(text: String? = null, body: CellTemplateBuilder.() -> Unit = {}) { + fun IProperty.flagCell( + text: String? = null, + body: CellTemplateBuilder.() -> Unit = {}, + ) { PropertyCellTemplateBuilder(FlagCellTemplate(template.concept, this, text ?: getSimpleName()), concept, nodeConverter) - .also(body).template.also(template::addChild) + .also(body) + .template + .also(template::addChild) } - fun ITypedProperty.booleanCell(trueText: String = "true", falseText: String = "false", body: CellTemplateBuilder.() -> Unit = {}) { + fun ITypedProperty.booleanCell( + trueText: String = "true", + falseText: String = "false", + body: CellTemplateBuilder.() -> Unit = { + }, + ) { // TODO generate code completion entries for the two possible values untyped().propertyCell { readReplace { if (it == "true") trueText else falseText } @@ -247,19 +330,24 @@ open class CellTemplateBuilder(val template: CellTe } } - private fun IReferenceLink.cell(presentation: TargetNodeT.() -> String?, body: ReferenceCellTemplateBuilder.() -> Unit = {}, targetNodeConverter: INodeConverter) { + private fun IReferenceLink.cell( + presentation: TargetNodeT.() -> String?, + body: ReferenceCellTemplateBuilder.() -> Unit = { + }, + targetNodeConverter: INodeConverter, + ) { ReferenceCellTemplateBuilder( - template = ReferenceCellTemplate( - concept = template.concept, - link = this, - presentation = { - runCatching { - presentation(targetNodeConverter.fromUntyped(this)) - } - .onFailure { LOG.error(it) { "Failed computing presentation for reference target: $this (${this.concept})" } } - .getOrNull() - }, - ), + template = + ReferenceCellTemplate( + concept = template.concept, + link = this, + presentation = { + runCatching { + presentation(targetNodeConverter.fromUntyped(this)) + }.onFailure { LOG.error(it) { "Failed computing presentation for reference target: $this (${this.concept})" } } + .getOrNull() + }, + ), link = this, concept = concept, sourceNodeConverter = nodeConverter, @@ -267,12 +355,19 @@ open class CellTemplateBuilder(val template: CellTe ).also(body).template.also(template::addChild) } - fun ITypedReferenceLink.cell(presentation: TargetNodeT.() -> String?, body: ReferenceCellTemplateBuilder.() -> Unit = {}) { + fun ITypedReferenceLink.cell( + presentation: TargetNodeT.() -> String?, + body: ReferenceCellTemplateBuilder.() -> Unit = { + }, + ) { val targetNodeConverter = INodeConverter.Typed(this.getTypedTargetConcept()) this.untyped().cell(presentation, body, targetNodeConverter) } - fun IReferenceLink.cell(presentation: INode.() -> String?, body: ReferenceCellTemplateBuilder.() -> Unit = {}) { + fun IReferenceLink.cell( + presentation: INode.() -> String?, + body: ReferenceCellTemplateBuilder.() -> Unit = {}, + ) { val targetNodeConverter = INodeConverter.Untyped this.cell(presentation, body, targetNodeConverter) } @@ -283,7 +378,11 @@ open class CellTemplateBuilder(val template: CellTe fun IChildLink.cell(body: CellTemplateBuilder.() -> Unit = {}) { require(!this.isMultiple) { "Not allowed on child lists" } - ChildCellTemplate(template.concept, this).builder().also(body).template.also(template::addChild) + ChildCellTemplate(template.concept, this) + .builder() + .also(body) + .template + .also(template::addChild) } fun ITypedChildListLink<*>.vertical(body: ChildCellTemplateBuilder.() -> Unit = {}) { @@ -297,37 +396,52 @@ open class CellTemplateBuilder(val template: CellTe } } - fun ITypedChildListLink<*>.horizontal(separator: String? = ",", body: ChildCellTemplateBuilder.() -> Unit = {}) { + fun ITypedChildListLink<*>.horizontal( + separator: String? = ",", + body: ChildCellTemplateBuilder.() -> Unit = {}, + ) { this.untyped().horizontal(separator, body) } - fun IChildLink.horizontal(separator: String? = ",", body: ChildCellTemplateBuilder.() -> Unit = {}) { + fun IChildLink.horizontal( + separator: String? = ",", + body: ChildCellTemplateBuilder.() -> Unit = {}, + ) { ChildCellTemplateBuilder(ChildCellTemplate(template.concept, this), concept, nodeConverter) .also { if (separator != null) it.separator { constant(separator) } } - .also(body).template.also(template::addChild) + .also(body) + .template + .also(template::addChild) } fun modelAccess(body: ModelAccessBuilder.() -> Unit) { var setter: (String?) -> Unit = {} var getter: () -> String? = { "" } - body(object : ModelAccessBuilder { - override fun get(body: () -> String?) { - getter = body - } + body( + object : ModelAccessBuilder { + override fun get(body: () -> String?) { + getter = body + } - override fun set(body: (String?) -> Unit) { - setter = body + override fun set(body: (String?) -> Unit) { + setter = body + } } - }) + ) modelAccess(getter, setter) } - fun modelAccess(getter: () -> String?, setter: (String?) -> Unit) { + fun modelAccess( + getter: () -> String?, + setter: (String?) -> Unit, + ) { // TODO ModelAccessCellTemplate ConstantCellTemplate(template.concept, "").builder().template.also(template::addChild) } - inner class WithNodeContext(val node: NodeT) + inner class WithNodeContext( + val node: NodeT, + ) } class NotationRootCellTemplateBuilder( @@ -412,20 +526,30 @@ class ReferenceCellTemplateBuilder { fun fromUntyped(node: INode): NodeT + fun toUntyped(node: NodeT): INode - class Typed(private val nodeClass: KClass) : INodeConverter { + class Typed( + private val nodeClass: KClass, + ) : INodeConverter { constructor(concept: IConceptOfTypedNode) : this(concept.getInstanceInterface()) + override fun fromUntyped(node: INode): NodeT = node.typed(nodeClass) + override fun toUntyped(node: NodeT): INode = node.untyped() } + object Untyped : INodeConverter { override fun fromUntyped(node: INode): INode = node + override fun toUntyped(node: INode): INode = node } } @@ -434,18 +558,24 @@ interface ITypedOrUntypedNode { val node: NodeT val untypedNode: INode - class Typed(override val node: NodeT) : ITypedOrUntypedNode { + class Typed( + override val node: NodeT, + ) : ITypedOrUntypedNode { override val untypedNode: INode get() = node.untyped() } - class Untyped(override val node: INode) : ITypedOrUntypedNode { + class Untyped( + override val node: INode, + ) : ITypedOrUntypedNode { override val untypedNode: INode get() = node } } -fun > CellTemplate.builder(concept: ConceptT): CellTemplateBuilder { +fun > CellTemplate.builder( + concept: ConceptT, +): CellTemplateBuilder { require(this.concept == concept.untyped()) return CellTemplateBuilder(this, concept, INodeConverter.Typed(concept)) } diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTemplateReference.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTemplateReference.kt index 295fae9c..19c67796 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTemplateReference.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTemplateReference.kt @@ -1,12 +1,24 @@ package org.modelix.editor -import org.modelix.model.api.IConceptReference -import org.modelix.model.api.INodeReference +import kotlinx.serialization.Serializable +import org.modelix.model.api.ConceptReference -interface ICellTemplateReference +@Serializable +sealed interface ICellTemplateReference -data class RooCellTemplateReference(val conceptEditor: ConceptEditor, val subConcept: IConceptReference) : ICellTemplateReference -data class ChildCellTemplateReference(val parent: ICellTemplateReference, val index: Int) : ICellTemplateReference -data class SeparatorCellTemplateReference(val parent: ICellTemplateReference) : ICellTemplateReference +@Serializable +data class RooCellTemplateReference( + val conceptEditorId: Long, + val subConcept: ConceptReference, +) : ICellTemplateReference -data class TemplateCellReference(val template: ICellTemplateReference, val node: INodeReference) : CellReference() +@Serializable +data class ChildCellTemplateReference( + val parent: ICellTemplateReference, + val index: Int, +) : ICellTemplateReference + +@Serializable +data class SeparatorCellTemplateReference( + val parent: ICellTemplateReference, +) : ICellTemplateReference diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/EditorState.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTreeState.kt similarity index 61% rename from projectional-editor/src/commonMain/kotlin/org/modelix/editor/EditorState.kt rename to projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTreeState.kt index 09ecdccb..893aaf29 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/EditorState.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTreeState.kt @@ -1,8 +1,11 @@ package org.modelix.editor +import org.modelix.editor.text.shared.celltree.BackendCellTree +import org.modelix.editor.text.shared.celltree.cellReferences import org.modelix.incremental.TrackableMap -class EditorState { +class CellTreeState { + val cellTree = BackendCellTree() val substitutionPlaceholderPositions = TrackableMap() val forceShowOptionals = TrackableMap() val textReplacements = TrackableMap() @@ -13,11 +16,11 @@ class EditorState { textReplacements.clear() } - fun clearTextReplacement(cell: LayoutableCell): Unit = clearTextReplacement(cell.cell) - - fun clearTextReplacement(cell: Cell): Unit = cell.data.cellReferences.forEach { clearTextReplacement(it) } + fun clearTextReplacement(cell: Cell): Unit = cell.cellReferences.forEach { clearTextReplacement(it) } fun clearTextReplacement(cell: CellReference): Unit = textReplacements.remove(cell) } -class SubstitutionPlaceholderPosition(val index: Int) +class SubstitutionPlaceholderPosition( + val index: Int, +) diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/Cells.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/Cells.kt index b508a07f..c93f4f53 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/Cells.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/Cells.kt @@ -1,14 +1,18 @@ package org.modelix.editor -import org.modelix.incremental.IncrementalList +import org.modelix.editor.text.frontend.getSelectableText +import org.modelix.editor.text.shared.celltree.ICellTree +import org.modelix.editor.text.shared.celltree.IMutableCellTree interface IFreezable { fun freeze() + fun checkNotFrozen() } open class Freezable : IFreezable { private var frozen: Boolean = false + override fun freeze() { frozen = true } @@ -22,85 +26,85 @@ open class Freezable : IFreezable { } } -class Cell(val data: CellData = CellData()) : Freezable() { - private var editorComponentValue: EditorComponent? = null - var parent: Cell? = null - private val children: MutableList = ArrayList() - private var layout_ = ResettableLazy { - TextLayouter().also { data.layout(it, this) }.done() - } - val layout: LayoutedText - get() = layout_.value - val referencesIndexList: IncrementalList> by lazy { - IncrementalList.concat( - IncrementalList.of(data.cellReferences.map { it to this }), - IncrementalList.concat(children.map { it.referencesIndexList }), - ) - } - var editorComponent: EditorComponent? - get() = editorComponentValue ?: parent?.editorComponent - set(value) { - if (value != null && parent != null) throw IllegalStateException("Only allowed on the root cell") - editorComponentValue = value - } - - fun clearCachedLayout() { - layout_.reset() - } +typealias Cell = ICellTree.Cell +typealias MutableCell = IMutableCellTree.MutableCell + +// class DeprecatedCell(val data: CellSpec = CellSpec()) : Freezable() { +// private var editorComponentValue: EditorComponent? = null +// var parent: Cell? = null +// private val children: MutableList = ArrayList() +// private var layout_ = ResettableLazy { +// TextLayouter().also { data.layout(it, this) }.done() +// } +// val layout: LayoutedText +// get() = layout_.value +// val referencesIndexList: IncrementalList> by lazy { +// IncrementalList.concat( +// IncrementalList.of(data.cellReferences.map { it to this }), +// IncrementalList.concat(children.map { it.referencesIndexList }), +// ) +// } +// var editorComponent: EditorComponent? +// get() = editorComponentValue ?: parent?.editorComponent +// set(value) { +// if (value != null && parent != null) throw IllegalStateException("Only allowed on the root cell") +// editorComponentValue = value +// } +// +// fun clearCachedLayout() { +// layout_.reset() +// } +// +// override fun freeze() { +// if (isFrozen()) return +// super.freeze() +// data.freeze() +// children.forEach { it.freeze() } +// } +// +// override fun toString(): String { +// return data.cellToString(this) +// } +// +// fun addChild(child: Cell) { +// checkNotFrozen() +// require(child.parent == null) { "$child already has a parent ${child.parent}" } +// children.add(child) +// child.parent = this +// } +// +// fun removeChild(child: Cell) { +// checkNotFrozen() +// require(child.parent == this) { "$child is not a child of $this" } +// children.remove(child) +// child.parent = null +// } +// +// fun getChildren(): List = children +// +// fun getProperty(key: CellPropertyKey): T { +// return if (key.inherits && !data.properties.isSet(key)) { +// parent.let { if (it != null) it.getProperty(key) else key.defaultValue } +// } else { +// data.properties[key] +// } +// } +// +// fun rootCell(): Cell = parent?.rootCell() ?: this +// } + +fun ICellTree.Cell.getMaxCaretPos(): Int = getSelectableText()?.length ?: 0 - override fun freeze() { - if (isFrozen()) return - super.freeze() - data.freeze() - children.forEach { it.freeze() } - } - - override fun toString(): String { - return data.cellToString(this) - } - - fun addChild(child: Cell) { - require(child.parent == null) { "$child already has a parent ${child.parent}" } - children.add(child) - child.parent = this - } - - fun removeChild(child: Cell) { - require(child.parent == this) { "$child is not a child of $this" } - children.remove(child) - child.parent = null - } - - fun getChildren(): List = children - - fun getProperty(key: CellPropertyKey): T { - return if (key.inherits && !data.properties.isSet(key)) { - parent.let { if (it != null) it.getProperty(key) else key.defaultValue } - } else { - data.properties[key] - } - } - - fun rootCell(): Cell = parent?.rootCell() ?: this -} - -fun Cell.getVisibleText(): String? { - return getProperty(CommonCellProperties.textReplacement) ?: (data as? TextCellData)?.getVisibleText(this) -} -fun Cell.getSelectableText(): String? { - return getProperty(CommonCellProperties.textReplacement) ?: (data as? TextCellData)?.text -} -fun Cell.getMaxCaretPos(): Int = getSelectableText()?.length ?: 0 fun LayoutableCell.getMaxCaretPos(): Int = cell.getSelectableText()?.length ?: 0 -class ResettableLazy(private val initializer: () -> E) : Lazy { +class ResettableLazy( + private val initializer: () -> E, +) : Lazy { private var lazy: Lazy = lazy(initializer) override val value: E get() = lazy.value - override fun isInitialized(): Boolean { - return lazy.isInitialized() - } + override fun isInitialized(): Boolean = lazy.isInitialized() fun reset() { lazy = lazy(initializer) diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CodeCompletionActionWrapper.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CodeCompletionActionWrapper.kt index fad8cdfc..260428b9 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CodeCompletionActionWrapper.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CodeCompletionActionWrapper.kt @@ -1,106 +1,112 @@ package org.modelix.editor -open class CodeCompletionActionWrapper(val wrappedAction: ICodeCompletionAction) : ICodeCompletionAction by wrappedAction { - override fun shadowedBy(shadowing: ICodeCompletionAction): Boolean { - return wrappedAction.shadowedBy(if (shadowing is CodeCompletionActionWrapper) shadowing.wrappedAction else shadowing) - } +import org.modelix.editor.text.backend.BackendEditorComponent - override fun shadows(shadowed: ICodeCompletionAction): Boolean { - return wrappedAction.shadows(if (shadowed is CodeCompletionActionWrapper) shadowed.wrappedAction else shadowed) - } +open class CodeCompletionActionWrapper( + val wrappedAction: ICodeCompletionAction, +) : ICodeCompletionAction by wrappedAction { + override fun shadowedBy(shadowing: ICodeCompletionAction): Boolean = + wrappedAction.shadowedBy(if (shadowing is CodeCompletionActionWrapper) shadowing.wrappedAction else shadowing) + + override fun shadows(shadowed: ICodeCompletionAction): Boolean = + wrappedAction.shadows(if (shadowed is CodeCompletionActionWrapper) shadowed.wrappedAction else shadowed) } class CodeCompletionActionProviderWrapper( val wrappedProvider: ICodeCompletionActionProvider, val wrapAction: (CodeCompletionParameters, ICodeCompletionAction) -> ICodeCompletionAction, ) : ICodeCompletionActionProvider { - override fun getApplicableActions(parameters: CodeCompletionParameters): List { - return wrappedProvider.getApplicableActions(parameters).map { + override fun getApplicableActions(parameters: CodeCompletionParameters): List = + wrappedProvider.getApplicableActions(parameters).map { when (it) { is ICodeCompletionAction -> wrapAction(parameters, it) is ICodeCompletionActionProvider -> CodeCompletionActionProviderWrapper(it, wrapAction) else -> throw RuntimeException("Unexpected type: " + it::class) } } - } } -class CodeCompletionActionWithPostprocessor(action: ICodeCompletionAction, val after: () -> Unit) : CodeCompletionActionWrapper(action) { - override fun execute(editor: EditorComponent): ICaretPositionPolicy? { +class CodeCompletionActionWithPostprocessor( + action: ICodeCompletionAction, + val after: () -> Unit, +) : CodeCompletionActionWrapper(action) { + override fun execute(editor: BackendEditorComponent): ICaretPositionPolicy? { val policy = wrappedAction.execute(editor) after() return policy } } -class CodeCompletionActionWithCaretPolicy(action: ICodeCompletionAction, val policy: (ICaretPositionPolicy?) -> ICaretPositionPolicy?) : CodeCompletionActionWrapper(action) { - override fun execute(editor: EditorComponent): ICaretPositionPolicy? { - return policy(wrappedAction.execute(editor)) - } +class CodeCompletionActionWithCaretPolicy( + action: ICodeCompletionAction, + val policy: (ICaretPositionPolicy?) -> ICaretPositionPolicy?, +) : CodeCompletionActionWrapper(action) { + override fun execute(editor: BackendEditorComponent): ICaretPositionPolicy? = policy(wrappedAction.execute(editor)) } -class CodeCompletionActionWithMatchingText(action: ICodeCompletionAction, val overridingMatchingText: (String) -> String) : CodeCompletionActionWrapper(action) { - override fun getMatchingText(): String { - return overridingMatchingText(super.getMatchingText()) - } +class CodeCompletionActionWithMatchingText( + action: ICodeCompletionAction, + val overridingMatchingText: (String) -> String, +) : CodeCompletionActionWrapper(action) { + override fun getMatchingText(): String = overridingMatchingText(super.getMatchingText()) - override fun getTokens(): ICompletionTokenOrList { - return ConstantCompletionToken(getMatchingText()) - } + override fun getTokens(): ICompletionTokenOrList = ConstantCompletionToken(getMatchingText()) } -class CodeCompletionActionWithDescription(action: ICodeCompletionAction, val overridingDescription: String) : CodeCompletionActionWrapper(action) { - override fun getDescription(): String { - return overridingDescription - } +class CodeCompletionActionWithDescription( + action: ICodeCompletionAction, + val overridingDescription: String, +) : CodeCompletionActionWrapper(action) { + override fun getDescription(): String = overridingDescription } -class CodeCompletionActionWithTokens(action: ICodeCompletionAction, val overrideTokens: (ICompletionTokenOrList) -> ICompletionTokenOrList) : CodeCompletionActionWrapper(action) { - override fun getTokens(): ICompletionTokenOrList { - return overrideTokens(super.getTokens()) - } +class CodeCompletionActionWithTokens( + action: ICodeCompletionAction, + val overrideTokens: (ICompletionTokenOrList) -> ICompletionTokenOrList, +) : CodeCompletionActionWrapper(action) { + override fun getTokens(): ICompletionTokenOrList = overrideTokens(super.getTokens()) } -fun ICodeCompletionActionProvider.after(body: () -> Unit): CodeCompletionActionProviderWrapper { - return CodeCompletionActionProviderWrapper(this) { _, it -> +fun ICodeCompletionActionProvider.after(body: () -> Unit): CodeCompletionActionProviderWrapper = + CodeCompletionActionProviderWrapper(this) { _, it -> CodeCompletionActionWithPostprocessor(it, body) } -} -fun ICodeCompletionActionProvider.withMatchingText(text: (CodeCompletionParameters) -> String): CodeCompletionActionProviderWrapper { - return CodeCompletionActionProviderWrapper(this) { parameters, it -> +fun ICodeCompletionActionProvider.withMatchingText(text: (CodeCompletionParameters) -> String): CodeCompletionActionProviderWrapper = + CodeCompletionActionProviderWrapper(this) { parameters, it -> CodeCompletionActionWithMatchingText(it, { text(parameters) }) } -} -fun ICodeCompletionActionProvider.modifyMatchingText(text: (CodeCompletionParameters, String) -> String): CodeCompletionActionProviderWrapper { - return CodeCompletionActionProviderWrapper(this) { parameters, it -> +fun ICodeCompletionActionProvider.modifyMatchingText( + text: (CodeCompletionParameters, String) -> String, +): CodeCompletionActionProviderWrapper = + CodeCompletionActionProviderWrapper(this) { parameters, it -> CodeCompletionActionWithMatchingText(it, { text(parameters, it) }) } -} -fun ICodeCompletionActionProvider.withDescription(text: (CodeCompletionParameters) -> String): CodeCompletionActionProviderWrapper { - return CodeCompletionActionProviderWrapper(this) { parameters, it -> +fun ICodeCompletionActionProvider.withDescription(text: (CodeCompletionParameters) -> String): CodeCompletionActionProviderWrapper = + CodeCompletionActionProviderWrapper(this) { parameters, it -> CodeCompletionActionWithDescription(it, text(parameters)) } -} -fun ICodeCompletionAction.withMatchingText(text: String): CodeCompletionActionWithMatchingText { - return CodeCompletionActionWithMatchingText(this, { text }) -} +fun ICodeCompletionAction.withMatchingText(text: String): CodeCompletionActionWithMatchingText = + CodeCompletionActionWithMatchingText(this, { + text + }) -fun ICodeCompletionActionProvider.withTokens(replacement: (ICompletionTokenOrList) -> ICompletionTokenOrList): ICodeCompletionActionProvider { - return CodeCompletionActionProviderWrapper(this) { parameters, it -> +fun ICodeCompletionActionProvider.withTokens( + replacement: (ICompletionTokenOrList) -> ICompletionTokenOrList, +): ICodeCompletionActionProvider = + CodeCompletionActionProviderWrapper(this) { parameters, it -> CodeCompletionActionWithTokens(it, replacement) } -} -fun ICodeCompletionAction.withCaretPolicy(policy: (ICaretPositionPolicy?) -> ICaretPositionPolicy?): CodeCompletionActionWithCaretPolicy { - return CodeCompletionActionWithCaretPolicy(this, policy) -} +fun ICodeCompletionAction.withCaretPolicy(policy: (ICaretPositionPolicy?) -> ICaretPositionPolicy?): CodeCompletionActionWithCaretPolicy = + CodeCompletionActionWithCaretPolicy(this, policy) -fun ICodeCompletionActionProvider.withCaretPolicy(policy: (ICaretPositionPolicy?) -> ICaretPositionPolicy?): CodeCompletionActionProviderWrapper { - return CodeCompletionActionProviderWrapper(this) { parameters, it -> +fun ICodeCompletionActionProvider.withCaretPolicy( + policy: (ICaretPositionPolicy?) -> ICaretPositionPolicy?, +): CodeCompletionActionProviderWrapper = + CodeCompletionActionProviderWrapper(this) { parameters, it -> CodeCompletionActionWithCaretPolicy(it, policy) } -} diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CodeCompletionMenu.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CodeCompletionMenu.kt index 9fababe2..8889569b 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CodeCompletionMenu.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CodeCompletionMenu.kt @@ -5,73 +5,89 @@ import kotlinx.html.div import kotlinx.html.table import kotlinx.html.td import kotlinx.html.tr +import org.modelix.editor.text.backend.BackendEditorComponent +import org.modelix.editor.text.shared.CompletionMenuEntryData class CodeCompletionMenu( - val editor: EditorComponent, + val editor: FrontendEditorComponent, val anchor: LayoutableCell, val completionPosition: CompletionPosition, - val providers: List, + initialEntries: List, initialPattern: String = "", initialCaretPosition: Int? = null, -) : IProducesHtml, IKeyboardHandler { +) : IProducesHtml, + IKeyboardHandler { val patternEditor = PatternEditor(initialPattern, initialCaretPosition) - private val actionsCache = CachedCodeCompletionActions(providers) private var selectedIndex: Int = 0 - private var entries: List = emptyList() + private var allEntries: List = initialEntries + private var filteredEntries: List = allEntries + + init { + applyFilter() + } override fun isHtmlOutputValid(): Boolean = false - fun updateActions() { - entries = computeActions(patternEditor.getTextBeforeCaret()) + fun loadEntries(newActions: List) { + allEntries = newActions + applyFilter() } - fun getEntries(): List = entries - - private fun computeActions(pattern: String): List { - return editor.runRead { - val parameters = CodeCompletionParameters(editor, pattern) - actionsCache.update(parameters) - .filter { - val matchingText = it.getCompletionPattern() - matchingText.isNotEmpty() && matchingText.startsWith(parameters.pattern) - } - .applyShadowing() - .sortedBy { it.getCompletionPattern().lowercase() } - } + fun applyFilter() { + val pattern = patternEditor.pattern + filteredEntries = allEntries.filter { it.matches(pattern) } } - private fun parameters() = CodeCompletionParameters(editor, patternEditor.getTextBeforeCaret()) + suspend fun updateActions() { + editor.serviceCall { this.updateCodeCompletionActions(editor.editorId, anchor.cell.getId(), patternEditor.pattern) } + } fun selectNext() { selectedIndex++ - if (selectedIndex >= entries.size) selectedIndex = 0 + if (selectedIndex >= filteredEntries.size) selectedIndex = 0 } fun selectPrevious() { selectedIndex-- - if (selectedIndex < 0) selectedIndex = (entries.size - 1).coerceAtLeast(0) + if (selectedIndex < 0) selectedIndex = (filteredEntries.size - 1).coerceAtLeast(0) } - fun getSelectedEntry(): ICodeCompletionAction? = entries.getOrNull(selectedIndex) + fun getSelectedEntry(): CompletionMenuEntryData? = filteredEntries.getOrNull(selectedIndex) - override fun processKeyDown(event: JSKeyboardEvent): Boolean { + override suspend fun processKeyDown(event: JSKeyboardEvent): Boolean { when (event.knownKey) { - KnownKeys.ArrowUp -> selectPrevious() - KnownKeys.ArrowDown -> selectNext() - KnownKeys.ArrowLeft -> patternEditor.moveCaret(-1) - KnownKeys.ArrowRight -> patternEditor.moveCaret(1) - KnownKeys.Escape -> editor.closeCodeCompletionMenu() - KnownKeys.Enter -> { - getSelectedEntry()?.let { entry -> - editor.runWrite { - entry.executeAndUpdateSelection(editor) - editor.state.clearTextReplacement(anchor) - } - } + KnownKeys.ArrowUp -> { + selectPrevious() + } + + KnownKeys.ArrowDown -> { + selectNext() + } + + KnownKeys.ArrowLeft -> { + patternEditor.moveCaret(-1) + } + + KnownKeys.ArrowRight -> { + patternEditor.moveCaret(1) + } + + KnownKeys.Escape -> { editor.closeCodeCompletionMenu() } - KnownKeys.Backspace -> patternEditor.deleteText(true) - KnownKeys.Delete -> patternEditor.deleteText(false) + + KnownKeys.Enter -> { + getSelectedEntry()?.execute() + } + + KnownKeys.Backspace -> { + patternEditor.deleteText(true) + } + + KnownKeys.Delete -> { + patternEditor.deleteText(false) + } + else -> { if (!event.typedText.isNullOrEmpty()) { patternEditor.insertText(event.typedText) @@ -80,27 +96,32 @@ class CodeCompletionMenu( } } } - editor.update() + editor.flushLocal() return true } + private suspend fun CompletionMenuEntryData.execute() { + val entry = this + editor.serviceCall { executeCodeCompletionAction(editor.editorId, entry.id) } + editor.closeCodeCompletionMenu() + } + override fun produceHtml(consumer: TagConsumer) { consumer.div("ccmenu-container") { produceChild(patternEditor) div("ccmenu") { table { - val parameters = parameters() - entries.forEachIndexed { index, action -> + filteredEntries.forEachIndexed { index, action -> tr("ccSelectedEntry".takeIf { index == selectedIndex }) { td("matchingText") { - +action.getCompletionPattern() + +action.matchingText } td("description") { - +action.getDescription() + +action.description } } } - if (entries.isEmpty()) { + if (filteredEntries.isEmpty()) { tr { td { +"No matches found" @@ -112,14 +133,17 @@ class CodeCompletionMenu( } } - fun executeIfSingleAction() { - if (entries.size == 1 && entries.first().getMatchingText() == patternEditor.pattern) { - entries.first().executeAndUpdateSelection(editor) - editor.closeCodeCompletionMenu() + suspend fun executeIfSingleAction() { + val singleEntry = filteredEntries.singleOrNull() ?: return + if (singleEntry.matchesExactly(patternEditor.pattern)) { + singleEntry.execute() } } - inner class PatternEditor(initialPattern: String, initialCaretPosition: Int?) : IProducesHtml { + inner class PatternEditor( + initialPattern: String, + initialCaretPosition: Int?, + ) : IProducesHtml { private var patternCell: Cell? = null var caretPos: Int = initialCaretPosition ?: initialPattern.length var pattern: String = initialPattern @@ -128,7 +152,7 @@ class CodeCompletionMenu( fun getTextBeforeCaret() = pattern.substring(0, caretPos) - fun deleteText(before: Boolean): Boolean { + suspend fun deleteText(before: Boolean): Boolean { if (before) { if (caretPos == 0) return false pattern = pattern.removeRange((caretPos - 1) until caretPos) @@ -142,20 +166,22 @@ class CodeCompletionMenu( return true } - fun insertText(text: String) { + suspend fun insertText(text: String) { val oldTextBeforeCaret = pattern.substring(0, caretPos) pattern = pattern.replaceRange(caretPos until caretPos, text) val remainingText = pattern.substring(caretPos) caretPos += text.length val newTextBeforeCaret = pattern.substring(0, caretPos) - val exactMatches = entries.filter { it.getMatchingText() == oldTextBeforeCaret } - if (exactMatches.size == 1 && computeActions(newTextBeforeCaret).isEmpty()) { - editor.runWrite { - editor.insertTextAfterUpdate(remainingText) - exactMatches.single().executeAndUpdateSelection(editor) - editor.closeCodeCompletionMenu() - editor.update() + val exactMatches = allEntries.filter { it.matchesExactly(oldTextBeforeCaret) } + if (exactMatches.size == 1 && + !editor.serviceCall { hasCodeCompletionActions(editor.editorId, anchor.cell.getId(), newTextBeforeCaret) } + ) { + exactMatches.single().execute() + editor.closeCodeCompletionMenu() + if (remainingText.isNotEmpty()) { + editor.flushLocal() + (editor.getSelection() as? CaretSelection)?.processTypedText(remainingText) } } else { updateActions() @@ -163,7 +189,7 @@ class CodeCompletionMenu( } } - fun moveCaret(delta: Int) { + suspend fun moveCaret(delta: Int) { caretPos = (caretPos + delta).coerceIn(0..pattern.length) updateActions() } @@ -179,20 +205,26 @@ class CodeCompletionMenu( } } -class CachedCodeCompletionActions(providers: List) { +class CachedCodeCompletionActions( + providers: List, +) { private var cacheEntries: List = providers.map { CacheEntry(it) } - fun update(parameters: CodeCompletionParameters): List { - return cacheEntries.flatMap { it.update(parameters) }.toList() - } + fun update(parameters: CodeCompletionParameters): List = cacheEntries.flatMap { it.update(parameters) }.toList() - inner class CacheEntry(val provider: IActionOrProvider) { + inner class CacheEntry( + val provider: IActionOrProvider, + ) { private var initialized = false private var cacheEntries: List = emptyList() private var dependsOnPattern: Boolean = true + fun update(parameters: CodeCompletionParameters): Sequence { return when (provider) { - is ICodeCompletionAction -> sequenceOf(provider) + is ICodeCompletionAction -> { + sequenceOf(provider) + } + is ICodeCompletionActionProvider -> { parameters.wasPatternAccessed() // reset state if (!initialized || dependsOnPattern) { @@ -202,7 +234,10 @@ class CachedCodeCompletionActions(providers: List } return cacheEntries.asSequence().flatMap { it.update(parameters) } } - else -> throw RuntimeException("Unknown type: " + provider::class) + + else -> { + throw RuntimeException("Unknown type: " + provider::class) + } } } } @@ -214,61 +249,58 @@ interface ICodeCompletionActionProvider : IActionOrProvider { fun getApplicableActions(parameters: CodeCompletionParameters): List } -fun ICodeCompletionActionProvider.flattenApplicableActions(parameters: CodeCompletionParameters): List { - return flatten(parameters).toList() -} +fun ICodeCompletionActionProvider.flattenApplicableActions(parameters: CodeCompletionParameters): List = + flatten(parameters).toList() -class ActionAsProvider(val action: ICodeCompletionAction) : ICodeCompletionActionProvider { - override fun getApplicableActions(parameters: CodeCompletionParameters): List { - return listOf(action) - } +class ActionAsProvider( + val action: ICodeCompletionAction, +) : ICodeCompletionActionProvider { + override fun getApplicableActions(parameters: CodeCompletionParameters): List = listOf(action) } fun ICodeCompletionAction.asProvider(): ICodeCompletionActionProvider = ActionAsProvider(this) -fun IActionOrProvider.asProvider(): ICodeCompletionActionProvider = when (this) { - is ICodeCompletionAction -> ActionAsProvider(this) - is ICodeCompletionActionProvider -> this - else -> error("Unknown type: $this") -} -private fun IActionOrProvider.flatten(parameters: CodeCompletionParameters): Sequence = when (this) { - is ICodeCompletionAction -> sequenceOf(this) - is ICodeCompletionActionProvider -> getApplicableActions(parameters).asSequence().flatMap { it.flatten(parameters) } - else -> throw RuntimeException("Unknown type: " + this::class) -} +fun IActionOrProvider.asProvider(): ICodeCompletionActionProvider = + when (this) { + is ICodeCompletionAction -> ActionAsProvider(this) + is ICodeCompletionActionProvider -> this + else -> error("Unknown type: $this") + } + +private fun IActionOrProvider.flatten(parameters: CodeCompletionParameters): Sequence = + when (this) { + is ICodeCompletionAction -> sequenceOf(this) + is ICodeCompletionActionProvider -> getApplicableActions(parameters).asSequence().flatMap { it.flatten(parameters) } + else -> throw RuntimeException("Unknown type: " + this::class) + } interface ICodeCompletionAction : IActionOrProvider { fun getMatchingText(): String + fun getTokens(): ICompletionTokenOrList = ConstantCompletionToken(getMatchingText()) + fun getDescription(): String - fun execute(editor: EditorComponent): ICaretPositionPolicy? + + fun execute(editor: BackendEditorComponent): ICaretPositionPolicy? + fun shadows(shadowed: ICodeCompletionAction) = false + fun shadowedBy(shadowing: ICodeCompletionAction) = false } fun ICodeCompletionAction.getCompletionPattern(): String = getTokens().toString() -fun ICodeCompletionAction.executeAndUpdateSelection(editor: EditorComponent) { - val policy = execute(editor) - if (policy != null) { - editor.selectAfterUpdate { policy.getBestSelection(editor) } - } -} - -fun ICellAction.executeAndUpdateSelection(editor: EditorComponent) { - val policy = execute(editor) - if (policy != null) { - editor.selectAfterUpdate { policy.getBestSelection(editor) } - } -} - -class CodeCompletionParameters(val editor: EditorComponent, pattern: String) { +class CodeCompletionParameters( + val editor: BackendEditorComponent, + pattern: String, +) { val pattern: String = pattern get() { patternAccessed = true return field } private var patternAccessed: Boolean = false + fun wasPatternAccessed(): Boolean { val result = patternAccessed patternAccessed = false @@ -282,13 +314,13 @@ enum class CompletionPosition { RIGHT, } -fun List.applyShadowing(): List { - return groupBy { it.getCompletionPattern() }.flatMap { applyShadowingToGroup(it.value) } -} +fun List.applyShadowing(): List = + groupBy { + it.getCompletionPattern() + }.flatMap { applyShadowingToGroup(it.value) } -private fun applyShadowingToGroup(actions: List): List { - return actions.filter { a1 -> +private fun applyShadowingToGroup(actions: List): List = + actions.filter { a1 -> val isShadowed = actions.any { a2 -> a2 !== a1 && (a2.shadows(a1) || a1.shadowedBy(a2)) } !isShadowed } -} diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CodeCompletionMenuUI.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CodeCompletionMenuUI.kt index 524e0bab..73c49998 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CodeCompletionMenuUI.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CodeCompletionMenuUI.kt @@ -1,6 +1,9 @@ package org.modelix.editor -class CodeCompletionMenuUI(val ccmenu: CodeCompletionMenu, val editor: EditorComponent) { +class CodeCompletionMenuUI( + val ccmenu: CodeCompletionMenu, + val editor: FrontendEditorComponent, +) { fun updateBounds() { val ccContainerElement = editor.generatedHtmlMap.getOutput(ccmenu) ?: return val layoutable = ccmenu.anchor @@ -8,16 +11,25 @@ class CodeCompletionMenuUI(val ccmenu: CodeCompletionMenu, val editor: EditorCom val anchorAbsoluteBounds = anchorElement.getOuterBounds() val anchorRelativeBounds = anchorAbsoluteBounds.relativeTo(editor.getMainLayer()?.getOuterBounds() ?: Bounds.ZERO) - val patternElement = ccContainerElement.descendants().filterIsInstance() - .first { it.getClasses().contains("ccmenu-pattern") } - val left: Double = when (ccmenu.completionPosition) { - CompletionPosition.CENTER -> anchorRelativeBounds.x - CompletionPosition.LEFT -> { - anchorRelativeBounds.x - patternElement.getOuterBounds().width - } + val patternElement = + ccContainerElement + .descendants() + .filterIsInstance() + .first { it.getClasses().contains("ccmenu-pattern") } + val left: Double = + when (ccmenu.completionPosition) { + CompletionPosition.CENTER -> { + anchorRelativeBounds.x + } + + CompletionPosition.LEFT -> { + anchorRelativeBounds.x - patternElement.getOuterBounds().width + } - CompletionPosition.RIGHT -> anchorRelativeBounds.maxX() - } + CompletionPosition.RIGHT -> { + anchorRelativeBounds.maxX() + } + } ccContainerElement.style.left = "${left}px" ccContainerElement.style.top = "${anchorRelativeBounds.y}px" diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CompletionPattern.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CompletionPattern.kt index ff42787a..a6178bd7 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CompletionPattern.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CompletionPattern.kt @@ -12,14 +12,21 @@ class CompletionEntry( interface ICompletionTokenOrList { fun flatten(): List + fun isEmpty(): Boolean = false + fun normalize(): ICompletionTokenOrList = this + fun consumeForAutoApply(input: CharSequence): CharSequence? } -class CompletionTokenList(val tokens: List) : ICompletionTokenOrList { +class CompletionTokenList( + val tokens: List, +) : ICompletionTokenOrList { override fun flatten(): List = tokens.flatMap { it.flatten() } + override fun isEmpty(): Boolean = tokens.isEmpty() + override fun normalize(): ICompletionTokenOrList { val unfiltered = tokens.flatMap { it.normalize().flatten() } val filtered = ArrayList() @@ -27,7 +34,10 @@ class CompletionTokenList(val tokens: List) : ICompletio var spaceType: SpaceTokenType = SpaceTokenType.OPTIONAL for (token in unfiltered) { when (token) { - is SpaceCompletionToken -> spaceType = spaceType.merge(token.type) + is SpaceCompletionToken -> { + spaceType = spaceType.merge(token.type) + } + else -> { filtered += SpaceCompletionToken(spaceType) filtered += token @@ -51,15 +61,14 @@ class CompletionTokenList(val tokens: List) : ICompletio return remainingInput } - override fun toString(): String { - return tokens.withIndex().joinToString("") { (index, token) -> + override fun toString(): String = + tokens.withIndex().joinToString("") { (index, token) -> if (token is SpaceCompletionToken && (index == 0 || index == tokens.lastIndex)) { "" } else { token.toString() } } - } } fun List.asTokenList() = if (size == 1) first() else CompletionTokenList(this) @@ -68,45 +77,54 @@ sealed class CompletionToken : ICompletionTokenOrList { var actions: IActionOrProvider? = null var highlighted: Boolean = false val alternatives: MutableList = ArrayList() + override fun flatten(): List = listOf(this) } sealed class RoleToken : CompletionToken() { abstract val role: IRole - override fun toString(): String { - return "<" + role.getSimpleName() + ">" - } - override fun consumeForAutoApply(input: CharSequence): CharSequence? { - return null - } + override fun toString(): String = "<" + role.getSimpleName() + ">" + + override fun consumeForAutoApply(input: CharSequence): CharSequence? = null } -class ChildCompletionToken(override val role: IChildLink) : RoleToken() -class PropertyCompletionToken(override val role: IProperty) : RoleToken() -class ReferenceCompletionToken(override val role: IReferenceLink) : RoleToken() -class ConstantCompletionToken(val text: String) : CompletionToken() { - override fun toString(): String { - return text - } +class ChildCompletionToken( + override val role: IChildLink, +) : RoleToken() - override fun consumeForAutoApply(input: CharSequence): CharSequence? { - return if (input.startsWith(text)) { +class PropertyCompletionToken( + override val role: IProperty, +) : RoleToken() + +class ReferenceCompletionToken( + override val role: IReferenceLink, +) : RoleToken() + +class ConstantCompletionToken( + val text: String, +) : CompletionToken() { + override fun toString(): String = text + + override fun consumeForAutoApply(input: CharSequence): CharSequence? = + if (input.startsWith(text)) { input.subSequence(text.length, input.length) } else { null } - } } -class SpaceCompletionToken(val type: SpaceTokenType) : CompletionToken() { - override fun toString(): String { - return if (type == SpaceTokenType.NONE) "" else " " - } +class SpaceCompletionToken( + val type: SpaceTokenType, +) : CompletionToken() { + override fun toString(): String = if (type == SpaceTokenType.NONE) "" else " " + + override fun consumeForAutoApply(input: CharSequence): CharSequence? = + when (type) { + SpaceTokenType.NONE -> { + input + } - override fun consumeForAutoApply(input: CharSequence): CharSequence? { - return when (type) { - SpaceTokenType.NONE -> input SpaceTokenType.MANDATORY -> { if (input.startsWith(" ")) { input.subSequence(1, input.length) @@ -114,6 +132,7 @@ class SpaceCompletionToken(val type: SpaceTokenType) : CompletionToken() { null } } + SpaceTokenType.OPTIONAL -> { if (input.startsWith(" ")) { input.subSequence(1, input.length) @@ -122,16 +141,15 @@ class SpaceCompletionToken(val type: SpaceTokenType) : CompletionToken() { } } } - } } -enum class SpaceTokenType(val prio: Int) { +enum class SpaceTokenType( + val prio: Int, +) { NONE(3), MANDATORY(2), OPTIONAL(1), ; - fun merge(other: SpaceTokenType): SpaceTokenType { - return if (this.prio > other.prio) this else other - } + fun merge(other: SpaceTokenType): SpaceTokenType = if (this.prio > other.prio) this else other } diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/ConceptEditor.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/ConceptEditor.kt index 9945791a..f0465c4f 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/ConceptEditor.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/ConceptEditor.kt @@ -1,66 +1,82 @@ package org.modelix.editor import org.modelix.editor.celltemplate.NotationRootCellTemplate +import org.modelix.kotlin.utils.AtomicLong import org.modelix.model.api.IConcept import org.modelix.model.api.INode import org.modelix.model.api.IProperty import org.modelix.model.api.meta.NullConcept +import org.modelix.model.api.upcast class ConceptEditor( val declaredConcept: IConcept?, val applicableToSubConcepts: Boolean, val templateBuilder: (subConcept: IConcept) -> NotationRootCellTemplate, ) { - fun isApplicable(context: CellCreationContext, node: INode): Boolean { - return apply(node.concept ?: NullConcept).condition?.invoke(node) != false + companion object { + private val idSequence = AtomicLong(0L) } - fun apply(subConcept: IConcept): NotationRootCellTemplate { - return templateBuilder(subConcept) - .also { it.setReference(RooCellTemplateReference(this, subConcept.getReference())) } - } + val id: Long = idSequence.incrementAndGet() + + fun isApplicable( + context: CellCreationContext, + node: INode, + ): Boolean = apply(node.concept ?: NullConcept).condition?.invoke(node) != false - fun applyIfApplicable(context: CellCreationContext, node: INode): CellData? { + fun apply(subConcept: IConcept): NotationRootCellTemplate = + templateBuilder(subConcept) + .also { it.setReference(RooCellTemplateReference(id, subConcept.getReference().upcast())) } + + fun applyIfApplicable( + context: CellCreationContext, + node: INode, + ): CellSpecBase? { // TODO evaluate .withNode blocks during creation of the template return apply(node.concept ?: NullConcept) .takeIf { it.condition?.invoke(node) != false } ?.apply(context, node) } - fun apply(context: CellCreationContext, node: INode): CellData { + fun apply( + context: CellCreationContext, + node: INode, + ): CellSpecBase { // TODO evaluate .withNode blocks during creation of the template return apply(node.concept ?: NullConcept).apply(context, node) } } -val defaultConceptEditor = ConceptEditor(null as IConcept?, applicableToSubConcepts = true) { subConcept -> - NotationRootCellTemplateBuilder(NotationRootCellTemplate(subConcept), subConcept, INodeConverter.Untyped).apply { - subConcept.getShortName().constant() - curlyBrackets { - for (property in subConcept.getAllProperties()) { - newLine() - label(property.getSimpleName() + ":") - property.cell() - } - for (link in subConcept.getAllReferenceLinks()) { - newLine() - label(link.getSimpleName() + ":") - link.cell(presentation = { - getPropertyValue(IProperty.fromName("name")) ?: reference.serialize() - }) - } - for (link in subConcept.getAllChildLinks()) { - newLine() - label(link.getSimpleName() + ":") - if (link.isMultiple) { - newLine() - indented { - link.vertical() +val defaultConceptEditor = + ConceptEditor(null as IConcept?, applicableToSubConcepts = true) { subConcept -> + NotationRootCellTemplateBuilder(NotationRootCellTemplate(subConcept), subConcept, INodeConverter.Untyped) + .apply { + subConcept.getShortName().constant() + curlyBrackets { + for (property in subConcept.getAllProperties()) { + newLine() + label(property.getSimpleName() + ":") + property.cell() + } + for (link in subConcept.getAllReferenceLinks()) { + newLine() + label(link.getSimpleName() + ":") + link.cell(presentation = { + getPropertyValue(IProperty.fromName("name")) ?: reference.serialize() + }) + } + for (link in subConcept.getAllChildLinks()) { + newLine() + label(link.getSimpleName() + ":") + if (link.isMultiple) { + newLine() + indented { + link.vertical() + } + } else { + link.cell() + } } - } else { - link.cell() } - } - } - }.template as NotationRootCellTemplate -} + }.template as NotationRootCellTemplate + } diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/EditorAspect.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/EditorAspect.kt index c56d891b..79ed937b 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/EditorAspect.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/EditorAspect.kt @@ -14,29 +14,38 @@ import org.modelix.model.api.INode class EditorAspect : ILanguageAspect { val conceptEditors: MutableList = ArrayList() - fun > conceptEditor(concept: ConceptT, applicableToSubConcepts: Boolean = false, body: NotationRootCellTemplateBuilder.() -> Unit): ConceptEditor { - return ConceptEditor(concept.untyped(), applicableToSubConcepts = applicableToSubConcepts) { subConcept -> + fun > conceptEditor( + concept: ConceptT, + applicableToSubConcepts: Boolean = false, + body: NotationRootCellTemplateBuilder.() -> Unit, + ): ConceptEditor = + ConceptEditor(concept.untyped(), applicableToSubConcepts = applicableToSubConcepts) { subConcept -> val typedSubconcept = subConcept.typed() as ConceptT - NotationRootCellTemplateBuilder(NotationRootCellTemplate(subConcept), typedSubconcept, INodeConverter.Typed(typedSubconcept)) - .also(body).template as NotationRootCellTemplate + NotationRootCellTemplateBuilder( + NotationRootCellTemplate(subConcept), + typedSubconcept, + INodeConverter.Typed(typedSubconcept) + ).also(body) + .template as NotationRootCellTemplate }.also(conceptEditors::add) - } - fun conceptEditor(concept: IConcept, applicableToSubConcepts: Boolean = false, body: NotationRootCellTemplateBuilder.() -> Unit): ConceptEditor { - return ConceptEditor(concept, applicableToSubConcepts = applicableToSubConcepts) { subConcept -> + fun conceptEditor( + concept: IConcept, + applicableToSubConcepts: Boolean = false, + body: NotationRootCellTemplateBuilder.() -> Unit, + ): ConceptEditor = + ConceptEditor(concept, applicableToSubConcepts = applicableToSubConcepts) { subConcept -> NotationRootCellTemplateBuilder(NotationRootCellTemplate(subConcept), subConcept, INodeConverter.Untyped) - .also(body).template as NotationRootCellTemplate + .also(body) + .template as NotationRootCellTemplate }.also(conceptEditors::add) - } fun register(editorEngine: EditorEngine) { editorEngine.registerEditors(this) } companion object : ILanguageAspectFactory { - override fun createInstance(language: ILanguage): EditorAspect { - return EditorAspect() - } + override fun createInstance(language: ILanguage): EditorAspect = EditorAspect() } } diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/EditorComponent.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/EditorComponent.kt deleted file mode 100644 index 1ba2cca9..00000000 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/EditorComponent.kt +++ /dev/null @@ -1,281 +0,0 @@ -package org.modelix.editor - -import kotlinx.html.TagConsumer -import kotlinx.html.div -import org.modelix.incremental.IncrementalIndex -import org.modelix.model.area.IArea -import kotlin.math.abs -import kotlin.math.min -import kotlin.math.roundToInt - -open class EditorComponent( - val engine: EditorEngine?, - val virtualDom: IVirtualDom = IVirtualDom.newInstance(), - private val transactionManager: IArea? = null, - private val rootCellCreator: (EditorState) -> Cell, -) : IProducesHtml { - val state: EditorState = EditorState() - private var selection: Selection? = null - private val cellIndex: IncrementalIndex = IncrementalIndex() - private val layoutablesIndex: IncrementalIndex = IncrementalIndex() - private var selectionUpdater: (() -> Selection?)? = null - protected var codeCompletionMenu: CodeCompletionMenu? = null - private var rootCell: Cell = rootCellCreator(state).also { - it.editorComponent = this - cellIndex.update(it.referencesIndexList) - layoutablesIndex.update(it.layout.layoutablesIndexList) - } - private var selectionView: SelectionView<*>? = null - val generatedHtmlMap = GeneratedHtmlMap() - private var highlightedLine: IVirtualDom.HTMLElement? = null - private var highlightedCell: IVirtualDom.HTMLElement? = null - private var textToInsertAfterUpdate: String? = null - - fun getMainLayer(): IVirtualDom.HTMLElement? { - return getHtmlElement()?.childNodes?.filterIsInstance()?.find { it.getClasses().contains(MAIN_LAYER_CLASS_NAME) } - } - - fun selectAfterUpdate(newSelection: () -> Selection?) { - selectionUpdater = newSelection - } - - fun resolveCell(reference: CellReference): List = cellIndex.lookup(reference) - - fun resolveLayoutable(cell: Cell): LayoutableCell? = layoutablesIndex.lookup(cell).firstOrNull() - - override fun isHtmlOutputValid(): Boolean = false - - fun getHtmlElement(): IVirtualDom.HTMLElement? = generatedHtmlMap.getOutput(this) - - private fun updateRootCell() { - val oldRootCell = rootCell - val newRootCell = rootCellCreator(state) - if (oldRootCell !== newRootCell) { - oldRootCell.editorComponent = null - newRootCell.editorComponent = this - rootCell = newRootCell - cellIndex.update(rootCell.referencesIndexList) - layoutablesIndex.update(rootCell.layout.layoutablesIndexList) - } - } - - open fun update() { - updateRootCell() - updateSelection() - updateSelectionView() - updateHtml() - selectionView?.update() - codeCompletionMenu?.let { CodeCompletionMenuUI(it, this).updateBounds() } - } - - protected open fun editorElementChanged(newElement: IVirtualDom.HTMLElement) {} - - fun updateHtml() { - val oldEditorElement = generatedHtmlMap.getOutput(this) - val newEditorElement = IncrementalVirtualDOMBuilder(virtualDom, oldEditorElement, generatedHtmlMap).produce(this)() - if (newEditorElement != oldEditorElement) { - editorElementChanged(newEditorElement) - } - - val selectedLayoutable = (getSelection() as? CaretSelection)?.layoutable - - val newHighlightedLine = selectedLayoutable?.getLine()?.let { generatedHtmlMap.getOutput(it) } - if (newHighlightedLine != highlightedLine) { - highlightedLine?.removeClass("highlighted") - } - newHighlightedLine?.addClass("highlighted") - highlightedLine = newHighlightedLine - - val newHighlightedCell = selectedLayoutable?.let { generatedHtmlMap.getOutput(it) } - if (newHighlightedCell != highlightedCell) { - highlightedCell?.removeClass("highlighted-cell") - } - newHighlightedCell?.addClass("highlighted-cell") - highlightedCell = newHighlightedCell - } - - private fun updateSelectionView() { - if (selectionView?.selection != getSelection()) { - selectionView = when (val selection = getSelection()) { - is CaretSelection -> CaretSelectionView(selection, this) - is CellSelection -> CellSelectionView(selection, this) - else -> null - } - } - } - - fun getRootCell() = rootCell - - private fun updateSelection() { - val updater = selectionUpdater - selectionUpdater = null - - selection = updater?.invoke() - ?: selection?.takeIf { it.isValid() } - ?: selection?.update(this) - selection?.also { - val text = textToInsertAfterUpdate - textToInsertAfterUpdate = null - if (text != null) { - (it as? CaretSelection)?.processTypedText(text, this) - } - } - } - - fun insertTextAfterUpdate(text: String) { - textToInsertAfterUpdate = text - } - - open fun changeSelection(newSelection: Selection) { - selection = newSelection - codeCompletionMenu = null - update() - } - - fun getSelection(): Selection? = selection - - fun showCodeCompletionMenu( - anchor: LayoutableCell, - position: CompletionPosition, - entries: List, - pattern: String = "", - caretPosition: Int? = null, - ) { - codeCompletionMenu = CodeCompletionMenu(this, anchor, position, entries, pattern, caretPosition) - codeCompletionMenu?.updateActions() - update() - } - - fun getCodeCompletionActions(): List { - return codeCompletionMenu?.getEntries() ?: emptyList() - } - - fun closeCodeCompletionMenu() { - codeCompletionMenu = null - update() - } - - fun dispose() { - } - - protected open fun processKeyUp(event: JSKeyboardEvent): Boolean { - return true - } - - protected open fun processKeyDown(event: JSKeyboardEvent): Boolean { - try { - if (event.knownKey == KnownKeys.F5) { - clearLayoutCache() - state.reset() - return true - } - for (handler in listOfNotNull(codeCompletionMenu, selection)) { - if (handler.processKeyDown(event)) return true - } - return false - } finally { - update() - } - } - - open fun processMouseEvent(event: JSMouseEvent) { - when (event.eventType) { - JSMouseEventType.CLICK -> processClick(event) - } - } - - open fun processKeyEvent(event: JSKeyboardEvent) { - when (event.eventType) { - JSKeyboardEventType.KEYDOWN -> processKeyDown(event) - JSKeyboardEventType.KEYUP -> processKeyUp(event) - } - } - - protected open fun processClick(event: JSMouseEvent): Boolean { - val targets = virtualDom.ui.getElementsAt(event.x, event.y) - for (target in targets) { - val htmlElement = target as? IVirtualDom.HTMLElement - val producer: IProducesHtml = htmlElement?.let { generatedHtmlMap.getProducer(it) } ?: continue - when (producer) { - is LayoutableCell -> { - val layoutable = producer as? LayoutableCell ?: continue - val text = layoutable.toText() // htmlElement.innerText - val cellAbsoluteBounds = htmlElement.getInnerBounds() - val relativeClickX = event.x - cellAbsoluteBounds.x - val characterWidth = cellAbsoluteBounds.width / text.length - val caretPos = (relativeClickX / characterWidth).roundToInt() - .coerceAtMost(layoutable.cell.getMaxCaretPos()) - changeSelection(CaretSelection(layoutable, caretPos)) - return true - } - is Layoutable -> { - if (selectClosestInLine(producer.getLine() ?: continue, event.x)) return true - } - is TextLine -> { - if (selectClosestInLine(producer, event.x)) return true - } - else -> continue - } - } - return false - } - - private fun selectClosestInLine(line: TextLine, absoluteClickX: Double): Boolean { - val words = line.words.filterIsInstance() - val closest = words.map { it to generatedHtmlMap.getOutput(it)!! }.minByOrNull { - min( - abs(absoluteClickX - it.second.getOuterBounds().minX()), - abs(absoluteClickX - it.second.getOuterBounds().maxX()), - ) - } ?: return false - val caretPos = if (absoluteClickX <= closest.second.getOuterBounds().minX()) { - 0 - } else { - closest.first.cell.getSelectableText()?.length ?: 0 - } - changeSelection(CaretSelection(closest.first, caretPos)) - return true - } - - fun clearLayoutCache() { - rootCell.descendantsAndSelf().forEach { it.clearCachedLayout() } - } - - override fun produceHtml(consumer: TagConsumer) { - consumer.div("editor") { - div(MAIN_LAYER_CLASS_NAME) { - produceChild(getRootCell().layout) - } - div("selection-layer relative-layer") { - produceChild(selectionView) - } - div("popup-layer relative-layer") { - produceChild(codeCompletionMenu) - } - } - } - - fun runRead(body: () -> R): R { - return if (transactionManager == null) { - body() - } else { - transactionManager.executeRead { body() } - } - } - - fun runWrite(body: () -> R): R { - return if (transactionManager == null) { - body() - } else { - transactionManager.executeWrite { body() } - } - } - - companion object { - val MAIN_LAYER_CLASS_NAME = "main-layer" - } -} - -interface IKeyboardHandler { - fun processKeyDown(event: JSKeyboardEvent): Boolean -} diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/EditorDSL.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/EditorDSL.kt index 8f6b5e05..30544cce 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/EditorDSL.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/EditorDSL.kt @@ -8,16 +8,21 @@ import org.modelix.model.api.INode import kotlin.jvm.JvmOverloads @JvmOverloads -fun > LanguageAspectsBuilder<*>.editor(concept: ConceptT, applicableToSubConcepts: Boolean = false, body: NotationRootCellTemplateBuilder.() -> Unit): ConceptEditor { - return aspects.getAspect(language, EditorAspect).conceptEditor(concept, applicableToSubConcepts = applicableToSubConcepts, body) -} +fun > LanguageAspectsBuilder<*>.editor( + concept: ConceptT, + applicableToSubConcepts: Boolean = false, + body: NotationRootCellTemplateBuilder.() -> Unit, +): ConceptEditor = aspects.getAspect(language, EditorAspect).conceptEditor(concept, applicableToSubConcepts = applicableToSubConcepts, body) @JvmOverloads -fun LanguageAspectsBuilder<*>.editor(concept: IConcept, applicableToSubConcepts: Boolean = false, body: NotationRootCellTemplateBuilder.() -> Unit): ConceptEditor { - return aspects.getAspect(language, EditorAspect).conceptEditor(concept, applicableToSubConcepts = applicableToSubConcepts, body) -} +fun LanguageAspectsBuilder<*>.editor( + concept: IConcept, + applicableToSubConcepts: Boolean = false, + body: NotationRootCellTemplateBuilder.() -> Unit, +): ConceptEditor = aspects.getAspect(language, EditorAspect).conceptEditor(concept, applicableToSubConcepts = applicableToSubConcepts, body) interface ModelAccessBuilder { fun get(body: () -> String?) + fun set(body: (String?) -> Unit) } 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 eddb63b1..361bf51c 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/EditorEngine.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/EditorEngine.kt @@ -1,27 +1,26 @@ package org.modelix.editor -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.cancel import org.modelix.editor.celltemplate.CellTemplate import org.modelix.editor.celltemplate.ParserForEditor +import org.modelix.editor.text.backend.BackendEditorComponent +import org.modelix.editor.text.shared.celltree.IMutableCellTree import org.modelix.incremental.IncrementalEngine import org.modelix.incremental.incrementalFunction -import org.modelix.metamodel.ITypedNode import org.modelix.model.api.IConcept import org.modelix.model.api.IConceptReference import org.modelix.model.api.INode +import org.modelix.model.api.IWritableNode import org.modelix.model.api.getAllConcepts import org.modelix.model.api.remove import org.modelix.parser.IParseTreeNode -class EditorEngine(incrementalEngine: IncrementalEngine? = null) { - +class EditorEngine( + incrementalEngine: IncrementalEngine? = null, +) { private val incrementalEngine: IncrementalEngine private val ownsIncrementalEngine: Boolean private val editorsForConcept: MutableMap> = LinkedHashMap() private val conceptEditorRegistries = ArrayList() - private val coroutineScope = CoroutineScope(Dispatchers.Default) private val parser = ParserForEditor(this) init { @@ -34,19 +33,27 @@ class EditorEngine(incrementalEngine: IncrementalEngine? = null) { } } - private val createCellIncremental: (EditorState, INode) -> Cell = this.incrementalEngine.incrementalFunction("createCell") { _, editorState, node -> - val cell = doCreateCell(editorState, node) - cell.freeze() - LOG.trace { "Cell created for $node: $cell" } - cell - } - private val createCellDataIncremental: (EditorState, INode) -> CellData = this.incrementalEngine.incrementalFunction("createCellData") { _, editorState, node -> - val cellData = doCreateCellData(editorState, node) - cellData.properties[CommonCellProperties.node] = node.toNonExisting() - cellData.freeze() - LOG.trace { "Cell created for $node: $cellData" } - cellData - } + private val createCellIncremental: (CellTreeState, CellCreationCall) -> IMutableCellTree.MutableCell = + this.incrementalEngine.incrementalFunction("createCell") { _, editorState, call -> + val cell = doCreateCell(editorState, call) + LOG.trace { "Cell created for $call: $cell" } + cell + } + + private val createCellSpecIncremental: (CellTreeState, CellCreationCall) -> CellSpecBase = + this.incrementalEngine.incrementalFunction("createCellData") { _, editorState, call -> + when (call) { + is NodeCellCreationCall -> { + val node = call.node.asLegacyNode() + val cellData = doCreateCellData(editorState, node) + cellData.properties[CommonCellProperties.node] = node.toNonExisting() + cellData.properties[CommonCellProperties.cellCall] = call + cellData.freeze() + LOG.trace { "Cell created for $node: $cellData" } + cellData + } + } + } fun addRegistry(registry: IConceptEditorRegistry) { conceptEditorRegistries += registry @@ -63,9 +70,20 @@ class EditorEngine(incrementalEngine: IncrementalEngine? = null) { } } - fun createCell(editorState: EditorState, node: INode): Cell { - return createCellIncremental(editorState, node) - } + fun createCell( + cellTreeState: CellTreeState, + node: INode, + ) = createCell(cellTreeState, node.asWritableNode()) + + fun createCell( + cellTreeState: CellTreeState, + node: IWritableNode, + ) = createCell(cellTreeState, NodeCellCreationCall(node)) + + fun createCell( + cellTreeState: CellTreeState, + call: CellCreationCall, + ) = createCellIncremental(cellTreeState, call) fun createCellModel(concept: IConcept): CellTemplate { val editor: ConceptEditor = resolveConceptEditor(concept).first() @@ -73,44 +91,67 @@ class EditorEngine(incrementalEngine: IncrementalEngine? = null) { return template } - fun createCellModelExcludingDefault(concept: IConcept): CellTemplate? { - return resolveConceptEditor(concept).minus(defaultConceptEditor).firstOrNull()?.apply(concept) - } + fun createCellModelExcludingDefault(concept: IConcept): CellTemplate? = + resolveConceptEditor(concept).minus(defaultConceptEditor).firstOrNull()?.apply(concept) - fun editNode(node: INode, virtualDom: IVirtualDom = IVirtualDom.newInstance()): EditorComponent { - return EditorComponent(this, virtualDom = virtualDom, transactionManager = node.getArea()) { editorState -> - node.getArea().executeRead { createCell(editorState, node) } - } - } + fun editNode(node: IWritableNode): BackendEditorComponent = BackendEditorComponent(NodeCellCreationCall(node), this) - @Deprecated("provide an untyped node", ReplaceWith("editorNode(node.unwrap(), virtualDom)")) - fun editNode(node: ITypedNode, virtualDom: IVirtualDom = IVirtualDom.newInstance()) = editNode(node.unwrap(), virtualDom) + private fun doCreateCell( + cellTreeState: CellTreeState, + call: CellCreationCall, + ): IMutableCellTree.MutableCell = + dataToCell(cellTreeState, createCellSpecIncremental(cellTreeState, call), cellTreeState.cellTree.createCell()) - private fun doCreateCell(editorState: EditorState, node: INode): Cell { - return dataToCell(editorState, createCellDataIncremental(editorState, node)) - } + private fun dataToCell( + cellTreeState: CellTreeState, + data: CellSpecBase, + cell: IMutableCellTree.MutableCell, + ): IMutableCellTree.MutableCell { + data.cellReferences.takeIf { it.isNotEmpty() }?.let { + cell.setProperty(CommonCellProperties.cellReferences, it.toList()) + } + for (key in data.properties.getKeys()) { + cell.setProperty(key as CellPropertyKey, data.properties[key]) + } + when (data) { + is CellSpec -> { + cell.setProperty(CommonCellProperties.type, ECellType.COLLECTION) + } - private fun dataToCell(editorState: EditorState, data: CellData): Cell { - val cell = Cell(data) - for (childData in data.children) { - val childCell: Cell = when (childData) { - is CellData -> { - dataToCell(editorState, childData) - } - is ChildDataReference -> { - createCell(editorState, childData.childNode).also { it.parent?.removeChild(it) } + is TextCellSpec -> { + cell.setProperty(CommonCellProperties.type, ECellType.TEXT) + cell.setProperty(TextCellProperties.text, data.text) + cell.setProperty(TextCellProperties.placeholderText, data.placeholderText) + } + } + for ((index, childRef) in data.children.withIndex()) { + val childCell = + when (childRef) { + is CellSpecBase -> { + dataToCell(cellTreeState, childRef, cell.addNewChild(index)) + } + + is ChildSpecReference -> { + createCell(cellTreeState, childRef.childNode) + } } - else -> throw RuntimeException("Unsupported: $childData") + if (childCell.getParent() != cell) { + childCell.moveCell(cell, index) + } else if (cell.getChildAt(index) != childCell) { + childCell.moveCell(index) } - cell.addChild(childCell) } + cell.getChildren().drop(data.children.size).forEach { it.detach() } return cell } - private fun doCreateCellData(editorState: EditorState, node: INode): CellData { + private fun doCreateCellData( + cellTreeState: CellTreeState, + node: INode, + ): CellSpecBase { try { val editor = resolveConceptEditor(node.concept) - val context = CellCreationContext(this, editorState) + val context = CellCreationContext(this, cellTreeState) // TODO do some proper conflict resolution between multiple applicable editors instead of just taking the first one. val data = editor.asSequence().mapNotNull { it.applyIfApplicable(context, node) }.first() @@ -124,7 +165,7 @@ class EditorEngine(incrementalEngine: IncrementalEngine? = null) { return data } catch (ex: Exception) { LOG.error(ex) { "Failed to create cell for $node" } - return TextCellData("", "").apply { + return TextCellSpec("", "").apply { properties[CommonCellProperties.textColor] = "red" } } @@ -132,39 +173,52 @@ class EditorEngine(incrementalEngine: IncrementalEngine? = null) { fun resolveConceptEditor(concept: IConcept?): List { if (concept == null) return listOf(defaultConceptEditor) - val editors = concept.getAllConcepts().firstNotNullOfOrNull { superConcept -> - val conceptReference = superConcept.getReference() - val allEditors = (editorsForConcept[conceptReference] ?: emptyList()) + - conceptEditorRegistries.flatMap { it.getConceptEditors(conceptReference) } - allEditors - .filter { it.declaredConcept == null || it.applicableToSubConcepts || concept.isExactly(it.declaredConcept) } - .takeIf { it.isNotEmpty() } - } + val editors = + concept.getAllConcepts().firstNotNullOfOrNull { superConcept -> + val conceptReference = superConcept.getReference() + val allEditors = + (editorsForConcept[conceptReference] ?: emptyList()) + + conceptEditorRegistries.flatMap { it.getConceptEditors(conceptReference) } + allEditors + .filter { it.declaredConcept == null || it.applicableToSubConcepts || concept.isExactly(it.declaredConcept) } + .takeIf { it.isNotEmpty() } + } return (editors ?: emptyList()) + defaultConceptEditor } - fun parse(input: String, outputConcept: IConcept, complete: Boolean): List { - return parser.getParser(startConcept = outputConcept, forCodeCompletion = complete).parseForest(input, complete).toList() - } + fun parse( + input: String, + outputConcept: IConcept, + complete: Boolean, + ): List = + parser.getParser(startConcept = outputConcept, forCodeCompletion = complete).parseForest(input, complete).toList() fun dispose() { - coroutineScope.cancel("EditorEngine disposed") if (ownsIncrementalEngine) incrementalEngine.dispose() } companion object { - private val LOG = io.github.oshai.kotlinlogging.KotlinLogging.logger {} + private val LOG = + io.github.oshai.kotlinlogging.KotlinLogging + .logger {} } } -class DeleteNodeCellAction(val node: INode) : ICellAction { +class DeleteNodeCellAction( + val node: INode, +) : ICellAction { override fun isApplicable(): Boolean = true - override fun execute(editor: EditorComponent): ICaretPositionPolicy? { - return SavedCaretPosition.saveAndRun(editor) { - editor.runWrite { - node.remove() - } + override fun execute(editor: BackendEditorComponent): ICaretPositionPolicy? { + editor.runWrite { + node.remove() } + return null // The frontend updates the caret position using SavedCaretPosition } } + +sealed class CellCreationCall + +data class NodeCellCreationCall( + val node: IWritableNode, +) : CellCreationCall() diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/EditorTestUtils.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/EditorTestUtils.kt index 0d72b81e..fef1ef67 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/EditorTestUtils.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/EditorTestUtils.kt @@ -1,5 +1,6 @@ package org.modelix.editor +import org.modelix.editor.text.shared.celltree.IMutableCellTree import kotlin.random.Random object EditorTestUtils { @@ -7,35 +8,74 @@ object EditorTestUtils { val newLine = Any() val indentChildren = Any() - fun buildCells(template: Any): Cell { - return when (template) { - is Cell -> template - noSpace -> Cell(CellData().apply { properties[CommonCellProperties.noSpace] = true }) - newLine -> Cell(CellData().apply { properties[CommonCellProperties.onNewLine] = true }) - is String -> Cell(TextCellData(template, "")) - is List<*> -> Cell(CellData()).apply { - template.forEach { child -> - when (child) { - indentChildren -> data.properties[CommonCellProperties.indentChildren] = true - is ECellLayout -> data.properties[CommonCellProperties.layout] = child - else -> addChild(buildCells(child!!)) + fun buildCells( + template: Any, + tree: IMutableCellTree, + ): MutableCell = + when (template) { + is IMutableCellTree.MutableCell -> { + template + } + + noSpace -> { + tree.createCell().apply { setProperty(CommonCellProperties.noSpace, true) } + } + + newLine -> { + tree.createCell().apply { setProperty(CommonCellProperties.onNewLine, true) } + } + + is String -> { + tree.createCell().apply { + setProperty(CommonCellProperties.type, ECellType.TEXT) + setProperty(TextCellProperties.text, template) + setProperty(TextCellProperties.placeholderText, "") + } + } + + is List<*> -> { + tree.createCell().apply { + template.forEach { child -> + when (child) { + indentChildren -> setProperty(CommonCellProperties.indentChildren, true) + is ECellLayout -> setProperty(CommonCellProperties.layout, child) + else -> buildCells(child!!, tree).moveCell(this, getChildren().size) + } } } } - else -> throw IllegalArgumentException("Unsupported: $template") + + else -> { + throw IllegalArgumentException("Unsupported: $template") + } } - } - fun buildRandomCells(rand: Random, cellsPerLevel: Int, levels: Int): Cell { - return buildCells(buildRandomTemplate(rand, cellsPerLevel, levels)) - } + fun buildRandomCells( + rand: Random, + cellsPerLevel: Int, + levels: Int, + tree: IMutableCellTree, + ): MutableCell = buildCells(buildRandomTemplate(rand, cellsPerLevel, levels), tree) - fun buildRandomTemplate(rand: Random, cellsPerLevel: Int, levels: Int): Any { - return (1..cellsPerLevel).map { + fun buildRandomTemplate( + rand: Random, + cellsPerLevel: Int, + levels: Int, + ): Any = + (1..cellsPerLevel).map { when (rand.nextInt(10)) { - 0 -> noSpace - 1 -> newLine - 2 -> indentChildren + 0 -> { + noSpace + } + + 1 -> { + newLine + } + + 2 -> { + indentChildren + } + else -> { if (levels == 0) { rand.nextInt(1000, 10000).toString() @@ -45,5 +85,4 @@ object EditorTestUtils { } } } - } } diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/FrontendEditorComponent.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/FrontendEditorComponent.kt new file mode 100644 index 00000000..a449a7cc --- /dev/null +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/FrontendEditorComponent.kt @@ -0,0 +1,391 @@ +package org.modelix.editor + +import io.github.oshai.kotlinlogging.KotlinLogging +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.onFailure +import kotlinx.coroutines.completeWith +import kotlinx.coroutines.launch +import kotlinx.html.TagConsumer +import kotlinx.html.div +import org.modelix.editor.text.backend.AtomicReference +import org.modelix.editor.text.frontend.FrontendCellTree +import org.modelix.editor.text.frontend.getSelectableText +import org.modelix.editor.text.shared.CompletionMenuEntryData +import org.modelix.editor.text.shared.EditorId +import org.modelix.editor.text.shared.EditorUpdateData +import org.modelix.editor.text.shared.ServiceCallResult +import org.modelix.editor.text.shared.TextEditorService +import org.modelix.editor.text.shared.consume +import org.modelix.incremental.IncrementalIndex +import org.modelix.kotlin.utils.AtomicLong +import org.modelix.model.api.INodeReference +import org.modelix.model.api.runSynchronized +import org.modelix.model.api.toSerialized +import kotlin.math.abs +import kotlin.math.min +import kotlin.math.roundToInt + +open class FrontendEditorComponent( + private val service: TextEditorService, + val virtualDom: IVirtualDom = IVirtualDom.newInstance(), +) : IProducesHtml { + val editorId: EditorId = idSequence.incrementAndGet().toInt() + val cellTree = FrontendCellTree(this) + private var selection: Selection? = null + private val layoutablesIndex: IncrementalIndex = IncrementalIndex() + protected var codeCompletionMenu: CodeCompletionMenu? = null + private var selectionView: SelectionView<*>? = null + val generatedHtmlMap = GeneratedHtmlMap() + private var highlightedLine: IVirtualDom.HTMLElement? = null + private var highlightedCell: IVirtualDom.HTMLElement? = null + private val coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.Default) + private val eventQueue = UIEventDispatcher(coroutineScope) + private val updateLoop: AtomicReference = AtomicReference(null) + private val updateLock = Any() + + fun openNode(rootNode: INodeReference): Deferred { + val firstUpdate = CompletableDeferred() + updateLoop.getAndUpdate { currentJob -> + currentJob?.cancel("root node changed") + val updateFlow = service.openNode(editorId, rootNode.toSerialized()) + coroutineScope.launch { + var isFirst = true + updateFlow.collect { update -> + if (isFirst) { + isFirst = false + firstUpdate.complete(update) + } + enqueueUpdate(update) + } + } + } + return firstUpdate + } + + suspend fun flush() = flushRemote() + + suspend fun flushRemote() = enqueueUpdate(service.flush(editorId)).await() + + suspend fun flushLocal() = enqueueUpdate(EditorUpdateData()).await() + + fun getMainLayer(): IVirtualDom.HTMLElement? = + getHtmlElement()?.childNodes?.filterIsInstance()?.find { + it.getClasses().contains(MAIN_LAYER_CLASS_NAME) + } + + suspend fun flushAndUpdateSelection(newSelection: () -> Selection?) { + val updateData = service.flush(editorId) + enqueueUpdate { + updateNow(updateData) + newSelection()?.let { doChangeSelection(it) } + }.await() + } + + fun resolveCell(reference: CellReference): List = cellTree.resolveCell(reference) + + fun resolveLayoutable(cell: Cell): LayoutableCell? { + updateLayoutablesIndex() + return layoutablesIndex.lookup(cell).firstOrNull() + } + + private fun updateLayoutablesIndex() { + layoutablesIndex.update(cellTree.getRoot().layout.layoutablesIndexList) + } + + override fun isHtmlOutputValid(): Boolean = false + + fun getHtmlElement(): IVirtualDom.HTMLElement? = generatedHtmlMap.getOutput(this) + + suspend fun editNode(node: INodeReference) { + openNode(node.toSerialized()).await() + } + + fun enqueueUpdate(updateData: EditorUpdateData): Deferred = enqueueUpdate { updateNow(updateData) } + + fun enqueueUpdate(body: suspend () -> R): Deferred = eventQueue.invokeLater(body) + + fun updateNow(update: EditorUpdateData? = null) { + runSynchronized(updateLock) { + if (update != null) { + cellTree.applyChanges(update.cellTreeChanges) + } + update?.selectionChange?.getBestSelection(this)?.let { selection = it } + updateSelection() + updateSelectionView() + update?.completionMenuTrigger?.let { + val layoutable = cellTree.getCell(it.anchor).layoutable() ?: return@let + showCodeCompletionMenu( + layoutable, + it.completionPosition, + update.completionEntries.orEmpty(), + it.pattern, + it.caretPosition, + ) + } + update?.completionEntries?.let { newEntries -> + codeCompletionMenu?.loadEntries(newEntries) + } + updateHtml() + selectionView?.update() + codeCompletionMenu?.let { CodeCompletionMenuUI(it, this).updateBounds() } + } + } + + suspend fun resetState() { + serviceCall { resetState(editorId) } + } + + protected open fun editorElementChanged(newElement: IVirtualDom.HTMLElement) {} + + fun updateHtml() { + val oldEditorElement = generatedHtmlMap.getOutput(this) + val newEditorElement = IncrementalVirtualDOMBuilder(virtualDom, oldEditorElement, generatedHtmlMap).produce(this)() + if (newEditorElement != oldEditorElement) { + editorElementChanged(newEditorElement) + } + + val selectedLayoutable = (getSelection() as? CaretSelection)?.layoutable + + val newHighlightedLine = selectedLayoutable?.getLine()?.let { generatedHtmlMap.getOutput(it) } + if (newHighlightedLine != highlightedLine) { + highlightedLine?.removeClass("highlighted") + } + newHighlightedLine?.addClass("highlighted") + highlightedLine = newHighlightedLine + + val newHighlightedCell = selectedLayoutable?.let { generatedHtmlMap.getOutput(it) } + if (newHighlightedCell != highlightedCell) { + highlightedCell?.removeClass("highlighted-cell") + } + newHighlightedCell?.addClass("highlighted-cell") + highlightedCell = newHighlightedCell + } + + private fun updateSelectionView() { + if (selectionView?.selection != getSelection()) { + selectionView = + when (val selection = getSelection()) { + is CaretSelection -> CaretSelectionView(selection, this) + is CellSelection -> CellSelectionView(selection, this) + else -> null + } + } + } + + fun getRootCell() = cellTree.getRoot() + + private fun updateSelection() { + selection = selection?.takeIf { it.isValid() } + ?: selection?.update(this) + } + + suspend fun changeSelection(newSelection: Selection) { + changeSelectionLater(newSelection).await() + } + + fun doChangeSelection(newSelection: Selection) { + selection = newSelection + codeCompletionMenu = null + updateNow() + } + + fun changeSelectionLater(newSelection: Selection): Deferred = + eventQueue.invokeLater { + doChangeSelection(newSelection) + } + + fun getSelection(): Selection? = selection + + fun showCodeCompletionMenu( + anchor: LayoutableCell, + position: CompletionPosition, + entries: List, + pattern: String = "", + caretPosition: Int? = null, + ): Deferred = + eventQueue.invokeLater { + codeCompletionMenu = CodeCompletionMenu(this, anchor, position, entries, pattern, caretPosition) + updateNow() + } + + fun closeCodeCompletionMenu(): Deferred = + eventQueue.invokeLater { + codeCompletionMenu = null + updateNow() + } + + fun dispose() { + eventQueue.dispose() + updateLoop.getAndUpdate { currentJob -> + currentJob?.cancel("disposed") + null + } + coroutineScope.cancel("disposed") + } + + fun enqueueUIEvent(event: JSUIEvent): Boolean { + eventQueue.invokeLater { + when (event) { + is JSKeyboardEvent -> processKeyEvent(event) + is JSMouseEvent -> processMouseEvent(event) + } + } + return true + } + + private fun processKeyUp(event: JSKeyboardEvent): Boolean = true + + private suspend fun processKeyDown(event: JSKeyboardEvent): Boolean { + try { + if (event.knownKey == KnownKeys.F5) { + clearLayoutCache() + // state.reset() + return true + } + for (handler in listOfNotNull(codeCompletionMenu, selection)) { + if (handler.processKeyDown(event)) return true + } + return false + } finally { + flushLocal() + } + } + + suspend fun processMouseEvent(event: JSMouseEvent) { + when (event.eventType) { + JSMouseEventType.CLICK -> processClick(event) + } + } + + suspend fun processKeyEvent(event: JSKeyboardEvent) { + when (event.eventType) { + JSKeyboardEventType.KEYDOWN -> processKeyDown(event) + JSKeyboardEventType.KEYUP -> processKeyUp(event) + } + } + + suspend fun processClick(event: JSMouseEvent): Boolean { + val targets = virtualDom.ui.getElementsAt(event.x, event.y) + for (target in targets) { + val htmlElement = target as? IVirtualDom.HTMLElement + val producer: IProducesHtml = htmlElement?.let { generatedHtmlMap.getProducer(it) } ?: continue + when (producer) { + is LayoutableCell -> { + val layoutable = producer as? LayoutableCell ?: continue + val text = layoutable.toText() // htmlElement.innerText + val cellAbsoluteBounds = htmlElement.getInnerBounds() + val relativeClickX = event.x - cellAbsoluteBounds.x + val characterWidth = cellAbsoluteBounds.width / text.length + val caretPos = + (relativeClickX / characterWidth) + .roundToInt() + .coerceAtMost(layoutable.cell.getMaxCaretPos()) + doChangeSelection(CaretSelection(this, layoutable, caretPos)) + return true + } + + is Layoutable -> { + if (selectClosestInLine(producer.getLine() ?: continue, event.x)) return true + } + + is TextLine -> { + if (selectClosestInLine(producer, event.x)) return true + } + + else -> { + continue + } + } + } + return false + } + + private fun selectClosestInLine( + line: TextLine, + absoluteClickX: Double, + ): Boolean { + val words = line.words.filterIsInstance() + val closest = + words.map { it to generatedHtmlMap.getOutput(it)!! }.minByOrNull { + min( + abs(absoluteClickX - it.second.getOuterBounds().minX()), + abs(absoluteClickX - it.second.getOuterBounds().maxX()), + ) + } ?: return false + val caretPos = + if (absoluteClickX <= closest.second.getOuterBounds().minX()) { + 0 + } else { + closest.first.cell + .getSelectableText() + ?.length ?: 0 + } + doChangeSelection(CaretSelection(this, closest.first, caretPos)) + return true + } + + fun clearLayoutCache() { + cellTree.getRoot().descendantsAndSelf().forEach { (it as FrontendCellTree.FrontendCellImpl).clearCachedLayout() } + } + + override fun produceHtml(consumer: TagConsumer) { + consumer.div("editor") { + div(MAIN_LAYER_CLASS_NAME) { + produceChild(getRootCell().layout) + } + div("selection-layer relative-layer") { + produceChild(selectionView) + } + div("popup-layer relative-layer") { + produceChild(codeCompletionMenu) + } + } + } + + suspend fun serviceCall(call: suspend TextEditorService.() -> R): R { + val result = call(service) + when (result) { + is EditorUpdateData -> enqueueUpdate(result).await() + is ServiceCallResult<*> -> result.updateData?.let { enqueueUpdate(it).await() } + } + return result + } + + companion object { + private val idSequence = AtomicLong(0L) + val MAIN_LAYER_CLASS_NAME = "main-layer" + private val LOG = KotlinLogging.logger { } + } +} + +interface IKeyboardHandler { + suspend fun processKeyDown(event: JSKeyboardEvent): Boolean +} + +class UIEventDispatcher( + val coroutineScope: CoroutineScope, +) { + private val eventQueue = + coroutineScope.consume Any?, CompletableDeferred>>(capacity = Channel.UNLIMITED) { + it.second.completeWith(runCatching { it.first.invoke() }) + } + + fun invokeLater(body: suspend () -> R): Deferred { + val result = CompletableDeferred() + eventQueue.trySend(body to result as CompletableDeferred).getOrThrow() + return result + } + + suspend fun invokeAndWait(body: () -> R): R = invokeLater(body).await() + + fun dispose() { + eventQueue.close() + } +} diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/ICellAction.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/ICellAction.kt index 4120c8dd..6d1fe5ed 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/ICellAction.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/ICellAction.kt @@ -1,28 +1,40 @@ package org.modelix.editor +import org.modelix.editor.text.backend.BackendEditorComponent import org.modelix.model.api.INode import org.modelix.model.api.getInstantiatableSubConcepts interface ICellAction { fun isApplicable(): Boolean - fun execute(editor: EditorComponent): ICaretPositionPolicy? + + fun execute(editor: BackendEditorComponent): ICaretPositionPolicy? } interface ITextChangeAction { fun isValid(value: String?): Boolean - fun replaceText(editor: EditorComponent, range: IntRange, replacement: String, newText: String): Boolean -} -class CompositeTextChangeAction(val actions: List) : ITextChangeAction { - override fun isValid(value: String?): Boolean { - return actions.any { it.isValid(value) } - } + fun replaceText( + editor: CellTreeState, + range: IntRange, + replacement: String, + newText: String, + ): Boolean +} - override fun replaceText(editor: EditorComponent, range: IntRange, replacement: String, newText: String): Boolean { - return actions +class CompositeTextChangeAction( + val actions: List, +) : ITextChangeAction { + override fun isValid(value: String?): Boolean = actions.any { it.isValid(value) } + + override fun replaceText( + editor: CellTreeState, + range: IntRange, + replacement: String, + newText: String, + ): Boolean = + actions .filter { it.isValid(newText) } .any { it.replaceText(editor, range, replacement, newText) } - } companion object { fun create(actions: List): ITextChangeAction? { @@ -37,34 +49,46 @@ class CompositeTextChangeAction(val actions: List) : ITextCha } object CellActionProperties { - val substitute = CellPropertyKey("substitute", null) - val transformBefore = CellPropertyKey("transformBefore", null) - val transformAfter = CellPropertyKey("transformAfter", null) - val insert = CellPropertyKey("insert", null) - val delete = CellPropertyKey("delete", null) - val show = CellPropertyKey("show", null) - val replaceText = CellPropertyKey("replaceText", null) + val substitute = BackendCellPropertyKey("substitute", null) + val transformBefore = BackendCellPropertyKey("transformBefore", null) + val transformAfter = BackendCellPropertyKey("transformAfter", null) + val insert = BackendCellPropertyKey("insert", null) + val delete = BackendCellPropertyKey("delete", null) + val show = BackendCellPropertyKey("show", null) + val replaceText = BackendCellPropertyKey("replaceText", null) } -class SideTransformNode(val before: Boolean, val node: INode) : ICodeCompletionActionProvider { +class SideTransformNode( + val before: Boolean, + val node: INode, +) : ICodeCompletionActionProvider { override fun getApplicableActions(parameters: CodeCompletionParameters): List { val engine = parameters.editor.engine ?: return emptyList() val location = ExistingNode(node) val expectedConcept = location.expectedConcept() ?: return emptyList() val allowedConcepts = expectedConcept.getInstantiatableSubConcepts() - val cellModels = allowedConcepts.map { concept -> - engine.createCellModel(concept) - } + val cellModels = + allowedConcepts.map { concept -> + engine.createCellModel(concept) + } return cellModels.flatMap { it.getSideTransformActions(before, node) ?: emptyList() } } } -fun Cell.getSubstituteActions() = collectSubstituteActionsBetween(previousLeaf { it.isVisible() }, firstLeaf()).distinct() // TODO non-leafs can also be visible (text cells can have children) +fun Cell.getSubstituteActions() = + collectSubstituteActionsBetween( + previousLeaf { + it.isVisible() + }, + firstLeaf() + ).distinct() // TODO non-leafs can also be visible (text cells can have children) + fun Cell.getActionsBefore(): Sequence { val stopAt = previousLeaf { it.isVisible() }?.rightBorder() - return firstLeaf().leftBorder() + return firstLeaf() + .leftBorder() .allPrevious() .takeWhile { it != stopAt } .takeUnlessPrevious { it.isLeft && it.cell.getProperty(CommonCellProperties.onNewLine) } @@ -75,7 +99,8 @@ fun Cell.getActionsBefore(): Sequence { fun Cell.getActionsAfter(): Sequence { val stopAt = nextLeaf { it.isVisible() }?.leftBorder() - return lastLeaf().rightBorder() + return lastLeaf() + .rightBorder() .allNext() .takeWhile { it != stopAt } .takeWhile { !(it.isLeft && it.cell.getProperty(CommonCellProperties.onNewLine)) } @@ -84,38 +109,48 @@ fun Cell.getActionsAfter(): Sequence { // TODO non-leafs can also be visible (text cells can have children) } -private fun collectSubstituteActionsBetween(leftLeaf: Cell?, rightLeaf: Cell?): Sequence { - return getBordersBetween(leftLeaf?.rightBorder(), rightLeaf?.leftBorder()) +private fun collectSubstituteActionsBetween( + leftLeaf: Cell?, + rightLeaf: Cell?, +): Sequence = + getBordersBetween(leftLeaf?.rightBorder(), rightLeaf?.leftBorder()) .filter { it.isLeft } .mapNotNull { it.cell.getProperty(CellActionProperties.substitute) } -} -private fun collectTransformActionsBetween(leftLeaf: Cell?, rightLeaf: Cell?): Sequence { - return getBordersBetween(leftLeaf?.rightBorder(), rightLeaf?.leftBorder()) +private fun collectTransformActionsBetween( + leftLeaf: Cell?, + rightLeaf: Cell?, +): Sequence = + getBordersBetween(leftLeaf?.rightBorder(), rightLeaf?.leftBorder()) .mapNotNull { it.cell.getProperty(if (it.isLeft) CellActionProperties.transformBefore else CellActionProperties.transformAfter) } -} -fun getBordersBetween(left: CellBorder?, right: CellBorder?): Sequence { - return if (left != null && right != null) { +fun getBordersBetween( + left: CellBorder?, + right: CellBorder?, +): Sequence = + if (left != null && right != null) { generateSequence(left) { it.next() }.takeWhilePrevious { it != right } } else if (left != null) { generateSequence(left) { it.next() } } else { generateSequence(right) { it.previous() } } -} -data class CellBorder(val cell: Cell, val isLeft: Boolean) { +data class CellBorder( + val cell: Cell, + val isLeft: Boolean, +) { val isRight: Boolean get() = !isLeft fun allPrevious() = generateSequence(this) { it.previous() } + fun allNext() = generateSequence(this) { it.next() } fun previous(): CellBorder? { if (isLeft) { val previousSibling = cell.previousSibling() if (previousSibling == null) { - val parent = cell.parent ?: return null + val parent = cell.getParent() ?: return null return parent.leftBorder() } else { return previousSibling.rightBorder() @@ -141,7 +176,7 @@ data class CellBorder(val cell: Cell, val isLeft: Boolean) { } else { val nextSibling = cell.nextSibling() if (nextSibling == null) { - val parent = cell.parent ?: return null + val parent = cell.getParent() ?: return null return parent.rightBorder() } else { return nextSibling.leftBorder() @@ -151,4 +186,5 @@ data class CellBorder(val cell: Cell, val isLeft: Boolean) { } fun Cell.leftBorder() = CellBorder(this, true) + fun Cell.rightBorder() = CellBorder(this, false) diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/IEditorComponentUI.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/IEditorComponentUI.kt index 1ad5b330..2e640ef2 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/IEditorComponentUI.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/IEditorComponentUI.kt @@ -5,7 +5,11 @@ interface IEditorComponentUI { * Relative to the top left corner of the editor */ fun getOuterBounds(element: IVirtualDom.Element): Bounds + fun getInnerBounds(element: IVirtualDom.Element): Bounds - fun getElementsAt(x: Double, y: Double): List + fun getElementsAt( + x: Double, + y: Double, + ): List } 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 9f7b6d1f..a04ba641 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/INonExistingNode.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/INonExistingNode.kt @@ -14,12 +14,19 @@ import org.modelix.model.api.upcast interface INonExistingNode { fun getExistingAncestor(): INode? + fun getParent(): INonExistingNode? + fun getContainmentLink(): IChildLinkDefinition? + fun index(): Int + fun replaceNode(subConcept: IConcept?): INode + fun getOrCreateNode(subConcept: IConcept? = null): INode + fun getNode(): INode? + fun expectedConcept(): IConcept? /** @@ -28,9 +35,10 @@ interface INonExistingNode { fun nodeCreationDepth(): Int } -fun INonExistingNode.ancestors(includeSelf: Boolean = false): Sequence { - return generateSequence(if (includeSelf) this else getParent()) { it.getParent() } -} +fun INonExistingNode.ancestors(includeSelf: Boolean = false): Sequence = + generateSequence(if (includeSelf) this else getParent()) { + it.getParent() + } fun INonExistingNode.commonAncestor(otherNode: INonExistingNode): INonExistingNode? { val ancestors1 = HashSet() @@ -45,7 +53,10 @@ fun INonExistingNode.commonAncestor(otherNode: INonExistingNode): INonExistingNo return null } -data class NodeReplacement(val nodeToReplace: INonExistingNode, val replacementConcept: IConcept) : INonExistingNode { +data class NodeReplacement( + val nodeToReplace: INonExistingNode, + val replacementConcept: IConcept, +) : INonExistingNode { override fun getExistingAncestor(): INode? = nodeToReplace.getExistingAncestor() override fun getParent(): INonExistingNode? = nodeToReplace.getParent() @@ -54,23 +65,15 @@ data class NodeReplacement(val nodeToReplace: INonExistingNode, val replacementC override fun index(): Int = nodeToReplace.index() - override fun replaceNode(subConcept: IConcept?): INode { - return nodeToReplace.replaceNode(coerceOutputConcept(subConcept)) - } + override fun replaceNode(subConcept: IConcept?): INode = nodeToReplace.replaceNode(coerceOutputConcept(subConcept)) - override fun getOrCreateNode(subConcept: IConcept?): INode { - return replaceNode(subConcept) - } + override fun getOrCreateNode(subConcept: IConcept?): INode = replaceNode(subConcept) override fun nodeCreationDepth(): Int = nodeToReplace.nodeCreationDepth().coerceAtLeast(1) - override fun getNode(): INode? { - return null - } + override fun getNode(): INode? = null - override fun expectedConcept(): IConcept { - return replacementConcept - } + override fun expectedConcept(): IConcept = replacementConcept } fun INonExistingNode.replacement(newConcept: IConcept): INonExistingNode = NodeReplacement(this, newConcept) @@ -87,7 +90,9 @@ fun INonExistingNode.coerceOutputConcept(subConcept: IConcept?): IConcept? { } } -data class ExistingNode(private val node: INode) : INonExistingNode { +data class ExistingNode( + private val node: INode, +) : INonExistingNode { override fun getExistingAncestor(): INode = node override fun getParent(): INonExistingNode? = node.parent?.let { ExistingNode(it) } @@ -98,37 +103,37 @@ data class ExistingNode(private val node: INode) : INonExistingNode { override fun replaceNode(subConcept: IConcept?): INode { val parent = node.parent ?: throw RuntimeException("cannot replace the root node") - val newNode = parent.asWritableNode().addNewChild( - node.asWritableNode().getContainmentLink(), - node.index(), - coerceOutputConcept(subConcept)?.getReference().upcast() - ) + val newNode = + parent.asWritableNode().addNewChild( + node.asWritableNode().getContainmentLink(), + node.index(), + coerceOutputConcept(subConcept)?.getReference().upcast() + ) node.remove() return newNode.asLegacyNode() } - override fun getOrCreateNode(subConcept: IConcept?): INode { - return if (subConcept == null || node.isInstanceOf(coerceOutputConcept(subConcept))) { + override fun getOrCreateNode(subConcept: IConcept?): INode = + if (subConcept == null || node.isInstanceOf(coerceOutputConcept(subConcept))) { node } else { replaceNode(subConcept) } - } override fun nodeCreationDepth(): Int = 0 - override fun getNode(): INode { - return node - } + override fun getNode(): INode = node - override fun expectedConcept(): IConcept? { - return node.asWritableNode().getContainmentLinkDefinition()?.targetConcept - } + override fun expectedConcept(): IConcept? = node.asWritableNode().getContainmentLinkDefinition()?.targetConcept } fun INode.toNonExisting() = ExistingNode(this) -data class NonExistingChild(private val parent: INonExistingNode, val link: IChildLink, private val index: Int = 0) : INonExistingNode { +data class NonExistingChild( + private val parent: INonExistingNode, + val link: IChildLink, + private val index: Int = 0, +) : INonExistingNode { override fun getExistingAncestor(): INode? = parent.getExistingAncestor() override fun getParent() = parent @@ -143,22 +148,18 @@ data class NonExistingChild(private val parent: INonExistingNode, val link: IChi return newNode } - override fun getOrCreateNode(subConcept: IConcept?): INode { - return replaceNode(subConcept) - } + override fun getOrCreateNode(subConcept: IConcept?): INode = replaceNode(subConcept) override fun nodeCreationDepth(): Int = parent.nodeCreationDepth() + 1 - override fun getNode(): INode? { - return null - } + override fun getNode(): INode? = null - override fun expectedConcept(): IConcept { - return link.targetConcept - } + override fun expectedConcept(): IConcept = link.targetConcept } -data class NonExistingNode(val concept: IConcept) : INonExistingNode { +data class NonExistingNode( + val concept: IConcept, +) : INonExistingNode { override fun getExistingAncestor(): INode? = null override fun getParent() = null @@ -167,23 +168,15 @@ data class NonExistingNode(val concept: IConcept) : INonExistingNode { override fun index(): Int = 0 - override fun replaceNode(subConcept: IConcept?): INode { - throw UnsupportedOperationException("Don't know where to create the node") - } + override fun replaceNode(subConcept: IConcept?): INode = throw UnsupportedOperationException("Don't know where to create the node") - override fun getOrCreateNode(subConcept: IConcept?): INode { - return replaceNode(subConcept) - } + override fun getOrCreateNode(subConcept: IConcept?): INode = replaceNode(subConcept) override fun nodeCreationDepth(): Int = 0 - override fun getNode(): INode? { - return null - } + override fun getNode(): INode? = null - override fun expectedConcept(): IConcept { - return concept - } + override fun expectedConcept(): IConcept = concept } fun IReadableNode.getContainmentLinkDefinition(): IChildLinkDefinition? { diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/IProducesHtml.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/IProducesHtml.kt index 54a3ca14..cdddf6ee 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/IProducesHtml.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/IProducesHtml.kt @@ -5,6 +5,7 @@ import kotlinx.html.TagConsumer interface IProducesHtml { fun isHtmlOutputValid(): Boolean = true + fun produceHtml(consumer: TagConsumer) } @@ -25,11 +26,10 @@ fun TagConsumer.produceChild(child: IProducesHtml?) { } } -fun IProducesHtml.toHtml(consumer: TagConsumer): T { - return if (consumer is IIncrementalTagConsumer) { +fun IProducesHtml.toHtml(consumer: TagConsumer): T = + if (consumer is IIncrementalTagConsumer) { consumer.produce(this)() } else { produceHtml(consumer) consumer.finalize() } -} 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 a0a7e161..828eefac 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/IncrementalVirtualDOMBuilder.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/IncrementalVirtualDOMBuilder.kt @@ -6,7 +6,11 @@ import kotlinx.html.Unsafe import kotlinx.html.org.w3c.dom.events.Event import org.modelix.incremental.AtomicLong -class IncrementalVirtualDOMBuilder(val document: IVirtualDom, existingRootElement: IVirtualDom.HTMLElement?, val generatedHtmlMap: GeneratedHtmlMap) : IIncrementalTagConsumer { +class IncrementalVirtualDOMBuilder( + val document: IVirtualDom, + existingRootElement: IVirtualDom.HTMLElement?, + val generatedHtmlMap: GeneratedHtmlMap, +) : IIncrementalTagConsumer { private inner class StackFrame { var forcedReuseNext: IVirtualDom.HTMLElement? = null var reusableChildren: ReusableChildren? = null @@ -34,9 +38,10 @@ class IncrementalVirtualDOMBuilder(val document: IVirtualDom, existingRootElemen fun applyChildren() { val parent: IVirtualDom.HTMLElement? = resultingHtml reusableChildren?.processStillUsed(generatedChildren.mapNotNull { it.producer }) - val generatedNodes = ArrayList(generatedChildren).map { - it.node ?: runProducer(it.producer!!) - } + val generatedNodes = + ArrayList(generatedChildren).map { + it.node ?: runProducer(it.producer!!) + } if (parent != null) { parent.childNodes.minus(generatedNodes.toSet()).forEach { parent.removeChild(it) } @@ -88,30 +93,38 @@ class IncrementalVirtualDOMBuilder(val document: IVirtualDom, existingRootElemen stack.add(frame) frame.tag = tag - val reusable: IVirtualDom.HTMLElement? = parentFrame.forcedReuseNext - ?.takeIf { it.tagName.lowercase() == tag.tagName.lowercase() } - ?: parentFrame.reusableChildren?.findReusable(tag) + val reusable: IVirtualDom.HTMLElement? = + parentFrame.forcedReuseNext + ?.takeIf { it.tagName.lowercase() == tag.tagName.lowercase() } + ?: parentFrame.reusableChildren?.findReusable(tag) parentFrame.forcedReuseNext = null if (reusable != null) { frame.reusableChildren = ReusableChildren(reusable) } - val element: IVirtualDom.HTMLElement = reusable ?: when { - tag.namespace != null -> TODO("namespaces not supported yet") - else -> document.createElement(tag.tagName) as IVirtualDom.HTMLElement - } + val element: IVirtualDom.HTMLElement = + reusable ?: when { + tag.namespace != null -> TODO("namespaces not supported yet") + else -> document.createElement(tag.tagName) as IVirtualDom.HTMLElement + } frame.resultingHtml = element parentFrame.generatedChildren.add(NodeOrProducer.node(element)) } - override fun onTagAttributeChange(tag: Tag, attribute: String, value: String?) { + override fun onTagAttributeChange( + tag: Tag, + attribute: String, + value: String?, + ) { // handled in StackFrame.applyAttributes } - override fun onTagEvent(tag: Tag, event: String, value: (Event) -> Unit) { - throw UnsupportedOperationException("Use DelayedConsumer") - } + override fun onTagEvent( + tag: Tag, + event: String, + value: (Event) -> Unit, + ): Unit = throw UnsupportedOperationException("Use DelayedConsumer") override fun onTagEnd(tag: Tag) { val frame = stack.last() @@ -129,17 +142,11 @@ class IncrementalVirtualDOMBuilder(val document: IVirtualDom, existingRootElemen frame.generatedChildren.add(NodeOrProducer.node(element)) } - override fun onTagContentEntity(entity: Entities) { - throw UnsupportedOperationException() - } + override fun onTagContentEntity(entity: Entities): Unit = throw UnsupportedOperationException() - override fun onTagContentUnsafe(block: Unsafe.() -> Unit) { - throw UnsupportedOperationException() - } + override fun onTagContentUnsafe(block: Unsafe.() -> Unit): Unit = throw UnsupportedOperationException() - override fun onTagComment(content: CharSequence) { - throw UnsupportedOperationException() - } + override fun onTagComment(content: CharSequence): Unit = throw UnsupportedOperationException() override fun finalize(): IVirtualDom.HTMLElement = lastClosed!!.resultingHtml!! @@ -149,24 +156,38 @@ class IncrementalVirtualDOMBuilder(val document: IVirtualDom, existingRootElemen reusableChildren = children.toMutableList() } constructor(parent: IVirtualDom.HTMLElement) { - reusableChildren = parent.childNodes - // .filter { it.generatedBy == null } - .toMutableList() + reusableChildren = + parent.childNodes + // .filter { it.generatedBy == null } + .toMutableList() } + fun processStillUsed(childProducers: List) { - val stillUsedElements: HashSet = childProducers.mapNotNull { generatedHtmlMap.getOutput(it) }.toHashSet() + val stillUsedElements: HashSet = + childProducers + .mapNotNull { + generatedHtmlMap.getOutput( + it + ) + }.toHashSet() reusableChildren.removeAll(stillUsedElements) reusableChildren.filterIsInstance().forEach { generatedHtmlMap.unassign(it) } } + fun findReusable(tag: Tag): IVirtualDom.HTMLElement? { // TODO only reuse those where the element in .generatedBy was removed/replaced (this is only known after generating all children) - val foundIndex = reusableChildren.indexOfFirst { it is IVirtualDom.HTMLElement && generatedHtmlMap.getProducer(it) == null && it.tagName.lowercase() == tag.tagName.lowercase() } + val foundIndex = + reusableChildren.indexOfFirst { + it is IVirtualDom.HTMLElement && generatedHtmlMap.getProducer(it) == null && + it.tagName.lowercase() == tag.tagName.lowercase() + } return if (foundIndex >= 0) { reusableChildren.removeAt(foundIndex) as IVirtualDom.HTMLElement } else { null } } + fun findReusableTextNode(text: String): IVirtualDom.Text? { val foundIndex = reusableChildren.indexOfFirst { it is IVirtualDom.Text && it.textContent == text } return if (foundIndex >= 0) { @@ -177,9 +198,13 @@ class IncrementalVirtualDOMBuilder(val document: IVirtualDom, existingRootElemen } } - private class NodeOrProducer(val producer: IProducesHtml?, val node: IVirtualDom.Node?) { + private class NodeOrProducer( + val producer: IProducesHtml?, + val node: IVirtualDom.Node?, + ) { companion object { fun producer(producer: IProducesHtml) = NodeOrProducer(producer, null) + fun node(node: IVirtualDom.Node) = NodeOrProducer(null, node) } } @@ -191,25 +216,33 @@ class GeneratedHtmlMap { private val producerIds: MutableMap = HashMap() private val idSequence = AtomicLong() - fun getProducerId(producer: IProducesHtml): Long { - return producerIds.getOrPut(producer) { idSequence.incrementAndGet() } - } + fun getProducerId(producer: IProducesHtml): Long = producerIds.getOrPut(producer) { idSequence.incrementAndGet() } private var IProducesHtml.generatedHtml: IVirtualDom.HTMLElement? get() = producer2element[this] - set(value) { if (value == null) producer2element.remove(this) else producer2element[this] = value } + set(value) { + if (value == null) producer2element.remove(this) else producer2element[this] = value + } private var IVirtualDom.HTMLElement.generatedBy: IProducesHtml? get() = element2producer[this] - set(value) { if (value == null) element2producer.remove(this) else element2producer[this] = value } + set(value) { + if (value == null) element2producer.remove(this) else element2producer[this] = value + } - fun reassign(producer: IProducesHtml, output: IVirtualDom.HTMLElement) { + fun reassign( + producer: IProducesHtml, + output: IVirtualDom.HTMLElement, + ) { unassign(producer) unassign(output) assign(producer, output) } - fun assign(producer: IProducesHtml, output: IVirtualDom.HTMLElement) { + fun assign( + producer: IProducesHtml, + output: IVirtualDom.HTMLElement, + ) { require(producer.generatedHtml == null) require(output.generatedBy == null) producer.generatedHtml = output @@ -217,12 +250,17 @@ class GeneratedHtmlMap { } fun unassign(producer: IProducesHtml) = producer.generatedHtml?.let { unassign(producer, it) } + fun unassign(output: IVirtualDom.HTMLElement) = output.generatedBy?.let { unassign(it, output) } fun getProducer(output: IVirtualDom.HTMLElement) = output.generatedBy + fun getOutput(producer: IProducesHtml) = producer.generatedHtml - private fun unassign(producer: IProducesHtml, output: IVirtualDom.HTMLElement) { + private fun unassign( + producer: IProducesHtml, + output: IVirtualDom.HTMLElement, + ) { require(producer.generatedHtml == output) require(output.generatedBy == producer) producer.generatedHtml = null diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/JSKeyboardEvent.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/JSKeyboardEvent.kt index fb5c5051..bb6ca648 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/JSKeyboardEvent.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/JSKeyboardEvent.kt @@ -12,7 +12,7 @@ class JSKeyboardEvent( val location: KeyLocation = KeyLocation.STANDARD, val repeat: Boolean = false, val composing: Boolean = false, -) { +) : JSUIEvent { constructor(eventType: JSKeyboardEventType, knownKey: KnownKeys) : this(eventType, null, knownKey, knownKey.name, Modifiers.NONE, KeyLocation.STANDARD, false, false) } diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/JSMouseEvent.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/JSMouseEvent.kt index 25dc8726..f3659bfc 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/JSMouseEvent.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/JSMouseEvent.kt @@ -2,6 +2,8 @@ package org.modelix.editor import kotlinx.serialization.Serializable +sealed interface JSUIEvent + @Serializable data class JSMouseEvent( val eventType: JSMouseEventType, @@ -10,25 +12,31 @@ data class JSMouseEvent( val modifiers: Modifiers = Modifiers.NONE, val button: Short, val buttons: Short, -) { - fun getButtonAsEnum(): JSMouseButton = when (button) { - 0.toShort() -> JSMouseButton.PRIMARY - 1.toShort() -> JSMouseButton.AUXILIARY - 2.toShort() -> JSMouseButton.SECONDARY - 3.toShort() -> JSMouseButton.FOURTH - 4.toShort() -> JSMouseButton.FIFTH - else -> JSMouseButton.NONE - } +) : JSUIEvent { + fun getButtonAsEnum(): JSMouseButton = + when (button) { + 0.toShort() -> JSMouseButton.PRIMARY + 1.toShort() -> JSMouseButton.AUXILIARY + 2.toShort() -> JSMouseButton.SECONDARY + 3.toShort() -> JSMouseButton.FOURTH + 4.toShort() -> JSMouseButton.FIFTH + else -> JSMouseButton.NONE + } fun getButtonsAsEnum(): Set { - val bitToValue = listOf( - JSMouseButton.PRIMARY, - JSMouseButton.SECONDARY, - JSMouseButton.AUXILIARY, - JSMouseButton.FOURTH, - JSMouseButton.FIFTH, - ) - return bitToValue.withIndex().filter { (buttons.toInt() ushr it.index) and 1 == 1 }.map { it.value }.toSet() + val bitToValue = + listOf( + JSMouseButton.PRIMARY, + JSMouseButton.SECONDARY, + JSMouseButton.AUXILIARY, + JSMouseButton.FOURTH, + JSMouseButton.FIFTH, + ) + return bitToValue + .withIndex() + .filter { (buttons.toInt() ushr it.index) and 1 == 1 } + .map { it.value } + .toSet() } } diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/ModelApiExtensions.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/ModelApiExtensions.kt index b9ca25b6..ae6af8ab 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/ModelApiExtensions.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/ModelApiExtensions.kt @@ -11,27 +11,29 @@ import org.modelix.metamodel.setTypedPropertyValue import org.modelix.model.api.INode import org.modelix.model.api.IProperty -fun INode.getBooleanPropertyValue(property: IProperty): Boolean { - return getTypedPropertyValue(TypedPropertyAdapter(property, MandatoryBooleanPropertySerializer)) -} +fun INode.getBooleanPropertyValue(property: IProperty): Boolean = + getTypedPropertyValue(TypedPropertyAdapter(property, MandatoryBooleanPropertySerializer)) -fun INode.setBooleanPropertyValue(property: IProperty, value: Boolean?) { - return setTypedPropertyValue(TypedPropertyAdapter(property, OptionalBooleanPropertySerializer), value) -} +fun INode.setBooleanPropertyValue( + property: IProperty, + value: Boolean?, +) = setTypedPropertyValue(TypedPropertyAdapter(property, OptionalBooleanPropertySerializer), value) -fun INode.getIntPropertyValue(property: IProperty): Int { - return getTypedPropertyValue(TypedPropertyAdapter(property, MandatoryIntPropertySerializer)) -} +fun INode.getIntPropertyValue(property: IProperty): Int = + getTypedPropertyValue(TypedPropertyAdapter(property, MandatoryIntPropertySerializer)) -fun INode.setIntPropertyValue(property: IProperty, value: Int?) { - return setTypedPropertyValue(TypedPropertyAdapter(property, OptionalIntPropertySerializer), value) -} +fun INode.setIntPropertyValue( + property: IProperty, + value: Int?, +) = setTypedPropertyValue(TypedPropertyAdapter(property, OptionalIntPropertySerializer), value) class TypedPropertyAdapter( private val untypedProperty: IProperty, val serializer: IPropertyValueSerializer, ) : ITypedProperty { override fun untyped() = untypedProperty + override fun serializeValue(value: ValueT) = serializer.serialize(value) + override fun deserializeValue(serialized: String?) = serializer.deserialize(serialized) } 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 c0c04d3b..c1f3247f 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/ReplaceNodeActionProvider.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/ReplaceNodeActionProvider.kt @@ -1,23 +1,29 @@ package org.modelix.editor import org.modelix.constraints.ConstraintsAspect +import org.modelix.editor.text.backend.BackendEditorComponent import org.modelix.model.api.IReferenceLink import org.modelix.model.api.getAllSubConcepts import org.modelix.scopes.ScopeAspect -data class ReplaceNodeActionProvider(val location: INonExistingNode) : ICodeCompletionActionProvider { +data class ReplaceNodeActionProvider( + val location: INonExistingNode, +) : ICodeCompletionActionProvider { override fun getApplicableActions(parameters: CodeCompletionParameters): List { - val engine = parameters.editor.engine ?: return emptyList() + val engine = parameters.editor.engine val expectedConcept = location.expectedConcept() ?: return emptyList() - val allowedConcepts = expectedConcept.getAllSubConcepts(true) - .filterNot { it.isAbstract() } - .filter { concept -> - val newNode = location.replacement(concept) - ConstraintsAspect.canCreate(newNode) + val allowedConcepts = + expectedConcept + .getAllSubConcepts(true) + .filterNot { it.isAbstract() } + .filter { concept -> + val newNode = location.replacement(concept) + ConstraintsAspect.canCreate(newNode) + } + val cellModels = + allowedConcepts.map { concept -> + engine.createCellModel(concept) } - val cellModels = allowedConcepts.map { concept -> - engine.createCellModel(concept) - } return cellModels.flatMap { it.getInstantiationActions(location, parameters) ?: emptyList() } @@ -29,7 +35,11 @@ data class ReplaceNodeActionProvider(val location: INonExistingNode) : ICodeComp } } -data class ReferenceTargetActionProvider(val sourceNode: INonExistingNode, val link: IReferenceLink, val presentation: (INonExistingNode) -> String) : ICodeCompletionActionProvider { +data class ReferenceTargetActionProvider( + val sourceNode: INonExistingNode, + val link: IReferenceLink, + val presentation: (INonExistingNode) -> String, +) : ICodeCompletionActionProvider { override fun getApplicableActions(parameters: CodeCompletionParameters): List { val scope = ScopeAspect.getScope(sourceNode, link) val targetNodes = scope.getVisibleElements(sourceNode, link) @@ -39,18 +49,19 @@ data class ReferenceTargetActionProvider(val sourceNode: INonExistingNode, val l } } -class ChangeReferenceTargetAction(val sourceLocation: INonExistingNode, val link: IReferenceLink, val targetNode: INonExistingNode, val presentation: String) : ICodeCompletionAction { - override fun getMatchingText(): String { - return presentation - } +class ChangeReferenceTargetAction( + val sourceLocation: INonExistingNode, + val link: IReferenceLink, + val targetNode: INonExistingNode, + val presentation: String, +) : ICodeCompletionAction { + override fun getMatchingText(): String = presentation - override fun getDescription(): String { - return "set reference '" + link.getSimpleName() + "'" - } + override fun getDescription(): String = "set reference '" + link.getSimpleName() + "'" - override fun execute(editor: EditorComponent): CaretPositionPolicy? { + override fun execute(editor: BackendEditorComponent): CaretPositionPolicy? { val sourceNode = sourceLocation.getOrCreateNode(null) sourceNode.setReferenceTarget(link, targetNode.getOrCreateNode()) - return CaretPositionPolicy(ReferencedNodeCellReference(sourceNode.reference, link)) + return CaretPositionPolicy(ReferencedNodeCellReference(sourceNode.reference, link.toReference())) } } diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/Selection.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/Selection.kt index 4ad68a5f..662ab35e 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/Selection.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/Selection.kt @@ -1,11 +1,17 @@ package org.modelix.editor +import org.modelix.editor.text.shared.celltree.ICellTree + abstract class Selection : IKeyboardHandler { abstract fun isValid(): Boolean - abstract fun update(editor: EditorComponent): Selection? - abstract fun getSelectedCells(): List + + abstract fun update(editor: FrontendEditorComponent): Selection? + + abstract fun getSelectedCells(): List } -abstract class SelectionView(val selection: E) : IProducesHtml { +abstract class SelectionView( + val selection: E, +) : IProducesHtml { abstract fun update() } diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/TextLayouter.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/TextLayouter.kt index 2e590a7a..475cfda4 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/TextLayouter.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/TextLayouter.kt @@ -4,13 +4,20 @@ import kotlinx.html.TagConsumer import kotlinx.html.div import kotlinx.html.span import kotlinx.html.style +import org.modelix.editor.text.frontend.editorComponent +import org.modelix.editor.text.frontend.getVisibleText +import org.modelix.editor.text.frontend.text +import org.modelix.editor.text.frontend.type +import org.modelix.editor.text.shared.celltree.ICellTree import org.modelix.incremental.IncrementalList -class TextLine(words_: Iterable) : IProducesHtml { +class TextLine( + words_: Iterable, +) : IProducesHtml { var initialText: LayoutedText? = null var finalText: LayoutedText? = null val words: List = words_.toList() - val layoutablesIndexList: IncrementalList> = + val layoutablesIndexList: IncrementalList> = IncrementalList.of(words.filterIsInstance().map { it.cell to it }) init { @@ -57,7 +64,7 @@ class LayoutedText( var indent: Int = 0, ) : IProducesHtml { var owner: LayoutedText? = null - val layoutablesIndexList: IncrementalList> = + val layoutablesIndexList: IncrementalList> = IncrementalList.concat(lines.map { it.layoutablesIndexList }) init { @@ -104,13 +111,14 @@ class TextLayouter { val endsWithNoSpace = !autoInsertSpace val endsWithNewLine = insertNewLineNext closeLine() - val newText = LayoutedText( - TreeList.flatten(closedLines), - beginsWithNewLine = beginsWithNewLine, - endsWithNewLine = endsWithNewLine, - beginsWithNoSpace = beginsWithNoSpace, - endsWithNoSpace = endsWithNoSpace, - ) + val newText = + LayoutedText( + TreeList.flatten(closedLines), + beginsWithNewLine = beginsWithNewLine, + endsWithNewLine = endsWithNewLine, + beginsWithNoSpace = beginsWithNoSpace, + endsWithNoSpace = endsWithNoSpace, + ) childTexts.forEach { it.owner = newText } return newText } @@ -148,10 +156,12 @@ class TextLayouter { if (isEmpty()) beginsWithNewLine = true insertNewLineNext = true } + fun emptyLine() { addNewLine() onNewLine() } + fun withIndent(body: () -> Unit) { val oldIndent = currentIndent try { @@ -161,6 +171,7 @@ class TextLayouter { currentIndent = oldIndent } } + fun noSpace() { if (isEmpty()) beginsWithNoSpace = true autoInsertSpace = false @@ -234,8 +245,11 @@ abstract class Layoutable : IProducesHtml { var finalLine: TextLine? = null abstract fun getLength(): Int + abstract fun isWhitespace(): Boolean + abstract fun toText(): String + override fun toString(): String = toText() fun getX(): Int { @@ -265,9 +279,7 @@ abstract class Layoutable : IProducesHtml { return if (next) nonEmptySiblingLine.words.first() else nonEmptySiblingLine.words.last() } - fun getSiblingsInText(next: Boolean): Sequence { - return generateSequence(getSiblingInText(next)) { it.getSiblingInText(next) } - } + fun getSiblingsInText(next: Boolean): Sequence = generateSequence(getSiblingInText(next)) { it.getSiblingInText(next) } } /*class LayoutableWord(val text: String) : ILayoutable { @@ -278,30 +290,35 @@ abstract class Layoutable : IProducesHtml { consumer.onTagContent(text.useNbsp()) } }*/ -class LayoutableCell(val cell: Cell) : Layoutable() { +class LayoutableCell( + val cell: ICellTree.Cell, +) : Layoutable() { init { - require(cell.data is TextCellData) { "Not a text cell: $cell" } - } - override fun getLength(): Int { - return toText().length - } - override fun toText(): String { - return cell.getProperty(CommonCellProperties.textReplacement) - ?: (cell.data as TextCellData).getVisibleText(cell) + require(cell.type == ECellType.TEXT) { "Not a text cell: $cell" } } + + override fun getLength(): Int = toText().length + + override fun toText(): String = + cell.getProperty(CommonCellProperties.textReplacement) + ?: cell.getVisibleText() + override fun isWhitespace(): Boolean = false + override fun produceHtml(consumer: TagConsumer) { val textIsOverridden = cell.getProperty(CommonCellProperties.textReplacement) != null - val isPlaceholder = (cell.data as TextCellData).text.isEmpty() - val textColor = when { - textIsOverridden -> "#A81E1E" - isPlaceholder -> cell.getProperty(CommonCellProperties.placeholderTextColor) - else -> cell.getProperty(CommonCellProperties.textColor) - } - val backgroundColor = when { - textIsOverridden -> "rgba(255, 0, 0, 0.5)" - else -> null - } + val isPlaceholder = cell.text.isNullOrEmpty() + val textColor = + when { + textIsOverridden -> "#A81E1E" + isPlaceholder -> cell.getProperty(CommonCellProperties.placeholderTextColor) + else -> cell.getProperty(CommonCellProperties.textColor) + } + val backgroundColor = + when { + textIsOverridden -> "rgba(255, 0, 0, 0.5)" + else -> null + } consumer.span("text-cell") { val styleParts = mutableListOf() if (textColor != null) styleParts += "color: $textColor" @@ -315,24 +332,34 @@ class LayoutableCell(val cell: Cell) : Layoutable() { fun Cell.layoutable(): LayoutableCell? { // return rootCell().layout.lines.asSequence().flatMap { it.words }.filterIsInstance().find { it.cell == this } - return editorComponent?.resolveLayoutable(this) + return editorComponent.resolveLayoutable(this) } -class LayoutableIndent(val indentSize: Int) : Layoutable() { +class LayoutableIndent( + val indentSize: Int, +) : Layoutable() { fun totalIndent() = indentSize + (initialLine?.getContextIndent() ?: 0) + override fun getLength(): Int = totalIndent() * 2 + override fun isWhitespace(): Boolean = true + override fun toText(): String = (1..totalIndent()).joinToString("") { " " } + override fun produceHtml(consumer: TagConsumer) { consumer.span("indent") { +toText().useNbsp() } } } -class LayoutableSpace() : Layoutable() { + +class LayoutableSpace : Layoutable() { override fun getLength(): Int = 1 + override fun isWhitespace(): Boolean = true + override fun toText(): String = " " + override fun produceHtml(consumer: TagConsumer) { consumer.span { +Typography.nbsp.toString() diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/TreeList.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/TreeList.kt index 61a8575e..98a690c4 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/TreeList.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/TreeList.kt @@ -2,38 +2,40 @@ package org.modelix.editor abstract class TreeList : Iterable { abstract val size: Int + operator fun get(index: Int): E { require(index in 0 until size) { "$index not in range 0 until $size" } return getUnsafe(index) } + abstract fun getUnsafe(index: Int): E + abstract fun asSequence(): Sequence - override fun iterator(): Iterator { - return asSequence().iterator() - } + + override fun iterator(): Iterator = asSequence().iterator() abstract fun withoutLast(): TreeList + abstract fun withoutFirst(): TreeList + abstract fun last(): E? + abstract fun first(): E? + fun isNotEmpty() = asSequence().iterator().hasNext() companion object { - fun of(vararg elements: T): TreeList { - return TreeListParent(elements.map { TreeListLeaf(it) }).normalized() - } + fun of(vararg elements: T): TreeList = TreeListParent(elements.map { TreeListLeaf(it) }).normalized() - fun fromCollection(elements: Collection): TreeList { - return TreeListParent(elements.map { TreeListLeaf(it) }).normalized() - } + fun fromCollection(elements: Collection): TreeList = TreeListParent(elements.map { TreeListLeaf(it) }).normalized() - fun flatten(elements: Iterable>): TreeList { - return TreeListParent(elements.toList()).normalized() - } + fun flatten(elements: Iterable>): TreeList = TreeListParent(elements.toList()).normalized() } } -private class TreeListLeaf(val element: E) : TreeList() { +private class TreeListLeaf( + val element: E, +) : TreeList() { override val size: Int get() = 1 @@ -42,28 +44,20 @@ private class TreeListLeaf(val element: E) : TreeList() { return element } - override fun asSequence(): Sequence { - return sequenceOf(element) - } + override fun asSequence(): Sequence = sequenceOf(element) - override fun withoutLast(): TreeList { - return TreeListEmpty() - } + override fun withoutLast(): TreeList = TreeListEmpty() - override fun withoutFirst(): TreeList { - return TreeListEmpty() - } + override fun withoutFirst(): TreeList = TreeListEmpty() - override fun last(): E { - return element - } + override fun last(): E = element - override fun first(): E? { - return element - } + override fun first(): E? = element } -private class TreeListParent(val children: List>) : TreeList() { +private class TreeListParent( + val children: List>, +) : TreeList() { override val size: Int = children.sumOf { it.size } override fun getUnsafe(index: Int): E { @@ -75,25 +69,15 @@ private class TreeListParent(val children: List>) : TreeList() throw IndexOutOfBoundsException("index: $index, size: $size") } - override fun asSequence(): Sequence { - return children.asSequence().flatMap { it.asSequence() } - } + override fun asSequence(): Sequence = children.asSequence().flatMap { it.asSequence() } - override fun withoutLast(): TreeList { - return TreeListParent(children.dropLast(1).plusElement(children.last().withoutLast())).normalized() - } + override fun withoutLast(): TreeList = TreeListParent(children.dropLast(1).plusElement(children.last().withoutLast())).normalized() - override fun withoutFirst(): TreeList { - return TreeListParent(listOf(children.first().withoutFirst()) + children.drop(1)).normalized() - } + override fun withoutFirst(): TreeList = TreeListParent(listOf(children.first().withoutFirst()) + children.drop(1)).normalized() - override fun last(): E? { - return children.last().last() - } + override fun last(): E? = children.last().last() - override fun first(): E? { - return children.first().first() - } + override fun first(): E? = children.first().first() fun normalized(): TreeList { val withoutEmpty = this.children.filter { it !is TreeListEmpty } @@ -109,27 +93,15 @@ private class TreeListEmpty : TreeList() { override val size: Int get() = 0 - override fun getUnsafe(index: Int): E { - throw IndexOutOfBoundsException("index = $index, size = 0") - } + override fun getUnsafe(index: Int): E = throw IndexOutOfBoundsException("index = $index, size = 0") - override fun asSequence(): Sequence { - return emptySequence() - } + override fun asSequence(): Sequence = emptySequence() - override fun withoutLast(): TreeList { - return this - } + override fun withoutLast(): TreeList = this - override fun withoutFirst(): TreeList { - return this - } + override fun withoutFirst(): TreeList = this - override fun last(): E? { - return null - } + override fun last(): E? = null - override fun first(): E? { - return null - } + override fun first(): E? = null } 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 24780620..dcff247f 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/VirtualDom.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/VirtualDom.kt @@ -9,97 +9,146 @@ interface IVirtualDom { interface Node { fun getVDom(): IVirtualDom + val parent: IVirtualDom.Node? val childNodes: List + fun getUserObject(key: String): Any? - fun putUserObject(key: String, value: Any?) - fun insertBefore(newNode: IVirtualDom.Node, referenceNode: IVirtualDom.Node?): IVirtualDom.Node + + fun putUserObject( + key: String, + value: Any?, + ) + + fun insertBefore( + newNode: IVirtualDom.Node, + referenceNode: IVirtualDom.Node?, + ): IVirtualDom.Node + fun appendChild(child: IVirtualDom.Node): IVirtualDom.Node - fun replaceChild(newChild: IVirtualDom.Node, oldChild: IVirtualDom.Node): IVirtualDom.Node + + fun replaceChild( + newChild: IVirtualDom.Node, + oldChild: IVirtualDom.Node, + ): IVirtualDom.Node + fun removeChild(child: IVirtualDom.Node): IVirtualDom.Node + fun remove() } + interface Element : Node { val tagName: String + fun getAttributeNames(): Array + fun getAttribute(qualifiedName: String): String? - fun setAttribute(qualifiedName: String, value: String) + + fun setAttribute( + qualifiedName: String, + value: String, + ) + fun removeAttribute(qualifiedName: String) + fun getAttributes(): Map fun getInnerBounds(): Bounds + fun getOuterBounds(): Bounds } + interface HTMLElement : Element + interface Text : Node { var textContent: String? } fun getElementById(id: String): Element? + fun createElement(localName: String): IVirtualDom.Element + fun createTextNode(data: String): IVirtualDom.Text companion object { fun newInstance(): IVirtualDom = newInstance(DummyUI()) + fun newInstance(ui: IVirtualDomUI): IVirtualDom = VirtualDom(ui) } } -fun IVirtualDom.create(): TagConsumer { - return IncrementalVirtualDOMBuilder(this, null, GeneratedHtmlMap()) -} +fun IVirtualDom.create(): TagConsumer = IncrementalVirtualDOMBuilder(this, null, GeneratedHtmlMap()) private class DummyUI : IVirtualDomUI { override fun getOuterBounds(element: IVirtualDom.Element): Bounds = Bounds.ZERO + override fun getInnerBounds(element: IVirtualDom.Element): Bounds = Bounds.ZERO - override fun getElementsAt(x: Double, y: Double): List = emptyList() + + override fun getElementsAt( + x: Double, + y: Double, + ): List = emptyList() } -fun IVirtualDom.Node.descendants(includeSelf: Boolean = false): Sequence { - return if (includeSelf) { +fun IVirtualDom.Node.descendants(includeSelf: Boolean = false): Sequence = + if (includeSelf) { sequenceOf(this) + descendants(false) } else { childNodes.asSequence().flatMap { it.descendants(true) } } -} -fun IVirtualDom.Element.getClasses(): Set { - return getAttribute("class")?.let { - it.split(' ').asSequence().map { it.trim() }.filter { it.isNotEmpty() }.toSet() +fun IVirtualDom.Element.getClasses(): Set = + getAttribute("class")?.let { + it + .split(' ') + .asSequence() + .map { it.trim() } + .filter { it.isNotEmpty() } + .toSet() } ?: emptySet() -} + fun IVirtualDom.Element.removeClass(name: String) { val classes = getClasses() if (!classes.contains(name)) return setAttribute("class", (classes - name).joinToString(" ")) } + fun IVirtualDom.Element.addClass(name: String) { val classes = getClasses() if (classes.contains(name)) return setAttribute("class", (classes + name).joinToString(" ")) } -fun IVirtualDom.Element.innerText(): String { - return (childNodes.single() as IVirtualDom.Text).textContent ?: "" -} +fun IVirtualDom.Element.innerText(): String = (childNodes.single() as IVirtualDom.Text).textContent ?: "" val IVirtualDom.HTMLElement.style: VirtualDomStyle get() = VirtualDomStyle(this) object StyleAttributeDelegate { - operator fun getValue(style: VirtualDomStyle, property: KProperty<*>): String? { - return style[property.name] - } - - operator fun setValue(style: VirtualDomStyle, property: KProperty<*>, value: String?) { + operator fun getValue( + style: VirtualDomStyle, + property: KProperty<*>, + ): String? = style[property.name] + + operator fun setValue( + style: VirtualDomStyle, + property: KProperty<*>, + value: String?, + ) { style[property.name] = value } } -object ElementAttributeDelegate { - operator fun getValue(element: IVirtualDom.Element, property: KProperty<*>): String? { - return element.getAttribute(property.name) - } - operator fun setValue(element: IVirtualDom.Element, property: KProperty<*>, value: String?) { +object ElementAttributeDelegate { + operator fun getValue( + element: IVirtualDom.Element, + property: KProperty<*>, + ): String? = element.getAttribute(property.name) + + operator fun setValue( + element: IVirtualDom.Element, + property: KProperty<*>, + value: String?, + ) { if (value == null) { element.removeAttribute(property.name) } else { @@ -116,16 +165,26 @@ var VirtualDomStyle.top by StyleAttributeDelegate var VirtualDomStyle.width by StyleAttributeDelegate var VirtualDomStyle.height by StyleAttributeDelegate -class VirtualDomStyle(private val element: IVirtualDom.Element) { - fun toMap(): Map = (element.getAttribute("style") ?: "") - .split(';') - .map { it.split(':', limit = 2) } - .filter { it.size == 2 } - .associate { it[0].trim() to it[1].trim() } +class VirtualDomStyle( + private val element: IVirtualDom.Element, +) { + fun toMap(): Map = + (element.getAttribute("style") ?: "") + .split(';') + .map { it.split(':', limit = 2) } + .filter { it.size == 2 } + .associate { it[0].trim() to it[1].trim() } + operator fun get(name: String): String? = toMap()[name] - operator fun set(name: String, value: String?) = toMap().toMutableMap() + + operator fun set( + name: String, + value: String?, + ) = toMap() + .toMutableMap() .also { if (value == null) it.remove(name) else it[name] = value } - .entries.joinToString(";") { "${it.key}:${it.value}" } + .entries + .joinToString(";") { "${it.key}:${it.value}" } .let { element.setAttribute("style", it) } } @@ -140,24 +199,30 @@ fun IVirtualDom.HTMLElement.setBounds(bounds: Bounds) { interface IVirtualDomUI { fun getOuterBounds(element: IVirtualDom.Element): Bounds + fun getInnerBounds(element: IVirtualDom.Element): Bounds - fun getElementsAt(x: Double, y: Double): List + + fun getElementsAt( + x: Double, + y: Double, + ): List } -class VirtualDom(override val ui: IVirtualDomUI, val idPrefix: String = "") : IVirtualDom { +class VirtualDom( + override val ui: IVirtualDomUI, + val idPrefix: String = "", +) : IVirtualDom { private val idSequence = AtomicLong() private val elementsMap: MutableMap = HashMap() - override fun getElementById(id: String): IVirtualDom.Element? { - return elementsMap[id]?.takeIf { it.id == id } - } + override fun getElementById(id: String): IVirtualDom.Element? = elementsMap[id]?.takeIf { it.id == id } - override fun createElement(localName: String): Element { - return HTMLElement(localName).also { it.id = idPrefix + idSequence.incrementAndGet().toString() } - } - override fun createTextNode(data: String): Text { - return Text().also { it.textContent = data } - } + override fun createElement(localName: String): Element = + HTMLElement(localName).also { + it.id = idPrefix + idSequence.incrementAndGet().toString() + } + + override fun createTextNode(data: String): Text = Text().also { it.textContent = data } open inner class Node : IVirtualDom.Node { private var wasModified: Boolean = true @@ -192,7 +257,10 @@ class VirtualDom(override val ui: IVirtualDomUI, val idPrefix: String = "") : IV override fun getUserObject(key: String): Any? = userObjects[key] - override fun putUserObject(key: String, value: Any?) { + override fun putUserObject( + key: String, + value: Any?, + ) { if (value == null) { userObjects.remove(key) } else { @@ -200,7 +268,10 @@ class VirtualDom(override val ui: IVirtualDomUI, val idPrefix: String = "") : IV } } - override fun insertBefore(newNode: IVirtualDom.Node, referenceNode: IVirtualDom.Node?): IVirtualDom.Node { + override fun insertBefore( + newNode: IVirtualDom.Node, + referenceNode: IVirtualDom.Node?, + ): IVirtualDom.Node { if (referenceNode == null) return appendChild(newNode) val index = childNodes.indexOf(referenceNode) require(index >= 0) { "$referenceNode is not a child of $this" } @@ -213,7 +284,10 @@ class VirtualDom(override val ui: IVirtualDomUI, val idPrefix: String = "") : IV return child } - override fun replaceChild(newChild: IVirtualDom.Node, oldChild: IVirtualDom.Node): IVirtualDom.Node { + 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" } @@ -235,7 +309,10 @@ class VirtualDom(override val ui: IVirtualDomUI, val idPrefix: String = "") : IV markModified() } - fun addChild(index: Int, child: IVirtualDom.Node) { + 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) @@ -249,11 +326,20 @@ class VirtualDom(override val ui: IVirtualDomUI, val idPrefix: String = "") : IV } } - open inner class Element(override val tagName: String) : Node(), IVirtualDom.Element { + open inner class Element( + override val tagName: String, + ) : Node(), + IVirtualDom.Element { private val attributes: MutableMap = LinkedHashMap() + override fun getAttributeNames(): Array = attributes.keys.toTypedArray() + override fun getAttribute(qualifiedName: String): String? = attributes[qualifiedName] - override fun setAttribute(qualifiedName: String, value: String) { + + override fun setAttribute( + qualifiedName: String, + value: String, + ) { if (attributes[qualifiedName] == value) return attributes[qualifiedName] = value if (qualifiedName == "id") { @@ -261,18 +347,21 @@ class VirtualDom(override val ui: IVirtualDomUI, val idPrefix: String = "") : IV } markModified() } + override fun removeAttribute(qualifiedName: String) { if (attributes.remove(qualifiedName) != null) { markModified() } } + 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 { + override fun toString(): String = + buildString { append("<$tagName>") attributes.forEach { attribute -> append(" ") @@ -287,12 +376,16 @@ class VirtualDom(override val ui: IVirtualDomUI, val idPrefix: String = "") : IV } append("") } - } } - inner class HTMLElement(tagName: String) : Element(tagName), IVirtualDom.HTMLElement + inner class HTMLElement( + tagName: String, + ) : Element(tagName), + IVirtualDom.HTMLElement - inner class Text : Node(), IVirtualDom.Text { + inner class Text : + Node(), + IVirtualDom.Text { override var textContent: String? = null set(value) { if (field == value) return @@ -300,8 +393,6 @@ class VirtualDom(override val ui: IVirtualDomUI, val idPrefix: String = "") : IV markModified() } - override fun toString(): String { - return textContent ?: "" - } + override fun toString(): String = textContent ?: "" } } diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/CellTemplate.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/CellTemplate.kt index 01c26e13..e6467ca2 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/CellTemplate.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/CellTemplate.kt @@ -2,20 +2,20 @@ package org.modelix.editor.celltemplate import org.modelix.editor.CellActionProperties import org.modelix.editor.CellCreationContext -import org.modelix.editor.CellData import org.modelix.editor.CellProperties +import org.modelix.editor.CellSpecBase +import org.modelix.editor.CellTreeState import org.modelix.editor.ChildCellTemplateReference import org.modelix.editor.CodeCompletionParameters import org.modelix.editor.CommonCellProperties import org.modelix.editor.ECellLayout -import org.modelix.editor.EditorState import org.modelix.editor.IActionOrProvider import org.modelix.editor.ICellTemplateReference import org.modelix.editor.ICodeCompletionAction import org.modelix.editor.ICompletionTokenOrList import org.modelix.editor.INonExistingNode import org.modelix.editor.TemplateCellReference -import org.modelix.editor.TextCellData +import org.modelix.editor.TextCellSpec import org.modelix.editor.asProvider import org.modelix.editor.asTokenList import org.modelix.editor.withTokens @@ -27,7 +27,9 @@ import org.modelix.model.api.INodeReference import org.modelix.model.api.IWritableNode import kotlin.jvm.JvmName -abstract class CellTemplate(val concept: IConcept) { +abstract class CellTemplate( + val concept: IConcept, +) { val properties = CellProperties() private val children: MutableList = ArrayList() @@ -35,25 +37,42 @@ abstract class CellTemplate(val concept: IConcept) { @set:JvmName("setReferenceField") protected var reference: ICellTemplateReference? = null val withNode: MutableList<(node: INode) -> Unit> = ArrayList() - fun apply(context: CellCreationContext, node: INode): CellData { + + fun apply( + context: CellCreationContext, + node: INode, + ): CellSpecBase { val cellData = createCell(context, node) cellData.properties.addAll(properties) cellData.children.addAll(applyChildren(context, node, cellData)) if (properties[CommonCellProperties.layout] == ECellLayout.VERTICAL) { - cellData.children.drop(1).forEach { (it as CellData).properties[CommonCellProperties.onNewLine] = true } + cellData.children.drop(1).forEach { (it as CellSpecBase).properties[CommonCellProperties.onNewLine] = true } } withNode.forEach { it(node) } val cellReference: TemplateCellReference = createCellReference(node) cellData.cellReferences.add(cellReference) - applyTextReplacement(cellData, context.editorState) + applyTextReplacement(cellData, context.cellTreeState) return cellData } - protected open fun applyChildren(context: CellCreationContext, node: INode, cell: CellData): List { - return children.map { it.apply(context, node) } - } - protected abstract fun createCell(context: CellCreationContext, node: INode): CellData - open fun getInstantiationActions(location: INonExistingNode, parameters: CodeCompletionParameters): List? { + protected open fun applyChildren( + context: CellCreationContext, + node: INode, + cell: CellSpecBase, + ): List = + children.map { + it.apply(context, node) + } + + protected abstract fun createCell( + context: CellCreationContext, + node: INode, + ): CellSpecBase + + open fun getInstantiationActions( + location: INonExistingNode, + parameters: CodeCompletionParameters, + ): List? { val completionText = properties[CommonCellProperties.codeCompletionText] if (completionText != null) { return listOf(InstantiateNodeCompletionAction(completionText, concept, location)) @@ -64,7 +83,11 @@ abstract class CellTemplate(val concept: IConcept) { if (actions != null) { val nextTokens = children.drop(index + 1).mapNotNull { it.toCompletionToken() }.asTokenList() if (!nextTokens.isEmpty()) { - return actions.map { it.asProvider().withTokens { innerTokens -> listOf(innerTokens, nextTokens).asTokenList().normalize() } } + return actions.map { + it.asProvider().withTokens { innerTokens -> + listOf(innerTokens, nextTokens).asTokenList().normalize() + } + } } return actions } @@ -73,28 +96,36 @@ abstract class CellTemplate(val concept: IConcept) { return null } - fun getSideTransformActions(before: Boolean, nodeToReplace: INode): List? { + fun getSideTransformActions( + before: Boolean, + nodeToReplace: INode, + ): List? { val symbols = getGrammarSymbols().toList() val conceptToReplace = nodeToReplace.concept ?: return null - return symbols.mapIndexedNotNull { index, symbol -> - if (symbol is ChildCellTemplate && conceptToReplace.isSubConceptOf(symbol.link.targetConcept)) { - val prevNextIndex = if (before)index - 1 else index + 1 - val prevNextSymbol = symbols.getOrNull(prevNextIndex) ?: return@mapIndexedNotNull null - return@mapIndexedNotNull prevNextSymbol.createWrapperAction(nodeToReplace, symbol.link) - } - return@mapIndexedNotNull null - }.flatten() + return symbols + .mapIndexedNotNull { index, symbol -> + if (symbol is ChildCellTemplate && conceptToReplace.isSubConceptOf(symbol.link.targetConcept)) { + val prevNextIndex = if (before) index - 1 else index + 1 + val prevNextSymbol = symbols.getOrNull(prevNextIndex) ?: return@mapIndexedNotNull null + return@mapIndexedNotNull prevNextSymbol.createWrapperAction(nodeToReplace, symbol.link) + } + return@mapIndexedNotNull null + }.flatten() } - open fun getGrammarSymbols(): Sequence { - return if (this is IGrammarSymbol) { + open fun getGrammarSymbols(): Sequence = + if (this is IGrammarSymbol) { sequenceOf(this) } else { children.asSequence().flatMap { it.getGrammarSymbols() } } - } - open fun toCompletionToken(): ICompletionTokenOrList? = children.mapNotNull { it.toCompletionToken() }.asTokenList().takeIf { !it.isEmpty() } + open fun toCompletionToken(): ICompletionTokenOrList? = + children + .mapNotNull { + it.toCompletionToken() + }.asTokenList() + .takeIf { !it.isEmpty() } fun addChild(child: CellTemplate) { children.add(child) @@ -111,36 +142,43 @@ abstract class CellTemplate(val concept: IConcept) { fun getReference() = reference ?: throw IllegalStateException("reference isn't set yet") - fun createCellReference(node: Any) = when (node) { - is INodeReference -> createCellReference(node) - is INode -> createCellReference(node) - is ITypedNode -> createCellReference(node) - is IWritableNode -> createCellReference(node.asLegacyNode()) - else -> throw IllegalArgumentException("Unsupported node type: $node") - } + fun createCellReference(node: Any) = + when (node) { + is INodeReference -> createCellReference(node) + is INode -> createCellReference(node) + is ITypedNode -> createCellReference(node) + is IWritableNode -> createCellReference(node.asLegacyNode()) + else -> throw IllegalArgumentException("Unsupported node type: $node") + } + fun createCellReference(nodeRef: INodeReference) = TemplateCellReference(getReference(), nodeRef) + fun createCellReference(node: INode) = createCellReference(node.reference) + fun createCellReference(node: ITypedNode) = createCellReference(node.untyped()) - private fun applyTextReplacement(cellData: CellData, editorState: EditorState) { - if (cellData is TextCellData) { - val cellRef = cellData.cellReferences.firstOrNull() + private fun applyTextReplacement( + cellSpec: CellSpecBase, + cellTreeState: CellTreeState, + ) { + if (cellSpec is TextCellSpec) { + val cellRef = cellSpec.cellReferences.firstOrNull() if (cellRef != null) { - editorState.textReplacements[cellRef] - ?.let { cellData.properties[CommonCellProperties.textReplacement] = it } - cellData.properties[CellActionProperties.replaceText] = - OverrideText(cellData, cellData.properties[CellActionProperties.replaceText]) + cellTreeState.textReplacements[cellRef] + ?.let { cellSpec.properties[CommonCellProperties.textReplacement] = it } + cellSpec.properties[CellActionProperties.replaceText] = + OverrideText(cellSpec, cellSpec.properties[CellActionProperties.replaceText]) } } - cellData.children.filterIsInstance().forEach { applyTextReplacement(it, editorState) } + cellSpec.children.filterIsInstance().forEach { applyTextReplacement(it, cellTreeState) } } } fun CellTemplate.firstLeaf(): CellTemplate = if (getChildren().isEmpty()) this else getChildren().first().firstLeaf() -fun CellTemplate.descendants(includeSelf: Boolean = false): Sequence { - return if (includeSelf) { + +fun CellTemplate.descendants(includeSelf: Boolean = false): Sequence = + if (includeSelf) { sequenceOf(this) + descendants(false) } else { getChildren().asSequence().flatMap { it.descendants(true) } } -} diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/ChangePropertyCellAction.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/ChangePropertyCellAction.kt index 713b9355..54ecb6aa 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/ChangePropertyCellAction.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/ChangePropertyCellAction.kt @@ -1,11 +1,11 @@ package org.modelix.editor.celltemplate import org.modelix.editor.CaretPositionPolicy -import org.modelix.editor.EditorComponent import org.modelix.editor.ICaretPositionPolicy import org.modelix.editor.ICellAction import org.modelix.editor.INonExistingNode import org.modelix.editor.PropertyCellReference +import org.modelix.editor.text.backend.BackendEditorComponent import org.modelix.model.api.IProperty class ChangePropertyCellAction( @@ -13,16 +13,15 @@ class ChangePropertyCellAction( val property: IProperty, val value: String, ) : ICellAction { - override fun execute(editor: EditorComponent): ICaretPositionPolicy? { - val node = editor.runWrite { - node.getOrCreateNode().also { - it.setPropertyValue(property, value) + override fun execute(editor: BackendEditorComponent): ICaretPositionPolicy? { + val node = + editor.runWrite { + node.getOrCreateNode().also { + it.setPropertyValue(property, value) + } } - } - return CaretPositionPolicy(PropertyCellReference(property, node.reference)) + return CaretPositionPolicy(PropertyCellReference(property.toReference(), node.reference)) } - override fun isApplicable(): Boolean { - return true - } + override fun isApplicable(): Boolean = true } diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/ChildCellTemplate.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/ChildCellTemplate.kt index 32134c51..ec36eece 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/ChildCellTemplate.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/ChildCellTemplate.kt @@ -3,14 +3,13 @@ package org.modelix.editor.celltemplate import org.modelix.editor.CaretPositionPolicy import org.modelix.editor.CellActionProperties import org.modelix.editor.CellCreationContext -import org.modelix.editor.CellData import org.modelix.editor.CellReference +import org.modelix.editor.CellSpec import org.modelix.editor.ChildCompletionToken -import org.modelix.editor.ChildDataReference import org.modelix.editor.ChildNodeCellReference +import org.modelix.editor.ChildSpecReference import org.modelix.editor.CodeCompletionParameters import org.modelix.editor.CommonCellProperties -import org.modelix.editor.EditorComponent import org.modelix.editor.ExistingNode import org.modelix.editor.IActionOrProvider import org.modelix.editor.ICaretPositionPolicy @@ -21,17 +20,18 @@ import org.modelix.editor.ICompletionTokenOrList import org.modelix.editor.INonExistingNode import org.modelix.editor.IParseTreeToAstBuilder import org.modelix.editor.NonExistingChild +import org.modelix.editor.PlaceholderCellReference import org.modelix.editor.ReplaceNodeActionProvider -import org.modelix.editor.SavedCaretPosition import org.modelix.editor.SeparatorCellReference import org.modelix.editor.SeparatorCellTemplateReference import org.modelix.editor.SubstitutionPlaceholderPosition import org.modelix.editor.TemplateCellReference -import org.modelix.editor.TextCellData +import org.modelix.editor.TextCellSpec import org.modelix.editor.after import org.modelix.editor.ancestors import org.modelix.editor.asProvider import org.modelix.editor.replacement +import org.modelix.editor.text.backend.BackendEditorComponent import org.modelix.editor.toNonExisting import org.modelix.model.api.IChildLink import org.modelix.model.api.IConcept @@ -50,8 +50,8 @@ import org.modelix.parser.SubConceptsSymbol class ChildCellTemplate( concept: IConcept, val link: IChildLink, -) : CellTemplate(concept), IGrammarConditionSymbol { - +) : CellTemplate(concept), + IGrammarConditionSymbol { private var separatorCell: CellTemplate? = null /** @@ -61,19 +61,18 @@ class ChildCellTemplate( */ var newLineConcept: IConcept? = null - override fun toParserSymbol(): ISymbol { - return if (link.isMultiple) { - val separatorSymbols = (separatorCell?.getGrammarSymbols()?.toList() ?: emptyList()) - .map { it.toParserSymbol() }.filterIsInstance() + override fun toParserSymbol(): ISymbol = + if (link.isMultiple) { + val separatorSymbols = + (separatorCell?.getGrammarSymbols()?.toList() ?: emptyList()) + .map { it.toParserSymbol() } + .filterIsInstance() ListSymbol(SubConceptsSymbol(link.targetConcept), separatorSymbols.firstOrNull()) } else { SubConceptsSymbol(link.targetConcept) } - } - override fun toCompletionToken(): ICompletionTokenOrList? { - return ChildCompletionToken(link) - } + override fun toCompletionToken(): ICompletionTokenOrList? = ChildCompletionToken(link) override fun consumeTokens(builder: IParseTreeToAstBuilder) { val symbol = toParserSymbol() @@ -81,7 +80,10 @@ class ChildCellTemplate( loadChildrenFromParseTree(builder, token) } - private fun loadChildrenFromParseTree(builder: IParseTreeToAstBuilder, parseTree: IParseTreeNode) { + private fun loadChildrenFromParseTree( + builder: IParseTreeToAstBuilder, + parseTree: IParseTreeNode, + ) { when (parseTree) { is ParseTreeNode -> { val nonTerminal = parseTree.rule.head @@ -89,19 +91,28 @@ class ChildCellTemplate( is ExactConceptSymbol -> { builder.buildChild(link, parseTree) } + is SubConceptsSymbol -> { parseTree.children.forEach { loadChildrenFromParseTree(builder, it) } } + is ListSymbol -> { parseTree.children.forEach { loadChildrenFromParseTree(builder, it) } } - else -> throw NotImplementedError("$nonTerminal") + + else -> { + throw NotImplementedError("$nonTerminal") + } } } + is AmbiguousNode -> { builder.buildChild(link, parseTree) } - else -> throw NotImplementedError("$parseTree") + + else -> { + throw NotImplementedError("$parseTree") + } } } @@ -115,44 +126,51 @@ class ChildCellTemplate( separatorCell?.setReference(SeparatorCellTemplateReference(ref)) } - override fun createCell(context: CellCreationContext, node: INode) = CellData().also { cell -> + override fun createCell( + context: CellCreationContext, + node: INode, + ) = CellSpec().also { cell -> val childNodes = getChildNodes(node) - val substitutionPlaceholder = context.editorState.substitutionPlaceholderPositions[createCellReference(node)] + val substitutionPlaceholder = context.cellTreeState.substitutionPlaceholderPositions[createCellReference(node)] val placeholderIndex = substitutionPlaceholder?.index?.coerceIn(0..childNodes.size) ?: 0 + fun addSubstitutionPlaceholder(index: Int) { val isDefaultPlaceholder = childNodes.isEmpty() val placeholderText = if (isDefaultPlaceholder) "" else "" - val placeholder = TextCellData("", placeholderText) + val placeholder = TextCellSpec("", placeholderText) placeholder.properties[CellActionProperties.substitute] = ReplaceNodeActionProvider(NonExistingChild(node.toNonExisting(), link, index)).after { - context.editorState.substitutionPlaceholderPositions.remove(createCellReference(node)) + context.cellTreeState.substitutionPlaceholderPositions.remove(createCellReference(node)) } placeholder.cellReferences.add(PlaceholderCellReference(createCellReference(node))) if (isDefaultPlaceholder) { - placeholder.cellReferences += ChildNodeCellReference(node.reference, link, index) + placeholder.cellReferences += ChildNodeCellReference(node.reference, link.toReference(), index) } placeholder.properties[CommonCellProperties.tabTarget] = true - placeholder.properties[CellActionProperties.delete] = object : ICellAction { - override fun execute(editor: EditorComponent): ICaretPositionPolicy? { - return SavedCaretPosition.saveAndRun(editor) { - context.editorState.substitutionPlaceholderPositions.remove(createCellReference(node)) + placeholder.properties[CellActionProperties.delete] = + object : ICellAction { + override fun execute(editor: BackendEditorComponent): ICaretPositionPolicy? { + context.cellTreeState.substitutionPlaceholderPositions.remove(createCellReference(node)) + return null // Position is updated by the frontend } - } - override fun isApplicable(): Boolean = true - } + override fun isApplicable(): Boolean = true + } cell.addChild(placeholder) } + fun addInsertActionCell(index: Int) { if (link.isMultiple) { - val actionCell = CellData() - val action = newLineConcept?.let { - InstantiateNodeCellAction(NonExistingChild(ExistingNode(node), link, index), it) - } ?: InsertSubstitutionPlaceholderAction(context.editorState, createCellReference(node), index) + val actionCell = CellSpec() + val action = + newLineConcept?.let { + InstantiateNodeCellAction(NonExistingChild(ExistingNode(node), link, index), it) + } ?: InsertSubstitutionPlaceholderAction(context.cellTreeState, createCellReference(node), index) actionCell.properties[CellActionProperties.insert] = action cell.addChild(actionCell) } } + fun addSeparator(before: CellReference) { separatorCell?.let { cell.addChild( @@ -165,11 +183,15 @@ class ChildCellTemplate( if (childNodes.isEmpty()) { addSubstitutionPlaceholder(0) } else { - val separatorText = separatorCell?.getGrammarSymbols()?.filterIsInstance() - ?.firstOrNull()?.text - val childCells = childNodes.map { ChildDataReference(it) } + val separatorText = + separatorCell + ?.getGrammarSymbols() + ?.filterIsInstance() + ?.firstOrNull() + ?.text + val childCells = childNodes.map { ChildSpecReference(it) } childCells.forEachIndexed { index, child -> - val childCellReference = ChildNodeCellReference(node.reference, link, index) + val childCellReference = ChildNodeCellReference(node.reference, link.toReference(), index) if (index != 0) { addSeparator(childCellReference) } @@ -182,21 +204,23 @@ class ChildCellTemplate( } // child.parent?.removeChild(child) // child may be cached and is still attached to the old parent - val wrapper = CellData() // allow setting properties by the parent, because the cell is already frozen + val wrapper = CellSpec() // allow setting properties by the parent, because the cell is already frozen wrapper.addChild(child) wrapper.cellReferences += childCellReference if (separatorText != null) { - wrapper.properties[CellActionProperties.transformBefore] = InsertSubstitutionPlaceholderCompletionAction( - index, - separatorText, - createCellReference(node), - ).asProvider() - wrapper.properties[CellActionProperties.transformAfter] = InsertSubstitutionPlaceholderCompletionAction( - index + 1, - separatorText, - createCellReference(node), - ).asProvider() + wrapper.properties[CellActionProperties.transformBefore] = + InsertSubstitutionPlaceholderCompletionAction( + index, + separatorText, + createCellReference(node), + ).asProvider() + wrapper.properties[CellActionProperties.transformAfter] = + InsertSubstitutionPlaceholderCompletionAction( + index + 1, + separatorText, + createCellReference(node), + ).asProvider() } cell.addChild(wrapper) @@ -214,7 +238,10 @@ class ChildCellTemplate( fun getChildNodes(node: INode) = node.getChildren(link).toList() - override fun getInstantiationActions(location: INonExistingNode, parameters: CodeCompletionParameters): List? { + override fun getInstantiationActions( + location: INonExistingNode, + parameters: CodeCompletionParameters, + ): List? { // 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. @@ -231,32 +258,27 @@ class ChildCellTemplate( return listOf(ReplaceNodeActionProvider(childNode)) } - override fun getSymbolConditionState(node: INode): Boolean { - return node.getChildren(link).iterator().hasNext() - } + override fun getSymbolConditionState(node: INode): Boolean = node.getChildren(link).iterator().hasNext() override fun setSymbolConditionFalse(node: INode) { node.getChildren(link).toList().forEach { it.remove() } } - override fun getSymbolTransformationAction(node: INode, optionalCell: TemplateCellReference): IActionOrProvider? { - return ReplaceNodeActionProvider(NonExistingChild(node.toNonExisting(), link)) - } + override fun getSymbolTransformationAction( + node: INode, + optionalCell: TemplateCellReference, + ): IActionOrProvider? = ReplaceNodeActionProvider(NonExistingChild(node.toNonExisting(), link)) inner class InsertSubstitutionPlaceholderCompletionAction( val index: Int, val separatorText: String, val ref: TemplateCellReference, ) : ICodeCompletionAction { - override fun getDescription(): String { - return "Add new node to ${link.getSimpleName()}" - } + override fun getDescription(): String = "Add new node to ${link.getSimpleName()}" - override fun getMatchingText(): String { - return separatorText - } + override fun getMatchingText(): String = separatorText - override fun execute(editor: EditorComponent): CaretPositionPolicy? { + override fun execute(editor: BackendEditorComponent): CaretPositionPolicy? { editor.state.substitutionPlaceholderPositions[ref] = SubstitutionPlaceholderPosition(index) editor.state.textReplacements.remove(PlaceholderCellReference(ref)) return CaretPositionPolicy(PlaceholderCellReference(ref)) diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/CollectionCellTemplate.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/CollectionCellTemplate.kt index 64b74c79..dd8abf12 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/CollectionCellTemplate.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/CollectionCellTemplate.kt @@ -1,11 +1,15 @@ package org.modelix.editor.celltemplate import org.modelix.editor.CellCreationContext -import org.modelix.editor.CellData +import org.modelix.editor.CellSpec import org.modelix.model.api.IConcept import org.modelix.model.api.INode -class CollectionCellTemplate(concept: IConcept) : - CellTemplate(concept) { - override fun createCell(context: CellCreationContext, node: INode) = CellData() +class CollectionCellTemplate( + concept: IConcept, +) : CellTemplate(concept) { + override fun createCell( + context: CellCreationContext, + node: INode, + ) = CellSpec() } diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/ConstantCellTemplate.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/ConstantCellTemplate.kt index b7bfa93f..6d2050ef 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/ConstantCellTemplate.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/ConstantCellTemplate.kt @@ -5,16 +5,16 @@ import org.modelix.editor.CellCreationContext import org.modelix.editor.ChildNodeCellReference import org.modelix.editor.CodeCompletionParameters import org.modelix.editor.ConstantCompletionToken -import org.modelix.editor.EditorComponent import org.modelix.editor.IActionOrProvider import org.modelix.editor.ICodeCompletionAction import org.modelix.editor.ICompletionTokenOrList import org.modelix.editor.INonExistingNode import org.modelix.editor.IParseTreeToAstBuilder import org.modelix.editor.TemplateCellReference -import org.modelix.editor.TextCellData +import org.modelix.editor.TextCellSpec import org.modelix.editor.ancestors import org.modelix.editor.commonAncestor +import org.modelix.editor.text.backend.BackendEditorComponent import org.modelix.editor.toNonExisting import org.modelix.editor.withCaretPolicy import org.modelix.editor.withMatchingText @@ -26,50 +26,65 @@ import org.modelix.parser.ConstantSymbol import org.modelix.parser.ISymbol import org.modelix.parser.Token -class ConstantCellTemplate(concept: IConcept, val text: String) : CellTemplate(concept), IGrammarSymbol { - +class ConstantCellTemplate( + concept: IConcept, + val text: String, +) : CellTemplate(concept), + IGrammarSymbol { override fun toParserSymbol(): ISymbol = ConstantSymbol(text) - override fun toCompletionToken(): ICompletionTokenOrList? { - return ConstantCompletionToken(text) - } + override fun toCompletionToken(): ICompletionTokenOrList? = ConstantCompletionToken(text) override fun consumeTokens(builder: IParseTreeToAstBuilder) { val symbol = toParserSymbol() val token = builder.consumeNextToken { it is Token && it.symbol == symbol } ?: return } - override fun createCell(context: CellCreationContext, node: INode) = TextCellData(text, "") + override fun createCell( + context: CellCreationContext, + node: INode, + ) = TextCellSpec(text, "") - override fun getInstantiationActions(location: INonExistingNode, parameters: CodeCompletionParameters): List? { - return listOf(InstantiateNodeCompletionAction(text, concept, location)) - } + override fun getInstantiationActions( + location: INonExistingNode, + parameters: CodeCompletionParameters, + ): List? = listOf(InstantiateNodeCompletionAction(text, concept, location)) - override fun createWrapperAction(nodeToWrap: INode, wrappingLink: IChildLink): List { - return listOf(SideTransformWrapper(nodeToWrap.toNonExisting(), wrappingLink)) - } + override fun createWrapperAction( + nodeToWrap: INode, + wrappingLink: IChildLink, + ): List = listOf(SideTransformWrapper(nodeToWrap.toNonExisting(), wrappingLink)) - override fun getSymbolTransformationAction(node: INode, optionalCell: TemplateCellReference): IActionOrProvider? { - return ForceShowOptionalCellAction(optionalCell) + override fun getSymbolTransformationAction( + node: INode, + optionalCell: TemplateCellReference, + ): IActionOrProvider? = + ForceShowOptionalCellAction(optionalCell) .withCaretPolicy { when (it) { is CaretPositionPolicy -> it.avoid(createCellReference(node)) else -> it } - } - .withMatchingText(text) - } + }.withMatchingText(text) - inner class SideTransformWrapper(val nodeToWrap: INonExistingNode, val wrappingLink: IChildLink) : - ICodeCompletionAction { + inner class SideTransformWrapper( + val nodeToWrap: INonExistingNode, + val wrappingLink: IChildLink, + ) : ICodeCompletionAction { override fun getMatchingText(): String = text + override fun getDescription(): String = concept.getShortName() - override fun execute(editor: EditorComponent): CaretPositionPolicy? { - val wrapper = nodeToWrap.getParent()!!.getOrCreateNode(null).asWritableNode() - .addNewChild(nodeToWrap.getContainmentLink()?.toReference()!!, nodeToWrap.index(), concept.getReference().upcast()) + + override fun execute(editor: BackendEditorComponent): CaretPositionPolicy? { + val wrapper = + nodeToWrap + .getParent()!! + .getOrCreateNode(null) + .asWritableNode() + .addNewChild(nodeToWrap.getContainmentLink()?.toReference()!!, nodeToWrap.index(), concept.getReference().upcast()) wrapper.moveChild(wrappingLink.toReference(), 0, nodeToWrap.getOrCreateNode(null).asWritableNode()) return CaretPositionPolicy(wrapper.asLegacyNode()) - .avoid(ChildNodeCellReference(wrapper.getNodeReference(), wrappingLink)) + .avoid(ChildNodeCellReference(wrapper.getNodeReference(), wrappingLink.toReference())) .avoid(createCellReference(wrapper)) } @@ -78,7 +93,11 @@ class ConstantCellTemplate(concept: IConcept, val text: String) : CellTemplate(c if (shadowed.getTemplate().concept != concept) return false val commonAncestor = nodeToWrap.commonAncestor(shadowed.nodeToWrap) val ownDepth = nodeToWrap.ancestors(true).takeWhile { it != commonAncestor }.count() - val otherDepth = shadowed.nodeToWrap.ancestors(true).takeWhile { it != commonAncestor }.count() + val otherDepth = + shadowed.nodeToWrap + .ancestors(true) + .takeWhile { it != commonAncestor } + .count() if (ownDepth > otherDepth) return true return false } diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/FlagCellTemplate.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/FlagCellTemplate.kt index 290b76f9..bc149777 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/FlagCellTemplate.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/FlagCellTemplate.kt @@ -2,14 +2,15 @@ package org.modelix.editor.celltemplate import org.modelix.editor.CellActionProperties import org.modelix.editor.CellCreationContext -import org.modelix.editor.CellData +import org.modelix.editor.CellSpec +import org.modelix.editor.CellSpecBase import org.modelix.editor.CodeCompletionParameters import org.modelix.editor.CommonCellProperties import org.modelix.editor.IActionOrProvider import org.modelix.editor.ICompletionTokenOrList import org.modelix.editor.INonExistingNode import org.modelix.editor.IParseTreeToAstBuilder -import org.modelix.editor.TextCellData +import org.modelix.editor.TextCellSpec import org.modelix.editor.toNonExisting import org.modelix.model.api.IConcept import org.modelix.model.api.INode @@ -23,13 +24,11 @@ class FlagCellTemplate( concept: IConcept, property: IProperty, val text: String, -) : PropertyCellTemplate(concept, property), IGrammarSymbol { - +) : PropertyCellTemplate(concept, property), + IGrammarSymbol { override fun toParserSymbol(): ISymbol = OptionalSymbol(ConstantSymbol(text)) - override fun toCompletionToken(): ICompletionTokenOrList? { - return null - } + override fun toCompletionToken(): ICompletionTokenOrList? = null override fun consumeTokens(builder: IParseTreeToAstBuilder) { val symbol = toParserSymbol() @@ -37,24 +36,30 @@ class FlagCellTemplate( builder.currentNode().setPropertyValue(property, "true") } - override fun createCell(context: CellCreationContext, node: INode): CellData { - if (node.getPropertyValue(property) == "true") return TextCellData(text, "") + override fun createCell( + context: CellCreationContext, + node: INode, + ): CellSpecBase { + if (node.getPropertyValue(property) == "true") return TextCellSpec(text, "") - val forceShow = context.editorState.forceShowOptionals[createCellReference(node)] == true + val forceShow = context.cellTreeState.forceShowOptionals[createCellReference(node)] == true return if (forceShow) { - TextCellData("", text).also { + TextCellSpec("", text).also { it.properties[CommonCellProperties.isForceShown] = true it.properties[CellActionProperties.insert] = ChangePropertyCellAction(node.toNonExisting(), property, "true") } } else { - CellData().also { + CellSpec().also { it.properties[CellActionProperties.show] = ForceShowOptionalCellAction(createCellReference(node)) } } } - override fun getInstantiationActions(location: INonExistingNode, parameters: CodeCompletionParameters): List? { + override fun getInstantiationActions( + location: INonExistingNode, + parameters: CodeCompletionParameters, + ): List? { // TODO return listOf() } diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/ForceShowOptionalCellAction.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/ForceShowOptionalCellAction.kt index 4e01c553..91ec9ba4 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/ForceShowOptionalCellAction.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/ForceShowOptionalCellAction.kt @@ -1,27 +1,24 @@ package org.modelix.editor.celltemplate import org.modelix.editor.CaretPositionPolicy -import org.modelix.editor.EditorComponent import org.modelix.editor.ICaretPositionPolicy import org.modelix.editor.ICellAction import org.modelix.editor.ICodeCompletionAction import org.modelix.editor.TemplateCellReference +import org.modelix.editor.text.backend.BackendEditorComponent -class ForceShowOptionalCellAction(val cell: TemplateCellReference) : ICodeCompletionAction, ICellAction { - override fun execute(editor: EditorComponent): ICaretPositionPolicy { +class ForceShowOptionalCellAction( + val cell: TemplateCellReference, +) : ICodeCompletionAction, + ICellAction { + override fun execute(editor: BackendEditorComponent): ICaretPositionPolicy { editor.state.forceShowOptionals[cell] = true return CaretPositionPolicy(cell) } - override fun getMatchingText(): String { - return "" - } + override fun getMatchingText(): String = "" - override fun getDescription(): String { - return "Add optional part" - } + override fun getDescription(): String = "Add optional part" - override fun isApplicable(): Boolean { - return true - } + override fun isApplicable(): Boolean = true } diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/IGrammarConditionSymbol.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/IGrammarConditionSymbol.kt index 52c36190..dacbef5d 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/IGrammarConditionSymbol.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/IGrammarConditionSymbol.kt @@ -4,5 +4,6 @@ import org.modelix.model.api.INode interface IGrammarConditionSymbol : IGrammarSymbol { fun getSymbolConditionState(node: INode): Boolean + fun setSymbolConditionFalse(node: INode) } diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/IGrammarSymbol.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/IGrammarSymbol.kt index 8ede722e..ef12567d 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/IGrammarSymbol.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/IGrammarSymbol.kt @@ -9,11 +9,15 @@ import org.modelix.model.api.INode import org.modelix.parser.ISymbol interface IGrammarSymbol { - fun createWrapperAction(nodeToWrap: INode, wrappingLink: IChildLink): List { - return emptyList() - } + fun createWrapperAction( + nodeToWrap: INode, + wrappingLink: IChildLink, + ): List = emptyList() - fun getSymbolTransformationAction(node: INode, optionalCell: TemplateCellReference): IActionOrProvider? + fun getSymbolTransformationAction( + node: INode, + optionalCell: TemplateCellReference, + ): IActionOrProvider? fun toParserSymbol(): ISymbol @@ -24,8 +28,7 @@ interface IOptionalSymbol : IGrammarSymbol { fun getChildSymbols(): Sequence } -fun Sequence.leafSymbols(): Sequence { - return flatMap { +fun Sequence.leafSymbols(): Sequence = + flatMap { if (it is IOptionalSymbol) it.getChildSymbols() else sequenceOf(it) } -} diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/InsertSubstitutionPlaceholderAction.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/InsertSubstitutionPlaceholderAction.kt index f2eede24..6977fcd8 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/InsertSubstitutionPlaceholderAction.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/InsertSubstitutionPlaceholderAction.kt @@ -1,22 +1,23 @@ package org.modelix.editor.celltemplate import org.modelix.editor.CaretPositionPolicy -import org.modelix.editor.EditorComponent -import org.modelix.editor.EditorState +import org.modelix.editor.CellTreeState import org.modelix.editor.ICellAction +import org.modelix.editor.PlaceholderCellReference import org.modelix.editor.SubstitutionPlaceholderPosition import org.modelix.editor.TemplateCellReference +import org.modelix.editor.text.backend.BackendEditorComponent class InsertSubstitutionPlaceholderAction( - val editorState: EditorState, + val cellTreeState: CellTreeState, val ref: TemplateCellReference, val index: Int, ) : ICellAction { override fun isApplicable(): Boolean = true - override fun execute(editor: EditorComponent): CaretPositionPolicy { - editorState.substitutionPlaceholderPositions[ref] = SubstitutionPlaceholderPosition(index) - editorState.textReplacements.remove(PlaceholderCellReference(ref)) + override fun execute(editor: BackendEditorComponent): CaretPositionPolicy { + cellTreeState.substitutionPlaceholderPositions[ref] = SubstitutionPlaceholderPosition(index) + cellTreeState.textReplacements.remove(PlaceholderCellReference(ref)) return CaretPositionPolicy(PlaceholderCellReference(ref)) } } diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/InstantiateNodeCellAction.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/InstantiateNodeCellAction.kt index 37333dbd..d45f18e1 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/InstantiateNodeCellAction.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/InstantiateNodeCellAction.kt @@ -1,18 +1,22 @@ package org.modelix.editor.celltemplate import org.modelix.editor.CaretPositionPolicy -import org.modelix.editor.EditorComponent import org.modelix.editor.ICellAction import org.modelix.editor.INonExistingNode +import org.modelix.editor.text.backend.BackendEditorComponent import org.modelix.model.api.IConcept -class InstantiateNodeCellAction(val location: INonExistingNode, val concept: IConcept) : ICellAction { +class InstantiateNodeCellAction( + val location: INonExistingNode, + val concept: IConcept, +) : ICellAction { override fun isApplicable(): Boolean = true - override fun execute(editor: EditorComponent): CaretPositionPolicy { - val newNode = location.getExistingAncestor()!!.getArea().executeWrite { - location.replaceNode(concept) - } + override fun execute(editor: BackendEditorComponent): CaretPositionPolicy { + val newNode = + location.getExistingAncestor()!!.getArea().executeWrite { + location.replaceNode(concept) + } return CaretPositionPolicy(newNode) } } diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/InstantiateNodeCompletionAction.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/InstantiateNodeCompletionAction.kt index 85bdbf22..46ee7f55 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/InstantiateNodeCompletionAction.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/InstantiateNodeCompletionAction.kt @@ -1,9 +1,9 @@ package org.modelix.editor.celltemplate import org.modelix.editor.CaretPositionPolicy -import org.modelix.editor.EditorComponent import org.modelix.editor.ICodeCompletionAction import org.modelix.editor.INonExistingNode +import org.modelix.editor.text.backend.BackendEditorComponent import org.modelix.model.api.IConcept class InstantiateNodeCompletionAction( @@ -11,45 +11,53 @@ class InstantiateNodeCompletionAction( val concept: IConcept, val location: INonExistingNode, ) : ICodeCompletionAction { - private val description = let { - fun wrapperText(innerText: String, wrapper: INonExistingNode?): String = if (wrapper != null && wrapper.getNode() == null) { - wrapperText("${wrapper.expectedConcept()?.getShortName()}[$innerText]", wrapper.getParent()) - } else { - innerText + private val description = + let { + fun wrapperText( + innerText: String, + wrapper: INonExistingNode?, + ): String = + if (wrapper != null && wrapper.getNode() == null) { + wrapperText("${wrapper.expectedConcept()?.getShortName()}[$innerText]", wrapper.getParent()) + } else { + innerText + } + wrapperText(concept.getShortName(), location.getParent()) } - wrapperText(concept.getShortName(), location.getParent()) - } - override fun getMatchingText(): String { - return matchingText - } + override fun getMatchingText(): String = matchingText override fun getDescription(): String = description - override fun execute(editor: EditorComponent): CaretPositionPolicy? { - val newNode = location.getExistingAncestor()!!.getArea().executeWrite { - location.replaceNode(concept) - } + override fun execute(editor: BackendEditorComponent): CaretPositionPolicy? { + val newNode = + location.getExistingAncestor()!!.getArea().executeWrite { + location.replaceNode(concept) + } return CaretPositionPolicy(newNode) } - override fun shadowedBy(shadowing: ICodeCompletionAction): Boolean { - return when (shadowing) { + override fun shadowedBy(shadowing: ICodeCompletionAction): Boolean = + when (shadowing) { is InstantiateNodeCompletionAction -> { // Avoid showing the same entry twice, once with and once without a wrapper. shadowing.concept == concept && shadowing.location.nodeCreationDepth() < location.nodeCreationDepth() } - else -> false + + else -> { + false + } } - } - override fun shadows(shadowed: ICodeCompletionAction): Boolean { - return when (shadowed) { + override fun shadows(shadowed: ICodeCompletionAction): Boolean = + when (shadowed) { is InstantiateNodeCompletionAction -> { // Avoid showing the same entry twice, once with and once without a wrapper. shadowed.concept == concept && shadowed.location.nodeCreationDepth() > location.nodeCreationDepth() } - else -> false + + else -> { + false + } } - } } diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/LabelCellTemplate.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/LabelCellTemplate.kt index dd613001..b831e718 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/LabelCellTemplate.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/LabelCellTemplate.kt @@ -5,7 +5,7 @@ import org.modelix.editor.CodeCompletionParameters import org.modelix.editor.CommonCellProperties import org.modelix.editor.IActionOrProvider import org.modelix.editor.INonExistingNode -import org.modelix.editor.TextCellData +import org.modelix.editor.TextCellSpec import org.modelix.model.api.IConcept import org.modelix.model.api.INode @@ -15,16 +15,22 @@ import org.modelix.model.api.INode * It is ignored when generating transformation action. * A constant is part of the grammar. */ -class LabelCellTemplate(concept: IConcept, val text: String) : - CellTemplate(concept) { - override fun createCell(context: CellCreationContext, node: INode): TextCellData { - return TextCellData(text, "").also { +class LabelCellTemplate( + concept: IConcept, + val text: String, +) : CellTemplate(concept) { + override fun createCell( + context: CellCreationContext, + node: INode, + ): TextCellSpec = + TextCellSpec(text, "").also { if (!it.properties.isSet(CommonCellProperties.textColor)) { it.properties[CommonCellProperties.textColor] = "LightGray" } } - } - override fun getInstantiationActions(location: INonExistingNode, parameters: CodeCompletionParameters): List? { - return emptyList() - } + + override fun getInstantiationActions( + location: INonExistingNode, + parameters: CodeCompletionParameters, + ): List? = emptyList() } diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/Levensthein.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/Levensthein.kt index b0acfdc0..17c14f97 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/Levensthein.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/Levensthein.kt @@ -67,11 +67,12 @@ object Levenshtein { var swapDistance: Int if (candidateSwapIndex != null && jSwap != -1) { val iSwap = candidateSwapIndex - var preSwapCost = if (iSwap == 0 && jSwap == 0) { - 0 - } else { - table[max(0, iSwap - 1)][max(0, jSwap - 1)] - } + var preSwapCost = + if (iSwap == 0 && jSwap == 0) { + 0 + } else { + table[max(0, iSwap - 1)][max(0, jSwap - 1)] + } swapDistance = preSwapCost + (i - iSwap - 1) * deleteCost + (j - jSwap - 1) * insertCost + swapCost diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/NewLineCellTemplate.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/NewLineCellTemplate.kt index a61ae5ca..802e9047 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/NewLineCellTemplate.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/NewLineCellTemplate.kt @@ -1,7 +1,8 @@ package org.modelix.editor.celltemplate import org.modelix.editor.CellCreationContext -import org.modelix.editor.CellData +import org.modelix.editor.CellSpec +import org.modelix.editor.CellSpecBase import org.modelix.editor.CommonCellProperties import org.modelix.editor.ICompletionTokenOrList import org.modelix.editor.SpaceCompletionToken @@ -9,13 +10,13 @@ import org.modelix.editor.SpaceTokenType import org.modelix.model.api.IConcept import org.modelix.model.api.INode -class NewLineCellTemplate(concept: IConcept) : - CellTemplate(concept) { - override fun createCell(context: CellCreationContext, node: INode): CellData { - return CellData().also { cell -> cell.properties[CommonCellProperties.onNewLine] = true } - } +class NewLineCellTemplate( + concept: IConcept, +) : CellTemplate(concept) { + override fun createCell( + context: CellCreationContext, + node: INode, + ): CellSpecBase = CellSpec().also { cell -> cell.properties[CommonCellProperties.onNewLine] = true } - override fun toCompletionToken(): ICompletionTokenOrList? { - return SpaceCompletionToken(SpaceTokenType.MANDATORY) - } + override fun toCompletionToken(): ICompletionTokenOrList? = SpaceCompletionToken(SpaceTokenType.MANDATORY) } diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/NoSpaceCellTemplate.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/NoSpaceCellTemplate.kt index b25a5d87..c90db153 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/NoSpaceCellTemplate.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/NoSpaceCellTemplate.kt @@ -1,7 +1,8 @@ package org.modelix.editor.celltemplate import org.modelix.editor.CellCreationContext -import org.modelix.editor.CellData +import org.modelix.editor.CellSpec +import org.modelix.editor.CellSpecBase import org.modelix.editor.CommonCellProperties import org.modelix.editor.ICompletionTokenOrList import org.modelix.editor.SpaceCompletionToken @@ -9,13 +10,13 @@ import org.modelix.editor.SpaceTokenType import org.modelix.model.api.IConcept import org.modelix.model.api.INode -class NoSpaceCellTemplate(concept: IConcept) : - CellTemplate(concept) { - override fun createCell(context: CellCreationContext, node: INode): CellData { - return CellData().also { cell -> cell.properties[CommonCellProperties.noSpace] = true } - } +class NoSpaceCellTemplate( + concept: IConcept, +) : CellTemplate(concept) { + override fun createCell( + context: CellCreationContext, + node: INode, + ): CellSpecBase = CellSpec().also { cell -> cell.properties[CommonCellProperties.noSpace] = true } - override fun toCompletionToken(): ICompletionTokenOrList? { - return SpaceCompletionToken(SpaceTokenType.NONE) - } + override fun toCompletionToken(): ICompletionTokenOrList? = SpaceCompletionToken(SpaceTokenType.NONE) } diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/NotationRootCellTemplate.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/NotationRootCellTemplate.kt index 1532606c..3f832625 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/NotationRootCellTemplate.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/NotationRootCellTemplate.kt @@ -1,12 +1,17 @@ package org.modelix.editor.celltemplate import org.modelix.editor.CellCreationContext -import org.modelix.editor.CellData +import org.modelix.editor.CellSpec import org.modelix.model.api.IConcept import org.modelix.model.api.INode -class NotationRootCellTemplate(concept: IConcept) : - CellTemplate(concept) { +class NotationRootCellTemplate( + concept: IConcept, +) : CellTemplate(concept) { var condition: ((INode) -> Boolean)? = null - override fun createCell(context: CellCreationContext, node: INode) = CellData() + + override fun createCell( + context: CellCreationContext, + node: INode, + ) = CellSpec() } diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/OptionalCellTemplate.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/OptionalCellTemplate.kt index 70a0c01d..5c2b5e6e 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/OptionalCellTemplate.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/OptionalCellTemplate.kt @@ -2,7 +2,8 @@ package org.modelix.editor.celltemplate import org.modelix.editor.CellActionProperties import org.modelix.editor.CellCreationContext -import org.modelix.editor.CellData +import org.modelix.editor.CellSpec +import org.modelix.editor.CellSpecBase import org.modelix.editor.CodeCompletionParameters import org.modelix.editor.CommonCellProperties import org.modelix.editor.IActionOrProvider @@ -17,15 +18,13 @@ import org.modelix.parser.INonTerminalToken import org.modelix.parser.OptionalSymbol import org.modelix.parser.ParseTreeNode -class OptionalCellTemplate(concept: IConcept) : CellTemplate(concept), IOptionalSymbol { +class OptionalCellTemplate( + concept: IConcept, +) : CellTemplate(concept), + IOptionalSymbol { + override fun toParserSymbol(): OptionalSymbol = OptionalSymbol(getChildSymbols().map { it.toParserSymbol() }.toList()) - override fun toParserSymbol(): OptionalSymbol { - return OptionalSymbol(getChildSymbols().map { it.toParserSymbol() }.toList()) - } - - override fun toCompletionToken(): ICompletionTokenOrList? { - return null - } + override fun toCompletionToken(): ICompletionTokenOrList? = null override fun consumeTokens(builder: IParseTreeToAstBuilder) { val symbol = toParserSymbol() @@ -34,16 +33,24 @@ class OptionalCellTemplate(concept: IConcept) : CellTemplate(concept), IOptional is ParseTreeNode -> { builder.consumeTokens(token.children) } - else -> TODO() + + else -> { + TODO() + } } } - override fun createCell(context: CellCreationContext, node: INode): CellData { - return CellData() - } + override fun createCell( + context: CellCreationContext, + node: INode, + ): CellSpecBase = CellSpec() - override fun applyChildren(context: CellCreationContext, node: INode, cell: CellData): List { - fun forceShow() = context.editorState.forceShowOptionals[createCellReference(node)] == true + override fun applyChildren( + context: CellCreationContext, + node: INode, + cell: CellSpecBase, + ): List { + fun forceShow() = context.cellTreeState.forceShowOptionals[createCellReference(node)] == true val symbols = getChildren().asSequence().flatMap { it.getGrammarSymbols() } val conditionSymbol = symbols.filterIsInstance().firstOrNull() @@ -67,15 +74,17 @@ class OptionalCellTemplate(concept: IConcept) : CellTemplate(concept), IOptional } } - override fun getInstantiationActions(location: INonExistingNode, parameters: CodeCompletionParameters): List? { + override fun getInstantiationActions( + location: INonExistingNode, + parameters: CodeCompletionParameters, + ): List? { return null // skip optional. Don't search in children. } - override fun getChildSymbols(): Sequence { - return getChildren().asSequence().flatMap { it.getGrammarSymbols() } - } + override fun getChildSymbols(): Sequence = getChildren().asSequence().flatMap { it.getGrammarSymbols() } - override fun getSymbolTransformationAction(node: INode, optionalCell: TemplateCellReference): IActionOrProvider? { - return null - } + override fun getSymbolTransformationAction( + node: INode, + optionalCell: TemplateCellReference, + ): IActionOrProvider? = null } diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/OverrideText.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/OverrideText.kt index ad60630d..37149589 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/OverrideText.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/OverrideText.kt @@ -1,25 +1,31 @@ package org.modelix.editor.celltemplate -import org.modelix.editor.EditorComponent +import org.modelix.editor.CellTreeState import org.modelix.editor.ITextChangeAction -import org.modelix.editor.TextCellData +import org.modelix.editor.TextCellSpec -class OverrideText(val cell: TextCellData, val delegate: ITextChangeAction?) : ITextChangeAction { - override fun isValid(value: String?): Boolean { - return true - } +class OverrideText( + val cell: TextCellSpec, + val delegate: ITextChangeAction?, +) : ITextChangeAction { + override fun isValid(value: String?): Boolean = true - override fun replaceText(editor: EditorComponent, range: IntRange, replacement: String, newText: String): Boolean { + override fun replaceText( + editor: CellTreeState, + range: IntRange, + replacement: String, + newText: String, + ): Boolean { val cellRef = cell.cellReferences.first() if (delegate != null && delegate.isValid(newText)) { - editor.state.textReplacements.remove(cellRef) + editor.textReplacements.remove(cellRef) return delegate.replaceText(editor, range, replacement, newText) } if (cell.text == newText) { - editor.state.textReplacements.remove(cellRef) + editor.textReplacements.remove(cellRef) } else { - editor.state.textReplacements[cellRef] = newText + editor.textReplacements[cellRef] = newText } return true } diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/ParserForEditor.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/ParserForEditor.kt index af9947e9..c6133207 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/ParserForEditor.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/ParserForEditor.kt @@ -12,24 +12,35 @@ import org.modelix.parser.LRTable import org.modelix.parser.ProductionRule import org.modelix.parser.createParseTable -class ParserForEditor(val engine: EditorEngine) { +class ParserForEditor( + val engine: EditorEngine, +) { private var parseTables = HashMap, LRTable>() - private fun getParseTable(startConcept: IConcept, forCodeCompletion: Boolean): LRTable { - return runSynchronized(parseTables) { + private fun getParseTable( + startConcept: IConcept, + forCodeCompletion: Boolean, + ): LRTable = + runSynchronized(parseTables) { parseTables.getOrPut(startConcept to forCodeCompletion) { val rules = ArrayList() loadRulesFromSubconcepts(rules, startConcept, HashSet(), engine) Grammar(startConcept, rules, forCodeCompletion = forCodeCompletion).createParseTable() } } - } - fun getParser(startConcept: IConcept, forCodeCompletion: Boolean, disambiguator: IDisambiguator = IDisambiguator.default()): LRParser { - return LRParser(getParseTable(startConcept, forCodeCompletion), disambiguator) - } + fun getParser( + startConcept: IConcept, + forCodeCompletion: Boolean, + disambiguator: IDisambiguator = IDisambiguator.default(), + ): LRParser = LRParser(getParseTable(startConcept, forCodeCompletion), disambiguator) - private fun loadRulesFromSubconcepts(rules: MutableList, concept: IConcept, visited: MutableSet, engine: EditorEngine) { + private fun loadRulesFromSubconcepts( + rules: MutableList, + concept: IConcept, + visited: MutableSet, + engine: EditorEngine, + ) { if (visited.contains(concept)) return for (subConcept in concept.getInstantiatableSubConcepts()) { loadRules(rules, subConcept, visited, engine) @@ -37,7 +48,12 @@ class ParserForEditor(val engine: EditorEngine) { visited.add(concept) } - private fun loadRules(rules: MutableList, concept: IConcept, visited: MutableSet, engine: EditorEngine) { + private fun loadRules( + rules: MutableList, + concept: IConcept, + visited: MutableSet, + engine: EditorEngine, + ) { if (visited.contains(concept)) return visited.add(concept) @@ -49,7 +65,12 @@ class ParserForEditor(val engine: EditorEngine) { rules.add(rule) } - val childConcepts = cellModel.getGrammarSymbols().leafSymbols().filterIsInstance().map { it.link.targetConcept } + val childConcepts = + cellModel + .getGrammarSymbols() + .leafSymbols() + .filterIsInstance() + .map { it.link.targetConcept } for (childConcept in childConcepts) { loadRulesFromSubconcepts(rules, childConcept, visited, engine) } diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/PlaceholderCellReference.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/PlaceholderCellReference.kt deleted file mode 100644 index dd19d14f..00000000 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/PlaceholderCellReference.kt +++ /dev/null @@ -1,6 +0,0 @@ -package org.modelix.editor.celltemplate - -import org.modelix.editor.CellReference -import org.modelix.editor.TemplateCellReference - -data class PlaceholderCellReference(val childCellRef: TemplateCellReference) : CellReference() diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/PropertyCellTemplate.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/PropertyCellTemplate.kt index 3a532bc8..68f150a1 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/PropertyCellTemplate.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/PropertyCellTemplate.kt @@ -4,10 +4,10 @@ import org.modelix.constraints.ConstraintsAspect import org.modelix.editor.CaretPositionPolicy import org.modelix.editor.CellActionProperties import org.modelix.editor.CellCreationContext -import org.modelix.editor.CellData +import org.modelix.editor.CellSpecBase +import org.modelix.editor.CellTreeState import org.modelix.editor.CodeCompletionParameters import org.modelix.editor.CommonCellProperties -import org.modelix.editor.EditorComponent import org.modelix.editor.IActionOrProvider import org.modelix.editor.ICodeCompletionAction import org.modelix.editor.ICodeCompletionActionProvider @@ -18,8 +18,9 @@ import org.modelix.editor.ITextChangeAction import org.modelix.editor.PropertyCellReference import org.modelix.editor.PropertyCompletionToken import org.modelix.editor.TemplateCellReference -import org.modelix.editor.TextCellData +import org.modelix.editor.TextCellSpec import org.modelix.editor.replacement +import org.modelix.editor.text.backend.BackendEditorComponent import org.modelix.editor.toNonExisting import org.modelix.model.api.IConcept import org.modelix.model.api.INode @@ -29,19 +30,18 @@ import org.modelix.parser.PropertySymbol import org.modelix.parser.RegexSymbol import org.modelix.parser.Token -open class PropertyCellTemplate(concept: IConcept, val property: IProperty) : - CellTemplate(concept), IGrammarConditionSymbol { +open class PropertyCellTemplate( + concept: IConcept, + val property: IProperty, +) : CellTemplate(concept), + IGrammarConditionSymbol { var placeholderText: String = "" var validator: ((String) -> Boolean)? = null var regex: Regex? = null - override fun toParserSymbol(): ISymbol { - return PropertySymbol(property, regex ?: RegexSymbol.defaultPropertyPattern) - } + override fun toParserSymbol(): ISymbol = PropertySymbol(property, regex ?: RegexSymbol.defaultPropertyPattern) - override fun toCompletionToken(): ICompletionTokenOrList? { - return PropertyCompletionToken(property) - } + override fun toCompletionToken(): ICompletionTokenOrList? = PropertyCompletionToken(property) override fun consumeTokens(builder: IParseTreeToAstBuilder) { val symbol = toParserSymbol() @@ -49,70 +49,80 @@ open class PropertyCellTemplate(concept: IConcept, val property: IProperty) : builder.currentNode().setPropertyValue(property, (token as Token).text) } - override fun createCell(context: CellCreationContext, node: INode): CellData { + override fun createCell( + context: CellCreationContext, + node: INode, + ): CellSpecBase { val value = node.getPropertyValue(property) - val data = TextCellData(value ?: "", if (value == null) placeholderText else "") + val data = TextCellSpec(value ?: "", if (value == null) placeholderText else "") data.properties[CellActionProperties.replaceText] = ChangePropertyAction(node) data.properties[CommonCellProperties.tabTarget] = true - data.cellReferences += PropertyCellReference(property, node.reference) + data.cellReferences += PropertyCellReference(property.toReference(), node.reference) return data } - override fun getInstantiationActions(location: INonExistingNode, parameters: CodeCompletionParameters): List? { - return listOf(WrapPropertyValueProvider(location)) - } + override fun getInstantiationActions( + location: INonExistingNode, + parameters: CodeCompletionParameters, + ): List? = listOf(WrapPropertyValueProvider(location)) - private fun validateValue(node: INonExistingNode, value: String): Boolean { - return validator?.invoke(value) + private fun validateValue( + node: INonExistingNode, + value: String, + ): Boolean = + validator?.invoke(value) ?: regex?.matches(value) ?: ConstraintsAspect.checkPropertyValue(node, property, value).isEmpty() - } - override fun getSymbolConditionState(node: INode): Boolean { - return node.getPropertyValue(property) != null - } + override fun getSymbolConditionState(node: INode): Boolean = node.getPropertyValue(property) != null - override fun setSymbolConditionFalse(node: INode) { - return node.setPropertyValue(property, null) - } + override fun setSymbolConditionFalse(node: INode) = node.setPropertyValue(property, null) - override fun getSymbolTransformationAction(node: INode, optionalCell: TemplateCellReference): IActionOrProvider? { - return WrapPropertyValueProvider(node.toNonExisting()) - } + override fun getSymbolTransformationAction( + node: INode, + optionalCell: TemplateCellReference, + ): IActionOrProvider? = WrapPropertyValueProvider(node.toNonExisting()) - inner class WrapPropertyValueProvider(val location: INonExistingNode) : ICodeCompletionActionProvider { - override fun getApplicableActions(parameters: CodeCompletionParameters): List { - return if (parameters.pattern.isNotBlank() && validateValue(location.replacement(concept), parameters.pattern)) { + inner class WrapPropertyValueProvider( + val location: INonExistingNode, + ) : ICodeCompletionActionProvider { + override fun getApplicableActions(parameters: CodeCompletionParameters): List = + if (parameters.pattern.isNotBlank() && validateValue(location.replacement(concept), parameters.pattern)) { listOf(WrapPropertyValue(location, parameters.pattern)) } else { emptyList() } - } } - inner class WrapPropertyValue(val location: INonExistingNode, val value: String) : ICodeCompletionAction { - override fun getMatchingText(): String { - return value - } + inner class WrapPropertyValue( + val location: INonExistingNode, + val value: String, + ) : ICodeCompletionAction { + override fun getMatchingText(): String = value - override fun getDescription(): String { - return concept.getShortName() - } + override fun getDescription(): String = concept.getShortName() - override fun execute(editor: EditorComponent): CaretPositionPolicy? { + override fun execute(editor: BackendEditorComponent): CaretPositionPolicy? { val node = location.getOrCreateNode(concept) node.setPropertyValue(property, value) return CaretPositionPolicy(createCellReference(node)) } } - inner class ChangePropertyAction(val node: INode) : ITextChangeAction { + inner class ChangePropertyAction( + val node: INode, + ) : ITextChangeAction { override fun isValid(value: String?): Boolean { if (value == null) return true return validateValue(node.toNonExisting(), value) } - override fun replaceText(editor: EditorComponent, range: IntRange, replacement: String, newText: String): Boolean { + override fun replaceText( + editor: CellTreeState, + range: IntRange, + replacement: String, + newText: String, + ): Boolean { node.getArea().executeWrite { node.setPropertyValue(property, newText) } diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/ReferenceCellTemplate.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/ReferenceCellTemplate.kt index 9acf777e..b70276c3 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/ReferenceCellTemplate.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/ReferenceCellTemplate.kt @@ -3,10 +3,9 @@ package org.modelix.editor.celltemplate import org.modelix.editor.CaretPositionPolicy import org.modelix.editor.CellActionProperties import org.modelix.editor.CellCreationContext -import org.modelix.editor.CellData +import org.modelix.editor.CellSpecBase import org.modelix.editor.CodeCompletionParameters import org.modelix.editor.CommonCellProperties -import org.modelix.editor.EditorComponent import org.modelix.editor.ExistingNode import org.modelix.editor.IActionOrProvider import org.modelix.editor.ICodeCompletionAction @@ -18,9 +17,10 @@ import org.modelix.editor.ReferenceCompletionToken import org.modelix.editor.ReferenceTargetActionProvider import org.modelix.editor.ReferencedNodeCellReference import org.modelix.editor.TemplateCellReference -import org.modelix.editor.TextCellData +import org.modelix.editor.TextCellSpec import org.modelix.editor.after import org.modelix.editor.replacement +import org.modelix.editor.text.backend.BackendEditorComponent import org.modelix.editor.toNonExisting import org.modelix.model.api.IConcept import org.modelix.model.api.INode @@ -34,15 +34,11 @@ class ReferenceCellTemplate( concept: IConcept, val link: IReferenceLink, val presentation: INode.() -> String?, -) : CellTemplate(concept), IGrammarSymbol { +) : CellTemplate(concept), + IGrammarSymbol { + override fun toParserSymbol(): ISymbol = ReferenceSymbol(link) - override fun toParserSymbol(): ISymbol { - return ReferenceSymbol(link) - } - - override fun toCompletionToken(): ICompletionTokenOrList? { - return ReferenceCompletionToken(link) - } + override fun toCompletionToken(): ICompletionTokenOrList? = ReferenceCompletionToken(link) override fun consumeTokens(builder: IParseTreeToAstBuilder) { val symbol = toParserSymbol() @@ -50,53 +46,61 @@ class ReferenceCellTemplate( // TODO builder.currentNode().setReferenceTarget(link, TODO()) } - override fun createCell(context: CellCreationContext, node: INode): CellData { - val data = TextCellData(getText(node), "") - data.cellReferences += ReferencedNodeCellReference(node.reference, link) + override fun createCell( + context: CellCreationContext, + node: INode, + ): CellSpecBase { + val data = TextCellSpec(getText(node), "") + data.cellReferences += ReferencedNodeCellReference(node.reference, link.toReference()) data.properties[CommonCellProperties.tabTarget] = true data.properties[CellActionProperties.substitute] = ReferenceTargetActionProvider(ExistingNode(node), link, { it.getNode()?.let(presentation) ?: "" }).after { - context.editorState.substitutionPlaceholderPositions.remove(createCellReference(node)) + context.cellTreeState.substitutionPlaceholderPositions.remove(createCellReference(node)) } return data } + private fun getText(node: INode): String = getTargetNode(node)?.let(presentation) ?: "" - private fun getTargetNode(sourceNode: INode): INode? { - return sourceNode.getReferenceTarget(link) - } - override fun getInstantiationActions(location: INonExistingNode, parameters: CodeCompletionParameters): List { - return listOf(WrapReferenceTargetProvider(location.replacement(concept))) - } - override fun getSymbolTransformationAction(node: INode, optionalCell: TemplateCellReference): IActionOrProvider? { - return WrapReferenceTargetProvider(node.toNonExisting()) - } + private fun getTargetNode(sourceNode: INode): INode? = sourceNode.getReferenceTarget(link) + + override fun getInstantiationActions( + location: INonExistingNode, + parameters: CodeCompletionParameters, + ): List = listOf(WrapReferenceTargetProvider(location.replacement(concept))) + + override fun getSymbolTransformationAction( + node: INode, + optionalCell: TemplateCellReference, + ): IActionOrProvider? = WrapReferenceTargetProvider(node.toNonExisting()) - inner class WrapReferenceTargetProvider(val sourceNode: INonExistingNode) : ICodeCompletionActionProvider { + inner class WrapReferenceTargetProvider( + val sourceNode: INonExistingNode, + ) : ICodeCompletionActionProvider { override fun getApplicableActions(parameters: CodeCompletionParameters): List { val scope = ScopeAspect.getScope(sourceNode, link) val targets = scope.getVisibleElements(sourceNode, link) return targets.map { target -> - val text = when (target) { - is ExistingNode -> presentation(target.getNode()) ?: "" - else -> "" - } + val text = + when (target) { + is ExistingNode -> presentation(target.getNode()) ?: "" + else -> "" + } WrapReferenceTarget(sourceNode, target, text) } } } - inner class WrapReferenceTarget(val location: INonExistingNode, val target: INonExistingNode, val presentation: String) : - ICodeCompletionAction { - override fun getMatchingText(): String { - return presentation - } + inner class WrapReferenceTarget( + val location: INonExistingNode, + val target: INonExistingNode, + val presentation: String, + ) : ICodeCompletionAction { + override fun getMatchingText(): String = presentation - override fun getDescription(): String { - return concept.getShortName() - } + override fun getDescription(): String = concept.getShortName() - override fun execute(editor: EditorComponent): CaretPositionPolicy? { + override fun execute(editor: BackendEditorComponent): CaretPositionPolicy? { val sourceNode = location.getOrCreateNode(concept) sourceNode.setReferenceTarget(link, target.getOrCreateNode()) return CaretPositionPolicy(createCellReference(sourceNode)) diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/text/backend/BackendEditorComponent.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/text/backend/BackendEditorComponent.kt new file mode 100644 index 00000000..f856a1ec --- /dev/null +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/text/backend/BackendEditorComponent.kt @@ -0,0 +1,90 @@ +package org.modelix.editor.text.backend + +import org.modelix.editor.CachedCodeCompletionActions +import org.modelix.editor.CellCreationCall +import org.modelix.editor.CellTreeState +import org.modelix.editor.CodeCompletionParameters +import org.modelix.editor.EditorEngine +import org.modelix.editor.ICodeCompletionAction +import org.modelix.editor.ICodeCompletionActionProvider +import org.modelix.editor.NodeCellCreationCall +import org.modelix.editor.Selection +import org.modelix.editor.applyShadowing +import org.modelix.editor.getCompletionPattern +import org.modelix.editor.text.shared.celltree.BackendCellTree +import org.modelix.editor.text.shared.celltree.CellTreeOp + +class BackendEditorComponent( + val rootCall: CellCreationCall, + val engine: EditorEngine, +) { + val state = CellTreeState() + val tree: BackendCellTree get() = state.cellTree + var completionMenu: CompletionMenuBackend? = null + private var selectionUpdater: (() -> Selection?)? = null + + fun dispose() {} + + fun updateNow() = update() + + fun selectAfterUpdate(newSelection: () -> Selection?) { + selectionUpdater = newSelection + } + + fun update(): List = + tree.runUpdate { + runRead { + val newCell = engine.createCell(state, rootCall) + tree + .getRoot() + .getChildren() + .minus(newCell) + .forEach { it.detach() } + if (newCell.getParent() != tree.getRoot()) { + newCell.moveCell(tree.getRoot(), 0) + } + } + } + + fun loadCompletionEntries( + providers: List, + pattern: String, + ): List = + CompletionMenuBackend(providers).let { + completionMenu = it + it.updateActions(pattern) + } + + fun runWrite(body: () -> R): R = + when (rootCall) { + is NodeCellCreationCall -> rootCall.node.getModel().executeWrite(body) + } + + fun runRead(body: () -> R): R = + when (rootCall) { + is NodeCellCreationCall -> rootCall.node.getModel().executeRead(body) + } + + inner class CompletionMenuBackend( + val providers: List, + ) { + val actionsCache = CachedCodeCompletionActions(providers) + private var entries: List = emptyList() + + fun updateActions(pattern: String): List = computeActions(pattern).also { entries = it } + + fun getEntries(): List = entries + + fun computeActions(pattern: String): List = + runRead { + val parameters = CodeCompletionParameters(this@BackendEditorComponent, pattern) + actionsCache + .update(parameters) + .filter { + val matchingText = it.getCompletionPattern() + matchingText.isNotEmpty() && matchingText.startsWith(parameters.pattern) + }.applyShadowing() + .sortedBy { it.getCompletionPattern().lowercase() } + } + } +} diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/text/backend/TextEditorServiceImpl.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/text/backend/TextEditorServiceImpl.kt new file mode 100644 index 00000000..5197ccaf --- /dev/null +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/text/backend/TextEditorServiceImpl.kt @@ -0,0 +1,500 @@ +package org.modelix.editor.text.backend + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.SendChannel +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import org.modelix.editor.CaretPositionPolicy +import org.modelix.editor.CaretPositionPolicyWithIndex +import org.modelix.editor.Cell +import org.modelix.editor.CellActionProperties +import org.modelix.editor.CodeCompletionParameters +import org.modelix.editor.CommonCellProperties +import org.modelix.editor.CompletionPosition +import org.modelix.editor.ECellType +import org.modelix.editor.EditorEngine +import org.modelix.editor.ICaretPositionPolicy +import org.modelix.editor.ICellAction +import org.modelix.editor.ICodeCompletionAction +import org.modelix.editor.ancestors +import org.modelix.editor.applyShadowing +import org.modelix.editor.centerAlignedHierarchy +import org.modelix.editor.flattenApplicableActions +import org.modelix.editor.getActionsAfter +import org.modelix.editor.getActionsBefore +import org.modelix.editor.getBordersBetween +import org.modelix.editor.getCompletionPattern +import org.modelix.editor.getSubstituteActions +import org.modelix.editor.isTabTarget +import org.modelix.editor.isVisible +import org.modelix.editor.leftBorder +import org.modelix.editor.nextCells +import org.modelix.editor.nextLeaf +import org.modelix.editor.previousCells +import org.modelix.editor.rightBorder +import org.modelix.editor.text.frontend.getSelectableText +import org.modelix.editor.text.frontend.type +import org.modelix.editor.text.shared.CompletionMenuEntryData +import org.modelix.editor.text.shared.CompletionMenuTrigger +import org.modelix.editor.text.shared.EditorId +import org.modelix.editor.text.shared.EditorUpdateData +import org.modelix.editor.text.shared.ServiceCallResult +import org.modelix.editor.text.shared.TextEditorService +import org.modelix.editor.text.shared.celltree.CellInstanceId +import org.modelix.editor.text.shared.celltree.ICellTree +import org.modelix.editor.text.shared.celltree.cellReferences +import org.modelix.model.api.IMutableModel +import org.modelix.model.api.NodeReference +import org.modelix.model.api.runSynchronized + +class TextEditorServiceImpl( + val engine: EditorEngine, + val model: IMutableModel, + val coroutineScope: CoroutineScope, +) : TextEditorService { + private var updateChannels: AtomicReference> = AtomicReference(emptyMap()) + private val validator = Validator(coroutineScope) { sendUpdates() } + + init { + validator.start() + } + + fun getAllEditorBackends(): List = updateChannels.get().map { it.value.editor } + + fun getEditorBackend(editorId: Int): BackendEditorComponent = updateChannels.get().getValue(editorId).editor + + override fun openNode( + editorId: EditorId, + nodeRef: NodeReference, + ): Flow { + val node = model.executeRead { model.resolveNode(nodeRef) } + val editorBackend = engine.editNode(node) + return channelFlow { + val updateChannel = EditorUpdateChannel(editorId, editorBackend, channel) + try { + updateChannels.getAndUpdate { it + (editorId to updateChannel) } + updateChannel.sendUpdate() + awaitClose() + } finally { + updateChannels.getAndUpdate { it - editorId } + } + } + } + + private suspend fun runWithCell( + editorId: Int, + cellId: CellInstanceId, + body: (EditorUpdateChannel, ICellTree.Cell) -> R, + ): R = + runWithEditor(editorId) { updateChannel, editor -> + body(updateChannel, editor.tree.getCell(cellId)) + } + + private suspend fun runWithEditor( + editorId: Int, + body: (EditorUpdateChannel, BackendEditorComponent) -> R, + ): R { + val updateChannel: EditorUpdateChannel = + requireNotNull(updateChannels.get().get(editorId)) { + "Editor not found: $editorId" + } + return updateChannel.withPausedUpdates { + val editor = updateChannel.editor + body(updateChannel, editor) + } + } + + override suspend fun navigateTab( + editorId: Int, + cellId: CellInstanceId, + forward: Boolean, + ): EditorUpdateData = + runWithCell(editorId, cellId) { updateChannel, cell -> + for (c in if (forward) cell.nextCells() else cell.previousCells()) { + if (c.isTabTarget()) { + if (c.type == ECellType.TEXT) { + return@runWithCell updateChannel.createSelection(c, 0) + } + } + val action = c.getProperty(CellActionProperties.show) + if (action != null) { + // cannot tab into nested optionals because the parent optional will disappear + if (!c.ancestors(true).any { it.getProperty(CommonCellProperties.isForceShown) }) { + updateChannel.editor.state.forceShowOptionals + .clear() + return@runWithCell updateChannel.createSelection(action.execute(updateChannel.editor)) + } + } + } + return@runWithCell updateChannel.createUpdate() + } + + override suspend fun executeDelete( + editorId: Int, + cellId: CellInstanceId, + ): EditorUpdateData = + runWithCell(editorId, cellId) { updateChannel, cell -> + val deleteAction = + cell + .ancestors(true) + .mapNotNull { it.getProperty(CellActionProperties.delete) } + .firstOrNull { it.isApplicable() } + if (deleteAction != null) { + return@runWithCell updateChannel.createSelection(deleteAction.execute(updateChannel.editor)) + } + return@runWithCell updateChannel.createUpdate() + } + + override suspend fun executeInsert( + editorId: Int, + cellId: CellInstanceId, + ): EditorUpdateData = + runWithCell(editorId, cellId) { updateChannel, cell -> + val actionOnSelectedCell = cell.getProperty(CellActionProperties.insert)?.takeIf { it.isApplicable() } + if (actionOnSelectedCell != null) { + return@runWithCell updateChannel.createSelection(actionOnSelectedCell.execute(updateChannel.editor)) + } else { + var previousLeaf: Cell? = cell + while (previousLeaf != null) { + val nextLeaf = previousLeaf.nextLeaf { it.isVisible() } + val actions = + getBordersBetween(previousLeaf.rightBorder(), nextLeaf?.leftBorder()) + .filter { it.isLeft } + .mapNotNull { it.cell.getProperty(CellActionProperties.insert) } + .distinct() + .filter { it.isApplicable() } + // TODO resolve conflicts if multiple actions are applicable + val action = actions.firstOrNull() + if (action != null) { + return@runWithCell updateChannel.createSelection(action.execute(updateChannel.editor)) + } + previousLeaf = nextLeaf + } + } + return@runWithCell updateChannel.createUpdate() + } + + override suspend fun processTypedText( + editorId: Int, + cellId: CellInstanceId, + range: IntRange, + replacement: String, + ): EditorUpdateData { + return runWithCell(editorId, cellId) { updateChannel, cell -> + val oldText = cell.getSelectableText() ?: "" + val textLength = oldText.length + val leftTransform = range.isEmpty() && range.first == 0 + val rightTransform = range.isEmpty() && range.first == textLength + if (leftTransform || rightTransform) { + // if (replaceText(range, typedText, editor, false)) return + + val completionPosition = if (leftTransform) CompletionPosition.LEFT else CompletionPosition.RIGHT + val providers = + ( + if (completionPosition == CompletionPosition.LEFT) { + cell.getActionsBefore() + } else { + cell.getActionsAfter() + } + ).toList() + val params = CodeCompletionParameters(updateChannel.editor, replacement) + val matchingActions = + updateChannel.editor.runRead { + val actions = providers.flatMap { it.flattenApplicableActions(params) } + actions + .filter { it.getCompletionPattern().startsWith(replacement) } + .applyShadowing() + } + if (matchingActions.isNotEmpty()) { + if (matchingActions.size == 1 && matchingActions.first().getCompletionPattern() == replacement) { + return@runWithCell matchingActions.first().executeAndUpdateSelection(updateChannel) + } + return@runWithCell updateChannel.createUpdate().copy( + completionMenuTrigger = + CompletionMenuTrigger( + anchor = cell.getId(), + completionPosition = completionPosition, + pattern = replacement, + caretPosition = replacement.length + ), + completionEntries = + updateChannel.editor.loadCompletionEntries(providers, replacement).mapIndexed { index, entry -> + CompletionMenuEntryData( + id = index, + matchingText = entry.getMatchingText(), + description = entry.getDescription() + ) + } + ) + } + } + replaceText(cell, range, replacement, updateChannel, true) ?: updateChannel.createUpdate() + } + } + + private fun replaceText( + cell: ICellTree.Cell, + range: IntRange, + replacement: String, + updateChannel: EditorUpdateChannel, + triggerCompletion: Boolean, + ): EditorUpdateData? { + val editor = updateChannel.editor + val oldText = cell.getSelectableText() ?: "" + val newText = oldText.replaceRange(range, replacement) + + if (triggerCompletion) { + // complete immediately if there is a single matching action + val providers = cell.getSubstituteActions() + val params = CodeCompletionParameters(editor, newText) + val actions = editor.runRead { providers.flatMap { it.flattenApplicableActions(params) }.toList() } + val matchingActions = + actions + .filter { it.getTokens().consumeForAutoApply(newText)?.length == 0 } + .applyShadowing() + val singleAction = matchingActions.singleOrNull() + if (singleAction != null) { + val caretPolicy = + editor.runWrite { + singleAction.execute(editor).also { + editor.state.clearTextReplacement(cell) + } + } + return updateChannel.createSelection(caretPolicy) + } + } + + val replaceTextActions = cell.centerAlignedHierarchy().mapNotNull { it.getProperty(CellActionProperties.replaceText) } + for (action in replaceTextActions) { + if (action.isValid(newText) && action.replaceText(editor.state, range, replacement, newText)) { + val cellReferences = cell.cellReferences.toSet() + return updateChannel + .createUpdate() + .copy(selectionChange = CaretPositionPolicyWithIndex(cellReferences, range.first + replacement.length)) + } + } + return null + } + + override suspend fun triggerCodeCompletion( + editorId: Int, + cellId: CellInstanceId, + caretPosition: Int, + ): EditorUpdateData = + runWithCell(editorId, cellId) { updateChannel, cell -> + val pattern = cell.getSelectableText().orEmpty().take(caretPosition) + val providers = cell.getSubstituteActions().toList() + updateChannel.createUpdate().copy( + completionMenuTrigger = + CompletionMenuTrigger( + anchor = cell.getId(), + completionPosition = CompletionPosition.CENTER, + pattern = pattern, + caretPosition = caretPosition + ), + completionEntries = + updateChannel.editor.loadCompletionEntries(providers, pattern).mapIndexed { index, entry -> + CompletionMenuEntryData( + id = index, + matchingText = entry.getMatchingText(), + description = entry.getDescription() + ) + } + ) + } + + override suspend fun updateCodeCompletionActions( + editorId: Int, + cellId: CellInstanceId, + pattern: String, + ): EditorUpdateData = + runWithCell(editorId, cellId) { updateChannel, cell -> + val providers = cell.getSubstituteActions().toList() + updateChannel.createUpdate().copy( + completionEntries = + updateChannel.editor.loadCompletionEntries(providers, pattern).mapIndexed { index, entry -> + CompletionMenuEntryData( + id = index, + matchingText = entry.getMatchingText(), + description = entry.getDescription() + ) + } + ) + } + + override suspend fun hasCodeCompletionActions( + editorId: Int, + cellId: CellInstanceId, + pattern: String, + ): Boolean = + runWithCell(editorId, cellId) { updateChannel, cell -> + model.executeRead { + updateChannel.editor.completionMenu + ?.computeActions(pattern) + ?.any() == true + } + } + + override suspend fun executeCodeCompletionAction( + editorId: Int, + actionId: Int, + ): EditorUpdateData = + runWithEditor(editorId) { updateChannel, editor -> + model.executeWrite { + val action = + requireNotNull(editor.completionMenu?.getEntries()?.getOrNull(actionId)) { + "Action with ID $actionId not found" + } + val policy = action.execute(editor) + val update = updateChannel.createUpdate() + update.copy( + selectionChange = policy ?: update.selectionChange, + ) + } + } + + override suspend fun replaceText( + editorId: Int, + cellId: CellInstanceId, + range: IntRange, + replacement: String, + triggerCompletion: Boolean, + ): ServiceCallResult { + return runWithCell(editorId, cellId) { updateChannel, cell -> + val editor = updateChannel.editor + val oldText = cell.getSelectableText() ?: "" + val newText = oldText.replaceRange(range, replacement) + + if (triggerCompletion) { + // complete immediately if there is a single matching action + val providers = cell.getSubstituteActions() + val params = CodeCompletionParameters(editor, newText) + val actions = editor.runRead { providers.flatMap { it.flattenApplicableActions(params) }.toList() } + val matchingActions = + actions + .filter { it.getTokens().consumeForAutoApply(newText)?.length == 0 } + .applyShadowing() + val singleAction = matchingActions.singleOrNull() + if (singleAction != null) { + editor.runWrite { + singleAction.executeAndUpdateSelection(updateChannel) + editor.state.clearTextReplacement(cell) + } + return@runWithCell ServiceCallResult( + updateData = updateChannel.createUpdate(), + result = true + ) + } + } + + val replaceTextActions = cell.centerAlignedHierarchy().mapNotNull { it.getProperty(CellActionProperties.replaceText) } + for (action in replaceTextActions) { + val newCaretPosition = + CaretPositionPolicyWithIndex( + CaretPositionPolicy(avoidedCellRefs = emptySet(), preferredCellRefs = cell.cellReferences.toSet()), + range.first + replacement.length + ) + if (action.isValid(newText) && action.replaceText(editor.state, range, replacement, newText)) { + return@runWithCell ServiceCallResult( + updateData = + updateChannel.createUpdate().copy( + selectionChange = newCaretPosition + ), + result = true + ) + } + } + return@runWithCell ServiceCallResult(false) + } + } + + override suspend fun resetState(editorId: Int): EditorUpdateData = + runWithEditor(editorId) { updateChannel, editor -> + editor.state.reset() + updateChannel.createUpdate() + } + + override suspend fun flush(editorId: Int): EditorUpdateData = + runWithEditor(editorId) { updateChannel, editor -> updateChannel.createUpdate() } + + private fun ICellAction.executeAndUpdateSelection(channel: EditorUpdateChannel): EditorUpdateData = + channel.createSelection(execute(channel.editor)) + + private fun ICodeCompletionAction.executeAndUpdateSelection(channel: EditorUpdateChannel): EditorUpdateData = + channel.createSelection(execute(channel.editor)) + + fun triggerUpdates() { + validator.invalidate() + } + + private suspend fun sendUpdates() { + for (updateChannel in updateChannels.get().values) { + updateChannel.sendUpdate() + } + } + + fun dispose() { + validator.stop() + engine.dispose() + } +} + +class EditorUpdateChannel( + val editorId: EditorId, + val editor: BackendEditorComponent, + val channel: SendChannel, +) { + private val mutex = Mutex() + + suspend fun sendUpdate() { + mutex.withLock { + editor + .update() + .takeIf { it.isNotEmpty() } + ?.let { channel.send(EditorUpdateData(it)) } + } + } + + suspend fun withPausedUpdates(body: suspend () -> R): R = + mutex.withLock { + body() + } + + fun createSelection( + textCell: ICellTree.Cell, + position: Int, + ): EditorUpdateData { + require(textCell.type == ECellType.TEXT) { "Not a text cell: $textCell" } + val newSelection = + CaretPositionPolicyWithIndex( + policy = + CaretPositionPolicy( + avoidedCellRefs = emptySet(), + preferredCellRefs = textCell.cellReferences.toSet() + ), + index = position + ) + return createSelection(newSelection) + } + + fun createSelection(newSelection: ICaretPositionPolicy?): EditorUpdateData = + EditorUpdateData(cellTreeChanges = editor.update(), selectionChange = newSelection) + + fun createUpdate(): EditorUpdateData = EditorUpdateData(editor.update()) +} + +class AtomicReference( + private var value: E, +) { + fun getAndUpdate(updater: (E) -> E): E { + runSynchronized(this) { + value = updater(value) + return value + } + } + + fun get() = value +} diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/text/backend/Validator.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/text/backend/Validator.kt new file mode 100644 index 00000000..f28fc31f --- /dev/null +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/text/backend/Validator.kt @@ -0,0 +1,51 @@ +package org.modelix.editor.text.backend + +import io.github.oshai.kotlinlogging.KotlinLogging +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.launch + +/** + * When calling invalidate(), the `validator` function is executed, but avoid executing it too often when there are + * many invalidate() calls. + */ +class Validator( + val coroutineScope: CoroutineScope, + private val validator: suspend () -> Unit, +) { + private val channel = Channel(capacity = 1, onBufferOverflow = BufferOverflow.DROP_LATEST) + private var validationJob: Job? = null + + fun invalidate() { + channel.trySend(Unit) + } + + fun start() { + check(validationJob?.isActive != true) { "Already started" } + validationJob = + coroutineScope.launch { + for (x in channel) { + try { + validator() + } catch (ex: CancellationException) { + throw ex + } catch (ex: Throwable) { + LOG.error(ex) { "Validation failed" } + } + } + } + } + + fun stop() { + validationJob?.cancel("stopped") + validationJob = null + } + + companion object { + private val LOG = KotlinLogging.logger { } + } +} diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/text/frontend/FrontendCellTree.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/text/frontend/FrontendCellTree.kt new file mode 100644 index 00000000..e1aca5b2 --- /dev/null +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/text/frontend/FrontendCellTree.kt @@ -0,0 +1,219 @@ +package org.modelix.editor.text.frontend + +import org.modelix.editor.CellPropertyKey +import org.modelix.editor.CommonCellProperties +import org.modelix.editor.ECellType +import org.modelix.editor.FrontendEditorComponent +import org.modelix.editor.LayoutableCell +import org.modelix.editor.LayoutedText +import org.modelix.editor.ResettableLazy +import org.modelix.editor.TextCellProperties +import org.modelix.editor.TextLayouter +import org.modelix.editor.text.backend.TextEditorServiceImpl +import org.modelix.editor.text.shared.EditorId +import org.modelix.editor.text.shared.celltree.CellDeleteOp +import org.modelix.editor.text.shared.celltree.CellDetachOp +import org.modelix.editor.text.shared.celltree.CellInstanceId +import org.modelix.editor.text.shared.celltree.CellPropertyChangeOp +import org.modelix.editor.text.shared.celltree.CellPropertyRemoveOp +import org.modelix.editor.text.shared.celltree.CellTreeBase +import org.modelix.editor.text.shared.celltree.CellTreeOp +import org.modelix.editor.text.shared.celltree.ICellTree +import org.modelix.editor.text.shared.celltree.IMutableCellTree +import org.modelix.editor.text.shared.celltree.MoveCellOp +import org.modelix.editor.text.shared.celltree.MoveCellToOp +import org.modelix.editor.text.shared.celltree.NewCellOp +import org.modelix.editor.text.shared.celltree.NewChildCellOp + +class FrontendCellTree( + val editorComponent: FrontendEditorComponent? = null, +) : CellTreeBase() { + override fun newCellInstance( + id: CellInstanceId, + parent: CellImpl?, + ): CellImpl = FrontendCellImpl(id, parent as FrontendCellImpl?) + + override fun getRoot(): FrontendCellImpl = super.getRoot() as FrontendCellImpl + + fun applyChanges(changes: List) { + withTreeLock { + for (op in changes) { + when (op) { + is CellDeleteOp -> getCell(op.id).detach() + is CellDetachOp -> getCell(op.id).detach() + is CellPropertyChangeOp -> getCell(op.id).setProperty(op.key, op.value) + is CellPropertyRemoveOp -> getCell(op.id).removeProperty(op.key) + is MoveCellOp -> getCell(op.childId).moveCell(op.index) + is MoveCellToOp -> getCell(op.childId).moveCell(getCell(op.targetParent), op.index) + is NewCellOp -> createCell(op.id) + is NewChildCellOp -> getCell(op.parentId).addNewChild(op.index, op.childId) + } + } + } + } + + inner class FrontendCellImpl( + id: CellInstanceId, + parent: FrontendCellImpl?, + ) : CellImpl(id, parent) { + private var cachedLayout = + ResettableLazy { + runLayoutOnCell(this) { it.layout } + } + val layout: LayoutedText + get() = cachedLayout.value + + fun clearCachedLayout() { + withTreeLock { + cachedLayout.reset() + } + } + + fun invalidateLayout() { + withTreeLock { + cachedLayout.reset() + getParent()?.invalidateLayout() + } + } + + fun getEditorComponent() = this@FrontendCellTree.editorComponent + + override fun getParent() = super.getParent() as FrontendCellImpl? + + override fun getProperty(key: CellPropertyKey): T = + withTreeLock { + require(key.frontend) { "Property ${key.name} is not available in the frontend" } + if (properties.containsKey(key.name)) key.fromSerializableValue(properties[key.name]) else key.defaultValue + } + + override fun setProperty( + key: CellPropertyKey, + newValue: T, + ) { + withTreeLock { + super.setProperty(key, newValue) + invalidateLayout() + } + } + + override fun removeProperty(key: CellPropertyKey<*>) { + withTreeLock { + super.removeProperty(key) + invalidateLayout() + } + } + + override fun addNewChild(index: Int): IMutableCellTree.MutableCell = + withTreeLock { + super.addNewChild(index).also { + invalidateLayout() + } + } + + override fun moveCell(index: Int) { + withTreeLock { + super.moveCell(index) + invalidateLayout() + } + } + + override fun moveCell( + targetParent: IMutableCellTree.MutableCell, + index: Int, + ) { + withTreeLock { + invalidateLayout() + super.moveCell(targetParent, index) + invalidateLayout() + } + } + + override fun detach() { + withTreeLock { + invalidateLayout() + super.detach() + } + } + + override fun delete() { + withTreeLock { + invalidateLayout() + super.delete() + } + } + } +} + +val ICellTree.Cell.type: ECellType get() = getProperty(CommonCellProperties.type) + +val ICellTree.Cell.text: String? get() = getProperty(TextCellProperties.text) +var IMutableCellTree.MutableCell.text: String? + get() = getProperty(TextCellProperties.text) + set(value) = setProperty(TextCellProperties.text, value) +val ICellTree.Cell.placeholderText: String? get() = getProperty(TextCellProperties.placeholderText) + +fun ICellTree.Cell.getVisibleText(): String = + getProperty(CommonCellProperties.textReplacement) + ?: text?.takeIf { it.isNotEmpty() } + ?: placeholderText + ?: "" + +fun ICellTree.Cell.getSelectableText(): String? = getProperty(CommonCellProperties.textReplacement) ?: text + +val ICellTree.Cell.layout: LayoutedText get() = (this as FrontendCellTree.FrontendCellImpl).layout + +val ICellTree.Cell.editorComponent: FrontendEditorComponent get() { + return checkNotNull((this.getTree() as FrontendCellTree).editorComponent) { + "Cell tree isn't attached to any editor component" + } +} + +fun ICellTree.Cell.backend( + service: TextEditorServiceImpl, + editor: FrontendEditorComponent, +) = backend(service, editor.editorId) + +fun ICellTree.Cell.backend( + service: TextEditorServiceImpl, + editorId: EditorId, +) = service.getEditorBackend(editorId).tree.getCell(getId()) + +fun runLayoutOnCell(cell: ICellTree.Cell): LayoutedText = runLayoutOnCell(cell) { runLayoutOnCell(it) } + +fun runLayoutOnCell( + cell: ICellTree.Cell, + layoutChild: (ICellTree.Cell) -> LayoutedText, +): LayoutedText = + TextLayouter() + .also { + runLayoutOnCell(it, cell, layoutChild) + }.done() + +fun runLayoutOnCell( + layouter: TextLayouter, + cell: ICellTree.Cell, + layoutChild: (ICellTree.Cell) -> LayoutedText, +) { + when (cell.type) { + ECellType.COLLECTION -> { + val body: () -> Unit = { + if (cell.getProperty(CommonCellProperties.onNewLine)) layouter.onNewLine() + if (cell.getProperty(CommonCellProperties.noSpace)) layouter.noSpace() + cell.getChildren().forEach { layouter.append(layoutChild(it)) } + if (cell.getProperty(CommonCellProperties.noSpace)) layouter.noSpace() + } + if (cell.getProperty(CommonCellProperties.indentChildren)) { + layouter.withIndent(body) + } else { + body() + } + } + + ECellType.TEXT -> { + if (cell.getProperty(CommonCellProperties.onNewLine)) layouter.onNewLine() + if (cell.getProperty(CommonCellProperties.noSpace)) layouter.noSpace() + layouter.append(LayoutableCell(cell)) + if (cell.getProperty(CommonCellProperties.noSpace)) layouter.noSpace() + } + } +} diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/text/shared/Actor.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/text/shared/Actor.kt new file mode 100644 index 00000000..8665059d --- /dev/null +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/text/shared/Actor.kt @@ -0,0 +1,33 @@ +package org.modelix.editor.text.shared + +import io.github.oshai.kotlinlogging.KotlinLogging +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.Channel.Factory.RENDEZVOUS +import kotlinx.coroutines.channels.SendChannel +import kotlinx.coroutines.channels.consumeEach +import kotlinx.coroutines.launch + +private val LOG = KotlinLogging.logger { } + +fun CoroutineScope.consume( + capacity: Int = RENDEZVOUS, + onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND, + consumer: suspend (E) -> Unit, +): SendChannel { + val channel = Channel(capacity = capacity, onBufferOverflow = onBufferOverflow) + launch { + channel.consumeEach { + try { + consumer(it) + } catch (ex: CancellationException) { + throw ex + } catch (ex: Throwable) { + LOG.error(ex) { "UI event processing failed" } + } + } + } + return channel +} diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/text/shared/TextEditorService.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/text/shared/TextEditorService.kt new file mode 100644 index 00000000..8c5b9914 --- /dev/null +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/text/shared/TextEditorService.kt @@ -0,0 +1,203 @@ +package org.modelix.editor.text.shared + +import kotlinx.coroutines.flow.Flow +import kotlinx.rpc.annotations.Rpc +import kotlinx.serialization.Serializable +import org.modelix.editor.CompletionPosition +import org.modelix.editor.ICaretPositionPolicy +import org.modelix.editor.text.shared.celltree.CellInstanceId +import org.modelix.editor.text.shared.celltree.CellTreeOp +import org.modelix.model.api.NodeReference + +typealias EditorId = Int + +@Rpc +interface TextEditorService { + fun openNode( + editorId: EditorId, + nodeRef: NodeReference, + ): Flow + + suspend fun navigateTab( + editorId: Int, + cellId: CellInstanceId, + forward: Boolean, + ): EditorUpdateData + + suspend fun executeDelete( + editorId: Int, + cellId: CellInstanceId, + ): EditorUpdateData + + suspend fun executeInsert( + editorId: Int, + cellId: CellInstanceId, + ): EditorUpdateData + + suspend fun processTypedText( + editorId: Int, + cellId: CellInstanceId, + range: IntRange, + replacement: String, + ): EditorUpdateData + + suspend fun triggerCodeCompletion( + editorId: Int, + cellId: CellInstanceId, + caretPosition: Int, + ): EditorUpdateData + + suspend fun updateCodeCompletionActions( + editorId: Int, + cellId: CellInstanceId, + pattern: String, + ): EditorUpdateData + + suspend fun hasCodeCompletionActions( + editorId: Int, + cellId: CellInstanceId, + pattern: String, + ): Boolean + + suspend fun executeCodeCompletionAction( + editorId: Int, + actionId: Int, + ): EditorUpdateData + + suspend fun replaceText( + editorId: Int, + cellId: CellInstanceId, + range: IntRange, + replacement: String, + triggerCompletion: Boolean, + ): ServiceCallResult + + suspend fun resetState(editorId: Int): EditorUpdateData + + suspend fun flush(editorId: Int): EditorUpdateData +} + +class NullTextEditorService : TextEditorService { + override fun openNode( + editorId: EditorId, + nodeRef: NodeReference, + ): Flow { + TODO("Not yet implemented") + } + + override suspend fun navigateTab( + editorId: Int, + cellId: CellInstanceId, + forward: Boolean, + ): EditorUpdateData { + TODO("Not yet implemented") + } + + override suspend fun executeDelete( + editorId: Int, + cellId: CellInstanceId, + ): EditorUpdateData { + TODO("Not yet implemented") + } + + override suspend fun executeInsert( + editorId: Int, + cellId: CellInstanceId, + ): EditorUpdateData { + TODO("Not yet implemented") + } + + override suspend fun processTypedText( + editorId: Int, + cellId: CellInstanceId, + range: IntRange, + replacement: String, + ): EditorUpdateData { + TODO("Not yet implemented") + } + + override suspend fun triggerCodeCompletion( + editorId: Int, + cellId: CellInstanceId, + caretPosition: Int, + ): EditorUpdateData { + TODO("Not yet implemented") + } + + override suspend fun updateCodeCompletionActions( + editorId: Int, + cellId: CellInstanceId, + pattern: String, + ): EditorUpdateData { + TODO("Not yet implemented") + } + + override suspend fun hasCodeCompletionActions( + editorId: Int, + cellId: CellInstanceId, + pattern: String, + ): Boolean { + TODO("Not yet implemented") + } + + override suspend fun executeCodeCompletionAction( + editorId: Int, + actionId: Int, + ): EditorUpdateData { + TODO("Not yet implemented") + } + + override suspend fun replaceText( + editorId: Int, + cellId: CellInstanceId, + range: IntRange, + replacement: String, + triggerCompletion: Boolean, + ): ServiceCallResult { + TODO("Not yet implemented") + } + + override suspend fun resetState(editorId: Int): EditorUpdateData { + TODO("Not yet implemented") + } + + override suspend fun flush(editorId: Int): EditorUpdateData { + TODO("Not yet implemented") + } +} + +@Serializable +data class ServiceCallResult( + val result: E, + val updateData: EditorUpdateData? = null, +) + +@Serializable +data class EditorUpdateData( + val cellTreeChanges: List = emptyList(), + val selectionChange: ICaretPositionPolicy? = null, + val completionMenuTrigger: CompletionMenuTrigger? = null, + val completionEntries: List? = null, +) + +@Serializable +data class CompletionMenuTrigger( + val anchor: CellInstanceId, + val completionPosition: CompletionPosition = CompletionPosition.CENTER, + val pattern: String = "", + val caretPosition: Int = 0, +) + +@Serializable +data class CompletionMenuEntryData( + val id: Int, + val matchingText: String, + val description: String, +) { + fun matches(pattern: String): Boolean { + // TODO more sophisticated pattern matching + return matchingText.contains(pattern) + } + + fun matchesExactly(pattern: String): Boolean = matchingText == pattern +} diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/text/shared/celltree/BackendCellTree.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/text/shared/celltree/BackendCellTree.kt new file mode 100644 index 00000000..6ec74b9b --- /dev/null +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/text/shared/celltree/BackendCellTree.kt @@ -0,0 +1,117 @@ +package org.modelix.editor.text.shared.celltree + +import org.modelix.editor.CellPropertyKey +import org.modelix.editor.CellReference +import org.modelix.editor.CommonCellProperties + +class BackendCellTree : CellTreeBase() { + private var operations: MutableList = ArrayList() + private var updating = false + + override fun getRoot() = super.getRoot() as BackendCellImpl + + override fun getCell(id: CellInstanceId): BackendCellImpl = super.getCell(id) as BackendCellImpl + + override fun createCell(id: CellInstanceId): IMutableCellTree.MutableCell = + withTreeLock { + super.createCell(id).also { + operations += NewCellOp(it.getId()) + } + } + + override fun newCellInstance( + id: CellInstanceId, + parent: CellImpl?, + ): CellImpl = BackendCellImpl(id, parent as BackendCellImpl?) + + fun runUpdate(body: () -> Unit): List { + return withTreeLock { + check(!updating) { "Already updating" } + updating = true + try { + body() + deleteDetachedCells() + return@withTreeLock getPendingChanges() + } finally { + updating = false + } + } + } + + private fun getPendingChanges(): List = withTreeLock { operations.also { operations = ArrayList() } } + + inner class BackendCellImpl( + id: CellInstanceId, + parent: BackendCellImpl? = null, + ) : CellTreeBase.CellImpl(id, parent) { + override fun setProperty( + key: CellPropertyKey, + newValue: T, + ) { + withTreeLock { + if (getProperty(key) == newValue) return@withTreeLock + super.setProperty(key, newValue) + if (key.frontend) { + operations += CellPropertyChangeOp(getId(), key.name, key.toSerializableValue(newValue)) + } + } + } + + override fun removeProperty(key: CellPropertyKey<*>) { + withTreeLock { + if (!hasProperty(key)) return@withTreeLock + super.removeProperty(key) + if (key.frontend) { + operations += CellPropertyRemoveOp(getId(), key.name) + } + } + } + + override fun getParent(): BackendCellImpl? = super.getParent() as BackendCellImpl? + + override fun getChildren(): List = super.getChildren() as List + + override fun getChildAt(index: Int): BackendCellImpl? = super.getChildAt(index) as BackendCellImpl? + + override fun addNewChild(index: Int): IMutableCellTree.MutableCell = + withTreeLock { + val newChild = super.addNewChild(index) + operations += NewChildCellOp(getId(), index, newChild.getId()) + newChild + } + + override fun moveCell(index: Int) { + withTreeLock { + super.moveCell(index) + operations += MoveCellOp(index, getId()) + } + } + + override fun moveCell( + targetParent: IMutableCellTree.MutableCell, + index: Int, + ) { + withTreeLock { + targetParent as BackendCellImpl + super.moveCell(targetParent, index) + operations += MoveCellToOp(targetParent.getId(), index, getId()) + } + } + + override fun detach() { + withTreeLock { + super.detach() + operations += CellDetachOp(getId()) + } + } + + override fun delete() { + withTreeLock { + super.delete() + operations += CellDeleteOp(getId()) + } + } + } +} + +val ICellTree.Cell.cellReferences: List get() = getProperty(CommonCellProperties.cellReferences) diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/text/shared/celltree/CellPropertyValue.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/text/shared/celltree/CellPropertyValue.kt new file mode 100644 index 00000000..8b9f6f13 --- /dev/null +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/text/shared/celltree/CellPropertyValue.kt @@ -0,0 +1,24 @@ +package org.modelix.editor.text.shared.celltree + +import kotlinx.serialization.Serializable +import org.modelix.editor.CellReference + +@Serializable +sealed class CellPropertyValue { + abstract val value: E +} + +@Serializable +data class BooleanCellPropertyValue( + override val value: Boolean, +) : CellPropertyValue() + +@Serializable +data class StringCellPropertyValue( + override val value: String, +) : CellPropertyValue() + +@Serializable +data class CellReferenceListValue( + override val value: List, +) : CellPropertyValue>() diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/text/shared/celltree/CellTreeBase.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/text/shared/celltree/CellTreeBase.kt new file mode 100644 index 00000000..cf9b5bfb --- /dev/null +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/text/shared/celltree/CellTreeBase.kt @@ -0,0 +1,209 @@ +package org.modelix.editor.text.shared.celltree + +import org.modelix.editor.Cell +import org.modelix.editor.CellPropertyKey +import org.modelix.editor.CellReference +import org.modelix.editor.CommonCellProperties +import org.modelix.editor.ResettableLazy +import org.modelix.incremental.IncrementalIndex +import org.modelix.incremental.IncrementalList +import org.modelix.model.api.runSynchronized +import kotlin.getValue +import kotlin.jvm.Synchronized + +open class CellTreeBase : IMutableCellTree { + private val root = newCellInstance(CellInstanceId(1L), null) + private var nextId: Long = 2L + private val allCells = HashMap() + private var detachedCells = HashSet() + private val cellIndex: IncrementalIndex = IncrementalIndex() + protected val treeLock = Any() + + init { + allCells[root.getId()] = root + } + + fun withTreeLock(body: () -> R): R = runSynchronized(treeLock, body) + + override fun getRoot(): CellImpl = root + + override fun getCell(id: CellInstanceId): CellImpl = withTreeLock { allCells[id] ?: throw NoSuchElementException("Cell ID: ${id.id}") } + + final override fun createCell(): IMutableCellTree.MutableCell = createCell(CellInstanceId(nextId++)) + + override fun createCell(id: CellInstanceId): IMutableCellTree.MutableCell = + withTreeLock { + require(!allCells.containsKey(id)) { "Cell already exists: $id" } + newCellInstance(id, null).also { + registerCell(it) + detachedCells.add(id) + } + } + + fun resolveCell(reference: CellReference): List = + withTreeLock { + cellIndex.update(getRoot().referencesIndexList) + cellIndex.lookup(reference).map { getCell(it) } + } + + fun deleteDetachedCells() { + withTreeLock { + val cells = detachedCells + detachedCells = HashSet() + cells.forEach { getCell(it).delete() } + } + } + + private fun registerCell(cell: CellImpl) { + withTreeLock { + allCells[cell.getId()] = cell + } + } + + protected open fun newCellInstance( + id: CellInstanceId, + parent: CellImpl? = null, + ) = CellImpl(id, parent) + + open inner class CellImpl( + private val id: CellInstanceId, + private var parent: CellImpl? = null, + ) : IMutableCellTree.MutableCell { + protected val properties: MutableMap = HashMap() + private val children: MutableList = ArrayList() + + private val cachedReferencesIndexList = + ResettableLazy { + withTreeLock { + IncrementalList.concat( + IncrementalList.of(this.cellReferences.map { it to id }), + IncrementalList.concat(getChildren().map { (it as CellImpl).referencesIndexList }), + ) + } + } + val referencesIndexList: IncrementalList> by cachedReferencesIndexList + + override fun getTree(): IMutableCellTree = this@CellTreeBase + + override fun getId(): CellInstanceId = id + + override fun getParent(): IMutableCellTree.MutableCell? = withTreeLock { parent } + + override fun isAttached(): Boolean = withTreeLock { this == root || parent?.isAttached() == true } + + override fun getProperty(key: CellPropertyKey): T = + withTreeLock { + if (properties.containsKey(key.name)) properties[key.name] as T else key.defaultValue + } + + fun setProperty( + key: String, + newValue: CellPropertyValue<*>?, + ) { + withTreeLock { + properties[key] = newValue?.value + } + } + + @Synchronized + override fun setProperty( + key: CellPropertyKey, + newValue: T, + ) { + withTreeLock { + require(newValue !is CellPropertyKey<*>) + properties[key.name] = newValue + if (key == CommonCellProperties.cellReferences) cachedReferencesIndexList.reset() + } + } + + fun removeProperty(key: String) { + withTreeLock { + properties.remove(key) + } + } + + override fun removeProperty(key: CellPropertyKey<*>) { + withTreeLock { + properties.remove(key.name) + if (key == CommonCellProperties.cellReferences) cachedReferencesIndexList.reset() + } + } + + override fun hasProperty(key: CellPropertyKey<*>): Boolean = withTreeLock { properties.containsKey(key.name) } + + override fun getChildren(): List = withTreeLock { children } + + override fun getChildAt(index: Int): IMutableCellTree.MutableCell? = withTreeLock { children.getOrNull(index) } + + fun addNewChild( + index: Int, + childId: CellInstanceId, + ): IMutableCellTree.MutableCell = + withTreeLock { + newCellInstance(childId, this).also { + children.add(index, it) + registerCell(it) + } + } + + override fun addNewChild(index: Int): IMutableCellTree.MutableCell = + withTreeLock { + cachedReferencesIndexList.reset() + addNewChild(index, CellInstanceId(nextId++)) + } + + override fun moveCell(index: Int) { + withTreeLock { + val parent = requireNotNull(parent) + parent.children.remove(this) + parent.children.add(index, this) + } + } + + override fun moveCell( + targetParent: IMutableCellTree.MutableCell, + index: Int, + ) { + withTreeLock { + targetParent as CellImpl + require(targetParent != parent) { "Use moveCell(index: Int)" } + parent?.cachedReferencesIndexList?.reset() + targetParent.cachedReferencesIndexList.reset() + val oldParent = parent + oldParent?.children?.remove(this) + targetParent.children.add(index, this) + parent = targetParent + detachedCells.remove(id) + } + } + + override fun detach() { + withTreeLock { + parent?.cachedReferencesIndexList?.reset() + detachedCells.add(id) + parent?.children?.remove(this) + parent = null + } + } + + override fun delete() { + withTreeLock { + parent?.cachedReferencesIndexList?.reset() + children.toList().forEach { it.delete() } + parent?.children?.remove(this) + parent = null + allCells.remove(id) + detachedCells.remove(id) + } + } + + override fun index(): Int { + val parent = parent ?: return 0 + val index = parent.children.indexOf(this) + return if (index >= 0) index else 0 + } + + override fun toString(): String = id.id.toString() + } +} diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/text/shared/celltree/CellTreeOp.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/text/shared/celltree/CellTreeOp.kt new file mode 100644 index 00000000..de2f242d --- /dev/null +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/text/shared/celltree/CellTreeOp.kt @@ -0,0 +1,54 @@ +package org.modelix.editor.text.shared.celltree + +import kotlinx.serialization.Serializable + +@Serializable +sealed class CellTreeOp + +@Serializable +data class CellPropertyChangeOp( + val id: CellInstanceId, + val key: String, + val value: CellPropertyValue<*>?, +) : CellTreeOp() + +@Serializable +data class CellPropertyRemoveOp( + val id: CellInstanceId, + val key: String, +) : CellTreeOp() + +@Serializable +data class NewChildCellOp( + val parentId: CellInstanceId, + val index: Int, + val childId: CellInstanceId, +) : CellTreeOp() + +@Serializable +data class NewCellOp( + val id: CellInstanceId, +) : CellTreeOp() + +@Serializable +data class MoveCellOp( + val index: Int, + val childId: CellInstanceId, +) : CellTreeOp() + +@Serializable +data class MoveCellToOp( + val targetParent: CellInstanceId, + val index: Int, + val childId: CellInstanceId, +) : CellTreeOp() + +@Serializable +data class CellDeleteOp( + val id: CellInstanceId, +) : CellTreeOp() + +@Serializable +data class CellDetachOp( + val id: CellInstanceId, +) : CellTreeOp() diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/text/shared/celltree/ICellTree.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/text/shared/celltree/ICellTree.kt new file mode 100644 index 00000000..3a41f450 --- /dev/null +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/text/shared/celltree/ICellTree.kt @@ -0,0 +1,79 @@ +package org.modelix.editor.text.shared.celltree + +import kotlinx.serialization.Serializable +import org.modelix.editor.CellPropertyKey +import kotlin.jvm.JvmInline + +@Serializable +@JvmInline +value class CellInstanceId( + val id: Long, +) + +interface ICellTree { + fun getRoot(): Cell + + fun getCell(id: CellInstanceId): Cell? + + interface Cell { + fun getTree(): ICellTree + + fun getId(): CellInstanceId + + fun getProperty(key: CellPropertyKey): T + + fun hasProperty(key: CellPropertyKey<*>): Boolean + + fun getChildren(): List + + fun getChildAt(index: Int): Cell? + + fun getParent(): Cell? + + fun isAttached(): Boolean + + fun index(): Int + } +} + +interface IMutableCellTree : ICellTree { + override fun getRoot(): MutableCell + + override fun getCell(id: CellInstanceId): MutableCell? + + fun createCell(): MutableCell + + fun createCell(id: CellInstanceId): MutableCell + + interface MutableCell : ICellTree.Cell { + override fun getTree(): IMutableCellTree + + override fun getParent(): MutableCell? + + fun setProperty( + key: CellPropertyKey, + newValue: T, + ) + + fun removeProperty(key: CellPropertyKey<*>) + + override fun getChildren(): List + + override fun getChildAt(index: Int): MutableCell? + + fun addNewChild(index: Int): MutableCell + + fun addNewChild(): MutableCell = addNewChild(getChildren().size) + + fun moveCell(index: Int) + + fun moveCell( + targetParent: MutableCell, + index: Int, + ) + + fun detach() + + fun delete() + } +} diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/scopes/DefaultScope.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/scopes/DefaultScope.kt index 039a55e2..fb3a445b 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/scopes/DefaultScope.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/scopes/DefaultScope.kt @@ -8,7 +8,10 @@ import org.modelix.model.api.getRoot import org.modelix.model.api.isInstanceOfSafe class DefaultScope : IScope { - override fun getVisibleElements(node: INonExistingNode, link: IReferenceLink): List { + override fun getVisibleElements( + node: INonExistingNode, + link: IReferenceLink, + ): List { // TODO performance val targetConcept = link.targetConcept return (node.getExistingAncestor() ?: return emptyList()) diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/scopes/EmptyScope.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/scopes/EmptyScope.kt index 759bc8de..6f667e44 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/scopes/EmptyScope.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/scopes/EmptyScope.kt @@ -4,7 +4,8 @@ import org.modelix.editor.INonExistingNode import org.modelix.model.api.IReferenceLink class EmptyScope : IScope { - override fun getVisibleElements(node: INonExistingNode, link: IReferenceLink): List { - return emptyList() - } + override fun getVisibleElements( + node: INonExistingNode, + link: IReferenceLink, + ): List = emptyList() } diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/scopes/IScope.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/scopes/IScope.kt index 27153ea4..6938c87f 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/scopes/IScope.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/scopes/IScope.kt @@ -4,5 +4,8 @@ import org.modelix.editor.INonExistingNode import org.modelix.model.api.IReferenceLink interface IScope { - fun getVisibleElements(node: INonExistingNode, link: IReferenceLink): List + fun getVisibleElements( + node: INonExistingNode, + link: IReferenceLink, + ): List } diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/scopes/ScopeAspect.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/scopes/ScopeAspect.kt index a8d9a26b..6b311d0c 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/scopes/ScopeAspect.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/scopes/ScopeAspect.kt @@ -11,7 +11,9 @@ import org.modelix.metamodel.ITypedReferenceLink import org.modelix.model.api.ILanguage import org.modelix.model.api.IReferenceLink -class ScopeAspect(val language: ILanguage) : ILanguageAspect { +class ScopeAspect( + val language: ILanguage, +) : ILanguageAspect { private val scopes: MutableMap = HashMap() fun getScope(link: IReferenceLink): IScope? { @@ -19,7 +21,10 @@ class ScopeAspect(val language: ILanguage) : ILanguageAspect { return scopes[link] } - fun registerScope(link: IReferenceLink, scope: IScope) { + fun registerScope( + link: IReferenceLink, + scope: IScope, + ) { require(link.getConcept().language == language) { "$link doesn't belong to $language" } scopes[link] = scope } @@ -27,15 +32,15 @@ class ScopeAspect(val language: ILanguage) : ILanguageAspect { companion object : ILanguageAspectFactory { private val scopeProviders = HashSet() - override fun createInstance(language: ILanguage): ScopeAspect { - return ScopeAspect(language) - } + override fun createInstance(language: ILanguage): ScopeAspect = ScopeAspect(language) - fun getScope(sourceNode: INonExistingNode, link: IReferenceLink): IScope { - return scopeProviders.asSequence().mapNotNull { it.getScope(sourceNode, link) }.firstOrNull() + fun getScope( + sourceNode: INonExistingNode, + link: IReferenceLink, + ): IScope = + scopeProviders.asSequence().mapNotNull { it.getScope(sourceNode, link) }.firstOrNull() ?: ScopeAspect.getInstance(link.getConcept().language!!).getScope(link) ?: EmptyScope() - } fun registerScopeProvider(provider: IScopeProvider) { scopeProviders.add(provider) @@ -48,19 +53,27 @@ class ScopeAspect(val language: ILanguage) : ILanguageAspect { } interface IScopeProvider { - fun getScope(sourceNode: INonExistingNode, link: IReferenceLink): IScope? + fun getScope( + sourceNode: INonExistingNode, + link: IReferenceLink, + ): IScope? } -fun LanguageAspectsBuilder<*>.scope(link: ITypedReferenceLink<*>, scope: IScope) { - return aspects.getAspect(language, ScopeAspect).registerScope(link.untyped(), scope) -} +fun LanguageAspectsBuilder<*>.scope( + link: ITypedReferenceLink<*>, + scope: IScope, +) = aspects.getAspect(language, ScopeAspect).registerScope(link.untyped(), scope) -fun LanguageAspectsBuilder<*>.scope(link: ITypedReferenceLink, scopeFunction: (INonExistingNode) -> List) { - return scope(link, ScopeFunction(scopeFunction)) -} +fun LanguageAspectsBuilder<*>.scope( + link: ITypedReferenceLink, + scopeFunction: (INonExistingNode) -> List, +) = scope(link, ScopeFunction(scopeFunction)) -class ScopeFunction(val function: (INonExistingNode) -> List) : IScope { - override fun getVisibleElements(node: INonExistingNode, link: IReferenceLink): List { - return function(node).map { it.unwrap().toNonExisting() } - } +class ScopeFunction( + val function: (INonExistingNode) -> List, +) : IScope { + override fun getVisibleElements( + node: INonExistingNode, + link: IReferenceLink, + ): List = function(node).map { it.unwrap().toNonExisting() } } diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/typesystem/TypesystemAspect.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/typesystem/TypesystemAspect.kt index c55bafa4..535e3201 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/typesystem/TypesystemAspect.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/typesystem/TypesystemAspect.kt @@ -20,18 +20,22 @@ import org.modelix.model.api.getRoot class TypesystemAspect : ILanguageAspect { private val constraintBuilders: MutableMap = HashMap() - fun registerConstraintsBuilder(concept: IConcept, builder: ITypesystemConstraintsBuilderFactory) { + fun registerConstraintsBuilder( + concept: IConcept, + builder: ITypesystemConstraintsBuilderFactory, + ) { TypesystemEngine.registerConstraintsBuilder(concept, builder) } companion object : ILanguageAspectFactory { - override fun createInstance(language: ILanguage): TypesystemAspect { - return TypesystemAspect() - } + override fun createInstance(language: ILanguage): TypesystemAspect = TypesystemAspect() } } -fun > LanguageAspectsBuilder<*>.typesystem(concept: ConceptT, body: TypesystemConstraintsBuilder.() -> Unit) { +fun > LanguageAspectsBuilder<*>.typesystem( + concept: ConceptT, + body: TypesystemConstraintsBuilder.() -> Unit, +) { aspects.getAspect(language, TypesystemAspect).registerConstraintsBuilder( concept.untyped(), object : ITypesystemConstraintsBuilderFactory { @@ -48,44 +52,73 @@ interface ITypesystemConstraintsBuilderFactory { fun buildConstraints(node: INode): List } -class TypesystemConstraintsBuilder(val node: NodeT) { +class TypesystemConstraintsBuilder( + val node: NodeT, +) { val constraints: MutableList = ArrayList() fun typeofNode(node: INode): IVariableReference = TypeofNode(node) + fun typeofNode(node: ITypedNode): IVariableReference = typeofNode(node.untyped()) - fun equalType(operand1: IOperand, operand2: IOperand) { + fun equalType( + operand1: IOperand, + operand2: IOperand, + ) { constraints += EqualType(operand1, operand2) } - fun equalType(operand1: IOperand, operand2: ITypesystemType) = equalType(operand1, KnownValue(operand2)) - fun equalType(operand1: ITypesystemType, operand2: ITypesystemType) = equalType(KnownValue(operand1), KnownValue(operand2)) - fun equalType(operand1: ITypesystemType, operand2: IOperand) = equalType(KnownValue(operand1), operand2) + + fun equalType( + operand1: IOperand, + operand2: ITypesystemType, + ) = equalType(operand1, KnownValue(operand2)) + + fun equalType( + operand1: ITypesystemType, + operand2: ITypesystemType, + ) = equalType(KnownValue(operand1), KnownValue(operand2)) + + fun equalType( + operand1: ITypesystemType, + operand2: IOperand, + ) = equalType(KnownValue(operand1), operand2) infix fun IOperand.equalTo(other: IOperand) = equalType(this, other) + infix fun IOperand.equalTo(other: ITypesystemType) = equalType(this, KnownValue(other)) + infix fun ITypesystemType.equalTo(other: IOperand) = equalType(KnownValue(this), other) + infix fun ITypesystemType.equalTo(other: ITypesystemType) = equalType(KnownValue(this), KnownValue(other)) infix fun IOperand.subtypeOf(superType: IOperand) { constraints += Subtype(this, superType) } + infix fun IOperand.subtypeOf(superType: ITypesystemType) = subtypeOf(KnownValue(superType)) + infix fun ITypesystemType.subtypeOf(superType: IOperand) = KnownValue(this).subtypeOf(superType) + infix fun ITypesystemType.subtypeOf(superType: ITypesystemType) = KnownValue(this).subtypeOf(KnownValue(superType)) } // TODO make a class and provide IncrementalEngine to constructor object TypesystemEngine { private val incrementalEngine: IncrementalEngine = IncrementalEngine(100_000) - private val getConstraintsFromSubtree: (INode) -> IncrementalList = incrementalEngine.incrementalFunction("getConstraintsFromSubtree") { context, node: INode -> - return@incrementalFunction doGetConstraintsFromSubtree(node) - } - private val getConstraintsFromNode: (INode) -> IncrementalList = incrementalEngine.incrementalFunction("getConstraintsFromSubtree") { context, node: INode -> - return@incrementalFunction doGetConstraintsFromNode(node) - } + private val getConstraintsFromSubtree: (INode) -> IncrementalList = + incrementalEngine.incrementalFunction("getConstraintsFromSubtree") { context, node: INode -> + return@incrementalFunction doGetConstraintsFromSubtree(node) + } + private val getConstraintsFromNode: (INode) -> IncrementalList = + incrementalEngine.incrementalFunction("getConstraintsFromSubtree") { context, node: INode -> + return@incrementalFunction doGetConstraintsFromNode(node) + } private val constraintBuilders: MutableMap = HashMap() - fun registerConstraintsBuilder(concept: IConcept, builder: ITypesystemConstraintsBuilderFactory) { + fun registerConstraintsBuilder( + concept: IConcept, + builder: ITypesystemConstraintsBuilderFactory, + ) { constraintBuilders[concept.getReference()] = builder } @@ -104,36 +137,35 @@ object TypesystemEngine { fun solve(constraintProviderRoots: Sequence) = solve(constraintProviderRoots.flatMap { getConstraintsFromSubtree(it).asSequence() }.toList()) - private fun doGetConstraintsFromSubtree(node: INode): IncrementalList { - return IncrementalList.concat( + private fun doGetConstraintsFromSubtree(node: INode): IncrementalList = + IncrementalList.concat( listOf(getConstraintsFromNode(node)) + node.allChildren.map { getConstraintsFromSubtree(it) }, ) - } private fun doGetConstraintsFromNode(node: INode): IncrementalList { val concept = node.tryGetConcept() ?: return IncrementalList.empty() - val constraints = concept.getAllConcepts().flatMap { superConcept -> - constraintBuilders[superConcept.getReference()]?.buildConstraints(node) ?: emptyList() - } + val constraints = + concept.getAllConcepts().flatMap { superConcept -> + constraintBuilders[superConcept.getReference()]?.buildConstraints(node) ?: emptyList() + } return IncrementalList.of(constraints) } - fun computeType(node: INode): ITypesystemType? { - return solve(sequenceOf(node.getRoot()))[node.asTypeVariable()] - } + fun computeType(node: INode): ITypesystemType? = solve(sequenceOf(node.getRoot()))[node.asTypeVariable()] } fun INode.rawType(): ITypesystemType? = TypesystemEngine.computeType(this) + fun ITypedNode.rawType(): ITypesystemType? = untyped().rawType() + fun INode.type(): INode? = (rawType() as? NodeAsType)?.node + fun ITypedNode.type(): ITypedNode? = untyped().type()?.typed() class TypesystemSolver { private val variables: MutableMap = HashMap() - fun getTypes(): Map { - return variables.mapValues { it.value.getValue()?.getValue() } - } + fun getTypes(): Map = variables.mapValues { it.value.getValue()?.getValue() } fun solve(constraints: List) { for (constraint in constraints) { @@ -180,21 +212,26 @@ class TypesystemSolver { } } - fun getVariable(ref: IVariableReference): Variable { - return variables.getOrPut(ref) { Variable(ref) } - } + fun getVariable(ref: IVariableReference): Variable = variables.getOrPut(ref) { Variable(ref) } } interface ITypesystemType fun ITypedNode.asType(): ITypesystemType = NodeAsType(this.untyped()) + fun INode.asType(): ITypesystemType = NodeAsType(this) + fun ITypedNode.asTypeVariable(): IVariableReference = TypeofNode(this.untyped()) + fun INode.asTypeVariable(): IVariableReference = TypeofNode(this) -data class NodeAsType(val node: INode) : ITypesystemType +data class NodeAsType( + val node: INode, +) : ITypesystemType -class Variable(val ref: IVariableReference) { +class Variable( + val ref: IVariableReference, +) { private var value: VariableValue? = null fun getValue(): VariableValue? = value @@ -212,12 +249,20 @@ sealed class VariableValue { abstract fun getValue(): ITypesystemType? } -data class ExactValue(private val value: ITypesystemType) : VariableValue() { + +data class ExactValue( + private val value: ITypesystemType, +) : VariableValue() { override fun getValue(): ITypesystemType = value } -data class IndirectValue(val variable: Variable) : VariableValue() { + +data class IndirectValue( + val variable: Variable, +) : VariableValue() { fun getDeepestVariable(): Variable = (variable.getValue() as? IndirectValue)?.getDeepestVariable() ?: variable + override fun getValue(): ITypesystemType? = variable.getValue()?.getValue() + override fun combine(other: VariableValue): VariableValue { if (other is IndirectValue && other.getDeepestVariable() == getDeepestVariable()) { return IndirectValue(getDeepestVariable()) @@ -231,23 +276,37 @@ data class IndirectValue(val variable: Variable) : VariableValue() { } sealed interface IOperand + interface IVariableReference : IOperand -data class TypeofNode(val node: INode) : IVariableReference -data class NamedVariable(val name: String) : IVariableReference -class UnnamedVariable() : IVariableReference -data class KnownValue(val value: ITypesystemType) : IOperand +data class TypeofNode( + val node: INode, +) : IVariableReference + +data class NamedVariable( + val name: String, +) : IVariableReference + +class UnnamedVariable : IVariableReference + +data class KnownValue( + val value: ITypesystemType, +) : IOperand sealed class Constraint { abstract fun getVariables(): Sequence } -class EqualType(val type1: IOperand, val type2: IOperand) : Constraint() { - override fun getVariables(): Sequence { - return sequenceOf(type1, type2).filterIsInstance() - } + +class EqualType( + val type1: IOperand, + val type2: IOperand, +) : Constraint() { + override fun getVariables(): Sequence = sequenceOf(type1, type2).filterIsInstance() } -class Subtype(val subtype: IOperand, val supertype: IOperand) : Constraint() { - override fun getVariables(): Sequence { - return sequenceOf(subtype, supertype).filterIsInstance() - } + +class Subtype( + val subtype: IOperand, + val supertype: IOperand, +) : Constraint() { + override fun getVariables(): Sequence = sequenceOf(subtype, supertype).filterIsInstance() } diff --git a/projectional-editor/src/commonTest/kotlin/org/modelix/editor/CellNavigationTest.kt b/projectional-editor/src/commonTest/kotlin/org/modelix/editor/CellNavigationTest.kt index ccbaab04..9536ddca 100644 --- a/projectional-editor/src/commonTest/kotlin/org/modelix/editor/CellNavigationTest.kt +++ b/projectional-editor/src/commonTest/kotlin/org/modelix/editor/CellNavigationTest.kt @@ -1,31 +1,37 @@ package org.modelix.editor +import org.modelix.editor.text.frontend.text +import org.modelix.editor.text.shared.celltree.BackendCellTree +import org.modelix.editor.text.shared.celltree.IMutableCellTree import kotlin.test.Test import kotlin.test.assertEquals class CellNavigationTest { - private val rootCell = cell("root") { - cell("1") { - cell("11") { - cell("111") - cell("112") - } - cell("12") { - cell("121") - cell("122") - } - } - cell("2") { - cell("21") { - cell("211") - cell("212") - } - cell("22") { - cell("221") - cell("222") + private val rootCell = + BackendCellTree().run { + cell("root") { + cell("1") { + cell("11") { + cell("111") + cell("112") + } + cell("12") { + cell("121") + cell("122") + } + } + cell("2") { + cell("21") { + cell("211") + cell("212") + } + cell("22") { + cell("221") + cell("222") + } + } } } - } @Test fun order_of_previousCells() { @@ -46,12 +52,17 @@ class CellNavigationTest { "111", "root", ), - rootCell.lastLeaf().previousCells().map { (it.data as TextCellData).text }.toList(), + rootCell + .lastLeaf() + .previousCells() + .map { it.text } + .toList(), ) } @Test fun order_of_nextCells() { + assertEquals("111", rootCell.firstLeaf().text) assertEquals( listOf( "112", @@ -69,15 +80,27 @@ class CellNavigationTest { "222", "root", ), - rootCell.firstLeaf().nextCells().map { (it.data as TextCellData).text }.toList(), + rootCell + .firstLeaf() + .nextCells() + .map { it.text } + .toList(), ) } - private fun cell(text: String, body: Cell.() -> Unit): Cell { - return Cell(TextCellData(text)).also(body) - } + private fun IMutableCellTree.cell( + text: String, + body: IMutableCellTree.MutableCell.() -> Unit, + ): Cell = + this + .createCell() + .also { + it.setProperty(TextCellProperties.text, text) + }.also(body) - private fun Cell.cell(text: String, body: Cell.() -> Unit = {}): Cell { - return Cell(TextCellData(text)).also { addChild(it) }.also(body) - } + private fun IMutableCellTree.MutableCell.cell( + text: String, + body: IMutableCellTree.MutableCell.() -> Unit = { + }, + ): Cell = this.addNewChild().also { it.setProperty(TextCellProperties.text, text) }.also(body) } diff --git a/projectional-editor/src/commonTest/kotlin/org/modelix/editor/EditorKeyboardTest.kt b/projectional-editor/src/commonTest/kotlin/org/modelix/editor/EditorKeyboardTest.kt index d176b35e..439c7a67 100644 --- a/projectional-editor/src/commonTest/kotlin/org/modelix/editor/EditorKeyboardTest.kt +++ b/projectional-editor/src/commonTest/kotlin/org/modelix/editor/EditorKeyboardTest.kt @@ -1,67 +1,110 @@ package org.modelix.editor +import kotlinx.coroutines.test.runTest +import org.modelix.editor.text.frontend.getVisibleText +import org.modelix.editor.text.shared.NullTextEditorService import kotlin.test.Test import kotlin.test.assertEquals class EditorKeyboardTest { @Test - fun arrowLeft() { - val rootCell = EditorTestUtils.buildCells(listOf(listOf("111"), listOf(EditorTestUtils.indentChildren, "222", listOf(EditorTestUtils.newLine, listOf("333")), listOf(listOf("444"), "555")), EditorTestUtils.newLine, "666", "777", "888")) - val editor = EditorComponent(engine = null) { rootCell } - val findByText: (String) -> LayoutableCell = { text -> rootCell.descendants().find { it.getVisibleText() == text }!!.layoutable()!! } - val layoutable444 = findByText("444") - editor.changeSelection(CaretSelection(layoutable444, 2)) - assertEquals(CaretSelection(layoutable444, 2), editor.getSelection()) + fun arrowLeft() = + runTest { + val editor = FrontendEditorComponent(NullTextEditorService()) + val rootCell = + EditorTestUtils.buildCells( + listOf( + listOf("111"), + listOf( + EditorTestUtils.indentChildren, + "222", + listOf(EditorTestUtils.newLine, listOf("333")), + listOf(listOf("444"), "555") + ), + EditorTestUtils.newLine, + "666", + "777", + "888" + ), + editor.cellTree + ) + rootCell.moveCell(editor.cellTree.getRoot(), 0) + val findByText: (String) -> LayoutableCell = { text -> + val cell = rootCell.descendants().find { it.getVisibleText() == text }!! + cell.layoutable()!! + } + val layoutable444 = findByText("444") + editor.changeSelection(CaretSelection(editor, layoutable444, 2)) + assertEquals(CaretSelection(editor, layoutable444, 2), editor.getSelection()) - testCaretChange(editor, KnownKeys.ArrowLeft, "444", 1) - testCaretChange(editor, KnownKeys.ArrowLeft, "444", 0) - testCaretChange(editor, KnownKeys.ArrowLeft, "333", 3) - testCaretChange(editor, KnownKeys.ArrowLeft, "333", 2) - testCaretChange(editor, KnownKeys.ArrowLeft, "333", 1) - testCaretChange(editor, KnownKeys.ArrowLeft, "333", 0) - testCaretChange(editor, KnownKeys.ArrowLeft, "222", 3) - testCaretChange(editor, KnownKeys.ArrowLeft, "222", 2) - testCaretChange(editor, KnownKeys.ArrowLeft, "222", 1) - testCaretChange(editor, KnownKeys.ArrowLeft, "222", 0) - testCaretChange(editor, KnownKeys.ArrowLeft, "111", 3) - testCaretChange(editor, KnownKeys.ArrowLeft, "111", 2) - testCaretChange(editor, KnownKeys.ArrowLeft, "111", 1) - testCaretChange(editor, KnownKeys.ArrowLeft, "111", 0) - testCaretChange(editor, KnownKeys.ArrowLeft, "111", 0) // don't move at the beginning - } + testCaretChange(editor, KnownKeys.ArrowLeft, "444", 1) + testCaretChange(editor, KnownKeys.ArrowLeft, "444", 0) + testCaretChange(editor, KnownKeys.ArrowLeft, "333", 3) + testCaretChange(editor, KnownKeys.ArrowLeft, "333", 2) + testCaretChange(editor, KnownKeys.ArrowLeft, "333", 1) + testCaretChange(editor, KnownKeys.ArrowLeft, "333", 0) + testCaretChange(editor, KnownKeys.ArrowLeft, "222", 3) + testCaretChange(editor, KnownKeys.ArrowLeft, "222", 2) + testCaretChange(editor, KnownKeys.ArrowLeft, "222", 1) + testCaretChange(editor, KnownKeys.ArrowLeft, "222", 0) + testCaretChange(editor, KnownKeys.ArrowLeft, "111", 3) + testCaretChange(editor, KnownKeys.ArrowLeft, "111", 2) + testCaretChange(editor, KnownKeys.ArrowLeft, "111", 1) + testCaretChange(editor, KnownKeys.ArrowLeft, "111", 0) + testCaretChange(editor, KnownKeys.ArrowLeft, "111", 0) // don't move at the beginning + } @Test - fun arrowRight() { - val rootCell = EditorTestUtils.buildCells(listOf("111", "222", EditorTestUtils.newLine, "333", "444", "555", EditorTestUtils.newLine, "666", "777", "888")) - val editor = EditorComponent(engine = null) { rootCell } - val findByText: (String) -> LayoutableCell = { text -> rootCell.descendants().find { it.getVisibleText() == text }!!.layoutable()!! } - val layoutable444 = findByText("444") - editor.changeSelection(CaretSelection(layoutable444, 2)) - assertEquals(CaretSelection(layoutable444, 2), editor.getSelection()) + fun arrowRight() = + runTest { + val editor = FrontendEditorComponent(NullTextEditorService()) + val rootCell = + EditorTestUtils.buildCells( + listOf("111", "222", EditorTestUtils.newLine, "333", "444", "555", EditorTestUtils.newLine, "666", "777", "888"), + editor.cellTree + ) + rootCell.moveCell(editor.cellTree.getRoot(), 0) + val findByText: (String) -> LayoutableCell = { text -> + val cell = rootCell.descendants().find { it.getVisibleText() == text }!! + cell.layoutable()!! + } + val layoutable444 = findByText("444") + editor.changeSelection(CaretSelection(editor, layoutable444, 2)) + assertEquals(CaretSelection(editor, layoutable444, 2), editor.getSelection()) - testCaretChange(editor, KnownKeys.ArrowRight, "444", 3) - testCaretChange(editor, KnownKeys.ArrowRight, "555", 0) - testCaretChange(editor, KnownKeys.ArrowRight, "555", 1) - testCaretChange(editor, KnownKeys.ArrowRight, "555", 2) - testCaretChange(editor, KnownKeys.ArrowRight, "555", 3) - testCaretChange(editor, KnownKeys.ArrowRight, "666", 0) - testCaretChange(editor, KnownKeys.ArrowRight, "666", 1) - testCaretChange(editor, KnownKeys.ArrowRight, "666", 2) - testCaretChange(editor, KnownKeys.ArrowRight, "666", 3) - testCaretChange(editor, KnownKeys.ArrowRight, "777", 0) - testCaretChange(editor, KnownKeys.ArrowRight, "777", 1) - testCaretChange(editor, KnownKeys.ArrowRight, "777", 2) - testCaretChange(editor, KnownKeys.ArrowRight, "777", 3) - testCaretChange(editor, KnownKeys.ArrowRight, "888", 0) - testCaretChange(editor, KnownKeys.ArrowRight, "888", 1) - testCaretChange(editor, KnownKeys.ArrowRight, "888", 2) - testCaretChange(editor, KnownKeys.ArrowRight, "888", 3) - testCaretChange(editor, KnownKeys.ArrowRight, "888", 3) // don't move at the end - } + testCaretChange(editor, KnownKeys.ArrowRight, "444", 3) + testCaretChange(editor, KnownKeys.ArrowRight, "555", 0) + testCaretChange(editor, KnownKeys.ArrowRight, "555", 1) + testCaretChange(editor, KnownKeys.ArrowRight, "555", 2) + testCaretChange(editor, KnownKeys.ArrowRight, "555", 3) + testCaretChange(editor, KnownKeys.ArrowRight, "666", 0) + testCaretChange(editor, KnownKeys.ArrowRight, "666", 1) + testCaretChange(editor, KnownKeys.ArrowRight, "666", 2) + testCaretChange(editor, KnownKeys.ArrowRight, "666", 3) + testCaretChange(editor, KnownKeys.ArrowRight, "777", 0) + testCaretChange(editor, KnownKeys.ArrowRight, "777", 1) + testCaretChange(editor, KnownKeys.ArrowRight, "777", 2) + testCaretChange(editor, KnownKeys.ArrowRight, "777", 3) + testCaretChange(editor, KnownKeys.ArrowRight, "888", 0) + testCaretChange(editor, KnownKeys.ArrowRight, "888", 1) + testCaretChange(editor, KnownKeys.ArrowRight, "888", 2) + testCaretChange(editor, KnownKeys.ArrowRight, "888", 3) + testCaretChange(editor, KnownKeys.ArrowRight, "888", 3) // don't move at the end + } - private fun testCaretChange(editor: EditorComponent, key: KnownKeys, expectedCellText: String, expectedPosition: Int) { + private suspend fun testCaretChange( + editor: FrontendEditorComponent, + key: KnownKeys, + expectedCellText: String, + expectedPosition: Int, + ) { editor.processKeyEvent(JSKeyboardEvent(JSKeyboardEventType.KEYDOWN, key)) - val layoutable = editor.getRootCell().descendants().find { it.getVisibleText() == expectedCellText }!!.layoutable()!! - assertEquals(CaretSelection(layoutable, expectedPosition), editor.getSelection()) + val layoutable = + editor + .getRootCell() + .descendants() + .find { it.getVisibleText() == expectedCellText }!! + .layoutable()!! + assertEquals(CaretSelection(editor, layoutable, expectedPosition), editor.getSelection()) } } diff --git a/projectional-editor/src/commonTest/kotlin/org/modelix/editor/TextLayouterTest.kt b/projectional-editor/src/commonTest/kotlin/org/modelix/editor/TextLayouterTest.kt index a9b4c5af..a15239bc 100644 --- a/projectional-editor/src/commonTest/kotlin/org/modelix/editor/TextLayouterTest.kt +++ b/projectional-editor/src/commonTest/kotlin/org/modelix/editor/TextLayouterTest.kt @@ -1,5 +1,7 @@ package org.modelix.editor +import org.modelix.editor.text.frontend.FrontendCellTree +import org.modelix.editor.text.frontend.layout import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertSame @@ -25,20 +27,39 @@ class TextLayouterTest { @Test fun newLine3() = testCells("a {\nb\n}", listOf(listOf(listOf("a"), listOf("{", newLine, listOf("b"), newLine, "}")))) - @Test fun indent1() = testCells("{\n b\n c\n d\n}", listOf("{", newLine, listOf(indentChildren, "b", newLine, "c", newLine, "d"), newLine, "}")) - - @Test fun indent2() = testCells("{\n b\n c\n d\n}", listOf("{", newLine, listOf(indentChildren, "b", newLine, "c", newLine, "d", newLine), "}")) - - @Test fun indent3() = testCells(" {\n b\n c\n d\n }", listOf(indentChildren, "{", newLine, "b", newLine, "c", newLine, "d", newLine, "}")) - - @Test fun indent4() = testCells("a {\n b\n c\n d\n}", listOf("a", listOf("{", newLine, listOf(indentChildren, "b", newLine, "c", newLine, "d"), newLine, "}"))) - - @Test fun indent5() = testCells("a {\n b\n c\n d\n}", listOf("a", listOf("{", newLine, listOf(indentChildren, "b", newLine, "c", newLine, "d", newLine), "}"))) - - @Test fun indent6() = testCells("a {\n b\n c\n d\n }", listOf("a", listOf(indentChildren, "{", newLine, "b", newLine, "c", newLine, "d", newLine, "}"))) - - private fun testCells(expected: String, template: Any) { - val text = EditorTestUtils.buildCells(template).layout + @Test fun indent1() = + testCells("{\n b\n c\n d\n}", listOf("{", newLine, listOf(indentChildren, "b", newLine, "c", newLine, "d"), newLine, "}")) + + @Test fun indent2() = + testCells("{\n b\n c\n d\n}", listOf("{", newLine, listOf(indentChildren, "b", newLine, "c", newLine, "d", newLine), "}")) + + @Test fun indent3() = + testCells(" {\n b\n c\n d\n }", listOf(indentChildren, "{", newLine, "b", newLine, "c", newLine, "d", newLine, "}")) + + @Test fun indent4() = + testCells( + "a {\n b\n c\n d\n}", + listOf("a", listOf("{", newLine, listOf(indentChildren, "b", newLine, "c", newLine, "d"), newLine, "}")) + ) + + @Test fun indent5() = + testCells( + "a {\n b\n c\n d\n}", + listOf("a", listOf("{", newLine, listOf(indentChildren, "b", newLine, "c", newLine, "d", newLine), "}")) + ) + + @Test fun indent6() = + testCells( + "a {\n b\n c\n d\n }", + listOf("a", listOf(indentChildren, "{", newLine, "b", newLine, "c", newLine, "d", newLine, "}")) + ) + + private fun testCells( + expected: String, + template: Any, + ) { + val tree = FrontendCellTree() + val text = EditorTestUtils.buildCells(template, tree).layout text.lines.forEach { line -> assertSame(text, line.getText()) line.words.forEach { word -> diff --git a/projectional-editor/src/jsMain/kotlin/org/modelix/editor/DomUtils.kt b/projectional-editor/src/jsMain/kotlin/org/modelix/editor/DomUtils.kt index 00e15fa7..23157869 100644 --- a/projectional-editor/src/jsMain/kotlin/org/modelix/editor/DomUtils.kt +++ b/projectional-editor/src/jsMain/kotlin/org/modelix/editor/DomUtils.kt @@ -9,9 +9,7 @@ import org.w3c.dom.Node import org.w3c.dom.asList import org.w3c.dom.events.MouseEvent -fun Element.getAbsoluteBounds(): Bounds { - return getBoundingClientRect().toBounds().translated(window.scrollX, window.scrollY) -} +fun Element.getAbsoluteBounds(): Bounds = getBoundingClientRect().toBounds().translated(window.scrollX, window.scrollY) fun HTMLElement.setBounds(bounds: Bounds) { with(style) { @@ -22,14 +20,21 @@ fun HTMLElement.setBounds(bounds: Bounds) { } } -fun Element.getAbsoluteInnerBounds(): Bounds { - return (getClientRects().asSequence().firstOrNull()?.toBounds()?.translated(window.scrollX, window.scrollY) ?: Bounds.ZERO) -} +fun Element.getAbsoluteInnerBounds(): Bounds = + ( + getClientRects() + .asSequence() + .firstOrNull() + ?.toBounds() + ?.translated(window.scrollX, window.scrollY) ?: Bounds.ZERO + ) fun DOMRect.toBounds() = Bounds(x, y, width, height) private fun getBodyAbsoluteBounds() = document.body?.getBoundingClientRect()?.toBounds() ?: Bounds.ZERO + fun MouseEvent.getAbsolutePositionX() = clientX + window.scrollX + fun MouseEvent.getAbsolutePositionY() = clientY + window.scrollY fun Node.descendants(): Sequence = childNodes.asList().asSequence().flatMap { sequenceOf(it) + it.descendants() } diff --git a/projectional-editor/src/jsMain/kotlin/org/modelix/editor/JSDom.kt b/projectional-editor/src/jsMain/kotlin/org/modelix/editor/JSDom.kt index d65cc96c..65f1deee 100644 --- a/projectional-editor/src/jsMain/kotlin/org/modelix/editor/JSDom.kt +++ b/projectional-editor/src/jsMain/kotlin/org/modelix/editor/JSDom.kt @@ -9,58 +9,61 @@ import org.w3c.dom.Text import org.w3c.dom.asList import org.w3c.dom.get -class JSDom(private val doc: Document = document) : IVirtualDom, IVirtualDomUI { +class JSDom( + private val doc: Document = document, +) : IVirtualDom, + IVirtualDomUI { var originElement: Element? = null + private fun getOrigin() = originElement?.getAbsoluteBounds() ?: Bounds.ZERO override val ui: IVirtualDomUI get() = this - override fun getOuterBounds(element: IVirtualDom.Element): Bounds { - return element.unwrap().getAbsoluteBounds().relativeTo(getOrigin()) - } + override fun getOuterBounds(element: IVirtualDom.Element): Bounds = element.unwrap().getAbsoluteBounds().relativeTo(getOrigin()) - override fun getInnerBounds(element: IVirtualDom.Element): Bounds { - return element.unwrap().getAbsoluteInnerBounds().relativeTo(getOrigin()) - } + override fun getInnerBounds(element: IVirtualDom.Element): Bounds = element.unwrap().getAbsoluteInnerBounds().relativeTo(getOrigin()) - override fun getElementsAt(x: Double, y: Double): List { - return doc.elementsFromPoint(x, y).map { it.wrap() } - } + override fun getElementsAt( + x: Double, + y: Double, + ): List = doc.elementsFromPoint(x, y).map { it.wrap() } - override fun getElementById(id: String): IVirtualDom.Element? { - return doc.getElementById(id)?.wrap() - } + override fun getElementById(id: String): IVirtualDom.Element? = doc.getElementById(id)?.wrap() - override fun createElement(localName: String): IVirtualDom.Element { - return doc.createElement(localName).wrap() - } + override fun createElement(localName: String): IVirtualDom.Element = doc.createElement(localName).wrap() - override fun createTextNode(data: String): IVirtualDom.Text { - return doc.createTextNode(data).wrap() - } + override fun createTextNode(data: String): IVirtualDom.Text = doc.createTextNode(data).wrap() fun wrap(node: HTMLElement) = wrapNode(node) as IVirtualDom.HTMLElement + fun wrap(node: Element) = wrapNode(node) as IVirtualDom.Element + fun wrap(node: Text) = wrapNode(node) as IVirtualDom.Text + fun wrap(node: Node) = wrapNode(node) fun Node.wrap() = wrap(this) + fun Element.wrap() = wrap(this) + fun HTMLElement.wrap() = wrap(this) + fun Text.wrap() = wrap(this) - private fun wrapNode(node: Node): IVirtualDom.Node { - return when (node) { + private fun wrapNode(node: Node): IVirtualDom.Node = + when (node) { is HTMLElement -> HTMLElementWrapper(node) is Element -> ElementWrapper(node) is Text -> TextNodeWrapper(node) else -> NodeWrapper(node) } - } - open inner class NodeWrapper(private val node: Node) : IVirtualDom.Node { + open inner class NodeWrapper( + private val node: Node, + ) : IVirtualDom.Node { open fun getWrappedNode(): Node = node + override fun getVDom(): IVirtualDom = this@JSDom override fun equals(other: Any?): Boolean { @@ -70,71 +73,73 @@ class JSDom(private val doc: Document = document) : IVirtualDom, IVirtualDomUI { return true } - override fun hashCode(): Int { - return node.hashCode() - } + override fun hashCode(): Int = node.hashCode() override val parent: IVirtualDom.Node? get() = node.parentNode?.wrap() override val childNodes: List get() = node.childNodes.asList().map { it.wrap() } - override fun getUserObject(key: String): Any? { - return node.asDynamic()["key"] - } + override fun getUserObject(key: String): Any? = node.asDynamic()["key"] - override fun putUserObject(key: String, value: Any?) { + override fun putUserObject( + key: String, + value: Any?, + ) { node.asDynamic()["key"] = value } - override fun insertBefore(newNode: IVirtualDom.Node, referenceNode: IVirtualDom.Node?): IVirtualDom.Node { - return node.insertBefore(newNode.unwrap(), referenceNode?.unwrap()).wrap() - } + override fun insertBefore( + newNode: IVirtualDom.Node, + referenceNode: IVirtualDom.Node?, + ): IVirtualDom.Node = node.insertBefore(newNode.unwrap(), referenceNode?.unwrap()).wrap() - override fun appendChild(child: IVirtualDom.Node): IVirtualDom.Node { - return node.appendChild(child.unwrap()).wrap() - } + override fun appendChild(child: IVirtualDom.Node): IVirtualDom.Node = node.appendChild(child.unwrap()).wrap() - override fun replaceChild(newChild: IVirtualDom.Node, oldChild: IVirtualDom.Node): IVirtualDom.Node { - return node.replaceChild(newChild.unwrap(), oldChild.unwrap()).wrap() - } + override fun replaceChild( + newChild: IVirtualDom.Node, + oldChild: IVirtualDom.Node, + ): IVirtualDom.Node = node.replaceChild(newChild.unwrap(), oldChild.unwrap()).wrap() - override fun removeChild(child: IVirtualDom.Node): IVirtualDom.Node { - return node.removeChild(child.unwrap()).wrap() - } + override fun removeChild(child: IVirtualDom.Node): IVirtualDom.Node = node.removeChild(child.unwrap()).wrap() override fun remove() { node.parentNode?.removeChild(node) } } - open inner class TextNodeWrapper(node: Text) : NodeWrapper(node), IVirtualDom.Text { + open inner class TextNodeWrapper( + node: Text, + ) : NodeWrapper(node), + IVirtualDom.Text { override fun getWrappedNode() = super.getWrappedNode() as Text + override var textContent: String? get() = getWrappedNode().textContent - set(value) { getWrappedNode().textContent = value } + set(value) { + getWrappedNode().textContent = value + } } - open inner class ElementWrapper(node: Element) : NodeWrapper(node), IVirtualDom.Element { + open inner class ElementWrapper( + node: Element, + ) : NodeWrapper(node), + IVirtualDom.Element { override fun getWrappedNode() = super.getWrappedNode() as Element + override val tagName: String get() = getWrappedNode().nodeName - override fun getAttributeNames(): Array { - return getWrappedNode().getAttributeNames() - } + override fun getAttributeNames(): Array = getWrappedNode().getAttributeNames() - override fun getAttribute(qualifiedName: String): String? { - return getWrappedNode().getAttribute(qualifiedName) - } + override fun getAttribute(qualifiedName: String): String? = getWrappedNode().getAttribute(qualifiedName) - override fun setAttribute(qualifiedName: String, value: String) { - return getWrappedNode().setAttribute(qualifiedName, value) - } + override fun setAttribute( + qualifiedName: String, + value: String, + ) = getWrappedNode().setAttribute(qualifiedName, value) - override fun removeAttribute(qualifiedName: String) { - return getWrappedNode().removeAttribute(qualifiedName) - } + override fun removeAttribute(qualifiedName: String) = getWrappedNode().removeAttribute(qualifiedName) override fun getAttributes(): Map { val attributes = getWrappedNode().attributes @@ -143,19 +148,21 @@ class JSDom(private val doc: Document = document) : IVirtualDom, IVirtualDomUI { .associate { it.name to it.value } } - override fun getInnerBounds(): Bounds { - return getWrappedNode().getAbsoluteInnerBounds().relativeTo(getOrigin()) - } + override fun getInnerBounds(): Bounds = getWrappedNode().getAbsoluteInnerBounds().relativeTo(getOrigin()) - override fun getOuterBounds(): Bounds { - return getWrappedNode().getAbsoluteBounds().relativeTo(getOrigin()) - } + override fun getOuterBounds(): Bounds = getWrappedNode().getAbsoluteBounds().relativeTo(getOrigin()) } - inner class HTMLElementWrapper(node: HTMLElement) : ElementWrapper(node), IVirtualDom.HTMLElement { + + inner class HTMLElementWrapper( + node: HTMLElement, + ) : ElementWrapper(node), + IVirtualDom.HTMLElement { override fun getWrappedNode() = super.getWrappedNode() as HTMLElement } } fun IVirtualDom.HTMLElement.unwrap(): HTMLElement = (this as JSDom.HTMLElementWrapper).getWrappedNode() + fun IVirtualDom.Element.unwrap(): Element = (this as JSDom.ElementWrapper).getWrappedNode() + fun IVirtualDom.Node.unwrap(): Node = (this as JSDom.NodeWrapper).getWrappedNode() diff --git a/projectional-editor/src/jsMain/kotlin/org/modelix/editor/JsEditorComponent.kt b/projectional-editor/src/jsMain/kotlin/org/modelix/editor/JsEditorComponent.kt index a7f6fa28..aa84d7fc 100644 --- a/projectional-editor/src/jsMain/kotlin/org/modelix/editor/JsEditorComponent.kt +++ b/projectional-editor/src/jsMain/kotlin/org/modelix/editor/JsEditorComponent.kt @@ -2,27 +2,30 @@ package org.modelix.editor import kotlinx.html.div import kotlinx.html.tabIndex -import org.modelix.model.area.IArea +import org.modelix.editor.text.shared.TextEditorService import org.w3c.dom.events.Event import org.w3c.dom.events.KeyboardEvent import org.w3c.dom.events.MouseEvent -class JsEditorComponent(engine: EditorEngine, transactionManager: IArea?, rootCellCreator: (EditorState) -> Cell) : EditorComponent(engine, JSDom(), transactionManager, rootCellCreator), IProducesHtml { - - val containerElement: IVirtualDom.HTMLElement = virtualDom.create().div("js-editor-component") { - tabIndex = "-1" // allows setting keyboard focus - } +class JsEditorComponent( + service: TextEditorService, +) : FrontendEditorComponent(service, JSDom()), + IProducesHtml { + val containerElement: IVirtualDom.HTMLElement = + virtualDom.create().div("js-editor-component") { + tabIndex = "-1" // allows setting keyboard focus + } init { (virtualDom as JSDom).originElement = containerElement.unwrap() containerElement.unwrap().addEventListener("click", { event: Event -> - (event as? MouseEvent)?.let { processMouseEvent(it.convert(JSMouseEventType.CLICK, containerElement.unwrap())) } + (event as? MouseEvent)?.let { enqueueUIEvent(it.convert(JSMouseEventType.CLICK, containerElement.unwrap())) } }) containerElement.unwrap().addEventListener("keydown", { event: Event -> - (event as? KeyboardEvent)?.let { if (processKeyDown(it.convert(JSKeyboardEventType.KEYDOWN))) event.preventDefault() } + (event as? KeyboardEvent)?.let { if (enqueueUIEvent(it.convert(JSKeyboardEventType.KEYDOWN))) event.preventDefault() } }) containerElement.unwrap().addEventListener("keyup", { event: Event -> - (event as? KeyboardEvent)?.let { if (processKeyDown(it.convert(JSKeyboardEventType.KEYUP))) event.preventDefault() } + (event as? KeyboardEvent)?.let { if (enqueueUIEvent(it.convert(JSKeyboardEventType.KEYUP))) event.preventDefault() } }) } diff --git a/projectional-editor/src/jsMain/kotlin/org/modelix/editor/JsKeyboardTranslator.kt b/projectional-editor/src/jsMain/kotlin/org/modelix/editor/JsKeyboardTranslator.kt index 2c4ffbed..cfe08c91 100644 --- a/projectional-editor/src/jsMain/kotlin/org/modelix/editor/JsKeyboardTranslator.kt +++ b/projectional-editor/src/jsMain/kotlin/org/modelix/editor/JsKeyboardTranslator.kt @@ -7,13 +7,14 @@ import org.w3c.dom.events.MouseEvent fun KeyboardEvent.convert(eventType: JSKeyboardEventType): JSKeyboardEvent { val knownKey = KnownKeys.getIfKnown(key) val typedText: String? = key.let { if (it.length == 1) it else null } - val locationEnum = when (this.location) { - KeyboardEvent.DOM_KEY_LOCATION_STANDARD -> KeyLocation.STANDARD - KeyboardEvent.DOM_KEY_LOCATION_LEFT -> KeyLocation.LEFT - KeyboardEvent.DOM_KEY_LOCATION_RIGHT -> KeyLocation.RIGHT - KeyboardEvent.DOM_KEY_LOCATION_NUMPAD -> KeyLocation.NUMPAD - else -> KeyLocation.STANDARD - } + val locationEnum = + when (this.location) { + KeyboardEvent.DOM_KEY_LOCATION_STANDARD -> KeyLocation.STANDARD + KeyboardEvent.DOM_KEY_LOCATION_LEFT -> KeyLocation.LEFT + KeyboardEvent.DOM_KEY_LOCATION_RIGHT -> KeyLocation.RIGHT + KeyboardEvent.DOM_KEY_LOCATION_NUMPAD -> KeyLocation.NUMPAD + else -> KeyLocation.STANDARD + } return JSKeyboardEvent( eventType = eventType, typedText = typedText, @@ -26,7 +27,10 @@ fun KeyboardEvent.convert(eventType: JSKeyboardEventType): JSKeyboardEvent { ) } -fun MouseEvent.convert(eventType: JSMouseEventType, relativeTo: HTMLElement?): JSMouseEvent { +fun MouseEvent.convert( + eventType: JSMouseEventType, + relativeTo: HTMLElement?, +): JSMouseEvent { val origin = relativeTo?.getAbsoluteBounds() ?: Bounds.ZERO return JSMouseEvent( eventType = eventType, diff --git a/projectional-editor/src/jsTest/kotlin/org/modelix/editor/IncrementalJSDOMBuilderTest.kt b/projectional-editor/src/jsTest/kotlin/org/modelix/editor/IncrementalJSDOMBuilderTest.kt index 0242b324..9bc8fe0a 100644 --- a/projectional-editor/src/jsTest/kotlin/org/modelix/editor/IncrementalJSDOMBuilderTest.kt +++ b/projectional-editor/src/jsTest/kotlin/org/modelix/editor/IncrementalJSDOMBuilderTest.kt @@ -1,11 +1,15 @@ package org.modelix.editor import kotlinx.html.TagConsumer +import org.modelix.editor.text.frontend.FrontendCellTree +import org.modelix.editor.text.frontend.getVisibleText +import org.modelix.editor.text.frontend.layout +import org.modelix.editor.text.frontend.text +import org.modelix.editor.text.shared.celltree.IMutableCellTree import kotlin.random.Random import kotlin.test.Ignore import kotlin.test.Test import kotlin.test.assertEquals -import kotlin.test.assertNotSame import kotlin.test.assertTrue class IncrementalJSDOMBuilderTest { @@ -20,88 +24,48 @@ class IncrementalJSDOMBuilderTest { fun test() { val generatedHtmlMap = GeneratedHtmlMap() - val textCellToChange = Cell(TextCellData("b")) - val cell = Cell(CellData()).apply { - addChild(Cell(TextCellData("a"))) - addChild( - Cell(CellData()).apply { - addChild(textCellToChange) - addChild(Cell(CellData().also { it.properties[CommonCellProperties.onNewLine] = true })) - addChild(Cell(TextCellData("c"))) - }, - ) - addChild(Cell(TextCellData("d"))) - } + val tree = FrontendCellTree() + lateinit var textCellToChange: IMutableCellTree.MutableCell + val rootCell = + tree.createCell().apply { + cell("a") + addNewChild().apply { + cell("b").also { textCellToChange = it } + addNewChild().also { + it.setProperty(CommonCellProperties.onNewLine, true) + } + cell("c") + } + cell("d") + } var domBuilder: TagConsumer = IncrementalVirtualDOMBuilder(JSDom(), null, generatedHtmlMap) - val dom = cell.layout.toHtml(domBuilder) + val dom = rootCell.layout.toHtml(domBuilder) val elements1: List = listOf(dom) + dom.descendants() - println(cell) - println(dom.unwrap().outerHTML) + println("cell: " + rootCell) + println("html: " + dom.unwrap().outerHTML) val newText = "X" - val cell2 = replaceCell(cell, textCellToChange, Cell(TextCellData(newText))) - assertNotSame(cell, cell2, "No cell was replaced") + textCellToChange.text = newText domBuilder = IncrementalVirtualDOMBuilder(JSDom(), dom, generatedHtmlMap) - val dom2 = cell2.layout.toHtml(domBuilder) + val dom2 = rootCell.layout.toHtml(domBuilder) val elements2: List = listOf(dom2) + dom2.descendants() - println(cell2) - println(dom2.unwrap().outerHTML) + println("cell: " + rootCell) + println("html: " + dom2.unwrap().outerHTML) assertEquals(elements1.size, elements2.size) - val expectedChanges = elements1.indices.joinToString("") { - val element2 = elements2[it] - if (element2 is IVirtualDom.Text && element2.textContent == newText) "C" else "-" - } - println(expectedChanges) + val expectedChanges = + elements1.indices.joinToString("") { + val element2 = elements2[it] + if (element2 is IVirtualDom.Text && element2.textContent == newText) "C" else "-" + } + println("expected changes: " + expectedChanges) assertTrue(expectedChanges.contains("C")) val actualChanges = elements1.indices.joinToString("") { if (elements1[it] == elements2[it]) "-" else "C" } - println(actualChanges) + println("actual changes: " + actualChanges) assertEquals(expectedChanges, actualChanges) } - fun replaceCell(tree: Cell, oldCell: Cell, newCell: Cell): Cell { - val oldTreeStr = tree.toString() - if (tree == oldCell) return newCell - val oldChildren = tree.getChildren() - val newChildren = oldChildren.map { replaceCell(it, oldCell, newCell) } - if (oldChildren != newChildren) { - val newTree = Cell(tree.data).also { newParent -> - newChildren.forEach { - it.parent?.removeChild(it) - newParent.addChild(it) - } - } - val newTreeStr = newTree.toString() - return newTree - } - return tree - } - - fun insertCell(tree: Cell, anchor: Cell, newCell: Cell): Cell { - val oldTreeStr = tree.toString() - if (tree == anchor) { - return Cell().also { newParent -> - newParent.addChild(newCell) - anchor.parent?.removeChild(anchor) - newParent.addChild(anchor) - } - } - val oldChildren = tree.getChildren() - val newChildren = oldChildren.map { insertCell(it, anchor, newCell) } - if (oldChildren != newChildren) { - val newTree = Cell(tree.data).also { newParent -> - newChildren.forEach { - it.parent?.removeChild(it) - newParent.addChild(it) - } - } - val newTreeStr = newTree.toString() - return newTree - } - return tree - } - @Test fun runRandomTest_4_3() = runRandomTests(567454, 4, 3) @Test fun runRandomTest_1_1() = runRandomTests(567454, 1, 1) @@ -132,33 +96,53 @@ class IncrementalJSDOMBuilderTest { } } - fun runRandomTests(seed: Int, cellsPerLevel: Int, levels: Int) { + fun runRandomTests( + seed: Int, + cellsPerLevel: Int, + levels: Int, + ) { val rand = Random(seed) runRandomTest(rand, cellsPerLevel, levels) { cell -> - val randomLeafCell = cell.descendants().filter { !it.getVisibleText().isNullOrEmpty() }.shuffled(rand).firstOrNull() - ?: cell.descendants().filter { it.getChildren().isEmpty() }.shuffled(rand).first() + val randomLeafCell = + cell + .descendants() + .filter { it.getVisibleText().isNotEmpty() } + .shuffled(rand) + .firstOrNull() + ?: cell + .descendants() + .filter { it.getChildren().isEmpty() } + .shuffled(rand) + .first() println("replace $randomLeafCell") - replaceCell( - cell, - randomLeafCell, - Cell(TextCellData("replacement")), - ) + (randomLeafCell as IMutableCellTree.MutableCell).text = "replacement" + randomLeafCell } runRandomTest(rand, cellsPerLevel, levels) { cell -> - val randomCell = cell.descendants().shuffled(rand).firstOrNull() - ?: cell.descendants().filter { it.getChildren().isEmpty() }.shuffled(rand).first() + val randomCell = + cell.descendants().shuffled(rand).firstOrNull() + ?: cell + .descendants() + .filter { it.getChildren().isEmpty() } + .shuffled(rand) + .first() + randomCell as MutableCell println("insertBefore $randomCell") - insertCell( - cell, - randomCell, - Cell(TextCellData("insertion")), - ) + randomCell.getParent()!!.addNewChild(randomCell.index()).also { + it.text = "insertion" + } } } - fun runRandomTest(rand: Random, cellsPerLevel: Int, levels: Int, modify: (Cell) -> Cell) { + fun runRandomTest( + rand: Random, + cellsPerLevel: Int, + levels: Int, + modify: (MutableCell) -> MutableCell, + ) { val generatedHtmlMap = GeneratedHtmlMap() - val cell = EditorTestUtils.buildRandomCells(rand, cellsPerLevel, levels) + val tree = FrontendCellTree() + val cell = EditorTestUtils.buildRandomCells(rand, cellsPerLevel, levels, tree) val dom = cell.layout.toHtml(IncrementalVirtualDOMBuilder(JSDom(), null, generatedHtmlMap)) val html = dom.unwrap().outerHTML println("old html: " + html) @@ -168,9 +152,31 @@ class IncrementalJSDOMBuilderTest { val dom2incremental = newCell.layout.toHtml(IncrementalVirtualDOMBuilder(JSDom(), dom, generatedHtmlMap)) val html2incremental = dom2incremental.unwrap().outerHTML - newCell.descendantsAndSelf().forEach { it.clearCachedLayout() } + newCell.descendantsAndSelf().forEach { (it as FrontendCellTree.FrontendCellImpl).clearCachedLayout() } val dom2nonIncremental = newCell.layout.toHtml(IncrementalVirtualDOMBuilder(JSDom(), null, generatedHtmlMap)) val html2nonIncremental = dom2nonIncremental.unwrap().outerHTML assertEquals(html2nonIncremental, html2incremental) } + + private fun IMutableCellTree.cell( + text: String, + body: IMutableCellTree.MutableCell.() -> Unit, + ): IMutableCellTree.MutableCell = + this + .createCell() + .also { + it.setProperty(TextCellProperties.text, text) + }.also(body) + + private fun IMutableCellTree.MutableCell.cell( + text: String, + body: IMutableCellTree.MutableCell.() -> Unit = { + }, + ): IMutableCellTree.MutableCell = + this + .addNewChild() + .also { + it.type = ECellType.TEXT + it.text = text + }.also(body) } diff --git a/react-ssr-client/yarn.lock b/react-ssr-client/yarn.lock index 1dce1f06..d4e01ce2 100644 --- a/react-ssr-client/yarn.lock +++ b/react-ssr-client/yarn.lock @@ -673,15 +673,14 @@ integrity sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w== "@modelix/projectional-editor-ssr-client-lib@file:../projectional-editor-ssr-client-lib/build/packages/js": - version "1.5.2-3-g212dc07.dirty-SNAPSHOT" + version "1.10.0-20-g33d5a61-dirty-SNAPSHOT" dependencies: - format-util "^1.0.5" ws "8.18.0" -"@modelix/ts-model-api@^8.15.0": - version "8.20.0" - resolved "https://artifacts.itemis.cloud/repository/npm/@modelix/ts-model-api/-/ts-model-api-8.20.0.tgz#42da9340f93dde2a9b5d77e39ed0597db0677cfc" - integrity sha512-lXYOWBdBbUQ2dxpuIbphiMFWhYwF8GvF+dCMbBAa3F9cKGzlPJ6Qk/fQsT77+xgbFL3D6ueY62IUOquS7B01oQ== +"@modelix/ts-model-api@^16.2.1": + version "16.6.1" + resolved "https://artifacts.itemis.cloud/repository/npm/@modelix/ts-model-api/-/ts-model-api-16.6.1.tgz#bb2432bd8a018b767ca14c02ec9c5f35aea562c4" + integrity sha512-7kQop6X7YEiJToGJF2Ze0W5tZVdAWhbByBjLUC49tFKfqxRRcXpb3NLafBhIIleYALhsTOKuE3woWgZL1tz2zQ== "@mui/base@5.0.0-beta.40-0": version "5.0.0-beta.40-0" @@ -1895,11 +1894,6 @@ flatted@^3.2.9: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.1.tgz#21db470729a6734d4997002f439cb308987f567a" integrity sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw== -format-util@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/format-util/-/format-util-1.0.5.tgz#1ffb450c8a03e7bccffe40643180918cc297d271" - integrity sha512-varLbTj0e0yVyRpqQhuWV+8hlePAgaoFRhNFj50BNjEIrw1/DphHSObtqwskVCPWNgzwPoQrZAbfa/SBiicNeg== - fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" diff --git a/react-ssr-mps-test/src/test/kotlin/PagesTest.kt b/react-ssr-mps-test/src/test/kotlin/PagesTest.kt index e53279f1..9761f12e 100644 --- a/react-ssr-mps-test/src/test/kotlin/PagesTest.kt +++ b/react-ssr-mps-test/src/test/kotlin/PagesTest.kt @@ -19,7 +19,6 @@ import kotlin.time.Duration.Companion.seconds import kotlin.time.toJavaDuration class PagesTest { - companion object { var mps: GenericContainer<*>? = null var playwright: Playwright? = null @@ -28,16 +27,20 @@ class PagesTest { @BeforeAll @JvmStatic fun beforeAll() { - mps = GenericContainer("modelix/mps-vnc-baseimage:0.9.4-mps${System.getenv("MPS_VERSION")}") - .withCopyFileToContainer(MountableFile.forHostPath(File(System.getenv("MODELIX_MPS_PLUGINS_PATH")).toPath()), "/mps/plugins") - .withCopyFileToContainer(MountableFile.forHostPath(File(System.getenv("MODELIX_TEST_LANGUAGES_PATH")).toPath()), "/mps-languages") - .withExposedPorts(43595) + mps = + GenericContainer("modelix/mps-vnc-baseimage:0.9.4-mps2023.2") + .withCopyFileToContainer( + MountableFile.forHostPath(File(System.getenv("MODELIX_MPS_PLUGINS_PATH")).toPath()), + "/mps/plugins" + ).withCopyFileToContainer( + MountableFile.forHostPath(File(System.getenv("MODELIX_TEST_LANGUAGES_PATH")).toPath()), + "/mps-languages" + ).withExposedPorts(43595) // .waitingFor(Wait.forListeningPort().withStartupTimeout(3.minutes.toJavaDuration())) - .waitingFor(Wait.forHttp("/pages/modelix/test/modules-list/").withStartupTimeout(3.minutes.toJavaDuration())) - .withLogConsumer { - println(it.utf8StringWithoutLineEnding) - } - .also { it.start() } + .waitingFor(Wait.forHttp("/pages/modelix/test/modules-list/").withStartupTimeout(3.minutes.toJavaDuration())) + .withLogConsumer { + println(it.utf8StringWithoutLineEnding) + }.also { it.start() } playwright = Playwright.create() browser = playwright!!.chromium().launch() } @@ -55,29 +58,31 @@ class PagesTest { } @Test - fun `custom page is available`() = runBrowserTest("pages/modelix/test/modules-list/") { page -> - page.locator("ul").waitFor() - val content = page.content() - println(content) - assertTrue(content.contains("""
  • """)) - assertTrue(content.contains("""Module: org.modelix.mps.react""")) - } + fun `custom page is available`() = + runBrowserTest("pages/modelix/test/modules-list/") { page -> + page.locator("ul").waitFor() + val content = page.content() + println(content) + assertTrue(content.contains("""
  • """)) + assertTrue(content.contains("""Module: org.modelix.mps.react""")) + } @Test - fun `text field is editable`() = runBrowserTest("pages/modelix/test/text-field/") { page -> - val textField = page.locator("input") - val readOnlyText = page.locator("div[class='name']") - textField.waitFor() - val content = page.content() - println(content) - assertEquals("MyClass", textField.getAttribute("value")) - assertEquals("MyClass", readOnlyText.textContent()) + fun `text field is editable`() = + runBrowserTest("pages/modelix/test/text-field/") { page -> + val textField = page.locator("input") + val readOnlyText = page.locator("div[class='name']") + textField.waitFor() + val content = page.content() + println(content) + assertEquals("MyClass", textField.getAttribute("value")) + assertEquals("MyClass", readOnlyText.textContent()) - textField.fill("MyChangedClass") - page.locator("div:has-text('MyChangedClass')[class='name']").waitFor() - assertEquals("MyChangedClass", textField.getAttribute("value")) - assertEquals("MyChangedClass", readOnlyText.textContent()) - } + textField.fill("MyChangedClass") + page.locator("div:has-text('MyChangedClass')[class='name']").waitFor() + assertEquals("MyChangedClass", textField.getAttribute("value")) + assertEquals("MyChangedClass", readOnlyText.textContent()) + } suspend fun Page.waitForContent(expected: String) { for (i in 1..10) { @@ -87,7 +92,10 @@ class PagesTest { error("Content not found.\n\n${content()}") } - private fun runBrowserTest(path: String, body: suspend (Page) -> Unit) = runTest { + private fun runBrowserTest( + path: String, + body: suspend (Page) -> Unit, + ) = runTest { browser!!.newPage().use { page -> page.navigate("http://localhost:${mps!!.firstMappedPort}/$path") body(page) diff --git a/react-ssr-mps/build.gradle.kts b/react-ssr-mps/build.gradle.kts index e3dda7b4..f8cc72d4 100644 --- a/react-ssr-mps/build.gradle.kts +++ b/react-ssr-mps/build.gradle.kts @@ -56,11 +56,12 @@ tasks { val pluginDir = mpsPluginsDir if (pluginDir != null) { - val installMpsPlugin = register("installMpsPlugin") { - dependsOn(prepareSandbox) - from(project.layout.buildDirectory.dir("idea-sandbox/plugins/${project.name}")) - into(pluginDir.resolve(project.name)) - } + val installMpsPlugin = + register("installMpsPlugin") { + dependsOn(prepareSandbox) + from(project.layout.buildDirectory.dir("idea-sandbox/plugins/${project.name}")) + into(pluginDir.resolve(project.name)) + } register("installMpsDevPlugins") { dependsOn(installMpsPlugin) } @@ -74,7 +75,14 @@ tasks { .from(patchPluginXml.flatMap { it.outputFiles }) doLast { - val jarsInBasePlugin = defaultDestinationDir.get().resolve(project(":editor-common-mps").name).resolve("lib").list()?.toHashSet() ?: emptySet() + val jarsInBasePlugin = + defaultDestinationDir + .get() + .resolve(project(":editor-common-mps").name) + .resolve("lib") + .list() + ?.toHashSet() + ?: emptySet() defaultDestinationDir.get().resolve(project.name).resolve("lib").listFiles()?.forEach { if (jarsInBasePlugin.contains(it.name)) it.delete() } @@ -117,13 +125,22 @@ publishing { metamodel { mpsHeapSize = "2g" mpsHome = mpsHomeDir.get().asFile.absoluteFile - modulesFrom(project(":mps").layout.projectDirectory.dir("modules/org.modelix.mps.react").asFile) + modulesFrom( + project(":mps") + .layout.projectDirectory + .dir("modules/org.modelix.mps.react") + .asFile, + ) includeNamespace("org.modelix") includeLanguage("jetbrains.mps.baseLanguage") includeLanguage("jetbrains.mps.lang.structure") kotlinProject = project - kotlinDir = project.layout.buildDirectory.dir("apigen/kotlin_gen").get().asFile + kotlinDir = + project.layout.buildDirectory + .dir("apigen/kotlin_gen") + .get() + .asFile registrationHelperName = "org.modelix.react.ApiGenLanguages" conceptPropertiesInterfaceName = "org.modelix.react.IConceptProperties" } diff --git a/react-ssr-mps/src/main/kotlin/org/modelix/react/ssr/mps/CompiledMPSRenderer.kt b/react-ssr-mps/src/main/kotlin/org/modelix/react/ssr/mps/CompiledMPSRenderer.kt index 889b02f7..49f2d73b 100644 --- a/react-ssr-mps/src/main/kotlin/org/modelix/react/ssr/mps/CompiledMPSRenderer.kt +++ b/react-ssr-mps/src/main/kotlin/org/modelix/react/ssr/mps/CompiledMPSRenderer.kt @@ -40,7 +40,10 @@ import org.modelix.react.ssr.server.RendererCall import org.modelix.react.ssr.server.ViewModel import org.modelix.react.ssr.server.buildViewModel -class MPSRendererFactory(val repository: () -> SRepository) : IRendererFactory, Disposable { +class MPSRendererFactory( + val repository: () -> SRepository, +) : IRendererFactory, + Disposable { val useInterpreter = TrackableValue(false) private val descriptors = ReactSSRAspectDescriptors().also { Disposer.register(this, it) } @@ -51,13 +54,12 @@ class MPSRendererFactory(val repository: () -> SRepository) : IRendererFactory, nodeRef: RendererCall, parameters: Map>, coroutineScope: CoroutineScope, - ): GenericNodeRenderer { - return if (useInterpreter.getValue()) { + ): GenericNodeRenderer = + if (useInterpreter.getValue()) { InterpretedMPSRenderer(incrementalEngine, repository, nodeRef, coroutineScope) } else { CompiledMPSRenderer(incrementalEngine, repository, nodeRef, coroutineScope, descriptors) } - } override fun createPageRenderer( incrementalEngine: IIncrementalEngine, @@ -75,9 +77,7 @@ class MPSRendererFactory(val repository: () -> SRepository) : IRendererFactory, val parameters: Map>, val coroutineScope: CoroutineScope, ) : IRenderer { - override fun runRead(body: () -> R): R { - return repository().computeRead(body) - } + override fun runRead(body: () -> R): R = repository().computeRead(body) fun createRootRenderer(): IRenderer? { val repository = repository() @@ -115,9 +115,7 @@ class CompiledMPSRenderer( coroutineScope: CoroutineScope, val descriptors: ReactSSRAspectDescriptors, ) : GenericNodeRenderer(incrementalEngine, root, coroutineScope) { - override fun resolveNode(nodeRef: NodeReference): INode? { - return MPSArea(repository()).resolveNode(nodeRef) - } + override fun resolveNode(nodeRef: NodeReference): INode? = MPSArea(repository()).resolveNode(nodeRef) override suspend fun runWrite(body: () -> R): R { var result: R? = null @@ -129,86 +127,105 @@ class CompiledMPSRenderer( return result as R } - override fun runRead(body: () -> R): R { - return repository().modelAccess.computeRead { + override fun runRead(body: () -> R): R = + repository().modelAccess.computeRead { body() } - } fun getDescriptor() = CompositeReactSSRAspectDescriptor(descriptors.findDescriptors(repository()).toSet()) - override fun renderNodeEditor(node: RendererCall): ViewModel { - return buildViewModel { + override fun renderNodeEditor(node: RendererCall): ViewModel = + buildViewModel { child(renderMPSNode(node, getDescriptor())) } - } - private fun resolveRenderers(call: RendererCall, descriptor: IReactSSRAspectDescriptor): List = when (call) { - is NodeRendererCall -> { - call.node.getConcept().getAllConcepts().flatMap { - descriptor.getRenderersForConcept(it.getReference() as ConceptReference).filter { it.isApplicable(call.node.asLegacyNode()) } + private fun resolveRenderers( + call: RendererCall, + descriptor: IReactSSRAspectDescriptor, + ): List = + when (call) { + is NodeRendererCall -> { + call.node.getConcept().getAllConcepts().flatMap { + descriptor + .getRenderersForConcept( + it.getReference() as ConceptReference + ).filter { it.isApplicable(call.node.asLegacyNode()) } + } + } + + is NamedRendererCall -> { + descriptor.getRenderers(NamedRendererSignature(call.id)) + } + + is NodeRefRendererCall -> { + val node = MPSArea(repository()).asModel().resolveNode(call.node) + resolveRenderers(NodeRendererCall(node), descriptor) } } - is NamedRendererCall -> { - descriptor.getRenderers(NamedRendererSignature(call.id)) - } - is NodeRefRendererCall -> { - val node = MPSArea(repository()).asModel().resolveNode(call.node) - resolveRenderers(NodeRendererCall(node), descriptor) - } - } - private fun ensureIsTracked(obj: T): T { - return when (obj) { + private fun ensureIsTracked(obj: T): T = + when (obj) { is NodeRendererCall -> obj.copy(node = ensureIsTracked(obj.node)) is NamedRendererCall -> obj.copy(parameterValues = ensureIsTracked(obj.parameterValues)) is SNode -> ModelixNodeAsMPSNode.ensureIsTracked(obj) is List<*> -> obj.map { ensureIsTracked(it) } else -> obj } as T - } - - fun renderMPSNode(call: RendererCall, descriptor: IReactSSRAspectDescriptor): IComponentOrList = renderMPSNodeIncremental(call, descriptor) - private val renderMPSNodeIncremental: (RendererCall, IReactSSRAspectDescriptor) -> IComponentOrList = incremenentalEngine.incrementalFunction("renderMPSNode") { _, call: RendererCall, descriptor: IReactSSRAspectDescriptor -> - if (call is NodeRefRendererCall) { - val node = MPSArea(repository()).asModel().resolveNode(call.node) - return@incrementalFunction renderMPSNode(NodeRendererCall(node), descriptor) - } - - val call = ensureIsTracked(call) - val renderers = resolveRenderers(call, descriptor) - val renderer = renderers.firstOrNull() // TODO resolve conflict if multiple renderers are applicable - ?: return@incrementalFunction renderNode(call) - val context = object : IRenderContext { - override fun getIncrementalEngine(): IIncrementalEngine = incrementalEngine + fun renderMPSNode( + call: RendererCall, + descriptor: IReactSSRAspectDescriptor, + ): IComponentOrList = renderMPSNodeIncremental(call, descriptor) - override fun callRenderer(call: RendererCall): IComponentOrList { - return renderMPSNode(call, descriptor) + private val renderMPSNodeIncremental: (RendererCall, IReactSSRAspectDescriptor) -> IComponentOrList = + incremenentalEngine.incrementalFunction("renderMPSNode") { _, call: RendererCall, descriptor: IReactSSRAspectDescriptor -> + if (call is NodeRefRendererCall) { + val node = MPSArea(repository()).asModel().resolveNode(call.node) + return@incrementalFunction renderMPSNode(NodeRendererCall(node), descriptor) } - override fun getState(id: String, defaultValue: Boolean): Boolean { - return (allStates[id] as? JsonPrimitive)?.booleanOrNull ?: defaultValue - } - - override fun getState(id: String, defaultValue: String?): String? { - return (allStates[id] as? JsonPrimitive)?.content ?: defaultValue - } + val call = ensureIsTracked(call) + + val renderers = resolveRenderers(call, descriptor) + val renderer = + renderers.firstOrNull() // TODO resolve conflict if multiple renderers are applicable + ?: return@incrementalFunction renderNode(call) + val context = + object : IRenderContext { + override fun getIncrementalEngine(): IIncrementalEngine = incrementalEngine + + override fun callRenderer(call: RendererCall): IComponentOrList = renderMPSNode(call, descriptor) + + override fun getState( + id: String, + defaultValue: Boolean, + ): Boolean = (allStates[id] as? JsonPrimitive)?.booleanOrNull ?: defaultValue + + override fun getState( + id: String, + defaultValue: String?, + ): String? = (allStates[id] as? JsonPrimitive)?.content ?: defaultValue + + override fun setState( + id: String, + value: String?, + ): String? { + if (value == null) { + allStates.remove(id) + } else { + allStates[id] = JsonPrimitive(value) + } + return value + } - override fun setState(id: String, value: String?): String? { - if (value == null) { - allStates.remove(id) - } else { - allStates[id] = JsonPrimitive(value) + override fun setState( + id: String, + value: Boolean, + ): Boolean { + allStates[id] = JsonPrimitive(value) + return value + } } - return value - } - - override fun setState(id: String, value: Boolean): Boolean { - allStates[id] = JsonPrimitive(value) - return value - } + renderer.render(call, context) } - renderer.render(call, context) - } } diff --git a/react-ssr-mps/src/main/kotlin/org/modelix/react/ssr/mps/InterpretedMPSRenderer.kt b/react-ssr-mps/src/main/kotlin/org/modelix/react/ssr/mps/InterpretedMPSRenderer.kt index 4769a52b..3e19bcbe 100644 --- a/react-ssr-mps/src/main/kotlin/org/modelix/react/ssr/mps/InterpretedMPSRenderer.kt +++ b/react-ssr-mps/src/main/kotlin/org/modelix/react/ssr/mps/InterpretedMPSRenderer.kt @@ -61,9 +61,7 @@ class InterpretedMPSRenderer( nodeRef: RendererCall, coroutineScope: CoroutineScope, ) : GenericNodeRenderer(incrementalEngine, nodeRef, coroutineScope) { - override fun resolveNode(nodeRef: NodeReference): INode? { - return MPSArea(repository()).resolveNode(nodeRef) - } + override fun resolveNode(nodeRef: NodeReference): INode? = MPSArea(repository()).resolveNode(nodeRef) override suspend fun runWrite(body: () -> R): R { var result: R? = null @@ -75,143 +73,209 @@ class InterpretedMPSRenderer( return result as R } - override fun runRead(body: () -> R): R { - return repository().modelAccess.computeRead { + override fun runRead(body: () -> R): R = + repository().modelAccess.computeRead { body() } - } - override fun renderNodeEditor(node: RendererCall): ViewModel { - return buildViewModel { + override fun renderNodeEditor(node: RendererCall): ViewModel = + buildViewModel { child(renderMPSNode(node)) } - } - private val findConceptComponents: () -> Map = incremenentalEngine.incrementalFunction("findConceptComponents") { _ -> - repository().modules.asSequence() - .flatMap { it.models } - .flatMap { it.rootNodes } - .map { ModelixNodeAsMPSNode.toModelixNode(it).typed() } - .filterIsInstance() - .flatMap { it.content } - .filterIsInstance() - .associateBy { it.concept.asConceptReference() } - } + private val findConceptComponents: () -> Map = + incremenentalEngine.incrementalFunction("findConceptComponents") { _ -> + repository() + .modules + .asSequence() + .flatMap { it.models } + .flatMap { it.rootNodes } + .map { ModelixNodeAsMPSNode.toModelixNode(it).typed() } + .filterIsInstance() + .flatMap { it.content } + .filterIsInstance() + .associateBy { it.concept.asConceptReference() } + } private fun renderMPSNode(node: RendererCall): IComponentOrList = renderMPSNodeIncremental(node) - private val renderMPSNodeIncremental: (RendererCall) -> IComponentOrList = incremenentalEngine.incrementalFunction("renderMPSNode") { _, call: RendererCall -> - val node = (call as NodeRendererCall).node.asLegacyNode() - val allComponents = findConceptComponents() - val renderers = node.concept!!.getAllConcepts().asSequence().mapNotNull { - allComponents[it.getReference() as ConceptReference] + private val renderMPSNodeIncremental: (RendererCall) -> IComponentOrList = + incremenentalEngine.incrementalFunction("renderMPSNode") { _, call: RendererCall -> + val node = (call as NodeRendererCall).node.asLegacyNode() + val allComponents = findConceptComponents() + + val renderers = + node.concept!!.getAllConcepts().asSequence().mapNotNull { + allComponents[it.getReference() as ConceptReference] + } + val renderer = + renderers.firstOrNull() // TODO resolve conflict if multiple renderers are applicable + ?: return@incrementalFunction renderNode(call) + + val rootComponents = checkNotNull(renderer.components) { "No root component found" } + IComponentOrList.fromSequence(rootComponents.asSequence().map { renderComponent(node, it) }) } - val renderer = renderers.firstOrNull() // TODO resolve conflict if multiple renderers are applicable - ?: return@incrementalFunction renderNode(call) - val rootComponents = checkNotNull(renderer.components) { "No root component found" } - IComponentOrList.fromSequence(rootComponents.asSequence().map { renderComponent(node, it) }) - } + private fun renderComponent( + node: INode, + component: N_IReactComponent, + ): List = renderComponentIncremental(node, component) - private fun renderComponent(node: INode, component: N_IReactComponent): List = renderComponentIncremental(node, component) - private val renderComponentIncremental: (INode, N_IReactComponent) -> List = incremenentalEngine.incrementalFunction("renderComponent") { _, node: INode, component: N_IReactComponent -> - try { - when (component) { - is N_ChildrenComponent -> { - val link = MPSChildLink(MetaAdapterByDeclaration.getContainmentLink((component.link.untyped().asWritableNode() as MPSWritableNode).node)) - node.getChildren(link).map { renderMPSNode(NodeRendererCall(it.asReadableNode())) } - } - is N_TextComponent -> { - listOf(ComponentOrText(text = evaluateExpression(node, component.value.get())?.toString())) - } - is N_GenericReactComponent -> { - listOf( - ComponentOrText( - component = buildComponent(component.componentType) { - for (property in component.properties) { - val value = property.value.get() - when (value) { - is N_PrimitivePropertyValue -> { - val evaluatedValue = evaluateExpression(node, value.value.get()) - when (evaluatedValue) { - null -> {} - is String -> property(property.propertyName, evaluatedValue) - is Number -> property(property.propertyName, evaluatedValue) - is Boolean -> property(property.propertyName, evaluatedValue) - is Component -> property(property.propertyName, evaluatedValue) - is ComponentOrText -> property(property.propertyName, evaluatedValue) - is JsonElement -> property(property.propertyName, evaluatedValue) - is JsCode -> property(property.propertyName, evaluatedValue) - else -> property(property.propertyName, evaluatedValue.toString()) - } - } - is N_ComponentPropertyValue -> { - value.component.get()?.let { renderComponent(node, it).firstOrNull() }?.let { - property(property.propertyName, it.flatten().single()) + private val renderComponentIncremental: (INode, N_IReactComponent) -> List = + incremenentalEngine.incrementalFunction("renderComponent") { _, node: INode, component: N_IReactComponent -> + try { + when (component) { + is N_ChildrenComponent -> { + val link = + MPSChildLink( + MetaAdapterByDeclaration.getContainmentLink( + (component.link.untyped().asWritableNode() as MPSWritableNode).node + ) + ) + node.getChildren(link).map { renderMPSNode(NodeRendererCall(it.asReadableNode())) } + } + + is N_TextComponent -> { + listOf(ComponentOrText(text = evaluateExpression(node, component.value.get())?.toString())) + } + + is N_GenericReactComponent -> { + listOf( + ComponentOrText( + component = + buildComponent(component.componentType) { + for (property in component.properties) { + val value = property.value.get() + when (value) { + is N_PrimitivePropertyValue -> { + val evaluatedValue = evaluateExpression(node, value.value.get()) + when (evaluatedValue) { + null -> {} + + is String -> { + property(property.propertyName, evaluatedValue) + } + + is Number -> { + property(property.propertyName, evaluatedValue) + } + + is Boolean -> { + property(property.propertyName, evaluatedValue) + } + + is Component -> { + property(property.propertyName, evaluatedValue) + } + + is ComponentOrText -> { + property(property.propertyName, evaluatedValue) + } + + is JsonElement -> { + property(property.propertyName, evaluatedValue) + } + + is JsCode -> { + property(property.propertyName, evaluatedValue) + } + + else -> { + property(property.propertyName, evaluatedValue.toString()) + } + } + } + + is N_ComponentPropertyValue -> { + value.component.get()?.let { renderComponent(node, it).firstOrNull() }?.let { + property(property.propertyName, it.flatten().single()) + } + } + + is N_JsFunctionPropertyValue -> {} + + is N_IJsonValue -> { + property(property.propertyName, createJsonElement(node, value)) + } + + else -> {} } } - is N_JsFunctionPropertyValue -> {} - is N_IJsonValue -> { - property(property.propertyName, createJsonElement(node, value)) + + for (child in component.children) { + child(IComponentOrList.create(renderComponent(node, child))) } - else -> {} } - } - - for (child in component.children) { - child(IComponentOrList.create(renderComponent(node, child))) - } - } + ) ) - ) + } + + else -> { + listOf(ComponentOrText(text = "Unknown component type: ${component.untypedConcept().getLongName()}")) + } } - else -> listOf(ComponentOrText(text = "Unknown component type: ${component.untypedConcept().getLongName()}")) + } catch (ex: Exception) { + listOf(ComponentOrText(text = ex.message)) } - } catch (ex: Exception) { - listOf(ComponentOrText(text = ex.message)) } - } - fun createJsonElement(contextNode: INode, value: N_IJsonValue?): JsonElement = createJsonElementIncremental(contextNode, value) - val createJsonElementIncremental: (contextNode: INode, value: N_IJsonValue?) -> JsonElement = incrementalEngine.incrementalFunction("createJsonElementIncremental") { _, contextNode, value -> - when (value) { - null -> JsonNull - is N_JsonObjectValue -> { - buildJsonObject { - for (member in value.members) { - property(member.key, createJsonElement(contextNode, member.value.get())) + fun createJsonElement( + contextNode: INode, + value: N_IJsonValue?, + ): JsonElement = createJsonElementIncremental(contextNode, value) + + val createJsonElementIncremental: (contextNode: INode, value: N_IJsonValue?) -> JsonElement = + incrementalEngine.incrementalFunction("createJsonElementIncremental") { _, contextNode, value -> + when (value) { + null -> { + JsonNull + } + + is N_JsonObjectValue -> { + buildJsonObject { + for (member in value.members) { + property(member.key, createJsonElement(contextNode, member.value.get())) + } } } - } - is N_JsonArray -> { - JsonArray(value.elements.map { createJsonElement(contextNode, it) }) - } - is N_PrimitivePropertyValue -> { - val evaluatedPrimitive = evaluateExpression(contextNode, value.value.get()) - when (evaluatedPrimitive) { - is String -> JsonPrimitive(evaluatedPrimitive) - is Number -> JsonPrimitive(evaluatedPrimitive) - is Boolean -> JsonPrimitive(evaluatedPrimitive) - else -> error("Unknown json primitive type: $evaluatedPrimitive") + + is N_JsonArray -> { + JsonArray(value.elements.map { createJsonElement(contextNode, it) }) + } + + is N_PrimitivePropertyValue -> { + val evaluatedPrimitive = evaluateExpression(contextNode, value.value.get()) + when (evaluatedPrimitive) { + is String -> JsonPrimitive(evaluatedPrimitive) + is Number -> JsonPrimitive(evaluatedPrimitive) + is Boolean -> JsonPrimitive(evaluatedPrimitive) + else -> error("Unknown json primitive type: $evaluatedPrimitive") + } + } + + else -> { + error("Unknown json element type: $value") } } - else -> error("Unknown json element type: $value") } - } - fun evaluateExpression(contextNode: INode, expression: N_Expression?): Any? = evaluateExpressionIncremental(contextNode, expression) - private val evaluateExpressionIncremental: (INode, N_Expression?) -> Any? = incrementalEngine.incrementalFunction("evaluateExpression") { _, contextNode: INode, expression: N_Expression? -> - when (expression) { - null -> null - is N_StringLiteral -> expression.value - is N_IntegerConstant -> expression.value - is N_BooleanConstant -> expression.value - is N_ComponentNodeExpression -> contextNode - else -> null + fun evaluateExpression( + contextNode: INode, + expression: N_Expression?, + ): Any? = evaluateExpressionIncremental(contextNode, expression) + + private val evaluateExpressionIncremental: (INode, N_Expression?) -> Any? = + incrementalEngine.incrementalFunction("evaluateExpression") { _, contextNode: INode, expression: N_Expression? -> + when (expression) { + null -> null + is N_StringLiteral -> expression.value + is N_IntegerConstant -> expression.value + is N_BooleanConstant -> expression.value + is N_ComponentNodeExpression -> contextNode + else -> null + } } - } } -fun N_AbstractConceptDeclaration.asConceptReference(): ConceptReference { - return MPSConcept(MetaAdapterByDeclaration.getConcept((this.untyped().asWritableNode() as MPSWritableNode).node)).getReference() -} +fun N_AbstractConceptDeclaration.asConceptReference(): ConceptReference = + MPSConcept(MetaAdapterByDeclaration.getConcept((this.untyped().asWritableNode() as MPSWritableNode).node)).getReference() diff --git a/react-ssr-mps/src/main/kotlin/org/modelix/react/ssr/mps/ModelCheckerIntegration.kt b/react-ssr-mps/src/main/kotlin/org/modelix/react/ssr/mps/ModelCheckerIntegration.kt index 1af4198f..01bbe1e4 100644 --- a/react-ssr-mps/src/main/kotlin/org/modelix/react/ssr/mps/ModelCheckerIntegration.kt +++ b/react-ssr-mps/src/main/kotlin/org/modelix/react/ssr/mps/ModelCheckerIntegration.kt @@ -23,47 +23,51 @@ import org.modelix.react.ssr.server.IRenderer @Suppress("unused") object ModelCheckerIntegration { + private val fCheckRootNode = + incrementalFunction, INode>("checkRootNode") { context, node -> + runCheck(ModelixNodeAsMPSNode.toMPSNode(node)) + } - private val fCheckRootNode = incrementalFunction, INode>("checkRootNode") { context, node -> - runCheck(ModelixNodeAsMPSNode.toMPSNode(node)) - } - - private val fGetRootNode = incrementalFunction("getRootNode") { context, node -> - if (node.getContainmentLink()?.getUID() == BuiltinLanguages.MPSRepositoryConcepts.Model.rootNodes.getUID()) { - node - } else { - getRootNode(node.parent ?: return@incrementalFunction node) + private val fGetRootNode = + incrementalFunction("getRootNode") { context, node -> + if (node.getContainmentLink()?.getUID() == + BuiltinLanguages.MPSRepositoryConcepts.Model.rootNodes + .getUID() + ) { + node + } else { + getRootNode(node.parent ?: return@incrementalFunction node) + } } - } @JvmStatic - fun getAllMessages(node: INode): List { - return checkRoot(getRootNode(node))[node.reference] ?: emptyList() - } + fun getAllMessages(node: INode): List = checkRoot(getRootNode(node))[node.reference] ?: emptyList() @JvmStatic - fun getAllMessages(node: SNode): List { - return getAllMessages(ModelixNodeAsMPSNode.toModelixNode(node)) - } + fun getAllMessages(node: SNode): List = getAllMessages(ModelixNodeAsMPSNode.toModelixNode(node)) @JvmStatic - fun getNodeMessages(node: SNode): List { - return getMessages(node, NodeMessageTarget()) - } + fun getNodeMessages(node: SNode): List = getMessages(node, NodeMessageTarget()) @JvmStatic - fun getMessages(node: SNode, feature: SConceptFeature?): List { - return getMessages(node, NodeReportItem.conceptFeatureToMessageTarget(feature)) - } + fun getMessages( + node: SNode, + feature: SConceptFeature?, + ): List = getMessages(node, NodeReportItem.conceptFeatureToMessageTarget(feature)) @JvmStatic - fun getMessages(node: SNode, target: MessageTarget): List { - return getAllMessages(node).filter { it.messageTarget.sameAs(target) } - } + fun getMessages( + node: SNode, + target: MessageTarget, + ): List = getAllMessages(node).filter { it.messageTarget.sameAs(target) } @JvmStatic @Deprecated("Provide an SConceptFeature") - fun getMessages(node: SNode, onlyGlobal: Boolean, featureName: String?): String { + fun getMessages( + node: SNode, + onlyGlobal: Boolean, + featureName: String?, + ): String { fun roleName(t: MessageTarget): String? { if (t is PropertyMessageTarget) { return t.role @@ -89,17 +93,16 @@ object ModelCheckerIntegration { return messages.groupBy { it.node.toModelix() } } - private fun getRootNode(node: INode): INode { - return fGetRootNode(node).bind(IRenderer.contextIncrementalEngine.getValue()).invoke() - } + private fun getRootNode(node: INode): INode = fGetRootNode(node).bind(IRenderer.contextIncrementalEngine.getValue()).invoke() private fun runCheck(root: SNode): List { val items = ArrayList() - val consumer: Consumer = object : Consumer { - override fun consume(item: NodeReportItem) { - items.add(item) + val consumer: Consumer = + object : Consumer { + override fun consume(item: NodeReportItem) { + items.add(item) + } } - } @Suppress("removal") val repository = MPSModuleRepository.getInstance() diff --git a/react-ssr-mps/src/main/kotlin/org/modelix/react/ssr/mps/OpenNodeInWebEditorAction.kt b/react-ssr-mps/src/main/kotlin/org/modelix/react/ssr/mps/OpenNodeInWebEditorAction.kt index 60a171ec..6d7f98d7 100644 --- a/react-ssr-mps/src/main/kotlin/org/modelix/react/ssr/mps/OpenNodeInWebEditorAction.kt +++ b/react-ssr-mps/src/main/kotlin/org/modelix/react/ssr/mps/OpenNodeInWebEditorAction.kt @@ -14,10 +14,7 @@ import java.net.URLEncoder import java.nio.charset.StandardCharsets class OpenNodeInWebEditorAction : DumbAwareAction() { - - override fun getActionUpdateThread(): ActionUpdateThread { - return ActionUpdateThread.EDT - } + override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.EDT override fun actionPerformed(event: AnActionEvent) { val project = event.project ?: return @@ -26,17 +23,20 @@ class OpenNodeInWebEditorAction : DumbAwareAction() { val nodeFile = NodeVirtualFileSystem.getInstance().getFileFor(mpsProject.repository, node) fun urlEncode(input: String) = URLEncoder.encode(input, StandardCharsets.UTF_8) + fun concatUrl(nodeRef: String) = "http://localhost:43595/nodes/${urlEncode(nodeRef)}/client/" + fun parseUrl(url: String) = Urls.parse(url, false)!! val nodeRef = ModelixNodeAsMPSNode.toModelixNode(node).reference.serialize() val expectedUrl = concatUrl(nodeRef) val parsedUrl = parseUrl(expectedUrl) - val workaroundUrl = if (parsedUrl.toExternalForm() == expectedUrl) { - parsedUrl - } else { - // double encode to work around a bug in IntelliJ - parseUrl(concatUrl(urlEncode(nodeRef))) - } + val workaroundUrl = + if (parsedUrl.toExternalForm() == expectedUrl) { + parsedUrl + } else { + // double encode to work around a bug in IntelliJ + parseUrl(concatUrl(urlEncode(nodeRef))) + } val file = WebPreviewVirtualFile(nodeFile, workaroundUrl) FileEditorManagerEx.getInstanceEx(project).openFile( diff --git a/react-ssr-mps/src/main/kotlin/org/modelix/react/ssr/mps/ReactSSRServerForMPS.kt b/react-ssr-mps/src/main/kotlin/org/modelix/react/ssr/mps/ReactSSRServerForMPS.kt index fd9bcb85..000c1cc5 100644 --- a/react-ssr-mps/src/main/kotlin/org/modelix/react/ssr/mps/ReactSSRServerForMPS.kt +++ b/react-ssr-mps/src/main/kotlin/org/modelix/react/ssr/mps/ReactSSRServerForMPS.kt @@ -48,8 +48,9 @@ import javax.swing.Icon import kotlin.time.Duration.Companion.seconds @Service(Service.Level.PROJECT) -class ReactSSRServerForMPSProject(private val project: Project) : Disposable { - +class ReactSSRServerForMPSProject( + private val project: Project, +) : Disposable { init { ApplicationManager.getApplication().service().registerProject(project) } @@ -61,7 +62,6 @@ class ReactSSRServerForMPSProject(private val project: Project) : Disposable { @Service(Service.Level.APP) class ReactSSRServerForMPS : Disposable { - companion object { fun getInstance() = ApplicationManager.getApplication().getService(ReactSSRServerForMPS::class.java) } @@ -71,11 +71,12 @@ class ReactSSRServerForMPS : Disposable { private var rendererFactory: MPSRendererFactory? = null private val projects: MutableSet = Collections.synchronizedSet(HashSet()) private val changeTranslator = MPSChangeTranslator() - private val commandLister = object : org.jetbrains.mps.openapi.repository.CommandListener { - override fun commandFinished() { - ssrServer?.updateAll() + private val commandLister = + object : org.jetbrains.mps.openapi.repository.CommandListener { + override fun commandFinished() { + ssrServer?.updateAll() + } } - } fun getKnownComponents(): List = ssrServer?.knownComponents ?: emptyList() @@ -92,21 +93,19 @@ class ReactSSRServerForMPS : Disposable { projects.remove(project) } - private fun getMPSProjects(): List { - return runSynchronized(projects) { + private fun getMPSProjects(): List = + runSynchronized(projects) { projects.mapNotNull { it.getComponent(MPSProject::class.java) } } - } - private fun getRepository(): SRepository { - return getMPSProjects().asSequence().map { - it.repository - }.firstOrNull() ?: MPSModuleRepository.getInstance() - } + private fun getRepository(): SRepository = + getMPSProjects() + .asSequence() + .map { + it.repository + }.firstOrNull() ?: MPSModuleRepository.getInstance() - private fun getRootNode(): INode { - return MPSRepositoryAsNode(getRepository()).asLegacyNode() - } + private fun getRootNode(): INode = MPSRepositoryAsNode(getRepository()).asLegacyNode() fun ensureStarted() { runSynchronized(this) { @@ -121,9 +120,10 @@ class ReactSSRServerForMPS : Disposable { MPSModuleRepository.getInstance().modelAccess.addCommandListener(commandLister) this.ssrServer = ssrServer - ktorServer = org.modelix.mps.editor.common.embeddedServer(port = 43595, classLoader = this.javaClass.classLoader) { - initKtorServer(ssrServer) - } + ktorServer = + org.modelix.mps.editor.common.embeddedServer(port = 43595, classLoader = this.javaClass.classLoader) { + initKtorServer(ssrServer) + } ktorServer!!.start() } @@ -203,15 +203,20 @@ fun ModelAccess.computeRead(body: () -> R): R { suspend fun ApplicationCall.respondIcon(icon: Icon) { val image = BufferedImage(icon.iconWidth, icon.iconHeight, BufferedImage.TYPE_INT_ARGB) icon.paintIcon(null, image.graphics, 0, 0) - val bytes = ByteArrayOutputStream().also { - it.use { - ImageIO.write(image, "png", it) - } - }.toByteArray() + val bytes = + ByteArrayOutputStream() + .also { + it.use { + ImageIO.write(image, "png", it) + } + }.toByteArray() respondBytes(bytes = bytes, contentType = ContentType.Image.PNG) } -private fun resolveIcon(cls: Class<*>, path: List): Icon? { +private fun resolveIcon( + cls: Class<*>, + path: List, +): Icon? { val remainingPath: List = path.drop(1) if (Sequence.fromIterable(remainingPath).isNotEmpty) { val nestedClasses = cls.declaredClasses diff --git a/react-ssr-mps/src/main/kotlin/org/modelix/react/ssr/mps/ToggleInterpretedRenderer.kt b/react-ssr-mps/src/main/kotlin/org/modelix/react/ssr/mps/ToggleInterpretedRenderer.kt index cdc65cb1..ec8f92cb 100644 --- a/react-ssr-mps/src/main/kotlin/org/modelix/react/ssr/mps/ToggleInterpretedRenderer.kt +++ b/react-ssr-mps/src/main/kotlin/org/modelix/react/ssr/mps/ToggleInterpretedRenderer.kt @@ -5,16 +5,14 @@ import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.project.DumbAwareAction class ToggleInterpretedRenderer : DumbAwareAction() { - - override fun getActionUpdateThread(): ActionUpdateThread { - return ActionUpdateThread.EDT - } + override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.EDT override fun actionPerformed(event: AnActionEvent) { ReactSSRServerForMPS.getInstance().toggleInterpreterMode() } override fun update(e: AnActionEvent) { - e.presentation.text = (if (ReactSSRServerForMPS.getInstance().isInterpreterMode()) "Disabled" else "Enable") + " Web Editor Interpreter Mode" + e.presentation.text = + (if (ReactSSRServerForMPS.getInstance().isInterpreterMode()) "Disabled" else "Enable") + " Web Editor Interpreter Mode" } } diff --git a/react-ssr-mps/src/main/kotlin/org/modelix/react/ssr/mps/aspect/DescriptorCache.kt b/react-ssr-mps/src/main/kotlin/org/modelix/react/ssr/mps/aspect/DescriptorCache.kt index 28c960bb..cd7b6d3f 100644 --- a/react-ssr-mps/src/main/kotlin/org/modelix/react/ssr/mps/aspect/DescriptorCache.kt +++ b/react-ssr-mps/src/main/kotlin/org/modelix/react/ssr/mps/aspect/DescriptorCache.kt @@ -15,7 +15,10 @@ import org.modelix.incremental.DependencyTracking import org.modelix.incremental.IStateVariableGroup import org.modelix.incremental.IStateVariableReference -class DescriptorCache(val descriptorClass: Class) : Disposable, IStateVariableReference { +class DescriptorCache( + val descriptorClass: Class, +) : Disposable, + IStateVariableReference { private var loadedDescriptors: MutableMap, E?> = HashMap() private var deployListener: DeployListener? = null @@ -34,43 +37,62 @@ class DescriptorCache(val descriptorClass: Class) : Disposable, ISta override fun dispose() { if (deployListener != null) { - val classLoaderManager = ApplicationManager.getApplication().getComponent( - MPSCoreComponents::class.java - ).classLoaderManager + val classLoaderManager = + ApplicationManager + .getApplication() + .getComponent( + MPSCoreComponents::class.java + ).classLoaderManager classLoaderManager.removeListener(deployListener!!) } loadedDescriptors = HashMap() } - fun getDescriptor(module: SModule, modelAndClassName: String): E? { + fun getDescriptor( + module: SModule, + modelAndClassName: String, + ): E? { DependencyTracking.accessed(this) - val descriptor = getDescriptor_(module, modelAndClassName) + val descriptor = getDescriptor1(module, modelAndClassName) if (descriptor != null) { if (deployListener == null) { - deployListener = object : DeployListener { - override fun onUnloaded(modules: Set, p1: ProgressMonitor) { - invalidate() - } + deployListener = + object : DeployListener { + override fun onUnloaded( + modules: Set, + p1: ProgressMonitor, + ) { + invalidate() + } - override fun onLoaded(modules: Set, p1: ProgressMonitor) { - invalidate() + override fun onLoaded( + modules: Set, + p1: ProgressMonitor, + ) { + invalidate() + } + }.also { + // The non deprecated API doesn't work when executing tests from the command line, because getApplication returns NULL. + val classLoaderManager = ClassLoaderManager.getInstance() + classLoaderManager.addListener(it) } - }.also { - // The non deprecated API doesn't work when executing tests from the command line, because getApplication returns NULL. - val classLoaderManager = ClassLoaderManager.getInstance() - classLoaderManager.addListener(it) - } } } return descriptor } - protected fun getDescriptor_(module: SModule, modelAndClassName: String): E? { + private fun getDescriptor1( + module: SModule, + modelAndClassName: String, + ): E? { if (module !is ReloadableModule) return null - return loadedDescriptors.getOrPut(module to modelAndClassName) { getDescriptor__(module, modelAndClassName) } + return loadedDescriptors.getOrPut(module to modelAndClassName) { getDescriptor0(module, modelAndClassName) } } - protected fun getDescriptor__(module: ReloadableModule, modelAndClassName: String): E? { + private fun getDescriptor0( + module: ReloadableModule, + modelAndClassName: String, + ): E? { val className = module.moduleName + "." + modelAndClassName try { val cls = module.getOwnClass(className) diff --git a/react-ssr-mps/src/main/kotlin/org/modelix/react/ssr/mps/aspect/ReactPageDescriptor.kt b/react-ssr-mps/src/main/kotlin/org/modelix/react/ssr/mps/aspect/ReactPageDescriptor.kt index 506157cd..6eba3d1b 100644 --- a/react-ssr-mps/src/main/kotlin/org/modelix/react/ssr/mps/aspect/ReactPageDescriptor.kt +++ b/react-ssr-mps/src/main/kotlin/org/modelix/react/ssr/mps/aspect/ReactPageDescriptor.kt @@ -5,11 +5,18 @@ import org.modelix.react.ssr.server.RendererCall interface IReactPageDescriptor { fun getPath(): PagePath - fun getRoot(repository: IReadableNode, pathParameterValues: Map): RendererCall + + fun getRoot( + repository: IReadableNode, + pathParameterValues: Map, + ): RendererCall } -class PagePath(val parts: List) { +class PagePath( + val parts: List, +) { override fun toString(): String = parts.joinToString("/") + fun match(actualParts: List): Map? { if (actualParts.size != parts.size) return null val assignedValues = HashMap() @@ -21,15 +28,32 @@ class PagePath(val parts: List) { } sealed class PagePathPart { - abstract fun matches(actualPart: String, assignedValues: MutableMap): Boolean + abstract fun matches( + actualPart: String, + assignedValues: MutableMap, + ): Boolean } -data class StaticPagePathPart(val value: String) : PagePathPart() { + +data class StaticPagePathPart( + val value: String, +) : PagePathPart() { override fun toString(): String = value - override fun matches(actualPart: String, assignedValues: MutableMap): Boolean = value == actualPart + + override fun matches( + actualPart: String, + assignedValues: MutableMap, + ): Boolean = value == actualPart } -data class VariablePagePathPart(val name: String) : PagePathPart() { + +data class VariablePagePathPart( + val name: String, +) : PagePathPart() { override fun toString(): String = "{$name}" - override fun matches(actualPart: String, assignedValues: MutableMap): Boolean { + + override fun matches( + actualPart: String, + assignedValues: MutableMap, + ): Boolean { assignedValues[name] = actualPart return true } diff --git a/react-ssr-mps/src/main/kotlin/org/modelix/react/ssr/mps/aspect/ReactSSRAspectDescriptor.kt b/react-ssr-mps/src/main/kotlin/org/modelix/react/ssr/mps/aspect/ReactSSRAspectDescriptor.kt index b5065c89..bd8e16df 100644 --- a/react-ssr-mps/src/main/kotlin/org/modelix/react/ssr/mps/aspect/ReactSSRAspectDescriptor.kt +++ b/react-ssr-mps/src/main/kotlin/org/modelix/react/ssr/mps/aspect/ReactSSRAspectDescriptor.kt @@ -14,23 +14,26 @@ import org.modelix.react.ssr.server.RendererCall import org.modelix.react.ssr.server.RendererSignature class ReactSSRAspectDescriptors : Disposable { - private val descriptorCache: DescriptorCache = DescriptorCache(IReactSSRAspectDescriptor::class.java).also { Disposer.register(this, it) } + private val descriptorCache: DescriptorCache = + DescriptorCache(IReactSSRAspectDescriptor::class.java).also { + Disposer.register(this, it) + } override fun dispose() {} - fun findDescriptors(repository: SRepository): List { - return repository.computeRead { repository.modules.mapNotNull { descriptorCache.getDescriptor(it, "modelix.ReactDescriptor") } } - } + fun findDescriptors(repository: SRepository): List = + repository.computeRead { + repository.modules.mapNotNull { descriptorCache.getDescriptor(it, "modelix.ReactDescriptor") } + } } -data class CompositeReactSSRAspectDescriptor(val descriptors: Set) : IReactSSRAspectDescriptor { - override fun getRenderersForConcept(concept: ConceptReference): List { - return descriptors.flatMap { it.getRenderersForConcept(concept) } - } +data class CompositeReactSSRAspectDescriptor( + val descriptors: Set, +) : IReactSSRAspectDescriptor { + override fun getRenderersForConcept(concept: ConceptReference): List = + descriptors.flatMap { it.getRenderersForConcept(concept) } - override fun getRenderers(signature: RendererSignature): List { - return descriptors.flatMap { it.getRenderers(signature) } - } + override fun getRenderers(signature: RendererSignature): List = descriptors.flatMap { it.getRenderers(signature) } override fun getPages(): List = descriptors.flatMap { it.getPages() } } @@ -40,6 +43,7 @@ interface IReactSSRAspectDescriptor { * Only for the exact concept, not for super concepts. */ fun getRenderersForConcept(concept: ConceptReference): List + fun getRenderers(signature: RendererSignature): List fun getPages(): List @@ -48,24 +52,27 @@ interface IReactSSRAspectDescriptor { abstract class ReactSSRAspectDescriptorBase : IReactSSRAspectDescriptor { private val renderers: MutableMap> = HashMap() private val pages: MutableList = ArrayList() - override fun getRenderersForConcept(concept: ConceptReference): List { - return getRenderers(ConceptRendererSignature(concept)) - } - override fun getRenderers(signature: RendererSignature): List { - return renderers[signature] ?: emptyList() - } - protected fun addRenderer(concept: ConceptReference, renderer: IReactNodeRenderer) { + override fun getRenderersForConcept(concept: ConceptReference): List = + getRenderers(ConceptRendererSignature(concept)) + + override fun getRenderers(signature: RendererSignature): List = renderers[signature] ?: emptyList() + + protected fun addRenderer( + concept: ConceptReference, + renderer: IReactNodeRenderer, + ) { addRenderer(ConceptRendererSignature(concept), renderer) } - protected fun addRenderer(signature: RendererSignature, renderer: IReactNodeRenderer) { + protected fun addRenderer( + signature: RendererSignature, + renderer: IReactNodeRenderer, + ) { renderers[signature] = (renderers[signature] ?: emptyList()) + renderer } - override fun getPages(): List { - return pages - } + override fun getPages(): List = pages protected fun addPage(page: IReactPageDescriptor) { pages.add(page) @@ -74,16 +81,42 @@ abstract class ReactSSRAspectDescriptorBase : IReactSSRAspectDescriptor { interface IReactNodeRenderer { fun isApplicable(node: INode): Boolean - fun render(node: INode, context: IRenderContext): IComponentOrList - fun render(call: RendererCall, context: IRenderContext): IComponentOrList + + fun render( + node: INode, + context: IRenderContext, + ): IComponentOrList + + fun render( + call: RendererCall, + context: IRenderContext, + ): IComponentOrList } interface IRenderContext { fun getIncrementalEngine(): IIncrementalEngine + fun renderNode(node: INode): IComponentOrList = callRenderer(NodeRendererCall(node.asReadableNode())) + fun callRenderer(call: RendererCall): IComponentOrList - fun getState(id: String, defaultValue: String?): String? - fun setState(id: String, value: String?): String? - fun getState(id: String, defaultValue: Boolean): Boolean - fun setState(id: String, value: Boolean): Boolean + + fun getState( + id: String, + defaultValue: String?, + ): String? + + fun setState( + id: String, + value: String?, + ): String? + + fun getState( + id: String, + defaultValue: Boolean, + ): Boolean + + fun setState( + id: String, + value: Boolean, + ): Boolean } diff --git a/react-ssr-server/build.gradle.kts b/react-ssr-server/build.gradle.kts index 1c10fda8..18531bbe 100644 --- a/react-ssr-server/build.gradle.kts +++ b/react-ssr-server/build.gradle.kts @@ -18,11 +18,12 @@ kotlin { jvmToolchain(17) } -val copyClient = tasks.register("copyClient", Sync::class.java) { - dependsOn(project(":react-ssr-client").tasks.named("yarn_run_build")) - from(project(":react-ssr-client").layout.projectDirectory.dir("dist")) - into(project.layout.buildDirectory.dir("client/org/modelix/react/ssr/client")) -} +val copyClient = + tasks.register("copyClient", Sync::class.java) { + dependsOn(project(":react-ssr-client").tasks.named("yarn_run_build")) + from(project(":react-ssr-client").layout.projectDirectory.dir("dist")) + into(project.layout.buildDirectory.dir("client/org/modelix/react/ssr/client")) + } tasks.processResources { dependsOn(copyClient) diff --git a/react-ssr-server/src/main/kotlin/org/modelix/react/ssr/server/ComponentBuilder.kt b/react-ssr-server/src/main/kotlin/org/modelix/react/ssr/server/ComponentBuilder.kt index 93e4ab33..183acf96 100644 --- a/react-ssr-server/src/main/kotlin/org/modelix/react/ssr/server/ComponentBuilder.kt +++ b/react-ssr-server/src/main/kotlin/org/modelix/react/ssr/server/ComponentBuilder.kt @@ -10,21 +10,24 @@ import kotlinx.serialization.json.encodeToJsonElement import org.intellij.lang.annotations.Language import java.util.concurrent.atomic.AtomicLong -fun buildViewModel(body: ViewModelBuilder.() -> Unit): ViewModel { - return ViewModelBuilder().apply(body).build() -} +fun buildViewModel(body: ViewModelBuilder.() -> Unit): ViewModel = ViewModelBuilder().apply(body).build() -fun buildComponent(type: String, body: ComponentBuilder.() -> Unit): Component { - return ComponentBuilder(type).apply(body).build() -} +fun buildComponent( + type: String, + body: ComponentBuilder.() -> Unit, +): Component = ComponentBuilder(type).apply(body).build() abstract class ComponentContainerBuilder { private val children: MutableList = ArrayList() - fun component(type: String, body: ComponentBuilder.() -> Unit) { - children += ComponentOrText( - component = ComponentBuilder(type).apply(body).build() - ) + fun component( + type: String, + body: ComponentBuilder.() -> Unit, + ) { + children += + ComponentOrText( + component = ComponentBuilder(type).apply(body).build() + ) } fun component(component: Component) { @@ -47,45 +50,48 @@ abstract class ComponentContainerBuilder { children += ComponentOrText(text = text) } - fun buildComponents(): List { - return children - } + fun buildComponents(): List = children } class ViewModelBuilder : ComponentContainerBuilder() { - - fun build(): ViewModel { - return ViewModel(buildComponents()) - } + fun build(): ViewModel = ViewModel(buildComponents()) } class ComponentBuilder( private val type: String, private val propertiesBuilder: JsonObjectBuilder = JsonObjectBuilder(), -) : ComponentContainerBuilder(), IJsonObjectBuilder by propertiesBuilder, ICustomHandlerBuilder { +) : ComponentContainerBuilder(), + IJsonObjectBuilder by propertiesBuilder, + ICustomHandlerBuilder { var key: String? = null private val customHandlers: MutableMap = HashMap() - fun build(): Component { - return Component( + fun build(): Component = + Component( type = type, key = key, properties = propertiesBuilder.build().takeIf { it.isNotEmpty() }, children = buildComponents().takeIf { it.isNotEmpty() }, customMessageHandlers = customHandlers.takeIf { it.isNotEmpty() } ) - } fun key(vararg parts: String) { this.key = parts.joinToString("-") } - fun customHandler(name: String, builderBody: CustomHandlerBuilder.() -> Unit, serverSideHandler: ICustomMessageHandler) { + fun customHandler( + name: String, + builderBody: CustomHandlerBuilder.() -> Unit, + serverSideHandler: ICustomMessageHandler, + ) { val code = buildCustomHandler(builderBody, serverSideHandler) jsCodeProperty(name, code.jsCode) } - override fun buildCustomHandler(builderBody: CustomHandlerBuilder.() -> Unit, serverSideHandler: ICustomMessageHandler): JsCode { + override fun buildCustomHandler( + builderBody: CustomHandlerBuilder.() -> Unit, + serverSideHandler: ICustomMessageHandler, + ): JsCode { val builder = CustomHandlerBuilder().also(builderBody) val code = builder.build() customHandlers[builder.handlerId] = serverSideHandler @@ -98,119 +104,214 @@ class ComponentBuilder( } interface ICustomHandlerBuilder { - fun buildCustomHandler(builderBody: CustomHandlerBuilder.() -> Unit, serverSideHandler: ICustomMessageHandler): JsCode + fun buildCustomHandler( + builderBody: CustomHandlerBuilder.() -> Unit, + serverSideHandler: ICustomMessageHandler, + ): JsCode + fun registerHandlers(h: Map) + fun registerHandlers(returnValue: ReturnValueWithCustomHandlers): R { registerHandlers(returnValue.handlers) return returnValue.value } } -data class ReturnValueWithCustomHandlers(val value: R, val handlers: Map) +data class ReturnValueWithCustomHandlers( + val value: R, + val handlers: Map, +) fun functionWithCustomHandlers(body: ICustomHandlerBuilder.() -> R): ReturnValueWithCustomHandlers { var handlers: Map = emptyMap() - val builder = object : ICustomHandlerBuilder { - override fun buildCustomHandler( - builderBody: CustomHandlerBuilder.() -> Unit, - serverSideHandler: ICustomMessageHandler, - ): JsCode { - val builder = CustomHandlerBuilder().also(builderBody) - val code = builder.build() - handlers += builder.handlerId to serverSideHandler - return code - } + val builder = + object : ICustomHandlerBuilder { + override fun buildCustomHandler( + builderBody: CustomHandlerBuilder.() -> Unit, + serverSideHandler: ICustomMessageHandler, + ): JsCode { + val builder = CustomHandlerBuilder().also(builderBody) + val code = builder.build() + handlers += builder.handlerId to serverSideHandler + return code + } - override fun registerHandlers(h: Map) { - handlers += h + override fun registerHandlers(h: Map) { + handlers += h + } } - } return ReturnValueWithCustomHandlers(body(builder), handlers) } interface IJsonObjectBuilder { - fun property(name: String, value: String?) - fun property(name: String, value: Number?) - fun property(name: String, value: Boolean?) - fun property(name: String, value: JsonElement) - fun property(name: String, value: ComponentOrText) - fun property(name: String, value: Component) - fun property(name: String, value: JsCode) - fun jsonObjectProperty(name: String, body: IJsonObjectBuilder.() -> Unit) - fun componentProperty(name: String, type: String, body: ComponentBuilder.() -> Unit) - fun jsCodeProperty(name: String, @Language("JavaScript") code: String) - fun messageSendingHandler(name: String, messageId: String, body: MessageSendingHandlerBuilder.() -> Unit) + fun property( + name: String, + value: String?, + ) + + fun property( + name: String, + value: Number?, + ) + + fun property( + name: String, + value: Boolean?, + ) + + fun property( + name: String, + value: JsonElement, + ) + + fun property( + name: String, + value: ComponentOrText, + ) + + fun property( + name: String, + value: Component, + ) + + fun property( + name: String, + value: JsCode, + ) + + fun jsonObjectProperty( + name: String, + body: IJsonObjectBuilder.() -> Unit, + ) + + fun componentProperty( + name: String, + type: String, + body: ComponentBuilder.() -> Unit, + ) + + fun jsCodeProperty( + name: String, + @Language("JavaScript") code: String, + ) + + fun messageSendingHandler( + name: String, + messageId: String, + body: MessageSendingHandlerBuilder.() -> Unit, + ) } -fun buildJsonObject(body: JsonObjectBuilder.() -> Unit): JsonObject { - return JsonObjectBuilder().apply(body).build() -} +fun buildJsonObject(body: JsonObjectBuilder.() -> Unit): JsonObject = JsonObjectBuilder().apply(body).build() class JsonObjectBuilder : IJsonObjectBuilder { private val properties: MutableMap = LinkedHashMap() - override fun property(name: String, value: String?) { + override fun property( + name: String, + value: String?, + ) { properties[name] = JsonPrimitive(value) } - override fun property(name: String, value: Number?) { + override fun property( + name: String, + value: Number?, + ) { properties[name] = JsonPrimitive(value) } - override fun property(name: String, value: Boolean?) { + override fun property( + name: String, + value: Boolean?, + ) { properties[name] = JsonPrimitive(value) } - override fun property(name: String, value: JsonElement) { + override fun property( + name: String, + value: JsonElement, + ) { properties[name] = value } - override fun property(name: String, value: Component) { + override fun property( + name: String, + value: Component, + ) { properties[name] = Json.encodeToJsonElement(value) } - override fun property(name: String, value: ComponentOrText) { + override fun property( + name: String, + value: ComponentOrText, + ) { value.component?.let { properties[name] = Json.encodeToJsonElement(it) } value.text?.let { properties[name] = JsonPrimitive(it) } } - override fun property(name: String, value: JsCode) { + override fun property( + name: String, + value: JsCode, + ) { properties[name] = Json.encodeToJsonElement(value) } - override fun jsonObjectProperty(name: String, body: IJsonObjectBuilder.() -> Unit) { + override fun jsonObjectProperty( + name: String, + body: IJsonObjectBuilder.() -> Unit, + ) { property(name, buildJsonObject(body)) } - override fun componentProperty(name: String, type: String, body: ComponentBuilder.() -> Unit) { + override fun componentProperty( + name: String, + type: String, + body: ComponentBuilder.() -> Unit, + ) { val comp = ComponentBuilder(type).apply(body).build() properties[name] = Json.encodeToJsonElement(comp) } - override fun jsCodeProperty(name: String, @Language("JavaScript") code: String) { + override fun jsCodeProperty( + name: String, + @Language("JavaScript") code: String, + ) { properties[name] = Json.encodeToJsonElement(JsCode(code)) } - override fun messageSendingHandler(name: String, messageId: String, body: MessageSendingHandlerBuilder.() -> Unit) { + override fun messageSendingHandler( + name: String, + messageId: String, + body: MessageSendingHandlerBuilder.() -> Unit, + ) { jsCodeProperty(name, buildMessageSendingHandler(messageId, body)) } - fun build(): JsonObject { - return JsonObject(properties) - } + fun build(): JsonObject = JsonObject(properties) } -fun buildJsonArray(body: JsonArrayBuilder.() -> Unit): JsonArray { - return JsonArrayBuilder().apply(body).build() -} +fun buildJsonArray(body: JsonArrayBuilder.() -> Unit): JsonArray = JsonArrayBuilder().apply(body).build() class JsonArrayBuilder { private val elements: MutableList = ArrayList() - fun element(value: String) { elements.add(JsonPrimitive(value)) } - fun element(value: Number) { elements.add(JsonPrimitive(value)) } - fun element(value: Boolean) { elements.add(JsonPrimitive(value)) } - fun element(value: JsonElement) { elements.add(value) } + fun element(value: String) { + elements.add(JsonPrimitive(value)) + } + + fun element(value: Number) { + elements.add(JsonPrimitive(value)) + } + + fun element(value: Boolean) { + elements.add(JsonPrimitive(value)) + } + + fun element(value: JsonElement) { + elements.add(value) + } + fun jsonObject(body: JsonObjectBuilder.() -> Unit) { elements.add(buildJsonObject(body)) } @@ -218,11 +319,14 @@ class JsonArrayBuilder { fun build(): JsonArray = JsonArray(elements) } -fun buildMessageSendingHandler(messageId: String, body: MessageSendingHandlerBuilder.() -> Unit): String { - return MessageSendingHandlerBuilder(messageId).apply(body).build() -} +fun buildMessageSendingHandler( + messageId: String, + body: MessageSendingHandlerBuilder.() -> Unit, +): String = MessageSendingHandlerBuilder(messageId).apply(body).build() -class MessageSendingHandlerBuilder(val messageId: String) { +class MessageSendingHandlerBuilder( + val messageId: String, +) { private val functionParameters: MutableList = ArrayList() private val messageParameters: MutableMap = LinkedHashMap() @@ -230,22 +334,31 @@ class MessageSendingHandlerBuilder(val messageId: String) { functionParameters += name } - fun constantParameter(name: String, value: JsonElement) { + fun constantParameter( + name: String, + value: JsonElement, + ) { messageParameters[name] = Json.encodeToString(value) } - fun constantParameter(name: String, value: String) { + fun constantParameter( + name: String, + value: String, + ) { constantParameter(name, JsonPrimitive(value)) } - fun codeParameter(name: String, @Language("JavaScript") expression: String) { + fun codeParameter( + name: String, + @Language("JavaScript") expression: String, + ) { messageParameters[name] = expression } - fun build(): String { - return """ + fun build(): String = + """ (${functionParameters.joinToString(",")}) => window.modelix.sendMessage({ - ${MessageFromClient::messageId.name}: "$messageId", + ${MessageFromClient::messageId.name}: "$messageId", ${MessageFromClient::parameters.name}: { ${ messageParameters.entries.joinToString(", ") { @@ -255,10 +368,10 @@ class MessageSendingHandlerBuilder(val messageId: String) { } }) """ - } } private val customHandlerIdSequence = AtomicLong(0) + class CustomHandlerBuilder { var handlerId: String = customHandlerIdSequence.incrementAndGet().toString(16) private val clientSideParameters: MutableList = ArrayList() @@ -272,12 +385,15 @@ class CustomHandlerBuilder { clientSideParameters += name } - fun serverSideParameter(name: String, @Language("JavaScript") valueJsCode: String) { + fun serverSideParameter( + name: String, + @Language("JavaScript") valueJsCode: String, + ) { serverSideParameters[name] = valueJsCode } - fun build(): JsCode { - return JsCode( + fun build(): JsCode = + JsCode( """ (${clientSideParameters.joinToString(",")}) => window.modelix.sendMessage({ ${MessageFromClient::messageId.name}: "callCustomHandler", @@ -294,5 +410,4 @@ class CustomHandlerBuilder { }) """ ) - } } diff --git a/react-ssr-server/src/main/kotlin/org/modelix/react/ssr/server/GenericNodeRenderer.kt b/react-ssr-server/src/main/kotlin/org/modelix/react/ssr/server/GenericNodeRenderer.kt index c2bd9332..79c2e9d2 100644 --- a/react-ssr-server/src/main/kotlin/org/modelix/react/ssr/server/GenericNodeRenderer.kt +++ b/react-ssr-server/src/main/kotlin/org/modelix/react/ssr/server/GenericNodeRenderer.kt @@ -12,8 +12,11 @@ import org.modelix.model.api.IProperty import org.modelix.model.api.NodeReference import org.modelix.model.api.toSerialized -abstract class GenericNodeRenderer(val incremenentalEngine: IIncrementalEngine, private val nodeRef: RendererCall, val coroutineScope: CoroutineScope) : RendererBase() { - +abstract class GenericNodeRenderer( + val incremenentalEngine: IIncrementalEngine, + private val nodeRef: RendererCall, + val coroutineScope: CoroutineScope, +) : RendererBase() { init { registerMessageHandler("changeProperty") { message -> val serializedNodeRef = message.getStringProperty("node")!! @@ -28,26 +31,29 @@ abstract class GenericNodeRenderer(val incremenentalEngine: IIncrementalEngine, } abstract override fun runRead(body: () -> R): R + abstract fun resolveNode(nodeRef: NodeReference): INode? - override fun doRender(): ViewModel { - return try { + override fun doRender(): ViewModel = + try { runRead { when (nodeRef) { is NodeRefRendererCall -> { val node = requireNotNull(resolveNode(nodeRef.node.toSerialized())) { "Node not found: $nodeRef" } renderNodeEditor(NodeRendererCall(node.asReadableNode())) } - else -> renderNodeEditor(nodeRef) + + else -> { + renderNodeEditor(nodeRef) + } } } } catch (ex: Exception) { renderError("Failed loading $nodeRef: " + ex.message + "\n" + ex.stackTraceToString()) } - } - fun renderError(message: String): ViewModel { - return buildViewModel { + fun renderError(message: String): ViewModel = + buildViewModel { component("mui.Alert") { property("severity", "error") component("html.pre") { @@ -55,83 +61,86 @@ abstract class GenericNodeRenderer(val incremenentalEngine: IIncrementalEngine, } } } - } - open fun renderNodeEditor(node: RendererCall): ViewModel { - return buildViewModel { + open fun renderNodeEditor(node: RendererCall): ViewModel = + buildViewModel { child(renderNode(node)) } - } fun renderNode(node: RendererCall): ComponentOrText = renderNodeIncremental(node) - private val renderNodeIncremental = incremenentalEngine.incrementalFunction("renderNode") { _, call: RendererCall -> - if (call !is NodeRendererCall) return@incrementalFunction ComponentOrText(text = "renderer not found for $call") - val node = call.node.asLegacyNode() - val text = (node.concept?.getShortName().toString()) + - " [" + - node.getAllProperties().joinToString(", ") { "${it.first.getSimpleName()}=${it.second}" } + - "]" - - val nodeId = node.reference.serialize() - val stateId = "accordion-expanded-" + nodeId - val isExpanded = (allStates[stateId] as? JsonPrimitive)?.booleanOrNull ?: false - - return@incrementalFunction buildComponent("mui.Accordion") { - key(nodeId) - - messageSendingHandler("onChange", "changeState") { - inputParameter("event") - inputParameter("isExpanded") - constantParameter("key", stateId) - codeParameter("value", """isExpanded""") - } - component("mui.AccordionSummary") { - componentProperty("expandIcon", "mui.icons.ExpandMore") {} - text(text) - } - component("mui.AccordionDetails") { - if (isExpanded) { - renderProperties(node) - - for (link in node.concept!!.getAllReferenceLinks()) { - fun createEntry(target: INode): JsonObject { - val label = target.getAllProperties().find { it.first.getSimpleName() == "name" }?.second ?: target.reference.serialize() - return buildJsonObject { - property("label", label) - property("target", target.reference.serialize()) + private val renderNodeIncremental = + incremenentalEngine.incrementalFunction("renderNode") { _, call: RendererCall -> + if (call !is NodeRendererCall) return@incrementalFunction ComponentOrText(text = "renderer not found for $call") + val node = call.node.asLegacyNode() + val text = + (node.concept?.getShortName().toString()) + + " [" + + node.getAllProperties().joinToString(", ") { "${it.first.getSimpleName()}=${it.second}" } + + "]" + + val nodeId = node.reference.serialize() + val stateId = "accordion-expanded-" + nodeId + val isExpanded = (allStates[stateId] as? JsonPrimitive)?.booleanOrNull ?: false + + return@incrementalFunction buildComponent("mui.Accordion") { + key(nodeId) + + messageSendingHandler("onChange", "changeState") { + inputParameter("event") + inputParameter("isExpanded") + constantParameter("key", stateId) + codeParameter("value", """isExpanded""") + } + + component("mui.AccordionSummary") { + componentProperty("expandIcon", "mui.icons.ExpandMore") {} + text(text) + } + component("mui.AccordionDetails") { + if (isExpanded) { + renderProperties(node) + + for (link in node.concept!!.getAllReferenceLinks()) { + fun createEntry(target: INode): JsonObject { + val label = + target.getAllProperties().find { it.first.getSimpleName() == "name" }?.second + ?: target.reference.serialize() + return buildJsonObject { + property("label", label) + property("target", target.reference.serialize()) + } + } + val target = node.getReferenceTarget(link) + val entries = listOfNotNull(target).map { createEntry(it) } + component("modelix.ReferenceTargetChooser") { + property("linkName", link.getSimpleName()) + target?.let { property("selected", createEntry(it)) } + property("entries", JsonArray(entries)) } } - val target = node.getReferenceTarget(link) - val entries = listOfNotNull(target).map { createEntry(it) } - component("modelix.ReferenceTargetChooser") { - property("linkName", link.getSimpleName()) - target?.let { property("selected", createEntry(it)) } - property("entries", JsonArray(entries)) - } - } - for (child in node.allChildren) { - child(renderNode(NodeRendererCall(child.asReadableNode()))) - } - } else { - component("mui.Stack") { - property("spacing", 1) - component("mui.Skeleton") { - property("variant", "rectangular") - property("width", 300) - property("height", 50) + for (child in node.allChildren) { + child(renderNode(NodeRendererCall(child.asReadableNode()))) } - component("mui.Skeleton") { - property("variant", "rectangular") - property("width", 300) - property("height", 50) + } else { + component("mui.Stack") { + property("spacing", 1) + component("mui.Skeleton") { + property("variant", "rectangular") + property("width", 300) + property("height", 50) + } + component("mui.Skeleton") { + property("variant", "rectangular") + property("width", 300) + property("height", 50) + } } } } - } - }.let { ComponentOrText(component = it) } - } + }.let { ComponentOrText(component = it) } + } private fun ComponentBuilder.renderProperties(node: INode) { component("mui.Paper") { diff --git a/react-ssr-server/src/main/kotlin/org/modelix/react/ssr/server/Messages.kt b/react-ssr-server/src/main/kotlin/org/modelix/react/ssr/server/Messages.kt index f3c00c99..7093fcdb 100644 --- a/react-ssr-server/src/main/kotlin/org/modelix/react/ssr/server/Messages.kt +++ b/react-ssr-server/src/main/kotlin/org/modelix/react/ssr/server/Messages.kt @@ -11,9 +11,7 @@ data class MessageFromClient( val messageId: String, val parameters: Map? = null, ) { - fun getStringProperty(name: String): String? { - return parameters?.get(name)?.jsonPrimitive?.content - } + fun getStringProperty(name: String): String? = parameters?.get(name)?.jsonPrimitive?.content } @Serializable @@ -57,7 +55,9 @@ interface ICustomMessageHandlerParameters { fun getString(name: String): String? } -class JsonObjectAsCustomMessageHandlerParameters(val obj: JsonObject) : ICustomMessageHandlerParameters { +class JsonObjectAsCustomMessageHandlerParameters( + val obj: JsonObject, +) : ICustomMessageHandlerParameters { override fun getString(name: String): String? = obj.get(name)?.jsonPrimitive?.content } @@ -66,13 +66,11 @@ sealed interface IComponentOrList { companion object { @JvmStatic - fun create(vararg parameters: Any?): IComponentOrList { - return fromSequence(parameters.asSequence()) - } + fun create(vararg parameters: Any?): IComponentOrList = fromSequence(parameters.asSequence()) @JvmStatic - fun create(parameter: Any?): IComponentOrList { - return when (parameter) { + fun create(parameter: Any?): IComponentOrList = + when (parameter) { null -> ComponentsList(emptyList()) is String -> ComponentOrText(text = parameter) is Component -> ComponentOrText(component = parameter) @@ -82,18 +80,21 @@ sealed interface IComponentOrList { is Sequence -> fromSequence(parameter) else -> throw IllegalArgumentException("Unsupported: $parameter") } - } fun fromSequence(seq: Sequence): IComponentOrList { - val elements = seq.map { create(it) } - .flatMap { it.flatten() } - .toList() + val elements = + seq + .map { create(it) } + .flatMap { it.flatten() } + .toList() return if (elements.size == 1) elements.single() else ComponentsList(elements) } } } -data class ComponentsList(val components: List) : IComponentOrList { +data class ComponentsList( + val components: List, +) : IComponentOrList { override fun flatten(): List = components } @@ -104,9 +105,7 @@ data class ComponentOrText( ) : IComponentOrList { override fun flatten(): List = listOf(this) - fun findHandler(id: String): ICustomMessageHandler? { - return component?.findHandler(id) - } + fun findHandler(id: String): ICustomMessageHandler? = component?.findHandler(id) } @Serializable diff --git a/react-ssr-server/src/main/kotlin/org/modelix/react/ssr/server/ReactSSRServer.kt b/react-ssr-server/src/main/kotlin/org/modelix/react/ssr/server/ReactSSRServer.kt index 8dcdcd88..edbc932c 100644 --- a/react-ssr-server/src/main/kotlin/org/modelix/react/ssr/server/ReactSSRServer.kt +++ b/react-ssr-server/src/main/kotlin/org/modelix/react/ssr/server/ReactSSRServer.kt @@ -44,8 +44,19 @@ fun main() { } interface IRendererFactory { - fun createRenderer(incrementalEngine: IIncrementalEngine, nodeRef: RendererCall, parameters: Map>, coroutineScope: CoroutineScope): IRenderer - fun createPageRenderer(incrementalEngine: IIncrementalEngine, pathParts: List, parameters: Map>, coroutineScope: CoroutineScope): IRenderer + fun createRenderer( + incrementalEngine: IIncrementalEngine, + nodeRef: RendererCall, + parameters: Map>, + coroutineScope: CoroutineScope, + ): IRenderer + + fun createPageRenderer( + incrementalEngine: IIncrementalEngine, + pathParts: List, + parameters: Map>, + coroutineScope: CoroutineScope, + ): IRenderer } interface IRenderer { @@ -54,20 +65,19 @@ interface IRenderer { } fun runRead(body: () -> R): R + fun render(): ViewModel + suspend fun messageReceived(message: MessageFromClient) } class DefaultRendererFactory : IRendererFactory { - override fun createRenderer( incrementalEngine: IIncrementalEngine, nodeRef: RendererCall, parameters: Map>, coroutineScope: CoroutineScope, - ): IRenderer { - return Renderer(nodeRef) - } + ): IRenderer = Renderer(nodeRef) override fun createPageRenderer( incrementalEngine: IIncrementalEngine, @@ -80,24 +90,28 @@ class DefaultRendererFactory : IRendererFactory { return createRenderer(incrementalEngine, NodeRefRendererCall(pathParts[1]), parameters, coroutineScope) } - class Renderer(val nodeRef: RendererCall) : IRenderer { - override fun render(): ViewModel { - return ViewModel( - children = listOf( - ComponentOrText( - text = "No renderer defined for $nodeRef" + class Renderer( + val nodeRef: RendererCall, + ) : IRenderer { + override fun render(): ViewModel = + ViewModel( + children = + listOf( + ComponentOrText( + text = "No renderer defined for $nodeRef" + ) ) - ) ) - } override suspend fun messageReceived(message: MessageFromClient) {} + override fun runRead(body: () -> R): R = body() } } -class ReactSSRServer(val rendererFactory: IRendererFactory = DefaultRendererFactory()) { - +class ReactSSRServer( + val rendererFactory: IRendererFactory = DefaultRendererFactory(), +) { private val coroutinesScope = CoroutineScope(Dispatchers.Default) private val allUpdaters: MutableSet<() -> Unit> = Collections.synchronizedSet(HashSet()) var knownComponents: List = emptyList() @@ -140,30 +154,38 @@ class ReactSSRServer(val rendererFactory: IRendererFactory = DefaultRendererFact get { val parts: List = call.parameters.getAll("parts").orEmpty() val rootPath = parts.joinToString("/") { ".." }.ifEmpty { "." } - val indexHtml = ReactSSRServer::class.java.classLoader.getResourceAsStream("org/modelix/react/ssr/client/index.html") - .use { it.reader().readText() } - .replace("", "\n ") + val indexHtml = + ReactSSRServer::class.java.classLoader + .getResourceAsStream("org/modelix/react/ssr/client/index.html") + .use { it.reader().readText() } + .replace("", "\n ") call.respondText(text = indexHtml, contentType = ContentType.Text.Html) } webSocket { - val parts: List = call.parameters.getAll("parts").orEmpty().filter { it.isNotEmpty() } + val parts: List = + call.parameters + .getAll("parts") + .orEmpty() + .filter { it.isNotEmpty() } val incrementalEngine = IncrementalEngine() lateinit var updateFunction: () -> Unit try { val queryParameters = call.request.queryParameters.toMap() - val createRenderer = incrementalEngine.incrementalFunction("createPageRenderer") { _ -> - rendererFactory.createPageRenderer( - incrementalEngine, - parts, - queryParameters, - this - ) - } + val createRenderer = + incrementalEngine.incrementalFunction("createPageRenderer") { _ -> + rendererFactory.createPageRenderer( + incrementalEngine, + parts, + queryParameters, + this + ) + } var previousViewModel: ViewModel? = null var previousText = "" val mutex = Mutex() + suspend fun sendUpdate(viewModel: ViewModel) { mutex.withLock { if (!isActive) return@withLock @@ -179,28 +201,31 @@ class ReactSSRServer(val rendererFactory: IRendererFactory = DefaultRendererFact } } - val createViewModel = incrementalEngine.incrementalFunction("createViewModel") { _ -> - IRenderer.contextIncrementalEngine.computeWith(incrementalEngine) { - createRenderer().render() + val createViewModel = + incrementalEngine.incrementalFunction("createViewModel") { _ -> + IRenderer.contextIncrementalEngine.computeWith(incrementalEngine) { + createRenderer().render() + } } - } + suspend fun sendUpdate() { if (!isActive) return - val viewModel = try { - createRenderer().runRead { - if (!isActive) return@runRead null - synchronized(incrementalEngine) { - if (incrementalEngine.isDisposed()) return@runRead null - createViewModel() + val viewModel = + try { + createRenderer().runRead { + if (!isActive) return@runRead null + synchronized(incrementalEngine) { + if (incrementalEngine.isDisposed()) return@runRead null + createViewModel() + } } - } - } catch (ex: Exception) { - buildViewModel { - component("html.pre") { - text(ex.stackTraceToString()) + } catch (ex: Exception) { + buildViewModel { + component("html.pre") { + text(ex.stackTraceToString()) + } } - } - } ?: return + } ?: return sendUpdate(viewModel) } updateFunction = { launch { sendUpdate() } } @@ -222,6 +247,7 @@ class ReactSSRServer(val rendererFactory: IRendererFactory = DefaultRendererFact createRenderer().messageReceived(Json.decodeFromString(message)) sendUpdate() } + else -> {} } } catch (ex: Throwable) { @@ -239,6 +265,9 @@ class ReactSSRServer(val rendererFactory: IRendererFactory = DefaultRendererFact } } -fun IncrementalEngine.isDisposed(): Boolean { - return this::class.java.getDeclaredField("disposed").also { it.isAccessible = true }.get(this) as Boolean -} +fun IncrementalEngine.isDisposed(): Boolean = + this::class.java + .getDeclaredField("disposed") + .also { + it.isAccessible = true + }.get(this) as Boolean diff --git a/react-ssr-server/src/main/kotlin/org/modelix/react/ssr/server/RendererBase.kt b/react-ssr-server/src/main/kotlin/org/modelix/react/ssr/server/RendererBase.kt index 7c1809c1..268f0bc7 100644 --- a/react-ssr-server/src/main/kotlin/org/modelix/react/ssr/server/RendererBase.kt +++ b/react-ssr-server/src/main/kotlin/org/modelix/react/ssr/server/RendererBase.kt @@ -26,7 +26,10 @@ abstract class RendererBase : IRenderer { abstract suspend fun runWrite(body: () -> R): R - fun registerMessageHandler(id: String, impl: suspend (MessageFromClient) -> Unit) { + fun registerMessageHandler( + id: String, + impl: suspend (MessageFromClient) -> Unit, + ) { messageHandlers[id] = impl } @@ -34,9 +37,7 @@ abstract class RendererBase : IRenderer { messageHandlers[message.messageId]?.invoke(message) } - final override fun render(): ViewModel { - return doRender().also { lastViewModel = it } - } + final override fun render(): ViewModel = doRender().also { lastViewModel = it } protected abstract fun doRender(): ViewModel } diff --git a/react-ssr-server/src/main/kotlin/org/modelix/react/ssr/server/RendererCall.kt b/react-ssr-server/src/main/kotlin/org/modelix/react/ssr/server/RendererCall.kt index 1739c111..8474a1c7 100644 --- a/react-ssr-server/src/main/kotlin/org/modelix/react/ssr/server/RendererCall.kt +++ b/react-ssr-server/src/main/kotlin/org/modelix/react/ssr/server/RendererCall.kt @@ -6,12 +6,28 @@ import org.modelix.model.api.IReadableNode import org.modelix.model.api.NodeReference sealed class RendererSignature -data class ConceptRendererSignature(val concept: ConceptReference) : RendererSignature() -data class NamedRendererSignature(val id: String) : RendererSignature() + +data class ConceptRendererSignature( + val concept: ConceptReference, +) : RendererSignature() + +data class NamedRendererSignature( + val id: String, +) : RendererSignature() sealed class RendererCall -data class NodeRefRendererCall(val node: INodeReference) : RendererCall() { + +data class NodeRefRendererCall( + val node: INodeReference, +) : RendererCall() { constructor(nodeRef: String) : this(NodeReference(nodeRef)) } -data class NodeRendererCall(val node: IReadableNode) : RendererCall() -data class NamedRendererCall(val id: String, val parameterValues: List) : RendererCall() + +data class NodeRendererCall( + val node: IReadableNode, +) : RendererCall() + +data class NamedRendererCall( + val id: String, + val parameterValues: List, +) : RendererCall() diff --git a/renovate.json5 b/renovate.json5 index 1e73da1a..36a01b05 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -1,18 +1,29 @@ { - $schema: "https://docs.renovatebot.com/renovate-schema.json", + $schema: 'https://docs.renovatebot.com/renovate-schema.json', extends: [ - "config:best-practices", - // Opt-in to beta support for pre-commit. - // See https://docs.renovatebot.com/modules/manager/pre-commit/ - ":enablePreCommit", - // Use the same commit type as with Dependabot. - ":semanticCommitTypeAll(build)" + 'config:best-practices', + ':enablePreCommit', + ':semanticCommitTypeAll(build)', ], enabledManagers: [ - "pre-commit", - "gradle-wrapper", - "nvm", - "github-actions", - "custom.regex" + 'pre-commit', + 'gradle-wrapper', + 'nvm', + 'github-actions', + 'custom.regex', + ], + customManagers: [ + { + depNameTemplate: 'Node.js', + customType: 'regex', + managerFilePatterns: [ + '/^gradle/libs.versions.toml$/', + ], + matchStrings: [ + 'node="(?.*?)"', + ], + datasourceTemplate: 'node-version', + versioningTemplate: 'node', + }, ], } diff --git a/reverse-mpsadapters/src/main/kotlin/org/modelix/model/mpsadapters/tomps/ModelixNodeAsMPSNode.kt b/reverse-mpsadapters/src/main/kotlin/org/modelix/model/mpsadapters/tomps/ModelixNodeAsMPSNode.kt index a18a4a33..002073ad 100644 --- a/reverse-mpsadapters/src/main/kotlin/org/modelix/model/mpsadapters/tomps/ModelixNodeAsMPSNode.kt +++ b/reverse-mpsadapters/src/main/kotlin/org/modelix/model/mpsadapters/tomps/ModelixNodeAsMPSNode.kt @@ -28,30 +28,28 @@ import org.modelix.model.mpsadapters.MPSProperty import org.modelix.model.mpsadapters.MPSReferenceLink import org.modelix.model.mpsadapters.MPSWritableNode -data class ModelixNodeAsMPSNode(val node: IReadableNode) : SNode { +data class ModelixNodeAsMPSNode( + val node: IReadableNode, +) : SNode { companion object { @JvmStatic - fun toModelixNode(node: SNode): INode { - return when (node) { + fun toModelixNode(node: SNode): INode = + when (node) { is ModelixNodeAsMPSNode -> node.node.asLegacyNode() else -> MPSNode(node) } - } @JvmStatic @JvmName("toModelixNodeNullable") - fun toModelixNode(node: SNode?): INode? { - return when (node) { + fun toModelixNode(node: SNode?): INode? = + when (node) { null -> null is ModelixNodeAsMPSNode -> node.node.asLegacyNode() else -> MPSNode(node) } - } @JvmStatic - fun toMPSNode(node: INode): SNode { - return ModelixNodeAsMPSNode(node.asWritableNode()) - } + fun toMPSNode(node: INode): SNode = ModelixNodeAsMPSNode(node.asWritableNode()) @JvmStatic @JvmName("toMPSNodeNullable") @@ -61,9 +59,7 @@ data class ModelixNodeAsMPSNode(val node: IReadableNode) : SNode { } @JvmStatic - fun toMPSNode(node: IReadableNode): SNode { - return ModelixNodeAsMPSNode(node) - } + fun toMPSNode(node: IReadableNode): SNode = ModelixNodeAsMPSNode(node) @JvmStatic @JvmName("toMPSNodeNullable") @@ -73,26 +69,22 @@ data class ModelixNodeAsMPSNode(val node: IReadableNode) : SNode { } @JvmStatic - fun ensureIsTracked(node: SNode): SNode { - return when (node) { + fun ensureIsTracked(node: SNode): SNode = + when (node) { is ModelixNodeAsMPSNode -> node else -> ModelixNodeAsMPSNode(MPSWritableNode(node)) } - } @JvmStatic @JvmName("ensureIsTrackedNullable") - fun ensureIsTracked(node: SNode?): SNode? { - return if (node == null) null else ensureIsTracked(node) - } + fun ensureIsTracked(node: SNode?): SNode? = if (node == null) null else ensureIsTracked(node) - private fun unwrapMPSNode(node: SNode): SNode { - return ((node as? ModelixNodeAsMPSNode)?.node as? MPSWritableNode)?.node + private fun unwrapMPSNode(node: SNode): SNode = + ((node as? ModelixNodeAsMPSNode)?.node as? MPSWritableNode)?.node ?: node - } - private fun forceUnwrapMPSNode(node: SNode): SNode { - return if (node is ModelixNodeAsMPSNode) { + private fun forceUnwrapMPSNode(node: SNode): SNode = + if (node is ModelixNodeAsMPSNode) { val writableNode = node.node if (writableNode is MPSWritableNode) { writableNode.node @@ -102,32 +94,29 @@ data class ModelixNodeAsMPSNode(val node: IReadableNode) : SNode { } else { node } - } } constructor(node: INode) : this(node.asReadableNode()) private val writableNode: IWritableNode get() = node as IWritableNode - override fun addChild(link: SContainmentLink, newChild: SNode) { + override fun addChild( + link: SContainmentLink, + newChild: SNode, + ) { forceUnwrapMPSNode(this).addChild(link, forceUnwrapMPSNode(newChild)) } - override fun getModel(): SModel? { - return forceUnwrapMPSNode(this).model - } + override fun getModel(): SModel? = forceUnwrapMPSNode(this).model - override fun getNodeId(): SNodeId { - return forceUnwrapMPSNode(this).nodeId - } + override fun getNodeId(): SNodeId = forceUnwrapMPSNode(this).nodeId - override fun getReference(): SNodeReference { - return forceUnwrapMPSNode(this).reference - } + override fun getReference(): SNodeReference = forceUnwrapMPSNode(this).reference - override fun getReference(link: SReferenceLink): SReference? { - return ReferenceAdapter(link).takeIf { node.getReferenceTarget(MPSReferenceLink(link).toReference()) != null } - } + override fun getReference(link: SReferenceLink): SReference? = + ReferenceAdapter(link).takeIf { + node.getReferenceTarget(MPSReferenceLink(link).toReference()) != null + } @Suppress("removal") override fun getReference(p0: String?): SReference { @@ -140,33 +129,44 @@ data class ModelixNodeAsMPSNode(val node: IReadableNode) : SNode { return jetbrains.mps.smodel.SNodeUtil.concept_BaseConcept } - override fun isInstanceOfConcept(superConcept: SAbstractConcept): Boolean { - return node.getConcept().isSubConceptOf(MPSConcept(superConcept)) - } + override fun isInstanceOfConcept(superConcept: SAbstractConcept): Boolean = node.getConcept().isSubConceptOf(MPSConcept(superConcept)) override fun getPresentation(): String { TODO("Not yet implemented") } - override fun getName(): String? { - return getProperty(SNodeUtil.property_INamedConcept_name) - } + override fun getName(): String? = getProperty(SNodeUtil.property_INamedConcept_name) @Suppress("removal") - override fun addChild(role: String?, newChild: SNode?) { + override fun addChild( + role: String?, + newChild: SNode?, + ) { TODO("Not yet implemented") } - override fun insertChildBefore(link: SContainmentLink, newChild: SNode, anchor: SNode?) { + override fun insertChildBefore( + link: SContainmentLink, + newChild: SNode, + anchor: SNode?, + ) { forceUnwrapMPSNode(this).insertChildBefore(link, forceUnwrapMPSNode(newChild), anchor?.let { forceUnwrapMPSNode(it) }) } @Suppress("removal") - override fun insertChildBefore(role: String, p1: SNode, p2: SNode?) { + override fun insertChildBefore( + role: String, + p1: SNode, + p2: SNode?, + ) { TODO("Not yet implemented") } - override fun insertChildAfter(link: SContainmentLink, newChild: SNode, anchor: SNode?) { + override fun insertChildAfter( + link: SContainmentLink, + newChild: SNode, + anchor: SNode?, + ) { forceUnwrapMPSNode(this).insertChildAfter(link, forceUnwrapMPSNode(newChild), anchor?.let { forceUnwrapMPSNode(it) }) } @@ -178,25 +178,15 @@ data class ModelixNodeAsMPSNode(val node: IReadableNode) : SNode { writableNode.remove() } - override fun getParent(): SNode? { - return node.getParent()?.let { ModelixNodeAsMPSNode(it) } - } + override fun getParent(): SNode? = node.getParent()?.let { ModelixNodeAsMPSNode(it) } - override fun getContainingRoot(): SNode { - return parent?.containingRoot ?: this - } + override fun getContainingRoot(): SNode = parent?.containingRoot ?: this - override fun getContainmentLink(): SContainmentLink? { - return (node.getContainmentLink() as? MPSChildLink)?.link - } + override fun getContainmentLink(): SContainmentLink? = (node.getContainmentLink() as? MPSChildLink)?.link - override fun getFirstChild(): SNode? { - return node.getAllChildren().firstOrNull()?.let { ModelixNodeAsMPSNode(it) } - } + override fun getFirstChild(): SNode? = node.getAllChildren().firstOrNull()?.let { ModelixNodeAsMPSNode(it) } - override fun getLastChild(): SNode? { - return node.getAllChildren().lastOrNull()?.let { ModelixNodeAsMPSNode(it) } - } + override fun getLastChild(): SNode? = node.getAllChildren().lastOrNull()?.let { ModelixNodeAsMPSNode(it) } override fun getPrevSibling(): SNode? { val siblings = node.getParent()?.getAllChildren()?.toList() ?: return null @@ -210,17 +200,17 @@ data class ModelixNodeAsMPSNode(val node: IReadableNode) : SNode { return siblings.getOrNull(index + 1)?.let { ModelixNodeAsMPSNode(it) } } - override fun getChildren(link: SContainmentLink?): MutableIterable { - return node.getChildren(link?.let { MPSChildLink(it).toReference() } ?: NullChildLinkReference) + override fun getChildren(link: SContainmentLink?): MutableIterable = + node + .getChildren(link?.let { MPSChildLink(it).toReference() } ?: NullChildLinkReference) .map { ModelixNodeAsMPSNode(it) } .toMutableList() - } - override fun getChildren(): MutableIterable { - return node.getAllChildren() + override fun getChildren(): MutableIterable = + node + .getAllChildren() .map { ModelixNodeAsMPSNode(it) } .toMutableList() - } @Suppress("removal") override fun getChildren(role: String?): MutableIterable { @@ -228,36 +218,52 @@ data class ModelixNodeAsMPSNode(val node: IReadableNode) : SNode { return node.getChildren(IChildLinkReference.fromName(role)).wrap().toMutableList() } - override fun setReferenceTarget(role: SReferenceLink, target: SNode?) { + override fun setReferenceTarget( + role: SReferenceLink, + target: SNode?, + ) { writableNode.setReferenceTarget(MPSReferenceLink(role).toReference(), target?.let { toModelixNode(it).asWritableNode() }) } @Suppress("removal") - override fun setReferenceTarget(role: String?, target: SNode?) { + override fun setReferenceTarget( + role: String?, + target: SNode?, + ) { requireNotNull(role) writableNode.setReferenceTarget(IReferenceLinkReference.fromName(role), target?.let { toModelixNode(it).asWritableNode() }) } - override fun setReference(p0: SReferenceLink, p1: ResolveInfo?) { + override fun setReference( + p0: SReferenceLink, + p1: ResolveInfo?, + ) { TODO("Not yet implemented") } - override fun setReference(p0: SReferenceLink, p1: SNodeReference) { + override fun setReference( + p0: SReferenceLink, + p1: SNodeReference, + ) { TODO("Not yet implemented") } - override fun setReference(p0: SReferenceLink, p1: SReference?) { + override fun setReference( + p0: SReferenceLink, + p1: SReference?, + ) { TODO("Not yet implemented") } @Suppress("removal") - override fun setReference(role: String?, reference: SReference?) { + override fun setReference( + role: String?, + reference: SReference?, + ) { TODO("Not yet implemented") } - override fun getReferenceTarget(link: SReferenceLink): SNode? { - return node.getReferenceTarget(MPSReferenceLink(link).toReference()).wrap() - } + override fun getReferenceTarget(link: SReferenceLink): SNode? = node.getReferenceTarget(MPSReferenceLink(link).toReference()).wrap() @Suppress("removal") override fun getReferenceTarget(role: String?): SNode? { @@ -269,34 +275,32 @@ data class ModelixNodeAsMPSNode(val node: IReadableNode) : SNode { writableNode.setReferenceTargetRef(MPSReferenceLink(link).toReference(), null) } - override fun getReferences(): MutableIterable { - return node.getReferenceLinks() + override fun getReferences(): MutableIterable = + node + .getReferenceLinks() .mapNotNull { MPSReferenceLink.tryFromReference(it) } .map { ReferenceAdapter(it.link) } .toMutableList() - } - override fun getProperties(): MutableIterable { - return node.getPropertyLinks() + override fun getProperties(): MutableIterable = + node + .getPropertyLinks() .mapNotNull { MPSProperty.tryFromReference(it) } .map { it.property } .toMutableList() - } - override fun hasProperty(role: SProperty): Boolean { - return node.getPropertyLinks() + override fun hasProperty(role: SProperty): Boolean = + node + .getPropertyLinks() .mapNotNull { MPSProperty.tryFromReference(it) } .any { it.property == role } - } @Suppress("removal") override fun hasProperty(p0: String?): Boolean { TODO("Not yet implemented") } - override fun getProperty(role: SProperty): String? { - return node.getPropertyValue(MPSProperty(role).toReference()) - } + override fun getProperty(role: SProperty): String? = node.getPropertyValue(MPSProperty(role).toReference()) @Suppress("removal") override fun getProperty(role: String?): String? { @@ -304,35 +308,36 @@ data class ModelixNodeAsMPSNode(val node: IReadableNode) : SNode { return node.getPropertyValue(IPropertyReference.fromName(role)) } - override fun setProperty(role: SProperty, value: String?) { + override fun setProperty( + role: SProperty, + value: String?, + ) { writableNode.setPropertyValue(MPSProperty(role).toReference(), value) } @Suppress("removal") - override fun setProperty(role: String?, value: String?) { + override fun setProperty( + role: String?, + value: String?, + ) { requireNotNull(role) writableNode.setPropertyValue(IPropertyReference.fromName(role), value) } - override fun getUserObject(key: Any?): Any? { - return null - } + override fun getUserObject(key: Any?): Any? = null - override fun putUserObject(key: Any?, value: Any?) { + override fun putUserObject( + key: Any?, + value: Any?, + ) { TODO("Not yet implemented") } - override fun getUserObjectKeys(): MutableIterable { - return mutableListOf() - } + override fun getUserObjectKeys(): MutableIterable = mutableListOf() - override fun getRoleInParent(): String? { - return containmentLink?.name - } + override fun getRoleInParent(): String? = containmentLink?.name - override fun getPropertyNames(): MutableIterable { - return properties.map { it.name }.toMutableList() - } + override fun getPropertyNames(): MutableIterable = properties.map { it.name }.toMutableList() @JvmName("wrapNode") private fun IReadableNode.wrap(): ModelixNodeAsMPSNode = ModelixNodeAsMPSNode(this) @@ -344,7 +349,9 @@ data class ModelixNodeAsMPSNode(val node: IReadableNode) : SNode { @JvmName("wrapNodes") private fun Iterable.wrap(): List = map { it.wrap() } - inner class ReferenceAdapter(private val link: SReferenceLink) : SReference { + inner class ReferenceAdapter( + private val link: SReferenceLink, + ) : SReference { override fun getLink(): SReferenceLink = link override fun getSourceNode(): SNode = this@ModelixNodeAsMPSNode diff --git a/settings.gradle.kts b/settings.gradle.kts index b4e972d1..865a8874 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -14,10 +14,12 @@ pluginManagement { } versionCatalogs { create("coreLibs") { - val modelixCoreVersion = file("gradle/libs.versions.toml").readLines() - .first { it.startsWith("modelixCore = ") } - .substringAfter('"') - .substringBefore('"') + val modelixCoreVersion = + file("gradle/libs.versions.toml") + .readLines() + .first { it.startsWith("modelixCore = ") } + .substringAfter('"') + .substringBefore('"') from("org.modelix:core-version-catalog:$modelixCoreVersion") } }