aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--pearl.el31
-rw-r--r--tests/test-pearl-merge.el14
-rw-r--r--tests/test-pearl-todo-keywords.el29
3 files changed, 69 insertions, 5 deletions
diff --git a/pearl.el b/pearl.el
index 19166db..c61180c 100644
--- a/pearl.el
+++ b/pearl.el
@@ -4063,6 +4063,36 @@ provenance advances on a refresh."
(when (re-search-forward "^#\\+LINEAR-TRUNCATED: .*$" nil t)
(replace-match (format "#+LINEAR-TRUNCATED: %s" (if truncated "yes" "no")) t t))))
+(defun pearl--update-derived-todo-header (states)
+ "Rewrite the buffer's `#+TODO:' line from the final headings plus STATES.
+Scans every Linear issue heading (carrying `LINEAR-ID') for its TODO keyword
+and `:LINEAR-STATE-TYPE:' drawer, unions those with STATES (the fetched teams'
+states, see `pearl--gather-header-states'), and rewrites the line via
+`pearl--derive-todo-line'. Scanning the final buffer -- not just the fetched
+issue list -- keeps retained / skipped dirty subtrees declared. A heading with
+no state-type drawer (a legacy render) is classified by `org-done-keywords'.
+Creates the line after `#+STARTUP:' if it is absent."
+ (save-excursion
+ (let (scanned)
+ (goto-char (point-min))
+ (while (re-search-forward "^\\*+ " nil t)
+ (when (org-entry-get nil "LINEAR-ID")
+ (let ((kw (org-get-todo-state))
+ (type (org-entry-get nil "LINEAR-STATE-TYPE")))
+ (when kw
+ (push (list :name kw
+ :type (cond ((and type (not (string-empty-p type))) type)
+ ((member kw org-done-keywords) "completed")
+ (t nil)))
+ scanned)))))
+ (let ((line (pearl--derive-todo-line (append states (nreverse scanned)))))
+ (goto-char (point-min))
+ (if (re-search-forward "^#\\+TODO: .*$" nil t)
+ (replace-match (format "#+TODO: %s" line) t t)
+ (when (re-search-forward "^#\\+STARTUP: .*$" nil t)
+ (end-of-line)
+ (insert (format "\n#+TODO: %s" line))))))))
+
(defun pearl--merge-query-result (result source)
"Merge a query RESULT into the current buffer by `LINEAR-ID', tagged with SOURCE.
The same-source refresh counterpart to `pearl--render-query-result': rather than
@@ -4083,6 +4113,7 @@ of the replace path."
(truncated (pearl--query-result-truncated-p result))
(counts (pearl--merge-issues-into-buffer issues)))
(pearl--update-source-header (length issues) truncated)
+ (pearl--update-derived-todo-header (pearl--gather-header-states issues))
(pearl-highlight-comments)
(pearl--restore-page-visibility)
(pearl--surface-buffer (current-buffer))
diff --git a/tests/test-pearl-merge.el b/tests/test-pearl-merge.el
index e55127b..ba73cc6 100644
--- a/tests/test-pearl-merge.el
+++ b/tests/test-pearl-merge.el
@@ -38,11 +38,15 @@
`(let ((pearl-state-to-todo-mapping
'(("Todo" . "TODO") ("In Progress" . "IN-PROGRESS") ("Done" . "DONE")))
(org-todo-keywords '((sequence "TODO" "IN-PROGRESS" "|" "DONE"))))
- (with-temp-buffer
- (insert ,content)
- (org-mode)
- (goto-char (point-min))
- ,@body)))
+ ;; the merge's derived-header rebuild gathers team states synchronously;
+ ;; stub it so the merge tests stay network-free (the header then derives
+ ;; from the displayed issues' own states)
+ (cl-letf (((symbol-function 'pearl--team-states) (lambda (_team-id) nil)))
+ (with-temp-buffer
+ (insert ,content)
+ (org-mode)
+ (goto-char (point-min))
+ ,@body))))
(defun test-pearl-merge--issue (id title desc)
"A normalized issue plist with ID, TITLE, and DESC for merge input."
diff --git a/tests/test-pearl-todo-keywords.el b/tests/test-pearl-todo-keywords.el
index b941b26..281beea 100644
--- a/tests/test-pearl-todo-keywords.el
+++ b/tests/test-pearl-todo-keywords.el
@@ -121,5 +121,34 @@
(let ((out (pearl--build-org-content '() '(:type filter :name "X" :filter nil) nil nil)))
(should (string-match-p "^#\\+TODO: TODO IN-PROGRESS IN-REVIEW BACKLOG BLOCKED \\| DONE$" out))))
+;;; --update-derived-todo-header (merge-refresh, final-buffer scan)
+
+(ert-deftest test-pearl-update-derived-todo-header-covers-buffer-headings ()
+ "The header rebuild scans the final buffer so every visible heading is declared."
+ (let ((org-todo-keywords '((sequence "TODO" "IN-PROGRESS" "|" "DONE"))))
+ (with-temp-buffer
+ (insert "#+STARTUP: show3levels\n#+TODO: TODO | DONE\n\n"
+ "* View\n"
+ "** IN-PROGRESS Issue A\n:PROPERTIES:\n:LINEAR-ID: a\n:LINEAR-STATE-TYPE: started\n:END:\n"
+ "** DONE Issue B\n:PROPERTIES:\n:LINEAR-ID: b\n:LINEAR-STATE-TYPE: completed\n:END:\n")
+ (org-mode)
+ ;; no team states passed -- the rebuild comes purely from the buffer scan
+ (pearl--update-derived-todo-header '())
+ (goto-char (point-min))
+ (should (re-search-forward "^#\\+TODO: IN-PROGRESS \\| DONE$" nil t)))))
+
+(ert-deftest test-pearl-update-derived-todo-header-classifies-legacy-by-done-keyword ()
+ "A retained heading with no STATE-TYPE drawer is classified by org-done-keywords."
+ (let ((org-todo-keywords '((sequence "TODO" "|" "SHIPPED"))))
+ (with-temp-buffer
+ (insert "#+STARTUP: show3levels\n#+TODO: TODO | SHIPPED\n\n"
+ "* View\n"
+ "** SHIPPED Old Issue\n:PROPERTIES:\n:LINEAR-ID: a\n:END:\n") ; no STATE-TYPE
+ (org-mode)
+ (pearl--update-derived-todo-header '())
+ (goto-char (point-min))
+ ;; SHIPPED is an org done-keyword, so it lands on the done side
+ (should (re-search-forward "^#\\+TODO: *\\| SHIPPED$" nil t)))))
+
(provide 'test-pearl-todo-keywords)
;;; test-pearl-todo-keywords.el ends here