312 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			EmacsLisp
		
	
	
	
	
	
			
		
		
	
	
			312 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			EmacsLisp
		
	
	
	
	
	
;;; 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
 |