aboutsummaryrefslogtreecommitdiff
path: root/pearl.el
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-05-24 17:12:52 -0500
committerCraig Jennings <c@cjennings.net>2026-05-24 17:12:52 -0500
commitbb30b0f9146722b279832a991540917c3fa3cb81 (patch)
tree01ebe73400e3aa28136764c180fe174db9900839 /pearl.el
parent424d7048d5450131283f6bdb99822aa6bccd6b16 (diff)
downloadpearl-bb30b0f9146722b279832a991540917c3fa3cb81.tar.gz
pearl-bb30b0f9146722b279832a991540917c3fa3cb81.zip
feat: compose comments and descriptions in an Org buffer
The minibuffer is too cramped to write a real comment or description. I added a shared compose buffer: a focused Org buffer with a read-only instructional header at the top (like a git commit template) and an editable body below, where C-c C-c submits and C-c C-k cancels. It's the sibling of the smerge conflict buffer, built the same way. Two commands use it. pearl-add-comment, run interactively, now opens the composer and converts the Org body to Markdown before sending. Called with an explicit body (tests, programmatic callers), it still sends that directly. pearl-compose-current-description is new: it pops the issue's current description into the composer and, on submit, writes it back into the body and syncs through the existing conflict gate. Both work from anywhere inside an issue subtree. The header is genuinely uneditable: read-only text properties, with only the last character rear-nonsticky so the body stays editable while edits inside the header are refused. The body is everything below it, extracted by a marker. I left pearl-new-issue on the minibuffer for now. Wiring its description into the composer means restructuring that long, untested interactive flow to defer the create into the submit callback, which is worth doing on its own rather than riding along here. Filed as a follow-up.
Diffstat (limited to 'pearl.el')
-rw-r--r--pearl.el156
1 files changed, 138 insertions, 18 deletions
diff --git a/pearl.el b/pearl.el
index b939ab3..56b6d44 100644
--- a/pearl.el
+++ b/pearl.el
@@ -2013,6 +2013,82 @@ nothing is lost."
(message "Synced merged %s to Linear" label))
(message "Failed to push merged %s" label)))))))))
+;;; Compose Buffer
+;;
+;; A focused Org buffer for composing multi-line text (comments, descriptions)
+;; that's awkward in the one-line minibuffer. A read-only instructional header
+;; sits at the top, like a git commit template; the editable body is below it.
+;; C-c C-c hands the body to an armed callback, C-c C-k aborts. The shared
+;; sibling of the smerge conflict buffer below.
+
+(defvar-local pearl--compose-on-finish nil
+ "Callback invoked with the composed Org body when a compose buffer submits.")
+
+(defvar-local pearl--compose-body-start nil
+ "Marker at the start of the editable body, just past the read-only header.")
+
+(defconst pearl--compose-comment-instructions
+ "# Write a comment below, then C-c C-c to send or C-c C-k to cancel.
+# This is Org markup; it is converted to Markdown for Linear.
+"
+ "Read-only header shown atop the comment compose buffer.")
+
+(defconst pearl--compose-description-instructions
+ "# Edit the description below, then C-c C-c to sync or C-c C-k to cancel.
+# This is Org markup; it is converted to Markdown for Linear.
+# The usual conflict check still applies on sync.
+"
+ "Read-only header shown atop the description compose buffer.")
+
+(defun pearl--compose-body ()
+ "Return the trimmed editable body of the current compose buffer.
+The text below the read-only header, from `pearl--compose-body-start' on."
+ (string-trim
+ (buffer-substring-no-properties pearl--compose-body-start (point-max))))
+
+(defun pearl--compose-submit ()
+ "Submit the compose buffer: hand the body to the armed callback, kill the buffer."
+ (interactive)
+ (let ((body (pearl--compose-body))
+ (callback pearl--compose-on-finish))
+ (kill-buffer (current-buffer))
+ (when callback (funcall callback body))))
+
+(defun pearl--compose-abort ()
+ "Abort the compose buffer without submitting."
+ (interactive)
+ (kill-buffer (current-buffer))
+ (message "Compose canceled"))
+
+(defun pearl--compose-in-buffer (label instructions initial on-finish)
+ "Pop an Org compose buffer for LABEL with a read-only INSTRUCTIONS header.
+INITIAL is the editable body (Org markup, may be empty). \\<global-map>C-c C-c
+\(`pearl--compose-submit') hands the body to ON-FINISH and kills the buffer;
+C-c C-k (`pearl--compose-abort') cancels. ON-FINISH receives the Org body and
+is responsible for any markdown conversion. The shared multi-line composer,
+sibling of `pearl--resolve-conflict-in-smerge'."
+ (let ((buf (get-buffer-create (format "*pearl-compose: %s*" label))))
+ (with-current-buffer buf
+ (let ((inhibit-read-only t))
+ (erase-buffer)
+ ;; `org-mode' clears buffer-local vars, so set ours after it
+ (org-mode)
+ (insert instructions)
+ (let ((end (point)))
+ ;; the whole header is read-only; only its last char is rear-nonsticky
+ ;; so the body inserted just after stays editable (the interior is not,
+ ;; so edits inside the header are refused)
+ (add-text-properties (point-min) end '(read-only t))
+ (add-text-properties (1- end) end '(rear-nonsticky t))
+ (setq pearl--compose-body-start (copy-marker end nil)))
+ (insert (or initial "")))
+ (setq-local pearl--compose-on-finish on-finish)
+ (local-set-key (kbd "C-c C-c") #'pearl--compose-submit)
+ (local-set-key (kbd "C-c C-k") #'pearl--compose-abort)
+ (goto-char (point-max)))
+ (pop-to-buffer buf)
+ buf))
+
(defvar-local pearl--conflict-on-finish nil
"Callback invoked with the reconciled text when a conflict buffer commits.")
@@ -2430,30 +2506,73 @@ that subtree at the end of the issue when it does not exist yet."
(insert "*** Comments\n" (pearl--format-comment comment))))))
;;;###autoload
-(defun pearl-add-comment (body)
- "Add a comment with BODY to the Linear issue at point and insert it.
-Works from anywhere inside an issue subtree. The new comment is the viewer's
-own, so it renders editable; edit it later with
-`pearl-edit-current-comment'."
- (interactive "sComment: ")
+(defun pearl--create-and-append-comment (issue-id marker body)
+ "Create a comment with BODY on ISSUE-ID, appending it at MARKER on success.
+MARKER points at the issue heading; the append, comment highlighting, and
+buffer surfacing all run in the marker's buffer, so it works even when the
+create callback fires with another buffer current (the compose path)."
+ (pearl--progress "Adding comment to %s..." issue-id)
+ (pearl--create-comment-async
+ issue-id body
+ (lambda (comment)
+ (if (null comment)
+ (message "Failed to add comment to %s" issue-id)
+ (let ((buf (marker-buffer marker)))
+ (when (buffer-live-p buf)
+ (with-current-buffer buf
+ (save-excursion
+ (goto-char marker)
+ (pearl--append-comment-to-issue comment))
+ (pearl-highlight-comments))
+ (pearl--surface-buffer buf)))
+ (message "Added comment to %s" issue-id)))))
+
+;;;###autoload
+(defun pearl-add-comment (&optional body)
+ "Add a comment to the Linear issue at point.
+Interactively, opens an Org compose buffer (C-c C-c sends, C-c C-k cancels) and
+converts the composed Org to Markdown before sending -- room to write a real
+comment instead of the one-line minibuffer. BODY, when supplied
+non-interactively, is sent as-is. Works from anywhere inside an issue subtree;
+the new comment is the viewer's own, so it renders editable."
+ (interactive (list nil))
(save-excursion
(pearl--goto-heading-or-error)
(let ((issue-id (org-entry-get nil "LINEAR-ID"))
(marker (point-marker)))
(unless issue-id
(user-error "Not on a Linear issue heading"))
- (pearl--progress "Adding comment to %s..." issue-id)
- (pearl--create-comment-async
- issue-id body
- (lambda (comment)
- (if (null comment)
- (message "Failed to add comment to %s" issue-id)
- (save-excursion
- (goto-char marker)
- (pearl--append-comment-to-issue comment))
- (pearl-highlight-comments)
- (pearl--surface-buffer (marker-buffer marker))
- (message "Added comment to %s" issue-id)))))))
+ (if body
+ (pearl--create-and-append-comment issue-id marker body)
+ (pearl--compose-in-buffer
+ (format "comment on %s" issue-id)
+ pearl--compose-comment-instructions ""
+ (lambda (org)
+ (pearl--create-and-append-comment
+ issue-id marker (pearl--org-to-md org))))))))
+
+;;;###autoload
+(defun pearl-compose-current-description ()
+ "Edit the description of the Linear issue at point in an Org compose buffer.
+Pops the current description into a focused buffer; C-c C-c writes it back into
+the issue body and syncs to Linear through the usual conflict gate, C-c C-k
+cancels. An alternative to editing the description inline for anyone who wants
+a dedicated buffer. Works from anywhere inside an issue subtree."
+ (interactive)
+ (save-excursion
+ (pearl--goto-heading-or-error)
+ (let ((issue-id (org-entry-get nil "LINEAR-ID"))
+ (marker (point-marker)))
+ (unless issue-id
+ (user-error "Not on a Linear issue heading"))
+ (pearl--compose-in-buffer
+ (format "description for %s" issue-id)
+ pearl--compose-description-instructions
+ (org-with-point-at marker (pearl--issue-body-at-point))
+ (lambda (org)
+ (org-with-point-at marker
+ (pearl--set-entry-body-at-point org)
+ (pearl-sync-current-issue)))))))
;;;###autoload
(defun pearl-open-current-issue ()
@@ -3674,6 +3793,7 @@ reported (refresh to reconcile)."
("b" "Open view in Linear" pearl-open-current-view-in-linear)]
["Issue at point"
("e" "Edit desc -> push" pearl-sync-current-issue)
+ ("D" "Compose desc -> push" pearl-compose-current-description)
("t" "Edit title -> push" pearl-sync-current-issue-title)
("s" "Set state" pearl-set-state)
("a" "Set assignee" pearl-set-assignee)