一路从 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)
(completions-format 'vertical)
(live-completions-sort-order 'cycle)
:defer nil
:bind
(:map minibuffer-local-completion-map
("C-p" . my/live-completions-previous-line)
("C-n" . my/live-completions-next-line)
("C-v" . my/live-completions-next-page)
("<C-return>" . my/live-completions-force-complete)
("M-g" . 'my/live-completions-goto-line))
(:map completion-list-mode-map
("C-g" . quit-window)
("TAB" . other-window)
("<C-return>" . my/live-completions-force-complete)
("M-g" . 'my/live-completions-goto-line)
("C-c C-o" . embark-export))
: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*")
12
(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 +1)
(defun my/completion-list-mode-hook-function ()
;; Fix line numbers which start from 5 in completions list.
(setq-local display-line-numbers-offset -4)
(display-line-numbers-mode +1)
(setq-local mode-line-format nil)
;; Default to 10 lines even if candidates less than 10.
(set-window-text-height (get-buffer-window (current-buffer)) 12)
(setq-local window-size-fixed t)
(hl-line-mode +1)
(face-remap-add-relative 'line-number :background 'unspecified :foregound 'unspecified :inherit 'default)
(face-remap-add-relative 'line-number-current-line :background 'unspecified :foregound 'unspecified :inherit 'default))
(add-hook 'completion-list-mode-hook #'my/completion-list-mode-hook-function)
(defun my/live-completions-next-line ()
(interactive)
(let ((old-line-number (line-number-at-pos)))
(ignore-errors
(next-line))
(when (eq old-line-number (line-number-at-pos))
(switch-to-completions)
(unless (eq (point-min) (point))
(next-line)))))
(defun my/live-completions-previous-line ()
(interactive)
(let ((old-line-number (line-number-at-pos)))
(ignore-errors
(previous-line))
(when (eq old-line-number (line-number-at-pos))
(switch-to-completions)
(unless (eq (point-min) (point))
(previous-line)))))
(defun my/live-completions-next-page ()
(interactive)
(switch-to-completions)
(scroll-up-command))
(defun my/live-completions-goto-line (n)
"Select candidate by M-<number> or input a line number."
(interactive
(list (let* ((type (event-basic-type last-command-event))
(char (if (characterp type)
;; Number on the main row.
type
;; Keypad number, if bound directly.
(car (last (string-to-list (symbol-name type))))))
(n (- char ?0)))
(if (zerop n) 10 n))))
(switch-to-completions)
(let ((line (if (<= n 10) n
(read-number "Select line: "))))
(goto-line (- line display-line-numbers-offset))
(choose-completion)))
(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)
;; M-<number> to select Nth candidate.
(dotimes (i 10)
(define-key minibuffer-local-completion-map (read-kbd-macro (format "M-%d" i)) 'my/live-completions-goto-line)
(define-key completion-list-mode-map (read-kbd-macro (format "M-%d" i)) 'my/live-completions-goto-line)))
;; orderless
(use-package orderless
:custom (completion-styles '(basic partial-completion orderless)))
;; consult
(use-package consult
:custom
(consult-project-root-function #'projectile-project-root)
:bind
(("C-x b" . consult-buffer)
("C-c C-s" . consult-line))
:config
(defun my/consult-recent-file ()
(interactive)
(recentf-mode +1)
(consult-recent-file)))
;; consult-flycheck
(use-package consult-flycheck)
;; marginalia
(use-package marginalia
:config
(marginalia-mode +1))
;; embark
(use-package embark
:bind
(:map minibuffer-local-completion-map
("C-c C-o" . embark-export)
("M-o" . embark-act))
(:map completion-list-mode-map
("C-c C-o" . embark-export)
("M-o" . embark-act))
:config
;; Hide the mode line of the Embark live/completions buffers
(add-to-list 'display-buffer-alist
'("\\`\\*Embark Collect \\(Live\\|Completions\\)\\*"
nil
(window-parameters (mode-line-format . none))))
(setq embark-action-indicator
(lambda (map _target)
(which-key--show-keymap "Embark" map nil nil 'no-paging)
#'which-key--hide-popup-ignore-command)
embark-become-indicator embark-action-indicator))
;; embark-consult
(use-package embark-consult
:after (embark consult)
:demand t
:hook
(embark-collect-mode . embark-consult-preview-minor-mode))
主要是 minibuffer 按上一行、下一行、翻页时自动转到在 completions 中执行, C-return 用于快速选择 completions buffer 的强制候选项,completions buffer 中的按 键输入自动做为 minibuffer 中的过滤输入 。
由于 consult marginalia embark 等可以用于原生的 completions,所以功能方面是不用 担心的,目前用起来感觉非常好。