diff options
| author | Craig Jennings <c@cjennings.net> | 2026-05-24 17:21:29 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-05-24 17:21:29 -0500 |
| commit | cc599d3e9d37c200d49f723ff8e8ce694acd33f8 (patch) | |
| tree | 6ee899bcd4a0a45c946329859d671ff6825efce7 | |
| parent | bb30b0f9146722b279832a991540917c3fa3cb81 (diff) | |
| download | pearl-cc599d3e9d37c200d49f723ff8e8ce694acd33f8.tar.gz pearl-cc599d3e9d37c200d49f723ff8e8ce694acd33f8.zip | |
feat: make comment sort order configurable, newest-first by default
I added pearl-comment-sort-order to choose how an issue's comments render under the Comments heading: newest-first (the default, most recent on top) or oldest-first (chronological, like an email thread). Both the render sort and the add-comment insertion point honor it, so a new comment lands at the top for newest-first and at the bottom for oldest-first.
| -rw-r--r-- | pearl.el | 38 | ||||
| -rw-r--r-- | tests/test-pearl-comments.el | 65 | ||||
| -rw-r--r-- | tests/test-pearl-list-comments.el | 11 |
3 files changed, 77 insertions, 37 deletions
@@ -1812,18 +1812,30 @@ path; nil (the single-issue thread) yields no marker." (if (plist-get count-info :overflow) "+" "")) "")) +(defcustom pearl-comment-sort-order 'newest-first + "Order in which an issue's comments render under the Comments heading. +`newest-first' puts the most recent comment at the top, and a newly added +comment is inserted there. `oldest-first' reads top-to-bottom, oldest to +newest, like an email thread, and a new comment appends at the bottom." + :type '(choice (const :tag "Newest first (most recent on top)" newest-first) + (const :tag "Oldest first (chronological)" oldest-first)) + :group 'pearl) + (defun pearl--format-comments (comments &optional count-info) "Format COMMENTS (a list of normalized comment plists) as a Comments subtree. -Comments render oldest-first under a level-3 `Comments' heading. Returns the -empty string when COMMENTS is nil, so an issue with no comments renders no -subtree. COUNT-INFO, when non-nil, adds a `💬 shown/total' marker to the -heading (the bulk list passes the issue's `:comment-count')." +Comments render under a level-3 `Comments' heading, 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, adds a `💬 shown/total' marker to +the heading (the bulk list passes the issue's `:comment-count')." (if (null comments) "" - (let ((sorted (sort (copy-sequence comments) - (lambda (a b) - (string< (or (plist-get a :created-at) "") - (or (plist-get b :created-at) "")))))) + (let* ((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 ""))))) @@ -2487,8 +2499,10 @@ GraphQL/transport failure or a non-success payload." (defun pearl--append-comment-to-issue (comment) "Insert COMMENT (a normalized plist) under the issue subtree at point. -Appends after any existing comments in the issue's `Comments' subtree, creating -that subtree at the end of the issue when it does not exist yet." +A new comment is the newest, so it lands where `pearl-comment-sort-order' puts +the newest: at the top of the `Comments' subtree (just under the heading) for +`newest-first', or after the last comment for `oldest-first'. Creates the +subtree at the end of the issue when it does not exist yet." (save-excursion (org-back-to-heading t) (let* ((issue-end (save-excursion (org-end-of-subtree t t) (point))) @@ -2500,7 +2514,9 @@ that subtree at the end of the issue when it does not exist yet." (if comments-pos (progn (goto-char comments-pos) - (org-end-of-subtree t t) + (if (eq pearl-comment-sort-order 'newest-first) + (forward-line 1) ; just under the heading, before the first comment + (org-end-of-subtree t t)) ; after the last comment (insert (pearl--format-comment comment))) (goto-char issue-end) (insert "*** Comments\n" (pearl--format-comment comment)))))) diff --git a/tests/test-pearl-comments.el b/tests/test-pearl-comments.el index d335132..85d1c84 100644 --- a/tests/test-pearl-comments.el +++ b/tests/test-pearl-comments.el @@ -20,9 +20,9 @@ ;;; Commentary: ;; Tests for the comment thread: rendering a normalized comment and the -;; oldest-first Comments subtree, including comments in the issue render, the -;; commentCreate helper (stubbed at the HTTP boundary), the in-place append -;; under the Comments subtree (creating it when absent), and the +;; Comments subtree (ordered per `pearl-comment-sort-order'), including comments +;; in the issue render, the commentCreate helper (stubbed at the HTTP boundary), +;; the in-place append under the Comments subtree (creating it when absent), and the ;; `pearl-add-comment' command. ;;; Code: @@ -64,12 +64,22 @@ "No comments renders nothing (no empty Comments subtree)." (should (string= "" (pearl--format-comments nil)))) -(ert-deftest test-pearl-format-comments-oldest-first () - "Comments render under a Comments heading, oldest first regardless of input order." - (let ((out (pearl--format-comments - '((:id "c2" :author "B" :created-at "2026-05-23T12:00:00.000Z" :body "second") - (:id "c1" :author "A" :created-at "2026-05-23T09:00:00.000Z" :body "first"))))) +(defconst test-pearl--two-comments + '((:id "c1" :author "A" :created-at "2026-05-23T09:00:00.000Z" :body "first") + (:id "c2" :author "B" :created-at "2026-05-23T12:00:00.000Z" :body "second")) + "Two comments, c1 older than c2, in input order regardless of render order.") + +(ert-deftest test-pearl-format-comments-newest-first () + "With newest-first order, the most recent comment renders on top." + (let* ((pearl-comment-sort-order 'newest-first) + (out (pearl--format-comments test-pearl--two-comments))) (should (string-match-p "^\\*\\*\\* Comments$" out)) + (should (< (string-match "second" out) (string-match "first" out))))) + +(ert-deftest test-pearl-format-comments-oldest-first () + "With oldest-first order, comments render chronologically, oldest on top." + (let* ((pearl-comment-sort-order 'oldest-first) + (out (pearl--format-comments test-pearl--two-comments))) (should (< (string-match "first" out) (string-match "second" out))))) ;;; comments in the issue render @@ -121,19 +131,32 @@ (should (re-search-forward "^\\*\\*\\* Comments$" nil t)) (should (re-search-forward "first comment" nil t)))) -(ert-deftest test-pearl-append-comment-after-existing () - "A new comment appends after an existing one under the Comments subtree." - (test-pearl--in-org - "** TODO [#B] Title\n:PROPERTIES:\n:LINEAR-ID: a\n:END:\nBody.\n*** Comments\n**** A — 2026-05-23T09:00:00.000Z\nfirst\n" - (pearl--append-comment-to-issue - '(:id "c2" :author "B" :created-at "2026-05-23T12:00:00.000Z" :body "second")) - (goto-char (point-min)) - ;; only one Comments heading, and the new comment follows the first - (should (re-search-forward "^\\*\\*\\* Comments$" nil t)) - (should-not (re-search-forward "^\\*\\*\\* Comments$" nil t)) - (goto-char (point-min)) - (should (< (progn (re-search-forward "first") (point)) - (progn (re-search-forward "second") (point)))))) +(ert-deftest test-pearl-append-comment-newest-first-inserts-at-top () + "With newest-first order, a new comment lands above the existing ones." + (let ((pearl-comment-sort-order 'newest-first)) + (test-pearl--in-org + "** TODO [#B] Title\n:PROPERTIES:\n:LINEAR-ID: a\n:END:\nBody.\n*** Comments\n**** A — 2026-05-23T09:00:00.000Z\nfirst\n" + (pearl--append-comment-to-issue + '(:id "c2" :author "B" :created-at "2026-05-23T12:00:00.000Z" :body "second")) + (goto-char (point-min)) + ;; still exactly one Comments heading + (should (re-search-forward "^\\*\\*\\* Comments$" nil t)) + (should-not (re-search-forward "^\\*\\*\\* Comments$" nil t)) + (goto-char (point-min)) + ;; the new comment is above the existing one + (should (< (progn (re-search-forward "second") (point)) + (progn (re-search-forward "first") (point))))))) + +(ert-deftest test-pearl-append-comment-oldest-first-appends-at-bottom () + "With oldest-first order, a new comment appends after the existing ones." + (let ((pearl-comment-sort-order 'oldest-first)) + (test-pearl--in-org + "** TODO [#B] Title\n:PROPERTIES:\n:LINEAR-ID: a\n:END:\nBody.\n*** Comments\n**** A — 2026-05-23T09:00:00.000Z\nfirst\n" + (pearl--append-comment-to-issue + '(:id "c2" :author "B" :created-at "2026-05-23T12:00:00.000Z" :body "second")) + (goto-char (point-min)) + (should (< (progn (re-search-forward "first") (point)) + (progn (re-search-forward "second") (point))))))) ;;; pearl-add-comment diff --git a/tests/test-pearl-list-comments.el b/tests/test-pearl-list-comments.el index 630c712..be0880e 100644 --- a/tests/test-pearl-list-comments.el +++ b/tests/test-pearl-list-comments.el @@ -98,13 +98,14 @@ ;;; --format-comments with a marker -(ert-deftest test-pearl-format-comments-renders-marker-and-oldest-first () - "With count-info, the Comments heading carries the marker; bodies stay oldest-first." - (let* ((comments (test-pearl--comments 5)) ; newest-first as fetched +(ert-deftest test-pearl-format-comments-renders-marker-and-order () + "With count-info the Comments heading carries the marker; bodies honor the sort order." + (let* ((pearl-comment-sort-order 'newest-first) + (comments (test-pearl--comments 5)) ; newest-first as fetched (out (pearl--format-comments comments '(:shown 5 :total 12 :overflow nil)))) (should (string-match-p "^\\*\\*\\* Comments 💬 5/12$" out)) - ;; oldest (comment 1) renders before newest (comment 5) - (should (< (string-match "comment 1\\b" out) (string-match "comment 5\\b" out))))) + ;; newest (comment 5) renders before oldest (comment 1) + (should (< (string-match "comment 5\\b" out) (string-match "comment 1\\b" out))))) (ert-deftest test-pearl-format-comments-no-marker-without-count () "Without count-info (the single-issue thread), the heading has no marker." |
