diff options
Diffstat (limited to 'archive/2.vm/sandbox/003-shortcuts.mu')
-rw-r--r-- | archive/2.vm/sandbox/003-shortcuts.mu | 2800 |
1 files changed, 2800 insertions, 0 deletions
diff --git a/archive/2.vm/sandbox/003-shortcuts.mu b/archive/2.vm/sandbox/003-shortcuts.mu new file mode 100644 index 00000000..c9e66d5b --- /dev/null +++ b/archive/2.vm/sandbox/003-shortcuts.mu @@ -0,0 +1,2800 @@ +## special shortcuts for manipulating the editor +# Some keys on the keyboard generate unicode characters, others generate +# terminfo key codes. We need to modify different places in the two cases. + +# tab - insert two spaces + +scenario editor-inserts-two-spaces-on-tab [ + local-scope + assume-screen 10/width, 5/height + s:text <- new [ab +cd] + e:&:editor <- new-editor s, 0/left, 5/right + editor-render screen, e + $clear-trace + assume-console [ + press tab + ] + run [ + editor-event-loop screen, console, e + ] + screen-should-contain [ + . . + . ab . + .cd . + ] + # we render at most two editor rows worth (one row for each space) + check-trace-count-for-label-lesser-than 10, [print-character] +] + +scenario editor-inserts-two-spaces-and-wraps-line-on-tab [ + local-scope + assume-screen 10/width, 5/height + e:&:editor <- new-editor [abcd], 0/left, 5/right + editor-render screen, e + $clear-trace + assume-console [ + press tab + ] + run [ + editor-event-loop screen, console, e + ] + screen-should-contain [ + . . + . ab↩ . + .cd . + ] + # we re-render the whole editor + check-trace-count-for-label-greater-than 10, [print-character] +] + +after <handle-special-character> [ + { + tab?:bool <- equal c, 9/tab + break-unless tab? + <begin-insert-character> + # todo: decompose insert-at-cursor into editor update and screen update, + # so that 'tab' doesn't render the current line multiple times + insert-at-cursor editor, 32/space, screen + go-render? <- insert-at-cursor editor, 32/space, screen + <end-insert-character> + return + } +] + +# backspace - delete character before cursor + +scenario editor-handles-backspace-key [ + local-scope + assume-screen 10/width, 5/height + e:&:editor <- new-editor [abc], 0/left, 10/right + editor-render screen, e + $clear-trace + assume-console [ + left-click 1, 1 + press backspace + ] + run [ + editor-event-loop screen, console, e + 4:num/raw <- get *e, cursor-row:offset + 5:num/raw <- get *e, cursor-column:offset + ] + screen-should-contain [ + . . + .bc . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] + memory-should-contain [ + 4 <- 1 + 5 <- 0 + ] + check-trace-count-for-label 3, [print-character] # length of original line to overwrite +] + +after <handle-special-character> [ + { + delete-previous-character?:bool <- equal c, 8/backspace + break-unless delete-previous-character? + <begin-backspace-character> + go-render?:bool, backspaced-cell:&:duplex-list:char <- delete-before-cursor editor, screen + <end-backspace-character> + return + } +] + +# return values: +# go-render? - whether caller needs to update the screen +# backspaced-cell - value deleted (or 0 if nothing was deleted) so we can save it for undo, etc. +def delete-before-cursor editor:&:editor, screen:&:screen -> go-render?:bool, backspaced-cell:&:duplex-list:char, editor:&:editor, screen:&:screen [ + local-scope + load-inputs + before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset + data:&:duplex-list:char <- get *editor, data:offset + # if at start of text (before-cursor at § sentinel), return + prev:&:duplex-list:char <- prev before-cursor + return-unless prev, false/no-more-render, null/nothing-deleted + trace 10, [app], [delete-before-cursor] + original-row:num <- get *editor, cursor-row:offset + move-cursor-coordinates-left editor + backspaced-cell:&:duplex-list:char <- copy before-cursor + data <- remove before-cursor, data # will also neatly trim next/prev pointers in backspaced-cell/before-cursor + before-cursor <- copy prev + *editor <- put *editor, before-cursor:offset, before-cursor + screen-width:num <- screen-width screen + cursor-row:num <- get *editor, cursor-row:offset + cursor-column:num <- get *editor, cursor-column:offset + # did we just backspace over a newline? + same-row?:bool <- equal cursor-row, original-row + return-unless same-row?, true/go-render + left:num <- get *editor, left:offset + right:num <- get *editor, right:offset + curr:&:duplex-list:char <- next before-cursor + screen <- move-cursor screen, cursor-row, cursor-column + curr-column:num <- copy cursor-column + { + # hit right margin? give up and let caller render + at-right?:bool <- greater-or-equal curr-column, right + return-if at-right?, true/go-render + break-unless curr + # newline? done. + currc:char <- get *curr, value:offset + at-newline?:bool <- equal currc, 10/newline + break-if at-newline? + screen <- print screen, currc + curr-column <- add curr-column, 1 + curr <- next curr + loop + } + # we're guaranteed not to be at the right margin + space:char <- copy 32/space + screen <- print screen, space + go-render? <- copy false +] + +def move-cursor-coordinates-left editor:&:editor -> editor:&:editor [ + local-scope + load-inputs + before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset + cursor-row:num <- get *editor, cursor-row:offset + cursor-column:num <- get *editor, cursor-column:offset + left:num <- get *editor, left:offset + # if not at left margin, move one character left + { + at-left-margin?:bool <- equal cursor-column, left + break-if at-left-margin? + trace 10, [app], [decrementing cursor column] + cursor-column <- subtract cursor-column, 1 + *editor <- put *editor, cursor-column:offset, cursor-column + return + } + # if at left margin, we must move to previous row: + top-of-screen?:bool <- equal cursor-row, 1 # exclude menu bar + { + break-if top-of-screen? + cursor-row <- subtract cursor-row, 1 + *editor <- put *editor, cursor-row:offset, cursor-row + } + { + break-unless top-of-screen? + # no scroll, so do nothing + } + { + # case 1: if previous character was newline, figure out how long the previous line is + previous-character:char <- get *before-cursor, value:offset + previous-character-is-newline?:bool <- equal previous-character, 10/newline + break-unless previous-character-is-newline? + # compute length of previous line + trace 10, [app], [switching to previous line] + d:&:duplex-list:char <- get *editor, data:offset + end-of-line:num <- previous-line-length before-cursor, d + right:num <- get *editor, right:offset + width:num <- subtract right, left + wrap?:bool <- greater-than end-of-line, width + { + break-unless wrap? + _, column-offset:num <- divide-with-remainder end-of-line, width + cursor-column <- add left, column-offset + *editor <- put *editor, cursor-column:offset, cursor-column + } + { + break-if wrap? + cursor-column <- add left, end-of-line + *editor <- put *editor, cursor-column:offset, cursor-column + } + return + } + # case 2: if previous-character was not newline, we're just at a wrapped line + trace 10, [app], [wrapping to previous line] + right:num <- get *editor, right:offset + cursor-column <- subtract right, 1 # leave room for wrap icon + *editor <- put *editor, cursor-column:offset, cursor-column +] + +# takes a pointer 'curr' into the doubly-linked list and its sentinel, counts +# the length of the previous line before the 'curr' pointer. +def previous-line-length curr:&:duplex-list:char, start:&:duplex-list:char -> result:num [ + local-scope + load-inputs + result:num <- copy 0 + return-unless curr + at-start?:bool <- equal curr, start + return-if at-start? + { + curr <- prev curr + break-unless curr + at-start?:bool <- equal curr, start + break-if at-start? + c:char <- get *curr, value:offset + at-newline?:bool <- equal c, 10/newline + break-if at-newline? + result <- add result, 1 + loop + } +] + +scenario editor-clears-last-line-on-backspace [ + local-scope + assume-screen 10/width, 5/height + s:text <- new [ab +cd] + e:&:editor <- new-editor s, 0/left, 10/right + assume-console [ + left-click 2, 0 + press backspace + ] + run [ + editor-event-loop screen, console, e + 4:num/raw <- get *e, cursor-row:offset + 5:num/raw <- get *e, cursor-column:offset + ] + screen-should-contain [ + . . + .abcd . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] + memory-should-contain [ + 4 <- 1 + 5 <- 2 + ] +] + +scenario editor-joins-and-wraps-lines-on-backspace [ + local-scope + assume-screen 10/width, 5/height + # initialize editor with two long-ish but non-wrapping lines + s:text <- new [abc def +ghi jkl] + e:&:editor <- new-editor s, 0/left, 10/right + editor-render screen, e + $clear-trace + # position the cursor at the start of the second and hit backspace + assume-console [ + left-click 2, 0 + press backspace + ] + run [ + editor-event-loop screen, console, e + ] + # resulting single line should wrap correctly + screen-should-contain [ + . . + .abc defgh↩. + .i jkl . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] +] + +scenario editor-wraps-long-lines-on-backspace [ + local-scope + assume-screen 10/width, 5/height + # initialize editor in part of the screen with a long line + e:&:editor <- new-editor [abc def ghij], 0/left, 8/right + editor-render screen, e + # confirm that it wraps + screen-should-contain [ + . . + .abc def↩ . + . ghij . + .┈┈┈┈┈┈┈┈ . + ] + $clear-trace + # position the cursor somewhere in the middle of the top screen line and hit backspace + assume-console [ + left-click 1, 4 + press backspace + ] + run [ + editor-event-loop screen, console, e + ] + # resulting single line should wrap correctly and not overflow its bounds + screen-should-contain [ + . . + .abcdef ↩ . + .ghij . + .┈┈┈┈┈┈┈┈ . + . . + ] +] + +# delete - delete character at cursor + +scenario editor-handles-delete-key [ + local-scope + assume-screen 10/width, 5/height + e:&:editor <- new-editor [abc], 0/left, 10/right + editor-render screen, e + $clear-trace + assume-console [ + press delete + ] + run [ + editor-event-loop screen, console, e + ] + screen-should-contain [ + . . + .bc . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] + check-trace-count-for-label 3, [print-character] # length of original line to overwrite + $clear-trace + assume-console [ + press delete + ] + run [ + editor-event-loop screen, console, e + ] + screen-should-contain [ + . . + .c . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] + check-trace-count-for-label 2, [print-character] # new length to overwrite +] + +after <handle-special-key> [ + { + delete-next-character?:bool <- equal k, 65522/delete + break-unless delete-next-character? + <begin-delete-character> + go-render?:bool, deleted-cell:&:duplex-list:char <- delete-at-cursor editor, screen + <end-delete-character> + return + } +] + +def delete-at-cursor editor:&:editor, screen:&:screen -> go-render?:bool, deleted-cell:&:duplex-list:char, editor:&:editor, screen:&:screen [ + local-scope + load-inputs + before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset + data:&:duplex-list:char <- get *editor, data:offset + deleted-cell:&:duplex-list:char <- next before-cursor + return-unless deleted-cell, false/don't-render + currc:char <- get *deleted-cell, value:offset + data <- remove deleted-cell, data + deleted-newline?:bool <- equal currc, 10/newline + return-if deleted-newline?, true/go-render + # wasn't a newline? render rest of line + curr:&:duplex-list:char <- next before-cursor # refresh after remove above + cursor-row:num <- get *editor, cursor-row:offset + cursor-column:num <- get *editor, cursor-column:offset + screen <- move-cursor screen, cursor-row, cursor-column + curr-column:num <- copy cursor-column + screen-width:num <- screen-width screen + { + # hit right margin? give up and let caller render + at-right?:bool <- greater-or-equal curr-column, screen-width + return-if at-right?, true/go-render + break-unless curr + currc:char <- get *curr, value:offset + at-newline?:bool <- equal currc, 10/newline + break-if at-newline? + screen <- print screen, currc + curr-column <- add curr-column, 1 + curr <- next curr + loop + } + # we're guaranteed not to be at the right margin + space:char <- copy 32/space + screen <- print screen, space + go-render? <- copy false +] + +# right arrow + +scenario editor-moves-cursor-right-with-key [ + local-scope + assume-screen 10/width, 5/height + e:&:editor <- new-editor [abc], 0/left, 10/right + editor-render screen, e + $clear-trace + assume-console [ + press right-arrow + type [0] + ] + run [ + editor-event-loop screen, console, e + ] + screen-should-contain [ + . . + .a0bc . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] + check-trace-count-for-label 3, [print-character] # 0 and following characters +] + +after <handle-special-key> [ + { + move-to-next-character?:bool <- equal k, 65514/right-arrow + break-unless move-to-next-character? + # if not at end of text + next-cursor:&:duplex-list:char <- next before-cursor + break-unless next-cursor + # scan to next character + <begin-move-cursor> + before-cursor <- copy next-cursor + *editor <- put *editor, before-cursor:offset, before-cursor + go-render?:bool <- move-cursor-coordinates-right editor, screen-height + screen <- move-cursor screen, cursor-row, cursor-column + undo-coalesce-tag:num <- copy 2/right-arrow + <end-move-cursor> + return + } +] + +def move-cursor-coordinates-right editor:&:editor, screen-height:num -> go-render?:bool, editor:&:editor [ + local-scope + load-inputs + before-cursor:&:duplex-list:char <- get *editor before-cursor:offset + cursor-row:num <- get *editor, cursor-row:offset + cursor-column:num <- get *editor, cursor-column:offset + left:num <- get *editor, left:offset + right:num <- get *editor, right:offset + # if crossed a newline, move cursor to start of next row + { + old-cursor-character:char <- get *before-cursor, value:offset + was-at-newline?:bool <- equal old-cursor-character, 10/newline + break-unless was-at-newline? + cursor-row <- add cursor-row, 1 + *editor <- put *editor, cursor-row:offset, cursor-row + cursor-column <- copy left + *editor <- put *editor, cursor-column:offset, cursor-column + below-screen?:bool <- greater-or-equal cursor-row, screen-height # must be equal + return-unless below-screen?, false/don't-render + cursor-row <- subtract cursor-row, 1 # bring back into screen range + *editor <- put *editor, cursor-row:offset, cursor-row + return true/go-render + } + # if the line wraps, move cursor to start of next row + { + # if we're at the column just before the wrap indicator + wrap-column:num <- subtract right, 1 + at-wrap?:bool <- equal cursor-column, wrap-column + break-unless at-wrap? + # and if next character isn't newline + next:&:duplex-list:char <- next before-cursor + break-unless next + next-character:char <- get *next, value:offset + newline?:bool <- equal next-character, 10/newline + break-if newline? + cursor-row <- add cursor-row, 1 + *editor <- put *editor, cursor-row:offset, cursor-row + cursor-column <- copy left + *editor <- put *editor, cursor-column:offset, cursor-column + below-screen?:bool <- greater-or-equal cursor-row, screen-height # must be equal + return-unless below-screen?, false/no-more-render + cursor-row <- subtract cursor-row, 1 # bring back into screen range + *editor <- put *editor, cursor-row:offset, cursor-row + return true/go-render + } + # otherwise move cursor one character right + cursor-column <- add cursor-column, 1 + *editor <- put *editor, cursor-column:offset, cursor-column + go-render? <- copy false +] + +scenario editor-moves-cursor-to-next-line-with-right-arrow [ + local-scope + assume-screen 10/width, 5/height + s:text <- new [abc +d] + e:&:editor <- new-editor s, 0/left, 10/right + editor-render screen, e + $clear-trace + # type right-arrow a few times to get to start of second line + assume-console [ + press right-arrow + press right-arrow + press right-arrow + press right-arrow # next line + ] + run [ + editor-event-loop screen, console, e + ] + check-trace-count-for-label 0, [print-character] + # type something and ensure it goes where it should + assume-console [ + type [0] + ] + run [ + editor-event-loop screen, console, e + ] + screen-should-contain [ + . . + .abc . + .0d . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] + check-trace-count-for-label 2, [print-character] # new length of second line +] + +scenario editor-moves-cursor-to-next-line-with-right-arrow-2 [ + local-scope + assume-screen 10/width, 5/height + s:text <- new [abc +d] + e:&:editor <- new-editor s, 1/left, 10/right + editor-render screen, e + assume-console [ + press right-arrow + press right-arrow + press right-arrow + press right-arrow # next line + type [0] + ] + run [ + editor-event-loop screen, console, e + ] + screen-should-contain [ + . . + . abc . + . 0d . + . ┈┈┈┈┈┈┈┈┈. + . . + ] +] + +scenario editor-moves-cursor-to-next-wrapped-line-with-right-arrow [ + local-scope + assume-screen 10/width, 5/height + e:&:editor <- new-editor [abcdef], 0/left, 5/right + editor-render screen, e + $clear-trace + assume-console [ + left-click 1, 3 + press right-arrow + ] + run [ + 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 [ + . . + .abcd↩ . + .ef . + .┈┈┈┈┈ . + . . + ] + memory-should-contain [ + 3 <- 2 + 4 <- 0 + ] + check-trace-count-for-label 0, [print-character] +] + +scenario editor-moves-cursor-to-next-wrapped-line-with-right-arrow-2 [ + local-scope + assume-screen 10/width, 5/height + # line just barely wrapping + e:&:editor <- new-editor [abcde], 0/left, 5/right + editor-render screen, e + $clear-trace + # position cursor at last character before wrap and hit right-arrow + assume-console [ + left-click 1, 3 + press right-arrow + ] + 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 <- 2 + 4 <- 0 + ] + # now hit right arrow again + assume-console [ + press right-arrow + ] + 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 <- 2 + 4 <- 1 + ] + check-trace-count-for-label 0, [print-character] +] + +scenario editor-moves-cursor-to-next-wrapped-line-with-right-arrow-3 [ + local-scope + assume-screen 10/width, 5/height + e:&:editor <- new-editor [abcdef], 1/left, 6/right + editor-render screen, e + $clear-trace + assume-console [ + left-click 1, 4 + press right-arrow + ] + run [ + 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 [ + . . + . abcd↩ . + . ef . + . ┈┈┈┈┈ . + . . + ] + memory-should-contain [ + 3 <- 2 + 4 <- 1 + ] + check-trace-count-for-label 0, [print-character] +] + +scenario editor-moves-cursor-to-next-line-with-right-arrow-at-end-of-line [ + local-scope + assume-screen 10/width, 5/height + s:text <- new [abc +d] + e:&:editor <- new-editor s, 0/left, 10/right + editor-render screen, e + $clear-trace + # move to end of line, press right-arrow, type a character + assume-console [ + left-click 1, 3 + press right-arrow + type [0] + ] + run [ + editor-event-loop screen, console, e + ] + # new character should be in next line + screen-should-contain [ + . . + .abc . + .0d . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] + check-trace-count-for-label 2, [print-character] +] + +# todo: ctrl-right: next word-end + +# left arrow + +scenario editor-moves-cursor-left-with-key [ + local-scope + assume-screen 10/width, 5/height + e:&:editor <- new-editor [abc], 0/left, 10/right + editor-render screen, e + $clear-trace + assume-console [ + left-click 1, 2 + press left-arrow + type [0] + ] + run [ + editor-event-loop screen, console, e + ] + screen-should-contain [ + . . + .a0bc . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] + check-trace-count-for-label 3, [print-character] +] + +after <handle-special-key> [ + { + move-to-previous-character?:bool <- equal k, 65515/left-arrow + break-unless move-to-previous-character? + trace 10, [app], [left arrow] + # if not at start of text (before-cursor at § sentinel) + prev:&:duplex-list:char <- prev before-cursor + return-unless prev, false/don't-render + <begin-move-cursor> + move-cursor-coordinates-left editor + before-cursor <- copy prev + *editor <- put *editor, before-cursor:offset, before-cursor + undo-coalesce-tag:num <- copy 1/left-arrow + <end-move-cursor> + return + } +] + +scenario editor-moves-cursor-to-previous-line-with-left-arrow-at-start-of-line [ + local-scope + assume-screen 10/width, 5/height + # initialize editor with two lines + s:text <- new [abc +d] + e:&:editor <- new-editor s, 0/left, 10/right + editor-render screen, e + $clear-trace + # position cursor at start of second line (so there's no previous newline) + assume-console [ + left-click 2, 0 + press left-arrow + ] + 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 + ] + check-trace-count-for-label 0, [print-character] +] + +scenario editor-moves-cursor-to-previous-line-with-left-arrow-at-start-of-line-2 [ + local-scope + assume-screen 10/width, 5/height + # initialize editor with three lines + s:text <- new [abc +def +g] + e:&:editor <- new-editor s:text, 0/left, 10/right + editor-render screen, e + $clear-trace + # position cursor further down (so there's a newline before the character at + # the cursor) + assume-console [ + left-click 3, 0 + press left-arrow + type [0] + ] + run [ + editor-event-loop screen, console, e + ] + screen-should-contain [ + . . + .abc . + .def0 . + .g . + .┈┈┈┈┈┈┈┈┈┈. + ] + check-trace-count-for-label 1, [print-character] # just the '0' +] + +scenario editor-moves-cursor-to-previous-line-with-left-arrow-at-start-of-line-3 [ + local-scope + assume-screen 10/width, 5/height + s:text <- new [abc +def +g] + e:&:editor <- new-editor s, 0/left, 10/right + editor-render screen, e + $clear-trace + # position cursor at start of text, press left-arrow, then type a character + assume-console [ + left-click 1, 0 + press left-arrow + type [0] + ] + run [ + editor-event-loop screen, console, e + ] + # left-arrow should have had no effect + screen-should-contain [ + . . + .0abc . + .def . + .g . + .┈┈┈┈┈┈┈┈┈┈. + ] + check-trace-count-for-label 4, [print-character] # length of first line +] + +scenario editor-moves-cursor-to-previous-line-with-left-arrow-at-start-of-line-4 [ + local-scope + assume-screen 10/width, 5/height + # initialize editor with text containing an empty line + s:text <- new [abc + +d] + e:&:editor <- new-editor s, 0/left, 10/right + editor-render screen, e:&:editor + $clear-trace + # position cursor right after empty line + assume-console [ + left-click 3, 0 + press left-arrow + type [0] + ] + run [ + editor-event-loop screen, console, e + ] + screen-should-contain [ + . . + .abc . + .0 . + .d . + .┈┈┈┈┈┈┈┈┈┈. + ] + check-trace-count-for-label 1, [print-character] # just the '0' +] + +scenario editor-moves-across-screen-lines-across-wrap-with-left-arrow [ + local-scope + assume-screen 10/width, 5/height + # initialize editor with a wrapping line + e:&:editor <- new-editor [abcdef], 0/left, 5/right + editor-render screen, e + $clear-trace + screen-should-contain [ + . . + .abcd↩ . + .ef . + .┈┈┈┈┈ . + . . + ] + # position cursor right after empty line + assume-console [ + left-click 2, 0 + press left-arrow + ] + 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 # previous row + 4 <- 3 # right margin except wrap icon + ] + check-trace-count-for-label 0, [print-character] +] + +scenario editor-moves-across-screen-lines-to-wrapping-line-with-left-arrow [ + local-scope + assume-screen 10/width, 5/height + # initialize editor with a wrapping line followed by a second line + s:text <- new [abcdef +g] + e:&:editor <- new-editor s, 0/left, 5/right + editor-render screen, e + $clear-trace + screen-should-contain [ + . . + .abcd↩ . + .ef . + .g . + .┈┈┈┈┈ . + ] + # position cursor right after empty line + assume-console [ + left-click 3, 0 + press left-arrow + ] + 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 <- 2 # previous row + 4 <- 2 # end of wrapped line + ] + check-trace-count-for-label 0, [print-character] +] + +scenario editor-moves-across-screen-lines-to-non-wrapping-line-with-left-arrow [ + local-scope + assume-screen 10/width, 5/height + # initialize editor with a line on the verge of wrapping, followed by a second line + s:text <- new [abcd +e] + e:&:editor <- new-editor s, 0/left, 5/right + editor-render screen, e + $clear-trace + screen-should-contain [ + . . + .abcd . + .e . + .┈┈┈┈┈ . + . . + ] + # position cursor right after empty line + assume-console [ + left-click 2, 0 + press left-arrow + ] + 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 # previous row + 4 <- 4 # end of wrapped line + ] + check-trace-count-for-label 0, [print-character] +] + +# todo: ctrl-left: previous word-start + +# up arrow + +scenario editor-moves-to-previous-line-with-up-arrow [ + local-scope + assume-screen 10/width, 5/height + s:text <- new [abc +def] + e:&:editor <- new-editor s, 0/left, 10/right + editor-render screen, e + $clear-trace + assume-console [ + left-click 2, 1 + press up-arrow + ] + 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 + ] + check-trace-count-for-label 0, [print-character] + assume-console [ + type [0] + ] + run [ + editor-event-loop screen, console, e + ] + screen-should-contain [ + . . + .a0bc . + .def . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] +] + +after <handle-special-key> [ + { + move-to-previous-line?:bool <- equal k, 65517/up-arrow + break-unless move-to-previous-line? + <begin-move-cursor> + move-to-previous-line editor + undo-coalesce-tag:num <- copy 3/up-arrow + <end-move-cursor> + return + } +] + +def move-to-previous-line editor:&:editor -> editor:&:editor [ + local-scope + load-inputs + 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 + left:num <- get *editor, left:offset + right:num <- get *editor, right:offset + already-at-top?:bool <- lesser-or-equal cursor-row, 1/top + { + # if cursor not at top, move it + break-if already-at-top? + # if not at newline, move to start of line (previous newline) + # then scan back another line + # if either step fails, give up without modifying cursor or coordinates + curr:&:duplex-list:char <- copy before-cursor + old:&:duplex-list:char <- copy curr + { + at-left?:bool <- equal cursor-column, left + break-if at-left? + curr <- before-previous-screen-line curr, editor + no-motion?:bool <- equal curr, old + return-if no-motion? + } + { + curr <- before-previous-screen-line curr, editor + no-motion?:bool <- equal curr, old + return-if no-motion? + } + before-cursor <- copy curr + *editor <- put *editor, before-cursor:offset, before-cursor + cursor-row <- subtract cursor-row, 1 + *editor <- put *editor, cursor-row:offset, cursor-row + # scan ahead to right column or until end of line + target-column:num <- copy cursor-column + cursor-column <- copy left + *editor <- put *editor, cursor-column:offset, cursor-column + { + done?:bool <- greater-or-equal cursor-column, target-column + break-if done? + curr:&:duplex-list:char <- next before-cursor + break-unless curr + currc:char <- get *curr, value:offset + at-newline?:bool <- equal currc, 10/newline + break-if at-newline? + # + before-cursor <- copy curr + *editor <- put *editor, before-cursor:offset, before-cursor + cursor-column <- add cursor-column, 1 + *editor <- put *editor, cursor-column:offset, cursor-column + loop + } + } +] + +# Takes a pointer into the doubly-linked list, scans back to before start of +# previous *wrapped* line. +# Returns original if no next newline. +# Beware: never return null pointer. +def before-previous-screen-line in:&:duplex-list:char, editor:&:editor -> out:&:duplex-list:char [ + local-scope + load-inputs + curr:&:duplex-list:char <- copy in + c:char <- get *curr, value:offset + # compute max, number of characters to skip + # 1 + len%(width-1) + # except rotate second term to vary from 1 to width-1 rather than 0 to width-2 + left:num <- get *editor, left:offset + right:num <- get *editor, right:offset + max-line-length:num <- subtract right, left, -1/exclusive-right, 1/wrap-icon + sentinel:&:duplex-list:char <- get *editor, data:offset + len:num <- previous-line-length curr, sentinel + { + break-if len + # empty line; just skip this newline + prev:&:duplex-list:char <- prev curr + return-unless prev, curr + return prev + } + _, max:num <- divide-with-remainder len, max-line-length + # remainder 0 => scan one width-worth + { + break-if max + max <- copy max-line-length + } + max <- add max, 1 + count:num <- copy 0 + # skip 'max' characters + { + done?:bool <- greater-or-equal count, max + break-if done? + prev:&:duplex-list:char <- prev curr + break-unless prev + curr <- copy prev + count <- add count, 1 + loop + } + return curr +] + +scenario editor-adjusts-column-at-previous-line [ + local-scope + assume-screen 10/width, 5/height + s:text <- new [ab +def] + e:&:editor <- new-editor s, 0/left, 10/right + editor-render screen, e + $clear-trace + assume-console [ + left-click 2, 3 + press up-arrow + ] + 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 + ] + check-trace-count-for-label 0, [print-character] + assume-console [ + type [0] + ] + run [ + editor-event-loop screen, console, e + ] + screen-should-contain [ + . . + .ab0 . + .def . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] +] + +scenario editor-adjusts-column-at-empty-line [ + local-scope + assume-screen 10/width, 5/height + s:text <- new [ +def] + e:&:editor <- new-editor s, 0/left, 10/right + editor-render screen, e + $clear-trace + assume-console [ + left-click 2, 3 + press up-arrow + ] + 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 <- 0 + ] + check-trace-count-for-label 0, [print-character] + assume-console [ + type [0] + ] + run [ + editor-event-loop screen, console, e + ] + screen-should-contain [ + . . + .0 . + .def . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] +] + +scenario editor-moves-to-previous-line-from-zero-margin [ + local-scope + assume-screen 10/width, 5/height + # start out with three lines + s:text <- new [abc +def +ghi] + e:&:editor <- new-editor s, 0/left, 10/right + editor-render screen, e + $clear-trace + # click on the third line and hit up-arrow, so you end up just after a newline + assume-console [ + left-click 3, 0 + press up-arrow + ] + 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 <- 2 + 4 <- 0 + ] + check-trace-count-for-label 0, [print-character] + assume-console [ + type [0] + ] + run [ + editor-event-loop screen, console, e + ] + screen-should-contain [ + . . + .abc . + .0def . + .ghi . + .┈┈┈┈┈┈┈┈┈┈. + ] +] + +scenario editor-moves-to-previous-line-from-left-margin [ + local-scope + assume-screen 10/width, 5/height + # start out with three lines + s:text <- new [abc +def +ghi] + e:&:editor <- new-editor s, 1/left, 10/right + editor-render screen, e + $clear-trace + # click on the third line and hit up-arrow, so you end up just after a newline + assume-console [ + left-click 3, 1 + press up-arrow + ] + 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 <- 2 + 4 <- 1 + ] + check-trace-count-for-label 0, [print-character] + assume-console [ + type [0] + ] + run [ + editor-event-loop screen, console, e + ] + screen-should-contain [ + . . + . abc . + . 0def . + . ghi . + . ┈┈┈┈┈┈┈┈┈. + ] +] + +scenario editor-moves-to-top-line-in-presence-of-wrapped-line [ + local-scope + assume-screen 10/width, 5/height + e:&:editor <- new-editor [abcde], 0/left, 5/right + editor-render screen, e + screen-should-contain [ + . . + .abcd↩ . + .e . + .┈┈┈┈┈ . + ] + $clear-trace + assume-console [ + left-click 2, 0 + press up-arrow + ] + 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 <- 0 + ] + check-trace-count-for-label 0, [print-character] + assume-console [ + type [0] + ] + run [ + editor-event-loop screen, console, e + ] + screen-should-contain [ + . . + .0abc↩ . + .de . + .┈┈┈┈┈ . + ] +] + +scenario editor-moves-to-top-line-in-presence-of-wrapped-line-2 [ + local-scope + assume-screen 10/width, 5/height + s:text <- new [abc +defgh] + e:&:editor <- new-editor s, 0/left, 5/right + editor-render screen, e + screen-should-contain [ + . . + .abc . + .defg↩ . + .h . + .┈┈┈┈┈ . + ] + $clear-trace + assume-console [ + left-click 3, 0 + press up-arrow + press up-arrow + ] + 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 <- 0 + ] + check-trace-count-for-label 0, [print-character] + assume-console [ + type [0] + ] + run [ + editor-event-loop screen, console, e + ] + screen-should-contain [ + . . + .0abc . + .defg↩ . + .h . + .┈┈┈┈┈ . + ] +] + +# down arrow + +scenario editor-moves-to-next-line-with-down-arrow [ + local-scope + assume-screen 10/width, 5/height + s:text <- new [abc +def] + e:&:editor <- new-editor s, 0/left, 10/right + editor-render screen, e + $clear-trace + # cursor starts out at (1, 0) + assume-console [ + press down-arrow + ] + run [ + editor-event-loop screen, console, e + 3:num/raw <- get *e, cursor-row:offset + 4:num/raw <- get *e, cursor-column:offset + ] + # ..and ends at (2, 0) + memory-should-contain [ + 3 <- 2 + 4 <- 0 + ] + check-trace-count-for-label 0, [print-character] + assume-console [ + type [0] + ] + run [ + editor-event-loop screen, console, e + ] + screen-should-contain [ + . . + .abc . + .0def . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] +] + +after <handle-special-key> [ + { + move-to-next-line?:bool <- equal k, 65516/down-arrow + break-unless move-to-next-line? + <begin-move-cursor> + move-to-next-line editor, screen-height + undo-coalesce-tag:num <- copy 4/down-arrow + <end-move-cursor> + return + } +] + +def move-to-next-line editor:&:editor, screen-height:num -> editor:&:editor [ + local-scope + load-inputs + 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 + left:num <- get *editor, left:offset + right:num <- get *editor, right:offset + last-line:num <- subtract screen-height, 1 + bottom:num <- get *editor, bottom:offset + at-bottom-of-screen?:bool <- greater-or-equal bottom, last-line + return-unless before-cursor + next:&:duplex-list:char <- next before-cursor + return-unless next + already-at-bottom?:bool <- greater-or-equal cursor-row, last-line + # if cursor not at bottom, move it + return-if already-at-bottom? + target-column:num <- copy cursor-column + # scan to start of next line + { + next:&:duplex-list:char <- next before-cursor + break-unless next + done?:bool <- greater-or-equal cursor-column, right + break-if done? + cursor-column <- add cursor-column, 1 + before-cursor <- copy next + c:char <- get *next, value:offset + at-newline?:bool <- equal c, 10/newline + break-if at-newline? + loop + } + return-unless next + cursor-row <- add cursor-row, 1 + cursor-column <- copy left + { + next:&:duplex-list:char <- next before-cursor + break-unless next + c:char <- get *next, value:offset + at-newline?:bool <- equal c, 10/newline + break-if at-newline? + done?:bool <- greater-or-equal cursor-column, target-column + break-if done? + cursor-column <- add cursor-column, 1 + before-cursor <- copy next + loop + } + *editor <- put *editor, before-cursor:offset, before-cursor + *editor <- put *editor, cursor-column:offset, cursor-column + *editor <- put *editor, cursor-row:offset, cursor-row +] + +scenario editor-adjusts-column-at-next-line [ + local-scope + assume-screen 10/width, 5/height + # second line is shorter than first + s:text <- new [abcde +fg +hi] + e:&:editor <- new-editor s, 0/left, 10/right + editor-render screen, e + $clear-trace + # move to end of first line, then press down + assume-console [ + left-click 1, 8 + press down-arrow + ] + run [ + editor-event-loop screen, console, e + 3:num/raw <- get *e, cursor-row:offset + 4:num/raw <- get *e, cursor-column:offset + ] + # cursor doesn't go vertically down, it goes to end of shorter line + memory-should-contain [ + 3 <- 2 + 4 <- 2 + ] + check-trace-count-for-label 0, [print-character] + assume-console [ + type [0] + ] + run [ + editor-event-loop screen, console, e + ] + screen-should-contain [ + . . + .abcde . + .fg0 . + .hi . + .┈┈┈┈┈┈┈┈┈┈. + ] +] + +# ctrl-a/home - move cursor to start of line + +scenario editor-moves-to-start-of-line-with-ctrl-a [ + local-scope + assume-screen 10/width, 5/height + s:text <- new [123 +456] + e:&:editor <- new-editor s, 0/left, 10/right + editor-render screen, e + $clear-trace + # start on second line, press ctrl-a + assume-console [ + left-click 2, 3 + press ctrl-a + ] + run [ + editor-event-loop screen, console, e + 4:num/raw <- get *e, cursor-row:offset + 5:num/raw <- get *e, cursor-column:offset + ] + # cursor moves to start of line + memory-should-contain [ + 4 <- 2 + 5 <- 0 + ] + check-trace-count-for-label 0, [print-character] +] + +after <handle-special-character> [ + { + move-to-start-of-line?:bool <- equal c, 1/ctrl-a + break-unless move-to-start-of-line? + <begin-move-cursor> + move-to-start-of-screen-line editor + undo-coalesce-tag:num <- copy 0/never + <end-move-cursor> + return false/don't-render + } +] + +after <handle-special-key> [ + { + move-to-start-of-line?:bool <- equal k, 65521/home + break-unless move-to-start-of-line? + <begin-move-cursor> + move-to-start-of-screen-line editor + undo-coalesce-tag:num <- copy 0/never + <end-move-cursor> + return false/don't-render + } +] + +# handles wrapped lines +# precondition: cursor-column should be in a consistent state +def move-to-start-of-screen-line editor:&:editor -> editor:&:editor [ + local-scope + load-inputs + # update cursor column + left:num <- get *editor, left:offset + col:num <- get *editor, cursor-column:offset + # update before-cursor + curr:&:duplex-list:char <- get *editor, before-cursor:offset + # while not at start of line, move + { + done?:bool <- equal col, left + break-if done? + assert curr, [move-to-start-of-line tried to move before start of text] + curr <- prev curr + col <- subtract col, 1 + loop + } + *editor <- put *editor, cursor-column:offset, col + *editor <- put *editor, before-cursor:offset, curr +] + +scenario editor-moves-to-start-of-line-with-ctrl-a-2 [ + local-scope + assume-screen 10/width, 5/height + s:text <- new [123 +456] + e:&:editor <- new-editor s, 0/left, 10/right + editor-render screen, e + $clear-trace + # start on first line (no newline before), press ctrl-a + assume-console [ + left-click 1, 3 + press ctrl-a + ] + run [ + editor-event-loop screen, console, e + 4:num/raw <- get *e, cursor-row:offset + 5:num/raw <- get *e, cursor-column:offset + ] + # cursor moves to start of line + memory-should-contain [ + 4 <- 1 + 5 <- 0 + ] + check-trace-count-for-label 0, [print-character] +] + +scenario editor-moves-to-start-of-line-with-home [ + local-scope + assume-screen 10/width, 5/height + s:text <- new [123 +456] + e:&:editor <- new-editor s, 0/left, 10/right + $clear-trace + # start on second line, press 'home' + assume-console [ + left-click 2, 3 + press home + ] + 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 to start of line + memory-should-contain [ + 3 <- 2 + 4 <- 0 + ] + check-trace-count-for-label 0, [print-character] +] + +scenario editor-moves-to-start-of-line-with-home-2 [ + local-scope + assume-screen 10/width, 5/height + s:text <- new [123 +456] + e:&:editor <- new-editor s, 0/left, 10/right + editor-render screen, e + $clear-trace + # start on first line (no newline before), press 'home' + assume-console [ + left-click 1, 3 + press home + ] + 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 to start of line + memory-should-contain [ + 3 <- 1 + 4 <- 0 + ] + check-trace-count-for-label 0, [print-character] +] + +scenario editor-moves-to-start-of-screen-line-with-ctrl-a [ + local-scope + assume-screen 10/width, 5/height + e:&:editor <- new-editor [123456], 0/left, 5/right + editor-render screen, e + screen-should-contain [ + . . + .1234↩ . + .56 . + .┈┈┈┈┈ . + . . + ] + $clear-trace + # start on second line, press ctrl-a then up + assume-console [ + left-click 2, 1 + press ctrl-a + press up-arrow + ] + run [ + editor-event-loop screen, console, e + 4:num/raw <- get *e, cursor-row:offset + 5:num/raw <- get *e, cursor-column:offset + ] + # cursor moves to start of first line + memory-should-contain [ + 4 <- 1 # cursor-row + 5 <- 0 # cursor-column + ] + check-trace-count-for-label 0, [print-character] + # make sure before-cursor is in sync + assume-console [ + type [a] + ] + run [ + editor-event-loop screen, console, e + 4:num/raw <- get *e, cursor-row:offset + 5:num/raw <- get *e, cursor-column:offset + ] + screen-should-contain [ + . . + .a123↩ . + .456 . + .┈┈┈┈┈ . + . . + ] + memory-should-contain [ + 4 <- 1 # cursor-row + 5 <- 1 # cursor-column + ] +] + +# ctrl-e/end - move cursor to end of line + +scenario editor-moves-to-end-of-line-with-ctrl-e [ + local-scope + assume-screen 10/width, 5/height + s:text <- new [123 +456] + e:&:editor <- new-editor s, 0/left, 10/right + editor-render screen, e + $clear-trace + # start on first line, press ctrl-e + assume-console [ + left-click 1, 1 + press ctrl-e + ] + run [ + editor-event-loop screen, console, e + 4:num/raw <- get *e, cursor-row:offset + 5:num/raw <- get *e, cursor-column:offset + ] + # cursor moves to end of line + memory-should-contain [ + 4 <- 1 + 5 <- 3 + ] + check-trace-count-for-label 0, [print-character] + # editor inserts future characters at cursor + assume-console [ + type [z] + ] + run [ + editor-event-loop screen, console, e + 4:num/raw <- get *e, cursor-row:offset + 5:num/raw <- get *e, cursor-column:offset + ] + memory-should-contain [ + 4 <- 1 + 5 <- 4 + ] + screen-should-contain [ + . . + .123z . + .456 . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] + check-trace-count-for-label 1, [print-character] +] + +after <handle-special-character> [ + { + move-to-end-of-line?:bool <- equal c, 5/ctrl-e + break-unless move-to-end-of-line? + <begin-move-cursor> + move-to-end-of-line editor + undo-coalesce-tag:num <- copy 0/never + <end-move-cursor> + return false/don't-render + } +] + +after <handle-special-key> [ + { + move-to-end-of-line?:bool <- equal k, 65520/end + break-unless move-to-end-of-line? + <begin-move-cursor> + move-to-end-of-line editor + undo-coalesce-tag:num <- copy 0/never + <end-move-cursor> + return false/don't-render + } +] + +def move-to-end-of-line editor:&:editor -> editor:&:editor [ + local-scope + load-inputs + before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset + cursor-column:num <- get *editor, cursor-column:offset + right:num <- get *editor, right:offset + # while not at end of line, move + { + next:&:duplex-list:char <- next before-cursor + break-unless next # end of text + nextc:char <- get *next, value:offset + at-end-of-line?:bool <- equal nextc, 10/newline + break-if at-end-of-line? + cursor-column <- add cursor-column, 1 + at-right?:bool <- equal cursor-column, right + break-if at-right? + *editor <- put *editor, cursor-column:offset, cursor-column + before-cursor <- copy next + *editor <- put *editor, before-cursor:offset, before-cursor + loop + } +] + +scenario editor-moves-to-end-of-line-with-ctrl-e-2 [ + local-scope + assume-screen 10/width, 5/height + s:text <- new [123 +456] + e:&:editor <- new-editor s, 0/left, 10/right + editor-render screen, e + $clear-trace + # start on second line (no newline after), press ctrl-e + assume-console [ + left-click 2, 1 + press ctrl-e + ] + run [ + editor-event-loop screen, console, e + 4:num/raw <- get *e, cursor-row:offset + 5:num/raw <- get *e, cursor-column:offset + ] + # cursor moves to end of line + memory-should-contain [ + 4 <- 2 + 5 <- 3 + ] + check-trace-count-for-label 0, [print-character] +] + +scenario editor-moves-to-end-of-line-with-end [ + local-scope + assume-screen 10/width, 5/height + s:text <- new [123 +456] + e:&:editor <- new-editor s, 0/left, 10/right + editor-render screen, e + $clear-trace + # start on first line, press 'end' + assume-console [ + left-click 1, 1 + press end + ] + 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 to end of line + memory-should-contain [ + 3 <- 1 + 4 <- 3 + ] + check-trace-count-for-label 0, [print-character] +] + +scenario editor-moves-to-end-of-line-with-end-2 [ + local-scope + assume-screen 10/width, 5/height + s:text <- new [123 +456] + e:&:editor <- new-editor s, 0/left, 10/right + editor-render screen, e + $clear-trace + # start on second line (no newline after), press 'end' + assume-console [ + left-click 2, 1 + press end + ] + 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 to end of line + memory-should-contain [ + 3 <- 2 + 4 <- 3 + ] + check-trace-count-for-label 0, [print-character] +] + +scenario editor-moves-to-end-of-wrapped-line [ + local-scope + assume-screen 10/width, 5/height + s:text <- new [123456 +789] + e:&:editor <- new-editor s, 0/left, 5/right + editor-render screen, e + $clear-trace + # start on first line, press 'end' + assume-console [ + left-click 1, 1 + press end + ] + run [ + editor-event-loop screen, console, e + 10:num/raw <- get *e, cursor-row:offset + 11:num/raw <- get *e, cursor-column:offset + ] + # cursor moves to end of line + memory-should-contain [ + 10 <- 1 + 11 <- 3 + ] + # no prints + check-trace-count-for-label 0, [print-character] + # before-cursor is also consistent + assume-console [ + type [a] + ] + run [ + editor-event-loop screen, console, e + ] + screen-should-contain [ + . . + .123a↩ . + .456 . + .789 . + .┈┈┈┈┈ . + ] +] + +# ctrl-u - delete text from start of line until (but not at) cursor + +scenario editor-deletes-to-start-of-line-with-ctrl-u [ + local-scope + assume-screen 10/width, 5/height + s:text <- new [123 +456] + e:&:editor <- new-editor s, 0/left, 10/right + editor-render screen, e + $clear-trace + # start on second line, press ctrl-u + assume-console [ + left-click 2, 2 + press ctrl-u + ] + run [ + editor-event-loop screen, console, e + ] + # cursor deletes to start of line + screen-should-contain [ + . . + .123 . + .6 . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] + check-trace-count-for-label 10, [print-character] +] + +after <handle-special-character> [ + { + delete-to-start-of-line?:bool <- equal c, 21/ctrl-u + break-unless delete-to-start-of-line? + <begin-delete-to-start-of-line> + deleted-cells:&:duplex-list:char <- delete-to-start-of-line editor + <end-delete-to-start-of-line> + go-render?:bool <- minimal-render-for-ctrl-u screen, editor, deleted-cells + return + } +] + +def minimal-render-for-ctrl-u screen:&:screen, editor:&:editor, deleted-cells:&:duplex-list:char -> go-render?:bool, screen:&:screen [ + local-scope + load-inputs + curr-column:num <- get *editor, cursor-column:offset + # accumulate the current line as text and render it + buf:&:buffer:char <- new-buffer 30 # accumulator for the text we need to render + curr:&:duplex-list:char <- get *editor, before-cursor:offset + i:num <- copy curr-column + right:num <- get *editor, right:offset + { + # if we have a wrapped line, give up and render the whole screen + wrap?:bool <- greater-or-equal i, right + return-if wrap?, true/go-render + curr <- next curr + break-unless curr + c:char <- get *curr, value:offset + b:bool <- equal c, 10 + break-if b + buf <- append buf, c + i <- add i, 1 + loop + } + # if the line used to be wrapped, give up and render the whole screen + num-deleted-cells:num <- length deleted-cells + old-row-len:num <- add i, num-deleted-cells + left:num <- get *editor, left:offset + end:num <- subtract right, left + wrap?:bool <- greater-or-equal old-row-len, end + return-if wrap?, true/go-render + curr-line:text <- buffer-to-array buf + curr-row:num <- get *editor, cursor-row:offset + render-code screen, curr-line, curr-column, right, curr-row + return false/dont-render +] + +def delete-to-start-of-line editor:&:editor -> result:&:duplex-list:char, editor:&:editor [ + local-scope + load-inputs + # compute range to delete + init:&:duplex-list:char <- get *editor, data:offset + before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset + start:&:duplex-list:char <- copy before-cursor + end:&:duplex-list:char <- next before-cursor + { + at-start-of-text?:bool <- equal start, init + break-if at-start-of-text? + curr:char <- get *start, value:offset + at-start-of-line?:bool <- equal curr, 10/newline + break-if at-start-of-line? + start <- prev start + assert start, [delete-to-start-of-line tried to move before start of text] + loop + } + # snip it out + result:&:duplex-list:char <- next start + remove-between start, end + # adjust cursor + before-cursor <- copy start + *editor <- put *editor, before-cursor:offset, before-cursor + left:num <- get *editor, left:offset + *editor <- put *editor, cursor-column:offset, left + # if the line wrapped before, we may need to adjust cursor-row as well + right:num <- get *editor, right:offset + width:num <- subtract right, left + num-deleted:num <- length result + cursor-row-adjustment:num <- divide-with-remainder num-deleted, width + return-unless cursor-row-adjustment + cursor-row:num <- get *editor, cursor-row:offset + cursor-row <- subtract cursor-row, cursor-row-adjustment + put *editor, cursor-row:offset, cursor-row +] + +def render-code screen:&:screen, s:text, left:num, right:num, row:num -> row:num, screen:&:screen [ + local-scope + load-inputs + return-unless s + color:num <- copy 7/white + column:num <- copy left + screen <- move-cursor screen, row, column + screen-height:num <- screen-height screen + i:num <- copy 0 + len:num <- length *s + { + +next-character + done?:bool <- greater-or-equal i, len + break-if done? + done? <- greater-or-equal row, screen-height + break-if done? + c:char <- index *s, i + <character-c-received> + { + # newline? move to left rather than 0 + newline?:bool <- equal c, 10/newline + break-unless newline? + # clear rest of line in this window + { + done?:bool <- greater-than column, right + break-if done? + space:char <- copy 32/space + print screen, space + column <- add column, 1 + loop + } + row <- add row, 1 + column <- copy left + screen <- move-cursor screen, row, column + i <- add i, 1 + loop +next-character + } + { + # at right? wrap. + at-right?:bool <- equal column, right + break-unless at-right? + # print wrap icon + wrap-icon:char <- copy 8617/loop-back-to-left + print screen, wrap-icon, 245/grey + column <- copy left + row <- add row, 1 + screen <- move-cursor screen, row, column + # don't increment i + loop +next-character + } + i <- add i, 1 + print screen, c, color + column <- add column, 1 + loop + } + was-at-left?:bool <- equal column, left + clear-line-until screen, right + { + break-if was-at-left? + row <- add row, 1 + } + move-cursor screen, row, left +] + +scenario editor-deletes-to-start-of-line-with-ctrl-u-2 [ + local-scope + assume-screen 10/width, 5/height + s:text <- new [123 +456] + e:&:editor <- new-editor s, 0/left, 10/right + editor-render screen, e + $clear-trace + # start on first line (no newline before), press ctrl-u + assume-console [ + left-click 1, 2 + press ctrl-u + ] + run [ + editor-event-loop screen, console, e + ] + # cursor deletes to start of line + screen-should-contain [ + . . + .3 . + .456 . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] + check-trace-count-for-label 10, [print-character] +] + +scenario editor-deletes-to-start-of-line-with-ctrl-u-3 [ + local-scope + assume-screen 10/width, 5/height + s:text <- new [123 +456] + e:&:editor <- new-editor s, 0/left, 10/right + editor-render screen, e + $clear-trace + # start past end of line, press ctrl-u + assume-console [ + left-click 1, 3 + press ctrl-u + ] + run [ + editor-event-loop screen, console, e + ] + # cursor deletes to start of line + screen-should-contain [ + . . + . . + .456 . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] + check-trace-count-for-label 10, [print-character] +] + +scenario editor-deletes-to-start-of-final-line-with-ctrl-u [ + local-scope + assume-screen 10/width, 5/height + s:text <- new [123 +456] + e:&:editor <- new-editor s, 0/left, 10/right + editor-render screen, e + $clear-trace + # start past end of final line, press ctrl-u + assume-console [ + left-click 2, 3 + press ctrl-u + ] + run [ + editor-event-loop screen, console, e + ] + # cursor deletes to start of line + screen-should-contain [ + . . + .123 . + . . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] + check-trace-count-for-label 10, [print-character] +] + +scenario editor-deletes-to-start-of-wrapped-line-with-ctrl-u [ + local-scope + assume-screen 10/width, 10/height + # first line starts out wrapping + s:text <- new [123456 +789] + e:&:editor <- new-editor s, 0/left, 5/right + editor-render screen, e + screen-should-contain [ + . . + .1234↩ . + .56 . + .789 . + .┈┈┈┈┈ . + . . + ] + $clear-trace + # ctrl-u enough of the first line that it's no longer wrapping + assume-console [ + left-click 1, 3 + press ctrl-u + ] + run [ + editor-event-loop screen, console, e + ] + # entire screen needs to be refreshed + screen-should-contain [ + . . + .456 . + .789 . + .┈┈┈┈┈ . + . . + ] + check-trace-count-for-label 45, [print-character] +] + +# sometimes hitting ctrl-u needs to adjust the cursor row +scenario editor-deletes-to-start-of-wrapped-line-with-ctrl-u-2 [ + local-scope + assume-screen 10/width, 10/height + # third line starts out wrapping + s:text <- new [1 +2 +345678 +9] + e:&:editor <- new-editor s, 0/left, 5/right + editor-render screen, e + screen-should-contain [ + . . + .1 . + .2 . + .3456↩ . + .78 . + .9 . + .┈┈┈┈┈ . + . . + ] + # position cursor on screen line after the wrap and hit ctrl-u + assume-console [ + left-click 4, 1 # on '8' + press ctrl-u + ] + run [ + editor-event-loop screen, console, e + 10:num/raw <- get *e, cursor-row:offset + 11:num/raw <- get *e, cursor-column:offset + ] + screen-should-contain [ + . . + .1 . + .2 . + .8 . + .9 . + .┈┈┈┈┈ . + . . + ] + # cursor moves up one screen line + memory-should-contain [ + 10 <- 3 # cursor-row + 11 <- 0 # cursor-column + ] +] + +# line wrapping twice (taking up 3 screen lines) +scenario editor-deletes-to-start-of-wrapped-line-with-ctrl-u-3 [ + local-scope + assume-screen 10/width, 10/height + # third line starts out wrapping + s:text <- new [1 +2 +3456789abcd +e] + e:&:editor <- new-editor s, 0/left, 5/right + editor-render screen, e + assume-console [ + left-click 4, 1 # on '8' + ] + editor-event-loop screen, console, e + screen-should-contain [ + . . + .1 . + .2 . + .3456↩ . + .789a↩ . + .bcd . + .e . + .┈┈┈┈┈ . + . . + ] + assume-console [ + left-click 5, 1 + press ctrl-u + ] + run [ + editor-event-loop screen, console, e + 10:num/raw <- get *e, cursor-row:offset + 11:num/raw <- get *e, cursor-column:offset + ] + screen-should-contain [ + . . + .1 . + .2 . + .cd . + .e . + .┈┈┈┈┈ . + . . + ] + # make sure we adjusted cursor-row + memory-should-contain [ + 10 <- 3 # cursor-row + 11 <- 0 # cursor-column + ] +] + +# ctrl-k - delete text from cursor to end of line (but not the newline) + +scenario editor-deletes-to-end-of-line-with-ctrl-k [ + local-scope + assume-screen 10/width, 5/height + s:text <- new [123 +456] + e:&:editor <- new-editor s, 0/left, 10/right + editor-render screen, e + $clear-trace + # start on first line, press ctrl-k + assume-console [ + left-click 1, 1 + press ctrl-k + ] + run [ + editor-event-loop screen, console, e + ] + # cursor deletes to end of line + screen-should-contain [ + . . + .1 . + .456 . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] + check-trace-count-for-label 9, [print-character] +] + +after <handle-special-character> [ + { + delete-to-end-of-line?:bool <- equal c, 11/ctrl-k + break-unless delete-to-end-of-line? + <begin-delete-to-end-of-line> + deleted-cells:&:duplex-list:char <- delete-to-end-of-line editor + <end-delete-to-end-of-line> + # checks if we can do a minimal render and if we can it will do a minimal render + go-render?:bool <- minimal-render-for-ctrl-k screen, editor, deleted-cells + return + } +] + +def minimal-render-for-ctrl-k screen:&:screen, editor:&:editor, deleted-cells:&:duplex-list:char -> go-render?:bool, screen:&:screen [ + local-scope + load-inputs + # if we deleted nothing, there's nothing to render + return-unless deleted-cells, false/dont-render + # if the line used to wrap before, give up and render the whole screen + curr-column:num <- get *editor, cursor-column:offset + num-deleted-cells:num <- length deleted-cells + old-row-len:num <- add curr-column, num-deleted-cells + left:num <- get *editor, left:offset + right:num <- get *editor, right:offset + end:num <- subtract right, left + wrap?:bool <- greater-or-equal old-row-len, end + return-if wrap?, true/go-render + clear-line-until screen, right + return false/dont-render +] + +def delete-to-end-of-line editor:&:editor -> result:&:duplex-list:char, editor:&:editor [ + local-scope + load-inputs + # compute range to delete + start:&:duplex-list:char <- get *editor, before-cursor:offset + end:&:duplex-list:char <- next start + { + at-end-of-text?:bool <- equal end, null + break-if at-end-of-text? + curr:char <- get *end, value:offset + at-end-of-line?:bool <- equal curr, 10/newline + break-if at-end-of-line? + end <- next end + loop + } + # snip it out + result <- next start + remove-between start, end +] + +scenario editor-deletes-to-end-of-line-with-ctrl-k-2 [ + local-scope + assume-screen 10/width, 5/height + s:text <- new [123 +456] + e:&:editor <- new-editor s, 0/left, 10/right + editor-render screen, e + $clear-trace + # start on second line (no newline after), press ctrl-k + assume-console [ + left-click 2, 1 + press ctrl-k + ] + run [ + editor-event-loop screen, console, e + ] + # cursor deletes to end of line + screen-should-contain [ + . . + .123 . + .4 . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] + check-trace-count-for-label 9, [print-character] +] + +scenario editor-deletes-to-end-of-line-with-ctrl-k-3 [ + local-scope + assume-screen 10/width, 5/height + s:text <- new [123 +456] + e:&:editor <- new-editor s, 0/left, 10/right + editor-render screen, e + $clear-trace + # start at end of line + assume-console [ + left-click 1, 2 + press ctrl-k + ] + run [ + editor-event-loop screen, console, e + ] + # cursor deletes just last character + screen-should-contain [ + . . + .12 . + .456 . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] + check-trace-count-for-label 8, [print-character] +] + +scenario editor-deletes-to-end-of-line-with-ctrl-k-4 [ + local-scope + assume-screen 10/width, 5/height + s:text <- new [123 +456] + e:&:editor <- new-editor s, 0/left, 10/right + editor-render screen, e + $clear-trace + # start past end of line + assume-console [ + left-click 1, 3 + press ctrl-k + ] + run [ + editor-event-loop screen, console, e + ] + # cursor deletes nothing + screen-should-contain [ + . . + .123 . + .456 . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] + check-trace-count-for-label 7, [print-character] +] + +scenario editor-deletes-to-end-of-line-with-ctrl-k-5 [ + local-scope + assume-screen 10/width, 5/height + s:text <- new [123 +456] + e:&:editor <- new-editor s, 0/left, 10/right + editor-render screen, e + $clear-trace + # start at end of text + assume-console [ + left-click 2, 2 + press ctrl-k + ] + run [ + editor-event-loop screen, console, e + ] + # cursor deletes just the final character + screen-should-contain [ + . . + .123 . + .45 . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] + check-trace-count-for-label 8, [print-character] +] + +scenario editor-deletes-to-end-of-line-with-ctrl-k-6 [ + local-scope + assume-screen 10/width, 5/height + s:text <- new [123 +456] + e:&:editor <- new-editor s, 0/left, 10/right + editor-render screen, e + $clear-trace + # start past end of text + assume-console [ + left-click 2, 3 + press ctrl-k + ] + run [ + editor-event-loop screen, console, e + ] + # cursor deletes nothing + screen-should-contain [ + . . + .123 . + .456 . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] + # no prints necessary + check-trace-count-for-label 0, [print-character] +] + +scenario editor-deletes-to-end-of-wrapped-line-with-ctrl-k [ + local-scope + assume-screen 10/width, 5/height + # create an editor with the first line wrapping to a second screen row + s:text <- new [1234 +567] + e:&:editor <- new-editor s, 0/left, 4/right + editor-render screen, e + $clear-trace + # delete all of the first wrapped line + assume-console [ + press ctrl-k + ] + run [ + editor-event-loop screen, console, e + ] + # screen shows an empty unwrapped first line + screen-should-contain [ + . . + . . + .567 . + .┈┈┈┈ . + . . + ] + # entire screen is refreshed + check-trace-count-for-label 16, [print-character] +] + +# takes a pointer into the doubly-linked list, scans ahead at most 'max' +# positions until the next newline +# returns original if no next newline +# beware: never return null pointer. +def before-start-of-next-line original:&:duplex-list:char, max:num -> curr:&:duplex-list:char [ + local-scope + load-inputs + count:num <- copy 0 + curr:&:duplex-list:char <- copy original + # skip the initial newline if it exists + { + c:char <- get *curr, value:offset + at-newline?:bool <- equal c, 10/newline + break-unless at-newline? + curr <- next curr + count <- add count, 1 + } + { + return-unless curr, original + done?:bool <- greater-or-equal count, max + break-if done? + c:char <- get *curr, value:offset + at-newline?:bool <- equal c, 10/newline + break-if at-newline? + curr <- next curr + count <- add count, 1 + loop + } + return-unless curr, original + return curr +] + +# ctrl-/ - comment/uncomment current line + +after <handle-special-character> [ + { + comment-toggle?:bool <- equal c, 31/ctrl-slash + break-unless comment-toggle? + cursor-column:num <- get *editor, cursor-column:offset + data:&:duplex-list:char <- get *editor, data:offset + <begin-insert-character> + before-line-start:&:duplex-list:char <- before-start-of-screen-line editor + line-start:&:duplex-list:char <- next before-line-start + commented-out?:bool <- match line-start, [#? ] # comment prefix + { + break-unless commented-out? + # uncomment + data <- remove line-start, 3/length-comment-prefix, data + cursor-column <- subtract cursor-column, 3/length-comment-prefix + *editor <- put *editor, cursor-column:offset, cursor-column + go-render? <- render-line-from-start screen, editor, 3/size-of-comment-leader + } + { + break-if commented-out? + # comment + insert before-line-start, [#? ] + cursor-column <- add cursor-column, 3/length-comment-prefix + *editor <- put *editor, cursor-column:offset, cursor-column + go-render? <- render-line-from-start screen, editor, 0 + } + <end-insert-character> + return + } +] + +# Render just from the start of the current line, and only if it wasn't +# wrapping before (include margin) and isn't wrapping now. Otherwise just tell +# the caller to go-render? the entire screen. +def render-line-from-start screen:&:screen, editor:&:editor, right-margin:num -> go-render?:bool, screen:&:screen [ + local-scope + load-inputs + before-line-start:&:duplex-list:char <- before-start-of-screen-line editor + line-start:&:duplex-list:char <- next before-line-start + color:num <- copy 7/white + left:num <- get *editor, left:offset + cursor-row:num <- get *editor, cursor-row:offset + screen <- move-cursor screen, cursor-row, left + right:num <- get *editor, right:offset + end:num <- subtract right, right-margin + i:num <- copy 0 + curr:&:duplex-list:char <- copy line-start + { + render-all?:bool <- greater-or-equal i, end + return-if render-all?, true/go-render + break-unless curr + c:char <- get *curr, value:offset + newline?:bool <- equal c, 10/newline + break-if newline? + color <- get-color color, c + print screen, c, color + curr <- next curr + i <- add i, 1 + loop + } + clear-line-until screen, right + return false/dont-render +] + +def before-start-of-screen-line editor:&:editor -> result:&:duplex-list:char [ + local-scope + load-inputs + cursor:&:duplex-list:char <- get *editor, before-cursor:offset + { + next:&:duplex-list:char <- next cursor + break-unless next + cursor <- copy next + } + result <- before-previous-screen-line cursor, editor +] + +scenario editor-comments-empty-line [ + local-scope + assume-screen 10/width, 5/height + e:&:editor <- new-editor [], 0/left, 5/right + editor-render screen, e + $clear-trace + assume-console [ + press ctrl-slash + ] + run [ + editor-event-loop screen, console, e + 4:num/raw <- get *e, cursor-row:offset + 5:num/raw <- get *e, cursor-column:offset + ] + screen-should-contain [ + . . + .#? . + .┈┈┈┈┈ . + . . + ] + memory-should-contain [ + 4 <- 1 + 5 <- 3 + ] + check-trace-count-for-label 5, [print-character] +] + +scenario editor-comments-at-start-of-contents [ + local-scope + assume-screen 10/width, 5/height + e:&:editor <- new-editor [ab], 0/left, 10/right + editor-render screen, e + $clear-trace + assume-console [ + press ctrl-slash + ] + run [ + editor-event-loop screen, console, e + 4:num/raw <- get *e, cursor-row:offset + 5:num/raw <- get *e, cursor-column:offset + ] + screen-should-contain [ + . . + .#? ab . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] + memory-should-contain [ + 4 <- 1 + 5 <- 3 + ] + check-trace-count-for-label 10, [print-character] +] + +scenario editor-comments-at-end-of-contents [ + local-scope + assume-screen 10/width, 5/height + e:&:editor <- new-editor [ab], 0/left, 10/right + editor-render screen, e + $clear-trace + assume-console [ + left-click 1, 7 + press ctrl-slash + ] + run [ + editor-event-loop screen, console, e + 4:num/raw <- get *e, cursor-row:offset + 5:num/raw <- get *e, cursor-column:offset + ] + screen-should-contain [ + . . + .#? ab . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] + memory-should-contain [ + 4 <- 1 + 5 <- 5 + ] + check-trace-count-for-label 10, [print-character] + # toggle to uncomment + $clear-trace + assume-console [ + press ctrl-slash + ] + run [ + editor-event-loop screen, console, e + 4:num/raw <- get *e, cursor-row:offset + 5:num/raw <- get *e, cursor-column:offset + ] + screen-should-contain [ + . . + .ab . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] + check-trace-count-for-label 10, [print-character] +] + +scenario editor-comments-almost-wrapping-line [ + local-scope + assume-screen 10/width, 5/height + # editor starts out with a non-wrapping line + e:&:editor <- new-editor [abcd], 0/left, 5/right + editor-render screen, e + screen-should-contain [ + . . + .abcd . + .┈┈┈┈┈ . + . . + ] + $clear-trace + # on commenting the line is now wrapped + assume-console [ + left-click 1, 7 + press ctrl-slash + ] + run [ + editor-event-loop screen, console, e + ] + screen-should-contain [ + . . + .#? a↩ . + .bcd . + .┈┈┈┈┈ . + . . + ] +] + +scenario editor-uncomments-just-wrapping-line [ + local-scope + assume-screen 10/width, 5/height + # editor starts out with a comment that wraps the line + e:&:editor <- new-editor [#? ab], 0/left, 5/right + editor-render screen, e + screen-should-contain [ + . . + .#? a↩ . + .b . + .┈┈┈┈┈ . + . . + ] + $clear-trace + # on uncommenting the line is no longer wrapped + assume-console [ + left-click 1, 7 + press ctrl-slash + ] + run [ + editor-event-loop screen, console, e + ] + screen-should-contain [ + . . + .ab . + .┈┈┈┈┈ . + . . + ] +] |