经过一番折腾,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.
赞!可以取代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 上吧。
一般像这类似的包我都不敢直接用
,怕会有性能问题。顺便贴一下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 限高/限宽,带来不必要的字符串分配与渲染。
详细说明与优化建议
- 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)
- 频繁增删 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)
- 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))
- 直接使用 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))
- 避免频繁 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))
- 请求去重与结果缓存
- 问题:对同一符号反复请求 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 请求。
- 事件早期过滤,减少不必要计算
- 问题: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))))))
- 语法分析调用优化
- 问题:每次都会 (syntax-ppss) 判断注释;该函数有缓存,但仍应避免不必要调用。
- 优化思路:
- 先用轻量级条件过滤(同符号边界、不在 eolp 等),再调用 (syntax-ppss)。
- 如果已有缓存 key 未命中再调用语法分析。
- 避免重复向 eldoc-display-functions 追加
- 问题:多次 append 会形成重复项,导致 display 函数链重复调用。
- 优化思路:使用 (add-to-list 'eldoc-display-functions #'eldoc-mouse-display-in-posframe t) 并在 disable 时对称移除。
- 可调节节流策略
- 建议提供变量根据 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 请求次数,从而改善整体性能与流畅度。上述代码片段可按需择优合并进当前实现。
你说你不敢使用,你是说如果你使用中出现性能问题,就会对你造成巨大损失? 通常如果一个包用起来不好用,我们停用就是,不会说不敢用。
我是这样看待性能优化这个东西的,性能优化是无止境的,但是我们不可能做无止境的性能优化,需要找到一个平衡,这个平衡就是必要性,这个必要性怎么确定,根据使用体验来。所以性能优化需要问题驱动,我天天写代码,天天使用这个包,目前没有任何性能问题。 当然GPT的建议还是非常有启发意义,特别是当出现问题以后。
Should be fixed in my last two commits.
有很多地方确实需要改进,不过也不至于完全不敢用吧?可以试试再提改进意见。GPT说的很多也是胡说八道,posframe不hide功能都不对了呀
其他思路可以借鉴下
可以嘗試些建置工具 (Cask, Eask, 或 Eldev, 等等) + Github Actions 看看, 這樣開發會順利也放心許多. ![]()
我很好奇它跟 eldoc-box 差別是什麼? 有哪些優勢或劣勢? ![]()
区别主要在,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.
很多建议都是过度优化。比如频繁创建 timer vs 复用 timer 这个就是完全过度优化了。emacs 的性能再差也不至于负担不起在鼠标事件中创建 timer 的开销。
个人观点, 不喜欢在论坛看见不经人工查阅删改原样复制粘贴 gpt 的内容。大家又不是没有 gpt,想看 gpt 的内容自己和 gpt 去聊就好了。就算你想用 gpt 生成的内容,也最好是你自己查阅删改过后的,能代表你的观点和输出保留下来的精华部分,而不是原样复制粘贴。
自己细看过吗🤣
我自己天天使用,确实没有发现什么性能问题,对比以前没有用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里面能用快捷键弹出文档
根据的你的建议,我把这几个配置项都加上了。另外如果能有个好默认值就好了,有没有什么建议呢?