diff options
-rw-r--r-- | 082persist.cc | 7 | ||||
-rw-r--r-- | edit/005-sandbox.mu | 5 | ||||
-rw-r--r-- | edit/006-sandbox-edit.mu | 9 | ||||
-rw-r--r-- | edit/007-sandbox-delete.mu | 4 | ||||
-rw-r--r-- | edit/008-sandbox-test.mu | 2 | ||||
-rw-r--r-- | edit/009-sandbox-trace.mu | 8 | ||||
-rw-r--r-- | sandbox/001-editor.mu | 500 | ||||
-rw-r--r-- | sandbox/002-typing.mu | 1053 | ||||
-rw-r--r-- | sandbox/003-shortcuts.mu | 3069 | ||||
-rw-r--r-- | sandbox/004-programming-environment.mu | 360 | ||||
-rw-r--r-- | sandbox/005-sandbox.mu | 456 | ||||
-rw-r--r-- | sandbox/006-sandbox-edit.mu | 176 | ||||
-rw-r--r-- | sandbox/007-sandbox-delete.mu | 108 | ||||
-rw-r--r-- | sandbox/008-sandbox-test.mu | 94 | ||||
-rw-r--r-- | sandbox/009-sandbox-trace.mu | 202 | ||||
-rw-r--r-- | sandbox/010-warnings.mu | 203 | ||||
-rw-r--r-- | sandbox/011-editor-undo.mu | 2077 | ||||
-rw-r--r-- | sandbox/Readme.md | 15 | ||||
-rwxr-xr-x | sandbox/mu_run | 15 | ||||
-rw-r--r-- | sandbox/tmux.conf | 2 |
20 files changed, 8346 insertions, 19 deletions
diff --git a/082persist.cc b/082persist.cc index 9d2a6d63..6ab6ea42 100644 --- a/082persist.cc +++ b/082persist.cc @@ -23,7 +23,12 @@ case RESTORE: { raise << current_recipe_name() << ": first ingredient of 'restore' should be a string, but got " << current_instruction().ingredients.at(0).to_string() << '\n' << end(); break; } - if (Current_scenario) break; // do nothing in tests + if (Current_scenario) { + // do nothing in tests + products.resize(1); + products.at(0).push_back(0); + break; + } string contents = slurp("lesson/"+filename); products.resize(1); if (contents.empty()) diff --git a/edit/005-sandbox.mu b/edit/005-sandbox.mu index adf39f6a..5d2e575e 100644 --- a/edit/005-sandbox.mu +++ b/edit/005-sandbox.mu @@ -405,10 +405,7 @@ z:number <- add 2, 2 assume-console [ press F4 ] - run [ - event-loop screen:address, console:address, 3:address:programming-environment-data - ] - # check that screen prints the results + event-loop screen:address, console:address, 3:address:programming-environment-data screen-should-contain [ . run (F4) . . ┊ . diff --git a/edit/006-sandbox-edit.mu b/edit/006-sandbox-edit.mu index e43e09b2..90be2de7 100644 --- a/edit/006-sandbox-edit.mu +++ b/edit/006-sandbox-edit.mu @@ -63,8 +63,7 @@ recipe foo [ ] after <global-touch> [ - # right side of screen and below sandbox editor? pop appropriate sandbox - # contents back into sandbox editor provided it's empty + # below sandbox editor? pop appropriate sandbox contents back into sandbox editor { sandbox-left-margin:number <- get *current-sandbox, left:offset click-column:number <- get *t, column:offset @@ -77,7 +76,7 @@ after <global-touch> [ below-sandbox-editor?:boolean <- greater-or-equal click-row, first-sandbox-begins break-unless below-sandbox-editor? empty-sandbox-editor?:boolean <- empty-editor? current-sandbox - break-unless empty-sandbox-editor? # make the user hit F4 before editing a new sandbox + break-unless empty-sandbox-editor? # don't clobber existing contents # identify the sandbox to edit and remove it from the sandbox list sandbox:address:sandbox-data <- extract-sandbox env, click-row text:address:array:character <- get *sandbox, data:offset @@ -139,9 +138,7 @@ scenario sandbox-with-print-can-be-edited [ assume-console [ press F4 ] - run [ - event-loop screen:address, console:address, 3:address:programming-environment-data - ] + event-loop screen:address, console:address, 3:address:programming-environment-data screen-should-contain [ . run (F4) . . ┊ . diff --git a/edit/007-sandbox-delete.mu b/edit/007-sandbox-delete.mu index b4b317e0..12c84adf 100644 --- a/edit/007-sandbox-delete.mu +++ b/edit/007-sandbox-delete.mu @@ -14,9 +14,7 @@ scenario deleting-sandboxes [ type [add 2, 2] press F4 ] - run [ - event-loop screen:address, console:address, 3:address:programming-environment-data - ] + event-loop screen:address, console:address, 3:address:programming-environment-data screen-should-contain [ . run (F4) . . ┊ . diff --git a/edit/008-sandbox-test.mu b/edit/008-sandbox-test.mu index cdad7d3c..63da5822 100644 --- a/edit/008-sandbox-test.mu +++ b/edit/008-sandbox-test.mu @@ -83,7 +83,7 @@ recipe foo [ # clicks on sandbox responses save it as 'expected' after <global-touch> [ - # right side of screen? check if it's inside the output of any sandbox + # check if it's inside the output of any sandbox { sandbox-left-margin:number <- get *current-sandbox, left:offset click-column:number <- get *t, column:offset diff --git a/edit/009-sandbox-trace.mu b/edit/009-sandbox-trace.mu index 1091981e..a7a9b2e0 100644 --- a/edit/009-sandbox-trace.mu +++ b/edit/009-sandbox-trace.mu @@ -24,7 +24,7 @@ recipe foo [ .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊━━━━━━━━━━━━━━━━━━━. . ┊ . ] - # click on the 'foo' line in the sandbox + # click on the code in the sandbox assume-console [ left-click 4, 21 ] @@ -99,14 +99,14 @@ recipe foo [ .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊━━━━━━━━━━━━━━━━━━━. . ┊ . ] - # click on the 'foo' line in the sandbox + # click on the code in the sandbox assume-console [ left-click 4, 21 ] run [ event-loop screen:address, console:address, 3:address:programming-environment-data ] - # trace now printed + # trace now printed above result screen-should-contain [ . run (F4) . . ┊ . @@ -138,7 +138,7 @@ recipe! update-sandbox [ # clicks on sandbox code toggle its display-trace? flag after <global-touch> [ - # right side of screen? check if it's inside the code of any sandbox + # check if it's inside the code of any sandbox { sandbox-left-margin:number <- get *current-sandbox, left:offset click-column:number <- get *t, column:offset diff --git a/sandbox/001-editor.mu b/sandbox/001-editor.mu new file mode 100644 index 00000000..8f41c5c1 --- /dev/null +++ b/sandbox/001-editor.mu @@ -0,0 +1,500 @@ +## the basic editor data structure, and how it displays text to the screen + +# temporary main for this layer: just render the given string at the given +# screen dimensions, then stop +recipe! main [ + local-scope + text:address:array:character <- next-ingredient + open-console + hide-screen 0/screen + new-editor text, 0/screen, 0/left, 5/right + show-screen 0/screen + wait-for-event 0/console + close-console +] + +scenario editor-initially-prints-string-to-screen [ + assume-screen 10/width, 5/height + run [ + 1:address:array:character <- new [abc] + new-editor 1:address:array:character, screen:address, 0/left, 10/right + ] + screen-should-contain [ + # top line of screen reserved for menu + . . + .abc . + . . + ] +] + +container editor-data [ + # editable text: doubly linked list of characters (head contains a special sentinel) + data:address:duplex-list:character + top-of-screen:address:duplex-list:character + bottom-of-screen:address:duplex-list:character + # location before cursor inside data + before-cursor:address:duplex-list:character + + # raw bounds of display area on screen + # always displays from row 1 (leaving row 0 for a menu) and at most until bottom of screen + left:number + right:number + # raw screen coordinates of cursor + cursor-row:number + cursor-column:number +] + +# editor:address, screen <- new-editor s:address:array:character, screen:address, left:number, right:number +# creates a new editor widget and renders its initial appearance to screen. +# top/left/right constrain the screen area available to the new editor. +# right is exclusive. +recipe new-editor [ + local-scope + s:address:array:character <- next-ingredient + screen:address <- next-ingredient + # no clipping of bounds + left:number <- next-ingredient + right:number <- next-ingredient + right <- subtract right, 1 + result:address:editor-data <- new editor-data:type + # initialize screen-related fields + x:address:number <- get-address *result, left:offset + *x <- copy left + x <- get-address *result, right:offset + *x <- copy right + # initialize cursor + x <- get-address *result, cursor-row:offset + *x <- copy 1/top + x <- get-address *result, cursor-column:offset + *x <- copy left + init:address:address:duplex-list <- get-address *result, data:offset + *init <- push-duplex 167/§, 0/tail + top-of-screen:address:address:duplex-list <- get-address *result, top-of-screen:offset + *top-of-screen <- copy *init + y:address:address:duplex-list <- get-address *result, before-cursor:offset + *y <- copy *init + result <- insert-text result, s + # initialize cursor to top of screen + y <- get-address *result, before-cursor:offset + *y <- copy *init + # initial render to screen, just for some old tests + _, _, screen, result <- render screen, result + <editor-initialization> + reply result +] + +recipe insert-text [ + local-scope + editor:address:editor-data <- next-ingredient + text:address:array:character <- next-ingredient + # early exit if text is empty + reply-unless text, editor/same-as-ingredient:0 + len:number <- length *text + reply-unless len, editor/same-as-ingredient:0 + idx:number <- copy 0 + # now we can start appending the rest, character by character + curr:address:duplex-list <- get *editor, data:offset + { + done?:boolean <- greater-or-equal idx, len + break-if done? + c:character <- index *text, idx + insert-duplex c, curr + # next iter + curr <- next-duplex curr + idx <- add idx, 1 + loop + } + reply editor/same-as-ingredient:0 +] + +scenario editor-initializes-without-data [ + assume-screen 5/width, 3/height + run [ + 1:address:editor-data <- new-editor 0/data, screen:address, 2/left, 5/right + 2:editor-data <- copy *1:address:editor-data + ] + memory-should-contain [ + # 2 (data) <- just the § sentinel + # 3 (top of screen) <- the § sentinel + 4 <- 0 # bottom-of-screen; null since text fits on screen + # 5 (before cursor) <- the § sentinel + 6 <- 2 # left + 7 <- 4 # right (inclusive) + 8 <- 1 # cursor row + 9 <- 2 # cursor column + ] + screen-should-contain [ + . . + . . + . . + ] +] + +# last-row:number, last-column:number, screen, editor <- render screen:address, editor:address:editor-data +# +# Assumes cursor should be at coordinates (cursor-row, cursor-column) and +# updates before-cursor to match. Might also move coordinates if they're +# outside text. +recipe render [ + local-scope + screen:address <- next-ingredient + editor:address:editor-data <- next-ingredient + reply-unless editor, 1/top, 0/left, screen/same-as-ingredient:0, editor/same-as-ingredient:1 + left:number <- get *editor, left:offset + screen-height:number <- screen-height screen + right:number <- get *editor, right:offset + # traversing editor + curr:address:duplex-list <- get *editor, top-of-screen:offset + prev:address:duplex-list <- copy curr # just in case curr becomes null and we can't compute prev-duplex + curr <- next-duplex curr + # traversing screen + +render-loop-initialization + color:number <- copy 7/white + row:number <- copy 1/top + column:number <- copy left + cursor-row:address:number <- get-address *editor, cursor-row:offset + cursor-column:address:number <- get-address *editor, cursor-column:offset + before-cursor:address:address:duplex-list <- get-address *editor, before-cursor:offset + screen <- move-cursor screen, row, column + { + +next-character + break-unless curr + off-screen?:boolean <- greater-or-equal row, screen-height + break-if off-screen? + # update editor-data.before-cursor + # Doing so at the start of each iteration ensures it stays one step behind + # the current character. + { + at-cursor-row?:boolean <- equal row, *cursor-row + break-unless at-cursor-row? + at-cursor?:boolean <- equal column, *cursor-column + break-unless at-cursor? + *before-cursor <- copy prev + } + c:character <- get *curr, value:offset + <character-c-received> + { + # newline? move to left rather than 0 + newline?:boolean <- equal c, 10/newline + break-unless newline? + # adjust cursor if necessary + { + at-cursor-row?:boolean <- equal row, *cursor-row + break-unless at-cursor-row? + left-of-cursor?:boolean <- lesser-than column, *cursor-column + break-unless left-of-cursor? + *cursor-column <- copy column + *before-cursor <- prev-duplex curr + } + # clear rest of line in this window + clear-line-delimited screen, column, right + # skip to next line + row <- add row, 1 + column <- copy left + screen <- move-cursor screen, row, column + curr <- next-duplex curr + prev <- next-duplex prev + loop +next-character:label + } + { + # at right? wrap. even if there's only one more letter left; we need + # room for clicking on the cursor after it. + at-right?:boolean <- equal column, right + break-unless at-right? + # print wrap icon + print-character screen, 8617/loop-back-to-left, 245/grey + column <- copy left + row <- add row, 1 + screen <- move-cursor screen, row, column + # don't increment curr + loop +next-character:label + } + print-character screen, c, color + curr <- next-duplex curr + prev <- next-duplex prev + column <- add column, 1 + loop + } + # save first character off-screen + bottom-of-screen:address:address:duplex-list <- get-address *editor, bottom-of-screen:offset + *bottom-of-screen <- copy curr + # is cursor to the right of the last line? move to end + { + at-cursor-row?:boolean <- equal row, *cursor-row + cursor-outside-line?:boolean <- lesser-or-equal column, *cursor-column + before-cursor-on-same-line?:boolean <- and at-cursor-row?, cursor-outside-line? + above-cursor-row?:boolean <- lesser-than row, *cursor-row + before-cursor?:boolean <- or before-cursor-on-same-line?, above-cursor-row? + break-unless before-cursor? + *cursor-row <- copy row + *cursor-column <- copy column + *before-cursor <- copy prev + } + reply row, column, screen/same-as-ingredient:0, editor/same-as-ingredient:1 +] + +recipe clear-line-delimited [ + local-scope + screen:address <- next-ingredient + column:number <- next-ingredient + right:number <- next-ingredient + { + done?:boolean <- greater-than column, right + break-if done? + print-character screen, 32/space + column <- add column, 1 + loop + } +] + +recipe clear-screen-from [ + local-scope + screen:address <- next-ingredient + row:number <- next-ingredient + column:number <- next-ingredient + left:number <- next-ingredient + right:number <- next-ingredient + # if it's the real screen, use the optimized primitive + { + break-if screen + clear-display-from row, column, left, right + reply screen/same-as-ingredient:0 + } + # if not, go the slower route + screen <- move-cursor screen, row, column + clear-line-delimited screen, column, right + clear-rest-of-screen screen, row, left, right + reply screen/same-as-ingredient:0 +] + +recipe clear-rest-of-screen [ + local-scope + screen:address <- next-ingredient + row:number <- next-ingredient + left:number <- next-ingredient + right:number <- next-ingredient + row <- add row, 1 + screen <- move-cursor screen, row, left + screen-height:number <- screen-height screen + { + at-bottom-of-screen?:boolean <- greater-or-equal row, screen-height + break-if at-bottom-of-screen? + screen <- move-cursor screen, row, left + clear-line-delimited screen, left, right + row <- add row, 1 + loop + } +] + +scenario editor-initially-prints-multiple-lines [ + assume-screen 5/width, 5/height + run [ + s:address:array:character <- new [abc +def] + new-editor s:address:array:character, screen:address, 0/left, 5/right + ] + screen-should-contain [ + . . + .abc . + .def . + . . + ] +] + +scenario editor-initially-handles-offsets [ + assume-screen 5/width, 5/height + run [ + s:address:array:character <- new [abc] + new-editor s:address:array:character, screen:address, 1/left, 5/right + ] + screen-should-contain [ + . . + . abc . + . . + ] +] + +scenario editor-initially-prints-multiple-lines-at-offset [ + assume-screen 5/width, 5/height + run [ + s:address:array:character <- new [abc +def] + new-editor s:address:array:character, screen:address, 1/left, 5/right + ] + screen-should-contain [ + . . + . abc . + . def . + . . + ] +] + +scenario editor-initially-wraps-long-lines [ + assume-screen 5/width, 5/height + run [ + s:address:array:character <- new [abc def] + new-editor s:address:array:character, screen:address, 0/left, 5/right + ] + screen-should-contain [ + . . + .abc ↩. + .def . + . . + ] + screen-should-contain-in-color 245/grey [ + . . + . ↩. + . . + . . + ] +] + +scenario editor-initially-wraps-barely-long-lines [ + assume-screen 5/width, 5/height + run [ + s:address:array:character <- new [abcde] + new-editor s:address:array:character, screen:address, 0/left, 5/right + ] + # still wrap, even though the line would fit. We need room to click on the + # end of the line + screen-should-contain [ + . . + .abcd↩. + .e . + . . + ] + screen-should-contain-in-color 245/grey [ + . . + . ↩. + . . + . . + ] +] + +scenario editor-initializes-empty-text [ + assume-screen 5/width, 5/height + run [ + 1:address:array:character <- new [] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 5/right + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, cursor-column:offset + ] + screen-should-contain [ + . . + . . + . . + ] + memory-should-contain [ + 3 <- 1 # cursor row + 4 <- 0 # cursor column + ] +] + +# just a little color for mu code + +scenario render-colors-comments [ + assume-screen 5/width, 5/height + run [ + s:address:array:character <- new [abc +# de +f] + new-editor s:address:array:character, screen:address, 0/left, 5/right + ] + screen-should-contain [ + . . + .abc . + .# de . + .f . + . . + ] + screen-should-contain-in-color 12/lightblue, [ + . . + . . + .# de . + . . + . . + ] + screen-should-contain-in-color 7/white, [ + . . + .abc . + . . + .f . + . . + ] +] + +after <character-c-received> [ + color <- get-color color, c +] + +# color <- get-color color:number, c:character +# so far the previous color is all the information we need; that may change +recipe get-color [ + local-scope + color:number <- next-ingredient + c:character <- next-ingredient + color-is-white?:boolean <- equal color, 7/white + # if color is white and next character is '#', switch color to blue + { + break-unless color-is-white? + starting-comment?:boolean <- equal c, 35/# + break-unless starting-comment? + trace 90, [app], [switch color back to blue] + color <- copy 12/lightblue + jump +exit:label + } + # if color is blue and next character is newline, switch color to white + { + color-is-blue?:boolean <- equal color, 12/lightblue + break-unless color-is-blue? + ending-comment?:boolean <- equal c, 10/newline + break-unless ending-comment? + trace 90, [app], [switch color back to white] + color <- copy 7/white + jump +exit:label + } + # if color is white (no comments) and next character is '<', switch color to red + { + break-unless color-is-white? + starting-assignment?:boolean <- equal c, 60/< + break-unless starting-assignment? + color <- copy 1/red + jump +exit:label + } + # if color is red and next character is space, switch color to white + { + color-is-red?:boolean <- equal color, 1/red + break-unless color-is-red? + ending-assignment?:boolean <- equal c, 32/space + break-unless ending-assignment? + color <- copy 7/white + jump +exit:label + } + # otherwise no change + +exit + reply color +] + +scenario render-colors-assignment [ + assume-screen 8/width, 5/height + run [ + s:address:array:character <- new [abc +d <- e +f] + new-editor s:address:array:character, screen:address, 0/left, 8/right + ] + screen-should-contain [ + . . + .abc . + .d <- e . + .f . + . . + ] + screen-should-contain-in-color 1/red, [ + . . + . . + . <- . + . . + . . + ] +] diff --git a/sandbox/002-typing.mu b/sandbox/002-typing.mu new file mode 100644 index 00000000..463b413d --- /dev/null +++ b/sandbox/002-typing.mu @@ -0,0 +1,1053 @@ +## handling events from the keyboard, mouse, touch screen, ... + +# temporary main: interactive editor +# hit ctrl-c to exit +recipe! main [ + local-scope + text:address:array:character <- next-ingredient + open-console + editor:address:editor-data <- new-editor text, 0/screen, 5/left, 45/right + editor-event-loop 0/screen, 0/console, editor + close-console +] + +recipe editor-event-loop [ + local-scope + screen:address <- next-ingredient + console:address <- next-ingredient + editor:address:editor-data <- next-ingredient + { + # looping over each (keyboard or touch) event as it occurs + +next-event + cursor-row:number <- get *editor, cursor-row:offset + cursor-column:number <- get *editor, cursor-column:offset + screen <- move-cursor screen, cursor-row, cursor-column + e:event, console:address, found?:boolean, quit?:boolean <- read-event console + loop-unless found? + break-if quit? # only in tests + trace 10, [app], [next-event] + # 'touch' event + t:address:touch-event <- maybe-convert e, touch:variant + { + break-unless t + move-cursor-in-editor screen, editor, *t + loop +next-event:label + } + # keyboard events + { + break-if t + screen, editor, go-render?:boolean <- handle-keyboard-event screen, editor, e + { + break-unless go-render? + screen <- editor-render screen, editor + } + } + loop + } +] + +# process click, return if it was on current editor +recipe move-cursor-in-editor [ + local-scope + screen:address <- next-ingredient + editor:address:editor-data <- next-ingredient + t:touch-event <- next-ingredient + reply-unless editor, 0/false + click-row:number <- get t, row:offset + reply-unless click-row, 0/false # ignore clicks on 'menu' + click-column:number <- get t, column:offset + left:number <- get *editor, left:offset + too-far-left?:boolean <- lesser-than click-column, left + reply-if too-far-left?, 0/false + right:number <- get *editor, right:offset + too-far-right?:boolean <- greater-than click-column, right + reply-if too-far-right?, 0/false + # position cursor + <move-cursor-begin> + editor <- snap-cursor screen, editor, click-row, click-column + undo-coalesce-tag:number <- copy 0/never + <move-cursor-end> + # gain focus + reply 1/true +] + +# editor <- snap-cursor screen:address, editor:address:editor-data, target-row:number, target-column:number +# +# Variant of 'render' that only moves the cursor (coordinates and +# before-cursor). If it's past the end of a line, it 'slides' it left. If it's +# past the last line it positions at end of last line. +recipe snap-cursor [ + local-scope + screen:address <- next-ingredient + editor:address:editor-data <- next-ingredient + target-row:number <- next-ingredient + target-column:number <- next-ingredient + reply-unless editor, 1/top, editor/same-as-ingredient:1 + left:number <- get *editor, left:offset + right:number <- get *editor, right:offset + screen-height:number <- screen-height screen + # count newlines until screen row + curr:address:duplex-list <- get *editor, top-of-screen:offset + prev:address:duplex-list <- copy curr # just in case curr becomes null and we can't compute prev-duplex + curr <- next-duplex curr + row:number <- copy 1/top + column:number <- copy left + cursor-row:address:number <- get-address *editor, cursor-row:offset + *cursor-row <- copy target-row + cursor-column:address:number <- get-address *editor, cursor-column:offset + *cursor-column <- copy target-column + before-cursor:address:address:duplex-list <- get-address *editor, before-cursor:offset + { + +next-character + break-unless curr + off-screen?:boolean <- greater-or-equal row, screen-height + break-if off-screen? + # update editor-data.before-cursor + # Doing so at the start of each iteration ensures it stays one step behind + # the current character. + { + at-cursor-row?:boolean <- equal row, *cursor-row + break-unless at-cursor-row? + at-cursor?:boolean <- equal column, *cursor-column + break-unless at-cursor? + *before-cursor <- copy prev + } + c:character <- get *curr, value:offset + { + # newline? move to left rather than 0 + newline?:boolean <- equal c, 10/newline + break-unless newline? + # adjust cursor if necessary + { + at-cursor-row?:boolean <- equal row, *cursor-row + break-unless at-cursor-row? + left-of-cursor?:boolean <- lesser-than column, *cursor-column + break-unless left-of-cursor? + *cursor-column <- copy column + *before-cursor <- copy prev + } + # skip to next line + row <- add row, 1 + column <- copy left + curr <- next-duplex curr + prev <- next-duplex prev + loop +next-character:label + } + { + # at right? wrap. even if there's only one more letter left; we need + # room for clicking on the cursor after it. + at-right?:boolean <- equal column, right + break-unless at-right? + column <- copy left + row <- add row, 1 + # don't increment curr/prev + loop +next-character:label + } + curr <- next-duplex curr + prev <- next-duplex prev + column <- add column, 1 + loop + } + # is cursor to the right of the last line? move to end + { + at-cursor-row?:boolean <- equal row, *cursor-row + cursor-outside-line?:boolean <- lesser-or-equal column, *cursor-column + before-cursor-on-same-line?:boolean <- and at-cursor-row?, cursor-outside-line? + above-cursor-row?:boolean <- lesser-than row, *cursor-row + before-cursor?:boolean <- or before-cursor-on-same-line?, above-cursor-row? + break-unless before-cursor? + *cursor-row <- copy row + *cursor-column <- copy column + *before-cursor <- copy prev + } + reply editor/same-as-ingredient:1 +] + +# screen, editor, go-render?:boolean <- handle-keyboard-event screen:address, editor:address:editor-data, e:event +# Process an event 'e' and try to minimally update the screen. +# Set 'go-render?' to true to indicate the caller must perform a non-minimal update. +recipe handle-keyboard-event [ + local-scope + screen:address <- next-ingredient + editor:address:editor-data <- next-ingredient + e:event <- next-ingredient + reply-unless editor, screen/same-as-ingredient:0, editor/same-as-ingredient:1, 0/no-more-render + screen-width:number <- screen-width screen + screen-height:number <- screen-height screen + left:number <- get *editor, left:offset + right:number <- get *editor, right:offset + before-cursor:address:address:duplex-list <- get-address *editor, before-cursor:offset + cursor-row:address:number <- get-address *editor, cursor-row:offset + cursor-column:address:number <- get-address *editor, cursor-column:offset + save-row:number <- copy *cursor-row + save-column:number <- copy *cursor-column + # character + { + c:address:character <- maybe-convert e, text:variant + break-unless c + trace 10, [app], [handle-keyboard-event: special character] + # exceptions for special characters go here + <handle-special-character> + # ignore any other special characters + regular-character?:boolean <- greater-or-equal *c, 32/space + reply-unless regular-character?, screen/same-as-ingredient:0, editor/same-as-ingredient:1, 0/no-more-render + # otherwise type it in + <insert-character-begin> + editor, screen, go-render?:boolean <- insert-at-cursor editor, *c, screen + <insert-character-end> + reply screen/same-as-ingredient:0, editor/same-as-ingredient:1, go-render? + } + # special key to modify the text or move the cursor + k:address:number <- maybe-convert e:event, keycode:variant + assert k, [event was of unknown type; neither keyboard nor mouse] + # handlers for each special key will go here + <handle-special-key> + reply screen/same-as-ingredient:0, editor/same-as-ingredient:1, 1/go-render +] + +recipe insert-at-cursor [ + local-scope + editor:address:editor-data <- next-ingredient + c:character <- next-ingredient + screen:address <- next-ingredient + before-cursor:address:address:duplex-list <- get-address *editor, before-cursor:offset + insert-duplex c, *before-cursor + *before-cursor <- next-duplex *before-cursor + cursor-row:address:number <- get-address *editor, cursor-row:offset + cursor-column:address:number <- get-address *editor, cursor-column:offset + left:number <- get *editor, left:offset + right:number <- get *editor, right:offset + save-row:number <- copy *cursor-row + save-column:number <- copy *cursor-column + screen-width:number <- screen-width screen + screen-height:number <- screen-height screen + # occasionally we'll need to mess with the cursor + <insert-character-special-case> + # but mostly we'll just move the cursor right + *cursor-column <- add *cursor-column, 1 + next:address:duplex-list <- next-duplex *before-cursor + { + # at end of all text? no need to scroll? just print the character and leave + at-end?:boolean <- equal next, 0/null + break-unless at-end? + bottom:number <- subtract screen-height, 1 + at-bottom?:boolean <- equal save-row, bottom + at-right?:boolean <- equal save-column, right + overflow?:boolean <- and at-bottom?, at-right? + break-if overflow? + move-cursor screen, save-row, save-column + print-character screen, c + reply editor/same-as-ingredient:0, screen/same-as-ingredient:2, 0/no-more-render + } + { + # not at right margin? print the character and rest of line + break-unless next + at-right?:boolean <- greater-or-equal *cursor-column, screen-width + break-if at-right? + curr:address:duplex-list <- copy *before-cursor + move-cursor screen, save-row, save-column + curr-column:number <- copy save-column + { + # hit right margin? give up and let caller render + at-right?:boolean <- greater-than curr-column, right + reply-if at-right?, editor/same-as-ingredient:0, screen/same-as-ingredient:2, 1/go-render + break-unless curr + # newline? done. + currc:character <- get *curr, value:offset + at-newline?:boolean <- equal currc, 10/newline + break-if at-newline? + print-character screen, currc + curr-column <- add curr-column, 1 + curr <- next-duplex curr + loop + } + reply editor/same-as-ingredient:0, screen/same-as-ingredient:2, 0/no-more-render + } + reply editor/same-as-ingredient:0, screen/same-as-ingredient:2, 1/go-render +] + +# helper for tests +recipe editor-render [ + local-scope + screen:address <- next-ingredient + editor:address:editor-data <- next-ingredient + left:number <- get *editor, left:offset + right:number <- get *editor, right:offset + row:number, column:number <- render screen, editor + clear-line-delimited screen, column, right + row <- add row, 1 + draw-horizontal screen, row, left, right, 9480/horizontal-dotted + row <- add row, 1 + clear-screen-from screen, row, left, left, right +] + +scenario editor-handles-empty-event-queue [ + assume-screen 10/width, 5/height + 1:address:array:character <- new [abc] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + editor-render screen, 2:address:editor-data + assume-console [] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + screen-should-contain [ + . . + .abc . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] +] + +scenario editor-handles-mouse-clicks [ + assume-screen 10/width, 5/height + 1:address:array:character <- new [abc] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + editor-render screen, 2:address:editor-data + $clear-trace + assume-console [ + left-click 1, 1 # on the 'b' + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, cursor-column:offset + ] + screen-should-contain [ + . . + .abc . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] + memory-should-contain [ + 3 <- 1 # cursor is at row 0.. + 4 <- 1 # ..and column 1 + ] + check-trace-count-for-label 0, [print-character] +] + +scenario editor-handles-mouse-clicks-outside-text [ + assume-screen 10/width, 5/height + 1:address:array:character <- new [abc] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + $clear-trace + assume-console [ + left-click 1, 7 # last line, to the right of text + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, cursor-column:offset + ] + memory-should-contain [ + 3 <- 1 # cursor row + 4 <- 3 # cursor column + ] + check-trace-count-for-label 0, [print-character] +] + +scenario editor-handles-mouse-clicks-outside-text-2 [ + assume-screen 10/width, 5/height + 1:address:array:character <- new [abc +def] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + $clear-trace + assume-console [ + left-click 1, 7 # interior line, to the right of text + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, cursor-column:offset + ] + memory-should-contain [ + 3 <- 1 # cursor row + 4 <- 3 # cursor column + ] + check-trace-count-for-label 0, [print-character] +] + +scenario editor-handles-mouse-clicks-outside-text-3 [ + assume-screen 10/width, 5/height + 1:address:array:character <- new [abc +def] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + $clear-trace + assume-console [ + left-click 3, 7 # below text + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, cursor-column:offset + ] + memory-should-contain [ + 3 <- 2 # cursor row + 4 <- 3 # cursor column + ] + check-trace-count-for-label 0, [print-character] +] + +scenario editor-handles-mouse-clicks-outside-column [ + assume-screen 10/width, 5/height + 1:address:array:character <- new [abc] + # editor occupies only left half of screen + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 5/right + editor-render screen, 2:address:editor-data + $clear-trace + assume-console [ + # click on right half of screen + left-click 3, 8 + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, cursor-column:offset + ] + screen-should-contain [ + . . + .abc . + .┈┈┈┈┈ . + . . + ] + memory-should-contain [ + 3 <- 1 # no change to cursor row + 4 <- 0 # ..or column + ] + check-trace-count-for-label 0, [print-character] +] + +scenario editor-handles-mouse-clicks-in-menu-area [ + assume-screen 10/width, 5/height + 1:address:array:character <- new [abc] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 5/right + editor-render screen, 2:address:editor-data + $clear-trace + assume-console [ + # click on first, 'menu' row + left-click 0, 3 + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, cursor-column:offset + ] + # no change to cursor + memory-should-contain [ + 3 <- 1 + 4 <- 0 + ] +] + +scenario editor-inserts-characters-into-empty-editor [ + assume-screen 10/width, 5/height + 1:address:array:character <- new [] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 5/right + editor-render screen, 2:address:editor-data + $clear-trace + assume-console [ + type [abc] + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + screen-should-contain [ + . . + .abc . + .┈┈┈┈┈ . + . . + ] + check-trace-count-for-label 3, [print-character] +] + +scenario editor-inserts-characters-at-cursor [ + assume-screen 10/width, 5/height + 1:address:array:character <- new [abc] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + editor-render screen, 2:address:editor-data + $clear-trace + # type two letters at different places + assume-console [ + type [0] + left-click 1, 2 + type [d] + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + screen-should-contain [ + . . + .0adbc . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] + check-trace-count-for-label 7, [print-character] # 4 for first letter, 3 for second +] + +scenario editor-inserts-characters-at-cursor-2 [ + assume-screen 10/width, 5/height + 1:address:array:character <- new [abc] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + editor-render screen, 2:address:editor-data + $clear-trace + assume-console [ + left-click 1, 5 # right of last line + type [d] + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + screen-should-contain [ + . . + .abcd . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] + check-trace-count-for-label 1, [print-character] +] + +scenario editor-inserts-characters-at-cursor-5 [ + assume-screen 10/width, 5/height + 1:address:array:character <- new [abc +d] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + editor-render screen, 2:address:editor-data + $clear-trace + assume-console [ + left-click 1, 5 # right of non-last line + type [e] + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + screen-should-contain [ + . . + .abce . + .d . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] + check-trace-count-for-label 1, [print-character] +] + +scenario editor-inserts-characters-at-cursor-3 [ + assume-screen 10/width, 5/height + 1:address:array:character <- new [abc] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + editor-render screen, 2:address:editor-data + $clear-trace + assume-console [ + left-click 3, 5 # below all text + type [d] + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + screen-should-contain [ + . . + .abcd . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] + check-trace-count-for-label 1, [print-character] +] + +scenario editor-inserts-characters-at-cursor-4 [ + assume-screen 10/width, 5/height + 1:address:array:character <- new [abc +d] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + editor-render screen, 2:address:editor-data + $clear-trace + assume-console [ + left-click 3, 5 # below all text + type [e] + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + screen-should-contain [ + . . + .abc . + .de . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] + check-trace-count-for-label 1, [print-character] +] + +scenario editor-inserts-characters-at-cursor-6 [ + assume-screen 10/width, 5/height + 1:address:array:character <- new [abc +d] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + editor-render screen, 2:address:editor-data + $clear-trace + assume-console [ + left-click 3, 5 # below all text + type [ef] + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + screen-should-contain [ + . . + .abc . + .def . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] + check-trace-count-for-label 2, [print-character] +] + +scenario editor-moves-cursor-after-inserting-characters [ + assume-screen 10/width, 5/height + 1:address:array:character <- new [ab] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 5/right + editor-render screen, 2:address:editor-data + assume-console [ + type [01] + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + screen-should-contain [ + . . + .01ab . + .┈┈┈┈┈ . + . . + ] +] + +# if the cursor reaches the right margin, wrap the line + +scenario editor-wraps-line-on-insert [ + assume-screen 5/width, 5/height + 1:address:array:character <- new [abc] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 5/right + editor-render screen, 2:address:editor-data + # type a letter + assume-console [ + type [e] + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + # no wrap yet + screen-should-contain [ + . . + .eabc . + .┈┈┈┈┈. + . . + . . + ] + # type a second letter + assume-console [ + type [f] + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + # now wrap + screen-should-contain [ + . . + .efab↩. + .c . + .┈┈┈┈┈. + . . + ] +] + +scenario editor-wraps-line-on-insert-2 [ + # create an editor with some text + assume-screen 10/width, 5/height + 1:address:array:character <- new [abcdefg +defg] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 5/right + editor-render screen, 2:address:editor-data + # type more text at the start + assume-console [ + left-click 3, 0 + type [abc] + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, cursor-column:offset + ] + # cursor is not wrapped + memory-should-contain [ + 3 <- 3 + 4 <- 3 + ] + # but line is wrapped + screen-should-contain [ + . . + .abcd↩ . + .efg . + .abcd↩ . + .efg . + ] +] + +after <insert-character-special-case> [ + # if the line wraps at the cursor, move cursor to start of next row + { + # if we're at the column just before the wrap indicator + wrap-column:number <- subtract right, 1 + at-wrap?:boolean <- greater-or-equal *cursor-column, wrap-column + break-unless at-wrap? + *cursor-column <- subtract *cursor-column, wrap-column + *cursor-column <- add *cursor-column, left + *cursor-row <- add *cursor-row, 1 + # if we're out of the screen, scroll down + { + below-screen?:boolean <- greater-or-equal *cursor-row, screen-height + break-unless below-screen? + <scroll-down> + } + reply editor/same-as-ingredient:0, screen/same-as-ingredient:2, 1/go-render + } +] + +scenario editor-wraps-cursor-after-inserting-characters [ + assume-screen 10/width, 5/height + 1:address:array:character <- new [abcde] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 5/right + assume-console [ + left-click 1, 4 # line is full; no wrap icon yet + type [f] + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, cursor-column:offset + ] + screen-should-contain [ + . . + .abcd↩ . + .fe . + .┈┈┈┈┈ . + . . + ] + memory-should-contain [ + 3 <- 2 # cursor row + 4 <- 1 # cursor column + ] +] + +scenario editor-wraps-cursor-after-inserting-characters-2 [ + assume-screen 10/width, 5/height + 1:address:array:character <- new [abcde] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 5/right + assume-console [ + left-click 1, 3 # right before the wrap icon + type [f] + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, cursor-column:offset + ] + screen-should-contain [ + . . + .abcf↩ . + .de . + .┈┈┈┈┈ . + . . + ] + memory-should-contain [ + 3 <- 2 # cursor row + 4 <- 0 # cursor column + ] +] + +scenario editor-wraps-cursor-to-left-margin [ + assume-screen 10/width, 5/height + 1:address:array:character <- new [abcde] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 2/left, 7/right + assume-console [ + left-click 1, 5 # line is full; no wrap icon yet + type [01] + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, cursor-column:offset + ] + screen-should-contain [ + . . + . abc0↩ . + . 1de . + . ┈┈┈┈┈ . + . . + ] + memory-should-contain [ + 3 <- 2 # cursor row + 4 <- 3 # cursor column + ] +] + +# if newline, move cursor to start of next line, and maybe align indent with previous line + +container editor-data [ + indent?:boolean +] + +after <editor-initialization> [ + indent?:address:boolean <- get-address *result, indent?:offset + *indent? <- copy 1/true +] + +scenario editor-moves-cursor-down-after-inserting-newline [ + assume-screen 10/width, 5/height + 1:address:array:character <- new [abc] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + assume-console [ + type [0 +1] + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + screen-should-contain [ + . . + .0 . + .1abc . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] +] + +after <handle-special-character> [ + { + newline?:boolean <- equal *c, 10/newline + break-unless newline? + <insert-enter-begin> + editor <- insert-new-line-and-indent editor, screen + <insert-enter-end> + reply screen/same-as-ingredient:0, editor/same-as-ingredient:1, 1/go-render + } +] + +recipe insert-new-line-and-indent [ + local-scope + editor:address:editor-data <- next-ingredient + screen:address <- next-ingredient + cursor-row:address:number <- get-address *editor, cursor-row:offset + cursor-column:address:number <- get-address *editor, cursor-column:offset + before-cursor:address:address:duplex-list <- get-address *editor, before-cursor:offset + left:number <- get *editor, left:offset + right:number <- get *editor, right:offset + screen-height:number <- screen-height screen + # insert newline + insert-duplex 10/newline, *before-cursor + *before-cursor <- next-duplex *before-cursor + *cursor-row <- add *cursor-row, 1 + *cursor-column <- copy left + # maybe scroll + { + below-screen?:boolean <- greater-or-equal *cursor-row, screen-height # must be equal, never greater + break-unless below-screen? + <scroll-down> + *cursor-row <- subtract *cursor-row, 1 # bring back into screen range + } + # indent if necessary + indent?:boolean <- get *editor, indent?:offset + reply-unless indent?, editor/same-as-ingredient:0, screen/same-as-ingredient:1 + d:address:duplex-list <- get *editor, data:offset + end-of-previous-line:address:duplex-list <- prev-duplex *before-cursor + indent:number <- line-indent end-of-previous-line, d + i:number <- copy 0 + { + indent-done?:boolean <- greater-or-equal i, indent + break-if indent-done? + editor, screen, go-render?:boolean <- insert-at-cursor editor, 32/space, screen + i <- add i, 1 + loop + } + reply editor/same-as-ingredient:0, screen/same-as-ingredient:1 +] + +# takes a pointer 'curr' into the doubly-linked list and its sentinel, counts +# the number of spaces at the start of the line containing 'curr'. +recipe line-indent [ + local-scope + curr:address:duplex-list <- next-ingredient + start:address:duplex-list <- next-ingredient + result:number <- copy 0 + reply-unless curr, result + at-start?:boolean <- equal curr, start + reply-if at-start?, result + { + curr <- prev-duplex curr + break-unless curr + at-start?:boolean <- equal curr, start + break-if at-start? + c:character <- get *curr, value:offset + at-newline?:boolean <- equal c, 10/newline + break-if at-newline? + # if c is a space, increment result + is-space?:boolean <- equal c, 32/space + { + break-unless is-space? + result <- add result, 1 + } + # if c is not a space, reset result + { + break-if is-space? + result <- copy 0 + } + loop + } + reply result +] + +scenario editor-moves-cursor-down-after-inserting-newline-2 [ + assume-screen 10/width, 5/height + 1:address:array:character <- new [abc] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 1/left, 10/right + assume-console [ + type [0 +1] + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + screen-should-contain [ + . . + . 0 . + . 1abc . + . ┈┈┈┈┈┈┈┈┈. + . . + ] +] + +scenario editor-clears-previous-line-completely-after-inserting-newline [ + assume-screen 10/width, 5/height + 1:address:array:character <- new [abcde] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 5/right + assume-console [ + press enter + ] + screen-should-contain [ + . . + .abcd↩ . + .e . + . . + . . + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + # line should be fully cleared + screen-should-contain [ + . . + . . + .abcd↩ . + .e . + .┈┈┈┈┈ . + ] +] + +scenario editor-inserts-indent-after-newline [ + assume-screen 10/width, 10/height + 1:address:array:character <- new [ab + cd +ef] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + # position cursor after 'cd' and hit 'newline' + assume-console [ + left-click 2, 8 + type [ +] + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, cursor-column:offset + ] + # cursor should be below start of previous line + memory-should-contain [ + 3 <- 3 # cursor row + 4 <- 2 # cursor column (indented) + ] +] + +scenario editor-skips-indent-around-paste [ + assume-screen 10/width, 10/height + 1:address:array:character <- new [ab + cd +ef] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + # position cursor after 'cd' and hit 'newline' surrounded by paste markers + assume-console [ + left-click 2, 8 + press 65507 # start paste + press enter + press 65506 # end paste + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, cursor-column:offset + ] + # cursor should be below start of previous line + memory-should-contain [ + 3 <- 3 # cursor row + 4 <- 0 # cursor column (not indented) + ] +] + +after <handle-special-key> [ + { + paste-start?:boolean <- equal *k, 65507/paste-start + break-unless paste-start? + indent?:address:boolean <- get-address *editor, indent?:offset + *indent? <- copy 0/false + reply screen/same-as-ingredient:0, editor/same-as-ingredient:1, 1/go-render + } +] + +after <handle-special-key> [ + { + paste-end?:boolean <- equal *k, 65506/paste-end + break-unless paste-end? + indent?:address:boolean <- get-address *editor, indent?:offset + *indent? <- copy 1/true + reply screen/same-as-ingredient:0, editor/same-as-ingredient:1, 1/go-render + } +] + +## helpers + +recipe draw-horizontal [ + local-scope + screen:address <- next-ingredient + row:number <- next-ingredient + x:number <- next-ingredient + right:number <- next-ingredient + style:character, style-found?:boolean <- next-ingredient + { + break-if style-found? + style <- copy 9472/horizontal + } + color:number, color-found?:boolean <- next-ingredient + { + # default color to white + break-if color-found? + color <- copy 245/grey + } + bg-color:number, bg-color-found?:boolean <- next-ingredient + { + break-if bg-color-found? + bg-color <- copy 0/black + } + screen <- move-cursor screen, row, x + { + continue?:boolean <- lesser-or-equal x, right # right is inclusive, to match editor-data semantics + break-unless continue? + print-character screen, style, color, bg-color + x <- add x, 1 + loop + } +] diff --git a/sandbox/003-shortcuts.mu b/sandbox/003-shortcuts.mu new file mode 100644 index 00000000..fe6156a1 --- /dev/null +++ b/sandbox/003-shortcuts.mu @@ -0,0 +1,3069 @@ +## 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 [ + assume-screen 10/width, 5/height + # just one character in final line + 1:address:array:character <- new [ab +cd] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 5/right + assume-console [ + press tab + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + screen-should-contain [ + . . + . ab . + .cd . + ] +] + +after <handle-special-character> [ + { + tab?:boolean <- equal *c, 9/tab + break-unless tab? + <insert-character-begin> + editor, screen, go-render?:boolean <- insert-at-cursor editor, 32/space, screen + editor, screen, go-render?:boolean <- insert-at-cursor editor, 32/space, screen + <insert-character-end> + reply screen/same-as-ingredient:0, editor/same-as-ingredient:1, 1/go-render + } +] + +# backspace - delete character before cursor + +scenario editor-handles-backspace-key [ + assume-screen 10/width, 5/height + 1:address:array:character <- new [abc] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + editor-render screen, 2:address:editor-data + $clear-trace + assume-console [ + left-click 1, 1 + press backspace + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + 4:number <- get *2:address:editor-data, cursor-row:offset + 5:number <- get *2:address:editor-data, 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?:boolean <- equal *c, 8/backspace + break-unless delete-previous-character? + <backspace-character-begin> + editor, screen, go-render?:boolean, backspaced-cell:address:duplex-list <- delete-before-cursor editor, screen + <backspace-character-end> + reply screen/same-as-ingredient:0, editor/same-as-ingredient:1, go-render? + } +] + +# editor, screen, go-render?:boolean, backspaced-cell:address:duplex-list <- delete-before-cursor editor:address:editor-data, screen +# 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. +recipe delete-before-cursor [ + local-scope + editor:address:editor-data <- next-ingredient + screen:address <- next-ingredient + before-cursor:address:address:duplex-list <- get-address *editor, before-cursor:offset + # if at start of text (before-cursor at § sentinel), return + prev:address:duplex-list <- prev-duplex *before-cursor + reply-unless prev, editor/same-as-ingredient:0, screen/same-as-ingredient:1, 0/no-more-render, 0/nothing-deleted + trace 10, [app], [delete-before-cursor] + original-row:number <- get *editor, cursor-row:offset + editor, scroll?:boolean <- move-cursor-coordinates-left editor + backspaced-cell:address:duplex-list <- copy *before-cursor + remove-duplex *before-cursor # will also neatly trim next/prev pointers in backspaced-cell/*before-cursor + *before-cursor <- copy prev + reply-if scroll?, editor/same-as-ingredient:0, 1/go-render, backspaced-cell + screen-width:number <- screen-width screen + cursor-row:number <- get *editor, cursor-row:offset + cursor-column:number <- get *editor, cursor-column:offset + # did we just backspace over a newline? + same-row?:boolean <- equal cursor-row, original-row + reply-unless same-row?, editor/same-as-ingredient:0, screen/same-as-ingredient:1, 1/go-render, backspaced-cell + left:number <- get *editor, left:offset + right:number <- get *editor, right:offset + curr:address:duplex-list <- next-duplex *before-cursor + screen <- move-cursor screen, cursor-row, cursor-column + curr-column:number <- copy cursor-column + { + # hit right margin? give up and let caller render + at-right?:boolean <- greater-or-equal curr-column, screen-width + reply-if at-right?, editor/same-as-ingredient:0, screen/same-as-ingredient:1, 1/go-render, backspaced-cell + break-unless curr + # newline? done. + currc:character <- get *curr, value:offset + at-newline?:boolean <- equal currc, 10/newline + break-if at-newline? + screen <- print-character screen, currc + curr-column <- add curr-column, 1 + curr <- next-duplex curr + loop + } + # we're guaranteed not to be at the right margin + screen <- print-character screen, 32/space + reply editor/same-as-ingredient:0, screen/same-as-ingredient:1, 0/no-more-render, backspaced-cell +] + +recipe move-cursor-coordinates-left [ + local-scope + editor:address:editor-data <- next-ingredient + before-cursor:address:duplex-list <- get *editor, before-cursor:offset + cursor-row:address:number <- get-address *editor, cursor-row:offset + cursor-column:address:number <- get-address *editor, cursor-column:offset + left:number <- get *editor, left:offset + # if not at left margin, move one character left + { + at-left-margin?:boolean <- equal *cursor-column, left + break-if at-left-margin? + trace 10, [app], [decrementing cursor column] + *cursor-column <- subtract *cursor-column, 1 + reply editor/same-as-ingredient:0, 0/no-more-render + } + # if at left margin, we must move to previous row: + top-of-screen?:boolean <- equal *cursor-row, 1 # exclude menu bar + go-render?:boolean <- copy 0/false + { + break-if top-of-screen? + *cursor-row <- subtract *cursor-row, 1 + } + { + break-unless top-of-screen? + <scroll-up> + go-render? <- copy 1/true + } + { + # case 1: if previous character was newline, figure out how long the previous line is + previous-character:character <- get *before-cursor, value:offset + previous-character-is-newline?:boolean <- equal previous-character, 10/newline + break-unless previous-character-is-newline? + # compute length of previous line + trace 10, [app], [switching to previous line] + d:address:duplex-list <- get *editor, data:offset + end-of-line:number <- previous-line-length before-cursor, d + *cursor-column <- add left, end-of-line + reply editor/same-as-ingredient:0, go-render? + } + # case 2: if previous-character was not newline, we're just at a wrapped line + trace 10, [app], [wrapping to previous line] + right:number <- get *editor, right:offset + *cursor-column <- subtract right, 1 # leave room for wrap icon + reply editor/same-as-ingredient:0, go-render? +] + +# takes a pointer 'curr' into the doubly-linked list and its sentinel, counts +# the length of the previous line before the 'curr' pointer. +recipe previous-line-length [ + local-scope + curr:address:duplex-list <- next-ingredient + start:address:duplex-list <- next-ingredient + result:number <- copy 0 + reply-unless curr, result + at-start?:boolean <- equal curr, start + reply-if at-start?, result + { + curr <- prev-duplex curr + break-unless curr + at-start?:boolean <- equal curr, start + break-if at-start? + c:character <- get *curr, value:offset + at-newline?:boolean <- equal c, 10/newline + break-if at-newline? + result <- add result, 1 + loop + } + reply result +] + +scenario editor-clears-last-line-on-backspace [ + assume-screen 10/width, 5/height + # just one character in final line + 1:address:array:character <- new [ab +cd] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + assume-console [ + left-click 2, 0 # cursor at only character in final line + press backspace + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + 4:number <- get *2:address:editor-data, cursor-row:offset + 5:number <- get *2:address:editor-data, cursor-column:offset + ] + screen-should-contain [ + . . + .abcd . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] + memory-should-contain [ + 4 <- 1 + 5 <- 2 + ] +] + +# delete - delete character at cursor + +scenario editor-handles-delete-key [ + assume-screen 10/width, 5/height + 1:address:array:character <- new [abc] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + editor-render screen, 2:address:editor-data + $clear-trace + assume-console [ + press delete + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + 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:address, console:address, 2:address:editor-data + ] + screen-should-contain [ + . . + .c . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] + check-trace-count-for-label 2, [print-character] # new length to overwrite +] + +after <handle-special-key> [ + { + delete-next-character?:boolean <- equal *k, 65522/delete + break-unless delete-next-character? + <delete-character-begin> + editor, screen, go-render?:boolean, deleted-cell:address:duplex-list <- delete-at-cursor editor, screen + <delete-character-end> + reply screen/same-as-ingredient:0, editor/same-as-ingredient:1, go-render? + } +] + +recipe delete-at-cursor [ + local-scope + editor:address:editor-data <- next-ingredient + screen:address <- next-ingredient + before-cursor:address:address:duplex-list <- get-address *editor, before-cursor:offset + candidate:address:duplex-list <- next-duplex *before-cursor + reply-unless candidate, editor/same-as-ingredient:0, screen/same-as-ingredient:1, 0/no-more-render, 0/nothing-deleted + currc:character <- get *candidate, value:offset + remove-duplex candidate + deleted-newline?:boolean <- equal currc, 10/newline + reply-if deleted-newline?, editor/same-as-ingredient:0, screen/same-as-ingredient:1, 1/go-render, candidate/deleted-cell + # wasn't a newline? render rest of line + curr:address:duplex-list <- next-duplex *before-cursor # refresh after remove-duplex above + cursor-row:address:number <- get-address *editor, cursor-row:offset + cursor-column:address:number <- get-address *editor, cursor-column:offset + screen <- move-cursor screen, *cursor-row, *cursor-column + curr-column:number <- copy *cursor-column + screen-width:number <- screen-width screen + { + # hit right margin? give up and let caller render + at-right?:boolean <- greater-or-equal curr-column, screen-width + reply-if at-right?, editor/same-as-ingredient:0, screen/same-as-ingredient:1, 1/go-render, candidate/deleted-cell + break-unless curr + # newline? done. + currc:character <- get *curr, value:offset + at-newline?:boolean <- equal currc, 10/newline + break-if at-newline? + screen <- print-character screen, currc + curr-column <- add curr-column, 1 + curr <- next-duplex curr + loop + } + # we're guaranteed not to be at the right margin + screen <- print-character screen, 32/space + reply editor/same-as-ingredient:0, screen/same-as-ingredient:1, 0/no-more-render, candidate/deleted-cell +] + +# right arrow + +scenario editor-moves-cursor-right-with-key [ + assume-screen 10/width, 5/height + 1:address:array:character <- new [abc] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + editor-render screen, 2:address:editor-data + $clear-trace + assume-console [ + press right-arrow + type [0] + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + screen-should-contain [ + . . + .a0bc . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] + check-trace-count-for-label 3, [print-character] # 0 and following characters +] + +after <handle-special-key> [ + { + move-to-next-character?:boolean <- equal *k, 65514/right-arrow + break-unless move-to-next-character? + # if not at end of text + next-cursor:address:duplex-list <- next-duplex *before-cursor + break-unless next-cursor + # scan to next character + <move-cursor-begin> + *before-cursor <- copy next-cursor + editor, go-render?:boolean <- move-cursor-coordinates-right editor, screen-height + screen <- move-cursor screen, *cursor-row, *cursor-column + undo-coalesce-tag:number <- copy 2/right-arrow + <move-cursor-end> + reply screen/same-as-ingredient:0, editor/same-as-ingredient:1, go-render? + } +] + +recipe move-cursor-coordinates-right [ + local-scope + editor:address:editor-data <- next-ingredient + screen-height:number <- next-ingredient + before-cursor:address:duplex-list <- get *editor before-cursor:offset + cursor-row:address:number <- get-address *editor, cursor-row:offset + cursor-column:address:number <- get-address *editor, cursor-column:offset + left:number <- get *editor, left:offset + right:number <- get *editor, right:offset + # if crossed a newline, move cursor to start of next row + { + old-cursor-character:character <- get *before-cursor, value:offset + was-at-newline?:boolean <- equal old-cursor-character, 10/newline + break-unless was-at-newline? + *cursor-row <- add *cursor-row, 1 + *cursor-column <- copy left + below-screen?:boolean <- greater-or-equal *cursor-row, screen-height # must be equal + reply-unless below-screen?, editor/same-as-ingredient:0, 0/no-more-render + <scroll-down> + *cursor-row <- subtract *cursor-row, 1 # bring back into screen range + reply editor/same-as-ingredient:0, 1/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:number <- subtract right, 1 + at-wrap?:boolean <- equal *cursor-column, wrap-column + break-unless at-wrap? + # and if next character isn't newline + next:address:duplex-list <- next-duplex before-cursor + break-unless next + next-character:character <- get *next, value:offset + newline?:boolean <- equal next-character, 10/newline + break-if newline? + *cursor-row <- add *cursor-row, 1 + *cursor-column <- copy left + below-screen?:boolean <- greater-or-equal *cursor-row, screen-height # must be equal + reply-unless below-screen?, editor/same-as-ingredient:0, 0/no-more-render + <scroll-down> + *cursor-row <- subtract *cursor-row, 1 # bring back into screen range + reply editor/same-as-ingredient:0, 1/go-render + } + # otherwise move cursor one character right + *cursor-column <- add *cursor-column, 1 + reply editor/same-as-ingredient:0, 0/no-more-render +] + +scenario editor-moves-cursor-to-next-line-with-right-arrow [ + assume-screen 10/width, 5/height + 1:address:array:character <- new [abc +d] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + editor-render screen, 2:address:editor-data + $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:address, console:address, 2:address:editor-data + ] + 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:address, console:address, 2:address:editor-data + ] + 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 [ + assume-screen 10/width, 5/height + 1:address:array:character <- new [abc +d] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 1/left, 10/right + editor-render screen, 2:address:editor-data + assume-console [ + press right-arrow + press right-arrow + press right-arrow + press right-arrow # next line + type [0] + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + screen-should-contain [ + . . + . abc . + . 0d . + . ┈┈┈┈┈┈┈┈┈. + . . + ] +] + +scenario editor-moves-cursor-to-next-wrapped-line-with-right-arrow [ + assume-screen 10/width, 5/height + 1:address:array:character <- new [abcdef] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 5/right + editor-render screen, 2:address:editor-data + $clear-trace + assume-console [ + left-click 1, 3 + press right-arrow + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, 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 [ + assume-screen 10/width, 5/height + # line just barely wrapping + 1:address:array:character <- new [abcde] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 5/right + editor-render screen, 2:address:editor-data + $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:address, console:address, 2:address:editor-data + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, 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:address, console:address, 2:address:editor-data + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, 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 [ + assume-screen 10/width, 5/height + 1:address:array:character <- new [abcdef] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 1/left, 6/right + editor-render screen, 2:address:editor-data + $clear-trace + assume-console [ + left-click 1, 4 + press right-arrow + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, 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 [ + assume-screen 10/width, 5/height + 1:address:array:character <- new [abc +d] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + editor-render screen, 2:address:editor-data + $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:address, console:address, 2:address:editor-data + ] + # 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 [ + assume-screen 10/width, 5/height + 1:address:array:character <- new [abc] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + editor-render screen, 2:address:editor-data + $clear-trace + assume-console [ + left-click 1, 2 + press left-arrow + type [0] + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + screen-should-contain [ + . . + .a0bc . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] + check-trace-count-for-label 3, [print-character] +] + +after <handle-special-key> [ + { + move-to-previous-character?:boolean <- 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:address:duplex-list <- prev-duplex *before-cursor + reply-unless prev, screen/same-as-ingredient:0, editor/same-as-ingredient:1, 0/no-more-render + <move-cursor-begin> + editor, go-render? <- move-cursor-coordinates-left editor + *before-cursor <- copy prev + undo-coalesce-tag:number <- copy 1/left-arrow + <move-cursor-end> + reply screen/same-as-ingredient:0, editor/same-as-ingredient:1, go-render? + } +] + +scenario editor-moves-cursor-to-previous-line-with-left-arrow-at-start-of-line [ + assume-screen 10/width, 5/height + # initialize editor with two lines + 1:address:array:character <- new [abc +d] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + editor-render screen, 2:address:editor-data + $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:address, console:address, 2:address:editor-data + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, 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 [ + assume-screen 10/width, 5/height + # initialize editor with three lines + 1:address:array:character <- new [abc +def +g] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + editor-render screen, 2:address:editor-data + $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:address, console:address, 2:address:editor-data + ] + 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 [ + assume-screen 10/width, 5/height + 1:address:array:character <- new [abc +def +g] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + editor-render screen, 2:address:editor-data + $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:address, console:address, 2:address:editor-data + ] + # 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 [ + assume-screen 10/width, 5/height + # initialize editor with text containing an empty line + 1:address:array:character <- new [abc + +d] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + editor-render screen, 2:address:editor-data + $clear-trace + # position cursor right after empty line + assume-console [ + left-click 3, 0 + press left-arrow + type [0] + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + 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 [ + assume-screen 10/width, 5/height + # initialize editor with text containing an empty line + 1:address:array:character <- new [abcdef] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 5/right + editor-render screen, 2:address:editor-data + $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:address, console:address, 2:address:editor-data + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, cursor-column:offset + ] + memory-should-contain [ + 3 <- 1 # previous row + 4 <- 3 # 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 [ + assume-screen 10/width, 5/height + 1:address:array:character <- new [abc +def] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + editor-render screen, 2:address:editor-data + $clear-trace + assume-console [ + left-click 2, 1 + press up-arrow + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, 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:address, console:address, 2:address:editor-data + ] + screen-should-contain [ + . . + .a0bc . + .def . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] +] + +after <handle-special-key> [ + { + move-to-previous-line?:boolean <- equal *k, 65517/up-arrow + break-unless move-to-previous-line? + <move-cursor-begin> + editor, go-render? <- move-to-previous-line editor + undo-coalesce-tag:number <- copy 3/up-arrow + <move-cursor-end> + reply screen/same-as-ingredient:0, editor/same-as-ingredient:1, go-render? + } +] + +recipe move-to-previous-line [ + local-scope + editor:address:editor-data <- next-ingredient + cursor-row:address:number <- get-address *editor, cursor-row:offset + cursor-column:address:number <- get-address *editor, cursor-column:offset + before-cursor:address:address:duplex-list <- get-address *editor, before-cursor:offset + left:number <- get *editor, left:offset + right:number <- get *editor, right:offset + already-at-top?:boolean <- 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:address:duplex-list <- copy *before-cursor + { + old:address:duplex-list <- copy curr + c2:character <- get *curr, value:offset + at-newline?:boolean <- equal c2, 10/newline + break-if at-newline? + curr:address:duplex-list <- before-previous-line curr, editor + no-motion?:boolean <- equal curr, old + reply-if no-motion?, editor/same-as-ingredient:0, 0/no-more-render + } + { + old <- copy curr + curr <- before-previous-line curr, editor + no-motion?:boolean <- equal curr, old + reply-if no-motion?, editor/same-as-ingredient:0, 0/no-more-render + } + *before-cursor <- copy curr + *cursor-row <- subtract *cursor-row, 1 + # scan ahead to right column or until end of line + target-column:number <- copy *cursor-column + *cursor-column <- copy left + { + done?:boolean <- greater-or-equal *cursor-column, target-column + break-if done? + curr:address:duplex-list <- next-duplex *before-cursor + break-unless curr + currc:character <- get *curr, value:offset + at-newline?:boolean <- equal currc, 10/newline + break-if at-newline? + # + *before-cursor <- copy curr + *cursor-column <- add *cursor-column, 1 + loop + } + reply editor/same-as-ingredient:0, 0/no-more-render + } + { + # if cursor already at top, scroll up + break-unless already-at-top? + <scroll-up> + reply editor/same-as-ingredient:0, 1/go-render + } +] + +scenario editor-adjusts-column-at-previous-line [ + assume-screen 10/width, 5/height + 1:address:array:character <- new [ab +def] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + editor-render screen, 2:address:editor-data + $clear-trace + assume-console [ + left-click 2, 3 + press up-arrow + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, 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:address, console:address, 2:address:editor-data + ] + screen-should-contain [ + . . + .ab0 . + .def . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] +] + +scenario editor-adjusts-column-at-empty-line [ + assume-screen 10/width, 5/height + 1:address:array:character <- new [ +def] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + editor-render screen, 2:address:editor-data + $clear-trace + assume-console [ + left-click 2, 3 + press up-arrow + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, 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:address, console:address, 2:address:editor-data + ] + screen-should-contain [ + . . + .0 . + .def . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] +] + +scenario editor-moves-to-previous-line-from-left-margin [ + assume-screen 10/width, 5/height + # start out with three lines + 1:address:array:character <- new [abc +def +ghi] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + editor-render screen, 2:address:editor-data + $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:address, console:address, 2:address:editor-data + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, 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:address, console:address, 2:address:editor-data + ] + screen-should-contain [ + . . + .abc . + .0def . + .ghi . + .┈┈┈┈┈┈┈┈┈┈. + ] +] + +# down arrow + +scenario editor-moves-to-next-line-with-down-arrow [ + assume-screen 10/width, 5/height + 1:address:array:character <- new [abc +def] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + editor-render screen, 2:address:editor-data + $clear-trace + # cursor starts out at (1, 0) + assume-console [ + press down-arrow + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, 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:address, console:address, 2:address:editor-data + ] + screen-should-contain [ + . . + .abc . + .0def . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] +] + +after <handle-special-key> [ + { + move-to-next-line?:boolean <- equal *k, 65516/down-arrow + break-unless move-to-next-line? + <move-cursor-begin> + editor, go-render? <- move-to-next-line editor, screen-height + undo-coalesce-tag:number <- copy 4/down-arrow + <move-cursor-end> + reply screen/same-as-ingredient:0, editor/same-as-ingredient:1, go-render? + } +] + +recipe move-to-next-line [ + local-scope + editor:address:editor-data <- next-ingredient + screen-height:number <- next-ingredient + cursor-row:address:number <- get-address *editor, cursor-row:offset + cursor-column:address:number <- get-address *editor, cursor-column:offset + before-cursor:address:address:duplex-list <- get-address *editor, before-cursor:offset + left:number <- get *editor, left:offset + right:number <- get *editor, right:offset + last-line:number <- subtract screen-height, 1 + already-at-bottom?:boolean <- greater-or-equal *cursor-row, last-line + { + # if cursor not at bottom, move it + break-if already-at-bottom? + # scan to start of next line, then to right column or until end of line + max:number <- subtract right, left + next-line:address:duplex-list <- before-start-of-next-line *before-cursor, max + { + # already at end of buffer? try to scroll up (so we can see more + # warnings or sandboxes below) + no-motion?:boolean <- equal next-line, *before-cursor + break-unless no-motion? + scroll?:boolean <- greater-than *cursor-row, 1 + break-if scroll?, +try-to-scroll:label + reply editor/same-as-ingredient:0, 0/no-more-render + } + *cursor-row <- add *cursor-row, 1 + *before-cursor <- copy next-line + target-column:number <- copy *cursor-column + *cursor-column <- copy left + { + done?:boolean <- greater-or-equal *cursor-column, target-column + break-if done? + curr:address:duplex-list <- next-duplex *before-cursor + break-unless curr + currc:character <- get *curr, value:offset + at-newline?:boolean <- equal currc, 10/newline + break-if at-newline? + # + *before-cursor <- copy curr + *cursor-column <- add *cursor-column, 1 + loop + } + reply editor/same-as-ingredient:0, 0/no-more-render + } + +try-to-scroll + <scroll-down> + reply editor/same-as-ingredient:0, 1/go-render +] + +scenario editor-adjusts-column-at-next-line [ + assume-screen 10/width, 5/height + 1:address:array:character <- new [abc +de] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + editor-render screen, 2:address:editor-data + $clear-trace + assume-console [ + left-click 1, 3 + press down-arrow + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, cursor-column:offset + ] + memory-should-contain [ + 3 <- 2 + 4 <- 2 + ] + check-trace-count-for-label 0, [print-character] + assume-console [ + type [0] + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + screen-should-contain [ + . . + .abc . + .de0 . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] +] + +scenario editor-scrolls-at-end-on-down-arrow [ + assume-screen 10/width, 5/height + 1:address:array:character <- new [abc +de] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + editor-render screen, 2:address:editor-data + $clear-trace + # try to move down past end of text + assume-console [ + left-click 2, 0 + press down-arrow + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, cursor-column:offset + ] + # screen should scroll, moving cursor to end of text + memory-should-contain [ + 3 <- 1 + 4 <- 2 + ] + assume-console [ + type [0] + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + screen-should-contain [ + . . + .de0 . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] + # try to move down again + $clear-trace + assume-console [ + left-click 2, 0 + press down-arrow + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, cursor-column:offset + ] + # screen stops scrolling because cursor is already at top + memory-should-contain [ + 3 <- 1 + 4 <- 3 + ] + check-trace-count-for-label 0, [print-character] + assume-console [ + type [1] + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + screen-should-contain [ + . . + .de01 . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] +] + +# ctrl-a/home - move cursor to start of line + +scenario editor-moves-to-start-of-line-with-ctrl-a [ + assume-screen 10/width, 5/height + 1:address:array:character <- new [123 +456] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + editor-render screen, 2:address:editor-data + $clear-trace + # start on second line, press ctrl-a + assume-console [ + left-click 2, 3 + press ctrl-a + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + 4:number <- get *2:address:editor-data, cursor-row:offset + 5:number <- get *2:address:editor-data, 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?:boolean <- equal *c, 1/ctrl-a + break-unless move-to-start-of-line? + <move-cursor-begin> + move-to-start-of-line editor + undo-coalesce-tag:number <- copy 0/never + <move-cursor-end> + reply screen/same-as-ingredient:0, editor/same-as-ingredient:1, 0/no-more-render + } +] + +after <handle-special-key> [ + { + move-to-start-of-line?:boolean <- equal *k, 65521/home + break-unless move-to-start-of-line? + <move-cursor-begin> + move-to-start-of-line editor + undo-coalesce-tag:number <- copy 0/never + <move-cursor-end> + reply screen/same-as-ingredient:0, editor/same-as-ingredient:1, 0/no-more-render + } +] + +recipe move-to-start-of-line [ + local-scope + editor:address:editor-data <- next-ingredient + # update cursor column + left:number <- get *editor, left:offset + cursor-column:address:number <- get-address *editor, cursor-column:offset + *cursor-column <- copy left + # update before-cursor + before-cursor:address:address:duplex-list <- get-address *editor, before-cursor:offset + init:address:duplex-list <- get *editor, data:offset + # while not at start of line, move + { + at-start-of-text?:boolean <- equal *before-cursor, init + break-if at-start-of-text? + prev:character <- get **before-cursor, value:offset + at-start-of-line?:boolean <- equal prev, 10/newline + break-if at-start-of-line? + *before-cursor <- prev-duplex *before-cursor + assert *before-cursor, [move-to-start-of-line tried to move before start of text] + loop + } +] + +scenario editor-moves-to-start-of-line-with-ctrl-a-2 [ + assume-screen 10/width, 5/height + 1:address:array:character <- new [123 +456] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + editor-render screen, 2:address:editor-data + $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:address, console:address, 2:address:editor-data + 4:number <- get *2:address:editor-data, cursor-row:offset + 5:number <- get *2:address:editor-data, 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 [ + assume-screen 10/width, 5/height + 1:address:array:character <- new [123 +456] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 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:address, console:address, 2:address:editor-data + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, 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 [ + assume-screen 10/width, 5/height + 1:address:array:character <- new [123 +456] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + editor-render screen, 2:address:editor-data + $clear-trace + # start on first line (no newline before), press 'home' + assume-console [ + left-click 1, 3 + press home + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, cursor-column:offset + ] + # cursor moves to start of line + memory-should-contain [ + 3 <- 1 + 4 <- 0 + ] + check-trace-count-for-label 0, [print-character] +] + +# ctrl-e/end - move cursor to end of line + +scenario editor-moves-to-end-of-line-with-ctrl-e [ + assume-screen 10/width, 5/height + 1:address:array:character <- new [123 +456] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + editor-render screen, 2:address:editor-data + $clear-trace + # start on first line, press ctrl-e + assume-console [ + left-click 1, 1 + press ctrl-e + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + 4:number <- get *2:address:editor-data, cursor-row:offset + 5:number <- get *2:address:editor-data, 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:address, console:address, 2:address:editor-data + 4:number <- get *2:address:editor-data, cursor-row:offset + 5:number <- get *2:address:editor-data, 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?:boolean <- equal *c, 5/ctrl-e + break-unless move-to-end-of-line? + <move-cursor-begin> + move-to-end-of-line editor + undo-coalesce-tag:number <- copy 0/never + <move-cursor-end> + reply screen/same-as-ingredient:0, editor/same-as-ingredient:1, 0/no-more-render + } +] + +after <handle-special-key> [ + { + move-to-end-of-line?:boolean <- equal *k, 65520/end + break-unless move-to-end-of-line? + <move-cursor-begin> + move-to-end-of-line editor + undo-coalesce-tag:number <- copy 0/never + <move-cursor-end> + reply screen/same-as-ingredient:0, editor/same-as-ingredient:1, 0/no-more-render + } +] + +recipe move-to-end-of-line [ + local-scope + editor:address:editor-data <- next-ingredient + before-cursor:address:address:duplex-list <- get-address *editor, before-cursor:offset + cursor-column:address:number <- get-address *editor, cursor-column:offset + # while not at start of line, move + { + next:address:duplex-list <- next-duplex *before-cursor + break-unless next # end of text + nextc:character <- get *next, value:offset + at-end-of-line?:boolean <- equal nextc, 10/newline + break-if at-end-of-line? + *before-cursor <- copy next + *cursor-column <- add *cursor-column, 1 + loop + } +] + +scenario editor-moves-to-end-of-line-with-ctrl-e-2 [ + assume-screen 10/width, 5/height + 1:address:array:character <- new [123 +456] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + editor-render screen, 2:address:editor-data + $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:address, console:address, 2:address:editor-data + 4:number <- get *2:address:editor-data, cursor-row:offset + 5:number <- get *2:address:editor-data, 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 [ + assume-screen 10/width, 5/height + 1:address:array:character <- new [123 +456] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + editor-render screen, 2:address:editor-data + $clear-trace + # start on first line, press 'end' + assume-console [ + left-click 1, 1 + press end + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, 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 [ + assume-screen 10/width, 5/height + 1:address:array:character <- new [123 +456] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + editor-render screen, 2:address:editor-data + $clear-trace + # start on second line (no newline after), press 'end' + assume-console [ + left-click 2, 1 + press end + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, cursor-column:offset + ] + # cursor moves to end of line + memory-should-contain [ + 3 <- 2 + 4 <- 3 + ] + check-trace-count-for-label 0, [print-character] +] + +# ctrl-u - delete text from start of line until (but not at) cursor + +scenario editor-deletes-to-start-of-line-with-ctrl-u [ + assume-screen 10/width, 5/height + 1:address:array:character <- new [123 +456] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + # start on second line, press ctrl-u + assume-console [ + left-click 2, 2 + press ctrl-u + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + # cursor deletes to start of line + screen-should-contain [ + . . + .123 . + .6 . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] +] + +after <handle-special-character> [ + { + delete-to-start-of-line?:boolean <- equal *c, 21/ctrl-u + break-unless delete-to-start-of-line? + <delete-to-start-of-line-begin> + deleted-cells:address:duplex-list <- delete-to-start-of-line editor + <delete-to-start-of-line-end> + reply screen/same-as-ingredient:0, editor/same-as-ingredient:1, 1/go-render + } +] + +recipe delete-to-start-of-line [ + local-scope + editor:address:editor-data <- next-ingredient + # compute range to delete + init:address:duplex-list <- get *editor, data:offset + before-cursor:address:address:duplex-list <- get-address *editor, before-cursor:offset + start:address:duplex-list <- copy *before-cursor + end:address:duplex-list <- next-duplex *before-cursor + { + at-start-of-text?:boolean <- equal start, init + break-if at-start-of-text? + curr:character <- get *start, value:offset + at-start-of-line?:boolean <- equal curr, 10/newline + break-if at-start-of-line? + start <- prev-duplex start + assert start, [delete-to-start-of-line tried to move before start of text] + loop + } + # snip it out + result:address:duplex-list <- next-duplex start + remove-duplex-between start, end + # adjust cursor + *before-cursor <- copy start + left:number <- get *editor, left:offset + cursor-column:address:number <- get-address *editor, cursor-column:offset + *cursor-column <- copy left + reply result +] + +scenario editor-deletes-to-start-of-line-with-ctrl-u-2 [ + assume-screen 10/width, 5/height + 1:address:array:character <- new [123 +456] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + # start on first line (no newline before), press ctrl-u + assume-console [ + left-click 1, 2 + press ctrl-u + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + # cursor deletes to start of line + screen-should-contain [ + . . + .3 . + .456 . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] +] + +scenario editor-deletes-to-start-of-line-with-ctrl-u-3 [ + assume-screen 10/width, 5/height + 1:address:array:character <- new [123 +456] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + # start past end of line, press ctrl-u + assume-console [ + left-click 1, 3 + press ctrl-u + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + # cursor deletes to start of line + screen-should-contain [ + . . + . . + .456 . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] +] + +scenario editor-deletes-to-start-of-final-line-with-ctrl-u [ + assume-screen 10/width, 5/height + 1:address:array:character <- new [123 +456] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + # start past end of final line, press ctrl-u + assume-console [ + left-click 2, 3 + press ctrl-u + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + # cursor deletes to start of line + screen-should-contain [ + . . + .123 . + . . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] +] + +# ctrl-k - delete text from cursor to end of line (but not the newline) + +scenario editor-deletes-to-end-of-line-with-ctrl-k [ + assume-screen 10/width, 5/height + 1:address:array:character <- new [123 +456] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + # start on first line, press ctrl-k + assume-console [ + left-click 1, 1 + press ctrl-k + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + # cursor deletes to end of line + screen-should-contain [ + . . + .1 . + .456 . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] +] + +after <handle-special-character> [ + { + delete-to-end-of-line?:boolean <- equal *c, 11/ctrl-k + break-unless delete-to-end-of-line? + <delete-to-end-of-line-begin> + deleted-cells:address:duplex-list <- delete-to-end-of-line editor + <delete-to-end-of-line-end> + reply screen/same-as-ingredient:0, editor/same-as-ingredient:1, 1/go-render + } +] + +recipe delete-to-end-of-line [ + local-scope + editor:address:editor-data <- next-ingredient + # compute range to delete + start:address:duplex-list <- get *editor, before-cursor:offset + end:address:duplex-list <- next-duplex start + { + at-end-of-text?:boolean <- equal end, 0/null + break-if at-end-of-text? + curr:character <- get *end, value:offset + at-end-of-line?:boolean <- equal curr, 10/newline + break-if at-end-of-line? + end <- next-duplex end + loop + } + # snip it out + result:address:duplex-list <- next-duplex start + remove-duplex-between start, end + reply result +] + +scenario editor-deletes-to-end-of-line-with-ctrl-k-2 [ + assume-screen 10/width, 5/height + 1:address:array:character <- new [123 +456] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + # start on second line (no newline after), press ctrl-k + assume-console [ + left-click 2, 1 + press ctrl-k + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + # cursor deletes to end of line + screen-should-contain [ + . . + .123 . + .4 . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] +] + +scenario editor-deletes-to-end-of-line-with-ctrl-k-3 [ + assume-screen 10/width, 5/height + 1:address:array:character <- new [123 +456] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + # start at end of line + assume-console [ + left-click 1, 2 + press ctrl-k + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + # cursor deletes just last character + screen-should-contain [ + . . + .12 . + .456 . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] +] + +scenario editor-deletes-to-end-of-line-with-ctrl-k-4 [ + assume-screen 10/width, 5/height + 1:address:array:character <- new [123 +456] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + # start past end of line + assume-console [ + left-click 1, 3 + press ctrl-k + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + # cursor deletes nothing + screen-should-contain [ + . . + .123 . + .456 . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] +] + +scenario editor-deletes-to-end-of-line-with-ctrl-k-5 [ + assume-screen 10/width, 5/height + 1:address:array:character <- new [123 +456] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + # start at end of text + assume-console [ + left-click 2, 2 + press ctrl-k + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + # cursor deletes just the final character + screen-should-contain [ + . . + .123 . + .45 . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] +] + +scenario editor-deletes-to-end-of-line-with-ctrl-k-6 [ + assume-screen 10/width, 5/height + 1:address:array:character <- new [123 +456] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + # start past end of text + assume-console [ + left-click 2, 3 + press ctrl-k + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + # cursor deletes nothing + screen-should-contain [ + . . + .123 . + .456 . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] +] + +# cursor-down can scroll if necessary + +scenario editor-can-scroll-down-using-arrow-keys [ + # screen has 1 line for menu + 3 lines + assume-screen 10/width, 4/height + # initialize editor with >3 lines + 1:address:array:character <- new [a +b +c +d] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + screen-should-contain [ + . . + .a . + .b . + .c . + ] + # position cursor at last line, then try to move further down + assume-console [ + left-click 3, 0 + press down-arrow + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + # screen slides by one line + screen-should-contain [ + . . + .b . + .c . + .d . + ] +] + +after <scroll-down> [ + trace 10, [app], [scroll down] + top-of-screen:address:address:duplex-list <- get-address *editor, top-of-screen:offset + left:number <- get *editor, left:offset + right:number <- get *editor, right:offset + max:number <- subtract right, left + old-top:address:duplex-list <- copy *top-of-screen + *top-of-screen <- before-start-of-next-line *top-of-screen, max + no-movement?:boolean <- equal old-top, *top-of-screen + # Hack: this reply doesn't match one of the locations of <scroll-down>, + # directly within insert-at-cursor. However, I'm unable to trigger the + # error.. If necessary create a duplicate copy of <scroll-down> with the + # right 'reply-if'. + reply-if no-movement?, editor/same-as-ingredient:0, 0/no-more-render +] + +# takes a pointer into the doubly-linked list, scans ahead at most 'max' +# positions until the next newline +# beware: never return null pointer. +recipe before-start-of-next-line [ + local-scope + original:address:duplex-list <- next-ingredient + max:number <- next-ingredient + count:number <- copy 0 + curr:address:duplex-list <- copy original + # skip the initial newline if it exists + { + c:character <- get *curr, value:offset + at-newline?:boolean <- equal c, 10/newline + break-unless at-newline? + curr <- next-duplex curr + count <- add count, 1 + } + { + reply-unless curr, original + done?:boolean <- greater-or-equal count, max + break-if done? + c:character <- get *curr, value:offset + at-newline?:boolean <- equal c, 10/newline + break-if at-newline? + curr <- next-duplex curr + count <- add count, 1 + loop + } + reply-unless curr, original + reply curr +] + +scenario editor-scrolls-down-past-wrapped-line-using-arrow-keys [ + # screen has 1 line for menu + 3 lines + assume-screen 10/width, 4/height + # initialize editor with a long, wrapped line and more than a screen of + # other lines + 1:address:array:character <- new [abcdef +g +h +i] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 5/right + screen-should-contain [ + . . + .abcd↩ . + .ef . + .g . + ] + # position cursor at last line, then try to move further down + assume-console [ + left-click 3, 0 + press down-arrow + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + # screen shows partial wrapped line + screen-should-contain [ + . . + .ef . + .g . + .h . + ] +] + +scenario editor-scrolls-down-past-wrapped-line-using-arrow-keys-2 [ + # screen has 1 line for menu + 3 lines + assume-screen 10/width, 4/height + # editor starts with a long line wrapping twice + 1:address:array:character <- new [abcdefghij +k +l +m] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 5/right + # position cursor at last line, then try to move further down + assume-console [ + left-click 3, 0 + press down-arrow + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + # screen shows partial wrapped line containing a wrap icon + screen-should-contain [ + . . + .efgh↩ . + .ij . + .k . + ] + # scroll down again + assume-console [ + press down-arrow + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + # screen shows partial wrapped line + screen-should-contain [ + . . + .ij . + .k . + .l . + ] +] + +scenario editor-scrolls-down-when-line-wraps [ + # screen has 1 line for menu + 3 lines + assume-screen 5/width, 4/height + # editor contains a long line in the third line + 1:address:array:character <- new [a +b +cdef] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 5/right + # position cursor at end, type a character + assume-console [ + left-click 3, 4 + type [g] + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, cursor-column:offset + ] + # screen scrolls + screen-should-contain [ + . . + .b . + .cdef↩. + .g . + ] + memory-should-contain [ + 3 <- 3 + 4 <- 1 + ] +] + +scenario editor-scrolls-down-on-newline [ + assume-screen 5/width, 4/height + # position cursor after last line and type newline + 1:address:array:character <- new [a +b +c] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 5/right + assume-console [ + left-click 3, 4 + type [ +] + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, cursor-column:offset + ] + # screen scrolls + screen-should-contain [ + . . + .b . + .c . + . . + ] + memory-should-contain [ + 3 <- 3 + 4 <- 0 + ] +] + +scenario editor-scrolls-down-on-right-arrow [ + # screen has 1 line for menu + 3 lines + assume-screen 5/width, 4/height + # editor contains a wrapped line + 1:address:array:character <- new [a +b +cdefgh] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 5/right + # position cursor at end of screen and try to move right + assume-console [ + left-click 3, 3 + press right-arrow + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, cursor-column:offset + ] + # screen scrolls + screen-should-contain [ + . . + .b . + .cdef↩. + .gh . + ] + memory-should-contain [ + 3 <- 3 + 4 <- 0 + ] +] + +scenario editor-scrolls-down-on-right-arrow-2 [ + # screen has 1 line for menu + 3 lines + assume-screen 5/width, 4/height + # editor contains more lines than can fit on screen + 1:address:array:character <- new [a +b +c +d] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 5/right + # position cursor at end of screen and try to move right + assume-console [ + left-click 3, 3 + press right-arrow + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, cursor-column:offset + ] + # screen scrolls + screen-should-contain [ + . . + .b . + .c . + .d . + ] + memory-should-contain [ + 3 <- 3 + 4 <- 0 + ] +] + +scenario editor-combines-page-and-line-scroll [ + # screen has 1 line for menu + 3 lines + assume-screen 10/width, 4/height + # initialize editor with a few pages of lines + 1:address:array:character <- new [a +b +c +d +e +f +g] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 5/right + # scroll down one page and one line + assume-console [ + press page-down + left-click 3, 0 + press down-arrow + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + # screen scrolls down 3 lines + screen-should-contain [ + . . + .d . + .e . + .f . + ] +] + +# cursor-up can scroll if necessary + +scenario editor-can-scroll-up-using-arrow-keys [ + # screen has 1 line for menu + 3 lines + assume-screen 10/width, 4/height + # initialize editor with >3 lines + 1:address:array:character <- new [a +b +c +d] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + screen-should-contain [ + . . + .a . + .b . + .c . + ] + # position cursor at top of second page, then try to move up + assume-console [ + press page-down + press up-arrow + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + # screen slides by one line + screen-should-contain [ + . . + .b . + .c . + .d . + ] +] + +after <scroll-up> [ + trace 10, [app], [scroll up] + top-of-screen:address:address:duplex-list <- get-address *editor, top-of-screen:offset + old-top:address:duplex-list <- copy *top-of-screen + *top-of-screen <- before-previous-line *top-of-screen, editor + no-movement?:boolean <- equal old-top, *top-of-screen + reply-if no-movement?, editor/same-as-ingredient:0, 0/no-more-render +] + +# takes a pointer into the doubly-linked list, scans back to before start of +# previous *wrapped* line +# beware: never return null pointer +recipe before-previous-line [ + local-scope + curr:address:duplex-list <- next-ingredient + c:character <- 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 + editor:address:editor-data <- next-ingredient + left:number <- get *editor, left:offset + right:number <- get *editor, right:offset + max-line-length:number <- subtract right, left, -1/exclusive-right, 1/wrap-icon + sentinel:address:duplex-list <- get *editor, data:offset + len:number <- previous-line-length curr, sentinel + { + break-if len + # empty line; just skip this newline + prev:address:duplex-list <- prev-duplex curr + reply-unless prev, curr + reply prev + } + _, max:number <- 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:number <- copy 0 + # skip 'max' characters + { + done?:boolean <- greater-or-equal count, max + break-if done? + prev:address:duplex-list <- prev-duplex curr + break-unless prev + curr <- copy prev + count <- add count, 1 + loop + } + reply curr +] + +scenario editor-scrolls-up-past-wrapped-line-using-arrow-keys [ + # screen has 1 line for menu + 3 lines + assume-screen 10/width, 4/height + # initialize editor with a long, wrapped line and more than a screen of + # other lines + 1:address:array:character <- new [abcdef +g +h +i] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 5/right + screen-should-contain [ + . . + .abcd↩ . + .ef . + .g . + ] + # position cursor at top of second page, just below wrapped line + assume-console [ + press page-down + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + screen-should-contain [ + . . + .g . + .h . + .i . + ] + # now move up one line + assume-console [ + press up-arrow + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + # screen shows partial wrapped line + screen-should-contain [ + . . + .ef . + .g . + .h . + ] +] + +scenario editor-scrolls-up-past-wrapped-line-using-arrow-keys-2 [ + # screen has 1 line for menu + 4 lines + assume-screen 10/width, 5/height + # editor starts with a long line wrapping twice, occupying 3 of the 4 lines + 1:address:array:character <- new [abcdefghij +k +l +m] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 5/right + # position cursor at top of second page + assume-console [ + press page-down + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + screen-should-contain [ + . . + .k . + .l . + .m . + .┈┈┈┈┈ . + ] + # move up one line + assume-console [ + press up-arrow + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + # screen shows partial wrapped line + screen-should-contain [ + . . + .ij . + .k . + .l . + .m . + ] + # move up a second line + assume-console [ + press up-arrow + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + # screen shows partial wrapped line + screen-should-contain [ + . . + .efgh↩ . + .ij . + .k . + .l . + ] + # move up a third line + assume-console [ + press up-arrow + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + # screen shows partial wrapped line + screen-should-contain [ + . . + .abcd↩ . + .efgh↩ . + .ij . + .k . + ] +] + +# same as editor-scrolls-up-past-wrapped-line-using-arrow-keys but length +# slightly off, just to prevent over-training +scenario editor-scrolls-up-past-wrapped-line-using-arrow-keys-3 [ + # screen has 1 line for menu + 3 lines + assume-screen 10/width, 4/height + # initialize editor with a long, wrapped line and more than a screen of + # other lines + 1:address:array:character <- new [abcdef +g +h +i] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 6/right + screen-should-contain [ + . . + .abcde↩ . + .f . + .g . + ] + # position cursor at top of second page, just below wrapped line + assume-console [ + press page-down + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + screen-should-contain [ + . . + .g . + .h . + .i . + ] + # now move up one line + assume-console [ + press up-arrow + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + # screen shows partial wrapped line + screen-should-contain [ + . . + .f . + .g . + .h . + ] +] + +# check empty lines +scenario editor-scrolls-up-past-wrapped-line-using-arrow-keys-4 [ + assume-screen 10/width, 4/height + # initialize editor with some lines around an empty line + 1:address:array:character <- new [a +b + +c +d +e] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 6/right + assume-console [ + press page-down + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + screen-should-contain [ + . . + . . + .c . + .d . + ] + assume-console [ + press page-down + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + screen-should-contain [ + . . + .d . + .e . + .┈┈┈┈┈┈ . + ] + assume-console [ + press page-up + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + screen-should-contain [ + . . + . . + .c . + .d . + ] +] + +scenario editor-scrolls-up-on-left-arrow [ + # screen has 1 line for menu + 3 lines + assume-screen 5/width, 4/height + # editor contains >3 lines + 1:address:array:character <- new [a +b +c +d +e] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 5/right + # position cursor at top of second page + assume-console [ + press page-down + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + screen-should-contain [ + . . + .c . + .d . + .e . + ] + # now try to move left + assume-console [ + press left-arrow + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, cursor-column:offset + ] + # screen scrolls + screen-should-contain [ + . . + .b . + .c . + .d . + ] + memory-should-contain [ + 3 <- 1 + 4 <- 1 + ] +] + +scenario editor-can-scroll-up-to-start-of-file [ + # screen has 1 line for menu + 3 lines + assume-screen 10/width, 4/height + # initialize editor with >3 lines + 1:address:array:character <- new [a +b +c +d] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + screen-should-contain [ + . . + .a . + .b . + .c . + ] + # position cursor at top of second page, then try to move up to start of + # text + assume-console [ + press page-down + press up-arrow + press up-arrow + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + # screen slides by one line + screen-should-contain [ + . . + .a . + .b . + .c . + ] + # try to move up again + assume-console [ + press up-arrow + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + # screen remains unchanged + screen-should-contain [ + . . + .a . + .b . + .c . + ] +] + +# ctrl-f/page-down - render next page if it exists + +scenario editor-can-scroll [ + assume-screen 10/width, 4/height + 1:address:array:character <- new [a +b +c +d] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + screen-should-contain [ + . . + .a . + .b . + .c . + ] + # scroll down + assume-console [ + press page-down + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + # screen shows next page + screen-should-contain [ + . . + .c . + .d . + .┈┈┈┈┈┈┈┈┈┈. + ] +] + +after <handle-special-character> [ + { + page-down?:boolean <- equal *c, 6/ctrl-f + break-unless page-down? + top-of-screen:address:address:duplex-list <- get-address *editor, top-of-screen:offset + old-top:address:duplex-list <- copy *top-of-screen + <move-cursor-begin> + page-down editor + undo-coalesce-tag:number <- copy 0/never + <move-cursor-end> + no-movement?:boolean <- equal *top-of-screen, old-top + reply-if no-movement?, screen/same-as-ingredient:0, editor/same-as-ingredient:1, 0/no-more-render + reply screen/same-as-ingredient:0, editor/same-as-ingredient:1, 1/go-render + } +] + +after <handle-special-key> [ + { + page-down?:boolean <- equal *k, 65518/page-down + break-unless page-down? + top-of-screen:address:address:duplex-list <- get-address *editor, top-of-screen:offset + old-top:address:duplex-list <- copy *top-of-screen + <move-cursor-begin> + page-down editor + undo-coalesce-tag:number <- copy 0/never + <move-cursor-end> + no-movement?:boolean <- equal *top-of-screen, old-top + reply-if no-movement?, screen/same-as-ingredient:0, editor/same-as-ingredient:1, 0/no-more-render + reply screen/same-as-ingredient:0, editor/same-as-ingredient:1, 1/go-render + } +] + +# page-down skips entire wrapped lines, so it can't scroll past lines +# taking up the entire screen +recipe page-down [ + local-scope + editor:address:editor-data <- next-ingredient + # if editor contents don't overflow screen, do nothing + bottom-of-screen:address:duplex-list <- get *editor, bottom-of-screen:offset + reply-unless bottom-of-screen, editor/same-as-ingredient:0 + # if not, position cursor at final character + before-cursor:address:address:duplex-list <- get-address *editor, before-cursor:offset + *before-cursor <- prev-duplex bottom-of-screen + # keep one line in common with previous page + { + last:character <- get **before-cursor, value:offset + newline?:boolean <- equal last, 10/newline + break-unless newline?:boolean + *before-cursor <- prev-duplex *before-cursor + } + # move cursor and top-of-screen to start of that line + move-to-start-of-line editor + top-of-screen:address:address:duplex-list <- get-address *editor, top-of-screen:offset + *top-of-screen <- copy *before-cursor + reply editor/same-as-ingredient:0 +] + +scenario editor-does-not-scroll-past-end [ + assume-screen 10/width, 4/height + 1:address:array:character <- new [a +b] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + editor-render screen, 2:address:editor-data + screen-should-contain [ + . . + .a . + .b . + .┈┈┈┈┈┈┈┈┈┈. + ] + # scroll down + assume-console [ + press page-down + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + # screen remains unmodified + screen-should-contain [ + . . + .a . + .b . + .┈┈┈┈┈┈┈┈┈┈. + ] +] + +scenario editor-starts-next-page-at-start-of-wrapped-line [ + # screen has 1 line for menu + 3 lines for text + assume-screen 10/width, 4/height + # editor contains a long last line + 1:address:array:character <- new [a +b +cdefgh] + # editor screen triggers wrap of last line + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 4/right + # some part of last line is not displayed + screen-should-contain [ + . . + .a . + .b . + .cde↩ . + ] + # scroll down + assume-console [ + press page-down + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + # screen shows entire wrapped line + screen-should-contain [ + . . + .cde↩ . + .fgh . + .┈┈┈┈ . + ] +] + +scenario editor-starts-next-page-at-start-of-wrapped-line-2 [ + # screen has 1 line for menu + 3 lines for text + assume-screen 10/width, 4/height + # editor contains a very long line that occupies last two lines of screen + # and still has something left over + 1:address:array:character <- new [a +bcdefgh] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 4/right + # some part of last line is not displayed + screen-should-contain [ + . . + .a . + .bcd↩ . + .efg↩ . + ] + # scroll down + assume-console [ + press page-down + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + # screen shows entire wrapped line + screen-should-contain [ + . . + .bcd↩ . + .efg↩ . + .h . + ] +] + +# ctrl-b/page-up - render previous page if it exists + +scenario editor-can-scroll-up [ + assume-screen 10/width, 4/height + 1:address:array:character <- new [a +b +c +d] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + screen-should-contain [ + . . + .a . + .b . + .c . + ] + # scroll down + assume-console [ + press page-down + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + # screen shows next page + screen-should-contain [ + . . + .c . + .d . + .┈┈┈┈┈┈┈┈┈┈. + ] + # scroll back up + assume-console [ + press page-up + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + # screen shows original page again + screen-should-contain [ + . . + .a . + .b . + .c . + ] +] + +after <handle-special-character> [ + { + page-up?:boolean <- equal *c, 2/ctrl-b + break-unless page-up? + top-of-screen:address:address:duplex-list <- get-address *editor, top-of-screen:offset + old-top:address:duplex-list <- copy *top-of-screen + <move-cursor-begin> + editor <- page-up editor, screen-height + undo-coalesce-tag:number <- copy 0/never + <move-cursor-end> + no-movement?:boolean <- equal *top-of-screen, old-top + reply-if no-movement?, screen/same-as-ingredient:0, editor/same-as-ingredient:1, 0/no-more-render + reply screen/same-as-ingredient:0, editor/same-as-ingredient:1, 1/go-render + } +] + +after <handle-special-key> [ + { + page-up?:boolean <- equal *k, 65519/page-up + break-unless page-up? + top-of-screen:address:address:duplex-list <- get-address *editor, top-of-screen:offset + old-top:address:duplex-list <- copy *top-of-screen + <move-cursor-begin> + editor <- page-up editor, screen-height + undo-coalesce-tag:number <- copy 0/never + <move-cursor-end> + no-movement?:boolean <- equal *top-of-screen, old-top + # don't bother re-rendering if nothing changed. todo: test this + reply-if no-movement?, screen/same-as-ingredient:0, editor/same-as-ingredient:1, 0/no-more-render + reply screen/same-as-ingredient:0, editor/same-as-ingredient:1, 1/go-render + } +] + +recipe page-up [ + local-scope + editor:address:editor-data <- next-ingredient + screen-height:number <- next-ingredient + max:number <- subtract screen-height, 1/menu-bar, 1/overlapping-line + count:number <- copy 0 + top-of-screen:address:address:duplex-list <- get-address *editor, top-of-screen:offset + { + done?:boolean <- greater-or-equal count, max + break-if done? + prev:address:duplex-list <- before-previous-line *top-of-screen, editor + break-unless prev + *top-of-screen <- copy prev + count <- add count, 1 + loop + } + reply editor/same-as-ingredient:0 +] + +scenario editor-can-scroll-up-multiple-pages [ + # screen has 1 line for menu + 3 lines + assume-screen 10/width, 4/height + # initialize editor with 8 lines + 1:address:array:character <- new [a +b +c +d +e +f +g +h] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + screen-should-contain [ + . . + .a . + .b . + .c . + ] + # scroll down two pages + assume-console [ + press page-down + press page-down + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + # screen shows third page + screen-should-contain [ + . . + .e . + .f . + .g . + ] + # scroll up + assume-console [ + press page-up + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + # screen shows second page + screen-should-contain [ + . . + .c . + .d . + .e . + ] + # scroll up again + assume-console [ + press page-up + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + # screen shows original page again + screen-should-contain [ + . . + .a . + .b . + .c . + ] +] + +scenario editor-can-scroll-up-wrapped-lines [ + # screen has 1 line for menu + 5 lines for text + assume-screen 10/width, 6/height + # editor contains a long line in the first page + 1:address:array:character <- new [a +b +cdefgh +i +j +k +l +m +n +o] + # editor screen triggers wrap of last line + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 4/right + # some part of last line is not displayed + screen-should-contain [ + . . + .a . + .b . + .cde↩ . + .fgh . + .i . + ] + # scroll down a page and a line + assume-console [ + press page-down + left-click 5, 0 + press down-arrow + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + # screen shows entire wrapped line + screen-should-contain [ + . . + .j . + .k . + .l . + .m . + .n . + ] + # now scroll up one page + assume-console [ + press page-up + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + # screen resets + screen-should-contain [ + . . + .b . + .cde↩ . + .fgh . + .i . + .j . + ] +] + +scenario editor-can-scroll-up-wrapped-lines-2 [ + # screen has 1 line for menu + 3 lines for text + assume-screen 10/width, 4/height + # editor contains a very long line that occupies last two lines of screen + # and still has something left over + 1:address:array:character <- new [a +bcdefgh] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 4/right + # some part of last line is not displayed + screen-should-contain [ + . . + .a . + .bcd↩ . + .efg↩ . + ] + # scroll down + assume-console [ + press page-down + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + # screen shows entire wrapped line + screen-should-contain [ + . . + .bcd↩ . + .efg↩ . + .h . + ] + # scroll back up + assume-console [ + press page-up + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + # screen resets + screen-should-contain [ + . . + .a . + .bcd↩ . + .efg↩ . + ] +] + +scenario editor-can-scroll-up-past-nonempty-lines [ + assume-screen 10/width, 4/height + # text with empty line in second screen + 1:address:array:character <- new [axx +bxx +cxx +dxx +exx +fxx +gxx +hxx +] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 4/right + screen-should-contain [ + . . + .axx . + .bxx . + .cxx . + ] + assume-console [ + press page-down + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + screen-should-contain [ + . . + .cxx . + .dxx . + .exx . + ] + assume-console [ + press page-down + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + screen-should-contain [ + . . + .exx . + .fxx . + .gxx . + ] + # scroll back up past empty line + assume-console [ + press page-up + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + screen-should-contain [ + . . + .cxx . + .dxx . + .exx . + ] +] + +scenario editor-can-scroll-up-past-empty-lines [ + assume-screen 10/width, 4/height + # text with empty line in second screen + 1:address:array:character <- new [axy +bxy +cxy + +dxy +exy +fxy +gxy +] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 4/right + screen-should-contain [ + . . + .axy . + .bxy . + .cxy . + ] + assume-console [ + press page-down + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + screen-should-contain [ + . . + .cxy . + . . + .dxy . + ] + assume-console [ + press page-down + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + screen-should-contain [ + . . + .dxy . + .exy . + .fxy . + ] + # scroll back up past empty line + assume-console [ + press page-up + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + screen-should-contain [ + . . + .cxy . + . . + .dxy . + ] +] diff --git a/sandbox/004-programming-environment.mu b/sandbox/004-programming-environment.mu new file mode 100644 index 00000000..864946aa --- /dev/null +++ b/sandbox/004-programming-environment.mu @@ -0,0 +1,360 @@ +## putting the environment together out of editors + +recipe! main [ + local-scope + open-console + initial-sandbox:address:array:character <- new [] + hide-screen 0/screen + env:address:programming-environment-data <- new-programming-environment 0/screen, initial-sandbox + env <- restore-sandboxes env + render-sandbox-side 0/screen, env + current-sandbox:address:editor-data <- get *env, current-sandbox:offset + update-cursor 0/screen, current-sandbox + show-screen 0/screen + event-loop 0/screen, 0/console, env + # never gets here +] + +container programming-environment-data [ + current-sandbox:address:editor-data +] + +recipe new-programming-environment [ + local-scope + screen:address <- next-ingredient + initial-sandbox-contents:address:array:character <- next-ingredient + width:number <- screen-width screen + height:number <- screen-height screen + # top menu + result:address:programming-environment-data <- new programming-environment-data:type + draw-horizontal screen, 0, 0/left, width, 32/space, 0/black, 238/grey + button-start:number <- subtract width, 20 + button-on-screen?:boolean <- greater-or-equal button-start, 0 + assert button-on-screen?, [screen too narrow for menu] + screen <- move-cursor screen, 0/row, button-start + run-button:address:array:character <- new [ run (F4) ] + print-string screen, run-button, 255/white, 161/reddish + # sandbox editor + current-sandbox:address:address:editor-data <- get-address *result, current-sandbox:offset + *current-sandbox <- new-editor initial-sandbox-contents, screen, 0, width/right + +programming-environment-initialization + reply result +] + +recipe event-loop [ + local-scope + screen:address <- next-ingredient + console:address <- next-ingredient + env:address:programming-environment-data <- next-ingredient + current-sandbox:address:editor-data <- get *env, current-sandbox:offset + # if we fall behind we'll stop updating the screen, but then we have to + # render the entire screen when we catch up. + # todo: test this + render-all-on-no-more-events?:boolean <- copy 0/false + { + # looping over each (keyboard or touch) event as it occurs + +next-event + e:event, console, found?:boolean, quit?:boolean <- read-event console + loop-unless found? + break-if quit? # only in tests + trace 10, [app], [next-event] + <handle-event> + # check for global events that will trigger regardless of which editor has focus + { + k:address:number <- maybe-convert e:event, keycode:variant + break-unless k + <global-keypress> + } + { + c:address:character <- maybe-convert e:event, text:variant + break-unless c + <global-type> + } + # 'touch' event + { + t:address:touch-event <- maybe-convert e:event, touch:variant + break-unless t + # ignore all but 'left-click' events for now + # todo: test this + touch-type:number <- get *t, type:offset + is-left-click?:boolean <- equal touch-type, 65513/mouse-left + loop-unless is-left-click?, +next-event:label + # later exceptions for non-editor touches will go here + <global-touch> + move-cursor-in-editor screen, current-sandbox, *t + screen <- update-cursor screen, current-sandbox + loop +next-event:label + } + # 'resize' event - redraw editor + # todo: test this after supporting resize in assume-console + { + r:address:resize-event <- maybe-convert e:event, resize:variant + break-unless r + # if more events, we're still resizing; wait until we stop + more-events?:boolean <- has-more-events? console + { + break-unless more-events? + render-all-on-no-more-events? <- copy 1/true # no rendering now, full rendering on some future event + } + { + break-if more-events? + env <- resize screen, env + screen <- render-all screen, env + render-all-on-no-more-events? <- copy 0/false # full render done + } + loop +next-event:label + } + # if it's not global and not a touch event, send to appropriate editor + { + hide-screen screen + screen, current-sandbox, render?:boolean <- handle-keyboard-event screen, current-sandbox, e:event + # refresh screen only if no more events + # if there are more events to process, wait for them to clear up, then make sure you render-all afterward. + more-events?:boolean <- has-more-events? console + { + break-unless more-events? + render-all-on-no-more-events? <- copy 1/true # no rendering now, full rendering on some future event + jump +finish-event:label + } + { + break-if more-events? + { + break-unless render-all-on-no-more-events? + # no more events, and we have to force render + screen <- render-all screen, env + render-all-on-no-more-events? <- copy 0/false + jump +finish-event:label + } + # no more events, no force render + { + break-unless render? + screen <- render-sandbox-side screen, env + jump +finish-event:label + } + } + +finish-event + screen <- update-cursor screen, current-sandbox + show-screen screen + } + loop + } +] + +recipe resize [ + local-scope + screen:address <- next-ingredient + env:address:programming-environment-data <- next-ingredient + clear-screen screen # update screen dimensions + width:number <- screen-width screen + # update sandbox editor + current-sandbox:address:editor-data <- get *env, current-sandbox:offset + right:address:number <- get-address *current-sandbox, right:offset + *right <- subtract width, 1 + # reset cursor + cursor-row:address:number <- get-address *current-sandbox, cursor-row:offset + *cursor-row <- copy 1 + cursor-column:address:number <- get-address *current-sandbox, cursor-column:offset + *cursor-column <- copy 0 + reply env/same-as-ingredient:1 +] + +recipe render-all [ + local-scope + screen:address <- next-ingredient + env:address:programming-environment-data <- next-ingredient + trace 10, [app], [render all] + hide-screen screen + # top menu + trace 11, [app], [render top menu] + width:number <- screen-width screen + draw-horizontal screen, 0, 0/left, width, 32/space, 0/black, 238/grey + button-start:number <- subtract width, 20 + button-on-screen?:boolean <- greater-or-equal button-start, 0 + assert button-on-screen?, [screen too narrow for menu] + screen <- move-cursor screen, 0/row, button-start + run-button:address:array:character <- new [ run (F4) ] + print-string screen, run-button, 255/white, 161/reddish + # + screen <- render-sandbox-side screen, env + <render-components-end> + # + current-sandbox:address:editor-data <- get *env, current-sandbox:offset + screen <- update-cursor screen, current-sandbox + # + show-screen screen + reply screen/same-as-ingredient:0 +] + +# replaced in a later layer +recipe render-sandbox-side [ + local-scope + screen:address <- next-ingredient + env:address:programming-environment-data <- next-ingredient + current-sandbox:address:editor-data <- get *env, current-sandbox:offset + left:number <- get *current-sandbox, left:offset + right:number <- get *current-sandbox, right:offset + row:number, column:number, screen, current-sandbox <- render screen, current-sandbox + clear-line-delimited screen, column, right + row <- add row, 1 + # draw solid line after code + draw-horizontal screen, row, left, right, 9473/horizontal + row <- add row, 1 + clear-screen-from screen, row, left, left, right + reply screen/same-as-ingredient:0 +] + +recipe update-cursor [ + local-scope + screen:address <- next-ingredient + current-sandbox:address:editor-data <- next-ingredient + cursor-row:number <- get *current-sandbox, cursor-row:offset + cursor-column:number <- get *current-sandbox, cursor-column:offset + screen <- move-cursor screen, cursor-row, cursor-column + reply screen/same-as-ingredient:0 +] + +# row, screen <- render-string screen:address, s:address:array:character, left:number, right:number, color:number, row:number +# print a string 's' to 'editor' in 'color' starting at 'row' +# clear rest of last line, move cursor to next line +recipe render-string [ + local-scope + screen:address <- next-ingredient + s:address:array:character <- next-ingredient + left:number <- next-ingredient + right:number <- next-ingredient + color:number <- next-ingredient + row:number <- next-ingredient + reply-unless s, row/same-as-ingredient:5, screen/same-as-ingredient:0 + column:number <- copy left + screen <- move-cursor screen, row, column + screen-height:number <- screen-height screen + i:number <- copy 0 + len:number <- length *s + { + +next-character + done?:boolean <- greater-or-equal i, len + break-if done? + done? <- greater-or-equal row, screen-height + break-if done? + c:character <- index *s, i + { + # at right? wrap. + at-right?:boolean <- equal column, right + break-unless at-right? + # print wrap icon + print-character screen, 8617/loop-back-to-left, 245/grey + column <- copy left + row <- add row, 1 + screen <- move-cursor screen, row, column + loop +next-character:label # retry i + } + i <- add i, 1 + { + # newline? move to left rather than 0 + newline?:boolean <- equal c, 10/newline + break-unless newline? + # clear rest of line in this window + { + done?:boolean <- greater-than column, right + break-if done? + print-character screen, 32/space + column <- add column, 1 + loop + } + row <- add row, 1 + column <- copy left + screen <- move-cursor screen, row, column + loop +next-character:label + } + print-character screen, c, color + column <- add column, 1 + loop + } + was-at-left?:boolean <- equal column, left + clear-line-delimited screen, column, right + { + break-if was-at-left? + row <- add row, 1 + } + move-cursor row, left + reply row/same-as-ingredient:5, screen/same-as-ingredient:0 +] + +# row, screen <- render-code-string screen:address, s:address:array:character, left:number, right:number, row:number +# like 'render-string' but with colorization for comments like in the editor +recipe render-code-string [ + local-scope + screen:address <- next-ingredient + s:address:array:character <- next-ingredient + left:number <- next-ingredient + right:number <- next-ingredient + row:number <- next-ingredient + reply-unless s, row/same-as-ingredient:4, screen/same-as-ingredient:0 + color:number <- copy 7/white + column:number <- copy left + screen <- move-cursor screen, row, column + screen-height:number <- screen-height screen + i:number <- copy 0 + len:number <- length *s + { + +next-character + done?:boolean <- greater-or-equal i, len + break-if done? + done? <- greater-or-equal row, screen-height + break-if done? + c:character <- index *s, i + <character-c-received> # only line different from render-string + { + # at right? wrap. + at-right?:boolean <- equal column, right + break-unless at-right? + # print wrap icon + print-character screen, 8617/loop-back-to-left, 245/grey + column <- copy left + row <- add row, 1 + screen <- move-cursor screen, row, column + loop +next-character:label # retry i + } + i <- add i, 1 + { + # newline? move to left rather than 0 + newline?:boolean <- equal c, 10/newline + break-unless newline? + # clear rest of line in this window + { + done?:boolean <- greater-than column, right + break-if done? + print-character screen, 32/space + column <- add column, 1 + loop + } + row <- add row, 1 + column <- copy left + screen <- move-cursor screen, row, column + loop +next-character:label + } + print-character screen, c, color + column <- add column, 1 + loop + } + was-at-left?:boolean <- equal column, left + clear-line-delimited screen, column, right + { + break-if was-at-left? + row <- add row, 1 + } + move-cursor row, left + reply row/same-as-ingredient:4, screen/same-as-ingredient:0 +] + +# ctrl-l - redraw screen (just in case it printed junk somehow) + +after <global-type> [ + { + redraw-screen?:boolean <- equal *c, 12/ctrl-l + break-unless redraw-screen? + screen <- render-all screen, env:address:programming-environment-data + sync-screen screen + loop +next-event:label + } +] diff --git a/sandbox/005-sandbox.mu b/sandbox/005-sandbox.mu new file mode 100644 index 00000000..f3cf94a8 --- /dev/null +++ b/sandbox/005-sandbox.mu @@ -0,0 +1,456 @@ +## running code from the editor and creating sandboxes +# +# Running code in the sandbox editor prepends its contents to a list of +# (non-editable) sandboxes below the editor, showing the result and a maybe +# few other things. + +container programming-environment-data [ + sandbox:address:sandbox-data # list of sandboxes, from top to bottom +] + +container sandbox-data [ + data:address:array:character + response:address:array:character + expected-response:address:array:character + # coordinates to track clicks + starting-row-on-screen:number + code-ending-row-on-screen:number # past end of code + response-starting-row-on-screen:number + screen:address:screen # prints in the sandbox go here + next-sandbox:address:sandbox-data +] + +scenario run-and-show-results [ + $close-trace # trace too long + assume-screen 50/width, 15/height + # sandbox editor contains an instruction without storing outputs + 1:address:array:character <- new [divide-with-remainder 11, 3] + 2:address:programming-environment-data <- new-programming-environment screen:address, 1:address:array:character + # run the code in the editors + assume-console [ + press F4 + ] + run [ + event-loop screen:address, console:address, 2:address:programming-environment-data + ] + # check that screen prints the results + screen-should-contain [ + . run (F4) . + . . + .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━. + . x. + .divide-with-remainder 11, 3 . + .3 . + .2 . + .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━. + . . + ] + screen-should-contain-in-color 7/white, [ + . . + . . + . . + . . + .divide-with-remainder 11, 3 . + . . + . . + . . + . . + ] + screen-should-contain-in-color 245/grey, [ + . . + . . + .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━. + . x. + . . + .3 . + .2 . + .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━. + . . + ] + # run another command + assume-console [ + left-click 1, 80 + type [add 2, 2] + press F4 + ] + run [ + event-loop screen:address, console:address, 2:address:programming-environment-data + ] + # check that screen prints both sandboxes + screen-should-contain [ + . run (F4) . + . . + .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━. + . x. + .add 2, 2 . + .4 . + .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━. + . x. + .divide-with-remainder 11, 3 . + .3 . + .2 . + .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━. + . . + ] +] + +after <global-keypress> [ + # F4? load all code and run all sandboxes. + { + do-run?:boolean <- equal *k, 65532/F4 + break-unless do-run? + status:address:array:character <- new [running... ] + screen <- update-status screen, status, 245/grey + error?:boolean, env, screen <- run-sandboxes env, screen + # F4 might update warnings and results on both sides + screen <- render-all screen, env + { + break-if error? + status:address:array:character <- new [ ] + screen <- update-status screen, status, 245/grey + } + screen <- update-cursor screen, current-sandbox + loop +next-event:label + } +] + +recipe run-sandboxes [ + local-scope + env:address:programming-environment-data <- next-ingredient + screen:address <- next-ingredient + stop?:boolean, env, screen <- update-recipes env, screen + reply-if stop?, 1/errors-found, env/same-as-ingredient:0, screen/same-as-ingredient:1 + # check contents of editor + current-sandbox:address:editor-data <- get *env, current-sandbox:offset + { + sandbox-contents:address:array:character <- editor-contents current-sandbox + break-unless sandbox-contents + # if contents exist, first save them + # run them and turn them into a new sandbox-data + new-sandbox:address:sandbox-data <- new sandbox-data:type + data:address:address:array:character <- get-address *new-sandbox, data:offset + *data <- copy sandbox-contents + # push to head of sandbox list + dest:address:address:sandbox-data <- get-address *env, sandbox:offset + next:address:address:sandbox-data <- get-address *new-sandbox, next-sandbox:offset + *next <- copy *dest + *dest <- copy new-sandbox + # clear sandbox editor + init:address:address:duplex-list <- get-address *current-sandbox, data:offset + *init <- push-duplex 167/§, 0/tail + top-of-screen:address:address:duplex-list <- get-address *current-sandbox, top-of-screen:offset + *top-of-screen <- copy *init + } + # save all sandboxes before running, just in case we die when running + save-sandboxes env + # run all sandboxes + curr:address:sandbox-data <- get *env, sandbox:offset + { + break-unless curr + update-sandbox curr + curr <- get *curr, next-sandbox:offset + loop + } + reply 0/no-errors-found, env/same-as-ingredient:0, screen/same-as-ingredient:1 +] + +# load code from recipes.mu +# replaced in a later layer +recipe update-recipes [ + local-scope + env:address:programming-environment-data <- next-ingredient + screen:address <- next-ingredient + in:address:array:character <- restore [recipes.mu] + reload in + reply 0/no-errors-found, env/same-as-ingredient:0, screen/same-as-ingredient:1 +] + +# replaced in a later layer +recipe update-sandbox [ + local-scope + sandbox:address:sandbox-data <- next-ingredient + data:address:array:character <- get *sandbox, data:offset + response:address:address:array:character <- get-address *sandbox, response:offset + fake-screen:address:address:screen <- get-address *sandbox, screen:offset + *response, _, *fake-screen <- run-interactive data +] + +recipe update-status [ + local-scope + screen:address <- next-ingredient + msg:address:array:character <- next-ingredient + color:number <- next-ingredient + screen <- move-cursor screen, 0, 2 + screen <- print-string screen, msg, color, 238/grey/background + reply screen/same-as-ingredient:0 +] + +recipe save-sandboxes [ + local-scope + env:address:programming-environment-data <- next-ingredient + current-sandbox:address:editor-data <- get *env, current-sandbox:offset + # first clear previous versions, in case we deleted some sandbox + $system [rm lesson/[0-9]* >/dev/null 2>/dev/null] # some shells can't handle '>&' + curr:address:sandbox-data <- get *env, sandbox:offset + suffix:address:array:character <- new [.out] + idx:number <- copy 0 + { + break-unless curr + data:address:array:character <- get *curr, data:offset + filename:address:array:character <- integer-to-decimal-string idx + save filename, data + { + expected-response:address:array:character <- get *curr, expected-response:offset + break-unless expected-response + filename <- string-append filename, suffix + save filename, expected-response + } + idx <- add idx, 1 + curr <- get *curr, next-sandbox:offset + loop + } +] + +recipe! render-sandbox-side [ + local-scope + screen:address <- next-ingredient + env:address:programming-environment-data <- next-ingredient + trace 11, [app], [render sandbox side] + current-sandbox:address:editor-data <- get *env, current-sandbox:offset + left:number <- get *current-sandbox, left:offset + right:number <- get *current-sandbox, right:offset + row:number, column:number, screen, current-sandbox <- render screen, current-sandbox + clear-screen-from screen, row, column, left, right + row <- add row, 1 + draw-horizontal screen, row, left, right, 9473/horizontal-double + sandbox:address:sandbox-data <- get *env, sandbox:offset + row, screen <- render-sandboxes screen, sandbox, left, right, row + clear-rest-of-screen screen, row, left, left, right + reply screen/same-as-ingredient:0 +] + +recipe render-sandboxes [ + local-scope + screen:address <- next-ingredient + sandbox:address:sandbox-data <- next-ingredient + left:number <- next-ingredient + right:number <- next-ingredient + row:number <- next-ingredient + reply-unless sandbox, row/same-as-ingredient:4, screen/same-as-ingredient:0 + screen-height:number <- screen-height screen + at-bottom?:boolean <- greater-or-equal row, screen-height + reply-if at-bottom?:boolean, row/same-as-ingredient:4, screen/same-as-ingredient:0 + # render sandbox menu + row <- add row, 1 + screen <- move-cursor screen, row, left + clear-line-delimited screen, left, right + print-character screen, 120/x, 245/grey + # save menu row so we can detect clicks to it later + starting-row:address:number <- get-address *sandbox, starting-row-on-screen:offset + *starting-row <- copy row + # render sandbox contents + row <- add row, 1 + screen <- move-cursor screen, row, left + sandbox-data:address:array:character <- get *sandbox, data:offset + row, screen <- render-code-string screen, sandbox-data, left, right, row + code-ending-row:address:number <- get-address *sandbox, code-ending-row-on-screen:offset + *code-ending-row <- copy row + # render sandbox warnings, screen or response, in that order + response-starting-row:address:number <- get-address *sandbox, response-starting-row-on-screen:offset + sandbox-response:address:array:character <- get *sandbox, response:offset + <render-sandbox-results> + { + sandbox-screen:address <- get *sandbox, screen:offset + empty-screen?:boolean <- fake-screen-is-empty? sandbox-screen + break-if empty-screen? + row, screen <- render-screen screen, sandbox-screen, left, right, row + } + { + break-unless empty-screen? + *response-starting-row <- copy row + <render-sandbox-response> + row, screen <- render-string screen, sandbox-response, left, right, 245/grey, row + } + +render-sandbox-end + at-bottom?:boolean <- greater-or-equal row, screen-height + reply-if at-bottom?, row/same-as-ingredient:4, screen/same-as-ingredient:0 + # draw solid line after sandbox + draw-horizontal screen, row, left, right, 9473/horizontal-double + # draw next sandbox + next-sandbox:address:sandbox-data <- get *sandbox, next-sandbox:offset + row, screen <- render-sandboxes screen, next-sandbox, left, right, row + reply row/same-as-ingredient:4, screen/same-as-ingredient:0 +] + +# assumes programming environment has no sandboxes; restores them from previous session +recipe restore-sandboxes [ + local-scope + env:address:programming-environment-data <- next-ingredient + # read all scenarios, pushing them to end of a list of scenarios + suffix:address:array:character <- new [.out] + idx:number <- copy 0 + curr:address:address:sandbox-data <- get-address *env, sandbox:offset + { + filename:address:array:character <- integer-to-decimal-string idx + contents:address:array:character <- restore filename + break-unless contents # stop at first error; assuming file didn't exist + # create new sandbox for file + *curr <- new sandbox-data:type + data:address:address:array:character <- get-address **curr, data:offset + *data <- copy contents + # restore expected output for sandbox if it exists + { + filename <- string-append filename, suffix + contents <- restore filename + break-unless contents + expected-response:address:address:array:character <- get-address **curr, expected-response:offset + *expected-response <- copy contents + } + +continue + idx <- add idx, 1 + curr <- get-address **curr, next-sandbox:offset + loop + } + reply env/same-as-ingredient:0 +] + +# row, screen <- render-screen screen:address, sandbox-screen:address, left:number, right:number, row:number +# print the fake sandbox screen to 'screen' with appropriate delimiters +# leave cursor at start of next line +recipe render-screen [ + local-scope + screen:address <- next-ingredient + s:address:screen <- next-ingredient + left:number <- next-ingredient + right:number <- next-ingredient + row:number <- next-ingredient + reply-unless s, row/same-as-ingredient:4, screen/same-as-ingredient:0 + # print 'screen:' + header:address:array:character <- new [screen:] + row <- render-string screen, header, left, right, 245/grey, row + screen <- move-cursor screen, row, left + # start printing s + column:number <- copy left + s-width:number <- screen-width s + s-height:number <- screen-height s + buf:address:array:screen-cell <- get *s, data:offset + stop-printing:number <- add left, s-width, 3 + max-column:number <- min stop-printing, right + i:number <- copy 0 + len:number <- length *buf + screen-height:number <- screen-height screen + { + done?:boolean <- greater-or-equal i, len + break-if done? + done? <- greater-or-equal row, screen-height + break-if done? + column <- copy left + screen <- move-cursor screen, row, column + # initial leader for each row: two spaces and a '.' + print-character screen, 32/space, 245/grey + print-character screen, 32/space, 245/grey + print-character screen, 46/full-stop, 245/grey + column <- add left, 3 + { + # print row + row-done?:boolean <- greater-or-equal column, max-column + break-if row-done? + curr:screen-cell <- index *buf, i + c:character <- get curr, contents:offset + color:number <- get curr, color:offset + { + # damp whites down to grey + white?:boolean <- equal color, 7/white + break-unless white? + color <- copy 245/grey + } + print-character screen, c, color + column <- add column, 1 + i <- add i, 1 + loop + } + # print final '.' + print-character screen, 46/full-stop, 245/grey + column <- add column, 1 + { + # clear rest of current line + line-done?:boolean <- greater-than column, right + break-if line-done? + print-character screen, 32/space + column <- add column, 1 + loop + } + row <- add row, 1 + loop + } + reply row/same-as-ingredient:4, screen/same-as-ingredient:0 +] + +scenario run-instruction-manages-screen-per-sandbox [ + $close-trace # trace too long + assume-screen 50/width, 20/height + # editor contains an instruction + 1:address:array:character <- new [print-integer screen:address, 4] + 2:address:programming-environment-data <- new-programming-environment screen:address, 1:address:array:character + # run the code in the editor + assume-console [ + press F4 + ] + run [ + event-loop screen:address, console:address, 2:address:programming-environment-data + ] + # check that it prints a little toy screen + screen-should-contain [ + . run (F4) . + . . + .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━. + . x. + .print-integer screen:address, 4 . + .screen: . + . .4 . . + . . . . + . . . . + . . . . + . . . . + .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━. + . . + ] +] + +recipe editor-contents [ + local-scope + editor:address:editor-data <- next-ingredient + buf:address:buffer <- new-buffer 80 + curr:address:duplex-list <- get *editor, data:offset + # skip § sentinel + assert curr, [editor without data is illegal; must have at least a sentinel] + curr <- next-duplex curr + reply-unless curr, 0 + { + break-unless curr + c:character <- get *curr, value:offset + buffer-append buf, c + curr <- next-duplex curr + loop + } + result:address:array:character <- buffer-to-array buf + reply result +] + +scenario editor-provides-edited-contents [ + assume-screen 10/width, 5/height + 1:address:array:character <- new [abc] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + assume-console [ + left-click 1, 2 + type [def] + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + 3:address:array:character <- editor-contents 2:address:editor-data + 4:array:character <- copy *3:address:array:character + ] + memory-should-contain [ + 4:string <- [abdefc] + ] +] diff --git a/sandbox/006-sandbox-edit.mu b/sandbox/006-sandbox-edit.mu new file mode 100644 index 00000000..15872675 --- /dev/null +++ b/sandbox/006-sandbox-edit.mu @@ -0,0 +1,176 @@ +## editing sandboxes after they've been created + +scenario clicking-on-a-sandbox-moves-it-to-editor [ + $close-trace # trace too long + assume-screen 40/width, 10/height + # run something + 1:address:array:character <- new [add 2, 2] + assume-console [ + press F4 + ] + 2:address:programming-environment-data <- new-programming-environment screen:address, 1:address:array:character + event-loop screen:address, console:address, 2:address:programming-environment-data + screen-should-contain [ + . run (F4) . + . . + .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━. + . x. + .add 2, 2 . + .4 . + .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━. + . . + . . + . . + ] + # click somewhere on the sandbox + assume-console [ + left-click 3, 0 + ] + run [ + event-loop screen:address, console:address, 2:address:programming-environment-data + ] + # it pops back into editor + screen-should-contain [ + . run (F4) . + .add 2, 2 . + .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━. + . . + . . + . . + . . + . . + . . + . . + ] + # cursor should be in the right place + assume-console [ + type [0] + ] + run [ + event-loop screen:address, console:address, 2:address:programming-environment-data + ] + screen-should-contain [ + . run (F4) . + .0add 2, 2 . + .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━. + . . + . . + . . + . . + . . + . . + . . + ] +] + +after <global-touch> [ + # below editor? pop appropriate sandbox contents back into sandbox editor provided it's empty + { + sandbox-left-margin:number <- get *current-sandbox, left:offset + click-column:number <- get *t, column:offset + on-sandbox-side?:boolean <- greater-or-equal click-column, sandbox-left-margin + break-unless on-sandbox-side? + first-sandbox:address:sandbox-data <- get *env, sandbox:offset + break-unless first-sandbox + first-sandbox-begins:number <- get *first-sandbox, starting-row-on-screen:offset + click-row:number <- get *t, row:offset + below-sandbox-editor?:boolean <- greater-or-equal click-row, first-sandbox-begins + break-unless below-sandbox-editor? + empty-sandbox-editor?:boolean <- empty-editor? current-sandbox + break-unless empty-sandbox-editor? # make the user hit F4 before editing a new sandbox + # identify the sandbox to edit and remove it from the sandbox list + sandbox:address:sandbox-data <- extract-sandbox env, click-row + text:address:array:character <- get *sandbox, data:offset + current-sandbox <- insert-text current-sandbox, text + hide-screen screen + screen <- render-sandbox-side screen, env + screen <- update-cursor screen, current-sandbox + show-screen screen + loop +next-event:label + } +] + +recipe empty-editor? [ + local-scope + editor:address:editor-data <- next-ingredient + head:address:duplex-list <- get *editor, data:offset + first:address:duplex-list <- next-duplex head + result:boolean <- not first + reply result +] + +recipe extract-sandbox [ + local-scope + env:address:programming-environment-data <- next-ingredient + click-row:number <- next-ingredient + # assert click-row >= sandbox.starting-row-on-screen + sandbox:address:address:sandbox-data <- get-address *env, sandbox:offset + start:number <- get **sandbox, starting-row-on-screen:offset + clicked-on-sandboxes?:boolean <- greater-or-equal click-row, start + assert clicked-on-sandboxes?, [extract-sandbox called on click to sandbox editor] + { + next-sandbox:address:sandbox-data <- get **sandbox, next-sandbox:offset + break-unless next-sandbox + # if click-row < sandbox.next-sandbox.starting-row-on-screen, break + next-start:number <- get *next-sandbox, starting-row-on-screen:offset + found?:boolean <- lesser-than click-row, next-start + break-if found? + sandbox <- get-address **sandbox, next-sandbox:offset + loop + } + # snip sandbox out of its list + result:address:sandbox-data <- copy *sandbox + *sandbox <- copy next-sandbox + reply result +] + +scenario sandbox-with-print-can-be-edited [ + $close-trace # trace too long + assume-screen 50/width, 20/height + # run a print instruction + 1:address:array:character <- new [print-integer screen:address, 4] + 2:address:programming-environment-data <- new-programming-environment screen:address, 1:address:array:character + # run the sandbox + assume-console [ + press F4 + ] + run [ + event-loop screen:address, console:address, 2:address:programming-environment-data + ] + screen-should-contain [ + . run (F4) . + . . + .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━. + . x. + .print-integer screen:address, 4 . + .screen: . + . .4 . . + . . . . + . . . . + . . . . + . . . . + .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━. + . . + . . + . . + . . + . . + . . + . . + . . + ] + # edit the sandbox + assume-console [ + left-click 3, 70 + ] + run [ + event-loop screen:address, console:address, 2:address:programming-environment-data + ] + screen-should-contain [ + . run (F4) . + .print-integer screen:address, 4 . + .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━. + . . + . . + ] +] diff --git a/sandbox/007-sandbox-delete.mu b/sandbox/007-sandbox-delete.mu new file mode 100644 index 00000000..a84da4fa --- /dev/null +++ b/sandbox/007-sandbox-delete.mu @@ -0,0 +1,108 @@ +## deleting sandboxes + +scenario deleting-sandboxes [ + $close-trace # trace too long + assume-screen 50/width, 15/height + 1:address:array:character <- new [] + 2:address:programming-environment-data <- new-programming-environment screen:address, 1:address:array:character + # run a few commands + assume-console [ + left-click 1, 0 + type [divide-with-remainder 11, 3] + press F4 + type [add 2, 2] + press F4 + ] + event-loop screen:address, console:address, 2:address:programming-environment-data + screen-should-contain [ + . run (F4) . + . . + .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━. + . x. + .add 2, 2 . + .4 . + .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━. + . x. + .divide-with-remainder 11, 3 . + .3 . + .2 . + .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━. + . . + ] + # delete second sandbox + assume-console [ + left-click 7, 49 + ] + run [ + event-loop screen:address, console:address, 2:address:programming-environment-data + ] + screen-should-contain [ + . run (F4) . + . . + .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━. + . x. + .add 2, 2 . + .4 . + .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━. + . . + . . + ] + # delete first sandbox + assume-console [ + left-click 3, 49 + ] + run [ + event-loop screen:address, console:address, 2:address:programming-environment-data + ] + screen-should-contain [ + . run (F4) . + . . + .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━. + . . + . . + ] +] + +after <global-touch> [ + # on a sandbox delete icon? process delete + { + was-delete?:boolean <- delete-sandbox *t, env + break-unless was-delete? + hide-screen screen + screen <- render-sandbox-side screen, env + screen <- update-cursor screen, current-sandbox + show-screen screen + loop +next-event:label + } +] + +# was-deleted?:boolean <- delete-sandbox t:touch-event, env:address:programming-environment-data +recipe delete-sandbox [ + local-scope + t:touch-event <- next-ingredient + env:address:programming-environment-data <- next-ingredient + click-column:number <- get t, column:offset + current-sandbox:address:editor-data <- get *env, current-sandbox:offset + right:number <- get *current-sandbox, right:offset + at-right?:boolean <- equal click-column, right + reply-unless at-right?, 0/false + click-row:number <- get t, row:offset + prev:address:address:sandbox-data <- get-address *env, sandbox:offset + curr:address:sandbox-data <- get *env, sandbox:offset + { + break-unless curr + # more sandboxes to check + { + target-row:number <- get *curr, starting-row-on-screen:offset + delete-curr?:boolean <- equal target-row, click-row + break-unless delete-curr? + # delete this sandbox, rerender and stop + *prev <- get *curr, next-sandbox:offset + reply 1/true + } + prev <- get-address *curr, next-sandbox:offset + curr <- get *curr, next-sandbox:offset + loop + } + reply 0/false +] diff --git a/sandbox/008-sandbox-test.mu b/sandbox/008-sandbox-test.mu new file mode 100644 index 00000000..bc6ca80a --- /dev/null +++ b/sandbox/008-sandbox-test.mu @@ -0,0 +1,94 @@ +## clicking on sandbox results to 'fix' them and turn sandboxes into tests + +# todo: perform test from edit/ by faking file system + +# clicks on sandbox responses save it as 'expected' +after <global-touch> [ + # check if it's inside the output of any sandbox + { + sandbox-left-margin:number <- get *current-sandbox, left:offset + click-column:number <- get *t, column:offset + on-sandbox-side?:boolean <- greater-or-equal click-column, sandbox-left-margin + break-unless on-sandbox-side? + first-sandbox:address:sandbox-data <- get *env, sandbox:offset + break-unless first-sandbox + first-sandbox-begins:number <- get *first-sandbox, starting-row-on-screen:offset + click-row:number <- get *t, row:offset + below-sandbox-editor?:boolean <- greater-or-equal click-row, first-sandbox-begins + break-unless below-sandbox-editor? + # identify the sandbox whose output is being clicked on + sandbox:address:sandbox-data <- find-click-in-sandbox-output env, click-row + break-unless sandbox + # toggle its expected-response, and save session + sandbox <- toggle-expected-response sandbox + save-sandboxes env + hide-screen screen + screen <- render-sandbox-side screen, env, 1/clear + screen <- update-cursor screen, current-sandbox + # no change in cursor + show-screen screen + loop +next-event:label + } +] + +recipe find-click-in-sandbox-output [ + local-scope + env:address:programming-environment-data <- next-ingredient + click-row:number <- next-ingredient + # assert click-row >= sandbox.starting-row-on-screen + sandbox:address:sandbox-data <- get *env, sandbox:offset + start:number <- get *sandbox, starting-row-on-screen:offset + clicked-on-sandboxes?:boolean <- greater-or-equal click-row, start + assert clicked-on-sandboxes?, [extract-sandbox called on click to sandbox editor] + # while click-row < sandbox.next-sandbox.starting-row-on-screen + { + next-sandbox:address:sandbox-data <- get *sandbox, next-sandbox:offset + break-unless next-sandbox + next-start:number <- get *next-sandbox, starting-row-on-screen:offset + found?:boolean <- lesser-than click-row, next-start + break-if found? + sandbox <- copy next-sandbox + loop + } + # return sandbox if click is in its output region + response-starting-row:number <- get *sandbox, response-starting-row-on-screen:offset + reply-unless response-starting-row, 0/no-click-in-sandbox-output + click-in-response?:boolean <- greater-or-equal click-row, response-starting-row + reply-unless click-in-response?, 0/no-click-in-sandbox-output + reply sandbox +] + +recipe toggle-expected-response [ + local-scope + sandbox:address:sandbox-data <- next-ingredient + expected-response:address:address:array:character <- get-address *sandbox, expected-response:offset + { + # if expected-response is set, reset + break-unless *expected-response + *expected-response <- copy 0 + reply sandbox/same-as-ingredient:0 + } + # if not, current response is the expected response + response:address:array:character <- get *sandbox, response:offset + *expected-response <- copy response + reply sandbox/same-as-ingredient:0 +] + +# when rendering a sandbox, color it in red/green if expected response exists +after <render-sandbox-response> [ + { + break-unless sandbox-response + expected-response:address:array:character <- get *sandbox, expected-response:offset + break-unless expected-response # fall-through to print in grey + response-is-expected?:boolean <- string-equal expected-response, sandbox-response + { + break-if response-is-expected?:boolean + row, screen <- render-string screen, sandbox-response, left, right, 1/red, row + } + { + break-unless response-is-expected?:boolean + row, screen <- render-string screen, sandbox-response, left, right, 2/green, row + } + jump +render-sandbox-end:label + } +] diff --git a/sandbox/009-sandbox-trace.mu b/sandbox/009-sandbox-trace.mu new file mode 100644 index 00000000..8c5b87d8 --- /dev/null +++ b/sandbox/009-sandbox-trace.mu @@ -0,0 +1,202 @@ +## clicking on the code typed into a sandbox toggles its trace + +scenario sandbox-click-on-code-toggles-app-trace [ + $close-trace # trace too long + assume-screen 40/width, 10/height + # run a stash instruction + 1:address:array:character <- new [stash [abc]] + assume-console [ + press F4 + ] + 2:address:programming-environment-data <- new-programming-environment screen:address, 1:address:array:character + event-loop screen:address, console:address, 2:address:programming-environment-data + screen-should-contain [ + . run (F4) . + . . + .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━. + . x. + .stash [abc] . + .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━. + . . + ] + # click on the code in the sandbox + assume-console [ + left-click 4, 21 + ] + run [ + event-loop screen:address, console:address, 2:address:programming-environment-data + print-character screen:address, 9251/␣/cursor + ] + # trace now printed and cursor shouldn't have budged + screen-should-contain [ + . run (F4) . + .␣ . + .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━. + . x. + .stash [abc] . + .abc . + .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━. + . . + ] + screen-should-contain-in-color 245/grey, [ + . . + . . + .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━. + . x. + . . + .abc . + .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━. + . . + ] + # click again on the same region + assume-console [ + left-click 4, 25 + ] + run [ + event-loop screen:address, console:address, 2:address:programming-environment-data + print-character screen:address, 9251/␣/cursor + ] + # trace hidden again + screen-should-contain [ + . run (F4) . + .␣ . + .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━. + . x. + .stash [abc] . + .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━. + . . + ] +] + +scenario sandbox-shows-app-trace-and-result [ + $close-trace # trace too long + assume-screen 40/width, 10/height + # run a stash instruction and some code + 1:address:array:character <- new [stash [abc] +add 2, 2] + assume-console [ + press F4 + ] + 2:address:programming-environment-data <- new-programming-environment screen:address, 1:address:array:character + event-loop screen:address, console:address, 2:address:programming-environment-data + screen-should-contain [ + . run (F4) . + . . + .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━. + . x. + .stash [abc] . + .add 2, 2 . + .4 . + .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━. + . . + ] + # click on the code in the sandbox + assume-console [ + left-click 4, 21 + ] + run [ + event-loop screen:address, console:address, 2:address:programming-environment-data + ] + # trace now printed above result + screen-should-contain [ + . run (F4) . + . . + .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━. + . x. + .stash [abc] . + .add 2, 2 . + .abc . + .4 . + .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━. + . . + ] +] + +container sandbox-data [ + trace:address:array:character + display-trace?:boolean +] + +# replaced in a later layer +recipe! update-sandbox [ + local-scope + sandbox:address:sandbox-data <- next-ingredient + data:address:array:character <- get *sandbox, data:offset + response:address:address:array:character <- get-address *sandbox, response:offset + trace:address:address:array:character <- get-address *sandbox, trace:offset + fake-screen:address:address:screen <- get-address *sandbox, screen:offset + *response, _, *fake-screen, *trace <- run-interactive data +] + +# clicks on sandbox code toggle its display-trace? flag +after <global-touch> [ + # check if it's inside the code of any sandbox + { + sandbox-left-margin:number <- get *current-sandbox, left:offset + click-column:number <- get *t, column:offset + on-sandbox-side?:boolean <- greater-or-equal click-column, sandbox-left-margin + break-unless on-sandbox-side? + first-sandbox:address:sandbox-data <- get *env, sandbox:offset + break-unless first-sandbox + first-sandbox-begins:number <- get *first-sandbox, starting-row-on-screen:offset + click-row:number <- get *t, row:offset + below-sandbox-editor?:boolean <- greater-or-equal click-row, first-sandbox-begins + break-unless below-sandbox-editor? + # identify the sandbox whose code is being clicked on + sandbox:address:sandbox-data <- find-click-in-sandbox-code env, click-row + break-unless sandbox + # toggle its display-trace? property + x:address:boolean <- get-address *sandbox, display-trace?:offset + *x <- not *x + hide-screen screen + screen <- render-sandbox-side screen, env, 1/clear + screen <- update-cursor screen, current-sandbox + # no change in cursor + show-screen screen + loop +next-event:label + } +] + +recipe find-click-in-sandbox-code [ + local-scope + env:address:programming-environment-data <- next-ingredient + click-row:number <- next-ingredient + # assert click-row >= sandbox.starting-row-on-screen + sandbox:address:sandbox-data <- get *env, sandbox:offset + start:number <- get *sandbox, starting-row-on-screen:offset + clicked-on-sandboxes?:boolean <- greater-or-equal click-row, start + assert clicked-on-sandboxes?, [extract-sandbox called on click to sandbox editor] + # while click-row < sandbox.next-sandbox.starting-row-on-screen + { + next-sandbox:address:sandbox-data <- get *sandbox, next-sandbox:offset + break-unless next-sandbox + next-start:number <- get *next-sandbox, starting-row-on-screen:offset + found?:boolean <- lesser-than click-row, next-start + break-if found? + sandbox <- copy next-sandbox + loop + } + # return sandbox if click is in its code region + code-ending-row:number <- get *sandbox, code-ending-row-on-screen:offset + click-above-response?:boolean <- lesser-than click-row, code-ending-row + start:number <- get *sandbox, starting-row-on-screen:offset + click-below-menu?:boolean <- greater-than click-row, start + click-on-sandbox-code?:boolean <- and click-above-response?, click-below-menu? + { + break-if click-on-sandbox-code? + reply 0/no-click-in-sandbox-output + } + reply sandbox +] + +# when rendering a sandbox, dump its trace before response/warning if display-trace? property is set +after <render-sandbox-results> [ + { + display-trace?:boolean <- get *sandbox, display-trace?:offset + break-unless display-trace? + sandbox-trace:address:array:character <- get *sandbox, trace:offset + break-unless sandbox-trace # nothing to print; move on + row, screen <- render-string, screen, sandbox-trace, left, right, 245/grey, row + } + <render-sandbox-trace-done> +] diff --git a/sandbox/010-warnings.mu b/sandbox/010-warnings.mu new file mode 100644 index 00000000..077deafb --- /dev/null +++ b/sandbox/010-warnings.mu @@ -0,0 +1,203 @@ +## handling malformed programs + +container programming-environment-data [ + recipe-warnings:address:array:character +] + +# copy code from recipe editor, persist, load into mu, save any warnings +recipe! update-recipes [ + local-scope + env:address:programming-environment-data <- next-ingredient + screen:address <- next-ingredient + in:address:array:character <- restore [recipes.mu] + recipe-warnings:address:address:array:character <- get-address *env, recipe-warnings:offset + *recipe-warnings <- reload in + # if recipe editor has errors, stop + { + break-unless *recipe-warnings + status:address:array:character <- new [errors found] + update-status screen, status, 1/red + reply 1/errors-found, env/same-as-ingredient:0, screen/same-as-ingredient:1 + } + reply 0/no-errors-found, env/same-as-ingredient:0, screen/same-as-ingredient:1 +] + +before <render-components-end> [ + trace 11, [app], [render status] + recipe-warnings:address:array:character <- get *env, recipe-warnings:offset + { + break-unless recipe-warnings + status:address:array:character <- new [errors found] + update-status screen, status, 1/red + } +] + +container sandbox-data [ + warnings:address:array:character +] + +recipe! update-sandbox [ + local-scope + sandbox:address:sandbox-data <- next-ingredient + data:address:array:character <- get *sandbox, data:offset + response:address:address:array:character <- get-address *sandbox, response:offset + warnings:address:address:array:character <- get-address *sandbox, warnings:offset + trace:address:address:array:character <- get-address *sandbox, trace:offset + fake-screen:address:address:screen <- get-address *sandbox, screen:offset + *response, *warnings, *fake-screen, *trace, completed?:boolean <- run-interactive data + { + break-if *warnings + break-if completed?:boolean + *warnings <- new [took too long! +] + } +] + +# make sure we render any trace +after <render-sandbox-trace-done> [ + { + sandbox-warnings:address:array:character <- get *sandbox, warnings:offset + break-unless sandbox-warnings + *response-starting-row <- copy 0 # no response + row, screen <- render-string screen, sandbox-warnings, left, right, 1/red, row + # don't try to print anything more for this sandbox + jump +render-sandbox-end:label + } +] + +scenario run-instruction-and-print-warnings [ + $close-trace # trace too long + assume-screen 50/width, 15/height + 1:address:array:character <- new [get 1:address:point, 1:offset] + 2:address:programming-environment-data <- new-programming-environment screen:address, 1:address:array:character + assume-console [ + press F4 + ] + run [ + event-loop screen:address, console:address, 2:address:programming-environment-data + ] + screen-should-contain [ + . run (F4) . + . . + .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━. + . x. + .get 1:address:point, 1:offset . + .interactive: first ingredient of 'get' should be ↩. + .a container, but got 1:address:point . + .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━. + . . + ] + screen-should-contain-in-color 1/red, [ + . . + . . + . . + . . + . . + .interactive: first ingredient of 'get' should be . + .a container, but got 1:address:point . + . . + . . + ] +] + +scenario run-instruction-and-print-warnings-only-once [ + $close-trace # trace too long + assume-screen 50/width, 10/height + # editor contains an illegal instruction + 1:address:array:character <- new [get 1234:number, foo:offset] + 2:address:programming-environment-data <- new-programming-environment screen:address, 1:address:array:character + # run the code in the editors multiple times + assume-console [ + press F4 + press F4 + ] + run [ + event-loop screen:address, console:address, 2:address:programming-environment-data + ] + # check that screen prints error message just once + screen-should-contain [ + . run (F4) . + . . + .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━. + . x. + .get 1234:number, foo:offset . + .unknown element foo in container number . + .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━. + . . + ] +] + +scenario sandbox-can-handle-infinite-loop [ + $close-trace # trace too long + assume-screen 50/width, 20/height + # editor contains an infinite loop + 1:address:array:character <- new [{ +loop +}] + 2:address:programming-environment-data <- new-programming-environment screen:address, 1:address:array:character + # run the sandbox + assume-console [ + press F4 + ] + run [ + event-loop screen:address, console:address, 2:address:programming-environment-data + ] + screen-should-contain [ + . run (F4) . + . . + .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━. + . x. + .{ . + .loop . + .} . + .took too long! . + .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━. + . . + ] +] + +scenario sandbox-with-warnings-shows-trace [ + $close-trace # trace too long + assume-screen 50/width, 10/height + # generate a stash and a warning + 1:address:array:character <- new [stash [abc] +foo 4, 0] + 2:address:programming-environment-data <- new-programming-environment screen:address, 1:address:array:character + # run + assume-console [ + press F4 + ] + event-loop screen:address, console:address, 2:address:programming-environment-data + # screen prints error message + screen-should-contain [ + . run (F4) . + . . + .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━. + . x. + .stash [abc] . + .foo 4, 0 . + .interactive: undefined operation in 'foo 4, 0' . + .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━. + . . + ] + # click on the call in the sandbox + assume-console [ + left-click 4, 25 + ] + run [ + event-loop screen:address, console:address, 2:address:programming-environment-data + ] + # screen should expand trace + screen-should-contain [ + . run (F4) . + . . + .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━. + . x. + .stash [abc] . + .foo 4, 0 . + .abc . + .interactive: undefined operation in 'foo 4, 0' . + .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━. + . . + ] +] diff --git a/sandbox/011-editor-undo.mu b/sandbox/011-editor-undo.mu new file mode 100644 index 00000000..adf85130 --- /dev/null +++ b/sandbox/011-editor-undo.mu @@ -0,0 +1,2077 @@ +## 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:number + before-column:number + before-top-of-screen:address:duplex-list:character + after-row:number + after-column:number + after-top-of-screen:address:duplex-list:character + # inserted text is from 'insert-from' until 'insert-until'; list doesn't have to terminate + insert-from:address:duplex-list:character + insert-until:address:duplex-list:character + tag:number # 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:number + before-column:number + before-top-of-screen:address:duplex-list:character + after-row:number + after-column:number + after-top-of-screen:address:duplex-list:character + tag:number # 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:number + before-column:number + before-top-of-screen:address:duplex-list:character + after-row:number + after-column:number + after-top-of-screen:address:duplex-list:character + deleted-text:address:duplex-list:character + delete-from:address:duplex-list:character + delete-until:address:duplex-list:character + tag:number # 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-data [ + undo:address:list:address:operation + redo:address:list:address:operation +] + +# ctrl-z - undo operation +after <handle-special-character> [ + { + undo?:boolean <- equal *c, 26/ctrl-z + break-unless undo? + undo:address:address:list <- get-address *editor, undo:offset + break-unless *undo + op:address:operation <- first *undo + *undo <- rest *undo + redo:address:address:list <- get-address *editor, redo:offset + *redo <- push op, *redo + <handle-undo> + reply screen/same-as-ingredient:0, editor/same-as-ingredient:1, 1/go-render + } +] + +# ctrl-y - redo operation +after <handle-special-character> [ + { + redo?:boolean <- equal *c, 25/ctrl-y + break-unless redo? + redo:address:address:list <- get-address *editor, redo:offset + break-unless *redo + op:address:operation <- first *redo + *redo <- rest *redo + undo:address:address:list <- get-address *editor, undo:offset + *undo <- push op, *undo + <handle-redo> + reply screen/same-as-ingredient:0, editor/same-as-ingredient:1, 1/go-render + } +] + +# undo typing + +scenario editor-can-undo-typing [ + # create an editor and type a character + assume-screen 10/width, 5/height + 1:address:array:character <- new [] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + editor-render screen, 2:address:editor-data + assume-console [ + type [0] + ] + editor-event-loop screen:address, console:address, 2:address:editor-data + # undo + assume-console [ + press ctrl-z + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + # character should be gone + screen-should-contain [ + . . + . . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] + # cursor should be in the right place + assume-console [ + type [1] + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + screen-should-contain [ + . . + .1 . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] +] + +# save operation to undo +after <insert-character-begin> [ + top-before:address:duplex-list <- get *editor, top-of-screen:offset + cursor-before:address:duplex-list <- copy *before-cursor +] +before <insert-character-end> [ + top-after:address:duplex-list <- get *editor, top-of-screen:offset + undo:address:address:list <- get-address *editor, undo:offset + { + # if previous operation was an insert, coalesce this operation with it + break-unless *undo + op:address:operation <- first *undo + typing:address:insert-operation <- maybe-convert *op, typing:variant + break-unless typing + previous-coalesce-tag:number <- get *typing, tag:offset + break-unless previous-coalesce-tag + insert-until:address:address:duplex-list <- get-address *typing, insert-until:offset + *insert-until <- next-duplex *before-cursor + after-row:address:number <- get-address *typing, after-row:offset + *after-row <- copy *cursor-row + after-column:address:number <- get-address *typing, after-column:offset + *after-column <- copy *cursor-column + after-top:address:number <- get-address *typing, after-top-of-screen:offset + *after-top <- get *editor, top-of-screen:offset + break +done-adding-insert-operation:label + } + # if not, create a new operation + insert-from:address:duplex-list <- next-duplex cursor-before + insert-to:address:duplex-list <- next-duplex insert-from + op:address: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 <insert-enter-begin> [ + cursor-row-before:number <- copy *cursor-row + cursor-column-before:number <- copy *cursor-column + top-before:address:duplex-list <- get *editor, top-of-screen:offset + cursor-before:address:duplex-list <- copy *before-cursor +] +before <insert-enter-end> [ + top-after:address:duplex-list <- get *editor, top-of-screen:offset + # never coalesce + insert-from:address:duplex-list <- next-duplex cursor-before + insert-to:address:duplex-list <- next-duplex *before-cursor + op:address: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. +recipe add-operation [ + local-scope + editor:address:editor-data <- next-ingredient + op:address:operation <- next-ingredient + undo:address:address:list:address:operation <- get-address *editor, undo:offset + *undo <- push op *undo + redo:address:address:list:address:operation <- get-address *editor, redo:offset + *redo <- copy 0 + reply editor/same-as-ingredient:0 +] + +after <handle-undo> [ + { + typing:address:insert-operation <- maybe-convert *op, typing:variant + break-unless typing + start:address:duplex-list <- get *typing, insert-from:offset + end:address:duplex-list <- get *typing, insert-until:offset + # assert cursor-row/cursor-column/top-of-screen match after-row/after-column/after-top-of-screen + *before-cursor <- prev-duplex start + remove-duplex-between *before-cursor, end + *cursor-row <- get *typing, before-row:offset + *cursor-column <- get *typing, before-column:offset + top:address:address:duplex-list <- get *editor, top-of-screen:offset + *top <- get *typing, before-top-of-screen:offset + } +] + +scenario editor-can-undo-typing-multiple [ + # create an editor and type multiple characters + assume-screen 10/width, 5/height + 1:address:array:character <- new [] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + editor-render screen, 2:address:editor-data + assume-console [ + type [012] + ] + editor-event-loop screen:address, console:address, 2:address:editor-data + # undo + assume-console [ + press ctrl-z + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + # all characters must be gone + screen-should-contain [ + . . + . . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] +] + +scenario editor-can-undo-typing-multiple-2 [ + # create an editor with some text + assume-screen 10/width, 5/height + 1:address:array:character <- new [a] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + editor-render screen, 2:address:editor-data + # type some characters + assume-console [ + type [012] + ] + editor-event-loop screen:address, console:address, 2:address:editor-data + screen-should-contain [ + . . + .012a . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] + # undo + assume-console [ + press ctrl-z + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + # back to original text + screen-should-contain [ + . . + .a . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] + # cursor should be in the right place + assume-console [ + type [3] + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + screen-should-contain [ + . . + .3a . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] +] + +scenario editor-can-undo-typing-enter [ + # create an editor with some text + assume-screen 10/width, 5/height + 1:address:array:character <- new [ abc] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + editor-render screen, 2:address:editor-data + # new line + assume-console [ + left-click 1, 8 + press enter + ] + editor-event-loop screen:address, console:address, 2:address:editor-data + screen-should-contain [ + . . + . abc . + . . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] + # line is indented + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, cursor-column:offset + memory-should-contain [ + 3 <- 2 + 4 <- 2 + ] + # undo + assume-console [ + press ctrl-z + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, 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:address, console:address, 2:address:editor-data + ] + screen-should-contain [ + . . + . abc1 . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] +] + +# redo typing + +scenario editor-redo-typing [ + # create an editor, type something, undo + assume-screen 10/width, 5/height + 1:address:array:character <- new [a] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + editor-render screen, 2:address:editor-data + assume-console [ + type [012] + press ctrl-z + ] + editor-event-loop screen:address, console:address, 2:address:editor-data + screen-should-contain [ + . . + .a . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] + # redo + assume-console [ + press ctrl-y + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + # 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:address, console:address, 2:address:editor-data + ] + screen-should-contain [ + . . + .0123a . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] +] + +after <handle-redo> [ + { + typing:address:insert-operation <- maybe-convert *op, typing:variant + break-unless typing + insert-from:address:duplex-list <- get *typing, insert-from:offset # ignore insert-to because it's already been spliced away + # assert insert-to matches next-duplex(*before-cursor) + insert-duplex-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 + *cursor-column <- get *typing, after-column:offset + top:address:address:duplex-list <- get *editor, top-of-screen:offset + *top <- get *typing, after-top-of-screen:offset + } +] + +scenario editor-redo-typing-empty [ + # create an editor, type something, undo + assume-screen 10/width, 5/height + 1:address:array:character <- new [] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + editor-render screen, 2:address:editor-data + assume-console [ + type [012] + press ctrl-z + ] + editor-event-loop screen:address, console:address, 2:address:editor-data + screen-should-contain [ + . . + . . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] + # redo + assume-console [ + press ctrl-y + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + # 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:address, console:address, 2:address:editor-data + ] + screen-should-contain [ + . . + .0123 . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] +] + +scenario editor-work-clears-redo-stack [ + # create an editor with some text, do some work, undo + assume-screen 10/width, 5/height + 1:address:array:character <- new [abc +def +ghi] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + editor-render screen, 2:address:editor-data + assume-console [ + type [1] + press ctrl-z + ] + editor-event-loop screen:address, console:address, 2:address:editor-data + # do some more work + assume-console [ + type [0] + ] + editor-event-loop screen:address, console:address, 2:address:editor-data + screen-should-contain [ + . . + .0abc . + .def . + .ghi . + .┈┈┈┈┈┈┈┈┈┈. + ] + # redo + assume-console [ + press ctrl-y + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + # nothing should happen + screen-should-contain [ + . . + .0abc . + .def . + .ghi . + .┈┈┈┈┈┈┈┈┈┈. + ] +] + +scenario editor-can-redo-typing-and-enter-and-tab [ + # create an editor + assume-screen 10/width, 5/height + 1:address:array:character <- new [] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + editor-render screen, 2:address:editor-data + # 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:address, console:address, 2:address:editor-data + screen-should-contain [ + . . + . ab cd . + . efg . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, cursor-column:offset + memory-should-contain [ + 3 <- 2 + 4 <- 7 + ] + # undo + assume-console [ + press ctrl-z + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + # typing in second line deleted, but not indent + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, 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:address, console:address, 2:address:editor-data + ] + # indent and newline deleted + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, 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:address, console:address, 2:address:editor-data + ] + # empty screen + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, cursor-column:offset + memory-should-contain [ + 3 <- 1 + 4 <- 0 + ] + screen-should-contain [ + . . + . . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] + # redo + assume-console [ + press ctrl-y + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + # first line inserted + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, 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:address, console:address, 2:address:editor-data + ] + # newline and indent inserted + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, 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:address, console:address, 2:address:editor-data + ] + # indent and newline deleted + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, 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 [ + # create an editor with some text + assume-screen 10/width, 5/height + 1:address:array:character <- new [abc +def +ghi] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + editor-render screen, 2:address:editor-data + # move the cursor + assume-console [ + left-click 3, 1 + ] + editor-event-loop screen:address, console:address, 2:address:editor-data + # undo + assume-console [ + press ctrl-z + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, cursor-column:offset + ] + # click undone + memory-should-contain [ + 3 <- 1 + 4 <- 0 + ] + # cursor should be in the right place + assume-console [ + type [1] + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + screen-should-contain [ + . . + .1abc . + .def . + .ghi . + .┈┈┈┈┈┈┈┈┈┈. + ] +] + +after <move-cursor-begin> [ + before-cursor-row:number <- get *editor, cursor-row:offset + before-cursor-column:number <- get *editor, cursor-column:offset + before-top-of-screen:address:duplex-list <- get *editor, top-of-screen:offset +] +before <move-cursor-end> [ + after-cursor-row:number <- get *editor, cursor-row:offset + after-cursor-column:number <- get *editor, cursor-column:offset + after-top-of-screen:address:duplex-list <- get *editor, top-of-screen:offset + { + break-unless undo-coalesce-tag + # if previous operation was also a move, and also had the same coalesce + # tag, coalesce with it + undo:address:address:list <- get-address *editor, undo:offset + break-unless *undo + op:address:operation <- first *undo + move:address:move-operation <- maybe-convert *op, move:variant + break-unless move + previous-coalesce-tag:number <- get *move, tag:offset + coalesce?:boolean <- equal undo-coalesce-tag, previous-coalesce-tag + break-unless coalesce? + after-row:address:number <- get-address *move, after-row:offset + *after-row <- copy after-cursor-row + after-column:address:number <- get-address *move, after-column:offset + *after-column <- copy after-cursor-column + after-top:address:number <- get-address *move, after-top-of-screen:offset + *after-top <- get *editor, top-of-screen:offset + break +done-adding-move-operation:label + } + op:address:operation <- new operation:type + *op <- merge 1/move-operation, before-cursor-row, before-cursor-column, before-top-of-screen, after-cursor-row, after-cursor-column, after-top-of-screen, undo-coalesce-tag + editor <- add-operation editor, op + +done-adding-move-operation +] + +after <handle-undo> [ + { + move:address:move-operation <- maybe-convert *op, move:variant + break-unless move + # assert cursor-row/cursor-column/top-of-screen match after-row/after-column/after-top-of-screen + top:address:address:duplex-list <- get-address *editor, top-of-screen:offset + *cursor-row <- get *move, before-row:offset + *cursor-column <- get *move, before-column:offset + *top <- get *move, before-top-of-screen:offset + } +] + +scenario editor-can-undo-scroll [ + # screen has 1 line for menu + 3 lines + assume-screen 5/width, 4/height + # editor contains a wrapped line + 1:address:array:character <- new [a +b +cdefgh] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 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:address, console:address, 2:address:editor-data + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, 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:address, console:address, 2:address:editor-data + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, cursor-column:offset + ] + # cursor moved back + 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:address, console:address, 2:address:editor-data + ] + screen-should-contain [ + . . + .b . + .cde1↩. + .fgh . + ] +] + +scenario editor-can-undo-left-arrow [ + # create an editor with some text + assume-screen 10/width, 5/height + 1:address:array:character <- new [abc +def +ghi] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + editor-render screen, 2:address:editor-data + # move the cursor + assume-console [ + left-click 3, 1 + press left-arrow + ] + editor-event-loop screen:address, console:address, 2:address:editor-data + # undo + assume-console [ + press ctrl-z + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, cursor-column:offset + ] + # cursor moves back + memory-should-contain [ + 3 <- 3 + 4 <- 1 + ] + # cursor should be in the right place + assume-console [ + type [1] + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + screen-should-contain [ + . . + .abc . + .def . + .g1hi . + .┈┈┈┈┈┈┈┈┈┈. + ] +] + +scenario editor-can-undo-up-arrow [ + # create an editor with some text + assume-screen 10/width, 5/height + 1:address:array:character <- new [abc +def +ghi] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + editor-render screen, 2:address:editor-data + # move the cursor + assume-console [ + left-click 3, 1 + press up-arrow + ] + editor-event-loop screen:address, console:address, 2:address:editor-data + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, cursor-column:offset + memory-should-contain [ + 3 <- 2 + 4 <- 1 + ] + # undo + assume-console [ + press ctrl-z + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, cursor-column:offset + ] + # cursor moves back + memory-should-contain [ + 3 <- 3 + 4 <- 1 + ] + # cursor should be in the right place + assume-console [ + type [1] + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + screen-should-contain [ + . . + .abc . + .def . + .g1hi . + .┈┈┈┈┈┈┈┈┈┈. + ] +] + +scenario editor-can-undo-down-arrow [ + # create an editor with some text + assume-screen 10/width, 5/height + 1:address:array:character <- new [abc +def +ghi] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + editor-render screen, 2:address:editor-data + # move the cursor + assume-console [ + left-click 2, 1 + press down-arrow + ] + editor-event-loop screen:address, console:address, 2:address:editor-data + # undo + assume-console [ + press ctrl-z + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, cursor-column:offset + ] + # cursor moves back + memory-should-contain [ + 3 <- 2 + 4 <- 1 + ] + # cursor should be in the right place + assume-console [ + type [1] + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + screen-should-contain [ + . . + .abc . + .d1ef . + .ghi . + .┈┈┈┈┈┈┈┈┈┈. + ] +] + +scenario editor-can-undo-ctrl-f [ + # create an editor with multiple pages of text + assume-screen 10/width, 5/height + 1:address:array:character <- new [a +b +c +d +e +f] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + editor-render screen, 2:address:editor-data + # scroll the page + assume-console [ + press ctrl-f + ] + editor-event-loop screen:address, console:address, 2:address:editor-data + # undo + assume-console [ + press ctrl-z + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, cursor-column:offset + ] + # screen should again show page 1 + screen-should-contain [ + . . + .a . + .b . + .c . + .d . + ] +] + +scenario editor-can-undo-page-down [ + # create an editor with multiple pages of text + assume-screen 10/width, 5/height + 1:address:array:character <- new [a +b +c +d +e +f] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + editor-render screen, 2:address:editor-data + # scroll the page + assume-console [ + press page-down + ] + editor-event-loop screen:address, console:address, 2:address:editor-data + # undo + assume-console [ + press ctrl-z + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, cursor-column:offset + ] + # screen should again show page 1 + screen-should-contain [ + . . + .a . + .b . + .c . + .d . + ] +] + +scenario editor-can-undo-ctrl-b [ + # create an editor with multiple pages of text + assume-screen 10/width, 5/height + 1:address:array:character <- new [a +b +c +d +e +f] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + editor-render screen, 2:address:editor-data + # scroll the page down and up + assume-console [ + press page-down + press ctrl-b + ] + editor-event-loop screen:address, console:address, 2:address:editor-data + # undo + assume-console [ + press ctrl-z + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, cursor-column:offset + ] + # screen should again show page 2 + screen-should-contain [ + . . + .d . + .e . + .f . + .┈┈┈┈┈┈┈┈┈┈. + ] +] + +scenario editor-can-undo-page-up [ + # create an editor with multiple pages of text + assume-screen 10/width, 5/height + 1:address:array:character <- new [a +b +c +d +e +f] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + editor-render screen, 2:address:editor-data + # scroll the page down and up + assume-console [ + press page-down + press page-up + ] + editor-event-loop screen:address, console:address, 2:address:editor-data + # undo + assume-console [ + press ctrl-z + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, cursor-column:offset + ] + # screen should again show page 2 + screen-should-contain [ + . . + .d . + .e . + .f . + .┈┈┈┈┈┈┈┈┈┈. + ] +] + +scenario editor-can-undo-ctrl-a [ + # create an editor with some text + assume-screen 10/width, 5/height + 1:address:array:character <- new [abc +def +ghi] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + editor-render screen, 2:address:editor-data + # move the cursor, then to start of line + assume-console [ + left-click 2, 1 + press ctrl-a + ] + editor-event-loop screen:address, console:address, 2:address:editor-data + # undo + assume-console [ + press ctrl-z + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, cursor-column:offset + ] + # cursor moves back + memory-should-contain [ + 3 <- 2 + 4 <- 1 + ] + # cursor should be in the right place + assume-console [ + type [1] + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + screen-should-contain [ + . . + .abc . + .d1ef . + .ghi . + .┈┈┈┈┈┈┈┈┈┈. + ] +] + +scenario editor-can-undo-home [ + # create an editor with some text + assume-screen 10/width, 5/height + 1:address:array:character <- new [abc +def +ghi] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + editor-render screen, 2:address:editor-data + # move the cursor, then to start of line + assume-console [ + left-click 2, 1 + press home + ] + editor-event-loop screen:address, console:address, 2:address:editor-data + # undo + assume-console [ + press ctrl-z + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, cursor-column:offset + ] + # cursor moves back + memory-should-contain [ + 3 <- 2 + 4 <- 1 + ] + # cursor should be in the right place + assume-console [ + type [1] + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + screen-should-contain [ + . . + .abc . + .d1ef . + .ghi . + .┈┈┈┈┈┈┈┈┈┈. + ] +] + +scenario editor-can-undo-ctrl-e [ + # create an editor with some text + assume-screen 10/width, 5/height + 1:address:array:character <- new [abc +def +ghi] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + editor-render screen, 2:address:editor-data + # move the cursor, then to start of line + assume-console [ + left-click 2, 1 + press ctrl-e + ] + editor-event-loop screen:address, console:address, 2:address:editor-data + # undo + assume-console [ + press ctrl-z + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, cursor-column:offset + ] + # cursor moves back + memory-should-contain [ + 3 <- 2 + 4 <- 1 + ] + # cursor should be in the right place + assume-console [ + type [1] + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + screen-should-contain [ + . . + .abc . + .d1ef . + .ghi . + .┈┈┈┈┈┈┈┈┈┈. + ] +] + +scenario editor-can-undo-end [ + # create an editor with some text + assume-screen 10/width, 5/height + 1:address:array:character <- new [abc +def +ghi] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + editor-render screen, 2:address:editor-data + # move the cursor, then to start of line + assume-console [ + left-click 2, 1 + press end + ] + editor-event-loop screen:address, console:address, 2:address:editor-data + # undo + assume-console [ + press ctrl-z + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, cursor-column:offset + ] + # cursor moves back + memory-should-contain [ + 3 <- 2 + 4 <- 1 + ] + # cursor should be in the right place + assume-console [ + type [1] + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + screen-should-contain [ + . . + .abc . + .d1ef . + .ghi . + .┈┈┈┈┈┈┈┈┈┈. + ] +] + +scenario editor-separates-undo-insert-from-undo-cursor-move [ + # create an editor, type some text, move the cursor, type some more text + assume-screen 10/width, 5/height + 1:address:array:character <- new [] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + editor-render screen, 2:address:editor-data + assume-console [ + type [abc] + left-click 1, 1 + type [d] + ] + editor-event-loop screen:address, console:address, 2:address:editor-data + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, 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:address, console:address, 2:address:editor-data + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, 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:address, console:address, 2:address:editor-data + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, 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:address, console:address, 2:address:editor-data + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, 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:address, console:address, 2:address:editor-data + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, 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:address, console:address, 2:address:editor-data + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, cursor-column:offset + ] + # cursor moves + screen-should-contain [ + . . + .abc . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] + memory-should-contain [ + 3 <- 1 + 4 <- 1 + ] + # redo again + assume-console [ + press ctrl-y + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, cursor-column:offset + ] + # second insert + screen-should-contain [ + . . + .adbc . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] + memory-should-contain [ + 3 <- 1 + 4 <- 2 + ] +] + +scenario editor-can-undo-multiple-arrows-in-the-same-direction [ + # create an editor with some text + assume-screen 10/width, 5/height + 1:address:array:character <- new [abc +def +ghi] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + editor-render screen, 2:address:editor-data + # move the cursor + assume-console [ + left-click 2, 1 + press right-arrow + press right-arrow + press up-arrow + ] + editor-event-loop screen:address, console:address, 2:address:editor-data + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, cursor-column:offset + memory-should-contain [ + 3 <- 1 + 4 <- 3 + ] + # undo + assume-console [ + press ctrl-z + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, cursor-column:offset + ] + # up-arrow is undone + memory-should-contain [ + 3 <- 2 + 4 <- 3 + ] + # undo again + assume-console [ + press ctrl-z + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, cursor-column:offset + ] + # both right-arrows are undone + memory-should-contain [ + 3 <- 2 + 4 <- 1 + ] +] + +# redo cursor movement and scroll + +scenario editor-redo-touch [ + # create an editor with some text, click on a character, undo + assume-screen 10/width, 5/height + 1:address:array:character <- new [abc +def +ghi] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + editor-render screen, 2:address:editor-data + assume-console [ + left-click 3, 1 + press ctrl-z + ] + editor-event-loop screen:address, console:address, 2:address:editor-data + # redo + assume-console [ + press ctrl-y + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, cursor-column:offset + ] + # cursor moves to left-click + memory-should-contain [ + 3 <- 3 + 4 <- 1 + ] + # cursor should be in the right place + assume-console [ + type [1] + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + screen-should-contain [ + . . + .abc . + .def . + .g1hi . + .┈┈┈┈┈┈┈┈┈┈. + ] +] + +after <handle-redo> [ + { + move:address:move-operation <- maybe-convert *op, move:variant + break-unless 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 + *cursor-column <- get *move, after-column:offset + top:address:address:duplex-list <- get *editor, top-of-screen:offset + *top <- get *move, after-top-of-screen:offset + } +] + +# undo backspace + +scenario editor-can-undo-and-redo-backspace [ + # create an editor + assume-screen 10/width, 5/height + 1:address:array:character <- new [] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + editor-render screen, 2:address:editor-data + # insert some text and hit backspace + assume-console [ + type [abc] + press backspace + press backspace + ] + editor-event-loop screen:address, console:address, 2:address:editor-data + screen-should-contain [ + . . + .a . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, cursor-column:offset + memory-should-contain [ + 3 <- 1 + 4 <- 1 + ] + # undo + assume-console [ + press ctrl-z + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, 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:address, console:address, 2:address:editor-data + ] + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, cursor-column:offset + memory-should-contain [ + 3 <- 1 + 4 <- 1 + ] + screen-should-contain [ + . . + .a . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] +] + +# save operation to undo +after <backspace-character-begin> [ + top-before:address:duplex-list <- get *editor, top-of-screen:offset +] +before <backspace-character-end> [ + { + break-unless backspaced-cell # backspace failed; don't add an undo operation + top-after:address:duplex-list <- get *editor, top-of-screen:offset + undo:address:address:list <- get-address *editor, undo:offset + { + # if previous operation was an insert, coalesce this operation with it + break-unless *undo + op:address:operation <- first *undo + deletion:address:delete-operation <- maybe-convert *op, delete:variant + break-unless deletion + previous-coalesce-tag:number <- get *deletion, tag:offset + coalesce?:boolean <- equal previous-coalesce-tag, 1/coalesce-backspace + break-unless coalesce? + delete-from:address:address:duplex-list <- get-address *deletion, delete-from:offset + *delete-from <- copy *before-cursor + backspaced-so-far:address:address:duplex-list <- get-address *deletion, deleted-text:offset + insert-duplex-range backspaced-cell, *backspaced-so-far + *backspaced-so-far <- copy backspaced-cell + after-row:address:number <- get-address *deletion, after-row:offset + *after-row <- copy *cursor-row + after-column:address:number <- get-address *deletion, after-column:offset + *after-column <- copy *cursor-column + after-top:address:number <- get-address *deletion, after-top-of-screen:offset + *after-top <- get *editor, top-of-screen:offset + break +done-adding-backspace-operation:label + } + # if not, create a new operation + op:address:operation <- new operation:type + deleted-until:address:duplex-list <- next-duplex *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 <handle-undo> [ + { + deletion:address:delete-operation <- maybe-convert *op, delete:variant + break-unless deletion + start2:address:address:duplex-list <- get-address *editor, data:offset + anchor:address:duplex-list <- get *deletion, delete-from:offset + break-unless anchor + deleted:address:duplex-list <- get *deletion, deleted-text:offset + old-cursor:address:duplex-list <- last-duplex deleted + insert-duplex-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 + *cursor-column <- get *deletion, before-column:offset + top:address:address:duplex-list <- get *editor, top-of-screen:offset + *top <- get *deletion, before-top-of-screen:offset + } +] + +after <handle-redo> [ + { + deletion:address:delete-operation <- maybe-convert *op, delete:variant + break-unless deletion + start:address:duplex-list <- get *deletion, delete-from:offset + end:address:duplex-list <- get *deletion, delete-until:offset + remove-duplex-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 + *cursor-column <- get *deletion, after-column:offset + top:address:address:duplex-list <- get *editor, top-of-screen:offset + *top <- get *deletion, after-top-of-screen:offset + } +] + +# undo delete + +scenario editor-can-undo-and-redo-delete [ + # create an editor + assume-screen 10/width, 5/height + 1:address:array:character <- new [] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + editor-render screen, 2:address:editor-data + # 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:address, console:address, 2:address:editor-data + screen-should-contain [ + . . + .af . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, cursor-column:offset + memory-should-contain [ + 3 <- 1 + 4 <- 1 + ] + # undo deletes + assume-console [ + press ctrl-z + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, 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:address, console:address, 2:address:editor-data + ] + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, 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:address, console:address, 2:address:editor-data + ] + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, 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:address, console:address, 2:address:editor-data + ] + # first line inserted + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, 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:address, console:address, 2:address:editor-data + ] + # first line inserted + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, 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:address, console:address, 2:address:editor-data + ] + # first line inserted + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, cursor-column:offset + memory-should-contain [ + 3 <- 1 + 4 <- 1 + ] + screen-should-contain [ + . . + .af . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] +] + +after <delete-character-begin> [ + top-before:address:duplex-list <- get *editor, top-of-screen:offset +] +before <delete-character-end> [ + { + break-unless deleted-cell # delete failed; don't add an undo operation + top-after:address:duplex-list <- get *editor, top-of-screen:offset + undo:address:address:list <- get-address *editor, undo:offset + { + # if previous operation was an insert, coalesce this operation with it + break-unless *undo + op:address:operation <- first *undo + deletion:address:delete-operation <- maybe-convert *op, delete:variant + break-unless deletion + previous-coalesce-tag:number <- get *deletion, tag:offset + coalesce?:boolean <- equal previous-coalesce-tag, 2/coalesce-delete + break-unless coalesce? + delete-until:address:address:duplex-list <- get-address *deletion, delete-until:offset + *delete-until <- next-duplex *before-cursor + deleted-so-far:address:address:duplex-list <- get-address *deletion, deleted-text:offset + *deleted-so-far <- append-duplex *deleted-so-far, deleted-cell + after-row:address:number <- get-address *deletion, after-row:offset + *after-row <- copy *cursor-row + after-column:address:number <- get-address *deletion, after-column:offset + *after-column <- copy *cursor-column + after-top:address:number <- get-address *deletion, after-top-of-screen:offset + *after-top <- get *editor, top-of-screen:offset + break +done-adding-delete-operation:label + } + # if not, create a new operation + op:address:operation <- new operation:type + deleted-until:address:duplex-list <- next-duplex *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 [ + # create an editor + assume-screen 10/width, 5/height + 1:address:array:character <- new [abc +def] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + editor-render screen, 2:address:editor-data + # insert some text and hit delete and backspace a few times + assume-console [ + left-click 1, 1 + press ctrl-k + ] + editor-event-loop screen:address, console:address, 2:address:editor-data + screen-should-contain [ + . . + .a . + .def . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, cursor-column:offset + memory-should-contain [ + 3 <- 1 + 4 <- 1 + ] + # undo + assume-console [ + press ctrl-z + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + screen-should-contain [ + . . + .abc . + .def . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, cursor-column:offset + memory-should-contain [ + 3 <- 1 + 4 <- 1 + ] + # redo + assume-console [ + press ctrl-y + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + # first line inserted + screen-should-contain [ + . . + .a . + .def . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, 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:address, console:address, 2:address:editor-data + ] + screen-should-contain [ + . . + .a1 . + .def . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] +] + +after <delete-to-end-of-line-begin> [ + top-before:address:duplex-list <- get *editor, top-of-screen:offset +] +before <delete-to-end-of-line-end> [ + { + break-unless deleted-cells # delete failed; don't add an undo operation + top-after:address:duplex-list <- get *editor, top-of-screen:offset + undo:address:address:list <- get-address *editor, undo:offset + op:address:operation <- new operation:type + deleted-until:address:duplex-list <- next-duplex *before-cursor + *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 [ + # create an editor + assume-screen 10/width, 5/height + 1:address:array:character <- new [abc +def] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + editor-render screen, 2:address:editor-data + # insert some text and hit delete and backspace a few times + assume-console [ + left-click 1, 2 + press ctrl-u + ] + editor-event-loop screen:address, console:address, 2:address:editor-data + screen-should-contain [ + . . + .c . + .def . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, cursor-column:offset + memory-should-contain [ + 3 <- 1 + 4 <- 0 + ] + # undo + assume-console [ + press ctrl-z + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + screen-should-contain [ + . . + .abc . + .def . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, cursor-column:offset + memory-should-contain [ + 3 <- 1 + 4 <- 2 + ] + # redo + assume-console [ + press ctrl-y + ] + run [ + editor-event-loop screen:address, console:address, 2:address:editor-data + ] + # first line inserted + screen-should-contain [ + . . + .c . + .def . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] + 3:number <- get *2:address:editor-data, cursor-row:offset + 4:number <- get *2:address:editor-data, 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:address, console:address, 2:address:editor-data + ] + screen-should-contain [ + . . + .1c . + .def . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] +] + +after <delete-to-start-of-line-begin> [ + top-before:address:duplex-list <- get *editor, top-of-screen:offset +] +before <delete-to-start-of-line-end> [ + { + break-unless deleted-cells # delete failed; don't add an undo operation + top-after:address:duplex-list <- get *editor, top-of-screen:offset + undo:address:address:list <- get-address *editor, undo:offset + op:address:operation <- new operation:type + deleted-until:address:duplex-list <- next-duplex *before-cursor + *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 [ + # create an editor + assume-screen 10/width, 5/height + 1:address:array:character <- new [] + 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right + editor-render screen, 2:address:editor-data + # 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:address, console:address, 2:address:editor-data + screen-should-contain [ + . . + .abc . + .┈┈┈┈┈┈┈┈┈┈. + . . + ] +] + +# todo: +# operations for recipe side and each sandbox-data +# undo delete sandbox as a separate primitive on the status bar diff --git a/sandbox/Readme.md b/sandbox/Readme.md new file mode 100644 index 00000000..1e37dc3f --- /dev/null +++ b/sandbox/Readme.md @@ -0,0 +1,15 @@ +Variant of edit that runs just the sandbox. + +Suitable for running inside split panes atop tmux so you can run vim or some +other editor with mu. + +To set this up: + a) copy the lines in tmux.conf into $HOME/.tmux.conf + b) copy the file `mu_run` somewhere in your $PATH + +Now when you start tmux, split it into two vertical panes, run `mu sandbox` on +the right pane and your editor on the left. You should be able to hit F4 in +either side to run the sandbox. + +Known issues: you have to explicitly save inside your editor before hitting +F4, unlike with 'mu edit'. diff --git a/sandbox/mu_run b/sandbox/mu_run new file mode 100755 index 00000000..8d606fcd --- /dev/null +++ b/sandbox/mu_run @@ -0,0 +1,15 @@ +#!/usr/bin/zsh + +export ALREADY_FOCUSED=0 +tmux list-panes |grep "^1.*active" -q && export ALREADY_FOCUSED=1 +if [[ $ALREADY_FOCUSED -eq 0 ]] +then + tmux select-pane -t 1 +fi + +tmux send-keys 'F4' + +if [[ $ALREADY_FOCUSED -eq 0 ]] +then + tmux last-pane +fi diff --git a/sandbox/tmux.conf b/sandbox/tmux.conf new file mode 100644 index 00000000..7c716c59 --- /dev/null +++ b/sandbox/tmux.conf @@ -0,0 +1,2 @@ +# hotkey for running mu over tmux (assumes exactly two panes, 'mu sandbox' running on the right/second window) +bind-key -n F4 run mu_run |