emacs-devel
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

Proposal for a closed-buffer tracker


From: Kelly Dean
Subject: Proposal for a closed-buffer tracker
Date: Sun, 22 Feb 2015 04:11:54 +0000

Below is code for a closed-buffer tracker. It lets you reopen closed buffers, 
and restores the major mode, minor modes, point, mark, mark ring, and other 
buffer-local variables returned by the function desktop-buffer-info. Currently, 
it's implemented only for file-visiting buffers. It's comparable to the «closed 
tabs» feature of modern web browsers, and useful for the same reasons.

Note that although it uses functions in desktop.el, it doesn't require 
desktop-save-mode to be enabled; the two operate independently. Also note, it 
relies on a recent patch to trunk; it won't work on 24.4 or 24.5. And the 
«silently» macro is generated from second-level just because I happen to use 
the generator for other things not included here.

Stefan suggested I submit this feature as a patch. If other people might find 
it useful, should it go into desktop.el? Or perhaps GNU Elpa? Currently it's 
just part of a larger convenience package at:
http://prtime.org/emacs/usablizer.html

The «silently» macro and its generator are in another package (that Usablizer 
requires) at:
http://prtime.org/emacs/vimizer.html


;;; Utilities

(defmacro dlet (binders &rest body)
  "Like `let', but always bind dynamically, even if `lexical-binding' is t.
Uses the local-specialness feature of `defvar'."
  (unless (listp binders) (error "%S is not a list" binders))
  ;; Contain the local-specialness, so it doesn't infect «let»s outside dlet,
  ;; because the purpose of local-specialness is to avoid global infection.
  `(progn
     ,@(let (vardefs) ; Generate all the «defvar»s
         (dolist (binder binders (nreverse vardefs))
           (cond ((symbolp binder)
                  (push `(defvar ,binder) vardefs))
                 ((and (listp binder)
                       (symbolp (car binder)))
                  (push `(defvar ,(car binder)) vardefs))
                 (t (error "%S is not a symbol or list" binder)))))
     (let ,binders ,@body)))

(defmacro define-function-suppressor (wrapper victim docstring)
  "Make a macro named WRAPPER (a symbol), with DOCSTRING, that takes a body
and evaluates it with function VICTIM suppressed."
  `(defmacro ,wrapper (&rest body) ,docstring
             `(cl-letf (((symbol-function ',',victim) (lambda (&rest _dummy) 
())))
                ,@body)))

(define-function-suppressor silently message
  "Do BODY without messaging anything.")

;; Copied from assq-delete-all in subr.el, but «eq» replaced by «equal»
(defun assoc-delete-all (key alist)
  "Delete from ALIST all elements whose car is `equal' to KEY.
Return the modified alist.
Elements of ALIST that are not conses are ignored."
  (while (and (consp (car alist))
              (equal (car (car alist)) key))
    (setq alist (cdr alist)))
  (let ((tail alist) tail-cdr)
    (while (setq tail-cdr (cdr tail))
      (if (and (consp (car tail-cdr))
               (equal (car (car tail-cdr)) key))
          (setcdr tail (cdr tail-cdr))
        (setq tail tail-cdr))))
  alist)


;;; Closed-buffer tracker. Inspired by:
;;; 
http://stackoverflow.com/questions/2227401/how-to-get-a-list-of-last-closed-files-in-emacs

(defvar closed-buffer-history nil
  "Reverse chronological list of closed buffers.
This list stores filenames and/or full buffer states as stored by
`desktop-save-mode', including point, mark, and various other buffer-local
variables.
The list size is limited by `closed-buffer-history-max-saved-items' and
`closed-buffer-history-max-full-items'.
When a buffer already in the list is closed again, it's moved to the head of
the list.")

(defvar closed-buffer-history-max-saved-items 1000
  "Max items to save on `closed-buffer-history' list.
Use -1 for unlimited, or zero to disable tracking closed files.
If disabled after having been enabled, `closed-buffer-history' will retain
the list from when it was enabled, even though no new items will be added to
it. To clear the list, set it to nil.
See also `closed-buffer-history-max-full-items'.")

(defvar closed-buffer-history-max-full-items 100
  "Max full items to save on `closed-buffer-history' list.
Use -1 for unlimited, or zero to disable tracking of full items. If this
limit is less than `closed-buffer-history-max-saved-items', then non-full
items will be stored for the difference. If this limit is greater, then
`closed-buffer-history-max-saved-items' is the controlling limit. When new
items are added to `closed-buffer-history', full items which exceed this
limit are converted to non-full items. The purpose of that is to save space.
 A full item is a buffer state, including `buffer-file-name', `point',
`mark', `mark-ring', `major-mode', minor modes, and various other
buffer-local variables as configured for `desktop-save-mode', but excluding
the buffer contents, which are stored only in the named file. A non-full
item is just a file name.")

(defun untrack-closed-buffer (name)
  ;; Could be just name, or info list; delete in either case
  (setq closed-buffer-history
        (delete name
                (assoc-delete-all name closed-buffer-history))))

(defun track-closed-buffer ()
  (when (and buffer-file-name (not (= closed-buffer-history-max-saved-items 0)))
    ;; Remove from not-head of list
    (untrack-closed-buffer buffer-file-name)
    ;; Add to head of list
    (pushnew (if (desktop-save-buffer-p buffer-file-name (buffer-name) 
major-mode)
                 (cdr (save-current-buffer
                        (desktop-buffer-info (current-buffer))))
               buffer-file-name)
             closed-buffer-history)
    ;; Truncate excess elements
    (let* ((max-full closed-buffer-history-max-full-items)
           (max-saved closed-buffer-history-max-saved-items)
           (truncatees (nthcdr max-saved closed-buffer-history))
           demotees)
      (and (> max-saved 0) truncatees (setcdr truncatees nil))
      (unless (< max-full 0)
        (setq demotees (nthcdr max-full closed-buffer-history))
        ;; Demote buffer info lists to filenames.
        (letrec ((demote (lambda (x) (when (and (consp x) (consp (car x)))
                                       (setcar x (caar x)) (funcall demote (cdr 
x))))))
          (funcall demote demotees))))))

(defun reopen-buffer (name &optional remove-missing select)
  "Open file, and restore buffer state if recorded in `closed-buffer-history'.
Return buffer for the opened file, or nil if not listed in 
`closed-buffer-history'.

If unable to open file, then remove from `closed-buffer-history' if confirmed
interactively or REMOVE-MISSING is non-nil, or signal error if it is
nil and reopen-buffer was not called interactively.

If called interactively, or SELECT is non-nil, then switch to the buffer."
  (interactive
   (list (ido-completing-read "Last closed: "
                              (mapcar (lambda (x) (if (consp x) (car x) x))
                                      closed-buffer-history)
                              nil t) nil t))
  (let* ((bufinfo (assoc name closed-buffer-history))
         (bufinfo (or bufinfo (if (memq name closed-buffer-history)
                                  (make-list 8 nil)))))
    (assert bufinfo)
    ;;Load from info list, using base filename as new buffer name.
    (let ((buf
           ;; Set variables needed by desktop-create-buffer.
           ;; Need dlet because they're not globally special, but only locally
           ;; special in desktop.el, which according to Stefan, is not weird.
           (dlet ((desktop-buffer-ok-count 0)
                  (desktop-buffer-fail-count 0)
                  desktop-first-buffer)
                 (silently ; Silence desktop-restore-file-buffer if file can't 
be found
                  (apply 'desktop-create-buffer (string-to-number 
desktop-file-version)
                         name (file-name-nondirectory name) (cddr bufinfo))))))
      (if buf (progn
                (untrack-closed-buffer name)
                (with-current-buffer buf (run-hooks 'desktop-delay-hook))
                (setq desktop-delay-hook nil)
                (when select
                  ;; 3 lines copied from desktop-restore-file-buffer in 
desktop.el
                  (condition-case nil
                      (switch-to-buffer buf)
                    (error (pop-to-buffer buf))))
                buf)
        (when (or remove-missing
                  (and
                   (called-interactively-p 'any)
                   (y-or-n-p (format
                              "Failed to open file %s; remove from 
closed-buffer-history? "
                              name))))
          (untrack-closed-buffer name))
        (unless (or remove-missing (called-interactively-p 'any))
          (error "Failed to open file %s" name))))))

(add-hook 'kill-buffer-hook #'track-closed-buffer)
(global-set-key [S-XF86Close] #'reopen-buffer)



reply via email to

[Prev in Thread] Current Thread [Next in Thread]