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:
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:
- Using a different buffer: Initially, I create another buffer and start from there. However, it is usually the case that the other buffer is also a useful buffer.
- Using a different workspace: I am using eyebrowse, a package that can create a whole new workspace and save the previous window configuration as another workspace. Effectively, you can switch between different window configurations with ease. This solution quickly becomes cumbersome, because I must remember which workspace is for browsing code, not to mention it is quite common for me to create 5 other workspaces for other purposes. It quickly becomes a burden to manage the workspaces.
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:
- Find the absolute position of the current beginning of the symbol at point, in pixels.
- Create a new invisible frame, with the current buffer in it.
- Position the new frame right under the beginning of the symbol at point.
- Jump to the symbol at point.
- Make frame visible again.
That's all for a peek definition popup:
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.