Literate Programming 2

Jumping from compiler errors to the literate program

The problem

Most compilers and linters are not designed to operate on literate programs. 1 The mere act of writing this line helped me realize that there's another solution to this problem, at least for literate Emacs Lisp programs…it's called literate-elisp-byte-compile-file from the literate-elisp package, and it seems to be made to address this very problem. I have a Makefile to tangle an Org LP and compile the resulting tangled sources, but if you use M-x compile to run make, compiler warnings and errors taking you to the tangled sources instead of the LP.

The basic solution

Org has a source block header argument called :comments - if it is set to yes or link, Org creates comments in the tangled file, delineating each source block. The comments contain links (in Org syntax) to the original LP. The command org-babel-tangle-jump-to-org can use these links to jump from the tangled source to the LP.

That sounds like what we're after.

The command for jumping to the error is called compile-goto-error, so my first approach was to create :after advice for it.

(defun my-org-lp-goto-error (&rest args)
  (org-babel-tangle-jump-to-org))

(advice-add 'compile-goto-error :after #'my-org-lp-goto-error)
;; useful to keep handy during testing, or if something goes wrong
;; (advice-remove 'compile-goto-error #'my-org-lp-goto-error)

Refinement 1 - preserving window configuration

The above works, but you'll find that each time you jump to an error, your window configuration changes. The solution was rather simple, but it took me some time to realize it.

We want to wrap compile-goto-error and org-babel-tangle-jump-to-org in a save-window-excursion (which saves and restores the window configuration), so we switch from :after to :around advice. Note that the function signature changes accordingly.

(defun my-org-lp-goto-error (oldfn &rest args)
  (let (buffer position)
    (save-window-excursion
      (funcall oldfn)
      (org-babel-tangle-jump-to-org)
      (setq buffer   (current-buffer)
            position (point)))
    (select-window (get-buffer-window buffer))
    (goto-char position)))

(advice-add 'compile-goto-error :around #'my-org-lp-goto-error)

Refinement 2 - when the Org LP buffer is not visible

The above works fine when the Org literate program buffer is visible - the window configuration is restored - but if it isn't, we get an error. Let's sort that out.

(defun my-org-lp-goto-error (oldfn &rest args)
  (let (buffer position)
    (save-window-excursion
      (funcall oldfn)
      (org-babel-tangle-jump-to-org)
      (setq buffer   (current-buffer)
            position (point)))
    (let ((org-window (get-buffer-window buffer)))
      ;; if the Org buffer is visible, switch to its window
      (if (window-live-p org-window)
          (select-window org-window)
        (switch-to-buffer buffer)))
    (goto-char position)))

(advice-add 'compile-goto-error :around #'my-org-lp-goto-error)

Refinement 3 - interop with literate-elisp-byte-compile-file

The latest iteration is correct in itself. However, the link generation and/or org-babel-tangle-jump-to-org itself have some bugs - the latter sometimes places me in parts of the Org LP buffer which are wildily different from the position in the tangled source.

Till a time that the Org bugs are fixed, I can use literate-elisp-byte-compile-file. Since that, too, uses compilation-mode and compile-goto-error, we patch our advice to handle cases where compile-goto-error leads to an Org LP. We could remove the advice, but unlike literate-elisp it has the advantanges of working with non-Elisp LPs as well as with multiple files at a time.

The code is beginning to look a little ugly—there's probably a clearer way to write it. But this is where I'm calling it a day for the time being.

(defun my-org-lp-goto-error (oldfn &rest _args)
  (let (buffer position tangled-file-exists-p)
    (save-window-excursion
      (funcall oldfn)
      ;; `compile-goto-error' might be called from the output of
      ;; `literate-elisp-byte-compile-file', which means
      ;; `org-babel-tangle-jump-to-org' would error
      (when (ignore-errors (org-babel-tangle-jump-to-org))
        (setq buffer         (current-buffer)
              position       (point)
              tangled-file-exists-p t)))
    ;; back to where we started - the `compilation-mode' buffer
    (if tangled-file-exists-p
        (let ((org-window (get-buffer-window buffer)))
          ;; if the Org buffer is visible, switch to its window
          (if (window-live-p org-window)
              (select-window org-window)
            (switch-to-buffer buffer))
          (goto-char position))
      (funcall oldfn))))

(advice-add 'compile-goto-error :around #'my-org-lp-goto-error)

Bonus - preserving column, and temporary disable via prefix argument

I noticed that org-babel-tangle-jump-to-org does not preserve the column position for me. We save the column when we're in the tangled source and restore that later. Also, I sometimes want to temporarily disable the advice so I can see where compile-goto-error itself takes us - so I add support for a prefix argument. Calling compile-goto-error with a prefix argument will now skip the rest of the code, as though there was no advice.

(defun my-org-lp-goto-error (oldfn &optional prefix &rest args)
  "Make `compile-goto-error' lead to an Org literate program, if present.
This is meant to be used as `:around' advice for `compile-goto-error'.
OLDFN is `compile-goto-error'.
With PREFIX arg, just run `compile-goto-error' as though unadvised.
ARGS are ignored."
  (interactive "P")
  (if prefix
      (funcall oldfn)
    (let (buffer position column tangled-file-exists-p)
      (save-window-excursion
        (funcall oldfn)
        (setq column (- (point) (point-at-bol)))
        (when (ignore-errors (org-babel-tangle-jump-to-org))
          (setq buffer         (current-buffer)
                position       (point)
                tangled-file-exists-p t)))
      ;; back to where we started - the `compilation-mode' buffer
      (if tangled-file-exists-p
          (let ((org-window (get-buffer-window buffer)))
            ;; if the Org buffer is visible, switch to its window
            (if (window-live-p org-window)
                (select-window org-window)
              (switch-to-buffer buffer))
            (goto-char (+ position column)))
        (funcall oldfn)))))

(advice-add 'compile-goto-error :around #'my-org-lp-goto-error)

You can see what the final code looks like in my init.org.

Conclusion