diff options
| author | Craig Jennings <c@cjennings.net> | 2026-07-21 07:27:58 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-07-21 07:27:58 -0500 |
| commit | a30074db7a7ca06e1baf312928076130be37e676 (patch) | |
| tree | f4fa4dd6ca6e1429fd88769edf66d47074379093 /org-drill.el | |
| parent | 24f8c5aac8873837824ca6d61e566e3e24c96ad5 (diff) | |
| download | org-drill-a30074db7a7ca06e1baf312928076130be37e676.tar.gz org-drill-a30074db7a7ca06e1baf312928076130be37e676.zip | |
With the default file scope, the dashboard rendered and exported from its own buffer, so every count read zero and cards.csv came out empty. The render, refresh, cycle, and CSV-export paths now run their card scans in the deck buffer the command was invoked from, stored buffer-locally, and a dead deck signals a clear user-error instead of scanning the wrong buffer.
Card links are now a registered org-drill-card link type with a follow handler, and the link path carries the file identity so RET jumps work across files. Eleven new ERT tests cover the render, refresh, export, link, and dead-buffer paths.
Diffstat (limited to 'org-drill.el')
| -rw-r--r-- | org-drill.el | 127 |
1 files changed, 93 insertions, 34 deletions
diff --git a/org-drill.el b/org-drill.el index 11f161e..8c895b6 100644 --- a/org-drill.el +++ b/org-drill.el @@ -5003,9 +5003,11 @@ an integer (0 when absent). AVG-QUALITY is DRILL_AVERAGE_QUALITY as a float, or nil when absent. DAYS-SINCE-REVIEW is the integer day count since DRILL_LAST_REVIEWED, or nil when never reviewed. DAYS-SINCE-ADDED is the integer day count since DATE_ADDED, or nil when absent. -TOTAL-REPEATS is DRILL_TOTAL_REPEATS as an integer (0 when absent)." +TOTAL-REPEATS is DRILL_TOTAL_REPEATS as an integer (0 when absent). +FILE is the file the entry's buffer visits, or nil for a fileless +buffer; card links carry it so the dashboard can jump across files." heading pos failure-count avg-quality - days-since-review days-since-added total-repeats) + days-since-review days-since-added total-repeats file) ;; Short aliases for the long struct accessors, used by the sort ;; comparators below so the comparator lines stay readable. @@ -5057,7 +5059,8 @@ via `org-get-heading', so the point must be on a drill heading." (org-drill-statistics--days-since-org-timestamp last-raw today-day) :days-since-added (org-drill-statistics--days-since-org-timestamp added-raw today-day) - :total-repeats (if repeats-raw (string-to-number repeats-raw) 0)))) + :total-repeats (if repeats-raw (string-to-number repeats-raw) 0) + :file (buffer-file-name)))) (defun org-drill-statistics--days-since-org-timestamp (timestamp today-day) "Return integer days from org TIMESTAMP string to TODAY-DAY. @@ -5266,25 +5269,51 @@ with no SCHEDULED time are ignored." ;; each scan and cap) gives the pre-cap totals needed for the "+N more" ;; footers for free. -(defun org-drill-statistics--card-link (heading pos) +(defun org-drill-statistics--card-link (heading pos &optional file) "Return an org bracket link to a drill card heading. HEADING is the card's outline heading string, used as the link -description. POS is the integer buffer position of the heading, carried -in the link path so the dashboard's RET handler can jump to the card. -The path has the form \"org-drill-card:POS\". Any closing bracket in -HEADING is replaced so a literal \"]]\" cannot terminate the link early. -An empty or nil HEADING falls back to a position-based description." +description. POS is the integer buffer position of the heading. FILE +is the file the card's buffer visits, or nil for a fileless buffer. +The path has the form \"org-drill-card:POS@FILE\" (FILE may be empty), +so the follow handler can jump to the card even across files. Any +closing bracket in HEADING is replaced so a literal \"]]\" cannot +terminate the link early. An empty or nil HEADING falls back to a +position-based description." (let* ((desc (if (and heading (not (string-empty-p heading))) heading (format "card at %d" pos))) (safe (replace-regexp-in-string "]" "}" desc))) - (format "[[org-drill-card:%d][%s]]" pos safe))) + (format "[[org-drill-card:%d@%s][%s]]" pos (or file "") safe))) + +(defun org-drill-statistics--follow-card-link (path &optional _prefix) + "Follow an org-drill-card link to its card. +PATH is \"POS@FILE\" (FILE may be empty) or a legacy bare \"POS\". +With a file, visit it in another window and move point to POS. Without +one, fall back to the dashboard's stored source buffer. Signals a +`user-error' when neither identifies a live target." + (let* ((at (string-match "@" path)) + (pos (string-to-number (if at (substring path 0 at) path))) + (file (and at (substring path (1+ at)))) + (source (and (boundp 'org-drill-statistics--source-buffer) + org-drill-statistics--source-buffer))) + (cond + ((and file (not (string-empty-p file))) + (find-file-other-window file) + (goto-char (min (max 1 pos) (point-max)))) + ((buffer-live-p source) + (pop-to-buffer source) + (goto-char (min (max 1 pos) (point-max)))) + (t (user-error + "Card buffer is gone; refresh the dashboard from a deck buffer"))))) + +(org-link-set-parameters "org-drill-card" + :follow #'org-drill-statistics--follow-card-link) (defun org-drill-statistics--render-attention-table (title rows total empty-note) "Render one needs-attention subsection as an org string. TITLE is the third-level heading text for the subsection. ROWS is a -list of (HEADING . POS) cons cells, already capped at +list of (HEADING POS FILE) lists, already capped at `org-drill-statistics-attention-row-limit' and already sorted. TOTAL is the full count of matching cards before capping, used for the \"+N more\" footer. EMPTY-NOTE is the line shown when ROWS is empty. @@ -5304,7 +5333,8 @@ the number of rows shown." (mapconcat (lambda (row) (format "| %s |" - (org-drill-statistics--card-link (car row) (cdr row)))) + (org-drill-statistics--card-link + (nth 0 row) (nth 1 row) (nth 2 row)))) rows "\n") "\n" @@ -5368,8 +5398,9 @@ pre-cap totals are available for the footers without rescanning." (lambda (structs) (mapcar (lambda (d) - (cons (org-drill-statistics--entry-attention-data-heading d) - (org-drill-statistics--entry-attention-data-pos d))) + (list (org-drill-statistics--entry-attention-data-heading d) + (org-drill-statistics--entry-attention-data-pos d) + (org-drill-statistics--entry-attention-data-file d))) (org-drill-statistics--cap-rows structs))))) (concat "** Needs attention\n" @@ -5634,6 +5665,14 @@ algorithms. Set by `org-drill-statistics' and rotated by `org-drill-statistics-cycle-algorithm'. Read by the render helpers via the filtered log they are handed.") +(defvar-local org-drill-statistics--source-buffer nil + "Buffer-local deck buffer the dashboard's card scans run in. +Set by `org-drill-statistics' to the buffer the command was invoked +from and read by `org-drill-statistics--render' (via refresh and the +cycle commands) so every card traversal sees the deck, not the +dashboard's own text. Also the fallback jump target for card links +whose path carries no file.") + (defun org-drill-statistics--range-cutoff-float (label) "Return the float-time cutoff for range LABEL, or nil for all time. LABEL is a key in `org-drill-statistics-range-presets'. When its DAYS @@ -5720,24 +5759,34 @@ is put in `org-mode'; not intended to be toggled by hand." :lighter " OrgDrillStats" :keymap org-drill-statistics-mode-map) -(defun org-drill-statistics--render (buffer scope range algorithm) +(defun org-drill-statistics--render (buffer scope range algorithm + &optional source) "Render the dashboard for SCOPE, RANGE, ALGORITHM into BUFFER. -BUFFER is the target buffer. Its read-only state is lifted for the -write, the assembled body replaces the contents, point returns to the -top, and the buffer is left read-only. The three filter values are -stored buffer-locally so refresh and the cycle commands can read and -rotate them. Returns BUFFER." - (with-current-buffer buffer - (let ((inhibit-read-only t) - (body (org-drill-statistics--render-all scope range algorithm))) - (erase-buffer) - (insert body) - (goto-char (point-min))) - (setq org-drill-statistics--scope scope - org-drill-statistics--range range - org-drill-statistics--algorithm algorithm) - (setq buffer-read-only t) - buffer)) +BUFFER is the target buffer. SOURCE is the deck buffer the card scans +run in, defaulting to the current buffer, so the traversals see the +deck rather than the dashboard's own text. A dead SOURCE signals a +`user-error'. BUFFER's read-only state is lifted for the write, the +assembled body replaces the contents, point returns to the top, and the +buffer is left read-only. The filter values and SOURCE are stored +buffer-locally so refresh and the cycle commands can read and rotate +them. Returns BUFFER." + (let ((source (or source (current-buffer)))) + (unless (buffer-live-p source) + (user-error + "The dashboard's deck buffer is gone; run org-drill-statistics from a deck")) + (let ((body (with-current-buffer source + (org-drill-statistics--render-all scope range algorithm)))) + (with-current-buffer buffer + (let ((inhibit-read-only t)) + (erase-buffer) + (insert body) + (goto-char (point-min))) + (setq org-drill-statistics--scope scope + org-drill-statistics--range range + org-drill-statistics--algorithm algorithm + org-drill-statistics--source-buffer source) + (setq buffer-read-only t) + buffer)))) ;;;###autoload (defun org-drill-statistics () @@ -5748,13 +5797,14 @@ filter header and the five render sections, using the current to it. Refresh and the s/r/a cycle commands re-render in place." (interactive) (let ((buffer (get-buffer-create org-drill-statistics--buffer-name)) + (source (current-buffer)) (scope org-drill-scope) (range (caar org-drill-statistics-range-presets)) (algorithm nil)) (with-current-buffer buffer (org-mode) (org-drill-statistics-mode 1)) - (org-drill-statistics--render buffer scope range algorithm) + (org-drill-statistics--render buffer scope range algorithm source) (switch-to-buffer buffer) buffer)) @@ -5767,7 +5817,8 @@ every section against the current log and card state. Bound to g." (current-buffer) org-drill-statistics--scope org-drill-statistics--range - org-drill-statistics--algorithm)) + org-drill-statistics--algorithm + org-drill-statistics--source-buffer)) (defun org-drill-statistics-cycle-scope () "Cycle the dashboard scope filter and refresh. @@ -5990,7 +6041,15 @@ do. Interactively, prompts for DIRECTORY." (org-drill-statistics--write-csv (expand-file-name "cards.csv" directory) org-drill-statistics--cards-csv-header - (org-drill-statistics--cards-rows scope)) + (let ((source org-drill-statistics--source-buffer)) + (cond + ((buffer-live-p source) + (with-current-buffer source + (org-drill-statistics--cards-rows scope))) + (source + (user-error + "The dashboard's deck buffer is gone; refresh from a deck first")) + (t (org-drill-statistics--cards-rows scope))))) (org-drill-statistics--write-csv (expand-file-name "daily.csv" directory) org-drill-statistics--daily-csv-header |
