From b1df299c164bd688ce4d0887603e6b71ea51bd22 Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Thu, 28 May 2026 10:32:25 -0500 Subject: refactor(render): drop the leading 💬 glyph from the Comments heading MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pearl was rendering the Comments subtree heading as `*** 💬 Comments [N/M]`. Craig flagged that it looks strange, and the renderer should work cleanly before it gets glyphed. I dropped the emoji prefix so the heading reads `*** Comments [N/M]`. Partly reverts 41a3396. The overlay-glyph idea (a content-aware display overlay on the leading stars) stays tracked as a separate task. I touched pearl--format-comments (the new-subtree literal) and pearl--append-comment-to-issue (both the new-subtree insert and a couple of stale docstring references). The append-locator regex stays tolerant of the legacy `💬 Comments` heading and of the older pre-2026-05 `Comments 💬 N/N` trailing-glyph layout. Buffers rendered in any of the three states still locate their Comments heading on append. I added one regression test (test-pearl-append-comment-locates-legacy-emoji-prefixed-heading) that seeds a `*** 💬 Comments 1/1` heading and asserts append still finds it, bumps the count to 2/2, and doesn't add a second Comments heading. I updated assertions in test-pearl-comments.el, test-pearl-list-comments.el, and test-integration-acceptance.el to expect the no-emoji form. I also updated docstrings on pearl-fetch-comments-in-list, pearl-list-comments-count-cap, pearl--format-comments, pearl--bump-comments-count-marker, and pearl--comment-count-marker. The README's sample buffer block now matches what the renderer emits. All 669 ert tests pass. make compile and make lint are clean. --- README.org | 30 +++++++++++++++--------------- pearl.el | 27 ++++++++++++++------------- tests/test-integration-acceptance.el | 2 +- tests/test-pearl-comments.el | 28 ++++++++++++++++++++++------ tests/test-pearl-list-comments.el | 12 ++++++------ 5 files changed, 58 insertions(+), 41 deletions(-) diff --git a/README.org b/README.org index ba28157..9b3af9a 100644 --- a/README.org +++ b/README.org @@ -168,10 +168,10 @@ The hot-path commands sit one key under the prefix; the rest are grouped into su | =s= / =S= | save the issue at point / save every issue in the file | | =d= | edit the description | | =m= | open the full transient menu | -| =f= ... | fetch: =s= pick source, =o= open issues, =p= by project, =f= filter, =v= view, =q= saved query | +| =f= ... | fetch: =s= pick source, =o= open issues, =p= by project, =f= filter, =v= view, =q= saved query, =S= sync saved query to Linear, =P= publish current source | | =e= ... | edit: =d= description, =s= state, =a= assignee, =l= labels, =c= comment | | =c= ... | create: =t= issue, =c= comment | -| =k= ... | delete: =t= issue, =c= comment | +| =k= ... | delete: =t= issue, =c= comment, =q= saved query | | =o= ... | open: =i= issue in browser, =v= view in Linear | | =y= ... | copy: =u= issue URL | @@ -188,18 +188,18 @@ With =which-key=, each step shows a labeled menu. Every command is also availabl A *source* is anything Pearl can fetch from: a Linear favorite, a Custom View, a project / cycle / label / user, a local saved query, or an ad-hoc filter. The everyday front door is =pearl-pick-source= (=C-; L f s=, or =P= in the transient), which lists all your Linear favorites first (in their Linear sort order) and then your local saved queries, each tagged by kind: #+begin_example - [view] Active sprint bugs - [project] Orchestration Dashboard - [user] Vrezh Mikayelyan - [label] security - [saved] My open work - [saved → Eng] Active sprint bugs (synced) - [saved → Personal] My ICEBOX scratchpad (synced) + [view] Active sprint bugs + [project] Orchestration Dashboard + [user] Vrezh Mikayelyan + [label] security + [saved] My open work + [saved → Eng] Active sprint bugs (synced) + [saved → Personal] My ICEBOX scratchpad (synced) #+end_example Pick one and it fetches. List-capable favorites (Custom View / project / cycle / label / user) resolve to the existing filter or view fetch and render into the active file; non-list favorites (issue, document, dashboard, ...) open in the browser instead. Favorites are picker entries, never persisted -- a chosen favorite resolves to a concrete filter/view source before rendering, so refresh re-runs that resolved source and stays stable even if your favorites list later changes. Label and user favorites resolve by id, not by name or email, so renames in Linear don't break a saved fetch. -A saved query you've synced up to Linear (see /Publishing a saved query as a Linear view/ below) renders as =[saved → ] Name= so you can see at a glance which scope it lives in. The arrow reads "where it lives now." Picking it dispatches as a Linear view (server-side filter run via =customView(id:)=), not as a local filter -- any drift you've made to the view on Linear's side is honored. Plain =[saved]= entries are local-only and still run the authoring filter Pearl has on disk. =[saved → ?]= means the entry is synced but Pearl couldn't resolve the team id (deleted or renamed on Linear) -- the dispatch still works, the label is just flagging stale scope metadata. +A saved query you've synced up to Linear (see [[*Publishing a saved query as a Linear view]] below) renders as =[saved → ] Name= so you can see at a glance which scope it lives in. The arrow reads "where it lives now." Picking it dispatches as a Linear view (server-side filter run via =customView(id:)=), not as a local filter -- any drift you've made to the view on Linear's side is honored. Plain =[saved]= entries are local-only and still run the authoring filter Pearl has on disk. =[saved → ?]= means the entry is synced but Pearl couldn't resolve the team id (deleted or renamed on Linear) -- the dispatch still works, the label is just flagging stale scope metadata. If the favorites fetch fails (network/auth/transport), the picker still offers any local saved queries -- a failed fetch is not the same as "no favorites." With neither favorites nor saved queries, =pearl-pick-source= refuses with a clear message naming the missing setup. @@ -209,10 +209,10 @@ A local saved query is great for your own quick filters, but the team can't see Two commands publish: -| Command | What it does | -|------------------------------------+--------------------------------------------------------------------| -| =pearl-sync-saved-query-to-linear= | Pick a saved query by name and publish (or update) it on Linear | -| =pearl-publish-current-source= | Read the buffer's =#+LINEAR-SOURCE=; if it names a local saved query, publish that one | +| Command | Binding | What it does | +|------------------------------------+--------------------------------------+--------------------------------------------------------------------| +| =pearl-sync-saved-query-to-linear= | =C-; L f S=, =S= in the transient Fetch group | Pick a saved query by name and publish (or update) it on Linear | +| =pearl-publish-current-source= | =C-; L f P=, =U= in the transient Fetch group | Read the buffer's =#+LINEAR-SOURCE=; if it names a local saved query, publish that one | The first time you sync a query, Pearl asks where the view should live with one enriched prompt that spells out the full end-state per option: @@ -334,7 +334,7 @@ A fetched Pearl file is intentionally readable. The header records the source, r The issue description renders here as Org and can be edited in place. - *** 💬 Comments + *** Comments **** Author Name - 2026-05-23T10:00:00.000Z A comment, oldest first. #+end_src diff --git a/pearl.el b/pearl.el index a8192b9..00271f9 100644 --- a/pearl.el +++ b/pearl.el @@ -982,7 +982,7 @@ is the already-extracted (normalized) list, so this stays pure." (defcustom pearl-fetch-comments-in-list t "When non-nil, the bulk list and Custom View fetch each issue's recent comments. A populated list then renders the latest few comments per issue with a -`💬 shown/total' marker on the Comments heading, instead of looking like +`shown/total' marker on the Comments heading, instead of looking like nothing has comments. Set to nil to keep the list fetch light (comments load only on a single-issue refresh)." :type 'boolean @@ -998,7 +998,7 @@ list/view view. See `pearl-fetch-comments-in-list'." (defcustom pearl-list-comments-count-cap 25 "Ceiling for the exact comment total shown in the bulk list marker. The fetch pulls one more than this so the marker can show an exact total up to -the cap (`💬 5/18') and a `+' beyond it (`💬 5/25+'). A higher cap means an +the cap (`5/18') and a `+' beyond it (`5/25+'). A higher cap means an exact count for busier issues at the cost of a larger fetch." :type 'integer :group 'pearl) @@ -2002,8 +2002,8 @@ A null author renders as `(unknown)'." (defun pearl--comment-count-marker (count-info) "Render COUNT-INFO as a ` shown/total[+]' suffix, or the empty string. COUNT-INFO is a plist (:shown N :total M :overflow BOOL) set on the bulk-list -path; nil (the single-issue thread) yields no marker. The 💬 glyph leads the -`Comments' heading itself (see `pearl--format-comments'), not this count." +path; nil (the single-issue thread) yields no marker. The marker appends +to the `Comments' heading written by `pearl--format-comments'." (if count-info (format " %d/%d%s" (plist-get count-info :shown) @@ -2025,9 +2025,8 @@ newest, like an email thread, and a new comment appends at the bottom." 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. The heading leads with a 💬 glyph; COUNT-INFO, when non-nil, -appends a ` shown/total' count (the bulk list passes the issue's -`:comment-count')." +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)) @@ -2036,7 +2035,7 @@ appends a ` shown/total' count (the bulk list passes the issue's (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" + (concat "*** Comments" (pearl--comment-count-marker count-info) "\n" (mapconcat #'pearl--format-comment sorted ""))))) (defun pearl--format-issue-as-org-entry (issue) @@ -2714,8 +2713,7 @@ GraphQL/transport failure or a non-success payload." (defun pearl--bump-comments-count-marker () "Increment the ` N/M[+]' count on the Comments heading at point, if present. A locally added comment grows both the shown and total counts by one. A heading -that carries no count marker (the single-issue view) is left untouched, and the -leading 💬 glyph is preserved either way." +that carries no count marker (the single-issue view) is left untouched." (save-excursion (let ((eol (line-end-position))) (beginning-of-line) @@ -2737,8 +2735,11 @@ subtree at the end of the issue when it does not exist yet." (let* ((issue-end (save-excursion (org-end-of-subtree t t) (point))) (comments-pos (save-excursion - ;; tolerate the leading 💬 glyph and a trailing count marker; also - ;; match the pre-2026-05 "Comments 💬 N/N" layout in older files + ;; Match the new no-emoji form, the prior `💬 ' leading-glyph + ;; layout (rendered between 2026-05-26 and the de-emoji + ;; revert), and the pre-2026-05 trailing-glyph layout + ;; (`Comments 💬 N/N'). Buffers rendered in any of the three + ;; states locate their Comments heading. (when (re-search-forward "^\\*+ \\(?:💬 \\)?Comments\\(?:[ \t].*\\)?$" issue-end t) (match-beginning 0))))) (if comments-pos @@ -2756,7 +2757,7 @@ subtree at the end of the issue when it does not exist yet." ;; no Comments subtree yet: this is the first comment, so it is 1/1 (goto-char issue-end) (let ((start (point))) - (insert "*** 💬 Comments 1/1\n" (pearl--format-comment comment)) + (insert "*** Comments 1/1\n" (pearl--format-comment comment)) (pearl--hide-drawers-in-region start (point))))))) ;;;###autoload diff --git a/tests/test-integration-acceptance.el b/tests/test-integration-acceptance.el index 37dce34..523d1b3 100644 --- a/tests/test-integration-acceptance.el +++ b/tests/test-integration-acceptance.el @@ -192,7 +192,7 @@ because the description-update mutation contains both `IssueDescription' and (pearl-create-comment "looks good") (should (memq 'comment-create test-integration--ops)) (goto-char (point-min)) - (should (re-search-forward "^\\*\\*\\* 💬 Comments 1/1$" nil t)) + (should (re-search-forward "^\\*\\*\\* Comments 1/1$" nil t)) (should (re-search-forward "looks good" nil t)) ;; --- edit the priority cookie, then save (reconcile-on-save) --- (goto-char (point-min)) diff --git a/tests/test-pearl-comments.el b/tests/test-pearl-comments.el index 323d7b7..0a0cb45 100644 --- a/tests/test-pearl-comments.el +++ b/tests/test-pearl-comments.el @@ -71,7 +71,7 @@ "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-p "^\\*\\*\\* Comments$" out)) (should (< (string-match "second" out) (string-match "first" out))))) (ert-deftest test-pearl-format-comments-oldest-first () @@ -90,7 +90,7 @@ :state (:name "Todo") :description "Body text." :comments ((:id "c1" :author "A" :created-at "2026-05-23T09:00:00.000Z" :body "a comment")))))) - (should (string-match-p "^\\*\\*\\* 💬 Comments$" out)) + (should (string-match-p "^\\*\\*\\* Comments$" out)) (should (< (string-match "Body text." out) (string-match "a comment" out)))))) ;;; --create-comment-async @@ -120,23 +120,39 @@ ;;; --append-comment-to-issue (ert-deftest test-pearl-append-comment-creates-subtree () - "Appending to an issue with no Comments subtree creates one, glyphed and 1/1." + "Appending to an issue with no Comments subtree creates one with the 1/1 marker." (test-pearl--in-org "*** TODO [#B] Title\n:PROPERTIES:\n:LINEAR-ID: a\n:END:\nBody.\n" (pearl--append-comment-to-issue '(:id "c1" :author "A" :created-at "2026-05-23T09:00:00.000Z" :body "first comment")) (goto-char (point-min)) - (should (re-search-forward "^\\*\\*\\* 💬 Comments 1/1$" nil t)) + (should (re-search-forward "^\\*\\*\\* Comments 1/1$" nil t)) (should (re-search-forward "first comment" nil t)))) (ert-deftest test-pearl-append-comment-bumps-count-marker () "Appending increments the shown/total count on an existing Comments heading." + (test-pearl--in-org + "** TODO [#B] Title\n:PROPERTIES:\n:LINEAR-ID: a\n:END:\nBody.\n*** Comments 1/1\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 (re-search-forward "^\\*\\*\\* Comments 2/2$" nil t)))) + +(ert-deftest test-pearl-append-comment-locates-legacy-emoji-prefixed-heading () + "The locator regex still finds a pre-existing `💬 Comments' heading. +Buffers rendered between 2026-05-26 and the 💬-emoji revert carry the +leading glyph; pearl--append-comment-to-issue should still find and +extend them without re-rendering the file." (test-pearl--in-org "** TODO [#B] Title\n:PROPERTIES:\n:LINEAR-ID: a\n:END:\nBody.\n*** 💬 Comments 1/1\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 (re-search-forward "^\\*\\*\\* 💬 Comments 2/2$" nil t)))) + ;; The bump-count operates on the legacy heading; the new comment appears + ;; under it without a second Comments heading being added. + (should (re-search-forward "^\\*\\*\\* 💬 Comments 2/2$" nil t)) + (goto-char (point-min)) + (should-not (re-search-forward "^\\*\\*\\* Comments " nil t)))) (ert-deftest test-pearl-append-comment-newest-first-inserts-at-top () "With newest-first order, a new comment lands above the existing ones." @@ -178,7 +194,7 @@ (re-search-forward "Body.") (pearl-create-comment "my new comment") (goto-char (point-min)) - (should (re-search-forward "^\\*\\*\\* 💬 Comments 1/1$" nil t)) + (should (re-search-forward "^\\*\\*\\* Comments 1/1$" nil t)) (should (re-search-forward "my new comment" nil t))))) (ert-deftest test-pearl-create-comment-reports-failure () diff --git a/tests/test-pearl-list-comments.el b/tests/test-pearl-list-comments.el index a9517c9..751e220 100644 --- a/tests/test-pearl-list-comments.el +++ b/tests/test-pearl-list-comments.el @@ -20,7 +20,7 @@ ;;; Commentary: ;; Tests for rendering comments in the bulk issue list: the per-issue cap -;; (`pearl--cap-issue-list-comments'), the `💬 shown/total' marker +;; (`pearl--cap-issue-list-comments'), the ` shown/total' marker ;; (`pearl--comment-count-marker', the Comments heading), and the way the ;; append locator tolerates the marked heading so a later add-comment finds the ;; existing subtree instead of creating a second one. @@ -103,14 +103,14 @@ (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)) + (should (string-match-p "^\\*\\*\\* Comments 5/12$" 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." (let ((out (pearl--format-comments (test-pearl--comments 2)))) - (should (string-match-p "^\\*\\*\\* 💬 Comments$" out)))) + (should (string-match-p "^\\*\\*\\* Comments$" out)))) ;;; the marked heading is still found by the append locator @@ -127,15 +127,15 @@ (org-mode) (goto-char (point-min)) ;; the rendered Comments heading carries the marker - (should (re-search-forward "^\\*\\*\\* 💬 Comments 5/8$" nil t)) + (should (re-search-forward "^\\*\\*\\* Comments 5/8$" nil t)) (goto-char (point-min)) (re-search-forward "issue") (pearl--append-comment-to-issue '(:id "cnew" :author "Z" :created-at "2026-06-01T00:00:00.000Z" :body "appended")) ;; exactly one Comments heading, and the new comment landed under it (goto-char (point-min)) - (should (re-search-forward "^\\*\\*\\* 💬 Comments" nil t)) - (should-not (re-search-forward "^\\*\\*\\* 💬 Comments" nil t)) + (should (re-search-forward "^\\*\\*\\* Comments" nil t)) + (should-not (re-search-forward "^\\*\\*\\* Comments" nil t)) (should (string-match-p "appended" (buffer-string))))))) (provide 'test-pearl-list-comments) -- cgit v1.2.3