super-save-0.5.0/ 0000775 0001750 0001750 00000000000 15156457456 013464 5 ustar dogsleg dogsleg super-save-0.5.0/test/ 0000775 0001750 0001750 00000000000 15156457456 014443 5 ustar dogsleg dogsleg super-save-0.5.0/test/super-save-test.el 0000664 0001750 0001750 00000033364 15156457456 020045 0 ustar dogsleg dogsleg ;;; 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/Eldev 0000664 0001750 0001750 00000000330 15156457456 014442 0 ustar dogsleg dogsleg ; -*- 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.md 0000664 0001750 0001750 00000021010 15156457456 014735 0 ustar dogsleg dogsleg [![License GPL 3][badge-license]][copying]
[![MELPA][melpa-badge]][melpa-package]
[![MELPA Stable][melpa-stable-badge]][melpa-stable-package]
[](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.md 0000664 0001750 0001750 00000002701 15156457456 015715 0 ustar dogsleg dogsleg # 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.el 0000664 0001750 0001750 00000031672 15156457456 016111 0 ustar dogsleg dogsleg ;;; 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.md 0000664 0001750 0001750 00000007447 15156457456 015311 0 ustar dogsleg dogsleg # 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).