kmacro-x

a fork of vifon's kmacro-x
git clone https://git.trogloxene.org/kmacro-x.git
Log | Files | Refs | README | LICENSE

commit 327bde2d3211ae10bc8ecad21ae801d4a5087c41
parent 429abd82c97031948b7681197551bb77708bd174
Author: Wojciech Siewierski <wojciech@siewierski.eu>
Date:   Fri,  3 Mar 2023 02:37:50 +0100

Merge branch 'mc-rewrite'

Diffstat:
MREADME.md | 32+++++++++++++++-----------------
Akmacro-x-mc-extra.el | 78++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Akmacro-x-mc-legacy.el | 109+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Akmacro-x-mc.el | 199+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mkmacro-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."