branch: externals/bufferlo commit e88c3670b21d01e0ebc04b4ab763038e969a0eb6 Author: shipmints <shipmi...@gmail.com> Commit: shipmints <shipmi...@gmail.com>
WIP --- bufferlo.el | 369 +++++++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 266 insertions(+), 103 deletions(-) diff --git a/bufferlo.el b/bufferlo.el index 7fdceb07fe..55badc7d1d 100644 --- a/bufferlo.el +++ b/bufferlo.el @@ -98,7 +98,7 @@ Matching buffers are hidden even if displayed in the current frame or tab." This is a list of regular expressions that match buffer names." :type '(repeat string)) -(defcustom bufferlo-bookmark-buffers-exclude-filters ; TODO: +++ +(defcustom bufferlo-bookmark-buffers-exclude-filters (list (rx "*Messages*") (rx "*scratch*") @@ -118,12 +118,12 @@ This is a list of regular expressions that match buffer names." (rx "*timer-list*") (rx "*cvs*") (rx "*esh command on file*")) - "Buffers that should be excluded in Bufferlo bookmarks. + "Buffers that should be excluded from being stored bufferlo bookmarks. This is a list of regular expressions to filter buffer names." :type '(repeat string)) (defcustom bufferlo-bookmark-frame-load-make-frame nil - "If non-nil, a new frame is created before loading frame bookmarks." + "If non-nil, create a new frame to hold a loaded frame bookmark." :type 'boolean) (defcustom bufferlo-delete-frame-kill-buffers-save-bookmark-prompt nil @@ -136,16 +136,16 @@ and its buffers." :type 'boolean) (defcustom bufferlo-bookmark-frame-load-policy 'prompt - "Behavior when a frame bookmark is loaded into an -already-bookmarked frame. \\='prompt asks you to pick a policy. -\\='disallow prevents accidental overlays on existing bookmarked -frames, with the exception that a bookmarked frame may be -reloaded to restore its state. \\='current replaces the frame -content using the existing frame bookmark name. \\='replace replaces -the new content and adopts the new bookmark name. \\='merge adds the -new tabs to the existing frame retaining the existing bookmark -name. This policy is d useful when -\\=`bufferlo-bookmark-frame-load-make-frame\\=' is not enabled or frame + "Control loading a frame bookmark into a already-bookmarked frame. +\\='prompt offers interactive policy selection. \\='disallow +prevents accidental overlays on already-bookmarked frames, with +the exception that a bookmarked frame may be reloaded to restore +its state. \\='current replaces the frame content using the +existing frame bookmark name. \\='replace replaces the frame +content and adopts the new bookmark name. \\='merge adds new +frame bookmark tabs to the existing frame, retaining the existing +bookmark name. This policy is useful when +`bufferlo-bookmark-frame-load-make-frame' is not enabled or frame loading is not overridden with a prefix argument that suppresses making a new frame." :type '(radio (const :tag "Prompt" prompt) @@ -155,53 +155,89 @@ making a new frame." (const :tag "Merge" merge))) (defcustom bufferlo-bookmark-frame-duplicate-policy 'prompt - "Behavior controlling duplicate active frame bookmarks. One -typically does not want to save the same bookmark with content -that may differ among frames. \\='prompt asks you to pick a policy. -\\='allow will allow duplicates. \\='raise will locate the frame with -the existing bookmark and raise its frame." + "Control duplicate active frame bookmarks. +Duplicate active bookmarks cause potentially confusing race +conditions where the most recently saved bookmark wins. +\\='prompt asks you to pick a policy. \\='allow will allow +duplicates. \\='raise will locate the frame with the existing +bookmark and raise its frame." :type '(radio (const :tag "Prompt" prompt) (const :tag "Allow" allow) (const :tag "Raise" raise))) +(defcustom bufferlo-bookmark-frame-clone-policy 'prompt + "Control bookmark duplication on cloned and undeleted frames. +Duplicate active bookmarks cause potentially confusing race +conditions where the most recently saved bookmark wins. +\\='prompt asks you to pick a policy. \\='allow will allow +duplicates. \\='clear will clear the bookmark on the cloned frame." + :type '(radio (const :tag "Prompt" prompt) + (const :tag "Allow" allow) + (const :tag "Clear" clear))) + (defcustom bufferlo-bookmark-tab-load-with-bookmarked-frame-policy 'clear-warn - "Behavior when a bookmarked tab is loaded into an -already-bookmarked frame. \\='clear will silently clear the tab -bookmark which is natural reified frame bookmark behavior. -\\='clear-warn warns about the tab losing its bookmark. \\='allow will -retain the tab bookmark to enable it to be saved or -updated--note that tab will be added to the frame bookmark when -that gets saved and the tab will lose its own bookmark -association when the frame bookmark is loaded." + "Control when a tab bookmark is loaded into an already-bookmarked frame. +\\='clear will silently clear the tab bookmark which is natural +reified frame bookmark behavior. \\='clear-warn issues a warning +message about the tab losing its bookmark. \\='allow will retain +the tab bookmark to enable it to be saved or updated, but note +that the frame bookmark supersedes the tab bookmark." :type '(radio (const :tag "Clear (silently)" clear) (const :tag "Clear (with message)" clear-warn) (const :tag "Allow" allow))) (defcustom bufferlo-bookmarks-auto-save-frame-policy 'all - "Bufferlo auto save bookmarks frame policy. \\='current -saves bookmarks on the current frame only. \\='other saves -bookmarks on non-current frames. \\='all saves bookmarks across -all frames." + "Control bufferlo frame bookmark auto save behavior. +\\='current saves bookmarks on the current frame only. \\='other +saves bookmarks on non-current frames. \\='all saves bookmarks +across all frames." :type '(radio (const :tag "Current frame" current) (const :tag "Other frames" other) (const :tag "All frames" all))) -(defcustom bufferlo-bookmarks-save-predicate-functions nil ; TODO: +++ set to #'bufferlo-bookmarks-save-all-p? - "Functions to call for each active bufferlo bookmark to determine -if the bookmark should be automatically saved by the auto-save -timer. Functions are passed the bufferlo bookmark name and -invoked until the first positive result." +(defcustom bufferlo-bookmarks-save-predicate-functions nil + "Functions to filter active bufferlo bookmarks to save +automatically when `bufferlo-bookmarks-auto-save-idle-interval' +is > 0, or manually via `bufferlo-bookmarks-save'. Functions are +passed the bufferlo bookmark name and invoked until the first +positive result. Set to `#'bufferlo-bookmarks-save-all-p' to save +all bookmarks or provide your own predicates." :type 'hook) +(defcustom bufferlo-bookmarks-load-predicate-functions nil + "Functions to filter stored bufferlo bookmarks to load +via `bufferlo-bookmarks-load' which might also be invoked at +Emacs startup time using `window-setup-hook'. Functions are +passed the bufferlo bookmark name and invoked until the first +positive result. Set to `#'bufferlo-bookmarks-load-all-p' to load +all bookmarks or provide your own predicates." + :type 'hook) + +(defcustom bufferlo-bookmarks-load-tabs-make-frame nil + "If non-nil, a new frame is created to contain loaded tab bookmarks. +If nil, tab bookmarks are loaded into the current frame." + :type 'boolean) + (defcustom bufferlo-bookmarks-save-at-emacs-exit 'nosave - "Bufferlo save bookmarks at Emacs exit policy. \\'=nosave does not -save any bookmarks. \\='all saves all active bufferlo bookmarks. -\\='pred honors auto-save predicates in -\\=`bufferlo-bookmarks-save-predicate-functions\\='." + "Bufferlo can save active bookmarks at Emacs exit. +\\'=nosave does not save any active bookmarks. \\='all saves all +active bufferlo bookmarks. \\='pred honors the filter predicates +in `bufferlo-bookmarks-save-predicate-functions'." :type '(radio (const :tag "Do not save at exit" nosave) (const :tag "Predicate-filtered bookmarks" pred) (const :tag "All bookmarks" all))) +(defcustom bufferlo-bookmarks-load-at-emacs-startup 'noload + "Bufferlo can load stored bookmarks at Emacs startup. +\\'=noload does not load any stored bookmarks. \\='all loads all +stored bufferlo bookmarks. \\='pred honors the filter predicates +in `bufferlo-bookmarks-load-predicate-functions'. Note that +`bufferlo-mode' must be enabled before `window-setup-hook' is +invoked for this to take effect." + :type '(radio (const :tag "Do not load at startup" noload) + (const :tag "Predicate-filtered bookmarks" pred) + (const :tag "All bookmarks" all))) + (defcustom bufferlo-ibuffer-bind-local-buffer-filter t "If non-nil, bind the local buffer filter and the orphan filter in ibuffer. The local buffer filter is bound to \"/ l\" and the orphan filter to \"/ L\"." @@ -278,35 +314,36 @@ frame bookmark is a collection of tab bookmarks." "Timer to save bufferlo bookmarks on `bufferlo-bookmarks-auto-save-idle-interval'.") (defun bufferlo--bookmarks-auto-save-timer-maybe-cancel () + "Cancel and clear the bufferlo bookmark auto-save timer, if set." (when (timerp bufferlo--bookmarks-auto-save-timer) (cancel-timer bufferlo--bookmarks-auto-save-timer)) (setq bufferlo--bookmarks-auto-save-timer nil)) (defvar bufferlo-bookmarks-auto-save-idle-interval) ; byte compiler (defun bufferlo--bookmarks-auto-save-timer-maybe-start () + "Start the bufferlo auto-save bookmarks timer, if needed." (bufferlo--bookmarks-auto-save-timer-maybe-cancel) (when (> bufferlo-bookmarks-auto-save-idle-interval 0) (setq bufferlo--bookmarks-auto-save-timer (run-with-idle-timer bufferlo-bookmarks-auto-save-idle-interval t #'bufferlo-bookmarks-save)))) -;; NOTE: must come after the above timer variable and function definitions (defcustom bufferlo-bookmarks-auto-save-idle-interval 0 "Save bufferlo bookmarks when Emacs has been idle this many seconds. -Set to 0 to disable timer." +Set to 0 to disable the timer." :type 'natnum :set (lambda (sym val) (set-default sym val) (bufferlo--bookmarks-auto-save-timer-maybe-start))) -(defun bufferlo-mode-line-format () ; TODO: needs refinement - "Bufferlo mode-line format." +(defun bufferlo-mode-line-format () + "Bufferlo mode-line format to display the current active frame or tab bookmark." (when bufferlo-mode (let ((fbm (frame-parameter nil 'bufferlo-bookmark-frame-name)) (tbm (alist-get 'bufferlo-bookmark-tab-name (tab-bar--current-tab-find)))) - (concat " 🐃" - (if fbm (concat "f=" fbm)) - "." - (if tbm (concat "t=" tbm)))))) + (concat " 🐮" ; "🐃"; It's a cow, but the water buffalo is dark and hard to see. + (if fbm (concat "f:" fbm)) + (if (and fbm tbm) "/") + (if tbm (concat "t:" tbm)))))) (defcustom bufferlo-mode-line-lighter '(:eval (bufferlo-mode-line-format)) "Bufferlo mode line definition." @@ -340,9 +377,9 @@ Set to 0 to disable timer." (advice-add #'tab-bar-select-tab :around #'bufferlo--activate-force) ;; Clone & undelete frame (when (>= emacs-major-version 28) - (advice-add #'clone-frame :around #'bufferlo--activate-force)) + (advice-add #'clone-frame :around #'bufferlo--clone-undelete-frame-advice)) (when (>= emacs-major-version 29) - (advice-add #'undelete-frame :around #'bufferlo--activate-force)) + (advice-add #'undelete-frame :around #'bufferlo--clone-undelete-frame-advice)) ;; Switch-tab workaround (advice-add #'tab-bar-select-tab :around #'bufferlo--clear-buffer-lists-activate) (advice-add #'tab-bar--tab :after #'bufferlo--clear-buffer-lists) @@ -350,7 +387,10 @@ Set to 0 to disable timer." (bufferlo--bookmarks-auto-save-timer-maybe-start) ;; kill-emacs-hook save bookmarks option (when (not (eq bufferlo-bookmarks-save-at-emacs-exit 'nosave)) - (add-hook 'kill-emacs-hook #'bufferlo--bookmarks-save-at-emacs-exit))) + (add-hook 'kill-emacs-hook #'bufferlo--bookmarks-save-at-emacs-exit)) + ;; load bookmarks at startup option + (when (not (eq bufferlo-bookmarks-load-at-emacs-startup 'noload)) + (add-hook 'window-setup-hook #'bufferlo-bookmarks-load))) ;; Prefer local buffers (dolist (frame (frame-list)) (bufferlo--reset-buffer-predicate frame)) @@ -367,16 +407,18 @@ Set to 0 to disable timer." (advice-remove #'tab-bar-select-tab #'bufferlo--activate-force) ;; Clone & undelete frame (when (>= emacs-major-version 28) - (advice-remove #'clone-frame #'bufferlo--activate-force)) + (advice-remove #'clone-frame #'bufferlo--clone-undelete-frame-advice)) (when (>= emacs-major-version 29) - (advice-remove #'undelete-frame #'bufferlo--activate-force)) + (advice-remove #'undelete-frame #'bufferlo--clone-undelete-frame-advice)) ;; Switch-tab workaround (advice-remove #'tab-bar-select-tab #'bufferlo--clear-buffer-lists-activate) (advice-remove #'tab-bar--tab #'bufferlo--clear-buffer-lists) ;; Cancel bookmarks save timer (bufferlo--bookmarks-auto-save-timer-maybe-cancel) ;; kill-emacs-hook save bookmarks option - (remove-hook 'kill-emacs-hook #'bufferlo--bookmarks-save-at-emacs-exit))) + (remove-hook 'kill-emacs-hook #'bufferlo--bookmarks-save-at-emacs-exit) + ;; load bookmarks at startup option + (remove-hook 'window-setup-hook #'bufferlo-bookmarks-load))) (defun bufferlo-local-buffer-p (buffer &optional frame tabnum include-hidden) "Return non-nil if BUFFER is in the list of local buffers. @@ -430,7 +472,7 @@ before calling OLDFN with ARGS. See `bufferlo--clear-buffer-lists'." result)) (defun bufferlo--buffer-predicate (buffer) - "Return whether BUFFER is local to the current fram/tab. + "Return whether BUFFER is local to the current frame/tab. Includes hidden buffers." (bufferlo-local-buffer-p buffer nil nil t)) @@ -589,6 +631,30 @@ the adviced functions." (bufferlo--desktop-advice-active-force t)) (apply oldfn args))) +(defun bufferlo--clone-undelete-frame-advice (oldfn &rest args) + "Activate the advice for `bufferlo--window-state-{get,put}'. +OLDFN is the original function. ARGS is for compatibility with +the adviced functions. Honors `bufferlo-bookmark-frame-clone-policy'." + (let ((bufferlo--desktop-advice-active t) + (bufferlo--desktop-advice-active-force t)) + (apply oldfn args)) + (let ((fbm (frame-parameter nil 'bufferlo-bookmark-frame-name)) + (clone-policy bufferlo-bookmark-frame-clone-policy)) + (when fbm + (when (eq clone-policy 'prompt) + (pcase (let ((read-answer-short t)) + (read-answer "Cloned/undeleted frame bookmark: Allow, Clear " + '(("allow" ?a "Allow duplicate bookmark") + ("clear" ?c "Clear bookmark") + ("help" ?h "Help") + ("quit" ?q "Quit--retains the bookmark")))) + ("clear" (setq clone-policy 'clear)) + (_ (setq clone-policy 'allow))) ; allow, quit cases + (pcase clone-policy + ('allow) + ('clear + (set-frame-parameter nil 'bufferlo-bookmark-frame-name nil))))))) + (defsubst bufferlo--warn () "Warn if `bufferlo-mode' is not enabled." (defvar bufferlo--warn-current-command nil) @@ -795,17 +861,16 @@ prefix argument is given, remove only buffers that visit a file. Buffers matching `bufferlo-include-buffer-filters' are not removed." (interactive "P") (bufferlo--warn) - (let ((curr-project (project-current)) - (include (bufferlo--merge-regexp-list - (append '("a^") bufferlo-include-buffer-filters)))) - (if curr-project - (dolist (buffer (bufferlo-buffer-list)) - (when (and (not (string-match-p include (buffer-name buffer))) - (not (equal curr-project - (with-current-buffer buffer (project-current)))) - (or (not file-buffers-only) (buffer-file-name buffer))) - (bufferlo-remove buffer))) - (message "Current buffer is not part of a project")))) + (if-let ((curr-project (project-current)) + (include (bufferlo--merge-regexp-list + (append '("a^") bufferlo-include-buffer-filters)))) + (dolist (buffer (bufferlo-buffer-list)) + (when (and (not (string-match-p include (buffer-name buffer))) + (not (equal curr-project + (with-current-buffer buffer (project-current)))) + (or (not file-buffers-only) (buffer-file-name buffer))) + (bufferlo-remove buffer))) + (message "Current buffer is not part of a project"))) (defun bufferlo-find-buffer (buffer-or-name) "Switch to the frame/tab containing BUFFER-OR-NAME in its local list. @@ -1198,14 +1263,12 @@ In contrast to `bufferlo-anywhere-mode', this does not adhere to (mapcar #'bufferlo--bookmark-get-for-buffer (bufferlo--bookmark-filter-excluded-buffers frame))))) -(defun bufferlo--bookmark-tab-get (&optional name frame) +(defun bufferlo--bookmark-tab-get (&optional frame) "Get the bufferlo tab bookmark for the current tab in FRAME. -Optional argument NAME provides a name for the bookmarks. FRAME specifies the frame; the default value of nil selects the current frame." `((buffer-bookmarks . ,(bufferlo--bookmark-get-for-buffers-in-tab frame)) (buffer-list . ,(mapcar #'buffer-name (bufferlo-buffer-list frame nil t))) (window . ,(window-state-get (frame-root-window frame) 'writable)) - (name . ,name) ; DEPRECATED: ? bookmark-name-from-full-record works fine in the handler (handler . ,#'bufferlo--bookmark-tab-handler))) (defun bufferlo--ws-replace-buffer-names (ws replace-alist) @@ -1219,14 +1282,16 @@ FRAME specifies the frame; the default value of nil selects the current frame." (when-let (replace (assoc (cadr bc) replace-alist)) (setf (cadr bc) (cdr replace))))))))) -(defun bufferlo--bookmark-tab-handler (bookmark &optional no-message is-fbm-tab) +(defun bufferlo--bookmark-tab-handler (bookmark &optional no-message embedded-tab) "Handle bufferlo tab bookmark. The argument BOOKMARK is the to-be restored tab bookmark created via `bufferlo--bookmark-tab-get'. The optional argument NO-MESSAGE inhibits the message after successfully restoring the bookmark." (let* ((ws (copy-tree (alist-get 'window bookmark))) (dummy (generate-new-buffer " *bufferlo dummy buffer*")) ; TODO: needs unwind-protect? - (bookmark-name (if (null is-fbm-tab) (bookmark-name-from-full-record bookmark) nil)) + (bookmark-name (if (null embedded-tab) + (bookmark-name-from-full-record bookmark) + nil)) (msg) (renamed (mapcar @@ -1258,7 +1323,8 @@ the message after successfully restoring the bookmark." (window-state-put ws (frame-root-window) 'safe) (set-frame-parameter nil 'buffer-list bl) (set-frame-parameter nil 'buried-buffer-list nil) - (if (frame-parameter nil 'bufferlo-bookmark-frame-name) + (if (and (not embedded-tab) + (frame-parameter nil 'bufferlo-bookmark-frame-name)) (pcase bufferlo-bookmark-tab-load-with-bookmarked-frame-policy ('clear) ; do nothing ('clear-warn @@ -1276,9 +1342,8 @@ the message after successfully restoring the bookmark." (put #'bufferlo--bookmark-tab-handler 'bookmark-handler-type "B-Tab") ; short name here as bookmark-bmenu-list hard codes width of 8 chars -(defun bufferlo--bookmark-frame-get (&optional name frame) +(defun bufferlo--bookmark-frame-get (&optional frame) "Get the bufferlo frame bookmark. -Optional argument NAME provides a name for the bookmarks. FRAME specifies the frame; the default value of nil selects the current frame." (let ((org-tab (1+ (tab-bar--current-tab-index nil frame))) (tabs nil)) @@ -1287,7 +1352,7 @@ FRAME specifies the frame; the default value of nil selects the current frame." (let* ((curr (alist-get 'current-tab (funcall tab-bar-tabs-function frame))) (name (alist-get 'name curr)) (explicit-name (alist-get 'explicit-name curr)) - (tbm (bufferlo--bookmark-tab-get nil frame))) + (tbm (bufferlo--bookmark-tab-get frame))) (if explicit-name (push (cons 'tab-name name) tbm) (push (cons 'tab-name nil) tbm)) @@ -1295,7 +1360,6 @@ FRAME specifies the frame; the default value of nil selects the current frame." (tab-bar-select-tab org-tab) `((tabs . ,(reverse tabs)) (current . ,org-tab) - (name . ,name) ; DEPRECATED: ? bookmark-name-from-full-record works fine in the handler (handler . ,#'bufferlo--bookmark-frame-handler)))) (defun bufferlo--bookmark-frame-handler (bookmark &optional no-message) @@ -1310,7 +1374,7 @@ the message after successfully restoring the bookmark." (duplicate-policy bufferlo-bookmark-frame-duplicate-policy)) (when (eq duplicate-policy 'prompt) (pcase (let ((read-answer-short t)) - (read-answer "Frame bookmark already loaded " + (read-answer "Bookmark already loaded in another frame: Allow, Raise existing " '(("allow" ?a "Allow duplicate") ("raise" ?r "Raise the frame with the existing bookmark") ("help" ?h "Help") @@ -1334,7 +1398,7 @@ the message after successfully restoring the bookmark." (progn (when (eq load-policy 'prompt) (pcase (let ((read-answer-short t)) - (read-answer "Frame already bookmarked. Choose a bookmark for this frame: " + (read-answer "Frame already bookmarked: use Current, Replace with new, Merge with existing " '(("current" ?c "Use the existing bookmark") ("replace" ?r "Replace the bookmark with the selected bookmark") ("merge" ?m "Merge the new tab content with the existing bookmark") @@ -1368,7 +1432,7 @@ the message after successfully restoring the bookmark." (if first (setq first nil) (tab-bar-new-tab-to)) - (bufferlo--bookmark-tab-handler tbm t 'is-fbm-tab) + (bufferlo--bookmark-tab-handler tbm t 'embedded-tab) (when-let (tab-name (alist-get 'tab-name tbm)) (tab-bar-rename-tab tab-name))) (alist-get 'tabs bookmark))) @@ -1414,7 +1478,7 @@ buffer list." (bufferlo--bookmark-get-names #'bufferlo--bookmark-tab-handler) nil nil nil 'bufferlo-bookmark-tab-history))) (bufferlo--warn) - (bookmark-store name (bufferlo--bookmark-tab-get name) no-overwrite) + (bookmark-store name (bufferlo--bookmark-tab-get) no-overwrite) (setf (alist-get 'bufferlo-bookmark-tab-name (cdr (bufferlo--current-tab))) name) @@ -1475,15 +1539,16 @@ state (not the contents) of the bookmarkable buffers for each tab." nil nil nil 'bufferlo-bookmark-frame-history (frame-parameter nil 'bufferlo-bookmark-frame-name)))) (bufferlo--warn) - (bookmark-store name (bufferlo--bookmark-frame-get name) no-overwrite) + (bookmark-store name (bufferlo--bookmark-frame-get) no-overwrite) (set-frame-parameter nil 'bufferlo-bookmark-frame-name name) (unless no-message (message "Saved bufferlo frame bookmark: %s" name))) (defun bufferlo-bookmark-frame-load (name) - "Load a frame bookmark; replace the current frame's state if -`bufferlo-bookmark-frame-load-make-frame' is nil -NAME is the bookmark's name." + "Load a frame bookmark. +Replace the current frame's state if +`bufferlo-bookmark-frame-load-make-frame' is nil. NAME is the +bookmark's name." (interactive (list (completing-read "Load bufferlo frame bookmark: " @@ -1519,21 +1584,20 @@ associated bookmark exists." (bufferlo-bookmark-frame-load bm) (call-interactively #'bufferlo-bookmark-frame-load)))) -(defun bufferlo-stored-bookmarks () - (let ((bookmarks)) - (dolist (bookmark bookmark-alist) - (let ((bookmark-name (bookmark-name-from-full-record bookmark)) - (bookmark-handler (bookmark-get-handler bookmark))) - (when (eq bookmark-handler #'bufferlo--bookmark-frame-handler) - (push (cons 'fbm bookmark-name) bookmarks)) - (when (eq bookmark-handler #'bufferlo--bookmark-tab-handler) - (push (cons 'tbm bookmark-name) bookmarks)))) - bookmarks)) +(defun bufferlo-bookmark-frame-load-merge () + "Load a bufferlo frame bookmark merging its content into the current frame." + (interactive) + (let ((bufferlo-bookmark-frame-duplicate-policy 'allow) + (bufferlo-bookmark-frame-load-policy 'merge) + (bufferlo-bookmark-frame-load-make-frame nil)) + (call-interactively #'bufferlo-bookmark-frame-load))) (defun bufferlo-active-bookmarks (&optional frames type) - "Produces an alist of the form - (bookmark-name . (('type . type) ('frame . frame) ('tab . tab))) -for the specified FRAMES, filtered by TYPE" + "Produces an alist of active bufferlo bookmarks +of the form +((bookmark-name . ((\\='type . type) (\\='frame . frame) (\\='tab +. tab))) ...) for the specified FRAMES, filtered by TYPE, where +type is \\='fbm for frame bookmarks or \\='tbm for tab bookmarks." (let ((bookmarks)) (dolist (frame (or frames (frame-list))) (when-let ((fbm (frame-parameter frame 'bufferlo-bookmark-frame-name))) @@ -1546,13 +1610,38 @@ for the specified FRAMES, filtered by TYPE" bookmarks)) (defun bufferlo-bookmarks-save-all-p (_bookmark-name) + "`bufferlo-bookmarks-save-predicate-functions' predicate that matches all bookmarks." + t) + +(defun bufferlo-bookmarks-load-all-p (_bookmark-name) + "`bufferlo-bookmarks-load-predicate-functions' predicate that matches all bookmarks." t) -(defun bufferlo-bookmarks-save () +(defun bufferlo-bookmarks-save (&optional all) + "Save active bufferlo bookmarks. +This is invoked via an optional idle timer which runs according +to `bufferlo-bookmarks-auto-save-idle-interval' and is +optionally invoked at Emacs exit. + +You may invoke this manually at any time to save active +bookmarks; however, doing so does not reset the save interval +timer. + +Each bookmark is filtered according to +`bufferlo-bookmarks-save-predicate-functions'. + +Specify ALL to ignore the predicates and save every active +bufferlo bookmark or use a prefix argument. Note that if there +are duplicate active bufferlo bookmarks, the last one to be saved +will take precedence." + (interactive) (let ((bookmarks-saved nil) - (start-time (current-time))) - (let ((bookmark-save-flag nil) - (frames (pcase bufferlo-bookmarks-auto-save-frame-policy + (start-time (current-time)) + (bufferlo-bookmarks-save-predicate-functions + (if (or all current-prefix-arg) + (list #'bufferlo-bookmarks-save-all-p) + bufferlo-bookmarks-save-predicate-functions))) + (let ((frames (pcase bufferlo-bookmarks-auto-save-frame-policy ('current (list (selected-frame))) ('other @@ -1564,20 +1653,21 @@ for the specified FRAMES, filtered by TYPE" (bookmark-type (alist-get 'type bookmark))) (when (run-hook-with-args-until-success 'bufferlo-bookmarks-save-predicate-functions bookmark-name) (when (eq bookmark-type 'fbm) - ;; BUG: fbm's not yet enforced to be unique among frames, so we may save the same bookmark more than once (push bookmark-name bookmarks-saved) (bufferlo-bookmark-frame-save bookmark-name nil t)) (when (eq bookmark-type 'tbm) - ;; BUG: tbm's not yet enforced to be unique within or among frames, so we may save the same bookmark more than once (push bookmark-name bookmarks-saved) (bufferlo-bookmark-tab-save bookmark-name nil t)))))) (when (and bookmarks-saved (bookmark-time-to-save-p)) (bookmark-save) - (message "Auto-saved bufferlo bookmarks: %s, in %.2f seconds " + (message "Saved bufferlo bookmarks: %s, in %.2f seconds " (mapconcat 'identity bookmarks-saved " ") (float-time (time-subtract (current-time) start-time)))))) (defun bufferlo--bookmarks-save-at-emacs-exit () + "Save bufferlo bookmarks at Emacs exit +honoring `bufferlo-bookmarks-save-at-emacs-exit' by predicate or + all. Intended to be invoked via `kill-emacs-hook'." (bufferlo--bookmarks-auto-save-timer-maybe-cancel) (let ((bufferlo-bookmarks-save-predicate-functions (if (eq bufferlo-bookmarks-save-at-emacs-exit 'all) @@ -1585,6 +1675,79 @@ for the specified FRAMES, filtered by TYPE" bufferlo-bookmarks-save-predicate-functions))) (bufferlo-bookmarks-save))) +(defun bufferlo-bookmarks-load (&optional all) + "Load stored bufferlo bookmarks. +Invoke manually or via `window-setup-hook' to restore bookmarks +at Emacs startup. + +Each bookmark is filtered according to +`bufferlo-bookmarks-load-predicate-functions'. + +ALL, or a prefix argument, ignores the load predicates and loads +all stored bufferlo bookmarks. Tab bookmarks are loaded into the +current or new frame according to +`bufferlo-bookmarks-load-tabs-make-frame'." + (interactive) + (let ((bookmarks-loaded nil) + (start-time (current-time)) + (tab-bar-new-tab-choice t) + (new-tab-frame nil) + (bufferlo-bookmarks-load-predicate-functions + (if (or all current-prefix-arg) + (list #'bufferlo-bookmarks-load-all-p) + bufferlo-bookmarks-load-predicate-functions))) + (dolist (bookmark-name (bufferlo--bookmark-get-names #'bufferlo--bookmark-tab-handler)) + (when (run-hook-with-args-until-success 'bufferlo-bookmarks-load-predicate-functions bookmark-name) + (if (and bufferlo-bookmarks-load-tabs-make-frame (not new-tab-frame)) + (setq new-tab-frame (make-frame)) + (tab-bar-new-tab-to)) + (bufferlo-bookmark-tab-load bookmark-name) + (push bookmark-name bookmarks-loaded))) + (dolist (bookmark-name (bufferlo--bookmark-get-names #'bufferlo--bookmark-frame-handler)) + (when (run-hook-with-args-until-success 'bufferlo-bookmarks-load-predicate-functions bookmark-name) + (bufferlo-bookmark-frame-load bookmark-name) + (push bookmark-name bookmarks-loaded))) + (when bookmarks-loaded + (message "Loaded bufferlo bookmarks: %s, in %.2f seconds " + (mapconcat 'identity bookmarks-loaded " ") + (float-time (time-subtract (current-time) start-time)))))) + +(defun bufferlo-maybe-clear-active-bookmark (&optional force) + "Clear the current frame and/or tab bufferlo bookmark +if there is another active bufferlo bookmark with the same name. + +This is useful if an active bookmark has been loaded twice, and +especially if you use auto saving features and want to ensure +that only one bookmark is active. + +FORCE will clear the bookmark even if it is currently unique." + (interactive) + (let* ((fbm (frame-parameter nil 'bufferlo-bookmark-frame-name)) + (tbm (alist-get 'bufferlo-bookmark-tab-name (tab-bar--current-tab-find))) + (duplicate-fbm (> (length (seq-filter (lambda (x) (equal fbm (car x))) (bufferlo-active-bookmarks nil 'fbm))) 1)) + (duplicate-tbm (> (length (seq-filter (lambda (x) (equal tbm (car x))) (bufferlo-active-bookmarks nil 'tbm))) 1))) + (when (or force duplicate-fbm) + (set-frame-parameter nil 'bufferlo-bookmark-frame-name nil)) + (when (or force duplicate-tbm) + (setf (alist-get 'bufferlo-bookmark-tab-name + (cdr (bufferlo--current-tab))) + nil)))) + +(defun bufferlo-clear-active-bookmarks () + "Clear all active bufferlo frame and tab bookmarks. +This leaves all content untouched and does not impact stored bookmarks. + +This is useful when you have accumulated a complex working set of +frames, tabs, buffers and want to save new bookmarks without +disturbing existing bookmarks, or where auto-saving is enabled +and you want to avoid overwriting stored bookmarks, perhaps with +transient work." + (interactive) + (dolist (frame (frame-list)) + (set-frame-parameter frame 'bufferlo-bookmark-frame-name nil) + (dolist (tab (funcall tab-bar-tabs-function frame)) + (setf (alist-get 'bufferlo-bookmark-tab-name tab) nil)))) + (provide 'bufferlo) ;;; bufferlo.el ends here