This article describes an updated version of the T-Deck Lisp Editor. This second version is functionally identical to the first version, but it has been improved to make it easier to extend it by adding your own editing commands.
For example, you can now add a command g to insert a ‘>’ symbol in your program simply by evaluating:
(define-atomic-cmd #\g "greater" #'(lambda (fun) (pop *cmds*) (%edit (cons '> fun))))
These updates make the T-Deck Lisp Editor a bit like a baby Emacs.
Here’s the source of the Extensible T-Deck Lisp Editor: Extensible Lisp Editor.
It encodes the string as a C++ Raw String Literal to avoid needing to enclose each line in double quotes, and to escape special characters within the strings. Thanks to @nanomonkey for suggesting this improvement.
For information about installing it and using it see the earlier article: A Lisp Editor for T-Deck.
How the editor works
The main edit function reads the key presses you type, and rejects any illegal key presses. It builds up a list of the editing commands you have given in the global variable *cmds*.
After each key press it calls %edit, which executes the list of editing commands on the function (or variable) you are editing, and prettyprints the result, using pprint.
Here’s the function %edit, that executes the list of editing commands in *cmds*:
(defun %edit (fun) (let ((cmd (car *cmds*))) (cond ((null *cmds*) fun) ((eq cmd #\h) (pop *cmds*) (%edit (cons 'highlight (list fun)))) ((consp cmd) (funcall (cdr (assoc (car cmd) *binary-cmds*)) (cdr cmd) fun)) ((assoc cmd *atomic-cmds*) (funcall (cdr (assoc cmd *atomic-cmds*)) fun)) (t fun))))
There are two types of editing commands:
- Atomic (single-key) commands. These are represented by a single character, such as #\d.
- Binary commands that take a parameter. These are represented by a cons, consisting of the editing key followed by the argument, such as (#\r . +).
The single-key commands are defined in the association list *atomic-cmds*, and the binary commands are defined in the association list *binary-cmds*.
You can see the effect of %edit by setting *cmds* to a list of editing commands, and then calling it on a list. For example, suppose we have the list:
(lambda (x) (* x x))
The following edit sequence changes the ‘*’ to a ‘+’:
(setq *cmds* '(#\d #\d #\a #\a (#\r . +)))
Trying it out:
> (setq *cmds* '(#\d #\d #\a #\a (#\r . +))) (#\d #\d #\a #\a (#\r . +)) > (%edit '(lambda (x) (* x x))) (lambda (x) (+ x x))
Processing the editing commands
The function %edit is called for each of the editing commands in the *cmds* list. It first sets the local variable cmd to the first command in the list, and a cond structure then selects the appropriate action for that command.
For example, the action for the x (delete) command is defined by the following entry in the *atomic-cmds* association list:
(#\x . #'(lambda (fun) (pop *cmds*) (if (atom fun) (%edit fun) (%edit (cdr fun)))))
This first pops the completed command off the front of the *cmds* list.
It then checks whether the current selection fun is an atom, in which case the command is invalid and simply calls %edit on fun again.
Otherwise it calls %edit on the cdr of fun, effectively deleting the car.
The main edit function
The main function edit prettyprints the function being edited, and then waits for a key press.
It handles the special editing commands, such as q (quit), s (save) and z (undo), that don’t get passed to %edit.
If the key is in the *binary-cmds* or *atomic-cmds* association lists it adds it to the command buffer.
It then calls %edit to apply the editing commands in *cmds* to the function, and loops around to print the result.
Adding commands to the editor
The functions define-atomic-cmd and define-binary-cmd are provided as a convenient way of adding commands to the corresponding association lists of commands:
(defun define-atomic-cmd (char name cmd) (push (cons char cmd) *atomic-cmds*)) (defun define-binary-cmd (char name cmd) (push (cons char cmd) *binary-cmds*))
For example, to define the x (delete) command already described, you call:
(define-atomic-cmd #\x "delete" #'(lambda (fun) (pop *cmds*) (if (atom fun) (%edit fun) (%edit (cdr fun)))))
You can use these commands to define a new editing command and add it to the Lisp Editor.
Defining your own editing command
As an example, let’s define an editing command w that swaps the next two forms after the editor cursor (we can’t call it s because that is already used for save).
For example, it will be useful if we’ve tried to define a function big that tests whether its argument is greater than 100, but we’ve written it incorrectly as:
(defun big (a) (> 100 a))
In the Lisp Editor we will be able to move the green block cursor in front of 100 and press w.
To define the swap command execute the following:
(define-atomic-cmd #\w "swap" #'(lambda (fun) (pop *cmds*) (cond ((null (cdr fun)) (%edit fun)) (t (%edit (cons (second fun) (cons (first fun) (cddr fun))))))))
If there aren’t two forms after the cursor (cdr fun) is nil, and we ignore the command.
Otherwise we call %edit on the current value of fun with the first two items swapped.
Let’s try it out:
Type d d a d to position the block cursor:
Type w to swap the next two items:
Type s to save the corrected version.
A second example
Here’s a second example. It defines a command p, for prototype, that inserts a function call prototype into your program at the block cursor position. It’s useful if you can’t remember the correct arguments for a function, or what order they should be in.
For example, if you want to insert a call to subseq into the program you are editing, position the cursor at the correct position and type p.
The Lisp Editor prompts for the name of the function you want to insert:
Here’s the definition:
(define-binary-cmd #\p "prototype" #'(lambda (val fun) (pop *cmds*) (%edit (cons (read-from-string (documentation val)) fun))))
For example, we have defined an initial attempt at a function to print the day name:
(defun day (n) (let ((days "MonTueWedThuFriSatSun")) (print)))
We want to insert the correct function call after print.
Type d d a d d a d to position the block cursor after print.
Type p followed by subseq:
The p command inserts the prototype for subseq at the cursor position:
Type a d a to select seq and type r days to replace it with days:
Type b d a to select start and type r (* n 3) to replace it with (* n 3):
Type b d a to select [end] and type r (* (1+ n) 3) to replace it with (* (1+ n) 3):
Finally type s to save the edited function.
Checking it works correctly:
> (day 4) "Fri"