aboutsummaryrefslogtreecommitdiff
path: root/pearl.el
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-05-25 05:00:48 -0500
committerCraig Jennings <c@cjennings.net>2026-05-25 05:00:48 -0500
commit9b17dc89fc25417e207943dafe8bf09ac211ae32 (patch)
treec92779db74b0a4cccc6e38d7307c781aa62e9356 /pearl.el
parent9f15d1823f388910f695da25ad5878312cc80330 (diff)
downloadpearl-9b17dc89fc25417e207943dafe8bf09ac211ae32.tar.gz
pearl-9b17dc89fc25417e207943dafe8bf09ac211ae32.zip
feat: add pearl-delete-current-comment
Pearl could render, create, and edit comments but not delete one, so removing your own comment meant leaving Emacs for the web app. I added the delete command and filled the reserved C-; L d c keymap slot. pearl-delete-current-comment runs from inside a comment's subtree. It's own-only, reusing the viewer gate from pearl-edit-current-comment: a comment authored by someone else, a bot, or an integration is refused with no commentDelete call. It confirms before the destructive mutation, and on success removes that comment's Org subtree, leaving sibling comments and the issue body untouched. pearl--delete-comment-async mirrors pearl--delete-issue-async. The delete is permanent. I verified Linear's commentDelete against the live API: the comment is not found immediately after, with no restore path, so unlike issueDelete this isn't a recoverable soft delete. Because of that, a comment with unsaved local edits (or no stored hash) gets a stronger "discard your local edits" confirmation rather than the plain prompt. I surfaced it as K in the transient Delete group and as d c in the prefix keymap, and inverted the keymap test that asserted d c was unbound.
Diffstat (limited to 'pearl.el')
-rw-r--r--pearl.el76
1 files changed, 75 insertions, 1 deletions
diff --git a/pearl.el b/pearl.el
index fc7667e..2ec5497 100644
--- a/pearl.el
+++ b/pearl.el
@@ -4068,6 +4068,23 @@ CALLBACK is called with a plist (:success BOOL)."
(funcall callback (list :success success))))
(lambda (_error _response _data) (funcall callback '(:success nil))))))
+(defun pearl--delete-comment-async (comment-id callback)
+ "Delete COMMENT-ID on Linear via commentDelete.
+CALLBACK receives a plist (:success BOOL). Verified live (2026-05-25):
+`commentDelete' removes the comment immediately with no API restore path, so
+unlike `issueDelete' this is not a recoverable soft delete."
+ (let ((query "mutation CommentDelete($id: String!) {
+ commentDelete(id: $id) { success }
+ }")
+ (variables `(("id" . ,comment-id))))
+ (pearl--graphql-request-async
+ query variables
+ (lambda (data)
+ (let ((payload (cdr (assoc 'commentDelete (assoc 'data data)))))
+ (funcall callback
+ (list :success (eq t (cdr (assoc 'success payload)))))))
+ (lambda (_error _response _data) (funcall callback '(:success nil))))))
+
(defun pearl--apply-comment-highlights (viewer-id)
"Color every comment heading in the buffer by editability for VIEWER-ID.
The viewer's own comments get `pearl-editable-comment'; all others get
@@ -4135,6 +4152,61 @@ reported (refresh to reconcile)."
marker (and viewer (plist-get viewer :id))
(lambda (outcome) (pearl--report-save-outcome outcome buf))))))))))
+;;;###autoload
+(defun pearl-delete-current-comment ()
+ "Delete the comment at point on Linear after confirmation, removing its subtree.
+Works from anywhere inside a comment's subtree. Only your own comments are
+deletable: a comment authored by anyone else (or a bot/integration) is refused
+without a `commentDelete' call. The delete is permanent -- Linear's
+`commentDelete' has no API restore path -- so a locally edited comment, or one
+with no stored hash, gets a stronger discard-local-edits confirmation. On
+success the comment's Org subtree is removed; sibling comments and the issue
+body stay."
+ (interactive)
+ (save-excursion
+ (pearl--goto-heading-or-error "Not on a Linear comment")
+ (let ((comment-id (org-entry-get nil "LINEAR-COMMENT-ID"))
+ (author-id (org-entry-get nil "LINEAR-COMMENT-AUTHOR-ID"))
+ (stored (org-entry-get nil "LINEAR-COMMENT-SHA256"))
+ (marker (point-marker))
+ (buf (current-buffer)))
+ (unless comment-id
+ (user-error "Not on a Linear comment"))
+ (pearl--viewer-async
+ (lambda (viewer)
+ (cond
+ ((null viewer)
+ (message "Could not determine your Linear identity; not deleting"))
+ ((not (pearl--comment-editable-p author-id (plist-get viewer :id)))
+ (message "You can only delete your own comments"))
+ (t
+ ;; A clean comment (stored hash present and matching the rendered
+ ;; body) gets the plain prompt; a locally edited body or a missing
+ ;; hash (unknown provenance) gets the discard wording. The delete
+ ;; proceeds either way (decision 2).
+ (let* ((clean (and stored
+ (string= (secure-hash 'sha256
+ (pearl--org-to-md
+ (org-with-point-at marker
+ (pearl--issue-body-at-point))))
+ stored)))
+ (prompt (if clean
+ "Delete this comment from Linear and remove it here? "
+ "Delete this comment from Linear and discard your local edits here? ")))
+ (when (yes-or-no-p prompt)
+ (pearl--delete-comment-async
+ comment-id
+ (lambda (result)
+ (if (plist-get result :success)
+ (when (buffer-live-p buf)
+ (org-with-point-at marker
+ (org-back-to-heading t)
+ (delete-region (point)
+ (progn (org-end-of-subtree t t) (point))))
+ (message "Deleted comment from Linear")
+ (pearl--surface-buffer buf))
+ (message "Failed to delete comment")))))))))))))
+
;;; Transient Menu
;;;###autoload (autoload 'pearl-menu "pearl" nil t)
@@ -4159,7 +4231,8 @@ reported (refresh to reconcile)."
("n" "new ticket" pearl-new-issue)
("c" "new comment" pearl-add-comment)]
["Delete"
- ("k" "delete ticket" pearl-delete-current-issue)]]
+ ("k" "delete ticket" pearl-delete-current-issue)
+ ("K" "delete comment" pearl-delete-current-comment)]]
["Workspace"
["Fetch"
("l" "my open issues" pearl-list-issues)
@@ -4225,6 +4298,7 @@ reported (refresh to reconcile)."
(defvar pearl-delete-map
(let ((map (make-sparse-keymap)))
(define-key map "t" (cons "delete ticket" #'pearl-delete-current-issue))
+ (define-key map "c" (cons "delete comment" #'pearl-delete-current-comment))
map)
"Pearl delete commands; a sub-keymap of `pearl-prefix-map'.")