Cursors, Part 4: The Textfield cursor

Date 2019-02-14

This is the fourth post in a series about cursors. It prepares the right data structure to write a simple text editor

Disclaimer: cursor is a library based off the concepts outlined in this blog post. It originated in the work on smos, a Purely Functional Semantic Forest Editor.

An extra parameter

To make a text field cursor, we must first break down what a text field actually is. A text field could be considered just a Text value. However, it could also be considered a nonempty list of lines where one of those lines are selected, and on the selected line, we select between two characters.

It is at this point that it becomes important that cursors can be composed. Indeed, if we slightly modify the definition of the NonEmptyCursor from the last blogpost, then the definition of a TextFieldCursor will come out naturally:

data NonEmptyCursor a b = NonEmptyCursor
    { nonEmptyCursorPrev :: [b]
    , nonEmptyCursorCurrent :: a
    , nonEmptyCursorNext :: [b]
    } deriving (Show, Eq, Generic, Functor)

We added a type variable so that we can differentiate, in type, between the elements in the nonempty cursor that is selected and the ones that are not. Now we can define a TextFieldCursor simply as follows:

newtype TextFieldCursor = TextFieldCursor
    { textFieldCursorNonEmpty :: NonEmptyCursor TextCursor Text
    } deriving (Show, Eq, Generic)

In this definition, it is important that neither the TextCursor nor the Text values contain any newlines. The newlines are implied. We will need to write an appropriate Validity instance.

Tradeoff

Note that there is an alternative definition of a TextFieldCursor that does not require any invariants:

newtype TextFieldCursor = TextFieldCursor
    { textFieldCursorListCursor :: ListCursor Char
    } deriving (Nhow, Eq, Generic)

I did not choose this version in the cursor library for a few reasons:

  • It makes implementing the functions to work with a text field cursor much simpler.

  • Validity-based testing makes it easy to test whether invariants are maintained.

  • It makes some operations faster, because you only need to find newlines once.

Manipulations

When implementing the text field cursor manipulations, we find that many of the functions that we expect are either manipulations of the NonEmptyCursor or of the TextCursor within. Indeed, the textFieldCursorInsertChar and textFieldCursorAppendChar functions are just manipulations of the selected TextCursor. (A simple lens comes in handy here: textFieldCursorSelectedL :: Lens' TextFieldCursor TextCursor) The textFieldCursorSelectPrevLine and textFieldCursorSelectNextLine functions are just manipulations of the NonEmptyCursor.

There are only a few functions that work across the boundaries of the cursors and they mostly all have to do with newlines. For example, textFieldCursorRemove, which removes the character before the cursor, needs to merge the previous line with the current line when the text field cursor is at the start of the line.

References

Text field cursors are available in the cursor package on Hackage. Cursors originated in the work on Smos. This post is part of an effort to encourage contributions to Smos. The simplest contribution could be to just try out smos and provide feedback on the experience. Smos is a purely functional semantic forest editor of a subset of YAML that is intended to replace Emacs' Org-mode for Getting Things Done.

Previous
Property Testing in Haskell

Looking for a lead engineer?

Hire me
Next
The quitting list