CS 334: Lab 9: Scala Command Pattern

Overview

In this lab, you will implement the core data structures of a text editor using the Command Design Pattern in Scala, supporting multiple levels of undo and redo.

Partner

You are encouraged to work with a partner on this lab. As always, please send email if you would like help finding a partner.

Getting Started

Setting Up Your Repository

You will receive an email with an invitation link to the lab9 assignment on GitHub Classroom. You can follow the same instructions as on Lab 2 for accessing and cloning your repository. See the GitHub reference for instructions to add a partner. You should answer the following in the appropriate files in your repository.

Scala Setup

See instructions here for setting up Scala.

The scala command will give you a "read-eval-print" loop, as in Lisp and ML.

You can also compile and run a whole file as follows. Suppose file A.scala contains:

object A {
    def main(args : Array[String]) : Unit = {
        println(args(0));
    }
}

You then compile the program with scalac A.scala, and run it (and provide command-line arguments) with "scala A moo cow".

Programming

1. Undoable Commands (40 pts)

The goal of this problem is to implement the core data structures of a text editor using the Command Design Pattern.

Text Editor

In essence, a text editor manages a character sequence that can be changed in response to commands issued by the user, such as inserting new text or deleting text. Typically, these commands operate on the underlying character buffer at the current position of the cursor. Thus, if the cursor is positioned at the beginning of the buffer, typing the string "moo" will cause those letters to be inserted at the start of the buffer, and so on. This question explores the internal design of a simple editor.

Most text editors involve a GUI and the user issues commands to the editor by keyboard and mouse events. For us, however, the most interesting part of a text editor's is what happens behind the scenes. Therefore, our text editor will just be a simple command line program that prompts you for edit commands. The program will print the contents of the text editor's buffer, including a ^ to indicate the current cursor position, print the prompt "?", and then wait for you to enter a command. At one point in time, this was in fact how many text editors worked --- look up ed text editor in Wikipedia, for example (or run it on our lab machines...). The following shows one run of our editor:

Here is a summary of all available editor commands (including some described below). The term [num] indicates an optional number.

Command Description
I text Insert text at the current cursor, moving cursor to after the new text.
D [num] Delete num characters to the right of cursor position. (If num is missing, delete 1 character.)
< [num] Move the cursor num characters to the left. (If num is missing, move 1 character.)
> [num] Move the cursor num characters to the right. (If num is missing, move 1 character.)
Q Quit
U Undo the previous edit command
P Print the history of edit commands
R Redo an undone edit command

I have provided a working program for all but the last three commands. Your job is to change TextEditor to support multiple levels of undo and redo using the Command Design Pattern.

The figure below shows an example that uses "U" (undo) and "P" (print history). (We'll look at "R" (redo) at the very end of the problem.) Notice that you can undo multiple edits, not simply the last one. To support this, the text editor must keep track of an edit command history that permits you to undo as many commands from the history as you like. Undoing all commands will lead you all the way back to the original empty buffer.

The starter code for this problem is divided into two classes:

  • Buffer: This class manages the internal state of the editor's buffer (ie, character sequence and current cursor location), and it supports commands for getting/setting the cursor location and for inserting/deleting text. Refer to the javadoc on the handouts page for more details. You should not change this class.

  • TextEditor: This class stores a Buffer named buffer. The processOneCommand() method reads in a command from the user and performs the appropriate operation on buffer by invoking one of the following methods:

    • protected def setCursor(loc: Int): Unit

    • protected def insert(text: String): Unit

    • protected def delete(count: Int): Unit

    • protected def undo(): Unit

    • protected def redo(): Unit

    • protected def printHistory(): Unit

    These methods are all quite simple. For example, the insert method simply inserts the text into buffer and repositions the cursor:

    protected def insert(text: String) = {
        buffer.insert(text);
        buffer.setCursor(buffer.getCursor() + text.length());
    }
    

The EditCommand Class

To support undo, we first change the way the TextEditor operates on the underlying buffer. Rather than changing it directly, the TextEditor constructs EditCommand objects that know how to perform the desired operations and --- more importantly --- know how to undo those operations. All EditCommand objects will be derived from the EditCommand abstract class:

abstract class EditCommand(val target: Buffer) {

    /** Perform the command on the target buffer */
    def execute(): Unit;

    /** Undo the command on the target buffer */
    def undo(): Unit;

    /** Print out what this command represents */
    def toString(): String; 
}

Here, the execute() method carries out the desired operation on the target buffer, and undo() would perform the inverse operation. For example, to make insert undoable, the first step would be to define an InsertCommand class in a new file InsertCommand.scala:

class InsertCommand(b: Buffer, val text: String) extends EditCommand(b) {
    override def execute(): Unit = { ... }
    override def undo(): Unit = { ... }
    override def toString(): String = { ... }
}

The TextEditor would then perform code like the following inside insert:

protected def insert(text: String) = {
    val command = new InsertCommand(buffer, text);
    command.execute();
    ...
}

Assuming InsertCommand is implemented properly, the insertion would happen as before. However, the TextEditor can now remember that the last operation performed was the InsertCommand we created, and we can undo it simply by calling that object's undo() method. In essence, an EditCommand object describes one modification to a Buffer's state and how to undo that modification. Supporting undo is then as simple as writing a new kind of EditCommand object for each type of buffer modification you support.

And of course, to implement multiple levels of undo, you need to keep track of more than just the last command object created...

Implementation Strategy

I suggest tackling the implementation the following steps:

  1. Download the starter code from the handouts page. Compile the Scala files with the command fsc *.scala as usual. I have added some assert statements to the Buffer class to aid in debugging. The general form is

    assert(condition, { "message" })
    

    You may find it useful to add similar asserts to your own code as well.

  2. Implement InsertCommand, DeleteCommand, and MoveCommand subclasses of EditCommand. For each one, you must define: 1) execute(), (2) undo(), and (3) toString(). I recommend holding off on undo() for the moment. Change TextEditor to create and execute edit command objects appropriately.

  3. Extend TextEditor to remember the last command executed, and change TextEditor's undo() method to undo that command. Go back and implement undo for each type of EditCommand.

  4. Once a single level of undo is working, extend TextEditor to support undoing multiple previous commands. Specifically, change TextEditor to maintain a history of commands that have been executed and not undone. Also implement the printHistory() method to aid in debugging. Your program should simply ignore undo requests if there are no commands are in the history. You are free to use any Scala libraries you like in your implementation (ie, any immutable or mutable collection class).

  5. The last task is to implement redo. Specifically, if you undo one or more commands but have not yet performed any new operations on the buffer, you can redo the commands you undid:

    Note that redoing undone commands is no longer possible if the buffer is changed in any way. For example, if you insert text after undoing some command E, you should no longer be able to redo command E:

    Also, redone commands should be able to be subsequently undone:

    Extend TextEditor to support multiple levels of redo. You should not need to change any class other than TextEditor to implement this feature.

There are many extensions that would make our editor more "realistic". One idea is listed below. It should not require more than a few additional lines of code and really highlights the elegance and simplicity of adopting this design pattern.

Testing

You will want to test as you go on small inputs. I have also given you some test files that you may run as follows:

scala TextEditor < ex1.scala

The test files will all produce readily identifiable prose once you have properly implemented the different commands.

The autograder will also run a number of tests. If you pass them all, you should be in good shape.

2. Optional Extension: Composable Commands

Here is one interesting extension to the basic Text Editor. (No need to hand in a separate version of the code for this part if you choose to work on it -- simply change your solution to P1. But be sure to commit your earlier version in case you wish to revert back to it!)

Most of the time, two consecutive commands of the same type are lumped together into a single command. Thus, if I type "hello" followed immediately by " there" into an editor (such as emacs), the editor lumps them together into a single insertion command that removes all of "hello there" from the buffer when undone. Similarly, if I perform two cursor movement commands in a row, that is recorded in the undo history as a single command. Here is an example:

Implement composable commands. A good way to start is to extend the EditCommand class and its subclasses to define the following method:

def compose(other : EditCommand) : Option[EditCommand]

This method either:

  • returns None if the current command cannot be composed with other.

  • returns a new command if the current command can be composed with other, because, for example, they are both insert commands.

For example,

val c1 = new InsertCommand(target, "hel");
val c2 = new InsertCommand(target, "lo"));
c1.compose(c2) match {
    case None     => // can't combine them
    case Some(c3) => c3.execute();
}

would create the command c3 that inserts "hello" into the target. If we changed c2 to be a DeleteCommand, the compose operation would return None. You may find it useful to test whether an object has a certain type, which can be done in Scala with pattern matching, as in:

x match {
    case i : InsertCommand  => ... // x is an InsertCommand, now bound to i
    case i : DeleteCommand  => ... // x is an DeleteCommand, now bound to i
    case i                  => ... // match all other types
}

Submitting Your Work

Submit your code to the GradeScope assignment named, for example, "Lab 1". You can submit in one of two ways:

  • Upload files: Click "Upload" and select all of your source files, or
  • Link GitHub: Click "GitHub" and select your repository and branch.

Please do not change the names of the starter files. Also:

  • If you worked with a partner, only one of each pair needs to submit the code.
  • Indicate who your partner is when you submit. Specifically, after you upload your files, there will be an "Add Group Member" button on the right of the Gradescope webpage -- click that and add your partner.

Autograding: Gradescope will run an autograder on your code that performs some simple tests. Be sure to look at the autograder output to verify your code works as expected. We will run more extensive tests on your code after the deadline.