branch: externals/bufferlo commit 41e4786fd7396391ddff9f4e2fc5c6cc40928609 Author: shipmints <shipmi...@gmail.com> Commit: Flo Rommel <m...@florommel.de>
Final stretch 1.1-pre WIP. - Correct 'bufferlo-kill-buffers' and 'bufferlo-kill-orphan-buffers' 'internal-too' and hidden buffer filtering (the logic had been inverted). - Refine 'bufferlo--bookmark-tab-handler' terminology from success/failure to restored/skipped. - Improve bookmark auto save handling to avoid timer reentrancy issues and improve handling bookmark.el prompts which should also address minibuffer prompts changing focus. - Remove arguments from 'bufferlo--bookmark-tab-make' and 'bufferlo--bookmark-frame-make' (that code was unused). - Improve 'bufferlo--kill-buffer-safely' to avoid spurious window creation by kill-buffer when the buffer is on the sole window of a frame. - Refine 'bufferlo--close-active-bookmarks' tab-tag logic to tag only tabs on bookmarked tabs. - README corrected default for 'bufferlo-bookmark-tab-failed-buffer-policy'. - General typos, docstring improvement, and some formatting improvements. - flymake/byte compiler noise. --- README.org | 4 +- bufferlo.el | 325 ++++++++++++++++++++++++++++++++---------------------------- 2 files changed, 174 insertions(+), 155 deletions(-) diff --git a/README.org b/README.org index cceff0f1f1..f1d882d6d4 100644 --- a/README.org +++ b/README.org @@ -587,11 +587,11 @@ Note: 'raise is considered to act as 'clear by bookmark set loading. #+end_src #+begin_src emacs-lisp ;; handle buffer bookmarks that could not be restored - (setq bufferlo-bookmark-tab-failed-buffer-policy 'placeholder) ; placeholder buffer and buffer name; the default + (setq bufferlo-bookmark-tab-failed-buffer-policy nil) ; ignore; the default + (setq bufferlo-bookmark-tab-failed-buffer-policy 'placeholder) ; placeholder buffer and buffer name (setq bufferlo-bookmark-tab-failed-buffer-policy 'placeholder-orig) ; placeholder buffer with original buffer name (setq bufferlo-bookmark-tab-failed-buffer-policy "*scratch*") ; default to a specific buffer (setq bufferlo-bookmark-tab-failed-buffer-policy #'my/failed-bookmark-handler) ; function to call that returns a buffer - (setq bufferlo-bookmark-tab-failed-buffer-policy nil) ; ignore #+end_src *** Bookmark set options diff --git a/bufferlo.el b/bufferlo.el index 0ddfd422b6..35be9efb28 100644 --- a/bufferlo.el +++ b/bufferlo.el @@ -27,30 +27,27 @@ ;;; Commentary: -;; With bufferlo, every frame and tab (i.e., tab-bar-mode tab) has an -;; additional manageable local buffer list. A buffer is added to the -;; local buffer list when displayed in the frame/tab (e.g., by opening a -;; new file in the tab or by switching to the buffer from the global -;; buffer list). - -;; Using Emacs's built-in buffer-list frame parameter, bufferlo -;; integrates seamlessly with all standard frame and tab management -;; facilities, including undeletion of frames and tabs, tab duplication -;; and moving, frame cloning, and session persistence with desktop.el -;; (though bufferlo frame and tab bookmarks offer an alternative -;; persistence method). - -;; Bufferlo provides extensive management functions for its local lists -;; and offers features on top of switch-buffer functions, buffer menu, -;; and ibuffer. You can configure any command that selects a buffer to -;; use the local buffer list via bufferlo-anywhere-mode. - -;; In addition, bufferlo offers lightweight Emacs bookmarks-based -;; persistence for frames, tabs, and sets of frames/tabs to help you -;; manage your transient workflows. Bufferlo bookmarks are compatible -;; with built-in features such as bookmark-bmenu-list and third-party -;; packages such as consult which offers consult-bookmark for interactive -;; bookmark selection. +;; With bufferlo, every frame and tab (i.e., `tab-bar-mode' tab) has an +;; additional manageable local buffer list. A buffer is added to the local +;; buffer list when displayed in the frame/tab (e.g., by opening a new file in +;; the tab or by switching to the buffer from the global buffer list). + +;; Using Emacs's built-in buffer-list frame parameter, bufferlo integrates +;; seamlessly with all standard frame and tab management facilities, including +;; undeletion of frames and tabs, tab duplication and moving, frame cloning, +;; and session persistence with `desktop' (though bufferlo frame and tab +;; bookmarks offer an alternative persistence method). + +;; Bufferlo provides extensive management functions for its local lists and +;; offers features on top of switch-buffer functions, buffer menu, and +;; `ibuffer'. You can configure any command that selects a buffer to use the +;; local buffer list via bufferlo-anywhere-mode. + +;; In addition, bufferlo offers lightweight Emacs bookmarks-based persistence +;; for frames, tabs, and sets of frames/tabs to help you manage your transient +;; workflows. Bufferlo bookmarks are compatible with built-in features such +;; as `bookmark-bmenu-list' and third-party packages such as consult which +;; offers consult-bookmark for interactive bookmark selection. ;;; Code: @@ -612,8 +609,8 @@ Each function takes the following arguments: bookmark-name: source bookmark name effective-bookmark-name: nil, if tab bookmark cleared tab: the handled tab - succ-buffer-names: list of buffer names successfully restored - fail-buffer-names: list of buffer names that could not be restored" + restored-buffer-names: list of restored buffer names + skipped-buffer-names: list of skipped buffer names" :package-version '(bufferlo . "1.1") :type 'hook) @@ -659,7 +656,7 @@ This is controlled by `bufferlo-bookmarks-auto-save-interval'.") (setq bufferlo--bookmarks-auto-save-timer (run-with-timer bufferlo-bookmarks-auto-save-interval - bufferlo-bookmarks-auto-save-interval + nil ; We reschedule in `bufferlo-bookmarks-save'. #'bufferlo-bookmarks-save)))) (defcustom bufferlo-bookmarks-auto-save-interval 0 @@ -1532,7 +1529,7 @@ advised functions. Honors `bufferlo-bookmark-frame-duplicate-policy'." (throw :abort t)) ('raise ;; NOTE: We throw nil here! - ;; We delete the frame ourselfs before raising. + ;; We delete the frame ourselves before raising. (delete-frame) (bufferlo--bookmark-raise abm) (throw :abort nil))) @@ -1573,7 +1570,7 @@ Honors `bufferlo-bookmark-tab-duplicate-policy'." (throw :abort t)) ('raise ;; NOTE: We throw nil here! - ;; We delete the frame ourselfs before raising. + ;; We delete the frame ourselves before raising. (let (tab-bar-tab-prevent-close-functions) (tab-bar-close-tab)) ;; Find bookmark to raise; tab numbers changes when closing. @@ -1740,11 +1737,12 @@ If BUFFER is nil, the current buffer is killed." (when (and (one-window-p 'no-mini) (eq (window-deletable-p) 'frame)) ;; Kill the requested buffer but leave one live window on a "hidden" - ;; buffer. The caller will close the tab or frame under its control. - (switch-to-buffer-other-window " *bufferlo temp*")) + ;; buffer. The caller may close the tab or frame under its control. + (switch-to-buffer " *bufferlo temp*" 'norecord 'force-same-window)) (let ((frame-auto-hide-function) ; inhibit automatic frame deletion ;; No interference for buffer replacement selection - (switch-to-prev-buffer-skip)) + (switch-to-prev-buffer-skip) + (switch-to-prev-buffer-skip-regexp)) (kill-buffer buffer))) (defun bufferlo--kill-buffer-forced (buffer) @@ -1797,8 +1795,8 @@ argument INTERNAL-TOO is non-nil." (buffers (seq-filter (lambda (b) (not (and - (or internal-too - (not (string-prefix-p " " (buffer-name b)))) + (and (not internal-too) + (string-prefix-p " " (buffer-name b))) (string-match-p exclude (buffer-name b))))) kill-list))) (dolist (b buffers) @@ -1818,8 +1816,8 @@ Buffers matching `bufferlo-kill-buffers-exclude-filters' are never killed." (buffers (seq-filter (lambda (b) (not (and - (or internal-too - (not (string-prefix-p " " (buffer-name b)))) + (and (not internal-too) + (string-prefix-p " " (buffer-name b))) (string-match-p exclude (buffer-name b))))) (bufferlo--get-orphan-buffers)))) (dolist (b buffers) @@ -2388,23 +2386,20 @@ local buffer list to use. If it is nil, the current frame is used." (mapcar #'bufferlo--bookmark-get-for-buffer buffers))) -(defun bufferlo--bookmark-tab-make (&optional frame) - "Get the bufferlo tab bookmark for the current tab in FRAME. -FRAME specifies the frame; the default value of nil selects the current frame." - (setq frame (or frame (selected-frame))) - (with-selected-frame frame - (let ((filtered-buffers - (bufferlo--bookmark-filter-buffers)) - (current-tab (bufferlo--current-tab))) - `((tab-explicit-name . ,(when (alist-get 'explicit-name current-tab) - (alist-get 'name current-tab))) - (tab-group . ,(alist-get 'group current-tab)) - (buffer-bookmarks . ,(bufferlo--bookmark-get-for-buffers-in-tab +(defun bufferlo--bookmark-tab-make () + "Make the tab bookmark record for the current frame and tab." + (let ((filtered-buffers + (bufferlo--bookmark-filter-buffers)) + (current-tab (bufferlo--current-tab))) + `((tab-explicit-name . ,(when (alist-get 'explicit-name current-tab) + (alist-get 'name current-tab))) + (tab-group . ,(alist-get 'group current-tab)) + (buffer-bookmarks . ,(bufferlo--bookmark-get-for-buffers-in-tab + filtered-buffers)) + (buffer-list . ,(mapcar #'buffer-name filtered-buffers)) - (buffer-list . ,(mapcar #'buffer-name - filtered-buffers)) - (window . ,(window-state-get (frame-root-window) 'writable)) - (handler . ,#'bufferlo--bookmark-tab-handler))))) + (window . ,(window-state-get (frame-root-window) 'writable)) + (handler . ,#'bufferlo--bookmark-tab-handler)))) (defun bufferlo--ws-replace-buffer-names (ws replace-alist) "Replace buffer names according to REPLACE-ALIST in the window state WS." @@ -2438,13 +2433,13 @@ and quit which are cumbersome during set loading.") "Get the duplicate policy for THING BOOKMARK-NAME. THING should be either \"frame\" or \"tab\". Ask the user if DEFAULT-POLICY is set to \\='prompt. -MODE can be one of \\='load \\='save \\='undelete, depending on the invoking -action. -EMBEDDED-TAB is non-nil if the tab bookmark is embedded in a frame bookmark. -This functions throws :abort when the user quits. +MODE can be one of \\='load \\='save \\='undelete, depending on the +invoking action. +EMBEDDED-TAB is non-nil if the tab bookmark is embedded in a frame +bookmark. This functions throws :abort when the user quits. -The varaible `bufferlo--bookmark-set-loading' should be non-nil if the function -is invoked as part of a bookmark set restoration. +The variable `bufferlo--bookmark-set-loading' should be non-nil if the +function is invoked as part of a bookmark set restoration. The functions presents the user with the following options: allow, clear, ignore, raise, help, quit @@ -2607,6 +2602,9 @@ invoking action. This functions throws :abort when the user quits." (_ (throw :abort t))))) (defun bufferlo--bookmark-insert-placeholer (orig-name) + "`bufferlo--bookmark-tab-handler' helper function. +Use ORIG-NAME to create a placeholder buffer for buffers that failed to +restore." (let ((buffer-existed (get-buffer orig-name)) (fail-buffer (cond @@ -2665,8 +2663,8 @@ Returns nil on success, non-nil on abort." (orig-bookmark-name bookmark-name) (abm (assoc bookmark-name (bufferlo--active-bookmarks))) (disconnect-tbm-p) - (succ-buffer-names) - (fail-buffer-names) + (restored-buffer-names) + (skipped-buffer-names) (msg) (msg-append (lambda (s) (setq msg (concat msg "; " s))))) @@ -2743,7 +2741,7 @@ Returns nil on success, non-nil on abort." (progn (funcall (or (bookmark-get-handler record) 'bookmark-default-handler) - record) + record) (run-hooks 'bookmark-after-jump-hook) nil) (error @@ -2757,8 +2755,8 @@ Returns nil on success, non-nil on abort." (if restore-failed (progn (bufferlo--bookmark-insert-placeholer orig-name) - (push orig-name fail-buffer-names)) - (push orig-name succ-buffer-names)) + (push orig-name skipped-buffer-names)) + (push orig-name restored-buffer-names)) (unless (eq (current-buffer) dummy) ;; Return a list of (cons <string> <buffer>). @@ -2819,8 +2817,8 @@ Returns nil on success, non-nil on abort." bookmark-name (unless disconnect-tbm-p bookmark-name) (bufferlo--current-tab) - succ-buffer-names - fail-buffer-names) + restored-buffer-names + skipped-buffer-names) buffer)))) (add-hook 'bookmark-after-jump-hook bm-after-jump-hook-sym -99) (when not-jump @@ -2828,29 +2826,34 @@ Returns nil on success, non-nil on abort." ;; Log message (unless (or no-message bufferlo--bookmark-handler-no-message) - (message "Restored bufferlo tab bookmark%s%s" + (message "Restored bufferlo tab bookmark%s%s%s%s" (if orig-bookmark-name (format ": %s" orig-bookmark-name) "") - (or msg ""))) - ; explicitly return success; abort returns non-nil + (or msg "") + (if restored-buffer-names + (format " (%s)" + (mapconcat #'identity restored-buffer-names ", ")) "") + (if skipped-buffer-names + (format " (skipped: %s)" + (mapconcat #'identity skipped-buffer-names ", ")) ""))) + ;; Explicitly return success; abort returns non-nil nil))) ;; We use a short name here as bookmark-bmenu-list hard codes width of 8 chars (put #'bufferlo--bookmark-tab-handler 'bookmark-handler-type "B-Tab") (put #'bufferlo--bookmark-tab-handler 'bookmark-inhibit 'insert) -(defun bufferlo--bookmark-frame-make (&optional frame) - "Make a bufferlo frame bookmark. -FRAME specifies the frame; the default value of nil selects the current frame." - (let* ((tabs (funcall tab-bar-tabs-function frame)) - (orig-tab (1+ (tab-bar--current-tab-index tabs frame))) - (tab-bar-tab-post-select-functions) - (tabs-to-bookmark)) +(defun bufferlo--bookmark-frame-make () + "Make a bufferlo frame bookmark record for the current frame." + (let ((tabs (funcall tab-bar-tabs-function)) + (orig-tab (1+ (tab-bar--current-tab-index))) + (tab-bar-tab-post-select-functions) + tabs-to-bookmark) (dotimes (i (length tabs)) (tab-bar-select-tab (1+ i)) (let* ((curr (alist-get 'current-tab tabs)) (name (alist-get 'name curr)) (explicit-name (alist-get 'explicit-name curr)) - (tbm (bufferlo--bookmark-tab-make frame))) + (tbm (bufferlo--bookmark-tab-make))) (if explicit-name (push (cons 'tab-name name) tbm) (push (cons 'tab-name nil) tbm)) @@ -2859,7 +2862,7 @@ FRAME specifies the frame; the default value of nil selects the current frame." `((tabs . ,(reverse tabs-to-bookmark)) (current . ,orig-tab) (bufferlo--frame-geometry . ,(funcall bufferlo-frame-geometry-function - (or frame (selected-frame)))) + (selected-frame))) (handler . ,#'bufferlo--bookmark-frame-handler)))) (defun bufferlo--bookmark-frame-get-load-policy () @@ -3738,19 +3741,21 @@ This closes their associated bookmarks and kills their buffers." (assq 'current-tab (funcall tab-bar-tabs-function nil)))) (defun bufferlo--bookmark-tab-save (name &optional no-overwrite no-message msg) - "Save the current tab as a bookmark. +"Save the current tab as a bookmark. NAME is the bookmark's name. If NO-OVERWRITE is non-nil, record the new bookmark without throwing away the old one. NO-MESSAGE inhibits the save status message. If MSG is non-nil, it is added -to the save message." - (bookmark-store name (bufferlo--bookmark-set-location - (bufferlo--bookmark-tab-make)) - no-overwrite) - (setf (alist-get 'bufferlo-bookmark-tab-name - (cdr (bufferlo--current-tab))) - name) - (unless no-message - (message "Saved bufferlo tab bookmark: %s%s" name (if msg msg "")))) +to the save message. + +This function operates on the current frame and its current tab." +(bookmark-store name (bufferlo--bookmark-set-location + (bufferlo--bookmark-tab-make)) + no-overwrite) +(setf (alist-get 'bufferlo-bookmark-tab-name + (cdr (bufferlo--current-tab))) + name) +(unless no-message + (message "Saved bufferlo tab bookmark: %s%s" name (if msg msg "")))) (defun bufferlo-bookmark-tab-save (name &optional no-overwrite no-message) "Save the current tab as a bookmark. @@ -4071,10 +4076,16 @@ Equality test is \\='equal." (defun bufferlo--bookmarks-save (active-bookmark-names active-bookmarks &optional no-message) "Save the bookmarks in ACTIVE-BOOKMARK-NAMES indexed by ACTIVE-BOOKMARKS. Specify NO-MESSAGE to inhibit the bookmark save status message." - (let ((bookmarks-saved nil) - (start-time (current-time))) - ;; Inhibit built-in bookmark file saving until we're done - (let ((bookmark-save-flag nil)) + (let (bookmarks-saved) + ;; Offer to reload before we update bookmark entries. NOTE: There is + ;; still a potential race condition if the bookmark file changes via + ;; another process during this loop and the user will be prompted below + ;; when `bookmark-save' is called so we inhibit loading again around that + ;; call. + (bookmark-maybe-load-default-file) + ;; Inhibit built-in bookmark file saving and reloading until we're done. + (let (bookmark-save-flag + bookmark-watch-bookmark-file) (dolist (abm-name active-bookmark-names) (when-let* ((abm (assoc abm-name active-bookmarks)) (abm-type (alist-get 'type (cadr abm))) @@ -4085,7 +4096,7 @@ Specify NO-MESSAGE to inhibit the bookmark save status message." (bufferlo--bookmark-frame-save abm-name nil t)) ((eq abm-type 'tbm) (let ((orig-tab-number (1+ (tab-bar--current-tab-index))) - (tab-bar-tab-post-select-functions)) + tab-bar-tab-post-select-functions) (tab-bar-select-tab (alist-get 'tab-number (cadr abm))) (bufferlo--bookmark-tab-save abm-name nil t) (tab-bar-select-tab orig-tab-number)))) @@ -4095,10 +4106,12 @@ Specify NO-MESSAGE to inhibit the bookmark save status message." (let ((inhibit-message (or no-message (not (memq bufferlo-bookmarks-auto-save-messages (list 'saved t)))))) - (bookmark-save) - (message "Saved bufferlo bookmarks: %s, in %.2f second(s)" - (mapconcat 'identity bookmarks-saved ", ") - (float-time (time-subtract (current-time) start-time))))) + (let (bookmark-watch-bookmark-file ; see NOTE: above + (start-time (current-time))) + (bookmark-save) + (message "Saved bufferlo bookmarks: %s, in %.2f second(s)" + (mapconcat 'identity bookmarks-saved ", ") + (float-time (time-subtract (current-time) start-time)))))) (t (when (and (not no-message) (memq bufferlo-bookmarks-auto-save-messages @@ -4127,61 +4140,65 @@ be saved will take precedence. Duplicate bookmarks are handled according to `bufferlo-bookmarks-save-duplicates-policy'." (interactive) - (catch :abort - (let* ((frames (if all - (frame-list) - (pcase bufferlo-bookmarks-save-frame-policy - ('current - (list (selected-frame))) - ('other - (seq-filter (lambda (x) (not (eq x (selected-frame)))) - (frame-list))) - (_ (frame-list))))) - ;; Get the active bookmarks for the frames captured by the current - ;; bufferlo-bookmarks-save-frame-policy only - (abms (bufferlo--active-bookmarks frames)) - - ;; Override bufferlo-bookmarks-save-predicate-functions on prefix arg - (bufferlo-bookmarks-save-predicate-functions - (if (or all (consp current-prefix-arg)) - (list #'bufferlo-bookmarks-save-all-p) - bufferlo-bookmarks-save-predicate-functions)) - - ;; Filter the bookmark names to save - (abm-names-to-save - (seq-filter (lambda (x) (not (null x))) - (mapcar - (lambda (abm) - (let ((abm-name (car abm))) - (when (run-hook-with-args-until-success - 'bufferlo-bookmarks-save-predicate-functions - abm-name) - abm-name))) - abms))) - - ;; There may be open bookmarks that are duplicates - (dupes-to-save (bufferlo--list-duplicates abm-names-to-save)) - ;; We'll handle these bookmarks according to the duplicate-policy - (duplicate-policy bufferlo-bookmarks-save-duplicates-policy)) - - (when (> (length dupes-to-save) 0) - (when (eq duplicate-policy 'prompt) - (pcase (with-local-quit - (read-answer - (format "Duplicate active bookmarks %s: Allow to save, Disallow to cancel " - dupes-to-save) - '(("allow" ?a "Allow duplicate") - ("disallow" ?d "Disallow duplicates; cancel saving") - ("help" ?h "Help") - ("quit" ?q "Quit with no changes")))) - ("allow" (setq duplicate-policy 'allow)) - ("disallow" (setq duplicate-policy 'disallow)) - (_ (throw :abort t)))) - (pcase duplicate-policy - ('allow) - (_ (throw :abort t)))) - - (bufferlo--bookmarks-save abm-names-to-save abms)))) + (unwind-protect ; restart the timer in case the user quits any prompts + (catch :abort + (let* ((frames (if all + (frame-list) + (pcase bufferlo-bookmarks-save-frame-policy + ('current + (list (selected-frame))) + ('other + (seq-filter (lambda (x) (not (eq x (selected-frame)))) + (frame-list))) + (_ (frame-list))))) + ;; Get the active bookmarks for the frames captured by the current + ;; bufferlo-bookmarks-save-frame-policy only + (abms (bufferlo--active-bookmarks frames)) + + ;; Override bufferlo-bookmarks-save-predicate-functions on prefix arg + (bufferlo-bookmarks-save-predicate-functions + (if (or all (consp current-prefix-arg)) + (list #'bufferlo-bookmarks-save-all-p) + bufferlo-bookmarks-save-predicate-functions)) + + ;; Filter the bookmark names to save + (abm-names-to-save + (seq-filter (lambda (x) (not (null x))) + (mapcar + (lambda (abm) + (let ((abm-name (car abm))) + (when (run-hook-with-args-until-success + 'bufferlo-bookmarks-save-predicate-functions + abm-name) + abm-name))) + abms))) + + ;; There may be open bookmarks that are duplicates + (dupes-to-save (bufferlo--list-duplicates abm-names-to-save)) + ;; We'll handle these bookmarks according to the duplicate-policy + (duplicate-policy bufferlo-bookmarks-save-duplicates-policy)) + + (when (> (length dupes-to-save) 0) + (when (eq duplicate-policy 'prompt) + (pcase (with-local-quit + (read-answer + (format "Duplicate active bookmarks %s: Allow to save, Disallow to cancel " + dupes-to-save) + '(("allow" ?a "Allow duplicate") + ("disallow" ?d "Disallow duplicates; cancel saving") + ("help" ?h "Help") + ("quit" ?q "Quit with no changes")))) + ("allow" (setq duplicate-policy 'allow)) + ("disallow" (setq duplicate-policy 'disallow)) + (_ (throw :abort t)))) + (pcase duplicate-policy + ('allow) + (_ (throw :abort t)))) + + (bufferlo--bookmarks-save abm-names-to-save abms))) + ;; Run the timer again only after this function is complete to avoid race + ;; conditions with user prompts. + (bufferlo--bookmarks-auto-save-timer-maybe-start))) (defun bufferlo-bookmark--frame-save-on-delete (frame) "`frame-delete' advice for saving the current frame bookmark on deletion. @@ -4439,10 +4456,12 @@ which defaults to all frames, if not specified." ;; They are removed in that pass. If the user quits when prompted, the ;; tags are cleared on all tabs before being reassigned in the first ;; pass.. - ;; Clear any lingering tab tags. + ;; + ;; Clear any lingering tab tags on bookmarked tabs. (dolist (frame (frame-list)) (dolist (tab (funcall tab-bar-tabs-function frame)) - (setf (alist-get 'bufferlo--tab-tag (cdr tab)) nil))) + (when (alist-get 'bufferlo-bookmark-tab-name tab) + (setf (alist-get 'bufferlo--tab-tag (cdr tab)) nil)))) ;; Now assign fresh tab tags. (let ((tab-tag 0)) (dolist (tbm tbms)