仅仅使用一个 TAB 键,让 company 和 yasnippet 一起舒服的工作。(样版代码及新手教学)

在你使用 auto-complete/company 的时候,有没有遇到和 yasnippet 一起使用时,TAB 键冲突的问题呢? 你是如何优雅的使用 auto complete, 同时还能享受随时展开自己定义的一大堆 snippet 红利呢?

如果你遇到类似的难题,那么这篇介绍的帖子就是你的菜,否则,你可能首先得了解一下稍后介绍的一些使用 auto complete 的习惯,以及该如何使用 Yet another snippet.

我会尽可能介绍的浅显易懂,希望也可以帮到从未用过 company, yasnippet 的初学者。

不废话,先贴配置。

company 配置

(require 'company)

(defun set-company-tab ()
  (define-key company-active-map [tab] 'company-select-next-if-tooltip-visible-or-complete-selection)
  (define-key company-active-map (kbd "TAB") 'company-select-next-if-tooltip-visible-or-complete-selection)
  )

(set-company-tab)

(define-key company-active-map (kbd "<backtab>") 'company-select-previous)
(define-key company-active-map (kbd "S-TAB") 'company-select-previous)
(define-key company-active-map (kbd "C-s") 'company-filter-candidates)
(setq company-show-numbers t)

(setq company-frontends
      '(company-pseudo-tooltip-unless-just-one-frontend
        company-preview-frontend
        company-echo-metadata-frontend))

(add-hook 'company-mode-hook '(lambda ()
                                (setq company-backends
                                      (delete 'company-bbdb
                                              (delete 'company-oddmuse
                                                      (delete 'company-cmake
                                                              (delete 'company-clang company-backends)))))
                                ))

(defun advice-only-show-tooltip-when-invoked (orig-fun command)
  "原始的 company-pseudo-tooltip-unless-just-one-frontend-with-delay, 它一直会显示
candidates tooltip, 除非只有一个候选结果时,此时,它会不显示, 这个 advice 则是让其
完全不显示, 但是同时仍旧保持 inline 提示, 类似于 auto-complete 当中, 设定
ac-auto-show-menu 为 nil 的情形, 这种模式比较适合在 yasnippet 正在 expanding 时使用。"
  (when (company-explicit-action-p)
    (apply orig-fun command)))

(defun advice-always-trigger-yas (orig-fun &rest command)
  (interactive)
  (unless (ignore-errors (yas-expand))
    (apply orig-fun command)))

(with-eval-after-load 'yasnippet
  (defun yas/disable-company-tooltip ()
    (interactive)
    (advice-add #'company-pseudo-tooltip-unless-just-one-frontend :around #'advice-only-show-tooltip-when-invoked)
    (define-key company-active-map [tab] 'yas-next-field-or-maybe-expand)
    (define-key company-active-map (kbd "TAB") 'yas-next-field-or-maybe-expand)
    )
  (defun yas/restore-company-tooltip ()
    (interactive)
    (advice-remove #'company-pseudo-tooltip-unless-just-one-frontend #'advice-only-show-tooltip-when-invoked)
    (set-company-tab)
    )
  (add-hook 'yas-before-expand-snippet-hook 'yas/disable-company-tooltip)
  (add-hook 'yas-after-exit-snippet-hook 'yas/restore-company-tooltip)

  ;; 这个可以确保,如果当前 key 是一个 snippet, 则一定展开 snippet,
  ;; 而忽略掉正常的 company 完成。
  (advice-add #'company-select-next-if-tooltip-visible-or-complete-selection :around #'advice-always-trigger-yas)
  (advice-add #'company-complete-common :around #'advice-always-trigger-yas)
  (advice-add #'company-complete-common-or-cycle :around #'advice-always-trigger-yas)
  )


(setq company-auto-commit t)
;; 32 空格, 41 右圆括号, 46 是 dot 字符
;; 这里我们移除空格,添加逗号(44), 分号(59)
;; 注意: C-x = 用来检测光标下字符的数字,(insert 数字) 用来测试数字对应的字符。
(setq company-auto-commit-chars '(41 46 44 59))

(add-hook 'after-init-hook 'global-company-mode)

(provide 'company_init)

;;; company_init.el ends here

你可以 (require 'company_init) 来加载这个文件。

company 配置简单介绍

首先在这个配置下,使用自动完成,是遵循一些早期在 AC 下面的一些惯例来着,下面列出来。

  1. 在只有唯一的一个 candidate 的时候, 可以使用 TAB/回车 来自动完成.
  2. 如果不止一个 candidates, 此时,TAB 的功能是选择下一个 candidate.
  3. 任何时候,回车键,总是完成当前选择的 candidate

当然还有其他玩法,但是本配置采用的是以上玩法,下面是一个示例:

image

在下面的介绍中,我会将上面截图中显示 候选列表菜单‘ 的那个弹窗叫做 tooltip. (company 官方叫法), 自动出现在光标后面的那一部分自动完成的字符(这里是 idate`),我称之为 overlay, 也有叫做 inline 的

开始介绍配置:

上面的 (set-company-tab) 这个 function, 就是在设定 TAB 在 tooltip 出来之后的行为, 我的选择就是前面介绍的行为,company-select-next-if-tooltip-visible-or-complete-selection.


(define-key company-active-map (kbd "<backtab>") 'company-select-previous)
(define-key company-active-map (kbd "S-TAB") 'company-select-previous)
(define-key company-active-map (kbd "C-s") 'company-filter-candidates)
(setq company-show-numbers t)

上面几行代码,设定了Shift + TAB, 可以反着往回选。 也可以像 helm 那样, 通过 C-s, 直接根据关键字, narrow 匹配的 candidates. 同时,也可以 Alt + 1, Alt + 2, 这种直接选择 candidate.


(setq company-frontends
      '(company-pseudo-tooltip-unless-just-one-frontend
        company-preview-frontend
        company-echo-metadata-frontend))

上面的代码,就是将 company 前端当中的 company-preview-if-just-one-frontend 替换为 company-preview-frontend, 这带来的效果就是,无论有几个 candidates, 总会显示 overlay, 控制类似功能的变量在 AC 里也有,叫做 ac-auto-show-menu


(add-hook 'company-mode-hook '(lambda ()
                                (setq company-backends
                                      (delete 'company-bbdb
                                              (delete 'company-oddmuse
                                                      (delete 'company-cmake
                                                              (delete 'company-clang company-backends)))))
                                ))

然后,我删除了一些对我没啥用的 backends.

当遇到 yasnippet.

接下来, 描述下当引入 yasnippet 之后,我想解决的问题.

  1. yas 也使用同样的快捷键 TAB 来自动展开(expand)我们自己写的模板, 例如,你定义了一个 snippet, 他使用 pr 作为 key, 当你键入 pr 之后, 它会为你 expand 成 println!("{}", |), 但同时,如果你开起了 company, 当你键入 pr 时,company 可能会给你一大堆像下面一样的 candidates 建议。

image

那么, 此时按下 TAB 有两个选择:

  • 选择下一个 candidate, 也就是那个 priv.
  • 展开 pr 为 println!

此时,我希望总是优先展开我自己定义的 snippet。

  1. 当使用 yas 在 expanding 过程中时, yas 同样使用 TAB 切换到下一个 tab stop field. 详情见 Writing snippets

此时,我的选择是:

  • 我不想受到 company 的 tooltip 干扰,这个时候出个 menu, 会很烦.
  • 我也不想在此时使用 TAB 来自动完成 overlay, 如果我真想这样,我可以用回车键。

如果能看明白上面介绍的痛点,那么再来继续看代码,如果不明白,之有先自己试试了。


继续介绍代码

(add-hook 'yas-before-expand-snippet-hook 'yas/disable-company-tooltip)
(add-hook 'yas-after-exit-snippet-hook 'yas/restore-company-tooltip)

这两行代码解决的问题是, 当 yas 正在 expanding 的时候,关闭 tooltip, 并且使用 TAB 键时,总是切换到下一个 tab stop field, 如果希望 auto complete, 可以使用回车。 当 yas 展开完成后,会自动恢复成原来的配置。


  (advice-add #'company-select-next-if-tooltip-visible-or-complete-selection :around #'advice-always-trigger-yas)
  (advice-add #'company-complete-common :around #'advice-always-trigger-yas)
  (advice-add #'company-complete-common-or-cycle :around #'advice-always-trigger-yas)

上面三行代码,解决的是,无论你使用 company 提供的无论哪种策略的来 complete, 使用 yas 展开 snippet 总是优先。

感谢

在编写本贴时,我注意到有坛子里已经有一些类似的帖子,但是在我编辑时, 并没有看过或参考过, 所以一并贴在这里,可以一起学习,阅读.

最后,感谢电报群里面 Youmu(@Youmu), 1ab(@stanley_110101011), 爱丽冥王星(@casouri) 以及等等早期帮过我,都可能记不得名字的大佬, 是你们不吝赐教,才有了上面的这个 company 配置。

有任何疑问欢迎评论,也欢迎指正。

18赞

aboabo这篇 Using digits to select company-mode candidates省去按alt

2赞

company 有个 tng 模式可以自动选择首项,感觉非常好用。

抄过来了,可以用,多谢,先试上一段时间。

另外, 我没有解绑回车。

看了下介绍,挺有趣,不过就像里面说的那样,这个模式不适合和 yas 一起工作。

https://github.com/company-mode/company-mode/blob/master/company-tng.el

通过一些设置就可以了,我就同时用 tng 和 yasnippet

我也没绑回车,还做了点小改动,方便输入数字。

(defun ora-company-number ()
  "Forward to `company-complete-number'.

Unless the number is potentially part of the candidate.
In that case, insert the number."
  (interactive)
  (let* ((k (this-command-keys))
         (re (concat "^" company-prefix k))
         ;; 数字、小数直接输入
         (digits "^[[:digit:].]+"))
    (if (cl-find-if (lambda (s) (or (string-match digits s)
                               (string-match re s)))
                    company-candidates)
        (progn
          (self-insert-command 1))
      (company-complete-number (if (zerop (string-to-number k))
                                   10
                                 (string-to-number k))))))

我试了下不太好搞,开启 tng 之后,overlay 没了,tooltip 又出来了,回车变成默认换行了, 看来,同样一个包,每个人使用方式完全不一样啊。

虽然这位坛友自己的版本已经修复了,但是还是发一下,abo-abo的blog里面,0不能正确地选到第10个,但只是blog没有更新,他的代码里已经改正了: https://github.com/abo-abo/oremacs/blob/9c1dd95f52bd6f65313c50c1a85c8bacdde74581/modes/ora-company.el

/cc @redguardtoo (知乎上看到的)

1赞

嗯,回头我也开个贴讲下自己的 company 配置。。

原来还能这样,,之前自己配置的时候实在没办法,用general.el把yas展开定义成 =SPC SPC= 空格连按了 :sweat_smile:还好摁起来还算顺手。。。。

谢谢。

我又想到了company可以类比于输入法,实际上0键很少用到。可以用于特定命令如company-filter-candidates ,

(defvar my-company-zero-key-for-filter nil
  "If t, pressing 0 calls `company-filter-candidates' per company's status.")

(defun my-company-number ()
  "Forward to `company-complete-number'.
Unless the number is potentially part of the candidate.
In that case, insert the number."
  (interactive)
  (let* ((k (this-command-keys))
         (re (concat "^" company-prefix k))
         (n (if (equal k "0") 10 (string-to-number k))))
    (cond
     ((or (cl-find-if (lambda (s) (string-match re s)) company-candidates)
          (> n (length company-candidates))
          (looking-back "[0-9]+\\.[0-9]*" (line-beginning-position)))
      (self-insert-command 1))

     ((and (eq n 10) my-company-zero-key-for-filter)
      (company-filter-candidates))

     (t
      (company-complete-number n)))))

谢谢分享。当时看了博客,发现有不方便地方就改了下,大佬写得简洁多了。不明白 company-filter-candidates 有什么作用,看文档及 C-s 都没发现特别的地方。

类似于company-search-candidates,输入额外的keyword搜索candidates。filter只显示匹配keyword的candidates。

还是没能体会到优点,似乎是选项多时有用?直接输入缩小范围好像更直观。

额外的keyword可以距离prefix比较远,不一定是紧挨着的。

多谢,这个功能几天用下来,蛮好的,没遇到不方便的情况。

后来我发现, 其实这个省略 alt, 其实并不方便, 反倒是有时候想继续输入数字时, tooltip 出来, 直接选择 candidate 了.

我修改了下数字的情况,好像遇到的情况少些.

(defun ora-company-number ()
  "Forward to `company-complete-number'.

Unless the number is potentially part of the candidate.
In that case, insert the number."
  (interactive)
  (let* ((k (this-command-keys))
         (re (concat "^" company-prefix k))
         (digits "^[[:digit:]\\.]+")
         (n (if (equal k "0")
                10
              (string-to-number k))))
    (if (or (cl-find-if (lambda (s) (or (string-match digits s)
                                        (string-match re s)))
                        company-candidates)
            (> n
               (length company-candidates)))
        (self-insert-command 1)
      (company-complete-number n))))

有什么改善吗? tooltip 出来, 选择数字, 肯定还是选择 candidate 吧. 要是想输入数字就不自然了, 其实我发现, 我 alt + number 都记不得用, 还是习惯用 TAB 一个一个选, 或者 C-n, C-p 上下选择.