about summary refs log tree commit diff stats
path: root/archive/1.vm/edit/005-sandbox.mu
diff options
context:
space:
mode:
Diffstat (limited to 'archive/1.vm/edit/005-sandbox.mu')
-rw-r--r--archive/1.vm/edit/005-sandbox.mu1193
1 files changed, 1193 insertions, 0 deletions
diff --git a/archive/1.vm/edit/005-sandbox.mu b/archive/1.vm/edit/005-sandbox.mu
new file mode 100644
index 00000000..96ec804d
--- /dev/null
+++ b/archive/1.vm/edit/005-sandbox.mu
@@ -0,0 +1,1193 @@
+## 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 maybe a
+# few other things (later layers).
+#
+# This layer draws the menubar buttons in non-editable sandboxes but they
+# don't do anything yet. Later layers implement each button.
+
+def! main [
+  local-scope
+  open-console
+  clear-screen null/screen  # non-scrolling app
+  env:&:environment <- new-programming-environment null/filesystem, null/screen
+  env <- restore-sandboxes env, null/filesystem
+  render-all null/screen, env, render
+  event-loop null/screen, null/console, env, null/filesystem
+]
+
+container environment [
+  sandbox:&:sandbox  # list of sandboxes, from top to bottom. TODO: switch to &:list:sandbox
+  render-from:num
+  number-of-sandboxes:num
+]
+
+after <programming-environment-initialization> [
+  *result <- put *result, render-from:offset, -1
+]
+
+container sandbox [
+  data:text
+  response:text
+  # coordinates to track clicks
+  # constraint: will be 0 for sandboxes at positions before env.render-from
+  starting-row-on-screen:num
+  code-ending-row-on-screen:num  # past end of code
+  screen:&:screen  # prints in the sandbox go here
+  next-sandbox:&:sandbox
+]
+
+scenario run-and-show-results [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 100/width, 15/height
+  # recipe editor is empty
+  assume-resources [
+  ]
+  # sandbox editor contains an instruction without storing outputs
+  env:&:environment <- new-programming-environment resources, screen, [divide-with-remainder 11, 3]
+  render-all screen, env, render
+  # run the code in the editors
+  assume-console [
+    press F4
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # check that screen prints the results
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊                                                 .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊─────────────────────────────────────────────────.
+    .                                                  ┊0   edit       copy       to recipe    delete    .
+    .                                                  ┊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, [
+    .                                                                                                    .
+    .                                                  ┊                                                 .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊─────────────────────────────────────────────────.
+    .                                                  ┊                                                 .
+    .                                                  ┊                                                 .
+    .                                                  ┊3                                                .
+    .                                                  ┊2                                                .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .                                                  ┊                                                 .
+  ]
+  # sandbox menu in reverse video
+  screen-should-contain-in-color 232/black, [
+    .                                                                                                    .
+    .                                                                                                    .
+    .                                                                                                    .
+    .                                                   0   edit       copy       to recipe    delete    .
+  ]
+  # run another command
+  assume-console [
+    left-click 1, 80
+    type [add 2, 2]
+    press F4
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # check that screen prints both sandboxes
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊                                                 .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊─────────────────────────────────────────────────.
+    .                                                  ┊0   edit       copy       to recipe    delete    .
+    .                                                  ┊add 2, 2                                         .
+    .                                                  ┊4                                                .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .                                                  ┊1   edit       copy       to recipe    delete    .
+    .                                                  ┊divide-with-remainder 11, 3                      .
+    .                                                  ┊3                                                .
+    .                                                  ┊2                                                .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .                                                  ┊                                                 .
+  ]
+]
+
+after <global-keypress> [
+  # F4? load all code and run all sandboxes.
+  {
+    do-run?:bool <- equal k, 65532/F4
+    break-unless do-run?
+    screen <- update-status screen, [running...       ], 245/grey
+    <begin-run-sandboxes-on-F4>
+    error?:bool <- run-sandboxes env, resources, screen
+    # we could just render-all, but we do some work to minimize the number of prints to screen
+    <end-run-sandboxes-on-F4>
+    screen <- render-sandbox-side screen, env, render
+    {
+      break-if error?
+      screen <- update-status screen, [                 ], 245/grey
+    }
+    screen <- update-cursor screen, recipes, current-sandbox, sandbox-in-focus?, env
+    loop +next-event
+  }
+]
+
+def run-sandboxes env:&:environment, resources:&:resources, screen:&:screen -> errors-found?:bool, env:&:environment, resources:&:resources, screen:&:screen [
+  local-scope
+  load-inputs
+  errors-found?:bool <- update-recipes env, resources, screen
+  jump-if errors-found?, +return
+  # check contents of right editor (sandbox)
+  <begin-run-sandboxes>
+  current-sandbox:&:editor <- get *env, current-sandbox:offset
+  {
+    sandbox-contents:text <- editor-contents current-sandbox
+    break-unless sandbox-contents
+    # if contents exist, first save them
+    # run them and turn them into a new sandbox
+    new-sandbox:&:sandbox <- new sandbox:type
+    *new-sandbox <- put *new-sandbox, data:offset, sandbox-contents
+    # push to head of sandbox list
+    dest:&:sandbox <- get *env, sandbox:offset
+    *new-sandbox <- put *new-sandbox, next-sandbox:offset, dest
+    *env <- put *env, sandbox:offset, new-sandbox
+    # update sandbox count
+    sandbox-count:num <- get *env, number-of-sandboxes:offset
+    sandbox-count <- add sandbox-count, 1
+    *env <- put *env, number-of-sandboxes:offset, sandbox-count
+    # save all sandboxes
+    # needs to be before running them, in case we die when running
+    save-sandboxes env, resources
+    # clear sandbox editor
+    init:&:duplex-list:char <- push 167/§, null
+    *current-sandbox <- put *current-sandbox, data:offset, init
+    *current-sandbox <- put *current-sandbox, top-of-screen:offset, init
+  }
+  # run all sandboxes
+  curr:&:sandbox <- get *env, sandbox:offset
+  idx:num <- copy 0
+  {
+    break-unless curr
+    curr <- update-sandbox curr, env, idx
+    curr <- get *curr, next-sandbox:offset
+    idx <- add idx, 1
+    loop
+  }
+  <end-run-sandboxes>
+  +return
+  {
+    break-if resources  # ignore this in tests
+    $system [./snapshot_lesson]
+  }
+]
+
+# load code from disk
+# replaced in a later layer (whereupon errors-found? will actually be set)
+def update-recipes env:&:environment, resources:&:resources, screen:&:screen -> errors-found?:bool, env:&:environment, resources:&:resources, screen:&:screen [
+  local-scope
+  load-inputs
+  recipes:&:editor <- get *env, recipes:offset
+  in:text <- editor-contents recipes
+  resources <- dump resources, [lesson/recipes.mu], in
+  reload in
+  errors-found? <- copy false
+]
+
+# replaced in a later layer
+def update-sandbox sandbox:&:sandbox, env:&:environment, idx:num -> sandbox:&:sandbox, env:&:environment [
+  local-scope
+  load-inputs
+  data:text <- get *sandbox, data:offset
+  response:text, _, fake-screen:&:screen <- run-sandboxed data
+  *sandbox <- put *sandbox, response:offset, response
+  *sandbox <- put *sandbox, screen:offset, fake-screen
+]
+
+def update-status screen:&:screen, msg:text, color:num -> screen:&:screen [
+  local-scope
+  load-inputs
+  screen <- move-cursor screen, 0, 2
+  screen <- print screen, msg, color, 238/grey/background
+]
+
+def save-sandboxes env:&:environment, resources:&:resources -> resources:&:resources [
+  local-scope
+  load-inputs
+  trace 11, [app], [save sandboxes]
+  current-sandbox:&:editor <- 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:&:sandbox <- get *env, sandbox:offset
+  idx:num <- copy 0
+  {
+    break-unless curr
+    resources <- save-sandbox resources, curr, idx
+    idx <- add idx, 1
+    curr <- get *curr, next-sandbox:offset
+    loop
+  }
+]
+
+def save-sandbox resources:&:resources, sandbox:&:sandbox, sandbox-index:num -> resources:&:resources [
+  local-scope
+  load-inputs
+  data:text <- get *sandbox, data:offset
+  filename:text <- append [lesson/], sandbox-index
+  resources <- dump resources, filename, data
+  <end-save-sandbox>
+]
+
+def! render-sandbox-side screen:&:screen, env:&:environment, render-editor:render-recipe -> screen:&:screen, env:&:environment [
+  local-scope
+  load-inputs
+  trace 11, [app], [render sandbox side]
+  old-top-idx:num <- save-top-idx screen
+  current-sandbox:&:editor <- get *env, current-sandbox:offset
+  row:num, column:num <- copy 1, 0
+  left:num <- get *current-sandbox, left:offset
+  right:num <- get *current-sandbox, right:offset
+  # render sandbox editor
+  render-from:num <- get *env, render-from:offset
+  {
+    render-current-sandbox?:bool <- equal render-from, -1
+    break-unless render-current-sandbox?
+    row, column, screen, current-sandbox <- call render-editor, screen, current-sandbox
+  }
+  # render sandboxes
+  draw-horizontal screen, row, left, right
+  sandbox:&:sandbox <- get *env, sandbox:offset
+  row, screen <- render-sandboxes screen, sandbox, left, right, row, render-from
+  clear-rest-of-screen screen, row, left, right
+  #
+  assert-no-scroll screen, old-top-idx
+]
+
+def render-sandboxes screen:&:screen, sandbox:&:sandbox, left:num, right:num, row:num, render-from:num, idx:num -> row:num, screen:&:screen, sandbox:&:sandbox [
+  local-scope
+  load-inputs
+  return-unless sandbox
+  screen-height:num <- screen-height screen
+  hidden?:bool <- lesser-than idx, render-from
+  {
+    break-if hidden?
+    # render sandbox menu
+    row <- add row, 1
+    at-bottom?:bool <- greater-or-equal row, screen-height
+    return-if at-bottom?
+    screen <- move-cursor screen, row, left
+    screen <- render-sandbox-menu screen, idx, left, right
+    # save menu row so we can detect clicks to it later
+    *sandbox <- put *sandbox, starting-row-on-screen:offset, row
+    # render sandbox contents
+    row <- add row, 1
+    screen <- move-cursor screen, row, left
+    sandbox-data:text <- get *sandbox, data:offset
+    row, screen <- render-code screen, sandbox-data, left, right, row
+    *sandbox <- put *sandbox, code-ending-row-on-screen:offset, row
+    # render sandbox warnings, screen or response, in that order
+    sandbox-response:text <- get *sandbox, response:offset
+    <render-sandbox-results>
+    {
+      sandbox-screen:&:screen <- get *sandbox, screen:offset
+      empty-screen?:bool <- fake-screen-is-empty? sandbox-screen
+      break-if empty-screen?
+      row, screen <- render-screen screen, sandbox-screen, left, right, row
+    }
+    {
+      break-unless empty-screen?
+      <render-sandbox-response>
+      row, screen <- render-text screen, sandbox-response, left, right, 245/grey, row
+    }
+    +render-sandbox-end
+    at-bottom?:bool <- greater-or-equal row, screen-height
+    return-if at-bottom?
+    # draw solid line after sandbox
+    draw-horizontal screen, row, left, right
+  }
+  # if hidden, reset row attributes
+  {
+    break-unless hidden?
+    *sandbox <- put *sandbox, starting-row-on-screen:offset, 0
+    *sandbox <- put *sandbox, code-ending-row-on-screen:offset, 0
+    <end-render-sandbox-reset-hidden>
+  }
+  # draw next sandbox
+  next-sandbox:&:sandbox <- get *sandbox, next-sandbox:offset
+  next-idx:num <- add idx, 1
+  row, screen <- render-sandboxes screen, next-sandbox, left, right, row, render-from, next-idx
+]
+
+def render-sandbox-menu screen:&:screen, sandbox-index:num, left:num, right:num -> screen:&:screen [
+  local-scope
+  load-inputs
+  move-cursor-to-column screen, left
+  edit-button-left:num, edit-button-right:num, copy-button-left:num, copy-button-right:num, recipe-button-left:num, recipe-button-right:num, delete-button-left:num <- sandbox-menu-columns left, right
+  print screen, sandbox-index, 232/dark-grey, 245/grey
+  start-buttons:num <- subtract edit-button-left, 1
+  clear-line-until screen, start-buttons, 245/grey
+  print screen, [edit], 232/black, 25/background-blue
+  clear-line-until screen, edit-button-right, 25/background-blue
+  print screen, [copy], 232/black, 58/background-green
+  clear-line-until screen, copy-button-right, 58/background-green
+  print screen, [to recipe], 232/black, 94/background-orange
+  clear-line-until screen, recipe-button-right, 94/background-orange
+  print screen, [delete], 232/black, 52/background-red
+  clear-line-until screen, right, 52/background-red
+]
+
+scenario skip-rendering-sandbox-menu-past-bottom-row [
+  trace-until 100/app  # trace too long
+  assume-screen 100/width, 6/height
+  # recipe editor is empty
+  assume-resources [
+    [lesson/0] <- [|add 2, 2|]
+    [lesson/1] <- [|add 1, 1|]
+  ]
+  # create two sandboxes such that the top one just barely fills the screen
+  env:&:environment <- new-programming-environment resources, screen, []
+  env <- restore-sandboxes env, resources
+  run [
+    render-all screen, env, render
+  ]
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊                                                 .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊─────────────────────────────────────────────────.
+    .                                                  ┊0   edit       copy       to recipe    delete    .
+    .                                                  ┊add 2, 2                                         .
+    .                                                  ┊─────────────────────────────────────────────────.
+  ]
+]
+
+# divide up the menu bar for a sandbox into 3 segments, for edit/copy/delete buttons
+# delete-button-right == right
+# all left/right pairs are inclusive
+def sandbox-menu-columns left:num, right:num -> edit-button-left:num, edit-button-right:num, copy-button-left:num, copy-button-right:num, recipe-button-left:num, recipe-button-right:num, delete-button-left:num [
+  local-scope
+  load-inputs
+  start-buttons:num <- add left, 4/space-for-sandbox-index
+  buttons-space:num <- subtract right, start-buttons
+  button-width:num <- divide-with-remainder buttons-space, 4  # integer division
+  buttons-wide-enough?:bool <- greater-or-equal button-width, 10
+  assert buttons-wide-enough?, [sandbox must be at least 40 or so characters wide]
+  edit-button-left:num <- copy start-buttons
+  copy-button-left:num <- add start-buttons, button-width
+  edit-button-right:num <- subtract copy-button-left, 1
+  recipe-button-left:num <- add copy-button-left, button-width
+  copy-button-right:num <- subtract recipe-button-left, 1
+  delete-button-left:num <- subtract right, button-width, -2  # because 'to recipe' is wider than 'delete'
+  recipe-button-right:num <- subtract delete-button-left, 1
+]
+
+# print a text 's' to 'editor' in 'color' starting at 'row'
+# clear rest of last line, move cursor to next line
+# like 'render-code' but without syntax-based colorization
+def render-text screen:&:screen, s:text, left:num, right:num, color:num, row:num -> row:num, screen:&:screen [
+  local-scope
+  load-inputs
+  return-unless s
+  column:num <- copy left
+  screen <- move-cursor screen, row, column
+  screen-height:num <- screen-height screen
+  i:num <- copy 0
+  len:num <- length *s
+  {
+    +next-character
+    done?:bool <- greater-or-equal i, len
+    break-if done?
+    done? <- greater-or-equal row, screen-height
+    break-if done?
+    c:char <- index *s, i
+    {
+      # newline? move to left rather than 0
+      newline?:bool <- equal c, 10/newline
+      break-unless newline?
+      # clear rest of line in this window
+      {
+        done?:bool <- greater-than column, right
+        break-if done?
+        space:char <- copy 32/space
+        print screen, space
+        column <- add column, 1
+        loop
+      }
+      row <- add row, 1
+      column <- copy left
+      screen <- move-cursor screen, row, column
+      i <- add i, 1
+      loop +next-character
+    }
+    {
+      # at right? wrap.
+      at-right?:bool <- equal column, right
+      break-unless at-right?
+      # print wrap icon
+      wrap-icon:char <- copy 8617/loop-back-to-left
+      print screen, wrap-icon, 245/grey
+      column <- copy left
+      row <- add row, 1
+      screen <- move-cursor screen, row, column
+      # don't increment i
+      loop +next-character
+    }
+    i <- add i, 1
+    print screen, c, color
+    column <- add column, 1
+    loop
+  }
+  was-at-left?:bool <- equal column, left
+  clear-line-until screen, right
+  {
+    break-if was-at-left?
+    row <- add row, 1
+  }
+  move-cursor screen, row, left
+]
+
+scenario render-text-wraps-barely-long-lines [
+  local-scope
+  assume-screen 5/width, 5/height
+  run [
+    render-text screen, [abcde], 0/left, 4/right, 7/white, 1/row
+  ]
+  screen-should-contain [
+    .     .
+    .abcd↩.
+    .e    .
+    .     .
+  ]
+]
+
+# assumes programming environment has no sandboxes; restores them from previous session
+def restore-sandboxes env:&:environment, resources:&:resources -> env:&:environment [
+  local-scope
+  load-inputs
+  # read all scenarios, pushing them to end of a list of scenarios
+  idx:num <- copy 0
+  curr:&:sandbox <- copy null
+  prev:&:sandbox <- copy null
+  {
+    filename:text <- append [lesson/], idx
+    contents:text <- slurp resources, filename
+    break-unless contents  # stop at first error; assuming file didn't exist
+                           # todo: handle empty sandbox
+    # create new sandbox for file
+    curr <- new sandbox:type
+    *curr <- put *curr, data:offset, contents
+    <end-restore-sandbox>
+    {
+      break-if idx
+      *env <- put *env, sandbox:offset, curr
+    }
+    {
+      break-unless idx
+      *prev <- put *prev, next-sandbox:offset, curr
+    }
+    idx <- add idx, 1
+    prev <- copy curr
+    loop
+  }
+  # update sandbox count
+  *env <- put *env, number-of-sandboxes:offset, idx
+]
+
+# print the fake sandbox screen to 'screen' with appropriate delimiters
+# leave cursor at start of next line
+def render-screen screen:&:screen, sandbox-screen:&:screen, left:num, right:num, row:num -> row:num, screen:&:screen [
+  local-scope
+  load-inputs
+  return-unless sandbox-screen
+  # print 'screen:'
+  row <- render-text screen, [screen:], left, right, 245/grey, row
+  screen <- move-cursor screen, row, left
+  # start printing sandbox-screen
+  column:num <- copy left
+  s-width:num <- screen-width sandbox-screen
+  s-height:num <- screen-height sandbox-screen
+  buf:&:@:screen-cell <- get *sandbox-screen, data:offset
+  stop-printing:num <- add left, s-width, 3
+  max-column:num <- min stop-printing, right
+  i:num <- copy 0
+  len:num <- length *buf
+  screen-height:num <- screen-height screen
+  {
+    done?:bool <- 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 '.'
+    space:char <- copy 32/space
+    print screen, space, 245/grey
+    print screen, space, 245/grey
+    full-stop:char <- copy 46/period
+    print screen, full-stop, 245/grey
+    column <- add left, 3
+    {
+      # print row
+      row-done?:bool <- greater-or-equal column, max-column
+      break-if row-done?
+      curr:screen-cell <- index *buf, i
+      c:char <- get curr, contents:offset
+      color:num <- get curr, color:offset
+      {
+        # damp whites down to grey
+        white?:bool <- equal color, 7/white
+        break-unless white?
+        color <- copy 245/grey
+      }
+      print screen, c, color
+      column <- add column, 1
+      i <- add i, 1
+      loop
+    }
+    # print final '.'
+    print screen, full-stop, 245/grey
+    column <- add column, 1
+    {
+      # clear rest of current line
+      line-done?:bool <- greater-than column, right
+      break-if line-done?
+      print screen, space
+      column <- add column, 1
+      loop
+    }
+    row <- add row, 1
+    loop
+  }
+]
+
+scenario run-updates-results [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 100/width, 12/height
+  # define a recipe (no indent for the 'add' line below so column numbers are more obvious)
+  assume-resources [
+    [lesson/recipes.mu] <- [
+      ||
+      |recipe foo [|
+      |  local-scope|
+      |  z:num <- add 2, 2|
+      |  reply z|
+      |]|
+    ]
+  ]
+  # sandbox editor contains an instruction without storing outputs
+  env:&:environment <- new-programming-environment resources, screen, [foo]  # contents of sandbox editor
+  render-all screen, env, render
+  $clear-trace
+  # run the code in the editors
+  assume-console [
+    press F4
+  ]
+  event-loop screen, console, env, resources
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊                                                 .
+    .recipe foo [                                      ┊─────────────────────────────────────────────────.
+    .  local-scope                                     ┊0   edit       copy       to recipe    delete    .
+    .  z:num <- add 2, 2                               ┊foo                                              .
+    .  reply z                                         ┊4                                                .
+    .]                                                 ┊─────────────────────────────────────────────────.
+    .                                                  ┊                                                 .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊                                                 .
+    .                                                  ┊                                                 .
+  ]
+  # the new sandbox should be saved to disk
+  trace-should-contain [
+    app: save sandboxes
+  ]
+  # no need to update editor
+  trace-should-not-contain [
+    app: render recipes
+  ]
+  # make a change (incrementing one of the args to 'add'), then rerun
+  $clear-trace
+  assume-console [
+    left-click 4, 28  # one past the value of the second arg
+    press backspace
+    type [3]
+    press F4
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # check that screen updates the result on the right
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊                                                 .
+    .recipe foo [                                      ┊─────────────────────────────────────────────────.
+    .  local-scope                                     ┊0   edit       copy       to recipe    delete    .
+    .  z:num <- add 2, 3                               ┊foo                                              .
+    .  reply z                                         ┊5                                                .
+    .]                                                 ┊─────────────────────────────────────────────────.
+    .                                                  ┊                                                 .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊                                                 .
+    .                                                  ┊                                                 .
+  ]
+  # no need to save sandboxes all over again
+  trace-should-not-contain [
+    app: save sandboxes
+  ]
+]
+
+scenario run-instruction-manages-screen-per-sandbox [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 100/width, 20/height
+  # empty recipes
+  assume-resources [
+  ]
+  # sandbox editor contains an instruction
+  env:&:environment <- new-programming-environment resources, screen, [print screen, 4]  # contents of sandbox editor
+  render-all screen, env, render
+  # run the code in the editor
+  assume-console [
+    press F4
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # check that it prints a little toy screen
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊                                                 .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊─────────────────────────────────────────────────.
+    .                                                  ┊0   edit       copy       to recipe    delete    .
+    .                                                  ┊print screen, 4                                  .
+    .                                                  ┊screen:                                          .
+    .                                                  ┊  .4                             .               .
+    .                                                  ┊  .                              .               .
+    .                                                  ┊  .                              .               .
+    .                                                  ┊  .                              .               .
+    .                                                  ┊  .                              .               .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .                                                  ┊                                                 .
+  ]
+]
+
+def editor-contents editor:&:editor -> result:text [
+  local-scope
+  load-inputs
+  buf:&:buffer:char <- new-buffer 80
+  curr:&:duplex-list:char <- get *editor, data:offset
+  # skip § sentinel
+  assert curr, [editor without data is illegal; must have at least a sentinel]
+  curr <- next curr
+  return-unless curr, null
+  {
+    break-unless curr
+    c:char <- get *curr, value:offset
+    buf <- append buf, c
+    curr <- next curr
+    loop
+  }
+  result <- buffer-to-array buf
+]
+
+scenario editor-provides-edited-contents [
+  local-scope
+  assume-screen 10/width, 5/height
+  e:&:editor <- new-editor [abc], 0/left, 10/right
+  assume-console [
+    left-click 1, 2
+    type [def]
+  ]
+  run [
+    editor-event-loop screen, console, e
+    s:text <- editor-contents e
+    1:@:char/raw <- copy *s
+  ]
+  memory-should-contain [
+    1:array:character <- [abdefc]
+  ]
+]
+
+# keep the bottom of recipes from scrolling off the screen
+
+scenario scrolling-down-past-bottom-of-recipe-editor [
+  local-scope
+  trace-until 100/app
+  assume-screen 100/width, 10/height
+  assume-resources [
+  ]
+  env:&:environment <- new-programming-environment resources, screen, []
+  render-all screen, env, render
+  assume-console [
+    press enter
+    press down-arrow
+  ]
+  event-loop screen, console, env, resources
+  # no scroll
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊                                                 .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊                                                 .
+    .                                                  ┊                                                 .
+  ]
+]
+
+scenario cursor-down-in-recipe-editor [
+  local-scope
+  trace-until 100/app
+  assume-screen 100/width, 10/height
+  assume-resources [
+  ]
+  env:&:environment <- new-programming-environment resources, screen, []
+  render-all screen, env, render
+  assume-console [
+    press enter
+    press up-arrow
+    press down-arrow  # while cursor isn't at bottom
+  ]
+  event-loop screen, console, env, resources
+  cursor:char <- copy 9251/␣
+  print screen, cursor
+  # cursor moves back to bottom
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊                                                 .
+    .␣                                                 ┊─────────────────────────────────────────────────.
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊                                                 .
+    .                                                  ┊                                                 .
+  ]
+]
+
+scenario scrolling-down-past-bottom-of-recipe-editor-2 [
+  local-scope
+  trace-until 100/app
+  assume-screen 100/width, 10/height
+  assume-resources [
+  ]
+  env:&:environment <- new-programming-environment resources, screen, []
+  render-all screen, env, render
+  assume-console [
+    # add a line
+    press enter
+    # cursor back to top line
+    press up-arrow
+    # try to scroll
+    press page-down  # or ctrl-f
+  ]
+  event-loop screen, console, env, resources
+  # no scroll, and cursor remains at top line
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊                                                 .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊                                                 .
+    .                                                  ┊                                                 .
+  ]
+]
+
+scenario scrolling-down-past-bottom-of-recipe-editor-3 [
+  local-scope
+  trace-until 100/app
+  assume-screen 100/width, 10/height
+  assume-resources [
+  ]
+  env:&:environment <- new-programming-environment resources, screen, [ab
+cd]
+  render-all screen, env, render
+  assume-console [
+    # add a line
+    press enter
+    # switch to sandbox
+    press ctrl-n
+    # move cursor
+    press down-arrow
+  ]
+  event-loop screen, console, env, resources
+  cursor:char <- copy 9251/␣
+  print screen, cursor
+  # no scroll on recipe side, cursor moves on sandbox side
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊ab                                               .
+    .                                                  ┊␣d                                               .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊─────────────────────────────────────────────────.
+    .                                                  ┊                                                 .
+  ]
+]
+
+# scrolling through sandboxes
+
+scenario scrolling-down-past-bottom-of-sandbox-editor [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 100/width, 10/height
+  # initialize
+  assume-resources [
+  ]
+  env:&:environment <- new-programming-environment resources, screen, [add 2, 2]
+  render-all screen, env, render
+  assume-console [
+    # create a sandbox
+    press F4
+  ]
+  event-loop screen, console, env, resources
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊                                                 .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊─────────────────────────────────────────────────.
+    .                                                  ┊0   edit       copy       to recipe    delete    .
+    .                                                  ┊add 2, 2                                         .
+  ]
+  # switch to sandbox window and hit 'page-down'
+  assume-console [
+    press ctrl-n
+    press page-down
+  ]
+  run [
+    event-loop screen, console, env, resources
+    cursor:char <- copy 9251/␣
+    print screen, cursor
+  ]
+  # sandbox editor hidden; first sandbox displayed
+  # cursor moves to first sandbox
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊␣   edit       copy       to recipe    delete    .
+    .                                                  ┊add 2, 2                                         .
+    .                                                  ┊4                                                .
+  ]
+  # hit 'page-up'
+  assume-console [
+    press page-up
+  ]
+  run [
+    event-loop screen, console, env, resources
+    cursor:char <- copy 9251/␣
+    print screen, cursor
+  ]
+  # sandbox editor displays again, cursor is in editor
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊␣                                                .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊─────────────────────────────────────────────────.
+    .                                                  ┊0   edit       copy       to recipe    delete    .
+    .                                                  ┊add 2, 2                                         .
+  ]
+]
+
+# page-down on sandbox side updates render-from to scroll sandboxes
+after <global-keypress> [
+  {
+    break-unless sandbox-in-focus?
+    page-down?:bool <- equal k, 65518/page-down
+    break-unless page-down?
+    sandbox:&:sandbox <- get *env, sandbox:offset
+    break-unless sandbox
+    # slide down if possible
+    {
+      render-from:num <- get *env, render-from:offset
+      number-of-sandboxes:num <- get *env, number-of-sandboxes:offset
+      max:num <- subtract number-of-sandboxes, 1
+      at-end?:bool <- greater-or-equal render-from, max
+      loop-if at-end?, +next-event  # render nothing
+      render-from <- add render-from, 1
+      *env <- put *env, render-from:offset, render-from
+    }
+    screen <- render-sandbox-side screen, env, render
+    screen <- update-cursor screen, recipes, current-sandbox, sandbox-in-focus?, env
+    loop +next-event
+  }
+]
+
+# update-cursor takes render-from into account
+after <update-cursor-special-cases> [
+  {
+    break-unless sandbox-in-focus?
+    render-from:num <- get *env, render-from:offset
+    scrolling?:bool <- greater-or-equal render-from, 0
+    break-unless scrolling?
+    cursor-column:num <- get *current-sandbox, left:offset
+    screen <- move-cursor screen, 2/row, cursor-column  # highlighted sandbox will always start at row 2
+    return
+  }
+]
+
+# 'page-up' on sandbox side is like 'page-down': updates render-from when necessary
+after <global-keypress> [
+  {
+    break-unless sandbox-in-focus?
+    page-up?:bool <- equal k, 65519/page-up
+    break-unless page-up?
+    render-from:num <- get *env, render-from:offset
+    at-beginning?:bool <- equal render-from, -1
+    break-if at-beginning?
+    render-from <- subtract render-from, 1
+    *env <- put *env, render-from:offset, render-from
+    screen <- render-sandbox-side screen, env, render
+    screen <- update-cursor screen, recipes, current-sandbox, sandbox-in-focus?, env
+    loop +next-event
+  }
+]
+
+# sandbox belonging to 'env' whose next-sandbox is 'in'
+# return null if there's no such sandbox, either because 'in' doesn't exist in 'env', or because it's the first sandbox
+def previous-sandbox env:&:environment, in:&:sandbox -> out:&:sandbox [
+  local-scope
+  load-inputs
+  curr:&:sandbox <- get *env, sandbox:offset
+  return-unless curr, null
+  next:&:sandbox <- get *curr, next-sandbox:offset
+  {
+    return-unless next, null
+    found?:bool <- equal next, in
+    break-if found?
+    curr <- copy next
+    next <- get *curr, next-sandbox:offset
+    loop
+  }
+  return curr
+]
+
+scenario scrolling-through-multiple-sandboxes [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 100/width, 10/height
+  # initialize environment
+  assume-resources [
+  ]
+  env:&:environment <- new-programming-environment resources, screen, []
+  render-all screen, env, render
+  # create 2 sandboxes
+  assume-console [
+    press ctrl-n
+    type [add 2, 2]
+    press F4
+    type [add 1, 1]
+    press F4
+  ]
+  event-loop screen, console, env, resources
+  cursor:char <- copy 9251/␣
+  print screen, cursor
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊␣                                                .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊─────────────────────────────────────────────────.
+    .                                                  ┊0   edit       copy       to recipe    delete    .
+    .                                                  ┊add 1, 1                                         .
+    .                                                  ┊2                                                .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .                                                  ┊1   edit       copy       to recipe    delete    .
+    .                                                  ┊add 2, 2                                         .
+    .                                                  ┊4                                                .
+  ]
+  # hit 'page-down'
+  assume-console [
+    press page-down
+  ]
+  run [
+    event-loop screen, console, env, resources
+    cursor:char <- copy 9251/␣
+    print screen, cursor
+  ]
+  # sandbox editor hidden; first sandbox displayed
+  # cursor moves to first sandbox
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊␣   edit       copy       to recipe    delete    .
+    .                                                  ┊add 1, 1                                         .
+    .                                                  ┊2                                                .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .                                                  ┊1   edit       copy       to recipe    delete    .
+    .                                                  ┊add 2, 2                                         .
+    .                                                  ┊4                                                .
+  ]
+  # hit 'page-down' again
+  assume-console [
+    press page-down
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # just second sandbox displayed
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊1   edit       copy       to recipe    delete    .
+    .                                                  ┊add 2, 2                                         .
+    .                                                  ┊4                                                .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .                                                  ┊                                                 .
+  ]
+  # hit 'page-down' again
+  assume-console [
+    press page-down
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # no change
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊1   edit       copy       to recipe    delete    .
+    .                                                  ┊add 2, 2                                         .
+    .                                                  ┊4                                                .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .                                                  ┊                                                 .
+  ]
+  # hit 'page-up'
+  assume-console [
+    press page-up
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # back to displaying both sandboxes without editor
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊0   edit       copy       to recipe    delete    .
+    .                                                  ┊add 1, 1                                         .
+    .                                                  ┊2                                                .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .                                                  ┊1   edit       copy       to recipe    delete    .
+    .                                                  ┊add 2, 2                                         .
+    .                                                  ┊4                                                .
+  ]
+  # hit 'page-up' again
+  assume-console [
+    press page-up
+  ]
+  run [
+    event-loop screen, console, env, resources
+    cursor:char <- copy 9251/␣
+    print screen, cursor
+  ]
+  # back to displaying both sandboxes as well as editor
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊␣                                                .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊─────────────────────────────────────────────────.
+    .                                                  ┊0   edit       copy       to recipe    delete    .
+    .                                                  ┊add 1, 1                                         .
+    .                                                  ┊2                                                .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .                                                  ┊1   edit       copy       to recipe    delete    .
+    .                                                  ┊add 2, 2                                         .
+    .                                                  ┊4                                                .
+  ]
+  # hit 'page-up' again
+  assume-console [
+    press page-up
+  ]
+  run [
+    event-loop screen, console, env, resources
+    cursor:char <- copy 9251/␣
+    print screen, cursor
+  ]
+  # no change
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊␣                                                .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊─────────────────────────────────────────────────.
+    .                                                  ┊0   edit       copy       to recipe    delete    .
+    .                                                  ┊add 1, 1                                         .
+    .                                                  ┊2                                                .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .                                                  ┊1   edit       copy       to recipe    delete    .
+    .                                                  ┊add 2, 2                                         .
+    .                                                  ┊4                                                .
+  ]
+]
+
+scenario scrolling-manages-sandbox-index-correctly [
+  local-scope
+  trace-until 100/app  # trace too long
+  assume-screen 100/width, 10/height
+  # initialize environment
+  assume-resources [
+  ]
+  env:&:environment <- new-programming-environment resources, screen, []
+  render-all screen, env, render
+  # create a sandbox
+  assume-console [
+    press ctrl-n
+    type [add 1, 1]
+    press F4
+  ]
+  event-loop screen, console, env, resources
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊                                                 .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊─────────────────────────────────────────────────.
+    .                                                  ┊0   edit       copy       to recipe    delete    .
+    .                                                  ┊add 1, 1                                         .
+    .                                                  ┊2                                                .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .                                                  ┊                                                 .
+  ]
+  # hit 'page-down' and 'page-up' a couple of times. sandbox index should be stable
+  assume-console [
+    press page-down
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # sandbox editor hidden; first sandbox displayed
+  # cursor moves to first sandbox
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊0   edit       copy       to recipe    delete    .
+    .                                                  ┊add 1, 1                                         .
+    .                                                  ┊2                                                .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .                                                  ┊                                                 .
+  ]
+  # hit 'page-up' again
+  assume-console [
+    press page-up
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # back to displaying both sandboxes as well as editor
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊                                                 .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊─────────────────────────────────────────────────.
+    .                                                  ┊0   edit       copy       to recipe    delete    .
+    .                                                  ┊add 1, 1                                         .
+    .                                                  ┊2                                                .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .                                                  ┊                                                 .
+  ]
+  # hit 'page-down'
+  assume-console [
+    press page-down
+  ]
+  run [
+    event-loop screen, console, env, resources
+  ]
+  # sandbox editor hidden; first sandbox displayed
+  # cursor moves to first sandbox
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊0   edit       copy       to recipe    delete    .
+    .                                                  ┊add 1, 1                                         .
+    .                                                  ┊2                                                .
+    .                                                  ┊─────────────────────────────────────────────────.
+    .                                                  ┊                                                 .
+  ]
+]