config

Personal configuration.
git clone git://code.dwrz.net/config
Log | Files | Refs

gptel-rewrite.el (27170B)


      1 ;;; gptel-rewrite.el --- Refactoring functions for gptel  -*- lexical-binding: t; -*-
      2 
      3 ;; Copyright (C) 2024  Karthik Chikmagalur
      4 
      5 ;; Author: Karthik Chikmagalur <karthikchikmagalur@gmail.com>
      6 ;; Keywords: hypermedia, convenience, tools
      7 
      8 ;; This program is free software; you can redistribute it and/or modify
      9 ;; it under the terms of the GNU General Public License as published by
     10 ;; the Free Software Foundation, either version 3 of the License, or
     11 ;; (at your option) any later version.
     12 
     13 ;; This program is distributed in the hope that it will be useful,
     14 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
     15 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     16 ;; GNU General Public License for more details.
     17 
     18 ;; You should have received a copy of the GNU General Public License
     19 ;; along with this program.  If not, see <https://www.gnu.org/licenses/>.
     20 
     21 ;;; Commentary:
     22 
     23 ;;
     24 
     25 ;;; Code:
     26 (require 'gptel-transient)
     27 (require 'cl-lib)
     28 
     29 (defvar eldoc-documentation-functions)
     30 (defvar diff-entire-buffers)
     31 
     32 (declare-function diff-no-select "diff")
     33 
     34 ;; * User options
     35 
     36 (defcustom gptel-rewrite-directives-hook nil
     37   "Hook run to generate gptel's default rewrite directives.
     38 
     39 Each function in this hook is called with no arguments until one
     40 returns a non-nil value, the base string to use as the
     41 rewrite/refactor instruction.
     42 
     43 Use this hook to tailor context-specific refactoring directives.
     44 For example, you can specialize the default refactor directive
     45 for a particular major-mode or project."
     46   :group 'gptel
     47   :type 'hook)
     48 
     49 (defcustom gptel-rewrite-default-action nil
     50   "Action to take when rewriting a text region using gptel.
     51 
     52 When the LLM response with the rewritten text is received, you can
     53 - merge it with the current region, possibly creating a merge conflict,
     54 - diff or ediff against the original region,
     55 - or accept it in place, replacing the original region.
     56 
     57 If this option is nil (the default), gptel waits for an explicit
     58 command.  Set it to the symbol `merge', `diff', `ediff' or
     59 `accept' to automatically do one of these things instead."
     60   :group 'gptel
     61   :type '(choice
     62           (const :tag "Wait" nil)
     63           (const :tag "Merge with current region" merge)
     64           (const :tag  "Diff against current region" diff)
     65           (const :tag "Ediff against current region" ediff)
     66           (const :tag "Accept rewrite" accept)
     67           (function :tag "Custom action")))
     68 
     69 (defface gptel-rewrite-highlight-face
     70   '((((class color) (min-colors 88) (background dark))
     71      :background "#041714" :extend t)
     72     (((class color) (min-colors 88) (background light))
     73      :background "light goldenrod yellow" :extend t)
     74     (t :inherit secondary-selection))
     75   "Face for highlighting regions with pending rewrites."
     76   :group 'gptel)
     77 
     78 ;; * Variables
     79 
     80 (defvar-keymap gptel-rewrite-actions-map
     81   :doc "Keymap for gptel rewrite actions at point."
     82   "RET" #'gptel--rewrite-dispatch
     83   "<mouse-1>" #'gptel--rewrite-dispatch
     84   "C-c C-k" #'gptel--rewrite-reject
     85   "C-c C-a" #'gptel--rewrite-accept
     86   "C-c C-d" #'gptel--rewrite-diff
     87   "C-c C-e" #'gptel--rewrite-ediff
     88   "C-c C-n" #'gptel--rewrite-next
     89   "C-c C-p" #'gptel--rewrite-previous
     90   "C-c C-m" #'gptel--rewrite-merge)
     91 
     92 (defvar-local gptel--rewrite-overlays nil
     93   "List of active rewrite overlays in the buffer.")
     94 
     95 (defvar-local gptel--rewrite-message nil
     96   "Request-specific instructions for a gptel-rewrite action.")
     97 
     98 ;; Add the rewrite directive to `gptel-directives'
     99 (unless (alist-get 'rewrite gptel-directives)
    100   (add-to-list 'gptel-directives `(rewrite . ,#'gptel--rewrite-directive-default)))
    101 
    102 (defvar gptel--rewrite-directive
    103   (or (alist-get 'rewrite gptel-directives)
    104       #'gptel--rewrite-directive-default)
    105   "Active system message for rewrite actions.
    106 
    107 This variable is for internal use only.  To customize the rewrite
    108 system message, set a system message (or function that generates
    109 the system message) as the value of the `rewrite' key in
    110 `gptel-directives':
    111 
    112  (setf (alist-get \\='rewrite gptel-directives)
    113        #\\='my-rewrite-message-generator)
    114 
    115 You can also customize `gptel-rewrite-directives-hook' to
    116 dynamically inject a rewrite-specific system message.")
    117 
    118 (defun gptel--rewrite-directive-default ()
    119   "Generic directive for rewriting or refactoring.
    120 
    121 These are instructions not specific to any particular required
    122 change.
    123 
    124 The returned string is interpreted as the system message for the
    125 rewrite request.  To use your own, add a different directive to
    126 `gptel-directives', or add to `gptel-rewrite-directives-hook',
    127 which see."
    128   (or (save-mark-and-excursion
    129         (run-hook-with-args-until-success
    130          'gptel-rewrite-directives-hook))
    131       (let* ((lang (downcase (gptel--strip-mode-suffix major-mode)))
    132              (article (if (and lang (not (string-empty-p lang))
    133                                (memq (aref lang 0) '(?a ?e ?i ?o ?u)))
    134                           "an" "a")))
    135         (if (derived-mode-p 'prog-mode)
    136             (format (concat "You are %s %s programmer.  "
    137                             "Follow my instructions and refactor %s code I provide.\n"
    138                             "- Generate ONLY %s code as output, without "
    139                             "any explanation or markdown code fences.\n"
    140                             "- Generate code in full, do not abbreviate or omit code.\n"
    141                             "- Do not ask for further clarification, and make "
    142                             "any assumptions you need to follow instructions.")
    143                     article lang lang lang)
    144           (concat
    145            (if (string-empty-p lang)
    146                "You are an editor."
    147              (format "You are %s %s editor." article lang))
    148            "  Follow my instructions and improve or rewrite the text I provide."
    149            "  Generate ONLY the replacement text,"
    150            " without any explanation or markdown code fences.")))))
    151 
    152 ;; * Helper functions
    153 
    154 (defsubst gptel--refactor-or-rewrite ()
    155   "Rewrite should be refactored into refactor.
    156 
    157 Or is it the other way around?"
    158   (if (derived-mode-p 'prog-mode)
    159       "Refactor" "Rewrite"))
    160 
    161 (defun gptel--rewrite-key-help (callback)
    162   "Eldoc documentation function for gptel rewrite actions.
    163 
    164 CALLBACK is supplied by Eldoc, see
    165 `eldoc-documentation-functions'."
    166   (when (and gptel--rewrite-overlays
    167              (get-char-property (point) 'gptel-rewrite))
    168       (funcall callback
    169                (format (substitute-command-keys "%s rewrite available: accept \\[gptel--rewrite-accept], clear \\[gptel--rewrite-reject], merge \\[gptel--rewrite-merge], diff \\[gptel--rewrite-diff] or ediff \\[gptel--rewrite-ediff]")
    170                        (propertize (concat (gptel-backend-name gptel-backend)
    171                                            ":" (gptel--model-name gptel-model))
    172                                    'face 'mode-line-emphasis)))))
    173 
    174 (defun gptel--rewrite-move (search-func)
    175   "Move directionally to a gptel rewrite location using SEARCH-FUNC."
    176   (let* ((ov (cdr (get-char-property-and-overlay (point) 'gptel-rewrite)))
    177          (pt (save-excursion
    178                (if ov
    179                    (goto-char
    180                     (funcall search-func (overlay-start ov) 'gptel-rewrite))
    181                  (goto-char
    182                   (max (1- (funcall search-func (point) 'gptel-rewrite))
    183                        (point-min))))
    184                (funcall search-func (point) 'gptel-rewrite))))
    185     (if (get-char-property pt 'gptel-rewrite)
    186         (goto-char pt)
    187       (user-error "No further rewrite regions!"))))
    188 
    189 (defun gptel--rewrite-next ()
    190   "Go to next pending LLM rewrite in buffer, if one exists."
    191   (interactive)
    192   (gptel--rewrite-move #'next-single-char-property-change))
    193 
    194 (defun gptel--rewrite-previous ()
    195   "Go to previous pending LLM rewrite in buffer, if one exists."
    196   (interactive)
    197   (gptel--rewrite-move #'previous-single-char-property-change))
    198 
    199 (defun gptel--rewrite-overlay-at (&optional pt)
    200   "Check for a gptel rewrite overlay at PT and return it.
    201 
    202 If no suitable overlay is found, raise an error."
    203   (pcase-let ((`(,response . ,ov)
    204                (get-char-property-and-overlay (or pt (point)) 'gptel-rewrite))
    205               (diff-entire-buffers nil))
    206     (unless ov (user-error "Could not find region being rewritten."))
    207     (unless response (user-error "No LLM output available for this rewrite."))
    208     ov))
    209 
    210 (defun gptel--rewrite-prepare-buffer (ovs &optional buf)
    211   "Prepare new buffer with LLM changes applied and return it.
    212 
    213 This is used for (e)diff purposes.
    214 
    215 RESPONSE is the LLM response.  OVS are the overlays specifying
    216 the changed regions. BUF is the (current) buffer."
    217   (setq buf (or buf (overlay-buffer (or (car-safe ovs) ovs))))
    218   (with-current-buffer buf
    219     (let ((pmin (point-min))
    220           (pmax (point-max))
    221           (pt   (point))
    222           ;; (mode major-mode)
    223           (newbuf (get-buffer-create "*gptel-diff*"))
    224           (inhibit-read-only t)
    225           (inhibit-message t))
    226       (save-restriction
    227         (widen)
    228         (with-current-buffer newbuf
    229           (erase-buffer)
    230           (insert-buffer-substring buf)))
    231       (with-current-buffer newbuf
    232         (narrow-to-region pmin pmax)
    233         (goto-char pt)
    234         ;; We mostly just want font-locking
    235         ;; (delay-mode-hooks (funcall mode))
    236         ;; Apply the changes to the new buffer
    237         (save-excursion
    238           (gptel--rewrite-accept ovs newbuf)))
    239       newbuf)))
    240 
    241 ;; * Refactor action functions
    242 
    243 (defun gptel--rewrite-reject (&optional ovs)
    244   "Clear pending LLM responses in OVS or at point."
    245   (interactive (list (gptel--rewrite-overlay-at)))
    246   (dolist (ov (ensure-list ovs))
    247     (setq gptel--rewrite-overlays (delq ov gptel--rewrite-overlays))
    248     (delete-overlay ov))
    249   (unless gptel--rewrite-overlays
    250     (remove-hook 'eldoc-documentation-functions 'gptel--rewrite-key-help 'local))
    251   (message "Cleared pending LLM response(s)."))
    252 
    253 (defun gptel--rewrite-accept (&optional ovs buf)
    254   "Apply pending LLM responses in OVS or at point.
    255 
    256 BUF is the buffer to modify, defaults to the overlay buffer."
    257   (interactive (list (gptel--rewrite-overlay-at)))
    258   (when-let* ((ov-buf (overlay-buffer (or (car-safe ovs) ovs)))
    259               (buf (or buf ov-buf))
    260               ((buffer-live-p buf)))
    261     (with-current-buffer ov-buf
    262       (cl-loop for ov in (ensure-list ovs)
    263                for ov-beg = (overlay-start ov)
    264                for ov-end = (overlay-end ov)
    265                for response = (overlay-get ov 'gptel-rewrite)
    266                do (overlay-put ov 'before-string nil)
    267                (with-current-buffer buf
    268                  (goto-char ov-beg)
    269                  (delete-region ov-beg ov-end)
    270                  (insert response))))
    271     (message "Replaced region(s) with LLM output in buffer: %s."
    272              (buffer-name ov-buf))))
    273 
    274 (defun gptel--rewrite-diff (&optional ovs switches)
    275   "Diff pending LLM responses in OVS or at point."
    276   (interactive (list (gptel--rewrite-overlay-at)))
    277   (when-let* ((ov-buf (overlay-buffer (or (car-safe ovs) ovs)))
    278               ((buffer-live-p ov-buf)))
    279     (let* ((newbuf (gptel--rewrite-prepare-buffer ovs))
    280            (diff-buf (diff-no-select
    281                       (if-let ((buf-file (buffer-file-name ov-buf)))
    282                           (expand-file-name buf-file) ov-buf)
    283                       newbuf switches)))
    284       (with-current-buffer diff-buf
    285         (setq-local diff-jump-to-old-file t))
    286       (display-buffer diff-buf))))
    287 
    288 (defun gptel--rewrite-ediff (&optional ovs)
    289   "Ediff pending LLM responses in OVS or at point."
    290   (interactive (list (gptel--rewrite-overlay-at)))
    291   (when-let* ((ov-buf (overlay-buffer (or (car-safe ovs) ovs)))
    292               ((buffer-live-p ov-buf)))
    293     (letrec ((newbuf (gptel--rewrite-prepare-buffer ovs))
    294              (cwc (current-window-configuration))
    295              (hideshow
    296               (lambda (&optional restore)
    297                 (dolist (ov (ensure-list ovs))
    298                   (when-let ((overlay-buffer ov))
    299                     (let ((disp (overlay-get ov 'display))
    300                           (stored (overlay-get ov 'gptel--ediff)))
    301                       (overlay-put ov 'display (and restore stored))
    302                       (overlay-put ov 'gptel--ediff (unless restore disp)))))))
    303              (gptel--ediff-restore
    304               (lambda ()
    305                 (when (window-configuration-p cwc)
    306                   (set-window-configuration cwc))
    307                 (funcall hideshow 'restore)
    308                 (remove-hook 'ediff-quit-hook gptel--ediff-restore))))
    309       (funcall hideshow)
    310       (add-hook 'ediff-quit-hook gptel--ediff-restore)
    311       (ediff-buffers ov-buf newbuf))))
    312 
    313 (defun gptel--rewrite-merge (&optional ovs)
    314   "Insert pending LLM responses in OVS as merge conflicts."
    315   (interactive (list (gptel--rewrite-overlay-at)))
    316   (when-let* ((ov-buf (overlay-buffer (or (car-safe ovs) ovs)))
    317               ((buffer-live-p ov-buf)))
    318     (with-current-buffer ov-buf
    319       (let ((changed))
    320         (dolist (ov (ensure-list ovs))
    321           (save-excursion
    322             (when-let (new-str (overlay-get ov 'gptel-rewrite))
    323               ;; Insert merge
    324               (goto-char (overlay-start ov))
    325               (unless (bolp) (insert "\n"))
    326               (insert-before-markers "<<<<<<< original\n")
    327               (goto-char (overlay-end ov))
    328               (unless (bolp) (insert "\n"))
    329               (insert
    330                "=======\n" new-str
    331                "\n>>>>>>> " (gptel-backend-name gptel-backend) "\n")
    332               (setq changed t))))
    333         (when changed (smerge-mode 1)))
    334       (gptel--rewrite-reject ovs))))
    335 
    336 (defun gptel--rewrite-dispatch (choice)
    337   "Dispatch actions for gptel rewrites."
    338   (interactive
    339    (list
    340     (if-let* ((ov (cdr-safe (get-char-property-and-overlay (point) 'gptel-rewrite))))
    341       (unwind-protect
    342           (pcase-let ((choices '((?a "accept") (?k "reject") (?m "merge")
    343                                  (?d "diff") (?e "ediff")))
    344                       (hint-str (concat "[" (gptel--model-name gptel-model) "]\n")))
    345             (overlay-put
    346              ov 'before-string
    347              (concat
    348               (unless (eq (char-before (overlay-start ov)) ?\n) "\n")
    349               (propertize "REWRITE READY: " 'face 'success)
    350               (mapconcat (lambda (e) (cdr e)) (mapcar #'rmc--add-key-description choices) ", ")
    351               (propertize
    352                " " 'display `(space :align-to (- right ,(1+ (length hint-str)))))
    353               (propertize hint-str 'face 'success)))
    354             (read-multiple-choice "Action: " choices))
    355         (overlay-put ov 'before-string nil))
    356       (user-error "No gptel rewrite at point!"))))
    357   (call-interactively
    358    (intern (concat "gptel--rewrite-" (cadr choice)))))
    359 
    360 (defun gptel--rewrite-callback (response info)
    361   "Callback for gptel rewrite actions.
    362 
    363 Show the rewrite result in an overlay over the original text, and
    364 set up dispatch actions.
    365 
    366 RESPONSE is the response received.  It may also be t (to indicate
    367 success) nil (to indicate failure), or the symbol `abort'.
    368 
    369 INFO is the async communication channel for the rewrite request."
    370   (when-let* ((ov-and-buf (plist-get info :context))
    371               (ov (car ov-and-buf))
    372               (proc-buf (cdr ov-and-buf))
    373               (buf (overlay-buffer ov)))
    374     (cond
    375      ((stringp response)                ;partial or fully successful result
    376       (with-current-buffer proc-buf     ;auxiliary buffer, insert text here and copy to overlay
    377         (let ((inhibit-modification-hooks nil)
    378               (inhibit-read-only t))
    379           (when (= (buffer-size) 0)
    380             (buffer-disable-undo)
    381             (insert-buffer-substring buf (overlay-start ov) (overlay-end ov))
    382             (when (eq (char-before (point-max)) ?\n)
    383               (plist-put info :newline t))
    384             (delay-mode-hooks (funcall (buffer-local-value 'major-mode buf)))
    385             (add-text-properties (point-min) (point-max) '(face shadow font-lock-face shadow))
    386             (goto-char (point-min)))
    387           (insert response)
    388           (unless (eobp) (ignore-errors (delete-char (length response))))
    389           (font-lock-ensure)
    390           (cl-callf concat (overlay-get ov 'gptel-rewrite) response)
    391           (overlay-put ov 'display (buffer-string))))
    392       (unless (plist-get info :stream) (gptel--rewrite-callback t info)))
    393      ((eq response 'abort)              ;request aborted
    394       (when-let* ((proc-buf (cdr-safe (plist-get info :context))))
    395         (kill-buffer proc-buf))
    396       (delete-overlay ov))
    397      ((null response)                   ;finished with error
    398       (message (concat "LLM response error: %s. Rewrite/refactor in buffer %s canceled.")
    399                (plist-get info :status) (plist-get info :buffer))
    400       (gptel--rewrite-callback 'abort info))
    401      (t (let ((proc-buf (cdr-safe (plist-get info :context))) ;finished successfully
    402               (mkb (propertize "<mouse-1>" 'face 'help-key-binding)))
    403           (with-current-buffer proc-buf
    404             (let ((inhibit-read-only t))
    405               (delete-region (point) (point-max))
    406               (when (and (plist-get info :newline)
    407                          (not (eq (char-before (point-max)) ?\n)))
    408                 (insert "\n"))
    409               (font-lock-ensure))
    410             (overlay-put ov 'display (buffer-string))
    411             (kill-buffer proc-buf))
    412           (when (buffer-live-p buf)
    413             (with-current-buffer buf
    414               (pulse-momentary-highlight-region (overlay-start ov) (overlay-end ov))
    415               (add-hook 'eldoc-documentation-functions #'gptel--rewrite-key-help nil 'local)
    416               ;; (overlay-put ov 'gptel-rewrite response)
    417               (overlay-put ov 'face 'gptel-rewrite-highlight-face)
    418               (overlay-put ov 'keymap gptel-rewrite-actions-map)
    419               (overlay-put ov 'mouse-face 'highlight)
    420               (overlay-put
    421                ov 'help-echo
    422                (format (concat "%s rewrite available: %s or \\[gptel--rewrite-dispatch] for options")
    423                        (concat (gptel-backend-name gptel-backend) ":" (gptel--model-name gptel-model))
    424                        mkb))
    425               (push ov gptel--rewrite-overlays))
    426             (if-let* ((sym gptel-rewrite-default-action))
    427                 (if-let* ((action (intern (concat "gptel--rewrite-" (symbol-name sym))))
    428                           ((functionp action)))
    429                     (funcall action ov) (funcall sym ov))
    430               (message (concat
    431                         "LLM rewrite output"
    432                         (unless (eq (current-buffer) buf)
    433                           (format " in buffer %s " (buffer-name buf)))
    434                         (concat " ready: " mkb ", " (propertize "RET" 'face 'help-key-binding)
    435                                 " or " (substitute-command-keys "\\[gptel-rewrite] to continue.")))))))))))
    436 
    437 ;; * Transient Prefixes for rewriting/refactoring
    438 
    439 (transient-define-prefix gptel--rewrite-directive-menu ()
    440   "Set the directive (system message) for rewrite actions.
    441 
    442 By default, gptel uses the directive associated with the `rewrite'
    443  key in `gptel-directives'.  You can add more rewrite-specific
    444  directives to `gptel-directives' and pick one from here."
    445   [:description gptel-system-prompt--format
    446    [(gptel--suffix-rewrite-directive)]
    447    [(gptel--infix-variable-scope)]]
    448    [:class transient-column
    449     :setup-children
    450     (lambda (_) (transient-parse-suffixes
    451             'gptel--rewrite-directive-menu
    452             (gptel--setup-directive-menu
    453              'gptel--rewrite-directive "Rewrite directive")))
    454     :pad-keys t])
    455 
    456 (define-obsolete-function-alias 'gptel-rewrite-menu 'gptel-rewrite "0.9.6")
    457 
    458 ;;;###autoload (autoload 'gptel-rewrite "gptel-rewrite" nil t)
    459 (transient-define-prefix gptel-rewrite ()
    460   "Rewrite or refactor text region using an LLM."
    461   [:description
    462    (lambda ()
    463      (gptel--describe-directive
    464       gptel--rewrite-directive (max (- (window-width) 14) 20) " "))
    465    [""
    466     ("s" "Set full directive" gptel--rewrite-directive-menu)
    467     (gptel--infix-rewrite-extra)]]
    468   ;; FIXME: We are requiring `gptel-transient' because of this suffix, perhaps
    469   ;; we can get find some way around that?
    470   [:description (lambda () (concat "Context for " (gptel--refactor-or-rewrite)))
    471    :if use-region-p
    472    (gptel--infix-context-remove-all :key "-d")
    473    (gptel--suffix-context-buffer :key "C" :format "  %k %d")]
    474   [[:description "Diff Options"
    475     :if (lambda () gptel--rewrite-overlays)
    476     ("-b" "Ignore whitespace changes"      ("-b" "--ignore-space-change"))
    477     ("-w" "Ignore all whitespace"          ("-w" "--ignore-all-space"))
    478     ("-i" "Ignore case"                    ("-i" "--ignore-case"))
    479     (gptel--infix-rewrite-diff:-U)]
    480    [:description "Accept all"
    481     :if (lambda () gptel--rewrite-overlays)
    482     (gptel--suffix-rewrite-merge)
    483     (gptel--suffix-rewrite-accept)
    484     "Reject all"
    485     (gptel--suffix-rewrite-reject)]]
    486   [[:description (lambda () (concat "Diff " (gptel--refactor-or-rewrite) "s"))
    487     :if (lambda () gptel--rewrite-overlays)
    488     (gptel--suffix-rewrite-diff)
    489     (gptel--suffix-rewrite-ediff)]]
    490   [[:description gptel--refactor-or-rewrite
    491     :if use-region-p
    492     (gptel--suffix-rewrite)]
    493    ["Dry Run"
    494     :if (lambda () (and (use-region-p)
    495                    (or gptel-log-level gptel-expert-commands)))
    496     ("I" "Inspect query (Lisp)"
    497      (lambda ()
    498        "Inspect the query that will be sent as a lisp object."
    499        (interactive)
    500        (gptel--sanitize-model)
    501        (gptel--inspect-query
    502         (gptel--suffix-rewrite gptel--rewrite-message t))))
    503     ("J" "Inspect query (JSON)"
    504      (lambda ()
    505        "Inspect the query that will be sent as a JSON object."
    506        (interactive)
    507        (gptel--sanitize-model)
    508        (gptel--inspect-query
    509         (gptel--suffix-rewrite gptel--rewrite-message t)
    510         'json)))]]
    511   (interactive)
    512   (gptel--rewrite-sanitize-overlays)
    513   (unless (or gptel--rewrite-overlays (use-region-p))
    514     (user-error "`gptel-rewrite' requires an active region or rewrite in progress."))
    515   (unless gptel--rewrite-message
    516     (setq gptel--rewrite-message
    517           (concat (gptel--refactor-or-rewrite) ": ")))
    518   (transient-setup 'gptel-rewrite))
    519 
    520 ;; * Transient infixes for rewriting/refactoring
    521 
    522 (transient-define-infix gptel--infix-rewrite-extra ()
    523   "Chat directive (system message) to use for rewriting or refactoring."
    524   :description (lambda () (if (derived-mode-p 'prog-mode)
    525                          "Refactor instruction"
    526                        "Rewrite instruction"))
    527   :class 'gptel-lisp-variable
    528   :variable 'gptel--rewrite-message
    529   :set-value #'gptel--set-with-scope
    530   :display-nil "(None)"
    531   :key "d"
    532   :format " %k %d %v"
    533   :prompt (concat "Instructions " gptel--read-with-prefix-help)
    534   :reader (lambda (prompt _ history)
    535             (let* ((rewrite-directive
    536                     (car-safe (gptel--parse-directive gptel--rewrite-directive
    537                                                       'raw)))
    538                    (cycle-prefix
    539                     (lambda () (interactive)
    540                       (gptel--read-with-prefix rewrite-directive)))
    541                    (minibuffer-local-map
    542                     (make-composed-keymap
    543                      (define-keymap "TAB" cycle-prefix "<tab>" cycle-prefix)
    544                      minibuffer-local-map)))
    545               (minibuffer-with-setup-hook cycle-prefix
    546                 (read-string
    547                  prompt
    548                  (or gptel--rewrite-message
    549                      (concat (gptel--refactor-or-rewrite) ": "))
    550                  history)))))
    551 
    552 (transient-define-argument gptel--infix-rewrite-diff:-U ()
    553   :description "Context lines"
    554   :class 'transient-option
    555   :argument "-U"
    556   :reader #'transient-read-number-N0)
    557 
    558 ;; * Transient suffixes for rewriting/refactoring
    559 
    560 (transient-define-suffix gptel--suffix-rewrite-directive (&optional cancel)
    561   "Edit Rewrite directive.
    562 
    563 CANCEL is used to avoid touching dynamic rewrite directives,
    564 generated from functions."
    565   :transient 'transient--do-exit
    566   :description "Edit full rewrite directive"
    567   :key "s"
    568   (interactive
    569    (list (and
    570           (functionp gptel--rewrite-directive)
    571           (not (y-or-n-p
    572                 "Rewrite directive is dynamically generated: Edit its current value instead?")))))
    573   (if cancel (progn (message "Edit canceled")
    574                     (call-interactively #'gptel-rewrite))
    575     (gptel--edit-directive 'gptel--rewrite-directive #'gptel-rewrite)))
    576 
    577 (transient-define-suffix gptel--suffix-rewrite (&optional rewrite-message dry-run)
    578   "Rewrite or refactor region contents."
    579   :key "r"
    580   :description #'gptel--refactor-or-rewrite
    581   (interactive (list gptel--rewrite-message))
    582   (let* ((nosystem (gptel--model-capable-p 'nosystem))
    583          ;; Try to send context with system message
    584          (gptel-use-context
    585           (and gptel-use-context (if nosystem 'user 'system)))
    586          (prompt (list (buffer-substring-no-properties (region-beginning) (region-end))
    587                        "What is the required change?"
    588                        (or rewrite-message gptel--rewrite-message))))
    589     (deactivate-mark)
    590     (when nosystem
    591       (setcar prompt (concat (car-safe (gptel--parse-directive
    592                                         gptel--rewrite-directive 'raw))
    593                              "\n\n" (car prompt))))
    594     (gptel-request prompt
    595       :dry-run dry-run
    596       :system gptel--rewrite-directive
    597       :stream gptel-stream
    598       :context
    599       (let ((ov (make-overlay (region-beginning) (region-end) nil t)))
    600         (overlay-put ov 'category 'gptel)
    601         (overlay-put ov 'evaporate t)
    602         (cons ov (generate-new-buffer "*gptel-rewrite*")))
    603       :callback #'gptel--rewrite-callback)))
    604 
    605 (transient-define-suffix gptel--suffix-rewrite-diff (&optional switches)
    606   "Diff LLM output against buffer."
    607   :if (lambda () gptel--rewrite-overlays)
    608   :key "D"
    609   :description (concat "Diff  LLM " (downcase (gptel--refactor-or-rewrite)) "s")
    610   (interactive (list (transient-args transient-current-command)))
    611   (gptel--rewrite-diff gptel--rewrite-overlays switches))
    612 
    613 (transient-define-suffix gptel--suffix-rewrite-ediff ()
    614   "Ediff LLM output against buffer."
    615   :if (lambda () gptel--rewrite-overlays)
    616   :key "E"
    617   :description (concat "Ediff LLM " (downcase (gptel--refactor-or-rewrite)) "s")
    618   (interactive)
    619   (gptel--rewrite-ediff gptel--rewrite-overlays))
    620 
    621 (transient-define-suffix gptel--suffix-rewrite-merge ()
    622   "Insert LLM output as merge conflicts"
    623   :if (lambda () gptel--rewrite-overlays)
    624   :key "M"
    625   :description "Merge with conflicts"
    626   (interactive)
    627   (gptel--rewrite-merge gptel--rewrite-overlays))
    628 
    629 (transient-define-suffix gptel--suffix-rewrite-accept ()
    630   "Accept pending LLM rewrites."
    631   :if (lambda () gptel--rewrite-overlays)
    632   :key "A"
    633   :description "Accept and replace"
    634   (interactive)
    635   (gptel--rewrite-accept gptel--rewrite-overlays))
    636 
    637 (transient-define-suffix gptel--suffix-rewrite-reject ()
    638   "Clear pending LLM rewrites."
    639   :if (lambda () gptel--rewrite-overlays)
    640   :key "K"
    641   :description (concat "Clear pending "
    642                        (downcase (gptel--refactor-or-rewrite))
    643                        "s")
    644   (interactive)
    645   (gptel--rewrite-reject gptel--rewrite-overlays))
    646 
    647 (provide 'gptel-rewrite)
    648 ;;; gptel-rewrite.el ends here
    649 
    650 ;; Local Variables:
    651 ;; outline-regexp: "^;; \\*+"
    652 ;; End: