|
|
- ;;; macrostep.el --- interactive macro expander
-
- ;; Copyright (C) 2012-2015 Jon Oddie <j.j.oddie@gmail.com>
-
- ;; Author: joddie <j.j.oddie@gmail.com>
- ;; Maintainer: joddie <j.j.oddie@gmail.com>
- ;; Created: 16 January 2012
- ;; Updated: 07 December 2015
- ;; Version: 0.9
- ;; Keywords: lisp, languages, macro, debugging
- ;; Url: https://github.com/joddie/macrostep
- ;; Package-Requires: ((cl-lib "0.5"))
-
- ;; This file is NOT part of GNU Emacs.
-
- ;; This program is free software: you can redistribute it and/or
- ;; modify it under the terms of the GNU General Public License as
- ;; published by the Free Software Foundation, either version 3 of the
- ;; License, or (at your option) any later version.
- ;;
- ;; This program is distributed in the hope that it will be useful, but
- ;; WITHOUT ANY WARRANTY; without even the implied warranty of
- ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- ;; General Public License for more details.
- ;;
- ;; You should have received a copy of the GNU General Public License
- ;; along with this program. If not, see `http://www.gnu.org/licenses/'.
-
- ;;; Commentary:
-
- ;; `macrostep' is an Emacs minor mode for interactively stepping through
- ;; the expansion of macros in Emacs Lisp source code. It lets you see
- ;; exactly what happens at each step of the expansion process by
- ;; pretty-printing the expanded forms inline in the source buffer, which is
- ;; temporarily read-only while macro expansions are visible. You can
- ;; expand and collapse macro forms one step at a time, and evaluate or
- ;; instrument the expansions for debugging with Edebug as normal (but see
- ;; "Bugs and known limitations", below). Single-stepping through the
- ;; expansion is particularly useful for debugging macros that expand into
- ;; another macro form. These can be difficult to debug with Emacs'
- ;; built-in `macroexpand', which continues expansion until the top-level
- ;; form is no longer a macro call.
-
- ;; Both globally-visible macros as defined by `defmacro' and local macros
- ;; bound by `(cl-)macrolet' or another macro-defining form can be expanded.
- ;; Within macro expansions, calls to macros and compiler macros are
- ;; fontified specially: macro forms using `macrostep-macro-face', and
- ;; functions with compiler macros using `macrostep-compiler-macro-face'.
- ;; Uninterned symbols (gensyms) are fontified based on which step in the
- ;; expansion created them, to distinguish them both from normal symbols and
- ;; from other gensyms with the same print name.
-
- ;; As of version 0.9, it is also possible to extend `macrostep' to work
- ;; with other languages with macro systems in addition to Emacs Lisp. An
- ;; extension for Common Lisp (via SLIME) is in the works; contributions for
- ;; other languages are welcome. See "Extending macrostep" below for
- ;; details.
-
-
- ;; 1 Key-bindings and usage
- ;; ========================
-
- ;; The standard keybindings in `macrostep-mode' are the following:
-
- ;; e, =, RET : expand the macro form following point one step
- ;; c, u, DEL : collapse the form following point
- ;; q, C-c C-c: collapse all expanded forms and exit macrostep-mode
- ;; n, TAB : jump to the next macro form in the expansion
- ;; p, M-TAB : jump to the previous macro form in the expansion
-
- ;; It's not very useful to enable and disable macrostep-mode directly.
- ;; Instead, bind `macrostep-expand' to a key in `emacs-lisp-mode-map',
- ;; for example C-c e:
-
- ;; ,----
- ;; | (define-key emacs-lisp-mode-map (kbd "C-c e") 'macrostep-expand)
- ;; `----
-
- ;; You can then enter macrostep-mode and expand a macro form completely
- ;; by typing `C-c e e e ...' as many times as necessary.
-
- ;; Exit macrostep-mode by typing `q' or `C-c C-c', or by successively
- ;; typing `c' to collapse all surrounding expansions.
-
-
- ;; 2 Customization options
- ;; =======================
-
- ;; Type `M-x customize-group RET macrostep RET' to customize options and
- ;; faces.
-
- ;; To display macro expansions in a separate window, instead of inline in
- ;; the source buffer, customize `macrostep-expand-in-separate-buffer' to
- ;; `t'. The default is `nil'. Whichever default behavior is selected,
- ;; the alternative behavior can be obtained temporarily by giving a
- ;; prefix argument to `macrostep-expand'.
-
- ;; To have `macrostep' ignore compiler macros, customize
- ;; `macrostep-expand-compiler-macros' to `nil'. The default is `t'.
-
- ;; Customize the faces `macrostep-macro-face',
- ;; `macrostep-compiler-macro-face', and `macrostep-gensym-1' through
- ;; `macrostep-gensym-5' to alter the appearance of macro expansions.
-
-
- ;; 3 Locally-bound macros
- ;; ======================
-
- ;; As of version 0.9, `macrostep' can expand calls to a locally-bound
- ;; macro, whether defined by a surrounding `(cl-)macrolet' form, or by
- ;; another macro-defining macro. In other words, it is possible to
- ;; expand the inner `local-macro' forms in both the following examples,
- ;; whether `local-macro' is defined by an enclosing `cl-macrolet' --
-
- ;; ,----
- ;; | (cl-macrolet ((local-macro (&rest args)
- ;; | `(expansion of ,args)))
- ;; | (local-macro (do-something)))
- ;; `----
-
- ;; -- or by a macro which expands into `cl-macrolet', provided that its
- ;; definition of macro is evaluated prior to calling `macrostep-expand':
-
- ;; ,----
- ;; | (defmacro with-local-macro (&rest body)
- ;; | `(cl-macrolet ((local-macro (&rest args)
- ;; | `(expansion of ,args)))
- ;; | ,@body))
- ;; |
- ;; | (with-local-macro
- ;; | (local-macro (do something (else)))
- ;; `----
-
- ;; See the `with-js' macro in Emacs's `js.el' for a real example of the
- ;; latter kind of macro.
-
- ;; Expansion of locally-bound macros is implemented by instrumenting
- ;; Emacs Lisp's macro-expander to capture the environment at point. A
- ;; similar trick is used to detect macro- and compiler-macro calls within
- ;; expanded text so that they can be fontified accurately.
-
-
- ;; 4 Expanding sub-forms
- ;; =====================
-
- ;; By moving point around in the macro expansion using
- ;; `macrostep-next-macro' and `macrostep-prev-macro' (bound to the `n'
- ;; and `p' keys), it is possible to expand other macro calls within the
- ;; expansion before expanding the outermost form. This can sometimes be
- ;; useful, although it does not correspond to the real order of macro
- ;; expansion in Emacs Lisp, which proceeds by fully expanding the outer
- ;; form to a non-macro form before expanding sub-forms.
-
- ;; The main reason to expand sub-forms out of order is to help with
- ;; debugging macros which programmatically expand their arguments in
- ;; order to rewrite them. Expanding the arguments of such a macro lets
- ;; you visualise what the macro definition would compute via
- ;; `macroexpand-all'.
-
-
- ;; 5 Extending macrostep for other languages
- ;; =========================================
-
- ;; Since version 0.9, it is possible to extend macrostep to work with
- ;; other languages besides Emacs Lisp. In typical Emacs fashion, this is
- ;; implemented by setting buffer-local variables to different function
- ;; values. Six buffer-local variables define the language-specific part
- ;; of the implementation:
-
- ;; - `macrostep-sexp-bounds-function'
- ;; - `macrostep-sexp-at-point-function'
- ;; - `macrostep-environment-at-point-function'
- ;; - `macrostep-expand-1-function'
- ;; - `macrostep-print-function'
- ;; - `macrostep-macro-form-p-function'
-
- ;; Typically, an implementation for another language would set these
- ;; variables in a major-mode hook. See the docstrings of each variable
- ;; for details on how each one is called and what it should return. At a
- ;; minimum, another language implementation needs to provide
- ;; `macrostep-sexp-at-point-function', `macrostep-expand-1-function', and
- ;; `macrostep-print-function'. Lisp-like languages may be able to reuse
- ;; the default `macrostep-sexp-bounds-function' if they provide another
- ;; implementation of `macrostep-macro-form-p-function'. Languages which
- ;; do not implement locally-defined macros can set
- ;; `macrostep-environment-at-point-function' to `ignore'.
-
- ;; Note that the core `macrostep' machinery only interprets the return
- ;; value of `macrostep-sexp-bounds-function', so implementations for
- ;; other languages can use any internal representations of code and
- ;; environments which is convenient. Although the terminology is
- ;; Lisp-specific, there is no reason that implementations could not be
- ;; provided for non-Lisp languages with macro systems, provided there is
- ;; some way of identifying macro calls and calling the compiler /
- ;; preprocessor to obtain their expansions.
-
-
- ;; 6 Bugs and known limitations
- ;; ============================
-
- ;; You can evaluate and edebug macro-expanded forms and step through the
- ;; macro-expanded version, but the form that `eval-defun' and friends
- ;; read from the buffer won't have the uninterned symbols of the real
- ;; macro expansion. This will probably work OK with CL-style gensyms,
- ;; but may cause problems with `make-symbol' symbols if they have the
- ;; same print name as another symbol in the expansion. It's possible that
- ;; using `print-circle' and `print-gensym' could get around this.
-
- ;; Please send other bug reports and feature requests to the author.
-
-
- ;; 7 Acknowledgements
- ;; ==================
-
- ;; Thanks to:
- ;; - John Wiegley for fixing a bug with the face definitions under Emacs
- ;; 24 & for plugging macrostep in his [EmacsConf presentation]!
- ;; - George Kettleborough for bug reports, and patches to highlight the
- ;; expanded region and properly handle backquotes.
- ;; - Nic Ferrier for suggesting support for local definitions within
- ;; macrolet forms
- ;; - Luís Oliveira for suggesting and implementing SLIME support
-
- ;; `macrostep' was originally inspired by J. V. Toups's 'Deep Emacs Lisp'
- ;; articles ([part 1], [part 2], [screencast]).
-
- ;; [EmacsConf presentation] http://youtu.be/RvPFZL6NJNQ
-
- ;; [part 1]
- ;; http://dorophone.blogspot.co.uk/2011/04/deep-emacs-part-1.html
-
- ;; [part 2]
- ;; http://dorophone.blogspot.co.uk/2011/04/deep-emacs-lisp-part-2.html
-
- ;; [screencast]
- ;; http://dorophone.blogspot.co.uk/2011/05/monadic-parser-combinators-in-elisp.html
-
-
- ;; 8 Changelog
- ;; ===========
-
- ;; - v0.9, 2015-10-01:
- ;; - separate into Elisp-specific and generic components
- ;; - highlight and expand compiler macros
- ;; - improve local macro expansion and macro form identification by
- ;; instrumenting `macroexpand(-all)'
- ;; - v0.8, 2014-05-29: fix a bug with printing the first element of lists
- ;; - v0.7, 2014-05-11: expand locally-defined macros within
- ;; `(cl-)macrolet' forms
- ;; - v0.6, 2013-05-04: better handling of quote and backquote
- ;; - v0.5, 2013-04-16: highlight region, maintain cleaner buffer state
- ;; - v0.4, 2013-04-07: only enter macrostep-mode on successful
- ;; macro-expansion
- ;; - v0.3, 2012-10-30: print dotted lists correctly. autoload
- ;; definitions.
-
- ;;; Code:
-
- (require 'pp)
- (require 'ring)
- (eval-and-compile
- (require 'cl-lib nil t)
- (require 'cl-lib "lib/cl-lib"))
-
- ;;; Constants and dynamically bound variables
- (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 dedicated 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)
-
- ;;; Customization options and faces
- (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)) :background "#eee8d5")
- (((min-colors 16581375) (background dark)) :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, expand compiler macros as well as `defmacro' and `macrolet' macros."
- :group 'macrostep
- :type 'boolean)
-
- ;; Need the following for making the ring of faces
- (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.")
-
- ;; Other modes can enable macrostep by redefining these functions to
- ;; language-specific versions.
- (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)
-
- ;;; Define keymap and minor mode
- (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'.")
-
- ;;;###autoload
- (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}"
- nil " Macro-Stepper"
- :keymap macrostep-keymap
- :group macrostep
- (if macrostep-mode
- (progn
- ;; Disable recording of undo information
- (setq macrostep-saved-undo-list buffer-undo-list
- buffer-undo-list t)
- ;; Remember whether buffer was read-only
- (setq macrostep-saved-read-only buffer-read-only
- buffer-read-only t)
- ;; Set up post-command hook to bail out on leaving read-only
- (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.")))
-
- ;; Exiting mode
- (if macrostep-expansion-buffer
- ;; Kill dedicated expansion buffers
- (quit-window t)
- ;; Collapse any remaining overlays
- (when macrostep-overlays (macrostep-collapse-all))
- ;; Restore undo info & read-only state
- (setq buffer-undo-list macrostep-saved-undo-list
- buffer-read-only macrostep-saved-read-only
- macrostep-saved-undo-list nil)
- ;; Remove our post-command hook
- (remove-hook 'post-command-hook 'macrostep-command-hook t))))
-
- ;; Post-command hook: bail out of macrostep-mode if the user types C-x
- ;; C-q to make the buffer writable again.
- (defun macrostep-command-hook ()
- (if (not buffer-read-only)
- (macrostep-mode 0)))
-
- ;;; Interactive functions
- ;;;###autoload
- (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)))
-
- ;; Create a dedicated macro-expansion buffer and copy the text to
- ;; be expanded into it, if required
- (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 ; Expanding part of a previous macro-expansion
- (setq priority (1+ (overlay-get existing-overlay 'priority)))
- (setq macrostep-gensym-depth
- (overlay-get existing-overlay 'macrostep-gensym-depth)))
- ;; Expanding source buffer text
- (setq priority 1)
- (setq macrostep-gensym-depth -1))
-
- (with-silent-modifications
- (atomic-change-group
- (let ((inhibit-read-only t))
- (save-excursion
- ;; Insert expansion
- (funcall macrostep-print-function expansion env)
- ;; Delete the original form
- (macrostep-collapse-overlays-in (point) end)
- (delete-region (point) end)
- ;; Create a new overlay
- (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
- ;; Highlight the overlay in original source buffers only
- (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)))
- ;; We only need restore the original text for the outermost
- ;; overlays
- (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"))))
-
- ;;; Utility functions (not language-specific)
-
- (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)
- ;; If we're cleaning up we don't need to bother restoring text
- ;; or checking for inner overlays to delete
- (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))))
- ;; Remove overlay from the list and delete it
- (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) ; collapsing may delete other overlays
- (> (overlay-start ol) start)
- (< (overlay-end ol) end)
- (overlay-get ol 'macrostep-original-text))
- (macrostep-collapse-overlay ol t))))
-
- ;;; Emacs Lisp implementation
-
- (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)))
- ;; If this isn't a macro form, try to find the next one in the buffer
- (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."
- ;; If point is on a macro form within an expansion inserted by
- ;; `macrostep-print-sexp', a local environment may have been
- ;; previously saved as a text property.
- (let ((saved-environment
- (get-text-property (point) 'macrostep-environment)))
- (if saved-environment
- saved-environment
- ;; Otherwise, we (ab)use the macro-expander to return the
- ;; environment at point. If point is not at an evaluated
- ;; position in the containing form,
- ;; `macrostep-environment-at-point-1' will raise an error, and
- ;; we back up progressively through the containing forms until
- ;; it succeeds.
- (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."
- ;; Macro environments are extracted using Emacs Lisp's builtin
- ;; macro-expansion machinery. The form containing point is copied
- ;; to a temporary buffer, and a call to
- ;; `--macrostep-grab-environment--' is inserted at point. This
- ;; altered form is then fully macro-expanded, in an environment
- ;; where `--macrostep-grab-environment--' is defined as a macro
- ;; which throws the environment to a uniquely-generated tag.
- (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)
- ;; Point is now after the expanded form; pretty-print it
- (save-restriction
- (narrow-to-region (scan-sexps (point) -1) (point))
- (save-excursion
- (pp-buffer)
- ;; Remove the extra newline inserted by pp-buffer
- (goto-char (point-max))
- (delete-region
- (point)
- (save-excursion (skip-chars-backward " \t\n") (point))))
- ;; Indent the newly-inserted form in context
- (widen)
- (save-excursion
- (backward-sexp)
- (indent-sexp))))))
-
- ;; This must be defined before `macrostep-print-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)
- ;; Fontify gensyms
- (if (not (eq sexp (intern-soft (symbol-name sexp))))
- (macrostep-propertize
- (prin1 sexp (current-buffer))
- 'font-lock-face (macrostep-get-gensym-face sexp))
- ;; Print other symbols as normal
- (prin1 sexp (current-buffer))))
-
- ((listp sexp)
- ;; Print quoted and quasiquoted forms nicely.
- (let ((head (car sexp)))
- (cond ((and (eq head 'quote) ; quote
- (= (length sexp) 2))
- (insert "'")
- (macrostep-print-sexp (cadr sexp)))
-
- ((and (eq head '\`) ; backquote
- (= (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 '(\, \,@)) ; unquote
- (= (length sexp) 2))
- (princ head (current-buffer))
- (macrostep-print-sexp (cadr sexp)))
-
- (t ; other list form
- (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
- ;; Save the real expansion as a text property on the
- ;; opening paren
- (macrostep-propertize
- (insert "(")
- 'macrostep-macro-start t
- 'macrostep-expanded-text sexp
- 'macrostep-environment environment)
- ;; Fontify the head of the macro
- (macrostep-propertize
- (macrostep-print-sexp head)
- 'font-lock-face
- (if macro?
- 'macrostep-macro-face
- 'macrostep-compiler-macro-face)))
- ;; Not a macro form
- (insert "(")
- (macrostep-print-sexp head))))
-
- ;; Print remaining list elements
- (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)))
- ;; Print tail of dotted list
- (insert ". ")
- (macrostep-print-sexp sexp)
- (setq sexp nil)))
- (insert ")")))))
-
- ;; Print everything except symbols and lists as normal
- (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)
-
- ;;; macrostep.el ends here
|