[分享讨论] smart query replace

我现在在尝试从evil切换到emacs键位,给evil里的功能一个一个找替代品。


一般需要替换重复字符的时候,我会用evil的cgn,emacs键位下用query-replace代替。但是手动输入from-stringto-string有点麻烦,我就写了一个小包裹函数。使用方法:

(我绑定到C-x C-q

  1. 选中要替换的地方
  2. C-x C-q
  3. 修改选区(选区会高亮)
  4. 完成后C-x C-q
  5. 进入query replace模式,from-string是原来的字符,to-string是修改后的选区

一开始写的时候比较紧凑,后来不需要了,但是我也懒得改了,反正不复杂,可读性还可以

(defvar smart-query-edit-mode-overlay nil
  "Overlay of region to be replaced.")

(define-minor-mode smart-query-edit-mode
  "Edit region and query replace."
  :lighter "QUERY"
  (if smart-query-edit-mode
      (if (not mark-active)
          (setq smart-query-edit-mode nil)
        (overlay-put
         (setq smart-query-edit-mode-from-string
               (buffer-substring
                (region-beginning)
                (region-end))
               smart-query-edit-mode-overlay
               (make-overlay (region-beginning)
                             (region-end)
                             nil
                             nil
                             t))
         'face '(:inherit highlight)))
    (overlay-put smart-query-edit-mode-overlay
                 'face '(:inherit default))
    (goto-char (overlay-end
                smart-query-edit-mode-overlay))
    (query-replace smart-query-edit-mode-from-string
                   (buffer-substring-no-properties
                    (overlay-start
                     smart-query-edit-mode-overlay)
                    (overlay-end
                     smart-query-edit-mode-overlay)))
    (delete-overlay
     smart-query-edit-mode-overlay)))
2 个赞

解决了 from-string 输入的问题。

to-string 原本也是要输入的,如果输入内容简单,问题不大。如果输入内容复杂,我倾向开一个临时 buffer,优点是:

  • 避免因误删导致 overlay 跟前后字符粘连
  • 方便从别的缓冲区复制内容

如果改成临时 buffer 输入 to-string,那么 from-string 的方式也可以简化:

  1. 选中内容
  2. C-x C-q 调用 query-replace+ 并处于等待输入 to-string,仍然保持原有的 query-replace 风格。此时根据 to-string 内容:
    • 简单:直接输入,回车提交
    • 复杂:按 C-c C-e 进入临时 buffer,输入完,按 C-c C-c 提交

cool,我觉得可以,晚上搞一下

抱歉忘了回了。我后来想了想,你说的其实只需要做一个自动前冲from-string的简单wrapper就好了。to-string的复杂编辑可以用minibuffer-edit搞定。(我记得有一个包是可以弹出一个temp buffer编辑minibuffer的内容)

似乎你是想用anzu的功能?

不完全是吧,重点还是在于替换。

(defvar query-replace-iedit-mode-overlay nil
  "Overlay of region to be replaced.")
(defvar query-replace-iedit-mode-from-string nil)
(defvar query-replace-iedit-mode-delimited nil)

;;;###autoload
(define-minor-mode query-replace-iedit-mode
  "Edit region and query replace."
  :lighter "Q"
  (if query-replace-iedit-mode
      (let ((bounds (if (region-active-p) (cons (region-beginning) (region-end))
                      (bounds-of-thing-at-point 'symbol))))
        (if (not bounds)
            (setq query-replace-iedit-mode nil)
          (overlay-put
           (setq query-replace-iedit-mode-from-string
                 (buffer-substring
                  (car bounds)
                  (cdr bounds))
                 query-replace-iedit-mode-overlay
                 (make-overlay (car bounds)
                               (cdr bounds)
                               nil nil t))
           'face '(:inherit highlight))
          (setq query-replace-iedit-mode-delimited
                (or (not (region-active-p))
                    (and current-prefix-arg
                         (not (eq current-prefix-arg '-)))))
          (message "C-; replace, g:all, c:act e:edit replacement C-e:临时退出 C-cC-c:恢复")))

    (let* ((start (overlay-start
                   query-replace-iedit-mode-overlay))
           (end (overlay-end
                 query-replace-iedit-mode-overlay))
           (to (buffer-substring-no-properties start end)))
      (delete-overlay query-replace-iedit-mode-overlay)
      (unless (string-equal query-replace-iedit-mode-from-string to)
        (save-excursion
          (delete-region  start end)
          (insert query-replace-iedit-mode-from-string)
          (goto-char (point-min))
          (query-replace query-replace-iedit-mode-from-string
                         to
                         query-replace-iedit-mode-delimited
                         (point)
                         (point-max)
                         nil (use-region-noncontiguous-p)))))))

略微改进了点,如果当前没有选中区域,则使用光标下的symbol 作为搜索内容

还有其他一些配置

;当敲 query-replace-map之外的按键不要退出query-replace
;; https://emacs.stackexchange.com/questions/80484/query-replace-ignore-events-not-binded-in-query-replace-map
(defvar vmacs-do-nothing-map
  (let ((map (make-keymap)))
    (set-char-table-range (nth 1 map) t 'ignore)
    map))
(set-keymap-parent query-replace-map vmacs-do-nothing-map)

(define-key query-replace-map "g" 'automatic) ;old ! replace all automatic
(define-key query-replace-map "p" 'backup)
(define-key query-replace-map "c" 'act)     ;old y
(define-key query-replace-map "\C-e" 'edit) ;临时退出 old C-r
(global-set-key (kbd "C-c C-c") #'exit-recursive-edit) ;query-replace C-r临时退出replace 后,可C-cC-c 继续replace


(setq query-replace-read-from-default #'vmacs-query-replace-read-from-default)
(defvar vmacs-query-replace-read-from-def nil)
(defun vmacs-query-replace-read-from-default()
  (if (eq this-command 'vmacs-replace-all)
      vmacs-query-replace-read-from-def
    (if (use-region-p)
        (buffer-substring-no-properties (region-beginning) (region-end))
      (thing-at-point 'symbol))))

(defun vmacs-replace-all()
  (interactive)
  (save-excursion
    (setq vmacs-query-replace-read-from-def
          (if (use-region-p)
              (buffer-substring-no-properties (region-beginning) (region-end))
            (thing-at-point 'symbol)))
    (goto-char (point-min))
    (call-interactively #'query-replace)))

1 个赞