branch: externals/org commit edd7f2962fe146805ab275974274596f994ebd9f Author: Ihor Radchenko <yanta...@gmail.com> Commit: Ihor Radchenko <yanta...@gmail.com>
org-persist: Reimplement using more generic approach --- lisp/org-element.el | 70 ++-- lisp/org-persist.el | 936 ++++++++++++++++++++++++++++++++++++++-------------- lisp/org.el | 7 +- 3 files changed, 726 insertions(+), 287 deletions(-) diff --git a/lisp/org-element.el b/lisp/org-element.el index b82475a14e..d556ab2eed 100644 --- a/lisp/org-element.el +++ b/lisp/org-element.el @@ -7054,43 +7054,53 @@ The element is: %S\n The real element is: %S\n Cache around :begin:\n%S\n%S\n%S" ;;; Cache persistance -(defun org-element--cache-persist-before-write (var &optional buffer) +(defun org-element--cache-persist-before-write (container &optional associated) "Sync cache before saving." - (when (and org-element-use-cache - buffer - org-element-cache-persistent - (eq var 'org-element--cache) - (derived-mode-p 'org-mode) - org-element--cache) - (with-current-buffer buffer - ;; Cleanup cache request keys to avoid collisions during next - ;; Emacs session. - (avl-tree-mapc - (lambda (el) - (org-element-put-property el :org-element--cache-sync-key nil)) - org-element--cache) - (org-with-wide-buffer - (org-element-at-point (point-max)))) - nil)) - -(defun org-element--cache-persist-before-read (var &optional buffer) + (when (equal container '("elisp" org-element--cache)) + (if (and org-element-use-cache + (plist-get associated :file) + (get-file-buffer (plist-get associated :file)) + org-element-cache-persistent) + (with-current-buffer (get-file-buffer (plist-get associated :file)) + (if (and (derived-mode-p 'org-mode) + org-element--cache) + (progn + ;; Cleanup cache request keys to avoid collisions during next + ;; Emacs session. + (avl-tree-mapc + (lambda (el) + (org-element-put-property el :org-element--cache-sync-key nil)) + org-element--cache) + (org-with-wide-buffer + (org-element-at-point (point-max))) + nil) + 'forbid)) + 'forbid))) + +(defun org-element--cache-persist-before-read (container &optional associated) "Avoid reading cache before Org mode is loaded." - (when (memq var '(org-element--cache org-element--headline-cache)) - (if (not buffer) 'forbid - (with-current-buffer buffer + (when (equal container '("elisp" org-element--cache)) + (if (not (and (plist-get associated :file) + (get-file-buffer (plist-get associated :file)))) + 'forbid + (with-current-buffer (get-file-buffer (plist-get associated :file)) (unless (and org-element-use-cache org-element-cache-persistent - (derived-mode-p 'org-mode)) + (derived-mode-p 'org-mode) + (equal (secure-hash 'md5 (current-buffer)) + (plist-get associated :hash))) 'forbid))))) -(defun org-element--cache-persist-after-read (var &optional buffer) +(defun org-element--cache-persist-after-read (container &optional associated) "Setup restored cache." - (with-current-buffer buffer - (when (and org-element-use-cache org-element-cache-persistent) - (when (and (eq var 'org-element--cache) org-element--cache) - (setq-local org-element--cache-size (avl-tree-size org-element--cache))) - (when (and (eq var 'org-element--headline-cache) org-element--headline-cache) - (setq-local org-element--headline-cache-size (avl-tree-size org-element--headline-cache)))))) + (when (and (plist-get associated :file) + (get-file-buffer (plist-get associated :file))) + (with-current-buffer (get-file-buffer (plist-get associated :file)) + (when (and org-element-use-cache org-element-cache-persistent) + (when (and (equal container '("elisp" org-element--cache)) org-element--cache) + (setq-local org-element--cache-size (avl-tree-size org-element--cache))) + (when (and (equal container '("elisp" org-element--headline-cache)) org-element--headline-cache) + (setq-local org-element--headline-cache-size (avl-tree-size org-element--headline-cache))))))) (add-hook 'org-persist-before-write-hook #'org-element--cache-persist-before-write) (add-hook 'org-persist-before-read-hook #'org-element--cache-persist-before-read) diff --git a/lisp/org-persist.el b/lisp/org-persist.el index 266c0a513e..9fee5f7936 100644 --- a/lisp/org-persist.el +++ b/lisp/org-persist.el @@ -24,6 +24,77 @@ ;; ;; This file implements persistant data storage across Emacs sessions. ;; Both global and buffer-local data can be stored. +;; +;; Most common data type is variable data. However, other data types +;; can also be stored. +;; +;; Persistent data is stored in individual files. Each of the files +;; can contain a collection of related data, which is particularly +;; useful when, say, several variables cross-reference each-other's +;; data-cells and we want to preserve their circular structure. +;; +;; Each data collection can be associated with a local or remote file, +;; its inode number, or contents hash. The persistent data collection +;; can later be accessed using either file bufer, file, inode, or +;; contents hash. +;; +;; The data collections can be versioned and removed upon expiry. +;; +;; In the code below I will use the following naming conventions: +;; 1. Container :: a type of data to be stored +;; Containers can store elisp variables, files, and version +;; numbers. Each container can be customized with container +;; options. For example, "elisp" container is customized with +;; variable symbol. ("elisp" variable) is a container storing +;; Lisp variable value. Similarly, ("version" "2.0") container +;; will store version number. +;; 2. Associated :: an object the container is associated with. The +;; object can be a buffer, file, inode number, file contents hash, +;; a generic key, or multiple of them. Associated can also be nil. +;; 3. Data collection :: a list of containers linked to an associated +;; object/objects. Each data collection can also have auxiliary +;; records. Their only purpose is readability of the collection +;; index. +;; 4. Index file :: a file listing all the stored data collections. +;; 5. Persist file :: a file holding data values or references to +;; actual data values for a single data collection. This file +;; contains an alist associating each data container in data +;; collection with its value or a reference to the actual value. +;; +;; All the persistent data is stored in `org-persist-directory'. The data +;; collections are listed in `org-persist-index-file' and the actual data is +;; stored in UID-style subfolders. +;; +;; The `org-persist-index-file' stores the value of `org-persist--index'. +;; +;; Each collection is represented as a plist containing the following +;; properties: +;; - `:container' : list of data continers to be stored in single +;; file; +;; - `:persist-file': data file name; +;; - `:associated' : list of associated objects; +;; - `:last-access' : last date when the container has been read; +;; - `:expiry' : list of expiry conditions. +;; - all other keywords are ignored +;; +;; The available types of data containers are: +;; 1. ("elisp" variable-symbol) or just variable-symbol :: Storing +;; elisp variable data. +;; 2. ("file") :: Store a copy of the associated file preserving the +;; extension. +;; 3. ("version" "version number") :: Version the data collection. +;; If the stored collection has different version than "version +;; number", disregard it. +;; 4. ("url") :: Store a downloaded copy of URL object. +;; +;; The data collections can expire, in which case they will be removed +;; from the persistent storage at the end of Emacs session. The +;; expiry condition can be set when saving/registering data +;; containers. The expirty condition can be `never' - data will never +;; expire; `nil' - data will expire at the end of current Emacs session; +;; a number - data will expire after the number days from last access; +;; a function - data will expire if the function, called with a single +;; argument - collection, returns non-nil. ;;; Code: @@ -31,10 +102,8 @@ (require 'org-id) (require 'xdg nil t) -(declare-function org-back-to-heading "org" (&optional invisible-ok)) -(declare-function org-next-visible-heading "org" (arg)) -(declare-function org-at-heading-p "org" (&optional invisible-not-ok)) - +(defconst org-persist--storage-version "2.0" + "Persistent storage layout version.") (defgroup org-persist nil "Persistent cache for Org mode." @@ -42,51 +111,74 @@ :group 'org) (defcustom org-persist-directory (expand-file-name - (org-file-name-concat - (let ((cache-dir (when (fboundp 'xdg-cache-home) - (xdg-cache-home)))) - (if (or (seq-empty-p cache-dir) - (not (file-exists-p cache-dir)) - (file-exists-p (org-file-name-concat - user-emacs-directory - "org-persist"))) - user-emacs-directory - cache-dir)) - "org-persist/")) + (org-file-name-concat + (let ((cache-dir (when (fboundp 'xdg-cache-home) + (xdg-cache-home)))) + (if (or (seq-empty-p cache-dir) + (not (file-exists-p cache-dir)) + (file-exists-p (org-file-name-concat + user-emacs-directory + "org-persist"))) + user-emacs-directory + cache-dir)) + "org-persist/")) "Directory where the data is stored." :group 'org-persist :type 'directory) -(defvar org-persist-index-file "index" +(defcustom org-persist-remote-files 100 + "Whether to keep persistent data for remote files. + +When this variable is nil, never save persitent data associated with +remote files. When `t', always keep the data. When +`check-existence', contact remote server containing the file and only +keep the data when the file exists on the server. When a number, keep +up to that number persistent values for remote files. + +Note that the last option `check-existence' may cause Emacs to show +password prompts to log in." + :group 'org-persist + :type '(choice (const :tag "Never" nil) + (const :tag "Always" t) + (number :tag "Keep not more than X files") + (const :tag "Check if exist on remote" 'check-existence))) + +(defconst org-persist-index-file "index" "File name used to store the data index.") (defvar org-persist-before-write-hook nil - "Abnormal hook ran before saving data for a single variable in a buffer. + "Abnormal hook ran before saving data. The hook must accept the same arguments as `org-persist-write'. The hooks will be evaluated until a hook returns non-nil. If any of the hooks return non-nil, do not save the data.") (defvar org-persist-before-read-hook nil - "Abnormal hook ran before reading data for a single variable in a buffer. + "Abnormal hook ran before reading data. The hook must accept the same arguments as `org-persist-read'. The hooks will be evaluated until a hook returns non-nil. If any of the hooks return non-nil, do not read the data.") (defvar org-persist-after-read-hook nil - "Abnormal hook ran after reading data for a single variable in a buffer. + "Abnormal hook ran after reading data. The hook must accept the same arguments as `org-persist-read'.") (defvar org-persist--index nil "Global index. The index is a list of plists. Each plist contains information about -a data variable. Each plist contains the following properties: +persistent data storage. Each plist contains the following +properties: - - `:variable' list of variables to be stored in single file + - `:container' : list of data continers to be stored in single file - `:persist-file': data file name - - `:path': buffer file path, if any - - `:inode': buffer file inode, if any - - `:hash': buffer hash, if any") + - `:associated' : list of associated objects + - `:last-access' : last date when the container has been read + - `:expiry' : list of expiry conditions + - all other keywords are ignored.") + +(defvar org-persist--index-hash nil + "Hash table storing `org-persist--index'. Used for quick access. +They keys are conses of (container . associated).") (defvar org-persist--report-time 0.5 "Whether to report read/write time. @@ -98,245 +190,577 @@ message is displayed. When the value is a non-nil non-number, always display the message. When the value is nil, never diplay the message.") -(defun org-persist--get-index (var &optional buffer) - "Return plist used to store VAR in BUFFER. -When BUFFER is nil, return plist for global VAR." - (org-persist--read-index) - (let* ((buffer-file (when buffer (buffer-file-name (or (buffer-base-buffer buffer) - buffer)))) - (inode (when buffer-file - (and (fboundp 'file-attribute-inode-number) - (file-attribute-inode-number (file-attributes buffer-file))))) - (buffer-hash (when buffer (secure-hash 'md5 buffer)))) - (let ((result (seq-find (lambda (plist) - (and (or (memq var (plist-get plist :variable)) - (eq var (plist-get plist :variable))) - (or (and inode (equal inode (plist-get plist :inode))) - (and buffer-file (equal buffer-file (plist-get plist :path))) - (and buffer-hash (equal buffer-hash (plist-get plist :hash)))))) - org-persist--index))) - (when result - (unless (equal buffer-file (plist-get result :path)) - (setf result (plist-put result :path buffer-file)))) - (unless result - (push (list :variable (if (listp var) var (list var)) - :persist-file (replace-regexp-in-string "^.." "\\&/" (org-id-uuid)) - :path buffer-file - :inode inode - :hash buffer-hash) - org-persist--index) - (setf result (car org-persist--index))) - result))) - -(defun org-persist--read-index () - "Read `org-persist--index'" - (unless org-persist--index - (when (file-exists-p (org-file-name-concat org-persist-directory org-persist-index-file)) - (with-temp-buffer - (insert-file-contents (org-file-name-concat org-persist-directory org-persist-index-file)) - (setq org-persist--index - (condition-case err - (read (current-buffer)) - ;; Recover gracefully if index file is corrupted. - (error - (warn "Emacs reader failed to read data for `org-persist--index' from %S. The error was: %S" - (org-file-name-concat org-persist-directory org-persist-index-file) - (error-message-string err)) - nil))))))) - -(cl-defun org-persist-register (var &optional buffer &key inherit) - "Register VAR in BUFFER to be persistent. -Optional key INHERIT make VAR dependent on another variable. Such -dependency means that data shared between variables will be preserved -(see elisp#Circular Objects)." - (unless org-persist--index (org-persist--read-index)) - (when inherit - (let ((inherited-index (org-persist--get-index inherit buffer))) - (unless (memq var (plist-get inherited-index :variable)) - (setq inherited-index - (plist-put inherited-index :variable - (cons var (plist-get inherited-index :variable))))))) - (org-persist--get-index var buffer) - (when buffer - (add-hook 'kill-buffer-hook #'org-persist-write-all-buffer nil 'local))) - -(defun org-persist-unregister (var &optional buffer) - "Unregister VAR in BUFFER to be persistent. -When BUFFER is `all', unregister VAR in all buffers." - (unless org-persist--index (org-persist--read-index)) - (setq org-persist--index - (seq-remove - (lambda (plist) - (when (and (memq var (plist-get plist :variable)) - (or (eq buffer 'all) - (string= (buffer-file-name - (or (buffer-base-buffer buffer) - buffer)) - (or (plist-get plist :path) "")))) - (if (> (length (plist-get plist :variable)) 1) - (progn - (setq plist - (plist-put plist :variable - (delq var (plist-get plist :variable)))) - ;; Do not remove the index though. - nil) - (let ((persist-file (org-file-name-concat org-persist-directory (plist-get plist :persist-file)))) - (delete-file persist-file) - (when (org-directory-empty-p (file-name-directory persist-file)) - (delete-directory (file-name-directory persist-file)))) - 'delete-from-index))) - org-persist--index)) - (org-persist-gc)) - -(defun org-persist-write (var &optional buffer) - "Save buffer-local data in BUFFER for VAR." - (unless (and buffer (not (get-buffer buffer))) - (unless (listp var) (setq var (list var))) - (with-current-buffer (or buffer (current-buffer)) - (let ((index (org-persist--get-index var buffer)) - (start-time (float-time))) - (setf index (plist-put index :hash (when buffer (secure-hash 'md5 buffer)))) - (let ((print-circle t) - print-level - print-length - print-quoted - (print-escape-control-characters t) - (print-escape-nonascii t) - (print-continuous-numbering t) - print-number-table) - (unless (seq-find (lambda (v) - (run-hook-with-args-until-success 'org-persist-before-write-hook v buffer)) - (plist-get index :variable)) - (unless (file-exists-p org-persist-directory) - (make-directory org-persist-directory)) - (unless (file-exists-p org-persist-directory) - (warn "Failed to create org-persist storage in %s." - org-persist-directory) - (let ((dir (directory-file-name - (file-name-as-directory org-persist-directory)))) - (while (and (not (file-exists-p dir)) - (not (equal dir (setq dir (directory-file-name - (file-name-directory dir))))))) - (unless (file-writable-p dir) - (message "Missing write access rights to org-persist-directory: %S" - org-persist-directory)))) - (when (file-exists-p org-persist-directory) - (with-temp-file (org-file-name-concat org-persist-directory org-persist-index-file) - (prin1 org-persist--index (current-buffer))) - (let ((file (org-file-name-concat org-persist-directory (plist-get index :persist-file))) - (data (mapcar (lambda (s) (cons s (symbol-value s))) - (plist-get index :variable)))) - (unless (file-exists-p (file-name-directory file)) - (make-directory (file-name-directory file) t)) - (with-temp-file file - (prin1 data (current-buffer))) - (let ((duration (- (float-time) start-time))) - (when (or (and org-persist--report-time - (numberp org-persist--report-time) - (>= duration org-persist--report-time)) - (and org-persist--report-time - (not (numberp org-persist--report-time)))) - (if buffer - (message "org-persist: Writing %S from %S took %.2f sec" - var buffer duration) - (message "org-persist: Writing %S took %.2f sec" - var duration)))))))))))) - -(defun org-persist-write-all (&optional buffer) - "Save all the persistent data." - (unless (and buffer (not (buffer-file-name buffer))) - (dolist (index org-persist--index) - (when (or (and (not (plist-get index :path)) - (not buffer)) - (and (plist-get index :path) - (get-file-buffer (plist-get index :path)) - (equal (buffer-file-name - (or buffer - (get-file-buffer (plist-get index :path)))) - (plist-get index :path)))) - (org-persist-write (plist-get index :variable) - (when (plist-get index :path) - (get-file-buffer (plist-get index :path)))))))) - -(defun org-persist-write-all-buffer () - "Call `org-persist-write-all' in current buffer." - (org-persist-write-all (current-buffer))) - -(defun org-persist-read (var &optional buffer) - "Restore VAR data in BUFFER." - (let* ((index (org-persist--get-index var buffer)) - (persist-file (org-file-name-concat org-persist-directory (plist-get index :persist-file))) - (data nil) - (start-time (float-time))) - (when (and index - (file-exists-p persist-file) - (or (not buffer) - (equal (secure-hash 'md5 buffer) (plist-get index :hash)))) - (unless (seq-find (lambda (v) - (run-hook-with-args-until-success 'org-persist-before-read-hook v buffer)) - (plist-get index :variable)) - (with-temp-buffer - (let ((coding-system-for-read 'utf-8) - (read-circle t)) - (insert-file-contents persist-file)) +;;;; Common functions + +(defun org-persist--display-time (duration format &rest args) + "Report DURATION according to FORMAT + ARGS message. +FORMAT and ARGS are passed to `message'." + (when (or (and org-persist--report-time + (numberp org-persist--report-time) + (>= duration org-persist--report-time)) + (and org-persist--report-time + (not (numberp org-persist--report-time)))) + (apply #'message + (format "org-persist: %s took %%.2f sec" format) + (append args (list duration))))) + +(defun org-persist--read-elisp-file (&optional buffer-or-file) + "Read elisp data from BUFFER-OR-FILE or current buffer." + (unless buffer-or-file (setq buffer-or-file (current-buffer))) + (with-temp-buffer + (if (bufferp buffer-or-file) + (set-buffer buffer-or-file) + (insert-file-contents buffer-or-file)) + (condition-case err + (let ((coding-system-for-read 'utf-8) + (read-circle t) + (start-time (float-time))) ;; FIXME: Reading sometimes fails to read circular objects. ;; I suspect that it happens when we have object reference ;; #N# read before object definition #N=. If it is really ;; so, it should be Emacs bug - either in `read' or in ;; `prin1'. Meanwhile, just fail silently when `read' ;; fails to parse the saved cache object. - (condition-case err - (setq data (read (current-buffer))) - (error - ;; Do not report the known error to user. - (unless (string-match-p "Invalid read syntax" (error-message-string err)) - (warn "Emacs reader failed to read data for %S:%S. The error was: %S" - (or buffer "global") var (error-message-string err))) - (setq data nil)))) - (with-current-buffer (or buffer (current-buffer)) - (cl-loop for var1 in (plist-get index :variable) - do - (when (alist-get var1 data) - (setf (symbol-value var1) (alist-get var1 data))) - (run-hook-with-args 'org-persist-after-read-hook var1 buffer))) - (let ((duration (- (float-time) start-time))) - (when (or (and org-persist--report-time - (numberp org-persist--report-time) - (>= duration org-persist--report-time)) - (and org-persist--report-time - (not (numberp org-persist--report-time)))) - (if buffer - (message "org-persist: Reading %S from %S took %.2f sec" - var buffer duration) - (message "org-persist: Reading %S took %.2f sec" - var duration)))))))) - -(defun org-persist-read-all (&optional buffer) - "Restore all the persistent data in BUFFER." - (unless org-persist--index (org-persist--read-index)) - (dolist (index org-persist--index) - (org-persist-read (plist-get index :variable) buffer))) - -(defun org-persist-read-all-buffer () - "Call `org-persist-read-all' in current buffer." - (org-persist-read-all (current-buffer))) + (prog1 + (read (current-buffer)) + (org-persist--display-time + (- (float-time) start-time) + "Reading from %S" buffer-or-file))) + ;; Recover gracefully if index file is corrupted. + (error + ;; Remove problematic file. + (unless (bufferp buffer-or-file) (delete-file buffer-or-file)) + ;; Do not report the known error to user. + (unless (string-match-p "Invalid read syntax" (error-message-string err)) + (warn "Emacs reader failed to read data in %S. The error was: %S" + buffer-or-file (error-message-string err))) + nil)))) + +(defun org-persist--write-elisp-file (file data &optional no-circular pp) + "Write elisp DATA to FILE." + (let ((print-circle (not no-circular)) + print-level + print-length + print-quoted + (print-escape-control-characters t) + (print-escape-nonascii t) + (print-continuous-numbering t) + print-number-table + (start-time (float-time))) + (unless (file-exists-p (file-name-directory file)) + (make-directory (file-name-directory file) t)) + (with-temp-file file + (if pp + (pp data (current-buffer)) + (prin1 data (current-buffer)))) + (org-persist--display-time + (- (float-time) start-time) + "Writing to %S" file))) + +;;;; Working with index + +(defmacro org-persist-collection-let (collection &rest body) + "Bind container and associated from COLLECTION and execute BODY." + (declare (debug (form body)) (indent 1)) + `(let* ((container (plist-get ,collection :container)) + (associated (plist-get ,collection :associated)) + (path (plist-get associated :file)) + (inode (plist-get associated :inode)) + (hash (plist-get associated :hash)) + (key (plist-get associated :key))) + ,@body)) + +(defun org-persist--find-index (collection) + "Find COLLECTION in `org-persist--index'." + (org-persist-collection-let collection + (and org-persist--index-hash + (catch :found + (dolist (cont (cons container container)) + (let (r) + (setq r (or (gethash (cons cont associated) org-persist--index-hash) + (and path (gethash (cons cont (list :file path)) org-persist--index-hash)) + (and inode (gethash (cons cont (list :inode inode)) org-persist--index-hash)) + (and hash (gethash (cons cont (list :hash hash)) org-persist--index-hash)) + (and key (gethash (cons cont (list :key key)) org-persist--index-hash)))) + (when r (throw :found r)))))))) + +(defun org-persist--add-to-index (collection &optional hash-only) + "Add or update COLLECTION in `org-persist--index'. +When optional HASH-ONLY is non-nil, only modify the hash table. +Return PLIST." + (org-persist-collection-let collection + (let ((existing (org-persist--find-index collection))) + (if existing + (progn + (plist-put existing :container container) + (plist-put (plist-get existing :associated) :file path) + (plist-put (plist-get existing :associated) :inode inode) + (plist-put (plist-get existing :associated) :hash hash) + (plist-put (plist-get existing :associated) :key key) + existing) + (unless hash-only (push collection org-persist--index)) + (unless org-persist--index-hash (setq org-persist--index-hash (make-hash-table :test 'equal))) + (dolist (cont (cons container container)) + (puthash (cons cont associated) collection org-persist--index-hash) + (when path (puthash (cons cont (list :file path)) collection org-persist--index-hash)) + (when inode (puthash (cons cont (list :inode inode)) collection org-persist--index-hash)) + (when hash (puthash (cons cont (list :hash inode)) collection org-persist--index-hash)) + (when key (puthash (cons cont (list :key inode)) collection org-persist--index-hash))) + collection)))) + +(defun org-persist--remove-from-index (collection) + "Remove COLLECTION from `org-persist--index'." + (let ((existing (org-persist--find-index collection))) + (when existing + (org-persist-collection-let collection + (dolist (cont (cons container container)) + (remhash (cons cont associated) org-persist--index-hash) + (when path (remhash (cons cont (list :file path)) org-persist--index-hash)) + (when inode (remhash (cons cont (list :inode inode)) org-persist--index-hash)) + (when hash (remhash (cons cont (list :hash hash)) org-persist--index-hash)) + (when key (remhash (cons cont (list :key key)) org-persist--index-hash)))) + (setq org-persist--index (delq existing org-persist--index))))) + +(defun org-persist--get-collection (container &optional associated &rest misc) + "Return or create collection used to store CONTAINER for ASSOCIATED. +When ASSOCIATED is nil, it is a global CONTAINER. +ASSOCIATED can also be a (:buffer buffer) or buffer, (:file file-path) +or file-path, (:inode inode), (:hash hash), or or (:key key)." + (unless (and (listp container) (listp (car container))) + (setq container (list container))) + (setq associated (org-persist--normalize-associated associated)) + (unless (equal misc '(nil)) + (setq associated (append associated misc))) + (or (org-persist--find-index + `( :container ,(org-persist--normalize-container container) + :associated ,associated)) + (org-persist--add-to-index + (list :container (org-persist--normalize-container container) + :persist-file + (replace-regexp-in-string "^.." "\\&/" (org-id-uuid)) + :associated associated)))) + +;;;; Reading container data. + +(defun org-persist--normalize-container (container) + "Normalize CONTAINER representation into (type . settings)." + (if (and (listp container) (listp (car container))) + (mapcar #'org-persist--normalize-container container) + (pcase container + ((pred symbolp) + (list "elisp" container)) + ((pred stringp) + (list container nil)) + (`(,(or "elisp" "version" "file" "index" "url") . ,_) + container) + (_ (error "org-persist: Unknown container type: %S" container))))) + +(defun org-persist--normalize-associated (associated) + "Normalize ASSOCIATED representation into (:type value)." + (pcase associated + ((or (pred stringp) `(:file ,associated2)) + (when associated2 (setq associated associated2)) + (let* ((rtn `(:file ,associated)) + (inode (and (fboundp 'file-attribute-inode-number) + (file-attribute-inode-number + (file-attributes associated))))) + (when inode (plist-put rtn :inode inode)) + rtn)) + ((or (pred bufferp) `(:buffer ,associated2)) + (when associated2 (setq associated associated2)) + (let* ((file (buffer-file-name + (or (buffer-base-buffer associated) + associated))) + (inode (when (and file + (fboundp 'file-attribute-inode-number)) + (file-attribute-inode-number + (file-attributes file)))) + (hash (secure-hash 'md5 associated))) + (let ((rtn `(:hash ,hash))) + (when file (setq rtn (plist-put rtn :file file))) + (when inode (setq rtn (plist-put rtn :inode inode))) + rtn))) + ((pred listp) + associated) + (_ (error "Unknown associated object %S" associated)))) + +(defmacro org-persist-read:generic (container reference-data collection) + "Read and return the data stored in CONTAINER. +REFERENCE-DATA is associated with CONTAINER in the persist file. +COLLECTION is the plist holding data collectin." + `(let* ((c (org-persist--normalize-container ,container)) + (read-func-symbol (intern (format "org-persist-read:%s" (car c))))) + (setf ,collection (plist-put ,collection :last-access (float-time))) + (unless (fboundp read-func-symbol) + (error "org-persist: Read function %s not defined" + read-func-symbol)) + (funcall read-func-symbol c ,reference-data ,collection))) + +(defun org-persist-read:elisp (_ lisp-value _) + "Read elisp container and return the stored data." + lisp-value) + +(defun org-persist-read:version (container _ _) + "Read version container." + (cadr container)) + +(defun org-persist-read:file (_ path _) + "Read file container." + (when (and path (file-exists-p (concat org-persist-directory path))) + (concat org-persist-directory path))) + +(defun org-persist-read:url (_ path _) + "Read file container." + (when (and path (file-exists-p (concat org-persist-directory path))) + (concat org-persist-directory path))) + +(defun org-persist-read:index (cont index-file _) + "Read index container." + (when (file-exists-p index-file) + (let ((index (org-persist--read-elisp-file index-file))) + (when index + (catch :found + (dolist (collection index) + (org-persist-collection-let collection + (when (and (not associated) + (pcase container + (`(("index" ,version)) + (equal version (cadr cont))) + (_ nil))) + (throw :found index))))))))) + +;;;; Applying container data for side effects. + +(defmacro org-persist-load:generic (container reference-data collection) + "Load the data stored in CONTAINER for side effects. +REFERENCE-DATA is associated with CONTAINER in the persist file. +COLLECTION is the plist holding data collectin." + `(let* ((container (org-persist--normalize-container ,container)) + (load-func-symbol (intern (format "org-persist-load:%s" (car container))))) + (setf ,collection (plist-put ,collection :last-access (float-time))) + (unless (fboundp load-func-symbol) + (error "org-persist: Load function %s not defined" + load-func-symbol)) + (funcall load-func-symbol container ,reference-data ,collection))) + +(defun org-persist-load:elisp (container lisp-value associated) + "Load elisp variable container and assign the data to variable symbol." + (let ((lisp-symbol (cadr container)) + (buffer (when (plist-get associated :file) + (get-file-buffer (plist-get associated :file))))) + (if buffer + (with-current-buffer buffer + (make-variable-buffer-local lisp-symbol) + (set lisp-symbol lisp-value)) + (set lisp-symbol lisp-value)))) + +(defalias 'org-persist-load:version #'org-persist-read:version) +(defalias 'org-persist-load:file #'org-persist-read:file) + +(defun org-persist-load:index (container index-file _) + "Load `org-persist--index'." + (unless org-persist--index + (setq org-persist--index (org-persist-read:index container index-file nil)) + (setq org-persist--index-hash nil) + (if org-persist--index + (mapc (lambda (collection) (org-persist--add-to-index collection 'hash)) org-persist--index) + (setq org-persist--index nil) + (plist-put (org-persist--get-collection container) :expiry 'never)))) + +(defun org-persist--load-index () + "Load `org-persist--index." + (org-persist-load:index + `("index" ,org-persist--storage-version) + (org-file-name-concat org-persist-directory org-persist-index-file) + nil)) + +;;;; Writing container data + +(defmacro org-persist-write:generic (container collection) + "Write CONTAINER in COLLECTION." + `(let* ((c (org-persist--normalize-container ,container)) + (write-func-symbol (intern (format "org-persist-write:%s" (car c))))) + (unless (fboundp write-func-symbol) + (error "org-persist: Write function %s not defined" + write-func-symbol)) + (funcall write-func-symbol c ,collection))) + +(defun org-persist-write:elisp (container collection) + "Write elisp CONTAINER." + (if (and (plist-get (plist-get collection :associated) :file) + (get-file-buffer (plist-get (plist-get collection :associated) :file))) + (buffer-local-value + (cadr container) + (get-file-buffer (plist-get (plist-get collection :associated) :file))) + (symbol-value (cadr container)))) + +(defalias 'org-persist-write:version #'ignore) + +(defun org-persist-write:file (container collection) + "Write file container." + (org-persist-collection-let collection + (when (and path (file-exists-p path)) + (let* ((persist-file (plist-get collection :persist-file)) + (ext (file-name-extension path)) + (file-copy (org-file-name-concat + org-persist-directory + (format "%s-file.%s" persist-file ext)))) + (unless (file-exists-p (file-name-directory file-copy)) + (make-directory (file-name-directory file-copy) t)) + (unless (file-exists-p file-copy) + (copy-file path file-copy 'overwrite)) + (format "%s-file.%s" persist-file ext))))) + +(defun org-persist-write:url (container collection) + "Write url container." + (org-persist-collection-let collection + (when path + (let* ((persist-file (plist-get collection :persist-file)) + (ext (file-name-extension path)) + (file-copy (org-file-name-concat + org-persist-directory + (format "%s-file.%s" persist-file ext)))) + (unless (file-exists-p (file-name-directory file-copy)) + (make-directory (file-name-directory file-copy) t)) + (unless (file-exists-p file-copy) + (url-copy-file path file-copy 'overwrite)) + (format "%s-file.%s" persist-file ext))))) + +(defun org-persist-write:index (container _) + "Write index container." + (org-persist--get-collection container) + (unless (file-exists-p org-persist-directory) + (make-directory org-persist-directory)) + (unless (file-exists-p org-persist-directory) + (warn "Failed to create org-persist storage in %s." + org-persist-directory) + (let ((dir (directory-file-name + (file-name-as-directory org-persist-directory)))) + (while (and (not (file-exists-p dir)) + (not (equal dir (setq dir (directory-file-name + (file-name-directory dir))))))) + (unless (file-writable-p dir) + (message "Missing write access rights to org-persist-directory: %S" + org-persist-directory)))) + (when (file-exists-p org-persist-directory) + (org-persist--write-elisp-file + (org-file-name-concat org-persist-directory org-persist-index-file) + org-persist--index + t t) + t)) + +(defun org-persist--save-index () + "Save `org-persist--index." + (org-persist-write:index + `("index" ,org-persist--storage-version) nil)) + +;;;; Public API + +(cl-defun org-persist-register (container &optional associated &rest misc &key inherit &key (expiry 'never) &allow-other-keys) + "Register CONTAINER in ASSOCIATED to be persistent across Emacs sessions. +Optional key INHERIT makes CONTAINER dependent on another container. +Such dependency means that data shared between variables will be +preserved (see elisp#Circular Objects). +Optional key EXPIRY will set the expiry condition of the container. +It can be `never', `nil' - until end of session, a number of days since +last access, or a function accepting a single argument - collection. +EXPIRY key has no effect when INHERIT is non-nil." + (unless org-persist--index (org-persist--load-index)) + (setq container (org-persist--normalize-container container)) + (when inherit + (setq inherit (org-persist--normalize-container inherit)) + (let ((inherited-collection (org-persist--get-collection inherit associated)) + new-collection) + (unless (member container (plist-get inherited-collection :container)) + (setq new-collection + (plist-put (copy-sequence inherited-collection) :container + (cons container (plist-get inherited-collection :container)))) + (org-persist--remove-from-index inherited-collection) + (org-persist--add-to-index new-collection)))) + (let ((collection (org-persist--get-collection container associated misc))) + (when (and expiry (not inherit)) + (when expiry (plist-put collection :expiry expiry)))) + (when (or (bufferp associated) (bufferp (plist-get associated :buffer))) + (with-current-buffer (if (bufferp associated) + associated + (plist-get associated :buffer)) + (add-hook 'kill-buffer-hook #'org-persist-write-all-buffer nil 'local)))) + +(defun org-persist-unregister (container &optional associated) + "Unregister CONTAINER in ASSOCIATED to be persistent. +When ASSOCIATED is `all', unregister CONTAINER everywhere." + (unless org-persist--index (org-persist--load-index)) + (if (eq associated 'all) + (mapc (lambda (collection) + (when (member container (plist-get collection :container)) + (org-persist-unregister container (plist-get collection :associated)))) + org-persist--index) + (let ((collection (org-persist--get-collection container associated))) + (if (= (length (plist-get collection :container)) 1) + (org-persist--remove-from-index collection) + (plist-put collection :container + (remove container (plist-get collection :container))) + (org-persist--add-to-index collection))))) + +(defun org-persist-read (container &optional associated hash-must-match load?) + "Restore CONTAINER data for ASSOCIATED. +When HASH-MUST-MATCH is non-nil, do not restore data if hash for +ASSOCIATED file or buffer does not match. +ASSOCIATED can be a plist, a buffer, or a string. +A buffer is treated as (:buffer ASSOCIATED). +A string is treated as (:file ASSOCIATED)." + (setq associated (org-persist--normalize-associated associated)) + (setq container (org-persist--normalize-container container)) + (let* ((collection (org-persist--get-collection container associated)) + (persist-file (org-file-name-concat org-persist-directory (plist-get collection :persist-file))) + (data nil)) + (when (and collection + (file-exists-p persist-file) + (or (not hash-must-match) + (and (plist-get associated :hash) + (equal (plist-get associated :hash) + (plist-get (plist-get collection :associated) :hash))))) + (unless (seq-find (lambda (v) + (run-hook-with-args-until-success 'org-persist-before-read-hook v associated)) + (plist-get collection :container)) + (setq data (org-persist--read-elisp-file persist-file)) + (cl-loop for container in (plist-get collection :container) + with result = nil + do + (if load? + (push (org-persist-load:generic container (alist-get container data nil nil #'equal) collection) result) + (push (org-persist-read:generic container (alist-get container data nil nil #'equal) collection) result)) + (run-hook-with-args 'org-persist-after-read-hook container associated) + finally return (if (= 1 (length result)) (car result) result)))))) + +(defun org-persist-load (container &optional associated hash-must-match) + "Load CONTAINER data for ASSOCIATED. +The arguments have the same meaning as in `org-persist-read'." + (org-persist-read container associated hash-must-match t)) + +(defun org-persist-load-all (&optional associated) + "Restore all the persistent data associated with ASSOCIATED." + (unless org-persist--index (org-persist--load-index)) + (setq associated (org-persist--normalize-associated associated)) + (let (all-containers) + (dolist (collection org-persist--index) + (when collection + (cl-pushnew (plist-get collection :container) all-containers :test #'equal))) + (dolist (container all-containers) + (org-persist-load container associated t)))) + +(defun org-persist-load-all-buffer () + "Call `org-persist-load-all' in current buffer." + (org-persist-load-all (current-buffer))) + +(defun org-persist-write (container &optional associated) + "Save CONTAINER according to ASSOCIATED. +ASSOCIATED can be a plist, a buffer, or a string. +A buffer is treated as (:buffer ASSOCIATED). +A string is treated as (:file ASSOCIATED)." + (setq associated (org-persist--normalize-associated associated)) + (let ((collection (org-persist--get-collection container associated))) + (setf collection (plist-put collection :associated associated)) + (unless (seq-find (lambda (v) + (run-hook-with-args-until-success 'org-persist-before-write-hook v associated)) + (plist-get collection :container)) + (when (or (file-exists-p org-persist-directory) (org-persist--save-index)) + (let ((file (org-file-name-concat org-persist-directory (plist-get collection :persist-file))) + (data (mapcar (lambda (c) (cons c (org-persist-write:generic c collection))) + (plist-get collection :container)))) + (org-persist--write-elisp-file file data)))))) + +(defun org-persist-write-all (&optional associated) + "Save all the persistent data." + (unless org-persist--index (org-persist--load-index)) + (setq associated (org-persist--normalize-associated associated)) + (let (all-containers) + (dolist (collection org-persist--index) + (if associated + (when collection + (cl-pushnew (plist-get collection :container) all-containers :test #'equal)) + (org-persist-write (plist-get collection :container) (plist-get collection :associated)))) + (dolist (container all-containers) + (when (org-persist--find-index `(:container ,container :associated ,associated)) + (org-persist-write container associated))))) + +(defun org-persist-write-all-buffer () + "Call `org-persist-write-all' in current buffer. +Do nothing in an indirect buffer." + (unless (buffer-base-buffer (current-buffer)) + (org-persist-write-all (current-buffer)))) + +(defmacro org-persist-gc:generic (container collection) + "Garbage collect CONTAINER data from COLLECTION." + `(let* ((c (org-persist--normalize-container ,container)) + (gc-func-symbol (intern (format "org-persist-gc:%s" (car c))))) + (unless (fboundp gc-func-symbol) + (error "org-persist: GC function %s not defined" + gc-func-symbol)) + (funcall gc-func-symbol c ,collection))) + +(defalias 'org-persist-gc:elisp #'ignore) +(defalias 'org-persist-gc:index #'ignore) + +(defun org-persist-gc:file (container collection) + "Garbage collect file container." + (let ((file (org-persist-read container (plist-get collection :associated)))) + (when (file-exists-p file) + (delete-file file)))) + +(defun org-persist-gc:url (container collection) + "Garbage collect url container." + (let ((file (org-persist-read container (plist-get collection :associated)))) + (when (file-exists-p file) + (delete-file file)))) + +(defmacro org-persist--gc-persist-file (persist-file) + "Garbage collect PERSIST-FILE." + `(when (file-exists-p ,persist-file) + (delete-file ,persist-file) + (when (org-directory-empty-p (file-name-directory ,persist-file)) + (delete-directory (file-name-directory ,persist-file))))) + +(defmacro org-persist--gc-expired-p (cnd collection) + "Check if expiry condition CND triggers." + `(pcase ,cnd + (`nil t) + (`never nil) + ((pred numberp) + (<= (float-time) (+ (plist-get ,collection :access-time) (* ,cnd 24 60 60)))) + ((pred functionp) + (funcall ,cnd ,collection)) + (_ (error "org-persist: Unsupported expiry type %S" cnd)))) (defun org-persist-gc () - "Remove stored data for not existing files or unregistered variables." - (let (new-index) - (dolist (index org-persist--index) - (let ((file (plist-get index :path)) - (persist-file (when (plist-get index :persist-file) - (org-file-name-concat - org-persist-directory - (plist-get index :persist-file))))) - (when (and file persist-file) - (if (file-exists-p file) - (push index new-index) - (when (file-exists-p persist-file) - (delete-file persist-file) - (when (org-directory-empty-p (file-name-directory persist-file)) - (delete-directory (file-name-directory persist-file)))))))) + "Remove expired or unregisted containers. +Also, remove containers associated with non-existing files." + (let (new-index (remote-files-num 0)) + (dolist (collection org-persist--index) + (let* ((file (plist-get (plist-get collection :associated) :file)) + (file-remote (when file (file-remote-p file))) + (persist-file (when (plist-get collection :persist-file) + (org-file-name-concat + org-persist-directory + (plist-get collection :persist-file)))) + (expired? (org-persist--gc-expired-p + (plist-get collection :expiry) collection))) + (when persist-file + (when file + (when file-remote (cl-incf remote-files-num)) + (unless (if (not file-remote) + (file-exists-p file) + (pcase org-persist-remote-files + ('t t) + ('check-existence + (file-exists-p file)) + ((pred #'numberp) + (<= org-persist-remote-files remote-files-num)) + (_ nil))) + (setq expired? t))) + (if expired? + (org-persist--gc-persist-file persist-file) + (push collection new-index))))) (setq org-persist--index (nreverse new-index)))) ;; Automatically write the data, but only when we have write access. @@ -353,7 +777,7 @@ When BUFFER is `all', unregister VAR in all buffers." ;; hook after `org-persist-write-all'. (add-hook 'kill-emacs-hook #'org-persist-gc))) -(add-hook 'after-init-hook #'org-persist-read-all) +(add-hook 'after-init-hook #'org-persist-load-all) (provide 'org-persist) diff --git a/lisp/org.el b/lisp/org.el index cb5d52c76e..4bd8a6c994 100644 --- a/lisp/org.el +++ b/lisp/org.el @@ -191,7 +191,7 @@ Stars are put in group 1 and the trimmed body in group 2.") (declare-function org-latex-make-preamble "ox-latex" (info &optional template snippet?)) (declare-function org-num-mode "org-num" (&optional arg)) (declare-function org-plot/gnuplot "org-plot" (&optional params)) -(declare-function org-persist-read "org-persist" (var &optional buffer)) +(declare-function org-persist-load "org-persist" (container &optional associated hash-must-match)) (declare-function org-tags-view "org-agenda" (&optional todo-only match)) (declare-function org-timer "org-timer" (&optional restart no-insert)) (declare-function org-timer-item "org-timer" (&optional arg)) @@ -4893,6 +4893,11 @@ The following commands are available: (org-setup-filling) ;; Comments. (org-setup-comments-handling) + ;; Initialize cache. + (org-element-cache-reset) + (when (and org-element-cache-persistent + org-element-use-cache) + (org-persist-load 'org-element--cache (current-buffer) t)) ;; Beginning/end of defun (setq-local beginning-of-defun-function 'org-backward-element) (setq-local end-of-defun-function