aboutsummaryrefslogtreecommitdiff
path: root/pearl.el
diff options
context:
space:
mode:
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)