💾 Archived View for tilde.team › ~contrapunctus › gemlog › literate-programming-2.gmi captured on 2022-04-28 at 19:21:13. Gemini links have been rewritten to link to archived content

View Raw

More Information

⬅️ Previous capture (2021-11-30)

-=-=-=-=-=-=-

Literate Programming 2: Jumping from compiler errors to the literate program

↩ gemlog

→ Last: Literate Programming 2: Jumping from compile errors to the literate program

← Previous: Keyboard Machinations with Kmonad

<2021-11-21 Sun>

The problem

Most compilers and linters are not designed to operate on literate programs. [1] 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.

Makefile

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].

init.org

Conclusion

If this post helped you, or maybe you know a clearer way to write the advice, I'd love to hear about it.

Drop me a line

Support me on Liberapay

Footnotes

_________

[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.