Class: VER::Undo::Record

Inherits:
Struct
  • Object
show all
Defined in:
lib/ver/undo.rb

Overview

Every Record is responsible for one change that it can apply or undo. There is only a very limited set of methods for modifications, as some of the destructive String methods in Ruby can have unpredictable results. In order to undo a change, we have to predict what the change will do before it happens to avoid expensive diff algorithms.

A Record has one parent and a number of childs. If there are any childs, one of them is called current, and is the child that was last active, this way we can provide an intuitive way of choosing a child record to apply when a user wants to redo an undone change.

Record has a direct pointer to the data in the tree, since it has to know about nothing else.

Apart from that, Record also knows the time when it was created, this way you can move forward and backward in time.

Revisions only keep the data necessary to undo/redo a change, not the whole data that was modified, that way it can keep overall memory-usage to a minimum.

The applied property indicates whether or not this change has been applied already.

Constant Summary

Constants inherited from Struct

Struct::CACHE

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods inherited from Struct

new

Constructor Details

#initialize(tree, widget, parent = nil) ⇒ Record

Returns a new instance of Record.



163
164
165
166
167
168
169
# File 'lib/ver/undo.rb', line 163

def initialize(tree, widget, parent = nil)
  self.tree, self.widget, self.parent = tree, widget, parent
  self.ctime = Time.now
  self.childs = []
  self.applied = false
  self.separator = false
end

Instance Attribute Details

#appliedObject

Returns the value of attribute applied

Returns:

  • (Object)

    the current value of applied



160
161
162
# File 'lib/ver/undo.rb', line 160

def applied
  @applied
end

#childsObject

Returns the value of attribute childs

Returns:

  • (Object)

    the current value of childs



160
161
162
# File 'lib/ver/undo.rb', line 160

def childs
  @childs
end

#ctimeObject

Returns the value of attribute ctime

Returns:

  • (Object)

    the current value of ctime



160
161
162
# File 'lib/ver/undo.rb', line 160

def ctime
  @ctime
end

#parentObject

Returns the value of attribute parent

Returns:

  • (Object)

    the current value of parent



160
161
162
# File 'lib/ver/undo.rb', line 160

def parent
  @parent
end

#redo_infoObject

Returns the value of attribute redo_info

Returns:

  • (Object)

    the current value of redo_info



160
161
162
# File 'lib/ver/undo.rb', line 160

def redo_info
  @redo_info
end

#separatorObject

Returns the value of attribute separator

Returns:

  • (Object)

    the current value of separator



160
161
162
# File 'lib/ver/undo.rb', line 160

def separator
  @separator
end

#treeObject

Returns the value of attribute tree

Returns:

  • (Object)

    the current value of tree



160
161
162
# File 'lib/ver/undo.rb', line 160

def tree
  @tree
end

#undo_infoObject

Returns the value of attribute undo_info

Returns:

  • (Object)

    the current value of undo_info



160
161
162
# File 'lib/ver/undo.rb', line 160

def undo_info
  @undo_info
end

#widgetObject

Returns the value of attribute widget

Returns:

  • (Object)

    the current value of widget



160
161
162
# File 'lib/ver/undo.rb', line 160

def widget
  @widget
end

Instance Method Details

#applied?Boolean

Returns:

  • (Boolean)


313
314
315
# File 'lib/ver/undo.rb', line 313

def applied?
  applied
end

#compact!Object



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
# File 'lib/ver/undo.rb', line 238

def compact!
  return if separator
  return unless parent = self.parent

  pundo_from, pundo_to, pundo_string = parent.undo_info
  sundo_from, sundo_to, sundo_string = undo_info

  predo_name, *predo_args = parent.redo_info
  sredo_name, *sredo_args = redo_info

  # only compact identical methods
  return unless predo_name == sredo_name

  case predo_name
  when :insert
    predo_pos, predo_string = predo_args
    sredo_pos, sredo_string = sredo_args

    # the records have to be consecutive so they can still be applied by a
    # single undo/redo
    consecutive = index("#{predo_pos} + #{predo_string.size} chars") == sredo_pos
    return parent.compact! unless consecutive

    redo_string = "#{predo_string}#{sredo_string}"
    self.redo_info = [:insert, predo_pos, redo_string]

    undo_string = "#{pundo_string}#{sundo_string}"
    self.undo_info = [pundo_from, sundo_to, undo_string]
  when :replace
    predo_from, predo_to, predo_string = predo_args
    sredo_from, sredo_to, sredo_string = sredo_args

    # the records have to be consecutive so they can still be applied by a
    # single undo/redo
    consecutive = predo_to == sredo_from
    return parent.compact! unless consecutive

    redo_string = "#{predo_string}#{sredo_string}"
    self.redo_info = [:replace, predo_from, sredo_to, undo_string]

    undo_string = "#{pundo_string}#{sundo_string}"
    self.undo_info = [pundo_from, sundo_to, undo_string]
  when :delete
    predo_from, predo_to = predo_args
    sredo_from, sredo_to = sredo_args

    consecutive = predo_to == sredo_from
    return parent.compact! unless consecutive

    self.redo_info = [:delete, predo_from, sredo_to]

    undo_string = "#{sundo_string}#{pundo_string}"
    self.undo_info = [pundo_from, sundo_to, undo_string]
  else
    return
  end

  # the parent of our parent (grandparent) becomes our parent
  self.parent = grandparent = parent.parent

  # recurse into a new compact cycle if we have a grandparent
  if grandparent
    grandparent.next = self
    compact!
  end
end

#delete(*indices) ⇒ Object



196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
# File 'lib/ver/undo.rb', line 196

def delete(*indices)
  case indices.size
  when 0
    return
  when 1 # pad to two
    index = index(*indices)
    delete(index, index + '1 chars')
  when 2
    first, last = indices(*indices)

    data = widget.get(first, last)
    widget.execute_only(:delete, first, last)
    widget.touch!(first, last)

    self.redo_info = [:delete, first, last]
    self.undo_info = [first, first, data]
    self.applied = true
    first
  else # sanitize and chunk into deletes
    sanitize(*indices).map{|first, last|
      tree.record{|rec| rec.delete(first, last) }
    }.last
  end
end

#index(given_index) ⇒ Object



323
324
325
326
327
328
329
# File 'lib/ver/undo.rb', line 323

def index(given_index)
  if given_index.respond_to?(:to_index)
    given_index.to_index
  else
    widget.index(given_index)
  end
end

#indices(*given_indices) ⇒ Object



317
318
319
320
321
# File 'lib/ver/undo.rb', line 317

def indices(*given_indices)
  given_indices.map{|index|
    index.respond_to?(:to_index) ? index.to_index : widget.index(index)
  }.sort
end

#insert(pos, string, tag = Tk::None) ⇒ Object



171
172
173
174
175
176
177
178
179
180
181
# File 'lib/ver/undo.rb', line 171

def insert(pos, string, tag = Tk::None)
  pos = index(pos)

  widget.execute_only(:insert, pos, string, tag)
  widget.touch!(pos, "#{pos} + #{string.size} chars")

  self.redo_info = [:insert, pos, string, tag]
  self.undo_info = [pos, index("#{pos} + #{string.size} chars"), '']
  self.applied = true
  pos
end

#inspectObject



379
380
381
# File 'lib/ver/undo.rb', line 379

def inspect
  "#<Undo::Record sep=%p undo=%p redo=%p>" % [separator, undo_info, redo_info]
end

#nextObject



309
310
311
# File 'lib/ver/undo.rb', line 309

def next
  childs.first
end

#next=(child) ⇒ Object



305
306
307
# File 'lib/ver/undo.rb', line 305

def next=(child)
  childs.unshift(childs.delete(child) || child)
end

#redoObject



232
233
234
235
236
# File 'lib/ver/undo.rb', line 232

def redo
  return unless redo_info && !applied
  pos = send(*redo_info)
  widget.mark_set(:insert, pos)
end

#replace(from, to, string, tag = Tk::None) ⇒ Object



183
184
185
186
187
188
189
190
191
192
193
194
# File 'lib/ver/undo.rb', line 183

def replace(from, to, string, tag = Tk::None)
  from, to = indices(from, to)

  data = widget.get(from, to)
  widget.execute_only(:replace, from, to, string, tag)
  widget.touch!(from, to)

  self.redo_info = [:replace, from, to, string, tag]
  self.undo_info = [from, index("#{from} + #{string.size} chars"), data]
  self.applied = true
  from
end

#sanitize(*indices) ⇒ Object

Multi-index pair case requires that we prevalidate the indices and sort from last to first so that deletes occur in the exact (unshifted) text. It also needs to handle partial and fully overlapping ranges. We have to do this with multiple passes.

Raises:

  • (ArgumentError)


335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
# File 'lib/ver/undo.rb', line 335

def sanitize(*indices)
  raise ArgumentError if indices.size % 2 != 0

  # first we get the real indices
  indices = indices.map{|index| widget.index(index) }

  # pair them, to make later code easier.
  indices = indices.each_slice(2).to_a

  # then we sort the indices in increasing order
  indices = indices.sort

  # Now we eliminate ranges where end is before start.
  indices = indices.select{|st, en| st <= en }

  # And finally we merge ranges where the end is after the start of a
  # following range.
  final = []

  while rang = indices.shift
    if prev = final.last
      prev_start, prev_end = prev.at(0), prev.at(1)
      rang_start, rang_end = rang.at(0), rang.at(1)
    else
      final << rang
      next
    end

    if prev_start == rang_start
      # starts are overlapping, use longer end
      prev[1] = [prev_end, rang_end].max
    elsif prev_end >= rang_start
      # prev end is overlapping rang start, use longer end
      prev[1] = [prev_end, rang_end].max
    elsif prev_end >= rang_end
      # prev end is overlapping rang end, skip
    else
      final << rang
    end
  end

  final.reverse
end

#undoObject



221
222
223
224
225
226
227
228
229
230
# File 'lib/ver/undo.rb', line 221

def undo
  return unless undo_info && applied

  from, to, string = undo_info
  widget.execute_only(:replace, from, to, string)
  widget.touch!(from, to)
  widget.mark_set(:insert, from)

  self.applied = false
end