karthink / gptel

A simple LLM client for Emacs
GNU General Public License v3.0
1.54k stars 149 forks source link

Rewrite and wdiff? #362

Closed bradmont closed 1 month ago

bradmont commented 2 months ago

I would find an in-place rewrite function that, instead of simply deleting the source text, or running ediff immediately, could run it through something like gnu wdiff ( https://www.gnu.org/software/wdiff/ ) and replace the source text with the word-wise diff output. Essentially this would allow for a workflow similar to Word or Libreoffice's track changes; that is, I could ask chatgpt to proofread a text for me, but not be obligated to deal with the output right away, but let it sit and circle back as needed. Even better would be the ability to run it non-interactively, without having to go through the menu -- just call a function on a selected region or in a given paragraph, and let chatgpt do its thing.

Do you think this is a sensible addition to gptel? Thanks for the great project!

bradmont commented 2 months ago

I managed to throw together a working example of the idea; my elisp is weak so I relied heavily on chatGPT to create a modified version of gptel--suffix-rewrite-and-ediff in gptel-transient.el. It seems to do what I want.

------------------------------ gptel-transient.el ------------------------------
index c0add9a..da1d258 100644
@@ -469,7 +469,8 @@ Customize `gptel-directives' for task-specific prompts."
               "Refactor" "Rewrite"))
     (gptel--suffix-rewrite)
     (gptel--suffix-rewrite-and-replace)
-    (gptel--suffix-rewrite-and-ediff)]]
+    (gptel--suffix-rewrite-and-ediff)
+    (gptel--suffix-rewrite-and-wdiff)]]
   (interactive)
   (unless gptel--rewrite-message
     (setq gptel--rewrite-message (gptel--rewrite-message)))
@@ -1079,6 +1080,77 @@ When LOCAL is non-nil, set the system message only in the current buffer."
                   (list 'ediff-regions-wordwise 'word-wise nil)
                 (list 'ediff-regions-linewise nil nil))))))))))

+(defun gptel--generate-wdiff (old-text new-text)
+  "Generate a wdiff comparison between OLD-TEXT and NEW-TEXT and return the result."
+  (let* ((old-file (make-temp-file "old-text"))
+         (new-file (make-temp-file "new-text"))
+         (wdiff-buffer (get-buffer-create "*gptel-wdiff*"))
+         (wdiff-result "")
+         (inhibit-read-only t))
+    (unwind-protect
+        (progn
+          ;; Write texts to temporary files
+          (with-temp-file old-file (insert old-text))
+          (with-temp-file new-file (insert new-text))
+
+          ;; Run wdiff and capture the result
+          (with-current-buffer wdiff-buffer
+            (erase-buffer)
+            (call-process "wdiff" nil t nil old-file new-file)
+
+            (setq wdiff-result (buffer-string)))
+          wdiff-result)
+      ;; Clean up temporary files
+      (delete-file old-file)
+      (delete-file new-file))))
+
+(transient-define-suffix gptel--suffix-rewrite-and-wdiff (args)
+  "Refactor or rewrite region contents and run wdiff."
+  :key "w"
+  :description (lambda () (concat (gptel--refactor-or-rewrite) " and wdiff"))
+  (interactive (list (transient-args transient-current-command)))
+  (letrec ((prompt (buffer-substring-no-properties
+                    (region-beginning) (region-end)))
+           (gptel--system-message gptel--rewrite-message)
+           (cwc (current-window-configuration))
+           (gptel--wdiff-restore
+            (lambda ()
+              (when (window-configuration-p cwc)
+                (set-window-configuration cwc))
+              (remove-hook 'wdiff-quit-hook gptel--wdiff-restore))))
+    (message "Waiting for response... ")
+    (gptel-request
+     prompt
+     :context (cons (region-beginning) (region-end))
+     :callback
+     (lambda (response info)
+       (if (not response)
+           (message "ChatGPT response error: %s" (plist-get info :status))
+         (let* ((gptel-buffer (plist-get info :buffer))
+                (gptel-bounds (plist-get info :context))
+                (buffer-mode
+                 (buffer-local-value 'major-mode gptel-buffer)))
+           (pcase-let ((`(,new-buf ,new-beg ,new-end)
+                        (with-current-buffer (get-buffer-create "*gptel-rewrite-Region.B-*")
+                          (let ((inhibit-read-only t))
+                            (erase-buffer)
+                            (funcall buffer-mode)
+                            (insert response)
+                            (goto-char (point-min))
+                            (list (current-buffer) (point-min) (point-max))))))
+             ;; Generate wdiff and replace the original text with it
+             (let* ((old-text (buffer-substring-no-properties (car gptel-bounds) (cdr gptel-bounds)))
+                    (new-text (with-current-buffer new-buf (buffer-substring-no-properties new-beg new-end)))
+                    (wdiff-result (gptel--generate-wdiff old-text new-text)))
+               (save-excursion
+                 ;; Replace the original text with the wdiff result
+                 (goto-char (car gptel-bounds))
+                 (delete-region (car gptel-bounds) (cdr gptel-bounds))
+                 (insert wdiff-result))))))))))
+
+
+
+
 (provide 'gptel-transient)
 ;;; gptel-transient.el ends here
karthink commented 2 months ago

but let it sit and circle back as needed. Even better would be the ability to run it non-interactively, without having to go through the menu -- just call a function on a selected region or in a given paragraph, and let chatgpt do its thing.

These are both good ideas. The rewrite interface is a bit of an afterthought in gptel right now. However it's getting a lot of traction recently (#370, #357 and more) so it makes sense to redesign it to work better. I'll work on it when I can, this issue can stay open until then.

karthink commented 2 months ago

Okay, I sat down and gave the refactor interface some thought. It has been completely redesigned. Please update and try it out now. It should be obvious how it works the first time you try refactoring it from the transient menu as usual.

I will add a direct interactive function after your feedback.

mateialexandru commented 2 months ago

@karthink Many many thanks!

karthink commented 2 months ago

@mateialexandru If you have feedback for me about the (i) workflow, (ii) action discoverability, (iii) appearance of the redesigned feature it will help. (Please start a discussion instead of commenting here)

karthink commented 1 month ago

I could ask chatgpt to proofread a text for me, but not be obligated to deal with the output right away, but let it sit and circle back as needed.

This has been added.

karthink commented 1 month ago

Closing since gptel's rewrite interface has been completely redesigned. If you want to suggest changes to the new interface, please create a new issue.