diff options
| author | Craig Jennings <c@cjennings.net> | 2026-05-25 21:22:27 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-05-25 21:22:27 -0500 |
| commit | 60b51e1c996f1c8f152696946b9440f26ea9a956 (patch) | |
| tree | 3154332cc2b7d5bb3e0eb71a812d9effc8c46633 /pearl.el | |
| parent | f9841539b3adb192d0c15470fa9c31d6cbbdf447 (diff) | |
| download | pearl-60b51e1c996f1c8f152696946b9440f26ea9a956.tar.gz pearl-60b51e1c996f1c8f152696946b9440f26ea9a956.zip | |
refactor(save): retire the immediate-push field setters for buffer-editors
The structured fields now reconcile at save (the previous commits), so the immediate-push setters are the duplicate path that made the package feel like it had two save models. I retired them. pearl-set-priority is deleted outright — priority is edited org-natively through the heading cookie. pearl-set-state, pearl-set-assignee, and pearl-set-labels become pearl-edit-state / -assignee / -labels: the same completing-read picker, but it writes the buffer representation (keyword/name/id, the assignee drawer, the labels drawer plus the live LINEAR-LABEL-IDS set) and marks the field dirty instead of pushing. The change goes out at the next pearl-save-issue / pearl-save-all. pearl-compose-current-description is renamed pearl-edit-description (its compose-and-push behavior is unchanged — v2 doesn't touch the compose buffers).
The transient, the C-; L e keymap, the affordance preamble, and the README follow the new names, and the priority slots are dropped (no command — it's the org cookie). Deleting the setters left pearl--push-issue-field and pearl--priority-choices with no callers, so both are gone, along with the killed-buffer test that only exercised the removed helper. I filed a follow-up: the atomic savers' async commit callbacks need the same killed-buffer guard that helper carried.
The setter command tests are rewritten to assert the buffer-write-and-no-push contract. The reconcile-and-push path they used to cover is exercised in test-pearl-save.
Diffstat (limited to 'pearl.el')
| -rw-r--r-- | pearl.el | 153 |
1 files changed, 47 insertions, 106 deletions
@@ -2613,7 +2613,7 @@ the new comment is the viewer's own, so it renders editable." issue-id marker (pearl--org-to-md org)))))))) ;;;###autoload -(defun pearl-compose-current-description () +(defun pearl-edit-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 @@ -2718,10 +2718,6 @@ the generic mutation the field commands share." :updated-at (cdr (assoc 'updatedAt issue)))))) (lambda (_error _response _data) (funcall callback '(:success nil)))))) -(defconst pearl--priority-choices - '(("None" . 0) ("Urgent" . 1) ("High" . 2) ("Medium" . 3) ("Low" . 4)) - "Linear priority names mapped to their numeric API values.") - (defun pearl--set-priority-cookie (priority-num) "Set the Org priority cookie on the heading at point from PRIORITY-NUM. 1-4 map to #A-#D; 0 (None) removes the cookie. The priority range is bound @@ -2737,46 +2733,6 @@ locally so #D is accepted regardless of the user's `org-priority' settings." (4 (org-priority ?D)) (_ (org-priority 'remove)))))) -;;;###autoload -(defun pearl--push-issue-field (issue-id marker fields on-success success-message fail-message) - "Push FIELDS (an `issueUpdate' input alist) for ISSUE-ID. -On success, run the ON-SUCCESS thunk at MARKER, report SUCCESS-MESSAGE, and -surface the buffer; on failure report FAIL-MESSAGE. The shared push-and-report -tail of the field-setter commands (priority/state/assignee/labels)." - (pearl--update-issue-async - issue-id fields - (lambda (result) - (if (plist-get result :success) - ;; The push may complete after the buffer was killed; only touch it - ;; when it's still live, but still report the (remote) success. - (progn - (when (buffer-live-p (marker-buffer marker)) - (save-excursion - (goto-char marker) - (funcall on-success)) - (pearl--surface-buffer (marker-buffer marker))) - (message "%s" success-message)) - (message "%s" fail-message))))) - -(defun pearl-set-priority (priority-name) - "Set the priority of the Linear issue at point to PRIORITY-NAME. -Interactively, completes over None/Urgent/High/Medium/Low. Pushes the numeric -priority to Linear and rewrites the heading cookie on success. Works from -anywhere inside an issue subtree." - (interactive - (list (completing-read "Priority: " pearl--priority-choices nil t))) - (save-excursion - (pearl--goto-issue-heading-or-error) - (let ((issue-id (org-entry-get nil "LINEAR-ID")) - (priority-num (cdr (assoc priority-name pearl--priority-choices))) - (marker (point-marker))) - (unless priority-num - (user-error "Unknown priority: %s" priority-name)) - (pearl--push-issue-field - issue-id marker `(("priority" . ,priority-num)) - (lambda () (pearl--set-priority-cookie priority-num)) - (format "Set %s priority to %s" issue-id priority-name) - (format "Failed to set priority for %s" issue-id))))) (defun pearl--set-heading-state (state-name state-id) "Update the heading at point to STATE-NAME / STATE-ID. @@ -2792,11 +2748,12 @@ second push back to Linear." (org-todo (pearl--map-linear-state-to-org state-name))))) ;;;###autoload -(defun pearl-set-state (state-name) - "Set the workflow state of the Linear issue at point to STATE-NAME. -Interactively, completes over the issue team's workflow states. Pushes the -resolved state id to Linear and updates the heading keyword and drawer on -success. Works from anywhere inside an issue subtree." +(defun pearl-edit-state (state-name) + "Set the workflow state of the issue at point to STATE-NAME in the buffer. +Completes over the issue team's workflow states and writes the keyword, name, +and id. The change is reconciled and pushed at the next save, not immediately. +Works from anywhere inside an issue subtree. This picker reaches any state, +including one a TODO-keyword cycle can't express." (interactive (let ((team-id (save-excursion (when (ignore-errors (org-back-to-heading t) t) @@ -2808,20 +2765,15 @@ success. Works from anywhere inside an issue subtree." nil t)))) (save-excursion (pearl--goto-issue-heading-or-error) - (let* ((issue-id (org-entry-get nil "LINEAR-ID")) - (team-id (org-entry-get nil "LINEAR-TEAM-ID")) - (marker (point-marker)) + (let* ((team-id (org-entry-get nil "LINEAR-TEAM-ID")) (states (and team-id (pearl--team-states team-id))) (state (seq-find (lambda (s) (string= (cdr (assoc 'name s)) state-name)) states)) (state-id (and state (cdr (assoc 'id state))))) (unless state-id (user-error "No workflow state named %s in this team" state-name)) - (pearl--push-issue-field - issue-id marker `(("stateId" . ,state-id)) - (lambda () (pearl--set-heading-state state-name state-id)) - (format "Set %s state to %s" issue-id state-name) - (format "Failed to set state for %s" issue-id))))) + (pearl--set-heading-state state-name state-id) + (message "Set state to %s (save to push)" state-name)))) (defun pearl--team-collection-names (kind team-id) "Return the display labels of the KIND collection for TEAM-ID, for completion." @@ -2829,11 +2781,12 @@ success. Works from anywhere inside an issue subtree." (and team-id (pearl--team-collection kind team-id)))) ;;;###autoload -(defun pearl-set-assignee (assignee-name) - "Set the assignee of the Linear issue at point to ASSIGNEE-NAME. -Interactively, completes over the issue team's members. Resolves the name to -a user id, pushes it, and updates the assignee drawer on success. Works from -anywhere inside an issue subtree." +(defun pearl-edit-assignee (assignee-name) + "Set the assignee of the issue at point to ASSIGNEE-NAME in the buffer. +Completes over the issue team's members, resolves the name to a user id, and +writes the assignee drawer; the change is reconciled and pushed at the next +`pearl-save-issue' / `pearl-save-all', not immediately. Works from anywhere +inside an issue subtree." (interactive (let ((team-id (save-excursion (when (ignore-errors (org-back-to-heading t) t) @@ -2843,28 +2796,23 @@ anywhere inside an issue subtree." nil t)))) (save-excursion (pearl--goto-issue-heading-or-error) - (let* ((issue-id (org-entry-get nil "LINEAR-ID")) - (team-id (org-entry-get nil "LINEAR-TEAM-ID")) - (marker (point-marker)) + (let* ((team-id (org-entry-get nil "LINEAR-TEAM-ID")) (assignee-id (and team-id (pearl--resolve-team-id 'members assignee-name team-id)))) (unless assignee-id (user-error "No team member matching %s" assignee-name)) - (pearl--push-issue-field - issue-id marker `(("assigneeId" . ,assignee-id)) - (lambda () - (org-entry-put nil "LINEAR-ASSIGNEE-NAME" assignee-name) - (org-entry-put nil "LINEAR-ASSIGNEE-ID" assignee-id)) - (format "Set %s assignee to %s" issue-id assignee-name) - (format "Failed to set assignee for %s" issue-id))))) + (org-entry-put nil "LINEAR-ASSIGNEE-NAME" assignee-name) + (org-entry-put nil "LINEAR-ASSIGNEE-ID" assignee-id) + (message "Set assignee to %s (save to push)" assignee-name)))) ;;;###autoload -(defun pearl-set-labels (label-names) - "Set the labels of the Linear issue at point to LABEL-NAMES. -Interactively, completes (multiple) over the issue team's labels; an empty -selection clears the labels. Resolves each name to a label id, pushes the id -list, and updates the labels drawer on success. Works from anywhere inside an -issue subtree." +(defun pearl-edit-labels (label-names) + "Set the labels of the issue at point to LABEL-NAMES in the buffer. +Completes (multiple) over the issue team's labels; an empty selection clears +them. Resolves each name to a label id and writes the labels drawer plus the +`LINEAR-LABEL-IDS' live set; the change is reconciled and pushed at the next +`pearl-save-issue' / `pearl-save-all', not immediately. Works from anywhere +inside an issue subtree." (interactive (let ((team-id (save-excursion (when (ignore-errors (org-back-to-heading t) t) @@ -2874,23 +2822,18 @@ issue subtree." (pearl--team-collection-names 'labels team-id))))) (save-excursion (pearl--goto-issue-heading-or-error) - (let* ((issue-id (org-entry-get nil "LINEAR-ID")) - (team-id (org-entry-get nil "LINEAR-TEAM-ID")) - (marker (point-marker)) + (let* ((team-id (org-entry-get nil "LINEAR-TEAM-ID")) (label-ids (mapcar (lambda (name) (or (and team-id (pearl--resolve-team-id 'labels name team-id)) (user-error "No label matching %s" name))) label-names))) - (pearl--push-issue-field - issue-id marker `(("labelIds" . ,label-ids)) - (lambda () - (org-entry-put nil "LINEAR-LABELS" - (format "[%s]" (mapconcat #'identity label-names ", ")))) - (format "Set %s labels to %s" issue-id - (if label-names (mapconcat #'identity label-names ", ") "none")) - (format "Failed to set labels for %s" issue-id))))) + (org-entry-put nil "LINEAR-LABELS" + (format "[%s]" (mapconcat #'identity label-names ", "))) + (org-entry-put nil "LINEAR-LABEL-IDS" (string-join label-ids " ")) + (message "Set labels to %s (save to push)" + (if label-names (mapconcat #'identity label-names ", ") "none"))))) ;;; User-facing Commands (Async) @@ -2952,9 +2895,9 @@ page cap was hit. Pure function, no side effects." (insert (format "#+LINEAR-TRUNCATED: %s\n" (if truncated "yes" "no"))) ;; Affordance preamble (org comments -- not rendered content). (insert "#\n") - (insert "# Body = the issue description; edit it, then M-x pearl-sync-current-issue to push.\n") + (insert "# Body = the issue description; edit it, then M-x pearl-save-issue to push.\n") (insert "# Comments subtree = the thread; add with M-x pearl-add-comment.\n") - (insert "# Drawer fields change via M-x pearl-set-priority / -state / -assignee / -labels.\n") + (insert "# State/assignee/labels: M-x pearl-edit-state / -assignee / -labels; priority: the org cookie (C-c ,). All push at save.\n") (insert "# Refresh with M-x pearl-refresh-current-view (whole file) or -current-issue (one).\n") (insert "\n") @@ -4693,12 +4636,11 @@ body stay." ("e" "save ticket" pearl-save-issue) ("E" "save all" pearl-save-all)] ["Edit" - ("D" "edit description" pearl-compose-current-description) + ("D" "edit description" pearl-edit-description) ("M" "edit comment" pearl-edit-current-comment) - ("P" "edit priority" pearl-set-priority) - ("s" "edit state" pearl-set-state) - ("a" "edit assignee" pearl-set-assignee) - ("L" "edit labels" pearl-set-labels)] + ("s" "edit state" pearl-edit-state) + ("a" "edit assignee" pearl-edit-assignee) + ("L" "edit labels" pearl-edit-labels)] ["New" ("n" "new ticket" pearl-new-issue) ("c" "new comment" pearl-add-comment)] @@ -4737,10 +4679,10 @@ body stay." ;; verb-matched label (e.g. "edit priority") without pearl depending on ;; which-key. Lookups still resolve straight to the command. ;; -;; The labels follow the edit/new verbs even though some commands keep older -;; names (`pearl-compose-current-description', `pearl-set-state', and -;; `pearl-add-comment'); aligning the command names is a separate task. The -;; `d c' delete-comment slot is intentionally empty until that command exists. +;; The labels follow the edit/new verbs. The edit commands are named to match +;; (`pearl-edit-description', `pearl-edit-state', ...); the new-comment command +;; keeps its `pearl-add-comment' name pending that rename. Priority has no edit +;; command -- it is edited org-natively via the heading cookie (C-c , / S-up). (defvar pearl-save-map (let ((map (make-sparse-keymap))) @@ -4751,12 +4693,11 @@ body stay." (defvar pearl-edit-map (let ((map (make-sparse-keymap))) - (define-key map "d" (cons "edit description" #'pearl-compose-current-description)) + (define-key map "d" (cons "edit description" #'pearl-edit-description)) (define-key map "c" (cons "edit comment" #'pearl-edit-current-comment)) - (define-key map "p" (cons "edit priority" #'pearl-set-priority)) - (define-key map "s" (cons "edit state" #'pearl-set-state)) - (define-key map "a" (cons "edit assignee" #'pearl-set-assignee)) - (define-key map "l" (cons "edit labels" #'pearl-set-labels)) + (define-key map "s" (cons "edit state" #'pearl-edit-state)) + (define-key map "a" (cons "edit assignee" #'pearl-edit-assignee)) + (define-key map "l" (cons "edit labels" #'pearl-edit-labels)) map) "Pearl edit commands; a sub-keymap of `pearl-prefix-map'.") |
