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

一路从 helm -> ivy -> selectrum,最终决定使用改造后的原生的 completions buffer。

历史总是惊人的相似,ivy 说 helm 太重太慢、selectrum 说 ivy API 太复杂,其实所有 项目成熟了就会显得笨重复杂。

我从 helm 转 ivy,是发现 helm 就像 windows 下的 Menu,用鼠标或键盘瞄来瞄去才能完 中自已要的,非常累。用上 ivy 后发现 ivy 把它做成了一个列表 ListView,但是又不能 用 Emacs 的内置编辑方式随意编辑列表项。然后最近用上了 selectrum,发现 selectrum 是做成了 Combox,它要好很多,让你可以随意编辑列表项。

最终我意识到这些东西都干扰了我常用的编辑状态:盲打,绝大部分时候我都不需要看完成 列表中的内容就可以通过手工输入按 Tab 键来达到我的目的,但是 helm/ivy/selectrum 会主动抢占输入,而且会自做主张的决定完成列表中哪一个被选中(通常是第一个),从此 我在 minibuffer 中按回车键的时候,列表中当前选中的是哪一项就非常关键了。最简单的 一个例子就是将文件拷贝到另一个目录,目标目录在的完成列表很可能被 ivy 等选中了第 一个文件,此时动作快一点就把那个文件给覆盖了。这导致日常的工作变得压力山大。

我发现其实 windows 原生的 completions buffer 只要解决一些小问题就很完美了: 1, 单列且固定高度 2, 能够自动更新 3, 能够快速过滤完成列表、无缝地跳转 minibuffer/completions buffer

前两者 live-completions 就可以解决,最后一点做一些小设置也可以解决,我的配置如下:

;; live-completions
(use-package live-completions
  :quelpa (live-completions :fetcher github :repo "oantolin/live-completions" :files ("*"))
  :custom
  (live-completions-columns 'single)
  :bind
  (:map minibuffer-local-completion-map
        ("C-p" . switch-to-completions)
        ("C-n" . my/live-completions-next-line)
        ("C-v" . my/live-completions-next-page)
        ("<C-return>" . my/live-completions-force-complete))
  (:map completion-list-mode-map
        ("C-g" . quit-window)
        ("TAB" . other-window)
        ("<C-return>" . my/live-completions-force-complete))
  :config
  (live-completions-mode +1)
  ;; set completions buffer with fixed height
  (defvar my/live-completions--old-temp-buffer-max-height temp-buffer-max-height)
  (defun my/live-completions--temp-buffer-max-height (buffer)
    (if (string= (buffer-name buffer) "*Completions*")
        10
      (funcall my/live-completions--old-temp-buffer-max-height buffer)))
  (setq temp-buffer-max-height #'my/live-completions--temp-buffer-max-height)
  (temp-buffer-resize-mode)
  (defun my/completion-list-mode-hook-function ()
    (setq-local hl-line-sticky-flag nil)
    (setq-local mode-line-format nil)
    (set-window-text-height (get-buffer-window (current-buffer))  10)
    (setq-local window-size-fixed t)
    (hl-line-mode +1))
  (add-hook 'completion-list-mode-hook #'my/completion-list-mode-hook-function)
  (defun my/live-completions-next-line ()
    (interactive)
    (switch-to-completions)
    (next-line))
  (defun my/live-completions-next-page ()
    (interactive)
    (switch-to-completions)
    (scroll-up-command))
  (defun my/live-completions-force-complete ()
    (interactive)
    (if (active-minibuffer-window)
      (select-window (active-minibuffer-window)))
    (minibuffer-force-complete)
    (minibuffer-complete-and-exit))
  (defun my/live-completions-self-insert-command (n)
    (interactive "p")
    (if (active-minibuffer-window)
        (select-window (active-minibuffer-window)))
    (self-insert-command n))
  (define-key completion-list-mode-map [remap self-insert-command] 'my/live-completions-self-insert-command))

;; orderless
(use-package orderless
  :custom (completion-styles '(orderless)))

主要是 minibuffer 按上一行、下一行、翻页时自动转到在 completions 中执行, C-return 用于快速选择 completions buffer 的强制候选项,completions buffer 中的按 键输入自动做为 minibuffer 中的过滤输入 。

由于 consult marginalia embark 等可以用于原生的 completions,所以功能方面是不用 担心的,目前用起来感觉非常好。

2赞

试试icomplete + icomplete-vertical?

我为了达到你不太喜欢的行为还特地改了改键位

;; 不太建议用fido-mode
;; 动了好多`completion-*'的变量
;; 这里的配置是为了模拟ivy的行为,原生icomplete的行为比较类似1f描述的
  (define-key icomplete-minibuffer-map (kbd "RET") 'icomplete-fido-ret)
  (define-key icomplete-minibuffer-map (kbd "DEL") 'icomplete-fido-backward-updir)
  (define-key icomplete-minibuffer-map (kbd "M-m") 'minibuffer-complete-and-exit)

(general-define-key
   :keymaps 'icomplete-minibuffer-map
   [?\t] 'icomplete-force-complete ; keep up with ivy or selectrum
   "C-c C-o" 'embark-export
   )

试用了一下 fido-mode 感觉 icomplete-fido-ret 这个行为有点怪异,其实想要的行为就是直观的选择,目前是在用 vertico

我上面的键位是为了贴近ivy的设置,

只用icomplete不用fido即可

略微尝试了一下 icomplete icomplete-vertical 感觉在盲打输入方面要比 ivy selectrum 好一些,但是选择候选项的直观性比 ivy selectrum 要差一些,还有就是 echo 中的干扰提示信息比较多。当然估计也是我没有做细致的定制的原因。

image 定制一下face和几个变量吧

  (setq icomplete-separator (propertize "₩\n" 'face  '(foreground-color . "SlateBlue1")) 
		icomplete-compute-delay 0
		icomplete-show-matches-on-no-input t
		icomplete-hide-common-prefix nil
		icomplete-tidy-shadowed-file-names t
		)
  ;; highlight current selected
  (custom-set-faces '(icomplete-first-match ((t (:inherit highlight)))))

干扰信息可能是marginalia的问题?

icomplete-modeselectrum 卡,我本来也是用的 icomplete-mode,后来体验了一下 selectrum 发现在 tramp 的情况下还能快速补全就切换过去了。

在看邮件列表的时候看到了 prot 分享的配置

https://lists.gnu.org/archive/html/emacs-devel/2021-04/msg01324.html

这里有个附件可以用 emacs -Q, eval-buffer 体验一下。它的效果与楼主想要的应该差不多,都是使用的自带的 completion 机制, 并且它完全没有使用 icomplete.

重新发明 helm/ivy/selectrum 的前奏。

icomplete-mode 确实在启动minibuffer的时候会卡一秒左右,不知道是我配置的问题不

helm ivy selectrum 之类的工具因为接管了 completing-read,所以它们被使用的上下文是未知的,它们自行决定选中某个候选项这种行为本身是很危险的,我看到这些工具对比只字不提,更多是在代码设计、交互体检、功能完备度上做文章。Helm 的 Menu 式、Ivy 的 ListView 式和 Selectrum 的 ComBox 式,它们都是 GUI 一种风格,毕竟人各有所好。只是不适合做为 completing-read 的统一交互方式。

是跟 live-complections 的单列很相似。原生的 completion buffer 体验太差了,单列加上交互性改进一下,就基本具有可用性了。但另一方面 helm ivy selectrum 又做过头了,失去了 emacs 文本编辑功能方面的精华。

icomplete 卡的原因大概是因为:

  1. 一次性处理所有匹配的条目。

    在“零”配置情况下,函数约 7.8k 个,命令约 2.8k 个,M-x i 也有 200 多项,全配置就更多了。

  2. 重复迭代。

    icomplete 会在 completion- 迭代的结果上重复迭代:

    completion-all-sorted-completions	;; 返回匹配的条目
        |
        V
    icomplete--sorted-completions      	;; 再次迭代
        |
        V
    icomplete-completions             	;; 显示到 minibuffer (直到这时才根据显示宽度进行截短)
    

如果把 icomplete 改造成下拉列表的样式,然后在每个符号旁边都显示一行文档,那么可以肯定它比 ivy/selectrum 都慢,甚至可能比 helm 还慢(helm 虽慢但是有缓存)。

ivy 在取得列表之后,并不全部显示,而是根据弹窗的高度,取 n 条显示(selectrum 好像也是这么做的)。并提供 API 方便用户对这 n 项可见条目进行再次处理而几乎不影响性能。

1赞