,@
就是 unquote-splicing
实现了函数热补丁功能。适用于不便 advice 又不想改源码的场景,例如:一个巨长的函数,你只想改中间一两句。
比 el-patch 更简便易用,没那么多复杂的概念,心智负担更小,只要会有用 pcase 就能理解。
我自己先用几天再发布。
添加了命令行 cli/psearch
,可以直接在命令行输入想要匹配的模式进行查找:
文件多了速度有点慢。
这是包里带的例子,用 psearch-replace 把 nil
变成 t
成功了。
但如果反过来把 t
变成 nil
,就会失败。不知道是不是我使用得不对?
(psearch-with-function-patch test-patch
(psearch-replace '`(if nil ,body)
'`(if t ,body)))
;; (defun test-patch () => (defun test-patch ()
;; (list '(1 2 3) (list '(1 2 3)
;; (if nil '(4 5 6)) (if t '(4 5 6))
;; '(7 8 9))) '(7 8 9)))
测试过程:
- 对象函数:
(defun test-patch ()
(list '(1 2 3)
(if t '(4 5 6))
'(7 8 9)))
- 执行替换
(psearch-replace '`(if t ,body) '`(if nil ,body))
- 替换结果及光标位置:
环境:emacs-plus@30, 昨天编译; macOS BigSur 11.6。
更新:刚找到个workaround, 用 (null t)
替换 nil
可行。
补充个使用 psearch 给 elisp 缩进打补丁的办法:
- 缩进问题:Wrong indent for :bind · Issue #887 · jwiegley/use-package · GitHub
- 参考方案:indentation - How to indent keywords aligned? - Emacs Stack Exchange (最高赞回答,回答的作者表示要给上游发补丁,但一直没打?)
(with-eval-after-load 'lisp-mode
(require 'psearch)
(psearch-patch calculate-lisp-indent
(psearch-replace
'`(if (or ,or1 ,_) ,then ,else)
'`(if (or ,or1
;; First sexp after `containing-sexp' is a keyword. This
;; condition is more debatable. It's so that I can have
;; unquoted plists in macros. It assumes that you won't
;; make a function whose name is a keyword.
(when-let (char-after (char-after (1+ containing-sexp)))
(char-equal char-after ?:))
;; Check for quotes or backquotes around.
(let* ((positions (elt state 9))
(last (car (last positions)))
(rest (reverse (butlast positions)))
(any-quoted-p nil)
(point nil))
(or
(when-let (char (char-before last))
(or (char-equal char ?')
(char-equal char ?`)))
(progn
(while (and rest (not any-quoted-p))
(setq point (pop rest))
(setq any-quoted-p
(or
(when-let (char (char-before point))
(or (char-equal char ?')
(char-equal char ?`)))
(save-excursion
(goto-char (1+ point))
(looking-at-p
"\\(?:back\\)?quote[\t\n\f\s]+(")))))
any-quoted-p))))
(null t) ;; 这儿本应使用 ",then", 但此处 ",then" 值为 nil,
;; 而 psearch 对应的 nil 功能是删除该匹配,所以用
;; (null t) 替代
,else))))
打完补丁前
`(:token ,token
:token-quality ,quality) ,
(use-package org
:bind (:map org-mode-map
("C-c C-'" . org-edit-special)
:map org-src-mode-map
("C-c C-'" . org-edit-src-exit)))
打完补丁后
`(:token ,token
:token-quality ,quality) .
(use-package org
:bind (:map org-mode-map
("C-c C-'" . org-edit-special)
:map org-src-mode-map
("C-c C-'" . org-edit-src-exit)))
psearch-patch 目前版本的副作用是,打完补丁后的原函数的位置找不到了:
(symbol-file 'calculate-lisp-indent)
;; >> nil
这个问题我也注意到了。因为 nil
存在歧义,目前没有想到好的解决方案。
因为是在一个临时的 buffer 里进行 patch 和 eval,所以位置信息丢失了,这个好解决。
另,缩进问题可以看看:
原来你们已经打了包了,我用上了。
嗯,盯着nil的值就好了,我先用 (null t) 绕一下。
psearch 是真的好用啊,无痛打补丁,打了好几个复杂的功能了,感谢大佬
这个包真是宝藏!非常简单方便的打补丁。之前研究了一段时间el-patch
,实在没看出来跟直接重写一遍函数有什么区别。。。
我希望能一次性改动多个函数,但我不知道在dolist里面该怎么用psearch-patch。。请问有什么办法解决这个吗
(dolist (func '(eglot--TextDocumentItem
eglot--signal-textDocument/didSave
eglot--signal-textDocument/didChange))
(psearch-patch 这里不知道该写什么
(psearch-replace '`(buffer-substring-no-properties (point-min) (point-max))
'`(zw/buffer-content (point-min) (point-max)))))
请问下面这个是特性还是Bug呀?
如果执行:
(psearch-replace '`(foo . ,args)
'`(bar () ,@args))
那么 (foo 'a 'b)
会变成 (bar )
,而不是 (bar () 'a 'b)
是 Bug,已经在 Fix psearch--prin1 · twlz0ne/psearch.el@25e7dc8 · GitHub 修复了。
其实是先前 #10 一直悬而未决的问题。
不过仍然要注意的是,()
会被转为 nil
(两者等价),如果想要保持 ()
原样输出,可以用转义符号:
(psearch--print-to-string '(bar nil 'a 'b)) ;; => "(bar nil 'a 'b)"
(psearch--print-to-string '(bar () 'a 'b)) ;; => "(bar nil 'a 'b)"
(cl-prin1-to-string '(bar () 'a 'b)) ;; => "(bar nil 'a 'b)"
(psearch--print-to-string '(bar \(\) 'a 'b)) ;; => "(bar () 'a 'b)"
试一下:
(eval `(psearch-patch ,func
最近用 psearch-patch
发现了一个新的问题:
有些函数并不是用 defun
宏定义的,而是用其他的宏,比如在包 doom-modeline 中,用 doom-modeline-def-segment
宏来定义一些函数,而这个宏的第一个参数在宏展开后,其实只是函数名的一个后缀:
(doom-modeline-def-segment TEST "doc" 'nil)
展开为
(defun doom-modeline-segment--TEST nil "doc" 'nil)
而在 psearch-patch
内部所调用的宏 psearch-patch-define
中,会替换掉这个“函数名”:
;; Modifiy function name
(unless (eq ',name (nth 1 func-def))
(setcdr func-def (cons ',name (nthcdr 2 func-def))))
导致最终执行的(延续上面的例子)实际上是:
(doom-modeline-def-segment doom-modeline-segment--TEST "doc" 'nil)
因而最终定义的函数有了双重前缀:doom-modeline-segment--doom-modeline-segment--TEST
请问这个有什么好的办法吗?
抱歉。我很久没摸电脑了。有点反应不过来。 能展示一下你的 patch 代码怎么写的吗?
比如在包 doom-modeline 中,函数 doom-modeline-segment--matches
是通过下面这个宏定义的:
(doom-modeline-def-segment matches
"Docstring"
(let ((meta (concat (doom-modeline--macro-recording)
(doom-modeline--anzu)
(doom-modeline--phi-search)
(doom-modeline--evil-substitute)
(doom-modeline--iedit)
(doom-modeline--symbol-overlay)
(doom-modeline--multiple-cursors))))
(or (and (not (string-empty-p meta)) meta)
(doom-modeline--buffer-size))))
我的 patch 代码没有什么特别,比如,就使用:
(psearch-patch doom-modeline-segment--matches
(psearch-replace '`meta
'`meta_))
但最终的结果是:并没有重新定义 doom-modeline-segment--matches
这个函数,而是定义了一个新的函数 doom-modeline-segment--doom-modeline-segment--matches
,而这,就是问题所在。
我明白了。
问题虽然已经明了,但是解决起来有点麻烦。
普通函数和 doom-modeline-def-segment
生成的函数是不一样的:
(psearch-patch--find-function 'doom-modeline-segment--foobar)
;; => (doom-modeline-def-segment foobar "Docstring" body...)
;; | | | |
;; | | | `--- [3]函数体
;; | | `------------ [2]文档
;; | `---------------------- [1]函数名
;; `------------------------------------ [0]声明符
(psearch-patch--find-function 'foobar)
;; => (defun foobar nil "Docstring" body...)
;; | | | | |
;; | | | | `--- [4]函数体
;; | | | `------------ [3]文档
;; | | `-------------------- [2]参数表
;; | `-------------------------- [1]函数名
;; `-------------------------------- [0]声明符
从以上对比可以看出,doom-modeline-def-segment
定义函数是没有参数列表的,所以它的 Docstring
位置和 defun
定义的函数不一样:
而且这个不一样还是 doom-modeline-def-segment
独有的, 换个其它 xxx-def-yyy
有可能就不一样了。有的可能需要参数列表,有的可能不需要 Docstring
。这就导致其生成的结构无法预料。
需要有一种方法,可以让用户指定如何解析 def 生成的结构,这无疑会给用户增加负担。还要考虑到不改变 psearch-patch
的声明格式,不影响旧的 patch 代码,或许可以在外层套个 let
:
(let ((psearch-patch-symbol-definition-pospec
'((symbol-name . 1)
(docstring . 2))))
(psearch-patch doom-modeline-segment--matches
(psearch-replace '`meta
'`meta_)))
让我再想一想,有可能指定一个 (let ((psarch-patch-docpos 2)) ...)
就够了。
真是很漂亮、精确和简洁的解释!没有认真读过前辈的代码,但是通过这个回答,学到了不少!
再次感谢这个很实用的工具!
同时,我有两个不太相关的小问题:
- 这部分的注释,是怎么排版成这种“图形化”的纯字符呢?有什么小工具吗?
- 我之所以会尝试使用如下“看似没用”的代码:
是因为,我 patch 了 doom-modeline--multiple-cursors
这个函数(它是用 defsubst
定义的),并且被 doom-modeline-segment--matches
调用。
但是仅仅做这个 patch 没有效果(是因为内联?还是 Doom Emacs 的黑魔法?),并且我发现,只要我重新求值一下 doom-modeline-segment--matches
的定义,就有效果了。(并且先 patch 和先重新求值皆可)
这才有了我上面这个“看似没用”的代码。但最终我是通过这个代码来强制重新求值的:
(eval (psearch-patch--find-function 'doom-modeline-segment--matches))
所以,前辈有遇到类似的情况吗?有相关原因和更漂亮的解决方案吗?
手打,没工具。
内联。
最新版本 Add support for macro-generated function · twlz0ne/psearch.el@b027a9d · GitHub 应该可以支持 doom-modeline-segment
函数了,用法:
(let ((psearch-patch-function-definition-docpos 2))
(psearch-patch doom-modeline-segment--matches
(psearch-replace '`MATCH-PATTERN
'`REPLACEMENT)))
嗯嗯,学到了!感谢!
我尝试这个版本发现,可以 patch defun
定义的函数,但是无法 patch defsubst
定义的函数呢(该提交之前,patch 功能是正常的)。
所以是不是把 defsubst
考虑进去比较好:
@@ -797,7 +797,7 @@
`(let ((func-def (psearch-patch--find-function ',orig-func-spec)))
(with-temp-buffer
;; Modifiy function name
- (when (and (eq 'defun (nth 0 func-def)) (not (eq ',name (nth 1 func-def))))
+ (when (and (memq (nth 0 func-def) '(defun defsubst)) (not (eq ',name (nth 1 func-def))))
(setcdr func-def (cons ',name (nthcdr 2 func-def))))
(print func-def (current-buffer))
;; Modify docstring.
@@ -805,7 +805,7 @@
(down-list)
(forward-sexp
(+ ,docpos (if (equal 'lambda (sexp-at-point)) 0
- (if (memq (sexp-at-point) '(defun cl-defgeneric cl-defmethod)) 1
+ (if (memq (sexp-at-point) '(defun defsubst cl-defgeneric cl-defmethod)) 1
0))))
(let ((str "[PATCHED]"))
(goto-char (car (bounds-of-thing-at-point 'sexp)))