diff options
| author | cashmere <cashmere@autistici.org> | 2026-07-11 08:50:14 +0200 |
|---|---|---|
| committer | cashmere <cashmere@autistici.org> | 2026-07-11 08:50:14 +0200 |
| commit | c74c3526fe719ff8127d17f44b67a33772adc334 (patch) | |
| tree | 7b05d6cad835f8704430ac38c4f22358601fe846 /lisp/agent-shell-diff.el | |
| parent | c6579ddf761aaa1c922213310dc1f46347ea06ca (diff) | |
| download | emacs-c74c3526fe719ff8127d17f44b67a33772adc334.tar.gz | |
2026-07-11T08:50:16+02:00
Diffstat (limited to 'lisp/agent-shell-diff.el')
| -rw-r--r-- | lisp/agent-shell-diff.el | 268 |
1 files changed, 268 insertions, 0 deletions
diff --git a/lisp/agent-shell-diff.el b/lisp/agent-shell-diff.el new file mode 100644 index 0000000..b78bdf1 --- /dev/null +++ b/lisp/agent-shell-diff.el @@ -0,0 +1,268 @@ +;;; agent-shell-diff.el --- A quick way to query/display a diff. -*- lexical-binding: t; -*- + +;; Copyright (C) 2024 Alvaro Ramirez + +;; Author: Alvaro Ramirez https://xenodium.com +;; URL: https://github.com/xenodium/agent-shell + +;; This package is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation; either version 3, or (at your option) +;; any later version. + +;; This package is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. + +;;; Commentary: +;; +;; Report issues at https://github.com/xenodium/agent-shell/issues +;; +;; ✨ Please support this work https://github.com/sponsors/xenodium ✨ + +;;; Code: + +(eval-when-compile + (require 'cl-lib)) +(require 'ansi-color) +(require 'diff) +(require 'diff-mode) + +(defcustom agent-shell-diff-delta-command + '("delta" "--no-gitconfig" "--color-only") + "Command and arguments used to colorize diffs with delta. + +Set to nil to disable delta and fall back to the standard `diff-no-select' +output. delta must be installed and on `exec-path'." + :type '(choice (const :tag "Disable delta" nil) + (repeat string)) + :group 'agent-shell) + +(defvar-local agent-shell-diff--on-exit nil + "Function to call when the diff buffer is killed. + +This variable is automatically set by :on-exit from `agent-shell-diff' +and can be temporarily let-bound to nil to prevent the +on-exit callback from running when the buffer is killed.") + +(defvar-local agent-shell-diff--file nil + "Buffer-local file path associated with the diff.") + +(defvar-local agent-shell-diff--accept-all-command nil + "Buffer-local command to accept all changes in the diff.") + +(defvar-local agent-shell-diff--reject-all-command nil + "Buffer-local command to reject all changes in the diff.") + +(defvar-local agent-shell-diff--used-delta nil + "Non-nil when the current buffer's diff was generated using delta.") + +(defvar agent-shell-diff-mode-map + (let ((map (make-sparse-keymap))) + (define-key map (kbd "n") #'diff-hunk-next) + (define-key map (kbd "p") #'diff-hunk-prev) + (define-key map (kbd "y") #'agent-shell-diff-accept-all) + (define-key map (kbd "C-c C-c") #'agent-shell-diff-reject-all) + (define-key map (kbd "f") #'agent-shell-diff-open-file) + (define-key map (kbd "q") #'kill-current-buffer) + map) + "Keymap for `agent-shell-diff-mode'.") + +(define-derived-mode agent-shell-diff-mode diff-mode "Agent-Shell-Diff" + "Major mode for `agent-shell' diff buffers. +Derives from `diff-mode'. Provides `agent-shell-diff-accept-all' +and `agent-shell-diff-reject-all' commands that can be rebound +via `agent-shell-diff-mode-map'." + :group 'agent-shell + ;; Don't inherit diff-mode-map (some bindings can be destructive). + (set-keymap-parent agent-shell-diff-mode-map nil) + (setq buffer-read-only t)) + +(defun agent-shell-diff-kill-buffer (buffer) + "Kill diff BUFFER, suppressing any `agent-shell-diff--on-exit' callback. +If BUFFER is not live, do nothing." + (when (buffer-live-p buffer) + (with-current-buffer buffer + (setq agent-shell-diff--on-exit nil)) + (kill-buffer buffer))) + +(defun agent-shell-diff-accept-all () + "Accept all changes in the current diff buffer." + (interactive) + (if agent-shell-diff--accept-all-command + (let ((buf (current-buffer))) + (funcall agent-shell-diff--accept-all-command) + (when (buffer-live-p buf) + (let ((agent-shell-diff--on-exit nil)) + (kill-buffer buf)))) + (user-error "No accept command available in this buffer"))) + +(defun agent-shell-diff-reject-all () + "Reject all changes in the current diff buffer." + (interactive) + (if agent-shell-diff--reject-all-command + (let ((buf (current-buffer))) + (when (funcall agent-shell-diff--reject-all-command) + (when (buffer-live-p buf) + (let ((agent-shell-diff--on-exit nil)) + (kill-buffer buf))))) + (user-error "No reject command available in this buffer"))) + +(cl-defun agent-shell-diff (&key old new on-exit on-accept on-reject title file) + "Display a diff between OLD and NEW strings in a buffer. + +Creates a new buffer showing the differences between OLD and NEW +using `agent-shell-diff-mode'. The buffer is read-only. + +When the buffer is killed, calls ON-EXIT with no arguments. + +Returns the newly created diff buffer. + +Arguments: + :OLD - Original string content + :NEW - Modified string content + :ON-EXIT - Function called with no arguments when buffer is killed + :ON-ACCEPT - Command to accept all changes + :ON-REJECT - Command to reject all changes + :TITLE - Optional title to display in header line + :FILE - File path" + (let* ((diff-buffer (generate-new-buffer "*agent-shell-diff*")) + (calling-window (selected-window)) + (calling-buffer (current-buffer)) + (interrupt-key (where-is-internal 'agent-shell-interrupt + (current-local-map) t))) + (unwind-protect + (progn + (with-current-buffer diff-buffer + (let ((inhibit-read-only t) + (diff-mode-read-only nil)) + (erase-buffer) + ;; Set mode before inserting diff so diff-no-select + ;; doesn't reset font-lock (see #316). + (agent-shell-diff-mode) + (agent-shell-diff--insert-diff old new file diff-buffer) + ;; Add overlays to hide scary text. + (save-excursion + (goto-char (point-min)) + ;; `diff-no-select' adds a command line at the top and + ;; "Diff finished." at the bottom. delta output does not, + ;; so only strip those when delta was not used. + (unless agent-shell-diff--used-delta + (delete-region (point) (progn (forward-line 1) (point))) + (delete-region (progn (goto-char (point-max)) (forward-line -1) (forward-line 0) (point)) + (point-max)) + (goto-char (point-min))) + (goto-char (point-min)) + ;; Hide --- and +++ lines + (while (re-search-forward "^\\(---\\|\\+\\+\\+\\).*\n" nil t) + (let ((overlay (make-overlay (match-beginning 0) (match-end 0)))) + (overlay-put overlay 'category 'diff-header) + (overlay-put overlay 'display "") + (overlay-put overlay 'evaporate t))) + ;; Replace @@ lines with "Changes" + (goto-char (point-min)) + (while (re-search-forward "^@@.*@@.*\n" nil t) + (let ((overlay (make-overlay (match-beginning 0) (match-end 0))) + (face 'diff-hunk-header)) ; or any face you prefer + (overlay-put overlay 'category 'diff-header) + ;; Intended display is: + ;; ╭─────────╮ + ;; │ changes │ + ;; ╰─────────╯ + ;; Using before-string so diff-hunk-next + ;; lands on "│" instead of "╭". + (overlay-put overlay 'before-string + (propertize "\n╭─────────╮\n" 'face face)) + (overlay-put overlay 'display + (propertize "│ changes │\n╰─────────╯\n\n" 'face face)) + (overlay-put overlay 'evaporate t))))) + (goto-char (point-min)) + (ignore-errors (diff-hunk-next)) + (setq agent-shell-diff--file file + agent-shell-diff--accept-all-command on-accept + agent-shell-diff--reject-all-command on-reject) + (when on-exit + (setq agent-shell-diff--on-exit on-exit) + (add-hook 'kill-buffer-hook + (lambda () + (when (and agent-shell-diff--on-exit + (buffer-live-p calling-buffer)) + (with-current-buffer calling-buffer + (funcall on-exit))) + ;; Give focus back to calling buffer. + (when (buffer-live-p calling-buffer) + (ignore-errors + (when (window-live-p calling-window) + (unless (eq (window-buffer calling-window) calling-buffer) + (set-window-buffer calling-window calling-buffer)) + (select-window calling-window))))) + nil t)) + (let ((map (copy-keymap agent-shell-diff-mode-map))) + (when (and interrupt-key + (not (lookup-key map interrupt-key))) + (define-key map interrupt-key #'agent-shell-diff-reject-all)) + (use-local-map map)) + (setq header-line-format + (substitute-command-keys + (concat + " " + (when title + (concat (propertize title 'face 'mode-line-emphasis) " ")) + "\\[diff-hunk-next] next hunk " + "\\[diff-hunk-prev] previous hunk " + "\\[agent-shell-diff-accept-all] accept " + "\\[agent-shell-diff-reject-all] reject " + "\\[agent-shell-diff-open-file] open " + "\\[kill-current-buffer] quit")))) + diff-buffer) + (pop-to-buffer diff-buffer '((display-buffer-use-some-window + display-buffer-same-window)))))) + +(defun agent-shell-diff-open-file () + "Open the file associated with the current diff buffer." + (interactive) + (if agent-shell-diff--file + (find-file agent-shell-diff--file) + (user-error "No file associated with this diff buffer"))) + +(defun agent-shell-diff--insert-diff (old new file buf) + "Insert diff from FILE between OLD and NEW strings in buffer BUF. + +When `agent-shell-diff-delta-command' is non-nil and delta is available +on `exec-path', pipe the unified diff through delta for syntax +highlighting; otherwise fall back to `diff-no-select'." + (let* ((suffix (format ".%s" (file-name-extension file))) + (old-file (make-temp-file "old" nil suffix)) + (new-file (make-temp-file "new" nil suffix)) + (use-delta (and agent-shell-diff-delta-command + (executable-find (car agent-shell-diff-delta-command))))) + (with-temp-file old-file (insert old)) + (with-temp-file new-file (insert new)) + (with-current-buffer buf + (setq agent-shell-diff--used-delta use-delta) + (if use-delta + (let ((coding-system-for-read 'utf-8) + (coding-system-for-write 'utf-8) + (command (concat "diff -U3 " + (shell-quote-argument old-file) + " " + (shell-quote-argument new-file) + " | " + (mapconcat #'shell-quote-argument + agent-shell-diff-delta-command + " ")))) + (call-process-shell-command command nil t nil) + ;; `ansi-color-apply-on-region' can drop properties in some + ;; Emacs builds, so apply to the string and re-insert. + (let ((colored (ansi-color-apply (buffer-string)))) + (erase-buffer) + (insert colored))) + (diff-no-select old-file new-file "-U3" t buf))))) + +(provide 'agent-shell-diff) + +;;; agent-shell-diff.el ends here |
