corfu-popupinfo.el (22196B)
1 ;;; corfu-popupinfo.el --- Candidate information popup for Corfu -*- lexical-binding: t -*- 2 3 ;; Copyright (C) 2021-2024 Free Software Foundation, Inc. 4 5 ;; Author: Yuwei Tian <fishtai0@gmail.com>, Daniel Mendler <mail@daniel-mendler.de> 6 ;; Maintainer: Daniel Mendler <mail@daniel-mendler.de> 7 ;; Created: 2022 8 ;; Version: 1.5 9 ;; Package-Requires: ((emacs "28.1") (compat "30") (corfu "1.5")) 10 ;; Homepage: https://github.com/minad/corfu 11 12 ;; This file is part of GNU Emacs. 13 14 ;; This program is free software: you can redistribute it and/or modify 15 ;; it under the terms of the GNU General Public License as published by 16 ;; the Free Software Foundation, either version 3 of the License, or 17 ;; (at your option) any later version. 18 19 ;; This program is distributed in the hope that it will be useful, 20 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 21 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 22 ;; GNU General Public License for more details. 23 24 ;; You should have received a copy of the GNU General Public License 25 ;; along with this program. If not, see <https://www.gnu.org/licenses/>. 26 27 ;;; Commentary: 28 29 ;; Display an information popup for completion candidate when using 30 ;; Corfu. The popup displays either the candidate documentation or the 31 ;; candidate location. The `corfu-popupinfo-mode' must be enabled 32 ;; globally. Set `corfu-popupinfo-delay' to nil if the info popup should 33 ;; not update automatically. If the popup should not appear initially, 34 ;; but update automatically afterwards, use `(setq corfu-popupinfo-delay 35 ;; (cons nil 1.0))'. 36 37 ;; For manual toggling the commands `corfu-popupinfo-toggle', 38 ;; `corfu-popupinfo-location' and `corfu-popupinfo-documentation' are 39 ;; bound in the `corfu-popupinfo-map'. 40 41 ;;; Code: 42 43 (require 'corfu) 44 (eval-when-compile 45 (require 'cl-lib) 46 (require 'subr-x)) 47 48 (defface corfu-popupinfo 49 '((t :inherit corfu-default)) 50 "Face used for the info popup." 51 :group 'corfu-faces) 52 53 (defcustom corfu-popupinfo-delay '(2.0 . 1.0) 54 "Automatically update info popup after that number of seconds. 55 56 The value can be a pair of two floats to specify initial and 57 subsequent delay. If the value is non-nil or the car of the pair 58 is non-nil, the popup will automatically appear for the 59 preselected candidate. Otherwise the popup can be requested 60 manually via `corfu-popupinfo-toggle', 61 `corfu-popupinfo-documentation' and `corfu-popupinfo-location'. 62 63 It is *not recommended* to use a short delay or even 0, since 64 this will create high load for Emacs. Retrieving the 65 documentation from the backend is usually expensive." 66 :type '(choice (const :tag "Never" nil) 67 (number :tag "Delay in seconds") 68 (cons :tag "Two Delays" 69 (choice :tag "Initial " 70 (choice (const nil) number)) 71 (choice :tag "Subsequent" 72 (choice (const nil) number)))) 73 :group 'corfu) 74 75 (defcustom corfu-popupinfo-hide t 76 "Hide the popup during the transition between candidates." 77 :type 'boolean 78 :group 'corfu) 79 80 (defcustom corfu-popupinfo-max-width 80 81 "The maximum width of the info popup in characters." 82 :type 'natnum 83 :group 'corfu) 84 85 (defcustom corfu-popupinfo-min-width 30 86 "The minimum width of the info popup in characters." 87 :type 'natnum 88 :group 'corfu) 89 90 (defcustom corfu-popupinfo-max-height 10 91 "The maximum height of the info popup in characters." 92 :type 'natnum 93 :group 'corfu) 94 95 (defcustom corfu-popupinfo-min-height 1 96 "The minimum height of the info popup in characters." 97 :type 'natnum 98 :group 'corfu) 99 100 (defcustom corfu-popupinfo-resize t 101 "Resize the info popup automatically if non-nil." 102 :type 'boolean 103 :group 'corfu) 104 105 (defcustom corfu-popupinfo-direction '(right left vertical) 106 "Preferred directions for the popup in order." 107 :type '(repeat 108 (choice 109 (const left) 110 (const right) 111 (const vertical) 112 (const force-left) 113 (const force-right) 114 (const force-vertical))) 115 :group 'corfu) 116 117 (defvar-keymap corfu-popupinfo-map 118 :doc "Additional keymap activated in popupinfo mode." 119 "M-t" #'corfu-popupinfo-toggle 120 "<remap> <corfu-info-documentation>" #'corfu-popupinfo-documentation 121 "<remap> <corfu-info-location>" #'corfu-popupinfo-location 122 "<remap> <scroll-other-window>" #'corfu-popupinfo-scroll-up 123 "<remap> <scroll-other-window-down>" #'corfu-popupinfo-scroll-down 124 "<remap> <end-of-buffer-other-window>" #'corfu-popupinfo-end 125 "<remap> <beginning-of-buffer-other-window>" #'corfu-popupinfo-beginning) 126 127 (defvar corfu-popupinfo--buffer-parameters 128 '((truncate-partial-width-windows . nil) 129 (truncate-lines . nil) 130 (left-margin-width . 1) 131 (right-margin-width . 1) 132 (word-wrap . t) 133 (fringe-indicator-alist (continuation)) 134 (char-property-alias-alist (face font-lock-face))) 135 "Buffer parameters.") 136 137 (defvar corfu-popupinfo--frame nil 138 "Info popup child frame.") 139 140 (defvar corfu-popupinfo--timer nil 141 "Corfu info popup auto display timer.") 142 143 (defvar corfu-popupinfo--toggle 'init 144 "Toggle state.") 145 146 (defvar corfu-popupinfo--function 147 #'corfu-popupinfo--get-documentation 148 "Function called to obtain documentation string.") 149 150 (defvar corfu-popupinfo--candidate nil 151 "Completion candidate for the info popup.") 152 153 (defvar corfu-popupinfo--coordinates nil 154 "Coordinates of the candidate popup. 155 The coordinates list has the form (LEFT TOP RIGHT BOTTOM) where 156 all values are in pixels relative to the origin. See 157 `frame-edges' for details.") 158 159 (defvar corfu-popupinfo--lock-dir nil 160 "Locked position direction of the info popup.") 161 162 (defconst corfu-popupinfo--initial-state 163 (mapcar 164 (lambda (k) (cons k (symbol-value k))) 165 '(corfu-popupinfo--candidate 166 corfu-popupinfo--coordinates 167 corfu-popupinfo--lock-dir 168 corfu-popupinfo--toggle 169 corfu-popupinfo--function)) 170 "Initial state of `corfu-popupinfo-mode'.") 171 172 (defun corfu-popupinfo--visible-p (&optional frame) 173 "Return non-nil if FRAME is visible." 174 (setq frame (or frame corfu-popupinfo--frame)) 175 (and (frame-live-p frame) (frame-visible-p frame))) 176 177 (defun corfu-popupinfo--get-location (candidate) 178 "Get source at location of CANDIDATE." 179 (save-excursion 180 (let ((old-buffers (buffer-list)) (buffer nil)) 181 (unwind-protect 182 (when-let 183 ((extra (nth 4 completion-in-region--data)) 184 (fun (plist-get extra :company-location)) 185 ;; BUG: company-location may throw errors if location is not found 186 (loc (ignore-errors (funcall fun candidate))) 187 ((setq buffer 188 (or (and (bufferp (car loc)) (car loc)) 189 (get-file-buffer (car loc)) 190 (let ((inhibit-message t) 191 (message-log-max nil) 192 (inhibit-redisplay t) 193 (enable-dir-local-variables nil) 194 (enable-local-variables :safe) 195 (non-essential t) 196 (delay-mode-hooks t) 197 (find-file-hook '(global-font-lock-mode-check-buffers))) 198 (find-file-noselect (car loc) t)))))) 199 (with-current-buffer buffer 200 (save-excursion 201 (without-restriction 202 (goto-char (point-min)) 203 (when-let ((pos (cdr loc))) 204 (if (bufferp (car loc)) 205 (goto-char pos) 206 (forward-line (1- pos)))) 207 (let ((beg (point))) 208 ;; Support a little bit of scrolling. 209 (forward-line (* 10 corfu-popupinfo-max-height)) 210 (when jit-lock-mode 211 (jit-lock-fontify-now beg (point))) 212 (let ((res (buffer-substring beg (point)))) 213 (and (not (string-blank-p res)) res))))))) 214 (when (and buffer (not (memq buffer old-buffers))) 215 (kill-buffer buffer)))))) 216 217 (defun corfu-popupinfo--get-documentation (candidate) 218 "Get the documentation for CANDIDATE." 219 (when-let ((extra (nth 4 completion-in-region--data)) 220 (fun (plist-get extra :company-doc-buffer)) 221 (res (save-excursion 222 (let ((inhibit-message t) 223 (message-log-max nil) 224 (inhibit-redisplay t) 225 ;; Reduce print length for elisp backend (#249) 226 (print-level 3) 227 (print-length (* corfu-popupinfo-max-width 228 corfu-popupinfo-max-height))) 229 (funcall fun candidate))))) 230 (with-current-buffer (or (car-safe res) res) 231 (setq res (string-trim 232 (replace-regexp-in-string 233 "[\n\t ]*\\[back\\][\n\t ]*" "" 234 (buffer-string)))) 235 (and (not (string-blank-p res)) res)))) 236 237 (defun corfu-popupinfo--size () 238 "Return popup size as pair." 239 (let* ((cw (default-font-width)) 240 (lh (default-line-height)) 241 (margin 242 (* cw (+ (alist-get 'left-margin-width corfu-popupinfo--buffer-parameters) 243 (alist-get 'right-margin-width corfu-popupinfo--buffer-parameters)))) 244 (max-height (* lh corfu-popupinfo-max-height)) 245 (max-width (* cw corfu-popupinfo-max-width))) 246 (or (when corfu-popupinfo-resize 247 (with-current-buffer " *corfu-popupinfo*" 248 (cl-letf* (((window-dedicated-p) nil) 249 ((window-buffer) (current-buffer)) 250 (size (window-text-pixel-size 251 nil (point-min) (point-max) 252 ;; Use 3*max-height as y-limit, to take more text 253 ;; into account. 254 max-width (* 3 max-height)))) 255 ;; Check that width is not exceeded. Otherwise use full height, 256 ;; since lines will get wrapped. 257 (when (<= (car size) max-width) 258 (cons (+ margin (car size)) 259 ;; XXX HACK: Ensure that popup has at least a height of 1, 260 ;; which is the minimum frame height (#261). Maybe we 261 ;; should ask upstream how smaller frames can be created. 262 ;; I only managed to create smaller frames by setting 263 ;; `window-safe-min-height' to 0, which feels problematic. 264 (min (max (cdr size) lh) max-height)))))) 265 (cons (+ margin max-width) max-height)))) 266 267 (defun corfu-popupinfo--frame-geometry (frame) 268 "Return position and size geometric attributes of FRAME. 269 270 The geometry represents the position and size in pixels 271 in the form of (X Y WIDTH HEIGHT)." 272 (pcase-let ((`(,x . ,y) (frame-position frame))) 273 (list x y (frame-pixel-width frame) (frame-pixel-height frame)))) 274 275 (defun corfu-popupinfo--fits-p (size area) 276 "Check if SIZE fits into the AREA. 277 278 SIZE is in the form (WIDTH . HEIGHT). 279 AREA is in the form (X Y WIDTH HEIGHT DIR)." 280 (and (>= (nth 2 area) (car size)) (>= (nth 3 area) (cdr size)))) 281 282 (defun corfu-popupinfo--larger-p (area1 area2) 283 "Check if AREA1 is larger than AREA2. 284 285 AREA1 and AREA2 are both in the form (X Y WIDTH HEIGHT DIR)." 286 (>= (* (nth 2 area1) (nth 3 area1)) (* (nth 2 area2) (nth 3 area2)))) 287 288 (defun corfu-popupinfo--area (ps) 289 "Calculate the display area for the info popup. 290 291 PS is the pixel size of the popup. The calculated area is in the 292 form (X Y WIDTH HEIGHT DIR)." 293 (pcase-let* 294 ((cw (default-font-width)) 295 (lh (default-line-height)) 296 (border (alist-get 'internal-border-width corfu--frame-parameters)) 297 (`(,_pfx ,_pfy ,pfw ,pfh) 298 (corfu-popupinfo--frame-geometry (frame-parent corfu--frame))) 299 (`(,cfx ,cfy ,cfw ,cfh) (corfu-popupinfo--frame-geometry corfu--frame)) 300 ;; Candidates popup below input 301 (below (>= cfy (+ lh (cadr (window-inside-pixel-edges)) 302 (window-tab-line-height) 303 (or (cdr (posn-x-y (posn-at-point (point)))) 0)))) 304 ;; Popups aligned at top 305 (top-aligned (or below (< (cdr ps) cfh))) 306 ;; Left display area 307 (ahy (if top-aligned 308 cfy 309 (max 0 (- (+ cfy cfh) border border (cdr ps))))) 310 (ahh (if top-aligned 311 (min (- pfh cfy) (cdr ps)) 312 (min (- (+ cfy cfh) border border) (cdr ps)))) 313 (al (list (max 0 (- cfx (car ps) border)) ahy 314 (min (- cfx border) (car ps)) ahh 'left)) 315 ;; Right display area 316 (arx (+ cfx cfw (- border))) 317 (ar (list arx ahy (min (- pfw arx border border) (car ps)) ahh 'right)) 318 ;; Vertical display area 319 (avw (min (car ps) (- pfw cfx border border))) 320 (av (if below 321 (list cfx (+ cfy cfh (- border)) avw (min (- pfh cfy cfh border) (cdr ps)) 'vertical) 322 (let ((h (min (- cfy border border) (cdr ps)))) 323 (list cfx (max 0 (- cfy h border)) avw h 'vertical))))) 324 (unless (and corfu-popupinfo--lock-dir 325 (corfu-popupinfo--fits-p 326 (cons (* cw corfu-popupinfo-min-width) (* lh corfu-popupinfo-min-height)) 327 (pcase corfu-popupinfo--lock-dir ('left al) ('right ar) ('vertical av)))) 328 (setq corfu-popupinfo--lock-dir nil)) 329 (or 330 (cl-loop for dir in corfu-popupinfo-direction thereis 331 (pcase dir 332 ((or 'force-right (guard (eq corfu-popupinfo--lock-dir 'right))) ar) 333 ((or 'force-left (guard (eq corfu-popupinfo--lock-dir 'left))) al) 334 ((or 'force-vertical (guard (eq corfu-popupinfo--lock-dir 'vertical))) av) 335 ((and 'right (guard (corfu-popupinfo--fits-p ps ar))) ar) 336 ((and 'left (guard (corfu-popupinfo--fits-p ps al))) al) 337 ((and 'vertical (guard (corfu-popupinfo--fits-p ps av))) av))) 338 (let ((ah (if (corfu-popupinfo--larger-p ar al) ar al))) 339 (if (corfu-popupinfo--larger-p av ah) av ah))))) 340 341 (defun corfu-popupinfo--show (candidate) 342 "Show the info popup for CANDIDATE." 343 (when corfu-popupinfo--timer 344 (cancel-timer corfu-popupinfo--timer) 345 (setq corfu-popupinfo--timer nil)) 346 (when (and (corfu-popupinfo--visible-p corfu--frame)) 347 (let* ((cand-changed 348 (not (and (corfu-popupinfo--visible-p) 349 (corfu--equal-including-properties 350 candidate corfu-popupinfo--candidate)))) 351 (new-coords (frame-edges corfu--frame 'inner-edges)) 352 (coords-changed (not (equal new-coords corfu-popupinfo--coordinates)))) 353 (when cand-changed 354 (if-let ((content (funcall corfu-popupinfo--function candidate))) 355 (with-current-buffer (corfu--make-buffer " *corfu-popupinfo*") 356 (with-silent-modifications 357 (erase-buffer) 358 (insert content) 359 (goto-char (point-min))) 360 (dolist (var corfu-popupinfo--buffer-parameters) 361 (set (make-local-variable (car var)) (cdr var))) 362 (when-let ((m (memq 'corfu-default (alist-get 'default face-remapping-alist)))) 363 (setcar m 'corfu-popupinfo))) 364 (corfu-popupinfo--hide) 365 (setq cand-changed nil coords-changed nil))) 366 (when (or cand-changed coords-changed) 367 (pcase-let* ((border (alist-get 'internal-border-width corfu--frame-parameters)) 368 (`(,area-x ,area-y ,area-w ,area-h ,area-d) 369 (corfu-popupinfo--area 370 (if cand-changed 371 (corfu-popupinfo--size) 372 (cons 373 (- (frame-pixel-width corfu-popupinfo--frame) border border) 374 (- (frame-pixel-height corfu-popupinfo--frame) border border))))) 375 (margin-quirk (not corfu-popupinfo--frame))) 376 (setq corfu-popupinfo--frame 377 (corfu--make-frame corfu-popupinfo--frame 378 area-x area-y area-w area-h 379 " *corfu-popupinfo*") 380 corfu-popupinfo--toggle t 381 corfu-popupinfo--lock-dir area-d 382 corfu-popupinfo--candidate candidate 383 corfu-popupinfo--coordinates new-coords) 384 ;; XXX HACK: Force margin update. For some reason, the call to 385 ;; `set-window-buffer' in `corfu--make-frame' is not effective the 386 ;; first time. Why does Emacs have all these quirks? 387 (when margin-quirk 388 (set-window-buffer 389 (frame-root-window corfu-popupinfo--frame) 390 " *corfu-popupinfo*"))))))) 391 392 (defun corfu-popupinfo--hide () 393 "Clear the info popup buffer content and hide it." 394 (corfu--hide-frame corfu-popupinfo--frame)) 395 396 (defun corfu-popupinfo-end (&optional n) 397 "Scroll text of info popup window to its end. 398 399 If arg N is omitted or nil, scroll to end. If a numerical value, 400 put point N/10 of the way from the end. If the info popup is not 401 visible, the other window is moved to beginning or end." 402 (interactive "P") 403 (if (corfu-popupinfo--visible-p) 404 (with-selected-frame corfu-popupinfo--frame 405 (with-current-buffer " *corfu-popupinfo*" 406 (with-no-warnings 407 (end-of-buffer n)))) 408 (end-of-buffer-other-window n))) 409 410 (defun corfu-popupinfo-beginning (&optional n) 411 "Scroll text of info popup window to beginning of buffer. 412 413 See `corfu-popupinfo-end' for the argument N." 414 (interactive "P") 415 (corfu-popupinfo-end (- 10 (if (numberp n) n 0)))) 416 417 (defun corfu-popupinfo-scroll-up (&optional n) 418 "Scroll text of info popup window upward N lines. 419 420 If ARG is omitted or nil, scroll upward by a near full screen. 421 See `scroll-up' for details. If the info popup is not visible, 422 the other window is scrolled." 423 (interactive "p") 424 (if (corfu-popupinfo--visible-p) 425 (with-selected-frame corfu-popupinfo--frame 426 (with-current-buffer " *corfu-popupinfo*" 427 (scroll-up n))) 428 (scroll-other-window n))) 429 430 (defun corfu-popupinfo-scroll-down (&optional n) 431 "Scroll text of info popup window down N lines. 432 433 See `corfu-popupinfo-scroll-up' for more details." 434 (interactive "p") 435 (corfu-popupinfo-scroll-up (- (or n 1)))) 436 437 (defun corfu-popupinfo--toggle (fun) 438 "Set documentation getter FUN and toggle popup." 439 (when (< corfu--index 0) 440 (corfu-popupinfo--hide) 441 (user-error "No candidate selected")) 442 (setq corfu-popupinfo--toggle 443 (not (and (corfu-popupinfo--visible-p) 444 (eq corfu-popupinfo--function fun)))) 445 (if (not corfu-popupinfo--toggle) 446 (corfu-popupinfo--hide) 447 (setq corfu-popupinfo--function fun 448 corfu-popupinfo--candidate nil) 449 (let ((cand (nth corfu--index corfu--candidates))) 450 (corfu-popupinfo--show cand) 451 (unless (corfu-popupinfo--visible-p) 452 (user-error "No %s available for `%s'" 453 (car (last (split-string (symbol-name fun) "-+"))) 454 cand))))) 455 456 (defun corfu-popupinfo-documentation () 457 "Show or hide documentation in popup. 458 Behaves like `corfu-popupinfo-toggle'." 459 (interactive) 460 (corfu-popupinfo--toggle #'corfu-popupinfo--get-documentation)) 461 462 (defun corfu-popupinfo-location () 463 "Show or hide location in popup. 464 Behaves like `corfu-popupinfo-toggle'." 465 (interactive) 466 (corfu-popupinfo--toggle #'corfu-popupinfo--get-location)) 467 468 (defun corfu-popupinfo-toggle () 469 "Toggle the info popup display or hide. 470 471 When using this command to manually hide the info popup, it will 472 not be displayed until this command is called again, even if 473 `corfu-popupinfo-delay' is non-nil." 474 (interactive) 475 (corfu-popupinfo--toggle corfu-popupinfo--function)) 476 477 ;;;###autoload 478 (define-minor-mode corfu-popupinfo-mode 479 "Corfu info popup minor mode." 480 :global t :group 'corfu) 481 482 (cl-defmethod corfu--exhibit :after (&context (corfu-popupinfo-mode (eql t)) &optional _auto) 483 (when completion-in-region-mode 484 (setf (alist-get #'corfu-popupinfo-mode minor-mode-overriding-map-alist) 485 corfu-popupinfo-map) 486 (when corfu-popupinfo--timer 487 (cancel-timer corfu-popupinfo--timer) 488 (setq corfu-popupinfo--timer nil)) 489 (if (and (>= corfu--index 0) (corfu-popupinfo--visible-p corfu--frame)) 490 (let ((cand (nth corfu--index corfu--candidates))) 491 (if-let ((delay (if (consp corfu-popupinfo-delay) 492 (funcall (if (eq corfu-popupinfo--toggle 'init) #'car #'cdr) 493 corfu-popupinfo-delay) 494 corfu-popupinfo-delay)) 495 (corfu-popupinfo--toggle)) 496 (if (or (<= delay 0) 497 (and (equal cand corfu-popupinfo--candidate) 498 (corfu-popupinfo--visible-p))) 499 (corfu-popupinfo--show cand) 500 (when (corfu-popupinfo--visible-p) 501 (cond 502 (corfu-popupinfo-hide 503 (corfu-popupinfo--hide)) 504 (corfu-popupinfo--candidate 505 (corfu-popupinfo--show corfu-popupinfo--candidate)))) 506 (setq corfu-popupinfo--timer 507 (run-at-time delay nil #'corfu-popupinfo--show cand))) 508 (unless (equal cand corfu-popupinfo--candidate) 509 (corfu-popupinfo--hide)))) 510 (corfu-popupinfo--hide)))) 511 512 (cl-defmethod corfu--teardown :before (_buf &context (corfu-popupinfo-mode (eql t))) 513 (corfu-popupinfo--hide) 514 (cl-loop for (k . v) in corfu-popupinfo--initial-state do (set k v)) 515 (cl-callf2 assq-delete-all #'corfu-popupinfo-mode minor-mode-overriding-map-alist)) 516 517 ;; Do not show Corfu commands with M-X 518 (dolist (sym '(corfu-popupinfo-scroll-down corfu-popupinfo-scroll-up 519 corfu-popupinfo-documentation corfu-popupinfo-location 520 corfu-popupinfo-beginning corfu-popupinfo-end 521 corfu-popupinfo-toggle)) 522 (put sym 'completion-predicate #'ignore)) 523 524 (provide 'corfu-popupinfo) 525 ;;; corfu-popupinfo.el ends here