8 | ;; URL: https://github.com/casouri/eldoc-box
9 | ;; Package-Requires: ((emacs "27.1"))
10 |
11 | ;;; License
12 | ;;
13 | ;; This program is free software; you can redistribute it and/or modify
14 | ;; it under the terms of the GNU General Public License as published by
15 | ;; the Free Software Foundation; either version 3, or (at your option)
16 | ;; any later version.
17 |
18 | ;; This program is distributed in the hope that it will be useful,
19 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
20 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 | ;; GNU General Public License for more details.
22 |
23 | ;; You should have received a copy of the GNU General Public License
24 | ;; along with this program; see the file COPYING. If not, write to
25 | ;; the Free Software Foundation, Inc., 51 Franklin Street, Fifth
26 | ;; Floor, Boston, MA 02110-1301, USA.
27 |
28 | ;;; This file is NOT part of GNU Emacs
29 |
30 | ;;; Commentary:
31 | ;;
32 | ;; Usage:
33 | ;;
34 | ;; There are three ways to use this package:
35 | ;;
36 | ;; 1. Enable ‘eldoc-box-hover-mode’. Emacs will show the documentation
37 | ;; of symbol at point in a children on the upper left or right corner.
38 | ;;
39 | ;; 2. Enable ‘eldoc-box-hover-at-point-mode’. Similar to
40 | ;; ‘eldoc-box-hover-mode’, but displays the childframe at point. (This
41 | ;; mode feels slower comparing to ‘eldoc-box-hover-mode’.)
42 | ;;
43 | ;; 3. Bind ‘eldoc-box-help-at-point’ to a key and bring up the
44 | ;; documentation childframe on-demand. This command requires Emacs 28
45 | ;; to work.
46 | ;;
47 | ;; Customization faces:
48 | ;;
49 | ;; - ‘eldoc-box-border’
50 | ;; - ‘eldoc-box-body’
51 | ;;
52 | ;; Hooks:
53 | ;;
54 | ;; - ‘eldoc-box-buffer-hook’
55 | ;; - ‘eldoc-box-frame-hook’
56 | ;;
57 | ;; Customize options:
58 | ;;
59 | ;; - ‘eldoc-box-max-pixel-width’
60 | ;; - ‘eldoc-box-max-pixel-height’
61 | ;; - ‘eldoc-box-only-multi-line’
62 | ;; - ‘eldoc-box-cleanup-interval’
63 | ;; - ‘eldoc-box-fringe-use-same-bg’
64 | ;; - ‘eldoc-box-self-insert-command-list’
65 |
66 | ;;; Code:
67 |
68 | (eval-when-compile
69 | (require 'pcase))
70 |
71 | (require 'cl-lib)
72 | (require 'seq)
73 | ;; For ‘eldoc-doc-buffer-separator’.
74 | (require 'eldoc)
75 |
76 | ;;;; Userland
77 | ;;;;; Variable
78 | (defgroup eldoc-box nil
79 | "Display Eldoc docs in a pretty child frame."
80 | :prefix "eldoc-box-"
81 | :group 'eldoc)
82 |
83 | (defface eldoc-box-border '((((background dark)) . (:background "white"))
84 | (((background light)) . (:background "black")))
85 | "The border color used in childframe.")
86 |
87 | (defface eldoc-box-body '((t . nil))
88 | "Body face used in documentation childframe.")
89 |
90 | (defface eldoc-box-markdown-separator '((t . ( :strike-through t
91 | :height 0.4)))
92 | "Face for the separator line in Markdown.")
93 |
94 | (defcustom eldoc-box-lighter " ELDOC-BOX"
95 | "Mode line lighter for all eldoc-box modes.
96 | If the value is nil, no lighter is displayed."
97 | :type '(choice string
98 | (const :tag "None" nil)))
99 |
100 | (defcustom eldoc-box-only-multi-line nil
101 | "If non-nil, only use childframe when there are more than one line."
102 | :type 'boolean)
103 |
104 | (defcustom eldoc-box-cleanup-interval 1
105 | "After this amount of seconds will eldoc-box attempt to cleanup the childframe.
106 | E.g. if it is set to 1, the childframe is cleared 1 second after
107 | you moved the point to somewhere else (that doesn't have a doc to show).
108 | This doesn't apply to `eldoc-box-hover-at-point-mode',
109 | in that mode the childframe is cleared as soon as point moves."
110 | :type 'number)
111 |
112 | (defcustom eldoc-box-clear-with-C-g nil
113 | "If set to non-nil, eldoc-box clears childframe on \\[keyboard-quit]."
114 | :type 'boolean)
115 |
116 | (defcustom eldoc-box-doc-separator "\n\n"
117 | "The separator between documentation from different sources.
118 |
119 | Since Emacs 28, Eldoc can combine documentation from different
120 | sources, this separator is used to separate documentation from
121 | different sources.
122 |
123 | This separator is used for the documentation shown in
124 | ‘eldoc-box-bover-mode’ but not ‘eldoc-box-help-at-point’.
125 | ‘eldoc-box-help-at-point’ just shows Eldoc doc buffer, which uses
126 | ‘eldoc-doc-buffer-separator’."
127 | :type 'string)
128 |
129 | (defvar eldoc-box-frame-parameters
130 | '(;; make the childframe unseen when first created
131 | (left . -1)
132 | (top . -1)
133 | (width . 0)
134 | (height . 0)
135 |
136 | (no-accept-focus . t)
137 | (no-focus-on-map . t)
138 | (min-width . 0)
139 | (min-height . 0)
140 | (internal-border-width . 1)
141 | (vertical-scroll-bars . nil)
142 | (horizontal-scroll-bars . nil)
143 | (right-fringe . 3)
144 | (left-fringe . 3)
145 | (menu-bar-lines . 0)
146 | (tool-bar-lines . 0)
147 | (line-spacing . 0)
148 | (unsplittable . t)
149 | (undecorated . t)
150 | (visibility . nil)
151 | (mouse-wheel-frame . nil)
152 | (no-other-frame . t)
153 | (cursor-type . nil)
154 | (inhibit-double-buffering . t)
155 | (drag-internal-border . t)
156 | (no-special-glyphs . t)
157 | (desktop-dont-save . t)
158 | (tab-bar-lines . 0)
159 | (tab-bar-lines-keep-state . 1))
160 | "Frame parameters used to create the frame.")
161 |
162 | (defcustom eldoc-box-max-pixel-width 800
163 | "Maximum width of doc childframe in pixel.
164 | Consider your machine's screen's resolution when setting this variable.
165 | Set it to a function with no argument
166 | if you want to dynamically change the maximum width."
167 | :type 'number)
168 |
169 | (defcustom eldoc-box-max-pixel-height 700
170 | "Maximum height of doc childframe in pixel.
171 | Consider your machine's screen's resolution when setting this variable.
172 | Set it to a function with no argument
173 | if you want to dynamically change the maximum height."
174 | :type 'number)
175 |
176 | (defcustom eldoc-box-offset '(16 16 16)
177 | "Sets left, right & top offset of the doc childframe.
178 | Its value should be a list: (left right top)"
179 | :type '(list
180 | (integer :tag "Left")
181 | (integer :tag "Right")
182 | (integer :tag "Top")))
183 |
184 | (defvar eldoc-box-position-function #'eldoc-box--default-upper-corner-position-function
185 | "Eldoc-box uses this function to set childframe's position.
186 |
187 | The function is passed two arguments, WIDTH and HEIGHT of the
188 | childframe, and should return a (X . Y) cons cell.")
189 |
190 | (defvar eldoc-box-at-point-position-function #'eldoc-box--default-at-point-position-function
191 | "Eldoc-box uses this function to set childframe's position.
192 | This function is used in ‘eldoc-box-help-at-point’ and in
193 | ‘eldoc-box-hover-at-point-mode’.
194 |
195 | The function is passed two arguments, WIDTH and HEIGHT of the
196 | childframe, and should return a (X . Y) cons cell.")
197 |
198 | (defcustom eldoc-box-fringe-use-same-bg t
199 | "T means fringe's background color is set to as same as that of default."
200 | :type 'boolean)
201 |
202 | (defvar-local eldoc-box-buffer-setup-function #'eldoc-box-buffer-setup
203 | "Function that setups the doc buffer.
204 |
205 | This function is given the original buffer as the sole argument, and
206 | runs with the eldoc-box buffer as the current buffer.
207 |
208 | Everytime eldoc-box displays a documentation, it inserts the doc and
209 | calls this function to setup the buffer.
210 |
211 | This is a buffer-local variable, and eldoc-box takes the value of this
212 | variable from the origin buffer, and runs it in the doc buffer. This
213 | allows different major modes to run different setup functions.")
214 |
215 | (defvar eldoc-box-buffer-setup-hook nil
216 | "Hooks that runs in the doc buffer before ‘eldoc-box-buffer-hook’.
217 |
218 | Functions in this hook are also passed the original buffer as the sole
219 | argument.")
220 |
221 | (defvar eldoc-box-buffer-hook '(eldoc-box--prettify-markdown-separator
222 | eldoc-box--replace-en-space
223 | eldoc-box--remove-linked-images
224 | eldoc-box--remove-noise-chars
225 | eldoc-box--fontify-html
226 | eldoc-box--condense-large-newline-gaps)
227 | "Hook run after buffer for doc is setup.
228 | Run inside the new buffer. By default, it contains some Markdown
229 | prettifiers, which see.")
230 |
231 | (defvar eldoc-box-frame-hook nil
232 | "Hook run after doc frame is setup but just before it is made visible.
233 | Each function runs inside the new frame and receives the main frame as argument.")
234 |
235 | (defcustom eldoc-box-self-insert-command-list '(self-insert-command outshine-self-insert-command)
236 | "Commands in this list are considered `self-insert-command' by eldoc-box.
237 | See `eldoc-box-inhibit-display-when-moving'."
238 | :type '(repeat symbol))
239 |
240 | ;;;;; Function
241 | (defvar eldoc-box--inhibit-childframe nil
242 | "If non-nil, inhibit display of childframe.")
243 |
244 | (defvar eldoc-box--frame nil ;; A backstage variable
245 | "The frame to display doc.")
246 |
247 | (defun eldoc-box-quit-frame ()
248 | "Hide documentation childframe."
249 | (interactive)
250 | (when (and eldoc-box--frame (frame-live-p eldoc-box--frame))
251 | (make-frame-invisible eldoc-box--frame t)))
252 |
253 | (defvar-local eldoc-box--old-eldoc-functions nil
254 | "The original value of ‘eldoc-display-functions’.
255 | The original value before enabling eldoc-box.")
256 |
257 | (defun eldoc-box--enable ()
258 | "Enable eldoc-box hover.
259 | Intended for internal use."
260 | (if (not (boundp 'eldoc-display-functions))
261 | (add-function :before-while (local 'eldoc-message-function)
262 | #'eldoc-box--eldoc-message-function)
263 |
264 | (setq-local eldoc-box--old-eldoc-functions
265 | eldoc-display-functions)
266 | (setq-local eldoc-display-functions
267 | (cons 'eldoc-box--eldoc-display-function
268 | (remq 'eldoc-display-in-echo-area
269 | eldoc-display-functions))))
270 |
271 | (when eldoc-box-clear-with-C-g
272 | (advice-add #'keyboard-quit :before #'eldoc-box-quit-frame)))
273 |
274 | (defun eldoc-box--disable ()
275 | "Disable eldoc-box hover.
276 | Intended for internal use."
277 | (if (not (boundp 'eldoc-display-functions))
278 | (remove-function (local 'eldoc-message-function) #'eldoc-box--eldoc-message-function)
279 |
280 | (setq-local eldoc-display-functions
281 | (remq 'eldoc-box--eldoc-display-function
282 | eldoc-display-functions))
283 | ;; If we removed eldoc-display-in-echo-area when enabling
284 | ;; eldoc-box, add it back.
285 | (when (memq 'eldoc-display-in-echo-area
286 | eldoc-box--old-eldoc-functions)
287 | (setq-local eldoc-display-functions
288 | (cons 'eldoc-display-in-echo-area
289 | eldoc-display-functions))))
290 |
291 | (advice-remove #'keyboard-quit #'eldoc-box-quit-frame)
292 | ;; If minor mode is turned off when the childframe is visible, hide it.
293 | (when eldoc-box--frame
294 | (delete-frame eldoc-box--frame)
295 | (setq eldoc-box--frame nil)))
296 |
297 | ;;;;; Commands
298 |
299 | (defun eldoc-box-scroll-up (arg)
300 | "Scroll up ARG lines in the childframe."
301 | (interactive "p")
302 | (when eldoc-box--frame
303 | (with-selected-frame eldoc-box--frame
304 | (scroll-up arg))))
305 |
306 | (defun eldoc-box-scroll-down (arg)
307 | "Scroll down ARG lines in the childframe."
308 | (interactive "p")
309 | (when eldoc-box--frame
310 | (with-selected-frame eldoc-box--frame
311 | (scroll-down arg))))
312 |
313 | ;;;;; Help at point
314 |
315 | (defvar eldoc-box--help-at-point-last-point 0
316 | "This point cache is used by the clean up function.
317 | If point != last point, hide the childframe.")
318 |
319 | (defun eldoc-box--help-at-point-cleanup ()
320 | "Try to clean up the childframe."
321 | (if (or (eq (point) eldoc-box--help-at-point-last-point)
322 | ;; Don't clean up when the user clicks into the childframe.
323 | (eq (selected-frame) eldoc-box--frame))
324 | (run-with-timer 0.1 nil #'eldoc-box--help-at-point-cleanup)
325 | (eldoc-box-quit-frame)))
326 |
327 | (defun eldoc-box--help-at-point-async-update (docs _interactive)
328 | "Update async doc changes to help-at-point childframe.
329 |
330 | This is added to ‘eldoc-display-functions’, such that when async doc
331 | comes in, the at-point doc pop-up can be updated.
332 |
333 | For DOCS, see ‘eldoc-display-functions’."
334 | (when (and eldoc-box--frame
335 | (frame-live-p eldoc-box--frame)
336 | (frame-visible-p eldoc-box--frame)
337 | (eq eldoc-box--help-at-point-last-point (point)))
338 | (let ((eldoc-box-position-function
339 | eldoc-box-at-point-position-function))
340 | (eldoc-box--display
341 | (string-join
342 | (mapcar #'car docs)
343 | (concat "\n"
344 | (or (bound-and-true-p eldoc-doc-buffer-separator) "---")
345 | "\n"))))))
346 |
347 | ;;;###autoload
348 | (defun eldoc-box-help-at-point ()
349 | "Display documentation of the symbol at point."
350 | (interactive)
351 | (when (boundp 'eldoc--doc-buffer)
352 | (add-hook 'eldoc-display-functions
353 | #'eldoc-box--help-at-point-async-update 0 t)
354 | (let ((eldoc-box-position-function
355 | eldoc-box-at-point-position-function)
356 | (doc (with-current-buffer eldoc--doc-buffer
357 | (buffer-string))))
358 | (eldoc-box--display
359 | (if (equal doc "")
360 | "There’s no doc to display at this point" doc)))
361 | (setq eldoc-box--help-at-point-last-point (point))
362 | (run-with-timer 0.1 nil #'eldoc-box--help-at-point-cleanup)
363 | (when eldoc-box-clear-with-C-g
364 | (advice-add #'keyboard-quit :before #'eldoc-box-quit-frame))))
365 |
366 | ;;;; Backstage
367 | ;;;;; Variable
368 | (defvar eldoc-box--buffer " *eldoc-box*"
369 | "The buffer used to display documentation.")
370 |
371 | ;;;;; Function
372 |
373 | ;; Please compiler.
374 | (defvar eldoc-box-hover-mode)
375 |
376 | (defun eldoc-box-buffer-setup (orig-buffer)
377 | "Setup the doc buffer."
378 | (setq mode-line-format nil)
379 | (setq header-line-format nil)
380 | ;; WORKAROUND: (issue#66) If cursor-type is ‘box’, sometimes the
381 | ;; cursor is still shown for some reason.
382 | (setq-local cursor-type t)
383 | (when (bound-and-true-p global-tab-line-mode)
384 | (setq tab-line-format nil))
385 | ;; Without this, clicking childframe will make doc buffer the
386 | ;; current buffer and `eldoc-box--maybe-cleanup' in
387 | ;; `eldoc-box--cleanup-timer' will clear the childframe
388 | (buffer-face-set 'eldoc-box-body)
389 | (setq eldoc-box-hover-mode t)
390 | (visual-line-mode)
391 | ;; Use buffer-local binding in the original buffer
392 | ;; for the setup hook to allow original mode-specific setup.
393 | (setq-local eldoc-box-buffer-setup-hook
394 | (buffer-local-value 'eldoc-box-buffer-setup-hook orig-buffer))
395 | (run-hook-with-args 'eldoc-box-buffer-setup-hook orig-buffer)
396 | (run-hook-with-args 'eldoc-box-buffer-hook))
397 |
398 | (defun eldoc-box--display (str)
399 | "Display STR in childframe.
400 | STR has to be a proper documentation, not empty string, not nil, etc."
401 | (let ((doc-buffer (get-buffer-create eldoc-box--buffer))
402 | (origin-buffer (current-buffer))
403 | (setup-function eldoc-box-buffer-setup-function))
404 | (with-current-buffer doc-buffer
405 | (let ((inhibit-read-only t))
406 | (erase-buffer)
407 | (insert str)
408 | (goto-char (point-min))
409 | (funcall setup-function origin-buffer)))
410 | (eldoc-box--get-frame doc-buffer)))
411 |
412 | (defun eldoc-box--window-side ()
413 | "Return the side of the selected window.
414 | Symbol ‘left’ if the selected window is on the left, ‘right’ if
415 | on the right. Return ‘left’ if there is only one window."
416 | ;; Calculate the left and right distances to the frame edge of the
417 | ;; active window. If the left distance is less than or equal to the
418 | ;; right distance, it indicates that the active window is on the left.
419 | ;; Otherwise, it is on the right.
420 | (let* ((window-left (nth 0 (window-absolute-pixel-edges)))
421 | (window-right (nth 2 (window-absolute-pixel-edges)))
422 | (frame-left (nth 0 (frame-edges)))
423 | (frame-right (nth 2 (frame-edges)))
424 | (distance-left (- window-left frame-left))
425 | (distance-right (- frame-right window-right)))
426 | ;; When `distance-left' equals `distance-right', it means there is
427 | ;; only one window in current frame, or the current active window
428 | ;; occupies the entire frame horizontally, return left.
429 | (if (<= distance-left distance-right)
430 | 'left
431 | 'right)))
432 |
433 | (defun eldoc-box--default-upper-corner-position-function (width _)
434 | "The default function to set childframe position.
435 | Used by `eldoc-box-position-function'.
436 | Position is calculated base on WIDTH and HEIGHT of childframe text window"
437 | (pcase-let ((`(,offset-l ,offset-r ,offset-t) eldoc-box-offset))
438 | (cons (pcase (eldoc-box--window-side) ; x position + offset
439 | ;; display doc on right
440 | ('left (- (frame-outer-width (selected-frame)) width offset-r))
441 | ;; display doc on left
442 | ('right offset-l))
443 | ;; y position + v-offset
444 | offset-t)))
445 |
446 | (defun eldoc-box--point-position-relative-to-native-frame (&optional point window)
447 | "Return (X . Y) as the coordinate of POINT in WINDOW.
448 | The coordinate is relative to the native frame.
449 |
450 | WINDOW nil means use selected window."
451 | (unless point
452 | ;; Handle edge case. See https://debbugs.gnu.org/cgi/bugreport.cgi?bug=69259.
453 | (setq point (window-point window)))
454 | (let* ((pos (pos-visible-in-window-p point window t))
455 | (x (car pos))
456 | (en (frame-char-width))
457 | (y (cadr pos))
458 | (edges (window-edges window nil nil t)))
459 | ;; HACK: for unknown reasons we need to add en to x position
460 | (cons (+ x (car edges) en)
461 | (+ y (cadr edges)))))
462 |
463 | (defun eldoc-box--default-at-point-position-function-1 (width height)
464 | "See `eldoc-box--default-at-point-position-function' for WIDTH & HEIGHT docs."
465 | (let* ((point-pos (eldoc-box--point-position-relative-to-native-frame))
466 | ;; calculate point coordinate relative to native frame
467 | ;; because childframe coordinate is relative to native frame
468 | (x (car point-pos))
469 | (y (cdr point-pos))
470 | (em (frame-char-height)))
471 | (cons (if (< (- (frame-inner-width) width) x)
472 | ;; space on the right of the pos is not enough
473 | ;; put to left
474 | (max 0 (- x width))
475 | ;; normal, just return x
476 | x)
477 | (if (< (- (frame-inner-height) height) y)
478 | ;; space under the pos is not enough
479 | ;; put above
480 | (max 0 (- y height))
481 | ;; normal, just return y + em
482 | (+ y em)))))
483 |
484 | (defun eldoc-box--default-at-point-position-function (width height)
485 | "Set `eldoc-box-position-function' to this function.
486 | To have childframe appear under point. Position is calculated
487 | base on WIDTH and HEIGHT of childframe text window."
488 | (let* ((pos (eldoc-box--default-at-point-position-function-1 width height))
489 | (x (car pos))
490 | (y (cdr pos)))
491 | (or (eldoc-box--at-point-x-y-by-corfu)
492 | (cons (or (eldoc-box--at-point-x-by-company) x)
493 | y))))
494 |
495 | (defvar eldoc-box--markdown-separator-display-props)
496 |
497 | (defun eldoc-box--update-childframe-geometry (frame window)
498 | "Update the size and the position of childframe.
499 | FRAME is the childframe, WINDOW is the primary window."
500 | ;; WORKAROUND: See issue#68. If there’s some text with a display
501 | ;; property of (space :width text) -- which is what we apply onto
502 | ;; markdown separators -- ‘window-text-pixel-size’ wouldn’t return
503 | ;; the correct value. Instead, it returns the current window width.
504 | ;; So now the childram only grows in size and never shrinks.
505 | ;;
506 | ;; (My guess is that the function takes (space :width text) at face
507 | ;; value, but that can’t be the whole picture because it works fine
508 | ;; when I manually evaluate the function in the childframe...)
509 | ;;
510 | ;; The original workaround of setting the frame size to something
511 | ;; small before calling ‘window-text-pixel-size’ works, but brings
512 | ;; other problems. Now we just set the display property to nil
513 | ;; before calling ‘window-text-pixel-size’, and set them back after.
514 | (setcdr eldoc-box--markdown-separator-display-props nil)
515 |
516 | (let* ((size
517 | (window-text-pixel-size
518 | window nil nil
519 | (if (functionp eldoc-box-max-pixel-width) (funcall eldoc-box-max-pixel-width) eldoc-box-max-pixel-width)
520 | (if (functionp eldoc-box-max-pixel-height) (funcall eldoc-box-max-pixel-height) eldoc-box-max-pixel-height)
521 | t))
522 | (width (car size))
523 | (height (cdr size))
524 | (width (+ width (frame-char-width frame))) ; add margin
525 | (frame-resize-pixelwise t)
526 | (pos (funcall eldoc-box-position-function width height)))
527 | (set-frame-size frame width height t)
528 |
529 | ;; Set the display property back.
530 | (setcdr eldoc-box--markdown-separator-display-props
531 | '(:width text))
532 |
533 | ;; move position
534 | (set-frame-position frame (car pos) (cdr pos))))
535 |
536 | (defun eldoc-box--inhibit-childframe-for (sec)
537 | "Inhibit display of childframe for SEC seconds after Emacs is idle again."
538 | (unless eldoc-box--inhibit-childframe
539 | (setq eldoc-box--inhibit-childframe t)
540 | (eldoc-box-quit-frame)
541 | (run-with-idle-timer sec nil
542 | (lambda ()
543 | (setq eldoc-box--inhibit-childframe nil)))))
544 |
545 | (defun eldoc-box--follow-cursor ()
546 | "Make childframe follow cursor in at-point mode."
547 | (unless eldoc-box--inhibit-childframe
548 | (if (member this-command eldoc-box-self-insert-command-list)
549 | (progn (when (frame-live-p eldoc-box--frame)
550 | (eldoc-box--update-childframe-geometry
551 | eldoc-box--frame (frame-selected-window eldoc-box--frame))))
552 | ;; if not typing, inhibit display
553 | (eldoc-box--inhibit-childframe-for 0.5))))
554 |
555 | (defun eldoc-box--get-frame (buffer)
556 | "Return a childframe displaying BUFFER.
557 | Checkout `lsp-ui-doc--make-frame', `lsp-ui-doc--move-frame'."
558 | (if eldoc-box--inhibit-childframe
559 | ;; if inhibit display, do nothing
560 | eldoc-box--frame
561 | (let* ((after-make-frame-functions nil)
562 | (before-make-frame-hook nil)
563 | (parameter (append eldoc-box-frame-parameters
564 | `((default-minibuffer-frame . ,(selected-frame))
565 | (minibuffer . ,(minibuffer-window))
566 | (left-fringe . ,(frame-char-width)))))
567 | window frame
568 | (main-frame (selected-frame)))
569 | (if (and eldoc-box--frame (frame-live-p eldoc-box--frame))
570 | (progn (setq frame eldoc-box--frame)
571 | (setq window (frame-selected-window frame))
572 | ;; in case the main frame changed
573 | (set-frame-parameter frame 'parent-frame main-frame))
574 | (setq window (display-buffer-in-child-frame
575 | buffer
576 | `((child-frame-parameters . ,parameter))))
577 | (setq frame (window-frame window)))
578 | ;; workaround
579 | ;; (set-frame-parameter frame 'left-fringe (alist-get 'left-fringe eldoc-box-frame-parameters))
580 | ;; (set-frame-parameter frame 'right-fringe (alist-get 'right-fringe eldoc-box-frame-parameters))
581 |
582 | (set-face-attribute 'fringe frame :background 'unspecified :inherit 'eldoc-box-body)
583 | (set-window-dedicated-p window t)
584 | (redirect-frame-focus frame (frame-parent frame))
585 | (set-face-attribute 'internal-border frame :inherit 'eldoc-box-border)
586 | (when (facep 'child-frame-border)
587 | (set-face-background 'child-frame-border
588 | (face-attribute 'eldoc-box-border :background nil t)
589 | frame))
590 | (eldoc-box--update-childframe-geometry frame window)
591 | (set-window-margins window nil nil)
592 | (setq eldoc-box--frame frame)
593 | (with-selected-frame frame
594 | (run-hook-with-args 'eldoc-box-frame-hook main-frame))
595 | (make-frame-visible frame))))
596 |
597 | ;;;;; ElDoc
598 |
599 | (defvar eldoc-box--cleanup-timer nil
600 | "The timer used to cleanup childframe after ElDoc.")
601 |
602 | (defvar eldoc-box--last-point 0
603 | ;; used in `eldoc-box--maybe-cleanup'
604 | "Last point when eldoc-box showed childframe.")
605 |
606 | ;; Please compiler.
607 | (defvar eldoc-box-hover-at-point-mode)
608 | (defun eldoc-box--maybe-cleanup ()
609 | "Clean up after ElDoc."
610 | ;; timer is global, so this function will be called outside
611 | ;; the buffer with `eldoc-box-hover-mode' enabled
612 | (if (and (frame-parameter eldoc-box--frame 'visibility)
613 | (or (and (not eldoc-last-message) ; 1
614 | (not (eq (point) eldoc-box--last-point)) ; 2
615 | (not (eq (current-buffer) (get-buffer eldoc-box--buffer)))) ; 3
616 | (not (or eldoc-box-hover-mode eldoc-box-hover-at-point-mode)))) ; 4
617 | ;; 1. Obviously, last-message nil means we are not on a valid symbol anymore.
618 | ;; 2. Or are we? If you scroll the childframe with mouse wheel
619 | ;; `eldoc-pre-command-refresh-echo-area' will set `eldoc-last-message' to nil.
620 | ;; Without the point test, this function, called by `eldoc-box--cleanup-timer'
621 | ;; will clear the doc frame, not good
622 | ;; 3. If scrolling can't satisfy you and you clicked the childframe
623 | ;; both 1. and 2. are satisfied. 3. is the last hope to prevent this function
624 | ;; from clearing your precious childframe. There is another safety pin in
625 | ;; `eldoc-box--display' that works with 3.
626 | ;; 4. Sometimes you switched buffer when childframe is on.
627 | ;; it wouldn't go away unless you goes back and let eldoc shut it off.
628 | ;; So if we are not in `eldoc-box-hover-mode', clear childframe
629 | (eldoc-box-quit-frame)
630 | ;; so you didn't clear the doc frame this time, and the last timer has ran out
631 | ;; setup another one to make sure the doc frame is cleared
632 | ;; once the condition above it met
633 | (setq eldoc-box--cleanup-timer
634 | (run-with-timer eldoc-box-cleanup-interval nil #'eldoc-box--maybe-cleanup))))
635 |
636 | (defun eldoc-box--count-newlines (str)
637 | "Count the number of newlines in STR, excluding invisible ones.
638 | Trailing newlines doesn’t count."
639 | (let ((idx 0)
640 | (count 0)
641 | (last-visible-newline nil)
642 | (len (length str))
643 | ;; Is the last visible newline a trailing newline?
644 | (last-newline-trailing-p nil))
645 |
646 | ;; Count visible newlines in STR.
647 | (while (and (not (eq idx len))
648 | (setq idx (string-search "\n" str
649 | (if (eq idx 0) 0 (1+ idx)))))
650 | (unless (memq 'invisible (text-properties-at idx str))
651 | (setq last-visible-newline idx)
652 | (cl-incf count)))
653 |
654 | ;; If there is any visible character after the last newline, it is
655 | ;; not a trailing newline.
656 | (when last-visible-newline
657 | (setq last-newline-trailing-p t)
658 | (let ((idx (1+ last-visible-newline)))
659 | (while (< idx len)
660 | (when (not (memq 'invisible (text-properties-at idx str)))
661 | (setq last-newline-trailing-p nil))
662 | (cl-incf idx))))
663 |
664 | (if last-newline-trailing-p
665 | (1- count)
666 | count)))
667 |
668 | (defun eldoc-box--eldoc-message-function (str &rest args)
669 | "Front-end for eldoc.
670 | Display STR in childframe and ARGS works like `message'."
671 | (when (stringp str)
672 | (let* ((doc (string-trim-right (apply #'format str args)))
673 | (single-line-p (and eldoc-box-only-multi-line
674 | (eq (eldoc-box--count-newlines doc) 0))))
675 | (when (and (not (equal doc ""))
676 | (not single-line-p))
677 | (eldoc-box--display doc)
678 | (setq eldoc-box--last-point (point))
679 | ;; Why a timer? ElDoc is mainly used in minibuffer,
680 | ;; where the text is constantly being flushed by other commands
681 | ;; so ElDoc doesn't try very hard to cleanup
682 | (when eldoc-box--cleanup-timer
683 | (cancel-timer eldoc-box--cleanup-timer))
684 | ;; This function is also called by
685 | ;; `eldoc-pre-command-refresh-echo-area' in
686 | ;; `pre-command-hook', which means the timer is reset before
687 | ;; every command if `eldoc-box-hover-mode' is on and
688 | ;; `eldoc-last-message' is not nil.
689 | (setq eldoc-box--cleanup-timer
690 | (run-with-timer eldoc-box-cleanup-interval
691 | nil #'eldoc-box--maybe-cleanup)))
692 | ;; Return nil to stop ‘eldoc--message’ from running, because
693 | ;; this function is added as a ‘:before-while’ advice.
694 | single-line-p)))
695 |
696 | (defun eldoc-box--compose-doc (doc)
697 | "Compose a doc passed from eldoc.
698 |
699 | DOC has the form of (TEXT :KEY VAL...), and KEY can be ‘:thing’
700 | and ‘:face’, among other things. If ‘:thing’ exists, it is put at
701 | the start of the doc followed by a colon. If ‘:face’ exists, it
702 | is applied to the thing.
703 |
704 | Return the composed string."
705 | (let ((thing (plist-get (cdr doc) :thing))
706 | (face (plist-get (cdr doc) :face)))
707 | (concat (if thing
708 | (concat (propertize (format "%s" thing) 'face face) ": ")
709 | "")
710 | (car doc))))
711 |
712 | (defun eldoc-box--eldoc-display-function (docs interactive)
713 | "Display DOCS in childframe.
714 | For DOCS and INTERACTIVE see ‘eldoc-display-functions’. Maybe
715 | display the docs in echo area depending on
716 | ‘eldoc-box-only-multi-line’."
717 | (let ((doc (string-trim (string-join
718 | (mapcar #'eldoc-box--compose-doc docs)
719 | eldoc-box-doc-separator))))
720 | (when (eldoc-box--eldoc-message-function "%s" doc)
721 | (eldoc-display-in-echo-area docs interactive))))
722 |
723 | ;;;###autoload
724 | (define-minor-mode eldoc-box-hover-mode
725 | "Display hover documentations in a childframe.
726 | The default position of childframe is upper corner."
727 | :lighter eldoc-box-lighter
728 | (if eldoc-box-hover-mode
729 | (progn (when eldoc-box-hover-at-point-mode
730 | (eldoc-box-hover-at-point-mode -1))
731 | (eldoc-box--enable))
732 | (eldoc-box--disable)))
733 |
734 | ;;;###autoload
735 | (define-minor-mode eldoc-box-hover-at-point-mode
736 | "A convenient minor mode to display doc at point.
737 | You can use \\[keyboard-quit] to hide the doc."
738 | :lighter eldoc-box-lighter
739 | (if eldoc-box-hover-at-point-mode
740 | (progn (when eldoc-box-hover-mode
741 | (eldoc-box-hover-mode -1))
742 | (setq-local eldoc-box-position-function
743 | eldoc-box-at-point-position-function)
744 | (setq-local eldoc-box-clear-with-C-g t)
745 | (remove-hook 'pre-command-hook #'eldoc-pre-command-refresh-echo-area t)
746 | (add-hook 'post-command-hook #'eldoc-box--follow-cursor t t)
747 | (eldoc-box--enable))
748 | (eldoc-box--disable)
749 | (add-hook 'pre-command-hook #'eldoc-pre-command-refresh-echo-area t)
750 | (remove-hook 'post-command-hook #'eldoc-box--follow-cursor t)
751 | (kill-local-variable 'eldoc-box-position-function)
752 | (kill-local-variable 'eldoc-box-clear-with-C-g)))
753 |
754 | ;;;; Eglot helper
755 |
756 | (make-obsolete 'eldoc-box-eglot-help-at-point 'eldoc-box-help-at-point
757 | "v1.11.1")
758 |
759 | (defun eldoc-box-eglot-help-at-point ()
760 | "Display documentation of the symbol at point.
761 | This is now obsolete, you should use ‘eldoc-box-help-at-point’
762 | instead."
763 | (interactive)
764 | (eldoc-box-help-at-point))
765 |
766 | ;;;; Company compatibility
767 | ;;
768 |
769 | ;; see also `eldoc-box--default-at-point-position-function'
770 |
771 | ;; please compiler
772 | (defvar company-pseudo-tooltip-overlay)
773 | (declare-function company-box--get-frame "company-box")
774 |
775 | (defun eldoc-box--at-point-x-by-company ()
776 | "Return the x position that accommodates company's popup."
777 | (cond
778 | ((and (boundp 'company-pseudo-tooltip-overlay)
779 | company-pseudo-tooltip-overlay)
780 | (+ (* (frame-char-width)
781 | (+ (overlay-get company-pseudo-tooltip-overlay
782 | 'company-width)
783 | (overlay-get company-pseudo-tooltip-overlay
784 | 'company-column)))
785 | (or (line-number-display-width t) 0)))
786 | ((and (boundp 'company-box--x) (numberp company-box--x))
787 | (+ company-box--x
788 | (frame-pixel-width (company-box--get-frame))))
789 | (t nil)))
790 |
791 | ;;;; Corfu compatibility
792 |
793 | (defvar corfu--frame)
794 | (defun eldoc-box--at-point-x-y-by-corfu ()
795 | "Return the x-y position that accommodates corfu's popup.
796 |
797 | Returns a cons (X . Y) of pixel positions relative to the native frame.
798 | Return nil if corfu frame isn’t visible."
799 | (when (and (boundp 'corfu--frame)
800 | corfu--frame
801 | (frame-live-p corfu--frame)
802 | (frame-visible-p corfu--frame))
803 | (cons (+ (car (frame-position corfu--frame))
804 | (frame-pixel-width corfu--frame))
805 | (cdr (frame-position corfu--frame)))))
806 |
807 | ;;;; Markdown compatibility
808 |
809 | (defvar-local eldoc-box--markdown-separator-display-props
810 | '(space :width text)
811 | "Stores the display text property applied to markdown separators.
812 |
813 | Due to a bug, in ‘eldoc-box--update-childframe-geometry’, we
814 | modify the display property temporarily and then set it back.")
815 |
816 | (defun eldoc-box--prettify-markdown-separator ()
817 | "Prettify the markdown separator in doc returned by Eglot.
818 | Refontify the separator so they span exactly the width of the
819 | childframe."
820 | (save-excursion
821 | (goto-char (point-min))
822 | (let (prop)
823 | (while (setq prop (text-property-search-forward 'markdown-hr))
824 | (add-text-properties
825 | (prop-match-beginning prop)
826 | (prop-match-end prop)
827 | `( display ,eldoc-box--markdown-separator-display-props
828 | face eldoc-box-markdown-separator))))))
829 |
830 | (defun eldoc-box--replace-en-space ()
831 | "Display the en spaces in documentation as regular spaces."
832 | (face-remap-set-base 'nobreak-space '(:inherit default))
833 | (face-remap-set-base 'markdown-line-break-face '(:inherit default)))
834 |
835 | (defun eldoc-box--condense-large-newline-gaps ()
836 | "Condense exceedingly large gaps made of consecutive newlines.
837 |
838 | These gaps are usually made of hidden \"```\" and/or consecutive
839 | newlines. Replace those gaps with a single empty line at 0.5 line
840 | height."
841 | (save-excursion
842 | (goto-char (point-min))
843 | (while (re-search-forward
844 | (rx (>= 2 (or "\n"
845 | (seq bol "```" (* (syntax word)) "\n")
846 | (seq (+ "
") "\n")
847 | (seq bol (+ (or " " "\t" " ")) "\n"))))
848 | nil t)
849 | (if (or (eq (match-beginning 0) (point-min))
850 | (eq (match-end 0) (point-max)))
851 | (replace-match "")
852 | (replace-match "\n\n")
853 | (add-text-properties (1- (point)) (point)
854 | '( font-lock-face (:height 0.4)
855 | face (:height 0.4)))))))
856 |
857 | (defun eldoc-box--remove-linked-images ()
858 | "Some documentation embed image links in the doc...remove them."
859 | (save-excursion
860 | (goto-char (point-min))
861 | ;; Find every Markdown image link, and remove them.
862 | (while (re-search-forward
863 | (rx "[" (seq " ")") "]"
864 | "(" (+? anychar) ")")
865 | nil t)
866 | (replace-match ""))))
867 |
868 | (defun eldoc-box--remove-noise-chars ()
869 | "Remove some noise characters like carriage return."
870 | (save-excursion
871 | (goto-char (point-min))
872 | (while (search-forward "\r" nil t)
873 | (replace-match ""))))
874 |
875 | (defun eldoc-box--fontify-html ()
876 | "Fontify HTML tags and special entities."
877 | (save-excursion
878 | ;; tags.
879 | (goto-char (point-min))
880 | (while (re-search-forward
881 | (rx bol
882 | (group "")
883 | (group (*? anychar))
884 | (group "")
885 | eol)
886 | nil t)
887 | (add-text-properties (match-beginning 2)
888 | (match-end 2)
889 | '( face (:weight bold)
890 | font-lock-face (:weight bold)))
891 | (put-text-property (match-beginning 1) (match-end 1)
892 | 'invisible t)
893 | (put-text-property (match-beginning 3) (match-end 3)
894 | 'invisible t))
895 | ;; Don't show these tags.
896 | (goto-char (point-min))
897 | (while (re-search-forward
898 | (rx (group "
")
899 | (group (*? anychar))
900 | (group "
"))
901 | nil t)
902 | (put-text-property (match-beginning 1) (match-end 1)
903 | 'invisible t)
904 | (put-text-property (match-beginning 3) (match-end 3)
905 | 'invisible t))
906 | ;; Special entities.
907 | (goto-char (point-min))
908 | (while (re-search-forward (rx (or "<" ">" " ")) nil t)
909 | (put-text-property (match-beginning 0) (match-end 0)
910 | 'display
911 | (pcase (match-string 0)
912 | ("<" "<")
913 | (">" ">")
914 | (" " " "))))))
915 |
916 | ;;;; Tab-bar compatibility
917 |
918 | (defun eldoc-box-reset-frame ()
919 | "Discard the current childframe and regenerate one.
920 | This allows any change in childframe parameter to take effect."
921 | (interactive)
922 | (when eldoc-box--frame
923 | (delete-frame eldoc-box--frame)
924 | (setq eldoc-box--frame nil)))
925 |
926 | (with-eval-after-load 'tab-bar
927 | (add-hook 'tab-bar-mode-hook #'eldoc-box-reset-frame))
928 |
929 | (with-eval-after-load 'tab-line
930 | (add-hook 'tab-line-mode-hook #'eldoc-box-reset-frame))
931 |
932 | ;;;; Prettify Typescript error message
933 |
934 | (defun eldoc-box-prettify-ts-errors (orig-buffer)
935 | "Quick-and-dirty prettification for Typescript errors.
936 |
937 | ORIG-BUFFER is used to get the Typescript major mode for fontification
938 | and indentation.
939 |
940 | The ‘noErrorTruncation’ compiler option must be set to true, otherwise
941 | the compiler truncates the types and formatting wouldn’t work."
942 | (goto-char (point-min))
943 | (let ((workbuf (get-buffer-create " *eldoc-box--prettify-ts-errors*"))
944 | type-text
945 | fontified-type
946 | multi-line)
947 | (with-current-buffer workbuf
948 | (funcall (buffer-local-value 'major-mode orig-buffer)))
949 | ;; 1. Prettify types.
950 | (while (re-search-forward
951 | ;; Typescript uses doble quotes for literal unions like
952 | ;; type A = "A" | "AA", so we don’t need to worry about
953 | ;; single quotes in the type.
954 | (rx (or "Type" "type") " "
955 | (group "'" (group (+? anychar)) "'"))
956 | nil t)
957 | (save-match-data
958 | (setq type-text (match-string 2))
959 | (setq fontified-type
960 | (with-current-buffer workbuf
961 | (erase-buffer)
962 | (insert "type A = ")
963 | (insert type-text)
964 |
965 | (goto-char (point-min))
966 | (while (re-search-forward (rx (or "{" ";")) nil t)
967 | (insert "\n"))
968 | (goto-char (point-min))
969 | (while (search-forward "|" nil t)
970 | (when (equal "}" (char-before (max (point-min) (- (point) 2))))
971 | (replace-match "\n|")))
972 | (indent-region (point-min) (point-max))
973 |
974 | (font-lock-fontify-region (point-min) (point-max))
975 | ;; Make sure the type are in monospace font.
976 | (font-lock-append-text-property
977 | (point-min) (point-max)
978 | 'face `(:family ,(face-attribute 'fixed-pitch :family)))
979 |
980 | ;; Don’t include the "type A = " we inserted earlier.
981 | (string-trim
982 | (buffer-substring (+ (point-min) 9) (point-max)))))
983 | (setq multi-line (string-search "\n" fontified-type))
984 | ;; Indent and add newline at the beginning and the end.
985 | (when multi-line
986 | (setq fontified-type
987 | (concat "\n"
988 | (mapconcat (lambda (line)
989 | (concat " " line))
990 | (string-split fontified-type "\n")
991 | "\n")
992 | "\n"))))
993 | (if (not multi-line)
994 | (replace-match fontified-type nil nil nil 2)
995 | (replace-match fontified-type nil nil nil 1)
996 | ;; Remove the first whitespace on the next line after the
997 | ;; multi-line type.
998 | (delete-char 1)))
999 | ;; 2. Prettify properties.
1000 | (goto-char (point-min))
1001 | (while (re-search-forward
1002 | (rx (or "Property" "property") " "
1003 | (group "'" (group (+? anychar)) "'"))
1004 | nil t)
1005 | (put-text-property (match-beginning 2) (match-end 2)
1006 | 'face 'font-lock-property-name-face))))
1007 |
1008 | (provide 'eldoc-box)
1009 |
1010 | ;;; eldoc-box.el ends here
1011 |
--------------------------------------------------------------------------------
/prettify-ts-error.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/casouri/eldoc-box/fb1ae42c37c5f3bb80b441b2fdfada914891a714/prettify-ts-error.png
--------------------------------------------------------------------------------
/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/casouri/eldoc-box/fb1ae42c37c5f3bb80b441b2fdfada914891a714/screenshot.png
--------------------------------------------------------------------------------