;;; eldoc-tooltip.el --- show ElDoc as tooltips -*- lexical-binding:t -*- ;; Copyright (C) 2015 Free Software Foundation, Inc. ;; Time-stamp: "2016-01-11 08:55:41 martin" ;; Author: Martin Rudalics ;; Keywords: ElDoc, tooltips ;; Version: 1.0 ;; eldoc-tooltip.el 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, or (at ;; your option) any later version. ;; eldoc-tooltip.el 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: ;; ElDoc tooltip mode is a minor mode to show the function arglist or ;; variable docstring provided by `eldoc-documentation-function' in a ;; tooltip window. ;; To turn on this mode automatically (and simultaneously turn off ;; showing tooltips in the echo area) put ;; ;; (eldoc-tooltip-mode 1) ;; (global-eldoc-mode -1) ;; ;; into your .emacs. ;; Eldoc tooltip mode requires Emacs 25 or higher for its handling of ;; toolbar window positioning. It requires `eldoc' only to (1) get ;; access to `eldoc-documentation-function' and (2) override the option ;; `eldoc-echo-area-use-multiline-p' in invocations of the former. ;;; Code: (defgroup eldoc-tooltip nil "Show ElDoc in tooltips." :version "25.1" :group 'extensions) ;;;###autoload (define-minor-mode eldoc-tooltip-mode "Toggle ElDoc tooltip mode on or off." :group 'eldoc-tooltip (if eldoc-tooltip-mode (progn (set-frame-parameter nil 'eldoc-focus t) (add-hook 'focus-in-hook 'eldoc-tooltip--focus-in) (add-hook 'focus-out-hook 'eldoc-tooltip--focus-out) (add-hook 'window-configuration-change-hook 'eldoc-tooltip--hide-tip) (add-hook 'window-size-change-functions 'eldoc-tooltip--hide-tip) (unless (memq eldoc-tooltip-idle-timer timer-idle-list) (setq eldoc-tooltip-idle-timer (run-with-idle-timer eldoc-tooltip-delay t (if (fboundp 'window-absolute-body-pixel-edges) 'eldoc-tooltip--make-2 'eldoc-tooltip--make))))) (when eldoc-tooltip-idle-timer (cancel-timer eldoc-tooltip-idle-timer) (setq eldoc-tooltip-idle-timer nil)) (remove-hook 'focus-in-hook 'eldoc-tooltip--focus-in) (remove-hook 'focus-out-hook 'eldoc-tooltip--focus-out) (remove-hook 'window-configuration-change-hook 'eldoc-tooltip--hide-tip) (remove-hook 'window-size-change-functions 'eldoc-tooltip--hide-tip))) (defgroup eldoc-tooltip nil "Show function arglist or variable docstring in tooltip." :group 'eldoc) (defcustom eldoc-tooltip-delay 1.0 "Number of seconds of idle time to wait before showing eldoc tooltip. If user input arrives before this interval of time has elapsed after the last input, no documentation will be shown. If this variable is set to 0, no idle time is required." :type 'number :group 'eldoc-tooltip) (defcustom eldoc-tooltip-duration 60 "Number of seconds to show eldoc tooltip. If user input arrives before this time has elapsed, the tooltip will be hidden." :type 'number :group 'eldoc-tooltip) (defcustom eldoc-tooltip-location 'eols "Where to show ElDoc tooltip. Options are: `above' - over text on line above point `below' - over text on line below point `eols' - at ends of lines near point `top' on top of selected window `bottom' - on bottom of selected window" :type '(choice (const :tag "on line above point" above) (const :tag "on line below point" below) (const :tag "at line ends near point" eols) (const :tag "on top of selected window" top) (const :tag "on bottom of selected window" bottom)) :group 'eldoc-tooltip) (defcustom eldoc-tooltip-x-offset-alist '((above . 2) (current . 2) (below . 2) (top . 2) (bottom . 2)) "Offsets to adjust x-position of tooltips. These pixel offsets are added to the x-position of tooltips that respectively appear on the line above the current line (`above'), the current line (`current'), the line below the current line (`below'), the top edge of the window (`top') and the bottom edge of the window (`bottom')." :type '(alist :key-type (symbol :tag "Location") :value-type integer) :group 'eldoc-tooltip) (defcustom eldoc-tooltip-indent-above-below t "Non-nil means to show tooltips at indent of current line. This options takes affect only when tooltips shown at the beginning of a line, that is when `eldoc-tooltip-location' equals `above' or `below'. When this option is nil, such tooltips start at the visual beginning of the respective line." :type 'boolean :group 'eldoc-tooltip) (defcustom eldoc-tooltip-current-eol-extra-x-offset t "Extra offset for tooltip at end of current line. If non-nil this adds an extra offset of one character's width to avoid that the tooltip window overlays a block cursor at the end of the current line. If nil the offset is entirely determined by the value of `eldoc-tooltip-x-offsets'. Tooltips appearing above or below the current line are not affected by this option." :type 'boolean :group 'eldoc-tooltip) (defcustom eldoc-tooltip-y-offset-alist '((above . 0) (current . -2) (below . -2) (top . 2) (bottom . -2)) "Alist of offsets to adjust y-position of tooltips. These pixel offsets are added to the y-position of tooltips that respectively appear on the line above the current line (`above'), the current line (`current'), the line below the current line (`below'), the top edge of the window (`top') and the bottom edge of the window (`bottom')." :type '(alist :key-type (symbol :tag "Location") :value-type integer) :group 'eldoc-tooltip) (defcustom eldoc-tooltip-max-size '(120 . 1) "Maximimum size of ElDoc tooltips. A cons of rows and columns." :type '(cons (integer :tag "Columns") (integer :tag "Lines")) :group 'eldoc-tooltip) (defcustom eldoc-tooltip-frame-parameters '((background-color . "honeydew") (alpha . 60)) "Frame parameter alist used for ElDoc tooltips. The list of frame parameters passed to `x-show-tip' is built by appending to this a list built from the `left' and `top'/`bottom' parameters as produced by `eldoc-tooltip-make' and the value of `tooltip-frame-parameters'." :type '(alist :key-type symbol :value-type sexp) :group 'tooltip) (defvar eldoc-tooltip-idle-timer nil "ElDoc tooltip timer.") (defvar eldoc-tooltip-debug nil "Non-nil means display message about position of tooltip window.") (defun eldoc-tooltip--focus-in () "ElDoc tooltip mode function when selected frame gains focus." (set-frame-parameter nil 'eldoc-focus t)) ;; In the following two we should check that we own the tooltip. (defun eldoc-tooltip--focus-out () "ElDoc tooltip mode function when selected frame loses focus." (set-frame-parameter nil 'eldoc-focus nil) (x-hide-tip)) (defun eldoc-tooltip--hide-tip (&optional _ignore) "Hide ElDoc tooltip." (x-hide-tip)) (defun eldoc-tooltip--text () "Return text string for ElDoc tooltip, nil if there's none." (save-excursion (goto-char (window-point)) (when (boundp 'eldoc-documentation-function) ;; Alwasy try to get full string. (let ((eldoc-echo-area-use-multiline-p t)) (funcall eldoc-documentation-function))))) (defun eldoc-tooltip--current-line-wrapped-p () "Return non-nil when current line is wrapped." (save-excursion (< (progn (beginning-of-line) (vertical-motion 1) (point)) (progn (forward-line) (point))))) (defun eldoc-tooltip--make () "Make ElDoc tooltip." (let* ((pos-point (pos-visible-in-window-p (point) nil t)) ;; We make a tooltip iff `point' is visible in the selected ;; window, the frame has focus and we get a suitable text from ;; the corresponding eldoc function. Remember that ;; `pos-visible-in-window-p' may return non-nil even when POS ;; is scrolled off horizontally. (text (and eldoc-tooltip-mode pos-point (frame-parameter nil 'eldoc-focus) (eldoc-tooltip--text)))) (when text (let* ((edges (window-inside-pixel-edges)) (frame-geometry (frame-geometry)) ;; (x-frame-geometry) (frame-left-position (or (cadr (assq 'frame-position frame-geometry)) (cadr (assq 'outer-position frame-geometry)) (nth 1 (assq 'outer-edges frame-geometry)))) (frame-top-position (or (cddr (assq 'frame-position frame-geometry)) (cddr (assq 'outer-position frame-geometry)) (nth 2 (assq 'outer-edges frame-geometry)))) (frame-left (let ((edge (frame-parameter nil 'left))) (if (numberp edge) edge 0))) (left (+ frame-left ;; Count left border iff frame's left edge is on ;; screen (this should catch frame maximation by ;; moving the border off-display tricks). (if (< frame-left-position 0) 0 (cadr (assq 'external-border-size frame-geometry))) (nth 0 edges) ;; Count tool bar when it's on the left. (or (and (cdr (assq 'tool-bar-external frame-geometry)) (eq (cdr (assq 'tool-bar-position frame-geometry)) 'left) (cddr (assq 'tool-bar-size frame-geometry))) 0) ;; Count vertical scroll bar when it's on the ;; left. Don't care about fringes or margins yet. (if (eq (car (window-current-scroll-bars)) 'left) (window-scroll-bar-width) 0))) (frame-top (let ((edge (frame-parameter nil 'top))) (if (numberp edge) edge 0))) (top (+ frame-top ;; Count top border iff frame's top edge is on screen ;; (to catch frame maximation by moving the border ;; off-display tricks). Also, apparently ns counts the ;; top border in the title height already. (if (or (< frame-top-position 0) (eq (window-system) 'ns)) 0 (cddr (assq 'external-border-size frame-geometry))) ;; Add heights of title, menu and tool bar but only if ;; they are external (an "internal" tool or menu bar is ;; already counted by `window-pixel-edges'). (or (cdr (assq 'title-height frame-geometry)) (cddr (assq 'title-bar-size frame-geometry)) 0) (or (and (cdr (assq 'menu-bar-external frame-geometry)) (cddr (assq 'menu-bar-size frame-geometry))) 0) (or (and (cdr (assq 'tool-bar-external frame-geometry)) (eq (cdr (assq 'tool-bar-position frame-geometry)) 'top) (cddr (assq 'tool-bar-size frame-geometry))) 0) (if (eq eldoc-tooltip-location 'bottom) ;; Don't obscure mode line. (- (nth 3 edges) (window-mode-line-height) (window-bottom-divider-width)) (nth 1 edges)))) (x-max-tooltip-size eldoc-tooltip-max-size) parameters) (pcase eldoc-tooltip-location ((or `above `below) (let ((x-adjust (or (cdr (assq eldoc-tooltip-location eldoc-tooltip-x-offset-alist)) 0)) (y-adjust (or (cdr (assq eldoc-tooltip-location eldoc-tooltip-y-offset-alist)) 0)) (pos-x (if eldoc-tooltip-indent-above-below (save-excursion (beginning-of-line) (skip-chars-forward " \t") (nth 0 (pos-visible-in-window-p (point) nil t))) 0)) (pos-y (+ (nth 1 pos-point) (if (eq eldoc-tooltip-location 'above) 0 ;; `window-line-height' returns nil when ;; display is not up-to-date. (or (car (window-line-height)) (frame-char-height)))))) (when eldoc-tooltip-debug (message "%s %s %s %s [x: %s %s %s] [y: %s %s %s] -> %s" (if (or (< frame-top 0) (eq (window-system) 'ns)) 0 (cddr (assq 'external-border-size frame-geometry))) (or (cdr (assq 'title-height frame-geometry)) (cddr (assq 'title-bar-size frame-geometry)) 0) (or (and (cdr (assq 'menu-bar-external frame-geometry)) (cddr (assq 'menu-bar-size frame-geometry))) 0) (or (and (cdr (assq 'tool-bar-external frame-geometry)) (eq (cdr (assq 'tool-bar-position frame-geometry)) 'top) (cddr (assq 'tool-bar-size frame-geometry))) 0) left pos-x x-adjust top pos-y y-adjust (cons (+ left 0 x-adjust) (+ top pos-y y-adjust)))) (setq parameters (list (cons 'left (+ left pos-x x-adjust)) (if (eq eldoc-tooltip-location 'above) (cons 'bottom (+ top pos-y y-adjust)) (cons 'top (+ top pos-y y-adjust))))))) (`eols (let (where pos-above pos-this pos-below pos-x pos-y x-adjust y-adjust) (setq pos-this (pos-visible-in-window-p (line-end-position) nil t)) ;; Try current line first. (when pos-this (setq where 'current) (setq pos-x (nth 0 pos-this)) (setq pos-y (nth 1 pos-this))) ;; Try line above next. (when (and (setq pos-above (and (/= (line-beginning-position) (point-min)) (pos-visible-in-window-p (line-end-position 0) nil t))) (or (not pos-x) (< (nth 0 pos-above) pos-x))) (setq where 'above) (setq pos-x (nth 0 pos-above)) (setq pos-y (if truncate-lines ;; When lines are truncated use top ;; of the current line. (nth 1 pos-point) ;; Otherwise use top of beginning of ;; current line. (or (nth 1 (pos-visible-in-window-p (line-beginning-position) nil t)) 0)))) ;; Try line below last. (when (and (/= (line-end-position) (point-max)) (or truncate-lines (save-excursion (forward-line) (not (eldoc-tooltip--current-line-wrapped-p)))) (setq pos-below (pos-visible-in-window-p (line-end-position 2) nil t)) (or (not pos-x) (< (nth 0 pos-below) pos-x))) (setq where 'below) (setq pos-x (nth 0 pos-below)) (setq pos-y (nth 1 pos-below))) ;; If we didn't get a result till now simulate `above'. (unless pos-x (setq where 'above) (setq pos-x 0) (setq pos-y (nth 1 pos-point))) ;; If the window is scrolled horizontally, make sure the ;; tooltip doesn't start on the left of it. (setq pos-x (max 0 pos-x)) ;; Adjust offsets now. (setq x-adjust (or (cdr (assq where eldoc-tooltip-x-offset-alist)) 0)) (setq y-adjust (or (cdr (assq where eldoc-tooltip-y-offset-alist)) 0)) (when (and eldoc-tooltip-current-eol-extra-x-offset (or (= (point) (point-max)) (looking-at "\n"))) ;; Don't obscure block pointer at EOL. (setq x-adjust (+ x-adjust (frame-char-width)))) (when eldoc-tooltip-debug (message "%s %s %s %s - %s / %s / %s / %s -> %s [x: %s %s %s] [y: %s %s %s] -> %s" (if (or (< frame-top 0) (eq (window-system) 'ns)) 0 (cddr (assq 'external-border-size frame-geometry))) (or (cdr (assq 'title-height frame-geometry)) (cddr (assq 'title-bar-size frame-geometry)) 0) (or (and (cdr (assq 'menu-bar-external frame-geometry)) (cddr (assq 'menu-bar-size frame-geometry))) 0) (or (and (cdr (assq 'tool-bar-external frame-geometry)) (eq (cdr (assq 'tool-bar-position frame-geometry)) 'top) (cddr (assq 'tool-bar-size frame-geometry))) 0) pos-above pos-point pos-this pos-below where left pos-x x-adjust top pos-y y-adjust (cons (+ left pos-x x-adjust) (+ top pos-y y-adjust)))) (setq parameters (list (cons 'left (+ left pos-x x-adjust)) (if (eq where 'above) (cons 'bottom (+ top pos-y y-adjust)) (cons 'top (+ top pos-y y-adjust))))))) (`top (let* ((x-adjust (cdr (assq 'top eldoc-tooltip-x-offset-alist))) (y-adjust (cdr (assq 'top eldoc-tooltip-y-offset-alist)))) (when eldoc-tooltip-debug (message "[x: %s %s] [y: %s %s] -> %s" left x-adjust top y-adjust (cons (+ left x-adjust) (+ top y-adjust)))) (setq parameters (list (cons 'left (+ left x-adjust)) (cons 'bottom (+ top y-adjust)))))) (`bottom (let* ((x-adjust (cdr (assq 'top eldoc-tooltip-x-offset-alist))) (y-adjust (cdr (assq 'top eldoc-tooltip-y-offset-alist)))) (when eldoc-tooltip-debug (message "[x: %s %s] [y: %s %s] -> %s" left x-adjust top y-adjust (cons (+ left x-adjust) (+ top y-adjust)))) (setq parameters (list (cons 'left (+ left x-adjust)) (cons 'bottom (+ top y-adjust))))))) ;; Show tip. (when parameters (x-show-tip text (selected-frame) (append eldoc-tooltip-frame-parameters parameters tooltip-frame-parameters) eldoc-tooltip-duration 0 0)))))) (defun eldoc-tooltip--make-2 () "Make ElDoc tooltip. `window-absolute-body-pixel-edges' is needed for this." (let* ((pos-point (pos-visible-in-window-p (point) nil t)) ;; We make a tooltip iff `point' is visible in the selected ;; window, the frame has focus and we get a suitable text from ;; the corresponding eldoc function. Remember that ;; `pos-visible-in-window-p' may return non-nil even when POS ;; is scrolled off horizontally. (text (and eldoc-tooltip-mode pos-point (frame-parameter nil 'eldoc-focus) (eldoc-tooltip--text))) old-consed) ;; Optional. (when (boundp 'mode-line-operation-consed) (setq old-consed cons-cells-consed)) (when text (let* ((edges (and (fboundp 'window-absolute-body-pixel-edges) (window-absolute-body-pixel-edges))) (left (nth 0 edges)) (top (nth 1 edges)) (x-max-tooltip-size eldoc-tooltip-max-size) parameters) (pcase eldoc-tooltip-location ((or `above `below) (let ((x-adjust (or (cdr (assq eldoc-tooltip-location eldoc-tooltip-x-offset-alist)) 0)) (y-adjust (or (cdr (assq eldoc-tooltip-location eldoc-tooltip-y-offset-alist)) 0)) (pos-x (if eldoc-tooltip-indent-above-below (save-excursion (beginning-of-line) (skip-chars-forward " \t") (nth 0 (pos-visible-in-window-p (point) nil t))) 0)) (pos-y (+ (nth 1 pos-point) (if (eq eldoc-tooltip-location 'above) 0 ;; `window-line-height' returns nil when ;; display is not up-to-date. (or (car (window-line-height)) (frame-char-height)))))) (setq parameters (list (cons 'left (+ left pos-x x-adjust)) (if (eq eldoc-tooltip-location 'above) (cons 'bottom (+ top pos-y y-adjust)) (cons 'top (+ top pos-y y-adjust))))))) (`eols (let (where pos-above pos-this pos-below pos-x pos-y x-adjust y-adjust) (setq pos-this (pos-visible-in-window-p (line-end-position) nil t)) ;; Try current line first. (when pos-this (setq where 'current) (setq pos-x (nth 0 pos-this)) (setq pos-y (nth 1 pos-this))) ;; Try line above next. (when (and (setq pos-above (and (/= (line-beginning-position) (point-min)) (pos-visible-in-window-p (line-end-position 0) nil t))) (or (not pos-x) (< (nth 0 pos-above) pos-x))) (setq where 'above) (setq pos-x (nth 0 pos-above)) (setq pos-y (if truncate-lines ;; When lines are truncated use top ;; of the current line. (nth 1 pos-point) ;; Otherwise use top of beginning of ;; current line. (or (nth 1 (pos-visible-in-window-p (line-beginning-position) nil t)) 0)))) ;; Try line below last. (when (and (/= (line-end-position) (point-max)) (or truncate-lines (save-excursion (forward-line) (not (eldoc-tooltip--current-line-wrapped-p)))) (setq pos-below (pos-visible-in-window-p (line-end-position 2) nil t)) (or (not pos-x) (< (nth 0 pos-below) pos-x))) (setq where 'below) (setq pos-x (nth 0 pos-below)) (setq pos-y (nth 1 pos-below))) ;; If we didn't get a result till now simulate `above'. (unless pos-x (setq where 'above) (setq pos-x 0) (setq pos-y (nth 1 pos-point))) ;; If the window is scrolled horizontally, make sure the ;; tooltip doesn't start on the left of it. (setq pos-x (max 0 pos-x)) ;; Adjust offsets now. (setq x-adjust (or (cdr (assq where eldoc-tooltip-x-offset-alist)) 0)) (setq y-adjust (or (cdr (assq where eldoc-tooltip-y-offset-alist)) 0)) (when (and eldoc-tooltip-current-eol-extra-x-offset (eq where 'current) (or (= (point) (point-max)) (looking-at "\n"))) ;; Don't obscure block pointer at EOL. (setq x-adjust (+ x-adjust (frame-char-width)))) (setq parameters (list (cons 'left (+ left pos-x x-adjust)) (if (eq where 'above) (cons 'bottom (+ top pos-y y-adjust)) (cons 'top (+ top pos-y y-adjust))))))) (`top (let* ((x-adjust (cdr (assq 'top eldoc-tooltip-x-offset-alist))) (y-adjust (cdr (assq 'top eldoc-tooltip-y-offset-alist)))) (setq parameters (list (cons 'left (+ left x-adjust)) (cons 'top (+ top y-adjust)))))) (`bottom (let* ((x-adjust (cdr (assq 'bottom eldoc-tooltip-x-offset-alist))) (y-adjust (cdr (assq 'bottom eldoc-tooltip-y-offset-alist)))) (setq parameters (list (cons 'left (+ left x-adjust)) (cons 'bottom (+ (nth 3 edges) y-adjust))))))) (when parameters ;; Show tip first. (x-show-tip text (selected-frame) (append eldoc-tooltip-frame-parameters parameters tooltip-frame-parameters) eldoc-tooltip-duration 0 0) ;; If necessary, move mouse pointer away afterwards. (let* ((buffer (get-buffer " *tip*")) (window (and buffer (get-buffer-window buffer t))) (frame (and window (window-frame window))) (tip-edges (and frame (frame-edges frame 'outer))) (tip-left (nth 0 tip-edges)) (tip-top (nth 1 tip-edges)) (tip-right (nth 2 tip-edges)) (tip-bottom (nth 3 tip-edges)) (mouse-position (mouse-absolute-pixel-position)) (mouse-x (car mouse-position)) (mouse-y (cdr mouse-position))) (when (and tip-left tip-right mouse-x (<= tip-left mouse-x) (<= mouse-x tip-right) tip-top tip-bottom mouse-y (<= tip-top mouse-y) (<= mouse-y tip-bottom)) (let* ((window-edges (window-edges nil t t t)) (window-top (nth 1 window-edges))) ;; (message "frame: %s tip-left: %s tip-top: %s tip-right: %s tip-bottom: %s" ;; frame tip-left tip-top tip-right tip-bottom) (set-mouse-absolute-pixel-position mouse-x ;; If there's enough space within the seelcted window, ;; move mouse pointer two pixels above top of tooltip, ;; otherwise move it one pixel below bottom of tooltip. (if (< (+ window-top 2) tip-top) (- tip-top 2) (1+ tip-bottom))))))))) ;; Optional. (when (boundp 'mode-line-operation-consed) (setq mode-line-operation-consed (- cons-cells-consed old-consed))) )) (provide 'eldoc-tooltip) ;;; eldoc-tooltip.el ends here