26

I have various things set up in my 'before-save-hook. For example, I run 'delete-trailing-whitespace. This is what I want in almost all occasions.

But sometimes, I'm working on files that are shared with other people, and the file already has a bunch of trailing whitespace. If I save the file, I'll get a big diff that's pretty confusing, as my change is buried in dozens or hundreds of meaningless changes. Yes, everyone could just tell their diff tool to not show whitespace changes, but that's something that everyone has to do every time they look at the diff. I'd rather not even have the whitespace change.

Is there anything I can do to save the file without the whitespace changes, short of starting a new instance of Emacs with no init.el file, or with a modified init.el that doesn't have the hook?

zck
  • 2,712
  • 2
  • 25
  • 44
  • 5
    For the exact reason you have encountered, stripping trailing whitespace using before-save-hook is just a bad idea. Instead you should use the likes of [ws-trim](ftp://ftp.lysator.liu.se/pub/emacs/ws-trim.el) or [ws-butler](https://github.com/lewang/ws-butler) to strip whitespace only from the lines that you yourself have edited. – phils Feb 17 '13 at 02:09
  • @zck: I use Emacs' *ethan-ws-mode* which is great: trailing whitespaces are shown in red. I don't pay attention to the ones already there but I immediately see if I'm creating new ones. And then I can say my co-workers: *"please stop committing source code with arbitrary whitespaces spread around"* every time I'm checking out some file and ethan-ws-mode shows me lots of trailing whitespaces ; ) So instead of using an aggressive hook I use a passive method: it justs *shows* the nasty whitespaces and I get to decide what to do with it. – TacticalCoder Feb 17 '13 at 13:21
  • 1
    Quick and dirty, edit with different editor. – Vasantha Ganesh Sep 14 '20 at 07:54

6 Answers6

21

Here is how I save without triggering delete-trailing-whitespace: C-x C-q C-x C-s C-x C-q: read-only, save, revert read-only

Nicolas Douma
  • 211
  • 2
  • 2
15

A simpler solution I came up with is that my fundamental-mode has no hooks installed, because I want it to be as plain as possible. Thus if I want to save a file without running hooks, I temporarily switch to fundamental-mode.

rafalcieslak
  • 915
  • 1
  • 12
  • 25
  • 1
    This does not work. In Emacs 27, save hooks still run in fundamental-mode. –  Jun 23 '20 at 06:01
5

Based on a comment discussion with @Stefan, here are two possible (untested) solutions:

Use let:

(defun save-buffer-without-dtw ()
  (interactive)
  (let ((b (current-buffer)))   ; memorize the buffer
    (with-temp-buffer ; new temp buffer to bind the global value of before-save-hook
      (let ((before-save-hook (remove 'delete-trailing-whitespace before-save-hook))) 
        (with-current-buffer b  ; go back to the current buffer, before-save-hook is now buffer-local
          (let ((before-save-hook (remove 'delete-trailing-whitespace before-save-hook)))
            (save-buffer)))))))

Use unwind-protect:

(defun save-buffer-without-dtw ()
  (interactive)
  (let ((restore-global
         (memq 'delete-trailing-whitespace (default-value before-save-hook)))
        (restore-local
         (and (local-variable-p 'before-save-hook)
              (memq 'delete-trailing-whitespace before-save-hook))))
    (unwind-protect
         (progn
           (when restore-global
             (remove-hook 'before-save-hook 'delete-trailing-whitespace))
           (when restore-local
             (remove-hook 'before-save-hook 'delete-trailing-whitespace t))
           (save-buffer))
      (when restore-global
        (add-hook 'before-save-hook 'delete-trailing-whitespace))
      (when restore-local
        (add-hook 'before-save-hook 'delete-trailing-whitespace nil t)))))

The problem with the second solution is that the order of functions in the before-save-hook may change.

sds
  • 58,617
  • 29
  • 161
  • 278
4

Here's another solution:

(defvar my-inhibit-dtw nil)
(defun my-delete-trailing-whitespace ()
  (unless my-inhibit-dtw (delete-trailing-whitespace)))
(add-hook 'before-save-hook 'my-delete-trailing-whitespace)

and then

(defun my-inhibit-dtw ()
  (interactive)
  (set (make-local-variable 'my-inhibit-dtw) t))

so you can M-x my-inhibit-dtw RET in the buffers where you don't want to trim whitespace.

miguelmorin
  • 5,025
  • 4
  • 29
  • 64
Stefan
  • 27,908
  • 4
  • 53
  • 82
  • I tested and it works. I like this solution best for defining my hooks because it keeps the order of the hooks without removing and adding them back. – miguelmorin Jul 05 '19 at 19:59
4

I wrote a command inspired by Nicholas Douma's solution.

(defun olav-save-buffer-as-is ()
  "Save file \"as is\", that is in read-only-mode."
  (interactive)
  (if buffer-read-only
      (save-buffer)
    (read-only-mode 1)
    (save-buffer)
    (read-only-mode 0)))
Olav Fosse
  • 128
  • 1
  • 5
1

What we need to do is remove 'delete-trailing-whitespace from before-save-hook, save the buffer, then add it back.

This code will do that, but only remove and add it if it's there to begin with.

;; save the buffer, removing and readding the 'delete-trailing-whitespace function
;; to 'before-save-hook if it's there
(defun save-buffer-no-delete-trailing-whitespace ()
  (interactive)
  (let ((normally-should-delete-trailing-whitespace (memq 'delete-trailing-whitespace before-save-hook)))
    (when normally-should-delete-trailing-whitespace
      (remove-hook 'before-save-hook 'delete-trailing-whitespace))
    (save-buffer)
    (when normally-should-delete-trailing-whitespace
      (add-hook 'before-save-hook 'delete-trailing-whitespace))))
(global-set-key (kbd "C-c C-s") 'save-buffer-no-delete-trailing-whitespace)

It also binds the command to (kbd C-c C-s), for convenience.

zck
  • 2,712
  • 2
  • 25
  • 44
  • You need to use `condition-case` to ensure that `before-save-hook` is restored even if `save-buffer` fail. Alternatively (and much better!) you can bind `before-save-hook` in `let`: `(let ((before-save-hook (remove 'delete-trailing-whitespace before-save-hook))) (save-buffer))`. – sds Feb 17 '13 at 00:18
  • @sds: your let-trick may look nice, but it will fail if something was added buffer-locally to `before-save-hook` (in which case `delete-trailing-whitespace` will be in `(default-value 'before-save-hook)` but not in the value of `before-save-hook`). – Stefan Feb 17 '13 at 14:53
  • @Stefan: the code is executed in the right buffer, so I am `let`-binding the right value, no? – sds Feb 17 '13 at 15:11
  • @sds: that's not the problem. The problem is that `(memq 'delete-trailing-whitespace before-save-hook)` will tell you nil, because `delete-trailing-whitespace` is only on the global part of the hook (i.e. in `(default-value 'before-save-hook)`). – Stefan Feb 17 '13 at 16:25
  • @Stefan: I am not proposing calling `memq`. In the `let` form in my first comment I bind `before-save-hook` to a value which does not contain `delete-trailing-whitespace`. Isn't that the value which `save-buffer` will see? – sds Feb 17 '13 at 17:47
  • @sds: The buffer-local value of `before-save-hook` did not contain `delete-trailing-whitespace` to begin with, so you did not remove anything: it still contains `t` which means "run the global part of the hook" and the global part still contains `delete-trailing-whitespace`. While hooks are represented by symbols, they aren't just simple variables. – Stefan Feb 18 '13 at 13:20
  • @Stefan: is it possible to `let`-bind the global value? – sds Feb 18 '13 at 19:09
  • @sds: of course, by doing a `let` in a buffer that does not have a buffer-local binding for that var. – Stefan Feb 18 '13 at 20:51
  • @Stefan: would this work: `(let ((b (current-buffer))) (with-temp-buffer (let ((before-save-hook (remove 'delete-trailing-whitespace before-save-hook))) (with-current-buffer b (let ((before-save-hook (remove 'delete-trailing-whitespace before-save-hook))) (save-buffer))))))`? I am creating a temp buffer to bind the global value of `before-save-hook` there. – sds Feb 18 '13 at 21:44
  • @sds: it might work, yes. Of course, at this stage the `unwind-proptect + remove-hook + add-hook` looks pretty attractive in comparison. – Stefan Feb 18 '13 at 22:12
  • @Stefan: please take a look at my answer to this question and comment there. – sds Feb 18 '13 at 22:45