Endless Parentheses

Concise ramblings on Emacs productivity.

Updating org-mode #+INCLUDE: statements on the fly

Today’s post regards my answer to kaushalmodi's question. Since the beta is still private, you might not be able to follow those links quite yet, so I’ll summ it up here.

The Question

Kaushalmodi uses #+INCLUDE: statements with line specifications in his org files. Here is an example similar to his. 14 and 80 are the first and last line of a class declaration in the source file, so they’re quite obvious for a human to identify.

#+INCLUDE: "code/my-class.sv" :src systemverilog :lines "14-80"

The problem here is that whenever “my-class.sv” is edited those line numbers are likely to become outdated. So you would have to go through each org file which might include “my-class.sv” and update the numbers.

The Solution

Unfortunately, org-mode doesn’t have a flexible way of declaring include statements. You either specify the line numbers or you don’t.

The solution was to add :range-begin and :range-end keywords to the statement

#+INCLUDE: "code/my-class.sv" :src systemverilog :range-begin "^class" :range-end "^endclass" :lines "14-80"

and then write a function which

  1. goes through each #+INCLUDE: statement in the buffer,
  2. checks if it has :range-begin and/or :range-end keywords and takes their arguments as regular expressions,
  3. visits the relevant file and searches for these regular expressions,
  4. checks what the line numbers are now,
  5. and updates them accordingly in the org buffer.

This function can then be assigned to a key, added to before-save-hook, or added to one of org-mode’s bajillion available hooks. Finally, to go the extra mile, we make the behaviour customizable per file extension through a defcustom.

(add-hook 'before-save-hook #'endless/update-includes)

(defun endless/update-includes (&rest ignore)
  "Update the line numbers of #+INCLUDE:s in current buffer.
Only looks at INCLUDEs that have either :range-begin or :range-end.
This function does nothing if not in org-mode, so you can safely
add it to `before-save-hook'."
  (interactive)
  (when (derived-mode-p 'org-mode)
    (save-excursion
      (goto-char (point-min))
      (while (search-forward-regexp
              "^\\s-*#\\+INCLUDE: *\"\\([^\"]+\\)\".*:range-\\(begin\\|end\\)"
              nil 'noerror)
        (let* ((file (expand-file-name (match-string-no-properties 1)))
               lines begin end)
          (forward-line 0)
          (when (looking-at "^.*:range-begin *\"\\([^\"]+\\)\"")
            (setq begin (match-string-no-properties 1)))
          (when (looking-at "^.*:range-end *\"\\([^\"]+\\)\"")
            (setq end (match-string-no-properties 1)))
          (setq lines (endless/decide-line-range file begin end))
          (when lines
            (if (looking-at ".*:lines *\"\\([-0-9]+\\)\"")
                (replace-match lines :fixedcase :literal nil 1)
              (goto-char (line-end-position))
              (insert " :lines \"" lines "\""))))))))

(defun endless/decide-line-range (file begin end)
  "Visit FILE and decide which lines to include.
BEGIN and END are regexps which define the line range to use."
  (let (l r)
    (save-match-data
      (with-temp-buffer
        (insert-file file)
        (goto-char (point-min))
        (if (null begin)
            (setq l "")
          (search-forward-regexp begin)
          (setq l (line-number-at-pos (match-beginning 0))))
        (if (null end)
            (setq r "")
          (search-forward-regexp end)
          (setq r (1+ (line-number-at-pos (match-end 0)))))
        (format "%s-%s" l r)))))

Tags: org-mode, emacs

Say thanks on Gratipay
comments powered by Disqus