123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312 |
- ;;; corfu-terminal-popupinfo.el --- corfu-popupinfo support in the terminal -*- lexical-binding: t -*-
- ;;; Commentary:
- ;; To make use of this file, simply `require' it, and then enable
- ;; `corfu-terminal-popupinfo-mode', which is a global mode. Note that
- ;; `corfu-terminal-mode' MUST be loaded and enabled for this to work.
- ;;; Code:
- (require 'popon)
- (require 'corfu-terminal)
- (require 'corfu-popupinfo)
- (require 'cl-lib)
- (defvar ctp--popon nil
- "The current popon, or nil if there is none.")
- (defvar ctp--buffer nil
- "The buffer holding the current candidate's documentation.")
- (defun ctp--get-buffer ()
- "Create or return `ctp--buffer'."
- (unless (and (bufferp ctp--buffer) (buffer-live-p ctp--buffer))
- (setq ctp--buffer (generate-new-buffer " *corfu-terminal-popupinfo*" t)))
- ctp--buffer)
- (defun ctp--visible-p ()
- "Return non-nil if the terminal popup window is visible."
- (popon-live-p ctp--popon))
- (defun ctp--corfu-popupinfo--visible-p-advice (oldfun &optional frame)
- "Advice for `corfu-popupinfo--visible-p'.
- If FRAME is nil, this will return `ctp--visible-p'. If
- FRAME is `corfu--frame', this will return weather the `corfu-terminal--popon' is
- live or not.
- As this is :around advice, OLDFUN is the real (advised) function to call."
- (cond
- ((and (not frame) (ctp--visible-p)))
- ((and (eq frame corfu--frame) (popon-live-p corfu-terminal--popon)))
- ((funcall oldfun frame))))
- (defun ctp--close ()
- "Close the popon."
- (popon-kill ctp--popon)
- (setq ctp--popon nil))
- (defalias 'ctp--corfu-popupinfo--hide-advice 'ctp--close
- "Advice for `corfu-popupinfo--hide' that works in the terminal.")
- (defun ctp--load-content (candidate buffer)
- "Load the documentation for CANDIDATE into BUFFER."
- (when-let ((content (funcall corfu-popupinfo--function candidate)))
- ;; A bunch of this comes straight from `corfu-popupinfo--show'
- (with-current-buffer buffer
- (dolist (var corfu-popupinfo--buffer-parameters)
- (set (make-local-variable (car var)) (cdr var)))
- (with-silent-modifications
- (erase-buffer)
- (insert content)
- ;; popon.el requires that each line be of the same width. As we are in
- ;; the terminal, we assume that each character is the same width (and
- ;; we can't do anything, or even know, if this is not the case). Thus,
- ;; we run over the buffer to pad out each line to the width of the
- ;; longest line.
- (goto-char (point-min))
- (let ((wrap-p (and (not truncate-lines) word-wrap))
- (longest-line 0))
- (cl-block nil
- (while (not (eobp))
- (let ((len (- (pos-eol) (pos-bol))))
- (when (> len longest-line)
- (setq longest-line len))
- (when (and wrap-p (> longest-line corfu-popupinfo-max-width))
- (setq longest-line corfu-popupinfo-max-width)
- (cl-return)))
- (forward-line)))
- (setq-local fill-column longest-line)
- (when wrap-p
- (fill-region (point-min) (point-max)))
- (goto-char (point-min))
- (while (not (eobp))
- (end-of-line)
- (let ((len (- (point) (pos-bol))))
- (when (< len longest-line)
- (insert (make-string (- longest-line len) ? ))))
- (forward-line))))
- (goto-char (point-min))
- (put-text-property (point-min) (point-max) 'face 'corfu-popupinfo)
- (when-let ((m (memq 'corfu-default (alist-get 'default face-remapping-alist))))
- (setcar m 'corfu-popupinfo)))
- ;; We succeeded in loading the data
- t))
- (defun ctp--popon-position (buffer)
- "Find a good position to open the popon for BUFFER's content.
- Return a list of the position, the max line length that can be shown, and the
- max height that can be shown. Each line of BUFFER _MUST_ be the same lenght."
- (when-let ((point-posn (posn-at-point))
- (point-x (car (posn-x-y point-posn)))
- (point-y (cdr (posn-x-y point-posn))))
- (with-current-buffer buffer
- (when-let ((completion-pos (popon-position corfu-terminal--popon))
- (completion-size (popon-size corfu-terminal--popon))
- (comp-x (car completion-pos))
- (comp-y (cdr completion-pos))
- (comp-w (car completion-size))
- (comp-h (cdr completion-size))
- (win-w (window-max-chars-per-line))
- (win-h (window-body-height))
- (line-len (- (pos-eol) (pos-bol)))
- (num-lines (count-lines (point-min) (point-max))))
- (let* ((align 'row)
- (width (min line-len corfu-popupinfo-max-width))
- (pop-x (cond
- ((<= (+ comp-x comp-w width) win-w)
- (+ comp-x comp-w))
- ((>= (- comp-x width) 0)
- (- comp-x width))
- ((<= (+ comp-x width) win-w)
- (setq align 'col)
- comp-x)
- ((>= (- win-w width) 0)
- (setq align 'col)
- (- win-w width))
- (t
- (setq align 'col
- width win-w)
- 0)))
- (height (min num-lines corfu-popupinfo-max-height))
- (pop-y (cl-case align
- (row (if (<= (+ comp-y height) win-h)
- comp-y
- (max 0 (- win-h height))))
- (col (cond
- ((<= (+ comp-y comp-h height)
- (- win-h scroll-margin))
- (+ comp-y comp-h))
- ;; If the completion dialog is above the point
- ((and (< comp-y point-y)
- (>= (- comp-y height) 0))
- (- comp-y height))
- ;; Emacs seems to hide the current text if this
- ;; number is 1 (I think it's too close to two
- ;; overlays)
- ((>= (- comp-y height 2) 0)
- (- comp-y height 2))
- (t (+ comp-y comp-h)))))))
- (list (cons pop-x pop-y) width height))))))
- (defun ctp--extract-content (buffer width height)
- "Extract the content from BUFFER for a popon.
- The content extracted is for a popon of size WIDTH by HEIGHT."
- (let (start end)
- (with-current-buffer buffer
- ;; we assume that we are scrolled to the start of the region we care about
- (save-excursion
- (let ((rem-lines (count-lines (point) (point-max))))
- (when (< rem-lines height)
- (forward-line (- rem-lines height))))
- (setq start (point)
- end (pos-eol height))))
- (with-temp-buffer
- (insert-buffer-substring buffer start end)
- (goto-char (point-min))
- (cl-loop repeat height
- until (eobp) do
- (let ((len (- (pos-eol) (pos-bol))))
- (when (> len width)
- (delete-region (+ (pos-bol) width) (pos-eol))))
- (forward-line))
- ;; "delete" the rest of the lines
- (narrow-to-region (point-min) (point))
- (buffer-string))))
- (defun ctp--display-buffer (buffer)
- "Display or redisplay BUFFER in a popon."
- (let ((inhibit-redisplay t))
- (cl-destructuring-bind (&optional pos width height)
- (ctp--popon-position buffer)
- (popon-kill ctp--popon)
- (when-let ((pos)
- (content (ctp--extract-content buffer width height)))
- (setq ctp--popon
- ;; appear behind the auto-complete window, in case something
- ;; happens
- (popon-create content pos nil nil 100))))))
- (defun ctp--corfu-popupinfo--show-advice (oldfun candidate)
- "Advice for `corfu-popupinfo--show' that works in the terminal.
- CANDIDATE is the same as for `corfu-popupinfo--show'. As this is meant to be
- :around advice, OLDFUN is assumed to be the real (advised) function."
- (if (display-graphic-p)
- (progn
- (popon-kill ctp--popon)
- (funcall oldfun candidate))
- (when corfu-popupinfo--timer
- (cancel-timer corfu-popupinfo--timer)
- (setq corfu-popupinfo--timer nil))
- (when (and (frame-live-p corfu-popupinfo--frame)
- (frame-visible-p corfu-popupinfo--frame))
- (corfu--hide-frame corfu-popupinfo--frame))
- (when (or (not (ctp--visible-p))
- (not (corfu--equal-including-properties
- candidate corfu-popupinfo--candidate)))
- (let ((buf (ctp--get-buffer)))
- (if (ctp--load-content candidate buf)
- (progn
- (ctp--display-buffer buf)
- (setq corfu-popupinfo--candidate candidate
- corfu-popupinfo--toggle t))
- (corfu-popupinfo--hide))))))
- (defun ctp--move-away-from-eob ()
- "Ensure the point isn't too close to the end of the buffer."
- (if-let ((total-lines (count-lines (point-min) (point-max)))
- ((> total-lines corfu-popupinfo-max-height))
- (rem-lines (count-lines (point) (point-max)))
- ((< rem-lines corfu-popupinfo-max-height)))
- (forward-line (- (- corfu-popupinfo-max-height rem-lines)))))
- (defun ctp--corfu-popupinfo-scroll-up-advice
- (oldfun &optional n)
- "Advice for `corfu-popupinfo-scroll-up'.
- N is the number of lines. As this is :around advice, OLDFUN is the real
- \(advised) function."
- (if (ctp--visible-p)
- (let ((buf (ctp--get-buffer)))
- (with-current-buffer buf
- (forward-line n)
- (beginning-of-line)
- (ctp--move-away-from-eob))
- (ctp--display-buffer buf))
- (funcall oldfun n)))
- (defun ctp--corfu-popupinfo-end-advice (oldfun &optional n)
- "Advice for `corfu-popupinfo-end'.
- N is the same as for `corfu-popupinfo-end'. As this is :around advice, OLDFUN
- is the real (advised) function."
- (if (ctp--visible-p)
- (let ((buf (ctp--get-buffer)))
- (with-current-buffer buf
- (let ((size (- (point-max) (point-min))))
- (goto-char (if n
- (- (point-max) (/ (* size n) 10))
- (point-max))))
- (beginning-of-line)
- (ctp--move-away-from-eob))
- (ctp--display-buffer buf))
- (funcall oldfun n)))
- (defun ctp--corfu--popup-hide-advice ()
- ":after advice for `corfu--popup-hide'."
- (unless completion-in-region-mode
- (ctp--close)))
- (defun ctp--enable ()
- "Enable corfu terminal popupinfo by advising some corfu functions."
- (advice-add 'corfu-popupinfo--visible-p :around
- 'ctp--corfu-popupinfo--visible-p-advice)
- (advice-add 'corfu-popupinfo--hide :after
- 'ctp--corfu-popupinfo--hide-advice)
- (advice-add 'corfu-popupinfo--show :around
- 'ctp--corfu-popupinfo--show-advice)
- (advice-add 'corfu-popupinfo-scroll-up :around
- 'ctp--corfu-popupinfo-scroll-up-advice)
- (advice-add 'corfu-popupinfo-end :around
- 'ctp--corfu-popupinfo-end-advice)
- (advice-add 'corfu--popup-hide :after
- 'ctp--corfu--popup-hide-advice))
- (defun ctp--disable ()
- "Disable corfu terminal popupinfo by remove advice added by `ctp--enable'."
- (ctp--close)
- (advice-remove 'corfu-popupinfo--visible-p
- 'ctp--corfu-popupinfo--visible-p-advice)
- (advice-remove 'corfu-popupinfo--hide
- 'ctp--corfu-popupinfo--hide-advice)
- (advice-remove 'corfu-popupinfo--show
- 'ctp--corfu-popupinfo--show-advice)
- (advice-remove 'corfu-popupinfo-scroll-up
- 'ctp--corfu-popupinfo-scroll-up-advice)
- (advice-remove 'corfu-popupinfo-end
- 'ctp--corfu-popupinfo-end-advice)
- (advice-remove 'corfu--popup-hide
- 'ctp--corfu--popup-hide-advice))
- (defun ctp--corfu-terminal-mode-hook ()
- "Hook run from `corfu-terminal-mode-hook'."
- (if (and corfu-terminal-mode
- (bound-and-true-p corfu-terminal-popupinfo-mode))
- (ctp--enable)
- (ctp--disable)))
- ;;;###autoload
- (define-minor-mode corfu-terminal-popupinfo-mode
- "Minor mode shows the `corfu-popupinfo-mode' popup in the terminal.
- Note that even with this enabled, you still need to enable the actual popup
- using `corfu-popupinfo-toggle'. Also, this does not do anything if
- `corfu-terminal-mode' is not enabled."
- :global t
- :group 'corfu-terminal-popupinfo
- (if corfu-terminal-popupinfo-mode
- (progn
- (add-hook 'corfu-terminal-mode-hook 'ctp--corfu-terminal-mode-hook)
- (when corfu-terminal-mode
- (ctp--enable)))
- (remove-hook 'corfu-terminal-mode-hook 'ctp--corfu-terminal-mode-hook)
- (ctp--disable)))
- (provide 'corfu-terminal-popupinfo)
- ;;; corfu-terminal-popupinfo.el ends here
|