Puni: 通用、可定制的语法删除

那好像得靠 treesitter 了…?

treesitter我记得已经到了可以用的程度了 (Walking :: Emacs Tree-sitter ?),怎么就没人用它做paredit呢……

我也好奇,有时间真应该研究下

我加了一个 API 叫做 puni-up-list,wiki 里很多命令都用这个重构了,你想要的那个 kill-line 也用这个做了:Useful commands · AmaiKinono/puni Wiki · GitHub

Edit: 有了这个 API 以后就可以做一些比较神奇的事情了,比如 slurp:

puni-slurp

barf、splice、split、raise 也都不是问题。不过我不太喜欢这些命令,所以只是玩玩 :rofl:

1 个赞

请问你能分享下 slurp 的函数吗,我借鉴下。

感觉 slurp 还是挺常用的,有了这些花里胡哨的东西,我就可以彻底删掉 paredit 了 :rofl:

先等等吧,我打算把它们都实现了 :rofl: 也许放在一个另外的文件里。能做全语言 paredit 为啥不做呢(

2 个赞

赞。已经用 puni-kill-line 来代替 paredit-kill 了。

不过在没有空白尾行的情况下有时会失败,例如:

(with-temp-buffer
  (insert "print <<~EOF.gsub(/\\n/, ' ')
This is a
sample text.
EOF") ;; <---
  (ruby-mode)
  (goto-char (point-min))
  (re-search-forward "~EOF" nil t)
  (puni-kill-line) ;; kill .gsub(/\\n/, ' ')
  (substring-no-properties (buffer-string)))
;; => "print <<~EOF.gsub(/\\n/, ' ')
;;    This is a
;;    sample text.
;;    EOF"

添加空白尾行之后就可以删除了:

(with-temp-buffer
  (insert "print <<~EOF.gsub(/\\n/, ' ')
This is a
sample text.
EOF\n") ;; <---
  (ruby-mode)
  (goto-char (point-min))
  (re-search-forward "~EOF" nil t)
  (puni-kill-line) ;; kill .gsub(/\\n/, ' ')
  (substring-no-properties (buffer-string)))
;; => "print <<~EOFThis is a
;;    sample text.
;;    EOF
;;    "

但是却多删了一个 \n,我预期的结果是 print <<~EOF\nThis is a,实际 print <<~EOFThis is a

另外,字符串当中的转义符号也无法删除: "foo|bar\n" -> "foo|\n"


  • macOS 10.12.6
  • Emacs 28.0-20210819 (rev: fba64e1697174369b87e3de0c189a0fb0963c49c)
  • puni-20210820.211 (rev: 5b1f7b7e13d0122bed564ca98408c32e3dbded95)

不过在没有空白尾行的情况下有时会失败,例如…

这个我觉得是 ruby-modeforward-sexp 表现比较怪。在你说的没有空白尾行的情况下:

print <<~EOF|.gsub(/\n/, ' ')

在这里用 forward-sexp,能工作,跳到的地方和有空白尾行的情况下一样,但是却会扔一个 no next sexp 的错误出来。这个应该是 smie-forward-sexp-command 干的,之后我再看一下。

因为 Puni 做的都是尽可能一般化的修正,问题就是如果 forward-sexp 动了光标,却扔了个错误,那我应该认为它成功了还是失败了。

但是却多删了一个 \n ,我预期的结果是 print <<~EOF\nThis is a

我这没有这个问题。看一下你 kill-whole-line 的设置?

另外,字符串当中的转义符号也无法删除: "foo|bar\n""foo|\n"

已经追查到原因。在这里:

"foo|\n"

求值:

(skip-chars-forward (char-to-string (char-after)))

我期望它能跳过光标面前相同的字符,可是对 \ 失效了。

Edit: 刚发现 skip-chars-forward 的参数是模仿正则表达式的,那就说得通了。可是我发现我不知道该咋写这个函数了 :rofl:

Edit2: 删不掉 \n 的问题修好了。ruby-mode 那个我还得想想。

看了一下,果然是我有设置这个变量。不过因为 paredit-kill&awesome-pair-kill 有时候不太理会这个变量,所以非 lisp 场合我都用 evil-delete-line 来代替 kill line 操作,久而久之竟忘了它的存在:

(cl-labels ((test-kill
             (kill-function)
             (with-temp-buffer
               (insert (concat "if cond:\n"
                               "    pass\n"))
               (python-mode)
               (goto-char (point-min))
               (re-search-forward "if" nil t)
               (let ((kill-whole-line nil))
                 (funcall kill-function)
                 (insert "|")
                 (substring-no-properties (buffer-string))))))
  (dolist (kill-function '(puni-kill-line awesome-pair-kill paredit-kill))
    (princ (format "%s:\n" kill-function))
    (print (test-kill kill-function))))
;; =>
;; puni-kill-line:
;;
;; "if|
;;     pass
;; "
;; awesome-pair-kill:
;;
;; "if|
;; "
;; paredit-kill:
;;
;; "if|
;; "

Emacs 还是有很多细节需要处理的,刚刚提了一个新的 issue:Indentation changed after `puni-kill-line` · Issue #6 · AmaiKinono/puni · GitHub

谢谢。我注意到 smartparens 有 sp-no-reindent-after-kill-modessp-no-reindent-after-kill-indent-line-functions,看来它也碰过这个坑。我想想有没有通用的方法。

Edit: 想到了。可以缩进两次,如果导致的 indent offset 一样,说明它算出了正确的位置;如果不一样,说明它只是在可能的位置中循环,那就回到未缩进时的位置。

一番奋战之后,终于把 ParEdit 那些花哨的表达式操作命令实现了:

  • web-mode 中的 slurp 和 barf

    slurp-barf

    注意到移动了的定界符会闪一下,操作的时候可以看得更清楚。

  • convolute!

    convolute

实现了的操作有 slurp、barf、raise、splice、split、transpose、convolute,还有一个我自己设计的 squeeze(用来代替 rewrap)。

另外「重新缩进」的处理也重新设计过了,现在 Puni 尽量不重新计算缩进,有多行需要重新缩进的话,就把它们之间相对的缩进量恢复到使用 Puni 的命令之前的状态。

另外 puni-expand-region 现在也内置了。

新功能肯定会有些小问题,欢迎大家试用和提意见。

1 个赞

今天更新以后,你要的这个命令在 wiki 里有了更清晰的实现,所以敲你一下 :wink:

1 个赞

哈哈,终于等到了。等到时有空了就去试试~

看来最近更新幅度比较大,先前遇到的几个问题都解决了。

forward-sexp 的确很好用,但有时也力有未逮,比如 "|(foo; bar)" 就因括号不平衡,而无法用 kill line 删除。又因在字符串内,无法判定其 Major mode,也就无法通过定制 forward-sexp-function 来解决。

另,不写测试的都艺高人大胆, manateelazycatDogLooksGood 和楼主皆属此列。

这个我觉得比较无解。有时候人们希望 Puni 把字符串里的东西看作代码,有时候又希望看作纯文本。

:rofl: Citre 是有测试的,Puni 没有是因为我真的不知道怎么写。Puni 的思路太奇怪了,如果我做的是 smartparens 那样的东西我就写了。

forward-sexp 遇到一些情况还是可以做的,这就是 paredit 和 awesome-pair 要做的事情,通过一些语法周围分析情况来处理一些极端情况。

反馈个问题,puni 对 Rust 里面的 <> 支持不是很好,举个例子:(| 表示光标位置)

Option<|>Result<i64>
// puni-slurp-forward,符合预期
Option<|Result><i64>
// puni-slurp-forward,不符合预期,正确的结果应该是 Option<|Result<i64>>
Option<|Result<>i64>

可以在 GitHub 上报一下吗,我有空会看

Filed. puni-slurp-forward unexpected behavior for <> · Issue #28 · AmaiKinono/puni · GitHub

1 个赞

如果使用 (load "auctex.el" nil t t) 后打开 TeX 文件,发现 puni-kill-line 没有如帖子图片中展示的行为,只是单纯的删除一行。请问有人遇到一样的问题吗?