Rewrite file-change-type, update on-change

This commit is contained in:
contrapunctus 2022-01-08 23:26:03 +05:30
parent 41728d511c
commit 0c4e3ef47c
2 changed files with 154 additions and 187 deletions

View File

@ -1149,91 +1149,69 @@ expression first)."
(read (current-buffer)))))) (read (current-buffer))))))
;; backend-empty-p:1 ends here ;; backend-empty-p:1 ends here
;; [[file:chronometrist.org::*file-hash][file-hash:1]] ;; [[file:chronometrist.org::*indices and hashes][indices and hashes:1]]
(cl-defun chronometrist-file-hash (&optional start end hash (file (chronometrist-backend-file (chronometrist-active-backend)))) (defun chronometrist-rest-start (file)
"Calculate hash of `chronometrist-file' between START and END. (chronometrist-sexp-in-file file
START can be (goto-char (point-min))
a number or marker, (forward-list)
:before-last - the position at the start of the last s-expression (backward-list)
nil or any other value - the value of `point-min'. (point)))
END can be (defun chronometrist-rest-end (file)
a number or marker,
:before-last - the position at the end of the second-last s-expression,
nil or any other value - the position at the end of the last s-expression.
Return (START END) if HASH is nil, else (START END HASH).
Return a list in the form (A B HASH), where A and B are markers
in `chronometrist-file' describing the region for which HASH was calculated."
(chronometrist-sexp-in-file file (chronometrist-sexp-in-file file
(let* ((start (cond ((number-or-marker-p start) start)
((eq :before-last start)
(goto-char (point-max))
(backward-list))
(t (point-min))))
(end (cond ((number-or-marker-p end) end)
((eq :before-last end)
(goto-char (point-max)) (goto-char (point-max))
(backward-list 2) (backward-list 2)
(forward-list)) (forward-list)
(t (goto-char (point-max)) (point)))
(backward-list)
(forward-list))))) (defun chronometrist-file-length (file)
(if hash (chronometrist-sexp-in-file file (point-max)))
(--> (buffer-substring-no-properties start end) ;; indices and hashes:1 ends here
(secure-hash 'sha1 it)
(list start end it)) ;; [[file:chronometrist.org::*file-hash][file-hash:1]]
(list start end))))) (cl-defun chronometrist-file-hash (start end &optional (file (chronometrist-backend-file (chronometrist-active-backend))))
"Calculate hash of `chronometrist-file' between START and END."
(chronometrist-sexp-in-file file
(secure-hash 'sha1
(buffer-substring-no-properties start end))))
;; file-hash:1 ends here ;; file-hash:1 ends here
;; [[file:chronometrist.org::*read-from][read-from:1]]
(cl-defun chronometrist-read-from (position &optional (file (chronometrist-backend-file (chronometrist-active-backend))))
(chronometrist-sexp-in-file file
(goto-char (if (number-or-marker-p position)
position
(funcall position)))
(ignore-errors (read (current-buffer)))))
;; read-from:1 ends here
;; [[file:chronometrist.org::*file-change-type][file-change-type:1]] ;; [[file:chronometrist.org::*file-change-type][file-change-type:1]]
(defun chronometrist-file-change-type (state) (defun chronometrist-file-change-type (backend)
"Determine the type of change made to `chronometrist-file'. "Determine the type of change made to BACKEND's file.
STATE must be a plist. (see `chronometrist--file-state')
Return Return
:append if a new s-expression was added to the end, :append if a new s-expression was added to the end,
:modify if the last s-expression was modified, :modify if the last s-expression was modified,
:remove if the last s-expression was removed, :remove if the last s-expression was removed,
nil if the contents didn't change, and nil if the contents didn't change, and
t for any other change." t for any other change."
(-let* (with-slots
(((last-start last-end) (plist-get state :last)) (file file-watch
((rest-start rest-end rest-hash) (plist-get state :rest)) ;; The slots contain the old state of the file.
(last-expr-file (chronometrist-read-from last-start)) hash-table
(last-expr-ht (chronometrist-events-last)) rest-start rest-end rest-hash
(file (chronometrist-backend-file (chronometrist-active-backend))) file-length last-hash) backend
(last-same-p (equal last-expr-ht last-expr-file)) (let* ((new-length (chronometrist-file-length file))
(file-new-length (chronometrist-sexp-in-file file (point-max))) (new-rest-hash (chronometrist-file-hash rest-start rest-end file))
(rest-same-p (unless (< file-new-length rest-end) (new-last-hash (chronometrist-file-hash rest-end new-length file)))
(--> (chronometrist-file-hash rest-start rest-end t) (cond ((and (= file-length new-length)
(cl-third it) (equal rest-hash new-rest-hash)
(equal rest-hash it))))) (equal last-hash new-last-hash))
;; (message "chronometrist - last-start\nlast-expr-file - %S\nlast-expr-ht - %S" nil)
;; last-expr-file ((or (< new-length rest-end) ;; File has shrunk so much that we cannot compare rest-hash.
;; last-expr-ht) (not (equal rest-hash new-rest-hash)))
;; (message "chronometrist - last-same-p - %S, rest-same-p - %S" t)
;; last-same-p rest-same-p) ;; From here on, it is implicit that the change has happened at the end of the file.
(cond ((not rest-same-p) t) ((and (< file-length new-length) ;; File has grown.
(last-same-p (equal last-hash new-last-hash))
(when (chronometrist-read-from last-end) :append)) :append)
((not (chronometrist-read-from last-start)) ((and (< new-length file-length) ;; File has shrunk.
(not (chronometrist-sexp-in-file file
(goto-char rest-end)
(ignore-errors
(read (current-buffer)))))) ;; There is no sexp after rest-end.
:remove) :remove)
((not (chronometrist-read-from (t :modify)))))
(lambda ()
(progn (goto-char last-start)
(forward-list)))))
:modify))))
;; file-change-type:1 ends here ;; file-change-type:1 ends here
;; [[file:chronometrist.org::*reset-task-list][reset-task-list:1]] ;; [[file:chronometrist.org::*reset-task-list][reset-task-list:1]]
@ -1298,7 +1276,7 @@ This may happen within Chronometrist (through the backend
protocol) or outside it (e.g. a user editing the backend file). protocol) or outside it (e.g. a user editing the backend file).
FS-EVENT is the event passed by the `filenotify' library (see `file-notify-add-watch')." FS-EVENT is the event passed by the `filenotify' library (see `file-notify-add-watch')."
(with-slots (hash-table file-watch (with-slots (file hash-table file-watch
rest-start rest-end rest-hash rest-start rest-end rest-hash
file-length last-hash) backend file-length last-hash) backend
(-let* (((_ action _ _) fs-event) (-let* (((_ action _ _) fs-event)
@ -1324,9 +1302,11 @@ FS-EVENT is the event passed by the `filenotify' library (see `file-notify-add-w
;; The last s-expression in the file was removed ;; The last s-expression in the file was removed
(:remove (chronometrist-on-remove backend)) (:remove (chronometrist-on-remove backend))
((pred null) nil)))) ((pred null) nil))))
(setf file-state (setf rest-start (chronometrist-rest-start file)
(list :last (chronometrist-file-hash :before-last nil) rest-end (chronometrist-rest-end file)
:rest (chronometrist-file-hash nil :before-last t)))))) file-length (chronometrist-file-length file)
last-hash (chronometrist-file-hash rest-end file-length file)
rest-hash (chronometrist-file-hash rest-start rest-end file)))))
;; on-change:1 ends here ;; on-change:1 ends here
;; [[file:chronometrist.org::*backend][backend:1]] ;; [[file:chronometrist.org::*backend][backend:1]]

View File

@ -1804,44 +1804,35 @@ expression first)."
(read (current-buffer)))))) (read (current-buffer))))))
#+END_SRC #+END_SRC
**** file-hash :reader: **** indices and hashes
#+BEGIN_SRC emacs-lisp #+BEGIN_SRC emacs-lisp
(cl-defun chronometrist-file-hash (&optional start end hash (file (chronometrist-backend-file (chronometrist-active-backend)))) (defun chronometrist-rest-start (file)
"Calculate hash of `chronometrist-file' between START and END. (chronometrist-sexp-in-file file
START can be (goto-char (point-min))
a number or marker, (forward-list)
:before-last - the position at the start of the last s-expression (backward-list)
nil or any other value - the value of `point-min'. (point)))
END can be (defun chronometrist-rest-end (file)
a number or marker,
:before-last - the position at the end of the second-last s-expression,
nil or any other value - the position at the end of the last s-expression.
Return (START END) if HASH is nil, else (START END HASH).
Return a list in the form (A B HASH), where A and B are markers
in `chronometrist-file' describing the region for which HASH was calculated."
(chronometrist-sexp-in-file file (chronometrist-sexp-in-file file
(let* ((start (cond ((number-or-marker-p start) start)
((eq :before-last start)
(goto-char (point-max))
(backward-list))
(t (point-min))))
(end (cond ((number-or-marker-p end) end)
((eq :before-last end)
(goto-char (point-max)) (goto-char (point-max))
(backward-list 2) (backward-list 2)
(forward-list)) (forward-list)
(t (goto-char (point-max)) (point)))
(backward-list)
(forward-list))))) (defun chronometrist-file-length (file)
(if hash (chronometrist-sexp-in-file file (point-max)))
(--> (buffer-substring-no-properties start end)
(secure-hash 'sha1 it)
(list start end it))
(list start end)))))
#+END_SRC #+END_SRC
**** file-hash :reader:
#+BEGIN_SRC emacs-lisp
(cl-defun chronometrist-file-hash (start end &optional (file (chronometrist-backend-file (chronometrist-active-backend))))
"Calculate hash of `chronometrist-file' between START and END."
(chronometrist-sexp-in-file file
(secure-hash 'sha1
(buffer-substring-no-properties start end))))
#+END_SRC
***** tests ***** tests
#+BEGIN_SRC emacs-lisp :tangle chronometrist-tests.el :load test #+BEGIN_SRC emacs-lisp :tangle chronometrist-tests.el :load test
(ert-deftest file-hash () (ert-deftest file-hash ()
@ -1856,71 +1847,64 @@ in `chronometrist-file' describing the region for which HASH was calculated."
(should (= 1256 last-start)) (should (= 1256 last-start))
(should (= 1426 last-end)))) (should (= 1426 last-end))))
#+END_SRC #+END_SRC
**** read-from :reader: **** file-change-type :reader:
#+BEGIN_SRC emacs-lisp + rest-start - start of first sexp
(cl-defun chronometrist-read-from (position &optional (file (chronometrist-backend-file (chronometrist-active-backend)))) + rest-end - end of second last sexp
(chronometrist-sexp-in-file file + file-length - end of file
(goto-char (if (number-or-marker-p position)
position
(funcall position)))
(ignore-errors (read (current-buffer)))))
#+END_SRC
**** TODO file-change-type :reader: + rest-hash - hash of content between rest-start and rest-end
1. [ ] add newline after last expression and save => nil + last-hash - hash of content between rest-end and file-length
2. [ ] remove newline after last expession and save => nil
The initial idea was to use two pairs of hashes, one for the content between the start of the file up to the last expression, and the other for the last expression itself. However, in the latter case, this can cause issues - + ht-last-sexp - last sexp in memory
+ the expression may shrink, and if we try to compute the hash of the previously-known region again, we will get an args-out-of-range error. + file-sexp-after-rest - sexp after rest-end
+ false negatives for whitespace/indentation differences. + file-last-sexp - last sexp in file
Thus, we use =read= for the last expression.
Possible states | situation | rest-hash | last-hash | file-sexp-after-rest | file-last-sexp | file-length |
: <rest-start> <rest-end> <last-start> <last-end> |--------------+-----------+-----------+----------------------+-----------------------------------------+------------------------------|
1. :append - rest same, last same, new expr after last-end | no change | same | same | same as ht-last-sexp | same as ht-last-sexp and file-last-sexp | same |
2. :modify - rest same, last not same, no expr after last-end | append | same | same | - | (new s-expression) | always greater |
3. :remove - rest same, last not same, no expr after last-start | modify | same | changed | changed | changed | may be smaller |
4. nil - rest same, last same, no expr after last-end | remove | same | changed | nil | same as second last sexp | always smaller |
5. t - rest changed | other change | changed | - | | - | may be smaller than rest-end |
We avoid comparing s-expressions in the file with the contents of the hash table, since the last s-expression might be represented differently in the hash tables of different elisp-sexp backends. Additionally, in =:modify= as well as =nil= situations, there is no s-expression after old-file-length.
#+BEGIN_SRC emacs-lisp #+BEGIN_SRC emacs-lisp
(defun chronometrist-file-change-type (state) (defun chronometrist-file-change-type (backend)
"Determine the type of change made to `chronometrist-file'. "Determine the type of change made to BACKEND's file.
STATE must be a plist. (see `chronometrist--file-state')
Return Return
:append if a new s-expression was added to the end, :append if a new s-expression was added to the end,
:modify if the last s-expression was modified, :modify if the last s-expression was modified,
:remove if the last s-expression was removed, :remove if the last s-expression was removed,
nil if the contents didn't change, and nil if the contents didn't change, and
t for any other change." t for any other change."
(-let* (with-slots
(((last-start last-end) (plist-get state :last)) (file file-watch
((rest-start rest-end rest-hash) (plist-get state :rest)) ;; The slots contain the old state of the file.
(last-expr-file (chronometrist-read-from last-start)) hash-table
(last-expr-ht (chronometrist-events-last)) rest-start rest-end rest-hash
(file (chronometrist-backend-file (chronometrist-active-backend))) file-length last-hash) backend
(last-same-p (equal last-expr-ht last-expr-file)) (let* ((new-length (chronometrist-file-length file))
(file-new-length (chronometrist-sexp-in-file file (point-max))) (new-rest-hash (chronometrist-file-hash rest-start rest-end file))
(rest-same-p (unless (< file-new-length rest-end) (new-last-hash (chronometrist-file-hash rest-end new-length file)))
(--> (chronometrist-file-hash rest-start rest-end t) (cond ((and (= file-length new-length)
(cl-third it) (equal rest-hash new-rest-hash)
(equal rest-hash it))))) (equal last-hash new-last-hash))
;; (message "chronometrist - last-start\nlast-expr-file - %S\nlast-expr-ht - %S" nil)
;; last-expr-file ((or (< new-length rest-end) ;; File has shrunk so much that we cannot compare rest-hash.
;; last-expr-ht) (not (equal rest-hash new-rest-hash)))
;; (message "chronometrist - last-same-p - %S, rest-same-p - %S" t)
;; last-same-p rest-same-p) ;; From here on, it is implicit that the change has happened at the end of the file.
(cond ((not rest-same-p) t) ((and (< file-length new-length) ;; File has grown.
(last-same-p (equal last-hash new-last-hash))
(when (chronometrist-read-from last-end) :append)) :append)
((not (chronometrist-read-from last-start)) ((and (< new-length file-length) ;; File has shrunk.
(not (chronometrist-sexp-in-file file
(goto-char rest-end)
(ignore-errors
(read (current-buffer)))))) ;; There is no sexp after rest-end.
:remove) :remove)
((not (chronometrist-read-from (t :modify)))))
(lambda ()
(progn (goto-char last-start)
(forward-list)))))
:modify))))
#+END_SRC #+END_SRC
***** tests ***** tests
@ -2049,7 +2033,7 @@ This may happen within Chronometrist (through the backend
protocol) or outside it (e.g. a user editing the backend file). protocol) or outside it (e.g. a user editing the backend file).
FS-EVENT is the event passed by the `filenotify' library (see `file-notify-add-watch')." FS-EVENT is the event passed by the `filenotify' library (see `file-notify-add-watch')."
(with-slots (hash-table file-watch (with-slots (file hash-table file-watch
rest-start rest-end rest-hash rest-start rest-end rest-hash
file-length last-hash) backend file-length last-hash) backend
(-let* (((_ action _ _) fs-event) (-let* (((_ action _ _) fs-event)
@ -2075,9 +2059,11 @@ FS-EVENT is the event passed by the `filenotify' library (see `file-notify-add-w
;; The last s-expression in the file was removed ;; The last s-expression in the file was removed
(:remove (chronometrist-on-remove backend)) (:remove (chronometrist-on-remove backend))
((pred null) nil)))) ((pred null) nil))))
(setf file-state (setf rest-start (chronometrist-rest-start file)
(list :last (chronometrist-file-hash :before-last nil) rest-end (chronometrist-rest-end file)
:rest (chronometrist-file-hash nil :before-last t)))))) file-length (chronometrist-file-length file)
last-hash (chronometrist-file-hash rest-end file-length file)
rest-hash (chronometrist-file-hash rest-start rest-end file)))))
#+END_SRC #+END_SRC
*** plist backend *** plist backend
@ -2812,6 +2798,7 @@ See `timeclock-log-data' for a description."
(goto-char (point-at-bol)))) (goto-char (point-at-bol))))
nil))) nil)))
#+END_SRC #+END_SRC
*** timelog-file-to-sexp-file :writer: *** timelog-file-to-sexp-file :writer:
#+BEGIN_SRC emacs-lisp #+BEGIN_SRC emacs-lisp
(defvar timeclock-file) (defvar timeclock-file)