糊了一个 corfu 适用的 all-the-icons 的 icon 效果

corfu 的官配是 kind-icon, 但是他需要 svg-lib,还要去下载一堆 svg,不是很喜欢,然后呢,kind-icon 官网那堆图标看起来总感觉大小不一,也不美观…然后想起来本来就已经安装了 all-the-icons 了,在以前使用 company-box 时候也是使用 all-the-icons 来显示图标的。所以仿照 kind-icon, 借鉴以前使用 company-box 时候的图标(也抄了点 centaur emacs 里面对于图标的配置,鸣谢一下),一起拼凑了一个 kind-all-the-icons.el 文件,可以用于显示 corfu 补全菜单的 icons。使用很简单,在 corfu 载入后 加上下面两句:

(require 'kind-all-the-icons)
(add-to-list 'corfu-margin-formatters 
               #'kind-all-the-icons-margin-formatter)

效果如下:

代码如下:

;;; kind-all-the-icons.el  -*- lexical-binding: t; -*-

(require 'all-the-icons)

(defvar kind-all-the-icons--cache nil
  "The cache of styled and padded label (text or icon).
An alist.")

(defun kind-all-the-icons-reset-cache ()
  "Remove all cached icons from `kind-all-the-icons-mapping'."
  (interactive)
  (setq kind-all-the-icons--cache nil))

(defun kind-all-the-icons--set-default-clear-cache (&rest args)
  (kind-all-the-icons-reset-cache)
  (apply #'set-default args))

(defvar kind-all-the-icons--icons
  `((unknown . ,(all-the-icons-material "find_in_page" :height 0.8 :v-adjust -0.15))
    (text . ,(all-the-icons-faicon "text-width" :height 0.8 :v-adjust -0.02))
    (method . ,(all-the-icons-faicon "cube" :height 0.8 :v-adjust -0.02 :face 'all-the-icons-purple))
    (function . ,(all-the-icons-faicon "cube" :height 0.8 :v-adjust -0.02 :face 'all-the-icons-purple))
    (fun . ,(all-the-icons-faicon "cube" :height 0.8 :v-adjust -0.02 :face 'all-the-icons-purple))
    (constructor . ,(all-the-icons-faicon "cube" :height 0.8 :v-adjust -0.02 :face 'all-the-icons-purple))
    (ctor . ,(all-the-icons-faicon "cube" :height 0.8 :v-adjust -0.02 :face 'all-the-icons-purple))
    (field . ,(all-the-icons-octicon "tag" :height 0.85 :v-adjust 0 :face 'all-the-icons-lblue))
    (variable . ,(all-the-icons-octicon "tag" :height 0.85 :v-adjust 0 :face 'all-the-icons-lblue))
    (var . ,(all-the-icons-octicon "tag" :height 0.85 :v-adjust 0 :face 'all-the-icons-lblue))
    (class . ,(all-the-icons-material "settings_input_component" :height 0.8 :v-adjust -0.15 :face 'all-the-icons-orange))
    (interface . ,(all-the-icons-material "share" :height 0.8 :v-adjust -0.15 :face 'all-the-icons-lblue))
    (i/f . ,(all-the-icons-material "share" :height 0.8 :v-adjust -0.15 :face 'all-the-icons-lblue))
    (module . ,(all-the-icons-material "view_module" :height 0.8 :v-adjust -0.15 :face 'all-the-icons-lblue))
    (mod . ,(all-the-icons-material "view_module" :height 0.8 :v-adjust -0.15 :face 'all-the-icons-lblue))
    (property . ,(all-the-icons-faicon "wrench" :height 0.8 :v-adjust -0.02))
    (prop . ,(all-the-icons-faicon "wrench" :height 0.8 :v-adjust -0.02))
    (unit . ,(all-the-icons-material "settings_system_daydream" :height 0.8 :v-adjust -0.15))
    (value . ,(all-the-icons-material "format_align_right" :height 0.8 :v-adjust -0.15 :face 'all-the-icons-lblue))
    (enum . ,(all-the-icons-material "storage" :height 0.8 :v-adjust -0.15 :face 'all-the-icons-orange))
    (keyword . ,(all-the-icons-material "filter_center_focus" :height 0.8 :v-adjust -0.15))
    (k/w . ,(all-the-icons-material "filter_center_focus" :height 0.8 :v-adjust -0.15))
    (snippet . ,(all-the-icons-material "format_align_center" :height 0.8 :v-adjust -0.15))
    (sn . ,(all-the-icons-material "format_align_center" :height 0.8 :v-adjust -0.15))
    (color . ,(all-the-icons-material "palette" :height 0.8 :v-adjust -0.15))
    (file . ,(all-the-icons-faicon "file-o" :height 0.8 :v-adjust -0.02))
    (reference . ,(all-the-icons-material "collections_bookmark" :height 0.8 :v-adjust -0.15))
    (ref . ,(all-the-icons-material "collections_bookmark" :height 0.8 :v-adjust -0.15))
    (folder . ,(all-the-icons-faicon "folder-open" :height 0.8 :v-adjust -0.02))
    (dir . ,(all-the-icons-faicon "folder-open" :height 0.8 :v-adjust -0.02))
    (enum-member . ,(all-the-icons-material "format_align_right" :height 0.8 :v-adjust -0.15))
    (enummember . ,(all-the-icons-material "format_align_right" :height 0.8 :v-adjust -0.15))
    (member . ,(all-the-icons-material "format_align_right" :height 0.8 :v-adjust -0.15))
    (constant . ,(all-the-icons-faicon "square-o" :height 0.8 :v-adjust -0.1))
    (const . ,(all-the-icons-faicon "square-o" :height 0.8 :v-adjust -0.1))
    (struct . ,(all-the-icons-material "settings_input_component" :height 0.8 :v-adjust -0.15 :face 'all-the-icons-orange))
    (event . ,(all-the-icons-octicon "zap" :height 0.8 :v-adjust 0 :face 'all-the-icons-orange))
    (operator . ,(all-the-icons-material "control_point" :height 0.8 :v-adjust -0.15))
    (op . ,(all-the-icons-material "control_point" :height 0.8 :v-adjust -0.15))
    (type-parameter . ,(all-the-icons-faicon "arrows" :height 0.8 :v-adjust -0.02))
    (param . ,(all-the-icons-faicon "arrows" :height 0.8 :v-adjust -0.02))
    (template . ,(all-the-icons-material "format_align_left" :height 0.8 :v-adjust -0.15))
    (t . ,(all-the-icons-material "find_in_page" :height 0.8 :v-adjust -0.15))))


(defsubst kind-all-the-icons--metadata-get (metadata type-name)
  (or
   (plist-get completion-extra-properties (intern (format ":%s" type-name)))
   (cdr (assq (intern type-name) metadata))))

(defun kind-all-the-icons-formatted (kind)
  "Format icon kind with all-the-icons"
  (or (alist-get kind kind-all-the-icons--cache)
      (let ((map (assq kind kind-all-the-icons--icons)))
          (let*  ((icon (if map 
                            (cdr map) 
                          (cdr (assq t kind-all-the-icons--icons))))
                  (half (/ (default-font-width) 2))
                  (pad (propertize " " 'display `(space :width (,half))))
                  (disp (concat pad icon pad)))
            (setf (alist-get kind kind-all-the-icons--cache) disp)
            disp))))

(defun kind-all-the-icons-margin-formatter (metadata)
  "Return a margin-formatter function which produces kind icons.
METADATA is the completion metadata supplied by the caller (see
info node `(elisp)Programmed Completion').  To use, add this
function to the relevant margin-formatters list."
  (if-let ((kind-func (kind-all-the-icons--metadata-get metadata "company-kind")))
      (lambda (cand)
	    (if-let ((kind (funcall kind-func cand)))
	        (kind-all-the-icons-formatted kind)
	      (kind-all-the-icons-formatted t))))) ;; as a backup


(provide 'kind-all-the-icons)
;;; kind-all-the-icons.el ends here
17 个赞

大佬,能否帮 lsp-bridge 写一个 corfu 前端?

应该就是整一个 capf 的函数,我这两天晚上抽空看下。

好的,谢谢大佬

不是有现成的吗

楼上那个 capf 函数看起来是没问题的,我测试一直出不来。发现简单敲一下, lsp-bridge 那个server 要跑好久好久,只有等他跑完了, (completion-at-point) 才能出来补全菜单。看了下,似乎是因为异步更新 lsp-bridge-completion-items, 刚敲的时候因为这个变量还是空,所以没有补全菜单,但是这个补全为啥 server 跑那么久… 搞不明白哪里出问题了。 nBwq8AqvZL

我在 lsp-bridge 设置的是 100 毫秒以后开始分析,因为 lsp-bridge 是完全异步的,如果 company 或者 corfu 补全过快就会导致 lsp-bridge-completion-items 还没有内容。

我的问题是 capf 能否支持中途更新的策略?

  1. 用户先写代码,不管 lsp server 分析多久
  2. 等 lsp-bridge 得到结果以后,我中途让 capf 更新,这样既不卡 Emacs 又可以最及时的返回补全信息给 Emacs

lsp-bridge 目前是敲了字符 100ms 以后就开始分析语法补全,如果你敲的很快 lsp server 就会很忙。 以后准备开放这个选项让用户自己去控制敲字符多久以后开始分析。

但是最核心的问题是 company 或 corfu 是否有机制支持中途异步更新 completion items ?

这个我再试试,目前我在异步返回的那个地方直接写了个 (completion-at-point) ,不过好像还不行。

如果 capf 支持异步更新的机制,你说的问题完全可以解决,我们随时沟通。 :grinning:

company 是支持异步补全的

candidates 这里支持传一个 '(:async . (lambda (callback) ...))

参考 Async autocompletion in Emacs - Kraken of Thought

completion-at-point-functions 应该也可以支持异步,可以参考Programmed Completion

一个简单的例子:

  (defun lsp-bridge-capf ()
    (let (
          (bounds (bounds-of-thing-at-point 'symbol)))
      (list (car bounds) (cdr bounds)
            (completion-table-dynamic
             ;; async here
             (lambda (prefix)
               (lsp-bridge-get-completion-items)))
            :exclusive 'no)))

恩,我的疑问是, lsp-bridge 这边获取到 completion items 以后,是 push 给Emacs这边的。

这段代码我没有明白的是,这里的回调函数 capf 是怎么知道什么时候获取 lsp-bridge push 给 Emacs 的 completion items的?

架构不一样,他们是用 company/corfu 来直接激活 completion 请求的。所以这个回调嵌在请求里面了。 lsp-bridge 原理好像是我敲着代码就自动去 completion, 然后回复来设置 completion-items 的值。

对, 因为 Emacs 这边的性能很弱,lsp-bridge为了彻底解决性能问题,是完全靠lsp-bridge的进程和子线程去触发 completion 请求, 如果接到 lsp server 请求后,先过滤掉过期的结果再push回Emacs更新补全内容。

company/corfru 如果是把回调嵌入请求,只有两条路:

  1. 像 lsp-mode 那样,Emacs过滤子进程消息,有消息的时候调用回调函数,这样做的缺点是把Emacs弱性能暴露在 lsp server 洪水返回的日志处理中,导致Emacs卡顿
  2. 写一个子线程,卡在 read 操作上,等 lsp-bridge 进程返回消息后再继续调用回调函数,但是这样做导致等待外部进程消息的函数会卡住 Emacs

剩下的方式只能看看 company 有没有暴力刷新的函数,lsp-bridge 得到push消息后强制更新 company 菜单内容,或者只能自己写一个 child frame 的补全前端去适应 lsp-bridge 的设计。

大概率菜单得自己写。无法获得其它源的候选项情况,company/corfru 也无法知道你源的状态。是应该与其它源合并 match、sort,还是已经过期杀掉线程丢弃候选。我的感受是:补全引擎没有 share 一说,天然独占。

哈哈哈哈,我五一节的时候用了 PyQt 来实现动态更新补全菜单的时候是实时补全的,没想到切换到 company-mode 会遇到这种设计问题。

这几天休息的时候,看看社区大佬们有没有好的方法, 早上读了 Async autocompletion in Emacs - Kraken of Thought 发现也不行。

等我这两天设计一下,看看能否写3个前端,分别用 child-frame, overlay 和 Qt, 满足大家使用 lsp-bridge 的各种场景吧。

3 个赞

大佬的工作量一下子又上去了 :joy_cat:

你知道我为什么迫切想改善补全功能的性能,但又拖了这么久了吧? 这玩意就是比EAF坑100倍的神坑。

1 个赞

一点浅见:

补全引擎后端本质上是一个小型的任务调度系统,前端是支持异步更新的 menu items 和 doc view。vim/nvim 的 pum 是个不支持异步更新的坑,只能通过类似双缓冲、debouce、cache,合并请求等方式减少请求降低菜单闪烁和延迟。coc.nvim 是这方面的佼佼者。再就是 cmp-nvim.lua,前期用的 pum,后期重构现在使用 float window 构建,终于彻底摆脱限制。

因为补全菜单的独占性,要么在 company 等框架下运作,只提供补全源不参与任务调度、fuzzy match 等,要么从头构建,做好接入其它源的预备工作,提供 elisp 到 python 的接口。也就是一个全功能、可扩展的、不与其它 engine 兼容的补全框架。没有 middle ground。确实是个神坑。 :sweat_smile: