去掉 helm/ivy/selectrum 使用原生的 completions buffer

emacs世界有才华的同学很多,但像 ivy, selectrum, helm等长期维护者却特别难找 :slight_smile:

2 个赞

是的,开源项目要维护下去需要很多的时间和精力,还有毅力

不是你怎么用的问题。而是 M-x (read-extended-command) 如何响应的问题。

每一次输入变化,都需要完成一系列操作: 过滤 → 排序 → 装饰/渲染(姑且这么叫):

(defun read-extended-command ()
  ...
  (completing-read
   PROMPT
   (lambda ()
     ;; 在这里排序/装饰/渲染
     ;; 1. 高亮匹配的部分
     ;; 例如输入的是 a,则给 "abc" 条目加上 text property,
     ;; 变成 #("abc" 0 1 (face (completions-common-part))
     ;;              1 2 (face (completions-first-difference)))
     ;; 2. 添加额外信息
     ;; 例如把 "command-name"
     ;;   变成 "command-name (keybinding)"
     ;;   甚至 "command-name (keybinding)  [docstring]"
     )
   (lambda (sym)
     ;; 在这里过滤
     )
   ...))

无论你的屏幕多高,即使为1,在初始加载的时候(即M-x 没输入关键字),所有符号都会被过滤,并装饰一遍。

这些步骤里,过滤/排序优化空间不大,而装饰/渲染如果限定在可见范围,则初始加载速度会有显著提升。ivy/selectrum 都对此做了优化,而内置的补全和你提到的 live-completions 则没有。

所以:

如果你想把补全做得高效&友好,最好直接基于 complete-read/read-from-minibuffer 等C函数实现,然后慢慢的你可能离 ivy/selectrum 也不远了。

如果仅仅是在内置的包上做扩展,恐怕很难有超越。

4 个赞

在某个特定的方面(例如这儿说的completion),已经有内置的功能之后还有这么多外部的包,那说明内置的肯定不够好。 Consult的作者最近经常提交completion相关的patch,就我最近关注到的Consult出现的一些跟内置功能缺陷相关的bug,说明competion这部分还有挺多可以提升的。

信ivy,保平安!我觉得ivy要好用得多。

再举几个例子,flymake/flychek,project/projectile,vc mode/magit,应该还有挺多的。

2 个赞

这几个就是最典型的例子。目前我的选择还是ivy系列。

1 个赞

最近完成了去 ivy 化,分享一下这其中的考虑过程以及最后的效果:

counsel-projectile,因为安装了它可以使得我不需要再安装 ripgrep 了。为什么我会这么抗拒安装 ripgrep 呢?主要是我已经是 rg.el 这个包的用户了,不太想安装 2 个功能一样的。为了能够去掉这个限制,最近给 projectile 提了一个 PR projectile-ripgrep: support rg.el package by condy0919 · Pull Request #1699 · bbatsov/projectile · GitHub 终于,rg.el 也在 projectile 内得到支持了。 @seagle0128 .emacs.d/init-utils.el at master · seagle0128/.emacs.d · GitHub 这个也是可以删掉了。

接下来就是要考虑是用 vertico 还是 selectrum. 当然最终这 2 个都没用到,因为发现我最终一定会使用 embark,而 embark 有提供一个 embark-collect-completions 的功能,只要它能够在 minibuffer 有输入的时候弹出对应的补全 buffer 就行。一开始我也是想用原生的 *Completion* 机制,但是发现它不会自动更新。于是必须要使用楼主提到的 live-completion 包,抑或是 vcomplete 。但是因为功能都与 embarkembark-collect-completions 功能雷同,不想再安装额外的一个包,于是就选择了 embark.

这里 可以看到 oantolin (embark 作者,也是使用的 embark-colect-completions) 的使用方式也是非常原生,但是在遇到 consult-ripgrep 之类的异步 API 时是没法按 TAB 补全的,因为补全之后原来的用户输入 (例如 #use-package#doom) 会被 consult-ripgrep 的当前选中行给替换掉,这样就会造成用户的困扰了。

如下是对应的 embark 配置,每一项列出来的都是有原因的,可以见对应的注释。

(use-package embark
  :ensure t
  :bind (:map minibuffer-local-completion-map
         ;; 默认情况下 TAB 绑定的是 minibuffer-complete,但是在补全候选项数量超
         ;; 过 completion-cycle-threshold 的限制时, minibuffer-complete 则不会
         ;; 遍历所有的补全候选项。我个人感觉这种行为有点不符合直觉,TAB 如果可以
         ;; 无视 completion-cycle-threshold 限制,不论数量多少都进行遍历。正好这
         ;; 个行为就是对应的 minibuffer-force-complete 函数.
         ("TAB"     . minibuffer-force-complete)
         ;; 这个键绑定就是为了解决在执行 consult-ripgrep 异步命令时如何有效的选
         ;; 择补全项。在前面提到,如果在 consult-ripgrep 时按 TAB (即
         ;; minibuffer-force-complete) 会覆盖用户的输入,这样就无法对输入进行二
         ;; 次修改了,显然是反直觉的。于是我就给 C-o 赋于了这样的一种能力: 在
         ;; minibuffer 里时如果想浏览 `*Embark Collect Completions*` buffer 的内
         ;; 容,按一下 C-o 就会跳到那个窗口中。然后就可以使用正常浏览 buffer 的
         ;; 方式浏览对应的补全项了。如果正好的对应的结果的话,也是按一下 RET 就
         ;; 好了。如果想二次修改输入,那么当前的 `*Embark Collect Completions*`
         ;; buffer 回到 minibuffer 也只需要再按一下 C-o 即可。我个人感觉这种处理
         ;; 方式更加满足一般人的需求,而不是像 oantolin 那样直接用
         ;; embark-collect-snapshot 来浏览结果,毕竟这样二次修改搜索内容。
         ("C-o"     . toggle-between-minibuffer-and-embark-collect-completions)
         ("SPC"     . nil))
         ;; 这里就是关键的一个了,在 minibuffer 建立的时候,如果用户有输入那么自
         ;; 动就打开 embark 的 embark-collect-completions 功能。这里除了可以是
         ;; embark-collect-completions-after-input 外还可以是
         ;; embark-collect-completions-after-delay. 如字面意思,一个是在有用户输
         ;; 入后,一个是在等待了一段时间后。
         ;;
         ;; 这里有一个小插曲。如果同时开启了 `minibuffer-eldef-shorten-default`
         ;; 和 `minibuffer-electric-default-mode`,C-h v 时会发现即使用户没有输
         ;; 入,在使用 `embark-collect-completions-after-input` 的情况下它过一段
         ;; 时间还是会弹出来对应的窗口。这是因为
         ;; `minibuffer-eldef-shorten-default` 在实现的时间会修改 minibuffer
         ;; prompt, 所以自然也就会让 `embark` 认为有用户输入了。因此规避的措施是
         ;; 关闭 `minibuffer-eldef-shorten-default`.
  :hook ((minibuffer-setup . embark-collect-completions-after-input)
         ;; 令 `*Embark Collect Completion*` 的窗口最大不超过当前 frame 的 40%。
         ;; 不然一个补全窗口占据太多视野功能就有点过了.
         (embark-collect-post-revert . resize-embark-collect-completions))
  :config
  (defun resize-embark-collect-completions ()
    (fit-window-to-buffer (get-buffer-window)
                          (floor (* 0.4 (frame-height))) 1))

  (defun toggle-between-minibuffer-and-embark-collect-completions ()
    (interactive)
    (let ((w (if (eq (active-minibuffer-window) (selected-window))
                 (get-buffer-window "*Embark Collect Completions*")
             (active-minibuffer-window))))
      (when (window-live-p w)
        (select-window w t)
        (select-frame-set-input-focus (selected-frame) t))))

  ;; evil 用户才需要这个,不然就可以定义在 use-package 的 :map 里了
  (with-eval-after-load 'evil-collection
    (evil-collection-define-key 'normal 'embark-collect-mode-map
      (kbd "C-o") 'toggle-between-minibuffer-and-embark-collect-completions))

  ;; 不显示 `*Embark Collect Completions*` 窗口的 modeline,看起来简洁一点
  (add-to-list 'display-buffer-alist
               '("\\*Embark Collect \\(Live\\|Completions\\)\\*"
                 (display-buffer-at-bottom)
                 (window-parameters . ((no-other-window . t)
                                       (mode-line-format . none)))))
  :custom
  ;; 永远使用单列来显示补全候选项
  (embark-collect-initial-view-alist '((t . list)))
  (embark-collect-live-initial-delay 0.15)
  (embark-collect-live-update-delay 0.15))

演示效果视频 论坛不支持 mp4 挺难受的…

3 个赞

多谢PR!这样可以删除了。

问下为何要去ivy化呢,有什么特殊原因?而且也没有选用vertico和selectrum。来几张最后的效果图更好 :smile:

:rofl: 主要是我想仅只用少量一点的包,并且越来越觉得 counsel/swiper 不是那么重要,于是就先把这2个给干掉了。然后就觉得 ivy-mode 是不是也不是那么重要?就尝试了一下去 ivy 化。

总体来说还是想令自己的 Emacs 依赖的外部包少一点

1 个赞

有时候想去掉一个包就是单纯看不顺眼了 :rofl: 我干过好多这样的事情

28内置的project也够用了,其实

理解了。我也干过同样的事情,比如用内置的electric-pair替代smartparens。

去掉一个包容易,但是要去掉一个生态有时挺麻烦的,比如楼上说的project。内置的功能够用了,但是还需要时间发展下生态,第三方扩展包要跟上才行。现在projectile和flycheck我暂时没法去掉,包括ivy,很多好用的功能还没找到替代者 :joy:

BTW,内置的不一定是最好的,当然是最方便的。

1 个赞

https://elpa.gnu.org/packages/aggressive-completion.html

最近才发现的包,配合emacs29原生补全效果很棒,比mct丝滑

1 个赞

这个包是干啥的?minibuffer补全吗?

和mct和楼主的方案类似,使用原生补全时自动化了completion buffer的行为,但更优雅、高效 (个人认为

1 个赞

貌似和vertico功能差不多

我现在也转到 Emacs 29 + aggressive-completion 了。

看起来还是有很多人在推动原生 completions buffer 的。几乎所有的补全框架都是着眼于为 completing-read 函数提供更友好的界面,但是原生的实现实际上才是对输入体验干扰最小的。

依次尝试过下面几种,都是专门针对特定版本的 Emacs 的,作者都不打算继续兼容新版本 Emacs 了,估计内置的 completions buffer 就足够好用的那一天也不远了。

Emacs 27: live-completions

Emacs 28: mct

Emacs 29: aggressive-completion

我刚刚安装了aggressive-completion,只是打开了这个minor mode,比起vertico体验差很多。能分享一下配置吗? @solatle @tangxinfa

;; ignore cases when complete
(setq read-file-name-completion-ignore-case t)
(setq read-buffer-completion-ignore-case t)
(setq completion-ignore-case t)

;; disable help message in the minibuffer
(setq completion-show-help nil)
(setq completion-show-inline-help nil)
(setq completion-auto-help 'always)
(setq completion-styles '(basic partial-completion substring))
(setq completion-category-overrides '((buffer (styles . (substring flex)))
				      (file (styles . (substring flex)))))
(setq completions-format 'one-column)
(setq completions-max-height 20)
(setq completions-detailed t)
(add-to-list 'display-buffer-alist
	     '("\\*Completions\\*"
	       (display-buffer-reuse-window display-buffer-in-side-window)
	       (side . bottom)
	       (slot . 0)))

;; delete a whole repo name in a path
(defun my/minibuffer-backward-kill (arg)
  "When minibuffer is completing a file name delete up to parent
folder, otherwise delete a word"
  (interactive "p")
  (if minibuffer-completing-file-name
      ;; Borrowed from https://github.com/raxod502/selectrum/issues/498#issuecomment-803283608
      (if (string-match-p "/." (minibuffer-contents))
          (zap-up-to-char (- arg) ?/)
        (delete-minibuffer-contents))
    (delete-word (- arg))))
(define-key minibuffer-mode-map (kbd "M-d") #'my/minibuffer-backward-kill)
(define-key minibuffer-local-completion-map (kbd "SPC") nil)
(define-key minibuffer-mode-map (kbd "C-n") #'minibuffer-next-completion)
(define-key minibuffer-mode-map (kbd "C-p") #'minibuffer-previous-completion)

(aggressive-completion-mode 1)

多谢,我试试看。

aggressive-completion 表现迟钝&怪异?

我用最小配置体验了一下:

$ emacsq.sh -P aggressive-completion -M aggressive-completion-mode

也许 aggressive 的意思就是它会自做主张帮用户补齐输入内容, 以缩小候选范围吧,例如当用户输入:

`M-x agg|`

等待 aggressive-completion-delay (默认0.3秒) 时长之后,自动补齐为:(候选项缩小到 3 条)

`M-x aggressive-completion-|`

初次使用有点不知所措,总是“抢拍”。

似乎作者的理念是,只要手速够快,aggressive-completion 就不来“打扰”你就感觉不到 aggressive-completion 的存在。

aggressive-completion-delay 除了控制自动补完输入之外,也控制着弹窗延时。对于 helm/ivy/selctrum 用户来说,总是会在输入之后习惯性的期待弹窗出现,而这个延时就让这群用户觉得有点迟钝。

但又不能把延时设为 0,这样你永远都抢不过 aggressive-completion

另外再试了一下 @solatle #38 楼的配置:

$ emacsq.sh -P aggressive-completion --eval "(progn {code-from-#38})"

由于先前的体验,在我输入 M-x agg 之后,已经不惊讶出现以下画面了:

image

但是当我按下 C-n 的时候,它居然直接选择了第一项,把其它选项都过滤掉了:

image

这完全超出了我的理解范畴。还是我哪里配置/操作不对?