Tidying my Bookmarks, Annotations and Code Notes

Pretty pleased with all the way I can explore a new code base.
This commit is contained in:
Howard Abrams 2026-07-15 11:51:17 -07:00
parent 58e18451d6
commit 968885e5f3
6 changed files with 391 additions and 38 deletions

View file

@ -1053,28 +1053,6 @@ I do want to change a couple of bindings, as ~j~ to pull up a =completing-read=
(define-key dired-mode-map (kbd "n") 'evil-search-next) (define-key dired-mode-map (kbd "n") 'evil-search-next)
(define-key dired-mode-map (kbd ",") 'major-mode-hydras/dired-mode/body) (define-key dired-mode-map (kbd ",") 'major-mode-hydras/dired-mode/body)
#+end_src #+end_src
* Annotations
Let's try [[https://github.com/bastibe/annotate.el][annotate-mode]], which allows you to drop "notes" and then move to them (yes, serious overlap with bookmarks, which we will return to).
#+begin_src emacs-lisp
(use-package annotate
:config
(ha-leader
"t A" '("annotations" . annotate-mode)
"n" '(:ignore t :which-key "notes")
"n a" '("toggle mode" . annotate-mode)
"n n" '("annotate" . annotate-annotate)
"n d" '("delete" . annotate-delete)
"n s" '("summary" . annotate-show-annotation-summary)
"n j" '("next" . annotate-goto-next-annotation)
"n k" '("prev" . annotate-goto-previous-annotation)
;; If a shift binding isn't set, it defaults to non-shift version
;; Use SPC N N to jump to the next error:
"n N" '("next error" . flycheck-next-error)))
#+end_src
Keep the annotations simple, almost /tag-like/, and then the summary allows you to display them.
* Keepass * Keepass
Use the [[https://github.com/ifosch/keepass-mode][keepass-mode]] to view a /read-only/ version of my Keepass file in Emacs: Use the [[https://github.com/ifosch/keepass-mode][keepass-mode]] to view a /read-only/ version of my Keepass file in Emacs:
#+begin_src emacs-lisp #+begin_src emacs-lisp

View file

@ -1,7 +1,7 @@
#+title: Capturing Notes with Org #+TITLE: Capturing Notes with Org
#+author: Howard X. Abrams #+AUTHOR: Howard X. Abrams
#+date: 2020-09-18 #+DATE: 2020-09-18
#+tags: emacs org #+TAGS: emacs org
A literate programming file for configuring org for capturing notes. A literate programming file for configuring org for capturing notes.

354
ha-code-notes.org Normal file
View file

@ -0,0 +1,354 @@
#+TITLE: Taking Notes with Org
#+AUTHOR: Howard X. Abrams
#+DATE: 2026-07-14
#+FILETAGS: emacs hamacs
#+LASTMOD: [2026-07-14 Tue]
A literate programming file for configuring Emacs to take notes.
#+begin_src emacs-lisp :exports none
;;; ha-code-notes --- configuring Emacs to take notes. -*- lexical-binding: t; -*-
;;
;; © 2026 Howard X. Abrams
;; Licensed under a Creative Commons Attribution 4.0 International License.
;; See http://creativecommons.org/licenses/by/4.0/
;;
;; Author: Howard X. Abrams <http://gitlab.com/howardabrams>
;; Maintainer: Howard X. Abrams
;; Created: July 14, 2026
;;
;; While obvious, GNU Emacs does not include this file or project.
;;
;; *NB:* Do not edit this file. Instead, edit the original literate file at:
;; /Users/howard.abrams/src/hamacs/ha-code-notes.org
;; And tangle the file to recreate this one.
;;
;;; Code:
#+end_src
* Introduction
Seems silly to have a full set of instructions for /taking notes/ using Org-mode, because that is what it does, but this is a bit special.
When I [[file:ha-capturing-notes.org][capture notes]] with a link to some source code, the /destination/ is a *clocked in* task. Useful, but seems to require a dedicated task and some forethought. The following project pairs a “source code file” (which can be any file, actually) and a “notes file” with headers based on the function in the original file.
#+begin_src dot :file ha-code-notes-illustration.png :exports file :results file
digraph G {
rankdir=LR;
// Make to icons side-by-side
code_file [shape=note, label="", fixedsize=true, width=1, height=1.5];
note_file [shape=note, label="", fixedsize=true, width=1, height=1.5];
// Create a couple of label to live _below_ the icons:
code_label [shape=plaintext, label="foobar.py"];
note_label [shape=plaintext, label=".foobar-note.org"];
{ rank=same; code_file; code_label; }
{ rank=same; note_file; note_label; }
code_file -> code_label [style=invis];
note_file -> note_label [style=invis];
// Connect the code and note icons:
code_file -> note_file;
}
#+end_src
#+attr_org: :width 800px
[[file:ha-code-notes-illustration.png]]
This allows me to /wax poetic/ with a parallel, but separate org file.
* Helper Functions
While this project started simple, Ive expanded the ideas, and this requires some custom helper functions. For instance, I want to add a buffer-level /property/ to an org-mode file, for instance:
#+begin_example
#+TITLE: The title of the file
#+PROPERTY: key1 value1
#+end_example
The tricky bit about this function is that if (in the above example) =key1= exists, we should /replace/ it, not add to it.
#+BEGIN_SRC emacs-lisp
(defun ha-org-set-buffer-property (key value)
"Set a buffer-level property at the top of an Org file.
If KEY already refers to a #+PROPERTY, replace it.
Otherwise, insert it at the end of the Org header lines."
(let ((property-line (format "#+PROPERTY: %s %s" key value))
(magic-re (rx (group line-start "#+PROPERTY:"
space (literal key) space
(zero-or-more any)
line-end))))
(defun process-line ()
"Helper function returns non-nil if more to process.
This works because `replace-match' and `insert' return nil,
while `forward-line' returns a non-nil value."
(cond
((looking-at magic-re) (replace-match property-line))
((looking-at "#") (forward-line))
(t (insert property-line ?\n))))
(save-excursion
(goto-char (point-min))
(while (process-line)))))
#+END_SRC
And we need the ability to read it. The Org API doesnt have a function to read it without parsing the entire structure, so we use the =org-collect-keywords= for the =PROPERTY= value, and then parse the results.
#+BEGIN_SRC emacs-lisp
(defun ha-org-get-buffer-property (key)
"Get the value of a buffer-level #+PROPERTY matching KEY."
(let* ((properties (thread-last "PROPERTY"
(list) ; Requires a list of keywords
(org-collect-keywords)
(car))) ; A list of lists? Get first entry
;; Each entry in properties is a string with the key and value:
(entry (seq-find (lambda (k) (s-starts-with? key k)) properties)))
(substring entry (1+ (s-index-of " " entry)))))
#+END_SRC
They work like:
#+BEGIN_SRC emacs-lisp :tangle no
(ha-org-set-buffer-property "foor" "bar")
(ha-org-get-buffer-property "foo") ; ⟹ "bar"
#+END_SRC
IMenu has a /mode-agnostic/ approach to jumping to /sections/. This could work whether the file is an formatted in org-mode, markdown, or even source code. This function allows me to jump to a particular header in either Org, Markdown, or other files that define the =defun= interface differently.
#+BEGIN_SRC emacs-lisp
(defun imenu-goto (header)
"Jump to the first `imenu' entry whose name contains HEADER.
Like `consult-imenu', this matches HEADER as a case-insensitive
substring rather than requiring an exact name, and jumps straight
to the first hit instead of prompting.
Note that this won't work if IMenu is stale and requires refreshing."
;; Overshadowing imenu's lookup function seems overly sneaky!
(let ((imenu-name-lookup-function
(lambda (str name)
(let ((case-fold-search t))
(string-match-p (regexp-quote str) name)))))
;; Calling `imenu' programmatically is a pain!
(when-let ((item (imenu--in-alist header (imenu--make-index-alist))))
(imenu item))))
#+END_SRC
* Code Notes
This function defines what the notes filename should look like, loads it in a side window, and adds a /back reference/ in the form of an org-mode =property=. Next it needs to either find the section that matches the function in the code we are writing about, or jumps to the bottom and creates it.
#+BEGIN_SRC emacs-lisp
(defun ha-code-notes ()
"Open an Org file based on the current opened file.
The pattern for choosing the name of the org file is:
foobar.py --> .foobar-notes.org
The Org header will be the name of the function in the original source
code file. This means you have one note section per function, which
should be fine in practice because functions are small and succinct,
right?"
(interactive)
(let* ((orig-file (buffer-file-name))
(line-num (line-number-at-pos))
(orig-parent (file-name-directory orig-file))
(orig-base (file-name-base orig-file))
;; Keep in mind the `orig-parent' has a final slash, so the
;; initial . here marks it as hidden:
(note-file (format "%s.%s-notes.org"
orig-parent orig-base))
(header (which-function)))
;; With the above local variables defined, we can open the file in
;; a window (pane) to the side:
(find-file-other-window note-file)
(ha-org-set-buffer-property "XREF" orig-file)
(goto-char (point-min)) ; jump to start of file
;; The `condition-case' is Elisp's way of a try..catch:
(condition-case nil
;; Find the first Org header that matches `header':
(re-search-forward (rx line-start
(one-or-more "*")
(one-or-more space)
(optional (or "=" "~"))
(literal header)
(optional (or "=" "~"))))
(error
;; We didn't find a header matching the function, so we jump to
;; the end of the file and create a new heading:
(goto-char (point-max))
(org-insert-heading)
(insert (format "%s" header))
(org-insert-property-drawer)
(org-set-property "XREF_LINE" (number-to-string line-num))
;; In case we want to change the section header name, we store
;; the name of the function that led us here as a property:
(org-set-property "XREF_NAME" header)
(when-let ((buf (find-buffer-visiting orig-file)))
(with-current-buffer buf
(ha-code--fringe-notes)))))
;; We are somewhere in the file, so go to the end of the block:
(org-end-of-subtree)
;; If the subtree ends mid-line, insert a newline:
(unless (eq (line-beginning-position) (point))
(end-of-line)
(insert "\n"))))
#+END_SRC
Use the builtin autoinsert feature to inject a basic template at the beginning of the notes file when we first create the notes file:
#+BEGIN_SRC emacs-lisp
(use-package autoinsert
:config
(define-auto-insert
(cons (rx "/." (one-or-more (not "/")) "-notes.org" string-end) "Org Notes Template")
'("Short description: "
"#+TITLE: "
(s-titleized-words (s-replace-regexp (rx (any "-" "_")) " "
(file-name-base (buffer-file-name))))
\n
"#+DATE:" (format-time-string "%Y-%m-%d %a")
\n
"#+LASTMOD:" (format-time-string "[%Y-%m-%d %a]")
\n \n)))
#+END_SRC
If we are /inside/ one of these note files, lets have a quick way to return back to the original “code” file:
#+BEGIN_SRC emacs-lisp
(defun ha-code-notes-return ()
"Return to the code referenced in the notes.
Essentially pretends we have a backlink without a database."
(interactive)
(let* ((filename (ha-org-get-buffer-property "XREF"))
(line-num (car (org-property-values "XREF_LINE")))
(function (or (car (org-property-values "XREF_NAME")) (which-function))))
;; If the property is set, load that file (which jumps to the
;; buffer if it is displayed), otherwise, we assume the previous
;; buffer contains it:
(if filename
(find-file-other-window filename)
(switch-to-prev-buffer))
(if line-num
(goto-line (string-to-number line-num)))
;; If the line number got out of sync so that the point is no
;; longer in the correct function, use `find-function' to
;; reposition the point:
(unless (equal (which-function) function)
(if (derived-mode-p 'prog-mode)
(xref-find-definitions function)
(imenu-goto function)))))
#+END_SRC
And give us keybinding that will either go to the notes (if we are in some code) or return to the source code (if we are in our notes):
#+BEGIN_SRC emacs-lisp
(defun ha-code-notes-dwim ()
"Open the notes buffer, or return to the code."
(interactive)
(when (buffer-file-name)
(if (string-match (rx "/." ; A hidden file
(one-or-more (not "/"))
"-notes.org" string-end)
(buffer-file-name))
(ha-code-notes-return)
(ha-code-notes))))
(ha-leader "n c" '("code notes" . ha-code-notes-dwim))
#+END_SRC
** Fringe Indicators for Notes
A code file with an associated notes file is easy to forget about. Let's mark, in the fringe, every line that has a note, so we notice it as we scroll past:
#+BEGIN_SRC emacs-lisp
(define-fringe-bitmap 'ha-code-notes-bitmap
[#b00001100
#b00010110
#b00010111
#b00101110
#b00101110
#b01011100
#b01011100
#b10110000
#b10010000
#b11100000]
nil nil 'center)
(defface ha-code-notes-face
'((t :foreground "yellow"))
"Face for the fringe marker indicating a line has an associated note.")
(defvar-local ha-code-notes-fringe-overlays nil
"Overlays marking lines in this buffer that have notes in the paired notes file.")
(defun ha-code-notes--fringe ()
"Mark, in the fringe, every line in this buffer that has a note.
Notes live in the paired `.BASE-notes.org' file (see
`ha-code-notes') as headings whose XREF/XREF_LINE
properties point back to a line in this file."
(mapc #'delete-overlay ha-code-notes--fringe-overlays)
(setq ha-code--notes--fringe-overlays nil)
(let* ((orig-file (buffer-file-name))
(note-file (and orig-file
(format "%s.%s-notes.org"
(file-name-directory orig-file)
(file-name-base orig-file))))
lines)
(when (and note-file (file-exists-p note-file))
(with-temp-buffer
(insert-file-contents note-file)
(org-mode)
(org-map-entries
(lambda ()
(when (equal (ha-org-get-buffer-property "XREF") orig-file)
(push (string-to-number (org-entry-get nil "XREF_LINE")) lines)))))
(dolist (line lines)
(save-excursion
(goto-char (point-min))
(forward-line (1- line))
(let ((ov (make-overlay (point) (point))))
(overlay-put ov 'before-string
(propertize "x" 'display
'(left-fringe ha-code-notes-bitmap
ha-code-notes-face)))
(push ov ha-code-notes--fringe-overlays)))))))
(add-hook 'find-file-hook #'ha-code-notes--fringe)
#+END_SRC
New notes and edited notes should refresh the markers too, so we hook into saving a notes file, and refresh right after inserting a new XREF property drawer:
#+BEGIN_SRC emacs-lisp
(defun ha-code-notes--fringe-notes-refresh-all ()
"Refresh fringe note markers in every buffer after saving a notes file."
(when (string-match (rx "-notes.org" string-end) (buffer-file-name))
(dolist (buf (buffer-list))
(with-current-buffer buf
(when buffer-file-name
(ha-code--fringe-notes))))))
(add-hook 'after-save-hook #'ha-code-notes--fringe-notes-refresh-all)
#+END_SRC
* Technical Artifacts :noexport:
Let's =provide= a name so we can =require= this file:
#+begin_src emacs-lisp :exports none
(provide 'ha-code-notes)
;;; ha-code-notes.el ends here
#+end_src
#+DESCRIPTION: configuring Emacs to take notes.
#+PROPERTY: header-args:sh :tangle no
#+PROPERTY: header-args:emacs-lisp :tangle yes
#+PROPERTY: header-args :results none :eval no-export :comments no mkdirp yes
#+OPTIONS: num:nil toc:nil todo:nil tasks:nil tags:nil date:nil
#+OPTIONS: skip:nil author:nil email:nil creator:nil timestamp:nil
#+INFOJS_OPT: view:nil toc:nil ltoc:t mouse:underline buttons:0 path:http://orgmode.org/org-info.js

View file

@ -957,16 +957,6 @@ I like the idea of dropping returnable bookmarks, but with /good defaults/ for t
(bookmark-set label))) (bookmark-set label)))
#+END_SRC #+END_SRC
The built-in behavior doesnt honor either /projects/ or /perspectives/, but I use [[https://codeberg.org/ideasman42/emacs-bookmark-in-project][bookmark-in-project]] package to make a =project=-specific bookmarks and use that to jump to only bookmarks in the current project.
#+BEGIN_SRC emacs-lisp
(use-package bookmark-in-project
:bind
(("C-x r m" . bookmark-in-project-toggle)
("C-x r M" . ha-bookmark-set)))
#+END_SRC
** Minor Keybinding Annoys ** Minor Keybinding Annoys
I like ~C-a~ to go to the beginning of the line, but what about getting to the beginning of text on that line? In Evil, you have ~^~ for beginning of line, and ~0~ for first text. Why not have ~C-a~ toggle between them both: I like ~C-a~ to go to the beginning of the line, but what about getting to the beginning of text on that line? In Evil, you have ~^~ for beginning of line, and ~0~ for first text. Why not have ~C-a~ toggle between them both:

View file

@ -360,10 +360,15 @@ And the collection of useful operations:
"b C-g" '(keyboard-escape-quit :which-key t)) "b C-g" '(keyboard-escape-quit :which-key t))
#+end_src #+end_src
* Bookmarks * Bookmarks
Expand on my [[file:ha-config.org::*Bookmarks][Bookmarks]] with the following key sequences: The built-in behavior of bookmarks doesnt honor either /projects/ or /perspectives/, but the [[https://codeberg.org/ideasman42/emacs-bookmark-in-project][bookmark-in-project]] package makes =project=-specific bookmarks.
This expands on my global [[file:ha-config.org::*Bookmarks][Bookmarks]]:
#+begin_src emacs-lisp #+begin_src emacs-lisp
(use-package bookmark-in-project (use-package bookmark-in-project
:bindd
(("C-x r m" . bookmark-in-project-toggle)
("C-x r M" . ha-bookmark-set))
:config :config
(ha-leader (ha-leader
;; Set or delete a bookmark associated with project: ;; Set or delete a bookmark associated with project:
@ -375,6 +380,31 @@ Expand on my [[file:ha-config.org::*Bookmarks][Bookmarks]] with the following ke
"b <down>" '("next mark" . bookmark-in-project-jump-next) "b <down>" '("next mark" . bookmark-in-project-jump-next)
"b <up>" '("previous mark" . bookmark-in-project-jump-previous))) "b <up>" '("previous mark" . bookmark-in-project-jump-previous)))
#+end_src #+end_src
** Annotations
Let's try [[https://github.com/bastibe/annotate.el][annotate-mode]], which allows you to drop "notes" displayed /inline/ with the buffer.
#+begin_src emacs-lisp
(use-package annotate
:config
(ha-leader
"t A" '("annotations" . annotate-mode)
"n" '(:ignore t :which-key "notes")
"n a" '("toggle mode" . annotate-mode)
"n n" '("annotate" . annotate-annotate)
"n d" '("delete" . annotate-delete-annotation)
;; Can't seem to get anything useful out of this function:
"n s" '("summary" . annotate-show-annotation-summary)
;; The next and previous only work for the current file:
"n j" '("next" . annotate-goto-next-annotation)
"n k" '("prev" . annotate-goto-previous-annotation)))
#+end_src
Keep the annotations simple, almost /tag-like/, and then the summary doesnt wrap. For longer notes, see my [[file:ha-capturing-notes.org::*Code Notes][Code Notes]] section.
The annotations are stored as s-expressions in the file, =~/.emacs.d/annotations=. Lets see how well this scales.
* Centering * Centering
After reading [[https://mbork.pl/2024-04-15_Improving_recenter-top-bottom_and_reposition-window][this essay]], I got to thinking that it would be nice to position the text in a buffer /near the top/, but show context based on some specific, textual /things/. My thought is to have a function that prompts for the thing (like the current paragraph, function, etc), but also create thing-specific functions. After reading [[https://mbork.pl/2024-04-15_Improving_recenter-top-bottom_and_reposition-window][this essay]], I got to thinking that it would be nice to position the text in a buffer /near the top/, but show context based on some specific, textual /things/. My thought is to have a function that prompts for the thing (like the current paragraph, function, etc), but also create thing-specific functions.

View file

@ -3,7 +3,7 @@
#+date: 2020-09-18 #+date: 2020-09-18
#+tags: emacs org #+tags: emacs org
#+startup: inlineimages #+startup: inlineimages
#+lastmod: [2026-04-08 Wed] #+lastmod: [2026-07-15 Wed]
A literate programming file for configuring org-mode and those files. A literate programming file for configuring org-mode and those files.
@ -65,6 +65,7 @@ Begin by initializing these org variables:
org-edit-src-content-indentation 2 ; Doom Emacs sets this to 0, org-edit-src-content-indentation 2 ; Doom Emacs sets this to 0,
; but uses a trick to make it ; but uses a trick to make it
; appear indented. ; appear indented.
org-use-sub-superscripts '{}
org-imenu-depth 4 org-imenu-depth 4
sentence-end-double-space nil ; I jump around by sentences, but seldom have two spaces. sentence-end-double-space nil ; I jump around by sentences, but seldom have two spaces.