psearch: 如何指定替换第几个匹配

最近使用 psearch 改较长的函数,节省了很多时间来写 el-patch。

请教一下,想替换 line 3 的内容:

(defun test-insert-lines ()
  "test."
  (interactive)
  (insert "short line 1\n")
  (insert "long line 2\n")
  (insert "very long line 3\n")
  ;; (省略几行)
  (insert "ultra long line 8\n")
  (insert "extra ultra long line 9\n"))

我一般做法是:

(require 'psearch)
(psearch-patch test-insert-lines
  (psearch-replace '`(insert "very long line 3\n")
                   '`(insert "patched line 3\n")))

但如果类似 sexp 有好几个多且较长,整个 sexp 照抄一遍就显得臃肿。如果使用 '(insert ,args) 会替换掉所有匹配。不知道 psearch-replace 支持只替换第几个匹配么,比如这样:

(require 'psearch)
(psearch-patch test-insert-lines
  (psearch-replace '`(insert ,args)
                   '`(insert "patched line 3\n")
                   nil  ; BEG
                   nil  ; END
                   nil  ; SPLICE
                   3    ; <- 第3个匹配,类似 re-search-forward 里的 COUNT
                   ))

EDIT: rewording

psearch-replace 暂时无法指定替换第几个。不过可以用 psearch-forward 来实现:

(psearch-patch test-insert-lines
  (dotimes (i 3)
    (psearch-forward '`(insert ,rest)
                     t (lambda (_ bounds)
                         (prog1 bounds
                           (when (eq i 2)
                             (delete-region (car bounds) (cdr bounds))
                             (insert (format "%S" '(insert "patched line 3\n")))))))))
1 个赞

搞定了,正好学习一下 psearch-forward 的使用。多谢大佬 :smiley:

我给 psearch-patch 加了一道判断:只有 PATCH-FORM 返回 non-nil 时才应用补丁,否则抛出异常提醒用户补丁失效。

所以上边的补丁 (dotimes ...) 之后要手动返回一个 t

后续我考虑给 psearch-{for,back}ward 加一个 count 参数,这样就不用 dotimes 包裹了。而且这俩函数本身就可以返回 non-nil,所以也就不用手动 t

1 个赞

代码里面的例子更清晰,更新后报错手动返回了个 t 就好了,好用!

psearch-{for,back}ward 现在已经支持 COUNT 参数,跟内置的 re-search-{for,back}ward 一样,count 是指向前/向后搜索的步数。

所以如果只替换第三个,可以使用 psearch-count-current 来判断:

(psearch-forward '`(insert ,rest)
                 t (lambda (_ bounds)
                     (when (= psearch-count-current 3)
                       (message "==> i: %s" psearch-count-current))
                     t)
                 3)
;; ==> i: 3
;; 385

如果 callback 函数返回 nil 可提前中止搜索:

(psearch-forward '`(insert ,rest)
                 t (lambda (_ bounds)
                     (when (<= psearch-count-current 2)
                       (message "==> i: %s" psearch-count-current)
                       t))
                 3)
;; ==> i: 1
;; ==> i: 2
;; 353
1 个赞

好用。可以通过 psearch-count-currentCOUNT 很好地限定范围了。

https://lists.gnu.org/archive/html/bug-gnu-emacs/2022-12/msg01661.html

@twlz0ne 我用 psearch 删除了函数中不需要的那第五个匹配结果,这么写还需要改进嘛?

不过修改之前必须删除 函数所在文件的 elc,比如这个例子中的 simple.elc, 否则的话会直接报错。

  ;; jump after inserted text after undo-redo
  (psearch-patch primitive-undo
    (psearch-forward '`(goto-char . ,rest)
                     t (lambda (_ bounds)
                         (when (= psearch-count-current 5)
                           (delete-region (car bounds) (cdr bounds)))
                         t)
                     5))

emacs -Q 底下会报错吗?在我这边无法复现:

$ emacs -Q -L ~/.repos/emacs-psearch --eval "\
  (progn
    (require 'psearch)
    (psearch-patch primitive-undo
      (psearch-forward '`(goto-char . ,rest)
                       t (lambda (_ bounds)
                           (when (= psearch-count-current 5)
                             (delete-region (car bounds) (cdr bounds)))
                           t)
                       5))
    (print (documentation 'primitive-undo t)))" -nw --batch
uncompressing simple.el.gz...
uncompressing simple.el.gz...done

"[PATCHED]Undo N records from the front of the list LIST.
Return what remains of the list."

奇怪,我刚才又重新编译 simple.elc 再试了一次,这次没有报错了。 我第一次尝试后报错就是从这个函数来的,“Can’t patch a byte-compiled function”。等到我删除了simple.elc 才成功。现在却又没报错了。难道安装emacs时候的编译文件和我手动编译的有区别?

(defun psearch-patch--symbol-function-def (function)
  (let ((def (symbol-function function)))
    (if (byte-code-function-p def)
        (signal 'psearch-patch-failed
                (list function "Can't patch a byte-compiled function"))
      (let ((new (nthcdr (if (eq (car def) 'closure) 2 1) def)))
        (push function new)
        (push 'defun new)
        new))))

没有区别。

“Can’t patch a byte-compiled function” 必需满足两个条件:

  1. 找不到源代码文件。
  2. (symbol-function function) 返回的是二进制。

如果你单用 psearch-patch--symbol-function-def 也可能会遇到这个问题,因为它前面还有查找源代码文件的动作。

@twlz0ne 小建议:我现在是每次启动 emacs 后都会自动打那个 primitive-undo 的 patch, 但是那个查找源代码打开的 buffer 并没有自动关闭,

(let ((info (find-function-library #'primitive-undo 'lisp-only t)))
  (find-function-search-for-symbol (car info) nil (cdr info)))
;; ==> (#<buffer simple.el.gz> . 153254)

比如 psearch-patch 运行完了之后应该 kill 这个 simple.el.gz 的 buffer,要不然我每次启动都得手动关闭。

先前有坛友提出过这个问题,目前没什么好的解决方案。你可以看下这个帖子 Buffer not killed after patch executed · Issue #1 · twlz0ne/psearch.el · GitHub 里面我有给个解决方案,看下能不能接受它的副作用。

算是一个临时的好办法啦!我觉得你要区分使用场景,如果是手动 eval 的话可以不管,但是如果是在配置文件中打的 patch,那么在 emacs init 完成后应当自动删除所有新打开的源代码 buffer。这可能需要维护一个 psearch-patch-find-buffer-list这类的变量,然后 after-init-hook kill 掉整个 list。

我也好奇真的有人手动 patch 嘛?一般都只是在尝试的时候才会吧,写好了就加进配置里了。反正我是这样的。应该足够适用于80%的场景

剩下 20% 也很难受。实际可能不止,after-init-hook 之后还有很多按需加载的初始化动作。

反正我现在很满意啦,恐怕也很难找到完美的方法。

完美的方案也不是没有,就是侵入性太强,我认为不值得。

在先前给出临时方案时就考虑过,当然是建立在不杀掉 buffer 的基础上。因为找不到合适的杀掉 buffer 的时机,after-init-hook 之后仍有漏网之鱼,可能还要加上 idle timer 作为补充,如此仍然存在与用户冲突的机会。

如果不杀掉 buffer,采用隐藏的方案会更简单一些,但是要在 find-file-noselect 加个永久的 advice,这是我不太愿意的:

+ (defvar psearch-patch-invoke-p nil "Whether psearch-patch is invoking or not.")
+
+ (defun psearch-patch--advice-find-file-noselect (&rest args)
+   "Advice to change the visibility of a file buffer opened by psearch-patch.
+
+ Hide a file buffer opened by psearch-patch from `read-buffer' completion until
+ the user opened it again."
+   (let ((prefix " [psearch-patched] ")
+         (old-buffer (get-file-buffer (cadr args)))
+         (new-buffer (apply args)))
+     (with-current-buffer new-buffer
+       (if psearch-patch-invoke-p
+           (unless (or old-buffer (string-prefix-p prefix (buffer-name)))
+             (rename-buffer (concat prefix (buffer-name))))
+         (when (and old-buffer (string-prefix-p prefix (buffer-name)))
+           (rename-buffer (string-remove-prefix prefix (buffer-name)) t))))
+     new-buffer))
+
+ (advice-add 'find-file-noselect :around #'psearch-patch--advice-find-file-noselect)
+
(defun psearch-patch--xref-function-def (xref-args)
+ (let ((psearch-patch-invoke-p t))
    (pcase-let* ((`(,fun ,file ,type) xref-args)
                 (`(,buf . ,pos) (find-function-search-for-symbol fun type file)))
      (with-current-buffer buf
        (goto-char pos)
        (let ((bounds (bounds-of-thing-at-point 'sexp)))
          (read (buffer-substring-no-properties (car bounds) (cdr bounds))))))))

确实不值得,还不如我自己手动关闭。。

或者全部在 idle timer 里关闭,如果段时间内有 patch 动作,就不断重置倒计时。

那这样的话,在 idle 的时间里面如果用户手动switch 到源代码文件的话,又得取消idle了。感觉也很复杂