;		A Simple Structure Editor (ASSE), Version 1.01
;			by
;		Jeffrey M. Jacobs
;		Copyright (c) 1986, CONSART Systems Inc.
;		All Rights Reserved.
;		Permission granted for non-commercial distribution.

;		CONSART Systems, Inc.
;		P.O. Box 3016, Manhattan Beach, CA 90266
;		(213)376-3802
;		CompuServe: 75076,2603
;		BIX: jeffjacobs
;		USENET: jjacobs@well.UUCP
;
; This is the code for the ASSE editor.  In many respects, it is a cram course
; in LISP programming.  Unlike some courses, this one starts out hard and gets
; easier. The code is laid out in roughly the order it was developed. The normal
; development cycle tends to be "middle out". Most of the early EDIT functions
; consist primarily of LISP primitives.  As development progresses, common
; sequences of calls are made into functions.  Normally we would go back and
; "fix" earlier functions to use the newer functions.  This WILL be done in the
; electronically distributed version, but for "educational" (and schedule)
; reasons, it was left as is.  So the "early" stuff is more difficult to
; read and understand.
;
; If you are new to LISP, it might be worthwhile to examine the code; there is
; a wide variety of LISP techniques used.  The techniques, functions, forms
; etc. are pre-COMMON LISP (CL) and should work in any LISP, including .
; SCHEME. The only exception to this is DEFMACRO, which is NOT the same as the
; pre-CL MACRO (so much for the compatibility of CL).  These are written to
; be easy to change to FEXPR's; just delete the &REST from the variable list.
;
; ASSE was developed using XLISP 1.6 on a Mac.  Areas where you might need to
; change things are indicated by comments beginning with ";*".  There aren't.
; many...			           - Jeff Jacobs, CONSART Systems Inc.

; The following functions are for convenience and readability

(DEFUN SUB1 (X) (1- X))	;* I like SUB1.

(DEFUN NEQ (X Y) (NOT (EQ X Y))) ;* "NOT EQ"...

;* SPECIAL is not defined or available in XLISP.

(DEFMACRO SPECIAL (&REST X) (PUTPROP (CAR X) T 'SPECIAL) NIL)  ;* XLISP.

;* Returns the function definition of an atom; NIL if undefined.
(DEFUN SYMBOL-FUNCTION (X) (SYMBOL-VALUE X)) ;* XLISP 1.6.

; MEMQ searches the list Y for an element EQ to X and returns the sublist
; starting with X.  For XLISP and Common LISP, MEMBER uses EQL, which is close
; enough.  Older LISP's MEMBER use EQUAL as the test, which is slower and
; could produce incorrect results.

(DEFUN MEMQ (X Y) (MEMBER X Y)) ;* XLISP and Common LISP 

; Declare the global variables to be SPECIAL and initialize them.

(SPECIAL *UNDO_LIST*)	; Contains all the information from destructive
(SETQ *UNDO_LIST* NIL)	; operations, i.e. smash_lists, edit_indicators, etc.

(SPECIAL *EDIT_CHAIN*)	; Contains the "navigation" chain, i.e. the previous
(SETQ *EDIT_CHAIN* NIL) ; POSitions visited.

(SPECIAL *EDIT_PRINT_LEVEL*) ; The level to which the P command prints.
(SETQ *EDIT_PRINT_LEVEL* (QUOTE 2)) ; I like 2...

(SPECIAL *LAST_TAIL*)	; Used to keep for results of the UP command for
(SETQ *LAST_TAIL* NIL)	; the "...".

(SPECIAL *EDIT_LAST*)	; The SAVED_STATE of the last edit, if necessary.	
(SETQ *EDIT_LAST* NIL)

; This is the entry into the guts of ASSE  "User interface" functions call
; EDIT_LIST, which initializes and saves globals, faking dynamic binding,
; which is missing in some LISPs, such as XLISP and SCHEME.  The caller
; is responsible for passing the correct initial states for the globals.
; Note that "break on error" should be disabled.  You will have to figure
; out how this is done in your LISP.

(DEFUN EDIT_LIST
       (EDIT_LIST UNDO_LIST)
       (PROG (SAVE_STATE RETURN_STATE SAVE_BRK)
             (SETQ SAVE_BRK *BREAKENABLE*)
             (SETQ *BREAKENABLE* NIL)	;* Turns off "break on error" in XLISP.
             (SETQ SAVE_STATE		; Save "current" values
                   (CONS *EDIT_CHAIN* *UNDO_LIST*) ) ;  of globals.
             (SETQ *EDIT_CHAIN* EDIT_LIST) ; Init 
             (SETQ *UNDO_LIST* UNDO_LIST)  ; globals.
             (SETQ RETURN_STATE		   ; Do the actual editing.
                   (LIST (EDIT_LIST1) *EDIT_CHAIN* *UNDO_LIST*) )
             (SETQ *EDIT_CHAIN* (CAR SAVE_STATE)) ; Restore previous values
             (SETQ *UNDO_LIST* (CDR SAVE_STATE))  ; of globals.	
             (SETQ *BREAKENABLE* SAVE_BRK) ; Restore state of"break on error".
             (RETURN RETURN_STATE) ) )

; All EDIT_LIST1 does is READ the user command and perform the appropriate
; action,usually dispatching to a function. The ERRSET catches any errors
; generated by the user, preventing the user from inadvertently being thrown
; back to the top level of LISP. If you don't understand it,  don't worry 
; about it.

(DEFUN EDIT_LIST1
       ()
       (PROG (COMMAND)
             LOOP
             (PRINC "*")
             (SETQ COMMAND (EDIT_GET_CMD))
             (ERRSET (COND ((NUMBERP COMMAND) (EDIT_SETTO_NTH COMMAND))
                           ((MEMQ COMMAND '(D DEL DELETE))
                            (EDIT_DEL) )
                           ((MEMQ COMMAND '(INSERT INS I))
                            (EDIT_INS_NTH) )
                           ((EQ COMMAND 'OK)
                            (SETQ *EDIT_EDIT_CHAIN* (LAST *EDIT_CHAIN*))
                            (RETURN 'OK) )
                           ((EQ COMMAND 'SAVE)
                            (SETQ *EDIT_CHAIN* (LAST *EDIT_CHAIN*))
                            (RETURN 'SAVE) )
                           ((EQ COMMAND 'P)
                            (EDIT_PRINT (CAR *EDIT_CHAIN*)
                                        *EDIT_PRINT_LEVEL* )
                            (TERPRI) )
                           ((EQ COMMAND 'PL)
                            (EDIT_PRINT (EDIT_CUR_EXP) (EDIT_GET_ARG)) )
                           ((MEMQ COMMAND '(EMB EMBED))
                            (EDIT_EMBED) )
                           ((EQ COMMAND 'PP)
                            (PP (EDIT_CUR_EXP)) )
                           ((MEMQ COMMAND '(R REP REPLACE))
                            (EDIT_REPLACE) )
                           ((MEMQ COMMAND '(EXTRACT EXT XTR))
                            (EDIT_EXTRACT) )
                           ((EQ COMMAND '??) (EDIT??))
                           ((EQ COMMAND '?) (EDIT?))
                           ((EQ COMMAND 'UP) (EDIT_UP))
                           ((EQ COMMAND 'UNDO) (EDIT_UNDO))
                           ((EQ COMMAND 'UNUNDO) (EDIT_UNUNDO))
                           ((EQ COMMAND 'REVERT) (EDIT_REVERT))
                           ((EQ COMMAND 'UNBLOCK) (EDIT_UNBLOCK))
                           ((EQ COMMAND 'TOP)
                            (SETQ *EDIT_CHAIN* (LAST *EDIT_CHAIN*)) )
                           ((MEMQ COMMAND '(NEXT NX N NEX))
                            (EDIT_NEXT) )
                           ((EQ COMMAND 'TEST)
                            (SETQ *UNDO_LIST*
                                  (CONS 'BLOCK *UNDO_LIST*) ) )
                           (T (EDIT_ERROR "?" COMMAND)) )
                     T )
             (GO LOOP) ) )

; EDIT_SETTO_NTH should be worked through and understood. This function consists
; entirely of LISP primitives and is good exercise.  Parts of this function
; are an obvious candidate for a separately defined function (see below).
; Note that there is only one form within the PROG.

(DEFUN EDIT_SETTO_NTH (N)
    (PROG (CUR_EXP_LEN)
          (COND ((ZEROP N)
                 (COND ((NULL (CDR *EDIT_CHAIN*))
                        (EDIT_ERROR "At TOP" 0) )
                       (T (SETQ *EDIT_CHAIN* (CDR *EDIT_CHAIN*))) ) )
                ((>= (SETQ CUR_EXP_LEN	; Save length of "current expression".
                           (LENGTH (CAR *EDIT_CHAIN*)) ) ;
                     (ABS N) )
                 (SETQ *EDIT_CHAIN*
                       (CONS (NTH (COND ((MINUSP N) (+ CUR_EXP_LEN N))
                                        (T (SUB1 N)) )
                                  (CAR *EDIT_CHAIN*) )
                             *EDIT_CHAIN* ) ) )
                (T (EDIT_ERROR "?" N)) ) ) )

; The following function simply returns the "current expression".  Otherwise
; we would have to look at miles and miles of (CAR *EDIT_CHAIN*).  Makes
; the rest of the code easier to read.

(DEFUN EDIT_CUR_EXP NIL (CAR *EDIT_CHAIN*))

; EDIT_DEL simply dispatches to the appropriate function based on the
; value of the argument. Most of the functions called by EDIT_LIST1 dispatch
; this way; future versions will also dispatch on the type as well.

; WARNING: EDIT_DEL_CURRENT is not defined, but, if it were, would delete the 
; current expression.  Something for you to try; see UP and NEXT for hints.

(DEFUN EDIT_DEL ()
    (PROG (N)
          (COND ((NUMBERP (SETQ N (EDIT_GET_ARG))) ; User arg must be numeric.
                 (COND ((ZEROP N) (EDIT_DEL_CURRENT))
                       ((EQ N 1) (EDIT_DEL_FIRST)) ; DEL 1 is a special case.
                       (T (EDIT_DEL_NTH N)) ) )
                (T (EDIT_ERROR "DEL-bad arg" N)) ) ) )

; You should also work your way through EDIT_DEL_NTH.  It deletes the nth 
; element if it exists.  Take careful note of the calculation for NTHCDR; it's
; tricky. Remember that the user specifies POSition "base 1", but NTHCDR is
; zero-based.

(DEFUN EDIT_DEL_NTH (N)
  (PROG (CUR_EXP_LEN POS)
        (SETQ CUR_EXP_LEN (LENGTH (EDIT_CUR_EXP)))
        (COND ((MINUSP (SETQ POS N))  ; Convert user arg to "absolute" POSition.
               (SETQ POS (+ CUR_EXP_LEN (+ 1 N))) ))
        (COND ((= POS 1) (EDIT_DEL_FIRST)) ; DEL 1 is a special case.
              ((>= CUR_EXP_LEN POS)
               (EDIT_SMASH (SETQ POS (NTHCDR (- POS 2) (EDIT_CUR_EXP)))
                           (CAR POS)
                           (CDDR POS) )
               (EDIT_CMD_DONE (LIST 'DEL N)))
              (T (EDIT_ERROR "DEL-bad arg" N)) ) ) )

; Even though this is a "special case", it's surprisingly simple.

(DEFUN EDIT_DEL_FIRST
       NIL
       (EDIT_SMASH (EDIT_CUR_EXP)
                   (CADR (EDIT_CUR_EXP))
                   (CDR (EDIT_CUR_EXP)) )
       (EDIT_SMASH (EDIT_CUR_EXP)
                   (CAR (EDIT_CUR_EXP))
                   (CDDR (EDIT_CUR_EXP)) )
       (EDIT_CMD_DONE (LIST 'DEL 1)) )

; Again, a simple dispatching function...

(DEFUN EDIT_INS_NTH
       NIL
       (PROG (WHAT ARG1 POS)
             (SETQ WHAT (EDIT_GET_ARG))
             (SETQ ARG1 (EDIT_GET_ARG))
             (SETQ POS (EDIT_GET_ARG))
             (COND ((NUMBERP POS)
                    (COND ((MEMQ ARG1 '(BEFORE BEF BF B))
                           (EDIT_INS_BEFORE WHAT POS) )
                          ((MEMQ ARG1 '(AFTER AFT AF A))
                           (EDIT_INS_AFTER WHAT POS) )
                          (T (EDIT_ERROR "Arg to INSERT must be BEFORE or AFTER"
                                         ARG1 )) ) )
                   (T (EDIT_ERROR "INSERT requires numeric arg"
                                  POS )) ) ) )

; EDIT_INS_BEFORE illustrates insertion.  Note the calculation of the POSition
; is now peformed by EDIT_VAL_NTH.  Compare this with EDIT_DEL_NTH.

(DEFUN EDIT_INS_BEFORE (WHAT WHERE)
 (PROG (POS Z)
       (COND ((SETQ POS (EDIT_VAL_NTH WHERE))(SETQ POS (SUB1 POS))) ; Before...
             (T(EDIT_ERROR "EDIT_INS_BEFORE-Illegal Position Specified" WHERE)))
       (COND ((ZEROP POS)
              (EDIT_SMASH (SETQ Z (EDIT_CUR_EXP))  WHAT  (CONS (CAR Z) (CDR Z)))
              (EDIT_CMD_DONE (LIST 'INSERT WHAT 'BEFORE  WHERE)))
             (T(EDIT_SMASH (SETQ Z (NTHCDR (SUB1 POS) (EDIT_CUR_EXP)))
                           (CAR Z)
                           (CONS WHAT (CDR Z)))
               (EDIT_CMD_DONE (LIST 'INSERT WHAT 'BEFORE WHERE))))))

; EDIT_INS_AFTER...

(DEFUN EDIT_INS_AFTER (WHAT WHERE)
 (PROG (POS Z)
       (COND ((SETQ POS (EDIT_VAL_NTH WHERE)))
             (T(EDIT_ERROR "EDIT_INS_AFTER-Illegal Position Specified" WHERE)))
       (COND ((ZEROP POS)
              (EDIT_SMASH (SETQ Z (EDIT_CUR_EXP))  WHAT  (CONS (CAR Z) (CDR Z)))
              (EDIT_CMD_DONE (LIST 'INSERT WHAT 'BEFORE  WHERE)))
             (T(EDIT_SMASH (SETQ Z (NTHCDR (SUB1 POS) (EDIT_CUR_EXP)))
                           (CAR Z)
                           (CONS WHAT (CDR Z)))
               (EDIT_CMD_DONE (LIST 'INSERT WHAT 'AFTER WHERE))))))


; This is the workhorse of the editor.  It takes the CONS cell to be modfied
; and the new CAR and CDR as it's arguments.  It saves the information on
; *UNDO_LIST* and then "smashes" the cell using RPLACA and RPLACD.

(DEFUN EDIT_SMASH
       (CELL NEW_CAR NEW_CDR)
       (COND ((ATOM CELL)
              (EDIT_ERROR "EDIT_SMASH-internal error, CELL must be CONS"
                          CELL ) )
             (T (SETQ *UNDO_LIST*
                      (CONS (LIST CELL (CAR CELL) (CDR CELL))
                            *UNDO_LIST* ) )
                (RPLACA CELL NEW_CAR)
                (RPLACD CELL NEW_CDR) ) ) )

; This saves the current state of the edit chain and the command issued by the
; user on *UNDO_LIST*.

(DEFUN EDIT_CMD_DONE
       (CMD)	; CMD is the "user's" command.
       (SETQ *UNDO_LIST*
             (CONS (CONS '*EDIT_CHAIN* *EDIT_CHAIN*)
                   *UNDO_LIST* ) )
       (SETQ *UNDO_LIST* (CONS CMD *UNDO_LIST*)) )

; The following function would have been nice to have earlier.  It takes
; a user supplied numerical argument and converts it to a positive, VALidated
; NTH position number for the current expression.  It is a predicate (test)
; that returns a number if the argument was VALid, or NIL if something was
; wrong.

(DEFUN EDIT_VAL_NTH
       (N)
       (COND ((NUMBERP N) (EDIT_NTH N)) (T NIL)) )

; Convert a POSition number and verify that it's within the current expression's
; length.  Called by EDIT_VAL_NTH.

(DEFUN EDIT_NTH
       (N)
       (PROG (POS CUR_EXP_LEN)
             (SETQ CUR_EXP_LEN (LENGTH (EDIT_CUR_EXP)))
             (COND ((MINUSP (SETQ POS N))
                    (SETQ POS (+ CUR_EXP_LEN (+ 1 N))) ))
             (RETURN (COND ((>= CUR_EXP_LEN POS) POS) (T NIL))) ) )

; Embed things in parentheses.  Calls a separate function to handle an arg of
; the type (n m).

(DEFUN EDIT_EMBED ()
       (PROG (ARG POS TMP)
             (COND ((AND (NUMBERP (SETQ ARG (EDIT_GET_ARG)))
                         (SETQ POS (EDIT_NTH ARG)) )
                    (EDIT_SMASH (SETQ TMP
                                      (NTHCDR (SUB1 POS) (EDIT_CUR_EXP)) )
                                (CONS (CAR TMP) NIL)
                                (CDR TMP) ) )
                   ((CONSP ARG) (EDIT_EMBD_RANGE ARG))
                   (T (EDIT_ERROR "EMBED-bad arg" ARG)) ) ) )
                   
; Embed a range of elements in parentheses.  Note the use of TMP1 and TMP2 to
; save values.  Called by EDIT_EMBED.

(DEFUN EDIT_EMBD_RANGE (ARG)
   (PROG (POS1 POS2 TMP1 TMP2)
    (COND ((AND (SETQ POS1 (EDIT_VAL_NTH (CAR ARG)))
                     (SETQ POS2 (EDIT_VAL_NTH (CADR ARG)))
                     (> POS2 POS1) )
                (EDIT_SMASH (SETQ TMP1
                                  (NTHCDR (SUB1 POS1) (EDIT_CUR_EXP)) )
                            (CONS (CAR TMP1) (CDR TMP1))
                            (CDR (SETQ TMP2
                                       (NTHCDR (SUB1 POS2) (EDIT_CUR_EXP)) )) )
                (EDIT_SMASH TMP2 (CAR TMP2) NIL)
                (EDIT_CMD_DONE (LIST 'EMBED ARG)) )
               (T (EDIT_ERROR "EDIT_EMBD_RANGE-bad arg" ARG)) ) ) )

; Remove a set of parentheses from the element specified by ARG. This should
; hopefully be fairly straightforward by now.  See the text and diagrams
; while examining the code...

(DEFUN EDIT_EXTRACT
       NIL
       (PROG (ARG POS CELL EXTRACTEE SAVE TMP)
             (COND ((SETQ POS
                          (EDIT_VAL_NTH (SETQ ARG (EDIT_GET_ARG))) )
                    (SETQ CELL
                          (NTHCDR (SUB1 POS) (EDIT_CUR_EXP)) )
                    (COND ((CONSP (SETQ EXTRACTEE (CAR CELL)))
                           (EDIT_SMASH EXTRACTEE
                                       (CAR EXTRACTEE)
                                       (CDR EXTRACTEE) )
                           (COND ((NULL (CDR EXTRACTEE))
                                  (EDIT_SMASH CELL (CAR EXTRACTEE) (CDR CELL)) )
                                 (T (SETQ SAVE (CDR CELL))
                                    (EDIT_SMASH CELL
                                                (CAR EXTRACTEE)
                                                (CDR EXTRACTEE) )
                                    (EDIT_SMASH (SETQ TMP (LAST EXTRACTEE))
                                                (CAR TMP)
                                                SAVE ) ) )
                           (EDIT_CMD_DONE (LIST 'EXTRACT ARG)) )
                          (T (EDIT_ERROR "EDIT_EXTRACT-bad arg" ARG)) ) )) ) )

; This is about as simple as things get.  It would be a lot messier if
; we used LISP primitives in place of EDIT_VAL_NTH...

(DEFUN EDIT_REPLACE
       NIL
       (PROG (WHERE WHAT POS TMP)
             (SETQ WHERE (EDIT_GET_ARG))
             (SETQ WHAT (EDIT_GET_ARG))
             (COND ((SETQ POS (EDIT_VAL_NTH WHERE))
                    (EDIT_SMASH (SETQ TMP
                                      (NTHCDR (SUB1 POS) (EDIT_CUR_EXP)) )
                                WHAT
                                (CDR TMP) )
                    (EDIT_CMD_DONE (LIST 'REPLACE WHERE WHAT)) )
                   (T (EDIT_ERROR "EDIT_REPLACE-bad arg" WHERE)) ) ) )

; Process the whole undo list and print out the user's commands.  This
; demonstrates "classic" LISP MAP function useage.  It applies (FUNCTION...
; to all of the CARs of *UNDO_LIST*.  Very elegant.  Note that the predicates
; EDIT_CMDP, EDIT_SMASHP and EDIT_CHAINP are defined below.

(DEFUN EDIT??
       NIL
       (MAPC (FUNCTION (LAMBDA (X)
                               (COND ((OR (EQ X 'BLOCK) (EDIT_CMDP X))
                                      (PRINT X) )) ))
             *UNDO_LIST* ) )

; EDIT? prints out a user specified number of commands.  Just a simple loop...

(DEFUN EDIT?
       NIL
       (PROG (COUNT LIST)
             (COND ((NOT (NUMBERP (SETQ COUNT (EDIT_GET_CMD))))
                    (EDIT_ERROR "EDIT?-bad arg" COUNT)
                    (RETURN NIL) ))
             (SETQ LIST *UNDO_LIST*)
             LOOP
             (COND ((OR (NULL LIST) (ZEROP COUNT))
                    (RETURN NIL) )
                   ((OR (EDIT_CMDP (CAR LIST))
                        (EQ (CAR LIST) 'BLOCK) )
                    (PRINT (CAR LIST))
                    (SETQ COUNT (SUB1 COUNT)) ) )
             (SETQ LIST (CDR LIST))
             (GO LOOP) ) )

; UP is explained in the text.  See the diagram. Note that if the result is
; a "tail", or sublist, *LAST_TAIL* is set.

(DEFUN EDIT_UP
       NIL
       (PROG (CUR_EXP HGHR_EXP)
             (COND ((NULL (SETQ HGHR_EXP (CADR *EDIT_CHAIN*)))
                    (EDIT_ERROR "Already at TOP" NIL) )
                   ((EQ (SETQ CUR_EXP (EDIT_CUR_EXP))
                        (CAR HGHR_EXP) )
                    (SETQ *LAST_TAIL* NIL)
                    (SETQ *EDIT_CHAIN* (CDR *EDIT_CHAIN*)) )
                   ((SETQ *LAST_TAIL* (MEMQ CUR_EXP HGHR_EXP))
                    (SETQ *EDIT_CHAIN*
                          (CONS *LAST_TAIL* (CDR *EDIT_CHAIN*)) ) )
                   ((SETQ *EDIT_CHAIN* (CDR *EDIT_CHAIN*))) ) ) )

(DEFUN EDIT_NEXT
       NIL
       (PROG (HGHR_EXP TMP)
             (COND ((NULL (SETQ HGHR_EXP (CADR *EDIT_CHAIN*)))
                    (EDIT_ERROR "No NEXT" NIL) )
                   ((AND (SETQ TMP (MEMQ (EDIT_CUR_EXP) HGHR_EXP))
                         (CONSP (CDR TMP)) )
                    (SETQ *EDIT_CHAIN*
                          (CONS (CADR TMP) (CDR *EDIT_CHAIN*)) ) )
                   (T (EDIT_ERROR "No NEXT" NIL)) ) ) )

; The following 3 predicates are used when searching *UNDO_LIST*.  EDIT_UNDOP,
; EDIT_SMASHP, and EDIT_CHAINP return "useful" values, i.e. either the
; undo list element if the predicate is true, or NIL if false.

; If X is an UNDO or UNUNDO command, return X, else NIL.

(DEFUN EDIT_UNDOP
       (X)
       (COND ((AND (CONSP X)
                   (MEMQ (CAR X) '(UNDO UNUNDO)) )
              X )) )

; If X is a smash_list, return it, otherwise NIL.

(DEFUN EDIT_SMASHP
       (X)
       (COND ((AND (CONSP X) (CONSP (CAR X))) X)
             (T NIL) ) )

; If X is a edit_chain indicator, return it, else NIL.

(DEFUN EDIT_CHAINP
       (X)
       (COND ((AND (CONSP X)
                   (EQ (CAR X) '*EDIT_CHAIN*) )
              X )) )

; If X is a command, return T, not X, else NIL.

(DEFUN EDIT_CMDP
       (X)
       (AND (CONSP X)
            (ATOM (CAR X))
            (NEQ (CAR X) '*EDIT_CHAIN*) ) )

; EDIT_UNDO undoes the last destructive command that was not an "undo" command,
; unless a BLOCK intervenes (or there is nothing to undo).  Note that UNDO
; is undoable by UNUNDO.

(DEFUN EDIT_UNDO ()
 (PROG (TMPLIST TMPCAR)
 (SETQ TMPLIST *UNDO_LIST*)
     LOOP
     (COND ((OR (NULL TMPLIST)
                (EQ (SETQ TMPCAR (CAR TMPLIST))
                    'BLOCK ) )
            (EDIT_ERROR "Nothing to UNDO" NIL) )
           ((AND (EDIT_CMDP TMPCAR)	; Must be a command
                 (NOT (EDIT_UNDOP TMPCAR)) ) ; but skip "undo" commands.
            (COND ((EDIT_CHAINP (CADR TMPLIST)) ; Verify that *UNDO_LIST* is ok.
                   (EDIT_UNDO1 (CDDR TMPLIST))	; Undo the previous command.
                   (EDIT_CMD_DONE (LIST 'UNDO))	; Save the UNDO command.
                   (SETQ *EDIT_CHAIN* (CDADR TMPLIST)) ; Reset *EDIT_CHAIN*.
                   (PRINT (LIST TMPCAR 'UNDONE)) )
                  (T (EDIT_ERROR "EDIT_UNDO-Garbaged *UNDO_LIST*, BIG trouble"
                                 NIL )) ) )
           (T (SETQ TMPLIST (CDR TMPLIST)) (GO LOOP)) ) ) )

; EDIT_UNDO1 actually performs the "undoing", using EDIT_SMASH to allow UNUNDO.

(DEFUN EDIT_UNDO1
       (LIST)
       (PROG (TMP)
             LOOP
             (COND ((SETQ TMP (EDIT_SMASHP (CAR LIST)))
                    (EDIT_SMASH (CAR TMP)
                                (CADR TMP)
                                (CADDR TMP) )
                    (SETQ LIST (CDR LIST))
                    (GO LOOP) )) ) )

; UNUNDO undoes the last command if and only if it was an UNDO command.

(DEFUN EDIT_UNUNDO ()
   (PROG (TMPLIST TMPCAR)
     (COND ((OR (NULL (SETQ TMPLIST *UNDO_LIST*))
                (EQ (SETQ TMPCAR (CAR TMPLIST)) 
                    'BLOCK ) )
            (EDIT_ERROR "No UNDO to UNDO" NIL) )
           ((EDIT_UNDOP TMPCAR)	; Is it an "undo" command?
            (COND ((EDIT_CHAINP (CADR TMPLIST))	; Verify that *UNDO_LIST* is ok.
                   (EDIT_UNDO1 (CDDR TMPLIST))	; Undo the UNDO.
                   (EDIT_CMD_DONE (LIST 'UNUNDO)) ; Save the UNUNDO command.
                   (SETQ *EDIT_CHAIN* (CDADR TMPLIST)) ; Restore *EDIT_CHAIN*.
                   (PRINT (LIST TMPCAR 'UNDONE)) )
                  (T (EDIT_ERROR "EDIT_UNDO-Garbaged *UNDO_LIST*, BIG trouble"
                                 NIL )) ) )
           (T (EDIT_ERROR "Last command was not UNDO" NIL)) ) ) )

; EDIT_REVERT restores everything that was done since the last TEST/BLOCK was
; issued.  REVERT is NOT undoable...

(DEFUN EDIT_REVERT
       NIL
       (PRINC "Are you SURE?")
       (COND ((MEMQ (READ) '(Y YE YES))
              (PROG (TMPLIST TMPCAR)
                    (SETQ TMPLIST *UNDO_LIST*)
                    LOOP
                    (COND ((OR (NULL TMPLIST)
                               (EQ (SETQ TMPCAR (CAR TMPLIST))
                                   'BLOCK ) )
                           (SETQ *UNDO_LIST* TMPLIST)
                           (RETURN NIL) )
                          ((EDIT_SMASHP TMPCAR)
                           (RPLACA (CAR TMPCAR) (CADR TMPCAR))
                           (RPLACD (CAR TMPCAR) (CADDR TMPCAR)) )
                          ((EDIT_CHAINP TMPCAR)
                           (SETQ *EDIT_CHAIN* (CDR TMPCAR)) ) )
                    (SETQ TMPLIST (CDR TMPLIST))
                    (GO LOOP) ) )) )

; Destructively removes a BLOCK from *UNDO_LIST*

(DEFUN EDIT_UNBLOCK
       NIL
       (PROG (TMPLIST)
             (COND ((NULL (SETQ TMPLIST *UNDO_LIST*))	; Check for nothing on
                    (RETURN NIL) )			; the list,
                   ((EQ (CAR *UNDO_LIST*) (QUOTE BLOCK)) ; or for BLOCK at the
                    (SETQ *UNDO_LIST* (CDR *UNDO_LIST*)) ) ) ; begining.
             LOOP
             (COND ((EQ (CADR TMPLIST) (QUOTE BLOCK)) ; If BLOCK found,
                    (RPLACD TMPLIST (CDDR TMPLIST))   ; remove it from the list.
                    (RETURN NIL) )
                   ((SETQ TMPLIST (CDR TMPLIST)) (GO LOOP)) ; Keep searching.
                   (T (EDIT_ERROR "No BLOCK found" NIL)) ) ) ) ; Report error.
 
                         
; The User Interface functions follow.  They are adequately described in the
; text. If your LISP is "old" and doesn't support DEFMACRO, just change it
; to a FEXPR and remove the &REST.

; NOTE that getting the function definition varies ridiculously from dialect
; to dialect.  In NIL, it's
;
; (SI:INTERPRETER-CLOSURE-LAMBDA (SYMBOL-FUNCTION FUN))
;
; You will have to change this for your LISP.  It shouldn't be as bad as NIL.
; If there is both a function definition and a saved state, EDITF assumes
; the saved state is the function.  (It probably should verify this...).


(DEFMACRO EDITF
          (&REST FUN)
          (SETQ FUN (CAR FUN)) ; Get the function name.
          (PROG (FUNDEF RESULT SAVED_STATE)
                (COND ((NULL (SETQ FUNDEF (SYMBOL-FUNCTION FUN))) ;*Change this?
                       (PRINT (LIST "No function definition for" FUN))
                       (RETURN NIL) )
                      ((SETQ SAVED_STATE
                             (GET FUN 'EDIT_SAVE) )
                       (SETQ RESULT
                             (EDIT_LIST (CAR SAVED_STATE)
                                        (CADR SAVED_STATE) ) ) )
                      (T (SETQ RESULT
                               (EDIT_LIST (CONS FUNDEF NIL) NIL) )) )
                (COND ((EQ (CAR RESULT) 'OK)
                       (REMPROP FUN 'EDIT_SAVE)
                       (PUTPROP '*EDIT_LAST* (CDR RESULT) 'EDIT_SAVE)
                       (RETURN (LIST 'QUOTE FUN)) )
                      ((EQ (CAR RESULT) 'SAVE)
                       (PUTPROP FUN (CDR RESULT) 'EDIT_SAVE)
                       (RETURN (LIST 'QUOTE FUN)) )
                      (T (PRINT "Unknown return from EDIT_LIST")
                         NIL ) ) ) )
                         
; EDITV is roughly the same as EDITF.  Note that in XLISP 1.6 they are the
; same.  As before, if you have "old" LISP, change to a FEXPR...

(DEFMACRO EDITV
          (&REST VAR)
          (SETQ VAR (CAR VAR))
          (PROG (VALUE RESULT SAVED_STATE)
                (COND ((ATOM (SETQ VALUE (SYMBOL-VALUE VAR)))
                       (PRINT (LIST "Value cannot be edited for" VAR))
                       (RETURN NIL) )
                      ((SETQ SAVED_STATE
                             (GET VAR 'EDIT_SAVE) )
                       (SETQ RESULT
                             (EDIT_LIST (CAR SAVED_STATE)
                                        (CADR SAVED_STATE) ) ) )
                      (T (SETQ RESULT
                               (EDIT_LIST (CONS VALUE NIL) NIL) )) )
                (COND ((EQ (CAR RESULT) 'OK)
                       (REMPROP VAR 'EDIT_SAVE)
                       (PUTPROP '*EDIT_LAST* (CDR RESULT) 'EDIT_SAVE)
                       (RETURN (LIST 'QUOTE VAR)) )
                      ((EQ (CAR RESULT) 'SAVE)
                       (PUTPROP VAR (CDR RESULT) 'EDIT_SAVE)
                       (RETURN (LIST 'QUOTE VAR)) )
                      (T (PRINT "Unknown return from EDIT_LIST")
                         NIL ) ) ) )

(DEFUN EDIT
       (EXPR)
       (PROG (RESULT SAVED_STATE)
             (COND ((NULL EXPR)
                    (COND ((SETQ SAVED_STATE
                                 (GET '*EDIT_LAST* 'EDIT_SAVE) )
                           (SETQ RESULT
                                 (EDIT_LIST (CAR SAVED_STATE)
                                            (CADR SAVED_STATE) ) ) )) )
                   ((CONSP EXPR)
                    (SETQ RESULT
                          (EDIT_LIST (CONS EXPR NIL) NIL) ) )
                   (T (PRINT "Nothing to EDIT") (RETURN NIL)) )
             (PUTPROP '*EDIT_LAST* (CDR RESULT) 'EDIT_SAVE)
             (RETURN (CAR (LAST (CADR RESULT)))) ) )

; This causes everything between the invocation of EDIT_ERROR and the
; ERRSET in EDIT_LIST1 to be thrown away.

(DEFUN EDIT_ERROR
       (STRING ARG)
       (ERROR STRING ARG)
       NIL )

; The I/O functions follow:

(DEFUN EDIT_PRINT
       (EXPR DEPTH)
       (COND ((EQ EXPR *LAST_TAIL*) (PRINC "...")))
       (PRINT_LEV EXPR DEPTH) )

(DEFUN PRINT_LEV
       (EXPR DEPTH)
       (COND ((ATOM EXPR) (PRIN1 EXPR))
             ((CONSP EXPR)
              (COND ((ZEROP DEPTH) (PRIN1 '&))
                    (T (PRINC "(")
                       (PRINT_LEV1 EXPR DEPTH)
                       (PRINC ")") ) ) ) ) )

(DEFUN PRINT_LEV1
       (EXPR DEPTH)
       (PROG (X)
             (SETQ X EXPR)
             LOOP
             (COND ((ATOM (CAR X)) (PRIN1 (CAR X)))
                   (T (PRINT_LEV (CAR X) (- DEPTH 1))) )
             (COND ((NULL (SETQ X (CDR X))) (RETURN))
                   (T (PRINC " ") (GO LOOP)) ) ) )

; Just a simple READ protected by an ERRSET to prevent accidental exits
; from the editor or a user induced break.

(DEFUN EDIT_GET_CMD
       NIL
       (PROG (X)
             LOOP
             (COND ((SETQ X (ERRSET (READ) NIL))
                    (RETURN (CAR X)) )
                   (T (PRINC "*") (GO LOOP)) ) ) )

(DEFUN EDIT_GET_ARG NIL (READ))
