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