;;; company-quickhelp.el --- Popup documentation for completion candidates ;; Copyright (C) 2016, Lars Andersen ;; Author: Lars Andersen ;; URL: https://www.github.com/expez/company-quickhelp ;; Package-Version: 20221212.534 ;; Package-Commit: 9505fb09d064581da142d75c139d48b5cf695bd5 ;; Keywords: company popup documentation quickhelp ;; Version: 2.2.0 ;; Package-Requires: ((emacs "24.3") (company "0.8.9") (pos-tip "0.4.6")) ;; This file is not part of GNU Emacs. ;; This program is free software: you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or ;; (at your option) any later version. ;; This program is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; You should have received a copy of the GNU General Public License ;; along with this program. If not, see . ;;; Commentary: ;; When idling on a completion candidate the documentation for the ;; candidate will pop up after `company-quickhelp-delay' seconds. ;;; Usage: ;; put (company-quickhelp-mode) in your init.el to activate ;; `company-quickhelp-mode'. ;; You can adjust the time it takes for the documentation to pop up by ;; changing `company-quickhelp-delay' ;;; Code: (require 'company) (require 'pos-tip) (require 'cl-lib) (defgroup company-quickhelp nil "Documentation popups for `company-mode'" :group 'company) (defcustom company-quickhelp-use-propertized-text t "Allow the text to have properties like color, font size, etc." :type '(choice (boolean :tag "Allow")) :group 'company-quickhelp) (defcustom company-quickhelp-delay 0.5 "Delay, in seconds, before the quickhelp popup appears. If set to nil the popup won't automatically appear, but can still be triggered manually using `company-quickhelp-show'." :type '(choice (number :tag "Delay in seconds") (const :tag "Don't popup help automatically" nil)) :group 'company-quickhelp) (defcustom company-quickhelp-max-lines nil "When not NIL, limits the number of lines in the popup." :type '(choice (integer :tag "Max lines to show in popup") (const :tag "Don't limit the number of lines shown" nil)) :group 'company-quickhelp) (defcustom company-quickhelp-color-foreground nil "Popup text foreground color." :type '(choice (color) (const :tag "Default" nil)) :group 'company-quickhelp) (defcustom company-quickhelp-color-background nil "Popup text background color." :type '(choice (color) (const :tag "Default" nil)) :group 'company-quickhelp) (defvar-local company-quickhelp--timer nil "Quickhelp idle timer.") (defvar-local company-quickhelp--original-tooltip-width company-tooltip-minimum-width "The documentation popup breaks inexplicably when we transition from a large pseudo-tooltip to a small one. We solve this by overriding `company-tooltip-minimum-width' and save the original value here so we can restore it.") (defun company-quickhelp-frontend (command) "`company-mode' front-end showing documentation in a `pos-tip' popup." (pcase command (`post-command (when company-quickhelp-delay (company-quickhelp--set-timer))) (`hide (when company-quickhelp-delay (company-quickhelp--cancel-timer)) (company-quickhelp--hide)))) (defun company-quickhelp--skip-footers-backwards () "Skip backwards over footers and blank lines." (beginning-of-line) (while (and (not (= (line-end-position) (point-min))) (or ;; [back] appears at the end of the help elisp help buffer (looking-at-p "\\[back\\]") ;; [source] cider's help buffer contains a link to source (looking-at-p "\\[source\\]") (looking-at-p "^\\s-*$"))) (forward-line -1))) (defun company-quickhelp--goto-max-line () "Go to last line to display in popup." (if company-quickhelp-max-lines (forward-line company-quickhelp-max-lines) (goto-char (point-max)))) (defun company-quickhelp--docstring-from-buffer (start) "Fetch docstring from START." (goto-char start) (company-quickhelp--goto-max-line) (let ((truncated (< (line-end-position) (point-max)))) (company-quickhelp--skip-footers-backwards) (list :doc (buffer-substring start (line-end-position)) :truncated truncated))) (defun company-quickhelp--completing-read (prompt candidates &rest rest) "`cider', and probably other libraries, prompt the user to resolve ambiguous documentation requests. Instead of failing we just grab the first candidate and press forward." (car candidates)) (defun company-quickhelp--fetch-docstring (selected) "Fetch docstring from the current backend for SELECTED string." (let ((quickhelp-str (company-call-backend 'quickhelp-string selected))) (if (stringp quickhelp-str) (with-temp-buffer (insert quickhelp-str) (company-quickhelp--docstring-from-buffer (point-min))) (let ((doc (company-call-backend 'doc-buffer selected))) (when doc ;; The company backend can either return a buffer with the doc or a ;; cons containing the doc buffer and a position at which to start ;; reading. (let ((doc-buffer (if (consp doc) (car doc) doc)) (doc-begin (when (consp doc) (cdr doc)))) (with-current-buffer doc-buffer (company-quickhelp--docstring-from-buffer (or doc-begin (point-min)))))))))) (defun company-quickhelp--doc (selected) (let ((message-log-max nil) (inhibit-message t)) (cl-letf (((symbol-function 'completing-read) #'company-quickhelp--completing-read)) (let* ((doc-and-meta (company-quickhelp--fetch-docstring selected)) (truncated (plist-get doc-and-meta :truncated)) (doc (plist-get doc-and-meta :doc))) (unless (member doc '(nil "")) (if truncated (concat doc "\n\n[...]") doc)))))) (defun company-quickhelp-manual-begin () "Manually trigger the `company-quickhelp' popup for the currently active `company' completion candidate." (interactive) ;; This might seem a bit roundabout, but when I attempted to call ;; `company-quickhelp--show' in a more direct manner it triggered a ;; redisplay of company's list of completion candidates which looked ;; quite weird. (let ((company-quickhelp-delay 0.01)) (company-quickhelp--set-timer))) (defun company-quickhelp--hide () (when (company-quickhelp-pos-tip-available-p) (pos-tip-hide))) (defun company-quickhelp--show () (company-quickhelp--cancel-timer) (when (and (company-quickhelp-pos-tip-available-p) company-selection) (while-no-input (let* ((selected (nth company-selection company-candidates)) (doc (let ((inhibit-message t)) (company-quickhelp--doc selected))) (ovl company-pseudo-tooltip-overlay)) (when (and ovl doc) (with-no-warnings (let ((width 80) (timeout 300) (overlay-width (* (frame-char-width) (overlay-get ovl 'company-width))) (overlay-position (* (frame-char-width) (- (overlay-get ovl 'company-column) 1))) (x-gtk-use-system-tooltips nil) (fg-bg `(,company-quickhelp-color-foreground . ,company-quickhelp-color-background)) (pos (save-excursion (goto-char (min (overlay-start ovl) (point))) (line-beginning-position))) (dy (if (< (overlay-get ovl 'company-height) 0) 0 (frame-char-height)))) (if company-quickhelp-use-propertized-text (let* ((frame (window-frame (selected-window))) (max-width (pos-tip-x-display-width frame)) (max-height (pos-tip-x-display-height frame)) (w-h (pos-tip-string-width-height doc))) (cond ((> (car w-h) width) (setq doc (pos-tip-fill-string doc width nil nil nil max-height) w-h (pos-tip-string-width-height doc))) ((or (> (car w-h) max-width) (> (cdr w-h) max-height)) (setq doc (pos-tip-truncate-string doc max-width max-height) w-h (pos-tip-string-width-height doc)))) (pos-tip-show-no-propertize doc fg-bg pos nil timeout (pos-tip-tooltip-width (car w-h) (frame-char-width frame)) (pos-tip-tooltip-height (cdr w-h) (frame-char-height frame) frame) nil (+ overlay-width overlay-position) dy)) (pos-tip-show doc fg-bg pos nil timeout width nil (+ overlay-width overlay-position) dy))))))))) (defun company-quickhelp--set-timer () (when (or (null company-quickhelp--timer) (eq this-command #'company-quickhelp-manual-begin)) (setq company-quickhelp--timer (run-with-idle-timer company-quickhelp-delay nil 'company-quickhelp--show)))) (defun company-quickhelp--cancel-timer () (when (timerp company-quickhelp--timer) (cancel-timer company-quickhelp--timer) (setq company-quickhelp--timer nil))) (defun company-quickhelp-hide () (company-cancel)) (defun company-quickhelp-pos-tip-available-p () "Return t if and only if pos-tip is expected work in the current frame." (and (fboundp 'x-hide-tip) (fboundp 'x-show-tip) (not (memq window-system (list nil 'pc))))) (defun company-quickhelp--enable () (add-hook 'focus-out-hook #'company-quickhelp-hide nil t) (setq-local company-quickhelp--original-tooltip-width company-tooltip-minimum-width) (setq-local company-tooltip-minimum-width (max company-tooltip-minimum-width 40)) (make-local-variable 'company-frontends) (add-to-list 'company-frontends 'company-quickhelp-frontend :append)) (defun company-quickhelp--disable () (remove-hook 'focus-out-hook #'company-quickhelp-hide t) (company-quickhelp--cancel-timer) (setq-local company-tooltip-minimum-width company-quickhelp--original-tooltip-width) (setq-local company-frontends (delq 'company-quickhelp-frontend company-frontends))) ;;;###autoload (define-minor-mode company-quickhelp-local-mode "Provides documentation popups for `company-mode' using `pos-tip'." :global nil (if company-quickhelp-local-mode (company-quickhelp--enable) (company-quickhelp--disable))) ;;;###autoload (define-globalized-minor-mode company-quickhelp-mode company-quickhelp-local-mode company-quickhelp-local-mode) (provide 'company-quickhelp) ;;; company-quickhelp.el ends here