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