commit 5fd81c9e3b8987b1203d5f0b96dc0a8e5676a9ec
parent 429abd82c97031948b7681197551bb77708bd174
Author: Wojciech Siewierski <wojciech@siewierski.eu>
Date: Thu, 2 Mar 2023 16:34:10 +0100
A complete rewrite of the kmacro-x-mc functionality
Diffstat:
| M | README.md | | | 28 | +++++++++++----------------- |
| A | kmacro-x-mc.el | | | 184 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| M | kmacro-x.el | | | 73 | +------------------------------------------------------------------------ |
3 files changed, 196 insertions(+), 89 deletions(-)
diff --git a/README.md b/README.md
@@ -14,7 +14,8 @@ INSTALLATION
(use-package kmacro-x
:ensure t
:init (kmacro-x-atomic-undo-mode 1)
- :bind ("C-c k" . kmacro-x-mc-region))
+ :bind (("C-<" . kmacro-x-mc-mark-previous)
+ ("C->" . kmacro-x-mc-mark-next)))
```
FEATURES
@@ -33,19 +34,12 @@ This way all the pitfalls of rolling a custom implementation of
multiple-cursors are avoided, while all the kmacro facilities, such as
counters, queries and kmacro editing, are gained virtually for free.
-It is assumed the user didn't rebind the basic isearch commands,
-otherwise the behavior may be unpredictable.
-
-A typical workflow with `kmacro-x-mc`:
-
-1. Select the text whose occurences are to be manipulated (in
- a trivial case: a symbol to be renamed).
-2. <kbd>M-x kmacro-x-mc RET</kbd>
-3. Do the necessary edits, either within the region or in its vicinity
- outside of it (this is the part that cannot be easily achieved with
- other mc alternatives such as iedit or query-replace). They will
- get recorded as a kmacro.
-4. Press any key that would end the kmacro recording:
- <kbd>F4</kbd>, <kbd>C-x )</kbd> or <kbd>C-x C-k C-k</kbd>
-5. Repeat the kmacro with <kbd>F4</kbd>, <kbd>C-x e</kbd> or
- <kbd>C-x C-k C-k</kbd>.
+Usage:
+
+1. `kmacro-x-mc-mark-next` and `kmacro-x-mc-mark-previous` are used to
+ find the next/previous occurrence of the symbol at point (or the
+ region if active) and create a fake cursor/selection at
+ its position.
+2. Some arbitrary actions are being performed.
+3. `RET` can be used to apply the actions to all the other cursors or
+ `C-g` can be used to abort the bulk operation.
diff --git a/kmacro-x-mc.el b/kmacro-x-mc.el
@@ -0,0 +1,184 @@
+;;; kmacro-x-mc.el --- Multiple cursors implemented with kmacros -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2022-2023 Wojciech Siewierski
+
+;; Author: Wojciech Siewierski
+;; URL: https://github.com/vifon/kmacro-x.el
+;; Keywords: convenience
+;; Version: 0.9
+
+;; 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 <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Efficient multiple cursors implemented using keyboard macros.
+
+;;; Code:
+
+(require 'kmacro-x)
+
+(defgroup kmacro-x-mc nil
+ "Multiple cursors implemented with keyboard macros."
+ :group 'kmacro-x)
+
+(defface kmacro-x-mc-cursor-face
+ '((t (:inverse-video t)))
+ "The face used for the fake cursors created in `kmacro-x-mc-mode'.")
+
+(defvar-local kmacro-x-mc-regexp nil
+ "A regexp matching the intended cursor positions.")
+
+(defvar-local kmacro-x-mc-offsets '(nil . nil)
+ "The cons with offsets of the point and the mark within the selection.")
+
+(defvar-local kmacro-x-mc-cursors nil
+ "The overlays for displaying and keeping the cursor positions.")
+
+(defun kmacro-x--mc-mark (&optional backwards)
+ "Create a new fake cursor for `kmacro-x-mc-mode'.
+
+Enables `kmacro-x-mc-mode' if not enabled yet, for the
+necessary initialization."
+
+ ;; Sanity check to prevent defining cursors with inconsistent
+ ;; starting conditions.
+ (when (and kmacro-x-mc-mode
+ (not (member last-command '(kmacro-x-mc-mark-next
+ kmacro-x-mc-mark-previous))))
+ (user-error "This command can only be used when starting a bulk edit."))
+
+ (unless kmacro-x-mc-mode
+ (kmacro-x-mc-mode 1))
+
+ ;; This function is always being called while a macro is being
+ ;; recorded, but we do not want it to become a part of the macro.
+ (cancel-kbd-macro-events)
+
+ (save-excursion
+ ;; Start the search right after/behind the last cursor.
+ (let ((ov (car (if backwards
+ kmacro-x-mc-cursors
+ (last kmacro-x-mc-cursors)))))
+ (goto-char (if backwards
+ (overlay-start ov)
+ (overlay-end ov))))
+
+ ;; Create a new cursor.
+ (if (if backwards
+ (search-backward-regexp kmacro-x-mc-regexp nil 'noerror)
+ (search-forward-regexp kmacro-x-mc-regexp nil 'noerror))
+ (let ((ov (make-overlay (match-beginning 0)
+ (match-end 0))))
+ (overlay-put ov 'face 'kmacro-x-mc-cursor-face)
+
+ ;; Store the offsets per cursor for future development.
+ (overlay-put ov 'offsets kmacro-x-mc-offsets)
+
+ ;; Either append or prepend the new cursor.
+ (if backwards
+ (push ov kmacro-x-mc-cursors)
+ (setq kmacro-x-mc-cursors (append kmacro-x-mc-cursors (list ov)))))
+ (message "No further matches"))))
+
+;;;###autoload
+(defun kmacro-x-mc-mark-next ()
+ "Create a new fake cursor forward."
+ (interactive)
+ (kmacro-x--mc-mark))
+
+;;;###autoload
+(defun kmacro-x-mc-mark-previous ()
+ "Create a new fake cursor backwards."
+ (interactive)
+ (kmacro-x--mc-mark 'backwards))
+
+(defun kmacro-x-mc-apply ()
+ "Apply the recoded macro for each cursor."
+ (interactive)
+ (end-kbd-macro)
+ (dolist (ov kmacro-x-mc-cursors)
+ (unless (overlay-get ov 'main-cursor)
+ (goto-char (+ (overlay-start ov)
+ (car (overlay-get ov 'offsets))))
+ (push-mark (+ (overlay-start ov)
+ (cdr (overlay-get ov 'offsets))))
+ (call-last-kbd-macro)))
+ (kmacro-x-mc-mode 0))
+
+(defun kmacro-x-mc-quit ()
+ "Cancel the macro recording, disable `kmacro-x-mc-mode'.
+
+If region is active, merely deactivate it instead. This event is
+omitted from the recorded macro to prevent premature termination."
+ (interactive)
+ (cancel-kbd-macro-events)
+ (if (region-active-p)
+ (deactivate-mark)
+ (kmacro-x-mc-mode 0)
+
+ ;; Make sure the macro recording is cancelled. Since it's bound
+ ;; to `C-g', it might be a good idea to call it anyway.
+ (keyboard-quit)))
+
+
+(defvar kmacro-x-mc-mode-map
+ (let ((map (make-sparse-keymap)))
+ (define-key map (kbd "C-g") #'kmacro-x-mc-quit)
+ (define-key map (kbd "RET") #'kmacro-x-mc-apply)
+ map))
+
+(define-minor-mode kmacro-x-mc-mode
+ "Record a keyboard macro to apply with multiple cursors.
+
+`RET' applies, `C-g' aborts."
+ :require 'macro-x
+ :lighter " kmacro-mc"
+ (if kmacro-x-mc-mode
+ (let ((bounds (if (use-region-p)
+ (cons (region-beginning) (region-end))
+ (bounds-of-thing-at-point 'symbol))))
+
+ (let ((regexp (regexp-quote
+ (buffer-substring-no-properties
+ (car bounds)
+ (cdr bounds)))))
+ (setq-local kmacro-x-mc-regexp
+ (if (use-region-p)
+ regexp
+ (concat "\\_<" regexp "\\_>"))))
+
+ (let ((ov (make-overlay (car bounds) (cdr bounds))))
+ (overlay-put ov 'main-cursor t)
+ (setq-local kmacro-x-mc-cursors (list ov)))
+
+ (unless (use-region-p)
+ ;; The end of the symbol is as good of a place as any for
+ ;; the mark. Definitely better than whatever random
+ ;; position it was before.
+ (push-mark (cdr bounds)))
+
+ (setq-local kmacro-x-mc-offsets
+ (cons (- (point)
+ (car bounds))
+ (- (mark)
+ (car bounds))))
+
+ (start-kbd-macro nil))
+
+ (mapc #'delete-overlay kmacro-x-mc-cursors)
+ (kill-local-variable 'kmacro-x-mc-regexp)
+ (kill-local-variable 'kmacro-x-mc-offsets)
+ (kill-local-variable 'kmacro-x-mc-cursors)))
+
+(provide 'kmacro-x-mc)
diff --git a/kmacro-x.el b/kmacro-x.el
@@ -1,6 +1,6 @@
;;; kmacro-x.el --- Keyboard macro helpers and extensions -*- lexical-binding: t; -*-
-;; Copyright (C) 2022 Wojciech Siewierski
+;; Copyright (C) 2022-2023 Wojciech Siewierski
;; Author: Wojciech Siewierski
;; URL: https://github.com/vifon/kmacro-x.el
@@ -35,77 +35,6 @@
"Keyboard macro helpers and extensions."
:group 'kmacro)
-(defface kmacro-x-highlight-face
- '((t (:inherit hi-yellow))) ;`highlight-regexp' hardcodes `hi-yellow'
- "The face used by `kmacro-x-mc-region'.")
-
-(defcustom kmacro-x-mc-region-sequence-fmt "C-s %s C-r RET 2*C-s RET"
- "The sequence of keys used by `kmacro-x-mc-region'.
-
-This sequence should search for the next occurrence of the query
-and leave the region in a predictable state for the user to use.
-
-The search query will be spliced into it using `format', and so
-it should contain exactly one `%s' placeholder."
- :type 'string)
-
-
-;;;###autoload
-(defun kmacro-x-mc-region (start end &optional highlight)
- "Record a keyboard macro emulating multiple cursors.
-
-The kmacro will first search for current region and then execute
-the rest of the recorded kmacro. During the kmacro execution,
-mark is always at the beginning of the match and point is at
-the end.
-
-START and END mark the region.
-
-If HIGHLIGHT is non-nil (or with the prefix argument when using
-interactively) highlight the query using `highlight-regexp'.
-Use `\\[unhighlight-regexp]' to remove the highlight later."
- (interactive "r\nP")
- (unless (and (= start (region-beginning))
- (= end (region-end)))
- ;; Ensure a consistent behavior when called interactively and when
- ;; called from Elisp with arguments not being the current region.
- ;; Considering the interactive nature of the kmacros, tampering
- ;; with the user's region seems very much justified.
- (push-mark start)
- (goto-char end))
- (let ((query (buffer-substring-no-properties start end)))
- (when (< (point) (mark))
- (exchange-point-and-mark))
- (deactivate-mark)
- (kmacro-push-ring)
- (setq last-kbd-macro
- (read-kbd-macro (format kmacro-x-mc-region-sequence-fmt
- (format-kbd-macro query))))
- (when highlight
- (highlight-regexp (regexp-quote query)
- 'kmacro-x-mc-highlight-face))
- (start-kbd-macro 'append 'no-exec)))
-
-;;;###autoload
-(defun kmacro-x-mc (&optional highlight)
- "A more user friendly version of `kmacro-x-mc-region'.
-
-If selection is active, it does the same thing as
-`kmacro-x-mc-region'. If there is no active selection, it uses
-the symbol at point instead.
-
-See `kmacro-x-mc-region' for the HIGHLIGHT argument."
- (interactive "P")
- (let ((bounds (if (use-region-p)
- (cons (region-beginning) (region-end))
- (bounds-of-thing-at-point 'symbol))))
- (unless bounds
- (error "%s" "No region or symbol to act on"))
- (let ((start (car bounds))
- (end (cdr bounds)))
- (kmacro-x-mc-region start end highlight))))
-
-
;;;###autoload
(define-minor-mode kmacro-x-atomic-undo-mode
"Undo the kmacro executions atomically."