[讨论] lsp版的company-capf截获了本该属于company-dabbrev的补全任务

背景:

主业C++开发。elisp初学。spacemacs develop用户。lsp-mode + ccls

问题: 比较新的spacemacs下,C++中lsp比较完美,但敲字符串后无法自动用 company-dabbrev 补全,影响体验。比较老的spacemacs develop的commit没有这个问题

排查:(已编辑)

根本原因不是OP中描述的那样,而是函数 company--capf-data 会在补全字符串时不为空。也就是说,ccls在这时候也会返回东西给到 lsp-mode

可能的魔改方案:

  • 魔改 ccls ,毕竟双引号确实应该是个 trigger-char,但仅限于补全头文件时,可以加个判断
  • 魔改某个elisp函数,加个advice之类的,也是加个判断筛选

采用的魔改方案:

  • company-capfprefix 阶段加个flag,捕获 company--capf-data 的返回值
  • 如果满足字符串的判定,将上述返回值改成 nil ,否则原样返回
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Fix `company-capf' catching string unwillingly
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(with-eval-after-load 'lsp-completion
  (defun dc4ever/filter-capf-lsp-retval (res)
    "Filter return value to `nil' in the following conditions:
- We are dealing with `company-capf' `pcase' `prefix'
- We are using `lsp-completion-at-point' as capf
- We are using `ccls' as lsp server
- We are trying to complete a string starting with \"
- We are not trying to complete header files"
    (unless (and (and (boundp 'dc4ever/capf-prefix?) dc4ever/capf-prefix?)
                 (eq 'lsp-completion-at-point (car res))
                 (eq 'ccls (lsp--workspace-server-id (car (lsp-workspaces))))
                 (let ((bounds-start (nth 1 res)))
                   (save-excursion
                     (goto-char bounds-start)
                     (unless (= (point) (point-at-bol))
                       (and
                        (not (equal "#include "
                                    (buffer-substring-no-properties
                                     (point-at-bol)
                                     (+ (point-at-bol) (length "#include ")))))
                        (equal "\"" (buffer-substring-no-properties
                                     (- (point) 1) (point))))))))
      res))
  (defun dc4ever/capf-wrapper (func &rest r)
    "Wrap `company-capf' with a special tmp variable `dc4ever/capf-prefix?'
when we are calling with 'prefix command"
    (if (eq 'prefix (car r))
        (let ((dc4ever/capf-prefix? t))
          (apply func r))
      (apply func r)))

  (advice-add 'company-capf
              :around 'dc4ever/capf-wrapper)
  (advice-add 'company--capf-data
              :filter-return 'dc4ever/filter-capf-lsp-retval))

请各位大佬指点,谢谢。



PS: 以下为OP

edebug 跟踪代码发现,在 lsp-completion-at-point 中,双引号 \" 是一个 trigger-char。这导致如下代码在尝试补全字符串时 (例如, "str|" 时)

       :company-prefix-length
       (save-excursion
         (goto-char bounds-start)
         (and (lsp-completion--looking-back-trigger-characterp trigger-chars) t))

evaluate成 t

导致无法交给优先级更低的 company-dabbrev 接管。

ccls作者在坛子里,可以问问看

Advice:

(defun lsp:completion-options-trigger-characters?@remove-quotes (orig-fn obj)
  (let ((chars (funcall orig-fn obj)))
    (if (nth 3 (syntax-ppss)) ;; point at string
        (remove "\"" chars)
      chars)))

(advice-add 'lsp:completion-options-trigger-characters?
            :around #'lsp:completion-options-trigger-characters?@remove-quotes)

company 貌似就是有这个问题,前面的 backends 会把后面的屏蔽,可以像默认 company-backends 一样把几个放到一组 (company-dabbrev-code company-gtags company-etags company-keywords),或者试试 https://github.com/Wilfred/company-try-hard

我试了一下,即使 trigger-char 里面没有双引号,company-capf 也不会把控制权交给 company-dabbrev

:with 把两者组合起来 (company-capf :with company-dabbrev) 倒是可以,但是这样也就污染了 company-capf

相关讨论:

没去认真研究,不知是 lsp 的问题还是 company 的问题。看了一下第 2 个帖子,发现 company 作者至今还没试过 lsp,这俩扩展之间的问题恐怕还得继续存在一段时间:

dgutov commented on Feb 3, 2019

:man_shrugging:

I haven’t tried LSP yet (I hear the Ruby impls are still very alpha), and I don’t write Python. However, LSP completion should include local variables etc. Maybe with some later release of whatever server you’re using.

如果采用 :with 组合两个后端,又不想太多干扰,或许可以自己实现一个过滤器,参考这个帖子《推荐入坑TabNine#40》的回复。

1赞

我之前问过,大概是这样:lsp 会立马(收到 server 响应之前)告诉 company 有结果,然后 company 就不会问后面的 backends 了(原因不记得了,也许是等到真的响应可能太久?)

我一晚上也要尝试写这个advice,感觉我OP里面的问题还是没有定位完整

一个思路是对于字符串的补全手工调用 hippie-expand

写了一个 capf/dabbrev 的过滤器,实现了以下效果:

point at company-capf company-dabbrev
trigger-chars :white_check_mark: :x:
string :x: :white_check_mark:
other :white_check_mark: :white_check_mark:

Screenshot_2020-08-26_at_1.23.48_AM

Screenshot_2020-08-26_at_1.50.01_AM

Screenshot_2020-08-26_at_1.29.16_AM

感谢回复。我再次定位了一下根本问题,我OP中描述不准确。

OP已经更新,烦请几位大佬指点一下有没有问题以及更简洁的方案,谢谢 @twlz0ne @seagle0128

系统太旧编译不了 ccls,我用的是 clangd.

我这边没发现双引号作为 trigger-chars 的现象,难道是 ccls 特有?如果是,可采用我在 #3楼 的方案,实现有条件剔除。

我观察到的现象是 company-capf 会阻断后续 backends 起作用。无论从 lsp 端还是 company 端应该都有解决方案 (原因无非就是中间某个地方断链了),不过最简单的应该是:

(defun lsp-completion-mode-setup ()
  (setq-local company-backends '((company-capf :with company-dabbrev :separate))))

(add-hook 'lsp-completion-mode-hook #'lsp-completion-mode-setup)

但这样会带来一个新的问题:两个后端的补全内容混在一起。

:with 的作用立竿见影,如果你试了没效果,应该是还有其他问题未排除。如果 :with 起作用了,再考虑如何解决它带来的副作用。

另,前面回复提到的一些链接可以点开看看。

感谢。我魔改的方案是不希望将两个后端混在一起,现在暂时解决了。不过有感觉慢了一“帧”(心理作用应该是)

:with 没了解过之前,我再研究研究,谢谢!

「混在一起」并非 bug,而是 feature。

:with 有其存在的必要,它相当于把“串联”改成“并联”。当你另起一行,此时即不处于字符中,也不处于成员变量/函数的补全中,这时就可能需要同时显示多个后端提供的 candidate,例如 capf + dabbrev + snippet。

如果不用 :with,只能看到排在最前的后端起作用,只有当他没有内容时,才会继续后面的 backends。

1赞