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

基于 pcase 的 elisp 代码搜索工具包。有了它就可以更方便实现 elisp 代码的查找/替换/重构了。

本包最初构想来自于之前写的一个函数 pcase-refactor (详见链接[2])。虽然没有 el-search 那么强大,实现上也简陋,但是更容易使用。目前提供的三个借口 forward / backward / replace 都是可以以 programmatically 方式使用的,psearch-replace 函数本身就是一个例子。

虽然 el-search 很强大,但我一直没找到如何通过编程的方式使用它。它的代码很复杂,主要的逻辑放在一个巨大的函数中,想扩展无从下手。虽然可以把常用的查找 → 替换 pattern 定义成 semi-automatic 形式,但也只是减少了一些输入量(作用有限,相当于别名),终究不是 programmatically,仍然需要一遍一遍回答 Yes/No。

相关讨论:


补充:

包名 psearch 来自于第一次见到 el-search 时的感叹:

3赞

用 psearch 重写上边链接[2]的 pcase-refactor-at-point

pcase-refactor-at-point 有个非常明显的缺点,就是光标必须位于 sexp 头部

  • |(sexp) :white_check_mark:
  • (se|xp) :no_entry:

然而大多数时候我们可能正处于 sexp 内部,想要重构就得先移动光标。如果用 psearch 重写就可以省却移动光标的麻烦:

(defun psearch-refactor-setq-local-at-point ()
  (interactive)
  (psearch-search-at-point '`(set (make-local-variable ',sym) . ,val)
                           '`(setq-local ,sym ,@val)))

;;        修改前                        修改后
;;    |(match a (b c))     =>     |(replace a (b c))
;;     (match a (b|c))     =>     |(replace a (b c))

这样无论光标处于 |(sexp) 还是 (se|xp) 都能应对。同时也保证不会无限 backward,改到其他的内容。

另一个衍生项目的名称已经想好想了:pefactor (谐音 perfact,其中 p 又表示 pcase/psearch)

借鉴大佬 @wilfredhttps://github.com/Wilfred/emacs-refactor 包。探查光标所在位置匹配的 refactor 操作。比如当前处于 let 语句中:

(let ((a 1)
      (b (1+ a|)))
  ...)

此时呼叫 refactor,只会列举匹配的操作 let -> let*,而不会出现 when -> unless 的重构操作。比从 M-x 万千函数中查找一个 refactor-xxx 要便捷得多。

基于 pcase 的优点是查找定位方便,pattern 直观易写。缺点是得到的替代项是重新打印格式化过的,会破坏原有的代码排版。如果这个问题解决了,就真的 perfact 了。

写了一对 setq 合并/拆分的函数:

(defun psearch-refactor-combine-setq-in-region (beg end)
  (interactive "r")
  (save-excursion
    (save-restriction
      (narrow-to-region beg end)
      (goto-char (point-max))
      (unwind-protect
          (let ((sym-val-list nil))
            (while (psearch-backward
                    '`(setq ,sym ,val)
                    '`(,sym ,val)
                    (lambda (result _bounds)
                      (setq sym-val-list (append result sym-val-list)))))
            (delete-region beg end)
            (insert (psearch--print-to-string `(setq ,@sym-val-list))))))))

拆分函数的 parttern 直接取自一楼链接[1]中 @xuchunyang 的代码例子,增加了 filter 部分用于消除拆分后最外层的多余 ()

(defun psearch-refactor-split-setq-at-point ()
  (interactive)
  (psearch-replace-at-point '`(setq . ,(and r (guard (> (length r) 2))))
                            '(->> (psearch--print-to-string
                                   (mapcar
                                    (lambda (pair)
                                      (cons 'setq pair))
                                    (seq-partition r 2)))
                              (string-remove-prefix "(")
                              (string-remove-suffix ")"))))

不知 el-search 如何消除外层括号?


EDIT: 其实可以直接在 pattern 内部完成去 () 的步骤(原先使用的 :filter 关键字已不再提供),el-search 应该也一样。不过这样会使得 pattern 变臃肿。

有个 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赞