From fea10d339b381b82b1d7c98c8f6fa3009ba76284 Mon Sep 17 00:00:00 2001 From: Sascha Lisson Date: Thu, 2 May 2024 10:57:02 +0200 Subject: [PATCH 01/18] feat: show wrapping concept in description of wrapper actions If there are multiple wrapper actions it wasn't possible to distinguish them before. --- .../kotlin/org/modelix/editor/CellTemplate.kt | 13 ++++++++++--- .../kotlin/org/modelix/editor/INonExistingNode.kt | 1 - 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTemplate.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTemplate.kt index 1e72da12..5df5535d 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTemplate.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTemplate.kt @@ -168,13 +168,20 @@ class ConstantCellTemplate(concept: IConcept, val text: String) : } inner class InstantiateNodeAction(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 + } + wrapperText(concept.getShortName(), location.getParent()) + } + override fun getMatchingText(): String { return text } - override fun getDescription(): String { - return concept.getShortName() - } + override fun getDescription(): String = description override fun execute(editor: EditorComponent) { val newNode = location.getExistingAncestor()!!.getArea().executeWrite { 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 c0ade64b..c2128170 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/INonExistingNode.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/INonExistingNode.kt @@ -6,7 +6,6 @@ import org.modelix.model.api.INode import org.modelix.model.api.NullChildLink import org.modelix.model.api.index import org.modelix.model.api.isInstanceOf -import org.modelix.model.api.isSubConceptOf import org.modelix.model.api.remove interface INonExistingNode { From 483596cf2a82c3f80d7dbac92825736acdd40f15 Mon Sep 17 00:00:00 2001 From: Sascha Lisson Date: Thu, 2 May 2024 10:58:01 +0200 Subject: [PATCH 02/18] fix: MPS constraints weren't evaluated for wrapper actions --- .../org/modelix/editor/ssr/mps/ModelixSSRServerForMPS.kt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) 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 c47d5d70..a9ba2876 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 @@ -291,9 +291,12 @@ object MPSConstraints : IConstraintsChecker { // Constraints only prevent creating a node. If it already exists, it's handled by the model checker. if (node.getNode() != null) return emptyList() - val parentNode = node.getParent()?.getNode().toMPS() - // MPS doesn't support constraint checking without a parent node - if (parentNode == null) return emptyList() + // Correct would be `parentNode = node.getParent()?.getNode().toMPS()` + // but the parent node is not allowed to be null. + // MPS itself then just passes the nearest existing ancestor to the constraints. + // Without this hack we cannot evaluate any constraints and there would be too many incorrect entries in the + // code completion menu. + val parentNode = node.getExistingAncestor().toMPS() // ConstraintsCanBeFacade.checkCanBeRoot() From ba541c6df1164f6af6ff09e68ffb06fad8aa6a77 Mon Sep 17 00:00:00 2001 From: Sascha Lisson Date: Thu, 2 May 2024 13:18:35 +0200 Subject: [PATCH 03/18] feat: text in the code completion menu for a concepts can be changed There is a `subsitute/...` cell that can be used to customize how a concept is shown in the code completion menu. And empty string removed it completely. --- ...mps.notation.impl.baseLanguage.modelix.mps | 26 +++++ ...notation.generator.templates@generator.mps | 106 ++++++++++++++++++ .../org/modelix/editor/CellProperties.kt | 1 + .../kotlin/org/modelix/editor/CellTemplate.kt | 55 +++++---- .../org/modelix/editor/CellTemplateBuilder.kt | 6 + 5 files changed, 170 insertions(+), 24 deletions(-) diff --git a/mps/modules/org.modelix.mps.notation.impl.baseLanguage/models/org.modelix.mps.notation.impl.baseLanguage.modelix.mps b/mps/modules/org.modelix.mps.notation.impl.baseLanguage/models/org.modelix.mps.notation.impl.baseLanguage.modelix.mps index 362bf057..b7b13b69 100644 --- a/mps/modules/org.modelix.mps.notation.impl.baseLanguage/models/org.modelix.mps.notation.impl.baseLanguage.modelix.mps +++ b/mps/modules/org.modelix.mps.notation.impl.baseLanguage/models/org.modelix.mps.notation.impl.baseLanguage.modelix.mps @@ -66,6 +66,7 @@ + @@ -293,6 +294,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + @@ -1722,3 +1747,4 @@ + diff --git a/mps/modules/org.modelix.mps.notation/generator/templates/org.modelix.mps.notation.generator.templates@generator.mps b/mps/modules/org.modelix.mps.notation/generator/templates/org.modelix.mps.notation.generator.templates@generator.mps index 1232833f..3caaf824 100644 --- a/mps/modules/org.modelix.mps.notation/generator/templates/org.modelix.mps.notation.generator.templates@generator.mps +++ b/mps/modules/org.modelix.mps.notation/generator/templates/org.modelix.mps.notation.generator.templates@generator.mps @@ -48,11 +48,15 @@ + + + + @@ -69,6 +73,18 @@ + + + + + + + + + + + + @@ -301,6 +317,14 @@ + + + + + + + + @@ -374,6 +398,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1567,3 +1672,4 @@ + 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 b206c775..6bf231f0 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellProperties.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellProperties.kt @@ -46,6 +46,7 @@ object CommonCellProperties { 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 } fun Cell.isTabTarget() = getProperty(CommonCellProperties.tabTarget) diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTemplate.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTemplate.kt index 5df5535d..b115d3fa 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTemplate.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTemplate.kt @@ -8,7 +8,6 @@ import org.modelix.model.api.INode import org.modelix.model.api.INodeReference import org.modelix.model.api.IProperty import org.modelix.model.api.IReferenceLink -import org.modelix.model.api.isSubConceptOf import org.modelix.scopes.ScopeAspect import kotlin.jvm.JvmName @@ -39,6 +38,10 @@ abstract class CellTemplate(val concept: IConcept) { protected abstract fun createCell(context: CellCreationContext, node: INode): CellData open fun getInstantiationActions(location: INonExistingNode, parameters: CodeCompletionParameters): List? { + val completionText = properties[CommonCellProperties.codeCompletionText] + if (completionText != null) { + return listOf(InstantiateNodeCompletionAction(completionText, concept, location)) + } return children.asSequence().mapNotNull { it.getInstantiationActions(location, parameters) }.firstOrNull() } @@ -133,7 +136,7 @@ class ConstantCellTemplate(concept: IConcept, val text: String) : CellTemplate(concept), IGrammarSymbol { override fun createCell(context: CellCreationContext, node: INode) = TextCellData(text, "") override fun getInstantiationActions(location: INonExistingNode, parameters: CodeCompletionParameters): List? { - return listOf(InstantiateNodeAction(location)) + return listOf(InstantiateNodeCompletionAction(text, concept, location)) } override fun createWrapperAction(nodeToWrap: INode, wrappingLink: IChildLink): List { @@ -166,31 +169,35 @@ class ConstantCellTemplate(concept: IConcept, val text: String) : fun getTemplate() = this@ConstantCellTemplate } +} - inner class InstantiateNodeAction(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 - } - wrapperText(concept.getShortName(), location.getParent()) +class InstantiateNodeCompletionAction( + private val matchingText: String, + 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 } + wrapperText(concept.getShortName(), location.getParent()) + } - override fun getMatchingText(): String { - return text - } + override fun getMatchingText(): String { + return matchingText + } - override fun getDescription(): String = description + override fun getDescription(): String = description - override fun execute(editor: EditorComponent) { - val newNode = location.getExistingAncestor()!!.getArea().executeWrite { - location.replaceNode(concept) - } - editor.selectAfterUpdate { - CaretPositionPolicy(newNode) - .getBestSelection(editor) - } + override fun execute(editor: EditorComponent) { + val newNode = location.getExistingAncestor()!!.getArea().executeWrite { + location.replaceNode(concept) + } + editor.selectAfterUpdate { + CaretPositionPolicy(newNode) + .getBestSelection(editor) } } } @@ -431,7 +438,7 @@ class ChildCellTemplate( if (link.isMultiple) { val actionCell = CellData() val action = newLineConcept?.let { - InstantiateNodeAction(NonExistingChild(ExistingNode(node), link, index), it) + InstantiateNodeCellAction(NonExistingChild(ExistingNode(node), link, index), it) } ?: InsertSubstitutionPlaceholderAction(context.editorState, createCellReference(node), index) actionCell.properties[CellActionProperties.insert] = action cell.addChild(actionCell) @@ -511,7 +518,7 @@ class InsertSubstitutionPlaceholderAction( } } -class InstantiateNodeAction(val location: INonExistingNode, val concept: IConcept) : ICellAction { +class InstantiateNodeCellAction(val location: INonExistingNode, val concept: IConcept) : ICellAction { override fun isApplicable(): Boolean = true override fun execute(editor: EditorComponent) { 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 d9a4916d..fb44e662 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTemplateBuilder.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTemplateBuilder.kt @@ -317,6 +317,12 @@ class NotationRootCellTemplateBuilder( fun condition(condition: (INode) -> Boolean) { (template as NotationRootCellTemplate).condition = condition } + + fun hideInCodeCompletion() = completionText("") + + fun completionText(text: String) { + template.properties[CommonCellProperties.codeCompletionText] = text + } } class PropertyCellTemplateBuilder( From fe24c2b25daf3b6434862821bc15dd2f7badc7de Mon Sep 17 00:00:00 2001 From: Sascha Lisson Date: Thu, 2 May 2024 13:20:30 +0200 Subject: [PATCH 04/18] fix: removed concepts of not imported languages from the CC menu --- .../editor/ssr/mps/ModelixSSRServerForMPS.kt | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) 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 a9ba2876..0901899c 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 @@ -37,8 +37,10 @@ import jetbrains.mps.core.aspects.constraints.rules.kinds.CanBeAncestorContext import jetbrains.mps.core.aspects.constraints.rules.kinds.ContainmentContext import jetbrains.mps.project.MPSProject import jetbrains.mps.scope.Scope +import jetbrains.mps.smodel.ModelDependencyResolver import jetbrains.mps.smodel.constraints.ConstraintsCanBeFacade import jetbrains.mps.smodel.constraints.ModelConstraints +import jetbrains.mps.smodel.language.LanguageRegistry import kotlinx.html.a import kotlinx.html.base import kotlinx.html.body @@ -70,6 +72,7 @@ import org.modelix.model.api.NodeReference import org.modelix.model.api.runSynchronized import org.modelix.model.mpsadapters.MPSChildLink import org.modelix.model.mpsadapters.MPSConcept +import org.modelix.model.mpsadapters.MPSModelAsNode import org.modelix.model.mpsadapters.MPSNode import org.modelix.model.mpsadapters.MPSProperty import org.modelix.model.mpsadapters.MPSReferenceLink @@ -322,7 +325,20 @@ object MPSConstraints : IConstraintsChecker { val parentViolations = ConstraintsCanBeFacade.checkCanBeParent(containmentContext).asSequence() val childViolations = ConstraintsCanBeFacade.checkCanBeChild(containmentContext).asSequence() return (ancestorViolations + parentViolations + childViolations).map { MPSConstraintViolation(it) }.toList() + - (node.getParent()?.let { check(it) } ?: emptyList()) + (node.getParent()?.let { check(it) } ?: emptyList()) + checkLanguageImported(node) + } + + 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() + return if (!usedLanguages.contains(language)) { + listOf(MPSLanguageNotImportedViolation(concept.concept)) + } else { + emptyList() + } } } @@ -335,3 +351,4 @@ fun IConcept?.toMPS(): SAbstractConcept? = if (this is MPSConcept) this.concept val INode.name get() = getPropertyValue(BuiltinLanguages.jetbrains_mps_lang_core.INamedConcept.name) class MPSConstraintViolation(val rule: Rule<*>) : IConstraintViolation +class MPSLanguageNotImportedViolation(val concept: SAbstractConcept) : IConstraintViolation From d641ba213702fa4378cc27080656445667b57437 Mon Sep 17 00:00:00 2001 From: Sascha Lisson Date: Thu, 2 May 2024 15:06:39 +0200 Subject: [PATCH 05/18] feat: evaluate MPS property constraints for code completion entries --- .../editor/ssr/mps/ModelixSSRServerForMPS.kt | 15 +++++++++++++++ .../org/modelix/constraints/ConstraintsAspect.kt | 4 ++++ .../kotlin/org/modelix/editor/CellTemplate.kt | 12 +++++++++--- 3 files changed, 28 insertions(+), 3 deletions(-) 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 0901899c..34d5e53d 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 @@ -35,12 +35,16 @@ import io.ktor.server.websocket.timeout import jetbrains.mps.core.aspects.constraints.rules.Rule import jetbrains.mps.core.aspects.constraints.rules.kinds.CanBeAncestorContext import jetbrains.mps.core.aspects.constraints.rules.kinds.ContainmentContext +import jetbrains.mps.core.aspects.feedback.messages.FailingPropertyConstraintContext +import jetbrains.mps.core.aspects.feedback.problem.Problem import jetbrains.mps.project.MPSProject import jetbrains.mps.scope.Scope import jetbrains.mps.smodel.ModelDependencyResolver import jetbrains.mps.smodel.constraints.ConstraintsCanBeFacade +import jetbrains.mps.smodel.constraints.ConstraintsChildAndPropFacade import jetbrains.mps.smodel.constraints.ModelConstraints import jetbrains.mps.smodel.language.LanguageRegistry +import jetbrains.mps.smodel.presentation.IPropertyPresentationProvider import kotlinx.html.a import kotlinx.html.base import kotlinx.html.body @@ -52,6 +56,7 @@ import kotlinx.html.script import kotlinx.html.title import kotlinx.html.ul import org.jetbrains.mps.openapi.language.SAbstractConcept +import org.jetbrains.mps.openapi.language.SConcept import org.jetbrains.mps.openapi.language.SContainmentLink import org.jetbrains.mps.openapi.language.SProperty import org.jetbrains.mps.openapi.language.SReferenceLink @@ -340,6 +345,15 @@ object MPSConstraints : IConstraintsChecker { emptyList() } } + + 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 context = FailingPropertyConstraintContext(mpsNode, mpsProperty, internalValue) + return ConstraintsChildAndPropFacade.checkPropertyValue(context).map { MPSProblem(it) } + } } fun INode?.toMPS(): SNode? = if (this is MPSNode) this.node else null @@ -351,4 +365,5 @@ fun IConcept?.toMPS(): SAbstractConcept? = if (this is MPSConcept) this.concept 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 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 a51524da..36b2d3f1 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/constraints/ConstraintsAspect.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/constraints/ConstraintsAspect.kt @@ -1,17 +1,21 @@ package org.modelix.constraints import org.modelix.editor.INonExistingNode +import org.modelix.model.api.IProperty object ConstraintsAspect { val checkers: MutableSet = HashSet() 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 canCreate(node: INonExistingNode) = check(node).isEmpty() } interface IConstraintsChecker { fun check(node: INonExistingNode): List + fun checkPropertyValue(node: INonExistingNode, property: IProperty, value: String): List } interface IConstraintViolation diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTemplate.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTemplate.kt index b115d3fa..ac7d0ed4 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTemplate.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTemplate.kt @@ -1,5 +1,6 @@ package org.modelix.editor +import org.modelix.constraints.ConstraintsAspect import org.modelix.metamodel.ITypedNode import org.modelix.metamodel.untyped import org.modelix.model.api.IChildLink @@ -271,7 +272,7 @@ class OptionalCellTemplate(concept: IConcept) : open class PropertyCellTemplate(concept: IConcept, val property: IProperty) : CellTemplate(concept), IGrammarSymbol { var placeholderText: String = "" - var validator: (String) -> Boolean = { true } + var validator: ((String) -> Boolean)? = null override fun createCell(context: CellCreationContext, node: INode): CellData { val value = node.getPropertyValue(property) val data = TextCellData(value ?: "", if (value == null) placeholderText else "") @@ -286,7 +287,7 @@ open class PropertyCellTemplate(concept: IConcept, val property: IProperty) : inner class WrapPropertyValueProvider(val location: INonExistingNode) : ICodeCompletionActionProvider { override fun getApplicableActions(parameters: CodeCompletionParameters): List { - return if (validator(parameters.pattern)) { + return if (validateValue(location.replacement(concept), parameters.pattern)) { listOf(WrapPropertyValue(location, parameters.pattern)) } else { emptyList() @@ -294,6 +295,11 @@ open class PropertyCellTemplate(concept: IConcept, val property: IProperty) : } } + private fun validateValue(node: INonExistingNode, value: String): Boolean { + return validator?.invoke(value) + ?: ConstraintsAspect.checkPropertyValue(node, property, value).isEmpty() + } + inner class WrapPropertyValue(val location: INonExistingNode, val value: String) : ICodeCompletionAction { override fun getMatchingText(): String { return value @@ -316,7 +322,7 @@ open class PropertyCellTemplate(concept: IConcept, val property: IProperty) : inner class ChangePropertyAction(val node: INode) : ITextChangeAction { override fun isValid(value: String?): Boolean { if (value == null) return true - return validator(value) + return validateValue(node.toNonExisting(), value) } override fun replaceText(editor: EditorComponent, range: IntRange, replacement: String, newText: String): Boolean { From a0cb4303262da74e299be68482b41c23954f518b Mon Sep 17 00:00:00 2001 From: Sascha Lisson Date: Tue, 30 Apr 2024 20:01:56 +0200 Subject: [PATCH 06/18] fix: filter some unnecessary wrapper actions --- .../editor/ssr/mps/BaseLanguageTests.kt | 128 ++++++++++++++---- .../kotlin/org/modelix/editor/CellTemplate.kt | 20 +++ .../org/modelix/editor/CodeCompletionMenu.kt | 8 ++ .../org/modelix/editor/INonExistingNode.kt | 13 ++ 4 files changed, 140 insertions(+), 29 deletions(-) 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 df211fd3..0dab883d 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,11 +1,20 @@ package org.modelix.editor.ssr.mps import org.modelix.editor.CaretSelection +import org.modelix.editor.CodeCompletionParameters +import org.modelix.editor.EditorComponent import org.modelix.editor.EditorEngine +import org.modelix.editor.ICodeCompletionAction +import org.modelix.editor.ICodeCompletionActionProvider import org.modelix.editor.JSKeyboardEvent import org.modelix.editor.JSKeyboardEventType import org.modelix.editor.KnownKeys import org.modelix.editor.NodeCellReference +import org.modelix.editor.applyShadowing +import org.modelix.editor.descendantsAndSelf +import org.modelix.editor.flattenApplicableActions +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 @@ -17,31 +26,75 @@ import org.modelix.model.mpsadapters.MPSNode * Test editor for MPS baseLanguage ClassConcept */ class BaseLanguageTests : TestBase("SimpleProject") { - fun `test inserting new line into class`() { - lateinit var rootNode: MPSNode - lateinit var firstMethod: INode + lateinit var editor: EditorComponent + lateinit var mpsIntegration: EditorIntegrationForMPS + lateinit var editorEngine: EditorEngine + lateinit var incrementalEngine: IncrementalEngine + lateinit var classNode: MPSNode + + override fun setUp() { + super.setUp() + readAction { val solution = mpsProject.projectModules.first { it.moduleName == "Solution1" } val model = solution.models.first() - rootNode = model.rootNodes.first().let { MPSNode(it) } - firstMethod = rootNode.allChildren.first { it.concept?.getShortName() == "InstanceMethodDeclaration" } - } - - fun memberConcepts() = readAction { - rootNode.allChildren.filter { it.getContainmentLink()?.getSimpleName() == "member" }.map { it.concept?.getShortName() } + classNode = model.rootNodes.first().let { MPSNode(it) } } - assertEquals(listOf("InstanceMethodDeclaration"), memberConcepts()) - val incrementalEngine = IncrementalEngine() - val editorEngine = EditorEngine(incrementalEngine) - val mpsIntegration = EditorIntegrationForMPS(editorEngine) + incrementalEngine = IncrementalEngine() + editorEngine = EditorEngine(incrementalEngine) + mpsIntegration = EditorIntegrationForMPS(editorEngine) mpsIntegration.init(mpsProject.repository) + editor = editorEngine.editNode(classNode) + } + + fun assertEditorText(expected: String) { + assertEquals(expected.trimIndent(), editor.getRootCell().layout.toString()) + } + + fun placeCaretAtEnd(node: INode) { + val cell = editor.resolveCell(NodeCellReference(node.reference)).first() + val lastLeafCell = cell.lastLeaf() + editor.changeSelection(CaretSelection(lastLeafCell.layoutable()!!, lastLeafCell.getMaxCaretPos())) + } - val editor = editorEngine.editNode(rootNode) - fun assertEditorText(expected: String) { - assertEquals(expected, editor.getRootCell().layout.toString()) + fun placeCaretIntoCellWithText(text: String) { + val cell = editor.getRootCell().descendantsAndSelf().first { it.getVisibleText() == text } + editor.changeSelection(CaretSelection(cell.layoutable()!!, cell.getMaxCaretPos())) + } + + fun pressEnter() { + editor.processKeyEvent(JSKeyboardEvent(JSKeyboardEventType.KEYDOWN, KnownKeys.Enter)) + } + + fun typeText(text: CharSequence) { + for (c in text) { + 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 matchingActions = actions.filter { + val matchingText = it.getMatchingText() + matchingText.isNotEmpty() && matchingText.startsWith(pattern) + } + val shadowedActions = matchingActions.applyShadowing() + val sortedActions = shadowedActions.sortedBy { it.getMatchingText().lowercase() } + sortedActions + } + } + fun `test initial editor`() { assertEditorText( """ class Class1 { @@ -51,23 +104,40 @@ class BaseLanguageTests : TestBase("SimpleProject") { } """.trimIndent(), ) + } - val firstMethodCell = editor.resolveCell(NodeCellReference(firstMethod.reference)).first() - val lastLeafCell = firstMethodCell.lastLeaf() - assertEquals("}", lastLeafCell.getVisibleText()) - - editor.changeSelection(CaretSelection(lastLeafCell.layoutable()!!, -1)) - editor.processKeyEvent(JSKeyboardEvent(JSKeyboardEventType.KEYDOWN, KnownKeys.Enter)) + fun `test inserting new line into class`() { + val lastMember = readAction { classNode.allChildren.last { it.getContainmentLink()?.getSimpleName() == "member" } } + placeCaretAtEnd(lastMember) + pressEnter() + assertEditorText( + """ + class Class1 { + public void method1() { + + } + + } + """, + ) + } + fun `test creating LocalVariableDeclarationStatement by typing a type`() { + placeCaretIntoCellWithText("") + val actions = getCodeCompletionEntries("int") + assertEquals( + "int | LocalVariableDeclarationStatement[LocalVariableDeclaration[IntegerType]]", + actions.joinToString("\n") { it.getMatchingText() + " | " + it.getDescription() }, + ) + typeText("int") assertEditorText( """ - class Class1 { - public void method1() { - - } - - } - """.trimIndent(), + class Class1 { + public void method1() { + int ; + } + } + """, ) } } diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTemplate.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTemplate.kt index ac7d0ed4..2ad0d2ea 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTemplate.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTemplate.kt @@ -201,6 +201,26 @@ class InstantiateNodeCompletionAction( .getBestSelection(editor) } } + + override fun shadowedBy(shadowing: ICodeCompletionAction): Boolean { + return 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 + } + } + + override fun shadows(shadowed: ICodeCompletionAction): Boolean { + return 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 + } + } } /** 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 7aa6adfc..8d7a1509 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CodeCompletionMenu.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CodeCompletionMenu.kt @@ -216,6 +216,14 @@ class CodeCompletionActionWithPostprocessor(val action: ICodeCompletionAction, v action.execute(editor) after() } + + override fun shadowedBy(shadowing: ICodeCompletionAction): Boolean { + return action.shadowedBy(if (shadowing is CodeCompletionActionWithPostprocessor) shadowing.action else shadowing) + } + + override fun shadows(shadowed: ICodeCompletionAction): Boolean { + return action.shadows(if (shadowed is CodeCompletionActionWithPostprocessor) shadowed.action else shadowed) + } } class CodeCompletionActionProviderWithPostprocessor( val actionProvider: ICodeCompletionActionProvider, 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 c2128170..fbe53556 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/INonExistingNode.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/INonExistingNode.kt @@ -17,6 +17,11 @@ interface INonExistingNode { fun getOrCreateNode(subConcept: IConcept? = null): INode fun getNode(): INode? fun expectedConcept(): IConcept? + + /** + * How many levels of nodes are created by getOrCreateNode(). + */ + fun nodeCreationDepth(): Int } fun INonExistingNode.ancestors(includeSelf: Boolean = false): Sequence { @@ -53,6 +58,8 @@ data class NodeReplacement(val nodeToReplace: INonExistingNode, val replacementC return replaceNode(subConcept) } + override fun nodeCreationDepth(): Int = nodeToReplace.nodeCreationDepth().coerceAtLeast(1) + override fun getNode(): INode? { return null } @@ -100,6 +107,8 @@ data class ExistingNode(private val node: INode) : INonExistingNode { } } + override fun nodeCreationDepth(): Int = 0 + override fun getNode(): INode { return node } @@ -130,6 +139,8 @@ data class NonExistingChild(private val parent: INonExistingNode, val link: IChi return replaceNode(subConcept) } + override fun nodeCreationDepth(): Int = parent.nodeCreationDepth() + 1 + override fun getNode(): INode? { return null } @@ -156,6 +167,8 @@ data class NonExistingNode(val concept: IConcept) : INonExistingNode { return replaceNode(subConcept) } + override fun nodeCreationDepth(): Int = 0 + override fun getNode(): INode? { return null } From ed2b23018838e720b210b3edd52af76439b56a4b Mon Sep 17 00:00:00 2001 From: Sascha Lisson Date: Thu, 2 May 2024 16:05:44 +0200 Subject: [PATCH 07/18] chore: check range of caret position in CaretSelection --- .../commonMain/kotlin/org/modelix/editor/CaretSelection.kt | 5 +++++ 1 file changed, 5 insertions(+) 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 bdcb9655..e5d3ddd1 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CaretSelection.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CaretSelection.kt @@ -7,6 +7,11 @@ class CaretSelection(val layoutable: LayoutableCell, val start: Int, val end: In constructor(cell: LayoutableCell, pos: Int) : this(cell, pos, pos) constructor(cell: LayoutableCell, pos: Int, desiredXPosition: Int?) : this(cell, pos, pos, desiredXPosition) + init { + require(start >= 0) { "invalid start: $start" } + require(end >= 0) { "invalid end: $start" } + } + override fun isValid(): Boolean { val editor = getEditor() ?: return false val visibleText = editor.getRootCell().layout From 75e6df38b7d09779f1ae65a3e226ed114c6775d7 Mon Sep 17 00:00:00 2001 From: Sascha Lisson Date: Fri, 3 May 2024 14:08:12 +0200 Subject: [PATCH 08/18] feat: side transformations for adding an optional part --- .../editor/ssr/mps/BaseLanguageTests.kt | 39 +++++- .../kotlin/org/modelix/editor/CellTemplate.kt | 111 +++++++++++++++--- .../editor/CodeCompletionActionWrapper.kt | 55 +++++++++ .../org/modelix/editor/CodeCompletionMenu.kt | 44 ++----- .../kotlin/org/modelix/editor/EditorState.kt | 2 + 5 files changed, 199 insertions(+), 52 deletions(-) create mode 100644 projectional-editor/src/commonMain/kotlin/org/modelix/editor/CodeCompletionActionWrapper.kt 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 0dab883d..8aafebe7 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 @@ -63,8 +63,10 @@ class BaseLanguageTests : TestBase("SimpleProject") { editor.changeSelection(CaretSelection(cell.layoutable()!!, cell.getMaxCaretPos())) } - fun pressEnter() { - editor.processKeyEvent(JSKeyboardEvent(JSKeyboardEventType.KEYDOWN, KnownKeys.Enter)) + fun pressEnter() = pressKey(KnownKeys.Enter) + + fun pressKey(key: KnownKeys) { + editor.processKeyEvent(JSKeyboardEvent(JSKeyboardEventType.KEYDOWN, key)) } fun typeText(text: CharSequence) { @@ -140,4 +142,37 @@ class BaseLanguageTests : TestBase("SimpleProject") { """, ) } + + fun `test naming LocalVariableDeclaration`() { + placeCaretIntoCellWithText("") + typeText("int") + pressKey(KnownKeys.Tab) + typeText("abc") + assertEditorText( + """ + class Class1 { + public void method1() { + int abc; + } + } + """, + ) + } + + fun `test adding initializer to LocalVariableDeclaration`() { + placeCaretIntoCellWithText("") + typeText("int") + pressKey(KnownKeys.Tab) + typeText("abc") + typeText("=") + assertEditorText( + """ + class Class1 { + public void method1() { + int abc = ; + } + } + """, + ) + } } diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTemplate.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTemplate.kt index 2ad0d2ea..7257758a 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTemplate.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTemplate.kt @@ -9,6 +9,7 @@ import org.modelix.model.api.INode import org.modelix.model.api.INodeReference import org.modelix.model.api.IProperty import org.modelix.model.api.IReferenceLink +import org.modelix.model.api.remove import org.modelix.scopes.ScopeAspect import kotlin.jvm.JvmName @@ -110,6 +111,13 @@ interface IGrammarSymbol { fun createWrapperAction(nodeToWrap: INode, wrappingLink: IChildLink): List { return emptyList() } + + fun getSymbolTransformationAction(node: INode, optionalCell: TemplateCellReference): IActionOrProvider? +} + +interface IGrammarConditionSymbol : IGrammarSymbol { + fun getSymbolConditionState(node: INode): Boolean + fun setSymbolConditionFalse(node: INode) } class OverrideText(val cell: TextCellData, val delegate: ITextChangeAction?) : ITextChangeAction { @@ -144,6 +152,10 @@ class ConstantCellTemplate(concept: IConcept, val text: String) : return listOf(SideTransformWrapper(nodeToWrap.toNonExisting(), wrappingLink)) } + override fun getSymbolTransformationAction(node: INode, optionalCell: TemplateCellReference): IActionOrProvider? { + return ForceShowOptionalCellAction(optionalCell).withMatchingText(text) + } + inner class SideTransformWrapper(val nodeToWrap: INonExistingNode, val wrappingLink: IChildLink) : ICodeCompletionAction { override fun getMatchingText(): String = text override fun getDescription(): String = concept.getShortName() @@ -271,12 +283,25 @@ class OptionalCellTemplate(concept: IConcept) : } override fun applyChildren(context: CellCreationContext, node: INode, cell: CellData): List { - // TODO support other cell types as condition for the optional - val childLinkCell = descendants().filterIsInstance().firstOrNull() - if (childLinkCell == null || childLinkCell.getChildNodes(node).isNotEmpty()) { + val forceShow = context.editorState.forceShowOptionals[createCellReference(node)] == true + + val symbols = getChildren().asSequence().flatMap { it.getGrammarSymbols() } + val conditionSymbol = symbols.filterIsInstance().firstOrNull() + val transformationSymbol = symbols.firstOrNull() + + if (conditionSymbol == null) return emptyList() + if (forceShow || conditionSymbol.getSymbolConditionState(node)) { return super.applyChildren(context, node, cell) } else { - return emptyList() + if (transformationSymbol == null) return emptyList() + val symbolTransformationAction = transformationSymbol.getSymbolTransformationAction(node, createCellReference(node)) + if (symbolTransformationAction != null) { + val sideTransformCell = CellData() + sideTransformCell.properties[CellActionProperties.transformBefore] = symbolTransformationAction.asProvider() + return listOf(sideTransformCell) + } else { + return emptyList() + } } } @@ -289,8 +314,22 @@ class OptionalCellTemplate(concept: IConcept) : } } +class ForceShowOptionalCellAction(val cell: TemplateCellReference) : ICodeCompletionAction { + override fun execute(editor: EditorComponent) { + editor.state.forceShowOptionals[cell] = true + } + + override fun getMatchingText(): String { + return "" + } + + override fun getDescription(): String { + return "Add optional part" + } +} + open class PropertyCellTemplate(concept: IConcept, val property: IProperty) : - CellTemplate(concept), IGrammarSymbol { + CellTemplate(concept), IGrammarConditionSymbol { var placeholderText: String = "" var validator: ((String) -> Boolean)? = null override fun createCell(context: CellCreationContext, node: INode): CellData { @@ -301,10 +340,28 @@ open class PropertyCellTemplate(concept: IConcept, val property: IProperty) : data.cellReferences += PropertyCellReference(property, node.reference) return data } + override fun getInstantiationActions(location: INonExistingNode, parameters: CodeCompletionParameters): List? { return listOf(WrapPropertyValueProvider(location)) } + private fun validateValue(node: INonExistingNode, value: String): Boolean { + return validator?.invoke(value) + ?: ConstraintsAspect.checkPropertyValue(node, property, value).isEmpty() + } + + override fun getSymbolConditionState(node: INode): Boolean { + return node.getPropertyValue(property) != null + } + + override fun setSymbolConditionFalse(node: INode) { + return node.setPropertyValue(property, null) + } + + override fun getSymbolTransformationAction(node: INode, optionalCell: TemplateCellReference): IActionOrProvider? { + return WrapPropertyValueProvider(node.toNonExisting()) + } + inner class WrapPropertyValueProvider(val location: INonExistingNode) : ICodeCompletionActionProvider { override fun getApplicableActions(parameters: CodeCompletionParameters): List { return if (validateValue(location.replacement(concept), parameters.pattern)) { @@ -315,11 +372,6 @@ open class PropertyCellTemplate(concept: IConcept, val property: IProperty) : } } - private fun validateValue(node: INonExistingNode, value: String): Boolean { - return validator?.invoke(value) - ?: ConstraintsAspect.checkPropertyValue(node, property, value).isEmpty() - } - inner class WrapPropertyValue(val location: INonExistingNode, val value: String) : ICodeCompletionAction { override fun getMatchingText(): String { return value @@ -374,15 +426,24 @@ class ReferenceCellTemplate( return sourceNode.getReferenceTarget(link) } override fun getInstantiationActions(location: INonExistingNode, parameters: CodeCompletionParameters): List { - val sourceNode = location.replacement(concept) - 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 -> "" + return listOf(WrapReferenceTargetProvider(location.replacement(concept))) + } + + override fun getSymbolTransformationAction(node: INode, optionalCell: TemplateCellReference): IActionOrProvider? { + return WrapReferenceTargetProvider(node.toNonExisting()) + } + + 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 -> "" + } + WrapReferenceTarget(sourceNode, target, text) } - WrapReferenceTarget(location, target, text) } } @@ -420,7 +481,7 @@ class FlagCellTemplate( class ChildCellTemplate( concept: IConcept, val link: IChildLink, -) : CellTemplate(concept), IGrammarSymbol { +) : CellTemplate(concept), IGrammarConditionSymbol { private var separatorCell: CellTemplate? = null @@ -524,6 +585,18 @@ class ChildCellTemplate( val childNode = NonExistingChild(location.replacement(concept), link) return listOf(ReplaceNodeActionProvider(childNode)) } + + override fun getSymbolConditionState(node: INode): Boolean { + return 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)) + } } data class PlaceholderCellReference(val childCellRef: TemplateCellReference) : CellReference() diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CodeCompletionActionWrapper.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CodeCompletionActionWrapper.kt new file mode 100644 index 00000000..cd709853 --- /dev/null +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CodeCompletionActionWrapper.kt @@ -0,0 +1,55 @@ +package org.modelix.editor + +open class CodeCompletionActionWrapper(val wrappedAction: ICodeCompletionAction) : ICodeCompletionAction by wrappedAction { + override fun shadowedBy(shadowing: ICodeCompletionAction): Boolean { + return wrappedAction.shadowedBy(if (shadowing is CodeCompletionActionWrapper) shadowing.wrappedAction else shadowing) + } + + override fun shadows(shadowed: ICodeCompletionAction): Boolean { + return 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 { + when (it) { + is ICodeCompletionAction -> wrapAction(parameters, it) + is ICodeCompletionActionProvider -> CodeCompletionActionProviderWrapper(it, wrapAction) + else -> throw RuntimeException("Unexpected type: " + it::class) + } + } + } +} + +class CodeCompletionActionWithPostprocessor(action: ICodeCompletionAction, val after: () -> Unit) : CodeCompletionActionWrapper(action) { + override fun execute(editor: EditorComponent) { + wrappedAction.execute(editor) + after() + } +} + +class CodeCompletionActionWithMatchingText(action: ICodeCompletionAction, val overridingMatchingText: String) : CodeCompletionActionWrapper(action) { + override fun getMatchingText(): String { + return overridingMatchingText + } +} + +fun ICodeCompletionActionProvider.after(body: () -> Unit): CodeCompletionActionProviderWrapper { + return CodeCompletionActionProviderWrapper(this) { _, it -> + CodeCompletionActionWithPostprocessor(it, body) + } +} + +fun ICodeCompletionActionProvider.withMatchingText(text: (CodeCompletionParameters) -> String): CodeCompletionActionProviderWrapper { + return CodeCompletionActionProviderWrapper(this) { parameters, it -> + CodeCompletionActionWithMatchingText(it, text(parameters)) + } +} + +fun ICodeCompletionAction.withMatchingText(text: String): CodeCompletionActionWithMatchingText { + return CodeCompletionActionWithMatchingText(this, text) +} 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 8d7a1509..aa5ca429 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CodeCompletionMenu.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CodeCompletionMenu.kt @@ -197,6 +197,19 @@ fun ICodeCompletionActionProvider.flattenApplicableActions(parameters: CodeCompl return flatten(parameters).toList() } +class ActionAsProvider(val action: ICodeCompletionAction) : ICodeCompletionActionProvider { + override fun getApplicableActions(parameters: CodeCompletionParameters): List { + return 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) } @@ -211,37 +224,6 @@ interface ICodeCompletionAction : IActionOrProvider { fun shadowedBy(shadowing: ICodeCompletionAction) = false } -class CodeCompletionActionWithPostprocessor(val action: ICodeCompletionAction, val after: () -> Unit) : ICodeCompletionAction by action { - override fun execute(editor: EditorComponent) { - action.execute(editor) - after() - } - - override fun shadowedBy(shadowing: ICodeCompletionAction): Boolean { - return action.shadowedBy(if (shadowing is CodeCompletionActionWithPostprocessor) shadowing.action else shadowing) - } - - override fun shadows(shadowed: ICodeCompletionAction): Boolean { - return action.shadows(if (shadowed is CodeCompletionActionWithPostprocessor) shadowed.action else shadowed) - } -} -class CodeCompletionActionProviderWithPostprocessor( - val actionProvider: ICodeCompletionActionProvider, - val after: () -> Unit, -) : ICodeCompletionActionProvider { - override fun getApplicableActions(parameters: CodeCompletionParameters): List { - return actionProvider.getApplicableActions(parameters).map { - when (it) { - is ICodeCompletionAction -> CodeCompletionActionWithPostprocessor(it, after) - is ICodeCompletionActionProvider -> CodeCompletionActionProviderWithPostprocessor(it, after) - else -> throw RuntimeException("Unexpected type: " + it::class) - } - } - } -} - -fun ICodeCompletionActionProvider.after(body: () -> Unit) = CodeCompletionActionProviderWithPostprocessor(this, body) - class CodeCompletionParameters(val editor: EditorComponent, pattern: String) { val pattern: String = pattern get() { diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/EditorState.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/EditorState.kt index 8cc083de..535caf64 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/EditorState.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/EditorState.kt @@ -4,10 +4,12 @@ import org.modelix.incremental.TrackableMap class EditorState { val substitutionPlaceholderPositions = TrackableMap() + val forceShowOptionals = TrackableMap() val textReplacements = TrackableMap() fun reset() { substitutionPlaceholderPositions.clear() + forceShowOptionals.clear() textReplacements.clear() } } From bb571be6e31576ba3360ef7da4c4af6b4eeefbe9 Mon Sep 17 00:00:00 2001 From: Sascha Lisson Date: Fri, 3 May 2024 14:43:48 +0200 Subject: [PATCH 09/18] fix: caret position after adding an optional part --- .../editor/ssr/mps/BaseLanguageTests.kt | 20 +++++++++- .../org/modelix/editor/CaretSelection.kt | 6 ++- .../kotlin/org/modelix/editor/CellTemplate.kt | 38 ++++++++----------- .../editor/CodeCompletionActionWrapper.kt | 21 +++++++++- .../org/modelix/editor/CodeCompletionMenu.kt | 13 +++++-- .../editor/ReplaceNodeActionProvider.kt | 7 +--- 6 files changed, 69 insertions(+), 36 deletions(-) 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 8aafebe7..559f6260 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 @@ -159,7 +159,7 @@ class BaseLanguageTests : TestBase("SimpleProject") { ) } - fun `test adding initializer to LocalVariableDeclaration`() { + fun `test showing initializer of LocalVariableDeclaration`() { placeCaretIntoCellWithText("") typeText("int") pressKey(KnownKeys.Tab) @@ -175,4 +175,22 @@ class BaseLanguageTests : TestBase("SimpleProject") { """, ) } + + fun `test adding initializer to LocalVariableDeclaration`() { + placeCaretIntoCellWithText("") + typeText("int") + pressKey(KnownKeys.Tab) + typeText("abc") + typeText("=") + typeText("10") + assertEditorText( + """ + class Class1 { + public void method1() { + int abc = 10; + } + } + """, + ) + } } 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 e5d3ddd1..e2464e58 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CaretSelection.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CaretSelection.kt @@ -177,7 +177,7 @@ class CaretSelection(val layoutable: LayoutableCell, val start: Int, val end: In } if (matchingActions.isNotEmpty()) { if (matchingActions.size == 1 && matchingActions.first().getMatchingText() == typedText) { - matchingActions.first().execute(editor) + matchingActions.first().executeAndUpdateSelection(editor) return } editor.showCodeCompletionMenu( @@ -222,7 +222,9 @@ class CaretSelection(val layoutable: LayoutableCell, val start: Int, val end: In .applyShadowing() val singleAction = matchingActions.singleOrNull() if (singleAction != null) { - singleAction.execute(editor) + editor.runWrite { + singleAction.executeAndUpdateSelection(editor) + } return true } diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTemplate.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTemplate.kt index 7257758a..07b6c10f 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTemplate.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTemplate.kt @@ -153,21 +153,20 @@ class ConstantCellTemplate(concept: IConcept, val text: String) : } override fun getSymbolTransformationAction(node: INode, optionalCell: TemplateCellReference): IActionOrProvider? { - return ForceShowOptionalCellAction(optionalCell).withMatchingText(text) + return ForceShowOptionalCellAction(optionalCell) + .withCaretPolicy { it?.avoid(createCellReference(node)) } + .withMatchingText(text) } inner class SideTransformWrapper(val nodeToWrap: INonExistingNode, val wrappingLink: IChildLink) : ICodeCompletionAction { override fun getMatchingText(): String = text override fun getDescription(): String = concept.getShortName() - override fun execute(editor: EditorComponent) { + override fun execute(editor: EditorComponent): CaretPositionPolicy? { val wrapper = nodeToWrap.getParent()!!.getOrCreateNode(null).addNewChild(nodeToWrap.getContainmentLink()!!, nodeToWrap.index(), concept) wrapper.moveChild(wrappingLink, 0, nodeToWrap.getOrCreateNode(null)) - editor.selectAfterUpdate { - CaretPositionPolicy(wrapper) - .avoid(ChildNodeCellReference(wrapper.reference, wrappingLink)) - .avoid(createCellReference(wrapper)) - .getBestSelection(editor) - } + return CaretPositionPolicy(wrapper) + .avoid(ChildNodeCellReference(wrapper.reference, wrappingLink)) + .avoid(createCellReference(wrapper)) } override fun shadows(shadowed: ICodeCompletionAction): Boolean { @@ -204,14 +203,11 @@ class InstantiateNodeCompletionAction( override fun getDescription(): String = description - override fun execute(editor: EditorComponent) { + override fun execute(editor: EditorComponent): CaretPositionPolicy? { val newNode = location.getExistingAncestor()!!.getArea().executeWrite { location.replaceNode(concept) } - editor.selectAfterUpdate { - CaretPositionPolicy(newNode) - .getBestSelection(editor) - } + return CaretPositionPolicy(newNode) } override fun shadowedBy(shadowing: ICodeCompletionAction): Boolean { @@ -315,8 +311,9 @@ class OptionalCellTemplate(concept: IConcept) : } class ForceShowOptionalCellAction(val cell: TemplateCellReference) : ICodeCompletionAction { - override fun execute(editor: EditorComponent) { + override fun execute(editor: EditorComponent): CaretPositionPolicy? { editor.state.forceShowOptionals[cell] = true + return CaretPositionPolicy(cell) } override fun getMatchingText(): String { @@ -381,13 +378,10 @@ open class PropertyCellTemplate(concept: IConcept, val property: IProperty) : return concept.getShortName() } - override fun execute(editor: EditorComponent) { + override fun execute(editor: EditorComponent): CaretPositionPolicy? { val node = location.getOrCreateNode(concept) node.setPropertyValue(property, value) - editor.selectAfterUpdate { - CaretPositionPolicy(createCellReference(node)) - .getBestSelection(editor) - } + return CaretPositionPolicy(createCellReference(node)) } } @@ -456,12 +450,10 @@ class ReferenceCellTemplate( return concept.getShortName() } - override fun execute(editor: EditorComponent) { + override fun execute(editor: EditorComponent): CaretPositionPolicy? { val sourceNode = location.getOrCreateNode(concept) sourceNode.setReferenceTarget(link, target.getOrCreateNode()) - editor.selectAfterUpdate { - CaretPositionPolicy(createCellReference(sourceNode)).getBestSelection(editor) - } + return CaretPositionPolicy(createCellReference(sourceNode)) } } } 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 cd709853..b28a2170 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CodeCompletionActionWrapper.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CodeCompletionActionWrapper.kt @@ -26,9 +26,16 @@ class CodeCompletionActionProviderWrapper( } class CodeCompletionActionWithPostprocessor(action: ICodeCompletionAction, val after: () -> Unit) : CodeCompletionActionWrapper(action) { - override fun execute(editor: EditorComponent) { - wrappedAction.execute(editor) + override fun execute(editor: EditorComponent): CaretPositionPolicy? { + val policy = wrappedAction.execute(editor) after() + return policy + } +} + +class CodeCompletionActionWithCaretPolicy(action: ICodeCompletionAction, val policy: (CaretPositionPolicy?) -> CaretPositionPolicy?) : CodeCompletionActionWrapper(action) { + override fun execute(editor: EditorComponent): CaretPositionPolicy? { + return policy(wrappedAction.execute(editor)) } } @@ -53,3 +60,13 @@ fun ICodeCompletionActionProvider.withMatchingText(text: (CodeCompletionParamete fun ICodeCompletionAction.withMatchingText(text: String): CodeCompletionActionWithMatchingText { return CodeCompletionActionWithMatchingText(this, text) } + +fun ICodeCompletionAction.withCaretPolicy(policy: (CaretPositionPolicy?) -> CaretPositionPolicy?): CodeCompletionActionWithCaretPolicy { + return CodeCompletionActionWithCaretPolicy(this, policy) +} + +fun ICodeCompletionActionProvider.withCaretPolicy(policy: (CaretPositionPolicy?) -> CaretPositionPolicy?): CodeCompletionActionProviderWrapper { + return 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 aa5ca429..b9cbf458 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CodeCompletionMenu.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CodeCompletionMenu.kt @@ -58,7 +58,7 @@ class CodeCompletionMenu( KnownKeys.Enter -> { getSelectedEntry()?.let { entry -> editor.runWrite { - entry.execute(editor) + entry.executeAndUpdateSelection(editor) } } editor.closeCodeCompletionMenu() @@ -107,7 +107,7 @@ class CodeCompletionMenu( fun executeIfSingleAction() { if (entries.size == 1 && entries.first().getMatchingText() == patternEditor.pattern) { - entries.first().execute(editor) + entries.first().executeAndUpdateSelection(editor) editor.closeCodeCompletionMenu() } } @@ -219,11 +219,18 @@ private fun IActionOrProvider.flatten(parameters: CodeCompletionParameters): Seq interface ICodeCompletionAction : IActionOrProvider { fun getMatchingText(): String fun getDescription(): String - fun execute(editor: EditorComponent) + fun execute(editor: EditorComponent): CaretPositionPolicy? fun shadows(shadowed: ICodeCompletionAction) = false fun shadowedBy(shadowing: ICodeCompletionAction) = false } +fun ICodeCompletionAction.executeAndUpdateSelection(editor: EditorComponent) { + val policy = execute(editor) + if (policy != null) { + editor.selectAfterUpdate { policy.getBestSelection(editor) } + } +} + class CodeCompletionParameters(val editor: EditorComponent, pattern: String) { val pattern: String = pattern get() { 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 88b081da..a1d0ea68 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/ReplaceNodeActionProvider.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/ReplaceNodeActionProvider.kt @@ -43,12 +43,9 @@ class ChangeReferenceTargetAction(val sourceLocation: INonExistingNode, val link return "set reference '" + link.getSimpleName() + "'" } - override fun execute(editor: EditorComponent) { + override fun execute(editor: EditorComponent): CaretPositionPolicy? { val sourceNode = sourceLocation.getOrCreateNode(null) sourceNode.setReferenceTarget(link, targetNode.getOrCreateNode()) - editor.selectAfterUpdate { - CaretPositionPolicy(ReferencedNodeCellReference(sourceNode.reference, link)) - .getBestSelection(editor) - } + return CaretPositionPolicy(ReferencedNodeCellReference(sourceNode.reference, link)) } } From f2961c2c87259be9e4b257faab18f8c019278a2a Mon Sep 17 00:00:00 2001 From: Sascha Lisson Date: Fri, 3 May 2024 20:01:55 +0200 Subject: [PATCH 10/18] feat: node can be added to a list by typing the separator --- .../editor/ssr/mps/BaseLanguageTests.kt | 95 ++++++++++++++----- .../org/modelix/editor/CellReference.kt | 2 +- .../kotlin/org/modelix/editor/CellTemplate.kt | 52 ++++++++-- 3 files changed, 117 insertions(+), 32 deletions(-) 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 559f6260..d3ad17b5 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 @@ -25,6 +25,7 @@ import org.modelix.model.mpsadapters.MPSNode /** * Test editor for MPS baseLanguage ClassConcept */ +@Suppress("ktlint:standard:wrapping", "ktlint:standard:trailing-comma-on-call-site") class BaseLanguageTests : TestBase("SimpleProject") { lateinit var editor: EditorComponent lateinit var mpsIntegration: EditorIntegrationForMPS @@ -48,6 +49,21 @@ class BaseLanguageTests : TestBase("SimpleProject") { editor = editorEngine.editNode(classNode) } + override fun tearDown() { + editor.dispose() + mpsIntegration.dispose() + editorEngine.dispose() + incrementalEngine.dispose() + super.tearDown() + } + + fun assertFinalEditorText(expected: String) { + assertEditorText(expected) + editor.state.reset() + editor.update() + assertEditorText(expected) + } + fun assertEditorText(expected: String) { assertEquals(expected.trimIndent(), editor.getRootCell().layout.toString()) } @@ -97,31 +113,27 @@ class BaseLanguageTests : TestBase("SimpleProject") { } fun `test initial editor`() { - assertEditorText( - """ + assertFinalEditorText(""" class Class1 { public void method1() { } } - """.trimIndent(), - ) + """) } fun `test inserting new line into class`() { val lastMember = readAction { classNode.allChildren.last { it.getContainmentLink()?.getSimpleName() == "member" } } placeCaretAtEnd(lastMember) pressEnter() - assertEditorText( - """ + assertFinalEditorText(""" class Class1 { public void method1() { } } - """, - ) + """) } fun `test creating LocalVariableDeclarationStatement by typing a type`() { @@ -132,15 +144,13 @@ class BaseLanguageTests : TestBase("SimpleProject") { actions.joinToString("\n") { it.getMatchingText() + " | " + it.getDescription() }, ) typeText("int") - assertEditorText( - """ + assertFinalEditorText(""" class Class1 { public void method1() { int ; } } - """, - ) + """) } fun `test naming LocalVariableDeclaration`() { @@ -148,15 +158,13 @@ class BaseLanguageTests : TestBase("SimpleProject") { typeText("int") pressKey(KnownKeys.Tab) typeText("abc") - assertEditorText( - """ + assertFinalEditorText(""" class Class1 { public void method1() { int abc; } } - """, - ) + """) } fun `test showing initializer of LocalVariableDeclaration`() { @@ -165,15 +173,13 @@ class BaseLanguageTests : TestBase("SimpleProject") { pressKey(KnownKeys.Tab) typeText("abc") typeText("=") - assertEditorText( - """ + assertEditorText(""" class Class1 { public void method1() { int abc = ; } } - """, - ) + """) } fun `test adding initializer to LocalVariableDeclaration`() { @@ -183,14 +189,55 @@ class BaseLanguageTests : TestBase("SimpleProject") { typeText("abc") typeText("=") typeText("10") - assertEditorText( - """ + assertFinalEditorText(""" 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(""" + class Class1 { + public void method1(int p1, ) { + + } + } + """) + typeText("string") + pressKey(KnownKeys.Tab) + typeText("p2") + assertFinalEditorText(""" + class Class1 { + public void method1(int p1, string p2) { + + } + } + """) + } + + fun `test adding second parameter to InstanceMethodDeclaration by typing separator`() { + placeCaretIntoCellWithText("") + typeText("int") + pressKey(KnownKeys.Tab) + typeText("p1") + typeText(",") + typeText("int") + pressKey(KnownKeys.Tab) + typeText("p2") + assertFinalEditorText(""" + class Class1 { + public void method1(int p1, int p2) { + + } + } + """) } } 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 d7cb590b..6e6cfc8f 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellReference.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellReference.kt @@ -41,5 +41,5 @@ fun EditorComponent.resolveNodeCell(node: ITypedNode): Cell? = resolveNodeCell(node.untypedReference()) data class ChildNodeCellReference(val parentNodeRef: INodeReference, val link: IChildLink, val index: Int = 0) : CellReference() -data class SeparatorCellReference(val before: ChildNodeCellReference) : CellReference() +data class SeparatorCellReference(val before: CellReference) : CellReference() data class ReferencedNodeCellReference(val sourceNodeRef: INodeReference, val link: IReferenceLink) : CellReference() diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTemplate.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTemplate.kt index 07b6c10f..d2d7fa63 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTemplate.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTemplate.kt @@ -520,9 +520,29 @@ class ChildCellTemplate( InstantiateNodeCellAction(NonExistingChild(ExistingNode(node), link, index), it) } ?: InsertSubstitutionPlaceholderAction(context.editorState, createCellReference(node), index) actionCell.properties[CellActionProperties.insert] = action + + val separatorText = separatorCell?.getGrammarSymbols()?.filterIsInstance() + ?.firstOrNull()?.text + if (separatorText != null) { + actionCell.properties[CellActionProperties.transformBefore] = InsertSubstitutionPlaceholderCompletionAction( + index, + separatorText, + createCellReference(node), + ).asProvider() + } + cell.addChild(actionCell) } } + fun addSeparator(before: CellReference) { + separatorCell?.let { + cell.addChild( + it.apply(context, node).also { + it.cellReferences += SeparatorCellReference(before) + }, + ) + } + } if (childNodes.isEmpty()) { addSubstitutionPlaceholder(0) } else { @@ -530,17 +550,12 @@ class ChildCellTemplate( childCells.forEachIndexed { index, child -> val childCellReference = ChildNodeCellReference(node.reference, link, index) if (index != 0) { - separatorCell?.let { - cell.addChild( - it.apply(context, node).also { - it.cellReferences += SeparatorCellReference(childCellReference) - }, - ) - } + addSeparator(childCellReference) } if (substitutionPlaceholder != null && placeholderIndex == index) { addSubstitutionPlaceholder(placeholderIndex) + addSeparator(PlaceholderCellReference(createCellReference(node))) } else { addInsertActionCell(index) } @@ -552,6 +567,9 @@ class ChildCellTemplate( cell.addChild(wrapper) } if (substitutionPlaceholder != null && placeholderIndex == childNodes.size) { + if (childCells.isNotEmpty()) { + addSeparator(PlaceholderCellReference(createCellReference(node))) + } addSubstitutionPlaceholder(placeholderIndex) } else { addInsertActionCell(childNodes.size) @@ -589,6 +607,26 @@ class ChildCellTemplate( override fun getSymbolTransformationAction(node: INode, optionalCell: TemplateCellReference): IActionOrProvider? { return 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 getMatchingText(): String { + return separatorText + } + + override fun execute(editor: EditorComponent): CaretPositionPolicy? { + editor.state.substitutionPlaceholderPositions[ref] = SubstitutionPlaceholderPosition(index) + editor.state.textReplacements.remove(PlaceholderCellReference(ref)) + return CaretPositionPolicy(PlaceholderCellReference(ref)) + } + } } data class PlaceholderCellReference(val childCellRef: TemplateCellReference) : CellReference() From cad9140e10af9fdc145d61b8700a84142a3107b4 Mon Sep 17 00:00:00 2001 From: Sascha Lisson Date: Fri, 3 May 2024 20:33:31 +0200 Subject: [PATCH 11/18] feat: node can be added to a list by typing the separator --- .../editor/ssr/mps/BaseLanguageTests.kt | 29 +++++++++++++++++-- .../kotlin/org/modelix/editor/CellTemplate.kt | 27 ++++++++++------- 2 files changed, 42 insertions(+), 14 deletions(-) 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 d3ad17b5..d4210980 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 @@ -74,9 +74,9 @@ class BaseLanguageTests : TestBase("SimpleProject") { editor.changeSelection(CaretSelection(lastLeafCell.layoutable()!!, lastLeafCell.getMaxCaretPos())) } - fun placeCaretIntoCellWithText(text: String) { + fun placeCaretIntoCellWithText(text: String, position: Int = -1) { val cell = editor.getRootCell().descendantsAndSelf().first { it.getVisibleText() == text } - editor.changeSelection(CaretSelection(cell.layoutable()!!, cell.getMaxCaretPos())) + editor.changeSelection(CaretSelection(cell.layoutable()!!, if (position == -1) cell.getMaxCaretPos() else position)) } fun pressEnter() = pressKey(KnownKeys.Enter) @@ -223,7 +223,7 @@ class BaseLanguageTests : TestBase("SimpleProject") { """) } - fun `test adding second parameter to InstanceMethodDeclaration by typing separator`() { + fun `test adding second parameter to InstanceMethodDeclaration by typing separator after last`() { placeCaretIntoCellWithText("") typeText("int") pressKey(KnownKeys.Tab) @@ -240,4 +240,27 @@ class BaseLanguageTests : TestBase("SimpleProject") { } """) } + + 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(""" + class Class1 { + public void method1(int p1, int p3, int p2) { + + } + } + """) + } } diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTemplate.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTemplate.kt index d2d7fa63..3ac5c311 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTemplate.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTemplate.kt @@ -520,17 +520,6 @@ class ChildCellTemplate( InstantiateNodeCellAction(NonExistingChild(ExistingNode(node), link, index), it) } ?: InsertSubstitutionPlaceholderAction(context.editorState, createCellReference(node), index) actionCell.properties[CellActionProperties.insert] = action - - val separatorText = separatorCell?.getGrammarSymbols()?.filterIsInstance() - ?.firstOrNull()?.text - if (separatorText != null) { - actionCell.properties[CellActionProperties.transformBefore] = InsertSubstitutionPlaceholderCompletionAction( - index, - separatorText, - createCellReference(node), - ).asProvider() - } - cell.addChild(actionCell) } } @@ -546,6 +535,8 @@ class ChildCellTemplate( if (childNodes.isEmpty()) { addSubstitutionPlaceholder(0) } else { + val separatorText = separatorCell?.getGrammarSymbols()?.filterIsInstance() + ?.firstOrNull()?.text val childCells = childNodes.map { ChildDataReference(it) } childCells.forEachIndexed { index, child -> val childCellReference = ChildNodeCellReference(node.reference, link, index) @@ -564,6 +555,20 @@ class ChildCellTemplate( val wrapper = CellData() // allow setting properties by the parent, because the cell is already frozen wrapper.addChild(child) wrapper.cellReferences += childCellReference + + if (separatorText != null) { + wrapper.properties[CellActionProperties.transformBefore] = InsertSubstitutionPlaceholderCompletionAction( + index, + separatorText, + createCellReference(node), + ).asProvider() + wrapper.properties[CellActionProperties.transformAfter] = InsertSubstitutionPlaceholderCompletionAction( + index + 1, + separatorText, + createCellReference(node), + ).asProvider() + } + cell.addChild(wrapper) } if (substitutionPlaceholder != null && placeholderIndex == childNodes.size) { From d21af0a44f913fe527c495fd6a28d385676c64ad Mon Sep 17 00:00:00 2001 From: Sascha Lisson Date: Sat, 4 May 2024 09:36:19 +0200 Subject: [PATCH 12/18] fix: caret position wasn't always updated --- .../org/modelix/editor/ssr/client/ModelixSSRClient.kt | 8 ++++++++ 1 file changed, 8 insertions(+) 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 6045bb1c..c8c7c16d 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 @@ -159,6 +159,11 @@ class ModelixSSRClient(private val httpClient: HttpClient, private val url: Stri 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()) @@ -170,6 +175,7 @@ class ModelixSSRClient(private val httpClient: HttpClient, private val url: Stri } 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( @@ -190,6 +196,8 @@ class ModelixSSRClient(private val httpClient: HttpClient, private val url: Stri } } possiblyDetachedElements.clear() + + sendBoundsUpdate() } private fun updateNode(data: INodeUpdateData): Node { From 517da1405d01a4e952d0fff40a1729f293827423 Mon Sep 17 00:00:00 2001 From: Sascha Lisson Date: Tue, 14 May 2024 16:50:42 +0200 Subject: [PATCH 13/18] feat: delete handler for nodes and placeholders --- .../editor/ssr/mps/BaseLanguageTests.kt | 101 ++++++++++++++++++ .../org/modelix/editor/CaretSelection.kt | 8 ++ .../kotlin/org/modelix/editor/CellTemplate.kt | 7 ++ .../org/modelix/editor/CodeCompletionMenu.kt | 1 + .../kotlin/org/modelix/editor/EditorEngine.kt | 12 +++ .../kotlin/org/modelix/editor/EditorState.kt | 6 ++ .../kotlin/org/modelix/editor/ICellAction.kt | 1 + 7 files changed, 136 insertions(+) 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 d4210980..5811e2f1 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 @@ -64,6 +64,10 @@ class BaseLanguageTests : TestBase("SimpleProject") { assertEditorText(expected) } + fun assertCaretPosition(cellTextWithCaret: String) { + assertEquals(cellTextWithCaret, editor.getSelection()?.toString()) + } + fun assertEditorText(expected: String) { assertEquals(expected.trimIndent(), editor.getRootCell().layout.toString()) } @@ -239,6 +243,7 @@ class BaseLanguageTests : TestBase("SimpleProject") { } } """) + assertCaretPosition("p2|") } fun `test adding second parameter to InstanceMethodDeclaration by typing separator after first`() { @@ -262,5 +267,101 @@ class BaseLanguageTests : TestBase("SimpleProject") { } } """) + assertCaretPosition("p3|") + } + +/* fun `test adding second parameter to InstanceMethodDeclaration by typing separator before last`() { + placeCaretIntoCellWithText("") + typeText("int") + pressKey(KnownKeys.Tab) + typeText("p1") + typeText(",") + typeText("string") + pressKey(KnownKeys.Tab) + typeText("p2") + placeCaretIntoCellWithText("string", 0) + typeText(",") + typeText("long") + pressKey(KnownKeys.Tab) + typeText("p3") + assertFinalEditorText(""" + class Class1 { + public void method1(int p1, long p3, int p2) { + + } + } + """) + }*/ + + fun `test deleting parameter using BACKSPACE`() { + placeCaretIntoCellWithText("") + typeText("int") + pressKey(KnownKeys.Tab) + typeText("p1") + assertEditorText(""" + class Class1 { + public void method1(int p1) { + + } + } + """) + assertCaretPosition("p1|") + pressKey(KnownKeys.ArrowLeft) + assertCaretPosition("p|1") + pressKey(KnownKeys.ArrowLeft) + assertCaretPosition("|p1") + pressKey(KnownKeys.Backspace) + assertFinalEditorText(""" + class Class1 { + public void method1() { + + } + } + """) + } + + fun `test deleting parameter using DELETE`() { + placeCaretIntoCellWithText("") + typeText("int") + pressKey(KnownKeys.Tab) + typeText("p1") + assertEditorText(""" + class Class1 { + public void method1(int p1) { + + } + } + """) + pressKey(KnownKeys.Delete) + assertFinalEditorText(""" + class Class1 { + public void method1() { + + } + } + """) + } + + fun `test deleting insertion placeholder`() { + placeCaretIntoCellWithText("") + typeText("int") + pressKey(KnownKeys.Tab) + typeText("p1") + pressKey(KnownKeys.Enter) + assertEditorText(""" + class Class1 { + public void method1(int p1, ) { + + } + } + """) + pressKey(KnownKeys.Backspace) + assertFinalEditorText(""" + class Class1 { + public void method1(int p1) { + + } + } + """) } } 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 e2464e58..040ed21c 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CaretSelection.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CaretSelection.kt @@ -108,6 +108,13 @@ 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) + } else { + val deleteAction = layoutable.cell.ancestors(true) + .mapNotNull { it.data.properties[CellActionProperties.delete] } + .firstOrNull { it.isApplicable() } + if (deleteAction != null) { + deleteAction.execute(editor) + } } } else { replaceText(min(start, end) until max(start, end), "", editor) @@ -224,6 +231,7 @@ class CaretSelection(val layoutable: LayoutableCell, val start: Int, val end: In if (singleAction != null) { editor.runWrite { singleAction.executeAndUpdateSelection(editor) + editor.state.clearTextReplacement(layoutable) } return true } diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTemplate.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTemplate.kt index 3ac5c311..8a672f6d 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTemplate.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTemplate.kt @@ -511,6 +511,13 @@ class ChildCellTemplate( placeholder.cellReferences += ChildNodeCellReference(node.reference, link, index) } placeholder.properties[CommonCellProperties.tabTarget] = true + placeholder.properties[CellActionProperties.delete] = object : ICellAction { + override fun execute(editor: EditorComponent) { + context.editorState.substitutionPlaceholderPositions.remove(createCellReference(node)) + } + + override fun isApplicable(): Boolean = true + } cell.addChild(placeholder) } fun addInsertActionCell(index: Int) { 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 b9cbf458..debba8f2 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CodeCompletionMenu.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CodeCompletionMenu.kt @@ -59,6 +59,7 @@ class CodeCompletionMenu( getSelectedEntry()?.let { entry -> editor.runWrite { entry.executeAndUpdateSelection(editor) + editor.state.clearTextReplacement(anchor) } } editor.closeCodeCompletionMenu() 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 a3b082c8..6564b95e 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/EditorEngine.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/EditorEngine.kt @@ -10,6 +10,7 @@ import org.modelix.model.api.IConcept import org.modelix.model.api.IConceptReference import org.modelix.model.api.INode import org.modelix.model.api.getAllConcepts +import org.modelix.model.api.remove class EditorEngine(incrementalEngine: IncrementalEngine? = null) { @@ -110,6 +111,7 @@ class EditorEngine(incrementalEngine: IncrementalEngine? = null) { data.properties[CellActionProperties.transformBefore] = SideTransformNode(true, node) data.properties[CellActionProperties.transformAfter] = SideTransformNode(false, node) data.properties[CommonCellProperties.selectable] = true + data.properties[CellActionProperties.delete] = DeleteNodeCellAction(node) return data } catch (ex: Exception) { LOG.error(ex) { "Failed to create cell for $node" } @@ -139,3 +141,13 @@ class EditorEngine(incrementalEngine: IncrementalEngine? = null) { private val LOG = mu.KotlinLogging.logger {} } } + +class DeleteNodeCellAction(val node: INode) : ICellAction { + override fun isApplicable(): Boolean = true + + override fun execute(editor: EditorComponent) { + editor.runWrite { + node.remove() + } + } +} diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/EditorState.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/EditorState.kt index 535caf64..09ecdccb 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/EditorState.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/EditorState.kt @@ -12,6 +12,12 @@ class EditorState { forceShowOptionals.clear() textReplacements.clear() } + + fun clearTextReplacement(cell: LayoutableCell): Unit = clearTextReplacement(cell.cell) + + fun clearTextReplacement(cell: Cell): Unit = cell.data.cellReferences.forEach { clearTextReplacement(it) } + + fun clearTextReplacement(cell: CellReference): Unit = textReplacements.remove(cell) } class SubstitutionPlaceholderPosition(val index: Int) 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 17d13ede..70c9cb51 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/ICellAction.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/ICellAction.kt @@ -41,6 +41,7 @@ object CellActionProperties { val transformBefore = CellPropertyKey("transformBefore", null) val transformAfter = CellPropertyKey("transformAfter", null) val insert = CellPropertyKey("insert", null) + val delete = CellPropertyKey("delete", null) val replaceText = CellPropertyKey("replaceText", null) } From 4ac07e4c4430dbeb1a8ebabe0bfbe623acc51312 Mon Sep 17 00:00:00 2001 From: Sascha Lisson Date: Tue, 14 May 2024 16:51:32 +0200 Subject: [PATCH 14/18] fix: don't create property wrapper actions for empty string --- .../src/commonMain/kotlin/org/modelix/editor/CellTemplate.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTemplate.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTemplate.kt index 8a672f6d..f9f27d07 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTemplate.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTemplate.kt @@ -361,7 +361,7 @@ open class PropertyCellTemplate(concept: IConcept, val property: IProperty) : inner class WrapPropertyValueProvider(val location: INonExistingNode) : ICodeCompletionActionProvider { override fun getApplicableActions(parameters: CodeCompletionParameters): List { - return if (validateValue(location.replacement(concept), parameters.pattern)) { + return if (parameters.pattern.isNotBlank() && validateValue(location.replacement(concept), parameters.pattern)) { listOf(WrapPropertyValue(location, parameters.pattern)) } else { emptyList() From b520055c754fe973d60ae034c56fab8b712ff993 Mon Sep 17 00:00:00 2001 From: Sascha Lisson Date: Wed, 15 May 2024 09:45:54 +0200 Subject: [PATCH 15/18] fix: caret positioning after deleting a node --- .../editor/ssr/mps/BaseLanguageTests.kt | 11 +++- .../org/modelix/editor/CaretPositionPolicy.kt | 63 ++++++++++++++++++- .../org/modelix/editor/CaretSelection.kt | 8 ++- .../org/modelix/editor/CellSelection.kt | 4 ++ .../kotlin/org/modelix/editor/CellTemplate.kt | 26 ++++---- .../kotlin/org/modelix/editor/Cells.kt | 1 + .../editor/CodeCompletionActionWrapper.kt | 10 +-- .../org/modelix/editor/CodeCompletionMenu.kt | 9 ++- .../kotlin/org/modelix/editor/EditorEngine.kt | 8 ++- .../kotlin/org/modelix/editor/ICellAction.kt | 2 +- .../kotlin/org/modelix/editor/Selection.kt | 1 + 11 files changed, 115 insertions(+), 28 deletions(-) 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 5811e2f1..c9411920 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 @@ -59,13 +59,17 @@ class BaseLanguageTests : TestBase("SimpleProject") { fun assertFinalEditorText(expected: String) { assertEditorText(expected) + + // Reset all editor state to ensure the typed text triggered a model transformation. editor.state.reset() editor.update() assertEditorText(expected) } fun assertCaretPosition(cellTextWithCaret: String) { - assertEquals(cellTextWithCaret, editor.getSelection()?.toString()) + val selection = checkNotNull(editor.getSelection()) { "No active selection" } + if (selection !is CaretSelection) error("Not a caret selection: $selection") + assertEquals(cellTextWithCaret, selection.toString()) } fun assertEditorText(expected: String) { @@ -318,6 +322,7 @@ class BaseLanguageTests : TestBase("SimpleProject") { } } """) + assertCaretPosition("|") } fun `test deleting parameter using DELETE`() { @@ -340,9 +345,10 @@ class BaseLanguageTests : TestBase("SimpleProject") { } } """) + assertCaretPosition("|") } - fun `test deleting insertion placeholder`() { + fun `test deleting placeholder`() { placeCaretIntoCellWithText("") typeText("int") pressKey(KnownKeys.Tab) @@ -363,5 +369,6 @@ class BaseLanguageTests : TestBase("SimpleProject") { } } """) + assertCaretPosition("p1|") } } 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 e9c4ee5e..9c2da00f 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CaretPositionPolicy.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CaretPositionPolicy.kt @@ -2,10 +2,14 @@ package org.modelix.editor import org.modelix.model.api.INode +interface ICaretPositionPolicy { + fun getBestSelection(editor: EditorComponent): CaretSelection? +} + data class CaretPositionPolicy( private val avoidedCellRefs: Set, private val preferredCellRefs: Set, -) { +) : ICaretPositionPolicy { constructor(preferredCellRef: CellReference) : this(emptySet(), setOf(preferredCellRef)) constructor(preferredNode: INode) : this(NodeCellReference(preferredNode.reference)) @@ -17,7 +21,7 @@ data class CaretPositionPolicy( preferredCellRefs + other.preferredCellRefs, ) - fun getBestSelection(editor: EditorComponent): CaretSelection? { + override fun getBestSelection(editor: EditorComponent): CaretSelection? { val candidates = preferredCellRefs .flatMap { editor.resolveCell(it) } .flatMap { it.descendantsAndSelf() } @@ -38,3 +42,58 @@ enum class CaretPositionType { START, END, } + +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() + ) + + override fun getBestSelection(editor: EditorComponent): CaretSelection? { + if (selectedCell != null) { + val resolvedCell = editor.resolveCell(selectedCell).firstOrNull()?.layoutable() + if (resolvedCell != null) { + return CaretSelection(resolvedCell, resolvedCell.getMaxCaretPos()) + } + } + + val leftCell = previousLeafs.asSequence().flatMap { editor.resolveCell(it) }.firstOrNull() + val rightCell = nextLeafs.asSequence().flatMap { editor.resolveCell(it) }.firstOrNull() + if (leftCell != null && rightCell != null) { + val centerCells = leftCell.nextLeafs(false).takeWhile { it != rightCell }.mapNotNull { it.layoutable() } + val lastCell = centerCells.lastOrNull() + if (lastCell != null) { + return CaretSelection(lastCell, lastCell.getMaxCaretPos()) + } + } + + if (leftCell != null) { + val layoutable = leftCell.layoutable() + if (layoutable != null) { + return CaretSelection(layoutable, layoutable.getMaxCaretPos()) + } + } + + if (rightCell != null) { + val layoutable = rightCell.layoutable() + if (layoutable != null) { + return CaretSelection(layoutable, 0) + } + } + + return null + } + + companion object { + fun saveAndRun(editor: EditorComponent, 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 040ed21c..daa9d171 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CaretSelection.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CaretSelection.kt @@ -12,6 +12,10 @@ class CaretSelection(val layoutable: LayoutableCell, val start: Int, val end: In require(end >= 0) { "invalid end: $start" } } + override fun getSelectedCells(): List { + return listOf(layoutable.cell) + } + override fun isValid(): Boolean { val editor = getEditor() ?: return false val visibleText = editor.getRootCell().layout @@ -113,7 +117,7 @@ class CaretSelection(val layoutable: LayoutableCell, val start: Int, val end: In .mapNotNull { it.data.properties[CellActionProperties.delete] } .firstOrNull { it.isApplicable() } if (deleteAction != null) { - deleteAction.execute(editor) + deleteAction.executeAndUpdateSelection(editor) } } } else { @@ -132,7 +136,7 @@ class CaretSelection(val layoutable: LayoutableCell, val start: Int, val end: In // TODO resolve conflicts if multiple actions are applicable val action = actions.firstOrNull() if (action != null) { - action.execute(editor) + action.executeAndUpdateSelection(editor) break } previousLeaf = nextLeaf 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 28100141..77321517 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellSelection.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellSelection.kt @@ -3,6 +3,10 @@ package org.modelix.editor data class CellSelection(val cell: Cell, val directionLeft: Boolean, val previousSelection: Selection?) : Selection() { fun getEditor(): EditorComponent? = cell.editorComponent + override fun getSelectedCells(): List { + return listOf(cell) + } + override fun isValid(): Boolean { return getEditor() != null } diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTemplate.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTemplate.kt index f9f27d07..0b868e56 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTemplate.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTemplate.kt @@ -154,7 +154,12 @@ class ConstantCellTemplate(concept: IConcept, val text: String) : override fun getSymbolTransformationAction(node: INode, optionalCell: TemplateCellReference): IActionOrProvider? { return ForceShowOptionalCellAction(optionalCell) - .withCaretPolicy { it?.avoid(createCellReference(node)) } + .withCaretPolicy { + when (it) { + is CaretPositionPolicy -> it.avoid(createCellReference(node)) + else -> it + } + } .withMatchingText(text) } @@ -512,8 +517,10 @@ class ChildCellTemplate( } placeholder.properties[CommonCellProperties.tabTarget] = true placeholder.properties[CellActionProperties.delete] = object : ICellAction { - override fun execute(editor: EditorComponent) { - context.editorState.substitutionPlaceholderPositions.remove(createCellReference(node)) + override fun execute(editor: EditorComponent): ICaretPositionPolicy? { + return SavedCaretPosition.saveAndRun(editor) { + context.editorState.substitutionPlaceholderPositions.remove(createCellReference(node)) + } } override fun isApplicable(): Boolean = true @@ -649,26 +656,21 @@ class InsertSubstitutionPlaceholderAction( ) : ICellAction { override fun isApplicable(): Boolean = true - override fun execute(editor: EditorComponent) { + override fun execute(editor: EditorComponent): CaretPositionPolicy { editorState.substitutionPlaceholderPositions[ref] = SubstitutionPlaceholderPosition(index) editorState.textReplacements.remove(PlaceholderCellReference(ref)) - editor.selectAfterUpdate { - editor.resolveCell(PlaceholderCellReference(ref)) - .firstOrNull()?.layoutable()?.let { CaretSelection(it, 0) } - } + return CaretPositionPolicy(PlaceholderCellReference(ref)) } } class InstantiateNodeCellAction(val location: INonExistingNode, val concept: IConcept) : ICellAction { override fun isApplicable(): Boolean = true - override fun execute(editor: EditorComponent) { + override fun execute(editor: EditorComponent): CaretPositionPolicy { val newNode = location.getExistingAncestor()!!.getArea().executeWrite { location.replaceNode(concept) } - editor.selectAfterUpdate { - CaretPositionPolicy(newNode).getBestSelection(editor) - } + return CaretPositionPolicy(newNode) } } 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 e265290b..b508a07f 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/Cells.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/Cells.kt @@ -91,6 +91,7 @@ fun Cell.getSelectableText(): String? { return getProperty(CommonCellProperties.textReplacement) ?: (data as? TextCellData)?.text } fun Cell.getMaxCaretPos(): Int = getSelectableText()?.length ?: 0 +fun LayoutableCell.getMaxCaretPos(): Int = cell.getSelectableText()?.length ?: 0 class ResettableLazy(private val initializer: () -> E) : Lazy { private var lazy: 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 b28a2170..27207b9c 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CodeCompletionActionWrapper.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CodeCompletionActionWrapper.kt @@ -26,15 +26,15 @@ class CodeCompletionActionProviderWrapper( } class CodeCompletionActionWithPostprocessor(action: ICodeCompletionAction, val after: () -> Unit) : CodeCompletionActionWrapper(action) { - override fun execute(editor: EditorComponent): CaretPositionPolicy? { + override fun execute(editor: EditorComponent): ICaretPositionPolicy? { val policy = wrappedAction.execute(editor) after() return policy } } -class CodeCompletionActionWithCaretPolicy(action: ICodeCompletionAction, val policy: (CaretPositionPolicy?) -> CaretPositionPolicy?) : CodeCompletionActionWrapper(action) { - override fun execute(editor: EditorComponent): CaretPositionPolicy? { +class CodeCompletionActionWithCaretPolicy(action: ICodeCompletionAction, val policy: (ICaretPositionPolicy?) -> ICaretPositionPolicy?) : CodeCompletionActionWrapper(action) { + override fun execute(editor: EditorComponent): ICaretPositionPolicy? { return policy(wrappedAction.execute(editor)) } } @@ -61,11 +61,11 @@ fun ICodeCompletionAction.withMatchingText(text: String): CodeCompletionActionWi return CodeCompletionActionWithMatchingText(this, text) } -fun ICodeCompletionAction.withCaretPolicy(policy: (CaretPositionPolicy?) -> CaretPositionPolicy?): CodeCompletionActionWithCaretPolicy { +fun ICodeCompletionAction.withCaretPolicy(policy: (ICaretPositionPolicy?) -> ICaretPositionPolicy?): CodeCompletionActionWithCaretPolicy { return CodeCompletionActionWithCaretPolicy(this, policy) } -fun ICodeCompletionActionProvider.withCaretPolicy(policy: (CaretPositionPolicy?) -> CaretPositionPolicy?): CodeCompletionActionProviderWrapper { +fun ICodeCompletionActionProvider.withCaretPolicy(policy: (ICaretPositionPolicy?) -> ICaretPositionPolicy?): CodeCompletionActionProviderWrapper { return 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 debba8f2..b095cc20 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CodeCompletionMenu.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CodeCompletionMenu.kt @@ -220,7 +220,7 @@ private fun IActionOrProvider.flatten(parameters: CodeCompletionParameters): Seq interface ICodeCompletionAction : IActionOrProvider { fun getMatchingText(): String fun getDescription(): String - fun execute(editor: EditorComponent): CaretPositionPolicy? + fun execute(editor: EditorComponent): ICaretPositionPolicy? fun shadows(shadowed: ICodeCompletionAction) = false fun shadowedBy(shadowing: ICodeCompletionAction) = false } @@ -232,6 +232,13 @@ fun ICodeCompletionAction.executeAndUpdateSelection(editor: EditorComponent) { } } +fun ICellAction.executeAndUpdateSelection(editor: EditorComponent) { + val policy = execute(editor) + if (policy != null) { + editor.selectAfterUpdate { policy.getBestSelection(editor) } + } +} + class CodeCompletionParameters(val editor: EditorComponent, pattern: String) { val pattern: String = pattern get() { 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 6564b95e..9a23e631 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/EditorEngine.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/EditorEngine.kt @@ -145,9 +145,11 @@ class EditorEngine(incrementalEngine: IncrementalEngine? = null) { class DeleteNodeCellAction(val node: INode) : ICellAction { override fun isApplicable(): Boolean = true - override fun execute(editor: EditorComponent) { - editor.runWrite { - node.remove() + override fun execute(editor: EditorComponent): ICaretPositionPolicy? { + return SavedCaretPosition.saveAndRun(editor) { + editor.runWrite { + node.remove() + } } } } 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 70c9cb51..15e75aa1 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/ICellAction.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/ICellAction.kt @@ -5,7 +5,7 @@ import org.modelix.model.api.getInstantiatableSubConcepts interface ICellAction { fun isApplicable(): Boolean - fun execute(editor: EditorComponent) + fun execute(editor: EditorComponent): ICaretPositionPolicy? } interface ITextChangeAction { 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 e45394d6..4ad68a5f 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/Selection.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/Selection.kt @@ -3,6 +3,7 @@ package org.modelix.editor abstract class Selection : IKeyboardHandler { abstract fun isValid(): Boolean abstract fun update(editor: EditorComponent): Selection? + abstract fun getSelectedCells(): List } abstract class SelectionView(val selection: E) : IProducesHtml { From 26fcbcbbea75677d26c126c7660b9c840d579cb5 Mon Sep 17 00:00:00 2001 From: Sascha Lisson Date: Wed, 15 May 2024 11:25:52 +0200 Subject: [PATCH 16/18] feat: TABing into optional cells An optional cell that is currently hidden can now be shown by just navigating into it using TAB. Makes the editor much more explorable because you don't need to know its exact location and which side transformation is needed to show it. --- ...mps.notation.impl.baseLanguage.modelix.mps | 53 ++++++++++++ .../editor/ssr/mps/BaseLanguageTests.kt | 64 +++++++++++++- .../org/modelix/editor/CaretSelection.kt | 23 +++-- .../org/modelix/editor/CellNavigationUtils.kt | 42 ++++++--- .../org/modelix/editor/CellProperties.kt | 1 + .../kotlin/org/modelix/editor/CellTemplate.kt | 24 ++++-- .../kotlin/org/modelix/editor/ICellAction.kt | 1 + .../org/modelix/editor/CellNavigationTest.kt | 85 +++++++++++++++++++ 8 files changed, 263 insertions(+), 30 deletions(-) create mode 100644 projectional-editor/src/commonTest/kotlin/org/modelix/editor/CellNavigationTest.kt diff --git a/mps/modules/org.modelix.mps.notation.impl.baseLanguage/models/org.modelix.mps.notation.impl.baseLanguage.modelix.mps b/mps/modules/org.modelix.mps.notation.impl.baseLanguage/models/org.modelix.mps.notation.impl.baseLanguage.modelix.mps index b7b13b69..1de258f7 100644 --- a/mps/modules/org.modelix.mps.notation.impl.baseLanguage/models/org.modelix.mps.notation.impl.baseLanguage.modelix.mps +++ b/mps/modules/org.modelix.mps.notation.impl.baseLanguage/models/org.modelix.mps.notation.impl.baseLanguage.modelix.mps @@ -115,6 +115,22 @@ + + + + + + + + + + + + + + + + @@ -1469,6 +1485,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 c9411920..9fd04bda 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 @@ -175,7 +175,7 @@ class BaseLanguageTests : TestBase("SimpleProject") { """) } - fun `test showing initializer of LocalVariableDeclaration`() { + fun `test showing initializer of LocalVariableDeclaration using side transformation`() { placeCaretIntoCellWithText("") typeText("int") pressKey(KnownKeys.Tab) @@ -188,6 +188,68 @@ class BaseLanguageTests : TestBase("SimpleProject") { } } """) + assertCaretPosition("|") + } + + fun `test showing initializer of LocalVariableDeclaration using TAB`() { + placeCaretIntoCellWithText("") + typeText("int") + pressKey(KnownKeys.Tab) + typeText("abc") + pressKey(KnownKeys.Tab) + assertEditorText(""" + 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") + + assertEditorText(""" + class Class1 { + public void method1() { + int abc; + int def; + } + } + """) + + pressKey(KnownKeys.Tab) + assertEditorText(""" + class Class1 { + public void method1() { + int abc = ; + int def; + } + } + """) + assertCaretPosition("|") + + pressKey(KnownKeys.Tab) + assertCaretPosition("|def") + pressKey(KnownKeys.Tab) + assertEditorText(""" + class Class1 { + public void method1() { + int abc; + int def = ; + } + } + """) + assertCaretPosition("|") } fun `test adding initializer to LocalVariableDeclaration`() { 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 daa9d171..ac748a38 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CaretSelection.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CaretSelection.kt @@ -94,12 +94,23 @@ class CaretSelection(val layoutable: LayoutableCell, val start: Int, val end: In } } KnownKeys.Tab -> { - val target = layoutable - .getSiblingsInText(!event.modifiers.shift) - .filterIsInstance() - .firstOrNull { it.cell.isTabTarget() } - if (target != null) { - editor.changeSelection(CaretSelection(target, 0)) + 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 + } + } } } KnownKeys.Delete, KnownKeys.Backspace -> { 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 3e246578..b6df07f3 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellNavigationUtils.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellNavigationUtils.kt @@ -1,5 +1,13 @@ package org.modelix.editor +fun Cell.nextCells(): Sequence { + return nextSiblings().flatMap { it.descendantsAndSelf() } + (parent?.let { sequenceOf(it) + it.nextCells() } ?: emptySequence()) +} + +fun Cell.previousCells(): Sequence { + return previousSiblings().flatMap { it.descendantsAndSelf(iterateBackwards = true) } + (parent?.let { sequenceOf(it) + it.previousCells() } ?: emptySequence()) +} + fun Cell.previousLeafs(includeSelf: Boolean = false): Sequence { return generateSequence(this) { it.previousLeaf() }.drop(if (includeSelf) 0 else 1) } @@ -37,25 +45,31 @@ fun Cell.lastLeaf(): Cell { } fun Cell.previousSibling(): Cell? { - val parent = this.parent ?: return null - val siblings = parent.getChildren() - val index = siblings.indexOf(this) - if (index == -1) throw RuntimeException("$this expected to be a child of $parent") - val siblingIndex = index - 1 - return if (siblingIndex >= 0) siblings[siblingIndex] else null + return previousSiblings().firstOrNull() } fun Cell.nextSibling(): Cell? { - val parent = this.parent ?: return null - val siblings = parent.getChildren() - val index = siblings.indexOf(this) - if (index == -1) throw RuntimeException("$this expected to be a child of $parent") - val siblingIndex = index + 1 - return if (siblingIndex < siblings.size) siblings[siblingIndex] else null + return nextSiblings().firstOrNull() +} + +fun Cell.previousSiblings(): Sequence { + val parent = this.parent ?: return emptySequence() + return parent.getChildren().asReversed().asSequence().dropWhile { it != this }.drop(1) +} + +fun Cell.nextSiblings(): Sequence { + val parent = this.parent ?: return emptySequence() + return parent.getChildren().asSequence().dropWhile { it != this }.drop(1) +} + +fun Cell.descendants(iterateBackwards: Boolean = false): Sequence { + return getChildren() + .let { if (iterateBackwards) it.asReversed() else it } + .asSequence() + .flatMap { it.descendantsAndSelf(iterateBackwards) } } -fun Cell.descendants(): Sequence = getChildren().asSequence().flatMap { it.descendantsAndSelf() } -fun Cell.descendantsAndSelf(): Sequence = sequenceOf(this) + descendants() +fun Cell.descendantsAndSelf(iterateBackwards: Boolean = false): Sequence = sequenceOf(this) + descendants(iterateBackwards) fun Cell.ancestors(includeSelf: Boolean = false) = generateSequence(if (includeSelf) this else this.parent) { it.parent } fun Cell.commonAncestor(other: Cell): Cell = (ancestors(true) - other.ancestors(true).toSet()).last().parent!! 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 6bf231f0..e10e0184 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellProperties.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellProperties.kt @@ -47,6 +47,7 @@ object CommonCellProperties { 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) } fun Cell.isTabTarget() = getProperty(CommonCellProperties.tabTarget) diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTemplate.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTemplate.kt index 0b868e56..3c474f52 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTemplate.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTemplate.kt @@ -284,25 +284,27 @@ class OptionalCellTemplate(concept: IConcept) : } override fun applyChildren(context: CellCreationContext, node: INode, cell: CellData): List { - val forceShow = context.editorState.forceShowOptionals[createCellReference(node)] == true + fun forceShow() = context.editorState.forceShowOptionals[createCellReference(node)] == true val symbols = getChildren().asSequence().flatMap { it.getGrammarSymbols() } val conditionSymbol = symbols.filterIsInstance().firstOrNull() val transformationSymbol = symbols.firstOrNull() if (conditionSymbol == null) return emptyList() - if (forceShow || conditionSymbol.getSymbolConditionState(node)) { + val conditionState = conditionSymbol.getSymbolConditionState(node) + if (conditionState || forceShow()) { + if (!conditionState) { + cell.properties[CommonCellProperties.isForceShown] = true + } return super.applyChildren(context, node, cell) } else { if (transformationSymbol == null) return emptyList() val symbolTransformationAction = transformationSymbol.getSymbolTransformationAction(node, createCellReference(node)) if (symbolTransformationAction != null) { - val sideTransformCell = CellData() - sideTransformCell.properties[CellActionProperties.transformBefore] = symbolTransformationAction.asProvider() - return listOf(sideTransformCell) - } else { - return emptyList() + cell.properties[CellActionProperties.transformBefore] = symbolTransformationAction.asProvider() } + cell.properties[CellActionProperties.show] = ForceShowOptionalCellAction(createCellReference(node)) + return emptyList() } } @@ -315,8 +317,8 @@ class OptionalCellTemplate(concept: IConcept) : } } -class ForceShowOptionalCellAction(val cell: TemplateCellReference) : ICodeCompletionAction { - override fun execute(editor: EditorComponent): CaretPositionPolicy? { +class ForceShowOptionalCellAction(val cell: TemplateCellReference) : ICodeCompletionAction, ICellAction { + override fun execute(editor: EditorComponent): ICaretPositionPolicy { editor.state.forceShowOptionals[cell] = true return CaretPositionPolicy(cell) } @@ -328,6 +330,10 @@ class ForceShowOptionalCellAction(val cell: TemplateCellReference) : ICodeComple override fun getDescription(): String { return "Add optional part" } + + override fun isApplicable(): Boolean { + return true + } } open class PropertyCellTemplate(concept: IConcept, val property: IProperty) : 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 15e75aa1..4120c8dd 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/ICellAction.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/ICellAction.kt @@ -42,6 +42,7 @@ object CellActionProperties { 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) } diff --git a/projectional-editor/src/commonTest/kotlin/org/modelix/editor/CellNavigationTest.kt b/projectional-editor/src/commonTest/kotlin/org/modelix/editor/CellNavigationTest.kt new file mode 100644 index 00000000..e7c94291 --- /dev/null +++ b/projectional-editor/src/commonTest/kotlin/org/modelix/editor/CellNavigationTest.kt @@ -0,0 +1,85 @@ +package org.modelix.editor + +import kotlin.test.Test +import kotlin.test.assertEquals + +class CellNavigationTest { + private val rootCell = cell("root") { + cell("1") { + cell("11") { + cell("111") + cell("112") + } + cell("12") { + cell("121") + cell("122") + } + } + cell("2") { + cell("21") { + cell("211") + cell("212") + } + cell("22") { + cell("221") + cell("222") + } + } + } + + @Test + fun order_of_previousCells() { + assertEquals( + listOf( + "221", + "22", + "21", + "212", + "211", + "2", + "1", + "12", + "122", + "121", + "11", + "112", + "111", + "root" + ), + rootCell.lastLeaf().previousCells().map { (it.data as TextCellData).text }.toList() + ) + } + + @Test + fun order_of_nextCells() { + assertEquals( + listOf( + "112", + "11", + "12", + "121", + "122", + "1", + "2", + "21", + "211", + "212", + "22", + "221", + "222", + "root" + ), + rootCell.firstLeaf().nextCells().map { (it.data as TextCellData).text }.toList() + ) + } + + private fun cell(text: String, body: Cell.() -> Unit): Cell { + return Cell(TextCellData(text)).also(body) + } + + private fun Cell.cell(text: String, body: Cell.() -> Unit = {}): Cell { + return Cell(TextCellData(text)).also { addChild(it) }.also(body) + } + +} + From 959a1c10793e1b737e9f706af4e613e7b15563f7 Mon Sep 17 00:00:00 2001 From: Sascha Lisson Date: Wed, 15 May 2024 16:50:41 +0200 Subject: [PATCH 17/18] feat: TABing into flag cells Similar to optional cells, you can now TAB into hidden flag cells and set them by pressing ENTER. --- .../org/modelix/editor/CaretSelection.kt | 33 +++++++++-------- .../kotlin/org/modelix/editor/CellTemplate.kt | 36 ++++++++++++++++++- 2 files changed, 54 insertions(+), 15 deletions(-) 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 ac748a38..4818cc2b 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CaretSelection.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CaretSelection.kt @@ -136,21 +136,26 @@ class CaretSelection(val layoutable: LayoutableCell, val start: Int, val end: In } } KnownKeys.Enter -> { - 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 + 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 } - previousLeaf = nextLeaf } } else -> { diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTemplate.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTemplate.kt index 3c474f52..b4bf3ff1 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTemplate.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTemplate.kt @@ -474,13 +474,47 @@ class FlagCellTemplate( property: IProperty, val text: String, ) : PropertyCellTemplate(concept, property), IGrammarSymbol { - override fun createCell(context: CellCreationContext, node: INode) = if (node.getPropertyValue(property) == "true") TextCellData(text, "") else CellData() + override fun createCell(context: CellCreationContext, node: INode): CellData { + if (node.getPropertyValue(property) == "true") return TextCellData(text, "") + + val forceShow = context.editorState.forceShowOptionals[createCellReference(node)] == true + return if (forceShow) { + TextCellData("", text).also { + it.properties[CommonCellProperties.isForceShown] = true + it.properties[CellActionProperties.insert] = ChangePropertyCellAction(node.toNonExisting(), property, "true") + } + } else { + CellData().also { + it.properties[CellActionProperties.show] = ForceShowOptionalCellAction(createCellReference(node)) + } + } + } + override fun getInstantiationActions(location: INonExistingNode, parameters: CodeCompletionParameters): List? { // TODO return listOf() } } +class ChangePropertyCellAction( + val node: INonExistingNode, + val property: IProperty, + val value: String +) : ICellAction { + override fun execute(editor: EditorComponent): ICaretPositionPolicy? { + val node = editor.runWrite { + node.getOrCreateNode().also { + it.setPropertyValue(property, value) + } + } + return CaretPositionPolicy(PropertyCellReference(property, node.reference)) + } + + override fun isApplicable(): Boolean { + return true + } +} + class ChildCellTemplate( concept: IConcept, val link: IChildLink, From 03c0a29ae4a11f5e0b4a7f720907ee5209151b6f Mon Sep 17 00:00:00 2001 From: Sascha Lisson Date: Thu, 16 May 2024 15:42:59 +0200 Subject: [PATCH 18/18] chore: format --- .../kotlin/org/modelix/editor/CaretPositionPolicy.kt | 4 ++-- .../kotlin/org/modelix/editor/CellTemplate.kt | 2 +- .../kotlin/org/modelix/editor/CellNavigationTest.kt | 10 ++++------ 3 files changed, 7 insertions(+), 9 deletions(-) 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 9c2da00f..53c33ed2 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CaretPositionPolicy.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CaretPositionPolicy.kt @@ -46,12 +46,12 @@ enum class CaretPositionType { class SavedCaretPosition( val previousLeafs: Set, val nextLeafs: Set, - val selectedCell: CellReference? + 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() + selectedCell.data.cellReferences.firstOrNull(), ) override fun getBestSelection(editor: EditorComponent): CaretSelection? { diff --git a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTemplate.kt b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTemplate.kt index b4bf3ff1..be707b92 100644 --- a/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTemplate.kt +++ b/projectional-editor/src/commonMain/kotlin/org/modelix/editor/CellTemplate.kt @@ -499,7 +499,7 @@ class FlagCellTemplate( class ChangePropertyCellAction( val node: INonExistingNode, val property: IProperty, - val value: String + val value: String, ) : ICellAction { override fun execute(editor: EditorComponent): ICaretPositionPolicy? { val node = editor.runWrite { 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 e7c94291..ccbaab04 100644 --- a/projectional-editor/src/commonTest/kotlin/org/modelix/editor/CellNavigationTest.kt +++ b/projectional-editor/src/commonTest/kotlin/org/modelix/editor/CellNavigationTest.kt @@ -44,9 +44,9 @@ class CellNavigationTest { "11", "112", "111", - "root" + "root", ), - rootCell.lastLeaf().previousCells().map { (it.data as TextCellData).text }.toList() + rootCell.lastLeaf().previousCells().map { (it.data as TextCellData).text }.toList(), ) } @@ -67,9 +67,9 @@ class CellNavigationTest { "22", "221", "222", - "root" + "root", ), - rootCell.firstLeaf().nextCells().map { (it.data as TextCellData).text }.toList() + rootCell.firstLeaf().nextCells().map { (it.data as TextCellData).text }.toList(), ) } @@ -80,6 +80,4 @@ class CellNavigationTest { private fun Cell.cell(text: String, body: Cell.() -> Unit = {}): Cell { return Cell(TextCellData(text)).also { addChild(it) }.also(body) } - } -