关于 company-box 的子frame 的 resize 和 position-reset 的 bug

关于这件事还得从这几天试用 company-box 遇到的他的tooltip的 childframe绘制的问题开始谈起。

之前在18年的时候我试用过company-box,但是因为当时它的性能问题而放弃了, 又因为之前一直在使用26版本的emacs,新版的company-box在其之上依然很卡, 遂搁置了很久。

由于种草于centaur-emacs很久了,想探索一些新的包或者有一些配置上的 实现需要借鉴的,我都会到它那里瞟一下。虽然很久就看到centuar-emacs引用 了company-box这个包,并且是默认在gui session中开启的,而且它除了对icon 映射的一些自定义外并没有什么性能上的patch(当然除了限制了一下 candidates的长度),于是乎我就很疑惑了,不知道大佬是怎么在company-box 的lag中愉快的玩耍的,难道颜值既是正义?

最近想对自己的配置做了pdumper的调优,于是把自己的emacs版本升级到了 emacs-27分支上, 使用27.0.91的tag。并且在手闲之际,再度试用了一下2020年 的company-box更新,虽然还是有点卡,但是相比之前可以说可以接受了(之后 在26.3上又试用了一下,依然卡,看来是emacs 27版本的某些优化吧),但是发 现一个问题:

在candi列表的滚动中,随着candidtes的更新,tooltip的位置和大小虽然也 会做相应的更新,但是母frame中会有它之前的残影,也就是之前的tooltip布 局还在,于是乎就会出现这么一种情况既candi列表的滚动怎么也不会到底, 这其实一种错觉,因为如果心得candi列表比之前的短的话,那么新绘制的 tooltip的高度当然比之前要短,如果之前的布局还在的话,两者一重叠,那 么就很会出现这种情况。而且在tooltip右边的滚动条也会随着当前candi列表 的最大宽度的变化而留残影,至此我想这两者是同一个问题。

于是乎我怀疑是company-box的tooltip的size和position更新机制的bug,于是 看了一下源码,发现它是使用childframe来绘制candi列表的,并且更新很自然 的就是对childframe的parameter做调整和使用set-frame-position 来做位置 信息更新,常规操作,没有错误啊,于是我陷入了苦思,很显然我开始怀疑是不 是之前自己的配置中的某些advice导致的?或者是使用的众多包中有什么不兼容?

于是乎用vanilla配置试了一下,同样,那么这个,我开始怀疑是emacs-devel的 问题,于是看了一下commit log,发现了在c49d379f17bcb0中有对childframe 的相关更新,发现了一个选项x-gtk-resize-child-frames, 详情如下:

If non-nil, resize child frames specially with GTK builds. If this is nil, resize child frames like any other frames. This is the default and usually works with most desktops. Some desktop environments (GNOME shell in particular when using the mutter window manager), however, may refuse to resize a child frame when Emacs is built with GTK3. For those environments, the two settings below are provided.

If this equals the symbol ’hide’, Emacs temporarily hides the child frame during resizing. This approach seems to work reliably, may however induce some flicker when the frame is made visible again.

If this equals the symbol ’resize-mode’, Emacs uses GTK’s resize mode to always trigger an immediate resize of the child frame. This method is deprecated by GTK and may not work in future versions of that toolkit. It also may freeze Emacs when used with other desktop environments. It avoids, however, the unpleasent flicker induced by the hiding approach.

This variable is considered a temporary workaround and will be hopefully eliminated in future versions of Emacs.

这才想到自己使用的是用gtk编译linux上的emacs,那么对应的这个选项中的 hide 值就是我可以一试的地方。果然,改善了,虽然就像之中说的有闪烁的 问题,在make invisible到重新创建childframe的过程中。

所以根据这条提示,我有两种方案可以不完美解决这个问题:

  1. 很简单直接 (setq x-gtk-resize-child-frames 'hide),但是这种方法在 其描述中有提到这个变量是个暂时的解决方案,并且在未来的正式版中可能 会被移除,因此加上 boundp 的判断是必须的。

  2. 这第二种方案是在第一种方案中提到的暂时变量不可用的情况下,并且上游 仍然没有解决这个问题的情况下使用的,就是咋每次窗口高度更新的时候强 制先make invisible它,等相关参数更新完成后在打开它:

(defvar my--company-box-company-prefix nil)
(defvar my--company-box-company-candidates-length nil)

(defun company-box-frontend (command)
  "`company-mode' frontend using child-frame.
COMMAND: See `company-frontends'.

NOTE: this function has been redefined for temporal bug fake fix
due to the emacs child-frame bug."
  (unless (stringp my--company-box-company-prefix)
    (setq my--company-box-company-prefix company-prefix))
  (unless my--company-box-company-candidates-length
    (setq my--company-box-company-candidates-length
          company-candidates-length))
  (cond
   ((eq command 'hide)
    (company-box-hide)
    (setq my--company-box-company-prefix nil
          my--company-box-company-candidates-length nil))
   ((and (equal company-candidates-length 1)
         (null company-box-show-single-candidate))
    (company-box-hide))
   ((eq command 'update)
    (when (or
           ;;; Update according to prefix length compare
           ;; (and (or (string-prefix-p my--company-box-company-prefix
           ;;                           company-prefix)
           ;;          (string-prefix-p company-prefix
           ;;                           my--company-box-company-prefix))
           ;;      (> (abs (- (length company-prefix)
           ;;                 (length my--company-box-company-prefix)))
           ;;         3))
           nil
           (>= (abs (- my--company-box-company-candidates-length
                       company-candidates-length))
               1))
      (setq my--company-box-company-prefix company-prefix)
      (setq my--company-box-company-candidates-length
            company-candidates-length)
      (company-box-hide))
    (company-box-show))
   ((eq command 'post-command)
    (company-box--post-command))))

第二种方案无法解决scrollbar重影的问题,因为scrollbar是通过side-window 来实现的,还需要写一个patch基于宽度改变关闭重开side-window的原理。因为 问题不大,就不管了,不然又是一笔性能开销。

至此,本帖结束,希望对其他也有此问题的朋友有所帮助,同时也希望大家分享 其他解决方案,另外如果有对emacs-devel熟悉的朋友,想向你了解下,emacs gtk环境下的这个 x-gtk-resize-child-frames 选项的最终演进会是什么,以 及emacs27正式版中是否会对此给出最终解决方案?

1赞

gnome-shell 无法正确使用 company-posframe, 很早就有人提出了,不过我英语水平不行,和 emacs 开发者沟通不畅,一直没法解决,后来company 的维护者试用 posframe ,遇到了同样的问题,经过他们两个月的找问题,测试,讨论,最后把这个问题基本得到解决,就是你看的方案,估计短期内几乎不会有其他好的方案

_(:з」∠)_

楼主分析得可以,能不能分析一下这个 company-mode 的问题?个人对Emacs内部机制不是很了解,毫无头绪

image

感谢大佬告知, 我好像找到了相关的mailing-list: https://lists.gnu.org/archive/html/emacs-devel/2020-01/msg00515.html。 如果稳定的话,那我就在27版本的配置就不改了。

对了还有tumashu大佬,company-posframe 为什么滚动selection的时候有点卡?(我没有看源代码),还有有没有 选项改变help-window的size随doc的内容自适应,目前总是一个大方块。

我记得company的native的tooltip使用的是overlay机制,在计算宽度的时候是好像用的char-width,应该和popup包是一样的原理,在gui下因为存在中英文等宽的问题,因此每个selection的位置计算结果其实是和显示效果有出入的,我建议直接使用company-posframe这样的childframe机制的tooltip, 他们可以使用pixel-wise机制计算位置,按道理说position的计算更精确, 而且本来一个frame就是一个大方块,也不存在这个问题了。

但是这个问题在terminal里就没了,所以我只在terminal里用原生tooltip(terminal里不支持chiledframe所以也没其他选择)。

暂时没有设置选项,因为那样比较难看,不过欢迎pr

company-box 确实有性能问题,不过由于主要用macOS,所以没有遇到太大的瓶颈。另外,其他因素也会导致上下移动会卡,比如linum、highlight-indent-guides、候选词多少等等,就看个人的选择了。你可以profiling看看。x-gtk-resize-child-frames 选项的演进就没研究过了,最近没有怎么使用Linux。

把字体设成等宽字体?