There are five files involved in the CS410 "credit" editor.
ANSIEscapes.hs
, which defines some useful
control codes for interacting with a console: driving the cursor
around, colouring text, that sort of thing. It's used to perform
the update operations in the console.Block.hs
, which is a small library for building
tilings of rectangular components, of which more shortly. Note,
there's some junk at the bottom of the file which looks like
something I typed during a lecture in 2009. It's used to model
the buffer contents for display purposes.Overlay.hs
, which is a practical from 2009. It
shows how to crop a layout from the Block
library. It's used
to select the region of the buffer to be redisplayed. However,
as its name suggests, it also sets an exercise in overlaying
block layouts with transparent regions. You may find this useful
if you want to implement popups of any kind.Prac1.hs
, which has your implementation of
handleKey
, giving meaning to the keystrokes. Note also the
crucial function whatAndWhere
which translates the nested
cursor structure that your code updates into the layout
structure from the Block
library.Main.hs
, which has the bindings to console
interaction and handles both the keystroke translation and the
main "event loop" in the inner
helper function, subordinate to
outer
. Your handleKey
is used to update the data model for
the editor, then whatAndWhere
is used to regenerate the
buffer.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.
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 Hole
s 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 Hole
s
(i.e., transparent regions) into Blank
s (i.e., opaque regions of
background colour).
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
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.