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.