A Guide to the Innards of the CS410 Editor

Overview

There are five files involved in the CS410 "credit" editor.

Blocks

The Block library gives a simple data structure for working with tiles of stuff. Block a is defined mutually with Layout a, where the latter is a sized block. The parameter a gets instantiated with whatever your basic tiles are made of, e.g. Box, being lists of strings (hopefully corresponding to a rectangle of text). Tiles can be put joined horizontally or vertically. You should always ensure that they fit snugly.

To help you, there's a bunch of combinators for layouts which make correctly sized components from strings, and which join components together even if they don't fit snugly. The Blank constructor for Block a gets used to pad out irregularly sized blocks so that they fit.

Once you've got a Layout, you can render it. The layout function computes one big Box from a sized tiling of wee boxes. If all the components have the sizes claimed and all of the fitting is snug, then you'll get a rectangular layout. If you look in Main.hs, you'll see layout being used to generate the sequence of text to send to the console.

If you look in Prac1.hs, you'll see that whatAndWhere generates a Layout Box corresponding to the whole text being edited, and also computes the Point where the cursor is. If you want to change the data model, e.g., to incorporate a selected region, you would also need to change whatAndWhere to build a suitable layout from your new structure and calculate the cursor position.

If you want to add foreground or background colour to your text, an easy way to do it is to add constructors to the Block a datatype which signal that a sub-block is coloured.

data Block a
  = Stuff a
  | Blank                      -- fits any size
  | Fg Colour (Block a)
  | Bg Colour (Block a)
  | Ver (Layout a) (Layout a)  -- should both have the right width
                               -- and heights which add correctly
  | Hor (Layout a) (Layout a)  -- should both have the right height
                               -- and widths which add correctly
  deriving Show

That way, you can add colour in your whatAndWhere operation. Some of you may remember the little evaluation game I built for you in second year, computing with highlighted code. That's how I did it. You can find the code here. Of course, you then need to modify the implementation of layout, e.g., by inserting the escape codes which make colour changes into the generated strings. You also need to extend the implementation of cropping, discussed next.

Cropping and Overlaying

The Overlay.hs file provides equipment for chopping layouts to size. It's used to fit the buffer contents to the console window. You can specify a region by giving the coordinates of its (left, top) corner and its (width, height). The key worker is cropLay, which crops a Layout a, given a cropper for the basic a components. A Box cropper, cropBox is provided, so your basic gadget is cropLay cropBox. You'll see that Main.hs uses that gadget to crop out the current line from the buffer when your "damage report" is LineChanged or to crop out the whole viewport when LotsChanged (or the window changes size, or the viewport needs to be repositioned to get the cursor back inside it).

If you wanted to add a status line to your editor, you could modify how the viewport is filled, taking one line fewer from the rendered buffer to make room for the status.

And then there's overlaying. That's an old exercise I first set in 2009. You might find it useful if you want to add a popup display of any sort. The idea is that we replace Box by Template, which allows either a Ready Box, or a Hole where a Box could go. We can then imagine an overlaying operation where Holes in the front layer allow you to see content from the back layer. To implement this, you have to work through the front layer, cropping the back layer to fit: whenever the front layer has a Hole, just paste in the back layer. Of course, if you can overlay two layers, you can overlay any number. When you're done, the holeBlank operation converts a Layout Template back to a Layout Box, just by turning the Holes (i.e., transparent regions) into Blanks (i.e., opaque regions of background colour).

The Prac1 Data Model

Our TextCursor type is rather handy for simple editor operations. It's also quite handy if you want to grab the text from the buffer as a String. You might want to build a thing which does that. You may find deactivate a useful function. Also

lines    :: String -> [String]
unlines  :: [String] -> String

map between one-big-string-with-newline-characters-in and lists-of-strings-one-for-each-line, as representations of text. It's easy to generate input for a parser by that means.

Think twice, though! In Prac4.hs, we implemented a particular interface for Parser functionality, consisting of Applicative, Alternative and a thing, char which inspects individual characters. Perhaps you could implement the same interface for parsers which keep their data in a TextCursor instead of a String. You might then be able to move the cursor to the position of a parse error!

You might also think of modifying the Prac1 data model, e.g. to support selection and cut/copy/paste. You'd need to store the clipboard contents and also store the selected text separately from the rest of the buffer. You could then modify whatAndWhere to assemble the pieces properly, and perhaps highlight the selection. If you do have selection available, you could maybe select regions for the FOUL interpreter to evaluate.

There's also nothing to stop you working with the Prac1 data model as it is. If you want special stuff to happen (e.g. saving and loading files, testing code, whatever,...) you could invent a special syntax for it and then just look for that syntax whenever you parse the file. You could then implement a special keystroke whose handleKey operation causes whatever action is requested in the text of the buffer itself. It would also not be silly to dump the printout from evaluating a FOUL expression as text in the buffer. Indeed, once upon a time, I built a programming language with a special declaration form

inspect <expression> = <value>

and whenever you were editing the file, the was always kept up to date with the .

The Main Module

There's a lot of boring stuff in this file about communicating with the console. Hopefully you can leave it alone.

It gets interesting around the definition of ScreenState which effectively describes the visible viewport through which we can inspect the buffer. The onScreen function checks whether the current cursor position is within the viewport and if not, it selects a new ScreenState within which the cursor becomes visible. If you had a more interesting display, featuring stuff other than the buffer, then you might need to keep track of more information.

There's also some code which turns raw console input into intelligible keystrokes. You might want to modify that if you have other keystrokes to trap. It might even be possible somehow (I wish I knew) to collect mouse actions, in which case you would want to generalise Key to some sort of Event.

The outer function is the main event-handling harness of the thing. It just kicks off inner with the initial state of the buffer rendered, and the damage report LotsChanged to force an initial redrawing. The top of the inner function handles redrawing: if either the screen size or viewport position has changed, the damage report is upgraded to LotsChanged, then we trigger whatever redrawing is indicated. Once that's done, we wait for a keystroke and decide how to proceed.

The simplest implementation of the required FOUL functionality replaces the line

Just Quit -> return ()

with something which tries to process the buffer contents as a FOUL program, just as in Prac4.hs, and deliver some (nicely formatted) output. That is, you edit interactively, but run in batch mode, like it's the mid 1970s.

More modern functionality might happen without quitting the editor (feel free to redeploy the ESC key and do quitting differently). Consider modifying the Damage type to allow for more messages from handleKey to inner, or even add other sorts of requests alongside Damage (e.g., saving the file).

What's left in Main? The main function itself. We switch off buffering, so input and output come character by character as befits an interactive console application. The code checks for a command line argument and treats it as a filename, loading its contents into the buffer. It's not very resilient to file nonexistence. Otherwise we kick off with an empty buffer.


Last modified: Tue Apr 16 12:38:40 BST 2013