zqso
2023 年4 月 8 日 05:53
1
最近使用 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 个赞
zqso
2023 年4 月 8 日 16:47
3
搞定了,正好学习一下 psearch-forward 的使用。多谢大佬
我给 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 个赞
zqso
2023 年4 月 12 日 07:07
7
好用。可以通过 psearch-count-current
和 COUNT
很好地限定范围了。
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” 必需满足两个条件:
找不到源代码文件。
(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,要不然我每次启动都得手动关闭。
算是一个临时的好办法啦!我觉得你要区分使用场景,如果是手动 eval 的话可以不管,但是如果是在配置文件中打的 patch,那么在 emacs init 完成后应当自动删除所有新打开的源代码 buffer。这可能需要维护一个 psearch-patch-find-buffer-list
这类的变量,然后 after-init-hook
kill 掉整个 list。
我也好奇真的有人手动 patch 嘛?一般都只是在尝试的时候才会吧,写好了就加进配置里了。反正我是这样的。应该足够适用于80%的场景
liuyinz:
应该足够适用于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了。感觉也很复杂