(require 'pp)
(require 'ring)
(require 'cl-lib)
(defvar macrostep-overlays nil
"List of all macro stepper overlays in the current buffer.")
(make-variable-buffer-local 'macrostep-overlays)
(defvar macrostep-gensym-depth nil
"Number of macro expansion levels that have introduced gensyms so far.")
(make-variable-buffer-local 'macrostep-gensym-depth)
(defvar macrostep-gensyms-this-level nil
"t if gensyms have been encountered during current level of macro expansion.")
(make-variable-buffer-local 'macrostep-gensyms-this-level)
(defvar macrostep-saved-undo-list nil
"Saved value of `buffer-undo-list' upon entering macrostep mode.")
(make-variable-buffer-local 'macrostep-saved-undo-list)
(defvar macrostep-saved-read-only nil
"Saved value of `buffer-read-only' upon entering macrostep mode.")
(make-variable-buffer-local 'macrostep-saved-read-only)
(defvar macrostep-expansion-buffer nil
"Non-nil if the current buffer is a macro-expansion buffer.")
(make-variable-buffer-local 'macrostep-expansion-buffer)
(defvar macrostep-outer-environment nil
"Outermost macro-expansion environment to use in a macro-expansion buffers.
This variable is used to save information about any enclosing
`cl-macrolet' context when a macro form is expanded in a separate
buffer.")
(make-variable-buffer-local 'macrostep-outer-environment)
(defgroup macrostep nil
"Interactive macro stepper for Emacs Lisp."
:group 'lisp
:link '(emacs-commentary-link :tag "commentary" "macrostep.el")
:link '(emacs-library-link :tag "lisp file" "macrostep.el")
:link '(url-link :tag "web page" "https://github.com/joddie/macrostep"))
(defface macrostep-gensym-1
'((((min-colors 16581375)) :foreground "#8080c0" :box t :bold t)
(((min-colors 8)) :background "cyan")
(t :inverse-video t))
"Face for gensyms created in the first level of macro expansion."
:group 'macrostep)
(defface macrostep-gensym-2
'((((min-colors 16581375)) :foreground "#8fbc8f" :box t :bold t)
(((min-colors 8)) :background "#00cd00")
(t :inverse-video t))
"Face for gensyms created in the second level of macro expansion."
:group 'macrostep)
(defface macrostep-gensym-3
'((((min-colors 16581375)) :foreground "#daa520" :box t :bold t)
(((min-colors 8)) :background "yellow")
(t :inverse-video t))
"Face for gensyms created in the third level of macro expansion."
:group 'macrostep)
(defface macrostep-gensym-4
'((((min-colors 16581375)) :foreground "#cd5c5c" :box t :bold t)
(((min-colors 8)) :background "red")
(t :inverse-video t))
"Face for gensyms created in the fourth level of macro expansion."
:group 'macrostep)
(defface macrostep-gensym-5
'((((min-colors 16581375)) :foreground "#da70d6" :box t :bold t)
(((min-colors 8)) :background "magenta")
(t :inverse-video t))
"Face for gensyms created in the fifth level of macro expansion."
:group 'macrostep)
(defface macrostep-expansion-highlight-face
`((((min-colors 16581375) (background light))
,@(and (>= emacs-major-version 27) '(:extend t))
:background "#eee8d5")
(((min-colors 16581375) (background dark))
,@(and (>= emacs-major-version 27) '(:extend t))
:background "#222222"))
"Face for macro-expansion highlight."
:group 'macrostep)
(defface macrostep-macro-face
'((t :underline t))
"Face for macros in macro-expanded code."
:group 'macrostep)
(defface macrostep-compiler-macro-face
'((t :slant italic))
"Face for compiler macros in macro-expanded code."
:group 'macrostep)
(defcustom macrostep-expand-in-separate-buffer nil
"When non-nil, show expansions in a separate buffer instead of inline."
:group 'macrostep
:type 'boolean)
(defcustom macrostep-expand-compiler-macros t
"When non-nil, also expand compiler macros."
:group 'macrostep
:type 'boolean)
(defun macrostep-make-ring (&rest items)
"Make a ring containing all of ITEMS with no empty slots."
(let ((ring (make-ring (length items))))
(mapc (lambda (item) (ring-insert ring item)) (reverse items))
ring))
(defvar macrostep-gensym-faces
(macrostep-make-ring
'macrostep-gensym-1 'macrostep-gensym-2 'macrostep-gensym-3
'macrostep-gensym-4 'macrostep-gensym-5)
"Ring of all macrostepper faces for fontifying gensyms.")
(defvar macrostep-sexp-bounds-function
#'macrostep-sexp-bounds
"Function to return the bounds of the macro form nearest point.
It will be called with no arguments and should return a cons of
buffer positions, (START . END). It should use `save-excursion'
to avoid changing the position of point.
The default value, `macrostep-sexp-bounds', implements this for
Emacs Lisp, and may be suitable for other Lisp-like languages.")
(make-variable-buffer-local 'macrostep-sexp-bounds-function)
(defvar macrostep-sexp-at-point-function
#'macrostep-sexp-at-point
"Function to return the macro form at point for expansion.
It will be called with two arguments, the values of START and END
returned by `macrostep-sexp-bounds-function', and with point
positioned at START. It should return a value suitable for
passing as the first argument to `macrostep-expand-1-function'.
The default value, `macrostep-sexp-at-point', implements this for
Emacs Lisp, and may be suitable for other Lisp-like languages.")
(make-variable-buffer-local 'macrostep-sexp-at-point-function)
(defvar macrostep-environment-at-point-function
#'macrostep-environment-at-point
"Function to return the local macro-expansion environment at point.
It will be called with no arguments, and should return a value
suitable for passing as the second argument to
`macrostep-expand-1-function'.
The default value, `macrostep-environment-at-point', is specific
to Emacs Lisp. For languages which do not implement local
macro-expansion environments, this should be set to `ignore'
or `(lambda () nil)'.")
(make-variable-buffer-local 'macrostep-environment-at-point-function)
(defvar macrostep-expand-1-function
#'macrostep-expand-1
"Function to perform one step of macro-expansion.
It will be called with two arguments, FORM and ENVIRONMENT, the
return values of `macrostep-sexp-at-point-function' and
`macrostep-environment-at-point-function' respectively. It
should return the result of expanding FORM by one step as a value
which is suitable for passing as the argument to
`macrostep-print-function'.
The default value, `macrostep-expand-1', is specific to Emacs Lisp.")
(make-variable-buffer-local 'macrostep-expand-1-function)
(defvar macrostep-print-function
#'macrostep-pp
"Function to pretty-print macro expansions.
It will be called with two arguments, FORM and ENVIRONMENT, the
return values of `macrostep-sexp-at-point-function' and
`macrostep-environment-at-point-function' respectively. It
should insert a pretty-printed representation at point in the
current buffer, leaving point just after the inserted
representation, without altering any other text in the current
buffer.
The default value, `macrostep-pp', is specific to Emacs Lisp.")
(make-variable-buffer-local 'macrostep-print-function)
(defvar macrostep-macro-form-p-function
#'macrostep-macro-form-p
"Function to check whether a form is a macro call.
It will be called with two arguments, FORM and ENVIRONMENT -- the
return values of `macrostep-sexp-at-point-function' and
`macrostep-environment-at-point-function' respectively -- and
should return non-nil if FORM would undergo macro-expansion in
ENVIRONMENT.
This is called only from `macrostep-sexp-bounds', so it need not
be provided if a different value is used for
`macrostep-sexp-bounds-function'.
The default value, `macrostep-macro-form-p', is specific to Emacs Lisp.")
(make-variable-buffer-local 'macrostep-macro-form-p-function)
(defvar macrostep-keymap
(let ((map (make-sparse-keymap)))
(define-key map (kbd "RET") 'macrostep-expand)
(define-key map "=" 'macrostep-expand)
(define-key map "e" 'macrostep-expand)
(define-key map (kbd "DEL") 'macrostep-collapse)
(define-key map "u" 'macrostep-collapse)
(define-key map "c" 'macrostep-collapse)
(define-key map (kbd "TAB") 'macrostep-next-macro)
(define-key map "n" 'macrostep-next-macro)
(define-key map (kbd "M-TAB") 'macrostep-prev-macro)
(define-key map "p" 'macrostep-prev-macro)
(define-key map "q" 'macrostep-collapse-all)
(define-key map (kbd "C-c C-c") 'macrostep-collapse-all)
map)
"Keymap for `macrostep-mode'.")
(define-minor-mode macrostep-mode
"Minor mode for inline expansion of macros in Emacs Lisp source buffers.
\\<macrostep-keymap>Progressively expand macro forms with \\[macrostep-expand], collapse them with \\[macrostep-collapse],
and move back and forth with \\[macrostep-next-macro] and \\[macrostep-prev-macro].
Use \\[macrostep-collapse-all] or collapse all visible expansions to
quit and return to normal editing.
\\{macrostep-keymap}"
:init-value nil
:lighter " Macro-Stepper"
:keymap macrostep-keymap
:group macrostep
(if macrostep-mode
(progn
(setq macrostep-saved-undo-list buffer-undo-list
buffer-undo-list t)
(setq macrostep-saved-read-only buffer-read-only
buffer-read-only t)
(add-hook 'post-command-hook 'macrostep-command-hook nil t)
(message
(substitute-command-keys
"\\<macrostep-keymap>Entering macro stepper mode. Use \\[macrostep-expand] to expand, \\[macrostep-collapse] to collapse, \\[macrostep-collapse-all] to exit.")))
(if macrostep-expansion-buffer
(quit-window t)
(when macrostep-overlays (macrostep-collapse-all))
(setq buffer-undo-list macrostep-saved-undo-list
buffer-read-only macrostep-saved-read-only
macrostep-saved-undo-list nil)
(remove-hook 'post-command-hook 'macrostep-command-hook t))))
(defun macrostep-command-hook ()
(if (not buffer-read-only)
(macrostep-mode 0)))
(defun macrostep-expand (&optional toggle-separate-buffer)
"Expand the macro form following point by one step.
Enters `macrostep-mode' if it is not already active, making the
buffer temporarily read-only. If macrostep-mode is active and the
form following point is not a macro form, search forward in the
buffer and expand the next macro form found, if any.
With a prefix argument, the expansion is displayed in a separate
buffer instead of inline in the current buffer. Setting
`macrostep-expand-in-separate-buffer' to non-nil swaps these two
behaviors."
(interactive "P")
(cl-destructuring-bind (start . end)
(funcall macrostep-sexp-bounds-function)
(goto-char start)
(let* ((sexp (funcall macrostep-sexp-at-point-function start end))
(end (copy-marker end))
(text (buffer-substring start end))
(env (funcall macrostep-environment-at-point-function))
(expansion (funcall macrostep-expand-1-function sexp env)))
(let ((separate-buffer-p
(if toggle-separate-buffer
(not macrostep-expand-in-separate-buffer)
macrostep-expand-in-separate-buffer)))
(when (and separate-buffer-p (not macrostep-expansion-buffer))
(let ((mode major-mode)
(buffer
(get-buffer-create (generate-new-buffer-name "*macro expansion*"))))
(set-buffer buffer)
(funcall mode)
(setq macrostep-expansion-buffer t)
(setq macrostep-outer-environment env)
(save-excursion
(setq start (point))
(insert text)
(setq end (point-marker)))
(pop-to-buffer buffer))))
(unless macrostep-mode (macrostep-mode t))
(let ((existing-overlay (macrostep-overlay-at-point))
(macrostep-gensym-depth macrostep-gensym-depth)
(macrostep-gensyms-this-level nil)
priority)
(if existing-overlay
(progn (setq priority (1+ (overlay-get existing-overlay 'priority)))
(setq macrostep-gensym-depth
(overlay-get existing-overlay 'macrostep-gensym-depth)))
(setq priority 1)
(setq macrostep-gensym-depth -1))
(with-silent-modifications
(atomic-change-group
(let ((inhibit-read-only t))
(save-excursion
(funcall macrostep-print-function expansion env)
(macrostep-collapse-overlays-in (point) end)
(delete-region (point) end)
(let* ((overlay
(make-overlay start
(if (looking-at "\n")
(1+ (point))
(point))))
(highlight-overlay (unless macrostep-expansion-buffer
(copy-overlay overlay))))
(unless macrostep-expansion-buffer
(overlay-put highlight-overlay 'face 'macrostep-expansion-highlight-face)
(overlay-put highlight-overlay 'priority -1)
(overlay-put overlay 'macrostep-highlight-overlay highlight-overlay))
(overlay-put overlay 'priority priority)
(overlay-put overlay 'macrostep-original-text text)
(overlay-put overlay 'macrostep-gensym-depth macrostep-gensym-depth)
(push overlay macrostep-overlays))))))))))
(defun macrostep-collapse ()
"Collapse the innermost macro expansion near point to its source text.
If no more macro expansions are visible after this, exit
`macrostep-mode'."
(interactive)
(let ((overlay (macrostep-overlay-at-point)))
(when (not overlay) (error "No macro expansion at point"))
(let ((inhibit-read-only t))
(with-silent-modifications
(atomic-change-group
(macrostep-collapse-overlay overlay)))))
(if (not macrostep-overlays)
(macrostep-mode 0)))
(defun macrostep-collapse-all ()
"Collapse all visible macro expansions and exit `macrostep-mode'."
(interactive)
(let ((inhibit-read-only t))
(with-silent-modifications
(dolist (overlay macrostep-overlays)
(let ((outermost (= (overlay-get overlay 'priority) 1)))
(macrostep-collapse-overlay overlay (not outermost))))))
(setq macrostep-overlays nil)
(macrostep-mode 0))
(defun macrostep-next-macro ()
"Move point forward to the next macro form in macro-expanded text."
(interactive)
(let* ((start
(if (get-text-property (point) 'macrostep-macro-start)
(1+ (point))
(point)))
(next (next-single-property-change start 'macrostep-macro-start)))
(if next
(goto-char next)
(error "No more macro forms found"))))
(defun macrostep-prev-macro ()
"Move point back to the previous macro form in macro-expanded text."
(interactive)
(let (prev)
(save-excursion
(while
(progn
(setq prev
(previous-single-property-change (point) 'macrostep-macro-start))
(if (or (not prev)
(get-text-property (1- prev) 'macrostep-macro-start))
nil
(prog1 t (goto-char prev))))))
(if prev
(goto-char (1- prev))
(error "No previous macro form found"))))
(defun macrostep-overlay-at-point ()
"Return the innermost macro stepper overlay at point."
(let ((result
(get-char-property-and-overlay (point) 'macrostep-original-text)))
(cdr result)))
(defun macrostep-collapse-overlay (overlay &optional no-restore-p)
"Collapse a macro-expansion overlay and restore the unexpanded source text.
As a minor optimization, does not restore the original source
text if NO-RESTORE-P is non-nil. This is safe to do when
collapsing all the sub-expansions of an outer overlay, since the
outer overlay will restore the original source itself.
Also removes the overlay from `macrostep-overlays'."
(with-current-buffer (overlay-buffer overlay)
(unless no-restore-p
(let* ((start (overlay-start overlay))
(end (overlay-end overlay))
(text (overlay-get overlay 'macrostep-original-text))
(sexp-end
(copy-marker
(if (equal (char-before end) ?\n) (1- end) end))))
(macrostep-collapse-overlays-in start end)
(goto-char (overlay-start overlay))
(save-excursion
(insert text)
(delete-region (point) sexp-end))))
(setq macrostep-overlays
(delq overlay macrostep-overlays))
(let ((highlight-overlay (overlay-get overlay 'macrostep-highlight-overlay)))
(when highlight-overlay (delete-overlay highlight-overlay)))
(delete-overlay overlay)))
(defun macrostep-collapse-overlays-in (start end)
"Collapse all macrostepper overlays that are strictly between START and END.
Will not collapse overlays that begin at START and end at END."
(dolist (ol (overlays-in start end))
(when (and (overlay-buffer ol) (> (overlay-start ol) start)
(< (overlay-end ol) end)
(overlay-get ol 'macrostep-original-text))
(macrostep-collapse-overlay ol t))))
(defun macrostep-sexp-bounds ()
"Find the bounds of the macro form nearest point.
If point is not before an open-paren, moves up to the nearest
enclosing list. If the form at point is not a macro call,
attempts to move forward to the next macro form as determined by
`macrostep-macro-form-p-function'.
Returns a cons of buffer positions, (START . END)."
(save-excursion
(if (not (looking-at "[(`]"))
(backward-up-list 1))
(if (equal (char-before) ?`)
(backward-char))
(let ((sexp (funcall macrostep-sexp-at-point-function))
(env (funcall macrostep-environment-at-point-function)))
(unless (funcall macrostep-macro-form-p-function sexp env)
(condition-case nil
(macrostep-next-macro)
(error
(if (consp sexp)
(error "(%s ...) is not a macro form" (car sexp))
(error "Text at point is not a macro form"))))))
(cons (point) (scan-sexps (point) 1))))
(defun macrostep-sexp-at-point (&rest ignore)
"Return the sexp near point for purposes of macro-stepper expansion.
If the sexp near point is part of a macro expansion, returns the
saved text of the macro expansion, and does not read from the
buffer. This preserves uninterned symbols in the macro
expansion, so that they can be fontified consistently. (See
`macrostep-print-sexp'.)"
(or (get-text-property (point) 'macrostep-expanded-text)
(sexp-at-point)))
(defun macrostep-macro-form-p (form environment)
"Return non-nil if FORM would be evaluated via macro expansion.
If FORM is an invocation of a macro defined by `defmacro' or an
enclosing `cl-macrolet' form, return the symbol `macro'.
If `macrostep-expand-compiler-macros' is non-nil and FORM is a
call to a function with a compiler macro, return the symbol
`compiler-macro'.
Otherwise, return nil."
(car (macrostep--macro-form-info form environment t)))
(defun macrostep--macro-form-info (form environment &optional inhibit-autoload)
"Return information about macro definitions that apply to FORM.
If no macros are involved in the evaluation of FORM within
ENVIRONMENT, returns nil. Otherwise, returns a cons (TYPE
. DEFINITION).
If FORM would be evaluated by a macro defined by `defmacro',
`cl-macrolet', etc., TYPE is the symbol `macro' and DEFINITION is
the macro definition, as a function.
If `macrostep-expand-compiler-macros' is non-nil and FORM would
be compiled using a compiler macro, TYPE is the symbol
`compiler-macro' and DEFINITION is the function that implements
the compiler macro.
If FORM is an invocation of an autoloaded macro, the behavior
depends on the value of INHIBIT-AUTOLOAD. If INHIBIT-AUTOLOAD is
nil, the file containing the macro definition will be loaded
using `load-library' and the macro definition returned as normal.
If INHIBIT-AUTOLOAD is non-nil, no files will be loaded, and the
value of DEFINITION in the result will be nil."
(if (not (and (consp form)
(symbolp (car form))))
`(nil . nil)
(let* ((head (car form))
(local-definition (assoc-default head environment #'eq)))
(if local-definition
`(macro . ,local-definition)
(let ((compiler-macro-definition
(and macrostep-expand-compiler-macros
(or (get head 'compiler-macro)
(get head 'cl-compiler-macro)))))
(if (and compiler-macro-definition
(not (eq form
(apply compiler-macro-definition form (cdr form)))))
`(compiler-macro . ,compiler-macro-definition)
(condition-case nil
(let ((fun (indirect-function head)))
(cl-case (car-safe fun)
((macro)
`(macro . ,(cdr fun)))
((autoload)
(when (memq (nth 4 fun) '(macro t))
(if inhibit-autoload
`(macro . nil)
(load-library (nth 1 fun))
(macrostep--macro-form-info form nil))))
(t
`(nil . nil))))
(void-function nil))))))))
(defun macrostep-expand-1 (form environment)
"Return result of macro-expanding the top level of FORM by exactly one step.
Unlike `macroexpand', this function does not continue macro
expansion until a non-macro-call results."
(cl-destructuring-bind (type . definition)
(macrostep--macro-form-info form environment)
(cl-ecase type
((nil)
form)
((macro)
(apply definition (cdr form)))
((compiler-macro)
(let ((expansion
(apply definition form (cdr form))))
(if (equal form expansion)
(error "Form left unchanged by compiler macro")
expansion))))))
(put 'macrostep-grab-environment-failed 'error-conditions
'(macrostep-grab-environment-failed error))
(defun macrostep-environment-at-point ()
"Return the local macro-expansion environment at point, if any.
The local environment includes macros declared by any `macrolet'
or `cl-macrolet' forms surrounding point, as well as by any macro
forms which expand into a `macrolet'.
The return value is an alist of elements (NAME . FUNCTION), where
NAME is the symbol locally bound to the macro and FUNCTION is the
lambda expression that returns its expansion."
(let ((saved-environment
(get-text-property (point) 'macrostep-environment)))
(if saved-environment
saved-environment
(save-excursion
(catch 'done
(while t
(condition-case nil
(throw 'done (macrostep-environment-at-point-1))
(macrostep-grab-environment-failed
(condition-case nil
(backward-sexp)
(scan-error (backward-up-list)))))))))))
(defun macrostep-environment-at-point-1 ()
"Attempt to extract the macro environment that would be active at point.
If point is not at an evaluated position within the containing
form, raise an error."
(let* ((point-at-top-level
(save-excursion
(while (ignore-errors (backward-up-list) t))
(point)))
(enclosing-form
(buffer-substring point-at-top-level
(scan-sexps point-at-top-level 1)))
(position (- (point) point-at-top-level))
(tag (make-symbol "macrostep-grab-environment-tag"))
(grab-environment '--macrostep-grab-environment--))
(if (= position 0)
nil
(with-temp-buffer
(emacs-lisp-mode)
(insert enclosing-form)
(goto-char (+ (point-min) position))
(prin1 `(,grab-environment) (current-buffer))
(let ((form (read (copy-marker (point-min)))))
(catch tag
(cl-letf (((symbol-function #'message) (symbol-function #'format)))
(with-no-warnings
(ignore-errors
(macroexpand-all
`(cl-macrolet ((,grab-environment (&environment env)
(throw ',tag env)))
,form)))))
(signal 'macrostep-grab-environment-failed nil)))))))
(defun macrostep-collect-macro-forms (form &optional environment)
"Identify sub-forms of FORM which undergo macro-expansion.
FORM is an Emacs Lisp form. ENVIRONMENT is a local environment of
macro definitions.
The return value is a list of two elements, (MACRO-FORM-ALIST
COMPILER-MACRO-FORMS).
MACRO-FORM-ALIST is an alist of elements of the form (SUBFORM
. ENVIRONMENT), where SUBFORM is a form which undergoes
macro-expansion in the course of expanding FORM, and ENVIRONMENT
is the local macro environment in force when it is expanded.
COMPILER-MACRO-FORMS is a list of subforms which would be
compiled using a compiler macro. Since there is no standard way
to provide a local compiler-macro definition in Emacs Lisp, no
corresponding local environments are collected for these.
Forms and environments are extracted from FORM by instrumenting
Emacs's builtin `macroexpand' function and calling
`macroexpand-all'."
(let ((real-macroexpand (indirect-function #'macroexpand))
(macro-form-alist '())
(compiler-macro-forms '()))
(cl-letf
(((symbol-function #'macroexpand)
(lambda (form environment &rest args)
(let ((expansion
(apply real-macroexpand form environment args)))
(cond ((not (eq expansion form))
(setq macro-form-alist
(cons (cons form environment)
macro-form-alist)))
((and (consp form)
(symbolp (car form))
macrostep-expand-compiler-macros
(not (eq form
(cl-compiler-macroexpand form))))
(setq compiler-macro-forms
(cons form compiler-macro-forms))))
expansion))))
(ignore-errors
(macroexpand-all form environment)))
(list macro-form-alist compiler-macro-forms)))
(defvar macrostep-collected-macro-form-alist nil
"An alist of macro forms and environments.
Controls the printing of sub-forms in `macrostep-print-sexp'.")
(defvar macrostep-collected-compiler-macro-forms nil
"A list of compiler-macro forms to be highlighted in `macrostep-print-sexp'.")
(defun macrostep-pp (sexp environment)
"Pretty-print SEXP, fontifying macro forms and uninterned symbols."
(cl-destructuring-bind
(macrostep-collected-macro-form-alist
macrostep-collected-compiler-macro-forms)
(macrostep-collect-macro-forms sexp environment)
(let ((print-quoted t))
(macrostep-print-sexp sexp)
(save-restriction
(narrow-to-region (scan-sexps (point) -1) (point))
(save-excursion
(pp-buffer)
(goto-char (point-max))
(delete-region
(point)
(save-excursion (skip-chars-backward " \t\n") (point))))
(widen)
(save-excursion
(backward-sexp)
(indent-sexp))))))
(defmacro macrostep-propertize (form &rest plist)
"Evaluate FORM, applying syntax properties in PLIST to any inserted text."
(declare (indent 1)
(debug (&rest form)))
(let ((start (make-symbol "start")))
`(let ((,start (point)))
(prog1
,form
,@(cl-loop for (key value) on plist by #'cddr
collect `(put-text-property ,start (point)
,key ,value))))))
(defun macrostep-print-sexp (sexp)
"Insert SEXP like `print', fontifying macro forms and uninterned symbols.
Fontifies uninterned symbols and macro forms using
`font-lock-face' property, and saves the actual text of SEXP's
sub-forms as the `macrostep-expanded-text' text property so that
any uninterned symbols can be reused in macro expansions of the
sub-forms. See also `macrostep-sexp-at-point'.
Macro and compiler-macro forms within SEXP are identified by
comparison with the `macrostep-collected-macro-form-alist' and
`macrostep-collected-compiler-macro-forms' variables, which
should be dynamically let-bound around calls to this function."
(cond
((symbolp sexp)
(if (not (eq sexp (intern-soft (symbol-name sexp))))
(macrostep-propertize
(prin1 sexp (current-buffer))
'font-lock-face (macrostep-get-gensym-face sexp))
(prin1 sexp (current-buffer))))
((listp sexp)
(let ((head (car sexp)))
(cond ((and (eq head 'quote) (= (length sexp) 2))
(insert "'")
(macrostep-print-sexp (cadr sexp)))
((and (eq head '\`) (= (length sexp) 2))
(if (assq sexp macrostep-collected-macro-form-alist)
(macrostep-propertize
(insert "`")
'macrostep-expanded-text sexp
'macrostep-macro-start t
'font-lock-face 'macrostep-macro-face)
(insert "`"))
(macrostep-print-sexp (cadr sexp)))
((and (memq head '(\, \,@)) (= (length sexp) 2))
(princ head (current-buffer))
(macrostep-print-sexp (cadr sexp)))
(t (cl-destructuring-bind (macro? . environment)
(or (assq sexp macrostep-collected-macro-form-alist)
'(nil . nil))
(let
((compiler-macro?
(memq sexp macrostep-collected-compiler-macro-forms)))
(if (or macro? compiler-macro?)
(progn
(macrostep-propertize
(insert "(")
'macrostep-macro-start t
'macrostep-expanded-text sexp
'macrostep-environment environment)
(macrostep-propertize
(macrostep-print-sexp head)
'font-lock-face
(if macro?
'macrostep-macro-face
'macrostep-compiler-macro-face)))
(insert "(")
(macrostep-print-sexp head))))
(setq sexp (cdr sexp))
(when sexp (insert " "))
(while sexp
(if (listp sexp)
(progn
(macrostep-print-sexp (car sexp))
(when (cdr sexp) (insert " "))
(setq sexp (cdr sexp)))
(insert ". ")
(macrostep-print-sexp sexp)
(setq sexp nil)))
(insert ")")))))
(t (prin1 sexp (current-buffer)))))
(defun macrostep-get-gensym-face (symbol)
"Return the face to use in fontifying SYMBOL in printed macro expansions.
All symbols introduced in the same level of macro expansion are
fontified using the same face (modulo the number of faces; see
`macrostep-gensym-faces')."
(or (get symbol 'macrostep-gensym-face)
(progn
(if (not macrostep-gensyms-this-level)
(setq macrostep-gensym-depth (1+ macrostep-gensym-depth)
macrostep-gensyms-this-level t))
(let ((face (ring-ref macrostep-gensym-faces macrostep-gensym-depth)))
(put symbol 'macrostep-gensym-face face)
face))))
(provide 'macrostep)