lsp-bridge -- 速度最快的语法补全插件

目前每头绪, 因为我不用 hydra , 欢迎PR

回车的时候报下面的问题

Debugger entered--Lisp error: (wrong-type-argument integer-or-marker-p nil)
  delete-region(nil 944)
  (if (fboundp candidate-expand) (funcall candidate-expand candidate-info bound-start) (delete-region bound-start (point)) (insert (plist-get candidate-info :label)))
  (let* ((candidate-info (acm-menu-current-candidate)) (bound-start acm-menu-frame-popup-point) (backend (plist-get candidate-info :backend)) (candidate-expand (intern-soft (format "acm-backend-%s-candidate-expand" backend)))) (if (fboundp candidate-expand) (funcall candidate-expand candidate-info bound-start) (delete-region bound-start (point)) (insert (plist-get candidate-info :label))))
  acm-complete()
  funcall-interactively(acm-complete)
  call-interactively(acm-complete nil nil)
  command-execute(acm-complete)

什么文件? 什么模式? 什么情况下复现的?

有没有和 emacs -Q 对比测试过?

请反馈问题详细点, 没有任何环境说明, 谁也不知道啥原因。

因为 lsp-bridge 的异步设计, 每敲一个字符, 都会让LSP Server重新计算补全候选词, 所以我们没有办法在输入字符的时候对LSP候选词进行正则过滤, 因为你新敲的字符就会触发LSP Server用新的候选词覆盖补全菜单的内容。

但是很多 Corfu 的用户经常反馈需要针对LSP候选词进行快速过滤以少敲一点字符提高补全的效率。

今天想了想, 其实有一个妥协的办法, 按一个按键进入过滤模式, 过滤模式开启时, 用户输入的字符不修改 buffer 的内容(这样也就不会触发LSP Server计算新的候选词), 而是通过 after-string overlay 显示到 buffer 上, 这样 acm 再根据 overlay 的内容二次过滤候选词即可达到目标。

使用方法:

  1. 正常输入, 触发acm菜单弹出
  2. acm菜单显示后, 按 Alt + u 进入过滤模式
  3. 接着输入过滤字符串, acm菜单消失时自动退出过滤模式

效果展示: Peek 2022-12-17 17-52

7 个赞

配置文件内容

(add-to-list 'load-path "~/.emacs.d/site-lisp/lsp-bridge")
(add-to-list 'load-path "~/.emacs.d/elpa/posframe-20221118.614")
(add-to-list 'load-path "~/.emacs.d/elpa/markdown-mode-20221210.348")
(add-to-list 'load-path "~/.emacs.d/elpa/yasnippet-20200604.246")

(require 'lsp-bridge)

(require 'acm)

(add-hook 'text-mode-hook 'lsp-bridge-mode)

(add-hook 'text-mode-hook 'acm-mode)

org 文件内容

* TODO Test
SCHEDULED: <2022-12-18 Sun +1d>
:PROPERTIES:
:LAST_REPEAT: [2022-12-17 Sat 19:01]
:END:
- State "DONE"       from "TODO"       [2022-12-17 Sat 19:01]

在 heading 上按 C-c C-t,会搞乱 properties的内容。显示如下:

你 C-c C-t 弄乱 org-mode 和 lsp-bridge 有啥关系呢?

C-c C-t是完成一次任务。

emacs -q 使用 C-c C-t 是正常的。

(add-to-list 'load-path "~/.emacs.d/site-lisp/lsp-bridge")
(add-to-list 'load-path "~/.emacs.d/elpa/posframe-20221118.614")
(add-to-list 'load-path "~/.emacs.d/elpa/markdown-mode-20221210.348")
(add-to-list 'load-path "~/.emacs.d/elpa/yasnippet-20200604.246")

(require 'lsp-bridge)

(require 'acm)

(add-hook 'text-mode-hook 'lsp-bridge-mode)

(add-hook 'text-mode-hook 'acm-mode)

这个最小配置是仅启用了 lsp-bridge,与lsp-bridge无关的话,是和lsp-bridge的依赖有关?

你先自己调试吧, 我不知道。

不知道lsp-bridge为什么会搞乱这个,暂时没有头绪。

你先自己调试吧, 我很少用 org-mode , 没有时间折腾 org-mode

(setq lsp-bridge--internal-hooks
      '(
        (before-change-functions lsp-bridge-monitor-before-change nil t)
        ;;(after-change-functions lsp-bridge-monitor-after-change nil t)
        (pre-command-hook lsp-bridge-monitor-pre-command nil t)
        (post-command-hook lsp-bridge-monitor-post-command nil t)
        (after-save-hook lsp-bridge-monitor-after-save nil t)
        (kill-buffer-hook lsp-bridge-close-buffer-file nil t)
        (find-file-hook lsp-bridge-search-words-update nil t)
        (before-revert-hook lsp-bridge-close-buffer-file nil t)
        (post-self-insert-hook lsp-bridge-monitor-post-self-insert 90 t)))

定位到了这里,是lsp-bridge-monitor-after-change这个导致了问题。

你继续注释 lsp-bridge-monitor-after-change 的代码排查吧, 看看具体是哪一行导致的?

emacs -q 在 scratch 中加载:

(add-to-list 'load-path "~/.emacs.d/site-lisp/lsp-bridge")
(add-to-list 'load-path "~/.emacs.d/elpa/posframe-20221118.614")
(add-to-list 'load-path "~/.emacs.d/elpa/markdown-mode-20221210.348")
(add-to-list 'load-path "~/.emacs.d/elpa/yasnippet-20200604.246")

(require 'lsp-bridge)

(require 'acm)

M-x eval buffer然后启用M-x lsp-bridge-modeM-x acm-mode

在 scratch中回车报下面的错误:

Debugger entered--Lisp error: (wrong-type-argument integer-or-marker-p nil)
  delete-region(nil 445)
  (if (fboundp candidate-expand) (funcall candidate-expand candidate-info bound-start) (delete-region bound-start (point)) (insert (plist-get candidate-info :label)))
  (let* ((candidate-info (acm-menu-current-candidate)) (bound-start acm-menu-frame-popup-point) (backend (plist-get candidate-info :backend)) (candidate-expand (intern-soft (format "acm-backend-%s-candidate-expand" backend)))) (if (fboundp candidate-expand) (funcall candidate-expand candidate-info bound-start) (delete-region bound-start (point)) (insert (plist-get candidate-info :label))))
  acm-complete()
  funcall-interactively(acm-complete)
  call-interactively(acm-complete nil nil)
  command-execute(acm-complete)

不启用acm-mode 没有上面的问题。

系统 MacOS 13, emacs-version 28.2.

你要看看 acm-get-input-prefix-bound 在 scratch 输出啥?

我已经不用 scratch 很多年了。

而且为啥要在 scratch buffer 用 lsp-bridge?

跟踪到了一个 (acm-get-input-prefix) 会对这个有影响。把这个去掉,下面的代码还有个函数会有影响,但是没找到原始操作。

没想明白原因,初步推测是 org-mode 在标记任务 DONE 的时候,会自己计算定位一个位置,插入字串,并更新一些元数据,是不是 lsp-bridge 此时有对光标的操作,把当前光标位置搞乱了。所以 org 这里把对应信息插入到了不正确的地方。

只是在 Scratch中测试,在使用 lsp-bridge 的时候,其他 buffer中也会出现这个问题,不是说只是在 scratch中会出现这种问题。

其他buffer中,不知道什么时候会触发,但是在scratch中是百分百触发这个问题。

原先使用的时候没想到是lsp-bridge的问题,org 记录的时候很少看原始的文件,也是昨天发现是lsp-bridge带来的问题,暂时在org中禁用了。

我吃完饭看看吧

我用 edebug 调试(先edebug-defun 标记函数 acm-get-input-prefix-bound, 然后等 acm-get-input-prefix-bound 执行停住的时候按 d 单步执行, 进入函数内部), 得到了调用链

  acm-get-input-prefix-bound()
  (let ((bound (acm-get-input-prefix-bound))) (if bound (buffer-substring-no-properties (car bound) (cdr bound)) ""))
  acm-get-input-prefix()
  (lsp-bridge-call-file-api "change_file" lsp-bridge--before-change-begin-pos lsp-bridge--before-change-end-pos length (buffer-substring-no-properties begin end) (lsp-bridge--position) (acm-char-before) (buffer-name) (acm-get-input-prefix))
  (if lsp-bridge-revert-buffer-flag nil (setq lsp-bridge-last-change-command (format "%s" this-command)) (setq lsp-bridge-last-change-position (list (current-buffer) (buffer-chars-modified-tick) (point))) (lsp-bridge-call-file-api "change_file" lsp-bridge--before-change-begin-pos lsp-bridge--before-change-end-pos length (buffer-substring-no-properties begin end) (lsp-bridge--position) (acm-char-before) (buffer-name) (acm-get-input-prefix)) (if (lsp-bridge-epc-live-p lsp-bridge-epc-process) (progn (let* ((current-word (thing-at-point 'word t)) (current-symbol (thing-at-point 'symbol t))) (if acm-enable-tabnine (progn (lsp-bridge-tabnine-complete))) (if acm-enable-search-sdcv-words (progn (if (or ... ...) nil (lsp-bridge-call-async "search_sdcv_words_search" current-word)))) (lsp-bridge-elisp-symbols-search current-symbol) (if lsp-bridge-prohibit-completion nil (if buffer-file-name (progn (let ... ...)))) (if (and (derived-mode-p 'web-mode) (acm-in-string-p) (save-excursion (search-backward-regexp "class=" ... t))) (progn (if (or ... ...) nil (lsp-bridge-call-async "search_tailwind_keywords_search" buffer-file-name current-symbol))))))))
  lsp-bridge-monitor-after-change(2 8 6)
  replace-match(" DONE " t t)
  org-todo(nil)
  funcall-interactively(org-todo nil)
  call-interactively(org-todo nil nil)
  command-execute(org-todo)

看样子是 acm-get-input-prefix-bound 干扰了 org-todo 内部的 replace-match 后面的代码。

修复很简单, 但是等我思考一下, 为啥这两个不相关的函数会相互干扰。

目前调试的结果是,

在 after-change-functions 函数中执行 (thing-at-point 'symbol t) 就会导致 org-todo 挂掉, 猜测应该是 org-todo 的实现不够健壮, 导致执行 (thing-at-point 'symbol t) 就会出错。