forked from jcfk/savefold.el
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsavefold-utils.el
229 lines (185 loc) · 9.16 KB
/
savefold-utils.el
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
;;; savefold-utils.el --- shared utilities for savefold -*- lexical-binding: t; -*-
;; Copyright (C) 2025 Jacob Fong
;; Permission is hereby granted, free of charge, to any person obtaining a copy
;; of this software and associated documentation files (the “Software”), to deal
;; in the Software without restriction, including without limitation the rights
;; to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
;; copies of the Software, and to permit persons to whom the Software is
;; furnished to do so, subject to the following conditions:
;; The above copyright notice and this permission notice shall be included in
;; all copies or substantial portions of the Software.
;; THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
;; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
;; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
;; AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
;; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
;; OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
;; SOFTWARE.
;;; Commentary:
;; Shared utilities for savefold
;;; Code:
(require 'savefold)
(declare-function readablep "subr.el" (object) t) ;; Missing in Emacs 28
(defvar savefold-utils--fpath-to-attr-table (make-hash-table :test #'equal)
"Hash table mapping file paths to file attribute hash tables.")
(defun savefold-utils--get-attr-table-fpath (fpath)
"Return the fpath of the attribute table file for FPATH.
This naively replaces path slashes with ! (/a/b/c -> !a!b!c) leading to a chance
of collision."
(let* ((fpath (expand-file-name fpath))
(fpath (string-replace "/" "!" fpath))
(fpath (string-replace ":" "!" fpath))) ; For windows
(expand-file-name fpath savefold-directory)))
(defun savefold-utils--get-file-attr-table (fpath)
"Get the attribute hash table for file FPATH.
If no attr table file exists, return a new hash table."
(let ((attr-table (gethash fpath savefold-utils--fpath-to-attr-table)))
(unless attr-table
(let ((attr-table-fpath (savefold-utils--get-attr-table-fpath fpath)))
(if (file-exists-p attr-table-fpath)
(setq attr-table
(with-temp-buffer
(insert-file-contents attr-table-fpath)
(goto-char (point-min))
(read (current-buffer))))
(setq attr-table (make-hash-table))))
(puthash fpath attr-table savefold-utils--fpath-to-attr-table))
attr-table))
(defun savefold-utils--get-file-attr (attr &optional fpath)
"Return attribute ATTR for the current file.
Use FPATH instead if non-nil."
(when-let* ((fpath
(or fpath (and (buffer-file-name) (expand-file-name (buffer-file-name))))))
(gethash attr (savefold-utils--get-file-attr-table fpath))))
(defun savefold-utils--set-file-attr (attr value &optional fpath)
"Set attribute ATTR for VALUE in the current file's hash table.
Make sure to `savefold-utils--write-out-file-attrs' after each batch of changes
to save them to the disk.
Use FPATH instead of the current buffer file if non-nil."
(when-let* ((fpath
(or fpath (and (buffer-file-name) (expand-file-name (buffer-file-name))))))
;; Use compat for 28.2?
(if (or (version< emacs-version "29.1")
(readablep value))
(puthash attr value (savefold-utils--get-file-attr-table fpath))
(error "savefold: File attr value must be readablep"))))
(defun savefold-utils--write-out-file-attrs (&optional fpath)
"Write attr hash table for the current file to the disk.
Use FPATH instead if non-nil."
(when-let* ((fpath
(or fpath (and (buffer-file-name) (expand-file-name (buffer-file-name))))))
(when (not (file-exists-p savefold-directory))
(make-directory savefold-directory))
(with-temp-file (savefold-utils--get-attr-table-fpath fpath)
(prin1
(gethash fpath savefold-utils--fpath-to-attr-table)
(current-buffer)))))
(defun savefold-utils--set-file-attr-modtime ()
"Set the current file's modtime as a file attribute.
Must `savefold-utils--write-out-file-attrs' afterwards."
(savefold-utils--set-file-attr 'savefold-modtime (visited-file-modtime)))
(defun savefold-utils--file-recently-modifiedp ()
"The current file has modtime recenter than the \\='savefold-modtime attr.
False if the current file doesn't have a \\='savefold-modtime attr."
(when-let* ((saved-modtime (savefold-utils--get-file-attr 'savefold-modtime)))
(< (float-time saved-modtime) (float-time (visited-file-modtime)))))
(defun savefold-utils--mapc-buffers (fun pred)
"For all buffers, inside `with-current-buffer', if PRED, do FUN."
(mapc
(lambda (buf)
(with-current-buffer buf
(when (funcall pred)
(funcall fun))))
(buffer-list)))
(defun savefold-utils--get-overlays (pred)
"Return all overlays in the current buffer for which PRED is non-nil."
;; overlays-in does not necessarily return overlays in order
(seq-filter pred (overlays-in (point-min) (point-max))))
(defmacro savefold-utils--unless-file-recently-modified (&rest body)
"Evaluate BODY unless current file recently modified, per file attrs."
`(if (not (savefold-utils--file-recently-modifiedp))
,@body
(message
"savefold: Buffer contents newer than fold data for buffer '%s'. Not applying."
(current-buffer))))
(defmacro savefold-utils--with-standard-functions (backend modes &rest body)
"Eval BODY with generic standard func symbols bound for BACKEND and MODES.
BACKEND should be the quoted backend symbol and MODES should be a quoted list of
the modes to which this backend should be hooked."
(declare (indent 2))
`(let ((mode-hooks
(mapcar
(lambda (mode)
(intern
(format "%s-hook" mode)))
,modes))
(set-up-save-on-kill-buffer
(intern
(format "savefold-%s--set-up-save-on-kill-buffer" ,backend)))
(unhook-save-on-kill-buffer
(intern
(format "savefold-%s--unhook-save-on-kill-buffer" ,backend)))
(save-all-buffers-folds
(intern
(format "savefold-%s--save-all-buffers-folds" ,backend))))
,@body))
(defmacro savefold-utils--set-up-standard-hooks (backend
modes
recover-folds
save-folds
backend-bufferp)
"Set up the standard hooks for BACKEND, given MODES and essential functions.
Essential functions are: RECOVER-FOLDS, which recovers folds for the current
buffer. SAVE-FOLDS, which saves fold data to disk for the current buffer.
BACKEND-BUFFERP, which checks whether the backend should be active in the
current buffer."
(savefold-utils--with-standard-functions (eval backend) (eval modes)
`(progn
(defun ,set-up-save-on-kill-buffer ()
"Helper func from `savefold-utils--set-up-standard-hooks'."
(add-hook 'kill-buffer-hook ,save-folds nil t))
(defun ,unhook-save-on-kill-buffer ()
"Helper func from `savefold-utils--set-up-standard-hooks'."
(remove-hook 'kill-buffer-hook ,save-folds t))
(defun ,save-all-buffers-folds ()
"Helper func from `savefold-utils--set-up-standard-hooks'."
(savefold-utils--mapc-buffers ,save-folds ,backend-bufferp))
(declare-function
,set-up-save-on-kill-buffer (format "savefold-%s" ,backend))
(declare-function
,unhook-save-on-kill-buffer (format "savefold-%s" ,backend))
(declare-function
,save-all-buffers-folds (format "savefold-%s" ,backend))
(mapc
(lambda (mode-hook)
;; Recover folds on file open
(add-hook mode-hook ,recover-folds)
;; Save folds on file close
(add-hook mode-hook #',set-up-save-on-kill-buffer))
',mode-hooks)
;; Save folds on emacs shutdown
(add-hook 'kill-emacs-hook #',save-all-buffers-folds)
;; Set up save folds on file close for existing buffers
(savefold-utils--mapc-buffers
#',set-up-save-on-kill-buffer ,backend-bufferp))))
(defmacro savefold-utils--unhook-standard-hooks (backend
modes
recover-folds
backend-bufferp)
"Undo the standard hooks for BACKEND."
(savefold-utils--with-standard-functions (eval backend) (eval modes)
`(progn
(mapc
(lambda (mode-hook)
(remove-hook mode-hook ,recover-folds)
(remove-hook mode-hook #',set-up-save-on-kill-buffer))
',mode-hooks)
(remove-hook 'kill-emacs-hook #',save-all-buffers-folds)
(savefold-utils--mapc-buffers
#',unhook-save-on-kill-buffer ,backend-bufferp))))
(defmacro savefold-utils--require (feature)
"Require FEATURE, giving error if it's not present."
`(unless (require ,feature nil t)
(error "savefold: Feature '%s' required, not present" ,feature)))
(provide 'savefold-utils)
;;; savefold-utils.el ends here