diff options
| author | Craig Jennings <c@cjennings.net> | 2026-05-16 01:55:16 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-05-16 01:55:16 -0500 |
| commit | 6ee37e0a68d31909861cf59684d3601bf40f5abe (patch) | |
| tree | d14726945d65853f183896a06532c1e88bbb81de /modules | |
| parent | 670117cccdbae4706dfaa5e05144c256c3a657f0 (diff) | |
| download | dotemacs-6ee37e0a68d31909861cf59684d3601bf40f5abe.tar.gz dotemacs-6ee37e0a68d31909861cf59684d3601bf40f5abe.zip | |
feat(ai-rewrite): add directive-picker wrappers around gptel-rewrite
`gptel-rewrite` is the killer feature for the keep-gptel decision,
and it now lives behind two commands instead of the bare call:
- `cj/gptel-rewrite-with-directive` (`C-; a r`, replacing the
former bare `gptel-rewrite` binding): completing-read on a
directive name from `cj/gptel-rewrite-directives`, then rewrite
the active region.
- `cj/gptel-rewrite-redo-with-different-directive` (`C-; a R`):
replay the prior region with a different directive. The region
is preserved via markers stored buffer-local on the first call so
it survives accept/reject of the prior rewrite.
I picked the hook injection approach over an `:after`-advice +
state-capture pattern. `gptel-rewrite-directives-hook` is an
abnormal hook gptel-rewrite already consults for a per-call
system message. Wrapping the call in a one-shot `let`-binding on
that hook gives the directive exactly the lifetime of the rewrite
and leaves nothing to clean up. Mutating `gptel-directives`
globally would mean either restoring it afterward or living with
the change -- both worse than the hook.
Directives ship inline as a `defcustom` alist with the six names
called out in the task -- `terse`, `fix-grammar`,
`refactor-readability`, `add-docstring`, `explain-as-comment`,
`shorten`. Customization is a `customize-variable` or `setq`
away.
9 tests cover the defcustom shape (default names present, bodies
non-empty strings), the wrapper (normal path, no-region error,
unknown-directive error, last-state recording), and the redo
(replays the prior region, errors when no previous, excludes the
current directive from the re-pick prompt). `gptel-rewrite`
stubbed in tests so no rewrite UI fires.
Diffstat (limited to 'modules')
| -rw-r--r-- | modules/ai-config.el | 8 | ||||
| -rw-r--r-- | modules/ai-rewrite.el | 108 |
2 files changed, 114 insertions, 2 deletions
diff --git a/modules/ai-config.el b/modules/ai-config.el index 6eff1ba6..c7a14cae 100644 --- a/modules/ai-config.el +++ b/modules/ai-config.el @@ -35,6 +35,8 @@ (autoload 'cj/gptel-delete-conversation "ai-conversations" "Delete a saved AI conversation." t) (autoload 'cj/gptel-autosave-toggle "ai-conversations" "Toggle autosave in the current GPTel buffer." t) (autoload 'cj/gptel-quick-ask "ai-quick-ask" "One-shot quick-ask in a transient buffer." t) +(autoload 'cj/gptel-rewrite-with-directive "ai-rewrite" "Pick a directive and run gptel-rewrite on the region." t) +(autoload 'cj/gptel-rewrite-redo-with-different-directive "ai-rewrite" "Re-run the previous rewrite with a different directive." t) ;;; ------------------------- AI Config Helper Functions ------------------------ @@ -510,7 +512,8 @@ Works for any buffer, whether it's visiting a file or not." "m" #'cj/gptel-change-model ;; change the LLM model "p" #'gptel-system-prompt ;; change prompt "q" #'cj/gptel-quick-ask ;; one-shot quick ask - "r" #'gptel-rewrite ;; rewrite a region of code/text + "r" #'cj/gptel-rewrite-with-directive ;; rewrite region with a chosen directive + "R" #'cj/gptel-rewrite-redo-with-different-directive ;; redo last rewrite, new directive "c" #'cj/gptel-context-clear ;; clear all context "s" #'cj/gptel-save-conversation ;; save conversation "t" #'cj/toggle-gptel ;; toggles the ai-assistant window @@ -530,7 +533,8 @@ Works for any buffer, whether it's visiting a file or not." "C-; a m" "change model" "C-; a p" "change prompt" "C-; a q" "quick ask" - "C-; a r" "rewrite region" + "C-; a r" "rewrite region (directive)" + "C-; a R" "redo rewrite, new directive" "C-; a c" "clear context" "C-; a s" "save conversation" "C-; a t" "toggle window" diff --git a/modules/ai-rewrite.el b/modules/ai-rewrite.el new file mode 100644 index 00000000..fb25c137 --- /dev/null +++ b/modules/ai-rewrite.el @@ -0,0 +1,108 @@ +;;; ai-rewrite.el --- Directive-picker wrappers for gptel-rewrite -*- lexical-binding: t; coding: utf-8; -*- + +;; Author: Craig Jennings <c@cjennings.net> + +;;; Commentary: +;; Adds two ergonomic wrappers around `gptel-rewrite': +;; +;; cj/gptel-rewrite-with-directive Pick a named directive, +;; then rewrite the region. +;; cj/gptel-rewrite-redo-with-different-directive +;; Re-run the previous region +;; with a different directive. +;; +;; A directive is a short system-message snippet attached to a name +;; (e.g. "terse", "fix-grammar"). The directive body is injected +;; into the rewrite via `gptel-rewrite-directives-hook' just for that +;; call -- no global state changes. + +;;; Code: + +;; Declare the hook variable special so our `let'-binding below is +;; dynamic (visible across the `call-interactively' that follows) +;; rather than lexical when this file is byte-compiled. +(defvar gptel-rewrite-directives-hook) + +(defcustom cj/gptel-rewrite-directives + '(("terse" + . "Rewrite the text to be as terse as possible without losing meaning.\nDo not add commentary. Return only the rewritten text.") + ("fix-grammar" + . "Fix grammar and spelling errors only. Do not rephrase, restructure,\nor change tone. Return only the corrected text.") + ("refactor-readability" + . "Refactor the code for readability. Improve naming, split long\nfunctions when appropriate, remove unnecessary complexity, and preserve\nbehavior exactly. Return only the refactored code.") + ("add-docstring" + . "Add or improve docstrings for every function in the region. Use the\nidiomatic docstring style for the language. Do not change executable\ncode. Return the whole region with the updated docstrings.") + ("explain-as-comment" + . "Replace the region with the original code, preceded by a concise\nblock comment explaining what the code does. Use the language's\nidiomatic comment syntax. Return code + comment, nothing else.") + ("shorten" + . "Shorten the text while preserving meaning, technical accuracy, and\nthe author's voice. Remove rhetorical padding. Return only the\nshortened text.")) + "Named system-message directives for `cj/gptel-rewrite-with-directive'. +Each entry is a (NAME . BODY) pair where NAME is the directive label +presented in the completing-read prompt and BODY is the system +message injected into the next `gptel-rewrite' call." + :type '(alist :key-type string :value-type string) + :group 'cj) + +(defvar-local cj/gptel-rewrite--last-region nil + "Cons (BEG-MARKER . END-MARKER) of the last directive-driven rewrite.") + +(defvar-local cj/gptel-rewrite--last-directive nil + "Name of the directive used in the last directive-driven rewrite.") + +(defun cj/gptel-rewrite--call-with-directive (directive-name beg end) + "Run `gptel-rewrite' over BEG..END with DIRECTIVE-NAME's system message. +Stores the region (as markers) and directive name on buffer-local +variables so `cj/gptel-rewrite-redo-with-different-directive' can +revisit them." + (let ((body (alist-get directive-name cj/gptel-rewrite-directives + nil nil #'equal))) + (unless body + (user-error "Unknown rewrite directive: %s" directive-name)) + (setq-local cj/gptel-rewrite--last-region + (cons (copy-marker beg) (copy-marker end))) + (setq-local cj/gptel-rewrite--last-directive directive-name) + (let ((gptel-rewrite-directives-hook + (cons (lambda () body) gptel-rewrite-directives-hook))) + (save-excursion + (goto-char beg) + (push-mark end t t) + (call-interactively #'gptel-rewrite))))) + +;;;###autoload +(defun cj/gptel-rewrite-with-directive (directive-name) + "Pick DIRECTIVE-NAME from `cj/gptel-rewrite-directives' and rewrite the region. +Requires an active region. The directive is applied only to this +call -- it does not modify global `gptel-directives'." + (interactive + (progn + (unless (use-region-p) + (user-error "No region selected")) + (list (completing-read + "Rewrite directive: " + (mapcar #'car cj/gptel-rewrite-directives) nil t)))) + (cj/gptel-rewrite--call-with-directive + directive-name (region-beginning) (region-end))) + +;;;###autoload +(defun cj/gptel-rewrite-redo-with-different-directive () + "Re-run the previous directive-driven rewrite with a different directive. +The region is restored from the markers captured at the last call; +the user picks a new directive from the remaining choices." + (interactive) + (unless cj/gptel-rewrite--last-region + (user-error "No previous rewrite to redo in this buffer")) + (let* ((beg-mk (car cj/gptel-rewrite--last-region)) + (end-mk (cdr cj/gptel-rewrite--last-region)) + (current cj/gptel-rewrite--last-directive) + (others (cl-remove + current + (mapcar #'car cj/gptel-rewrite-directives) + :test #'equal)) + (chosen (completing-read + (format "Re-rewrite with (was %s): " current) + others nil t))) + (cj/gptel-rewrite--call-with-directive + chosen (marker-position beg-mk) (marker-position end-mk)))) + +(provide 'ai-rewrite) +;;; ai-rewrite.el ends here |
