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