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-literate.org"
|
||||||
"ha-org-clipboard.org"
|
"ha-org-clipboard.org"
|
||||||
"ha-capturing-notes.org"
|
"ha-capturing-notes.org"
|
||||||
"ha-code-notes.org"
|
|
||||||
"ha-agendas.org"
|
"ha-agendas.org"
|
||||||
"ha-data.org"
|
"ha-data.org"
|
||||||
"ha-passwords.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-org-publishing.org"
|
||||||
"ha-email.org"
|
"ha-email.org"
|
||||||
"ha-aux-apps.org"))
|
"ha-aux-apps.org"))
|
||||||
|
"ha-code-notes.org"
|
||||||
"ha-dashboard.org"))
|
"ha-dashboard.org"))
|
||||||
"List of org files that complete the hamacs project.")
|
"List of org files that complete the hamacs project.")
|
||||||
#+end_src
|
#+end_src
|
||||||
|
|
@ -241,7 +241,7 @@ The list of /hamacs/ org-formatted files stored in =ha-hamacs-files= is selectiv
|
||||||
(if (not all)
|
(if (not all)
|
||||||
ha-hamacs-files
|
ha-hamacs-files
|
||||||
|
|
||||||
(thread-last (rx ".org" string-end)
|
(thread-last (rx (not ".") (+? any) ".org" string-end)
|
||||||
(directory-files hamacs-source-dir nil)
|
(directory-files hamacs-source-dir nil)
|
||||||
(append ha-hamacs-files)
|
(append ha-hamacs-files)
|
||||||
(--filter (not (string-match (rx "README") it)))
|
(--filter (not (string-match (rx "README") it)))
|
||||||
|
|
|
||||||
|
|
@ -158,7 +158,7 @@ And point Emacs to it:
|
||||||
|
|
||||||
(setq agent-shell-anthropic-claude-acp-command
|
(setq agent-shell-anthropic-claude-acp-command
|
||||||
(if (file-exists-p "/opt/homebrew")
|
(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"))))
|
'("/usr/local/bin/claude-agent-acp"))))
|
||||||
#+END_SRC
|
#+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]].
|
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.
|
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.
|
# TODO Create custom settings to override default behavior.
|
||||||
|
|
@ -109,17 +125,18 @@ This was far gnarlier than I originally thought, since IMenu only wants to /inte
|
||||||
|
|
||||||
(file qualified-name offset)"
|
(file qualified-name offset)"
|
||||||
(let* ((here (point))
|
(let* ((here (point))
|
||||||
(entry (thread-last (imenu--make-index-alist t)
|
(entry (ignore-errors
|
||||||
;; Flatten hierarchy labels with dot separators:
|
(thread-last (imenu--make-index-alist t)
|
||||||
(ha-code-notes-location--flatten-index)
|
;; Flatten hierarchy labels with dot separators:
|
||||||
;; Filter to only entries that occur after point:
|
(ha-code-notes-location--flatten-index)
|
||||||
(seq-filter (lambda (e) (<= (cdr e) here)))
|
;; Filter to only entries that occur after point:
|
||||||
;; Sort the remaining entries by position
|
(seq-filter (lambda (e) (<= (cdr e) here)))
|
||||||
;; (since out flatten-index function
|
;; Sort the remaining entries by position
|
||||||
;; doesn't guarantee the order):
|
;; (since out flatten-index function
|
||||||
(seq-sort-by #'cdr #'>)
|
;; doesn't guarantee the order):
|
||||||
;; And the first entry on the list is our goal:
|
(seq-sort-by #'cdr #'>)
|
||||||
car)))
|
;; And the first entry on the list is our goal:
|
||||||
|
car))))
|
||||||
(unless entry
|
(unless entry
|
||||||
(error "No enclosing definition found for point"))
|
(error "No enclosing definition found for point"))
|
||||||
(list (buffer-file-name)
|
(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
|
#+BEGIN_SRC emacs-lisp
|
||||||
(defun ha-code-notes-location--flatten-index (alist &optional prefix)
|
(defun ha-code-notes-location--flatten-index (alist &optional prefix)
|
||||||
"Flatten imenu ALIST into a list of (QUALIFIED-NAME . POSITION).
|
"Flatten imenu ALIST into a list of (NAME . POSITION).
|
||||||
Where the qualified-name matches what which-function/imenu expect.
|
Where the name matches what which-function/imenu expect.
|
||||||
|
|
||||||
For instance:
|
For instance:
|
||||||
|
|
||||||
|
|
@ -142,47 +159,115 @@ The needed /trick/ is to flatten this IMenu hierarchy using this recursive beaut
|
||||||
|
|
||||||
((\"foo_class.method_one\" ...) (\"foo_class.method_two\" ...)...)
|
((\"foo_class.method_one\" ...) (\"foo_class.method_two\" ...)...)
|
||||||
|
|
||||||
This is called recursively, so PREFIX could be a class name, or
|
Called recursively, where PREFIX could be a class name, or other higher
|
||||||
other higher abstraction."
|
abstraction when the nesting represents a real outline (Org, Markdown).
|
||||||
;; Like `mapconcat' but lets us concat into something other than a string.
|
|
||||||
;; Note that `mapcan' uses `nconc' which _mutates_ the `alist'.
|
Entries tagged with `imenu-kind' or `breadcrumb-kind' property (as Eglot
|
||||||
;; This is fine as a parameter that we then return:
|
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
|
(mapcan
|
||||||
(lambda (entry)
|
(lambda (entry)
|
||||||
;; Some imenu backends (e.g. python.el) annotate names with their
|
(unless (or (null entry) (atom entry))
|
||||||
;; category, e.g. "Pair (class)" or "sum (def)". Strip that so
|
(let* ((raw-name (car entry))
|
||||||
;; names match what `which-function'/`add-log-current-defun'
|
(kind (and (stringp raw-name)
|
||||||
;; produce, and what gets stored as XREF_NAME in notes files.
|
(or (get-text-property 0 'imenu-kind raw-name)
|
||||||
(let* ((name (replace-regexp-in-string
|
(get-text-property 0 'breadcrumb-kind raw-name))))
|
||||||
(rx space "(" (one-or-more alpha) ")" string-end)
|
(name (if (stringp raw-name)
|
||||||
"" (car entry)))
|
(replace-regexp-in-string
|
||||||
(value (cdr entry))
|
(rx space "(" (one-or-more alpha) ")" string-end)
|
||||||
(qualified (if prefix (format "%s.%s" prefix name) name)))
|
"" (substring-no-properties raw-name))
|
||||||
(if (listp value)
|
(format "%s" raw-name)))
|
||||||
(ha-code-notes-location--flatten-index value qualified)
|
(value (cdr entry))
|
||||||
(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))
|
alist))
|
||||||
#+END_SRC
|
#+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/:
|
Once inside a source file, we can jump to one of these /relative locations/:
|
||||||
|
|
||||||
#+BEGIN_SRC emacs-lisp
|
#+BEGIN_SRC emacs-lisp
|
||||||
(defun ha-code-notes-location-goto (file name offset)
|
(defun ha-code-notes-location-goto (file name offset)
|
||||||
"Move point to the position described by relative location.
|
"Move point to the position described by relative location.
|
||||||
|
|
||||||
FILE is the filename to load, may be nil to use current buffer.
|
FILE is the filename to load, may be nil to use current buffer.
|
||||||
NAME is a function name or other definition, e.g. ClassName.method
|
NAME is a function name or other definition, e.g. ClassName.method
|
||||||
OFFSET is the number of lines below NAME to position the point."
|
OFFSET is the number of lines below NAME to position the point."
|
||||||
(when (and file (not (equal file (buffer-file-name))))
|
(when (and file (not (equal file (buffer-file-name))))
|
||||||
(find-file-other-window file))
|
(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)))
|
(let* ((index (ha-code-notes-location--flatten-index (imenu--make-index-alist t)))
|
||||||
(entry (assoc name index)))
|
(entry (assoc name index)))
|
||||||
(if (not entry)
|
(when entry
|
||||||
(message "Definition `%s' no longer found in %s" name file)
|
|
||||||
(goto-char (cdr entry))
|
(goto-char (cdr entry))
|
||||||
(when offset
|
(when offset
|
||||||
(forward-line offset)))))
|
(forward-line offset)))))
|
||||||
#+END_SRC
|
#+END_SRC
|
||||||
|
|
||||||
To verify that this works:
|
To verify that this works:
|
||||||
|
|
@ -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
|
;; Keep in mind the `orig-parent' has a final slash, so the
|
||||||
;; initial . here marks it as hidden:
|
;; initial . here marks it as hidden:
|
||||||
(note-file (format "%s.%s-notes.org"
|
(note-file (ha-code-notes-file orig-base orig-parent))
|
||||||
orig-parent orig-base))
|
|
||||||
(header (which-function))
|
(header (which-function))
|
||||||
(offset (ha-code-notes-defun-rel-line)))
|
(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
|
(use-package autoinsert
|
||||||
:config
|
:config
|
||||||
(define-auto-insert
|
(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: "
|
'("Short description: "
|
||||||
"#+TITLE: "
|
"#+TITLE: "
|
||||||
(s-titleized-words (s-replace-regexp (rx (any "-" "_")) " "
|
(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."
|
"Open the notes buffer, or return to the code."
|
||||||
(interactive)
|
(interactive)
|
||||||
(when (buffer-file-name)
|
(when (buffer-file-name)
|
||||||
(if (string-match (rx "/." ; A hidden file
|
(if (string-match ha-code-notes-rx (buffer-file-name))
|
||||||
(one-or-more (not "/"))
|
|
||||||
"-notes.org" string-end)
|
|
||||||
(buffer-file-name))
|
|
||||||
(ha-code-notes-return)
|
(ha-code-notes-return)
|
||||||
(ha-code-notes))))
|
(ha-code-notes))))
|
||||||
|
|
||||||
|
|
@ -472,22 +553,27 @@ A code file with an associated notes file is easy to forget about. Let's mark, i
|
||||||
|
|
||||||
(defun ha-code-notes--fringe ()
|
(defun ha-code-notes--fringe ()
|
||||||
"Mark, in the fringe, every line in this buffer that has a note.
|
"Mark, in the fringe, every line in this buffer that has a note.
|
||||||
Notes live in the paired `.BASE-notes.org' file (see
|
Notes live in the paired `.BASE-notes.org' file (see
|
||||||
`ha-code-notes') as headings whose XREF property matches this
|
`ha-code-notes') as headings whose XREF property matches this
|
||||||
file, XREF_NAME names the function (inherited by subheadings from
|
file, XREF_NAME names the function (inherited by subheadings from
|
||||||
their parent), and XREF_LINE is a line offset from that function's
|
their parent), and XREF_LINE is a line offset from that function's
|
||||||
start, computed by `ha-code-notes-defun-rel-line'. We resolve each
|
start, computed by `ha-code-notes-defun-rel-line'. We resolve each
|
||||||
pair back to an absolute line via `ha-code-notes-location-goto', since the function
|
pair back to an absolute line via `ha-code-notes-location-goto', since the function
|
||||||
may have moved since the note was taken."
|
may have moved since the note was taken."
|
||||||
(mapc #'delete-overlay ha-code-notes--fringe-overlays)
|
(mapc #'delete-overlay ha-code-notes--fringe-overlays)
|
||||||
|
|
||||||
(setq ha-code-notes--fringe-overlays nil)
|
(setq ha-code-notes--fringe-overlays nil)
|
||||||
(let* ((orig-file (buffer-file-name))
|
(let* ((orig-file (buffer-file-name))
|
||||||
(note-file (and orig-file
|
(note-file (and orig-file
|
||||||
(format "%s.%s-notes.org"
|
(ha-code-notes-file
|
||||||
(file-name-directory orig-file)
|
(file-name-directory orig-file)
|
||||||
(file-name-base orig-file))))
|
(file-name-base orig-file))))
|
||||||
notes)
|
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
|
(with-temp-buffer
|
||||||
(insert-file-contents note-file)
|
(insert-file-contents note-file)
|
||||||
(org-mode)
|
(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
|
#+BEGIN_SRC emacs-lisp
|
||||||
(defun ha-code-notes--fringe-notes-refresh-all ()
|
(defun ha-code-notes--fringe-notes-refresh-all ()
|
||||||
"Refresh fringe note markers in every buffer after saving a notes file."
|
"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))
|
(dolist (buf (buffer-list))
|
||||||
(with-current-buffer buf
|
(with-current-buffer buf
|
||||||
(when buffer-file-name
|
(when buffer-file-name
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue