kmacro-x

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

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:
MREADME.md | 28+++++++++++-----------------
Akmacro-x-mc.el | 184+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mkmacro-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."