Fix critical bugs in my ha-code-note project
This commit is contained in:
parent
d9f3ec8806
commit
97e9b86026
3 changed files with 144 additions and 58 deletions
|
|
@ -210,7 +210,6 @@ The following /defines/ the rest of my org-mode literate files, that I load late
|
|||
"ha-org-literate.org"
|
||||
"ha-org-clipboard.org"
|
||||
"ha-capturing-notes.org"
|
||||
"ha-code-notes.org"
|
||||
"ha-agendas.org"
|
||||
"ha-data.org"
|
||||
"ha-passwords.org"
|
||||
|
|
@ -230,6 +229,7 @@ The following /defines/ the rest of my org-mode literate files, that I load late
|
|||
"ha-org-publishing.org"
|
||||
"ha-email.org"
|
||||
"ha-aux-apps.org"))
|
||||
"ha-code-notes.org"
|
||||
"ha-dashboard.org"))
|
||||
"List of org files that complete the hamacs project.")
|
||||
#+end_src
|
||||
|
|
@ -241,7 +241,7 @@ The list of /hamacs/ org-formatted files stored in =ha-hamacs-files= is selectiv
|
|||
(if (not all)
|
||||
ha-hamacs-files
|
||||
|
||||
(thread-last (rx ".org" string-end)
|
||||
(thread-last (rx (not ".") (+? any) ".org" string-end)
|
||||
(directory-files hamacs-source-dir nil)
|
||||
(append ha-hamacs-files)
|
||||
(--filter (not (string-match (rx "README") it)))
|
||||
|
|
|
|||
|
|
@ -158,7 +158,7 @@ And point Emacs to it:
|
|||
|
||||
(setq agent-shell-anthropic-claude-acp-command
|
||||
(if (file-exists-p "/opt/homebrew")
|
||||
(list (file-expand-wildcards "/opt/homebrew/bin/claude-agent-acp"))
|
||||
(file-expand-wildcards "/opt/homebrew/bin/claude-agent-acp")
|
||||
'("/usr/local/bin/claude-agent-acp"))))
|
||||
#+END_SRC
|
||||
|
||||
|
|
|
|||
|
|
@ -63,6 +63,22 @@ This allows me to /wax poetic/ with a parallel, but separate org file. Of course
|
|||
|
||||
If this isn’t good for you, check out [[https://github.com/agoodman42/montag][Montag]]. Same project, different approach using a single directory for all notes with [[https://www.orgroam.com/][org-roam]].
|
||||
|
||||
Let’s create a pattern for what a notes file is, and how to recognize them:
|
||||
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
(defconst ha-code-notes-rx (rx (optional "/")
|
||||
"." (one-or-more (not "/")) "-notes.org" string-end))
|
||||
|
||||
(defun ha-code-notes-file (file &optional parent)
|
||||
"Return a FILE's 'code notes' filename.
|
||||
Prepend the PARENT if given."
|
||||
(cond
|
||||
((and parent (string-match-p (rx "/" string-end) parent))
|
||||
(format "%s.%s-notes.org" parent file))
|
||||
(parent (format "%s/.%s-notes.org" parent file))
|
||||
(t (format ".%s-notes.org" file))))
|
||||
#+END_SRC
|
||||
|
||||
Another feature when looking at the notes in the org buffer, is to hit the same key to return to the source code file referencing the notes.
|
||||
|
||||
# TODO Create custom settings to override default behavior.
|
||||
|
|
@ -109,7 +125,8 @@ This was far gnarlier than I originally thought, since IMenu only wants to /inte
|
|||
|
||||
(file qualified-name offset)"
|
||||
(let* ((here (point))
|
||||
(entry (thread-last (imenu--make-index-alist t)
|
||||
(entry (ignore-errors
|
||||
(thread-last (imenu--make-index-alist t)
|
||||
;; Flatten hierarchy labels with dot separators:
|
||||
(ha-code-notes-location--flatten-index)
|
||||
;; Filter to only entries that occur after point:
|
||||
|
|
@ -119,7 +136,7 @@ This was far gnarlier than I originally thought, since IMenu only wants to /inte
|
|||
;; doesn't guarantee the order):
|
||||
(seq-sort-by #'cdr #'>)
|
||||
;; And the first entry on the list is our goal:
|
||||
car)))
|
||||
car))))
|
||||
(unless entry
|
||||
(error "No enclosing definition found for point"))
|
||||
(list (buffer-file-name)
|
||||
|
|
@ -131,8 +148,8 @@ The needed /trick/ is to flatten this IMenu hierarchy using this recursive beaut
|
|||
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
(defun ha-code-notes-location--flatten-index (alist &optional prefix)
|
||||
"Flatten imenu ALIST into a list of (QUALIFIED-NAME . POSITION).
|
||||
Where the qualified-name matches what which-function/imenu expect.
|
||||
"Flatten imenu ALIST into a list of (NAME . POSITION).
|
||||
Where the name matches what which-function/imenu expect.
|
||||
|
||||
For instance:
|
||||
|
||||
|
|
@ -142,28 +159,87 @@ The needed /trick/ is to flatten this IMenu hierarchy using this recursive beaut
|
|||
|
||||
((\"foo_class.method_one\" ...) (\"foo_class.method_two\" ...)...)
|
||||
|
||||
This is called recursively, so PREFIX could be a class name, or
|
||||
other higher abstraction."
|
||||
;; Like `mapconcat' but lets us concat into something other than a string.
|
||||
;; Note that `mapcan' uses `nconc' which _mutates_ the `alist'.
|
||||
;; This is fine as a parameter that we then return:
|
||||
Called recursively, where PREFIX could be a class name, or other higher
|
||||
abstraction when the nesting represents a real outline (Org, Markdown).
|
||||
|
||||
Entries tagged with `imenu-kind' or `breadcrumb-kind' property (as Eglot
|
||||
nests a function's local variables for Python) are already a flat
|
||||
namespace of functions/variables/constants, so their own names are kept
|
||||
bare and not qualified by an enclosing function's name."
|
||||
(mapcan
|
||||
(lambda (entry)
|
||||
;; Some imenu backends (e.g. python.el) annotate names with their
|
||||
;; category, e.g. "Pair (class)" or "sum (def)". Strip that so
|
||||
;; names match what `which-function'/`add-log-current-defun'
|
||||
;; produce, and what gets stored as XREF_NAME in notes files.
|
||||
(let* ((name (replace-regexp-in-string
|
||||
(unless (or (null entry) (atom entry))
|
||||
(let* ((raw-name (car entry))
|
||||
(kind (and (stringp raw-name)
|
||||
(or (get-text-property 0 'imenu-kind raw-name)
|
||||
(get-text-property 0 'breadcrumb-kind raw-name))))
|
||||
(name (if (stringp raw-name)
|
||||
(replace-regexp-in-string
|
||||
(rx space "(" (one-or-more alpha) ")" string-end)
|
||||
"" (car entry)))
|
||||
"" (substring-no-properties raw-name))
|
||||
(format "%s" raw-name)))
|
||||
(value (cdr entry))
|
||||
(qualified (if prefix (format "%s.%s" prefix name) name)))
|
||||
(if (listp value)
|
||||
(ha-code-notes-location--flatten-index value qualified)
|
||||
(list (cons qualified (if (markerp value) (marker-position value) value))))))
|
||||
(qualified (if (and prefix (not kind)) (format "%s.%s" prefix name) name)))
|
||||
(cond
|
||||
;; Case 1: Nested submenu (list of alist entries). Eglot
|
||||
;; never gives the container itself a position among its
|
||||
;; children, as Org does; it stashes it in the
|
||||
;; `imenu-region' text property instead, so recover it
|
||||
;; here. Children only inherit QUALIFIED as their prefix
|
||||
;; when this entry has no `imenu-kind' of its own -- i.e.
|
||||
;; when the nesting is a real outline, not Eglot's flat
|
||||
;; function/variable/constant tagging.
|
||||
((and (listp value) (consp (car value)))
|
||||
(append
|
||||
(let ((region (and (stringp raw-name)
|
||||
(get-text-property 0 'imenu-region raw-name))))
|
||||
(when region (list (cons qualified (car region)))))
|
||||
(ha-code-notes-location--flatten-index value (unless kind qualified))))
|
||||
|
||||
;; Case 2: Direct marker or integer position
|
||||
((integer-or-marker-p value)
|
||||
(list (cons qualified (if (markerp value) (marker-position value) value))))
|
||||
|
||||
;; Case 3: Overlay or element containing a position in car/cdr
|
||||
((overlayp value)
|
||||
(list (cons qualified (overlay-start value))))
|
||||
|
||||
((and (consp value) (integer-or-marker-p (car value)))
|
||||
(list (cons qualified (if (markerp (car value))
|
||||
(marker-position (car value))
|
||||
(car value)))))
|
||||
|
||||
;; Fallback: ignore non-positional Imenu metadata entries
|
||||
(t nil)))))
|
||||
alist))
|
||||
#+END_SRC
|
||||
|
||||
This flatten feature has been a pain in my side. An org-mode formatted file works well, for instance, running the following on this document:
|
||||
#+BEGIN_SRC emacs-lisp :tangle no :results replace value raw :export both :wrap example
|
||||
(ha-code-notes-location--flatten-index (imenu--make-index-alist t))
|
||||
#+END_SRC
|
||||
|
||||
#+RESULTS:
|
||||
#+begin_example
|
||||
((*Rescan* . -99) (Introduction . 946) (Helper Functions.Relative Code Locations . 3719) (Helper Functions.Org Properties . 13220) (Helper Functions . 3592) (Write a Note.Return to Source Code . 22408) (Write a Note.Fringe Indicators for Notes . 24001) (Write a Note . 17005) (Technical Artifacts . 27464))
|
||||
#+end_example
|
||||
|
||||
But running the same expression on a Python file (via Eglot) nests the local variables and parameters of each function as children in the index, each tagged with an =imenu-kind= text property on its name, e.g. a function like this:
|
||||
|
||||
#+begin_example
|
||||
(#("parse_args" 0 10 (imenu-region (1245 . 1627) imenu-kind "Function" breadcrumb-region (1245 . 1627) breadcrumb-kind "Function"))
|
||||
(#("argv" 0 4 (imenu-region (1260 . 1289) imenu-kind "Variable" ...)) . 1260)
|
||||
(#("parser" 0 6 (imenu-region (1365 . 1371) imenu-kind "Variable" ...)) . 1365))
|
||||
#+end_example
|
||||
|
||||
Naively flattening this treats =argv= and =parser= as if they were nested methods, producing useless entries like ~parse_args.argv~. Worse, the function itself never had its own position among its children (unlike Org, which gives a parent headline a self-pointing entry), so there was no way to jump straight to ~parse_args~ at all.
|
||||
|
||||
The fix: the presence of an =imenu-kind=/=breadcrumb-kind= property means the nesting is Eglot's flat function/variable/constant tagging, not a real outline -- so a child's name is never qualified by its parent's. When recursing into a nested submenu, the container's own position is recovered from its =imenu-region= text property, and the name is stripped of text properties. That turns the above into clean, bare entries for every name -- function, variable, and constant alike:
|
||||
|
||||
#+begin_example
|
||||
((parse_args . 1245) (argv . 1260) (parser . 1365) (registry_credentials . 1630) ...)
|
||||
#+end_example
|
||||
|
||||
Once inside a source file, we can jump to one of these /relative locations/:
|
||||
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
|
|
@ -176,10 +252,19 @@ Once inside a source file, we can jump to one of these /relative locations/:
|
|||
(when (and file (not (equal file (buffer-file-name))))
|
||||
(find-file-other-window file))
|
||||
|
||||
(condition-case t
|
||||
(ha-code-notes--location-goto name offset)
|
||||
(message "Definition `%s' no longer found in %s" name file))
|
||||
|
||||
(ignore-errors
|
||||
))
|
||||
|
||||
(defun ha-code-notes--location-goto (name &optional offset)
|
||||
"Use `imenu' interface to goto NAME location.
|
||||
If OFFSET given, move that many lines below."
|
||||
(let* ((index (ha-code-notes-location--flatten-index (imenu--make-index-alist t)))
|
||||
(entry (assoc name index)))
|
||||
(if (not entry)
|
||||
(message "Definition `%s' no longer found in %s" name file)
|
||||
(when entry
|
||||
(goto-char (cdr entry))
|
||||
(when offset
|
||||
(forward-line offset)))))
|
||||
|
|
@ -297,8 +382,7 @@ This function defines what the notes filename should look like, loads it in a si
|
|||
|
||||
;; 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))
|
||||
(note-file (ha-code-notes-file orig-base orig-parent))
|
||||
(header (which-function))
|
||||
(offset (ha-code-notes-defun-rel-line)))
|
||||
|
||||
|
|
@ -322,7 +406,7 @@ Use the builtin autoinsert feature to inject a basic template at the beginning o
|
|||
(use-package autoinsert
|
||||
:config
|
||||
(define-auto-insert
|
||||
(cons (rx "/." (one-or-more (not "/")) "-notes.org" string-end) "Org Notes Template")
|
||||
(cons ha-code-notes-rx "Org Notes Template")
|
||||
'("Short description: "
|
||||
"#+TITLE: "
|
||||
(s-titleized-words (s-replace-regexp (rx (any "-" "_")) " "
|
||||
|
|
@ -435,10 +519,7 @@ And give us keybinding that will either go to the notes (if we are in some code)
|
|||
"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))
|
||||
(if (string-match ha-code-notes-rx (buffer-file-name))
|
||||
(ha-code-notes-return)
|
||||
(ha-code-notes))))
|
||||
|
||||
|
|
@ -480,14 +561,19 @@ A code file with an associated notes file is easy to forget about. Let's mark, i
|
|||
pair back to an absolute line via `ha-code-notes-location-goto', since the function
|
||||
may have moved since the note was taken."
|
||||
(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"
|
||||
(ha-code-notes-file
|
||||
(file-name-directory orig-file)
|
||||
(file-name-base orig-file))))
|
||||
notes)
|
||||
(when (and note-file (file-exists-p note-file))
|
||||
;; Only run if this is an actual file AND its notes file exists:
|
||||
(when (and orig-file
|
||||
note-file
|
||||
(file-exists-p note-file)
|
||||
(not (string-match-p ha-code-notes-rx orig-file)))
|
||||
(with-temp-buffer
|
||||
(insert-file-contents note-file)
|
||||
(org-mode)
|
||||
|
|
@ -516,7 +602,7 @@ New notes and edited notes should refresh the markers too, so we hook into savin
|
|||
#+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))
|
||||
(when (string-match ha-code-notes-rx (buffer-file-name))
|
||||
(dolist (buf (buffer-list))
|
||||
(with-current-buffer buf
|
||||
(when buffer-file-name
|
||||
|
|
|
|||
Loading…
Reference in a new issue