## undo/redo # for every undoable event, create a type of *operation* that contains all the # information needed to reverse it exclusive-container operation [ typing:insert-operation move:move-operation delete:delete-operation ] container insert-operation [ before-row:num before-column:num before-top-of-screen:&:duplex-list:char after-row:num after-column:num after-top-of-screen:&:duplex-list:char # inserted text is from 'insert-from' until 'insert-until'; list doesn't have to terminate insert-from:&:duplex-list:char insert-until:&:duplex-list:char tag:num # event causing this operation; might be used to coalesce runs of similar events # 0: no coalesce (enter+indent) # 1: regular alphanumeric characters ] container move-operation [ before-row:num before-column:num before-top-of-screen:&:duplex-list:char after-row:num after-column:num after-top-of-screen:&:duplex-list:char tag:num # event causing this operation; might be used to coalesce runs of similar events # 0: no coalesce (touch events, etc) # 1: left arrow # 2: right arrow # 3: up arrow # 4: down arrow ] container delete-operation [ before-row:num before-column:num before-top-of-screen:&:duplex-list:char after-row:num after-column:num after-top-of-screen:&:duplex-list:char deleted-text:&:duplex-list:char delete-from:&:duplex-list:char delete-until:&:duplex-list:char tag:num # event causing this operation; might be used to coalesce runs of similar events # 0: no coalesce (ctrl-k, ctrl-u) # 1: backspace # 2: delete ] # every editor accumulates a list of operations to undo/redo container editor [ undo:&:list:&:operation redo:&:list:&:operation ] # ctrl-z - undo operation after [ { undo?:bool <- equal c, 26/ctrl-z break-unless undo? undo:&:list:&:operation <- get *editor, undo:offset break-unless undo op:&:operation <- first undo undo <- rest undo *editor <- put *editor, undo:offset, undo redo:&:list:&:operation <- get *editor, redo:offset redo <- push op, redo *editor <- put *editor, redo:offset, redo return screen/same-as-ingredient:0, editor/same-as-ingredient:1, 1/go-render } ] # ctrl-y - redo operation after [ { redo?:bool <- equal c, 25/ctrl-y break-unless redo? redo:&:list:&:operation <- get *editor, redo:offset break-unless redo op:&:operation <- first redo redo <- rest redo *editor <- put *editor, redo:offset, redo undo:&:list:&:operation <- get *editor, undo:offset undo <- push op, undo *editor <- put *editor, undo:offset, undo return screen/same-as-ingredient:0, editor/same-as-ingredient:1, 1/go-render } ] # undo typing scenario editor-can-undo-typing [ local-scope # create an editor and type a character assume-screen 10/width, 5/height e:&:editor <- new-editor [], 0/left, 10/right editor-render screen, e assume-console [ type [0] ] editor-event-loop screen, console, e # undo assume-console [ press ctrl-z ] run [ editor-event-loop screen, console, e ] # character should be gone screen-should-contain [ . . . . .┈┈┈┈┈┈┈┈┈┈. . . ] # cursor should be in the right place assume-console [ type [1] ] run [ editor-event-loop screen, console, e ] screen-should-contain [ . . .1 . .┈┈┈┈┈┈┈┈┈┈. . . ] ] # save operation to undo after [ top-before:&:duplex-list:char <- get *editor, top-of-screen:offset cursor-before:&:duplex-list:char <- get *editor, before-cursor:offset ] before [ top-after:&:duplex-list:char <- get *editor, top-of-screen:offset cursor-row:num <- get *editor, cursor-row:offset cursor-column:num <- get *editor, cursor-column:offset undo:&:list:&:operation <- get *editor, undo:offset { # if previous operation was an insert, coalesce this operation with it break-unless undo op:&:operation <- first undo typing:insert-operation, is-insert?:bool <- maybe-convert *op, typing:variant break-unless is-insert? previous-coalesce-tag:num <- get typing, tag:offset break-unless previous-coalesce-tag before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset insert-until:&:duplex-list:char <- next before-cursor typing <- put typing, insert-until:offset, insert-until typing <- put typing, after-row:offset, cursor-row typing <- put typing, after-column:offset, cursor-column typing <- put typing, after-top-of-screen:offset, top-after *op <- merge 0/insert-operation, typing break +done-adding-insert-operation } # if not, create a new operation insert-from:&:duplex-list:char <- next cursor-before insert-to:&:duplex-list:char <- next insert-from op:&:operation <- new operation:type *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 editor <- add-operation editor, op +done-adding-insert-operation ] # enter operations never coalesce with typing before or after after [ cursor-row-before:num <- copy cursor-row cursor-column-before:num <- copy cursor-column top-before:&:duplex-list:char <- get *editor, top-of-screen:offset cursor-before:&:duplex-list:char <- get *editor, before-cursor:offset ] before [ top-after:&:duplex-list:char <- get *editor, top-of-screen:offset cursor-row:num <- get *editor, cursor-row:offset cursor-column:num <- get *editor, cursor-row:offset # never coalesce insert-from:&:duplex-list:char <- next cursor-before before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset insert-to:&:duplex-list:char <- next before-cursor op:&:operation <- new operation:type *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 editor <- add-operation editor, op ] # Everytime you add a new operation to the undo stack, be sure to clear the # redo stack, because it's now obsolete. # Beware: since we're counting cursor moves as operations, this means just # moving the cursor can lose work on the undo stack. def add-operation editor:&:editor, op:&:operation -> editor:&:editor [ local-scope load-ingredients undo:&:list:&:operation <- get *editor, undo:offset undo <- push op undo *editor <- put *editor, undo:offset, undo redo:&:list:&:operation <- get *editor, redo:offset redo <- copy 0 *editor <- put *editor, redo:offset, redo return editor/same-as-ingredient:0 ] after [ { typing:insert-operation, is-insert?:bool <- maybe-convert *op, typing:variant break-unless is-insert? start:&:duplex-list:char <- get typing, insert-from:offset end:&:duplex-list:char <- get typing, insert-until:offset # assert cursor-row/cursor-column/top-of-screen match after-row/after-column/after-top-of-screen before-cursor:&:duplex-list:char <- prev start *editor <- put *editor, before-cursor:offset, before-cursor remove-between before-cursor, end cursor-row <- get typing, before-row:offset *editor <- put *editor, cursor-row:offset, cursor-row cursor-column <- get typing, before-column:offset *editor <- put *editor, cursor-column:offset, cursor-column top:&:duplex-list:char <- get typing, before-top-of-screen:offset *editor <- put *editor, top-of-screen:offset, top } ] scenario editor-can-undo-typing-multiple [ local-scope # create an editor and type multiple characters assume-screen 10/width, 5/height e:&:editor <- new-editor [], 0/left, 10/right editor-render screen, e assume-console [ type [012] ] editor-event-loop screen, console, e # undo assume-console [ press ctrl-z ] run [ editor-event-loop screen, console, e ] # all characters must be gone screen-should-contain [ . . . . .┈┈┈┈┈┈┈┈┈┈. . . ] ] scenario editor-can-undo-typing-multiple-2 [ local-scope # create an editor with some text assume-screen 10/width, 5/height e:&:editor <- new-editor [a], 0/left, 10/right editor-render screen, e # type some characters assume-console [ type [012] ] editor-event-loop screen, console, e screen-should-contain [ . . .012a . .┈┈┈┈┈┈┈┈┈┈. . . ] # undo assume-console [ press ctrl-z ] run [ editor-event-loop screen, console, e ] # back to original text screen-should-contain [ . . .a . .┈┈┈┈┈┈┈┈┈┈. . . ] # cursor should be in the right place assume-console [ type [3] ] run [ editor-event-loop screen, console, e ] screen-should-contain [ . . .3a . .┈┈┈┈┈┈┈┈┈┈. . . ] ] scenario editor-can-undo-typing-enter [ local-scope # create an editor with some text assume-screen 10/width, 5/height e:&:editor <- new-editor [ abc], 0/left, 10/right editor-render screen, e # new line assume-console [ left-click 1, 8 press enter ] editor-event-loop screen, console, e screen-should-contain [ . . . abc . . . .┈┈┈┈┈┈┈┈┈┈. . . ] # line is indented 3:num/raw <- get *e, cursor-row:offset 4:num/raw <- get *e, cursor-column:offset memory-should-contain [ 3 <- 2 4 <- 2 ] # undo assume-console [ press ctrl-z ] run [ editor-event-loop screen, console, e ] 3:num/raw <- get *e, cursor-row:offset 4:num/raw <- get *e, cursor-column:offset memory-should-contain [ 3 <- 1 4 <- 5 ] # back to original text screen-should-contain [ . . . abc . .┈┈┈┈┈┈┈┈┈┈. . . ] # cursor should be at end of line assume-console [ type [1] ] run [ editor-event-loop screen, console, e ] screen-should-contain [ . . . abc1 . .┈┈┈┈┈┈┈┈┈┈. . . ] ] # redo typing scenario editor-redo-typing [ local-scope # create an editor, type something, undo assume-screen 10/width, 5/height e:&:editor <- new-editor [a], 0/left, 10/right editor-render screen, e assume-console [ type [012] press ctrl-z ] editor-event-loop screen, console, e screen-should-contain [ . . .a . .┈┈┈┈┈┈┈┈┈┈. . . ] # redo assume-console [ press ctrl-y ] run [ editor-event-loop screen, console, e ] # all characters must be back screen-should-contain [ . . .012a . .┈┈┈┈┈┈┈┈┈┈. . . ] # cursor should be in the right place assume-console [ type [3] ] run [ editor-event-loop screen, console, e ] screen-should-contain [ . . .0123a . .┈┈┈┈┈┈┈┈┈┈. . . ] ] after [ { typing:insert-operation, is-insert?:bool <- maybe-convert *op, typing:variant break-unless is-insert? before-cursor <- get *editor, before-cursor:offset insert-from:&:duplex-list:char <- get typing, insert-from:offset # ignore insert-to because it's already been spliced away # assert insert-to matches next(before-cursor) insert-range before-cursor, insert-from # assert cursor-row/cursor-column/top-of-screen match after-row/after-column/after-top-of-screen cursor-row <- get typing, after-row:offset *editor <- put *editor, cursor-row:offset, cursor-row cursor-column <- get typing, after-column:offset *editor <- put *editor, cursor-column:offset, cursor-column top:&:duplex-list:char <- get typing, after-top-of-screen:offset *editor <- put *editor, top-of-screen:offset, top } ] scenario editor-redo-typing-empty [ local-scope # create an editor, type something, undo assume-screen 10/width, 5/height e:&:editor <- new-editor [], 0/left, 10/right editor-render screen, e assume-console [ type [012] press ctrl-z ] editor-event-loop screen, console, e screen-should-contain [ . . . . .┈┈┈┈┈┈┈┈┈┈. . . ] # redo assume-console [ press ctrl-y ] run [ editor-event-loop screen, console, e ] # all characters must be back screen-should-contain [ . . .012 . .┈┈┈┈┈┈┈┈┈┈. . . ] # cursor should be in the right place assume-console [ type [3] ] run [ editor-event-loop screen, console, e ] screen-should-contain [ . . .0123 . .┈┈┈┈┈┈┈┈┈┈. . . ] ] scenario editor-work-clears-redo-stack [ local-scope # create an editor with some text, do some work, undo assume-screen 10/width, 5/height contents:text <- new [abc def ghi] e:&:editor <- new-editor contents, 0/left, 10/right editor-render screen, e assume-console [ type [1] press ctrl-z ] editor-event-loop screen, console, e # do some more work assume-console [ type [0] ] editor-event-loop screen, console, e screen-should-contain [ . . .0abc . .def . .ghi . .┈┈┈┈┈┈┈┈┈┈. ] # redo assume-console [ press ctrl-y ] run [ editor-event-loop screen, console, e ] # nothing should happen screen-should-contain [ . . .0abc . .def . .ghi . .┈┈┈┈┈┈┈┈┈┈. ] ] scenario editor-can-redo-typing-and-enter-and-tab [ local-scope # create an editor assume-screen 10/width, 5/height e:&:editor <- new-editor [], 0/left, 10/right editor-render screen, e # insert some text and tabs, hit enter, some more text and tabs assume-console [ press tab type [ab] press tab type [cd] press enter press tab type [efg] ] editor-event-loop screen, console, e screen-should-contain [ . . . ab cd . . efg . .┈┈┈┈┈┈┈┈┈┈. . . ] 3:num/raw <- get *e, cursor-row:offset 4:num/raw <- get *e, cursor-column:offset memory-should-contain [ 3 <- 2 4 <- 7 ] # undo assume-console [ press ctrl-z ] run [ editor-event-loop screen, console, e ] # typing in second line deleted, but not indent 3:num/raw <- get *e, cursor-row:offset 4:num/raw <- get *e, cursor-column:offset memory-should-contain [ 3 <- 2 4 <- 2 ] screen-should-contain [ . . . ab cd . . . .┈┈┈┈┈┈┈┈┈┈. . . ] # undo again assume-console [ press ctrl-z ] run [ editor-event-loop screen, console, e ] # indent and newline deleted 3:num/raw <- get *e, cursor-row:offset 4:num/raw <- get *e, cursor-column:offset memory-should-contain [ 3 <- 1 4 <- 8 ] screen-should-contain [ . . . ab cd . .┈┈┈┈┈┈┈┈┈┈. . . ] # undo again assume-console [ press ctrl-z ] run [ editor-event-loop screen, console, e ] # empty screen 3:num/raw <- get *e, cursor-row:offset 4:num/raw <- get *e, cursor-column:offset memory-should-contain [ 3 <- 1 4 <- 0 ] screen-should-contain [ . . . . .┈┈┈┈┈┈┈┈┈┈. . . ] # redo assume-console [ press ctrl-y ] run [ editor-event-loop screen, console, e ] # first line inserted 3:num/raw <- get *e, cursor-row:offset 4:num/raw <- get *e, cursor-column:offset memory-should-contain [ 3 <- 1 4 <- 8 ] screen-should-contain [ . . . ab cd . .┈┈┈┈┈┈┈┈┈┈. . . ] # redo again assume-console [ press ctrl-y ] run [ editor-event-loop screen, console, e ] # newline and indent inserted 3:num/raw <- get *e, cursor-row:offset 4:num/raw <- get *e, cursor-column:offset memory-should-contain [ 3 <- 2 4 <- 2 ] screen-should-contain [ . . . ab cd . . . .┈┈┈┈┈┈┈┈┈┈. . . ] # redo again assume-console [ press ctrl-y ] run [ editor-event-loop screen, console, e ] # indent and newline deleted 3:num/raw <- get *e, cursor-row:offset 4:num/raw <- get *e, cursor-column:offset memory-should-contain [ 3 <- 2 4 <- 7 ] screen-should-contain [ . . . ab cd . . efg . .┈┈┈┈┈┈┈┈┈┈. . . ] ] # undo cursor movement and scroll scenario editor-can-undo-touch [ local-scope # create an editor with some text assume-screen 10/width, 5/height contents:text <- new [abc def ghi] e:&:editor <- new-editor contents, 0/left, 10/right editor-render screen, e # move the cursor assume-console [ left-click 3, 1 ] editor-event-loop screen, console, e # undo assume-console [ press ctrl-z ] run [ editor-event-loop screen, console, e ] # click undone 3:num/raw <- get *e, cursor-row:offset 4:num/raw <- get *e, cursor-column:offset memory-should-contain [ 3 <- 1 4 <- 0 ] # cursor should be in the right place assume-console [ type [1] ] run [ editor-event-loop screen, console, e ] screen-should-contain [ . . .1abc . .def . .ghi . .┈┈┈┈┈┈┈┈┈┈. ] ] after [ cursor-row-before:num <- get *editor, cursor-row:offset cursor-column-before:num <- get *editor, cursor-column:offset top-before:&:duplex-list:char <- get *editor, top-of-screen:offset ] before [ top-after:&:duplex-list:char <- get *editor, top-of-screen:offset cursor-row:num <- get *editor, cursor-row:offset cursor-column:num <- get *editor, cursor-column:offset { break-unless undo-coalesce-tag # if previous operation was also a move, and also had the same coalesce # tag, coalesce with it undo:&:list:&:operation <- get *editor, undo:offset break-unless undo op:&:operation <- first undo move:move-operation, is-move?:bool <- maybe-convert *op, move:variant break-unless is-move? previous-coalesce-tag:num <- get move, tag:offset coalesce?:bool <- equal undo-coalesce-tag, previous-coalesce-tag break-unless coalesce? move <- put move, after-row:offset, cursor-row move <- put move, after-column:offset, cursor-column move <- put move, after-top-of-screen:offset, top-after *op <- merge 1/move-operation, move break +done-adding-move-operation } op:&:operation <- new operation:type *op <- merge 1/move-operation, cursor-row-before, cursor-column-before, top-before, cursor-row/after, cursor-column/after, top-after, undo-coalesce-tag editor <- add-operation editor, op +done-adding-move-operation ] after [ { move:move-operation, is-move?:bool <- maybe-convert *op, move:variant break-unless is-move? # assert cursor-row/cursor-column/top-of-screen match after-row/after-column/after-top-of-screen cursor-row <- get move, before-row:offset *editor <- put *editor, cursor-row:offset, cursor-row cursor-column <- get move, before-column:offset *editor <- put *editor, cursor-column:offset, cursor-column top:&:duplex-list:char <- get move, before-top-of-screen:offset *editor <- put *editor, top-of-screen:offset, top } ] scenario editor-can-undo-scroll [ local-scope # screen has 1 line for menu + 3 lines assume-screen 5/width, 4/height # editor contains a wrapped line contents:text <- new [a b cdefgh] e:&:editor <- new-editor contents, 0/left, 5/right # position cursor at end of screen and try to move right assume-console [ left-click 3, 3 press right-arrow ] editor-event-loop screen, console, e 3:num/raw <- get *e, cursor-row:offset 4:num/raw <- get *e, cursor-column:offset # screen scrolls screen-should-contain [ . . .b . .cdef↩. .gh . ] memory-should-contain [ 3 <- 3 4 <- 0 ] # undo assume-console [ press ctrl-z ] run [ editor-event-loop screen, console, e ] # cursor moved back 3:num/raw <- get *e, cursor-row:offset 4:num/raw <- get *e, cursor-column:offset memory-should-contain [ 3 <- 3 4 <- 3 ] # scroll undone screen-should-contain [ . . .a . .b . .cdef↩. ] # cursor should be in the right place assume-console [ type [1] ] run [ editor-event-loop screen, console, e ] screen-should-contain [ . . .b . .cde1↩. .fgh . ] ] scenario editor-can-undo-left-arrow [ local-scope # create an editor with some text assume-screen 10/width, 5/height contents:text <- new [abc def ghi] e:&:editor <- new-editor contents, 0/left, 10/right editor-render screen, e # move the cursor assume-console [ left-click 3, 1 press left-arrow ] editor-event-loop screen, console, e # undo assume-console [ press ctrl-z ] run [ editor-event-loop screen, console, e ] # cursor moves back 3:num/raw <- get *e, cursor-row:offset 4:num/raw <- get *e, cursor-column:offset memory-should-contain [ 3 <- 3 4 <- 1 ] # cursor should be in the right place assume-console [ type [1] ] run [ editor-event-loop screen, console, e ] screen-should-contain [ . . .abc . .def . .g1hi . .┈┈┈┈┈┈┈┈┈┈. ] ] scenario editor-can-undo-up-arrow [ local-scope # create an editor with some text assume-screen 10/width, 5/height contents:text <- new [abc def ghi] e:&:editor <- new-editor contents, 0/left, 10/right editor-render screen, e # move the cursor assume-console [ left-click 3, 1 press up-arrow ] editor-event-loop screen, console, e 3:num/raw <- get *e, cursor-row:offset 4:num/raw <- get *e, cursor-column:offset memory-should-contain [ 3 <- 2 4 <- 1 ] # undo assume-console [ press ctrl-z ] run [ editor-event-loop screen, console, e ] # cursor moves back 3:num/raw <- get *e, cursor-row:offset 4:num/raw <- get *e, cursor-column:offset memory-should-contain [ 3 <- 3 4 <- 1 ] # cursor should be in the right place assume-console [ type [1] ] run [ editor-event-loop screen, console, e ] screen-should-contain [ . . .abc . .def . .g1hi . .┈┈┈┈┈┈┈┈┈┈. ] ] scenario editor-can-undo-down-arrow [ local-scope # create an editor with some text assume-screen 10/width, 5/height contents:text <- new [abc def ghi] e:&:editor <- new-editor contents, 0/left, 10/right editor-render screen, e # move the cursor assume-console [ left-click 2, 1 press down-arrow ] editor-event-loop screen, console, e # undo assume-console [ press ctrl-z ] run [ editor-event-loop screen, console, e ] # cursor moves back 3:num/raw <- get *e, cursor-row:offset 4:num/raw <- get *e, cursor-column:offset memory-should-contain [ 3 <- 2 4 <- 1 ] # cursor should be in the right place assume-console [ type [1] ] run [ editor-event-loop screen, console, e ] screen-should-contain [ . . .abc . .d1ef . .ghi . .┈┈┈┈┈┈┈┈┈┈. ] ] scenario editor-can-undo-ctrl-f [ local-scope # create an editor with multiple pages of text assume-screen 10/width, 5/height contents:text <- new [a b c d e f] e:&:editor <- new-editor contents, 0/left, 10/right editor-render screen, e # scroll the page assume-console [ press ctrl-f ] editor-event-loop screen, console, e # undo assume-console [ press ctrl-z ] run [ editor-event-loop screen, console, e ] # screen should again show page 1 screen-should-contain [ . . .a . .b . .c . .d . ] ] scenario editor-can-undo-page-down [ local-scope # create an editor with multiple pages of text assume-screen 10/width, 5/height contents:text <- new [a b c d e f] e:&:editor <- new-editor contents, 0/left, 10/right editor-render screen, e # scroll the page assume-console [ press page-down ] editor-event-loop screen, console, e # undo assume-console [ press ctrl-z ] run [ editor-event-loop screen, console, e ] # screen should again show page 1 screen-should-contain [ . . .a . .b . .c . .d . ] ] scenario editor-can-undo-ctrl-b [ local-scope # create an editor with multiple pages of text assume-screen 10/width, 5/height contents:text <- new [a b c d e f] e:&:editor <- new-editor contents, 0/left, 10/right editor-render screen, e # scroll the page down and up assume-console [ press page-down press ctrl-b ] editor-event-loop screen, console, e # undo assume-console [ press ctrl-z ] run [ editor-event-loop screen, console, e ] # screen should again show page 2 screen-should-contain [ . . .d . .e . .f . .┈┈┈┈┈┈┈┈┈┈. ] ] scenario editor-can-undo-page-up [ local-scope # create an editor with multiple pages of text assume-screen 10/width, 5/height contents:text <- new [a b c d e f] e:&:editor <- new-editor contents, 0/left, 10/right editor-render screen, e # scroll the page down and up assume-console [ press page-down press page-up ] editor-event-loop screen, console, e # undo assume-console [ press ctrl-z ] run [ editor-event-loop screen, console, e ] # screen should again show page 2 screen-should-contain [ . . .d . .e . .f . .┈┈┈┈┈┈┈┈┈┈. ] ] scenario editor-can-undo-ctrl-a [ local-scope # create an editor with some text assume-screen 10/width, 5/height contents:text <- new [abc def ghi] e:&:editor <- new-editor contents, 0/left, 10/right editor-render screen, e # move the cursor, then to start of line assume-console [ left-click 2, 1 press ctrl-a ] editor-event-loop screen, console, e # undo assume-console [ press ctrl-z ] run [ editor-event-loop screen, console, e ] # cursor moves back 3:num/raw <- get *e, cursor-row:offset 4:num/raw <- get *e, cursor-column:offset memory-should-contain [ 3 <- 2 4 <- 1 ] # cursor should be in the right place assume-console [ type [1] ] run [ editor-event-loop screen, console, e ] screen-should-contain [ . . .abc . .d1ef . .ghi . .┈┈┈┈┈┈┈┈┈┈. ] ] scenario editor-can-undo-home [ local-scope # create an editor with some text assume-screen 10/width, 5/height contents:text <- new [abc def ghi] e:&:editor <- new-editor contents, 0/left, 10/right editor-render screen, e # move the cursor, then to start of line assume-console [ left-click 2, 1 press home ] editor-event-loop screen, console, e # undo assume-console [ press ctrl-z ] run [ editor-event-loop screen, console, e ] # cursor moves back 3:num/raw <- get *e, cursor-row:offset 4:num/raw <- get *e, cursor-column:offset memory-should-contain [ 3 <- 2 4 <- 1 ] # cursor should be in the right place assume-console [ type [1] ] run [ editor-event-loop screen, console, e ] screen-should-contain [ . . .abc . .d1ef . .ghi . .┈┈┈┈┈┈┈┈┈┈. ] ] scenario editor-can-undo-ctrl-e [ local-scope # create an editor with some text assume-screen 10/width, 5/height contents:text <- new [abc def ghi] e:&:editor <- new-editor contents, 0/left, 10/right editor-render screen, e # move the cursor, then to start of line assume-console [ left-click 2, 1 press ctrl-e ] editor-event-loop screen, console, e # undo assume-console [ press ctrl-z ] run [ editor-event-loop screen, console, e ] # cursor moves back 3:num/raw <- get *e, cursor-row:offset 4:num/raw <- get *e, cursor-column:offset memory-should-contain [ 3 <- 2 4 <- 1 ] # cursor should be in the right place assume-console [ type [1] ] run [ editor-event-loop screen, console, e ] screen-should-contain [ . . .abc . .d1ef . .ghi . .┈┈┈┈┈┈┈┈┈┈. ] ] scenario editor-can-undo-end [ local-scope # create an editor with some text assume-screen 10/width, 5/height contents:text <- new [abc def ghi] e:&:editor <- new-editor contents, 0/left, 10/right editor-render screen, e # move the cursor, then to start of line assume-console [ left-click 2, 1 press end ] editor-event-loop screen, console, e # undo assume-console [ press ctrl-z ] run [ editor-event-loop screen, console, e ] # cursor moves back 3:num/raw <- get *e, cursor-row:offset 4:num/raw <- get *e, cursor-column:offset memory-should-contain [ 3 <- 2 4 <- 1 ] # cursor should be in the right place assume-console [ type [1] ] run [ editor-event-loop screen, console, e ] screen-should-contain [ . . .abc . .d1ef . .ghi . .┈┈┈┈┈┈┈┈┈┈. ] ] scenario editor-can-undo-multiple-arrows-in-the-same-direction [ local-scope # create an editor with some text assume-screen 10/width, 5/height contents:text <- new [abc def ghi] e:&:editor <- new-editor contents, 0/left, 10/right editor-render screen, e # move the cursor assume-console [ left-click 2, 1 press right-arrow press right-arrow press up-arrow ] editor-event-loop screen, console, e 3:num/raw <- get *e, cursor-row:offset 4:num/raw <- get *e, cursor-column:offset memory-should-contain [ 3 <- 1 4 <- 3 ] # undo assume-console [ press ctrl-z ] run [ editor-event-loop screen, console, e ] # up-arrow is undone 3:num/raw <- get *e, cursor-row:offset 4:num/raw <- get *e, cursor-column:offset memory-should-contain [ 3 <- 2 4 <- 3 ] # undo again assume-console [ press ctrl-z ] run [ editor-event-loop screen, console, e ] # both right-arrows are undone 3:num/raw <- get *e, cursor-row:offset 4:num/raw <- get *e, cursor-column:offset memory-should-contain [ 3 <- 2 4 <- 1 ] ] # redo cursor movement and scroll scenario editor-redo-touch [ local-scope # create an editor with some text, click on a character, undo assume-screen 10/width, 5/height contents:text <- new [abc def ghi] e:&:editor <- new-editor contents, 0/left, 10/right editor-render screen, e assume-console [ left-click 3, 1 press ctrl-z ] editor-event-loop screen, console, e # redo assume-console [ press ctrl-y ] run [ editor-event-loop screen, console, e ] # cursor moves to left-click 3:num/raw <- get *e, cursor-row:offset 4:num/raw <- get *e, cursor-column:offset memory-should-contain [ 3 <- 3 4 <- 1 ] # cursor should be in the right place assume-console [ type [1] ] run [ editor-event-loop screen, console, e ] screen-should-contain [ . . .abc . .def . .g1hi . .┈┈┈┈┈┈┈┈┈┈. ] ] after [ { move:move-operation, is-move?:bool <- maybe-convert *op, move:variant break-unless is-move? # assert cursor-row/cursor-column/top-of-screen match after-row/after-column/after-top-of-screen cursor-row <- get move, after-row:offset *editor <- put *editor, cursor-row:offset, cursor-row cursor-column <- get move, after-column:offset *editor <- put *editor, cursor-column:offset, cursor-column top:&:duplex-list:char <- get move, after-top-of-screen:offset *editor <- put *editor, top-of-screen:offset, top } ] scenario editor-separates-undo-insert-from-undo-cursor-move [ local-scope # create an editor, type some text, move the cursor, type some more text assume-screen 10/width, 5/height e:&:editor <- new-editor [], 0/left, 10/right editor-render screen, e assume-console [ type [abc] left-click 1, 1 type [d] ] editor-event-loop screen, console, e 3:num/raw <- get *e, cursor-row:offset 4:num/raw <- get *e, cursor-column:offset screen-should-contain [ . . .adbc . .┈┈┈┈┈┈┈┈┈┈. . . ] memory-should-contain [ 3 <- 1 4 <- 2 ] # undo assume-console [ press ctrl-z ] run [ editor-event-loop screen, console, e 3:num/raw <- get *e, cursor-row:offset 4:num/raw <- get *e, cursor-column:offset ] # last letter typed is deleted screen-should-contain [ . . .abc . .┈┈┈┈┈┈┈┈┈┈. . . ] memory-should-contain [ 3 <- 1 4 <- 1 ] # undo again assume-console [ press ctrl-z ] run [ editor-event-loop screen, console, e 3:num/raw <- get *e, cursor-row:offset 4:num/raw <- get *e, cursor-column:offset ] # no change to screen; cursor moves screen-should-contain [ . . .abc . .┈┈┈┈┈┈┈┈┈┈. . . ] memory-should-contain [ 3 <- 1 4 <- 3 ] # undo again assume-console [ press ctrl-z ] run [ editor-event-loop screen, console, e 3:num/raw <- get *e, cursor-row:offset 4:num/raw <- get *e, cursor-column:offset ] # screen empty screen-should-contain [ . . . . .┈┈┈┈┈┈┈┈┈┈. . . ] memory-should-contain [ 3 <- 1 4 <- 0 ] # redo assume-console [ press ctrl-y ] run [ editor-event-loop screen, console, e 3:num/raw <- get *e, cursor-row:offset 4:num/raw <- get *e, cursor-column:offset ] # first insert screen-should-contain [ . . .abc . .┈┈┈┈┈┈┈┈┈┈. . . ] memory-should-contain [ 3 <- 1 4 <- 3 ] # redo again assume-console [ press ctrl-y ] run [ editor-event-loop screen, console, e 3:num/raw <- get *e, cursor-row:offset 4:num/raw <- get *e, cursor-column:offset ] # cursor moves screen-should-contain [ . . .abc . .┈┈┈┈┈┈┈┈┈┈. . . ] # cursor moves memory-should-contain [ 3 <- 1 4 <- 1 ] # redo again assume-console [ press ctrl-y ] run [ editor-event-loop screen, console, e 3:num/raw <- get *e, cursor-row:offset 4:num/raw <- get *e, cursor-column:offset ] # second insert screen-should-contain [ . . .adbc . .┈┈┈┈┈┈┈┈┈┈. . . ] memory-should-contain [ 3 <- 1 4 <- 2 ] ] # undo backspace scenario editor-can-undo-and-redo-backspace [ local-scope # create an editor assume-screen 10/width, 5/height e:&:editor <- new-editor [], 0/left, 10/right editor-render screen, e # insert some text and hit backspace assume-console [ type [abc] press backspace press backspace ] editor-event-loop screen, console, e screen-should-contain [ . . .a . .┈┈┈┈┈┈┈┈┈┈. . . ] 3:num/raw <- get *e, cursor-row:offset 4:num/raw <- get *e, cursor-column:offset memory-should-contain [ 3 <- 1 4 <- 1 ] # undo assume-console [ press ctrl-z ] run [ editor-event-loop screen, console, e ] 3:num/raw <- get *e, cursor-row:offset 4:num/raw <- get *e, cursor-column:offset memory-should-contain [ 3 <- 1 4 <- 3 ] screen-should-contain [ . . .abc . .┈┈┈┈┈┈┈┈┈┈. . . ] # redo assume-console [ press ctrl-y ] run [ editor-event-loop screen, console, e ] 3:num/raw <- get *e, cursor-row:offset 4:num/raw <- get *e, cursor-column:offset memory-should-contain [ 3 <- 1 4 <- 1 ] screen-should-contain [ . . .a . .┈┈┈┈┈┈┈┈┈┈. . . ] ] # save operation to undo after [ top-before:&:duplex-list:char <- get *editor, top-of-screen:offset ] before [ { break-unless backspaced-cell # backspace failed; don't add an undo operation top-after:&:duplex-list:char <- get *editor, top-of-screen:offset cursor-row:num <- get *editor, cursor-row:offset cursor-column:num <- get *editor, cursor-row:offset before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset undo:&:list:&:operation <- get *editor, undo:offset { # if previous operation was an insert, coalesce this operation with it break-unless undo op:&:operation <- first undo deletion:delete-operation, is-delete?:bool <- maybe-convert *op, delete:variant break-unless is-delete? previous-coalesce-tag:num <- get deletion, tag:offset coalesce?:bool <- equal previous-coalesce-tag, 1/coalesce-backspace break-unless coalesce? deletion <- put deletion, delete-from:offset, before-cursor backspaced-so-far:&:duplex-list:char <- get deletion, deleted-text:offset insert-range backspaced-cell, backspaced-so-far deletion <- put deletion, deleted-text:offset, backspaced-cell deletion <- put deletion, after-row:offset, cursor-row deletion <- put deletion, after-column:offset, cursor-column deletion <- put deletion, after-top-of-screen:offset, top-after *op <- merge 2/delete-operation, deletion break +done-adding-backspace-operation } # if not, create a new operation op:&:operation <- new operation:type deleted-until:&:duplex-list:char <- next before-cursor *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 editor <- add-operation editor, op +done-adding-backspace-operation } ] after [ { deletion:delete-operation, is-delete?:bool <- maybe-convert *op, delete:variant break-unless is-delete? anchor:&:duplex-list:char <- get deletion, delete-from:offset break-unless anchor deleted:&:duplex-list:char <- get deletion, deleted-text:offset old-cursor:&:duplex-list:char <- last deleted insert-range anchor, deleted # assert cursor-row/cursor-column/top-of-screen match after-row/after-column/after-top-of-screen before-cursor <- copy old-cursor cursor-row <- get deletion, before-row:offset *editor <- put *editor, cursor-row:offset, cursor-row cursor-column <- get deletion, before-column:offset *editor <- put *editor, cursor-column:offset, cursor-column top:&:duplex-list:char <- get deletion, before-top-of-screen:offset *editor <- put *editor, top-of-screen:offset, top } ] after [ { deletion:delete-operation, is-delete?:bool <- maybe-convert *op, delete:variant break-unless is-delete? start:&:duplex-list:char <- get deletion, delete-from:offset end:&:duplex-list:char <- get deletion, delete-until:offset data:&:duplex-list:char <- get *editor, data:offset remove-between start, end # assert cursor-row/cursor-column/top-of-screen match after-row/after-column/after-top-of-screen cursor-row <- get deletion, after-row:offset *editor <- put *editor, cursor-row:offset, cursor-row cursor-column <- get deletion, after-column:offset *editor <- put *editor, cursor-column:offset, cursor-column top:&:duplex-list:char <- get deletion, before-top-of-screen:offset *editor <- put *editor, top-of-screen:offset, top } ] # undo delete scenario editor-can-undo-and-redo-delete [ local-scope # create an editor assume-screen 10/width, 5/height e:&:editor <- new-editor [], 0/left, 10/right editor-render screen, e # insert some text and hit delete and backspace a few times assume-console [ type [abcdef] left-click 1, 2 press delete press backspace press delete press delete ] editor-event-loop screen, console, e screen-should-contain [ . . .af . .┈┈┈┈┈┈┈┈┈┈. . . ] 3:num/raw <- get *e, cursor-row:offset 4:num/raw <- get *e, cursor-column:offset memory-should-contain [ 3 <- 1 4 <- 1 ] # undo deletes assume-console [ press ctrl-z ] run [ editor-event-loop screen, console, e ] 3:num/raw <- get *e, cursor-row:offset 4:num/raw <- get *e, cursor-column:offset memory-should-contain [ 3 <- 1 4 <- 1 ] screen-should-contain [ . . .adef . .┈┈┈┈┈┈┈┈┈┈. . . ] # undo backspace assume-console [ press ctrl-z ] run [ editor-event-loop screen, console, e ] 3:num/raw <- get *e, cursor-row:offset 4:num/raw <- get *e, cursor-column:offset memory-should-contain [ 3 <- 1 4 <- 2 ] screen-should-contain [ . . .abdef . .┈┈┈┈┈┈┈┈┈┈. . . ] # undo first delete assume-console [ press ctrl-z ] run [ editor-event-loop screen, console, e ] 3:num/raw <- get *e, cursor-row:offset 4:num/raw <- get *e, cursor-column:offset memory-should-contain [ 3 <- 1 4 <- 2 ] screen-should-contain [ . . .abcdef . .┈┈┈┈┈┈┈┈┈┈. . . ] # redo first delete assume-console [ press ctrl-y ] run [ editor-event-loop screen, console, e ] # first line inserted 3:num/raw <- get *e, cursor-row:offset 4:num/raw <- get *e, cursor-column:offset memory-should-contain [ 3 <- 1 4 <- 2 ] screen-should-contain [ . . .abdef . .┈┈┈┈┈┈┈┈┈┈. . . ] # redo backspace assume-console [ press ctrl-y ] run [ editor-event-loop screen, console, e ] # first line inserted 3:num/raw <- get *e, cursor-row:offset 4:num/raw <- get *e, cursor-column:offset memory-should-contain [ 3 <- 1 4 <- 1 ] screen-should-contain [ . . .adef . .┈┈┈┈┈┈┈┈┈┈. . . ] # redo deletes assume-console [ press ctrl-y ] run [ editor-event-loop screen, console, e ] # first line inserted 3:num/raw <- get *e, cursor-row:offset 4:num/raw <- get *e, cursor-column:offset memory-should-contain [ 3 <- 1 4 <- 1 ] screen-should-contain [ . . .af . .┈┈┈┈┈┈┈┈┈┈. . . ] ] after [ top-before:&:duplex-list:char <- get *editor, top-of-screen:offset ] before [ { break-unless deleted-cell # delete failed; don't add an undo operation top-after:&:duplex-list:char <- get *editor, top-of-screen:offset cursor-row:num <- get *editor, cursor-row:offset cursor-column:num <- get *editor, cursor-column:offset before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset undo:&:list:&:operation <- get *editor, undo:offset { # if previous operation was an insert, coalesce this operation with it break-unless undo op:&:operation <- first undo deletion:delete-operation, is-delete?:bool <- maybe-convert *op, delete:variant break-unless is-delete? previous-coalesce-tag:num <- get deletion, tag:offset coalesce?:bool <- equal previous-coalesce-tag, 2/coalesce-delete break-unless coalesce? delete-until:&:duplex-list:char <- next before-cursor deletion <- put deletion, delete-until:offset, delete-until deleted-so-far:&:duplex-list:char <- get deletion, deleted-text:offset deleted-so-far <- append deleted-so-far, deleted-cell deletion <- put deletion, deleted-text:offset, deleted-so-far deletion <- put deletion, after-row:offset, cursor-row deletion <- put deletion, after-column:offset, cursor-column deletion <- put deletion, after-top-of-screen:offset, top-after *op <- merge 2/delete-operation, deletion break +done-adding-delete-operation } # if not, create a new operation op:&:operation <- new operation:type deleted-until:&:duplex-list:char <- next before-cursor *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 editor <- add-operation editor, op +done-adding-delete-operation } ] # undo ctrl-k scenario editor-can-undo-and-redo-ctrl-k [ local-scope # create an editor assume-screen 10/width, 5/height contents:text <- new [abc def] e:&:editor <- new-editor contents, 0/left, 10/right editor-render screen, e # insert some text and hit delete and backspace a few times assume-console [ left-click 1, 1 press ctrl-k ] editor-event-loop screen, console, e screen-should-contain [ . . .a . .def . .┈┈┈┈┈┈┈┈┈┈. . . ] 3:num/raw <- get *e, cursor-row:offset 4:num/raw <- get *e, cursor-column:offset memory-should-contain [ 3 <- 1 4 <- 1 ] # undo assume-console [ press ctrl-z ] run [ editor-event-loop screen, console, e ] screen-should-contain [ . . .abc . .def . .┈┈┈┈┈┈┈┈┈┈. . . ] 3:num/raw <- get *e, cursor-row:offset 4:num/raw <- get *e, cursor-column:offset memory-should-contain [ 3 <- 1 4 <- 1 ] # redo assume-console [ press ctrl-y ] run [ editor-event-loop screen, console, e ] # first line inserted screen-should-contain [ . . .a . .def . .┈┈┈┈┈┈┈┈┈┈. . . ] 3:num/raw <- get *e, cursor-row:offset 4:num/raw <- get *e, cursor-column:offset memory-should-contain [ 3 <- 1 4 <- 1 ] # cursor should be in the right place assume-console [ type [1] ] run [ editor-event-loop screen, console, e ] screen-should-contain [ . . .a1 . .def . .┈┈┈┈┈┈┈┈┈┈. . . ] ] after [ top-before:&:duplex-list:char <- get *editor, top-of-screen:offset ] before [ { break-unless deleted-cells # delete failed; don't add an undo operation top-after:&:duplex-list:char <- get *editor, top-of-screen:offset cursor-row:num <- get *editor, cursor-row:offset cursor-column:num <- get *editor, cursor-column:offset deleted-until:&:duplex-list:char <- next before-cursor op:&:operation <- new operation:type *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 editor <- add-operation editor, op +done-adding-delete-operation } ] # undo ctrl-u scenario editor-can-undo-and-redo-ctrl-u [ local-scope # create an editor assume-screen 10/width, 5/height contents:text <- new [abc def] e:&:editor <- new-editor contents, 0/left, 10/right editor-render screen, e # insert some text and hit delete and backspace a few times assume-console [ left-click 1, 2 press ctrl-u ] editor-event-loop screen, console, e screen-should-contain [ . . .c . .def . .┈┈┈┈┈┈┈┈┈┈. . . ] 3:num/raw <- get *e, cursor-row:offset 4:num/raw <- get *e, cursor-column:offset memory-should-contain [ 3 <- 1 4 <- 0 ] # undo assume-console [ press ctrl-z ] run [ editor-event-loop screen, console, e ] screen-should-contain [ . . .abc . .def . .┈┈┈┈┈┈┈┈┈┈. . . ] 3:num/raw <- get *e, cursor-row:offset 4:num/raw <- get *e, cursor-column:offset memory-should-contain [ 3 <- 1 4 <- 2 ] # redo assume-console [ press ctrl-y ] run [ editor-event-loop screen, console, e ] # first line inserted screen-should-contain [ . . .c . .def . .┈┈┈┈┈┈┈┈┈┈. . . ] 3:num/raw <- get *e, cursor-row:offset 4:num/raw <- get *e, cursor-column:offset memory-should-contain [ 3 <- 1 4 <- 0 ] # cursor should be in the right place assume-console [ type [1] ] run [ editor-event-loop screen, console, e ] screen-should-contain [ . . .1c . .def . .┈┈┈┈┈┈┈┈┈┈. . . ] ] after [ top-before:&:duplex-list:char <- get *editor, top-of-screen:offset ] before [ { break-unless deleted-cells # delete failed; don't add an undo operation top-after:&:duplex-list:char <- get *editor, top-of-screen:offset op:&:operation <- new operation:type before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset deleted-until:&:duplex-list:char <- next before-cursor cursor-row:num <- get *editor, cursor-row:offset cursor-column:num <- get *editor, cursor-column:offset *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 editor <- add-operation editor, op +done-adding-delete-operation } ] scenario editor-can-undo-and-redo-ctrl-u-2 [ local-scope # create an editor assume-screen 10/width, 5/height e:&:editor <- new-editor [], 0/left, 10/right editor-render screen, e # insert some text and hit delete and backspace a few times assume-console [ type [abc] press ctrl-u press ctrl-z ] editor-event-loop screen, console, e screen-should-contain [ . . .abc . .┈┈┈┈┈┈┈┈┈┈. . . ] ]