Homework 9
Due Date: Friday March 26, 9pm
Purpose Warm-up Exercise plus Project – Part I
Expectations
This is a pair assignment. Remember that working in pairs means that you both work on the problems together, ideally by one person developing the solution, while the other person observes and comments. After a while, you switch roles. Working in pairs does not mean to divvy up the work between the two of you.
As with all assignments, you must upload one .rkt file (for the entire pair/team), in the language specified at the top of the assignment to the Handin Server. Using the wrong language will invalidate your submission and result in a score of 0, so please be careful.
Unless otherwise stated, all data and functions you design must be designed according to the data and function design recipes, resp., following all four steps in each case.
Your code must conform to the guidelines outlined in the style guide on the course website. The style guide will be updated as the semester progresses so please remember to read it before submitting each assignment.
Warm-Up Exercise
Exercise 1 A diagonal matrix is a quadratic matrix whose entries are all 0, except those on the diagonal. For example,
is a representation of a 3x3 diagonal unit matrix (whose diagonal entries are all 1). Design a function diag-matrix that takes two numbers n and d as input and returns a diagonal matrix, in the above representation, of dimension nxn, with all diagonal entries equal to d.
Project: Part I
The rest of the exercises will complete the first part of the project, the text editor, "Editor" henceforth. At the end of this homework, the Editor will be able to start on an empty text buffer and receive typed text from you, support editing and cursor-based navigation. It need not yet support displaying a command menu, starting with a non-empty text buffer, dumping the buffer contents into a variable for later reuse, mouse-event processing, font size changes, and the text search facility.
Some general facts about our Editor. It stores text internally as a list of lines lol, where each line is represented using a String. Each internal text line contains only printable characters; it does not contain control characters like return ("\n").
List lol containing the buffer text is never empty. If the buffer contains no text, lol will be a 1-element list, containing just the empty string.
Finally, the Editor also has a cursor. The cursor has a position, which consists of a line number, and a position within that line, called "column". The column either points to a character currently contained in the buffer, or it points to the past-end position, namely when the Editor is receiving new text. For example, if the cursor resides in a line containing the next "ABC", then there are four possible column values: 0,1,2,3. Column = 3 means that anything typed by the user is to be inserted at the end of the line. Note that the possible values of the column number vary from line to line.
Here is some initialization code (admittedly, not much!). You can change these values if you like, but the given ones work well.
(require 2htdp/image) (require 2htdp/universe) (define BACKGROUND (empty-scene 1000 500 "white")) ; the background for the text buffer (define FSIZE-TEXT 20) ; the text font size (define COLOR-TEXT "black") ; the text font color
Exercise 2 Let’s begin with data definitions for our Editor. Design data called Line, which is nothing but a synonym for String. Then design our main data structure, a Buffer. A buffer contains three things: the text, as a list of Lines, the line number where the cursor currently is, and the column number where the cursor currently is. Among your buffer data examples, include one that represents the empty buffer (of course!).
It is key that you design a Buffer correctly—
poorly designed data can make writing code later very painful. We recommend designing a few utility functions that operate on a buffer. We will not grade these, as they are not required, but we anticipate they will be helpful. (This is one way in which a project homework is different from a traditional homework.) Also, they provide good exercise understanding your buffer data design.
; start-of-buffer? : Buffer -> Boolean ; are we at the top-left corner of the buffer? (define (start-of-buffer? buffer) ...) ; end-of-buffer? : Buffer -> Boolean ; are we at the lower right corner of the buffer, i.e. last line, last column? (define (end-of-buffer? buffer) ...) ; end-of-line : Buffer Nat -> Nat ; the end-of-line position of the given line in the buffer (define (end-of-line buffer lnum) ...)
Exercise 3 Design the main function editor, which sets up the Editor. Naturally, the Editor will be implemented using big-bang. The state of your World will simply be the state of the buffer! That is to say, the buffer data already represents all information we need to run the Editor.
Your main function should take one argument, which—
in the future— will be the text to start the buffer with, as a String. We do not yet support this functionality in Part I, so your current implementation of editor can ignore this argument. In Part I, your editor must support drawing and key responses, so set this up. If you like: in big-bang, you can use the clause
[name "A Simple Text Editor"]
to tell the operating system (e.g. Windows) to name the window in which the editor runs "A Simple Text Editor".
Exercise 4 Design the drawing function for your Editor. This may require a couple of individual functions! The basic idea is as follows:
We draw the buffer text by converting each line into an image, and then stack these images on top of each other, aligned on the left (recall recommended utility function stack-images-left-aligned). The resulting image is placed into the top-left corner of the BACKGROUND. Use the place-image/align function.
To convert each line into an image, we distinguish two cases:
If the line does not contain the cursor, we just render the string for that line as text. Important: it is highly recommended to use a fixed-width font to render the text; this will hugely facilitate mouse-based navigation later. You can do this using the following call:
(text/font line FSIZE-TEXT COLOR-TEXT "Monospace" "default" "normal" "normal" #f) where line is the text line to be rendered.
If the line does contain the cursor, then—
how are we going to represent the cursor position in the drawing? One suggestion is to underline the text at the cursor position (but you can do this in other ways if you like). Underlining works well in case the cursor is at the past-end position: then you just show an underlined blank character: _ . Thus, to process a line that contains the cursor, you draw the text before the cursor as in the case above, then an underlined character indicating the cursor position, then the rest of the line again as above.
To render some piece of text underlined (in our case, just a 1-String), you can use the text/font function again. Look up the documentation.
The above will require using both recursion and list abstractions in smart combinations, but it is not tricky, in the sense of "designed to make you fail". Just use the knowledge you have acquired about these functions in class, follow the design recipes, and ask questions as needed.
Exercise 5 Design the key processing function for your editor. Your editor should be able to understand the following keys:
all "printable" characters: letters, digits, special characters above the digits on the keyboard, all kinds of brackets, all kinds of quotes, even backslash. The meaning is clear: if at the end of a line, append these characters. If not at the end of a line, insert these characters, and push the existing text to the right, just like a commercial text editor would. These characters do not change the line structure of the editor (i.e., lines may get long—
not our problem). the cursor keys "left", "right", "up", "down". They also "sort of" do the obvious thing, but here are some things to observe:
if you press "left" at the start of a line, the cursor should move to the end of the previous line (if it exists). Analogously for "right".
if you press "up" and the current cursor position is larger than the length of the previous line, make sure the cursor is positioned at the end of that previous line, rather than "in the middle of nowhere". For example, given buffer contents
X ABC_ and the cursor is at the end of the second line (indicated by _): if you press "up", the cursor should end up behind the "X": "X_". Similarly for "down".
backspace ("\b"): this key deletes the character before the current cursor position and moves the following text to the left. But: if the cursor is at the start of the line, then "\b" should delete the implicit newline character. That is, the current line should be appended to the previous line and then removed from the buffer as a separate line. Again, play with existing editors. Be sure you place the cursor at the right position after updating the text.
return ("\r"): this key is in many ways the inverse to backspace: we insert a new line into the editor, which contains the text of the current line, from the cursor position all the way to the end. The current line is truncated at the current cursor position. Of course, if you are at the end of the current line, pressing "\r" just inserts a blank new line into the buffer. In either case, the cursor should be placed at the start of the newly created line.
the "home" and "end" keys: they place the cursor at the start and end of the current line, resp. Here is a simple and useful extension that you don’t typically find in commercial editors: if the cursor is already at the start of the current line and someone presses "home", make the cursor go to the start of the whole buffer. In particular, this means if you press "home" twice, you end up at the start of the buffer. Analogously for "end".
keys to be ignored: you need to account for these somehow; otherwise an ugly printed version of them will appear in your text. Think about what it means to "ignore" some input key for your world program.
Ignorable keys include at least the tab key, the delete key ("\u007F"), the shift and control keys, the menu key (aka Windows key), the function keys "F1" .. "F12", and the escape key. You should also ignore the "keys" represented by "wheel-up", "wheel-down", "wheel-left", and "wheel-right", since ISL treats these as key-events even though they are triggered by mouse actions.
Note: the fact that you ignore the shift key(s) does not mean you cannot enter capital letters: if the user enters "shift-a" and you ignore "shift", your on-key function will still receive the capital "A".
One way to "efficiently ignore" keys is to make a list of strings representing keys to be ignored, and then write a small function that checks whether a given key belongs to that list.
Here is a list of other utility functions that may prove useful (they were for us).
; subst : [X] X Nat [List-of X] -> [List-of X] ; substitute new for the element at pos in lox ; (error if pos does not exist) ... (define (subst new pos lox) ...) ; subst-two : [X] X X Nat [List-of X] -> [List-of X] ; substitute the *two* elements new1 and new2 for the element at pos ; (error if pos does not exist in lox) ... (define (subst-two new1 new2 pos lox) ...) ; subst-for-two : [X] X Nat [List-of X]-> [List-of X] ; substitute new for the *two* elements as positions pos and pos+1 ; (error if pos, pos+1 do not exist in lox) (define (subst-for-two new pos lox) ...) ; char-insert : 1String Nat String -> String ; insert character c into s at position pos, or at end if pos = (string-length s) ... (define (char-insert c pos s) ...) ; char-delete : Nat String -> String ; delete the character at the given position (error if does not exist) ... (define (char-delete pos s) ...) ; stack-images-left-aligned : [List-of Image] -> Image ; stack the images on top of each other, aligned on the left ... (define (stack-images-left-aligned loi) ...)