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

有个 splice 模式

比如把 (setq a 1 b 2) 改成 (setq a 1) (setq b 2),替换的时候按 s

`(setq ,s1 ,v1 ,s2 ,v2) -> `((setq ,s1 ,v1) (setq ,s2 ,v2))

C-h f el-search-query-replace

It is possible to replace matches with an arbitrary number of expressions (even with zero expressions, effectively deleting matches) by using the “splicing” submode that can be toggled from the prompt with “s”. When splicing mode is on (default off), the replacement expression must evaluate to a list, and all of the list’s elements are inserted in order.

1 个赞

也考虑加入对 splice (对这个单词的字面意思和它在 el-search 中的涵义感到有点困惑) 的支持。

同时也内置一个与之相反的【合并】操作。这样在二次封装的时候就可以少些很多诸如 save-excursion 以及手工打印的步骤。

对于这个合并的 pattern 应该怎么写还没有思路,以合并多个 setq 为例。匹配每一项的规则是 `(setq ,sym ,val),合并规则不能立即应用,必须等到收集完所有匹配项之后再应用,此时合并规则中已无法引用 sym/val 变量,因为已经跳出上下文环境了。


EDIT1: 暂且通过增加一个 collect 概念来实现合并操作吧。

(psearch-replace '`(setq ,sym ,val)
                 ;;  collect   ->  replace (at the end)
                 '(`(,sym ,val) `(setq ,@(-flattern-n 1 its))))

当第二个参数是一对 pattern 的时候,前半部分就变成 collect (原本单个的时候是立即执行 replace)操作,最终由后半部分 pattern 来完成替换操作,其中 its 表示收集到的内容列表。


EDIT2: 引入 collect 带来的新问题:如果每个匹配项之间不是连续的,就不能删除 (beg end) 之间的整块空间,只能删除每个匹配项的 bounds 区域。面临几种选择:

  1. 删除 (beg end) 之间的内容,不考虑非连续的情况
  2. 支持不连续的场景,只删除 bounds 区域,但也留下删除后的大量空洞。
  3. 给个 custom 变量,让用户决定 1 还是 2。

,@ 就是 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 个赞