about summary refs log tree commit diff stats
path: root/browse-slack
diff options
context:
space:
mode:
authorKartik K. Agaram <vc@akkartik.com>2021-08-13 17:48:38 -0700
committerKartik K. Agaram <vc@akkartik.com>2021-08-13 17:48:38 -0700
commitd5f9329b65f4677006b9db1157c272339302b094 (patch)
tree27ce10bcc09c0c4e588319b5c3c5cc9c89d4f59c /browse-slack
parent234039e34de90b8d86d92956337c11e0659b75e2 (diff)
downloadmu-d5f9329b65f4677006b9db1157c272339302b094.tar.gz
slack: UI flow for search
Everything works, I think, _except_ actually performing the search.
Diffstat (limited to 'browse-slack')
-rw-r--r--browse-slack/environment.mu90
-rw-r--r--browse-slack/gap-buffer.mu1470
-rw-r--r--browse-slack/grapheme-stack.mu556
3 files changed, 2108 insertions, 8 deletions
diff --git a/browse-slack/environment.mu b/browse-slack/environment.mu
index 3c23a556..158275b0 100644
--- a/browse-slack/environment.mu
+++ b/browse-slack/environment.mu
@@ -1,6 +1,8 @@
 type environment {
+  search-terms: (handle gap-buffer)
   tabs: (handle array tab)
   current-tab-index: int  # index into tabs
+  cursor-in-search?: boolean
   cursor-in-channels?: boolean
   channel-cursor-index: int
 }
@@ -9,10 +11,14 @@ type tab {
   type: int
       # type 0: everything
       # type 1: items in a channel
+      # type 2: search for a term
   item-index: int  # what item in the corresponding list we start rendering
                    # the current page at
   # only for type 1
   channel-index: int
+  # only for type 2
+  search-terms: (handle gap-buffer)
+  items: (handle array int)
 }
 
 # static buffer sizes in this file:
@@ -31,6 +37,10 @@ type tab {
 
 fn initialize-environment _self: (addr environment), _items: (addr item-list) {
   var self/esi: (addr environment) <- copy _self
+  var search-terms-ah/eax: (addr handle gap-buffer) <- get self, search-terms
+  allocate search-terms-ah
+  var search-terms/eax: (addr gap-buffer) <- lookup *search-terms-ah
+  initialize-gap-buffer search-terms, 0x30/search-capacity
   var items/eax: (addr item-list) <- copy _items
   var items-data-first-free-a/eax: (addr int) <- get items, data-first-free
   var final-item/edx: int <- copy *items-data-first-free-a
@@ -210,27 +220,45 @@ fn render-progress screen: (addr screen), curr: int, max: int {
   draw-int32-decimal-wrapping-right-then-down-from-cursor-over-full-screen screen, max, 7/fg 0/bg
 }
 
-fn render-search-input screen: (addr screen), env: (addr environment) {
+fn render-search-input screen: (addr screen), _env: (addr environment) {
+  var env/esi: (addr environment) <- copy _env
   set-cursor-position 0/screen, 0x22/x=search-position-x 1/y
   draw-text-wrapping-right-then-down-from-cursor-over-full-screen screen, "search ", 7/fg 0/bg
-  draw-text-wrapping-right-then-down-from-cursor-over-full-screen screen, "________________________________", 0xf/fg 0/bg
+  var search-terms-ah/eax: (addr handle gap-buffer) <- get env, search-terms
+  var search-terms/eax: (addr gap-buffer) <- lookup *search-terms-ah
+  rewind-gap-buffer search-terms
+  var x/eax: int <- render-gap-buffer screen, search-terms, 0x2a/x 1/y, 1/render-cursor, 0xf/fg 0/bg
+  {
+    compare x, 0x4a/end-search
+    break-if->
+    var y/ecx: int <- copy 0
+    x, y <- render-grapheme screen, 0x5f/underscore, 0/xmin 1/ymin, 0x80/xmax, 1/ymax, x, 1/y, 0xf/fg 0/bg
+    loop
+  }
 }
 
 fn render-menu screen: (addr screen), _env: (addr environment) {
   var env/edi: (addr environment) <- copy _env
-  var cursor-in-channels?/eax: (addr boolean) <- get env, cursor-in-channels?
-  compare *cursor-in-channels?, 0/false
+  var cursor-in-search?/eax: (addr boolean) <- get env, cursor-in-search?
+  compare *cursor-in-search?, 0/false
   {
-    break-if-!=
-    render-main-menu screen, env
+    break-if-=
+    render-search-menu screen, env
     return
   }
-  compare *cursor-in-channels?, 1/true
+  var cursor-in-channels?/eax: (addr boolean) <- get env, cursor-in-channels?
+  compare *cursor-in-channels?, 0/false
   {
     break-if-=
     render-channels-menu screen, env
     return
   }
+  compare *cursor-in-channels?, 0/false
+  {
+    break-if-!=
+    render-main-menu screen, env
+    return
+  }
 }
 
 fn render-main-menu screen: (addr screen), _env: (addr environment) {
@@ -283,6 +311,18 @@ fn render-channels-menu screen: (addr screen), _env: (addr environment) {
   draw-text-rightward-from-cursor screen, " select  ", width, 0xf/fg, 0/bg
 }
 
+fn render-search-menu screen: (addr screen), _env: (addr environment) {
+  var width/eax: int <- copy 0
+  var y/ecx: int <- copy 0
+  width, y <- screen-size screen
+  y <- decrement
+  set-cursor-position screen, 2/x, y
+  draw-text-rightward-from-cursor screen, " Esc ", width, 0/fg 0xf/bg
+  draw-text-rightward-from-cursor screen, " cancel  ", width, 0xf/fg, 0/bg
+  draw-text-rightward-from-cursor screen, " Enter ", width, 0/fg 0xf/bg
+  draw-text-rightward-from-cursor screen, " select  ", width, 0xf/fg, 0/bg
+}
+
 fn render-item screen: (addr screen), _item: (addr item), _users: (addr array user), y: int, screen-height: int -> _/ecx: int {
   var item/esi: (addr item) <- copy _item
   var users/edi: (addr array user) <- copy _users
@@ -572,7 +612,14 @@ fn update-environment _env: (addr environment), key: byte, users: (addr array us
     previous-tab env
     return
   }
-  # TODO: search
+  {
+    compare key, 0x2f/slash
+    break-if-!=
+    # start search mode
+    var cursor-in-search?/eax: (addr boolean) <- get env, cursor-in-search?
+    copy-to *cursor-in-search?, 1/true
+    return
+  }
   var cursor-in-channels?/eax: (addr boolean) <- get env, cursor-in-channels?
   {
     compare key, 9/tab
@@ -583,6 +630,13 @@ fn update-environment _env: (addr environment), key: byte, users: (addr array us
   }
   # dispatch based on where the cursor is
   {
+    var cursor-in-search?/eax: (addr boolean) <- get env, cursor-in-search?
+    compare *cursor-in-search?, 0/false
+    break-if-!=
+    update-search env, key, users, channels, items
+    return
+  }
+  {
     compare *cursor-in-channels?, 0/false
     break-if-!=
     update-main-panel env, key, users, channels, items
@@ -649,6 +703,23 @@ fn update-channels-nav _env: (addr environment), key: byte, users: (addr array u
   }
 }
 
+fn update-search _env: (addr environment), key: byte, users: (addr array user), channels: (addr array channel), items: (addr item-list) {
+  var env/edi: (addr environment) <- copy _env
+  {
+    compare key, 0xa/newline
+    break-if-!=
+    new-search-tab env, items
+    var cursor-in-search?/eax: (addr boolean) <- get env, cursor-in-search?
+    copy-to *cursor-in-search?, 0/false
+    return
+  }
+  # otherwise delegate
+  var search-terms-ah/eax: (addr handle gap-buffer) <- get env, search-terms
+  var search-terms/eax: (addr gap-buffer) <- lookup *search-terms-ah
+  var g/ecx: grapheme <- copy key
+  edit-gap-buffer search-terms, g
+}
+
 fn new-channel-tab _env: (addr environment), channel-index: int, _channels: (addr array channel) {
   var env/edi: (addr environment) <- copy _env
   var current-tab-index-addr/eax: (addr int) <- get env, current-tab-index
@@ -681,6 +752,9 @@ fn new-channel-tab _env: (addr environment), channel-index: int, _channels: (add
   copy-to *dest, curr-channel-final-post-index
 }
 
+fn new-search-tab _env: (addr environment), _items: (addr item-list) {
+}
+
 fn previous-tab _env: (addr environment) {
   var env/edi: (addr environment) <- copy _env
   var current-tab-index-addr/eax: (addr int) <- get env, current-tab-index
diff --git a/browse-slack/gap-buffer.mu b/browse-slack/gap-buffer.mu
new file mode 100644
index 00000000..0077d6fd
--- /dev/null
+++ b/browse-slack/gap-buffer.mu
@@ -0,0 +1,1470 @@
+# primitive for editing text
+
+type gap-buffer {
+  left: grapheme-stack
+  right: grapheme-stack
+  # some fields for scanning incrementally through a gap-buffer
+  left-read-index: int
+  right-read-index: int
+}
+
+fn initialize-gap-buffer _self: (addr gap-buffer), capacity: int {
+  var self/esi: (addr gap-buffer) <- copy _self
+  var left/eax: (addr grapheme-stack) <- get self, left
+  initialize-grapheme-stack left, capacity
+  var right/eax: (addr grapheme-stack) <- get self, right
+  initialize-grapheme-stack right, capacity
+}
+
+fn clear-gap-buffer _self: (addr gap-buffer) {
+  var self/esi: (addr gap-buffer) <- copy _self
+  var left/eax: (addr grapheme-stack) <- get self, left
+  clear-grapheme-stack left
+  var right/eax: (addr grapheme-stack) <- get self, right
+  clear-grapheme-stack right
+}
+
+fn gap-buffer-empty? _self: (addr gap-buffer) -> _/eax: boolean {
+  var self/esi: (addr gap-buffer) <- copy _self
+  # if !empty?(left) return false
+  {
+    var left/eax: (addr grapheme-stack) <- get self, left
+    var result/eax: boolean <- grapheme-stack-empty? left
+    compare result, 0/false
+    break-if-!=
+    return 0/false
+  }
+  # return empty?(right)
+  var left/eax: (addr grapheme-stack) <- get self, left
+  var result/eax: boolean <- grapheme-stack-empty? left
+  return result
+}
+
+fn gap-buffer-capacity _gap: (addr gap-buffer) -> _/edx: int {
+  var gap/esi: (addr gap-buffer) <- copy _gap
+  var left/eax: (addr grapheme-stack) <- get gap, left
+  var left-data-ah/eax: (addr handle array grapheme) <- get left, data
+  var left-data/eax: (addr array grapheme) <- lookup *left-data-ah
+  var result/eax: int <- length left-data
+  return result
+}
+
+# just for tests
+fn initialize-gap-buffer-with self: (addr gap-buffer), keys: (addr array byte) {
+  initialize-gap-buffer self, 0x40/capacity
+  var input-stream-storage: (stream byte 0x40/capacity)
+  var input-stream/ecx: (addr stream byte) <- address input-stream-storage
+  write input-stream, keys
+  {
+    var done?/eax: boolean <- stream-empty? input-stream
+    compare done?, 0/false
+    break-if-!=
+    var g/eax: grapheme <- read-grapheme input-stream
+    add-grapheme-at-gap self, g
+    loop
+  }
+}
+
+fn load-gap-buffer-from-stream self: (addr gap-buffer), in: (addr stream byte) {
+  rewind-stream in
+  {
+    var done?/eax: boolean <- stream-empty? in
+    compare done?, 0/false
+    break-if-!=
+    var key/eax: byte <- read-byte in
+    compare key, 0/null
+    break-if-=
+    var g/eax: grapheme <- copy key
+    edit-gap-buffer self, g
+    loop
+  }
+}
+
+fn emit-gap-buffer self: (addr gap-buffer), out: (addr stream byte) {
+  clear-stream out
+  append-gap-buffer self, out
+}
+
+fn append-gap-buffer _self: (addr gap-buffer), out: (addr stream byte) {
+  var self/esi: (addr gap-buffer) <- copy _self
+  var left/eax: (addr grapheme-stack) <- get self, left
+  emit-stack-from-bottom left, out
+  var right/eax: (addr grapheme-stack) <- get self, right
+  emit-stack-from-top right, out
+}
+
+# dump stack from bottom to top
+fn emit-stack-from-bottom _self: (addr grapheme-stack), out: (addr stream byte) {
+  var self/esi: (addr grapheme-stack) <- copy _self
+  var data-ah/edi: (addr handle array grapheme) <- get self, data
+  var _data/eax: (addr array grapheme) <- lookup *data-ah
+  var data/edi: (addr array grapheme) <- copy _data
+  var top-addr/ecx: (addr int) <- get self, top
+  var i/eax: int <- copy 0
+  {
+    compare i, *top-addr
+    break-if->=
+    var g/edx: (addr grapheme) <- index data, i
+    write-grapheme out, *g
+    i <- increment
+    loop
+  }
+}
+
+# dump stack from top to bottom
+fn emit-stack-from-top _self: (addr grapheme-stack), out: (addr stream byte) {
+  var self/esi: (addr grapheme-stack) <- copy _self
+  var data-ah/edi: (addr handle array grapheme) <- get self, data
+  var _data/eax: (addr array grapheme) <- lookup *data-ah
+  var data/edi: (addr array grapheme) <- copy _data
+  var top-addr/ecx: (addr int) <- get self, top
+  var i/eax: int <- copy *top-addr
+  i <- decrement
+  {
+    compare i, 0
+    break-if-<
+    var g/edx: (addr grapheme) <- index data, i
+    write-grapheme out, *g
+    i <- decrement
+    loop
+  }
+}
+
+fn word-at-gap _self: (addr gap-buffer), out: (addr stream byte) {
+  var self/esi: (addr gap-buffer) <- copy _self
+  clear-stream out
+  {
+    var g/eax: grapheme <- grapheme-at-gap self
+    var at-word?/eax: boolean <- is-ascii-word-grapheme? g
+    compare at-word?, 0/false
+    break-if-!=
+    return
+  }
+  var left/ecx: (addr grapheme-stack) <- get self, left
+  var left-index/eax: int <- top-most-word left
+  emit-stack-from-index left, left-index, out
+  var right/ecx: (addr grapheme-stack) <- get self, right
+  var right-index/eax: int <- top-most-word right
+  emit-stack-to-index right, right-index, out
+}
+
+fn test-word-at-gap-single-word-with-gap-at-end {
+  var _g: gap-buffer
+  var g/esi: (addr gap-buffer) <- address _g
+  initialize-gap-buffer-with g, "abc"
+  # gap is at end (right is empty)
+  var out-storage: (stream byte 0x10)
+  var out/eax: (addr stream byte) <- address out-storage
+  word-at-gap g, out
+  check-stream-equal out, "abc", "F - test-word-at-gap-single-word-with-gap-at-end"
+}
+
+fn test-word-at-gap-single-word-with-gap-at-start {
+  var _g: gap-buffer
+  var g/esi: (addr gap-buffer) <- address _g
+  initialize-gap-buffer-with g, "abc"
+  gap-to-start g
+  #
+  var out-storage: (stream byte 0x10)
+  var out/eax: (addr stream byte) <- address out-storage
+  word-at-gap g, out
+  check-stream-equal out, "abc", "F - test-word-at-gap-single-word-with-gap-at-start"
+}
+
+fn test-word-at-gap-multiple-words-with-gap-at-non-word-grapheme-at-end {
+  var _g: gap-buffer
+  var g/esi: (addr gap-buffer) <- address _g
+  initialize-gap-buffer-with g, "abc "
+  # gap is at end (right is empty)
+  var out-storage: (stream byte 0x10)
+  var out/eax: (addr stream byte) <- address out-storage
+  word-at-gap g, out
+  check-stream-equal out, "", "F - test-word-at-gap-multiple-words-with-gap-at-non-word-grapheme-at-end"
+}
+
+fn test-word-at-gap-multiple-words-with-gap-at-non-word-grapheme-at-start {
+  var _g: gap-buffer
+  var g/esi: (addr gap-buffer) <- address _g
+  initialize-gap-buffer-with g, " abc"
+  gap-to-start g
+  #
+  var out-storage: (stream byte 0x10)
+  var out/eax: (addr stream byte) <- address out-storage
+  word-at-gap g, out
+  check-stream-equal out, "", "F - test-word-at-gap-multiple-words-with-gap-at-non-word-grapheme-at-start"
+}
+
+fn test-word-at-gap-multiple-words-with-gap-at-end {
+  var _g: gap-buffer
+  var g/esi: (addr gap-buffer) <- address _g
+  initialize-gap-buffer-with g, "a bc d"
+  # gap is at end (right is empty)
+  var out-storage: (stream byte 0x10)
+  var out/eax: (addr stream byte) <- address out-storage
+  word-at-gap g, out
+  check-stream-equal out, "d", "F - test-word-at-gap-multiple-words-with-gap-at-end"
+}
+
+fn test-word-at-gap-multiple-words-with-gap-at-initial-word {
+  var _g: gap-buffer
+  var g/esi: (addr gap-buffer) <- address _g
+  initialize-gap-buffer-with g, "a bc d"
+  gap-to-start g
+  #
+  var out-storage: (stream byte 0x10)
+  var out/eax: (addr stream byte) <- address out-storage
+  word-at-gap g, out
+  check-stream-equal out, "a", "F - test-word-at-gap-multiple-words-with-gap-at-initial-word"
+}
+
+fn test-word-at-gap-multiple-words-with-gap-at-final-word {
+  var _g: gap-buffer
+  var g/esi: (addr gap-buffer) <- address _g
+  initialize-gap-buffer-with g, "a bc d"
+  var dummy/eax: grapheme <- gap-left g
+  # gap is at final word
+  var out-storage: (stream byte 0x10)
+  var out/eax: (addr stream byte) <- address out-storage
+  word-at-gap g, out
+  check-stream-equal out, "d", "F - test-word-at-gap-multiple-words-with-gap-at-final-word"
+}
+
+fn test-word-at-gap-multiple-words-with-gap-at-final-non-word {
+  var _g: gap-buffer
+  var g/esi: (addr gap-buffer) <- address _g
+  initialize-gap-buffer-with g, "abc "
+  var dummy/eax: grapheme <- gap-left g
+  # gap is at final word
+  var out-storage: (stream byte 0x10)
+  var out/eax: (addr stream byte) <- address out-storage
+  word-at-gap g, out
+  check-stream-equal out, "", "F - test-word-at-gap-multiple-words-with-gap-at-final-non-word"
+}
+
+fn grapheme-at-gap _self: (addr gap-buffer) -> _/eax: grapheme {
+  # send top of right most of the time
+  var self/esi: (addr gap-buffer) <- copy _self
+  var right/edi: (addr grapheme-stack) <- get self, right
+  var data-ah/eax: (addr handle array grapheme) <- get right, data
+  var data/eax: (addr array grapheme) <- lookup *data-ah
+  var top-addr/ecx: (addr int) <- get right, top
+  {
+    compare *top-addr, 0
+    break-if-<=
+    var top/ecx: int <- copy *top-addr
+    top <- decrement
+    var result/eax: (addr grapheme) <- index data, top
+    return *result
+  }
+  # send top of left only if right is empty
+  var left/edi: (addr grapheme-stack) <- get self, left
+  var data-ah/eax: (addr handle array grapheme) <- get left, data
+  var data/eax: (addr array grapheme) <- lookup *data-ah
+  var top-addr/ecx: (addr int) <- get left, top
+  {
+    compare *top-addr, 0
+    break-if-<=
+    var top/ecx: int <- copy *top-addr
+    top <- decrement
+    var result/eax: (addr grapheme) <- index data, top
+    return *result
+  }
+  # send null if everything is empty
+  return 0
+}
+
+fn top-most-word _self: (addr grapheme-stack) -> _/eax: int {
+  var self/esi: (addr grapheme-stack) <- copy _self
+  var data-ah/edi: (addr handle array grapheme) <- get self, data
+  var _data/eax: (addr array grapheme) <- lookup *data-ah
+  var data/edi: (addr array grapheme) <- copy _data
+  var top-addr/ecx: (addr int) <- get self, top
+  var i/ebx: int <- copy *top-addr
+  i <- decrement
+  {
+    compare i, 0
+    break-if-<
+    var g/edx: (addr grapheme) <- index data, i
+    var is-word?/eax: boolean <- is-ascii-word-grapheme? *g
+    compare is-word?, 0/false
+    break-if-=
+    i <- decrement
+    loop
+  }
+  i <- increment
+  return i
+}
+
+fn emit-stack-from-index _self: (addr grapheme-stack), start: int, out: (addr stream byte) {
+  var self/esi: (addr grapheme-stack) <- copy _self
+  var data-ah/edi: (addr handle array grapheme) <- get self, data
+  var _data/eax: (addr array grapheme) <- lookup *data-ah
+  var data/edi: (addr array grapheme) <- copy _data
+  var top-addr/ecx: (addr int) <- get self, top
+  var i/eax: int <- copy start
+  {
+    compare i, *top-addr
+    break-if->=
+    var g/edx: (addr grapheme) <- index data, i
+    write-grapheme out, *g
+    i <- increment
+    loop
+  }
+}
+
+fn emit-stack-to-index _self: (addr grapheme-stack), end: int, out: (addr stream byte) {
+  var self/esi: (addr grapheme-stack) <- copy _self
+  var data-ah/edi: (addr handle array grapheme) <- get self, data
+  var _data/eax: (addr array grapheme) <- lookup *data-ah
+  var data/edi: (addr array grapheme) <- copy _data
+  var top-addr/ecx: (addr int) <- get self, top
+  var i/eax: int <- copy *top-addr
+  i <- decrement
+  {
+    compare i, 0
+    break-if-<
+    compare i, end
+    break-if-<
+    var g/edx: (addr grapheme) <- index data, i
+    write-grapheme out, *g
+    i <- decrement
+    loop
+  }
+}
+
+fn is-ascii-word-grapheme? g: grapheme -> _/eax: boolean {
+  compare g, 0x21/!
+  {
+    break-if-!=
+    return 1/true
+  }
+  compare g, 0x30/0
+  {
+    break-if->=
+    return 0/false
+  }
+  compare g, 0x39/9
+  {
+    break-if->
+    return 1/true
+  }
+  compare g, 0x3f/?
+  {
+    break-if-!=
+    return 1/true
+  }
+  compare g, 0x41/A
+  {
+    break-if->=
+    return 0/false
+  }
+  compare g, 0x5a/Z
+  {
+    break-if->
+    return 1/true
+  }
+  compare g, 0x5f/_
+  {
+    break-if-!=
+    return 1/true
+  }
+  compare g, 0x61/a
+  {
+    break-if->=
+    return 0/false
+  }
+  compare g, 0x7a/z
+  {
+    break-if->
+    return 1/true
+  }
+  return 0/false
+}
+
+# We implicitly render everything editable in a single color, and assume the
+# cursor is a single other color.
+fn render-gap-buffer-wrapping-right-then-down screen: (addr screen), _gap: (addr gap-buffer), xmin: int, ymin: int, xmax: int, ymax: int, render-cursor?: boolean, color: int, background-color: int -> _/eax: int, _/ecx: int {
+  var gap/esi: (addr gap-buffer) <- copy _gap
+  var left/edx: (addr grapheme-stack) <- get gap, left
+  var highlight-matching-open-paren?/ebx: boolean <- copy 0/false
+  var matching-open-paren-depth/edi: int <- copy 0
+  highlight-matching-open-paren?, matching-open-paren-depth <- highlight-matching-open-paren? gap, render-cursor?
+  var x2/eax: int <- copy 0
+  var y2/ecx: int <- copy 0
+  x2, y2 <- render-stack-from-bottom-wrapping-right-then-down screen, left, xmin, ymin, xmax, ymax, xmin, ymin, highlight-matching-open-paren?, matching-open-paren-depth, color, background-color
+  var right/edx: (addr grapheme-stack) <- get gap, right
+  x2, y2 <- render-stack-from-top-wrapping-right-then-down screen, right, xmin, ymin, xmax, ymax, x2, y2, render-cursor?, color, background-color
+  # decide whether we still need to print a cursor
+  var fg/edi: int <- copy color
+  var bg/ebx: int <- copy background-color
+  compare render-cursor?, 0/false
+  {
+    break-if-=
+    # if the right side is empty, grapheme stack didn't print the cursor
+    var empty?/eax: boolean <- grapheme-stack-empty? right
+    compare empty?, 0/false
+    break-if-=
+    # swap foreground and background
+    fg <- copy background-color
+    bg <- copy color
+  }
+  # print a grapheme either way so that cursor position doesn't affect printed width
+  var space/edx: grapheme <- copy 0x20
+  x2, y2 <- render-grapheme screen, space, xmin, ymin, xmax, ymax, x2, y2, fg, bg
+  return x2, y2
+}
+
+fn render-gap-buffer screen: (addr screen), gap: (addr gap-buffer), x: int, y: int, render-cursor?: boolean, color: int, background-color: int -> _/eax: int {
+  var _width/eax: int <- copy 0
+  var _height/ecx: int <- copy 0
+  _width, _height <- screen-size screen
+  var width/edx: int <- copy _width
+  var height/ebx: int <- copy _height
+  var x2/eax: int <- copy 0
+  var y2/ecx: int <- copy 0
+  x2, y2 <- render-gap-buffer-wrapping-right-then-down screen, gap, x, y, width, height, render-cursor?, color, background-color
+  return x2  # y2? yolo
+}
+
+fn gap-buffer-length _gap: (addr gap-buffer) -> _/eax: int {
+  var gap/esi: (addr gap-buffer) <- copy _gap
+  var left/eax: (addr grapheme-stack) <- get gap, left
+  var tmp/eax: (addr int) <- get left, top
+  var left-length/ecx: int <- copy *tmp
+  var right/esi: (addr grapheme-stack) <- get gap, right
+  tmp <- get right, top
+  var result/eax: int <- copy *tmp
+  result <- add left-length
+  return result
+}
+
+fn add-grapheme-at-gap _self: (addr gap-buffer), g: grapheme {
+  var self/esi: (addr gap-buffer) <- copy _self
+  var left/eax: (addr grapheme-stack) <- get self, left
+  push-grapheme-stack left, g
+}
+
+fn add-code-point-at-gap self: (addr gap-buffer), c: code-point {
+  var g/eax: grapheme <- copy c
+  add-grapheme-at-gap self, g
+}
+
+fn gap-to-start self: (addr gap-buffer) {
+  {
+    var curr/eax: grapheme <- gap-left self
+    compare curr, -1
+    loop-if-!=
+  }
+}
+
+fn gap-to-end self: (addr gap-buffer) {
+  {
+    var curr/eax: grapheme <- gap-right self
+    compare curr, -1
+    loop-if-!=
+  }
+}
+
+fn gap-at-start? _self: (addr gap-buffer) -> _/eax: boolean {
+  var self/esi: (addr gap-buffer) <- copy _self
+  var left/eax: (addr grapheme-stack) <- get self, left
+  var result/eax: boolean <- grapheme-stack-empty? left
+  return result
+}
+
+fn gap-at-end? _self: (addr gap-buffer) -> _/eax: boolean {
+  var self/esi: (addr gap-buffer) <- copy _self
+  var right/eax: (addr grapheme-stack) <- get self, right
+  var result/eax: boolean <- grapheme-stack-empty? right
+  return result
+}
+
+fn gap-right _self: (addr gap-buffer) -> _/eax: grapheme {
+  var self/esi: (addr gap-buffer) <- copy _self
+  var g/eax: grapheme <- copy 0
+  var right/ecx: (addr grapheme-stack) <- get self, right
+  g <- pop-grapheme-stack right
+  compare g, -1
+  {
+    break-if-=
+    var left/ecx: (addr grapheme-stack) <- get self, left
+    push-grapheme-stack left, g
+  }
+  return g
+}
+
+fn gap-left _self: (addr gap-buffer) -> _/eax: grapheme {
+  var self/esi: (addr gap-buffer) <- copy _self
+  var g/eax: grapheme <- copy 0
+  {
+    var left/ecx: (addr grapheme-stack) <- get self, left
+    g <- pop-grapheme-stack left
+  }
+  compare g, -1
+  {
+    break-if-=
+    var right/ecx: (addr grapheme-stack) <- get self, right
+    push-grapheme-stack right, g
+  }
+  return g
+}
+
+fn index-of-gap _self: (addr gap-buffer) -> _/eax: int {
+  var self/eax: (addr gap-buffer) <- copy _self
+  var left/eax: (addr grapheme-stack) <- get self, left
+  var top-addr/eax: (addr int) <- get left, top
+  var result/eax: int <- copy *top-addr
+  return result
+}
+
+fn first-grapheme-in-gap-buffer _self: (addr gap-buffer) -> _/eax: grapheme {
+  var self/esi: (addr gap-buffer) <- copy _self
+  # try to read from left
+  var left/eax: (addr grapheme-stack) <- get self, left
+  var top-addr/ecx: (addr int) <- get left, top
+  compare *top-addr, 0
+  {
+    break-if-<=
+    var data-ah/eax: (addr handle array grapheme) <- get left, data
+    var data/eax: (addr array grapheme) <- lookup *data-ah
+    var result-addr/eax: (addr grapheme) <- index data, 0
+    return *result-addr
+  }
+  # try to read from right
+  var right/eax: (addr grapheme-stack) <- get self, right
+  top-addr <- get right, top
+  compare *top-addr, 0
+  {
+    break-if-<=
+    var data-ah/eax: (addr handle array grapheme) <- get right, data
+    var data/eax: (addr array grapheme) <- lookup *data-ah
+    var top/ecx: int <- copy *top-addr
+    top <- decrement
+    var result-addr/eax: (addr grapheme) <- index data, top
+    return *result-addr
+  }
+  # give up
+  return -1
+}
+
+fn grapheme-before-cursor-in-gap-buffer _self: (addr gap-buffer) -> _/eax: grapheme {
+  var self/esi: (addr gap-buffer) <- copy _self
+  # try to read from left
+  var left/ecx: (addr grapheme-stack) <- get self, left
+  var top-addr/edx: (addr int) <- get left, top
+  compare *top-addr, 0
+  {
+    break-if-<=
+    var result/eax: grapheme <- pop-grapheme-stack left
+    push-grapheme-stack left, result
+    return result
+  }
+  # give up
+  return -1
+}
+
+fn delete-before-gap _self: (addr gap-buffer) {
+  var self/eax: (addr gap-buffer) <- copy _self
+  var left/eax: (addr grapheme-stack) <- get self, left
+  var dummy/eax: grapheme <- pop-grapheme-stack left
+}
+
+fn pop-after-gap _self: (addr gap-buffer) -> _/eax: grapheme {
+  var self/eax: (addr gap-buffer) <- copy _self
+  var right/eax: (addr grapheme-stack) <- get self, right
+  var result/eax: grapheme <- pop-grapheme-stack right
+  return result
+}
+
+fn gap-buffer-equal? _self: (addr gap-buffer), s: (addr array byte) -> _/eax: boolean {
+  var self/esi: (addr gap-buffer) <- copy _self
+  # complication: graphemes may be multiple bytes
+  # so don't rely on length
+  # instead turn the expected result into a stream and arrange to read from it in order
+  var stream-storage: (stream byte 0x10/capacity)
+  var expected-stream/ecx: (addr stream byte) <- address stream-storage
+  write expected-stream, s
+  # compare left
+  var left/edx: (addr grapheme-stack) <- get self, left
+  var result/eax: boolean <- prefix-match? left, expected-stream
+  compare result, 0/false
+  {
+    break-if-!=
+    return result
+  }
+  # compare right
+  var right/edx: (addr grapheme-stack) <- get self, right
+  result <- suffix-match? right, expected-stream
+  compare result, 0/false
+  {
+    break-if-!=
+    return result
+  }
+  # ensure there's nothing left over
+  result <- stream-empty? expected-stream
+  return result
+}
+
+fn test-gap-buffer-equal-from-end {
+  var _g: gap-buffer
+  var g/esi: (addr gap-buffer) <- address _g
+  initialize-gap-buffer g, 0x10
+  #
+  add-code-point-at-gap g, 0x61/a
+  add-code-point-at-gap g, 0x61/a
+  add-code-point-at-gap g, 0x61/a
+  # gap is at end (right is empty)
+  var result/eax: boolean <- gap-buffer-equal? g, "aaa"
+  check result, "F - test-gap-buffer-equal-from-end"
+}
+
+fn test-gap-buffer-equal-from-middle {
+  var _g: gap-buffer
+  var g/esi: (addr gap-buffer) <- address _g
+  initialize-gap-buffer g, 0x10
+  #
+  add-code-point-at-gap g, 0x61/a
+  add-code-point-at-gap g, 0x61/a
+  add-code-point-at-gap g, 0x61/a
+  var dummy/eax: grapheme <- gap-left g
+  # gap is in the middle
+  var result/eax: boolean <- gap-buffer-equal? g, "aaa"
+  check result, "F - test-gap-buffer-equal-from-middle"
+}
+
+fn test-gap-buffer-equal-from-start {
+  var _g: gap-buffer
+  var g/esi: (addr gap-buffer) <- address _g
+  initialize-gap-buffer g, 0x10
+  #
+  add-code-point-at-gap g, 0x61/a
+  add-code-point-at-gap g, 0x61/a
+  add-code-point-at-gap g, 0x61/a
+  var dummy/eax: grapheme <- gap-left g
+  dummy <- gap-left g
+  dummy <- gap-left g
+  # gap is at the start
+  var result/eax: boolean <- gap-buffer-equal? g, "aaa"
+  check result, "F - test-gap-buffer-equal-from-start"
+}
+
+fn test-gap-buffer-equal-fails {
+  # g = "aaa"
+  var _g: gap-buffer
+  var g/esi: (addr gap-buffer) <- address _g
+  initialize-gap-buffer g, 0x10
+  add-code-point-at-gap g, 0x61/a
+  add-code-point-at-gap g, 0x61/a
+  add-code-point-at-gap g, 0x61/a
+  #
+  var result/eax: boolean <- gap-buffer-equal? g, "aa"
+  check-not result, "F - test-gap-buffer-equal-fails"
+}
+
+fn gap-buffers-equal? self: (addr gap-buffer), g: (addr gap-buffer) -> _/eax: boolean {
+  var tmp/eax: int <- gap-buffer-length self
+  var len/ecx: int <- copy tmp
+  var leng/eax: int <- gap-buffer-length g
+  compare len, leng
+  {
+    break-if-=
+    return 0/false
+  }
+  var i/edx: int <- copy 0
+  {
+    compare i, len
+    break-if->=
+    {
+      var tmp/eax: grapheme <- gap-index self, i
+      var curr/ecx: grapheme <- copy tmp
+      var currg/eax: grapheme <- gap-index g, i
+      compare curr, currg
+      break-if-=
+      return 0/false
+    }
+    i <- increment
+    loop
+  }
+  return 1/true
+}
+
+fn gap-index _self: (addr gap-buffer), _n: int -> _/eax: grapheme {
+  var self/esi: (addr gap-buffer) <- copy _self
+  var n/ebx: int <- copy _n
+  # if n < left->length, index into left
+  var left/edi: (addr grapheme-stack) <- get self, left
+  var left-len-a/edx: (addr int) <- get left, top
+  compare n, *left-len-a
+  {
+    break-if->=
+    var data-ah/eax: (addr handle array grapheme) <- get left, data
+    var data/eax: (addr array grapheme) <- lookup *data-ah
+    var result/eax: (addr grapheme) <- index data, n
+    return *result
+  }
+  # shrink n
+  n <- subtract *left-len-a
+  # if n < right->length, index into right
+  var right/edi: (addr grapheme-stack) <- get self, right
+  var right-len-a/edx: (addr int) <- get right, top
+  compare n, *right-len-a
+  {
+    break-if->=
+    var data-ah/eax: (addr handle array grapheme) <- get right, data
+    var data/eax: (addr array grapheme) <- lookup *data-ah
+    # idx = right->len - n - 1
+    var idx/ebx: int <- copy n
+    idx <- subtract *right-len-a
+    idx <- negate
+    idx <- subtract 1
+    var result/eax: (addr grapheme) <- index data, idx
+    return *result
+  }
+  # error
+  abort "gap-index: out of bounds"
+  return 0
+}
+
+fn test-gap-buffers-equal? {
+  var _a: gap-buffer
+  var a/esi: (addr gap-buffer) <- address _a
+  initialize-gap-buffer-with a, "abc"
+  var _b: gap-buffer
+  var b/edi: (addr gap-buffer) <- address _b
+  initialize-gap-buffer-with b, "abc"
+  var _c: gap-buffer
+  var c/ebx: (addr gap-buffer) <- address _c
+  initialize-gap-buffer-with c, "ab"
+  var _d: gap-buffer
+  var d/edx: (addr gap-buffer) <- address _d
+  initialize-gap-buffer-with d, "abd"
+  #
+  var result/eax: boolean <- gap-buffers-equal? a, a
+  check result, "F - test-gap-buffers-equal? - reflexive"
+  result <- gap-buffers-equal? a, b
+  check result, "F - test-gap-buffers-equal? - equal"
+  # length not equal
+  result <- gap-buffers-equal? a, c
+  check-not result, "F - test-gap-buffers-equal? - not equal"
+  # contents not equal
+  result <- gap-buffers-equal? a, d
+  check-not result, "F - test-gap-buffers-equal? - not equal 2"
+  result <- gap-buffers-equal? d, a
+  check-not result, "F - test-gap-buffers-equal? - not equal 3"
+}
+
+fn test-gap-buffer-index {
+  var gap-storage: gap-buffer
+  var gap/esi: (addr gap-buffer) <- address gap-storage
+  initialize-gap-buffer-with gap, "abc"
+  # gap is at end, all contents are in left
+  var g/eax: grapheme <- gap-index gap, 0
+  var x/ecx: int <- copy g
+  check-ints-equal x, 0x61/a, "F - test-gap-index/left-1"
+  var g/eax: grapheme <- gap-index gap, 1
+  var x/ecx: int <- copy g
+  check-ints-equal x, 0x62/b, "F - test-gap-index/left-2"
+  var g/eax: grapheme <- gap-index gap, 2
+  var x/ecx: int <- copy g
+  check-ints-equal x, 0x63/c, "F - test-gap-index/left-3"
+  # now check when everything is to the right
+  gap-to-start gap
+  rewind-gap-buffer gap
+  var g/eax: grapheme <- gap-index gap, 0
+  var x/ecx: int <- copy g
+  check-ints-equal x, 0x61/a, "F - test-gap-index/right-1"
+  var g/eax: grapheme <- gap-index gap, 1
+  var x/ecx: int <- copy g
+  check-ints-equal x, 0x62/b, "F - test-gap-index/right-2"
+  var g/eax: grapheme <- gap-index gap, 2
+  var x/ecx: int <- copy g
+  check-ints-equal x, 0x63/c, "F - test-gap-index/right-3"
+}
+
+fn copy-gap-buffer _src-ah: (addr handle gap-buffer), _dest-ah: (addr handle gap-buffer) {
+  # obtain src-a, dest-a
+  var src-ah/eax: (addr handle gap-buffer) <- copy _src-ah
+  var _src-a/eax: (addr gap-buffer) <- lookup *src-ah
+  var src-a/esi: (addr gap-buffer) <- copy _src-a
+  var dest-ah/eax: (addr handle gap-buffer) <- copy _dest-ah
+  var _dest-a/eax: (addr gap-buffer) <- lookup *dest-ah
+  var dest-a/edi: (addr gap-buffer) <- copy _dest-a
+  # copy left grapheme-stack
+  var src/ecx: (addr grapheme-stack) <- get src-a, left
+  var dest/edx: (addr grapheme-stack) <- get dest-a, left
+  copy-grapheme-stack src, dest
+  # copy right grapheme-stack
+  src <- get src-a, right
+  dest <- get dest-a, right
+  copy-grapheme-stack src, dest
+}
+
+fn gap-buffer-is-decimal-integer? _self: (addr gap-buffer) -> _/eax: boolean {
+  var self/esi: (addr gap-buffer) <- copy _self
+  var curr/ecx: (addr grapheme-stack) <- get self, left
+  var result/eax: boolean <- grapheme-stack-is-decimal-integer? curr
+  {
+    compare result, 0/false
+    break-if-=
+    curr <- get self, right
+    result <- grapheme-stack-is-decimal-integer? curr
+  }
+  return result
+}
+
+fn test-render-gap-buffer-without-cursor {
+  # setup
+  var gap-storage: gap-buffer
+  var gap/esi: (addr gap-buffer) <- address gap-storage
+  initialize-gap-buffer-with gap, "abc"
+  # setup: screen
+  var screen-on-stack: screen
+  var screen/edi: (addr screen) <- address screen-on-stack
+  initialize-screen screen, 5, 4, 0/no-pixel-graphics
+  #
+  var x/eax: int <- render-gap-buffer screen, gap, 0/x, 0/y, 0/no-cursor, 3/fg, 0xc5/bg=blue-bg
+  check-screen-row screen, 0/y, "abc ", "F - test-render-gap-buffer-without-cursor"
+  check-ints-equal x, 4, "F - test-render-gap-buffer-without-cursor: result"
+                                                                # abc
+  check-background-color-in-screen-row screen, 3/bg=reverse, 0/y, "    ", "F - test-render-gap-buffer-without-cursor: bg"
+}
+
+fn test-render-gap-buffer-with-cursor-at-end {
+  # setup
+  var gap-storage: gap-buffer
+  var gap/esi: (addr gap-buffer) <- address gap-storage
+  initialize-gap-buffer-with gap, "abc"
+  gap-to-end gap
+  # setup: screen
+  var screen-on-stack: screen
+  var screen/edi: (addr screen) <- address screen-on-stack
+  initialize-screen screen, 5, 4, 0/no-pixel-graphics
+  #
+  var x/eax: int <- render-gap-buffer screen, gap, 0/x, 0/y, 1/show-cursor, 3/fg, 0xc5/bg=blue-bg
+  check-screen-row screen, 0/y, "abc ", "F - test-render-gap-buffer-with-cursor-at-end"
+  # we've drawn one extra grapheme for the cursor
+  check-ints-equal x, 4, "F - test-render-gap-buffer-with-cursor-at-end: result"
+                                                                # abc
+  check-background-color-in-screen-row screen, 3/bg=reverse, 0/y, "   |", "F - test-render-gap-buffer-with-cursor-at-end: bg"
+}
+
+fn test-render-gap-buffer-with-cursor-in-middle {
+  # setup
+  var gap-storage: gap-buffer
+  var gap/esi: (addr gap-buffer) <- address gap-storage
+  initialize-gap-buffer-with gap, "abc"
+  gap-to-end gap
+  var dummy/eax: grapheme <- gap-left gap
+  # setup: screen
+  var screen-on-stack: screen
+  var screen/edi: (addr screen) <- address screen-on-stack
+  initialize-screen screen, 5, 4, 0/no-pixel-graphics
+  #
+  var x/eax: int <- render-gap-buffer screen, gap, 0/x, 0/y, 1/show-cursor, 3/fg, 0xc5/bg=blue-bg
+  check-screen-row screen, 0/y, "abc ", "F - test-render-gap-buffer-with-cursor-in-middle"
+  check-ints-equal x, 4, "F - test-render-gap-buffer-with-cursor-in-middle: result"
+                                                                # abc
+  check-background-color-in-screen-row screen, 3/bg=reverse, 0/y, "  | ", "F - test-render-gap-buffer-with-cursor-in-middle: bg"
+}
+
+fn test-render-gap-buffer-with-cursor-at-start {
+  var gap-storage: gap-buffer
+  var gap/esi: (addr gap-buffer) <- address gap-storage
+  initialize-gap-buffer-with gap, "abc"
+  gap-to-start gap
+  # setup: screen
+  var screen-on-stack: screen
+  var screen/edi: (addr screen) <- address screen-on-stack
+  initialize-screen screen, 5, 4, 0/no-pixel-graphics
+  #
+  var x/eax: int <- render-gap-buffer screen, gap, 0/x, 0/y, 1/show-cursor, 3/fg, 0xc5/bg=blue-bg
+  check-screen-row screen, 0/y, "abc ", "F - test-render-gap-buffer-with-cursor-at-start"
+  check-ints-equal x, 4, "F - test-render-gap-buffer-with-cursor-at-start: result"
+                                                                # abc
+  check-background-color-in-screen-row screen, 3/bg=reverse, 0/y, "|   ", "F - test-render-gap-buffer-with-cursor-at-start: bg"
+}
+
+fn test-render-gap-buffer-highlight-matching-close-paren {
+  var gap-storage: gap-buffer
+  var gap/esi: (addr gap-buffer) <- address gap-storage
+  initialize-gap-buffer-with gap, "(a)"
+  gap-to-start gap
+  # setup: screen
+  var screen-on-stack: screen
+  var screen/edi: (addr screen) <- address screen-on-stack
+  initialize-screen screen, 5, 4, 0/no-pixel-graphics
+  #
+  var x/eax: int <- render-gap-buffer screen, gap, 0/x, 0/y, 1/show-cursor, 3/fg, 0xc5/bg=blue-bg
+  check-screen-row                     screen, 0/y,                   "(a) ", "F - test-render-gap-buffer-highlight-matching-close-paren"
+  check-ints-equal x, 4, "F - test-render-gap-buffer-highlight-matching-close-paren: result"
+  check-background-color-in-screen-row screen, 3/bg=reverse,      0/y, "|   ", "F - test-render-gap-buffer-highlight-matching-close-paren: cursor"
+  check-screen-row-in-color            screen, 0xf/fg=highlight, 0/y, "  ) ", "F - test-render-gap-buffer-highlight-matching-close-paren: matching paren"
+}
+
+fn test-render-gap-buffer-highlight-matching-open-paren {
+  var gap-storage: gap-buffer
+  var gap/esi: (addr gap-buffer) <- address gap-storage
+  initialize-gap-buffer-with gap, "(a)"
+  gap-to-end gap
+  var dummy/eax: grapheme <- gap-left gap
+  # setup: screen
+  var screen-on-stack: screen
+  var screen/edi: (addr screen) <- address screen-on-stack
+  initialize-screen screen, 5, 4, 0/no-pixel-graphics
+  #
+  var x/eax: int <- render-gap-buffer screen, gap, 0/x, 0/y, 1/show-cursor, 3/fg, 0xc5/bg=blue-bg
+  check-screen-row                     screen, 0/y,                   "(a) ", "F - test-render-gap-buffer-highlight-matching-open-paren"
+  check-ints-equal x, 4, "F - test-render-gap-buffer-highlight-matching-open-paren: result"
+  check-background-color-in-screen-row screen, 3/bg=reverse,      0/y, "  | ", "F - test-render-gap-buffer-highlight-matching-open-paren: cursor"
+  check-screen-row-in-color            screen, 0xf/fg=highlight, 0/y, "(   ", "F - test-render-gap-buffer-highlight-matching-open-paren: matching paren"
+}
+
+fn test-render-gap-buffer-highlight-matching-open-paren-of-end {
+  var gap-storage: gap-buffer
+  var gap/esi: (addr gap-buffer) <- address gap-storage
+  initialize-gap-buffer-with gap, "(a)"
+  gap-to-end gap
+  # setup: screen
+  var screen-on-stack: screen
+  var screen/edi: (addr screen) <- address screen-on-stack
+  initialize-screen screen, 5, 4, 0/no-pixel-graphics
+  #
+  var x/eax: int <- render-gap-buffer screen, gap, 0/x, 0/y, 1/show-cursor, 3/fg, 0xc5/bg=blue-bg
+  check-screen-row                     screen, 0/y,                   "(a) ", "F - test-render-gap-buffer-highlight-matching-open-paren-of-end"
+  check-ints-equal x, 4, "F - test-render-gap-buffer-highlight-matching-open-paren-of-end: result"
+  check-background-color-in-screen-row screen, 3/bg=reverse,      0/y, "   |", "F - test-render-gap-buffer-highlight-matching-open-paren-of-end: cursor"
+  check-screen-row-in-color            screen, 0xf/fg=highlight, 0/y, "(   ", "F - test-render-gap-buffer-highlight-matching-open-paren-of-end: matching paren"
+}
+
+# should I highlight a matching open paren? And if so, at what depth from top of left?
+# basically there are two cases to disambiguate here:
+#   Usually the cursor is at top of right. Highlight first '(' at depth 0 from top of left.
+#   If right is empty, match the ')' _before_ cursor. Highlight first '(' at depth _1_ from top of left.
+fn highlight-matching-open-paren? _gap: (addr gap-buffer), render-cursor?: boolean -> _/ebx: boolean, _/edi: int {
+  # if not rendering cursor, return
+  compare render-cursor?, 0/false
+  {
+    break-if-!=
+    return 0/false, 0
+  }
+  var gap/esi: (addr gap-buffer) <- copy _gap
+  var stack/edi: (addr grapheme-stack) <- get gap, right
+  var top-addr/eax: (addr int) <- get stack, top
+  var top-index/ecx: int <- copy *top-addr
+  compare top-index, 0
+  {
+    break-if->
+    # if cursor at end, return (char before cursor == ')', 1)
+    stack <- get gap, left
+    top-addr <- get stack, top
+    top-index <- copy *top-addr
+    compare top-index, 0
+    {
+      break-if->
+      return 0/false, 0
+    }
+    top-index <- decrement
+    var data-ah/eax: (addr handle array grapheme) <- get stack, data
+    var data/eax: (addr array grapheme) <- lookup *data-ah
+    var g/eax: (addr grapheme) <- index data, top-index
+    compare *g, 0x29/close-paren
+    {
+      break-if-=
+      return 0/false, 0
+    }
+    return 1/true, 1
+  }
+  # cursor is not at end; return (char at cursor == ')')
+  top-index <- decrement
+  var data-ah/eax: (addr handle array grapheme) <- get stack, data
+  var data/eax: (addr array grapheme) <- lookup *data-ah
+  var g/eax: (addr grapheme) <- index data, top-index
+  compare *g, 0x29/close-paren
+  {
+    break-if-=
+    return 0/false, 0
+  }
+  return 1/true, 0
+}
+
+fn test-highlight-matching-open-paren {
+  var gap-storage: gap-buffer
+  var gap/esi: (addr gap-buffer) <- address gap-storage
+  initialize-gap-buffer-with gap, "(a)"
+  gap-to-end gap
+  var highlight-matching-open-paren?/ebx: boolean <- copy 0/false
+  var open-paren-depth/edi: int <- copy 0
+  highlight-matching-open-paren?, open-paren-depth <- highlight-matching-open-paren? gap, 0/no-cursor
+  check-not highlight-matching-open-paren?, "F - test-highlight-matching-open-paren: no cursor"
+  highlight-matching-open-paren?, open-paren-depth <- highlight-matching-open-paren? gap, 1/render-cursor
+  check highlight-matching-open-paren?, "F - test-highlight-matching-open-paren: at end immediately after ')'"
+  check-ints-equal open-paren-depth, 1, "F - test-highlight-matching-open-paren: depth at end immediately after ')'"
+  var dummy/eax: grapheme <- gap-left gap
+  highlight-matching-open-paren?, open-paren-depth <- highlight-matching-open-paren? gap, 1/render-cursor
+  check highlight-matching-open-paren?, "F - test-highlight-matching-open-paren: on ')'"
+  dummy <- gap-left gap
+  highlight-matching-open-paren?, open-paren-depth <- highlight-matching-open-paren? gap, 1/render-cursor
+  check-not highlight-matching-open-paren?, "F - test-highlight-matching-open-paren: not on ')'"
+}
+
+## some primitives for scanning through a gap buffer
+# don't modify the gap buffer while scanning
+# this includes moving the cursor around
+
+# restart scan without affecting gap-buffer contents
+fn rewind-gap-buffer _self: (addr gap-buffer) {
+  var self/esi: (addr gap-buffer) <- copy _self
+  var dest/eax: (addr int) <- get self, left-read-index
+  copy-to *dest, 0
+  dest <- get self, right-read-index
+  copy-to *dest, 0
+}
+
+fn gap-buffer-scan-done? _self: (addr gap-buffer) -> _/eax: boolean {
+  var self/esi: (addr gap-buffer) <- copy _self
+  # more in left?
+  var left/eax: (addr grapheme-stack) <- get self, left
+  var left-size/eax: int <- grapheme-stack-length left
+  var left-read-index/ecx: (addr int) <- get self, left-read-index
+  compare *left-read-index, left-size
+  {
+    break-if->=
+    return 0/false
+  }
+  # more in right?
+  var right/eax: (addr grapheme-stack) <- get self, right
+  var right-size/eax: int <- grapheme-stack-length right
+  var right-read-index/ecx: (addr int) <- get self, right-read-index
+  compare *right-read-index, right-size
+  {
+    break-if->=
+    return 0/false
+  }
+  #
+  return 1/true
+}
+
+fn peek-from-gap-buffer _self: (addr gap-buffer) -> _/eax: grapheme {
+  var self/esi: (addr gap-buffer) <- copy _self
+  # more in left?
+  var left/ecx: (addr grapheme-stack) <- get self, left
+  var left-size/eax: int <- grapheme-stack-length left
+  var left-read-index-a/edx: (addr int) <- get self, left-read-index
+  compare *left-read-index-a, left-size
+  {
+    break-if->=
+    var left-data-ah/eax: (addr handle array grapheme) <- get left, data
+    var left-data/eax: (addr array grapheme) <- lookup *left-data-ah
+    var left-read-index/ecx: int <- copy *left-read-index-a
+    var result/eax: (addr grapheme) <- index left-data, left-read-index
+    return *result
+  }
+  # more in right?
+  var right/ecx: (addr grapheme-stack) <- get self, right
+  var _right-size/eax: int <- grapheme-stack-length right
+  var right-size/ebx: int <- copy _right-size
+  var right-read-index-a/edx: (addr int) <- get self, right-read-index
+  compare *right-read-index-a, right-size
+  {
+    break-if->=
+    # read the right from reverse
+    var right-data-ah/eax: (addr handle array grapheme) <- get right, data
+    var right-data/eax: (addr array grapheme) <- lookup *right-data-ah
+    var right-read-index/ebx: int <- copy right-size
+    right-read-index <- subtract *right-read-index-a
+    right-read-index <- subtract 1
+    var result/eax: (addr grapheme) <- index right-data, right-read-index
+    return *result
+  }
+  # if we get here there's nothing left
+  return 0/nul
+}
+
+fn read-from-gap-buffer _self: (addr gap-buffer) -> _/eax: grapheme {
+  var self/esi: (addr gap-buffer) <- copy _self
+  # more in left?
+  var left/ecx: (addr grapheme-stack) <- get self, left
+  var left-size/eax: int <- grapheme-stack-length left
+  var left-read-index-a/edx: (addr int) <- get self, left-read-index
+  compare *left-read-index-a, left-size
+  {
+    break-if->=
+    var left-data-ah/eax: (addr handle array grapheme) <- get left, data
+    var left-data/eax: (addr array grapheme) <- lookup *left-data-ah
+    var left-read-index/ecx: int <- copy *left-read-index-a
+    var result/eax: (addr grapheme) <- index left-data, left-read-index
+    increment *left-read-index-a
+    return *result
+  }
+  # more in right?
+  var right/ecx: (addr grapheme-stack) <- get self, right
+  var _right-size/eax: int <- grapheme-stack-length right
+  var right-size/ebx: int <- copy _right-size
+  var right-read-index-a/edx: (addr int) <- get self, right-read-index
+  compare *right-read-index-a, right-size
+  {
+    break-if->=
+    # read the right from reverse
+    var right-data-ah/eax: (addr handle array grapheme) <- get right, data
+    var right-data/eax: (addr array grapheme) <- lookup *right-data-ah
+    var right-read-index/ebx: int <- copy right-size
+    right-read-index <- subtract *right-read-index-a
+    right-read-index <- subtract 1
+    var result/eax: (addr grapheme) <- index right-data, right-read-index
+    increment *right-read-index-a
+    return *result
+  }
+  # if we get here there's nothing left
+  return 0/nul
+}
+
+fn put-back-from-gap-buffer _self: (addr gap-buffer) {
+  var self/esi: (addr gap-buffer) <- copy _self
+  # more in right?
+  var right/eax: (addr grapheme-stack) <- get self, right
+  var right-size/eax: int <- grapheme-stack-length right
+  var right-read-index-a/eax: (addr int) <- get self, right-read-index
+  compare *right-read-index-a, 0
+  {
+    break-if-<=
+    decrement *right-read-index-a
+    return
+  }
+  # more in left?
+  var left/eax: (addr grapheme-stack) <- get self, left
+  var left-size/eax: int <- grapheme-stack-length left
+  var left-read-index-a/eax: (addr int) <- get self, left-read-index
+  decrement *left-read-index-a
+}
+
+fn test-read-from-gap-buffer {
+  var gap-storage: gap-buffer
+  var gap/esi: (addr gap-buffer) <- address gap-storage
+  initialize-gap-buffer-with gap, "abc"
+  # gap is at end, all contents are in left
+  var done?/eax: boolean <- gap-buffer-scan-done? gap
+  check-not done?, "F - test-read-from-gap-buffer/left-1/done"
+  var g/eax: grapheme <- read-from-gap-buffer gap
+  var x/ecx: int <- copy g
+  check-ints-equal x, 0x61/a, "F - test-read-from-gap-buffer/left-1"
+  var done?/eax: boolean <- gap-buffer-scan-done? gap
+  check-not done?, "F - test-read-from-gap-buffer/left-2/done"
+  var g/eax: grapheme <- read-from-gap-buffer gap
+  var x/ecx: int <- copy g
+  check-ints-equal x, 0x62/b, "F - test-read-from-gap-buffer/left-2"
+  var done?/eax: boolean <- gap-buffer-scan-done? gap
+  check-not done?, "F - test-read-from-gap-buffer/left-3/done"
+  var g/eax: grapheme <- read-from-gap-buffer gap
+  var x/ecx: int <- copy g
+  check-ints-equal x, 0x63/c, "F - test-read-from-gap-buffer/left-3"
+  var done?/eax: boolean <- gap-buffer-scan-done? gap
+  check done?, "F - test-read-from-gap-buffer/left-4/done"
+  var g/eax: grapheme <- read-from-gap-buffer gap
+  var x/ecx: int <- copy g
+  check-ints-equal x, 0/nul, "F - test-read-from-gap-buffer/left-4"
+  # now check when everything is to the right
+  gap-to-start gap
+  rewind-gap-buffer gap
+  var done?/eax: boolean <- gap-buffer-scan-done? gap
+  check-not done?, "F - test-read-from-gap-buffer/right-1/done"
+  var g/eax: grapheme <- read-from-gap-buffer gap
+  var x/ecx: int <- copy g
+  check-ints-equal x, 0x61/a, "F - test-read-from-gap-buffer/right-1"
+  var done?/eax: boolean <- gap-buffer-scan-done? gap
+  check-not done?, "F - test-read-from-gap-buffer/right-2/done"
+  var g/eax: grapheme <- read-from-gap-buffer gap
+  var x/ecx: int <- copy g
+  check-ints-equal x, 0x62/b, "F - test-read-from-gap-buffer/right-2"
+  var done?/eax: boolean <- gap-buffer-scan-done? gap
+  check-not done?, "F - test-read-from-gap-buffer/right-3/done"
+  var g/eax: grapheme <- read-from-gap-buffer gap
+  var x/ecx: int <- copy g
+  check-ints-equal x, 0x63/c, "F - test-read-from-gap-buffer/right-3"
+  var done?/eax: boolean <- gap-buffer-scan-done? gap
+  check done?, "F - test-read-from-gap-buffer/right-4/done"
+  var g/eax: grapheme <- read-from-gap-buffer gap
+  var x/ecx: int <- copy g
+  check-ints-equal x, 0/nul, "F - test-read-from-gap-buffer/right-4"
+}
+
+fn skip-spaces-from-gap-buffer self: (addr gap-buffer) {
+  var done?/eax: boolean <- gap-buffer-scan-done? self
+  compare done?, 0/false
+  break-if-!=
+  var g/eax: grapheme <- peek-from-gap-buffer self
+  {
+    compare g, 0x20/space
+    break-if-=
+    return
+  }
+  g <- read-from-gap-buffer self
+  loop
+}
+
+fn edit-gap-buffer self: (addr gap-buffer), key: grapheme {
+  var g/edx: grapheme <- copy key
+  {
+    compare g, 8/backspace
+    break-if-!=
+    delete-before-gap self
+    return
+  }
+  {
+    compare g, 0x80/left-arrow
+    break-if-!=
+    var dummy/eax: grapheme <- gap-left self
+    return
+  }
+  {
+    compare g, 0x83/right-arrow
+    break-if-!=
+    var dummy/eax: grapheme <- gap-right self
+    return
+  }
+  {
+    compare g, 6/ctrl-f
+    break-if-!=
+    gap-to-start-of-next-word self
+    return
+  }
+  {
+    compare g, 2/ctrl-b
+    break-if-!=
+    gap-to-end-of-previous-word self
+    return
+  }
+  {
+    compare g, 1/ctrl-a
+    break-if-!=
+    gap-to-previous-start-of-line self
+    return
+  }
+  {
+    compare g, 5/ctrl-e
+    break-if-!=
+    gap-to-next-end-of-line self
+    return
+  }
+  {
+    compare g, 0x81/down-arrow
+    break-if-!=
+    gap-down self
+    return
+  }
+  {
+    compare g, 0x82/up-arrow
+    break-if-!=
+    gap-up self
+    return
+  }
+  {
+    compare g, 0x15/ctrl-u
+    break-if-!=
+    clear-gap-buffer self
+    return
+  }
+  {
+    compare g, 9/tab
+    break-if-!=
+    # tab = 2 spaces
+    add-code-point-at-gap self, 0x20/space
+    add-code-point-at-gap self, 0x20/space
+    return
+  }
+  # default: insert character
+  add-grapheme-at-gap self, g
+}
+
+fn gap-to-start-of-next-word self: (addr gap-buffer) {
+  var curr/eax: grapheme <- copy 0
+  # skip to next space
+  {
+    curr <- gap-right self
+    compare curr, -1
+    break-if-=
+    compare curr, 0x20/space
+    break-if-=
+    compare curr, 0xa/newline
+    break-if-=
+    loop
+  }
+  # skip past spaces
+  {
+    curr <- gap-right self
+    compare curr, -1
+    break-if-=
+    compare curr, 0x20/space
+    loop-if-=
+    compare curr, 0xa/space
+    loop-if-=
+    curr <- gap-left self
+    break
+  }
+}
+
+fn gap-to-end-of-previous-word self: (addr gap-buffer) {
+  var curr/eax: grapheme <- copy 0
+  # skip to previous space
+  {
+    curr <- gap-left self
+    compare curr, -1
+    break-if-=
+    compare curr, 0x20/space
+    break-if-=
+    compare curr, 0xa/newline
+    break-if-=
+    loop
+  }
+  # skip past all spaces but one
+  {
+    curr <- gap-left self
+    compare curr, -1
+    break-if-=
+    compare curr, 0x20/space
+    loop-if-=
+    compare curr, 0xa/space
+    loop-if-=
+    curr <- gap-right self
+    break
+  }
+}
+
+fn gap-to-previous-start-of-line self: (addr gap-buffer) {
+  # skip past immediate newline
+  var dummy/eax: grapheme <- gap-left self
+  # skip to previous newline
+  {
+    dummy <- gap-left self
+    {
+      compare dummy, -1
+      break-if-!=
+      return
+    }
+    {
+      compare dummy, 0xa/newline
+      break-if-!=
+      dummy <- gap-right self
+      return
+    }
+    loop
+  }
+}
+
+fn gap-to-next-end-of-line self: (addr gap-buffer) {
+  # skip past immediate newline
+  var dummy/eax: grapheme <- gap-right self
+  # skip to next newline
+  {
+    dummy <- gap-right self
+    {
+      compare dummy, -1
+      break-if-!=
+      return
+    }
+    {
+      compare dummy, 0xa/newline
+      break-if-!=
+      dummy <- gap-left self
+      return
+    }
+    loop
+  }
+}
+
+fn gap-up self: (addr gap-buffer) {
+  # compute column
+  var col/edx: int <- count-columns-to-start-of-line self
+  #
+  gap-to-previous-start-of-line self
+  # skip ahead by up to col on previous line
+  var i/ecx: int <- copy 0
+  {
+    compare i, col
+    break-if->=
+    var curr/eax: grapheme <- gap-right self
+    {
+      compare curr, -1
+      break-if-!=
+      return
+    }
+    compare curr, 0xa/newline
+    {
+      break-if-!=
+      curr <- gap-left self
+      return
+    }
+    i <- increment
+    loop
+  }
+}
+
+fn gap-down self: (addr gap-buffer) {
+  # compute column
+  var col/edx: int <- count-columns-to-start-of-line self
+  # skip to start of next line
+  gap-to-end-of-line self
+  var dummy/eax: grapheme <- gap-right self
+  # skip ahead by up to col on previous line
+  var i/ecx: int <- copy 0
+  {
+    compare i, col
+    break-if->=
+    var curr/eax: grapheme <- gap-right self
+    {
+      compare curr, -1
+      break-if-!=
+      return
+    }
+    compare curr, 0xa/newline
+    {
+      break-if-!=
+      curr <- gap-left self
+      return
+    }
+    i <- increment
+    loop
+  }
+}
+
+fn count-columns-to-start-of-line self: (addr gap-buffer) -> _/edx: int {
+  var count/edx: int <- copy 0
+  var dummy/eax: grapheme <- copy 0
+  # skip to previous newline
+  {
+    dummy <- gap-left self
+    {
+      compare dummy, -1
+      break-if-!=
+      return count
+    }
+    {
+      compare dummy, 0xa/newline
+      break-if-!=
+      dummy <- gap-right self
+      return count
+    }
+    count <- increment
+    loop
+  }
+  return count
+}
+
+fn gap-to-end-of-line self: (addr gap-buffer) {
+  var dummy/eax: grapheme <- copy 0
+  # skip to next newline
+  {
+    dummy <- gap-right self
+    {
+      compare dummy, -1
+      break-if-!=
+      return
+    }
+    {
+      compare dummy, 0xa/newline
+      break-if-!=
+      dummy <- gap-left self
+      return
+    }
+    loop
+  }
+}
diff --git a/browse-slack/grapheme-stack.mu b/browse-slack/grapheme-stack.mu
new file mode 100644
index 00000000..13f0e31c
--- /dev/null
+++ b/browse-slack/grapheme-stack.mu
@@ -0,0 +1,556 @@
+# grapheme stacks are the smallest unit of editable text
+
+type grapheme-stack {
+  data: (handle array grapheme)
+  top: int
+}
+
+fn initialize-grapheme-stack _self: (addr grapheme-stack), n: int {
+  var self/esi: (addr grapheme-stack) <- copy _self
+  var d/edi: (addr handle array grapheme) <- get self, data
+  populate d, n
+  var top/eax: (addr int) <- get self, top
+  copy-to *top, 0
+}
+
+fn clear-grapheme-stack _self: (addr grapheme-stack) {
+  var self/esi: (addr grapheme-stack) <- copy _self
+  var top/eax: (addr int) <- get self, top
+  copy-to *top, 0
+}
+
+fn grapheme-stack-empty? _self: (addr grapheme-stack) -> _/eax: boolean {
+  var self/esi: (addr grapheme-stack) <- copy _self
+  var top/eax: (addr int) <- get self, top
+  compare *top, 0
+  {
+    break-if-!=
+    return 1/true
+  }
+  return 0/false
+}
+
+fn grapheme-stack-length _self: (addr grapheme-stack) -> _/eax: int {
+  var self/esi: (addr grapheme-stack) <- copy _self
+  var top/eax: (addr int) <- get self, top
+  return *top
+}
+
+fn push-grapheme-stack _self: (addr grapheme-stack), _val: grapheme {
+  var self/esi: (addr grapheme-stack) <- copy _self
+  var top-addr/ecx: (addr int) <- get self, top
+  var data-ah/edx: (addr handle array grapheme) <- get self, data
+  var data/eax: (addr array grapheme) <- lookup *data-ah
+  var top/edx: int <- copy *top-addr
+  var dest-addr/edx: (addr grapheme) <- index data, top
+  var val/eax: grapheme <- copy _val
+  copy-to *dest-addr, val
+  add-to *top-addr, 1
+}
+
+fn pop-grapheme-stack _self: (addr grapheme-stack) -> _/eax: grapheme {
+  var self/esi: (addr grapheme-stack) <- copy _self
+  var top-addr/ecx: (addr int) <- get self, top
+  {
+    compare *top-addr, 0
+    break-if->
+    return -1
+  }
+  subtract-from *top-addr, 1
+  var data-ah/edx: (addr handle array grapheme) <- get self, data
+  var data/eax: (addr array grapheme) <- lookup *data-ah
+  var top/edx: int <- copy *top-addr
+  var result-addr/eax: (addr grapheme) <- index data, top
+  return *result-addr
+}
+
+fn copy-grapheme-stack _src: (addr grapheme-stack), dest: (addr grapheme-stack) {
+  var src/esi: (addr grapheme-stack) <- copy _src
+  var data-ah/edi: (addr handle array grapheme) <- get src, data
+  var _data/eax: (addr array grapheme) <- lookup *data-ah
+  var data/edi: (addr array grapheme) <- copy _data
+  var top-addr/ecx: (addr int) <- get src, top
+  var i/eax: int <- copy 0
+  {
+    compare i, *top-addr
+    break-if->=
+    var g/edx: (addr grapheme) <- index data, i
+    push-grapheme-stack dest, *g
+    i <- increment
+    loop
+  }
+}
+
+# dump stack to screen from bottom to top
+# hardcoded colors:
+#   matching paren
+fn render-stack-from-bottom-wrapping-right-then-down screen: (addr screen), _self: (addr grapheme-stack), xmin: int, ymin: int, xmax: int, ymax: int, _x: int, _y: int, highlight-matching-open-paren?: boolean, open-paren-depth: int, color: int, background-color: int -> _/eax: int, _/ecx: int {
+  var self/esi: (addr grapheme-stack) <- copy _self
+  var matching-open-paren-index/edx: int <- get-matching-open-paren-index self, highlight-matching-open-paren?, open-paren-depth
+  var data-ah/edi: (addr handle array grapheme) <- get self, data
+  var _data/eax: (addr array grapheme) <- lookup *data-ah
+  var data/edi: (addr array grapheme) <- copy _data
+  var x/eax: int <- copy _x
+  var y/ecx: int <- copy _y
+  var top-addr/esi: (addr int) <- get self, top
+  var i/ebx: int <- copy 0
+  {
+    compare i, *top-addr
+    break-if->=
+    {
+      var g/esi: (addr grapheme) <- index data, i
+      var fg: int
+      {
+        var tmp/eax: int <- copy color
+        copy-to fg, tmp
+      }
+      {
+        compare i, matching-open-paren-index
+        break-if-!=
+        copy-to fg, 0xf/highlight
+      }
+      x, y <- render-grapheme screen, *g, xmin, ymin, xmax, ymax, x, y, fg, background-color
+    }
+    i <- increment
+    loop
+  }
+  return x, y
+}
+
+# helper for small words
+fn render-stack-from-bottom screen: (addr screen), self: (addr grapheme-stack), x: int, y: int, highlight-matching-open-paren?: boolean, open-paren-depth: int -> _/eax: int {
+  var _width/eax: int <- copy 0
+  var _height/ecx: int <- copy 0
+  _width, _height <- screen-size screen
+  var width/edx: int <- copy _width
+  var height/ebx: int <- copy _height
+  var x2/eax: int <- copy 0
+  var y2/ecx: int <- copy 0
+  x2, y2 <- render-stack-from-bottom-wrapping-right-then-down screen, self, x, y, width, height, x, y, highlight-matching-open-paren?, open-paren-depth, 3/fg=cyan, 0xc5/bg=blue-bg
+  return x2  # y2? yolo
+}
+
+# dump stack to screen from top to bottom
+# optionally render a 'cursor' with the top grapheme
+# hard-coded colors:
+#   matching paren
+#   cursor
+fn render-stack-from-top-wrapping-right-then-down screen: (addr screen), _self: (addr grapheme-stack), xmin: int, ymin: int, xmax: int, ymax: int, _x: int, _y: int, render-cursor?: boolean, color: int, background-color: int -> _/eax: int, _/ecx: int {
+  var self/esi: (addr grapheme-stack) <- copy _self
+  var matching-close-paren-index/edx: int <- get-matching-close-paren-index self, render-cursor?
+  var data-ah/eax: (addr handle array grapheme) <- get self, data
+  var _data/eax: (addr array grapheme) <- lookup *data-ah
+  var data/edi: (addr array grapheme) <- copy _data
+  var x/eax: int <- copy _x
+  var y/ecx: int <- copy _y
+  var top-addr/ebx: (addr int) <- get self, top
+  var i/ebx: int <- copy *top-addr
+  i <- decrement
+  # if render-cursor?, peel off first iteration
+  {
+    compare render-cursor?, 0/false
+    break-if-=
+    compare i, 0
+    break-if-<
+    var g/esi: (addr grapheme) <- index data, i
+    x, y <- render-grapheme screen, *g, xmin, ymin, xmax, ymax, x, y, background-color, color
+    i <- decrement
+  }
+  # remaining iterations
+  {
+    compare i, 0
+    break-if-<
+    # highlight matching paren if needed
+    var fg: int
+    {
+      var tmp/eax: int <- copy color
+      copy-to fg, tmp
+    }
+    compare i, matching-close-paren-index
+    {
+      break-if-!=
+      copy-to fg, 0xf/highlight
+    }
+    #
+    var g/esi: (addr grapheme) <- index data, i
+    x, y <- render-grapheme screen, *g, xmin, ymin, xmax, ymax, x, y, fg, background-color
+    i <- decrement
+    loop
+  }
+  return x, y
+}
+
+# helper for small words
+fn render-stack-from-top screen: (addr screen), self: (addr grapheme-stack), x: int, y: int, render-cursor?: boolean -> _/eax: int {
+  var _width/eax: int <- copy 0
+  var _height/ecx: int <- copy 0
+  _width, _height <- screen-size screen
+  var width/edx: int <- copy _width
+  var height/ebx: int <- copy _height
+  var x2/eax: int <- copy 0
+  var y2/ecx: int <- copy 0
+  x2, y2 <- render-stack-from-top-wrapping-right-then-down screen, self, x, y, width, height, x, y, render-cursor?, 3/fg=cyan, 0xc5/bg=blue-bg
+  return x2  # y2? yolo
+}
+
+fn test-render-grapheme-stack {
+  # setup: gs = "abc"
+  var gs-storage: grapheme-stack
+  var gs/edi: (addr grapheme-stack) <- address gs-storage
+  initialize-grapheme-stack gs, 5
+  var g/eax: grapheme <- copy 0x61/a
+  push-grapheme-stack gs, g
+  g <- copy 0x62/b
+  push-grapheme-stack gs, g
+  g <- copy 0x63/c
+  push-grapheme-stack gs, g
+  # setup: screen
+  var screen-on-stack: screen
+  var screen/esi: (addr screen) <- address screen-on-stack
+  initialize-screen screen, 5, 4, 0/no-pixel-graphics
+  #
+  var x/eax: int <- render-stack-from-bottom screen, gs, 0/x, 0/y, 0/no-highlight-matching-open-paren, 0/open-paren-depth
+  check-screen-row screen, 0/y, "abc ", "F - test-render-grapheme-stack from bottom"
+  check-ints-equal x, 3, "F - test-render-grapheme-stack from bottom: result"
+  check-background-color-in-screen-row screen, 3/bg=reverse, 0/y, "   ", "F - test-render-grapheme-stack from bottom: bg"
+  #
+  var x/eax: int <- render-stack-from-top screen, gs, 0/x, 1/y, 0/cursor=false
+  check-screen-row screen, 1/y, "cba ", "F - test-render-grapheme-stack from top without cursor"
+  check-ints-equal x, 3, "F - test-render-grapheme-stack from top without cursor: result"
+  check-background-color-in-screen-row screen, 3/bg=reverse, 1/y, "   ", "F - test-render-grapheme-stack from top without cursor: bg"
+  #
+  var x/eax: int <- render-stack-from-top screen, gs, 0/x, 2/y, 1/cursor=true
+  check-screen-row screen, 2/y, "cba ", "F - test-render-grapheme-stack from top with cursor"
+  check-ints-equal x, 3, "F - test-render-grapheme-stack from top with cursor: result"
+  check-background-color-in-screen-row screen, 3/bg=reverse, 2/y, "|   ", "F - test-render-grapheme-stack from top with cursor: bg"
+}
+
+fn test-render-grapheme-stack-while-highlighting-matching-close-paren {
+  # setup: gs = "(b)"
+  var gs-storage: grapheme-stack
+  var gs/edi: (addr grapheme-stack) <- address gs-storage
+  initialize-grapheme-stack gs, 5
+  var g/eax: grapheme <- copy 0x29/close-paren
+  push-grapheme-stack gs, g
+  g <- copy 0x62/b
+  push-grapheme-stack gs, g
+  g <- copy 0x28/open-paren
+  push-grapheme-stack gs, g
+  # setup: screen
+  var screen-on-stack: screen
+  var screen/esi: (addr screen) <- address screen-on-stack
+  initialize-screen screen, 5, 4, 0/no-pixel-graphics
+  #
+  var x/eax: int <- render-stack-from-top screen, gs, 0/x, 2/y, 1/cursor=true
+  check-screen-row                      screen,               2/y, "(b) ", "F - test-render-grapheme-stack-while-highlighting-matching-close-paren"
+  check-background-color-in-screen-row  screen, 3/bg=reverse,  2/y, "|   ", "F - test-render-grapheme-stack-while-highlighting-matching-close-paren: cursor"
+  check-screen-row-in-color             screen, 0xf/fg=white, 2/y, "  ) ", "F - test-render-grapheme-stack-while-highlighting-matching-close-paren: matching paren"
+}
+
+fn test-render-grapheme-stack-while-highlighting-matching-close-paren-2 {
+  # setup: gs = "(a (b)) c"
+  var gs-storage: grapheme-stack
+  var gs/edi: (addr grapheme-stack) <- address gs-storage
+  initialize-grapheme-stack gs, 0x10
+  var g/eax: grapheme <- copy 0x63/c
+  push-grapheme-stack gs, g
+  g <- copy 0x20/space
+  push-grapheme-stack gs, g
+  g <- copy 0x29/close-paren
+  push-grapheme-stack gs, g
+  g <- copy 0x29/close-paren
+  push-grapheme-stack gs, g
+  g <- copy 0x62/b
+  push-grapheme-stack gs, g
+  g <- copy 0x28/open-paren
+  push-grapheme-stack gs, g
+  g <- copy 0x20/space
+  push-grapheme-stack gs, g
+  g <- copy 0x61/a
+  push-grapheme-stack gs, g
+  g <- copy 0x28/open-paren
+  push-grapheme-stack gs, g
+  # setup: screen
+  var screen-on-stack: screen
+  var screen/esi: (addr screen) <- address screen-on-stack
+  initialize-screen screen, 5, 4, 0/no-pixel-graphics
+  #
+  var x/eax: int <- render-stack-from-top screen, gs, 0/x, 2/y, 1/cursor=true
+  check-screen-row                      screen,               2/y, "(a (b)) c ", "F - test-render-grapheme-stack-while-highlighting-matching-close-paren-2"
+  check-background-color-in-screen-row  screen, 3/bg=reverse,  2/y, "|         ", "F - test-render-grapheme-stack-while-highlighting-matching-close-paren-2: cursor"
+  check-screen-row-in-color             screen, 0xf/fg=white, 2/y, "      )   ", "F - test-render-grapheme-stack-while-highlighting-matching-close-paren-2: matching paren"
+}
+
+fn test-render-grapheme-stack-while-highlighting-matching-open-paren-with-close-paren-at-end {
+  # setup: gs = "(b)"
+  var gs-storage: grapheme-stack
+  var gs/edi: (addr grapheme-stack) <- address gs-storage
+  initialize-grapheme-stack gs, 5
+  var g/eax: grapheme <- copy 0x28/open-paren
+  push-grapheme-stack gs, g
+  g <- copy 0x62/b
+  push-grapheme-stack gs, g
+  g <- copy 0x29/close-paren
+  push-grapheme-stack gs, g
+  # setup: screen
+  var screen-on-stack: screen
+  var screen/esi: (addr screen) <- address screen-on-stack
+  initialize-screen screen, 5, 4, 0/no-pixel-graphics
+  #
+  var x/eax: int <- render-stack-from-bottom screen, gs, 0/x, 2/y, 1/highlight-matching-open-paren, 1/open-paren-depth
+  check-screen-row          screen,               2/y, "(b) ", "F - test-render-grapheme-stack-while-highlighting-matching-open-paren-with-close-paren-at-end"
+  check-screen-row-in-color screen, 0xf/fg=white, 2/y, "(   ", "F - test-render-grapheme-stack-while-highlighting-matching-open-paren-with-close-paren-at-end: matching paren"
+}
+
+fn test-render-grapheme-stack-while-highlighting-matching-open-paren-with-close-paren-at-end-2 {
+  # setup: gs = "a((b))"
+  var gs-storage: grapheme-stack
+  var gs/edi: (addr grapheme-stack) <- address gs-storage
+  initialize-grapheme-stack gs, 0x10
+  var g/eax: grapheme <- copy 0x61/a
+  push-grapheme-stack gs, g
+  g <- copy 0x28/open-paren
+  push-grapheme-stack gs, g
+  g <- copy 0x28/open-paren
+  push-grapheme-stack gs, g
+  g <- copy 0x62/b
+  push-grapheme-stack gs, g
+  g <- copy 0x29/close-paren
+  push-grapheme-stack gs, g
+  g <- copy 0x29/close-paren
+  push-grapheme-stack gs, g
+  # setup: screen
+  var screen-on-stack: screen
+  var screen/esi: (addr screen) <- address screen-on-stack
+  initialize-screen screen, 5, 4, 0/no-pixel-graphics
+  #
+  var x/eax: int <- render-stack-from-bottom screen, gs, 0/x, 2/y, 1/highlight-matching-open-paren, 1/open-paren-depth
+  check-screen-row          screen,               2/y, "a((b)) ", "F - test-render-grapheme-stack-while-highlighting-matching-open-paren-with-close-paren-at-end-2"
+  check-screen-row-in-color screen, 0xf/fg=white, 2/y, " (     ", "F - test-render-grapheme-stack-while-highlighting-matching-open-paren-with-close-paren-at-end-2: matching paren"
+}
+
+fn test-render-grapheme-stack-while-highlighting-matching-open-paren {
+  # setup: gs = "(b"
+  var gs-storage: grapheme-stack
+  var gs/edi: (addr grapheme-stack) <- address gs-storage
+  initialize-grapheme-stack gs, 5
+  var g/eax: grapheme <- copy 0x28/open-paren
+  push-grapheme-stack gs, g
+  g <- copy 0x62/b
+  push-grapheme-stack gs, g
+  # setup: screen
+  var screen-on-stack: screen
+  var screen/esi: (addr screen) <- address screen-on-stack
+  initialize-screen screen, 5, 4, 0/no-pixel-graphics
+  #
+  var x/eax: int <- render-stack-from-bottom screen, gs, 0/x, 2/y, 1/highlight-matching-open-paren, 0/open-paren-depth
+  check-screen-row          screen,               2/y, "(b ", "F - test-render-grapheme-stack-while-highlighting-matching-open-paren"
+  check-screen-row-in-color screen, 0xf/fg=white, 2/y, "(  ", "F - test-render-grapheme-stack-while-highlighting-matching-open-paren: matching paren"
+}
+
+fn test-render-grapheme-stack-while-highlighting-matching-open-paren-2 {
+  # setup: gs = "a((b)"
+  var gs-storage: grapheme-stack
+  var gs/edi: (addr grapheme-stack) <- address gs-storage
+  initialize-grapheme-stack gs, 0x10
+  var g/eax: grapheme <- copy 0x61/a
+  push-grapheme-stack gs, g
+  g <- copy 0x28/open-paren
+  push-grapheme-stack gs, g
+  g <- copy 0x28/open-paren
+  push-grapheme-stack gs, g
+  g <- copy 0x62/b
+  push-grapheme-stack gs, g
+  g <- copy 0x29/close-paren
+  push-grapheme-stack gs, g
+  # setup: screen
+  var screen-on-stack: screen
+  var screen/esi: (addr screen) <- address screen-on-stack
+  initialize-screen screen, 5, 4, 0/no-pixel-graphics
+  #
+  var x/eax: int <- render-stack-from-bottom screen, gs, 0/x, 2/y, 1/highlight-matching-open-paren, 0/open-paren-depth
+  check-screen-row          screen,               2/y, "a((b) ", "F - test-render-grapheme-stack-while-highlighting-matching-open-paren-2"
+  check-screen-row-in-color screen, 0xf/fg=white, 2/y, " (    ", "F - test-render-grapheme-stack-while-highlighting-matching-open-paren-2: matching paren"
+}
+
+# return the index of the matching close-paren of the grapheme at cursor (top of stack)
+# or top index if there's no matching close-paren
+fn get-matching-close-paren-index _self: (addr grapheme-stack), render-cursor?: boolean -> _/edx: int {
+  var self/esi: (addr grapheme-stack) <- copy _self
+  var top-addr/edx: (addr int) <- get self, top
+  # if not rendering cursor, return
+  compare render-cursor?, 0/false
+  {
+    break-if-!=
+    return *top-addr
+  }
+  var data-ah/eax: (addr handle array grapheme) <- get self, data
+  var data/eax: (addr array grapheme) <- lookup *data-ah
+  var i/ecx: int <- copy *top-addr
+  # if stack is empty, return
+  compare i, 0
+  {
+    break-if->
+    return *top-addr
+  }
+  # if cursor is not '(' return
+  i <- decrement
+  var g/esi: (addr grapheme) <- index data, i
+  compare *g, 0x28/open-paren
+  {
+    break-if-=
+    return *top-addr
+  }
+  # otherwise scan to matching paren
+  var paren-count/ebx: int <- copy 1
+  i <- decrement
+  {
+    compare i, 0
+    break-if-<
+    var g/esi: (addr grapheme) <- index data, i
+    compare *g, 0x28/open-paren
+    {
+      break-if-!=
+      paren-count <- increment
+    }
+    compare *g, 0x29/close-paren
+    {
+      break-if-!=
+      compare paren-count, 1
+      {
+        break-if-!=
+        return i
+      }
+      paren-count <- decrement
+    }
+    i <- decrement
+    loop
+  }
+  return *top-addr
+}
+
+# return the index of the first open-paren at the given depth
+# or top index if there's no matching close-paren
+fn get-matching-open-paren-index _self: (addr grapheme-stack), control: boolean, depth: int -> _/edx: int {
+  var self/esi: (addr grapheme-stack) <- copy _self
+  var top-addr/edx: (addr int) <- get self, top
+  # if not rendering cursor, return
+  compare control, 0/false
+  {
+    break-if-!=
+    return *top-addr
+  }
+  var data-ah/eax: (addr handle array grapheme) <- get self, data
+  var data/eax: (addr array grapheme) <- lookup *data-ah
+  var i/ecx: int <- copy *top-addr
+  # if stack is empty, return
+  compare i, 0
+  {
+    break-if->
+    return *top-addr
+  }
+  # scan to matching open paren
+  var paren-count/ebx: int <- copy 0
+  i <- decrement
+  {
+    compare i, 0
+    break-if-<
+    var g/esi: (addr grapheme) <- index data, i
+    compare *g, 0x29/close-paren
+    {
+      break-if-!=
+      paren-count <- increment
+    }
+    compare *g, 0x28/open-paren
+    {
+      break-if-!=
+      compare paren-count, depth
+      {
+        break-if-!=
+        return i
+      }
+      paren-count <- decrement
+    }
+    i <- decrement
+    loop
+  }
+  return *top-addr
+}
+
+# compare from bottom
+# beware: modifies 'stream', which must be disposed of after a false result
+fn prefix-match? _self: (addr grapheme-stack), s: (addr stream byte) -> _/eax: boolean {
+  var self/esi: (addr grapheme-stack) <- copy _self
+  var data-ah/edi: (addr handle array grapheme) <- get self, data
+  var _data/eax: (addr array grapheme) <- lookup *data-ah
+  var data/edi: (addr array grapheme) <- copy _data
+  var top-addr/ecx: (addr int) <- get self, top
+  var i/ebx: int <- copy 0
+  {
+    compare i, *top-addr
+    break-if->=
+    # if curr != expected, return false
+    {
+      var curr-a/edx: (addr grapheme) <- index data, i
+      var expected/eax: grapheme <- read-grapheme s
+      {
+        compare expected, *curr-a
+        break-if-=
+        return 0/false
+      }
+    }
+    i <- increment
+    loop
+  }
+  return 1   # true
+}
+
+# compare from bottom
+# beware: modifies 'stream', which must be disposed of after a false result
+fn suffix-match? _self: (addr grapheme-stack), s: (addr stream byte) -> _/eax: boolean {
+  var self/esi: (addr grapheme-stack) <- copy _self
+  var data-ah/edi: (addr handle array grapheme) <- get self, data
+  var _data/eax: (addr array grapheme) <- lookup *data-ah
+  var data/edi: (addr array grapheme) <- copy _data
+  var top-addr/eax: (addr int) <- get self, top
+  var i/ebx: int <- copy *top-addr
+  i <- decrement
+  {
+    compare i, 0
+    break-if-<
+    {
+      var curr-a/edx: (addr grapheme) <- index data, i
+      var expected/eax: grapheme <- read-grapheme s
+      # if curr != expected, return false
+      {
+        compare expected, *curr-a
+        break-if-=
+        return 0/false
+      }
+    }
+    i <- decrement
+    loop
+  }
+  return 1   # true
+}
+
+fn grapheme-stack-is-decimal-integer? _self: (addr grapheme-stack) -> _/eax: boolean {
+  var self/esi: (addr grapheme-stack) <- copy _self
+  var data-ah/eax: (addr handle array grapheme) <- get self, data
+  var _data/eax: (addr array grapheme) <- lookup *data-ah
+  var data/edx: (addr array grapheme) <- copy _data
+  var top-addr/ecx: (addr int) <- get self, top
+  var i/ebx: int <- copy 0
+  var result/eax: boolean <- copy 1/true
+  $grapheme-stack-is-integer?:loop: {
+    compare i, *top-addr
+    break-if->=
+    var g/edx: (addr grapheme) <- index data, i
+    result <- decimal-digit? *g
+    compare result, 0/false
+    break-if-=
+    i <- increment
+    loop
+  }
+  return result
+}