/
rudel-operators.el
326 lines (278 loc) · 10 KB
/
rudel-operators.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
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
;;; rudel-operators.el --- Sets of modification operators for Rudel objects
;;
;; Copyright (C) 2009, 2010 Jan Moringen
;;
;; Author: Jan Moringen <scymtym@users.sourceforge.net>
;; Keywords: Rudel, operators
;; X-RCS: $Id:$
;;
;; This file is part of Rudel.
;;
;; Rudel 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.
;;
;; Rudel 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 Rudel. If not, see <http://www.gnu.org/licenses>.
;;; Commentary:
;;
;; Collections of operations on specific objects are collected into
;; classes. Currently there are:
;;
;; + `rudel-document-operators': perform operations on document objects
;;
;; + `rudel-connection-operators': perform operations on connection
;; objects
;;
;; + `rudel-overlay-operators': perform operations by altering
;; overlays of buffer objects
;;
;; + `rudel-hook-operators': perform operations by calling hooks
;;
;; Related classes:
;;
;; + `rudel-operation-merger': merge series of operations into more
;; compact series of operations without changing the effect of
;; applying the operations
;;; History:
;;
;; 0.2 - Persistent operator objects
;; - Operation merger
;;
;; 0.1 - Initial version
;;; Code:
;;
(require 'eieio)
(require 'rudel-overlay)
;;; Class rudel-document-operators
;;
(defclass rudel-document-operators ()
((document :initarg :document
:type rudel-document-child
:documentation
"Document to which modification operators are
applied."))
"Provides operation methods which modify an associated document.")
(defmethod rudel-insert ((this rudel-document-operators) position data)
"Insert DATA at POSITION into the document attached to THIS."
(with-slots (document) this
(rudel-insert document position data)))
(defmethod rudel-delete ((this rudel-document-operators) position length)
"Delete a region of LENGTH characters at POSITION from the document attached to THIS."
(with-slots (document) this
(rudel-delete document position length)))
(defmethod rudel-handle ((this rudel-document-operators) operation
&optional context)
"Handle OPERATION, possibly using information from CONTEXT.
Operation is an instance of a subclass of `rudel-operation'.
CONTEXT is a property list."
(rudel-apply operation this))
;;; Class rudel-connection-operators
;;
(defclass rudel-connection-operators ()
((connection :initarg :connection
:type rudel-connection-child
:documentation
"Connection on which the operations are
performed.")
(document :initarg :document
:type rudel-document-child
:documentation
"Document object to which operations refer."))
"Provides operation methods which affect an associated
connection.")
(defmethod rudel-insert ((this rudel-connection-operators) position data)
"Notify the connection associated to THIS of the insertion of DATA at POSITION."
(with-slots (connection document) this
(rudel-local-insert connection document position data)))
(defmethod rudel-delete ((this rudel-connection-operators) position length)
"Notify the connection associated to THIS of a deletion of LENGTH at POSITION."
(with-slots (connection document) this
(rudel-local-delete connection document position length)))
(defmethod rudel-handle ((this rudel-connection-operators) operation
&optional context)
"Handle OPERATION, possibly using information from CONTEXT.
Operation is an instance of a subclass of `rudel-operation'.
CONTEXT is a property list."
(rudel-apply operation this))
;;; Class rudel-overlay-operators
;;
(defclass rudel-overlay-operators ()
((document :initarg :document
:type rudel-document-child
:documentation
"The document to the overlays of which the
operations are applied")
(user :initarg :user
:type (or null rudel-user-child)
:documentation
"The user object associated to operations."))
"Provides operation methods which affect the overlays of a
buffer.")
(defmethod rudel-insert ((this rudel-overlay-operators) position data)
"Update the overlays associated to THIS to incorporate an insertion of DATA at POSITION."
(with-slots (document user) this
(with-slots (buffer) document
;; Since we inserted something, (point-max) is at least the
;; length of the insertion + 1. So we can safely subtract the
;; length of the insertion and 1.
(unless position
(with-current-buffer buffer
(setq position (- (point-max) (length data) 1))))
(rudel-update-author-overlay-after-insert
buffer (+ position 1) (length data) user)))
)
(defmethod rudel-delete ((this rudel-overlay-operators) position length)
"Update the overlays associated to THIS to incorporate a deletion of LENGTH at POSITION."
(with-slots (document user) this
(with-slots (buffer) document
(rudel-update-author-overlay-after-delete
buffer (+ position 1) length user))))
(defmethod rudel-handle ((this rudel-overlay-operators) operation
&optional context)
"Handle OPERATION, possibly using information from CONTEXT.
Operation is an instance of a subclass of `rudel-operation'.
CONTEXT is a property list."
;; TODO temporarily setting the user like this is not optimal
(oset this :user (plist-get context :user))
(rudel-apply operation this))
;;; Class rudel-hook-operators
;;
(defclass rudel-hook-operators ()
((document :initarg :document
:type rudel-document-child
:documentation
"The document object to which operations refer.")
(user :initarg :user
:type rudel-user-child
:documentation
"The user object associated to operations."))
"Provides operation methods which cause corresponding hooks to
be called.")
(defmethod rudel-insert ((this rudel-hook-operators) position data)
"Call insert hook associated to THIS with POSITION and DATA."
(with-slots (document user) this
(with-slots (buffer) document
(run-hook-with-args 'rudel-insert-hook buffer user position data))))
(defmethod rudel-delete ((this rudel-hook-operators) position length)
"Call delete hook associated to THIS with POSITION and LENGTH."
(with-slots (document user) this
(with-slots (buffer) document
(run-hook-with-args 'rudel-delete-hook buffer user position length))))
(defmethod rudel-handle ((this rudel-hook-operators) operation
&optional context)
"Handle OPERATION, possibly using information from CONTEXT.
Operation is an instance of a subclass of `rudel-operation'.
CONTEXT is a property list."
;; TODO temporarily setting the user like this is not optimal
(oset this :user (plist-get context :user))
(rudel-apply operation this))
;;; Class rudel-operation-merger
;;
(defclass rudel-operation-merger ()
((buffer :initarg :buffer
:type list
:initform nil
:documentation
"This buffer stores operations for merging when they
are received.")
(timer :initarg :timer
:type (or null timer)
:initform nil
:documentation
"This timer triggers the sending of queued, merged
messages.")
(window :initarg :window
:type (number 0)
:initform 0.5
:documentation
"")
(target :initarg :target
:type object
:reader rudel-target
:writer rudel-set-target
:documentation
"This slot holds an object to which processed
operations are passed."))
"Objects of this class handle operations by merging adjacent
operations if possible.")
(defmethod rudel-handle ((this rudel-operation-merger) operation
&optional context)
"Process OPERATION, merging it with stored operations, eventually sending it.
Operations are considered for merging within a time-window, then
sent, whether merged or not."
(with-slots (buffer timer window) this
;; Add OPERATION to buffer and try merging
(push operation buffer)
(let ((ops))
(while (and (> (length buffer) 1)
(< (length ops) 2))
(let ((second (pop buffer))
(first (pop buffer)))
(setq ops (rudel-merge first second))) ;; TODO correct?
(dolist (op ops)
(push op buffer))))
;; If there are operations in the buffer, potentially start the
;; timer.
(if buffer
;; Start timer if necessary
(unless timer
(setq timer (run-at-time
window nil ;; no repeat
'rudel-flush this)))
;; Cancel timer if necessary
(when timer
(cancel-timer timer)
(setq timer nil))))
)
(defmethod rudel-flush ((this rudel-operation-merger))
"Pass remaining queued operations to the target object."
(with-slots (buffer timer target) this
(dolist (operation (nreverse buffer))
(rudel-handle target operation))
(when timer
(cancel-timer timer))
(setq buffer nil
timer nil)))
;;; Merge methods for operation classes
;;
(defmethod rudel-merge ((first rudel-insert-op) second)
"Merge FIRST and SECOND, if possible."
;; If wish, I had multiple dispatch
(cond
;; Merge adjacent inserts.
((and (rudel-insert-op-child-p second)
(= (oref first :to)
(oref second :from)))
(list (clone first
"merged insert"
:from (oref first :from)
:data (concat (oref first :data)
(oref second :data)))))
;; Ignore other combinations.
(t
(list first second))))
(defmethod rudel-merge ((first rudel-delete-op) second)
"Merge FIRST and SECOND, if possible."
;; If wish, I had multiple dispatch
(cond
;; Merge adjacent deletes.
((and (rudel-delete-op-child-p second)
(= (oref first :from)
(oref second :from)))
(list (clone first
"merged delete"
:from (oref first :from)
:length (+ (oref first :length)
(oref second :length)))))
;; Ignore other combinations.
(t
(list first second))))
(provide 'rudel-operators)
;;; rudel-operators.el ends here