[comp.emacs] emacs macros to lisp

ciaran@hrc63.co.uk (Ciaran Byrne) (09/02/87)

Some people have expressed dissatisfaction with the keyboard macro
mechanism in GNU emacs, since a string of key-strokes is not the most
useful way of expressing a set of editing operations.

I agree with them.

What is really needed is a means of turning an editing sequence into
emacs lisp code (as one can in Gosling & Multics emaxen).

Here is a first attempt at a lisp-generating package.

It works by superimposing another editing command-loop on top of the
build-in one. This strategem is necessary because the command-history
mechanism only records commands that read args from the terminal 
(perhaps a later version of Gnumacs will let the user switch on
some sort of verbose command recording mechanism).

There are obviously some things that cannot be expanded directly into
lisp, for instance commands that read and act on raw keyboard input.

The only such commands that are handled are the incremental search
family, which are replaced by their equivalent non-incremental (excremental?-)
versions.

As this is a preliminary version, I would appreciate feedback.

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;
;;;; module: 	generate.el	
;;;; version: 	2.0
;;;; author: 	Ciaran A Byrne ciaran@gec-rl-hrc.co.uk
;;;; date:	2:Sept:87
;;;;
;;;;;;;;;;;;;;;;;;;; macro lisp expansion ;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;
;;;;	user commands:
;;;;		start-generating	- replaces start-kbd-macro ^X(
;;;;		stop-generating		-     "	   end-kbd-macro   ^X)
;;;;		expand-macro		- produces REAL emacs lisp code
;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(global-set-key "\^X(" 'start-generating)
(global-set-key "\^X)" 'stop-generating)

(defmacro caar (x) (list 'car (list 'car x)))
(defmacro cadr (x) (list 'car (list 'cdr x)))
(defmacro caadr (x) (list 'car (list 'car (list 'cdr x))))
(defmacro caddr (x) (list 'car (list 'cdr (list 'cdr x))))
(defmacro cadar (x) (list 'car (list 'cdr (list 'car x))))

(defmacro cdar (x) (list 'cdr (list 'car x)))
(defmacro cddr (l) "" (list 'cdr (list 'cdr l)))
(defmacro cdadr (x) (list 'cdr (list 'car (list 'cdr x))))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; replace macro bindings


(defvar gen-history '(first . last) "command-history subsection pair")
(defvar generate-on nil "true if recording commands")


(defun start-generating () "records commands issued
until the command stop-generating is invoked.

The recorded commands can be turned into emacs lisp using
the command expand-macro.

Keystrokes are echoed in the minibuffer to remind you that
you are doing something weird"

    (interactive)
    (if generate-on (message "Already generating !")
	(progn
	    (setq generate-on t)
	    (message "Started generating")
	    (rplaca gen-history command-history) ; note beginning of macro
	    (unwind-protect
		(command-loop-3)	; run soft command loop
		(stop-generating)
	    )
	)
    )
)

(defun stop-generating ()  "Ends command recording.
See also: start-generating
	  expand-macro"

    (interactive)
    (rplacd gen-history  command-history) ; note end of macro
    (message "Stopped generating")
    (setq generate-on nil)
)


(defun expand-macro (buffer fname doc) "Expands the most recently recorded command sequence
into emacs lisp. Outputs into BUFFER and calls the function NAME
with DOC string.

See also: start-generating, stop-generating"

    (interactive "sBuffer for expansion : 
SNew function name : 
sDoc string : ")

    (if generate-on (stop-generating))

    (let ( (macro (rev-sub-list gen-history)) )	; chop macro out

	(get-buffer-create buffer)
	(set-buffer buffer)
	(goto-char (point-max))
	(set-mark (point))

	(insert "\n(defun " (symbol-name fname) " () " )   ; function header
	(insert "\"" doc)
	(insert "\nmacroised by " (user-full-name))
	(insert " @ " (current-time-string)  "\"\n")
	(insert "\n(interactive)\n")

	(setq standard-output (get-buffer buffer))
	(mapcar 'print macro)
	
	(exchange-point-and-mark)

	(mapcar 'delete-matching-lines	; zap useless stuff
	    '(   "^$"
		 "start-generating"
		 "stop-generating"
		 "expand-macro"
		 "execute-extended-command nil"
		 ; etc ?
	      )
	)

	(narrow-to-region (point) (point-max))
	(emacs-lisp-mode)
	(indent-region (point) (point-max) nil)	; neaten it all up

	(mapcar 'merge-multiple-numeric-args
	    '(
		 previous-line
		 next-line
		 delete-backward-char
		 backward-delete-char-untabify
		 backward-kill-word
		 kill-word
		 forward-char
		 backward-char
		 ; etc ?
	     ))

	(goto-char (point-max))
	(insert "\n)\n")
	(widen)
  
    );let
)

(defun rev-sub-list (pp) "returns sublist from INTERVAL eg. (beginning . end) ,
where beginning & end point into the same list.
The item at end should be nearer the front of the list.
The car of the result is the element at beginning."

    (let ( (stop (car pp))
	   (here (cdr pp))
	   (result nil)  )

	(if (not (memq (car stop) here)) (message "bad arg to rev-sub-list")
	    (while (not (eq here stop))
		(setq result (cons (car here) result)) ; build in reverse
		(setq here (cdr here)))
	)
	result
    )
)

(defun command-loop-3 () "Mimics the internal command_loop_1,
but locks the RECORD arg to command-execute to true.

Handles universal & prefix arguments, fakes self-insert-command.

Fixes up incremental searches in command-history so that the non-incremental
versions are used instead
"
    (while generate-on			; global flag

	(if (null (input-pending-p)) (sit-for 2))

	(let* ( (ks (read-key-sequence ""))
		(last-command-char (string-to-char (substring ks -1)))
		(kc (key-binding ks)) )

	    (cond
		((eq kc 'universal-argument) (universal-argument))

		((eq kc 'digit-argument) (digit-argument prefix-arg))

		((eq kc 'self-insert-command) (log-self-insert prefix-arg))

		((eq kc 'stop-generating) (stop-generating))

		( t
		    (command-execute kc 'record)))

					; now patch search commands
	    (cond
		((eq kc 'isearch-forward)
		    (rplaca command-history
			(list 'search-forward search-last-string)))
		
		((eq kc 'isearch-backward)
		    (rplaca command-history
			(list 'search-backward search-last-string)))
		
		((eq kc 'isearch-forward-regexp)
		    (rplaca command-history
			(list 're-search-forward search-last-regexp)))
		
		((eq kc 'isearch-backward-regexp)
		    (rplaca command-history
			(list 're-search-backward search-last-regexp)))
	    )

	)
    )
)

(defun string-copy (s n) "returns STRING concatted N times"
    (let ( (res "") )
	(while (> n 0)
	    (setq res (concat res s))
	    (setq n (1- n)))
	res))
	   
(defun log-self-insert (n) "replaces self-insert-command (q.v.)
adds an insert command to command-history,
amalgamates the current insertion with a previous insert command
in command-history, if there is one."
    
    (setq n (if (integerp n) n 1))
    (let ((ins (string-copy (char-to-string last-input-char) n)))

	(insert ins)			;do the insertion
	
	; the comand-history may look like:
	; ( (insert "t") ... )
	; if, say, "o" is the last input char, change to just:
	; (insert "to")
	
	(if (eq 'insert (caar command-history))
	    (let* ( (prev (cadar command-history))
		    (str (concat prev ins)) )

		(rplacd (car command-history) (list str))
	    )
	    (setq command-history (cons (list 'insert ins) command-history)) 
	)
    )
)

(defconst numarg "[ \t]+\\([0-9]+\\)")

(defun merge-multiple-numeric-args (s) "coalesces a pair of lisp lines
invoking the same FUNCTION with a numeric arg so that
a single function with the 2 component args added is used instead.

e.g.
(previous-line 4)
(previous-line 1)	becomes just 	(previous-line 5)
"

    (goto-char (point-min))

    (if (symbolp s) (setq s (symbol-name s)))

    (while
	(re-search-forward 
	    (concat s numarg ".*\n[ \t]*(" s numarg) (point-max) t)

	(let* ( (md (match-data))
		(arg1 (buffer-substring (nth 2 md) (nth 3 md)))
		(arg2 (buffer-substring (nth 4 md) (nth 5 md)))  
		(newarg (+ (string-to-int arg1) (string-to-int arg2))) )

	    (delete-region (nth 0 md) (nth 1 md))
	    (insert s " " (int-to-string newarg))
	    (goto-char (nth 0 md))
	)
    )
)


;;;;;;;;;;;;;;;;;;;;;;;; end of generate.el ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;
;uucp  ...seismo!mcvax!ukc!gec-rl-hrc!ciaran
;
; My opinions are my own, but my onions belong to themselves.

jack@hpindda.HP.COM (Jack Repenning) (09/11/87)

			      I love it!

I would, however, add 
		      (run-hooks 'generate-hook)
at the end, and
		    (run-hooks 'expand-macro-hook)
at the end of expand-macro-hook.

There should always be a hook at load time, though I haven't thought
of a use for this particular one (yet:-).

I have thought of a use for expand-macro-hook: I want to
			(pop-to-buffer buffer)
at this point, so the lisp is on-screen.  Someone else might like to
		      (switch-to-buffer buffer)
or something else.

Jack Repenning
Hewlett-Packard
jack@hpda.hp.com