about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorKartik K. Agaram <vc@akkartik.com>2016-01-22 19:11:59 -0800
committerKartik K. Agaram <vc@akkartik.com>2016-01-22 19:11:59 -0800
commitaaf61a535c73ea8631e1820df973fa585b046528 (patch)
tree87ffd99387c4fbb4b53c33aaae8dc5004e126c8d
parent2894e8d5deba8af7afb2548420c8014348027a19 (diff)
downloadmu-aaf61a535c73ea8631e1820df973fa585b046528.tar.gz
2590 - support scrolling through sandboxes
-rw-r--r--edit/001-editor.mu8
-rw-r--r--edit/004-programming-environment.mu11
-rw-r--r--edit/005-sandbox.mu373
-rw-r--r--edit/006-sandbox-edit.mu2
-rw-r--r--edit/007-sandbox-delete.mu2
-rw-r--r--edit/008-sandbox-test.mu2
-rw-r--r--edit/009-sandbox-trace.mu2
-rw-r--r--sandbox/001-editor.mu8
-rw-r--r--sandbox/004-programming-environment.mu11
-rw-r--r--sandbox/005-sandbox.mu340
-rw-r--r--sandbox/006-sandbox-edit.mu2
-rw-r--r--sandbox/007-sandbox-delete.mu2
-rw-r--r--sandbox/008-sandbox-test.mu2
-rw-r--r--sandbox/009-sandbox-trace.mu2
14 files changed, 741 insertions, 26 deletions
diff --git a/edit/001-editor.mu b/edit/001-editor.mu
index 773b7d77..c773a0ad 100644
--- a/edit/001-editor.mu
+++ b/edit/001-editor.mu
@@ -39,6 +39,7 @@ container editor-data [
   # always displays from row 1 (leaving row 0 for a menu) and at most until bottom of screen
   left:number
   right:number
+  bottom:number
   # raw screen coordinates of cursor
   cursor-row:number
   cursor-column:number
@@ -114,8 +115,9 @@ scenario editor-initializes-without-data [
     # 5 (before cursor) <- the § sentinel
     6 <- 2  # left
     7 <- 4  # right  (inclusive)
-    8 <- 1  # cursor row
-    9 <- 2  # cursor column
+    8 <- 1  # bottom
+    9 <- 1  # cursor row
+    10 <- 2  # cursor column
   ]
   screen-should-contain [
     .     .
@@ -222,6 +224,8 @@ recipe render screen:address:shared:screen, editor:address:shared:editor-data ->
     *cursor-column <- copy column
     *before-cursor <- copy prev
   }
+  bottom:address:number <- get-address *editor, bottom:offset
+  *bottom <- copy row
   reply row, column, screen/same-as-ingredient:0, editor/same-as-ingredient:1
 ]
 
diff --git a/edit/004-programming-environment.mu b/edit/004-programming-environment.mu
index 359a32e2..79b7c7fe 100644
--- a/edit/004-programming-environment.mu
+++ b/edit/004-programming-environment.mu
@@ -91,7 +91,7 @@ recipe event-loop screen:address:shared:screen, console:address:shared:console,
       # send to both editors
       _ <- move-cursor-in-editor screen, recipes, *t
       *sandbox-in-focus? <- move-cursor-in-editor screen, current-sandbox, *t
-      screen <- update-cursor screen, recipes, current-sandbox, *sandbox-in-focus?
+      screen <- update-cursor screen, recipes, current-sandbox, *sandbox-in-focus?, env
       loop +next-event:label
     }
     # 'resize' event - redraw editor
@@ -173,7 +173,7 @@ recipe event-loop screen:address:shared:screen, console:address:shared:console,
         }
       }
       +finish-event
-      screen <- update-cursor screen, recipes, current-sandbox, *sandbox-in-focus?
+      screen <- update-cursor screen, recipes, current-sandbox, *sandbox-in-focus?, env
       show-screen screen
     }
     loop
@@ -402,7 +402,7 @@ recipe render-all screen:address:shared:screen, env:address:shared:programming-e
   recipes:address:shared:editor-data <- get *env, recipes:offset
   current-sandbox:address:shared:editor-data <- get *env, current-sandbox:offset
   sandbox-in-focus?:boolean <- get *env, sandbox-in-focus?:offset
-  screen <- update-cursor screen, recipes, current-sandbox, sandbox-in-focus?
+  screen <- update-cursor screen, recipes, current-sandbox, sandbox-in-focus?, env
   #
   show-screen screen
 ]
@@ -441,9 +441,10 @@ recipe render-sandbox-side screen:address:shared:screen, env:address:shared:prog
   clear-screen-from screen, row, left, left, right
 ]
 
-recipe update-cursor screen:address:shared:screen, recipes:address:shared:editor-data, current-sandbox:address:shared:editor-data, sandbox-in-focus?:boolean -> screen:address:shared:screen [
+recipe update-cursor screen:address:shared:screen, recipes:address:shared:editor-data, current-sandbox:address:shared:editor-data, sandbox-in-focus?:boolean, env:address:shared:programming-environment-data -> screen:address:shared:screen [
   local-scope
   load-ingredients
+  <update-cursor-special-cases>
   {
     break-if sandbox-in-focus?
     cursor-row:number <- get *recipes, cursor-row:offset
@@ -602,7 +603,7 @@ after <global-type> [
     switch-side?:boolean <- equal *c, 14/ctrl-n
     break-unless switch-side?
     *sandbox-in-focus? <- not *sandbox-in-focus?
-    screen <- update-cursor screen, recipes, current-sandbox, *sandbox-in-focus?
+    screen <- update-cursor screen, recipes, current-sandbox, *sandbox-in-focus?, env
     loop +next-event:label
   }
 ]
diff --git a/edit/005-sandbox.mu b/edit/005-sandbox.mu
index 7c229b2f..178642ba 100644
--- a/edit/005-sandbox.mu
+++ b/edit/005-sandbox.mu
@@ -19,6 +19,8 @@ recipe! main [
 
 container programming-environment-data [
   sandbox:address:shared:sandbox-data  # list of sandboxes, from top to bottom
+  first-sandbox-to-render:address:shared:sandbox-data  # 0 = display current-sandbox editor
+  first-sandbox-index:number
 ]
 
 container sandbox-data [
@@ -134,7 +136,7 @@ after <global-keypress> [
       status:address:shared:array:character <- new [                 ]
       screen <- update-status screen, status, 245/grey
     }
-    screen <- update-cursor screen, recipes, current-sandbox, *sandbox-in-focus?
+    screen <- update-cursor screen, recipes, current-sandbox, *sandbox-in-focus?, env
     loop +next-event:label
   }
 ]
@@ -244,13 +246,14 @@ recipe! render-sandbox-side screen:address:shared:screen, env:address:shared:pro
   current-sandbox:address:shared:editor-data <- get *env, current-sandbox:offset
   left:number <- get *current-sandbox, left:offset
   right:number <- get *current-sandbox, right:offset
+  <render-sandbox-side-special-cases>
   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:shared:sandbox-data <- get *env, sandbox:offset
   row, screen <- render-sandboxes screen, sandbox, left, right, row, 0
-  clear-rest-of-screen screen, row, left, left, right
+  clear-rest-of-screen screen, row, left, right
 ]
 
 recipe render-sandboxes screen:address:shared:screen, sandbox:address:shared:sandbox-data, left:number, right:number, row:number, idx:number -> row:number, screen:address:shared:screen, sandbox:address:shared:sandbox-data [
@@ -523,3 +526,369 @@ scenario editor-provides-edited-contents [
     4:array:character <- [abdefc]
   ]
 ]
+
+# scrolling through sandboxes
+
+scenario scrolling-down-past-bottom-of-sandbox-editor [
+  trace-until 100/app  # trace too long
+  assume-screen 30/width, 10/height
+  # initialize sandbox side
+  1:address:shared:array:character <- new []
+  2:address:shared:array:character <- new [add 2, 2]
+  3:address:shared:programming-environment-data <- new-programming-environment screen:address:shared:screen, 1:address:shared:array:character, 2:address:shared:array:character
+  render-all screen, 3:address:shared:programming-environment-data
+  assume-console [
+    # create a sandbox
+    press F4
+    # switch to sandbox editor and type in 2 lines
+    press ctrl-n
+    type [abc
+]
+  ]
+  run [
+    event-loop screen:address:shared:screen, console:address:shared:console, 3:address:shared:programming-environment-data
+    4:character/cursor <- copy 9251/␣
+    print screen:address:shared:screen, 4:character/cursor
+  ]
+  screen-should-contain [
+    .                              .  # minor: F4 clears menu tooltip in very narrow screens
+    .               ┊abc           .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊␣             .
+    .               ┊━━━━━━━━━━━━━━.
+    .               ┊0            x.
+    .               ┊add 2, 2      .
+  ]
+  # hit 'down' at bottom of sandbox editor
+  assume-console [
+    press down-arrow
+  ]
+  run [
+    event-loop screen:address:shared:screen, console:address:shared:console, 3:address:shared:programming-environment-data
+    4:character/cursor <- copy 9251/␣
+    print screen:address:shared:screen, 4:character/cursor
+  ]
+  # sandbox editor hidden; first sandbox displayed
+  # cursor moves to first sandbox
+  screen-should-contain [
+    .                              .
+    .               ┊━━━━━━━━━━━━━━.
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊␣            x.
+    .               ┊add 2, 2      .
+    .               ┊4             .
+    .               ┊━━━━━━━━━━━━━━.
+    .               ┊              .
+  ]
+  # hit 'up'
+  assume-console [
+    press up-arrow
+  ]
+  run [
+    event-loop screen:address:shared:screen, console:address:shared:console, 3:address:shared:programming-environment-data
+    4:character/cursor <- copy 9251/␣
+    print screen:address:shared:screen, 4:character/cursor
+  ]
+  # sandbox editor displays again
+  screen-should-contain [
+    .                              .  # minor: F4 clears menu tooltip in very narrow screens
+    .               ┊abc           .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊␣             .
+    .               ┊━━━━━━━━━━━━━━.
+    .               ┊0            x.
+    .               ┊add 2, 2      .
+  ]
+]
+
+# down on sandbox side updates first-sandbox-to-render when sandbox editor has cursor at bottom
+after <global-keypress> [
+  {
+    break-unless *sandbox-in-focus?
+    down?:boolean <- equal *k, 65516/down-arrow
+    break-unless down?
+    sandbox-bottom:number <- get *current-sandbox, bottom:offset
+    sandbox-cursor:number <- get *current-sandbox, cursor-row:offset
+    sandbox-cursor-on-last-line?:boolean <- equal sandbox-bottom, sandbox-cursor
+    break-unless sandbox-cursor-on-last-line?
+    sandbox:address:shared:sandbox-data <- get *env, sandbox:offset
+    break-unless sandbox
+    first-sandbox-to-render:address:address:shared:sandbox-data <- get-address *env, first-sandbox-to-render:offset
+    # if first-sandbox-to-render is set, slide it down if possible
+    {
+      break-unless *first-sandbox-to-render
+      next:address:shared:sandbox-data <- get **first-sandbox-to-render, next-sandbox:offset
+      break-unless next
+      *first-sandbox-to-render <- copy next
+      first-sandbox-index:address:number <- get-address *env, first-sandbox-index:offset
+      *first-sandbox-index <- add *first-sandbox-index, 1
+    }
+    # if first-sandbox-to-render is not set, set it to first sandbox
+    {
+      break-if *first-sandbox-to-render
+      *first-sandbox-to-render <- copy sandbox
+    }
+    hide-screen screen
+    screen <- render-sandbox-side screen, env
+    show-screen screen
+    jump +finish-event:label
+  }
+]
+
+# render-sandbox-side takes first-sandbox-to-render into account
+after <render-sandbox-side-special-cases> [
+  {
+    first-sandbox-to-render:address:shared:sandbox-data <- get *env, first-sandbox-to-render:offset
+    break-unless first-sandbox-to-render
+    row:number <- copy 1  # skip menu
+    draw-horizontal screen, row, left, right, 9473/horizontal-double
+    first-sandbox-index:number <- get *env, first-sandbox-index:offset
+    row, screen <- render-sandboxes screen, first-sandbox-to-render, left, right, row, first-sandbox-index
+    clear-rest-of-screen screen, row, left, right
+    reply
+  }
+]
+
+# update-cursor takes first-sandbox-to-render into account
+after <update-cursor-special-cases> [
+  {
+    break-unless sandbox-in-focus?
+    first-sandbox-to-render:address:shared:sandbox-data <- get *env, first-sandbox-to-render:offset
+    break-unless first-sandbox-to-render
+    cursor-column:number <- get *current-sandbox, left:offset
+    screen <- move-cursor screen, 2/row, cursor-column  # highlighted sandbox will always start at row 2
+    reply
+  }
+]
+
+# 'up' on sandbox side is like 'down': updates first-sandbox-to-render when necessary
+after <global-keypress> [
+  {
+    break-unless *sandbox-in-focus?
+    up?:boolean <- equal *k, 65517/up-arrow
+    break-unless up?
+    first-sandbox-to-render:address:address:shared:sandbox-data <- get-address *env, first-sandbox-to-render:offset
+    break-unless *first-sandbox-to-render
+    {
+      break-unless *first-sandbox-to-render
+      *first-sandbox-to-render <- previous-sandbox env, *first-sandbox-to-render
+      first-sandbox-index:address:number <- get-address *env, first-sandbox-index:offset
+      *first-sandbox-index <- subtract *first-sandbox-index, 1
+    }
+    hide-screen screen
+    screen <- render-sandbox-side screen, env
+    show-screen screen
+    jump +finish-event:label
+  }
+]
+
+# sandbox belonging to 'env' whose next-sandbox is 'in'
+# return 0 if there's no such sandbox, either because 'in' doesn't exist in 'env', or because it's the first sandbox
+recipe previous-sandbox env:address:shared:programming-environment-data, in:address:shared:sandbox-data -> out:address:shared:sandbox-data [
+  local-scope
+  load-ingredients
+  curr:address:shared:sandbox-data <- get *env, sandbox:offset
+  reply-unless curr, 0/nil
+  next:address:shared:sandbox-data <- get *curr, next-sandbox:offset
+  {
+    reply-unless next, 0/nil
+    found?:boolean <- equal next, in
+    break-if found?
+    curr <- copy next
+    next <- get *curr, next-sandbox:offset
+    loop
+  }
+  reply curr
+]
+
+scenario scrolling-down-on-recipe-side [
+  trace-until 100/app  # trace too long
+  assume-screen 30/width, 10/height
+  # initialize sandbox side and create a sandbox
+  1:address:shared:array:character <- new [ 
+]
+  # create a sandbox
+  2:address:shared:array:character <- new [add 2, 2]
+  3:address:shared:programming-environment-data <- new-programming-environment screen:address:shared:screen, 1:address:shared:array:character, 2:address:shared:array:character
+  render-all screen, 3:address:shared:programming-environment-data
+  assume-console [
+    press F4
+  ]
+  run [
+    event-loop screen:address:shared:screen, console:address:shared:console, 3:address:shared:programming-environment-data
+  ]
+  # hit 'down' in recipe editor
+  assume-console [
+    press down-arrow
+  ]
+  run [
+    event-loop screen:address:shared:screen, console:address:shared:console, 3:address:shared:programming-environment-data
+    4:character/cursor <- copy 9251/␣
+    print screen:address:shared:screen, 4:character/cursor
+  ]
+  # sandbox editor hidden; first sandbox displayed
+  # cursor moves to first sandbox
+  screen-should-contain [
+    .                              .
+    .               ┊              .
+    .␣              ┊━━━━━━━━━━━━━━.
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊0            x.
+    .               ┊add 2, 2      .
+    .               ┊4             .
+    .               ┊━━━━━━━━━━━━━━.
+    .               ┊              .
+  ]
+]
+
+scenario scrolling-through-multiple-sandboxes [
+  trace-until 100/app  # trace too long
+  assume-screen 30/width, 10/height
+  # initialize environment
+  1:address:shared:array:character <- new []
+  2:address:shared:array:character <- new []
+  3:address:shared:programming-environment-data <- new-programming-environment screen:address:shared:screen, 1:address:shared:array:character, 2:address:shared:array:character
+  render-all screen, 3:address:shared:programming-environment-data
+  # create 2 sandboxes
+  assume-console [
+    press ctrl-n
+    type [add 2, 2]
+    press F4
+    type [add 1, 1]
+    press F4
+  ]
+  run [
+    event-loop screen:address:shared:screen, console:address:shared:console, 3:address:shared:programming-environment-data
+    4:character/cursor <- copy 9251/␣
+    print screen:address:shared:screen, 4:character/cursor
+  ]
+  screen-should-contain [
+    .                              .
+    .               ┊␣             .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊━━━━━━━━━━━━━━.
+    .               ┊0            x.
+    .               ┊add 1, 1      .
+    .               ┊2             .
+    .               ┊━━━━━━━━━━━━━━.
+    .               ┊1            x.
+    .               ┊add 2, 2      .
+    .               ┊4             .
+  ]
+  # hit 'down'
+  assume-console [
+    press down-arrow
+  ]
+  run [
+    event-loop screen:address:shared:screen, console:address:shared:console, 3:address:shared:programming-environment-data
+    4:character/cursor <- copy 9251/␣
+    print screen:address:shared:screen, 4:character/cursor
+  ]
+  # sandbox editor hidden; first sandbox displayed
+  # cursor moves to first sandbox
+  screen-should-contain [
+    .                              .
+    .               ┊━━━━━━━━━━━━━━.
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊␣            x.
+    .               ┊add 1, 1      .
+    .               ┊2             .
+    .               ┊━━━━━━━━━━━━━━.
+    .               ┊1            x.
+    .               ┊add 2, 2      .
+    .               ┊4             .
+    .               ┊━━━━━━━━━━━━━━.
+  ]
+  # hit 'down' again
+  assume-console [
+    press down-arrow
+  ]
+  run [
+    event-loop screen:address:shared:screen, console:address:shared:console, 3:address:shared:programming-environment-data
+  ]
+  # just second sandbox displayed
+  screen-should-contain [
+    .                              .
+    .               ┊━━━━━━━━━━━━━━.
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊1            x.
+    .               ┊add 2, 2      .
+    .               ┊4             .
+    .               ┊━━━━━━━━━━━━━━.
+    .               ┊              .
+    .               ┊              .
+  ]
+  # hit 'down' again
+  assume-console [
+    press down-arrow
+  ]
+  run [
+    event-loop screen:address:shared:screen, console:address:shared:console, 3:address:shared:programming-environment-data
+  ]
+  # no change
+  screen-should-contain [
+    .                              .
+    .               ┊━━━━━━━━━━━━━━.
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊1            x.
+    .               ┊add 2, 2      .
+    .               ┊4             .
+    .               ┊━━━━━━━━━━━━━━.
+    .               ┊              .
+    .               ┊              .
+  ]
+  # hit 'up'
+  assume-console [
+    press up-arrow
+  ]
+  run [
+    event-loop screen:address:shared:screen, console:address:shared:console, 3:address:shared:programming-environment-data
+  ]
+  # back to displaying both sandboxes without editor
+  screen-should-contain [
+    .                              .
+    .               ┊━━━━━━━━━━━━━━.
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊0            x.
+    .               ┊add 1, 1      .
+    .               ┊2             .
+    .               ┊━━━━━━━━━━━━━━.
+    .               ┊1            x.
+    .               ┊add 2, 2      .
+    .               ┊4             .
+    .               ┊━━━━━━━━━━━━━━.
+  ]
+  # hit 'up' again
+  assume-console [
+    press up-arrow
+  ]
+  run [
+    event-loop screen:address:shared:screen, console:address:shared:console, 3:address:shared:programming-environment-data
+    4:character/cursor <- copy 9251/␣
+    print screen:address:shared:screen, 4:character/cursor
+  ]
+  # back to displaying both sandboxes as well as editor
+  screen-should-contain [
+    .                              .
+    .               ┊␣             .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊━━━━━━━━━━━━━━.
+    .               ┊0            x.
+    .               ┊add 1, 1      .
+    .               ┊2             .
+    .               ┊━━━━━━━━━━━━━━.
+    .               ┊1            x.
+    .               ┊add 2, 2      .
+    .               ┊4             .
+  ]
+  # hit 'up' again
+  assume-console [
+    press up-arrow
+  ]
+  run [
+    event-loop screen:address:shared:screen, console:address:shared:console, 3:address:shared:programming-environment-data
+  ]
+  # no change
+  screen-should-contain [
+    .                              .
+    .               ┊␣             .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊━━━━━━━━━━━━━━.
+    .               ┊0            x.
+    .               ┊add 1, 1      .
+    .               ┊2             .
+    .               ┊━━━━━━━━━━━━━━.
+    .               ┊1            x.
+    .               ┊add 2, 2      .
+    .               ┊4             .
+  ]
+]
diff --git a/edit/006-sandbox-edit.mu b/edit/006-sandbox-edit.mu
index db4dd2a8..496882d6 100644
--- a/edit/006-sandbox-edit.mu
+++ b/edit/006-sandbox-edit.mu
@@ -83,7 +83,7 @@ after <global-touch> [
     current-sandbox <- insert-text current-sandbox, text
     hide-screen screen
     screen <- render-sandbox-side screen, env
-    screen <- update-cursor screen, recipes, current-sandbox, *sandbox-in-focus?
+    screen <- update-cursor screen, recipes, current-sandbox, *sandbox-in-focus?, env
     show-screen screen
     loop +next-event:label
   }
diff --git a/edit/007-sandbox-delete.mu b/edit/007-sandbox-delete.mu
index 3cbe7d5d..e973d8a1 100644
--- a/edit/007-sandbox-delete.mu
+++ b/edit/007-sandbox-delete.mu
@@ -71,7 +71,7 @@ after <global-touch> [
     break-unless was-delete?
     hide-screen screen
     screen <- render-sandbox-side screen, env
-    screen <- update-cursor screen, recipes, current-sandbox, *sandbox-in-focus?
+    screen <- update-cursor screen, recipes, current-sandbox, *sandbox-in-focus?, env
     show-screen screen
     loop +next-event:label
   }
diff --git a/edit/008-sandbox-test.mu b/edit/008-sandbox-test.mu
index b525cfd1..fb6dc260 100644
--- a/edit/008-sandbox-test.mu
+++ b/edit/008-sandbox-test.mu
@@ -104,7 +104,7 @@ after <global-touch> [
     save-sandboxes env
     hide-screen screen
     screen <- render-sandbox-side screen, env, 1/clear
-    screen <- update-cursor screen, recipes, current-sandbox, *sandbox-in-focus?
+    screen <- update-cursor screen, recipes, current-sandbox, *sandbox-in-focus?, env
     # no change in cursor
     show-screen screen
     loop +next-event:label
diff --git a/edit/009-sandbox-trace.mu b/edit/009-sandbox-trace.mu
index 68dca1ab..d95350bc 100644
--- a/edit/009-sandbox-trace.mu
+++ b/edit/009-sandbox-trace.mu
@@ -159,7 +159,7 @@ after <global-touch> [
     *x <- not *x
     hide-screen screen
     screen <- render-sandbox-side screen, env, 1/clear
-    screen <- update-cursor screen, recipes, current-sandbox, *sandbox-in-focus?
+    screen <- update-cursor screen, recipes, current-sandbox, *sandbox-in-focus?, env
     # no change in cursor
     show-screen screen
     loop +next-event:label
diff --git a/sandbox/001-editor.mu b/sandbox/001-editor.mu
index 773b7d77..c773a0ad 100644
--- a/sandbox/001-editor.mu
+++ b/sandbox/001-editor.mu
@@ -39,6 +39,7 @@ container editor-data [
   # always displays from row 1 (leaving row 0 for a menu) and at most until bottom of screen
   left:number
   right:number
+  bottom:number
   # raw screen coordinates of cursor
   cursor-row:number
   cursor-column:number
@@ -114,8 +115,9 @@ scenario editor-initializes-without-data [
     # 5 (before cursor) <- the § sentinel
     6 <- 2  # left
     7 <- 4  # right  (inclusive)
-    8 <- 1  # cursor row
-    9 <- 2  # cursor column
+    8 <- 1  # bottom
+    9 <- 1  # cursor row
+    10 <- 2  # cursor column
   ]
   screen-should-contain [
     .     .
@@ -222,6 +224,8 @@ recipe render screen:address:shared:screen, editor:address:shared:editor-data ->
     *cursor-column <- copy column
     *before-cursor <- copy prev
   }
+  bottom:address:number <- get-address *editor, bottom:offset
+  *bottom <- copy row
   reply row, column, screen/same-as-ingredient:0, editor/same-as-ingredient:1
 ]
 
diff --git a/sandbox/004-programming-environment.mu b/sandbox/004-programming-environment.mu
index 81484f0f..7a449622 100644
--- a/sandbox/004-programming-environment.mu
+++ b/sandbox/004-programming-environment.mu
@@ -9,7 +9,7 @@ recipe! main [
   env <- restore-sandboxes env
   render-sandbox-side 0/screen, env
   current-sandbox:address:shared:editor-data <- get *env, current-sandbox:offset
-  update-cursor 0/screen, current-sandbox
+  update-cursor 0/screen, current-sandbox, env
   show-screen 0/screen
   event-loop 0/screen, 0/console, env
   # never gets here
@@ -78,7 +78,7 @@ recipe event-loop screen:address:shared:screen, console:address:shared:console,
       # 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
+      screen <- update-cursor screen, current-sandbox, env
       loop +next-event:label
     }
     # 'resize' event - redraw editor
@@ -129,7 +129,7 @@ recipe event-loop screen:address:shared:screen, console:address:shared:console,
         }
       }
       +finish-event
-      screen <- update-cursor screen, current-sandbox
+      screen <- update-cursor screen, current-sandbox, env
       show-screen screen
     }
     loop
@@ -174,7 +174,7 @@ recipe render-all screen:address:shared:screen, env:address:shared:programming-e
   <render-components-end>
   #
   current-sandbox:address:shared:editor-data <- get *env, current-sandbox:offset
-  screen <- update-cursor screen, current-sandbox
+  screen <- update-cursor screen, current-sandbox, env
   #
   show-screen screen
 ]
@@ -195,9 +195,10 @@ recipe render-sandbox-side screen:address:shared:screen, env:address:shared:prog
   clear-screen-from screen, row, left, left, right
 ]
 
-recipe update-cursor screen:address:shared:screen, current-sandbox:address:shared:editor-data -> screen:address:shared:screen [
+recipe update-cursor screen:address:shared:screen, current-sandbox:address:shared:editor-data, env:address:shared:programming-environment-data -> screen:address:shared:screen [
   local-scope
   load-ingredients
+  <update-cursor-special-cases>
   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
diff --git a/sandbox/005-sandbox.mu b/sandbox/005-sandbox.mu
index fec08106..98a7779c 100644
--- a/sandbox/005-sandbox.mu
+++ b/sandbox/005-sandbox.mu
@@ -6,6 +6,8 @@
 
 container programming-environment-data [
   sandbox:address:shared:sandbox-data  # list of sandboxes, from top to bottom
+  first-sandbox-to-render:address:shared:sandbox-data  # 0 = display current-sandbox editor
+  first-sandbox-index:number
 ]
 
 container sandbox-data [
@@ -117,7 +119,7 @@ after <global-keypress> [
       status:address:shared:array:character <- new [                 ]
       screen <- update-status screen, status, 245/grey
     }
-    screen <- update-cursor screen, current-sandbox
+    screen <- update-cursor screen, current-sandbox, env
     loop +next-event:label
   }
 ]
@@ -224,13 +226,14 @@ recipe! render-sandbox-side screen:address:shared:screen, env:address:shared:pro
   current-sandbox:address:shared:editor-data <- get *env, current-sandbox:offset
   left:number <- get *current-sandbox, left:offset
   right:number <- get *current-sandbox, right:offset
+  <render-sandbox-side-special-cases>
   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:shared:sandbox-data <- get *env, sandbox:offset
   row, screen <- render-sandboxes screen, sandbox, left, right, row, 0, env
-  clear-rest-of-screen screen, row, left, left, right
+  clear-rest-of-screen screen, row, left, right
 ]
 
 recipe render-sandboxes screen:address:shared:screen, sandbox:address:shared:sandbox-data, left:number, right:number, row:number, idx:number -> row:number, screen:address:shared:screen, sandbox:address:shared:sandbox-data [
@@ -451,3 +454,336 @@ scenario editor-provides-edited-contents [
     4:array:character <- [abdefc]
   ]
 ]
+
+# scrolling through sandboxes
+
+scenario scrolling-down-past-bottom-of-sandbox-editor [
+  trace-until 100/app  # trace too long
+  assume-screen 50/width, 20/height
+  # initialize
+  1:address:shared:array:character <- new [add 2, 2]
+  2:address:shared:programming-environment-data <- new-programming-environment screen:address:shared:screen, 1:address:shared:array:character
+  render-all screen, 2:address:shared:programming-environment-data
+  assume-console [
+    # create a sandbox
+    press F4
+    # type in 2 lines
+    type [abc
+]
+  ]
+  run [
+    event-loop screen:address:shared:screen, console:address:shared:console, 2:address:shared:programming-environment-data
+    3:character/cursor <- copy 9251/␣
+    print screen:address:shared:screen, 3:character/cursor
+  ]
+  screen-should-contain [
+    .                               run (F4)           .
+    .abc                                               .
+    .␣                                                 .
+    .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━.
+    .0                                                x.
+    .add 2, 2                                          .
+    .4                                                 .
+    .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━.
+    .                                                  .
+  ]
+  # hit 'down' at bottom of sandbox editor
+  assume-console [
+    press down-arrow
+  ]
+  run [
+    event-loop screen:address:shared:screen, console:address:shared:console, 2:address:shared:programming-environment-data
+    3:character/cursor <- copy 9251/␣
+    print screen:address:shared:screen, 3:character/cursor
+  ]
+  # sandbox editor hidden; first sandbox displayed
+  # cursor moves to first sandbox
+  screen-should-contain [
+    .                               run (F4)           .
+    .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━.
+    .␣                                                x.
+    .add 2, 2                                          .
+    .4                                                 .
+    .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━.
+    .                                                  .
+  ]
+  # hit 'up'
+  assume-console [
+    press up-arrow
+  ]
+  run [
+    event-loop screen:address:shared:screen, console:address:shared:console, 2:address:shared:programming-environment-data
+    3:character/cursor <- copy 9251/␣
+    print screen:address:shared:screen, 3:character/cursor
+  ]
+  # sandbox editor displays again
+  screen-should-contain [
+    .                               run (F4)           .
+    .abc                                               .
+    .␣                                                 .
+    .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━.
+    .0                                                x.
+    .add 2, 2                                          .
+    .4                                                 .
+    .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━.
+    .                                                  .
+  ]
+]
+
+# down on sandbox side updates first-sandbox-to-render when sandbox editor has cursor at bottom
+after <global-keypress> [
+  {
+    down?:boolean <- equal *k, 65516/down-arrow
+    break-unless down?
+    sandbox-bottom:number <- get *current-sandbox, bottom:offset
+    sandbox-cursor:number <- get *current-sandbox, cursor-row:offset
+    sandbox-cursor-on-last-line?:boolean <- equal sandbox-bottom, sandbox-cursor
+    break-unless sandbox-cursor-on-last-line?
+    sandbox:address:shared:sandbox-data <- get *env, sandbox:offset
+    break-unless sandbox
+    first-sandbox-to-render:address:address:shared:sandbox-data <- get-address *env, first-sandbox-to-render:offset
+    # if first-sandbox-to-render is set, slide it down if possible
+    {
+      break-unless *first-sandbox-to-render
+      next:address:shared:sandbox-data <- get **first-sandbox-to-render, next-sandbox:offset
+      break-unless next
+      *first-sandbox-to-render <- copy next
+      first-sandbox-index:address:number <- get-address *env, first-sandbox-index:offset
+      *first-sandbox-index <- add *first-sandbox-index, 1
+    }
+    # if first-sandbox-to-render is not set, set it to first sandbox
+    {
+      break-if *first-sandbox-to-render
+      *first-sandbox-to-render <- copy sandbox
+    }
+    hide-screen screen
+    screen <- render-sandbox-side screen, env
+    show-screen screen
+    jump +finish-event:label
+  }
+]
+
+# render-sandbox-side takes first-sandbox-to-render into account
+after <render-sandbox-side-special-cases> [
+  {
+    first-sandbox-to-render:address:shared:sandbox-data <- get *env, first-sandbox-to-render:offset
+    break-unless first-sandbox-to-render
+    row:number <- copy 1  # skip menu
+    draw-horizontal screen, row, left, right, 9473/horizontal-double
+    first-sandbox-index:number <- get *env, first-sandbox-index:offset
+    row, screen <- render-sandboxes screen, first-sandbox-to-render, left, right, row, first-sandbox-index
+    clear-rest-of-screen screen, row, left, right
+    reply
+  }
+]
+
+# update-cursor takes first-sandbox-to-render into account
+after <update-cursor-special-cases> [
+  {
+    first-sandbox-to-render:address:shared:sandbox-data <- get *env, first-sandbox-to-render:offset
+    break-unless first-sandbox-to-render
+    cursor-column:number <- get *current-sandbox, left:offset
+    screen <- move-cursor screen, 2/row, cursor-column
+    reply
+  }
+]
+
+# 'up' on sandbox side is like 'down': updates first-sandbox-to-render when necessary
+after <global-keypress> [
+  {
+    up?:boolean <- equal *k, 65517/up-arrow
+    break-unless up?
+    first-sandbox-to-render:address:address:shared:sandbox-data <- get-address *env, first-sandbox-to-render:offset
+    break-unless *first-sandbox-to-render
+    {
+      break-unless *first-sandbox-to-render
+      *first-sandbox-to-render <- previous-sandbox env, *first-sandbox-to-render
+      first-sandbox-index:address:number <- get-address *env, first-sandbox-index:offset
+      *first-sandbox-index <- subtract *first-sandbox-index, 1
+    }
+    hide-screen screen
+    screen <- render-sandbox-side screen, env
+    show-screen screen
+    jump +finish-event:label
+  }
+]
+
+# sandbox belonging to 'env' whose next-sandbox is 'in'
+# return 0 if there's no such sandbox, either because 'in' doesn't exist in 'env', or because it's the first sandbox
+recipe previous-sandbox env:address:shared:programming-environment-data, in:address:shared:sandbox-data -> out:address:shared:sandbox-data [
+  local-scope
+  load-ingredients
+  curr:address:shared:sandbox-data <- get *env, sandbox:offset
+  reply-unless curr, 0/nil
+  next:address:shared:sandbox-data <- get *curr, next-sandbox:offset
+  {
+    reply-unless next, 0/nil
+    found?:boolean <- equal next, in
+    break-if found?
+    curr <- copy next
+    next <- get *curr, next-sandbox:offset
+    loop
+  }
+  reply curr
+]
+
+scenario scrolling-through-multiple-sandboxes [
+  trace-until 100/app  # trace too long
+  assume-screen 50/width, 20/height
+  # initialize environment
+  1:address:shared:array:character <- new []
+  2:address:shared:programming-environment-data <- new-programming-environment screen:address:shared:screen, 1:address:shared:array:character
+  render-all screen, 2:address:shared:programming-environment-data
+  # create 2 sandboxes
+  assume-console [
+    press ctrl-n
+    type [add 2, 2]
+    press F4
+    type [add 1, 1]
+    press F4
+  ]
+  run [
+    event-loop screen:address:shared:screen, console:address:shared:console, 2:address:shared:programming-environment-data
+    3:character/cursor <- copy 9251/␣
+    print screen:address:shared:screen, 3:character/cursor
+  ]
+  screen-should-contain [
+    .                               run (F4)           .
+    .␣                                                 .
+    .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━.
+    .0                                                x.
+    .add 1, 1                                          .
+    .2                                                 .
+    .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━.
+    .1                                                x.
+    .add 2, 2                                          .
+    .4                                                 .
+    .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━.
+    .                                                  .
+  ]
+  # hit 'down'
+  assume-console [
+    press down-arrow
+  ]
+  run [
+    event-loop screen:address:shared:screen, console:address:shared:console, 2:address:shared:programming-environment-data
+    3:character/cursor <- copy 9251/␣
+    print screen:address:shared:screen, 3:character/cursor
+  ]
+  # sandbox editor hidden; first sandbox displayed
+  # cursor moves to first sandbox
+  screen-should-contain [
+    .                               run (F4)           .
+    .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━.
+    .␣                                                x.
+    .add 1, 1                                          .
+    .2                                                 .
+    .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━.
+    .1                                                x.
+    .add 2, 2                                          .
+    .4                                                 .
+    .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━.
+    .                                                  .
+  ]
+  # hit 'down' again
+  assume-console [
+    press down-arrow
+  ]
+  run [
+    event-loop screen:address:shared:screen, console:address:shared:console, 2:address:shared:programming-environment-data
+  ]
+  # just second sandbox displayed
+  screen-should-contain [
+    .                               run (F4)           .
+    .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━.
+    .1                                                x.
+    .add 2, 2                                          .
+    .4                                                 .
+    .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━.
+    .                                                  .
+  ]
+  # hit 'down' again
+  assume-console [
+    press down-arrow
+  ]
+  run [
+    event-loop screen:address:shared:screen, console:address:shared:console, 2:address:shared:programming-environment-data
+  ]
+  # no change
+  screen-should-contain [
+    .                               run (F4)           .
+    .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━.
+    .1                                                x.
+    .add 2, 2                                          .
+    .4                                                 .
+    .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━.
+    .                                                  .
+  ]
+  # hit 'up'
+  assume-console [
+    press up-arrow
+  ]
+  run [
+    event-loop screen:address:shared:screen, console:address:shared:console, 2:address:shared:programming-environment-data
+  ]
+  # back to displaying both sandboxes without editor
+  screen-should-contain [
+    .                               run (F4)           .
+    .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━.
+    .0                                                x.
+    .add 1, 1                                          .
+    .2                                                 .
+    .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━.
+    .1                                                x.
+    .add 2, 2                                          .
+    .4                                                 .
+    .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━.
+    .                                                  .
+  ]
+  # hit 'up' again
+  assume-console [
+    press up-arrow
+  ]
+  run [
+    event-loop screen:address:shared:screen, console:address:shared:console, 2:address:shared:programming-environment-data
+    3:character/cursor <- copy 9251/␣
+    print screen:address:shared:screen, 3:character/cursor
+  ]
+  # back to displaying both sandboxes as well as editor
+  screen-should-contain [
+    .                               run (F4)           .
+    .␣                                                 .
+    .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━.
+    .0                                                x.
+    .add 1, 1                                          .
+    .2                                                 .
+    .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━.
+    .1                                                x.
+    .add 2, 2                                          .
+    .4                                                 .
+    .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━.
+    .                                                  .
+  ]
+  # hit 'up' again
+  assume-console [
+    press up-arrow
+  ]
+  run [
+    event-loop screen:address:shared:screen, console:address:shared:console, 2:address:shared:programming-environment-data
+  ]
+  # no change
+  screen-should-contain [
+    .                               run (F4)           .
+    .␣                                                 .
+    .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━.
+    .0                                                x.
+    .add 1, 1                                          .
+    .2                                                 .
+    .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━.
+    .1                                                x.
+    .add 2, 2                                          .
+    .4                                                 .
+    .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━.
+    .                                                  .
+  ]
+]
diff --git a/sandbox/006-sandbox-edit.mu b/sandbox/006-sandbox-edit.mu
index 87bf3e38..825ae485 100644
--- a/sandbox/006-sandbox-edit.mu
+++ b/sandbox/006-sandbox-edit.mu
@@ -84,7 +84,7 @@ after <global-touch> [
     current-sandbox <- insert-text current-sandbox, text
     hide-screen screen
     screen <- render-sandbox-side screen, env
-    screen <- update-cursor screen, current-sandbox
+    screen <- update-cursor screen, current-sandbox, env
     show-screen screen
     loop +next-event:label
   }
diff --git a/sandbox/007-sandbox-delete.mu b/sandbox/007-sandbox-delete.mu
index 3df785f5..f3c3ef10 100644
--- a/sandbox/007-sandbox-delete.mu
+++ b/sandbox/007-sandbox-delete.mu
@@ -70,7 +70,7 @@ after <global-touch> [
     break-unless was-delete?
     hide-screen screen
     screen <- render-sandbox-side screen, env
-    screen <- update-cursor screen, current-sandbox
+    screen <- update-cursor screen, current-sandbox, env
     show-screen screen
     loop +next-event:label
   }
diff --git a/sandbox/008-sandbox-test.mu b/sandbox/008-sandbox-test.mu
index faf76441..4e6b2896 100644
--- a/sandbox/008-sandbox-test.mu
+++ b/sandbox/008-sandbox-test.mu
@@ -24,7 +24,7 @@ after <global-touch> [
     save-sandboxes env
     hide-screen screen
     screen <- render-sandbox-side screen, env, 1/clear
-    screen <- update-cursor screen, current-sandbox
+    screen <- update-cursor screen, current-sandbox, env
     # no change in cursor
     show-screen screen
     loop +next-event:label
diff --git a/sandbox/009-sandbox-trace.mu b/sandbox/009-sandbox-trace.mu
index 8ca16327..c59ecbbf 100644
--- a/sandbox/009-sandbox-trace.mu
+++ b/sandbox/009-sandbox-trace.mu
@@ -151,7 +151,7 @@ after <global-touch> [
     *x <- not *x
     hide-screen screen
     screen <- render-sandbox-side screen, env, 1/clear
-    screen <- update-cursor screen, current-sandbox
+    screen <- update-cursor screen, current-sandbox, env
     # no change in cursor
     show-screen screen
     loop +next-event:label