From 867b4883c9c987652fe2b7ea2c862d5f5ad12fdc Mon Sep 17 00:00:00 2001 From: Sascha Lisson Date: Thu, 11 Dec 2025 11:25:14 +0100 Subject: [PATCH 1/4] feat: split editor into frontend and backend components The backend component runs on the server and the frontend component on the client. This improves the responsiveness. The main reason for this refactoring was that the server doesn't know the bounds of the layouted elements and had to retrieve them from the client for computing properties such as the code completion menu position. --- build.gradle.kts | 3 +- gradle/libs.versions.toml | 10 + kernelf-angular-demo/package.json | 2 +- .../org/modelix/editor/kernelf/KernelfAPI.kt | 17 +- .../editor/kernelf/CodeCompletionTest.kt | 70 +-- .../kernelf/IncrementalLayoutAfterInsert.kt | 43 +- .../editor/kernelf/PropertyChangeTest.kt | 71 +-- .../src/jsMain/kotlin/KernelfApiJS.kt | 23 +- .../src/jsTest/kotlin/IncrementalDomTest.kt | 8 +- .../IncrementalLayouterAfterInsertJS.kt | 55 +-- .../editor/ssr/demo/kernelf/Application.kt | 2 +- .../build.gradle.kts | 2 + .../editor/ssr/client/ClientSideEditor.kt | 172 +------ .../editor/ssr/client/ClientSideEditors.kt | 37 +- .../editor/ssr/client/ClientSideEditorsAPI.kt | 4 +- .../editor/ssr/client/ModelixSSRClient.kt | 44 +- .../build.gradle.kts | 16 +- .../editor/ssr/mps/BaseLanguageTests.kt | 112 +++-- .../editor/ssr/mps/ModelixSSRServerForMPS.kt | 2 +- .../build.gradle.kts | 2 + .../editor/ssr/server/ModelixSSRServer.kt | 293 ++---------- projectional-editor/build.gradle.kts | 2 + .../org/modelix/editor/CaretPositionPolicy.kt | 60 ++- .../org/modelix/editor/CaretSelection.kt | 285 +++-------- .../org/modelix/editor/CaretSelectionView.kt | 3 +- .../org/modelix/editor/CellCreationContext.kt | 2 +- .../org/modelix/editor/CellNavigationUtils.kt | 16 +- .../org/modelix/editor/CellProperties.kt | 139 +++++- .../org/modelix/editor/CellReference.kt | 43 +- .../org/modelix/editor/CellSelection.kt | 45 +- .../org/modelix/editor/CellSelectionView.kt | 2 +- .../editor/{CellData.kt => CellSpecBase.kt} | 24 +- .../modelix/editor/CellTemplateReference.kt | 16 +- .../{EditorState.kt => CellTreeState.kt} | 9 +- .../kotlin/org/modelix/editor/Cells.kt | 141 +++--- .../editor/CodeCompletionActionWrapper.kt | 6 +- .../org/modelix/editor/CodeCompletionMenu.kt | 118 ++--- .../modelix/editor/CodeCompletionMenuUI.kt | 2 +- .../org/modelix/editor/ConceptEditor.kt | 14 +- .../org/modelix/editor/EditorComponent.kt | 281 ----------- .../kotlin/org/modelix/editor/EditorEngine.kt | 118 +++-- .../org/modelix/editor/EditorTestUtils.kt | 27 +- .../modelix/editor/FrontendEditorComponent.kt | 361 ++++++++++++++ .../kotlin/org/modelix/editor/ICellAction.kt | 25 +- .../org/modelix/editor/JSKeyboardEvent.kt | 2 +- .../kotlin/org/modelix/editor/JSMouseEvent.kt | 4 +- .../editor/ReplaceNodeActionProvider.kt | 7 +- .../kotlin/org/modelix/editor/Selection.kt | 6 +- .../kotlin/org/modelix/editor/TextLayouter.kt | 19 +- .../editor/celltemplate/CellTemplate.kt | 32 +- .../celltemplate/ChangePropertyCellAction.kt | 6 +- .../editor/celltemplate/ChildCellTemplate.kt | 39 +- .../celltemplate/CollectionCellTemplate.kt | 4 +- .../celltemplate/ConstantCellTemplate.kt | 10 +- .../editor/celltemplate/FlagCellTemplate.kt | 15 +- .../ForceShowOptionalCellAction.kt | 4 +- .../InsertSubstitutionPlaceholderAction.kt | 13 +- .../celltemplate/InstantiateNodeCellAction.kt | 4 +- .../InstantiateNodeCompletionAction.kt | 4 +- .../editor/celltemplate/LabelCellTemplate.kt | 6 +- .../celltemplate/NewLineCellTemplate.kt | 7 +- .../celltemplate/NoSpaceCellTemplate.kt | 7 +- .../celltemplate/NotationRootCellTemplate.kt | 4 +- .../celltemplate/OptionalCellTemplate.kt | 11 +- .../editor/celltemplate/OverrideText.kt | 14 +- .../celltemplate/PlaceholderCellReference.kt | 6 - .../celltemplate/PropertyCellTemplate.kt | 17 +- .../celltemplate/ReferenceCellTemplate.kt | 16 +- .../text/backend/BackendEditorComponent.kt | 88 ++++ .../text/backend/TextEditorServiceImpl.kt | 452 ++++++++++++++++++ .../modelix/editor/text/backend/Validator.kt | 42 ++ .../editor/text/frontend/FrontendCellTree.kt | 200 ++++++++ .../org/modelix/editor/text/shared/Actor.kt | 33 ++ .../editor/text/shared/TextEditorService.kt | 152 ++++++ .../text/shared/celltree/BackendCellTree.kt | 120 +++++ .../text/shared/celltree/CellPropertyValue.kt | 18 + .../text/shared/celltree/CellTreeBase.kt | 210 ++++++++ .../editor/text/shared/celltree/CellTreeOp.kt | 30 ++ .../editor/text/shared/celltree/ICellTree.kt | 51 ++ .../org/modelix/editor/CellNavigationTest.kt | 54 ++- .../org/modelix/editor/EditorKeyboardTest.kt | 39 +- .../org/modelix/editor/TextLayouterTest.kt | 5 +- .../org/modelix/editor/JsEditorComponent.kt | 10 +- .../editor/IncrementalJSDOMBuilderTest.kt | 126 ++--- react-ssr-client/yarn.lock | 16 +- .../src/test/kotlin/PagesTest.kt | 2 +- 86 files changed, 2898 insertions(+), 1734 deletions(-) rename projectional-editor/src/commonMain/kotlin/org/modelix/editor/{CellData.kt => CellSpecBase.kt} (64%) rename projectional-editor/src/commonMain/kotlin/org/modelix/editor/{EditorState.kt => CellTreeState.kt} (68%) delete mode 100644 projectional-editor/src/commonMain/kotlin/org/modelix/editor/EditorComponent.kt create mode 100644 projectional-editor/src/commonMain/kotlin/org/modelix/editor/FrontendEditorComponent.kt delete mode 100644 projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/PlaceholderCellReference.kt create mode 100644 projectional-editor/src/commonMain/kotlin/org/modelix/editor/text/backend/BackendEditorComponent.kt create mode 100644 projectional-editor/src/commonMain/kotlin/org/modelix/editor/text/backend/TextEditorServiceImpl.kt create mode 100644 projectional-editor/src/commonMain/kotlin/org/modelix/editor/text/backend/Validator.kt create mode 100644 projectional-editor/src/commonMain/kotlin/org/modelix/editor/text/frontend/FrontendCellTree.kt create mode 100644 projectional-editor/src/commonMain/kotlin/org/modelix/editor/text/shared/Actor.kt create mode 100644 projectional-editor/src/commonMain/kotlin/org/modelix/editor/text/shared/TextEditorService.kt create mode 100644 projectional-editor/src/commonMain/kotlin/org/modelix/editor/text/shared/celltree/BackendCellTree.kt create mode 100644 projectional-editor/src/commonMain/kotlin/org/modelix/editor/text/shared/celltree/CellPropertyValue.kt create mode 100644 projectional-editor/src/commonMain/kotlin/org/modelix/editor/text/shared/celltree/CellTreeBase.kt create mode 100644 projectional-editor/src/commonMain/kotlin/org/modelix/editor/text/shared/celltree/CellTreeOp.kt create mode 100644 projectional-editor/src/commonMain/kotlin/org/modelix/editor/text/shared/celltree/ICellTree.kt diff --git a/build.gradle.kts b/build.gradle.kts index bc596f4a..41ea0960 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 } @@ -151,7 +152,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/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/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/src/commonMain/kotlin/org/modelix/editor/kernelf/KernelfAPI.kt b/kernelf-editor/src/commonMain/kotlin/org/modelix/editor/kernelf/KernelfAPI.kt index f8618008..8511b415 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 @@ -109,18 +111,19 @@ object KernelfAPI { 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) } } 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..4179952a 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,113 +1,119 @@ 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) + 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() { + fun printModel() = runCompletionTest { println(editor.getRootCell().layout.toString()) } @Test - fun printActions() { + fun printActions() = runCompletionTest { val actions = getSubstituteActions(getNumberLiteralCell()) - val parameters = CodeCompletionParameters(editor, "") actions.forEach { println(it.getCompletionPattern() + " | " + it.getDescription()) } } @Test - fun notEmpty() { + fun notEmpty() = runCompletionTest { val actions = getSubstituteActions(getNumberLiteralCell()) assertTrue(actions.isNotEmpty()) } @Test - fun actionsOnNameProperty() { + fun actionsOnNameProperty() = runCompletionTest { val namePropertyCell = editor.getRootCell().descendants().find { it.getVisibleText() == "stringTests" }!! - editor.changeSelection(CaretSelection(namePropertyCell.layoutable()!!, 0)) + editor.changeSelection(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) + assertEquals(namePropertyCell.getParent(), commonAncestor) val actions = getSubstituteActions(namePropertyCell) assertEquals(emptyList(), actions) } @Test - fun noDuplicates() { - val parameters = CodeCompletionParameters(editor, "") + fun noDuplicates() = runCompletionTest { val actions = getSubstituteActions(getNumberLiteralCell()) val knownDuplicates = setOf( "it", @@ -125,10 +131,20 @@ class CodeCompletionTest { 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() } } + + 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..95c79992 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,19 +31,25 @@ 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() { + @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) + } + + private fun runLayoutTest(body: suspend () -> Unit) = runTest { KernelfLanguages.registerAll() branch = PBranch(ModelFacade.newLocalTree(useRoleIds = false), IdGenerator.getInstance(56754)).withIncrementalComputationSupport() ModelData.fromJson(modelJson).load(branch) @@ -48,27 +57,15 @@ open class IncrementalLayoutAfterInsert { val engine = EditorEngine(IncrementalEngine()) KernelfEditor.register(engine) testSuite = branch.computeRead { branch.getArea().getRoot().allChildren.ofType().models.rootNodes.ofType().first() } - editor = engine.editNode(testSuite) + editor = FrontendEditorComponent(TextEditorServiceImpl(engine, testSuite.untyped().asWritableNode().getModel(), backgroundScope)) + editor.editNode(testSuite.untypedReference()) assertTestItem = branch.computeRead { testSuite.descendants().drop(1).first() } - editor.selectAfterUpdate { + editor.flushAndUpdateSelection { val cell = editor.resolveNodeCell(assertTestItem)!!.firstLeaf().nextLeafs(true).first { it.isVisible() } println(cell.toString()) CaretSelection(cell.layoutable()!!, 0) } - editor.update() - } - - @AfterTest - fun afterTest() { + body() KernelfLanguages.languages.forEach { it.unregister() } } - - @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) - } } 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..d049a293 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,25 +21,46 @@ 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() { + private fun ICellTree.Cell.backend() = backend(service, editor) + + @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) + } + + @Test + 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()) + } + + private fun runPropertyTest(body: suspend () -> Unit) = runTest { KernelfLanguages.registerAll() val branch = ModelFacade.toLocalBranch(ModelFacade.newLocalTree(useRoleIds = false)).withIncrementalComputationSupport() val parensExpression = branch.computeWrite { @@ -63,32 +85,19 @@ class PropertyChangeTest { 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) + 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) } - editor.update() - } - - @AfterTest - fun afterTest() { + body() 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) - } - - @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()) - } } diff --git a/kernelf-editor/src/jsMain/kotlin/KernelfApiJS.kt b/kernelf-editor/src/jsMain/kotlin/KernelfApiJS.kt index 5b3aecb4..e7d87224 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 @@ -41,17 +44,17 @@ object KernelfApiJS { 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,8 +63,10 @@ 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) { @@ -73,7 +78,7 @@ object KernelfApiJS { if (!updateScheduled.getAndSet(true)) { coroutinesScope.launch { updateScheduled.getAndSet(false) - editor.update() + editor.updateNow() } } } else { @@ -93,7 +98,7 @@ object KernelfApiJS { if (!updateScheduled.getAndSet(true)) { coroutinesScope.launch { updateScheduled.getAndSet(false) - editor.update() + editor.updateNow() } } } else { diff --git a/kernelf-editor/src/jsTest/kotlin/IncrementalDomTest.kt b/kernelf-editor/src/jsTest/kotlin/IncrementalDomTest.kt index 9daeda63..f82fb0f1 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,11 +27,11 @@ 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 { 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..abd991fe 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,34 +47,11 @@ 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() { + fun domAfterInsert() = runLayoutTest { val containerElement = document.create.div() val generatedHtmlMap = GeneratedHtmlMap() var consumer = JSDom(containerElement.ownerDocument!!).let { vdom -> IncrementalVirtualDOMBuilder(vdom, vdom.wrap(containerElement), generatedHtmlMap) } @@ -91,4 +67,25 @@ open class IncrementalLayoutAfterInsertJS { 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-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..b4d9cf24 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 @@ -43,7 +43,7 @@ fun Application.module() { } 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/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..0d8e0f30 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,27 @@ 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 + id = editorElementId +"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() - } - containerElement.onkeydown = { event -> - MessageFromClient( - editorId = editorId, - keyboardEvent = event.convert(JSKeyboardEventType.KEYDOWN), - ).withBounds().send() - event.preventDefault() - } - 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..2106a02a 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,34 @@ 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 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 editorElementId = "modelix-editor-" + nextEditorId++.toString() + LOG.trace { "Trying to create new editor $editorElementId" } + + val editorComponent = JsEditorComponent(service) + editorComponent.openNode(rootNodeReference) + + 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..1a0293bf 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 @@ -29,7 +29,7 @@ object ClientSideEditorsAPI { 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") + pathSegments = listOf("rpc") }.buildString() console.log("Text editor URL: $wsUrl") initWithUrl(wsUrl) @@ -40,7 +40,7 @@ object ClientSideEditorsAPI { val httpClient = HttpClient(Js) { install(WebSockets) } - client = ModelixSSRClient(httpClient, url).also { it.connect { } } + client = ModelixSSRClient(httpClient, url) } fun createEditor(rootNodeReference: String, existingContainerElement: HTMLDivElement? = null): HTMLElement { 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..72813731 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,17 +3,16 @@ 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 @@ -24,12 +23,14 @@ class ModelixSSRClient(private val httpClient: HttpClient, private val url: Stri 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") @@ -38,25 +39,6 @@ class ModelixSSRClient(private val httpClient: HttpClient, private val url: Stri 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" } - } - } - } - } - } } inline fun KLogger.logExceptions(body: () -> R): R { @@ -68,7 +50,7 @@ inline fun KLogger.logExceptions(body: () -> R): R { } } -fun CoroutineScope.launchLogging(body: suspend () -> Unit): Job { +fun CoroutineScope.launchLogging(body: suspend CoroutineScope.() -> Unit): Job { return launch { LOG.logExceptions { body() } } 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-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..70813649 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,11 +22,14 @@ 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 /** @@ -31,11 +37,15 @@ import org.modelix.model.mpsadapters.MPSWritableNode */ @Suppress("ktlint:standard:wrapping", "ktlint:standard:trailing-comma-on-call-site") 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) + runBlocking { + editor = FrontendEditorComponent(service).also { + it.openNode(classNode.getNodeReference()).await() + } + editor.flush() + } } 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) } @@ -94,26 +112,29 @@ class BaseLanguageTests : TestBase("SimpleProject") { fun pressEnter() = pressKey(KnownKeys.Enter) fun pressKey(key: KnownKeys) { - editor.processKeyEvent(JSKeyboardEvent(JSKeyboardEventType.KEYDOWN, key)) + runBlocking { editor.processKeyEvent(JSKeyboardEvent(JSKeyboardEventType.KEYDOWN, key)) } } fun typeText(text: CharSequence) { for (c in text) { - editor.processKeyEvent( - JSKeyboardEvent( - eventType = JSKeyboardEventType.KEYDOWN, - typedText = c.toString(), - knownKey = null, - rawKey = c.toString(), - ), - ) + runBlocking { + editor.processKeyEvent( + JSKeyboardEvent( + eventType = JSKeyboardEventType.KEYDOWN, + typedText = c.toString(), + knownKey = null, + rawKey = c.toString(), + ), + ) + } } } 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 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) @@ -445,13 +466,47 @@ class BaseLanguageTests : TestBase("SimpleProject") { fun `test typing plus expression`() { 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("+") - // pressKey(KnownKeys.Enter) typeText("20") + assertEditorText(""" + public class Class1 { + public void method1() { + int abc = 10 + 20; + } + } + """) assertFinalEditorText(""" public class Class1 { public void method1() { @@ -469,7 +524,7 @@ class BaseLanguageTests : TestBase("SimpleProject") { placeCaretIntoCellWithText("") val layoutable = (editor.getSelection() as CaretSelection).layoutable - val node = layoutable.cell.ancestors(true) + val node = layoutable.cell.backend().ancestors(true) .mapNotNull { it.getProperty(CommonCellProperties.node) }.first() val parser = ParserForEditor(editorEngine).getParser(node.expectedConcept()!!, forCodeCompletion = completion) @@ -482,7 +537,7 @@ class BaseLanguageTests : TestBase("SimpleProject") { placeCaretIntoCellWithText("class") val layoutable = (editor.getSelection() as CaretSelection).layoutable - val node = layoutable.cell.ancestors(true) + 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) @@ -508,21 +563,4 @@ class BaseLanguageTests : TestBase("SimpleProject") { 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; - } - } - """) - } } 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..17b739d9 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 @@ -136,7 +136,7 @@ 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) 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..7116c6b2 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,75 +2,41 @@ 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 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<*>) { editorUpdater.invalidate() } + override fun modified(key: IStateVariableReference<*>) { serviceInstances.get().forEach { it.triggerUpdates() } } } fun install(route: Route) { @@ -79,235 +45,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/editor/CaretPositionPolicy.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CaretPositionPolicy.kt index 53c33ed2..a9f69dfe 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,36 @@ 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? { + return 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, @@ -21,9 +46,9 @@ data class CaretPositionPolicy( preferredCellRefs + other.preferredCellRefs, ) - override fun getBestSelection(editor: EditorComponent): CaretSelection? { + override fun getBestSelection(editor: FrontendEditorComponent): CaretSelection? { val candidates = preferredCellRefs - .flatMap { editor.resolveCell(it) } + .flatMap { editor.cellTree.resolveCell(it) } .flatMap { it.descendantsAndSelf() } .mapNotNull { editor.resolveLayoutable(it) } @@ -32,10 +57,10 @@ data class CaretPositionPolicy( .sortedBy { it.cell.ancestors(true).filter { isAvoided(it) }.count() } .firstOrNull() ?: return null - return CaretSelection(best, (best.cell.getSelectableText() ?: "").length) + 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 +68,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 +94,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,7 +116,7 @@ class SavedCaretPosition( } companion object { - fun saveAndRun(editor: EditorComponent, body: () -> Unit): SavedCaretPosition? { + 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..44dfb7ef 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CaretSelection.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CaretSelection.kt @@ -1,53 +1,56 @@ 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 { + fun getSelectedTextRange() = min(start, end) until max(start, end) + + override fun getSelectedCells(): List { return 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? { + return layoutable.cell.cellReferences.asSequence() .mapNotNull { editor.resolveCell(it).firstOrNull() } .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.changeSelection(CaretSelection(editor, layoutable, start, end - 1)) } else { - editor.changeSelection(CaretSelection(layoutable, end - 1)) + editor.changeSelection(CaretSelection(editor, layoutable, end - 1)) } } else { val previous = layoutable.getSiblingsInText(next = false) @@ -57,9 +60,9 @@ class CaretSelection(val layoutable: LayoutableCell, val start: Int, val end: In 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.changeSelection(CellSelection(editor, it, true, this)) } } else { - editor.changeSelection(CaretSelection(previous, previous.cell.getMaxCaretPos())) + editor.changeSelection(CaretSelection(editor, previous, previous.cell.getMaxCaretPos())) } } } @@ -67,9 +70,9 @@ class CaretSelection(val layoutable: LayoutableCell, val start: Int, val end: In KnownKeys.ArrowRight -> { if (end < (layoutable.cell.getSelectableText()?.length ?: 0)) { if (event.modifiers.shift) { - editor.changeSelection(CaretSelection(layoutable, start, end + 1)) + editor.changeSelection(CaretSelection(editor, layoutable, start, end + 1)) } else { - editor.changeSelection(CaretSelection(layoutable, end + 1)) + editor.changeSelection(CaretSelection(editor, layoutable, end + 1)) } } else { val next = layoutable.getSiblingsInText(next = true) @@ -79,9 +82,9 @@ class CaretSelection(val layoutable: LayoutableCell, val start: Int, val end: In 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.changeSelection(CellSelection(editor, it, false, this)) } } else { - editor.changeSelection(CaretSelection(next, 0)) + editor.changeSelection(CaretSelection(editor, next, 0)) } } } @@ -91,30 +94,13 @@ class CaretSelection(val layoutable: LayoutableCell, val start: Int, val end: In } KnownKeys.ArrowUp -> { if (event.modifiers.meta) { - layoutable.cell.let { editor.changeSelection(CellSelection(it, true, this)) } + layoutable.cell.let { editor.changeSelection(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) { @@ -125,13 +111,14 @@ class CaretSelection(val layoutable: LayoutableCell, val start: Int, val end: In } 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 { @@ -139,39 +126,15 @@ class CaretSelection(val layoutable: LayoutableCell, val start: Int, val end: In } } 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,165 +143,36 @@ 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?.changeSelection(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) + suspend fun replaceText(newText: String): Boolean { + return replaceText(0 until getCurrentCellText().length, newText, editor, 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 - } - } - - 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 + private suspend fun replaceText(range: IntRange, replacement: String, editor: FrontendEditorComponent, triggerCompletion: Boolean): Boolean { + return editor.serviceCall { replaceText(editor.editorId, layoutable.cell.getId(), range, replacement, triggerCompletion) }.result } - fun getEditor(): EditorComponent? = layoutable.cell.editorComponent - override fun equals(other: Any?): Boolean { if (this === other) return true if (other == null || this::class != other::class) return false 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 +181,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 @@ -369,18 +204,18 @@ class CaretSelection(val layoutable: LayoutableCell, val start: Int, val end: In } 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 } - currentOffset += length + return words.filterIsInstance().lastOrNull()?.let { CaretSelection(editor, it, it.cell.getMaxCaretPos(), desiredXPosition = x) } } - 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..cac86847 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CaretSelectionView.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CaretSelectionView.kt @@ -4,10 +4,11 @@ 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 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..dfc2c5a8 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,3 @@ 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..edf248a6 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellNavigationUtils.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellNavigationUtils.kt @@ -1,11 +1,11 @@ package org.modelix.editor fun Cell.nextCells(): Sequence { - return nextSiblings().flatMap { it.descendantsAndSelf() } + (parent?.let { sequenceOf(it) + it.nextCells() } ?: emptySequence()) + return 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()) + return previousSiblings().flatMap { it.descendantsAndSelf(iterateBackwards = true) } + (getParent()?.let { sequenceOf(it) + it.previousCells() } ?: emptySequence()) } fun Cell.previousLeafs(includeSelf: Boolean = false): Sequence { @@ -25,12 +25,12 @@ fun Cell.nextLeaf(condition: (Cell) -> Boolean): Cell? { } 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() } @@ -53,12 +53,12 @@ fun Cell.nextSibling(): Cell? { } fun Cell.previousSiblings(): Sequence { - val parent = this.parent ?: return emptySequence() + 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() + val parent = this.getParent() ?: return emptySequence() return parent.getChildren().asSequence().dropWhile { it != this }.drop(1) } @@ -70,9 +70,9 @@ fun Cell.descendants(iterateBackwards: Boolean = false): Sequence { } 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.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().parent!! +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 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..343165fd 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellProperties.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellProperties.kt @@ -1,5 +1,10 @@ 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 { @@ -22,33 +27,137 @@ class CellProperties : Freezable() { 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<*>? { + return CellReferenceListValue(value) + } + + override fun fromSerializableValue(value: Any?): List { + return 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 { + return 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) 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..1869511b 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,60 @@ 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? = +fun FrontendEditorComponent.resolvePropertyCell(property: IProperty, node: INode): Cell? = resolvePropertyCell(property, node.reference) -fun EditorComponent.resolvePropertyCell(property: IProperty, node: ITypedNode): Cell? = +fun FrontendEditorComponent.resolvePropertyCell(property: IProperty, node: ITypedNode): Cell? = resolvePropertyCell(property, node.untyped()) -fun EditorComponent.resolvePropertyCell(property: ITypedProperty<*>, node: ITypedNode): Cell? = +fun FrontendEditorComponent.resolvePropertyCell(property: ITypedProperty<*>, node: ITypedNode): Cell? = resolvePropertyCell(property.untyped(), node.untyped()) data class NodeCellReference(val nodeRef: INodeReference) : CellReference() -fun EditorComponent.resolveNodeCell(nodeRef: INodeReference): Cell? = +fun FrontendEditorComponent.resolveNodeCell(nodeRef: INodeReference): Cell? = resolveCell(NodeCellReference(nodeRef)).firstOrNull() -fun EditorComponent.resolveNodeCell(node: INode): Cell? = +fun FrontendEditorComponent.resolveNodeCell(node: INode): Cell? = resolveNodeCell(node.reference) -fun EditorComponent.resolveNodeCell(node: ITypedNode): Cell? = +fun FrontendEditorComponent.resolveNodeCell(node: ITypedNode): Cell? = resolveNodeCell(node.untypedReference()) -data class ChildNodeCellReference(val parentNodeRef: INodeReference, val link: IChildLink, val index: Int = 0) : CellReference() +@Serializable +data class ChildNodeCellReference(val parentNodeRef: INodeReference, val link: IChildLinkReference, val index: Int = 0) : CellReference() + +@Serializable data class SeparatorCellReference(val before: CellReference) : CellReference() -data class ReferencedNodeCellReference(val sourceNodeRef: INodeReference, val link: IReferenceLink) : 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..931d592d 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellSelection.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellSelection.kt @@ -1,30 +1,31 @@ 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 +data class CellSelection(val editor: FrontendEditorComponent, val cell: Cell, val directionLeft: Boolean, val previousSelection: Selection?) : Selection() { override fun getSelectedCells(): List { return listOf(cell) } override fun isValid(): Boolean { - return getEditor() != null + return cell.isAttached() } - override fun update(editor: EditorComponent): Selection? { - return cell.data.cellReferences.asSequence() + override fun update(editor: FrontendEditorComponent): Selection? { + return 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)) } + ?.let { editor.changeSelection(CellSelection(editor, it, directionLeft, this)) } } else { unwrapCaretSelection()?.selectNextPreviousLine(false) } @@ -41,22 +42,22 @@ data class CellSelection(val cell: Cell, val directionLeft: Boolean, val previou val isLeft = event.knownKey == KnownKeys.ArrowLeft if (isLeft == directionLeft) { cell.ancestors().firstOrNull { it.isSelectable() } - ?.let { editor.changeSelection(CellSelection(it, directionLeft, this)) } + ?.let { editor.changeSelection(CellSelection(editor, it, directionLeft, this)) } } else { previousSelection?.let { editor.changeSelection(it) } } } else { val caretSelection = unwrapCaretSelection() if (caretSelection != null) { - editor.changeSelection(CaretSelection(caretSelection.layoutable, caretSelection.start)) + editor.changeSelection(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)) } + ?.let { editor.changeSelection(CaretSelection(editor, it, 0)) } } else { tabTargets.lastOrNull()?.layoutable() - ?.let { editor.changeSelection(CaretSelection(it, it.cell.getSelectableText()?.length ?: 0)) } + ?.let { editor.changeSelection(CaretSelection(editor, it, it.cell.getSelectableText()?.length ?: 0)) } } } } @@ -66,23 +67,10 @@ data class CellSelection(val cell: Cell, val directionLeft: Boolean, val previou 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) } } } } @@ -98,7 +86,6 @@ data class CellSelection(val cell: Cell, val directionLeft: Boolean, val previou } 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() 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..70d6dab9 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellSelectionView.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellSelectionView.kt @@ -5,7 +5,7 @@ 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 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 64% 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..9b105f85 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,11 @@ 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() @@ -30,13 +33,18 @@ 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() { +class TextCellSpec(val text: String, val placeholderText: String = "") : CellSpecBase() { fun getVisibleText(cell: Cell): String { return if (cell.getChildren().isEmpty()) { text.ifEmpty { placeholderText } @@ -56,3 +64,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/CellTemplateReference.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTemplateReference.kt index 295fae9c..2247cd5c 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,16 @@ 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 +@Serializable +data class RooCellTemplateReference(val conceptEditorId: Long, val subConcept: ConceptReference) : ICellTemplateReference + +@Serializable data class ChildCellTemplateReference(val parent: ICellTemplateReference, val index: Int) : ICellTemplateReference -data class SeparatorCellTemplateReference(val parent: ICellTemplateReference) : ICellTemplateReference -data class TemplateCellReference(val template: ICellTemplateReference, val node: INodeReference) : CellReference() +@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 68% 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..8aa7f7b7 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,9 +16,7 @@ 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) } 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..00c2a7d8 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/Cells.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/Cells.kt @@ -1,6 +1,8 @@ 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() @@ -22,75 +24,74 @@ 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() - } - - 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 +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 fun LayoutableCell.getMaxCaretPos(): Int = cell.getSelectableText()?.length ?: 0 class ResettableLazy(private val initializer: () -> E) : Lazy { 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..b418fc49 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CodeCompletionActionWrapper.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CodeCompletionActionWrapper.kt @@ -1,5 +1,7 @@ package org.modelix.editor +import org.modelix.editor.text.backend.BackendEditorComponent + 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) @@ -26,7 +28,7 @@ class CodeCompletionActionProviderWrapper( } class CodeCompletionActionWithPostprocessor(action: ICodeCompletionAction, val after: () -> Unit) : CodeCompletionActionWrapper(action) { - override fun execute(editor: EditorComponent): ICaretPositionPolicy? { + override fun execute(editor: BackendEditorComponent): ICaretPositionPolicy? { val policy = wrappedAction.execute(editor) after() return policy @@ -34,7 +36,7 @@ class CodeCompletionActionWithPostprocessor(action: ICodeCompletionAction, val a } class CodeCompletionActionWithCaretPolicy(action: ICodeCompletionAction, val policy: (ICaretPositionPolicy?) -> ICaretPositionPolicy?) : CodeCompletionActionWrapper(action) { - override fun execute(editor: EditorComponent): ICaretPositionPolicy? { + override fun execute(editor: BackendEditorComponent): ICaretPositionPolicy? { return policy(wrappedAction.execute(editor)) } } 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..95f4b479 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CodeCompletionMenu.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CodeCompletionMenu.kt @@ -5,56 +5,55 @@ 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 { 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() @@ -62,13 +61,7 @@ class CodeCompletionMenu( KnownKeys.ArrowRight -> patternEditor.moveCaret(1) KnownKeys.Escape -> editor.closeCodeCompletionMenu() KnownKeys.Enter -> { - getSelectedEntry()?.let { entry -> - editor.runWrite { - entry.executeAndUpdateSelection(editor) - editor.state.clearTextReplacement(anchor) - } - } - editor.closeCodeCompletionMenu() + getSelectedEntry()?.execute() } KnownKeys.Backspace -> patternEditor.deleteText(true) KnownKeys.Delete -> patternEditor.deleteText(false) @@ -80,27 +73,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,10 +110,10 @@ 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() } } @@ -128,7 +126,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 +140,20 @@ 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 +161,7 @@ class CodeCompletionMenu( } } - fun moveCaret(delta: Int) { + suspend fun moveCaret(delta: Int) { caretPos = (caretPos + delta).coerceIn(0..pattern.length) updateActions() } @@ -241,28 +239,14 @@ 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 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..716876b5 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,6 @@ 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 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..2fa59a75 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/ConceptEditor.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/ConceptEditor.kt @@ -1,33 +1,41 @@ 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, ) { + companion object { + private val idSequence = AtomicLong(0L) + } + + val id: Long = idSequence.incrementAndGet() + fun isApplicable(context: CellCreationContext, node: INode): Boolean { return apply(node.concept ?: NullConcept).condition?.invoke(node) != false } fun apply(subConcept: IConcept): NotationRootCellTemplate { return templateBuilder(subConcept) - .also { it.setReference(RooCellTemplateReference(this, subConcept.getReference())) } + .also { it.setReference(RooCellTemplateReference(id, subConcept.getReference().upcast())) } } - fun applyIfApplicable(context: CellCreationContext, node: INode): CellData? { + 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) } 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/EditorEngine.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/EditorEngine.kt index eddb63b1..08e8fc95 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/EditorEngine.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/EditorEngine.kt @@ -1,16 +1,15 @@ 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 @@ -21,7 +20,6 @@ class EditorEngine(incrementalEngine: IncrementalEngine? = null) { 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,18 +32,25 @@ 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) { @@ -63,9 +68,11 @@ 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() @@ -77,40 +84,54 @@ class EditorEngine(incrementalEngine: IncrementalEngine? = null) { return 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 { + return 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(editorState: EditorState, node: INode): Cell { - return dataToCell(editorState, createCellDataIncremental(editorState, node)) + private fun doCreateCell(cellTreeState: CellTreeState, call: CellCreationCall): IMutableCellTree.MutableCell { + return dataToCell(cellTreeState, createCellSpecIncremental(cellTreeState, call), cellTreeState.cellTree.createCell()) } - 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) + 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) + } + 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 ChildDataReference -> { - createCell(editorState, childData.childNode).also { it.parent?.removeChild(it) } + is ChildSpecReference -> { + createCell(cellTreeState, childRef.childNode) } - else -> throw RuntimeException("Unsupported: $childData") } - cell.addChild(childCell) + if (childCell.getParent() != cell) { + childCell.moveCell(cell, index) + } else if (cell.getChildAt(index) != childCell) { + childCell.moveCell(index) + } } + 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 +145,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" } } @@ -148,7 +169,6 @@ class EditorEngine(incrementalEngine: IncrementalEngine? = null) { } fun dispose() { - coroutineScope.cancel("EditorEngine disposed") if (ownsIncrementalEngine) incrementalEngine.dispose() } @@ -160,11 +180,13 @@ class EditorEngine(incrementalEngine: IncrementalEngine? = null) { 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..0b9216f2 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,18 +8,22 @@ object EditorTestUtils { val newLine = Any() val indentChildren = Any() - fun buildCells(template: Any): Cell { + fun buildCells(template: Any, tree: IMutableCellTree): MutableCell { 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 { + 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 -> data.properties[CommonCellProperties.indentChildren] = true - is ECellLayout -> data.properties[CommonCellProperties.layout] = child - else -> addChild(buildCells(child!!)) + indentChildren -> setProperty(CommonCellProperties.indentChildren, true) + is ECellLayout -> setProperty(CommonCellProperties.layout, child) + else -> buildCells(child!!, tree).moveCell(this, getChildren().size) } } } @@ -26,8 +31,8 @@ object EditorTestUtils { } } - 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 { + return buildCells(buildRandomTemplate(rand, cellsPerLevel, levels), tree) } fun buildRandomTemplate(rand: Random, cellsPerLevel: Int, levels: Int): Any { 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..8818312c --- /dev/null +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/FrontendEditorComponent.kt @@ -0,0 +1,361 @@ +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 uiEventQueue = coroutineScope.consume(capacity = 100, onBufferOverflow = BufferOverflow.DROP_LATEST) { event -> + when (event) { + is JSKeyboardEvent -> processKeyEvent(event) + is JSMouseEvent -> processMouseEvent(event) + } + } + private val uiUpdateQueue = coroutineScope.consume Unit, CompletableDeferred>>(capacity = Channel.UNLIMITED) { + it.second.completeWith(runCatching { it.first.invoke() }) + } + 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() { + return enqueueUpdate(service.flush(editorId)).await() + } + + suspend fun flushRemote() { + return enqueueUpdate(service.flush(editorId)).await() + } + + suspend fun flushLocal() { + return enqueueUpdate(EditorUpdateData()).await() + } + + fun getMainLayer(): IVirtualDom.HTMLElement? { + return 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 { changeSelection(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 { + return enqueueUpdate { updateNow(updateData) } + } + + fun enqueueUpdate(body: () -> Unit): Deferred { + val result = CompletableDeferred() + uiUpdateQueue.trySend(body to result).getOrThrow() + return result + } + + 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) + } + + open fun changeSelection(newSelection: Selection) { + selection = newSelection + codeCompletionMenu = null + updateNow() + } + + 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) + updateNow() + } + + fun closeCodeCompletionMenu() { + codeCompletionMenu = null + updateNow() + } + + fun dispose() { + uiEventQueue.close() + uiUpdateQueue.close() + updateLoop.getAndUpdate { currentJob -> + currentJob?.cancel("disposed") + null + } + coroutineScope.cancel("disposed") + } + + fun enqueueUIEvent(event: JSUIEvent): Boolean { + uiEventQueue.trySend(event).onFailure { LOG.error(it) { "UI event ignored: $event" } } + return true + } + + private fun processKeyUp(event: JSKeyboardEvent): Boolean { + return 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()) + changeSelection(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 + } + changeSelection(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) + is ServiceCallResult<*> -> result.updateData?.let { enqueueUpdate(it) } + } + 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 +} 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..8d1c586f 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/ICellAction.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/ICellAction.kt @@ -1,16 +1,17 @@ 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 + fun replaceText(editor: CellTreeState, range: IntRange, replacement: String, newText: String): Boolean } class CompositeTextChangeAction(val actions: List) : ITextChangeAction { @@ -18,7 +19,7 @@ class CompositeTextChangeAction(val actions: List) : ITextCha return actions.any { it.isValid(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 { return actions .filter { it.isValid(newText) } .any { it.replaceText(editor, range, replacement, newText) } @@ -37,13 +38,13 @@ 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 { @@ -115,7 +116,7 @@ data class CellBorder(val cell: Cell, val isLeft: Boolean) { 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 +142,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() 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..ffb56db4 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,7 +12,7 @@ data class JSMouseEvent( val modifiers: Modifiers = Modifiers.NONE, val button: Short, val buttons: Short, -) { +) : JSUIEvent { fun getButtonAsEnum(): JSMouseButton = when (button) { 0.toShort() -> JSMouseButton.PRIMARY 1.toShort() -> JSMouseButton.AUXILIARY 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..3b3a322b 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/ReplaceNodeActionProvider.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/ReplaceNodeActionProvider.kt @@ -1,13 +1,14 @@ 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 { 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() } @@ -48,9 +49,9 @@ class ChangeReferenceTargetAction(val sourceLocation: INonExistingNode, val link return "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..7cde9a13 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/Selection.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/Selection.kt @@ -1,9 +1,11 @@ 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 { 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..f61b67e4 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,18 @@ 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 { 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 +62,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 { @@ -278,21 +283,21 @@ 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" } + require(cell.type == ECellType.TEXT) { "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) + ?: 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 isPlaceholder = cell.text.isNullOrEmpty() val textColor = when { textIsOverridden -> "#A81E1E" isPlaceholder -> cell.getProperty(CommonCellProperties.placeholderTextColor) @@ -315,7 +320,7 @@ 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() { 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..ae0b6122 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 @@ -35,23 +35,23 @@ 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 { + protected open fun applyChildren(context: CellCreationContext, node: INode, cell: CellSpecBase): List { return children.map { it.apply(context, node) } } - protected abstract fun createCell(context: CellCreationContext, node: INode): CellData + protected abstract fun createCell(context: CellCreationContext, node: INode): CellSpecBase open fun getInstantiationActions(location: INonExistingNode, parameters: CodeCompletionParameters): List? { val completionText = properties[CommonCellProperties.codeCompletionText] @@ -122,17 +122,17 @@ abstract class CellTemplate(val concept: IConcept) { 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) } } } 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..f2fad015 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,13 +13,13 @@ class ChangePropertyCellAction( val property: IProperty, val value: String, ) : ICellAction { - override fun execute(editor: EditorComponent): ICaretPositionPolicy? { + 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 { 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..11eb0918 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 @@ -115,28 +115,27 @@ 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)) - } + 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 @@ -145,10 +144,10 @@ class ChildCellTemplate( } fun addInsertActionCell(index: Int) { if (link.isMultiple) { - val actionCell = CellData() + val actionCell = CellSpec() val action = newLineConcept?.let { InstantiateNodeCellAction(NonExistingChild(ExistingNode(node), link, index), it) - } ?: InsertSubstitutionPlaceholderAction(context.editorState, createCellReference(node), index) + } ?: InsertSubstitutionPlaceholderAction(context.cellTreeState, createCellReference(node), index) actionCell.properties[CellActionProperties.insert] = action cell.addChild(actionCell) } @@ -167,9 +166,9 @@ class ChildCellTemplate( } else { val separatorText = separatorCell?.getGrammarSymbols()?.filterIsInstance() ?.firstOrNull()?.text - val childCells = childNodes.map { ChildDataReference(it) } + 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,7 +181,7 @@ 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 @@ -256,7 +255,7 @@ class ChildCellTemplate( return 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..39a70ce9 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,11 @@ 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() + 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..23f1bd72 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 @@ -39,7 +39,7 @@ class ConstantCellTemplate(concept: IConcept, val text: String) : CellTemplate(c 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)) @@ -64,12 +64,12 @@ class ConstantCellTemplate(concept: IConcept, val text: String) : CellTemplate(c ICodeCompletionAction { override fun getMatchingText(): String = text override fun getDescription(): String = concept.getShortName() - override fun execute(editor: EditorComponent): CaretPositionPolicy? { + 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)) } 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..df075807 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 @@ -37,18 +38,18 @@ 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)) } } 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..7427b1fb 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,14 +1,14 @@ 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 { + override fun execute(editor: BackendEditorComponent): ICaretPositionPolicy { editor.state.forceShowOptionals[cell] = true return CaretPositionPolicy(cell) } 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..5ae63609 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,15 +1,15 @@ 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 { override fun isApplicable(): Boolean = true - override fun execute(editor: EditorComponent): CaretPositionPolicy { + override fun execute(editor: BackendEditorComponent): CaretPositionPolicy { val newNode = location.getExistingAncestor()!!.getArea().executeWrite { location.replaceNode(concept) } 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..89bdbc7f 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( @@ -26,7 +26,7 @@ class InstantiateNodeCompletionAction( override fun getDescription(): String = description - override fun execute(editor: EditorComponent): CaretPositionPolicy? { + override fun execute(editor: BackendEditorComponent): CaretPositionPolicy? { val newNode = location.getExistingAncestor()!!.getArea().executeWrite { location.replaceNode(concept) } 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..47a73729 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 @@ -17,8 +17,8 @@ import org.modelix.model.api.INode */ class LabelCellTemplate(concept: IConcept, val text: String) : CellTemplate(concept) { - override fun createCell(context: CellCreationContext, node: INode): TextCellData { - return TextCellData(text, "").also { + override fun createCell(context: CellCreationContext, node: INode): TextCellSpec { + return TextCellSpec(text, "").also { if (!it.properties.isSet(CommonCellProperties.textColor)) { it.properties[CommonCellProperties.textColor] = "LightGray" } 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..d7b0b8b6 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 @@ -11,8 +12,8 @@ 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 } + override fun createCell(context: CellCreationContext, node: INode): CellSpecBase { + return CellSpec().also { cell -> cell.properties[CommonCellProperties.onNewLine] = true } } override fun toCompletionToken(): ICompletionTokenOrList? { 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..27952a1f 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 @@ -11,8 +12,8 @@ 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 } + override fun createCell(context: CellCreationContext, node: INode): CellSpecBase { + return CellSpec().also { cell -> cell.properties[CommonCellProperties.noSpace] = true } } override fun toCompletionToken(): ICompletionTokenOrList? { 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..49c5dae3 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,12 @@ 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) { 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..22cffc5a 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 @@ -38,12 +39,12 @@ class OptionalCellTemplate(concept: IConcept) : CellTemplate(concept), IOptional } } - override fun createCell(context: CellCreationContext, node: INode): CellData { - return CellData() + override fun createCell(context: CellCreationContext, node: INode): CellSpecBase { + return 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() 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..758671d9 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,25 @@ 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 { +class OverrideText(val cell: TextCellSpec, val delegate: ITextChangeAction?) : ITextChangeAction { override fun isValid(value: String?): Boolean { return 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/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..04c711d0 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 @@ -49,12 +50,12 @@ 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 } @@ -99,7 +100,7 @@ open class PropertyCellTemplate(concept: IConcept, val property: IProperty) : return 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)) @@ -112,7 +113,7 @@ open class PropertyCellTemplate(concept: IConcept, val property: IProperty) : 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..f01e801b 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 @@ -50,13 +50,13 @@ 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 } @@ -96,7 +96,7 @@ class ReferenceCellTemplate( return 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..80fd1027 --- /dev/null +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/text/backend/BackendEditorComponent.kt @@ -0,0 +1,88 @@ +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 { + return 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 { + return CompletionMenuBackend(providers).let { + completionMenu = it + it.updateActions(pattern) + } + } + + fun runWrite(body: () -> R): R { + return when (rootCall) { + is NodeCellCreationCall -> rootCall.node.getModel().executeWrite(body) + } + } + + fun runRead(body: () -> R): R { + return 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 { + return computeActions(pattern).also { entries = it } + } + + fun getEntries(): List = entries + + fun computeActions(pattern: String): List { + return 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..211eb50a --- /dev/null +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/text/backend/TextEditorServiceImpl.kt @@ -0,0 +1,452 @@ +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 { + return 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 { + return 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 { + return 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 { + return 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 { + return runWithCell(editorId, cellId) { updateChannel, cell -> + model.executeRead { + updateChannel.editor.completionMenu?.computeActions(pattern)?.any() == true + } + } + } + + override suspend fun executeCodeCompletionAction(editorId: Int, actionId: Int): EditorUpdateData { + return 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 { + return runWithEditor(editorId) { updateChannel, editor -> + editor.state.reset() + updateChannel.createUpdate() + } + } + + override suspend fun flush(editorId: Int): EditorUpdateData { + return runWithEditor(editorId) { updateChannel, editor -> updateChannel.createUpdate() } + } + + private fun ICellAction.executeAndUpdateSelection(channel: EditorUpdateChannel): EditorUpdateData { + return channel.createSelection(execute(channel.editor)) + } + + private fun ICodeCompletionAction.executeAndUpdateSelection(channel: EditorUpdateChannel): EditorUpdateData { + return 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 { + return 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 { + return EditorUpdateData(cellTreeChanges = editor.update(), selectionChange = newSelection) + } + + fun createUpdate(): EditorUpdateData { + return 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..a6f2412c --- /dev/null +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/text/backend/Validator.kt @@ -0,0 +1,42 @@ +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..f18642fd --- /dev/null +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/text/frontend/FrontendCellTree.kt @@ -0,0 +1,200 @@ +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 { + return 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 layout_ = ResettableLazy { + runLayoutOnCell(this) { it.layout } + } + val layout: LayoutedText + get() = layout_.value + + fun clearCachedLayout() { + withTreeLock { + layout_.reset() + } + } + + fun invalidateLayout() { + withTreeLock { + layout_.reset() + getParent()?.invalidateLayout() + } + } + + fun getEditorComponent() = this@FrontendCellTree.editorComponent + + override fun getParent() = super.getParent() as FrontendCellImpl? + + override fun getProperty(key: CellPropertyKey): T { + return 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 { + return 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 { + return getProperty(CommonCellProperties.textReplacement) + ?: text?.takeIf { it.isNotEmpty() } + ?: placeholderText + ?: "" +} + +fun ICellTree.Cell.getSelectableText(): String? { + return 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 { + return runLayoutOnCell(cell) { runLayoutOnCell(it) } +} + +fun runLayoutOnCell(cell: ICellTree.Cell, layoutChild: (ICellTree.Cell) -> LayoutedText): LayoutedText { + return 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..7b277c87 --- /dev/null +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/text/shared/TextEditorService.kt @@ -0,0 +1,152 @@ +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 { + return 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..8dac6a1e --- /dev/null +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/text/shared/celltree/BackendCellTree.kt @@ -0,0 +1,120 @@ +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 { + return withTreeLock { + super.createCell(id).also { + operations += NewCellOp(it.getId()) + } + } + } + + override fun newCellInstance( + id: CellInstanceId, + parent: CellImpl?, + ): CellImpl { + return 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 { + return 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? { + return super.getParent() as BackendCellImpl? + } + + override fun getChildren(): List { + return super.getChildren() as List + } + + override fun getChildAt(index: Int): BackendCellImpl? { + return super.getChildAt(index) as BackendCellImpl? + } + + override fun addNewChild(index: Int): IMutableCellTree.MutableCell { + return 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..358afb71 --- /dev/null +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/text/shared/celltree/CellPropertyValue.kt @@ -0,0 +1,18 @@ +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..66ed0637 --- /dev/null +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/text/shared/celltree/CellTreeBase.kt @@ -0,0 +1,210 @@ +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 { + return withTreeLock { allCells[id] ?: throw NoSuchElementException("Cell ID: ${id.id}") } + } + + final override fun createCell(): IMutableCellTree.MutableCell { + return createCell(CellInstanceId(nextId++)) + } + + override fun createCell(id: CellInstanceId): IMutableCellTree.MutableCell { + return withTreeLock { + require(!allCells.containsKey(id)) { "Cell already exists: $id" } + newCellInstance(id, null).also { + registerCell(it) + detachedCells.add(id) + } + } + } + + fun resolveCell(reference: CellReference): List { + return 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 referencesIndexList_ = ResettableLazy { + withTreeLock { + IncrementalList.concat( + IncrementalList.of(this.cellReferences.map { it to id }), + IncrementalList.concat(getChildren().map { (it as CellImpl).referencesIndexList }), + ) + } + } + val referencesIndexList: IncrementalList> by referencesIndexList_ + + override fun getTree(): IMutableCellTree = this@CellTreeBase + + override fun getId(): CellInstanceId = id + + override fun getParent(): IMutableCellTree.MutableCell? = withTreeLock { parent } + + override fun isAttached(): Boolean { + return withTreeLock { this == root || parent?.isAttached() == true } + } + + override fun getProperty(key: CellPropertyKey): T { + return 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) referencesIndexList_.reset() + } + } + + fun removeProperty(key: String) { + withTreeLock { + properties.remove(key) + } + } + + override fun removeProperty(key: CellPropertyKey<*>) { + withTreeLock { + properties.remove(key.name) + if (key == CommonCellProperties.cellReferences) referencesIndexList_.reset() + } + } + + override fun hasProperty(key: CellPropertyKey<*>): Boolean { + return withTreeLock { properties.containsKey(key.name) } + } + + override fun getChildren(): List { + return withTreeLock { children } + } + + override fun getChildAt(index: Int): IMutableCellTree.MutableCell? { + return withTreeLock { children.getOrNull(index) } + } + + fun addNewChild(index: Int, childId: CellInstanceId): IMutableCellTree.MutableCell { + return withTreeLock { + newCellInstance(childId, this).also { + children.add(index, it) + registerCell(it) + } + } + } + + override fun addNewChild(index: Int): IMutableCellTree.MutableCell { + return withTreeLock { + referencesIndexList_.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?.referencesIndexList_?.reset() + targetParent.referencesIndexList_.reset() + val oldParent = parent + oldParent?.children?.remove(this) + targetParent.children.add(index, this) + parent = targetParent + detachedCells.remove(id) + } + } + + override fun detach() { + withTreeLock { + parent?.referencesIndexList_?.reset() + detachedCells.add(id) + parent?.children?.remove(this) + parent = null + } + } + + override fun delete() { + withTreeLock { + parent?.referencesIndexList_?.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 { + return 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..03866ad9 --- /dev/null +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/text/shared/celltree/CellTreeOp.kt @@ -0,0 +1,30 @@ +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..0fa87c8e --- /dev/null +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/text/shared/celltree/ICellTree.kt @@ -0,0 +1,51 @@ +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/commonTest/kotlin/org/modelix/editor/CellNavigationTest.kt b/projectional-editor/src/commonTest/kotlin/org/modelix/editor/CellNavigationTest.kt index ccbaab04..a04b3b8d 100644 --- a/projectional-editor/src/commonTest/kotlin/org/modelix/editor/CellNavigationTest.kt +++ b/projectional-editor/src/commonTest/kotlin/org/modelix/editor/CellNavigationTest.kt @@ -1,28 +1,33 @@ 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") + private val rootCell = BackendCellTree().run { + cell("root") { + cell("1") { + cell("11") { + cell("111") + cell("112") + } + cell("12") { + cell("121") + cell("122") + } } - cell("12") { - cell("121") - cell("122") - } - } - cell("2") { - cell("21") { - cell("211") - cell("212") - } - cell("22") { - cell("221") - cell("222") + cell("2") { + cell("21") { + cell("211") + cell("212") + } + cell("22") { + cell("221") + cell("222") + } } } } @@ -46,12 +51,13 @@ 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 +75,15 @@ 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 { + return 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 { + return 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..59f78d30 100644 --- a/projectional-editor/src/commonTest/kotlin/org/modelix/editor/EditorKeyboardTest.kt +++ b/projectional-editor/src/commonTest/kotlin/org/modelix/editor/EditorKeyboardTest.kt @@ -1,17 +1,24 @@ 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()!! } + 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(layoutable444, 2)) - assertEquals(CaretSelection(layoutable444, 2), editor.getSelection()) + 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) @@ -31,13 +38,17 @@ class EditorKeyboardTest { } @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()!! } + 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(layoutable444, 2)) - assertEquals(CaretSelection(layoutable444, 2), editor.getSelection()) + 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) @@ -59,9 +70,9 @@ class EditorKeyboardTest { 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()) + 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..385ada20 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 @@ -38,7 +40,8 @@ class TextLayouterTest { @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 + 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/JsEditorComponent.kt b/projectional-editor/src/jsMain/kotlin/org/modelix/editor/JsEditorComponent.kt index a7f6fa28..b492d475 100644 --- a/projectional-editor/src/jsMain/kotlin/org/modelix/editor/JsEditorComponent.kt +++ b/projectional-editor/src/jsMain/kotlin/org/modelix/editor/JsEditorComponent.kt @@ -2,12 +2,12 @@ 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 { +class JsEditorComponent(service: TextEditorService) : FrontendEditorComponent(service, JSDom()), IProducesHtml { val containerElement: IVirtualDom.HTMLElement = virtualDom.create().div("js-editor-component") { tabIndex = "-1" // allows setting keyboard focus @@ -16,13 +16,13 @@ class JsEditorComponent(engine: EditorEngine, transactionManager: IArea?, rootCe 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/jsTest/kotlin/org/modelix/editor/IncrementalJSDOMBuilderTest.kt b/projectional-editor/src/jsTest/kotlin/org/modelix/editor/IncrementalJSDOMBuilderTest.kt index 0242b324..bdd3032a 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,46 @@ 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) + 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) @@ -135,30 +97,27 @@ class IncrementalJSDOMBuilderTest { 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() + 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() + 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 +127,20 @@ 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 { + return this.createCell().also { it.setProperty(TextCellProperties.text, text) }.also(body) + } + + private fun IMutableCellTree.MutableCell.cell(text: String, body: IMutableCellTree.MutableCell.() -> Unit = {}): IMutableCellTree.MutableCell { + return 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..a4b6c324 100644 --- a/react-ssr-mps-test/src/test/kotlin/PagesTest.kt +++ b/react-ssr-mps-test/src/test/kotlin/PagesTest.kt @@ -28,7 +28,7 @@ class PagesTest { @BeforeAll @JvmStatic fun beforeAll() { - mps = GenericContainer("modelix/mps-vnc-baseimage:0.9.4-mps${System.getenv("MPS_VERSION")}") + 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) From 5e97f12ed311809f2226bedee18e86a13ea13e5e Mon Sep 17 00:00:00 2001 From: Sascha Lisson Date: Thu, 12 Feb 2026 16:34:52 +0100 Subject: [PATCH 2/4] build: migration to ktlint 1.8.0 --- .pre-commit-config.yaml | 2 +- build.gradle.kts | 14 +- .../src/main/kotlin/org/modelix/CopyMps.kt | 23 +- editor-common-mps/build.gradle.kts | 11 +- .../modelix/mps/editor/common/KtorUtils.kt | 26 +- .../interpreter/vm/core/Instructions.kt | 107 +- .../interpreter/vm/core/InterpreterVM.kt | 105 +- .../interpreter/vm/core/ProgramBuilder.kt | 41 +- .../interpreter.vm.core/InterpreterTest.kt | 74 +- kernelf-angular-demo/build.gradle.kts | 37 +- kernelf-editor/build.gradle.kts | 55 +- .../kernelf/Behavior_org_iets3_core_expr.kt | 9 +- .../kernelf/Editor_de_slisson_mps_richtext.kt | 17 +- .../kernelf/Editor_jetbrains_mps_lang_test.kt | 25 +- .../Editor_org_iets3_core_expr_base.kt | 909 +++++++-------- .../Editor_org_iets3_core_expr_collections.kt | 275 ++--- .../Editor_org_iets3_core_expr_datetime.kt | 5 +- .../Editor_org_iets3_core_expr_lambda.kt | 235 ++-- .../Editor_org_iets3_core_expr_path.kt | 27 +- .../Editor_org_iets3_core_expr_repl.kt | 274 ++--- .../Editor_org_iets3_core_expr_simpleTypes.kt | 279 ++--- ...or_org_iets3_core_expr_simpleTypes_test.kt | 21 +- .../Editor_org_iets3_core_expr_tests.kt | 655 +++++------ .../Editor_org_iets3_core_expr_toplevel.kt | 551 ++++----- .../Editor_org_iets3_core_expr_tracing.kt | 5 +- .../org/modelix/editor/kernelf/KernelfAPI.kt | 80 +- .../modelix/editor/kernelf/KernelfEditor.kt | 1 + .../editor/kernelf/CodeCompletionTest.kt | 133 ++- .../kernelf/IncrementalLayoutAfterInsert.kt | 67 +- .../org/modelix/editor/kernelf/ParsingTest.kt | 15 +- .../editor/kernelf/PropertyChangeTest.kt | 109 +- .../org/modelix/editor/kernelf/TestModel.kt | 5 +- .../modelix/editor/kernelf/TypesystemTest.kt | 14 +- .../src/jsMain/kotlin/KernelfApiJS.kt | 88 +- .../src/jsTest/kotlin/IncrementalDomTest.kt | 9 +- .../IncrementalLayouterAfterInsertJS.kt | 106 +- .../jvmTest/kotlin/kernelf/EditorToText.kt | 1 - .../editor/ssr/demo/kernelf/Application.kt | 34 +- mps-image-editor-server/build.gradle.kts | 20 +- .../mps/editor/image/ImageEditorForMPS.kt | 71 +- .../kotlin/svg/plugin/EditorChangeDetector.kt | 51 +- .../main/kotlin/svg/plugin/ReflectionUtil.kt | 17 +- .../kotlin/svg/plugin/RemoteMouseCursor.kt | 42 +- .../main/kotlin/svg/plugin/RenderSession.kt | 1002 +++++++++-------- .../svg/plugin/ServerEditorComponent.kt | 117 +- .../plugin/ServerInspectorEditorComponent.kt | 16 +- .../src/main/kotlin/svg/svg/EditorToImage.kt | 17 +- .../src/main/kotlin/svg/util/AWTExtensions.kt | 13 +- mps/build.gradle.kts | 9 +- .../kotlin/org/modelix/parser/Grammar.kt | 42 +- .../modelix/parser/GraphStructuredStack.kt | 135 ++- .../org/modelix/parser/IDisambiguator.kt | 42 +- .../kotlin/org/modelix/parser/ISymbol.kt | 128 +-- .../kotlin/org/modelix/parser/IToken.kt | 97 +- .../org/modelix/parser/LRClosureTable.kt | 39 +- .../kotlin/org/modelix/parser/LRParser.kt | 187 +-- .../kotlin/org/modelix/parser/LRTable.kt | 95 +- .../kotlin/org/modelix/parser/RuleItem.kt | 24 +- .../kotlin/org/modelix/parser/Scanner.kt | 36 +- .../org/modelix/parser/ExpressionsTest.kt | 741 ++++++------ .../org/modelix/parser/StatementsTest.kt | 70 +- .../kotlin/org/modelix/parser/TestGrammar.kt | 27 +- .../editor/ssr/client/ClientSideEditor.kt | 16 +- .../editor/ssr/client/ClientSideEditors.kt | 22 +- .../editor/ssr/client/ClientSideEditorsAPI.kt | 29 +- .../editor/ssr/client/ModelixSSRClient.kt | 29 +- .../src/jsMain/kotlin/App.kt | 21 +- .../editor/ssr/client/ModelixSSRClient.kt | 91 +- .../editor/ssr/common/MessageFromClient.kt | 5 - .../editor/ssr/common/MessageFromServer.kt | 11 +- .../build.gradle.kts | 10 +- .../editor/ssr/mps/BaseLanguageTests.kt | 70 +- .../org/modelix/editor/ssr/mps/TestBase.kt | 27 +- .../editor/ssr/mps/TestFrameworkSetupTest.kt | 11 +- projectional-editor-ssr-mps/build.gradle.kts | 20 +- .../editor/ssr/mps/EditorIntegrationForMPS.kt | 4 +- .../ssr/mps/LanguageAspectsFromMPSModules.kt | 89 +- .../editor/ssr/mps/ModelixSSRServerForMPS.kt | 184 +-- .../editor/ssr/server/ModelixSSRServer.kt | 20 +- .../org/modelix/aspects/ILanguageAspect.kt | 10 +- .../org/modelix/aspects/LanguageAspects.kt | 33 +- .../modelix/aspects/LanguageAspectsBuilder.kt | 16 +- .../aspects/behavior/BehaviorAspect.kt | 4 +- .../aspects/behavior/PolymorphicDispatch.kt | 20 +- .../behavior/PolymorphicFunctionBuilder.kt | 41 +- .../aspects/behavior/PolymorphicValue.kt | 26 +- .../modelix/constraints/ConstraintsAspect.kt | 15 +- .../org/modelix/editor/AstTransformation.kt | 241 +++- .../kotlin/org/modelix/editor/Bounds.kt | 45 +- .../org/modelix/editor/CaretPositionPolicy.kt | 56 +- .../org/modelix/editor/CaretSelection.kt | 115 +- .../org/modelix/editor/CaretSelectionView.kt | 59 +- .../org/modelix/editor/CellCreationContext.kt | 5 +- .../org/modelix/editor/CellNavigationUtils.kt | 63 +- .../org/modelix/editor/CellProperties.kt | 88 +- .../org/modelix/editor/CellReference.kt | 66 +- .../org/modelix/editor/CellSelection.kt | 52 +- .../org/modelix/editor/CellSelectionView.kt | 6 +- .../kotlin/org/modelix/editor/CellSpecBase.kt | 37 +- .../org/modelix/editor/CellTemplateBuilder.kt | 272 +++-- .../modelix/editor/CellTemplateReference.kt | 14 +- .../org/modelix/editor/CellTreeState.kt | 4 +- .../kotlin/org/modelix/editor/Cells.kt | 11 +- .../editor/CodeCompletionActionWrapper.kt | 114 +- .../org/modelix/editor/CodeCompletionMenu.kt | 130 ++- .../modelix/editor/CodeCompletionMenuUI.kt | 32 +- .../org/modelix/editor/CompletionPattern.kt | 84 +- .../org/modelix/editor/ConceptEditor.kt | 82 +- .../kotlin/org/modelix/editor/EditorAspect.kt | 33 +- .../kotlin/org/modelix/editor/EditorDSL.kt | 17 +- .../kotlin/org/modelix/editor/EditorEngine.kt | 128 ++- .../org/modelix/editor/EditorTestUtils.kt | 86 +- .../modelix/editor/FrontendEditorComponent.kt | 100 +- .../kotlin/org/modelix/editor/ICellAction.kt | 87 +- .../org/modelix/editor/IEditorComponentUI.kt | 6 +- .../org/modelix/editor/INonExistingNode.kt | 101 +- .../org/modelix/editor/IProducesHtml.kt | 6 +- .../editor/IncrementalVirtualDOMBuilder.kt | 114 +- .../kotlin/org/modelix/editor/JSMouseEvent.kt | 38 +- .../org/modelix/editor/ModelApiExtensions.kt | 26 +- .../editor/ReplaceNodeActionProvider.kt | 44 +- .../kotlin/org/modelix/editor/Selection.kt | 6 +- .../kotlin/org/modelix/editor/TextLayouter.kt | 80 +- .../kotlin/org/modelix/editor/TreeList.kt | 96 +- .../kotlin/org/modelix/editor/VirtualDom.kt | 213 +++- .../editor/celltemplate/CellTemplate.kt | 102 +- .../celltemplate/ChangePropertyCellAction.kt | 13 +- .../editor/celltemplate/ChildCellTemplate.kt | 121 +- .../celltemplate/CollectionCellTemplate.kt | 10 +- .../celltemplate/ConstantCellTemplate.kt | 63 +- .../editor/celltemplate/FlagCellTemplate.kt | 18 +- .../ForceShowOptionalCellAction.kt | 17 +- .../celltemplate/IGrammarConditionSymbol.kt | 1 + .../editor/celltemplate/IGrammarSymbol.kt | 17 +- .../celltemplate/InstantiateNodeCellAction.kt | 12 +- .../InstantiateNodeCompletionAction.kt | 50 +- .../editor/celltemplate/LabelCellTemplate.kt | 22 +- .../editor/celltemplate/Levensthein.kt | 11 +- .../celltemplate/NewLineCellTemplate.kt | 16 +- .../celltemplate/NoSpaceCellTemplate.kt | 16 +- .../celltemplate/NotationRootCellTemplate.kt | 11 +- .../celltemplate/OptionalCellTemplate.kt | 48 +- .../editor/celltemplate/OverrideText.kt | 16 +- .../editor/celltemplate/ParserForEditor.kt | 41 +- .../celltemplate/PropertyCellTemplate.kt | 83 +- .../celltemplate/ReferenceCellTemplate.kt | 66 +- .../text/backend/BackendEditorComponent.kt | 50 +- .../text/backend/TextEditorServiceImpl.kt | 356 +++--- .../modelix/editor/text/backend/Validator.kt | 31 +- .../editor/text/frontend/FrontendCellTree.kt | 95 +- .../editor/text/shared/TextEditorService.kt | 79 +- .../text/shared/celltree/BackendCellTree.kt | 45 +- .../text/shared/celltree/CellPropertyValue.kt | 12 +- .../text/shared/celltree/CellTreeBase.kt | 109 +- .../editor/text/shared/celltree/CellTreeOp.kt | 40 +- .../editor/text/shared/celltree/ICellTree.kt | 38 +- .../kotlin/org/modelix/scopes/DefaultScope.kt | 5 +- .../kotlin/org/modelix/scopes/EmptyScope.kt | 7 +- .../kotlin/org/modelix/scopes/IScope.kt | 5 +- .../kotlin/org/modelix/scopes/ScopeAspect.kt | 51 +- .../modelix/typesystem/TypesystemAspect.kt | 155 ++- .../org/modelix/editor/CellNavigationTest.kt | 73 +- .../org/modelix/editor/EditorKeyboardTest.kt | 150 ++- .../org/modelix/editor/TextLayouterTest.kt | 44 +- .../kotlin/org/modelix/editor/DomUtils.kt | 17 +- .../jsMain/kotlin/org/modelix/editor/JSDom.kt | 135 +-- .../org/modelix/editor/JsEditorComponent.kt | 13 +- .../modelix/editor/JsKeyboardTranslator.kt | 20 +- .../editor/IncrementalJSDOMBuilderTest.kt | 94 +- .../src/test/kotlin/PagesTest.kt | 70 +- react-ssr-mps/build.gradle.kts | 33 +- .../react/ssr/mps/CompiledMPSRenderer.kt | 153 +-- .../react/ssr/mps/InterpretedMPSRenderer.kt | 290 +++-- .../react/ssr/mps/ModelCheckerIntegration.kt | 69 +- .../ssr/mps/OpenNodeInWebEditorAction.kt | 20 +- .../react/ssr/mps/ReactSSRServerForMPS.kt | 59 +- .../ssr/mps/ToggleInterpretedRenderer.kt | 8 +- .../react/ssr/mps/aspect/DescriptorCache.kt | 62 +- .../ssr/mps/aspect/ReactPageDescriptor.kt | 38 +- .../mps/aspect/ReactSSRAspectDescriptor.kt | 89 +- react-ssr-server/build.gradle.kts | 11 +- .../react/ssr/server/ComponentBuilder.kt | 289 +++-- .../react/ssr/server/GenericNodeRenderer.kt | 153 +-- .../org/modelix/react/ssr/server/Messages.kt | 33 +- .../react/ssr/server/ReactSSRServer.kt | 125 +- .../modelix/react/ssr/server/RendererBase.kt | 9 +- .../modelix/react/ssr/server/RendererCall.kt | 26 +- .../mpsadapters/tomps/ModelixNodeAsMPSNode.kt | 219 ++-- settings.gradle.kts | 10 +- 189 files changed, 8889 insertions(+), 6318 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bbc970c4..af63934a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -27,5 +27,5 @@ repos: hooks: - id: pretty-format-kotlin args: - - --ktlint-version=0.50.0 + - --ktlint-version=1.8.0 - --autofix diff --git a/build.gradle.kts b/build.gradle.kts index 41ea0960..d6f28707 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -62,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 { @@ -84,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() 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/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-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 8511b415..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 @@ -29,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 { @@ -37,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)) @@ -53,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/"))) { @@ -65,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() @@ -105,9 +114,7 @@ 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() @@ -115,11 +122,19 @@ object KernelfAPI { return sb.toString() } - fun renderNode(cellTreeState: CellTreeState, rootNode: INode, tagConsumer: TagConsumer) { + fun renderNode( + cellTreeState: CellTreeState, + rootNode: INode, + tagConsumer: TagConsumer, + ) { renderTypedNode(cellTreeState, rootNode.typed(), tagConsumer) } - fun renderTypedNode(cellTreeState: CellTreeState, rootNode: ITypedNode, tagConsumer: TagConsumer) { + fun renderTypedNode( + cellTreeState: CellTreeState, + rootNode: ITypedNode, + tagConsumer: TagConsumer, + ) { ModelFacade.readNode(rootNode.unwrap()) { val cell = editorEngine.createCell(cellTreeState, rootNode.unwrap()) val layout = runLayoutOnCell(cell) @@ -137,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 4179952a..f0cb0d34 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 @@ -57,18 +57,35 @@ class CodeCompletionTest { ModelData.fromJson(modelJson).load(branch) val engine = EditorEngine(IncrementalEngine()) KernelfEditor.register(engine) - testSuite = branch.computeRead { branch.getArea().getRoot().allChildren.ofType().models.rootNodes.ofType().first() } + 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.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" - } + 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) } } @@ -80,53 +97,59 @@ class CodeCompletionTest { } @Test - fun printModel() = runCompletionTest { - println(editor.getRootCell().layout.toString()) - } + fun printModel() = + runCompletionTest { + println(editor.getRootCell().layout.toString()) + } @Test - fun printActions() = runCompletionTest { - val actions = getSubstituteActions(getNumberLiteralCell()) - actions.forEach { println(it.getCompletionPattern() + " | " + it.getDescription()) } - } + fun printActions() = + runCompletionTest { + val actions = getSubstituteActions(getNumberLiteralCell()) + actions.forEach { println(it.getCompletionPattern() + " | " + it.getDescription()) } + } @Test - fun notEmpty() = runCompletionTest { - val actions = getSubstituteActions(getNumberLiteralCell()) - assertTrue(actions.isNotEmpty()) - } + fun notEmpty() = + runCompletionTest { + val actions = getSubstituteActions(getNumberLiteralCell()) + assertTrue(actions.isNotEmpty()) + } @Test - fun actionsOnNameProperty() = runCompletionTest { - val namePropertyCell = editor.getRootCell().descendants().find { it.getVisibleText() == "stringTests" }!! - editor.changeSelection(CaretSelection(editor, namePropertyCell.layoutable()!!, 0)) + fun actionsOnNameProperty() = + runCompletionTest { + val namePropertyCell = editor.getRootCell().descendants().find { it.getVisibleText() == "stringTests" }!! + editor.changeSelection(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.getParent(), 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() = 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) - } + 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)!! @@ -134,17 +157,21 @@ class CodeCompletionTest { 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() + 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 95c79992..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 @@ -41,31 +41,50 @@ open class IncrementalLayoutAfterInsert { lateinit var testSuite: N_TestSuite @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) - } + 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) + } - private fun runLayoutTest(body: suspend () -> Unit) = runTest { - KernelfLanguages.registerAll() - branch = PBranch(ModelFacade.newLocalTree(useRoleIds = false), IdGenerator.getInstance(56754)).withIncrementalComputationSupport() - ModelData.fromJson(modelJson).load(branch) + private fun runLayoutTest(body: suspend () -> Unit) = + runTest { + 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 = 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) + 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() } } - 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 d049a293..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 @@ -45,59 +45,76 @@ class PropertyChangeTest { private fun ICellTree.Cell.backend() = backend(service, editor) @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) - } + 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) + } @Test - 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()) - } + 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()) + } - 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" + 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" + } } } - 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" + 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) } - CaretSelection(layoutable, 0) + body() + KernelfLanguages.languages.forEach { it.unregister() } } - 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 e7d87224..a289315b 100644 --- a/kernelf-editor/src/jsMain/kotlin/KernelfApiJS.kt +++ b/kernelf-editor/src/jsMain/kotlin/KernelfApiJS.kt @@ -28,29 +28,44 @@ 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(cellTreeState: CellTreeState, rootNode: INode): HTMLElement { + private fun renderNodeAsDom( + cellTreeState: CellTreeState, + rootNode: INode, + ): HTMLElement { val tagConsumer = document.createTree() KernelfAPI.renderNode(cellTreeState, rootNode, tagConsumer) return tagConsumer.finalize() } - fun updateNodeAsDom(cellTreeState: CellTreeState, 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) @@ -70,44 +85,53 @@ object KernelfApiJS { } 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.updateNow() + 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.updateNow() + 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 f82fb0f1..9f538a5e 100644 --- a/kernelf-editor/src/jsTest/kotlin/IncrementalDomTest.kt +++ b/kernelf-editor/src/jsTest/kotlin/IncrementalDomTest.kt @@ -34,10 +34,11 @@ class IncrementalDomTest { 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 abd991fe..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 @@ -51,41 +51,85 @@ open class IncrementalLayoutAfterInsertJS { @Ignore @Test - 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 + 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) + 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) + 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() } } - 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 b4d9cf24..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,23 +24,33 @@ 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().asModel()) 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/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 0d8e0f30..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 @@ -16,13 +16,15 @@ class ClientSideEditor( existingContainerElement: HTMLDivElement? = null, 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 = editorElementId - +"Loading ..." - } + 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 = editorElementId + +"Loading ..." + } fun dispose() { // TODO call this method somewhere 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 2106a02a..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 @@ -10,23 +10,29 @@ import org.w3c.dom.HTMLElement private val LOG = KotlinLogging.logger { } -class ClientSideEditors(val service: TextEditorService, val coroutineScope: CoroutineScope) { - +class ClientSideEditors( + val service: TextEditorService, + val coroutineScope: CoroutineScope, +) { private val editors: MutableMap = HashMap() private var nextEditorId: Long = 1000 - fun createEditor(rootNodeReference: INodeReference, existingContainerElement: HTMLDivElement? = null): HTMLElement { + 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) - val editorSession = ClientSideEditor( - editorElementId = editorElementId, - existingContainerElement = existingContainerElement, - editorComponent = editorComponent - ) + 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 1a0293bf..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("rpc") - }.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) - } + 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 72813731..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 @@ -19,26 +19,30 @@ 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 val rpcClient = httpClient.rpc(urlString = url) { - rpcConfig { - serialization { - json() + 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 createEditor( + rootNodeReference: INodeReference, + existingContainerElement: HTMLDivElement? = null, + ): HTMLElement = editors.createEditor(rootNodeReference, existingContainerElement) } inline fun KLogger.logExceptions(body: () -> R): R { @@ -50,8 +54,7 @@ inline fun KLogger.logExceptions(body: () -> R): R { } } -fun CoroutineScope.launchLogging(body: suspend CoroutineScope.() -> 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/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..18b31b24 100644 --- a/projectional-editor-ssr-mps-languages/build.gradle.kts +++ b/projectional-editor-ssr-mps-languages/build.gradle.kts @@ -34,7 +34,8 @@ intellij { project(":projectional-editor-ssr-mps"), project(":editor-common-mps"), project(":react-ssr-mps"), - ) + listOf( + ) + + listOf( // "Git4Idea", // "Subversion", // "com.intellij.copyright", @@ -48,7 +49,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 +69,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 +80,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 70813649..94325154 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 @@ -35,7 +35,7 @@ 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: FrontendEditorComponent lateinit var service: TextEditorServiceImpl @@ -63,9 +63,10 @@ class BaseLanguageTests : TestBase("SimpleProject") { coroutineScope = CoroutineScope(Dispatchers.Default) service = TextEditorServiceImpl(editorEngine, MPSArea(mpsProject.repository).asModel(), coroutineScope) runBlocking { - editor = FrontendEditorComponent(service).also { - it.openNode(classNode.getNodeReference()).await() - } + editor = + FrontendEditorComponent(service).also { + it.openNode(classNode.getNodeReference()).await() + } editor.flush() } } @@ -104,7 +105,10 @@ class BaseLanguageTests : TestBase("SimpleProject") { 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)) } @@ -130,20 +134,23 @@ class BaseLanguageTests : TestBase("SimpleProject") { } } - fun getCodeCompletionEntries(pattern: String): List { - return readAction { + fun getCodeCompletionEntries(pattern: String): List = + readAction { val selection = editor.getSelection() as CaretSelection - val actionProviders: Sequence = selection.layoutable.cell.backend().getSubstituteActions() + 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 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(""" @@ -517,28 +524,45 @@ class BaseLanguageTests : TestBase("SimpleProject") { } 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.backend().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.backend().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) @@ -546,14 +570,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; @@ -562,5 +591,6 @@ class BaseLanguageTests : TestBase("SimpleProject") { """, false) fun `test completion 1`() = runParsingTest("""intᚹ""") + 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 17b739d9..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) { @@ -140,9 +140,10 @@ class ModelixSSRServerForMPS : Disposable { 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/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 7116c6b2..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 @@ -26,18 +26,24 @@ import kotlin.time.Duration.Companion.days private val LOG = KotlinLogging.logger { } -class ModelixSSRServer(private val model: IMutableModel) { - +class ModelixSSRServer( + private val model: IMutableModel, +) { private val incrementalEngine = IncrementalEngine() val editorEngine: EditorEngine = EditorEngine(incrementalEngine) private val lock = Any() private val coroutinesScope = CoroutineScope(Dispatchers.Default) 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() } } - } + 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() 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 a9f69dfe..cc78da87 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CaretPositionPolicy.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CaretPositionPolicy.kt @@ -12,7 +12,10 @@ sealed interface ICaretPositionPolicy { } @Serializable -data class CaretPositionPolicyWithIndex(val policy: ICaretPositionPolicy, val index: Int) : ICaretPositionPolicy { +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( @@ -22,12 +25,11 @@ data class CaretPositionPolicyWithIndex(val policy: ICaretPositionPolicy, val in index ) - override fun getBestSelection(editor: FrontendEditorComponent): CaretSelection? { - return policy.getBestSelection(editor)?.let { + 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 @@ -39,23 +41,31 @@ 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, - ) + 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 + 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) } @@ -116,8 +126,16 @@ data class SavedCaretPosition( } companion object { - fun saveAndRun(editor: FrontendEditorComponent, 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 44dfb7ef..f0a28ac4 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CaretSelection.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CaretSelection.kt @@ -7,9 +7,20 @@ import org.modelix.editor.text.shared.celltree.cellReferences import kotlin.math.max import kotlin.math.min -class CaretSelection(val editor: FrontendEditorComponent, val layoutable: LayoutableCell, val start: Int, val end: Int, val desiredXPosition: Int? = null) : Selection() { +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( + 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) @@ -20,9 +31,7 @@ class CaretSelection(val editor: FrontendEditorComponent, val layoutable: Layout fun getSelectedTextRange() = min(start, end) until max(start, end) - override fun getSelectedCells(): List { - return listOf(layoutable.cell) - } + override fun getSelectedCells(): List = listOf(layoutable.cell) override fun isValid(): Boolean { val visibleText = editor.getRootCell().layout @@ -30,11 +39,12 @@ class CaretSelection(val editor: FrontendEditorComponent, val layoutable: Layout return visibleText === ownText } - private fun reResolveLayoutable(editor: FrontendEditorComponent): LayoutableCell? { - return layoutable.cell.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: FrontendEditorComponent): Selection? { val newLayoutable = reResolveLayoutable(editor) ?: return null @@ -53,9 +63,11 @@ class CaretSelection(val editor: FrontendEditorComponent, val layoutable: Layout editor.changeSelection(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) @@ -67,6 +79,7 @@ class CaretSelection(val editor: FrontendEditorComponent, val layoutable: Layout } } } + KnownKeys.ArrowRight -> { if (end < (layoutable.cell.getSelectableText()?.length ?: 0)) { if (event.modifiers.shift) { @@ -75,9 +88,11 @@ class CaretSelection(val editor: FrontendEditorComponent, val layoutable: Layout editor.changeSelection(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) @@ -89,9 +104,11 @@ class CaretSelection(val editor: FrontendEditorComponent, val layoutable: Layout } } } + KnownKeys.ArrowDown -> { selectNextPreviousLine(true) } + KnownKeys.ArrowUp -> { if (event.modifiers.meta) { layoutable.cell.let { editor.changeSelection(CellSelection(editor, it, true, this)) } @@ -99,16 +116,19 @@ class CaretSelection(val editor: FrontendEditorComponent, val layoutable: Layout selectNextPreviousLine(false) } } + KnownKeys.Tab -> { 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 until posToDelete, "", editor, true) @@ -125,9 +145,11 @@ class CaretSelection(val editor: FrontendEditorComponent, val layoutable: Layout replaceText(min(start, end) until max(start, end), "", editor, true) } } + KnownKeys.Enter -> { editor.serviceCall { executeInsert(editor.editorId, layoutable.cell.getId()) } } + else -> { val typedText = event.typedText if (!typedText.isNullOrEmpty()) { @@ -158,13 +180,18 @@ class CaretSelection(val editor: FrontendEditorComponent, val layoutable: Layout fun getCurrentCellText() = layoutable.cell.getSelectableText() ?: "" - suspend fun replaceText(newText: String): Boolean { - return replaceText(0 until getCurrentCellText().length, newText, editor, false) - } + suspend fun replaceText(newText: String): Boolean = replaceText(0 until getCurrentCellText().length, newText, editor, false) - private suspend fun replaceText(range: IntRange, replacement: String, editor: FrontendEditorComponent, triggerCompletion: Boolean): Boolean { - return editor.serviceCall { replaceText(editor.editorId, layoutable.cell.getId(), range, replacement, triggerCompletion) }.result - } + 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 @@ -193,15 +220,23 @@ class CaretSelection(val editor: FrontendEditorComponent, val layoutable: Layout 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() } @@ -212,10 +247,22 @@ class CaretSelection(val editor: FrontendEditorComponent, val layoutable: Layout 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) + 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) } + return words.filterIsInstance().lastOrNull()?.let { + CaretSelection(editor, 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 cac86847..9d7f69ce 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CaretSelectionView.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CaretSelectionView.kt @@ -8,8 +8,10 @@ import org.modelix.editor.text.frontend.getVisibleText import kotlin.math.max import kotlin.math.min -class CaretSelectionView(selection: CaretSelection, val editor: FrontendEditorComponent) : SelectionView(selection) { - +class CaretSelectionView( + selection: CaretSelection, + val editor: FrontendEditorComponent, +) : SelectionView(selection) { private fun hasRange() = selection.start != selection.end override fun produceHtml(consumer: TagConsumer) { @@ -22,7 +24,10 @@ class CaretSelectionView(selection: CaretSelection, val editor: FrontendEditorCo } 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. @@ -56,11 +61,21 @@ class CaretSelectionView(selection: CaretSelection, val editor: FrontendEditorCo } 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() @@ -75,21 +90,33 @@ class CaretSelectionView(selection: CaretSelection, val editor: FrontendEditorCo } } -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 dfc2c5a8..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 cellTreeState: CellTreeState) +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 edf248a6..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,28 +1,24 @@ package org.modelix.editor -fun Cell.nextCells(): Sequence { - return nextSiblings().flatMap { it.descendantsAndSelf() } + (getParent()?.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) } + (getParent()?.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 getParent()?.previousLeaf() @@ -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.getParent() ?: return emptySequence() - return parent.getChildren().asReversed().asSequence().dropWhile { it != this }.drop(1) + return parent + .getChildren() + .asReversed() + .asSequence() + .dropWhile { it != this } + .drop(1) } fun Cell.nextSiblings(): Sequence { val parent = this.getParent() ?: return emptySequence() - return parent.getChildren().asSequence().dropWhile { it != this }.drop(1) + 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.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 343165fd..dcdf1887 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellProperties.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellProperties.kt @@ -7,21 +7,21 @@ 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() @@ -31,29 +31,50 @@ class CellProperties : Freezable() { fun getKeys(): Set> = properties.keys } -sealed class CellPropertyKey(val name: String, val defaultValue: E, val inherits: Boolean = false, val frontend: Boolean = true) { +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) { +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) { +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? } @@ -67,13 +88,9 @@ object CellReferenceListPropertyKey : TODO("Not yet implemented") } - override fun toSerializableValue(value: List): CellPropertyValue<*>? { - return CellReferenceListValue(value) - } + override fun toSerializableValue(value: List): CellPropertyValue<*>? = CellReferenceListValue(value) - override fun fromSerializableValue(value: Any?): List { - return value as List - } + override fun fromSerializableValue(value: Any?): List = value as List } class EnumCellPropertyKey>( @@ -82,11 +99,13 @@ class EnumCellPropertyKey>( val deserializer: (Any?) -> E, inherits: Boolean = false, frontend: Boolean = true, -) : - CellPropertyKey(name, defaultValue, inherits, frontend) { +) : 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) } @@ -95,33 +114,27 @@ inline fun > enumCellPropertyKey( defaultValue: E, inherits: Boolean = false, frontend: Boolean = true, -): EnumCellPropertyKey { - return EnumCellPropertyKey( +): 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") - } +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 valueFromString(str: String?): E = throw UnsupportedOperationException("backend only") - override fun toSerializableValue(value: E): CellPropertyValue<*>? { - throw UnsupportedOperationException("backend only") - } + override fun toSerializableValue(value: E): CellPropertyValue<*>? = throw UnsupportedOperationException("backend only") - override fun fromSerializableValue(value: Any?): E { - throw UnsupportedOperationException("backend only") - } + override fun fromSerializableValue(value: Any?): E = throw UnsupportedOperationException("backend only") } fun CellPropertyKey.from(cell: Cell) = cell.getProperty(this) @@ -161,4 +174,5 @@ object TextCellProperties { } 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 1869511b..b521bd5d 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellReference.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellReference.kt @@ -19,42 +19,66 @@ import org.modelix.model.api.IReferenceLinkReference sealed class CellReference @Serializable -data class PropertyCellReference(val property: IPropertyReference, val nodeRef: INodeReference) : CellReference() +data class PropertyCellReference( + val property: IPropertyReference, + val nodeRef: INodeReference, +) : CellReference() -fun FrontendEditorComponent.resolvePropertyCell(property: IProperty, nodeRef: INodeReference): Cell? = - resolveCell(PropertyCellReference(property.toReference(), nodeRef)).firstOrNull() +fun FrontendEditorComponent.resolvePropertyCell( + property: IProperty, + nodeRef: INodeReference, +): Cell? = resolveCell(PropertyCellReference(property.toReference(), nodeRef)).firstOrNull() -fun FrontendEditorComponent.resolvePropertyCell(property: IProperty, node: INode): Cell? = - resolvePropertyCell(property, node.reference) +fun FrontendEditorComponent.resolvePropertyCell( + property: IProperty, + node: INode, +): Cell? = resolvePropertyCell(property, node.reference) -fun FrontendEditorComponent.resolvePropertyCell(property: IProperty, node: ITypedNode): Cell? = - resolvePropertyCell(property, node.untyped()) +fun FrontendEditorComponent.resolvePropertyCell( + property: IProperty, + node: ITypedNode, +): Cell? = resolvePropertyCell(property, node.untyped()) -fun FrontendEditorComponent.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 FrontendEditorComponent.resolveNodeCell(nodeRef: INodeReference): Cell? = - resolveCell(NodeCellReference(nodeRef)).firstOrNull() +fun FrontendEditorComponent.resolveNodeCell(nodeRef: INodeReference): Cell? = resolveCell(NodeCellReference(nodeRef)).firstOrNull() -fun FrontendEditorComponent.resolveNodeCell(node: INode): Cell? = - resolveNodeCell(node.reference) +fun FrontendEditorComponent.resolveNodeCell(node: INode): Cell? = resolveNodeCell(node.reference) -fun FrontendEditorComponent.resolveNodeCell(node: ITypedNode): Cell? = - resolveNodeCell(node.untypedReference()) +fun FrontendEditorComponent.resolveNodeCell(node: ITypedNode): Cell? = resolveNodeCell(node.untypedReference()) @Serializable -data class ChildNodeCellReference(val parentNodeRef: INodeReference, val link: IChildLinkReference, val index: Int = 0) : CellReference() +data class ChildNodeCellReference( + val parentNodeRef: INodeReference, + val link: IChildLinkReference, + val index: Int = 0, +) : CellReference() @Serializable -data class SeparatorCellReference(val before: CellReference) : CellReference() +data class SeparatorCellReference( + val before: CellReference, +) : CellReference() @Serializable -data class ReferencedNodeCellReference(val sourceNodeRef: INodeReference, val link: IReferenceLinkReference) : CellReference() +data class ReferencedNodeCellReference( + val sourceNodeRef: INodeReference, + val link: IReferenceLinkReference, +) : CellReference() @Serializable -data class TemplateCellReference(val template: ICellTemplateReference, val node: INodeReference) : CellReference() +data class TemplateCellReference( + val template: ICellTemplateReference, + val node: INodeReference, +) : CellReference() @Serializable -data class PlaceholderCellReference(val childCellRef: TemplateCellReference) : CellReference() +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 931d592d..11912b5a 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellSelection.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellSelection.kt @@ -4,32 +4,36 @@ import org.modelix.editor.text.frontend.getSelectableText import org.modelix.editor.text.frontend.layout import org.modelix.editor.text.shared.celltree.cellReferences -data class CellSelection(val editor: FrontendEditorComponent, val cell: Cell, val directionLeft: Boolean, val previousSelection: Selection?) : Selection() { - 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 cell.isAttached() - } + override fun isValid(): Boolean = cell.isAttached() - override fun update(editor: FrontendEditorComponent): Selection? { - return cell.cellReferences.asSequence() + override fun update(editor: FrontendEditorComponent): Selection? = + cell.cellReferences + .asSequence() .flatMap { editor.resolveCell(it) } .map { CellSelection(editor, it, directionLeft, previousSelection?.update(editor)) } .firstOrNull() - } override suspend fun processKeyDown(event: JSKeyboardEvent): Boolean { when (event.knownKey) { KnownKeys.ArrowUp -> { if (event.modifiers.meta) { - cell.ancestors().firstOrNull { it.getProperty(CommonCellProperties.selectable) } + cell + .ancestors() + .firstOrNull { it.getProperty(CommonCellProperties.selectable) } ?.let { editor.changeSelection(CellSelection(editor, it, directionLeft, this)) } } else { unwrapCaretSelection()?.selectNextPreviousLine(false) } } + KnownKeys.ArrowDown -> { if (event.modifiers == Modifiers.META && previousSelection != null) { editor.changeSelection(previousSelection) @@ -37,11 +41,14 @@ data class CellSelection(val editor: FrontendEditorComponent, val cell: Cell, va 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() } + cell + .ancestors() + .firstOrNull { it.isSelectable() } ?.let { editor.changeSelection(CellSelection(editor, it, directionLeft, this)) } } else { previousSelection?.let { editor.changeSelection(it) } @@ -53,15 +60,20 @@ data class CellSelection(val editor: FrontendEditorComponent, val cell: Cell, va } else { val tabTargets = cell.descendantsAndSelf().filter { it.isTabTarget() } if (event.knownKey == KnownKeys.ArrowLeft) { - tabTargets.firstOrNull()?.layoutable() + tabTargets + .firstOrNull() + ?.layoutable() ?.let { editor.changeSelection(CaretSelection(editor, it, 0)) } } else { - tabTargets.lastOrNull()?.layoutable() + tabTargets + .lastOrNull() + ?.layoutable() ?.let { editor.changeSelection(CaretSelection(editor, it, it.cell.getSelectableText()?.length ?: 0)) } } } } } + else -> { val typedText = event.typedText if (!typedText.isNullOrEmpty()) { @@ -80,14 +92,16 @@ data class CellSelection(val editor: FrontendEditorComponent, val cell: Cell, va 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 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 70d6dab9..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: FrontendEditorComponent) : 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/CellSpecBase.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellSpecBase.kt index 9b105f85..9797b607 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellSpecBase.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellSpecBase.kt @@ -5,7 +5,9 @@ import org.modelix.editor.text.shared.celltree.ICellTree import org.modelix.editor.text.shared.celltree.IMutableCellTree import org.modelix.model.api.INode -sealed class CellSpecBase : Freezable(), ILocalOrChildNodeCell { +sealed class CellSpecBase : + Freezable(), + ILocalOrChildNodeCell { val cellReferences: MutableList = ArrayList() val children: MutableList = ArrayList() val properties = CellProperties() @@ -14,7 +16,10 @@ sealed class CellSpecBase : 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() @@ -33,27 +38,35 @@ sealed class CellSpecBase : Freezable(), ILocalOrChildNodeCell { open fun isVisible(): Boolean = false } -fun Cell.isVisible() = when (getProperty(CommonCellProperties.type)) { - ECellType.COLLECTION -> false - ECellType.TEXT -> true -} +fun Cell.isVisible() = + when (getProperty(CommonCellProperties.type)) { + ECellType.COLLECTION -> false + ECellType.TEXT -> true + } sealed interface ILocalOrChildNodeCell -class ChildSpecReference(val childNode: INode) : ILocalOrChildNodeCell +class ChildSpecReference( + val childNode: INode, +) : ILocalOrChildNodeCell class CellSpec : CellSpecBase() -class TextCellSpec(val text: String, val placeholderText: String = "") : CellSpecBase() { - 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)) 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 2247cd5c..19c67796 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTemplateReference.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTemplateReference.kt @@ -7,10 +7,18 @@ import org.modelix.model.api.ConceptReference sealed interface ICellTemplateReference @Serializable -data class RooCellTemplateReference(val conceptEditorId: Long, val subConcept: ConceptReference) : ICellTemplateReference +data class RooCellTemplateReference( + val conceptEditorId: Long, + val subConcept: ConceptReference, +) : ICellTemplateReference @Serializable -data class ChildCellTemplateReference(val parent: ICellTemplateReference, val index: Int) : ICellTemplateReference +data class ChildCellTemplateReference( + val parent: ICellTemplateReference, + val index: Int, +) : ICellTemplateReference @Serializable -data class SeparatorCellTemplateReference(val parent: ICellTemplateReference) : ICellTemplateReference +data class SeparatorCellTemplateReference( + val parent: ICellTemplateReference, +) : ICellTemplateReference diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTreeState.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTreeState.kt index 8aa7f7b7..893aaf29 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTreeState.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTreeState.kt @@ -21,4 +21,6 @@ class CellTreeState { 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 00c2a7d8..c93f4f53 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/Cells.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/Cells.kt @@ -6,11 +6,13 @@ 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 } @@ -92,16 +94,17 @@ typealias MutableCell = IMutableCellTree.MutableCell // } fun ICellTree.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 b418fc49..260428b9 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CodeCompletionActionWrapper.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CodeCompletionActionWrapper.kt @@ -2,32 +2,34 @@ package org.modelix.editor import org.modelix.editor.text.backend.BackendEditorComponent -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) - } - - 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) { +class CodeCompletionActionWithPostprocessor( + action: ICodeCompletionAction, + val after: () -> Unit, +) : CodeCompletionActionWrapper(action) { override fun execute(editor: BackendEditorComponent): ICaretPositionPolicy? { val policy = wrappedAction.execute(editor) after() @@ -35,74 +37,76 @@ class CodeCompletionActionWithPostprocessor(action: ICodeCompletionAction, val a } } -class CodeCompletionActionWithCaretPolicy(action: ICodeCompletionAction, val policy: (ICaretPositionPolicy?) -> ICaretPositionPolicy?) : CodeCompletionActionWrapper(action) { - override fun execute(editor: BackendEditorComponent): 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 95f4b479..8889569b 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CodeCompletionMenu.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CodeCompletionMenu.kt @@ -15,7 +15,8 @@ class CodeCompletionMenu( initialEntries: List, initialPattern: String = "", initialCaretPosition: Int? = null, -) : IProducesHtml, IKeyboardHandler { +) : IProducesHtml, + IKeyboardHandler { val patternEditor = PatternEditor(initialPattern, initialCaretPosition) private var selectedIndex: Int = 0 private var allEntries: List = initialEntries @@ -55,16 +56,38 @@ class CodeCompletionMenu( 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.ArrowUp -> { + selectPrevious() + } + + KnownKeys.ArrowDown -> { + selectNext() + } + + KnownKeys.ArrowLeft -> { + patternEditor.moveCaret(-1) + } + + KnownKeys.ArrowRight -> { + patternEditor.moveCaret(1) + } + + KnownKeys.Escape -> { + editor.closeCodeCompletionMenu() + } + KnownKeys.Enter -> { getSelectedEntry()?.execute() } - KnownKeys.Backspace -> patternEditor.deleteText(true) - KnownKeys.Delete -> patternEditor.deleteText(false) + + KnownKeys.Backspace -> { + patternEditor.deleteText(true) + } + + KnownKeys.Delete -> { + patternEditor.deleteText(false) + } + else -> { if (!event.typedText.isNullOrEmpty()) { patternEditor.insertText(event.typedText) @@ -117,7 +140,10 @@ class CodeCompletionMenu( } } - 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 @@ -148,7 +174,9 @@ class CodeCompletionMenu( val newTextBeforeCaret = pattern.substring(0, caretPos) val exactMatches = allEntries.filter { it.matchesExactly(oldTextBeforeCaret) } - if (exactMatches.size == 1 && !editor.serviceCall { hasCodeCompletionActions(editor.editorId, anchor.cell.getId(), newTextBeforeCaret) }) { + if (exactMatches.size == 1 && + !editor.serviceCall { hasCodeCompletionActions(editor.editorId, anchor.cell.getId(), newTextBeforeCaret) } + ) { exactMatches.single().execute() editor.closeCodeCompletionMenu() if (remainingText.isNotEmpty()) { @@ -177,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) { @@ -200,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) + } } } } @@ -212,47 +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: BackendEditorComponent): ICaretPositionPolicy? + fun shadows(shadowed: ICodeCompletionAction) = false + fun shadowedBy(shadowing: ICodeCompletionAction) = false } fun ICodeCompletionAction.getCompletionPattern(): String = getTokens().toString() -class CodeCompletionParameters(val editor: BackendEditorComponent, 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 @@ -266,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 716876b5..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: FrontendEditorComponent) { +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: FrontendE 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 2fa59a75..f0465c4f 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/ConceptEditor.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/ConceptEditor.kt @@ -19,56 +19,64 @@ class ConceptEditor( val id: Long = idSequence.incrementAndGet() - fun isApplicable(context: CellCreationContext, node: INode): Boolean { - return apply(node.concept ?: NullConcept).condition?.invoke(node) != false - } + fun isApplicable( + context: CellCreationContext, + node: INode, + ): Boolean = apply(node.concept ?: NullConcept).condition?.invoke(node) != false - fun apply(subConcept: IConcept): NotationRootCellTemplate { - return templateBuilder(subConcept) + fun apply(subConcept: IConcept): NotationRootCellTemplate = + templateBuilder(subConcept) .also { it.setReference(RooCellTemplateReference(id, subConcept.getReference().upcast())) } - } - fun applyIfApplicable(context: CellCreationContext, node: INode): CellSpecBase? { + 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): CellSpecBase { + 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/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 08e8fc95..361bf51c 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/EditorEngine.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/EditorEngine.kt @@ -14,8 +14,9 @@ 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() @@ -39,19 +40,20 @@ class EditorEngine(incrementalEngine: IncrementalEngine? = null) { 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 + 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 @@ -68,11 +70,20 @@ class EditorEngine(incrementalEngine: IncrementalEngine? = null) { } } - fun createCell(cellTreeState: CellTreeState, node: INode) = createCell(cellTreeState, node.asWritableNode()) + fun createCell( + cellTreeState: CellTreeState, + node: INode, + ) = createCell(cellTreeState, node.asWritableNode()) - fun createCell(cellTreeState: CellTreeState, node: IWritableNode) = createCell(cellTreeState, NodeCellCreationCall(node)) + fun createCell( + cellTreeState: CellTreeState, + node: IWritableNode, + ) = createCell(cellTreeState, NodeCellCreationCall(node)) - fun createCell(cellTreeState: CellTreeState, call: CellCreationCall) = createCellIncremental(cellTreeState, call) + fun createCell( + cellTreeState: CellTreeState, + call: CellCreationCall, + ) = createCellIncremental(cellTreeState, call) fun createCellModel(concept: IConcept): CellTemplate { val editor: ConceptEditor = resolveConceptEditor(concept).first() @@ -80,19 +91,22 @@ 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: IWritableNode): BackendEditorComponent { - return BackendEditorComponent(NodeCellCreationCall(node), this) - } + fun editNode(node: IWritableNode): BackendEditorComponent = BackendEditorComponent(NodeCellCreationCall(node), this) - private fun doCreateCell(cellTreeState: CellTreeState, call: CellCreationCall): IMutableCellTree.MutableCell { - return dataToCell(cellTreeState, createCellSpecIncremental(cellTreeState, call), cellTreeState.cellTree.createCell()) - } + private fun doCreateCell( + cellTreeState: CellTreeState, + call: CellCreationCall, + ): IMutableCellTree.MutableCell = + dataToCell(cellTreeState, createCellSpecIncremental(cellTreeState, call), cellTreeState.cellTree.createCell()) - private fun dataToCell(cellTreeState: CellTreeState, data: CellSpecBase, cell: IMutableCellTree.MutableCell): IMutableCellTree.MutableCell { + private fun dataToCell( + cellTreeState: CellTreeState, + data: CellSpecBase, + cell: IMutableCellTree.MutableCell, + ): IMutableCellTree.MutableCell { data.cellReferences.takeIf { it.isNotEmpty() }?.let { cell.setProperty(CommonCellProperties.cellReferences, it.toList()) } @@ -103,6 +117,7 @@ class EditorEngine(incrementalEngine: IncrementalEngine? = null) { is CellSpec -> { cell.setProperty(CommonCellProperties.type, ECellType.COLLECTION) } + is TextCellSpec -> { cell.setProperty(CommonCellProperties.type, ECellType.TEXT) cell.setProperty(TextCellProperties.text, data.text) @@ -110,14 +125,16 @@ class EditorEngine(incrementalEngine: IncrementalEngine? = null) { } } for ((index, childRef) in data.children.withIndex()) { - val childCell = when (childRef) { - is CellSpecBase -> { - dataToCell(cellTreeState, childRef, cell.addNewChild(index)) + val childCell = + when (childRef) { + is CellSpecBase -> { + dataToCell(cellTreeState, childRef, cell.addNewChild(index)) + } + + is ChildSpecReference -> { + createCell(cellTreeState, childRef.childNode) + } } - is ChildSpecReference -> { - createCell(cellTreeState, childRef.childNode) - } - } if (childCell.getParent() != cell) { childCell.moveCell(cell, index) } else if (cell.getChildAt(index) != childCell) { @@ -128,7 +145,10 @@ class EditorEngine(incrementalEngine: IncrementalEngine? = null) { return cell } - private fun doCreateCellData(cellTreeState: CellTreeState, node: INode): CellSpecBase { + private fun doCreateCellData( + cellTreeState: CellTreeState, + node: INode, + ): CellSpecBase { try { val editor = resolveConceptEditor(node.concept) val context = CellCreationContext(this, cellTreeState) @@ -153,31 +173,40 @@ 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() { 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: BackendEditorComponent): ICaretPositionPolicy? { @@ -189,4 +218,7 @@ class DeleteNodeCellAction(val node: INode) : ICellAction { } sealed class CellCreationCall -data class NodeCellCreationCall(val node: IWritableNode) : 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 0b9216f2..fef1ef67 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/EditorTestUtils.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/EditorTestUtils.kt @@ -8,39 +8,74 @@ object EditorTestUtils { val newLine = Any() val indentChildren = Any() - fun buildCells(template: Any, tree: IMutableCellTree): MutableCell { - return 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, "") + fun buildCells( + template: Any, + tree: IMutableCellTree, + ): MutableCell = + when (template) { + is IMutableCellTree.MutableCell -> { + template } - 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) + + 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, tree: IMutableCellTree): MutableCell { - return buildCells(buildRandomTemplate(rand, cellsPerLevel, levels), tree) - } + 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() @@ -50,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 index 8818312c..afacae5b 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/FrontendEditorComponent.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/FrontendEditorComponent.kt @@ -46,15 +46,17 @@ open class FrontendEditorComponent( private var highlightedLine: IVirtualDom.HTMLElement? = null private var highlightedCell: IVirtualDom.HTMLElement? = null private val coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.Default) - private val uiEventQueue = coroutineScope.consume(capacity = 100, onBufferOverflow = BufferOverflow.DROP_LATEST) { event -> - when (event) { - is JSKeyboardEvent -> processKeyEvent(event) - is JSMouseEvent -> processMouseEvent(event) + private val uiEventQueue = + coroutineScope.consume(capacity = 100, onBufferOverflow = BufferOverflow.DROP_LATEST) { event -> + when (event) { + is JSKeyboardEvent -> processKeyEvent(event) + is JSMouseEvent -> processMouseEvent(event) + } + } + private val uiUpdateQueue = + coroutineScope.consume Unit, CompletableDeferred>>(capacity = Channel.UNLIMITED) { + it.second.completeWith(runCatching { it.first.invoke() }) } - } - private val uiUpdateQueue = coroutineScope.consume Unit, CompletableDeferred>>(capacity = Channel.UNLIMITED) { - it.second.completeWith(runCatching { it.first.invoke() }) - } private val updateLoop: AtomicReference = AtomicReference(null) private val updateLock = Any() @@ -77,21 +79,16 @@ open class FrontendEditorComponent( return firstUpdate } - suspend fun flush() { - return enqueueUpdate(service.flush(editorId)).await() - } + suspend fun flush() = enqueueUpdate(service.flush(editorId)).await() - suspend fun flushRemote() { - return enqueueUpdate(service.flush(editorId)).await() - } + suspend fun flushRemote() = enqueueUpdate(service.flush(editorId)).await() - suspend fun flushLocal() { - return enqueueUpdate(EditorUpdateData()).await() - } + suspend fun flushLocal() = enqueueUpdate(EditorUpdateData()).await() - fun getMainLayer(): IVirtualDom.HTMLElement? { - return getHtmlElement()?.childNodes?.filterIsInstance()?.find { it.getClasses().contains(MAIN_LAYER_CLASS_NAME) } - } + 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) @@ -120,9 +117,7 @@ open class FrontendEditorComponent( openNode(node.toSerialized()).await() } - fun enqueueUpdate(updateData: EditorUpdateData): Deferred { - return enqueueUpdate { updateNow(updateData) } - } + fun enqueueUpdate(updateData: EditorUpdateData): Deferred = enqueueUpdate { updateNow(updateData) } fun enqueueUpdate(body: () -> Unit): Deferred { val result = CompletableDeferred() @@ -189,11 +184,12 @@ open class FrontendEditorComponent( private fun updateSelectionView() { if (selectionView?.selection != getSelection()) { - selectionView = when (val selection = getSelection()) { - is CaretSelection -> CaretSelectionView(selection, this) - is CellSelection -> CellSelectionView(selection, this) - else -> null - } + selectionView = + when (val selection = getSelection()) { + is CaretSelection -> CaretSelectionView(selection, this) + is CellSelection -> CellSelectionView(selection, this) + else -> null + } } } @@ -243,9 +239,7 @@ open class FrontendEditorComponent( return true } - private fun processKeyUp(event: JSKeyboardEvent): Boolean { - return true - } + private fun processKeyUp(event: JSKeyboardEvent): Boolean = true private suspend fun processKeyDown(event: JSKeyboardEvent): Boolean { try { @@ -288,36 +282,50 @@ open class FrontendEditorComponent( 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()) + val caretPos = + (relativeClickX / characterWidth) + .roundToInt() + .coerceAtMost(layoutable.cell.getMaxCaretPos()) changeSelection(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 + + else -> { + continue + } } } return false } - private fun selectClosestInLine(line: TextLine, absoluteClickX: Double): Boolean { + 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 - } + 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(this, closest.first, caretPos)) return true } 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 8d1c586f..6d1fe5ed 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/ICellAction.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/ICellAction.kt @@ -6,24 +6,35 @@ import org.modelix.model.api.getInstantiatableSubConcepts interface ICellAction { fun isApplicable(): Boolean + fun execute(editor: BackendEditorComponent): ICaretPositionPolicy? } interface ITextChangeAction { fun isValid(value: String?): Boolean - fun replaceText(editor: CellTreeState, 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: CellTreeState, 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? { @@ -47,25 +58,37 @@ object CellActionProperties { 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) } @@ -76,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)) } @@ -85,31 +109,41 @@ 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? { @@ -152,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/JSMouseEvent.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/JSMouseEvent.kt index ffb56db4..f3659bfc 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/JSMouseEvent.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/JSMouseEvent.kt @@ -13,24 +13,30 @@ data class JSMouseEvent( val button: Short, val buttons: Short, ) : 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 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 3b3a322b..c1f3247f 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/ReplaceNodeActionProvider.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/ReplaceNodeActionProvider.kt @@ -6,19 +6,24 @@ 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 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() } @@ -30,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) @@ -40,14 +49,15 @@ 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: BackendEditorComponent): CaretPositionPolicy? { val sourceNode = sourceLocation.getOrCreateNode(null) 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 7cde9a13..662ab35e 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/Selection.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/Selection.kt @@ -4,10 +4,14 @@ import org.modelix.editor.text.shared.celltree.ICellTree abstract class Selection : IKeyboardHandler { abstract fun isValid(): Boolean + 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 f61b67e4..475cfda4 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/TextLayouter.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/TextLayouter.kt @@ -11,7 +11,9 @@ 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() @@ -109,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 } @@ -153,10 +156,12 @@ class TextLayouter { if (isEmpty()) beginsWithNewLine = true insertNewLineNext = true } + fun emptyLine() { addNewLine() onNewLine() } + fun withIndent(body: () -> Unit) { val oldIndent = currentIndent try { @@ -166,6 +171,7 @@ class TextLayouter { currentIndent = oldIndent } } + fun noSpace() { if (isEmpty()) beginsWithNoSpace = true autoInsertSpace = false @@ -239,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 { @@ -270,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 { @@ -283,30 +290,35 @@ abstract class Layoutable : IProducesHtml { consumer.onTagContent(text.useNbsp()) } }*/ -class LayoutableCell(val cell: ICellTree.Cell) : Layoutable() { +class LayoutableCell( + val cell: ICellTree.Cell, +) : Layoutable() { init { require(cell.type == ECellType.TEXT) { "Not a text cell: $cell" } } - override fun getLength(): Int { - return toText().length - } - override fun toText(): String { - return cell.getProperty(CommonCellProperties.textReplacement) + + 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.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 - } + 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" @@ -323,21 +335,31 @@ fun Cell.layoutable(): LayoutableCell? { 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 ae0b6122..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 @@ -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,7 +37,11 @@ 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): CellSpecBase { + + fun apply( + context: CellCreationContext, + node: INode, + ): CellSpecBase { val cellData = createCell(context, node) cellData.properties.addAll(properties) cellData.children.addAll(applyChildren(context, node, cellData)) @@ -48,12 +54,25 @@ abstract class CellTemplate(val concept: IConcept) { applyTextReplacement(cellData, context.cellTreeState) return cellData } - protected open fun applyChildren(context: CellCreationContext, node: INode, cell: CellSpecBase): List { - return children.map { it.apply(context, node) } - } - protected abstract fun createCell(context: CellCreationContext, node: INode): CellSpecBase - 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,18 +142,25 @@ 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(cellSpec: CellSpecBase, cellTreeState: CellTreeState) { + private fun applyTextReplacement( + cellSpec: CellSpecBase, + cellTreeState: CellTreeState, + ) { if (cellSpec is TextCellSpec) { val cellRef = cellSpec.cellReferences.firstOrNull() if (cellRef != null) { @@ -137,10 +175,10 @@ abstract class CellTemplate(val concept: IConcept) { } 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 f2fad015..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 @@ -14,15 +14,14 @@ class ChangePropertyCellAction( val value: String, ) : ICellAction { override fun execute(editor: BackendEditorComponent): ICaretPositionPolicy? { - val node = editor.runWrite { - node.getOrCreateNode().also { - it.setPropertyValue(property, value) + val node = + editor.runWrite { + node.getOrCreateNode().also { + it.setPropertyValue(property, value) + } } - } 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 11eb0918..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 @@ -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,10 +126,14 @@ class ChildCellTemplate( separatorCell?.setReference(SeparatorCellTemplateReference(ref)) } - override fun createCell(context: CellCreationContext, node: INode) = CellSpec().also { cell -> + override fun createCell( + context: CellCreationContext, + node: INode, + ) = CellSpec().also { cell -> val childNodes = getChildNodes(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 "" @@ -132,26 +147,30 @@ class ChildCellTemplate( placeholder.cellReferences += ChildNodeCellReference(node.reference, link.toReference(), index) } placeholder.properties[CommonCellProperties.tabTarget] = true - 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 - } + 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 = CellSpec() - val action = newLineConcept?.let { - InstantiateNodeCellAction(NonExistingChild(ExistingNode(node), link, index), it) - } ?: InsertSubstitutionPlaceholderAction(context.cellTreeState, createCellReference(node), index) + 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( @@ -164,8 +183,12 @@ class ChildCellTemplate( if (childNodes.isEmpty()) { addSubstitutionPlaceholder(0) } else { - val separatorText = separatorCell?.getGrammarSymbols()?.filterIsInstance() - ?.firstOrNull()?.text + val separatorText = + separatorCell + ?.getGrammarSymbols() + ?.filterIsInstance() + ?.firstOrNull() + ?.text val childCells = childNodes.map { ChildSpecReference(it) } childCells.forEachIndexed { index, child -> val childCellReference = ChildNodeCellReference(node.reference, link.toReference(), index) @@ -186,16 +209,18 @@ class ChildCellTemplate( 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) @@ -213,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. @@ -230,30 +258,25 @@ 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: BackendEditorComponent): CaretPositionPolicy? { editor.state.substitutionPlaceholderPositions[ref] = SubstitutionPlaceholderPosition(index) 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 39a70ce9..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 @@ -5,7 +5,11 @@ 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) = CellSpec() +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 23f1bd72..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 @@ -26,47 +26,62 @@ 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) = TextCellSpec(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: BackendEditorComponent): CaretPositionPolicy? { - val wrapper = nodeToWrap.getParent()!!.getOrCreateNode(null).asWritableNode() - .addNewChild(nodeToWrap.getContainmentLink()?.toReference()!!, nodeToWrap.index(), concept.getReference().upcast()) + 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.toReference())) @@ -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 df075807..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 @@ -24,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() @@ -38,7 +36,10 @@ class FlagCellTemplate( builder.currentNode().setPropertyValue(property, "true") } - override fun createCell(context: CellCreationContext, node: INode): CellSpecBase { + override fun createCell( + context: CellCreationContext, + node: INode, + ): CellSpecBase { if (node.getPropertyValue(property) == "true") return TextCellSpec(text, "") val forceShow = context.cellTreeState.forceShowOptionals[createCellReference(node)] == true @@ -55,7 +56,10 @@ class FlagCellTemplate( } } - 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 7427b1fb..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 @@ -7,21 +7,18 @@ import org.modelix.editor.ICodeCompletionAction import org.modelix.editor.TemplateCellReference import org.modelix.editor.text.backend.BackendEditorComponent -class ForceShowOptionalCellAction(val cell: TemplateCellReference) : ICodeCompletionAction, ICellAction { +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/InstantiateNodeCellAction.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/InstantiateNodeCellAction.kt index 5ae63609..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 @@ -6,13 +6,17 @@ 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: BackendEditorComponent): CaretPositionPolicy { - val newNode = location.getExistingAncestor()!!.getArea().executeWrite { - location.replaceNode(concept) - } + 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 89bdbc7f..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 @@ -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: BackendEditorComponent): CaretPositionPolicy? { - val newNode = location.getExistingAncestor()!!.getArea().executeWrite { - location.replaceNode(concept) - } + 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 47a73729..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 @@ -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): TextCellSpec { - return TextCellSpec(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 d7b0b8b6..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 @@ -10,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): CellSpecBase { - return CellSpec().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 27952a1f..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 @@ -10,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): CellSpecBase { - return CellSpec().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 49c5dae3..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 @@ -5,8 +5,13 @@ 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) = CellSpec() + + 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 22cffc5a..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 @@ -18,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() @@ -35,15 +33,23 @@ class OptionalCellTemplate(concept: IConcept) : CellTemplate(concept), IOptional is ParseTreeNode -> { builder.consumeTokens(token.children) } - else -> TODO() + + else -> { + TODO() + } } } - override fun createCell(context: CellCreationContext, node: INode): CellSpecBase { - return CellSpec() - } + override fun createCell( + context: CellCreationContext, + node: INode, + ): CellSpecBase = CellSpec() - override fun applyChildren(context: CellCreationContext, node: INode, cell: CellSpecBase): List { + 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() } @@ -68,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 758671d9..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 @@ -4,12 +4,18 @@ import org.modelix.editor.CellTreeState import org.modelix.editor.ITextChangeAction import org.modelix.editor.TextCellSpec -class OverrideText(val cell: TextCellSpec, 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: CellTreeState, 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.textReplacements.remove(cellRef) 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/PropertyCellTemplate.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/celltemplate/PropertyCellTemplate.kt index 04c711d0..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 @@ -30,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() @@ -50,7 +49,10 @@ open class PropertyCellTemplate(concept: IConcept, val property: IProperty) : builder.currentNode().setPropertyValue(property, (token as Token).text) } - override fun createCell(context: CellCreationContext, node: INode): CellSpecBase { + override fun createCell( + context: CellCreationContext, + node: INode, + ): CellSpecBase { val value = node.getPropertyValue(property) val data = TextCellSpec(value ?: "", if (value == null) placeholderText else "") data.properties[CellActionProperties.replaceText] = ChangePropertyAction(node) @@ -59,46 +61,46 @@ open class PropertyCellTemplate(concept: IConcept, val property: IProperty) : 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: BackendEditorComponent): CaretPositionPolicy? { val node = location.getOrCreateNode(concept) @@ -107,13 +109,20 @@ open class PropertyCellTemplate(concept: IConcept, val property: IProperty) : } } - 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: CellTreeState, 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 f01e801b..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 @@ -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,7 +46,10 @@ class ReferenceCellTemplate( // TODO builder.currentNode().setReferenceTarget(link, TODO()) } - override fun createCell(context: CellCreationContext, node: INode): CellSpecBase { + 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 @@ -60,41 +59,46 @@ class ReferenceCellTemplate( } 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: BackendEditorComponent): CaretPositionPolicy? { val sourceNode = location.getOrCreateNode(concept) 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 index 80fd1027..f856a1ec 100644 --- 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 @@ -14,8 +14,10 @@ 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) { - +class BackendEditorComponent( + val rootCall: CellCreationCall, + val engine: EditorEngine, +) { val state = CellTreeState() val tree: BackendCellTree get() = state.cellTree var completionMenu: CompletionMenuBackend? = null @@ -29,36 +31,39 @@ class BackendEditorComponent(val rootCall: CellCreationCall, val engine: EditorE selectionUpdater = newSelection } - fun update(): List { - return tree.runUpdate { + fun update(): List = + tree.runUpdate { runRead { val newCell = engine.createCell(state, rootCall) - tree.getRoot().getChildren().minus(newCell).forEach { it.detach() } + 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 { - return CompletionMenuBackend(providers).let { + fun loadCompletionEntries( + providers: List, + pattern: String, + ): List = + CompletionMenuBackend(providers).let { completionMenu = it it.updateActions(pattern) } - } - fun runWrite(body: () -> R): R { - return when (rootCall) { + fun runWrite(body: () -> R): R = + when (rootCall) { is NodeCellCreationCall -> rootCall.node.getModel().executeWrite(body) } - } - fun runRead(body: () -> R): R { - return when (rootCall) { + fun runRead(body: () -> R): R = + when (rootCall) { is NodeCellCreationCall -> rootCall.node.getModel().executeRead(body) } - } inner class CompletionMenuBackend( val providers: List, @@ -66,23 +71,20 @@ class BackendEditorComponent(val rootCall: CellCreationCall, val engine: EditorE val actionsCache = CachedCodeCompletionActions(providers) private var entries: List = emptyList() - fun updateActions(pattern: String): List { - return computeActions(pattern).also { entries = it } - } + fun updateActions(pattern: String): List = computeActions(pattern).also { entries = it } fun getEntries(): List = entries - fun computeActions(pattern: String): List { - return runRead { + fun computeActions(pattern: String): List = + runRead { val parameters = CodeCompletionParameters(this@BackendEditorComponent, pattern) - actionsCache.update(parameters) + actionsCache + .update(parameters) .filter { val matchingText = it.getCompletionPattern() matchingText.isNotEmpty() && matchingText.startsWith(parameters.pattern) - } - .applyShadowing() + }.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 index 211eb50a..5197ccaf 100644 --- 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 @@ -50,7 +50,11 @@ 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 { +class TextEditorServiceImpl( + val engine: EditorEngine, + val model: IMutableModel, + val coroutineScope: CoroutineScope, +) : TextEditorService { private var updateChannels: AtomicReference> = AtomicReference(emptyMap()) private val validator = Validator(coroutineScope) { sendUpdates() } @@ -58,13 +62,14 @@ class TextEditorServiceImpl(val engine: EditorEngine, val model: IMutableModel, validator.start() } - fun getAllEditorBackends(): List { - return updateChannels.get().map { it.value.editor } - } + 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 { + override fun openNode( + editorId: EditorId, + nodeRef: NodeReference, + ): Flow { val node = model.executeRead { model.resolveNode(nodeRef) } val editorBackend = engine.editNode(node) return channelFlow { @@ -79,23 +84,34 @@ class TextEditorServiceImpl(val engine: EditorEngine, val model: IMutableModel, } } - private suspend fun runWithCell(editorId: Int, cellId: CellInstanceId, body: (EditorUpdateChannel, ICellTree.Cell) -> R): R { - return runWithEditor(editorId) { updateChannel, editor -> + 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" - } + 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 = + 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()) { @@ -107,7 +123,8 @@ class TextEditorServiceImpl(val engine: EditorEngine, val model: IMutableModel, 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() + updateChannel.editor.state.forceShowOptionals + .clear() return@runWithCell updateChannel.createSelection(action.execute(updateChannel.editor)) } } @@ -118,42 +135,47 @@ class TextEditorServiceImpl(val engine: EditorEngine, val model: IMutableModel, 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)) + ): 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() } - 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)) + ): 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 } - previousLeaf = nextLeaf } + return@runWithCell updateChannel.createUpdate() } - return@runWithCell updateChannel.createUpdate() - } override suspend fun processTypedText( editorId: Int, @@ -170,38 +192,42 @@ class TextEditorServiceImpl(val engine: EditorEngine, val model: IMutableModel, // 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() - } + 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() - } + 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() - ) - } + 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() + ) + } ) } } @@ -209,7 +235,13 @@ class TextEditorServiceImpl(val engine: EditorEngine, val model: IMutableModel, } } - private fun replaceText(cell: ICellTree.Cell, range: IntRange, replacement: String, updateChannel: EditorUpdateChannel, triggerCompletion: Boolean): EditorUpdateData? { + 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) @@ -219,16 +251,18 @@ class TextEditorServiceImpl(val engine: EditorEngine, val model: IMutableModel, 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 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) + val caretPolicy = + editor.runWrite { + singleAction.execute(editor).also { + editor.state.clearTextReplacement(cell) + } } - } return updateChannel.createSelection(caretPolicy) } } @@ -237,7 +271,8 @@ class TextEditorServiceImpl(val engine: EditorEngine, val model: IMutableModel, for (action in replaceTextActions) { if (action.isValid(newText) && action.replaceText(editor.state, range, replacement, newText)) { val cellReferences = cell.cellReferences.toSet() - return updateChannel.createUpdate() + return updateChannel + .createUpdate() .copy(selectionChange = CaretPositionPolicyWithIndex(cellReferences, range.first + replacement.length)) } } @@ -248,65 +283,71 @@ class TextEditorServiceImpl(val engine: EditorEngine, val model: IMutableModel, editorId: Int, cellId: CellInstanceId, caretPosition: Int, - ): EditorUpdateData { - return runWithCell(editorId, cellId) { updateChannel, cell -> + ): 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() - ) - } + 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 { - return runWithCell(editorId, cellId) { updateChannel, cell -> + ): 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() - ) - } + 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 { - return runWithCell(editorId, cellId) { updateChannel, cell -> + ): Boolean = + runWithCell(editorId, cellId) { updateChannel, cell -> model.executeRead { - updateChannel.editor.completionMenu?.computeActions(pattern)?.any() == true + updateChannel.editor.completionMenu + ?.computeActions(pattern) + ?.any() == true } } - } - override suspend fun executeCodeCompletionAction(editorId: Int, actionId: Int): EditorUpdateData { - return runWithEditor(editorId) { updateChannel, editor -> + 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 action = + requireNotNull(editor.completionMenu?.getEntries()?.getOrNull(actionId)) { + "Action with ID $actionId not found" + } val policy = action.execute(editor) val update = updateChannel.createUpdate() update.copy( @@ -314,7 +355,6 @@ class TextEditorServiceImpl(val engine: EditorEngine, val model: IMutableModel, ) } } - } override suspend fun replaceText( editorId: Int, @@ -333,9 +373,10 @@ class TextEditorServiceImpl(val engine: EditorEngine, val model: IMutableModel, 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 matchingActions = + actions + .filter { it.getTokens().consumeForAutoApply(newText)?.length == 0 } + .applyShadowing() val singleAction = matchingActions.singleOrNull() if (singleAction != null) { editor.runWrite { @@ -351,15 +392,17 @@ class TextEditorServiceImpl(val engine: EditorEngine, val model: IMutableModel, 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 - ) + 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 - ), + updateData = + updateChannel.createUpdate().copy( + selectionChange = newCaretPosition + ), result = true ) } @@ -368,24 +411,20 @@ class TextEditorServiceImpl(val engine: EditorEngine, val model: IMutableModel, } } - override suspend fun resetState(editorId: Int): EditorUpdateData { - return runWithEditor(editorId) { updateChannel, editor -> + override suspend fun resetState(editorId: Int): EditorUpdateData = + runWithEditor(editorId) { updateChannel, editor -> editor.state.reset() updateChannel.createUpdate() } - } - override suspend fun flush(editorId: Int): EditorUpdateData { - return runWithEditor(editorId) { updateChannel, editor -> updateChannel.createUpdate() } - } + override suspend fun flush(editorId: Int): EditorUpdateData = + runWithEditor(editorId) { updateChannel, editor -> updateChannel.createUpdate() } - private fun ICellAction.executeAndUpdateSelection(channel: EditorUpdateChannel): EditorUpdateData { - return channel.createSelection(execute(channel.editor)) - } + private fun ICellAction.executeAndUpdateSelection(channel: EditorUpdateChannel): EditorUpdateData = + channel.createSelection(execute(channel.editor)) - private fun ICodeCompletionAction.executeAndUpdateSelection(channel: EditorUpdateChannel): EditorUpdateData { - return channel.createSelection(execute(channel.editor)) - } + private fun ICodeCompletionAction.executeAndUpdateSelection(channel: EditorUpdateChannel): EditorUpdateData = + channel.createSelection(execute(channel.editor)) fun triggerUpdates() { validator.invalidate() @@ -403,44 +442,53 @@ class TextEditorServiceImpl(val engine: EditorEngine, val model: IMutableModel, } } -class EditorUpdateChannel(val editorId: EditorId, val editor: BackendEditorComponent, val channel: SendChannel) { +class EditorUpdateChannel( + val editorId: EditorId, + val editor: BackendEditorComponent, + val channel: SendChannel, +) { private val mutex = Mutex() + suspend fun sendUpdate() { mutex.withLock { - editor.update() + editor + .update() .takeIf { it.isNotEmpty() } ?.let { channel.send(EditorUpdateData(it)) } } } - suspend fun withPausedUpdates(body: suspend () -> R): R { - return mutex.withLock { + suspend fun withPausedUpdates(body: suspend () -> R): R = + mutex.withLock { body() } - } - fun createSelection(textCell: ICellTree.Cell, position: Int): EditorUpdateData { + 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 - ) + val newSelection = + CaretPositionPolicyWithIndex( + policy = + CaretPositionPolicy( + avoidedCellRefs = emptySet(), + preferredCellRefs = textCell.cellReferences.toSet() + ), + index = position + ) return createSelection(newSelection) } - fun createSelection(newSelection: ICaretPositionPolicy?): EditorUpdateData { - return EditorUpdateData(cellTreeChanges = editor.update(), selectionChange = newSelection) - } + fun createSelection(newSelection: ICaretPositionPolicy?): EditorUpdateData = + EditorUpdateData(cellTreeChanges = editor.update(), selectionChange = newSelection) - fun createUpdate(): EditorUpdateData { - return EditorUpdateData(editor.update()) - } + fun createUpdate(): EditorUpdateData = EditorUpdateData(editor.update()) } -class AtomicReference(private var value: E) { +class AtomicReference( + private var value: E, +) { fun getAndUpdate(updater: (E) -> E): E { runSynchronized(this) { value = updater(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 index a6f2412c..f28fc31f 100644 --- 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 @@ -13,24 +13,33 @@ 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) { +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 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" } + 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 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 index f18642fd..e1aca5b2 100644 --- 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 @@ -25,14 +25,13 @@ 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() { - +class FrontendCellTree( + val editorComponent: FrontendEditorComponent? = null, +) : CellTreeBase() { override fun newCellInstance( id: CellInstanceId, parent: CellImpl?, - ): CellImpl { - return FrontendCellImpl(id, parent as FrontendCellImpl?) - } + ): CellImpl = FrontendCellImpl(id, parent as FrontendCellImpl?) override fun getRoot(): FrontendCellImpl = super.getRoot() as FrontendCellImpl @@ -53,22 +52,26 @@ class FrontendCellTree(val editorComponent: FrontendEditorComponent? = null) : C } } - inner class FrontendCellImpl(id: CellInstanceId, parent: FrontendCellImpl?) : CellImpl(id, parent) { - private var layout_ = ResettableLazy { - runLayoutOnCell(this) { it.layout } - } + inner class FrontendCellImpl( + id: CellInstanceId, + parent: FrontendCellImpl?, + ) : CellImpl(id, parent) { + private var cachedLayout = + ResettableLazy { + runLayoutOnCell(this) { it.layout } + } val layout: LayoutedText - get() = layout_.value + get() = cachedLayout.value fun clearCachedLayout() { withTreeLock { - layout_.reset() + cachedLayout.reset() } } fun invalidateLayout() { withTreeLock { - layout_.reset() + cachedLayout.reset() getParent()?.invalidateLayout() } } @@ -77,14 +80,16 @@ class FrontendCellTree(val editorComponent: FrontendEditorComponent? = null) : C override fun getParent() = super.getParent() as FrontendCellImpl? - override fun getProperty(key: CellPropertyKey): T { - return withTreeLock { + 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) { + override fun setProperty( + key: CellPropertyKey, + newValue: T, + ) { withTreeLock { super.setProperty(key, newValue) invalidateLayout() @@ -98,13 +103,12 @@ class FrontendCellTree(val editorComponent: FrontendEditorComponent? = null) : C } } - override fun addNewChild(index: Int): IMutableCellTree.MutableCell { - return withTreeLock { + override fun addNewChild(index: Int): IMutableCellTree.MutableCell = + withTreeLock { super.addNewChild(index).also { invalidateLayout() } } - } override fun moveCell(index: Int) { withTreeLock { @@ -113,7 +117,10 @@ class FrontendCellTree(val editorComponent: FrontendEditorComponent? = null) : C } } - override fun moveCell(targetParent: IMutableCellTree.MutableCell, index: Int) { + override fun moveCell( + targetParent: IMutableCellTree.MutableCell, + index: Int, + ) { withTreeLock { invalidateLayout() super.moveCell(targetParent, index) @@ -145,16 +152,13 @@ var IMutableCellTree.MutableCell.text: String? set(value) = setProperty(TextCellProperties.text, value) val ICellTree.Cell.placeholderText: String? get() = getProperty(TextCellProperties.placeholderText) -fun ICellTree.Cell.getVisibleText(): String { - return getProperty(CommonCellProperties.textReplacement) +fun ICellTree.Cell.getVisibleText(): String = + getProperty(CommonCellProperties.textReplacement) ?: text?.takeIf { it.isNotEmpty() } ?: placeholderText ?: "" -} -fun ICellTree.Cell.getSelectableText(): String? { - return getProperty(CommonCellProperties.textReplacement) ?: text -} +fun ICellTree.Cell.getSelectableText(): String? = getProperty(CommonCellProperties.textReplacement) ?: text val ICellTree.Cell.layout: LayoutedText get() = (this as FrontendCellTree.FrontendCellImpl).layout @@ -164,18 +168,32 @@ val ICellTree.Cell.editorComponent: FrontendEditorComponent get() { } } -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 { - return runLayoutOnCell(cell) { runLayoutOnCell(it) } -} - -fun runLayoutOnCell(cell: ICellTree.Cell, layoutChild: (ICellTree.Cell) -> LayoutedText): LayoutedText { - return TextLayouter().also { runLayoutOnCell(it, cell, layoutChild) }.done() -} - -fun runLayoutOnCell(layouter: TextLayouter, cell: ICellTree.Cell, layoutChild: (ICellTree.Cell) -> LayoutedText) { +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 = { @@ -190,6 +208,7 @@ fun runLayoutOnCell(layouter: TextLayouter, cell: ICellTree.Cell, layoutChild: ( body() } } + ECellType.TEXT -> { if (cell.getProperty(CommonCellProperties.onNewLine)) layouter.onNewLine() if (cell.getProperty(CommonCellProperties.noSpace)) layouter.noSpace() 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 index 7b277c87..8c5b9914 100644 --- 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 @@ -13,17 +13,67 @@ 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 + 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 } @@ -90,7 +140,10 @@ class NullTextEditorService : TextEditorService { TODO("Not yet implemented") } - override suspend fun executeCodeCompletionAction(editorId: Int, actionId: Int): EditorUpdateData { + override suspend fun executeCodeCompletionAction( + editorId: Int, + actionId: Int, + ): EditorUpdateData { TODO("Not yet implemented") } @@ -146,7 +199,5 @@ data class CompletionMenuEntryData( return matchingText.contains(pattern) } - fun matchesExactly(pattern: String): Boolean { - return matchingText == 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 index 8dac6a1e..6ec74b9b 100644 --- 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 @@ -12,20 +12,17 @@ class BackendCellTree : CellTreeBase() { override fun getCell(id: CellInstanceId): BackendCellImpl = super.getCell(id) as BackendCellImpl - override fun createCell(id: CellInstanceId): IMutableCellTree.MutableCell { - return withTreeLock { + override fun createCell(id: CellInstanceId): IMutableCellTree.MutableCell = + withTreeLock { super.createCell(id).also { operations += NewCellOp(it.getId()) } } - } override fun newCellInstance( id: CellInstanceId, parent: CellImpl?, - ): CellImpl { - return BackendCellImpl(id, parent as BackendCellImpl?) - } + ): CellImpl = BackendCellImpl(id, parent as BackendCellImpl?) fun runUpdate(body: () -> Unit): List { return withTreeLock { @@ -41,12 +38,16 @@ class BackendCellTree : CellTreeBase() { } } - private fun getPendingChanges(): List { - return withTreeLock { operations.also { operations = ArrayList() } } - } + 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) { + 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) @@ -66,25 +67,18 @@ class BackendCellTree : CellTreeBase() { } } - override fun getParent(): BackendCellImpl? { - return super.getParent() as BackendCellImpl? - } + override fun getParent(): BackendCellImpl? = super.getParent() as BackendCellImpl? - override fun getChildren(): List { - return super.getChildren() as List - } + override fun getChildren(): List = super.getChildren() as List - override fun getChildAt(index: Int): BackendCellImpl? { - return super.getChildAt(index) as BackendCellImpl? - } + override fun getChildAt(index: Int): BackendCellImpl? = super.getChildAt(index) as BackendCellImpl? - override fun addNewChild(index: Int): IMutableCellTree.MutableCell { - return withTreeLock { + 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 { @@ -93,7 +87,10 @@ class BackendCellTree : CellTreeBase() { } } - override fun moveCell(targetParent: IMutableCellTree.MutableCell, index: Int) { + override fun moveCell( + targetParent: IMutableCellTree.MutableCell, + index: Int, + ) { withTreeLock { targetParent as BackendCellImpl super.moveCell(targetParent, index) 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 index 358afb71..8b9f6f13 100644 --- 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 @@ -9,10 +9,16 @@ sealed class CellPropertyValue { } @Serializable -data class BooleanCellPropertyValue(override val value: Boolean) : CellPropertyValue() +data class BooleanCellPropertyValue( + override val value: Boolean, +) : CellPropertyValue() @Serializable -data class StringCellPropertyValue(override val value: String) : CellPropertyValue() +data class StringCellPropertyValue( + override val value: String, +) : CellPropertyValue() @Serializable -data class CellReferenceListValue(override val value: List) : CellPropertyValue>() +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 index 66ed0637..cf9b5bfb 100644 --- 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 @@ -27,30 +27,24 @@ open class CellTreeBase : IMutableCellTree { override fun getRoot(): CellImpl = root - override fun getCell(id: CellInstanceId): CellImpl { - return withTreeLock { allCells[id] ?: throw NoSuchElementException("Cell ID: ${id.id}") } - } + override fun getCell(id: CellInstanceId): CellImpl = withTreeLock { allCells[id] ?: throw NoSuchElementException("Cell ID: ${id.id}") } - final override fun createCell(): IMutableCellTree.MutableCell { - return createCell(CellInstanceId(nextId++)) - } + final override fun createCell(): IMutableCellTree.MutableCell = createCell(CellInstanceId(nextId++)) - override fun createCell(id: CellInstanceId): IMutableCellTree.MutableCell { - return withTreeLock { + 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 { - return withTreeLock { + fun resolveCell(reference: CellReference): List = + withTreeLock { cellIndex.update(getRoot().referencesIndexList) cellIndex.lookup(reference).map { getCell(it) } } - } fun deleteDetachedCells() { withTreeLock { @@ -66,7 +60,10 @@ open class CellTreeBase : IMutableCellTree { } } - protected open fun newCellInstance(id: CellInstanceId, parent: CellImpl? = null) = CellImpl(id, parent) + protected open fun newCellInstance( + id: CellInstanceId, + parent: CellImpl? = null, + ) = CellImpl(id, parent) open inner class CellImpl( private val id: CellInstanceId, @@ -75,15 +72,16 @@ open class CellTreeBase : IMutableCellTree { protected val properties: MutableMap = HashMap() private val children: MutableList = ArrayList() - private val referencesIndexList_ = ResettableLazy { - withTreeLock { - IncrementalList.concat( - IncrementalList.of(this.cellReferences.map { it to id }), - IncrementalList.concat(getChildren().map { (it as CellImpl).referencesIndexList }), - ) + 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 referencesIndexList_ + val referencesIndexList: IncrementalList> by cachedReferencesIndexList override fun getTree(): IMutableCellTree = this@CellTreeBase @@ -91,26 +89,31 @@ open class CellTreeBase : IMutableCellTree { override fun getParent(): IMutableCellTree.MutableCell? = withTreeLock { parent } - override fun isAttached(): Boolean { - return withTreeLock { this == root || parent?.isAttached() == true } - } + override fun isAttached(): Boolean = withTreeLock { this == root || parent?.isAttached() == true } - override fun getProperty(key: CellPropertyKey): T { - return withTreeLock { if (properties.containsKey(key.name)) properties[key.name] as T else key.defaultValue } - } + 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<*>?) { + fun setProperty( + key: String, + newValue: CellPropertyValue<*>?, + ) { withTreeLock { properties[key] = newValue?.value } } @Synchronized - override fun setProperty(key: CellPropertyKey, newValue: T) { + override fun setProperty( + key: CellPropertyKey, + newValue: T, + ) { withTreeLock { require(newValue !is CellPropertyKey<*>) properties[key.name] = newValue - if (key == CommonCellProperties.cellReferences) referencesIndexList_.reset() + if (key == CommonCellProperties.cellReferences) cachedReferencesIndexList.reset() } } @@ -123,37 +126,32 @@ open class CellTreeBase : IMutableCellTree { override fun removeProperty(key: CellPropertyKey<*>) { withTreeLock { properties.remove(key.name) - if (key == CommonCellProperties.cellReferences) referencesIndexList_.reset() + if (key == CommonCellProperties.cellReferences) cachedReferencesIndexList.reset() } } - override fun hasProperty(key: CellPropertyKey<*>): Boolean { - return withTreeLock { properties.containsKey(key.name) } - } + override fun hasProperty(key: CellPropertyKey<*>): Boolean = withTreeLock { properties.containsKey(key.name) } - override fun getChildren(): List { - return withTreeLock { children } - } + override fun getChildren(): List = withTreeLock { children } - override fun getChildAt(index: Int): IMutableCellTree.MutableCell? { - return withTreeLock { children.getOrNull(index) } - } + override fun getChildAt(index: Int): IMutableCellTree.MutableCell? = withTreeLock { children.getOrNull(index) } - fun addNewChild(index: Int, childId: CellInstanceId): IMutableCellTree.MutableCell { - return withTreeLock { + 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 { - return withTreeLock { - referencesIndexList_.reset() + override fun addNewChild(index: Int): IMutableCellTree.MutableCell = + withTreeLock { + cachedReferencesIndexList.reset() addNewChild(index, CellInstanceId(nextId++)) } - } override fun moveCell(index: Int) { withTreeLock { @@ -163,12 +161,15 @@ open class CellTreeBase : IMutableCellTree { } } - override fun moveCell(targetParent: IMutableCellTree.MutableCell, index: Int) { + override fun moveCell( + targetParent: IMutableCellTree.MutableCell, + index: Int, + ) { withTreeLock { targetParent as CellImpl require(targetParent != parent) { "Use moveCell(index: Int)" } - parent?.referencesIndexList_?.reset() - targetParent.referencesIndexList_.reset() + parent?.cachedReferencesIndexList?.reset() + targetParent.cachedReferencesIndexList.reset() val oldParent = parent oldParent?.children?.remove(this) targetParent.children.add(index, this) @@ -179,7 +180,7 @@ open class CellTreeBase : IMutableCellTree { override fun detach() { withTreeLock { - parent?.referencesIndexList_?.reset() + parent?.cachedReferencesIndexList?.reset() detachedCells.add(id) parent?.children?.remove(this) parent = null @@ -188,7 +189,7 @@ open class CellTreeBase : IMutableCellTree { override fun delete() { withTreeLock { - parent?.referencesIndexList_?.reset() + parent?.cachedReferencesIndexList?.reset() children.toList().forEach { it.delete() } parent?.children?.remove(this) parent = null @@ -203,8 +204,6 @@ open class CellTreeBase : IMutableCellTree { return if (index >= 0) index else 0 } - override fun toString(): String { - return id.id.toString() - } + 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 index 03866ad9..de2f242d 100644 --- 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 @@ -6,25 +6,49 @@ import kotlinx.serialization.Serializable sealed class CellTreeOp @Serializable -data class CellPropertyChangeOp(val id: CellInstanceId, val key: String, val value: CellPropertyValue<*>?) : CellTreeOp() +data class CellPropertyChangeOp( + val id: CellInstanceId, + val key: String, + val value: CellPropertyValue<*>?, +) : CellTreeOp() @Serializable -data class CellPropertyRemoveOp(val id: CellInstanceId, val key: String) : CellTreeOp() +data class CellPropertyRemoveOp( + val id: CellInstanceId, + val key: String, +) : CellTreeOp() @Serializable -data class NewChildCellOp(val parentId: CellInstanceId, val index: Int, val childId: CellInstanceId) : CellTreeOp() +data class NewChildCellOp( + val parentId: CellInstanceId, + val index: Int, + val childId: CellInstanceId, +) : CellTreeOp() @Serializable -data class NewCellOp(val id: CellInstanceId) : CellTreeOp() +data class NewCellOp( + val id: CellInstanceId, +) : CellTreeOp() @Serializable -data class MoveCellOp(val index: Int, val childId: CellInstanceId) : CellTreeOp() +data class MoveCellOp( + val index: Int, + val childId: CellInstanceId, +) : CellTreeOp() @Serializable -data class MoveCellToOp(val targetParent: CellInstanceId, val index: Int, val childId: CellInstanceId) : CellTreeOp() +data class MoveCellToOp( + val targetParent: CellInstanceId, + val index: Int, + val childId: CellInstanceId, +) : CellTreeOp() @Serializable -data class CellDeleteOp(val id: CellInstanceId) : CellTreeOp() +data class CellDeleteOp( + val id: CellInstanceId, +) : CellTreeOp() @Serializable -data class CellDetachOp(val id: CellInstanceId) : CellTreeOp() +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 index 0fa87c8e..3a41f450 100644 --- 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 @@ -6,46 +6,74 @@ import kotlin.jvm.JvmInline @Serializable @JvmInline -value class CellInstanceId(val id: Long) +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 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 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 a04b3b8d..9536ddca 100644 --- a/projectional-editor/src/commonTest/kotlin/org/modelix/editor/CellNavigationTest.kt +++ b/projectional-editor/src/commonTest/kotlin/org/modelix/editor/CellNavigationTest.kt @@ -7,30 +7,31 @@ import kotlin.test.Test import kotlin.test.assertEquals class CellNavigationTest { - private val rootCell = BackendCellTree().run { - cell("root") { - cell("1") { - cell("11") { - cell("111") - cell("112") + private val rootCell = + BackendCellTree().run { + cell("root") { + cell("1") { + cell("11") { + cell("111") + cell("112") + } + cell("12") { + cell("121") + cell("122") + } } - cell("12") { - cell("121") - cell("122") - } - } - cell("2") { - cell("21") { - cell("211") - cell("212") - } - cell("22") { - cell("221") - cell("222") + cell("2") { + cell("21") { + cell("211") + cell("212") + } + cell("22") { + cell("221") + cell("222") + } } } } - } @Test fun order_of_previousCells() { @@ -51,7 +52,11 @@ class CellNavigationTest { "111", "root", ), - rootCell.lastLeaf().previousCells().map { it.text }.toList(), + rootCell + .lastLeaf() + .previousCells() + .map { it.text } + .toList(), ) } @@ -75,15 +80,27 @@ class CellNavigationTest { "222", "root", ), - rootCell.firstLeaf().nextCells().map { it.text }.toList(), + rootCell + .firstLeaf() + .nextCells() + .map { it.text } + .toList(), ) } - private fun IMutableCellTree.cell(text: String, body: IMutableCellTree.MutableCell.() -> Unit): Cell { - return this.createCell().also { it.setProperty(TextCellProperties.text, 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 IMutableCellTree.MutableCell.cell(text: String, body: IMutableCellTree.MutableCell.() -> Unit = {}): Cell { - return this.addNewChild().also { it.setProperty(TextCellProperties.text, text) }.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 59f78d30..439c7a67 100644 --- a/projectional-editor/src/commonTest/kotlin/org/modelix/editor/EditorKeyboardTest.kt +++ b/projectional-editor/src/commonTest/kotlin/org/modelix/editor/EditorKeyboardTest.kt @@ -8,71 +8,103 @@ import kotlin.test.assertEquals class EditorKeyboardTest { @Test - 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()) + 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() = 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()) + 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 suspend fun testCaretChange(editor: FrontendEditorComponent, 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()!! + 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 385ada20..a15239bc 100644 --- a/projectional-editor/src/commonTest/kotlin/org/modelix/editor/TextLayouterTest.kt +++ b/projectional-editor/src/commonTest/kotlin/org/modelix/editor/TextLayouterTest.kt @@ -27,19 +27,37 @@ 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) { + @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 -> 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 b492d475..aa84d7fc 100644 --- a/projectional-editor/src/jsMain/kotlin/org/modelix/editor/JsEditorComponent.kt +++ b/projectional-editor/src/jsMain/kotlin/org/modelix/editor/JsEditorComponent.kt @@ -7,11 +7,14 @@ import org.w3c.dom.events.Event import org.w3c.dom.events.KeyboardEvent import org.w3c.dom.events.MouseEvent -class JsEditorComponent(service: TextEditorService) : FrontendEditorComponent(service, JSDom()), 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() 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 bdd3032a..9bc8fe0a 100644 --- a/projectional-editor/src/jsTest/kotlin/org/modelix/editor/IncrementalJSDOMBuilderTest.kt +++ b/projectional-editor/src/jsTest/kotlin/org/modelix/editor/IncrementalJSDOMBuilderTest.kt @@ -26,17 +26,18 @@ class IncrementalJSDOMBuilderTest { 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) + val rootCell = + tree.createCell().apply { + cell("a") + addNewChild().apply { + cell("b").also { textCellToChange = it } + addNewChild().also { + it.setProperty(CommonCellProperties.onNewLine, true) + } + cell("c") } - cell("c") + cell("d") } - cell("d") - } var domBuilder: TagConsumer = IncrementalVirtualDOMBuilder(JSDom(), null, generatedHtmlMap) val dom = rootCell.layout.toHtml(domBuilder) @@ -53,10 +54,11 @@ class IncrementalJSDOMBuilderTest { 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 "-" - } + 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" } @@ -94,18 +96,36 @@ 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().isNotEmpty() }.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") (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") randomCell.getParent()!!.addNewChild(randomCell.index()).also { @@ -114,7 +134,12 @@ class IncrementalJSDOMBuilderTest { } } - fun runRandomTest(rand: Random, cellsPerLevel: Int, levels: Int, modify: (MutableCell) -> MutableCell) { + fun runRandomTest( + rand: Random, + cellsPerLevel: Int, + levels: Int, + modify: (MutableCell) -> MutableCell, + ) { val generatedHtmlMap = GeneratedHtmlMap() val tree = FrontendCellTree() val cell = EditorTestUtils.buildRandomCells(rand, cellsPerLevel, levels, tree) @@ -133,14 +158,25 @@ class IncrementalJSDOMBuilderTest { assertEquals(html2nonIncremental, html2incremental) } - private fun IMutableCellTree.cell(text: String, body: IMutableCellTree.MutableCell.() -> Unit): IMutableCellTree.MutableCell { - return this.createCell().also { it.setProperty(TextCellProperties.text, text) }.also(body) - } - - private fun IMutableCellTree.MutableCell.cell(text: String, body: IMutableCellTree.MutableCell.() -> Unit = {}): IMutableCellTree.MutableCell { - return this.addNewChild().also { - it.type = ECellType.TEXT - it.text = text - }.also(body) - } + 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-mps-test/src/test/kotlin/PagesTest.kt b/react-ssr-mps-test/src/test/kotlin/PagesTest.kt index a4b6c324..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-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) + 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..d624154e 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,20 +334,29 @@ 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::parameters.name}: { @@ -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/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") } } From 2986d6cbbe6df346850ffd994ad569f42055e749 Mon Sep 17 00:00:00 2001 From: Sascha Lisson Date: Thu, 12 Feb 2026 16:39:35 +0100 Subject: [PATCH 3/4] ci: fix pre-commit workflow --- .github/workflows/linting.yml | 17 ++++++--- .pre-commit-config.yaml | 9 +++-- README.md | 2 +- .../react/ssr/server/ComponentBuilder.kt | 2 +- renovate.json5 | 35 ++++++++++++------- 5 files changed, 44 insertions(+), 21 deletions(-) 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 af63934a..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=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/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 d624154e..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 @@ -358,7 +358,7 @@ class MessageSendingHandlerBuilder( fun build(): String = """ (${functionParameters.joinToString(",")}) => window.modelix.sendMessage({ - ${MessageFromClient::messageId.name}: "$messageId", + ${MessageFromClient::messageId.name}: "$messageId", ${MessageFromClient::parameters.name}: { ${ messageParameters.entries.joinToString(", ") { 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', + }, ], } From d2283270f25fe75c5418da15994766b3a6e22bdb Mon Sep 17 00:00:00 2001 From: Sascha Lisson Date: Fri, 13 Feb 2026 14:12:24 +0100 Subject: [PATCH 4/4] fix: race conditions in editor tests --- .../editor/kernelf/CodeCompletionTest.kt | 2 +- .../build.gradle.kts | 1 + .../editor/ssr/mps/BaseLanguageTests.kt | 388 +++++++++--------- .../org/modelix/editor/CaretSelection.kt | 20 +- .../org/modelix/editor/CellSelection.kt | 14 +- .../modelix/editor/FrontendEditorComponent.kt | 92 +++-- 6 files changed, 275 insertions(+), 242 deletions(-) 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 f0cb0d34..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 @@ -120,7 +120,7 @@ class CodeCompletionTest { fun actionsOnNameProperty() = runCompletionTest { val namePropertyCell = editor.getRootCell().descendants().find { it.getVisibleText() == "stringTests" }!! - editor.changeSelection(CaretSelection(editor, namePropertyCell.layoutable()!!, 0)) + editor.changeSelectionLater(CaretSelection(editor, namePropertyCell.layoutable()!!, 0)) val firstLeaf = namePropertyCell.firstLeaf() assertEquals("stringTests", firstLeaf.getVisibleText()) diff --git a/projectional-editor-ssr-mps-languages/build.gradle.kts b/projectional-editor-ssr-mps-languages/build.gradle.kts index 18b31b24..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) } 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 94325154..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 @@ -62,12 +62,11 @@ class BaseLanguageTests : TestBase("SimpleProject") { mpsIntegration.init(mpsProject.repository) coroutineScope = CoroutineScope(Dispatchers.Default) service = TextEditorServiceImpl(editorEngine, MPSArea(mpsProject.repository).asModel(), coroutineScope) + editor = FrontendEditorComponent(service) runBlocking { - editor = - FrontendEditorComponent(service).also { - it.openNode(classNode.getNodeReference()).await() - } + editor.openNode(classNode.getNodeReference()).await() editor.flush() + println(editor.getRootCell().layout.toString()) } } @@ -99,7 +98,7 @@ 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())) @@ -110,27 +109,25 @@ class BaseLanguageTests : TestBase("SimpleProject") { 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) { - runBlocking { editor.processKeyEvent(JSKeyboardEvent(JSKeyboardEventType.KEYDOWN, key)) } + suspend fun pressKey(key: KnownKeys) { + editor.processKeyEvent(JSKeyboardEvent(JSKeyboardEventType.KEYDOWN, key)) } - fun typeText(text: CharSequence) { + suspend fun typeText(text: CharSequence) { for (c in text) { - runBlocking { - editor.processKeyEvent( - JSKeyboardEvent( - eventType = JSKeyboardEventType.KEYDOWN, - typedText = c.toString(), - knownKey = null, - rawKey = c.toString(), - ), - ) - } + editor.processKeyEvent( + JSKeyboardEvent( + eventType = JSKeyboardEventType.KEYDOWN, + typedText = c.toString(), + knownKey = null, + rawKey = c.toString(), + ), + ) } } @@ -162,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() { @@ -174,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; @@ -263,8 +266,8 @@ class BaseLanguageTests : TestBase("SimpleProject") { } """) - pressKey(KnownKeys.Tab) - assertEditorText(""" + pressKey(KnownKeys.Tab) + assertEditorText(""" public class Class1 { public void method1() { int abc = ; @@ -272,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; @@ -285,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("") @@ -395,133 +402,137 @@ 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 ") - assertEditorText(""" + 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(""" + pressKey(KnownKeys.Tab) + typeText("abc") + assertEditorText(""" public class Class1 { public void method1() { int abc; } } """) - typeText("=") - assertEditorText(""" + typeText("=") + assertEditorText(""" public class Class1 { public void method1() { int abc = ; } } """) - typeText("10") - assertEditorText(""" + typeText("10") + assertEditorText(""" public class Class1 { public void method1() { int abc = 10; } } """) - typeText("+") - typeText("20") - assertEditorText(""" + typeText("+") + typeText("20") + assertEditorText(""" public class Class1 { public void method1() { int abc = 10 + 20; } } """) - assertFinalEditorText(""" + assertFinalEditorText(""" public class Class1 { public void method1() { int abc = 10 + 20; } } """) - } + } private fun runParsingTest(input: String) = runParsingTest(input, false) @@ -534,7 +545,6 @@ class BaseLanguageTests : TestBase("SimpleProject") { readAction { println("Running test ...") placeCaretIntoCellWithText("") - val layoutable = (editor.getSelection() as CaretSelection).layoutable val node = layoutable.cell 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 f0a28ac4..b1e6ab62 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CaretSelection.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CaretSelection.kt @@ -58,9 +58,9 @@ class CaretSelection( KnownKeys.ArrowLeft -> { if (end > 0) { if (event.modifiers.shift) { - editor.changeSelection(CaretSelection(editor, layoutable, start, end - 1)) + editor.doChangeSelection(CaretSelection(editor, layoutable, start, end - 1)) } else { - editor.changeSelection(CaretSelection(editor, layoutable, end - 1)) + editor.doChangeSelection(CaretSelection(editor, layoutable, end - 1)) } } else { val previous = @@ -72,9 +72,9 @@ class CaretSelection( 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(editor, it, true, this)) } + selectableAncestor?.let { editor.doChangeSelection(CellSelection(editor, it, true, this)) } } else { - editor.changeSelection(CaretSelection(editor, previous, previous.cell.getMaxCaretPos())) + editor.doChangeSelection(CaretSelection(editor, previous, previous.cell.getMaxCaretPos())) } } } @@ -83,9 +83,9 @@ class CaretSelection( KnownKeys.ArrowRight -> { if (end < (layoutable.cell.getSelectableText()?.length ?: 0)) { if (event.modifiers.shift) { - editor.changeSelection(CaretSelection(editor, layoutable, start, end + 1)) + editor.doChangeSelection(CaretSelection(editor, layoutable, start, end + 1)) } else { - editor.changeSelection(CaretSelection(editor, layoutable, end + 1)) + editor.doChangeSelection(CaretSelection(editor, layoutable, end + 1)) } } else { val next = @@ -97,9 +97,9 @@ class CaretSelection( 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(editor, it, false, this)) } + selectableAncestor?.let { editor.doChangeSelection(CellSelection(editor, it, false, this)) } } else { - editor.changeSelection(CaretSelection(editor, next, 0)) + editor.doChangeSelection(CaretSelection(editor, next, 0)) } } } @@ -111,7 +111,7 @@ class CaretSelection( KnownKeys.ArrowUp -> { if (event.modifiers.meta) { - layoutable.cell.let { editor.changeSelection(CellSelection(editor, it, true, this)) } + layoutable.cell.let { editor.doChangeSelection(CellSelection(editor, it, true, this)) } } else { selectNextPreviousLine(false) } @@ -171,7 +171,7 @@ class CaretSelection( fun selectNextPreviousLine(next: Boolean) { createNextPreviousLineSelection(next, desiredXPosition ?: getAbsoluteX()) - ?.let { editor?.changeSelection(it) } + ?.let { editor?.doChangeSelection(it) } } fun getAbsoluteX() = layoutable.getX() + end 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 11912b5a..e7230d53 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellSelection.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellSelection.kt @@ -28,7 +28,7 @@ data class CellSelection( cell .ancestors() .firstOrNull { it.getProperty(CommonCellProperties.selectable) } - ?.let { editor.changeSelection(CellSelection(editor, it, directionLeft, this)) } + ?.let { editor.doChangeSelection(CellSelection(editor, it, directionLeft, this)) } } else { unwrapCaretSelection()?.selectNextPreviousLine(false) } @@ -36,7 +36,7 @@ data class CellSelection( KnownKeys.ArrowDown -> { if (event.modifiers == Modifiers.META && previousSelection != null) { - editor.changeSelection(previousSelection) + editor.doChangeSelection(previousSelection) } else { unwrapCaretSelection()?.selectNextPreviousLine(true) } @@ -49,26 +49,26 @@ data class CellSelection( cell .ancestors() .firstOrNull { it.isSelectable() } - ?.let { editor.changeSelection(CellSelection(editor, it, directionLeft, this)) } + ?.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(editor, 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(editor, it, 0)) } + ?.let { editor.doChangeSelection(CaretSelection(editor, it, 0)) } } else { tabTargets .lastOrNull() ?.layoutable() - ?.let { editor.changeSelection(CaretSelection(editor, it, it.cell.getSelectableText()?.length ?: 0)) } + ?.let { editor.doChangeSelection(CaretSelection(editor, it, it.cell.getSelectableText()?.length ?: 0)) } } } } diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/FrontendEditorComponent.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/FrontendEditorComponent.kt index afacae5b..a449a7cc 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/FrontendEditorComponent.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/FrontendEditorComponent.kt @@ -46,17 +46,7 @@ open class FrontendEditorComponent( private var highlightedLine: IVirtualDom.HTMLElement? = null private var highlightedCell: IVirtualDom.HTMLElement? = null private val coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.Default) - private val uiEventQueue = - coroutineScope.consume(capacity = 100, onBufferOverflow = BufferOverflow.DROP_LATEST) { event -> - when (event) { - is JSKeyboardEvent -> processKeyEvent(event) - is JSMouseEvent -> processMouseEvent(event) - } - } - private val uiUpdateQueue = - coroutineScope.consume Unit, CompletableDeferred>>(capacity = Channel.UNLIMITED) { - it.second.completeWith(runCatching { it.first.invoke() }) - } + private val eventQueue = UIEventDispatcher(coroutineScope) private val updateLoop: AtomicReference = AtomicReference(null) private val updateLock = Any() @@ -79,7 +69,7 @@ open class FrontendEditorComponent( return firstUpdate } - suspend fun flush() = enqueueUpdate(service.flush(editorId)).await() + suspend fun flush() = flushRemote() suspend fun flushRemote() = enqueueUpdate(service.flush(editorId)).await() @@ -92,9 +82,9 @@ open class FrontendEditorComponent( suspend fun flushAndUpdateSelection(newSelection: () -> Selection?) { val updateData = service.flush(editorId) - enqueueUpdate { + enqueueUpdate { updateNow(updateData) - newSelection()?.let { changeSelection(it) } + newSelection()?.let { doChangeSelection(it) } }.await() } @@ -119,11 +109,7 @@ open class FrontendEditorComponent( fun enqueueUpdate(updateData: EditorUpdateData): Deferred = enqueueUpdate { updateNow(updateData) } - fun enqueueUpdate(body: () -> Unit): Deferred { - val result = CompletableDeferred() - uiUpdateQueue.trySend(body to result).getOrThrow() - return result - } + fun enqueueUpdate(body: suspend () -> R): Deferred = eventQueue.invokeLater(body) fun updateNow(update: EditorUpdateData? = null) { runSynchronized(updateLock) { @@ -200,12 +186,21 @@ open class FrontendEditorComponent( ?: selection?.update(this) } - open fun changeSelection(newSelection: Selection) { + 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( @@ -214,19 +209,20 @@ open class FrontendEditorComponent( entries: List, pattern: String = "", caretPosition: Int? = null, - ) { - codeCompletionMenu = CodeCompletionMenu(this, anchor, position, entries, pattern, caretPosition) - updateNow() - } + ): Deferred = + eventQueue.invokeLater { + codeCompletionMenu = CodeCompletionMenu(this, anchor, position, entries, pattern, caretPosition) + updateNow() + } - fun closeCodeCompletionMenu() { - codeCompletionMenu = null - updateNow() - } + fun closeCodeCompletionMenu(): Deferred = + eventQueue.invokeLater { + codeCompletionMenu = null + updateNow() + } fun dispose() { - uiEventQueue.close() - uiUpdateQueue.close() + eventQueue.dispose() updateLoop.getAndUpdate { currentJob -> currentJob?.cancel("disposed") null @@ -235,7 +231,12 @@ open class FrontendEditorComponent( } fun enqueueUIEvent(event: JSUIEvent): Boolean { - uiEventQueue.trySend(event).onFailure { LOG.error(it) { "UI event ignored: $event" } } + eventQueue.invokeLater { + when (event) { + is JSKeyboardEvent -> processKeyEvent(event) + is JSMouseEvent -> processMouseEvent(event) + } + } return true } @@ -286,7 +287,7 @@ open class FrontendEditorComponent( (relativeClickX / characterWidth) .roundToInt() .coerceAtMost(layoutable.cell.getMaxCaretPos()) - changeSelection(CaretSelection(this, layoutable, caretPos)) + doChangeSelection(CaretSelection(this, layoutable, caretPos)) return true } @@ -326,7 +327,7 @@ open class FrontendEditorComponent( .getSelectableText() ?.length ?: 0 } - changeSelection(CaretSelection(this, closest.first, caretPos)) + doChangeSelection(CaretSelection(this, closest.first, caretPos)) return true } @@ -351,8 +352,8 @@ open class FrontendEditorComponent( suspend fun serviceCall(call: suspend TextEditorService.() -> R): R { val result = call(service) when (result) { - is EditorUpdateData -> enqueueUpdate(result) - is ServiceCallResult<*> -> result.updateData?.let { enqueueUpdate(it) } + is EditorUpdateData -> enqueueUpdate(result).await() + is ServiceCallResult<*> -> result.updateData?.let { enqueueUpdate(it).await() } } return result } @@ -367,3 +368,24 @@ open class FrontendEditorComponent( 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() + } +}