In my emacs init file for 2015 post I state:
However it would be kind of nice to be able to run a command to open a new buffer with links to line numbers for all definitions in the current file, shouldn’t be too hard to implement either, we’ll see if I manage in 2015.
I didn’t, but I have now, better late than never!
This thing is just like occur but:
- Uses a predefined regex to get at function / method definitions in several languages (Clojure, Picolisp and PHP etc).
- Displays all results sorted alphabetically.
Note that the elisp below is probably not idiomatic or very clever, a long time elisp expert would probably manage in 5 lines.
Let’s get to the nitty-gritty of how I accomplished this amazing feat, the relevant sections in the source from top to bottom:
(setq list-functions-regex "function [[:alnum:]]+[ ]*(\\|(de \\|(dm \\|(defun \\|(defn ") (setq list-func-buffer nil)
This is the regex which you can override to manage your own language of choice.
(defun chomp (str) "Chomp leading and tailing whitespace from STR." (replace-regexp-in-string (rx (or (: bos (* (any " \t\n"))) (: (* (any " \t\n")) eos))) "" str)) (defun get-all-functions () (interactive) (let ((lnum 0) ret) (dolist (el (split-string (buffer-string) "\\\n")) (setq lnum (1+ lnum)) (when (string-match list-functions-regex el) (push (list lnum (chomp el)) ret))) ret))
We begin with splitting the current buffer into individual lines by running split-string on the whole buffer-string with newlines as the split character.
Then we keep track of the line numbers and if we get a match on our function regex we store the line number in the car and the chomped / trimmed line in the cdr.
I copy pasted chomp from somewhere, can’t find out where.
(defun sort-by-cadr (el1 el2) (dictionary-lessp (cadr el1) (cadr el2))) (defun goto-func-in-buffer (button) (let ((line-lbl (split-string (button-label button) ":"))) (switch-to-buffer-other-frame list-func-buffer) (goto-line (string-to-number (car line-lbl))))) (define-button-type 'goto-func-in-buffer-btn 'action 'goto-func-in-buffer 'follow-link t 'mouse-face nil)
Here we first define a sorting function the mechanics behind the sorting are copy pasted from here.
The goto-func-in-buffer will be called to jump to the original line in the original buffer, it simply gets the line number from the displayed text in our result buffer and uses that number to jump to the line in question in the original buffer with the help of switch-to-buffer-other-frame and my own list-func-buffer variable.
Next we define each “button” that will display the matched line, we link to our action function, we turn on follow link in order for mouse clicks to work. But we set mouse-face to nil because I couldn’t get it to work properly with the mouseover effect when it was turned on.
(defun show-all-functions () (interactive) (setq list-func-buffer (current-buffer)) (let ((funcs (get-all-functions))) (set-face-underline 'button nil) (when funcs (set-buffer (get-buffer-create "*Functions*")) (erase-buffer) (switch-to-buffer-other-window (current-buffer)) (dolist (el (sort funcs 'sort-by-cadr)) (insert-text-button (concat (number-to-string (car el)) ": " (cadr el) "\n") :type 'goto-func-in-buffer-btn)) (beginning-of-buffer)))) (provide 'list-functions)
Finally the invocation function:
3.) We begin with storing the current buffer whose functions we want to list in list-func-buffer.
4.) We get all lines with line numbers and the line as an alist.
5.) We turn off underlines for the button face because it made the result look like shit, unfortunately the side effect is that it’s turned off everywhere.
7-8.) We create (or get if it’s already there) our *Functions* buffer where we’ll display the result and set it as the current buffer, we also erase it to make way for the new result (in case it existed already).
9.) Then we move the cursor to it.
10-11.) We then loop the sorted alist and insert the buttons.
12.) Finally we move the cursor to the top of the buffer.
Full source and repository can be found here.