From 8644b5c469b22e5e358c165bfc3242a4b999a6ba Mon Sep 17 00:00:00 2001 From: Jens Luedicke Date: Wed, 10 Sep 2025 14:59:43 +0200 Subject: [PATCH] Optimize configuration reload to prevent UI freezing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implemented non-blocking reload mechanism that processes files incrementally during idle time to prevent Emacs from freezing. Changes: - New default reload-emacs-config: Non-blocking with idle timers - reload-emacs-config-blocking: Original blocking version - reload-emacs-config-fast: Uses byte-compiled files for speed - reload-emacs-config-smart: Only reloads recently changed files - reload-current-file: Quick reload of current .el file only Keybindings: - C-c C-r: Non-blocking reload (new default) - C-u C-c C-r: Blocking reload (old behavior) - C-c r: Reload current file - C-c R: Fast byte-compiled reload This fixes the lag issue during configuration reload by loading files one at a time with 0.01s idle gaps between them. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- CLAUDE.md | 18 +++- lisp/init-keybindings.el | 7 ++ lisp/init-utils.el | 173 ++++++++++++++++++++++++++++++++++++++- 3 files changed, 192 insertions(+), 6 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 5aada7f..1e1bf72 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,10 +10,20 @@ This is a modular Emacs configuration with 33+ configuration modules in the `lis ### Configuration Management ```elisp -M-x reload-emacs-config ; Reload entire configuration -M-x byte-compile-config ; Byte-compile all config files for faster loading -M-x clean-byte-compiled-files ; Remove all .elc files -M-x byte-compile-init-files ; Compile only lisp/ directory files +M-x reload-emacs-config ; Non-blocking reload (won't freeze UI) +M-x reload-emacs-config-blocking ; Original blocking reload +M-x reload-emacs-config-fast ; Fast reload using byte-compiled files +M-x reload-emacs-config-smart ; Smart reload (only changed files) +M-x reload-current-file ; Reload only current .el file +M-x byte-compile-config ; Byte-compile all config files for faster loading +M-x clean-byte-compiled-files ; Remove all .elc files +M-x byte-compile-init-files ; Compile only lisp/ directory files + +;; Keybindings: +C-c C-r ; Non-blocking reload (default) +C-u C-c C-r ; Blocking reload (old behavior) +C-c r ; Reload current file only +C-c R ; Fast reload (byte-compiled) ``` ### Emergency Fixes diff --git a/lisp/init-keybindings.el b/lisp/init-keybindings.el index 97e412a..e4b9504 100644 --- a/lisp/init-keybindings.el +++ b/lisp/init-keybindings.el @@ -8,7 +8,14 @@ (global-set-key (kbd "C-x k") 'kill-current-buffer-no-confirm) ;;; Configuration reload +;; Default: Non-blocking reload (global-set-key (kbd "C-c C-r") 'reload-emacs-config) +;; C-u prefix: Blocking reload (old behavior) +(global-set-key (kbd "C-u C-c C-r") 'reload-emacs-config-blocking) +;; Quick reload for current file only +(global-set-key (kbd "C-c r") 'reload-current-file) +;; Fast reload using byte-compiled files +(global-set-key (kbd "C-c R") 'reload-emacs-config-fast) ;;; Portfolio tracker (global-set-key (kbd "C-c $") 'portfolio-tracker) diff --git a/lisp/init-utils.el b/lisp/init-utils.el index fe80efb..c803952 100644 --- a/lisp/init-utils.el +++ b/lisp/init-utils.el @@ -10,8 +10,9 @@ (interactive) (kill-buffer (current-buffer))) -(defun reload-emacs-config () - "Reload the Emacs configuration file and all dependent configs." +;; Keep original function but rename it +(defun reload-emacs-config-blocking () + "Reload the Emacs configuration file and all dependent configs (blocking version)." (interactive) ;; First reload the main init.el (load-file (expand-file-name "init.el" user-emacs-directory)) @@ -41,6 +42,174 @@ (message "Emacs configuration fully reloaded!")) +(defvar reload-emacs-config-timer nil + "Timer for non-blocking configuration reload.") + +(defvar reload-emacs-config-files nil + "List of files to reload.") + +(defvar reload-emacs-config-index 0 + "Current index in the reload process.") + +(defun reload-emacs-config-process-next () + "Process the next file in the reload queue." + (when (< reload-emacs-config-index (length reload-emacs-config-files)) + (let ((file (nth reload-emacs-config-index reload-emacs-config-files))) + (condition-case err + (progn + (load-file file) + (message "[%d/%d] Loaded %s" + (1+ reload-emacs-config-index) + (length reload-emacs-config-files) + (file-name-nondirectory file))) + (error + (message "[%d/%d] Error loading %s: %s" + (1+ reload-emacs-config-index) + (length reload-emacs-config-files) + (file-name-nondirectory file) + err))) + (cl-incf reload-emacs-config-index) + ;; Schedule next file + (setq reload-emacs-config-timer + (run-with-idle-timer 0.01 nil #'reload-emacs-config-process-next))) + ;; All done + (when (>= reload-emacs-config-index (length reload-emacs-config-files)) + (message "✓ Configuration reload complete!") + (setq reload-emacs-config-timer nil + reload-emacs-config-files nil + reload-emacs-config-index 0)))) + +(defun reload-emacs-config () + "Reload Emacs configuration non-blocking with incremental updates. +This version loads configuration files one by one during idle time +to prevent UI freezing." + (interactive) + ;; Cancel any ongoing reload + (when reload-emacs-config-timer + (cancel-timer reload-emacs-config-timer) + (setq reload-emacs-config-timer nil)) + + ;; Prepare list of files to reload + (setq reload-emacs-config-files + (cl-remove-if-not + #'file-exists-p + (list (expand-file-name "init.el" user-emacs-directory) + (expand-file-name "emacs-dev-config.el" user-emacs-directory) + (expand-file-name "shr-config.el" user-emacs-directory) + (expand-file-name "elfeed-config.el" user-emacs-directory) + (expand-file-name "mu4e-config.el" user-emacs-directory)))) + + (setq reload-emacs-config-index 0) + + (message "Starting configuration reload (%d files)..." + (length reload-emacs-config-files)) + + ;; Start the reload process + (reload-emacs-config-process-next)) + +(defun reload-emacs-config-async () + "Reload Emacs configuration asynchronously with progress feedback." + (interactive) + (let* ((start-time (current-time)) + (config-files '()) + (total-files 0) + (loaded-files 0) + (progress-reporter nil)) + + ;; Collect all config files to reload + (setq config-files + (append + ;; Main init.el + (list (expand-file-name "init.el" user-emacs-directory)) + ;; Optional configs + (cl-remove-if-not + #'file-exists-p + (list + (expand-file-name "emacs-dev-config.el" user-emacs-directory) + (expand-file-name "shr-config.el" user-emacs-directory) + (expand-file-name "elfeed-config.el" user-emacs-directory) + (expand-file-name "mu4e-config.el" user-emacs-directory))))) + + (setq total-files (length config-files)) + (setq progress-reporter + (make-progress-reporter "Reloading configuration..." 0 total-files)) + + ;; Load files with progress updates + (dolist (file config-files) + (condition-case err + (progn + (load-file file) + (cl-incf loaded-files) + (progress-reporter-update progress-reporter loaded-files)) + (error + (message "Error loading %s: %s" (file-name-nondirectory file) err)))) + + (progress-reporter-done progress-reporter) + (message "Configuration reloaded in %.2f seconds" + (float-time (time-subtract (current-time) start-time))))) + +(defun reload-emacs-config-smart () + "Smart reload that only reloads changed files since last load." + (interactive) + (let* ((lisp-dir (expand-file-name "lisp" user-emacs-directory)) + (init-file (expand-file-name "init.el" user-emacs-directory)) + (changed-files '()) + (reload-all nil)) + + ;; Check if init.el has changed + (when (file-newer-than-file-p init-file (current-time)) + (setq reload-all t)) + + (if reload-all + ;; If init.el changed, do full reload + (reload-emacs-config-async) + ;; Otherwise, reload only changed files + (progn + ;; Find changed files in lisp directory + (dolist (file (directory-files lisp-dir t "\\.el$")) + (when (and (not (string-match-p "\\.elc$" file)) + (not (string-match-p "#" file)) + (file-newer-than-file-p file (time-subtract (current-time) 60))) + (push file changed-files))) + + (if changed-files + (progn + (message "Reloading %d changed file(s)..." (length changed-files)) + (dolist (file changed-files) + (condition-case err + (progn + (load-file file) + (message "Reloaded %s" (file-name-nondirectory file))) + (error + (message "Error reloading %s: %s" + (file-name-nondirectory file) err)))) + (message "Smart reload complete!")) + (message "No files changed recently. Use C-u C-c C-r for full reload."))))) + +(defun reload-current-file () + "Reload only the current file if it's an Emacs Lisp file." + (interactive) + (when (and (buffer-file-name) + (string-match-p "\\.el$" (buffer-file-name))) + (save-buffer) + (load-file (buffer-file-name)) + (message "Reloaded %s" (file-name-nondirectory (buffer-file-name))))) + +(defun reload-emacs-config-fast () + "Fast reload using byte-compiled files when available. +This is the fastest reload method but requires byte-compilation." + (interactive) + (let* ((init-file (expand-file-name "init" user-emacs-directory)) + (start-time (current-time))) + ;; Try to load byte-compiled version first, fall back to source + (if (file-exists-p (concat init-file ".elc")) + (progn + (load init-file t t) + (message "Fast reload complete (byte-compiled) in %.2f seconds" + (float-time (time-subtract (current-time) start-time)))) + ;; Fall back to non-blocking reload + (reload-emacs-config)))) + ;;; Package management helpers (defun package-refresh-without-proxy () "Temporarily disable proxy and refresh packages."