super-save-0.5.0/0000775000175000017500000000000015156457456013464 5ustar dogslegdogslegsuper-save-0.5.0/test/0000775000175000017500000000000015156457456014443 5ustar dogslegdogslegsuper-save-0.5.0/test/super-save-test.el0000664000175000017500000003336415156457456020045 0ustar dogslegdogsleg;;; super-save-test.el --- Tests for super-save -*- lexical-binding: t -*- ;;; Commentary: ;; ;; Buttercup test suite for super-save. ;; ;;; Code: (require 'buttercup) (require 'super-save) ;;; Helpers (defmacro super-save-test-with-temp-file (&rest body) "Create a temp file, visit it, run BODY, then clean up." (declare (indent 0) (debug body)) `(let ((temp-file (make-temp-file "super-save-test"))) (unwind-protect (progn (find-file temp-file) ,@body) (when (get-file-buffer temp-file) (with-current-buffer (get-file-buffer temp-file) (set-buffer-modified-p nil) (kill-buffer))) (when (file-exists-p temp-file) (delete-file temp-file))))) ;;; Mode activation (describe "super-save-mode" (after-each (super-save-mode -1)) (it "can be enabled and disabled" (super-save-mode +1) (expect super-save-mode :to-be-truthy) (super-save-mode -1) (expect super-save-mode :not :to-be-truthy)) (it "advises trigger commands when enabled" (let ((super-save-triggers '(switch-to-buffer))) (super-save-mode +1) (expect (advice-member-p #'super-save-command-advice 'switch-to-buffer) :to-be-truthy))) (it "removes advice from trigger commands when disabled" (let ((super-save-triggers '(switch-to-buffer))) (super-save-mode +1) (super-save-mode -1) (expect (advice-member-p #'super-save-command-advice 'switch-to-buffer) :not :to-be-truthy))) (it "registers window-change hooks when super-save-when-buffer-switched is t" (let ((super-save-when-buffer-switched t)) (super-save-mode +1) (expect (memq #'super-save-window-change-handler window-buffer-change-functions) :to-be-truthy) (expect (memq #'super-save-window-change-handler window-selection-change-functions) :to-be-truthy))) (it "does not register window-change hooks when super-save-when-buffer-switched is nil" (let ((super-save-when-buffer-switched nil)) (super-save-mode +1) (expect (memq #'super-save-window-change-handler window-buffer-change-functions) :not :to-be-truthy) (expect (memq #'super-save-window-change-handler window-selection-change-functions) :not :to-be-truthy))) (it "cleans up window-change hooks when disabled" (let ((super-save-when-buffer-switched t)) (super-save-mode +1)) (super-save-mode -1) (expect (memq #'super-save-window-change-handler window-buffer-change-functions) :not :to-be-truthy) (expect (memq #'super-save-window-change-handler window-selection-change-functions) :not :to-be-truthy)) (it "registers focus-change handler when super-save-when-focus-lost is t" (let ((super-save-when-focus-lost t)) (super-save-mode +1) (expect (advice-function-member-p #'super-save-focus-change-handler after-focus-change-function) :to-be-truthy))) (it "does not register focus-change handler when super-save-when-focus-lost is nil" (let ((super-save-when-focus-lost nil)) (super-save-mode +1) (expect (advice-function-member-p #'super-save-focus-change-handler after-focus-change-function) :not :to-be-truthy))) (it "cleans up focus-change handler when disabled" (let ((super-save-when-focus-lost t)) (super-save-mode +1)) (super-save-mode -1) (expect (advice-function-member-p #'super-save-focus-change-handler after-focus-change-function) :not :to-be-truthy)) (it "initializes idle timer when super-save-auto-save-when-idle is t" (let ((super-save-auto-save-when-idle t) (super-save-idle-duration 10)) (super-save-mode +1) (expect super-save-idle-timer :to-be-truthy))) (it "does not initialize idle timer when super-save-auto-save-when-idle is nil" (let ((super-save-auto-save-when-idle nil)) (super-save-mode +1) (expect super-save-idle-timer :not :to-be-truthy))) (it "cancels idle timer when disabled" (let ((super-save-auto-save-when-idle t) (super-save-idle-duration 10)) (super-save-mode +1) (let ((timer super-save-idle-timer)) (super-save-mode -1) (expect (timer--triggered timer) :not :to-be-truthy))))) ;;; Predicates (describe "super-save-p" (it "returns t for a modified file-visiting buffer" (super-save-test-with-temp-file (insert "modified") (expect (super-save-p) :to-be-truthy))) (it "returns nil for a non-file-visiting buffer" (with-temp-buffer (expect (super-save-p) :not :to-be-truthy))) (it "returns nil for an unmodified buffer" (super-save-test-with-temp-file (expect (super-save-p) :not :to-be-truthy))) (it "returns nil when buffer-file-name matches super-save-exclude" (super-save-test-with-temp-file (insert "modified") (let ((super-save-exclude (list (regexp-quote (file-name-nondirectory buffer-file-name))))) (expect (super-save-p) :not :to-be-truthy)))) (it "returns nil when buffer exceeds super-save-max-buffer-size" (super-save-test-with-temp-file (insert "modified") (let ((super-save-max-buffer-size 1)) (expect (super-save-p) :not :to-be-truthy)))) (it "returns t when super-save-max-buffer-size is nil" (super-save-test-with-temp-file (insert "modified") (let ((super-save-max-buffer-size nil)) (expect (super-save-p) :to-be-truthy)))) (it "returns nil when file has been modified externally" (super-save-test-with-temp-file (insert "modified") ;; Simulate external modification by changing the file's modtime (let ((temp-file buffer-file-name)) (write-region "external change" nil temp-file nil 'silent) (expect (super-save-p) :not :to-be-truthy)))) (it "returns nil for a remote file when super-save-remote-files is nil" (super-save-test-with-temp-file (insert "modified") (let ((super-save-remote-files nil)) (spy-on 'file-remote-p :and-return-value "/ssh:host:") (expect (super-save-p) :not :to-be-truthy)))) (it "returns t for a remote file when super-save-remote-files is t" (super-save-test-with-temp-file (insert "modified") (let ((super-save-remote-files t)) (spy-on 'file-remote-p :and-return-value "/ssh:host:") (expect (super-save-p) :to-be-truthy)))) (it "returns nil when parent directory no longer exists" (let* ((temp-dir (make-temp-file "super-save-test-dir" t)) (temp-file (expand-file-name "test.txt" temp-dir))) (unwind-protect (progn (write-region "content" nil temp-file) (find-file temp-file) (insert "modified") (delete-directory temp-dir t) (expect (super-save-p) :not :to-be-truthy)) (when (get-file-buffer temp-file) (with-current-buffer (get-file-buffer temp-file) (set-buffer-modified-p nil) (kill-buffer))) (when (file-exists-p temp-dir) (delete-directory temp-dir t)))))) (describe "super-save-p error handling" (it "handles broken predicates gracefully" (super-save-test-with-temp-file (insert "modified") (let ((super-save-predicates (list (lambda () (error "Broken predicate"))))) (expect (super-save-p) :not :to-be-truthy)))) (it "logs a message when a predicate errors" (super-save-test-with-temp-file (insert "modified") (let ((super-save-predicates (list (lambda () (error "Broken predicate"))))) (spy-on 'message) (super-save-p) (expect 'message :to-have-been-called))))) ;;; super-save-include-p (describe "super-save-include-p" (it "returns t when no excludes are set" (let ((super-save-exclude nil)) (expect (super-save-include-p "/some/file.el") :to-be-truthy))) (it "returns nil when filename matches an exclude pattern" (let ((super-save-exclude '("\\.gpg$"))) (expect (super-save-include-p "/some/file.gpg") :not :to-be-truthy))) (it "returns t when filename does not match any exclude pattern" (let ((super-save-exclude '("\\.gpg$"))) (expect (super-save-include-p "/some/file.el") :to-be-truthy)))) ;;; Saving (describe "super-save-command" (it "saves a modified file-visiting buffer" (super-save-test-with-temp-file (insert "new content") (super-save-command) (expect (buffer-modified-p) :not :to-be-truthy))) (it "does not save an unmodified buffer" (super-save-test-with-temp-file (spy-on 'basic-save-buffer) (super-save-command) (expect 'basic-save-buffer :not :to-have-been-called))) (it "saves silently when super-save-silent is t" (super-save-test-with-temp-file (insert "new content") (let ((super-save-silent t)) (spy-on 'basic-save-buffer :and-call-through) (super-save-command) (expect 'basic-save-buffer :to-have-been-called) (expect (buffer-modified-p) :not :to-be-truthy))))) (describe "super-save-command with super-save-all-buffers" (it "saves all modified buffers when super-save-all-buffers is t" (let ((temp-file-1 (make-temp-file "super-save-test-1")) (temp-file-2 (make-temp-file "super-save-test-2")) (super-save-all-buffers t)) (unwind-protect (progn (find-file temp-file-1) (insert "content 1") (find-file temp-file-2) (insert "content 2") (super-save-command) (expect (buffer-modified-p) :not :to-be-truthy) (with-current-buffer (get-file-buffer temp-file-1) (expect (buffer-modified-p) :not :to-be-truthy))) (dolist (f (list temp-file-1 temp-file-2)) (when (get-file-buffer f) (with-current-buffer (get-file-buffer f) (set-buffer-modified-p nil) (kill-buffer))) (when (file-exists-p f) (delete-file f))))))) (describe "super-save-command with super-save-all-buffers nil" (it "only saves the current buffer, not other modified buffers" (let ((temp-file-1 (make-temp-file "super-save-test-1")) (temp-file-2 (make-temp-file "super-save-test-2")) (super-save-all-buffers nil)) (unwind-protect (progn (find-file temp-file-1) (insert "content 1") (find-file temp-file-2) (insert "content 2") ;; current buffer is temp-file-2 (super-save-command) (expect (buffer-modified-p) :not :to-be-truthy) (with-current-buffer (get-file-buffer temp-file-1) (expect (buffer-modified-p) :to-be-truthy))) (dolist (f (list temp-file-1 temp-file-2)) (when (get-file-buffer f) (with-current-buffer (get-file-buffer f) (set-buffer-modified-p nil) (kill-buffer))) (when (file-exists-p f) (delete-file f))))))) ;;; Focus change handler (describe "super-save-focus-change-handler" (it "saves when no frame has focus" (spy-on 'frame-list :and-return-value '(fake-frame)) (spy-on 'frame-focus-state :and-return-value nil) (spy-on 'super-save-command) (super-save-focus-change-handler) (expect 'super-save-command :to-have-been-called)) (it "does not save when a frame has focus" (spy-on 'frame-list :and-return-value '(fake-frame)) (spy-on 'frame-focus-state :and-return-value t) (spy-on 'super-save-command) (super-save-focus-change-handler) (expect 'super-save-command :not :to-have-been-called))) ;;; Trailing whitespace (describe "super-save-delete-trailing-whitespace-maybe" (it "does nothing when super-save-delete-trailing-whitespace is nil" (super-save-test-with-temp-file (insert "hello ") (let ((super-save-delete-trailing-whitespace nil)) (super-save-delete-trailing-whitespace-maybe) (expect (buffer-string) :to-match "hello ")))) (it "deletes trailing whitespace when super-save-delete-trailing-whitespace is t" (super-save-test-with-temp-file (insert "hello \n") (goto-char (point-min)) (let ((super-save-delete-trailing-whitespace t)) (super-save-delete-trailing-whitespace-maybe) (expect (buffer-string) :to-equal "hello\n")))) (it "preserves current line whitespace with except-current-line" (super-save-test-with-temp-file (insert "line1 \nline2 \nline3 ") (goto-char (point-min)) (forward-line 1) (end-of-line) (let ((super-save-delete-trailing-whitespace 'except-current-line)) (super-save-delete-trailing-whitespace-maybe) (expect (buffer-string) :to-equal "line1\nline2 \nline3"))))) ;;; Special buffer detection (describe "super-save-org-src-buffer-p" (it "returns nil in a regular buffer" (with-temp-buffer (expect (super-save-org-src-buffer-p) :not :to-be-truthy))) (it "returns t when org-src-mode is active and org-edit-src-save is available" (with-temp-buffer (setq-local org-src-mode t) (spy-on 'org-edit-src-save) (expect (super-save-org-src-buffer-p) :to-be-truthy)))) (describe "super-save-edit-indirect-buffer-p" (it "returns nil in a regular buffer" (with-temp-buffer (expect (super-save-edit-indirect-buffer-p) :not :to-be-truthy))) (it "returns t when edit-indirect--overlay is set and edit-indirect--commit is available" (with-temp-buffer (setq-local edit-indirect--overlay (make-overlay (point-min) (point-max))) (spy-on 'edit-indirect--commit) (expect (super-save-edit-indirect-buffer-p) :to-be-truthy)))) ;;; super-save-test.el ends here super-save-0.5.0/Eldev0000664000175000017500000000033015156457456014442 0ustar dogslegdogsleg; -*- mode: emacs-lisp; lexical-binding: t; no-byte-compile: t -*- (eldev-use-package-archive 'gnu) (eldev-use-package-archive 'melpa) (eldev-add-extra-dependencies 'test 'buttercup) (eldev-use-plugin 'autoloads) super-save-0.5.0/README.md0000664000175000017500000002101015156457456014735 0ustar dogslegdogsleg[![License GPL 3][badge-license]][copying] [![MELPA][melpa-badge]][melpa-package] [![MELPA Stable][melpa-stable-badge]][melpa-stable-package] [![Build Status](https://github.com/bbatsov/super-save/workflows/CI/badge.svg)](https://github.com/bbatsov/super-save/actions?query=workflow%3ACI) # super-save `super-save` auto-saves your buffers when certain events happen - e.g. you switch between buffers, an Emacs frame loses focus, etc. You can think of it as both something that augments and replaces the standard `auto-save-mode`. **This package requires Emacs 27.1+.** ## Rationale I created `super-save` because I wanted Emacs to save files the way IntelliJ IDEA and other modern editors do — automatically, when you switch buffers or leave the editor. No manual `C-x C-s`, no thinking about it. I first wrote about this idea [back in 2012](https://batsov.com/articles/2012/03/08/emacs-tip-number-5-save-buffers-automatically-on-buffer-or-window-switch/), and `super-save` grew out of the buffer auto-saving functionality I had built for [Emacs Prelude](https://github.com/bbatsov/prelude). Emacs has a built-in `auto-save-mode`, but it solves a different problem — it's a crash-recovery mechanism that periodically writes buffer contents to temporary `#file#` backup files. That's useful as a safety net, but it's not the same as actually saving your files. You still have to remember to hit save, and you end up with `#backup#` files scattered around your filesystem. `super-save` takes a simpler approach: it just saves your files (for real) when natural editing events happen — switching buffers, switching windows, losing focus, or going idle. No backup files, no recovery dance, no extra complexity. Your files are always saved, and you never have to think about it. ## Installation Available on all major `package.el` community maintained repos - [MELPA Stable][] and [MELPA][] repos. MELPA Stable is recommended as it has the latest stable version. MELPA has a development snapshot for users who don't mind breakage but don't want to run from a git checkout. You can install `super-save` using the following command: M-x package-install [RET] super-save [RET] or if you'd rather keep it in your dotfiles: ```el (unless (package-installed-p 'super-save) (package-refresh-contents) (package-install 'super-save)) ``` If the installation doesn't work try refreshing the package list: M-x package-refresh-contents ### use-package If you're into `use-package` you can use the following snippet: ```el (use-package super-save :ensure t :config (super-save-mode +1)) ``` ### Emacs Prelude super-save started its life as the extraction of a similar functionality I had originally developed for [Emacs Prelude](https://github.com/bbatsov/prelude) and the package is bundled with Prelude. ## Usage Add the following to your Emacs config to enable `super-save`: ```el (super-save-mode +1) ``` If you want to enable the additional feature of auto-saving buffers when Emacs is idle, add the following as well: ```el (setq super-save-auto-save-when-idle t) ``` By default the idle delay is 5 seconds. You can change it via `super-save-idle-duration`: ```el (setq super-save-idle-duration 10) ``` At this point you can probably switch off the built-in `auto-save-mode` (unless you really care about its backups): ```el (setq auto-save-default nil) ``` ## Configuration super-save will save files when certain events happen: - **Frame focus loss** — controlled by `super-save-when-focus-lost` (enabled by default) - **Buffer/window switches** — controlled by `super-save-when-buffer-switched` (enabled by default) - **Command triggers** — configurable via `super-save-triggers` (empty by default, since the window-system hooks above already catch all buffer switches) - **Hook triggers** — configurable via `super-save-hook-triggers` (empty by default) ```el ;; disable saving on focus loss (setq super-save-when-focus-lost nil) ;; disable saving on buffer/window switch (setq super-save-when-buffer-switched nil) ;; add a command trigger (useful for commands that don't involve a buffer switch) (add-to-list 'super-save-triggers 'ace-window) ;; add a hook trigger (add-to-list 'super-save-hook-triggers 'find-file-hook) ``` You can turn off `super-save` for remote files like this: ```el (setq super-save-remote-files nil) ``` If you have very large files that are slow to save, you can set a size limit (in characters) via `super-save-max-buffer-size`: ```el (setq super-save-max-buffer-size 5000000) ``` Sometimes you might want to exclude specific files from super-save. You can achieve this via `super-save-exclude`, for example: ```el (setq super-save-exclude '(".gpg")) ``` The default predicates check that the buffer is visiting a file, is modified, is writable, hasn't been modified externally, and that its parent directory still exists. You can add your own predicates to `super-save-predicates`. These predicates must not take arguments and return nil when the current buffer shouldn't be saved. If a predicate doesn't know whether the buffer needs to be saved, it must return t. The following example stops `super-save` when the current buffer is in Markdown mode: ```el (add-to-list 'super-save-predicates (lambda () (not (eq major-mode 'markdown-mode)))) ``` When saving a file automatically, Emacs will display a message in the `*Messages*` buffer and in the echo area. If you want to suppress these messages, you can set `super-save-silent` to `t`. ```el ;; Save silently (setq super-save-silent t) ``` The `super-save-delete-trailing-whitespace` variable can be used to enable deleting trailing white spaces before saving (via Emacs' `delete-trailing-whitespace`). ```el ;; Enable deleting trailing white spaces before saving (setq super-save-delete-trailing-whitespace t) ;; Enable deleting trailing white spaces before saving (except for the current line) (setq super-save-delete-trailing-whitespace 'except-current-line) ``` ### org-src and edit-indirect buffers `super-save` can save `org-src` edit buffers (using `org-edit-src-save`) and `edit-indirect` buffers (using `edit-indirect--commit`). Both are enabled by default and can be disabled: ```el (setq super-save-handle-org-src nil) (setq super-save-handle-edit-indirect nil) ``` ### Saving all buffers By default, `super-save` will automatically save only the current buffer, if you want to save all open buffers you can set `super-save-all-buffers` to `t`. Setting this to `t` can be interesting when you make indirect buffer edits, like when editing `grep` results with `occur-mode` and `occur-edit-mode`, or when running a project-wide search and replace with `project-query-replace-regexp` and so on. In these cases, we can indirectly edit several buffers without actually visiting or switching to these buffers. Hence, this option allows you to automatically save these buffers, even when they aren't visible in any window. ## Alternatives and Overlap Emacs 26.1 introduced `auto-save-visited-mode`, which saves file-visiting buffers to their actual files after a configurable idle delay (`auto-save-visited-interval`, default 5 seconds). This overlaps directly with `super-save`'s `super-save-auto-save-when-idle` feature, so there's no need to enable both. If idle saving is all you need, the built-in mode might be enough. Where `super-save` goes further is event-driven saving — it saves immediately when you switch buffers, switch windows, or leave Emacs, rather than waiting for an idle timeout. It also provides a predicate system for fine-grained control (max buffer size, exclude patterns, remote file handling, external modification checks), silent saving, trailing whitespace cleanup, and special handling for `org-src` and `edit-indirect` buffers. A common setup is to use `super-save` for event-driven saves and `auto-save-visited-mode` for idle saves: ```el (super-save-mode +1) (auto-save-visited-mode +1) ``` ## License Copyright © 2015-2026 Bozhidar Batsov and [contributors][]. Distributed under the GNU General Public License; type C-h C-c to view it. [badge-license]: https://img.shields.io/badge/license-GPL_3-green.svg [melpa-badge]: http://melpa.org/packages/super-save-badge.svg [melpa-stable-badge]: http://stable.melpa.org/packages/super-save-badge.svg [melpa-package]: http://melpa.org/#/super-save [melpa-stable-package]: http://stable.melpa.org/#/super-save [COPYING]: http://www.gnu.org/copyleft/gpl.html [contributors]: https://github.com/bbatsov/super-save/contributors [melpa]: http://melpa.org [melpa stable]: http://stable.melpa.org super-save-0.5.0/CONTRIBUTING.md0000664000175000017500000000270115156457456015715 0ustar dogslegdogsleg# Contributing If you discover issues, have ideas for improvements or new features, or want to contribute a new module, please report them to the [issue tracker][1] of the repository or submit a pull request. Please, try to follow these guidelines when you do so. ## Issue reporting * Check that the issue has not already been reported. * Check that the issue has not already been fixed in the latest code (a.k.a. `master`). * Be clear, concise and precise in your description of the problem. * Open an issue with a descriptive title and a summary in grammatically correct, complete sentences. * Include any relevant code to the issue summary. ## Pull requests * Read [how to properly contribute to open source projects on Github][2]. * Use a topic branch to easily amend a pull request later, if necessary. * Write [good commit messages][3]. * Mention related tickets in the commit messages (e.g. `[Fix #N] Add missing autoload cookies`) * Use the same coding conventions as the rest of the project. * Verify your Emacs Lisp code with `checkdoc` (C-c ? d). * Open a [pull request][4] that relates to *only* one subject with a clear title and description in grammatically correct, complete sentences. [1]: https://github.com/bbatsov/super-save/issues [2]: http://gun.io/blog/how-to-github-fork-branch-and-pull-request [3]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html [4]: https://help.github.com/articles/using-pull-requests super-save-0.5.0/super-save.el0000664000175000017500000003167215156457456016111 0ustar dogslegdogsleg;;; super-save.el --- Auto-save buffers, based on your activity. -*- lexical-binding: t -*- ;; Copyright © 2015-2026 Bozhidar Batsov ;; Author: Bozhidar Batsov ;; URL: https://github.com/bbatsov/super-save ;; Keywords: convenience ;; Version: 0.5.0 ;; Package-Requires: ((emacs "27.1")) ;; This file is NOT part of GNU Emacs. ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation; either version 3, or (at your option) ;; any later version. ;; ;; This program is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; ;; You should have received a copy of the GNU General Public License ;; along with GNU Emacs; see the file COPYING. If not, write to the ;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, ;; Boston, MA 02110-1301, USA. ;;; Commentary: ;; ;; super-save auto-saves your buffers when certain events happen - e.g. you ;; switch between buffers, an Emacs frame loses focus, etc. You can think of it ;; as both something that augments and replaces the standard auto-save-mode. ;; ;;; Code: (require 'seq) (declare-function org-edit-src-save "org-src" ()) (declare-function edit-indirect--commit "edit-indirect" ()) (defgroup super-save nil "Smart-saving of buffers." :group 'tools :group 'convenience) (defcustom super-save-triggers nil "A list of commands which would trigger `super-save-command'. With `super-save-when-buffer-switched' enabled (the default), the window-system hooks already catch all buffer switches, so this list defaults to nil. You can still add commands here for triggers that don't involve a buffer switch (e.g. `ace-window')." :group 'super-save :type '(repeat symbol) :package-version '(super-save . "0.1.0")) (defcustom super-save-hook-triggers nil "A list of hooks which would trigger `super-save-command'. With `super-save-when-buffer-switched' and `super-save-when-focus-lost' enabled (the defaults), this list defaults to nil as those options cover the common triggers." :group 'super-save :type '(repeat symbol) :package-version '(super-save . "0.3.0")) (defcustom super-save-when-focus-lost t "Save buffers when an Emacs frame loses focus. Uses `after-focus-change-function' to detect focus changes." :group 'super-save :type 'boolean :package-version '(super-save . "0.5.0")) (defcustom super-save-when-buffer-switched t "Save buffers when the selected window's buffer changes. Uses `window-buffer-change-functions' and `window-selection-change-functions' to detect buffer switches. This catches all buffer switches regardless of how they happen, unlike `super-save-triggers' which only catches specific commands." :group 'super-save :type 'boolean :package-version '(super-save . "0.5.0")) (defcustom super-save-auto-save-when-idle nil "Save automatically when Emacs is idle." :group 'super-save :type 'boolean :package-version '(super-save . "0.2.0")) (defcustom super-save-all-buffers nil "Auto-save all buffers, not just the current one. Setting this to t can be interesting when you make indirect buffer edits, like when editing `grep' results with `occur-mode' and `occur-edit-mode', or when running a project-wide search and replace with `project-query-replace-regexp' and so on. In these cases, we can indirectly edit several buffers without actually visiting or switching to these buffers. Hence, this option allows you to automatically save these buffers, even when they aren't visible in any window." :group 'super-save :type 'boolean :package-version '(super-save . "0.4.0")) (defcustom super-save-idle-duration 5 "Delay in seconds for which Emacs has to be idle before auto-saving. See `super-save-auto-save-when-idle'." :group 'super-save :type 'integer :package-version '(super-save . "0.2.0")) (defcustom super-save-remote-files t "Save remote files when t, ignore them otherwise." :group 'super-save :type 'boolean :package-version '(super-save . "0.3.0")) (defcustom super-save-silent nil "Save silently, don't display any message." :group 'super-save :type 'boolean :package-version '(super-save . "0.4.0")) (defcustom super-save-delete-trailing-whitespace nil "Controls whether to delete the trailing whitespace before saving. Set to `except-current-line' if you want to avoid the current line." :group 'super-save :type '(choice (boolean :tag "Enable/disable deleting trailing whitespace for the whole buffer.") (symbol :tag "Delete trailing whitespace except the current line." except-current-line)) :package-version '(super-save . "0.4.0")) (defcustom super-save-exclude nil "A list of regexps for `buffer-file-name' excluded from super-save. When a `buffer-file-name' matches any of the regexps it is ignored." :group 'super-save :type '(repeat (choice regexp)) :package-version '(super-save . "0.4.0")) (defcustom super-save-max-buffer-size nil "Maximal size of buffer (in characters), for which super-save works. Exists mostly because saving constantly huge buffers can be slow in some cases. Set to 0 or nil to disable." :group 'super-save :type 'integer :package-version '(super-save . "0.4.0")) (defcustom super-save-predicates '((lambda () buffer-file-name) (lambda () (buffer-modified-p (current-buffer))) (lambda () (file-writable-p buffer-file-name)) (lambda () (if (and super-save-max-buffer-size (> super-save-max-buffer-size 0)) (< (buffer-size) super-save-max-buffer-size) t)) (lambda () (if (file-remote-p buffer-file-name) super-save-remote-files t)) (lambda () (super-save-include-p buffer-file-name)) (lambda () (verify-visited-file-modtime (current-buffer))) (lambda () (file-directory-p (file-name-directory buffer-file-name)))) "Predicates which return nil when the buffer doesn't need to be saved. Predicate functions don't take any arguments. If a predicate doesn't know whether the buffer needs to be super-saved or not, it must return t." :group 'super-save :type '(repeat function) :package-version '(super-save . "0.4.0")) (defun super-save-include-p (filename) "Return non-nil if FILENAME doesn't match any of the `super-save-exclude'." (not (seq-some (lambda (regexp) (string-match-p regexp filename)) super-save-exclude))) (defun super-save-p () "Return t when current buffer should be saved, otherwise return nil. This function relies on the variable `super-save-predicates'." (seq-every-p (lambda (pred) (condition-case err (funcall pred) (error (message "super-save: predicate %S failed: %S" pred err) nil))) super-save-predicates)) (defun super-save-delete-trailing-whitespace-maybe () "Delete trailing whitespace, optionally avoiding the current line. See `super-save-delete-trailing-whitespace'." (cond ((eq super-save-delete-trailing-whitespace 'except-current-line) (let ((start (line-beginning-position)) (current (point))) (save-excursion (when (< (point-min) start) (save-restriction (narrow-to-region (point-min) (1- start)) (delete-trailing-whitespace))) (when (> (point-max) current) (save-restriction (narrow-to-region current (point-max)) (delete-trailing-whitespace)))))) (super-save-delete-trailing-whitespace (delete-trailing-whitespace)))) (defcustom super-save-handle-org-src t "Save org-src edit buffers using `org-edit-src-save'." :group 'super-save :type 'boolean :package-version '(super-save . "0.5.0")) (defcustom super-save-handle-edit-indirect t "Save edit-indirect buffers using `edit-indirect--commit'." :group 'super-save :type 'boolean :package-version '(super-save . "0.5.0")) (defun super-save-org-src-buffer-p () "Return non-nil if the current buffer is an org-src edit buffer." (and (bound-and-true-p org-src-mode) (fboundp 'org-edit-src-save))) (defun super-save-edit-indirect-buffer-p () "Return non-nil if the current buffer is an edit-indirect buffer." (and (bound-and-true-p edit-indirect--overlay) (fboundp 'edit-indirect--commit))) (defun super-save-maybe-silently (fn) "Call FN, suppressing messages if `super-save-silent' is non-nil." (if super-save-silent (with-temp-message "" (let ((inhibit-message t) (inhibit-redisplay t) (message-log-max nil)) (funcall fn))) (funcall fn))) (defun super-save-buffer (buffer) "Save BUFFER if needed, super-save style." (with-current-buffer buffer (save-excursion (cond ;; org-src edit buffer ((and super-save-handle-org-src (super-save-org-src-buffer-p) (buffer-modified-p)) (super-save-maybe-silently #'org-edit-src-save)) ;; edit-indirect buffer ((and super-save-handle-edit-indirect (super-save-edit-indirect-buffer-p) (buffer-modified-p)) (super-save-maybe-silently #'edit-indirect--commit)) ;; regular file buffer ((super-save-p) (super-save-delete-trailing-whitespace-maybe) (super-save-maybe-silently #'basic-save-buffer)))))) (defun super-save-command () "Save the relevant buffers if needed. When `super-save-all-buffers' is non-nil, save all modified buffers, else, save only the current buffer." (mapc #'super-save-buffer (if super-save-all-buffers (buffer-list) (list (current-buffer))))) (defvar super-save-idle-timer) (defun super-save-command-advice (&rest _args) "A simple wrapper around `super-save-command' that's advice-friendly." (super-save-command)) (defun super-save-advise-trigger-commands () "Apply super-save advice to the commands listed in `super-save-triggers'." (mapc (lambda (command) (advice-add command :before #'super-save-command-advice)) super-save-triggers)) (defun super-save-remove-advice-from-trigger-commands () "Remove super-save advice from the commands listed in `super-save-triggers'." (mapc (lambda (command) (advice-remove command #'super-save-command-advice)) super-save-triggers)) (defun super-save-initialize-idle-timer () "Initialize super-save idle timer if `super-save-auto-save-when-idle' is true." (setq super-save-idle-timer (when super-save-auto-save-when-idle (run-with-idle-timer super-save-idle-duration t #'super-save-command)))) (defun super-save-stop-idle-timer () "Stop super-save idle timer if `super-save-idle-timer' is set." (when super-save-idle-timer (cancel-timer super-save-idle-timer))) (defun super-save-focus-change-handler () "Save buffers when Emacs loses focus (no frame is focused)." (unless (seq-some #'frame-focus-state (frame-list)) (super-save-command))) (defvar super-save--window-change-pending nil "Non-nil when a window-change save is already scheduled for this command loop.") (defun super-save-window-change-handler (&optional _frame) "Save buffers when the window buffer or selection changes. Intended for use with `window-buffer-change-functions' and `window-selection-change-functions'. Uses a flag to avoid duplicate saves when both hooks fire for the same event." (unless super-save--window-change-pending (setq super-save--window-change-pending t) (run-at-time 0 nil (lambda () (setq super-save--window-change-pending nil) (super-save-command))))) (defun super-save-initialize () "Setup super-save's advice and hooks." (super-save-advise-trigger-commands) (super-save-initialize-idle-timer) (dolist (hook super-save-hook-triggers) (add-hook hook #'super-save-command)) (when super-save-when-focus-lost (add-function :after after-focus-change-function #'super-save-focus-change-handler)) (when super-save-when-buffer-switched (add-hook 'window-buffer-change-functions #'super-save-window-change-handler) (add-hook 'window-selection-change-functions #'super-save-window-change-handler))) (defun super-save-stop () "Cleanup super-save's advice and hooks." (super-save-remove-advice-from-trigger-commands) (super-save-stop-idle-timer) (dolist (hook super-save-hook-triggers) (remove-hook hook #'super-save-command)) (remove-function after-focus-change-function #'super-save-focus-change-handler) (remove-hook 'window-buffer-change-functions #'super-save-window-change-handler) (remove-hook 'window-selection-change-functions #'super-save-window-change-handler)) ;;;###autoload (define-minor-mode super-save-mode "A minor mode that saves your Emacs buffers when they lose focus." :lighter " super-save" :group 'super-save :global t (cond (super-save-mode (super-save-initialize)) (t (super-save-stop)))) (provide 'super-save) ;;; super-save.el ends here super-save-0.5.0/CHANGELOG.md0000664000175000017500000000744715156457456015311 0ustar dogslegdogsleg# Changelog ## main (unreleased) ## 0.5.0 (2026-03-18) ### New features - Use `after-focus-change-function` instead of the obsolete `focus-out-hook` for detecting frame focus loss. Controlled by the new `super-save-when-focus-lost` option (enabled by default). - Add a default predicate that checks `verify-visited-file-modtime` to avoid overwriting files modified outside Emacs. - Add a default predicate that checks the parent directory exists before saving, to prevent errors when a file's directory has been removed. - Wrap predicate evaluation in `condition-case` so a broken predicate logs a warning instead of disabling all auto-saving. - Add support for saving `org-src` edit buffers (via `org-edit-src-save`) and `edit-indirect` buffers (via `edit-indirect--commit`). Controlled by `super-save-handle-org-src` and `super-save-handle-edit-indirect` (both enabled by default). - Use `window-buffer-change-functions` and `window-selection-change-functions` to detect buffer and window switches. Controlled by the new `super-save-when-buffer-switched` option (enabled by default). This catches all buffer switches regardless of how they happen, unlike `super-save-triggers`. ### Bug fixes - Fix redundant saves in `super-save-focus-change-handler` — previously it called `super-save-command` once per unfocused frame instead of once. ### Changes - Require Emacs 27.1. - Default `super-save-triggers` to nil, since `super-save-when-buffer-switched` now covers all buffer switches via window-system hooks. - Default `super-save-hook-triggers` to nil, since `super-save-when-focus-lost` and `super-save-when-buffer-switched` cover the common triggers. ## 0.4.0 (2023-12-09) ### New features - Make super-save checks customizable via `super-save-predicates`. - Introduce defcustom `super-save-max-buffer-size` as a way to avoid auto-saving big files. - Introduce defcustom `super-save-exclude` (a list of regular expressions) as a way to filter out certain buffer names from being auto-saved. - [#43](https://github.com/bbatsov/crux/issues/43): Introduce `super-save-silent` to avoid printing messages in the `*Messages*` buffer or in the echo area. - [#43](https://github.com/bbatsov/crux/issues/43): Introduce `super-save-delete-trailing-whitespace` which defaults to `nil` and accepts `t` to run `delete-trailing-whitespace` before saving the buffer. This variable accepts only the symbol `except-current-line` to delete trailing white spaces from all lines except the current one. This can be useful when we are in the middle of writing some thing and we add a space at the end, in this case, we more likely need the space to stay there instead of deleting it. - [#44](https://github.com/bbatsov/crux/issues/44) & [#20](https://github.com/bbatsov/crux/issues/20): Introduce `super-save-all-buffers` to save all modified buffers instead of only the current one. ### Changes - Require Emacs 25.1. ## 0.3.0 (2018-09-29) ### New features - [#16](https://github.com/bbatsov/crux/issues/16): Make list of hook triggers customizable (see `super-save-hook-triggers`). - [#18](https://github.com/bbatsov/crux/issues/18): Make it possible to disable super-save for remote files (see `super-save-remote-files`). ### Changes - Make `super-save-triggers` a list of symbols (it used to be a list of strings). - Trigger super-save on `next-buffer` and `previous-buffer`. ## 0.2.0 (2016-02-21) ### New features - [#3](https://github.com/bbatsov/crux/issues/3): Turn super-save into a global minor-mode (`super-save-mode`). - Add some functionality for auto-saving buffers when Emacs is idle (disabled by default). ## 0.1.0 (2016-02-11) Initial release. Most of super-save was an extraction of a similar functionality I had originally developed for [Emacs Prelude](https://github.com/bbatsov/prelude).