Guest post: Bookmarking PDFs in Emacs with pdf-tools and registers

| emacs

Someone wanted to share this post, so here it is!

-—

I read a lot of PDF documents on Emacs. I use the Emacs package pdf-tools for this. I also write a lot of LaTeX on Emacs (both in org-mode notes and .tex files). Frequently I find myself in the following situation: I need to jump between sections in documents to refer to definitions, before I resume my reading where I was. The natural Emacs keybindings for this are C-x r SPACE r (point-to-register) and C-x r j r (jump-to-register), which I found by searching online. I could not memorize them, so I decided to write my own keybindings that do the task. I read up on the documentation of these functions using C-h f, describing their function names and looking at the docstring. I realized that they receive an argument which is the “register name” and Emacs does the magic thing, the following two elisp lines ended up in my .emacs (after going through the usual phase where I look up online “how to set a keybinding on Emacs”, because I never remember these things):

;; Make C-f1 and C-f2 save at point & jump to region
;; useful when going back-and-forth between definitions in a file
;; subsequent code is to make this work for pdf-tools as well.
(global-set-key (kbd "<C-f1>") (lambda () (interactive) (point-to-register ?r)))
(global-set-key (kbd "<C-f2>") (lambda () (interactive) (jump-to-register ?r)))

I tested it with some text files and it worked wonderfully. However, it did not work on pdf-tools! There's something special about the way that package treats files that is not compatible with Emacs registers. I looked around online for solutions, and found this github issue. I skimmed through with my usual impatience and ignorant attitude that only cares for the solution. Someone there mentioned the package saveplace-pdf-view, which I looked up on github. In its source code, I was able to locate the functions pdf-view-bookmark-make-record and pdf-view-bookmark-jump, which (thank goodness!) do what I need. I obtained this:

(define-key pdf-view-mode-map (kbd "<C-f1>")
  (lambda ()
    "Saves the current position on the pdf to jump to later with <C-f2>."
    (interactive)
    (setf my-bookmark (pdf-view-bookmark-make-record))))

(define-key pdf-view-mode-map (kbd "<C-f2>")
  (lambda ()
    "Loads the position saved by <C-f1>."
    (interactive)
    (pdf-view-bookmark-jump my-bookmark)))

It works wonderfully, just like the text case, thanks to the folks who designed these functions (they remember the exact position, not just page). However, they only allow me to save one point at a time. It would be nice to be able to save multiple points, so that I can jump around various places on the document, definitions I often visit, other places of interest. So what I needed was a way to save in a variable all these bookmarks generated by pdf-view-bookmark-make-record, and then be able to look them up using a name. Anticipating all this, I wrote the following:

(defvar my-bookmarks nil
  "List of bookmarks, useful for pdf-mode where I save my positions with <C-f1> etc.")

(defconst my-default-bookmark ?1
  "This is the default bookmark name")

(define-key pdf-view-mode-map (kbd "<C-f1>")
  (lambda ()
    "Saves the current position on the pdf to jump to later with <C-f2>."
    (interactive)
    (setf (alist-get my-default-bookmark my-bookmarks) (pdf-view-bookmark-make-record))))

(define-key pdf-view-mode-map (kbd "<C-f2>")
  (lambda ()
    "Loads the position saved by <C-f1>."
    (interactive)
    (pdf-view-bookmark-jump (alist-get my-default-bookmark my-bookmarks))))

That's a typical use of an associative list, a data structure that Emacs is fond of. I looked up online ways to handle associative lists (or alists) in elisp. I wanted to emulate the behavior of point-to-register, which requires the register name (as a single character). Incidentally, single characters in elisp are denoted by ?a, ?b, ?c, etc. I found a nice Stack Exchange post that explained how to set and get values from alists. It works like this:

;;              ↓This is the key↓   ↓This is the alist↓
(setf (alist-get my-bookmark-name     my-bookmarks)
      my-value) ;; ← This is the value (bookmark data)

If you are not familiar with setf, it asks for a place to store something to, and alist-get points to the particular entry of your alist that you ask for.

The last ingredient that I needed was to figure out a way to emulate the behavior of C-x r SPACE, which shows in the minibuffer “Point to register:” and waits for input. I tried reading the documentation of point-to-register to understand how this is done, but I couldn't figure it out. I had a vague idea it is possible with (interactive) so I looked up some examples. It wasn't easy to figure out, but at the end I realized that the “Code characters for interactive” are just some prefix codes in the string provided to interactive that tell it how to supply its caller with input from Emacs.

To ensure I had the right idea, I tested:

(global-set-key (kbd "<C-f5>")
  (lambda (c) (interactive "cTest: ") (message "Read: %c" c)))

Pressing <C-f5> and entering "a", sure enough prints "Read: a", which is great. All I need to do is ask for the bookmark name to save to or load from. I decided to use the neighboring keybindings <C-f3> and <C-f4> for those purposes, still retaining <C-f1> and <C-f2> for a quick lookup.

I was ready to complete the functionality for pdf-tools,

(defvar my-bookmarks nil
  "List of bookmarks, useful for pdf-mode where I save my positions with <C-f1> etc.")

(defconst my-default-bookmark ?1
  "This is the default bookmark name")

(defun my-save-pdf-position (&optional b)
  "Saves the current PDF position of pdf-tools at a bookmark named B."
  (unless b (setq b my-default-bookmark))
  (setf (alist-get b my-bookmarks)
  (pdf-view-bookmark-make-record)))

(defun my-load-pdf-position (&optional b)
  "Loads the PDF position saved at the bookmark named B."
  (unless b (setq b my-default-bookmark))
  (pdf-view-bookmark-jump (alist-get b my-bookmarks)))

(define-key pdf-view-mode-map (kbd "<C-f1>")
  (lambda ()
    (interactive)
    (my-save-pdf-position)))

(define-key pdf-view-mode-map (kbd "<C-f2>")
  (lambda ()
    (interactive)
    (my-load-pdf-position)))

(define-key pdf-view-mode-map (kbd "<C-f3>")
  (lambda (b) (interactive "cSaving to bookmark name (single character): ")
    (my-save-pdf-position b)))

(define-key pdf-view-mode-map (kbd "<C-f4>")
  (lambda (b) (interactive "cLoading from bookmark name (single character): ")
    (my-load-pdf-position b)))

Now I could just complement it with the text functionality

(global-set-key (kbd "<C-f1>") (lambda () (interactive) (point-to-register my-default-bookmark)))
(global-set-key (kbd "<C-f2>") (lambda () (interactive) (jump-to-register my-default-bookmark)))
(global-set-key (kbd "<C-f3>") (lambda (r) (interactive "cSaving to register: ") (point-to-register r)))
(global-set-key (kbd "<C-f4>") (lambda (r) (interactive "cLoading from register: ") (jump-to-register r)))

I also have to thank the folks over at freenode's #emacs channel for helping me with this (and many other things over the years), and Sacha Chua in particular for encouraging me to write this blog post. So, big thank you!

The whole code, together with the comments, is below

;; Make <C-f1> and <C-f2> save at point & jump to region.
;; Useful when going back-and-forth between definitions in a file.
;; The code below makes this work for pdf-tools as well.
;;
;; You can use <C-f3> and <C-f4> to have more save and load slots.
;; They are named by single characters, i.e. try
;; <C-f3> 5
;; to save to slot 5 (you can use a letter as well)
;; <C-f4> 5
;; to load from slot 5. The default slot name is 1.

(defvar my-bookmarks nil
  "List of bookmarks, useful for pdf-mode where I save my positions with <C-f1> etc.")

(defconst my-default-bookmark ?1
  "This is the default bookmark name")

(defun my-save-pdf-position (&optional b)
  "Saves the current PDF position of pdf-tools at a bookmark named B."
  (unless b (setq b my-default-bookmark))
  (setf (alist-get b my-bookmarks)
  (pdf-view-bookmark-make-record)))

(defun my-load-pdf-position (&optional b)
  "Loads the PDF position saved at the bookmark named B."
  (unless b (setq b my-default-bookmark))
  (pdf-view-bookmark-jump (alist-get b my-bookmarks)))

(define-key pdf-view-mode-map (kbd "<C-f1>") 
  (lambda ()
    (interactive)
    (my-save-pdf-position)))

(define-key pdf-view-mode-map (kbd "<C-f2>")
  (lambda ()
    (interactive)
    (my-load-pdf-position)))

(define-key pdf-view-mode-map (kbd "<C-f3>")
  (lambda (b) (interactive "cSaving to bookmark name (single character): ")
    (my-save-pdf-position b)))

(define-key pdf-view-mode-map (kbd "<C-f4>")
  (lambda (b) (interactive "cLoading from bookmark name (single character): ")
    (my-load-pdf-position b)))

(global-set-key (kbd "<C-f1>") (lambda () (interactive) (point-to-register my-default-bookmark)))
(global-set-key (kbd "<C-f2>") (lambda () (interactive) (jump-to-register my-default-bookmark)))
(global-set-key (kbd "<C-f3>") (lambda (r) (interactive "cSaving to register: ") (point-to-register r)))
(global-set-key (kbd "<C-f4>") (lambda (r) (interactive "cLoading from register: ") (jump-to-register r)))

-–— Note from Sacha:

You can bind interactive functions without using a lambda, like this:

(global-set-key (kbd "<C-f3>") #'point-to-register)

For interactive functions that sometimes prompt for arguments and sometimes don't, I've seen people use the prefix argument like this:

(interactive (list (if current-prefix-arg ... "default argument")))

You can distinguish multiple uses of the prefix argument by checking the value of current-prefix-arg.

Alternatively, sometimes people define an interactive function that doesn't have arguments, and they have that function call the other interactive function that has arguments.

Also, if you're trying the keyboard shortcuts in this and you're wondering why Ctrl F1 doesn't seem to work for you (like it didn't work for me), check if your window manager is using the shortcuts for something else. I'm on KDE, so I needed to use Global Shortcuts to remove the KWin keyboard shortcuts for changing to desktop 1, 2, 3, and 4.

Let me know if you have comments or feedback and I can pass them along. If you want to share a tip about Emacs and don't have a place to put it, feel free to send it to me too. Enjoy!

You can comment with Disqus or you can e-mail me at sacha@sachachua.com.