diff --git a/ha-applications.org b/ha-applications.org index 5fc7127..8b68f69 100644 --- a/ha-applications.org +++ b/ha-applications.org @@ -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 ",") 'major-mode-hydras/dired-mode/body) #+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 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 diff --git a/ha-capturing-notes.org b/ha-capturing-notes.org index b273fed..5ae403a 100644 --- a/ha-capturing-notes.org +++ b/ha-capturing-notes.org @@ -1,7 +1,7 @@ -#+title: Capturing Notes with Org -#+author: Howard X. Abrams -#+date: 2020-09-18 -#+tags: emacs org +#+TITLE: Capturing Notes with Org +#+AUTHOR: Howard X. Abrams +#+DATE: 2020-09-18 +#+TAGS: emacs org A literate programming file for configuring org for capturing notes. diff --git a/ha-code-notes.org b/ha-code-notes.org new file mode 100644 index 0000000..c4668e2 --- /dev/null +++ b/ha-code-notes.org @@ -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 + ;; 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, I’ve 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 doesn’t 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, let’s 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 diff --git a/ha-config.org b/ha-config.org index c8664cf..7bed10d 100644 --- a/ha-config.org +++ b/ha-config.org @@ -957,16 +957,6 @@ I like the idea of dropping returnable bookmarks, but with /good defaults/ for t (bookmark-set label))) #+END_SRC -The built-in behavior doesn’t 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 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: diff --git a/ha-general.org b/ha-general.org index e67889f..c57660a 100644 --- a/ha-general.org +++ b/ha-general.org @@ -360,10 +360,15 @@ And the collection of useful operations: "b C-g" '(keyboard-escape-quit :which-key t)) #+end_src * Bookmarks -Expand on my [[file:ha-config.org::*Bookmarks][Bookmarks]] with the following key sequences: +The built-in behavior of bookmarks doesn’t 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 (use-package bookmark-in-project + :bindd + (("C-x r m" . bookmark-in-project-toggle) + ("C-x r M" . ha-bookmark-set)) :config (ha-leader ;; 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 " '("next mark" . bookmark-in-project-jump-next) "b " '("previous mark" . bookmark-in-project-jump-previous))) #+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 doesn’t 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=. Let’s see how well this scales. * 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. diff --git a/ha-org.org b/ha-org.org index 36723eb..4078a06 100644 --- a/ha-org.org +++ b/ha-org.org @@ -3,7 +3,7 @@ #+date: 2020-09-18 #+tags: emacs org #+startup: inlineimages -#+lastmod: [2026-04-08 Wed] +#+lastmod: [2026-07-15 Wed] 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, ; but uses a trick to make it ; appear indented. + org-use-sub-superscripts '{} org-imenu-depth 4 sentence-end-double-space nil ; I jump around by sentences, but seldom have two spaces.