1 ## undo/redo
   2 
   3 # for every undoable event, create a type of *operation* that contains all the
   4 # information needed to reverse it
   5 exclusive-container operation [
   6   typing:insert-operation
   7   move:move-operation
   8   delete:delete-operation
   9 ]
  10 
  11 container insert-operation [
  12   before-row:num
  13   before-column:num
  14   before-top-of-screen:&:duplex-list:char
  15   after-row:num
  16   after-column:num
  17   after-top-of-screen:&:duplex-list:char
  18   # inserted text is from 'insert-from' until 'insert-until'; list doesn't have to terminate
  19   insert-from:&:duplex-list:char
  20   insert-until:&:duplex-list:char
  21   tag:num  # event causing this operation; might be used to coalesce runs of similar events
  22     # 0: no coalesce (enter+indent)
  23     # 1: regular alphanumeric characters
  24 ]
  25 
  26 container move-operation [
  27   before-row:num
  28   before-column:num
  29   before-top-of-screen:&:duplex-list:char
  30   after-row:num
  31   after-column:num
  32   after-top-of-screen:&:duplex-list:char
  33   tag:num  # event causing this operation; might be used to coalesce runs of similar events
  34     # 0: no coalesce (touch events, etc)
  35     # 1: left arrow
  36     # 2: right arrow
  37     # 3: up arrow
  38     # 4: down arrow
  39 ]
  40 
  41 container delete-operation [
  42   before-row:num
  43   before-column:num
  44   before-top-of-screen:&:duplex-list:char
  45   after-row:num
  46   after-column:num
  47   after-top-of-screen:&:duplex-list:char
  48   deleted-text:&:duplex-list:char
  49   delete-from:&:duplex-list:char
  50   delete-until:&:duplex-list:char
  51   tag:num  # event causing this operation; might be used to coalesce runs of similar events
  52     # 0: no coalesce (ctrl-k, ctrl-u)
  53     # 1: backspace
  54     # 2: delete
  55 ]
  56 
  57 # every editor accumulates a list of operations to undo/redo
  58 container editor [
  59   undo:&:list:&:operation
  60   redo:&:list:&:operation
  61 ]
  62 
  63 # ctrl-z - undo operation
  64 after <handle-special-character> [
  65   {
  66     undo?:bool <- equal c, 26/ctrl-z
  67     break-unless undo?
  68     undo:&:list:&:operation <- get *editor, undo:offset
  69     break-unless undo
  70     op:&:operation <- first undo
  71     undo <- rest undo
  72     *editor <- put *editor, undo:offset, undo
  73     redo:&:list:&:operation <- get *editor, redo:offset
  74     redo <- push op, redo
  75     *editor <- put *editor, redo:offset, redo
  76     <handle-undo>
  77     return 1/go-render
  78   }
  79 ]
  80 
  81 # ctrl-y - redo operation
  82 after <handle-special-character> [
  83   {
  84     redo?:bool <- equal c, 25/ctrl-y
  85     break-unless redo?
  86     redo:&:list:&:operation <- get *editor, redo:offset
  87     break-unless redo
  88     op:&:operation <- first redo
  89     redo <- rest redo
  90     *editor <- put *editor, redo:offset, redo
  91     undo:&:list:&:operation <- get *editor, undo:offset
  92     undo <- push op, undo
  93     *editor <- put *editor, undo:offset, undo
  94     <handle-redo>
  95     return 1/go-render
  96   }
  97 ]
  98 
  99 # undo typing
 100 
 101 scenario editor-can-undo-typing [
 102   local-scope
 103   # create an editor and type a character
 104   assume-screen 10/width, 5/height
 105   e:&:editor <- new-editor [], 0/left, 10/right
 106   editor-render screen, e
 107   assume-console [
 108     type [0]
 109   ]
 110   editor-event-loop screen, console, e
 111   # undo
 112   assume-console [
 113     press ctrl-z
 114   ]
 115   run [
 116     editor-event-loop screen, console, e
 117   ]
 118   # character should be gone
 119   screen-should-contain [
 120     .          .
 121     .          .
 122     .╌╌╌╌╌╌╌╌╌╌.
 123     .          .
 124   ]
 125   # cursor should be in the right place
 126   assume-console [
 127     type [1]
 128   ]
 129   run [
 130     editor-event-loop screen, console, e
 131   ]
 132   screen-should-contain [
 133     .          .
 134     .1         .
 135     .╌╌╌╌╌╌╌╌╌╌.
 136     .          .
 137   ]
 138 ]
 139 
 140 # save operation to undo
 141 after <insert-character-begin> [
 142   top-before:&:duplex-list:char <- get *editor, top-of-screen:offset
 143   cursor-before:&:duplex-list:char <- get *editor, before-cursor:offset
 144 ]
 145 before <insert-character-end> [
 146   top-after:&:duplex-list:char <- get *editor, top-of-screen:offset
 147   cursor-row:num <- get *editor, cursor-row:offset
 148   cursor-column:num <- get *editor, cursor-column:offset
 149   undo:&:list:&:operation <- get *editor, undo:offset
 150   {
 151     # if previous operation was an insert, coalesce this operation with it
 152     break-unless undo
 153     op:&:operation <- first undo
 154     typing:insert-operation, is-insert?:bool <- maybe-convert *op, typing:variant
 155     break-unless is-insert?
 156     previous-coalesce-tag:num <- get typing, tag:offset
 157     break-unless previous-coalesce-tag
 158     before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
 159     insert-until:&:duplex-list:char <- next before-cursor
 160     typing <- put typing, insert-until:offset, insert-until
 161     typing <- put typing, after-row:offset, cursor-row
 162     typing <- put typing, after-column:offset, cursor-column
 163     typing <- put typing, after-top-of-screen:offset, top-after
 164     *op <- merge 0/insert-operation, typing
 165     break +done-adding-insert-operation
 166   }
 167   # if not, create a new operation
 168   insert-from:&:duplex-list:char <- next cursor-before
 169   insert-to:&:duplex-list:char <- next insert-from
 170   op:&:operation <- new operation:type
 171   *op <- merge 0/insert-operation, save-row/before, save-column/before, top-before, cursor-row/after, cursor-column/after, top-after, insert-from, insert-to, 1/coalesce
 172   editor <- add-operation editor, op
 173   +done-adding-insert-operation
 174 ]
 175 
 176 # enter operations never coalesce with typing before or after
 177 after <insert-enter-begin> [
 178   cursor-row-before:num <- copy cursor-row
 179   cursor-column-before:num <- copy cursor-column
 180   top-before:&:duplex-list:char <- get *editor, top-of-screen:offset
 181   cursor-before:&:duplex-list:char <- get *editor, before-cursor:offset
 182 ]
 183 before <insert-enter-end> [
 184   top-after:&:duplex-list:char <- get *editor, top-of-screen:offset
 185   cursor-row:num <- get *editor, cursor-row:offset
 186   cursor-column:num <- get *editor, cursor-row:offset
 187   # never coalesce
 188   insert-from:&:duplex-list:char <- next cursor-before
 189   before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
 190   insert-to:&:duplex-list:char <- next before-cursor
 191   op:&:operation <- new operation:type
 192   *op <- merge 0/insert-operation, cursor-row-before, cursor-column-before, top-before, cursor-row/after, cursor-column/after, top-after, insert-from, insert-to, 0/never-coalesce
 193   editor <- add-operation editor, op
 194 ]
 195 
 196 # Everytime you add a new operation to the undo stack, be sure to clear the
 197 # redo stack, because it's now obsolete.
 198 # Beware: since we're counting cursor moves as operations, this means just
 199 # moving the cursor can lose work on the undo stack.
 200 def add-operation editor:&:editor, op:&:operation -> editor:&:editor [
 201   local-scope
 202   load-ingredients
 203   undo:&:list:&:operation <- get *editor, undo:offset
 204   undo <- push op undo
 205   *editor <- put *editor, undo:offset, undo
 206   redo:&:list:&:operation <- get *editor, redo:offset
 207   redo <- copy 0
 208   *editor <- put *editor, redo:offset, redo
 209 ]
 210 
 211 after <handle-undo> [
 212   {
 213     typing:insert-operation, is-insert?:bool <- maybe-convert *op, typing:variant
 214     break-unless is-insert?
 215     start:&:duplex-list:char <- get typing, insert-from:offset
 216     end:&:duplex-list:char <- get typing, insert-until:offset
 217     # assert cursor-row/cursor-column/top-of-screen match after-row/after-column/after-top-of-screen
 218     before-cursor:&:duplex-list:char <- prev start
 219     *editor <- put *editor, before-cursor:offset, before-cursor
 220     remove-between before-cursor, end
 221     cursor-row <- get typing, before-row:offset
 222     *editor <- put *editor, cursor-row:offset, cursor-row
 223     cursor-column <- get typing, before-column:offset
 224     *editor <- put *editor, cursor-column:offset, cursor-column
 225     top:&:duplex-list:char <- get typing, before-top-of-screen:offset
 226     *editor <- put *editor, top-of-screen:offset, top
 227   }
 228 ]
 229 
 230 scenario editor-can-undo-typing-multiple [
 231   local-scope
 232   # create an editor and type multiple characters
 233   assume-screen 10/width, 5/height
 234   e:&:editor <- new-editor [], 0/left, 10/right
 235   editor-render screen, e
 236   assume-console [
 237     type [012]
 238   ]
 239   editor-event-loop screen, console, e
 240   # undo
 241   assume-console [
 242     press ctrl-z
 243   ]
 244   run [
 245     editor-event-loop screen, console, e
 246   ]
 247   # all characters must be gone
 248   screen-should-contain [
 249     .          .
 250     .          .
 251     .╌╌╌╌╌╌╌╌╌╌.
 252     .          .
 253   ]
 254 ]
 255 
 256 scenario editor-can-undo-typing-multiple-2 [
 257   local-scope
 258   # create an editor with some text
 259   assume-screen 10/width, 5/height
 260   e:&:editor <- new-editor [a], 0/left, 10/right
 261   editor-render screen, e
 262   # type some characters
 263   assume-console [
 264     type [012]
 265   ]
 266   editor-event-loop screen, console, e
 267   screen-should-contain [
 268     .          .
 269     .012a      .
 270     .╌╌╌╌╌╌╌╌╌╌.
 271     .          .
 272   ]
 273   # undo
 274   assume-console [
 275     press ctrl-z
 276   ]
 277   run [
 278     editor-event-loop screen, console, e
 279   ]
 280   # back to original text
 281   screen-should-contain [
 282     .          .
 283     .a         .
 284     .╌╌╌╌╌╌╌╌╌╌.
 285     .          .
 286   ]
 287   # cursor should be in the right place
 288   assume-console [
 289     type [3]
 290   ]
 291   run [
 292     editor-event-loop screen, console, e
 293   ]
 294   screen-should-contain [
 295     .          .
 296     .3a        .
 297     .╌╌╌╌╌╌╌╌╌╌.
 298     .          .
 299   ]
 300 ]
 301 
 302 scenario editor-can-undo-typing-enter [
 303   local-scope
 304   # create an editor with some text
 305   assume-screen 10/width, 5/height
 306   e:&:editor <- new-editor [  abc], 0/left, 10/right
 307   editor-render screen, e
 308   # new line
 309   assume-console [
 310     left-click 1, 8
 311     press enter
 312   ]
 313   editor-event-loop screen, console, e
 314   screen-should-contain [
 315     .          .
 316     .  abc     .
 317     .          .
 318     .╌╌╌╌╌╌╌╌╌╌.
 319     .          .
 320   ]
 321   # line is indented
 322   3:num/raw <- get *e, cursor-row:offset
 323   4:num/raw <- get *e, cursor-column:offset
 324   memory-should-contain [
 325     3 <- 2
 326     4 <- 2
 327   ]
 328   # undo
 329   assume-console [
 330     press ctrl-z
 331   ]
 332   run [
 333     editor-event-loop screen, console, e
 334   ]
 335   3:num/raw <- get *e, cursor-row:offset
 336   4:num/raw <- get *e, cursor-column:offset
 337   memory-should-contain [
 338     3 <- 1
 339     4 <- 5
 340   ]
 341   # back to original text
 342   screen-should-contain [
 343     .          .
 344     .  abc     .
 345     .╌╌╌╌╌╌╌╌╌╌.
 346     .          .
 347   ]
 348   # cursor should be at end of line
 349   assume-console [
 350     type [1]
 351   ]
 352   run [
 353     editor-event-loop screen, console, e
 354   ]
 355   screen-should-contain [
 356     .          .
 357     .  abc1    .
 358     .╌╌╌╌╌╌╌╌╌╌.
 359     .          .
 360   ]
 361 ]
 362 
 363 # redo typing
 364 
 365 scenario editor-redo-typing [
 366   local-scope
 367   # create an editor, type something, undo
 368   assume-screen 10/width, 5/height
 369   e:&:editor <- new-editor [a], 0/left, 10/right
 370   editor-render screen, e
 371   assume-console [
 372     type [012]
 373     press ctrl-z
 374   ]
 375   editor-event-loop screen, console, e
 376   screen-should-contain [
 377     .          .
 378     .a         .
 379     .╌╌╌╌╌╌╌╌╌╌.
 380     .          .
 381   ]
 382   # redo
 383   assume-console [
 384     press ctrl-y
 385   ]
 386   run [
 387     editor-event-loop screen, console, e
 388   ]
 389   # all characters must be back
 390   screen-should-contain [
 391     .          .
 392     .012a      .
 393     .╌╌╌╌╌╌╌╌╌╌.
 394     .          .
 395   ]
 396   # cursor should be in the right place
 397   assume-console [
 398     type [3]
 399   ]
 400   run [
 401     editor-event-loop screen, console, e
 402   ]
 403   screen-should-contain [
 404     .          .
 405     .0123a     .
 406     .╌╌╌╌╌╌╌╌╌╌.
 407     .          .
 408   ]
 409 ]
 410 
 411 after <handle-redo> [
 412   {
 413     typing:insert-operation, is-insert?:bool <- maybe-convert *op, typing:variant
 414     break-unless is-insert?
 415     before-cursor <- get *editor, before-cursor:offset
 416     insert-from:&:duplex-list:char <- get typing, insert-from:offset  # ignore insert-to because it's already been spliced away
 417     # assert insert-to matches next(before-cursor)
 418     insert-range before-cursor, insert-from
 419     # assert cursor-row/cursor-column/top-of-screen match after-row/after-column/after-top-of-screen
 420     cursor-row <- get typing, after-row:offset
 421     *editor <- put *editor, cursor-row:offset, cursor-row
 422     cursor-column <- get typing, after-column:offset
 423     *editor <- put *editor, cursor-column:offset, cursor-column
 424     top:&:duplex-list:char <- get typing, after-top-of-screen:offset
 425     *editor <- put *editor, top-of-screen:offset, top
 426   }
 427 ]
 428 
 429 scenario editor-redo-typing-empty [
 430   local-scope
 431   # create an editor, type something, undo
 432   assume-screen 10/width, 5/height
 433   e:&:editor <- new-editor [], 0/left, 10/right
 434   editor-render screen, e
 435   assume-console [
 436     type [012]
 437     press ctrl-z
 438   ]
 439   editor-event-loop screen, console, e
 440   screen-should-contain [
 441     .          .
 442     .          .
 443     .╌╌╌╌╌╌╌╌╌╌.
 444     .          .
 445   ]
 446   # redo
 447   assume-console [
 448     press ctrl-y
 449   ]
 450   run [
 451     editor-event-loop screen, console, e
 452   ]
 453   # all characters must be back
 454   screen-should-contain [
 455     .          .
 456     .012       .
 457     .╌╌╌╌╌╌╌╌╌╌.
 458     .          .
 459   ]
 460   # cursor should be in the right place
 461   assume-console [
 462     type [3]
 463   ]
 464   run [
 465     editor-event-loop screen, console, e
 466   ]
 467   screen-should-contain [
 468     .          .
 469     .0123      .
 470     .╌╌╌╌╌╌╌╌╌╌.
 471     .          .
 472   ]
 473 ]
 474 
 475 scenario editor-work-clears-redo-stack [
 476   local-scope
 477   # create an editor with some text, do some work, undo
 478   assume-screen 10/width, 5/height
 479   contents:text <- new [abc
 480 def
 481 ghi]
 482   e:&:editor <- new-editor contents, 0/left, 10/right
 483   editor-render screen, e
 484   assume-console [
 485     type [1]
 486     press ctrl-z
 487   ]
 488   editor-event-loop screen, console, e
 489   # do some more work
 490   assume-console [
 491     type [0]
 492   ]
 493   editor-event-loop screen, console, e
 494   screen-should-contain [
 495     .          .
 496     .0abc      .
 497     .def       .
 498     .ghi       .
 499     .╌╌╌╌╌╌╌╌╌╌.
 500   ]
 501   # redo
 502   assume-console [
 503     press ctrl-y
 504   ]
 505   run [
 506     editor-event-loop screen, console, e
 507   ]
 508   # nothing should happen
 509   screen-should-contain [
 510     .          .
 511     .0abc      .
 512     .def       .
 513     .ghi       .
 514     .╌╌╌╌╌╌╌╌╌╌.
 515   ]
 516 ]
 517 
 518 scenario editor-can-redo-typing-and-enter-and-tab [
 519   local-scope
 520   # create an editor
 521   assume-screen 10/width, 5/height
 522   e:&:editor <- new-editor [], 0/left, 10/right
 523   editor-render screen, e
 524   # insert some text and tabs, hit enter, some more text and tabs
 525   assume-console [
 526     press tab
 527     type [ab]
 528     press tab
 529     type [cd]
 530     press enter
 531     press tab
 532     type [efg]
 533   ]
 534   editor-event-loop screen, console, e
 535   screen-should-contain [
 536     .          .
 537     .  ab  cd  .
 538     .    efg   .
 539     .╌╌╌╌╌╌╌╌╌╌.
 540     .          .
 541   ]
 542   3:num/raw <- get *e, cursor-row:offset
 543   4:num/raw <- get *e, cursor-column:offset
 544   memory-should-contain [
 545     3 <- 2
 546     4 <- 7
 547   ]
 548   # undo
 549   assume-console [
 550     press ctrl-z
 551   ]
 552   run [
 553     editor-event-loop screen, console, e
 554   ]
 555   # typing in second line deleted, but not indent
 556   3:num/raw <- get *e, cursor-row:offset
 557   4:num/raw <- get *e, cursor-column:offset
 558   memory-should-contain [
 559     3 <- 2
 560     4 <- 2
 561   ]
 562   screen-should-contain [
 563     .          .
 564     .  ab  cd  .
 565     .          .
 566     .╌╌╌╌╌╌╌╌╌╌.
 567     .          .
 568   ]
 569   # undo again
 570   assume-console [
 571     press ctrl-z
 572   ]
 573   run [
 574     editor-event-loop screen, console, e
 575   ]
 576   # indent and newline deleted
 577   3:num/raw <- get *e, cursor-row:offset
 578   4:num/raw <- get *e, cursor-column:offset
 579   memory-should-contain [
 580     3 <- 1
 581     4 <- 8
 582   ]
 583   screen-should-contain [
 584     .          .
 585     .  ab  cd  .
 586     .╌╌╌╌╌╌╌╌╌╌.
 587     .          .
 588   ]
 589   # undo again
 590   assume-console [
 591     press ctrl-z
 592   ]
 593   run [
 594     editor-event-loop screen, console, e
 595   ]
 596   # empty screen
 597   3:num/raw <- get *e, cursor-row:offset
 598   4:num/raw <- get *e, cursor-column:offset
 599   memory-should-contain [
 600     3 <- 1
 601     4 <- 0
 602   ]
 603   screen-should-contain [
 604     .          .
 605     .          .
 606     .╌╌╌╌╌╌╌╌╌╌.
 607     .          .
 608   ]
 609   # redo
 610   assume-console [
 611     press ctrl-y
 612   ]
 613   run [
 614     editor-event-loop screen, console, e
 615   ]
 616   # first line inserted
 617   3:num/raw <- get *e, cursor-row:offset
 618   4:num/raw <- get *e, cursor-column:offset
 619   memory-should-contain [
 620     3 <- 1
 621     4 <- 8
 622   ]
 623   screen-should-contain [
 624     .          .
 625     .  ab  cd  .
 626     .╌╌╌╌╌╌╌╌╌╌.
 627     .          .
 628   ]
 629   # redo again
 630   assume-console [
 631     press ctrl-y
 632   ]
 633   run [
 634     editor-event-loop screen, console, e
 635   ]
 636   # newline and indent inserted
 637   3:num/raw <- get *e, cursor-row:offset
 638   4:num/raw <- get *e, cursor-column:offset
 639   memory-should-contain [
 640     3 <- 2
 641     4 <- 2
 642   ]
 643   screen-should-contain [
 644     .          .
 645     .  ab  cd  .
 646     .          .
 647     .╌╌╌╌╌╌╌╌╌╌.
 648     .          .
 649   ]
 650   # redo again
 651   assume-console [
 652     press ctrl-y
 653   ]
 654   run [
 655     editor-event-loop screen, console, e
 656   ]
 657   # indent and newline deleted
 658   3:num/raw <- get *e, cursor-row:offset
 659   4:num/raw <- get *e, cursor-column:offset
 660   memory-should-contain [
 661     3 <- 2
 662     4 <- 7
 663   ]
 664   screen-should-contain [
 665     .          .
 666     .  ab  cd  .
 667     .    efg   .
 668     .╌╌╌╌╌╌╌╌╌╌.
 669     .          .
 670   ]
 671 ]
 672 
 673 # undo cursor movement and scroll
 674 
 675 scenario editor-can-undo-touch [
 676   local-scope
 677   # create an editor with some text
 678   assume-screen 10/width, 5/height
 679   contents:text <- new [abc
 680 def
 681 ghi]
 682   e:&:editor <- new-editor contents, 0/left, 10/right
 683   editor-render screen, e
 684   # move the cursor
 685   assume-console [
 686     left-click 3, 1
 687   ]
 688   editor-event-loop screen, console, e
 689   # undo
 690   assume-console [
 691     press ctrl-z
 692   ]
 693   run [
 694     editor-event-loop screen, console, e
 695   ]
 696   # click undone
 697   3:num/raw <- get *e, cursor-row:offset
 698   4:num/raw <- get *e, cursor-column:offset
 699   memory-should-contain [
 700     3 <- 1
 701     4 <- 0
 702   ]
 703   # cursor should be in the right place
 704   assume-console [
 705     type [1]
 706   ]
 707   run [
 708     editor-event-loop screen, console, e
 709   ]
 710   screen-should-contain [
 711     .          .
 712     .1abc      .
 713     .def       .
 714     .ghi       .
 715     .╌╌╌╌╌╌╌╌╌╌.
 716   ]
 717 ]
 718 
 719 after <move-cursor-begin> [
 720   cursor-row-before:num <- get *editor, cursor-row:offset
 721   cursor-column-before:num <- get *editor, cursor-column:offset
 722   top-before:&:duplex-list:char <- get *editor, top-of-screen:offset
 723 ]
 724 before <move-cursor-end> [
 725   top-after:&:duplex-list:char <- get *editor, top-of-screen:offset
 726   cursor-row:num <- get *editor, cursor-row:offset
 727   cursor-column:num <- get *editor, cursor-column:offset
 728   {
 729     break-unless undo-coalesce-tag
 730     # if previous operation was also a move, and also had the same coalesce
 731     # tag, coalesce with it
 732     undo:&:list:&:operation <- get *editor, undo:offset
 733     break-unless undo
 734     op:&:operation <- first undo
 735     move:move-operation, is-move?:bool <- maybe-convert *op, move:variant
 736     break-unless is-move?
 737     previous-coalesce-tag:num <- get move, tag:offset
 738     coalesce?:bool <- equal undo-coalesce-tag, previous-coalesce-tag
 739     break-unless coalesce?
 740     move <- put move, after-row:offset, cursor-row
 741     move <- put move, after-column:offset, cursor-column
 742     move <- put move, after-top-of-screen:offset, top-after
 743     *op <- merge 1/move-operation, move
 744     break +done-adding-move-operation
 745   }
 746   op:&:operation <- new operation:type
 747   *op <- merge 1/move-operation, cursor-row-before, cursor-column-before, top-before, cursor-row/after, cursor-column/after, top-after, undo-coalesce-tag
 748   editor <- add-operation editor, op
 749   +done-adding-move-operation
 750 ]
 751 
 752 after <handle-undo> [
 753   {
 754     move:move-operation, is-move?:bool <- maybe-convert *op, move:variant
 755     break-unless is-move?
 756     # assert cursor-row/cursor-column/top-of-screen match after-row/after-column/after-top-of-screen
 757     cursor-row <- get move, before-row:offset
 758     *editor <- put *editor, cursor-row:offset, cursor-row
 759     cursor-column <- get move, before-column:offset
 760     *editor <- put *editor, cursor-column:offset, cursor-column
 761     top:&:duplex-list:char <- get move, before-top-of-screen:offset
 762     *editor <- put *editor, top-of-screen:offset, top
 763   }
 764 ]
 765 
 766 scenario editor-can-undo-scroll [
 767   local-scope
 768   # screen has 1 line for menu + 3 lines
 769   assume-screen 5/width, 4/height
 770   # editor contains a wrapped line
 771   contents:text <- new [a
 772 b
 773 cdefgh]
 774   e:&:editor <- new-editor contents, 0/left, 5/right
 775   # position cursor at end of screen and try to move right
 776   assume-console [
 777     left-click 3, 3
 778     press right-arrow
 779   ]
 780   editor-event-loop screen, console, e
 781   3:num/raw <- get *e, cursor-row:offset
 782   4:num/raw <- get *e, cursor-column:offset
 783   # screen scrolls
 784   screen-should-contain [
 785     .     .
 786     .b    .
 787     .cdef↩.
 788     .gh   .
 789   ]
 790   memory-should-contain [
 791     3 <- 3
 792     4 <- 0
 793   ]
 794   # undo
 795   assume-console [
 796     press ctrl-z
 797   ]
 798   run [
 799     editor-event-loop screen, console, e
 800   ]
 801   # cursor moved back
 802   3:num/raw <- get *e, cursor-row:offset
 803   4:num/raw <- get *e, cursor-column:offset
 804   memory-should-contain [
 805     3 <- 3
 806     4 <- 3
 807   ]
 808   # scroll undone
 809   screen-should-contain [
 810     .     .
 811     .a    .
 812     .b    .
 813     .cdef↩.
 814   ]
 815   # cursor should be in the right place
 816   assume-console [
 817     type [1]
 818   ]
 819   run [
 820     editor-event-loop screen, console, e
 821   ]
 822   screen-should-contain [
 823     .     .
 824     .b    .
 825     .cde1↩.
 826     .fgh  .
 827   ]
 828 ]
 829 
 830 scenario editor-can-undo-left-arrow [
 831   local-scope
 832   # create an editor with some text
 833   assume-screen 10/width, 5/height
 834   contents:text <- new [abc
 835 def
 836 ghi]
 837   e:&:editor <- new-editor contents, 0/left, 10/right
 838   editor-render screen, e
 839   # move the cursor
 840   assume-console [
 841     left-click 3, 1
 842     press left-arrow
 843   ]
 844   editor-event-loop screen, console, e
 845   # undo
 846   assume-console [
 847     press ctrl-z
 848   ]
 849   run [
 850     editor-event-loop screen, console, e
 851   ]
 852   # cursor moves back
 853   3:num/raw <- get *e, cursor-row:offset
 854   4:num/raw <- get *e, cursor-column:offset
 855   memory-should-contain [
 856     3 <- 3
 857     4 <- 1
 858   ]
 859   # cursor should be in the right place
 860   assume-console [
 861     type [1]
 862   ]
 863   run [
 864     editor-event-loop screen, console, e
 865   ]
 866   screen-should-contain [
 867     .          .
 868     .abc       .
 869     .def       .
 870     .g1hi      .
 871     .╌╌╌╌╌╌╌╌╌╌.
 872   ]
 873 ]
 874 
 875 scenario editor-can-undo-up-arrow [
 876   local-scope
 877   # create an editor with some text
 878   assume-screen 10/width, 5/height
 879   contents:text <- new [abc
 880 def
 881 ghi]
 882   e:&:editor <- new-editor contents, 0/left, 10/right
 883   editor-render screen, e
 884   # move the cursor
 885   assume-console [
 886     left-click 3, 1
 887     press up-arrow
 888   ]
 889   editor-event-loop screen, console, e
 890   3:num/raw <- get *e, cursor-row:offset
 891   4:num/raw <- get *e, cursor-column:offset
 892   memory-should-contain [
 893     3 <- 2
 894     4 <- 1
 895   ]
 896   # undo
 897   assume-console [
 898     press ctrl-z
 899   ]
 900   run [
 901     editor-event-loop screen, console, e
 902   ]
 903   # cursor moves back
 904   3:num/raw <- get *e, cursor-row:offset
 905   4:num/raw <- get *e, cursor-column:offset
 906   memory-should-contain [
 907     3 <- 3
 908     4 <- 1
 909   ]
 910   # cursor should be in the right place
 911   assume-console [
 912     type [1]
 913   ]
 914   run [
 915     editor-event-loop screen, console, e
 916   ]
 917   screen-should-contain [
 918     .          .
 919     .abc       .
 920     .def       .
 921     .g1hi      .
 922     .╌╌╌╌╌╌╌╌╌╌.
 923   ]
 924 ]
 925 
 926 scenario editor-can-undo-down-arrow [
 927   local-scope
 928   # create an editor with some text
 929   assume-screen 10/width, 5/height
 930   contents:text <- new [abc
 931 def
 932 ghi]
 933   e:&:editor <- new-editor contents, 0/left, 10/right
 934   editor-render screen, e
 935   # move the cursor
 936   assume-console [
 937     left-click 2, 1
 938     press down-arrow
 939   ]
 940   editor-event-loop screen, console, e
 941   # undo
 942   assume-console [
 943     press ctrl-z
 944   ]
 945   run [
 946     editor-event-loop screen, console, e
 947   ]
 948   # cursor moves back
 949   3:num/raw <- get *e, cursor-row:offset
 950   4:num/raw <- get *e, cursor-column:offset
 951   memory-should-contain [
 952     3 <- 2
 953     4 <- 1
 954   ]
 955   # cursor should be in the right place
 956   assume-console [
 957     type [1]
 958   ]
 959   run [
 960     editor-event-loop screen, console, e
 961   ]
 962   screen-should-contain [
 963     .          .
 964     .abc       .
 965     .d1ef      .
 966     .ghi       .
 967     .╌╌╌╌╌╌╌╌╌╌.
 968   ]
 969 ]
 970 
 971 scenario editor-can-undo-ctrl-f [
 972   local-scope
 973   # create an editor with multiple pages of text
 974   assume-screen 10/width, 5/height
 975   contents:text <- new [a
 976 b
 977 c
 978 d
 979 e
 980 f]
 981   e:&:editor <- new-editor contents, 0/left, 10/right
 982   editor-render screen, e
 983   # scroll the page
 984   assume-console [
 985     press ctrl-f
 986   ]
 987   editor-event-loop screen, console, e
 988   # undo
 989   assume-console [
 990     press ctrl-z
 991   ]
 992   run [
 993     editor-event-loop screen, console, e
 994   ]
 995   # screen should again show page 1
 996   screen-should-contain [
 997     .          .
 998     .a         .
 999     .b         .
1000     .c         .
1001     .d         .
1002   ]
1003 ]
1004 
1005 scenario editor-can-undo-page-down [
1006   local-scope
1007   # create an editor with multiple pages of text
1008   assume-screen 10/width, 5/height
1009   contents:text <- new [a
1010 b
1011 c
1012 d
1013 e
1014 f]
1015   e:&:editor <- new-editor contents, 0/left, 10/right
1016   editor-render screen, e
1017   # scroll the page
1018   assume-console [
1019     press page-down
1020   ]
1021   editor-event-loop screen, console, e
1022   # undo
1023   assume-console [
1024     press ctrl-z
1025   ]
1026   run [
1027     editor-event-loop screen, console, e
1028   ]
1029   # screen should again show page 1
1030   screen-should-contain [
1031     .          .
1032     .a         .
1033     .b         .
1034     .c         .
1035     .d         .
1036   ]
1037 ]
1038 
1039 scenario editor-can-undo-ctrl-b [
1040   local-scope
1041   # create an editor with multiple pages of text
1042   assume-screen 10/width, 5/height
1043   contents:text <- new [a
1044 b
1045 c
1046 d
1047 e
1048 f]
1049   e:&:editor <- new-editor contents, 0/left, 10/right
1050   editor-render screen, e
1051   # scroll the page down and up
1052   assume-console [
1053     press page-down
1054     press ctrl-b
1055   ]
1056   editor-event-loop screen, console, e
1057   # undo
1058   assume-console [
1059     press ctrl-z
1060   ]
1061   run [
1062     editor-event-loop screen, console, e
1063   ]
1064   # screen should again show page 2
1065   screen-should-contain [
1066     .          .
1067     .d         .
1068     .e         .
1069     .f         .
1070     .╌╌╌╌╌╌╌╌╌╌.
1071   ]
1072 ]
1073 
1074 scenario editor-can-undo-page-up [
1075   local-scope
1076   # create an editor with multiple pages of text
1077   assume-screen 10/width, 5/height
1078   contents:text <- new [a
1079 b
1080 c
1081 d
1082 e
1083 f]
1084   e:&:editor <- new-editor contents, 0/left, 10/right
1085   editor-render screen, e
1086   # scroll the page down and up
1087   assume-console [
1088     press page-down
1089     press page-up
1090   ]
1091   editor-event-loop screen, console, e
1092   # undo
1093   assume-console [
1094     press ctrl-z
1095   ]
1096   run [
1097     editor-event-loop screen, console, e
1098   ]
1099   # screen should again show page 2
1100   screen-should-contain [
1101     .          .
1102     .d         .
1103     .e         .
1104     .f         .
1105     .╌╌╌╌╌╌╌╌╌╌.
1106   ]
1107 ]
1108 
1109 scenario editor-can-undo-ctrl-a [
1110   local-scope
1111   # create an editor with some text
1112   assume-screen 10/width, 5/height
1113   contents:text <- new [abc
1114 def
1115 ghi]
1116   e:&:editor <- new-editor contents, 0/left, 10/right
1117   editor-render screen, e
1118   # move the cursor, then to start of line
1119   assume-console [
1120     left-click 2, 1
1121     press ctrl-a
1122   ]
1123   editor-event-loop screen, console, e
1124   # undo
1125   assume-console [
1126     press ctrl-z
1127   ]
1128   run [
1129     editor-event-loop screen, console, e
1130   ]
1131   # cursor moves back
1132   3:num/raw <- get *e, cursor-row:offset
1133   4:num/raw <- get *e, cursor-column:offset
1134   memory-should-contain [
1135     3 <- 2
1136     4 <- 1
1137   ]
1138   # cursor should be in the right place
1139   assume-console [
1140     type [1]
1141   ]
1142   run [
1143     editor-event-loop screen, console, e
1144   ]
1145   screen-should-contain [
1146     .          .
1147     .abc       .
1148     .d1ef      .
1149     .ghi       .
1150     .╌╌╌╌╌╌╌╌╌╌.
1151   ]
1152 ]
1153 
1154 scenario editor-can-undo-home [
1155   local-scope
1156   # create an editor with some text
1157   assume-screen 10/width, 5/height
1158   contents:text <- new [abc
1159 def
1160 ghi]
1161   e:&:editor <- new-editor contents, 0/left, 10/right
1162   editor-render screen, e
1163   # move the cursor, then to start of line
1164   assume-console [
1165     left-click 2, 1
1166     press home
1167   ]
1168   editor-event-loop screen, console, e
1169   # undo
1170   assume-console [
1171     press ctrl-z
1172   ]
1173   run [
1174     editor-event-loop screen, console, e
1175   ]
1176   # cursor moves back
1177   3:num/raw <- get *e, cursor-row:offset
1178   4:num/raw <- get *e, cursor-column:offset
1179   memory-should-contain [
1180     3 <- 2
1181     4 <- 1
1182   ]
1183   # cursor should be in the right place
1184   assume-console [
1185     type [1]
1186   ]
1187   run [
1188     editor-event-loop screen, console, e
1189   ]
1190   screen-should-contain [
1191     .          .
1192     .abc       .
1193     .d1ef      .
1194     .ghi       .
1195     .╌╌╌╌╌╌╌╌╌╌.
1196   ]
1197 ]
1198 
1199 scenario editor-can-undo-ctrl-e [
1200   local-scope
1201   # create an editor with some text
1202   assume-screen 10/width, 5/height
1203   contents:text <- new [abc
1204 def
1205 ghi]
1206   e:&:editor <- new-editor contents, 0/left, 10/right
1207   editor-render screen, e
1208   # move the cursor, then to start of line
1209   assume-console [
1210     left-click 2, 1
1211     press ctrl-e
1212   ]
1213   editor-event-loop screen, console, e
1214   # undo
1215   assume-console [
1216     press ctrl-z
1217   ]
1218   run [
1219     editor-event-loop screen, console, e
1220   ]
1221   # cursor moves back
1222   3:num/raw <- get *e, cursor-row:offset
1223   4:num/raw <- get *e, cursor-column:offset
1224   memory-should-contain [
1225     3 <- 2
1226     4 <- 1
1227   ]
1228   # cursor should be in the right place
1229   assume-console [
1230     type [1]
1231   ]
1232   run [
1233     editor-event-loop screen, console, e
1234   ]
1235   screen-should-contain [
1236     .          .
1237     .abc       .
1238     .d1ef      .
1239     .ghi       .
1240     .╌╌╌╌╌╌╌╌╌╌.
1241   ]
1242 ]
1243 
1244 scenario editor-can-undo-end [
1245   local-scope
1246   # create an editor with some text
1247   assume-screen 10/width, 5/height
1248   contents:text <- new [abc
1249 def
1250 ghi]
1251   e:&:editor <- new-editor contents, 0/left, 10/right
1252   editor-render screen, e
1253   # move the cursor, then to start of line
1254   assume-console [
1255     left-click 2, 1
1256     press end
1257   ]
1258   editor-event-loop screen, console, e
1259   # undo
1260   assume-console [
1261     press ctrl-z
1262   ]
1263   run [
1264     editor-event-loop screen, console, e
1265   ]
1266   # cursor moves back
1267   3:num/raw <- get *e, cursor-row:offset
1268   4:num/raw <- get *e, cursor-column:offset
1269   memory-should-contain [
1270     3 <- 2
1271     4 <- 1
1272   ]
1273   # cursor should be in the right place
1274   assume-console [
1275     type [1]
1276   ]
1277   run [
1278     editor-event-loop screen, console, e
1279   ]
1280   screen-should-contain [
1281     .          .
1282     .abc       .
1283     .d1ef      .
1284     .ghi       .
1285     .╌╌╌╌╌╌╌╌╌╌.
1286   ]
1287 ]
1288 
1289 scenario editor-can-undo-multiple-arrows-in-the-same-direction [
1290   local-scope
1291   # create an editor with some text
1292   assume-screen 10/width, 5/height
1293   contents:text <- new [abc
1294 def
1295 ghi]
1296   e:&:editor <- new-editor contents, 0/left, 10/right
1297   editor-render screen, e
1298   # move the cursor
1299   assume-console [
1300     left-click 2, 1
1301     press right-arrow
1302     press right-arrow
1303     press up-arrow
1304   ]
1305   editor-event-loop screen, console, e
1306   3:num/raw <- get *e, cursor-row:offset
1307   4:num/raw <- get *e, cursor-column:offset
1308   memory-should-contain [
1309     3 <- 1
1310     4 <- 3
1311   ]
1312   # undo
1313   assume-console [
1314     press ctrl-z
1315   ]
1316   run [
1317     editor-event-loop screen, console, e
1318   ]
1319   # up-arrow is undone
1320   3:num/raw <- get *e, cursor-row:offset
1321   4:num/raw <- get *e, cursor-column:offset
1322   memory-should-contain [
1323     3 <- 2
1324     4 <- 3
1325   ]
1326   # undo again
1327   assume-console [
1328     press ctrl-z
1329   ]
1330   run [
1331     editor-event-loop screen, console, e
1332   ]
1333   # both right-arrows are undone
1334   3:num/raw <- get *e, cursor-row:offset
1335   4:num/raw <- get *e, cursor-column:offset
1336   memory-should-contain [
1337     3 <- 2
1338     4 <- 1
1339   ]
1340 ]
1341 
1342 # redo cursor movement and scroll
1343 
1344 scenario editor-redo-touch [
1345   local-scope
1346   # create an editor with some text, click on a character, undo
1347   assume-screen 10/width, 5/height
1348   contents:text <- new [abc
1349 def
1350 ghi]
1351   e:&:editor <- new-editor contents, 0/left, 10/right
1352   editor-render screen, e
1353   assume-console [
1354     left-click 3, 1
1355     press ctrl-z
1356   ]
1357   editor-event-loop screen, console, e
1358   # redo
1359   assume-console [
1360     press ctrl-y
1361   ]
1362   run [
1363     editor-event-loop screen, console, e
1364   ]
1365   # cursor moves to left-click
1366   3:num/raw <- get *e, cursor-row:offset
1367   4:num/raw <- get *e, cursor-column:offset
1368   memory-should-contain [
1369     3 <- 3
1370     4 <- 1
1371   ]
1372   # cursor should be in the right place
1373   assume-console [
1374     type [1]
1375   ]
1376   run [
1377     editor-event-loop screen, console, e
1378   ]
1379   screen-should-contain [
1380     .          .
1381     .abc       .
1382     .def       .
1383     .g1hi      .
1384     .╌╌╌╌╌╌╌╌╌╌.
1385   ]
1386 ]
1387 
1388 after <handle-redo> [
1389   {
1390     move:move-operation, is-move?:bool <- maybe-convert *op, move:variant
1391     break-unless is-move?
1392     # assert cursor-row/cursor-column/top-of-screen match after-row/after-column/after-top-of-screen
1393     cursor-row <- get move, after-row:offset
1394     *editor <- put *editor, cursor-row:offset, cursor-row
1395     cursor-column <- get move, after-column:offset
1396     *editor <- put *editor, cursor-column:offset, cursor-column
1397     top:&:duplex-list:char <- get move, after-top-of-screen:offset
1398     *editor <- put *editor, top-of-screen:offset, top
1399   }
1400 ]
1401 
1402 scenario editor-separates-undo-insert-from-undo-cursor-move [
1403   local-scope
1404   # create an editor, type some text, move the cursor, type some more text
1405   assume-screen 10/width, 5/height
1406   e:&:editor <- new-editor [], 0/left, 10/right
1407   editor-render screen, e
1408   assume-console [
1409     type [abc]
1410     left-click 1, 1
1411     type [d]
1412   ]
1413   editor-event-loop screen, console, e
1414   3:num/raw <- get *e, cursor-row:offset
1415   4:num/raw <- get *e, cursor-column:offset
1416   screen-should-contain [
1417     .          .
1418     .adbc      .
1419     .╌╌╌╌╌╌╌╌╌╌.
1420     .          .
1421   ]
1422   memory-should-contain [
1423     3 <- 1
1424     4 <- 2
1425   ]
1426   # undo
1427   assume-console [
1428     press ctrl-z
1429   ]
1430   run [
1431     editor-event-loop screen, console, e
1432     3:num/raw <- get *e, cursor-row:offset
1433     4:num/raw <- get *e, cursor-column:offset
1434   ]
1435   # last letter typed is deleted
1436   screen-should-contain [
1437     .          .
1438     .abc       .
1439     .╌╌╌╌╌╌╌╌╌╌.
1440     .          .
1441   ]
1442   memory-should-contain [
1443     3 <- 1
1444     4 <- 1
1445   ]
1446   # undo again
1447   assume-console [
1448     press ctrl-z
1449   ]
1450   run [
1451     editor-event-loop screen, console, e
1452     3:num/raw <- get *e, cursor-row:offset
1453     4:num/raw <- get *e, cursor-column:offset
1454   ]
1455   # no change to screen; cursor moves
1456   screen-should-contain [
1457     .          .
1458     .abc       .
1459     .╌╌╌╌╌╌╌╌╌╌.
1460     .          .
1461   ]
1462   memory-should-contain [
1463     3 <- 1
1464     4 <- 3
1465   ]
1466   # undo again
1467   assume-console [
1468     press ctrl-z
1469   ]
1470   run [
1471     editor-event-loop screen, console, e
1472     3:num/raw <- get *e, cursor-row:offset
1473     4:num/raw <- get *e, cursor-column:offset
1474   ]
1475   # screen empty
1476   screen-should-contain [
1477     .          .
1478     .          .
1479     .╌╌╌╌╌╌╌╌╌╌.
1480     .          .
1481   ]
1482   memory-should-contain [
1483     3 <- 1
1484     4 <- 0
1485   ]
1486   # redo
1487   assume-console [
1488     press ctrl-y
1489   ]
1490   run [
1491     editor-event-loop screen, console, e
1492     3:num/raw <- get *e, cursor-row:offset
1493     4:num/raw <- get *e, cursor-column:offset
1494   ]
1495   # first insert
1496   screen-should-contain [
1497     .          .
1498     .abc       .
1499     .╌╌╌╌╌╌╌╌╌╌.
1500     .          .
1501   ]
1502   memory-should-contain [
1503     3 <- 1
1504     4 <- 3
1505   ]
1506   # redo again
1507   assume-console [
1508     press ctrl-y
1509   ]
1510   run [
1511     editor-event-loop screen, console, e
1512     3:num/raw <- get *e, cursor-row:offset
1513     4:num/raw <- get *e, cursor-column:offset
1514   ]
1515   # cursor moves
1516   screen-should-contain [
1517     .          .
1518     .abc       .
1519     .╌╌╌╌╌╌╌╌╌╌.
1520     .          .
1521   ]
1522   # cursor moves
1523   memory-should-contain [
1524     3 <- 1
1525     4 <- 1
1526   ]
1527   # redo again
1528   assume-console [
1529     press ctrl-y
1530   ]
1531   run [
1532     editor-event-loop screen, console, e
1533     3:num/raw <- get *e, cursor-row:offset
1534     4:num/raw <- get *e, cursor-column:offset
1535   ]
1536   # second insert
1537   screen-should-contain [
1538     .          .
1539     .adbc      .
1540     .╌╌╌╌╌╌╌╌╌╌.
1541     .          .
1542   ]
1543   memory-should-contain [
1544     3 <- 1
1545     4 <- 2
1546   ]
1547 ]
1548 
1549 # undo backspace
1550 
1551 scenario editor-can-undo-and-redo-backspace [
1552   local-scope
1553   # create an editor
1554   assume-screen 10/width, 5/height
1555   e:&:editor <- new-editor [], 0/left, 10/right
1556   editor-render screen, e
1557   # insert some text and hit backspace
1558   assume-console [
1559     type [abc]
1560     press backspace
1561     press backspace
1562   ]
1563   editor-event-loop screen, console, e
1564   screen-should-contain [
1565     .          .
1566     .a         .
1567     .╌╌╌╌╌╌╌╌╌╌.
1568     .          .
1569   ]
1570   3:num/raw <- get *e, cursor-row:offset
1571   4:num/raw <- get *e, cursor-column:offset
1572   memory-should-contain [
1573     3 <- 1
1574     4 <- 1
1575   ]
1576   # undo
1577   assume-console [
1578     press ctrl-z
1579   ]
1580   run [
1581     editor-event-loop screen, console, e
1582   ]
1583   3:num/raw <- get *e, cursor-row:offset
1584   4:num/raw <- get *e, cursor-column:offset
1585   memory-should-contain [
1586     3 <- 1
1587     4 <- 3
1588   ]
1589   screen-should-contain [
1590     .          .
1591     .abc       .
1592     .╌╌╌╌╌╌╌╌╌╌.
1593     .          .
1594   ]
1595   # redo
1596   assume-console [
1597     press ctrl-y
1598   ]
1599   run [
1600     editor-event-loop screen, console, e
1601   ]
1602   3:num/raw <- get *e, cursor-row:offset
1603   4:num/raw <- get *e, cursor-column:offset
1604   memory-should-contain [
1605     3 <- 1
1606     4 <- 1
1607   ]
1608   screen-should-contain [
1609     .          .
1610     .a         .
1611     .╌╌╌╌╌╌╌╌╌╌.
1612     .          .
1613   ]
1614 ]
1615 
1616 # save operation to undo
1617 after <backspace-character-begin> [
1618   top-before:&:duplex-list:char <- get *editor, top-of-screen:offset
1619 ]
1620 before <backspace-character-end> [
1621   {
1622     break-unless backspaced-cell  # backspace failed; don't add an undo operation
1623     top-after:&:duplex-list:char <- get *editor, top-of-screen:offset
1624     cursor-row:num <- get *editor, cursor-row:offset
1625     cursor-column:num <- get *editor, cursor-row:offset
1626     before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
1627     undo:&:list:&:operation <- get *editor, undo:offset
1628     {
1629       # if previous operation was an insert, coalesce this operation with it
1630       break-unless undo
1631       op:&:operation <- first undo
1632       deletion:delete-operation, is-delete?:bool <- maybe-convert *op, delete:variant
1633       break-unless is-delete?
1634       previous-coalesce-tag:num <- get deletion, tag:offset
1635       coalesce?:bool <- equal previous-coalesce-tag, 1/coalesce-backspace
1636       break-unless coalesce?
1637       deletion <- put deletion, delete-from:offset, before-cursor
1638       backspaced-so-far:&:duplex-list:char <- get deletion, deleted-text:offset
1639       insert-range backspaced-cell, backspaced-so-far
1640       deletion <- put deletion, deleted-text:offset, backspaced-cell
1641       deletion <- put deletion, after-row:offset, cursor-row
1642       deletion <- put deletion, after-column:offset, cursor-column
1643       deletion <- put deletion, after-top-of-screen:offset, top-after
1644       *op <- merge 2/delete-operation, deletion
1645       break +done-adding-backspace-operation
1646     }
1647     # if not, create a new operation
1648     op:&:operation <- new operation:type
1649     deleted-until:&:duplex-list:char <- next before-cursor
1650     *op <- merge 2/delete-operation, save-row/before, save-column/before, top-before, cursor-row/after, cursor-column/after, top-after, backspaced-cell/deleted, before-cursor/delete-from, deleted-until, 1/coalesce-backspace
1651     editor <- add-operation editor, op
1652     +done-adding-backspace-operation
1653   }
1654 ]
1655 
1656 after <handle-undo> [
1657   {
1658     deletion:delete-operation, is-delete?:bool <- maybe-convert *op, delete:variant
1659     break-unless is-delete?
1660     anchor:&:duplex-list:char <- get deletion, delete-from:offset
1661     break-unless anchor
1662     deleted:&:duplex-list:char <- get deletion, deleted-text:offset
1663     old-cursor:&:duplex-list:char <- last deleted
1664     insert-range anchor, deleted
1665     # assert cursor-row/cursor-column/top-of-screen match after-row/after-column/after-top-of-screen
1666     before-cursor <- copy old-cursor
1667     cursor-row <- get deletion, before-row:offset
1668     *editor <- put *editor, cursor-row:offset, cursor-row
1669     cursor-column <- get deletion, before-column:offset
1670     *editor <- put *editor, cursor-column:offset, cursor-column
1671     top:&:duplex-list:char <- get deletion, before-top-of-screen:offset
1672     *editor <- put *editor, top-of-screen:offset, top
1673   }
1674 ]
1675 
1676 after <handle-redo> [
1677   {
1678     deletion:delete-operation, is-delete?:bool <- maybe-convert *op, delete:variant
1679     break-unless is-delete?
1680     start:&:duplex-list:char <- get deletion, delete-from:offset
1681     end:&:duplex-list:char <- get deletion, delete-until:offset
1682     data:&:duplex-list:char <- get *editor, data:offset
1683     remove-between start, end
1684     # assert cursor-row/cursor-column/top-of-screen match after-row/after-column/after-top-of-screen
1685     cursor-row <- get deletion, after-row:offset
1686     *editor <- put *editor, cursor-row:offset, cursor-row
1687     cursor-column <- get deletion, after-column:offset
1688     *editor <- put *editor, cursor-column:offset, cursor-column
1689     top:&:duplex-list:char <- get deletion, before-top-of-screen:offset
1690     *editor <- put *editor, top-of-screen:offset, top
1691   }
1692 ]
1693 
1694 # undo delete
1695 
1696 scenario editor-can-undo-and-redo-delete [
1697   local-scope
1698   # create an editor
1699   assume-screen 10/width, 5/height
1700   e:&:editor <- new-editor [], 0/left, 10/right
1701   editor-render screen, e
1702   # insert some text and hit delete and backspace a few times
1703   assume-console [
1704     type [abcdef]
1705     left-click 1, 2
1706     press delete
1707     press backspace
1708     press delete
1709     press delete
1710   ]
1711   editor-event-loop screen, console, e
1712   screen-should-contain [
1713     .          .
1714     .af        .
1715     .╌╌╌╌╌╌╌╌╌╌.
1716     .          .
1717   ]
1718   3:num/raw <- get *e, cursor-row:offset
1719   4:num/raw <- get *e, cursor-column:offset
1720   memory-should-contain [
1721     3 <- 1
1722     4 <- 1
1723   ]
1724   # undo deletes
1725   assume-console [
1726     press ctrl-z
1727   ]
1728   run [
1729     editor-event-loop screen, console, e
1730   ]
1731   3:num/raw <- get *e, cursor-row:offset
1732   4:num/raw <- get *e, cursor-column:offset
1733   memory-should-contain [
1734     3 <- 1
1735     4 <- 1
1736   ]
1737   screen-should-contain [
1738     .          .
1739     .adef      .
1740     .╌╌╌╌╌╌╌╌╌╌.
1741     .          .
1742   ]
1743   # undo backspace
1744   assume-console [
1745     press ctrl-z
1746   ]
1747   run [
1748     editor-event-loop screen, console, e
1749   ]
1750   3:num/raw <- get *e, cursor-row:offset
1751   4:num/raw <- get *e, cursor-column:offset
1752   memory-should-contain [
1753     3 <- 1
1754     4 <- 2
1755   ]
1756   screen-should-contain [
1757     .          .
1758     .abdef     .
1759     .╌╌╌╌╌╌╌╌╌╌.
1760     .          .
1761   ]
1762   # undo first delete
1763   assume-console [
1764     press ctrl-z
1765   ]
1766   run [
1767     editor-event-loop screen, console, e
1768   ]
1769   3:num/raw <- get *e, cursor-row:offset
1770   4:num/raw <- get *e, cursor-column:offset
1771   memory-should-contain [
1772     3 <- 1
1773     4 <- 2
1774   ]
1775   screen-should-contain [
1776     .          .
1777     .abcdef    .
1778     .╌╌╌╌╌╌╌╌╌╌.
1779     .          .
1780   ]
1781   # redo first delete
1782   assume-console [
1783     press ctrl-y
1784   ]
1785   run [
1786     editor-event-loop screen, console, e
1787   ]
1788   # first line inserted
1789   3:num/raw <- get *e, cursor-row:offset
1790   4:num/raw <- get *e, cursor-column:offset
1791   memory-should-contain [
1792     3 <- 1
1793     4 <- 2
1794   ]
1795   screen-should-contain [
1796     .          .
1797     .abdef     .
1798     .╌╌╌╌╌╌╌╌╌╌.
1799     .          .
1800   ]
1801   # redo backspace
1802   assume-console [
1803     press ctrl-y
1804   ]
1805   run [
1806     editor-event-loop screen, console, e
1807   ]
1808   # first line inserted
1809   3:num/raw <- get *e, cursor-row:offset
1810   4:num/raw <- get *e, cursor-column:offset
1811   memory-should-contain [
1812     3 <- 1
1813     4 <- 1
1814   ]
1815   screen-should-contain [
1816     .          .
1817     .adef      .
1818     .╌╌╌╌╌╌╌╌╌╌.
1819     .          .
1820   ]
1821   # redo deletes
1822   assume-console [
1823     press ctrl-y
1824   ]
1825   run [
1826     editor-event-loop screen, console, e
1827   ]
1828   # first line inserted
1829   3:num/raw <- get *e, cursor-row:offset
1830   4:num/raw <- get *e, cursor-column:offset
1831   memory-should-contain [
1832     3 <- 1
1833     4 <- 1
1834   ]
1835   screen-should-contain [
1836     .          .
1837     .af        .
1838     .╌╌╌╌╌╌╌╌╌╌.
1839     .          .
1840   ]
1841 ]
1842 
1843 after <delete-character-begin> [
1844   top-before:&:duplex-list:char <- get *editor, top-of-screen:offset
1845 ]
1846 before <delete-character-end> [
1847   {
1848     break-unless deleted-cell  # delete failed; don't add an undo operation
1849     top-after:&:duplex-list:char <- get *editor, top-of-screen:offset
1850     cursor-row:num <- get *editor, cursor-row:offset
1851     cursor-column:num <- get *editor, cursor-column:offset
1852     before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
1853     undo:&:list:&:operation <- get *editor, undo:offset
1854     {
1855       # if previous operation was an insert, coalesce this operation with it
1856       break-unless undo
1857       op:&:operation <- first undo
1858       deletion:delete-operation, is-delete?:bool <- maybe-convert *op, delete:variant
1859       break-unless is-delete?
1860       previous-coalesce-tag:num <- get deletion, tag:offset
1861       coalesce?:bool <- equal previous-coalesce-tag, 2/coalesce-delete
1862       break-unless coalesce?
1863       delete-until:&:duplex-list:char <- next before-cursor
1864       deletion <- put deletion, delete-until:offset, delete-until
1865       deleted-so-far:&:duplex-list:char <- get deletion, deleted-text:offset
1866       deleted-so-far <- append deleted-so-far, deleted-cell
1867       deletion <- put deletion, deleted-text:offset, deleted-so-far
1868       deletion <- put deletion, after-row:offset, cursor-row
1869       deletion <- put deletion, after-column:offset, cursor-column
1870       deletion <- put deletion, after-top-of-screen:offset, top-after
1871       *op <- merge 2/delete-operation, deletion
1872       break +done-adding-delete-operation
1873     }
1874     # if not, create a new operation
1875     op:&:operation <- new operation:type
1876     deleted-until:&:duplex-list:char <- next before-cursor
1877     *op <- merge 2/delete-operation, save-row/before, save-column/before, top-before, cursor-row/after, cursor-column/after, top-after, deleted-cell/deleted, before-cursor/delete-from, deleted-until, 2/coalesce-delete
1878     editor <- add-operation editor, op
1879     +done-adding-delete-operation
1880   }
1881 ]
1882 
1883 # undo ctrl-k
1884 
1885 scenario editor-can-undo-and-redo-ctrl-k [
1886   local-scope
1887   # create an editor
1888   assume-screen 10/width, 5/height
1889   contents:text <- new [abc
1890 def]
1891   e:&:editor <- new-editor contents, 0/left, 10/right
1892   editor-render screen, e
1893   # insert some text and hit delete and backspace a few times
1894   assume-console [
1895     left-click 1, 1
1896     press ctrl-k
1897   ]
1898   editor-event-loop screen, console, e
1899   screen-should-contain [
1900     .          .
1901     .a         .
1902     .def       .
1903     .╌╌╌╌╌╌╌╌╌╌.
1904     .          .
1905   ]
1906   3:num/raw <- get *e, cursor-row:offset
1907   4:num/raw <- get *e, cursor-column:offset
1908   memory-should-contain [
1909     3 <- 1
1910     4 <- 1
1911   ]
1912   # undo
1913   assume-console [
1914     press ctrl-z
1915   ]
1916   run [
1917     editor-event-loop screen, console, e
1918   ]
1919   screen-should-contain [
1920     .          .
1921     .abc       .
1922     .def       .
1923     .╌╌╌╌╌╌╌╌╌╌.
1924     .          .
1925   ]
1926   3:num/raw <- get *e, cursor-row:offset
1927   4:num/raw <- get *e, cursor-column:offset
1928   memory-should-contain [
1929     3 <- 1
1930     4 <- 1
1931   ]
1932   # redo
1933   assume-console [
1934     press ctrl-y
1935   ]
1936   run [
1937     editor-event-loop screen, console, e
1938   ]
1939   # first line inserted
1940   screen-should-contain [
1941     .          .
1942     .a         .
1943     .def       .
1944     .╌╌╌╌╌╌╌╌╌╌.
1945     .          .
1946   ]
1947   3:num/raw <- get *e, cursor-row:offset
1948   4:num/raw <- get *e, cursor-column:offset
1949   memory-should-contain [
1950     3 <- 1
1951     4 <- 1
1952   ]
1953   # cursor should be in the right place
1954   assume-console [
1955     type [1]
1956   ]
1957   run [
1958     editor-event-loop screen, console, e
1959   ]
1960   screen-should-contain [
1961     .          .
1962     .a1        .
1963     .def       .
1964     .╌╌╌╌╌╌╌╌╌╌.
1965     .          .
1966   ]
1967 ]
1968 
1969 after <delete-to-end-of-line-begin> [
1970   top-before:&:duplex-list:char <- get *editor, top-of-screen:offset
1971 ]
1972 before <delete-to-end-of-line-end> [
1973   {
1974     break-unless deleted-cells  # delete failed; don't add an undo operation
1975     top-after:&:duplex-list:char <- get *editor, top-of-screen:offset
1976     cursor-row:num <- get *editor, cursor-row:offset
1977     cursor-column:num <- get *editor, cursor-column:offset
1978     deleted-until:&:duplex-list:char <- next before-cursor
1979     op:&:operation <- new operation:type
1980     *op <- merge 2/delete-operation, save-row/before, save-column/before, top-before, cursor-row/after, cursor-column/after, top-after, deleted-cells/deleted, before-cursor/delete-from, deleted-until, 0/never-coalesce
1981     editor <- add-operation editor, op
1982     +done-adding-delete-operation
1983   }
1984 ]
1985 
1986 # undo ctrl-u
1987 
1988 scenario editor-can-undo-and-redo-ctrl-u [
1989   local-scope
1990   # create an editor
1991   assume-screen 10/width, 5/height
1992   contents:text <- new [abc
1993 def]
1994   e:&:editor <- new-editor contents, 0/left, 10/right
1995   editor-render screen, e
1996   # insert some text and hit delete and backspace a few times
1997   assume-console [
1998     left-click 1, 2
1999     press ctrl-u
2000   ]
2001   editor-event-loop screen, console, e
2002   screen-should-contain [
2003     .          .
2004     .c         .
2005     .def       .
2006     .╌╌╌╌╌╌╌╌╌╌.
2007     .          .
2008   ]
2009   3:num/raw <- get *e, cursor-row:offset
2010   4:num/raw <- get *e, cursor-column:offset
2011   memory-should-contain [
2012     3 <- 1
2013     4 <- 0
2014   ]
2015   # undo
2016   assume-console [
2017     press ctrl-z
2018   ]
2019   run [
2020     editor-event-loop screen, console, e
2021   ]
2022   screen-should-contain [
2023     .          .
2024     .abc       .
2025     .def       .
2026     .╌╌╌╌╌╌╌╌╌╌.
2027     .          .
2028   ]
2029   3:num/raw <- get *e, cursor-row:offset
2030   4:num/raw <- get *e, cursor-column:offset
2031   memory-should-contain [
2032     3 <- 1
2033     4 <- 2
2034   ]
2035   # redo
2036   assume-console [
2037     press ctrl-y
2038   ]
2039   run [
2040     editor-event-loop screen, console, e
2041   ]
2042   # first line inserted
2043   screen-should-contain [
2044     .          .
2045     .c         .
2046     .def       .
2047     .╌╌╌╌╌╌╌╌╌╌.
2048     .          .
2049   ]
2050   3:num/raw <- get *e, cursor-row:offset
2051   4:num/raw <- get *e, cursor-column:offset
2052   memory-should-contain [
2053     3 <- 1
2054     4 <- 0
2055   ]
2056   # cursor should be in the right place
2057   assume-console [
2058     type [1]
2059   ]
2060   run [
2061     editor-event-loop screen, console, e
2062   ]
2063   screen-should-contain [
2064     .          .
2065     .1c        .
2066     .def       .
2067     .╌╌╌╌╌╌╌╌╌╌.
2068     .          .
2069   ]
2070 ]
2071 
2072 after <delete-to-start-of-line-begin> [
2073   top-before:&:duplex-list:char <- get *editor, top-of-screen:offset
2074 ]
2075 before <delete-to-start-of-line-end> [
2076   {
2077     break-unless deleted-cells  # delete failed; don't add an undo operation
2078     top-after:&:duplex-list:char <- get *editor, top-of-screen:offset
2079     op:&:operation <- new operation:type
2080     before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
2081     deleted-until:&:duplex-list:char <- next before-cursor
2082     cursor-row:num <- get *editor, cursor-row:offset
2083     cursor-column:num <- get *editor, cursor-column:offset
2084     *op <- merge 2/delete-operation, save-row/before, save-column/before, top-before, cursor-row/after, cursor-column/after, top-after, deleted-cells/deleted, before-cursor/delete-from, deleted-until, 0/never-coalesce
2085     editor <- add-operation editor, op
2086     +done-adding-delete-operation
2087   }
2088 ]
2089 
2090 scenario editor-can-undo-and-redo-ctrl-u-2 [
2091   local-scope
2092   # create an editor
2093   assume-screen 10/width, 5/height
2094   e:&:editor <- new-editor [], 0/left, 10/right
2095   editor-render screen, e
2096   # insert some text and hit delete and backspace a few times
2097   assume-console [
2098     type [abc]
2099     press ctrl-u
2100     press ctrl-z
2101   ]
2102   editor-event-loop screen, console, e
2103   screen-should-contain [
2104     .          .
2105     .abc       .
2106     .╌╌╌╌╌╌╌╌╌╌.
2107     .          .
2108   ]
2109 ]