ㅈㅅㄹ

이맥스를 사용하면 할 수록 이맥스 밖으로 나가서 뭔가 하는 일이 거의 사라지기 때문에, 회사에서 이맥스 한 번 띄워 놓으면 보통은 일주일 넘게 세션을 끄지 않게 마련이고, 그러다 보면 나중에는 엄청나게 많은 버퍼가 열려 있게 된다. 이런 상황에서 가끔 뭔가 버퍼에 수정을 해 놓긴 했는데 저장하지 않은 상태로 내버려 두거나 혹은 키 입력이 버퍼에 잘 못 들어가서 이상한 문자가 입력되어 있거나 하는 이유로, Magit 같은 걸 실행해서 save-some-files이 호출 될 때나 혹은 C-x kkill-buffer 할 때 니가 뭔가 바꿨으니 저장하겠냐고 물어보지만 사실 내가 뭘 바꿨는지 모르기 때문에 -_- C-g로 취소 후 실제 변경점을 찾아 보는 일련의 행위가 필요하게 된다.


문제는 그게 상당히 귀찮다는 점이다. 어쩌다 타이핑 미스로 한 두 글자 들어간 버퍼들 때문에 일일히 버퍼들 띄우는 것도 귀찮지만, save-some-files의 프롬프트에서 C-r을 눌러 해당 버퍼를 볼 수 있다 하더라도 실제 변경점이 표시되는 건 아니기 때문에 결국 버퍼에 가서 M-x undo RET를 해 보거나 M-x diff-buffer-with-file RET 해서 변경점을 보는 수 밖에 없다. 이런 변경점을 프롬프트 띄우면서 보여주면서 저장을 할 건지 그냥 진행할 건지를 선택할 수 있게 하면 좋지 않을까.


우선 해당 버퍼와 버퍼의 변경점을 보여 주기 위한 함수를 만든다.

(defun my:display-buffer-modification (&optional buffer-or-name)
  (let* ((buffer (or (and buffer-or-name (get-buffer buffer-or-name))
                     (current-buffer)))
         (diff-switches "-urN")
         (file-name (buffer-file-name buffer)))
    (display-buffer buffer '(display-buffer-same-window))
    (delete-other-windows)
    (diff (if (file-exists-p file-name) file-name null-device) buffer nil 'noasync)))

이 함수를 실행하면 아래의 그림 처럼 왼쪽에 원래 버퍼를, 오른쪽에 변경점을 보여주게 된다. 사실 어떻게 보여 주는 게 좋을 것인가 하는 문제는 취향의 영역이라 다른 모양을 원한다면 취향에 맞춰서 변경하면 되겠다.


my:display-buffer-modification 실행 결과


이 함수를 호출하도록 kill-buffer의 advice function을 정의하면,

(defadvice kill-buffer (around my:kill-buffer-modified (&optional buffer-or-name))
  "Adviced kill buffer to show diff with original file to verify the changes."
  (with-current-buffer (or buffer-or-name (current-buffer))
    (if (and (buffer-live-p (current-buffer))
             (buffer-modified-p)
             (buffer-file-name))
        (save-window-excursion
          (my:display-buffer-modification (current-buffer))
          (when (yes-or-no-p (format "Buffer %s modified; kill anyway? " (buffer-name)))
            (set-buffer-modified-p nil)
            ad-do-it))
      ad-do-it)))
(ad-activate 'kill-buffer)

버퍼가 수정되어 있으면 my:display-buffer-modification을 호출하고 프롬프트를 띄운다. 사용자가 yes를 입력할 경우 set-buffer-modified-p로 modification flag를 제거해 주어 다시 kill-buffer에서 프롬프트를 띄우지 않도록 하면 된다. my:display-buffer-modification에서 window configuration을 바꾸기 때문에 프롬프트 이후 원래 configuration으로 복구하도록 save-window-excursion을 걸어 주었다.


save-some-files의 경우는 내부에서 loop를 돌리고 마땅히 hook을 걸 만한 부분도 없기 때문에 advice function을 정의하기엔 난감한 부분이 있어 기존 함수를 가져와서 재정의 하도록 했다. 결국 함수 정의를 다시 덮어 쓰는 것이므로 새로 정의한 my:save-some-buffers 함수가 동작하도록 만들려면 일단 files.el이 load된 이후에 재정의 하도록 해야한다. eval-after-load 내부에서 내가 새로 정의한 함수로 alias를 걸도록 해 주었다.

;; override default save-some-buffers (from 24.5.5 files.el)
(defun my:save-some-buffers (&optional arg pred)
    "Save some modified file-visiting buffers.  Asks user about each one.
You can answer `y' to save, `n' not to save, `C-r' to look at the
buffer in question with `view-buffer' before deciding or `d' to
view the differences using `diff-buffer-with-file'.

This command first saves any buffers where `buffer-save-without-query' is
non-nil, without asking.

Optional argument (the prefix) non-nil means save all with no questions.
Optional second argument PRED determines which buffers are considered:
If PRED is nil, all the file-visiting buffers are considered.
If PRED is t, then certain non-file buffers will also be considered.
If PRED is a zero-argument function, it indicates for each buffer whether
to consider it or not when called with that buffer current.

See `save-some-buffers-action-alist' if you want to
change the additional actions you can take on files."
    (interactive "P")
    (save-window-excursion
      (let* (queried autosaved-buffers
                     files-done abbrevs-done)
        (dolist (buffer (buffer-list))
          ;; First save any buffers that we're supposed to save unconditionally.
          ;; That way the following code won't ask about them.
          (with-current-buffer buffer
            (when (and buffer-save-without-query (buffer-modified-p))
              (push (buffer-name) autosaved-buffers)
              (save-buffer))))
        ;; Ask about those buffers that merit it,
        ;; and record the number thus saved.
        (setq files-done
              (map-y-or-n-p
               (lambda (buffer)
                 ;; Note that killing some buffers may kill others via
                 ;; hooks (e.g. Rmail and its viewing buffer).
                 (and (buffer-live-p buffer)
                      (buffer-modified-p buffer)
                      (not (buffer-base-buffer buffer))
                      (or
                       (buffer-file-name buffer)
                       (and pred
                            (progn
                              (set-buffer buffer)
                              (and buffer-offer-save (> (buffer-size) 0)))))
                      (or (not (functionp pred))
                          (with-current-buffer buffer (funcall pred)))
                      (if arg
                          t
                        (setq queried t)
                        (if (buffer-file-name buffer)
                            (progn (my:display-buffer-modification buffer)
                                   (format "Save file %s? "
                                           (buffer-file-name buffer)))
                          (format "Save buffer %s? "
                                  (buffer-name buffer))))))
               (lambda (buffer)
                 (with-current-buffer buffer
                   (save-buffer)))
               (buffer-list)
               '("buffer" "buffers" "save")
               save-some-buffers-action-alist))
        ;; Maybe to save abbrevs, and record whether
        ;; we either saved them or asked to.
        (and save-abbrevs abbrevs-changed
             (progn
               (if (or arg
                       (eq save-abbrevs 'silently)
                       (y-or-n-p (format "Save abbrevs in %s? " abbrev-file-name)))
                   (write-abbrev-file nil))
               ;; Don't keep bothering user if he says no.
               (setq abbrevs-changed nil)
               (setq abbrevs-done t)))
        (or queried (> files-done 0) abbrevs-done
            (cond
             ((null autosaved-buffers)
              (message "(No files need saving)"))
             ((= (length autosaved-buffers) 1)
              (message "(Saved %s)" (car autosaved-buffers)))
             (t
              (message "(Saved %d files: %s)"
                       (length autosaved-buffers)
                       (mapconcat 'identity autosaved-buffers ", "))))))))
(eval-after-load 'files
  `(defalias 'save-some-buffers 'my:save-some-buffers))

이제 수정 내용을 저장할꺼냐고 물어보는 프롬프트와 동시에 저장되지 않은 수정 내용이 떠 주니 한결 편하다. 어쩌다 오타로 들어간 것들은 바로바로 무시해 줄 수 있고. 쓰다 보면 my:display-buffer-modification을 쓸 만한 다른 케이스들도 나올 것 같긴 한데, 일단은 이 두가지만 해줘도 아주 좋은 듯.