corfu-terminal-popupinfo.el 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
  1. ;;; corfu-terminal-popupinfo.el --- corfu-popupinfo support in the terminal -*- lexical-binding: t -*-
  2. ;;; Commentary:
  3. ;; To make use of this file, simply `require' it, and then enable
  4. ;; `corfu-terminal-popupinfo-mode', which is a global mode. Note that
  5. ;; `corfu-terminal-mode' MUST be loaded and enabled for this to work.
  6. ;;; Code:
  7. (require 'popon)
  8. (require 'corfu-terminal)
  9. (require 'corfu-popupinfo)
  10. (require 'cl-lib)
  11. (defvar ctp--popon nil
  12. "The current popon, or nil if there is none.")
  13. (defvar ctp--buffer nil
  14. "The buffer holding the current candidate's documentation.")
  15. (defun ctp--get-buffer ()
  16. "Create or return `ctp--buffer'."
  17. (unless (and (bufferp ctp--buffer) (buffer-live-p ctp--buffer))
  18. (setq ctp--buffer (generate-new-buffer " *corfu-terminal-popupinfo*" t)))
  19. ctp--buffer)
  20. (defun ctp--visible-p ()
  21. "Return non-nil if the terminal popup window is visible."
  22. (popon-live-p ctp--popon))
  23. (defun ctp--corfu-popupinfo--visible-p-advice (oldfun &optional frame)
  24. "Advice for `corfu-popupinfo--visible-p'.
  25. If FRAME is nil, this will return `ctp--visible-p'. If
  26. FRAME is `corfu--frame', this will return weather the `corfu-terminal--popon' is
  27. live or not.
  28. As this is :around advice, OLDFUN is the real (advised) function to call."
  29. (cond
  30. ((and (not frame) (ctp--visible-p)))
  31. ((and (eq frame corfu--frame) (popon-live-p corfu-terminal--popon)))
  32. ((funcall oldfun frame))))
  33. (defun ctp--close ()
  34. "Close the popon."
  35. (popon-kill ctp--popon)
  36. (setq ctp--popon nil))
  37. (defalias 'ctp--corfu-popupinfo--hide-advice 'ctp--close
  38. "Advice for `corfu-popupinfo--hide' that works in the terminal.")
  39. (defun ctp--load-content (candidate buffer)
  40. "Load the documentation for CANDIDATE into BUFFER."
  41. (when-let ((content (funcall corfu-popupinfo--function candidate)))
  42. ;; A bunch of this comes straight from `corfu-popupinfo--show'
  43. (with-current-buffer buffer
  44. (dolist (var corfu-popupinfo--buffer-parameters)
  45. (set (make-local-variable (car var)) (cdr var)))
  46. (with-silent-modifications
  47. (erase-buffer)
  48. (insert content)
  49. ;; popon.el requires that each line be of the same width. As we are in
  50. ;; the terminal, we assume that each character is the same width (and
  51. ;; we can't do anything, or even know, if this is not the case). Thus,
  52. ;; we run over the buffer to pad out each line to the width of the
  53. ;; longest line.
  54. (goto-char (point-min))
  55. (let ((wrap-p (and (not truncate-lines) word-wrap))
  56. (longest-line 0))
  57. (cl-block nil
  58. (while (not (eobp))
  59. (let ((len (- (pos-eol) (pos-bol))))
  60. (when (> len longest-line)
  61. (setq longest-line len))
  62. (when (and wrap-p (> longest-line corfu-popupinfo-max-width))
  63. (setq longest-line corfu-popupinfo-max-width)
  64. (cl-return)))
  65. (forward-line)))
  66. (setq-local fill-column longest-line)
  67. (when wrap-p
  68. (fill-region (point-min) (point-max)))
  69. (goto-char (point-min))
  70. (while (not (eobp))
  71. (end-of-line)
  72. (let ((len (- (point) (pos-bol))))
  73. (when (< len longest-line)
  74. (insert (make-string (- longest-line len) ? ))))
  75. (forward-line))))
  76. (goto-char (point-min))
  77. (put-text-property (point-min) (point-max) 'face 'corfu-popupinfo)
  78. (when-let ((m (memq 'corfu-default (alist-get 'default face-remapping-alist))))
  79. (setcar m 'corfu-popupinfo)))
  80. ;; We succeeded in loading the data
  81. t))
  82. (defun ctp--popon-position (buffer)
  83. "Find a good position to open the popon for BUFFER's content.
  84. Return a list of the position, the max line length that can be shown, and the
  85. max height that can be shown. Each line of BUFFER _MUST_ be the same lenght."
  86. (when-let ((point-posn (posn-at-point))
  87. (point-x (car (posn-x-y point-posn)))
  88. (point-y (cdr (posn-x-y point-posn))))
  89. (with-current-buffer buffer
  90. (when-let ((completion-pos (popon-position corfu-terminal--popon))
  91. (completion-size (popon-size corfu-terminal--popon))
  92. (comp-x (car completion-pos))
  93. (comp-y (cdr completion-pos))
  94. (comp-w (car completion-size))
  95. (comp-h (cdr completion-size))
  96. (win-w (window-max-chars-per-line))
  97. (win-h (window-body-height))
  98. (line-len (- (pos-eol) (pos-bol)))
  99. (num-lines (count-lines (point-min) (point-max))))
  100. (let* ((align 'row)
  101. (width (min line-len corfu-popupinfo-max-width))
  102. (pop-x (cond
  103. ((<= (+ comp-x comp-w width) win-w)
  104. (+ comp-x comp-w))
  105. ((>= (- comp-x width) 0)
  106. (- comp-x width))
  107. ((<= (+ comp-x width) win-w)
  108. (setq align 'col)
  109. comp-x)
  110. ((>= (- win-w width) 0)
  111. (setq align 'col)
  112. (- win-w width))
  113. (t
  114. (setq align 'col
  115. width win-w)
  116. 0)))
  117. (height (min num-lines corfu-popupinfo-max-height))
  118. (pop-y (cl-case align
  119. (row (if (<= (+ comp-y height) win-h)
  120. comp-y
  121. (max 0 (- win-h height))))
  122. (col (cond
  123. ((<= (+ comp-y comp-h height)
  124. (- win-h scroll-margin))
  125. (+ comp-y comp-h))
  126. ;; If the completion dialog is above the point
  127. ((and (< comp-y point-y)
  128. (>= (- comp-y height) 0))
  129. (- comp-y height))
  130. ;; Emacs seems to hide the current text if this
  131. ;; number is 1 (I think it's too close to two
  132. ;; overlays)
  133. ((>= (- comp-y height 2) 0)
  134. (- comp-y height 2))
  135. (t (+ comp-y comp-h)))))))
  136. (list (cons pop-x pop-y) width height))))))
  137. (defun ctp--extract-content (buffer width height)
  138. "Extract the content from BUFFER for a popon.
  139. The content extracted is for a popon of size WIDTH by HEIGHT."
  140. (let (start end)
  141. (with-current-buffer buffer
  142. ;; we assume that we are scrolled to the start of the region we care about
  143. (save-excursion
  144. (let ((rem-lines (count-lines (point) (point-max))))
  145. (when (< rem-lines height)
  146. (forward-line (- rem-lines height))))
  147. (setq start (point)
  148. end (pos-eol height))))
  149. (with-temp-buffer
  150. (insert-buffer-substring buffer start end)
  151. (goto-char (point-min))
  152. (cl-loop repeat height
  153. until (eobp) do
  154. (let ((len (- (pos-eol) (pos-bol))))
  155. (when (> len width)
  156. (delete-region (+ (pos-bol) width) (pos-eol))))
  157. (forward-line))
  158. ;; "delete" the rest of the lines
  159. (narrow-to-region (point-min) (point))
  160. (buffer-string))))
  161. (defun ctp--display-buffer (buffer)
  162. "Display or redisplay BUFFER in a popon."
  163. (let ((inhibit-redisplay t))
  164. (cl-destructuring-bind (&optional pos width height)
  165. (ctp--popon-position buffer)
  166. (popon-kill ctp--popon)
  167. (when-let ((pos)
  168. (content (ctp--extract-content buffer width height)))
  169. (setq ctp--popon
  170. ;; appear behind the auto-complete window, in case something
  171. ;; happens
  172. (popon-create content pos nil nil 100))))))
  173. (defun ctp--corfu-popupinfo--show-advice (oldfun candidate)
  174. "Advice for `corfu-popupinfo--show' that works in the terminal.
  175. CANDIDATE is the same as for `corfu-popupinfo--show'. As this is meant to be
  176. :around advice, OLDFUN is assumed to be the real (advised) function."
  177. (if (display-graphic-p)
  178. (progn
  179. (popon-kill ctp--popon)
  180. (funcall oldfun candidate))
  181. (when corfu-popupinfo--timer
  182. (cancel-timer corfu-popupinfo--timer)
  183. (setq corfu-popupinfo--timer nil))
  184. (when (and (frame-live-p corfu-popupinfo--frame)
  185. (frame-visible-p corfu-popupinfo--frame))
  186. (corfu--hide-frame corfu-popupinfo--frame))
  187. (when (or (not (ctp--visible-p))
  188. (not (corfu--equal-including-properties
  189. candidate corfu-popupinfo--candidate)))
  190. (let ((buf (ctp--get-buffer)))
  191. (if (ctp--load-content candidate buf)
  192. (progn
  193. (ctp--display-buffer buf)
  194. (setq corfu-popupinfo--candidate candidate
  195. corfu-popupinfo--toggle t))
  196. (corfu-popupinfo--hide))))))
  197. (defun ctp--move-away-from-eob ()
  198. "Ensure the point isn't too close to the end of the buffer."
  199. (if-let ((total-lines (count-lines (point-min) (point-max)))
  200. ((> total-lines corfu-popupinfo-max-height))
  201. (rem-lines (count-lines (point) (point-max)))
  202. ((< rem-lines corfu-popupinfo-max-height)))
  203. (forward-line (- (- corfu-popupinfo-max-height rem-lines)))))
  204. (defun ctp--corfu-popupinfo-scroll-up-advice
  205. (oldfun &optional n)
  206. "Advice for `corfu-popupinfo-scroll-up'.
  207. N is the number of lines. As this is :around advice, OLDFUN is the real
  208. \(advised) function."
  209. (if (ctp--visible-p)
  210. (let ((buf (ctp--get-buffer)))
  211. (with-current-buffer buf
  212. (forward-line n)
  213. (beginning-of-line)
  214. (ctp--move-away-from-eob))
  215. (ctp--display-buffer buf))
  216. (funcall oldfun n)))
  217. (defun ctp--corfu-popupinfo-end-advice (oldfun &optional n)
  218. "Advice for `corfu-popupinfo-end'.
  219. N is the same as for `corfu-popupinfo-end'. As this is :around advice, OLDFUN
  220. is the real (advised) function."
  221. (if (ctp--visible-p)
  222. (let ((buf (ctp--get-buffer)))
  223. (with-current-buffer buf
  224. (let ((size (- (point-max) (point-min))))
  225. (goto-char (if n
  226. (- (point-max) (/ (* size n) 10))
  227. (point-max))))
  228. (beginning-of-line)
  229. (ctp--move-away-from-eob))
  230. (ctp--display-buffer buf))
  231. (funcall oldfun n)))
  232. (defun ctp--corfu--popup-hide-advice ()
  233. ":after advice for `corfu--popup-hide'."
  234. (unless completion-in-region-mode
  235. (ctp--close)))
  236. (defun ctp--enable ()
  237. "Enable corfu terminal popupinfo by advising some corfu functions."
  238. (advice-add 'corfu-popupinfo--visible-p :around
  239. 'ctp--corfu-popupinfo--visible-p-advice)
  240. (advice-add 'corfu-popupinfo--hide :after
  241. 'ctp--corfu-popupinfo--hide-advice)
  242. (advice-add 'corfu-popupinfo--show :around
  243. 'ctp--corfu-popupinfo--show-advice)
  244. (advice-add 'corfu-popupinfo-scroll-up :around
  245. 'ctp--corfu-popupinfo-scroll-up-advice)
  246. (advice-add 'corfu-popupinfo-end :around
  247. 'ctp--corfu-popupinfo-end-advice)
  248. (advice-add 'corfu--popup-hide :after
  249. 'ctp--corfu--popup-hide-advice))
  250. (defun ctp--disable ()
  251. "Disable corfu terminal popupinfo by remove advice added by `ctp--enable'."
  252. (ctp--close)
  253. (advice-remove 'corfu-popupinfo--visible-p
  254. 'ctp--corfu-popupinfo--visible-p-advice)
  255. (advice-remove 'corfu-popupinfo--hide
  256. 'ctp--corfu-popupinfo--hide-advice)
  257. (advice-remove 'corfu-popupinfo--show
  258. 'ctp--corfu-popupinfo--show-advice)
  259. (advice-remove 'corfu-popupinfo-scroll-up
  260. 'ctp--corfu-popupinfo-scroll-up-advice)
  261. (advice-remove 'corfu-popupinfo-end
  262. 'ctp--corfu-popupinfo-end-advice)
  263. (advice-remove 'corfu--popup-hide
  264. 'ctp--corfu--popup-hide-advice))
  265. (defun ctp--corfu-terminal-mode-hook ()
  266. "Hook run from `corfu-terminal-mode-hook'."
  267. (if (and corfu-terminal-mode
  268. (bound-and-true-p corfu-terminal-popupinfo-mode))
  269. (ctp--enable)
  270. (ctp--disable)))
  271. ;;;###autoload
  272. (define-minor-mode corfu-terminal-popupinfo-mode
  273. "Minor mode shows the `corfu-popupinfo-mode' popup in the terminal.
  274. Note that even with this enabled, you still need to enable the actual popup
  275. using `corfu-popupinfo-toggle'. Also, this does not do anything if
  276. `corfu-terminal-mode' is not enabled."
  277. :global t
  278. :group 'corfu-terminal-popupinfo
  279. (if corfu-terminal-popupinfo-mode
  280. (progn
  281. (add-hook 'corfu-terminal-mode-hook 'ctp--corfu-terminal-mode-hook)
  282. (when corfu-terminal-mode
  283. (ctp--enable)))
  284. (remove-hook 'corfu-terminal-mode-hook 'ctp--corfu-terminal-mode-hook)
  285. (ctp--disable)))
  286. (provide 'corfu-terminal-popupinfo)
  287. ;;; corfu-terminal-popupinfo.el ends here