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 ]