Saturday, 7 November 2009

Emacs mark-stack

There's no worst feeling than being lost in your code. You know, when you need to look at a bunch of code fragments buried in other files or in the same, very large, file; or having to trace back a bunch of function calls spanning multiple emacs buffers and not knowing where the previous stuff was.
Okey, there are probably a lot of feelings that are worst. But you gotta give me it's annoying.

If only there was a way to push positions to a stack and then come back to them later on! I know about C-@ and C-U C-@, but if you use delete-selection-mode or anything else in which you do a lot of marks that's not an option. Plus, the default key bindings suck and it's only buffer local. So I decided to write my own mark system in emacs lisp.

Before people come rushing down to the comments to tell me there's already a better solution for this problem, take into account that I didn't look much into it. I did this not only to solve the problem but to practice my elisp. Nevertheless, if there are alternatives I would love to hear about them; so comment on, just don't troll =)

If you don't know emacs-lisp there's a guide by Xah Lee I can't recommend enough.


So I want two things: (i) To be able to push and pop marks locally to a buffer and (ii) to be able to pop marks globally. The later means that if I push marks in two different buffers and I do a global pop, the current buffer will change to the one with the latest mark.

To do this, I used two datastructures: A hash table that maps a buffer name to its stack, and a global stack that keeps all the pushed positions.

The first one looks like this:

"marks.el" -> (1622 1042 1283)

((1622 "marks.el") (1042 "marks.el") (194 "*Messages*") (1283 "marks.el"))

In the global stack we keep the name of the buffer so we can move to it and remove the mark from the right stack.

So, onto the code.


First we need a helper function to add the list to the hash if it doesn't exist already or push to the stack on an existing list

(defun add-or-push (key value hash)
(if (gethash key hash)
(puthash key (cons value (gethash key hash)) hash))
(puthash key (list value) hash))))

And some functions to clear the local and global stack should things go wrong.

(defun clear-push-mark-for-buffer ()
"Resets the buffer's stack"
(puthash (buffer-name) () local-mark-stack))

(defun clear-global-push-mark ()
"Resets the buffer's stack"
(setq global-mark-stack '())
(maphash (lambda (kk vv) (puthash kk () local-mark-stack)) local-mark-stack)

Now, the real code:

For pushing, we just need one function that adds the current possition to both datastructures:

(defun local-push-mark ()
"Pushes a the current point to a stack"
(if (boundp 'local-mark-stack)
(let (buffer point)
(setq buffer (buffer-name))
(setq point (point))
(add-or-push buffer point local-mark-stack)
(message "Pushed %d on %s" point buffer)
(if (boundp 'global-mark-stack)
(setq global-mark-stack (cons (list point buffer) global-mark-stack))
(setq global-mark-stack (list point buffer)))))
(setq local-mark-stack (make-hash-table))

For popping, however, we need a function for the local stack and another for the global stack

(defun local-pop-mark ()
"Pops the a mark from the current buffer's stack"
(let (stack)
(setq stack (gethash (buffer-name) local-mark-stack))
(if (and (boundp 'local-mark-stack) stack)
(goto-char (pop stack))
(puthash (buffer-name) stack local-mark-stack)
(setq global-mark-stack
(remove (list (point) (buffer-name)) global-mark-stack)))
(message "No marks to pop"))))

(defun global-pop-mark ()
"Pops a mark from any buffer"
(let (pos buffer)
(setq pos (car global-mark-stack))
(setq buffer (nth 1 (car global-mark-stack)))
(setq stack (gethash buffer local-mark-stack))
(if (and (boundp 'global-mark-stack) stack)
(switch-to-buffer buffer)
(goto-char (pop stack))
(puthash buffer stack local-mark-stack)
(setq global-mark-stack (remove (list pos buffer) global-mark-stack)))
(message "No marks to pop"))))

the only important detail to notice is that the global pop removes both from the local and global stacks. The rest should be self-explanatory.

And we add a statement to provide the marks module

(provide 'marks)

So now, we just need to load the module and add some key bindings:

(require 'marks)
(global-set-key (kbd "C-x ") 'local-push-mark)
(global-set-key (kbd "C-x ") 'local-pop-mark)
(global-set-key (kbd "C-x S-") 'global-pop-mark)

Using it.

If your using my .emacs you just need to ckeck out the code and compile it.

If not just copy everything up to " (provide "marks") " into a file called marks.el and load it in your .emacs.


Edit: Apparently there's no lisp syntax support for the highlighter I'm using. sorry for that.

No comments:

Post a Comment