diff options
Diffstat (limited to 'pearl.el')
| -rw-r--r-- | pearl.el | 156 |
1 files changed, 138 insertions, 18 deletions
@@ -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) |
