Class: VER::Undo::Record
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
Instance Attribute Summary collapse
-
#applied ⇒ Object
Returns the value of attribute applied.
-
#childs ⇒ Object
Returns the value of attribute childs.
-
#ctime ⇒ Object
Returns the value of attribute ctime.
-
#parent ⇒ Object
Returns the value of attribute parent.
-
#redo_info ⇒ Object
Returns the value of attribute redo_info.
-
#separator ⇒ Object
Returns the value of attribute separator.
-
#tree ⇒ Object
Returns the value of attribute tree.
-
#undo_info ⇒ Object
Returns the value of attribute undo_info.
-
#widget ⇒ Object
Returns the value of attribute widget.
Instance Method Summary collapse
- #applied? ⇒ Boolean
- #compact! ⇒ Object
- #delete(*indices) ⇒ Object
- #index(given_index) ⇒ Object
- #indices(*given_indices) ⇒ Object
-
#initialize(tree, widget, parent = nil) ⇒ Record
constructor
A new instance of Record.
- #insert(pos, string, tag = Tk::None) ⇒ Object
- #inspect ⇒ Object
- #next ⇒ Object
- #next=(child) ⇒ Object
- #redo ⇒ Object
- #replace(from, to, string, tag = Tk::None) ⇒ Object
-
#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.
- #undo ⇒ Object
Methods inherited from Struct
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, , parent = nil) self.tree, self., self.parent = tree, , parent self.ctime = Time.now self.childs = [] self.applied = false self.separator = false end |
Instance Attribute Details
#applied ⇒ Object
Returns the value of attribute applied
160 161 162 |
# File 'lib/ver/undo.rb', line 160 def applied @applied end |
#childs ⇒ Object
Returns the value of attribute childs
160 161 162 |
# File 'lib/ver/undo.rb', line 160 def childs @childs end |
#ctime ⇒ Object
Returns the value of attribute ctime
160 161 162 |
# File 'lib/ver/undo.rb', line 160 def ctime @ctime end |
#parent ⇒ Object
Returns the value of attribute parent
160 161 162 |
# File 'lib/ver/undo.rb', line 160 def parent @parent end |
#redo_info ⇒ Object
Returns the value of attribute redo_info
160 161 162 |
# File 'lib/ver/undo.rb', line 160 def redo_info @redo_info end |
#separator ⇒ Object
Returns the value of attribute separator
160 161 162 |
# File 'lib/ver/undo.rb', line 160 def separator @separator end |
#tree ⇒ Object
Returns the value of attribute tree
160 161 162 |
# File 'lib/ver/undo.rb', line 160 def tree @tree end |
#undo_info ⇒ Object
Returns the value of attribute undo_info
160 161 162 |
# File 'lib/ver/undo.rb', line 160 def undo_info @undo_info end |
#widget ⇒ Object
Returns the value of attribute widget
160 161 162 |
# File 'lib/ver/undo.rb', line 160 def @widget end |
Instance Method Details
#applied? ⇒ 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 = .get(first, last) .execute_only(:delete, first, last) .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 .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 : .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) .execute_only(:insert, pos, string, tag) .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 |
#inspect ⇒ Object
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 |
#next ⇒ Object
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 |
#redo ⇒ Object
232 233 234 235 236 |
# File 'lib/ver/undo.rb', line 232 def redo return unless redo_info && !applied pos = send(*redo_info) .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 = .get(from, to) .execute_only(:replace, from, to, string, tag) .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.
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| .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 |
#undo ⇒ Object
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 .execute_only(:replace, from, to, string) .touch!(from, to) .mark_set(:insert, from) self.applied = false end |