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.
START can be
a number or marker,
:before-last - the position at the start of the last s-expression
nil or any other value - the value of `point-min'.
END can be
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) (goto-char (point-min))
((eq :before-last start) (forward-list)
(goto-char (point-max)) (backward-list)
(backward-list)) (point)))
(t (point-min))))
(end (cond ((number-or-marker-p end) end) (defun chronometrist-rest-end (file)
((eq :before-last end) (chronometrist-sexp-in-file file
(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
:append if a new s-expression was added to the end,
Return :modify if the last s-expression was modified,
:append if a new s-expression was added to the end, :remove if the last s-expression was removed,
:modify if the last s-expression was modified, nil if the contents didn't change, and
:remove if the last s-expression was removed, t for any other change."
nil if the contents didn't change, and (with-slots
t for any other change." (file file-watch
(-let* ;; The slots contain the old state of the file.
(((last-start last-end) (plist-get state :last)) hash-table
((rest-start rest-end rest-hash) (plist-get state :rest)) rest-start rest-end rest-hash
(last-expr-file (chronometrist-read-from last-start)) file-length last-hash) backend
(last-expr-ht (chronometrist-events-last)) (let* ((new-length (chronometrist-file-length file))
(file (chronometrist-backend-file (chronometrist-active-backend))) (new-rest-hash (chronometrist-file-hash rest-start rest-end file))
(last-same-p (equal last-expr-ht last-expr-file)) (new-last-hash (chronometrist-file-hash rest-end new-length file)))
(file-new-length (chronometrist-sexp-in-file file (point-max))) (cond ((and (= file-length new-length)
(rest-same-p (unless (< file-new-length rest-end) (equal rest-hash new-rest-hash)
(--> (chronometrist-file-hash rest-start rest-end t) (equal last-hash new-last-hash))
(cl-third it) nil)
(equal rest-hash it))))) ((or (< new-length rest-end) ;; File has shrunk so much that we cannot compare rest-hash.
;; (message "chronometrist - last-start\nlast-expr-file - %S\nlast-expr-ht - %S" (not (equal rest-hash new-rest-hash)))
;; last-expr-file t)
;; last-expr-ht) ;; From here on, it is implicit that the change has happened at the end of the file.
;; (message "chronometrist - last-same-p - %S, rest-same-p - %S" ((and (< file-length new-length) ;; File has grown.
;; last-same-p rest-same-p) (equal last-hash new-last-hash))
(cond ((not rest-same-p) t) :append)
(last-same-p ((and (< new-length file-length) ;; File has shrunk.
(when (chronometrist-read-from last-end) :append)) (not (chronometrist-sexp-in-file file
((not (chronometrist-read-from last-start)) (goto-char rest-end)
:remove) (ignore-errors
((not (chronometrist-read-from (read (current-buffer)))))) ;; There is no sexp after rest-end.
(lambda () :remove)
(progn (goto-char last-start) (t :modify)))))
(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,9 +1276,9 @@ 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)
(file-state-bound-p (and rest-start rest-end rest-hash (file-state-bound-p (and rest-start rest-end rest-hash
file-length last-hash)) file-length last-hash))
@ -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
**** indices and hashes
#+BEGIN_SRC emacs-lisp
(defun chronometrist-rest-start (file)
(chronometrist-sexp-in-file file
(goto-char (point-min))
(forward-list)
(backward-list)
(point)))
(defun chronometrist-rest-end (file)
(chronometrist-sexp-in-file file
(goto-char (point-max))
(backward-list 2)
(forward-list)
(point)))
(defun chronometrist-file-length (file)
(chronometrist-sexp-in-file file (point-max)))
#+END_SRC
**** file-hash :reader: **** file-hash :reader:
#+BEGIN_SRC emacs-lisp #+BEGIN_SRC emacs-lisp
(cl-defun chronometrist-file-hash (&optional start end hash (file (chronometrist-backend-file (chronometrist-active-backend)))) (cl-defun chronometrist-file-hash (start end &optional (file (chronometrist-backend-file (chronometrist-active-backend))))
"Calculate hash of `chronometrist-file' between START and END. "Calculate hash of `chronometrist-file' between START and END."
START can be
a number or marker,
:before-last - the position at the start of the last s-expression
nil or any other value - the value of `point-min'.
END can be
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) (secure-hash 'sha1
((eq :before-last start) (buffer-substring-no-properties start end))))
(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))
(backward-list 2)
(forward-list))
(t (goto-char (point-max))
(backward-list)
(forward-list)))))
(if hash
(--> (buffer-substring-no-properties start end)
(secure-hash 'sha1 it)
(list start end it))
(list start end)))))
#+END_SRC #+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
:append if a new s-expression was added to the end,
Return :modify if the last s-expression was modified,
:append if a new s-expression was added to the end, :remove if the last s-expression was removed,
:modify if the last s-expression was modified, nil if the contents didn't change, and
:remove if the last s-expression was removed, t for any other change."
nil if the contents didn't change, and (with-slots
t for any other change." (file file-watch
(-let* ;; The slots contain the old state of the file.
(((last-start last-end) (plist-get state :last)) hash-table
((rest-start rest-end rest-hash) (plist-get state :rest)) rest-start rest-end rest-hash
(last-expr-file (chronometrist-read-from last-start)) file-length last-hash) backend
(last-expr-ht (chronometrist-events-last)) (let* ((new-length (chronometrist-file-length file))
(file (chronometrist-backend-file (chronometrist-active-backend))) (new-rest-hash (chronometrist-file-hash rest-start rest-end file))
(last-same-p (equal last-expr-ht last-expr-file)) (new-last-hash (chronometrist-file-hash rest-end new-length file)))
(file-new-length (chronometrist-sexp-in-file file (point-max))) (cond ((and (= file-length new-length)
(rest-same-p (unless (< file-new-length rest-end) (equal rest-hash new-rest-hash)
(--> (chronometrist-file-hash rest-start rest-end t) (equal last-hash new-last-hash))
(cl-third it) nil)
(equal rest-hash it))))) ((or (< new-length rest-end) ;; File has shrunk so much that we cannot compare rest-hash.
;; (message "chronometrist - last-start\nlast-expr-file - %S\nlast-expr-ht - %S" (not (equal rest-hash new-rest-hash)))
;; last-expr-file t)
;; last-expr-ht) ;; From here on, it is implicit that the change has happened at the end of the file.
;; (message "chronometrist - last-same-p - %S, rest-same-p - %S" ((and (< file-length new-length) ;; File has grown.
;; last-same-p rest-same-p) (equal last-hash new-last-hash))
(cond ((not rest-same-p) t) :append)
(last-same-p ((and (< new-length file-length) ;; File has shrunk.
(when (chronometrist-read-from last-end) :append)) (not (chronometrist-sexp-in-file file
((not (chronometrist-read-from last-start)) (goto-char rest-end)
:remove) (ignore-errors
((not (chronometrist-read-from (read (current-buffer)))))) ;; There is no sexp after rest-end.
(lambda () :remove)
(progn (goto-char last-start) (t :modify)))))
(forward-list)))))
:modify))))
#+END_SRC #+END_SRC
***** tests ***** tests
@ -2049,9 +2033,9 @@ 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)
(file-state-bound-p (and rest-start rest-end rest-hash (file-state-bound-p (and rest-start rest-end rest-hash
file-length last-hash)) file-length last-hash))
@ -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)