;;; user-save.el --- Do things when explicitly saving files -*- lexical-binding: t; -*-

;; Copyright (C) 2021--2022 Case Duckworth <acdw@acdw.net>
;; URL: ...
;; Version: 0.1.0
;; Package-Requires: ((emacs "24.3"))
;; Keywords: files

;;; Commentary:

;; Because `super-save-mode' automatically saves every time we move away from a
;; buffer, it tends to run a lot of `before-save-hook's that don't need to be
;; run that often.  For that reason, I'm writing a mode where C-x C-s saves
;; /and/ runs all the "real" before-save-hooks, so that super-save won't
;; automatically do things like format the buffer all the time.

;;; Code:

(require 'cl-lib)

(defgroup user-save nil
  "Group for `user-save-mode' customizations."
  :group 'emacs
  :prefix "user-save-")

(defcustom user-save-hook-into-kill-emacs nil
  "Add a hook to perform `user-save' to `kill-emacs-hook'.
This option is only useful is `user-save-mode' is active when
Emacs is killed."
  :type 'boolean)

(defcustom user-save-inhibit-modes '(special-mode)
  "List of modes to inhibit `user-save-mode' from activation in."
  :type '(repeat symbol))

(defcustom user-save-inhibit-predicates '(user-save-non-file-buffer-p)
  "List of predicates to inhibit `user-save-mode' from activation.
Each predicate will be called with no arguments, and if it
returns t, will inhibit `user-save-mode' from activating."
  :type '(repeat function))

(defvar user-save-hook nil
  "Hook to run when the user, not Emacs, saves the buffer.")

(defvar user-save-mode-map (let ((map (make-sparse-keymap)))
                             (define-key map (kbd "C-x C-s") #'user-save-buffer)
                             (define-key map (kbd "C-x s") #'user-save-some-buffers)
                             map)
  "Keymap for `user-save-mode'.
This map shadows the default map for `save-buffer'.")

(defun user-save-run-hooks (&rest _)
  "Run the hooks in `user-save-hook'.
This does /not/ also save the buffer."
  (with-demoted-errors "User-save-hook error: %S"
    (run-hooks 'user-save-hook)))

(defun user-save-non-file-buffer-p (&optional buffer-or-name)
  "Return whether BUFFER-OR-NAME is a non-file buffer.
BUFFER-OR-NAME, if omitted, defaults to the current buffer."
  (with-current-buffer (or buffer-or-name (current-buffer))
    (not (buffer-file-name))))

(defun user-save-buffer (&optional arg)
  "Save current buffer in visited file if modified.
This function is precisely the same as `save-buffer', but with
one modification: it also runs functions in `user-save-hook'.
This means that if you have some functionality in Emacs to
automatically save buffers periodically, but have hooks you want
to automatically run when the buffer saves that are
computationally expensive or just aren't something you want to
run all the time, put them in `user-save-hook'.

ARG is passed directly to `save-buffer'."
  (interactive '(called-interactively))
  (message "User-Saving the buffer...")
  (user-save-run-hooks)
  (save-buffer arg)
  (message "User-Saving the buffer...Done."))

(defun user-save-some-buffers (&optional pred)
  "Save some buffers as though the user saved them.
This function does not ask the user about each buffer, but PRED
is used in almost the same way as `save-some-buffers': if it's
nil or t, it will save all file-visiting modified buffers; if
it's a zero-argument function, that will be called to determine
whether the buffer needs to be saved."
  ;; This could maybe be much better.
  (interactive "P")
  (unless pred (setq pred save-some-buffers-default-predicate))
  (dolist (buf (buffer-list))
    (with-current-buffer buf
      (when (and (buffer-modified-p)
                 (buffer-file-name)
                 (or (null pred)
                     (if (functionp pred) (funcall pred) pred)))
        (user-save-buffer)))))

;;;###autoload
(define-minor-mode user-save-mode
  "Mode to enable an an extra user-save hook."
  :lighter " US"
  :keymap user-save-mode-map)

;;;###autoload
(defun user-save-mode-disable ()
  "Turn off `user-save-mode' in the current buffer."
  (user-save-mode -1))

;;;###autoload
(defun user-save-mode-in-some-buffers ()
  "Enable `user-save-mode', but only in some buffers.
The mode will not be enabled in buffers derived from modes in
`user-save-inhibit-modes', those for which
`user-save-inhibit-predicates' return t, or in the minibuffer."
  (unless (or (minibufferp)
              (cl-some #'derived-mode-p user-save-inhibit-modes)
              (run-hook-with-args-until-failure 'user-save-inhibit-predicates))
    (user-save-mode +1)))

;;;###autoload
(define-globalized-minor-mode user-save-global-mode user-save-mode user-save-mode-in-some-buffers
  (if user-save-global-mode
      (when user-save-hook-into-kill-emacs
        (add-hook 'kill-emacs-hook #'user-save-some-buffers))
    (remove-hook 'kill-emacs-hook #'user-save-some-buffers)))

(provide 'user-save)
;;; user-save.el ends here
