psearch: 基于 pcase 的 elisp 代码搜索工具

,@ 就是 unquote-splicing

1 个赞

实现了函数热补丁功能。适用于不便 advice 又不想改源码的场景,例如:一个巨长的函数,你只想改中间一两句。

el-patch 更简便易用,没那么多复杂的概念,心智负担更小,只要会有用 pcase 就能理解。

我自己先用几天再发布。

2 个赞

添加了命令行 cli/psearch,可以直接在命令行输入想要匹配的模式进行查找:

image

:warning:文件多了速度有点慢。

1 个赞

这是包里带的例子,用 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)))

测试过程:

  1. 对象函数:
(defun test-patch ()
  (list '(1 2 3)
        (if t '(4 5 6))
        '(7 8 9)))
  1. 执行替换
(psearch-replace '`(if t ,body) '`(if nil ,body))
  1. 替换结果及光标位置:

image


环境:emacs-plus@30, 昨天编译; macOS BigSur 11.6。


更新:刚找到个workaround, 用 (null t) 替换 nil 可行。

补充个使用 psearch 给 elisp 缩进打补丁的办法:

  1. 缩进问题:Wrong indent for :bind · Issue #887 · jwiegley/use-package · GitHub
  2. 参考方案: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,所以位置信息丢失了,这个好解决。

另,缩进问题可以看看:

2 个赞

原来你们已经打了包了,我用上了。 :grinning:

嗯,盯着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)"
2 个赞

试一下:

(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)) ...) 就够了。

1 个赞

真是很漂亮、精确和简洁的解释!没有认真读过前辈的代码,但是通过这个回答,学到了不少!

再次感谢这个很实用的工具!

同时,我有两个不太相关的小问题:

  1. 这部分的注释,是怎么排版成这种“图形化”的纯字符呢?有什么小工具吗?
  1. 我之所以会尝试使用如下“看似没用”的代码:

是因为,我 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)))
1 个赞

嗯嗯,学到了!感谢!

我尝试这个版本发现,可以 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)))