分享智能的 paredit-kill 函数

作为十几年忠实的 paredit 粉, 我几乎会在所有模式中使用 paredit 模式以进行语法快速删除, 而不仅仅只是傻傻的文本删除.

这么多年, 我也基于 paredit 扩展了很多智能函数, 今天发布了最新版的 paredit-extension.el EmacsWiki: paredit-extension.el

主要更新了 `paredit-kill+’ 这个函数:

(defun paredit-kill+ ()
  "It's annoying that we need re-indent line after we delete blank line with `paredit-kill'.
`paredt-kill+' fixed this problem.

If current mode is `web-mode', use `paredit-web-mode-kill' instead `paredit-kill' for smarter kill operation."
  (interactive)
  (cond ((eq major-mode 'web-mode)
         (paredit-web-mode-kill))
        ((eq major-mode 'ruby-mode)
         (paredit-ruby-mode-kill))
        (t
         (paredit-common-mode-kill))))

(defun paredit-common-mode-kill ()
  (interactive)
  (if (paredit-blank-line-p)
      (paredit-kill-blank-line-and-reindent)
    (paredit-kill)))

(defun paredit-web-mode-kill ()
  "It's a smarter kill function for `web-mode'.

If current line is blank line, re-indent line after kill whole line.
If point in string area, kill string content like `paredit-kill' do.
If point in tag area, kill nearest tag attribute around point.
Otherwise, do `paredit-kill'."
  (interactive)
  (if (paredit-blank-line-p)
      (paredit-kill-blank-line-and-reindent)
    (cond ((paredit-in-string-p)
           (paredit-kill))
          (t
           (let (char-count-before-kill
                 char-count-after-kill)
             (setq char-count-before-kill (- (point-max) (point-min)))
             (web-mode-attribute-kill)
             (setq char-count-after-kill (- (point-max) (point-min)))
             (when (equal char-count-before-kill char-count-after-kill)
               (paredit-kill))
             )))))

(defun paredit-ruby-mode-kill ()
  "It's a smarter kill function for `ruby-mode'.

If current line is blank line, re-indent line after kill whole line.
If point in string area, kill string content like `paredit-kill' do.
If point at block beginning, kill whole block.
If point at block end, kill rest line after end block.
Otherwise, do `paredit-kill'.
"
  (interactive)
  (if (paredit-blank-line-p)
      (paredit-kill-blank-line-and-reindent)
    (let (in-beginning-block-p in-end-block-p block-start-pos block-end-pos)
      (save-excursion
        (setq current-symbol (buffer-substring-no-properties (beginning-of-thing 'symbol) (end-of-thing 'symbol)))
        (setq in-beginning-block-p (member current-symbol '("class" "module" "def" "if" "unless" "case" "while" "until" "for" "begin" "do")))
        (setq in-end-block-p (member current-symbol '("end")))
        )
      (cond ((paredit-in-string-p)
             (paredit-kill))
            (in-beginning-block-p
             (beginning-of-thing 'symbol)
             (setq block-start-pos (point))
             (forward-sexp 1)
             (setq block-end-pos (point))
             (kill-region block-start-pos block-end-pos))
            (in-end-block-p
             (beginning-of-thing 'symbol)
             (save-excursion
               (kill-line)))
            (t
             (paredit-kill))))))

大部分模式都用 `paredit-common-mode-kill’ 进行语法删除:

  1. 如果当前是空行, 会删除整个行, 并自动缩进到下一行的缩进位置
  2. 如果当前不是空行, 执行 paredit-kill 函数, 比如直接删除一个语法块

但是 paredit-common-kill' 在 web-mode 和 ruby-mode 上表现很糟糕, 所以我又写了 paredit-web-mode-kill’ 和 `paredit-ruby-mode-kill’

paredit-web-mode-kill 的删除逻辑:

  1. 如果当前是空行, 会删除整个行, 并自动缩进到下一行的缩进位置
  2. 如果在字符串里面, 执行 paredit-kill
  3. 如果在 HTML tag 里面, 删除光标处最近的 tag 属性
  4. 其他的情况, 执行 paredit-kill, 比如直接删除一段 HTML tag 块

paredit-ruby-mode-kill 的删除逻辑:

  1. 如果当前是空行, 会删除整个行, 并自动缩进到下一行的缩进位置
  2. 如果在字符串里面, 执行 paredit-kill
  3. 如果在 ruby block 开始的地方, 比如 class, def, module 等, 直接删除整个 ruby block
  4. 如果在 ruby end关键字的位置, 直接删除 end 和后面的内容
  5. 其他的情况, 执行 paredit-kill, 比如直接删除一段 ruby 块

通过新版的 paredit-extension.el 的增强, 不仅仅保持了大部分模式 sexp 语法删除的爽快, 还非常智能的增强了 web-mode 和 ruby-mode 的语法删除体验.

欢迎大家试用并集成到自己的配置中.

参考配置连接:

https://github.com/manateelazycat/lazycat-emacs/blob/master/site-lisp/config/init-paredit.el

8 个赞

没怎么用过 paredit ,是不是功能和 GitHub - Fuco1/smartparens: Minor mode for Emacs that deals with parens pairs and tries to be smart about it. 相近?

pardit是第一个做语法结构化编辑的插件,smartparent至今都没有paredit好用

不怎么会lisp和python,似乎最近abo-abo的lispylpy比较流行。这里也有过讨论:

我用过 lispy 这些, paredit可以针对大多数模式, 而不仅仅是 lisp, 而且 paredit 的做法更加直觉化一点.

大致试了一下,在lisp Ruby Python之外的C风格语言中,好像只有paredit-kill能比较好的工作(kill到当前expression的末尾),语法太复杂,怪不得需要vim/evil来按字符划分text object,按语法划分太多了,按不过来。

不过光一个paredit-kill也有帮助的。有个小问题,好像没有和paredit-kill反向,删除到expression开头的函数?这个需求也会有吧?奇怪

其实用的最多的就是 paredit-kill 了, 在 lisp 中,我经常用 paredit-close-round-and-newline+

lazycat-toolkit里用了不认识的东西:

(file-missing "Cannot open load file" "No such file or directory" "mwe-log-commands")
  require(mwe-log-commands)
  eval-buffer(#<buffer  *load*-822461> nil "/Users/***/.spacemacs.d/extensions/lazycat-toolkit.el" nil t)  ; Reading at buffer position 2367

是有这个需求,似乎不怎么重视?How to do paredit-kill backwards? 的解决方案有点暴力,我用 syntax-ppss 也实现了一个,有些坑可能没有踩到。去除了对 paredit-mode 的依赖,比较晦涩,但愿一个月后还能看得懂。

  • save-excursion 应该可以去掉,但是不确定会产生什么副作用,所以还是保留了。

  • 有些支持 b style comment 的 mode,工作得不太好(也不支持 nestable comment),可能是我不会设置吧。

代码:

(defun paredit-backward-kill (&optional argument)
  "Backward version of `paredit-kill'.

With a `\\[universal-argument]' prefix argument, kill the text before point on
the current line.
With a positive integer prefix argument N, kill lines backward
many times.

With a `\\[universal-argument] \\[universal-argument]' prefix argument, kill all expressions before
the point in the current block, group, string or comment."
  (interactive "P")
  (unless (bobp)
    (let ((pos (point))
          (hungry-p (equal argument '(16))))
      (cond ((and (bolp) (not argument))
             (kill-region pos (1- pos)))
            ((and (integerp argument) (> argument 1))
             (kill-line (- (1- argument))))
            ((and argument (not hungry-p))
             (kill-line 0))
            (t (let* ((bol (point-at-bol))
                      (cur-ppss (syntax-ppss))
                      (cur-up-pos (cadr cur-ppss))
                      (cur-comment-stat (nth 4 cur-ppss))
                      (cur-comment-style (nth 7 cur-ppss)))
                 (if (nth 5 cur-ppss)
                     (cl-incf pos)
                   (when (and (not (eobp))
                              (nth 10 cur-ppss)
                              (not (eq cur-comment-stat
                                       (nth 4 (save-excursion
                                                (syntax-ppss (1+ pos)))))))
                     (cl-decf pos)
                     (backward-char 1)))
                 (cond ((or (nth 3 cur-ppss) (and cur-comment-stat cur-comment-style))
                        (let ((str/cmt-pos (1+ (nth 8 cur-ppss))))
                          (when cur-comment-style (cl-incf str/cmt-pos))
                          (kill-region pos (if hungry-p str/cmt-pos (max bol str/cmt-pos)))))
                       ((and cur-up-pos (or hungry-p (<= bol cur-up-pos)))
                        (kill-region pos (1+ cur-up-pos)))
                       (t (let* ((bol-ppss (save-excursion (syntax-ppss bol)))
                                 (bol-up-pos (cadr bol-ppss)))
                            (cond ((or (and cur-up-pos (> bol-up-pos cur-up-pos))
                                       (and (not cur-up-pos) bol-up-pos))
                                   (kill-region pos (nth (car cur-ppss) (nth 9 bol-ppss))))
                                  ((or (nth 3 bol-ppss) (nth 4 bol-ppss))
                                   (kill-region pos (nth 8 bol-ppss)))
                                  (t (kill-region pos bol))))))))))))

一些示例:

;; in expression
(foo bar
     baz |quux)
;; =>
(foo bar
|quux)
;; =>
(foo bar|quux)
;; =>
(|quux)

;; in string
"foo bar
baz |quux"
;; =>
"foo bar
|quux"
;; =>
"foo bar|quux"
;; =>
"|quux"

;; expr at bol
(foo ("bar
baz") zot |quux)
;; =>
(foo |quux)

;; C-u / M-<num> / C-u <num>
;; `kill-line' backward
(foo (bar |quux
;; =>
|quux

;; C-u C-u
;; hungry kill
(foo (bar
      baz)
     zot |quux)
;; =>
(|quux)

"foo bar
baz
zot |quux"
;; =>
"|quux"

更新了一下,现在支持 block comment 了,但仍不支持 nestable comment。还有现在可以用 yank 插入连续被 kill 的文本了。

如果不常使用 kill-line backward 的功能,可以将 '(16) 改为 '(4),把宝贵的 C-u 让出来。

  • 光标在 /**/ 中间时先退一个字符
/* foo bar
baz quux *|/
 =>
/* foo bar
|*/
 =>
/* foo bar|*/
 =>
/*|*/
{foo /|* bar */}
 =>
{|/* bar */}
  • 对于支持 nestable comment 的语言,需注意第二种情况会机械地删到头,应该只删到 baz 前的一个字符
foo /* bar /* baz */ zot |*/ quux
 =>
foo /*|*/ quux
foo /* bar /* baz |*/ zot */ quux
 =>
foo /*|*/ zot */ quux 
2 个赞

感谢分享,才知道 paredit 也能够用于 C/C++ ,之前以为只能用于 Lisp 。

简单在 c++-mode 上试了一下,下面这种情况会删除到 } ,( | 是光标)

switch |(foo) {
}

这种情况就能正确地删除到 ) 前:

switch (|foo) {
}

paredit 在绝大多数模式都工作良好, paredit 作者的功力相当深厚。

求知道paredit和smartparens到底有什么区别。跟风从paredit切换到了smartparens的人表示原本paredit用的好好的,只是觉得paredit不像smartparens代码提交的依旧很频繁,paredit连一个git源都找不到。 另表示,论坛的C-M-f快捷键和mac系统的文本编辑框光标移动快捷键有冲突。

paredit的作者功力更深厚,很多语法级处理都比smartparents好

smartparents commit多并不代表东西更好

两个我都是用过了,smartparents到现在都没有达到十年前paredit的水平

很多强的elisp插件取决于作者的功力,而不是commit数量和更新频繁

2 个赞

您在其他major mode里面也会默认开启paredit吗?

试用过 smartparens 和 paredit,我觉得 paredit 好用太多了。然而当我在c++里面开启了paredit-mode,发现有很多特性还是专门为s-exp设计的:比如字母后面敲括号会自动加一个空格。

然而我又特别喜欢paredit的“括号必须匹配才能删除”的功能emmm

我大部分模式都会用paredit,自动加空格是你其他模式影响的,paredit没有这个问题

请用emacs -Q单独测试paredit来排除其他插件的问题。

确实是 paredit 引起的

我这里包的版本是 25beta 20171127.205 来自 melpa

edit:发现了一个叫 paredit-everwhere 的包用于非lisp buffer

假的paredit 吧,我从来不用melpa

直接从作者官网下载吧,emacs大多数插件都是单文件,非要搞一个melpa,真麻烦

1 个赞

我的 paredit 的版本也是 20171127.205 来自 melpa 也没有自动加空格,估计真的是其他插件或者配置的影响

打算试试 paredit-extension,所以把包的依赖关系整理了一下,尽量找到原始下载地址不知道对不对:

paredit-extension                   "https://www.emacswiki.org/emacs/download/paredit-extension.el"
  \-> lazycat-toolkit               "https://www.emacswiki.org/emacs/download/lazycat-toolkit.el"
        |-> color-moccur            "https://www.emacswiki.org/emacs/download/color-moccur.el"
        |-> shell-command-extension "https://www.emacswiki.org/emacs/download/shell-command-extension.el"
        |-> mwe-log-commands        "http://www.foldr.org/~michaelw/emacs/mwe-log-commands.el"
        \-> basic-toolkit           "https://www.emacswiki.org/emacs/download/basic-toolkit.el"
              |-> cycle-buffer      "https://www.emacswiki.org/emacs/download/cycle-buffer.el"
              |-> css-sort-buffer   "https://www.emacswiki.org/emacs/download/css-sort-buffer.el"
              \-> windows           "http://www.gentei.org/~yuuji/software/windows.el"
                    \-> revive      "http://www.gentei.org/~yuuji/software/revive.el"

(我的配置是这样的: (foo :requires (bar (qux :source (git "https://~/qux.git")))),显式地声明依赖关系,避免因为初始化语句书写顺序产生问题,我还可以单独启用某个 “layer”,它本身包含了完整的依赖关系,不必关心它依赖的 package 在其他 layer 里没有加载)

1 个赞

已经解决了