about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorKartik K. Agaram <vc@akkartik.com>2016-01-23 13:32:00 -0800
committerKartik K. Agaram <vc@akkartik.com>2016-01-23 13:32:00 -0800
commita01dd5959383a0dc7ffe2c9b3eee8d2c28e94c45 (patch)
tree487472df0fba86cc8c0219a8677b682fd825035c
parente0d69d3b3326081dda661d714cef97a13dabb7c8 (diff)
downloadmu-a01dd5959383a0dc7ffe2c9b3eee8d2c28e94c45.tar.gz
2594 - bugfixes: managing state when deleting
This required completely redesigning scrolling.
-rw-r--r--edit/005-sandbox.mu174
-rw-r--r--edit/006-sandbox-edit.mu12
-rw-r--r--edit/007-sandbox-delete.mu213
-rw-r--r--sandbox/005-sandbox.mu174
-rw-r--r--sandbox/006-sandbox-edit.mu14
-rw-r--r--sandbox/007-sandbox-delete.mu215
6 files changed, 615 insertions, 187 deletions
diff --git a/edit/005-sandbox.mu b/edit/005-sandbox.mu
index 10ec7817..160c4794 100644
--- a/edit/005-sandbox.mu
+++ b/edit/005-sandbox.mu
@@ -19,8 +19,13 @@ 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
+  render-from:number
+  number-of-sandboxes:number
+]
+
+after <programming-environment-initialization> [
+  render-from:address:number <- get-address *result, render-from:offset
+  *render-from <- copy -1
 ]
 
 container sandbox-data [
@@ -28,6 +33,7 @@ container sandbox-data [
   response:address:shared:array:character
   expected-response:address:shared:array:character
   # coordinates to track clicks
+  # constraint: will be 0 for sandboxes at positions before env.render-from
   starting-row-on-screen:number
   code-ending-row-on-screen:number  # past end of code
   response-starting-row-on-screen:number
@@ -162,6 +168,9 @@ recipe run-sandboxes env:address:shared:programming-environment-data, screen:add
     next:address:address:shared:sandbox-data <- get-address *new-sandbox, next-sandbox:offset
     *next <- copy *dest
     *dest <- copy new-sandbox
+    # update sandbox count
+    sandbox-count:address:number <- get-address *env, number-of-sandboxes:offset
+    *sandbox-count <- add *sandbox-count, 1
     # clear sandbox editor
     init:address:address:shared:duplex-list:character <- get-address *current-sandbox, data:offset
     *init <- push 167/§, 0/tail
@@ -244,19 +253,26 @@ recipe! render-sandbox-side screen:address:shared:screen, env:address:shared:pro
 #?   $log [render sandbox side]
   trace 11, [app], [render sandbox side]
   current-sandbox:address:shared:editor-data <- get *env, current-sandbox:offset
+  row:number, column:number <- copy 1, 0
   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
+  # render sandbox editor
+  render-from:number <- get *env, render-from:offset
+  {
+    render-current-sandbox?:boolean <- equal render-from, -1
+    break-unless render-current-sandbox?
+    row, column, screen, current-sandbox <- render screen, current-sandbox
+    clear-screen-from screen, row, column, left, right
+    row <- add row, 1
+  }
+  # render sandboxes
   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
+  row, screen <- render-sandboxes screen, sandbox, left, right, row, render-from
   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 [
+recipe render-sandboxes screen:address:shared:screen, sandbox:address:shared:sandbox-data, left:number, right:number, row:number, render-from:number, idx:number -> row:number, screen:address:shared:screen, sandbox:address:shared:sandbox-data [
   local-scope
   load-ingredients
 #?   $log [render sandbox]
@@ -264,48 +280,62 @@ recipe render-sandboxes screen:address:shared:screen, sandbox:address:shared:san
   screen-height:number <- screen-height screen
   at-bottom?:boolean <- greater-or-equal row, screen-height
   reply-if at-bottom?:boolean
-  # render sandbox menu
-  row <- add row, 1
-  screen <- move-cursor screen, row, left
-  print screen, idx, 240/dark-grey
-  clear-line-delimited screen, left, right
-  delete-icon:character <- copy 120/x
-  print screen, delete-icon, 245/grey
-  # save menu row so we can detect clicks to it later
-  starting-row:address:number <- get-address *sandbox, starting-row-on-screen:offset
-  *starting-row <- copy row
-  # render sandbox contents
-  row <- add row, 1
-  screen <- move-cursor screen, row, left
-  sandbox-data:address:shared:array:character <- get *sandbox, data:offset
-  row, screen <- render-code screen, sandbox-data, left, right, row
-  code-ending-row:address:number <- get-address *sandbox, code-ending-row-on-screen:offset
-  *code-ending-row <- copy row
-  # render sandbox warnings, screen or response, in that order
-  response-starting-row:address:number <- get-address *sandbox, response-starting-row-on-screen:offset
-  sandbox-response:address:shared:array:character <- get *sandbox, response:offset
-  <render-sandbox-results>
+  hidden?:boolean <- lesser-than idx, render-from
   {
-    sandbox-screen:address:shared:screen <- get *sandbox, screen:offset
-    empty-screen?:boolean <- fake-screen-is-empty? sandbox-screen
-    break-if empty-screen?
-    row, screen <- render-screen screen, sandbox-screen, left, right, row
+    break-if hidden?
+    # render sandbox menu
+    row <- add row, 1
+    screen <- move-cursor screen, row, left
+    print screen, idx, 240/dark-grey
+    clear-line-delimited screen, left, right
+    delete-icon:character <- copy 120/x
+    print screen, delete-icon, 245/grey
+    # save menu row so we can detect clicks to it later
+    starting-row:address:number <- get-address *sandbox, starting-row-on-screen:offset
+    *starting-row <- copy row
+    # render sandbox contents
+    row <- add row, 1
+    screen <- move-cursor screen, row, left
+    sandbox-data:address:shared:array:character <- get *sandbox, data:offset
+    row, screen <- render-code screen, sandbox-data, left, right, row
+    code-ending-row:address:number <- get-address *sandbox, code-ending-row-on-screen:offset
+    *code-ending-row <- copy row
+    # render sandbox warnings, screen or response, in that order
+    response-starting-row:address:number <- get-address *sandbox, response-starting-row-on-screen:offset
+    sandbox-response:address:shared:array:character <- get *sandbox, response:offset
+    <render-sandbox-results>
+    {
+      sandbox-screen:address:shared:screen <- get *sandbox, screen:offset
+      empty-screen?:boolean <- fake-screen-is-empty? sandbox-screen
+      break-if empty-screen?
+      row, screen <- render-screen screen, sandbox-screen, left, right, row
+    }
+    {
+      break-unless empty-screen?
+      *response-starting-row <- copy row
+      <render-sandbox-response>
+      row, screen <- render screen, sandbox-response, left, right, 245/grey, row
+    }
+    +render-sandbox-end
+    at-bottom?:boolean <- greater-or-equal row, screen-height
+    reply-if at-bottom?
+    # draw solid line after sandbox
+    draw-horizontal screen, row, left, right, 9473/horizontal-double
   }
+  # if hidden, reset row attributes
   {
-    break-unless empty-screen?
-    *response-starting-row <- copy row
-    <render-sandbox-response>
-    row, screen <- render screen, sandbox-response, left, right, 245/grey, row
+    break-unless hidden?
+    tmp:address:number <- get-address *sandbox, starting-row-on-screen:offset
+    *tmp <- copy 0
+    tmp:address:number <- get-address *sandbox, code-ending-row-on-screen:offset
+    *tmp <- copy 0
+    tmp:address:number <- get-address *sandbox, response-starting-row-on-screen:offset
+    *tmp <- copy 0
   }
-  +render-sandbox-end
-  at-bottom?:boolean <- greater-or-equal row, screen-height
-  reply-if at-bottom?
-  # draw solid line after sandbox
-  draw-horizontal screen, row, left, right, 9473/horizontal-double
   # draw next sandbox
   next-sandbox:address:shared:sandbox-data <- get *sandbox, next-sandbox:offset
   next-idx:number <- add idx, 1
-  row, screen <- render-sandboxes screen, next-sandbox, left, right, row, next-idx
+  row, screen <- render-sandboxes screen, next-sandbox, left, right, row, render-from, next-idx
 ]
 
 # assumes programming environment has no sandboxes; restores them from previous session
@@ -598,7 +628,7 @@ scenario scrolling-down-past-bottom-of-sandbox-editor [
   ]
 ]
 
-# down on sandbox side updates first-sandbox-to-render when sandbox editor has cursor at bottom
+# down on sandbox side updates render-from when sandbox editor has cursor at bottom
 after <global-keypress> [
   {
     break-unless *sandbox-in-focus?
@@ -610,21 +640,14 @@ after <global-keypress> [
     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
-    first-sandbox-index:address:number <- get-address *env, first-sandbox-index:offset
-    # if first-sandbox-to-render is set, slide it down if possible
+    # slide 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 <- 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
-      *first-sandbox-index <- copy 0
+      render-from:address:number <- get-address *env, render-from:offset
+      number-of-sandboxes:number <- get *env, number-of-sandboxes:offset
+      max:number <- subtract number-of-sandboxes, 1
+      at-end?:boolean <- greater-or-equal *render-from, max
+      break-if at-end?
+      *render-from <- add *render-from, 1
     }
     hide-screen screen
     screen <- render-sandbox-side screen, env
@@ -633,46 +656,29 @@ after <global-keypress> [
   }
 ]
 
-# 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
+# update-cursor takes render-from 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
+    render-from:number <- get *env, render-from:offset
+    scrolling?:boolean <- greater-or-equal render-from, 0
+    break-unless scrolling?
     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
+# 'up' on sandbox side is like 'down': updates render-from 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
-    }
+    render-from:address:number <- get-address *env, render-from:offset
+    at-beginning?:boolean <- equal *render-from, -1
+    break-if at-beginning?
+    *render-from <- subtract *render-from, 1
     hide-screen screen
     screen <- render-sandbox-side screen, env
     show-screen screen
diff --git a/edit/006-sandbox-edit.mu b/edit/006-sandbox-edit.mu
index db1ee324..600a5c4c 100644
--- a/edit/006-sandbox-edit.mu
+++ b/edit/006-sandbox-edit.mu
@@ -79,12 +79,11 @@ after <global-touch> [
     break-unless empty-sandbox-editor?  # don't clobber existing contents
     # identify the sandbox to edit and remove it from the sandbox list
     sandbox:address:shared:sandbox-data <- extract-sandbox env, click-row
+    break-unless sandbox
     text:address:shared:array:character <- get *sandbox, data:offset
     current-sandbox <- insert-text current-sandbox, text
-    first-sandbox-to-render:address:address:shared:sandbox-data <- get-address *env, first-sandbox-to-render:offset
-    *first-sandbox-to-render <- copy 0
-    first-sandbox-index:address:number <- get-address *env, first-sandbox-index:offset
-    *first-sandbox-index <- copy 0
+    render-from:address:number <- get-address *env, render-from:offset
+    *render-from <- copy -1
     hide-screen screen
     screen <- render-sandbox-side screen, env
     screen <- update-cursor screen, recipes, current-sandbox, *sandbox-in-focus?, env
@@ -104,11 +103,10 @@ recipe empty-editor? editor:address:shared:editor-data -> result:boolean [
 recipe extract-sandbox env:address:shared:programming-environment-data, click-row:number -> result:address:shared:sandbox-data, env:address:shared:programming-environment-data [
   local-scope
   load-ingredients
-  # assert click-row >= sandbox.starting-row-on-screen
   sandbox:address:address:shared:sandbox-data <- get-address *env, sandbox:offset
   start:number <- get **sandbox, starting-row-on-screen:offset
-  clicked-on-sandboxes?:boolean <- greater-or-equal click-row, start
-  assert clicked-on-sandboxes?, [extract-sandbox called on click to sandbox editor]
+  in-editor?:boolean <- lesser-than click-row, start
+  reply-if in-editor?, 0
   {
     next-sandbox:address:shared:sandbox-data <- get **sandbox, next-sandbox:offset
     break-unless next-sandbox
diff --git a/edit/007-sandbox-delete.mu b/edit/007-sandbox-delete.mu
index e973d8a1..a478c93f 100644
--- a/edit/007-sandbox-delete.mu
+++ b/edit/007-sandbox-delete.mu
@@ -95,9 +95,20 @@ recipe delete-sandbox t:touch-event, env:address:shared:programming-environment-
       target-row:number <- get *curr, starting-row-on-screen:offset
       delete-curr?:boolean <- equal target-row, click-row
       break-unless delete-curr?
-      # delete this sandbox, rerender and stop
+      # delete this sandbox
       *prev <- get *curr, next-sandbox:offset
-      reply 1/true
+      # update sandbox count
+      sandbox-count:address:number <- get-address *env, number-of-sandboxes:offset
+      *sandbox-count <- subtract *sandbox-count, 1
+      # if it's the last sandbox and if it was the only sandbox rendered, reset scroll
+      {
+        break-if *prev
+        render-from:address:number <- get-address *env, render-from:offset
+        reset-scroll?:boolean <- equal *render-from, *sandbox-count
+        break-unless reset-scroll?
+        *render-from <- copy -1
+      }
+      reply 1/true  # force rerender
     }
     prev <- get-address *curr, next-sandbox:offset
     curr <- get *curr, next-sandbox:offset
@@ -105,3 +116,201 @@ recipe delete-sandbox t:touch-event, env:address:shared:programming-environment-
   }
   reply 0/false
 ]
+
+scenario deleting-sandbox-after-scroll [
+  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 and scroll to second
+  assume-console [
+    press ctrl-n
+    type [add 2, 2]
+    press F4
+    type [add 1, 1]
+    press F4
+    press down-arrow
+  ]
+  run [
+    event-loop screen:address:shared:screen, console:address:shared:console, 3:address:shared:programming-environment-data
+  ]
+  screen-should-contain [
+    .                              .
+    .               ┊━━━━━━━━━━━━━━.
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊0            x.
+    .               ┊add 1, 1      .
+    .               ┊2             .
+    .               ┊━━━━━━━━━━━━━━.
+    .               ┊1            x.
+  ]
+  # delete the second sandbox
+  assume-console [
+    left-click 6, 29
+  ]
+  run [
+    event-loop screen:address:shared:screen, console:address:shared:console, 3:address:shared:programming-environment-data
+  ]
+  # second sandbox shows in editor; scroll resets to display first sandbox
+  screen-should-contain [
+    .                              .
+    .               ┊━━━━━━━━━━━━━━.
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊0            x.
+    .               ┊add 1, 1      .
+    .               ┊2             .
+    .               ┊━━━━━━━━━━━━━━.
+    .               ┊              .
+  ]
+]
+
+scenario deleting-top-sandbox-after-scroll [
+  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 and scroll to second
+  assume-console [
+    press ctrl-n
+    type [add 2, 2]
+    press F4
+    type [add 1, 1]
+    press F4
+    press down-arrow
+  ]
+  run [
+    event-loop screen:address:shared:screen, console:address:shared:console, 3:address:shared:programming-environment-data
+  ]
+  screen-should-contain [
+    .                              .
+    .               ┊━━━━━━━━━━━━━━.
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊0            x.
+    .               ┊add 1, 1      .
+    .               ┊2             .
+    .               ┊━━━━━━━━━━━━━━.
+    .               ┊1            x.
+  ]
+  # delete the second sandbox
+  assume-console [
+    left-click 2, 29
+  ]
+  run [
+    event-loop screen:address:shared:screen, console:address:shared:console, 3:address:shared:programming-environment-data
+  ]
+  # second sandbox shows in editor; scroll resets to display first sandbox
+  screen-should-contain [
+    .                              .
+    .               ┊━━━━━━━━━━━━━━.
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊0            x.
+    .               ┊add 2, 2      .
+    .               ┊4             .
+    .               ┊━━━━━━━━━━━━━━.
+    .               ┊              .
+  ]
+]
+
+scenario deleting-final-sandbox-after-scroll [
+  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 and scroll to second
+  assume-console [
+    press ctrl-n
+    type [add 2, 2]
+    press F4
+    type [add 1, 1]
+    press F4
+    press down-arrow
+    press down-arrow
+  ]
+  run [
+    event-loop screen:address:shared:screen, console:address:shared:console, 3:address:shared:programming-environment-data
+  ]
+  screen-should-contain [
+    .                              .
+    .               ┊━━━━━━━━━━━━━━.
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊1            x.
+    .               ┊add 2, 2      .
+    .               ┊4             .
+    .               ┊━━━━━━━━━━━━━━.
+    .               ┊              .
+  ]
+  # delete the second sandbox
+  assume-console [
+    left-click 2, 29
+  ]
+  run [
+    event-loop screen:address:shared:screen, console:address:shared:console, 3:address:shared:programming-environment-data
+  ]
+  # implicitly scroll up to first sandbox
+  screen-should-contain [
+    .                              .
+    .               ┊              .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊━━━━━━━━━━━━━━.
+    .               ┊0            x.
+    .               ┊add 1, 1      .
+    .               ┊2             .
+    .               ┊━━━━━━━━━━━━━━.
+    .               ┊              .
+  ]
+]
+
+scenario deleting-updates-sandbox-count [
+  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
+  ]
+  screen-should-contain [
+    .                              .
+    .               ┊              .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊━━━━━━━━━━━━━━.
+    .               ┊0            x.
+    .               ┊add 1, 1      .
+    .               ┊2             .
+    .               ┊━━━━━━━━━━━━━━.
+    .               ┊1            x.
+    .               ┊add 2, 2      .
+    .               ┊4             .
+  ]
+  # delete the second sandbox, then try to scroll down twice
+  assume-console [
+    left-click 3, 29
+    press down-arrow
+    press down-arrow
+  ]
+  run [
+    event-loop screen:address:shared:screen, console:address:shared:console, 3:address:shared:programming-environment-data
+  ]
+  # shouldn't go past last sandbox
+  screen-should-contain [
+    .                              .
+    .               ┊━━━━━━━━━━━━━━.
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊0            x.
+    .               ┊add 2, 2      .
+    .               ┊4             .
+    .               ┊━━━━━━━━━━━━━━.
+    .               ┊              .
+  ]
+]
diff --git a/sandbox/005-sandbox.mu b/sandbox/005-sandbox.mu
index 08617080..2821f2db 100644
--- a/sandbox/005-sandbox.mu
+++ b/sandbox/005-sandbox.mu
@@ -6,8 +6,13 @@
 
 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
+  render-from:number
+  number-of-sandboxes:number
+]
+
+after <programming-environment-initialization> [
+  render-from:address:number <- get-address *result, render-from:offset
+  *render-from <- copy -1
 ]
 
 container sandbox-data [
@@ -15,6 +20,7 @@ container sandbox-data [
   response:address:shared:array:character
   expected-response:address:shared:array:character
   # coordinates to track clicks
+  # constraint: will be 0 for sandboxes at positions before env.render-from
   starting-row-on-screen:number
   code-ending-row-on-screen:number  # past end of code
   response-starting-row-on-screen:number
@@ -145,6 +151,9 @@ recipe run-sandboxes env:address:shared:programming-environment-data, screen:add
     next:address:address:shared:sandbox-data <- get-address *new-sandbox, next-sandbox:offset
     *next <- copy *dest
     *dest <- copy new-sandbox
+    # update sandbox count
+    sandbox-count:address:number <- get-address *env, number-of-sandboxes:offset
+    *sandbox-count <- add *sandbox-count, 1
     # clear sandbox editor
     init:address:address:shared:duplex-list:character <- get-address *current-sandbox, data:offset
     *init <- push 167/§, 0/tail
@@ -224,19 +233,26 @@ recipe! render-sandbox-side screen:address:shared:screen, env:address:shared:pro
   load-ingredients
   trace 11, [app], [render sandbox side]
   current-sandbox:address:shared:editor-data <- get *env, current-sandbox:offset
+  row:number, column:number <- copy 1, 0
   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
+  # render sandbox editor
+  render-from:number <- get *env, render-from:offset
+  {
+    render-current-sandbox?:boolean <- equal render-from, -1
+    break-unless render-current-sandbox?
+    row, column, screen, current-sandbox <- render screen, current-sandbox
+    clear-screen-from screen, row, column, left, right
+    row <- add row, 1
+  }
+  # render sandboxes
   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
+  row, screen <- render-sandboxes screen, sandbox, left, right, row, render-from, 0, env
   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 [
+recipe render-sandboxes screen:address:shared:screen, sandbox:address:shared:sandbox-data, left:number, right:number, row:number, render-from:number, idx:number -> row:number, screen:address:shared:screen, sandbox:address:shared:sandbox-data [
   local-scope
   load-ingredients
   env:address:shared:programming-environment-data, _/optional <- next-ingredient
@@ -244,48 +260,62 @@ recipe render-sandboxes screen:address:shared:screen, sandbox:address:shared:san
   screen-height:number <- screen-height screen
   at-bottom?:boolean <- greater-or-equal row, screen-height
   reply-if at-bottom?:boolean
-  # render sandbox menu
-  row <- add row, 1
-  screen <- move-cursor screen, row, left
-  print screen, idx, 240/dark-grey
-  clear-line-delimited screen, left, right
-  delete-icon:character <- copy 120/x
-  print screen, delete-icon, 245/grey
-  # save menu row so we can detect clicks to it later
-  starting-row:address:number <- get-address *sandbox, starting-row-on-screen:offset
-  *starting-row <- copy row
-  # render sandbox contents
-  row <- add row, 1
-  screen <- move-cursor screen, row, left
-  sandbox-data:address:shared:array:character <- get *sandbox, data:offset
-  row, screen <- render-code screen, sandbox-data, left, right, row
-  code-ending-row:address:number <- get-address *sandbox, code-ending-row-on-screen:offset
-  *code-ending-row <- copy row
-  # render sandbox warnings, screen or response, in that order
-  response-starting-row:address:number <- get-address *sandbox, response-starting-row-on-screen:offset
-  sandbox-response:address:shared:array:character <- get *sandbox, response:offset
-  <render-sandbox-results>
+  hidden?:boolean <- lesser-than idx, render-from
   {
-    sandbox-screen:address:shared:screen <- get *sandbox, screen:offset
-    empty-screen?:boolean <- fake-screen-is-empty? sandbox-screen
-    break-if empty-screen?
-    row, screen <- render-screen screen, sandbox-screen, left, right, row
+    break-if hidden?
+    # render sandbox menu
+    row <- add row, 1
+    screen <- move-cursor screen, row, left
+    print screen, idx, 240/dark-grey
+    clear-line-delimited screen, left, right
+    delete-icon:character <- copy 120/x
+    print screen, delete-icon, 245/grey
+    # save menu row so we can detect clicks to it later
+    starting-row:address:number <- get-address *sandbox, starting-row-on-screen:offset
+    *starting-row <- copy row
+    # render sandbox contents
+    row <- add row, 1
+    screen <- move-cursor screen, row, left
+    sandbox-data:address:shared:array:character <- get *sandbox, data:offset
+    row, screen <- render-code screen, sandbox-data, left, right, row
+    code-ending-row:address:number <- get-address *sandbox, code-ending-row-on-screen:offset
+    *code-ending-row <- copy row
+    # render sandbox warnings, screen or response, in that order
+    response-starting-row:address:number <- get-address *sandbox, response-starting-row-on-screen:offset
+    sandbox-response:address:shared:array:character <- get *sandbox, response:offset
+    <render-sandbox-results>
+    {
+      sandbox-screen:address:shared:screen <- get *sandbox, screen:offset
+      empty-screen?:boolean <- fake-screen-is-empty? sandbox-screen
+      break-if empty-screen?
+      row, screen <- render-screen screen, sandbox-screen, left, right, row
+    }
+    {
+      break-unless empty-screen?
+      *response-starting-row <- copy row
+      <render-sandbox-response>
+      row, screen <- render screen, sandbox-response, left, right, 245/grey, row
+    }
+    +render-sandbox-end
+    at-bottom?:boolean <- greater-or-equal row, screen-height
+    reply-if at-bottom?, row/same-as-ingredient:4, screen/same-as-ingredient:0
+    # draw solid line after sandbox
+    draw-horizontal screen, row, left, right, 9473/horizontal-double
   }
+  # if hidden, reset row attributes
   {
-    break-unless empty-screen?
-    *response-starting-row <- copy row
-    <render-sandbox-response>
-    row, screen <- render screen, sandbox-response, left, right, 245/grey, row
+    break-unless hidden?
+    tmp:address:number <- get-address *sandbox, starting-row-on-screen:offset
+    *tmp <- copy 0
+    tmp:address:number <- get-address *sandbox, code-ending-row-on-screen:offset
+    *tmp <- copy 0
+    tmp:address:number <- get-address *sandbox, response-starting-row-on-screen:offset
+    *tmp <- copy 0
   }
-  +render-sandbox-end
-  at-bottom?:boolean <- greater-or-equal row, screen-height
-  reply-if at-bottom?, row/same-as-ingredient:4, screen/same-as-ingredient:0
-  # draw solid line after sandbox
-  draw-horizontal screen, row, left, right, 9473/horizontal-double
   # draw next sandbox
   next-sandbox:address:shared:sandbox-data <- get *sandbox, next-sandbox:offset
   next-idx:number <- add idx, 1
-  row, screen <- render-sandboxes screen, next-sandbox, left, right, row, next-idx, env
+  row, screen <- render-sandboxes screen, next-sandbox, left, right, row, render-from, next-idx, env
 ]
 
 # assumes programming environment has no sandboxes; restores them from previous session
@@ -530,7 +560,7 @@ scenario scrolling-down-past-bottom-of-sandbox-editor [
   ]
 ]
 
-# down on sandbox side updates first-sandbox-to-render when sandbox editor has cursor at bottom
+# down on sandbox side updates render-from when sandbox editor has cursor at bottom
 after <global-keypress> [
   {
     down?:boolean <- equal *k, 65516/down-arrow
@@ -541,21 +571,14 @@ after <global-keypress> [
     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
-    first-sandbox-index:address:number <- get-address *env, first-sandbox-index:offset
-    # if first-sandbox-to-render is set, slide it down if possible
+    # slide 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 <- 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
-      *first-sandbox-index <- copy 0
+      render-from:address:number <- get-address *env, render-from:offset
+      number-of-sandboxes:number <- get *env, number-of-sandboxes:offset
+      max:number <- subtract number-of-sandboxes, 1
+      at-end?:boolean <- greater-or-equal *render-from, max
+      break-if at-end?
+      *render-from <- add *render-from, 1
     }
     hide-screen screen
     screen <- render-sandbox-side screen, env
@@ -564,27 +587,14 @@ after <global-keypress> [
   }
 ]
 
-# 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
+# update-cursor takes render-from 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
+    render-from:number <- get *env, render-from:offset
+    scrolling?:boolean <- greater-or-equal render-from, 0
+    break-unless scrolling?
     cursor-column:number <- get *current-sandbox, left:offset
-    screen <- move-cursor screen, 2/row, cursor-column
+    screen <- move-cursor screen, 2/row, cursor-column  # highlighted sandbox will always start at row 2
     reply
   }
 ]
@@ -594,14 +604,10 @@ 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
-    }
+    render-from:address:number <- get-address *env, render-from:offset
+    at-beginning?:boolean <- equal *render-from, -1
+    break-if at-beginning?
+    *render-from <- subtract *render-from, 1
     hide-screen screen
     screen <- render-sandbox-side screen, env
     show-screen screen
diff --git a/sandbox/006-sandbox-edit.mu b/sandbox/006-sandbox-edit.mu
index 3885588d..6f3ac272 100644
--- a/sandbox/006-sandbox-edit.mu
+++ b/sandbox/006-sandbox-edit.mu
@@ -77,15 +77,14 @@ after <global-touch> [
     below-sandbox-editor?:boolean <- greater-or-equal click-row, first-sandbox-begins
     break-unless below-sandbox-editor?
     empty-sandbox-editor?:boolean <- empty-editor? current-sandbox
-    break-unless empty-sandbox-editor?  # make the user hit F4 before editing a new sandbox
+    break-unless empty-sandbox-editor?  # don't clobber existing contents
     # identify the sandbox to edit and remove it from the sandbox list
     sandbox:address:shared:sandbox-data <- extract-sandbox env, click-row
+    break-unless sandbox
     text:address:shared:array:character <- get *sandbox, data:offset
     current-sandbox <- insert-text current-sandbox, text
-    first-sandbox-to-render:address:address:shared:sandbox-data <- get-address *env, first-sandbox-to-render:offset
-    *first-sandbox-to-render <- copy 0
-    first-sandbox-index:address:number <- get-address *env, first-sandbox-index:offset
-    *first-sandbox-index <- copy 0
+    render-from:address:number <- get-address *env, render-from:offset
+    *render-from <- copy -1
     hide-screen screen
     screen <- render-sandbox-side screen, env
     screen <- update-cursor screen, current-sandbox, env
@@ -105,11 +104,10 @@ recipe empty-editor? editor:address:shared:editor-data -> result:boolean [
 recipe extract-sandbox env:address:shared:programming-environment-data, click-row:number -> result:address:shared:sandbox-data, env:address:shared:programming-environment-data [
   local-scope
   load-ingredients
-  # assert click-row >= sandbox.starting-row-on-screen
   sandbox:address:address:shared:sandbox-data <- get-address *env, sandbox:offset
   start:number <- get **sandbox, starting-row-on-screen:offset
-  clicked-on-sandboxes?:boolean <- greater-or-equal click-row, start
-  assert clicked-on-sandboxes?, [extract-sandbox called on click to sandbox editor]
+  in-editor?:boolean <- lesser-than click-row, start
+  reply-if in-editor?, 0
   {
     next-sandbox:address:shared:sandbox-data <- get **sandbox, next-sandbox:offset
     break-unless next-sandbox
diff --git a/sandbox/007-sandbox-delete.mu b/sandbox/007-sandbox-delete.mu
index f3c3ef10..af9db190 100644
--- a/sandbox/007-sandbox-delete.mu
+++ b/sandbox/007-sandbox-delete.mu
@@ -94,9 +94,20 @@ recipe delete-sandbox t:touch-event, env:address:shared:programming-environment-
       target-row:number <- get *curr, starting-row-on-screen:offset
       delete-curr?:boolean <- equal target-row, click-row
       break-unless delete-curr?
-      # delete this sandbox, rerender and stop
+      # delete this sandbox
       *prev <- get *curr, next-sandbox:offset
-      reply 1/true
+      # update sandbox count
+      sandbox-count:address:number <- get-address *env, number-of-sandboxes:offset
+      *sandbox-count <- subtract *sandbox-count, 1
+      # if it's the last sandbox and if it was the only sandbox rendered, reset scroll
+      {
+        break-if *prev
+        render-from:address:number <- get-address *env, render-from:offset
+        reset-scroll?:boolean <- equal *render-from, *sandbox-count
+        break-unless reset-scroll?
+        *render-from <- copy -1
+      }
+      reply 1/true  # force rerender
     }
     prev <- get-address *curr, next-sandbox:offset
     curr <- get *curr, next-sandbox:offset
@@ -104,3 +115,203 @@ recipe delete-sandbox t:touch-event, env:address:shared:programming-environment-
   }
   reply 0/false
 ]
+
+scenario deleting-sandbox-after-scroll [
+  trace-until 100/app  # trace too long
+  assume-screen 30/width, 10/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 and scroll to second
+  assume-console [
+    press ctrl-n
+    type [add 2, 2]
+    press F4
+    type [add 1, 1]
+    press F4
+    press down-arrow
+  ]
+  run [
+    event-loop screen:address:shared:screen, console:address:shared:console, 2:address:shared:programming-environment-data
+  ]
+  screen-should-contain [
+    .                              .  # menu
+    .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━.
+    .0                            x.
+    .add 1, 1                      .
+    .2                             .
+    .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━.
+    .1                            x.
+    .add 2, 2                      .
+    .4                             .
+    .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━.
+  ]
+  # delete the second sandbox
+  assume-console [
+    left-click 6, 29
+  ]
+  run [
+    event-loop screen:address:shared:screen, console:address:shared:console, 2:address:shared:programming-environment-data
+  ]
+  # second sandbox shows in editor; scroll resets to display first sandbox
+  screen-should-contain [
+    .                              .
+    .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━.
+    .0                            x.
+    .add 1, 1                      .
+    .2                             .
+    .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━.
+    .                              .
+  ]
+]
+
+scenario deleting-top-sandbox-after-scroll [
+  trace-until 100/app  # trace too long
+  assume-screen 30/width, 10/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 and scroll to second
+  assume-console [
+    press ctrl-n
+    type [add 2, 2]
+    press F4
+    type [add 1, 1]
+    press F4
+    press down-arrow
+  ]
+  run [
+    event-loop screen:address:shared:screen, console:address:shared:console, 2:address:shared:programming-environment-data
+  ]
+  screen-should-contain [
+    .                              .  # menu
+    .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━.
+    .0                            x.
+    .add 1, 1                      .
+    .2                             .
+    .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━.
+    .1                            x.
+    .add 2, 2                      .
+    .4                             .
+    .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━.
+  ]
+  # delete the second sandbox
+  assume-console [
+    left-click 2, 29
+  ]
+  run [
+    event-loop screen:address:shared:screen, console:address:shared:console, 2:address:shared:programming-environment-data
+  ]
+  # second sandbox shows in editor; scroll resets to display first sandbox
+  screen-should-contain [
+    .                              .
+    .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━.
+    .0                            x.
+    .add 2, 2                      .
+    .4                             .
+    .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━.
+    .                              .
+  ]
+]
+
+scenario deleting-final-sandbox-after-scroll [
+  trace-until 100/app  # trace too long
+  assume-screen 30/width, 10/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 and scroll to second
+  assume-console [
+    press ctrl-n
+    type [add 2, 2]
+    press F4
+    type [add 1, 1]
+    press F4
+    press down-arrow
+    press down-arrow
+  ]
+  run [
+    event-loop screen:address:shared:screen, console:address:shared:console, 2:address:shared:programming-environment-data
+  ]
+  screen-should-contain [
+    .                              .
+    .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━.
+    .1                            x.
+    .add 2, 2                      .
+    .4                             .
+    .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━.
+    .                              .
+  ]
+  # delete the second sandbox
+  assume-console [
+    left-click 2, 29
+  ]
+  run [
+    event-loop screen:address:shared:screen, console:address:shared:console, 2:address:shared:programming-environment-data
+  ]
+  # implicitly scroll up to first sandbox
+  screen-should-contain [
+    .                              .
+    .                              .
+    .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━.
+    .0                            x.
+    .add 1, 1                      .
+    .2                             .
+    .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━.
+    .                              .
+  ]
+]
+
+scenario deleting-updates-sandbox-count [
+  trace-until 100/app  # trace too long
+  assume-screen 30/width, 10/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
+  ]
+  screen-should-contain [
+    .                              .
+    .                              .
+    .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━.
+    .0                            x.
+    .add 1, 1                      .
+    .2                             .
+    .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━.
+    .1                            x.
+    .add 2, 2                      .
+    .4                             .
+  ]
+  # delete the second sandbox, then try to scroll down twice
+  assume-console [
+    left-click 3, 29
+    press down-arrow
+    press down-arrow
+  ]
+  run [
+    event-loop screen:address:shared:screen, console:address:shared:console, 2:address:shared:programming-environment-data
+  ]
+  # shouldn't go past last sandbox
+  screen-should-contain [
+    .                              .
+    .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━.
+    .0                            x.
+    .add 2, 2                      .
+    .4                             .
+    .━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━.
+    .                              .
+  ]
+]