emacs世界有才华的同学很多,但像 ivy, selectrum, helm等长期维护者却特别难找
是的,开源项目要维护下去需要很多的时间和精力,还有毅力
不是你怎么用的问题。而是 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 也不远了。
如果仅仅是在内置的包上做扩展,恐怕很难有超越。
在某个特定的方面(例如这儿说的completion),已经有内置的功能之后还有这么多外部的包,那说明内置的肯定不够好。 Consult的作者最近经常提交completion相关的patch,就我最近关注到的Consult出现的一些跟内置功能缺陷相关的bug,说明competion这部分还有挺多可以提升的。
信ivy,保平安!我觉得ivy要好用得多。
再举几个例子,flymake/flychek,project/projectile,vc mode/magit,应该还有挺多的。
这几个就是最典型的例子。目前我的选择还是ivy系列。
最近完成了去 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
。但是因为功能都与 embark
的 embark-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 挺难受的…
多谢PR!这样可以删除了。
问下为何要去ivy化呢,有什么特殊原因?而且也没有选用vertico和selectrum。来几张最后的效果图更好
主要是我想仅只用少量一点的包,并且越来越觉得 counsel/swiper 不是那么重要,于是就先把这2个给干掉了。然后就觉得 ivy-mode 是不是也不是那么重要?就尝试了一下去 ivy 化。
总体来说还是想令自己的 Emacs 依赖的外部包少一点
有时候想去掉一个包就是单纯看不顺眼了 我干过好多这样的事情
28内置的project也够用了,其实
理解了。我也干过同样的事情,比如用内置的electric-pair替代smartparens。
去掉一个包容易,但是要去掉一个生态有时挺麻烦的,比如楼上说的project。内置的功能够用了,但是还需要时间发展下生态,第三方扩展包要跟上才行。现在projectile和flycheck我暂时没法去掉,包括ivy,很多好用的功能还没找到替代者
BTW,内置的不一定是最好的,当然是最方便的。
这个包是干啥的?minibuffer补全吗?
和mct和楼主的方案类似,使用原生补全时自动化了completion buffer的行为,但更优雅、高效 (个人认为
貌似和vertico功能差不多
我现在也转到 Emacs 29 + aggressive-completion 了。
看起来还是有很多人在推动原生 completions buffer 的。几乎所有的补全框架都是着眼于为 completing-read
函数提供更友好的界面,但是原生的实现实际上才是对输入体验干扰最小的。
依次尝试过下面几种,都是专门针对特定版本的 Emacs 的,作者都不打算继续兼容新版本 Emacs 了,估计内置的 completions buffer 就足够好用的那一天也不远了。
Emacs 27: live-completions
Emacs 28: mct
Emacs 29: aggressive-completion
;; 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
。
$ emacsq.sh -P aggressive-completion --eval "(progn {code-from-#38})"
由于先前的体验,在我输入 M-x agg
之后,已经不惊讶出现以下画面了:
但是当我按下 C-n
的时候,它居然直接选择了第一项,把其它选项都过滤掉了:
这完全超出了我的理解范畴。还是我哪里配置/操作不对?