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