Package: emacs;
Reported by: Alex Bochannek <alex <at> bochannek.com>
Date: Thu, 16 Feb 2023 08:19:02 UTC
Severity: wishlist
Tags: patch
Found in version 30.0.50
Done: Eli Zaretskii <eliz <at> gnu.org>
Bug is archived. No further changes may be made.
Message #5 received at submit <at> debbugs.gnu.org (full text, mbox):
From: Alex Bochannek <alex <at> bochannek.com> To: bug-gnu-emacs <at> gnu.org Subject: 30.0.50; [PATCH] New keyboard macro counter functions Date: Thu, 16 Feb 2023 00:17:25 -0800
[Message part 1 (text/plain, inline)]
Hello! I have been working on blog posts about keyboard macros and found that it would be useful to have comparison functions for the keyboard macro counter. I implemented two functions to load and save macro counter values from and to number registers; three comparison functions of the macro counter with a number register that conditionally increment the counter; three comparison functions of the macro counter with a prefix that terminate the macro execution. This simplifies handling multiple counters and conditional macro termination. I am attaching the changes to: emacs.texi kmacro.texi NEWS kmacro.el kmacro-tests.el I hope this functionality is useful and that I followed the coding and style standards. Thanks! Advanced keyboard macro counter commands for register integration and conditional macro termination * doc/emacs/emacs.texi (Top): Document advanced keyboard macro counter commands. * doc/emacs/kmacro.texi (Keyboard Macros, Keyboard Macro Counter): Document advanced keyboard macro counter commands. * etc/NEWS: Document advanced keyboard macro counter commands. * lisp/kmacro.el (kmacro-keymap, kmacro-reg-load-counter) (kmacro-reg-save-counter, kmacro-reg-add-counter-equal) (kmacro-reg-add-counter-equal, kmacro-reg-add-counter-less) (kmacro-reg-add-counter-greater, kmacro-reg-add-counter) (kmacro-quit-counter-equal, kmacro-quit-counter-less) (kmacro-quit-counter-greater, kmacro-quit-counter): Add advanced keyboard macro counter commands to kmacro keymap. Implement advanced keyboard macro counter commands. * test/lisp/kmacro-tests.el (kmacro-tests-test-reg-load) (kmacro-tests-test-reg-save) (kmacro-tests-test-reg-add-counter-equal-01) (kmacro-tests-test-reg-add-counter-equal-02) (kmacro-tests-test-reg-add-counter-equal-03) (kmacro-tests-test-reg-add-counter-equal-04) (kmacro-tests-test-reg-add-counter-less) (kmacro-tests-test-reg-add-counter-greater) (kmacro-tests-test-quit-counter-equal-01) (kmacro-tests-test-quit-counter-equal-02) (kmacro-tests-test-quit-counter-equal-03) (kmacro-tests-test-quit-counter-equal-04) (kmacro-tests-test-quit-counter-less) (kmacro-tests-test-quit-counter-greater): Implement unit tests for advanced keyboard macro counter commands.
[Message part 2 (text/x-patch, inline)]
diff --git a/doc/emacs/emacs.texi b/doc/emacs/emacs.texi index 7071ea44edd..2584dce8d44 100644 --- a/doc/emacs/emacs.texi +++ b/doc/emacs/emacs.texi @@ -434,6 +434,7 @@ Top * Basic Keyboard Macro:: Defining and running keyboard macros. * Keyboard Macro Ring:: Where previous keyboard macros are saved. * Keyboard Macro Counter:: Inserting incrementing numbers in macros. +* Advanced Macro Counter:: Advanced macro counter commands. * Keyboard Macro Query:: Making keyboard macros do different things each time. * Save Keyboard Macro:: Giving keyboard macros names; saving them in
[Message part 3 (text/x-patch, inline)]
diff --git a/doc/emacs/kmacro.texi b/doc/emacs/kmacro.texi index fc1402b489d..27c84c0f96f 100644 --- a/doc/emacs/kmacro.texi +++ b/doc/emacs/kmacro.texi @@ -35,6 +35,7 @@ Keyboard Macros * Basic Keyboard Macro:: Defining and running keyboard macros. * Keyboard Macro Ring:: Where previous keyboard macros are saved. * Keyboard Macro Counter:: Inserting incrementing numbers in macros. +* Advanced Macro Counter:: Advanced macro counter commands. * Keyboard Macro Query:: Making keyboard macros do different things each time. * Save Keyboard Macro:: Giving keyboard macros names; saving them in @@ -364,6 +365,123 @@ Keyboard Macro Counter keyboard macro counter. @xref{Number Registers}. For most purposes, it is simpler to use a keyboard macro counter. +@node Advanced Macro Counter +@section Advanced Macro Counter Commands + + The counter associated with a keyboard macro is sufficient in most +cases. If additional counters are required for a macro, registers can +be used and these advanced macro counter commands simplify the +interaction between the two. Additional commands are provided to +terminate a macro after a predefined number of runs. + + +@table @kbd +@item C-x C-k C-r l +Load the value of a number register into the macro counter +(@code{kmacro-reg-load-counter}). +@item C-x C-k C-r s +Save the value of the macro counter to a number register +(@code{kmacro-reg-save-counter}). +@end table + +@table @kbd +@item C-x C-k C-r a = +Compare if the macro counter is equal to the value of a register and +increment the counter if it is (@code{kmacro-reg-add-counter-equal}). +@item C-x C-k C-r a < +Compare if the macro counter is less than the value of a register and +increment the counter if it is (@code{kmacro-reg-add-counter-less}). +@item C-x C-k C-r a > +Compare if the macro counter is greater than the value of a register +and increment the counter if it is +(@code{kmacro-reg-add-counter-greater}). +@end table + +@table @kbd +@item C-x C-k C-q = +Compare if the macro counter is equal to the prefix and terminate the +macro if it is (@code{kmacro-quit-counter-equal}). +@item C-x C-k C-q < +Compare if the macro counter is less than the prefix and terminate the +macro if it is (@code{kmacro-quit-counter-less}). +@item C-x C-k C-q > +Compare if the macro counter is greater than the prefix and terminate +the macro if it is (@code{kmacro-quit-counter-greater}). +@end table + +@findex kmacro-reg-load-counter +@kindex C-x C-k C-r l +@findex kmacro-reg-save-counter +@kindex C-x C-k C-r s + The command @kbd{C-x C-k C-r l} (@code{kmacro-reg-load-counter}) +prompts for the register name from which to load a number into the +macro counter. The command @kbd{C-x C-k C-r s} +(@code{kmacro-reg-save-counter}) prompts for the register name into +which to save the macro counter's value. Both @kbd{C-x C-k C-r l} +(@code{kmacro-reg-load-counter}) and @kbd{C-x C-k C-r s} +(@code{kmacro-reg-save-counter}) show a preview of the registers by +default. @xref{Registers}. Both commands can be used during or +outside a keyboard macro definition. + +@findex kmacro-reg-add-counter-equal +@kindex C-x C-k C-r a = +@findex kmacro-reg-add-counter-less +@kindex C-x C-k C-r a < +@findex kmacro-reg-add-counter-greater +@kindex C-x C-k C-r a > + The @kbd{C-x C-k C-r a =} (@code{kmacro-reg-add-counter-equal}), +@kbd{C-x C-k C-r a <} (@code{kmacro-reg-add-counter-less}), and +@kbd{C-x C-k C-r a >} (@code{kmacro-reg-add-counter-greater}) commands +all follow the same pattern. During keyboard macro definition, the +command prompts for a register name (with preview by default), the +contents of which will be compared with the macro counter's value. If +the counter is equal to (@code{=}), less than (@code{<}), or greater +than (@code{>}) the number register's contents, the counter will be +incremented by the numeric prefix or one if no prefix was given to the +command. + + For example, + +@example +C-u 2 C-x C-k C-r a > N +@end example + +@noindent +compares the counter with the contents of register @code{N} and if the +counter is greater than that, increases it by two. + +@findex kmacro-quit-counter-equal +@kindex C-x C-k C-q = +@findex kmacro-quit-counter-less +@kindex C-x C-k C-q < +@findex kmacro-quit-counter-greater +@kindex C-x C-k C-q > + Finally, the @kbd{C-x C-k C-q =} (@code{kmacro-quit-counter-equal}), +@kbd{C-x C-k C-q <} (@code{kmacro-quit-counter-less}), and @kbd{C-x +C-k C-q >} (@code{kmacro-quit-counter-greater}) commands compare the +macro counter with the prefix given and terminate the execution of the +macro, if the comparison succeeds. If no numeric prefix or only +@code{C-u} are given, the counter will be compared with zero. The +macro is terminated using the @code{keyboard-quit} function. Using +this command to exit from a macro that has been called by another +macro is not supported, the entire executing macro is terminated. + + The quit commands can be used to construct the equivalent of a +@code{while}-loop. This example will stop after ten executions +assuming the starting value for the macro counter is the default zero. + +@example +C-u 10 C-x C-k C-q = C-x C-k C-i @key{RET} +@end example + + With the default counter value of zero, the macro called with a +prefix of @code{C-u C-u} to execute sixteen times, will stop after ten +iterations. The counter values that have been inserted will be from 0 +to 9. If the counter starts out a different value below ten, it will +still stop at ten, because the counter does not actually count macro +executions, but is incremented explicitly by the @code{C-x C-k C-i} +command. + @node Keyboard Macro Query @section Executing Macros with Variations
[Message part 4 (text/x-patch, inline)]
diff --git a/etc/NEWS b/etc/NEWS index 4fbe09e0541..d5a3ebb1df5 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -187,6 +187,25 @@ This command adds a docstring comment to the current defun. If a comment already exists, point is only moved to the comment. It is bound to 'C-c C-d' in 'go-ts-mode'. +** Kmacro + ++++ +*** New Advanced Macro Counter functions. +New commands have been added to to implement advanced macro counter +functions. + +The commands 'C-x C-k C-r l' and 'C-x C-k C-r s' load and save the +macro counter from a to a number register respectively. + +The commands 'C-x C-k C-r a =', 'C-x C-k C-r a <', and +'C-x C-k C-r a >' compare the macro counter with the contents of a +number register and increment the counter by a prefix if the +comparison succeeds. + +The commands 'C-x C-k C-q =', 'C-x C-k C-q <', and 'C-x C-k C-q >' +compare the macro counter with a prefix and terminate the macro if the +comparison succeeds. + * New Modes and Packages in Emacs 30.1
[Message part 5 (text/x-patch, inline)]
diff --git a/lisp/kmacro.el b/lisp/kmacro.el index 94d8794bd23..e7c3f75efd0 100644 --- a/lisp/kmacro.el +++ b/lisp/kmacro.el @@ -183,10 +183,18 @@ kmacro-keymap "C-l" #'kmacro-call-ring-2nd-repeat ;; macro counter - "C-f" #'kmacro-set-format - "C-c" #'kmacro-set-counter - "C-i" #'kmacro-insert-counter - "C-a" #'kmacro-add-counter + "C-f" #'kmacro-set-format + "C-c" #'kmacro-set-counter + "C-i" #'kmacro-insert-counter + "C-a" #'kmacro-add-counter + "C-r l" #'kmacro-reg-load-counter + "C-r s" #'kmacro-reg-save-counter + "C-r a =" #'kmacro-reg-add-counter-equal + "C-r a <" #'kmacro-reg-add-counter-less + "C-r a >" #'kmacro-reg-add-counter-greater + "C-q =" #'kmacro-quit-counter-equal + "C-q <" #'kmacro-quit-counter-less + "C-q >" #'kmacro-quit-counter-greater ;; macro editing "C-e" #'kmacro-edit-macro-repeat @@ -347,6 +355,89 @@ kmacro-add-counter (kmacro-display-counter))) +(defun kmacro-reg-load-counter (register) + "Load the value of a register into `kmacro-counter'" + (interactive + (list (register-read-with-preview "Load register to counter: "))) + (let ((register-val (get-register register))) + (when (numberp register-val) + (setq kmacro-counter register-val)))) + + +(defun kmacro-reg-save-counter (register) + "Save the value of `kmacro-counter' to a register" + (interactive + (list (register-read-with-preview "Save counter to register: "))) + (set-register register kmacro-counter)) + + +(defun kmacro-reg-add-counter-equal (&optional arg) + "Increment counter by ARG if it is equal to register value" + (interactive "p") + (let + ((register (register-read-with-preview "Compare counter to register: "))) + (kmacro-reg-add-counter '= register arg))) + + +(defun kmacro-reg-add-counter-less (&optional arg) + "Increment counter by ARG if it is less than register value" + (interactive "p") + (let + ((register (register-read-with-preview "Compare counter to register: "))) + (kmacro-reg-add-counter '< register arg))) + + +(defun kmacro-reg-add-counter-greater (&optional arg) + "Increment counter by ARG if it is greater than register value" + (interactive "p") + (let + ((register (register-read-with-preview "Compare counter to register: "))) + (kmacro-reg-add-counter '> register arg))) + + +(defun kmacro-reg-add-counter (func register &optional arg) + "Increment the counter by ARG if (FUNC kmacro-counter REGISTER-VALUE) +is true. +With no ARG, ARG is set to 1" + (let ((register-val (get-register register)) + (arg (if (null arg) 1 arg))) + (when (apply func (list kmacro-counter register-val)) + (setq current-prefix-arg nil) + (kmacro-add-counter arg)))) + + +(defun kmacro-quit-counter-equal (&optional arg) + "Quit the keyboard macro if the counter is equal to ARG" + (interactive "P") + (kmacro-quit-counter '= arg)) + + +(defun kmacro-quit-counter-less (&optional arg) + "Quit the keyboard macro if the counter is less than ARG" + (interactive "P") + (kmacro-quit-counter '< arg)) + + +(defun kmacro-quit-counter-greater (&optional arg) + "Quit the keyboard macro if the counter is greater than ARG" + (interactive "P") + (kmacro-quit-counter '> arg)) + + +(defun kmacro-quit-counter (func &optional arg) + "Quit the keyboard macro if (FUNC kmacro-counter ARG) is true. +With \\[universal-argument] or no ARG, ARG is set to 0" + (when kmacro-initial-counter-value + (setq kmacro-counter kmacro-initial-counter-value + kmacro-initial-counter-value nil)) + (let ((arg + (cond ((or (consp arg) (null arg)) 0) + ((eq '- arg) -1) + (t arg)))) + (when (apply func (list kmacro-counter arg)) + (keyboard-quit)))) + + (defun kmacro-loop-setup-function () "Function called prior to each iteration of macro." ;; Restore macro counter format to initial format, so it is ok to change
[Message part 6 (text/x-patch, inline)]
diff --git a/test/lisp/kmacro-tests.el b/test/lisp/kmacro-tests.el index 551fd8b60fc..e30681db539 100644 --- a/test/lisp/kmacro-tests.el +++ b/test/lisp/kmacro-tests.el @@ -275,6 +275,220 @@ kmacro-tests-start-insert-counter-appends-to-macro ;; Verify that the recording state has changed. (should (equal defining-kbd-macro 'append)))) + +(kmacro-tests-deftest kmacro-tests-test-reg-load () + "`kmacro-reg-load-counter' loads the value of register to into the counter" + (set-register ?\C-r 4) ;; Should be safe as a register name + (kmacro-tests-simulate-command '(kmacro-set-counter 1)) + (kmacro-tests-define-macro (vconcat + ;; Insert and increment counter + "\C-x\C-k\C-i" + "\C-x\C-k\C-i" + ;; Load from register + "\C-x\C-k\C-rl\C-r" + )) + (kmacro-tests-should-insert "1245" + (kmacro-tests-simulate-command '(kmacro-end-or-call-macro 2))) + (set-register ?\C-r nil)) + +(kmacro-tests-deftest kmacro-tests-test-reg-save () + "`kmacro-reg-save-counter' save the counter to a register" + (set-register ?\C-r nil) ;; Should be safe as a register name + (kmacro-tests-simulate-command '(kmacro-set-counter 1)) + (kmacro-tests-define-macro (vconcat + ;; Insert and increment counter + "\C-x\C-k\C-i" + ;; Save to register + "\C-x\C-k\C-rs\C-r" + ;; Add to counter + "\C-u2\C-x\C-k\C-a" + ;; Insert and increment counter + "\C-x\C-k\C-i" + ;; Insert register + "\C-xri\C-r" + )) + (kmacro-tests-should-insert "142586" + (kmacro-tests-simulate-command '(kmacro-end-or-call-macro 2))) + (set-register ?\C-r nil)) + + +(kmacro-tests-deftest kmacro-tests-test-reg-add-counter-equal-01 () + "`kmacro-reg-add-counter-equal' increments counter if equal to register" + (set-register ?\C-r 2) ;; Should be safe as a register name + (kmacro-tests-define-macro (vconcat + ;; Insert and increment counter + "\C-x\C-k\C-i" + ;; Increment counter if it matches + "\C-x\C-k\C-ra=\C-r" + )) + (kmacro-tests-should-insert "0134" + (kmacro-tests-simulate-command '(kmacro-end-or-call-macro 4))) + (set-register ?\C-r nil)) + +(kmacro-tests-deftest kmacro-tests-test-reg-add-counter-equal-02 () + "`kmacro-reg-add-counter-equal' increments counter if equal to register" + (set-register ?\C-r 2) ;; Should be safe as a register name + (kmacro-tests-define-macro (vconcat + ;; Insert and increment counter + "\C-x\C-k\C-i" + ;; Add two to counter if it matches + "\C-u2\C-x\C-k\C-ra=\C-r" + )) + (kmacro-tests-should-insert "0145" + (kmacro-tests-simulate-command '(kmacro-end-or-call-macro 4))) + (set-register ?\C-r nil)) + +(kmacro-tests-deftest kmacro-tests-test-reg-add-counter-equal-03 () + "`kmacro-reg-add-counter-equal' increments counter if equal to register" + (set-register ?\C-r 2) ;; Should be safe as a register name + (kmacro-tests-define-macro (vconcat + ;; Insert and increment counter + "\C-x\C-k\C-i" + ;; Add four to counter if it matches + "\C-u\C-x\C-k\C-ra=\C-r" + )) + (kmacro-tests-should-insert "0167" + (kmacro-tests-simulate-command '(kmacro-end-or-call-macro 4))) + (set-register ?\C-r nil)) + +(kmacro-tests-deftest kmacro-tests-test-reg-add-counter-equal-04 () + "`kmacro-reg-add-counter-equal' increments counter if equal to register" + (set-register ?\C-r 2) ;; Should be safe as a register name + (kmacro-tests-define-macro (vconcat + ;; Insert and increment counter + "\C-x\C-k\C-i" + ;; Decrement counter if it matches + "\C-u-\C-x\C-k\C-ra=\C-r" + )) + (kmacro-tests-should-insert "0111" + (kmacro-tests-simulate-command '(kmacro-end-or-call-macro 4))) + (set-register ?\C-r nil)) + +(kmacro-tests-deftest kmacro-tests-test-reg-add-counter-less () + "`kmacro-reg-add-counter-less' increments counter if less than register" + (set-register ?\C-r 6) ;; Should be safe as a register name + (kmacro-tests-simulate-command '(kmacro-set-counter 8)) + (kmacro-tests-define-macro (vconcat + ;; Decrement counter if it's + ;; less than the register + "\C-u-1\C-x\C-k\C-ra<\C-r" + ;; Insert and decrement counter + "\C-u-\C-x\C-k\C-i" + )) + (kmacro-tests-should-insert "8764" + (kmacro-tests-simulate-command '(kmacro-end-or-call-macro 4))) + (set-register ?\C-r nil)) + +(kmacro-tests-deftest kmacro-tests-test-reg-add-counter-greater () + "`kmacro-reg-add-counter-greater' increments counter if greater than register" + (set-register ?\C-r 2) ;; Should be safe as a register name + (kmacro-tests-define-macro (vconcat + ;; Insert and increment counter + "\C-x\C-k\C-i" + ;; Increment counter if it's greater + ;; than the register + "\C-x\C-k\C-ra>\C-r" + )) + (kmacro-tests-should-insert "0124" + (kmacro-tests-simulate-command '(kmacro-end-or-call-macro 4))) + (set-register ?\C-r nil)) + + +(kmacro-tests-deftest kmacro-tests-test-quit-counter-equal-01 () + "`kmacro-quit-counter-equal' stops macro if counter is equal to prefix" + (kmacro-tests-simulate-command '(kmacro-set-counter 5)) + (kmacro-tests-define-macro (vconcat + ;; Insert and decrement counter + "\C-u-\C-x\C-k\C-i" + ;; Stop if the counter is at 0 + "\C-x\C-k\C-q=" + )) + (kmacro-tests-should-insert "5432" + (kmacro-tests-simulate-command '(kmacro-end-or-call-macro 4))) + (should (condition-case abort + (should (= 1 kmacro-counter)) + (kmacro-tests-simulate-command '(kmacro-end-or-call-macro 1)) + (quit abort)))) + +(kmacro-tests-deftest kmacro-tests-test-quit-counter-equal-02 () + "`kmacro-quit-counter-equal' stops macro if counter is equal to prefix" + (kmacro-tests-define-macro (vconcat + ;; Insert and increment counter + "\C-x\C-k\C-i" + ;; Stop if the counter is at 5 + "\C-u5\C-x\C-k\C-q=" + )) + (kmacro-tests-should-insert "0123" + (kmacro-tests-simulate-command '(kmacro-end-or-call-macro 4))) + (should (condition-case abort + (should (= 4 kmacro-counter)) + (kmacro-tests-simulate-command '(kmacro-end-or-call-macro 1)) + (quit abort)))) + +(kmacro-tests-deftest kmacro-tests-test-quit-counter-equal-03 () + "`kmacro-quit-counter-equal' stops macro if counter is equal to prefix" + (kmacro-tests-simulate-command '(kmacro-set-counter 5)) + (kmacro-tests-define-macro (vconcat + ;; Insert and decrement counter + "\C-u-\C-x\C-k\C-i" + ;; Stop if the counter is at 0 + "\C-u\C-x\C-k\C-q=" + )) + (kmacro-tests-should-insert "5432" + (kmacro-tests-simulate-command '(kmacro-end-or-call-macro 4))) + (should (condition-case abort + (should (= 1 kmacro-counter)) + (kmacro-tests-simulate-command '(kmacro-end-or-call-macro 1)) + (quit abort)))) + +(kmacro-tests-deftest kmacro-tests-test-quit-counter-equal-04 () + "`kmacro-quit-counter-equal' stops macro if counter is equal to prefix" + (kmacro-tests-simulate-command '(kmacro-set-counter 4)) + (kmacro-tests-define-macro (vconcat + ;; Insert and decrement counter + "\C-u-\C-x\C-k\C-i" + ;; Stop if the counter is at -1 + "\C-u-\C-x\C-k\C-q=" + )) + (kmacro-tests-should-insert "4321" + (kmacro-tests-simulate-command '(kmacro-end-or-call-macro 4))) + (should (condition-case abort + (should (= 0 kmacro-counter)) + (kmacro-tests-simulate-command '(kmacro-end-or-call-macro 1)) + (quit abort)))) + +(kmacro-tests-deftest kmacro-tests-test-quit-counter-less () + "`kmacro-quit-counter-less' stops macro if counter is less than prefix" + (kmacro-tests-simulate-command '(kmacro-set-counter 8)) + (kmacro-tests-define-macro (vconcat + ;; Stop if the counter is less than 5 + "\C-u5\C-x\C-k\C-q<" + ;; Insert and decrement counter + "\C-u-\C-x\C-k\C-i" + )) + (kmacro-tests-should-insert "8765" + (kmacro-tests-simulate-command '(kmacro-end-or-call-macro 4))) + (should (condition-case abort + (should (= 4 kmacro-counter)) + (kmacro-tests-simulate-command '(kmacro-end-or-call-macro 1)) + (quit abort)))) + +(kmacro-tests-deftest kmacro-tests-test-quit-counter-greater () + "`kmacro-quit-counter-greater' stops macro if counter is greater than prefix" + (kmacro-tests-define-macro (vconcat + ;; Insert and increment counter + "\C-x\C-k\C-i" + ;; Stop if the counter is greater than 4 + "\C-u4\C-x\C-k\C-q>" + )) + (kmacro-tests-should-insert "0123" + (kmacro-tests-simulate-command '(kmacro-end-or-call-macro 4))) + (should (condition-case abort + (should (= 4 kmacro-counter)) + (kmacro-tests-simulate-command '(kmacro-end-or-call-macro 1)) + (quit abort)))) + + (kmacro-tests-deftest kmacro-tests-end-call-macro-prefix-args () "kmacro-end-call-macro changes behavior based on prefix arg." ;; "Record" two macros.
[Message part 7 (text/plain, inline)]
-- Alex.
GNU bug tracking system
Copyright (C) 1999 Darren O. Benham,
1997,2003 nCipher Corporation Ltd,
1994-97 Ian Jackson.