branch: elpa/aidermacs commit 07f3d319b13333745e56f613fa85593d3eb850c0 Author: Mingde (Matthew) Zeng <matthew...@posteo.net> Commit: Mingde (Matthew) Zeng <matthew...@posteo.net>
Preliminary implementation of ediff capture diffs This will fix #43 --- aidermacs-backends.el | 1 - aidermacs.el | 225 +++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 204 insertions(+), 22 deletions(-) diff --git a/aidermacs-backends.el b/aidermacs-backends.el index 86a99dffc1..4d564e82e5 100644 --- a/aidermacs-backends.el +++ b/aidermacs-backends.el @@ -188,7 +188,6 @@ BUFFER is the target buffer. COMMAND is the text to send. If REDIRECT is non-nil it redirects the output (hidden) for comint backend. If CALLBACK is non-nil it will be called after the command finishes." (setq aidermacs--last-command command - aidermacs--current-output nil aidermacs--current-callback callback) (if (eq aidermacs-backend 'vterm) (aidermacs--send-command-vterm buffer command) diff --git a/aidermacs.el b/aidermacs.el index 51d739924c..c3d20e335e 100644 --- a/aidermacs.el +++ b/aidermacs.el @@ -28,6 +28,7 @@ (require 'comint) (require 'dired) +(require 'ediff) (require 'project) (require 'transient) (require 'vc-git) @@ -101,6 +102,13 @@ This is the file name without path." (defvar-local aidermacs-read-string-history nil "History list for aidermacs read string inputs.") +(defvar-local aidermacs--pre-edit-files nil + "Alist of (filename . temp-filename) pairs storing file state before Aider edits.") + +(defvar-local aidermacs--pre-edit-buffers nil + "Alist of (filename . temp-buffer) pairs for active ediff sessions.") + + ;;;###autoload (defun aidermacs-plain-read-string (prompt &optional initial-input) "Read a string from the user with PROMPT and optional INITIAL-INPUT. @@ -292,27 +300,195 @@ This is useful for working in monorepos where you want to limit aider's scope." (default-directory (file-truename default-directory))) (aidermacs-run))) -(defun aidermacs--get-files-in-session (callback) - "Get list of files in current session and call CALLBACK with the result." - (aidermacs--send-command-redirect - "/ls" +(defun aidermacs--capture-file-state (filename) + "Store the current state of FILENAME in a temporary file." + (when (and filename (file-exists-p filename)) + (let ((temp-file (make-temp-file + (concat "aidermacs-" + (file-name-nondirectory filename) "-")))) + (condition-case err + (progn + (copy-file filename temp-file t) + (cons filename temp-file)) + (error + (message "Error capturing file state for %s: %s" + filename (error-message-string err)) + (when (file-exists-p temp-file) + (delete-file temp-file)) + nil))))) + +(defun aidermacs--prepare-for-code-edit () + "Prepare for Aider code edits by capturing current file states." + (let ((files (when (boundp 'aidermacs--tracked-files) + aidermacs--tracked-files))) + (when files + ;; Initialize the tracking variables if they don't exist + (unless (boundp 'aidermacs--pre-edit-files) + (setq-local aidermacs--pre-edit-files nil)) + (unless (boundp 'aidermacs--pre-edit-buffers) + (setq-local aidermacs--pre-edit-buffers nil)) + + (setq aidermacs--pre-edit-files + (mapcar (lambda (file) + (let ((clean-file (replace-regexp-in-string " (read-only)$" "" file))) + (aidermacs--capture-file-state + (expand-file-name clean-file (aidermacs-project-root))))) + files))))) + +(defun aidermacs-cleanup-all-ediff-sessions () + "Quit all active aidermacs ediff sessions and clean up all resources. +This function ensures a complete cleanup of all ediff-related resources." + (interactive) + ;; Find and quit all active aidermacs ediff sessions + (dolist (buf (buffer-list)) + (when (string-match-p "\\*ediff-aidermacs:" (buffer-name buf)) + (with-current-buffer buf + ;; This will trigger the normal ediff cleanup process + (when (fboundp 'ediff-quit) + (ediff-quit))))) + + ;; Force cleanup of any remaining resources + (aidermacs--cleanup-stale-pre-edit-buffers)) + +(defun aidermacs--cleanup-stale-pre-edit-buffers () + "Clean up any stale pre-edit buffers that weren't properly disposed of. +This function should only handle truly orphaned resources that weren't +cleaned up by the normal ediff quit process." + ;; Clean up any orphaned pre-edit buffers + (when (boundp 'aidermacs--pre-edit-buffers) + (dolist (pair aidermacs--pre-edit-buffers) + (let ((file (car pair)) + (buffer (cdr pair))) + (when (and buffer (buffer-live-p buffer)) + (message "Cleaning up orphaned pre-edit buffer for %s" file) + (kill-buffer buffer))))) + + ;; Clean up any orphaned temp files + (when (boundp 'aidermacs--pre-edit-files) + (dolist (pair aidermacs--pre-edit-files) + (let ((file (car pair)) + (temp-file (cdr pair))) + (when (and temp-file (stringp temp-file) (file-exists-p temp-file)) + (message "Cleaning up orphaned temp file for %s" file) + (delete-file temp-file)))))) + +(defun aidermacs--setup-ediff-cleanup-hooks () + "Set up hooks to ensure proper cleanup of temporary buffers after ediff." + (add-hook 'ediff-quit-hook #'aidermacs--cleanup-stale-pre-edit-buffers) + ;; Also add to kill-emacs-hook to ensure cleanup on Emacs exit + (add-hook 'kill-emacs-hook #'aidermacs-cleanup-all-ediff-sessions)) + + +(defun aidermacs--detect-edited-files () + "Parse current output to find files edited by Aider." + (let ((edited-files nil) + (output aidermacs--current-output)) + (with-temp-buffer + (insert output) + (goto-char (point-min)) + (while (re-search-forward "Applied edit to \\(.+\\)" nil t) + (push (match-string 1) edited-files))) + (nreverse edited-files))) + +(defun aidermacs--create-pre-edit-buffer (filename temp-file) + "Create a buffer for FILENAME using content from TEMP-FILE for ediff." + (condition-case err + (let ((buffer (generate-new-buffer (format "*aidermacs-pre-edit:%s*" + (file-name-nondirectory filename))))) + (with-current-buffer buffer + (condition-case err2 + (progn + (insert-file-contents temp-file) + (set-buffer-modified-p nil) + ;; Use same major mode as the original file would have + (let ((buffer-file-name filename)) + (set-auto-mode)) + (setq buffer-read-only t)) + (error + (message "Error setting up pre-edit buffer: %s" (error-message-string err2)) + (kill-buffer buffer) + nil))) + buffer) + (error + (message "Failed to create pre-edit buffer: %s" (error-message-string err)) + nil))) + +(defun aidermacs--show-ediff-for-edited-files (edited-files) + "Show ediff for each file in EDITED-FILES." + (when edited-files + ;; Display a message about which files were changed + (message "Modified %d file(s): %s" + (length edited-files) + (mapconcat #'identity edited-files ", ")) + + (let ((file (car edited-files)) + (remaining (cdr edited-files))) + + ;; Find the pre-edit temp file for this file + (let* ((full-path (expand-file-name file (aidermacs-project-root))) + (pre-edit-pair (when (boundp 'aidermacs--pre-edit-files) + (assoc full-path aidermacs--pre-edit-files))) + (temp-file (and pre-edit-pair (cdr pre-edit-pair)))) + + (if (and temp-file + (stringp temp-file) + (file-exists-p temp-file)) + (progn + ;; Create buffer from temp file only when needed + (let ((pre-edit-buffer (aidermacs--create-pre-edit-buffer full-path temp-file))) + ;; Store buffer for cleanup + (unless (boundp 'aidermacs--pre-edit-buffers) + (setq-local aidermacs--pre-edit-buffers nil)) + (push (cons full-path pre-edit-buffer) aidermacs--pre-edit-buffers) + + ;; Start ediff session + (ediff-buffers + pre-edit-buffer + (or (get-file-buffer full-path) + (find-file-noselect full-path)) + ;; Setup callback for when this ediff session ends + (lambda () + ;; Clean up resources immediately and directly + ;; 1. Kill the pre-edit buffer immediately + (when (and pre-edit-buffer (buffer-live-p pre-edit-buffer)) + (kill-buffer pre-edit-buffer)) + ;; 2. Remove this file from the buffer tracking list + (when (boundp 'aidermacs--pre-edit-buffers) + (setq aidermacs--pre-edit-buffers + (assoc-delete-all full-path aidermacs--pre-edit-buffers))) + ;; 3. Delete the temp file immediately + (when (and temp-file (stringp temp-file) (file-exists-p temp-file)) + (delete-file temp-file)) + ;; 4. Remove this file from the temp file tracking list + (when (boundp 'aidermacs--pre-edit-files) + (setq aidermacs--pre-edit-files + (assoc-delete-all full-path aidermacs--pre-edit-files))) + ;; Process remaining files + (when remaining + (aidermacs--show-ediff-for-edited-files remaining))))) + + ;; Customize ediff session + (when (boundp 'ediff-buffer) + (with-current-buffer ediff-buffer + (rename-buffer (format "*ediff-aidermacs: %s*" file))))) + + ;; If no pre-edit temp file found, continue with remaining files + (when remaining + (aidermacs--show-ediff-for-edited-files remaining))))))) + +(defun aidermacs--send-command-with-edit-tracking (buffer command) + "Send COMMAND to aidermacs with edit tracking to the BUFFER." + ;; Prepare for edits + (aidermacs--prepare-for-code-edit) + ;; Send command and track output + (aidermacs--send-command-backend + buffer command nil (lambda () - (let ((files (aidermacs--parse-ls-output aidermacs--current-output))) - (funcall callback files))))) - -(defun aidermacs--send-command (command &optional switch-to-buffer use-existing) - "Send command to the corresponding aidermacs process. -COMMAND is the text to send. -If SWITCH-TO-BUFFER is non-nil, switch to the aidermacs buffer. -If USE-EXISTING is non-nil, use an existing buffer instead of creating new." - (let* ((buffer-name (aidermacs-get-buffer-name use-existing)) - (buffer (or (get-buffer buffer-name) - (progn (aidermacs-run) - (get-buffer buffer-name)))) - (processed-command (aidermacs--process-message-if-multi-line command))) - (aidermacs--send-command-backend buffer processed-command) - (when (and switch-to-buffer (not (string= (buffer-name) buffer-name))) - (aidermacs-switch-to-buffer buffer-name)))) + ;; Check if this was a code edit command and show ediff if needed + (when (aidermacs--is-edit-command-p command buffer) + (let ((edited-files (aidermacs--detect-edited-files))) + (when edited-files + (aidermacs--show-ediff-for-edited-files edited-files))))))) (defun aidermacs--is-edit-command-p (command buffer) "Determine if COMMAND might result in code edits based on current mode in BUFFER. @@ -340,8 +516,12 @@ If CALLBACK is non-nil it will be called after the command finishes." (get-buffer buffer-name)))) (processed-command (aidermacs--process-message-if-multi-line command))) + ;; Reset current output before sending new command + (with-current-buffer buffer + (setq-local aidermacs--current-output nil)) + (if (aidermacs--is-edit-command-p command buffer) - (aidermacs--send-command-with-edit-tracking processed-command no-switch-to-buffer use-existing) + (aidermacs--send-command-with-edit-tracking buffer processed-command) (aidermacs--send-command-backend buffer processed-command redirect callback)) (when (and (not no-switch-to-buffer) (not (string= (buffer-name) buffer-name))) (aidermacs-switch-to-buffer buffer-name)))) @@ -1039,5 +1219,8 @@ troubleshooting, etc." (setq-local aidermacs--current-mode 'help)) (message "Switched to help mode - aider will answer questions about using aider")) +;; Initialize the cleanup mechanisms +(aidermacs--setup-ediff-cleanup-hooks) + (provide 'aidermacs) ;;; aidermacs.el ends here