aboutsummaryrefslogtreecommitdiff
path: root/pearl.el
diff options
context:
space:
mode:
Diffstat (limited to 'pearl.el')
-rw-r--r--pearl.el124
1 files changed, 122 insertions, 2 deletions
diff --git a/pearl.el b/pearl.el
index cc020a8..945c9af 100644
--- a/pearl.el
+++ b/pearl.el
@@ -1552,6 +1552,19 @@ exact count for busier issues at the cost of a larger fetch."
"id body createdAt user { id name displayName } botActor { name } externalUser { name }"
"The comment node field selection shared by every query that pulls comments.")
+(defconst pearl--all-comments-query
+ (format "query AllComments($id: String!, $after: String) {
+ issue(id: $id) {
+ comments(first: 100, after: $after, orderBy: createdAt) {
+ nodes { %s }
+ pageInfo { hasNextPage endCursor }
+ }
+ }
+}" pearl--comment-node-fields)
+ "Query for one issue's full comment thread; paged with first/after.
+Unlike `pearl--list-comments-fragment' (which caps the bulk list), this pulls
+every comment so `pearl-fetch-all-comments' can show the whole thread.")
+
(defun pearl--list-comments-fragment ()
"Return the GraphQL comments fragment for the bulk/view list, or empty string.
Fetches one more than `pearl-list-comments-count-cap', newest first, so the
@@ -3618,6 +3631,110 @@ create callback fires with another buffer current (the compose path)."
(pearl--surface-buffer buf)))
(message "Added comment to %s" issue-id)))))
+(defun pearl--fetch-all-comments-async (issue-id callback)
+ "Fetch every comment on ISSUE-ID, calling CALLBACK with the normalized list.
+Pages the comment connection via first/after. On a transport/GraphQL error
+CALLBACK receives the symbol `error' (already logged) so the caller can leave
+the buffer untouched rather than mistake a failure for an empty thread."
+ (let (acc)
+ (cl-labels
+ ((collect (data)
+ (let* ((issue (cdr (assoc 'issue (cdr (assoc 'data data)))))
+ (conn (and issue (cdr (assoc 'comments issue))))
+ (nodes (append (cdr (assoc 'nodes conn)) nil))
+ (page (cdr (assoc 'pageInfo conn))))
+ (dolist (n nodes)
+ (push (pearl--normalize-comment n) acc))
+ (if (eq (cdr (assoc 'hasNextPage page)) t)
+ (pearl--graphql-request-async
+ pearl--all-comments-query
+ (list (cons "id" issue-id)
+ (cons "after" (cdr (assoc 'endCursor page))))
+ #'collect
+ (lambda (&rest _) (funcall callback 'error)))
+ (funcall callback (nreverse acc))))))
+ (pearl--graphql-request-async
+ pearl--all-comments-query
+ (list (cons "id" issue-id))
+ #'collect
+ (lambda (&rest _) (funcall callback 'error))))))
+
+(defun pearl--replace-comments-subtree (comments)
+ "Replace the issue-at-point's Comments subtree with COMMENTS (the full thread).
+Climbs to the issue heading, removes its existing Comments subtree if any, and
+inserts a freshly formatted one carrying an exact `N/N' count (no `+'). Empty
+COMMENTS clears the subtree. Only the Comments subtree is touched, so the issue
+title, drawer, and description body -- including unsaved description edits --
+survive."
+ (save-excursion
+ (pearl--goto-issue-heading-or-error)
+ (let* ((level (org-current-level))
+ (issue-end (save-excursion (org-end-of-subtree t t) (point)))
+ (comments-pos
+ (save-excursion
+ (when (re-search-forward
+ "^\\*+ \\(?:💬 \\)?Comments\\(?:[ \t].*\\)?$" issue-end t)
+ (match-beginning 0))))
+ (n (length comments))
+ (count-info (and (> n 0) (list :shown n :total n :overflow nil)))
+ (rendered (pearl--format-comments comments count-info level)))
+ ;; The Comments subtree is the issue's last child, so its region runs from
+ ;; the Comments heading to the end of the issue subtree.
+ (if comments-pos
+ (progn (goto-char comments-pos)
+ (delete-region comments-pos issue-end))
+ (goto-char issue-end))
+ (let ((start (point)))
+ (insert rendered)
+ (pearl--hide-drawers-in-region start (point))))))
+
+(defun pearl--unpushed-comment-edits-p ()
+ "Non-nil when the issue at point has comments with unpushed local edits.
+The guard for `pearl-fetch-all-comments': replacing the thread would discard a
+comment whose body was edited but not yet saved, so the fetch refuses while any
+exist."
+ (save-excursion
+ (pearl--goto-issue-heading-or-error)
+ (consp (pearl--changed-comment-candidates))))
+
+;;;###autoload
+(defun pearl-fetch-all-comments ()
+ "Fetch every comment on the Linear issue at point and render the full thread.
+The bulk/view fetch shows only the newest `pearl-list-comments-shown' comments
+\(with a ` N/M+' marker when more exist); this pages the issue's whole comment
+connection and replaces its Comments subtree with the complete set. Works from
+anywhere inside the issue subtree. Refuses when the issue has unpushed local
+comment edits, so the fetch never clobbers an unsent change -- save or discard
+those first."
+ (interactive)
+ (pearl--require-account-context)
+ (save-excursion
+ (pearl--goto-issue-heading-or-error)
+ (let ((issue-id (org-entry-get nil "LINEAR-ID"))
+ (ident (or (org-entry-get nil "LINEAR-IDENTIFIER") "this issue"))
+ (marker (point-marker))
+ (buf (current-buffer)))
+ (when (pearl--unpushed-comment-edits-p)
+ (user-error "This issue has unpushed comment edits; save or discard them before fetching the full thread"))
+ (pearl--progress "Fetching all comments for %s..." ident)
+ (pearl--fetch-all-comments-async
+ issue-id
+ (lambda (comments)
+ (cond
+ ((eq comments 'error)
+ (message "Pearl: couldn't fetch comments for %s" ident))
+ ((not (buffer-live-p buf)) nil)
+ (t
+ (with-current-buffer buf
+ (save-excursion
+ (goto-char marker)
+ (pearl--replace-comments-subtree comments))
+ (pearl-highlight-comments))
+ (pearl--surface-buffer buf)
+ (message "Fetched all %d comment%s for %s"
+ (length comments)
+ (if (= 1 (length comments)) "" "s") ident))))))))
+
;;;###autoload
(defun pearl-create-comment (&optional body)
"Add a comment to the Linear issue at point.
@@ -7603,6 +7720,7 @@ body stay."
["Buffer"
("g" "refresh view" pearl-refresh-current-view)
("r" "refresh issue" pearl-refresh-current-issue)
+ ("F" "all comments for issue" pearl-fetch-all-comments)
("b" "open view in Linear" pearl-open-current-view-in-linear)
("o" "open issue in browser" pearl-open-current-issue)
("y" "copy issue URL" pearl-copy-issue-url)]
@@ -7642,11 +7760,13 @@ body stay."
(define-key map "o" (cons "my open issues" #'pearl-list-issues))
(define-key map "p" (cons "by project" #'pearl-list-issues-by-project))
(define-key map "f" (cons "build a filter" #'pearl-list-issues-filtered))
+ (define-key map "c" (cons "all comments for issue" #'pearl-fetch-all-comments))
map)
"Pearl issue-fetch commands; a sub-keymap of `pearl-prefix-map'.
Issue sources only -- pick a source, your open issues, a project, or an
-ad-hoc filter. View-movement commands (run / publish / save-locally) live
-in `pearl-views-map' so this group stays one concern.")
+ad-hoc filter -- plus `c' to pull the full comment thread of the issue at
+point. View-movement commands (run / publish / save-locally) live in
+`pearl-views-map' so this group stays one concern.")
(defvar pearl-views-map
(let ((map (make-sparse-keymap)))