branch: elpa/gptel
commit 00bcdf0551f97e0b496614a6dcebd5fdeda4751b
Author: Karthik Chikmagalur <karthikchikmaga...@gmail.com>
Commit: Karthik Chikmagalur <karthikchikmaga...@gmail.com>

    gptel-rewrite: Make minibuffer reads editable in a buffer
    
    When gptel asks you to type in a prompt or instruction in the
    minibuffer, there is a hidden feature: you can switch to editing
    it in a dedicated buffer with C-c C-e.  In Emacs 29.1 and up, the
    `string-edit' package provides this out of the box, but we roll
    our own since gptel supports Emacs 27.2+ and `string-edit' is not
    included in the compat library.
    
    This feature is undocumented as there is nowhere to display this
    without overwhelming the user.  This commit documents changes to
    this "directive-editor" feature.
    
    * gptel-transient.el (gptel--edit-directive, gptel--suffix-send):
    Add the ability to switch to a directive-editor buffer when typing
    in the prompt in the minibuffer, with C-c C-e (#933).  This
    feature is already available in other contexts.
    
    Make the directive editor callback behavior distinguish between a
    successful and aborted edit.  This is required for specifying
    rewrite messages via the directive editor.
    
    (gptel--read-crowdsourced-prompt, gptel--suffix-system-message):
    The crowdsourced system prompt editor and the gptel directive
    editor now take you back to gptel-menu when you're done. (#965)
    
    (gptel--infix-add-directive): Add TODO about allowing composition
    via the directive editor (C-c C-e) for this infix.  This feature
    does not exist yet because of issues with communicating the
    additional instruction as a scope argument to gptel-menu.  Unlike
    in the other cases, there is no elisp symbol to be set here, the
    result is an infix value that needs to be part of the prefix's
    scope.
    
    * gptel-rewrite.el (gptel--rewrite-read-message,
    gptel--suffix-rewrite-directive): Confirming a rewrite message in
    the directive editor now starts the rewrite, and canceling it
    aborts the rewrite.
---
 gptel-rewrite.el   | 15 +++++-----
 gptel-transient.el | 88 ++++++++++++++++++++++++++++++++++--------------------
 2 files changed, 62 insertions(+), 41 deletions(-)

diff --git a/gptel-rewrite.el b/gptel-rewrite.el
index 803276acaf4..a38d0807428 100644
--- a/gptel-rewrite.el
+++ b/gptel-rewrite.el
@@ -292,14 +292,12 @@ input history list."
               (gptel--edit-directive 'gptel--rewrite-message
                 :prompt rewrite-directive :initial (minibuffer-contents)
                 :buffer cb :setup (lambda () (ignore-errors (forward-char 
offset)))
-                ;; FIXME: We would like to (conditionally) start the rewrite
-                ;; here.  We can't because this callback is always called, even
-                ;; when quitting the edit buffer.
                 :callback
-                (lambda ()
-                  (run-at-time 0 nil #'transient-setup 'gptel-rewrite)
-                  (push (buffer-local-value 'gptel--rewrite-message cb)
-                        (alist-get 'gptel--infix-rewrite-extra 
transient-history))
+                (lambda (msg)
+                  (when msg
+                    (run-at-time 0 nil #'gptel--suffix-rewrite)
+                    (push (buffer-local-value 'gptel--rewrite-message cb)
+                          (alist-get 'gptel--infix-rewrite-extra 
transient-history)))
                   (when (minibufferp) (minibuffer-quit-recursive-edit)))))))
          (minibuffer-local-map
           (make-composed-keymap (define-keymap
@@ -688,7 +686,8 @@ generated from functions."
   (if cancel (progn (message "Edit canceled")
                     (call-interactively #'gptel-rewrite))
     (gptel--edit-directive 'gptel--rewrite-directive
-      :callback #'gptel-rewrite :setup #'activate-mark)))
+      :callback (lambda (_) (call-interactively #'gptel-rewrite))
+      :setup #'activate-mark)))
 
 (transient-define-suffix gptel--suffix-rewrite (&optional rewrite-message 
dry-run)
   "Rewrite or refactor region contents."
diff --git a/gptel-transient.el b/gptel-transient.el
index f99aca70d24..ba13c5730b7 100644
--- a/gptel-transient.el
+++ b/gptel-transient.el
@@ -1325,6 +1325,9 @@ Or in an extended conversation:
   :argument ":"
   :prompt (concat "Add instructions for next request only ("
                   gptel--read-with-prefix-help ") ")
+  ;; TODO: Add the ability to edit this in a separate buffer, with
+  ;; `gptel--edit-directive'.  This requires setting up gptel-menu with the
+  ;; result as the :scope.
   :reader (lambda (prompt initial history)
             (let* ((directive
                     (car-safe (gptel--parse-directive gptel--system-message 
'raw)))
@@ -1497,14 +1500,28 @@ This sets the variable `gptel-include-tool-results', 
which see."
         (prompt
          (cond
           ((member "m" args)
-           (minibuffer-with-setup-hook
-               (lambda () (add-hook 'completion-at-point-functions
-                               #'gptel-preset-capf nil t))
-             (read-string
-              (format "Ask %s: " (gptel-backend-name gptel-backend))
-              (and (use-region-p)
-                   (buffer-substring-no-properties
-                    (region-beginning) (region-end))))))
+           (let* ((edit-in-buffer
+                   (lambda () (interactive)
+                     (gptel--edit-directive nil
+                       :initial (minibuffer-contents)
+                       :prompt "Edit prompt here"
+                       :setup (lambda () (goto-char (point-max)))
+                       :callback (lambda (msg)
+                                   (if (not msg)
+                                       (minibuffer-quit-recursive-edit)
+                                     (delete-region (minibuffer-prompt-end) 
(point-max))
+                                     (insert msg) (exit-minibuffer))))))
+                  (minibuffer-local-map
+                   (make-composed-keymap (define-keymap "C-c C-e" 
edit-in-buffer)
+                                         minibuffer-local-map)))
+             (minibuffer-with-setup-hook
+                 (lambda () (add-hook 'completion-at-point-functions
+                                 #'gptel-preset-capf nil t))
+               (read-string
+                (format "Ask %s: " (gptel-backend-name gptel-backend))
+                (and (use-region-p)
+                     (buffer-substring-no-properties
+                      (region-beginning) (region-end)))))))
           ((member "y" args)
            (unless (car-safe kill-ring)
              (user-error "`kill-ring' is empty!  Nothing to send"))
@@ -1732,7 +1749,8 @@ This uses the prompts in the variable
         (when-let* ((prompt (gethash choice gptel--crowdsourced-prompts)))
           (gptel--set-with-scope
            'gptel--system-message prompt gptel--set-buffer-locally)
-          (gptel--edit-directive 'gptel--system-message)))
+          (gptel--edit-directive 'gptel--system-message
+            :callback (lambda () (call-interactively #'gptel-menu)))))
     (message "No prompts available.")))
 
 (transient-define-suffix gptel--suffix-system-message (&optional cancel)
@@ -1750,17 +1768,20 @@ generated from functions."
                     "Active directive is dynamically generated: Edit its 
current value instead?")))))
   (if cancel (progn (message "Edit canceled")
                     (call-interactively #'gptel-menu))
-    (gptel--edit-directive 'gptel--system-message :setup #'activate-mark)))
+    (gptel--edit-directive 'gptel--system-message
+      :setup #'activate-mark
+      :callback (lambda (_) (call-interactively #'gptel-menu)))))
 
 ;; MAYBE: Eventually can be simplified with string-edit, after we drop support
 ;; for Emacs 28.2.
-(cl-defun gptel--edit-directive (sym &key prompt initial callback setup buffer)
+(cl-defun gptel--edit-directive (&optional sym &key prompt initial callback 
setup buffer)
   "Edit a gptel directive in a dedicated buffer.
 
-Store the result in SYM, a symbol.  PROMPT and INITIAL are the
-heading and initial text.  If CALLBACK is specified, it is run
-after exiting the edit.  If SETUP is a function, run it after
-setting up the buffer."
+Store the result in SYM, a symbol.  PROMPT and INITIAL are the heading
+and initial text.  If SETUP is a function, run it after setting up the
+buffer.  If CALLBACK is specified, it is run after exiting the edit.  It
+is called with one argument: the buffer text or with nil depending on
+whether the action is confirmed/cancelled."
   (declare (indent 1))
   (let ((orig-buf (or buffer (current-buffer)))
         (msg-start (make-marker))
@@ -1768,7 +1789,7 @@ setting up the buffer."
     (when (functionp directive)
       (setq directive (funcall directive)))
     ;; TODO: Handle editing list-of-strings directives
-    (with-current-buffer (get-buffer-create "*gptel-system*")
+    (with-current-buffer (get-buffer-create "*gptel-prompt*")
       (let ((inhibit-read-only t) (inhibit-message t))
         (erase-buffer)
         (text-mode)
@@ -1797,38 +1818,39 @@ setting up the buffer."
           (push-mark nil 'nomsg))
         (and (functionp setup) (funcall setup)))
       (display-buffer (current-buffer)
-                      `((display-buffer-below-selected)
+                      `((display-buffer-below-selected
+                         display-buffer-use-some-window)
+                        (some-window   . lru)
                         (body-function . ,#'select-window)
                         (window-height . ,#'fit-window-to-buffer)))
       (let ((quit-to-menu
-             (lambda ()
-               "Cancel system message update and return."
-               (interactive)
+             (lambda () "Cancel system message update and return."
                (quit-window)
                (unless (minibufferp)
                  (display-buffer orig-buf
                                  `((display-buffer-reuse-window
                                     display-buffer-use-some-window)
-                                   (body-function . ,#'select-window))))
-               (cond ((commandp callback) (call-interactively callback))
-                     ((functionp callback) (funcall callback))))))
+                                   (body-function . ,#'select-window)))))))
         (use-local-map
          (make-composed-keymap
           (define-keymap
             "C-c C-c"
-            (lambda ()
-              "Confirm system message and return."
+            (lambda () "Confirm system message and return."
               (interactive)
               (let ((system-message
                      (buffer-substring-no-properties msg-start (point-max))))
-                (with-current-buffer orig-buf
-                  (gptel--set-with-scope sym
-                                         (if (cdr-safe directive) ;Handle list 
of strings
-                                             (prog1 directive (setcar 
directive system-message))
-                                           system-message)
-                                         gptel--set-buffer-locally)))
-              (funcall quit-to-menu))
-            "C-c C-k" quit-to-menu)
+                (when sym
+                  (with-current-buffer orig-buf
+                    (gptel--set-with-scope
+                     sym (if (cdr-safe directive) ;Handle list of strings
+                             (prog1 directive (setcar directive 
system-message))
+                           system-message)
+                     gptel--set-buffer-locally)))
+                (funcall quit-to-menu)
+                (when (functionp callback) (funcall callback system-message))))
+            "C-c C-k" (lambda () (interactive)
+                        (funcall quit-to-menu)
+                        (when (functionp callback) (funcall callback nil))))
           text-mode-map))))))
 
 ;; ** Suffix for displaying and removing context

Reply via email to