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.