From 86304a13af6cc2b3be562f5c1e4da61362aacf68 Mon Sep 17 00:00:00 2001 From: CsBigDataHub Date: Thu, 11 Sep 2025 22:27:55 -0400 Subject: [PATCH 01/10] fix(ediff): restore chat side-window after ediff session --- eca-chat.el | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/eca-chat.el b/eca-chat.el index b3a1b03..da04560 100644 --- a/eca-chat.el +++ b/eca-chat.el @@ -974,7 +974,8 @@ configuration is restored when Ediff quits via the cleanup hook." (buffer-list))) (session-ediff-buffers nil) (cleanup-fn nil) - (after-setup-fn nil)) + (after-setup-fn nil) + (chat-buf (current-buffer))) ;; Fill temporary buffers (with-current-buffer buf-orig (let ((inhibit-read-only t)) @@ -1013,6 +1014,12 @@ configuration is restored when Ediff quits via the cleanup hook." ;; Ensure focus returns to the original window if still live (when (window-live-p orig-selected) (select-window orig-selected)) + ;; If the chat buffer exists and is not currently shown in a + ;; side window, re-open it using the side-window display helper. + (when (and (buffer-live-p chat-buf) + (or (not (get-buffer-window chat-buf t)) + (not (window-parameter (get-buffer-window chat-buf t) 'window-side)))) + (ignore-errors (eca-chat--display-buffer chat-buf))) (when (buffer-live-p buf-orig) (kill-buffer buf-orig)) (when (buffer-live-p buf-new) (kill-buffer buf-new)) (dolist (b session-ediff-buffers) @@ -1052,6 +1059,11 @@ configuration is restored when Ediff quits via the cleanup hook." (remove-hook 'ediff-after-setup-windows-hook after-setup-fn) (when (window-configuration-p cwc) (set-window-configuration cwc)) + ;; Try to ensure chat gets its side-window back on error too + (when (and (buffer-live-p chat-buf) + (or (not (get-buffer-window chat-buf t)) + (not (window-parameter (get-buffer-window chat-buf t) 'window-side)))) + (ignore-errors (eca-chat--display-buffer chat-buf))) (when (buffer-live-p buf-orig) (kill-buffer buf-orig)) (when (buffer-live-p buf-new) (kill-buffer buf-new)) (message "eca-chat: error starting ediff: %s" err))))) From 2ea0ffa962940c451592ce0ec6957ea8388f42bb Mon Sep 17 00:00:00 2001 From: CsBigDataHub Date: Thu, 11 Sep 2025 22:51:50 -0400 Subject: [PATCH 02/10] enhance ediff function --- eca-chat.el | 217 ++++++++++++++++++++++++++-------------------------- 1 file changed, 110 insertions(+), 107 deletions(-) diff --git a/eca-chat.el b/eca-chat.el index da04560..dc0187f 100644 --- a/eca-chat.el +++ b/eca-chat.el @@ -955,118 +955,121 @@ Adds \\='q\\=' to quit." (pop-to-buffer (current-buffer)))) (defun eca-chat--show-diff-ediff (path diff) - "Show DIFF for file at PATH using Ediff side-by-side in windows. + "Show DIFF for file at PATH using Ediff side-by-side in windows. Uses window configuration management instead of creating frames. - If the current window is a side window, temporarily clear side-window protections so Ediff can split windows freely. The original window configuration is restored when Ediff quits via the cleanup hook." - (let* ((parsed (eca-chat--parse-unified-diff diff)) - (orig (plist-get parsed :original)) - (new (plist-get parsed :new)) - (buf-orig (generate-new-buffer (format "*eca-diff-orig:%s*" path))) - (buf-new (generate-new-buffer (format "*eca-diff-new:%s*" path))) - (cwc (current-window-configuration)) - (orig-selected (selected-window)) - (ediff-buffers-before - (seq-filter (lambda (b) - (string-match-p "\\*\\(ediff-\\|Ediff Control\\)" (buffer-name b))) - (buffer-list))) - (session-ediff-buffers nil) - (cleanup-fn nil) - (after-setup-fn nil) - (chat-buf (current-buffer))) - ;; Fill temporary buffers - (with-current-buffer buf-orig - (let ((inhibit-read-only t)) - (erase-buffer) - (insert orig) - (set-buffer-modified-p nil))) - (with-current-buffer buf-new - (let ((inhibit-read-only t)) - (erase-buffer) - (insert new) - (set-buffer-modified-p nil))) - - ;; Temporarily relax side-window protections in the current frame so - ;; Ediff can split windows. The original configuration will be - ;; restored by `cleanup-fn' (which calls `set-window-configuration'). - (let ((frame (selected-frame))) - (dolist (w (seq-filter (lambda (w) - (and (eq (window-frame w) frame) - (or (window-parameter w 'no-delete-other-windows) - (window-parameter w 'window-side)))) - (window-list))) - (when (window-live-p w) - (set-window-parameter w 'no-delete-other-windows nil) - (set-window-parameter w 'window-side nil)))) - - ;; Ensure Ediff has a single full window to manage (it will split it). - (unless (one-window-p t) - (delete-other-windows)) - - ;; Cleanup: restore windows and kill temp/session buffers - (setq cleanup-fn - (lambda () - ;; Restore window configuration saved at the beginning - (when (window-configuration-p cwc) - (set-window-configuration cwc)) - ;; Ensure focus returns to the original window if still live - (when (window-live-p orig-selected) - (select-window orig-selected)) - ;; If the chat buffer exists and is not currently shown in a - ;; side window, re-open it using the side-window display helper. - (when (and (buffer-live-p chat-buf) - (or (not (get-buffer-window chat-buf t)) - (not (window-parameter (get-buffer-window chat-buf t) 'window-side)))) - (ignore-errors (eca-chat--display-buffer chat-buf))) - (when (buffer-live-p buf-orig) (kill-buffer buf-orig)) - (when (buffer-live-p buf-new) (kill-buffer buf-new)) - (dolist (b session-ediff-buffers) - (when (and b (buffer-live-p b)) - (kill-buffer b))) - (remove-hook 'ediff-quit-hook cleanup-fn))) - - ;; After-setup hook: capture ediff buffers and move to first diff - (setq after-setup-fn - (lambda () - (let ((ediff-buffers-after - (seq-filter (lambda (b) - (string-match-p "\\*\\(ediff-\\|Ediff Control\\)" (buffer-name b))) - (buffer-list)))) - (setq session-ediff-buffers + (let* ((parsed (eca-chat--parse-unified-diff diff)) + (orig (plist-get parsed :original)) + (new (plist-get parsed :new)) + (buf-orig (generate-new-buffer (format "*eca-diff-orig:%s*" +path))) + (buf-new (generate-new-buffer (format "*eca-diff-new:%s*" +path))) + (chat-buf (current-buffer)) + (cwc (current-window-configuration)) + (orig-selected (selected-window)) + (ediff-buffers-before + (seq-filter (lambda (b) + (string-match-p "\\*\\(ediff-\\|Ediff Control\\)" +(buffer-name b))) + (buffer-list))) + (session-ediff-buffers nil) + cleanup-fn + after-setup-fn) + ;; Fill temporary buffers + (with-current-buffer buf-orig + (let ((inhibit-read-only t)) + (erase-buffer) + (insert orig) + (set-buffer-modified-p nil))) + (with-current-buffer buf-new + (let ((inhibit-read-only t)) + (erase-buffer) + (insert new) + (set-buffer-modified-p nil))) + ;; Temporarily relax side-window protections so Ediff can split + (let ((frame (selected-frame))) + (dolist (w (seq-filter (lambda (w) + (and (eq (window-frame w) frame) + (or (window-parameter w +'no-delete-other-windows) + (window-parameter w +'window-side)))) + (window-list))) + (when (window-live-p w) + (set-window-parameter w 'no-delete-other-windows nil) + (set-window-parameter w 'window-side nil)))) + ;; Ensure Ediff has a single full window to manage + (unless (one-window-p t) + (delete-other-windows)) + ;; Cleanup: restore windows and kill temp/session buffers + (setq cleanup-fn + (lambda () + ;; Restore window configuration + (when (window-configuration-p cwc) + (set-window-configuration cwc)) + ;; Return focus to original window + (when (window-live-p orig-selected) + (select-window orig-selected)) + ;; Re-display chat buffer in side window if needed + (when (and (buffer-live-p chat-buf) + (or (not (get-buffer-window chat-buf t)) + (not (window-parameter (get-buffer-window +chat-buf t) 'window-side)))) + (ignore-errors (eca-chat--display-buffer chat-buf))) + ;; Kill temp buffers + (when (buffer-live-p buf-orig) (kill-buffer buf-orig)) + (when (buffer-live-p buf-new) (kill-buffer buf-new)) + ;; Kill any additional Ediff-generated buffers + (dolist (b session-ediff-buffers) + (when (and b (buffer-live-p b)) + (kill-buffer b))) + (remove-hook 'ediff-quit-hook cleanup-fn))) + ;; After-setup hook: capture ediff buffers and move to first diff + (setq after-setup-fn + (lambda () + (let ((ediff-buffers-after (seq-filter (lambda (b) - (and (not (member b ediff-buffers-before)) - (not (string-match-p "\*Ediff Registry\*" (buffer-name b))))) - ediff-buffers-after))) - (condition-case _err - (progn - (setq ediff-current-difference -1) - (ediff-next-difference)) - (error nil)) - (remove-hook 'ediff-after-setup-windows-hook after-setup-fn))) - - (add-hook 'ediff-quit-hook cleanup-fn) - (add-hook 'ediff-after-setup-windows-hook after-setup-fn) - - ;; Start Ediff in the prepared window environment. Ediff will manage - ;; splits from here on. On error, restore original windows/config. - (condition-case err - (ediff-buffers buf-orig buf-new) - (error - ;; On error remove hooks and kill temps, and restore windows - (remove-hook 'ediff-quit-hook cleanup-fn) - (remove-hook 'ediff-after-setup-windows-hook after-setup-fn) - (when (window-configuration-p cwc) - (set-window-configuration cwc)) - ;; Try to ensure chat gets its side-window back on error too - (when (and (buffer-live-p chat-buf) - (or (not (get-buffer-window chat-buf t)) - (not (window-parameter (get-buffer-window chat-buf t) 'window-side)))) - (ignore-errors (eca-chat--display-buffer chat-buf))) - (when (buffer-live-p buf-orig) (kill-buffer buf-orig)) - (when (buffer-live-p buf-new) (kill-buffer buf-new)) - (message "eca-chat: error starting ediff: %s" err))))) + (string-match-p "\\*\\(ediff-\\|Ediff +Control\\)" (buffer-name b))) + (buffer-list)))) + (setq session-ediff-buffers + (seq-filter (lambda (b) + (and (not (member b +ediff-buffers-before)) + (not (string-match-p "*Ediff +Registry*" (buffer-name b))))) + ediff-buffers-after))) + (condition-case _err + (progn + (setq ediff-current-difference -1) + (ediff-next-difference)) + (error nil)) + (remove-hook 'ediff-after-setup-windows-hook after-setup-fn))) + ;; Install hooks + (add-hook 'ediff-quit-hook cleanup-fn) + (add-hook 'ediff-after-setup-windows-hook after-setup-fn) + ;; Start Ediff with error handling + (condition-case err + (ediff-buffers buf-orig buf-new) + (error + ;; On error, remove hooks and restore windows + (remove-hook 'ediff-quit-hook cleanup-fn) + (remove-hook 'ediff-after-setup-windows-hook after-setup-fn) + (when (window-configuration-p cwc) + (set-window-configuration cwc)) + ;; Re-display chat buffer on error + (when (and (buffer-live-p chat-buf) + (or (not (get-buffer-window chat-buf t)) + (not (window-parameter (get-buffer-window chat-buf +t) 'window-side)))) + (ignore-errors (eca-chat--display-buffer chat-buf))) + ;; Kill temp buffers + (when (buffer-live-p buf-orig) (kill-buffer buf-orig)) + (when (buffer-live-p buf-new) (kill-buffer buf-new)) + (message "eca-chat: error starting ediff: %s" err))))) (defun eca-chat--show-diff-smerge (path diff) "Show DIFF for file at PATH using Smerge in a dedicated window. From 3dddc5b5c4b2e64061133e01389146e492e0977b Mon Sep 17 00:00:00 2001 From: CsBigDataHub Date: Thu, 11 Sep 2025 22:53:58 -0400 Subject: [PATCH 03/10] fix formatting --- eca-chat.el | 218 ++++++++++++++++++++++++++-------------------------- 1 file changed, 109 insertions(+), 109 deletions(-) diff --git a/eca-chat.el b/eca-chat.el index dc0187f..b24db4d 100644 --- a/eca-chat.el +++ b/eca-chat.el @@ -955,121 +955,121 @@ Adds \\='q\\=' to quit." (pop-to-buffer (current-buffer)))) (defun eca-chat--show-diff-ediff (path diff) - "Show DIFF for file at PATH using Ediff side-by-side in windows. + "Show DIFF for file at PATH using Ediff side-by-side in windows. Uses window configuration management instead of creating frames. If the current window is a side window, temporarily clear side-window protections so Ediff can split windows freely. The original window configuration is restored when Ediff quits via the cleanup hook." - (let* ((parsed (eca-chat--parse-unified-diff diff)) - (orig (plist-get parsed :original)) - (new (plist-get parsed :new)) - (buf-orig (generate-new-buffer (format "*eca-diff-orig:%s*" -path))) - (buf-new (generate-new-buffer (format "*eca-diff-new:%s*" -path))) - (chat-buf (current-buffer)) - (cwc (current-window-configuration)) - (orig-selected (selected-window)) - (ediff-buffers-before - (seq-filter (lambda (b) - (string-match-p "\\*\\(ediff-\\|Ediff Control\\)" -(buffer-name b))) - (buffer-list))) - (session-ediff-buffers nil) - cleanup-fn - after-setup-fn) - ;; Fill temporary buffers - (with-current-buffer buf-orig - (let ((inhibit-read-only t)) - (erase-buffer) - (insert orig) - (set-buffer-modified-p nil))) - (with-current-buffer buf-new - (let ((inhibit-read-only t)) - (erase-buffer) - (insert new) - (set-buffer-modified-p nil))) - ;; Temporarily relax side-window protections so Ediff can split - (let ((frame (selected-frame))) - (dolist (w (seq-filter (lambda (w) - (and (eq (window-frame w) frame) - (or (window-parameter w -'no-delete-other-windows) - (window-parameter w -'window-side)))) - (window-list))) - (when (window-live-p w) - (set-window-parameter w 'no-delete-other-windows nil) - (set-window-parameter w 'window-side nil)))) - ;; Ensure Ediff has a single full window to manage - (unless (one-window-p t) - (delete-other-windows)) - ;; Cleanup: restore windows and kill temp/session buffers - (setq cleanup-fn - (lambda () - ;; Restore window configuration - (when (window-configuration-p cwc) - (set-window-configuration cwc)) - ;; Return focus to original window - (when (window-live-p orig-selected) - (select-window orig-selected)) - ;; Re-display chat buffer in side window if needed - (when (and (buffer-live-p chat-buf) - (or (not (get-buffer-window chat-buf t)) - (not (window-parameter (get-buffer-window -chat-buf t) 'window-side)))) - (ignore-errors (eca-chat--display-buffer chat-buf))) - ;; Kill temp buffers - (when (buffer-live-p buf-orig) (kill-buffer buf-orig)) - (when (buffer-live-p buf-new) (kill-buffer buf-new)) - ;; Kill any additional Ediff-generated buffers - (dolist (b session-ediff-buffers) - (when (and b (buffer-live-p b)) - (kill-buffer b))) - (remove-hook 'ediff-quit-hook cleanup-fn))) - ;; After-setup hook: capture ediff buffers and move to first diff - (setq after-setup-fn - (lambda () - (let ((ediff-buffers-after - (seq-filter (lambda (b) - (string-match-p "\\*\\(ediff-\\|Ediff + (let* ((parsed (eca-chat--parse-unified-diff diff)) + (orig (plist-get parsed :original)) + (new (plist-get parsed :new)) + (buf-orig (generate-new-buffer (format "*eca-diff-orig:%s*" + path))) + (buf-new (generate-new-buffer (format "*eca-diff-new:%s*" + path))) + (chat-buf (current-buffer)) + (cwc (current-window-configuration)) + (orig-selected (selected-window)) + (ediff-buffers-before + (seq-filter (lambda (b) + (string-match-p "\\*\\(ediff-\\|Ediff Control\\)" + (buffer-name b))) + (buffer-list))) + (session-ediff-buffers nil) + cleanup-fn + after-setup-fn) + ;; Fill temporary buffers + (with-current-buffer buf-orig + (let ((inhibit-read-only t)) + (erase-buffer) + (insert orig) + (set-buffer-modified-p nil))) + (with-current-buffer buf-new + (let ((inhibit-read-only t)) + (erase-buffer) + (insert new) + (set-buffer-modified-p nil))) + ;; Temporarily relax side-window protections so Ediff can split + (let ((frame (selected-frame))) + (dolist (w (seq-filter (lambda (w) + (and (eq (window-frame w) frame) + (or (window-parameter w + 'no-delete-other-windows) + (window-parameter w + 'window-side)))) + (window-list))) + (when (window-live-p w) + (set-window-parameter w 'no-delete-other-windows nil) + (set-window-parameter w 'window-side nil)))) + ;; Ensure Ediff has a single full window to manage + (unless (one-window-p t) + (delete-other-windows)) + ;; Cleanup: restore windows and kill temp/session buffers + (setq cleanup-fn + (lambda () + ;; Restore window configuration + (when (window-configuration-p cwc) + (set-window-configuration cwc)) + ;; Return focus to original window + (when (window-live-p orig-selected) + (select-window orig-selected)) + ;; Re-display chat buffer in side window if needed + (when (and (buffer-live-p chat-buf) + (or (not (get-buffer-window chat-buf t)) + (not (window-parameter (get-buffer-window + chat-buf t) 'window-side)))) + (ignore-errors (eca-chat--display-buffer chat-buf))) + ;; Kill temp buffers + (when (buffer-live-p buf-orig) (kill-buffer buf-orig)) + (when (buffer-live-p buf-new) (kill-buffer buf-new)) + ;; Kill any additional Ediff-generated buffers + (dolist (b session-ediff-buffers) + (when (and b (buffer-live-p b)) + (kill-buffer b))) + (remove-hook 'ediff-quit-hook cleanup-fn))) + ;; After-setup hook: capture ediff buffers and move to first diff + (setq after-setup-fn + (lambda () + (let ((ediff-buffers-after + (seq-filter (lambda (b) + (string-match-p "\\*\\(ediff-\\|Ediff Control\\)" (buffer-name b))) - (buffer-list)))) - (setq session-ediff-buffers - (seq-filter (lambda (b) - (and (not (member b -ediff-buffers-before)) - (not (string-match-p "*Ediff + (buffer-list)))) + (setq session-ediff-buffers + (seq-filter (lambda (b) + (and (not (member b + ediff-buffers-before)) + (not (string-match-p "*Ediff Registry*" (buffer-name b))))) - ediff-buffers-after))) - (condition-case _err - (progn - (setq ediff-current-difference -1) - (ediff-next-difference)) - (error nil)) - (remove-hook 'ediff-after-setup-windows-hook after-setup-fn))) - ;; Install hooks - (add-hook 'ediff-quit-hook cleanup-fn) - (add-hook 'ediff-after-setup-windows-hook after-setup-fn) - ;; Start Ediff with error handling - (condition-case err - (ediff-buffers buf-orig buf-new) - (error - ;; On error, remove hooks and restore windows - (remove-hook 'ediff-quit-hook cleanup-fn) - (remove-hook 'ediff-after-setup-windows-hook after-setup-fn) - (when (window-configuration-p cwc) - (set-window-configuration cwc)) - ;; Re-display chat buffer on error - (when (and (buffer-live-p chat-buf) - (or (not (get-buffer-window chat-buf t)) - (not (window-parameter (get-buffer-window chat-buf -t) 'window-side)))) - (ignore-errors (eca-chat--display-buffer chat-buf))) - ;; Kill temp buffers - (when (buffer-live-p buf-orig) (kill-buffer buf-orig)) - (when (buffer-live-p buf-new) (kill-buffer buf-new)) - (message "eca-chat: error starting ediff: %s" err))))) + ediff-buffers-after))) + (condition-case _err + (progn + (setq ediff-current-difference -1) + (ediff-next-difference)) + (error nil)) + (remove-hook 'ediff-after-setup-windows-hook after-setup-fn))) + ;; Install hooks + (add-hook 'ediff-quit-hook cleanup-fn) + (add-hook 'ediff-after-setup-windows-hook after-setup-fn) + ;; Start Ediff with error handling + (condition-case err + (ediff-buffers buf-orig buf-new) + (error + ;; On error, remove hooks and restore windows + (remove-hook 'ediff-quit-hook cleanup-fn) + (remove-hook 'ediff-after-setup-windows-hook after-setup-fn) + (when (window-configuration-p cwc) + (set-window-configuration cwc)) + ;; Re-display chat buffer on error + (when (and (buffer-live-p chat-buf) + (or (not (get-buffer-window chat-buf t)) + (not (window-parameter (get-buffer-window chat-buf + t) 'window-side)))) + (ignore-errors (eca-chat--display-buffer chat-buf))) + ;; Kill temp buffers + (when (buffer-live-p buf-orig) (kill-buffer buf-orig)) + (when (buffer-live-p buf-new) (kill-buffer buf-new)) + (message "eca-chat: error starting ediff: %s" err))))) (defun eca-chat--show-diff-smerge (path diff) "Show DIFF for file at PATH using Smerge in a dedicated window. From 6f4abbe028d7f113551730ad01c622bf5dd2b36c Mon Sep 17 00:00:00 2001 From: CsBigDataHub Date: Fri, 12 Sep 2025 09:28:12 -0400 Subject: [PATCH 04/10] feat(ediff): add Doom Emacs compatibility for window and popup handling enhance ediff integration to detect and adapt to Doom Emacs popup contexts prevent eca-diff and Ediff control buffers from being managed by Doom popups avoid saving/restoring window configuration when in Doom popup to prevent conflicts add doom-aware cleanup and error recovery, using Doom's popup restoration when needed use longer delay for window restoration under Doom for reliability ensure chat buffer is re-displayed correctly after ediff session or error refactor buffer and window management logic for improved compatibility and robustness --- eca-chat.el | 146 +++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 110 insertions(+), 36 deletions(-) diff --git a/eca-chat.el b/eca-chat.el index b24db4d..d70d4de 100644 --- a/eca-chat.el +++ b/eca-chat.el @@ -959,7 +959,8 @@ Adds \\='q\\=' to quit." Uses window configuration management instead of creating frames. If the current window is a side window, temporarily clear side-window protections so Ediff can split windows freely. The original window -configuration is restored when Ediff quits via the cleanup hook." +configuration is restored when Ediff quits via the cleanup hook. +Enhanced with Doom Emacs compatibility." (let* ((parsed (eca-chat--parse-unified-diff diff)) (orig (plist-get parsed :original)) (new (plist-get parsed :new)) @@ -968,8 +969,15 @@ configuration is restored when Ediff quits via the cleanup hook." (buf-new (generate-new-buffer (format "*eca-diff-new:%s*" path))) (chat-buf (current-buffer)) - (cwc (current-window-configuration)) + ;; Doom-specific: Check if we're in a popup context + (doom-popup-p (and (boundp '+popup-buffer-mode) + +popup-buffer-mode)) + (in-doom-p (featurep 'doom)) + ;; Don't save window config if in Doom popup - causes conflicts + (cwc (unless doom-popup-p + (current-window-configuration))) (orig-selected (selected-window)) + (orig-buffer (current-buffer)) (ediff-buffers-before (seq-filter (lambda (b) (string-match-p "\\*\\(ediff-\\|Ediff Control\\)" @@ -978,6 +986,15 @@ configuration is restored when Ediff quits via the cleanup hook." (session-ediff-buffers nil) cleanup-fn after-setup-fn) + + ;; Doom-specific: Set popup rules to prevent interference + (when in-doom-p + (when (fboundp 'set-popup-rule!) + ;; Prevent ECA diff buffers from being managed by Doom's popup + system + (set-popup-rule! "^\\*eca-diff-" :ignore t) + (set-popup-rule! "^\\*Ediff Control Panel" :ignore t))) + ;; Fill temporary buffers (with-current-buffer buf-orig (let ((inhibit-read-only t)) @@ -989,58 +1006,105 @@ configuration is restored when Ediff quits via the cleanup hook." (erase-buffer) (insert new) (set-buffer-modified-p nil))) - ;; Temporarily relax side-window protections so Ediff can split - (let ((frame (selected-frame))) - (dolist (w (seq-filter (lambda (w) - (and (eq (window-frame w) frame) - (or (window-parameter w - 'no-delete-other-windows) - (window-parameter w - 'window-side)))) - (window-list))) - (when (window-live-p w) - (set-window-parameter w 'no-delete-other-windows nil) - (set-window-parameter w 'window-side nil)))) - ;; Ensure Ediff has a single full window to manage - (unless (one-window-p t) - (delete-other-windows)) - ;; Cleanup: restore windows and kill temp/session buffers + + ;; Only manipulate windows if not in Doom popup context + (unless doom-popup-p + ;; Temporarily relax side-window protections so Ediff can split + (let ((frame (selected-frame))) + (dolist (w (seq-filter (lambda (w) + (and (eq (window-frame w) frame) + (or (window-parameter w + + 'no-delete-other-windows) + (window-parameter w + + 'window-side)))) + (window-list))) + (when (window-live-p w) + (set-window-parameter w 'no-delete-other-windows nil) + (set-window-parameter w 'window-side nil)))) + ;; Ensure Ediff has a single full window to manage + (unless (one-window-p t) + (delete-other-windows))) + + ;; Enhanced cleanup with Doom compatibility (setq cleanup-fn (lambda () - ;; Restore window configuration - (when (window-configuration-p cwc) - (set-window-configuration cwc)) - ;; Return focus to original window - (when (window-live-p orig-selected) - (select-window orig-selected)) - ;; Re-display chat buffer in side window if needed - (when (and (buffer-live-p chat-buf) - (or (not (get-buffer-window chat-buf t)) - (not (window-parameter (get-buffer-window - chat-buf t) 'window-side)))) - (ignore-errors (eca-chat--display-buffer chat-buf))) - ;; Kill temp buffers + ;; Kill temp buffers first to avoid conflicts (when (buffer-live-p buf-orig) (kill-buffer buf-orig)) (when (buffer-live-p buf-new) (kill-buffer buf-new)) ;; Kill any additional Ediff-generated buffers (dolist (b session-ediff-buffers) (when (and b (buffer-live-p b)) (kill-buffer b))) + + ;; Doom-aware window restoration + (cond + ;; If we were in a Doom popup, use Doom's restoration + (doom-popup-p + (cond + ((and (fboundp '+popup/restore) (buffer-live-p + orig-buffer)) + (+popup/restore)) + ((buffer-live-p orig-buffer) + (switch-to-buffer orig-buffer)))) + + ;; Standard window config restoration with Doom-compatible delay + (cwc + ;; Use longer delay for Doom compatibility + (run-with-timer (if in-doom-p 0.3 0.1) nil + (lambda () + (condition-case err + (progn + (set-window-configuration cwc) + (when (window-live-p orig-selected) + (select-window orig-selected)) + ;; Re-display chat buffer in side window if needed + (when (and (buffer-live-p chat-buf) + (or (not (get-buffer-window chat-buf + t)) + (not (window-parameter + (get-buffer-window chat-buf + t) + 'window-side)))) + (ignore-errors (eca-chat--display-buffer + chat-buf)))) + (error + (message "ECA: Could not restore window config: %s" + err) + ;; Fallback: ensure chat buffer is visible + (when (buffer-live-p chat-buf) + (ignore-errors (eca-chat--display-buffer + chat-buf)))))))) + + ;; Fallback for cases without saved config + (t + (when (and (buffer-live-p chat-buf) + (or (not (get-buffer-window chat-buf t)) + (not (window-parameter + (get-buffer-window chat-buf t) + 'window-side)))) + (ignore-errors (eca-chat--display-buffer chat-buf))))) + (remove-hook 'ediff-quit-hook cleanup-fn))) + ;; After-setup hook: capture ediff buffers and move to first diff (setq after-setup-fn (lambda () (let ((ediff-buffers-after (seq-filter (lambda (b) (string-match-p "\\*\\(ediff-\\|Ediff -Control\\)" (buffer-name b))) +Control\\)" + (buffer-name b))) (buffer-list)))) (setq session-ediff-buffers (seq-filter (lambda (b) (and (not (member b ediff-buffers-before)) (not (string-match-p "*Ediff -Registry*" (buffer-name b))))) +Registry*" + (buffer-name + b))))) ediff-buffers-after))) (condition-case _err (progn @@ -1048,9 +1112,11 @@ Registry*" (buffer-name b))))) (ediff-next-difference)) (error nil)) (remove-hook 'ediff-after-setup-windows-hook after-setup-fn))) + ;; Install hooks (add-hook 'ediff-quit-hook cleanup-fn) (add-hook 'ediff-after-setup-windows-hook after-setup-fn) + ;; Start Ediff with error handling (condition-case err (ediff-buffers buf-orig buf-new) @@ -1058,14 +1124,22 @@ Registry*" (buffer-name b))))) ;; On error, remove hooks and restore windows (remove-hook 'ediff-quit-hook cleanup-fn) (remove-hook 'ediff-after-setup-windows-hook after-setup-fn) - (when (window-configuration-p cwc) - (set-window-configuration cwc)) + + ;; Doom-aware error recovery + (cond + (cwc (set-window-configuration cwc)) + (doom-popup-p + (when (buffer-live-p orig-buffer) + (switch-to-buffer orig-buffer)))) + ;; Re-display chat buffer on error (when (and (buffer-live-p chat-buf) (or (not (get-buffer-window chat-buf t)) (not (window-parameter (get-buffer-window chat-buf - t) 'window-side)))) + t) + 'window-side)))) (ignore-errors (eca-chat--display-buffer chat-buf))) + ;; Kill temp buffers (when (buffer-live-p buf-orig) (kill-buffer buf-orig)) (when (buffer-live-p buf-new) (kill-buffer buf-new)) From 8fecd3f6bc79ff9f382b9c6bb1a330d896c052bf Mon Sep 17 00:00:00 2001 From: CsBigDataHub Date: Fri, 12 Sep 2025 12:26:52 -0400 Subject: [PATCH 05/10] decouple eco-chat.el , new file eca-diff.el --- eca-chat.el | 356 +++++++--------------------------------------------- eca-diff.el | 321 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 367 insertions(+), 310 deletions(-) create mode 100644 eca-diff.el diff --git a/eca-chat.el b/eca-chat.el index d70d4de..20cc4a6 100644 --- a/eca-chat.el +++ b/eca-chat.el @@ -20,6 +20,7 @@ (require 'eca-util) (require 'eca-api) (require 'eca-mcp) +(require 'eca-diff) (require 'evil nil t) @@ -922,316 +923,6 @@ If FORCE? decide to OPEN? or not." (propertize (concat "-" (number-to-string (plist-get details :linesRemoved))) 'font-lock-face 'error) " ")) -(defun eca-chat--parse-unified-diff (diff-text) - "Parse DIFF-TEXT and return a plist with :original and :new strings." - (let ((orig '()) (new '()) in-hunk) - (dolist (l (split-string diff-text "\n")) - (cond - ((string-match "^@@.*@@$" l) (setq in-hunk t)) - ((and in-hunk (string-prefix-p " " l)) - (push (substring l 1) orig) (push (substring l 1) new)) - ((and in-hunk (string-prefix-p "-" l)) - (push (substring l 1) orig)) - ((and in-hunk (string-prefix-p "+" l)) - (push (substring l 1) new)))) - (list :original (string-join (nreverse orig) "\n") - :new (string-join (nreverse new) "\n")))) - - -(defun eca-chat--show-diff-text (path diff) - "Show DIFF for file at PATH as plain unified diff text. -Adds \\='q\\=' to quit." - (with-current-buffer (get-buffer-create (format "*eca-diff:%s*" path)) - (let ((inhibit-read-only t)) - (erase-buffer) - (insert diff) - (diff-mode) - (goto-char (point-min)) - ;; Add a local 'q' binding to kill this buffer quickly - (let ((map (make-sparse-keymap))) - (set-keymap-parent map (current-local-map)) - (define-key map (kbd "q") (lambda () (interactive) (kill-buffer (current-buffer)))) - (use-local-map map))) - (pop-to-buffer (current-buffer)))) - -(defun eca-chat--show-diff-ediff (path diff) - "Show DIFF for file at PATH using Ediff side-by-side in windows. -Uses window configuration management instead of creating frames. -If the current window is a side window, temporarily clear side-window -protections so Ediff can split windows freely. The original window -configuration is restored when Ediff quits via the cleanup hook. -Enhanced with Doom Emacs compatibility." - (let* ((parsed (eca-chat--parse-unified-diff diff)) - (orig (plist-get parsed :original)) - (new (plist-get parsed :new)) - (buf-orig (generate-new-buffer (format "*eca-diff-orig:%s*" - path))) - (buf-new (generate-new-buffer (format "*eca-diff-new:%s*" - path))) - (chat-buf (current-buffer)) - ;; Doom-specific: Check if we're in a popup context - (doom-popup-p (and (boundp '+popup-buffer-mode) - +popup-buffer-mode)) - (in-doom-p (featurep 'doom)) - ;; Don't save window config if in Doom popup - causes conflicts - (cwc (unless doom-popup-p - (current-window-configuration))) - (orig-selected (selected-window)) - (orig-buffer (current-buffer)) - (ediff-buffers-before - (seq-filter (lambda (b) - (string-match-p "\\*\\(ediff-\\|Ediff Control\\)" - (buffer-name b))) - (buffer-list))) - (session-ediff-buffers nil) - cleanup-fn - after-setup-fn) - - ;; Doom-specific: Set popup rules to prevent interference - (when in-doom-p - (when (fboundp 'set-popup-rule!) - ;; Prevent ECA diff buffers from being managed by Doom's popup - system - (set-popup-rule! "^\\*eca-diff-" :ignore t) - (set-popup-rule! "^\\*Ediff Control Panel" :ignore t))) - - ;; Fill temporary buffers - (with-current-buffer buf-orig - (let ((inhibit-read-only t)) - (erase-buffer) - (insert orig) - (set-buffer-modified-p nil))) - (with-current-buffer buf-new - (let ((inhibit-read-only t)) - (erase-buffer) - (insert new) - (set-buffer-modified-p nil))) - - ;; Only manipulate windows if not in Doom popup context - (unless doom-popup-p - ;; Temporarily relax side-window protections so Ediff can split - (let ((frame (selected-frame))) - (dolist (w (seq-filter (lambda (w) - (and (eq (window-frame w) frame) - (or (window-parameter w - - 'no-delete-other-windows) - (window-parameter w - - 'window-side)))) - (window-list))) - (when (window-live-p w) - (set-window-parameter w 'no-delete-other-windows nil) - (set-window-parameter w 'window-side nil)))) - ;; Ensure Ediff has a single full window to manage - (unless (one-window-p t) - (delete-other-windows))) - - ;; Enhanced cleanup with Doom compatibility - (setq cleanup-fn - (lambda () - ;; Kill temp buffers first to avoid conflicts - (when (buffer-live-p buf-orig) (kill-buffer buf-orig)) - (when (buffer-live-p buf-new) (kill-buffer buf-new)) - ;; Kill any additional Ediff-generated buffers - (dolist (b session-ediff-buffers) - (when (and b (buffer-live-p b)) - (kill-buffer b))) - - ;; Doom-aware window restoration - (cond - ;; If we were in a Doom popup, use Doom's restoration - (doom-popup-p - (cond - ((and (fboundp '+popup/restore) (buffer-live-p - orig-buffer)) - (+popup/restore)) - ((buffer-live-p orig-buffer) - (switch-to-buffer orig-buffer)))) - - ;; Standard window config restoration with Doom-compatible delay - (cwc - ;; Use longer delay for Doom compatibility - (run-with-timer (if in-doom-p 0.3 0.1) nil - (lambda () - (condition-case err - (progn - (set-window-configuration cwc) - (when (window-live-p orig-selected) - (select-window orig-selected)) - ;; Re-display chat buffer in side window if needed - (when (and (buffer-live-p chat-buf) - (or (not (get-buffer-window chat-buf - t)) - (not (window-parameter - (get-buffer-window chat-buf - t) - 'window-side)))) - (ignore-errors (eca-chat--display-buffer - chat-buf)))) - (error - (message "ECA: Could not restore window config: %s" - err) - ;; Fallback: ensure chat buffer is visible - (when (buffer-live-p chat-buf) - (ignore-errors (eca-chat--display-buffer - chat-buf)))))))) - - ;; Fallback for cases without saved config - (t - (when (and (buffer-live-p chat-buf) - (or (not (get-buffer-window chat-buf t)) - (not (window-parameter - (get-buffer-window chat-buf t) - 'window-side)))) - (ignore-errors (eca-chat--display-buffer chat-buf))))) - - (remove-hook 'ediff-quit-hook cleanup-fn))) - - ;; After-setup hook: capture ediff buffers and move to first diff - (setq after-setup-fn - (lambda () - (let ((ediff-buffers-after - (seq-filter (lambda (b) - (string-match-p "\\*\\(ediff-\\|Ediff -Control\\)" - (buffer-name b))) - (buffer-list)))) - (setq session-ediff-buffers - (seq-filter (lambda (b) - (and (not (member b - ediff-buffers-before)) - (not (string-match-p "*Ediff -Registry*" - (buffer-name - b))))) - ediff-buffers-after))) - (condition-case _err - (progn - (setq ediff-current-difference -1) - (ediff-next-difference)) - (error nil)) - (remove-hook 'ediff-after-setup-windows-hook after-setup-fn))) - - ;; Install hooks - (add-hook 'ediff-quit-hook cleanup-fn) - (add-hook 'ediff-after-setup-windows-hook after-setup-fn) - - ;; Start Ediff with error handling - (condition-case err - (ediff-buffers buf-orig buf-new) - (error - ;; On error, remove hooks and restore windows - (remove-hook 'ediff-quit-hook cleanup-fn) - (remove-hook 'ediff-after-setup-windows-hook after-setup-fn) - - ;; Doom-aware error recovery - (cond - (cwc (set-window-configuration cwc)) - (doom-popup-p - (when (buffer-live-p orig-buffer) - (switch-to-buffer orig-buffer)))) - - ;; Re-display chat buffer on error - (when (and (buffer-live-p chat-buf) - (or (not (get-buffer-window chat-buf t)) - (not (window-parameter (get-buffer-window chat-buf - t) - 'window-side)))) - (ignore-errors (eca-chat--display-buffer chat-buf))) - - ;; Kill temp buffers - (when (buffer-live-p buf-orig) (kill-buffer buf-orig)) - (when (buffer-live-p buf-new) (kill-buffer buf-new)) - (message "eca-chat: error starting ediff: %s" err))))) - -(defun eca-chat--show-diff-smerge (path diff) - "Show DIFF for file at PATH using Smerge in a dedicated window. -Uses regular window management instead of creating frames. - -If the current window is a side window, temporarily clear side-window -protections so `delete-other-windows' can succeed. The original window -configuration is restored when the smerge buffer is killed." - (let* ((parsed (eca-chat--parse-unified-diff diff)) - (orig (plist-get parsed :original)) - (new (plist-get parsed :new)) - (buf (generate-new-buffer (format "*eca-smerge:%s*" path))) - (cwc (current-window-configuration)) - (orig-selected (selected-window)) - (frame (selected-frame)) - (cleanup-running nil) - cleanup-fn - window-config-hook) - - ;; Fill buffer with conflict markers - (with-current-buffer buf - (let ((inhibit-read-only t)) - (erase-buffer) - (insert (concat "<<<<<<< Original\n" orig "\n=======\n" new "\n>>>>>>> New\n")) - (diff-mode) - (smerge-mode 1) - (goto-char (point-min)))) - - ;; Define cleanup that restores window configuration and kills the buffer - (setq cleanup-fn - (lambda () - (unless cleanup-running - (setq cleanup-running t) - (when (window-configuration-p cwc) - (ignore-errors (set-window-configuration cwc))) - (when (window-live-p orig-selected) - (select-window orig-selected)) - (when (buffer-live-p buf) - (with-current-buffer buf - (remove-hook 'kill-buffer-hook cleanup-fn t))) - (when (functionp window-config-hook) - (remove-hook 'window-configuration-change-hook window-config-hook)) - (when (buffer-live-p buf) - (kill-buffer buf))))) - - ;; If buffer is no longer visible, run cleanup - (setq window-config-hook - (lambda () - (unless (get-buffer-window buf t) - (funcall cleanup-fn)))) - (add-hook 'window-configuration-change-hook window-config-hook) - - ;; Add local keymap and kill hook to the smerge buffer - (with-current-buffer buf - (let ((map (make-sparse-keymap))) - (set-keymap-parent map (current-local-map)) - (define-key map (kbd "q") (lambda () (interactive) (funcall cleanup-fn))) - (use-local-map map)) - (add-hook 'kill-buffer-hook cleanup-fn nil t)) - - ;; Temporarily relax side-window protections in the current frame so - ;; `delete-other-windows' can succeed. The original window - ;; configuration is restored by `cleanup-fn' (which calls `set-window-configuration'). - (dolist (w (seq-filter (lambda (w) - (and (eq (window-frame w) frame) - (or (window-parameter w 'no-delete-other-windows) - (window-parameter w 'window-side)))) - (window-list))) - (when (window-live-p w) - (set-window-parameter w 'no-delete-other-windows nil) - (set-window-parameter w 'window-side nil))) - ;; Present the buffer full-frame - (when (window-live-p (frame-root-window frame)) - (select-window (frame-root-window frame))) - (unless (one-window-p t) - (delete-other-windows)) - (switch-to-buffer buf) - - ;; Return nil explicitly - nil)) - -(defun eca-chat--show-diff (path diff) - "Dispatch DIFF view based on `eca-chat-diff-tool` for PATH." - (pcase eca-chat-diff-tool - ('ediff (eca-chat--show-diff-ediff path diff)) - ('smerge (eca-chat--show-diff-smerge path diff)) - (_ (eca-chat--show-diff-text path diff)))) - (defun eca-chat--relativize-filename-for-workspace-root (filename roots) "Relativize the FILENAME if a workspace root is found for ROOTS." (or (-some->> (-first (lambda (root) (f-ancestor-of? root filename)) roots) @@ -2031,5 +1722,50 @@ if ARG is current prefix, ask for file, otherwise add current file." (sit-for 0.5)) (whisper-run))))) +(defun eca-chat--parse-unified-diff (diff-text) + "Compatibility wrapper that delegates to `eca-diff-parse-unified-diff'. + +DIFF-TEXT is the unified diff string to parse and returns the parsed +plist produced by `eca-diff-parse-unified-diff'." + (require 'eca-diff) + (eca-diff-parse-unified-diff diff-text)) + + +(defun eca-chat--show-diff-text (path diff) + "Compatibility wrapper that delegates to `eca-diff-show-text'. + +PATH is the file path being shown and DIFF is the unified diff text +that will be displayed in the created `*eca-diff:PATH*' buffer." + (require 'eca-diff) + (eca-diff-show-text path diff)) + + +(defun eca-chat--show-diff-ediff (path diff) + "Compatibility wrapper delegating to `eca-diff-show-ediff'. + +PATH is the file path being shown and DIFF is the unified diff text. +This wrapper passes the current buffer as CHAT-BUF so `eca-diff' can +restore the chat display after Ediff quits." + (require 'eca-diff) + (eca-diff-show-ediff path diff (current-buffer) (lambda (b) (ignore-errors (eca-chat--display-buffer b))))) + + +(defun eca-chat--show-diff-smerge (path diff) + "Compatibility wrapper delegating to `eca-diff-show-smerge'. + +PATH is the file path being shown and DIFF is the unified diff text. +This wrapper passes the current buffer as CHAT-BUF so `eca-diff' can +restore the chat display after smerge quits." + (require 'eca-diff) + (eca-diff-show-smerge path diff (current-buffer) (lambda (b) (ignore-errors (eca-chat--display-buffer b))))) + + +(defun eca-chat--show-diff (path diff) + "Dispatch DIFF view based on `eca-chat-diff-tool` for PATH." + (pcase eca-chat-diff-tool + ('ediff (eca-chat--show-diff-ediff path diff)) + ('smerge (eca-chat--show-diff-smerge path diff)) + (_ (eca-chat--show-diff-text path diff)))) + (provide 'eca-chat) ;;; eca-chat.el ends here diff --git a/eca-diff.el b/eca-diff.el new file mode 100644 index 0000000..7a26f6f --- /dev/null +++ b/eca-diff.el @@ -0,0 +1,321 @@ +;;; eca-diff.el --- Diff helpers for ECA -*- lexical-binding: t; -*- +;; +;;; Commentary: +;; +;; Diff helpers for ECA: functions to parse and +;; display unified diffs via text, ediff or smerge. +;; This file is extracted from eca-chat.el to keep diff rendering +;; logic isolated and reusable. +;; +;;; Code: + +(require 'f) +(require 'ediff) +(require 'smerge-mode) +(require 'seq) +(require 'subr-x) + +(defun eca-diff-parse-unified-diff (diff-text) + "Parse DIFF-TEXT and return a plist with :original and :new strings. +Only hunks (lines between @@ ... @@) are considered for the content. +This mirrors the original parser used by the chat UI." + (let ((orig '()) (new '()) in-hunk) + (dolist (l (split-string diff-text "\n")) + (cond + ((string-match "^@@.*@@$" l) (setq in-hunk t)) + ((and in-hunk (string-prefix-p " " l)) + (push (substring l 1) orig) (push (substring l 1) new)) + ((and in-hunk (string-prefix-p "-" l)) + (push (substring l 1) orig)) + ((and in-hunk (string-prefix-p "+" l)) + (push (substring l 1) new)))) + (list :original (string-join (nreverse orig) "\n") + :new (string-join (nreverse new) "\n")))) + +(defun eca-diff-show-text (path diff) + "Show DIFF for file at PATH as plain unified diff text. +Creates a buffer `*eca-diff:PATH*' with `diff-mode'. A local `q' key +is installed to quit the buffer quickly." + (with-current-buffer (get-buffer-create (format "*eca-diff:%s*" path)) + (let ((inhibit-read-only t)) + (erase-buffer) + (insert diff) + (diff-mode) + (goto-char (point-min)) + ;; Add a local 'q' binding to kill this buffer quickly + (let ((map (make-sparse-keymap))) + (set-keymap-parent map (current-local-map)) + (define-key map (kbd "q") (lambda () (interactive) (kill-buffer (current-buffer)))) + (use-local-map map)))) + (pop-to-buffer (current-buffer))) + +(defun eca-diff--default-redisplay-fn (chat-buf) + "Default redisplay function used when a chat RE-DISPLAY-FN is not provided. +Shows CHAT-BUF using `display-buffer' if it's still alive." + (when (buffer-live-p chat-buf) + (ignore-errors (display-buffer chat-buf)))) + +(defun eca-diff-show-ediff (path diff &optional chat-buf redisplay-fn _) + "Show DIFF for file at PATH using Ediff side-by-side in windows. +If CHAT-BUF is provided it will be used to attempt to re-display the +chat buffer after Ediff quits. If REDISPLAY-FN is provided it will be +called with CHAT-BUF to perform that re-display (default: `display-buffer'). +ROOTS may be passed for path relativization if desired. + +This function tries to be Doom-compatible when Emacs runs Doom popup +system (it ignores those popup rules for the generated ediff buffers)." + (let* ((parsed (eca-diff-parse-unified-diff diff)) + (orig (plist-get parsed :original)) + (new (plist-get parsed :new)) + (buf-orig (generate-new-buffer (format "*eca-diff-orig:%s*" path))) + (buf-new (generate-new-buffer (format "*eca-diff-new:%s*" path))) + (chat-buf chat-buf) + (redisplay-fn (or redisplay-fn #'eca-diff--default-redisplay-fn)) + ;; Doom-specific: Check if we're in a popup context + (doom-popup-p (and (boundp '+popup-buffer-mode) +popup-buffer-mode)) + (in-doom-p (featurep 'doom)) + ;; Don't save window config if in Doom popup - causes conflicts + (cwc (unless doom-popup-p (current-window-configuration))) + (orig-selected (selected-window)) + (orig-buffer chat-buf) + (ediff-buffers-before + (seq-filter (lambda (b) + (string-match-p "\\*\\(ediff-\\|Ediff Control\\)" + (buffer-name b))) + (buffer-list))) + session-ediff-buffers + cleanup-fn + after-setup-fn) + + ;; Doom-specific: Set popup rules to prevent interference + (when in-doom-p + (when (fboundp 'set-popup-rule!) + ;; Prevent ECA diff buffers from being managed by Doom's popup system + (set-popup-rule! "^\\*eca-diff-" :ignore t) + (set-popup-rule! "^\\*Ediff Control Panel" :ignore t))) + + ;; Fill temporary buffers + (with-current-buffer buf-orig + (let ((inhibit-read-only t)) + (erase-buffer) + (insert orig) + (set-buffer-modified-p nil))) + (with-current-buffer buf-new + (let ((inhibit-read-only t)) + (erase-buffer) + (insert new) + (set-buffer-modified-p nil))) + + ;; Only manipulate windows if not in Doom popup context + (unless doom-popup-p + ;; Temporarily relax side-window protections so Ediff can split + (let ((frame (selected-frame))) + (dolist (w (seq-filter (lambda (w) + (and (eq (window-frame w) frame) + (or (window-parameter w 'no-delete-other-windows) + (window-parameter w 'window-side)))) + (window-list))) + (when (window-live-p w) + (set-window-parameter w 'no-delete-other-windows nil) + (set-window-parameter w 'window-side nil)))) + ;; Ensure Ediff has a single full window to manage + (unless (one-window-p t) + (delete-other-windows))) + + ;; Enhanced cleanup with Doom compatibility + (setq cleanup-fn + (lambda () + ;; Kill temp buffers first to avoid conflicts + (when (buffer-live-p buf-orig) (kill-buffer buf-orig)) + (when (buffer-live-p buf-new) (kill-buffer buf-new)) + ;; Kill any additional Ediff-generated buffers + (dolist (b session-ediff-buffers) + (when (and b (buffer-live-p b)) + (kill-buffer b))) + + ;; Doom-aware window restoration + (cond + ;; If we were in a Doom popup, use Doom's restoration + (doom-popup-p + (cond + ((and (fboundp '+popup/restore) (buffer-live-p orig-buffer)) + (+popup/restore)) + ((buffer-live-p orig-buffer) + (switch-to-buffer orig-buffer)))) + + ;; Standard window config restoration with Doom-compatible delay + (cwc + ;; Use longer delay for Doom compatibility + (run-with-timer (if in-doom-p 0.3 0.1) nil + (lambda () + (condition-case err + (progn + (set-window-configuration cwc) + (when (window-live-p orig-selected) + (select-window orig-selected)) + ;; Re-display chat buffer in side window if needed + (when (and (buffer-live-p chat-buf) + (or (not (get-buffer-window chat-buf t)) + (not (window-parameter (get-buffer-window chat-buf t) 'window-side)))) + (ignore-errors (funcall redisplay-fn chat-buf)))) + (error + (message "ECA: Could not restore window config: %s" err) + ;; Fallback: ensure chat buffer is visible + (when (buffer-live-p chat-buf) + (ignore-errors (funcall redisplay-fn chat-buf)))))))) + + ;; Fallback for cases without saved config + (t + (when (and (buffer-live-p chat-buf) + (or (not (get-buffer-window chat-buf t)) + (not (window-parameter (get-buffer-window chat-buf t) 'window-side)))) + (ignore-errors (funcall redisplay-fn chat-buf))))) + + (remove-hook 'ediff-quit-hook cleanup-fn))) + + ;; After-setup hook: capture ediff buffers and move to first diff + (setq after-setup-fn + (lambda () + (let ((ediff-buffers-after + (seq-filter (lambda (b) + (string-match-p "\\*\\(ediff-\\|Ediff\n Control\\)" (buffer-name b))) + (buffer-list)))) + (setq session-ediff-buffers + (seq-filter (lambda (b) + (and (not (member b ediff-buffers-before)) + (not (string-match-p "*Ediff\n Registry*" (buffer-name b))))) + ediff-buffers-after))) + (condition-case _err + (progn + (setq ediff-current-difference -1) + (ediff-next-difference)) + (error nil)) + (remove-hook 'ediff-after-setup-windows-hook after-setup-fn))) + + ;; Install hooks + (add-hook 'ediff-quit-hook cleanup-fn) + (add-hook 'ediff-after-setup-windows-hook after-setup-fn) + + ;; Start Ediff with error handling + (condition-case err + (ediff-buffers buf-orig buf-new) + (error + ;; On error, remove hooks and restore windows + (remove-hook 'ediff-quit-hook cleanup-fn) + (remove-hook 'ediff-after-setup-windows-hook after-setup-fn) + + ;; Doom-aware error recovery + (cond + (cwc (set-window-configuration cwc)) + (doom-popup-p + (when (buffer-live-p orig-buffer) + (switch-to-buffer orig-buffer)))) + + ;; Re-display chat buffer on error + (when (and (buffer-live-p chat-buf) + (or (not (get-buffer-window chat-buf t)) + (not (window-parameter (get-buffer-window chat-buf t) 'window-side)))) + (ignore-errors (funcall redisplay-fn chat-buf))) + + ;; Kill temp buffers + (when (buffer-live-p buf-orig) (kill-buffer buf-orig)) + (when (buffer-live-p buf-new) (kill-buffer buf-new)) + (message "eca-diff: error starting ediff: %s" err))))) + +(defun eca-diff-show-smerge (path diff &optional chat-buf redisplay-fn _) + "Show DIFF for file at PATH using Smerge in a dedicated window. +If CHAT-BUF is provided, REDISPLAY-FN (called with CHAT-BUF) will be used +to re-show the chat window when the smerge buffer is killed." + (let* ((parsed (eca-diff-parse-unified-diff diff)) + (orig (plist-get parsed :original)) + (new (plist-get parsed :new)) + (buf (generate-new-buffer (format "*eca-smerge:%s*" path))) + (cwc (current-window-configuration)) + (orig-selected (selected-window)) + (frame (selected-frame)) + (cleanup-running nil) + cleanup-fn + window-config-hook + (redisplay-fn (or redisplay-fn #'eca-diff--default-redisplay-fn))) + + ;; Fill buffer with conflict markers + (with-current-buffer buf + (let ((inhibit-read-only t)) + (erase-buffer) + (insert (concat "<<<<<<< Original\n" orig "\n=======\n" new "\n>>>>>>> New\n")) + (diff-mode) + (smerge-mode 1) + (goto-char (point-min)))) + + ;; Define cleanup that restores window configuration and kills the buffer + (setq cleanup-fn + (lambda () + (unless cleanup-running + (setq cleanup-running t) + (when (window-configuration-p cwc) + (ignore-errors (set-window-configuration cwc))) + (when (window-live-p orig-selected) + (select-window orig-selected)) + (when (buffer-live-p buf) + (with-current-buffer buf + (remove-hook 'kill-buffer-hook cleanup-fn t))) + (when (functionp window-config-hook) + (remove-hook 'window-configuration-change-hook window-config-hook)) + (when (buffer-live-p buf) + (kill-buffer buf)) + ;; Re-display chat buffer if needed + (when (and (buffer-live-p chat-buf) + (or (not (get-buffer-window chat-buf t)) + (not (window-parameter (get-buffer-window chat-buf t) 'window-side)))) + (ignore-errors (funcall redisplay-fn chat-buf)))))) + + ;; If buffer is no longer visible, run cleanup + (setq window-config-hook + (lambda () + (unless (get-buffer-window buf t) + (funcall cleanup-fn)))) + (add-hook 'window-configuration-change-hook window-config-hook) + + ;; Add local keymap and kill hook to the smerge buffer + (with-current-buffer buf + (let ((map (make-sparse-keymap))) + (set-keymap-parent map (current-local-map)) + (define-key map (kbd "q") (lambda () (interactive) (funcall cleanup-fn))) + (use-local-map map)) + (add-hook 'kill-buffer-hook cleanup-fn nil t)) + + ;; Temporarily relax side-window protections in the current frame so + ;; `delete-other-windows' can succeed. The original window + ;; configuration is restored by `cleanup-fn' (which calls `set-window-configuration'). + (dolist (w (seq-filter (lambda (w) + (and (eq (window-frame w) frame) + (or (window-parameter w 'no-delete-other-windows) + (window-parameter w 'window-side)))) + (window-list))) + (when (window-live-p w) + (set-window-parameter w 'no-delete-other-windows nil) + (set-window-parameter w 'window-side nil))) + ;; Present the buffer full-frame + (when (window-live-p (frame-root-window frame)) + (select-window (frame-root-window frame))) + (unless (one-window-p t) + (delete-other-windows)) + (switch-to-buffer buf) + + ;; Return nil explicitly + nil)) + +(defun eca-diff-show (path diff &optional tool chat-buf redisplay-fn &rest _) + "Public entry point to show a unified DIFF for PATH. +Optional TOOL is a symbol `ediff, `smerge or `text to override the default. +If CHAT-BUF is provided REDISPLAY-FN will be called with CHAT-BUF to attempt +re-showing the originating chat buffer after the diff viewer quits. +ROOTS is forwarded where useful for path relativization." + (let ((tool (or tool 'ediff))) + (pcase tool + ('ediff (eca-diff-show-ediff path diff chat-buf redisplay-fn)) + ('smerge (eca-diff-show-smerge path diff chat-buf redisplay-fn)) + (_ (eca-diff-show-text path diff))))) + +(provide 'eca-diff) +;;; eca-diff.el ends here From bec35559088dba132ead8c3fbf88fbc4bdb1a2df Mon Sep 17 00:00:00 2001 From: CsBigDataHub Date: Fri, 12 Sep 2025 13:31:39 -0400 Subject: [PATCH 06/10] clean up eca-chat.el --- eca-chat.el | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/eca-chat.el b/eca-chat.el index 20cc4a6..38b7385 100644 --- a/eca-chat.el +++ b/eca-chat.el @@ -146,8 +146,7 @@ Must be a valid model supported by server, check `eca-chat-select-model`." (defcustom eca-chat-diff-tool 'ediff "Select the method for displaying file-change diffs in ECA chat." :type '(choice (const :tag "Side-by-side Ediff" ediff) - (const :tag "Merge-style Smerge" smerge) - (const :tag "Plain text diff" text)) + (const :tag "Merge-style Smerge" smerge)) :group 'eca) @@ -1727,7 +1726,6 @@ if ARG is current prefix, ask for file, otherwise add current file." DIFF-TEXT is the unified diff string to parse and returns the parsed plist produced by `eca-diff-parse-unified-diff'." - (require 'eca-diff) (eca-diff-parse-unified-diff diff-text)) @@ -1736,7 +1734,6 @@ plist produced by `eca-diff-parse-unified-diff'." PATH is the file path being shown and DIFF is the unified diff text that will be displayed in the created `*eca-diff:PATH*' buffer." - (require 'eca-diff) (eca-diff-show-text path diff)) @@ -1746,7 +1743,6 @@ that will be displayed in the created `*eca-diff:PATH*' buffer." PATH is the file path being shown and DIFF is the unified diff text. This wrapper passes the current buffer as CHAT-BUF so `eca-diff' can restore the chat display after Ediff quits." - (require 'eca-diff) (eca-diff-show-ediff path diff (current-buffer) (lambda (b) (ignore-errors (eca-chat--display-buffer b))))) @@ -1756,7 +1752,6 @@ restore the chat display after Ediff quits." PATH is the file path being shown and DIFF is the unified diff text. This wrapper passes the current buffer as CHAT-BUF so `eca-diff' can restore the chat display after smerge quits." - (require 'eca-diff) (eca-diff-show-smerge path diff (current-buffer) (lambda (b) (ignore-errors (eca-chat--display-buffer b))))) @@ -1764,8 +1759,7 @@ restore the chat display after smerge quits." "Dispatch DIFF view based on `eca-chat-diff-tool` for PATH." (pcase eca-chat-diff-tool ('ediff (eca-chat--show-diff-ediff path diff)) - ('smerge (eca-chat--show-diff-smerge path diff)) - (_ (eca-chat--show-diff-text path diff)))) + ('smerge (eca-chat--show-diff-smerge path diff)))) (provide 'eca-chat) ;;; eca-chat.el ends here From 4f2733976996e2c7fbcd3aae2fbb99134da5327b Mon Sep 17 00:00:00 2001 From: CsBigDataHub Date: Fri, 12 Sep 2025 13:35:57 -0400 Subject: [PATCH 07/10] remove duplicates and clean up --- eca-diff.el | 36 ++++++------------------------------ 1 file changed, 6 insertions(+), 30 deletions(-) diff --git a/eca-diff.el b/eca-diff.el index 7a26f6f..f095b86 100644 --- a/eca-diff.el +++ b/eca-diff.el @@ -1,5 +1,9 @@ ;;; eca-diff.el --- Diff helpers for ECA -*- lexical-binding: t; -*- ;; +;; Utilities to show unified diffs via text, ediff or smerge. +;; This file is extracted from eca-chat.el to keep diff rendering +;; logic isolated and reusable. +;; ;;; Commentary: ;; ;; Diff helpers for ECA: functions to parse and @@ -9,7 +13,7 @@ ;; ;;; Code: -(require 'f) +(require 'f nil t) (require 'ediff) (require 'smerge-mode) (require 'seq) @@ -32,23 +36,6 @@ This mirrors the original parser used by the chat UI." (list :original (string-join (nreverse orig) "\n") :new (string-join (nreverse new) "\n")))) -(defun eca-diff-show-text (path diff) - "Show DIFF for file at PATH as plain unified diff text. -Creates a buffer `*eca-diff:PATH*' with `diff-mode'. A local `q' key -is installed to quit the buffer quickly." - (with-current-buffer (get-buffer-create (format "*eca-diff:%s*" path)) - (let ((inhibit-read-only t)) - (erase-buffer) - (insert diff) - (diff-mode) - (goto-char (point-min)) - ;; Add a local 'q' binding to kill this buffer quickly - (let ((map (make-sparse-keymap))) - (set-keymap-parent map (current-local-map)) - (define-key map (kbd "q") (lambda () (interactive) (kill-buffer (current-buffer)))) - (use-local-map map)))) - (pop-to-buffer (current-buffer))) - (defun eca-diff--default-redisplay-fn (chat-buf) "Default redisplay function used when a chat RE-DISPLAY-FN is not provided. Shows CHAT-BUF using `display-buffer' if it's still alive." @@ -59,7 +46,7 @@ Shows CHAT-BUF using `display-buffer' if it's still alive." "Show DIFF for file at PATH using Ediff side-by-side in windows. If CHAT-BUF is provided it will be used to attempt to re-display the chat buffer after Ediff quits. If REDISPLAY-FN is provided it will be -called with CHAT-BUF to perform that re-display (default: `display-buffer'). +called with CHAT-BUF to perform that re-display (default: display-buffer). ROOTS may be passed for path relativization if desired. This function tries to be Doom-compatible when Emacs runs Doom popup @@ -305,17 +292,6 @@ to re-show the chat window when the smerge buffer is killed." ;; Return nil explicitly nil)) -(defun eca-diff-show (path diff &optional tool chat-buf redisplay-fn &rest _) - "Public entry point to show a unified DIFF for PATH. -Optional TOOL is a symbol `ediff, `smerge or `text to override the default. -If CHAT-BUF is provided REDISPLAY-FN will be called with CHAT-BUF to attempt -re-showing the originating chat buffer after the diff viewer quits. -ROOTS is forwarded where useful for path relativization." - (let ((tool (or tool 'ediff))) - (pcase tool - ('ediff (eca-diff-show-ediff path diff chat-buf redisplay-fn)) - ('smerge (eca-diff-show-smerge path diff chat-buf redisplay-fn)) - (_ (eca-diff-show-text path diff))))) (provide 'eca-diff) ;;; eca-diff.el ends here From 3cc84f54e031a366fcce4f794d1f32ee0cc4dff0 Mon Sep 17 00:00:00 2001 From: CsBigDataHub Date: Fri, 12 Sep 2025 13:39:13 -0400 Subject: [PATCH 08/10] clean up eco-chat.el --- eca-chat.el | 71 +++++++++++++++++++++++------------------------------ 1 file changed, 31 insertions(+), 40 deletions(-) diff --git a/eca-chat.el b/eca-chat.el index 38b7385..a1e2f1b 100644 --- a/eca-chat.el +++ b/eca-chat.el @@ -1097,6 +1097,37 @@ of (LINE . CHARACTER) representing the current selection or cursor position." (declare-function evil-delete-backward-word "evil" ()) (declare-function evil-delete-back-to-indentation "evil" ()) +(defun eca-chat--parse-unified-diff (diff-text) + "Compatibility wrapper that delegates to `eca-diff-parse-unified-diff'. + +DIFF-TEXT is the unified diff string to parse and returns the parsed +plist produced by `eca-diff-parse-unified-diff'." + (eca-diff-parse-unified-diff diff-text)) + +(defun eca-chat--show-diff-ediff (path diff) + "Compatibility wrapper delegating to `eca-diff-show-ediff'. + +PATH is the file path being shown and DIFF is the unified diff text. +This wrapper passes the current buffer as CHAT-BUF so `eca-diff' can +restore the chat display after Ediff quits." + (eca-diff-show-ediff path diff (current-buffer) (lambda (b) (ignore-errors (eca-chat--display-buffer b))))) + + +(defun eca-chat--show-diff-smerge (path diff) + "Compatibility wrapper delegating to `eca-diff-show-smerge'. + +PATH is the file path being shown and DIFF is the unified diff text. +This wrapper passes the current buffer as CHAT-BUF so `eca-diff' can +restore the chat display after smerge quits." + (eca-diff-show-smerge path diff (current-buffer) (lambda (b) (ignore-errors (eca-chat--display-buffer b))))) + + +(defun eca-chat--show-diff (path diff) + "Dispatch DIFF view based on `eca-chat-diff-tool` for PATH." + (pcase eca-chat-diff-tool + ('ediff (eca-chat--show-diff-ediff path diff)) + ('smerge (eca-chat--show-diff-smerge path diff)))) + ;; Public (define-derived-mode eca-chat-mode markdown-mode "eca-chat" @@ -1721,45 +1752,5 @@ if ARG is current prefix, ask for file, otherwise add current file." (sit-for 0.5)) (whisper-run))))) -(defun eca-chat--parse-unified-diff (diff-text) - "Compatibility wrapper that delegates to `eca-diff-parse-unified-diff'. - -DIFF-TEXT is the unified diff string to parse and returns the parsed -plist produced by `eca-diff-parse-unified-diff'." - (eca-diff-parse-unified-diff diff-text)) - - -(defun eca-chat--show-diff-text (path diff) - "Compatibility wrapper that delegates to `eca-diff-show-text'. - -PATH is the file path being shown and DIFF is the unified diff text -that will be displayed in the created `*eca-diff:PATH*' buffer." - (eca-diff-show-text path diff)) - - -(defun eca-chat--show-diff-ediff (path diff) - "Compatibility wrapper delegating to `eca-diff-show-ediff'. - -PATH is the file path being shown and DIFF is the unified diff text. -This wrapper passes the current buffer as CHAT-BUF so `eca-diff' can -restore the chat display after Ediff quits." - (eca-diff-show-ediff path diff (current-buffer) (lambda (b) (ignore-errors (eca-chat--display-buffer b))))) - - -(defun eca-chat--show-diff-smerge (path diff) - "Compatibility wrapper delegating to `eca-diff-show-smerge'. - -PATH is the file path being shown and DIFF is the unified diff text. -This wrapper passes the current buffer as CHAT-BUF so `eca-diff' can -restore the chat display after smerge quits." - (eca-diff-show-smerge path diff (current-buffer) (lambda (b) (ignore-errors (eca-chat--display-buffer b))))) - - -(defun eca-chat--show-diff (path diff) - "Dispatch DIFF view based on `eca-chat-diff-tool` for PATH." - (pcase eca-chat-diff-tool - ('ediff (eca-chat--show-diff-ediff path diff)) - ('smerge (eca-chat--show-diff-smerge path diff)))) - (provide 'eca-chat) ;;; eca-chat.el ends here From f19a04643c34da5278e5b667f6e67b81d10cdcf7 Mon Sep 17 00:00:00 2001 From: CsBigDataHub Date: Fri, 12 Sep 2025 13:40:14 -0400 Subject: [PATCH 09/10] package `f` is already present --- eca-diff.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eca-diff.el b/eca-diff.el index f095b86..d25c5cf 100644 --- a/eca-diff.el +++ b/eca-diff.el @@ -13,7 +13,7 @@ ;; ;;; Code: -(require 'f nil t) +(require 'f) (require 'ediff) (require 'smerge-mode) (require 'seq) From 36dd0b985a36f48919630d0e259f1038d510079c Mon Sep 17 00:00:00 2001 From: CsBigDataHub Date: Fri, 12 Sep 2025 15:08:00 -0400 Subject: [PATCH 10/10] remove ediff and smerge requirement from eca-caht --- eca-chat.el | 2 -- 1 file changed, 2 deletions(-) diff --git a/eca-chat.el b/eca-chat.el index a1e2f1b..ea249a0 100644 --- a/eca-chat.el +++ b/eca-chat.el @@ -14,8 +14,6 @@ (require 'f) (require 'markdown-mode) (require 'compat) -(require 'ediff) -(require 'smerge-mode) (require 'eca-util) (require 'eca-api)