From a6a08fd6a418a5d404550478bde2883c440377a7 Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Wed, 3 Jun 2026 12:36:13 -0500 Subject: fix(render): nest comments under their issue in a grouped view In a grouped view, each issue's Comments subtree rendered at the issue's own level instead of one deeper, so the Comments heading sat as a sibling of its ticket rather than a child. The grouping feature gave pearl--format-issue-as-org-entry a level parameter (issues render at level 3 under a group heading) but left the comment subtree hardcoded: pearl--format-comments emitted "*** Comments" and pearl--format-comment "**** ". That nests right when the issue is at level 2 (flat) and breaks when it's at level 3 (grouped). I thread the issue's level through, so the Comments heading is level+1 and each comment body level+2: a flat issue stays 2/3/4, a grouped one becomes 3/4/5. --- pearl.el | 32 +++++++++++++++++++++----------- tests/test-pearl-list-comments.el | 26 ++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 11 deletions(-) diff --git a/pearl.el b/pearl.el index e9f1e8b..d2423ae 100644 --- a/pearl.el +++ b/pearl.el @@ -2715,8 +2715,10 @@ fetch. Both are documented in the conversion matrix." (t (push (pearl--org-line-to-md line) out)))) (string-join (nreverse out) "\n")))) -(defun pearl--format-comment (comment) - "Format a normalized COMMENT plist as a level-4 Org entry. +(defun pearl--format-comment (comment &optional level) + "Format a normalized COMMENT plist as an Org entry. +LEVEL is the heading depth (number of stars), defaulting to 4 -- a grouped view +renders one level deeper so the comment stays nested under its issue. The heading carries the author and timestamp; a property drawer carries the comment id, the author's user id (empty for bot/external comments, which are not editable), and a sha256 of the last-fetched body for the sync conflict @@ -2725,8 +2727,9 @@ A null author renders as `(unknown)'." (let ((author (or (plist-get comment :author) "(unknown)")) (created (or (plist-get comment :created-at) "")) (raw-body (or (plist-get comment :body) "")) - (body (pearl--md-to-org (or (plist-get comment :body) "")))) - (concat (format "**** %s — %s\n" author created) + (body (pearl--md-to-org (or (plist-get comment :body) ""))) + (stars (make-string (or level 4) ?*))) + (concat (format "%s %s — %s\n" stars author created) ":PROPERTIES:\n" (format ":LINEAR-COMMENT-ID: %s\n" (or (plist-get comment :id) "")) (format ":LINEAR-COMMENT-AUTHOR-ID: %s\n" (or (plist-get comment :author-id) "")) @@ -2756,23 +2759,29 @@ newest, like an email thread, and a new comment appends at the bottom." (const :tag "Oldest first (chronological)" oldest-first)) :group 'pearl) -(defun pearl--format-comments (comments &optional count-info) +(defun pearl--format-comments (comments &optional count-info level) "Format COMMENTS (a list of normalized comment plists) as a Comments subtree. -Comments render under a level-3 `Comments' heading, ordered per -`pearl-comment-sort-order'. +LEVEL is the parent issue's heading depth (default 2): the `Comments' heading +renders at LEVEL+1 and each comment body at LEVEL+2, so the subtree stays nested +under its issue whether the issue is flat (level 2) or grouped (level 3). +Ordered per `pearl-comment-sort-order'. Returns the empty string when COMMENTS is nil, so an issue with no comments renders no subtree. COUNT-INFO, when non-nil, appends a ` shown/total' count to the heading (the bulk list passes the issue's `:comment-count')." (if (null comments) "" - (let* ((newest-first (eq pearl-comment-sort-order 'newest-first)) + (let* ((issue-level (or level 2)) + (heading-stars (make-string (1+ issue-level) ?*)) + (comment-level (+ 2 issue-level)) + (newest-first (eq pearl-comment-sort-order 'newest-first)) (sorted (sort (copy-sequence comments) (lambda (a b) (let ((ca (or (plist-get a :created-at) "")) (cb (or (plist-get b :created-at) ""))) (if newest-first (string> ca cb) (string< ca cb))))))) - (concat "*** Comments" (pearl--comment-count-marker count-info) "\n" - (mapconcat #'pearl--format-comment sorted ""))))) + (concat heading-stars " Comments" (pearl--comment-count-marker count-info) "\n" + (mapconcat (lambda (c) (pearl--format-comment c comment-level)) + sorted ""))))) (defun pearl--format-issue-as-org-entry (issue &optional level) "Format a normalized ISSUE plist as an Org entry. @@ -2839,7 +2848,8 @@ identifier prefix -- so an unedited heading is a no-op on title sync)." ":END:\n" (if (string-empty-p body-org) "" (concat body-org "\n")) (pearl--format-comments (plist-get issue :comments) - (plist-get issue :comment-count))))) + (plist-get issue :comment-count) + (or level 2))))) ;;; Description Sync-Back diff --git a/tests/test-pearl-list-comments.el b/tests/test-pearl-list-comments.el index 751e220..71d3e73 100644 --- a/tests/test-pearl-list-comments.el +++ b/tests/test-pearl-list-comments.el @@ -112,6 +112,32 @@ (let ((out (pearl--format-comments (test-pearl--comments 2)))) (should (string-match-p "^\\*\\*\\* Comments$" out)))) +(ert-deftest test-pearl-format-comments-nests-under-issue-level () + "The Comments subtree nests one level under the issue: a flat issue (level 2, +the default) gets Comments at level 3 and bodies at level 4; a grouped issue at +level 3 gets Comments at level 4 and bodies at level 5. Regression guard for +the grouping bug where comments rendered as siblings of their issue." + (let ((comments (test-pearl--comments 1))) + ;; flat (default level 2) + (should (string-match-p "^\\*\\*\\* Comments$" (pearl--format-comments comments nil 2))) + (should (string-match-p "^\\*\\*\\*\\* " (pearl--format-comments comments nil 2))) + ;; grouped (issue at level 3) + (let ((out (pearl--format-comments comments nil 3))) + (should (string-match-p "^\\*\\*\\*\\* Comments$" out)) + (should (string-match-p "^\\*\\*\\*\\*\\* " out)) + (should-not (string-match-p "^\\*\\*\\* Comments$" out))))) + +(ert-deftest test-pearl-format-issue-grouped-nests-comments-under-issue () + "A level-3 issue with comments renders its Comments subtree at level 4 (a +child), not level 3 (a sibling) -- the grouped-view regression." + (let* ((issue (list :id "u" :identifier "ENG-1" :title "Fix it" + :priority 2 :state '(:name "Todo") + :comments (test-pearl--comments 1))) + (out (pearl--format-issue-as-org-entry issue 3))) + (should (string-match-p "^\\*\\*\\* TODO " out)) ; issue at level 3 + (should (string-match-p "^\\*\\*\\*\\* Comments$" out)) ; Comments nested at 4 + (should-not (string-match-p "^\\*\\*\\* Comments$" out)))) + ;;; the marked heading is still found by the append locator (ert-deftest test-pearl-append-finds-marked-comments-heading () -- cgit v1.2.3