commit 327bde2d3211ae10bc8ecad21ae801d4a5087c41
parent 429abd82c97031948b7681197551bb77708bd174
Author: Wojciech Siewierski <wojciech@siewierski.eu>
Date: Fri, 3 Mar 2023 02:37:50 +0100
Merge branch 'mc-rewrite'
Diffstat:
| M | README.md | | | 32 | +++++++++++++++----------------- |
| A | kmacro-x-mc-extra.el | | | 78 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | kmacro-x-mc-legacy.el | | | 109 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | kmacro-x-mc.el | | | 199 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| M | kmacro-x.el | | | 73 | +------------------------------------------------------------------------ |
5 files changed, 402 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,16 @@ 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.
+
+Note for the existing users: The old implementation of the multiple
+cursors is still available in `kmacro-x-mc-legacy.el`. All the
+functions are the same, just with the `legacy` infix added.
diff --git a/kmacro-x-mc-extra.el b/kmacro-x-mc-extra.el
@@ -0,0 +1,78 @@
+;;; kmacro-x-mc-extra.el --- Additional functionality for the multiple cursors -*- 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:
+
+;; Non-critical and/or experimental features for the multiple cursors
+;; based on keyboard macros.
+
+;;; Code:
+
+(require 'kmacro-x-mc)
+
+(defgroup kmacro-x-mc-extra nil
+ "Additional functionality for the multiple cursors"
+ :group 'kmacro-x-mc)
+
+;;;###autoload
+(defun kmacro-x-mc-mark-at-click (event)
+ "Toggle the fake cursor at the mouse position.
+
+Some code borrowed from `mc/fake-cursor-at-point'."
+ (interactive "e")
+ (mouse-minibuffer-check event)
+ (let ((position (event-end event)))
+ (unless (windowp (posn-window position))
+ (user-error "Position not in text area of window"))
+ (select-window (posn-window position))
+ (let* ((point (posn-point position))
+ (overlays-at-point (overlays-at point)))
+ (if overlays-at-point
+ (dolist (ov overlays-at-point)
+ (when (eq (overlay-get ov 'face) 'kmacro-x-mc-cursor-face)
+ (delete-overlay ov)
+ (setq kmacro-x-mc-cursors
+ (delete ov kmacro-x-mc-cursors))))
+ (let ((ov (make-overlay point (1+ point) nil t)))
+ (unless kmacro-x-mc-mode
+ (kmacro-x-mc-mode 1))
+ (overlay-put ov 'face 'kmacro-x-mc-cursor-face)
+ (overlay-put ov 'offsets '(0 . 0))
+ (push ov kmacro-x-mc-cursors))))))
+
+;;;###autoload
+(defun kmacro-x-mc-mark-next-or-rect ()
+ "Like `kmacro-x-mc-mark-next' but fall back to
+`rectangle-mark-mode' when there is no selection."
+ (interactive)
+ (require 'rect)
+ (if (and (not rectangle-mark-mode)
+ (use-region-p))
+ (progn
+ (setq this-command 'kmacro-x-mc-mark-next)
+ (kmacro-x-mc-mark-next))
+ (unless rectangle-mark-mode
+ (rectangle-mark-mode 1))
+ (rectangle-next-line)))
+
+(provide 'kmacro-x-mc-extra)
+;;; kmacro-x-mc-extra.el ends here
diff --git a/kmacro-x-mc-legacy.el b/kmacro-x-mc-legacy.el
@@ -0,0 +1,109 @@
+;;; kmacro-x-mc-legacy.el --- The old implementation of the multiple cursors -*- 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:
+
+;; The obsolete version of the multiple cursors based of keyboard
+;; macros. Less robust but with different semantics. No longer
+;; maintained but might be useful.
+
+;;; Code:
+
+(require 'kmacro-x-mc)
+
+(defgroup kmacro-x-mc-legacy nil
+ "Multiple cursors implemented with keyboard macros (legacy)."
+ :group 'kmacro-x-mc)
+
+(defface kmacro-x-mc-legacy-highlight-face
+ '((t (:inherit hi-yellow))) ;`highlight-regexp' hardcodes `hi-yellow'
+ "The face used by `kmacro-x-mc-legacy-region'.")
+
+(defcustom kmacro-x-mc-legacy-region-sequence-fmt "C-s %s C-r RET 2*C-s RET"
+ "The sequence of keys used by `kmacro-x-mc-legacy-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-legacy-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-legacy-region-sequence-fmt
+ (format-kbd-macro query))))
+ (when highlight
+ (highlight-regexp (regexp-quote query)
+ 'kmacro-x-mc-legacy-highlight-face))
+ (start-kbd-macro 'append 'no-exec)))
+
+;;;###autoload
+(defun kmacro-x-mc-legacy (&optional highlight)
+ "A more user friendly version of `kmacro-x-mc-legacy-region'.
+
+If selection is active, it does the same thing as
+`kmacro-x-mc-legacy-region'. If there is no active selection, it uses
+the symbol at point instead.
+
+See `kmacro-x-mc-legacy-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-legacy-region start end highlight))))
+
+
+(provide 'kmacro-x-mc-legacy)
+;;; kmacro-x-mc-legacy.el ends here
diff --git a/kmacro-x-mc.el b/kmacro-x-mc.el
@@ -0,0 +1,199 @@
+;;; 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'.")
+
+(defface kmacro-x-mc-main-cursor-face
+ '((t (:underline t)))
+ "The face used for the original cursor when using `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 (let ((case-fold-search nil))
+ (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 as mouse-created cursors
+ ;; have different offsets.
+ (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 (eq (overlay-get ov 'face) 'kmacro-x-mc-main-cursor-face)
+ (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))
+
+;; Always treat the bulk macro application as a single undo operation.
+(advice-add #'kmacro-x-mc-apply :around
+ #'kmacro-x-undo-amalgamate-advice)
+
+(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.
+
+\\{kmacro-x-mc-mode-map}"
+ :require 'macro-x
+ :lighter " kmacro-mc"
+ (if kmacro-x-mc-mode
+ (let ((bounds (if (use-region-p)
+ (cons (region-beginning) (region-end))
+ (or (bounds-of-thing-at-point 'symbol)
+ ;; A technically correct fallback.
+ ;; Relevant mostly for the alternative ways to
+ ;; create cursors such as mouse clicks.
+ (cons (point) (1+ (point)))))))
+
+ (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 'face 'kmacro-x-mc-main-cursor-face)
+ (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)
+;;; kmacro-x-mc.el ends here
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."