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: