Personal emacs config
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

304 lines
12 KiB

  1. ;;; elmacro.el --- Convert keyboard macros to emacs lisp -*- lexical-binding: t -*-
  2. ;; Author: Philippe Vaucher <philippe.vaucher@gmail.com>
  3. ;; URL: https://github.com/Silex/elmacro
  4. ;; Package-Version: 1.1.1
  5. ;; Package-Commit: 5bf9ba6009226b95e5ba0f50489ccced475753e3
  6. ;; Keywords: macro, elisp, convenience
  7. ;; Version: 1.1.1
  8. ;; Package-Requires: ((s "1.11.0") (dash "2.13.0"))
  9. ;; This file is NOT part of GNU Emacs.
  10. ;; This program is free software; you can redistribute it and/or modify
  11. ;; it under the terms of the GNU General Public License as published by
  12. ;; the Free Software Foundation, either version 3 of the License, or
  13. ;; (at your option) any later version.
  14. ;; This program is distributed in the hope that it will be useful,
  15. ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  16. ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  17. ;; GNU General Public License for more details.
  18. ;; You should have received a copy of the GNU General Public License
  19. ;; along with this program. If not, see <http://www.gnu.org/licenses/>.
  20. ;;; Commentary:
  21. ;;; Code:
  22. (require 's)
  23. (require 'dash)
  24. (defgroup elmacro nil
  25. "Show macros as emacs lisp."
  26. :group 'keyboard
  27. :group 'convenience)
  28. (defvar elmacro-command-history '()
  29. "Where elmacro process commands from variable `command-history'.")
  30. (defcustom elmacro-processors '(elmacro-processor-filter-unwanted
  31. elmacro-processor-prettify-inserts
  32. elmacro-processor-concatenate-inserts
  33. elmacro-processor-handle-special-objects)
  34. "List of processors functions used to improve code listing.
  35. Each function is passed the list of commands meant to be displayed and
  36. is expected to return a modified list of commands."
  37. :group 'elmacro
  38. :type '(repeat symbol))
  39. (defcustom elmacro-show-last-commands-default 30
  40. "Number of commands shown by default in `elmacro-show-last-commands'."
  41. :group 'elmacro
  42. :type 'integer)
  43. (defcustom elmacro-additional-recorded-functions '(copy-file
  44. copy-directory
  45. rename-file
  46. delete-file
  47. make-directory)
  48. "List of non-interactive functions that you also want to be recorded.
  49. For example, `dired-copy-file' (the C key in dired) doesn't reads its
  50. arguments as an interactive specification, and thus the file name is
  51. never stored."
  52. :group 'elmacro
  53. :type '(repeat symbol))
  54. (defcustom elmacro-unwanted-commands-regexps '("^(ido.*)$" "^(smex)$")
  55. "Regexps used to filter unwanted commands."
  56. :group 'elmacro
  57. :type '(repeat regexp))
  58. (defcustom elmacro-special-objects '(("#<frame .+? \\(0x[0-9a-f]+\\)>" ",(elmacro-get-frame \"\\1\")")
  59. ("#<window \\([0-9]+\\).*?>" ",(elmacro-get-window \\1)")
  60. ("#<buffer \\(.+?\\)>" ",(get-buffer \"\\1\")"))
  61. "List of (regexp replacement) for special objects.
  62. This will be used as arguments for `replace-regexp-in-string'."
  63. :group 'elmacro
  64. :type '(repeat (list regexp string)))
  65. (defcustom elmacro-debug nil
  66. "Set to true to turn debugging in buffer \"* elmacro debug *\"."
  67. :group 'elmacro
  68. :type 'boolean)
  69. (defun elmacro-process-commands (history)
  70. "Apply `elmacro-processors' to HISTORY."
  71. (let ((commands (reverse history)))
  72. (--each elmacro-processors
  73. (setq commands (funcall it commands)))
  74. commands))
  75. (defun elmacro-pp-to-string (object)
  76. "Like `pp-to-string', but make sure all options are set like desired.
  77. Also handles nil as parameter for defuns."
  78. (let ((pp-escape-newlines t)
  79. (print-quoted t)
  80. (print-length nil)
  81. (print-level nil))
  82. (replace-regexp-in-string "\\((defun +[^ ]+\\) +nil" "\\1 ()" (pp-to-string object))))
  83. (defun elmacro-processor-filter-unwanted (commands)
  84. "Remove unwanted commands using `elmacro-unwanted-commands-regexps'"
  85. (--remove (let ((str (elmacro-pp-to-string it)))
  86. (--any? (s-matches? it str) elmacro-unwanted-commands-regexps))
  87. commands))
  88. (defun elmacro-processor-prettify-inserts (commands)
  89. "Transform all occurences of `self-insert-command' into `insert'."
  90. (let (result)
  91. (--each commands
  92. (-let (((previous-command previous-arg1 previous-arg2) (car result))
  93. ((current-command current-arg) it))
  94. (if (and (eq 'setq previous-command)
  95. (eq 'last-command-event previous-arg1)
  96. (eq 'self-insert-command current-command))
  97. (setcar result `(insert ,(make-string current-arg previous-arg2)))
  98. (!cons it result))))
  99. (reverse result)))
  100. (defun elmacro-processor-concatenate-inserts (commands)
  101. "Concatenate multiple inserts together"
  102. (let (result)
  103. (--each commands
  104. (-let (((previous-command previous-args) (car result))
  105. ((current-command current-args) it))
  106. (if (and (eq 'insert current-command) (eq 'insert previous-command))
  107. (setcar result `(insert ,(concat previous-args current-args)))
  108. (!cons it result))))
  109. (reverse result)))
  110. (defun elmacro-processor-handle-special-objects (commands)
  111. "Turn special objects into usable objects."
  112. (--map (let ((str (elmacro-pp-to-string it)))
  113. (--each elmacro-special-objects
  114. (-let (((regex rep) it))
  115. (setq str (replace-regexp-in-string regex rep str))))
  116. (condition-case nil
  117. (car (read-from-string (s-replace "'(" "`(" str)))
  118. (error `(ignore ,str))))
  119. commands))
  120. (defun elmacro-get-frame (name)
  121. "Return the frame named NAME."
  122. (--first (s-matches? (format "^#<frame .* %s>$" name) (elmacro-pp-to-string it))
  123. (frame-list)))
  124. (defun elmacro-get-window (n)
  125. "Return the window numbered N."
  126. (--first (s-matches? (format "^#<window %d " n) (elmacro-pp-to-string it))
  127. (window-list)))
  128. (defun elmacro-assert-enabled ()
  129. "Ensure `elmacro-mode' is turned on."
  130. (unless elmacro-mode
  131. (error "elmacro is turned off! do `M-x elmacro-mode' first.")))
  132. (defun elmacro-debug-message (s &rest args)
  133. (when elmacro-debug
  134. (with-current-buffer (get-buffer-create "* elmacro - debug *")
  135. (insert (apply #'format s args) "\n"))))
  136. (defun elmacro-setq-last-command-event ()
  137. "Return a sexp setting up `last-command-event'."
  138. (if (symbolp last-command-event)
  139. `(setq last-command-event ',last-command-event)
  140. `(setq last-command-event ,last-command-event)))
  141. (defun elmacro-record-command (advised-function function &optional record keys)
  142. "Advice for `call-interactively' which makes it temporarily record
  143. commands in variable `command-history'."
  144. (let ((original-record record)
  145. retval)
  146. (elmacro-debug-message "[%s] ----- START -----" function)
  147. (setq record (or original-record (not (minibufferp)))) ;; don't record when in minibuffer
  148. (elmacro-debug-message "[%s] before - history %s record %s original %s"
  149. function (car command-history) record original-record)
  150. (setq retval (funcall advised-function function record keys))
  151. (elmacro-debug-message "[%s] after - history %s" function (car command-history))
  152. (let* ((sexp (car command-history))
  153. (cmd (car sexp)))
  154. (when record
  155. (elmacro-debug-message "[%s] recording %s" function cmd)
  156. (when (or (eq cmd 'self-insert-command) (command-remapping 'self-insert-command))
  157. (!cons (elmacro-setq-last-command-event) elmacro-command-history))
  158. (!cons sexp elmacro-command-history)
  159. (!cdr command-history)
  160. (elmacro-debug-message "[%s] clean %s" function (car command-history)))
  161. (elmacro-debug-message "[%s] ----- STOP -----" function)
  162. retval)))
  163. (defun elmacro-quoted-arguments (args)
  164. "Helper to correctly quote functions arguments of `elmacro-additional-recorded-functions'."
  165. (--map-when (and (symbolp it)
  166. (not (keywordp it))
  167. (not (eq nil it))
  168. (not (eq t it)))
  169. `(quote ,it) args))
  170. (defun elmacro-make-advice-lambda (function)
  171. "Generate the `defadvice' lambda used to record FUNCTION.
  172. See the variable `elmacro-additional-recorded-functions'."
  173. `(lambda (&rest args)
  174. (!cons ,(list '\` (list function ',@(elmacro-quoted-arguments args)))
  175. elmacro-command-history)))
  176. (defun elmacro-mode-on ()
  177. "Turn elmacro mode on."
  178. (--each elmacro-additional-recorded-functions
  179. (advice-add it :before (elmacro-make-advice-lambda it)))
  180. (advice-add 'call-interactively :around #'elmacro-record-command))
  181. (defun elmacro-mode-off ()
  182. "Turn elmacro mode off."
  183. (--each elmacro-additional-recorded-functions
  184. (advice-remove it (elmacro-make-advice-lambda it)))
  185. (advice-remove 'call-interactively #'elmacro-record-command))
  186. (defun elmacro-make-defun (symbol commands)
  187. "Makes a function named SYMBOL containing COMMANDS."
  188. `(defun ,symbol ()
  189. (interactive)
  190. ,@commands))
  191. (defun elmacro-show-defun (name commands)
  192. "Create a buffer containing a defun named NAME from COMMANDS."
  193. (let* ((buffer (generate-new-buffer (format "* elmacro - %s *" name))))
  194. (set-buffer buffer)
  195. (erase-buffer)
  196. (insert (elmacro-pp-to-string (elmacro-make-defun (make-symbol name) commands)))
  197. (emacs-lisp-mode)
  198. (indent-region (point-min) (point-max))
  199. (pop-to-buffer buffer)
  200. (goto-char (point-min))))
  201. (defun elmacro-extract-last-macro (history)
  202. "Extract the last keyboard macro from HISTORY."
  203. (let ((starters '(start-kbd-macro kmacro-start-macro kmacro-start-macro-or-insert-counter))
  204. (finishers '(end-kbd-macro kmacro-end-macro kmacro-end-or-call-macro kmacro-end-and-call-macro)))
  205. (elmacro-process-commands (-drop 1 (--take-while (not (-contains? starters (car it)))
  206. (--drop-while (not (-contains? finishers (car it))) history))))))
  207. ;;;###autoload
  208. (defun elmacro-show-last-macro (name)
  209. "Show the last macro as emacs lisp with NAME."
  210. (interactive (list (read-string "Defun name: " "last-macro" nil "last-macro")))
  211. (elmacro-assert-enabled)
  212. (-if-let (commands (elmacro-extract-last-macro elmacro-command-history))
  213. (elmacro-show-defun name commands)
  214. (message "No macros found. Please record one before using this command (F3/F4).")))
  215. ;;;###autoload
  216. (defun elmacro-show-last-commands (&optional count)
  217. "Take the latest COUNT commands and show them as emacs lisp.
  218. This is basically a better version of `kmacro-edit-lossage'.
  219. The default number of commands shown is modifiable in variable
  220. `elmacro-show-last-commands-default'.
  221. You can also modify this number by using a numeric prefix argument or
  222. by using the universal argument, in which case it'll ask for how many
  223. in the minibuffer."
  224. (interactive
  225. (list
  226. (cond
  227. ((equal current-prefix-arg nil)
  228. elmacro-show-last-commands-default)
  229. ((equal current-prefix-arg '(4))
  230. (read-number "How many commands?" elmacro-show-last-commands-default))
  231. (t
  232. (prefix-numeric-value current-prefix-arg)))))
  233. (elmacro-assert-enabled)
  234. (elmacro-show-defun (format "last-%s-commands" count) (-take-last count (elmacro-process-commands elmacro-command-history))))
  235. ;;;###autoload
  236. (defun elmacro-clear-command-history ()
  237. "Clear the list of recorded commands."
  238. (interactive)
  239. (setq elmacro-command-history '()))
  240. ;;;###autoload
  241. (define-minor-mode elmacro-mode
  242. "Toggle emacs activity recording (elmacro mode).
  243. With a prefix argument ARG, enable elmacro mode if ARG is
  244. positive, and disable it otherwise. If called from Lisp, enable
  245. the mode if ARG is omitted or nil."
  246. nil
  247. " elmacro"
  248. nil
  249. :global t
  250. :group 'elmacro
  251. (if elmacro-mode
  252. (elmacro-mode-on)
  253. (elmacro-mode-off)))
  254. (provide 'elmacro)
  255. ;;; elmacro.el ends here