伙计们,eldoc-mouse 可以从melpa 安装了

经过一番折腾,eldoc-mouse 各方面达到了我最初的预期。现在也收录到melpa 了,更加方便安装。下面是我写在github 主页上的安装和配置的代码。需要了解更多的话,请访问 GitHub - huangfeiyu/eldoc-mouse: Show document for mouse hover utilizing eldoc, eglot and posframe M-x package-install [RET] eldoc-mouse [RET] ;; The following two lines are both optional, but you would like to add at least one of them to your Emacs configuration. (use-package eldoc-mouse :hook (eglot-managed-mode)) ;; enable mouse hover for eglot managed buffers. (global-set-key (kbd “<f1> <f1>”) 'eldoc-mouse-pop-doc-at-cursor) ;; replace <f1> <f1> to a key you like. Displaying document on a popup when you press a key.

6 个赞

赞!可以取代eldoc-box了。 能否把border-width、border-color之类的都加个配置选项?最好再来个 OVERRIDE-PARAMETERS,这样更灵活(比如可以配置margin或者fringe看起来更美观)。具体可以参考posframe-show的文档。

还有个疑问,为什么只支持30+?有用到什么特别的特性吗?eldoc-box可以支持27+。

updated:disable eldoc-mouse-mode会有这个告警,直接设置track-mouse是不是不合适?

Disable mouse-tracking (may impact other modes)? (y or n)

谢谢你的建议,我会考虑的。

只支持30+是因为我没有测试低版本,所以不确定,也许低版本也能工作。

关于mouse tracking的启用和关闭, 我没有想到什么好办法,所以就留给用户决定了。

关于track-mouse,可以保存原有的状态,disable的时候恢复就好了。

原理上跟着posframe版本走应该都没问题,你可以测试下。

另外还遇到一个问题,在非eglot buffer中,会出现这个错误:

Debugger entered--Lisp error: (jsonrpc-error "No current JSON-RPC connection" (jsonrpc-error-code . -32603) (jsonrpc-error-message . "No current JSON-RPC connection"))
  jsonrpc-error("No current JSON-RPC connection")
  eglot--current-server-or-lose()
  eglot-server-capable(:hoverProvider)
  eglot-hover-eldoc-function(#f(compiled-function (string &rest plist) #<bytecode 0x168e42d713e37d8>))
  eldoc-mouse-hover-eldoc-function(#f(compiled-function (string &rest plist) #<bytecode 0x168e42d713e37d8>))
  #<subr F616e6f6e796d6f75732d6c616d626461_anonymous_lambda_17>(eldoc-mouse-hover-eldoc-function)
  eldoc-documentation-default()
  eldoc--invoke-strategy(nil)
  eldoc-print-current-symbol-info()
  #<subr F616e6f6e796d6f75732d6c616d626461_anonymous_lambda_13>()
  apply(#<subr F616e6f6e796d6f75732d6c616d626461_anonymous_lambda_13> nil)
  timer-event-handler([t 0 0 500000 nil #<subr F616e6f6e796d6f75732d6c616d626461_anonymous_lambda_13> nil idle 0 nil])

关于mouse tracking, 如果一个其它的需要mouse tracking的mode在eldoc-mouse-mode之后启用,你说的方法就不行。我认为mouse tracking的启用和关闭应该由emacs 本身来管理,其它的mode如果需要就告诉emacs, emacs 决定什么时候开启和关闭。由单独的mode自己管理的话,不太好管理。

关于那个jsonrpc-error, 我只在用dape做debug时,再repl窗口看到过, 可能还需要家一个什么判断, 其它情况我没有遇到过. 虽然mouse tracking是全局的,但是只是绑定mouse event是 buffer local, eldoc-mouse-mode也是buffer local. 所以一般不应该出现这个错误。

(local-set-key [mouse-movement] #'eldoc-mouse-doc-on-mouse) 非eglot buffer应该不会启用eldoc-mouse, 也没有mouse-movement的绑定,按理不应该出现这个问题 如果你的情况不是这样,请提交的一个bug report github 上吧。

一般像这类似的包我都不敢直接用 :joy:,怕会有性能问题。顺便贴一下gpt5的检查分析:

gptel> 代码性能方面有什么可以改进的地方,用中文输出

**Focusing on performance optimizations**

I need to review a code snippet with an emphasis on performance, as the user has specifically asked about performance aspects in Chinese. While developer guidelines suggest a structured format, including various topics, I’ll prioritize the user's request by focusing on performance improvements. I’ll provide a summary of key performance issues, detailed explanations, suggestions, and relevant code examples, all while maintaining a professional tone and avoiding heavy formatting. I'll analyze the Emacs Lisp code for specific performance concerns.**Analyzing debouncing and throttling**

It seems there’s an inconsistency with the default values for `eldoc-mouse-idle-time`, which could impact performance. I should discuss adaptive debouncing and the differences between trailing and leading edges. I think it’s better to use a single idle timer that resets instead of creating a new one for each movement. Also, maintaining the last hover position can prevent unnecessary queries and heavy function calls, like `eldoc-print-current-symbol-info`, which can cause multiple requests. Optimizing these aspects can significantly enhance performance.**Optimizing eldoc performance**

I’m thinking about how to avoid forcing updates unless the content has changed. One approach could be to maintain a hash of positions or use a combination of buffer-modified ticks and symbol names to decide when to force a refresh. It makes sense to cache the last documents in a local variable, too. 

There's also some overhead in adding and removing hooks frequently, which can slow things down. I could consider using a let-bound dynamic variable instead and explore directly calling Eglot methods for better performance.**Improving overlay and posframe efficiency**

I’m considering the current approach to overlay creation for each hover. It seems more efficient to reuse a single overlay per buffer instead of deleting and recreating it. Utilizing `move-overlay` could reduce unnecessary memory consumption.

For posframe operations, calling `posframe-show` each time could be heavy. I think using it with reuse or updating the string with `posframe-hide-show` could be better. Also, I should ensure to leverage the 'docs' parameter directly instead of repeatedly fetching the eldoc buffer, avoiding buffer switching. This would enhance performance considerably!**Streamlining eldoc performance**

I see significant potential for improvement by using the 'docs' parameter. It would be wise to unify this into a string for efficient usage. Also, avoiding frequent calls to `syntax-ppss` might help, and I can restrict usage by positioning it correctly.

I think reusing overlays instead of recreating them can reduce memory usage, and I should optimize `posframe` operations by only refreshing when necessary. Finally, I should cache previous documents to streamline performance and avoid unnecessary calculations. By implementing these changes, the overall efficiency of the code can greatly improve!**Enhancing performance with eldoc**

I can pass parameters like `:accept-focus nil` to optimize auto-hide behavior. It may be worth considering increasing idle time for heavy language servers, which can help performance.

I should also avoid using `message ""` in lambdas and opt for `ignore` instead. Filtering mouse movement events to consider only the active window can reduce unnecessary triggers. 

Plus, I could store a buffer-local flag for `eglot-managed-p` calls. By focusing solely on performance and minimizing other aspects, I aim to craft a clear and structured response in Chinese with helpful examples.

以下仅围绕“性能优化”给出改进建议。

概要(关键性能问题)

  • 频繁创建/取消 idle timer 与传参闭包,造成额外分配与 GC 压力。
  • 每次 hover 都增删 eldoc/eglot 相关 hooks 与 cl-letf 动态替换,产生不必要的 cons 与函数查找成本。
  • Overlay 每次删除再新建,未复用,造成频繁 cons。
  • 忽略 eldoc 显示回调的 docs 入参,转而扫描 buffer-list 找 eldoc 缓冲并 buffer-string 拷贝,O(N) + 大字符串复制。
  • 每次都 hide/show posframe,引起 child-frame 重建/重布局;未做增量更新。
  • 对同一符号/未修改缓冲重复请求 LSP,无缓存策略;某些路径还强制 eldoc 重新请求。
  • track-mouse 全局开启后会产生大量 mouse-movement 事件,未做窗口/位置层面的早期过滤。
  • 文本截断在 posframe 层处理,先构造大字符串再让 posframe 限高/限宽,带来不必要的字符串分配与渲染。

详细说明与优化建议

  1. Idle timer 反复取消+新建
  • 问题:每次 mouse-movement 都 cancel-timer 并 run-with-idle-timer 新建一个 timer,会有较多 timer 对象分配和 GC。
  • 优化思路:
    • 复用单个 idle timer:保存“待处理的最新位置”,重置 timer 的 idle 时间,不重复创建。
    • 将 event 位置保存到 buffer-local 变量,timer 到点只读取最新值执行一次。
  • 参考实现:
    (defvar-local eldoc-mouse--idle-timer nil)
    (defvar-local eldoc-mouse--pending-pos nil)
    
    (defun eldoc-mouse--schedule (pos)
      (setq eldoc-mouse--pending-pos pos)
      (if (timerp eldoc-mouse--idle-timer)
          (timer-set-idle-time eldoc-mouse--idle-timer eldoc-mouse-idle-time t)
        (setq eldoc-mouse--idle-timer
              (run-with-idle-timer eldoc-mouse-idle-time nil
                                   #'eldoc-mouse--run-pending))))
    
    (defun eldoc-mouse--run-pending ()
      (when (and (eglot-managed-p) eldoc-mouse--pending-pos)
        (eldoc-mouse-show-doc-at eldoc-mouse--pending-pos)
        (setq eldoc-mouse--pending-pos nil)))
    ;; 在 eldoc-mouse-doc-on-mouse 中调用 (eldoc-mouse--schedule pos)
    
  1. 频繁增删 hooks/cl-letf
  • 问题:在 hover 路径里 add-hook/remove-hook、cl-letf 重定义函数,会在高频路径里带来额外开销。
  • 优化思路:
    • 把与 eglot/eldoc 的集成(去掉签名、保留高亮、注入 display-function、抑制 highlight 的绕过逻辑)挪到 mode enable/disable 的生命周期里一次性设置。
    • 对需要“临时抑制某行为”的场景,使用一个 buffer-local 开关,在 advice 中读取,而不是 hover 时 add/remove/advice。
  • 参考实现(思路):
    (defvar-local eldoc-mouse--suppress-highlight nil)
    
    (defun eldoc-mouse--around-eglot-hover (orig cb)
      (if eldoc-mouse--suppress-highlight
          (cl-letf (((symbol-function 'eglot--highlight-piggyback) #'ignore))
            (funcall orig cb))
        (funcall orig cb)))
    
    ;; mode 启用时一次性:
    (advice-add 'eglot-hover-eldoc-function :around #'eldoc-mouse--around-eglot-hover)
    
    ;; hover 时仅设置开关而非增删 hook:
    (setq eldoc-mouse--suppress-highlight t)
    (eldoc-print-current-symbol-info)
    (setq eldoc-mouse--suppress-highlight nil)
    
  1. Overlay 复用
  • 问题:每次 hover 都 delete-overlay 再 make-overlay。
  • 优化思路:创建一次后持续用 move-overlay 移动,减少 cons 与 GC。
  • 参考实现:
    (if eldoc-mouse-mouse-overlay
        (move-overlay eldoc-mouse-mouse-overlay (car bounds) (cdr bounds))
      (setq eldoc-mouse-mouse-overlay
            (make-overlay (car bounds) (cdr bounds)))
      (overlay-put eldoc-mouse-mouse-overlay 'face 'highlight)
      (overlay-put eldoc-mouse-mouse-overlay 'evaporate t))
    
  1. 直接使用 eldoc display 回调的 docs 参数
  • 问题:eldoc-mouse-display-in-posframe 忽略 docs,反而 seq-filter + 正则从 buffer-list 找 eldoc buffer,再 buffer-string,O(N) 扫描 + 大字符串复制。
  • 优化思路:
    • 直接消费 docs(string/buffer/列表)并拼成字符串,避免扫描 buffer-list。
    • 提前截断(宽/高)后再传 posframe,减少字符串与渲染开销。
  • 参考实现:
    (defun eldoc-mouse--normalize-docs (docs)
      (cond
       ((stringp docs) docs)
       ((bufferp docs)
        (with-current-buffer docs (buffer-string)))
       ((listp docs) (mapconcat #'identity docs "\n"))
       (t (format "%s" docs))))
    
    (defun eldoc-mouse--truncate (text)
      (let* ((w eldoc-mouse-posframe-max-width)
             (h (or eldoc-mouse-posframe-max-height most-positive-fixnum))
             (lines (split-string text "\n"))
             (lines (cl-subseq lines 0 (min h (length lines)))))
        (mapconcat (lambda (s)
                     (if (and w (> (length s) w))
                         (concat (substring s 0 w) "…")
                       s))
                   lines "\n")))
    
    (defun eldoc-mouse-display-in-posframe (docs _interactive)
      (when (and docs eldoc-mouse-unsupress-posframe)
        (setq eldoc-mouse-unsupress-posframe nil)
        (let* ((raw (eldoc-mouse--normalize-docs docs))
               (text (eldoc-mouse--truncate raw))
               (border-color (face-foreground 'default)))
          (eldoc-mouse--pop-doc text border-color))
        t))
    
  1. 避免频繁 hide/show posframe,改为增量更新
  • 问题:每次新内容都先 hide 再 show,child-frame 频繁创建/布局。
  • 优化思路:
    • 保持同一个 posframe 存活,重复调用 posframe-show 同名 buffer 会更新内容与位置;仅在真正需要时 hide。
    • 对“同一符号”只更新内容,不改变几何形态时避免 hide。
  • 参考实现(在 --pop-doc 中不主动 hide;仅在符号变更/移动远离时 hide):
    (defun eldoc-mouse--pop-doc (doc border-color)
      (posframe-show
       eldoc-mouse-posframe-buffer-name
       :position (car eldoc-mouse-last-symbol-bounds)
       :poshandler #'posframe-poshandler-point-bottom-left-corner-upward
       :max-width eldoc-mouse-posframe-max-width
       :min-height eldoc-mouse-posframe-min-height
       :max-height eldoc-mouse-posframe-max-height
       :border-width 1
       :border-color border-color
       :string doc
       :accept-focus nil))
    
  1. 请求去重与结果缓存
  • 问题:对同一符号反复请求 LSP;某些路径还设置 eldoc–last-request-state=nil 强制重新请求。
  • 优化思路:
    • 使用 (thing-at-point 'symbol) 与 buffer-chars-modified-tick 组成 key,缓存 hover 结果(带 TTL,例如几秒)。同符号且 tick 未变化时直接显示缓存,避免 LSP 请求。
    • 仅在用户显式调用 eldoc-mouse-pop-doc-at-cursor 或 buffer 内容变化时才强制刷新。
  • 参考实现(简化示例):
    (defvar-local eldoc-mouse--cache (make-hash-table :test 'equal))
    (defvar-local eldoc-mouse--cache-ttl 3.0) ; 秒
    (defun eldoc-mouse--cache-key ()
      (list (or (thing-at-point 'symbol t) "")
            (buffer-chars-modified-tick)))
    
    (defun eldoc-mouse--cache-put (key value)
      (puthash key (list (float-time) value) eldoc-mouse--cache))
    
    (defun eldoc-mouse--cache-get (key)
      (pcase (gethash key eldoc-mouse--cache)
        (`(,ts ,val) (when (< (- (float-time) ts) eldoc-mouse--cache-ttl) val))
        (_ nil)))
    
    ;; show-doc-at 中先查缓存,命中则直接弹,不命中再触发 eldoc 请求。
    
  1. 事件早期过滤,减少不必要计算
  • 问题:track-mouse 全局开启后,所有缓冲都会收到大量 mouse-movement 事件;doc-on-mouse 中只简单检查 eglot-managed-p。
  • 优化思路:
    • 在 doc-on-mouse 中尽早过滤:仅处理当前 window、仅处理与上次符号边界不同的位置,且尽量避免 goto-char/ppss 等昂贵操作。
    • 示例过滤:
      (defun eldoc-mouse-doc-on-mouse (event)
        (interactive "e")
        (let* ((start (event-start event))
               (win (posn-window start)))
          (when (and (eq win (selected-window)) (eglot-managed-p))
            (let ((pos (posn-point start)))
              (when (and (integer-or-marker-p pos)
                         (or (null eldoc-mouse-last-symbol-bounds)
                             (< pos (car eldoc-mouse-last-symbol-bounds))
                             (> pos (cdr eldoc-mouse-last-symbol-bounds))))
                (eldoc-mouse--schedule pos))))))
      
  1. 语法分析调用优化
  • 问题:每次都会 (syntax-ppss) 判断注释;该函数有缓存,但仍应避免不必要调用。
  • 优化思路:
    • 先用轻量级条件过滤(同符号边界、不在 eolp 等),再调用 (syntax-ppss)。
    • 如果已有缓存 key 未命中再调用语法分析。
  1. 避免重复向 eldoc-display-functions 追加
  • 问题:多次 append 会形成重复项,导致 display 函数链重复调用。
  • 优化思路:使用 (add-to-list 'eldoc-display-functions #'eldoc-mouse-display-in-posframe t) 并在 disable 时对称移除。
  1. 可调节节流策略
  • 建议提供变量根据 LSP 负载自适应/可配置:当 hover 返回过慢时自动提高 idle 时间或限制请求频率(例如设置最短两次请求间隔)。
  • 示例:
    (defvar-local eldoc-mouse--last-req-time 0.0)
    (defcustom eldoc-mouse-min-request-interval 0.2 ...)
    (when (< (- (float-time) eldoc-mouse--last-req-time) eldoc-mouse-min-request-interval)
      (cl-return-from eldoc-mouse-show-doc-at))
    (setq eldoc-mouse--last-req-time (float-time))
    

总结

  • 通过“单例 idle timer + 待处理位置缓存”“复用 overlay”“直接消费 docs 入参 + 提前截断”“持久化 posframe、避免反复 hide/show”“请求缓存+去重”“一次性安装 hooks/advice + 轻量开关控制”“事件早期过滤”,可以显著减少 cons、GC、子框架重绘与 LSP 请求次数,从而改善整体性能与流畅度。上述代码片段可按需择优合并进当前实现。
1 个赞

你说你不敢使用,你是说如果你使用中出现性能问题,就会对你造成巨大损失? 通常如果一个包用起来不好用,我们停用就是,不会说不敢用。

我是这样看待性能优化这个东西的,性能优化是无止境的,但是我们不可能做无止境的性能优化,需要找到一个平衡,这个平衡就是必要性,这个必要性怎么确定,根据使用体验来。所以性能优化需要问题驱动,我天天写代码,天天使用这个包,目前没有任何性能问题。 当然GPT的建议还是非常有启发意义,特别是当出现问题以后。

2 个赞

Should be fixed in my last two commits.

有很多地方确实需要改进,不过也不至于完全不敢用吧?可以试试再提改进意见。GPT说的很多也是胡说八道,posframe不hide功能都不对了呀 :joy: 其他思路可以借鉴下

1 个赞

可以嘗試些建置工具 (Cask, Eask, 或 Eldev, 等等) + Github Actions 看看, 這樣開發會順利也放心許多. :smiley:

我很好奇它跟 eldoc-box 差別是什麼? 有哪些優勢或劣勢? :thinking:

区别主要在,eldoc-mouse 专注于鼠标hover, 而eldoc-box 最近虽然也支持了(是我在eldoc-box 开始鼠标hover 支持的讨论的),但是支持的不好。eldoc-mouse 利用posframe package, 并且尽可能的利用eldoc 已有功能,所有代码量很小。eldoc-mouse 提供可扩展,可支持不限于eglot, 这当然需要添加扩展,但是扩展很简单,可以看eldoc-mouse github 页面,我只做了eglot 和emacs-lisp-mode的扩展。 如果你感兴趣,可以看看eldoc-box 最近close 的issue 和pull requests.

1 个赞

很多建议都是过度优化。比如频繁创建 timer vs 复用 timer 这个就是完全过度优化了。emacs 的性能再差也不至于负担不起在鼠标事件中创建 timer 的开销。

1 个赞

个人观点, 不喜欢在论坛看见不经人工查阅删改原样复制粘贴 gpt 的内容。大家又不是没有 gpt,想看 gpt 的内容自己和 gpt 去聊就好了。就算你想用 gpt 生成的内容,也最好是你自己查阅删改过后的,能代表你的观点和输出保留下来的精华部分,而不是原样复制粘贴。

5 个赞

自己细看过吗🤣

我自己天天使用,确实没有发现什么性能问题,对比以前没有用eldoc-mouse 时,没发现什么不同。

想请问下和eldoc-box的eldoc-box-hover-at-point-mode有什么区别吗?这个功能在eldoc-box早就有了

eldoc-box 的hover at point 是针对光标的,在光标处自动弹出的问题是遮挡输入,光标位置往往是要输入的位置,而鼠标hover 时不需要光标,所以不存在遮挡输入的问题。

我记得使用eldoc-box 是,光标在函数调用处的括号内时,文档也会弹出来,体验非常不好。非常怀念lsp-mode 的lsp-ui-doc. 这是我弄eldoc-mouse 的原因。

另外,eldoc-mouse 也给喜欢鼠标键盘混用的伙伴们,包括我自己提供一个选择。个人认为鼠标键盘各有其优势。

Hi, 看上去很棒! 这个 eldoc-mouse 只能能在 命令行模式下用吗,还是只能在windows模式下用?

谢谢!

只能在gui的emacs 下用,听说terminal 下面emacs 是不支持鼠标的。

不过据说emacs 31在terminal 下面会支持child frame, 所有预计emacs 31里面能用快捷键弹出文档

1 个赞

根据的你的建议,我把这几个配置项都加上了。另外如果能有个好默认值就好了,有没有什么建议呢?