Skip to content

Basic Editors

Nikolaus Knop edited this page Oct 14, 2020 · 3 revisions

This section will show you how to reproduce an editor for arithmetic expressions, which will look like this when it is complete: Demo couldn't be loaded
The code from this section, can be found in the hextant-expr-submodule.
For thinking about the language, we first formalize the grammar of the expression-language in BNF. Note that this is in principle not required for getting a working editor, but it helps you to build a mental model of how the editor should behave.

<expr> ::= <integer-literal> | <operator-application> | <sum>
<integer-literal> ::= ['1'-'9'] ['0'-'9']*
<operator> ::= '+' | '-' | '*' | '/'
<operator-application> ::= <expr> <operator> <expr>
<sum> ::= 'sum' <expr>+

From this syntax-definition we can now build the data structures that store the Abstract Syntax Tree. Note that each production rule corresponds to one class.

package hextant.expr

import kotlin.text.Regex

//corresponding rule: <expr> ::= <integer-literal> | <operator-application> | <sum>
sealed class Expr

//corresponding rule: <int-literal> ::= ['1'-'9'] ['0'-'9']*
data class IntLiteral(val value: String) : Expr() {
    companion object {
        private val REGEX = Regex("[1-9][0-9]*")

        fun isValid(token: String) = token.matches(REGEX)
    }
}

//corresponding rule: <operator> ::= '+' | '-' | '*' | '/'
enum class Operator(val symbol: String) {
    Plus("+"),
    Minus("-"),
    Times("*"),
    Div("/");

    companion object {
        private val map = values().associateBy { it.symbol }

        fun of(text: String) = map[text] ?: throw NoSuchElementException("No such operator '$text'")

        fun isValid(text: String) = map.containsKey(text)
    }
}

//corresponding rule: <operator-application> ::= <expr> <operator> <expr>
data class OperatorApplication(val op1: Expr, val op2: Expr, val operator: Operator)

//corresponding rule: <sum> ::= 'sum' <expr>+
data class Sum(val expressions: List<Expr>) : Expr()

Now that we have a model of the AST of our expression-language, it is time to create the editors that are produce the different kinds of expressions.
When the user stars creating an expression, the editor cannot know which type of expression the user wants to create (a literal, a binary expression or a summation). Therefore, a kind of "placeholder" is needed, that allows the user to choose which type of expression he wants to substitute in place of it. In Hextant, those "placeholder"-editors are named expanders, because they allow the user to type in some text and then expand it to a new editor template eventually containing new placeholders that the user can then fill in recursively. The Hextant framework offers the abstract class hextant.core.editor.Expander for this purpose, and most often you can use hextant.core.editor.ConfiguredExpander, which is a direct subclass of Expander, as a more convenient and extensible mechanism.
So as an editor that can be used for all kinds of expression, we create the class ExprExpander: As we create more and more editors for expressions, we will register them in the ExpanderConfig.

package hextant.expr.editor

import hextant.context.Context
import hextant.core.editor.ConfiguredExpander
import hextant.core.editor.ExpanderConfig
import hextant.expr.Expr
import hextant.expr.Operator.*

class ExprExpander(context: Context) : ConfiguredExpander<Expr, Editor<Expr>>(config, context) {
    companion object {
        val config = ExpanderConfig<ExprEditor<Expr>>()
    }
}

The editors for integer literals can be easily implemented as token editors. A token editor is represented on the screen as a text field. In the background, everytime the user types into that text field, the token editor checks, whether the content of the text field is valid and then produces an AST-node corresponding to the text typed in. Hextant offers an abstract class hextant.core.editor.TokenEditor<R, V>, that implements just this functionality. The only thing you have to override is the function compile(token: String): Validated<R>, which tries to make a valid leave node from the given token.

package hextant.expr.editor

import hextant.codegen.ProvideImplementation
import hextant.context.Context
import hextant.context.EditorFactory
import hextant.core.editor.TokenEditor
import hextant.core.view.TokenEditorView
import hextant.expr.IntLiteral
import validated.*

class IntLiteralEditor(context: Context, text: String = "") : TokenEditor<IntLiteral, TokenEditorView>(context, text) {
    override fun compile(token: String): Validated<IntLiteral> =
        if (IntLiteral.isValid(token)) valid(IntLiteral(token)) else invalid("Invalid integer literal '$token'") 
}

We can now tell the ExprExpander that whenever the user types a string into the expander, which can be interpreted as an integer literal, and then expands it, an IntLiteralEditor should be substituted for the placeholder. To do this, we create a file named ExprPlugin in the default package, which we will later register as our plugin initializer, and there create an object extending hextant.core.plugin.PluginInitializer. The method registerInterceptor ensures that when the user expands some text, the function passed to registerInterceptor is called with this text and if it returns a non-null editor, this editor is substituted for the placeholder.

import hextant.core.plugin.PluginInitializer
import hextant.expr.editor.*

object ExprPlugin: PluginInitializer({
    ExprExpander.config.registerInterceptor { text, context -> 
        if (IntLiteral.isValid(text)) IntLiteralEditor(context, text)
        else null
    }
})

The next thing we want to have, is an editor for binary expressions. For that we first need an editor for operators. Because operators are also leave nodes of the AST, they can be edited using token editors.

package hextant.expr.editor

import hextant.context.Context
import hextant.core.editor.TokenEditor
import hextant.core.view.TokenEditorView
import hextant.expr.Operator
import validated.*

class OperatorEditor constructor(context: Context, text: String = ""): TokenEditor<Operator, TokenEditorView>(context, text) {
    constructor(context: Context, operator: Operator) : this(context, operator.name)

    override fun compile(token: String): Validated<Operator> =
        if (!Operator.isValid(token)) invalid("Invalid operator '$token'") else valid(Operator.of(token))
}

An operator application consists of two expressions and one operator. For such cases, where an editor is composed of different component editors, where is the abstract class hextant.core.editor.CompoundEditor. To build an OperatorApplication-node from the three component nodes, the function composeResult<R>(vararg editors: Editor<*>): ReactiveValidated<R> is used, which takes a variable number of editors and returns a time varying result, that is updated everytime, one of the component results changes. It is important, that you use the child delegating function on the component editors, so that the component editors are recognized as children of the compound editor.

package hextant.expr.editor

import hextant.context.Context
import hextant.core.editor.CompoundEditor
import hextant.core.editor.composeResult
import hextant.expr.Operator
import hextant.expr.OperatorApplication
import validated.reaktive.ReactiveValidated

class OperatorApplicationEditor(context: Context, op: String = "") : CompoundEditor<OperatorApplication>(context) {
    val operator by child(OperatorEditor(context, op))
    val operand1 by child(ExprExpander(context))
    val operand2 by child(ExprExpander(context))

    override val result: ReactiveValidated<OperatorApplication> = composeResult(operand1, operand2, operator)
}

We also have to tell Hextant how to display an OperatorApplicationEditor on the screen. This is done by providing a control factory. The view function that is provided by the CompoundEditorControl-class adds the view of the specified editor to children of the compound editor control. By default, the children of a compound editor control are laid out vertically, but everything that is inside the same line-closure is laid out horizontally.
The file views.kt:

package hextant.expr.view

import hextant.expr.editor.*
import hextant.core.view.*
import hextant.context.*
import hextant.fx.createBorder
import bundles.Bundle

@ProvideImplementation(ControlFactory::class)
fun createControl(editor: OperatorApplicationEditor, arguments: Bundle) = CompoundEditorControl(editor, arguments) {
    line {
        view(editor.operand1)
        view(editor.operator)
        view(editor.operand2)
    }
}

To make the functionality of OperatorApplicationEditor available to the user, we have to extend the configuration of the ExprExpander to replace the placeholder by an OperatorApplicationEditor when the user expands a valid operator symbol. To do this, we add the following to the ExprPlugin (of course importing the necessary classes).

for (op in Operator.values()) {
    ExprExpander.config.registerKey(op.symbol) { context -> OperatorApplicationEditor(context, op.symbol) }
}

The last thing we have to add is the editor for summations. This editor can also be implemented using the CompoundEditor base class, but first we have to create an editor for lists of expressions. This is done using the class hextant.core.editor.ListEditor, which only demands us to override the function createEditor().

package hextant.expr.editor

import hextant.context.Context
import hextant.core.editor.ListEditor
import hextant.expr.Expr

class ExprListEditor(context: Context) : ListEditor<Expr, ExprEditor<Expr>>(context) {
    override fun createEditor(): Editor<Expr> = ExprExpander(context)
}

Now the editor for summations can be written. The line expressions.ensureNotEmpty() ensures that the user is not able to remove all child expressions from the list editor. This can be sometimes desirable but in our case we don't want to allow empty summations.

package hextant.expr.editor

import hextant.context.Context
import hextant.core.editor.CompoundEditor
import hextant.expr.Sum
import validated.reaktive.ReactiveValidated
import hextant.core.editor.composeResult

class SumEditor(context: Context) : CompoundEditor<Sum>(context) {
    val expressions by child(ExprListEditor(context))

    init {
        expressions.ensureNotEmpty()
    }

    override val result: ReactiveValidated<Sum> = composeResult(expressions)
}

Like for the OperatorApplicationEditor we have to provide a view for the SumEditor by adding the following to views.kt. The line set(ListEditorControl.ORIENTATION, ListEditorControl.Orientation.Horizontal) tells the ListEditorControl, which is the default view for all list editors, to lay out the items in the list horizontally.

import hextant.core.view.ListEditorControl 

@ProvideImplementation(ControlFactory::class)
fun createControl(editor: SumEditor, arguments: Bundle) = CompoundEditorControl(editor, arguments) {
    line {
        operator("")
        view(editor.expressions) {
            set(ListEditorControl.ORIENTATION, ListEditorControl.Orientation.Horizontal)
        }       
    }
}

The last step is to add the keyword "sum" to the ExpanderConfig expanding to a SumEditor.

ExprExpander.config.registerKey("sum") { context -> SumEditor(context) }

To learn how to automate the task of creating editors, visit the next tutorial: Code-Generation annotations.