Back to Table of Contents

Peek definition with Emacs frame

In many IDEs, peek definition is a feature that opens a definition (of a function, a class, a symbol, etc) in a popup window without leaving the current buffer. Most of the time, jumping into a definition, then jump back, is enough, so why bother leaving the current buffer to reload a whole new buffer, then go back for another reload. This is distracting.

An example of peek definition:

peek-definition-vs.png (Source: Microsoft)

I already searched for an alternative in Emacs, but apparently, all the solutions that involve a popup is slow and lacking. The space for displaying text is limited, and there is no font locking. You can't even search, or if it does, search is difficult and slow.

Recently, while jumping around code definitions, I lost track of the original buffer I start the jump chain, e.g. I started at file1.c, then jumped to file2.c, file3.h, file4.c, and so on, until I forgot that I started at file1.c (file1.c can be a long and hard to remember name). It makes me want to do this whole code hopping process in another separated frame/buffer instead of messing with my current buffer. I tried improved the process:

It is at this point that I suddenly remember frames. That's right, Emacs frames! It is perfect for this use case. I can do whatever I want in this frame, and when done, simply close it with C-x 5 0. With frames, the number of my workspaces (using eyebrowse) is kept at a manageable number.

But wait, if an Emacs frame is small enough, isn't it the same as a popup window, but with every feature of a buffer (syntax highlighting, code jumping, etc) available? Not to mention, making a frame is much more lightweight than other popup solutions.

At this point, implementing a peek definition in Emacs is simply automating these steps:

  1. Find the absolute position of the current beginning of the symbol at point, in pixels.
  2. Create a new invisible frame, with the current buffer in it.
  3. Position the new frame right under the beginning of the symbol at point.
  4. Jump to the symbol at point.
  5. Make frame visible again.

That's all for a peek definition popup:

peek-definition-emacs.gif

In the example, I used rtags-find-symbol-at-point function for jumping. But you can use any function that finds a definition, as long as it jumps to a buffer. Finally, here is the code:

(defun rtags-peek-definition ()
  "Peek at definition at point using rtags."
  (interactive)
  (let ((func (lambda ()
                (rtags-find-symbol-at-point)
                (rtags-location-stack-forward))))
    (rtags-start-process-unless-running)
    (make-peek-frame func)))

(defun make-peek-frame (find-definition-function &rest args)
  "Make a new frame for peeking definition"
  (when (or (not (rtags-called-interactively-p)) (rtags-sandbox-id-matches))
    (let (summary
          doc-frame
          x y
          ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
          ;; 1. Find the absolute position of the current beginning of the symbol at point, ;;
          ;; in pixels.                                                                     ;;
          ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
          (abs-pixel-pos (save-excursion
                           (beginning-of-thing 'symbol)
                           (window-absolute-pixel-position))))
      (setq x (car abs-pixel-pos))
      ;; (setq y (cdr abs-pixel-pos))
      (setq y (+ (cdr abs-pixel-pos) (frame-char-height)))

      ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
      ;; 2. Create a new invisible frame, with the current buffer in it. ;;
      ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
      (setq doc-frame (make-frame '((minibuffer . nil)
                                    (name . "*RTags Peek*")
                                    (width . 80)
                                    (visibility . nil)
                                    (height . 15))))

      ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
      ;; 3. Position the new frame right under the beginning of the symbol at point. ;;
      ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
      (set-frame-position doc-frame x y)

      ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
      ;; 4. Jump to the symbol at point. ;;
      ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
      (with-selected-frame doc-frame
        (apply find-definition-function args)
        (read-only-mode)
        (when semantic-stickyfunc-mode (semantic-stickyfunc-mode -1))
        (recenter-top-bottom 0))

      ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
      ;; 5. Make frame visible again ;;
      ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
      (make-frame-visible doc-frame))))

Then, bind the new command to a key and try it out:

(global-set-key (kbd "C-c p") 'rtags-peek-definition)

To close the peek frame, simply use C-x 5 0 (runs delete-frame command). You can bind it to another key to close frame easier, e.g. f12 key.

The more I use Emacs, the more I start realizing how useful frames are. Especially with Emacs 26 onward, there is an option to remove a frame from OS taskbar, effectively you cannot use Alt+Tab to switch to any child frame created in Emacs. With this feature, you can create many Emacs frames without creating a mess that renders Alt+tab unusable. Perhaps it is the time to embrace the frames.

comments powered by Disqus