├── README.org ├── demo.png ├── eldoc-box.el ├── prettify-ts-error.png └── screenshot.png /README.org: -------------------------------------------------------------------------------- 1 | #+TITLE: ElDoc box 2 | 3 | This package displays ElDoc documentations in a childframe. The childframe is selectable and scrollable with mouse, even though the cursor is hidden. 4 | 5 | [[https://melpa.org/#/eldoc-box][file:https://melpa.org/packages/eldoc-box-badge.svg]] 6 | [[https://stable.melpa.org/#/eldoc-box][file:https://stable.melpa.org/packages/eldoc-box-badge.svg]] 7 | 8 | #+CAPTION: Using with eglot in python-mode 9 | [[./screenshot.png]] 10 | 11 | * Install 12 | Get the file, add to load path, and 13 | #+BEGIN_SRC emacs-lisp 14 | (require 'eldoc-box) 15 | #+END_SRC 16 | 17 | It is also available on [[https://melpa.org/#/eldoc-box][MELPA]]. 18 | 19 | * Usage 20 | *Note:* If you use Gnome and Emacs 27, set ~x-gtk-resize-child-frames~ to ~resize-mode~ to avoid breakage of childframe. 21 | 22 | ** Function 23 | 24 | *Minor modes* 25 | 26 | - =eldoc-box-hover-mode= :: Enables a minor mode that displays documentation of the symbol at point in a childframe on upper corner. 27 | - =eldoc-box-hover-at-point-mode= :: Same as =eldoc-box-hover-mode= except the childframe is displayed at point, instead of on the upper corner. /Note that this mode brings a small but noticeable slow-down./ 28 | - =eldoc-box-mouse-mode= :: Instead of showing doc at point, show doc at mouse pointer. /This mode needs to enable =track-mouse=, which might slow down Emacs./ 29 | 30 | Only one of these modes can be enabled at a time. 31 | 32 | *Commands* 33 | 34 | - =eldoc-box-help-at-point= :: Display the documentation of the symbol at point in a temporary childframe, moving point or typing =C-g= disposes the childframe. Requires Emacs 28. The minor modes don’t need to be enabled for this command to work. 35 | - =eldoc-box-scroll-up/down= :: scrolls the childframe up/down. 36 | 37 | ** Face 38 | - =eldoc-box-border= :: Adjust =:background= of this face for border color. 39 | - =eldoc-box-body= :: Default face used by childframe. I suggest to use a nice sans serif font. 40 | - =eldoc-box-markdown-separator=: Face for the separator line (=
=) in Markdown documentation. 41 | 42 | ** Hooks 43 | - =eldoc-box-buffer-hook= :: A hook that runs after buffer for doc is setup. Run inside the new buffer every time before the new documentation is displayed. 44 | - =eldoc-box-frame-hook= :: A hook that runs after doc frame is setup but just before it is made visible. Each function runs inside the child frame and receives the main frame as the sole argument. 45 | - =eldoc-box-buffer-setup-hook= :: A hook that runs before =eldoc-box-buffer-hook= in the doc buffer. Each function is passed the source code buffer as the sole argument. Unlike =eldoc-box-buffer-hook=, eldoc-box takes the value of this hook from the original buffer (that contains source code), not the doc buffer. This allows more flexible customization for this hook based on major modes. 46 | 47 | ** Variable 48 | - =eldoc-box-max-pixel-width= & =eldoc-box-max-pixel-height= :: The max width/height of the childframe. 49 | - =eldoc-box-only-multi-line= :: Set this to non-nil and eldoc-box will only display multi-line message in childframe, and one line messages are left in minibuffer. 50 | - =eldoc-box-cleanup-interval= :: After this amount of seconds, eldoc-box will attempt to cleanup the childframe. E.g. if it is set to 1, the childframe is cleared 1 second after you moved the point to somewhere else (that doesn't have a doc to show). This doesn't apply to =eldoc-box-hover-at-point-mode=. In that mode, the childframe is cleared as soon as point moves. 51 | - =eldoc-box-fringe-use-same-bg= :: Whether to set fringe’s background color to as same as that of default. Default to t. 52 | - =eldoc-box-self-insert-command-list= :: By default =eldoc-box-hover-at-point-mode= only keeps childframe display while you are typing (ie, when =this-command= is =self-insert-command=). But if you bind something else to your keys, eldoc-box can’t recognize it and will hide childframe when you type. Add your command to this list so eldoc-box won’t hide childframe when this command is called. 53 | - =eldoc-box-lighter= :: Lighter displayed on the mode line. 54 | - =eldoc-box-buffer-setup-function= :: A function that runs in eldoc-box doc buffer for setup. See docstring for more information. 55 | - =eldoc-box-hover-display-frame-above-point= :: If non-nil, eldoc-box tries to display the at-point childframe above the point, instead of below. 56 | - =eldoc-box-mouse-mode-idle-delay= :: Time to wait until showing the doc in =eldoc-box-mouse-mode=. 57 | 58 | ** Use with eglot 59 | 60 | #+BEGIN_SRC emacs-lisp 61 | (add-hook 'eglot-managed-mode-hook #'eldoc-box-hover-mode t) 62 | #+END_SRC 63 | 64 | To keep eldoc from displaying documentation at point without enabling any minor mode above: =(add-to-list 'eglot-ignored-server-capabilites :hoverProvider)=. 65 | 66 | ** Default prettifier 67 | 68 | By default, eldoc-box tries to prettify the displayed markdown documentation as shown below. If you wish to disable them, remove the prettifier functions from =eldoc-box-buffer-hook=. Report an issue if there are other things can be prettfied away. 69 | 70 | [[./demo.png]] 71 | 72 | ** Prettify Typescript error message 73 | 74 | To prettify Typescript error messages, add =eldoc-box-prettify-ts-errors= to =eldoc-box-buffer-setup-hook= in Typescript modes. 75 | 76 | #+begin_src elisp 77 | (add-hook 'eldoc-box-buffer-setup-hook #'eldoc-box-prettify-ts-errors 0 t) 78 | #+end_src 79 | 80 | This way, Eldoc-box will format and highlight the types and properties in error messages: 81 | 82 | [[./prettify-ts-error.png]] 83 | 84 | 85 | * Credit 86 | - Thanks to [[https://github.com/joaotavora][João Távora]] for valuable contribution and explaining eldoc and eglot internals to me. 87 | - This package is initially adapted from Sebastien Chapuis’s package lsp-ui.el. 88 | -------------------------------------------------------------------------------- /demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/casouri/eldoc-box/fead2cef661790417267e5498d4d14806e020f99/demo.png -------------------------------------------------------------------------------- /eldoc-box.el: -------------------------------------------------------------------------------- 1 | ;;; eldoc-box.el --- Display documentation in childframe -*- lexical-binding: t; -*- 2 | 3 | ;; Copyright (C) 2018 Yuan Fu 4 | 5 | ;; Version: 1.15.1 6 | 7 | ;; Author: Yuan Fu 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 "![" (+? anychar) "](" (+? anychar) ")") "]" 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 --------------------------------------------------------------------------------