branch: elpa/aidermacs
commit 5f3c6005c07bf4bff01314f0968253c5e90f069b
Author: Mingde (Matthew) Zeng <matthew...@posteo.net>
Commit: Mingde (Matthew) Zeng <matthew...@posteo.net>

    Implement colored output in source code blocks
    
    Signed-off-by: Mingde (Matthew) Zeng <matthew...@posteo.net>
---
 aider.el | 242 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
 1 file changed, 211 insertions(+), 31 deletions(-)

diff --git a/aider.el b/aider.el
index 41ee8da037..cad272c230 100644
--- a/aider.el
+++ b/aider.el
@@ -16,6 +16,7 @@
 (require 'transient)
 (require 'magit)
 (require 'which-func)
+(require 'ansi-color)
 
 (defgroup aider nil
   "Customization group for the Aider package."
@@ -50,6 +51,16 @@ Also based on aider LLM benchmark: 
https://aider.chat/docs/leaderboards/";
   :type '(repeat string)
   :group 'aider)
 
+(defcustom aider-language-name-map '(("elisp" . "emacs-lisp")
+                                     ("bash" . "sh")
+                                     ("objective-c" . "objc")
+                                     ("objectivec" . "objc")
+                                     ("cpp" . "c++"))
+  "Map external language names to Emacs names."
+  :type '(alist :key-type (string :tag "Language Name/Alias")
+                :value-type (string :tag "Mode Name (without -mode)"))
+  :group 'aider)
+
 (defcustom aider-prompt-file-name ".aider.prompt.org"
   "File name that will automatically enable aider-minor-mode when opened.
 This is the file name without path."
@@ -75,8 +86,19 @@ This is the file name without path."
   "Face for commands sent to aider buffer."
   :group 'aider)
 
-(defvar aider-font-lock-keywords '(("^\x2500+\n?" 0 '(face 
aider-command-separator) t)
-                                   ("^\x2500+" 0 '(face nil display (space 
:width 2))))
+(defface aider-search-replace-block
+  '((t :inherit 'diff-refine-added :bold t))
+  "Face for search/replace block content."
+  :group 'aider)
+
+(defvar aider-font-lock-keywords
+  '(("^\x2500+\n?" 0 '(face aider-command-separator) t)
+    ("^\x2500+" 0 '(face nil display (space :width 2)))
+    ("^\\([0-9]+\\). " 0 font-lock-constant-face)
+    ("^>>>>>>> REPLACE" 0 'aider-search-replace-block t)
+    ("^<<<<<<< SEARCH" 0 'aider-search-replace-block t)
+    ("^\\(```\\)\\([^[:space:]]*\\)" (1 'shadow t) (2 font-lock-builtin-face 
t))
+    ("^=======$" 0 'aider-search-replace-block t))
   "Font lock keywords for aider buffer.")
 
 ;;;###autoload
@@ -199,26 +221,6 @@ If not in a git repository and no buffer file exists, an 
error is raised."
      (t
       (error "Not in a git repository and current buffer is not associated 
with a file")))))
 
-(defun aider--inherit-source-highlighting (source-buffer)
-  "Inherit syntax highlighting settings from SOURCE-BUFFER."
-  (with-current-buffer source-buffer
-    (let ((source-keywords font-lock-keywords)
-          (source-keywords-only font-lock-keywords-only)
-          (source-keywords-case-fold-search 
font-lock-keywords-case-fold-search)
-          ;; (source-syntax-table (syntax-table))
-          (source-defaults font-lock-defaults))
-      (with-current-buffer (aider-buffer-name)
-        ;; (set-syntax-table source-syntax-table)
-        (setq font-lock-defaults
-              (if source-defaults
-                  source-defaults
-                `((,source-keywords)
-                  nil
-                  ,source-keywords-case-fold-search)))
-        (setq font-lock-keywords source-keywords
-              font-lock-keywords-only source-keywords-only
-              font-lock-keywords-case-fold-search 
source-keywords-case-fold-search)))))
-
 ;;;###autoload
 (defun aider-run-aider (&optional edit-args)
   "Create a comint-based buffer and run \"aider\" for interactive conversation.
@@ -236,17 +238,194 @@ With the universal argument, prompt to edit aider-args 
before running."
       (apply 'make-comint-in-buffer "aider" buffer-name aider-program nil 
current-args)
       (with-current-buffer buffer-name
         (comint-mode)
-        (font-lock-add-keywords nil aider-font-lock-keywords t)
-        ;; Only inherit syntax highlighting when source buffer is in prog-mode
-        (when (with-current-buffer source-buffer
-                (derived-mode-p 'prog-mode))
-          (aider--inherit-source-highlighting source-buffer)
-          (font-lock-mode 1)
-          (font-lock-ensure)
-          (message "Aider buffer syntax highlighting inherited from %s"
-                   (with-current-buffer source-buffer major-mode)))))
+        (setq-local comint-input-sender 'aider-input-sender)
+        (setq aider--font-lock-buffer
+              (get-buffer-create (concat " *aider-fontify" buffer-name)))
+        (add-hook 'kill-buffer-hook #'aider-kill-buffer nil t)
+        (add-hook 'comint-output-filter-functions #'aider-fontify-blocks 100 t)
+        (font-lock-add-keywords nil aider-font-lock-keywords t)))
     (aider-switch-to-buffer)))
 
+(defun aider-kill-buffer ()
+  "Clean-up fontify buffer."
+  (when (bufferp aider--font-lock-buffer)
+    (kill-buffer aider--font-lock-buffer)))
+
+(defun aider-input-sender (proc string)
+  "Reset font-lock state before executing a command."
+  (aider-reset-font-lock-state)
+  (comint-simple-send proc string))
+
+;; Buffer-local variables for block processing state
+(defvar-local aider--block-end-marker nil
+  "The end marker for the current block being processed.")
+
+(defvar-local aider--block-start nil
+  "The starting position of the current block being processed.")
+
+(defvar-local aider--block-end nil
+  "The end position of the current block being processed.")
+
+(defvar-local aider--last-output-start nil
+  "an alternative to `comint-last-output-start' used in aider.")
+
+(defvar-local aider--block-mode nil
+  "The major mode for the current block being processed.")
+
+(defvar-local aider--font-lock-buffer nil
+  "Temporary buffer for fontification.")
+
+(defconst aider-search-marker "<<<<<<< SEARCH")
+(defconst aider-diff-marker "=======")
+(defconst aider-replace-marker ">>>>>>> REPLACE")
+(defconst aider-fence-marker "```")
+(defvar aider-block-re
+  (format "^\\(?:\\(?1:%s\\|%s\\)\\|\\(?1:%s\\).+\\)$" aider-search-marker 
aider-diff-marker aider-fence-marker))
+
+(defun aider-reset-font-lock-state ()
+  "Reset font lock state to default for processing another a new src block."
+  (unless (equal aider--block-end-marker aider-diff-marker)
+    ;; if we are processing the other half of a SEARCH/REPLACE block, we need 
to
+    ;; keep the mode
+    (setq aider--block-mode nil))
+  (setq aider--block-end-marker nil
+        aider--last-output-start nil
+        aider--block-start nil
+        aider--block-end nil))
+
+(defun aider-fontify-blocks (_output)
+  "fontify search/replace blocks in comint output."
+  (save-excursion
+    (goto-char (or aider--last-output-start
+                   comint-last-output-start))
+    (beginning-of-line)
+
+    ;; Continue processing existing block if we're in one
+    (when aider--block-start
+      (aider--fontify-block))
+
+    (setq aider--last-output-start nil)
+    ;; Look for new blocks if we're not in one
+    (while (and (null aider--block-start)
+                (null aider--last-output-start)
+                (re-search-forward aider-block-re nil t))
+
+      ;; If it is code fence marker, we need to check if there is a SEARCH 
marker
+      ;; directly after it
+      (when (equal (match-string 1) aider-fence-marker)
+        (let* ((next-line (min (point-max) (1+ (line-end-position))))
+               (line-text (buffer-substring
+                           next-line
+                           (min (point-max) (+ next-line (length 
aider-search-marker))))))
+          (cond ((equal line-text aider-search-marker)
+                 ;; Next line is a SEARCH marker. use that instead of the 
fence marker
+                 (re-search-forward (format "^\\(%s\\)" aider-search-marker) 
nil t))
+                ((string-prefix-p line-text aider-search-marker)
+                 ;; Next line *might* be a SEARCH marker. Don't process more of
+                 ;; the buffer until we know for sure
+                 (setq aider--last-output-start comint-last-output-start)))))
+
+      (unless aider--last-output-start
+        ;; Set up new block state
+        (setq aider--block-end-marker
+              (pcase (match-string 1)
+                ((pred (equal aider-search-marker)) aider-diff-marker)
+                ((pred (equal aider-diff-marker)) aider-replace-marker)
+                ((pred (equal aider-fence-marker)) aider-fence-marker))
+              aider--block-start (line-end-position)
+              aider--block-end (line-end-position)
+              aider--block-mode (aider--guess-major-mode))
+
+        ;; Set the major-mode of the font lock buffer
+        (let ((mode aider--block-mode))
+          (with-current-buffer aider--font-lock-buffer
+            (erase-buffer)
+            (unless (eq mode major-mode)
+              (condition-case e
+                  (let ((inhibit-message t))
+                    (funcall mode))
+                (error (message "aider: failed to init major-mode `%s' for 
font-locking: %s" mode e))))))
+
+        ;; Process initial content
+        (aider--fontify-block)))))
+
+(defun aider--fontify-block ()
+  "Fontify as much of the current source block as possible."
+  (let* ((last-bol (save-excursion
+                     (goto-char (point-max))
+                     (line-beginning-position)))
+         (last-output-start aider--block-end)
+         end-of-block-p)
+
+    (setq aider--block-end
+          (cond ((re-search-forward (concat "^" aider--block-end-marker "$") 
nil t)
+                 ;; Found the end of the block
+                 (setq end-of-block-p t)
+                 (line-beginning-position))
+                ((string-prefix-p (buffer-substring last-bol (point-max)) 
aider--block-end-marker)
+                 ;; The end of the text *might* be the end marker. back up to
+                 ;; make sure we don't process it until we know for sure
+                 last-bol)
+                ;; We can process till the end of the text
+                (t (point-max))))
+
+  ;; Append new content to temp buffer and fontify
+  (let ((new-content (buffer-substring-no-properties
+                      last-output-start
+                      aider--block-end))
+        (pos aider--block-start)
+        (font-pos 0)
+        fontified)
+
+    ;; Insert the new text and get the fontified result
+    (with-current-buffer aider--font-lock-buffer
+      (goto-char (point-max))
+      (insert new-content)
+      (with-demoted-errors "aider block font lock error: %s"
+        (let ((inhibit-message t))
+          (font-lock-ensure)))
+      (setq fontified (buffer-string)))
+
+    ;; Apply the faces to the buffer
+    (remove-overlays aider--block-start aider--block-end)
+    (while (< pos aider--block-end)
+      (let* ((next-font-pos (or (next-property-change font-pos fontified) 
(length fontified)))
+             (next-pos (+ aider--block-start next-font-pos))
+             (face (get-text-property font-pos 'face fontified)))
+        (ansi-color-apply-overlay-face pos next-pos face)
+        (setq pos next-pos
+              font-pos next-font-pos))))
+
+  ;; If we found the end marker, finalize the block
+  (when end-of-block-p
+    (when (equal aider--block-end-marker aider-diff-marker)
+      ;; we will need to process the other half of the SEARCH/REPLACE block.
+      ;; Backup so it will get matched
+      (beginning-of-line))
+    (aider-reset-font-lock-state))))
+
+(defun aider--guess-major-mode ()
+  "Extract the major mode from fence markers or filename."
+  (save-excursion
+    (beginning-of-line)
+    (or
+     ;; check if the block has a language id
+     (when (let ((re "^```\\([^[:space:]]+\\)"))
+             (or (looking-at re)
+                 (save-excursion
+                   (forward-line -1)
+                   ;; check the previous line since this might be a SEARCH 
block
+                   (looking-at re))))
+       (let* ((lang (downcase (match-string 1)))
+              (mode (map-elt aider-language-name-map lang lang)))
+         (intern-soft (concat mode "-mode"))))
+     ;; check the file extension in auto-mode-alist
+     (when (re-search-backward "^\\([^[:space:]]+\\)" (line-beginning-position 
-3) t)
+       (let ((file (match-string 1)))
+         (cdr (cl-assoc-if (lambda (re) (string-match re file)) 
auto-mode-alist))))
+     aider--block-mode
+     'fundamental-mode)))
+
 ;; Function to switch to the Aider buffer
 ;;;###autoload
 (defun aider-switch-to-buffer ()
@@ -318,6 +497,7 @@ COMMAND should be a string representing the command to 
send."
         ;; Check if the corresponding aider buffer has an active process
         (if (and aider-process (comint-check-proc aider-buffer))
             (progn
+              (aider-reset-font-lock-state)
               ;; Send the command to the aider process
               (aider--comint-send-string-syntax-highlight aider-buffer (concat 
command "\n"))
               ;; Provide feedback to the user

Reply via email to