Custom Org-Mode Capture Function for Annotating Bad Code

Something Borrowed, Something Old, and Something New

This post follows-up on my Thinking Through Capturing and Annotating “Bad” Code post by describing functionality. Then I dive into the implementation details.

Articulating Functions

I will write to a file that is in version control, it will likely be a Denote Org-Mode 📖 file.

I want to capture two layers. Starting from the inside, there is the Code level. The data (and metadata) for that region is:

Function name
The name of the function; my dream state would be to use Tree Sitter 📖 to get the correct scope.
File name
Where on my machine is this code.
Git link reference
What is the URL to the code in it’s current state.
Code-block
What’s the code

The outer layer is the Example level. The data (and metadata) for that region is:

State
For example TODO.
Title
The terse and yet unique name (default to timestamp).
Introduction
Explaining the situation that brought me there.
Code Block(s)
One or more Code levels (see above).
Discussion
What’s up with the code.
Resolution
After some time what did the refactor look like. What were the pull requests/issues filed.

I won’t directly create the Example level, instead I will rely on creating a Code level, which will be part of a Example.

Creating the Code has three options for:

  • Assign to the current Example
  • Prompt for a Example
  • Create a new Example

The Example will require additional writing, but in the interest of expediency, I plan to quickly file it away.

I would like the ability to have a minor mode, that when active, calls attention to the fact that the current buffer has one or more Code levels in my catalog.

At this point, I want to revisit my capture tool chain. It’s already inserting the note at the right location. I assume that org-mode will provide lots of tooling.

Digging into Impelementation Details

, I spent a few hours building out Org-Mode capture templates and functions.

There were four tasks:

  • Prompt for the Example in which to write the Code.
  • Position point (e.g. the cursor) in the target file according to the prompted value.
  • Copy the content.
  • Paste the content into the file.

I refined the structure of the Example to the following:

** TODO ${example} :${tag}:

*** TODO Context

*** Code :code:

*** TODO Discussion

*** COMMENT Refactoring

By establishing a structure, I could settle on how I would insert content. I also reviewed Org-Mode’s documentation and some of my existing code.

I then started writing jf/org-mode/capture/prompt-for-example. That function prompts for the target Example where I file away the Code. Below is a copy of that code:

(cl-defun jf/org-mode/capture/prompt-for-example
    (&optional given-mode &key (tag "example"))
  "Prompt for the GIVEN-MODE example."
  (let* ((mode (or given-mode (completing-read "Example:"
                                               '("Existing" "New" "Stored")))))
    (cond
     ((string= mode "New")
      (let ((example (read-string "New Example Name: "
                                   nil
                                   nil
                                   (format-time-string "%Y-%m-%d %H:%M:%S"))))
        (with-current-buffer (find-file-noselect
                              jf/org-mode/capture/filename)
          (end-of-buffer)
          (insert (s-format jf/org-mode/capture/example-template
                            'aget
                            (list (cons "example" example) (cons "tag" tag))))
          example)))
     ((string= mode "Existing")
      (with-current-buffer (find-file-noselect
                            jf/org-mode/capture/filename)
        (let ((examples (org-map-entries
                         (lambda ()
                           (org-element-property :title (org-element-at-point)))
                         (concat "+LEVEL=2+" tag) 'file)))
          (if (s-blank? examples)
              (jf/org-mode/capture/prompt-for-example "New" :tag tag)
            (completing-read "Example: " examples nil t)))))
     ((string= mode "Stored")
      (or jf/org-mode/capture/stored-context
          (jf/org-mode/capture/prompt-for-example "Existing" :tag tag))))))

I then wrote jf/org-mode/capture/set-position-file to find where to position point.

(cl-defun jf/org-mode/capture/set-position-file
    (&key
     (headline (jf/org-mode/capture/prompt-for-example))
     (tag "code")
     (parent_headline "Examples"))
  "Find and position the cursor at the end of HEADLINE.

The HEADLINE must have the given TAG and be a descendant of the
given PARENT_HEADLINE.  If the HEADLINE does not exist, write it
at the end of the file."
  ;; We need to be using the right agenda file.
  (with-current-buffer (find-file-noselect jf/org-mode/capture/filename)
    (setq jf/org-mode/capture/stored-context headline)
    (let* ((existing-position (org-element-map
                                  (org-element-parse-buffer)
                                  'headline
                                (lambda (hl)
                                  (and (=(org-element-property :level hl) 3)
                                       (member tag
                                               (org-element-property :tags hl))
                                       (string= headline
                                                (plist-get
                                                 (cadr
                                                  (car
                                                   (org-element-lineage hl)))
                                                 :raw-value))
                                       (org-element-property :end hl)))
                                nil t)))
      (goto-char existing-position))))

I then modified an existing function, renaming it to jf/org-mode/capture/get-content. That function grabs the content (and metadata) of the selected region. For this function, I drew inspiration from Capturing Content for Emacs.

(cl-defun jf/org-mode/capture/get-content (start end &key (include-header t))
  "Get the text between START and END returning an `org-mode' formatted string."
  (require 'magit)
  (require 'git-link)
  (let* ((file-name (buffer-file-name (current-buffer)))
         (org-src-mode (replace-regexp-in-string
                        "-\\(ts-\\)?mode"
                        ""
                        (format "%s" major-mode)))
         (func-name (which-function))
         (type (cond
                ((eq major-mode 'nov-mode) "QUOTE")
                ((derived-mode-p 'prog-mode) "SRC")
                (t "SRC" "EXAMPLE")))
         (code-snippet (buffer-substring-no-properties start end))
         (file-base (if file-name
                        (file-name-nondirectory file-name)
                      (format "%s" (current-buffer))))
         (line-number (line-number-at-pos (region-beginning)))
         (remote-link (when (magit-list-remotes)
                        (progn
                          (call-interactively 'git-link)
                          (car kill-ring)))))
    (concat
     (when include-header
       (format "\n**** %s" (or func-name
                               (format-time-string "%Y-%m-%d %H:%M:%S"))))
     "\n:PROPERTIES:"
     (format "\n:CAPTURED_AT: %s" (format-time-string "%Y-%m-%d %H:%M:%S"))
     (format "\n:REMOTE_URL: [[%s]]" remote-link)
     (format "\n:LOCAL_FILE: [[file:%s::%s]]" file-name line-number)
     (when func-name (format "\n:FUNCTION_NAME: %s" func-name))
     "\n:END:\n"
     (format "\n#+BEGIN_%s %s" type org-src-mode)
     (format "\n%s" code-snippet)
     (format "\n#+END_%s\n" type))))

I refined and refactored my jf/org-mode-capture/insert-command; it started as the function to write content to my current clock but now serves as either write to clock or write to my back log file.

(cl-defun jf/org-mode/capture/insert-content (start end prefix)
  "Capture the text between START and END.  When given PREFIX capture to clock."
  (interactive "r\np")
  (let* ((capture-template (if (= 1 prefix) "c" "C"))
         (include-header (if (= 1 prefix) t nil))
         ;; When we're capturing to clock, we don't want a header.
         (text (jf/org-mode/capture/get-content start end
                                                  :include-header
                                                  include-header)))
    (org-capture-string text capture-template)))

And last, here are the declarations of those two capture templates. I’d imagine there’s a way I could leverage the same capture template by using a different capture function. But for now, I’ll settle for duplication.

("c" "Content to Backlog"
 plain (file+function
        jf/org-mode/capture/filename
        jf/org-mode/capture/set-position-file)
 "%i%?"
 :empty-lines 1)
("C" "Content to Clock"
 plain (clock)
 "%i%?"
 :empty-lines 1)

Conclusion

I put this forward for others to stumble upon my approach. And as I was writing this, I realized “Wow, I’m copying a lot of code to this blog post and what I just wrote should be able to help with that.”

Another future project.

Now, it’s time to start capturing “bad” code and start writing about that.