branch: elpa/aidermacs commit f3c8d006f524f807f2a43922b0d30c7af26862eb Author: Matthew Zeng <matthew...@posteo.net> Commit: GitHub <nore...@github.com>
Refactor output and ediff code into its own file (#80) * Refactor into aidermacs-output.el Signed-off-by: Mingde (Matthew) Zeng <matthew...@posteo.net> * Add aidermacs--command-may-edit-files Signed-off-by: Mingde (Matthew) Zeng <matthew...@posteo.net> * Remove aggressive cleanup Signed-off-by: Mingde (Matthew) Zeng <matthew...@posteo.net> * Rewrite aidermacs--prepare-for-code-edit Signed-off-by: Mingde (Matthew) Zeng <matthew...@posteo.net> * Fix linting Signed-off-by: Mingde (Matthew) Zeng <matthew...@posteo.net> * Fix capture file state font lock problem with treesitter Signed-off-by: Mingde (Matthew) Zeng <matthew...@posteo.net> --------- Signed-off-by: Mingde (Matthew) Zeng <matthew...@posteo.net> --- aidermacs-backend-comint.el | 21 +-- aidermacs-backend-vterm.el | 20 +-- aidermacs-backends.el | 155 +---------------- aidermacs-output.el | 400 ++++++++++++++++++++++++++++++++++++++++++++ aidermacs.el | 289 +++----------------------------- 5 files changed, 453 insertions(+), 432 deletions(-) diff --git a/aidermacs-backend-comint.el b/aidermacs-backend-comint.el index 1797ce53dd..3d30f6bbad 100644 --- a/aidermacs-backend-comint.el +++ b/aidermacs-backend-comint.el @@ -28,14 +28,15 @@ (require 'map) ;; Forward declarations -(declare-function aidermacs--prepare-for-code-edit "aidermacs") -(declare-function aidermacs--cleanup-temp-buffers "aidermacs") -(declare-function aidermacs--show-ediff-for-edited-files "aidermacs") -(declare-function aidermacs--detect-edited-files "aidermacs") -(declare-function aidermacs--process-message-if-multi-line "aidermacs" (str)) -(declare-function aidermacs--parse-output-for-files "aidermacs-backends" (output)) -(declare-function aidermacs--store-output "aidermacs-backends" (output)) -(declare-function aidermacs--is-aidermacs-buffer-p "aidermacs-backends" (&optional buffer)) +(declare-function aidermacs--prepare-for-code-edit "aidermacs-output") +(declare-function aidermacs--process-message-if-multi-line "aidermacs") +(declare-function aidermacs--command-may-edit-files "aidermacs") +(declare-function aidermacs--store-output "aidermacs-output") +(declare-function aidermacs--is-aidermacs-buffer-p "aidermacs-backends") +(declare-function aidermacs--parse-output-for-files "aidermacs-output") +(declare-function aidermacs--show-ediff-for-edited-files "aidermacs-output") +(declare-function aidermacs--cleanup-temp-buffers "aidermacs-output") +(declare-function aidermacs--detect-edited-files "aidermacs-output") (defvar aidermacs--last-command) @@ -288,8 +289,8 @@ PROC is the process to send to. STRING is the command to send." (if (member (downcase string) '("" "y" "n" "d" "yes" "no")) (aidermacs--parse-output-for-files aidermacs--comint-output-temp) (setq aidermacs--last-command string) - ;; Always prepare for potential edits - (aidermacs--prepare-for-code-edit))) + (when (aidermacs--command-may-edit-files string) + (aidermacs--prepare-for-code-edit)))) (comint-simple-send proc (aidermacs--process-message-if-multi-line string))) (defun aidermacs-run-comint (program args buffer-name) diff --git a/aidermacs-backend-vterm.el b/aidermacs-backend-vterm.el index 7ad1634ec4..e71c3dbf8b 100644 --- a/aidermacs-backend-vterm.el +++ b/aidermacs-backend-vterm.el @@ -19,8 +19,6 @@ ;; - Custom multi-line input keybindings ;; - Aider process management in VTerm -;; Originally forked from: Kang Tu <tni...@gmail.com> Aider.el - ;;; Code: (require 'vterm nil 'noerror) @@ -40,15 +38,15 @@ (declare-function vterm-insert "vterm") (declare-function vterm-send-C-c "vterm") -(declare-function aidermacs--prepare-for-code-edit "aidermacs") -(declare-function aidermacs--cleanup-temp-buffers "aidermacs") -(declare-function aidermacs--show-ediff-for-edited-files "aidermacs") (declare-function aidermacs--detect-edited-files "aidermacs") -(declare-function aidermacs--store-output "aidermacs") +(declare-function aidermacs--command-may-edit-files "aidermacs") (declare-function aidermacs--is-aidermacs-buffer-p "aidermacs") (declare-function aidermacs-get-buffer-name "aidermacs") - -(declare-function aidermacs--parse-output-for-files "aidermacs-backends" (output)) +(declare-function aidermacs--store-output "aidermacs-output") +(declare-function aidermacs--prepare-for-code-edit "aidermacs-output") +(declare-function aidermacs--parse-output-for-files "aidermacs-output" (output)) +(declare-function aidermacs--show-ediff-for-edited-files "aidermacs-output") +(declare-function aidermacs--cleanup-temp-buffers "aidermacs-output") (declare-function evil-define-minor-mode-key "evil-core") @@ -158,6 +156,8 @@ Use BUFFER if provided, otherwise retrieve it from `aidermacs-get-buffer-name'." (defun aidermacs--vterm-filter-buffer-substring (beg end &optional delete) + "Filter text from BEG to END in vterm buffer for cleaner display. +When DELETE is non-nil, extract and delete the region instead of copying it." (let* ((text (cond (delete (save-excursion @@ -308,8 +308,8 @@ BUFFER is the target buffer to send to. COMMAND is the text to send." (line-end-position)))) (when (not (string-empty-p command)) (setq-local aidermacs--last-command command) - ;; Always prepare for potential edits - (aidermacs--prepare-for-code-edit)))))) + (when (aidermacs--command-may-edit-files command) + (aidermacs--prepare-for-code-edit))))))) (defun aidermacs--vterm-cleanup () "Clean up vterm resources when buffer is killed." diff --git a/aidermacs-backends.el b/aidermacs-backends.el index 7087441507..3d923ba392 100644 --- a/aidermacs-backends.el +++ b/aidermacs-backends.el @@ -18,8 +18,6 @@ ;; - Abstracts backend-specific Aider functions ;; - Manages output history and callbacks -;; Originally forked from: Kang Tu <tni...@gmail.com> Aider.el - ;;; Code: (require 'aidermacs-backend-comint) @@ -28,8 +26,8 @@ (declare-function aidermacs-run-vterm "aidermacs-backend-vterm" (program args buffer-name)) (declare-function aidermacs--send-command-vterm "aidermacs-backend-vterm" (buffer command)) -(declare-function aidermacs-project-root "aidermacs" ()) -(declare-function aidermacs--prepare-for-code-edit "aidermacs" ()) +(declare-function aidermacs--prepare-for-code-edit "aidermacs-output") +(declare-function aidermacs-project-root "aidermacs") (declare-function aidermacs--get-files-in-session "aidermacs" (callback)) (defgroup aidermacs-backends nil @@ -44,155 +42,18 @@ of using a comint process." :type '(choice (const :tag "Comint" comint) (const :tag "VTerm" vterm))) -(defcustom aidermacs-output-limit 10 - "Maximum number of output entries to keep in history." - :type 'integer) - -(defvar-local aidermacs--output-history nil - "List to store aidermacs output history. -Each entry is a cons cell (timestamp . output-text).") - -(defvar-local aidermacs--last-command nil - "Store the last command sent to aidermacs.") - -(defvar-local aidermacs--current-output "" - "Accumulator for current output being captured as a string.") - -(defcustom aidermacs-before-run-backend-hook nil - "Hook run before the aidermacs backend is startd." - :type 'hook) - -(defun aidermacs-get-output-history (&optional limit) - "Get the output history, optionally limited to LIMIT entries. -LIMIT is the maximum number of entries to return. -Returns a list of (timestamp . output-text) pairs, most recent first." - (let ((history aidermacs--output-history)) - (if limit - (seq-take history limit) - history))) - -(defun aidermacs-clear-output-history () - "Clear the output history." - (interactive) - (setq aidermacs--output-history nil)) - (defvar-local aidermacs--current-callback nil "Store the callback function for the current command.") (defvar-local aidermacs--in-callback nil "Flag to prevent recursive callbacks.") -(defvar-local aidermacs--tracked-files nil - "List of files that have been mentioned in the aidermacs output. -This is used to avoid having to run /ls repeatedly.") - -(defun aidermacs--parse-output-for-files (output) - "Parse OUTPUT for files and add them to `aidermacs--tracked-files'." - (when output - (let ((lines (split-string output "\n")) - (last-line "") - (in-udiff nil) - (current-udiff-file nil)) - (dolist (line lines) - (cond - ;; Applied edit to <filename> - ((string-match "Applied edit to \\(\\./\\)?\\(.+\\)" line) - (when-let* ((file (match-string 2 line))) - (add-to-list 'aidermacs--tracked-files file))) - - ;; Added <filename> to the chat. - ((string-match "Added \\(\\./\\)?\\(.+\\) to the chat" line) - (when-let* ((file (match-string 2 line))) - (add-to-list 'aidermacs--tracked-files file))) - - ;; Removed <filename> from the chat (with or without ./ prefix) - ((string-match "Removed \\(\\./\\)?\\(.+\\) from the chat" line) - (when-let* ((file (match-string 2 line))) - (setq aidermacs--tracked-files (delete file aidermacs--tracked-files)))) - - ;; Added <filename> to read-only files. - ((string-match "Added \\(\\./\\)?\\(.+\\) to read-only files" line) - (when-let* ((file (match-string 2 line))) - (add-to-list 'aidermacs--tracked-files (concat file " (read-only)")))) - - ;; Moved <file> from editable to read-only files in the chat - ((string-match "Moved \\(\\./\\)?\\(.+\\) from editable to read-only files in the chat" line) - (when-let* ((file (match-string 2 line))) - (let ((editable-file (replace-regexp-in-string " (read-only)$" "" file))) - (setq aidermacs--tracked-files (delete editable-file aidermacs--tracked-files)) - (add-to-list 'aidermacs--tracked-files (concat file " (read-only)"))))) - - ;; Moved <file> from read-only to editable files in the chat - ((string-match "Moved \\(\\./\\)?\\(.+\\) from read-only to editable files in the chat" line) - (when-let* ((file (match-string 2 line))) - (let ((read-only-file (concat file " (read-only)"))) - (setq aidermacs--tracked-files (delete read-only-file aidermacs--tracked-files)) - (add-to-list 'aidermacs--tracked-files file)))) - - ;; <file>\nAdd file to the chat? - ((string-match "Add file to the chat?" line) - (add-to-list 'aidermacs--tracked-files last-line) - (aidermacs--prepare-for-code-edit)) - - ;; <file> is already in the chat as an editable file - ((string-match "\\(\\./\\)?\\(.+\\) is already in the chat as an editable file" line) - (when-let* ((file (match-string 2 line))) - (add-to-list 'aidermacs--tracked-files file))) - - ;; Handle udiff format - ;; Detect start of udiff with "--- filename" - ((string-match "^--- \\(\\./\\)?\\(.+\\)" line) - (setq in-udiff t - current-udiff-file (match-string 2 line))) - - ;; Confirm udiff file with "+++ filename" line - ((and in-udiff - current-udiff-file - (string-match "^\\+\\+\\+ \\(\\./\\)?\\(.+\\)" line)) - (let ((plus-file (match-string 2 line))) - ;; Only add if the filenames match (ignoring ./ prefix) - (when (string= (file-name-nondirectory current-udiff-file) - (file-name-nondirectory plus-file)) - (add-to-list 'aidermacs--tracked-files current-udiff-file) - (setq in-udiff nil - current-udiff-file nil))))) - - (setq last-line line)) - - ;; Verify all tracked files exist - (let* ((project-root (aidermacs-project-root)) - (is-remote (file-remote-p project-root)) - (valid-files nil)) - (dolist (file aidermacs--tracked-files) - (let* ((is-readonly (string-match-p " (read-only)$" file)) - (actual-file (if is-readonly - (substring file 0 (- (length file) 12)) - file)) - (full-path (expand-file-name actual-file project-root))) - (when (or (file-exists-p full-path) is-remote) - (push file valid-files)))) - (setq aidermacs--tracked-files valid-files))))) - -(defun aidermacs--store-output (output) - "Store output string in the history with timestamp. -OUTPUT is the string to store. -If there's a callback function, call it with the output." - (when (stringp output) - ;; Store the output - (setq aidermacs--current-output (substring-no-properties output)) - (push (cons (current-time) (substring-no-properties output)) aidermacs--output-history) - ;; Trim history if needed - (when (> (length aidermacs--output-history) aidermacs-output-limit) - (setq aidermacs--output-history - (seq-take aidermacs--output-history aidermacs-output-limit))) - ;; Parse files from output - (aidermacs--parse-output-for-files output) - ;; Handle callback if present - (unless aidermacs--in-callback - (when (functionp aidermacs--current-callback) - (let ((aidermacs--in-callback t)) - (funcall aidermacs--current-callback) - (setq aidermacs--current-callback nil)))))) +(defcustom aidermacs-before-run-backend-hook nil + "Hook run before the aidermacs backend is startd." + :type 'hook) + +(defconst aidermacs-prompt-regexp "^[^[:space:]<]*>[[:space:]]+$" + "Regexp to match Aider's command prompt.") ;; Backend dispatcher functions (defun aidermacs-run-backend (program args buffer-name) diff --git a/aidermacs-output.el b/aidermacs-output.el new file mode 100644 index 0000000000..d2ec8b840b --- /dev/null +++ b/aidermacs-output.el @@ -0,0 +1,400 @@ +;;; aidermacs-output.el --- Output manipulation for Aidermacs -*- lexical-binding: t; -*- +;; Author: Mingde (Matthew) Zeng <matthew...@posteo.net> +;; Version: 1.0 +;; Keywords: ai emacs llm aider ai-pair-programming tools +;; URL: https://github.com/MatthewZMD/aidermacs +;; SPDX-License-Identifier: Apache-2.0 + +;; This file is not part of GNU Emacs. + +;;; Commentary: + +;; This file contains the output and diff functionality for Aidermacs. + +;;; Code: + +(require 'ediff) +(require 'cl-lib) + +(declare-function aidermacs-get-buffer-name "aidermacs") +(declare-function aidermacs-project-root "aidermacs") +(declare-function aidermacs--is-aidermacs-buffer-p "aidermacs") +(declare-function aidermacs--prepare-file-paths-for-command "aidermacs") + +(defgroup aidermacs-output nil + "Output manipulation for Aidermacs." + :group 'aidermacs) + +(defvar-local aidermacs--tracked-files nil + "List of files that have been mentioned in the aidermacs output. +This is used to avoid having to run /ls repeatedly.") + +(defvar-local aidermacs--output-history nil + "List to store aidermacs output history. +Each entry is a cons cell (timestamp . output-text).") + +(defvar-local aidermacs--last-command nil + "Store the last command sent to aidermacs.") + +(defvar-local aidermacs--current-output "" + "Accumulator for current output being captured as a string.") + +(defcustom aidermacs-output-limit 10 + "Maximum number of output entries to keep in history." + :type 'integer) + +(defcustom aidermacs-show-diff-after-change t + "When non-nil, enable ediff for reviewing AI-generated changes. +When nil, skip preparing temp buffers and showing ediff comparisons." + :type 'boolean + :group 'aidermacs) + +(defvar-local aidermacs--pre-edit-file-buffers nil + "Alist of (filename . temp-buffer) storing file state before Aider edits. +These contain the original content of files that might be modified by Aider.") + +(defvar-local aidermacs--ediff-queue nil + "Buffer-local queue of files waiting to be processed by ediff.") + +(defvar aidermacs--pre-ediff-window-config nil + "Window configuration before starting ediff sessions.") + +(defun aidermacs--ensure-current-file-tracked () + "Ensure current file is tracked in the aidermacs session." + (when buffer-file-name + (let* ((session-buffer (get-buffer (aidermacs-get-buffer-name))) + (filename buffer-file-name) + (relative-path (file-relative-name filename (aidermacs-project-root)))) + (when session-buffer + (with-current-buffer session-buffer + (unless (member relative-path aidermacs--tracked-files) + (push relative-path aidermacs--tracked-files) + (let ((command (aidermacs--prepare-file-paths-for-command "/add" (list relative-path)))) + (aidermacs--send-command-backend session-buffer command nil)))))))) + +(defun aidermacs--capture-file-state (filename) + "Store the current state of FILENAME in a temporary buffer. +Creates a read-only buffer with the file's content, appropriate major mode, +and syntax highlighting to match the original file." + (when (and filename (file-exists-p filename)) + (condition-case err + (let ((temp-buffer (generate-new-buffer + (format " *aidermacs-pre-edit:%s*" + (file-name-nondirectory filename))))) + (with-current-buffer temp-buffer + (insert-file-contents filename) + (set-buffer-modified-p nil) + ;; Use same major mode as the original file + (let ((buffer-file-name filename) + (delay-mode-hooks t)) + (set-auto-mode) + ;; Ensure syntax highlighting is applied + (font-lock-mode 1)) + ;; Make buffer read-only + (setq buffer-read-only t)) + (cons filename temp-buffer)) + (error + (message "Error capturing file state for %s: %s" + filename (error-message-string err)) + nil)))) + +(defun aidermacs--cleanup-temp-buffers () + "Clean up all temporary buffers created for ediff sessions. +This is called when all ediff sessions are complete. +Kills all pre-edit buffers that were created to store original file content." + (interactive) + (with-current-buffer (get-buffer (aidermacs-get-buffer-name)) + ;; Clean up buffers in the tracking list + (dolist (file-pair aidermacs--pre-edit-file-buffers) + (let ((temp-buffer (cdr file-pair))) + (when (and temp-buffer (buffer-live-p temp-buffer)) + (kill-buffer temp-buffer)))) + ;; Clear the list after cleanup + (setq aidermacs--pre-edit-file-buffers nil))) + +(defun aidermacs--prepare-for-code-edit () + "Prepare for code edits by capturing current file states in memory buffers. +Creates temporary buffers containing the original content of all tracked files. +This is skipped if `aidermacs-show-diff-after-change' is nil." + (when aidermacs-show-diff-after-change + (when-let* ((files aidermacs--tracked-files)) + (let ((attempts 0) + (max-attempts 3)) + ;; Use iteration rather than recursion with a limit on attempts + (while (and (zerop (length aidermacs--pre-edit-file-buffers)) + (< attempts max-attempts)) + (setq aidermacs--pre-edit-file-buffers + (cl-remove-duplicates + (delq nil + (mapcar (lambda (file) + (let* ((clean-file (replace-regexp-in-string " (read-only)$" "" file)) + (full-path (expand-file-name clean-file (aidermacs-project-root)))) + ;; Only capture state if we don't already have it + (or (assoc full-path aidermacs--pre-edit-file-buffers) + (aidermacs--capture-file-state full-path)))) + files)) + :test (lambda (a b) (equal (car a) (car b))))) + (setq attempts (1+ attempts)) + ;; Add a small delay before retry to allow for file system operations + (when (and (zerop (length aidermacs--pre-edit-file-buffers)) + (< attempts max-attempts)) + (sit-for 0.1))) + + (if (zerop (length aidermacs--pre-edit-file-buffers)) + (message "Warning: Failed to capture file states after %d attempts" max-attempts) + (message "Prepared code edit for %d files" (length aidermacs--pre-edit-file-buffers))))))) + +(defun aidermacs--ediff-quit-handler () + "Handle ediff session cleanup and process next files in queue. +This function is called when an ediff session is quit and processes +the next file in the ediff queue if any remain." + (when (and (boundp 'ediff-buffer-A) + (buffer-live-p ediff-buffer-A) + (string-match " \\*aidermacs-pre-edit:" + (buffer-name ediff-buffer-A))) + (aidermacs--process-next-ediff-file))) + +(defun aidermacs--setup-ediff-cleanup-hooks () + "Set up hooks to ensure proper cleanup of temporary buffers after ediff. +Only adds the hook if it's not already present." + (unless (member #'aidermacs--ediff-quit-handler ediff-quit-hook) + (add-hook 'ediff-quit-hook #'aidermacs--ediff-quit-handler))) + +(defun aidermacs--parse-output-for-files (output) + "Parse OUTPUT for files and add them to `aidermacs--tracked-files'." + (when output + (let ((lines (split-string output "\n")) + (last-line "") + (in-udiff nil) + (current-udiff-file nil)) + (dolist (line lines) + (cond + ;; Applied edit to <filename> + ((string-match "Applied edit to \\(\\./\\)?\\(.+\\)" line) + (when-let* ((file (match-string 2 line))) + (add-to-list 'aidermacs--tracked-files file))) + + ;; Added <filename> to the chat. + ((string-match "Added \\(\\./\\)?\\(.+\\) to the chat" line) + (when-let* ((file (match-string 2 line))) + (add-to-list 'aidermacs--tracked-files file))) + + ;; Removed <filename> from the chat (with or without ./ prefix) + ((string-match "Removed \\(\\./\\)?\\(.+\\) from the chat" line) + (when-let* ((file (match-string 2 line))) + (setq aidermacs--tracked-files (delete file aidermacs--tracked-files)))) + + ;; Added <filename> to read-only files. + ((string-match "Added \\(\\./\\)?\\(.+\\) to read-only files" line) + (when-let* ((file (match-string 2 line))) + (add-to-list 'aidermacs--tracked-files (concat file " (read-only)")))) + + ;; Moved <file> from editable to read-only files in the chat + ((string-match "Moved \\(\\./\\)?\\(.+\\) from editable to read-only files in the chat" line) + (when-let* ((file (match-string 2 line))) + (let ((editable-file (replace-regexp-in-string " (read-only)$" "" file))) + (setq aidermacs--tracked-files (delete editable-file aidermacs--tracked-files)) + (add-to-list 'aidermacs--tracked-files (concat file " (read-only)"))))) + + ;; Moved <file> from read-only to editable files in the chat + ((string-match "Moved \\(\\./\\)?\\(.+\\) from read-only to editable files in the chat" line) + (when-let* ((file (match-string 2 line))) + (let ((read-only-file (concat file " (read-only)"))) + (setq aidermacs--tracked-files (delete read-only-file aidermacs--tracked-files)) + (add-to-list 'aidermacs--tracked-files file)))) + + ;; <file>\nAdd file to the chat? + ((string-match "Add file to the chat?" line) + (add-to-list 'aidermacs--tracked-files last-line) + (aidermacs--prepare-for-code-edit)) + + ;; <file> is already in the chat as an editable file + ((string-match "\\(\\./\\)?\\(.+\\) is already in the chat as an editable file" line) + (when-let* ((file (match-string 2 line))) + (add-to-list 'aidermacs--tracked-files file))) + + ;; Handle udiff format + ;; Detect start of udiff with "--- filename" + ((string-match "^--- \\(\\./\\)?\\(.+\\)" line) + (setq in-udiff t + current-udiff-file (match-string 2 line))) + + ;; Confirm udiff file with "+++ filename" line + ((and in-udiff + current-udiff-file + (string-match "^\\+\\+\\+ \\(\\./\\)?\\(.+\\)" line)) + (let ((plus-file (match-string 2 line))) + ;; Only add if the filenames match (ignoring ./ prefix) + (when (string= (file-name-nondirectory current-udiff-file) + (file-name-nondirectory plus-file)) + (add-to-list 'aidermacs--tracked-files current-udiff-file) + (setq in-udiff nil + current-udiff-file nil))))) + + (setq last-line line)) + + ;; Verify all tracked files exist + (let* ((project-root (aidermacs-project-root)) + (is-remote (file-remote-p project-root)) + (valid-files nil)) + (dolist (file aidermacs--tracked-files) + (let* ((is-readonly (string-match-p " (read-only)$" file)) + (actual-file (if is-readonly + (substring file 0 (- (length file) 12)) + file)) + (full-path (expand-file-name actual-file project-root))) + (when (or (file-exists-p full-path) is-remote) + (push file valid-files)))) + (setq aidermacs--tracked-files valid-files))))) + +(defun aidermacs--store-output (output) + "Store output string in the history with timestamp. +OUTPUT is the string to store. +If there's a callback function, call it with the output." + (when (stringp output) + ;; Store the output + (setq aidermacs--current-output (substring-no-properties output)) + (push (cons (current-time) (substring-no-properties output)) aidermacs--output-history) + ;; Trim history if needed + (when (> (length aidermacs--output-history) aidermacs-output-limit) + (setq aidermacs--output-history + (seq-take aidermacs--output-history aidermacs-output-limit))) + ;; Parse files from output + (aidermacs--parse-output-for-files output) + ;; Handle callback if present + (unless aidermacs--in-callback + (when (functionp aidermacs--current-callback) + (let ((aidermacs--in-callback t)) + (funcall aidermacs--current-callback) + (setq aidermacs--current-callback nil)))))) + +(defun aidermacs-show-output-history () + "Display the AI output history in a new buffer." + (interactive) + (let ((buf (get-buffer-create "*aidermacs-history*")) + (history aidermacs--output-history)) + (with-current-buffer buf + (org-mode) + (setq buffer-read-only nil) + (erase-buffer) + (display-line-numbers-mode 1) + (dolist (entry history) + (let ((timestamp (format-time-string "%F %T" (car entry))) + (output (cdr entry))) + (insert (format "* %s\n#+BEGIN_SRC\n%s\n#+END_SRC\n" timestamp output)))) + (goto-char (point-min)) + (setq buffer-read-only t) + (local-set-key (kbd "q") #'kill-this-buffer) + (switch-to-buffer-other-window buf)))) + +(defun aidermacs-clear-output-history () + "Clear the output history." + (interactive) + (setq aidermacs--output-history nil)) + +(defun aidermacs-get-last-output () + "Get the most recent output from aidermacs." + (interactive) + (when (stringp aidermacs--current-output) + (message "%s" aidermacs--current-output) + (kill-new aidermacs--current-output) + aidermacs--current-output)) + +(defun aidermacs--detect-edited-files () + "Parse current output to find files edited by Aider. +Returns a list of files that have been modified according to the output." + (let ((project-root (aidermacs-project-root)) + (output aidermacs--current-output) + (edited-files) + (unique-files) + (valid-files)) + (when output + (with-temp-buffer + (insert output) + (goto-char (point-min)) + + ;; Case 1: Find "Applied edit to" lines + (while (search-forward "Applied edit to" nil t) + (beginning-of-line) + (when-let* ((file (and (looking-at ".*Applied edit to \\(\\./\\)?\\([^[:space:]]+\\)") + (match-string-no-properties 2)))) + (push file edited-files)) + (forward-line 1)) + + ;; Case 2: Find triple backtick blocks with filenames + (goto-char (point-min)) + (while (search-forward "```" nil t) + (save-excursion + (forward-line -1) + (let ((potential-file (string-trim (buffer-substring (line-beginning-position) (line-end-position))))) + (when (and (not (string-empty-p potential-file)) + (not (string-match-p "\\`[[:space:]]*\\'" potential-file)) + (not (string-match-p "^```" potential-file))) + (push potential-file edited-files)))) + (forward-line 1)) + + ;; Case 3: Handle udiff format + (goto-char (point-min)) + (while (search-forward "--- " nil t) + (let* ((line-end (line-end-position)) + (current-udiff-file (buffer-substring (point) line-end))) + (forward-line 1) + (when (looking-at "\\+\\+\\+ ") + (let ((plus-file (buffer-substring (+ (point) 4) (line-end-position)))) + (when (string= (file-name-nondirectory current-udiff-file) + (file-name-nondirectory plus-file)) + (push current-udiff-file edited-files))))))) + + ;; Filter the list to only include valid files + (setq unique-files (delete-dups edited-files)) + (setq valid-files (nreverse (cl-remove-if-not + (lambda (file) + (file-exists-p (expand-file-name file project-root))) + unique-files))) + ;; Display a message about which files were changed + (message "Modified file(s): %s" valid-files) + valid-files))) + +(defun aidermacs--show-ediff-for-edited-files (edited-files) + "Show ediff for each file in EDITED-FILES. +This is skipped if `aidermacs-show-diff-after-change' is nil." + (when (and aidermacs-show-diff-after-change edited-files) + ;; Save current window configuration + (setq aidermacs--pre-ediff-window-config (current-window-configuration)) + ;; Set up the queue in the current buffer + (setq-local aidermacs--ediff-queue edited-files) + ;; Process the first file + (aidermacs--process-next-ediff-file))) + +(defun aidermacs--process-next-ediff-file () + "Process the next file in the ediff queue for the current buffer." + (with-current-buffer (get-buffer (aidermacs-get-buffer-name)) + (if aidermacs--ediff-queue + (let ((file (pop aidermacs--ediff-queue))) + (aidermacs--show-ediff-for-file file)) + (aidermacs--cleanup-temp-buffers) + ;; Restore original window configuration + (when aidermacs--pre-ediff-window-config + (set-window-configuration aidermacs--pre-ediff-window-config) + (setq aidermacs--pre-ediff-window-config nil))))) + +(defun aidermacs--show-ediff-for-file (file) + "Uses the pre-edit buffer stored to compare with the current FILE state." + (let* ((full-path (expand-file-name file (aidermacs-project-root))) + (pre-edit-pair (assoc full-path aidermacs--pre-edit-file-buffers)) + (pre-edit-buffer (and pre-edit-pair (cdr pre-edit-pair)))) + (if (and pre-edit-buffer (buffer-live-p pre-edit-buffer)) + (progn + (let ((current-buffer (or (get-file-buffer full-path) + (find-file-noselect full-path)))) + (with-current-buffer current-buffer + (revert-buffer t t t)) + (delete-other-windows (get-buffer-window (switch-to-buffer current-buffer))) + ;; Start ediff session + (ediff-buffers pre-edit-buffer current-buffer))) + ;; If no pre-edit buffer found, continue with next file + (message "No pre-edit buffer found for %s, skipping" file) + (aidermacs--process-next-ediff-file)))) + +(provide 'aidermacs-output) +;;; aidermacs-output.el ends here diff --git a/aidermacs.el b/aidermacs.el index 7cf439a982..597aa85225 100644 --- a/aidermacs.el +++ b/aidermacs.el @@ -28,7 +28,6 @@ (require 'compat) (require 'comint) (require 'dired) -(require 'ediff) (require 'project) (require 'transient) (require 'vc-git) @@ -40,28 +39,21 @@ (require 'aidermacs-backends) (require 'aidermacs-models) +(require 'aidermacs-output) + +(declare-function magit-show-commit "magit-diff" (rev &optional noselect module)) (defgroup aidermacs nil "AI pair programming with Aider." :group 'aidermacs) -(defvar-local aidermacs--current-mode nil - "Buffer-local variable to track the current aidermacs mode. -Possible values: `code', `ask', `architect', `help'.") - -(declare-function magit-show-commit "magit-diff" (rev &optional noselect module)) - -(defcustom aidermacs-show-diff-after-change t - "When non-nil, enable ediff for reviewing AI-generated changes. -When nil, skip preparing temp buffers and showing ediff comparisons." - :type 'boolean) - (defcustom aidermacs-program "aider" "The name or path of the aidermacs program." :type 'string) -(define-obsolete-variable-alias 'aidermacs-args 'aidermacs-extra-args "0.5.0" - "Old name for `aidermacs-extra-args', please update your config.") +(defvar-local aidermacs--current-mode nil + "Buffer-local variable to track the current aidermacs mode. +Possible values: `code', `ask', `architect', `help'.") (defcustom aidermacs-config-file nil "Path to aider configuration file. @@ -70,6 +62,9 @@ ignoring other configuration settings except `aidermacs-extra-args'." :type '(choice (const :tag "None" nil) (file :tag "Config file"))) +(define-obsolete-variable-alias 'aidermacs-args 'aidermacs-extra-args "0.5.0" + "Old name for `aidermacs-extra-args', please update your config.") + (defcustom aidermacs-extra-args '() "Additional arguments to pass to the aidermacs command." :type '(repeat string)) @@ -134,13 +129,6 @@ This function tries multiple methods to determine the project root." This is the file name without path." :type 'string) -(defconst aidermacs-prompt-regexp "^[^[:space:]<]*>[[:space:]]+$" - "Regexp to match Aider's command prompt.") - -(defvar-local aidermacs--pre-edit-file-buffers nil - "Alist of (filename . temp-buffer) storing file state before Aider edits. -These contain the original content of files that might be modified by Aider.") - (transient-define-prefix aidermacs-transient-menu () "AI Pair Programming Interface." ["Aidermacs: AI Pair Programming" @@ -327,196 +315,16 @@ This is useful for working in monorepos where you want to limit aider's scope." (let ((aidermacs-subtree-only t)) (aidermacs-run))) -(defun aidermacs--capture-file-state (filename) - "Store the current state of FILENAME in a temporary buffer. -Creates a read-only buffer with the file's content, appropriate major mode, -and syntax highlighting to match the original file." - (when (and filename (file-exists-p filename)) - (condition-case err - (let ((temp-buffer (generate-new-buffer - (format " *aidermacs-pre-edit:%s*" - (file-name-nondirectory filename))))) - (with-current-buffer temp-buffer - (insert-file-contents filename) - (set-buffer-modified-p nil) - ;; Use same major mode as the original file - (let ((buffer-file-name filename)) - (set-auto-mode) - ;; Ensure syntax highlighting is applied - (font-lock-ensure)) - ;; Make buffer read-only - (setq buffer-read-only t)) - (cons filename temp-buffer)) - (error - (message "Error capturing file state for %s: %s" - filename (error-message-string err)) - nil)))) - -(defun aidermacs--cleanup-temp-buffers () - "Clean up all temporary buffers created for ediff sessions. -This is called when all ediff sessions are complete. -Kills all pre-edit buffers that were created to store original file content." - (interactive) - (with-current-buffer (get-buffer (aidermacs-get-buffer-name)) - ;; Clean up buffers in the tracking list - (dolist (file-pair aidermacs--pre-edit-file-buffers) - (let ((temp-buffer (cdr file-pair))) - (when (and temp-buffer (buffer-live-p temp-buffer)) - (kill-buffer temp-buffer)))) - ;; Also clean up any stray pre-edit buffers that might have been missed - (dolist (buf (buffer-list)) - (when (and (string-match " \\*aidermacs-pre-edit:" (buffer-name buf)) - (buffer-live-p buf)) - (kill-buffer buf))) - ;; Clear the list after cleanup - (setq aidermacs--pre-edit-file-buffers nil))) - -(defun aidermacs--prepare-for-code-edit () - "Prepare for code edits by capturing current file states in memory buffers. -Creates temporary buffers containing the original content of all tracked files. -This is skipped if `aidermacs-show-diff-after-change' is nil." - (when aidermacs-show-diff-after-change - (let ((files aidermacs--tracked-files)) - (when files - (message "Preparing code edit for %s" files) - (setq aidermacs--pre-edit-file-buffers - (cl-remove-duplicates - (mapcar (lambda (file) - (let* ((clean-file (replace-regexp-in-string " (read-only)$" "" file)) - (full-path (expand-file-name clean-file (aidermacs-project-root)))) - ;; Only capture state if we don't already have it - (or (assoc full-path aidermacs--pre-edit-file-buffers) - (aidermacs--capture-file-state full-path)))) - files) - :test (lambda (a b) (equal (car a) (car b))))) - ;; Remove nil entries from the list (where capture failed or was skipped) - (setq aidermacs--pre-edit-file-buffers (delq nil aidermacs--pre-edit-file-buffers)) - ;; Run again if it's nil - (unless aidermacs--pre-edit-file-buffers - (aidermacs--prepare-for-code-edit)))))) - -(defun aidermacs--ediff-quit-handler () - "Handle ediff session cleanup and process next files in queue. -This function is called when an ediff session is quit and processes -the next file in the ediff queue if any remain." - (when (and (boundp 'ediff-buffer-A) - (buffer-live-p ediff-buffer-A) - (string-match " \\*aidermacs-pre-edit:" - (buffer-name ediff-buffer-A))) - (aidermacs--process-next-ediff-file))) - -(defun aidermacs--setup-ediff-cleanup-hooks () - "Set up hooks to ensure proper cleanup of temporary buffers after ediff. -Only adds the hook if it's not already present." - (unless (member #'aidermacs--ediff-quit-handler ediff-quit-hook) - (add-hook 'ediff-quit-hook #'aidermacs--ediff-quit-handler))) - -(defun aidermacs--detect-edited-files () - "Parse current output to find files edited by Aider. -Returns a list of files that have been modified according to the output." - (let ((project-root (aidermacs-project-root)) - (output aidermacs--current-output) - (edited-files) - (unique-files) - (valid-files)) - (when output - (with-temp-buffer - (insert output) - (goto-char (point-min)) - - ;; Case 1: Find "Applied edit to" lines - (while (search-forward "Applied edit to" nil t) - (beginning-of-line) - (when-let* ((file (and (looking-at ".*Applied edit to \\(\\./\\)?\\([^[:space:]]+\\)") - (match-string-no-properties 2)))) - (push file edited-files)) - (forward-line 1)) - - ;; Case 2: Find triple backtick blocks with filenames - (goto-char (point-min)) - (while (search-forward "```" nil t) - (save-excursion - (forward-line -1) - (let ((potential-file (string-trim (buffer-substring (line-beginning-position) (line-end-position))))) - (when (and (not (string-empty-p potential-file)) - (not (string-match-p "\\`[[:space:]]*\\'" potential-file)) - (not (string-match-p "^```" potential-file))) - (push potential-file edited-files)))) - (forward-line 1)) - - ;; Case 3: Handle udiff format - (goto-char (point-min)) - (while (search-forward "--- " nil t) - (let* ((line-end (line-end-position)) - (current-udiff-file (buffer-substring (point) line-end))) - (forward-line 1) - (when (looking-at "\\+\\+\\+ ") - (let ((plus-file (buffer-substring (+ (point) 4) (line-end-position)))) - (when (string= (file-name-nondirectory current-udiff-file) - (file-name-nondirectory plus-file)) - (push current-udiff-file edited-files))))))) - - ;; Filter the list to only include valid files - (setq unique-files (delete-dups edited-files)) - (setq valid-files (cl-remove-if-not - (lambda (file) - (file-exists-p (expand-file-name file project-root))) - unique-files)) - (nreverse valid-files)))) - -(defvar-local aidermacs--ediff-queue nil - "Buffer-local queue of files waiting to be processed by ediff.") - -(defvar aidermacs--pre-ediff-window-config nil - "Window configuration before starting ediff sessions.") - -(defun aidermacs--process-next-ediff-file () - "Process the next file in the ediff queue for the current buffer." - (with-current-buffer (get-buffer (aidermacs-get-buffer-name)) - (if aidermacs--ediff-queue - (let ((file (pop aidermacs--ediff-queue))) - (aidermacs--show-ediff-for-file file)) - (aidermacs--cleanup-temp-buffers) - ;; Restore original window configuration - (when aidermacs--pre-ediff-window-config - (set-window-configuration aidermacs--pre-ediff-window-config) - (setq aidermacs--pre-ediff-window-config nil))))) - -(defun aidermacs--show-ediff-for-file (file) - "Uses the pre-edit buffer stored to compare with the current FILE state." - (let* ((full-path (expand-file-name file (aidermacs-project-root))) - (pre-edit-pair (assoc full-path aidermacs--pre-edit-file-buffers)) - (pre-edit-buffer (and pre-edit-pair (cdr pre-edit-pair)))) - (if (and pre-edit-buffer (buffer-live-p pre-edit-buffer)) - (progn - (let ((current-buffer (or (get-file-buffer full-path) - (find-file-noselect full-path)))) - (with-current-buffer current-buffer - (revert-buffer t t t)) - (delete-other-windows (get-buffer-window (switch-to-buffer current-buffer))) - ;; Start ediff session - (ediff-buffers pre-edit-buffer current-buffer))) - ;; If no pre-edit buffer found, continue with next file - (message "No pre-edit buffer found for %s, skipping" file) - (aidermacs--process-next-ediff-file)))) - -(defun aidermacs--show-ediff-for-edited-files (edited-files) - "Show ediff for each file in EDITED-FILES. -This is skipped if `aidermacs-show-diff-after-change' is nil." - (when (and aidermacs-show-diff-after-change edited-files) - ;; Save current window configuration - (setq aidermacs--pre-ediff-window-config (current-window-configuration)) - - ;; Display a message about which files were changed - (message "Modified %d file(s): %s" - (length edited-files) - (mapconcat #'identity edited-files ", ")) - - ;; Set up the queue in the current buffer - (setq-local aidermacs--ediff-queue edited-files) - - ;; Process the first file - (aidermacs--process-next-ediff-file))) +(defun aidermacs--command-may-edit-files (command) + "Check if COMMAND may result in file edits. +Returns t if the command is likely to modify files, nil otherwise. +In code/architect mode, commands without prefixes may edit. +Commands containing /code or /architect always may edit." + (and (stringp command) + (or (and (memq aidermacs--current-mode '(code architect)) + (not (string-match-p "^/" command))) + (string-match-p "/code" command) + (string-match-p "/architect" command)))) (defun aidermacs--send-command (command &optional no-switch-to-buffer use-existing redirect callback) "Send command to the corresponding aidermacs process. @@ -530,16 +338,16 @@ If CALLBACK is non-nil it will be called after the command finishes." (progn (aidermacs-run) (get-buffer buffer-name)))) (processed-command (aidermacs--process-message-if-multi-line command))) - ;; Reset current output before sending new command + ;; Check if command may edit files and prepare accordingly (with-current-buffer buffer + ;; Reset current output before sending new command (setq aidermacs--current-output "") (setq aidermacs--current-callback callback) (setq aidermacs--last-command processed-command) - ;; Always prepare for potential edits (aidermacs--cleanup-temp-buffers) - ;; Ensure current file is tracked before preparing for code edit (aidermacs--ensure-current-file-tracked) - (aidermacs--prepare-for-code-edit) + (when (aidermacs--command-may-edit-files command) + (aidermacs--prepare-for-code-edit)) (aidermacs--send-command-backend buffer processed-command redirect)) (when (and (not no-switch-to-buffer) (not (string= (buffer-name) buffer-name))) @@ -721,8 +529,7 @@ Sends the \"/ls\" command and displays the results in a Dired buffer." (aidermacs--send-command "/drop")) (defun aidermacs-batch-drop-dired-marked-files () - "Drop Dired marked files from the aidermacs session. -If called from the special aidermacs files buffer, kill the buffer after dropping files." + "Drop Dired marked files from the aidermacs session." (interactive) (unless (derived-mode-p 'dired-mode) (user-error "This command can only be used in Dired mode")) @@ -735,54 +542,6 @@ If called from the special aidermacs files buffer, kill the buffer after droppin (message "Closing aidermacs file buffer after dropping files") (kill-buffer (aidermacs-get-buffer-name nil " Files"))))) -(defun aidermacs-show-output-history () - "Display the AI output history in a new buffer." - (interactive) - (let ((buf (get-buffer-create "*aidermacs-history*")) - (history aidermacs--output-history)) - (with-current-buffer buf - (org-mode) - (setq buffer-read-only nil) - (erase-buffer) - (display-line-numbers-mode 1) - (dolist (entry history) - (let ((timestamp (format-time-string "%F %T" (car entry))) - (output (cdr entry))) - (insert (format "* %s\n#+BEGIN_SRC\n%s\n#+END_SRC\n" timestamp output)))) - (goto-char (point-min)) - (setq buffer-read-only t) - (local-set-key (kbd "q") #'kill-this-buffer) - (switch-to-buffer-other-window buf)))) - -(defun aidermacs-get-last-output () - "Get the most recent output from aidermacs." - (interactive) - (when (stringp aidermacs--current-output) - (message "%s" aidermacs--current-output) - (kill-new aidermacs--current-output) - aidermacs--current-output)) - -(defun aidermacs--ensure-current-file-tracked () - "Ensure current file is tracked in the aidermacs session." - (when buffer-file-name - (let* ((session-buffer (get-buffer (aidermacs-get-buffer-name))) - (filename buffer-file-name) - (relative-path (file-relative-name filename (aidermacs-project-root)))) - (when session-buffer - (with-current-buffer session-buffer - (unless (member relative-path aidermacs--tracked-files) - (push relative-path aidermacs--tracked-files) - (let ((command (aidermacs--prepare-file-paths-for-command "/add" (list relative-path)))) - (aidermacs--send-command-backend session-buffer command nil)))))))) - -(defvar aidermacs--read-string-history nil - "History list for aidermacs read string inputs.") -(if (bound-and-true-p savehist-loaded) - (add-to-list 'savehist-additional-variables 'aidermacs--read-string-history) - (add-hook 'savehist-mode-hook - (lambda () - (add-to-list 'savehist-additional-variables 'aidermacs--read-string-history)))) - (defun aidermacs--form-prompt (command &optional prompt-prefix guide ignore-context) "Get command based on context with COMMAND and PROMPT-PREFIX. COMMAND is the text to prepend. PROMPT-PREFIX is the text to add after COMMAND.