| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360 | 
							- ;;; org-persist.el --- Persist data across Emacs sessions         -*- lexical-binding: t; -*-
 
- ;; Copyright (C) 2021-2022 Free Software Foundation, Inc.
 
- ;; Author: Ihor Radchenko <yantar92 at gmail dot com>
 
- ;; Keywords: cache, storage
 
- ;; This file is part of GNU Emacs.
 
- ;; GNU Emacs is free software: you can redistribute it and/or modify
 
- ;; it under the terms of the GNU General Public License as published by
 
- ;; the Free Software Foundation, either version 3 of the License, or
 
- ;; (at your option) any later version.
 
- ;; GNU Emacs is distributed in the hope that it will be useful,
 
- ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
 
- ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
- ;; GNU General Public License for more details.
 
- ;; You should have received a copy of the GNU General Public License
 
- ;; along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.
 
- ;;; Commentary:
 
- ;;
 
- ;; This file implements persistant data storage across Emacs sessions.
 
- ;; Both global and buffer-local data can be stored.
 
- ;;; Code:
 
- (require 'org-compat)
 
- (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))
 
- (defgroup org-persist nil
 
-   "Persistent cache for Org mode."
 
-   :tag "Org persist"
 
-   :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/"))
 
-   "Directory where the data is stored."
 
-   :group 'org-persist
 
-   :type 'directory)
 
- (defvar 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.
 
- 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.
 
- 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.
 
- 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:
 
-   - `:variable'    list of variables 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")
 
- (defvar org-persist--report-time 0.5
 
-   "Whether to report read/write time.
 
- When the value is a number, it is a threshold number of seconds.  If
 
- the read/write time of a single variable exceeds the threashold, a
 
- 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))
 
-           ;; 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)))
 
- (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))))))))
 
-     (setq org-persist--index (nreverse new-index))))
 
- ;; Automatically write the data, but only when we have write access.
 
- (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)))))))
 
-   (if (not (file-writable-p dir))
 
-       (message "Missing write access rights to org-persist-directory: %S"
 
-                org-persist-directory)
 
-     (add-hook 'kill-emacs-hook #'org-persist-write-all)
 
-     ;; `org-persist-gc' should run before `org-persist-write-all'.  So we are adding the
 
-     ;; hook after `org-persist-write-all'.
 
-     (add-hook 'kill-emacs-hook #'org-persist-gc)))
 
- (add-hook 'after-init-hook #'org-persist-read-all)
 
- (provide 'org-persist)
 
- ;;; org-persist.el ends here
 
 
  |