about summary refs log tree commit diff stats
path: root/sandbox
diff options
context:
space:
mode:
authorKartik Agaram <vc@akkartik.com>2019-07-27 16:01:55 -0700
committerKartik Agaram <vc@akkartik.com>2019-07-27 17:47:59 -0700
commit6e1eeeebfb453fa7c871869c19375ce60fbd7413 (patch)
tree539c4a3fdf1756ae79770d5c4aaf6366f1d1525e /sandbox
parent8846a7f85cc04b77b2fe8a67b6d317723437b00c (diff)
downloadmu-6e1eeeebfb453fa7c871869c19375ce60fbd7413.tar.gz
5485 - promote SubX to top-level
Diffstat (limited to 'sandbox')
-rw-r--r--sandbox/001-editor.mu464
-rw-r--r--sandbox/002-typing.mu1144
-rw-r--r--sandbox/003-shortcuts.mu2800
-rw-r--r--sandbox/004-programming-environment.mu268
-rw-r--r--sandbox/005-sandbox.mu1081
-rw-r--r--sandbox/006-sandbox-copy.mu286
-rw-r--r--sandbox/007-sandbox-delete.mu345
-rw-r--r--sandbox/008-sandbox-edit.mu319
-rw-r--r--sandbox/009-sandbox-test.mu233
-rw-r--r--sandbox/010-sandbox-trace.mu243
-rw-r--r--sandbox/011-errors.mu687
-rw-r--r--sandbox/012-editor-undo.mu1907
-rw-r--r--sandbox/Readme.md33
-rwxr-xr-xsandbox/mu_run16
-rw-r--r--sandbox/tmux.conf3
15 files changed, 0 insertions, 9829 deletions
diff --git a/sandbox/001-editor.mu b/sandbox/001-editor.mu
deleted file mode 100644
index b3399dbb..00000000
--- a/sandbox/001-editor.mu
+++ /dev/null
@@ -1,464 +0,0 @@
-## the basic editor data structure, and how it displays text to the screen
-
-# temporary main for this layer: just render the given text at the given
-# screen dimensions, then stop
-def main text:text [
-  local-scope
-  load-inputs
-  open-console
-  clear-screen null/screen  # non-scrolling app
-  e:&:editor <- new-editor text, 0/left, 5/right
-  render null/screen, e
-  wait-for-event null/console
-  close-console
-]
-
-scenario editor-renders-text-to-screen [
-  local-scope
-  assume-screen 10/width, 5/height
-  e:&:editor <- new-editor [abc], 0/left, 10/right
-  run [
-    render screen, e
-  ]
-  screen-should-contain [
-    # top line of screen reserved for menu
-    .          .
-    .abc       .
-    .          .
-  ]
-]
-
-container editor [
-  # editable text: doubly linked list of characters (head contains a special sentinel)
-  data:&:duplex-list:char
-  top-of-screen:&:duplex-list:char
-  bottom-of-screen:&:duplex-list:char
-  # location before cursor inside data
-  before-cursor:&:duplex-list:char
-
-  # raw bounds of display area on screen
-  # always displays from row 1 (leaving row 0 for a menu) and at most until bottom of screen
-  left:num
-  right:num
-  bottom:num
-  # raw screen coordinates of cursor
-  cursor-row:num
-  cursor-column:num
-]
-
-# creates a new editor widget
-#   right is exclusive
-def new-editor s:text, left:num, right:num -> result:&:editor [
-  local-scope
-  load-inputs
-  # no clipping of bounds
-  right <- subtract right, 1
-  result <- new editor:type
-  # initialize screen-related fields
-  *result <- put *result, left:offset, left
-  *result <- put *result, right:offset, right
-  # initialize cursor coordinates
-  *result <- put *result, cursor-row:offset, 1/top
-  *result <- put *result, cursor-column:offset, left
-  # initialize empty contents
-  init:&:duplex-list:char <- push 167/§, null
-  *result <- put *result, data:offset, init
-  *result <- put *result, top-of-screen:offset, init
-  *result <- put *result, before-cursor:offset, init
-  result <- insert-text result, s
-  <editor-initialization>
-]
-
-def insert-text editor:&:editor, text:text -> editor:&:editor [
-  local-scope
-  load-inputs
-  curr:&:duplex-list:char <- get *editor, data:offset
-  insert curr, text
-]
-
-scenario editor-initializes-without-data [
-  local-scope
-  assume-screen 5/width, 3/height
-  run [
-    e:&:editor <- new-editor null/data, 2/left, 5/right
-    1:editor/raw <- copy *e
-  ]
-  memory-should-contain [
-    # 1,2 (data) <- just the § sentinel
-    # 3,4 (top of screen) <- the § sentinel
-    # 5 (bottom of screen) <- null since text fits on screen
-    5 <- 0
-    6 <- 0
-    # 7,8 (before cursor) <- the § sentinel
-    9 <- 2  # left
-    10 <- 4  # right  (inclusive)
-    11 <- 0  # bottom (not set until render)
-    12 <- 1  # cursor row
-    13 <- 2  # cursor column
-  ]
-  screen-should-contain [
-    .     .
-    .     .
-    .     .
-  ]
-]
-
-# Assumes cursor should be at coordinates (cursor-row, cursor-column) and
-# updates before-cursor to match. Might also move coordinates if they're
-# outside text.
-def render screen:&:screen, editor:&:editor -> last-row:num, last-column:num, screen:&:screen, editor:&:editor [
-  local-scope
-  load-inputs
-  return-unless editor, 1/top, 0/left
-  left:num <- get *editor, left:offset
-  screen-height:num <- screen-height screen
-  right:num <- get *editor, right:offset
-  # traversing editor
-  curr:&:duplex-list:char <- get *editor, top-of-screen:offset
-  prev:&:duplex-list:char <- copy curr  # just in case curr becomes null and we can't compute prev
-  curr <- next curr
-  # traversing screen
-  color:num <- copy 7/white
-  row:num <- copy 1/top
-  column:num <- copy left
-  cursor-row:num <- get *editor, cursor-row:offset
-  cursor-column:num <- get *editor, cursor-column:offset
-  before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
-  screen <- move-cursor screen, row, column
-  {
-    +next-character
-    break-unless curr
-    off-screen?:bool <- greater-or-equal row, screen-height
-    break-if off-screen?
-    # update editor.before-cursor
-    # Doing so at the start of each iteration ensures it stays one step behind
-    # the current character.
-    {
-      at-cursor-row?:bool <- equal row, cursor-row
-      break-unless at-cursor-row?
-      at-cursor?:bool <- equal column, cursor-column
-      break-unless at-cursor?
-      before-cursor <- copy prev
-    }
-    c:char <- get *curr, value:offset
-    <character-c-received>
-    {
-      # newline? move to left rather than 0
-      newline?:bool <- equal c, 10/newline
-      break-unless newline?
-      # adjust cursor if necessary
-      {
-        at-cursor-row?:bool <- equal row, cursor-row
-        break-unless at-cursor-row?
-        left-of-cursor?:bool <- lesser-than column, cursor-column
-        break-unless left-of-cursor?
-        cursor-column <- copy column
-        before-cursor <- prev curr
-      }
-      # clear rest of line in this window
-      clear-line-until screen, right
-      # skip to next line
-      row <- add row, 1
-      column <- copy left
-      screen <- move-cursor screen, row, column
-      curr <- next curr
-      prev <- next prev
-      loop +next-character
-    }
-    {
-      # at right? wrap. even if there's only one more letter left; we need
-      # room for clicking on the cursor after it.
-      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 curr
-      loop +next-character
-    }
-    print screen, c, color
-    curr <- next curr
-    prev <- next prev
-    column <- add column, 1
-    loop
-  }
-  # save first character off-screen
-  *editor <- put *editor, bottom-of-screen:offset, curr
-  # is cursor to the right of the last line? move to end
-  {
-    at-cursor-row?:bool <- equal row, cursor-row
-    cursor-outside-line?:bool <- lesser-or-equal column, cursor-column
-    before-cursor-on-same-line?:bool <- and at-cursor-row?, cursor-outside-line?
-    above-cursor-row?:bool <- lesser-than row, cursor-row
-    before-cursor?:bool <- or before-cursor-on-same-line?, above-cursor-row?
-    break-unless before-cursor?
-    cursor-row <- copy row
-    cursor-column <- copy column
-    before-cursor <- copy prev
-  }
-  *editor <- put *editor, bottom:offset, row
-  *editor <- put *editor, cursor-row:offset, cursor-row
-  *editor <- put *editor, cursor-column:offset, cursor-column
-  *editor <- put *editor, before-cursor:offset, before-cursor
-  clear-line-until screen, right
-  row <- add row, 1
-  return row, left/column
-]
-
-def clear-screen-from screen:&:screen, row:num, column:num, left:num, right:num -> screen:&:screen [
-  local-scope
-  load-inputs
-  # if it's the real screen, use the optimized primitive
-  {
-    break-if screen
-    clear-display-from row, column, left, right
-    return
-  }
-  # if not, go the slower route
-  screen <- move-cursor screen, row, column
-  clear-line-until screen, right
-  clear-rest-of-screen screen, row, left, right
-]
-
-def clear-rest-of-screen screen:&:screen, row:num, left:num, right:num -> screen:&:screen [
-  local-scope
-  load-inputs
-  row <- add row, 1
-  # if it's the real screen, use the optimized primitive
-  {
-    break-if screen
-    clear-display-from row, left, left, right
-    return
-  }
-  screen <- move-cursor screen, row, left
-  screen-height:num <- screen-height screen
-  {
-    at-bottom-of-screen?:bool <- greater-or-equal row, screen-height
-    break-if at-bottom-of-screen?
-    screen <- move-cursor screen, row, left
-    clear-line-until screen, right
-    row <- add row, 1
-    loop
-  }
-]
-
-scenario editor-prints-multiple-lines [
-  local-scope
-  assume-screen 5/width, 5/height
-  s:text <- new [abc
-def]
-  e:&:editor <- new-editor s, 0/left, 5/right
-  run [
-    render screen, e
-  ]
-  screen-should-contain [
-    .     .
-    .abc  .
-    .def  .
-    .     .
-  ]
-]
-
-scenario editor-handles-offsets [
-  local-scope
-  assume-screen 5/width, 5/height
-  e:&:editor <- new-editor [abc], 1/left, 5/right
-  run [
-    render screen, e
-  ]
-  screen-should-contain [
-    .     .
-    . abc .
-    .     .
-  ]
-]
-
-scenario editor-prints-multiple-lines-at-offset [
-  local-scope
-  assume-screen 5/width, 5/height
-  s:text <- new [abc
-def]
-  e:&:editor <- new-editor s, 1/left, 5/right
-  run [
-    render screen, e
-  ]
-  screen-should-contain [
-    .     .
-    . abc .
-    . def .
-    .     .
-  ]
-]
-
-scenario editor-wraps-long-lines [
-  local-scope
-  assume-screen 5/width, 5/height
-  e:&:editor <- new-editor [abc def], 0/left, 5/right
-  run [
-    render screen, e
-  ]
-  screen-should-contain [
-    .     .
-    .abc ↩.
-    .def  .
-    .     .
-  ]
-  screen-should-contain-in-color 245/grey [
-    .     .
-    .    ↩.
-    .     .
-    .     .
-  ]
-]
-
-scenario editor-wraps-barely-long-lines [
-  local-scope
-  assume-screen 5/width, 5/height
-  e:&:editor <- new-editor [abcde], 0/left, 5/right
-  run [
-    render screen, e
-  ]
-  # still wrap, even though the line would fit. We need room to click on the
-  # end of the line
-  screen-should-contain [
-    .     .
-    .abcd↩.
-    .e    .
-    .     .
-  ]
-  screen-should-contain-in-color 245/grey [
-    .     .
-    .    ↩.
-    .     .
-    .     .
-  ]
-]
-
-scenario editor-with-empty-text [
-  local-scope
-  assume-screen 5/width, 5/height
-  e:&:editor <- new-editor [], 0/left, 5/right
-  run [
-    render screen, e
-    3:num/raw <- get *e, cursor-row:offset
-    4:num/raw <- get *e, cursor-column:offset
-  ]
-  screen-should-contain [
-    .     .
-    .     .
-    .     .
-  ]
-  memory-should-contain [
-    3 <- 1  # cursor row
-    4 <- 0  # cursor column
-  ]
-]
-
-# just a little color for Mu code
-
-scenario render-colors-comments [
-  local-scope
-  assume-screen 5/width, 5/height
-  s:text <- new [abc
-# de
-f]
-  e:&:editor <- new-editor s, 0/left, 5/right
-  run [
-    render screen, e
-  ]
-  screen-should-contain [
-    .     .
-    .abc  .
-    .# de .
-    .f    .
-    .     .
-  ]
-  screen-should-contain-in-color 12/lightblue, [
-    .     .
-    .     .
-    .# de .
-    .     .
-    .     .
-  ]
-  screen-should-contain-in-color 7/white, [
-    .     .
-    .abc  .
-    .     .
-    .f    .
-    .     .
-  ]
-]
-
-after <character-c-received> [
-  color <- get-color color, c
-]
-
-# so far the previous color is all the information we need; that may change
-def get-color color:num, c:char -> color:num [
-  local-scope
-  load-inputs
-  color-is-white?:bool <- equal color, 7/white
-  # if color is white and next character is '#', switch color to blue
-  {
-    break-unless color-is-white?
-    starting-comment?:bool <- equal c, 35/#
-    break-unless starting-comment?
-    trace 90, [app], [switch color back to blue]
-    return 12/lightblue
-  }
-  # if color is blue and next character is newline, switch color to white
-  {
-    color-is-blue?:bool <- equal color, 12/lightblue
-    break-unless color-is-blue?
-    ending-comment?:bool <- equal c, 10/newline
-    break-unless ending-comment?
-    trace 90, [app], [switch color back to white]
-    return 7/white
-  }
-  # if color is white (no comments) and next character is '<', switch color to red
-  {
-    break-unless color-is-white?
-    starting-assignment?:bool <- equal c, 60/<
-    break-unless starting-assignment?
-    return 1/red
-  }
-  # if color is red and next character is space, switch color to white
-  {
-    color-is-red?:bool <- equal color, 1/red
-    break-unless color-is-red?
-    ending-assignment?:bool <- equal c, 32/space
-    break-unless ending-assignment?
-    return 7/white
-  }
-  # otherwise no change
-  return color
-]
-
-scenario render-colors-assignment [
-  local-scope
-  assume-screen 8/width, 5/height
-  s:text <- new [abc
-d <- e
-f]
-  e:&:editor <- new-editor s, 0/left, 8/right
-  run [
-    render screen, e
-  ]
-  screen-should-contain [
-    .        .
-    .abc     .
-    .d <- e  .
-    .f       .
-    .        .
-  ]
-  screen-should-contain-in-color 1/red, [
-    .        .
-    .        .
-    .  <-    .
-    .        .
-    .        .
-  ]
-]
diff --git a/sandbox/002-typing.mu b/sandbox/002-typing.mu
deleted file mode 100644
index ef3f25d2..00000000
--- a/sandbox/002-typing.mu
+++ /dev/null
@@ -1,1144 +0,0 @@
-## handling events from the keyboard, mouse, touch screen, ...
-
-# temporary main: interactive editor
-# hit ctrl-c to exit
-def! main text:text [
-  local-scope
-  load-inputs
-  open-console
-  clear-screen null/screen  # non-scrolling app
-  editor:&:editor <- new-editor text, 5/left, 45/right
-  editor-render null/screen, editor
-  editor-event-loop null/screen, null/console, editor
-  close-console
-]
-
-def editor-event-loop screen:&:screen, console:&:console, editor:&:editor -> screen:&:screen, console:&:console, editor:&:editor [
-  local-scope
-  load-inputs
-  {
-    # looping over each (keyboard or touch) event as it occurs
-    +next-event
-    cursor-row:num <- get *editor, cursor-row:offset
-    cursor-column:num <- get *editor, cursor-column:offset
-    screen <- move-cursor screen, cursor-row, cursor-column
-    e:event, found?:bool, quit?:bool, console <- read-event console
-    loop-unless found?
-    break-if quit?  # only in tests
-    trace 10, [app], [next-event]
-    # 'touch' event
-    t:touch-event, is-touch?:bool <- maybe-convert e, touch:variant
-    {
-      break-unless is-touch?
-      move-cursor editor, screen, t
-      loop +next-event
-    }
-    # keyboard events
-    {
-      break-if is-touch?
-      go-render?:bool <- handle-keyboard-event screen, editor, e
-      {
-        break-unless go-render?
-        screen <- editor-render screen, editor
-      }
-    }
-    loop
-  }
-]
-
-# process click, return if it was on current editor
-def move-cursor editor:&:editor, screen:&:screen, t:touch-event -> in-focus?:bool, editor:&:editor [
-  local-scope
-  load-inputs
-  return-unless editor, false
-  click-row:num <- get t, row:offset
-  return-unless click-row, false  # ignore clicks on 'menu'
-  click-column:num <- get t, column:offset
-  left:num <- get *editor, left:offset
-  too-far-left?:bool <- lesser-than click-column, left
-  return-if too-far-left?, false
-  right:num <- get *editor, right:offset
-  too-far-right?:bool <- greater-than click-column, right
-  return-if too-far-right?, false
-  # position cursor
-  <begin-move-cursor>
-  editor <- snap-cursor editor, screen, click-row, click-column
-  undo-coalesce-tag:num <- copy 0/never
-  <end-move-cursor>
-  # gain focus
-  return true
-]
-
-# Variant of 'render' that only moves the cursor (coordinates and
-# before-cursor). If it's past the end of a line, it 'slides' it left. If it's
-# past the last line it positions at end of last line.
-def snap-cursor editor:&:editor, screen:&:screen, target-row:num, target-column:num -> editor:&:editor [
-  local-scope
-  load-inputs
-  return-unless editor
-  left:num <- get *editor, left:offset
-  right:num <- get *editor, right:offset
-  screen-height:num <- screen-height screen
-  # count newlines until screen row
-  curr:&:duplex-list:char <- get *editor, top-of-screen:offset
-  prev:&:duplex-list:char <- copy curr  # just in case curr becomes null and we can't compute prev
-  curr <- next curr
-  row:num <- copy 1/top
-  column:num <- copy left
-  *editor <- put *editor, cursor-row:offset, target-row
-  cursor-row:num <- copy target-row
-  *editor <- put *editor, cursor-column:offset, target-column
-  cursor-column:num <- copy target-column
-  before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
-  {
-    +next-character
-    break-unless curr
-    off-screen?:bool <- greater-or-equal row, screen-height
-    break-if off-screen?
-    # update editor.before-cursor
-    # Doing so at the start of each iteration ensures it stays one step behind
-    # the current character.
-    {
-      at-cursor-row?:bool <- equal row, cursor-row
-      break-unless at-cursor-row?
-      at-cursor?:bool <- equal column, cursor-column
-      break-unless at-cursor?
-      before-cursor <- copy prev
-      *editor <- put *editor, before-cursor:offset, before-cursor
-    }
-    c:char <- get *curr, value:offset
-    {
-      # newline? move to left rather than 0
-      newline?:bool <- equal c, 10/newline
-      break-unless newline?
-      # adjust cursor if necessary
-      {
-        at-cursor-row?:bool <- equal row, cursor-row
-        break-unless at-cursor-row?
-        left-of-cursor?:bool <- lesser-than column, cursor-column
-        break-unless left-of-cursor?
-        cursor-column <- copy column
-        *editor <- put *editor, cursor-column:offset, cursor-column
-        before-cursor <- copy prev
-        *editor <- put *editor, before-cursor:offset, before-cursor
-      }
-      # skip to next line
-      row <- add row, 1
-      column <- copy left
-      curr <- next curr
-      prev <- next prev
-      loop +next-character
-    }
-    {
-      # at right? wrap. even if there's only one more letter left; we need
-      # room for clicking on the cursor after it.
-      at-right?:bool <- equal column, right
-      break-unless at-right?
-      column <- copy left
-      row <- add row, 1
-      # don't increment curr/prev
-      loop +next-character
-    }
-    curr <- next curr
-    prev <- next prev
-    column <- add column, 1
-    loop
-  }
-  # is cursor to the right of the last line? move to end
-  {
-    at-cursor-row?:bool <- equal row, cursor-row
-    cursor-outside-line?:bool <- lesser-or-equal column, cursor-column
-    before-cursor-on-same-line?:bool <- and at-cursor-row?, cursor-outside-line?
-    above-cursor-row?:bool <- lesser-than row, cursor-row
-    before-cursor?:bool <- or before-cursor-on-same-line?, above-cursor-row?
-    break-unless before-cursor?
-    cursor-row <- copy row
-    *editor <- put *editor, cursor-row:offset, cursor-row
-    cursor-column <- copy column
-    *editor <- put *editor, cursor-column:offset, cursor-column
-    before-cursor <- copy prev
-    *editor <- put *editor, before-cursor:offset, before-cursor
-  }
-]
-
-# Process an event 'e' and try to minimally update the screen.
-# Set 'go-render?' to true to indicate the caller must perform a non-minimal update.
-def handle-keyboard-event screen:&:screen, editor:&:editor, e:event -> go-render?:bool, screen:&:screen, editor:&:editor [
-  local-scope
-  load-inputs
-  return-unless editor, false/don't-render
-  screen-width:num <- screen-width screen
-  screen-height:num <- screen-height screen
-  left:num <- get *editor, left:offset
-  right:num <- get *editor, right:offset
-  before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
-  cursor-row:num <- get *editor, cursor-row:offset
-  cursor-column:num <- get *editor, cursor-column:offset
-  save-row:num <- copy cursor-row
-  save-column:num <- copy cursor-column
-  # character
-  {
-    c:char, is-unicode?:bool <- maybe-convert e, text:variant
-    break-unless is-unicode?
-    trace 10, [app], [handle-keyboard-event: special character]
-    # exceptions for special characters go here
-    <handle-special-character>
-    # ignore any other special characters
-    regular-character?:bool <- greater-or-equal c, 32/space
-    return-unless regular-character?, false/don't-render
-    # otherwise type it in
-    <begin-insert-character>
-    go-render? <- insert-at-cursor editor, c, screen
-    <end-insert-character>
-    return
-  }
-  # special key to modify the text or move the cursor
-  k:num, is-keycode?:bool <- maybe-convert e:event, keycode:variant
-  assert is-keycode?, [event was of unknown type; neither keyboard nor mouse]
-  # handlers for each special key will go here
-  <handle-special-key>
-  return true/go-render
-]
-
-def insert-at-cursor editor:&:editor, c:char, screen:&:screen -> go-render?:bool, editor:&:editor, screen:&:screen [
-  local-scope
-  load-inputs
-  before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
-  insert c, before-cursor
-  before-cursor <- next before-cursor
-  *editor <- put *editor, before-cursor:offset, before-cursor
-  cursor-row:num <- get *editor, cursor-row:offset
-  cursor-column:num <- get *editor, cursor-column:offset
-  left:num <- get *editor, left:offset
-  right:num <- get *editor, right:offset
-  save-row:num <- copy cursor-row
-  save-column:num <- copy cursor-column
-  screen-width:num <- screen-width screen
-  screen-height:num <- screen-height screen
-  # occasionally we'll need to mess with the cursor
-  <insert-character-special-case>
-  # but mostly we'll just move the cursor right
-  cursor-column <- add cursor-column, 1
-  *editor <- put *editor, cursor-column:offset, cursor-column
-  next:&:duplex-list:char <- next before-cursor
-  {
-    # at end of all text? no need to scroll? just print the character and leave
-    at-end?:bool <- equal next, null
-    break-unless at-end?
-    bottom:num <- subtract screen-height, 1
-    at-bottom?:bool <- equal save-row, bottom
-    at-right?:bool <- equal save-column, right
-    overflow?:bool <- and at-bottom?, at-right?
-    break-if overflow?
-    move-cursor screen, save-row, save-column
-    print screen, c
-    return false/don't-render
-  }
-  {
-    # not at right margin? print the character and rest of line
-    break-unless next
-    at-right?:bool <- greater-or-equal cursor-column, screen-width
-    break-if at-right?
-    curr:&:duplex-list:char <- copy before-cursor
-    move-cursor screen, save-row, save-column
-    curr-column:num <- copy save-column
-    {
-      # hit right margin? give up and let caller render
-      at-right?:bool <- greater-than curr-column, right
-      return-if at-right?, true/go-render
-      break-unless curr
-      # newline? done.
-      currc:char <- get *curr, value:offset
-      at-newline?:bool <- equal currc, 10/newline
-      break-if at-newline?
-      print screen, currc
-      curr-column <- add curr-column, 1
-      curr <- next curr
-      loop
-    }
-    return false/don't-render
-  }
-  return true/go-render
-]
-
-# helper for tests
-def editor-render screen:&:screen, editor:&:editor -> screen:&:screen, editor:&:editor [
-  local-scope
-  load-inputs
-  old-top-idx:num <- save-top-idx screen
-  left:num <- get *editor, left:offset
-  right:num <- get *editor, right:offset
-  row:num, column:num <- render screen, editor
-  draw-horizontal screen, row, left, right, 9480/horizontal-dotted
-  row <- add row, 1
-  clear-screen-from screen, row, left, left, right
-  assert-no-scroll screen, old-top-idx
-]
-
-scenario editor-handles-empty-event-queue [
-  local-scope
-  assume-screen 10/width, 5/height
-  e:&:editor <- new-editor [abc], 0/left, 10/right
-  editor-render screen, e
-  assume-console []
-  run [
-    editor-event-loop screen, console, e
-  ]
-  screen-should-contain [
-    .          .
-    .abc       .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-]
-
-scenario editor-handles-mouse-clicks [
-  local-scope
-  assume-screen 10/width, 5/height
-  e:&:editor <- new-editor [abc], 0/left, 10/right
-  editor-render screen, e
-  $clear-trace
-  assume-console [
-    left-click 1, 1  # on the 'b'
-  ]
-  run [
-    editor-event-loop screen, console, e
-    3:num/raw <- get *e, cursor-row:offset
-    4:num/raw <- get *e, cursor-column:offset
-  ]
-  screen-should-contain [
-    .          .
-    .abc       .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-  memory-should-contain [
-    3 <- 1  # cursor is at row 0..
-    4 <- 1  # ..and column 1
-  ]
-  check-trace-count-for-label 0, [print-character]
-]
-
-scenario editor-handles-mouse-clicks-outside-text [
-  local-scope
-  assume-screen 10/width, 5/height
-  e:&:editor <- new-editor [abc], 0/left, 10/right
-  $clear-trace
-  assume-console [
-    left-click 1, 7  # last line, to the right of text
-  ]
-  run [
-    editor-event-loop screen, console, e
-    3:num/raw <- get *e, cursor-row:offset
-    4:num/raw <- get *e, cursor-column:offset
-  ]
-  memory-should-contain [
-    3 <- 1  # cursor row
-    4 <- 3  # cursor column
-  ]
-  check-trace-count-for-label 0, [print-character]
-]
-
-scenario editor-handles-mouse-clicks-outside-text-2 [
-  local-scope
-  assume-screen 10/width, 5/height
-  s:text <- new [abc
-def]
-  e:&:editor <- new-editor s, 0/left, 10/right
-  $clear-trace
-  assume-console [
-    left-click 1, 7  # interior line, to the right of text
-  ]
-  run [
-    editor-event-loop screen, console, e
-    3:num/raw <- get *e, cursor-row:offset
-    4:num/raw <- get *e, cursor-column:offset
-  ]
-  memory-should-contain [
-    3 <- 1  # cursor row
-    4 <- 3  # cursor column
-  ]
-  check-trace-count-for-label 0, [print-character]
-]
-
-scenario editor-handles-mouse-clicks-outside-text-3 [
-  local-scope
-  assume-screen 10/width, 5/height
-  s:text <- new [abc
-def]
-  e:&:editor <- new-editor s, 0/left, 10/right
-  $clear-trace
-  assume-console [
-    left-click 3, 7  # below text
-  ]
-  run [
-    editor-event-loop screen, console, e
-    3:num/raw <- get *e, cursor-row:offset
-    4:num/raw <- get *e, cursor-column:offset
-  ]
-  memory-should-contain [
-    3 <- 2  # cursor row
-    4 <- 3  # cursor column
-  ]
-  check-trace-count-for-label 0, [print-character]
-]
-
-scenario editor-handles-mouse-clicks-outside-column [
-  local-scope
-  assume-screen 10/width, 5/height
-  # editor occupies only left half of screen
-  e:&:editor <- new-editor [abc], 0/left, 5/right
-  editor-render screen, e
-  $clear-trace
-  assume-console [
-    # click on right half of screen
-    left-click 3, 8
-  ]
-  run [
-    editor-event-loop screen, console, e
-    3:num/raw <- get *e, cursor-row:offset
-    4:num/raw <- get *e, cursor-column:offset
-  ]
-  screen-should-contain [
-    .          .
-    .abc       .
-    .┈┈┈┈┈     .
-    .          .
-  ]
-  memory-should-contain [
-    3 <- 1  # no change to cursor row
-    4 <- 0  # ..or column
-  ]
-  check-trace-count-for-label 0, [print-character]
-]
-
-scenario editor-handles-mouse-clicks-in-menu-area [
-  local-scope
-  assume-screen 10/width, 5/height
-  e:&:editor <- new-editor [abc], 0/left, 5/right
-  editor-render screen, e
-  $clear-trace
-  assume-console [
-    # click on first, 'menu' row
-    left-click 0, 3
-  ]
-  run [
-    editor-event-loop screen, console, e
-    3:num/raw <- get *e, cursor-row:offset
-    4:num/raw <- get *e, cursor-column:offset
-  ]
-  # no change to cursor
-  memory-should-contain [
-    3 <- 1
-    4 <- 0
-  ]
-]
-
-scenario editor-inserts-characters-into-empty-editor [
-  local-scope
-  assume-screen 10/width, 5/height
-  e:&:editor <- new-editor [], 0/left, 5/right
-  editor-render screen, e
-  $clear-trace
-  assume-console [
-    type [abc]
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  screen-should-contain [
-    .          .
-    .abc       .
-    .┈┈┈┈┈     .
-    .          .
-  ]
-  check-trace-count-for-label 3, [print-character]
-]
-
-scenario editor-inserts-characters-at-cursor [
-  local-scope
-  assume-screen 10/width, 5/height
-  e:&:editor <- new-editor [abc], 0/left, 10/right
-  editor-render screen, e
-  $clear-trace
-  # type two letters at different places
-  assume-console [
-    type [0]
-    left-click 1, 2
-    type [d]
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  screen-should-contain [
-    .          .
-    .0adbc     .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-  check-trace-count-for-label 7, [print-character]  # 4 for first letter, 3 for second
-]
-
-scenario editor-inserts-characters-at-cursor-2 [
-  local-scope
-  assume-screen 10/width, 5/height
-  e:&:editor <- new-editor [abc], 0/left, 10/right
-  editor-render screen, e
-  $clear-trace
-  assume-console [
-    left-click 1, 5  # right of last line
-    type [d]
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  screen-should-contain [
-    .          .
-    .abcd      .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-  check-trace-count-for-label 1, [print-character]
-]
-
-scenario editor-inserts-characters-at-cursor-5 [
-  local-scope
-  assume-screen 10/width, 5/height
-  s:text <- new [abc
-d]
-  e:&:editor <- new-editor s, 0/left, 10/right
-  editor-render screen, e
-  $clear-trace
-  assume-console [
-    left-click 1, 5  # right of non-last line
-    type [e]
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  screen-should-contain [
-    .          .
-    .abce      .
-    .d         .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-  check-trace-count-for-label 1, [print-character]
-]
-
-scenario editor-inserts-characters-at-cursor-3 [
-  local-scope
-  assume-screen 10/width, 5/height
-  e:&:editor <- new-editor [abc], 0/left, 10/right
-  editor-render screen, e
-  $clear-trace
-  assume-console [
-    left-click 3, 5  # below all text
-    type [d]
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  screen-should-contain [
-    .          .
-    .abcd      .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-  check-trace-count-for-label 1, [print-character]
-]
-
-scenario editor-inserts-characters-at-cursor-4 [
-  local-scope
-  assume-screen 10/width, 5/height
-  s:text <- new [abc
-d]
-  e:&:editor <- new-editor s, 0/left, 10/right
-  editor-render screen, e
-  $clear-trace
-  assume-console [
-    left-click 3, 5  # below all text
-    type [e]
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  screen-should-contain [
-    .          .
-    .abc       .
-    .de        .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-  check-trace-count-for-label 1, [print-character]
-]
-
-scenario editor-inserts-characters-at-cursor-6 [
-  local-scope
-  assume-screen 10/width, 5/height
-  s:text <- new [abc
-d]
-  e:&:editor <- new-editor s, 0/left, 10/right
-  editor-render screen, e
-  $clear-trace
-  assume-console [
-    left-click 3, 5  # below all text
-    type [ef]
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  screen-should-contain [
-    .          .
-    .abc       .
-    .def       .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-  check-trace-count-for-label 2, [print-character]
-]
-
-scenario editor-moves-cursor-after-inserting-characters [
-  local-scope
-  assume-screen 10/width, 5/height
-  e:&:editor <- new-editor [ab], 0/left, 5/right
-  editor-render screen, e
-  assume-console [
-    type [01]
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  screen-should-contain [
-    .          .
-    .01ab      .
-    .┈┈┈┈┈     .
-    .          .
-  ]
-]
-
-# if the cursor reaches the right margin, wrap the line
-
-scenario editor-wraps-line-on-insert [
-  local-scope
-  assume-screen 5/width, 5/height
-  e:&:editor <- new-editor [abc], 0/left, 5/right
-  editor-render screen, e
-  # type a letter
-  assume-console [
-    type [e]
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  # no wrap yet
-  screen-should-contain [
-    .     .
-    .eabc .
-    .┈┈┈┈┈.
-    .     .
-    .     .
-  ]
-  # type a second letter
-  assume-console [
-    type [f]
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  # now wrap
-  screen-should-contain [
-    .     .
-    .efab↩.
-    .c    .
-    .┈┈┈┈┈.
-    .     .
-  ]
-]
-
-scenario editor-wraps-line-on-insert-2 [
-  local-scope
-  # create an editor with some text
-  assume-screen 10/width, 5/height
-  s:text <- new [abcdefg
-defg]
-  e:&:editor <- new-editor s, 0/left, 5/right
-  editor-render screen, e
-  # type more text at the start
-  assume-console [
-    left-click 3, 0
-    type [abc]
-  ]
-  run [
-    editor-event-loop screen, console, e
-    3:num/raw <- get *e, cursor-row:offset
-    4:num/raw <- get *e, cursor-column:offset
-  ]
-  # cursor is not wrapped
-  memory-should-contain [
-    3 <- 3
-    4 <- 3
-  ]
-  # but line is wrapped
-  screen-should-contain [
-    .          .
-    .abcd↩     .
-    .efg       .
-    .abcd↩     .
-    .efg       .
-  ]
-]
-
-after <insert-character-special-case> [
-  # if the line wraps at the cursor, move cursor to start of next row
-  {
-    # if either:
-    # a) we're at the end of the line and at the column of the wrap indicator, or
-    # b) we're not at end of line and just before the column of the wrap indicator
-    wrap-column:num <- copy right
-    before-wrap-column:num <- subtract wrap-column, 1
-    at-wrap?:bool <- greater-or-equal cursor-column, wrap-column
-    just-before-wrap?:bool <- greater-or-equal cursor-column, before-wrap-column
-    next:&:duplex-list:char <- next before-cursor
-    # at end of line? next == 0 || next.value == 10/newline
-    at-end-of-line?:bool <- equal next, null
-    {
-      break-if at-end-of-line?
-      next-character:char <- get *next, value:offset
-      at-end-of-line? <- equal next-character, 10/newline
-    }
-    # break unless ((eol? and at-wrap?) or (~eol? and just-before-wrap?))
-    move-cursor-to-next-line?:bool <- copy false
-    {
-      break-if at-end-of-line?
-      move-cursor-to-next-line? <- copy just-before-wrap?
-      # if we're moving the cursor because it's in the middle of a wrapping
-      # line, adjust it to left-most column
-      potential-new-cursor-column:num <- copy left
-    }
-    {
-      break-unless at-end-of-line?
-      move-cursor-to-next-line? <- copy at-wrap?
-      # if we're moving the cursor because it's at the end of a wrapping line,
-      # adjust it to one past the left-most column to make room for the
-      # newly-inserted wrap-indicator
-      potential-new-cursor-column:num <- add left, 1/make-room-for-wrap-indicator
-    }
-    break-unless move-cursor-to-next-line?
-    cursor-column <- copy potential-new-cursor-column
-    *editor <- put *editor, cursor-column:offset, cursor-column
-    cursor-row <- add cursor-row, 1
-    *editor <- put *editor, cursor-row:offset, cursor-row
-    # if we're out of the screen, scroll down
-    {
-      below-screen?:bool <- greater-or-equal cursor-row, screen-height
-      break-unless below-screen?
-      <scroll-down>
-    }
-    return true/go-render
-  }
-]
-
-scenario editor-wraps-cursor-after-inserting-characters-in-middle-of-line [
-  local-scope
-  assume-screen 10/width, 5/height
-  e:&:editor <- new-editor [abcde], 0/left, 5/right
-  assume-console [
-    left-click 1, 3  # right before the wrap icon
-    type [f]
-  ]
-  run [
-    editor-event-loop screen, console, e
-    3:num/raw <- get *e, cursor-row:offset
-    4:num/raw <- get *e, cursor-column:offset
-  ]
-  screen-should-contain [
-    .          .
-    .abcf↩     .
-    .de        .
-    .┈┈┈┈┈     .
-    .          .
-  ]
-  memory-should-contain [
-    3 <- 2  # cursor row
-    4 <- 0  # cursor column
-  ]
-]
-
-scenario editor-wraps-cursor-after-inserting-characters-at-end-of-line [
-  local-scope
-  assume-screen 10/width, 5/height
-  # create an editor containing two lines
-  s:text <- new [abc
-xyz]
-  e:&:editor <- new-editor s, 0/left, 5/right
-  editor-render screen, e
-  screen-should-contain [
-    .          .
-    .abc       .
-    .xyz       .
-    .┈┈┈┈┈     .
-    .          .
-  ]
-  assume-console [
-    left-click 1, 4  # at end of first line
-    type [de]  # trigger wrap
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  screen-should-contain [
-    .          .
-    .abcd↩     .
-    .e         .
-    .xyz       .
-    .┈┈┈┈┈     .
-  ]
-]
-
-scenario editor-wraps-cursor-to-left-margin [
-  local-scope
-  assume-screen 10/width, 5/height
-  e:&:editor <- new-editor [abcde], 2/left, 7/right
-  assume-console [
-    left-click 1, 5  # line is full; no wrap icon yet
-    type [01]
-  ]
-  run [
-    editor-event-loop screen, console, e
-    3:num/raw <- get *e, cursor-row:offset
-    4:num/raw <- get *e, cursor-column:offset
-  ]
-  screen-should-contain [
-    .          .
-    .  abc0↩   .
-    .  1de     .
-    .  ┈┈┈┈┈   .
-    .          .
-  ]
-  memory-should-contain [
-    3 <- 2  # cursor row
-    4 <- 3  # cursor column
-  ]
-]
-
-# if newline, move cursor to start of next line, and maybe align indent with previous line
-
-container editor [
-  indent?:bool
-]
-
-after <editor-initialization> [
-  *result <- put *result, indent?:offset, true
-]
-
-scenario editor-moves-cursor-down-after-inserting-newline [
-  local-scope
-  assume-screen 10/width, 5/height
-  e:&:editor <- new-editor [abc], 0/left, 10/right
-  assume-console [
-    type [0
-1]
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  screen-should-contain [
-    .          .
-    .0         .
-    .1abc      .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-]
-
-after <handle-special-character> [
-  {
-    newline?:bool <- equal c, 10/newline
-    break-unless newline?
-    <begin-insert-enter>
-    insert-new-line-and-indent editor, screen
-    <end-insert-enter>
-    return true/go-render
-  }
-]
-
-def insert-new-line-and-indent editor:&:editor, screen:&:screen -> editor:&:editor, screen:&:screen [
-  local-scope
-  load-inputs
-  cursor-row:num <- get *editor, cursor-row:offset
-  cursor-column:num <- get *editor, cursor-column:offset
-  before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
-  left:num <- get *editor, left:offset
-  right:num <- get *editor, right:offset
-  screen-height:num <- screen-height screen
-  # update cursor coordinates
-  at-start-of-wrapped-line?:bool <- at-start-of-wrapped-line? editor
-  {
-    break-if at-start-of-wrapped-line?
-    cursor-row <- add cursor-row, 1
-    *editor <- put *editor, cursor-row:offset, cursor-row
-  }
-  cursor-column <- copy left
-  *editor <- put *editor, cursor-column:offset, cursor-column
-  # maybe scroll
-  {
-    below-screen?:bool <- greater-or-equal cursor-row, screen-height  # must be equal, never greater
-    break-unless below-screen?
-    <scroll-down2>
-    cursor-row <- subtract cursor-row, 1  # bring back into screen range
-    *editor <- put *editor, cursor-row:offset, cursor-row
-  }
-  # insert newline
-  insert 10/newline, before-cursor
-  before-cursor <- next before-cursor
-  *editor <- put *editor, before-cursor:offset, before-cursor
-  # indent if necessary
-  indent?:bool <- get *editor, indent?:offset
-  return-unless indent?
-  d:&:duplex-list:char <- get *editor, data:offset
-  end-of-previous-line:&:duplex-list:char <- prev before-cursor
-  indent:num <- line-indent end-of-previous-line, d
-  i:num <- copy 0
-  {
-    indent-done?:bool <- greater-or-equal i, indent
-    break-if indent-done?
-    insert-at-cursor editor, 32/space, screen
-    i <- add i, 1
-    loop
-  }
-]
-
-def at-start-of-wrapped-line? editor:&:editor -> result:bool [
-  local-scope
-  load-inputs
-  left:num <- get *editor, left:offset
-  cursor-column:num <- get *editor, cursor-column:offset
-  cursor-at-left?:bool <- equal cursor-column, left
-  return-unless cursor-at-left?, false
-  before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
-  before-before-cursor:&:duplex-list:char <- prev before-cursor
-  return-unless before-before-cursor, false  # cursor is at start of editor
-  char-before-cursor:char <- get *before-cursor, value:offset
-  cursor-after-newline?:bool <- equal char-before-cursor, 10/newline
-  return-if cursor-after-newline?, false
-  # if cursor is at left margin and not at start, but previous character is not a newline,
-  # then we're at start of a wrapped line
-  return true
-]
-
-# takes a pointer 'curr' into the doubly-linked list and its sentinel, counts
-# the number of spaces at the start of the line containing 'curr'.
-def line-indent curr:&:duplex-list:char, start:&:duplex-list:char -> result:num [
-  local-scope
-  load-inputs
-  result:num <- copy 0
-  return-unless curr
-  at-start?:bool <- equal curr, start
-  return-if at-start?
-  {
-    curr <- prev curr
-    break-unless curr
-    at-start?:bool <- equal curr, start
-    break-if at-start?
-    c:char <- get *curr, value:offset
-    at-newline?:bool <- equal c, 10/newline
-    break-if at-newline?
-    # if c is a space, increment result
-    is-space?:bool <- equal c, 32/space
-    {
-      break-unless is-space?
-      result <- add result, 1
-    }
-    # if c is not a space, reset result
-    {
-      break-if is-space?
-      result <- copy 0
-    }
-    loop
-  }
-]
-
-scenario editor-moves-cursor-down-after-inserting-newline-2 [
-  local-scope
-  assume-screen 10/width, 5/height
-  e:&:editor <- new-editor [abc], 1/left, 10/right
-  assume-console [
-    type [0
-1]
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  screen-should-contain [
-    .          .
-    . 0        .
-    . 1abc     .
-    . ┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-]
-
-scenario editor-clears-previous-line-completely-after-inserting-newline [
-  local-scope
-  assume-screen 10/width, 5/height
-  e:&:editor <- new-editor [abcde], 0/left, 5/right
-  editor-render screen, e
-  screen-should-contain [
-    .          .
-    .abcd↩     .
-    .e         .
-    .┈┈┈┈┈     .
-    .          .
-  ]
-  assume-console [
-    press enter
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  # line should be fully cleared
-  screen-should-contain [
-    .          .
-    .          .
-    .abcd↩     .
-    .e         .
-    .┈┈┈┈┈     .
-  ]
-]
-
-scenario editor-splits-wrapped-line-after-inserting-newline [
-  local-scope
-  assume-screen 10/width, 5/height
-  e:&:editor <- new-editor [abcdef], 0/left, 5/right
-  editor-render screen, e
-  screen-should-contain [
-    .          .
-    .abcd↩     .
-    .ef        .
-    .┈┈┈┈┈     .
-    .          .
-  ]
-  assume-console [
-    left-click 2, 0
-    press enter
-  ]
-  run [
-    editor-event-loop screen, console, e
-    10:num/raw <- get *e, cursor-row:offset
-    11:num/raw <- get *e, cursor-column:offset
-  ]
-  screen-should-contain [
-    .          .
-    .abcd      .
-    .ef        .
-    .┈┈┈┈┈     .
-  ]
-  memory-should-contain [
-    10 <- 2  # cursor-row
-    11 <- 0  # cursor-column
-  ]
-]
-
-scenario editor-inserts-indent-after-newline [
-  local-scope
-  assume-screen 10/width, 10/height
-  s:text <- new [ab
-  cd
-ef]
-  e:&:editor <- new-editor s, 0/left, 10/right
-  # position cursor after 'cd' and hit 'newline'
-  assume-console [
-    left-click 2, 8
-    type [
-]
-  ]
-  run [
-    editor-event-loop screen, console, e
-    3:num/raw <- get *e, cursor-row:offset
-    4:num/raw <- get *e, cursor-column:offset
-  ]
-  # cursor should be below start of previous line
-  memory-should-contain [
-    3 <- 3  # cursor row
-    4 <- 2  # cursor column (indented)
-  ]
-]
-
-scenario editor-skips-indent-around-paste [
-  local-scope
-  assume-screen 10/width, 10/height
-  s:text <- new [ab
-  cd
-ef]
-  e:&:editor <- new-editor s, 0/left, 10/right
-  # position cursor after 'cd' and hit 'newline' surrounded by paste markers
-  assume-console [
-    left-click 2, 8
-    press 65507  # start paste
-    press enter
-    press 65506  # end paste
-  ]
-  run [
-    editor-event-loop screen, console, e
-    3:num/raw <- get *e, cursor-row:offset
-    4:num/raw <- get *e, cursor-column:offset
-  ]
-  # cursor should be below start of previous line
-  memory-should-contain [
-    3 <- 3  # cursor row
-    4 <- 0  # cursor column (not indented)
-  ]
-]
-
-after <handle-special-key> [
-  {
-    paste-start?:bool <- equal k, 65507/paste-start
-    break-unless paste-start?
-    *editor <- put *editor, indent?:offset, false
-    return true/go-render
-  }
-]
-
-after <handle-special-key> [
-  {
-    paste-end?:bool <- equal k, 65506/paste-end
-    break-unless paste-end?
-    *editor <- put *editor, indent?:offset, true
-    return true/go-render
-  }
-]
-
-## helpers
-
-def draw-horizontal screen:&:screen, row:num, x:num, right:num -> screen:&:screen [
-  local-scope
-  load-inputs
-  height:num <- screen-height screen
-  past-bottom?:bool <- greater-or-equal row, height
-  return-if past-bottom?
-  style:char, style-found?:bool <- next-input
-  {
-    break-if style-found?
-    style <- copy 9472/horizontal
-  }
-  color:num, color-found?:bool <- next-input
-  {
-    # default color to white
-    break-if color-found?
-    color <- copy 245/grey
-  }
-  bg-color:num, bg-color-found?:bool <- next-input
-  {
-    break-if bg-color-found?
-    bg-color <- copy 0/black
-  }
-  screen <- move-cursor screen, row, x
-  {
-    continue?:bool <- lesser-or-equal x, right  # right is inclusive, to match editor semantics
-    break-unless continue?
-    print screen, style, color, bg-color
-    x <- add x, 1
-    loop
-  }
-]
diff --git a/sandbox/003-shortcuts.mu b/sandbox/003-shortcuts.mu
deleted file mode 100644
index c9e66d5b..00000000
--- a/sandbox/003-shortcuts.mu
+++ /dev/null
@@ -1,2800 +0,0 @@
-## special shortcuts for manipulating the editor
-# Some keys on the keyboard generate unicode characters, others generate
-# terminfo key codes. We need to modify different places in the two cases.
-
-# tab - insert two spaces
-
-scenario editor-inserts-two-spaces-on-tab [
-  local-scope
-  assume-screen 10/width, 5/height
-  s:text <- new [ab
-cd]
-  e:&:editor <- new-editor s, 0/left, 5/right
-  editor-render screen, e
-  $clear-trace
-  assume-console [
-    press tab
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  screen-should-contain [
-    .          .
-    .  ab      .
-    .cd        .
-  ]
-  # we render at most two editor rows worth (one row for each space)
-  check-trace-count-for-label-lesser-than 10, [print-character]
-]
-
-scenario editor-inserts-two-spaces-and-wraps-line-on-tab [
-  local-scope
-  assume-screen 10/width, 5/height
-  e:&:editor <- new-editor [abcd], 0/left, 5/right
-  editor-render screen, e
-  $clear-trace
-  assume-console [
-    press tab
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  screen-should-contain [
-    .          .
-    .  ab↩     .
-    .cd        .
-  ]
-  # we re-render the whole editor
-  check-trace-count-for-label-greater-than 10, [print-character]
-]
-
-after <handle-special-character> [
-  {
-    tab?:bool <- equal c, 9/tab
-    break-unless tab?
-    <begin-insert-character>
-    # todo: decompose insert-at-cursor into editor update and screen update,
-    # so that 'tab' doesn't render the current line multiple times
-    insert-at-cursor editor, 32/space, screen
-    go-render? <- insert-at-cursor editor, 32/space, screen
-    <end-insert-character>
-    return
-  }
-]
-
-# backspace - delete character before cursor
-
-scenario editor-handles-backspace-key [
-  local-scope
-  assume-screen 10/width, 5/height
-  e:&:editor <- new-editor [abc], 0/left, 10/right
-  editor-render screen, e
-  $clear-trace
-  assume-console [
-    left-click 1, 1
-    press backspace
-  ]
-  run [
-    editor-event-loop screen, console, e
-    4:num/raw <- get *e, cursor-row:offset
-    5:num/raw <- get *e, cursor-column:offset
-  ]
-  screen-should-contain [
-    .          .
-    .bc        .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-  memory-should-contain [
-    4 <- 1
-    5 <- 0
-  ]
-  check-trace-count-for-label 3, [print-character]  # length of original line to overwrite
-]
-
-after <handle-special-character> [
-  {
-    delete-previous-character?:bool <- equal c, 8/backspace
-    break-unless delete-previous-character?
-    <begin-backspace-character>
-    go-render?:bool, backspaced-cell:&:duplex-list:char <- delete-before-cursor editor, screen
-    <end-backspace-character>
-    return
-  }
-]
-
-# return values:
-#   go-render? - whether caller needs to update the screen
-#   backspaced-cell - value deleted (or 0 if nothing was deleted) so we can save it for undo, etc.
-def delete-before-cursor editor:&:editor, screen:&:screen -> go-render?:bool, backspaced-cell:&:duplex-list:char, editor:&:editor, screen:&:screen [
-  local-scope
-  load-inputs
-  before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
-  data:&:duplex-list:char <- get *editor, data:offset
-  # if at start of text (before-cursor at § sentinel), return
-  prev:&:duplex-list:char <- prev before-cursor
-  return-unless prev, false/no-more-render, null/nothing-deleted
-  trace 10, [app], [delete-before-cursor]
-  original-row:num <- get *editor, cursor-row:offset
-  move-cursor-coordinates-left editor
-  backspaced-cell:&:duplex-list:char <- copy before-cursor
-  data <- remove before-cursor, data  # will also neatly trim next/prev pointers in backspaced-cell/before-cursor
-  before-cursor <- copy prev
-  *editor <- put *editor, before-cursor:offset, before-cursor
-  screen-width:num <- screen-width screen
-  cursor-row:num <- get *editor, cursor-row:offset
-  cursor-column:num <- get *editor, cursor-column:offset
-  # did we just backspace over a newline?
-  same-row?:bool <- equal cursor-row, original-row
-  return-unless same-row?, true/go-render
-  left:num <- get *editor, left:offset
-  right:num <- get *editor, right:offset
-  curr:&:duplex-list:char <- next before-cursor
-  screen <- move-cursor screen, cursor-row, cursor-column
-  curr-column:num <- copy cursor-column
-  {
-    # hit right margin? give up and let caller render
-    at-right?:bool <- greater-or-equal curr-column, right
-    return-if at-right?, true/go-render
-    break-unless curr
-    # newline? done.
-    currc:char <- get *curr, value:offset
-    at-newline?:bool <- equal currc, 10/newline
-    break-if at-newline?
-    screen <- print screen, currc
-    curr-column <- add curr-column, 1
-    curr <- next curr
-    loop
-  }
-  # we're guaranteed not to be at the right margin
-  space:char <- copy 32/space
-  screen <- print screen, space
-  go-render? <- copy false
-]
-
-def move-cursor-coordinates-left editor:&:editor -> editor:&:editor [
-  local-scope
-  load-inputs
-  before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
-  cursor-row:num <- get *editor, cursor-row:offset
-  cursor-column:num <- get *editor, cursor-column:offset
-  left:num <- get *editor, left:offset
-  # if not at left margin, move one character left
-  {
-    at-left-margin?:bool <- equal cursor-column, left
-    break-if at-left-margin?
-    trace 10, [app], [decrementing cursor column]
-    cursor-column <- subtract cursor-column, 1
-    *editor <- put *editor, cursor-column:offset, cursor-column
-    return
-  }
-  # if at left margin, we must move to previous row:
-  top-of-screen?:bool <- equal cursor-row, 1  # exclude menu bar
-  {
-    break-if top-of-screen?
-    cursor-row <- subtract cursor-row, 1
-    *editor <- put *editor, cursor-row:offset, cursor-row
-  }
-  {
-    break-unless top-of-screen?
-    # no scroll, so do nothing
-  }
-  {
-    # case 1: if previous character was newline, figure out how long the previous line is
-    previous-character:char <- get *before-cursor, value:offset
-    previous-character-is-newline?:bool <- equal previous-character, 10/newline
-    break-unless previous-character-is-newline?
-    # compute length of previous line
-    trace 10, [app], [switching to previous line]
-    d:&:duplex-list:char <- get *editor, data:offset
-    end-of-line:num <- previous-line-length before-cursor, d
-    right:num <- get *editor, right:offset
-    width:num <- subtract right, left
-    wrap?:bool <- greater-than end-of-line, width
-    {
-      break-unless wrap?
-      _, column-offset:num <- divide-with-remainder end-of-line, width
-      cursor-column <- add left, column-offset
-      *editor <- put *editor, cursor-column:offset, cursor-column
-    }
-    {
-      break-if wrap?
-      cursor-column <- add left, end-of-line
-      *editor <- put *editor, cursor-column:offset, cursor-column
-    }
-    return
-  }
-  # case 2: if previous-character was not newline, we're just at a wrapped line
-  trace 10, [app], [wrapping to previous line]
-  right:num <- get *editor, right:offset
-  cursor-column <- subtract right, 1  # leave room for wrap icon
-  *editor <- put *editor, cursor-column:offset, cursor-column
-]
-
-# takes a pointer 'curr' into the doubly-linked list and its sentinel, counts
-# the length of the previous line before the 'curr' pointer.
-def previous-line-length curr:&:duplex-list:char, start:&:duplex-list:char -> result:num [
-  local-scope
-  load-inputs
-  result:num <- copy 0
-  return-unless curr
-  at-start?:bool <- equal curr, start
-  return-if at-start?
-  {
-    curr <- prev curr
-    break-unless curr
-    at-start?:bool <- equal curr, start
-    break-if at-start?
-    c:char <- get *curr, value:offset
-    at-newline?:bool <- equal c, 10/newline
-    break-if at-newline?
-    result <- add result, 1
-    loop
-  }
-]
-
-scenario editor-clears-last-line-on-backspace [
-  local-scope
-  assume-screen 10/width, 5/height
-  s:text <- new [ab
-cd]
-  e:&:editor <- new-editor s, 0/left, 10/right
-  assume-console [
-    left-click 2, 0
-    press backspace
-  ]
-  run [
-    editor-event-loop screen, console, e
-    4:num/raw <- get *e, cursor-row:offset
-    5:num/raw <- get *e, cursor-column:offset
-  ]
-  screen-should-contain [
-    .          .
-    .abcd      .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-  memory-should-contain [
-    4 <- 1
-    5 <- 2
-  ]
-]
-
-scenario editor-joins-and-wraps-lines-on-backspace [
-  local-scope
-  assume-screen 10/width, 5/height
-  # initialize editor with two long-ish but non-wrapping lines
-  s:text <- new [abc def
-ghi jkl]
-  e:&:editor <- new-editor s, 0/left, 10/right
-  editor-render screen, e
-  $clear-trace
-  # position the cursor at the start of the second and hit backspace
-  assume-console [
-    left-click 2, 0
-    press backspace
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  # resulting single line should wrap correctly
-  screen-should-contain [
-    .          .
-    .abc defgh↩.
-    .i jkl     .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-]
-
-scenario editor-wraps-long-lines-on-backspace [
-  local-scope
-  assume-screen 10/width, 5/height
-  # initialize editor in part of the screen with a long line
-  e:&:editor <- new-editor [abc def ghij], 0/left, 8/right
-  editor-render screen, e
-  # confirm that it wraps
-  screen-should-contain [
-    .          .
-    .abc def↩  .
-    . ghij     .
-    .┈┈┈┈┈┈┈┈  .
-  ]
-  $clear-trace
-  # position the cursor somewhere in the middle of the top screen line and hit backspace
-  assume-console [
-    left-click 1, 4
-    press backspace
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  # resulting single line should wrap correctly and not overflow its bounds
-  screen-should-contain [
-    .          .
-    .abcdef ↩  .
-    .ghij      .
-    .┈┈┈┈┈┈┈┈  .
-    .          .
-  ]
-]
-
-# delete - delete character at cursor
-
-scenario editor-handles-delete-key [
-  local-scope
-  assume-screen 10/width, 5/height
-  e:&:editor <- new-editor [abc], 0/left, 10/right
-  editor-render screen, e
-  $clear-trace
-  assume-console [
-    press delete
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  screen-should-contain [
-    .          .
-    .bc        .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-  check-trace-count-for-label 3, [print-character]  # length of original line to overwrite
-  $clear-trace
-  assume-console [
-    press delete
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  screen-should-contain [
-    .          .
-    .c         .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-  check-trace-count-for-label 2, [print-character]  # new length to overwrite
-]
-
-after <handle-special-key> [
-  {
-    delete-next-character?:bool <- equal k, 65522/delete
-    break-unless delete-next-character?
-    <begin-delete-character>
-    go-render?:bool, deleted-cell:&:duplex-list:char <- delete-at-cursor editor, screen
-    <end-delete-character>
-    return
-  }
-]
-
-def delete-at-cursor editor:&:editor, screen:&:screen -> go-render?:bool, deleted-cell:&:duplex-list:char, editor:&:editor, screen:&:screen [
-  local-scope
-  load-inputs
-  before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
-  data:&:duplex-list:char <- get *editor, data:offset
-  deleted-cell:&:duplex-list:char <- next before-cursor
-  return-unless deleted-cell, false/don't-render
-  currc:char <- get *deleted-cell, value:offset
-  data <- remove deleted-cell, data
-  deleted-newline?:bool <- equal currc, 10/newline
-  return-if deleted-newline?, true/go-render
-  # wasn't a newline? render rest of line
-  curr:&:duplex-list:char <- next before-cursor  # refresh after remove above
-  cursor-row:num <- get *editor, cursor-row:offset
-  cursor-column:num <- get *editor, cursor-column:offset
-  screen <- move-cursor screen, cursor-row, cursor-column
-  curr-column:num <- copy cursor-column
-  screen-width:num <- screen-width screen
-  {
-    # hit right margin? give up and let caller render
-    at-right?:bool <- greater-or-equal curr-column, screen-width
-    return-if at-right?, true/go-render
-    break-unless curr
-    currc:char <- get *curr, value:offset
-    at-newline?:bool <- equal currc, 10/newline
-    break-if at-newline?
-    screen <- print screen, currc
-    curr-column <- add curr-column, 1
-    curr <- next curr
-    loop
-  }
-  # we're guaranteed not to be at the right margin
-  space:char <- copy 32/space
-  screen <- print screen, space
-  go-render? <- copy false
-]
-
-# right arrow
-
-scenario editor-moves-cursor-right-with-key [
-  local-scope
-  assume-screen 10/width, 5/height
-  e:&:editor <- new-editor [abc], 0/left, 10/right
-  editor-render screen, e
-  $clear-trace
-  assume-console [
-    press right-arrow
-    type [0]
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  screen-should-contain [
-    .          .
-    .a0bc      .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-  check-trace-count-for-label 3, [print-character]  # 0 and following characters
-]
-
-after <handle-special-key> [
-  {
-    move-to-next-character?:bool <- equal k, 65514/right-arrow
-    break-unless move-to-next-character?
-    # if not at end of text
-    next-cursor:&:duplex-list:char <- next before-cursor
-    break-unless next-cursor
-    # scan to next character
-    <begin-move-cursor>
-    before-cursor <- copy next-cursor
-    *editor <- put *editor, before-cursor:offset, before-cursor
-    go-render?:bool <- move-cursor-coordinates-right editor, screen-height
-    screen <- move-cursor screen, cursor-row, cursor-column
-    undo-coalesce-tag:num <- copy 2/right-arrow
-    <end-move-cursor>
-    return
-  }
-]
-
-def move-cursor-coordinates-right editor:&:editor, screen-height:num -> go-render?:bool, editor:&:editor [
-  local-scope
-  load-inputs
-  before-cursor:&:duplex-list:char <- get *editor before-cursor:offset
-  cursor-row:num <- get *editor, cursor-row:offset
-  cursor-column:num <- get *editor, cursor-column:offset
-  left:num <- get *editor, left:offset
-  right:num <- get *editor, right:offset
-  # if crossed a newline, move cursor to start of next row
-  {
-    old-cursor-character:char <- get *before-cursor, value:offset
-    was-at-newline?:bool <- equal old-cursor-character, 10/newline
-    break-unless was-at-newline?
-    cursor-row <- add cursor-row, 1
-    *editor <- put *editor, cursor-row:offset, cursor-row
-    cursor-column <- copy left
-    *editor <- put *editor, cursor-column:offset, cursor-column
-    below-screen?:bool <- greater-or-equal cursor-row, screen-height  # must be equal
-    return-unless below-screen?, false/don't-render
-    cursor-row <- subtract cursor-row, 1  # bring back into screen range
-    *editor <- put *editor, cursor-row:offset, cursor-row
-    return true/go-render
-  }
-  # if the line wraps, move cursor to start of next row
-  {
-    # if we're at the column just before the wrap indicator
-    wrap-column:num <- subtract right, 1
-    at-wrap?:bool <- equal cursor-column, wrap-column
-    break-unless at-wrap?
-    # and if next character isn't newline
-    next:&:duplex-list:char <- next before-cursor
-    break-unless next
-    next-character:char <- get *next, value:offset
-    newline?:bool <- equal next-character, 10/newline
-    break-if newline?
-    cursor-row <- add cursor-row, 1
-    *editor <- put *editor, cursor-row:offset, cursor-row
-    cursor-column <- copy left
-    *editor <- put *editor, cursor-column:offset, cursor-column
-    below-screen?:bool <- greater-or-equal cursor-row, screen-height  # must be equal
-    return-unless below-screen?, false/no-more-render
-    cursor-row <- subtract cursor-row, 1  # bring back into screen range
-    *editor <- put *editor, cursor-row:offset, cursor-row
-    return true/go-render
-  }
-  # otherwise move cursor one character right
-  cursor-column <- add cursor-column, 1
-  *editor <- put *editor, cursor-column:offset, cursor-column
-  go-render? <- copy false
-]
-
-scenario editor-moves-cursor-to-next-line-with-right-arrow [
-  local-scope
-  assume-screen 10/width, 5/height
-  s:text <- new [abc
-d]
-  e:&:editor <- new-editor s, 0/left, 10/right
-  editor-render screen, e
-  $clear-trace
-  # type right-arrow a few times to get to start of second line
-  assume-console [
-    press right-arrow
-    press right-arrow
-    press right-arrow
-    press right-arrow  # next line
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  check-trace-count-for-label 0, [print-character]
-  # type something and ensure it goes where it should
-  assume-console [
-    type [0]
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  screen-should-contain [
-    .          .
-    .abc       .
-    .0d        .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-  check-trace-count-for-label 2, [print-character]  # new length of second line
-]
-
-scenario editor-moves-cursor-to-next-line-with-right-arrow-2 [
-  local-scope
-  assume-screen 10/width, 5/height
-  s:text <- new [abc
-d]
-  e:&:editor <- new-editor s, 1/left, 10/right
-  editor-render screen, e
-  assume-console [
-    press right-arrow
-    press right-arrow
-    press right-arrow
-    press right-arrow  # next line
-    type [0]
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  screen-should-contain [
-    .          .
-    . abc      .
-    . 0d       .
-    . ┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-]
-
-scenario editor-moves-cursor-to-next-wrapped-line-with-right-arrow [
-  local-scope
-  assume-screen 10/width, 5/height
-  e:&:editor <- new-editor [abcdef], 0/left, 5/right
-  editor-render screen, e
-  $clear-trace
-  assume-console [
-    left-click 1, 3
-    press right-arrow
-  ]
-  run [
-    editor-event-loop screen, console, e
-    3:num/raw <- get *e, cursor-row:offset
-    4:num/raw <- get *e, cursor-column:offset
-  ]
-  screen-should-contain [
-    .          .
-    .abcd↩     .
-    .ef        .
-    .┈┈┈┈┈     .
-    .          .
-  ]
-  memory-should-contain [
-    3 <- 2
-    4 <- 0
-  ]
-  check-trace-count-for-label 0, [print-character]
-]
-
-scenario editor-moves-cursor-to-next-wrapped-line-with-right-arrow-2 [
-  local-scope
-  assume-screen 10/width, 5/height
-  # line just barely wrapping
-  e:&:editor <- new-editor [abcde], 0/left, 5/right
-  editor-render screen, e
-  $clear-trace
-  # position cursor at last character before wrap and hit right-arrow
-  assume-console [
-    left-click 1, 3
-    press right-arrow
-  ]
-  run [
-    editor-event-loop screen, console, e
-    3:num/raw <- get *e, cursor-row:offset
-    4:num/raw <- get *e, cursor-column:offset
-  ]
-  memory-should-contain [
-    3 <- 2
-    4 <- 0
-  ]
-  # now hit right arrow again
-  assume-console [
-    press right-arrow
-  ]
-  run [
-    editor-event-loop screen, console, e
-    3:num/raw <- get *e, cursor-row:offset
-    4:num/raw <- get *e, cursor-column:offset
-  ]
-  memory-should-contain [
-    3 <- 2
-    4 <- 1
-  ]
-  check-trace-count-for-label 0, [print-character]
-]
-
-scenario editor-moves-cursor-to-next-wrapped-line-with-right-arrow-3 [
-  local-scope
-  assume-screen 10/width, 5/height
-  e:&:editor <- new-editor [abcdef], 1/left, 6/right
-  editor-render screen, e
-  $clear-trace
-  assume-console [
-    left-click 1, 4
-    press right-arrow
-  ]
-  run [
-    editor-event-loop screen, console, e
-    3:num/raw <- get *e, cursor-row:offset
-    4:num/raw <- get *e, cursor-column:offset
-  ]
-  screen-should-contain [
-    .          .
-    . abcd↩    .
-    . ef       .
-    . ┈┈┈┈┈    .
-    .          .
-  ]
-  memory-should-contain [
-    3 <- 2
-    4 <- 1
-  ]
-  check-trace-count-for-label 0, [print-character]
-]
-
-scenario editor-moves-cursor-to-next-line-with-right-arrow-at-end-of-line [
-  local-scope
-  assume-screen 10/width, 5/height
-  s:text <- new [abc
-d]
-  e:&:editor <- new-editor s, 0/left, 10/right
-  editor-render screen, e
-  $clear-trace
-  # move to end of line, press right-arrow, type a character
-  assume-console [
-    left-click 1, 3
-    press right-arrow
-    type [0]
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  # new character should be in next line
-  screen-should-contain [
-    .          .
-    .abc       .
-    .0d        .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-  check-trace-count-for-label 2, [print-character]
-]
-
-# todo: ctrl-right: next word-end
-
-# left arrow
-
-scenario editor-moves-cursor-left-with-key [
-  local-scope
-  assume-screen 10/width, 5/height
-  e:&:editor <- new-editor [abc], 0/left, 10/right
-  editor-render screen, e
-  $clear-trace
-  assume-console [
-    left-click 1, 2
-    press left-arrow
-    type [0]
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  screen-should-contain [
-    .          .
-    .a0bc      .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-  check-trace-count-for-label 3, [print-character]
-]
-
-after <handle-special-key> [
-  {
-    move-to-previous-character?:bool <- equal k, 65515/left-arrow
-    break-unless move-to-previous-character?
-    trace 10, [app], [left arrow]
-    # if not at start of text (before-cursor at § sentinel)
-    prev:&:duplex-list:char <- prev before-cursor
-    return-unless prev, false/don't-render
-    <begin-move-cursor>
-    move-cursor-coordinates-left editor
-    before-cursor <- copy prev
-    *editor <- put *editor, before-cursor:offset, before-cursor
-    undo-coalesce-tag:num <- copy 1/left-arrow
-    <end-move-cursor>
-    return
-  }
-]
-
-scenario editor-moves-cursor-to-previous-line-with-left-arrow-at-start-of-line [
-  local-scope
-  assume-screen 10/width, 5/height
-  # initialize editor with two lines
-  s:text <- new [abc
-d]
-  e:&:editor <- new-editor s, 0/left, 10/right
-  editor-render screen, e
-  $clear-trace
-  # position cursor at start of second line (so there's no previous newline)
-  assume-console [
-    left-click 2, 0
-    press left-arrow
-  ]
-  run [
-    editor-event-loop screen, console, e
-    3:num/raw <- get *e, cursor-row:offset
-    4:num/raw <- get *e, cursor-column:offset
-  ]
-  memory-should-contain [
-    3 <- 1
-    4 <- 3
-  ]
-  check-trace-count-for-label 0, [print-character]
-]
-
-scenario editor-moves-cursor-to-previous-line-with-left-arrow-at-start-of-line-2 [
-  local-scope
-  assume-screen 10/width, 5/height
-  # initialize editor with three lines
-  s:text <- new [abc
-def
-g]
-  e:&:editor <- new-editor s:text, 0/left, 10/right
-  editor-render screen, e
-  $clear-trace
-  # position cursor further down (so there's a newline before the character at
-  # the cursor)
-  assume-console [
-    left-click 3, 0
-    press left-arrow
-    type [0]
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  screen-should-contain [
-    .          .
-    .abc       .
-    .def0      .
-    .g         .
-    .┈┈┈┈┈┈┈┈┈┈.
-  ]
-  check-trace-count-for-label 1, [print-character]  # just the '0'
-]
-
-scenario editor-moves-cursor-to-previous-line-with-left-arrow-at-start-of-line-3 [
-  local-scope
-  assume-screen 10/width, 5/height
-  s:text <- new [abc
-def
-g]
-  e:&:editor <- new-editor s, 0/left, 10/right
-  editor-render screen, e
-  $clear-trace
-  # position cursor at start of text, press left-arrow, then type a character
-  assume-console [
-    left-click 1, 0
-    press left-arrow
-    type [0]
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  # left-arrow should have had no effect
-  screen-should-contain [
-    .          .
-    .0abc      .
-    .def       .
-    .g         .
-    .┈┈┈┈┈┈┈┈┈┈.
-  ]
-  check-trace-count-for-label 4, [print-character]  # length of first line
-]
-
-scenario editor-moves-cursor-to-previous-line-with-left-arrow-at-start-of-line-4 [
-  local-scope
-  assume-screen 10/width, 5/height
-  # initialize editor with text containing an empty line
-  s:text <- new [abc
-
-d]
-  e:&:editor <- new-editor s, 0/left, 10/right
-  editor-render screen, e:&:editor
-  $clear-trace
-  # position cursor right after empty line
-  assume-console [
-    left-click 3, 0
-    press left-arrow
-    type [0]
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  screen-should-contain [
-    .          .
-    .abc       .
-    .0         .
-    .d         .
-    .┈┈┈┈┈┈┈┈┈┈.
-  ]
-  check-trace-count-for-label 1, [print-character]  # just the '0'
-]
-
-scenario editor-moves-across-screen-lines-across-wrap-with-left-arrow [
-  local-scope
-  assume-screen 10/width, 5/height
-  # initialize editor with a wrapping line
-  e:&:editor <- new-editor [abcdef], 0/left, 5/right
-  editor-render screen, e
-  $clear-trace
-  screen-should-contain [
-    .          .
-    .abcd↩     .
-    .ef        .
-    .┈┈┈┈┈     .
-    .          .
-  ]
-  # position cursor right after empty line
-  assume-console [
-    left-click 2, 0
-    press left-arrow
-  ]
-  run [
-    editor-event-loop screen, console, e
-    3:num/raw <- get *e, cursor-row:offset
-    4:num/raw <- get *e, cursor-column:offset
-  ]
-  memory-should-contain [
-    3 <- 1  # previous row
-    4 <- 3  # right margin except wrap icon
-  ]
-  check-trace-count-for-label 0, [print-character]
-]
-
-scenario editor-moves-across-screen-lines-to-wrapping-line-with-left-arrow [
-  local-scope
-  assume-screen 10/width, 5/height
-  # initialize editor with a wrapping line followed by a second line
-  s:text <- new [abcdef
-g]
-  e:&:editor <- new-editor s, 0/left, 5/right
-  editor-render screen, e
-  $clear-trace
-  screen-should-contain [
-    .          .
-    .abcd↩     .
-    .ef        .
-    .g         .
-    .┈┈┈┈┈     .
-  ]
-  # position cursor right after empty line
-  assume-console [
-    left-click 3, 0
-    press left-arrow
-  ]
-  run [
-    editor-event-loop screen, console, e
-    3:num/raw <- get *e, cursor-row:offset
-    4:num/raw <- get *e, cursor-column:offset
-  ]
-  memory-should-contain [
-    3 <- 2  # previous row
-    4 <- 2  # end of wrapped line
-  ]
-  check-trace-count-for-label 0, [print-character]
-]
-
-scenario editor-moves-across-screen-lines-to-non-wrapping-line-with-left-arrow [
-  local-scope
-  assume-screen 10/width, 5/height
-  # initialize editor with a line on the verge of wrapping, followed by a second line
-  s:text <- new [abcd
-e]
-  e:&:editor <- new-editor s, 0/left, 5/right
-  editor-render screen, e
-  $clear-trace
-  screen-should-contain [
-    .          .
-    .abcd      .
-    .e         .
-    .┈┈┈┈┈     .
-    .          .
-  ]
-  # position cursor right after empty line
-  assume-console [
-    left-click 2, 0
-    press left-arrow
-  ]
-  run [
-    editor-event-loop screen, console, e
-    3:num/raw <- get *e, cursor-row:offset
-    4:num/raw <- get *e, cursor-column:offset
-  ]
-  memory-should-contain [
-    3 <- 1  # previous row
-    4 <- 4  # end of wrapped line
-  ]
-  check-trace-count-for-label 0, [print-character]
-]
-
-# todo: ctrl-left: previous word-start
-
-# up arrow
-
-scenario editor-moves-to-previous-line-with-up-arrow [
-  local-scope
-  assume-screen 10/width, 5/height
-  s:text <- new [abc
-def]
-  e:&:editor <- new-editor s, 0/left, 10/right
-  editor-render screen, e
-  $clear-trace
-  assume-console [
-    left-click 2, 1
-    press up-arrow
-  ]
-  run [
-    editor-event-loop screen, console, e
-    3:num/raw <- get *e, cursor-row:offset
-    4:num/raw <- get *e, cursor-column:offset
-  ]
-  memory-should-contain [
-    3 <- 1
-    4 <- 1
-  ]
-  check-trace-count-for-label 0, [print-character]
-  assume-console [
-    type [0]
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  screen-should-contain [
-    .          .
-    .a0bc      .
-    .def       .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-]
-
-after <handle-special-key> [
-  {
-    move-to-previous-line?:bool <- equal k, 65517/up-arrow
-    break-unless move-to-previous-line?
-    <begin-move-cursor>
-    move-to-previous-line editor
-    undo-coalesce-tag:num <- copy 3/up-arrow
-    <end-move-cursor>
-    return
-  }
-]
-
-def move-to-previous-line editor:&:editor -> editor:&:editor [
-  local-scope
-  load-inputs
-  cursor-row:num <- get *editor, cursor-row:offset
-  cursor-column:num <- get *editor, cursor-column:offset
-  before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
-  left:num <- get *editor, left:offset
-  right:num <- get *editor, right:offset
-  already-at-top?:bool <- lesser-or-equal cursor-row, 1/top
-  {
-    # if cursor not at top, move it
-    break-if already-at-top?
-    # if not at newline, move to start of line (previous newline)
-    # then scan back another line
-    # if either step fails, give up without modifying cursor or coordinates
-    curr:&:duplex-list:char <- copy before-cursor
-    old:&:duplex-list:char <- copy curr
-    {
-      at-left?:bool <- equal cursor-column, left
-      break-if at-left?
-      curr <- before-previous-screen-line curr, editor
-      no-motion?:bool <- equal curr, old
-      return-if no-motion?
-    }
-    {
-      curr <- before-previous-screen-line curr, editor
-      no-motion?:bool <- equal curr, old
-      return-if no-motion?
-    }
-    before-cursor <- copy curr
-    *editor <- put *editor, before-cursor:offset, before-cursor
-    cursor-row <- subtract cursor-row, 1
-    *editor <- put *editor, cursor-row:offset, cursor-row
-    # scan ahead to right column or until end of line
-    target-column:num <- copy cursor-column
-    cursor-column <- copy left
-    *editor <- put *editor, cursor-column:offset, cursor-column
-    {
-      done?:bool <- greater-or-equal cursor-column, target-column
-      break-if done?
-      curr:&:duplex-list:char <- next before-cursor
-      break-unless curr
-      currc:char <- get *curr, value:offset
-      at-newline?:bool <- equal currc, 10/newline
-      break-if at-newline?
-      #
-      before-cursor <- copy curr
-      *editor <- put *editor, before-cursor:offset, before-cursor
-      cursor-column <- add cursor-column, 1
-      *editor <- put *editor, cursor-column:offset, cursor-column
-      loop
-    }
-  }
-]
-
-# Takes a pointer into the doubly-linked list, scans back to before start of
-# previous *wrapped* line.
-# Returns original if no next newline.
-# Beware: never return null pointer.
-def before-previous-screen-line in:&:duplex-list:char, editor:&:editor -> out:&:duplex-list:char [
-  local-scope
-  load-inputs
-  curr:&:duplex-list:char <- copy in
-  c:char <- get *curr, value:offset
-  # compute max, number of characters to skip
-  #   1 + len%(width-1)
-  #   except rotate second term to vary from 1 to width-1 rather than 0 to width-2
-  left:num <- get *editor, left:offset
-  right:num <- get *editor, right:offset
-  max-line-length:num <- subtract right, left, -1/exclusive-right, 1/wrap-icon
-  sentinel:&:duplex-list:char <- get *editor, data:offset
-  len:num <- previous-line-length curr, sentinel
-  {
-    break-if len
-    # empty line; just skip this newline
-    prev:&:duplex-list:char <- prev curr
-    return-unless prev, curr
-    return prev
-  }
-  _, max:num <- divide-with-remainder len, max-line-length
-  # remainder 0 => scan one width-worth
-  {
-    break-if max
-    max <- copy max-line-length
-  }
-  max <- add max, 1
-  count:num <- copy 0
-  # skip 'max' characters
-  {
-    done?:bool <- greater-or-equal count, max
-    break-if done?
-    prev:&:duplex-list:char <- prev curr
-    break-unless prev
-    curr <- copy prev
-    count <- add count, 1
-    loop
-  }
-  return curr
-]
-
-scenario editor-adjusts-column-at-previous-line [
-  local-scope
-  assume-screen 10/width, 5/height
-  s:text <- new [ab
-def]
-  e:&:editor <- new-editor s, 0/left, 10/right
-  editor-render screen, e
-  $clear-trace
-  assume-console [
-    left-click 2, 3
-    press up-arrow
-  ]
-  run [
-    editor-event-loop screen, console, e
-    3:num/raw <- get *e, cursor-row:offset
-    4:num/raw <- get *e, cursor-column:offset
-  ]
-  memory-should-contain [
-    3 <- 1
-    4 <- 2
-  ]
-  check-trace-count-for-label 0, [print-character]
-  assume-console [
-    type [0]
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  screen-should-contain [
-    .          .
-    .ab0       .
-    .def       .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-]
-
-scenario editor-adjusts-column-at-empty-line [
-  local-scope
-  assume-screen 10/width, 5/height
-  s:text <- new [
-def]
-  e:&:editor <- new-editor s, 0/left, 10/right
-  editor-render screen, e
-  $clear-trace
-  assume-console [
-    left-click 2, 3
-    press up-arrow
-  ]
-  run [
-    editor-event-loop screen, console, e
-    3:num/raw <- get *e, cursor-row:offset
-    4:num/raw <- get *e, cursor-column:offset
-  ]
-  memory-should-contain [
-    3 <- 1
-    4 <- 0
-  ]
-  check-trace-count-for-label 0, [print-character]
-  assume-console [
-    type [0]
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  screen-should-contain [
-    .          .
-    .0         .
-    .def       .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-]
-
-scenario editor-moves-to-previous-line-from-zero-margin [
-  local-scope
-  assume-screen 10/width, 5/height
-  # start out with three lines
-  s:text <- new [abc
-def
-ghi]
-  e:&:editor <- new-editor s, 0/left, 10/right
-  editor-render screen, e
-  $clear-trace
-  # click on the third line and hit up-arrow, so you end up just after a newline
-  assume-console [
-    left-click 3, 0
-    press up-arrow
-  ]
-  run [
-    editor-event-loop screen, console, e
-    3:num/raw <- get *e, cursor-row:offset
-    4:num/raw <- get *e, cursor-column:offset
-  ]
-  memory-should-contain [
-    3 <- 2
-    4 <- 0
-  ]
-  check-trace-count-for-label 0, [print-character]
-  assume-console [
-    type [0]
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  screen-should-contain [
-    .          .
-    .abc       .
-    .0def      .
-    .ghi       .
-    .┈┈┈┈┈┈┈┈┈┈.
-  ]
-]
-
-scenario editor-moves-to-previous-line-from-left-margin [
-  local-scope
-  assume-screen 10/width, 5/height
-  # start out with three lines
-  s:text <- new [abc
-def
-ghi]
-  e:&:editor <- new-editor s, 1/left, 10/right
-  editor-render screen, e
-  $clear-trace
-  # click on the third line and hit up-arrow, so you end up just after a newline
-  assume-console [
-    left-click 3, 1
-    press up-arrow
-  ]
-  run [
-    editor-event-loop screen, console, e
-    3:num/raw <- get *e, cursor-row:offset
-    4:num/raw <- get *e, cursor-column:offset
-  ]
-  memory-should-contain [
-    3 <- 2
-    4 <- 1
-  ]
-  check-trace-count-for-label 0, [print-character]
-  assume-console [
-    type [0]
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  screen-should-contain [
-    .          .
-    . abc      .
-    . 0def     .
-    . ghi      .
-    . ┈┈┈┈┈┈┈┈┈.
-  ]
-]
-
-scenario editor-moves-to-top-line-in-presence-of-wrapped-line [
-  local-scope
-  assume-screen 10/width, 5/height
-  e:&:editor <- new-editor [abcde], 0/left, 5/right
-  editor-render screen, e
-  screen-should-contain [
-    .          .
-    .abcd↩     .
-    .e         .
-    .┈┈┈┈┈     .
-  ]
-  $clear-trace
-  assume-console [
-    left-click 2, 0
-    press up-arrow
-  ]
-  run [
-    editor-event-loop screen, console, e
-    3:num/raw <- get *e, cursor-row:offset
-    4:num/raw <- get *e, cursor-column:offset
-  ]
-  memory-should-contain [
-    3 <- 1
-    4 <- 0
-  ]
-  check-trace-count-for-label 0, [print-character]
-  assume-console [
-    type [0]
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  screen-should-contain [
-    .          .
-    .0abc↩     .
-    .de        .
-    .┈┈┈┈┈     .
-  ]
-]
-
-scenario editor-moves-to-top-line-in-presence-of-wrapped-line-2 [
-  local-scope
-  assume-screen 10/width, 5/height
-  s:text <- new [abc
-defgh]
-  e:&:editor <- new-editor s, 0/left, 5/right
-  editor-render screen, e
-  screen-should-contain [
-    .          .
-    .abc       .
-    .defg↩     .
-    .h         .
-    .┈┈┈┈┈     .
-  ]
-  $clear-trace
-  assume-console [
-    left-click 3, 0
-    press up-arrow
-    press up-arrow
-  ]
-  run [
-    editor-event-loop screen, console, e
-    3:num/raw <- get *e, cursor-row:offset
-    4:num/raw <- get *e, cursor-column:offset
-  ]
-  memory-should-contain [
-    3 <- 1
-    4 <- 0
-  ]
-  check-trace-count-for-label 0, [print-character]
-  assume-console [
-    type [0]
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  screen-should-contain [
-    .          .
-    .0abc      .
-    .defg↩     .
-    .h         .
-    .┈┈┈┈┈     .
-  ]
-]
-
-# down arrow
-
-scenario editor-moves-to-next-line-with-down-arrow [
-  local-scope
-  assume-screen 10/width, 5/height
-  s:text <- new [abc
-def]
-  e:&:editor <- new-editor s, 0/left, 10/right
-  editor-render screen, e
-  $clear-trace
-  # cursor starts out at (1, 0)
-  assume-console [
-    press down-arrow
-  ]
-  run [
-    editor-event-loop screen, console, e
-    3:num/raw <- get *e, cursor-row:offset
-    4:num/raw <- get *e, cursor-column:offset
-  ]
-  # ..and ends at (2, 0)
-  memory-should-contain [
-    3 <- 2
-    4 <- 0
-  ]
-  check-trace-count-for-label 0, [print-character]
-  assume-console [
-    type [0]
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  screen-should-contain [
-    .          .
-    .abc       .
-    .0def      .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-]
-
-after <handle-special-key> [
-  {
-    move-to-next-line?:bool <- equal k, 65516/down-arrow
-    break-unless move-to-next-line?
-    <begin-move-cursor>
-    move-to-next-line editor, screen-height
-    undo-coalesce-tag:num <- copy 4/down-arrow
-    <end-move-cursor>
-    return
-  }
-]
-
-def move-to-next-line editor:&:editor, screen-height:num -> editor:&:editor [
-  local-scope
-  load-inputs
-  cursor-row:num <- get *editor, cursor-row:offset
-  cursor-column:num <- get *editor, cursor-column:offset
-  before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
-  left:num <- get *editor, left:offset
-  right:num <- get *editor, right:offset
-  last-line:num <- subtract screen-height, 1
-  bottom:num <- get *editor, bottom:offset
-  at-bottom-of-screen?:bool <- greater-or-equal bottom, last-line
-  return-unless before-cursor
-  next:&:duplex-list:char <- next before-cursor
-  return-unless next
-  already-at-bottom?:bool <- greater-or-equal cursor-row, last-line
-    # if cursor not at bottom, move it
-    return-if already-at-bottom?
-    target-column:num <- copy cursor-column
-    # scan to start of next line
-    {
-      next:&:duplex-list:char <- next before-cursor
-      break-unless next
-      done?:bool <- greater-or-equal cursor-column, right
-      break-if done?
-      cursor-column <- add cursor-column, 1
-      before-cursor <- copy next
-      c:char <- get *next, value:offset
-      at-newline?:bool <- equal c, 10/newline
-      break-if at-newline?
-      loop
-    }
-    return-unless next
-    cursor-row <- add cursor-row, 1
-    cursor-column <- copy left
-    {
-      next:&:duplex-list:char <- next before-cursor
-      break-unless next
-      c:char <- get *next, value:offset
-      at-newline?:bool <- equal c, 10/newline
-      break-if at-newline?
-      done?:bool <- greater-or-equal cursor-column, target-column
-      break-if done?
-      cursor-column <- add cursor-column, 1
-      before-cursor <- copy next
-      loop
-    }
-    *editor <- put *editor, before-cursor:offset, before-cursor
-    *editor <- put *editor, cursor-column:offset, cursor-column
-    *editor <- put *editor, cursor-row:offset, cursor-row
-]
-
-scenario editor-adjusts-column-at-next-line [
-  local-scope
-  assume-screen 10/width, 5/height
-  # second line is shorter than first
-  s:text <- new [abcde
-fg
-hi]
-  e:&:editor <- new-editor s, 0/left, 10/right
-  editor-render screen, e
-  $clear-trace
-  # move to end of first line, then press down
-  assume-console [
-    left-click 1, 8
-    press down-arrow
-  ]
-  run [
-    editor-event-loop screen, console, e
-    3:num/raw <- get *e, cursor-row:offset
-    4:num/raw <- get *e, cursor-column:offset
-  ]
-  # cursor doesn't go vertically down, it goes to end of shorter line
-  memory-should-contain [
-    3 <- 2
-    4 <- 2
-  ]
-  check-trace-count-for-label 0, [print-character]
-  assume-console [
-    type [0]
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  screen-should-contain [
-    .          .
-    .abcde     .
-    .fg0       .
-    .hi        .
-    .┈┈┈┈┈┈┈┈┈┈.
-  ]
-]
-
-# ctrl-a/home - move cursor to start of line
-
-scenario editor-moves-to-start-of-line-with-ctrl-a [
-  local-scope
-  assume-screen 10/width, 5/height
-  s:text <- new [123
-456]
-  e:&:editor <- new-editor s, 0/left, 10/right
-  editor-render screen, e
-  $clear-trace
-  # start on second line, press ctrl-a
-  assume-console [
-    left-click 2, 3
-    press ctrl-a
-  ]
-  run [
-    editor-event-loop screen, console, e
-    4:num/raw <- get *e, cursor-row:offset
-    5:num/raw <- get *e, cursor-column:offset
-  ]
-  # cursor moves to start of line
-  memory-should-contain [
-    4 <- 2
-    5 <- 0
-  ]
-  check-trace-count-for-label 0, [print-character]
-]
-
-after <handle-special-character> [
-  {
-    move-to-start-of-line?:bool <- equal c, 1/ctrl-a
-    break-unless move-to-start-of-line?
-    <begin-move-cursor>
-    move-to-start-of-screen-line editor
-    undo-coalesce-tag:num <- copy 0/never
-    <end-move-cursor>
-    return false/don't-render
-  }
-]
-
-after <handle-special-key> [
-  {
-    move-to-start-of-line?:bool <- equal k, 65521/home
-    break-unless move-to-start-of-line?
-    <begin-move-cursor>
-    move-to-start-of-screen-line editor
-    undo-coalesce-tag:num <- copy 0/never
-    <end-move-cursor>
-    return false/don't-render
-  }
-]
-
-# handles wrapped lines
-# precondition: cursor-column should be in a consistent state
-def move-to-start-of-screen-line editor:&:editor -> editor:&:editor [
-  local-scope
-  load-inputs
-  # update cursor column
-  left:num <- get *editor, left:offset
-  col:num <- get *editor, cursor-column:offset
-  # update before-cursor
-  curr:&:duplex-list:char <- get *editor, before-cursor:offset
-  # while not at start of line, move
-  {
-    done?:bool <- equal col, left
-    break-if done?
-    assert curr, [move-to-start-of-line tried to move before start of text]
-    curr <- prev curr
-    col <- subtract col, 1
-    loop
-  }
-  *editor <- put *editor, cursor-column:offset, col
-  *editor <- put *editor, before-cursor:offset, curr
-]
-
-scenario editor-moves-to-start-of-line-with-ctrl-a-2 [
-  local-scope
-  assume-screen 10/width, 5/height
-  s:text <- new [123
-456]
-  e:&:editor <- new-editor s, 0/left, 10/right
-  editor-render screen, e
-  $clear-trace
-  # start on first line (no newline before), press ctrl-a
-  assume-console [
-    left-click 1, 3
-    press ctrl-a
-  ]
-  run [
-    editor-event-loop screen, console, e
-    4:num/raw <- get *e, cursor-row:offset
-    5:num/raw <- get *e, cursor-column:offset
-  ]
-  # cursor moves to start of line
-  memory-should-contain [
-    4 <- 1
-    5 <- 0
-  ]
-  check-trace-count-for-label 0, [print-character]
-]
-
-scenario editor-moves-to-start-of-line-with-home [
-  local-scope
-  assume-screen 10/width, 5/height
-  s:text <- new [123
-456]
-  e:&:editor <- new-editor s, 0/left, 10/right
-  $clear-trace
-  # start on second line, press 'home'
-  assume-console [
-    left-click 2, 3
-    press home
-  ]
-  run [
-    editor-event-loop screen, console, e
-    3:num/raw <- get *e, cursor-row:offset
-    4:num/raw <- get *e, cursor-column:offset
-  ]
-  # cursor moves to start of line
-  memory-should-contain [
-    3 <- 2
-    4 <- 0
-  ]
-  check-trace-count-for-label 0, [print-character]
-]
-
-scenario editor-moves-to-start-of-line-with-home-2 [
-  local-scope
-  assume-screen 10/width, 5/height
-  s:text <- new [123
-456]
-  e:&:editor <- new-editor s, 0/left, 10/right
-  editor-render screen, e
-  $clear-trace
-  # start on first line (no newline before), press 'home'
-  assume-console [
-    left-click 1, 3
-    press home
-  ]
-  run [
-    editor-event-loop screen, console, e
-    3:num/raw <- get *e, cursor-row:offset
-    4:num/raw <- get *e, cursor-column:offset
-  ]
-  # cursor moves to start of line
-  memory-should-contain [
-    3 <- 1
-    4 <- 0
-  ]
-  check-trace-count-for-label 0, [print-character]
-]
-
-scenario editor-moves-to-start-of-screen-line-with-ctrl-a [
-  local-scope
-  assume-screen 10/width, 5/height
-  e:&:editor <- new-editor [123456], 0/left, 5/right
-  editor-render screen, e
-  screen-should-contain [
-    .          .
-    .1234↩     .
-    .56        .
-    .┈┈┈┈┈     .
-    .          .
-  ]
-  $clear-trace
-  # start on second line, press ctrl-a then up
-  assume-console [
-    left-click 2, 1
-    press ctrl-a
-    press up-arrow
-  ]
-  run [
-    editor-event-loop screen, console, e
-    4:num/raw <- get *e, cursor-row:offset
-    5:num/raw <- get *e, cursor-column:offset
-  ]
-  # cursor moves to start of first line
-  memory-should-contain [
-    4 <- 1  # cursor-row
-    5 <- 0  # cursor-column
-  ]
-  check-trace-count-for-label 0, [print-character]
-  # make sure before-cursor is in sync
-  assume-console [
-    type [a]
-  ]
-  run [
-    editor-event-loop screen, console, e
-    4:num/raw <- get *e, cursor-row:offset
-    5:num/raw <- get *e, cursor-column:offset
-  ]
-  screen-should-contain [
-    .          .
-    .a123↩     .
-    .456       .
-    .┈┈┈┈┈     .
-    .          .
-  ]
-  memory-should-contain [
-    4 <- 1  # cursor-row
-    5 <- 1  # cursor-column
-  ]
-]
-
-# ctrl-e/end - move cursor to end of line
-
-scenario editor-moves-to-end-of-line-with-ctrl-e [
-  local-scope
-  assume-screen 10/width, 5/height
-  s:text <- new [123
-456]
-  e:&:editor <- new-editor s, 0/left, 10/right
-  editor-render screen, e
-  $clear-trace
-  # start on first line, press ctrl-e
-  assume-console [
-    left-click 1, 1
-    press ctrl-e
-  ]
-  run [
-    editor-event-loop screen, console, e
-    4:num/raw <- get *e, cursor-row:offset
-    5:num/raw <- get *e, cursor-column:offset
-  ]
-  # cursor moves to end of line
-  memory-should-contain [
-    4 <- 1
-    5 <- 3
-  ]
-  check-trace-count-for-label 0, [print-character]
-  # editor inserts future characters at cursor
-  assume-console [
-    type [z]
-  ]
-  run [
-    editor-event-loop screen, console, e
-    4:num/raw <- get *e, cursor-row:offset
-    5:num/raw <- get *e, cursor-column:offset
-  ]
-  memory-should-contain [
-    4 <- 1
-    5 <- 4
-  ]
-  screen-should-contain [
-    .          .
-    .123z      .
-    .456       .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-  check-trace-count-for-label 1, [print-character]
-]
-
-after <handle-special-character> [
-  {
-    move-to-end-of-line?:bool <- equal c, 5/ctrl-e
-    break-unless move-to-end-of-line?
-    <begin-move-cursor>
-    move-to-end-of-line editor
-    undo-coalesce-tag:num <- copy 0/never
-    <end-move-cursor>
-    return false/don't-render
-  }
-]
-
-after <handle-special-key> [
-  {
-    move-to-end-of-line?:bool <- equal k, 65520/end
-    break-unless move-to-end-of-line?
-    <begin-move-cursor>
-    move-to-end-of-line editor
-    undo-coalesce-tag:num <- copy 0/never
-    <end-move-cursor>
-    return false/don't-render
-  }
-]
-
-def move-to-end-of-line editor:&:editor -> editor:&:editor [
-  local-scope
-  load-inputs
-  before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
-  cursor-column:num <- get *editor, cursor-column:offset
-  right:num <- get *editor, right:offset
-  # while not at end of line, move
-  {
-    next:&:duplex-list:char <- next before-cursor
-    break-unless next  # end of text
-    nextc:char <- get *next, value:offset
-    at-end-of-line?:bool <- equal nextc, 10/newline
-    break-if at-end-of-line?
-    cursor-column <- add cursor-column, 1
-    at-right?:bool <- equal cursor-column, right
-    break-if at-right?
-    *editor <- put *editor, cursor-column:offset, cursor-column
-    before-cursor <- copy next
-    *editor <- put *editor, before-cursor:offset, before-cursor
-    loop
-  }
-]
-
-scenario editor-moves-to-end-of-line-with-ctrl-e-2 [
-  local-scope
-  assume-screen 10/width, 5/height
-  s:text <- new [123
-456]
-  e:&:editor <- new-editor s, 0/left, 10/right
-  editor-render screen, e
-  $clear-trace
-  # start on second line (no newline after), press ctrl-e
-  assume-console [
-    left-click 2, 1
-    press ctrl-e
-  ]
-  run [
-    editor-event-loop screen, console, e
-    4:num/raw <- get *e, cursor-row:offset
-    5:num/raw <- get *e, cursor-column:offset
-  ]
-  # cursor moves to end of line
-  memory-should-contain [
-    4 <- 2
-    5 <- 3
-  ]
-  check-trace-count-for-label 0, [print-character]
-]
-
-scenario editor-moves-to-end-of-line-with-end [
-  local-scope
-  assume-screen 10/width, 5/height
-  s:text <- new [123
-456]
-  e:&:editor <- new-editor s, 0/left, 10/right
-  editor-render screen, e
-  $clear-trace
-  # start on first line, press 'end'
-  assume-console [
-    left-click 1, 1
-    press end
-  ]
-  run [
-    editor-event-loop screen, console, e
-    3:num/raw <- get *e, cursor-row:offset
-    4:num/raw <- get *e, cursor-column:offset
-  ]
-  # cursor moves to end of line
-  memory-should-contain [
-    3 <- 1
-    4 <- 3
-  ]
-  check-trace-count-for-label 0, [print-character]
-]
-
-scenario editor-moves-to-end-of-line-with-end-2 [
-  local-scope
-  assume-screen 10/width, 5/height
-  s:text <- new [123
-456]
-  e:&:editor <- new-editor s, 0/left, 10/right
-  editor-render screen, e
-  $clear-trace
-  # start on second line (no newline after), press 'end'
-  assume-console [
-    left-click 2, 1
-    press end
-  ]
-  run [
-    editor-event-loop screen, console, e
-    3:num/raw <- get *e, cursor-row:offset
-    4:num/raw <- get *e, cursor-column:offset
-  ]
-  # cursor moves to end of line
-  memory-should-contain [
-    3 <- 2
-    4 <- 3
-  ]
-  check-trace-count-for-label 0, [print-character]
-]
-
-scenario editor-moves-to-end-of-wrapped-line [
-  local-scope
-  assume-screen 10/width, 5/height
-  s:text <- new [123456
-789]
-  e:&:editor <- new-editor s, 0/left, 5/right
-  editor-render screen, e
-  $clear-trace
-  # start on first line, press 'end'
-  assume-console [
-    left-click 1, 1
-    press end
-  ]
-  run [
-    editor-event-loop screen, console, e
-    10:num/raw <- get *e, cursor-row:offset
-    11:num/raw <- get *e, cursor-column:offset
-  ]
-  # cursor moves to end of line
-  memory-should-contain [
-    10 <- 1
-    11 <- 3
-  ]
-  # no prints
-  check-trace-count-for-label 0, [print-character]
-  # before-cursor is also consistent
-  assume-console [
-    type [a]
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  screen-should-contain [
-    .          .
-    .123a↩     .
-    .456       .
-    .789       .
-    .┈┈┈┈┈     .
-  ]
-]
-
-# ctrl-u - delete text from start of line until (but not at) cursor
-
-scenario editor-deletes-to-start-of-line-with-ctrl-u [
-  local-scope
-  assume-screen 10/width, 5/height
-  s:text <- new [123
-456]
-  e:&:editor <- new-editor s, 0/left, 10/right
-  editor-render screen, e
-  $clear-trace
-  # start on second line, press ctrl-u
-  assume-console [
-    left-click 2, 2
-    press ctrl-u
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  # cursor deletes to start of line
-  screen-should-contain [
-    .          .
-    .123       .
-    .6         .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-  check-trace-count-for-label 10, [print-character]
-]
-
-after <handle-special-character> [
-  {
-    delete-to-start-of-line?:bool <- equal c, 21/ctrl-u
-    break-unless delete-to-start-of-line?
-    <begin-delete-to-start-of-line>
-    deleted-cells:&:duplex-list:char <- delete-to-start-of-line editor
-    <end-delete-to-start-of-line>
-    go-render?:bool <- minimal-render-for-ctrl-u screen, editor, deleted-cells
-    return
-  }
-]
-
-def minimal-render-for-ctrl-u screen:&:screen, editor:&:editor, deleted-cells:&:duplex-list:char -> go-render?:bool, screen:&:screen [
-  local-scope
-  load-inputs
-  curr-column:num <- get *editor, cursor-column:offset
-  # accumulate the current line as text and render it
-  buf:&:buffer:char <- new-buffer 30  # accumulator for the text we need to render
-  curr:&:duplex-list:char <- get *editor, before-cursor:offset
-  i:num <- copy curr-column
-  right:num <- get *editor, right:offset
-  {
-    # if we have a wrapped line, give up and render the whole screen
-    wrap?:bool <- greater-or-equal i, right
-    return-if wrap?, true/go-render
-    curr <- next curr
-    break-unless curr
-    c:char <- get *curr, value:offset
-    b:bool <- equal c, 10
-    break-if b
-    buf <- append buf, c
-    i <- add i, 1
-    loop
-  }
-  # if the line used to be wrapped, give up and render the whole screen
-  num-deleted-cells:num <- length deleted-cells
-  old-row-len:num <- add i, num-deleted-cells
-  left:num <- get *editor, left:offset
-  end:num <- subtract right, left
-  wrap?:bool <- greater-or-equal old-row-len, end
-  return-if wrap?, true/go-render
-  curr-line:text <- buffer-to-array buf
-  curr-row:num <- get *editor, cursor-row:offset
-  render-code screen, curr-line, curr-column, right, curr-row
-  return false/dont-render
-]
-
-def delete-to-start-of-line editor:&:editor -> result:&:duplex-list:char, editor:&:editor [
-  local-scope
-  load-inputs
-  # compute range to delete
-  init:&:duplex-list:char <- get *editor, data:offset
-  before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
-  start:&:duplex-list:char <- copy before-cursor
-  end:&:duplex-list:char <- next before-cursor
-  {
-    at-start-of-text?:bool <- equal start, init
-    break-if at-start-of-text?
-    curr:char <- get *start, value:offset
-    at-start-of-line?:bool <- equal curr, 10/newline
-    break-if at-start-of-line?
-    start <- prev start
-    assert start, [delete-to-start-of-line tried to move before start of text]
-    loop
-  }
-  # snip it out
-  result:&:duplex-list:char <- next start
-  remove-between start, end
-  # adjust cursor
-  before-cursor <- copy start
-  *editor <- put *editor, before-cursor:offset, before-cursor
-  left:num <- get *editor, left:offset
-  *editor <- put *editor, cursor-column:offset, left
-  # if the line wrapped before, we may need to adjust cursor-row as well
-  right:num <- get *editor, right:offset
-  width:num <- subtract right, left
-  num-deleted:num <- length result
-  cursor-row-adjustment:num <- divide-with-remainder num-deleted, width
-  return-unless cursor-row-adjustment
-  cursor-row:num <- get *editor, cursor-row:offset
-  cursor-row <- subtract cursor-row, cursor-row-adjustment
-  put *editor, cursor-row:offset, cursor-row
-]
-
-def render-code screen:&:screen, s:text, left:num, right:num, row:num -> row:num, screen:&:screen [
-  local-scope
-  load-inputs
-  return-unless s
-  color:num <- copy 7/white
-  column:num <- copy left
-  screen <- move-cursor screen, row, column
-  screen-height:num <- screen-height screen
-  i:num <- copy 0
-  len:num <- length *s
-  {
-    +next-character
-    done?:bool <- greater-or-equal i, len
-    break-if done?
-    done? <- greater-or-equal row, screen-height
-    break-if done?
-    c:char <- index *s, i
-    <character-c-received>
-    {
-      # newline? move to left rather than 0
-      newline?:bool <- equal c, 10/newline
-      break-unless newline?
-      # clear rest of line in this window
-      {
-        done?:bool <- greater-than column, right
-        break-if done?
-        space:char <- copy 32/space
-        print screen, space
-        column <- add column, 1
-        loop
-      }
-      row <- add row, 1
-      column <- copy left
-      screen <- move-cursor screen, row, column
-      i <- add i, 1
-      loop +next-character
-    }
-    {
-      # at right? wrap.
-      at-right?:bool <- equal column, right
-      break-unless at-right?
-      # print wrap icon
-      wrap-icon:char <- copy 8617/loop-back-to-left
-      print screen, wrap-icon, 245/grey
-      column <- copy left
-      row <- add row, 1
-      screen <- move-cursor screen, row, column
-      # don't increment i
-      loop +next-character
-    }
-    i <- add i, 1
-    print screen, c, color
-    column <- add column, 1
-    loop
-  }
-  was-at-left?:bool <- equal column, left
-  clear-line-until screen, right
-  {
-    break-if was-at-left?
-    row <- add row, 1
-  }
-  move-cursor screen, row, left
-]
-
-scenario editor-deletes-to-start-of-line-with-ctrl-u-2 [
-  local-scope
-  assume-screen 10/width, 5/height
-  s:text <- new [123
-456]
-  e:&:editor <- new-editor s, 0/left, 10/right
-  editor-render screen, e
-  $clear-trace
-  # start on first line (no newline before), press ctrl-u
-  assume-console [
-    left-click 1, 2
-    press ctrl-u
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  # cursor deletes to start of line
-  screen-should-contain [
-    .          .
-    .3         .
-    .456       .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-  check-trace-count-for-label 10, [print-character]
-]
-
-scenario editor-deletes-to-start-of-line-with-ctrl-u-3 [
-  local-scope
-  assume-screen 10/width, 5/height
-  s:text <- new [123
-456]
-  e:&:editor <- new-editor s, 0/left, 10/right
-  editor-render screen, e
-  $clear-trace
-  # start past end of line, press ctrl-u
-  assume-console [
-    left-click 1, 3
-    press ctrl-u
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  # cursor deletes to start of line
-  screen-should-contain [
-    .          .
-    .          .
-    .456       .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-  check-trace-count-for-label 10, [print-character]
-]
-
-scenario editor-deletes-to-start-of-final-line-with-ctrl-u [
-  local-scope
-  assume-screen 10/width, 5/height
-  s:text <- new [123
-456]
-  e:&:editor <- new-editor s, 0/left, 10/right
-  editor-render screen, e
-  $clear-trace
-  # start past end of final line, press ctrl-u
-  assume-console [
-    left-click 2, 3
-    press ctrl-u
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  # cursor deletes to start of line
-  screen-should-contain [
-    .          .
-    .123       .
-    .          .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-  check-trace-count-for-label 10, [print-character]
-]
-
-scenario editor-deletes-to-start-of-wrapped-line-with-ctrl-u [
-  local-scope
-  assume-screen 10/width, 10/height
-  # first line starts out wrapping
-  s:text <- new [123456
-789]
-  e:&:editor <- new-editor s, 0/left, 5/right
-  editor-render screen, e
-  screen-should-contain [
-    .          .
-    .1234↩     .
-    .56        .
-    .789       .
-    .┈┈┈┈┈     .
-    .          .
-  ]
-  $clear-trace
-  # ctrl-u enough of the first line that it's no longer wrapping
-  assume-console [
-    left-click 1, 3
-    press ctrl-u
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  # entire screen needs to be refreshed
-  screen-should-contain [
-    .          .
-    .456       .
-    .789       .
-    .┈┈┈┈┈     .
-    .          .
-  ]
-  check-trace-count-for-label 45, [print-character]
-]
-
-# sometimes hitting ctrl-u needs to adjust the cursor row
-scenario editor-deletes-to-start-of-wrapped-line-with-ctrl-u-2 [
-  local-scope
-  assume-screen 10/width, 10/height
-  # third line starts out wrapping
-  s:text <- new [1
-2
-345678
-9]
-  e:&:editor <- new-editor s, 0/left, 5/right
-  editor-render screen, e
-  screen-should-contain [
-    .          .
-    .1         .
-    .2         .
-    .3456↩     .
-    .78        .
-    .9         .
-    .┈┈┈┈┈     .
-    .          .
-  ]
-  # position cursor on screen line after the wrap and hit ctrl-u
-  assume-console [
-    left-click 4, 1  # on '8'
-    press ctrl-u
-  ]
-  run [
-    editor-event-loop screen, console, e
-    10:num/raw <- get *e, cursor-row:offset
-    11:num/raw <- get *e, cursor-column:offset
-  ]
-  screen-should-contain [
-    .          .
-    .1         .
-    .2         .
-    .8         .
-    .9         .
-    .┈┈┈┈┈     .
-    .          .
-  ]
-  # cursor moves up one screen line
-  memory-should-contain [
-    10 <- 3  # cursor-row
-    11 <- 0  # cursor-column
-  ]
-]
-
-# line wrapping twice (taking up 3 screen lines)
-scenario editor-deletes-to-start-of-wrapped-line-with-ctrl-u-3 [
-  local-scope
-  assume-screen 10/width, 10/height
-  # third line starts out wrapping
-  s:text <- new [1
-2
-3456789abcd
-e]
-  e:&:editor <- new-editor s, 0/left, 5/right
-  editor-render screen, e
-  assume-console [
-    left-click 4, 1  # on '8'
-  ]
-  editor-event-loop screen, console, e
-  screen-should-contain [
-    .          .
-    .1         .
-    .2         .
-    .3456↩     .
-    .789a↩     .
-    .bcd       .
-    .e         .
-    .┈┈┈┈┈     .
-    .          .
-  ]
-  assume-console [
-    left-click 5, 1
-    press ctrl-u
-  ]
-  run [
-    editor-event-loop screen, console, e
-    10:num/raw <- get *e, cursor-row:offset
-    11:num/raw <- get *e, cursor-column:offset
-  ]
-  screen-should-contain [
-    .          .
-    .1         .
-    .2         .
-    .cd        .
-    .e         .
-    .┈┈┈┈┈     .
-    .          .
-  ]
-  # make sure we adjusted cursor-row
-  memory-should-contain [
-    10 <- 3  # cursor-row
-    11 <- 0  # cursor-column
-  ]
-]
-
-# ctrl-k - delete text from cursor to end of line (but not the newline)
-
-scenario editor-deletes-to-end-of-line-with-ctrl-k [
-  local-scope
-  assume-screen 10/width, 5/height
-  s:text <- new [123
-456]
-  e:&:editor <- new-editor s, 0/left, 10/right
-  editor-render screen, e
-  $clear-trace
-  # start on first line, press ctrl-k
-  assume-console [
-    left-click 1, 1
-    press ctrl-k
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  # cursor deletes to end of line
-  screen-should-contain [
-    .          .
-    .1         .
-    .456       .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-  check-trace-count-for-label 9, [print-character]
-]
-
-after <handle-special-character> [
-  {
-    delete-to-end-of-line?:bool <- equal c, 11/ctrl-k
-    break-unless delete-to-end-of-line?
-    <begin-delete-to-end-of-line>
-    deleted-cells:&:duplex-list:char <- delete-to-end-of-line editor
-    <end-delete-to-end-of-line>
-    # checks if we can do a minimal render and if we can it will do a minimal render
-    go-render?:bool <- minimal-render-for-ctrl-k screen, editor, deleted-cells
-    return
-  }
-]
-
-def minimal-render-for-ctrl-k screen:&:screen, editor:&:editor, deleted-cells:&:duplex-list:char -> go-render?:bool, screen:&:screen [
-  local-scope
-  load-inputs
-  # if we deleted nothing, there's nothing to render
-  return-unless deleted-cells, false/dont-render
-  # if the line used to wrap before, give up and render the whole screen
-  curr-column:num <- get *editor, cursor-column:offset
-  num-deleted-cells:num <- length deleted-cells
-  old-row-len:num <- add curr-column, num-deleted-cells
-  left:num <- get *editor, left:offset
-  right:num <- get *editor, right:offset
-  end:num <- subtract right, left
-  wrap?:bool <- greater-or-equal old-row-len, end
-  return-if wrap?, true/go-render
-  clear-line-until screen, right
-  return false/dont-render
-]
-
-def delete-to-end-of-line editor:&:editor -> result:&:duplex-list:char, editor:&:editor [
-  local-scope
-  load-inputs
-  # compute range to delete
-  start:&:duplex-list:char <- get *editor, before-cursor:offset
-  end:&:duplex-list:char <- next start
-  {
-    at-end-of-text?:bool <- equal end, null
-    break-if at-end-of-text?
-    curr:char <- get *end, value:offset
-    at-end-of-line?:bool <- equal curr, 10/newline
-    break-if at-end-of-line?
-    end <- next end
-    loop
-  }
-  # snip it out
-  result <- next start
-  remove-between start, end
-]
-
-scenario editor-deletes-to-end-of-line-with-ctrl-k-2 [
-  local-scope
-  assume-screen 10/width, 5/height
-  s:text <- new [123
-456]
-  e:&:editor <- new-editor s, 0/left, 10/right
-  editor-render screen, e
-  $clear-trace
-  # start on second line (no newline after), press ctrl-k
-  assume-console [
-    left-click 2, 1
-    press ctrl-k
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  # cursor deletes to end of line
-  screen-should-contain [
-    .          .
-    .123       .
-    .4         .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-  check-trace-count-for-label 9, [print-character]
-]
-
-scenario editor-deletes-to-end-of-line-with-ctrl-k-3 [
-  local-scope
-  assume-screen 10/width, 5/height
-  s:text <- new [123
-456]
-  e:&:editor <- new-editor s, 0/left, 10/right
-  editor-render screen, e
-  $clear-trace
-  # start at end of line
-  assume-console [
-    left-click 1, 2
-    press ctrl-k
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  # cursor deletes just last character
-  screen-should-contain [
-    .          .
-    .12        .
-    .456       .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-  check-trace-count-for-label 8, [print-character]
-]
-
-scenario editor-deletes-to-end-of-line-with-ctrl-k-4 [
-  local-scope
-  assume-screen 10/width, 5/height
-  s:text <- new [123
-456]
-  e:&:editor <- new-editor s, 0/left, 10/right
-  editor-render screen, e
-  $clear-trace
-  # start past end of line
-  assume-console [
-    left-click 1, 3
-    press ctrl-k
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  # cursor deletes nothing
-  screen-should-contain [
-    .          .
-    .123       .
-    .456       .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-  check-trace-count-for-label 7, [print-character]
-]
-
-scenario editor-deletes-to-end-of-line-with-ctrl-k-5 [
-  local-scope
-  assume-screen 10/width, 5/height
-  s:text <- new [123
-456]
-  e:&:editor <- new-editor s, 0/left, 10/right
-  editor-render screen, e
-  $clear-trace
-  # start at end of text
-  assume-console [
-    left-click 2, 2
-    press ctrl-k
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  # cursor deletes just the final character
-  screen-should-contain [
-    .          .
-    .123       .
-    .45        .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-  check-trace-count-for-label 8, [print-character]
-]
-
-scenario editor-deletes-to-end-of-line-with-ctrl-k-6 [
-  local-scope
-  assume-screen 10/width, 5/height
-  s:text <- new [123
-456]
-  e:&:editor <- new-editor s, 0/left, 10/right
-  editor-render screen, e
-  $clear-trace
-  # start past end of text
-  assume-console [
-    left-click 2, 3
-    press ctrl-k
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  # cursor deletes nothing
-  screen-should-contain [
-    .          .
-    .123       .
-    .456       .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-  # no prints necessary
-  check-trace-count-for-label 0, [print-character]
-]
-
-scenario editor-deletes-to-end-of-wrapped-line-with-ctrl-k [
-  local-scope
-  assume-screen 10/width, 5/height
-  # create an editor with the first line wrapping to a second screen row
-  s:text <- new [1234
-567]
-  e:&:editor <- new-editor s, 0/left, 4/right
-  editor-render screen, e
-  $clear-trace
-  # delete all of the first wrapped line
-  assume-console [
-    press ctrl-k
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  # screen shows an empty unwrapped first line
-  screen-should-contain [
-    .          .
-    .          .
-    .567       .
-    .┈┈┈┈      .
-    .          .
-  ]
-  # entire screen is refreshed
-  check-trace-count-for-label 16, [print-character]
-]
-
-# takes a pointer into the doubly-linked list, scans ahead at most 'max'
-# positions until the next newline
-# returns original if no next newline
-# beware: never return null pointer.
-def before-start-of-next-line original:&:duplex-list:char, max:num -> curr:&:duplex-list:char [
-  local-scope
-  load-inputs
-  count:num <- copy 0
-  curr:&:duplex-list:char <- copy original
-  # skip the initial newline if it exists
-  {
-    c:char <- get *curr, value:offset
-    at-newline?:bool <- equal c, 10/newline
-    break-unless at-newline?
-    curr <- next curr
-    count <- add count, 1
-  }
-  {
-    return-unless curr, original
-    done?:bool <- greater-or-equal count, max
-    break-if done?
-    c:char <- get *curr, value:offset
-    at-newline?:bool <- equal c, 10/newline
-    break-if at-newline?
-    curr <- next curr
-    count <- add count, 1
-    loop
-  }
-  return-unless curr, original
-  return curr
-]
-
-# ctrl-/ - comment/uncomment current line
-
-after <handle-special-character> [
-  {
-    comment-toggle?:bool <- equal c, 31/ctrl-slash
-    break-unless comment-toggle?
-    cursor-column:num <- get *editor, cursor-column:offset
-    data:&:duplex-list:char <- get *editor, data:offset
-    <begin-insert-character>
-    before-line-start:&:duplex-list:char <- before-start-of-screen-line editor
-    line-start:&:duplex-list:char <- next before-line-start
-    commented-out?:bool <- match line-start, [#? ]  # comment prefix
-    {
-      break-unless commented-out?
-      # uncomment
-      data <- remove line-start, 3/length-comment-prefix, data
-      cursor-column <- subtract cursor-column, 3/length-comment-prefix
-      *editor <- put *editor, cursor-column:offset, cursor-column
-      go-render? <- render-line-from-start screen, editor, 3/size-of-comment-leader
-    }
-    {
-      break-if commented-out?
-      # comment
-      insert before-line-start, [#? ]
-      cursor-column <- add cursor-column, 3/length-comment-prefix
-      *editor <- put *editor, cursor-column:offset, cursor-column
-      go-render? <- render-line-from-start screen, editor, 0
-    }
-    <end-insert-character>
-    return
-  }
-]
-
-# Render just from the start of the current line, and only if it wasn't
-# wrapping before (include margin) and isn't wrapping now. Otherwise just tell
-# the caller to go-render? the entire screen.
-def render-line-from-start screen:&:screen, editor:&:editor, right-margin:num -> go-render?:bool, screen:&:screen [
-  local-scope
-  load-inputs
-  before-line-start:&:duplex-list:char <- before-start-of-screen-line editor
-  line-start:&:duplex-list:char <- next before-line-start
-  color:num <- copy 7/white
-  left:num <- get *editor, left:offset
-  cursor-row:num <- get *editor, cursor-row:offset
-  screen <- move-cursor screen, cursor-row, left
-  right:num <- get *editor, right:offset
-  end:num <- subtract right, right-margin
-  i:num <- copy 0
-  curr:&:duplex-list:char <- copy line-start
-  {
-    render-all?:bool <- greater-or-equal i, end
-    return-if render-all?, true/go-render
-    break-unless curr
-    c:char <- get *curr, value:offset
-    newline?:bool <- equal c, 10/newline
-    break-if newline?
-    color <- get-color color, c
-    print screen, c, color
-    curr <- next curr
-    i <- add i, 1
-    loop
-  }
-  clear-line-until screen, right
-  return false/dont-render
-]
-
-def before-start-of-screen-line editor:&:editor -> result:&:duplex-list:char [
-  local-scope
-  load-inputs
-  cursor:&:duplex-list:char <- get *editor, before-cursor:offset
-  {
-    next:&:duplex-list:char <- next cursor
-    break-unless next
-    cursor <- copy next
-  }
-  result <- before-previous-screen-line cursor, editor
-]
-
-scenario editor-comments-empty-line [
-  local-scope
-  assume-screen 10/width, 5/height
-  e:&:editor <- new-editor [], 0/left, 5/right
-  editor-render screen, e
-  $clear-trace
-  assume-console [
-    press ctrl-slash
-  ]
-  run [
-    editor-event-loop screen, console, e
-    4:num/raw <- get *e, cursor-row:offset
-    5:num/raw <- get *e, cursor-column:offset
-  ]
-  screen-should-contain [
-    .          .
-    .#?        .
-    .┈┈┈┈┈     .
-    .          .
-  ]
-  memory-should-contain [
-    4 <- 1
-    5 <- 3
-  ]
-  check-trace-count-for-label 5, [print-character]
-]
-
-scenario editor-comments-at-start-of-contents [
-  local-scope
-  assume-screen 10/width, 5/height
-  e:&:editor <- new-editor [ab], 0/left, 10/right
-  editor-render screen, e
-  $clear-trace
-  assume-console [
-    press ctrl-slash
-  ]
-  run [
-    editor-event-loop screen, console, e
-    4:num/raw <- get *e, cursor-row:offset
-    5:num/raw <- get *e, cursor-column:offset
-  ]
-  screen-should-contain [
-    .          .
-    .#? ab     .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-  memory-should-contain [
-    4 <- 1
-    5 <- 3
-  ]
-  check-trace-count-for-label 10, [print-character]
-]
-
-scenario editor-comments-at-end-of-contents [
-  local-scope
-  assume-screen 10/width, 5/height
-  e:&:editor <- new-editor [ab], 0/left, 10/right
-  editor-render screen, e
-  $clear-trace
-  assume-console [
-    left-click 1, 7
-    press ctrl-slash
-  ]
-  run [
-    editor-event-loop screen, console, e
-    4:num/raw <- get *e, cursor-row:offset
-    5:num/raw <- get *e, cursor-column:offset
-  ]
-  screen-should-contain [
-    .          .
-    .#? ab     .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-  memory-should-contain [
-    4 <- 1
-    5 <- 5
-  ]
-  check-trace-count-for-label 10, [print-character]
-  # toggle to uncomment
-  $clear-trace
-  assume-console [
-    press ctrl-slash
-  ]
-  run [
-    editor-event-loop screen, console, e
-    4:num/raw <- get *e, cursor-row:offset
-    5:num/raw <- get *e, cursor-column:offset
-  ]
-  screen-should-contain [
-    .          .
-    .ab        .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-  check-trace-count-for-label 10, [print-character]
-]
-
-scenario editor-comments-almost-wrapping-line [
-  local-scope
-  assume-screen 10/width, 5/height
-  # editor starts out with a non-wrapping line
-  e:&:editor <- new-editor [abcd], 0/left, 5/right
-  editor-render screen, e
-  screen-should-contain [
-    .          .
-    .abcd      .
-    .┈┈┈┈┈     .
-    .          .
-  ]
-  $clear-trace
-  # on commenting the line is now wrapped
-  assume-console [
-    left-click 1, 7
-    press ctrl-slash
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  screen-should-contain [
-    .          .
-    .#? a↩     .
-    .bcd       .
-    .┈┈┈┈┈     .
-    .          .
-  ]
-]
-
-scenario editor-uncomments-just-wrapping-line [
-  local-scope
-  assume-screen 10/width, 5/height
-  # editor starts out with a comment that wraps the line
-  e:&:editor <- new-editor [#? ab], 0/left, 5/right
-  editor-render screen, e
-  screen-should-contain [
-    .          .
-    .#? a↩     .
-    .b         .
-    .┈┈┈┈┈     .
-    .          .
-  ]
-  $clear-trace
-  # on uncommenting the line is no longer wrapped
-  assume-console [
-    left-click 1, 7
-    press ctrl-slash
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  screen-should-contain [
-    .          .
-    .ab        .
-    .┈┈┈┈┈     .
-    .          .
-  ]
-]
diff --git a/sandbox/004-programming-environment.mu b/sandbox/004-programming-environment.mu
deleted file mode 100644
index 1454144b..00000000
--- a/sandbox/004-programming-environment.mu
+++ /dev/null
@@ -1,268 +0,0 @@
-## putting the environment together out of editors
-
-def! main [
-  local-scope
-  open-console
-  clear-screen null/screen  # non-scrolling app
-  env:&:environment <- new-programming-environment null/filesystem, null/screen
-  render-all null/screen, env, render
-  event-loop null/screen, null/console, env, null/filesystem
-]
-
-container environment [
-  current-sandbox:&:editor
-]
-
-def new-programming-environment resources:&:resources, screen:&:screen, test-sandbox-editor-contents:text -> result:&:environment [
-  local-scope
-  load-inputs
-  width:num <- screen-width screen
-  result <- new environment:type
-  # sandbox editor
-  current-sandbox:&:editor <- new-editor test-sandbox-editor-contents, 0/left, width/right
-  *result <- put *result, current-sandbox:offset, current-sandbox
-  <programming-environment-initialization>
-]
-
-def event-loop screen:&:screen, console:&:console, env:&:environment, resources:&:resources -> screen:&:screen, console:&:console, env:&:environment, resources:&:resources [
-  local-scope
-  load-inputs
-  current-sandbox:&:editor <- get *env, current-sandbox:offset
-  # if we fall behind we'll stop updating the screen, but then we have to
-  # render the entire screen when we catch up.
-  # todo: test this
-  render-all-on-no-more-events?:bool <- copy false
-  {
-    # looping over each (keyboard or touch) event as it occurs
-    +next-event
-    e:event, found?:bool, quit?:bool, console <- read-event console
-    loop-unless found?
-    break-if quit?  # only in tests
-    trace 10, [app], [next-event]
-    <handle-event>
-    # check for global events that will trigger regardless of which editor has focus
-    {
-      k:num, is-keycode?:bool <- maybe-convert e:event, keycode:variant
-      break-unless is-keycode?
-      <global-keypress>
-    }
-    {
-      c:char, is-unicode?:bool <- maybe-convert e:event, text:variant
-      break-unless is-unicode?
-      <global-type>
-    }
-    # 'touch' event
-    {
-      t:touch-event, is-touch?:bool <- maybe-convert e:event, touch:variant
-      break-unless is-touch?
-      # ignore all but 'left-click' events for now
-      # todo: test this
-      touch-type:num <- get t, type:offset
-      is-left-click?:bool <- equal touch-type, 65513/mouse-left
-      loop-unless is-left-click?, +next-event
-      click-row:num <- get t, row:offset
-      click-column:num <- get t, column:offset
-      # later exceptions for non-editor touches will go here
-      <global-touch>
-      move-cursor current-sandbox, screen, t
-      screen <- update-cursor screen, current-sandbox, env
-      loop +next-event
-    }
-    # 'resize' event - redraw editor
-    # todo: test this after supporting resize in assume-console
-    {
-      r:resize-event, is-resize?:bool <- maybe-convert e:event, resize:variant
-      break-unless is-resize?
-      env, screen <- resize screen, env
-      screen <- render-all screen, env, render-without-moving-cursor
-      loop +next-event
-    }
-    # not global and not a touch event
-    {
-      render?:bool <- handle-keyboard-event screen, current-sandbox, e:event
-      # try to batch up rendering if there are more events queued up
-      render-all-on-no-more-events? <- or render-all-on-no-more-events?, render?
-      more-events?:bool <- has-more-events? console
-      {
-        break-if more-events?
-        break-unless render-all-on-no-more-events?
-        render-all-on-no-more-events? <- copy false
-        screen <- render-all screen, env, render
-      }
-      screen <- update-cursor screen, current-sandbox, env
-    }
-    loop
-  }
-]
-
-def resize screen:&:screen, env:&:environment -> env:&:environment, screen:&:screen [
-  local-scope
-  load-inputs
-  clear-screen screen  # update screen dimensions
-  width:num <- screen-width screen
-  # update sandbox editor
-  current-sandbox:&:editor <- get *env, current-sandbox:offset
-  right:num <- subtract width, 1
-  *current-sandbox <- put *current-sandbox right:offset, right
-  # reset cursor
-  *current-sandbox <- put *current-sandbox, cursor-row:offset, 1
-  *current-sandbox <- put *current-sandbox, cursor-column:offset, 0
-]
-
-# Variant of 'render' that updates cursor-row and cursor-column based on
-# before-cursor (rather than the other way around). If before-cursor moves
-# off-screen, it resets cursor-row and cursor-column.
-def render-without-moving-cursor screen:&:screen, editor:&:editor -> last-row:num, last-column:num, screen:&:screen, editor:&:editor [
-  local-scope
-  load-inputs
-  return-unless editor, 1/top, 0/left
-  left:num <- get *editor, left:offset
-  screen-height:num <- screen-height screen
-  right:num <- get *editor, right:offset
-  curr:&:duplex-list:char <- get *editor, top-of-screen:offset
-  prev:&:duplex-list:char <- copy curr  # just in case curr becomes null and we can't compute prev
-  curr <- next curr
-  color:num <- copy 7/white
-  row:num <- copy 1/top
-  column:num <- copy left
-  # save before-cursor
-  old-before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
-  # initialze cursor-row/cursor-column/before-cursor to the top of the screen
-  # by default
-  *editor <- put *editor, cursor-row:offset, row
-  *editor <- put *editor, cursor-column:offset, column
-  top-of-screen:&:duplex-list:char <- get *editor, top-of-screen:offset
-  *editor <- put *editor, before-cursor:offset, top-of-screen
-  screen <- move-cursor screen, row, column
-  {
-    +next-character
-    break-unless curr
-    off-screen?:bool <- greater-or-equal row, screen-height
-    break-if off-screen?
-    # if we find old-before-cursor still on the new resized screen, update
-    # editor.cursor-row and editor.cursor-column based on
-    # old-before-cursor
-    {
-      at-cursor?:bool <- equal old-before-cursor, prev
-      break-unless at-cursor?
-      *editor <- put *editor, cursor-row:offset, row
-      *editor <- put *editor, cursor-column:offset, column
-      *editor <- put *editor, before-cursor:offset, old-before-cursor
-    }
-    c:char <- get *curr, value:offset
-    <character-c-received>
-    {
-      # newline? move to left rather than 0
-      newline?:bool <- equal c, 10/newline
-      break-unless newline?
-      # clear rest of line in this window
-      clear-line-until screen, right
-      # skip to next line
-      row <- add row, 1
-      column <- copy left
-      screen <- move-cursor screen, row, column
-      curr <- next curr
-      prev <- next prev
-      loop +next-character
-    }
-    {
-      # at right? wrap. even if there's only one more letter left; we need
-      # room for clicking on the cursor after it.
-      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 curr
-      loop +next-character
-    }
-    print screen, c, color
-    curr <- next curr
-    prev <- next prev
-    column <- add column, 1
-    loop
-  }
-  # save first character off-screen
-  *editor <- put *editor, bottom-of-screen:offset, curr
-  *editor <- put *editor, bottom:offset, row
-  return row, column
-]
-
-type render-recipe = (recipe (address screen) (address editor) -> number number (address screen) (address editor))
-
-def render-all screen:&:screen, env:&:environment, render-editor:render-recipe -> screen:&:screen, env:&:environment [
-  local-scope
-  load-inputs
-  trace 10, [app], [render all]
-  # top menu
-  trace 11, [app], [render top menu]
-  width:num <- screen-width screen
-  draw-horizontal screen, 0, 0/left, width, 32/space, 0/black, 238/grey
-  button-start:num <- subtract width, 20
-  button-on-screen?:bool <- greater-or-equal button-start, 0
-  assert button-on-screen?, [screen too narrow for menu]
-  screen <- move-cursor screen, 0/row, button-start
-  print screen, [ run (F4) ], 255/white, 161/reddish
-  #
-  screen <- render-sandbox-side screen, env, render-editor
-  <end-render-components>  # no early returns permitted
-  #
-  current-sandbox:&:editor <- get *env, current-sandbox:offset
-  screen <- update-cursor screen, current-sandbox, env
-]
-
-# replaced in a later layer
-def render-sandbox-side screen:&:screen, env:&:environment, render-editor:render-recipe -> screen:&:screen, env:&:environment [
-  local-scope
-  load-inputs
-  trace 11, [app], [render sandboxes]
-  old-top-idx:num <- save-top-idx screen
-  current-sandbox:&:editor <- get *env, current-sandbox:offset
-  left:num <- get *current-sandbox, left:offset
-  right:num <- get *current-sandbox, right:offset
-  row:num, column:num, screen, current-sandbox <- call render-editor, screen, current-sandbox
-  # draw solid line after code (you'll see why in later layers)
-  draw-horizontal screen, row, left, right
-  row <- add row, 1
-  clear-screen-from screen, row, left, left, right
-  #
-  assert-no-scroll screen, old-top-idx
-]
-
-def update-cursor screen:&:screen, current-sandbox:&:editor, env:&:environment -> screen:&:screen [
-  local-scope
-  load-inputs
-  <update-cursor-special-cases>
-  cursor-row:num <- get *current-sandbox, cursor-row:offset
-  cursor-column:num <- get *current-sandbox, cursor-column:offset
-  screen <- move-cursor screen, cursor-row, cursor-column
-]
-
-scenario backspace-over-text [
-  local-scope
-  trace-until 100/app  # trace too long
-  assume-screen 50/width, 15/height
-  # recipes.mu is empty
-  assume-resources [
-  ]
-  # sandbox editor contains an instruction without storing outputs
-  env:&:environment <- new-programming-environment resources, screen, []
-  render-all screen, env, render
-  # run the code in the editors
-  assume-console [
-    type [a]
-    press backspace
-  ]
-  run [
-    event-loop screen, console, env, resources
-    10:num/raw <- get *screen, cursor-row:offset
-    11:num/raw <- get *screen, cursor-column:offset
-  ]
-  memory-should-contain [
-    10 <- 1
-    11 <- 0
-  ]
-]
diff --git a/sandbox/005-sandbox.mu b/sandbox/005-sandbox.mu
deleted file mode 100644
index 632a5df1..00000000
--- a/sandbox/005-sandbox.mu
+++ /dev/null
@@ -1,1081 +0,0 @@
-## 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 50/width, 15/height
-  # recipes.mu 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           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                                                 .
-    .──────────────────────────────────────────────────.
-    .                                                  .
-  ]
-  # 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           delete          .
-    .add 2, 2                                          .
-    .4                                                 .
-    .──────────────────────────────────────────────────.
-    .1   edit           copy           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
-    error?:bool <- run-sandboxes env, resources, screen
-    # F4 might update warnings and results on both sides
-    screen <- render-all screen, env, render
-    {
-      break-if error?
-      screen <- update-status screen, [                 ], 245/grey
-    }
-    screen <- update-cursor screen, current-sandbox, 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
-  # check contents of editor
-  <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>
-  {
-    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, screen:&:screen [
-  local-scope
-  load-inputs
-  in:text <- slurp resources, [lesson/recipes.mu]
-  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, 0, env
-  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
-  env:&:environment, _/optional <- next-input
-  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, env
-]
-
-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, 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, [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 50/width, 6/height
-  # recipes.mu 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           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, 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, 3  # integer division
-  buttons-wide-enough?:bool <- greater-or-equal button-width, 8
-  assert buttons-wide-enough?, [sandbox must be at least 30 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
-  delete-button-left:num <- subtract right, button-width
-  copy-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
-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 50/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)           .
-    .                                                  .
-    .──────────────────────────────────────────────────.
-    .0   edit           copy           delete          .
-    .foo                                               .
-    .4                                                 .
-    .──────────────────────────────────────────────────.
-    .                                                  .
-  ]
-  # the new sandbox should be saved to disk
-  trace-should-contain [
-    app: save sandboxes
-  ]
-  # make a change (incrementing one of the args to 'add'), then rerun
-  assume-resources [
-    [lesson/recipes.mu] <- [
-      ||
-      |recipe foo [|
-      |  local-scope|
-      |  z:num <- add 2, 3|
-      |  reply z|
-      |]|
-    ]
-  ]
-  $clear-trace
-  assume-console [
-    press F4
-  ]
-  run [
-    event-loop screen, console, env, resources
-  ]
-  # check that screen updates the result on the right
-  screen-should-contain [
-    .                               run (F4)           .
-    .                                                  .
-    .──────────────────────────────────────────────────.
-    .0   edit           copy           delete          .
-    .foo                                               .
-    .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 50/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           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]
-  ]
-]
-
-# scrolling through sandboxes
-
-scenario scrolling-down-past-bottom-of-sandbox-editor [
-  local-scope
-  trace-until 100/app  # trace too long
-  assume-screen 50/width, 20/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           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           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
-  screen-should-contain [
-    .                               run (F4)           .
-    .␣                                                 .
-    .──────────────────────────────────────────────────.
-    .0   edit           copy           delete          .
-    .add 2, 2                                          .
-    .4                                                 .
-    .──────────────────────────────────────────────────.
-    .                                                  .
-  ]
-]
-
-# page-down updates render-from to scroll sandboxes
-after <global-keypress> [
-  {
-    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
-      break-if at-end?
-      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, current-sandbox, env
-    loop +next-event
-  }
-]
-
-# update-cursor takes render-from into account
-after <update-cursor-special-cases> [
-  {
-    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' is like 'page-down': updates first-sandbox-to-render when necessary
-after <global-keypress> [
-  {
-    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, current-sandbox, 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 50/width, 20/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           delete          .
-    .add 1, 1                                          .
-    .2                                                 .
-    .──────────────────────────────────────────────────.
-    .1   edit           copy           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           delete          .
-    .add 1, 1                                          .
-    .2                                                 .
-    .──────────────────────────────────────────────────.
-    .1   edit           copy           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           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           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           delete          .
-    .add 1, 1                                          .
-    .2                                                 .
-    .──────────────────────────────────────────────────.
-    .1   edit           copy           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           delete          .
-    .add 1, 1                                          .
-    .2                                                 .
-    .──────────────────────────────────────────────────.
-    .1   edit           copy           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           delete          .
-    .add 1, 1                                          .
-    .2                                                 .
-    .──────────────────────────────────────────────────.
-    .1   edit           copy           delete          .
-    .add 2, 2                                          .
-    .4                                                 .
-    .──────────────────────────────────────────────────.
-    .                                                  .
-  ]
-]
-
-scenario scrolling-manages-sandbox-index-correctly [
-  local-scope
-  trace-until 100/app  # trace too long
-  assume-screen 50/width, 20/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           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           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           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           delete          .  # no change
-    .add 1, 1                                          .
-    .2                                                 .
-    .──────────────────────────────────────────────────.
-    .                                                  .
-  ]
-]
diff --git a/sandbox/006-sandbox-copy.mu b/sandbox/006-sandbox-copy.mu
deleted file mode 100644
index 0eae6cf7..00000000
--- a/sandbox/006-sandbox-copy.mu
+++ /dev/null
@@ -1,286 +0,0 @@
-## the 'copy' button makes it easy to duplicate a sandbox, and thence to
-## see code operate in multiple situations
-
-scenario copy-a-sandbox-to-editor [
-  local-scope
-  trace-until 50/app  # trace too long
-  assume-screen 50/width, 10/height
-  # empty recipes
-  assume-resources [
-  ]
-  env:&:environment <- new-programming-environment resources, screen, [add 1, 1]  # contents of sandbox editor
-  render-all screen, env, render
-  # run it
-  assume-console [
-    press F4
-  ]
-  event-loop screen, console, env, resources
-  screen-should-contain [
-    .                               run (F4)           .
-    .                                                  .
-    .──────────────────────────────────────────────────.
-    .0   edit           copy           delete          .
-    .add 1, 1                                          .
-    .2                                                 .
-    .──────────────────────────────────────────────────.
-    .                                                  .
-    .                                                  .
-    .                                                  .
-  ]
-  # click at left edge of 'copy' button
-  assume-console [
-    left-click 3, 19
-  ]
-  run [
-    event-loop screen, console, env
-  ]
-  # it copies into editor
-  screen-should-contain [
-    .                               run (F4)           .
-    .add 1, 1                                          .
-    .──────────────────────────────────────────────────.
-    .0   edit           copy           delete          .
-    .add 1, 1                                          .
-    .2                                                 .
-    .──────────────────────────────────────────────────.
-    .                                                  .
-    .                                                  .
-    .                                                  .
-  ]
-  # cursor should be in the right place
-  assume-console [
-    type [0]
-  ]
-  run [
-    event-loop screen, console, env, resources
-  ]
-  screen-should-contain [
-    .                               run (F4)           .
-    .0add 1, 1                                         .
-    .──────────────────────────────────────────────────.
-    .0   edit           copy           delete          .
-    .add 1, 1                                          .
-    .2                                                 .
-    .──────────────────────────────────────────────────.
-    .                                                  .
-    .                                                  .
-    .                                                  .
-  ]
-]
-
-scenario copy-a-sandbox-to-editor-2 [
-  local-scope
-  trace-until 50/app  # trace too long
-  assume-screen 50/width, 10/height
-  # empty recipes
-  assume-resources [
-  ]
-  env:&:environment <- new-programming-environment resources, screen, [add 1, 1]  # contents of sandbox editor
-  render-all screen, env, render
-  # run it
-  assume-console [
-    press F4
-  ]
-  event-loop screen, console, env, resources
-  screen-should-contain [
-    .                               run (F4)           .
-    .                                                  .
-    .──────────────────────────────────────────────────.
-    .0   edit           copy           delete          .
-    .add 1, 1                                          .
-    .2                                                 .
-    .──────────────────────────────────────────────────.
-    .                                                  .
-    .                                                  .
-    .                                                  .
-  ]
-  # click at right edge of 'copy' button (just before 'delete')
-  assume-console [
-    left-click 3, 33
-  ]
-  run [
-    event-loop screen, console, env, resources
-  ]
-  # it copies into editor
-  screen-should-contain [
-    .                               run (F4)           .
-    .add 1, 1                                          .
-    .──────────────────────────────────────────────────.
-    .0   edit           copy           delete          .
-    .add 1, 1                                          .
-    .2                                                 .
-    .──────────────────────────────────────────────────.
-    .                                                  .
-    .                                                  .
-    .                                                  .
-  ]
-  # cursor should be in the right place
-  assume-console [
-    type [0]
-  ]
-  run [
-    event-loop screen, console, env, resources
-  ]
-  screen-should-contain [
-    .                               run (F4)           .
-    .0add 1, 1                                         .
-    .──────────────────────────────────────────────────.
-    .0   edit           copy           delete          .
-    .add 1, 1                                          .
-    .2                                                 .
-    .──────────────────────────────────────────────────.
-    .                                                  .
-    .                                                  .
-    .                                                  .
-  ]
-]
-
-after <global-touch> [
-  # support 'copy' button
-  {
-    copy?:bool <- should-attempt-copy? click-row, click-column, env
-    break-unless copy?
-    copy?, env <- try-copy-sandbox click-row, env
-    break-unless copy?
-    screen <- render-sandbox-side screen, env, render
-    screen <- update-cursor screen, current-sandbox, env
-    loop +next-event
-  }
-]
-
-# some preconditions for attempting to copy a sandbox
-def should-attempt-copy? click-row:num, click-column:num, env:&:environment -> result:bool [
-  local-scope
-  load-inputs
-  # are we below the sandbox editor?
-  click-sandbox-area?:bool <- click-on-sandbox-area? click-row, env
-  return-unless click-sandbox-area?, false
-  # narrower, is the click in the columns spanning the 'copy' button?
-  first-sandbox:&:editor <- get *env, current-sandbox:offset
-  assert first-sandbox, [!!]
-  sandbox-left-margin:num <- get *first-sandbox, left:offset
-  sandbox-right-margin:num <- get *first-sandbox, right:offset
-  _, _, copy-button-left:num, copy-button-right:num, _ <- sandbox-menu-columns sandbox-left-margin, sandbox-right-margin
-  copy-button-vertical-area?:bool <- within-range? click-column, copy-button-left, copy-button-right
-  return-unless copy-button-vertical-area?, false
-  # finally, is sandbox editor empty?
-  current-sandbox:&:editor <- get *env, current-sandbox:offset
-  result <- empty-editor? current-sandbox
-]
-
-def try-copy-sandbox click-row:num, env:&:environment -> clicked-on-copy-button?:bool, env:&:environment [
-  local-scope
-  load-inputs
-  # identify the sandbox to copy, if the click was actually on the 'copy' button
-  sandbox:&:sandbox <- find-sandbox env, click-row
-  return-unless sandbox, false
-  clicked-on-copy-button? <- copy true
-  text:text <- get *sandbox, data:offset
-  current-sandbox:&:editor <- get *env, current-sandbox:offset
-  current-sandbox <- insert-text current-sandbox, text
-  # reset scroll
-  *env <- put *env, render-from:offset, -1
-]
-
-def find-sandbox env:&:environment, click-row:num -> result:&:sandbox [
-  local-scope
-  load-inputs
-  curr-sandbox:&:sandbox <- get *env, sandbox:offset
-  {
-    break-unless curr-sandbox
-    start:num <- get *curr-sandbox, starting-row-on-screen:offset
-    found?:bool <- equal click-row, start
-    return-if found?, curr-sandbox
-    curr-sandbox <- get *curr-sandbox, next-sandbox:offset
-    loop
-  }
-  return null/not-found
-]
-
-def click-on-sandbox-area? click-row:num, env:&:environment -> result:bool [
-  local-scope
-  load-inputs
-  first-sandbox:&:sandbox <- get *env, sandbox:offset
-  return-unless first-sandbox, false
-  first-sandbox-begins:num <- get *first-sandbox, starting-row-on-screen:offset
-  result <- greater-or-equal click-row, first-sandbox-begins
-]
-
-def empty-editor? editor:&:editor -> result:bool [
-  local-scope
-  load-inputs
-  head:&:duplex-list:char <- get *editor, data:offset
-  first:&:duplex-list:char <- next head
-  result <- not first
-]
-
-def within-range? x:num, low:num, high:num -> result:bool [
-  local-scope
-  load-inputs
-  not-too-far-left?:bool <- greater-or-equal x, low
-  not-too-far-right?:bool <- lesser-or-equal x, high
-  result <- and not-too-far-left? not-too-far-right?
-]
-
-scenario copy-fails-if-sandbox-editor-not-empty [
-  local-scope
-  trace-until 50/app  # trace too long
-  assume-screen 50/width, 10/height
-  # empty recipes
-  assume-resources [
-  ]
-  env:&:environment <- new-programming-environment resources, screen, [add 1, 1]  # contents of sandbox editor
-  render-all screen, env, render
-  # run it
-  assume-console [
-    press F4
-  ]
-  event-loop screen, console, env, resources
-  screen-should-contain [
-    .                               run (F4)           .
-    .                                                  .
-    .──────────────────────────────────────────────────.
-    .0   edit           copy           delete          .
-    .add 1, 1                                          .
-    .2                                                 .
-    .──────────────────────────────────────────────────.
-    .                                                  .
-  ]
-  # type something into the sandbox editor, then click on the 'copy' button
-  assume-console [
-    left-click 2, 20  # put cursor in sandbox editor
-    type [0]  # type something
-    left-click 3, 20  # click 'copy' button
-  ]
-  run [
-    event-loop screen, console, env
-  ]
-  # copy doesn't happen
-  screen-should-contain [
-    .                               run (F4)           .
-    .0                                                 .
-    .──────────────────────────────────────────────────.
-    .0   edit           copy           delete          .
-    .add 1, 1                                          .
-    .2                                                 .
-    .──────────────────────────────────────────────────.
-    .                                                  .
-  ]
-  # cursor should be in the right place
-  assume-console [
-    type [1]
-  ]
-  run [
-    event-loop screen, console, env, resources
-  ]
-  screen-should-contain [
-    .                               run (F4)           .
-    .01                                                .
-    .──────────────────────────────────────────────────.
-    .0   edit           copy           delete          .
-    .add 1, 1                                          .
-    .2                                                 .
-    .──────────────────────────────────────────────────.
-    .                                                  .
-  ]
-]
diff --git a/sandbox/007-sandbox-delete.mu b/sandbox/007-sandbox-delete.mu
deleted file mode 100644
index 01f01f42..00000000
--- a/sandbox/007-sandbox-delete.mu
+++ /dev/null
@@ -1,345 +0,0 @@
-## deleting sandboxes
-
-scenario deleting-sandboxes [
-  local-scope
-  trace-until 100/app  # trace too long
-  assume-screen 50/width, 15/height
-  assume-resources [
-  ]
-  env:&:environment <- new-programming-environment resources, screen, []
-  render-all screen, env, render
-  # run a few commands
-  assume-console [
-    type [divide-with-remainder 11, 3]
-    press F4
-    type [add 2, 2]
-    press F4
-  ]
-  event-loop screen, console, env, resources
-  screen-should-contain [
-    .                               run (F4)           .
-    .                                                  .
-    .──────────────────────────────────────────────────.
-    .0   edit           copy           delete          .
-    .add 2, 2                                          .
-    .4                                                 .
-    .──────────────────────────────────────────────────.
-    .1   edit           copy           delete          .
-    .divide-with-remainder 11, 3                       .
-    .3                                                 .
-    .2                                                 .
-    .──────────────────────────────────────────────────.
-    .                                                  .
-  ]
-  # delete second sandbox by clicking on left edge of 'delete' button
-  assume-console [
-    left-click 7, 34
-  ]
-  run [
-    event-loop screen, console, env, resources
-  ]
-  screen-should-contain [
-    .                               run (F4)           .
-    .                                                  .
-    .──────────────────────────────────────────────────.
-    .0   edit           copy           delete          .
-    .add 2, 2                                          .
-    .4                                                 .
-    .──────────────────────────────────────────────────.
-    .                                                  .
-  ]
-  # delete first sandbox by clicking at right edge of 'delete' button
-  assume-console [
-    left-click 3, 49
-  ]
-  run [
-    event-loop screen, console, env, resources
-  ]
-  screen-should-contain [
-    .                               run (F4)           .
-    .                                                  .
-    .──────────────────────────────────────────────────.
-    .                                                  .
-  ]
-]
-
-after <global-touch> [
-  # support 'delete' button
-  {
-    delete?:bool <- should-attempt-delete? click-row, click-column, env
-    break-unless delete?
-    delete?, env <- try-delete-sandbox click-row, env
-    break-unless delete?
-    screen <- render-sandbox-side screen, env, render
-    screen <- update-cursor screen, current-sandbox, env
-    loop +next-event
-  }
-]
-
-# some preconditions for attempting to delete a sandbox
-def should-attempt-delete? click-row:num, click-column:num, env:&:environment -> result:bool [
-  local-scope
-  load-inputs
-  # are we below the sandbox editor?
-  click-sandbox-area?:bool <- click-on-sandbox-area? click-row, env
-  return-unless click-sandbox-area?, false
-  # narrower, is the click in the columns spanning the 'copy' button?
-  first-sandbox:&:editor <- get *env, current-sandbox:offset
-  assert first-sandbox, [!!]
-  sandbox-left-margin:num <- get *first-sandbox, left:offset
-  sandbox-right-margin:num <- get *first-sandbox, right:offset
-  _, _, _, _, delete-button-left:num <- sandbox-menu-columns sandbox-left-margin, sandbox-right-margin
-  result <- within-range? click-column, delete-button-left, sandbox-right-margin
-]
-
-def try-delete-sandbox click-row:num, env:&:environment -> clicked-on-delete-button?:bool, env:&:environment [
-  local-scope
-  load-inputs
-  # identify the sandbox to delete, if the click was actually on the 'delete' button
-  sandbox:&:sandbox <- find-sandbox env, click-row
-  return-unless sandbox, false
-  clicked-on-delete-button? <- copy true
-  env <- delete-sandbox env, sandbox
-]
-
-def delete-sandbox env:&:environment, sandbox:&:sandbox -> env:&:environment [
-  local-scope
-  load-inputs
-  curr-sandbox:&:sandbox <- get *env, sandbox:offset
-  first-sandbox?:bool <- equal curr-sandbox, sandbox
-  {
-    # first sandbox? pop
-    break-unless first-sandbox?
-    next-sandbox:&:sandbox <- get *curr-sandbox, next-sandbox:offset
-    *env <- put *env, sandbox:offset, next-sandbox
-  }
-  {
-    # not first sandbox?
-    break-if first-sandbox?
-    prev-sandbox:&:sandbox <- copy curr-sandbox
-    curr-sandbox <- get *curr-sandbox, next-sandbox:offset
-    {
-      assert curr-sandbox, [sandbox not found! something is wrong.]
-      found?:bool <- equal curr-sandbox, sandbox
-      break-if found?
-      prev-sandbox <- copy curr-sandbox
-      curr-sandbox <- get *curr-sandbox, next-sandbox:offset
-      loop
-    }
-    # snip sandbox out of its list
-    next-sandbox:&:sandbox <- get *curr-sandbox, next-sandbox:offset
-    *prev-sandbox <- put *prev-sandbox, next-sandbox:offset, next-sandbox
-  }
-  # update sandbox count
-  sandbox-count:num <- get *env, number-of-sandboxes:offset
-  sandbox-count <- subtract sandbox-count, 1
-  *env <- put *env, number-of-sandboxes:offset, sandbox-count
-  # reset scroll if deleted sandbox was last
-  {
-    break-if next-sandbox
-    render-from:num <- get *env, render-from:offset
-    reset-scroll?:bool <- equal render-from, sandbox-count
-    break-unless reset-scroll?
-    *env <- put *env, render-from:offset, -1
-  }
-]
-
-scenario deleting-sandbox-after-scroll [
-  local-scope
-  trace-until 100/app  # trace too long
-  assume-screen 50/width, 10/height
-  # initialize environment
-  assume-resources [
-  ]
-  env:&:environment <- new-programming-environment resources, screen, []
-  render-all screen, env, render
-  # 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 page-down
-  ]
-  event-loop screen, console, env, resources
-  screen-should-contain [
-    .                               run (F4)           .
-    .──────────────────────────────────────────────────.
-    .0   edit           copy           delete          .
-    .add 1, 1                                          .
-    .2                                                 .
-    .──────────────────────────────────────────────────.
-    .1   edit           copy           delete          .
-    .add 2, 2                                          .
-    .4                                                 .
-    .──────────────────────────────────────────────────.
-  ]
-  # delete the second sandbox
-  assume-console [
-    left-click 6, 34
-  ]
-  run [
-    event-loop screen, console, env, resources
-  ]
-  # second sandbox shows in editor; scroll resets to display first sandbox
-  screen-should-contain [
-    .                               run (F4)           .
-    .──────────────────────────────────────────────────.
-    .0   edit           copy           delete          .
-    .add 1, 1                                          .
-    .2                                                 .
-    .──────────────────────────────────────────────────.
-    .                                                  .
-  ]
-]
-
-scenario deleting-top-sandbox-after-scroll [
-  local-scope
-  trace-until 100/app  # trace too long
-  assume-screen 50/width, 10/height
-  # initialize environment
-  assume-resources [
-  ]
-  env:&:environment <- new-programming-environment resources, screen, []
-  render-all screen, env, render
-  # 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 page-down
-  ]
-  event-loop screen, console, env, resources
-  screen-should-contain [
-    .                               run (F4)           .
-    .──────────────────────────────────────────────────.
-    .0   edit           copy           delete          .
-    .add 1, 1                                          .
-    .2                                                 .
-    .──────────────────────────────────────────────────.
-    .1   edit           copy           delete          .
-    .add 2, 2                                          .
-    .4                                                 .
-    .──────────────────────────────────────────────────.
-  ]
-  # delete the second sandbox
-  assume-console [
-    left-click 2, 34
-  ]
-  run [
-    event-loop screen, console, env, resources
-  ]
-  # second sandbox shows in editor; scroll resets to display first sandbox
-  screen-should-contain [
-    .                               run (F4)           .
-    .──────────────────────────────────────────────────.
-    .0   edit           copy           delete          .
-    .add 2, 2                                          .
-    .4                                                 .
-    .──────────────────────────────────────────────────.
-    .                                                  .
-  ]
-]
-
-scenario deleting-final-sandbox-after-scroll [
-  local-scope
-  trace-until 100/app  # trace too long
-  assume-screen 50/width, 10/height
-  # initialize environment
-  assume-resources [
-  ]
-  env:&:environment <- new-programming-environment resources, screen, []
-  render-all screen, env, render
-  # 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 page-down
-    press page-down
-  ]
-  event-loop screen, console, env, resources
-  screen-should-contain [
-    .                               run (F4)           .
-    .──────────────────────────────────────────────────.
-    .1   edit           copy           delete          .
-    .add 2, 2                                          .
-    .4                                                 .
-    .──────────────────────────────────────────────────.
-    .                                                  .
-  ]
-  # delete the second sandbox
-  assume-console [
-    left-click 2, 34
-  ]
-  run [
-    event-loop screen, console, env, resources
-  ]
-  # implicitly scroll up to first sandbox
-  screen-should-contain [
-    .                               run (F4)           .
-    .                                                  .
-    .──────────────────────────────────────────────────.
-    .0   edit           copy           delete          .
-    .add 1, 1                                          .
-    .2                                                 .
-    .──────────────────────────────────────────────────.
-    .                                                  .
-  ]
-]
-
-scenario deleting-updates-sandbox-count [
-  local-scope
-  trace-until 100/app  # trace too long
-  assume-screen 50/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
-  screen-should-contain [
-    .                               run (F4)           .
-    .                                                  .
-    .──────────────────────────────────────────────────.
-    .0   edit           copy           delete          .
-    .add 1, 1                                          .
-    .2                                                 .
-    .──────────────────────────────────────────────────.
-    .1   edit           copy           delete          .
-    .add 2, 2                                          .
-    .4                                                 .
-  ]
-  # delete the second sandbox, then try to scroll down twice
-  assume-console [
-    left-click 3, 34
-    press page-down
-    press page-down
-  ]
-  run [
-    event-loop screen, console, env, resources
-  ]
-  # shouldn't go past last sandbox
-  screen-should-contain [
-    .                               run (F4)           .
-    .──────────────────────────────────────────────────.
-    .0   edit           copy           delete          .
-    .add 2, 2                                          .
-    .4                                                 .
-    .──────────────────────────────────────────────────.
-    .                                                  .
-  ]
-]
diff --git a/sandbox/008-sandbox-edit.mu b/sandbox/008-sandbox-edit.mu
deleted file mode 100644
index fb3981bf..00000000
--- a/sandbox/008-sandbox-edit.mu
+++ /dev/null
@@ -1,319 +0,0 @@
-## editing sandboxes after they've been created
-
-scenario clicking-on-sandbox-edit-button-moves-it-to-editor [
-  local-scope
-  trace-until 100/app  # trace too long
-  assume-screen 50/width, 10/height
-  # empty recipes
-  assume-resources [
-  ]
-  env:&:environment <- new-programming-environment resources, screen, [add 2, 2]
-  render-all screen, env, render
-  # run it
-  assume-console [
-    press F4
-  ]
-  event-loop screen, console, env, resources
-  screen-should-contain [
-    .                               run (F4)           .
-    .                                                  .
-    .──────────────────────────────────────────────────.
-    .0   edit           copy           delete          .
-    .add 2, 2                                          .
-    .4                                                 .
-    .──────────────────────────────────────────────────.
-    .                                                  .
-  ]
-  # click at left edge of 'edit' button
-  assume-console [
-    left-click 3, 4
-  ]
-  run [
-    event-loop screen, console, env, resources
-  ]
-  # it pops back into editor
-  screen-should-contain [
-    .                               run (F4)           .
-    .add 2, 2                                          .
-    .──────────────────────────────────────────────────.
-    .                                                  .
-  ]
-  # cursor should be in the right place
-  assume-console [
-    type [0]
-  ]
-  run [
-    event-loop screen, console, env, resources
-  ]
-  screen-should-contain [
-    .                               run (F4)           .
-    .0add 2, 2                                         .
-    .──────────────────────────────────────────────────.
-    .                                                  .
-  ]
-]
-
-scenario clicking-on-sandbox-edit-button-moves-it-to-editor-2 [
-  local-scope
-  trace-until 100/app  # trace too long
-  assume-screen 50/width, 10/height
-  # empty recipes
-  assume-resources [
-  ]
-  env:&:environment <- new-programming-environment resources, screen, [add 2, 2]
-  render-all screen, env, render
-  # run it
-  assume-console [
-    press F4
-  ]
-  event-loop screen, console, env, resources
-  screen-should-contain [
-    .                               run (F4)           .
-    .                                                  .
-    .──────────────────────────────────────────────────.
-    .0   edit           copy           delete          .
-    .add 2, 2                                          .
-    .4                                                 .
-    .──────────────────────────────────────────────────.
-    .                                                  .
-  ]
-  # click at right edge of 'edit' button (just before 'copy')
-  assume-console [
-    left-click 3, 18
-  ]
-  run [
-    event-loop screen, console, env, resources
-  ]
-  # it pops back into editor
-  screen-should-contain [
-    .                               run (F4)           .
-    .add 2, 2                                          .
-    .──────────────────────────────────────────────────.
-    .                                                  .
-  ]
-  # cursor should be in the right place
-  assume-console [
-    type [0]
-  ]
-  run [
-    event-loop screen, console, env, resources
-  ]
-  screen-should-contain [
-    .                               run (F4)           .
-    .0add 2, 2                                         .
-    .──────────────────────────────────────────────────.
-    .                                                  .
-  ]
-]
-
-after <global-touch> [
-  # support 'edit' button
-  {
-    edit?:bool <- should-attempt-edit? click-row, click-column, env
-    break-unless edit?
-    edit?, env <- try-edit-sandbox click-row, env
-    break-unless edit?
-    screen <- render-sandbox-side screen, env, render
-    screen <- update-cursor screen, current-sandbox, env
-    loop +next-event
-  }
-]
-
-# some preconditions for attempting to edit a sandbox
-def should-attempt-edit? click-row:num, click-column:num, env:&:environment -> result:bool [
-  local-scope
-  load-inputs
-  # are we below the sandbox editor?
-  click-sandbox-area?:bool <- click-on-sandbox-area? click-row, env
-  return-unless click-sandbox-area?, false
-  # narrower, is the click in the columns spanning the 'edit' button?
-  first-sandbox:&:editor <- get *env, current-sandbox:offset
-  assert first-sandbox, [!!]
-  sandbox-left-margin:num <- get *first-sandbox, left:offset
-  sandbox-right-margin:num <- get *first-sandbox, right:offset
-  edit-button-left:num, edit-button-right:num, _ <- sandbox-menu-columns sandbox-left-margin, sandbox-right-margin
-  edit-button-vertical-area?:bool <- within-range? click-column, edit-button-left, edit-button-right
-  return-unless edit-button-vertical-area?, false
-  # finally, is sandbox editor empty?
-  current-sandbox:&:editor <- get *env, current-sandbox:offset
-  result <- empty-editor? current-sandbox
-]
-
-def try-edit-sandbox click-row:num, env:&:environment -> clicked-on-edit-button?:bool, env:&:environment [
-  local-scope
-  load-inputs
-  # identify the sandbox to edit, if the click was actually on the 'edit' button
-  sandbox:&:sandbox <- find-sandbox env, click-row
-  return-unless sandbox, false
-  clicked-on-edit-button? <- copy true
-  # 'edit' button = 'copy' button + 'delete' button
-  text:text <- get *sandbox, data:offset
-  current-sandbox:&:editor <- get *env, current-sandbox:offset
-  current-sandbox <- insert-text current-sandbox, text
-  env <- delete-sandbox env, sandbox
-  # reset scroll
-  *env <- put *env, render-from:offset, -1
-]
-
-scenario sandbox-with-print-can-be-edited [
-  local-scope
-  trace-until 100/app  # trace too long
-  assume-screen 50/width, 20/height
-  # empty recipes
-  assume-resources [
-  ]
-  # right editor contains a print instruction
-  env:&:environment <- new-programming-environment resources, screen, [print screen, 4]
-  render-all screen, env, render
-  # run the sandbox
-  assume-console [
-    press F4
-  ]
-  event-loop screen, console, env, resources
-  screen-should-contain [
-    .                               run (F4)           .
-    .                                                  .
-    .──────────────────────────────────────────────────.
-    .0   edit           copy           delete          .
-    .print screen, 4                                   .
-    .screen:                                           .
-    .  .4                             .                .
-    .  .                              .                .
-    .  .                              .                .
-    .  .                              .                .
-    .  .                              .                .
-    .──────────────────────────────────────────────────.
-    .                                                  .
-  ]
-  # edit the sandbox
-  assume-console [
-    left-click 3, 18
-  ]
-  run [
-    event-loop screen, console, env, resources
-  ]
-  screen-should-contain [
-    .                               run (F4)           .
-    .print screen, 4                                   .
-    .──────────────────────────────────────────────────.
-    .                                                  .
-    .                                                  .
-  ]
-]
-
-scenario editing-sandbox-after-scrolling-resets-scroll [
-  local-scope
-  trace-until 100/app  # trace too long
-  assume-screen 50/width, 20/height
-  # initialize environment
-  assume-resources [
-  ]
-  env:&:environment <- new-programming-environment resources, screen, []
-  render-all screen, env, render
-  # 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 page-down
-    press page-down
-  ]
-  event-loop screen, console, env, resources
-  screen-should-contain [
-    .                               run (F4)           .
-    .──────────────────────────────────────────────────.
-    .1   edit           copy           delete          .
-    .add 2, 2                                          .
-    .4                                                 .
-    .──────────────────────────────────────────────────.
-    .                                                  .
-  ]
-  # edit the second sandbox
-  assume-console [
-    left-click 2, 10
-  ]
-  run [
-    event-loop screen, console, env, resources
-  ]
-  # second sandbox shows in editor; scroll resets to display first sandbox
-  screen-should-contain [
-    .                               run (F4)           .
-    .add 2, 2                                          .
-    .──────────────────────────────────────────────────.
-    .0   edit           copy           delete          .
-    .add 1, 1                                          .
-    .2                                                 .
-    .──────────────────────────────────────────────────.
-    .                                                  .
-  ]
-]
-
-scenario editing-sandbox-updates-sandbox-count [
-  local-scope
-  trace-until 100/app  # trace too long
-  assume-screen 50/width, 20/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
-  screen-should-contain [
-    .                               run (F4)           .
-    .                                                  .
-    .──────────────────────────────────────────────────.
-    .0   edit           copy           delete          .
-    .add 1, 1                                          .
-    .2                                                 .
-    .──────────────────────────────────────────────────.
-    .1   edit           copy           delete          .
-  ]
-  # edit the second sandbox, then resave
-  assume-console [
-    left-click 3, 10
-    press F4
-  ]
-  run [
-    event-loop screen, console, env, resources
-  ]
-  # no change in contents
-  screen-should-contain [
-    .                               run (F4)           .
-    .                                                  .
-    .──────────────────────────────────────────────────.
-    .0   edit           copy           delete          .
-    .add 1, 1                                          .
-    .2                                                 .
-    .──────────────────────────────────────────────────.
-    .1   edit           copy           delete          .
-  ]
-  # now try to scroll past end
-  assume-console [
-    press page-down
-    press page-down
-    press page-down
-  ]
-  run [
-    event-loop screen, console, env, resources
-  ]
-  # screen should show just final sandbox with the right index (1)
-  screen-should-contain [
-    .                               run (F4)           .
-    .──────────────────────────────────────────────────.
-    .1   edit           copy           delete          .
-    .add 2, 2                                          .
-    .4                                                 .
-    .──────────────────────────────────────────────────.
-    .                                                  .
-  ]
-]
diff --git a/sandbox/009-sandbox-test.mu b/sandbox/009-sandbox-test.mu
deleted file mode 100644
index c22916a7..00000000
--- a/sandbox/009-sandbox-test.mu
+++ /dev/null
@@ -1,233 +0,0 @@
-## clicking on sandbox results to 'fix' them and turn sandboxes into tests
-
-scenario sandbox-click-on-result-toggles-color-to-green [
-  local-scope
-  trace-until 100/app  # trace too long
-  assume-screen 50/width, 20/height
-  # basic recipe
-  assume-resources [
-    [lesson/recipes.mu] <- [
-      |recipe foo [|
-      |  reply 4|
-      |]|
-    ]
-  ]
-  env:&:environment <- new-programming-environment resources, screen, [foo]
-  render-all screen, env, render
-  # run it
-  assume-console [
-    press F4
-  ]
-  event-loop screen, console, env, resources
-  screen-should-contain [
-    .                               run (F4)           .
-    .                                                  .
-    .──────────────────────────────────────────────────.
-    .0   edit           copy           delete          .
-    .foo                                               .
-    .4                                                 .
-    .──────────────────────────────────────────────────.
-    .                                                  .
-  ]
-  # click on the '4' in the result
-  $clear-trace
-  assume-console [
-    left-click 5, 21
-  ]
-  run [
-    event-loop screen, console, env, resources
-  ]
-  # color toggles to green
-  screen-should-contain-in-color 2/green, [
-    .                                                  .
-    .                                                  .
-    .                                                  .
-    .                                                  .
-    .                                                  .
-    .4                                                 .
-    .                                                  .
-  ]
-  # don't render entire sandbox side
-  check-trace-count-for-label-lesser-than 250, [print-character]  # say 5 sandbox lines
-  # cursor should remain unmoved
-  run [
-    cursor:char <- copy 9251/␣
-    print screen, cursor
-  ]
-  screen-should-contain [
-    .                               run (F4)           .
-    .␣                                                 .
-    .──────────────────────────────────────────────────.
-    .0   edit           copy           delete          .
-    .foo                                               .
-    .4                                                 .
-    .──────────────────────────────────────────────────.
-    .                                                  .
-  ]
-  # now change the result
-  assume-resources [
-    [lesson/recipes.mu] <- [
-      |recipe foo [|
-      |  reply 3|
-      |]|
-    ]
-  ]
-  # then rerun
-  assume-console [
-    press F4
-  ]
-  run [
-    event-loop screen, console, env, resources
-  ]
-  # result turns red
-  screen-should-contain-in-color 1/red, [
-    .                                                  .
-    .                                                  .
-    .                                                  .
-    .                                                  .
-    .                                                  .
-    .3                                                 .
-    .                                                  .
-  ]
-]
-
-# this requires tracking a couple more things
-container sandbox [
-  response-starting-row-on-screen:num
-  expected-response:text
-]
-
-# include expected response when saving or restoring a sandbox
-before <end-save-sandbox> [
-  {
-    expected-response:text <- get *sandbox, expected-response:offset
-    break-unless expected-response
-    filename <- append filename, [.out]
-    resources <- dump resources, filename, expected-response
-  }
-]
-
-before <end-restore-sandbox> [
-  {
-    filename <- append filename, [.out]
-    contents <- slurp resources, filename
-    break-unless contents
-    *curr <- put *curr, expected-response:offset, contents
-  }
-]
-
-# clicks on sandbox responses save it as 'expected'
-after <global-touch> [
-  # check if it's inside the output of any sandbox
-  {
-    sandbox-left-margin:num <- get *current-sandbox, left:offset
-    click-column:num <- get t, column:offset
-    on-sandbox-side?:bool <- greater-or-equal click-column, sandbox-left-margin
-    break-unless on-sandbox-side?
-    first-sandbox:&:sandbox <- get *env, sandbox:offset
-    break-unless first-sandbox
-    first-sandbox-begins:num <- get *first-sandbox, starting-row-on-screen:offset
-    click-row:num <- get t, row:offset
-    below-sandbox-editor?:bool <- greater-or-equal click-row, first-sandbox-begins
-    break-unless below-sandbox-editor?
-    # identify the sandbox whose output is being clicked on
-    sandbox:&:sandbox, sandbox-index:num <- find-click-in-sandbox-output env, click-row
-    break-unless sandbox
-    # update it
-    sandbox <- toggle-expected-response sandbox
-    # minimal update to disk
-    save-sandbox resources, sandbox, sandbox-index
-    # minimal update to screen
-    sandbox-right-margin:num <- get *current-sandbox, right:offset
-    row:num <- render-sandbox-response screen, sandbox, sandbox-left-margin, sandbox-right-margin
-    {
-      height:num <- screen-height screen
-      at-bottom?:bool <- greater-or-equal row, height
-      break-if at-bottom?
-      draw-horizontal screen, row, sandbox-left-margin, sandbox-right-margin
-    }
-    screen <- update-cursor screen, current-sandbox, env
-    loop +next-event
-  }
-]
-
-def find-click-in-sandbox-output env:&:environment, click-row:num -> sandbox:&:sandbox, sandbox-index:num [
-  local-scope
-  load-inputs
-  # assert click-row >= sandbox.starting-row-on-screen
-  sandbox:&:sandbox <- get *env, sandbox:offset
-  start:num <- get *sandbox, starting-row-on-screen:offset
-  clicked-on-sandboxes?:bool <- greater-or-equal click-row, start
-  assert clicked-on-sandboxes?, [extract-sandbox called on click to sandbox editor]
-  # while click-row < sandbox.next-sandbox.starting-row-on-screen
-  sandbox-index <- copy 0
-  {
-    next-sandbox:&:sandbox <- get *sandbox, next-sandbox:offset
-    break-unless next-sandbox
-    next-start:num <- get *next-sandbox, starting-row-on-screen:offset
-    found?:bool <- lesser-than click-row, next-start
-    break-if found?
-    sandbox <- copy next-sandbox
-    sandbox-index <- add sandbox-index, 1
-    loop
-  }
-  # return sandbox if click is in its output region
-  response-starting-row:num <- get *sandbox, response-starting-row-on-screen:offset
-  return-unless response-starting-row, null/no-click-in-sandbox-output, 0/sandbox-index
-  click-in-response?:bool <- greater-or-equal click-row, response-starting-row
-  return-unless click-in-response?, null/no-click-in-sandbox-output, 0/sandbox-index
-  return sandbox, sandbox-index
-]
-
-def toggle-expected-response sandbox:&:sandbox -> sandbox:&:sandbox [
-  local-scope
-  load-inputs
-  expected-response:text <- get *sandbox, expected-response:offset
-  {
-    # if expected-response is set, reset
-    break-unless expected-response
-    *sandbox <- put *sandbox, expected-response:offset, null
-  }
-  {
-    # if not, set expected response to the current response
-    break-if expected-response
-    response:text <- get *sandbox, response:offset
-    *sandbox <- put *sandbox, expected-response:offset, response
-  }
-]
-
-# when rendering a sandbox, color it in red/green if expected response exists
-after <render-sandbox-response> [
-  {
-    break-unless sandbox-response
-    *sandbox <- put *sandbox, response-starting-row-on-screen:offset, row
-    row <- render-sandbox-response screen, sandbox, left, right
-    jump +render-sandbox-end
-  }
-]
-
-def render-sandbox-response screen:&:screen, sandbox:&:sandbox, left:num, right:num -> row:num, screen:&:screen [
-  local-scope
-  load-inputs
-  sandbox-response:text <- get *sandbox, response:offset
-  expected-response:text <- get *sandbox, expected-response:offset
-  row:num <- get *sandbox response-starting-row-on-screen:offset
-  {
-    break-if expected-response
-    row <- render-text screen, sandbox-response, left, right, 245/grey, row
-    return
-  }
-  response-is-expected?:bool <- equal expected-response, sandbox-response
-  {
-    break-if response-is-expected?
-    row <- render-text screen, sandbox-response, left, right, 1/red, row
-  }
-  {
-    break-unless response-is-expected?:bool
-    row <- render-text screen, sandbox-response, left, right, 2/green, row
-  }
-]
-
-before <end-render-sandbox-reset-hidden> [
-  *sandbox <- put *sandbox, response-starting-row-on-screen:offset, 0
-]
diff --git a/sandbox/010-sandbox-trace.mu b/sandbox/010-sandbox-trace.mu
deleted file mode 100644
index d544dd8c..00000000
--- a/sandbox/010-sandbox-trace.mu
+++ /dev/null
@@ -1,243 +0,0 @@
-## clicking on the code typed into a sandbox toggles its trace
-
-scenario sandbox-click-on-code-toggles-app-trace [
-  local-scope
-  trace-until 100/app  # trace too long
-  assume-screen 50/width, 10/height
-  # run a stash instruction
-  assume-resources [
-  ]
-  env:&:environment <- new-programming-environment resources, screen, [stash [abc]]
-  render-all screen, env, render
-  assume-console [
-    press F4
-  ]
-  event-loop screen, console, env, resources
-  screen-should-contain [
-    .                               run (F4)           .
-    .                                                  .
-    .──────────────────────────────────────────────────.
-    .0   edit           copy           delete          .
-    .stash [abc]                                       .
-    .──────────────────────────────────────────────────.
-    .                                                  .
-  ]
-  # click on the code in the sandbox
-  assume-console [
-    left-click 4, 21
-  ]
-  run [
-    event-loop screen, console, env, resources
-    cursor:char <- copy 9251/␣
-    print screen, cursor
-  ]
-  # trace now printed and cursor shouldn't have budged
-  screen-should-contain [
-    .                               run (F4)           .
-    .␣                                                 .
-    .──────────────────────────────────────────────────.
-    .0   edit           copy           delete          .
-    .stash [abc]                                       .
-    .abc                                               .
-  ]
-  screen-should-contain-in-color 245/grey, [
-    .                                                  .
-    .                                                  .
-    .──────────────────────────────────────────────────.
-    .                                                  .
-    .                                                  .
-    .abc                                               .
-  ]
-  # click again on the same region
-  assume-console [
-    left-click 4, 25
-  ]
-  run [
-    event-loop screen, console, env, resources
-    print screen, cursor
-  ]
-  # trace hidden again
-  screen-should-contain [
-    .                               run (F4)           .
-    .␣                                                 .
-    .──────────────────────────────────────────────────.
-    .0   edit           copy           delete          .
-    .stash [abc]                                       .
-    .──────────────────────────────────────────────────.
-    .                                                  .
-  ]
-]
-
-scenario sandbox-shows-app-trace-and-result [
-  local-scope
-  trace-until 100/app  # trace too long
-  assume-screen 50/width, 10/height
-  # run a stash instruction and some code
-  assume-resources [
-  ]
-  test-sandbox:text <- new [stash [abc]
-add 2, 2]
-  env:&:environment <- new-programming-environment resources, screen, test-sandbox
-  render-all screen, env, render
-  assume-console [
-    press F4
-  ]
-  event-loop screen, console, env, resources
-  screen-should-contain [
-    .                               run (F4)           .
-    .                                                  .
-    .──────────────────────────────────────────────────.
-    .0   edit           copy           delete          .
-    .stash [abc]                                       .
-    .add 2, 2                                          .
-    .4                                                 .
-    .──────────────────────────────────────────────────.
-    .                                                  .
-  ]
-  # click on the code in the sandbox
-  assume-console [
-    left-click 4, 21
-  ]
-  run [
-    event-loop screen, console, env, resources
-  ]
-  # trace now printed above result
-  screen-should-contain [
-    .                               run (F4)           .
-    .                                                  .
-    .──────────────────────────────────────────────────.
-    .0   edit           copy           delete          .
-    .stash [abc]                                       .
-    .add 2, 2                                          .
-    .abc                                               .
-    .7 instructions run                                .
-    .4                                                 .
-    .──────────────────────────────────────────────────.
-  ]
-]
-
-scenario clicking-on-app-trace-does-nothing [
-  local-scope
-  trace-until 100/app  # trace too long
-  assume-screen 50/width, 10/height
-  assume-resources [
-  ]
-  env:&:environment <- new-programming-environment resources, screen, [stash 123456789]
-  render-all screen, env, render
-  # create and expand the trace
-  assume-console [
-    press F4
-    left-click 4, 1
-  ]
-  event-loop screen, console, env, resources
-  screen-should-contain [
-    .                               run (F4)           .
-    .                                                  .
-    .──────────────────────────────────────────────────.
-    .0   edit           copy           delete          .
-    .stash 123456789                                   .
-    .123456789                                         .
-  ]
-  # click on the stash under the edit-button region (or any of the other buttons, really)
-  assume-console [
-    left-click 5, 7
-  ]
-  run [
-    event-loop screen, console, env, resources
-  ]
-  # no change; doesn't die
-  screen-should-contain [
-    .                               run (F4)           .
-    .                                                  .
-    .──────────────────────────────────────────────────.
-    .0   edit           copy           delete          .
-    .stash 123456789                                   .
-    .123456789                                         .
-  ]
-]
-
-container sandbox [
-  trace:text
-  display-trace?:bool
-]
-
-# 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, trace:text <- run-sandboxed data
-  *sandbox <- put *sandbox, response:offset, response
-  *sandbox <- put *sandbox, screen:offset, fake-screen
-  *sandbox <- put *sandbox, trace:offset, trace
-]
-
-# clicks on sandbox code toggle its display-trace? flag
-after <global-touch> [
-  # check if it's inside the code of any sandbox
-  {
-    sandbox-left-margin:num <- get *current-sandbox, left:offset
-    click-column:num <- get t, column:offset
-    on-sandbox-side?:bool <- greater-or-equal click-column, sandbox-left-margin
-    break-unless on-sandbox-side?
-    first-sandbox:&:sandbox <- get *env, sandbox:offset
-    break-unless first-sandbox
-    first-sandbox-begins:num <- get *first-sandbox, starting-row-on-screen:offset
-    click-row:num <- get t, row:offset
-    below-sandbox-editor?:bool <- greater-or-equal click-row, first-sandbox-begins
-    break-unless below-sandbox-editor?
-    # identify the sandbox whose code is being clicked on
-    sandbox:&:sandbox <- find-click-in-sandbox-code env, click-row
-    break-unless sandbox
-    # toggle its display-trace? property
-    x:bool <- get *sandbox, display-trace?:offset
-    x <- not x
-    *sandbox <- put *sandbox, display-trace?:offset, x
-    screen <- render-sandbox-side screen, env, render
-    screen <- update-cursor screen, current-sandbox, env
-    loop +next-event
-  }
-]
-
-def find-click-in-sandbox-code env:&:environment, click-row:num -> sandbox:&:sandbox [
-  local-scope
-  load-inputs
-  # assert click-row >= sandbox.starting-row-on-screen
-  sandbox <- get *env, sandbox:offset
-  start:num <- get *sandbox, starting-row-on-screen:offset
-  clicked-on-sandboxes?:bool <- greater-or-equal click-row, start
-  assert clicked-on-sandboxes?, [extract-sandbox called on click to sandbox editor]
-  # while click-row < sandbox.next-sandbox.starting-row-on-screen
-  {
-    next-sandbox:&:sandbox <- get *sandbox, next-sandbox:offset
-    break-unless next-sandbox
-    next-start:num <- get *next-sandbox, starting-row-on-screen:offset
-    found?:bool <- lesser-than click-row, next-start
-    break-if found?
-    sandbox <- copy next-sandbox
-    loop
-  }
-  # return sandbox if click is in its code region
-  code-ending-row:num <- get *sandbox, code-ending-row-on-screen:offset
-  click-above-response?:bool <- lesser-than click-row, code-ending-row
-  start:num <- get *sandbox, starting-row-on-screen:offset
-  click-below-menu?:bool <- greater-than click-row, start
-  click-on-sandbox-code?:bool <- and click-above-response?, click-below-menu?
-  {
-    break-if click-on-sandbox-code?
-    return null/no-click-in-sandbox-output
-  }
-  return sandbox
-]
-
-# when rendering a sandbox, dump its trace before response/warning if display-trace? property is set
-after <render-sandbox-results> [
-  {
-    display-trace?:bool <- get *sandbox, display-trace?:offset
-    break-unless display-trace?
-    sandbox-trace:text <- get *sandbox, trace:offset
-    break-unless sandbox-trace  # nothing to print; move on
-    row, screen <- render-text screen, sandbox-trace, left, right, 245/grey, row
-  }
-  <render-sandbox-trace-done>
-]
diff --git a/sandbox/011-errors.mu b/sandbox/011-errors.mu
deleted file mode 100644
index 2f59d1fe..00000000
--- a/sandbox/011-errors.mu
+++ /dev/null
@@ -1,687 +0,0 @@
-## handling malformed programs
-
-container environment [
-  recipe-errors:text
-]
-
-# load code from disk, save any errors
-def! update-recipes env:&:environment, resources:&:resources, screen:&:screen -> errors-found?:bool, env:&:environment, screen:&:screen [
-  local-scope
-  load-inputs
-  in:text <- slurp resources, [lesson/recipes.mu]
-  recipe-errors:text <- reload in
-  *env <- put *env, recipe-errors:offset, recipe-errors
-  # if recipe editor has errors, stop
-  {
-    break-unless recipe-errors
-    update-status screen, [errors found     ], 1/red
-    errors-found? <- copy true
-    return
-  }
-  errors-found? <- copy false
-]
-
-before <end-render-components> [
-  trace 11, [app], [render status]
-  recipe-errors:text <- get *env, recipe-errors:offset
-  {
-    break-unless recipe-errors
-    update-status screen, [errors found     ], 1/red
-  }
-]
-
-container environment [
-  error-index:num  # index of first sandbox with an error (or -1 if none)
-]
-
-after <programming-environment-initialization> [
-  *result <- put *result, error-index:offset, -1
-]
-
-after <begin-run-sandboxes> [
-  *env <- put *env, error-index:offset, -1
-]
-
-before <end-run-sandboxes> [
-  {
-    error-index:num <- get *env, error-index:offset
-    sandboxes-completed-successfully?:bool <- equal error-index, -1
-    break-if sandboxes-completed-successfully?
-    errors-found? <- copy true
-  }
-]
-
-before <end-render-components> [
-  {
-    break-if recipe-errors
-    error-index:num <- get *env, error-index:offset
-    sandboxes-completed-successfully?:bool <- equal error-index, -1
-    break-if sandboxes-completed-successfully?
-    error-index-text:text <- to-text error-index
-    status:text <- interpolate [errors found (_)    ], error-index-text
-    update-status screen, status, 1/red
-  }
-]
-
-container sandbox [
-  errors:text
-]
-
-def! update-sandbox sandbox:&:sandbox, env:&:environment, idx:num -> sandbox:&:sandbox, env:&:environment [
-  local-scope
-  load-inputs
-  {
-    recipe-errors:text <- get *env, recipe-errors:offset
-    break-unless recipe-errors
-    *sandbox <- put *sandbox, errors:offset, recipe-errors
-    return
-  }
-  data:text <- get *sandbox, data:offset
-  response:text, errors:text, fake-screen:&:screen, trace:text, completed?:bool <- run-sandboxed data
-  *sandbox <- put *sandbox, response:offset, response
-  *sandbox <- put *sandbox, errors:offset, errors
-  *sandbox <- put *sandbox, screen:offset, fake-screen
-  *sandbox <- put *sandbox, trace:offset, trace
-  {
-    break-if errors
-    break-if completed?
-    errors <- new [took too long!
-]
-    *sandbox <- put *sandbox, errors:offset, errors
-  }
-  {
-    break-unless errors
-    error-index:num <- get *env, error-index:offset
-    error-not-set?:bool <- equal error-index, -1
-    break-unless error-not-set?
-    *env <- put *env, error-index:offset, idx
-  }
-]
-
-# make sure we render any trace
-after <render-sandbox-trace-done> [
-  {
-    sandbox-errors:text <- get *sandbox, errors:offset
-    break-unless sandbox-errors
-    *sandbox <- put *sandbox, response-starting-row-on-screen:offset, 0  # no response
-    {
-      break-unless env
-      recipe-errors:text <- get *env, recipe-errors:offset
-      row, screen <- render-text screen, recipe-errors, left, right, 1/red, row
-    }
-    row, screen <- render-text screen, sandbox-errors, left, right, 1/red, row
-    # don't try to print anything more for this sandbox
-    jump +render-sandbox-end
-  }
-]
-
-scenario run-shows-errors-in-get [
-  local-scope
-  trace-until 100/app  # trace too long
-  assume-screen 50/width, 20/height
-  assume-resources [
-    [lesson/recipes.mu] <- [
-      |recipe foo [|
-      |  get 123:num, foo:offset|
-      |]|
-    ]
-  ]
-  env:&:environment <- new-programming-environment resources, screen, [foo]
-  render-all screen, env, render
-  screen-should-contain [
-    .                               run (F4)           .
-    .foo                                               .
-    .──────────────────────────────────────────────────.
-    .                                                  .
-  ]
-  assume-console [
-    press F4
-  ]
-  run [
-    event-loop screen, console, env, resources
-  ]
-  screen-should-contain [
-    .  errors found                 run (F4)           .
-    .                                                  .
-    .──────────────────────────────────────────────────.
-    .0   edit           copy           delete          .
-    .foo                                               .
-    .foo: unknown element 'foo' in container 'number'  .
-    .foo: first ingredient of 'get' should be a contai↩.
-    .ner, but got '123:num'                            .
-  ]
-  screen-should-contain-in-color 1/red, [
-    .  errors found                                    .
-    .                                                  .
-  ]
-]
-
-scenario run-updates-status-with-first-erroneous-sandbox [
-  local-scope
-  trace-until 100/app  # trace too long
-  assume-screen 50/width, 20/height
-  assume-resources [
-  ]
-  env:&:environment <- new-programming-environment resources, screen, []
-  render-all screen, env, render
-  assume-console [
-    # create invalid sandbox 1
-    type [get foo, x:offset]
-    press F4
-    # create invalid sandbox 0
-    type [get foo, x:offset]
-    press F4
-  ]
-  run [
-    event-loop screen, console, env, resources
-  ]
-  # status line shows that error is in first sandbox
-  screen-should-contain [
-    .  errors found (0)             run (F4)           .
-  ]
-]
-
-scenario run-updates-status-with-first-erroneous-sandbox-2 [
-  local-scope
-  trace-until 100/app  # trace too long
-  assume-screen 50/width, 20/height
-  assume-resources [
-  ]
-  env:&:environment <- new-programming-environment resources, screen, []
-  render-all screen, env, render
-  assume-console [
-    # create invalid sandbox 2
-    type [get foo, x:offset]
-    press F4
-    # create invalid sandbox 1
-    type [get foo, x:offset]
-    press F4
-    # create valid sandbox 0
-    type [add 2, 2]
-    press F4
-  ]
-  run [
-    event-loop screen, console, env, resources
-  ]
-  # status line shows that error is in second sandbox
-  screen-should-contain [
-    .  errors found (1)             run (F4)           .
-  ]
-]
-
-scenario run-hides-errors-from-past-sandboxes [
-  local-scope
-  trace-until 100/app  # trace too long
-  assume-screen 50/width, 20/height
-  assume-resources [
-  ]
-  env:&:environment <- new-programming-environment resources, screen, [get foo, x:offset]  # invalid
-  render-all screen, env, render
-  assume-console [
-    press F4  # generate error
-  ]
-  event-loop screen, console, env, resources
-  assume-console [
-    left-click 3, 10
-    press ctrl-k
-    type [add 2, 2]  # valid code
-    press F4  # update sandbox
-  ]
-  run [
-    event-loop screen, console, env, resources
-  ]
-  # error should disappear
-  screen-should-contain [
-    .                               run (F4)           .
-    .                                                  .
-    .──────────────────────────────────────────────────.
-    .0   edit           copy           delete          .
-    .add 2, 2                                          .
-    .4                                                 .
-    .──────────────────────────────────────────────────.
-    .                                                  .
-  ]
-]
-
-scenario run-updates-errors-for-shape-shifting-recipes [
-  local-scope
-  trace-until 100/app  # trace too long
-  assume-screen 50/width, 20/height
-  # define a shape-shifting recipe with an error
-  assume-resources [
-    [lesson/recipes.mu] <- [
-      |recipe foo x:_elem -> z:_elem [|
-      |  local-scope|
-      |  load-ingredients|
-      |  y:&:num <- copy null|
-      |  z <- add x, y|
-      |]|
-    ]
-  ]
-  env:&:environment <- new-programming-environment resources, screen, [foo 2]
-  render-all screen, env, render
-  assume-console [
-    press F4
-  ]
-  event-loop screen, console, env, resources
-  screen-should-contain [
-    .  errors found (0)             run (F4)           .
-    .                                                  .
-    .──────────────────────────────────────────────────.
-    .0   edit           copy           delete          .
-    .foo 2                                             .
-    .foo_2: 'add' requires number ingredients, but got↩.
-    . 'y'                                              .
-    .──────────────────────────────────────────────────.
-    .                                                  .
-  ]
-  # now rerun everything
-  assume-console [
-    press F4
-  ]
-  run [
-    event-loop screen, console, env, resources
-  ]
-  # error should remain unchanged
-  screen-should-contain [
-    .  errors found (0)             run (F4)           .
-    .                                                  .
-    .──────────────────────────────────────────────────.
-    .0   edit           copy           delete          .
-    .foo 2                                             .
-    .foo_3: 'add' requires number ingredients, but got↩.
-    . 'y'                                              .
-    .──────────────────────────────────────────────────.
-    .                                                  .
-  ]
-]
-
-scenario run-avoids-spurious-errors-on-reloading-shape-shifting-recipes [
-  local-scope
-  trace-until 100/app  # trace too long
-  assume-screen 50/width, 20/height
-  # overload a well-known shape-shifting recipe
-  assume-resources [
-    [lesson/recipes.mu] <- [
-      |recipe length l:&:list:_elem -> n:num [|
-      |]|
-    ]
-  ]
-  # call code that uses other variants of it, but not it itself
-  test-sandbox:text <- new [x:&:list:num <- copy null
-to-text x]
-  env:&:environment <- new-programming-environment resources, screen, test-sandbox
-  render-all screen, env, render
-  # run it once
-  assume-console [
-    press F4
-  ]
-  event-loop screen, console, env, resources
-  # no errors anywhere on screen (can't check anything else, since to-text will return an address)
-  screen-should-contain-in-color 1/red, [
-    .                                                  .
-    .                                                  .
-    .                                                  .
-    .                                                  .
-    .             <-                                   .
-    .                                                  .
-    .                                                  .
-    .                                                  .
-    .                                                  .
-  ]
-  # rerun everything
-  assume-console [
-    press F4
-  ]
-  run [
-    event-loop screen, console, env, resources
-  ]
-  # still no errors
-  screen-should-contain-in-color 1/red, [
-    .                                                  .
-    .                                                  .
-    .                                                  .
-    .                                                  .
-    .             <-                                   .
-    .                                                  .
-    .                                                  .
-    .                                                  .
-    .                                                  .
-  ]
-]
-
-scenario run-shows-missing-type-errors [
-  local-scope
-  trace-until 100/app  # trace too long
-  assume-screen 50/width, 20/height
-  assume-resources [
-    [lesson/recipes.mu] <- [
-      |recipe foo [|
-      |  x <- copy 0|
-      |]|
-    ]
-  ]
-  env:&:environment <- new-programming-environment resources, screen, [foo]
-  render-all screen, env, render
-  assume-console [
-    press F4
-  ]
-  run [
-    event-loop screen, console, env, resources
-  ]
-  screen-should-contain [
-    .  errors found                 run (F4)           .
-    .                                                  .
-    .──────────────────────────────────────────────────.
-    .0   edit           copy           delete          .
-    .foo                                               .
-    .foo: missing type for 'x' in 'x <- copy 0'        .
-  ]
-]
-
-scenario run-shows-unbalanced-bracket-errors [
-  local-scope
-  trace-until 100/app  # trace too long
-  assume-screen 50/width, 20/height
-  # recipe is incomplete (unbalanced '[')
-  assume-resources [
-    [lesson/recipes.mu] <- [
-      |recipe foo \\\[|
-      |  x <- copy 0|
-    ]
-  ]
-  env:&:environment <- new-programming-environment resources, screen, [foo]
-  render-all screen, env, render
-  assume-console [
-    press F4
-  ]
-  run [
-    event-loop screen, console, env, resources
-  ]
-  screen-should-contain [
-    .  errors found                 run (F4)           .
-    .                                                  .
-    .──────────────────────────────────────────────────.
-    .0   edit           copy           delete          .
-    .foo                                               .
-    .9: unbalanced '\\[' for recipe                      .
-    .9: unbalanced '\\[' for recipe                      .
-    .──────────────────────────────────────────────────.
-    .                                                  .
-  ]
-]
-
-scenario run-shows-get-on-non-container-errors [
-  local-scope
-  trace-until 100/app  # trace too long
-  assume-screen 50/width, 20/height
-  assume-resources [
-    [lesson/recipes.mu] <- [
-      |recipe foo [|
-      |  local-scope|
-      |  x:&:point <- new point:type|
-      |  get x:&:point, 1:offset|
-      |]|
-    ]
-  ]
-  env:&:environment <- new-programming-environment resources, screen, [foo]
-  render-all screen, env, render
-  assume-console [
-    press F4
-  ]
-  run [
-    event-loop screen, console, env, resources
-  ]
-  screen-should-contain [
-    .  errors found                 run (F4)           .
-    .                                                  .
-    .──────────────────────────────────────────────────.
-    .0   edit           copy           delete          .
-    .foo                                               .
-    .foo: first ingredient of 'get' should be a contai↩.
-    .ner, but got 'x:&:point'                          .
-  ]
-]
-
-scenario run-shows-non-literal-get-argument-errors [
-  local-scope
-  trace-until 100/app  # trace too long
-  assume-screen 50/width, 20/height
-  assume-resources [
-    [lesson/recipes.mu] <- [
-      |recipe foo [|
-      |  local-scope|
-      |  x:num <- copy 0|
-      |  y:&:point <- new point:type|
-      |  get *y:&:point, x:num|
-      |]|
-    ]
-  ]
-  env:&:environment <- new-programming-environment resources, screen, [foo]
-  render-all screen, env, render
-  assume-console [
-    press F4
-  ]
-  run [
-    event-loop screen, console, env, resources
-  ]
-  screen-should-contain [
-    .  errors found                 run (F4)           .
-    .                                                  .
-    .──────────────────────────────────────────────────.
-    .0   edit           copy           delete          .
-    .foo                                               .
-    .foo: second ingredient of 'get' should have type ↩.
-    .'offset', but got 'x:num'                         .
-  ]
-]
-
-scenario run-shows-errors-every-time [
-  local-scope
-  trace-until 100/app  # trace too long
-  assume-screen 50/width, 20/height
-  # try to run a file with an error
-  assume-resources [
-    [lesson/recipes.mu] <- [
-      |recipe foo [|
-      |  local-scope|
-      |  x:num <- copy y:num|
-      |]|
-    ]
-  ]
-  env:&:environment <- new-programming-environment resources, screen, [foo]
-  render-all screen, env, render
-  assume-console [
-    press F4
-  ]
-  event-loop screen, console, env, resources
-  screen-should-contain [
-    .  errors found                 run (F4)           .
-    .                                                  .
-    .──────────────────────────────────────────────────.
-    .0   edit           copy           delete          .
-    .foo                                               .
-    .foo: tried to read ingredient 'y' in 'x:num <- co↩.
-    .py y:num' but it hasn't been written to yet       .
-  ]
-  # rerun the file, check for the same error
-  assume-console [
-    press F4
-  ]
-  run [
-    event-loop screen, console, env, resources
-  ]
-  screen-should-contain [
-    .  errors found                 run (F4)           .
-    .                                                  .
-    .──────────────────────────────────────────────────.
-    .0   edit           copy           delete          .
-    .foo                                               .
-    .foo: tried to read ingredient 'y' in 'x:num <- co↩.
-    .py y:num' but it hasn't been written to yet       .
-  ]
-]
-
-scenario run-instruction-and-print-errors [
-  local-scope
-  trace-until 100/app  # trace too long
-  assume-screen 50/width, 15/height
-  assume-resources [
-  ]
-  # editor contains an illegal instruction
-  env:&:environment <- new-programming-environment resources, screen, [get 1:&:point, 1:offset]
-  render-all screen, env, render
-  assume-console [
-    press F4
-  ]
-  run [
-    event-loop screen, console, env, resources
-  ]
-  # check that screen prints error message in red
-  screen-should-contain [
-    .  errors found (0)             run (F4)           .
-    .                                                  .
-    .──────────────────────────────────────────────────.
-    .0   edit           copy           delete          .
-    .get 1:&:point, 1:offset                           .
-    .first ingredient of 'get' should be a container, ↩.
-    .but got '1:&:point'                               .
-    .──────────────────────────────────────────────────.
-    .                                                  .
-  ]
-  screen-should-contain-in-color 1/red, [
-    .  errors found (0)                                .
-    .                                                  .
-    .                                                  .
-    .                                                  .
-    .                                                  .
-    .first ingredient of 'get' should be a container,  .
-    .but got '1:&:point'                               .
-    .                                                  .
-    .                                                  .
-  ]
-]
-
-scenario run-instruction-and-print-errors-only-once [
-  local-scope
-  trace-until 100/app  # trace too long
-  assume-screen 50/width, 10/height
-  assume-resources [
-  ]
-  # editor contains an illegal instruction
-  env:&:environment <- new-programming-environment resources, screen, [get 1234:num, foo:offset]
-  render-all screen, env, render
-  # run the code in the editors multiple times
-  assume-console [
-    press F4
-    press F4
-  ]
-  run [
-    event-loop screen, console, env, resources
-  ]
-  # check that screen prints error message just once
-  screen-should-contain [
-    .  errors found (0)             run (F4)           .
-    .                                                  .
-    .──────────────────────────────────────────────────.
-    .0   edit           copy           delete          .
-    .get 1234:num, foo:offset                          .
-    .unknown element 'foo' in container 'number'       .
-    .first ingredient of 'get' should be a container, ↩.
-    .but got '1234:num'                                .
-    .──────────────────────────────────────────────────.
-    .                                                  .
-  ]
-]
-
-scenario sandbox-can-handle-infinite-loop [
-  local-scope
-  trace-until 100/app  # trace too long
-  assume-screen 50/width, 20/height
-  assume-resources [
-  ]
-  # editor contains an infinite loop
-  test-sandbox:text <- new [{
-loop
-}]
-  env:&:environment <- new-programming-environment resources, screen, test-sandbox
-  render-all screen, env, render
-  # run the sandbox
-  assume-console [
-    press F4
-  ]
-  run [
-    event-loop screen, console, env, resources
-  ]
-  screen-should-contain [
-    .  errors found (0)             run (F4)           .
-    .                                                  .
-    .──────────────────────────────────────────────────.
-    .0   edit           copy           delete          .
-    .{                                                 .
-    .loop                                              .
-    .}                                                 .
-    .took too long!                                    .
-    .──────────────────────────────────────────────────.
-    .                                                  .
-  ]
-]
-
-scenario sandbox-with-errors-shows-trace [
-  local-scope
-  trace-until 100/app  # trace too long
-  assume-screen 50/width, 20/height
-  # generate a stash and a error
-  assume-resources [
-    [lesson/recipes.mu] <- [
-      |recipe foo [|
-      |  local-scope|
-      |  a:num <- next-ingredient|
-      |  b:num <- next-ingredient|
-      |  stash [dividing by], b|
-      |  _, c:num <- divide-with-remainder a, b|
-      |  reply b|
-      |]|
-    ]
-  ]
-  env:&:environment <- new-programming-environment resources, screen, [foo 4, 0]
-  render-all screen, env, render
-  # run
-  assume-console [
-    press F4
-  ]
-  event-loop screen, console, env, resources
-  # screen prints error message
-  screen-should-contain [
-    .  errors found (0)             run (F4)           .
-    .                                                  .
-    .──────────────────────────────────────────────────.
-    .0   edit           copy           delete          .
-    .foo 4, 0                                          .
-    .foo: divide by zero in '_, c:num <- divide-with-r↩.
-    .emainder a, b'                                    .
-    .──────────────────────────────────────────────────.
-    .                                                  .
-  ]
-  # click on the call in the sandbox
-  assume-console [
-    left-click 4, 15
-  ]
-  run [
-    event-loop screen, console, env, resources
-  ]
-  # screen should expand trace
-  screen-should-contain [
-    .  errors found (0)             run (F4)           .
-    .                                                  .
-    .──────────────────────────────────────────────────.
-    .0   edit           copy           delete          .
-    .foo 4, 0                                          .
-    .dividing by 0                                     .
-    .14 instructions run                               .
-    .foo: divide by zero in '_, c:num <- divide-with-r↩.
-    .emainder a, b'                                    .
-    .──────────────────────────────────────────────────.
-    .                                                  .
-  ]
-]
diff --git a/sandbox/012-editor-undo.mu b/sandbox/012-editor-undo.mu
deleted file mode 100644
index 69afd207..00000000
--- a/sandbox/012-editor-undo.mu
+++ /dev/null
@@ -1,1907 +0,0 @@
-## undo/redo
-
-# for every undoable event, create a type of *operation* that contains all the
-# information needed to reverse it
-exclusive-container operation [
-  typing:insert-operation
-  move:move-operation
-  delete:delete-operation
-]
-
-container insert-operation [
-  before-row:num
-  before-column:num
-  before-top-of-screen:&:duplex-list:char
-  after-row:num
-  after-column:num
-  after-top-of-screen:&:duplex-list:char
-  # inserted text is from 'insert-from' until 'insert-until'; list doesn't have to terminate
-  insert-from:&:duplex-list:char
-  insert-until:&:duplex-list:char
-  tag:num  # event causing this operation; might be used to coalesce runs of similar events
-    # 0: no coalesce (enter+indent)
-    # 1: regular alphanumeric characters
-]
-
-container move-operation [
-  before-row:num
-  before-column:num
-  before-top-of-screen:&:duplex-list:char
-  after-row:num
-  after-column:num
-  after-top-of-screen:&:duplex-list:char
-  tag:num  # event causing this operation; might be used to coalesce runs of similar events
-    # 0: no coalesce (touch events, etc)
-    # 1: left arrow
-    # 2: right arrow
-    # 3: up arrow
-    # 4: down arrow
-]
-
-container delete-operation [
-  before-row:num
-  before-column:num
-  before-top-of-screen:&:duplex-list:char
-  after-row:num
-  after-column:num
-  after-top-of-screen:&:duplex-list:char
-  deleted-text:&:duplex-list:char
-  delete-from:&:duplex-list:char
-  delete-until:&:duplex-list:char
-  tag:num  # event causing this operation; might be used to coalesce runs of similar events
-    # 0: no coalesce (ctrl-k, ctrl-u)
-    # 1: backspace
-    # 2: delete
-]
-
-# every editor accumulates a list of operations to undo/redo
-container editor [
-  undo:&:list:&:operation
-  redo:&:list:&:operation
-]
-
-# ctrl-z - undo operation
-after <handle-special-character> [
-  {
-    undo?:bool <- equal c, 26/ctrl-z
-    break-unless undo?
-    undo:&:list:&:operation <- get *editor, undo:offset
-    break-unless undo
-    op:&:operation <- first undo
-    undo <- rest undo
-    *editor <- put *editor, undo:offset, undo
-    redo:&:list:&:operation <- get *editor, redo:offset
-    redo <- push op, redo
-    *editor <- put *editor, redo:offset, redo
-    <handle-undo>
-    return true/go-render
-  }
-]
-
-# ctrl-y - redo operation
-after <handle-special-character> [
-  {
-    redo?:bool <- equal c, 25/ctrl-y
-    break-unless redo?
-    redo:&:list:&:operation <- get *editor, redo:offset
-    break-unless redo
-    op:&:operation <- first redo
-    redo <- rest redo
-    *editor <- put *editor, redo:offset, redo
-    undo:&:list:&:operation <- get *editor, undo:offset
-    undo <- push op, undo
-    *editor <- put *editor, undo:offset, undo
-    <handle-redo>
-    return true/go-render
-  }
-]
-
-# undo typing
-
-scenario editor-can-undo-typing [
-  local-scope
-  # create an editor and type a character
-  assume-screen 10/width, 5/height
-  e:&:editor <- new-editor [], 0/left, 10/right
-  editor-render screen, e
-  assume-console [
-    type [0]
-  ]
-  editor-event-loop screen, console, e
-  # undo
-  assume-console [
-    press ctrl-z
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  # character should be gone
-  screen-should-contain [
-    .          .
-    .          .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-  # cursor should be in the right place
-  assume-console [
-    type [1]
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  screen-should-contain [
-    .          .
-    .1         .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-]
-
-# save operation to undo
-after <begin-insert-character> [
-  top-before:&:duplex-list:char <- get *editor, top-of-screen:offset
-  cursor-before:&:duplex-list:char <- get *editor, before-cursor:offset
-]
-before <end-insert-character> [
-  top-after:&:duplex-list:char <- get *editor, top-of-screen:offset
-  cursor-row:num <- get *editor, cursor-row:offset
-  cursor-column:num <- get *editor, cursor-column:offset
-  undo:&:list:&:operation <- get *editor, undo:offset
-  {
-    # if previous operation was an insert, coalesce this operation with it
-    break-unless undo
-    op:&:operation <- first undo
-    typing:insert-operation, is-insert?:bool <- maybe-convert *op, typing:variant
-    break-unless is-insert?
-    previous-coalesce-tag:num <- get typing, tag:offset
-    break-unless previous-coalesce-tag
-    before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
-    insert-until:&:duplex-list:char <- next before-cursor
-    typing <- put typing, insert-until:offset, insert-until
-    typing <- put typing, after-row:offset, cursor-row
-    typing <- put typing, after-column:offset, cursor-column
-    typing <- put typing, after-top-of-screen:offset, top-after
-    *op <- merge 0/insert-operation, typing
-    break +done-adding-insert-operation
-  }
-  # if not, create a new operation
-  insert-from:&:duplex-list:char <- next cursor-before
-  insert-to:&:duplex-list:char <- next insert-from
-  op:&:operation <- new operation:type
-  *op <- merge 0/insert-operation, save-row/before, save-column/before, top-before, cursor-row/after, cursor-column/after, top-after, insert-from, insert-to, 1/coalesce
-  editor <- add-operation editor, op
-  +done-adding-insert-operation
-]
-
-# enter operations never coalesce with typing before or after
-after <begin-insert-enter> [
-  cursor-row-before:num <- copy cursor-row
-  cursor-column-before:num <- copy cursor-column
-  top-before:&:duplex-list:char <- get *editor, top-of-screen:offset
-  cursor-before:&:duplex-list:char <- get *editor, before-cursor:offset
-]
-before <end-insert-enter> [
-  top-after:&:duplex-list:char <- get *editor, top-of-screen:offset
-  cursor-row:num <- get *editor, cursor-row:offset
-  cursor-column:num <- get *editor, cursor-row:offset
-  # never coalesce
-  insert-from:&:duplex-list:char <- next cursor-before
-  before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
-  insert-to:&:duplex-list:char <- next before-cursor
-  op:&:operation <- new operation:type
-  *op <- merge 0/insert-operation, cursor-row-before, cursor-column-before, top-before, cursor-row/after, cursor-column/after, top-after, insert-from, insert-to, 0/never-coalesce
-  editor <- add-operation editor, op
-]
-
-# Everytime you add a new operation to the undo stack, be sure to clear the
-# redo stack, because it's now obsolete.
-# Beware: since we're counting cursor moves as operations, this means just
-# moving the cursor can lose work on the undo stack.
-def add-operation editor:&:editor, op:&:operation -> editor:&:editor [
-  local-scope
-  load-inputs
-  undo:&:list:&:operation <- get *editor, undo:offset
-  undo <- push op undo
-  *editor <- put *editor, undo:offset, undo
-  redo:&:list:&:operation <- get *editor, redo:offset
-  redo <- copy null
-  *editor <- put *editor, redo:offset, redo
-]
-
-after <handle-undo> [
-  {
-    typing:insert-operation, is-insert?:bool <- maybe-convert *op, typing:variant
-    break-unless is-insert?
-    start:&:duplex-list:char <- get typing, insert-from:offset
-    end:&:duplex-list:char <- get typing, insert-until:offset
-    # assert cursor-row/cursor-column/top-of-screen match after-row/after-column/after-top-of-screen
-    before-cursor:&:duplex-list:char <- prev start
-    *editor <- put *editor, before-cursor:offset, before-cursor
-    remove-between before-cursor, end
-    cursor-row <- get typing, before-row:offset
-    *editor <- put *editor, cursor-row:offset, cursor-row
-    cursor-column <- get typing, before-column:offset
-    *editor <- put *editor, cursor-column:offset, cursor-column
-    top:&:duplex-list:char <- get typing, before-top-of-screen:offset
-    *editor <- put *editor, top-of-screen:offset, top
-  }
-]
-
-scenario editor-can-undo-typing-multiple [
-  local-scope
-  # create an editor and type multiple characters
-  assume-screen 10/width, 5/height
-  e:&:editor <- new-editor [], 0/left, 10/right
-  editor-render screen, e
-  assume-console [
-    type [012]
-  ]
-  editor-event-loop screen, console, e
-  # undo
-  assume-console [
-    press ctrl-z
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  # all characters must be gone
-  screen-should-contain [
-    .          .
-    .          .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-]
-
-scenario editor-can-undo-typing-multiple-2 [
-  local-scope
-  # create an editor with some text
-  assume-screen 10/width, 5/height
-  e:&:editor <- new-editor [a], 0/left, 10/right
-  editor-render screen, e
-  # type some characters
-  assume-console [
-    type [012]
-  ]
-  editor-event-loop screen, console, e
-  screen-should-contain [
-    .          .
-    .012a      .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-  # undo
-  assume-console [
-    press ctrl-z
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  # back to original text
-  screen-should-contain [
-    .          .
-    .a         .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-  # cursor should be in the right place
-  assume-console [
-    type [3]
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  screen-should-contain [
-    .          .
-    .3a        .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-]
-
-scenario editor-can-undo-typing-enter [
-  local-scope
-  # create an editor with some text
-  assume-screen 10/width, 5/height
-  e:&:editor <- new-editor [  abc], 0/left, 10/right
-  editor-render screen, e
-  # new line
-  assume-console [
-    left-click 1, 8
-    press enter
-  ]
-  editor-event-loop screen, console, e
-  screen-should-contain [
-    .          .
-    .  abc     .
-    .          .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-  # line is indented
-  3:num/raw <- get *e, cursor-row:offset
-  4:num/raw <- get *e, cursor-column:offset
-  memory-should-contain [
-    3 <- 2
-    4 <- 2
-  ]
-  # undo
-  assume-console [
-    press ctrl-z
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  3:num/raw <- get *e, cursor-row:offset
-  4:num/raw <- get *e, cursor-column:offset
-  memory-should-contain [
-    3 <- 1
-    4 <- 5
-  ]
-  # back to original text
-  screen-should-contain [
-    .          .
-    .  abc     .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-  # cursor should be at end of line
-  assume-console [
-    type [1]
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  screen-should-contain [
-    .          .
-    .  abc1    .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-]
-
-# redo typing
-
-scenario editor-redo-typing [
-  local-scope
-  # create an editor, type something, undo
-  assume-screen 10/width, 5/height
-  e:&:editor <- new-editor [a], 0/left, 10/right
-  editor-render screen, e
-  assume-console [
-    type [012]
-    press ctrl-z
-  ]
-  editor-event-loop screen, console, e
-  screen-should-contain [
-    .          .
-    .a         .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-  # redo
-  assume-console [
-    press ctrl-y
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  # all characters must be back
-  screen-should-contain [
-    .          .
-    .012a      .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-  # cursor should be in the right place
-  assume-console [
-    type [3]
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  screen-should-contain [
-    .          .
-    .0123a     .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-]
-
-after <handle-redo> [
-  {
-    typing:insert-operation, is-insert?:bool <- maybe-convert *op, typing:variant
-    break-unless is-insert?
-    before-cursor <- get *editor, before-cursor:offset
-    insert-from:&:duplex-list:char <- get typing, insert-from:offset  # ignore insert-to because it's already been spliced away
-    # assert insert-to matches next(before-cursor)
-    splice before-cursor, insert-from
-    # assert cursor-row/cursor-column/top-of-screen match after-row/after-column/after-top-of-screen
-    cursor-row <- get typing, after-row:offset
-    *editor <- put *editor, cursor-row:offset, cursor-row
-    cursor-column <- get typing, after-column:offset
-    *editor <- put *editor, cursor-column:offset, cursor-column
-    top:&:duplex-list:char <- get typing, after-top-of-screen:offset
-    *editor <- put *editor, top-of-screen:offset, top
-  }
-]
-
-scenario editor-redo-typing-empty [
-  local-scope
-  # create an editor, type something, undo
-  assume-screen 10/width, 5/height
-  e:&:editor <- new-editor [], 0/left, 10/right
-  editor-render screen, e
-  assume-console [
-    type [012]
-    press ctrl-z
-  ]
-  editor-event-loop screen, console, e
-  screen-should-contain [
-    .          .
-    .          .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-  # redo
-  assume-console [
-    press ctrl-y
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  # all characters must be back
-  screen-should-contain [
-    .          .
-    .012       .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-  # cursor should be in the right place
-  assume-console [
-    type [3]
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  screen-should-contain [
-    .          .
-    .0123      .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-]
-
-scenario editor-work-clears-redo-stack [
-  local-scope
-  # create an editor with some text, do some work, undo
-  assume-screen 10/width, 5/height
-  contents:text <- new [abc
-def
-ghi]
-  e:&:editor <- new-editor contents, 0/left, 10/right
-  editor-render screen, e
-  assume-console [
-    type [1]
-    press ctrl-z
-  ]
-  editor-event-loop screen, console, e
-  # do some more work
-  assume-console [
-    type [0]
-  ]
-  editor-event-loop screen, console, e
-  screen-should-contain [
-    .          .
-    .0abc      .
-    .def       .
-    .ghi       .
-    .┈┈┈┈┈┈┈┈┈┈.
-  ]
-  # redo
-  assume-console [
-    press ctrl-y
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  # nothing should happen
-  screen-should-contain [
-    .          .
-    .0abc      .
-    .def       .
-    .ghi       .
-    .┈┈┈┈┈┈┈┈┈┈.
-  ]
-]
-
-scenario editor-can-redo-typing-and-enter-and-tab [
-  local-scope
-  # create an editor
-  assume-screen 10/width, 5/height
-  e:&:editor <- new-editor [], 0/left, 10/right
-  editor-render screen, e
-  # insert some text and tabs, hit enter, some more text and tabs
-  assume-console [
-    press tab
-    type [ab]
-    press tab
-    type [cd]
-    press enter
-    press tab
-    type [efg]
-  ]
-  editor-event-loop screen, console, e
-  screen-should-contain [
-    .          .
-    .  ab  cd  .
-    .    efg   .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-  3:num/raw <- get *e, cursor-row:offset
-  4:num/raw <- get *e, cursor-column:offset
-  memory-should-contain [
-    3 <- 2
-    4 <- 7
-  ]
-  # undo
-  assume-console [
-    press ctrl-z
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  # typing in second line deleted, but not indent
-  3:num/raw <- get *e, cursor-row:offset
-  4:num/raw <- get *e, cursor-column:offset
-  memory-should-contain [
-    3 <- 2
-    4 <- 2
-  ]
-  screen-should-contain [
-    .          .
-    .  ab  cd  .
-    .          .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-  # undo again
-  assume-console [
-    press ctrl-z
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  # indent and newline deleted
-  3:num/raw <- get *e, cursor-row:offset
-  4:num/raw <- get *e, cursor-column:offset
-  memory-should-contain [
-    3 <- 1
-    4 <- 8
-  ]
-  screen-should-contain [
-    .          .
-    .  ab  cd  .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-  # undo again
-  assume-console [
-    press ctrl-z
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  # empty screen
-  3:num/raw <- get *e, cursor-row:offset
-  4:num/raw <- get *e, cursor-column:offset
-  memory-should-contain [
-    3 <- 1
-    4 <- 0
-  ]
-  screen-should-contain [
-    .          .
-    .          .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-  # redo
-  assume-console [
-    press ctrl-y
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  # first line inserted
-  3:num/raw <- get *e, cursor-row:offset
-  4:num/raw <- get *e, cursor-column:offset
-  memory-should-contain [
-    3 <- 1
-    4 <- 8
-  ]
-  screen-should-contain [
-    .          .
-    .  ab  cd  .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-  # redo again
-  assume-console [
-    press ctrl-y
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  # newline and indent inserted
-  3:num/raw <- get *e, cursor-row:offset
-  4:num/raw <- get *e, cursor-column:offset
-  memory-should-contain [
-    3 <- 2
-    4 <- 2
-  ]
-  screen-should-contain [
-    .          .
-    .  ab  cd  .
-    .          .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-  # redo again
-  assume-console [
-    press ctrl-y
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  # indent and newline deleted
-  3:num/raw <- get *e, cursor-row:offset
-  4:num/raw <- get *e, cursor-column:offset
-  memory-should-contain [
-    3 <- 2
-    4 <- 7
-  ]
-  screen-should-contain [
-    .          .
-    .  ab  cd  .
-    .    efg   .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-]
-
-# undo cursor movement
-
-scenario editor-can-undo-touch [
-  local-scope
-  # create an editor with some text
-  assume-screen 10/width, 5/height
-  contents:text <- new [abc
-def
-ghi]
-  e:&:editor <- new-editor contents, 0/left, 10/right
-  editor-render screen, e
-  # move the cursor
-  assume-console [
-    left-click 3, 1
-  ]
-  editor-event-loop screen, console, e
-  # undo
-  assume-console [
-    press ctrl-z
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  # click undone
-  3:num/raw <- get *e, cursor-row:offset
-  4:num/raw <- get *e, cursor-column:offset
-  memory-should-contain [
-    3 <- 1
-    4 <- 0
-  ]
-  # cursor should be in the right place
-  assume-console [
-    type [1]
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  screen-should-contain [
-    .          .
-    .1abc      .
-    .def       .
-    .ghi       .
-    .┈┈┈┈┈┈┈┈┈┈.
-  ]
-]
-
-after <begin-move-cursor> [
-  cursor-row-before:num <- get *editor, cursor-row:offset
-  cursor-column-before:num <- get *editor, cursor-column:offset
-  top-before:&:duplex-list:char <- get *editor, top-of-screen:offset
-]
-before <end-move-cursor> [
-  top-after:&:duplex-list:char <- get *editor, top-of-screen:offset
-  cursor-row:num <- get *editor, cursor-row:offset
-  cursor-column:num <- get *editor, cursor-column:offset
-  {
-    break-unless undo-coalesce-tag
-    # if previous operation was also a move, and also had the same coalesce
-    # tag, coalesce with it
-    undo:&:list:&:operation <- get *editor, undo:offset
-    break-unless undo
-    op:&:operation <- first undo
-    move:move-operation, is-move?:bool <- maybe-convert *op, move:variant
-    break-unless is-move?
-    previous-coalesce-tag:num <- get move, tag:offset
-    coalesce?:bool <- equal undo-coalesce-tag, previous-coalesce-tag
-    break-unless coalesce?
-    move <- put move, after-row:offset, cursor-row
-    move <- put move, after-column:offset, cursor-column
-    move <- put move, after-top-of-screen:offset, top-after
-    *op <- merge 1/move-operation, move
-    break +done-adding-move-operation
-  }
-  op:&:operation <- new operation:type
-  *op <- merge 1/move-operation, cursor-row-before, cursor-column-before, top-before, cursor-row/after, cursor-column/after, top-after, undo-coalesce-tag
-  editor <- add-operation editor, op
-  +done-adding-move-operation
-]
-
-after <handle-undo> [
-  {
-    move:move-operation, is-move?:bool <- maybe-convert *op, move:variant
-    break-unless is-move?
-    # assert cursor-row/cursor-column/top-of-screen match after-row/after-column/after-top-of-screen
-    cursor-row <- get move, before-row:offset
-    *editor <- put *editor, cursor-row:offset, cursor-row
-    cursor-column <- get move, before-column:offset
-    *editor <- put *editor, cursor-column:offset, cursor-column
-    top:&:duplex-list:char <- get move, before-top-of-screen:offset
-    *editor <- put *editor, top-of-screen:offset, top
-  }
-]
-
-scenario editor-can-undo-left-arrow [
-  local-scope
-  # create an editor with some text
-  assume-screen 10/width, 5/height
-  contents:text <- new [abc
-def
-ghi]
-  e:&:editor <- new-editor contents, 0/left, 10/right
-  editor-render screen, e
-  # move the cursor
-  assume-console [
-    left-click 3, 1
-    press left-arrow
-  ]
-  editor-event-loop screen, console, e
-  # undo
-  assume-console [
-    press ctrl-z
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  # cursor moves back
-  3:num/raw <- get *e, cursor-row:offset
-  4:num/raw <- get *e, cursor-column:offset
-  memory-should-contain [
-    3 <- 3
-    4 <- 1
-  ]
-  # cursor should be in the right place
-  assume-console [
-    type [1]
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  screen-should-contain [
-    .          .
-    .abc       .
-    .def       .
-    .g1hi      .
-    .┈┈┈┈┈┈┈┈┈┈.
-  ]
-]
-
-scenario editor-can-undo-up-arrow [
-  local-scope
-  # create an editor with some text
-  assume-screen 10/width, 5/height
-  contents:text <- new [abc
-def
-ghi]
-  e:&:editor <- new-editor contents, 0/left, 10/right
-  editor-render screen, e
-  # move the cursor
-  assume-console [
-    left-click 3, 1
-    press up-arrow
-  ]
-  editor-event-loop screen, console, e
-  3:num/raw <- get *e, cursor-row:offset
-  4:num/raw <- get *e, cursor-column:offset
-  memory-should-contain [
-    3 <- 2
-    4 <- 1
-  ]
-  # undo
-  assume-console [
-    press ctrl-z
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  # cursor moves back
-  3:num/raw <- get *e, cursor-row:offset
-  4:num/raw <- get *e, cursor-column:offset
-  memory-should-contain [
-    3 <- 3
-    4 <- 1
-  ]
-  # cursor should be in the right place
-  assume-console [
-    type [1]
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  screen-should-contain [
-    .          .
-    .abc       .
-    .def       .
-    .g1hi      .
-    .┈┈┈┈┈┈┈┈┈┈.
-  ]
-]
-
-scenario editor-can-undo-down-arrow [
-  local-scope
-  # create an editor with some text
-  assume-screen 10/width, 5/height
-  contents:text <- new [abc
-def
-ghi]
-  e:&:editor <- new-editor contents, 0/left, 10/right
-  editor-render screen, e
-  # move the cursor
-  assume-console [
-    left-click 2, 1
-    press down-arrow
-  ]
-  editor-event-loop screen, console, e
-  # undo
-  assume-console [
-    press ctrl-z
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  # cursor moves back
-  3:num/raw <- get *e, cursor-row:offset
-  4:num/raw <- get *e, cursor-column:offset
-  memory-should-contain [
-    3 <- 2
-    4 <- 1
-  ]
-  # cursor should be in the right place
-  assume-console [
-    type [1]
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  screen-should-contain [
-    .          .
-    .abc       .
-    .d1ef      .
-    .ghi       .
-    .┈┈┈┈┈┈┈┈┈┈.
-  ]
-]
-
-scenario editor-can-undo-ctrl-a [
-  local-scope
-  # create an editor with some text
-  assume-screen 10/width, 5/height
-  contents:text <- new [abc
-def
-ghi]
-  e:&:editor <- new-editor contents, 0/left, 10/right
-  editor-render screen, e
-  # move the cursor, then to start of line
-  assume-console [
-    left-click 2, 1
-    press ctrl-a
-  ]
-  editor-event-loop screen, console, e
-  # undo
-  assume-console [
-    press ctrl-z
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  # cursor moves back
-  3:num/raw <- get *e, cursor-row:offset
-  4:num/raw <- get *e, cursor-column:offset
-  memory-should-contain [
-    3 <- 2
-    4 <- 1
-  ]
-  # cursor should be in the right place
-  assume-console [
-    type [1]
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  screen-should-contain [
-    .          .
-    .abc       .
-    .d1ef      .
-    .ghi       .
-    .┈┈┈┈┈┈┈┈┈┈.
-  ]
-]
-
-scenario editor-can-undo-home [
-  local-scope
-  # create an editor with some text
-  assume-screen 10/width, 5/height
-  contents:text <- new [abc
-def
-ghi]
-  e:&:editor <- new-editor contents, 0/left, 10/right
-  editor-render screen, e
-  # move the cursor, then to start of line
-  assume-console [
-    left-click 2, 1
-    press home
-  ]
-  editor-event-loop screen, console, e
-  # undo
-  assume-console [
-    press ctrl-z
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  # cursor moves back
-  3:num/raw <- get *e, cursor-row:offset
-  4:num/raw <- get *e, cursor-column:offset
-  memory-should-contain [
-    3 <- 2
-    4 <- 1
-  ]
-  # cursor should be in the right place
-  assume-console [
-    type [1]
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  screen-should-contain [
-    .          .
-    .abc       .
-    .d1ef      .
-    .ghi       .
-    .┈┈┈┈┈┈┈┈┈┈.
-  ]
-]
-
-scenario editor-can-undo-ctrl-e [
-  local-scope
-  # create an editor with some text
-  assume-screen 10/width, 5/height
-  contents:text <- new [abc
-def
-ghi]
-  e:&:editor <- new-editor contents, 0/left, 10/right
-  editor-render screen, e
-  # move the cursor, then to start of line
-  assume-console [
-    left-click 2, 1
-    press ctrl-e
-  ]
-  editor-event-loop screen, console, e
-  # undo
-  assume-console [
-    press ctrl-z
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  # cursor moves back
-  3:num/raw <- get *e, cursor-row:offset
-  4:num/raw <- get *e, cursor-column:offset
-  memory-should-contain [
-    3 <- 2
-    4 <- 1
-  ]
-  # cursor should be in the right place
-  assume-console [
-    type [1]
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  screen-should-contain [
-    .          .
-    .abc       .
-    .d1ef      .
-    .ghi       .
-    .┈┈┈┈┈┈┈┈┈┈.
-  ]
-]
-
-scenario editor-can-undo-end [
-  local-scope
-  # create an editor with some text
-  assume-screen 10/width, 5/height
-  contents:text <- new [abc
-def
-ghi]
-  e:&:editor <- new-editor contents, 0/left, 10/right
-  editor-render screen, e
-  # move the cursor, then to start of line
-  assume-console [
-    left-click 2, 1
-    press end
-  ]
-  editor-event-loop screen, console, e
-  # undo
-  assume-console [
-    press ctrl-z
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  # cursor moves back
-  3:num/raw <- get *e, cursor-row:offset
-  4:num/raw <- get *e, cursor-column:offset
-  memory-should-contain [
-    3 <- 2
-    4 <- 1
-  ]
-  # cursor should be in the right place
-  assume-console [
-    type [1]
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  screen-should-contain [
-    .          .
-    .abc       .
-    .d1ef      .
-    .ghi       .
-    .┈┈┈┈┈┈┈┈┈┈.
-  ]
-]
-
-scenario editor-can-undo-multiple-arrows-in-the-same-direction [
-  local-scope
-  # create an editor with some text
-  assume-screen 10/width, 5/height
-  contents:text <- new [abc
-def
-ghi]
-  e:&:editor <- new-editor contents, 0/left, 10/right
-  editor-render screen, e
-  # move the cursor
-  assume-console [
-    left-click 2, 1
-    press right-arrow
-    press right-arrow
-    press up-arrow
-  ]
-  editor-event-loop screen, console, e
-  3:num/raw <- get *e, cursor-row:offset
-  4:num/raw <- get *e, cursor-column:offset
-  memory-should-contain [
-    3 <- 1
-    4 <- 3
-  ]
-  # undo
-  assume-console [
-    press ctrl-z
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  # up-arrow is undone
-  3:num/raw <- get *e, cursor-row:offset
-  4:num/raw <- get *e, cursor-column:offset
-  memory-should-contain [
-    3 <- 2
-    4 <- 3
-  ]
-  # undo again
-  assume-console [
-    press ctrl-z
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  # both right-arrows are undone
-  3:num/raw <- get *e, cursor-row:offset
-  4:num/raw <- get *e, cursor-column:offset
-  memory-should-contain [
-    3 <- 2
-    4 <- 1
-  ]
-]
-
-# redo cursor movement
-
-scenario editor-redo-touch [
-  local-scope
-  # create an editor with some text, click on a character, undo
-  assume-screen 10/width, 5/height
-  contents:text <- new [abc
-def
-ghi]
-  e:&:editor <- new-editor contents, 0/left, 10/right
-  editor-render screen, e
-  assume-console [
-    left-click 3, 1
-    press ctrl-z
-  ]
-  editor-event-loop screen, console, e
-  # redo
-  assume-console [
-    press ctrl-y
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  # cursor moves to left-click
-  3:num/raw <- get *e, cursor-row:offset
-  4:num/raw <- get *e, cursor-column:offset
-  memory-should-contain [
-    3 <- 3
-    4 <- 1
-  ]
-  # cursor should be in the right place
-  assume-console [
-    type [1]
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  screen-should-contain [
-    .          .
-    .abc       .
-    .def       .
-    .g1hi      .
-    .┈┈┈┈┈┈┈┈┈┈.
-  ]
-]
-
-after <handle-redo> [
-  {
-    move:move-operation, is-move?:bool <- maybe-convert *op, move:variant
-    break-unless is-move?
-    # assert cursor-row/cursor-column/top-of-screen match after-row/after-column/after-top-of-screen
-    cursor-row <- get move, after-row:offset
-    *editor <- put *editor, cursor-row:offset, cursor-row
-    cursor-column <- get move, after-column:offset
-    *editor <- put *editor, cursor-column:offset, cursor-column
-    top:&:duplex-list:char <- get move, after-top-of-screen:offset
-    *editor <- put *editor, top-of-screen:offset, top
-  }
-]
-
-scenario editor-separates-undo-insert-from-undo-cursor-move [
-  local-scope
-  # create an editor, type some text, move the cursor, type some more text
-  assume-screen 10/width, 5/height
-  e:&:editor <- new-editor [], 0/left, 10/right
-  editor-render screen, e
-  assume-console [
-    type [abc]
-    left-click 1, 1
-    type [d]
-  ]
-  editor-event-loop screen, console, e
-  3:num/raw <- get *e, cursor-row:offset
-  4:num/raw <- get *e, cursor-column:offset
-  screen-should-contain [
-    .          .
-    .adbc      .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-  memory-should-contain [
-    3 <- 1
-    4 <- 2
-  ]
-  # undo
-  assume-console [
-    press ctrl-z
-  ]
-  run [
-    editor-event-loop screen, console, e
-    3:num/raw <- get *e, cursor-row:offset
-    4:num/raw <- get *e, cursor-column:offset
-  ]
-  # last letter typed is deleted
-  screen-should-contain [
-    .          .
-    .abc       .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-  memory-should-contain [
-    3 <- 1
-    4 <- 1
-  ]
-  # undo again
-  assume-console [
-    press ctrl-z
-  ]
-  run [
-    editor-event-loop screen, console, e
-    3:num/raw <- get *e, cursor-row:offset
-    4:num/raw <- get *e, cursor-column:offset
-  ]
-  # no change to screen; cursor moves
-  screen-should-contain [
-    .          .
-    .abc       .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-  memory-should-contain [
-    3 <- 1
-    4 <- 3
-  ]
-  # undo again
-  assume-console [
-    press ctrl-z
-  ]
-  run [
-    editor-event-loop screen, console, e
-    3:num/raw <- get *e, cursor-row:offset
-    4:num/raw <- get *e, cursor-column:offset
-  ]
-  # screen empty
-  screen-should-contain [
-    .          .
-    .          .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-  memory-should-contain [
-    3 <- 1
-    4 <- 0
-  ]
-  # redo
-  assume-console [
-    press ctrl-y
-  ]
-  run [
-    editor-event-loop screen, console, e
-    3:num/raw <- get *e, cursor-row:offset
-    4:num/raw <- get *e, cursor-column:offset
-  ]
-  # first insert
-  screen-should-contain [
-    .          .
-    .abc       .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-  memory-should-contain [
-    3 <- 1
-    4 <- 3
-  ]
-  # redo again
-  assume-console [
-    press ctrl-y
-  ]
-  run [
-    editor-event-loop screen, console, e
-    3:num/raw <- get *e, cursor-row:offset
-    4:num/raw <- get *e, cursor-column:offset
-  ]
-  # cursor moves
-  screen-should-contain [
-    .          .
-    .abc       .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-  # cursor moves
-  memory-should-contain [
-    3 <- 1
-    4 <- 1
-  ]
-  # redo again
-  assume-console [
-    press ctrl-y
-  ]
-  run [
-    editor-event-loop screen, console, e
-    3:num/raw <- get *e, cursor-row:offset
-    4:num/raw <- get *e, cursor-column:offset
-  ]
-  # second insert
-  screen-should-contain [
-    .          .
-    .adbc      .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-  memory-should-contain [
-    3 <- 1
-    4 <- 2
-  ]
-]
-
-# undo backspace
-
-scenario editor-can-undo-and-redo-backspace [
-  local-scope
-  # create an editor
-  assume-screen 10/width, 5/height
-  e:&:editor <- new-editor [], 0/left, 10/right
-  editor-render screen, e
-  # insert some text and hit backspace
-  assume-console [
-    type [abc]
-    press backspace
-    press backspace
-  ]
-  editor-event-loop screen, console, e
-  screen-should-contain [
-    .          .
-    .a         .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-  3:num/raw <- get *e, cursor-row:offset
-  4:num/raw <- get *e, cursor-column:offset
-  memory-should-contain [
-    3 <- 1
-    4 <- 1
-  ]
-  # undo
-  assume-console [
-    press ctrl-z
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  3:num/raw <- get *e, cursor-row:offset
-  4:num/raw <- get *e, cursor-column:offset
-  memory-should-contain [
-    3 <- 1
-    4 <- 3
-  ]
-  screen-should-contain [
-    .          .
-    .abc       .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-  # redo
-  assume-console [
-    press ctrl-y
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  3:num/raw <- get *e, cursor-row:offset
-  4:num/raw <- get *e, cursor-column:offset
-  memory-should-contain [
-    3 <- 1
-    4 <- 1
-  ]
-  screen-should-contain [
-    .          .
-    .a         .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-]
-
-# save operation to undo
-after <begin-backspace-character> [
-  top-before:&:duplex-list:char <- get *editor, top-of-screen:offset
-]
-before <end-backspace-character> [
-  {
-    break-unless backspaced-cell  # backspace failed; don't add an undo operation
-    top-after:&:duplex-list:char <- get *editor, top-of-screen:offset
-    cursor-row:num <- get *editor, cursor-row:offset
-    cursor-column:num <- get *editor, cursor-row:offset
-    before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
-    undo:&:list:&:operation <- get *editor, undo:offset
-    {
-      # if previous operation was an insert, coalesce this operation with it
-      break-unless undo
-      op:&:operation <- first undo
-      deletion:delete-operation, is-delete?:bool <- maybe-convert *op, delete:variant
-      break-unless is-delete?
-      previous-coalesce-tag:num <- get deletion, tag:offset
-      coalesce?:bool <- equal previous-coalesce-tag, 1/coalesce-backspace
-      break-unless coalesce?
-      deletion <- put deletion, delete-from:offset, before-cursor
-      backspaced-so-far:&:duplex-list:char <- get deletion, deleted-text:offset
-      splice backspaced-cell, backspaced-so-far
-      deletion <- put deletion, deleted-text:offset, backspaced-cell
-      deletion <- put deletion, after-row:offset, cursor-row
-      deletion <- put deletion, after-column:offset, cursor-column
-      deletion <- put deletion, after-top-of-screen:offset, top-after
-      *op <- merge 2/delete-operation, deletion
-      break +done-adding-backspace-operation
-    }
-    # if not, create a new operation
-    op:&:operation <- new operation:type
-    deleted-until:&:duplex-list:char <- next before-cursor
-    *op <- merge 2/delete-operation, save-row/before, save-column/before, top-before, cursor-row/after, cursor-column/after, top-after, backspaced-cell/deleted, before-cursor/delete-from, deleted-until, 1/coalesce-backspace
-    editor <- add-operation editor, op
-    +done-adding-backspace-operation
-  }
-]
-
-after <handle-undo> [
-  {
-    deletion:delete-operation, is-delete?:bool <- maybe-convert *op, delete:variant
-    break-unless is-delete?
-    anchor:&:duplex-list:char <- get deletion, delete-from:offset
-    break-unless anchor
-    deleted:&:duplex-list:char <- get deletion, deleted-text:offset
-    old-cursor:&:duplex-list:char <- last deleted
-    splice anchor, deleted
-    # assert cursor-row/cursor-column/top-of-screen match after-row/after-column/after-top-of-screen
-    before-cursor <- copy old-cursor
-    cursor-row <- get deletion, before-row:offset
-    *editor <- put *editor, cursor-row:offset, cursor-row
-    cursor-column <- get deletion, before-column:offset
-    *editor <- put *editor, cursor-column:offset, cursor-column
-    top:&:duplex-list:char <- get deletion, before-top-of-screen:offset
-    *editor <- put *editor, top-of-screen:offset, top
-  }
-]
-
-after <handle-redo> [
-  {
-    deletion:delete-operation, is-delete?:bool <- maybe-convert *op, delete:variant
-    break-unless is-delete?
-    start:&:duplex-list:char <- get deletion, delete-from:offset
-    end:&:duplex-list:char <- get deletion, delete-until:offset
-    data:&:duplex-list:char <- get *editor, data:offset
-    remove-between start, end
-    # assert cursor-row/cursor-column/top-of-screen match after-row/after-column/after-top-of-screen
-    cursor-row <- get deletion, after-row:offset
-    *editor <- put *editor, cursor-row:offset, cursor-row
-    cursor-column <- get deletion, after-column:offset
-    *editor <- put *editor, cursor-column:offset, cursor-column
-    top:&:duplex-list:char <- get deletion, before-top-of-screen:offset
-    *editor <- put *editor, top-of-screen:offset, top
-  }
-]
-
-# undo delete
-
-scenario editor-can-undo-and-redo-delete [
-  local-scope
-  # create an editor
-  assume-screen 10/width, 5/height
-  e:&:editor <- new-editor [], 0/left, 10/right
-  editor-render screen, e
-  # insert some text and hit delete and backspace a few times
-  assume-console [
-    type [abcdef]
-    left-click 1, 2
-    press delete
-    press backspace
-    press delete
-    press delete
-  ]
-  editor-event-loop screen, console, e
-  screen-should-contain [
-    .          .
-    .af        .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-  3:num/raw <- get *e, cursor-row:offset
-  4:num/raw <- get *e, cursor-column:offset
-  memory-should-contain [
-    3 <- 1
-    4 <- 1
-  ]
-  # undo deletes
-  assume-console [
-    press ctrl-z
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  3:num/raw <- get *e, cursor-row:offset
-  4:num/raw <- get *e, cursor-column:offset
-  memory-should-contain [
-    3 <- 1
-    4 <- 1
-  ]
-  screen-should-contain [
-    .          .
-    .adef      .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-  # undo backspace
-  assume-console [
-    press ctrl-z
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  3:num/raw <- get *e, cursor-row:offset
-  4:num/raw <- get *e, cursor-column:offset
-  memory-should-contain [
-    3 <- 1
-    4 <- 2
-  ]
-  screen-should-contain [
-    .          .
-    .abdef     .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-  # undo first delete
-  assume-console [
-    press ctrl-z
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  3:num/raw <- get *e, cursor-row:offset
-  4:num/raw <- get *e, cursor-column:offset
-  memory-should-contain [
-    3 <- 1
-    4 <- 2
-  ]
-  screen-should-contain [
-    .          .
-    .abcdef    .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-  # redo first delete
-  assume-console [
-    press ctrl-y
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  # first line inserted
-  3:num/raw <- get *e, cursor-row:offset
-  4:num/raw <- get *e, cursor-column:offset
-  memory-should-contain [
-    3 <- 1
-    4 <- 2
-  ]
-  screen-should-contain [
-    .          .
-    .abdef     .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-  # redo backspace
-  assume-console [
-    press ctrl-y
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  # first line inserted
-  3:num/raw <- get *e, cursor-row:offset
-  4:num/raw <- get *e, cursor-column:offset
-  memory-should-contain [
-    3 <- 1
-    4 <- 1
-  ]
-  screen-should-contain [
-    .          .
-    .adef      .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-  # redo deletes
-  assume-console [
-    press ctrl-y
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  # first line inserted
-  3:num/raw <- get *e, cursor-row:offset
-  4:num/raw <- get *e, cursor-column:offset
-  memory-should-contain [
-    3 <- 1
-    4 <- 1
-  ]
-  screen-should-contain [
-    .          .
-    .af        .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-]
-
-after <begin-delete-character> [
-  top-before:&:duplex-list:char <- get *editor, top-of-screen:offset
-]
-before <end-delete-character> [
-  {
-    break-unless deleted-cell  # delete failed; don't add an undo operation
-    top-after:&:duplex-list:char <- get *editor, top-of-screen:offset
-    cursor-row:num <- get *editor, cursor-row:offset
-    cursor-column:num <- get *editor, cursor-column:offset
-    before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
-    undo:&:list:&:operation <- get *editor, undo:offset
-    {
-      # if previous operation was an insert, coalesce this operation with it
-      break-unless undo
-      op:&:operation <- first undo
-      deletion:delete-operation, is-delete?:bool <- maybe-convert *op, delete:variant
-      break-unless is-delete?
-      previous-coalesce-tag:num <- get deletion, tag:offset
-      coalesce?:bool <- equal previous-coalesce-tag, 2/coalesce-delete
-      break-unless coalesce?
-      delete-until:&:duplex-list:char <- next before-cursor
-      deletion <- put deletion, delete-until:offset, delete-until
-      deleted-so-far:&:duplex-list:char <- get deletion, deleted-text:offset
-      deleted-so-far <- append deleted-so-far, deleted-cell
-      deletion <- put deletion, deleted-text:offset, deleted-so-far
-      deletion <- put deletion, after-row:offset, cursor-row
-      deletion <- put deletion, after-column:offset, cursor-column
-      deletion <- put deletion, after-top-of-screen:offset, top-after
-      *op <- merge 2/delete-operation, deletion
-      break +done-adding-delete-operation
-    }
-    # if not, create a new operation
-    op:&:operation <- new operation:type
-    deleted-until:&:duplex-list:char <- next before-cursor
-    *op <- merge 2/delete-operation, save-row/before, save-column/before, top-before, cursor-row/after, cursor-column/after, top-after, deleted-cell/deleted, before-cursor/delete-from, deleted-until, 2/coalesce-delete
-    editor <- add-operation editor, op
-    +done-adding-delete-operation
-  }
-]
-
-# undo ctrl-k
-
-scenario editor-can-undo-and-redo-ctrl-k [
-  local-scope
-  # create an editor
-  assume-screen 10/width, 5/height
-  contents:text <- new [abc
-def]
-  e:&:editor <- new-editor contents, 0/left, 10/right
-  editor-render screen, e
-  # insert some text and hit delete and backspace a few times
-  assume-console [
-    left-click 1, 1
-    press ctrl-k
-  ]
-  editor-event-loop screen, console, e
-  screen-should-contain [
-    .          .
-    .a         .
-    .def       .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-  3:num/raw <- get *e, cursor-row:offset
-  4:num/raw <- get *e, cursor-column:offset
-  memory-should-contain [
-    3 <- 1
-    4 <- 1
-  ]
-  # undo
-  assume-console [
-    press ctrl-z
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  screen-should-contain [
-    .          .
-    .abc       .
-    .def       .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-  3:num/raw <- get *e, cursor-row:offset
-  4:num/raw <- get *e, cursor-column:offset
-  memory-should-contain [
-    3 <- 1
-    4 <- 1
-  ]
-  # redo
-  assume-console [
-    press ctrl-y
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  # first line inserted
-  screen-should-contain [
-    .          .
-    .a         .
-    .def       .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-  3:num/raw <- get *e, cursor-row:offset
-  4:num/raw <- get *e, cursor-column:offset
-  memory-should-contain [
-    3 <- 1
-    4 <- 1
-  ]
-  # cursor should be in the right place
-  assume-console [
-    type [1]
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  screen-should-contain [
-    .          .
-    .a1        .
-    .def       .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-]
-
-after <begin-delete-to-end-of-line> [
-  top-before:&:duplex-list:char <- get *editor, top-of-screen:offset
-]
-before <end-delete-to-end-of-line> [
-  {
-    break-unless deleted-cells  # delete failed; don't add an undo operation
-    top-after:&:duplex-list:char <- get *editor, top-of-screen:offset
-    cursor-row:num <- get *editor, cursor-row:offset
-    cursor-column:num <- get *editor, cursor-column:offset
-    deleted-until:&:duplex-list:char <- next before-cursor
-    op:&:operation <- new operation:type
-    *op <- merge 2/delete-operation, save-row/before, save-column/before, top-before, cursor-row/after, cursor-column/after, top-after, deleted-cells/deleted, before-cursor/delete-from, deleted-until, 0/never-coalesce
-    editor <- add-operation editor, op
-    +done-adding-delete-operation
-  }
-]
-
-# undo ctrl-u
-
-scenario editor-can-undo-and-redo-ctrl-u [
-  local-scope
-  # create an editor
-  assume-screen 10/width, 5/height
-  contents:text <- new [abc
-def]
-  e:&:editor <- new-editor contents, 0/left, 10/right
-  editor-render screen, e
-  # insert some text and hit delete and backspace a few times
-  assume-console [
-    left-click 1, 2
-    press ctrl-u
-  ]
-  editor-event-loop screen, console, e
-  screen-should-contain [
-    .          .
-    .c         .
-    .def       .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-  3:num/raw <- get *e, cursor-row:offset
-  4:num/raw <- get *e, cursor-column:offset
-  memory-should-contain [
-    3 <- 1
-    4 <- 0
-  ]
-  # undo
-  assume-console [
-    press ctrl-z
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  screen-should-contain [
-    .          .
-    .abc       .
-    .def       .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-  3:num/raw <- get *e, cursor-row:offset
-  4:num/raw <- get *e, cursor-column:offset
-  memory-should-contain [
-    3 <- 1
-    4 <- 2
-  ]
-  # redo
-  assume-console [
-    press ctrl-y
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  # first line inserted
-  screen-should-contain [
-    .          .
-    .c         .
-    .def       .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-  3:num/raw <- get *e, cursor-row:offset
-  4:num/raw <- get *e, cursor-column:offset
-  memory-should-contain [
-    3 <- 1
-    4 <- 0
-  ]
-  # cursor should be in the right place
-  assume-console [
-    type [1]
-  ]
-  run [
-    editor-event-loop screen, console, e
-  ]
-  screen-should-contain [
-    .          .
-    .1c        .
-    .def       .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-]
-
-after <begin-delete-to-start-of-line> [
-  top-before:&:duplex-list:char <- get *editor, top-of-screen:offset
-]
-before <end-delete-to-start-of-line> [
-  {
-    break-unless deleted-cells  # delete failed; don't add an undo operation
-    top-after:&:duplex-list:char <- get *editor, top-of-screen:offset
-    op:&:operation <- new operation:type
-    before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
-    deleted-until:&:duplex-list:char <- next before-cursor
-    cursor-row:num <- get *editor, cursor-row:offset
-    cursor-column:num <- get *editor, cursor-column:offset
-    *op <- merge 2/delete-operation, save-row/before, save-column/before, top-before, cursor-row/after, cursor-column/after, top-after, deleted-cells/deleted, before-cursor/delete-from, deleted-until, 0/never-coalesce
-    editor <- add-operation editor, op
-    +done-adding-delete-operation
-  }
-]
-
-scenario editor-can-undo-and-redo-ctrl-u-2 [
-  local-scope
-  # create an editor
-  assume-screen 10/width, 5/height
-  e:&:editor <- new-editor [], 0/left, 10/right
-  editor-render screen, e
-  # insert some text and hit delete and backspace a few times
-  assume-console [
-    type [abc]
-    press ctrl-u
-    press ctrl-z
-  ]
-  editor-event-loop screen, console, e
-  screen-should-contain [
-    .          .
-    .abc       .
-    .┈┈┈┈┈┈┈┈┈┈.
-    .          .
-  ]
-]
diff --git a/sandbox/Readme.md b/sandbox/Readme.md
deleted file mode 100644
index e8acd78b..00000000
--- a/sandbox/Readme.md
+++ /dev/null
@@ -1,33 +0,0 @@
-Variant of [the Mu programming environment](../edit) that runs just the sandbox.
-
-Suitable for people who want to run their favorite terminal-based editor with
-Mu. Just run editor and sandbox inside split panes atop tmux. For example,
-here's Mu running alongside vim:
-
-<img alt='tmux+vim example' src='../html/tmux-vim-sandbox.png'>
-
-To set this up:
-
-  a) copy the lines in tmux.conf into `$HOME/.tmux.conf`.
-
-  b) copy the file `mu_run` somewhere in your `$PATH`.
-
-Now when you start tmux, split it into two vertical panes, run `./mu sandbox`
-on the right pane and your editor on the left. You should be able to hit F4 in
-either side to run the sandbox.
-
-Known issues: you have to explicitly save inside your editor before hitting
-F4, unlike with `./mu edit`.
-
----
-
-Appendix: keyboard shortcuts
-
-  _moving_
-  - `ctrl-a` or `home`: move cursor to start of line
-  - `ctrl-e` or `end`: move cursor to end of line
-
-  _modifying text_
-  - `ctrl-k`: delete text from cursor to end of line
-  - `ctrl-u`: delete text from start of line until just before cursor
-  - `ctrl-/`: comment/uncomment current line (using a special leader to ignore real comments https://www.reddit.com/r/vim/comments/4ootmz/_/d4ehmql)
diff --git a/sandbox/mu_run b/sandbox/mu_run
deleted file mode 100755
index b96cfd1c..00000000
--- a/sandbox/mu_run
+++ /dev/null
@@ -1,16 +0,0 @@
-#!/usr/bin/zsh
-# Little bit of glue to support running Mu from Vim over tmux.
-
-export ALREADY_FOCUSED=0
-tmux list-panes |grep "^1.*active" -q && export ALREADY_FOCUSED=1
-if [[ $ALREADY_FOCUSED -eq 0 ]]
-then
-  tmux select-pane -t 1
-fi
-
-tmux send-keys 'F4'
-
-if [[ $ALREADY_FOCUSED -eq 0 ]]
-then
-  tmux last-pane
-fi
diff --git a/sandbox/tmux.conf b/sandbox/tmux.conf
deleted file mode 100644
index 7816b1eb..00000000
--- a/sandbox/tmux.conf
+++ /dev/null
@@ -1,3 +0,0 @@
-# Hotkey for running Mu over tmux
-# Assumes exactly two panes, with vim running on the left side and `./mu sandbox` running on the right side.
-bind-key -n F4 run mu_run